From e33ba2ac0b45c8a970d815bf7235504dd04fcde7 Mon Sep 17 00:00:00 2001 From: keanemind Date: Wed, 3 Oct 2018 23:55:22 -0500 Subject: [PATCH 0001/3170] Abort when --no-binary/--only-binary receives bad arg --- src/pip/_internal/cli/cmdoptions.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index 3033cd4b5e6..841e9992120 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -403,6 +403,8 @@ def _get_format_control(values, option): def _handle_no_binary(option, opt_str, value, parser): + if value[0] == '-': + raise CommandError("--no-binary requires an argument.") existing = _get_format_control(parser.values, option) FormatControl.handle_mutual_excludes( value, existing.no_binary, existing.only_binary, @@ -410,6 +412,8 @@ def _handle_no_binary(option, opt_str, value, parser): def _handle_only_binary(option, opt_str, value, parser): + if value[0] == '-': + raise CommandError("--only-binary requries an argument.") existing = _get_format_control(parser.values, option) FormatControl.handle_mutual_excludes( value, existing.only_binary, existing.no_binary, From c8fdf9c95e23a4bce7dfbbba43855fd828952463 Mon Sep 17 00:00:00 2001 From: keanemind Date: Thu, 4 Oct 2018 00:01:59 -0500 Subject: [PATCH 0002/3170] Add news --- news/3191.feature | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 news/3191.feature diff --git a/news/3191.feature b/news/3191.feature new file mode 100644 index 00000000000..df743d05d32 --- /dev/null +++ b/news/3191.feature @@ -0,0 +1,2 @@ +Have pip abort if --no-binary or --only-binary receives an argument that starts +with a -. From 419b252f8b3fb00a512857eef6b5b25aec86b884 Mon Sep 17 00:00:00 2001 From: keanemind Date: Thu, 4 Oct 2018 12:54:10 -0500 Subject: [PATCH 0003/3170] Implement suggestions Move check to handle_mutual_excludes and use code formatting in the news file. --- news/3191.feature | 4 ++-- src/pip/_internal/cli/cmdoptions.py | 4 ---- src/pip/_internal/models/format_control.py | 5 +++++ 3 files changed, 7 insertions(+), 6 deletions(-) diff --git a/news/3191.feature b/news/3191.feature index df743d05d32..7dedafd421a 100644 --- a/news/3191.feature +++ b/news/3191.feature @@ -1,2 +1,2 @@ -Have pip abort if --no-binary or --only-binary receives an argument that starts -with a -. +Print a better error message when ``--no-binary`` or ``--only-binary`` is given +an argument starting with ``-``. diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index 841e9992120..3033cd4b5e6 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -403,8 +403,6 @@ def _get_format_control(values, option): def _handle_no_binary(option, opt_str, value, parser): - if value[0] == '-': - raise CommandError("--no-binary requires an argument.") existing = _get_format_control(parser.values, option) FormatControl.handle_mutual_excludes( value, existing.no_binary, existing.only_binary, @@ -412,8 +410,6 @@ def _handle_no_binary(option, opt_str, value, parser): def _handle_only_binary(option, opt_str, value, parser): - if value[0] == '-': - raise CommandError("--only-binary requries an argument.") existing = _get_format_control(parser.values, option) FormatControl.handle_mutual_excludes( value, existing.only_binary, existing.no_binary, diff --git a/src/pip/_internal/models/format_control.py b/src/pip/_internal/models/format_control.py index 27488563669..02559096436 100644 --- a/src/pip/_internal/models/format_control.py +++ b/src/pip/_internal/models/format_control.py @@ -1,3 +1,4 @@ +from pip._internal.exceptions import CommandError from pip._vendor.packaging.utils import canonicalize_name @@ -27,6 +28,10 @@ def __repr__(self): @staticmethod def handle_mutual_excludes(value, target, other): + if value.startswith('-'): + raise CommandError( + "--no-binary / --only-binary option requires 1 argument." + ) new = value.split(',') while ':all:' in new: other.clear() From f0a842e12fbcc7c7858c720a1f111bdc9c9c6354 Mon Sep 17 00:00:00 2001 From: keanemind Date: Fri, 5 Oct 2018 09:23:22 -0500 Subject: [PATCH 0004/3170] Change order of imports --- src/pip/_internal/models/format_control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/models/format_control.py b/src/pip/_internal/models/format_control.py index 02559096436..aeae9c6e744 100644 --- a/src/pip/_internal/models/format_control.py +++ b/src/pip/_internal/models/format_control.py @@ -1,5 +1,5 @@ -from pip._internal.exceptions import CommandError from pip._vendor.packaging.utils import canonicalize_name +from pip._internal.exceptions import CommandError class FormatControl(object): From 8fac4de39a8648e1cbf3227eccfcb7099689128e Mon Sep 17 00:00:00 2001 From: keanemind Date: Fri, 5 Oct 2018 12:42:40 -0500 Subject: [PATCH 0005/3170] Satisfy isort --- src/pip/_internal/models/format_control.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pip/_internal/models/format_control.py b/src/pip/_internal/models/format_control.py index aeae9c6e744..57c6e88c90a 100644 --- a/src/pip/_internal/models/format_control.py +++ b/src/pip/_internal/models/format_control.py @@ -1,4 +1,5 @@ from pip._vendor.packaging.utils import canonicalize_name + from pip._internal.exceptions import CommandError From 5643475d1c4968dae5289853fb5ab11900648795 Mon Sep 17 00:00:00 2001 From: Ani Hayrapetyan Date: Fri, 26 Oct 2018 12:11:11 -0400 Subject: [PATCH 0006/3170] pip autocompletion should silence of KeyboardInterrupts --- src/pip/_internal/commands/completion.py | 4 ++-- tests/functional/test_completion.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/commands/completion.py b/src/pip/_internal/commands/completion.py index 2fcdd393ed6..904e3bf8c62 100644 --- a/src/pip/_internal/commands/completion.py +++ b/src/pip/_internal/commands/completion.py @@ -16,7 +16,7 @@ { COMPREPLY=( $( COMP_WORDS="${COMP_WORDS[*]}" \\ COMP_CWORD=$COMP_CWORD \\ - PIP_AUTO_COMPLETE=1 $1 ) ) + PIP_AUTO_COMPLETE=1 $1 2>/dev/null ) ) } complete -o default -F _pip_completion %(prog)s """, @@ -27,7 +27,7 @@ read -cn cword reply=( $( COMP_WORDS="$words[*]" \\ COMP_CWORD=$(( cword-1 )) \\ - PIP_AUTO_COMPLETE=1 $words[1] ) ) + PIP_AUTO_COMPLETE=1 $words[1] 2>/dev/null )) } compctl -K _pip_completion %(prog)s """, diff --git a/tests/functional/test_completion.py b/tests/functional/test_completion.py index bee64d319f3..13968191c4f 100644 --- a/tests/functional/test_completion.py +++ b/tests/functional/test_completion.py @@ -9,7 +9,7 @@ { COMPREPLY=( $( COMP_WORDS="${COMP_WORDS[*]}" \\ COMP_CWORD=$COMP_CWORD \\ - PIP_AUTO_COMPLETE=1 $1 ) ) + PIP_AUTO_COMPLETE=1 $1 2>/dev/null ) ) } complete -o default -F _pip_completion pip"""), ('fish', """\ @@ -29,7 +29,7 @@ read -cn cword reply=( $( COMP_WORDS="$words[*]" \\ COMP_CWORD=$(( cword-1 )) \\ - PIP_AUTO_COMPLETE=1 $words[1] ) ) + PIP_AUTO_COMPLETE=1 $words[1] 2>/dev/null )) } compctl -K _pip_completion pip"""), ) From 8fb89805f48c379e666f5ef70957dbacf06d17fe Mon Sep 17 00:00:00 2001 From: ahayrapetyan Date: Fri, 26 Oct 2018 17:29:21 +0100 Subject: [PATCH 0007/3170] Create 3942.feature --- news/3942.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/3942.feature diff --git a/news/3942.feature b/news/3942.feature new file mode 100644 index 00000000000..2c845333d13 --- /dev/null +++ b/news/3942.feature @@ -0,0 +1 @@ +This change will silence KeyboardInterrupts for pip autocompletion. From 3095632487d0dc1e874a648fc5f62a3d103628db Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Thu, 8 Nov 2018 12:54:43 +0530 Subject: [PATCH 0008/3170] Update and rename 3942.feature to 3942.bugfix --- news/3942.bugfix | 1 + news/3942.feature | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 news/3942.bugfix delete mode 100644 news/3942.feature diff --git a/news/3942.bugfix b/news/3942.bugfix new file mode 100644 index 00000000000..a15077cc378 --- /dev/null +++ b/news/3942.bugfix @@ -0,0 +1 @@ +pip's CLI completion code no longer prints a Traceback if it is interrupted. diff --git a/news/3942.feature b/news/3942.feature deleted file mode 100644 index 2c845333d13..00000000000 --- a/news/3942.feature +++ /dev/null @@ -1 +0,0 @@ -This change will silence KeyboardInterrupts for pip autocompletion. From 105045b885c5f8ce45fd2a817bc1f88f5191304c Mon Sep 17 00:00:00 2001 From: anatoly techtonik Date: Sun, 11 Nov 2018 22:52:16 +0300 Subject: [PATCH 0009/3170] setup.py: Simplify version extraction This removes `re` dependency --- setup.py | 32 +++++++++++++------------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/setup.py b/setup.py index e2e14b4965a..15e08593d00 100644 --- a/setup.py +++ b/setup.py @@ -1,38 +1,32 @@ import codecs import os -import re import sys from setuptools import find_packages, setup -here = os.path.abspath(os.path.dirname(__file__)) - -def read(*parts): +def codopen(rel_path): + here = os.path.abspath(os.path.dirname(__file__)) # intentionally *not* adding an encoding option to open, See: # https://github.com/pypa/virtualenv/issues/201#issuecomment-3145690 - with codecs.open(os.path.join(here, *parts), 'r') as fp: - return fp.read() - + return codecs.open(os.path.join(here, rel_path), 'r') -def find_version(*file_paths): - version_file = read(*file_paths) - version_match = re.search( - r"^__version__ = ['\"]([^'\"]*)['\"]", - version_file, - re.M, - ) - if version_match: - return version_match.group(1) - raise RuntimeError("Unable to find version string.") +def get_version(rel_path): + for line in codopen(rel_path): + if line.startswith('__version__'): + # __version__ = "0.9" + delim = '\"' if '\"' in line else '\'' + return line.split(delim)[1] + else: + raise RuntimeError("Unable to find version string.") -long_description = read('README.rst') +long_description = codopen('README.rst').read() setup( name="pip", - version=find_version("src", "pip", "__init__.py"), + version=get_version("src/pip/__init__.py"), description="The PyPA recommended tool for installing Python packages.", long_description=long_description, From fd3e6a6e895a39e069fc3e8cb17ff6457b5ea202 Mon Sep 17 00:00:00 2001 From: anatoly techtonik Date: Sun, 11 Nov 2018 22:59:30 +0300 Subject: [PATCH 0010/3170] Create 6004.removal --- news/6004.removal | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/6004.removal diff --git a/news/6004.removal b/news/6004.removal new file mode 100644 index 00000000000..79eb6962c19 --- /dev/null +++ b/news/6004.removal @@ -0,0 +1 @@ +Read version in setup.py without re From 7ee1a554d070ddb0ffb8c44ea86042a8c6fa1b72 Mon Sep 17 00:00:00 2001 From: Hugo Date: Sun, 10 Mar 2019 23:37:41 +0200 Subject: [PATCH 0011/3170] pip-wheel-metadata doesn't need to persist between pip invocations --- .gitignore | 1 - news/6213.bugfix | 1 + src/pip/_internal/req/req_install.py | 31 ++++++++++++++-------------- 3 files changed, 17 insertions(+), 16 deletions(-) create mode 100644 news/6213.bugfix diff --git a/.gitignore b/.gitignore index 276d24d4d3d..6c387e2f0d0 100644 --- a/.gitignore +++ b/.gitignore @@ -27,7 +27,6 @@ nosetests.xml coverage.xml *.cover tests/data/common_wheels/ -pip-wheel-metadata # Misc *~ diff --git a/news/6213.bugfix b/news/6213.bugfix new file mode 100644 index 00000000000..08a2483fc80 --- /dev/null +++ b/news/6213.bugfix @@ -0,0 +1 @@ +The ``pip-wheel-metadata`` directory does not need to persist between invocations of pip, use a temporary directory instead of the current ``setup.py`` directory. diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index ddca56861ae..4ac290c1b42 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -556,23 +556,24 @@ def prepare_pep517_metadata(self): # type: () -> None assert self.pep517_backend is not None - metadata_dir = os.path.join( - self.setup_py_dir, - 'pip-wheel-metadata' - ) - ensure_dir(metadata_dir) - - with self.build_env: - # Note that Pep517HookCaller implements a fallback for - # prepare_metadata_for_build_wheel, so we don't have to - # consider the possibility that this hook doesn't exist. - backend = self.pep517_backend - self.spin_message = "Preparing wheel metadata" - distinfo_dir = backend.prepare_metadata_for_build_wheel( - metadata_dir + with TempDirectory(delete=False) as temp_dir: + metadata_dir = os.path.join( + temp_dir.path, + 'pip-wheel-metadata', ) + ensure_dir(metadata_dir) + + with self.build_env: + # Note that Pep517HookCaller implements a fallback for + # prepare_metadata_for_build_wheel, so we don't have to + # consider the possibility that this hook doesn't exist. + backend = self.pep517_backend + self.spin_message = "Preparing wheel metadata" + distinfo_dir = backend.prepare_metadata_for_build_wheel( + metadata_dir + ) - self.metadata_directory = os.path.join(metadata_dir, distinfo_dir) + self.metadata_directory = os.path.join(metadata_dir, distinfo_dir) def run_egg_info(self): # type: () -> None From 820f8bb87e660122f52895f729f7eb7f16fb194d Mon Sep 17 00:00:00 2001 From: Hugo Date: Mon, 11 Mar 2019 15:03:11 +0200 Subject: [PATCH 0012/3170] Clean up temporary directory at exit --- src/pip/_internal/req/req_install.py | 41 +++++++++++++++++----------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 4ac290c1b42..2e883964ab8 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -1,5 +1,6 @@ from __future__ import absolute_import +import atexit import logging import os import shutil @@ -552,28 +553,36 @@ def prepare_metadata(self): ) self.req = Requirement(metadata_name) + def cleanup(self): + # type: () -> None + if self._temp_dir is not None: + self._temp_dir.cleanup() + def prepare_pep517_metadata(self): # type: () -> None assert self.pep517_backend is not None - with TempDirectory(delete=False) as temp_dir: - metadata_dir = os.path.join( - temp_dir.path, - 'pip-wheel-metadata', - ) - ensure_dir(metadata_dir) + self._temp_dir = TempDirectory(delete=False, kind="req-install") + self._temp_dir.create() + metadata_dir = os.path.join( + self._temp_dir.path, + 'pip-wheel-metadata', + ) + atexit.register(self.cleanup) - with self.build_env: - # Note that Pep517HookCaller implements a fallback for - # prepare_metadata_for_build_wheel, so we don't have to - # consider the possibility that this hook doesn't exist. - backend = self.pep517_backend - self.spin_message = "Preparing wheel metadata" - distinfo_dir = backend.prepare_metadata_for_build_wheel( - metadata_dir - ) + ensure_dir(metadata_dir) + + with self.build_env: + # Note that Pep517HookCaller implements a fallback for + # prepare_metadata_for_build_wheel, so we don't have to + # consider the possibility that this hook doesn't exist. + backend = self.pep517_backend + self.spin_message = "Preparing wheel metadata" + distinfo_dir = backend.prepare_metadata_for_build_wheel( + metadata_dir + ) - self.metadata_directory = os.path.join(metadata_dir, distinfo_dir) + self.metadata_directory = os.path.join(metadata_dir, distinfo_dir) def run_egg_info(self): # type: () -> None From 1bcc718804ebb3eef40c9f2feee3666981cd5711 Mon Sep 17 00:00:00 2001 From: Hugo Date: Thu, 14 Mar 2019 11:10:57 +0200 Subject: [PATCH 0013/3170] Add note: This needs to be refactored to stop using atexit --- src/pip/_internal/req/req_install.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 2e883964ab8..4333d693625 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -562,6 +562,7 @@ def prepare_pep517_metadata(self): # type: () -> None assert self.pep517_backend is not None + # NOTE: This needs to be refactored to stop using atexit self._temp_dir = TempDirectory(delete=False, kind="req-install") self._temp_dir.create() metadata_dir = os.path.join( From d62bef43623eeada2eef810c824d95b4797e3c70 Mon Sep 17 00:00:00 2001 From: Mike Date: Fri, 17 May 2019 11:06:59 -0700 Subject: [PATCH 0014/3170] Document caveats for UNC paths in uninstall and add .pth unit tests --- news/6516.doc | 1 + src/pip/_internal/req/req_uninstall.py | 5 ++++ tests/unit/test_req_uninstall.py | 35 +++++++++++++++++++++++++- 3 files changed, 40 insertions(+), 1 deletion(-) create mode 100644 news/6516.doc diff --git a/news/6516.doc b/news/6516.doc new file mode 100644 index 00000000000..e8b880ef4b4 --- /dev/null +++ b/news/6516.doc @@ -0,0 +1 @@ +Document caveats for UNC paths in uninstall and add .pth unit tests. diff --git a/src/pip/_internal/req/req_uninstall.py b/src/pip/_internal/req/req_uninstall.py index 733301cee6c..359fb7cded7 100644 --- a/src/pip/_internal/req/req_uninstall.py +++ b/src/pip/_internal/req/req_uninstall.py @@ -593,6 +593,11 @@ def add(self, entry): # backslashes. This is correct for entries that describe absolute # paths outside of site-packages, but all the others use forward # slashes. + # os.path.splitdrive is used instead of os.path.isabs because isabs + # treats non-absolute paths with drive letter markings like c:foo\bar + # as absolute paths. It also does not recognize UNC paths if they don't + # have more than "\\sever\share". Valid examples: "\\server\share\" or + # "\\server\share\folder". Python 2.7.8+ support UNC in splitdrive. if WINDOWS and not os.path.splitdrive(entry)[0]: entry = entry.replace('\\', '/') self.entries.add(entry) diff --git a/tests/unit/test_req_uninstall.py b/tests/unit/test_req_uninstall.py index 37edb7dfa42..99b315b1550 100644 --- a/tests/unit/test_req_uninstall.py +++ b/tests/unit/test_req_uninstall.py @@ -1,11 +1,12 @@ import os +import sys import pytest from mock import Mock import pip._internal.req.req_uninstall from pip._internal.req.req_uninstall import ( - StashedUninstallPathSet, UninstallPathSet, compact, + StashedUninstallPathSet, UninstallPathSet, UninstallPthEntries, compact, compress_for_output_listing, compress_for_rename, uninstallation_paths, ) from tests.lib import create_file @@ -129,6 +130,38 @@ def test_add(self, tmpdir, monkeypatch): ups.add(file_nonexistent) assert ups.paths == {file_extant} + def test_add_pth(self, tmpdir, monkeypatch): + monkeypatch.setattr(pip._internal.req.req_uninstall, 'is_local', + mock_is_local) + # Fix case for windows tests + tmpdir = os.path.normcase(tmpdir) + on_windows = sys.platform == 'win32' + pth_file = os.path.join(tmpdir, 'foo.pth') + relative = '../../example' + if on_windows: + share = '\\\\example\\share\\' + share_com = '\\\\example.com\\share\\' + # Create a .pth file for testing + with open(pth_file, 'w') as f: + f.writelines([tmpdir, '\n', + relative, '\n']) + if on_windows: + f.writelines([share, '\n', + share_com, '\n']) + # Add paths to be removed + pth = UninstallPthEntries(pth_file) + pth.add(tmpdir) + pth.add(relative) + if on_windows: + pth.add(share) + pth.add(share_com) + # Check that the paths were added to entries + if on_windows: + check = set([tmpdir, relative, share, share_com]) + else: + check = set([tmpdir, relative]) + assert pth.entries == check + @pytest.mark.skipif("sys.platform == 'win32'") def test_add_symlink(self, tmpdir, monkeypatch): monkeypatch.setattr(pip._internal.req.req_uninstall, 'is_local', From 9ab2b415dbc26bc07716fc37cd2da9c4d624a4ad Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Fri, 30 Nov 2018 07:07:44 +0530 Subject: [PATCH 0015/3170] Drop a useless import in favor of explicitness --- src/pip/_internal/req/req_install.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 65313675e8b..f5c93504e4b 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -37,7 +37,6 @@ from pip._internal.utils.ui import open_spinner from pip._internal.utils.virtualenv import running_under_virtualenv from pip._internal.vcs import vcs -from pip._internal.wheel import move_wheel_files if MYPY_CHECK_RUNNING: from typing import ( @@ -451,7 +450,7 @@ def move_wheel_files( pycompile=True # type: bool ): # type: (...) -> None - move_wheel_files( + wheel.move_wheel_files( self.name, self.req, wheeldir, user=use_user_site, home=home, From 537b0fca120668c4e76abb2a38858eaa96498a5b Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Mon, 22 Jul 2019 10:15:27 +0530 Subject: [PATCH 0016/3170] Change isort multi_line_output to 3 --- setup.cfg | 2 +- src/pip/_internal/cli/base_command.py | 16 ++++++++--- src/pip/_internal/cli/main_parser.py | 7 +++-- src/pip/_internal/commands/check.py | 3 +- src/pip/_internal/commands/configuration.py | 4 ++- src/pip/_internal/commands/install.py | 7 +++-- src/pip/_internal/commands/list.py | 3 +- src/pip/_internal/configuration.py | 3 +- src/pip/_internal/download.py | 20 ++++++++++--- src/pip/_internal/index.py | 9 ++++-- src/pip/_internal/legacy_resolve.py | 12 ++++++-- src/pip/_internal/models/link.py | 7 +++-- src/pip/_internal/operations/freeze.py | 6 ++-- src/pip/_internal/operations/prepare.py | 13 +++++++-- src/pip/_internal/req/req_file.py | 3 +- src/pip/_internal/req/req_install.py | 14 +++++++-- src/pip/_internal/req/req_uninstall.py | 11 +++++-- src/pip/_internal/utils/hashes.py | 4 ++- src/pip/_internal/utils/misc.py | 9 ++++-- src/pip/_internal/vcs/git.py | 4 ++- src/pip/_internal/vcs/subversion.py | 4 ++- src/pip/_internal/vcs/versioncontrol.py | 6 +++- src/pip/_internal/wheel.py | 13 +++++++-- tests/functional/test_freeze.py | 8 ++++-- tests/functional/test_install.py | 13 +++++++-- tests/functional/test_install_reqs.py | 4 ++- tests/functional/test_install_vcs_git.py | 7 +++-- tests/functional/test_search.py | 5 +++- tests/functional/test_vcs_bazaar.py | 6 +++- tests/unit/test_cmdoptions.py | 3 +- tests/unit/test_compat.py | 6 +++- tests/unit/test_download.py | 13 +++++++-- tests/unit/test_finder.py | 8 ++++-- tests/unit/test_index.py | 18 +++++++++--- tests/unit/test_index_html_page.py | 6 +++- tests/unit/test_legacy_resolve.py | 3 +- tests/unit/test_logging.py | 4 ++- tests/unit/test_req.py | 14 +++++++-- tests/unit/test_req_file.py | 15 +++++++--- tests/unit/test_req_install.py | 3 +- tests/unit/test_req_uninstall.py | 8 ++++-- tests/unit/test_utils.py | 32 ++++++++++++++++----- 42 files changed, 271 insertions(+), 85 deletions(-) diff --git a/setup.cfg b/setup.cfg index c6712bcd7ca..cb6d790a961 100644 --- a/setup.cfg +++ b/setup.cfg @@ -4,7 +4,7 @@ skip = .scratch, _vendor, data -multi_line_output = 5 +multi_line_output = 3 known_third_party = pip._vendor known_first_party = diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index 90830be4a56..177524804fb 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -12,22 +12,30 @@ from pip._internal.cli import cmdoptions from pip._internal.cli.cmdoptions import make_search_scope from pip._internal.cli.parser import ( - ConfigOptionParser, UpdatingDefaultsHelpFormatter, + ConfigOptionParser, + UpdatingDefaultsHelpFormatter, ) from pip._internal.cli.status_codes import ( - ERROR, PREVIOUS_BUILD_DIR_ERROR, SUCCESS, UNKNOWN_ERROR, + ERROR, + PREVIOUS_BUILD_DIR_ERROR, + SUCCESS, + UNKNOWN_ERROR, VIRTUALENV_NOT_FOUND, ) from pip._internal.download import PipSession from pip._internal.exceptions import ( - BadCommand, CommandError, InstallationError, PreviousBuildDirError, + BadCommand, + CommandError, + InstallationError, + PreviousBuildDirError, UninstallationError, ) from pip._internal.index import PackageFinder from pip._internal.models.selection_prefs import SelectionPreferences from pip._internal.models.target_python import TargetPython from pip._internal.req.constructors import ( - install_req_from_editable, install_req_from_line, + install_req_from_editable, + install_req_from_line, ) from pip._internal.req.req_file import parse_requirements from pip._internal.utils.deprecation import deprecated diff --git a/src/pip/_internal/cli/main_parser.py b/src/pip/_internal/cli/main_parser.py index 6d0b719aa42..1c371ca3b56 100644 --- a/src/pip/_internal/cli/main_parser.py +++ b/src/pip/_internal/cli/main_parser.py @@ -6,10 +6,13 @@ from pip._internal.cli import cmdoptions from pip._internal.cli.parser import ( - ConfigOptionParser, UpdatingDefaultsHelpFormatter, + ConfigOptionParser, + UpdatingDefaultsHelpFormatter, ) from pip._internal.commands import ( - commands_dict, get_similar_commands, get_summaries, + commands_dict, + get_similar_commands, + get_summaries, ) from pip._internal.exceptions import CommandError from pip._internal.utils.misc import get_pip_version, get_prog diff --git a/src/pip/_internal/commands/check.py b/src/pip/_internal/commands/check.py index 801cecc0b84..865285b5065 100644 --- a/src/pip/_internal/commands/check.py +++ b/src/pip/_internal/commands/check.py @@ -2,7 +2,8 @@ from pip._internal.cli.base_command import Command from pip._internal.operations.check import ( - check_package_set, create_package_set_from_installed, + check_package_set, + create_package_set_from_installed, ) logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/commands/configuration.py b/src/pip/_internal/commands/configuration.py index 1ec77d2a654..91f916e85ac 100644 --- a/src/pip/_internal/commands/configuration.py +++ b/src/pip/_internal/commands/configuration.py @@ -5,7 +5,9 @@ from pip._internal.cli.base_command import Command from pip._internal.cli.status_codes import ERROR, SUCCESS from pip._internal.configuration import ( - Configuration, get_configuration_files, kinds, + Configuration, + get_configuration_files, + kinds, ) from pip._internal.exceptions import PipError from pip._internal.utils.deprecation import deprecated diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index ebeceacffad..927e7817be2 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -15,7 +15,9 @@ from pip._internal.cli.cmdoptions import make_target_python from pip._internal.cli.status_codes import ERROR from pip._internal.exceptions import ( - CommandError, InstallationError, PreviousBuildDirError, + CommandError, + InstallationError, + PreviousBuildDirError, ) from pip._internal.legacy_resolve import Resolver from pip._internal.locations import distutils_scheme @@ -25,7 +27,8 @@ from pip._internal.req.req_tracker import RequirementTracker from pip._internal.utils.filesystem import check_path_owner from pip._internal.utils.misc import ( - ensure_dir, get_installed_version, + ensure_dir, + get_installed_version, protect_pip_from_modification_on_windows, ) from pip._internal.utils.temp_dir import TempDirectory diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index cf71b13ebd6..ea7f5eb9c44 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -13,7 +13,8 @@ from pip._internal.index import PackageFinder from pip._internal.models.selection_prefs import SelectionPreferences from pip._internal.utils.misc import ( - dist_is_editable, get_installed_distributions, + dist_is_editable, + get_installed_distributions, ) from pip._internal.utils.packaging import get_installer diff --git a/src/pip/_internal/configuration.py b/src/pip/_internal/configuration.py index 437e92eeb3a..10edb4829ab 100644 --- a/src/pip/_internal/configuration.py +++ b/src/pip/_internal/configuration.py @@ -19,7 +19,8 @@ from pip._vendor.six.moves import configparser from pip._internal.exceptions import ( - ConfigurationError, ConfigurationFileCouldNotBeLoaded, + ConfigurationError, + ConfigurationFileCouldNotBeLoaded, ) from pip._internal.utils import appdirs from pip._internal.utils.compat import WINDOWS, expanduser diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index 8715eb5b19d..1578521c765 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -36,10 +36,22 @@ from pip._internal.utils.glibc import libc_ver from pip._internal.utils.marker_files import write_delete_marker_file from pip._internal.utils.misc import ( - ARCHIVE_EXTENSIONS, ask, ask_input, ask_password, ask_path_exists, - backup_dir, consume, display_path, format_size, get_installed_version, - path_to_url, remove_auth_from_url, rmtree, split_auth_netloc_from_url, - splitext, unpack_file, + ARCHIVE_EXTENSIONS, + ask, + ask_input, + ask_password, + ask_path_exists, + backup_dir, + consume, + display_path, + format_size, + get_installed_version, + path_to_url, + remove_auth_from_url, + rmtree, + split_auth_netloc_from_url, + splitext, + unpack_file, ) from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING diff --git a/src/pip/_internal/index.py b/src/pip/_internal/index.py index a1aaad59c88..75fd4e005ff 100644 --- a/src/pip/_internal/index.py +++ b/src/pip/_internal/index.py @@ -19,7 +19,9 @@ from pip._internal.download import is_url, url_to_path from pip._internal.exceptions import ( - BestVersionAlreadyInstalled, DistributionNotFound, InvalidWheelFilename, + BestVersionAlreadyInstalled, + DistributionNotFound, + InvalidWheelFilename, UnsupportedWheel, ) from pip._internal.models.candidate import InstallationCandidate @@ -30,7 +32,10 @@ from pip._internal.utils.compat import ipaddress from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import ( - ARCHIVE_EXTENSIONS, SUPPORTED_EXTENSIONS, WHEEL_EXTENSION, path_to_url, + ARCHIVE_EXTENSIONS, + SUPPORTED_EXTENSIONS, + WHEEL_EXTENSION, + path_to_url, redact_password_from_url, ) from pip._internal.utils.packaging import check_requires_python diff --git a/src/pip/_internal/legacy_resolve.py b/src/pip/_internal/legacy_resolve.py index 1d9229cb621..2f629b39d11 100644 --- a/src/pip/_internal/legacy_resolve.py +++ b/src/pip/_internal/legacy_resolve.py @@ -18,16 +18,22 @@ from pip._vendor.packaging import specifiers from pip._internal.exceptions import ( - BestVersionAlreadyInstalled, DistributionNotFound, HashError, HashErrors, + BestVersionAlreadyInstalled, + DistributionNotFound, + HashError, + HashErrors, UnsupportedPythonVersion, ) from pip._internal.req.constructors import install_req_from_req_string from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import ( - dist_in_usersite, ensure_dir, normalize_version_info, + dist_in_usersite, + ensure_dir, + normalize_version_info, ) from pip._internal.utils.packaging import ( - check_requires_python, get_requires_python, + check_requires_python, + get_requires_python, ) from pip._internal.utils.typing import MYPY_CHECK_RUNNING diff --git a/src/pip/_internal/models/link.py b/src/pip/_internal/models/link.py index 01983e7079c..4f30ab6b06c 100644 --- a/src/pip/_internal/models/link.py +++ b/src/pip/_internal/models/link.py @@ -4,8 +4,11 @@ from pip._vendor.six.moves.urllib import parse as urllib_parse from pip._internal.utils.misc import ( - WHEEL_EXTENSION, path_to_url, redact_password_from_url, - split_auth_from_netloc, splitext, + WHEEL_EXTENSION, + path_to_url, + redact_password_from_url, + split_auth_from_netloc, + splitext, ) from pip._internal.utils.models import KeyBasedCompareMixin from pip._internal.utils.typing import MYPY_CHECK_RUNNING diff --git a/src/pip/_internal/operations/freeze.py b/src/pip/_internal/operations/freeze.py index 6f5a3dd976f..b4193730a03 100644 --- a/src/pip/_internal/operations/freeze.py +++ b/src/pip/_internal/operations/freeze.py @@ -11,11 +11,13 @@ from pip._internal.exceptions import BadCommand, InstallationError from pip._internal.req.constructors import ( - install_req_from_editable, install_req_from_line, + install_req_from_editable, + install_req_from_line, ) from pip._internal.req.req_file import COMMENT_RE from pip._internal.utils.misc import ( - dist_is_editable, get_installed_distributions, + dist_is_editable, + get_installed_distributions, ) from pip._internal.utils.typing import MYPY_CHECK_RUNNING diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 6cf5f0edd68..471a60ce828 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -11,11 +11,18 @@ ) from pip._internal.distributions.installed import InstalledDistribution from pip._internal.download import ( - is_dir_url, is_file_url, is_vcs_url, unpack_url, url_to_path, + is_dir_url, + is_file_url, + is_vcs_url, + unpack_url, + url_to_path, ) from pip._internal.exceptions import ( - DirectoryUrlHashUnsupported, HashUnpinned, InstallationError, - PreviousBuildDirError, VcsHashUnsupported, + DirectoryUrlHashUnsupported, + HashUnpinned, + InstallationError, + PreviousBuildDirError, + VcsHashUnsupported, ) from pip._internal.utils.compat import expanduser from pip._internal.utils.hashes import MissingHashes diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index 5a9920fe963..3e39f9603ed 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -18,7 +18,8 @@ from pip._internal.exceptions import RequirementsFileParseError from pip._internal.models.search_scope import SearchScope from pip._internal.req.constructors import ( - install_req_from_editable, install_req_from_line, + install_req_from_editable, + install_req_from_line, ) from pip._internal.utils.typing import MYPY_CHECK_RUNNING diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index f5c93504e4b..3cbe722f5fe 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -26,9 +26,17 @@ from pip._internal.utils.logging import indent_log from pip._internal.utils.marker_files import PIP_DELETE_MARKER_FILENAME from pip._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, redact_password_from_url, rmtree, + _make_build_dir, + ask_path_exists, + backup_dir, + call_subprocess, + display_path, + dist_in_site_packages, + dist_in_usersite, + ensure_dir, + get_installed_version, + redact_password_from_url, + rmtree, ) from pip._internal.utils.packaging import get_metadata from pip._internal.utils.setuptools_build import make_setuptools_shim_args diff --git a/src/pip/_internal/req/req_uninstall.py b/src/pip/_internal/req/req_uninstall.py index 733301cee6c..466654584b7 100644 --- a/src/pip/_internal/req/req_uninstall.py +++ b/src/pip/_internal/req/req_uninstall.py @@ -14,8 +14,15 @@ from pip._internal.utils.compat import WINDOWS, cache_from_source, uses_pycache from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import ( - FakeFile, ask, dist_in_usersite, dist_is_local, egg_link_path, is_local, - normalize_path, renames, rmtree, + FakeFile, + ask, + dist_in_usersite, + dist_is_local, + egg_link_path, + is_local, + normalize_path, + renames, + rmtree, ) from pip._internal.utils.temp_dir import AdjacentTempDirectory, TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING diff --git a/src/pip/_internal/utils/hashes.py b/src/pip/_internal/utils/hashes.py index e8aabe1a56e..4f075b0918c 100644 --- a/src/pip/_internal/utils/hashes.py +++ b/src/pip/_internal/utils/hashes.py @@ -5,7 +5,9 @@ from pip._vendor.six import iteritems, iterkeys, itervalues from pip._internal.exceptions import ( - HashMismatch, HashMissing, InstallationError, + HashMismatch, + HashMissing, + InstallationError, ) from pip._internal.utils.misc import read_chunks from pip._internal.utils.typing import MYPY_CHECK_RUNNING diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index abb95979abc..f094cac990d 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -32,12 +32,17 @@ from pip._internal.exceptions import CommandError, InstallationError from pip._internal.locations import site_packages, user_site from pip._internal.utils.compat import ( - WINDOWS, console_to_str, expanduser, stdlib_pkgs, str_to_display, + WINDOWS, + console_to_str, + expanduser, + stdlib_pkgs, + str_to_display, ) from pip._internal.utils.marker_files import write_delete_marker_file from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.virtualenv import ( - running_under_virtualenv, virtualenv_no_global, + running_under_virtualenv, + virtualenv_no_global, ) if PY2: diff --git a/src/pip/_internal/vcs/git.py b/src/pip/_internal/vcs/git.py index 3445c1b3a73..d8617ba88f2 100644 --- a/src/pip/_internal/vcs/git.py +++ b/src/pip/_internal/vcs/git.py @@ -13,7 +13,9 @@ from pip._internal.utils.misc import display_path, redact_password_from_url from pip._internal.utils.temp_dir import TempDirectory from pip._internal.vcs.versioncontrol import ( - RemoteNotFoundError, VersionControl, vcs, + RemoteNotFoundError, + VersionControl, + vcs, ) urlsplit = urllib_parse.urlsplit diff --git a/src/pip/_internal/vcs/subversion.py b/src/pip/_internal/vcs/subversion.py index 6bb4c8c5ccf..50c10ef3938 100644 --- a/src/pip/_internal/vcs/subversion.py +++ b/src/pip/_internal/vcs/subversion.py @@ -7,7 +7,9 @@ from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import ( - display_path, rmtree, split_auth_from_netloc, + display_path, + rmtree, + split_auth_from_netloc, ) from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.vcs.versioncontrol import VersionControl, vcs diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index 2d05fc13327..2bb868f1caf 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -12,7 +12,11 @@ from pip._internal.exceptions import BadCommand from pip._internal.utils.misc import ( - ask_path_exists, backup_dir, call_subprocess, display_path, rmtree, + ask_path_exists, + backup_dir, + call_subprocess, + display_path, + rmtree, ) from pip._internal.utils.typing import MYPY_CHECK_RUNNING diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index 6f034cd0e0b..4b391c0092f 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -25,15 +25,22 @@ from pip._internal import pep425tags from pip._internal.download import unpack_url from pip._internal.exceptions import ( - InstallationError, InvalidWheelFilename, UnsupportedWheel, + InstallationError, + InvalidWheelFilename, + UnsupportedWheel, ) from pip._internal.locations import distutils_scheme from pip._internal.models.link import Link from pip._internal.utils.logging import indent_log from pip._internal.utils.marker_files import PIP_DELETE_MARKER_FILENAME from pip._internal.utils.misc import ( - LOG_DIVIDER, call_subprocess, captured_stdout, ensure_dir, - format_command_args, path_to_url, read_chunks, + LOG_DIVIDER, + call_subprocess, + captured_stdout, + ensure_dir, + format_command_args, + path_to_url, + read_chunks, ) from pip._internal.utils.setuptools_build import make_setuptools_shim_args from pip._internal.utils.temp_dir import TempDirectory diff --git a/tests/functional/test_freeze.py b/tests/functional/test_freeze.py index 94a2cf4390c..77f83796abc 100644 --- a/tests/functional/test_freeze.py +++ b/tests/functional/test_freeze.py @@ -7,8 +7,12 @@ import pytest from tests.lib import ( - _create_test_package, _create_test_package_with_srcdir, _git_commit, - need_bzr, need_mercurial, path_to_url, + _create_test_package, + _create_test_package_with_srcdir, + _git_commit, + need_bzr, + need_mercurial, + path_to_url, ) distribute_re = re.compile('^distribute==[0-9.]+\n', re.MULTILINE) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 96b780f3cc3..f38de0561df 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -12,9 +12,16 @@ from pip._internal.models.index import PyPI, TestPyPI from pip._internal.utils.misc import rmtree from tests.lib import ( - _create_svn_repo, _create_test_package, create_basic_wheel_for_package, - create_test_package_with_setup, need_bzr, need_mercurial, path_to_url, - pyversion, pyversion_tuple, requirements_file, + _create_svn_repo, + _create_test_package, + create_basic_wheel_for_package, + create_test_package_with_setup, + need_bzr, + need_mercurial, + path_to_url, + pyversion, + pyversion_tuple, + requirements_file, ) from tests.lib.local_repos import local_checkout from tests.lib.path import Path diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index e659056a52a..14368833186 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -4,7 +4,9 @@ import pytest from tests.lib import ( - _create_test_package_with_subdirectory, path_to_url, pyversion, + _create_test_package_with_subdirectory, + path_to_url, + pyversion, requirements_file, ) from tests.lib.local_repos import local_checkout diff --git a/tests/functional/test_install_vcs_git.py b/tests/functional/test_install_vcs_git.py index 62b5ac78b76..11b63a84488 100644 --- a/tests/functional/test_install_vcs_git.py +++ b/tests/functional/test_install_vcs_git.py @@ -1,11 +1,14 @@ import pytest from tests.lib import ( - _change_test_package_version, _create_test_package, _test_path_to_file_url, + _change_test_package_version, + _create_test_package, + _test_path_to_file_url, pyversion, ) from tests.lib.git_submodule_helpers import ( - _change_test_package_submodule, _create_test_package_with_submodule, + _change_test_package_submodule, + _create_test_package_with_submodule, _pull_in_submodule_changes_to_module, ) from tests.lib.local_repos import local_checkout diff --git a/tests/functional/test_search.py b/tests/functional/test_search.py index 4c5fd5d0ce3..ea438a8bb09 100644 --- a/tests/functional/test_search.py +++ b/tests/functional/test_search.py @@ -5,7 +5,10 @@ from pip._internal.cli.status_codes import NO_MATCHES_FOUND, SUCCESS from pip._internal.commands.search import ( - SearchCommand, highest_version, print_results, transform_hits, + SearchCommand, + highest_version, + print_results, + transform_hits, ) from tests.lib import pyversion diff --git a/tests/functional/test_vcs_bazaar.py b/tests/functional/test_vcs_bazaar.py index 7417854419a..6bd61611ac1 100644 --- a/tests/functional/test_vcs_bazaar.py +++ b/tests/functional/test_vcs_bazaar.py @@ -8,7 +8,11 @@ from pip._internal.vcs.bazaar import Bazaar from tests.lib import ( - _test_path_to_file_url, _vcs_add, create_file, is_bzr_installed, need_bzr, + _test_path_to_file_url, + _vcs_add, + create_file, + is_bzr_installed, + need_bzr, ) diff --git a/tests/unit/test_cmdoptions.py b/tests/unit/test_cmdoptions.py index 2c72244fdd7..8deb66a5f30 100644 --- a/tests/unit/test_cmdoptions.py +++ b/tests/unit/test_cmdoptions.py @@ -2,7 +2,8 @@ import pytest from pip._internal.cli.cmdoptions import ( - _convert_python_version, make_search_scope, + _convert_python_version, + make_search_scope, ) diff --git a/tests/unit/test_compat.py b/tests/unit/test_compat.py index 6f5fe5b51bd..cf273da8697 100644 --- a/tests/unit/test_compat.py +++ b/tests/unit/test_compat.py @@ -7,7 +7,11 @@ import pip._internal.utils.compat as pip_compat from pip._internal.utils.compat import ( - console_to_str, expanduser, get_path_uid, native_str, str_to_display, + console_to_str, + expanduser, + get_path_uid, + native_str, + str_to_display, ) diff --git a/tests/unit/test_download.py b/tests/unit/test_download.py index 7b421a7d7d5..d63d7420178 100644 --- a/tests/unit/test_download.py +++ b/tests/unit/test_download.py @@ -11,9 +11,16 @@ import pip from pip._internal.download import ( - CI_ENVIRONMENT_VARIABLES, MultiDomainBasicAuth, PipSession, SafeFileCache, - _download_http_url, parse_content_disposition, sanitize_content_filename, - unpack_file_url, unpack_http_url, url_to_path, + CI_ENVIRONMENT_VARIABLES, + MultiDomainBasicAuth, + PipSession, + SafeFileCache, + _download_http_url, + parse_content_disposition, + sanitize_content_filename, + unpack_file_url, + unpack_http_url, + url_to_path, ) from pip._internal.exceptions import HashMismatch from pip._internal.models.link import Link diff --git a/tests/unit/test_finder.py b/tests/unit/test_finder.py index 0e0c41081d1..f3d6366bc4d 100644 --- a/tests/unit/test_finder.py +++ b/tests/unit/test_finder.py @@ -9,10 +9,14 @@ import pip._internal.pep425tags import pip._internal.wheel from pip._internal.exceptions import ( - BestVersionAlreadyInstalled, DistributionNotFound, + BestVersionAlreadyInstalled, + DistributionNotFound, ) from pip._internal.index import ( - CandidateEvaluator, InstallationCandidate, Link, LinkEvaluator, + CandidateEvaluator, + InstallationCandidate, + Link, + LinkEvaluator, ) from pip._internal.models.target_python import TargetPython from pip._internal.req.constructors import install_req_from_line diff --git a/tests/unit/test_index.py b/tests/unit/test_index.py index e3b36d6dcdd..4d7b5933117 100644 --- a/tests/unit/test_index.py +++ b/tests/unit/test_index.py @@ -8,10 +8,20 @@ from pip._internal.download import PipSession from pip._internal.index import ( - CandidateEvaluator, CandidatePreferences, FormatControl, HTMLPage, Link, - LinkEvaluator, PackageFinder, _check_link_requires_python, _clean_link, - _determine_base_url, _extract_version_from_fragment, - _find_name_version_sep, _get_html_page, filter_unallowed_hashes, + CandidateEvaluator, + CandidatePreferences, + FormatControl, + HTMLPage, + Link, + LinkEvaluator, + PackageFinder, + _check_link_requires_python, + _clean_link, + _determine_base_url, + _extract_version_from_fragment, + _find_name_version_sep, + _get_html_page, + filter_unallowed_hashes, ) from pip._internal.models.candidate import InstallationCandidate from pip._internal.models.search_scope import SearchScope diff --git a/tests/unit/test_index_html_page.py b/tests/unit/test_index_html_page.py index f0d7a817fb6..ec2a3950e7a 100644 --- a/tests/unit/test_index_html_page.py +++ b/tests/unit/test_index_html_page.py @@ -6,7 +6,11 @@ from pip._internal.download import PipSession from pip._internal.index import ( - Link, _get_html_page, _get_html_response, _NotHTML, _NotHTTP, + Link, + _get_html_page, + _get_html_response, + _NotHTML, + _NotHTTP, ) diff --git a/tests/unit/test_legacy_resolve.py b/tests/unit/test_legacy_resolve.py index 4d7f27b20d0..e0edccc837b 100644 --- a/tests/unit/test_legacy_resolve.py +++ b/tests/unit/test_legacy_resolve.py @@ -4,7 +4,8 @@ from pip._vendor import pkg_resources from pip._internal.exceptions import ( - NoneMetadataError, UnsupportedPythonVersion, + NoneMetadataError, + UnsupportedPythonVersion, ) from pip._internal.legacy_resolve import _check_dist_requires_python from pip._internal.utils.packaging import get_requires_python diff --git a/tests/unit/test_logging.py b/tests/unit/test_logging.py index ddff2b38d00..a2bab3ea9c5 100644 --- a/tests/unit/test_logging.py +++ b/tests/unit/test_logging.py @@ -8,7 +8,9 @@ from pip._vendor.six import PY2 from pip._internal.utils.logging import ( - BrokenStdoutLoggingError, ColorizedStreamHandler, IndentingFormatter, + BrokenStdoutLoggingError, + ColorizedStreamHandler, + IndentingFormatter, ) from pip._internal.utils.misc import captured_stderr, captured_stdout diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index 23a70f77890..2b1428686e1 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -12,19 +12,27 @@ from pip._internal.commands.install import InstallCommand from pip._internal.download import PipSession from pip._internal.exceptions import ( - HashErrors, InstallationError, InvalidWheelFilename, PreviousBuildDirError, + HashErrors, + InstallationError, + InvalidWheelFilename, + PreviousBuildDirError, ) from pip._internal.legacy_resolve import Resolver from pip._internal.operations.prepare import RequirementPreparer from pip._internal.req import InstallRequirement, RequirementSet from pip._internal.req.constructors import ( - install_req_from_editable, install_req_from_line, parse_editable, + install_req_from_editable, + install_req_from_line, + parse_editable, ) from pip._internal.req.req_file import process_line from pip._internal.req.req_tracker import RequirementTracker from pip._internal.utils.misc import path_to_url from tests.lib import ( - DATA_DIR, assert_raises_regexp, make_test_finder, requirements_file, + DATA_DIR, + assert_raises_regexp, + make_test_finder, + requirements_file, ) diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py index ade7b4160dc..3ebb55d3cfc 100644 --- a/tests/unit/test_req_file.py +++ b/tests/unit/test_req_file.py @@ -10,15 +10,22 @@ import pip._internal.index from pip._internal.download import PipSession from pip._internal.exceptions import ( - InstallationError, RequirementsFileParseError, + InstallationError, + RequirementsFileParseError, ) from pip._internal.models.format_control import FormatControl from pip._internal.req.constructors import ( - install_req_from_editable, install_req_from_line, + install_req_from_editable, + install_req_from_line, ) from pip._internal.req.req_file import ( - break_args_options, ignore_comments, join_lines, parse_requirements, - preprocess, process_line, skip_regex, + break_args_options, + ignore_comments, + join_lines, + parse_requirements, + preprocess, + process_line, + skip_regex, ) from tests.lib import make_test_finder, requirements_file diff --git a/tests/unit/test_req_install.py b/tests/unit/test_req_install.py index 66a84e1ba2a..a0c7711dcae 100644 --- a/tests/unit/test_req_install.py +++ b/tests/unit/test_req_install.py @@ -6,7 +6,8 @@ from pip._internal.exceptions import InstallationError from pip._internal.req.constructors import ( - install_req_from_line, install_req_from_req_string, + install_req_from_line, + install_req_from_req_string, ) from pip._internal.req.req_install import InstallRequirement diff --git a/tests/unit/test_req_uninstall.py b/tests/unit/test_req_uninstall.py index c83cba8e74b..bd135b9c123 100644 --- a/tests/unit/test_req_uninstall.py +++ b/tests/unit/test_req_uninstall.py @@ -5,8 +5,12 @@ import pip._internal.req.req_uninstall from pip._internal.req.req_uninstall import ( - StashedUninstallPathSet, UninstallPathSet, compact, - compress_for_output_listing, compress_for_rename, uninstallation_paths, + StashedUninstallPathSet, + UninstallPathSet, + compact, + compress_for_output_listing, + compress_for_rename, + uninstallation_paths, ) from tests.lib import create_file diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index b88c0fecd33..e026c2d9f58 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -23,21 +23,39 @@ from pip._vendor.six.moves.urllib import request as urllib_request from pip._internal.exceptions import ( - HashMismatch, HashMissing, InstallationError, + HashMismatch, + HashMissing, + InstallationError, ) from pip._internal.utils.deprecation import PipDeprecationWarning, deprecated from pip._internal.utils.encoding import BOMS, auto_decode from pip._internal.utils.glibc import ( - check_glibc_version, glibc_version_string, glibc_version_string_confstr, + check_glibc_version, + glibc_version_string, + glibc_version_string_confstr, glibc_version_string_ctypes, ) from pip._internal.utils.hashes import Hashes, MissingHashes from pip._internal.utils.misc import ( - call_subprocess, egg_link_path, ensure_dir, format_command_args, - get_installed_distributions, get_prog, make_subprocess_output_error, - normalize_path, normalize_version_info, path_to_display, path_to_url, - redact_netloc, redact_password_from_url, remove_auth_from_url, rmtree, - split_auth_from_netloc, split_auth_netloc_from_url, untar_file, unzip_file, + call_subprocess, + egg_link_path, + ensure_dir, + format_command_args, + get_installed_distributions, + get_prog, + make_subprocess_output_error, + normalize_path, + normalize_version_info, + path_to_display, + path_to_url, + redact_netloc, + redact_password_from_url, + remove_auth_from_url, + rmtree, + split_auth_from_netloc, + split_auth_netloc_from_url, + untar_file, + unzip_file, ) from pip._internal.utils.setuptools_build import make_setuptools_shim_args from pip._internal.utils.temp_dir import AdjacentTempDirectory, TempDirectory From 3626530b4ac5f255ba8e0fb151a096c62f0335f3 Mon Sep 17 00:00:00 2001 From: Omry Yadan Date: Mon, 22 Jul 2019 19:17:12 -0700 Subject: [PATCH 0017/3170] exclude '.tox', '.nox', '.git', '.hg', '.bzr', '.svn' from pip install . to speed installation up --- src/pip/_internal/download.py | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index 8715eb5b19d..6c2aafdf19d 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -939,7 +939,19 @@ def unpack_file_url( if is_dir_url(link): if os.path.isdir(location): rmtree(location) - shutil.copytree(link_path, location, symlinks=True) + shutil.copytree(link_path, + location, + symlinks=True, + ignore=shutil.ignore_patterns( + # Pulling in those directories can potentially be very slow. + # Excludin them speeds things up substantially in some cases. + # see dicsussion at: + # https://github.com/pypa/pip/issues/2195 + # https://github.com/pypa/pip/pull/2196 + '.tox', '.nox', '.git', '.hg', '.bzr', '.svn' + ) + ) + if download_dir: logger.info('Link is a directory, ignoring download_dir') return From 82e89a94c6ec4c6303331710e3be7ad0131b4a3b Mon Sep 17 00:00:00 2001 From: Omry Yadan Date: Mon, 22 Jul 2019 19:24:34 -0700 Subject: [PATCH 0018/3170] news --- news/2195.bugfix | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 news/2195.bugfix diff --git a/news/2195.bugfix b/news/2195.bugfix new file mode 100644 index 00000000000..9016516db76 --- /dev/null +++ b/news/2195.bugfix @@ -0,0 +1,4 @@ +pip install . is very slow in the presence of large directories in the source tree. +Typical cases are source control directories (.git, .svn etc) and test automation directries (.tox, .nox). +This diff excludes the common culprits from the copy to a temporary directory, speeding up pip install . +significantly in such cases. From f285407d7acb6521e2fde077e7eb1a41723908dc Mon Sep 17 00:00:00 2001 From: Omry Yadan Date: Mon, 22 Jul 2019 19:53:43 -0700 Subject: [PATCH 0019/3170] lint --- src/pip/_internal/download.py | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index 6c2aafdf19d..a0a3a4a2030 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -942,14 +942,12 @@ def unpack_file_url( shutil.copytree(link_path, location, symlinks=True, - ignore=shutil.ignore_patterns( - # Pulling in those directories can potentially be very slow. - # Excludin them speeds things up substantially in some cases. - # see dicsussion at: - # https://github.com/pypa/pip/issues/2195 - # https://github.com/pypa/pip/pull/2196 - '.tox', '.nox', '.git', '.hg', '.bzr', '.svn' - ) + # Pulling in those directories can potentially be very slow. + # Excludin them speeds things up substantially in some cases. + # see dicsussion at: + # https://github.com/pypa/pip/issues/2195 + # https://github.com/pypa/pip/pull/2196 + ignore=shutil.ignore_patterns('.tox', '.nox', '.git', '.hg', '.bzr', '.svn') ) if download_dir: From e8330528bfb0e2dac63e02cd6d1f45fd8f11ce92 Mon Sep 17 00:00:00 2001 From: Omry Yadan Date: Mon, 22 Jul 2019 20:22:07 -0700 Subject: [PATCH 0020/3170] lint take 2 (not sure how to test locally) --- src/pip/_internal/download.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index a0a3a4a2030..c73c639ac2a 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -947,8 +947,7 @@ def unpack_file_url( # see dicsussion at: # https://github.com/pypa/pip/issues/2195 # https://github.com/pypa/pip/pull/2196 - ignore=shutil.ignore_patterns('.tox', '.nox', '.git', '.hg', '.bzr', '.svn') - ) + ignore=shutil.ignore_patterns('.tox', '.nox', '.git', '.hg', '.bzr', '.svn')) if download_dir: logger.info('Link is a directory, ignoring download_dir') From 67ce4427dd3500d2f886c1c04cd729cb4b55b242 Mon Sep 17 00:00:00 2001 From: Omry Yadan Date: Tue, 23 Jul 2019 11:29:09 -0700 Subject: [PATCH 0021/3170] fixed lint, excludes only .tox and .nox --- src/pip/_internal/download.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index c73c639ac2a..fbaaffaf6f9 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -942,12 +942,12 @@ def unpack_file_url( shutil.copytree(link_path, location, symlinks=True, - # Pulling in those directories can potentially be very slow. - # Excludin them speeds things up substantially in some cases. + # Pulling in those directories can potentially + # be very slow. # see dicsussion at: # https://github.com/pypa/pip/issues/2195 # https://github.com/pypa/pip/pull/2196 - ignore=shutil.ignore_patterns('.tox', '.nox', '.git', '.hg', '.bzr', '.svn')) + ignore=shutil.ignore_patterns('.tox', '.nox')) if download_dir: logger.info('Link is a directory, ignoring download_dir') From 29a8b6ed38dd945d8c88de5571bd3aff3acd63a4 Mon Sep 17 00:00:00 2001 From: Omry Yadan Date: Tue, 23 Jul 2019 11:35:41 -0700 Subject: [PATCH 0022/3170] updated news --- news/2195.bugfix | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/news/2195.bugfix b/news/2195.bugfix index 9016516db76..3df54f10fcf 100644 --- a/news/2195.bugfix +++ b/news/2195.bugfix @@ -1,4 +1,4 @@ -pip install . is very slow in the presence of large directories in the source tree. -Typical cases are source control directories (.git, .svn etc) and test automation directries (.tox, .nox). -This diff excludes the common culprits from the copy to a temporary directory, speeding up pip install . -significantly in such cases. +'pip install .' is very slow in the presence of large directories in the source tree. +Specifialy test automation directries (.tox, .nox). +#6770 excludes .tox and .nox from being copied when pip install . is being exectued, +significantly speeding up the build in the presence of large .tox and .nox directories. From 9d9c73e563642f1c2106aed1d4376807a458e95b Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Wed, 24 Jul 2019 00:15:40 +0530 Subject: [PATCH 0023/3170] Bump for development --- src/pip/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/__init__.py b/src/pip/__init__.py index a0196ad5937..9c2e2a3bb48 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -1 +1 @@ -__version__ = "19.2.1" +__version__ = "19.3.dev0" From 8f32b8f425e6b064a7ffc2e2e2294e26f0ee0f9b Mon Sep 17 00:00:00 2001 From: Hugo Date: Tue, 23 Jul 2019 22:49:21 +0300 Subject: [PATCH 0024/3170] Drop support for EOL Python 3.4 --- src/pip/_internal/utils/compat.py | 6 +++--- tests/conftest.py | 2 +- tests/data/packages/LocalEnvironMarker/setup.py | 2 +- tests/functional/test_install.py | 4 ++-- tests/unit/test_req.py | 4 ++-- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/utils/compat.py b/src/pip/_internal/utils/compat.py index ec3995c2968..3bb87648d0c 100644 --- a/src/pip/_internal/utils/compat.py +++ b/src/pip/_internal/utils/compat.py @@ -47,7 +47,7 @@ HAS_TLS = (ssl is not None) or IS_PYOPENSSL -if sys.version_info >= (3, 4): +if sys.version_info >= (3,): uses_pycache = True from importlib.util import cache_from_source else: @@ -62,7 +62,7 @@ uses_pycache = cache_from_source is not None -if sys.version_info >= (3, 5): +if sys.version_info >= (3,): backslashreplace_decode = "backslashreplace" else: # In version 3.4 and older, backslashreplace exists @@ -202,7 +202,7 @@ def get_path_uid(path): return file_uid -if sys.version_info >= (3, 4): +if sys.version_info >= (3,): from importlib.machinery import EXTENSION_SUFFIXES def get_extension_suffixes(): diff --git a/tests/conftest.py b/tests/conftest.py index d1a12d65e5d..f682564966f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -352,4 +352,4 @@ def in_memory_pip(): @pytest.fixture def deprecated_python(): """Used to indicate whether pip deprecated this python version""" - return sys.version_info[:2] in [(3, 4), (2, 7)] + return sys.version_info[:2] in [(2, 7)] diff --git a/tests/data/packages/LocalEnvironMarker/setup.py b/tests/data/packages/LocalEnvironMarker/setup.py index cc2cd317aa4..36ceb214cf7 100644 --- a/tests/data/packages/LocalEnvironMarker/setup.py +++ b/tests/data/packages/LocalEnvironMarker/setup.py @@ -22,6 +22,6 @@ def path_to_url(path): version='0.0.1', packages=find_packages(), extras_require={ - ":python_version == '2.7' or python_version == '3.4'": ['simple'], + ":python_version == '2.7'": ['simple'], } ) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 96b780f3cc3..e7b6a272d55 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -528,7 +528,7 @@ def test_editable_install__local_dir_no_setup_py_with_pyproject( assert 'A "pyproject.toml" file was found' in msg -@pytest.mark.skipif("sys.version_info >= (3,4)") +@pytest.mark.skipif("sys.version_info >= (3,)") @pytest.mark.xfail def test_install_argparse_shadowed(script): # When argparse is in the stdlib, we support installing it @@ -543,7 +543,7 @@ def test_install_argparse_shadowed(script): @pytest.mark.network -@pytest.mark.skipif("sys.version_info < (3,4)") +@pytest.mark.skipif("sys.version_info < (3,)") def test_upgrade_argparse_shadowed(script): # If argparse is installed - even if shadowed for imported - we support # upgrading it and properly remove the older versions files. diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index 23a70f77890..4b729cefc33 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -83,7 +83,7 @@ def test_no_reuse_existing_build_dir(self, data): reqset, ) - # TODO: Update test when Python 2.7 or Python 3.4 is dropped. + # TODO: Update test when Python 2.7 is dropped. def test_environment_marker_extras(self, data): """ Test that the environment marker extras are used with @@ -99,7 +99,7 @@ def test_environment_marker_extras(self, data): resolver = self._basic_resolver(finder) resolver.resolve(reqset) # This is hacky but does test both case in py2 and py3 - if sys.version_info[:2] in ((2, 7), (3, 4)): + if sys.version_info[:2] == (2, 7): assert reqset.has_requirement('simple') else: assert not reqset.has_requirement('simple') From 459b4dc238e52351291bf40b78ce35289822b63c Mon Sep 17 00:00:00 2001 From: Omry Yadan Date: Tue, 23 Jul 2019 18:23:59 -0700 Subject: [PATCH 0025/3170] renamed news file to match PR --- news/{2195.bugfix => 6770.bugfix} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename news/{2195.bugfix => 6770.bugfix} (100%) diff --git a/news/2195.bugfix b/news/6770.bugfix similarity index 100% rename from news/2195.bugfix rename to news/6770.bugfix From 6b1aa6f6c00236a1f7952da1c1f944779f2a0757 Mon Sep 17 00:00:00 2001 From: Omry Yadan Date: Tue, 23 Jul 2019 18:26:06 -0700 Subject: [PATCH 0026/3170] simplifies new language --- news/6770.bugfix | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/news/6770.bugfix b/news/6770.bugfix index 3df54f10fcf..174b8f62c8a 100644 --- a/news/6770.bugfix +++ b/news/6770.bugfix @@ -1,4 +1,3 @@ 'pip install .' is very slow in the presence of large directories in the source tree. -Specifialy test automation directries (.tox, .nox). -#6770 excludes .tox and .nox from being copied when pip install . is being exectued, -significantly speeding up the build in the presence of large .tox and .nox directories. +#6770 excludes .tox and .nox from being copied by 'pip install .', significantly speeding +up the builds when those directories are large. From 5f40c21f29bd1c5694220cd8031687a318958b05 Mon Sep 17 00:00:00 2001 From: Hugo Date: Thu, 25 Jul 2019 10:10:04 +0300 Subject: [PATCH 0027/3170] Simplify using PY2 and PY3 constants --- src/pip/_internal/utils/compat.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/utils/compat.py b/src/pip/_internal/utils/compat.py index 3bb87648d0c..36398614fe9 100644 --- a/src/pip/_internal/utils/compat.py +++ b/src/pip/_internal/utils/compat.py @@ -9,7 +9,7 @@ import shutil import sys -from pip._vendor.six import text_type +from pip._vendor.six import PY2, PY3, text_type from pip._vendor.urllib3.util import IS_PYOPENSSL from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -47,7 +47,7 @@ HAS_TLS = (ssl is not None) or IS_PYOPENSSL -if sys.version_info >= (3,): +if PY3: uses_pycache = True from importlib.util import cache_from_source else: @@ -62,7 +62,7 @@ uses_pycache = cache_from_source is not None -if sys.version_info >= (3,): +if PY3: backslashreplace_decode = "backslashreplace" else: # In version 3.4 and older, backslashreplace exists @@ -72,7 +72,7 @@ # 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: + if PY2: # 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 @@ -156,7 +156,7 @@ def console_to_str(data): return str_to_display(data, desc='Subprocess output') -if sys.version_info >= (3,): +if PY3: def native_str(s, replace=False): # type: (str, bool) -> str if isinstance(s, bytes): @@ -202,7 +202,7 @@ def get_path_uid(path): return file_uid -if sys.version_info >= (3,): +if PY3: from importlib.machinery import EXTENSION_SUFFIXES def get_extension_suffixes(): From e7babff80fdd994c7eaf26fa0c2020ef16339b34 Mon Sep 17 00:00:00 2001 From: Hugo Date: Thu, 25 Jul 2019 11:14:54 +0300 Subject: [PATCH 0028/3170] Simplify using PY2 and PY3 constants --- tests/functional/test_install.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index e7b6a272d55..374c2403af7 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -5,6 +5,7 @@ import textwrap from os.path import curdir, join, pardir +import pip._vendor.six # noqa: F401 import pytest from pip._internal import pep425tags @@ -528,7 +529,7 @@ def test_editable_install__local_dir_no_setup_py_with_pyproject( assert 'A "pyproject.toml" file was found' in msg -@pytest.mark.skipif("sys.version_info >= (3,)") +@pytest.mark.skipif("pip._vendor.six.PY3") @pytest.mark.xfail def test_install_argparse_shadowed(script): # When argparse is in the stdlib, we support installing it @@ -543,7 +544,7 @@ def test_install_argparse_shadowed(script): @pytest.mark.network -@pytest.mark.skipif("sys.version_info < (3,)") +@pytest.mark.skipif("pip._vendor.six.PY2") def test_upgrade_argparse_shadowed(script): # If argparse is installed - even if shadowed for imported - we support # upgrading it and properly remove the older versions files. From 4a7b345cc518cecbdc817e503391c231a6e967ac Mon Sep 17 00:00:00 2001 From: Hugo Date: Thu, 25 Jul 2019 10:47:05 +0300 Subject: [PATCH 0029/3170] Consistently get version string, ignoring alpha/beta --- tests/functional/test_install.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 96b780f3cc3..9481cfd0869 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1306,7 +1306,7 @@ def test_double_install_fail(script): def _get_expected_error_text(): return ( "Package 'pkga' requires a different Python: {} not in '<1.0'" - ).format(sys.version.split()[0]) + ).format('.'.join(map(str, sys.version_info[:3]))) def test_install_incompatible_python_requires(script): From b4ac45cdd3db50f3cb1271853a40deb1a53d0703 Mon Sep 17 00:00:00 2001 From: Hugo Date: Thu, 25 Jul 2019 18:48:07 +0300 Subject: [PATCH 0030/3170] Ignore because flake8 can't detect the use inside skipif() --- tests/functional/test_install.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 374c2403af7..a8d3cf9628e 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -5,6 +5,7 @@ import textwrap from os.path import curdir, join, pardir +# Ignore because flake8 can't detect the use inside skipif(). import pip._vendor.six # noqa: F401 import pytest From 1f736c5e778f48c91adbd68b45f15d6400e21b4d Mon Sep 17 00:00:00 2001 From: Xavier Fernandez Date: Fri, 26 Jul 2019 00:30:44 +0200 Subject: [PATCH 0031/3170] Spread the Windows test load between Azure & Appveyor --- .appveyor.yml | 9 ++++----- .azure-pipelines/jobs/test-windows.yml | 17 ++++------------- 2 files changed, 8 insertions(+), 18 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 2819e052120..9bd0f595c76 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -1,15 +1,14 @@ environment: matrix: # Unit and integration tests. - - PYTHON: "C:\\Python27" + - PYTHON: "C:\\Python27-x64" RUN_INTEGRATION_TESTS: "True" - PYTHON: "C:\\Python36-x64" RUN_INTEGRATION_TESTS: "True" + - PYTHON: "C:\\Python37-x64" + RUN_INTEGRATION_TESTS: "True" # Unit tests only. - - PYTHON: "C:\\Python27-x64" - - PYTHON: "C:\\Python35" - - PYTHON: "C:\\Python35-x64" - - PYTHON: "C:\\Python36" + # Nothing for the moment matrix: fast_finish: true diff --git a/.azure-pipelines/jobs/test-windows.yml b/.azure-pipelines/jobs/test-windows.yml index 752646247d0..691c9df13ae 100644 --- a/.azure-pipelines/jobs/test-windows.yml +++ b/.azure-pipelines/jobs/test-windows.yml @@ -9,11 +9,11 @@ jobs: vmImage: ${{ parameters.vmImage }} strategy: matrix: - Python27-x64: + Python27-x86: python.version: '2.7' - python.architecture: x64 - Python36-x64: - python.version: '3.6' + python.architecture: x86 + Python35-x64: + python.version: '3.5' python.architecture: x64 maxParallel: 2 @@ -32,16 +32,7 @@ jobs: vmImage: ${{ parameters.vmImage }} strategy: matrix: - Python35-x64: - python.version: '3.5' - python.architecture: x64 - Python37-x64: - python.version: '3.7' - python.architecture: x64 # This is for Windows, so test x86 builds - Python27-x86: - python.version: '2.7' - python.architecture: x86 Python35-x86: python.version: '3.5' python.architecture: x86 From 30b7720f4b111051ff130992beba9748892166e0 Mon Sep 17 00:00:00 2001 From: Xavier Fernandez Date: Fri, 26 Jul 2019 13:28:33 +0200 Subject: [PATCH 0032/3170] Move Python 37-x64 to Azure --- .appveyor.yml | 4 ++-- .azure-pipelines/jobs/test-windows.yml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 9bd0f595c76..4efa9159584 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -3,9 +3,9 @@ environment: # Unit and integration tests. - PYTHON: "C:\\Python27-x64" RUN_INTEGRATION_TESTS: "True" - - PYTHON: "C:\\Python36-x64" + - PYTHON: "C:\\Python35-x64" RUN_INTEGRATION_TESTS: "True" - - PYTHON: "C:\\Python37-x64" + - PYTHON: "C:\\Python36-x64" RUN_INTEGRATION_TESTS: "True" # Unit tests only. # Nothing for the moment diff --git a/.azure-pipelines/jobs/test-windows.yml b/.azure-pipelines/jobs/test-windows.yml index 691c9df13ae..ee869c87c89 100644 --- a/.azure-pipelines/jobs/test-windows.yml +++ b/.azure-pipelines/jobs/test-windows.yml @@ -12,8 +12,8 @@ jobs: Python27-x86: python.version: '2.7' python.architecture: x86 - Python35-x64: - python.version: '3.5' + Python37-x64: + python.version: '3.7' python.architecture: x64 maxParallel: 2 From d948f63fe266d275f1f421dbc440b26c7933de70 Mon Sep 17 00:00:00 2001 From: Ivan Pozdeev Date: Sat, 27 Jul 2019 02:46:08 +0300 Subject: [PATCH 0033/3170] Document that `--ignore-installed` is dangerous --- src/pip/_internal/commands/install.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 927e7817be2..7835606714f 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -187,7 +187,11 @@ def __init__(self, *args, **kw): '-I', '--ignore-installed', dest='ignore_installed', action='store_true', - help='Ignore the installed packages (reinstalling instead).') + help='Ignore the installed packages, overwriting them. ' + 'This can break your system if the existing package ' + 'is of a different version or was installed ' + 'with a different package manager!' + ) cmd_opts.add_option(cmdoptions.ignore_requires_python()) cmd_opts.add_option(cmdoptions.no_build_isolation()) From 125aae08efeb9097c48309599fbbe75dbb84dc87 Mon Sep 17 00:00:00 2001 From: Ivan Pozdeev Date: Sat, 27 Jul 2019 03:08:21 +0300 Subject: [PATCH 0034/3170] add a NEWS entry --- news/6794.doc | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/6794.doc diff --git a/news/6794.doc b/news/6794.doc new file mode 100644 index 00000000000..55bc01404dd --- /dev/null +++ b/news/6794.doc @@ -0,0 +1 @@ +Document that ``--ignore-installed`` is dangerous. From 6c7d309a1cfb99b8a0ea463de028cf9cd7f9e28a Mon Sep 17 00:00:00 2001 From: Ivan Pozdeev Date: Sat, 27 Jul 2019 03:19:11 +0300 Subject: [PATCH 0035/3170] fix flake8 violation --- src/pip/_internal/commands/install.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 7835606714f..e7e16809646 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -191,7 +191,7 @@ def __init__(self, *args, **kw): 'This can break your system if the existing package ' 'is of a different version or was installed ' 'with a different package manager!' - ) + ) cmd_opts.add_option(cmdoptions.ignore_requires_python()) cmd_opts.add_option(cmdoptions.no_build_isolation()) From 1f09e67f342037fb2ae38ef5a82d232ef9116880 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 10 Jul 2019 00:36:33 -0700 Subject: [PATCH 0036/3170] Only import a Command class when it is actually needed. This resulted in an approximate 24% speed-up of a vanilla `pip` invocation on one system (0.477 secs before, 0.363 secs after). --- docs/pip_sphinxext.py | 9 +- src/pip/_internal/__init__.py | 5 +- src/pip/_internal/cli/autocompletion.py | 4 +- src/pip/_internal/cli/base_command.py | 8 +- src/pip/_internal/commands/__init__.py | 146 ++++++++++++-------- src/pip/_internal/commands/check.py | 3 +- src/pip/_internal/commands/completion.py | 3 +- src/pip/_internal/commands/configuration.py | 3 - src/pip/_internal/commands/debug.py | 2 - src/pip/_internal/commands/download.py | 3 - src/pip/_internal/commands/freeze.py | 3 +- src/pip/_internal/commands/hash.py | 4 +- src/pip/_internal/commands/help.py | 9 +- src/pip/_internal/commands/install.py | 3 - src/pip/_internal/commands/list.py | 3 +- src/pip/_internal/commands/search.py | 3 +- src/pip/_internal/commands/show.py | 3 +- src/pip/_internal/commands/uninstall.py | 3 +- src/pip/_internal/commands/wheel.py | 3 - tests/functional/test_help.py | 11 +- tests/functional/test_search.py | 6 +- tests/lib/options_helpers.py | 10 +- tests/unit/test_base_command.py | 9 +- tests/unit/test_commands.py | 31 +++++ tests/unit/test_format_control.py | 4 +- tests/unit/test_options.py | 8 +- tests/unit/test_req.py | 4 +- 27 files changed, 174 insertions(+), 129 deletions(-) create mode 100644 tests/unit/test_commands.py diff --git a/docs/pip_sphinxext.py b/docs/pip_sphinxext.py index b250be47f97..c34c457e394 100644 --- a/docs/pip_sphinxext.py +++ b/docs/pip_sphinxext.py @@ -9,14 +9,14 @@ from docutils.statemachine import ViewList from pip._internal.cli import cmdoptions -from pip._internal.commands import commands_dict as commands +from pip._internal.commands import create_command class PipCommandUsage(rst.Directive): required_arguments = 1 def run(self): - cmd = commands[self.arguments[0]] + cmd = create_command(self.arguments[0]) usage = dedent( cmd.usage.replace('%prog', 'pip {}'.format(cmd.name)) ).strip() @@ -31,7 +31,8 @@ def run(self): node = nodes.paragraph() node.document = self.state.document desc = ViewList() - description = dedent(commands[self.arguments[0]].__doc__) + cmd = create_command(self.arguments[0]) + description = dedent(cmd.__doc__) for line in description.split('\n'): desc.append(line, "") self.state.nested_parse(desc, 0, node) @@ -95,7 +96,7 @@ class PipCommandOptions(PipOptions): required_arguments = 1 def process_options(self): - cmd = commands[self.arguments[0]]() + cmd = create_command(self.arguments[0]) self._format_options( cmd.parser.option_groups[0].option_list, cmd_name=cmd.name, diff --git a/src/pip/_internal/__init__.py b/src/pip/_internal/__init__.py index fbadc28ac21..bb8f0ff8705 100755 --- a/src/pip/_internal/__init__.py +++ b/src/pip/_internal/__init__.py @@ -39,7 +39,7 @@ from pip._internal.cli.autocompletion import autocomplete from pip._internal.cli.main_parser import parse_command -from pip._internal.commands import commands_dict +from pip._internal.commands import create_command from pip._internal.exceptions import PipError from pip._internal.utils import deprecation from pip._vendor.urllib3.exceptions import InsecureRequestWarning @@ -73,5 +73,6 @@ def main(args=None): 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=("--isolated" in cmd_args)) + command = create_command(cmd_name, isolated=("--isolated" in cmd_args)) + return command.main(cmd_args) diff --git a/src/pip/_internal/cli/autocompletion.py b/src/pip/_internal/cli/autocompletion.py index 0a04199e6dc..25db462f441 100644 --- a/src/pip/_internal/cli/autocompletion.py +++ b/src/pip/_internal/cli/autocompletion.py @@ -6,7 +6,7 @@ import sys from pip._internal.cli.main_parser import create_main_parser -from pip._internal.commands import commands_dict, get_summaries +from pip._internal.commands import create_command, get_summaries from pip._internal.utils.misc import get_installed_distributions @@ -54,7 +54,7 @@ def autocomplete(): print(dist) sys.exit(1) - subcommand = commands_dict[subcommand_name]() + subcommand = create_command(subcommand_name) for opt in subcommand.parser.option_list_all: if opt.help != optparse.SUPPRESS_HELP: diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index 177524804fb..fd771b1412c 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -61,18 +61,20 @@ class Command(object): usage = None # type: Optional[str] ignore_require_venv = False # type: bool - def __init__(self, isolated=False): - # type: (bool) -> None + def __init__(self, name, summary, isolated=False): + # type: (str, str, bool) -> None parser_kw = { 'usage': self.usage, 'prog': '%s %s' % (get_prog(), self.name), 'formatter': UpdatingDefaultsHelpFormatter(), 'add_help_option': False, - 'name': self.name, + 'name': name, 'description': self.__doc__, 'isolated': isolated, } + self.name = name + self.summary = summary self.parser = ConfigOptionParser(**parser_kw) # Commands should add options to this option group diff --git a/src/pip/_internal/commands/__init__.py b/src/pip/_internal/commands/__init__.py index 9e0ab86b9ca..81cb5e27087 100644 --- a/src/pip/_internal/commands/__init__.py +++ b/src/pip/_internal/commands/__init__.py @@ -3,57 +3,104 @@ """ from __future__ import absolute_import -from pip._internal.commands.completion import CompletionCommand -from pip._internal.commands.configuration import ConfigurationCommand -from pip._internal.commands.debug import DebugCommand -from pip._internal.commands.download import DownloadCommand -from pip._internal.commands.freeze import FreezeCommand -from pip._internal.commands.hash import HashCommand -from pip._internal.commands.help import HelpCommand -from pip._internal.commands.list import ListCommand -from pip._internal.commands.check import CheckCommand -from pip._internal.commands.search import SearchCommand -from pip._internal.commands.show import ShowCommand -from pip._internal.commands.install import InstallCommand -from pip._internal.commands.uninstall import UninstallCommand -from pip._internal.commands.wheel import WheelCommand +import importlib +from collections import namedtuple, OrderedDict from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import List, Type + from typing import Any, Iterable, Tuple from pip._internal.cli.base_command import Command -commands_order = [ - InstallCommand, - DownloadCommand, - UninstallCommand, - FreezeCommand, - ListCommand, - ShowCommand, - CheckCommand, - ConfigurationCommand, - SearchCommand, - WheelCommand, - HashCommand, - CompletionCommand, - DebugCommand, - HelpCommand, -] # type: List[Type[Command]] - -commands_dict = {c.name: c for c in commands_order} - - -def get_summaries(ordered=True): - """Yields sorted (command name, command summary) tuples.""" - - if ordered: - cmditems = _sort_commands(commands_dict, commands_order) - else: - cmditems = commands_dict.items() - for name, command_class in cmditems: - yield (name, command_class.summary) +CommandInfo = namedtuple('CommandInfo', 'module_path, class_name, summary') + +# The ordering matters for help display. +# Also, even though the module path starts with the same +# "pip._internal.commands" prefix in each case, we include the full path +# because it makes testing easier (specifically when modifying commands_dict +# in test setup / teardown by adding info for a FakeCommand class defined +# in a test-related module). +# Finally, we need to pass an iterable of pairs here rather than a dict +# so that the ordering won't be lost when using Python 2.7. +commands_dict = OrderedDict([ + ('install', CommandInfo( + 'pip._internal.commands.install', 'InstallCommand', + 'Install packages.', + )), + ('download', CommandInfo( + 'pip._internal.commands.download', 'DownloadCommand', + 'Download packages.', + )), + ('uninstall', CommandInfo( + 'pip._internal.commands.uninstall', 'UninstallCommand', + 'Uninstall packages.', + )), + ('freeze', CommandInfo( + 'pip._internal.commands.freeze', 'FreezeCommand', + 'Output installed packages in requirements format.', + )), + ('list', CommandInfo( + 'pip._internal.commands.list', 'ListCommand', + 'List installed packages.', + )), + ('show', CommandInfo( + 'pip._internal.commands.show', 'ShowCommand', + 'Show information about installed packages.', + )), + ('check', CommandInfo( + 'pip._internal.commands.check', 'CheckCommand', + 'Verify installed packages have compatible dependencies.', + )), + ('config', CommandInfo( + 'pip._internal.commands.configuration', 'ConfigurationCommand', + 'Manage local and global configuration.', + )), + ('search', CommandInfo( + 'pip._internal.commands.search', 'SearchCommand', + 'Search PyPI for packages.', + )), + ('wheel', CommandInfo( + 'pip._internal.commands.wheel', 'WheelCommand', + 'Build wheels from your requirements.', + )), + ('hash', CommandInfo( + 'pip._internal.commands.hash', 'HashCommand', + 'Compute hashes of package archives.', + )), + ('completion', CommandInfo( + 'pip._internal.commands.completion', 'CompletionCommand', + 'A helper command used for command completion.', + )), + ('debug', CommandInfo( + 'pip._internal.commands.debug', 'DebugCommand', + 'Show information useful for debugging.', + )), + ('help', CommandInfo( + 'pip._internal.commands.help', 'HelpCommand', + 'Show help for commands.', + )), +]) # type: OrderedDict[str, CommandInfo] + + +def create_command(name, **kwargs): + # type: (str, **Any) -> Command + """ + Create an instance of the Command class with the given name. + """ + module_path, class_name, summary = commands_dict[name] + module = importlib.import_module(module_path) + command_class = getattr(module, class_name) + command = command_class(name=name, summary=summary, **kwargs) + + return command + + +def get_summaries(): + # type: () -> Iterable[Tuple[str, str]] + """Yield command (name, summary) tuples in display order.""" + for name, command_info in commands_dict.items(): + yield (name, command_info.summary) def get_similar_commands(name): @@ -68,14 +115,3 @@ def get_similar_commands(name): return close_commands[0] else: return False - - -def _sort_commands(cmddict, order): - def keyfn(key): - try: - return order.index(key[1]) - except ValueError: - # unordered items should come last - return 0xff - - return sorted(cmddict.items(), key=keyfn) diff --git a/src/pip/_internal/commands/check.py b/src/pip/_internal/commands/check.py index 865285b5065..88dff795a1d 100644 --- a/src/pip/_internal/commands/check.py +++ b/src/pip/_internal/commands/check.py @@ -11,10 +11,9 @@ 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, parsing_probs = create_package_set_from_installed() diff --git a/src/pip/_internal/commands/completion.py b/src/pip/_internal/commands/completion.py index 2fcdd393ed6..6c37162813c 100644 --- a/src/pip/_internal/commands/completion.py +++ b/src/pip/_internal/commands/completion.py @@ -47,8 +47,7 @@ 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): diff --git a/src/pip/_internal/commands/configuration.py b/src/pip/_internal/commands/configuration.py index 91f916e85ac..31b93d96b17 100644 --- a/src/pip/_internal/commands/configuration.py +++ b/src/pip/_internal/commands/configuration.py @@ -34,7 +34,6 @@ class ConfigurationCommand(Command): default. """ - name = 'config' usage = """ %prog [] list %prog [] [--editor ] edit @@ -44,8 +43,6 @@ class ConfigurationCommand(Command): %prog [] unset name """ - summary = "Manage local and global configuration." - def __init__(self, *args, **kwargs): super(ConfigurationCommand, self).__init__(*args, **kwargs) diff --git a/src/pip/_internal/commands/debug.py b/src/pip/_internal/commands/debug.py index eb4f8c4e6fc..df31c83bf51 100644 --- a/src/pip/_internal/commands/debug.py +++ b/src/pip/_internal/commands/debug.py @@ -77,10 +77,8 @@ class DebugCommand(Command): Display debug information. """ - name = 'debug' usage = """ %prog """ - summary = 'Show information useful for debugging.' ignore_require_venv = True def __init__(self, *args, **kw): diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index 5642b561758..2bb27f876ad 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -29,7 +29,6 @@ class DownloadCommand(RequirementCommand): pip also supports downloading from "requirements files", which provide an easy way to specify a whole environment to be downloaded. """ - name = 'download' usage = """ %prog [options] [package-index-options] ... @@ -38,8 +37,6 @@ class DownloadCommand(RequirementCommand): %prog [options] ... %prog [options] ...""" - summary = 'Download packages.' - def __init__(self, *args, **kw): super(DownloadCommand, self).__init__(*args, **kw) diff --git a/src/pip/_internal/commands/freeze.py b/src/pip/_internal/commands/freeze.py index 9fc5b04693c..112a380439d 100644 --- a/src/pip/_internal/commands/freeze.py +++ b/src/pip/_internal/commands/freeze.py @@ -18,10 +18,9 @@ class FreezeCommand(Command): packages are listed in a case-insensitive sorted order. """ - name = 'freeze' + usage = """ %prog [options]""" - summary = 'Output installed packages in requirements format.' log_streams = ("ext://sys.stderr", "ext://sys.stderr") def __init__(self, *args, **kw): diff --git a/src/pip/_internal/commands/hash.py b/src/pip/_internal/commands/hash.py index 423440e9c29..87b09f071e1 100644 --- a/src/pip/_internal/commands/hash.py +++ b/src/pip/_internal/commands/hash.py @@ -18,11 +18,9 @@ class HashCommand(Command): These can be used with --hash in a requirements file to do repeatable installs. - """ - name = 'hash' + usage = '%prog [options] ...' - summary = 'Compute hashes of package archives.' ignore_require_venv = True def __init__(self, *args, **kw): diff --git a/src/pip/_internal/commands/help.py b/src/pip/_internal/commands/help.py index 49a81cbb074..270e82b28c6 100644 --- a/src/pip/_internal/commands/help.py +++ b/src/pip/_internal/commands/help.py @@ -7,14 +7,15 @@ class HelpCommand(Command): """Show help for commands""" - name = 'help' + usage = """ %prog """ - summary = 'Show help for commands.' ignore_require_venv = True def run(self, options, args): - from pip._internal.commands import commands_dict, get_similar_commands + from pip._internal.commands import ( + commands_dict, create_command, get_similar_commands, + ) try: # 'pip help' with no args is handled by pip.__init__.parseopt() @@ -31,7 +32,7 @@ def run(self, options, args): raise CommandError(' - '.join(msg)) - command = commands_dict[cmd_name]() + command = create_command(cmd_name) command.parser.print_help() return SUCCESS diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 927e7817be2..b18bbd45172 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -87,7 +87,6 @@ class InstallCommand(RequirementCommand): 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] [package-index-options] ... @@ -96,8 +95,6 @@ class InstallCommand(RequirementCommand): %prog [options] [-e] ... %prog [options] ...""" - summary = 'Install packages.' - def __init__(self, *args, **kw): super(InstallCommand, self).__init__(*args, **kw) diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index ea7f5eb9c44..2fd39097c12 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -27,10 +27,9 @@ class ListCommand(Command): Packages are listed in a case-insensitive sorted order. """ - name = 'list' + usage = """ %prog [options]""" - summary = 'List installed packages.' def __init__(self, *args, **kw): super(ListCommand, self).__init__(*args, **kw) diff --git a/src/pip/_internal/commands/search.py b/src/pip/_internal/commands/search.py index 58027112402..c96f0b90423 100644 --- a/src/pip/_internal/commands/search.py +++ b/src/pip/_internal/commands/search.py @@ -24,10 +24,9 @@ class SearchCommand(Command): """Search for PyPI packages whose name or summary contains .""" - name = 'search' + usage = """ %prog [options] """ - summary = 'Search PyPI for packages.' ignore_require_venv = True def __init__(self, *args, **kw): diff --git a/src/pip/_internal/commands/show.py b/src/pip/_internal/commands/show.py index a18a9020c41..bacd002ae51 100644 --- a/src/pip/_internal/commands/show.py +++ b/src/pip/_internal/commands/show.py @@ -19,10 +19,9 @@ class ShowCommand(Command): The output is in RFC-compliant mail header format. """ - name = 'show' + usage = """ %prog [options] ...""" - summary = 'Show information about installed packages.' ignore_require_venv = True def __init__(self, *args, **kw): diff --git a/src/pip/_internal/commands/uninstall.py b/src/pip/_internal/commands/uninstall.py index 0cd6f54bd86..ede23083857 100644 --- a/src/pip/_internal/commands/uninstall.py +++ b/src/pip/_internal/commands/uninstall.py @@ -19,11 +19,10 @@ class UninstallCommand(Command): leave behind no metadata to determine what files were installed. - Script wrappers installed by ``python setup.py develop``. """ - name = 'uninstall' + usage = """ %prog [options] ... %prog [options] -r ...""" - summary = 'Uninstall packages.' def __init__(self, *args, **kw): super(UninstallCommand, self).__init__(*args, **kw) diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index 97f3b148af5..a246db65579 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -33,7 +33,6 @@ class WheelCommand(RequirementCommand): """ - name = 'wheel' usage = """ %prog [options] ... %prog [options] -r ... @@ -41,8 +40,6 @@ class WheelCommand(RequirementCommand): %prog [options] [-e] ... %prog [options] ...""" - summary = 'Build wheels from your requirements.' - def __init__(self, *args, **kw): super(WheelCommand, self).__init__(*args, **kw) diff --git a/tests/functional/test_help.py b/tests/functional/test_help.py index 9e3dbcde8c9..1d1e439cf63 100644 --- a/tests/functional/test_help.py +++ b/tests/functional/test_help.py @@ -2,8 +2,7 @@ from mock import Mock from pip._internal.cli.base_command import ERROR, SUCCESS -from pip._internal.commands import commands_dict as commands -from pip._internal.commands.help import HelpCommand +from pip._internal.commands import commands_dict, create_command from pip._internal.exceptions import CommandError @@ -13,7 +12,7 @@ def test_run_method_should_return_success_when_finds_command_name(): """ options_mock = Mock() args = ('freeze',) - help_cmd = HelpCommand() + help_cmd = create_command('help') status = help_cmd.run(options_mock, args) assert status == SUCCESS @@ -24,7 +23,7 @@ def test_run_method_should_return_success_when_command_name_not_specified(): """ options_mock = Mock() args = () - help_cmd = HelpCommand() + help_cmd = create_command('help') status = help_cmd.run(options_mock, args) assert status == SUCCESS @@ -35,7 +34,7 @@ def test_run_method_should_raise_command_error_when_command_does_not_exist(): """ options_mock = Mock() args = ('mycommand',) - help_cmd = HelpCommand() + help_cmd = create_command('help') with pytest.raises(CommandError): help_cmd.run(options_mock, args) @@ -80,7 +79,7 @@ def test_help_commands_equally_functional(in_memory_pip): assert sum(ret) == 0, 'exit codes of: ' + msg assert all(len(o) > 0 for o in out) - for name, cls in commands.items(): + for name in commands_dict: assert ( in_memory_pip.pip('help', name).stdout == in_memory_pip.pip(name, '--help').stdout != "" diff --git a/tests/functional/test_search.py b/tests/functional/test_search.py index ea438a8bb09..70421de3091 100644 --- a/tests/functional/test_search.py +++ b/tests/functional/test_search.py @@ -4,8 +4,8 @@ import pytest from pip._internal.cli.status_codes import NO_MATCHES_FOUND, SUCCESS +from pip._internal.commands import create_command from pip._internal.commands.search import ( - SearchCommand, highest_version, print_results, transform_hits, @@ -111,7 +111,7 @@ def test_run_method_should_return_success_when_find_packages(): """ Test SearchCommand.run for found package """ - command = SearchCommand() + command = create_command('search') cmdline = "--index=https://pypi.org/pypi pip" options, args = command.parse_args(cmdline.split()) status = command.run(options, args) @@ -123,7 +123,7 @@ def test_run_method_should_return_no_matches_found_when_does_not_find_pkgs(): """ Test SearchCommand.run for no matches """ - command = SearchCommand() + command = create_command('search') cmdline = "--index=https://pypi.org/pypi nonexistentpackage" options, args = command.parse_args(cmdline.split()) status = command.run(options, args) diff --git a/tests/lib/options_helpers.py b/tests/lib/options_helpers.py index a93c40e3aea..120070abb99 100644 --- a/tests/lib/options_helpers.py +++ b/tests/lib/options_helpers.py @@ -5,13 +5,11 @@ from pip._internal.cli import cmdoptions from pip._internal.cli.base_command import Command -from pip._internal.commands import commands_dict +from pip._internal.commands import CommandInfo, commands_dict from tests.lib.configuration_helpers import reset_os_environ class FakeCommand(Command): - name = 'fake' - summary = name def main(self, args): index_opts = cmdoptions.make_option_group( @@ -26,8 +24,10 @@ class AddFakeCommandMixin(object): def setup(self): self.environ_before = os.environ.copy() - commands_dict[FakeCommand.name] = FakeCommand + commands_dict['fake'] = CommandInfo( + 'tests.lib.options_helpers', 'FakeCommand', 'fake summary', + ) def teardown(self): reset_os_environ(self.environ_before) - commands_dict.pop(FakeCommand.name) + commands_dict.pop('fake') diff --git a/tests/unit/test_base_command.py b/tests/unit/test_base_command.py index 293573ab9a4..fc6cf2b7a78 100644 --- a/tests/unit/test_base_command.py +++ b/tests/unit/test_base_command.py @@ -7,8 +7,8 @@ class FakeCommand(Command): - name = 'fake' - summary = name + + _name = 'fake' def __init__(self, run_func=None, error=False): if error: @@ -16,7 +16,7 @@ def run_func(): raise SystemExit(1) self.run_func = run_func - super(FakeCommand, self).__init__() + super(FakeCommand, self).__init__(self._name, self._name) def main(self, args): args.append("--disable-pip-version-check") @@ -29,8 +29,7 @@ def run(self, options, args): class FakeCommandWithUnicode(FakeCommand): - name = 'fake_unicode' - summary = name + _name = 'fake_unicode' def run(self, options, args): logging.getLogger("pip.tests").info(b"bytes here \xE9") diff --git a/tests/unit/test_commands.py b/tests/unit/test_commands.py new file mode 100644 index 00000000000..c300c04a3c4 --- /dev/null +++ b/tests/unit/test_commands.py @@ -0,0 +1,31 @@ +import pytest + +from pip._internal.commands import commands_dict, create_command, get_summaries + + +def test_commands_dict__order(): + """ + Check the ordering of commands_dict. + """ + names = list(commands_dict) + # A spot-check is sufficient to check that commands_dict encodes an + # ordering. + assert names[0] == 'install' + assert names[-1] == 'help' + + +@pytest.mark.parametrize('name', list(commands_dict)) +def test_create_command(name): + """Test creating an instance of each available command.""" + command = create_command(name) + assert command.name == name + assert command.summary == commands_dict[name].summary + + +def test_get_summaries(): + actual = list(get_summaries()) + for name, summary in actual: + assert summary == commands_dict[name].summary + + # Also check that the result is ordered correctly. + assert [item[0] for item in actual] == list(commands_dict) diff --git a/tests/unit/test_format_control.py b/tests/unit/test_format_control.py index 0b376624724..0b0e2bde221 100644 --- a/tests/unit/test_format_control.py +++ b/tests/unit/test_format_control.py @@ -6,11 +6,9 @@ class SimpleCommand(Command): - name = 'fake' - summary = name def __init__(self): - super(SimpleCommand, self).__init__() + super(SimpleCommand, self).__init__('fake', 'fake summary') self.cmd_opts.add_option(cmdoptions.no_binary()) self.cmd_opts.add_option(cmdoptions.only_binary()) diff --git a/tests/unit/test_options.py b/tests/unit/test_options.py index 0eb452ef4da..32509209869 100644 --- a/tests/unit/test_options.py +++ b/tests/unit/test_options.py @@ -5,7 +5,7 @@ import pip._internal.configuration from pip._internal import main -from pip._internal.commands import ConfigurationCommand, DownloadCommand +from pip._internal.commands import create_command from pip._internal.exceptions import PipError from tests.lib.options_helpers import AddFakeCommandMixin @@ -193,7 +193,7 @@ class TestUsePEP517Options(object): def parse_args(self, args): # We use DownloadCommand since that is one of the few Command # classes with the use_pep517 options. - command = DownloadCommand() + command = create_command('download') options, args = command.parse_args(args) return options @@ -411,7 +411,7 @@ def test_venv_config_file_found(self, monkeypatch): ) ) def test_config_file_options(self, monkeypatch, args, expect): - cmd = ConfigurationCommand() + cmd = create_command('config') # Replace a handler with a no-op to avoid side effects monkeypatch.setattr(cmd, "get_name", lambda *a: None) @@ -423,7 +423,7 @@ def test_config_file_options(self, monkeypatch, args, expect): assert expect == cmd._determine_file(options, need_value=False) def test_config_file_venv_option(self, monkeypatch): - cmd = ConfigurationCommand() + cmd = create_command('config') # Replace a handler with a no-op to avoid side effects monkeypatch.setattr(cmd, "get_name", lambda *a: None) diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index 2b1428686e1..4fe71cbfde5 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -9,7 +9,7 @@ from pip._vendor.packaging.markers import Marker from pip._vendor.packaging.requirements import Requirement -from pip._internal.commands.install import InstallCommand +from pip._internal.commands import create_command from pip._internal.download import PipSession from pip._internal.exceptions import ( HashErrors, @@ -186,7 +186,7 @@ def test_missing_hash_with_require_hashes_in_reqs_file(self, data, tmpdir): req_set = RequirementSet(require_hashes=False) finder = make_test_finder(find_links=[data.find_links]) session = finder.session - command = InstallCommand() + command = create_command('install') with requirements_file('--require-hashes', tmpdir) as reqs_file: options, args = command.parse_args(['-r', reqs_file]) command.populate_requirement_set( From 4cd825873b4a0e179212143d4a9bfd13f3d853ee Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Thu, 11 Jul 2019 01:27:21 -0700 Subject: [PATCH 0037/3170] Remove get_summaries(). --- src/pip/_internal/cli/autocompletion.py | 4 ++-- src/pip/_internal/cli/main_parser.py | 12 +++++------- src/pip/_internal/commands/__init__.py | 9 +-------- tests/unit/test_commands.py | 11 +---------- 4 files changed, 9 insertions(+), 27 deletions(-) diff --git a/src/pip/_internal/cli/autocompletion.py b/src/pip/_internal/cli/autocompletion.py index 25db462f441..287e62c78a5 100644 --- a/src/pip/_internal/cli/autocompletion.py +++ b/src/pip/_internal/cli/autocompletion.py @@ -6,7 +6,7 @@ import sys from pip._internal.cli.main_parser import create_main_parser -from pip._internal.commands import create_command, get_summaries +from pip._internal.commands import commands_dict, create_command from pip._internal.utils.misc import get_installed_distributions @@ -23,7 +23,7 @@ def autocomplete(): except IndexError: current = '' - subcommands = [cmd for cmd, summary in get_summaries()] + subcommands = list(commands_dict) options = [] # subcommand try: diff --git a/src/pip/_internal/cli/main_parser.py b/src/pip/_internal/cli/main_parser.py index 1c371ca3b56..a89821d4489 100644 --- a/src/pip/_internal/cli/main_parser.py +++ b/src/pip/_internal/cli/main_parser.py @@ -9,11 +9,7 @@ ConfigOptionParser, UpdatingDefaultsHelpFormatter, ) -from pip._internal.commands import ( - commands_dict, - get_similar_commands, - get_summaries, -) +from pip._internal.commands import commands_dict, get_similar_commands from pip._internal.exceptions import CommandError from pip._internal.utils.misc import get_pip_version, get_prog from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -51,8 +47,10 @@ def create_main_parser(): parser.main = True # type: ignore # create command listing for description - command_summaries = get_summaries() - description = [''] + ['%-27s %s' % (i, j) for i, j in command_summaries] + description = [''] + [ + '%-27s %s' % (name, command_info.summary) + for name, command_info in commands_dict.items() + ] parser.description = '\n'.join(description) return parser diff --git a/src/pip/_internal/commands/__init__.py b/src/pip/_internal/commands/__init__.py index 81cb5e27087..a5d454192d6 100644 --- a/src/pip/_internal/commands/__init__.py +++ b/src/pip/_internal/commands/__init__.py @@ -9,7 +9,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Any, Iterable, Tuple + from typing import Any from pip._internal.cli.base_command import Command @@ -96,13 +96,6 @@ def create_command(name, **kwargs): return command -def get_summaries(): - # type: () -> Iterable[Tuple[str, str]] - """Yield command (name, summary) tuples in display order.""" - for name, command_info in commands_dict.items(): - yield (name, command_info.summary) - - def get_similar_commands(name): """Command name auto-correct.""" from difflib import get_close_matches diff --git a/tests/unit/test_commands.py b/tests/unit/test_commands.py index c300c04a3c4..324e322a3f8 100644 --- a/tests/unit/test_commands.py +++ b/tests/unit/test_commands.py @@ -1,6 +1,6 @@ import pytest -from pip._internal.commands import commands_dict, create_command, get_summaries +from pip._internal.commands import commands_dict, create_command def test_commands_dict__order(): @@ -20,12 +20,3 @@ def test_create_command(name): command = create_command(name) assert command.name == name assert command.summary == commands_dict[name].summary - - -def test_get_summaries(): - actual = list(get_summaries()) - for name, summary in actual: - assert summary == commands_dict[name].summary - - # Also check that the result is ordered correctly. - assert [item[0] for item in actual] == list(commands_dict) From c3fb5cbff004c9b070fce2bfce7b3b23e8505d80 Mon Sep 17 00:00:00 2001 From: Anthony Sottile Date: Thu, 18 Jul 2019 00:12:05 -0700 Subject: [PATCH 0038/3170] sys.version fixes for python 3.10 --- src/pip/_internal/locations.py | 2 +- src/pip/_internal/utils/misc.py | 2 +- src/pip/_internal/wheel.py | 8 +++++--- tests/lib/__init__.py | 2 +- tests/unit/test_target_python.py | 4 +--- 5 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/locations.py b/src/pip/_internal/locations.py index 5f843d797c4..5c8ac0f9d83 100644 --- a/src/pip/_internal/locations.py +++ b/src/pip/_internal/locations.py @@ -127,7 +127,7 @@ def distutils_scheme(dist_name, user=False, home=None, root=None, sys.prefix, 'include', 'site', - 'python' + sys.version[:3], + 'python{}.{}'.format(*sys.version_info), dist_name, ) diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index f094cac990d..ed3fe3199c8 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -118,7 +118,7 @@ def get_pip_version(): return ( 'pip {} from {} (python {})'.format( - __version__, pip_pkg_dir, sys.version[:3], + __version__, pip_pkg_dir, '{}.{}'.format(*sys.version_info), ) ) diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index 4b391c0092f..5b86a237adc 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -557,10 +557,10 @@ def _get_script_text(entry): generated.extend(maker.make(spec)) if os.environ.get("ENSUREPIP_OPTIONS", "") != "altinstall": - spec = 'pip%s = %s' % (sys.version[:1], pip_script) + spec = 'pip%s = %s' % (sys.version_info[0], pip_script) generated.extend(maker.make(spec)) - spec = 'pip%s = %s' % (sys.version[:3], pip_script) + spec = 'pip%s = %s' % ('{}.{}'.format(*sys.version_info), pip_script) generated.extend(maker.make(spec)) # Delete any other versioned pip entry points pip_ep = [k for k in console if re.match(r'pip(\d(\.\d)?)?$', k)] @@ -572,7 +572,9 @@ def _get_script_text(entry): spec = 'easy_install = ' + easy_install_script generated.extend(maker.make(spec)) - spec = 'easy_install-%s = %s' % (sys.version[:3], easy_install_script) + spec = 'easy_install-%s = %s' % ( + '{}.{}'.format(*sys.version_info), easy_install_script, + ) generated.extend(maker.make(spec)) # Delete any other versioned easy_install entry points easy_install_ep = [ diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index 61fcc954710..3f9dee4d927 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -29,7 +29,7 @@ DATA_DIR = Path(__file__).parent.parent.joinpath("data").abspath SRC_DIR = Path(__file__).abspath.parent.parent.parent -pyversion = sys.version[:3] +pyversion = '{}.{}'.format(*sys.version_info) pyversion_tuple = sys.version_info CURRENT_PY_VERSION_INFO = sys.version_info[:3] diff --git a/tests/unit/test_target_python.py b/tests/unit/test_target_python.py index 0cca55e13a1..66fd5d4df31 100644 --- a/tests/unit/test_target_python.py +++ b/tests/unit/test_target_python.py @@ -36,9 +36,7 @@ def test_init__py_version_info_none(self): """ Test passing py_version_info=None. """ - # Get the index of the second dot. - index = sys.version.find('.', 2) - current_major_minor = sys.version[:index] # e.g. "3.6" + current_major_minor = '{}.{}'.format(*sys.version_info) target_python = TargetPython(py_version_info=None) From 6f638eb5919302d0c543b94727a3f9080076b317 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Thu, 18 Jul 2019 13:03:01 -0400 Subject: [PATCH 0039/3170] Add get_major_minor_version(). --- src/pip/_internal/locations.py | 11 ++++++++++- src/pip/_internal/utils/misc.py | 8 ++++++-- src/pip/_internal/wheel.py | 6 +++--- tests/lib/__init__.py | 3 ++- tests/unit/test_target_python.py | 8 ++------ 5 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/pip/_internal/locations.py b/src/pip/_internal/locations.py index 5c8ac0f9d83..27eb5113168 100644 --- a/src/pip/_internal/locations.py +++ b/src/pip/_internal/locations.py @@ -23,6 +23,15 @@ USER_CACHE_DIR = appdirs.user_cache_dir("pip") +def get_major_minor_version(): + # type: () -> str + """ + Return the major-minor version of the current Python as a string, e.g. + "3.7" or "3.10". + """ + return '{}.{}'.format(*sys.version_info) + + def get_src_prefix(): if running_under_virtualenv(): src_prefix = os.path.join(sys.prefix, 'src') @@ -127,7 +136,7 @@ def distutils_scheme(dist_name, user=False, home=None, root=None, sys.prefix, 'include', 'site', - 'python{}.{}'.format(*sys.version_info), + 'python{}'.format(get_major_minor_version()), dist_name, ) diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index ed3fe3199c8..a7527266aee 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -30,7 +30,11 @@ from pip import __version__ from pip._internal.exceptions import CommandError, InstallationError -from pip._internal.locations import site_packages, user_site +from pip._internal.locations import ( + get_major_minor_version, + site_packages, + user_site, +) from pip._internal.utils.compat import ( WINDOWS, console_to_str, @@ -118,7 +122,7 @@ def get_pip_version(): return ( 'pip {} from {} (python {})'.format( - __version__, pip_pkg_dir, '{}.{}'.format(*sys.version_info), + __version__, pip_pkg_dir, get_major_minor_version(), ) ) diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index 5b86a237adc..c08e82e347c 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -29,7 +29,7 @@ InvalidWheelFilename, UnsupportedWheel, ) -from pip._internal.locations import distutils_scheme +from pip._internal.locations import distutils_scheme, get_major_minor_version from pip._internal.models.link import Link from pip._internal.utils.logging import indent_log from pip._internal.utils.marker_files import PIP_DELETE_MARKER_FILENAME @@ -560,7 +560,7 @@ def _get_script_text(entry): spec = 'pip%s = %s' % (sys.version_info[0], pip_script) generated.extend(maker.make(spec)) - spec = 'pip%s = %s' % ('{}.{}'.format(*sys.version_info), pip_script) + spec = 'pip%s = %s' % (get_major_minor_version(), pip_script) generated.extend(maker.make(spec)) # Delete any other versioned pip entry points pip_ep = [k for k in console if re.match(r'pip(\d(\.\d)?)?$', k)] @@ -573,7 +573,7 @@ def _get_script_text(entry): generated.extend(maker.make(spec)) spec = 'easy_install-%s = %s' % ( - '{}.{}'.format(*sys.version_info), easy_install_script, + get_major_minor_version(), easy_install_script, ) generated.extend(maker.make(spec)) # Delete any other versioned easy_install entry points diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index 3f9dee4d927..c0c7ea89264 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -15,6 +15,7 @@ from pip._internal.download import PipSession from pip._internal.index import PackageFinder +from pip._internal.locations import get_major_minor_version from pip._internal.models.search_scope import SearchScope from pip._internal.models.selection_prefs import SelectionPreferences from pip._internal.utils.deprecation import DEPRECATION_MSG_PREFIX @@ -29,7 +30,7 @@ DATA_DIR = Path(__file__).parent.parent.joinpath("data").abspath SRC_DIR = Path(__file__).abspath.parent.parent.parent -pyversion = '{}.{}'.format(*sys.version_info) +pyversion = get_major_minor_version() pyversion_tuple = sys.version_info CURRENT_PY_VERSION_INFO = sys.version_info[:3] diff --git a/tests/unit/test_target_python.py b/tests/unit/test_target_python.py index 66fd5d4df31..9c08427ccb6 100644 --- a/tests/unit/test_target_python.py +++ b/tests/unit/test_target_python.py @@ -1,10 +1,8 @@ -import sys - import pytest from mock import patch from pip._internal.models.target_python import TargetPython -from tests.lib import CURRENT_PY_VERSION_INFO +from tests.lib import CURRENT_PY_VERSION_INFO, pyversion class TestTargetPython: @@ -36,14 +34,12 @@ def test_init__py_version_info_none(self): """ Test passing py_version_info=None. """ - current_major_minor = '{}.{}'.format(*sys.version_info) - target_python = TargetPython(py_version_info=None) assert target_python._given_py_version_info is None assert target_python.py_version_info == CURRENT_PY_VERSION_INFO - assert target_python.py_version == current_major_minor + assert target_python.py_version == pyversion @pytest.mark.parametrize('kwargs, expected', [ ({}, ''), From f76014efac796c0656ad61bd2bbcc23b67d66495 Mon Sep 17 00:00:00 2001 From: Prabakaran Kumaresshan Date: Sun, 28 Jul 2019 23:58:35 +0530 Subject: [PATCH 0040/3170] Add global options and no user config args to make_setuptools_shim_args (#6706) --- src/pip/_internal/req/req_install.py | 34 ++++++------- src/pip/_internal/utils/setuptools_build.py | 17 +++++-- src/pip/_internal/wheel.py | 8 +-- tests/unit/test_utils.py | 54 ++++++++++++++++++--- 4 files changed, 82 insertions(+), 31 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 3cbe722f5fe..9c0e69a0bd7 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -616,9 +616,10 @@ def run_egg_info(self): 'Running setup.py (path:%s) egg_info for package from %s', self.setup_py_path, self.link, ) - base_cmd = make_setuptools_shim_args(self.setup_py_path) - if self.isolated: - base_cmd += ["--no-user-cfg"] + base_cmd = make_setuptools_shim_args( + self.setup_py_path, + no_user_config=self.isolated + ) egg_info_cmd = base_cmd + ['egg_info'] # We can't put the .egg-info files at the root, because then the # source code will be mistaken for an installed egg, causing @@ -763,19 +764,19 @@ def install_editable( # type: (...) -> None logger.info('Running setup.py develop for %s', self.name) - if self.isolated: - global_options = list(global_options) + ["--no-user-cfg"] - if prefix: prefix_param = ['--prefix={}'.format(prefix)] install_options = list(install_options) + prefix_param - + base_cmd = make_setuptools_shim_args( + self.setup_py_path, + global_options=global_options, + no_user_config=self.isolated + ) with indent_log(): # FIXME: should we do --install-headers here too? with self.build_env: call_subprocess( - make_setuptools_shim_args(self.setup_py_path) + - list(global_options) + + base_cmd + ['develop', '--no-deps'] + list(install_options), @@ -948,10 +949,6 @@ def install( install_options = list(install_options) + \ self.options.get('install_options', []) - if self.isolated: - # https://github.com/python/mypy/issues/1174 - global_options = global_options + ["--no-user-cfg"] # type: ignore - with TempDirectory(kind="record") as temp_dir: record_filename = os.path.join(temp_dir.path, 'install-record.txt') install_args = self.get_install_args( @@ -1018,10 +1015,13 @@ def get_install_args( pycompile # type: bool ): # type: (...) -> List[str] - install_args = make_setuptools_shim_args(self.setup_py_path, - unbuffered_output=True) - install_args += list(global_options) + \ - ['install', '--record', record_filename] + install_args = make_setuptools_shim_args( + self.setup_py_path, + global_options=global_options, + no_user_config=self.isolated, + unbuffered_output=True + ) + install_args += ['install', '--record', record_filename] install_args += ['--single-version-externally-managed'] if root is not None: diff --git a/src/pip/_internal/utils/setuptools_build.py b/src/pip/_internal/utils/setuptools_build.py index 589560726a2..12d866e00a0 100644 --- a/src/pip/_internal/utils/setuptools_build.py +++ b/src/pip/_internal/utils/setuptools_build.py @@ -3,7 +3,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import List + from typing import List, Sequence # Shim to wrap setup.py invocation with setuptools # @@ -20,12 +20,19 @@ ) -def make_setuptools_shim_args(setup_py_path, unbuffered_output=False): - # type: (str, bool) -> List[str] +def make_setuptools_shim_args( + setup_py_path, # type: str + global_options=None, # type: Sequence[str] + no_user_config=False, # type: bool + unbuffered_output=False # type: bool +): + # type: (...) -> List[str] """ Get setuptools command arguments with shim wrapped setup file invocation. :param setup_py_path: The path to setup.py to be wrapped. + :param global_options: Additional global options. + :param no_user_config: If True, disables personal user configuration. :param unbuffered_output: If True, adds the unbuffered switch to the argument list. """ @@ -33,4 +40,8 @@ def make_setuptools_shim_args(setup_py_path, unbuffered_output=False): if unbuffered_output: args.append('-u') args.extend(['-c', _SETUPTOOLS_SHIM.format(setup_py_path)]) + if global_options: + args.extend(global_options) + if no_user_config: + args.append('--no-user-cfg') return args diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index 4b391c0092f..b59094fdfbd 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -934,9 +934,11 @@ def _base_setup_args(self, req): # isolating. Currently, it breaks Python in virtualenvs, because it # relies on site.py to find parts of the standard library outside the # virtualenv. - base_cmd = make_setuptools_shim_args(req.setup_py_path, - unbuffered_output=True) - return base_cmd + list(self.global_options) + return make_setuptools_shim_args( + req.setup_py_path, + global_options=self.global_options, + unbuffered_output=True + ) def _build_one_pep517(self, req, tempd, python_tag=None): """Build one InstallRequirement using the PEP 517 build process. diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index e026c2d9f58..31d2b383cca 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -1405,16 +1405,54 @@ def test_deprecated_message_reads_well(): ) -@pytest.mark.parametrize("unbuffered_output", [False, True]) -def test_make_setuptools_shim_args(unbuffered_output): +def test_make_setuptools_shim_args(): + # Test all arguments at once, including the overall ordering. args = make_setuptools_shim_args( - "/dir/path/setup.py", - unbuffered_output=unbuffered_output + '/dir/path/setup.py', + global_options=['--some', '--option'], + no_user_config=True, + unbuffered_output=True, + ) + + assert args[1:3] == ['-u', '-c'] + # Spot-check key aspects of the command string. + assert "sys.argv[0] = '/dir/path/setup.py'" in args[3] + assert "__file__='/dir/path/setup.py'" in args[3] + assert args[4:] == ['--some', '--option', '--no-user-cfg'] + + +@pytest.mark.parametrize('global_options', [ + None, + [], + ['--some', '--option'] +]) +def test_make_setuptools_shim_args__global_options(global_options): + args = make_setuptools_shim_args( + '/dir/path/setup.py', + global_options=global_options, ) - assert ("-u" in args) == unbuffered_output + if global_options: + assert len(args) == 5 + for option in global_options: + assert option in args + else: + assert len(args) == 3 - assert args[-2] == "-c" - assert "sys.argv[0] = '/dir/path/setup.py'" in args[-1] - assert "__file__='/dir/path/setup.py'" in args[-1] +@pytest.mark.parametrize('no_user_config', [False, True]) +def test_make_setuptools_shim_args__no_user_config(no_user_config): + args = make_setuptools_shim_args( + '/dir/path/setup.py', + no_user_config=no_user_config, + ) + assert ('--no-user-cfg' in args) == no_user_config + + +@pytest.mark.parametrize('unbuffered_output', [False, True]) +def test_make_setuptools_shim_args__unbuffered_output(unbuffered_output): + args = make_setuptools_shim_args( + '/dir/path/setup.py', + unbuffered_output=unbuffered_output + ) + assert ('-u' in args) == unbuffered_output From 365d602e3a0bc64fc764299977207b39ca114ef0 Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara Date: Sun, 21 Jul 2019 15:17:24 -0400 Subject: [PATCH 0041/3170] Update tox docs-building Python version to 3.7. --- news/080c4888-abed-11e9-a614-b72e6663bd8a.trivial | 0 tox.ini | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 news/080c4888-abed-11e9-a614-b72e6663bd8a.trivial diff --git a/news/080c4888-abed-11e9-a614-b72e6663bd8a.trivial b/news/080c4888-abed-11e9-a614-b72e6663bd8a.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tox.ini b/tox.ini index 512bad52ded..91293fc2002 100644 --- a/tox.ini +++ b/tox.ini @@ -30,7 +30,7 @@ commands = pytest --timeout 300 --cov=pip --cov-report=term-missing --cov-report [testenv:docs] # Don't skip install here since pip_sphinxext uses pip's internals. deps = -r{toxinidir}/tools/docs-requirements.txt -basepython = python3.6 +basepython = python3.7 commands = sphinx-build -W -d {envtmpdir}/doctrees/html -b html docs/html docs/build/html # Having the conf.py in the docs/html is weird but needed because we From fc976bb32b99e7e2047c614090ceddc8293dbfc7 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Mon, 29 Jul 2019 14:27:28 +0530 Subject: [PATCH 0042/3170] Utilize "pip_install_local" where possible --- tests/functional/test_list.py | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/tests/functional/test_list.py b/tests/functional/test_list.py index 16b725ddee7..764d326f7b8 100644 --- a/tests/functional/test_list.py +++ b/tests/functional/test_list.py @@ -513,11 +513,10 @@ def test_list_path(tmpdir, script, data): Test list with --path. """ result = script.pip('list', '--path', tmpdir, '--format=json') - assert {'name': 'simple', - 'version': '2.0'} not in json.loads(result.stdout) + json_result = json.loads(result.stdout) + assert {'name': 'simple', 'version': '2.0'} not in json_result - script.pip('install', '--find-links', data.find_links, - '--target', tmpdir, 'simple==2.0') + script.pip_install_local('--target', tmpdir, 'simple==2.0') result = script.pip('list', '--path', tmpdir, '--format=json') json_result = json.loads(result.stdout) assert {'name': 'simple', 'version': '2.0'} in json_result @@ -528,10 +527,9 @@ def test_list_path_exclude_user(tmpdir, script, data): Test list with --path and make sure packages from --user are not picked up. """ - script.pip_install_local('--find-links', data.find_links, - '--user', 'simple2') - script.pip('install', '--find-links', data.find_links, - '--target', tmpdir, 'simple==1.0') + script.pip_install_local('--user', 'simple2') + script.pip_install_local('--target', tmpdir, 'simple==1.0') + result = script.pip('list', '--user', '--format=json') json_result = json.loads(result.stdout) assert {'name': 'simple2', 'version': '3.0'} in json_result @@ -549,10 +547,10 @@ def test_list_path_multiple(tmpdir, script, data): os.mkdir(path1) path2 = tmpdir / "path2" os.mkdir(path2) - script.pip('install', '--find-links', data.find_links, - '--target', path1, 'simple==2.0') - script.pip('install', '--find-links', data.find_links, - '--target', path2, 'simple2==3.0') + + script.pip_install_local('--target', path1, 'simple==2.0') + script.pip_install_local('--target', path2, 'simple2==3.0') + result = script.pip('list', '--path', path1, '--format=json') json_result = json.loads(result.stdout) assert {'name': 'simple', 'version': '2.0'} in json_result From 4b8f185fa8db62d06aefd4f55668b720e2cd6e50 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Mon, 29 Jul 2019 14:27:46 +0530 Subject: [PATCH 0043/3170] Mark a network dependent test as network --- tests/functional/test_pep517.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/functional/test_pep517.py b/tests/functional/test_pep517.py index 6abbad5d072..c71d1a40102 100644 --- a/tests/functional/test_pep517.py +++ b/tests/functional/test_pep517.py @@ -1,3 +1,4 @@ +import pytest from pip._vendor import pytoml from pip._internal.build_env import BuildEnvironment @@ -198,6 +199,7 @@ def test_explicit_setuptools_backend(script, tmpdir, data, common_wheels): result.assert_installed(name, editable=False) +@pytest.mark.network def test_pep517_and_build_options(script, tmpdir, data, common_wheels): """Backend generated requirements are installed in the build env""" project_dir, name = make_pyproject_with_setup(tmpdir) From ec725cb1a04f1128e39f2adb7fbe74e6cfa77fb4 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Mon, 29 Jul 2019 20:29:34 +0530 Subject: [PATCH 0044/3170] Populate InstallRequirement.link before preparing it --- src/pip/_internal/legacy_resolve.py | 6 ++++-- src/pip/_internal/operations/prepare.py | 22 +++++++--------------- 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/pip/_internal/legacy_resolve.py b/src/pip/_internal/legacy_resolve.py index 2f629b39d11..48c8d4e5a4e 100644 --- a/src/pip/_internal/legacy_resolve.py +++ b/src/pip/_internal/legacy_resolve.py @@ -308,9 +308,11 @@ def _get_abstract_dist_for(self, req): ) upgrade_allowed = self._is_upgrade_allowed(req) + + # We eagerly populate the link, since that's our "legacy" behavior. + req.populate_link(self.finder, upgrade_allowed, self.require_hashes) abstract_dist = self.preparer.prepare_linked_requirement( - req, self.session, self.finder, upgrade_allowed, - self.require_hashes + req, self.session, self.finder, self.require_hashes ) # NOTE diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 471a60ce828..5ead5d858fc 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -104,18 +104,19 @@ def prepare_linked_requirement( req, # type: InstallRequirement session, # type: PipSession finder, # type: PackageFinder - upgrade_allowed, # type: bool - require_hashes # type: bool + require_hashes, # type: bool ): # type: (...) -> AbstractDistribution """Prepare a requirement that would be obtained from req.link """ + assert req.link + # TODO: Breakup into smaller functions - if req.link and req.link.scheme == 'file': + if req.link.scheme == 'file': path = url_to_path(req.link.url) logger.info('Processing %s', display_path(path)) else: - logger.info('Collecting %s', req) + logger.info('Collecting %s', req.req or req) with indent_log(): # @@ if filesystem packages are not marked @@ -138,16 +139,7 @@ def prepare_linked_requirement( "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 @@ -214,7 +206,7 @@ def prepare_linked_requirement( raise InstallationError( 'Could not install requirement %s because of HTTP ' 'error %s for URL %s' % - (req, exc, req.link) + (req, exc, link) ) abstract_dist = make_distribution_for_install_requirement(req) with self.req_tracker.track(req): @@ -223,7 +215,7 @@ def prepare_linked_requirement( ) if self._download_should_save: # Make a .zip of the source_dir we already created. - if not req.link.is_artifact: + if not link.is_artifact: req.archive(self.download_dir) return abstract_dist From b43c71cbc76b6f224c895dd9578e25fe5ae5cd1f Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Mon, 29 Jul 2019 20:30:55 +0530 Subject: [PATCH 0045/3170] Simplify references to req.link --- src/pip/_internal/operations/prepare.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 5ead5d858fc..bac5ede89bb 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -110,10 +110,11 @@ def prepare_linked_requirement( """Prepare a requirement that would be obtained from req.link """ assert req.link + link = req.link # TODO: Breakup into smaller functions - if req.link.scheme == 'file': - path = url_to_path(req.link.url) + if link.scheme == 'file': + path = url_to_path(link.url) logger.info('Processing %s', display_path(path)) else: logger.info('Collecting %s', req.req or req) @@ -140,8 +141,6 @@ def prepare_linked_requirement( % (req, req.source_dir) ) - 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 @@ -178,11 +177,11 @@ def prepare_linked_requirement( 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: + if 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 link.is_wheel: if download_dir: # When downloading, we only unpack wheels to get # metadata. @@ -192,7 +191,7 @@ def prepare_linked_requirement( # wheel. autodelete_unpacked = False unpack_url( - req.link, req.source_dir, + link, req.source_dir, download_dir, autodelete_unpacked, session=session, hashes=hashes, progress_bar=self.progress_bar From 145a753cd3edb9478ea0947b3be62f3470817672 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Mon, 29 Jul 2019 20:31:52 +0530 Subject: [PATCH 0046/3170] Update tests for eagerly populated links --- tests/functional/test_install.py | 2 +- tests/functional/test_install_reqs.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index bae1e7f33fb..084b08ed0dd 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -600,7 +600,7 @@ def test_install_global_option(script): result = script.pip( 'install', '--global-option=--version', "INITools==0.1", expect_stderr=True) - assert '0.1\n' in result.stdout + assert 'INITools==0.1\n' in result.stdout def test_install_with_hacked_egg_info(script, data): diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index 14368833186..d2b23df036a 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -162,7 +162,7 @@ def test_respect_order_in_requirements_file(script, data): ) downloaded = [line for line in result.stdout.split('\n') - if 'Collecting' in line] + if 'Processing' in line] assert 'parent' in downloaded[0], ( 'First download should be "parent" but was "%s"' % downloaded[0] From ebdde3d17d9d9074a4b53453bdb667c87e97c2ad Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Mon, 29 Jul 2019 17:27:52 +0530 Subject: [PATCH 0047/3170] Bump mypy version --- tools/mypy-requirements.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/mypy-requirements.txt b/tools/mypy-requirements.txt index ac69c5ad480..50615d913be 100644 --- a/tools/mypy-requirements.txt +++ b/tools/mypy-requirements.txt @@ -1 +1 @@ -mypy == 0.670 +mypy == 0.720 From 3b98423981a4fc60f4dcbf4b0291eb7ee75744ff Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Mon, 29 Jul 2019 20:17:06 +0530 Subject: [PATCH 0048/3170] Work around mypy quirks --- src/pip/_internal/pep425tags.py | 7 +++++-- src/pip/_internal/utils/appdirs.py | 4 +++- src/pip/_internal/utils/misc.py | 4 ++-- 3 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index 07dc148eec3..4dc381619e2 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -105,6 +105,8 @@ def get_abi_tag(): (CPython 2, PyPy).""" soabi = get_config_var('SOABI') impl = get_abbr_impl() + abi = None # type: Optional[str] + if not soabi and impl in {'cp', 'pp'} and hasattr(sys, 'maxunicode'): d = '' m = '' @@ -129,8 +131,7 @@ def get_abi_tag(): abi = 'cp' + soabi.split('-')[1] elif soabi: abi = soabi.replace('.', '_').replace('-', '_') - else: - abi = None + return abi @@ -353,6 +354,7 @@ def get_supported( # Current version, current API (built specifically for our Python): for abi in abis: + assert abi for arch in arches: supported.append(('%s%s' % (impl, versions[0]), abi, arch)) @@ -362,6 +364,7 @@ def get_supported( if version in {'31', '30'}: break for abi in abi3s: # empty set if not Python 3 + assert abi for arch in arches: supported.append(("%s%s" % (impl, version), abi, arch)) diff --git a/src/pip/_internal/utils/appdirs.py b/src/pip/_internal/utils/appdirs.py index fb2611104c8..b0953cab873 100644 --- a/src/pip/_internal/utils/appdirs.py +++ b/src/pip/_internal/utils/appdirs.py @@ -239,7 +239,9 @@ def _get_win_folder_with_ctypes(csidl_name): if ctypes.windll.kernel32.GetShortPathNameW(buf.value, buf2, 1024): buf = buf2 - return buf.value + # On Python 2, ctypes.create_unicode_buffer().value returns "unicode", + # which isn't the same as str in the annotation above. + return buf.value # type: ignore if WINDOWS: diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index f094cac990d..156ae0a964f 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -65,9 +65,9 @@ except ImportError: # typing's cast() isn't supported in code comments, so we need to # define a dummy, no-op version. - def cast(typ, val): + def cast(typ, val): # type: ignore return val - VersionInfo = None + VersionInfo = None # type: ignore __all__ = ['rmtree', 'display_path', 'backup_dir', From 80b82b52158f288e97d5b6abe95e0b574ca51d93 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Tue, 30 Jul 2019 10:10:13 +0530 Subject: [PATCH 0049/3170] Address review comments --- src/pip/_internal/pep425tags.py | 6 ++---- src/pip/_internal/utils/appdirs.py | 5 +++-- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index 4dc381619e2..03a906b94bc 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -16,7 +16,7 @@ if MYPY_CHECK_RUNNING: from typing import ( - Tuple, Callable, List, Optional, Union, Dict + Tuple, Callable, List, Optional, Union, Dict, Set ) Pep425Tag = Tuple[str, str, str] @@ -311,7 +311,7 @@ def get_supported( if abi: abis[0:0] = [abi] - abi3s = set() + abi3s = set() # type: Set[str] for suffix in get_extension_suffixes(): if suffix.startswith('.abi'): abi3s.add(suffix.split('.', 2)[1]) @@ -354,7 +354,6 @@ def get_supported( # Current version, current API (built specifically for our Python): for abi in abis: - assert abi for arch in arches: supported.append(('%s%s' % (impl, versions[0]), abi, arch)) @@ -364,7 +363,6 @@ def get_supported( if version in {'31', '30'}: break for abi in abi3s: # empty set if not Python 3 - assert abi for arch in arches: supported.append(("%s%s" % (impl, version), abi, arch)) diff --git a/src/pip/_internal/utils/appdirs.py b/src/pip/_internal/utils/appdirs.py index b0953cab873..dcd757c7e05 100644 --- a/src/pip/_internal/utils/appdirs.py +++ b/src/pip/_internal/utils/appdirs.py @@ -218,6 +218,8 @@ def _get_win_folder_from_registry(csidl_name): def _get_win_folder_with_ctypes(csidl_name): # type: (str) -> str + # On Python 2, ctypes.create_unicode_buffer().value returns "unicode", + # which isn't the same as str in the annotation above. csidl_const = { "CSIDL_APPDATA": 26, "CSIDL_COMMON_APPDATA": 35, @@ -239,8 +241,7 @@ def _get_win_folder_with_ctypes(csidl_name): if ctypes.windll.kernel32.GetShortPathNameW(buf.value, buf2, 1024): buf = buf2 - # On Python 2, ctypes.create_unicode_buffer().value returns "unicode", - # which isn't the same as str in the annotation above. + # The type: ignore is explained under the type annotation for this function return buf.value # type: ignore From 2a5c23b3d5c47ebdb8c30aaf84aad36a645a1482 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Tue, 30 Jul 2019 10:16:57 +0530 Subject: [PATCH 0050/3170] Simplify the handling of "typing.cast" --- src/pip/_internal/utils/misc.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 156ae0a964f..a08669a4b85 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -53,21 +53,18 @@ if MYPY_CHECK_RUNNING: from typing import ( Any, AnyStr, Container, Iterable, List, Mapping, Match, Optional, Text, - Union, + Tuple, Union, cast, ) from pip._vendor.pkg_resources import Distribution from pip._internal.models.link import Link from pip._internal.utils.ui import SpinnerInterface -try: - from typing import cast, Tuple VersionInfo = Tuple[int, int, int] -except ImportError: - # typing's cast() isn't supported in code comments, so we need to - # define a dummy, no-op version. - def cast(typ, val): # type: ignore - return val - VersionInfo = None # type: ignore +else: + # typing's cast() is needed at runtime, but we don't want to import typing. + # Thus, we use a dummy no-op version, which we tell mypy to ignore. + def cast(type_, value): # type: ignore + return value __all__ = ['rmtree', 'display_path', 'backup_dir', @@ -140,7 +137,7 @@ def normalize_version_info(py_version_info): elif len(py_version_info) > 3: py_version_info = py_version_info[:3] - return cast(VersionInfo, py_version_info) + return cast('VersionInfo', py_version_info) def ensure_dir(path): From 9214099f09bed1800bce603d294621641959cb56 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Tue, 30 Jul 2019 12:28:20 +0530 Subject: [PATCH 0051/3170] Revert "Ignore new warnings traced on tox invocation." --- .appveyor.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 2819e052120..4aa5e217bc3 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -65,9 +65,6 @@ test_script: subst T: $env:TEMP $env:TEMP = "T:\" $env:TMP = "T:\" - # Workaround warnings traced in packaging.requirements with pyparsing 2.4.1. - # See pypa/packaging#170 and tox-dev/tox#1375. - $env:PYTHONWARNINGS = "ignore:warn_ungrouped_named_tokens_in_collection" tox -e py -- -m unit if ($LastExitCode -eq 0 -and $env:RUN_INTEGRATION_TESTS -eq "True") { tox -e py -- --use-venv -m integration -n2 --durations=20 From f377148f6de201c6f0e23baea340e0be69518325 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Sat, 20 Jul 2019 12:06:59 +0530 Subject: [PATCH 0052/3170] Use mypy's inline configuration syntax for opt-outs --- setup.cfg | 75 ---------------------- src/pip/_internal/build_env.py | 3 + src/pip/_internal/cache.py | 3 + src/pip/_internal/cli/base_command.py | 4 ++ src/pip/_internal/cli/cmdoptions.py | 5 +- src/pip/_internal/configuration.py | 3 + src/pip/_internal/index.py | 4 ++ src/pip/_internal/legacy_resolve.py | 3 + src/pip/_internal/locations.py | 4 ++ src/pip/_internal/models/format_control.py | 3 + src/pip/_internal/operations/check.py | 3 + src/pip/_internal/operations/freeze.py | 3 + src/pip/_internal/operations/prepare.py | 3 + src/pip/_internal/req/__init__.py | 3 + src/pip/_internal/req/constructors.py | 3 + src/pip/_internal/req/req_file.py | 3 + src/pip/_internal/req/req_install.py | 3 + src/pip/_internal/req/req_set.py | 3 + src/pip/_internal/req/req_tracker.py | 3 + src/pip/_internal/utils/encoding.py | 3 + src/pip/_internal/utils/glibc.py | 3 + src/pip/_internal/utils/misc.py | 3 + src/pip/_internal/utils/ui.py | 3 + src/pip/_internal/wheel.py | 4 ++ 24 files changed, 74 insertions(+), 76 deletions(-) diff --git a/setup.cfg b/setup.cfg index cb6d790a961..cd5a2b256d6 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,81 +26,6 @@ ignore = W504 follow_imports = silent ignore_missing_imports = True -[mypy-pip/_internal/build_env] -strict_optional = False - -[mypy-pip/_internal/cache] -strict_optional = False - -[mypy-pip/_internal/cli/base_command] -strict_optional = False - -[mypy-pip/_internal/cli/cmdoptions] -strict_optional = False - -[mypy-pip/_internal/configuration] -strict_optional = False - -[mypy-pip/_internal/index] -strict_optional = False - -[mypy-pip/_internal/legacy_resolve] -strict_optional = False - -[mypy-pip/_internal/locations] -strict_optional = False - -[mypy-pip/_internal/models/format_control] -strict_optional = False - -[mypy-pip/_internal/operations/check] -strict_optional = False - -[mypy-pip/_internal/operations/freeze] -strict_optional = False - -[mypy-pip/_internal/operations/prepare] -strict_optional = False - -[mypy-pip/_internal/pep425tags] -strict_optional = False - -[mypy-pip/_internal/req/*] -disallow_untyped_defs = True - -[mypy-pip/_internal/req] -strict_optional = False - -[mypy-pip/_internal/req/constructors] -strict_optional = False - -[mypy-pip/_internal/req/req_file] -strict_optional = False - -[mypy-pip/_internal/req/req_install] -strict_optional = False - -[mypy-pip/_internal/req/req_set] -strict_optional = False - -[mypy-pip/_internal/req/req_tracker] -strict_optional = False - -[mypy-pip/_internal/utils/encoding] -strict_optional = False - -[mypy-pip/_internal/utils/glibc] -strict_optional = False - -[mypy-pip/_internal/utils/misc] -strict_optional = False - -[mypy-pip/_internal/utils/ui] -strict_optional = False - -[mypy-pip/_internal/wheel] -strict_optional = False - [mypy-pip/_vendor/*] follow_imports = skip ignore_errors = True diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py index a060ceea2ca..5decc91050e 100644 --- a/src/pip/_internal/build_env.py +++ b/src/pip/_internal/build_env.py @@ -1,6 +1,9 @@ """Build Environment used for isolation during sdist building """ +# The following comment should be removed at some point in the future. +# mypy: strict-optional=False + import logging import os import sys diff --git a/src/pip/_internal/cache.py b/src/pip/_internal/cache.py index 894624c1dbc..0e893cd45a7 100644 --- a/src/pip/_internal/cache.py +++ b/src/pip/_internal/cache.py @@ -1,6 +1,9 @@ """Cache Management """ +# The following comment should be removed at some point in the future. +# mypy: strict-optional=False + import errno import hashlib import logging diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index fd771b1412c..2ba19f944b0 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -1,4 +1,8 @@ """Base Command class, and related routines""" + +# The following comment should be removed at some point in the future. +# mypy: strict-optional=False + from __future__ import absolute_import, print_function import logging diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index ecf4d20bcd6..ea4d4de5cb1 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -5,8 +5,11 @@ globally. One reason being that options with action='append' can carry state between parses. pip parses general options twice internally, and shouldn't pass on state. To be consistent, all options will follow this design. - """ + +# The following comment should be removed at some point in the future. +# mypy: strict-optional=False + from __future__ import absolute_import import logging diff --git a/src/pip/_internal/configuration.py b/src/pip/_internal/configuration.py index 10edb4829ab..6843557855f 100644 --- a/src/pip/_internal/configuration.py +++ b/src/pip/_internal/configuration.py @@ -11,6 +11,9 @@ A single word describing where the configuration key-value pair came from """ +# The following comment should be removed at some point in the future. +# mypy: strict-optional=False + import locale import logging import os diff --git a/src/pip/_internal/index.py b/src/pip/_internal/index.py index 75fd4e005ff..c5bc3bc3428 100644 --- a/src/pip/_internal/index.py +++ b/src/pip/_internal/index.py @@ -1,4 +1,8 @@ """Routines related to PyPI, indexes""" + +# The following comment should be removed at some point in the future. +# mypy: strict-optional=False + from __future__ import absolute_import import cgi diff --git a/src/pip/_internal/legacy_resolve.py b/src/pip/_internal/legacy_resolve.py index 2f629b39d11..f52db418b21 100644 --- a/src/pip/_internal/legacy_resolve.py +++ b/src/pip/_internal/legacy_resolve.py @@ -10,6 +10,9 @@ a. "first found, wins" (where the order is breadth first) """ +# The following comment should be removed at some point in the future. +# mypy: strict-optional=False + import logging import sys from collections import defaultdict diff --git a/src/pip/_internal/locations.py b/src/pip/_internal/locations.py index 5f843d797c4..7fbc0ad8907 100644 --- a/src/pip/_internal/locations.py +++ b/src/pip/_internal/locations.py @@ -1,4 +1,8 @@ """Locations where we look for configs, install stuff, etc""" + +# The following comment should be removed at some point in the future. +# mypy: strict-optional=False + from __future__ import absolute_import import os diff --git a/src/pip/_internal/models/format_control.py b/src/pip/_internal/models/format_control.py index 53138e48eca..c08c30f83f9 100644 --- a/src/pip/_internal/models/format_control.py +++ b/src/pip/_internal/models/format_control.py @@ -1,3 +1,6 @@ +# The following comment should be removed at some point in the future. +# mypy: strict-optional=False + from pip._vendor.packaging.utils import canonicalize_name from pip._internal.utils.typing import MYPY_CHECK_RUNNING diff --git a/src/pip/_internal/operations/check.py b/src/pip/_internal/operations/check.py index 7b8b369fc90..3bc9f8107ea 100644 --- a/src/pip/_internal/operations/check.py +++ b/src/pip/_internal/operations/check.py @@ -1,6 +1,9 @@ """Validation of dependencies of packages """ +# The following comment should be removed at some point in the future. +# mypy: strict-optional=False + import logging from collections import namedtuple diff --git a/src/pip/_internal/operations/freeze.py b/src/pip/_internal/operations/freeze.py index b4193730a03..6eaa85b19ec 100644 --- a/src/pip/_internal/operations/freeze.py +++ b/src/pip/_internal/operations/freeze.py @@ -1,3 +1,6 @@ +# The following comment should be removed at some point in the future. +# mypy: strict-optional=False + from __future__ import absolute_import import collections diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 471a60ce828..2449e8efc28 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -1,6 +1,9 @@ """Prepares a distribution for installation """ +# The following comment should be removed at some point in the future. +# mypy: strict-optional=False + import logging import os diff --git a/src/pip/_internal/req/__init__.py b/src/pip/_internal/req/__init__.py index c39f63fa831..9955a716ce2 100644 --- a/src/pip/_internal/req/__init__.py +++ b/src/pip/_internal/req/__init__.py @@ -1,3 +1,6 @@ +# The following comment should be removed at some point in the future. +# mypy: strict-optional=False + from __future__ import absolute_import import logging diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index cd0ab504282..28cef933221 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -8,6 +8,9 @@ InstallRequirement. """ +# The following comment should be removed at some point in the future. +# mypy: strict-optional=False + import logging import os import re diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index 3e39f9603ed..65772b20a83 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -2,6 +2,9 @@ Requirements file parsing """ +# The following comment should be removed at some point in the future. +# mypy: strict-optional=False + from __future__ import absolute_import import optparse diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 9c0e69a0bd7..4752fa88816 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -1,3 +1,6 @@ +# The following comment should be removed at some point in the future. +# mypy: strict-optional=False + from __future__ import absolute_import import logging diff --git a/src/pip/_internal/req/req_set.py b/src/pip/_internal/req/req_set.py index d1966a4aa9c..0b81ed215b0 100644 --- a/src/pip/_internal/req/req_set.py +++ b/src/pip/_internal/req/req_set.py @@ -1,3 +1,6 @@ +# The following comment should be removed at some point in the future. +# mypy: strict-optional=False + from __future__ import absolute_import import logging diff --git a/src/pip/_internal/req/req_tracker.py b/src/pip/_internal/req/req_tracker.py index e36a3f6b5c2..bef30f236af 100644 --- a/src/pip/_internal/req/req_tracker.py +++ b/src/pip/_internal/req/req_tracker.py @@ -1,3 +1,6 @@ +# The following comment should be removed at some point in the future. +# mypy: strict-optional=False + from __future__ import absolute_import import contextlib diff --git a/src/pip/_internal/utils/encoding.py b/src/pip/_internal/utils/encoding.py index 30139f2e591..ab4d4b98e3e 100644 --- a/src/pip/_internal/utils/encoding.py +++ b/src/pip/_internal/utils/encoding.py @@ -1,3 +1,6 @@ +# The following comment should be removed at some point in the future. +# mypy: strict-optional=False + import codecs import locale import re diff --git a/src/pip/_internal/utils/glibc.py b/src/pip/_internal/utils/glibc.py index aa77d9b60f8..544b4c2792b 100644 --- a/src/pip/_internal/utils/glibc.py +++ b/src/pip/_internal/utils/glibc.py @@ -1,3 +1,6 @@ +# The following comment should be removed at some point in the future. +# mypy: strict-optional=False + from __future__ import absolute_import import os diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index a08669a4b85..33b0653a0a8 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -1,3 +1,6 @@ +# The following comment should be removed at some point in the future. +# mypy: strict-optional=False + from __future__ import absolute_import import contextlib diff --git a/src/pip/_internal/utils/ui.py b/src/pip/_internal/utils/ui.py index 46390f4a598..80cdab37912 100644 --- a/src/pip/_internal/utils/ui.py +++ b/src/pip/_internal/utils/ui.py @@ -1,3 +1,6 @@ +# The following comment should be removed at some point in the future. +# mypy: strict-optional=False + from __future__ import absolute_import, division import contextlib diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index b59094fdfbd..35c73210bbf 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -1,6 +1,10 @@ """ Support for installing and building the "wheel" binary package format. """ + +# The following comment should be removed at some point in the future. +# mypy: strict-optional=False + from __future__ import absolute_import import collections From c6da85a6ebde3c6d6a1d0bcc82e994514fe79c72 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Tue, 30 Jul 2019 17:01:05 +0530 Subject: [PATCH 0053/3170] Move requirement files to tools/requirements/ --- tools/{docs-requirements.txt => requirements/docs.txt} | 0 tools/{lint-requirements.txt => requirements/lint.txt} | 0 tools/{mypy-requirements.txt => requirements/mypy.txt} | 0 .../tests-common_wheels.txt} | 0 .../{tests-requirements.txt => requirements/tests.txt} | 0 tox.ini | 10 +++++----- 6 files changed, 5 insertions(+), 5 deletions(-) rename tools/{docs-requirements.txt => requirements/docs.txt} (100%) rename tools/{lint-requirements.txt => requirements/lint.txt} (100%) rename tools/{mypy-requirements.txt => requirements/mypy.txt} (100%) rename tools/{tests-common_wheels-requirements.txt => requirements/tests-common_wheels.txt} (100%) rename tools/{tests-requirements.txt => requirements/tests.txt} (100%) diff --git a/tools/docs-requirements.txt b/tools/requirements/docs.txt similarity index 100% rename from tools/docs-requirements.txt rename to tools/requirements/docs.txt diff --git a/tools/lint-requirements.txt b/tools/requirements/lint.txt similarity index 100% rename from tools/lint-requirements.txt rename to tools/requirements/lint.txt diff --git a/tools/mypy-requirements.txt b/tools/requirements/mypy.txt similarity index 100% rename from tools/mypy-requirements.txt rename to tools/requirements/mypy.txt diff --git a/tools/tests-common_wheels-requirements.txt b/tools/requirements/tests-common_wheels.txt similarity index 100% rename from tools/tests-common_wheels-requirements.txt rename to tools/requirements/tests-common_wheels.txt diff --git a/tools/tests-requirements.txt b/tools/requirements/tests.txt similarity index 100% rename from tools/tests-requirements.txt rename to tools/requirements/tests.txt diff --git a/tox.ini b/tox.ini index 512bad52ded..879c234e809 100644 --- a/tox.ini +++ b/tox.ini @@ -15,10 +15,10 @@ setenv = # This is required in order to get UTF-8 output inside of the subprocesses # that our tests use. LC_CTYPE = en_US.UTF-8 -deps = -r{toxinidir}/tools/tests-requirements.txt +deps = -r{toxinidir}/tools/requirements/tests.txt commands_pre = python -c 'import shutil, sys; shutil.rmtree(sys.argv[1], ignore_errors=True)' {toxinidir}/tests/data/common_wheels - {[helpers]pip} wheel -w {toxinidir}/tests/data/common_wheels -r {toxinidir}/tools/tests-common_wheels-requirements.txt + {[helpers]pip} wheel -w {toxinidir}/tests/data/common_wheels -r {toxinidir}/tools/requirements/tests-common_wheels.txt commands = pytest --timeout 300 [] install_command = {[helpers]pip} install {opts} {packages} list_dependencies_command = {[helpers]pip} freeze --all @@ -29,7 +29,7 @@ commands = pytest --timeout 300 --cov=pip --cov-report=term-missing --cov-report [testenv:docs] # Don't skip install here since pip_sphinxext uses pip's internals. -deps = -r{toxinidir}/tools/docs-requirements.txt +deps = -r{toxinidir}/tools/requirements/docs.txt basepython = python3.6 commands = sphinx-build -W -d {envtmpdir}/doctrees/html -b html docs/html docs/build/html @@ -50,7 +50,7 @@ commands = python setup.py check -m -r -s [lint] -deps = -r{toxinidir}/tools/lint-requirements.txt +deps = -r{toxinidir}/tools/requirements/lint.txt [testenv:lint-py2] skip_install = True @@ -74,7 +74,7 @@ commands = [testenv:mypy] skip_install = True basepython = python3 -deps = -r{toxinidir}/tools/mypy-requirements.txt +deps = -r{toxinidir}/tools/requirements/mypy.txt commands_pre = commands = mypy src From c9b6d252a8ffbbbe932853192d0187e457eb20cb Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Tue, 30 Jul 2019 17:41:22 +0530 Subject: [PATCH 0054/3170] Move invoke commands to tools.automation --- tasks/__init__.py | 3 +-- {tasks => tools/automation}/generate.py | 0 {tasks => tools/automation}/vendoring/__init__.py | 0 {tasks => tools/automation}/vendoring/patches/appdirs.patch | 0 {tasks => tools/automation}/vendoring/patches/html5lib.patch | 0 {tasks => tools/automation}/vendoring/patches/requests.patch | 0 6 files changed, 1 insertion(+), 2 deletions(-) rename {tasks => tools/automation}/generate.py (100%) rename {tasks => tools/automation}/vendoring/__init__.py (100%) rename {tasks => tools/automation}/vendoring/patches/appdirs.patch (100%) rename {tasks => tools/automation}/vendoring/patches/html5lib.patch (100%) rename {tasks => tools/automation}/vendoring/patches/requests.patch (100%) diff --git a/tasks/__init__.py b/tasks/__init__.py index 5f1a4aff63a..ecde439968e 100644 --- a/tasks/__init__.py +++ b/tasks/__init__.py @@ -1,6 +1,5 @@ import invoke -from . import generate -from . import vendoring +from tools.automation import generate, vendoring ns = invoke.Collection(generate, vendoring) diff --git a/tasks/generate.py b/tools/automation/generate.py similarity index 100% rename from tasks/generate.py rename to tools/automation/generate.py diff --git a/tasks/vendoring/__init__.py b/tools/automation/vendoring/__init__.py similarity index 100% rename from tasks/vendoring/__init__.py rename to tools/automation/vendoring/__init__.py diff --git a/tasks/vendoring/patches/appdirs.patch b/tools/automation/vendoring/patches/appdirs.patch similarity index 100% rename from tasks/vendoring/patches/appdirs.patch rename to tools/automation/vendoring/patches/appdirs.patch diff --git a/tasks/vendoring/patches/html5lib.patch b/tools/automation/vendoring/patches/html5lib.patch similarity index 100% rename from tasks/vendoring/patches/html5lib.patch rename to tools/automation/vendoring/patches/html5lib.patch diff --git a/tasks/vendoring/patches/requests.patch b/tools/automation/vendoring/patches/requests.patch similarity index 100% rename from tasks/vendoring/patches/requests.patch rename to tools/automation/vendoring/patches/requests.patch From d4171f28d18cd7e6af7b5b0712fa3c6b632435b1 Mon Sep 17 00:00:00 2001 From: Omry Yadan Date: Tue, 30 Jul 2019 17:52:12 -0700 Subject: [PATCH 0055/3170] minor updates --- news/6770.bugfix | 4 +--- src/pip/_internal/download.py | 4 ++-- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/news/6770.bugfix b/news/6770.bugfix index 174b8f62c8a..c0ab57ee109 100644 --- a/news/6770.bugfix +++ b/news/6770.bugfix @@ -1,3 +1 @@ -'pip install .' is very slow in the presence of large directories in the source tree. -#6770 excludes .tox and .nox from being copied by 'pip install .', significantly speeding -up the builds when those directories are large. +Skip copying .tox and .nox directories to temporary build directories diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index fbaaffaf6f9..f58ba475c06 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -934,7 +934,6 @@ def unpack_file_url( of the link file inside download_dir. """ link_path = url_to_path(link.url_without_fragment) - # If it's a url to a local directory if is_dir_url(link): if os.path.isdir(location): @@ -944,7 +943,8 @@ def unpack_file_url( symlinks=True, # Pulling in those directories can potentially # be very slow. - # see dicsussion at: + # see discussion at: + # https://github.com/pypa/pip/pull/6770 # https://github.com/pypa/pip/issues/2195 # https://github.com/pypa/pip/pull/2196 ignore=shutil.ignore_patterns('.tox', '.nox')) From 66d5486b9d17c4e94ae9646f17c20c040c1699b9 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Wed, 31 Jul 2019 12:29:04 +0530 Subject: [PATCH 0056/3170] Update Python versions on Travis CI --- .travis.yml | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index ba0775c49a3..c0aac7a4903 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: python cache: pip dist: xenial -python: 3.6 +python: 3.7 addons: apt: packages: @@ -27,9 +27,7 @@ jobs: - env: GROUP=2 python: 2.7 - env: GROUP=1 - python: 3.6 - env: GROUP=2 - python: 3.6 # Complete checking for ensuring compatibility # PyPy @@ -44,9 +42,9 @@ jobs: python: pypy2.7-6.0 # Other Supported CPython - env: GROUP=1 - python: 3.7 + python: 3.6 - env: GROUP=2 - python: 3.7 + python: 3.6 - env: GROUP=1 python: 3.5 - env: GROUP=2 From c941dca9bb736415761a450557361ada80bb496e Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Thu, 1 Aug 2019 12:08:00 +0530 Subject: [PATCH 0057/3170] Add ReadTheDocs configuration --- .readthedocs.yml | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 .readthedocs.yml diff --git a/.readthedocs.yml b/.readthedocs.yml new file mode 100644 index 00000000000..672bc20636f --- /dev/null +++ b/.readthedocs.yml @@ -0,0 +1,9 @@ +version: 2 + +sphinx: + configuration: docs/html/conf.py + +python: + version: 3.7 + install: + - requirements: tools/requirements/docs.txt From 82dbcdae87f7a8aaaf1a9bae663a5fac8c856a40 Mon Sep 17 00:00:00 2001 From: Prabakaran Kumaresshan Date: Fri, 2 Aug 2019 08:59:44 +0530 Subject: [PATCH 0058/3170] Add make_requirement_preparer() to RequirementCommand base class (#6810) --- src/pip/_internal/cli/base_command.py | 25 +++++++++++++++++++++++++ src/pip/_internal/commands/download.py | 12 ++++-------- src/pip/_internal/commands/install.py | 12 +++--------- src/pip/_internal/commands/wheel.py | 12 ++++-------- 4 files changed, 36 insertions(+), 25 deletions(-) diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index fd771b1412c..a17d6640ccf 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -33,6 +33,7 @@ from pip._internal.index import PackageFinder from pip._internal.models.selection_prefs import SelectionPreferences from pip._internal.models.target_python import TargetPython +from pip._internal.operations.prepare import RequirementPreparer from pip._internal.req.constructors import ( install_req_from_editable, install_req_from_line, @@ -50,6 +51,8 @@ from optparse import Values from pip._internal.cache import WheelCache from pip._internal.req.req_set import RequirementSet + from pip._internal.req.req_tracker import RequirementTracker + from pip._internal.utils.temp_dir import TempDirectory __all__ = ['Command'] @@ -257,6 +260,28 @@ def main(self, args): class RequirementCommand(Command): + @staticmethod + def make_requirement_preparer( + temp_directory, # type: TempDirectory + options, # type: Values + req_tracker, # type: RequirementTracker + download_dir=None, # type: str + wheel_download_dir=None, # type: str + ): + # type: (...) -> RequirementPreparer + """ + Create a RequirementPreparer instance for the given parameters. + """ + return RequirementPreparer( + build_dir=temp_directory.path, + src_dir=options.src_dir, + download_dir=download_dir, + wheel_download_dir=wheel_download_dir, + progress_bar=options.progress_bar, + build_isolation=options.build_isolation, + req_tracker=req_tracker, + ) + @staticmethod def populate_requirement_set(requirement_set, # type: RequirementSet args, # type: List[str] diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index 2bb27f876ad..fd299d708bb 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -7,7 +7,6 @@ from pip._internal.cli.base_command import RequirementCommand from pip._internal.cli.cmdoptions import make_target_python from pip._internal.legacy_resolve import Resolver -from pip._internal.operations.prepare import RequirementPreparer from pip._internal.req import RequirementSet from pip._internal.req.req_tracker import RequirementTracker from pip._internal.utils.filesystem import check_path_owner @@ -126,14 +125,11 @@ def run(self, options, args): None ) - 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, + preparer = self.make_requirement_preparer( + temp_directory=directory, + options=options, req_tracker=req_tracker, + download_dir=options.download_dir, ) resolver = Resolver( diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 38166b85549..9b2fa0dcae2 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -22,7 +22,6 @@ from pip._internal.legacy_resolve import Resolver from pip._internal.locations import distutils_scheme from pip._internal.operations.check import check_install_conflicts -from pip._internal.operations.prepare import RequirementPreparer from pip._internal.req import RequirementSet, install_given_reqs from pip._internal.req.req_tracker import RequirementTracker from pip._internal.utils.filesystem import check_path_owner @@ -322,16 +321,11 @@ def run(self, options, args): 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, + preparer = self.make_requirement_preparer( + temp_directory=directory, + options=options, req_tracker=req_tracker, ) - resolver = Resolver( preparer=preparer, finder=finder, diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index a246db65579..84b4d32bc7f 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -9,7 +9,6 @@ from pip._internal.cli.base_command import RequirementCommand from pip._internal.exceptions import CommandError, PreviousBuildDirError from pip._internal.legacy_resolve import Resolver -from pip._internal.operations.prepare import RequirementPreparer from pip._internal.req import RequirementSet from pip._internal.req.req_tracker import RequirementTracker from pip._internal.utils.temp_dir import TempDirectory @@ -129,14 +128,11 @@ def run(self, options, args): 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, + preparer = self.make_requirement_preparer( + temp_directory=directory, + options=options, req_tracker=req_tracker, + wheel_download_dir=options.wheel_dir, ) resolver = Resolver( From 77c1504137f92b1b91e26ae13a7d21c776536b61 Mon Sep 17 00:00:00 2001 From: Omry Yadan Date: Fri, 2 Aug 2019 18:17:53 -0700 Subject: [PATCH 0059/3170] unit test --- src/pip/_internal/download.py | 16 +++++++------ tests/unit/test_download.py | 44 +++++++++++++++++++++++++++++++++++ 2 files changed, 53 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index f58ba475c06..07e08af6eee 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -936,18 +936,20 @@ def unpack_file_url( link_path = url_to_path(link.url_without_fragment) # If it's a url to a local directory if is_dir_url(link): + + def ignore(d, names): + # Pulling in those directories can potentially + # be very slow. + # see discussion at: + # https://github.com/pypa/pip/pull/6770 + return ['.tox', '.nox'] if d == link_path else [] + if os.path.isdir(location): rmtree(location) shutil.copytree(link_path, location, symlinks=True, - # Pulling in those directories can potentially - # be very slow. - # see discussion at: - # https://github.com/pypa/pip/pull/6770 - # https://github.com/pypa/pip/issues/2195 - # https://github.com/pypa/pip/pull/2196 - ignore=shutil.ignore_patterns('.tox', '.nox')) + ignore=ignore) if download_dir: logger.info('Link is a directory, ignoring download_dir') diff --git a/tests/unit/test_download.py b/tests/unit/test_download.py index 7b421a7d7d5..86493cba27d 100644 --- a/tests/unit/test_download.py +++ b/tests/unit/test_download.py @@ -413,6 +413,50 @@ def test_unpack_file_url_thats_a_dir(self, tmpdir, data): assert os.path.isdir(os.path.join(self.build_dir, 'fspkg')) +@pytest.mark.parametrize('exclude_dir', [ + '.nox', + '.tox' +]) +def test_unpack_file_url_with_excluded_dirs(exclude_dir): + + def touch(path): + with open(path, 'a'): + os.utime(path, None) + + src_dir = mkdtemp() + src_included_file = os.path.join(src_dir, 'file.txt') + src_excluded_dir = os.path.join(src_dir, exclude_dir) + src_excluded_file = os.path.join(src_dir, exclude_dir, 'file.txt') + src_included_dir = os.path.join(src_dir, 'subdir', exclude_dir) + + # set up source directory + os.makedirs(src_excluded_dir, exist_ok=True) + os.makedirs(src_included_dir, exist_ok=True) + touch(src_included_file) + touch(src_excluded_file) + + dst_dir = mkdtemp() + dst_included_file = os.path.join(dst_dir, 'file.txt') + dst_excluded_dir = os.path.join(dst_dir, exclude_dir) + dst_excluded_file = os.path.join(dst_dir, exclude_dir, 'file.txt') + dst_included_dir = os.path.join(dst_dir, 'subdir', exclude_dir) + + try: + src_link = Link(path_to_url(src_dir)) + unpack_file_url( + src_link, + dst_dir, + download_dir=None + ) + assert not os.path.isdir(dst_excluded_dir) + assert not os.path.isfile(dst_excluded_file) + assert os.path.isfile(dst_included_file) + assert os.path.isdir(dst_included_dir) + finally: + rmtree(src_dir) + rmtree(dst_dir) + + class TestSafeFileCache: """ The no_perms test are useless on Windows since SafeFileCache uses From e67f066169b4b4bde68e8ea4a0dd57daf8acaee9 Mon Sep 17 00:00:00 2001 From: Omry Yadan Date: Fri, 2 Aug 2019 18:38:13 -0700 Subject: [PATCH 0060/3170] fixed on python 2.7 --- tests/unit/test_download.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_download.py b/tests/unit/test_download.py index 86493cba27d..0b00644a80a 100644 --- a/tests/unit/test_download.py +++ b/tests/unit/test_download.py @@ -18,7 +18,7 @@ from pip._internal.exceptions import HashMismatch from pip._internal.models.link import Link from pip._internal.utils.hashes import Hashes -from pip._internal.utils.misc import path_to_url +from pip._internal.utils.misc import ensure_dir, path_to_url from tests.lib import create_file @@ -430,8 +430,8 @@ def touch(path): src_included_dir = os.path.join(src_dir, 'subdir', exclude_dir) # set up source directory - os.makedirs(src_excluded_dir, exist_ok=True) - os.makedirs(src_included_dir, exist_ok=True) + ensure_dir(src_excluded_dir) + ensure_dir(src_included_dir) touch(src_included_file) touch(src_excluded_file) From 8e87980d17631ffc55e386ec8042ca10f586d7b1 Mon Sep 17 00:00:00 2001 From: Omry Yadan Date: Fri, 2 Aug 2019 23:14:41 -0700 Subject: [PATCH 0061/3170] responding to comments --- src/pip/_internal/download.py | 8 ++--- tests/unit/test_download.py | 67 +++++++++++++++-------------------- 2 files changed, 33 insertions(+), 42 deletions(-) diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index 07e08af6eee..95963a1d2d5 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -938,10 +938,10 @@ def unpack_file_url( if is_dir_url(link): def ignore(d, names): - # Pulling in those directories can potentially - # be very slow. - # see discussion at: - # https://github.com/pypa/pip/pull/6770 + # Pulling in those directories can potentially be very slow, + # exclude the following directories if they appear in the top + # level dir (and only it). + # See discussion at https://github.com/pypa/pip/pull/6770 return ['.tox', '.nox'] if d == link_path else [] if os.path.isdir(location): diff --git a/tests/unit/test_download.py b/tests/unit/test_download.py index 0b00644a80a..068c45dd5a1 100644 --- a/tests/unit/test_download.py +++ b/tests/unit/test_download.py @@ -18,8 +18,8 @@ from pip._internal.exceptions import HashMismatch from pip._internal.models.link import Link from pip._internal.utils.hashes import Hashes -from pip._internal.utils.misc import ensure_dir, path_to_url -from tests.lib import create_file +from pip._internal.utils.misc import path_to_url +from tests.lib import Path, create_file @pytest.fixture(scope="function") @@ -417,44 +417,35 @@ def test_unpack_file_url_thats_a_dir(self, tmpdir, data): '.nox', '.tox' ]) -def test_unpack_file_url_with_excluded_dirs(exclude_dir): - - def touch(path): - with open(path, 'a'): - os.utime(path, None) - - src_dir = mkdtemp() - src_included_file = os.path.join(src_dir, 'file.txt') - src_excluded_dir = os.path.join(src_dir, exclude_dir) - src_excluded_file = os.path.join(src_dir, exclude_dir, 'file.txt') - src_included_dir = os.path.join(src_dir, 'subdir', exclude_dir) +def test_unpack_file_url_excludes_expected_dirs(tmpdir, exclude_dir): + src_dir = tmpdir / 'src' + dst_dir = tmpdir / 'dst' + src_included_file = Path.joinpath(src_dir, 'file.txt') + src_excluded_dir = Path.joinpath(src_dir, exclude_dir) + src_excluded_file = Path.joinpath(src_dir, exclude_dir, 'file.txt') + src_included_dir = Path.joinpath(src_dir, 'subdir', exclude_dir) # set up source directory - ensure_dir(src_excluded_dir) - ensure_dir(src_included_dir) - touch(src_included_file) - touch(src_excluded_file) - - dst_dir = mkdtemp() - dst_included_file = os.path.join(dst_dir, 'file.txt') - dst_excluded_dir = os.path.join(dst_dir, exclude_dir) - dst_excluded_file = os.path.join(dst_dir, exclude_dir, 'file.txt') - dst_included_dir = os.path.join(dst_dir, 'subdir', exclude_dir) - - try: - src_link = Link(path_to_url(src_dir)) - unpack_file_url( - src_link, - dst_dir, - download_dir=None - ) - assert not os.path.isdir(dst_excluded_dir) - assert not os.path.isfile(dst_excluded_file) - assert os.path.isfile(dst_included_file) - assert os.path.isdir(dst_included_dir) - finally: - rmtree(src_dir) - rmtree(dst_dir) + src_excluded_dir.mkdir(parents=True) + src_included_dir.mkdir(parents=True) + Path.touch(src_included_file) + Path.touch(src_excluded_file) + + dst_included_file = Path.joinpath(dst_dir, 'file.txt') + dst_excluded_dir = Path.joinpath(dst_dir, exclude_dir) + dst_excluded_file = Path.joinpath(dst_dir, exclude_dir, 'file.txt') + dst_included_dir = Path.joinpath(dst_dir, 'subdir', exclude_dir) + + src_link = Link(path_to_url(src_dir)) + unpack_file_url( + src_link, + dst_dir, + download_dir=None + ) + assert not os.path.isdir(dst_excluded_dir) + assert not os.path.isfile(dst_excluded_file) + assert os.path.isfile(dst_included_file) + assert os.path.isdir(dst_included_dir) class TestSafeFileCache: From 82ef9d67e2ce46baf2f0e55d2f53a1f8c58f7b9d Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Sat, 20 Jul 2019 22:51:10 -0400 Subject: [PATCH 0062/3170] Refactor download.get_file_content. --- src/pip/_internal/download.py | 53 ++++++++++++++++++++--------------- tests/unit/test_download.py | 11 ++++++++ 2 files changed, 41 insertions(+), 23 deletions(-) diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index 1578521c765..4781d79a2c8 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -636,29 +636,30 @@ def get_file_content(url, comes_from=None, session=None): "get_file_content() missing 1 required keyword argument: 'session'" ) - match = _scheme_re.search(url) - if match: - scheme = match.group(1).lower() - if (scheme == 'file' and comes_from and - comes_from.startswith('http')): + scheme = _get_url_scheme(url) + + if scheme in ['http', 'https']: + # FIXME: catch some errors + resp = session.get(url) + resp.raise_for_status() + return resp.url, resp.text + + elif scheme == 'file': + if comes_from and comes_from.startswith('http'): raise InstallationError( 'Requirements file %s references URL %s, which is local' % (comes_from, url)) - if scheme == 'file': - path = url.split(':', 1)[1] - path = path.replace('\\', '/') - match = _url_slash_drive_re.match(path) - if match: - path = match.group(1) + ':' + path.split('|', 1)[1] - path = urllib_parse.unquote(path) - if path.startswith('/'): - path = '/' + path.lstrip('/') - url = path - else: - # FIXME: catch some errors - resp = session.get(url) - resp.raise_for_status() - return resp.url, resp.text + + path = url.split(':', 1)[1] + path = path.replace('\\', '/') + match = _url_slash_drive_re.match(path) + if match: + path = match.group(1) + ':' + path.split('|', 1)[1] + path = urllib_parse.unquote(path) + if path.startswith('/'): + path = '/' + path.lstrip('/') + url = path + try: with open(url, 'rb') as f: content = auto_decode(f.read()) @@ -669,16 +670,22 @@ def get_file_content(url, comes_from=None, session=None): return url, content -_scheme_re = re.compile(r'^(http|https|file):', re.I) _url_slash_drive_re = re.compile(r'/*([a-z])\|', re.I) +def _get_url_scheme(url): + # type: (Union[str, Text]) -> Optional[Text] + if ':' not in url: + return None + return url.split(':', 1)[0].lower() + + def is_url(name): # type: (Union[str, Text]) -> bool """Returns true if the name looks like a URL""" - if ':' not in name: + scheme = _get_url_scheme(name) + if scheme is None: return False - scheme = name.split(':', 1)[0].lower() return scheme in ['http', 'https', 'file', 'ftp'] + vcs.all_schemes diff --git a/tests/unit/test_download.py b/tests/unit/test_download.py index d63d7420178..102e0a9a581 100644 --- a/tests/unit/test_download.py +++ b/tests/unit/test_download.py @@ -16,6 +16,7 @@ PipSession, SafeFileCache, _download_http_url, + _get_url_scheme, parse_content_disposition, sanitize_content_filename, unpack_file_url, @@ -291,6 +292,16 @@ def test_download_http_url__no_directory_traversal(tmpdir): assert actual == ['out_dir_file'] +@pytest.mark.parametrize("url,expected", [ + ('http://localhost:8080/', 'http'), + ('file:c:/path/to/file', 'file'), + ('file:/dev/null', 'file'), + ('', None), +]) +def test__get_url_scheme(url, expected): + assert _get_url_scheme(url) == expected + + @pytest.mark.parametrize("url,win_expected,non_win_expected", [ ('file:tmp', 'tmp', 'tmp'), ('file:c:/path/to/file', r'C:\path\to\file', 'c:/path/to/file'), From ec47c866c85a6677a1500d376df5897d8524b075 Mon Sep 17 00:00:00 2001 From: Omry Yadan Date: Sat, 3 Aug 2019 10:31:42 -0700 Subject: [PATCH 0063/3170] updated docs --- docs/html/reference/pip_install.rst | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index 96fd1c018ba..e919693b3fe 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -694,10 +694,21 @@ does not satisfy the ``--require-hashes`` demand that every package have a local hash. +Local project installs +++++++++++++++++++++++ +pip supports installing local project in both regular mode and editable mode. +You can install local projects by specifying the project path to pip:: + +$ pip install path/to/SomeProject + +During the installation, pip will copy the entire project directory to a temporary location and install from there. +The exception is that pip will exclude .tox and .nox directories present in the top level of the project from being copied. + + .. _`editable-installs`: "Editable" Installs -+++++++++++++++++++ +~~~~~~~~~~~~~~~~~~~ "Editable" installs are fundamentally `"setuptools develop mode" `_ From 6dd5727773ab1d99d57fa7f77b611801e68ce6d3 Mon Sep 17 00:00:00 2001 From: Omry Yadan Date: Sat, 3 Aug 2019 11:14:36 -0700 Subject: [PATCH 0064/3170] responded to comments --- docs/html/reference/pip_install.rst | 2 +- tests/unit/test_download.py | 22 +++++++++++----------- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index e919693b3fe..f56dc9a0e9f 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -701,7 +701,7 @@ You can install local projects by specifying the project path to pip:: $ pip install path/to/SomeProject -During the installation, pip will copy the entire project directory to a temporary location and install from there. +During regular installation, pip will copy the entire project directory to a temporary location and install from there. The exception is that pip will exclude .tox and .nox directories present in the top level of the project from being copied. diff --git a/tests/unit/test_download.py b/tests/unit/test_download.py index 068c45dd5a1..afc32e29dca 100644 --- a/tests/unit/test_download.py +++ b/tests/unit/test_download.py @@ -19,7 +19,7 @@ from pip._internal.models.link import Link from pip._internal.utils.hashes import Hashes from pip._internal.utils.misc import path_to_url -from tests.lib import Path, create_file +from tests.lib import create_file @pytest.fixture(scope="function") @@ -420,21 +420,21 @@ def test_unpack_file_url_thats_a_dir(self, tmpdir, data): def test_unpack_file_url_excludes_expected_dirs(tmpdir, exclude_dir): src_dir = tmpdir / 'src' dst_dir = tmpdir / 'dst' - src_included_file = Path.joinpath(src_dir, 'file.txt') - src_excluded_dir = Path.joinpath(src_dir, exclude_dir) - src_excluded_file = Path.joinpath(src_dir, exclude_dir, 'file.txt') - src_included_dir = Path.joinpath(src_dir, 'subdir', exclude_dir) + src_included_file = src_dir.joinpath('file.txt') + src_excluded_dir = src_dir.joinpath(exclude_dir) + src_excluded_file = src_dir.joinpath(exclude_dir, 'file.txt') + src_included_dir = src_dir.joinpath('subdir', exclude_dir) # set up source directory src_excluded_dir.mkdir(parents=True) src_included_dir.mkdir(parents=True) - Path.touch(src_included_file) - Path.touch(src_excluded_file) + src_included_file.touch() + src_excluded_file.touch() - dst_included_file = Path.joinpath(dst_dir, 'file.txt') - dst_excluded_dir = Path.joinpath(dst_dir, exclude_dir) - dst_excluded_file = Path.joinpath(dst_dir, exclude_dir, 'file.txt') - dst_included_dir = Path.joinpath(dst_dir, 'subdir', exclude_dir) + dst_included_file = dst_dir.joinpath('file.txt') + dst_excluded_dir = dst_dir.joinpath(exclude_dir) + dst_excluded_file = dst_dir.joinpath(exclude_dir, 'file.txt') + dst_included_dir = dst_dir.joinpath('subdir', exclude_dir) src_link = Link(path_to_url(src_dir)) unpack_file_url( From 56324a3f388fdc2711e7479d50f55de93ca2afdd Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 30 Jul 2019 16:54:24 -0700 Subject: [PATCH 0065/3170] Fix "~" expansion in --find-links paths. --- news/6804.bugfix | 2 + src/pip/_internal/cli/cmdoptions.py | 8 ++-- tests/unit/test_cmdoptions.py | 57 ++++++++++++++++++++++++----- tests/unit/test_unit_outdated.py | 2 +- 4 files changed, 56 insertions(+), 13 deletions(-) create mode 100644 news/6804.bugfix diff --git a/news/6804.bugfix b/news/6804.bugfix new file mode 100644 index 00000000000..f9599f9fda6 --- /dev/null +++ b/news/6804.bugfix @@ -0,0 +1,2 @@ +Fix a regression that caused ``~`` expansion not to occur in ``--find-links`` +paths. diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index ecf4d20bcd6..c5c6c22dcfd 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -370,9 +370,11 @@ def make_search_scope(options, suppress_no_index=False): ) index_urls = [] - search_scope = SearchScope( - find_links=options.find_links, - index_urls=index_urls, + # Make sure find_links is a list before passing to create(). + find_links = options.find_links or [] + + search_scope = SearchScope.create( + find_links=find_links, index_urls=index_urls, ) return search_scope diff --git a/tests/unit/test_cmdoptions.py b/tests/unit/test_cmdoptions.py index 8deb66a5f30..3cf2ba8b9ab 100644 --- a/tests/unit/test_cmdoptions.py +++ b/tests/unit/test_cmdoptions.py @@ -1,5 +1,8 @@ +import os + import pretend import pytest +from mock import patch from pip._internal.cli.cmdoptions import ( _convert_python_version, @@ -8,20 +11,24 @@ @pytest.mark.parametrize( - 'no_index, suppress_no_index, expected_index_urls', [ - (False, False, ['default_url', 'url1', 'url2']), - (False, True, ['default_url', 'url1', 'url2']), - (True, False, []), + 'find_links, no_index, suppress_no_index, expected', [ + (['link1'], False, False, + (['link1'], ['default_url', 'url1', 'url2'])), + (['link1'], False, True, (['link1'], ['default_url', 'url1', 'url2'])), + (['link1'], True, False, (['link1'], [])), # Passing suppress_no_index=True suppresses no_index=True. - (True, True, ['default_url', 'url1', 'url2']), + (['link1'], True, True, (['link1'], ['default_url', 'url1', 'url2'])), + # Test options.find_links=False. + (False, False, False, ([], ['default_url', 'url1', 'url2'])), ], ) -def test_make_search_scope(no_index, suppress_no_index, expected_index_urls): +def test_make_search_scope(find_links, no_index, suppress_no_index, expected): """ - :param expected: the expected index_urls value. + :param expected: the expected (find_links, index_urls) values. """ + expected_find_links, expected_index_urls = expected options = pretend.stub( - find_links=['link1'], + find_links=find_links, index_url='default_url', extra_index_urls=['url1', 'url2'], no_index=no_index, @@ -29,10 +36,42 @@ def test_make_search_scope(no_index, suppress_no_index, expected_index_urls): search_scope = make_search_scope( options, suppress_no_index=suppress_no_index, ) - assert search_scope.find_links == ['link1'] + assert search_scope.find_links == expected_find_links assert search_scope.index_urls == expected_index_urls +@patch('pip._internal.utils.misc.expanduser') +def test_make_search_scope__find_links_expansion(mock_expanduser, tmpdir): + """ + Test "~" expansion in --find-links paths. + """ + # This is a mock version of expanduser() that expands "~" to the tmpdir. + def expand_path(path): + if path.startswith('~/'): + path = os.path.join(tmpdir, path[2:]) + return path + + mock_expanduser.side_effect = expand_path + + options = pretend.stub( + find_links=['~/temp1', '~/temp2'], + index_url='default_url', + extra_index_urls=[], + no_index=False, + ) + # Only create temp2 and not temp1 to test that "~" expansion only occurs + # when the directory exists. + temp2_dir = os.path.join(tmpdir, 'temp2') + os.mkdir(temp2_dir) + + search_scope = make_search_scope(options) + + # Only ~/temp2 gets expanded. Also, the path is normalized when expanded. + expected_temp2_dir = os.path.normcase(temp2_dir) + assert search_scope.find_links == ['~/temp1', expected_temp2_dir] + assert search_scope.index_urls == ['default_url'] + + @pytest.mark.parametrize('value, expected', [ ('', (None, None)), ('2', ((2,), None)), diff --git a/tests/unit/test_unit_outdated.py b/tests/unit/test_unit_outdated.py index 5a8eb5c1e9f..a5d37f81868 100644 --- a/tests/unit/test_unit_outdated.py +++ b/tests/unit/test_unit_outdated.py @@ -58,7 +58,7 @@ def get_metadata_lines(self, name): def _options(): ''' Some default options that we pass to outdated.pip_version_check ''' return pretend.stub( - find_links=False, index_url='default_url', extra_index_urls=[], + find_links=[], index_url='default_url', extra_index_urls=[], no_index=False, pre=False, trusted_hosts=False, cache_dir='', ) From 8963e3712a1ee012f9e46b781ffcb3cf0ba5ea3b Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Sun, 4 Aug 2019 11:14:09 +0530 Subject: [PATCH 0066/3170] Exclude .readthedocs.yml from distributions --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index 5b19010e0c0..747f84ad4ce 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -15,6 +15,7 @@ exclude .coveragerc exclude .mailmap exclude .appveyor.yml exclude .travis.yml +exclude .readthedocs.yml exclude tox.ini recursive-include src/pip/_vendor *.pem From f18ec526db69575e4e8de282e35d6545594a9a90 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Sun, 4 Aug 2019 11:16:16 +0530 Subject: [PATCH 0067/3170] Unify calls to prepare_distribution_metadata to one callsite --- src/pip/_internal/operations/prepare.py | 27 ++++++++++++++++--------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index bac5ede89bb..47c2c652b77 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -42,6 +42,15 @@ logger = logging.getLogger(__name__) +def _get_prepared_distribution(req, req_tracker, finder, build_isolation): + """Prepare a distribution for installation. + """ + abstract_dist = make_distribution_for_install_requirement(req) + with req_tracker.track(req): + abstract_dist.prepare_distribution_metadata(finder, build_isolation) + return abstract_dist + + class RequirementPreparer(object): """Prepares a Requirement """ @@ -207,11 +216,11 @@ def prepare_linked_requirement( 'error %s for URL %s' % (req, exc, link) ) - abstract_dist = make_distribution_for_install_requirement(req) - with self.req_tracker.track(req): - abstract_dist.prepare_distribution_metadata( - finder, self.build_isolation, - ) + + abstract_dist = _get_prepared_distribution( + req, self.req_tracker, finder, self.build_isolation, + ) + if self._download_should_save: # Make a .zip of the source_dir we already created. if not link.is_artifact: @@ -242,11 +251,9 @@ def prepare_editable_requirement( req.ensure_has_source_dir(self.src_dir) req.update_editable(not self._download_should_save) - abstract_dist = make_distribution_for_install_requirement(req) - with self.req_tracker.track(req): - abstract_dist.prepare_distribution_metadata( - finder, self.build_isolation, - ) + abstract_dist = _get_prepared_distribution( + req, self.req_tracker, finder, self.build_isolation, + ) if self._download_should_save: req.archive(self.download_dir) From b562531cc526c1e6119960b27113cecf18591a55 Mon Sep 17 00:00:00 2001 From: Prabakaran Kumaresshan Date: Sun, 4 Aug 2019 12:17:50 +0530 Subject: [PATCH 0068/3170] Add make_resolver() to RequirementCommand base class (#6826). --- src/pip/_internal/cli/base_command.py | 36 ++++++++++++++++++++++++++ src/pip/_internal/commands/download.py | 12 ++------- src/pip/_internal/commands/install.py | 14 +++++----- src/pip/_internal/commands/wheel.py | 12 +++------ 4 files changed, 47 insertions(+), 27 deletions(-) diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index a17d6640ccf..cc92cab289c 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -31,6 +31,7 @@ UninstallationError, ) from pip._internal.index import PackageFinder +from pip._internal.legacy_resolve import Resolver from pip._internal.models.selection_prefs import SelectionPreferences from pip._internal.models.target_python import TargetPython from pip._internal.operations.prepare import RequirementPreparer @@ -282,6 +283,41 @@ def make_requirement_preparer( req_tracker=req_tracker, ) + @staticmethod + def make_resolver( + preparer, # type: RequirementPreparer + session, # type: PipSession + finder, # type: PackageFinder + options, # type: Values + wheel_cache=None, # type: Optional[WheelCache] + use_user_site=False, # type: bool + ignore_installed=True, # type: bool + ignore_requires_python=False, # type: bool + force_reinstall=False, # type: bool + upgrade_strategy="to-satisfy-only", # type: str + use_pep517=None, # type: Optional[bool] + py_version_info=None # type: Optional[Tuple[int, ...]] + ): + # type: (...) -> Resolver + """ + Create a Resolver instance for the given parameters. + """ + return Resolver( + preparer=preparer, + session=session, + finder=finder, + wheel_cache=wheel_cache, + use_user_site=use_user_site, + ignore_dependencies=options.ignore_dependencies, + ignore_installed=ignore_installed, + ignore_requires_python=ignore_requires_python, + force_reinstall=force_reinstall, + isolated=options.isolated_mode, + upgrade_strategy=upgrade_strategy, + use_pep517=use_pep517, + py_version_info=py_version_info + ) + @staticmethod def populate_requirement_set(requirement_set, # type: RequirementSet args, # type: List[str] diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index fd299d708bb..22e0b380830 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -6,7 +6,6 @@ from pip._internal.cli import cmdoptions from pip._internal.cli.base_command import RequirementCommand from pip._internal.cli.cmdoptions import make_target_python -from pip._internal.legacy_resolve import Resolver from pip._internal.req import RequirementSet from pip._internal.req.req_tracker import RequirementTracker from pip._internal.utils.filesystem import check_path_owner @@ -132,19 +131,12 @@ def run(self, options, args): download_dir=options.download_dir, ) - resolver = Resolver( + resolver = self.make_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, + options=options, py_version_info=options.python_version, - ignore_requires_python=False, - ignore_installed=True, - isolated=options.isolated_mode, ) resolver.resolve(requirement_set) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 9b2fa0dcae2..a4c5f5952e5 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -19,7 +19,6 @@ InstallationError, PreviousBuildDirError, ) -from pip._internal.legacy_resolve import Resolver from pip._internal.locations import distutils_scheme from pip._internal.operations.check import check_install_conflicts from pip._internal.req import RequirementSet, install_given_reqs @@ -326,19 +325,18 @@ def run(self, options, args): options=options, req_tracker=req_tracker, ) - resolver = Resolver( + resolver = self.make_resolver( preparer=preparer, finder=finder, session=session, + options=options, 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, - use_pep517=options.use_pep517 + ignore_requires_python=options.ignore_requires_python, + force_reinstall=options.force_reinstall, + upgrade_strategy=upgrade_strategy, + use_pep517=options.use_pep517, ) resolver.resolve(requirement_set) diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index 84b4d32bc7f..88396330360 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -8,7 +8,6 @@ from pip._internal.cli import cmdoptions from pip._internal.cli.base_command import RequirementCommand from pip._internal.exceptions import CommandError, PreviousBuildDirError -from pip._internal.legacy_resolve import Resolver from pip._internal.req import RequirementSet from pip._internal.req.req_tracker import RequirementTracker from pip._internal.utils.temp_dir import TempDirectory @@ -135,19 +134,14 @@ def run(self, options, args): wheel_download_dir=options.wheel_dir, ) - resolver = Resolver( + resolver = self.make_resolver( preparer=preparer, finder=finder, session=session, + options=options, 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, - use_pep517=options.use_pep517 + use_pep517=options.use_pep517, ) resolver.resolve(requirement_set) From da9ebed9df8f1aa41360c653a12273689bd6012a Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 3 Aug 2019 18:35:59 -0700 Subject: [PATCH 0069/3170] Move RequirementCommand to req_command.py. --- src/pip/_internal/cli/base_command.py | 173 ----------------------- src/pip/_internal/cli/req_command.py | 188 +++++++++++++++++++++++++ src/pip/_internal/commands/download.py | 2 +- src/pip/_internal/commands/install.py | 2 +- src/pip/_internal/commands/wheel.py | 2 +- 5 files changed, 191 insertions(+), 176 deletions(-) create mode 100644 src/pip/_internal/cli/req_command.py diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index cc92cab289c..aaab6ca4c43 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -10,7 +10,6 @@ import traceback from pip._internal.cli import cmdoptions -from pip._internal.cli.cmdoptions import make_search_scope from pip._internal.cli.parser import ( ConfigOptionParser, UpdatingDefaultsHelpFormatter, @@ -30,16 +29,6 @@ PreviousBuildDirError, UninstallationError, ) -from pip._internal.index import PackageFinder -from pip._internal.legacy_resolve import Resolver -from pip._internal.models.selection_prefs import SelectionPreferences -from pip._internal.models.target_python import TargetPython -from pip._internal.operations.prepare import RequirementPreparer -from pip._internal.req.constructors import ( - install_req_from_editable, - install_req_from_line, -) -from pip._internal.req.req_file import parse_requirements from pip._internal.utils.deprecation import deprecated from pip._internal.utils.logging import BrokenStdoutLoggingError, setup_logging from pip._internal.utils.misc import get_prog, normalize_path @@ -50,10 +39,6 @@ if MYPY_CHECK_RUNNING: from typing import Optional, List, Tuple, Any from optparse import Values - from pip._internal.cache import WheelCache - from pip._internal.req.req_set import RequirementSet - from pip._internal.req.req_tracker import RequirementTracker - from pip._internal.utils.temp_dir import TempDirectory __all__ = ['Command'] @@ -257,161 +242,3 @@ def main(self, args): logging.shutdown() return SUCCESS - - -class RequirementCommand(Command): - - @staticmethod - def make_requirement_preparer( - temp_directory, # type: TempDirectory - options, # type: Values - req_tracker, # type: RequirementTracker - download_dir=None, # type: str - wheel_download_dir=None, # type: str - ): - # type: (...) -> RequirementPreparer - """ - Create a RequirementPreparer instance for the given parameters. - """ - return RequirementPreparer( - build_dir=temp_directory.path, - src_dir=options.src_dir, - download_dir=download_dir, - wheel_download_dir=wheel_download_dir, - progress_bar=options.progress_bar, - build_isolation=options.build_isolation, - req_tracker=req_tracker, - ) - - @staticmethod - def make_resolver( - preparer, # type: RequirementPreparer - session, # type: PipSession - finder, # type: PackageFinder - options, # type: Values - wheel_cache=None, # type: Optional[WheelCache] - use_user_site=False, # type: bool - ignore_installed=True, # type: bool - ignore_requires_python=False, # type: bool - force_reinstall=False, # type: bool - upgrade_strategy="to-satisfy-only", # type: str - use_pep517=None, # type: Optional[bool] - py_version_info=None # type: Optional[Tuple[int, ...]] - ): - # type: (...) -> Resolver - """ - Create a Resolver instance for the given parameters. - """ - return Resolver( - preparer=preparer, - session=session, - finder=finder, - wheel_cache=wheel_cache, - use_user_site=use_user_site, - ignore_dependencies=options.ignore_dependencies, - ignore_installed=ignore_installed, - ignore_requires_python=ignore_requires_python, - force_reinstall=force_reinstall, - isolated=options.isolated_mode, - upgrade_strategy=upgrade_strategy, - use_pep517=use_pep517, - py_version_info=py_version_info - ) - - @staticmethod - def populate_requirement_set(requirement_set, # type: RequirementSet - args, # type: List[str] - options, # type: Values - finder, # type: PackageFinder - session, # type: PipSession - name, # type: str - wheel_cache # type: Optional[WheelCache] - ): - # type: (...) -> None - """ - Marshal cmd line args into a requirement set. - """ - # NOTE: As a side-effect, options.require_hashes and - # requirement_set.require_hashes may be updated - - for filename in options.constraints: - for req_to_add in parse_requirements( - filename, - constraint=True, finder=finder, options=options, - session=session, wheel_cache=wheel_cache): - req_to_add.is_direct = True - requirement_set.add_requirement(req_to_add) - - for req in args: - req_to_add = install_req_from_line( - req, None, isolated=options.isolated_mode, - use_pep517=options.use_pep517, - wheel_cache=wheel_cache - ) - req_to_add.is_direct = True - requirement_set.add_requirement(req_to_add) - - for req in options.editables: - req_to_add = install_req_from_editable( - req, - isolated=options.isolated_mode, - use_pep517=options.use_pep517, - wheel_cache=wheel_cache - ) - req_to_add.is_direct = True - requirement_set.add_requirement(req_to_add) - - for filename in options.requirements: - for req_to_add in parse_requirements( - filename, - finder=finder, options=options, session=session, - wheel_cache=wheel_cache, - use_pep517=options.use_pep517): - req_to_add.is_direct = True - requirement_set.add_requirement(req_to_add) - # If --require-hashes was a line in a requirements file, tell - # RequirementSet about it: - requirement_set.require_hashes = options.require_hashes - - if not (args or options.editables or options.requirements): - opts = {'name': name} - if 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: - raise CommandError( - 'You must give at least one requirement to %(name)s ' - '(see "pip help %(name)s")' % opts) - - def _build_package_finder( - self, - options, # type: Values - session, # type: PipSession - target_python=None, # type: Optional[TargetPython] - ignore_requires_python=None, # type: Optional[bool] - ): - # type: (...) -> PackageFinder - """ - Create a package finder appropriate to this requirement command. - - :param ignore_requires_python: Whether to ignore incompatible - "Requires-Python" values in links. Defaults to False. - """ - search_scope = make_search_scope(options) - selection_prefs = SelectionPreferences( - allow_yanked=True, - format_control=options.format_control, - allow_all_prereleases=options.pre, - prefer_binary=options.prefer_binary, - ignore_requires_python=ignore_requires_python, - ) - - return PackageFinder.create( - search_scope=search_scope, - selection_prefs=selection_prefs, - trusted_hosts=options.trusted_hosts, - session=session, - target_python=target_python, - ) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py new file mode 100644 index 00000000000..63776f523b1 --- /dev/null +++ b/src/pip/_internal/cli/req_command.py @@ -0,0 +1,188 @@ +"""Contains the RequirementCommand base class. + +This is in a separate module so that Command classes not inheriting from +RequirementCommand don't need to import e.g. the PackageFinder machinery +and all its vendored dependencies. +""" + +from pip._internal.cli.base_command import Command +from pip._internal.cli.cmdoptions import make_search_scope +from pip._internal.exceptions import CommandError +from pip._internal.index import PackageFinder +from pip._internal.legacy_resolve import Resolver +from pip._internal.models.selection_prefs import SelectionPreferences +from pip._internal.operations.prepare import RequirementPreparer +from pip._internal.req.constructors import ( + install_req_from_editable, + install_req_from_line, +) +from pip._internal.req.req_file import parse_requirements +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from optparse import Values + from typing import List, Optional, Tuple + from pip._internal.cache import WheelCache + from pip._internal.download import PipSession + from pip._internal.models.target_python import TargetPython + from pip._internal.req.req_set import RequirementSet + from pip._internal.req.req_tracker import RequirementTracker + from pip._internal.utils.temp_dir import TempDirectory + + +class RequirementCommand(Command): + + @staticmethod + def make_requirement_preparer( + temp_directory, # type: TempDirectory + options, # type: Values + req_tracker, # type: RequirementTracker + download_dir=None, # type: str + wheel_download_dir=None, # type: str + ): + # type: (...) -> RequirementPreparer + """ + Create a RequirementPreparer instance for the given parameters. + """ + return RequirementPreparer( + build_dir=temp_directory.path, + src_dir=options.src_dir, + download_dir=download_dir, + wheel_download_dir=wheel_download_dir, + progress_bar=options.progress_bar, + build_isolation=options.build_isolation, + req_tracker=req_tracker, + ) + + @staticmethod + def make_resolver( + preparer, # type: RequirementPreparer + session, # type: PipSession + finder, # type: PackageFinder + options, # type: Values + wheel_cache=None, # type: Optional[WheelCache] + use_user_site=False, # type: bool + ignore_installed=True, # type: bool + ignore_requires_python=False, # type: bool + force_reinstall=False, # type: bool + upgrade_strategy="to-satisfy-only", # type: str + use_pep517=None, # type: Optional[bool] + py_version_info=None # type: Optional[Tuple[int, ...]] + ): + # type: (...) -> Resolver + """ + Create a Resolver instance for the given parameters. + """ + return Resolver( + preparer=preparer, + session=session, + finder=finder, + wheel_cache=wheel_cache, + use_user_site=use_user_site, + ignore_dependencies=options.ignore_dependencies, + ignore_installed=ignore_installed, + ignore_requires_python=ignore_requires_python, + force_reinstall=force_reinstall, + isolated=options.isolated_mode, + upgrade_strategy=upgrade_strategy, + use_pep517=use_pep517, + py_version_info=py_version_info + ) + + @staticmethod + def populate_requirement_set(requirement_set, # type: RequirementSet + args, # type: List[str] + options, # type: Values + finder, # type: PackageFinder + session, # type: PipSession + name, # type: str + wheel_cache # type: Optional[WheelCache] + ): + # type: (...) -> None + """ + Marshal cmd line args into a requirement set. + """ + # NOTE: As a side-effect, options.require_hashes and + # requirement_set.require_hashes may be updated + + for filename in options.constraints: + for req_to_add in parse_requirements( + filename, + constraint=True, finder=finder, options=options, + session=session, wheel_cache=wheel_cache): + req_to_add.is_direct = True + requirement_set.add_requirement(req_to_add) + + for req in args: + req_to_add = install_req_from_line( + req, None, isolated=options.isolated_mode, + use_pep517=options.use_pep517, + wheel_cache=wheel_cache + ) + req_to_add.is_direct = True + requirement_set.add_requirement(req_to_add) + + for req in options.editables: + req_to_add = install_req_from_editable( + req, + isolated=options.isolated_mode, + use_pep517=options.use_pep517, + wheel_cache=wheel_cache + ) + req_to_add.is_direct = True + requirement_set.add_requirement(req_to_add) + + for filename in options.requirements: + for req_to_add in parse_requirements( + filename, + finder=finder, options=options, session=session, + wheel_cache=wheel_cache, + use_pep517=options.use_pep517): + req_to_add.is_direct = True + requirement_set.add_requirement(req_to_add) + # If --require-hashes was a line in a requirements file, tell + # RequirementSet about it: + requirement_set.require_hashes = options.require_hashes + + if not (args or options.editables or options.requirements): + opts = {'name': name} + if 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: + raise CommandError( + 'You must give at least one requirement to %(name)s ' + '(see "pip help %(name)s")' % opts) + + def _build_package_finder( + self, + options, # type: Values + session, # type: PipSession + target_python=None, # type: Optional[TargetPython] + ignore_requires_python=None, # type: Optional[bool] + ): + # type: (...) -> PackageFinder + """ + Create a package finder appropriate to this requirement command. + + :param ignore_requires_python: Whether to ignore incompatible + "Requires-Python" values in links. Defaults to False. + """ + search_scope = make_search_scope(options) + selection_prefs = SelectionPreferences( + allow_yanked=True, + format_control=options.format_control, + allow_all_prereleases=options.pre, + prefer_binary=options.prefer_binary, + ignore_requires_python=ignore_requires_python, + ) + + return PackageFinder.create( + search_scope=search_scope, + selection_prefs=selection_prefs, + trusted_hosts=options.trusted_hosts, + session=session, + target_python=target_python, + ) diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index 22e0b380830..92665eae32b 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -4,8 +4,8 @@ import os from pip._internal.cli import cmdoptions -from pip._internal.cli.base_command import RequirementCommand from pip._internal.cli.cmdoptions import make_target_python +from pip._internal.cli.req_command import RequirementCommand from pip._internal.req import RequirementSet from pip._internal.req.req_tracker import RequirementTracker from pip._internal.utils.filesystem import check_path_owner diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index a4c5f5952e5..fc81b66f4d2 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -11,8 +11,8 @@ from pip._internal.cache import WheelCache from pip._internal.cli import cmdoptions -from pip._internal.cli.base_command import RequirementCommand from pip._internal.cli.cmdoptions import make_target_python +from pip._internal.cli.req_command import RequirementCommand from pip._internal.cli.status_codes import ERROR from pip._internal.exceptions import ( CommandError, diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index 88396330360..d4a0aa858d2 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -6,7 +6,7 @@ from pip._internal.cache import WheelCache from pip._internal.cli import cmdoptions -from pip._internal.cli.base_command import RequirementCommand +from pip._internal.cli.req_command import RequirementCommand from pip._internal.exceptions import CommandError, PreviousBuildDirError from pip._internal.req import RequirementSet from pip._internal.req.req_tracker import RequirementTracker From 7d29841ced8df7e3c4616ea7358f2f1ecc7c32a9 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Sun, 4 Aug 2019 19:56:41 +0530 Subject: [PATCH 0070/3170] Fix handling of tokens (single part credentials) in URLs (#6818) --- news/6795.bugfix | 1 + src/pip/_internal/download.py | 26 ++++++++++++++----- tests/unit/test_download.py | 47 ++++++++++++++++++++++++++++++----- 3 files changed, 62 insertions(+), 12 deletions(-) create mode 100644 news/6795.bugfix diff --git a/news/6795.bugfix b/news/6795.bugfix new file mode 100644 index 00000000000..f80bd9b4b2f --- /dev/null +++ b/news/6795.bugfix @@ -0,0 +1 @@ + Fix handling of tokens (single part credentials) in URLs. diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index 1578521c765..d5c57ee0395 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -307,7 +307,7 @@ def _get_new_credentials(self, original_url, allow_netrc=True, logger.debug("Found credentials in keyring for %s", netloc) return kr_auth - return None, None + return username, password def _get_url_and_credentials(self, original_url): """Return the credentials to use for the provided URL. @@ -324,15 +324,29 @@ def _get_url_and_credentials(self, original_url): # Use any stored credentials that we have for this netloc username, password = self.passwords.get(netloc, (None, None)) - # If nothing cached, acquire new credentials without prompting - # the user (e.g. from netrc, keyring, or similar). - if username is None or password is None: + if username is None and password is None: + # No stored credentials. Acquire new credentials without prompting + # the user. (e.g. from netrc, keyring, or the URL itself) username, password = self._get_new_credentials(original_url) - if username is not None and password is not None: - # Store the username and password + if username is not None or password is not None: + # Convert the username and password if they're None, so that + # this netloc will show up as "cached" in the conditional above. + # Further, HTTPBasicAuth doesn't accept None, so it makes sense to + # cache the value that is going to be used. + username = username or "" + password = password or "" + + # Store any acquired credentials. self.passwords[netloc] = (username, password) + assert ( + # Credentials were found + (username is not None and password is not None) or + # Credentials were not found + (username is None and password is None) + ), "Could not load credentials from url: {}".format(original_url) + return url, username, password def __call__(self, req): diff --git a/tests/unit/test_download.py b/tests/unit/test_download.py index d63d7420178..42918c20e75 100644 --- a/tests/unit/test_download.py +++ b/tests/unit/test_download.py @@ -490,18 +490,53 @@ def test_insecure_host_cache_is_not_enabled(self, tmpdir): assert not hasattr(session.adapters["https://example.com/"], "cache") -def test_get_credentials(): +@pytest.mark.parametrize(["input_url", "url", "username", "password"], [ + ( + "http://user%40email.com:password@example.com/path", + "http://example.com/path", + "user@email.com", + "password", + ), + ( + "http://username:password@example.com/path", + "http://example.com/path", + "username", + "password", + ), + ( + "http://token@example.com/path", + "http://example.com/path", + "token", + "", + ), + ( + "http://example.com/path", + "http://example.com/path", + None, + None, + ), +]) +def test_get_credentials_parses_correctly(input_url, url, username, password): auth = MultiDomainBasicAuth() get = auth._get_url_and_credentials # Check URL parsing - assert get("http://foo:bar@example.com/path") \ - == ('http://example.com/path', 'foo', 'bar') - assert auth.passwords['example.com'] == ('foo', 'bar') + assert get(input_url) == (url, username, password) + assert ( + # There are no credentials in the URL + (username is None and password is None) or + # Credentials were found and "cached" appropriately + auth.passwords['example.com'] == (username, password) + ) + +def test_get_credentials_uses_cached_credentials(): + auth = MultiDomainBasicAuth() auth.passwords['example.com'] = ('user', 'pass') - assert get("http://foo:bar@example.com/path") \ - == ('http://example.com/path', 'user', 'pass') + + got = auth._get_url_and_credentials("http://foo:bar@example.com/path") + expected = ('http://example.com/path', 'user', 'pass') + assert got == expected def test_get_index_url_credentials(): From 12e63b9deffe7def72bc82e50b3e50bbcacd6e08 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Sun, 4 Aug 2019 22:36:26 +0530 Subject: [PATCH 0071/3170] Move logic for SecureTransport injection to utils --- src/pip/_internal/__init__.py | 19 +-------------- .../_internal/utils/inject_securetransport.py | 24 +++++++++++++++++++ 2 files changed, 25 insertions(+), 18 deletions(-) create mode 100644 src/pip/_internal/utils/inject_securetransport.py diff --git a/src/pip/_internal/__init__.py b/src/pip/_internal/__init__.py index bb8f0ff8705..4c95284a5ba 100755 --- a/src/pip/_internal/__init__.py +++ b/src/pip/_internal/__init__.py @@ -19,24 +19,7 @@ from pip._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 pip._vendor.urllib3.contrib import securetransport - except (ImportError, OSError): - pass - else: - securetransport.inject_into_urllib3() - +import pip._internal.utils.inject_securetransport # noqa from pip._internal.cli.autocompletion import autocomplete from pip._internal.cli.main_parser import parse_command from pip._internal.commands import create_command diff --git a/src/pip/_internal/utils/inject_securetransport.py b/src/pip/_internal/utils/inject_securetransport.py new file mode 100644 index 00000000000..f56731f6bf1 --- /dev/null +++ b/src/pip/_internal/utils/inject_securetransport.py @@ -0,0 +1,24 @@ +"""A helper module that injects SecureTransport, on import. + +The import should be done as early as possible, to ensure all requests and +sessions (or whatever) are created after injecting SecureTransport. + +Note that we only do the injection on macOS, when the linked OpenSSL is too +old to handle TLSv1.2. +""" + +try: + import ssl +except ImportError: + pass +else: + import sys + + # Checks for OpenSSL 1.0.1 on MacOS + if sys.platform == "darwin" and ssl.OPENSSL_VERSION_NUMBER < 0x1000100f: + try: + from pip._vendor.urllib3.contrib import securetransport + except (ImportError, OSError): + pass + else: + securetransport.inject_into_urllib3() From 2f9f91882a3adacfbc713e637648bbcdc54dedd5 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Sun, 4 Aug 2019 22:45:03 +0530 Subject: [PATCH 0072/3170] Simplify ignoring urllib3 warnings --- src/pip/_internal/__init__.py | 30 ++++++++++++++---------------- 1 file changed, 14 insertions(+), 16 deletions(-) diff --git a/src/pip/_internal/__init__.py b/src/pip/_internal/__init__.py index 4c95284a5ba..bb5cbff323c 100755 --- a/src/pip/_internal/__init__.py +++ b/src/pip/_internal/__init__.py @@ -4,20 +4,15 @@ import locale import logging import os -import warnings - import sys +import warnings -# 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-stderr -# 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 pip._vendor.urllib3.exceptions import DependencyWarning -warnings.filterwarnings("ignore", category=DependencyWarning) # noqa +# We ignore certain warnings from urllib3, since they are not relevant to pip's +# usecases. +from pip._vendor.urllib3.exceptions import ( + DependencyWarning, + InsecureRequestWarning, +) import pip._internal.utils.inject_securetransport # noqa from pip._internal.cli.autocompletion import autocomplete @@ -25,12 +20,15 @@ from pip._internal.commands import create_command from pip._internal.exceptions import PipError from pip._internal.utils import deprecation -from pip._vendor.urllib3.exceptions import InsecureRequestWarning -logger = logging.getLogger(__name__) - -# Hide the InsecureRequestWarning from urllib3 +# Raised when using --trusted-host. warnings.filterwarnings("ignore", category=InsecureRequestWarning) +# Raised since socks support depends on PySocks, which may not be installed. +# Barry Warsaw noted (on 2016-06-17) that this should be done before +# importing pip.vcs, which has since moved to pip._internal.vcs. +warnings.filterwarnings("ignore", category=DependencyWarning) + +logger = logging.getLogger(__name__) def main(args=None): From 1429a55f402246f80c994e65aab6b17c8f055453 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Mon, 5 Aug 2019 17:51:28 +0530 Subject: [PATCH 0073/3170] Add a skeleton for architecture documentation --- docs/html/development/architecture/index.rst | 20 ++++++++++++++++++++ docs/html/development/index.rst | 1 + 2 files changed, 21 insertions(+) create mode 100644 docs/html/development/architecture/index.rst diff --git a/docs/html/development/architecture/index.rst b/docs/html/development/architecture/index.rst new file mode 100644 index 00000000000..4f5b4515237 --- /dev/null +++ b/docs/html/development/architecture/index.rst @@ -0,0 +1,20 @@ +=============================== +Architecture of pip's internals +=============================== + +.. note:: + This section of the documentation is currently being written. pip + developers welcome help to complete this documentation. If you're + interested in helping out, please let us know in the `tracking issue`_. + +.. note:: + Direct use of pip's internals is *not supported*. + For more details, see :ref:`Using pip from your program`. + + +.. toctree:: + :maxdepth: 2 + + + +.. _`tracking issue`: https://github.com/pypa/pip/issues/6831 diff --git a/docs/html/development/index.rst b/docs/html/development/index.rst index 85ab69ec64b..7d10230e00c 100644 --- a/docs/html/development/index.rst +++ b/docs/html/development/index.rst @@ -14,6 +14,7 @@ or the `pypa-dev mailing list`_, to ask questions or get involved. getting-started contributing + architecture/index release-process vendoring-policy From e50bf782daa38ac3ac735e8b352691c0ebbd1e9d Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Mon, 5 Aug 2019 18:09:07 +0530 Subject: [PATCH 0074/3170] Add correct builder to ReadTheDocs configuration --- .readthedocs.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.readthedocs.yml b/.readthedocs.yml index 672bc20636f..c123a1939fb 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -1,6 +1,7 @@ version: 2 sphinx: + builder: htmldir configuration: docs/html/conf.py python: From aaf86008d0fc59ad217de3273476199312c6e5a2 Mon Sep 17 00:00:00 2001 From: Albert Tugushev Date: Tue, 6 Aug 2019 01:27:44 +0300 Subject: [PATCH 0075/3170] Unskip pip install from wheel with headers Closes #1373. --- tests/functional/test_install_wheel.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/functional/test_install_wheel.py b/tests/functional/test_install_wheel.py index b4871a2e027..fd764fea391 100644 --- a/tests/functional/test_install_wheel.py +++ b/tests/functional/test_install_wheel.py @@ -99,9 +99,6 @@ def test_basic_install_from_wheel_file(script, data): result.stdout) -# header installs are broke in pypy virtualenvs -# https://github.com/pypa/virtualenv/issues/510 -@pytest.mark.skipif("hasattr(sys, 'pypy_version_info')") def test_install_from_wheel_with_headers(script, data): """ Test installing from a wheel file with headers From 5280483c06646d19c05038e1b765c9ac9ad5f61b Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Wed, 7 Aug 2019 02:40:08 +0530 Subject: [PATCH 0076/3170] Clarify who we welcome help from --- docs/html/development/architecture/index.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/html/development/architecture/index.rst b/docs/html/development/architecture/index.rst index 4f5b4515237..f3f221565c4 100644 --- a/docs/html/development/architecture/index.rst +++ b/docs/html/development/architecture/index.rst @@ -4,7 +4,7 @@ Architecture of pip's internals .. note:: This section of the documentation is currently being written. pip - developers welcome help to complete this documentation. If you're + developers welcome your help to complete this documentation. If you're interested in helping out, please let us know in the `tracking issue`_. .. note:: From e74fd24c71de688d5b93e426c2f4961a4de2ccd0 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 7 Aug 2019 13:04:02 -0700 Subject: [PATCH 0077/3170] Eliminate base_command.py's import dependency on PipSession. --- src/pip/_internal/cli/base_command.py | 82 +++--------------- src/pip/_internal/cli/req_command.py | 106 ++++++++++++++++++++++-- src/pip/_internal/commands/list.py | 4 +- src/pip/_internal/commands/search.py | 3 +- src/pip/_internal/commands/uninstall.py | 3 +- 5 files changed, 118 insertions(+), 80 deletions(-) diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index c8e7fe4095c..b4677d75668 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -25,7 +25,6 @@ UNKNOWN_ERROR, VIRTUALENV_NOT_FOUND, ) -from pip._internal.download import PipSession from pip._internal.exceptions import ( BadCommand, CommandError, @@ -35,8 +34,7 @@ ) from pip._internal.utils.deprecation import deprecated from pip._internal.utils.logging import BrokenStdoutLoggingError, setup_logging -from pip._internal.utils.misc import get_prog, normalize_path -from pip._internal.utils.outdated import pip_version_check +from pip._internal.utils.misc import get_prog from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.virtualenv import running_under_virtualenv @@ -81,62 +79,20 @@ def __init__(self, name, summary, isolated=False): ) self.parser.add_option_group(gen_opts) + def handle_pip_version_check(self, options): + # type: (Values) -> None + """ + This is a no-op so that commands by default do not do the pip version + check. + """ + # Make sure we do the pip version check if the index_group options + # are present. + assert not hasattr(options, 'no_index') + def run(self, options, args): # type: (Values, List[Any]) -> Any raise NotImplementedError - @classmethod - def _get_index_urls(cls, options): - """Return a list of index urls from user-provided options.""" - index_urls = [] - if not getattr(options, "no_index", False): - url = getattr(options, "index_url", None) - if url: - index_urls.append(url) - urls = getattr(options, "extra_index_urls", None) - if urls: - index_urls.extend(urls) - # Return None rather than an empty list - return index_urls or None - - def _build_session(self, options, retries=None, timeout=None): - # type: (Values, Optional[int], Optional[int]) -> PipSession - session = PipSession( - cache=( - normalize_path(os.path.join(options.cache_dir, "http")) - if options.cache_dir else None - ), - retries=retries if retries is not None else options.retries, - insecure_hosts=options.trusted_hosts, - index_urls=self._get_index_urls(options), - ) - - # Handle custom ca-bundles from the user - if options.cert: - session.verify = options.cert - - # Handle SSL client certificate - if options.client_cert: - session.cert = options.client_cert - - # Handle timeouts - if options.timeout or timeout: - session.timeout = ( - timeout if timeout is not None else options.timeout - ) - - # Handle configured proxies - if options.proxy: - session.proxies = { - "http": options.proxy, - "https": options.proxy, - } - - # Determine if we can prompt the user for authentication or not - session.auth.prompting = not options.no_input - - return session - def parse_args(self, args): # type: (List[str]) -> Tuple # factored out for testability @@ -226,21 +182,7 @@ def main(self, args): return UNKNOWN_ERROR finally: - allow_version_check = ( - # Does this command have the index_group options? - hasattr(options, "no_index") and - # Is this command allowed to perform this check? - not (options.disable_pip_version_check or options.no_index) - ) - # Check if we're using the latest version of pip available - if allow_version_check: - session = self._build_session( - options, - retries=0, - timeout=min(5, options.timeout) - ) - with session: - pip_version_check(session, options) + self.handle_pip_version_check(options) # Shutdown the logging module logging.shutdown() diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 63776f523b1..8b1d4dcc811 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -1,12 +1,15 @@ -"""Contains the RequirementCommand base class. +"""Contains the Command base classes that depend on PipSession. -This is in a separate module so that Command classes not inheriting from -RequirementCommand don't need to import e.g. the PackageFinder machinery -and all its vendored dependencies. +The classes in this module are in a separate module so the commands not +needing download / PackageFinder capability don't unnecessarily import the +PackageFinder machinery and all its vendored dependencies, etc. """ +import os + from pip._internal.cli.base_command import Command from pip._internal.cli.cmdoptions import make_search_scope +from pip._internal.download import PipSession from pip._internal.exceptions import CommandError from pip._internal.index import PackageFinder from pip._internal.legacy_resolve import Resolver @@ -17,20 +20,111 @@ install_req_from_line, ) from pip._internal.req.req_file import parse_requirements +from pip._internal.utils.misc import 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 optparse import Values from typing import List, Optional, Tuple from pip._internal.cache import WheelCache - from pip._internal.download import PipSession from pip._internal.models.target_python import TargetPython from pip._internal.req.req_set import RequirementSet from pip._internal.req.req_tracker import RequirementTracker from pip._internal.utils.temp_dir import TempDirectory -class RequirementCommand(Command): +class SessionCommandMixin(object): + + """ + A class mixin for command classes needing _build_session(). + """ + + @classmethod + def _get_index_urls(cls, options): + """Return a list of index urls from user-provided options.""" + index_urls = [] + if not getattr(options, "no_index", False): + url = getattr(options, "index_url", None) + if url: + index_urls.append(url) + urls = getattr(options, "extra_index_urls", None) + if urls: + index_urls.extend(urls) + # Return None rather than an empty list + return index_urls or None + + def _build_session(self, options, retries=None, timeout=None): + # type: (Values, Optional[int], Optional[int]) -> PipSession + session = PipSession( + cache=( + normalize_path(os.path.join(options.cache_dir, "http")) + if options.cache_dir else None + ), + retries=retries if retries is not None else options.retries, + insecure_hosts=options.trusted_hosts, + index_urls=self._get_index_urls(options), + ) + + # Handle custom ca-bundles from the user + if options.cert: + session.verify = options.cert + + # Handle SSL client certificate + if options.client_cert: + session.cert = options.client_cert + + # Handle timeouts + if options.timeout or timeout: + session.timeout = ( + timeout if timeout is not None else options.timeout + ) + + # Handle configured proxies + if options.proxy: + session.proxies = { + "http": options.proxy, + "https": options.proxy, + } + + # Determine if we can prompt the user for authentication or not + session.auth.prompting = not options.no_input + + return session + + +class IndexGroupCommand(SessionCommandMixin, Command): + + """ + Abstract base class for commands with the index_group options. + + This also corresponds to the commands that permit the pip version check. + """ + + def handle_pip_version_check(self, options): + # type: (Values) -> None + """ + Do the pip version check if not disabled. + + This overrides the default behavior of not doing the check. + """ + # Make sure the index_group options are present. + assert hasattr(options, 'no_index') + + if options.disable_pip_version_check or options.no_index: + return + + # Otherwise, check if we're using the latest version of pip available. + session = self._build_session( + options, + retries=0, + timeout=min(5, options.timeout) + ) + with session: + pip_version_check(session, options) + + +class RequirementCommand(IndexGroupCommand): @staticmethod def make_requirement_preparer( diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index 2fd39097c12..aacd5680ca1 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -7,8 +7,8 @@ from pip._vendor.six.moves import zip_longest from pip._internal.cli import cmdoptions -from pip._internal.cli.base_command import Command from pip._internal.cli.cmdoptions import make_search_scope +from pip._internal.cli.req_command import IndexGroupCommand from pip._internal.exceptions import CommandError from pip._internal.index import PackageFinder from pip._internal.models.selection_prefs import SelectionPreferences @@ -21,7 +21,7 @@ logger = logging.getLogger(__name__) -class ListCommand(Command): +class ListCommand(IndexGroupCommand): """ List installed packages, including editables. diff --git a/src/pip/_internal/commands/search.py b/src/pip/_internal/commands/search.py index c96f0b90423..6889375e06d 100644 --- a/src/pip/_internal/commands/search.py +++ b/src/pip/_internal/commands/search.py @@ -12,6 +12,7 @@ from pip._vendor.six.moves import xmlrpc_client # type: ignore from pip._internal.cli.base_command import Command +from pip._internal.cli.req_command import SessionCommandMixin from pip._internal.cli.status_codes import NO_MATCHES_FOUND, SUCCESS from pip._internal.download import PipXmlrpcTransport from pip._internal.exceptions import CommandError @@ -22,7 +23,7 @@ logger = logging.getLogger(__name__) -class SearchCommand(Command): +class SearchCommand(SessionCommandMixin, Command): """Search for PyPI packages whose name or summary contains .""" usage = """ diff --git a/src/pip/_internal/commands/uninstall.py b/src/pip/_internal/commands/uninstall.py index ede23083857..6d72400e6b3 100644 --- a/src/pip/_internal/commands/uninstall.py +++ b/src/pip/_internal/commands/uninstall.py @@ -3,13 +3,14 @@ from pip._vendor.packaging.utils import canonicalize_name from pip._internal.cli.base_command import Command +from pip._internal.cli.req_command import SessionCommandMixin from pip._internal.exceptions import InstallationError from pip._internal.req import parse_requirements from pip._internal.req.constructors import install_req_from_line from pip._internal.utils.misc import protect_pip_from_modification_on_windows -class UninstallCommand(Command): +class UninstallCommand(SessionCommandMixin, Command): """ Uninstall packages. From 24181c6f9d582fb6bc475afcf47141ca3bd47f80 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 7 Aug 2019 13:07:09 -0700 Subject: [PATCH 0078/3170] Make indentation consistent in req_command.py. --- src/pip/_internal/cli/req_command.py | 51 ++++++++++++++-------------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 8b1d4dcc811..cc1d392f9fe 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -128,11 +128,11 @@ class RequirementCommand(IndexGroupCommand): @staticmethod def make_requirement_preparer( - temp_directory, # type: TempDirectory - options, # type: Values - req_tracker, # type: RequirementTracker - download_dir=None, # type: str - wheel_download_dir=None, # type: str + temp_directory, # type: TempDirectory + options, # type: Values + req_tracker, # type: RequirementTracker + download_dir=None, # type: str + wheel_download_dir=None, # type: str ): # type: (...) -> RequirementPreparer """ @@ -150,18 +150,18 @@ def make_requirement_preparer( @staticmethod def make_resolver( - preparer, # type: RequirementPreparer - session, # type: PipSession - finder, # type: PackageFinder - options, # type: Values - wheel_cache=None, # type: Optional[WheelCache] - use_user_site=False, # type: bool - ignore_installed=True, # type: bool - ignore_requires_python=False, # type: bool - force_reinstall=False, # type: bool - upgrade_strategy="to-satisfy-only", # type: str - use_pep517=None, # type: Optional[bool] - py_version_info=None # type: Optional[Tuple[int, ...]] + preparer, # type: RequirementPreparer + session, # type: PipSession + finder, # type: PackageFinder + options, # type: Values + wheel_cache=None, # type: Optional[WheelCache] + use_user_site=False, # type: bool + ignore_installed=True, # type: bool + ignore_requires_python=False, # type: bool + force_reinstall=False, # type: bool + upgrade_strategy="to-satisfy-only", # type: str + use_pep517=None, # type: Optional[bool] + py_version_info=None # type: Optional[Tuple[int, ...]] ): # type: (...) -> Resolver """ @@ -184,14 +184,15 @@ def make_resolver( ) @staticmethod - def populate_requirement_set(requirement_set, # type: RequirementSet - args, # type: List[str] - options, # type: Values - finder, # type: PackageFinder - session, # type: PipSession - name, # type: str - wheel_cache # type: Optional[WheelCache] - ): + def populate_requirement_set( + requirement_set, # type: RequirementSet + args, # type: List[str] + options, # type: Values + finder, # type: PackageFinder + session, # type: PipSession + name, # type: str + wheel_cache, # type: Optional[WheelCache] + ): # type: (...) -> None """ Marshal cmd line args into a requirement set. From 505456fed7bdbd6b2cd78eae10b3b64657cd377b Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 7 Aug 2019 14:08:12 -0700 Subject: [PATCH 0079/3170] Test the command class inheritance for each command. --- tests/unit/test_commands.py | 54 +++++++++++++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/tests/unit/test_commands.py b/tests/unit/test_commands.py index 324e322a3f8..42a18a819fb 100644 --- a/tests/unit/test_commands.py +++ b/tests/unit/test_commands.py @@ -1,8 +1,22 @@ import pytest +from pip._internal.cli.req_command import ( + IndexGroupCommand, + RequirementCommand, + SessionCommandMixin, +) from pip._internal.commands import commands_dict, create_command +def check_commands(pred, expected): + """ + Check the commands satisfying a predicate. + """ + commands = [create_command(name) for name in sorted(commands_dict)] + actual = [command.name for command in commands if pred(command)] + assert actual == expected, 'actual: {}'.format(actual) + + def test_commands_dict__order(): """ Check the ordering of commands_dict. @@ -20,3 +34,43 @@ def test_create_command(name): command = create_command(name) assert command.name == name assert command.summary == commands_dict[name].summary + + +def test_session_commands(): + """ + Test which commands inherit from SessionCommandMixin. + """ + def is_session_command(command): + return isinstance(command, SessionCommandMixin) + + expected = ['download', 'install', 'list', 'search', 'uninstall', 'wheel'] + check_commands(is_session_command, expected) + + +def test_index_group_commands(): + """ + Test the commands inheriting from IndexGroupCommand. + """ + expected = ['download', 'install', 'list', 'wheel'] + + def is_index_group_command(command): + return isinstance(command, IndexGroupCommand) + + check_commands(is_index_group_command, expected) + + # Also check that the commands inheriting from IndexGroupCommand are + # exactly the commands with the --no-index option. + def has_option_no_index(command): + return command.parser.has_option('--no-index') + + check_commands(has_option_no_index, expected) + + +def test_requirement_commands(): + """ + Test which commands inherit from RequirementCommand. + """ + def is_requirement_command(command): + return isinstance(command, RequirementCommand) + + check_commands(is_requirement_command, ['download', 'install', 'wheel']) From 2571398f4d0971ef0508c22a207d504a35683d27 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Wed, 7 Aug 2019 22:59:10 -0400 Subject: [PATCH 0080/3170] Fix tests when running in git linked worktree. --- tests/conftest.py | 10 ++++++++-- tests/unit/test_vcs.py | 8 ++++++-- 2 files changed, 14 insertions(+), 4 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index d1a12d65e5d..d16ff56758d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -169,10 +169,16 @@ def isolate(tmpdir): @pytest.fixture(scope='session') def pip_src(tmpdir_factory): def not_code_files_and_folders(path, names): - # In the root directory, ignore all folders except "src" + # In the root directory... if path == SRC_DIR: + # ignore all folders except "src" folders = {name for name in names if os.path.isdir(path / name)} - return folders - {"src"} + to_ignore = folders - {"src"} + # and ignore ".git" if present (which may be a file if in a linked + # worktree). + if ".git" in names: + to_ignore.add(".git") + return to_ignore # Ignore all compiled files and egg-info. ignored = list() diff --git a/tests/unit/test_vcs.py b/tests/unit/test_vcs.py index 66754c667ac..6e0fdc3fa0f 100644 --- a/tests/unit/test_vcs.py +++ b/tests/unit/test_vcs.py @@ -123,15 +123,19 @@ def test_should_add_vcs_url_prefix(vcs_cls, remote_url, expected): assert actual == expected -@patch('pip._internal.vcs.git.Git.get_revision') @patch('pip._internal.vcs.git.Git.get_remote_url') +@patch('pip._internal.vcs.git.Git.get_revision') +@patch('pip._internal.vcs.git.Git.get_subdirectory') @pytest.mark.network -def test_git_get_src_requirements(mock_get_remote_url, mock_get_revision): +def test_git_get_src_requirements( + mock_get_subdirectory, mock_get_revision, mock_get_remote_url +): git_url = 'https://github.com/pypa/pip-test-package' sha = '5547fa909e83df8bd743d3978d6667497983a4b7' mock_get_remote_url.return_value = git_url mock_get_revision.return_value = sha + mock_get_subdirectory.return_value = None ret = Git.get_src_requirement('.', 'pip-test-package') From 9b1ece3c96de67b3e757ba218f6da861f2f1ce09 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 7 Aug 2019 22:03:49 -0700 Subject: [PATCH 0081/3170] Add some handle_pip_version_check() tests. --- tests/unit/test_base_command.py | 12 +++++++++ tests/unit/test_commands.py | 44 ++++++++++++++++++++++++++++++--- 2 files changed, 52 insertions(+), 4 deletions(-) diff --git a/tests/unit/test_base_command.py b/tests/unit/test_base_command.py index fc6cf2b7a78..ba34b3922f8 100644 --- a/tests/unit/test_base_command.py +++ b/tests/unit/test_base_command.py @@ -2,6 +2,8 @@ import os import time +from mock import patch + from pip._internal.cli.base_command import Command from pip._internal.utils.logging import BrokenStdoutLoggingError @@ -72,6 +74,16 @@ def test_raise_broken_stdout__debug_logging(self, capsys): assert 'Traceback (most recent call last):' in stderr +@patch('pip._internal.cli.req_command.Command.handle_pip_version_check') +def test_handle_pip_version_check_called(mock_handle_version_check): + """ + Check that Command.handle_pip_version_check() is called. + """ + cmd = FakeCommand() + cmd.main([]) + mock_handle_version_check.assert_called_once() + + class Test_base_command_logging(object): """ Test `pip.base_command.Command` setting up logging consumers based on diff --git a/tests/unit/test_commands.py b/tests/unit/test_commands.py index 42a18a819fb..be6c783524d 100644 --- a/tests/unit/test_commands.py +++ b/tests/unit/test_commands.py @@ -1,4 +1,5 @@ import pytest +from mock import patch from pip._internal.cli.req_command import ( IndexGroupCommand, @@ -7,6 +8,10 @@ ) from pip._internal.commands import commands_dict, create_command +# These are the expected names of the commands whose classes inherit from +# IndexGroupCommand. +EXPECTED_INDEX_GROUP_COMMANDS = ['download', 'install', 'list', 'wheel'] + def check_commands(pred, expected): """ @@ -51,19 +56,50 @@ def test_index_group_commands(): """ Test the commands inheriting from IndexGroupCommand. """ - expected = ['download', 'install', 'list', 'wheel'] - def is_index_group_command(command): return isinstance(command, IndexGroupCommand) - check_commands(is_index_group_command, expected) + check_commands(is_index_group_command, EXPECTED_INDEX_GROUP_COMMANDS) # Also check that the commands inheriting from IndexGroupCommand are # exactly the commands with the --no-index option. def has_option_no_index(command): return command.parser.has_option('--no-index') - check_commands(has_option_no_index, expected) + check_commands(has_option_no_index, EXPECTED_INDEX_GROUP_COMMANDS) + + +@pytest.mark.parametrize('command_name', EXPECTED_INDEX_GROUP_COMMANDS) +@pytest.mark.parametrize( + 'disable_pip_version_check, no_index, expected_called', + [ + # pip_version_check() is only called when both + # disable_pip_version_check and no_index are False. + (False, False, True), + (False, True, False), + (True, False, False), + (True, True, False), + ], +) +@patch('pip._internal.cli.req_command.pip_version_check') +def test_index_group_handle_pip_version_check( + mock_version_check, command_name, disable_pip_version_check, no_index, + expected_called, +): + """ + Test whether pip_version_check() is called when handle_pip_version_check() + is called, for each of the IndexGroupCommand classes. + """ + command = create_command(command_name) + options = command.parser.get_default_values() + options.disable_pip_version_check = disable_pip_version_check + options.no_index = no_index + + command.handle_pip_version_check(options) + if expected_called: + mock_version_check.assert_called_once() + else: + mock_version_check.assert_not_called() def test_requirement_commands(): From 234ef4391567c08f319c08b33ab1fd08bd9d3d0d Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 7 Aug 2019 23:33:53 -0700 Subject: [PATCH 0082/3170] Remove mypy: strict-optional=False from base_command.py. --- src/pip/_internal/cli/base_command.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index b4677d75668..694527c1d65 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -1,8 +1,5 @@ """Base Command class, and related routines""" -# The following comment should be removed at some point in the future. -# mypy: strict-optional=False - from __future__ import absolute_import, print_function import logging @@ -39,7 +36,7 @@ from pip._internal.utils.virtualenv import running_under_virtualenv if MYPY_CHECK_RUNNING: - from typing import Optional, List, Tuple, Any + from typing import List, Tuple, Any from optparse import Values __all__ = ['Command'] @@ -48,15 +45,14 @@ class Command(object): - name = None # type: Optional[str] - usage = None # type: Optional[str] + usage = None # type: str ignore_require_venv = False # type: bool def __init__(self, name, summary, isolated=False): # type: (str, str, bool) -> None parser_kw = { 'usage': self.usage, - 'prog': '%s %s' % (get_prog(), self.name), + 'prog': '%s %s' % (get_prog(), name), 'formatter': UpdatingDefaultsHelpFormatter(), 'add_help_option': False, 'name': name, From 23446f6d0e7cc1609afcb05534d0ece876c71358 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Thu, 8 Aug 2019 17:29:02 -0700 Subject: [PATCH 0083/3170] Remove the unneeded name argument from populate_requirement_set(). --- src/pip/_internal/cli/req_command.py | 5 ++--- src/pip/_internal/commands/download.py | 1 - src/pip/_internal/commands/install.py | 2 +- src/pip/_internal/commands/wheel.py | 2 +- tests/unit/test_req.py | 3 +-- 5 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index cc1d392f9fe..dc29f17434d 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -183,14 +183,13 @@ def make_resolver( py_version_info=py_version_info ) - @staticmethod def populate_requirement_set( + self, requirement_set, # type: RequirementSet args, # type: List[str] options, # type: Values finder, # type: PackageFinder session, # type: PipSession - name, # type: str wheel_cache, # type: Optional[WheelCache] ): # type: (...) -> None @@ -240,7 +239,7 @@ def populate_requirement_set( requirement_set.require_hashes = options.require_hashes if not (args or options.editables or options.requirements): - opts = {'name': name} + opts = {'name': self.name} if options.find_links: raise CommandError( 'You must give at least one requirement to %(name)s ' diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index 92665eae32b..f4526b57a93 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -120,7 +120,6 @@ def run(self, options, args): options, finder, session, - self.name, None ) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index fc81b66f4d2..4d7d50759ce 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -318,7 +318,7 @@ def run(self, options, args): try: self.populate_requirement_set( requirement_set, args, options, finder, session, - self.name, wheel_cache + wheel_cache ) preparer = self.make_requirement_preparer( temp_directory=directory, diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index d4a0aa858d2..cf49b600586 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -124,7 +124,7 @@ def run(self, options, args): try: self.populate_requirement_set( requirement_set, args, options, finder, session, - self.name, wheel_cache + wheel_cache ) preparer = self.make_requirement_preparer( diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index 4fe71cbfde5..ebd3e8f03bf 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -190,8 +190,7 @@ def test_missing_hash_with_require_hashes_in_reqs_file(self, data, tmpdir): with requirements_file('--require-hashes', tmpdir) as reqs_file: options, args = command.parse_args(['-r', reqs_file]) command.populate_requirement_set( - req_set, args, options, finder, session, command.name, - wheel_cache=None, + req_set, args, options, finder, session, wheel_cache=None, ) assert req_set.require_hashes From 76e1f1b5e6172f3a603f42fd9f15a3ef980c094d Mon Sep 17 00:00:00 2001 From: Christopher Hunt Date: Thu, 8 Aug 2019 20:39:12 -0400 Subject: [PATCH 0084/3170] Simplify file cache. (#6845) --- src/pip/_internal/download.py | 80 ++++++++++++++--------------------- 1 file changed, 31 insertions(+), 49 deletions(-) diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index a4db2382438..3ad5eb31a04 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -10,6 +10,7 @@ import re import shutil import sys +from contextlib import contextmanager from pip._vendor import requests, urllib3 from pip._vendor.cachecontrol import CacheControlAdapter @@ -482,70 +483,38 @@ def close(self): pass +@contextmanager +def suppressed_cache_errors(): + """If we can't access the cache then we can just skip caching and process + requests as if caching wasn't enabled. + """ + try: + yield + except (LockError, OSError, IOError): + pass + + class SafeFileCache(FileCache): """ A file based cache which is safe to use even when the target directory may not be accessible or writable. """ - def __init__(self, *args, **kwargs): - super(SafeFileCache, self).__init__(*args, **kwargs) - - # Check to ensure that the directory containing our cache directory - # is owned by the user current executing pip. If it does not exist - # we will check the parent directory until we find one that does exist. - # If it is not owned by the user executing pip then we will disable - # the cache and log a warning. - if not check_path_owner(self.directory): - logger.warning( - "The directory '%s' or its parent directory is not owned by " - "the current user and the cache has been disabled. Please " - "check the permissions and owner of that directory. If " - "executing pip with sudo, you may want sudo's -H flag.", - self.directory, - ) - - # Set our directory to None to disable the Cache - self.directory = None + def __init__(self, directory, *args, **kwargs): + assert directory is not None, "Cache directory must not be None." + super(SafeFileCache, self).__init__(directory, *args, **kwargs) def get(self, *args, **kwargs): - # If we don't have a directory, then the cache should be a no-op. - if self.directory is None: - return - - try: + with suppressed_cache_errors(): return super(SafeFileCache, self).get(*args, **kwargs) - except (LockError, OSError, IOError): - # We intentionally silence this error, if we can't access the cache - # then we can just skip caching and process the request as if - # caching wasn't enabled. - pass def set(self, *args, **kwargs): - # If we don't have a directory, then the cache should be a no-op. - if self.directory is None: - return - - try: + with suppressed_cache_errors(): return super(SafeFileCache, self).set(*args, **kwargs) - except (LockError, OSError, IOError): - # We intentionally silence this error, if we can't access the cache - # then we can just skip caching and process the request as if - # caching wasn't enabled. - pass def delete(self, *args, **kwargs): - # If we don't have a directory, then the cache should be a no-op. - if self.directory is None: - return - - try: + with suppressed_cache_errors(): return super(SafeFileCache, self).delete(*args, **kwargs) - except (LockError, OSError, IOError): - # We intentionally silence this error, if we can't access the cache - # then we can just skip caching and process the request as if - # caching wasn't enabled. - pass class InsecureHTTPAdapter(HTTPAdapter): @@ -593,6 +562,19 @@ def __init__(self, *args, **kwargs): backoff_factor=0.25, ) + # Check to ensure that the directory containing our cache directory + # is owned by the user current executing pip. If it does not exist + # we will check the parent directory until we find one that does exist. + if cache and not check_path_owner(cache): + logger.warning( + "The directory '%s' or its parent directory is not owned by " + "the current user and the cache has been disabled. Please " + "check the permissions and owner of that directory. If " + "executing pip with sudo, you may want sudo's -H flag.", + cache, + ) + cache = None + # We want to _only_ cache responses on securely fetched origins. We do # this because we can't validate the response of an insecurely fetched # origin, and we don't want someone to be able to poison the cache and From 075358a0ec2f593458b0f87cd51daf2d5d7db5e8 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Thu, 8 Aug 2019 22:49:48 -0700 Subject: [PATCH 0085/3170] Simplify Wheel.supported(). --- src/pip/_internal/wheel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index 35c73210bbf..64992bdaf15 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -751,10 +751,10 @@ def support_index_min(self, tags=None): def supported(self, tags=None): # type: (Optional[List[Pep425Tag]]) -> bool - """Is this wheel supported on this system?""" + """Return whether this wheel is supported by one of the given tags.""" if tags is None: # for mock tags = pep425tags.get_supported() - return bool(set(tags).intersection(self.file_tags)) + return not self.file_tags.isdisjoint(tags) def _contains_egg_info( From bb9cb5e19da76910ea2d8df3e8bdc79babb55cda Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Thu, 8 Aug 2019 22:58:17 -0700 Subject: [PATCH 0086/3170] Change the return type of Wheel.support_index_min() to int. --- src/pip/_internal/wheel.py | 15 +++++++++------ tests/unit/test_wheel.py | 7 ++++--- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index 64992bdaf15..d864ffb605c 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -737,17 +737,20 @@ def get_formatted_file_tags(self): return sorted(format_tag(tag) for tag in self.file_tags) def support_index_min(self, tags=None): - # type: (Optional[List[Pep425Tag]]) -> Optional[int] + # type: (Optional[List[Pep425Tag]]) -> int """ Return the lowest index that one of the wheel's file_tag combinations - achieves in the supported_tags list e.g. if there are 8 supported tags, - and one of the file tags is first in the list, then return 0. Returns - None is the wheel is not supported. + achieves in the given list of supported tags. + + For example, if there are 8 supported tags and one of the file tags + is first in the list, then return 0. + + :raises ValueError: If none of the wheel's file tags match one of + the supported tags. """ if tags is None: # for mock 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 + return min(tags.index(tag) for tag in self.file_tags if tag in tags) def supported(self, tags=None): # type: (Optional[List[Pep425Tag]]) -> bool diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index fd8e9914ca0..bc8894ed165 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -566,12 +566,13 @@ def test_support_index_min(self): w = wheel.Wheel('simple-0.1-py2-none-TEST.whl') assert w.support_index_min(tags=tags) == 0 - def test_support_index_min_none(self): + def test_support_index_min__none_supported(self): """ - Test `support_index_min` returns None, when wheel not supported + Test a wheel not supported by the given tags. """ w = wheel.Wheel('simple-0.1-py2-none-any.whl') - assert w.support_index_min(tags=[]) is None + with pytest.raises(ValueError): + w.support_index_min(tags=[]) def test_unpack_wheel_no_flatten(self): from pip._internal.utils import misc as utils From 63639bfc0f6daa5cf18580db71d44e37d2a3b682 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 9 Aug 2019 23:19:19 -0700 Subject: [PATCH 0087/3170] Make tags required in Wheel.support_index_min() and supported(). --- src/pip/_internal/cache.py | 51 +++++++++++++++++++++------- src/pip/_internal/req/req_install.py | 9 +++-- src/pip/_internal/req/req_set.py | 4 ++- src/pip/_internal/wheel.py | 21 +++++++----- 4 files changed, 61 insertions(+), 24 deletions(-) diff --git a/src/pip/_internal/cache.py b/src/pip/_internal/cache.py index 0e893cd45a7..433b27bb9c2 100644 --- a/src/pip/_internal/cache.py +++ b/src/pip/_internal/cache.py @@ -21,6 +21,7 @@ if MYPY_CHECK_RUNNING: from typing import Optional, Set, List, Any from pip._internal.index import FormatControl + from pip._internal.pep425tags import Pep425Tag logger = logging.getLogger(__name__) @@ -103,8 +104,13 @@ def get_path_for_link(self, link): """ raise NotImplementedError() - def get(self, link, package_name): - # type: (Link, Optional[str]) -> Link + def get( + self, + link, # type: Link + package_name, # type: Optional[str] + supported_tags, # type: List[Pep425Tag] + ): + # type: (...) -> Link """Returns a link to a cached item if it exists, otherwise returns the passed link. """ @@ -153,8 +159,13 @@ def get_path_for_link(self, link): # Store wheels within the root cache_dir return os.path.join(self.cache_dir, "wheels", *parts) - def get(self, link, package_name): - # type: (Link, Optional[str]) -> Link + def get( + self, + link, # type: Link + package_name, # type: Optional[str] + supported_tags, # type: List[Pep425Tag] + ): + # type: (...) -> Link candidates = [] for wheel_name in self._get_candidates(link, package_name): @@ -162,10 +173,12 @@ def get(self, link, package_name): wheel = Wheel(wheel_name) except InvalidWheelFilename: continue - if not wheel.supported(): + if not wheel.supported(supported_tags): # Built for a different python/arch/etc continue - candidates.append((wheel.support_index_min(), wheel_name)) + candidates.append( + (wheel.support_index_min(supported_tags), wheel_name) + ) if not candidates: return link @@ -214,12 +227,26 @@ def get_ephem_path_for_link(self, link): # type: (Link) -> str return self._ephem_cache.get_path_for_link(link) - def get(self, link, package_name): - # type: (Link, Optional[str]) -> Link - retval = self._wheel_cache.get(link, package_name) - if retval is link: - retval = self._ephem_cache.get(link, package_name) - return retval + def get( + self, + link, # type: Link + package_name, # type: Optional[str] + supported_tags, # type: List[Pep425Tag] + ): + # type: (...) -> Link + retval = self._wheel_cache.get( + link=link, + package_name=package_name, + supported_tags=supported_tags, + ) + if retval is not link: + return retval + + return self._ephem_cache.get( + link=link, + package_name=package_name, + supported_tags=supported_tags, + ) def cleanup(self): # type: () -> None diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 4752fa88816..d57804da188 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -18,7 +18,7 @@ from pip._vendor.packaging.version import parse as parse_version from pip._vendor.pep517.wrappers import Pep517HookCaller -from pip._internal import wheel +from pip._internal import pep425tags, wheel from pip._internal.build_env import NoOpBuildEnvironment from pip._internal.exceptions import InstallationError from pip._internal.models.link import Link @@ -222,7 +222,12 @@ def populate_link(self, finder, upgrade, require_hashes): 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.get(self.link, self.name) + supported_tags = pep425tags.get_supported() + self.link = self._wheel_cache.get( + link=self.link, + package_name=self.name, + supported_tags=supported_tags, + ) if old_link != self.link: logger.debug('Using cached wheel link: %s', self.link) diff --git a/src/pip/_internal/req/req_set.py b/src/pip/_internal/req/req_set.py index 0b81ed215b0..cd51d17250e 100644 --- a/src/pip/_internal/req/req_set.py +++ b/src/pip/_internal/req/req_set.py @@ -6,6 +6,7 @@ import logging from collections import OrderedDict +from pip._internal import pep425tags from pip._internal.exceptions import InstallationError from pip._internal.utils.logging import indent_log from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -88,7 +89,8 @@ def add_requirement( # single requirements file. if install_req.link and install_req.link.is_wheel: wheel = Wheel(install_req.link.filename) - if self.check_supported_wheels and not wheel.supported(): + tags = pep425tags.get_supported() + if (self.check_supported_wheels and not wheel.supported(tags)): raise InstallationError( "%s is not a supported wheel on this platform." % wheel.filename diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index d864ffb605c..47416735c8b 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -736,8 +736,8 @@ def get_formatted_file_tags(self): """ return sorted(format_tag(tag) for tag in self.file_tags) - def support_index_min(self, tags=None): - # type: (Optional[List[Pep425Tag]]) -> int + def support_index_min(self, tags): + # type: (List[Pep425Tag]) -> int """ Return the lowest index that one of the wheel's file_tag combinations achieves in the given list of supported tags. @@ -745,18 +745,21 @@ def support_index_min(self, tags=None): For example, if there are 8 supported tags and one of the file tags is first in the list, then return 0. + :param tags: the PEP 425 tags to check the wheel against, in order + with most preferred first. + :raises ValueError: If none of the wheel's file tags match one of the supported tags. """ - if tags is None: # for mock - tags = pep425tags.get_supported() return min(tags.index(tag) for tag in self.file_tags if tag in tags) - def supported(self, tags=None): - # type: (Optional[List[Pep425Tag]]) -> bool - """Return whether this wheel is supported by one of the given tags.""" - if tags is None: # for mock - tags = pep425tags.get_supported() + def supported(self, tags): + # type: (List[Pep425Tag]) -> bool + """ + Return whether the wheel is compatible with one of the given tags. + + :param tags: the PEP 425 tags to check the wheel against. + """ return not self.file_tags.isdisjoint(tags) From a0418719b00e09c200dcd42ae251ba727442c661 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Sat, 10 Aug 2019 13:54:35 -0400 Subject: [PATCH 0088/3170] Clean up import in moved module. --- src/pip/_internal/utils/misc.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 33b0653a0a8..576a25138ed 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -7,9 +7,7 @@ import errno import getpass import io -# we have a submodule named 'logging' which would shadow this if we used the -# regular name: -import logging as std_logging +import logging import os import posixpath import re @@ -83,8 +81,8 @@ def cast(type_, value): # type: ignore 'get_installed_version', 'remove_auth_from_url'] -logger = std_logging.getLogger(__name__) -subprocess_logger = std_logging.getLogger('pip.subprocessor') +logger = logging.getLogger(__name__) +subprocess_logger = logging.getLogger('pip.subprocessor') LOG_DIVIDER = '----------------------------------------' @@ -859,12 +857,12 @@ def call_subprocess( if show_stdout: # Then log the subprocess output at INFO level. log_subprocess = subprocess_logger.info - used_level = std_logging.INFO + used_level = logging.INFO else: # Then log the subprocess output using DEBUG. This also ensures # it will be logged to the log file (aka user_log), if enabled. log_subprocess = subprocess_logger.debug - used_level = std_logging.DEBUG + used_level = logging.DEBUG # Whether the subprocess will be visible in the console. showing_subprocess = subprocess_logger.getEffectiveLevel() <= used_level From 0a6b3cedaea4233ded72e96d9c6d7524d9c7d902 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Sat, 10 Aug 2019 22:04:44 -0400 Subject: [PATCH 0089/3170] Remove unnecessary expect_error. --- tests/functional/test_check.py | 2 +- tests/functional/test_completion.py | 2 +- tests/functional/test_download.py | 9 +-- tests/functional/test_install.py | 16 ++--- tests/functional/test_install_check.py | 8 ++- tests/functional/test_install_cleanup.py | 1 - tests/functional/test_install_compat.py | 2 +- tests/functional/test_install_config.py | 6 +- .../test_install_force_reinstall.py | 2 +- tests/functional/test_install_reqs.py | 1 - tests/functional/test_install_upgrade.py | 58 +++++++++---------- tests/functional/test_install_wheel.py | 2 +- tests/functional/test_uninstall.py | 13 ++--- tests/lib/git_submodule_helpers.py | 5 +- 14 files changed, 56 insertions(+), 71 deletions(-) diff --git a/tests/functional/test_check.py b/tests/functional/test_check.py index 45611e73ec1..e06d2e1dd84 100644 --- a/tests/functional/test_check.py +++ b/tests/functional/test_check.py @@ -166,7 +166,7 @@ def test_check_complicated_name_clean(script): ) assert "Successfully installed dependency-b-1.0" in result.stdout - result = script.pip('check', expect_error=True) + result = script.pip('check') expected_lines = ( "No broken requirements found.", ) diff --git a/tests/functional/test_completion.py b/tests/functional/test_completion.py index b4e93da402b..49bee8ec2ca 100644 --- a/tests/functional/test_completion.py +++ b/tests/functional/test_completion.py @@ -65,7 +65,7 @@ def test_completion_alone(script): """ Test getting completion for none shell, just pip completion """ - result = script.pip('completion', expect_error=True) + result = script.pip('completion', allow_stderr_error=True) assert 'ERROR: You must pass --bash or --fish or --zsh' in result.stderr, \ 'completion alone failed -- ' + result.stderr diff --git a/tests/functional/test_download.py b/tests/functional/test_download.py index 1a20870c087..7873e255b61 100644 --- a/tests/functional/test_download.py +++ b/tests/functional/test_download.py @@ -21,7 +21,7 @@ def test_download_if_requested(script): It should download (in the scratch path) and not install if requested. """ result = script.pip( - 'download', '-d', 'pip_downloads', 'INITools==0.1', expect_error=True + 'download', '-d', 'pip_downloads', 'INITools==0.1' ) assert Path('scratch') / 'pip_downloads' / 'INITools-0.1.tar.gz' \ in result.files_created @@ -68,7 +68,6 @@ def test_single_download_from_requirements_file(script): """)) result = script.pip( 'download', '-r', script.scratch_path / 'test-req.txt', '-d', '.', - expect_error=True, ) assert Path('scratch') / 'INITools-0.1.tar.gz' in result.files_created assert script.site_packages / 'initools' not in result.files_created @@ -80,7 +79,7 @@ def test_basic_download_should_download_dependencies(script): It should download dependencies (in the scratch path) """ result = script.pip( - 'download', 'Paste[openid]==1.7.5.1', '-d', '.', expect_error=True, + 'download', 'Paste[openid]==1.7.5.1', '-d', '.' ) assert Path('scratch') / 'Paste-1.7.5.1.tar.gz' in result.files_created openid_tarball_prefix = str(Path('scratch') / 'python-openid-') @@ -129,7 +128,6 @@ def test_download_should_skip_existing_files(script): result = script.pip( 'download', '-r', script.scratch_path / 'test-req.txt', '-d', '.', - expect_error=True, ) assert Path('scratch') / 'INITools-0.1.tar.gz' in result.files_created assert script.site_packages / 'initools' not in result.files_created @@ -143,7 +141,6 @@ def test_download_should_skip_existing_files(script): # only the second package should be downloaded result = script.pip( 'download', '-r', script.scratch_path / 'test-req.txt', '-d', '.', - expect_error=True, ) openid_tarball_prefix = str(Path('scratch') / 'python-openid-') assert any( @@ -387,7 +384,6 @@ def test_explicit_platform_only(self, data, script): '--dest', '.', '--platform', 'linux_x86_64', 'fake', - expect_error=True, ) @@ -571,7 +567,6 @@ def test_download_specify_abi(script, data): '--dest', '.', '--abi', 'cp27m', 'fake', - expect_error=True, ) data.reset() diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 084b08ed0dd..6e1e3dd400b 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -262,7 +262,7 @@ def _test_install_editable_from_git(script, tmpdir): """Test cloning from Git.""" pkg_path = _create_test_package(script, name='testpackage', vcs='git') args = ['install', '-e', 'git+%s#egg=testpackage' % path_to_url(pkg_path)] - result = script.pip(*args, **{"expect_error": True}) + result = script.pip(*args) result.assert_installed('testpackage', with_files=['.git']) @@ -331,7 +331,7 @@ def test_basic_install_editable_from_hg(script, tmpdir): """Test cloning from Mercurial.""" pkg_path = _create_test_package(script, name='testpackage', vcs='hg') args = ['install', '-e', 'hg+%s#egg=testpackage' % path_to_url(pkg_path)] - result = script.pip(*args, **{"expect_error": True}) + result = script.pip(*args) result.assert_installed('testpackage', with_files=['.hg']) @@ -342,7 +342,7 @@ def test_vcs_url_final_slash_normalization(script, tmpdir): """ pkg_path = _create_test_package(script, name='testpackage', vcs='hg') args = ['install', '-e', 'hg+%s/#egg=testpackage' % path_to_url(pkg_path)] - result = script.pip(*args, **{"expect_error": True}) + result = script.pip(*args) result.assert_installed('testpackage', with_files=['.hg']) @@ -351,7 +351,7 @@ def test_install_editable_from_bazaar(script, tmpdir): """Test checking out from Bazaar.""" pkg_path = _create_test_package(script, name='testpackage', vcs='bazaar') args = ['install', '-e', 'bzr+%s/#egg=testpackage' % path_to_url(pkg_path)] - result = script.pip(*args, **{"expect_error": True}) + result = script.pip(*args) result.assert_installed('testpackage', with_files=['.bzr']) @@ -1264,8 +1264,8 @@ def test_install_editable_with_wrong_egg_name(script): version='0.1') """)) result = script.pip( - 'install', '--editable', 'file://%s#egg=pkgb' % pkga_path, - expect_error=True) + 'install', '--editable', 'file://%s#egg=pkgb' % pkga_path + ) assert ("Generating metadata for package pkgb produced metadata " "for project name pkga. Fix your #egg=pkgb " "fragments.") in result.stderr @@ -1368,7 +1368,7 @@ def test_install_compatible_python_requires(script): python_requires='>1.0', version='0.1') """)) - res = script.pip('install', pkga_path, expect_error=True) + res = script.pip('install', pkga_path) assert "Successfully installed pkga-0.1" in res.stdout, res @@ -1424,7 +1424,7 @@ def test_install_from_test_pypi_with_ext_url_dep_is_blocked(script, index): def test_installing_scripts_outside_path_prints_warning(script): result = script.pip_install_local( - "--prefix", script.scratch_path, "script_wheel1", expect_error=True + "--prefix", script.scratch_path, "script_wheel1" ) assert "Successfully installed script-wheel1" in result.stdout, str(result) assert "--no-warn-script-location" in result.stderr diff --git a/tests/functional/test_install_check.py b/tests/functional/test_install_check.py index 46c7dabe5df..017d6256b6c 100644 --- a/tests/functional/test_install_check.py +++ b/tests/functional/test_install_check.py @@ -31,7 +31,11 @@ def test_check_install_canonicalization(script, deprecated_python): # Install the first missing dependency. Only an error for the # second dependency should remain. result = script.pip( - 'install', '--no-index', normal_path, '--quiet', expect_error=True + 'install', + '--no-index', + normal_path, + '--quiet', + allow_stderr_error=True, ) expected_lines = [ "ERROR: pkga 1.0 requires SPECIAL.missing, which is not installed.", @@ -87,7 +91,7 @@ def test_check_install_does_not_warn_for_out_of_graph_issues( # Install conflict package result = script.pip( - 'install', '--no-index', pkg_conflict_path, expect_error=True, + 'install', '--no-index', pkg_conflict_path, allow_stderr_error=True, ) assert matches_expected_lines(result.stderr, [ "ERROR: broken 1.0 requires missing, which is not installed.", diff --git a/tests/functional/test_install_cleanup.py b/tests/functional/test_install_cleanup.py index f6bf903e49a..bc51074eacc 100644 --- a/tests/functional/test_install_cleanup.py +++ b/tests/functional/test_install_cleanup.py @@ -51,7 +51,6 @@ def test_cleanup_after_install_editable_from_hg(script, tmpdir): 'hg+https://bitbucket.org/ianb/scripttest', tmpdir.joinpath("cache"), ), - expect_error=True, ) build = script.venv_path / 'build' src = script.venv_path / 'src' diff --git a/tests/functional/test_install_compat.py b/tests/functional/test_install_compat.py index c48ba3f6fdc..3c77b3e046f 100644 --- a/tests/functional/test_install_compat.py +++ b/tests/functional/test_install_compat.py @@ -21,7 +21,7 @@ def test_debian_egg_name_workaround(script): https://bitbucket.org/ianb/pip/issue/104/pip-uninstall-on-ubuntu-linux """ - result = script.pip('install', 'INITools==0.2', expect_error=True) + result = script.pip('install', 'INITools==0.2') egg_info = os.path.join( script.site_packages, "INITools-0.2-py%s.egg-info" % pyversion) diff --git a/tests/functional/test_install_config.py b/tests/functional/test_install_config.py index 4cb2e3e7ce0..bcf83f163a8 100644 --- a/tests/functional/test_install_config.py +++ b/tests/functional/test_install_config.py @@ -74,7 +74,7 @@ def _test_env_vars_override_config_file(script, virtualenv, config_file): ) script.environ['PIP_NO_INDEX'] = '0' virtualenv.clear() - result = script.pip('install', '-vvv', 'INITools', expect_error=True) + result = script.pip('install', '-vvv', 'INITools') assert "Successfully installed INITools" in result.stdout @@ -89,7 +89,6 @@ def test_command_line_append_flags(script, virtualenv, data): result = script.pip( 'install', '-vvv', 'INITools', '--trusted-host', 'test.pypi.org', - expect_error=True, ) assert ( "Analyzing links from page https://test.pypi.org" @@ -99,7 +98,6 @@ def test_command_line_append_flags(script, virtualenv, data): result = script.pip( 'install', '-vvv', '--find-links', data.find_links, 'INITools', '--trusted-host', 'test.pypi.org', - expect_error=True, ) assert ( "Analyzing links from page https://test.pypi.org" @@ -123,7 +121,6 @@ def test_command_line_appends_correctly(script, data): result = script.pip( 'install', '-vvv', 'INITools', '--trusted-host', 'test.pypi.org', - expect_error=True, ) assert ( @@ -175,7 +172,6 @@ def _test_config_file_override_stack(script, virtualenv, config_file): result = script.pip( 'install', '-vvv', '--index-url', 'https://pypi.org/simple/', 'INITools', - expect_error=True, ) assert ( "Getting page http://download.zope.org/ppix/INITools" diff --git a/tests/functional/test_install_force_reinstall.py b/tests/functional/test_install_force_reinstall.py index 33aabeb7602..56c7aee396f 100644 --- a/tests/functional/test_install_force_reinstall.py +++ b/tests/functional/test_install_force_reinstall.py @@ -26,7 +26,7 @@ def check_force_reinstall(script, specifier, expected): assert result2.files_updated, 'force-reinstall failed' check_installed_version(script, 'simplewheel', expected) - result3 = script.pip('uninstall', 'simplewheel', '-y', expect_error=True) + result3 = script.pip('uninstall', 'simplewheel', '-y') assert_all_changes(result, result3, [script.venv / 'build', 'cache']) diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index d2b23df036a..63f163ca97a 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -534,7 +534,6 @@ def test_install_options_local_to_package(script, data): 'install', '--no-index', '-f', data.find_links, '-r', reqs_file, - expect_error=True, ) simple = test_simple / 'lib' / 'python' / 'simple' diff --git a/tests/functional/test_install_upgrade.py b/tests/functional/test_install_upgrade.py index 3264e9c34cd..91f81e168df 100644 --- a/tests/functional/test_install_upgrade.py +++ b/tests/functional/test_install_upgrade.py @@ -13,8 +13,8 @@ def test_no_upgrade_unless_requested(script): No upgrade if not specifically requested. """ - script.pip('install', 'INITools==0.1', expect_error=True) - result = script.pip('install', 'INITools', expect_error=True) + script.pip('install', 'INITools==0.1') + result = script.pip('install', 'INITools') assert not result.files_created, ( 'pip install INITools upgraded when it should not have' ) @@ -39,10 +39,9 @@ def test_only_if_needed_does_not_upgrade_deps_when_satisfied(script): It doesn't upgrade a dependency if it already satisfies the requirements. """ - script.pip_install_local('simple==2.0', expect_error=True) + script.pip_install_local('simple==2.0') result = script.pip_install_local( - '--upgrade', '--upgrade-strategy=only-if-needed', 'require_simple', - expect_error=True + '--upgrade', '--upgrade-strategy=only-if-needed', 'require_simple' ) assert ( @@ -64,10 +63,9 @@ def test_only_if_needed_does_upgrade_deps_when_no_longer_satisfied(script): It does upgrade a dependency if it no longer satisfies the requirements. """ - script.pip_install_local('simple==1.0', expect_error=True) + script.pip_install_local('simple==1.0') result = script.pip_install_local( - '--upgrade', '--upgrade-strategy=only-if-needed', 'require_simple', - expect_error=True + '--upgrade', '--upgrade-strategy=only-if-needed', 'require_simple' ) assert ( @@ -89,10 +87,9 @@ def test_eager_does_upgrade_dependecies_when_currently_satisfied(script): It does upgrade a dependency even if it already satisfies the requirements. """ - script.pip_install_local('simple==2.0', expect_error=True) + script.pip_install_local('simple==2.0') result = script.pip_install_local( - '--upgrade', '--upgrade-strategy=eager', 'require_simple', - expect_error=True + '--upgrade', '--upgrade-strategy=eager', 'require_simple' ) assert ( @@ -110,10 +107,9 @@ def test_eager_does_upgrade_dependecies_when_no_longer_satisfied(script): It does upgrade a dependency if it no longer satisfies the requirements. """ - script.pip_install_local('simple==1.0', expect_error=True) + script.pip_install_local('simple==1.0') result = script.pip_install_local( - '--upgrade', '--upgrade-strategy=eager', 'require_simple', - expect_error=True + '--upgrade', '--upgrade-strategy=eager', 'require_simple' ) assert ( @@ -136,8 +132,8 @@ def test_upgrade_to_specific_version(script): It does upgrade to specific version requested. """ - script.pip('install', 'INITools==0.1', expect_error=True) - result = script.pip('install', 'INITools==0.2', expect_error=True) + script.pip('install', 'INITools==0.1') + result = script.pip('install', 'INITools==0.2') assert result.files_created, ( 'pip install with specific version did not upgrade' ) @@ -157,8 +153,8 @@ def test_upgrade_if_requested(script): And it does upgrade if requested. """ - script.pip('install', 'INITools==0.1', expect_error=True) - result = script.pip('install', '--upgrade', 'INITools', expect_error=True) + script.pip('install', 'INITools==0.1') + result = script.pip('install', '--upgrade', 'INITools') assert result.files_created, 'pip install --upgrade did not upgrade' assert ( script.site_packages / 'INITools-0.1-py%s.egg-info' % @@ -193,7 +189,7 @@ def test_upgrade_force_reinstall_newest(script): 'install', '--upgrade', '--force-reinstall', 'INITools' ) assert result2.files_updated, 'upgrade to INITools 0.3 failed' - result3 = script.pip('uninstall', 'initools', '-y', expect_error=True) + result3 = script.pip('uninstall', 'initools', '-y') assert_all_changes(result, result3, [script.venv / 'build', 'cache']) @@ -203,13 +199,13 @@ def test_uninstall_before_upgrade(script): Automatic uninstall-before-upgrade. """ - result = script.pip('install', 'INITools==0.2', expect_error=True) + result = script.pip('install', 'INITools==0.2') assert script.site_packages / 'initools' in result.files_created, ( sorted(result.files_created.keys()) ) - result2 = script.pip('install', 'INITools==0.3', expect_error=True) + result2 = script.pip('install', 'INITools==0.3') assert result2.files_created, 'upgrade to INITools 0.3 failed' - result3 = script.pip('uninstall', 'initools', '-y', expect_error=True) + result3 = script.pip('uninstall', 'initools', '-y') assert_all_changes(result, result3, [script.venv / 'build', 'cache']) @@ -219,7 +215,7 @@ def test_uninstall_before_upgrade_from_url(script): Automatic uninstall-before-upgrade from URL. """ - result = script.pip('install', 'INITools==0.2', expect_error=True) + result = script.pip('install', 'INITools==0.2') assert script.site_packages / 'initools' in result.files_created, ( sorted(result.files_created.keys()) ) @@ -227,10 +223,9 @@ def test_uninstall_before_upgrade_from_url(script): 'install', 'https://files.pythonhosted.org/packages/source/I/INITools/INITools-' '0.3.tar.gz', - expect_error=True, ) assert result2.files_created, 'upgrade to INITools 0.3 failed' - result3 = script.pip('uninstall', 'initools', '-y', expect_error=True) + result3 = script.pip('uninstall', 'initools', '-y') assert_all_changes(result, result3, [script.venv / 'build', 'cache']) @@ -241,7 +236,7 @@ def test_upgrade_to_same_version_from_url(script): need to uninstall and reinstall if --upgrade is not specified. """ - result = script.pip('install', 'INITools==0.3', expect_error=True) + result = script.pip('install', 'INITools==0.3') assert script.site_packages / 'initools' in result.files_created, ( sorted(result.files_created.keys()) ) @@ -249,10 +244,9 @@ def test_upgrade_to_same_version_from_url(script): 'install', 'https://files.pythonhosted.org/packages/source/I/INITools/INITools-' '0.3.tar.gz', - expect_error=True, ) assert not result2.files_updated, 'INITools 0.3 reinstalled same version' - result3 = script.pip('uninstall', 'initools', '-y', expect_error=True) + result3 = script.pip('uninstall', 'initools', '-y') assert_all_changes(result, result3, [script.venv / 'build', 'cache']) @@ -321,9 +315,9 @@ def test_should_not_install_always_from_cache(script): If there is an old cached package, pip should download the newer version Related to issue #175 """ - script.pip('install', 'INITools==0.2', expect_error=True) + script.pip('install', 'INITools==0.2') script.pip('uninstall', '-y', 'INITools') - result = script.pip('install', 'INITools==0.1', expect_error=True) + result = script.pip('install', 'INITools==0.1') assert ( script.site_packages / 'INITools-0.2-py%s.egg-info' % pyversion not in result.files_created @@ -339,8 +333,8 @@ def test_install_with_ignoreinstalled_requested(script): """ Test old conflicting package is completely ignored """ - script.pip('install', 'INITools==0.1', expect_error=True) - result = script.pip('install', '-I', 'INITools==0.3', expect_error=True) + script.pip('install', 'INITools==0.1') + result = script.pip('install', '-I', 'INITools==0.3') assert result.files_created, 'pip install -I did not install' # both the old and new metadata should be present. assert os.path.exists( diff --git a/tests/functional/test_install_wheel.py b/tests/functional/test_install_wheel.py index b4871a2e027..8d530a50b65 100644 --- a/tests/functional/test_install_wheel.py +++ b/tests/functional/test_install_wheel.py @@ -412,7 +412,7 @@ def test_wheel_no_compiles_pyc(script, data): def test_install_from_wheel_uninstalls_old_version(script, data): # regression test for https://github.com/pypa/pip/issues/1825 package = data.packages.joinpath("simplewheel-1.0-py2.py3-none-any.whl") - result = script.pip('install', package, '--no-index', expect_error=True) + result = script.pip('install', package, '--no-index') package = data.packages.joinpath("simplewheel-2.0-py2.py3-none-any.whl") result = script.pip('install', package, '--no-index', expect_error=False) dist_info_folder = script.site_packages / 'simplewheel-2.0.dist-info' diff --git a/tests/functional/test_uninstall.py b/tests/functional/test_uninstall.py index 87c58751620..128d66102c0 100644 --- a/tests/functional/test_uninstall.py +++ b/tests/functional/test_uninstall.py @@ -142,11 +142,11 @@ def test_basic_uninstall_namespace_package(script): the namespace and everything in it. """ - result = script.pip('install', 'pd.requires==0.0.3', expect_error=True) + result = script.pip('install', 'pd.requires==0.0.3') assert join(script.site_packages, 'pd') in result.files_created, ( sorted(result.files_created.keys()) ) - result2 = script.pip('uninstall', 'pd.find', '-y', expect_error=True) + result2 = script.pip('uninstall', 'pd.find', '-y') assert join(script.site_packages, 'pd') not in result2.files_deleted, ( sorted(result2.files_deleted.keys()) ) @@ -259,11 +259,11 @@ def test_uninstall_console_scripts(script): """ args = ['install'] args.append('discover') - result = script.pip(*args, **{"expect_error": True}) + result = script.pip(*args) assert script.bin / 'discover' + script.exe in result.files_created, ( sorted(result.files_created.keys()) ) - result2 = script.pip('uninstall', 'discover', '-y', expect_error=True) + result2 = script.pip('uninstall', 'discover', '-y') assert_all_changes(result, result2, [script.venv / 'build', 'cache']) @@ -272,7 +272,7 @@ def test_uninstall_easy_installed_console_scripts(script): """ Test uninstalling package with console_scripts that is easy_installed. """ - result = script.easy_install('discover', expect_error=True) + result = script.easy_install('discover') assert script.bin / 'discover' + script.exe in result.files_created, ( sorted(result.files_created.keys()) ) @@ -349,8 +349,7 @@ def _test_uninstall_editable_with_source_outside_venv( assert join( script.site_packages, 'pip-test-package.egg-link' ) in result2.files_created, list(result2.files_created.keys()) - result3 = script.pip('uninstall', '-y', - 'pip-test-package', expect_error=True) + result3 = script.pip('uninstall', '-y', 'pip-test-package') assert_all_changes( result, result3, diff --git a/tests/lib/git_submodule_helpers.py b/tests/lib/git_submodule_helpers.py index 501b51f1e43..c529620f76f 100644 --- a/tests/lib/git_submodule_helpers.py +++ b/tests/lib/git_submodule_helpers.py @@ -53,8 +53,8 @@ def _create_test_package_with_submodule(env, rel_path): packages=find_packages(), ) ''')) - env.run('git', 'init', cwd=version_pkg_path, expect_error=True) - env.run('git', 'add', '.', cwd=version_pkg_path, expect_error=True) + env.run('git', 'init', cwd=version_pkg_path) + env.run('git', 'add', '.', cwd=version_pkg_path) _git_commit(env, version_pkg_path, message='initial version') submodule_path = _create_test_package_submodule(env) @@ -66,7 +66,6 @@ def _create_test_package_with_submodule(env, rel_path): submodule_path, rel_path, cwd=version_pkg_path, - expect_error=True, ) _git_commit(env, version_pkg_path, message='initial version w submodule') From 698b27b87570bf90c257cc1409d026bab6de5e90 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 11 Aug 2019 04:13:44 -0700 Subject: [PATCH 0090/3170] Start type checking commands/install.py and commands/wheel.py. --- src/pip/_internal/commands/install.py | 29 ++++++++++++++++++++++----- src/pip/_internal/commands/wheel.py | 7 +++++++ 2 files changed, 31 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 4d7d50759ce..c171fcb8391 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -1,3 +1,10 @@ + +# The following comment should be removed at some point in the future. +# It's included for now because without it InstallCommand.run() has a +# couple errors where we have to know req.name is str rather than +# Optional[str] for the InstallRequirement req. +# mypy: strict-optional=False + from __future__ import absolute_import import errno @@ -13,7 +20,7 @@ from pip._internal.cli import cmdoptions from pip._internal.cli.cmdoptions import make_target_python from pip._internal.cli.req_command import RequirementCommand -from pip._internal.cli.status_codes import ERROR +from pip._internal.cli.status_codes import ERROR, SUCCESS from pip._internal.exceptions import ( CommandError, InstallationError, @@ -30,9 +37,17 @@ protect_pip_from_modification_on_windows, ) from pip._internal.utils.temp_dir import TempDirectory +from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.virtualenv import virtualenv_no_global from pip._internal.wheel import WheelBuilder +if MYPY_CHECK_RUNNING: + from optparse import Values + from typing import Any, List + + from pip._internal.req.req_install import InstallRequirement + + logger = logging.getLogger(__name__) @@ -242,6 +257,7 @@ def __init__(self, *args, **kw): self.parser.insert_option_group(0, cmd_opts) def run(self, options, args): + # type: (Values, List[Any]) -> int cmdoptions.check_install_build_global(options) upgrade_strategy = "to-satisfy-only" if options.upgrade: @@ -425,9 +441,11 @@ def run(self, options, args): except Exception: pass items.append(item) - installed = ' '.join(items) - if installed: - logger.info('Successfully installed %s', installed) + installed_desc = ' '.join(items) + if installed_desc: + logger.info( + 'Successfully installed %s', installed_desc, + ) except EnvironmentError as error: show_traceback = (self.verbosity >= 1) @@ -450,7 +468,8 @@ def run(self, options, args): self._handle_target_dir( options.target_dir, target_temp_dir, options.upgrade ) - return requirement_set + + return SUCCESS def _handle_target_dir(self, target_dir, target_temp_dir, upgrade): ensure_dir(target_dir) diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index cf49b600586..dca0157b1cd 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -11,8 +11,14 @@ from pip._internal.req import RequirementSet from pip._internal.req.req_tracker import RequirementTracker from pip._internal.utils.temp_dir import TempDirectory +from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.wheel import WheelBuilder +if MYPY_CHECK_RUNNING: + from optparse import Values + from typing import Any, List + + logger = logging.getLogger(__name__) @@ -101,6 +107,7 @@ def __init__(self, *args, **kw): self.parser.insert_option_group(0, cmd_opts) def run(self, options, args): + # type: (Values, List[Any]) -> None cmdoptions.check_install_build_global(options) if options.build_dir: From 022a36662f5223fc9a7a5c4af4f7f3ecb7f02620 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sun, 11 Aug 2019 04:15:49 -0700 Subject: [PATCH 0091/3170] Remove WheelBuilder's dependence on PipSession. --- src/pip/_internal/commands/install.py | 12 ++++++++---- src/pip/_internal/commands/wheel.py | 2 +- src/pip/_internal/wheel.py | 9 ++------- tests/unit/test_command_install.py | 11 +++-------- tests/unit/test_wheel.py | 2 +- 5 files changed, 15 insertions(+), 21 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index c171fcb8391..46e667433ea 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -63,7 +63,12 @@ def is_wheel_installed(): return True -def build_wheels(builder, pep517_requirements, legacy_requirements, session): +def build_wheels( + builder, # type: WheelBuilder + pep517_requirements, # type: List[InstallRequirement] + legacy_requirements, # type: List[InstallRequirement] +): + # type: (...) -> List[InstallRequirement] """ Build wheels for requirements, depending on whether wheel is installed. """ @@ -73,7 +78,7 @@ def build_wheels(builder, pep517_requirements, legacy_requirements, session): # Always build PEP 517 requirements build_failures = builder.build( pep517_requirements, - session=session, autobuilding=True + autobuilding=True, ) if should_build_legacy: @@ -82,7 +87,7 @@ def build_wheels(builder, pep517_requirements, legacy_requirements, session): # install for those. builder.build( legacy_requirements, - session=session, autobuilding=True + autobuilding=True, ) return build_failures @@ -378,7 +383,6 @@ def run(self, options, args): builder=wheel_builder, pep517_requirements=pep517_requirements, legacy_requirements=legacy_requirements, - session=session, ) # If we're using PEP 517, we cannot do a direct install diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index dca0157b1cd..7b0c799b21f 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -160,7 +160,7 @@ def run(self, options, args): no_clean=options.no_clean, ) build_failures = wb.build( - requirement_set.requirements.values(), session=session, + requirement_set.requirements.values(), ) if len(build_failures) != 0: raise CommandError( diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index 47416735c8b..71109e2cd33 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -27,7 +27,7 @@ from pip._vendor.six import StringIO from pip._internal import pep425tags -from pip._internal.download import unpack_url +from pip._internal.download import unpack_file_url from pip._internal.exceptions import ( InstallationError, InvalidWheelFilename, @@ -57,7 +57,6 @@ ) from pip._vendor.packaging.requirements import Requirement from pip._internal.req.req_install import InstallRequirement - from pip._internal.download import PipSession from pip._internal.index import FormatControl, PackageFinder from pip._internal.operations.prepare import ( RequirementPreparer @@ -1032,7 +1031,6 @@ def _clean_one(self, req): def build( self, requirements, # type: Iterable[InstallRequirement] - session, # type: PipSession autobuilding=False # type: bool ): # type: (...) -> List[InstallRequirement] @@ -1122,10 +1120,7 @@ def build( req.link = 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=session, - ) + unpack_file_url(link=req.link, location=req.source_dir) else: build_failure.append(req) diff --git a/tests/unit/test_command_install.py b/tests/unit/test_command_install.py index c04a61118b1..30469171f6b 100644 --- a/tests/unit/test_command_install.py +++ b/tests/unit/test_command_install.py @@ -9,7 +9,6 @@ def check_build_wheels( self, pep517_requirements, legacy_requirements, - session, ): """ Return: (mock_calls, return_value). @@ -25,8 +24,6 @@ def build(reqs, **kwargs): builder=builder, pep517_requirements=pep517_requirements, legacy_requirements=legacy_requirements, - # A session value isn't needed. - session='', ) return (builder.build.mock_calls, build_failures) @@ -38,13 +35,12 @@ def test_build_wheels__wheel_installed(self, is_wheel_installed): mock_calls, build_failures = self.check_build_wheels( pep517_requirements=['a', 'b'], legacy_requirements=['c', 'd'], - session='', ) # Legacy requirements were built. assert mock_calls == [ - call(['a', 'b'], autobuilding=True, session=''), - call(['c', 'd'], autobuilding=True, session=''), + call(['a', 'b'], autobuilding=True), + call(['c', 'd'], autobuilding=True), ] # Legacy build failures are not included in the return value. @@ -57,12 +53,11 @@ def test_build_wheels__wheel_not_installed(self, is_wheel_installed): mock_calls, build_failures = self.check_build_wheels( pep517_requirements=['a', 'b'], legacy_requirements=['c', 'd'], - session='', ) # Legacy requirements were not built. assert mock_calls == [ - call(['a', 'b'], autobuilding=True, session=''), + call(['a', 'b'], autobuilding=True), ] assert build_failures == ['a'] diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index bc8894ed165..ea4fe4ebaf9 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -700,7 +700,7 @@ def test_skip_building_wheels(self, caplog): finder=Mock(), preparer=Mock(), wheel_cache=None, ) with caplog.at_level(logging.INFO): - wb.build([wheel_req], session=Mock()) + wb.build([wheel_req]) assert "due to already being wheel" in caplog.text assert mock_build_one.mock_calls == [] From 5b93c0919912efc548caa3e71df5e8bdf9706d2d Mon Sep 17 00:00:00 2001 From: Vinicyus Macedo <7549205+vinicyusmacedo@users.noreply.github.com> Date: Fri, 25 Jan 2019 10:24:14 -0200 Subject: [PATCH 0092/3170] Added test to fail pep508 --- news/6202.bugfix | 2 + src/pip/_internal/req/constructors.py | 79 ++++++++++++++++++++------- tests/unit/test_req.py | 27 +++++++++ tests/unit/test_req_file.py | 7 +++ 4 files changed, 94 insertions(+), 21 deletions(-) create mode 100644 news/6202.bugfix diff --git a/news/6202.bugfix b/news/6202.bugfix new file mode 100644 index 00000000000..03184fa8d93 --- /dev/null +++ b/news/6202.bugfix @@ -0,0 +1,2 @@ +Fix requirement line parser to correctly handle PEP 440 requirements with a URL +pointing to an archive file. diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index 28cef933221..c4e59209f5e 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -20,7 +20,9 @@ from pip._vendor.packaging.specifiers import Specifier from pip._vendor.pkg_resources import RequirementParseError, parse_requirements -from pip._internal.download import is_archive_file, is_url, url_to_path +from pip._internal.download import ( + is_archive_file, is_url, path_to_url, url_to_path, +) from pip._internal.exceptions import InstallationError from pip._internal.models.index import PyPI, TestPyPI from pip._internal.models.link import Link @@ -201,6 +203,58 @@ def install_req_from_editable( ) +def _get_path_or_url(path, name): + # type: (str, str) -> str + """ + First, it checks whether a provided path is an installable directory + (e.g. it has a setup.py). If it is, returns the path. + + If false, check if the path is an archive file (such as a .whl). + The function checks if the path is a file. If false, if the path has + an @, it will treat it as a PEP 440 URL requirement and return the path. + """ + if os.path.isdir(path) and _looks_like_path(name): + if not is_installable_dir(path): + raise InstallationError( + "Directory %r is not installable. Neither 'setup.py' " + "nor 'pyproject.toml' found." % name + ) + return path_to_url(path) + elif is_archive_file(path): + if os.path.isfile(path): + return path_to_url(path) + else: + urlreq_parts = name.split('@', 1) + if len(urlreq_parts) < 2 or _looks_like_path(urlreq_parts[0]): + logger.warning( + 'Requirement %r looks like a filename, but the ' + 'file does not exist', + name + ) + return path_to_url(path) + # If the path contains '@' and the part before it does not look + # like a path, try to treat it as a PEP 440 URL req instead. + + +def _looks_like_path(name): + # type: (str) -> bool + """Checks whether the string "looks like" a path on the filesystem. + + This does not check whether the target actually exists, only judge from the + apperance. Returns true if any of the following is true: + + * A path separator is found (either os.path.sep or os.path.altsep). + * The string starts with "." (current directory). + """ + if os.path.sep in name: + return True + if os.path.altsep is not None and os.path.altsep in name: + return True + if name.startswith('.'): + return True + return False + + def install_req_from_line( name, # type: str comes_from=None, # type: Optional[Union[str, InstallRequirement]] @@ -241,26 +295,9 @@ def install_req_from_line( link = Link(name) else: p, extras_as_string = _strip_extras(path) - 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. Neither 'setup.py' " - "nor 'pyproject.toml' found." % name - ) - link = Link(path_to_url(p)) - elif is_archive_file(p): - if not os.path.isfile(p): - logger.warning( - 'Requirement %r looks like a filename, but the ' - 'file does not exist', - name - ) - link = Link(path_to_url(p)) + path_or_url = _get_path_or_url(p, name) + if path_or_url: + link = Link(path_or_url) # it's a local file, dir, or url if link: diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index ebd3e8f03bf..a85f5f70cfe 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -335,6 +335,33 @@ def test_url_with_query(self): req = install_req_from_line(url + fragment) assert req.link.url == url + fragment, req.link + def test_pep440_wheel_link_requirement(self): + url = 'https://whatever.com/test-0.4-py2.py3-bogus-any.whl' + line = 'test @ https://whatever.com/test-0.4-py2.py3-bogus-any.whl' + req = install_req_from_line(line) + parts = str(req.req).split('@', 1) + assert len(parts) == 2 + assert parts[0].strip() == 'test' + assert parts[1].strip() == url + + def test_pep440_url_link_requirement(self): + url = 'git+http://foo.com@ref#egg=foo' + line = 'foo @ git+http://foo.com@ref#egg=foo' + req = install_req_from_line(line) + parts = str(req.req).split('@', 1) + assert len(parts) == 2 + assert parts[0].strip() == 'foo' + assert parts[1].strip() == url + + def test_url_with_authentication_link_requirement(self): + url = 'https://what@whatever.com/test-0.4-py2.py3-bogus-any.whl' + line = 'https://what@whatever.com/test-0.4-py2.py3-bogus-any.whl' + req = install_req_from_line(line) + assert req.link is not None + assert req.link.is_wheel + assert req.link.scheme == "https" + assert req.link.url == url + def test_unsupported_wheel_link_requirement_raises(self): reqset = RequirementSet() req = install_req_from_line( diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py index 3ebb55d3cfc..3bc208f34bb 100644 --- a/tests/unit/test_req_file.py +++ b/tests/unit/test_req_file.py @@ -225,6 +225,13 @@ def test_yield_line_requirement(self): req = install_req_from_line(line, comes_from=comes_from) assert repr(list(process_line(line, filename, 1))[0]) == repr(req) + def test_yield_pep440_line_requirement(self): + line = 'SomeProject @ https://url/SomeProject-py2-py3-none-any.whl' + filename = 'filename' + comes_from = '-r %s (line %s)' % (filename, 1) + req = install_req_from_line(line, comes_from=comes_from) + assert repr(list(process_line(line, filename, 1))[0]) == repr(req) + def test_yield_line_constraint(self): line = 'SomeProject' filename = 'filename' From 16af35c61345866d582b43abfc649a0d79b16c9c Mon Sep 17 00:00:00 2001 From: Vinicyus Macedo <7549205+vinicyusmacedo@users.noreply.github.com> Date: Sat, 23 Feb 2019 15:10:34 -0300 Subject: [PATCH 0093/3170] Adding improvements to the _get_path_to_url function --- docs/html/reference/pip_install.rst | 18 ++++- src/pip/_internal/req/constructors.py | 86 ++++++++++++------------ tests/unit/test_req.py | 94 +++++++++++++++++++++++++++ 3 files changed, 153 insertions(+), 45 deletions(-) diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index f56dc9a0e9f..fa475c462c8 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -244,8 +244,7 @@ pip supports installing from a package index using a :term:`requirement specifier `. Generally speaking, a requirement specifier is composed of a project name followed by optional :term:`version specifiers `. :pep:`508` contains a full specification -of the format of a requirement (pip does not support the ``url_req`` form -of specifier at this time). +of the format of a requirement. Some examples: @@ -265,6 +264,13 @@ Since version 6.0, pip also supports specifiers containing `environment markers SomeProject ==5.4 ; python_version < '2.7' SomeProject; sys_platform == 'win32' +Since version 19.1, pip also supports `direct references +`__ like so: + + :: + + SomeProject @ file:///somewhere/... + Environment markers are supported in the command line and in requirements files. .. note:: @@ -880,6 +886,14 @@ Examples $ pip install http://my.package.repo/SomePackage-1.0.4.zip +#. Install a particular source archive file following :pep:`440` direct references. + + :: + + $ pip install SomeProject==1.0.4@http://my.package.repo//SomeProject-1.2.3-py33-none-any.whl + $ pip install "SomeProject==1.0.4 @ http://my.package.repo//SomeProject-1.2.3-py33-none-any.whl" + + #. Install from alternative package repositories. Install from a different index, and not `PyPI`_ :: diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index c4e59209f5e..cacf84bfc85 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -20,9 +20,7 @@ from pip._vendor.packaging.specifiers import Specifier from pip._vendor.pkg_resources import RequirementParseError, parse_requirements -from pip._internal.download import ( - is_archive_file, is_url, path_to_url, url_to_path, -) +from pip._internal.download import is_archive_file, is_url, url_to_path from pip._internal.exceptions import InstallationError from pip._internal.models.index import PyPI, TestPyPI from pip._internal.models.link import Link @@ -203,58 +201,60 @@ def install_req_from_editable( ) -def _get_path_or_url(path, name): - # type: (str, str) -> str - """ - First, it checks whether a provided path is an installable directory - (e.g. it has a setup.py). If it is, returns the path. - - If false, check if the path is an archive file (such as a .whl). - The function checks if the path is a file. If false, if the path has - an @, it will treat it as a PEP 440 URL requirement and return the path. - """ - if os.path.isdir(path) and _looks_like_path(name): - if not is_installable_dir(path): - raise InstallationError( - "Directory %r is not installable. Neither 'setup.py' " - "nor 'pyproject.toml' found." % name - ) - return path_to_url(path) - elif is_archive_file(path): - if os.path.isfile(path): - return path_to_url(path) - else: - urlreq_parts = name.split('@', 1) - if len(urlreq_parts) < 2 or _looks_like_path(urlreq_parts[0]): - logger.warning( - 'Requirement %r looks like a filename, but the ' - 'file does not exist', - name - ) - return path_to_url(path) - # If the path contains '@' and the part before it does not look - # like a path, try to treat it as a PEP 440 URL req instead. - - def _looks_like_path(name): # type: (str) -> bool """Checks whether the string "looks like" a path on the filesystem. This does not check whether the target actually exists, only judge from the - apperance. Returns true if any of the following is true: + appearance. - * A path separator is found (either os.path.sep or os.path.altsep). - * The string starts with "." (current directory). + Returns true if any of the following conditions is true: + * a path separator is found (either os.path.sep or os.path.altsep); + * a dot is found (which represents the current directory). """ if os.path.sep in name: return True if os.path.altsep is not None and os.path.altsep in name: return True - if name.startswith('.'): + if name.startswith("."): return True return False +def _get_url_from_path(path, name): + # type: (str, str) -> str + """ + First, it checks whether a provided path is an installable directory + (e.g. it has a setup.py). If it is, returns the path. + + If false, check if the path is an archive file (such as a .whl). + The function checks if the path is a file. If false, if the path has + an @, it will treat it as a PEP 440 URL requirement and return the path. + """ + if _looks_like_path(name) and os.path.isdir(path): + if is_installable_dir(path): + return path_to_url(path) + raise InstallationError( + "Directory %r is not installable. Neither 'setup.py' " + "nor 'pyproject.toml' found." % name + ) + if not is_archive_file(path): + return None + if os.path.isfile(path): + return path_to_url(path) + urlreq_parts = name.split('@', 1) + if len(urlreq_parts) >= 2 and not _looks_like_path(urlreq_parts[0]): + # If the path contains '@' and the part before it does not look + # like a path, try to treat it as a PEP 440 URL req instead. + return None + logger.warning( + 'Requirement %r looks like a filename, but the ' + 'file does not exist', + name + ) + return path_to_url(path) + + def install_req_from_line( name, # type: str comes_from=None, # type: Optional[Union[str, InstallRequirement]] @@ -295,9 +295,9 @@ def install_req_from_line( link = Link(name) else: p, extras_as_string = _strip_extras(path) - path_or_url = _get_path_or_url(p, name) - if path_or_url: - link = Link(path_or_url) + url = _get_url_from_path(p, name) + if url is not None: + link = Link(url) # it's a local file, dir, or url if link: diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index a85f5f70cfe..8151586e5e2 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -21,6 +21,8 @@ from pip._internal.operations.prepare import RequirementPreparer from pip._internal.req import InstallRequirement, RequirementSet from pip._internal.req.constructors import ( + _get_url_from_path, + _looks_like_path, install_req_from_editable, install_req_from_line, parse_editable, @@ -653,3 +655,95 @@ def test_mismatched_versions(caplog, tmpdir): 'Requested simplewheel==2.0, ' 'but installing version 1.0' ) + + +@pytest.mark.parametrize('args, expected', [ + # Test UNIX-like paths + (('/path/to/installable'), True), + # Test relative paths + (('./path/to/installable'), True), + # Test current path + (('.'), True), + # Test url paths + (('https://whatever.com/test-0.4-py2.py3-bogus-any.whl'), True), + # Test pep440 paths + (('test @ https://whatever.com/test-0.4-py2.py3-bogus-any.whl'), True), + # Test wheel + (('simple-0.1-py2.py3-none-any.whl'), False), +]) +def test_looks_like_path(args, expected): + assert _looks_like_path(args) == expected + + +@pytest.mark.skipif( + not sys.platform.startswith("win"), + reason='Test only available on Windows' +) +@pytest.mark.parametrize('args, expected', [ + # Test relative paths + (('.\\path\\to\\installable'), True), + (('relative\\path'), True), + # Test absolute paths + (('C:\\absolute\\path'), True), +]) +def test_looks_like_path_win(args, expected): + assert _looks_like_path(args) == expected + + +@pytest.mark.parametrize('args, mock_returns, expected', [ + # Test pep440 urls + (('/path/to/foo @ git+http://foo.com@ref#egg=foo', + 'foo @ git+http://foo.com@ref#egg=foo'), (False, False), None), + # Test pep440 urls without spaces + (('/path/to/foo@git+http://foo.com@ref#egg=foo', + 'foo @ git+http://foo.com@ref#egg=foo'), (False, False), None), + # Test pep440 wheel + (('/path/to/test @ https://whatever.com/test-0.4-py2.py3-bogus-any.whl', + 'test @ https://whatever.com/test-0.4-py2.py3-bogus-any.whl'), + (False, False), None), + # Test name is not a file + (('/path/to/simple==0.1', + 'simple==0.1'), + (False, False), None), +]) +@patch('pip._internal.req.req_install.os.path.isdir') +@patch('pip._internal.req.req_install.os.path.isfile') +def test_get_url_from_path( + isdir_mock, isfile_mock, args, mock_returns, expected +): + isdir_mock.return_value = mock_returns[0] + isfile_mock.return_value = mock_returns[1] + assert _get_url_from_path(*args) is expected + + +@patch('pip._internal.req.req_install.os.path.isdir') +@patch('pip._internal.req.req_install.os.path.isfile') +def test_get_url_from_path__archive_file(isdir_mock, isfile_mock): + isdir_mock.return_value = False + isfile_mock.return_value = True + name = 'simple-0.1-py2.py3-none-any.whl' + path = os.path.join('/path/to/' + name) + url = path_to_url(path) + assert _get_url_from_path(path, name) == url + + +@patch('pip._internal.req.req_install.os.path.isdir') +@patch('pip._internal.req.req_install.os.path.isfile') +def test_get_url_from_path__installable_dir(isdir_mock, isfile_mock): + isdir_mock.return_value = True + isfile_mock.return_value = True + name = 'some/setuptools/project' + path = os.path.join('/path/to/' + name) + url = path_to_url(path) + assert _get_url_from_path(path, name) == url + + +@patch('pip._internal.req.req_install.os.path.isdir') +def test_get_url_from_path__installable_error(isdir_mock): + isdir_mock.return_value = True + name = 'some/setuptools/project' + path = os.path.join('/path/to/' + name) + with pytest.raises(InstallationError) as e: + _get_url_from_path(path, name) + err_msg = e.value.args[0] + assert "Neither 'setup.py' nor 'pyproject.toml' found" in err_msg From 73e33ae803e5e3482b8da6528ee00c7f085d01c7 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Sun, 11 Aug 2019 00:02:44 -0400 Subject: [PATCH 0094/3170] Assert that expect_error script invocations return non-zero. --- tests/lib/__init__.py | 57 +++++++++++++++++++++++++++---------------- tests/lib/test_lib.py | 14 +++++++++++ 2 files changed, 50 insertions(+), 21 deletions(-) diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index 61fcc954710..6c0a5f7d341 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -329,30 +329,18 @@ def make_check_stderr_message(stderr, line, reason): """).format(stderr=stderr, line=line, reason=reason) -def check_stderr( - stderr, allow_stderr_warning=None, allow_stderr_error=None, +def _check_stderr( + stderr, allow_stderr_warning, allow_stderr_error, ): """ Check the given stderr for logged warnings and errors. :param stderr: stderr output as a string. :param allow_stderr_warning: whether a logged warning (or deprecation - message) is allowed. Defaults to `allow_stderr_error`. - :param allow_stderr_error: whether a logged error is allowed. Passing - True for this argument implies that warnings are also allowed. - Defaults to False. + message) is allowed. Must be True if allow_stderr_error is True. + :param allow_stderr_error: whether a logged error is allowed. """ - if allow_stderr_error is None: - allow_stderr_error = False - - if allow_stderr_warning is None: - allow_stderr_warning = allow_stderr_error - - if allow_stderr_error and not allow_stderr_warning: - raise RuntimeError( - 'cannot pass allow_stderr_warning=False with ' - 'allow_stderr_error=True' - ) + assert not (allow_stderr_error and not allow_stderr_warning) lines = stderr.splitlines() for line in lines: @@ -500,11 +488,15 @@ def _find_traverse(self, path, result): def run(self, *args, **kw): """ - :param allow_stderr_warning: whether a logged warning (or - deprecation message) is allowed in stderr. :param allow_stderr_error: whether a logged error is allowed in stderr. Passing True for this argument implies `allow_stderr_warning` since warnings are weaker than errors. + :param allow_stderr_warning: whether a logged warning (or + deprecation message) is allowed in stderr. + :param expect_error: if False (the default), asserts that the command + exits with 0. Otherwise, asserts that the command exits with a + non-zero exit code. Passing True also implies allow_stderr_error + and allow_stderr_warning. :param expect_stderr: whether to allow warnings in stderr (equivalent to `allow_stderr_warning`). This argument is an abbreviated version of `allow_stderr_warning` and is also kept for backwards @@ -512,6 +504,7 @@ def run(self, *args, **kw): """ if self.verbose: print('>> running %s %s' % (args, kw)) + cwd = kw.pop('cwd', None) run_from = kw.pop('run_from', None) assert not cwd or not run_from, "Don't use run_from; it's going away" @@ -525,7 +518,9 @@ def run(self, *args, **kw): allow_stderr_error = kw.pop('allow_stderr_error', None) allow_stderr_warning = kw.pop('allow_stderr_warning', None) - if kw.get('expect_error'): + # Propagate default values. + expect_error = kw.get('expect_error') + if expect_error: # Then default to allowing logged errors. if allow_stderr_error is not None and not allow_stderr_error: raise RuntimeError( @@ -533,6 +528,7 @@ def run(self, *args, **kw): 'expect_error=True' ) allow_stderr_error = True + elif kw.get('expect_stderr'): # Then default to allowing logged warnings. if allow_stderr_warning is not None and not allow_stderr_warning: @@ -542,12 +538,30 @@ def run(self, *args, **kw): ) allow_stderr_warning = True + if allow_stderr_error: + if allow_stderr_warning is not None and not allow_stderr_warning: + raise RuntimeError( + 'cannot pass allow_stderr_warning=False with ' + 'allow_stderr_error=True' + ) + + # Default values if not set. + if allow_stderr_error is None: + allow_stderr_error = False + if allow_stderr_warning is None: + allow_stderr_warning = allow_stderr_error + # Pass expect_stderr=True to allow any stderr. We do this because # we do our checking of stderr further on in check_stderr(). kw['expect_stderr'] = True result = super(PipTestEnvironment, self).run(cwd=cwd, *args, **kw) - check_stderr( + if expect_error: + if result.returncode == 0: + __tracebackhide__ = True + raise AssertionError("Script passed unexpectedly.") + + _check_stderr( result.stderr, allow_stderr_error=allow_stderr_error, allow_stderr_warning=allow_stderr_warning, ) @@ -555,6 +569,7 @@ def run(self, *args, **kw): return TestPipResult(result, verbose=self.verbose) def pip(self, *args, **kwargs): + __tracebackhide__ = True if self.pip_expect_warning: kwargs['allow_stderr_warning'] = True if kwargs.pop('use_module', True): diff --git a/tests/lib/test_lib.py b/tests/lib/test_lib.py index dc50196bdc0..051196032a2 100644 --- a/tests/lib/test_lib.py +++ b/tests/lib/test_lib.py @@ -211,3 +211,17 @@ def test_run__allow_stderr_warning_false_error(self, script, arg_name): ) with assert_error_startswith(RuntimeError, expected_start): script.run('python', **kwargs) + + def test_run__expect_error_fails_when_zero_returncode(self, script): + expected_start = 'Script passed unexpectedly' + with assert_error_startswith(AssertionError, expected_start): + script.run( + 'python', expect_error=True + ) + + def test_run__no_expect_error_fails_when_nonzero_returncode(self, script): + expected_start = 'Script returned code: 1' + with assert_error_startswith(AssertionError, expected_start): + script.run( + 'python', '-c', 'import sys; sys.exit(1)' + ) From 4b8ecd6f2dfdae5fb623c4abc45f95c5afeef017 Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara Date: Sat, 22 Jun 2019 22:46:33 -0400 Subject: [PATCH 0095/3170] Add architecture overview. --- docs/html/development/architecture.rst | 474 +++++++++++++++++++++++++ docs/html/development/index.rst | 1 + news/6637.feature | 1 + 3 files changed, 476 insertions(+) create mode 100644 docs/html/development/architecture.rst create mode 100644 news/6637.feature diff --git a/docs/html/development/architecture.rst b/docs/html/development/architecture.rst new file mode 100644 index 00000000000..702a1e27686 --- /dev/null +++ b/docs/html/development/architecture.rst @@ -0,0 +1,474 @@ +============ +Architecture +============ + +Broad functionality overview +---------------------------- + +Pip is a package installer. + +pip does a lot more than installation; it also has a cache, and it has +configuration, and it + +has a CLI, which has its own quirks. But mainly: + +Things pip does: + +1. | Manages the building of packages (offloads package building to a + backend) when that’s necessary (a source distribution package -- + this is not necessary if the package is a wheel). + + 1. | By default, pip delegates package-building to setuptools, for + backwards compat reasons. But thing with setuptools: has a + setup.py file that it invokes to …… get info? + +2. Decides where to install stuff. Once the package is built, resulting + artifact is then installed into system in appropriate place. PEP 517 + defines interface between build backend & installer. + +Broad overview of flow +---------------------- + +In sequence, what does pip do?: + +1. Get user input (user-supplied string saying what package they want) +2. Figure out what that means: exactly what the user requested -- + translate to a thing pip can operate on (user input to requirements) +3. CORE OF THE WHOLE PROCESS, MAYBE? Once you have a set of reqs from + Step 2, you have to expand those into a concrete “things to install” + -- Figure out what other requirements it has to install based on + user-given requirements, and where to get them from. + + a. this step is convoluted - also exploratory, involves dependency + resolution -- we need to get to the index, see what versions + are available + + b. Sometimes you need to build the package itself in order to get + dependency information, which means fetching the package from + package index, which means knowing whether it exists. For a + single package, + +4. Install the actual items to be installed. + +Why? Pip installs from places other than PyPI! But also, we’ve never had +guarantees of PyPI’s JSON API before now, so no one has been getting +metadata from PyPI separate from downloading the package itself. + +In terms of flow of the install process: + +For 1 package: Get abstract requirement(s) for that package, and try and +see what that means (this abstract requirement can take various forms). + +Once we have a set of "this package, get it from here, this is that +version of that package", + +Modify the environment to install those things (which means: place the +files in the right place). For example: if you already have req 2.0 and +you are installing 3.0, uninstall 2 and install 3. + +define abstract dependencies + +Downloading + +Now: download process. What happens in an install? Well, a subset of +\`install\` is \`download`; \`pip download\` is also a process pip does +during install (it downloads from PyPI). And we download and INSPECT +packages to get manifests. + +This is the \`pip download\` command. + +pip download somepackage + +With no other arguments + +By default we look at PyPI which is where packages are stored in a +structured format so an installer like pip can find them + +pip knows where to look to get more info for what the package index +knows about that pkg + +pip then knows: what files are avail + +pip knows what files are available + +what their filenames are + +IN OTHER WORDS + +While all dependencies have not been resolved, do the following: + +- Following the API defined in PEP503, fetch the index page from + `http://{pypi_index}/simple/{package_name `__} + +- Parse all of the file links from the page. + +- Select a single file to download from the list of links. +- Extract the metadata from the downloaded package +- Update the dependency tree based on the metadata + +the package index + +gives pip a list of files for that pkg + +(via the existing PyPI API) + +the files have the version and some other info that helps pip decide +whether that's something they want to download + +pip chooses from the list a single file to download + +It may go back and choose another file to download + +When pip looks at the package index, the place where it looks has +basically a link. The link’s text is the name of the file + +This is the PyPI Simple API -- docs +https://warehouse.readthedocs.io/api-reference/legacy/#simple-project-api + +(PyPI has several APIs, some are being deprecated) + +Pip looks at Simple API, documented initially at PEP 503 -- +packaging.python.org has PyPA specifications with more details for +Simple Repository API + +For this package name -- this is the list of files available + +Looks there for: + +The list of filenames + +Other info + +Once it has those, selects one file, downloads it + +If I want to pip install flask, I think the whole list of filenames +cannot….should not be …. ? I want only the Flask …. Why am I getting the +whole list? + +IT’s not every file, just files of Flask. No API for getting alllllll +files on PyPI. It’s for getting all files of Flask. + +Repository anatomy & directory structure +---------------------------------------- + +https://github.com/pypa/pip/ + +\`pip`’s repo: it’s a standard Python package. Readme, license, +pyproject.toml, setup.py, etc. in the top level. + +There’s a tox.ini https://github.com/pypa/pip/blob/master/tox.ini that +has a lot of …. Describes a few environments pip uses during development +for simplifying how tests are run (complicated situation there) -- tox +-e -py36 …. Can run for different versions of Python by changing “36” to +“27” or similar. Tox is an automation tool + +[question: why a news directory? Mostly description is based on GitHub +issues….] + +[question: is the \_template.rst a Jinja 2 file? Pradyun: idk, check +towncrier docs] + +├── docs/ *[documentation, built with Sphinx]* + +│ ├── html/ *[sources to HTML documentation avail. online]* + +│ ├── man/ *[man pages the distros can use by running \`man pip`]* + +│ └── pip_sphinxext.py *[an extension -- pip-specific plugins to Sphinx +that do not apply to other packages]* + +├── news/ *[pip stores news fragments… Every time pip makes a +user-facing change, a file is added to this directory with the right +extension & name so it gets included in release notes…. So every release +the maintainers will be deleting old files in this directory? Yes - we +use the towncrier automation to generate a NEWS file and auto-delete old +stuff. There’s more about this in the contributor documentation!]* + +│ └── \_template.rst *[template for release notes -- this is a file +towncrier uses…. Is this jinja? I don’t know]* + +├── src/ *[source]* + +│ ├── pip/ *[where all the source code lives. Within that, 2 +directories]* + +│ │ ├── \__init__.py + +│ │ ├── \__main__.py + +│ │ ├── \__pycache__/ *[not discussing contents right now]* + +│ │ ├── \_internal/ *[where all the pip code lives that’s written by pip +maintainers -- underscore means private. Pip is not a library -- it’s a +command line tool! A very important distinction! People who want to +install stuff with pip should not use the internals -- they should use +the CLI. There’s a note on this in the docs.]* + +│ │ │ ├── \__init__.py + +│ │ │ ├── build_env.py [not discussing now] + +│ │ │ ├── cache.py *[has all the info for how to handle caching within +pip -- cache-handling stuff. Uses cachecontrol from PyPI, vendored into +pip]* + +│ │ │ ├── cli/ *[subpackage containing helpers & additional code for +managing the command line interface. Uses argparse from stdlib]* + +│ │ │ │ ├── \__init__.py + +│ │ │ │ ├── autocompletion.py + +│ │ │ │ ├── base_command.py + +│ │ │ │ ├── cmdoptions.py + +│ │ │ │ ├── main_parser.py + +│ │ │ │ ├── parser.py + +│ │ │ │ └── status_codes.py + +│ │ │ ├── commands/ *[literally - each file is the name of the command +on the pip CLI. Each has a class that defines what’s needed to set it +up, what happens]* + +│ │ │ │ ├── \__init__.py + +│ │ │ │ ├── check.py + +│ │ │ │ ├── completion.py + +│ │ │ │ ├── configuration.py + +│ │ │ │ ├── download.py + +│ │ │ │ ├── freeze.py + +│ │ │ │ ├── hash.py + +│ │ │ │ ├── help.py + +│ │ │ │ ├── install.py + +│ │ │ │ ├── list.py + +│ │ │ │ ├── search.py + +│ │ │ │ ├── show.py + +│ │ │ │ ├── uninstall.py + +│ │ │ │ └── wheel.py + +│ │ │ ├── configuration.py + +│ │ │ ├── download.py + +│ │ │ ├── exceptions.py + +│ │ │ ├── index.py + +│ │ │ ├── locations.py + +│ │ │ ├── models/ *[in-process refactoring! Goal: improve how pip +internally models representations it has for data -- data +representation. General overall cleanup. Data reps are spread throughout +codebase….link is defined in a class in 1 file, and then another file +imports Link from that file. Sometimes cyclic dependency?!?! To prevent +future situations like this, etc., Pradyun started moving these into a +models directory.]* + +│ │ │ │ ├── \__init__.py + +│ │ │ │ ├── candidate.py + +│ │ │ │ ├── format_control.py + +│ │ │ │ ├── index.py + +│ │ │ │ └── link.py + +│ │ │ ├── operations/ *[a bit of a weird directory….. Freeze.py used to +be in there. Freeze is an operation -- there was an operations.freeze. +Then “prepare” got added (the operation of preparing a pkg). Then +“check” got added for checking the state of an env.] [what’s a command +vs an operation? Command is on CLI; an operation would be an internal +bit of code that actually does some subset of the operation the command +says. \`install\` command uses bits of \`check\` and \`prepare`, for +instance. In the long run, Pradyun’s goal: \`prepare.py\` goes away +(gets refactored into other files) such that \`operations\` is just +\`check\` and \`freeze`..... … Pradyun plans to refactor this.] [how +does this compare to \`utils`?]* + +│ │ │ │ ├── \__init__.py + +│ │ │ │ ├── check.py + +│ │ │ │ ├── freeze.py + +│ │ │ │ └── prepare.py + +│ │ │ ├── pep425tags.py *[getting refactored into packaging.tags (a +library on PyPI) which is external to pip (but vendored by pip). PEP 425 +tags: turns out lots of people want this! Compatibility tags for built +distributions -> e.g., platform, Python version, etc.]* + +│ │ │ ├── pyproject.py *[pyproject.toml is a new standard (PEP 518 and +517). This file reads pyproject.toml and passes that info elsewhere. The +rest of the processing happens in a different file. All the handling for +517 and 518 is in a different file.]* + +│ │ │ ├── req/ *[*\ **A DIRECTORY THAT NEEDS REFACTORING. A LOT**\ *\ …… +Remember Step 3? Dependency resolution etc.? This is that step! Each +file represents … have the entire flow of installing & uninstalling, +getting info about packages…. Some files here are more than 1,000 lines +long! (used to be longer?!) Refactor will deeply improve developer +experience.]* + +│ │ │ │ ├── \__init__.py + +│ │ │ │ ├── constructors.py + +│ │ │ │ ├── req_file.py + +│ │ │ │ ├── req_install.py + +│ │ │ │ ├── req_set.py + +│ │ │ │ ├── req_tracker.py + +│ │ │ │ └── req_uninstall.py + +│ │ │ ├── resolve.py *[This is where the current dependency resolution +algorithm sits. Pradyun is improving the pip dependency +resolver*\ https://github.com/pypa/pip/issues/988\ *. Pradyun will get +rid of this file and replace it with a directory called “resolution”. +[this work is in git master…. There is further work that is going to be +in a branch soon]]* + +│ │ │ ├── utils/ *[everything that is not “operationally” pip ….. Misc +functions and files get dumped. There’s some organization here. There’s +a models.py here which needs refactoring. Deprecation.py is useful, as +are other things, but some things do not belong here. There ought to be +some GitHub issues for refactoring some things here. Maybe a few issues +with checkbox lists.]* + +│ │ │ │ ├── \__init__.py + +│ │ │ │ ├── appdirs.py + +│ │ │ │ ├── compat.py + +│ │ │ │ ├── deprecation.py + +│ │ │ │ ├── encoding.py + +│ │ │ │ ├── filesystem.py + +│ │ │ │ ├── glibc.py + +│ │ │ │ ├── hashes.py + +│ │ │ │ ├── logging.py + +│ │ │ │ ├── misc.py + +│ │ │ │ ├── models.py + +│ │ │ │ ├── outdated.py + +│ │ │ │ ├── packaging.py + +│ │ │ │ ├── setuptools_build.py + +│ │ │ │ ├── temp_dir.py + +│ │ │ │ ├── typing.py + +│ │ │ │ └── ui.py + +│ │ │ ├── vcs/ *[stands for Version Control System. Where pip handles +all version control stuff -- one of the \`pip install\` arguments you +can use is a version control link. …. Are any of these commands +vendored? No, via subprocesses. For performance, it makes sense (we +think) to do this instead of pygitlib2 or similar -- and has to be pure +Python, can’t include C libraries, because you can’t include compiled C +stuff, because you might not have it for the platform you are running +on.]* + +│ │ │ │ ├── \__init__.py + +│ │ │ │ ├── bazaar.py + +│ │ │ │ ├── git.py + +│ │ │ │ ├── mercurial.py + +│ │ │ │ └── subversion.py + +│ │ │ └── wheel.py *[file that manages installation of a wheel file. +This handles unpacking wheels -- “unpack and spread”. There is a package +on PyPI called \`wheel\` that builds wheels -- do not confuse it with +this.]* + +│ │ └── \_vendor/ *[code from other packages -- pip’s own dependencies…. +Has them in its own source tree, because pip cannot depend on pip being +installed on the machine already!]* + +│ └── pip.egg-info/ *[ignore the contents for now]* + +├── tasks/ *[invoke is a PyPI library which uses files in this directory +to define automation commands that are used in pip’s development +processes -- not discussing further right now. For instance, automating +the release.]* + +├── tests/ *[contains tests you can run. There are instructions in pip’s +Getting Started guide! Which Pradyun wrote!!!!!]* + +│ ├── \__init__.py + +│ ├── conftest.py + +│ ├── data/ *[test data for running tests -- pesudo package index in it! +Lots of small packages that are invalid or are valid. Test fixtures. +Used by functional tests]* + +│ ├── functional/ *[functional tests of pip’s CLI -- end-to-end, invoke +pip in subprocess & check results of execution against desired result. +This also is what makes test suite slow]* + +│ ├── lib/ *[helpers for tests]* + +│ ├── scripts/ *[will probably die in future in a refactor -- scripts +for running all of the tests, but we use pytest now. Someone could make +a PR to remove this! Good first issue!]* + +│ ├── unit/ *[unit tests -- fast and small and nice!]* + +│ └── yaml/ *[resolver tests! They’re written in YAML. This folder just +contains .yaml files -- actual code for reading/running them is in +lib/yaml.py . This is fine!]* + +├── tools/ *[misc development workflow tools, like requirements files & +Travis CI files & helpers for tox]* + +├── AUTHORS.txt + +├── LICENSE.txt + +├── MANIFEST.in + +├── NEWS.rst + +├── README.rst + +├── pyproject.toml + +├── setup.cfg + +├── setup.py + +└── tox.ini + + diff --git a/docs/html/development/index.rst b/docs/html/development/index.rst index 7d10230e00c..65227171ab7 100644 --- a/docs/html/development/index.rst +++ b/docs/html/development/index.rst @@ -13,6 +13,7 @@ or the `pypa-dev mailing list`_, to ask questions or get involved. :maxdepth: 2 getting-started + architecture contributing architecture/index release-process diff --git a/news/6637.feature b/news/6637.feature new file mode 100644 index 00000000000..f6b607d7d95 --- /dev/null +++ b/news/6637.feature @@ -0,0 +1 @@ +Add architectural overview documentation. \ No newline at end of file From 7662d840ed862ecc630c98b014fc67edb26f63b5 Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara Date: Sun, 21 Jul 2019 16:16:58 -0400 Subject: [PATCH 0096/3170] Polish formatting and wording --- docs/html/development/architecture.rst | 130 ++++++++++++------------- 1 file changed, 62 insertions(+), 68 deletions(-) diff --git a/docs/html/development/architecture.rst b/docs/html/development/architecture.rst index 702a1e27686..215867c4f86 100644 --- a/docs/html/development/architecture.rst +++ b/docs/html/development/architecture.rst @@ -1,16 +1,17 @@ -============ +############ Architecture -============ +############ +.. contents:: + +**************************** Broad functionality overview ----------------------------- +**************************** Pip is a package installer. pip does a lot more than installation; it also has a cache, and it has -configuration, and it - -has a CLI, which has its own quirks. But mainly: +configuration, and it has a CLI, which has its own quirks. But mainly: Things pip does: @@ -27,7 +28,7 @@ Things pip does: defines interface between build backend & installer. Broad overview of flow ----------------------- +====================== In sequence, what does pip do?: @@ -56,66 +57,57 @@ metadata from PyPI separate from downloading the package itself. In terms of flow of the install process: -For 1 package: Get abstract requirement(s) for that package, and try and -see what that means (this abstract requirement can take various forms). - -Once we have a set of "this package, get it from here, this is that -version of that package", - -Modify the environment to install those things (which means: place the -files in the right place). For example: if you already have req 2.0 and -you are installing 3.0, uninstall 2 and install 3. +1. For 1 package: Get abstract requirement(s) for that package, and + try and see what that means (this abstract requirement can take + various forms). Define abstract dependencies. -define abstract dependencies +2. Once we have a set of "this package, get it from here, this is that + version of that package", -Downloading +3. Modify the environment to install those things (which means: place + the files in the right place). For example: if you already have + version 6.0 of a requirement and you are installing 7.2, uninstall + 6.0 and install 7.2. -Now: download process. What happens in an install? Well, a subset of -\`install\` is \`download`; \`pip download\` is also a process pip does -during install (it downloads from PyPI). And we download and INSPECT -packages to get manifests. +Download process +---------------- -This is the \`pip download\` command. +What happens in an install? Well, a subset of ``install``, a process +pip usually does during a ``pip install``, is ``download`` (also +available to the user as the :ref:`pip download` command). And we +download and INSPECT packages to get manifests. For any given package +name, we need to know what files are available and what their +filenames are. -pip download somepackage +pip can download from a Python package repository, where packages are +stored in a structured format so an installer like pip can find them. -With no other arguments +:pep:`503` defines the API we use to talk to a Python package repository. -By default we look at PyPI which is where packages are stored in a -structured format so an installer like pip can find them +PyPI +^^^^ -pip knows where to look to get more info for what the package index -knows about that pkg +What happens if we run ``pip download somepackage`` with no other +arguments? By default we look at `PyPI`_, which is where pip knows +where to look to get more info for what the package index knows about +``somepackage``. -pip then knows: what files are avail - -pip knows what files are available - -what their filenames are +pip then knows: what files are available, and what their filenames are IN OTHER WORDS While all dependencies have not been resolved, do the following: -- Following the API defined in PEP503, fetch the index page from - `http://{pypi_index}/simple/{package_name `__} - -- Parse all of the file links from the page. +1. Following the API defined in PEP503, fetch the index page from + `http://{pypi_index}/simple/{package_name `__} +2. Parse all of the file links from the page. +3. Select a single file to download from the list of links. +4. Extract the metadata from the downloaded package +5. Update the dependency tree based on the metadata -- Select a single file to download from the list of links. -- Extract the metadata from the downloaded package -- Update the dependency tree based on the metadata +The package index gives pip a list of files for that pkg (via the existing PyPI API). The files have the version and some other info that helps pip decide whether that's something pip ought to download. -the package index - -gives pip a list of files for that pkg - -(via the existing PyPI API) - -the files have the version and some other info that helps pip decide -whether that's something they want to download - -pip chooses from the list a single file to download +pip chooses from the list a single file to download. It may go back and choose another file to download @@ -135,26 +127,26 @@ For this package name -- this is the list of files available Looks there for: -The list of filenames - -Other info +* The list of filenames +* Other info Once it has those, selects one file, downloads it -If I want to pip install flask, I think the whole list of filenames +(Question: If I want to ``pip install flask``, I think the whole list of filenames cannot….should not be …. ? I want only the Flask …. Why am I getting the whole list? -IT’s not every file, just files of Flask. No API for getting alllllll -files on PyPI. It’s for getting all files of Flask. +Answer: It's not every file, just files of Flask. No API for getting alllllll +files on PyPI. It’s for getting all files of Flask.) +**************************************** Repository anatomy & directory structure ----------------------------------------- +**************************************** https://github.com/pypa/pip/ -\`pip`’s repo: it’s a standard Python package. Readme, license, -pyproject.toml, setup.py, etc. in the top level. +``pip``’s repo: it’s a standard Python package. ``README``, license, +``pyproject.toml``, ``setup.py``, etc. in the top level. There’s a tox.ini https://github.com/pypa/pip/blob/master/tox.ini that has a lot of …. Describes a few environments pip uses during development @@ -172,7 +164,7 @@ towncrier docs] │ ├── html/ *[sources to HTML documentation avail. online]* -│ ├── man/ *[man pages the distros can use by running \`man pip`]* +│ ├── man/ *[man pages the distros can use by running ``man pip``]* │ └── pip_sphinxext.py *[an extension -- pip-specific plugins to Sphinx that do not apply to other packages]* @@ -295,11 +287,11 @@ Then “prepare” got added (the operation of preparing a pkg). Then “check” got added for checking the state of an env.] [what’s a command vs an operation? Command is on CLI; an operation would be an internal bit of code that actually does some subset of the operation the command -says. \`install\` command uses bits of \`check\` and \`prepare`, for -instance. In the long run, Pradyun’s goal: \`prepare.py\` goes away -(gets refactored into other files) such that \`operations\` is just -\`check\` and \`freeze`..... … Pradyun plans to refactor this.] [how -does this compare to \`utils`?]* +says. ``install`` command uses bits of ``check`` and ``prepare``, for +instance. In the long run, Pradyun’s goal: ``prepare.py`` goes away +(gets refactored into other files) such that ``operations`` is just +``check`` and ``freeze``..... … Pradyun plans to refactor this.] [how +does this compare to ``utils``?]* │ │ │ │ ├── \__init__.py @@ -389,7 +381,7 @@ with checkbox lists.]* │ │ │ │ └── ui.py │ │ │ ├── vcs/ *[stands for Version Control System. Where pip handles -all version control stuff -- one of the \`pip install\` arguments you +all version control stuff -- one of the ``pip install`` arguments you can use is a version control link. …. Are any of these commands vendored? No, via subprocesses. For performance, it makes sense (we think) to do this instead of pygitlib2 or similar -- and has to be pure @@ -409,7 +401,7 @@ on.]* │ │ │ └── wheel.py *[file that manages installation of a wheel file. This handles unpacking wheels -- “unpack and spread”. There is a package -on PyPI called \`wheel\` that builds wheels -- do not confuse it with +on PyPI called ``wheel`` that builds wheels -- do not confuse it with this.]* │ │ └── \_vendor/ *[code from other packages -- pip’s own dependencies…. @@ -472,3 +464,5 @@ Travis CI files & helpers for tox]* └── tox.ini + +.. _PyPI: https://pypi.org/ From 8f2d28b56306b0895b4c81b2aed1239cacf9fa89 Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara Date: Wed, 31 Jul 2019 19:06:50 -0400 Subject: [PATCH 0097/3170] WIP to squash --- docs/html/development/architecture.rst | 43 ++++++++++++-------------- 1 file changed, 20 insertions(+), 23 deletions(-) diff --git a/docs/html/development/architecture.rst b/docs/html/development/architecture.rst index 215867c4f86..ecd01989879 100644 --- a/docs/html/development/architecture.rst +++ b/docs/html/development/architecture.rst @@ -20,11 +20,11 @@ Things pip does: this is not necessary if the package is a wheel). 1. | By default, pip delegates package-building to setuptools, for - backwards compat reasons. But thing with setuptools: has a - setup.py file that it invokes to …… get info? + backwards compatibility reasons. But thing with setuptools: + has a ``setup.py`` file that it invokes to …… get info? 2. Decides where to install stuff. Once the package is built, resulting - artifact is then installed into system in appropriate place. PEP 517 + artifact is then installed into system in appropriate place. :pep:`517` defines interface between build backend & installer. Broad overview of flow @@ -92,20 +92,21 @@ arguments? By default we look at `PyPI`_, which is where pip knows where to look to get more info for what the package index knows about ``somepackage``. -pip then knows: what files are available, and what their filenames are +``pip`` then knows: what files are available, and what their filenames +are. IN OTHER WORDS While all dependencies have not been resolved, do the following: -1. Following the API defined in PEP503, fetch the index page from +1. Following the API defined in :pep:`503`, fetch the index page from `http://{pypi_index}/simple/{package_name `__} 2. Parse all of the file links from the page. 3. Select a single file to download from the list of links. -4. Extract the metadata from the downloaded package -5. Update the dependency tree based on the metadata +4. Extract the metadata from the downloaded package. +5. Update the dependency tree based on the metadata. -The package index gives pip a list of files for that pkg (via the existing PyPI API). The files have the version and some other info that helps pip decide whether that's something pip ought to download. +The package index gives pip a list of files for that package (via the existing PyPI API). The files have the version and some other info that helps pip decide whether that's something pip ought to download. pip chooses from the list a single file to download. @@ -114,12 +115,8 @@ It may go back and choose another file to download When pip looks at the package index, the place where it looks has basically a link. The link’s text is the name of the file -This is the PyPI Simple API -- docs -https://warehouse.readthedocs.io/api-reference/legacy/#simple-project-api - -(PyPI has several APIs, some are being deprecated) - -Pip looks at Simple API, documented initially at PEP 503 -- +This is the `PyPI Simple API`_ (PyPI has several APIs, some are being +deprecated). Pip looks at Simple API, documented initially at :pep:`503` -- packaging.python.org has PyPA specifications with more details for Simple Repository API @@ -143,16 +140,13 @@ files on PyPI. It’s for getting all files of Flask.) Repository anatomy & directory structure **************************************** -https://github.com/pypa/pip/ - -``pip``’s repo: it’s a standard Python package. ``README``, license, -``pyproject.toml``, ``setup.py``, etc. in the top level. +``pip``’s codebase (`GitHub repository`_) is structured as a standard Python package. The ``README``, license, ``pyproject.toml``, ``setup.py``, and so on are in the top level. -There’s a tox.ini https://github.com/pypa/pip/blob/master/tox.ini that -has a lot of …. Describes a few environments pip uses during development -for simplifying how tests are run (complicated situation there) -- tox --e -py36 …. Can run for different versions of Python by changing “36” to -“27” or similar. Tox is an automation tool +``pip`` uses Tox, an automation tool, configured by the `tox.ini`_ in +the top level. ``tox.ini`` describes a few environments ``pip`` uses +during development for simplifying how tests are run (complicated +situation there). Example: `` tox -e -py36`` We can run tests for +different versions of Python by changing “36” to “27” or similar. [question: why a news directory? Mostly description is based on GitHub issues….] @@ -466,3 +460,6 @@ Travis CI files & helpers for tox]* .. _PyPI: https://pypi.org/ +.. _GitHub repository: https://github.com/pypa/pip/ +.. _tox.ini: https://github.com/pypa/pip/blob/master/tox.ini +.. _PyPI Simple API: https://warehouse.readthedocs.io/api-reference/legacy/#simple-project-api From 7e51405806cf1f80ee2fc1e677ada981cff0255e Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara Date: Tue, 6 Aug 2019 15:56:44 -0400 Subject: [PATCH 0098/3170] WIP reformatting as nested list --- docs/html/development/architecture.rst | 409 ++++++------------------- 1 file changed, 93 insertions(+), 316 deletions(-) diff --git a/docs/html/development/architecture.rst b/docs/html/development/architecture.rst index ecd01989879..f9d4d4adfbc 100644 --- a/docs/html/development/architecture.rst +++ b/docs/html/development/architecture.rst @@ -140,322 +140,99 @@ files on PyPI. It’s for getting all files of Flask.) Repository anatomy & directory structure **************************************** -``pip``’s codebase (`GitHub repository`_) is structured as a standard Python package. The ``README``, license, ``pyproject.toml``, ``setup.py``, and so on are in the top level. - -``pip`` uses Tox, an automation tool, configured by the `tox.ini`_ in -the top level. ``tox.ini`` describes a few environments ``pip`` uses -during development for simplifying how tests are run (complicated -situation there). Example: `` tox -e -py36`` We can run tests for -different versions of Python by changing “36” to “27” or similar. - -[question: why a news directory? Mostly description is based on GitHub -issues….] - -[question: is the \_template.rst a Jinja 2 file? Pradyun: idk, check -towncrier docs] - -├── docs/ *[documentation, built with Sphinx]* - -│ ├── html/ *[sources to HTML documentation avail. online]* - -│ ├── man/ *[man pages the distros can use by running ``man pip``]* - -│ └── pip_sphinxext.py *[an extension -- pip-specific plugins to Sphinx -that do not apply to other packages]* - -├── news/ *[pip stores news fragments… Every time pip makes a -user-facing change, a file is added to this directory with the right -extension & name so it gets included in release notes…. So every release -the maintainers will be deleting old files in this directory? Yes - we -use the towncrier automation to generate a NEWS file and auto-delete old -stuff. There’s more about this in the contributor documentation!]* - -│ └── \_template.rst *[template for release notes -- this is a file -towncrier uses…. Is this jinja? I don’t know]* - -├── src/ *[source]* - -│ ├── pip/ *[where all the source code lives. Within that, 2 -directories]* - -│ │ ├── \__init__.py - -│ │ ├── \__main__.py - -│ │ ├── \__pycache__/ *[not discussing contents right now]* - -│ │ ├── \_internal/ *[where all the pip code lives that’s written by pip -maintainers -- underscore means private. Pip is not a library -- it’s a -command line tool! A very important distinction! People who want to -install stuff with pip should not use the internals -- they should use -the CLI. There’s a note on this in the docs.]* - -│ │ │ ├── \__init__.py - -│ │ │ ├── build_env.py [not discussing now] - -│ │ │ ├── cache.py *[has all the info for how to handle caching within -pip -- cache-handling stuff. Uses cachecontrol from PyPI, vendored into -pip]* - -│ │ │ ├── cli/ *[subpackage containing helpers & additional code for -managing the command line interface. Uses argparse from stdlib]* - -│ │ │ │ ├── \__init__.py - -│ │ │ │ ├── autocompletion.py - -│ │ │ │ ├── base_command.py - -│ │ │ │ ├── cmdoptions.py - -│ │ │ │ ├── main_parser.py - -│ │ │ │ ├── parser.py - -│ │ │ │ └── status_codes.py - -│ │ │ ├── commands/ *[literally - each file is the name of the command -on the pip CLI. Each has a class that defines what’s needed to set it -up, what happens]* - -│ │ │ │ ├── \__init__.py - -│ │ │ │ ├── check.py - -│ │ │ │ ├── completion.py - -│ │ │ │ ├── configuration.py - -│ │ │ │ ├── download.py - -│ │ │ │ ├── freeze.py - -│ │ │ │ ├── hash.py - -│ │ │ │ ├── help.py - -│ │ │ │ ├── install.py - -│ │ │ │ ├── list.py - -│ │ │ │ ├── search.py - -│ │ │ │ ├── show.py - -│ │ │ │ ├── uninstall.py - -│ │ │ │ └── wheel.py - -│ │ │ ├── configuration.py - -│ │ │ ├── download.py - -│ │ │ ├── exceptions.py - -│ │ │ ├── index.py - -│ │ │ ├── locations.py - -│ │ │ ├── models/ *[in-process refactoring! Goal: improve how pip -internally models representations it has for data -- data -representation. General overall cleanup. Data reps are spread throughout -codebase….link is defined in a class in 1 file, and then another file -imports Link from that file. Sometimes cyclic dependency?!?! To prevent -future situations like this, etc., Pradyun started moving these into a -models directory.]* - -│ │ │ │ ├── \__init__.py - -│ │ │ │ ├── candidate.py - -│ │ │ │ ├── format_control.py - -│ │ │ │ ├── index.py - -│ │ │ │ └── link.py - -│ │ │ ├── operations/ *[a bit of a weird directory….. Freeze.py used to -be in there. Freeze is an operation -- there was an operations.freeze. -Then “prepare” got added (the operation of preparing a pkg). Then -“check” got added for checking the state of an env.] [what’s a command -vs an operation? Command is on CLI; an operation would be an internal -bit of code that actually does some subset of the operation the command -says. ``install`` command uses bits of ``check`` and ``prepare``, for -instance. In the long run, Pradyun’s goal: ``prepare.py`` goes away -(gets refactored into other files) such that ``operations`` is just -``check`` and ``freeze``..... … Pradyun plans to refactor this.] [how -does this compare to ``utils``?]* - -│ │ │ │ ├── \__init__.py - -│ │ │ │ ├── check.py - -│ │ │ │ ├── freeze.py - -│ │ │ │ └── prepare.py - -│ │ │ ├── pep425tags.py *[getting refactored into packaging.tags (a -library on PyPI) which is external to pip (but vendored by pip). PEP 425 -tags: turns out lots of people want this! Compatibility tags for built -distributions -> e.g., platform, Python version, etc.]* - -│ │ │ ├── pyproject.py *[pyproject.toml is a new standard (PEP 518 and -517). This file reads pyproject.toml and passes that info elsewhere. The -rest of the processing happens in a different file. All the handling for -517 and 518 is in a different file.]* - -│ │ │ ├── req/ *[*\ **A DIRECTORY THAT NEEDS REFACTORING. A LOT**\ *\ …… -Remember Step 3? Dependency resolution etc.? This is that step! Each -file represents … have the entire flow of installing & uninstalling, -getting info about packages…. Some files here are more than 1,000 lines -long! (used to be longer?!) Refactor will deeply improve developer -experience.]* - -│ │ │ │ ├── \__init__.py - -│ │ │ │ ├── constructors.py - -│ │ │ │ ├── req_file.py - -│ │ │ │ ├── req_install.py - -│ │ │ │ ├── req_set.py - -│ │ │ │ ├── req_tracker.py - -│ │ │ │ └── req_uninstall.py - -│ │ │ ├── resolve.py *[This is where the current dependency resolution -algorithm sits. Pradyun is improving the pip dependency -resolver*\ https://github.com/pypa/pip/issues/988\ *. Pradyun will get -rid of this file and replace it with a directory called “resolution”. -[this work is in git master…. There is further work that is going to be -in a branch soon]]* - -│ │ │ ├── utils/ *[everything that is not “operationally” pip ….. Misc -functions and files get dumped. There’s some organization here. There’s -a models.py here which needs refactoring. Deprecation.py is useful, as -are other things, but some things do not belong here. There ought to be -some GitHub issues for refactoring some things here. Maybe a few issues -with checkbox lists.]* - -│ │ │ │ ├── \__init__.py - -│ │ │ │ ├── appdirs.py - -│ │ │ │ ├── compat.py - -│ │ │ │ ├── deprecation.py - -│ │ │ │ ├── encoding.py - -│ │ │ │ ├── filesystem.py - -│ │ │ │ ├── glibc.py - -│ │ │ │ ├── hashes.py - -│ │ │ │ ├── logging.py - -│ │ │ │ ├── misc.py - -│ │ │ │ ├── models.py - -│ │ │ │ ├── outdated.py - -│ │ │ │ ├── packaging.py - -│ │ │ │ ├── setuptools_build.py - -│ │ │ │ ├── temp_dir.py - -│ │ │ │ ├── typing.py - -│ │ │ │ └── ui.py - -│ │ │ ├── vcs/ *[stands for Version Control System. Where pip handles -all version control stuff -- one of the ``pip install`` arguments you -can use is a version control link. …. Are any of these commands -vendored? No, via subprocesses. For performance, it makes sense (we -think) to do this instead of pygitlib2 or similar -- and has to be pure -Python, can’t include C libraries, because you can’t include compiled C -stuff, because you might not have it for the platform you are running -on.]* - -│ │ │ │ ├── \__init__.py - -│ │ │ │ ├── bazaar.py - -│ │ │ │ ├── git.py - -│ │ │ │ ├── mercurial.py - -│ │ │ │ └── subversion.py - -│ │ │ └── wheel.py *[file that manages installation of a wheel file. -This handles unpacking wheels -- “unpack and spread”. There is a package -on PyPI called ``wheel`` that builds wheels -- do not confuse it with -this.]* - -│ │ └── \_vendor/ *[code from other packages -- pip’s own dependencies…. -Has them in its own source tree, because pip cannot depend on pip being -installed on the machine already!]* - -│ └── pip.egg-info/ *[ignore the contents for now]* - -├── tasks/ *[invoke is a PyPI library which uses files in this directory -to define automation commands that are used in pip’s development -processes -- not discussing further right now. For instance, automating -the release.]* - -├── tests/ *[contains tests you can run. There are instructions in pip’s -Getting Started guide! Which Pradyun wrote!!!!!]* - -│ ├── \__init__.py - -│ ├── conftest.py - -│ ├── data/ *[test data for running tests -- pesudo package index in it! -Lots of small packages that are invalid or are valid. Test fixtures. -Used by functional tests]* - -│ ├── functional/ *[functional tests of pip’s CLI -- end-to-end, invoke -pip in subprocess & check results of execution against desired result. -This also is what makes test suite slow]* - -│ ├── lib/ *[helpers for tests]* - -│ ├── scripts/ *[will probably die in future in a refactor -- scripts -for running all of the tests, but we use pytest now. Someone could make -a PR to remove this! Good first issue!]* - -│ ├── unit/ *[unit tests -- fast and small and nice!]* - -│ └── yaml/ *[resolver tests! They’re written in YAML. This folder just -contains .yaml files -- actual code for reading/running them is in -lib/yaml.py . This is fine!]* - -├── tools/ *[misc development workflow tools, like requirements files & -Travis CI files & helpers for tox]* - -├── AUTHORS.txt - -├── LICENSE.txt - -├── MANIFEST.in - -├── NEWS.rst - -├── README.rst - -├── pyproject.toml - -├── setup.cfg - -├── setup.py - -└── tox.ini +``pip``’s codebase (`GitHub repository`_) is structured as a standard Python package. + + +Root and tools +============== + +The ``README``, license, ``pyproject.toml``, ``setup.py``, and so on are in the top level. + + +* ``AUTHORS.txt`` +* ``LICENSE.txt`` +* ``MANIFEST.in`` +* ``NEWS.rst`` +* ``pyproject.toml`` +* ``README.rst`` +* ``setup.cfg`` +* ``setup.py`` +* ``tox.ini`` ``pip`` uses Tox, an automation tool, configured by the `tox.ini`_ in the top level. ``tox.ini`` describes a few environments ``pip`` uses during development for simplifying how tests are run (complicated situation there). Example: `` tox -e -py36``. We can run tests for different versions of Python by changing “36” to “27” or similar. +* ``.appveyor.yml`` +* ``.coveragerc`` +* ``.gitattributes`` +* ``.gitignore`` +* ``.mailmap`` +* ``.readthedocs.yml`` +* ``.travis.yml`` +* ``docs/`` *[documentation, built with Sphinx]* + + * ``html/`` *[sources to HTML documentation avail. online]* + * ``man/`` *[man pages the distros can use by running ``man pip``]* + * ``pip_sphinxext.py`` *[an extension -- pip-specific plugins to Sphinx that do not apply to other packages]* + +* ``news/`` *[pip stores news fragments… Every time pip makes a user-facing change, a file is added to this directory (usually a short note referring to a GitHub issue) with the right extension & name so it gets included in release notes…. So every release the maintainers will be deleting old files in this directory? Yes - we use the towncrier automation to generate a NEWS file, and auto-delete old stuff. There’s more about this in the contributor documentation!]* + + * ``template.rst`` *[template for release notes -- this is a file towncrier uses…. Is this jinja? I don’t know, check towncrier docs]* + +* ``src/`` *[source; see below]* +* ``tasks/`` *[invoke is a PyPI library which uses files in this directory to define automation commands that are used in pip’s development processes -- not discussing further right now. For instance, automating the release.]* +* ``tests/`` *[contains tests you can run. There are instructions in pip’s Getting Started guide! Which Pradyun wrote!!!!!]* + + * ``__init__.py`` + * ``conftest.py`` + * ``data/`` *[test data for running tests -- pesudo package index in it! Lots of small packages that are invalid or are valid. Test fixtures. Used by functional tests]* + * ``functional/`` *[functional tests of pip’s CLI -- end-to-end, invoke pip in subprocess & check results of execution against desired result. This also is what makes test suite slow]* + * ``lib/`` *[helpers for tests]* + * ``scripts/`` *[will probably die in future in a refactor -- scripts for running all of the tests, but we use pytest now. Someone could make a PR to remove this! Good first issue!]* + * ``unit/`` *[unit tests -- fast and small and nice!]* + * ``yaml/`` *[resolver tests! They’re written in YAML. This folder just contains .yaml files -- actual code for reading/running them is in lib/yaml.py . This is fine!]* + +* ``tools`` *[misc development workflow tools, like requirements files & Travis CI files & helpers for tox]* +* ``.azure-pipelines`` +* ``.github`` +* ``.tox`` + + + +src directory +============= + +In the root directory, the ``src/`` directory contains pip's core +source code. Within ``src/pip/``, ``_internal/`` has the pip code +that's written by pip maintainers, and ``\_vendor/`` is pip's +dependencies (code from other packages). + +* ``pip/`` + + * ``__init__.py`` + * ``__main__.py`` + * ``__pycache__/`` *[not discussing contents right now]* + * ``_internal/`` *[where all the pip code lives that’s written by pip maintainers -- underscore means private. Pip is not a library -- it’s a command line tool! A very important distinction! People who want to install stuff with pip should not use the internals -- they should use the CLI. There’s a note on this in the docs.]* + * ``__init__.py`` + * ``build_env.py`` [not discussing now] + * ``cache.py`` *[has all the info for how to handle caching within pip -- cache-handling stuff. Uses cachecontrol from PyPI, vendored into pip]* + * ``cli/`` *[subpackage containing helpers & additional code for managing the command line interface. Uses argparse from stdlib]* + + * ``commands/`` *[literally - each file is the name of the command on the pip CLI. Each has a class that defines what’s needed to set it up, what happens]* + * ``configuration.py`` + * ``download.py`` + * ``exceptions.py`` + * ``index.py`` + * ``locations.py`` + * ``models/`` *[in-process refactoring! Goal: improve how pip internally models representations it has for data -- data representation. General overall cleanup. Data reps are spread throughout codebase….link is defined in a class in 1 file, and then another file imports Link from that file. Sometimes cyclic dependency?!?! To prevent future situations like this, etc., Pradyun started moving these into a models directory.]* + * ``operations/`` *[a bit of a weird directory….. Freeze.py used to be in there. Freeze is an operation -- there was an operations.freeze. Then “prepare” got added (the operation of preparing a pkg). Then “check” got added for checking the state of an env.] [what’s a command vs an operation? Command is on CLI; an operation would be an internal bit of code that actually does some subset of the operation the command says. ``install`` command uses bits of ``check`` and ``prepare``, for instance. In the long run, Pradyun’s goal: ``prepare.py`` goes away (gets refactored into other files) such that ``operations`` is just ``check`` and ``freeze``..... … Pradyun plans to refactor this.] [how does this compare to ``utils``?]* + * ``pep425tags.py`` *[getting refactored into packaging.tags (a library on PyPI) which is external to pip (but vendored by pip). PEP 425 tags: turns out lots of people want this! Compatibility tags for built distributions -> e.g., platform, Python version, etc.]* + * ``pyproject.py`` *[pyproject.toml is a new standard (PEP 518 and 517). This file reads pyproject.toml and passes that info elsewhere. The rest of the processing happens in a different file. All the handling for 517 and 518 is in a different file.]* + * ``req/`` *[*\ **A DIRECTORY THAT NEEDS REFACTORING. A LOT**\ *\ …… Remember Step 3? Dependency resolution etc.? This is that step! Each file represents … have the entire flow of installing & uninstalling, getting info about packages…. Some files here are more than 1,000 lines long! (used to be longer?!) Refactor will deeply improve developer experience.]* + * ``resolve.py`` *[This is where the current dependency resolution algorithm sits. Pradyun is improving the pip dependency resolver*\ https://github.com/pypa/pip/issues/988\ *. Pradyun will get rid of this file and replace it with a directory called “resolution”. [this work is in git master…. There is further work that is going to be in a branch soon]]* + * ``utils/`` *[everything that is not “operationally” pip ….. Misc functions and files get dumped. There’s some organization here. There’s a models.py here which needs refactoring. Deprecation.py is useful, as are other things, but some things do not belong here. There ought to be some GitHub issues for refactoring some things here. Maybe a few issues with checkbox lists.]* + * ``vcs/`` *[stands for Version Control System. Where pip handles all version control stuff -- one of the ``pip install`` arguments you can use is a version control link. …. Are any of these commands +vendored? No, via subprocesses. For performance, it makes sense (we think) to do this instead of pygitlib2 or similar -- and has to be pure Python, can’t include C libraries, because you can’t include compiled C stuff, because you might not have it for the platform you are running on.]* + * ``wheel.py`` *[file that manages installation of a wheel file. This handles unpacking wheels -- “unpack and spread”. There is a package on PyPI called ``wheel`` that builds wheels -- do not confuse it with this.]* + * ``_vendor/`` *[code from other packages -- pip’s own dependencies…. Has them in its own source tree, because pip cannot depend on pip being installed on the machine already!]* + +* ``pip.egg-info/`` *[ignore the contents for now]* From c03335502a42d98685b4dc90da5c6d332f08fdd3 Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara Date: Tue, 6 Aug 2019 16:07:44 -0400 Subject: [PATCH 0099/3170] WIP formatting of source section --- docs/html/development/architecture.rst | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/docs/html/development/architecture.rst b/docs/html/development/architecture.rst index f9d4d4adfbc..711e91c87a1 100644 --- a/docs/html/development/architecture.rst +++ b/docs/html/development/architecture.rst @@ -148,7 +148,6 @@ Root and tools The ``README``, license, ``pyproject.toml``, ``setup.py``, and so on are in the top level. - * ``AUTHORS.txt`` * ``LICENSE.txt`` * ``MANIFEST.in`` @@ -203,17 +202,20 @@ source code. Within ``src/pip/``, ``_internal/`` has the pip code that's written by pip maintainers, and ``\_vendor/`` is pip's dependencies (code from other packages). +Within ``src/``: + +* ``pip.egg-info/`` *[ignore the contents for now]* * ``pip/`` * ``__init__.py`` * ``__main__.py`` * ``__pycache__/`` *[not discussing contents right now]* * ``_internal/`` *[where all the pip code lives that’s written by pip maintainers -- underscore means private. Pip is not a library -- it’s a command line tool! A very important distinction! People who want to install stuff with pip should not use the internals -- they should use the CLI. There’s a note on this in the docs.]* + * ``__init__.py`` * ``build_env.py`` [not discussing now] * ``cache.py`` *[has all the info for how to handle caching within pip -- cache-handling stuff. Uses cachecontrol from PyPI, vendored into pip]* * ``cli/`` *[subpackage containing helpers & additional code for managing the command line interface. Uses argparse from stdlib]* - * ``commands/`` *[literally - each file is the name of the command on the pip CLI. Each has a class that defines what’s needed to set it up, what happens]* * ``configuration.py`` * ``download.py`` @@ -225,14 +227,13 @@ dependencies (code from other packages). * ``pep425tags.py`` *[getting refactored into packaging.tags (a library on PyPI) which is external to pip (but vendored by pip). PEP 425 tags: turns out lots of people want this! Compatibility tags for built distributions -> e.g., platform, Python version, etc.]* * ``pyproject.py`` *[pyproject.toml is a new standard (PEP 518 and 517). This file reads pyproject.toml and passes that info elsewhere. The rest of the processing happens in a different file. All the handling for 517 and 518 is in a different file.]* * ``req/`` *[*\ **A DIRECTORY THAT NEEDS REFACTORING. A LOT**\ *\ …… Remember Step 3? Dependency resolution etc.? This is that step! Each file represents … have the entire flow of installing & uninstalling, getting info about packages…. Some files here are more than 1,000 lines long! (used to be longer?!) Refactor will deeply improve developer experience.]* - * ``resolve.py`` *[This is where the current dependency resolution algorithm sits. Pradyun is improving the pip dependency resolver*\ https://github.com/pypa/pip/issues/988\ *. Pradyun will get rid of this file and replace it with a directory called “resolution”. [this work is in git master…. There is further work that is going to be in a branch soon]]* + * ``resolve.py`` *[This is where the current dependency resolution algorithm sits. Pradyun is `improving the pip dependency resolver`_. Pradyun will get rid of this file and replace it with a directory called “resolution”. (this work is in git master…. There is further work that is going to be in a branch soon)]* * ``utils/`` *[everything that is not “operationally” pip ….. Misc functions and files get dumped. There’s some organization here. There’s a models.py here which needs refactoring. Deprecation.py is useful, as are other things, but some things do not belong here. There ought to be some GitHub issues for refactoring some things here. Maybe a few issues with checkbox lists.]* - * ``vcs/`` *[stands for Version Control System. Where pip handles all version control stuff -- one of the ``pip install`` arguments you can use is a version control link. …. Are any of these commands -vendored? No, via subprocesses. For performance, it makes sense (we think) to do this instead of pygitlib2 or similar -- and has to be pure Python, can’t include C libraries, because you can’t include compiled C stuff, because you might not have it for the platform you are running on.]* + * ``vcs/`` *[stands for Version Control System. Where pip handles all version control stuff -- one of the ``pip install`` arguments you can use is a version control link. Are any of these commands vendored? No, via subprocesses. For performance, it makes sense (we think) to do this instead of pygitlib2 or similar -- and has to be pure Python, can’t include C libraries, because you can’t include compiled C stuff, because you might not have it for the platform you are running on.]* * ``wheel.py`` *[file that manages installation of a wheel file. This handles unpacking wheels -- “unpack and spread”. There is a package on PyPI called ``wheel`` that builds wheels -- do not confuse it with this.]* + * ``_vendor/`` *[code from other packages -- pip’s own dependencies…. Has them in its own source tree, because pip cannot depend on pip being installed on the machine already!]* - -* ``pip.egg-info/`` *[ignore the contents for now]* + @@ -240,3 +241,4 @@ vendored? No, via subprocesses. For performance, it makes sense (we think) to do .. _GitHub repository: https://github.com/pypa/pip/ .. _tox.ini: https://github.com/pypa/pip/blob/master/tox.ini .. _PyPI Simple API: https://warehouse.readthedocs.io/api-reference/legacy/#simple-project-api +.. _improving the pip depedency resolver: https://github.com/pypa/pip/issues/988 From 165e5a33ffee9baf2b11c3e6ffd674a322d47f5d Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara Date: Tue, 6 Aug 2019 17:41:59 -0400 Subject: [PATCH 0100/3170] WIP reorganizing --- docs/html/development/architecture.rst | 241 ------------------ .../html/development/architecture/anatomy.rst | 100 ++++++++ docs/html/development/architecture/index.rst | 4 + .../development/architecture/overview.rst | 139 ++++++++++ docs/html/development/index.rst | 1 - 5 files changed, 243 insertions(+), 242 deletions(-) create mode 100644 docs/html/development/architecture/anatomy.rst create mode 100644 docs/html/development/architecture/overview.rst diff --git a/docs/html/development/architecture.rst b/docs/html/development/architecture.rst index 711e91c87a1..9d22f971229 100644 --- a/docs/html/development/architecture.rst +++ b/docs/html/development/architecture.rst @@ -1,244 +1,3 @@ ############ Architecture ############ - -.. contents:: - -**************************** -Broad functionality overview -**************************** - -Pip is a package installer. - -pip does a lot more than installation; it also has a cache, and it has -configuration, and it has a CLI, which has its own quirks. But mainly: - -Things pip does: - -1. | Manages the building of packages (offloads package building to a - backend) when that’s necessary (a source distribution package -- - this is not necessary if the package is a wheel). - - 1. | By default, pip delegates package-building to setuptools, for - backwards compatibility reasons. But thing with setuptools: - has a ``setup.py`` file that it invokes to …… get info? - -2. Decides where to install stuff. Once the package is built, resulting - artifact is then installed into system in appropriate place. :pep:`517` - defines interface between build backend & installer. - -Broad overview of flow -====================== - -In sequence, what does pip do?: - -1. Get user input (user-supplied string saying what package they want) -2. Figure out what that means: exactly what the user requested -- - translate to a thing pip can operate on (user input to requirements) -3. CORE OF THE WHOLE PROCESS, MAYBE? Once you have a set of reqs from - Step 2, you have to expand those into a concrete “things to install” - -- Figure out what other requirements it has to install based on - user-given requirements, and where to get them from. - - a. this step is convoluted - also exploratory, involves dependency - resolution -- we need to get to the index, see what versions - are available - - b. Sometimes you need to build the package itself in order to get - dependency information, which means fetching the package from - package index, which means knowing whether it exists. For a - single package, - -4. Install the actual items to be installed. - -Why? Pip installs from places other than PyPI! But also, we’ve never had -guarantees of PyPI’s JSON API before now, so no one has been getting -metadata from PyPI separate from downloading the package itself. - -In terms of flow of the install process: - -1. For 1 package: Get abstract requirement(s) for that package, and - try and see what that means (this abstract requirement can take - various forms). Define abstract dependencies. - -2. Once we have a set of "this package, get it from here, this is that - version of that package", - -3. Modify the environment to install those things (which means: place - the files in the right place). For example: if you already have - version 6.0 of a requirement and you are installing 7.2, uninstall - 6.0 and install 7.2. - -Download process ----------------- - -What happens in an install? Well, a subset of ``install``, a process -pip usually does during a ``pip install``, is ``download`` (also -available to the user as the :ref:`pip download` command). And we -download and INSPECT packages to get manifests. For any given package -name, we need to know what files are available and what their -filenames are. - -pip can download from a Python package repository, where packages are -stored in a structured format so an installer like pip can find them. - -:pep:`503` defines the API we use to talk to a Python package repository. - -PyPI -^^^^ - -What happens if we run ``pip download somepackage`` with no other -arguments? By default we look at `PyPI`_, which is where pip knows -where to look to get more info for what the package index knows about -``somepackage``. - -``pip`` then knows: what files are available, and what their filenames -are. - -IN OTHER WORDS - -While all dependencies have not been resolved, do the following: - -1. Following the API defined in :pep:`503`, fetch the index page from - `http://{pypi_index}/simple/{package_name `__} -2. Parse all of the file links from the page. -3. Select a single file to download from the list of links. -4. Extract the metadata from the downloaded package. -5. Update the dependency tree based on the metadata. - -The package index gives pip a list of files for that package (via the existing PyPI API). The files have the version and some other info that helps pip decide whether that's something pip ought to download. - -pip chooses from the list a single file to download. - -It may go back and choose another file to download - -When pip looks at the package index, the place where it looks has -basically a link. The link’s text is the name of the file - -This is the `PyPI Simple API`_ (PyPI has several APIs, some are being -deprecated). Pip looks at Simple API, documented initially at :pep:`503` -- -packaging.python.org has PyPA specifications with more details for -Simple Repository API - -For this package name -- this is the list of files available - -Looks there for: - -* The list of filenames -* Other info - -Once it has those, selects one file, downloads it - -(Question: If I want to ``pip install flask``, I think the whole list of filenames -cannot….should not be …. ? I want only the Flask …. Why am I getting the -whole list? - -Answer: It's not every file, just files of Flask. No API for getting alllllll -files on PyPI. It’s for getting all files of Flask.) - -**************************************** -Repository anatomy & directory structure -**************************************** - -``pip``’s codebase (`GitHub repository`_) is structured as a standard Python package. - - -Root and tools -============== - -The ``README``, license, ``pyproject.toml``, ``setup.py``, and so on are in the top level. - -* ``AUTHORS.txt`` -* ``LICENSE.txt`` -* ``MANIFEST.in`` -* ``NEWS.rst`` -* ``pyproject.toml`` -* ``README.rst`` -* ``setup.cfg`` -* ``setup.py`` -* ``tox.ini`` ``pip`` uses Tox, an automation tool, configured by the `tox.ini`_ in the top level. ``tox.ini`` describes a few environments ``pip`` uses during development for simplifying how tests are run (complicated situation there). Example: `` tox -e -py36``. We can run tests for different versions of Python by changing “36” to “27” or similar. -* ``.appveyor.yml`` -* ``.coveragerc`` -* ``.gitattributes`` -* ``.gitignore`` -* ``.mailmap`` -* ``.readthedocs.yml`` -* ``.travis.yml`` -* ``docs/`` *[documentation, built with Sphinx]* - - * ``html/`` *[sources to HTML documentation avail. online]* - * ``man/`` *[man pages the distros can use by running ``man pip``]* - * ``pip_sphinxext.py`` *[an extension -- pip-specific plugins to Sphinx that do not apply to other packages]* - -* ``news/`` *[pip stores news fragments… Every time pip makes a user-facing change, a file is added to this directory (usually a short note referring to a GitHub issue) with the right extension & name so it gets included in release notes…. So every release the maintainers will be deleting old files in this directory? Yes - we use the towncrier automation to generate a NEWS file, and auto-delete old stuff. There’s more about this in the contributor documentation!]* - - * ``template.rst`` *[template for release notes -- this is a file towncrier uses…. Is this jinja? I don’t know, check towncrier docs]* - -* ``src/`` *[source; see below]* -* ``tasks/`` *[invoke is a PyPI library which uses files in this directory to define automation commands that are used in pip’s development processes -- not discussing further right now. For instance, automating the release.]* -* ``tests/`` *[contains tests you can run. There are instructions in pip’s Getting Started guide! Which Pradyun wrote!!!!!]* - - * ``__init__.py`` - * ``conftest.py`` - * ``data/`` *[test data for running tests -- pesudo package index in it! Lots of small packages that are invalid or are valid. Test fixtures. Used by functional tests]* - * ``functional/`` *[functional tests of pip’s CLI -- end-to-end, invoke pip in subprocess & check results of execution against desired result. This also is what makes test suite slow]* - * ``lib/`` *[helpers for tests]* - * ``scripts/`` *[will probably die in future in a refactor -- scripts for running all of the tests, but we use pytest now. Someone could make a PR to remove this! Good first issue!]* - * ``unit/`` *[unit tests -- fast and small and nice!]* - * ``yaml/`` *[resolver tests! They’re written in YAML. This folder just contains .yaml files -- actual code for reading/running them is in lib/yaml.py . This is fine!]* - -* ``tools`` *[misc development workflow tools, like requirements files & Travis CI files & helpers for tox]* -* ``.azure-pipelines`` -* ``.github`` -* ``.tox`` - - - -src directory -============= - -In the root directory, the ``src/`` directory contains pip's core -source code. Within ``src/pip/``, ``_internal/`` has the pip code -that's written by pip maintainers, and ``\_vendor/`` is pip's -dependencies (code from other packages). - -Within ``src/``: - -* ``pip.egg-info/`` *[ignore the contents for now]* -* ``pip/`` - - * ``__init__.py`` - * ``__main__.py`` - * ``__pycache__/`` *[not discussing contents right now]* - * ``_internal/`` *[where all the pip code lives that’s written by pip maintainers -- underscore means private. Pip is not a library -- it’s a command line tool! A very important distinction! People who want to install stuff with pip should not use the internals -- they should use the CLI. There’s a note on this in the docs.]* - - * ``__init__.py`` - * ``build_env.py`` [not discussing now] - * ``cache.py`` *[has all the info for how to handle caching within pip -- cache-handling stuff. Uses cachecontrol from PyPI, vendored into pip]* - * ``cli/`` *[subpackage containing helpers & additional code for managing the command line interface. Uses argparse from stdlib]* - * ``commands/`` *[literally - each file is the name of the command on the pip CLI. Each has a class that defines what’s needed to set it up, what happens]* - * ``configuration.py`` - * ``download.py`` - * ``exceptions.py`` - * ``index.py`` - * ``locations.py`` - * ``models/`` *[in-process refactoring! Goal: improve how pip internally models representations it has for data -- data representation. General overall cleanup. Data reps are spread throughout codebase….link is defined in a class in 1 file, and then another file imports Link from that file. Sometimes cyclic dependency?!?! To prevent future situations like this, etc., Pradyun started moving these into a models directory.]* - * ``operations/`` *[a bit of a weird directory….. Freeze.py used to be in there. Freeze is an operation -- there was an operations.freeze. Then “prepare” got added (the operation of preparing a pkg). Then “check” got added for checking the state of an env.] [what’s a command vs an operation? Command is on CLI; an operation would be an internal bit of code that actually does some subset of the operation the command says. ``install`` command uses bits of ``check`` and ``prepare``, for instance. In the long run, Pradyun’s goal: ``prepare.py`` goes away (gets refactored into other files) such that ``operations`` is just ``check`` and ``freeze``..... … Pradyun plans to refactor this.] [how does this compare to ``utils``?]* - * ``pep425tags.py`` *[getting refactored into packaging.tags (a library on PyPI) which is external to pip (but vendored by pip). PEP 425 tags: turns out lots of people want this! Compatibility tags for built distributions -> e.g., platform, Python version, etc.]* - * ``pyproject.py`` *[pyproject.toml is a new standard (PEP 518 and 517). This file reads pyproject.toml and passes that info elsewhere. The rest of the processing happens in a different file. All the handling for 517 and 518 is in a different file.]* - * ``req/`` *[*\ **A DIRECTORY THAT NEEDS REFACTORING. A LOT**\ *\ …… Remember Step 3? Dependency resolution etc.? This is that step! Each file represents … have the entire flow of installing & uninstalling, getting info about packages…. Some files here are more than 1,000 lines long! (used to be longer?!) Refactor will deeply improve developer experience.]* - * ``resolve.py`` *[This is where the current dependency resolution algorithm sits. Pradyun is `improving the pip dependency resolver`_. Pradyun will get rid of this file and replace it with a directory called “resolution”. (this work is in git master…. There is further work that is going to be in a branch soon)]* - * ``utils/`` *[everything that is not “operationally” pip ….. Misc functions and files get dumped. There’s some organization here. There’s a models.py here which needs refactoring. Deprecation.py is useful, as are other things, but some things do not belong here. There ought to be some GitHub issues for refactoring some things here. Maybe a few issues with checkbox lists.]* - * ``vcs/`` *[stands for Version Control System. Where pip handles all version control stuff -- one of the ``pip install`` arguments you can use is a version control link. Are any of these commands vendored? No, via subprocesses. For performance, it makes sense (we think) to do this instead of pygitlib2 or similar -- and has to be pure Python, can’t include C libraries, because you can’t include compiled C stuff, because you might not have it for the platform you are running on.]* - * ``wheel.py`` *[file that manages installation of a wheel file. This handles unpacking wheels -- “unpack and spread”. There is a package on PyPI called ``wheel`` that builds wheels -- do not confuse it with this.]* - - * ``_vendor/`` *[code from other packages -- pip’s own dependencies…. Has them in its own source tree, because pip cannot depend on pip being installed on the machine already!]* - - - - -.. _PyPI: https://pypi.org/ -.. _GitHub repository: https://github.com/pypa/pip/ -.. _tox.ini: https://github.com/pypa/pip/blob/master/tox.ini -.. _PyPI Simple API: https://warehouse.readthedocs.io/api-reference/legacy/#simple-project-api -.. _improving the pip depedency resolver: https://github.com/pypa/pip/issues/988 diff --git a/docs/html/development/architecture/anatomy.rst b/docs/html/development/architecture/anatomy.rst new file mode 100644 index 00000000000..02c4c8c12e5 --- /dev/null +++ b/docs/html/development/architecture/anatomy.rst @@ -0,0 +1,100 @@ + +**************************************** +Repository anatomy & directory structure +**************************************** + +``pip``’s codebase (`GitHub repository`_) is structured as a standard Python package. + + +Root and tools +============== + +The ``README``, license, ``pyproject.toml``, ``setup.py``, and so on are in the top level. + +* ``AUTHORS.txt`` +* ``LICENSE.txt`` +* ``MANIFEST.in`` +* ``NEWS.rst`` +* ``pyproject.toml`` +* ``README.rst`` +* ``setup.cfg`` +* ``setup.py`` +* ``tox.ini`` ``pip`` uses Tox, an automation tool, configured by the `tox.ini`_ in the top level. ``tox.ini`` describes a few environments ``pip`` uses during development for simplifying how tests are run (complicated situation there). Example: `` tox -e -py36``. We can run tests for different versions of Python by changing “36” to “27” or similar. +* ``.appveyor.yml`` +* ``.coveragerc`` +* ``.gitattributes`` +* ``.gitignore`` +* ``.mailmap`` +* ``.readthedocs.yml`` +* ``.travis.yml`` +* ``docs/`` *[documentation, built with Sphinx]* + + * ``html/`` *[sources to HTML documentation avail. online]* + * ``man/`` *[man pages the distros can use by running ``man pip``]* + * ``pip_sphinxext.py`` *[an extension -- pip-specific plugins to Sphinx that do not apply to other packages]* + +* ``news/`` *[pip stores news fragments… Every time pip makes a user-facing change, a file is added to this directory (usually a short note referring to a GitHub issue) with the right extension & name so it gets included in release notes…. So every release the maintainers will be deleting old files in this directory? Yes - we use the towncrier automation to generate a NEWS file, and auto-delete old stuff. There’s more about this in the contributor documentation!]* + + * ``template.rst`` *[template for release notes -- this is a file towncrier uses…. Is this jinja? I don’t know, check towncrier docs]* + +* ``src/`` *[source; see below]* +* ``tasks/`` *[invoke is a PyPI library which uses files in this directory to define automation commands that are used in pip’s development processes -- not discussing further right now. For instance, automating the release.]* +* ``tests/`` *[contains tests you can run. There are instructions in pip’s Getting Started guide! Which Pradyun wrote!!!!!]* + + * ``__init__.py`` + * ``conftest.py`` + * ``data/`` *[test data for running tests -- pesudo package index in it! Lots of small packages that are invalid or are valid. Test fixtures. Used by functional tests]* + * ``functional/`` *[functional tests of pip’s CLI -- end-to-end, invoke pip in subprocess & check results of execution against desired result. This also is what makes test suite slow]* + * ``lib/`` *[helpers for tests]* + * ``scripts/`` *[will probably die in future in a refactor -- scripts for running all of the tests, but we use pytest now. Someone could make a PR to remove this! Good first issue!]* + * ``unit/`` *[unit tests -- fast and small and nice!]* + * ``yaml/`` *[resolver tests! They’re written in YAML. This folder just contains .yaml files -- actual code for reading/running them is in lib/yaml.py . This is fine!]* + +* ``tools`` *[misc development workflow tools, like requirements files & Travis CI files & helpers for tox]* +* ``.azure-pipelines`` +* ``.github`` +* ``.tox`` + + + +src directory +============= + +In the root directory, the ``src/`` directory contains pip's core +source code. Within ``src/pip/``, ``_internal/`` has the pip code +that's written by pip maintainers, and ``\_vendor/`` is pip's +dependencies (code from other packages). + +Within ``src/``: + +* ``pip.egg-info/`` *[ignore the contents for now]* +* ``pip/`` + + * ``__init__.py`` + * ``__main__.py`` + * ``__pycache__/`` *[not discussing contents right now]* + * ``_internal/`` *[where all the pip code lives that’s written by pip maintainers -- underscore means private. Pip is not a library -- it’s a command line tool! A very important distinction! People who want to install stuff with pip should not use the internals -- they should use the CLI. There’s a note on this in the docs.]* + + * ``__init__.py`` + * ``build_env.py`` [not discussing now] + * ``cache.py`` *[has all the info for how to handle caching within pip -- cache-handling stuff. Uses cachecontrol from PyPI, vendored into pip]* + * ``cli/`` *[subpackage containing helpers & additional code for managing the command line interface. Uses argparse from stdlib]* + * ``commands/`` *[literally - each file is the name of the command on the pip CLI. Each has a class that defines what’s needed to set it up, what happens]* + * ``configuration.py`` + * ``download.py`` + * ``exceptions.py`` + * ``index.py`` + * ``locations.py`` + * ``models/`` *[in-process refactoring! Goal: improve how pip internally models representations it has for data -- data representation. General overall cleanup. Data reps are spread throughout codebase….link is defined in a class in 1 file, and then another file imports Link from that file. Sometimes cyclic dependency?!?! To prevent future situations like this, etc., Pradyun started moving these into a models directory.]* + * ``operations/`` *[a bit of a weird directory….. Freeze.py used to be in there. Freeze is an operation -- there was an operations.freeze. Then “prepare” got added (the operation of preparing a pkg). Then “check” got added for checking the state of an env.] [what’s a command vs an operation? Command is on CLI; an operation would be an internal bit of code that actually does some subset of the operation the command says. ``install`` command uses bits of ``check`` and ``prepare``, for instance. In the long run, Pradyun’s goal: ``prepare.py`` goes away (gets refactored into other files) such that ``operations`` is just ``check`` and ``freeze``..... … Pradyun plans to refactor this.] [how does this compare to ``utils``?]* + * ``pep425tags.py`` *[getting refactored into packaging.tags (a library on PyPI) which is external to pip (but vendored by pip). PEP 425 tags: turns out lots of people want this! Compatibility tags for built distributions -> e.g., platform, Python version, etc.]* + * ``pyproject.py`` *[pyproject.toml is a new standard (PEP 518 and 517). This file reads pyproject.toml and passes that info elsewhere. The rest of the processing happens in a different file. All the handling for 517 and 518 is in a different file.]* + * ``req/`` *[*\ **A DIRECTORY THAT NEEDS REFACTORING. A LOT**\ *\ …… Remember Step 3? Dependency resolution etc.? This is that step! Each file represents … have the entire flow of installing & uninstalling, getting info about packages…. Some files here are more than 1,000 lines long! (used to be longer?!) Refactor will deeply improve developer experience.]* + * ``resolve.py`` *[This is where the current dependency resolution algorithm sits. Pradyun is `improving the pip dependency resolver`_. Pradyun will get rid of this file and replace it with a directory called “resolution”. (this work is in git master…. There is further work that is going to be in a branch soon)]* + * ``utils/`` *[everything that is not “operationally” pip ….. Misc functions and files get dumped. There’s some organization here. There’s a models.py here which needs refactoring. Deprecation.py is useful, as are other things, but some things do not belong here. There ought to be some GitHub issues for refactoring some things here. Maybe a few issues with checkbox lists.]* + * ``vcs/`` *[stands for Version Control System. Where pip handles all version control stuff -- one of the ``pip install`` arguments you can use is a version control link. Are any of these commands vendored? No, via subprocesses. For performance, it makes sense (we think) to do this instead of pygitlib2 or similar -- and has to be pure Python, can’t include C libraries, because you can’t include compiled C stuff, because you might not have it for the platform you are running on.]* + * ``wheel.py`` *[file that manages installation of a wheel file. This handles unpacking wheels -- “unpack and spread”. There is a package on PyPI called ``wheel`` that builds wheels -- do not confuse it with this.]* + + * ``_vendor/`` *[code from other packages -- pip’s own dependencies…. Has them in its own source tree, because pip cannot depend on pip being installed on the machine already!]* + +.. _improving the pip depedency resolver: https://github.com/pypa/pip/issues/988 diff --git a/docs/html/development/architecture/index.rst b/docs/html/development/architecture/index.rst index f3f221565c4..25dfe5b686f 100644 --- a/docs/html/development/architecture/index.rst +++ b/docs/html/development/architecture/index.rst @@ -15,6 +15,10 @@ Architecture of pip's internals .. toctree:: :maxdepth: 2 + overview + anatomy + + .. _`tracking issue`: https://github.com/pypa/pip/issues/6831 diff --git a/docs/html/development/architecture/overview.rst b/docs/html/development/architecture/overview.rst new file mode 100644 index 00000000000..a165debb05d --- /dev/null +++ b/docs/html/development/architecture/overview.rst @@ -0,0 +1,139 @@ +.. contents:: + +**************************** +Broad functionality overview +**************************** + +Pip is a package installer. + +pip does a lot more than installation; it also has a cache, and it has +configuration, and it has a CLI, which has its own quirks. But mainly: + +Things pip does: + +1. | Manages the building of packages (offloads package building to a + backend) when that’s necessary (a source distribution package -- + this is not necessary if the package is a wheel). + + 1. | By default, pip delegates package-building to setuptools, for + backwards compatibility reasons. But thing with setuptools: + has a ``setup.py`` file that it invokes to …… get info? + +2. Decides where to install stuff. Once the package is built, resulting + artifact is then installed into system in appropriate place. :pep:`517` + defines interface between build backend & installer. + +Broad overview of flow +====================== + +In sequence, what does pip do?: + +1. Get user input (user-supplied string saying what package they want) +2. Figure out what that means: exactly what the user requested -- + translate to a thing pip can operate on (user input to requirements) +3. CORE OF THE WHOLE PROCESS, MAYBE? Once you have a set of reqs from + Step 2, you have to expand those into a concrete “things to install” + -- Figure out what other requirements it has to install based on + user-given requirements, and where to get them from. + + a. this step is convoluted - also exploratory, involves dependency + resolution -- we need to get to the index, see what versions + are available + + b. Sometimes you need to build the package itself in order to get + dependency information, which means fetching the package from + package index, which means knowing whether it exists. For a + single package, + +4. Install the actual items to be installed. + +Why? Pip installs from places other than PyPI! But also, we’ve never had +guarantees of PyPI’s JSON API before now, so no one has been getting +metadata from PyPI separate from downloading the package itself. + +In terms of flow of the install process: + +1. For 1 package: Get abstract requirement(s) for that package, and + try and see what that means (this abstract requirement can take + various forms). Define abstract dependencies. + +2. Once we have a set of "this package, get it from here, this is that + version of that package", + +3. Modify the environment to install those things (which means: place + the files in the right place). For example: if you already have + version 6.0 of a requirement and you are installing 7.2, uninstall + 6.0 and install 7.2. + +Download process +---------------- + +What happens in an install? Well, a subset of ``install``, a process +pip usually does during a ``pip install``, is ``download`` (also +available to the user as the :ref:`pip download` command). And we +download and INSPECT packages to get manifests. For any given package +name, we need to know what files are available and what their +filenames are. + +pip can download from a Python package repository, where packages are +stored in a structured format so an installer like pip can find them. + +:pep:`503` defines the API we use to talk to a Python package repository. + +PyPI +^^^^ + +What happens if we run ``pip download somepackage`` with no other +arguments? By default we look at `PyPI`_, which is where pip knows +where to look to get more info for what the package index knows about +``somepackage``. + +``pip`` then knows: what files are available, and what their filenames +are. + +IN OTHER WORDS + +While all dependencies have not been resolved, do the following: + +1. Following the API defined in :pep:`503`, fetch the index page from + `http://{pypi_index}/simple/{package_name `__} +2. Parse all of the file links from the page. +3. Select a single file to download from the list of links. +4. Extract the metadata from the downloaded package. +5. Update the dependency tree based on the metadata. + +The package index gives pip a list of files for that package (via the existing PyPI API). The files have the version and some other info that helps pip decide whether that's something pip ought to download. + +pip chooses from the list a single file to download. + +It may go back and choose another file to download + +When pip looks at the package index, the place where it looks has +basically a link. The link’s text is the name of the file + +This is the `PyPI Simple API`_ (PyPI has several APIs, some are being +deprecated). Pip looks at Simple API, documented initially at :pep:`503` -- +packaging.python.org has PyPA specifications with more details for +Simple Repository API + +For this package name -- this is the list of files available + +Looks there for: + +* The list of filenames +* Other info + +Once it has those, selects one file, downloads it + +(Question: If I want to ``pip install flask``, I think the whole list of filenames +cannot….should not be …. ? I want only the Flask …. Why am I getting the +whole list? + +Answer: It's not every file, just files of Flask. No API for getting alllllll +files on PyPI. It’s for getting all files of Flask.) + +.. _PyPI: https://pypi.org/ +.. _GitHub repository: https://github.com/pypa/pip/ +.. _tox.ini: https://github.com/pypa/pip/blob/master/tox.ini +.. _PyPI Simple API: https://warehouse.readthedocs.io/api-reference/legacy/#simple-project-api + diff --git a/docs/html/development/index.rst b/docs/html/development/index.rst index 65227171ab7..7d10230e00c 100644 --- a/docs/html/development/index.rst +++ b/docs/html/development/index.rst @@ -13,7 +13,6 @@ or the `pypa-dev mailing list`_, to ask questions or get involved. :maxdepth: 2 getting-started - architecture contributing architecture/index release-process From f6a4c71c9538fe5dae5a74fd03b16244719f6003 Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara Date: Tue, 6 Aug 2019 17:44:45 -0400 Subject: [PATCH 0101/3170] WIP fixing errors --- docs/html/development/architecture.rst | 3 --- docs/html/development/architecture/anatomy.rst | 2 ++ docs/html/development/architecture/overview.rst | 2 -- 3 files changed, 2 insertions(+), 5 deletions(-) delete mode 100644 docs/html/development/architecture.rst diff --git a/docs/html/development/architecture.rst b/docs/html/development/architecture.rst deleted file mode 100644 index 9d22f971229..00000000000 --- a/docs/html/development/architecture.rst +++ /dev/null @@ -1,3 +0,0 @@ -############ -Architecture -############ diff --git a/docs/html/development/architecture/anatomy.rst b/docs/html/development/architecture/anatomy.rst index 02c4c8c12e5..b97256fe959 100644 --- a/docs/html/development/architecture/anatomy.rst +++ b/docs/html/development/architecture/anatomy.rst @@ -97,4 +97,6 @@ Within ``src/``: * ``_vendor/`` *[code from other packages -- pip’s own dependencies…. Has them in its own source tree, because pip cannot depend on pip being installed on the machine already!]* +.. _GitHub repository: https://github.com/pypa/pip/ +.. _tox.ini: https://github.com/pypa/pip/blob/master/tox.ini .. _improving the pip depedency resolver: https://github.com/pypa/pip/issues/988 diff --git a/docs/html/development/architecture/overview.rst b/docs/html/development/architecture/overview.rst index a165debb05d..75ccf142043 100644 --- a/docs/html/development/architecture/overview.rst +++ b/docs/html/development/architecture/overview.rst @@ -133,7 +133,5 @@ Answer: It's not every file, just files of Flask. No API for getting alllllll files on PyPI. It’s for getting all files of Flask.) .. _PyPI: https://pypi.org/ -.. _GitHub repository: https://github.com/pypa/pip/ -.. _tox.ini: https://github.com/pypa/pip/blob/master/tox.ini .. _PyPI Simple API: https://warehouse.readthedocs.io/api-reference/legacy/#simple-project-api From 72094355dc3c34448a28942f1cd5b3c5561b6633 Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara Date: Tue, 6 Aug 2019 17:51:00 -0400 Subject: [PATCH 0102/3170] WIP polish formatting --- docs/html/development/architecture/anatomy.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/html/development/architecture/anatomy.rst b/docs/html/development/architecture/anatomy.rst index b97256fe959..65597dd3d09 100644 --- a/docs/html/development/architecture/anatomy.rst +++ b/docs/html/development/architecture/anatomy.rst @@ -19,7 +19,7 @@ The ``README``, license, ``pyproject.toml``, ``setup.py``, and so on are in the * ``README.rst`` * ``setup.cfg`` * ``setup.py`` -* ``tox.ini`` ``pip`` uses Tox, an automation tool, configured by the `tox.ini`_ in the top level. ``tox.ini`` describes a few environments ``pip`` uses during development for simplifying how tests are run (complicated situation there). Example: `` tox -e -py36``. We can run tests for different versions of Python by changing “36” to “27” or similar. +* ``tox.ini`` -- ``pip`` uses Tox, an automation tool, configured by this `tox.ini`_ file. ``tox.ini`` describes a few environments ``pip`` uses during development for simplifying how tests are run (complicated situation there). Example: ``tox -e -py36``. We can run tests for different versions of Python by changing “36” to “27” or similar. * ``.appveyor.yml`` * ``.coveragerc`` * ``.gitattributes`` @@ -62,7 +62,7 @@ src directory In the root directory, the ``src/`` directory contains pip's core source code. Within ``src/pip/``, ``_internal/`` has the pip code -that's written by pip maintainers, and ``\_vendor/`` is pip's +that's written by pip maintainers, and ``_vendor/`` is pip's dependencies (code from other packages). Within ``src/``: @@ -86,11 +86,11 @@ Within ``src/``: * ``index.py`` * ``locations.py`` * ``models/`` *[in-process refactoring! Goal: improve how pip internally models representations it has for data -- data representation. General overall cleanup. Data reps are spread throughout codebase….link is defined in a class in 1 file, and then another file imports Link from that file. Sometimes cyclic dependency?!?! To prevent future situations like this, etc., Pradyun started moving these into a models directory.]* - * ``operations/`` *[a bit of a weird directory….. Freeze.py used to be in there. Freeze is an operation -- there was an operations.freeze. Then “prepare” got added (the operation of preparing a pkg). Then “check” got added for checking the state of an env.] [what’s a command vs an operation? Command is on CLI; an operation would be an internal bit of code that actually does some subset of the operation the command says. ``install`` command uses bits of ``check`` and ``prepare``, for instance. In the long run, Pradyun’s goal: ``prepare.py`` goes away (gets refactored into other files) such that ``operations`` is just ``check`` and ``freeze``..... … Pradyun plans to refactor this.] [how does this compare to ``utils``?]* - * ``pep425tags.py`` *[getting refactored into packaging.tags (a library on PyPI) which is external to pip (but vendored by pip). PEP 425 tags: turns out lots of people want this! Compatibility tags for built distributions -> e.g., platform, Python version, etc.]* - * ``pyproject.py`` *[pyproject.toml is a new standard (PEP 518 and 517). This file reads pyproject.toml and passes that info elsewhere. The rest of the processing happens in a different file. All the handling for 517 and 518 is in a different file.]* + * ``operations/`` -- a bit of a weird directory….. Freeze.py used to be in there. Freeze is an operation -- there was an operations.freeze. Then “prepare” got added (the operation of preparing a pkg). Then “check” got added for checking the state of an env.] [what’s a command vs an operation? Command is on CLI; an operation would be an internal bit of code that actually does some subset of the operation the command says. ``install`` command uses bits of ``check`` and ``prepare``, for instance. In the long run, Pradyun’s goal: ``prepare.py`` goes away (gets refactored into other files) such that ``operations`` is just ``check`` and ``freeze``..... … Pradyun plans to refactor this. [how does this compare to ``utils``?] + * ``pep425tags.py`` *[getting refactored into packaging.tags (a library on PyPI) which is external to pip (but vendored by pip). :pep:`425` tags: turns out lots of people want this! Compatibility tags for built distributions -> e.g., platform, Python version, etc.]* + * ``pyproject.py`` *[pyproject.toml is a new standard (:pep:`518` and :pep:`517`). This file reads pyproject.toml and passes that info elsewhere. The rest of the processing happens in a different file. All the handling for 517 and 518 is in a different file.]* * ``req/`` *[*\ **A DIRECTORY THAT NEEDS REFACTORING. A LOT**\ *\ …… Remember Step 3? Dependency resolution etc.? This is that step! Each file represents … have the entire flow of installing & uninstalling, getting info about packages…. Some files here are more than 1,000 lines long! (used to be longer?!) Refactor will deeply improve developer experience.]* - * ``resolve.py`` *[This is where the current dependency resolution algorithm sits. Pradyun is `improving the pip dependency resolver`_. Pradyun will get rid of this file and replace it with a directory called “resolution”. (this work is in git master…. There is further work that is going to be in a branch soon)]* + * ``resolve.py`` -- This is where the current dependency resolution algorithm sits. Pradyun is `improving the pip dependency resolver`_. Pradyun will get rid of this file and replace it with a directory called “resolution”. (this work is in git master…. There is further work that is going to be in a branch soon) * ``utils/`` *[everything that is not “operationally” pip ….. Misc functions and files get dumped. There’s some organization here. There’s a models.py here which needs refactoring. Deprecation.py is useful, as are other things, but some things do not belong here. There ought to be some GitHub issues for refactoring some things here. Maybe a few issues with checkbox lists.]* * ``vcs/`` *[stands for Version Control System. Where pip handles all version control stuff -- one of the ``pip install`` arguments you can use is a version control link. Are any of these commands vendored? No, via subprocesses. For performance, it makes sense (we think) to do this instead of pygitlib2 or similar -- and has to be pure Python, can’t include C libraries, because you can’t include compiled C stuff, because you might not have it for the platform you are running on.]* * ``wheel.py`` *[file that manages installation of a wheel file. This handles unpacking wheels -- “unpack and spread”. There is a package on PyPI called ``wheel`` that builds wheels -- do not confuse it with this.]* @@ -99,4 +99,4 @@ Within ``src/``: .. _GitHub repository: https://github.com/pypa/pip/ .. _tox.ini: https://github.com/pypa/pip/blob/master/tox.ini -.. _improving the pip depedency resolver: https://github.com/pypa/pip/issues/988 +.. _improving the pip dependency resolver: https://github.com/pypa/pip/issues/988 From 9b0383ea6cd318ac05f19dead33d88f885351aee Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara Date: Tue, 6 Aug 2019 18:03:28 -0400 Subject: [PATCH 0103/3170] WIP improve linking and formatting --- docs/html/development/architecture/anatomy.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/html/development/architecture/anatomy.rst b/docs/html/development/architecture/anatomy.rst index 65597dd3d09..5569f165a90 100644 --- a/docs/html/development/architecture/anatomy.rst +++ b/docs/html/development/architecture/anatomy.rst @@ -30,7 +30,7 @@ The ``README``, license, ``pyproject.toml``, ``setup.py``, and so on are in the * ``docs/`` *[documentation, built with Sphinx]* * ``html/`` *[sources to HTML documentation avail. online]* - * ``man/`` *[man pages the distros can use by running ``man pip``]* + * ``man/`` has man pages the distros can use by running ``man pip`` * ``pip_sphinxext.py`` *[an extension -- pip-specific plugins to Sphinx that do not apply to other packages]* * ``news/`` *[pip stores news fragments… Every time pip makes a user-facing change, a file is added to this directory (usually a short note referring to a GitHub issue) with the right extension & name so it gets included in release notes…. So every release the maintainers will be deleting old files in this directory? Yes - we use the towncrier automation to generate a NEWS file, and auto-delete old stuff. There’s more about this in the contributor documentation!]* @@ -39,7 +39,7 @@ The ``README``, license, ``pyproject.toml``, ``setup.py``, and so on are in the * ``src/`` *[source; see below]* * ``tasks/`` *[invoke is a PyPI library which uses files in this directory to define automation commands that are used in pip’s development processes -- not discussing further right now. For instance, automating the release.]* -* ``tests/`` *[contains tests you can run. There are instructions in pip’s Getting Started guide! Which Pradyun wrote!!!!!]* +* ``tests/`` -- contains tests you can run. There are instructions in :doc:`../getting-started`. * ``__init__.py`` * ``conftest.py`` @@ -86,14 +86,14 @@ Within ``src/``: * ``index.py`` * ``locations.py`` * ``models/`` *[in-process refactoring! Goal: improve how pip internally models representations it has for data -- data representation. General overall cleanup. Data reps are spread throughout codebase….link is defined in a class in 1 file, and then another file imports Link from that file. Sometimes cyclic dependency?!?! To prevent future situations like this, etc., Pradyun started moving these into a models directory.]* - * ``operations/`` -- a bit of a weird directory….. Freeze.py used to be in there. Freeze is an operation -- there was an operations.freeze. Then “prepare” got added (the operation of preparing a pkg). Then “check” got added for checking the state of an env.] [what’s a command vs an operation? Command is on CLI; an operation would be an internal bit of code that actually does some subset of the operation the command says. ``install`` command uses bits of ``check`` and ``prepare``, for instance. In the long run, Pradyun’s goal: ``prepare.py`` goes away (gets refactored into other files) such that ``operations`` is just ``check`` and ``freeze``..... … Pradyun plans to refactor this. [how does this compare to ``utils``?] - * ``pep425tags.py`` *[getting refactored into packaging.tags (a library on PyPI) which is external to pip (but vendored by pip). :pep:`425` tags: turns out lots of people want this! Compatibility tags for built distributions -> e.g., platform, Python version, etc.]* - * ``pyproject.py`` *[pyproject.toml is a new standard (:pep:`518` and :pep:`517`). This file reads pyproject.toml and passes that info elsewhere. The rest of the processing happens in a different file. All the handling for 517 and 518 is in a different file.]* + * ``operations/`` -- a bit of a weird directory….. ``Freeze.py`` used to be in there. Freeze is an operation -- there was an operations.freeze. Then “prepare” got added (the operation of preparing a pkg). Then “check” got added for checking the state of an env.] [what’s a command vs an operation? Command is on CLI; an operation would be an internal bit of code that actually does some subset of the operation the command says. ``install`` command uses bits of ``check`` and ``prepare``, for instance. In the long run, Pradyun’s goal: ``prepare.py`` goes away (gets refactored into other files) such that ``operations`` is just ``check`` and ``freeze``..... … Pradyun plans to refactor this. [how does this compare to ``utils``?] + * ``pep425tags.py`` -- getting refactored into packaging.tags (a library on PyPI) which is external to pip (but vendored by pip). :pep:`425` tags: turns out lots of people want this! Compatibility tags for built distributions -> e.g., platform, Python version, etc. + * ``pyproject.py`` -- ``pyproject.toml`` is a new standard (:pep:`518` and :pep:`517`). This file reads pyproject.toml and passes that info elsewhere. The rest of the processing happens in a different file. All the handling for 517 and 518 is in a different file. * ``req/`` *[*\ **A DIRECTORY THAT NEEDS REFACTORING. A LOT**\ *\ …… Remember Step 3? Dependency resolution etc.? This is that step! Each file represents … have the entire flow of installing & uninstalling, getting info about packages…. Some files here are more than 1,000 lines long! (used to be longer?!) Refactor will deeply improve developer experience.]* * ``resolve.py`` -- This is where the current dependency resolution algorithm sits. Pradyun is `improving the pip dependency resolver`_. Pradyun will get rid of this file and replace it with a directory called “resolution”. (this work is in git master…. There is further work that is going to be in a branch soon) * ``utils/`` *[everything that is not “operationally” pip ….. Misc functions and files get dumped. There’s some organization here. There’s a models.py here which needs refactoring. Deprecation.py is useful, as are other things, but some things do not belong here. There ought to be some GitHub issues for refactoring some things here. Maybe a few issues with checkbox lists.]* * ``vcs/`` *[stands for Version Control System. Where pip handles all version control stuff -- one of the ``pip install`` arguments you can use is a version control link. Are any of these commands vendored? No, via subprocesses. For performance, it makes sense (we think) to do this instead of pygitlib2 or similar -- and has to be pure Python, can’t include C libraries, because you can’t include compiled C stuff, because you might not have it for the platform you are running on.]* - * ``wheel.py`` *[file that manages installation of a wheel file. This handles unpacking wheels -- “unpack and spread”. There is a package on PyPI called ``wheel`` that builds wheels -- do not confuse it with this.]* + * ``wheel.py`` is a file that manages installation of a wheel file. This handles unpacking wheels -- “unpack and spread”. There is a package on PyPI called ``wheel`` that builds wheels -- do not confuse it with this. * ``_vendor/`` *[code from other packages -- pip’s own dependencies…. Has them in its own source tree, because pip cannot depend on pip being installed on the machine already!]* From 8d732c709f5e897375a8c50df21d5f81b2cb26e1 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Tue, 13 Aug 2019 20:29:36 -0400 Subject: [PATCH 0104/3170] Make selfcheck state key explicit. --- src/pip/_internal/utils/outdated.py | 8 ++++++-- tests/unit/test_unit_outdated.py | 9 +++++++++ 2 files changed, 15 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/utils/outdated.py b/src/pip/_internal/utils/outdated.py index 2b10aeff6bb..d58f48c6e17 100644 --- a/src/pip/_internal/utils/outdated.py +++ b/src/pip/_internal/utils/outdated.py @@ -41,12 +41,16 @@ def __init__(self, cache_dir): self.statefile_path = os.path.join(cache_dir, "selfcheck.json") try: with open(self.statefile_path) as statefile: - self.state = json.load(statefile)[sys.prefix] + self.state = json.load(statefile)[self.key] except (IOError, ValueError, KeyError): # Explicitly suppressing exceptions, since we don't want to # error out if the cache file is invalid. pass + @property + def key(self): + return sys.prefix + def save(self, pypi_version, current_time): # type: (str, datetime.datetime) -> None # If we do not have a path to cache in, don't bother saving. @@ -69,7 +73,7 @@ def save(self, pypi_version, current_time): else: state = {} - state[sys.prefix] = { + state[self.key] = { "last_check": current_time.strftime(SELFCHECK_DATE_FMT), "pypi_version": pypi_version, } diff --git a/tests/unit/test_unit_outdated.py b/tests/unit/test_unit_outdated.py index a5d37f81868..9b6edf9f7f1 100644 --- a/tests/unit/test_unit_outdated.py +++ b/tests/unit/test_unit_outdated.py @@ -183,3 +183,12 @@ def test_self_check_state_no_cache_dir(): state = outdated.SelfCheckState(cache_dir=False) assert state.state == {} assert state.statefile_path is None + + +def test_self_check_state_key_uses_sys_prefix(monkeypatch): + key = "helloworld" + + monkeypatch.setattr(sys, "prefix", key) + state = outdated.SelfCheckState("") + + assert state.key == key From 0220fbc6d6085dc88f6586e7545bf3fdce7bd992 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Tue, 13 Aug 2019 20:58:18 -0400 Subject: [PATCH 0105/3170] Add tests for reading/writing selfcheck state file. --- src/pip/_internal/utils/outdated.py | 5 ++- tests/unit/test_unit_outdated.py | 59 ++++++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/utils/outdated.py b/src/pip/_internal/utils/outdated.py index d58f48c6e17..89269453285 100644 --- a/src/pip/_internal/utils/outdated.py +++ b/src/pip/_internal/utils/outdated.py @@ -30,6 +30,9 @@ logger = logging.getLogger(__name__) +_STATEFILE_NAME = "selfcheck.json" + + class SelfCheckState(object): def __init__(self, cache_dir): # type: (str) -> None @@ -38,7 +41,7 @@ def __init__(self, cache_dir): # Try to load the existing state if cache_dir: - self.statefile_path = os.path.join(cache_dir, "selfcheck.json") + self.statefile_path = os.path.join(cache_dir, _STATEFILE_NAME) try: with open(self.statefile_path) as statefile: self.state = json.load(statefile)[self.key] diff --git a/tests/unit/test_unit_outdated.py b/tests/unit/test_unit_outdated.py index 9b6edf9f7f1..31943d093d7 100644 --- a/tests/unit/test_unit_outdated.py +++ b/tests/unit/test_unit_outdated.py @@ -1,4 +1,5 @@ import datetime +import json import os import sys from contextlib import contextmanager @@ -135,6 +136,10 @@ def test_pip_version_check(monkeypatch, stored_time, installed_ver, new_ver, assert len(outdated.logger.warning.calls) == 0 +def _get_statefile_path(cache_dir): + return os.path.join(cache_dir, outdated._STATEFILE_NAME) + + def test_self_check_state(monkeypatch, tmpdir): CONTENT = '''{"pip_prefix": {"last_check": "1970-01-02T11:00:00Z", "pypi_version": "1.0"}}''' @@ -166,7 +171,7 @@ def fake_lock(filename): state = outdated.SelfCheckState(cache_dir=cache_dir) state.save('2.0', datetime.datetime.utcnow()) - expected_path = cache_dir / 'selfcheck.json' + expected_path = _get_statefile_path(str(cache_dir)) assert fake_lock.calls == [pretend.call(expected_path)] assert fake_open.calls == [ @@ -192,3 +197,55 @@ def test_self_check_state_key_uses_sys_prefix(monkeypatch): state = outdated.SelfCheckState("") assert state.key == key + + +def test_self_check_state_reads_expected_statefile(monkeypatch, tmpdir): + cache_dir = tmpdir / "cache_dir" + cache_dir.mkdir() + key = "helloworld" + statefile_path = _get_statefile_path(str(cache_dir)) + + last_check = "1970-01-02T11:00:00Z" + pypi_version = "1.0" + content = { + key: { + "last_check": last_check, + "pypi_version": pypi_version, + }, + } + + with open(statefile_path, "w") as f: + json.dump(content, f) + + monkeypatch.setattr(sys, "prefix", key) + state = outdated.SelfCheckState(str(cache_dir)) + + assert state.state["last_check"] == last_check + assert state.state["pypi_version"] == pypi_version + + +def test_self_check_state_writes_expected_statefile(monkeypatch, tmpdir): + cache_dir = tmpdir / "cache_dir" + cache_dir.mkdir() + key = "helloworld" + statefile_path = _get_statefile_path(str(cache_dir)) + + last_check = datetime.datetime.strptime( + "1970-01-02T11:00:00Z", outdated.SELFCHECK_DATE_FMT + ) + pypi_version = "1.0" + + monkeypatch.setattr(sys, "prefix", key) + state = outdated.SelfCheckState(str(cache_dir)) + + state.save(pypi_version, last_check) + with open(statefile_path) as f: + saved = json.load(f) + + expected = { + key: { + "last_check": last_check.strftime(outdated.SELFCHECK_DATE_FMT), + "pypi_version": pypi_version, + }, + } + assert expected == saved From f6468178c22a2f2ac8a40bd041f43da2f169b86b Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Tue, 13 Aug 2019 21:49:02 -0400 Subject: [PATCH 0106/3170] Make selfcheck state file path prefix-specific. --- src/pip/_internal/utils/outdated.py | 14 +++++++++++--- tests/unit/test_unit_outdated.py | 15 +++++++++------ 2 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/utils/outdated.py b/src/pip/_internal/utils/outdated.py index 89269453285..e1f9129d108 100644 --- a/src/pip/_internal/utils/outdated.py +++ b/src/pip/_internal/utils/outdated.py @@ -1,6 +1,7 @@ from __future__ import absolute_import import datetime +import hashlib import json import logging import os.path @@ -8,6 +9,7 @@ from pip._vendor import lockfile, pkg_resources from pip._vendor.packaging import version as packaging_version +from pip._vendor.six import ensure_binary from pip._internal.cli.cmdoptions import make_search_scope from pip._internal.index import PackageFinder @@ -20,7 +22,7 @@ if MYPY_CHECK_RUNNING: import optparse - from typing import Any, Dict + from typing import Any, Dict, Text, Union from pip._internal.download import PipSession @@ -30,7 +32,11 @@ logger = logging.getLogger(__name__) -_STATEFILE_NAME = "selfcheck.json" +def _get_statefile_name(key): + # type: (Union[str, Text]) -> str + key_bytes = ensure_binary(key) + name = hashlib.sha256(key_bytes).hexdigest() + return "selfcheck-{}.json".format(name) class SelfCheckState(object): @@ -41,7 +47,9 @@ def __init__(self, cache_dir): # Try to load the existing state if cache_dir: - self.statefile_path = os.path.join(cache_dir, _STATEFILE_NAME) + self.statefile_path = os.path.join( + cache_dir, _get_statefile_name(self.key) + ) try: with open(self.statefile_path) as statefile: self.state = json.load(statefile)[self.key] diff --git a/tests/unit/test_unit_outdated.py b/tests/unit/test_unit_outdated.py index 31943d093d7..fd8b4cdd9b9 100644 --- a/tests/unit/test_unit_outdated.py +++ b/tests/unit/test_unit_outdated.py @@ -136,8 +136,10 @@ def test_pip_version_check(monkeypatch, stored_time, installed_ver, new_ver, assert len(outdated.logger.warning.calls) == 0 -def _get_statefile_path(cache_dir): - return os.path.join(cache_dir, outdated._STATEFILE_NAME) +def _get_statefile_path(cache_dir, key): + return os.path.join( + cache_dir, outdated._get_statefile_name(key) + ) def test_self_check_state(monkeypatch, tmpdir): @@ -166,12 +168,13 @@ def fake_lock(filename): monkeypatch.setattr(os.path, "exists", lambda p: True) cache_dir = tmpdir / 'cache_dir' - monkeypatch.setattr(sys, 'prefix', tmpdir / 'pip_prefix') + key = 'pip_prefix' + monkeypatch.setattr(sys, 'prefix', key) state = outdated.SelfCheckState(cache_dir=cache_dir) state.save('2.0', datetime.datetime.utcnow()) - expected_path = _get_statefile_path(str(cache_dir)) + expected_path = _get_statefile_path(str(cache_dir), key) assert fake_lock.calls == [pretend.call(expected_path)] assert fake_open.calls == [ @@ -203,7 +206,7 @@ def test_self_check_state_reads_expected_statefile(monkeypatch, tmpdir): cache_dir = tmpdir / "cache_dir" cache_dir.mkdir() key = "helloworld" - statefile_path = _get_statefile_path(str(cache_dir)) + statefile_path = _get_statefile_path(str(cache_dir), key) last_check = "1970-01-02T11:00:00Z" pypi_version = "1.0" @@ -228,7 +231,7 @@ def test_self_check_state_writes_expected_statefile(monkeypatch, tmpdir): cache_dir = tmpdir / "cache_dir" cache_dir.mkdir() key = "helloworld" - statefile_path = _get_statefile_path(str(cache_dir)) + statefile_path = _get_statefile_path(str(cache_dir), key) last_check = datetime.datetime.strptime( "1970-01-02T11:00:00Z", outdated.SELFCHECK_DATE_FMT From c6c2ee433ba1d5b1871cd1701042aa8f399e146a Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Tue, 13 Aug 2019 21:49:57 -0400 Subject: [PATCH 0107/3170] Add test for specific state file names. --- tests/unit/test_unit_outdated.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/unit/test_unit_outdated.py b/tests/unit/test_unit_outdated.py index fd8b4cdd9b9..6a7c6a0e048 100644 --- a/tests/unit/test_unit_outdated.py +++ b/tests/unit/test_unit_outdated.py @@ -136,6 +136,25 @@ def test_pip_version_check(monkeypatch, stored_time, installed_ver, new_ver, assert len(outdated.logger.warning.calls) == 0 +statefile_name_case_1 = ( + "selfcheck-" + "d0d922be2c876108df5bd95254ebf2b9228716063584a623cadcc72159364474.json" +) + +statefile_name_case_2 = ( + "selfcheck-" + "37d748d2f9a7d61c07aa598962da9a6a620b6b2203038952062471fbf22762ec.json" +) + + +@pytest.mark.parametrize("key,expected", [ + ("/hello/world/venv", statefile_name_case_1), + ("C:\\Users\\User\\Desktop\\venv", statefile_name_case_2), +]) +def test_get_statefile_name_known_values(key, expected): + assert expected == outdated._get_statefile_name(key) + + def _get_statefile_path(cache_dir, key): return os.path.join( cache_dir, outdated._get_statefile_name(key) From 9928409b2f734323b430f2d40245e93a4fbb8e04 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Tue, 13 Aug 2019 21:33:43 -0400 Subject: [PATCH 0108/3170] Do not read selfcheck state file when saving. --- src/pip/_internal/utils/outdated.py | 16 +++++++--------- tests/unit/test_unit_outdated.py | 1 - 2 files changed, 7 insertions(+), 10 deletions(-) diff --git a/src/pip/_internal/utils/outdated.py b/src/pip/_internal/utils/outdated.py index e1f9129d108..2525a795584 100644 --- a/src/pip/_internal/utils/outdated.py +++ b/src/pip/_internal/utils/outdated.py @@ -78,15 +78,13 @@ def save(self, pypi_version, current_time): # Attempt to write out our version check file with lockfile.LockFile(self.statefile_path): - if os.path.exists(self.statefile_path): - with open(self.statefile_path) as statefile: - state = json.load(statefile) - else: - state = {} - - state[self.key] = { - "last_check": current_time.strftime(SELFCHECK_DATE_FMT), - "pypi_version": pypi_version, + # Since we have a prefix-specific state file, we can just + # overwrite whatever is there, no need to check. + state = { + self.key: { + "last_check": current_time.strftime(SELFCHECK_DATE_FMT), + "pypi_version": pypi_version, + }, } with open(self.statefile_path, "w") as statefile: diff --git a/tests/unit/test_unit_outdated.py b/tests/unit/test_unit_outdated.py index 6a7c6a0e048..04c64140897 100644 --- a/tests/unit/test_unit_outdated.py +++ b/tests/unit/test_unit_outdated.py @@ -197,7 +197,6 @@ def fake_lock(filename): assert fake_lock.calls == [pretend.call(expected_path)] assert fake_open.calls == [ - pretend.call(expected_path), pretend.call(expected_path), pretend.call(expected_path, 'w'), ] From d81a95a0952fadf449c3329a2efc96833396436c Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Tue, 13 Aug 2019 21:38:39 -0400 Subject: [PATCH 0109/3170] Simplify selfcheck state file structure. Since we are in a prefix-specific state file, we no longer need to have a top-level map keyed by prefix and can have a simple flat structure. --- src/pip/_internal/utils/outdated.py | 11 ++++++----- tests/unit/test_unit_outdated.py | 18 ++++++++---------- 2 files changed, 14 insertions(+), 15 deletions(-) diff --git a/src/pip/_internal/utils/outdated.py b/src/pip/_internal/utils/outdated.py index 2525a795584..5797c71ee2e 100644 --- a/src/pip/_internal/utils/outdated.py +++ b/src/pip/_internal/utils/outdated.py @@ -52,7 +52,7 @@ def __init__(self, cache_dir): ) try: with open(self.statefile_path) as statefile: - self.state = json.load(statefile)[self.key] + self.state = json.load(statefile) except (IOError, ValueError, KeyError): # Explicitly suppressing exceptions, since we don't want to # error out if the cache file is invalid. @@ -81,10 +81,11 @@ def save(self, pypi_version, current_time): # Since we have a prefix-specific state file, we can just # overwrite whatever is there, no need to check. state = { - self.key: { - "last_check": current_time.strftime(SELFCHECK_DATE_FMT), - "pypi_version": pypi_version, - }, + # Include the key so it's easy to tell which pip wrote the + # file. + "key": self.key, + "last_check": current_time.strftime(SELFCHECK_DATE_FMT), + "pypi_version": pypi_version, } with open(self.statefile_path, "w") as statefile: diff --git a/tests/unit/test_unit_outdated.py b/tests/unit/test_unit_outdated.py index 04c64140897..8946ee5b189 100644 --- a/tests/unit/test_unit_outdated.py +++ b/tests/unit/test_unit_outdated.py @@ -162,8 +162,8 @@ def _get_statefile_path(cache_dir, key): def test_self_check_state(monkeypatch, tmpdir): - CONTENT = '''{"pip_prefix": {"last_check": "1970-01-02T11:00:00Z", - "pypi_version": "1.0"}}''' + CONTENT = '''{"key": "pip_prefix", "last_check": "1970-01-02T11:00:00Z", + "pypi_version": "1.0"}''' fake_file = pretend.stub( read=pretend.call_recorder(lambda: CONTENT), write=pretend.call_recorder(lambda s: None), @@ -229,10 +229,9 @@ def test_self_check_state_reads_expected_statefile(monkeypatch, tmpdir): last_check = "1970-01-02T11:00:00Z" pypi_version = "1.0" content = { - key: { - "last_check": last_check, - "pypi_version": pypi_version, - }, + "key": key, + "last_check": last_check, + "pypi_version": pypi_version, } with open(statefile_path, "w") as f: @@ -264,9 +263,8 @@ def test_self_check_state_writes_expected_statefile(monkeypatch, tmpdir): saved = json.load(f) expected = { - key: { - "last_check": last_check.strftime(outdated.SELFCHECK_DATE_FMT), - "pypi_version": pypi_version, - }, + "key": key, + "last_check": last_check.strftime(outdated.SELFCHECK_DATE_FMT), + "pypi_version": pypi_version, } assert expected == saved From c91af0dadeaa3a2fe7b03c401e8161053552f0eb Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Tue, 13 Aug 2019 21:43:20 -0400 Subject: [PATCH 0110/3170] Move selfcheck state file content creation outside lock. --- src/pip/_internal/utils/outdated.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/pip/_internal/utils/outdated.py b/src/pip/_internal/utils/outdated.py index 5797c71ee2e..927bf044953 100644 --- a/src/pip/_internal/utils/outdated.py +++ b/src/pip/_internal/utils/outdated.py @@ -76,21 +76,22 @@ def save(self, pypi_version, current_time): # ahead and make sure that all our directories are created. ensure_dir(os.path.dirname(self.statefile_path)) + state = { + # Include the key so it's easy to tell which pip wrote the + # file. + "key": self.key, + "last_check": current_time.strftime(SELFCHECK_DATE_FMT), + "pypi_version": pypi_version, + } + + text = json.dumps(state, sort_keys=True, separators=(",", ":")) + # Attempt to write out our version check file with lockfile.LockFile(self.statefile_path): # Since we have a prefix-specific state file, we can just # overwrite whatever is there, no need to check. - state = { - # Include the key so it's easy to tell which pip wrote the - # file. - "key": self.key, - "last_check": current_time.strftime(SELFCHECK_DATE_FMT), - "pypi_version": pypi_version, - } - with open(self.statefile_path, "w") as statefile: - json.dump(state, statefile, sort_keys=True, - separators=(",", ":")) + statefile.write(text) def was_installed_by_pip(pkg): From 9a317d3b209a283fac36f2d0dcf41a4565eececb Mon Sep 17 00:00:00 2001 From: Albert Tugushev Date: Mon, 12 Aug 2019 11:34:21 +0300 Subject: [PATCH 0111/3170] Fix bypassed pip upgrade warning on Windows --- news/6841.bugfix | 1 + src/pip/_internal/utils/misc.py | 1 + tests/functional/test_install.py | 92 ++++++++++++++++++++++++++++++++ 3 files changed, 94 insertions(+) create mode 100644 news/6841.bugfix diff --git a/news/6841.bugfix b/news/6841.bugfix new file mode 100644 index 00000000000..278caa64e54 --- /dev/null +++ b/news/6841.bugfix @@ -0,0 +1 @@ +Fix bypassed pip upgrade warning on Windows. diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 576a25138ed..15eb01dc1de 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -1185,6 +1185,7 @@ def protect_pip_from_modification_on_windows(modifying_pip): python -m pip ... """ pip_names = [ + "pip", "pip.exe", "pip{}.exe".format(sys.version_info[0]), "pip{}.{}.exe".format(*sys.version_info[:2]) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 6e1e3dd400b..ba36a2844bf 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -7,6 +7,7 @@ import pytest +import pip from pip._internal import pep425tags from pip._internal.cli.status_codes import ERROR, SUCCESS from pip._internal.models.index import PyPI, TestPyPI @@ -1527,3 +1528,94 @@ def test_target_install_ignores_distutils_config_install_prefix(script): (target - script.base_path) in result.files_created and (prefix - script.base_path) not in result.files_created ), str(result) + + +@pytest.mark.skipif("sys.platform != 'win32'") +def test_protect_pip_from_modification_on_windows(script): + """ + Test ``pip install --upgrade pip`` is raised an error on Windows. + """ + command = ['pip', 'install', '--upgrade', 'pip'] + result = script.run(*command, expect_error=True) + assert result.returncode != 0 + new_command = [sys.executable, '-m'] + command + assert 'To modify pip, please run the following command:\n{}'.format( + ' '.join(new_command)) in result.stderr, str(result) + + +@pytest.mark.skipif("sys.platform != 'win32'") +def test_protect_pip_from_modification_via_deps_on_windows(script, with_wheel): + """ + Test ``pip install pkga`` is raised and error on Windows + if `pkga` implicitly tries to upgrade pip. + """ + # Make a wheel for pkga which requires pip + script.scratch_path.joinpath('pkga').mkdir() + pkga_path = script.scratch_path / 'pkga' + pkga_path.joinpath('setup.py').write_text(textwrap.dedent(""" + from setuptools import setup + setup(name='pkga', + version='0.1', + install_requires = ["pip<{}"]) + """.format(pip.__version__))) + result = script.run( + 'python', 'setup.py', 'bdist_wheel', '--universal', cwd=pkga_path + ) + assert result.returncode == 0 + + # Make sure pip install pkga is raised error + pkga_wheel_path = './pkga/dist/pkga-0.1-py2.py3-none-any.whl' + command = ['pip', 'install', pkga_wheel_path] + result = script.run(*command, expect_error=True) + assert result.returncode != 0 + new_command = [sys.executable, "-m"] + command + assert "To modify pip, please run the following command:\n{}".format( + " ".join(new_command)) in result.stderr, str(result) + + +@pytest.mark.skipif("sys.platform != 'win32'") +def test_protect_pip_from_modification_via_sub_deps_on_windows( + script, with_wheel +): + """ + Test ``pip install pkga`` is raised and error on Windows + if sub-dependencies of `pkga` implicitly tries to upgrade pip. + """ + # Make a wheel for pkga which requires pip + script.scratch_path.joinpath('pkga').mkdir() + pkga_path = script.scratch_path / 'pkga' + pkga_path.joinpath('setup.py').write_text(textwrap.dedent(""" + from setuptools import setup + setup(name='pkga', + version='0.1', + install_requires = ["pip<{}"]) + """.format(pip.__version__))) + result = script.run( + 'python', 'setup.py', 'bdist_wheel', '--universal', cwd=pkga_path + ) + assert result.returncode == 0 + + # Make a wheel for pkgb which requires pkgb + script.scratch_path.joinpath('pkgb').mkdir() + pkgb_path = script.scratch_path / 'pkgb' + pkgb_path.joinpath('setup.py').write_text(textwrap.dedent(""" + from setuptools import setup + setup(name='pkgb', + version='0.1', + install_requires = ["pkga"]) + """)) + result = script.run( + 'python', 'setup.py', 'bdist_wheel', '--universal', cwd=pkgb_path + ) + assert result.returncode == 0 + + # Make sure pip install pkgb is raised error + pkgb_wheel_path = './pkgb/dist/pkgb-0.1-py2.py3-none-any.whl' + command = [ + 'pip', 'install', pkgb_wheel_path, '--find-links', pkga_path / 'dist' + ] + result = script.run(*command, expect_error=True) + assert result.returncode != 0 + new_command = [sys.executable, '-m'] + command + assert 'To modify pip, please run the following command:\n{}'.format( + ' '.join(new_command)) in result.stderr, str(result) From a1fa84e57e6a15033a173ef21c0bfe13f193a9f2 Mon Sep 17 00:00:00 2001 From: Albert Tugushev Date: Tue, 13 Aug 2019 01:34:35 +0300 Subject: [PATCH 0112/3170] Address review comments Handle the case of invoking pip via pip3 and pip3.7 --- src/pip/_internal/utils/misc.py | 11 +++++------ tests/functional/test_install.py | 14 +++++++++++--- 2 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 15eb01dc1de..5d359f62cbe 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -1184,12 +1184,11 @@ def protect_pip_from_modification_on_windows(modifying_pip): On Windows, any operation modifying pip should be run as: python -m pip ... """ - pip_names = [ - "pip", - "pip.exe", - "pip{}.exe".format(sys.version_info[0]), - "pip{}.{}.exe".format(*sys.version_info[:2]) - ] + pip_names = set() + for ext in ('', '.exe'): + pip_names.add('pip{0}'.format(ext)) + pip_names.add('pip{0}{1}'.format(sys.version_info[0], ext)) + pip_names.add('pip{0}.{1}{ext}'.format(*sys.version_info[:2], ext=ext)) # See https://github.com/pypa/pip/issues/1299 for more discussion should_show_use_python_msg = ( diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index ba36a2844bf..c0266067eef 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1531,14 +1531,22 @@ def test_target_install_ignores_distutils_config_install_prefix(script): @pytest.mark.skipif("sys.platform != 'win32'") -def test_protect_pip_from_modification_on_windows(script): +@pytest.mark.parametrize('pip_name', [ + 'pip', + 'pip{}'.format(sys.version_info[0]), + 'pip{}.{}'.format(*sys.version_info[:2]), + 'pip.exe', + 'pip{}.exe'.format(sys.version_info[0]), + 'pip{}.{}.exe'.format(*sys.version_info[:2]) +]) +def test_protect_pip_from_modification_on_windows(script, pip_name): """ Test ``pip install --upgrade pip`` is raised an error on Windows. """ - command = ['pip', 'install', '--upgrade', 'pip'] + command = [pip_name, 'install', '--upgrade', 'pip'] result = script.run(*command, expect_error=True) assert result.returncode != 0 - new_command = [sys.executable, '-m'] + command + new_command = [sys.executable, '-m', 'pip'] + command[1:] assert 'To modify pip, please run the following command:\n{}'.format( ' '.join(new_command)) in result.stderr, str(result) From 94979001e091869112f8d1e5f88675bf484300c3 Mon Sep 17 00:00:00 2001 From: Albert Tugushev Date: Tue, 13 Aug 2019 12:46:18 +0300 Subject: [PATCH 0113/3170] Address review comments Fix typos and remove unneeded assertions. --- tests/functional/test_install.py | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index c0266067eef..ce2ecf5eaad 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1554,7 +1554,7 @@ def test_protect_pip_from_modification_on_windows(script, pip_name): @pytest.mark.skipif("sys.platform != 'win32'") def test_protect_pip_from_modification_via_deps_on_windows(script, with_wheel): """ - Test ``pip install pkga`` is raised and error on Windows + Test ``pip install pkga`` is raised an error on Windows if `pkga` implicitly tries to upgrade pip. """ # Make a wheel for pkga which requires pip @@ -1566,10 +1566,9 @@ def test_protect_pip_from_modification_via_deps_on_windows(script, with_wheel): version='0.1', install_requires = ["pip<{}"]) """.format(pip.__version__))) - result = script.run( + script.run( 'python', 'setup.py', 'bdist_wheel', '--universal', cwd=pkga_path ) - assert result.returncode == 0 # Make sure pip install pkga is raised error pkga_wheel_path = './pkga/dist/pkga-0.1-py2.py3-none-any.whl' @@ -1586,8 +1585,8 @@ def test_protect_pip_from_modification_via_sub_deps_on_windows( script, with_wheel ): """ - Test ``pip install pkga`` is raised and error on Windows - if sub-dependencies of `pkga` implicitly tries to upgrade pip. + Test ``pip install pkg`` is raised an error on Windows + if sub-dependencies of `pkg` implicitly tries to upgrade pip. """ # Make a wheel for pkga which requires pip script.scratch_path.joinpath('pkga').mkdir() @@ -1598,12 +1597,11 @@ def test_protect_pip_from_modification_via_sub_deps_on_windows( version='0.1', install_requires = ["pip<{}"]) """.format(pip.__version__))) - result = script.run( + script.run( 'python', 'setup.py', 'bdist_wheel', '--universal', cwd=pkga_path ) - assert result.returncode == 0 - # Make a wheel for pkgb which requires pkgb + # Make a wheel for pkgb which requires pkga script.scratch_path.joinpath('pkgb').mkdir() pkgb_path = script.scratch_path / 'pkgb' pkgb_path.joinpath('setup.py').write_text(textwrap.dedent(""" @@ -1612,12 +1610,11 @@ def test_protect_pip_from_modification_via_sub_deps_on_windows( version='0.1', install_requires = ["pkga"]) """)) - result = script.run( + script.run( 'python', 'setup.py', 'bdist_wheel', '--universal', cwd=pkgb_path ) - assert result.returncode == 0 - # Make sure pip install pkgb is raised error + # Make sure pip install pkgb is raised an error pkgb_wheel_path = './pkgb/dist/pkgb-0.1-py2.py3-none-any.whl' command = [ 'pip', 'install', pkgb_wheel_path, '--find-links', pkga_path / 'dist' From 173761c03070db2795349f61b7c0229eb666b3ef Mon Sep 17 00:00:00 2001 From: Albert Tugushev Date: Wed, 14 Aug 2019 11:23:38 +0300 Subject: [PATCH 0114/3170] Address review comments Rebased to the latest master Remove unneeded assertions Use create_basic_wheel_for_package Couple other nitpicks --- src/pip/_internal/utils/misc.py | 6 +- tests/functional/test_install.py | 100 ++++++++++++++----------------- 2 files changed, 47 insertions(+), 59 deletions(-) diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 5d359f62cbe..2a6735b7f4a 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -1186,9 +1186,9 @@ def protect_pip_from_modification_on_windows(modifying_pip): """ pip_names = set() for ext in ('', '.exe'): - pip_names.add('pip{0}'.format(ext)) - pip_names.add('pip{0}{1}'.format(sys.version_info[0], ext)) - pip_names.add('pip{0}.{1}{ext}'.format(*sys.version_info[:2], ext=ext)) + pip_names.add('pip{ext}'.format(ext=ext)) + pip_names.add('pip{}{ext}'.format(sys.version_info[0], ext=ext)) + pip_names.add('pip{}.{}{ext}'.format(*sys.version_info[:2], ext=ext)) # See https://github.com/pypa/pip/issues/1299 for more discussion should_show_use_python_msg = ( diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index ce2ecf5eaad..ac8aad9a2ae 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -7,7 +7,7 @@ import pytest -import pip +from pip import __version__ as pip_current_version from pip._internal import pep425tags from pip._internal.cli.status_codes import ERROR, SUCCESS from pip._internal.models.index import PyPI, TestPyPI @@ -1530,6 +1530,7 @@ def test_target_install_ignores_distutils_config_install_prefix(script): ), str(result) +@pytest.mark.network @pytest.mark.skipif("sys.platform != 'win32'") @pytest.mark.parametrize('pip_name', [ 'pip', @@ -1541,86 +1542,73 @@ def test_target_install_ignores_distutils_config_install_prefix(script): ]) def test_protect_pip_from_modification_on_windows(script, pip_name): """ - Test ``pip install --upgrade pip`` is raised an error on Windows. + Test that pip modification command using ``pip install ...`` + raises an error on Windows. """ - command = [pip_name, 'install', '--upgrade', 'pip'] + command = [pip_name, 'install', 'pip != {}'.format(pip_current_version)] result = script.run(*command, expect_error=True) - assert result.returncode != 0 new_command = [sys.executable, '-m', 'pip'] + command[1:] - assert 'To modify pip, please run the following command:\n{}'.format( - ' '.join(new_command)) in result.stderr, str(result) + expected_message = ( + 'To modify pip, please run the following command:\n{}' + .format(' '.join(new_command)) + ) + assert expected_message in result.stderr, str(result) +@pytest.mark.network @pytest.mark.skipif("sys.platform != 'win32'") -def test_protect_pip_from_modification_via_deps_on_windows(script, with_wheel): +def test_protect_pip_from_modification_via_deps_on_windows(script): """ Test ``pip install pkga`` is raised an error on Windows if `pkga` implicitly tries to upgrade pip. """ - # Make a wheel for pkga which requires pip - script.scratch_path.joinpath('pkga').mkdir() - pkga_path = script.scratch_path / 'pkga' - pkga_path.joinpath('setup.py').write_text(textwrap.dedent(""" - from setuptools import setup - setup(name='pkga', - version='0.1', - install_requires = ["pip<{}"]) - """.format(pip.__version__))) - script.run( - 'python', 'setup.py', 'bdist_wheel', '--universal', cwd=pkga_path + pkga_wheel_path = create_basic_wheel_for_package( + script, + 'pkga', '0.1', + depends=['pip != {}'.format(pip_current_version)], ) - # Make sure pip install pkga is raised error - pkga_wheel_path = './pkga/dist/pkga-0.1-py2.py3-none-any.whl' + # Make sure pip install pkga is raised an error command = ['pip', 'install', pkga_wheel_path] result = script.run(*command, expect_error=True) - assert result.returncode != 0 new_command = [sys.executable, "-m"] + command - assert "To modify pip, please run the following command:\n{}".format( - " ".join(new_command)) in result.stderr, str(result) + expected_message = ( + 'To modify pip, please run the following command:\n{}' + .format(' '.join(new_command)) + ) + assert expected_message in result.stderr, str(result) +@pytest.mark.network @pytest.mark.skipif("sys.platform != 'win32'") -def test_protect_pip_from_modification_via_sub_deps_on_windows( - script, with_wheel -): +def test_protect_pip_from_modification_via_sub_deps_on_windows(script): """ - Test ``pip install pkg`` is raised an error on Windows - if sub-dependencies of `pkg` implicitly tries to upgrade pip. + Test ``pip install pkga`` is raised an error on Windows + if sub-dependencies of `pkga` implicitly tries to upgrade pip. """ - # Make a wheel for pkga which requires pip - script.scratch_path.joinpath('pkga').mkdir() - pkga_path = script.scratch_path / 'pkga' - pkga_path.joinpath('setup.py').write_text(textwrap.dedent(""" - from setuptools import setup - setup(name='pkga', - version='0.1', - install_requires = ["pip<{}"]) - """.format(pip.__version__))) - script.run( - 'python', 'setup.py', 'bdist_wheel', '--universal', cwd=pkga_path + # Make a wheel for pkga which requires pkgb + pkga_wheel_path = create_basic_wheel_for_package( + script, + 'pkga', '0.1', + depends=['pkgb'], ) - # Make a wheel for pkgb which requires pkga - script.scratch_path.joinpath('pkgb').mkdir() - pkgb_path = script.scratch_path / 'pkgb' - pkgb_path.joinpath('setup.py').write_text(textwrap.dedent(""" - from setuptools import setup - setup(name='pkgb', - version='0.1', - install_requires = ["pkga"]) - """)) - script.run( - 'python', 'setup.py', 'bdist_wheel', '--universal', cwd=pkgb_path + # Make a wheel for pkgb which requires pip + pkgb_wheel_path = create_basic_wheel_for_package( + script, + 'pkgb', '0.1', + depends=['pip != {}'.format(pip_current_version)], ) - # Make sure pip install pkgb is raised an error - pkgb_wheel_path = './pkgb/dist/pkgb-0.1-py2.py3-none-any.whl' + # Make sure pip install pkga is raised an error command = [ - 'pip', 'install', pkgb_wheel_path, '--find-links', pkga_path / 'dist' + 'pip', 'install', pkga_wheel_path, + '--find-links', pkgb_wheel_path.parent, ] result = script.run(*command, expect_error=True) - assert result.returncode != 0 new_command = [sys.executable, '-m'] + command - assert 'To modify pip, please run the following command:\n{}'.format( - ' '.join(new_command)) in result.stderr, str(result) + expected_message = ( + 'To modify pip, please run the following command:\n{}' + .format(' '.join(new_command)) + ) + assert expected_message in result.stderr, str(result) From ea517a2bb9be824da0bd5508dc7598928d13d92d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= Date: Tue, 13 Aug 2019 14:14:23 +0200 Subject: [PATCH 0115/3170] clarify WheelBuilder.build() a bit --- news/6869.trivial | 1 + src/pip/_internal/commands/install.py | 4 +-- src/pip/_internal/wheel.py | 42 ++++++++++++++++++--------- tests/unit/test_command_install.py | 6 ++-- tests/unit/test_wheel.py | 14 +++++---- 5 files changed, 43 insertions(+), 24 deletions(-) create mode 100644 news/6869.trivial diff --git a/news/6869.trivial b/news/6869.trivial new file mode 100644 index 00000000000..1da3453fb4f --- /dev/null +++ b/news/6869.trivial @@ -0,0 +1 @@ +Clarify WheelBuilder.build() a bit \ No newline at end of file diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 46e667433ea..673c6d21484 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -78,7 +78,7 @@ def build_wheels( # Always build PEP 517 requirements build_failures = builder.build( pep517_requirements, - autobuilding=True, + should_unpack=True, ) if should_build_legacy: @@ -87,7 +87,7 @@ def build_wheels( # install for those. builder.build( legacy_requirements, - autobuilding=True, + should_unpack=True, ) return build_failures diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index 71109e2cd33..cd0d3460f9e 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -774,7 +774,7 @@ def _contains_egg_info( def should_use_ephemeral_cache( req, # type: InstallRequirement format_control, # type: FormatControl - autobuilding, # type: bool + should_unpack, # type: bool cache_available # type: bool ): # type: (...) -> Optional[bool] @@ -783,20 +783,25 @@ def should_use_ephemeral_cache( ephemeral cache. :param cache_available: whether a cache directory is available for the - autobuilding=True case. + should_unpack=True case. :return: True or False to build the requirement with ephem_cache=True or False, respectively; or None not to build the requirement. """ if req.constraint: + # never build requirements that are merely constraints return None if req.is_wheel: - if not autobuilding: + if not should_unpack: logger.info( 'Skipping %s, due to already being wheel.', req.name, ) return None - if not autobuilding: + if not should_unpack: + # i.e. pip wheel, not pip install; + # return False, knowing that the caller will never cache + # in this case anyway, so this return merely means "build it". + # TODO improve this behavior return False if req.editable or not req.source_dir: @@ -1031,23 +1036,34 @@ def _clean_one(self, req): def build( self, requirements, # type: Iterable[InstallRequirement] - autobuilding=False # type: bool + should_unpack=False # type: bool ): # type: (...) -> List[InstallRequirement] """Build wheels. - :param unpack: If True, replace the sdist we built from with the - newly built wheel, in preparation for installation. + :param should_unpack: If True, after building the wheel, unpack it + and replace the sdist with the unpacked version in preparation + for installation. :return: True if all the wheels built correctly. """ + # pip install uses should_unpack=True. + # pip install never provides a _wheel_dir. + # pip wheel uses should_unpack=False. + # pip wheel always provides a _wheel_dir (via the preparer). + assert ( + (should_unpack and not self._wheel_dir) or + (not should_unpack and self._wheel_dir) + ) + buildset = [] format_control = self.finder.format_control - # Whether a cache directory is available for autobuilding=True. - cache_available = bool(self._wheel_dir or self.wheel_cache.cache_dir) + cache_available = bool(self.wheel_cache.cache_dir) for req in requirements: ephem_cache = should_use_ephemeral_cache( - req, format_control=format_control, autobuilding=autobuilding, + req, + format_control=format_control, + should_unpack=should_unpack, cache_available=cache_available, ) if ephem_cache is None: @@ -1061,7 +1077,7 @@ def build( # Is any wheel build not using the ephemeral cache? if any(not ephem_cache for _, ephem_cache in buildset): have_directory_for_build = self._wheel_dir or ( - autobuilding and self.wheel_cache.cache_dir + should_unpack and self.wheel_cache.cache_dir ) assert have_directory_for_build @@ -1078,7 +1094,7 @@ def build( build_success, build_failure = [], [] for req, ephem in buildset: python_tag = None - if autobuilding: + if should_unpack: python_tag = pep425tags.implementation_tag if ephem: output_dir = _cache.get_ephem_path_for_link(req.link) @@ -1099,7 +1115,7 @@ def build( ) if wheel_file: build_success.append(req) - if autobuilding: + if should_unpack: # XXX: This is mildly duplicative with prepare_files, # but not close enough to pull out to a single common # method. diff --git a/tests/unit/test_command_install.py b/tests/unit/test_command_install.py index 30469171f6b..1a3fee5c9ae 100644 --- a/tests/unit/test_command_install.py +++ b/tests/unit/test_command_install.py @@ -39,8 +39,8 @@ def test_build_wheels__wheel_installed(self, is_wheel_installed): # Legacy requirements were built. assert mock_calls == [ - call(['a', 'b'], autobuilding=True), - call(['c', 'd'], autobuilding=True), + call(['a', 'b'], should_unpack=True), + call(['c', 'd'], should_unpack=True), ] # Legacy build failures are not included in the return value. @@ -57,7 +57,7 @@ def test_build_wheels__wheel_not_installed(self, is_wheel_installed): # Legacy requirements were not built. assert mock_calls == [ - call(['a', 'b'], autobuilding=True), + call(['a', 'b'], should_unpack=True), ] assert build_failures == ['a'] diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index ea4fe4ebaf9..9775d6f317e 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -78,10 +78,10 @@ def test_format_tag(file_tag, expected): @pytest.mark.parametrize( - "base_name, autobuilding, cache_available, expected", + "base_name, should_unpack, cache_available, expected", [ ('pendulum-2.0.4', False, False, False), - # The following cases test autobuilding=True. + # The following cases test should_unpack=True. # Test _contains_egg_info() returning True. ('pendulum-2.0.4', True, True, False), ('pendulum-2.0.4', True, False, True), @@ -91,7 +91,7 @@ def test_format_tag(file_tag, expected): ], ) def test_should_use_ephemeral_cache__issue_6197( - base_name, autobuilding, cache_available, expected, + base_name, should_unpack, cache_available, expected, ): """ Regression test for: https://github.com/pypa/pip/issues/6197 @@ -102,7 +102,7 @@ def test_should_use_ephemeral_cache__issue_6197( format_control = FormatControl() ephem_cache = wheel.should_use_ephemeral_cache( - req, format_control=format_control, autobuilding=autobuilding, + req, format_control=format_control, should_unpack=should_unpack, cache_available=cache_available, ) assert ephem_cache is expected @@ -145,7 +145,7 @@ def test_should_use_ephemeral_cache__disallow_binaries_and_vcs_checkout( # The cache_available value doesn't matter for this test. ephem_cache = wheel.should_use_ephemeral_cache( - req, format_control=format_control, autobuilding=True, + req, format_control=format_control, should_unpack=True, cache_available=True, ) assert ephem_cache is expected @@ -697,7 +697,9 @@ def test_skip_building_wheels(self, caplog): as mock_build_one: wheel_req = Mock(is_wheel=True, editable=False, constraint=False) wb = wheel.WheelBuilder( - finder=Mock(), preparer=Mock(), wheel_cache=None, + finder=Mock(), + preparer=Mock(), + wheel_cache=Mock(cache_dir=None), ) with caplog.at_level(logging.INFO): wb.build([wheel_req]) From 5f4da50adf5751afb81de4929ad1740bce66d5f9 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Wed, 14 Aug 2019 20:48:32 -0400 Subject: [PATCH 0116/3170] Shorten selfcheck filenames and move to cache subdirectory. --- src/pip/_internal/utils/outdated.py | 6 +++--- tests/unit/test_unit_outdated.py | 12 ++++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/utils/outdated.py b/src/pip/_internal/utils/outdated.py index 927bf044953..d0fdd8fd315 100644 --- a/src/pip/_internal/utils/outdated.py +++ b/src/pip/_internal/utils/outdated.py @@ -35,8 +35,8 @@ def _get_statefile_name(key): # type: (Union[str, Text]) -> str key_bytes = ensure_binary(key) - name = hashlib.sha256(key_bytes).hexdigest() - return "selfcheck-{}.json".format(name) + name = hashlib.sha224(key_bytes).hexdigest() + return name class SelfCheckState(object): @@ -48,7 +48,7 @@ def __init__(self, cache_dir): # Try to load the existing state if cache_dir: self.statefile_path = os.path.join( - cache_dir, _get_statefile_name(self.key) + cache_dir, "selfcheck", _get_statefile_name(self.key) ) try: with open(self.statefile_path) as statefile: diff --git a/tests/unit/test_unit_outdated.py b/tests/unit/test_unit_outdated.py index 8946ee5b189..5670ba2ce50 100644 --- a/tests/unit/test_unit_outdated.py +++ b/tests/unit/test_unit_outdated.py @@ -11,6 +11,7 @@ from pip._internal.index import InstallationCandidate from pip._internal.utils import outdated +from tests.lib.path import Path class MockFoundCandidates(object): @@ -137,13 +138,11 @@ def test_pip_version_check(monkeypatch, stored_time, installed_ver, new_ver, statefile_name_case_1 = ( - "selfcheck-" - "d0d922be2c876108df5bd95254ebf2b9228716063584a623cadcc72159364474.json" + "fcd2d5175dd33d5df759ee7b045264230205ef837bf9f582f7c3ada7" ) statefile_name_case_2 = ( - "selfcheck-" - "37d748d2f9a7d61c07aa598962da9a6a620b6b2203038952062471fbf22762ec.json" + "902cecc0745b8ecf2509ba473f3556f0ba222fedc6df433acda24aa5" ) @@ -157,7 +156,7 @@ def test_get_statefile_name_known_values(key, expected): def _get_statefile_path(cache_dir, key): return os.path.join( - cache_dir, outdated._get_statefile_name(key) + cache_dir, "selfcheck", outdated._get_statefile_name(key) ) @@ -184,7 +183,6 @@ def fake_lock(filename): monkeypatch.setattr(outdated, "check_path_owner", lambda p: True) monkeypatch.setattr(lockfile, 'LockFile', fake_lock) - monkeypatch.setattr(os.path, "exists", lambda p: True) cache_dir = tmpdir / 'cache_dir' key = 'pip_prefix' @@ -234,6 +232,8 @@ def test_self_check_state_reads_expected_statefile(monkeypatch, tmpdir): "pypi_version": pypi_version, } + Path(statefile_path).parent.mkdir() + with open(statefile_path, "w") as f: json.dump(content, f) From 3f9136f885d6559959f43a76c52f1e1667deef87 Mon Sep 17 00:00:00 2001 From: Frost Ming Date: Sun, 14 Jul 2019 17:00:05 +0800 Subject: [PATCH 0117/3170] Make trusted host w/o port work for HTTPS --- news/6705.bugfix | 1 + src/pip/_internal/download.py | 10 +++++++++- src/pip/_internal/utils/misc.py | 21 +++++++++++++++++++++ tests/unit/test_download.py | 4 +++- tests/unit/test_utils.py | 25 +++++++++++++++++++++++++ 5 files changed, 59 insertions(+), 2 deletions(-) create mode 100644 news/6705.bugfix diff --git a/news/6705.bugfix b/news/6705.bugfix new file mode 100644 index 00000000000..84f100ce450 --- /dev/null +++ b/news/6705.bugfix @@ -0,0 +1 @@ +Fix --trusted-host processing under HTTPS to trust any port number used with the host. diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index 3ad5eb31a04..0c6492a8d78 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -43,10 +43,12 @@ ask_password, ask_path_exists, backup_dir, + build_url_from_netloc, consume, display_path, format_size, get_installed_version, + netloc_has_port, path_to_url, remove_auth_from_url, rmtree, @@ -608,7 +610,13 @@ def __init__(self, *args, **kwargs): def add_insecure_host(self, host): # type: (str) -> None - self.mount('https://{}/'.format(host), self._insecure_adapter) + self.mount(build_url_from_netloc(host) + '/', self._insecure_adapter) + if not netloc_has_port(host): + # Mount wildcard ports for the same host. + self.mount( + build_url_from_netloc(host) + ':', + self._insecure_adapter + ) def request(self, method, url, *args, **kwargs): # Allow setting a default timeout on a session diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 576a25138ed..dbad394acaa 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -1081,6 +1081,27 @@ def path_to_url(path): return url +def build_url_from_netloc(netloc, scheme='https'): + # type: (str, str) -> str + """ + Build a full URL from a netloc. + """ + if netloc.count(':') >= 2 and '@' not in netloc and '[' not in netloc: + # It must be a bare IPv6 address, then wrap it with brackets. + netloc = '[{}]'.format(netloc) + return '{}://{}'.format(scheme, netloc) + + +def netloc_has_port(netloc): + # type: (str) -> bool + """ + Return whether the netloc has a port part. + """ + url = build_url_from_netloc(netloc) + parsed = urllib_parse.urlparse(url) + return bool(parsed.port) + + def split_auth_from_netloc(netloc): """ Parse out and remove the auth information from a netloc. diff --git a/tests/unit/test_download.py b/tests/unit/test_download.py index c0712b9a45e..562dfc067a1 100644 --- a/tests/unit/test_download.py +++ b/tests/unit/test_download.py @@ -527,13 +527,15 @@ def test_http_cache_is_not_enabled(self, tmpdir): assert not hasattr(session.adapters["http://"], "cache") - def test_insecure_host_cache_is_not_enabled(self, tmpdir): + def test_insecure_host_adapter(self, tmpdir): session = PipSession( cache=tmpdir.joinpath("test-cache"), insecure_hosts=["example.com"], ) assert not hasattr(session.adapters["https://example.com/"], "cache") + assert "https://example.com/" in session.adapters + assert "https://example.com:" in session.adapters @pytest.mark.parametrize(["input_url", "url", "username", "password"], [ diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 31d2b383cca..97ee36265d1 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -37,6 +37,7 @@ ) from pip._internal.utils.hashes import Hashes, MissingHashes from pip._internal.utils.misc import ( + build_url_from_netloc, call_subprocess, egg_link_path, ensure_dir, @@ -44,6 +45,7 @@ get_installed_distributions, get_prog, make_subprocess_output_error, + netloc_has_port, normalize_path, normalize_version_info, path_to_display, @@ -1223,6 +1225,29 @@ def test_path_to_url_win(): assert path_to_url('file') == 'file:' + urllib_request.pathname2url(path) +@pytest.mark.parametrize('netloc, url, has_port', [ + # Test domain name. + ('example.com', 'https://example.com', False), + ('example.com:5000', 'https://example.com:5000', True), + # Test IPv4 address. + ('127.0.0.1', 'https://127.0.0.1', False), + ('127.0.0.1:5000', 'https://127.0.0.1:5000', True), + # Test bare IPv6 address. + ('2001:DB6::1', 'https://[2001:DB6::1]', False), + # Test IPv6 with port. + ('[2001:DB6::1]:5000', 'https://[2001:DB6::1]:5000', True), + # Test netloc with auth. + ( + 'user:password@localhost:5000', + 'https://user:password@localhost:5000', + True + ) +]) +def test_build_url_from_netloc_and_netloc_has_port(netloc, url, has_port): + assert build_url_from_netloc(netloc) == url + assert netloc_has_port(netloc) is has_port + + @pytest.mark.parametrize('netloc, expected', [ # Test a basic case. ('example.com', ('example.com', (None, None))), From bbae384d5b156b9c9f22c349ed0554a743ae7372 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 14 Aug 2019 22:11:57 -0700 Subject: [PATCH 0118/3170] Make some final tweaks to the PR. --- news/6705.bugfix | 3 ++- src/pip/_internal/utils/misc.py | 2 +- tests/unit/test_download.py | 4 +++- tests/unit/test_utils.py | 10 ++++++---- 4 files changed, 12 insertions(+), 7 deletions(-) diff --git a/news/6705.bugfix b/news/6705.bugfix index 84f100ce450..e8f67ff3868 100644 --- a/news/6705.bugfix +++ b/news/6705.bugfix @@ -1 +1,2 @@ -Fix --trusted-host processing under HTTPS to trust any port number used with the host. +Fix ``--trusted-host`` processing under HTTPS to trust any port number used +with the host. diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index dbad394acaa..9ae5f481c92 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -1087,7 +1087,7 @@ def build_url_from_netloc(netloc, scheme='https'): Build a full URL from a netloc. """ if netloc.count(':') >= 2 and '@' not in netloc and '[' not in netloc: - # It must be a bare IPv6 address, then wrap it with brackets. + # It must be a bare IPv6 address, so wrap it with brackets. netloc = '[{}]'.format(netloc) return '{}://{}'.format(scheme, netloc) diff --git a/tests/unit/test_download.py b/tests/unit/test_download.py index 562dfc067a1..f7595f52bcd 100644 --- a/tests/unit/test_download.py +++ b/tests/unit/test_download.py @@ -533,9 +533,11 @@ def test_insecure_host_adapter(self, tmpdir): insecure_hosts=["example.com"], ) - assert not hasattr(session.adapters["https://example.com/"], "cache") assert "https://example.com/" in session.adapters + # Check that the "port wildcard" is present. assert "https://example.com:" in session.adapters + # Check that the cache isn't enabled. + assert not hasattr(session.adapters["https://example.com/"], "cache") @pytest.mark.parametrize(["input_url", "url", "username", "password"], [ diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 97ee36265d1..8db3384f02b 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -1225,7 +1225,7 @@ def test_path_to_url_win(): assert path_to_url('file') == 'file:' + urllib_request.pathname2url(path) -@pytest.mark.parametrize('netloc, url, has_port', [ +@pytest.mark.parametrize('netloc, expected_url, expected_has_port', [ # Test domain name. ('example.com', 'https://example.com', False), ('example.com:5000', 'https://example.com:5000', True), @@ -1243,9 +1243,11 @@ def test_path_to_url_win(): True ) ]) -def test_build_url_from_netloc_and_netloc_has_port(netloc, url, has_port): - assert build_url_from_netloc(netloc) == url - assert netloc_has_port(netloc) is has_port +def test_build_url_from_netloc_and_netloc_has_port( + netloc, expected_url, expected_has_port, +): + assert build_url_from_netloc(netloc) == expected_url + assert netloc_has_port(netloc) is expected_has_port @pytest.mark.parametrize('netloc, expected', [ From 376f1136d825cda3cb76a7a1bfa415e45e3b2ab2 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Thu, 15 Aug 2019 23:02:40 +0900 Subject: [PATCH 0119/3170] Update marker test to use Python 3.6 instead of 2.6 --- tests/unit/test_req.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index ebd3e8f03bf..fb01bdecc0f 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -602,10 +602,10 @@ def test_parse_editable_local_extras( def test_exclusive_environment_markers(): """Make sure RequirementSet accepts several excluding env markers""" eq26 = install_req_from_line( - "Django>=1.6.10,<1.7 ; python_version == '2.6'") + "Django>=1.6.10,<1.7 ; python_version == '3.6'") eq26.is_direct = True ne26 = install_req_from_line( - "Django>=1.6.10,<1.8 ; python_version != '2.6'") + "Django>=1.6.10,<1.8 ; python_version != '3.6'") ne26.is_direct = True req_set = RequirementSet() From 5d7cc475d4b340487e6bbc9c884c2ff4d0b1ab2b Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Thu, 15 Aug 2019 23:03:29 +0900 Subject: [PATCH 0120/3170] :newspaper: --- news/update-marker-test.trivial | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 news/update-marker-test.trivial diff --git a/news/update-marker-test.trivial b/news/update-marker-test.trivial new file mode 100644 index 00000000000..e69de29bb2d From f9fc6673257c0cb8854e3df03f11c6ec91b5b8a8 Mon Sep 17 00:00:00 2001 From: Albert Tugushev Date: Wed, 14 Aug 2019 18:59:46 +0300 Subject: [PATCH 0121/3170] Addrees review comments Fix typos Use script.pip instead of script.run --- tests/functional/test_install.py | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index ac8aad9a2ae..852f2dde8b3 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1559,7 +1559,7 @@ def test_protect_pip_from_modification_on_windows(script, pip_name): @pytest.mark.skipif("sys.platform != 'win32'") def test_protect_pip_from_modification_via_deps_on_windows(script): """ - Test ``pip install pkga`` is raised an error on Windows + Test ``pip install pkga`` raises an error on Windows if `pkga` implicitly tries to upgrade pip. """ pkga_wheel_path = create_basic_wheel_for_package( @@ -1568,10 +1568,10 @@ def test_protect_pip_from_modification_via_deps_on_windows(script): depends=['pip != {}'.format(pip_current_version)], ) - # Make sure pip install pkga is raised an error - command = ['pip', 'install', pkga_wheel_path] - result = script.run(*command, expect_error=True) - new_command = [sys.executable, "-m"] + command + # Make sure pip install pkga raises an error + args = ['install', pkga_wheel_path] + result = script.pip(*args, expect_error=True, use_module=False) + new_command = [sys.executable, '-m', 'pip'] + args expected_message = ( 'To modify pip, please run the following command:\n{}' .format(' '.join(new_command)) @@ -1583,7 +1583,7 @@ def test_protect_pip_from_modification_via_deps_on_windows(script): @pytest.mark.skipif("sys.platform != 'win32'") def test_protect_pip_from_modification_via_sub_deps_on_windows(script): """ - Test ``pip install pkga`` is raised an error on Windows + Test ``pip install pkga`` raises an error on Windows if sub-dependencies of `pkga` implicitly tries to upgrade pip. """ # Make a wheel for pkga which requires pkgb @@ -1600,13 +1600,12 @@ def test_protect_pip_from_modification_via_sub_deps_on_windows(script): depends=['pip != {}'.format(pip_current_version)], ) - # Make sure pip install pkga is raised an error - command = [ - 'pip', 'install', pkga_wheel_path, - '--find-links', pkgb_wheel_path.parent, + # Make sure pip install pkga raises an error + args = [ + 'install', pkga_wheel_path, '--find-links', pkgb_wheel_path.parent ] - result = script.run(*command, expect_error=True) - new_command = [sys.executable, '-m'] + command + result = script.pip(*args, expect_error=True, use_module=False) + new_command = [sys.executable, '-m', 'pip'] + args expected_message = ( 'To modify pip, please run the following command:\n{}' .format(' '.join(new_command)) From 2e1dfbef76190ee512151cd0d7c18d30e3ebe730 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= Date: Thu, 15 Aug 2019 23:00:55 +0200 Subject: [PATCH 0122/3170] consolidate vcs link detection --- news/6883.trivial | 1 + src/pip/_internal/download.py | 9 ++------- src/pip/_internal/models/link.py | 14 ++++++++------ src/pip/_internal/operations/prepare.py | 3 +-- src/pip/_internal/wheel.py | 2 +- tests/unit/test_link.py | 10 ++++++++++ tests/unit/test_wheel.py | 3 +-- 7 files changed, 24 insertions(+), 18 deletions(-) create mode 100644 news/6883.trivial diff --git a/news/6883.trivial b/news/6883.trivial new file mode 100644 index 00000000000..e6731cdbef6 --- /dev/null +++ b/news/6883.trivial @@ -0,0 +1 @@ +replace is_vcs_url function by is_vcs Link property \ No newline at end of file diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index 0c6492a8d78..159a4433e39 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -76,7 +76,7 @@ __all__ = ['get_file_content', 'is_url', 'url_to_path', 'path_to_url', 'is_archive_file', 'unpack_vcs_link', - 'unpack_file_url', 'is_vcs_url', 'is_file_url', + 'unpack_file_url', 'is_file_url', 'unpack_http_url', 'unpack_url', 'parse_content_disposition', 'sanitize_content_filename'] @@ -744,11 +744,6 @@ def _get_used_vcs_backend(link): return None -def is_vcs_url(link): - # type: (Link) -> bool - return bool(_get_used_vcs_backend(link)) - - def is_file_url(link): # type: (Link) -> bool return link.url.lower().startswith('file:') @@ -1063,7 +1058,7 @@ def unpack_url( would ordinarily raise HashUnsupported) are allowed. """ # non-editable vcs urls - if is_vcs_url(link): + if link.is_vcs: unpack_vcs_link(link, location) # file urls diff --git a/src/pip/_internal/models/link.py b/src/pip/_internal/models/link.py index 190e89b3b9a..56ad2a5be5c 100644 --- a/src/pip/_internal/models/link.py +++ b/src/pip/_internal/models/link.py @@ -179,6 +179,13 @@ def is_wheel(self): # type: () -> bool return self.ext == WHEEL_EXTENSION + @property + def is_vcs(self): + # type: () -> bool + from pip._internal.vcs import vcs + + return self.scheme in vcs.all_schemes + @property def is_artifact(self): # type: () -> bool @@ -186,12 +193,7 @@ def is_artifact(self): 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 pip._internal.vcs import vcs - - if self.scheme in vcs.all_schemes: - return False - - return True + return not self.is_vcs @property def is_yanked(self): diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 1d9ee8af53c..3cb09d83c68 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -16,7 +16,6 @@ from pip._internal.download import ( is_dir_url, is_file_url, - is_vcs_url, unpack_url, url_to_path, ) @@ -163,7 +162,7 @@ def prepare_linked_requirement( # we would report less-useful error messages for # unhashable requirements, complaining that there's no # hash provided. - if is_vcs_url(link): + if link.is_vcs: raise VcsHashUnsupported() elif is_file_url(link) and is_dir_url(link): raise DirectoryUrlHashUnsupported() diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index cd0d3460f9e..da890089c5a 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -815,7 +815,7 @@ def should_use_ephemeral_cache( ) return None - if req.link and not req.link.is_artifact: + if req.link and req.link.is_vcs: # VCS checkout. Build wheel just for this run. return True diff --git a/tests/unit/test_link.py b/tests/unit/test_link.py index 8a8182d3919..8fbafe082e8 100644 --- a/tests/unit/test_link.py +++ b/tests/unit/test_link.py @@ -127,3 +127,13 @@ def test_is_hash_allowed__none_hashes(self, hashes, expected): url = 'https://example.com/wheel.whl#sha512={}'.format(128 * 'a') link = Link(url) assert link.is_hash_allowed(hashes) == expected + + @pytest.mark.parametrize('url, expected', [ + ('git+https://github.com/org/repo', True), + ('bzr+http://bzr.myproject.org/MyProject/trunk/#egg=MyProject', True), + ('https://example.com/some.whl', False), + ('file://home/foo/some.whl', False), + ]) + def test_is_vcs(self, url, expected): + link = Link(url) + assert link.is_vcs is expected diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 9775d6f317e..449da28c199 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -126,7 +126,6 @@ def test_should_use_ephemeral_cache__disallow_binaries_and_vcs_checkout( causes should_use_ephemeral_cache() to return None for VCS checkouts. """ req = Requirement('pendulum') - # Passing a VCS url causes link.is_artifact to return False. link = Link(url='git+https://git.example.com/pendulum.git') req = InstallRequirement( req=req, @@ -137,7 +136,7 @@ def test_should_use_ephemeral_cache__disallow_binaries_and_vcs_checkout( source_dir='/tmp/pip-install-9py5m2z1/pendulum', ) assert not req.is_wheel - assert not req.link.is_artifact + assert req.link.is_vcs format_control = FormatControl() if disallow_binaries: From 48109b1bb512d8a1373c81cb1bae24f9f7014822 Mon Sep 17 00:00:00 2001 From: rdb Date: Wed, 14 Aug 2019 16:06:43 +0200 Subject: [PATCH 0123/3170] Don't append 'm' ABI flag in Python 3.8 --- news/6885.bugfix | 1 + src/pip/_internal/pep425tags.py | 4 +++- tests/unit/test_pep425tags.py | 4 ++++ 3 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 news/6885.bugfix diff --git a/news/6885.bugfix b/news/6885.bugfix new file mode 100644 index 00000000000..1eedfec9376 --- /dev/null +++ b/news/6885.bugfix @@ -0,0 +1 @@ +Fix 'm' flag erroneously being appended to ABI tag in Python 3.8 on platforms that do not provide SOABI diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index 03a906b94bc..dc4fdcc3add 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -117,7 +117,9 @@ def get_abi_tag(): d = 'd' if get_flag('WITH_PYMALLOC', lambda: impl == 'cp', - warn=(impl == 'cp')): + warn=(impl == 'cp' and + sys.version_info < (3, 8))) \ + and sys.version_info < (3, 8): m = 'm' if get_flag('Py_UNICODE_SIZE', lambda: sys.maxunicode == 0x10ffff, diff --git a/tests/unit/test_pep425tags.py b/tests/unit/test_pep425tags.py index f570de62133..a18f525a98f 100644 --- a/tests/unit/test_pep425tags.py +++ b/tests/unit/test_pep425tags.py @@ -47,6 +47,10 @@ def abi_tag_unicode(self, flags, config_vars): base = pip._internal.pep425tags.get_abbr_impl() + \ pip._internal.pep425tags.get_impl_ver() + if sys.version_info >= (3, 8): + # Python 3.8 removes the m flag, so don't look for it. + flags = flags.replace('m', '') + if sys.version_info < (3, 3): config_vars.update({'Py_UNICODE_SIZE': 2}) mock_gcf = self.mock_get_config_var(**config_vars) From 393f6262762346706b1c14526b8703d8857a20d7 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 16 Aug 2019 16:16:16 -0700 Subject: [PATCH 0124/3170] Add type annotations to VCS methods passing an url to run_command(). --- src/pip/_internal/vcs/bazaar.py | 11 +++++++++++ src/pip/_internal/vcs/git.py | 16 ++++++++++++++++ src/pip/_internal/vcs/mercurial.py | 9 +++++++++ src/pip/_internal/vcs/subversion.py | 8 ++++++-- src/pip/_internal/vcs/versioncontrol.py | 6 ++++++ 5 files changed, 48 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/vcs/bazaar.py b/src/pip/_internal/vcs/bazaar.py index 4f1e114ba23..61b7f41e408 100644 --- a/src/pip/_internal/vcs/bazaar.py +++ b/src/pip/_internal/vcs/bazaar.py @@ -6,8 +6,14 @@ from pip._vendor.six.moves.urllib import parse as urllib_parse from pip._internal.utils.misc import display_path, path_to_url, rmtree +from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.vcs.versioncontrol import VersionControl, vcs +if MYPY_CHECK_RUNNING: + from typing import Optional, Tuple + from pip._internal.vcs.versioncontrol import AuthInfo, RevOptions + + logger = logging.getLogger(__name__) @@ -32,6 +38,7 @@ def get_base_rev_args(rev): return ['-r', rev] def export(self, location, url): + # type: (str, str) -> None """ Export the Bazaar repository at the url to the destination location """ @@ -46,6 +53,7 @@ def export(self, location, url): ) def fetch_new(self, dest, url, rev_options): + # type: (str, str, RevOptions) -> None rev_display = rev_options.to_display() logger.info( 'Checking out %s%s to %s', @@ -57,14 +65,17 @@ def fetch_new(self, dest, url, rev_options): self.run_command(cmd_args) def switch(self, dest, url, rev_options): + # type: (str, str, RevOptions) -> None self.run_command(['switch', url], cwd=dest) def update(self, dest, url, rev_options): + # type: (str, str, RevOptions) -> None cmd_args = ['pull', '-q'] + rev_options.to_args() self.run_command(cmd_args, cwd=dest) @classmethod def get_url_rev_and_auth(cls, url): + # type: (str) -> Tuple[str, Optional[str], AuthInfo] # hotfix the URL scheme after removing bzr+ from bzr+ssh:// readd it url, rev, user_pass = super(Bazaar, cls).get_url_rev_and_auth(url) if url.startswith('ssh://'): diff --git a/src/pip/_internal/vcs/git.py b/src/pip/_internal/vcs/git.py index d8617ba88f2..89b25782bf7 100644 --- a/src/pip/_internal/vcs/git.py +++ b/src/pip/_internal/vcs/git.py @@ -12,12 +12,18 @@ from pip._internal.utils.compat import samefile from pip._internal.utils.misc import display_path, redact_password_from_url from pip._internal.utils.temp_dir import TempDirectory +from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.vcs.versioncontrol import ( RemoteNotFoundError, VersionControl, vcs, ) +if MYPY_CHECK_RUNNING: + from typing import Optional, Tuple + from pip._internal.vcs.versioncontrol import AuthInfo, RevOptions + + urlsplit = urllib_parse.urlsplit urlunsplit = urllib_parse.urlunsplit @@ -83,6 +89,7 @@ def get_current_branch(cls, location): return None def export(self, location, url): + # type: (str, str) -> None """Export the Git repository at the url to the destination location""" if not location.endswith('/'): location = location + '/' @@ -131,6 +138,7 @@ def get_revision_sha(cls, dest, rev): @classmethod def resolve_revision(cls, dest, url, rev_options): + # type: (str, str, RevOptions) -> RevOptions """ Resolve a revision to a new RevOptions object with the SHA1 of the branch, tag, or ref if found. @@ -139,6 +147,10 @@ def resolve_revision(cls, dest, url, rev_options): rev_options: a RevOptions object. """ rev = rev_options.arg_rev + # The arg_rev property's implementation for Git ensures that the + # rev return value is always non-None. + assert rev is not None + sha, is_branch = cls.get_revision_sha(dest, rev) if sha is not None: @@ -185,6 +197,7 @@ def is_commit_id_equal(cls, dest, name): return cls.get_revision(dest) == name def fetch_new(self, dest, url, rev_options): + # type: (str, str, RevOptions) -> None rev_display = rev_options.to_display() logger.info( 'Cloning %s%s to %s', redact_password_from_url(url), @@ -215,6 +228,7 @@ def fetch_new(self, dest, url, rev_options): self.update_submodules(dest) def switch(self, dest, url, rev_options): + # type: (str, str, RevOptions) -> None self.run_command(['config', 'remote.origin.url', url], cwd=dest) cmd_args = ['checkout', '-q'] + rev_options.to_args() self.run_command(cmd_args, cwd=dest) @@ -222,6 +236,7 @@ def switch(self, dest, url, rev_options): self.update_submodules(dest) def update(self, dest, url, rev_options): + # type: (str, str, RevOptions) -> None # First fetch changes from the default remote if self.get_git_version() >= parse_version('1.9.0'): # fetch tags in addition to everything else @@ -300,6 +315,7 @@ def get_subdirectory(cls, location): @classmethod def get_url_rev_and_auth(cls, url): + # type: (str) -> Tuple[str, Optional[str], AuthInfo] """ Prefixes stub URLs like 'user@hostname:user/repo.git' with 'ssh://'. That's required because although they use SSH they sometimes don't diff --git a/src/pip/_internal/vcs/mercurial.py b/src/pip/_internal/vcs/mercurial.py index db42783dce5..18c779934e8 100644 --- a/src/pip/_internal/vcs/mercurial.py +++ b/src/pip/_internal/vcs/mercurial.py @@ -7,8 +7,13 @@ from pip._internal.utils.misc import display_path, path_to_url from pip._internal.utils.temp_dir import TempDirectory +from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.vcs.versioncontrol import VersionControl, vcs +if MYPY_CHECK_RUNNING: + from pip._internal.vcs.versioncontrol import RevOptions + + logger = logging.getLogger(__name__) @@ -23,6 +28,7 @@ def get_base_rev_args(rev): return [rev] def export(self, location, url): + # type: (str, str) -> None """Export the Hg repository at the url to the destination location""" with TempDirectory(kind="export") as temp_dir: self.unpack(temp_dir.path, url=url) @@ -32,6 +38,7 @@ def export(self, location, url): ) def fetch_new(self, dest, url, rev_options): + # type: (str, str, RevOptions) -> None rev_display = rev_options.to_display() logger.info( 'Cloning hg %s%s to %s', @@ -44,6 +51,7 @@ def fetch_new(self, dest, url, rev_options): self.run_command(cmd_args, cwd=dest) def switch(self, dest, url, rev_options): + # type: (str, str, RevOptions) -> None repo_config = os.path.join(dest, self.dirname, 'hgrc') config = configparser.RawConfigParser() try: @@ -60,6 +68,7 @@ def switch(self, dest, url, rev_options): self.run_command(cmd_args, cwd=dest) def update(self, dest, url, rev_options): + # type: (str, str, RevOptions) -> None self.run_command(['pull', '-q'], cwd=dest) cmd_args = ['update', '-q'] + rev_options.to_args() self.run_command(cmd_args, cwd=dest) diff --git a/src/pip/_internal/vcs/subversion.py b/src/pip/_internal/vcs/subversion.py index 50c10ef3938..a15883fa7fa 100644 --- a/src/pip/_internal/vcs/subversion.py +++ b/src/pip/_internal/vcs/subversion.py @@ -22,7 +22,8 @@ if MYPY_CHECK_RUNNING: from typing import List, Optional, Tuple - from pip._internal.vcs.versioncontrol import RevOptions + from pip._internal.vcs.versioncontrol import AuthInfo, RevOptions + logger = logging.getLogger(__name__) @@ -84,6 +85,7 @@ def get_netloc_and_auth(cls, netloc, scheme): @classmethod def get_url_rev_and_auth(cls, url): + # type: (str) -> Tuple[str, Optional[str], AuthInfo] # hotfix the URL scheme after removing svn+ from svn+ssh:// readd it url, rev, user_pass = super(Subversion, cls).get_url_rev_and_auth(url) if url.startswith('ssh://'): @@ -92,7 +94,8 @@ def get_url_rev_and_auth(cls, url): @staticmethod def make_rev_args(username, password): - extra_args = [] + # type: (Optional[str], Optional[str]) -> List[str] + extra_args = [] # type: List[str] if username: extra_args += ['--username', username] if password: @@ -273,6 +276,7 @@ def get_remote_call_options(self): return [] def export(self, location, url): + # type: (str, str) -> None """Export the svn repository at the url to the destination location""" url, rev_options = self.get_url_rev_options(url) diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index 2bb868f1caf..6d400d56883 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -82,6 +82,7 @@ def __init__( self.extra_args = extra_args self.rev = rev self.vc_class = vc_class + self.branch_name = None # type: Optional[str] def __repr__(self): return ''.format(self.vc_class.name, self.rev) @@ -291,6 +292,7 @@ def _is_local_repository(cls, repo): return repo.startswith(os.path.sep) or bool(drive) def export(self, location, url): + # type: (str, str) -> None """ Export the repository at the url to the destination location i.e. only download the files, without vcs informations @@ -345,6 +347,7 @@ def get_url_rev_and_auth(cls, url): @staticmethod def make_rev_args(username, password): + # type: (Optional[str], Optional[str]) -> List[str] """ Return the RevOptions "extra arguments" to use in obtain(). """ @@ -381,6 +384,7 @@ def compare_urls(cls, url1, url2): return (cls.normalize_url(url1) == cls.normalize_url(url2)) def fetch_new(self, dest, url, rev_options): + # type: (str, str, RevOptions) -> None """ Fetch a revision from a repository, in the case that this is the first fetch from the repository. @@ -392,6 +396,7 @@ def fetch_new(self, dest, url, rev_options): raise NotImplementedError def switch(self, dest, url, rev_options): + # type: (str, str, RevOptions) -> None """ Switch the repo at ``dest`` to point to ``URL``. @@ -401,6 +406,7 @@ def switch(self, dest, url, rev_options): raise NotImplementedError def update(self, dest, url, rev_options): + # type: (str, str, RevOptions) -> None """ Update an already-existing repo to the given ``rev_options``. From 344f44c1e87b22fa271bc2f012df1f63cb88960b Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 17 Aug 2019 02:28:00 -0700 Subject: [PATCH 0125/3170] Simplify the get_flag() conditionals in pep425tags.py's get_abi_tag(). --- src/pip/_internal/pep425tags.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index dc4fdcc3add..dddfafc6cae 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -115,18 +115,12 @@ def get_abi_tag(): lambda: hasattr(sys, 'gettotalrefcount'), warn=(impl == 'cp')): d = 'd' - if get_flag('WITH_PYMALLOC', - lambda: impl == 'cp', - warn=(impl == 'cp' and - sys.version_info < (3, 8))) \ - and sys.version_info < (3, 8): + if sys.version_info < (3, 8) and get_flag( + 'WITH_PYMALLOC', lambda: impl == 'cp', warn=(impl == 'cp')): m = 'm' - if get_flag('Py_UNICODE_SIZE', - lambda: sys.maxunicode == 0x10ffff, - expected=4, - warn=(impl == 'cp' and - sys.version_info < (3, 3))) \ - and sys.version_info < (3, 3): + if sys.version_info < (3, 3) and get_flag( + 'Py_UNICODE_SIZE', lambda: sys.maxunicode == 0x10ffff, + expected=4, warn=(impl == 'cp')): u = 'u' abi = '%s%s%s%s%s' % (impl, get_impl_ver(), d, m, u) elif soabi and soabi.startswith('cpython-'): From 6c96b27a372a9f17be03e2deeab98e09dbbb9ab6 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Sat, 17 Aug 2019 10:29:48 -0700 Subject: [PATCH 0126/3170] Add intermediate is_cpython variable. --- src/pip/_internal/pep425tags.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index dddfafc6cae..f60d7a63707 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -111,16 +111,17 @@ def get_abi_tag(): d = '' m = '' u = '' - if get_flag('Py_DEBUG', - lambda: hasattr(sys, 'gettotalrefcount'), - warn=(impl == 'cp')): + is_cpython = (impl == 'cp') + if get_flag( + 'Py_DEBUG', lambda: hasattr(sys, 'gettotalrefcount'), + warn=is_cpython): d = 'd' if sys.version_info < (3, 8) and get_flag( - 'WITH_PYMALLOC', lambda: impl == 'cp', warn=(impl == 'cp')): + 'WITH_PYMALLOC', lambda: is_cpython, warn=is_cpython): m = 'm' if sys.version_info < (3, 3) and get_flag( 'Py_UNICODE_SIZE', lambda: sys.maxunicode == 0x10ffff, - expected=4, warn=(impl == 'cp')): + expected=4, warn=is_cpython): u = 'u' abi = '%s%s%s%s%s' % (impl, get_impl_ver(), d, m, u) elif soabi and soabi.startswith('cpython-'): From 3198ba61850c8b34b47ee61d35768f5143e14e21 Mon Sep 17 00:00:00 2001 From: Steve Barnes Date: Thu, 15 Aug 2019 10:59:20 +0100 Subject: [PATCH 0127/3170] Change pip show to report missing packages. --- news/6858.feature | 1 + src/pip/_internal/commands/show.py | 5 +++++ tests/functional/test_show.py | 30 +++++++++++++++++++++++++++++- 3 files changed, 35 insertions(+), 1 deletion(-) create mode 100644 news/6858.feature diff --git a/news/6858.feature b/news/6858.feature new file mode 100644 index 00000000000..be01bc82652 --- /dev/null +++ b/news/6858.feature @@ -0,0 +1 @@ +Make ``pip show`` warn about packages not found. diff --git a/src/pip/_internal/commands/show.py b/src/pip/_internal/commands/show.py index bacd002ae51..6107b4df551 100644 --- a/src/pip/_internal/commands/show.py +++ b/src/pip/_internal/commands/show.py @@ -60,6 +60,11 @@ def search_packages_info(query): installed[canonicalize_name(p.project_name)] = p query_names = [canonicalize_name(name) for name in query] + missing = sorted( + [name for name, pkg in zip(query, query_names) if pkg not in installed] + ) + if missing: + logger.warning('Package(s) not found: %s', ', '.join(missing)) for dist in [installed[pkg] for pkg in query_names if pkg in installed]: package = { diff --git a/tests/functional/test_show.py b/tests/functional/test_show.py index b0a69ba8965..8f63d3eca24 100644 --- a/tests/functional/test_show.py +++ b/tests/functional/test_show.py @@ -79,6 +79,34 @@ def test_find_package_not_found(): assert len(list(result)) == 0 +def test_report_single_not_found(script): + """ + Test passing one name and that isn't found. + """ + # We choose a non-canonicalized name to test that the non-canonical + # form is logged. + # Also, the following should report an error as there are no results + # to print. Consequently, there is no need to pass + # allow_stderr_warning=True since this is implied by expect_error=True. + result = script.pip('show', 'Abcd-3', expect_error=True) + assert 'WARNING: Package(s) not found: Abcd-3' in result.stderr + assert not result.stdout.splitlines() + + +def test_report_mixed_not_found(script): + """ + Test passing a mixture of found and not-found names. + """ + # We test passing non-canonicalized names. + result = script.pip( + 'show', 'Abcd3', 'A-B-C', 'pip', allow_stderr_warning=True + ) + assert 'WARNING: Package(s) not found: A-B-C, Abcd3' in result.stderr + lines = result.stdout.splitlines() + assert len(lines) == 10 + assert 'Name: pip' in lines + + def test_search_any_case(): """ Search for a package in any case. @@ -86,7 +114,7 @@ def test_search_any_case(): """ result = list(search_packages_info(['PIP'])) assert len(result) == 1 - assert 'pip' == result[0]['name'] + assert result[0]['name'] == 'pip' def test_more_than_one_package(): From 1e8a47c2f37a4301c29dd0274e02c3d99bc6fea4 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Wed, 21 Aug 2019 01:53:33 +0530 Subject: [PATCH 0128/3170] Update variable names --- tests/unit/test_req.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index fb01bdecc0f..89442886d28 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -601,16 +601,16 @@ def test_parse_editable_local_extras( def test_exclusive_environment_markers(): """Make sure RequirementSet accepts several excluding env markers""" - eq26 = install_req_from_line( + eq36 = install_req_from_line( "Django>=1.6.10,<1.7 ; python_version == '3.6'") - eq26.is_direct = True - ne26 = install_req_from_line( + eq36.is_direct = True + ne36 = install_req_from_line( "Django>=1.6.10,<1.8 ; python_version != '3.6'") - ne26.is_direct = True + ne36.is_direct = True req_set = RequirementSet() - req_set.add_requirement(eq26) - req_set.add_requirement(ne26) + req_set.add_requirement(eq36) + req_set.add_requirement(ne36) assert req_set.has_requirement('Django') From e8eda16ed84fa861dccb490167010db49a6828c8 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 19 Jul 2019 22:07:49 -0400 Subject: [PATCH 0129/3170] Rename some PackageFinder "best candidate" classes and methods: * Rename FoundCandidates to BestCandidateResult. * Rename CandidateEvaluator's make_found_candidates() to compute_best_candidate(). * Rename CandidateEvaluator's get_best_candidate() to sort_best_candidate(). * Rename PackageFinder's find_candidates() to find_best_candidate(). --- src/pip/_internal/commands/list.py | 2 +- src/pip/_internal/index.py | 45 ++++++++++++++--------------- src/pip/_internal/utils/outdated.py | 6 ++-- tests/unit/test_index.py | 28 +++++++++--------- tests/unit/test_unit_outdated.py | 6 ++-- 5 files changed, 44 insertions(+), 43 deletions(-) diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index aacd5680ca1..b86e38d1c20 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -192,7 +192,7 @@ def iter_packages_latest_infos(self, packages, options): evaluator = finder.make_candidate_evaluator( project_name=dist.project_name, ) - best_candidate = evaluator.get_best_candidate(all_candidates) + best_candidate = evaluator.sort_best_candidate(all_candidates) if best_candidate is None: continue diff --git a/src/pip/_internal/index.py b/src/pip/_internal/index.py index c5bc3bc3428..12f1071d414 100644 --- a/src/pip/_internal/index.py +++ b/src/pip/_internal/index.py @@ -69,7 +69,7 @@ SecureOrigin = Tuple[str, str, Optional[str]] -__all__ = ['FormatControl', 'FoundCandidates', 'PackageFinder'] +__all__ = ['FormatControl', 'BestCandidateResult', 'PackageFinder'] SECURE_ORIGINS = [ @@ -568,6 +568,9 @@ def create( :param target_python: The target Python interpreter to use when checking compatibility. If None (the default), a TargetPython object will be constructed from the running Python. + :param specifier: An optional object implementing `filter` + (e.g. `packaging.specifiers.SpecifierSet`) to filter applicable + versions. :param hashes: An optional collection of allowed hashes. """ if target_python is None: @@ -643,21 +646,17 @@ def get_applicable_candidates( project_name=self._project_name, ) - def make_found_candidates( + def compute_best_candidate( self, candidates, # type: List[InstallationCandidate] ): - # type: (...) -> FoundCandidates + # type: (...) -> BestCandidateResult """ - Create and return a `FoundCandidates` instance. - - :param specifier: An optional object implementing `filter` - (e.g. `packaging.specifiers.SpecifierSet`) to filter applicable - versions. + Compute and return a `BestCandidateResult` instance. """ applicable_candidates = self.get_applicable_candidates(candidates) - return FoundCandidates( + return BestCandidateResult( candidates, applicable_candidates=applicable_candidates, evaluator=self, @@ -723,7 +722,7 @@ def _sort_key(self, candidate): build_tag, pri, ) - def get_best_candidate( + def sort_best_candidate( self, candidates, # type: List[InstallationCandidate] ): @@ -754,11 +753,11 @@ def get_best_candidate( return best_candidate -class FoundCandidates(object): - """A collection of candidates, returned by `PackageFinder.find_candidates`. +class BestCandidateResult(object): + """A collection of candidates, returned by `PackageFinder.find_best_candidate`. This class is only intended to be instantiated by CandidateEvaluator's - `make_found_candidates()` method. + `compute_best_candidate()` method. """ def __init__( @@ -796,7 +795,7 @@ def get_best(self): candidates are found. """ candidates = list(self.iter_applicable()) - return self._evaluator.get_best_candidate(candidates) + return self._evaluator.sort_best_candidate(candidates) class PackageFinder(object): @@ -1174,20 +1173,20 @@ def make_candidate_evaluator( hashes=hashes, ) - def find_candidates( + def find_best_candidate( self, project_name, # type: str specifier=None, # type: Optional[specifiers.BaseSpecifier] hashes=None, # type: Optional[Hashes] ): - # type: (...) -> FoundCandidates + # type: (...) -> BestCandidateResult """Find matches for the given project and specifier. :param specifier: An optional object implementing `filter` (e.g. `packaging.specifiers.SpecifierSet`) to filter applicable versions. - :return: A `FoundCandidates` instance. + :return: A `BestCandidateResult` instance. """ candidates = self.find_all_candidates(project_name) candidate_evaluator = self.make_candidate_evaluator( @@ -1195,7 +1194,7 @@ def find_candidates( specifier=specifier, hashes=hashes, ) - return candidate_evaluator.make_found_candidates(candidates) + return candidate_evaluator.compute_best_candidate(candidates) def find_requirement(self, req, upgrade): # type: (InstallRequirement, bool) -> Optional[Link] @@ -1206,10 +1205,10 @@ def find_requirement(self, req, upgrade): Raises DistributionNotFound or BestVersionAlreadyInstalled otherwise """ hashes = req.hashes(trust_internet=False) - candidates = self.find_candidates( + best_candidate_result = self.find_best_candidate( req.name, specifier=req.specifier, hashes=hashes, ) - best_candidate = candidates.get_best() + best_candidate = best_candidate_result.get_best() installed_version = None # type: Optional[_BaseVersion] if req.satisfied_by is not None: @@ -1230,7 +1229,7 @@ def _format_versions(cand_iter): 'Could not find a version that satisfies the requirement %s ' '(from versions: %s)', req, - _format_versions(candidates.iter_all()), + _format_versions(best_candidate_result.iter_all()), ) raise DistributionNotFound( @@ -1265,14 +1264,14 @@ def _format_versions(cand_iter): 'Installed version (%s) is most up-to-date (past versions: ' '%s)', installed_version, - _format_versions(candidates.iter_applicable()), + _format_versions(best_candidate_result.iter_applicable()), ) raise BestVersionAlreadyInstalled logger.debug( 'Using version %s (newest of versions: %s)', best_candidate.version, - _format_versions(candidates.iter_applicable()), + _format_versions(best_candidate_result.iter_applicable()), ) return best_candidate.link diff --git a/src/pip/_internal/utils/outdated.py b/src/pip/_internal/utils/outdated.py index 2b10aeff6bb..67971ad69c3 100644 --- a/src/pip/_internal/utils/outdated.py +++ b/src/pip/_internal/utils/outdated.py @@ -139,10 +139,10 @@ def pip_version_check(session, options): trusted_hosts=options.trusted_hosts, session=session, ) - candidate = finder.find_candidates("pip").get_best() - if candidate is None: + best_candidate = finder.find_best_candidate("pip").get_best() + if best_candidate is None: return - pypi_version = str(candidate.version) + pypi_version = str(best_candidate.version) # save that we've performed a check state.save(pypi_version, current_time) diff --git a/tests/unit/test_index.py b/tests/unit/test_index.py index 4d7b5933117..79ec3f7b522 100644 --- a/tests/unit/test_index.py +++ b/tests/unit/test_index.py @@ -387,7 +387,7 @@ def test_get_applicable_candidates__hashes( actual_versions = [str(c.version) for c in actual] assert actual_versions == expected_versions - def test_make_found_candidates(self): + def test_compute_best_candidate(self): specifier = SpecifierSet('<= 1.11') versions = ['1.10', '1.11', '1.12'] candidates = [ @@ -397,16 +397,16 @@ def test_make_found_candidates(self): 'my-project', specifier=specifier, ) - found_candidates = evaluator.make_found_candidates(candidates) + result = evaluator.compute_best_candidate(candidates) - assert found_candidates._candidates == candidates - assert found_candidates._evaluator is evaluator + assert result._candidates == candidates + assert result._evaluator is evaluator expected_applicable = candidates[:2] assert [str(c.version) for c in expected_applicable] == [ '1.10', '1.11', ] - assert found_candidates._applicable_candidates == expected_applicable + assert result._applicable_candidates == expected_applicable @pytest.mark.parametrize('hex_digest, expected', [ # Test a link with no hash. @@ -448,15 +448,15 @@ def test_sort_key__is_yanked(self, yanked_reason, expected): actual = sort_value[1] assert actual == expected - def test_get_best_candidate__no_candidates(self): + def test_sort_best_candidate__no_candidates(self): """ Test passing an empty list. """ evaluator = CandidateEvaluator.create('my-project') - actual = evaluator.get_best_candidate([]) + actual = evaluator.sort_best_candidate([]) assert actual is None - def test_get_best_candidate__all_yanked(self, caplog): + def test_sort_best_candidate__all_yanked(self, caplog): """ Test all candidates yanked. """ @@ -468,7 +468,7 @@ def test_get_best_candidate__all_yanked(self, caplog): ] expected_best = candidates[1] evaluator = CandidateEvaluator.create('my-project') - actual = evaluator.get_best_candidate(candidates) + actual = evaluator.sort_best_candidate(candidates) assert actual is expected_best assert str(actual.version) == '3.0' @@ -489,7 +489,7 @@ def test_get_best_candidate__all_yanked(self, caplog): # Test a unicode string with a non-ascii character. (u'curly quote: \u2018', u'curly quote: \u2018'), ]) - def test_get_best_candidate__yanked_reason( + def test_sort_best_candidate__yanked_reason( self, caplog, yanked_reason, expected_reason, ): """ @@ -499,7 +499,7 @@ def test_get_best_candidate__yanked_reason( make_mock_candidate('1.0', yanked_reason=yanked_reason), ] evaluator = CandidateEvaluator.create('my-project') - actual = evaluator.get_best_candidate(candidates) + actual = evaluator.sort_best_candidate(candidates) assert str(actual.version) == '1.0' assert len(caplog.records) == 1 @@ -513,7 +513,9 @@ def test_get_best_candidate__yanked_reason( ) + expected_reason assert record.message == expected_message - def test_get_best_candidate__best_yanked_but_not_all(self, caplog): + def test_sort_best_candidate__best_yanked_but_not_all( + self, caplog, + ): """ Test the best candidates being yanked, but not all. """ @@ -526,7 +528,7 @@ def test_get_best_candidate__best_yanked_but_not_all(self, caplog): ] expected_best = candidates[1] evaluator = CandidateEvaluator.create('my-project') - actual = evaluator.get_best_candidate(candidates) + actual = evaluator.sort_best_candidate(candidates) assert actual is expected_best assert str(actual.version) == '2.0' diff --git a/tests/unit/test_unit_outdated.py b/tests/unit/test_unit_outdated.py index a5d37f81868..87ed16f0943 100644 --- a/tests/unit/test_unit_outdated.py +++ b/tests/unit/test_unit_outdated.py @@ -12,7 +12,7 @@ from pip._internal.utils import outdated -class MockFoundCandidates(object): +class MockBestCandidateResult(object): def __init__(self, best): self._best = best @@ -37,8 +37,8 @@ class MockPackageFinder(object): def create(cls, *args, **kwargs): return cls() - def find_candidates(self, project_name): - return MockFoundCandidates(self.INSTALLATION_CANDIDATES[0]) + def find_best_candidate(self, project_name): + return MockBestCandidateResult(self.INSTALLATION_CANDIDATES[0]) class MockDistribution(object): From 6554273fe5aa7f4acab8c893acfa230b15d4f745 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 19 Jul 2019 22:38:03 -0400 Subject: [PATCH 0130/3170] Pass the best candidate to BestCandidateResult instead of CandidateEvaluator. --- src/pip/_internal/index.py | 23 +++++++++-------------- src/pip/_internal/utils/outdated.py | 2 +- tests/unit/test_index.py | 3 ++- tests/unit/test_unit_outdated.py | 5 +---- 4 files changed, 13 insertions(+), 20 deletions(-) diff --git a/src/pip/_internal/index.py b/src/pip/_internal/index.py index 12f1071d414..78bc3dde4f1 100644 --- a/src/pip/_internal/index.py +++ b/src/pip/_internal/index.py @@ -656,10 +656,12 @@ def compute_best_candidate( """ applicable_candidates = self.get_applicable_candidates(candidates) + best_candidate = self.sort_best_candidate(applicable_candidates) + return BestCandidateResult( candidates, applicable_candidates=applicable_candidates, - evaluator=self, + best_candidate=best_candidate, ) def _sort_key(self, candidate): @@ -764,18 +766,19 @@ def __init__( self, candidates, # type: List[InstallationCandidate] applicable_candidates, # type: List[InstallationCandidate] - evaluator, # type: CandidateEvaluator + best_candidate, # type: Optional[InstallationCandidate] ): # type: (...) -> None """ :param candidates: A sequence of all available candidates found. :param applicable_candidates: The applicable candidates. - :param evaluator: A CandidateEvaluator object to sort applicable - candidates by order of preference. + :param best_candidate: The most preferred candidate found, or None + if no applicable candidates were found. """ self._applicable_candidates = applicable_candidates self._candidates = candidates - self._evaluator = evaluator + + self.best_candidate = best_candidate def iter_all(self): # type: () -> Iterable[InstallationCandidate] @@ -789,14 +792,6 @@ def iter_applicable(self): """ return iter(self._applicable_candidates) - def get_best(self): - # type: () -> Optional[InstallationCandidate] - """Return the best candidate available, or None if no applicable - candidates are found. - """ - candidates = list(self.iter_applicable()) - return self._evaluator.sort_best_candidate(candidates) - class PackageFinder(object): """This finds packages. @@ -1208,7 +1203,7 @@ def find_requirement(self, req, upgrade): best_candidate_result = self.find_best_candidate( req.name, specifier=req.specifier, hashes=hashes, ) - best_candidate = best_candidate_result.get_best() + best_candidate = best_candidate_result.best_candidate installed_version = None # type: Optional[_BaseVersion] if req.satisfied_by is not None: diff --git a/src/pip/_internal/utils/outdated.py b/src/pip/_internal/utils/outdated.py index 67971ad69c3..7f8ea3ca507 100644 --- a/src/pip/_internal/utils/outdated.py +++ b/src/pip/_internal/utils/outdated.py @@ -139,7 +139,7 @@ def pip_version_check(session, options): trusted_hosts=options.trusted_hosts, session=session, ) - best_candidate = finder.find_best_candidate("pip").get_best() + best_candidate = finder.find_best_candidate("pip").best_candidate if best_candidate is None: return pypi_version = str(best_candidate.version) diff --git a/tests/unit/test_index.py b/tests/unit/test_index.py index 79ec3f7b522..696c6810f95 100644 --- a/tests/unit/test_index.py +++ b/tests/unit/test_index.py @@ -400,7 +400,6 @@ def test_compute_best_candidate(self): result = evaluator.compute_best_candidate(candidates) assert result._candidates == candidates - assert result._evaluator is evaluator expected_applicable = candidates[:2] assert [str(c.version) for c in expected_applicable] == [ '1.10', @@ -408,6 +407,8 @@ def test_compute_best_candidate(self): ] assert result._applicable_candidates == expected_applicable + assert result.best_candidate is expected_applicable[1] + @pytest.mark.parametrize('hex_digest, expected', [ # Test a link with no hash. (None, 0), diff --git a/tests/unit/test_unit_outdated.py b/tests/unit/test_unit_outdated.py index 87ed16f0943..bdafc5fb775 100644 --- a/tests/unit/test_unit_outdated.py +++ b/tests/unit/test_unit_outdated.py @@ -14,10 +14,7 @@ class MockBestCandidateResult(object): def __init__(self, best): - self._best = best - - def get_best(self): - return self._best + self.best_candidate = best class MockPackageFinder(object): From 1a8dc9cda1be04b18389977e4d7e1998be3c799f Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 19 Jul 2019 22:40:07 -0400 Subject: [PATCH 0131/3170] Move compute_best_candidate() to the end of CandidateEvaluator. --- src/pip/_internal/index.py | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/src/pip/_internal/index.py b/src/pip/_internal/index.py index 78bc3dde4f1..df79ff13e3e 100644 --- a/src/pip/_internal/index.py +++ b/src/pip/_internal/index.py @@ -646,24 +646,6 @@ def get_applicable_candidates( project_name=self._project_name, ) - def compute_best_candidate( - self, - candidates, # type: List[InstallationCandidate] - ): - # type: (...) -> BestCandidateResult - """ - Compute and return a `BestCandidateResult` instance. - """ - applicable_candidates = self.get_applicable_candidates(candidates) - - best_candidate = self.sort_best_candidate(applicable_candidates) - - return BestCandidateResult( - candidates, - applicable_candidates=applicable_candidates, - best_candidate=best_candidate, - ) - def _sort_key(self, candidate): # type: (InstallationCandidate) -> CandidateSortingKey """ @@ -754,6 +736,24 @@ def sort_best_candidate( return best_candidate + def compute_best_candidate( + self, + candidates, # type: List[InstallationCandidate] + ): + # type: (...) -> BestCandidateResult + """ + Compute and return a `BestCandidateResult` instance. + """ + applicable_candidates = self.get_applicable_candidates(candidates) + + best_candidate = self.sort_best_candidate(applicable_candidates) + + return BestCandidateResult( + candidates, + applicable_candidates=applicable_candidates, + best_candidate=best_candidate, + ) + class BestCandidateResult(object): """A collection of candidates, returned by `PackageFinder.find_best_candidate`. From a644fb074fa2d4d3043406c37bde9805be96739c Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 19 Jul 2019 22:40:59 -0400 Subject: [PATCH 0132/3170] Move BestCandidateResult before CandidateEvaluator. --- src/pip/_internal/index.py | 76 +++++++++++++++++++------------------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/src/pip/_internal/index.py b/src/pip/_internal/index.py index df79ff13e3e..2fa805555d6 100644 --- a/src/pip/_internal/index.py +++ b/src/pip/_internal/index.py @@ -545,6 +545,44 @@ def __init__( self.prefer_binary = prefer_binary +class BestCandidateResult(object): + """A collection of candidates, returned by `PackageFinder.find_best_candidate`. + + This class is only intended to be instantiated by CandidateEvaluator's + `compute_best_candidate()` method. + """ + + def __init__( + self, + candidates, # type: List[InstallationCandidate] + applicable_candidates, # type: List[InstallationCandidate] + best_candidate, # type: Optional[InstallationCandidate] + ): + # type: (...) -> None + """ + :param candidates: A sequence of all available candidates found. + :param applicable_candidates: The applicable candidates. + :param best_candidate: The most preferred candidate found, or None + if no applicable candidates were found. + """ + self._applicable_candidates = applicable_candidates + self._candidates = candidates + + self.best_candidate = best_candidate + + def iter_all(self): + # type: () -> Iterable[InstallationCandidate] + """Iterate through all candidates. + """ + return iter(self._candidates) + + def iter_applicable(self): + # type: () -> Iterable[InstallationCandidate] + """Iterate through the applicable candidates. + """ + return iter(self._applicable_candidates) + + class CandidateEvaluator(object): """ @@ -755,44 +793,6 @@ def compute_best_candidate( ) -class BestCandidateResult(object): - """A collection of candidates, returned by `PackageFinder.find_best_candidate`. - - This class is only intended to be instantiated by CandidateEvaluator's - `compute_best_candidate()` method. - """ - - def __init__( - self, - candidates, # type: List[InstallationCandidate] - applicable_candidates, # type: List[InstallationCandidate] - best_candidate, # type: Optional[InstallationCandidate] - ): - # type: (...) -> None - """ - :param candidates: A sequence of all available candidates found. - :param applicable_candidates: The applicable candidates. - :param best_candidate: The most preferred candidate found, or None - if no applicable candidates were found. - """ - self._applicable_candidates = applicable_candidates - self._candidates = candidates - - self.best_candidate = best_candidate - - def iter_all(self): - # type: () -> Iterable[InstallationCandidate] - """Iterate through all candidates. - """ - return iter(self._candidates) - - def iter_applicable(self): - # type: () -> Iterable[InstallationCandidate] - """Iterate through the applicable candidates. - """ - return iter(self._applicable_candidates) - - class PackageFinder(object): """This finds packages. From 3eb803aa26d4aa77ab2b1dc71efa77a6a4552a49 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 19 Jul 2019 22:45:54 -0400 Subject: [PATCH 0133/3170] Add some assertions to BestCandidateResult.__init__(). --- src/pip/_internal/index.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/pip/_internal/index.py b/src/pip/_internal/index.py index 2fa805555d6..881ffcc3763 100644 --- a/src/pip/_internal/index.py +++ b/src/pip/_internal/index.py @@ -565,6 +565,13 @@ def __init__( :param best_candidate: The most preferred candidate found, or None if no applicable candidates were found. """ + assert set(applicable_candidates) <= set(candidates) + + if best_candidate is None: + assert not applicable_candidates + else: + assert best_candidate in applicable_candidates + self._applicable_candidates = applicable_candidates self._candidates = candidates From 06d786dee1e0cd3423dad4f3c067a4be0c505178 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 24 Jul 2019 15:11:05 -0400 Subject: [PATCH 0134/3170] Add a test for compute_best_candidate() returning a None best candidate. --- tests/unit/test_index.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/unit/test_index.py b/tests/unit/test_index.py index 696c6810f95..a2b7752f737 100644 --- a/tests/unit/test_index.py +++ b/tests/unit/test_index.py @@ -409,6 +409,25 @@ def test_compute_best_candidate(self): assert result.best_candidate is expected_applicable[1] + def test_compute_best_candidate__none_best(self): + """ + Test returning a None best candidate. + """ + specifier = SpecifierSet('<= 1.10') + versions = ['1.11', '1.12'] + candidates = [ + make_mock_candidate(version) for version in versions + ] + evaluator = CandidateEvaluator.create( + 'my-project', + specifier=specifier, + ) + result = evaluator.compute_best_candidate(candidates) + + assert result._candidates == candidates + assert result._applicable_candidates == [] + assert result.best_candidate is None + @pytest.mark.parametrize('hex_digest, expected', [ # Test a link with no hash. (None, 0), From 8db3944a64787127b9a80e34bc30d2bb6489e52a Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 20 Aug 2019 22:48:38 -0700 Subject: [PATCH 0135/3170] Add initial architecture section for index.py and PackageFinder. --- docs/html/development/architecture/index.rst | 1 + .../architecture/package-finding.rst | 202 ++++++++++++++++++ 2 files changed, 203 insertions(+) create mode 100644 docs/html/development/architecture/package-finding.rst diff --git a/docs/html/development/architecture/index.rst b/docs/html/development/architecture/index.rst index f3f221565c4..204604e8948 100644 --- a/docs/html/development/architecture/index.rst +++ b/docs/html/development/architecture/index.rst @@ -15,6 +15,7 @@ Architecture of pip's internals .. toctree:: :maxdepth: 2 + package-finding .. _`tracking issue`: https://github.com/pypa/pip/issues/6831 diff --git a/docs/html/development/architecture/package-finding.rst b/docs/html/development/architecture/package-finding.rst new file mode 100644 index 00000000000..1f17cb2c80e --- /dev/null +++ b/docs/html/development/architecture/package-finding.rst @@ -0,0 +1,202 @@ +Finding and choosing files (``index.py`` and ``PackageFinder``) +--------------------------------------------------------------- + +The ``index.py`` module is a top-level module in pip responsible for deciding +what file to download and from where, given a requirement for a project. The +module's functionality is largely exposed through and coordinated by the +module's ``PackageFinder`` class. + + +.. _index-py-overview: + +Overview +******** + +Here is a rough description of the process that pip uses to choose what +file to download for a package, given a requirement: + +1. Access the various network and file system locations configured for pip + that contain package files. These locations can include, for example, + pip's :ref:`--index-url <--index-url>` (with default + https://pypi.org/simple/ ) and any configured + :ref:`--extra-index-url <--extra-index-url>` locations. + Each of these locations is a `PEP 503`_ "simple repository" page, which + is an HTML page of anchor links. +2. Collect together all of the links (e.g. by parsing the anchor links + from the HTML pages) and create ``Link`` objects from each of these. +3. Determine which of the links are minimally relevant, using the + :ref:`LinkEvaluator ` class. Create an + ``InstallationCandidate`` object (aka candidate for install) for each + of these relevant links. +4. Further filter the collection of ``InstallationCandidate`` objects (using + the :ref:`CandidateEvaluator ` class) to a + collection of "applicable" candidates. +5. If there are applicable candidates, choose the best candidate by sorting + them (again using the :ref:`CandidateEvaluator + ` class). + +The remainder of this section is organized by documenting some of the +classes inside ``index.py``, in the following order: + +* the main :ref:`PackageFinder ` class, +* the :ref:`LinkEvaluator ` class, +* the :ref:`CandidateEvaluator ` class, +* the :ref:`CandidatePreferences ` class, and +* the :ref:`BestCandidateResult ` class. + + +.. _package-finder-class: + +The ``PackageFinder`` class +*************************** + +The ``PackageFinder`` class is the primary way through which code in pip +interacts with ``index.py``. It is an umbrella class that encapsulates and +groups together various package-finding functionality. + +The ``PackageFinder`` class is responsible for searching the network and file +system for what versions of a package pip can install, and also for deciding +which version is most preferred, given the user's preferences, target Python +environment, etc. + +The pip commands that use the ``PackageFinder`` class are: + +* :ref:`pip download` +* :ref:`pip install` +* :ref:`pip list` +* :ref:`pip wheel` + +The pip commands requiring use of the ``PackageFinder`` class generally +instantiate ``PackageFinder`` only once for the whole pip invocation. In +fact, pip creates this ``PackageFinder`` instance when command options +are first parsed. + +With the excepton of :ref:`pip list`, each of the above commands is +implemented as a ``Command`` class inheriting from ``RequirementCommand`` +(for example :ref:`pip download` is implemented by ``DownloadCommand``), and +the ``PackageFinder`` instance is created by calling the +``RequirementCommand`` class's ``_build_package_finder()`` method. ``pip +list``, on the other hand, constructs its ``PackageFinder`` instance by +calling the ``ListCommand`` class's ``_build_package_finder()``. (This +difference may simply be historical and may not actually be necessary.) + +Each of these commands also uses the ``PackageFinder`` class for pip's +"self-check," (i.e. to check whether a pip upgrade is available). In this +case, the ``PackageFinder`` instance is created by the ``outdated.py`` +module's ``pip_version_check()`` function. + +The ``PackageFinder`` class is responsible for doing all of the things listed +in the :ref:`Overview ` section like fetching and parsing +`PEP 503`_ simple repository HTML pages, evaluating which links in the simple +repository pages are relevant for each requirement, and further filtering and +sorting by preference the candidates for install coming from the relevant +links. + +One of ``PackageFinder``'s main top-level methods is +``find_best_candidate()``. This method does the following two things: + +1. Calls its ``find_all_candidates()`` method, which reads and parses all the + index URL's provided by the user, constructs a :ref:`LinkEvaluator + ` object to filter out some of those links, and then + returns a list of ``InstallationCandidates`` (aka candidates for install). + This corresponds to steps 1-3 of the :ref:`Overview ` + above. +2. Constructs a ``CandidateEvaluator`` object and uses that to determine + the best candidate. It does this by calling the ``CandidateEvaluator`` + class's ``compute_best_candidate()`` method on the return value of + ``find_all_candidates()``. This corresponds to steps 4-5 of the Overview. + + +.. _link-evaluator-class: + +The ``LinkEvaluator`` class +*************************** + +The ``LinkEvaluator`` class contains the business logic for determining +whether a link (e.g. in a simple repository page) satisfies minimal +conditions to be a candidate for install (resulting in an +``InstallationCandidate`` object). When making this determination, the +``LinkEvaluator`` instance uses information like the target Python +interpreter as well as user preferences like whether binary files are +allowed or preferred, etc. + +Specifically, the ``LinkEvaluator`` class has an ``evaluate_link()`` method +that returns whether a link is a candidate for install. + +Instances of this class are created by the ``PackageFinder`` class's +``make_link_evaluator()`` on a per-requirement basis. + + +.. _candidate-evaluator-class: + +The ``CandidateEvaluator`` class +******************************** + +The ``CandidateEvaluator`` class contains the business logic for evaluating +which ``InstallationCandidate`` objects should be preferred. This can be +viewed as a determination that is finer-grained than that performed by the +``LinkEvaluator`` class. + +In particular, the ``CandidateEvaluator`` class uses the whole set of +``InstallationCandidate`` objects when making its determinations, as opposed +to evaluating each candidate in isolation, as ``LinkEvaluator`` does. For +example, whether a pre-release is eligible for selection or whether a file +whose hash doesn't match is eligible depends on properties of the collection +as a whole. + +The ``CandidateEvaluator`` class uses information like the list of `PEP 425`_ +tags compatible with the target Python interpreter, hashes provided by the +user, and other user preferences, etc. + +Specifically, the class has a ``get_applicable_candidates()`` method. +This accepts the ``InstallationCandidate`` objects resulting from the links +accepted by the ``LinkEvaluator`` class's ``evaluate_link()`` method, and +it further filters them to a list of "applicable" candidates. + +The ``CandidateEvaluator`` class also has a ``sort_best_candidate()`` method +that orders the applicable candidates by preference, and then returns the +best (i.e. most preferred). + +Finally, the class has a ``compute_best_candidate()`` method that calls +``get_applicable_candidates()`` followed by ``sort_best_candidate()``, and +then returning a :ref:`BestCandidateResult ` +object encapsulating both the intermediate and final results of the decision. + +Instances of ``CandidateEvaluator`` are created by the ``PackageFinder`` +class's ``make_candidate_evaluator()`` method on a per-requirement basis. + + +.. _candidate-preferences-class: + +The ``CandidatePreferences`` class +********************************** + +The ``CandidatePreferences`` class is a simple container class that groups +together some of the user preferences that ``PackageFinder`` uses to +construct ``CandidateEvaluator`` objects (via the ``PackageFinder`` class's +``make_candidate_evaluator()`` method). + +A ``PackageFinder`` instance has a ``_candidate_prefs`` attribute whose value +is a ``CandidatePreferences`` instance. Since ``PackageFinder`` has a number +of responsibilities and options that control its behavior, grouping the +preferences specific to ``CandidateEvaluator`` helps maintainers know which +attributes are needed only for ``CandidateEvaluator``. + + +.. _best-candidate-result-class: + +The ``BestCandidateResult`` class +********************************* + +The ``BestCandidateResult`` class is a convenience "container" class that +encapsulates the result of finding the best candidate for a requirement. +(By "container" we mean an object that simply contains data and has no +business logic or state-changing methods of its own.) + +The class is the return type of both the ``CandidateEvaluator`` class's +``compute_best_candidate()`` method and the ``PackageFinder`` class's +``find_best_candidate()`` method. + + +.. _`PEP 425`: https://www.python.org/dev/peps/pep-0425/ +.. _`PEP 503`: https://www.python.org/dev/peps/pep-0503/ From 5e97de47730c6c2e308def0115227cc3138c5405 Mon Sep 17 00:00:00 2001 From: Christopher Hunt Date: Wed, 21 Aug 2019 05:19:02 -0400 Subject: [PATCH 0136/3170] Ignore errors copying socket files for source installs (in Python 3). (#6844) --- news/5306.bugfix | 1 + src/pip/_internal/download.py | 84 +++++++++++++++++++++----- src/pip/_internal/utils/filesystem.py | 31 ++++++++++ tests/functional/test_install.py | 25 ++++++++ tests/lib/filesystem.py | 48 +++++++++++++++ tests/unit/test_download.py | 87 +++++++++++++++++++++++++++ tests/unit/test_utils_filesystem.py | 61 +++++++++++++++++++ tox.ini | 3 +- 8 files changed, 324 insertions(+), 16 deletions(-) create mode 100644 news/5306.bugfix create mode 100644 tests/lib/filesystem.py create mode 100644 tests/unit/test_utils_filesystem.py diff --git a/news/5306.bugfix b/news/5306.bugfix new file mode 100644 index 00000000000..bf040a95fcf --- /dev/null +++ b/news/5306.bugfix @@ -0,0 +1 @@ +Ignore errors copying socket files for local source installs (in Python 3). diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index 159a4433e39..bb9bd868580 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -21,6 +21,7 @@ from pip._vendor.requests.models import CONTENT_CHUNK_SIZE, Response from pip._vendor.requests.structures import CaseInsensitiveDict from pip._vendor.requests.utils import get_netrc_auth +from pip._vendor.six import PY2 # 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 @@ -33,7 +34,7 @@ # Import ssl from compat so the initial import occurs in only one place. from pip._internal.utils.compat import HAS_TLS, ssl from pip._internal.utils.encoding import auto_decode -from pip._internal.utils.filesystem import check_path_owner +from pip._internal.utils.filesystem import check_path_owner, copy2_fixed from pip._internal.utils.glibc import libc_ver from pip._internal.utils.marker_files import write_delete_marker_file from pip._internal.utils.misc import ( @@ -49,6 +50,7 @@ format_size, get_installed_version, netloc_has_port, + path_to_display, path_to_url, remove_auth_from_url, rmtree, @@ -63,15 +65,39 @@ if MYPY_CHECK_RUNNING: from typing import ( - Optional, Tuple, Dict, IO, Text, Union + Callable, Dict, List, IO, Optional, Text, Tuple, Union ) from optparse import Values + + from mypy_extensions import TypedDict + from pip._internal.models.link import Link from pip._internal.utils.hashes import Hashes from pip._internal.vcs.versioncontrol import AuthInfo, VersionControl Credentials = Tuple[str, str, str] + if PY2: + CopytreeKwargs = TypedDict( + 'CopytreeKwargs', + { + 'ignore': Callable[[str, List[str]], List[str]], + 'symlinks': bool, + }, + total=False, + ) + else: + CopytreeKwargs = TypedDict( + 'CopytreeKwargs', + { + 'copy_function': Callable[[str, str], None], + 'ignore': Callable[[str, List[str]], List[str]], + 'ignore_dangling_symlinks': bool, + 'symlinks': bool, + }, + total=False, + ) + __all__ = ['get_file_content', 'is_url', 'url_to_path', 'path_to_url', @@ -939,6 +965,46 @@ def unpack_http_url( os.unlink(from_path) +def _copy2_ignoring_special_files(src, dest): + # type: (str, str) -> None + """Copying special files is not supported, but as a convenience to users + we skip errors copying them. This supports tools that may create e.g. + socket files in the project source directory. + """ + try: + copy2_fixed(src, dest) + except shutil.SpecialFileError as e: + # SpecialFileError may be raised due to either the source or + # destination. If the destination was the cause then we would actually + # care, but since the destination directory is deleted prior to + # copy we ignore all of them assuming it is caused by the source. + logger.warning( + "Ignoring special file error '%s' encountered copying %s to %s.", + str(e), + path_to_display(src), + path_to_display(dest), + ) + + +def _copy_source_tree(source, target): + # type: (str, str) -> None + def ignore(d, names): + # Pulling in those directories can potentially be very slow, + # exclude the following directories if they appear in the top + # level dir (and only it). + # See discussion at https://github.com/pypa/pip/pull/6770 + return ['.tox', '.nox'] if d == source else [] + + kwargs = dict(ignore=ignore, symlinks=True) # type: CopytreeKwargs + + if not PY2: + # Python 2 does not support copy_function, so we only ignore + # errors on special file copy in Python 3. + kwargs['copy_function'] = _copy2_ignoring_special_files + + shutil.copytree(source, target, **kwargs) + + def unpack_file_url( link, # type: Link location, # type: str @@ -954,21 +1020,9 @@ def unpack_file_url( link_path = url_to_path(link.url_without_fragment) # If it's a url to a local directory if is_dir_url(link): - - def ignore(d, names): - # Pulling in those directories can potentially be very slow, - # exclude the following directories if they appear in the top - # level dir (and only it). - # See discussion at https://github.com/pypa/pip/pull/6770 - return ['.tox', '.nox'] if d == link_path else [] - if os.path.isdir(location): rmtree(location) - shutil.copytree(link_path, - location, - symlinks=True, - ignore=ignore) - + _copy_source_tree(link_path, location) if download_dir: logger.info('Link is a directory, ignoring download_dir') return diff --git a/src/pip/_internal/utils/filesystem.py b/src/pip/_internal/utils/filesystem.py index 1e6b0338581..c5233ebbc71 100644 --- a/src/pip/_internal/utils/filesystem.py +++ b/src/pip/_internal/utils/filesystem.py @@ -1,5 +1,7 @@ import os import os.path +import shutil +import stat from pip._internal.utils.compat import get_path_uid @@ -28,3 +30,32 @@ def check_path_owner(path): else: previous, path = path, os.path.dirname(path) return False # assume we don't own the path + + +def copy2_fixed(src, dest): + # type: (str, str) -> None + """Wrap shutil.copy2() but map errors copying socket files to + SpecialFileError as expected. + + See also https://bugs.python.org/issue37700. + """ + try: + shutil.copy2(src, dest) + except (OSError, IOError): + for f in [src, dest]: + try: + is_socket_file = is_socket(f) + except OSError: + # An error has already occurred. Another error here is not + # a problem and we can ignore it. + pass + else: + if is_socket_file: + raise shutil.SpecialFileError("`%s` is a socket" % f) + + raise + + +def is_socket(path): + # type: (str) -> bool + return stat.S_ISSOCK(os.lstat(path).st_mode) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 6e1e3dd400b..3151b77bb30 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1,6 +1,7 @@ import distutils import glob import os +import shutil import sys import textwrap from os.path import curdir, join, pardir @@ -23,6 +24,7 @@ pyversion_tuple, requirements_file, ) +from tests.lib.filesystem import make_socket_file from tests.lib.local_repos import local_checkout from tests.lib.path import Path @@ -488,6 +490,29 @@ def test_install_from_local_directory_with_symlinks_to_directories( assert egg_info_folder in result.files_created, str(result) +@pytest.mark.skipif("sys.platform == 'win32' or sys.version_info < (3,)") +def test_install_from_local_directory_with_socket_file(script, data, tmpdir): + """ + Test installing from a local directory containing a socket file. + """ + egg_info_file = ( + script.site_packages / "FSPkg-0.1.dev0-py%s.egg-info" % pyversion + ) + package_folder = script.site_packages / "fspkg" + to_copy = data.packages.joinpath("FSPkg") + to_install = tmpdir.joinpath("src") + + shutil.copytree(to_copy, to_install) + # Socket file, should be ignored. + socket_file_path = os.path.join(to_install, "example") + make_socket_file(socket_file_path) + + result = script.pip("install", "--verbose", to_install, expect_error=False) + assert package_folder in result.files_created, str(result.stdout) + assert egg_info_file in result.files_created, str(result) + assert str(socket_file_path) in result.stderr + + def test_install_from_local_directory_with_no_setup_py(script, data): """ Test installing from a local directory with no 'setup.py'. diff --git a/tests/lib/filesystem.py b/tests/lib/filesystem.py new file mode 100644 index 00000000000..dc14b323e33 --- /dev/null +++ b/tests/lib/filesystem.py @@ -0,0 +1,48 @@ +"""Helpers for filesystem-dependent tests. +""" +import os +import socket +import subprocess +import sys +from functools import partial +from itertools import chain + +from .path import Path + + +def make_socket_file(path): + # Socket paths are limited to 108 characters (sometimes less) so we + # chdir before creating it and use a relative path name. + cwd = os.getcwd() + os.chdir(os.path.dirname(path)) + try: + sock = socket.socket(socket.AF_UNIX) + sock.bind(os.path.basename(path)) + finally: + os.chdir(cwd) + + +def make_unreadable_file(path): + Path(path).touch() + os.chmod(path, 0o000) + if sys.platform == "win32": + # Once we drop PY2 we can use `os.getlogin()` instead. + username = os.environ["USERNAME"] + # Remove "Read Data/List Directory" permission for current user, but + # leave everything else. + args = ["icacls", path, "/deny", username + ":(RD)"] + subprocess.check_call(args) + + +def get_filelist(base): + def join(dirpath, dirnames, filenames): + relative_dirpath = os.path.relpath(dirpath, base) + join_dirpath = partial(os.path.join, relative_dirpath) + return chain( + (join_dirpath(p) for p in dirnames), + (join_dirpath(p) for p in filenames), + ) + + return set(chain.from_iterable( + join(*dirinfo) for dirinfo in os.walk(base) + )) diff --git a/tests/unit/test_download.py b/tests/unit/test_download.py index f7595f52bcd..22eb9576d44 100644 --- a/tests/unit/test_download.py +++ b/tests/unit/test_download.py @@ -1,6 +1,7 @@ import functools import hashlib import os +import shutil import sys from io import BytesIO from shutil import copy, rmtree @@ -15,6 +16,7 @@ MultiDomainBasicAuth, PipSession, SafeFileCache, + _copy_source_tree, _download_http_url, _get_url_scheme, parse_content_disposition, @@ -28,6 +30,12 @@ from pip._internal.utils.hashes import Hashes from pip._internal.utils.misc import path_to_url from tests.lib import create_file +from tests.lib.filesystem import ( + get_filelist, + make_socket_file, + make_unreadable_file, +) +from tests.lib.path import Path @pytest.fixture(scope="function") @@ -334,6 +342,85 @@ def test_url_to_path_path_to_url_symmetry_win(): assert url_to_path(path_to_url(unc_path)) == unc_path +@pytest.fixture +def clean_project(tmpdir_factory, data): + tmpdir = Path(str(tmpdir_factory.mktemp("clean_project"))) + new_project_dir = tmpdir.joinpath("FSPkg") + path = data.packages.joinpath("FSPkg") + shutil.copytree(path, new_project_dir) + return new_project_dir + + +def test_copy_source_tree(clean_project, tmpdir): + target = tmpdir.joinpath("target") + expected_files = get_filelist(clean_project) + assert len(expected_files) == 3 + + _copy_source_tree(clean_project, target) + + copied_files = get_filelist(target) + assert expected_files == copied_files + + +@pytest.mark.skipif("sys.platform == 'win32' or sys.version_info < (3,)") +def test_copy_source_tree_with_socket(clean_project, tmpdir, caplog): + target = tmpdir.joinpath("target") + expected_files = get_filelist(clean_project) + socket_path = str(clean_project.joinpath("aaa")) + make_socket_file(socket_path) + + _copy_source_tree(clean_project, target) + + copied_files = get_filelist(target) + assert expected_files == copied_files + + # Warning should have been logged. + assert len(caplog.records) == 1 + record = caplog.records[0] + assert record.levelname == 'WARNING' + assert socket_path in record.message + + +@pytest.mark.skipif("sys.platform == 'win32' or sys.version_info < (3,)") +def test_copy_source_tree_with_socket_fails_with_no_socket_error( + clean_project, tmpdir +): + target = tmpdir.joinpath("target") + expected_files = get_filelist(clean_project) + make_socket_file(clean_project.joinpath("aaa")) + unreadable_file = clean_project.joinpath("bbb") + make_unreadable_file(unreadable_file) + + with pytest.raises(shutil.Error) as e: + _copy_source_tree(clean_project, target) + + errored_files = [err[0] for err in e.value.args[0]] + assert len(errored_files) == 1 + assert unreadable_file in errored_files + + copied_files = get_filelist(target) + # All files without errors should have been copied. + assert expected_files == copied_files + + +def test_copy_source_tree_with_unreadable_dir_fails(clean_project, tmpdir): + target = tmpdir.joinpath("target") + expected_files = get_filelist(clean_project) + unreadable_file = clean_project.joinpath("bbb") + make_unreadable_file(unreadable_file) + + with pytest.raises(shutil.Error) as e: + _copy_source_tree(clean_project, target) + + errored_files = [err[0] for err in e.value.args[0]] + assert len(errored_files) == 1 + assert unreadable_file in errored_files + + copied_files = get_filelist(target) + # All files without errors should have been copied. + assert expected_files == copied_files + + class Test_unpack_file_url(object): def prep(self, tmpdir, data): diff --git a/tests/unit/test_utils_filesystem.py b/tests/unit/test_utils_filesystem.py new file mode 100644 index 00000000000..3ef814dce4b --- /dev/null +++ b/tests/unit/test_utils_filesystem.py @@ -0,0 +1,61 @@ +import os +import shutil + +import pytest + +from pip._internal.utils.filesystem import copy2_fixed, is_socket +from tests.lib.filesystem import make_socket_file, make_unreadable_file +from tests.lib.path import Path + + +def make_file(path): + Path(path).touch() + + +def make_valid_symlink(path): + target = path + "1" + make_file(target) + os.symlink(target, path) + + +def make_broken_symlink(path): + os.symlink("foo", path) + + +def make_dir(path): + os.mkdir(path) + + +skip_on_windows = pytest.mark.skipif("sys.platform == 'win32'") + + +@skip_on_windows +@pytest.mark.parametrize("create,result", [ + (make_socket_file, True), + (make_file, False), + (make_valid_symlink, False), + (make_broken_symlink, False), + (make_dir, False), +]) +def test_is_socket(create, result, tmpdir): + target = tmpdir.joinpath("target") + create(target) + assert os.path.lexists(target) + assert is_socket(target) == result + + +@pytest.mark.parametrize("create,error_type", [ + pytest.param( + make_socket_file, shutil.SpecialFileError, marks=skip_on_windows + ), + (make_unreadable_file, OSError), +]) +def test_copy2_fixed_raises_appropriate_errors(create, error_type, tmpdir): + src = tmpdir.joinpath("src") + create(src) + dest = tmpdir.joinpath("dest") + + with pytest.raises(error_type): + copy2_fixed(src, dest) + + assert not dest.exists() diff --git a/tox.ini b/tox.ini index fc1f3bd98ce..608d491e3fe 100644 --- a/tox.ini +++ b/tox.ini @@ -10,7 +10,8 @@ envlist = pip = python {toxinidir}/tools/tox_pip.py [testenv] -passenv = CI GIT_SSL_CAINFO +# Remove USERNAME once we drop PY2. +passenv = CI GIT_SSL_CAINFO USERNAME setenv = # This is required in order to get UTF-8 output inside of the subprocesses # that our tests use. From a6cdb490b0d4f32fe9aa2f7100def1c808e6a283 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 20 Aug 2019 10:42:10 -0700 Subject: [PATCH 0137/3170] Move trusted_hosts logic to PipSession. --- src/pip/_internal/cli/req_command.py | 1 - src/pip/_internal/commands/list.py | 1 - src/pip/_internal/download.py | 136 +++++++++++++++++++++++++-- src/pip/_internal/index.py | 136 ++------------------------- src/pip/_internal/req/req_file.py | 2 +- src/pip/_internal/utils/outdated.py | 1 - tests/lib/__init__.py | 4 +- tests/unit/test_download.py | 101 ++++++++++++++++++++ tests/unit/test_index.py | 120 ----------------------- tests/unit/test_req_file.py | 5 +- tests/unit/test_unit_outdated.py | 2 +- 11 files changed, 243 insertions(+), 266 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index dc29f17434d..bc994738812 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -276,7 +276,6 @@ def _build_package_finder( return PackageFinder.create( search_scope=search_scope, selection_prefs=selection_prefs, - trusted_hosts=options.trusted_hosts, session=session, target_python=target_python, ) diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index aacd5680ca1..0498bb93c37 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -126,7 +126,6 @@ def _build_package_finder(self, options, session): return PackageFinder.create( search_scope=search_scope, selection_prefs=selection_prefs, - trusted_hosts=options.trusted_hosts, session=session, ) diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index bb9bd868580..c91d07997db 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -12,7 +12,7 @@ import sys from contextlib import contextmanager -from pip._vendor import requests, urllib3 +from pip._vendor import requests, six, urllib3 from pip._vendor.cachecontrol import CacheControlAdapter from pip._vendor.cachecontrol.caches import FileCache from pip._vendor.lockfile import LockError @@ -32,7 +32,7 @@ from pip._internal.exceptions import HashMismatch, InstallationError from pip._internal.models.index import PyPI # Import ssl from compat so the initial import occurs in only one place. -from pip._internal.utils.compat import HAS_TLS, ssl +from pip._internal.utils.compat import HAS_TLS, ipaddress, ssl from pip._internal.utils.encoding import auto_decode from pip._internal.utils.filesystem import check_path_owner, copy2_fixed from pip._internal.utils.glibc import libc_ver @@ -64,8 +64,9 @@ from pip._internal.vcs import vcs if MYPY_CHECK_RUNNING: + from logging import Logger from typing import ( - Callable, Dict, List, IO, Optional, Text, Tuple, Union + IO, Callable, Dict, Iterator, List, Optional, Text, Tuple, Union, ) from optparse import Values @@ -76,6 +77,7 @@ from pip._internal.vcs.versioncontrol import AuthInfo, VersionControl Credentials = Tuple[str, str, str] + SecureOrigin = Tuple[str, str, Optional[str]] if PY2: CopytreeKwargs = TypedDict( @@ -119,6 +121,20 @@ str(exc)) keyring = None + +SECURE_ORIGINS = [ + # protocol, hostname, port + # Taken from Chrome's list of secure origins (See: http://bit.ly/1qrySKC) + ("https", "*", "*"), + ("*", "localhost", "*"), + ("*", "127.0.0.0/8", "*"), + ("*", "::1/128", "*"), + ("file", "*", None), + # ssh is always secure. + ("ssh", "*", "*"), +] # type: List[SecureOrigin] + + # These are environment variables present when running under various # CI systems. For each variable, some CI systems that use the variable # are indicated. The collection was chosen so that for each of a number @@ -557,13 +573,21 @@ class PipSession(requests.Session): timeout = None # type: Optional[int] def __init__(self, *args, **kwargs): + """ + :param insecure_hosts: Domains not to emit warnings for when not using + HTTPS. + """ retries = kwargs.pop("retries", 0) cache = kwargs.pop("cache", None) - insecure_hosts = kwargs.pop("insecure_hosts", []) + insecure_hosts = kwargs.pop("insecure_hosts", []) # type: List[str] index_urls = kwargs.pop("index_urls", None) super(PipSession, self).__init__(*args, **kwargs) + # Namespace the attribute with "pip_" just in case to prevent + # possible conflicts with the base class. + self.pip_trusted_hosts = [] # type: List[str] + # Attach our User Agent to the request self.headers["User-Agent"] = user_agent() @@ -629,13 +653,26 @@ def __init__(self, *args, **kwargs): # Enable file:// urls self.mount("file://", LocalFSAdapter()) - # We want to use a non-validating adapter for any requests which are - # deemed insecure. for host in insecure_hosts: - self.add_insecure_host(host) + self.add_trusted_host(host, suppress_logging=True) + + def add_trusted_host(self, host, source=None, suppress_logging=False): + # type: (str, Optional[str], bool) -> None + """ + :param host: It is okay to provide a host that has previously been + added. + :param source: An optional source string, for logging where the host + string came from. + """ + if not suppress_logging: + msg = 'adding trusted host: {!r}'.format(host) + if source is not None: + msg += ' (from {})'.format(source) + logger.info(msg) + + if host not in self.pip_trusted_hosts: + self.pip_trusted_hosts.append(host) - def add_insecure_host(self, host): - # type: (str) -> None self.mount(build_url_from_netloc(host) + '/', self._insecure_adapter) if not netloc_has_port(host): # Mount wildcard ports for the same host. @@ -644,6 +681,87 @@ def add_insecure_host(self, host): self._insecure_adapter ) + def iter_secure_origins(self): + # type: () -> Iterator[SecureOrigin] + for secure_origin in SECURE_ORIGINS: + yield secure_origin + for host in self.pip_trusted_hosts: + yield ('*', host, '*') + + def is_secure_origin(self, logger, location): + # type: (Logger, Link) -> bool + # Determine if this url used a secure transport mechanism + parsed = urllib_parse.urlparse(str(location)) + origin = (parsed.scheme, parsed.hostname, parsed.port) + + # The protocol to use to see if the protocol matches. + # Don't count the repository type as part of the protocol: in + # cases such as "git+ssh", only use "ssh". (I.e., Only verify against + # the last scheme.) + protocol = origin[0].rsplit('+', 1)[-1] + + # Determine if our origin is a secure origin by looking through our + # hardcoded list of secure origins, as well as any additional ones + # configured on this PackageFinder instance. + for secure_origin in self.iter_secure_origins(): + if protocol != secure_origin[0] and secure_origin[0] != "*": + continue + + try: + # We need to do this decode dance to ensure that we have a + # unicode object, even on Python 2.x. + addr = ipaddress.ip_address( + origin[1] + if ( + isinstance(origin[1], six.text_type) or + origin[1] is None + ) + else origin[1].decode("utf8") + ) + network = ipaddress.ip_network( + secure_origin[1] + if isinstance(secure_origin[1], six.text_type) + # setting secure_origin[1] to proper Union[bytes, str] + # creates problems in other places + else secure_origin[1].decode("utf8") # type: ignore + ) + except ValueError: + # We don't have both a valid address or a valid network, so + # we'll check this origin against hostnames. + if (origin[1] and + origin[1].lower() != secure_origin[1].lower() and + secure_origin[1] != "*"): + continue + else: + # We have a valid address and network, so see if the address + # is contained within the network. + if addr not in network: + continue + + # Check to see if the port patches + if (origin[2] != secure_origin[2] and + secure_origin[2] != "*" and + secure_origin[2] is not None): + continue + + # If we've gotten here, then this origin matches the current + # secure origin and we should return True + return True + + # If we've gotten to this point, then the origin isn't secure and we + # will not accept it as a valid location to search. We will however + # 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 we " + "recommend you use HTTPS instead, otherwise you may silence " + "this warning and allow it anyway with '--trusted-host %s'.", + parsed.hostname, + parsed.hostname, + ) + + return False + def request(self, method, url, *args, **kwargs): # Allow setting a default timeout on a session kwargs.setdefault("timeout", self.timeout) diff --git a/src/pip/_internal/index.py b/src/pip/_internal/index.py index c5bc3bc3428..b782ff2b0da 100644 --- a/src/pip/_internal/index.py +++ b/src/pip/_internal/index.py @@ -12,7 +12,7 @@ import os import re -from pip._vendor import html5lib, requests, six +from pip._vendor import html5lib, requests from pip._vendor.distlib.compat import unescape from pip._vendor.packaging import specifiers from pip._vendor.packaging.utils import canonicalize_name @@ -33,7 +33,6 @@ from pip._internal.models.link import Link from pip._internal.models.selection_prefs import SelectionPreferences from pip._internal.models.target_python import TargetPython -from pip._internal.utils.compat import ipaddress from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import ( ARCHIVE_EXTENSIONS, @@ -47,10 +46,9 @@ from pip._internal.wheel import Wheel if MYPY_CHECK_RUNNING: - from logging import Logger from typing import ( - Any, Callable, FrozenSet, Iterable, Iterator, List, MutableMapping, - Optional, Sequence, Set, Text, Tuple, Union, + Any, Callable, FrozenSet, Iterable, List, MutableMapping, Optional, + Sequence, Set, Text, Tuple, Union, ) import xml.etree.ElementTree from pip._vendor.packaging.version import _BaseVersion @@ -66,25 +64,11 @@ Tuple[int, int, int, _BaseVersion, BuildTag, Optional[int]] ) HTMLElement = xml.etree.ElementTree.Element - SecureOrigin = Tuple[str, str, Optional[str]] __all__ = ['FormatControl', 'FoundCandidates', 'PackageFinder'] -SECURE_ORIGINS = [ - # protocol, hostname, port - # Taken from Chrome's list of secure origins (See: http://bit.ly/1qrySKC) - ("https", "*", "*"), - ("*", "localhost", "*"), - ("*", "127.0.0.0/8", "*"), - ("*", "::1/128", "*"), - ("file", "*", None), - # ssh is always secure. - ("ssh", "*", "*"), -] # type: List[SecureOrigin] - - logger = logging.getLogger(__name__) @@ -813,7 +797,6 @@ def __init__( target_python, # type: TargetPython allow_yanked, # type: bool format_control=None, # type: Optional[FormatControl] - trusted_hosts=None, # type: Optional[List[str]] candidate_prefs=None, # type: CandidatePreferences ignore_requires_python=None, # type: Optional[bool] ): @@ -829,8 +812,6 @@ def __init__( :param candidate_prefs: Options to use when creating a CandidateEvaluator object. """ - if trusted_hosts is None: - trusted_hosts = [] if candidate_prefs is None: candidate_prefs = CandidatePreferences() @@ -844,7 +825,6 @@ def __init__( self.search_scope = search_scope self.session = session self.format_control = format_control - self.trusted_hosts = trusted_hosts # These are boring links that have already been logged somehow. self._logged_links = set() # type: Set[Link] @@ -858,7 +838,6 @@ def create( cls, search_scope, # type: SearchScope selection_prefs, # type: SelectionPreferences - trusted_hosts=None, # type: Optional[List[str]] session=None, # type: Optional[PipSession] target_python=None, # type: Optional[TargetPython] ): @@ -867,8 +846,6 @@ def create( :param selection_prefs: The candidate selection preferences, as a SelectionPreferences object. - :param trusted_hosts: Domains not to emit warnings for when not using - HTTPS. :param session: The Session to use to make requests. :param target_python: The target Python interpreter to use when checking compatibility. If None (the default), a TargetPython @@ -894,7 +871,6 @@ def create( target_python=target_python, allow_yanked=selection_prefs.allow_yanked, format_control=selection_prefs.format_control, - trusted_hosts=trusted_hosts, ignore_requires_python=selection_prefs.ignore_requires_python, ) @@ -908,6 +884,11 @@ def index_urls(self): # type: () -> List[str] return self.search_scope.index_urls + @property + def trusted_hosts(self): + # type: () -> Iterable[str] + return iter(self.session.pip_trusted_hosts) + @property def allow_all_prereleases(self): # type: () -> bool @@ -917,31 +898,6 @@ def set_allow_all_prereleases(self): # type: () -> None self._candidate_prefs.allow_all_prereleases = True - def add_trusted_host(self, host, source=None): - # type: (str, Optional[str]) -> None - """ - :param source: An optional source string, for logging where the host - string came from. - """ - # It is okay to add a previously added host because PipSession stores - # the resulting prefixes in a dict. - msg = 'adding trusted host: {!r}'.format(host) - if source is not None: - msg += ' (from {})'.format(source) - logger.info(msg) - self.session.add_insecure_host(host) - if host in self.trusted_hosts: - return - - self.trusted_hosts.append(host) - - def iter_secure_origins(self): - # type: () -> Iterator[SecureOrigin] - for secure_origin in SECURE_ORIGINS: - yield secure_origin - for host in self.trusted_hosts: - yield ('*', host, '*') - @staticmethod def _sort_locations(locations, expand_dir=False): # type: (Sequence[str], bool) -> Tuple[List[str], List[str]] @@ -1000,80 +956,6 @@ def sort_path(path): return files, urls - def _validate_secure_origin(self, logger, location): - # type: (Logger, Link) -> bool - # Determine if this url used a secure transport mechanism - parsed = urllib_parse.urlparse(str(location)) - origin = (parsed.scheme, parsed.hostname, parsed.port) - - # The protocol to use to see if the protocol matches. - # Don't count the repository type as part of the protocol: in - # cases such as "git+ssh", only use "ssh". (I.e., Only verify against - # the last scheme.) - protocol = origin[0].rsplit('+', 1)[-1] - - # Determine if our origin is a secure origin by looking through our - # hardcoded list of secure origins, as well as any additional ones - # configured on this PackageFinder instance. - for secure_origin in self.iter_secure_origins(): - if protocol != secure_origin[0] and secure_origin[0] != "*": - continue - - try: - # We need to do this decode dance to ensure that we have a - # unicode object, even on Python 2.x. - addr = ipaddress.ip_address( - origin[1] - if ( - isinstance(origin[1], six.text_type) or - origin[1] is None - ) - else origin[1].decode("utf8") - ) - network = ipaddress.ip_network( - secure_origin[1] - if isinstance(secure_origin[1], six.text_type) - # setting secure_origin[1] to proper Union[bytes, str] - # creates problems in other places - else secure_origin[1].decode("utf8") # type: ignore - ) - except ValueError: - # We don't have both a valid address or a valid network, so - # we'll check this origin against hostnames. - if (origin[1] and - origin[1].lower() != secure_origin[1].lower() and - secure_origin[1] != "*"): - continue - else: - # We have a valid address and network, so see if the address - # is contained within the network. - if addr not in network: - continue - - # Check to see if the port patches - if (origin[2] != secure_origin[2] and - secure_origin[2] != "*" and - secure_origin[2] is not None): - continue - - # If we've gotten here, then this origin matches the current - # secure origin and we should return True - return True - - # If we've gotten to this point, then the origin isn't secure and we - # will not accept it as a valid location to search. We will however - # 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 we " - "recommend you use HTTPS instead, otherwise you may silence " - "this warning and allow it anyway with '--trusted-host %s'.", - parsed.hostname, - parsed.hostname, - ) - - return False - def make_link_evaluator(self, project_name): # type: (str) -> LinkEvaluator canonical_name = canonicalize_name(project_name) @@ -1117,7 +999,7 @@ def find_all_candidates(self, project_name): (Link(url) for url in index_url_loc), (Link(url) for url in fl_url_loc), ) - if self._validate_secure_origin(logger, link) + if self.session.is_secure_origin(logger, link) ] logger.debug('%d location(s) to search for versions of %s:', diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index 65772b20a83..1e4aa689d57 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -272,7 +272,7 @@ def process_line( finder.set_allow_all_prereleases() for host in opts.trusted_hosts or []: source = 'line {} of {}'.format(line_number, filename) - finder.add_trusted_host(host, source=source) + session.add_trusted_host(host, source=source) def break_args_options(line): diff --git a/src/pip/_internal/utils/outdated.py b/src/pip/_internal/utils/outdated.py index 2b10aeff6bb..f08434ab3c5 100644 --- a/src/pip/_internal/utils/outdated.py +++ b/src/pip/_internal/utils/outdated.py @@ -136,7 +136,6 @@ def pip_version_check(session, options): finder = PackageFinder.create( search_scope=search_scope, selection_prefs=selection_prefs, - trusted_hosts=options.trusted_hosts, session=session, ) candidate = finder.find_candidates("pip").get_best() diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index 846a0b530cb..e895c7c1c3f 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -23,7 +23,7 @@ from tests.lib.path import Path, curdir if MYPY_CHECK_RUNNING: - from typing import Iterable, List, Optional + from typing import List, Optional from pip._internal.models.target_python import TargetPython @@ -84,7 +84,6 @@ def make_test_finder( find_links=None, # type: Optional[List[str]] index_urls=None, # type: Optional[List[str]] allow_all_prereleases=False, # type: bool - trusted_hosts=None, # type: Optional[Iterable[str]] session=None, # type: Optional[PipSession] target_python=None, # type: Optional[TargetPython] ): @@ -111,7 +110,6 @@ def make_test_finder( return PackageFinder.create( search_scope=search_scope, selection_prefs=selection_prefs, - trusted_hosts=trusted_hosts, session=session, target_python=target_python, ) diff --git a/tests/unit/test_download.py b/tests/unit/test_download.py index 22eb9576d44..40229f9314d 100644 --- a/tests/unit/test_download.py +++ b/tests/unit/test_download.py @@ -1,5 +1,6 @@ import functools import hashlib +import logging import os import shutil import sys @@ -626,6 +627,106 @@ def test_insecure_host_adapter(self, tmpdir): # Check that the cache isn't enabled. assert not hasattr(session.adapters["https://example.com/"], "cache") + def test_add_trusted_host(self): + # Leave a gap to test how the ordering is affected. + trusted_hosts = ['host1', 'host3'] + session = PipSession(insecure_hosts=trusted_hosts) + insecure_adapter = session._insecure_adapter + prefix2 = 'https://host2/' + prefix3 = 'https://host3/' + + # Confirm some initial conditions as a baseline. + assert session.pip_trusted_hosts == ['host1', 'host3'] + assert session.adapters[prefix3] is insecure_adapter + assert prefix2 not in session.adapters + + # Test adding a new host. + session.add_trusted_host('host2') + assert session.pip_trusted_hosts == ['host1', 'host3', 'host2'] + # Check that prefix3 is still present. + assert session.adapters[prefix3] is insecure_adapter + assert session.adapters[prefix2] is insecure_adapter + + # Test that adding the same host doesn't create a duplicate. + session.add_trusted_host('host3') + assert session.pip_trusted_hosts == ['host1', 'host3', 'host2'], ( + 'actual: {}'.format(session.pip_trusted_hosts) + ) + + def test_add_trusted_host__logging(self, caplog): + """ + Test logging when add_trusted_host() is called. + """ + trusted_hosts = ['host0', 'host1'] + session = PipSession(insecure_hosts=trusted_hosts) + with caplog.at_level(logging.INFO): + # Test adding an existing host. + session.add_trusted_host('host1', source='somewhere') + session.add_trusted_host('host2') + # Test calling add_trusted_host() on the same host twice. + session.add_trusted_host('host2') + + actual = [(r.levelname, r.message) for r in caplog.records] + # Observe that "host0" isn't included in the logs. + expected = [ + ('INFO', "adding trusted host: 'host1' (from somewhere)"), + ('INFO', "adding trusted host: 'host2'"), + ('INFO', "adding trusted host: 'host2'"), + ] + assert actual == expected + + def test_iter_secure_origins(self): + trusted_hosts = ['host1', 'host2'] + session = PipSession(insecure_hosts=trusted_hosts) + + actual = list(session.iter_secure_origins()) + assert len(actual) == 8 + # Spot-check that SECURE_ORIGINS is included. + assert actual[0] == ('https', '*', '*') + assert actual[-2:] == [ + ('*', 'host1', '*'), + ('*', 'host2', '*'), + ] + + def test_iter_secure_origins__insecure_hosts_empty(self): + """ + Test iter_secure_origins() after passing insecure_hosts=[]. + """ + session = PipSession(insecure_hosts=[]) + + actual = list(session.iter_secure_origins()) + assert len(actual) == 6 + # Spot-check that SECURE_ORIGINS is included. + assert actual[0] == ('https', '*', '*') + + @pytest.mark.parametrize( + ("location", "trusted", "expected"), + [ + ("http://pypi.org/something", [], True), + ("https://pypi.org/something", [], False), + ("git+http://pypi.org/something", [], True), + ("git+https://pypi.org/something", [], False), + ("git+ssh://git@pypi.org/something", [], False), + ("http://localhost", [], False), + ("http://127.0.0.1", [], False), + ("http://example.com/something/", [], True), + ("http://example.com/something/", ["example.com"], False), + ("http://eXample.com/something/", ["example.cOm"], False), + ], + ) + def test_secure_origin(self, location, trusted, expected): + class MockLogger(object): + def __init__(self): + self.called = False + + def warning(self, *args, **kwargs): + self.called = True + + session = PipSession(insecure_hosts=trusted) + logger = MockLogger() + session.is_secure_origin(logger, location) + assert logger.called == expected + @pytest.mark.parametrize(["input_url", "url", "username", "password"], [ ( diff --git a/tests/unit/test_index.py b/tests/unit/test_index.py index 4d7b5933117..bcb50a55622 100644 --- a/tests/unit/test_index.py +++ b/tests/unit/test_index.py @@ -642,96 +642,6 @@ def test_create__format_control(self): # Check that the attributes weren't reset. assert actual_format_control.only_binary == {':all:'} - def test_add_trusted_host(self): - # Leave a gap to test how the ordering is affected. - trusted_hosts = ['host1', 'host3'] - session = PipSession(insecure_hosts=trusted_hosts) - finder = make_test_finder( - session=session, - trusted_hosts=trusted_hosts, - ) - insecure_adapter = session._insecure_adapter - prefix2 = 'https://host2/' - prefix3 = 'https://host3/' - - # Confirm some initial conditions as a baseline. - assert finder.trusted_hosts == ['host1', 'host3'] - assert session.adapters[prefix3] is insecure_adapter - assert prefix2 not in session.adapters - - # Test adding a new host. - finder.add_trusted_host('host2') - assert finder.trusted_hosts == ['host1', 'host3', 'host2'] - # Check that prefix3 is still present. - assert session.adapters[prefix3] is insecure_adapter - assert session.adapters[prefix2] is insecure_adapter - - # Test that adding the same host doesn't create a duplicate. - finder.add_trusted_host('host3') - assert finder.trusted_hosts == ['host1', 'host3', 'host2'], ( - 'actual: {}'.format(finder.trusted_hosts) - ) - - def test_add_trusted_host__logging(self, caplog): - """ - Test logging when add_trusted_host() is called. - """ - trusted_hosts = ['host1'] - session = PipSession(insecure_hosts=trusted_hosts) - finder = make_test_finder( - session=session, - trusted_hosts=trusted_hosts, - ) - with caplog.at_level(logging.INFO): - # Test adding an existing host. - finder.add_trusted_host('host1', source='somewhere') - finder.add_trusted_host('host2') - # Test calling add_trusted_host() on the same host twice. - finder.add_trusted_host('host2') - - actual = [(r.levelname, r.message) for r in caplog.records] - expected = [ - ('INFO', "adding trusted host: 'host1' (from somewhere)"), - ('INFO', "adding trusted host: 'host2'"), - ('INFO', "adding trusted host: 'host2'"), - ] - assert actual == expected - - def test_iter_secure_origins(self): - trusted_hosts = ['host1', 'host2'] - finder = make_test_finder(trusted_hosts=trusted_hosts) - - actual = list(finder.iter_secure_origins()) - assert len(actual) == 8 - # Spot-check that SECURE_ORIGINS is included. - assert actual[0] == ('https', '*', '*') - assert actual[-2:] == [ - ('*', 'host1', '*'), - ('*', 'host2', '*'), - ] - - def test_iter_secure_origins__none_trusted_hosts(self): - """ - Test iter_secure_origins() after passing trusted_hosts=None. - """ - # Use PackageFinder.create() rather than make_test_finder() - # to make sure we're really passing trusted_hosts=None. - search_scope = SearchScope([], []) - selection_prefs = SelectionPreferences( - allow_yanked=True, - ) - finder = PackageFinder.create( - search_scope=search_scope, - selection_prefs=selection_prefs, - trusted_hosts=None, - session=object(), - ) - - actual = list(finder.iter_secure_origins()) - assert len(actual) == 6 - # Spot-check that SECURE_ORIGINS is included. - assert actual[0] == ('https', '*', '*') - @pytest.mark.parametrize( 'allow_yanked, ignore_requires_python, only_binary, expected_formats', [ @@ -875,36 +785,6 @@ def test_determine_base_url(html, url, expected): assert _determine_base_url(document, url) == expected -class MockLogger(object): - def __init__(self): - self.called = False - - def warning(self, *args, **kwargs): - self.called = True - - -@pytest.mark.parametrize( - ("location", "trusted", "expected"), - [ - ("http://pypi.org/something", [], True), - ("https://pypi.org/something", [], False), - ("git+http://pypi.org/something", [], True), - ("git+https://pypi.org/something", [], False), - ("git+ssh://git@pypi.org/something", [], False), - ("http://localhost", [], False), - ("http://127.0.0.1", [], False), - ("http://example.com/something/", [], True), - ("http://example.com/something/", ["example.com"], False), - ("http://eXample.com/something/", ["example.cOm"], False), - ], -) -def test_secure_origin(location, trusted, expected): - finder = make_test_finder(trusted_hosts=trusted) - logger = MockLogger() - finder._validate_secure_origin(logger, location) - assert logger.called == expected - - @pytest.mark.parametrize( ("fragment", "canonical_name", "expected"), [ diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py index 3ebb55d3cfc..a1153270759 100644 --- a/tests/unit/test_req_file.py +++ b/tests/unit/test_req_file.py @@ -342,12 +342,13 @@ def test_set_finder_extra_index_urls(self, finder): list(process_line("--extra-index-url=url", "file", 1, finder=finder)) assert finder.index_urls == ['url'] - def test_set_finder_trusted_host(self, caplog, finder): + def test_set_finder_trusted_host(self, caplog, session, finder): with caplog.at_level(logging.INFO): list(process_line( "--trusted-host=host", "file.txt", 1, finder=finder, + session=session, )) - assert finder.trusted_hosts == ['host'] + assert list(finder.trusted_hosts) == ['host'] session = finder.session assert session.adapters['https://host/'] is session._insecure_adapter diff --git a/tests/unit/test_unit_outdated.py b/tests/unit/test_unit_outdated.py index a5d37f81868..a1931eb42a2 100644 --- a/tests/unit/test_unit_outdated.py +++ b/tests/unit/test_unit_outdated.py @@ -59,7 +59,7 @@ def _options(): ''' Some default options that we pass to outdated.pip_version_check ''' return pretend.stub( find_links=[], index_url='default_url', extra_index_urls=[], - no_index=False, pre=False, trusted_hosts=False, cache_dir='', + no_index=False, pre=False, cache_dir='', ) From 305c671d1fb5068b53454df79667d4fd8d172234 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Tue, 20 Aug 2019 12:40:35 +0530 Subject: [PATCH 0138/3170] Add RequirementSet.add_unnamed_requirement() --- src/pip/_internal/req/req_set.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/req/req_set.py b/src/pip/_internal/req/req_set.py index cd51d17250e..118d92f9a79 100644 --- a/src/pip/_internal/req/req_set.py +++ b/src/pip/_internal/req/req_set.py @@ -52,6 +52,11 @@ def __repr__(self): return ('<%s object; %d requirement(s): %s>' % (self.__class__.__name__, len(reqs), reqs_str)) + def add_unnamed_requirement(self, install_req): + # type: (InstallRequirement) -> None + assert not install_req.name + self.unnamed_requirements.append(install_req) + def add_requirement( self, install_req, # type: InstallRequirement @@ -105,8 +110,7 @@ def add_requirement( # Unnamed requirements are scanned again and the requirement won't be # added as a dependency until after scanning. if not name: - # url or path requirement w/o an egg fragment - self.unnamed_requirements.append(install_req) + self.add_unnamed_requirement(install_req) return [install_req], None try: From 476dfd2a8cef09f8e661682518857776ab42d15b Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Wed, 21 Aug 2019 15:44:48 +0530 Subject: [PATCH 0139/3170] Add RequirementSet.add_named_requirement() --- src/pip/_internal/req/req_set.py | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/req/req_set.py b/src/pip/_internal/req/req_set.py index 118d92f9a79..269a045d592 100644 --- a/src/pip/_internal/req/req_set.py +++ b/src/pip/_internal/req/req_set.py @@ -57,6 +57,16 @@ def add_unnamed_requirement(self, install_req): assert not install_req.name self.unnamed_requirements.append(install_req) + def add_named_requirement(self, install_req): + # type: (InstallRequirement) -> None + assert install_req.name + name = install_req.name + + self.requirements[name] = install_req + # FIXME: what about other normalizations? E.g., _ vs. -? + if name.lower() != name: + self.requirement_aliases[name.lower()] = name + def add_requirement( self, install_req, # type: InstallRequirement @@ -134,11 +144,8 @@ def add_requirement( # When no existing requirement exists, add the requirement as a # dependency and it will be scanned again after. if not existing_req: - self.requirements[name] = install_req - # FIXME: what about other normalizations? E.g., _ vs. -? - if name.lower() != name: - self.requirement_aliases[name.lower()] = name - # We'd want to rescan this requirements later + self.add_named_requirement(install_req) + # We'd want to rescan this requirement later return [install_req], install_req # Assume there's no need to scan, and that we've already From e888587c68080cb85bb065837c80940804b02c70 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 21 Aug 2019 08:18:58 +0530 Subject: [PATCH 0140/3170] Note that pip's internals can change at any time --- docs/html/development/architecture/index.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/html/development/architecture/index.rst b/docs/html/development/architecture/index.rst index f3f221565c4..7d50d998caf 100644 --- a/docs/html/development/architecture/index.rst +++ b/docs/html/development/architecture/index.rst @@ -8,8 +8,9 @@ Architecture of pip's internals interested in helping out, please let us know in the `tracking issue`_. .. note:: - Direct use of pip's internals is *not supported*. - For more details, see :ref:`Using pip from your program`. + Direct use of pip's internals is *not supported*, and these internals + can change at any time. For more details, see :ref:`Using pip from + your program`. .. toctree:: From 7783c4713183dee59c3f91a15785db30bae69946 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 21 Aug 2019 03:22:57 -0700 Subject: [PATCH 0141/3170] Hide security-sensitive strings in VCS command log messages (#6890) --- news/6890.bugfix | 2 + src/pip/_internal/download.py | 5 +- src/pip/_internal/req/req_install.py | 7 +- src/pip/_internal/utils/misc.py | 99 +++++++++++++++++++++++-- src/pip/_internal/vcs/bazaar.py | 26 ++++--- src/pip/_internal/vcs/git.py | 35 +++++---- src/pip/_internal/vcs/mercurial.py | 25 ++++--- src/pip/_internal/vcs/subversion.py | 43 ++++++----- src/pip/_internal/vcs/versioncontrol.py | 46 +++++++----- tests/functional/test_vcs_bazaar.py | 5 +- tests/lib/local_repos.py | 4 +- tests/unit/test_utils.py | 74 ++++++++++++++++++ tests/unit/test_vcs.py | 80 +++++++++++--------- tests/unit/test_vcs_mercurial.py | 3 +- 14 files changed, 332 insertions(+), 122 deletions(-) create mode 100644 news/6890.bugfix diff --git a/news/6890.bugfix b/news/6890.bugfix new file mode 100644 index 00000000000..3da0d5bb2fa --- /dev/null +++ b/news/6890.bugfix @@ -0,0 +1,2 @@ +Hide security-sensitive strings like passwords in log messages related to +version control system (aka VCS) command invocations. diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index bb9bd868580..cf8cbe9d501 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -49,6 +49,7 @@ display_path, format_size, get_installed_version, + hide_url, netloc_has_port, path_to_display, path_to_url, @@ -755,8 +756,10 @@ def is_archive_file(name): def unpack_vcs_link(link, location): + # type: (Link, str) -> None vcs_backend = _get_used_vcs_backend(link) - vcs_backend.unpack(location, url=link.url) + assert vcs_backend is not None + vcs_backend.unpack(location, url=hide_url(link.url)) def _get_used_vcs_backend(link): diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index d57804da188..264fade4cfa 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -38,6 +38,7 @@ dist_in_usersite, ensure_dir, get_installed_version, + hide_url, redact_password_from_url, rmtree, ) @@ -813,11 +814,11 @@ def update_editable(self, obtain=True): vc_type, url = self.link.url.split('+', 1) vcs_backend = vcs.get_backend(vc_type) if vcs_backend: - url = self.link.url + hidden_url = hide_url(self.link.url) if obtain: - vcs_backend.obtain(self.source_dir, url=url) + vcs_backend.obtain(self.source_dir, url=hidden_url) else: - vcs_backend.export(self.source_dir, url=url) + vcs_backend.export(self.source_dir, url=hidden_url) else: assert 0, ( 'Unexpected version control type (in %s): %s' diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index ff35c8346da..bade221ce52 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -65,6 +65,7 @@ from pip._internal.utils.ui import SpinnerInterface VersionInfo = Tuple[int, int, int] + CommandArgs = List[Union[str, 'HiddenText']] else: # typing's cast() is needed at runtime, but we don't want to import typing. # Thus, we use a dummy no-op version, which we tell mypy to ignore. @@ -753,8 +754,8 @@ def unpack_file( is_svn_page(file_contents(filename))): # We don't really care about this from pip._internal.vcs.subversion import Subversion - url = 'svn+' + link.url - Subversion().unpack(location, url=url) + hidden_url = hide_url('svn+' + link.url) + Subversion().unpack(location, url=hidden_url) else: # FIXME: handle? # FIXME: magic signatures? @@ -768,16 +769,52 @@ def unpack_file( ) +def make_command(*args): + # type: (Union[str, HiddenText, CommandArgs]) -> CommandArgs + """ + Create a CommandArgs object. + """ + command_args = [] # type: CommandArgs + for arg in args: + # Check for list instead of CommandArgs since CommandArgs is + # only known during type-checking. + if isinstance(arg, list): + command_args.extend(arg) + else: + # Otherwise, arg is str or HiddenText. + command_args.append(arg) + + return command_args + + def format_command_args(args): - # type: (List[str]) -> str + # type: (Union[List[str], CommandArgs]) -> str """ Format command arguments for display. """ - return ' '.join(shlex_quote(arg) for arg in args) + # For HiddenText arguments, display the redacted form by calling str(). + # Also, we don't apply str() to arguments that aren't HiddenText since + # this can trigger a UnicodeDecodeError in Python 2 if the argument + # has type unicode and includes a non-ascii character. (The type + # checker doesn't ensure the annotations are correct in all cases.) + return ' '.join( + shlex_quote(str(arg)) if isinstance(arg, HiddenText) + else shlex_quote(arg) for arg in args + ) + + +def reveal_command_args(args): + # type: (Union[List[str], CommandArgs]) -> List[str] + """ + Return the arguments in their raw, unredacted form. + """ + return [ + arg.secret if isinstance(arg, HiddenText) else arg for arg in args + ] def make_subprocess_output_error( - cmd_args, # type: List[str] + cmd_args, # type: Union[List[str], CommandArgs] cwd, # type: Optional[str] lines, # type: List[Text] exit_status, # type: int @@ -819,7 +856,7 @@ def make_subprocess_output_error( def call_subprocess( - cmd, # type: List[str] + cmd, # type: Union[List[str], CommandArgs] show_stdout=False, # type: bool cwd=None, # type: Optional[str] on_returncode='raise', # type: str @@ -886,7 +923,9 @@ def call_subprocess( env.pop(name, None) try: proc = subprocess.Popen( - cmd, stderr=subprocess.STDOUT, stdin=subprocess.PIPE, + # Convert HiddenText objects to the underlying str. + reveal_command_args(cmd), + stderr=subprocess.STDOUT, stdin=subprocess.PIPE, stdout=subprocess.PIPE, cwd=cwd, env=env, ) proc.stdin.close() @@ -1203,6 +1242,52 @@ def redact_password_from_url(url): return _transform_url(url, _redact_netloc)[0] +class HiddenText(object): + def __init__( + self, + secret, # type: str + redacted, # type: str + ): + # type: (...) -> None + self.secret = secret + self.redacted = redacted + + def __repr__(self): + # type: (...) -> str + return ''.format(str(self)) + + def __str__(self): + # type: (...) -> str + return self.redacted + + # This is useful for testing. + def __eq__(self, other): + # type: (Any) -> bool + if type(self) != type(other): + return False + + # The string being used for redaction doesn't also have to match, + # just the raw, original string. + return (self.secret == other.secret) + + # We need to provide an explicit __ne__ implementation for Python 2. + # TODO: remove this when we drop PY2 support. + def __ne__(self, other): + # type: (Any) -> bool + return not self == other + + +def hide_value(value): + # type: (str) -> HiddenText + return HiddenText(value, redacted='****') + + +def hide_url(url): + # type: (str) -> HiddenText + redacted = redact_password_from_url(url) + return HiddenText(url, redacted=redacted) + + def protect_pip_from_modification_on_windows(modifying_pip): """Protection of pip.exe from modification on Windows diff --git a/src/pip/_internal/vcs/bazaar.py b/src/pip/_internal/vcs/bazaar.py index 61b7f41e408..c49eba50e53 100644 --- a/src/pip/_internal/vcs/bazaar.py +++ b/src/pip/_internal/vcs/bazaar.py @@ -5,12 +5,18 @@ from pip._vendor.six.moves.urllib import parse as urllib_parse -from pip._internal.utils.misc import display_path, path_to_url, rmtree +from pip._internal.utils.misc import ( + display_path, + make_command, + path_to_url, + rmtree, +) from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.vcs.versioncontrol import VersionControl, vcs if MYPY_CHECK_RUNNING: from typing import Optional, Tuple + from pip._internal.utils.misc import HiddenText from pip._internal.vcs.versioncontrol import AuthInfo, RevOptions @@ -38,7 +44,7 @@ def get_base_rev_args(rev): return ['-r', rev] def export(self, location, url): - # type: (str, str) -> None + # type: (str, HiddenText) -> None """ Export the Bazaar repository at the url to the destination location """ @@ -48,12 +54,12 @@ def export(self, location, url): url, rev_options = self.get_url_rev_options(url) self.run_command( - ['export', location, url] + rev_options.to_args(), + make_command('export', location, url, rev_options.to_args()), show_stdout=False, ) def fetch_new(self, dest, url, rev_options): - # type: (str, str, RevOptions) -> None + # type: (str, HiddenText, RevOptions) -> None rev_display = rev_options.to_display() logger.info( 'Checking out %s%s to %s', @@ -61,16 +67,18 @@ def fetch_new(self, dest, url, rev_options): rev_display, display_path(dest), ) - cmd_args = ['branch', '-q'] + rev_options.to_args() + [url, dest] + cmd_args = ( + make_command('branch', '-q', rev_options.to_args(), url, dest) + ) self.run_command(cmd_args) def switch(self, dest, url, rev_options): - # type: (str, str, RevOptions) -> None - self.run_command(['switch', url], cwd=dest) + # type: (str, HiddenText, RevOptions) -> None + self.run_command(make_command('switch', url), cwd=dest) def update(self, dest, url, rev_options): - # type: (str, str, RevOptions) -> None - cmd_args = ['pull', '-q'] + rev_options.to_args() + # type: (str, HiddenText, RevOptions) -> None + cmd_args = make_command('pull', '-q', rev_options.to_args()) self.run_command(cmd_args, cwd=dest) @classmethod diff --git a/src/pip/_internal/vcs/git.py b/src/pip/_internal/vcs/git.py index 89b25782bf7..65069af7b7b 100644 --- a/src/pip/_internal/vcs/git.py +++ b/src/pip/_internal/vcs/git.py @@ -10,7 +10,7 @@ from pip._internal.exceptions import BadCommand from pip._internal.utils.compat import samefile -from pip._internal.utils.misc import display_path, redact_password_from_url +from pip._internal.utils.misc import display_path, make_command from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.vcs.versioncontrol import ( @@ -21,6 +21,7 @@ if MYPY_CHECK_RUNNING: from typing import Optional, Tuple + from pip._internal.utils.misc import HiddenText from pip._internal.vcs.versioncontrol import AuthInfo, RevOptions @@ -89,7 +90,7 @@ def get_current_branch(cls, location): return None def export(self, location, url): - # type: (str, str) -> None + # type: (str, HiddenText) -> None """Export the Git repository at the url to the destination location""" if not location.endswith('/'): location = location + '/' @@ -138,7 +139,7 @@ def get_revision_sha(cls, dest, rev): @classmethod def resolve_revision(cls, dest, url, rev_options): - # type: (str, str, RevOptions) -> RevOptions + # type: (str, HiddenText, RevOptions) -> RevOptions """ Resolve a revision to a new RevOptions object with the SHA1 of the branch, tag, or ref if found. @@ -172,7 +173,7 @@ def resolve_revision(cls, dest, url, rev_options): # If it looks like a ref, we have to fetch it explicitly. cls.run_command( - ['fetch', '-q', url] + rev_options.to_args(), + make_command('fetch', '-q', url, rev_options.to_args()), cwd=dest, ) # Change the revision to the SHA of the ref we fetched @@ -197,13 +198,10 @@ def is_commit_id_equal(cls, dest, name): return cls.get_revision(dest) == name def fetch_new(self, dest, url, rev_options): - # type: (str, str, RevOptions) -> None + # type: (str, HiddenText, RevOptions) -> None rev_display = rev_options.to_display() - logger.info( - 'Cloning %s%s to %s', redact_password_from_url(url), - rev_display, display_path(dest), - ) - self.run_command(['clone', '-q', url, dest]) + logger.info('Cloning %s%s to %s', url, rev_display, display_path(dest)) + self.run_command(make_command('clone', '-q', url, dest)) if rev_options.rev: # Then a specific revision was requested. @@ -213,7 +211,9 @@ def fetch_new(self, dest, url, 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): - cmd_args = ['checkout', '-q'] + rev_options.to_args() + cmd_args = make_command( + 'checkout', '-q', rev_options.to_args(), + ) self.run_command(cmd_args, cwd=dest) elif self.get_current_branch(dest) != branch_name: # Then a specific branch was requested, and that branch @@ -228,15 +228,18 @@ def fetch_new(self, dest, url, rev_options): self.update_submodules(dest) def switch(self, dest, url, rev_options): - # type: (str, str, RevOptions) -> None - self.run_command(['config', 'remote.origin.url', url], cwd=dest) - cmd_args = ['checkout', '-q'] + rev_options.to_args() + # type: (str, HiddenText, RevOptions) -> None + self.run_command( + make_command('config', 'remote.origin.url', url), + cwd=dest, + ) + cmd_args = make_command('checkout', '-q', rev_options.to_args()) self.run_command(cmd_args, cwd=dest) self.update_submodules(dest) def update(self, dest, url, rev_options): - # type: (str, str, RevOptions) -> None + # type: (str, HiddenText, RevOptions) -> None # First fetch changes from the default remote if self.get_git_version() >= parse_version('1.9.0'): # fetch tags in addition to everything else @@ -245,7 +248,7 @@ def update(self, dest, url, rev_options): self.run_command(['fetch', '-q'], cwd=dest) # Then reset to wanted revision (maybe even origin/master) rev_options = self.resolve_revision(dest, url, rev_options) - cmd_args = ['reset', '--hard', '-q'] + rev_options.to_args() + cmd_args = make_command('reset', '--hard', '-q', rev_options.to_args()) self.run_command(cmd_args, cwd=dest) #: update submodules self.update_submodules(dest) diff --git a/src/pip/_internal/vcs/mercurial.py b/src/pip/_internal/vcs/mercurial.py index 18c779934e8..21697ff1584 100644 --- a/src/pip/_internal/vcs/mercurial.py +++ b/src/pip/_internal/vcs/mercurial.py @@ -5,12 +5,13 @@ from pip._vendor.six.moves import configparser -from pip._internal.utils.misc import display_path, path_to_url +from pip._internal.utils.misc import display_path, make_command, path_to_url from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.vcs.versioncontrol import VersionControl, vcs if MYPY_CHECK_RUNNING: + from pip._internal.utils.misc import HiddenText from pip._internal.vcs.versioncontrol import RevOptions @@ -28,7 +29,7 @@ def get_base_rev_args(rev): return [rev] def export(self, location, url): - # type: (str, str) -> None + # type: (str, HiddenText) -> None """Export the Hg repository at the url to the destination location""" with TempDirectory(kind="export") as temp_dir: self.unpack(temp_dir.path, url=url) @@ -38,7 +39,7 @@ def export(self, location, url): ) def fetch_new(self, dest, url, rev_options): - # type: (str, str, RevOptions) -> None + # type: (str, HiddenText, RevOptions) -> None rev_display = rev_options.to_display() logger.info( 'Cloning hg %s%s to %s', @@ -46,17 +47,19 @@ def fetch_new(self, dest, url, rev_options): rev_display, display_path(dest), ) - self.run_command(['clone', '--noupdate', '-q', url, dest]) - cmd_args = ['update', '-q'] + rev_options.to_args() - self.run_command(cmd_args, cwd=dest) + self.run_command(make_command('clone', '--noupdate', '-q', url, dest)) + self.run_command( + make_command('update', '-q', rev_options.to_args()), + cwd=dest, + ) def switch(self, dest, url, rev_options): - # type: (str, str, RevOptions) -> None + # type: (str, HiddenText, RevOptions) -> None repo_config = os.path.join(dest, self.dirname, 'hgrc') config = configparser.RawConfigParser() try: config.read(repo_config) - config.set('paths', 'default', url) + config.set('paths', 'default', url.secret) with open(repo_config, 'w') as config_file: config.write(config_file) except (OSError, configparser.NoSectionError) as exc: @@ -64,13 +67,13 @@ def switch(self, dest, url, rev_options): 'Could not switch Mercurial repository to %s: %s', url, exc, ) else: - cmd_args = ['update', '-q'] + rev_options.to_args() + cmd_args = make_command('update', '-q', rev_options.to_args()) self.run_command(cmd_args, cwd=dest) def update(self, dest, url, rev_options): - # type: (str, str, RevOptions) -> None + # type: (str, HiddenText, RevOptions) -> None self.run_command(['pull', '-q'], cwd=dest) - cmd_args = ['update', '-q'] + rev_options.to_args() + cmd_args = make_command('update', '-q', rev_options.to_args()) self.run_command(cmd_args, cwd=dest) @classmethod diff --git a/src/pip/_internal/vcs/subversion.py b/src/pip/_internal/vcs/subversion.py index a15883fa7fa..2d9ed9a100d 100644 --- a/src/pip/_internal/vcs/subversion.py +++ b/src/pip/_internal/vcs/subversion.py @@ -8,6 +8,7 @@ from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import ( display_path, + make_command, rmtree, split_auth_from_netloc, ) @@ -21,7 +22,8 @@ if MYPY_CHECK_RUNNING: - from typing import List, Optional, Tuple + from typing import Optional, Tuple + from pip._internal.utils.misc import CommandArgs, HiddenText from pip._internal.vcs.versioncontrol import AuthInfo, RevOptions @@ -94,8 +96,8 @@ def get_url_rev_and_auth(cls, url): @staticmethod def make_rev_args(username, password): - # type: (Optional[str], Optional[str]) -> List[str] - extra_args = [] # type: List[str] + # type: (Optional[str], Optional[HiddenText]) -> CommandArgs + extra_args = [] # type: CommandArgs if username: extra_args += ['--username', username] if password: @@ -243,7 +245,7 @@ def get_vcs_version(self): return vcs_version def get_remote_call_options(self): - # type: () -> List[str] + # type: () -> CommandArgs """Return options to be used on calls to Subversion that contact the server. These options are applicable for the following ``svn`` subcommands used @@ -276,7 +278,7 @@ def get_remote_call_options(self): return [] def export(self, location, url): - # type: (str, str) -> None + # type: (str, HiddenText) -> None """Export the svn repository at the url to the destination location""" url, rev_options = self.get_url_rev_options(url) @@ -286,12 +288,14 @@ def export(self, location, url): # Subversion doesn't like to check out over an existing # directory --force fixes this, but was only added in svn 1.5 rmtree(location) - cmd_args = (['export'] + self.get_remote_call_options() + - rev_options.to_args() + [url, location]) + cmd_args = make_command( + 'export', self.get_remote_call_options(), + rev_options.to_args(), url, location, + ) self.run_command(cmd_args, show_stdout=False) def fetch_new(self, dest, url, rev_options): - # type: (str, str, RevOptions) -> None + # type: (str, HiddenText, RevOptions) -> None rev_display = rev_options.to_display() logger.info( 'Checking out %s%s to %s', @@ -299,21 +303,26 @@ def fetch_new(self, dest, url, rev_options): rev_display, display_path(dest), ) - cmd_args = (['checkout', '-q'] + - self.get_remote_call_options() + - rev_options.to_args() + [url, dest]) + cmd_args = make_command( + 'checkout', '-q', self.get_remote_call_options(), + rev_options.to_args(), url, dest, + ) self.run_command(cmd_args) def switch(self, dest, url, rev_options): - # type: (str, str, RevOptions) -> None - cmd_args = (['switch'] + self.get_remote_call_options() + - rev_options.to_args() + [url, dest]) + # type: (str, HiddenText, RevOptions) -> None + cmd_args = make_command( + 'switch', self.get_remote_call_options(), rev_options.to_args(), + url, dest, + ) self.run_command(cmd_args) def update(self, dest, url, rev_options): - # type: (str, str, RevOptions) -> None - cmd_args = (['update'] + self.get_remote_call_options() + - rev_options.to_args() + [dest]) + # type: (str, HiddenText, RevOptions) -> None + cmd_args = make_command( + 'update', self.get_remote_call_options(), rev_options.to_args(), + dest, + ) self.run_command(cmd_args) diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index 6d400d56883..40740e97867 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -16,18 +16,23 @@ backup_dir, call_subprocess, display_path, + hide_url, + hide_value, + make_command, rmtree, ) from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: from typing import ( - Any, Dict, Iterable, List, Mapping, Optional, Text, Tuple, Type + Any, Dict, Iterable, List, Mapping, Optional, Text, Tuple, Type, Union ) from pip._internal.utils.ui import SpinnerInterface + from pip._internal.utils.misc import CommandArgs, HiddenText AuthInfo = Tuple[Optional[str], Optional[str]] + __all__ = ['vcs'] @@ -67,7 +72,7 @@ def __init__( self, vc_class, # type: Type[VersionControl] rev=None, # type: Optional[str] - extra_args=None, # type: Optional[List[str]] + extra_args=None, # type: Optional[CommandArgs] ): # type: (...) -> None """ @@ -96,11 +101,11 @@ def arg_rev(self): return self.rev def to_args(self): - # type: () -> List[str] + # type: () -> CommandArgs """ Return the VCS-specific command arguments. """ - args = [] # type: List[str] + args = [] # type: CommandArgs rev = self.arg_rev if rev is not None: args += self.vc_class.get_base_rev_args(rev) @@ -271,7 +276,7 @@ def get_base_rev_args(rev): @classmethod def make_rev_options(cls, rev=None, extra_args=None): - # type: (Optional[str], Optional[List[str]]) -> RevOptions + # type: (Optional[str], Optional[CommandArgs]) -> RevOptions """ Return a RevOptions object. @@ -292,7 +297,7 @@ def _is_local_repository(cls, repo): return repo.startswith(os.path.sep) or bool(drive) def export(self, location, url): - # type: (str, str) -> None + # type: (str, HiddenText) -> None """ Export the repository at the url to the destination location i.e. only download the files, without vcs informations @@ -347,24 +352,27 @@ def get_url_rev_and_auth(cls, url): @staticmethod def make_rev_args(username, password): - # type: (Optional[str], Optional[str]) -> List[str] + # type: (Optional[str], Optional[HiddenText]) -> CommandArgs """ Return the RevOptions "extra arguments" to use in obtain(). """ return [] def get_url_rev_options(self, url): - # type: (str) -> Tuple[str, RevOptions] + # type: (HiddenText) -> Tuple[HiddenText, RevOptions] """ Return the URL and RevOptions object to use in obtain() and in some cases export(), as a tuple (url, rev_options). """ - url, rev, user_pass = self.get_url_rev_and_auth(url) - username, password = user_pass + secret_url, rev, user_pass = self.get_url_rev_and_auth(url.secret) + username, secret_password = user_pass + password = None # type: Optional[HiddenText] + if secret_password is not None: + password = hide_value(secret_password) extra_args = self.make_rev_args(username, password) rev_options = self.make_rev_options(rev, extra_args=extra_args) - return url, rev_options + return hide_url(secret_url), rev_options @staticmethod def normalize_url(url): @@ -384,7 +392,7 @@ def compare_urls(cls, url1, url2): return (cls.normalize_url(url1) == cls.normalize_url(url2)) def fetch_new(self, dest, url, rev_options): - # type: (str, str, RevOptions) -> None + # type: (str, HiddenText, RevOptions) -> None """ Fetch a revision from a repository, in the case that this is the first fetch from the repository. @@ -396,7 +404,7 @@ def fetch_new(self, dest, url, rev_options): raise NotImplementedError def switch(self, dest, url, rev_options): - # type: (str, str, RevOptions) -> None + # type: (str, HiddenText, RevOptions) -> None """ Switch the repo at ``dest`` to point to ``URL``. @@ -406,7 +414,7 @@ def switch(self, dest, url, rev_options): raise NotImplementedError def update(self, dest, url, rev_options): - # type: (str, str, RevOptions) -> None + # type: (str, HiddenText, RevOptions) -> None """ Update an already-existing repo to the given ``rev_options``. @@ -427,7 +435,7 @@ def is_commit_id_equal(cls, dest, name): raise NotImplementedError def obtain(self, dest, url): - # type: (str, str) -> None + # type: (str, HiddenText) -> None """ Install or update in editable mode the package represented by this VersionControl object. @@ -444,7 +452,7 @@ def obtain(self, dest, url): rev_display = rev_options.to_display() if self.is_repository_directory(dest): existing_url = self.get_remote_url(dest) - if self.compare_urls(existing_url, url): + if self.compare_urls(existing_url, url.secret): logger.debug( '%s in %s exists, and has correct URL (%s)', self.repo_name.title(), @@ -520,7 +528,7 @@ def obtain(self, dest, url): self.switch(dest, url, rev_options) def unpack(self, location, url): - # type: (str, str) -> None + # type: (str, HiddenText) -> None """ Clean up current location and download the url repository (and vcs infos) into location @@ -551,7 +559,7 @@ def get_revision(cls, location): @classmethod def run_command( cls, - cmd, # type: List[str] + cmd, # type: Union[List[str], CommandArgs] show_stdout=True, # type: bool cwd=None, # type: Optional[str] on_returncode='raise', # type: str @@ -566,7 +574,7 @@ def run_command( This is simply a wrapper around call_subprocess that adds the VCS command name, and checks that the VCS is available """ - cmd = [cls.name] + cmd + cmd = make_command(cls.name, *cmd) try: return call_subprocess(cmd, show_stdout, cwd, on_returncode=on_returncode, diff --git a/tests/functional/test_vcs_bazaar.py b/tests/functional/test_vcs_bazaar.py index 6bd61611ac1..af52daa63ca 100644 --- a/tests/functional/test_vcs_bazaar.py +++ b/tests/functional/test_vcs_bazaar.py @@ -6,6 +6,7 @@ import pytest +from pip._internal.utils.misc import hide_url from pip._internal.vcs.bazaar import Bazaar from tests.lib import ( _test_path_to_file_url, @@ -35,7 +36,7 @@ def test_export(script, tmpdir): _vcs_add(script, str(source_dir), vcs='bazaar') export_dir = str(tmpdir / 'export') - url = 'bzr+' + _test_path_to_file_url(source_dir) + url = hide_url('bzr+' + _test_path_to_file_url(source_dir)) Bazaar().export(export_dir, url=url) assert os.listdir(export_dir) == ['test_file'] @@ -59,7 +60,7 @@ def test_export_rev(script, tmpdir): ) export_dir = tmpdir / 'export' - url = 'bzr+' + _test_path_to_file_url(source_dir) + '@1' + url = hide_url('bzr+' + _test_path_to_file_url(source_dir) + '@1') Bazaar().export(str(export_dir), url=url) with open(export_dir / 'test_file', 'r') as f: diff --git a/tests/lib/local_repos.py b/tests/lib/local_repos.py index 4e193e72499..69c60adb3fb 100644 --- a/tests/lib/local_repos.py +++ b/tests/lib/local_repos.py @@ -5,6 +5,7 @@ from pip._vendor.six.moves.urllib import request as urllib_request +from pip._internal.utils.misc import hide_url from pip._internal.vcs import bazaar, git, mercurial, subversion from tests.lib import path_to_url @@ -59,7 +60,8 @@ def _get_vcs_and_checkout_url(remote_repository, directory): destination_path = os.path.join(directory, repository_name) if not os.path.exists(destination_path): - vcs_class().obtain(destination_path, url=remote_repository) + url = hide_url(remote_repository) + vcs_class().obtain(destination_path, url=url) return '%s+%s' % ( vcs, path_to_url('/'.join([directory, repository_name, branch])), diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 8db3384f02b..ea252ceb257 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -37,6 +37,7 @@ ) from pip._internal.utils.hashes import Hashes, MissingHashes from pip._internal.utils.misc import ( + HiddenText, build_url_from_netloc, call_subprocess, egg_link_path, @@ -44,6 +45,9 @@ format_command_args, get_installed_distributions, get_prog, + hide_url, + hide_value, + make_command, make_subprocess_output_error, netloc_has_port, normalize_path, @@ -830,6 +834,9 @@ def test_get_prog(self, monkeypatch, argv, executable, expected): (['pip', 'list'], 'pip list'), (['foo', 'space space', 'new\nline', 'double"quote', "single'quote"], """foo 'space space' 'new\nline' 'double"quote' 'single'"'"'quote'"""), + # Test HiddenText arguments. + (make_command(hide_value('secret1'), 'foo', hide_value('secret2')), + "'****' foo '****'"), ]) def test_format_command_args(args, expected): actual = format_command_args(args) @@ -1356,6 +1363,73 @@ def test_redact_password_from_url(auth_url, expected_url): assert url == expected_url +class TestHiddenText: + + def test_basic(self): + """ + Test str(), repr(), and attribute access. + """ + hidden = HiddenText('my-secret', redacted='######') + assert repr(hidden) == "" + assert str(hidden) == '######' + assert hidden.redacted == '######' + assert hidden.secret == 'my-secret' + + def test_equality_with_str(self): + """ + Test equality (and inequality) with str objects. + """ + hidden = HiddenText('secret', redacted='****') + + # Test that the object doesn't compare equal to either its original + # or redacted forms. + assert hidden != hidden.secret + assert hidden.secret != hidden + + assert hidden != hidden.redacted + assert hidden.redacted != hidden + + def test_equality_same_secret(self): + """ + Test equality with an object having the same secret. + """ + # Choose different redactions for the two objects. + hidden1 = HiddenText('secret', redacted='****') + hidden2 = HiddenText('secret', redacted='####') + + assert hidden1 == hidden2 + # Also test __ne__. This assertion fails in Python 2 without + # defining HiddenText.__ne__. + assert not hidden1 != hidden2 + + def test_equality_different_secret(self): + """ + Test equality with an object having a different secret. + """ + hidden1 = HiddenText('secret-1', redacted='****') + hidden2 = HiddenText('secret-2', redacted='****') + + assert hidden1 != hidden2 + # Also test __eq__. + assert not hidden1 == hidden2 + + +def test_hide_value(): + hidden = hide_value('my-secret') + assert repr(hidden) == "" + assert str(hidden) == '****' + assert hidden.redacted == '****' + assert hidden.secret == 'my-secret' + + +def test_hide_url(): + hidden_url = hide_url('https://user:password@example.com') + assert repr(hidden_url) == "" + assert str(hidden_url) == 'https://user:****@example.com' + assert hidden_url.redacted == 'https://user:****@example.com' + assert hidden_url.secret == 'https://user:password@example.com' + + @pytest.fixture() def patch_deprecation_check_version(): # We do this, so that the deprecation tests are easier to write. diff --git a/tests/unit/test_vcs.py b/tests/unit/test_vcs.py index 66754c667ac..c64ec2797f7 100644 --- a/tests/unit/test_vcs.py +++ b/tests/unit/test_vcs.py @@ -6,6 +6,7 @@ from pip._vendor.packaging.version import parse as parse_version from pip._internal.exceptions import BadCommand +from pip._internal.utils.misc import hide_url, hide_value from pip._internal.vcs import make_vcs_requirement_url from pip._internal.vcs.bazaar import Bazaar from pip._internal.vcs.git import Git, looks_like_hash @@ -342,7 +343,7 @@ def test_subversion__get_url_rev_and_auth(url, expected): @pytest.mark.parametrize('username, password, expected', [ (None, None, []), ('user', None, []), - ('user', 'pass', []), + ('user', hide_value('pass'), []), ]) def test_git__make_rev_args(username, password, expected): """ @@ -355,7 +356,8 @@ def test_git__make_rev_args(username, password, expected): @pytest.mark.parametrize('username, password, expected', [ (None, None, []), ('user', None, ['--username', 'user']), - ('user', 'pass', ['--username', 'user', '--password', 'pass']), + ('user', hide_value('pass'), + ['--username', 'user', '--password', hide_value('pass')]), ]) def test_subversion__make_rev_args(username, password, expected): """ @@ -369,12 +371,15 @@ def test_subversion__get_url_rev_options(): """ Test Subversion.get_url_rev_options(). """ - url = 'svn+https://user:pass@svn.example.com/MyProject@v1.0#egg=MyProject' - url, rev_options = Subversion().get_url_rev_options(url) - assert url == 'https://svn.example.com/MyProject' + secret_url = ( + 'svn+https://user:pass@svn.example.com/MyProject@v1.0#egg=MyProject' + ) + hidden_url = hide_url(secret_url) + url, rev_options = Subversion().get_url_rev_options(hidden_url) + assert url == hide_url('https://svn.example.com/MyProject') assert rev_options.rev == 'v1.0' assert rev_options.extra_args == ( - ['--username', 'user', '--password', 'pass'] + ['--username', 'user', '--password', hide_value('pass')] ) @@ -519,43 +524,48 @@ def assert_call_args(self, args): assert self.call_subprocess_mock.call_args[0][0] == args def test_obtain(self): - self.svn.obtain(self.dest, self.url) - self.assert_call_args( - ['svn', 'checkout', '-q', '--non-interactive', '--username', - 'username', '--password', 'password', - 'http://svn.example.com/', '/tmp/test']) + self.svn.obtain(self.dest, hide_url(self.url)) + self.assert_call_args([ + 'svn', 'checkout', '-q', '--non-interactive', '--username', + 'username', '--password', hide_value('password'), + hide_url('http://svn.example.com/'), '/tmp/test', + ]) def test_export(self): - self.svn.export(self.dest, self.url) - self.assert_call_args( - ['svn', 'export', '--non-interactive', '--username', 'username', - '--password', 'password', 'http://svn.example.com/', - '/tmp/test']) + self.svn.export(self.dest, hide_url(self.url)) + self.assert_call_args([ + 'svn', 'export', '--non-interactive', '--username', 'username', + '--password', hide_value('password'), + hide_url('http://svn.example.com/'), '/tmp/test', + ]) def test_fetch_new(self): - self.svn.fetch_new(self.dest, self.url, self.rev_options) - self.assert_call_args( - ['svn', 'checkout', '-q', '--non-interactive', - 'svn+http://username:password@svn.example.com/', - '/tmp/test']) + self.svn.fetch_new(self.dest, hide_url(self.url), self.rev_options) + self.assert_call_args([ + 'svn', 'checkout', '-q', '--non-interactive', + hide_url('svn+http://username:password@svn.example.com/'), + '/tmp/test', + ]) def test_fetch_new_revision(self): rev_options = RevOptions(Subversion, '123') - self.svn.fetch_new(self.dest, self.url, rev_options) - self.assert_call_args( - ['svn', 'checkout', '-q', '--non-interactive', - '-r', '123', - 'svn+http://username:password@svn.example.com/', - '/tmp/test']) + self.svn.fetch_new(self.dest, hide_url(self.url), rev_options) + self.assert_call_args([ + 'svn', 'checkout', '-q', '--non-interactive', '-r', '123', + hide_url('svn+http://username:password@svn.example.com/'), + '/tmp/test', + ]) def test_switch(self): - self.svn.switch(self.dest, self.url, self.rev_options) - self.assert_call_args( - ['svn', 'switch', '--non-interactive', - 'svn+http://username:password@svn.example.com/', - '/tmp/test']) + self.svn.switch(self.dest, hide_url(self.url), self.rev_options) + self.assert_call_args([ + 'svn', 'switch', '--non-interactive', + hide_url('svn+http://username:password@svn.example.com/'), + '/tmp/test', + ]) def test_update(self): - self.svn.update(self.dest, self.url, self.rev_options) - self.assert_call_args( - ['svn', 'update', '--non-interactive', '/tmp/test']) + self.svn.update(self.dest, hide_url(self.url), self.rev_options) + self.assert_call_args([ + 'svn', 'update', '--non-interactive', '/tmp/test', + ]) diff --git a/tests/unit/test_vcs_mercurial.py b/tests/unit/test_vcs_mercurial.py index f47b3882e31..630619b8236 100644 --- a/tests/unit/test_vcs_mercurial.py +++ b/tests/unit/test_vcs_mercurial.py @@ -6,6 +6,7 @@ from pip._vendor.six.moves import configparser +from pip._internal.utils.misc import hide_url from pip._internal.vcs.mercurial import Mercurial from tests.lib import need_mercurial @@ -24,7 +25,7 @@ def test_mercurial_switch_updates_config_file_when_found(tmpdir): hgrc_path = os.path.join(hg_dir, 'hgrc') with open(hgrc_path, 'w') as f: config.write(f) - hg.switch(tmpdir, 'new_url', options) + hg.switch(tmpdir, hide_url('new_url'), options) config.read(hgrc_path) From cbd62171cd4c6f6d4ac21c52ca3ce52d17956384 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 20 Aug 2019 10:51:28 -0700 Subject: [PATCH 0142/3170] Remove the unneeded logger argument from is_secure_origin(). --- src/pip/_internal/download.py | 5 ++--- src/pip/_internal/index.py | 2 +- tests/unit/test_download.py | 40 ++++++++++++++++++++++------------- 3 files changed, 28 insertions(+), 19 deletions(-) diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index c91d07997db..b8698c44336 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -64,7 +64,6 @@ from pip._internal.vcs import vcs if MYPY_CHECK_RUNNING: - from logging import Logger from typing import ( IO, Callable, Dict, Iterator, List, Optional, Text, Tuple, Union, ) @@ -688,8 +687,8 @@ def iter_secure_origins(self): for host in self.pip_trusted_hosts: yield ('*', host, '*') - def is_secure_origin(self, logger, location): - # type: (Logger, Link) -> bool + def is_secure_origin(self, location): + # type: (Link) -> bool # Determine if this url used a secure transport mechanism parsed = urllib_parse.urlparse(str(location)) origin = (parsed.scheme, parsed.hostname, parsed.port) diff --git a/src/pip/_internal/index.py b/src/pip/_internal/index.py index b782ff2b0da..ca89c722fcc 100644 --- a/src/pip/_internal/index.py +++ b/src/pip/_internal/index.py @@ -999,7 +999,7 @@ def find_all_candidates(self, project_name): (Link(url) for url in index_url_loc), (Link(url) for url in fl_url_loc), ) - if self.session.is_secure_origin(logger, link) + if self.session.is_secure_origin(link) ] logger.debug('%d location(s) to search for versions of %s:', diff --git a/tests/unit/test_download.py b/tests/unit/test_download.py index 40229f9314d..c75871e39fa 100644 --- a/tests/unit/test_download.py +++ b/tests/unit/test_download.py @@ -700,21 +700,22 @@ def test_iter_secure_origins__insecure_hosts_empty(self): assert actual[0] == ('https', '*', '*') @pytest.mark.parametrize( - ("location", "trusted", "expected"), + 'location, trusted, expected', [ - ("http://pypi.org/something", [], True), - ("https://pypi.org/something", [], False), - ("git+http://pypi.org/something", [], True), - ("git+https://pypi.org/something", [], False), - ("git+ssh://git@pypi.org/something", [], False), - ("http://localhost", [], False), - ("http://127.0.0.1", [], False), - ("http://example.com/something/", [], True), - ("http://example.com/something/", ["example.com"], False), - ("http://eXample.com/something/", ["example.cOm"], False), + ("http://pypi.org/something", [], False), + ("https://pypi.org/something", [], True), + ("git+http://pypi.org/something", [], False), + ("git+https://pypi.org/something", [], True), + ("git+ssh://git@pypi.org/something", [], True), + ("http://localhost", [], True), + ("http://127.0.0.1", [], True), + ("http://example.com/something/", [], False), + ("http://example.com/something/", ["example.com"], True), + # Try changing the case. + ("http://eXample.com/something/", ["example.cOm"], True), ], ) - def test_secure_origin(self, location, trusted, expected): + def test_is_secure_origin(self, caplog, location, trusted, expected): class MockLogger(object): def __init__(self): self.called = False @@ -723,9 +724,18 @@ def warning(self, *args, **kwargs): self.called = True session = PipSession(insecure_hosts=trusted) - logger = MockLogger() - session.is_secure_origin(logger, location) - assert logger.called == expected + actual = session.is_secure_origin(location) + assert actual == expected + + log_records = [(r.levelname, r.message) for r in caplog.records] + if expected: + assert not log_records + return + + assert len(log_records) == 1 + actual_level, actual_message = log_records[0] + assert actual_level == 'WARNING' + assert 'is not a trusted or secure host' in actual_message @pytest.mark.parametrize(["input_url", "url", "username", "password"], [ From 970dfdea2e9923367494a4049c3410331a80e9fc Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 20 Aug 2019 11:16:34 -0700 Subject: [PATCH 0143/3170] Use variable names instead of indices in is_secure_origin(). --- src/pip/_internal/download.py | 43 +++++++++++++++++++---------------- 1 file changed, 23 insertions(+), 20 deletions(-) diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index b8698c44336..08522788ca2 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -691,45 +691,48 @@ def is_secure_origin(self, location): # type: (Link) -> bool # Determine if this url used a secure transport mechanism parsed = urllib_parse.urlparse(str(location)) - origin = (parsed.scheme, parsed.hostname, parsed.port) + origin_protocol, origin_host, origin_port = ( + parsed.scheme, parsed.hostname, parsed.port, + ) # The protocol to use to see if the protocol matches. # Don't count the repository type as part of the protocol: in # cases such as "git+ssh", only use "ssh". (I.e., Only verify against # the last scheme.) - protocol = origin[0].rsplit('+', 1)[-1] + origin_protocol = origin_protocol.rsplit('+', 1)[-1] # Determine if our origin is a secure origin by looking through our # hardcoded list of secure origins, as well as any additional ones # configured on this PackageFinder instance. for secure_origin in self.iter_secure_origins(): - if protocol != secure_origin[0] and secure_origin[0] != "*": + secure_protocol, secure_host, secure_port = secure_origin + if origin_protocol != secure_protocol and secure_protocol != "*": continue try: # We need to do this decode dance to ensure that we have a # unicode object, even on Python 2.x. addr = ipaddress.ip_address( - origin[1] + origin_host if ( - isinstance(origin[1], six.text_type) or - origin[1] is None + isinstance(origin_host, six.text_type) or + origin_host is None ) - else origin[1].decode("utf8") + else origin_host.decode("utf8") ) network = ipaddress.ip_network( - secure_origin[1] - if isinstance(secure_origin[1], six.text_type) - # setting secure_origin[1] to proper Union[bytes, str] + secure_host + if isinstance(secure_host, six.text_type) + # setting secure_host to proper Union[bytes, str] # creates problems in other places - else secure_origin[1].decode("utf8") # type: ignore + else secure_host.decode("utf8") # type: ignore ) except ValueError: # We don't have both a valid address or a valid network, so # we'll check this origin against hostnames. - if (origin[1] and - origin[1].lower() != secure_origin[1].lower() and - secure_origin[1] != "*"): + if (origin_host and + origin_host.lower() != secure_host.lower() and + secure_host != "*"): continue else: # We have a valid address and network, so see if the address @@ -737,10 +740,10 @@ def is_secure_origin(self, location): if addr not in network: continue - # Check to see if the port patches - if (origin[2] != secure_origin[2] and - secure_origin[2] != "*" and - secure_origin[2] is not None): + # Check to see if the port matches. + if (origin_port != secure_port and + secure_port != "*" and + secure_port is not None): continue # If we've gotten here, then this origin matches the current @@ -755,8 +758,8 @@ def is_secure_origin(self, location): "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, + origin_host, + origin_host, ) return False From ce218c340d169e51ea7c2b46f1ad9695f0322bbe Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 20 Aug 2019 12:58:31 -0700 Subject: [PATCH 0144/3170] Rename the insecure_hosts argument to trusted_hosts. --- src/pip/_internal/cli/req_command.py | 2 +- src/pip/_internal/download.py | 6 +++--- tests/unit/test_download.py | 16 ++++++++-------- 3 files changed, 12 insertions(+), 12 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index bc994738812..361182abc07 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -62,7 +62,7 @@ def _build_session(self, options, retries=None, timeout=None): if options.cache_dir else None ), retries=retries if retries is not None else options.retries, - insecure_hosts=options.trusted_hosts, + trusted_hosts=options.trusted_hosts, index_urls=self._get_index_urls(options), ) diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index 08522788ca2..670cb5cc4a4 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -573,12 +573,12 @@ class PipSession(requests.Session): def __init__(self, *args, **kwargs): """ - :param insecure_hosts: Domains not to emit warnings for when not using + :param trusted_hosts: Domains not to emit warnings for when not using HTTPS. """ retries = kwargs.pop("retries", 0) cache = kwargs.pop("cache", None) - insecure_hosts = kwargs.pop("insecure_hosts", []) # type: List[str] + trusted_hosts = kwargs.pop("trusted_hosts", []) # type: List[str] index_urls = kwargs.pop("index_urls", None) super(PipSession, self).__init__(*args, **kwargs) @@ -652,7 +652,7 @@ def __init__(self, *args, **kwargs): # Enable file:// urls self.mount("file://", LocalFSAdapter()) - for host in insecure_hosts: + for host in trusted_hosts: self.add_trusted_host(host, suppress_logging=True) def add_trusted_host(self, host, source=None, suppress_logging=False): diff --git a/tests/unit/test_download.py b/tests/unit/test_download.py index c75871e39fa..42265f327ea 100644 --- a/tests/unit/test_download.py +++ b/tests/unit/test_download.py @@ -618,7 +618,7 @@ def test_http_cache_is_not_enabled(self, tmpdir): def test_insecure_host_adapter(self, tmpdir): session = PipSession( cache=tmpdir.joinpath("test-cache"), - insecure_hosts=["example.com"], + trusted_hosts=["example.com"], ) assert "https://example.com/" in session.adapters @@ -630,7 +630,7 @@ def test_insecure_host_adapter(self, tmpdir): def test_add_trusted_host(self): # Leave a gap to test how the ordering is affected. trusted_hosts = ['host1', 'host3'] - session = PipSession(insecure_hosts=trusted_hosts) + session = PipSession(trusted_hosts=trusted_hosts) insecure_adapter = session._insecure_adapter prefix2 = 'https://host2/' prefix3 = 'https://host3/' @@ -658,7 +658,7 @@ def test_add_trusted_host__logging(self, caplog): Test logging when add_trusted_host() is called. """ trusted_hosts = ['host0', 'host1'] - session = PipSession(insecure_hosts=trusted_hosts) + session = PipSession(trusted_hosts=trusted_hosts) with caplog.at_level(logging.INFO): # Test adding an existing host. session.add_trusted_host('host1', source='somewhere') @@ -677,7 +677,7 @@ def test_add_trusted_host__logging(self, caplog): def test_iter_secure_origins(self): trusted_hosts = ['host1', 'host2'] - session = PipSession(insecure_hosts=trusted_hosts) + session = PipSession(trusted_hosts=trusted_hosts) actual = list(session.iter_secure_origins()) assert len(actual) == 8 @@ -688,11 +688,11 @@ def test_iter_secure_origins(self): ('*', 'host2', '*'), ] - def test_iter_secure_origins__insecure_hosts_empty(self): + def test_iter_secure_origins__trusted_hosts_empty(self): """ - Test iter_secure_origins() after passing insecure_hosts=[]. + Test iter_secure_origins() after passing trusted_hosts=[]. """ - session = PipSession(insecure_hosts=[]) + session = PipSession(trusted_hosts=[]) actual = list(session.iter_secure_origins()) assert len(actual) == 6 @@ -723,7 +723,7 @@ def __init__(self): def warning(self, *args, **kwargs): self.called = True - session = PipSession(insecure_hosts=trusted) + session = PipSession(trusted_hosts=trusted) actual = session.is_secure_origin(location) assert actual == expected From 289af52b59e09eb90fd045be016731ba8281c089 Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara Date: Thu, 22 Aug 2019 12:42:35 -0400 Subject: [PATCH 0145/3170] Add WIP note --- docs/html/development/architecture/anatomy.rst | 5 +++++ docs/html/development/architecture/overview.rst | 6 ++++++ 2 files changed, 11 insertions(+) diff --git a/docs/html/development/architecture/anatomy.rst b/docs/html/development/architecture/anatomy.rst index 5569f165a90..a9e7ad6b5e4 100644 --- a/docs/html/development/architecture/anatomy.rst +++ b/docs/html/development/architecture/anatomy.rst @@ -1,3 +1,7 @@ +.. note:: + This section of the documentation is currently being written. pip + developers welcome your help to complete this documentation. If you're + interested in helping out, please let us know in the `tracking issue`_. **************************************** Repository anatomy & directory structure @@ -97,6 +101,7 @@ Within ``src/``: * ``_vendor/`` *[code from other packages -- pip’s own dependencies…. Has them in its own source tree, because pip cannot depend on pip being installed on the machine already!]* +.. _`tracking issue`: https://github.com/pypa/pip/issues/6831 .. _GitHub repository: https://github.com/pypa/pip/ .. _tox.ini: https://github.com/pypa/pip/blob/master/tox.ini .. _improving the pip dependency resolver: https://github.com/pypa/pip/issues/988 diff --git a/docs/html/development/architecture/overview.rst b/docs/html/development/architecture/overview.rst index 75ccf142043..bd0e7ece402 100644 --- a/docs/html/development/architecture/overview.rst +++ b/docs/html/development/architecture/overview.rst @@ -1,3 +1,8 @@ +.. note:: + This section of the documentation is currently being written. pip + developers welcome your help to complete this documentation. If you're + interested in helping out, please let us know in the `tracking issue`_. + .. contents:: **************************** @@ -132,6 +137,7 @@ whole list? Answer: It's not every file, just files of Flask. No API for getting alllllll files on PyPI. It’s for getting all files of Flask.) +.. _`tracking issue`: https://github.com/pypa/pip/issues/6831 .. _PyPI: https://pypi.org/ .. _PyPI Simple API: https://warehouse.readthedocs.io/api-reference/legacy/#simple-project-api From 2b572d5e850fac91f69ea6056b622bbb066f09ad Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Thu, 22 Aug 2019 23:02:13 +0530 Subject: [PATCH 0146/3170] Rename 6637.feature to 6637.doc --- news/6637.doc | 1 + news/6637.feature | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 news/6637.doc delete mode 100644 news/6637.feature diff --git a/news/6637.doc b/news/6637.doc new file mode 100644 index 00000000000..f79d729bea1 --- /dev/null +++ b/news/6637.doc @@ -0,0 +1 @@ +Add architectural overview documentation. diff --git a/news/6637.feature b/news/6637.feature deleted file mode 100644 index f6b607d7d95..00000000000 --- a/news/6637.feature +++ /dev/null @@ -1 +0,0 @@ -Add architectural overview documentation. \ No newline at end of file From 70027b2f50ab9b533ed5d15d970d079ab244db1e Mon Sep 17 00:00:00 2001 From: A_Rog Date: Fri, 23 Aug 2019 01:29:22 +0100 Subject: [PATCH 0147/3170] Address #6876: Make command output go through a single function (#6881) --- news/1234.trival | 0 src/pip/_internal/commands/check.py | 7 ++-- src/pip/_internal/commands/configuration.py | 6 +-- src/pip/_internal/commands/download.py | 4 +- src/pip/_internal/commands/hash.py | 6 +-- src/pip/_internal/commands/install.py | 3 +- src/pip/_internal/commands/list.py | 11 +++--- src/pip/_internal/commands/search.py | 13 ++++--- src/pip/_internal/commands/show.py | 43 +++++++++++---------- src/pip/_internal/utils/misc.py | 5 +++ 10 files changed, 54 insertions(+), 44 deletions(-) create mode 100644 news/1234.trival diff --git a/news/1234.trival b/news/1234.trival new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/commands/check.py b/src/pip/_internal/commands/check.py index 88dff795a1d..c9c3e30f3a8 100644 --- a/src/pip/_internal/commands/check.py +++ b/src/pip/_internal/commands/check.py @@ -5,6 +5,7 @@ check_package_set, create_package_set_from_installed, ) +from pip._internal.utils.misc import write_output logger = logging.getLogger(__name__) @@ -22,7 +23,7 @@ def run(self, options, args): for project_name in missing: version = package_set[project_name].version for dependency in missing[project_name]: - logger.info( + write_output( "%s %s requires %s, which is not installed.", project_name, version, dependency[0], ) @@ -30,7 +31,7 @@ def run(self, options, args): for project_name in conflicting: version = package_set[project_name].version for dep_name, dep_version, req in conflicting[project_name]: - logger.info( + write_output( "%s %s has requirement %s, but you have %s %s.", project_name, version, req, dep_name, dep_version, ) @@ -38,4 +39,4 @@ def run(self, options, args): if missing or conflicting or parsing_probs: return 1 else: - logger.info("No broken requirements found.") + write_output("No broken requirements found.") diff --git a/src/pip/_internal/commands/configuration.py b/src/pip/_internal/commands/configuration.py index 31b93d96b17..4b3fc2baec4 100644 --- a/src/pip/_internal/commands/configuration.py +++ b/src/pip/_internal/commands/configuration.py @@ -11,7 +11,7 @@ ) from pip._internal.exceptions import PipError from pip._internal.utils.deprecation import deprecated -from pip._internal.utils.misc import get_prog +from pip._internal.utils.misc import get_prog, write_output from pip._internal.utils.virtualenv import running_under_virtualenv logger = logging.getLogger(__name__) @@ -184,13 +184,13 @@ 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) + write_output("%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) + write_output("%s", value) def set_name_value(self, options, args): key, value = self._get_n_args(args, "set [name] [value]", n=2) diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index f4526b57a93..1f0cd7c7347 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -9,7 +9,7 @@ from pip._internal.req import RequirementSet from pip._internal.req.req_tracker import RequirementTracker from pip._internal.utils.filesystem import check_path_owner -from pip._internal.utils.misc import ensure_dir, normalize_path +from pip._internal.utils.misc import ensure_dir, normalize_path, write_output from pip._internal.utils.temp_dir import TempDirectory logger = logging.getLogger(__name__) @@ -143,7 +143,7 @@ def run(self, options, args): req.name for req in requirement_set.successfully_downloaded ]) if downloaded: - logger.info('Successfully downloaded %s', downloaded) + write_output('Successfully downloaded %s', downloaded) # Clean up if not options.no_clean: diff --git a/src/pip/_internal/commands/hash.py b/src/pip/_internal/commands/hash.py index 87b09f071e1..f8194648940 100644 --- a/src/pip/_internal/commands/hash.py +++ b/src/pip/_internal/commands/hash.py @@ -7,7 +7,7 @@ from pip._internal.cli.base_command import Command from pip._internal.cli.status_codes import ERROR from pip._internal.utils.hashes import FAVORITE_HASH, STRONG_HASHES -from pip._internal.utils.misc import read_chunks +from pip._internal.utils.misc import read_chunks, write_output logger = logging.getLogger(__name__) @@ -42,8 +42,8 @@ def run(self, options, args): algorithm = options.algorithm for path in args: - logger.info('%s:\n--hash=%s:%s', - path, algorithm, _hash_of_file(path, algorithm)) + write_output('%s:\n--hash=%s:%s', + path, algorithm, _hash_of_file(path, algorithm)) def _hash_of_file(path, algorithm): diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 673c6d21484..bd8a2a26d8e 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -35,6 +35,7 @@ ensure_dir, get_installed_version, protect_pip_from_modification_on_windows, + write_output, ) from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -447,7 +448,7 @@ def run(self, options, args): items.append(item) installed_desc = ' '.join(items) if installed_desc: - logger.info( + write_output( 'Successfully installed %s', installed_desc, ) except EnvironmentError as error: diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index 82731eb2f31..6b80d58021e 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -15,6 +15,7 @@ from pip._internal.utils.misc import ( dist_is_editable, get_installed_distributions, + write_output, ) from pip._internal.utils.packaging import get_installer @@ -216,12 +217,12 @@ def output_package_listing(self, packages, options): elif options.list_format == 'freeze': for dist in packages: if options.verbose >= 1: - logger.info("%s==%s (%s)", dist.project_name, - dist.version, dist.location) + write_output("%s==%s (%s)", dist.project_name, + dist.version, dist.location) else: - logger.info("%s==%s", dist.project_name, dist.version) + write_output("%s==%s", dist.project_name, dist.version) elif options.list_format == 'json': - logger.info(format_for_json(packages, options)) + write_output(format_for_json(packages, options)) def output_package_listing_columns(self, data, header): # insert the header first: we need to know the size of column names @@ -235,7 +236,7 @@ def output_package_listing_columns(self, data, header): pkg_strings.insert(1, " ".join(map(lambda x: '-' * x, sizes))) for val in pkg_strings: - logger.info(val) + write_output(val) def tabulate(vals): diff --git a/src/pip/_internal/commands/search.py b/src/pip/_internal/commands/search.py index 6889375e06d..d0a36e83f65 100644 --- a/src/pip/_internal/commands/search.py +++ b/src/pip/_internal/commands/search.py @@ -19,6 +19,7 @@ from pip._internal.models.index import PyPI from pip._internal.utils.compat import get_terminal_size from pip._internal.utils.logging import indent_log +from pip._internal.utils.misc import write_output logger = logging.getLogger(__name__) @@ -118,19 +119,19 @@ def print_results(hits, name_column_width=None, terminal_width=None): line = '%-*s - %s' % (name_column_width, '%s (%s)' % (name, latest), summary) try: - logger.info(line) + write_output(line) if name in installed_packages: dist = pkg_resources.get_distribution(name) with indent_log(): if dist.version == latest: - logger.info('INSTALLED: %s (latest)', dist.version) + write_output('INSTALLED: %s (latest)', dist.version) else: - logger.info('INSTALLED: %s', dist.version) + write_output('INSTALLED: %s', dist.version) if parse_version(latest).pre: - logger.info('LATEST: %s (pre-release; install' - ' with "pip install --pre")', latest) + write_output('LATEST: %s (pre-release; install' + ' with "pip install --pre")', latest) else: - logger.info('LATEST: %s', latest) + write_output('LATEST: %s', latest) except UnicodeEncodeError: pass diff --git a/src/pip/_internal/commands/show.py b/src/pip/_internal/commands/show.py index 6107b4df551..8ae27699bc9 100644 --- a/src/pip/_internal/commands/show.py +++ b/src/pip/_internal/commands/show.py @@ -9,6 +9,7 @@ from pip._internal.cli.base_command import Command from pip._internal.cli.status_codes import ERROR, SUCCESS +from pip._internal.utils.misc import write_output logger = logging.getLogger(__name__) @@ -134,7 +135,7 @@ def print_results(distributions, list_files=False, verbose=False): for i, dist in enumerate(distributions): results_printed = True if i > 0: - logger.info("---") + write_output("---") name = dist.get('name', '') required_by = [ @@ -142,31 +143,31 @@ def print_results(distributions, list_files=False, verbose=False): 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', '')) - logger.info("Author: %s", dist.get('author', '')) - logger.info("Author-email: %s", dist.get('author-email', '')) - 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)) + write_output("Name: %s", name) + write_output("Version: %s", dist.get('version', '')) + write_output("Summary: %s", dist.get('summary', '')) + write_output("Home-page: %s", dist.get('home-page', '')) + write_output("Author: %s", dist.get('author', '')) + write_output("Author-email: %s", dist.get('author-email', '')) + write_output("License: %s", dist.get('license', '')) + write_output("Location: %s", dist.get('location', '')) + write_output("Requires: %s", ', '.join(dist.get('requires', []))) + write_output("Required-by: %s", ', '.join(required_by)) if verbose: - logger.info("Metadata-Version: %s", - dist.get('metadata-version', '')) - logger.info("Installer: %s", dist.get('installer', '')) - logger.info("Classifiers:") + write_output("Metadata-Version: %s", + dist.get('metadata-version', '')) + write_output("Installer: %s", dist.get('installer', '')) + write_output("Classifiers:") for classifier in dist.get('classifiers', []): - logger.info(" %s", classifier) - logger.info("Entry-points:") + write_output(" %s", classifier) + write_output("Entry-points:") for entry in dist.get('entry_points', []): - logger.info(" %s", entry.strip()) + write_output(" %s", entry.strip()) if list_files: - logger.info("Files:") + write_output("Files:") for line in dist.get('files', []): - logger.info(" %s", line.strip()) + write_output(" %s", line.strip()) if "files" not in dist: - logger.info("Cannot locate installed-files.txt") + write_output("Cannot locate installed-files.txt") return results_printed diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index bade221ce52..399de7116ba 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -991,6 +991,11 @@ def call_subprocess( return ''.join(all_output) +def write_output(msg, *args): + # type: (str, str) -> None + logger.info(msg, *args) + + def _make_build_dir(build_dir): os.makedirs(build_dir) write_delete_marker_file(build_dir) From d20e02e728b104feca1612fea06b2b3e1a8ad995 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 23 Aug 2019 07:12:59 -0700 Subject: [PATCH 0148/3170] Change PackageFinder._sort_locations() to a group_locations() function. --- src/pip/_internal/index.py | 121 +++++++++++++++++++------------------ tests/unit/test_index.py | 18 +++--- 2 files changed, 69 insertions(+), 70 deletions(-) diff --git a/src/pip/_internal/index.py b/src/pip/_internal/index.py index b70048778b8..d6da516bef5 100644 --- a/src/pip/_internal/index.py +++ b/src/pip/_internal/index.py @@ -252,6 +252,65 @@ def _get_html_page(link, session=None): return None +def group_locations(locations, expand_dir=False): + # type: (Sequence[str], bool) -> Tuple[List[str], List[str]] + """ + Divide a list of locations into two groups: "files" (archives) and "urls." + + :return: A pair of lists (files, urls). + """ + files = [] + urls = [] + + # puts the url for the given file path into the appropriate list + def sort_path(path): + url = path_to_url(path) + if mimetypes.guess_type(url, strict=False)[0] == 'text/html': + urls.append(url) + else: + files.append(url) + + for url in locations: + + is_local_path = os.path.exists(url) + is_file_url = url.startswith('file:') + + if is_local_path or is_file_url: + if is_local_path: + path = url + else: + path = url_to_path(url) + if os.path.isdir(path): + if expand_dir: + path = os.path.realpath(path) + for item in os.listdir(path): + sort_path(os.path.join(path, item)) + elif is_file_url: + urls.append(url) + else: + logger.warning( + "Path '{0}' is ignored: " + "it is a directory.".format(path), + ) + elif os.path.isfile(path): + sort_path(path) + else: + logger.warning( + "Url '%s' is ignored: it is neither a file " + "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, + ) + + return files, urls + + def _check_link_requires_python( link, # type: Link version_info, # type: Tuple[int, int, int] @@ -899,64 +958,6 @@ def set_allow_all_prereleases(self): # type: () -> None self._candidate_prefs.allow_all_prereleases = True - @staticmethod - def _sort_locations(locations, expand_dir=False): - # type: (Sequence[str], bool) -> Tuple[List[str], List[str]] - """ - Sort locations into "files" (archives) and "urls", and return - a pair of lists (files,urls) - """ - files = [] - urls = [] - - # puts the url for the given file path into the appropriate list - def sort_path(path): - url = path_to_url(path) - if mimetypes.guess_type(url, strict=False)[0] == 'text/html': - urls.append(url) - else: - files.append(url) - - for url in locations: - - is_local_path = os.path.exists(url) - is_file_url = url.startswith('file:') - - if is_local_path or is_file_url: - if is_local_path: - path = url - else: - path = url_to_path(url) - if os.path.isdir(path): - if expand_dir: - path = os.path.realpath(path) - for item in os.listdir(path): - sort_path(os.path.join(path, item)) - elif is_file_url: - urls.append(url) - else: - logger.warning( - "Path '{0}' is ignored: " - "it is a directory.".format(path), - ) - elif os.path.isfile(path): - sort_path(path) - else: - logger.warning( - "Url '%s' is ignored: it is neither a file " - "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, - ) - - return files, urls - def make_link_evaluator(self, project_name): # type: (str) -> LinkEvaluator canonical_name = canonicalize_name(project_name) @@ -983,8 +984,8 @@ def find_all_candidates(self, project_name): """ search_scope = self.search_scope index_locations = search_scope.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( + index_file_loc, index_url_loc = group_locations(index_locations) + fl_file_loc, fl_url_loc = group_locations( self.find_links, expand_dir=True, ) diff --git a/tests/unit/test_index.py b/tests/unit/test_index.py index 71bfd2a163b..c27b3589d86 100644 --- a/tests/unit/test_index.py +++ b/tests/unit/test_index.py @@ -22,6 +22,7 @@ _find_name_version_sep, _get_html_page, filter_unallowed_hashes, + group_locations, ) from pip._internal.models.candidate import InstallationCandidate from pip._internal.models.search_scope import SearchScope @@ -29,7 +30,7 @@ from pip._internal.models.target_python import TargetPython from pip._internal.pep425tags import get_supported from pip._internal.utils.hashes import Hashes -from tests.lib import CURRENT_PY_VERSION_INFO, make_test_finder +from tests.lib import CURRENT_PY_VERSION_INFO def make_mock_candidate(version, yanked_reason=None, hex_digest=None): @@ -748,34 +749,31 @@ def test_make_candidate_evaluator( assert evaluator._supported_tags == [('py36', 'none', 'any')] -def test_sort_locations_file_expand_dir(data): +def test_group_locations__file_expand_dir(data): """ Test that a file:// dir gets listdir run with expand_dir """ - finder = make_test_finder(find_links=[data.find_links]) - files, urls = finder._sort_locations([data.find_links], expand_dir=True) + files, urls = group_locations([data.find_links], expand_dir=True) assert files and not urls, ( "files and not urls should have been found at find-links url: %s" % data.find_links ) -def test_sort_locations_file_not_find_link(data): +def test_group_locations__file_not_find_link(data): """ Test that a file:// url dir that's not a find-link, doesn't get a listdir run """ - finder = make_test_finder() - files, urls = finder._sort_locations([data.index_url("empty_with_pkg")]) + files, urls = group_locations([data.index_url("empty_with_pkg")]) assert urls and not files, "urls, but not files should have been found" -def test_sort_locations_non_existing_path(): +def test_group_locations__non_existing_path(): """ Test that a non-existing path is ignored. """ - finder = make_test_finder() - files, urls = finder._sort_locations( + files, urls = group_locations( [os.path.join('this', 'doesnt', 'exist')]) assert not urls and not files, "nothing should have been found" From aa14d3aecf7800933eb11d17b8bdae30ed6f2010 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 23 Aug 2019 07:20:43 -0700 Subject: [PATCH 0149/3170] Add a make_no_network_finder() test helper. --- tests/unit/test_finder.py | 84 +++++++++++++++++++++------------------ 1 file changed, 46 insertions(+), 38 deletions(-) diff --git a/tests/unit/test_finder.py b/tests/unit/test_finder.py index f3d6366bc4d..cc8fd8fc19b 100644 --- a/tests/unit/test_finder.py +++ b/tests/unit/test_finder.py @@ -23,6 +23,24 @@ from tests.lib import make_test_finder +def make_no_network_finder( + find_links, + allow_all_prereleases=False, # type: bool +): + """ + Create and return a PackageFinder instance for test purposes that + doesn't make any network requests when _get_pages() is called. + """ + finder = make_test_finder( + find_links=find_links, + allow_all_prereleases=allow_all_prereleases, + ) + # Replace the PackageFinder object's _get_pages() with a no-op. + finder._get_pages = lambda locations, project_name: [] + + return finder + + def test_no_mpkg(data): """Finder skips zipfiles with "macosx10" in the name.""" finder = make_test_finder(find_links=[data.find_links]) @@ -279,25 +297,22 @@ def test_finder_priority_nonegg_over_eggfragments(): req = install_req_from_line('bar==1.0', None) links = ['http://foo/bar.py#egg=bar-1.0', 'http://foo/bar-1.0.tar.gz'] - finder = make_test_finder(find_links=links) - - with patch.object(finder, "_get_pages", lambda x, y: []): - all_versions = finder.find_all_candidates(req.name) - assert all_versions[0].link.url.endswith('tar.gz') - assert all_versions[1].link.url.endswith('#egg=bar-1.0') + finder = make_no_network_finder(links) + all_versions = finder.find_all_candidates(req.name) + assert all_versions[0].link.url.endswith('tar.gz') + assert all_versions[1].link.url.endswith('#egg=bar-1.0') - link = finder.find_requirement(req, False) + link = finder.find_requirement(req, False) assert link.url.endswith('tar.gz') links.reverse() - finder = make_test_finder(find_links=links) - with patch.object(finder, "_get_pages", lambda x, y: []): - all_versions = finder.find_all_candidates(req.name) - assert all_versions[0].link.url.endswith('tar.gz') - assert all_versions[1].link.url.endswith('#egg=bar-1.0') - link = finder.find_requirement(req, False) + finder = make_no_network_finder(links) + all_versions = finder.find_all_candidates(req.name) + assert all_versions[0].link.url.endswith('tar.gz') + assert all_versions[1].link.url.endswith('#egg=bar-1.0') + link = finder.find_requirement(req, False) assert link.url.endswith('tar.gz') @@ -316,18 +331,16 @@ def test_finder_only_installs_stable_releases(data): # using find-links links = ["https://foo/bar-1.0.tar.gz", "https://foo/bar-2.0b1.tar.gz"] - finder = make_test_finder(find_links=links) - with patch.object(finder, "_get_pages", lambda x, y: []): - link = finder.find_requirement(req, False) - assert link.url == "https://foo/bar-1.0.tar.gz" + finder = make_no_network_finder(links) + link = finder.find_requirement(req, False) + assert link.url == "https://foo/bar-1.0.tar.gz" links.reverse() - finder = make_test_finder(find_links=links) - with patch.object(finder, "_get_pages", lambda x, y: []): - link = finder.find_requirement(req, False) - assert link.url == "https://foo/bar-1.0.tar.gz" + finder = make_no_network_finder(links) + link = finder.find_requirement(req, False) + assert link.url == "https://foo/bar-1.0.tar.gz" def test_finder_only_installs_data_require(data): @@ -371,18 +384,16 @@ def test_finder_installs_pre_releases(data): # using find-links links = ["https://foo/bar-1.0.tar.gz", "https://foo/bar-2.0b1.tar.gz"] - finder = make_test_finder(find_links=links, allow_all_prereleases=True) - with patch.object(finder, "_get_pages", lambda x, y: []): - link = finder.find_requirement(req, False) - assert link.url == "https://foo/bar-2.0b1.tar.gz" + finder = make_no_network_finder(links, allow_all_prereleases=True) + link = finder.find_requirement(req, False) + assert link.url == "https://foo/bar-2.0b1.tar.gz" links.reverse() - finder = make_test_finder(find_links=links, allow_all_prereleases=True) - with patch.object(finder, "_get_pages", lambda x, y: []): - link = finder.find_requirement(req, False) - assert link.url == "https://foo/bar-2.0b1.tar.gz" + finder = make_no_network_finder(links, allow_all_prereleases=True) + link = finder.find_requirement(req, False) + assert link.url == "https://foo/bar-2.0b1.tar.gz" def test_finder_installs_dev_releases(data): @@ -408,18 +419,15 @@ def test_finder_installs_pre_releases_with_version_spec(): req = install_req_from_line("bar>=0.0.dev0", None) links = ["https://foo/bar-1.0.tar.gz", "https://foo/bar-2.0b1.tar.gz"] - finder = make_test_finder(find_links=links) - - with patch.object(finder, "_get_pages", lambda x, y: []): - link = finder.find_requirement(req, False) - assert link.url == "https://foo/bar-2.0b1.tar.gz" + finder = make_no_network_finder(links) + link = finder.find_requirement(req, False) + assert link.url == "https://foo/bar-2.0b1.tar.gz" links.reverse() - finder = make_test_finder(find_links=links) - with patch.object(finder, "_get_pages", lambda x, y: []): - link = finder.find_requirement(req, False) - assert link.url == "https://foo/bar-2.0b1.tar.gz" + finder = make_no_network_finder(links) + link = finder.find_requirement(req, False) + assert link.url == "https://foo/bar-2.0b1.tar.gz" class TestLinkEvaluator(object): From 5e80b8286d84ebbf9f11897356b2c5b770322874 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 23 Aug 2019 07:31:30 -0700 Subject: [PATCH 0150/3170] Move four PackageFinder methods. This moves the methods to before the method where they are called. --- src/pip/_internal/index.py | 114 ++++++++++++++++++------------------- 1 file changed, 57 insertions(+), 57 deletions(-) diff --git a/src/pip/_internal/index.py b/src/pip/_internal/index.py index d6da516bef5..71331861e24 100644 --- a/src/pip/_internal/index.py +++ b/src/pip/_internal/index.py @@ -972,6 +972,63 @@ def make_link_evaluator(self, project_name): ignore_requires_python=self._ignore_requires_python, ) + def _sort_links(self, links): + # type: (Iterable[Link]) -> List[Link] + """ + Returns elements of links in order, non-egg links first, egg links + second, while eliminating duplicates + """ + eggs, no_eggs = [], [] + seen = set() # type: Set[Link] + for link in links: + if link not in seen: + seen.add(link) + if link.egg_fragment: + eggs.append(link) + else: + no_eggs.append(link) + return no_eggs + eggs + + def _log_skipped_link(self, link, reason): + # type: (Link, Text) -> None + if link not in self._logged_links: + # Mark this as a unicode string to prevent "UnicodeEncodeError: + # 'ascii' codec can't encode character" in Python 2 when + # the reason contains non-ascii characters. + # Also, put the link at the end so the reason is more visible + # and because the link string is usually very long. + logger.debug(u'Skipping link: %s: %s', reason, link) + self._logged_links.add(link) + + def get_install_candidate(self, link_evaluator, link): + # type: (LinkEvaluator, Link) -> Optional[InstallationCandidate] + """ + If the link is a candidate for install, convert it to an + InstallationCandidate and return it. Otherwise, return None. + """ + is_candidate, result = link_evaluator.evaluate_link(link) + if not is_candidate: + if result: + self._log_skipped_link(link, reason=result) + return None + + return InstallationCandidate( + project=link_evaluator.project_name, + link=link, + # Convert the Text result to str since InstallationCandidate + # accepts str. + version=str(result), + ) + + def _package_versions(self, link_evaluator, links): + # type: (LinkEvaluator, Iterable[Link]) -> List[InstallationCandidate] + result = [] + for link in self._sort_links(links): + candidate = self.get_install_candidate(link_evaluator, link) + if candidate is not None: + result.append(candidate) + return result + def find_all_candidates(self, project_name): # type: (str) -> List[InstallationCandidate] """Find all available InstallationCandidate for project_name @@ -1178,63 +1235,6 @@ def _get_pages(self, locations, project_name): yield page - def _sort_links(self, links): - # type: (Iterable[Link]) -> List[Link] - """ - Returns elements of links in order, non-egg links first, egg links - second, while eliminating duplicates - """ - eggs, no_eggs = [], [] - seen = set() # type: Set[Link] - for link in links: - if link not in seen: - seen.add(link) - if link.egg_fragment: - eggs.append(link) - else: - no_eggs.append(link) - return no_eggs + eggs - - def _log_skipped_link(self, link, reason): - # type: (Link, Text) -> None - if link not in self._logged_links: - # Mark this as a unicode string to prevent "UnicodeEncodeError: - # 'ascii' codec can't encode character" in Python 2 when - # the reason contains non-ascii characters. - # Also, put the link at the end so the reason is more visible - # and because the link string is usually very long. - logger.debug(u'Skipping link: %s: %s', reason, link) - self._logged_links.add(link) - - def get_install_candidate(self, link_evaluator, link): - # type: (LinkEvaluator, Link) -> Optional[InstallationCandidate] - """ - If the link is a candidate for install, convert it to an - InstallationCandidate and return it. Otherwise, return None. - """ - is_candidate, result = link_evaluator.evaluate_link(link) - if not is_candidate: - if result: - self._log_skipped_link(link, reason=result) - return None - - return InstallationCandidate( - project=link_evaluator.project_name, - link=link, - # Convert the Text result to str since InstallationCandidate - # accepts str. - version=str(result), - ) - - def _package_versions(self, link_evaluator, links): - # type: (LinkEvaluator, Iterable[Link]) -> List[InstallationCandidate] - result = [] - for link in self._sort_links(links): - candidate = self.get_install_candidate(link_evaluator, link) - if candidate is not None: - result.append(candidate) - return result - def _find_name_version_sep(fragment, canonical_name): # type: (str, str) -> int From f8d58256b74ad1b8909a4efd86bdcc4449f54ef3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= Date: Fri, 23 Aug 2019 23:11:12 +0200 Subject: [PATCH 0151/3170] Add failing test for symlink uninstall --- tests/functional/test_uninstall.py | 21 +++++++++++++++++++++ tests/lib/path.py | 10 ++++++++++ 2 files changed, 31 insertions(+) diff --git a/tests/functional/test_uninstall.py b/tests/functional/test_uninstall.py index 128d66102c0..2e7330feee2 100644 --- a/tests/functional/test_uninstall.py +++ b/tests/functional/test_uninstall.py @@ -458,6 +458,27 @@ def test_uninstall_wheel(script, data): assert_all_changes(result, result2, []) +@pytest.mark.skipif("sys.platform == 'win32'") +def test_uninstall_with_symlink(script, data, tmpdir): + """ + Test uninstalling a wheel, with an additional symlink + https://github.com/pypa/pip/issues/6892 + """ + package = data.packages.joinpath("simple.dist-0.1-py2.py3-none-any.whl") + script.pip('install', package, '--no-index') + symlink_target = tmpdir / "target" + symlink_target.mkdir() + symlink_source = script.site_packages / "symlink" + (script.base_path / symlink_source).symlink_to(symlink_target) + st_mode = symlink_target.stat().st_mode + distinfo_path = script.site_packages_path / 'simple.dist-0.1.dist-info' + record_path = distinfo_path / 'RECORD' + record_path.append_text("symlink,,\n") + uninstall_result = script.pip('uninstall', 'simple.dist', '-y') + assert symlink_source in uninstall_result.files_deleted + assert symlink_target.stat().st_mode == st_mode + + def test_uninstall_setuptools_develop_install(script, data): """Try uninstall after setup.py develop followed of setup.py install""" pkg_path = data.packages.joinpath("FSPkg") diff --git a/tests/lib/path.py b/tests/lib/path.py index cb2e6bda7ed..d541468686f 100644 --- a/tests/lib/path.py +++ b/tests/lib/path.py @@ -203,9 +203,19 @@ def write_text(self, content): with open(self, "w") as fp: fp.write(content) + def append_text(self, content): + with open(self, "a") as fp: + fp.write(content) + def touch(self): with open(self, "a") as fp: path = fp.fileno() if os.utime in supports_fd else self os.utime(path, None) # times is not optional on Python 2.7 + def symlink_to(self, target): + os.symlink(target, self) + + def stat(self): + return os.stat(self) + curdir = Path(os.path.curdir) From 885fdc375446258cbc66fde576118c1a3defddc1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= Date: Sun, 18 Aug 2019 12:46:11 +0200 Subject: [PATCH 0152/3170] Correctly uninstall symlinks --- news/6892.bugfix | 2 ++ src/pip/_internal/utils/misc.py | 16 ++++++++-------- tests/unit/test_req_uninstall.py | 4 ++-- 3 files changed, 12 insertions(+), 10 deletions(-) create mode 100644 news/6892.bugfix diff --git a/news/6892.bugfix b/news/6892.bugfix new file mode 100644 index 00000000000..763f2520b47 --- /dev/null +++ b/news/6892.bugfix @@ -0,0 +1,2 @@ +Correctly uninstall symlinks that were installed in a virtualenv, +by tools such as ``flit install --symlink``. \ No newline at end of file diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 399de7116ba..e59aba534aa 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -427,10 +427,12 @@ def is_local(path): If we're not in a virtualenv, all paths are considered "local." + Caution: this function assumes the head of path has been normalized + with normalize_path. """ if not running_under_virtualenv(): return True - return normalize_path(path).startswith(normalize_path(sys.prefix)) + return path.startswith(normalize_path(sys.prefix)) def dist_is_local(dist): @@ -450,8 +452,7 @@ def dist_in_usersite(dist): """ Return True if given Distribution is installed in user site. """ - norm_path = normalize_path(dist_location(dist)) - return norm_path.startswith(normalize_path(user_site)) + return dist_location(dist).startswith(normalize_path(user_site)) def dist_in_site_packages(dist): @@ -460,9 +461,7 @@ def dist_in_site_packages(dist): Return True if given Distribution is installed in sysconfig.get_python_lib(). """ - return normalize_path( - dist_location(dist) - ).startswith(normalize_path(site_packages)) + return dist_location(dist).startswith(normalize_path(site_packages)) def dist_is_editable(dist): @@ -593,11 +592,12 @@ def dist_location(dist): packages, where dist.location is the source code location, and we want to know where the egg-link file is. + The returned location is normalized (in particular, with symlinks removed). """ egg_link = egg_link_path(dist) if egg_link: - return egg_link - return dist.location + return normalize_path(egg_link) + return normalize_path(dist.location) def current_umask(): diff --git a/tests/unit/test_req_uninstall.py b/tests/unit/test_req_uninstall.py index 69dbeebfe8d..ce5940d753c 100644 --- a/tests/unit/test_req_uninstall.py +++ b/tests/unit/test_req_uninstall.py @@ -183,7 +183,7 @@ def test_add_symlink(self, tmpdir, monkeypatch): def test_compact_shorter_path(self, monkeypatch): monkeypatch.setattr(pip._internal.req.req_uninstall, 'is_local', - lambda p: True) + mock_is_local) monkeypatch.setattr('os.path.exists', lambda p: True) # This deals with nt/posix path differences short_path = os.path.normcase(os.path.abspath( @@ -196,7 +196,7 @@ def test_compact_shorter_path(self, monkeypatch): @pytest.mark.skipif("sys.platform == 'win32'") def test_detect_symlink_dirs(self, monkeypatch, tmpdir): monkeypatch.setattr(pip._internal.req.req_uninstall, 'is_local', - lambda p: True) + mock_is_local) # construct 2 paths: # tmpdir/dir/file From 75a12ff42359316374365656400319589666d3ac Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= Date: Sat, 24 Aug 2019 23:27:56 +0200 Subject: [PATCH 0153/3170] Add failing test for StashedUninstallPathSet symlink --- tests/unit/test_req_uninstall.py | 64 ++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/tests/unit/test_req_uninstall.py b/tests/unit/test_req_uninstall.py index ce5940d753c..d4d707e6042 100644 --- a/tests/unit/test_req_uninstall.py +++ b/tests/unit/test_req_uninstall.py @@ -306,3 +306,67 @@ def test_rollback(self, tmpdir): for old_path, new_path in stashed_paths: assert os.path.exists(old_path) assert not os.path.exists(new_path) + + @pytest.mark.skipif("sys.platform == 'win32'") + def test_commit_symlinks(self, tmpdir): + adir = tmpdir / "dir" + adir.mkdir() + dirlink = tmpdir / "dirlink" + dirlink.symlink_to(adir) + afile = tmpdir / "file" + afile.write_text("...") + filelink = tmpdir / "filelink" + filelink.symlink_to(afile) + + pathset = StashedUninstallPathSet() + stashed_paths = [] + stashed_paths.append(pathset.stash(dirlink)) + stashed_paths.append(pathset.stash(filelink)) + for stashed_path in stashed_paths: + assert os.path.lexists(stashed_path) + assert not os.path.exists(dirlink) + assert not os.path.exists(filelink) + + pathset.commit() + + # stash removed, links removed + for stashed_path in stashed_paths: + assert not os.path.lexists(stashed_path) + assert not os.path.lexists(dirlink) and not os.path.isdir(dirlink) + assert not os.path.lexists(filelink) and not os.path.isfile(filelink) + + # link targets untouched + assert os.path.isdir(adir) + assert os.path.isfile(afile) + + @pytest.mark.skipif("sys.platform == 'win32'") + def test_rollback_symlinks(self, tmpdir): + adir = tmpdir / "dir" + adir.mkdir() + dirlink = tmpdir / "dirlink" + dirlink.symlink_to(adir) + afile = tmpdir / "file" + afile.write_text("...") + filelink = tmpdir / "filelink" + filelink.symlink_to(afile) + + pathset = StashedUninstallPathSet() + stashed_paths = [] + stashed_paths.append(pathset.stash(dirlink)) + stashed_paths.append(pathset.stash(filelink)) + for stashed_path in stashed_paths: + assert os.path.lexists(stashed_path) + assert not os.path.lexists(dirlink) + assert not os.path.lexists(filelink) + + pathset.rollback() + + # stash removed, links restored + for stashed_path in stashed_paths: + assert not os.path.lexists(stashed_path) + assert os.path.lexists(dirlink) and os.path.isdir(dirlink) + assert os.path.lexists(filelink) and os.path.isfile(filelink) + + # link targets untouched + assert os.path.isdir(adir) + assert os.path.isfile(afile) From 4b99a2f161cac266e84f0d2579e1d14696b79593 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Sun, 25 Aug 2019 10:00:06 +0530 Subject: [PATCH 0154/3170] Bump for development --- src/pip/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/__init__.py b/src/pip/__init__.py index 0803e00112a..9c2e2a3bb48 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -1 +1 @@ -__version__ = "19.2.3" +__version__ = "19.3.dev0" From 8ac22141c2c1815c023166ac6afeb8152945e4f3 Mon Sep 17 00:00:00 2001 From: Frost Ming Date: Mon, 26 Aug 2019 07:26:01 +0800 Subject: [PATCH 0155/3170] Support including port part in trusted-host (#6909) --- news/6886.feature | 1 + src/pip/_internal/cli/cmdoptions.py | 4 +-- src/pip/_internal/download.py | 17 ++++++----- src/pip/_internal/index.py | 4 ++- src/pip/_internal/utils/misc.py | 21 +++++++++++--- tests/unit/test_download.py | 42 ++++++++++++++++++++++----- tests/unit/test_req_file.py | 20 ++++++++----- tests/unit/test_utils.py | 45 +++++++++++++++++++++-------- 8 files changed, 111 insertions(+), 43 deletions(-) create mode 100644 news/6886.feature diff --git a/news/6886.feature b/news/6886.feature new file mode 100644 index 00000000000..b4f500b1b22 --- /dev/null +++ b/news/6886.feature @@ -0,0 +1 @@ +Support including a port number in ``--trusted-host`` for both HTTP and HTTPS. diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index 74efaecf97f..1bfecef5471 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -391,8 +391,8 @@ def trusted_host(): action="append", metavar="HOSTNAME", default=[], - help="Mark this host as trusted, even though it does not have valid " - "or any HTTPS.", + help="Mark this host or host:port pair as trusted, even though it " + "does not have valid or any HTTPS.", ) diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index 72d4cfc12cb..b681ccb0681 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -50,7 +50,7 @@ format_size, get_installed_version, hide_url, - netloc_has_port, + parse_netloc, path_to_display, path_to_url, remove_auth_from_url, @@ -77,7 +77,7 @@ from pip._internal.vcs.versioncontrol import AuthInfo, VersionControl Credentials = Tuple[str, str, str] - SecureOrigin = Tuple[str, str, Optional[str]] + SecureOrigin = Tuple[str, str, Optional[Union[int, str]]] if PY2: CopytreeKwargs = TypedDict( @@ -586,7 +586,7 @@ def __init__(self, *args, **kwargs): # Namespace the attribute with "pip_" just in case to prevent # possible conflicts with the base class. - self.pip_trusted_hosts = [] # type: List[str] + self.pip_trusted_origins = [] # type: List[Tuple[str, Optional[int]]] # Attach our User Agent to the request self.headers["User-Agent"] = user_agent() @@ -670,11 +670,12 @@ def add_trusted_host(self, host, source=None, suppress_logging=False): msg += ' (from {})'.format(source) logger.info(msg) - if host not in self.pip_trusted_hosts: - self.pip_trusted_hosts.append(host) + host_port = parse_netloc(host) + if host_port not in self.pip_trusted_origins: + self.pip_trusted_origins.append(host_port) self.mount(build_url_from_netloc(host) + '/', self._insecure_adapter) - if not netloc_has_port(host): + if not host_port[1]: # Mount wildcard ports for the same host. self.mount( build_url_from_netloc(host) + ':', @@ -685,8 +686,8 @@ def iter_secure_origins(self): # type: () -> Iterator[SecureOrigin] for secure_origin in SECURE_ORIGINS: yield secure_origin - for host in self.pip_trusted_hosts: - yield ('*', host, '*') + for host, port in self.pip_trusted_origins: + yield ('*', host, '*' if port is None else port) def is_secure_origin(self, location): # type: (Link) -> bool diff --git a/src/pip/_internal/index.py b/src/pip/_internal/index.py index 71331861e24..93c198433c4 100644 --- a/src/pip/_internal/index.py +++ b/src/pip/_internal/index.py @@ -38,6 +38,7 @@ ARCHIVE_EXTENSIONS, SUPPORTED_EXTENSIONS, WHEEL_EXTENSION, + build_netloc, path_to_url, redact_password_from_url, ) @@ -947,7 +948,8 @@ def index_urls(self): @property def trusted_hosts(self): # type: () -> Iterable[str] - return iter(self.session.pip_trusted_hosts) + for host_port in self.session.pip_trusted_origins: + yield build_netloc(*host_port) @property def allow_all_prereleases(self): diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index f6da2a5ab2a..d19df9459ac 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -1129,6 +1129,19 @@ def path_to_url(path): return url +def build_netloc(host, port): + # type: (str, Optional[int]) -> str + """ + Build a netloc from a host-port pair + """ + if port is None: + return host + if ':' in host: + # Only wrap host with square brackets when it is IPv6 + host = '[{}]'.format(host) + return '{}:{}'.format(host, port) + + def build_url_from_netloc(netloc, scheme='https'): # type: (str, str) -> str """ @@ -1140,14 +1153,14 @@ def build_url_from_netloc(netloc, scheme='https'): return '{}://{}'.format(scheme, netloc) -def netloc_has_port(netloc): - # type: (str) -> bool +def parse_netloc(netloc): + # type: (str) -> Tuple[str, Optional[int]] """ - Return whether the netloc has a port part. + Return the host-port pair from a netloc. """ url = build_url_from_netloc(netloc) parsed = urllib_parse.urlparse(url) - return bool(parsed.port) + return parsed.hostname, parsed.port def split_auth_from_netloc(netloc): diff --git a/tests/unit/test_download.py b/tests/unit/test_download.py index 42265f327ea..9c0ccc2cf30 100644 --- a/tests/unit/test_download.py +++ b/tests/unit/test_download.py @@ -634,24 +634,39 @@ def test_add_trusted_host(self): insecure_adapter = session._insecure_adapter prefix2 = 'https://host2/' prefix3 = 'https://host3/' + prefix3_wildcard = 'https://host3:' # Confirm some initial conditions as a baseline. - assert session.pip_trusted_hosts == ['host1', 'host3'] + assert session.pip_trusted_origins == [ + ('host1', None), ('host3', None) + ] assert session.adapters[prefix3] is insecure_adapter + assert session.adapters[prefix3_wildcard] is insecure_adapter + assert prefix2 not in session.adapters # Test adding a new host. session.add_trusted_host('host2') - assert session.pip_trusted_hosts == ['host1', 'host3', 'host2'] + assert session.pip_trusted_origins == [ + ('host1', None), ('host3', None), ('host2', None) + ] # Check that prefix3 is still present. assert session.adapters[prefix3] is insecure_adapter assert session.adapters[prefix2] is insecure_adapter # Test that adding the same host doesn't create a duplicate. session.add_trusted_host('host3') - assert session.pip_trusted_hosts == ['host1', 'host3', 'host2'], ( - 'actual: {}'.format(session.pip_trusted_hosts) - ) + assert session.pip_trusted_origins == [ + ('host1', None), ('host3', None), ('host2', None) + ], 'actual: {}'.format(session.pip_trusted_origins) + + session.add_trusted_host('host4:8080') + prefix4 = 'https://host4:8080/' + assert session.pip_trusted_origins == [ + ('host1', None), ('host3', None), + ('host2', None), ('host4', 8080) + ] + assert session.adapters[prefix4] is insecure_adapter def test_add_trusted_host__logging(self, caplog): """ @@ -676,16 +691,17 @@ def test_add_trusted_host__logging(self, caplog): assert actual == expected def test_iter_secure_origins(self): - trusted_hosts = ['host1', 'host2'] + trusted_hosts = ['host1', 'host2', 'host3:8080'] session = PipSession(trusted_hosts=trusted_hosts) actual = list(session.iter_secure_origins()) - assert len(actual) == 8 + assert len(actual) == 9 # Spot-check that SECURE_ORIGINS is included. assert actual[0] == ('https', '*', '*') - assert actual[-2:] == [ + assert actual[-3:] == [ ('*', 'host1', '*'), ('*', 'host2', '*'), + ('*', 'host3', 8080) ] def test_iter_secure_origins__trusted_hosts_empty(self): @@ -713,6 +729,16 @@ def test_iter_secure_origins__trusted_hosts_empty(self): ("http://example.com/something/", ["example.com"], True), # Try changing the case. ("http://eXample.com/something/", ["example.cOm"], True), + # Test hosts with port. + ("http://example.com:8080/something/", ["example.com"], True), + # Test a trusted_host with a port. + ("http://example.com:8080/something/", ["example.com:8080"], True), + ("http://example.com/something/", ["example.com:8080"], False), + ( + "http://example.com:8888/something/", + ["example.com:8080"], + False + ), ], ) def test_is_secure_origin(self, caplog, location, trusted, expected): diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py index a1153270759..443a7605469 100644 --- a/tests/unit/test_req_file.py +++ b/tests/unit/test_req_file.py @@ -345,19 +345,23 @@ def test_set_finder_extra_index_urls(self, finder): def test_set_finder_trusted_host(self, caplog, session, finder): with caplog.at_level(logging.INFO): list(process_line( - "--trusted-host=host", "file.txt", 1, finder=finder, - session=session, + "--trusted-host=host1 --trusted-host=host2:8080", + "file.txt", 1, finder=finder, session=session, )) - assert list(finder.trusted_hosts) == ['host'] + assert list(finder.trusted_hosts) == ['host1', 'host2:8080'] session = finder.session - assert session.adapters['https://host/'] is session._insecure_adapter + assert session.adapters['https://host1/'] is session._insecure_adapter + assert ( + session.adapters['https://host2:8080/'] + is session._insecure_adapter + ) # Test the log message. actual = [(r.levelname, r.message) for r in caplog.records] - expected = [ - ('INFO', "adding trusted host: 'host' (from line 1 of file.txt)"), - ] - assert actual == expected + expected = ( + 'INFO', "adding trusted host: 'host1' (from line 1 of file.txt)" + ) + assert expected in actual def test_noop_always_unzip(self, finder): # noop, but confirm it can be set diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index ea252ceb257..51cf2a23a0d 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -38,6 +38,7 @@ from pip._internal.utils.hashes import Hashes, MissingHashes from pip._internal.utils.misc import ( HiddenText, + build_netloc, build_url_from_netloc, call_subprocess, egg_link_path, @@ -49,9 +50,9 @@ hide_value, make_command, make_subprocess_output_error, - netloc_has_port, normalize_path, normalize_version_info, + parse_netloc, path_to_display, path_to_url, redact_netloc, @@ -1232,29 +1233,49 @@ def test_path_to_url_win(): assert path_to_url('file') == 'file:' + urllib_request.pathname2url(path) -@pytest.mark.parametrize('netloc, expected_url, expected_has_port', [ +@pytest.mark.parametrize('host_port, expected_netloc', [ # Test domain name. - ('example.com', 'https://example.com', False), - ('example.com:5000', 'https://example.com:5000', True), + (('example.com', None), 'example.com'), + (('example.com', 5000), 'example.com:5000'), # Test IPv4 address. - ('127.0.0.1', 'https://127.0.0.1', False), - ('127.0.0.1:5000', 'https://127.0.0.1:5000', True), + (('127.0.0.1', None), '127.0.0.1'), + (('127.0.0.1', 5000), '127.0.0.1:5000'), # Test bare IPv6 address. - ('2001:DB6::1', 'https://[2001:DB6::1]', False), + (('2001:db6::1', None), '2001:db6::1'), # Test IPv6 with port. - ('[2001:DB6::1]:5000', 'https://[2001:DB6::1]:5000', True), + (('2001:db6::1', 5000), '[2001:db6::1]:5000'), +]) +def test_build_netloc(host_port, expected_netloc): + assert build_netloc(*host_port) == expected_netloc + + +@pytest.mark.parametrize('netloc, expected_url, expected_host_port', [ + # Test domain name. + ('example.com', 'https://example.com', ('example.com', None)), + ('example.com:5000', 'https://example.com:5000', ('example.com', 5000)), + # Test IPv4 address. + ('127.0.0.1', 'https://127.0.0.1', ('127.0.0.1', None)), + ('127.0.0.1:5000', 'https://127.0.0.1:5000', ('127.0.0.1', 5000)), + # Test bare IPv6 address. + ('2001:db6::1', 'https://[2001:db6::1]', ('2001:db6::1', None)), + # Test IPv6 with port. + ( + '[2001:db6::1]:5000', + 'https://[2001:db6::1]:5000', + ('2001:db6::1', 5000) + ), # Test netloc with auth. ( 'user:password@localhost:5000', 'https://user:password@localhost:5000', - True + ('localhost', 5000) ) ]) -def test_build_url_from_netloc_and_netloc_has_port( - netloc, expected_url, expected_has_port, +def test_build_url_from_netloc_and_parse_netloc( + netloc, expected_url, expected_host_port, ): assert build_url_from_netloc(netloc) == expected_url - assert netloc_has_port(netloc) is expected_has_port + assert parse_netloc(netloc) == expected_host_port @pytest.mark.parametrize('netloc, expected', [ From d8b6296227d858353c3d1d0f1864247f8cc71337 Mon Sep 17 00:00:00 2001 From: Albert Tugushev Date: Mon, 26 Aug 2019 12:28:12 +0300 Subject: [PATCH 0156/3170] Fix pip.exe upgrade pip fails on Windows It fails if it already satisfies the requirement, when pip has been modifying via `pip.exe install ...` command. --- news/6924.bugfix | 2 ++ src/pip/_internal/commands/install.py | 10 +++++++++- src/pip/_internal/utils/misc.py | 1 + tests/functional/test_install.py | 18 ++++++++++++++++++ 4 files changed, 30 insertions(+), 1 deletion(-) create mode 100644 news/6924.bugfix diff --git a/news/6924.bugfix b/news/6924.bugfix new file mode 100644 index 00000000000..4d8b4aa0e0d --- /dev/null +++ b/news/6924.bugfix @@ -0,0 +1,2 @@ +Fix an error when upgrade pip using ``pip.exe`` on Windows fails +if the requirement already satisfied. diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index bd8a2a26d8e..b8c315b91e0 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -362,8 +362,16 @@ def run(self, options, args): ) resolver.resolve(requirement_set) + try: + pip_req = requirement_set.get_requirement("pip") + except KeyError: + pip_req = None + modifying_pip = ( + pip_req is not None and + pip_req.satisfied_by is None + ) protect_pip_from_modification_on_windows( - modifying_pip=requirement_set.has_requirement("pip") + modifying_pip=modifying_pip ) # Consider legacy and PEP517-using requirements separately diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index d19df9459ac..b1e0b7f1bcc 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -1307,6 +1307,7 @@ def hide_url(url): def protect_pip_from_modification_on_windows(modifying_pip): + # type: (bool) -> None """Protection of pip.exe from modification on Windows On Windows, any operation modifying pip should be run as: diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 27d8b8ef68b..06520fb1c2f 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1636,3 +1636,21 @@ def test_protect_pip_from_modification_via_sub_deps_on_windows(script): .format(' '.join(new_command)) ) assert expected_message in result.stderr, str(result) + + +@pytest.mark.parametrize( + 'install_args, expected_message', [ + ([], 'Requirement already satisfied: pip in'), + (['--upgrade'], 'Requirement already up-to-date: pip in'), + ] +) +@pytest.mark.parametrize("use_module", [True, False]) +def test_install_pip_does_not_modify_pip_when_satisfied( + script, install_args, expected_message, use_module): + """ + Test it doesn't upgrade the pip if it already satisfies the requirement. + """ + result = script.pip_install_local( + 'pip', *install_args, use_module=use_module + ) + assert expected_message in result.stdout, str(result) From bb68ebaa84f33ce4968909f2199993c6744ec6e6 Mon Sep 17 00:00:00 2001 From: Albert Tugushev Date: Mon, 26 Aug 2019 17:22:46 +0300 Subject: [PATCH 0157/3170] Address review comments Simplify code --- src/pip/_internal/commands/install.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index b8c315b91e0..71e67068f2e 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -365,11 +365,11 @@ def run(self, options, args): try: pip_req = requirement_set.get_requirement("pip") except KeyError: - pip_req = None - modifying_pip = ( - pip_req is not None and - pip_req.satisfied_by is None - ) + modifying_pip = None + else: + # If we're not replacing an already installed pip, + # we're not modifying it. + modifying_pip = pip_req.satisfied_by is None protect_pip_from_modification_on_windows( modifying_pip=modifying_pip ) From 3c9770d9b22b6e3e4de927ef8e1aa9c5155b74d6 Mon Sep 17 00:00:00 2001 From: Albert Tugushev Date: Tue, 27 Aug 2019 01:50:08 +0300 Subject: [PATCH 0158/3170] Redact single-part login credentials from URLs (#6921) --- news/6891.feature | 1 + src/pip/_internal/cli/cmdoptions.py | 4 ++-- src/pip/_internal/index.py | 6 +++--- src/pip/_internal/models/link.py | 6 +++--- src/pip/_internal/models/search_scope.py | 6 +++--- src/pip/_internal/req/req_install.py | 6 +++--- src/pip/_internal/utils/misc.py | 19 +++++++++++++------ tests/unit/test_utils.py | 10 +++++----- 8 files changed, 33 insertions(+), 25 deletions(-) create mode 100644 news/6891.feature diff --git a/news/6891.feature b/news/6891.feature new file mode 100644 index 00000000000..4d08eedfbfc --- /dev/null +++ b/news/6891.feature @@ -0,0 +1 @@ +Redact single-part login credentials from URLs in log messages. diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index 1bfecef5471..155211903b1 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -27,7 +27,7 @@ from pip._internal.models.search_scope import SearchScope from pip._internal.models.target_python import TargetPython from pip._internal.utils.hashes import STRONG_HASHES -from pip._internal.utils.misc import redact_password_from_url +from pip._internal.utils.misc import redact_auth_from_url from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.ui import BAR_TYPES @@ -369,7 +369,7 @@ def make_search_scope(options, suppress_no_index=False): if options.no_index and not suppress_no_index: logger.debug( 'Ignoring indexes: %s', - ','.join(redact_password_from_url(url) for url in index_urls), + ','.join(redact_auth_from_url(url) for url in index_urls), ) index_urls = [] diff --git a/src/pip/_internal/index.py b/src/pip/_internal/index.py index 93c198433c4..ba44e7bb44c 100644 --- a/src/pip/_internal/index.py +++ b/src/pip/_internal/index.py @@ -40,7 +40,7 @@ WHEEL_EXTENSION, build_netloc, path_to_url, - redact_password_from_url, + redact_auth_from_url, ) from pip._internal.utils.packaging import check_requires_python from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -154,7 +154,7 @@ def _get_html_response(url, session): if _is_url_like_archive(url): _ensure_html_response(url, session=session) - logger.debug('Getting page %s', redact_password_from_url(url)) + logger.debug('Getting page %s', redact_auth_from_url(url)) resp = session.get( url, @@ -1381,7 +1381,7 @@ def __init__(self, content, url, headers=None): self.headers = headers def __str__(self): - return redact_password_from_url(self.url) + return redact_auth_from_url(self.url) def iter_links(self): # type: () -> Iterable[Link] diff --git a/src/pip/_internal/models/link.py b/src/pip/_internal/models/link.py index 56ad2a5be5c..2a3c605e583 100644 --- a/src/pip/_internal/models/link.py +++ b/src/pip/_internal/models/link.py @@ -6,7 +6,7 @@ from pip._internal.utils.misc import ( WHEEL_EXTENSION, path_to_url, - redact_password_from_url, + redact_auth_from_url, split_auth_from_netloc, splitext, ) @@ -68,10 +68,10 @@ def __str__(self): else: rp = '' if self.comes_from: - return '%s (from %s)%s' % (redact_password_from_url(self._url), + return '%s (from %s)%s' % (redact_auth_from_url(self._url), self.comes_from, rp) else: - return redact_password_from_url(str(self._url)) + return redact_auth_from_url(str(self._url)) def __repr__(self): return '' % self diff --git a/src/pip/_internal/models/search_scope.py b/src/pip/_internal/models/search_scope.py index 62152449540..5d667deef59 100644 --- a/src/pip/_internal/models/search_scope.py +++ b/src/pip/_internal/models/search_scope.py @@ -8,7 +8,7 @@ from pip._internal.models.index import PyPI from pip._internal.utils.compat import HAS_TLS -from pip._internal.utils.misc import normalize_path, redact_password_from_url +from pip._internal.utils.misc import normalize_path, redact_auth_from_url from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: @@ -80,12 +80,12 @@ def get_formatted_locations(self): if self.index_urls and self.index_urls != [PyPI.simple_url]: lines.append( 'Looking in indexes: {}'.format(', '.join( - redact_password_from_url(url) for url in self.index_urls)) + redact_auth_from_url(url) for url in self.index_urls)) ) if self.find_links: lines.append( 'Looking in links: {}'.format(', '.join( - redact_password_from_url(url) for url in self.find_links)) + redact_auth_from_url(url) for url in self.find_links)) ) return '\n'.join(lines) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 264fade4cfa..a72d4dd7065 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -39,7 +39,7 @@ ensure_dir, get_installed_version, hide_url, - redact_password_from_url, + redact_auth_from_url, rmtree, ) from pip._internal.utils.packaging import get_metadata @@ -170,9 +170,9 @@ def __str__(self): if self.req: s = str(self.req) if self.link: - s += ' from %s' % redact_password_from_url(self.link.url) + s += ' from %s' % redact_auth_from_url(self.link.url) elif self.link: - s = redact_password_from_url(self.link.url) + s = redact_auth_from_url(self.link.url) else: s = '' if self.satisfied_by is not None: diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index d19df9459ac..f6e6e291ca7 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -1194,15 +1194,22 @@ def split_auth_from_netloc(netloc): def redact_netloc(netloc): # type: (str) -> str """ - Replace the password in a netloc with "****", if it exists. + Replace the sensitive data in a netloc with "****", if it exists. - For example, "user:pass@example.com" returns "user:****@example.com". + For example: + - "user:pass@example.com" returns "user:****@example.com" + - "accesstoken@example.com" returns "****@example.com" """ netloc, (user, password) = split_auth_from_netloc(netloc) if user is None: return netloc - password = '' if password is None else ':****' - return '{user}{password}@{netloc}'.format(user=urllib_parse.quote(user), + if password is None: + user = '****' + password = '' + else: + user = urllib_parse.quote(user) + password = ':****' + return '{user}{password}@{netloc}'.format(user=user, password=password, netloc=netloc) @@ -1254,7 +1261,7 @@ def remove_auth_from_url(url): return _transform_url(url, _get_netloc)[0] -def redact_password_from_url(url): +def redact_auth_from_url(url): # type: (str) -> str """Replace the password in a given url with ****.""" return _transform_url(url, _redact_netloc)[0] @@ -1302,7 +1309,7 @@ def hide_value(value): def hide_url(url): # type: (str) -> HiddenText - redacted = redact_password_from_url(url) + redacted = redact_auth_from_url(url) return HiddenText(url, redacted=redacted) diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 51cf2a23a0d..6c1ad16f807 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -55,8 +55,8 @@ parse_netloc, path_to_display, path_to_url, + redact_auth_from_url, redact_netloc, - redact_password_from_url, remove_auth_from_url, rmtree, split_auth_from_netloc, @@ -1332,7 +1332,7 @@ def test_split_auth_netloc_from_url(url, expected): # Test a basic case. ('example.com', 'example.com'), # Test with username and no password. - ('user@example.com', 'user@example.com'), + ('accesstoken@example.com', '****@example.com'), # Test with username and password. ('user:pass@example.com', 'user:****@example.com'), # Test with username and empty password. @@ -1371,7 +1371,7 @@ def test_remove_auth_from_url(auth_url, expected_url): @pytest.mark.parametrize('auth_url, expected_url', [ - ('https://user@example.com/abc', 'https://user@example.com/abc'), + ('https://accesstoken@example.com/abc', 'https://****@example.com/abc'), ('https://user:password@example.com', 'https://user:****@example.com'), ('https://user:@example.com', 'https://user:****@example.com'), ('https://example.com', 'https://example.com'), @@ -1379,8 +1379,8 @@ def test_remove_auth_from_url(auth_url, expected_url): ('https://user%3Aname:%23%40%5E@example.com', 'https://user%3Aname:****@example.com'), ]) -def test_redact_password_from_url(auth_url, expected_url): - url = redact_password_from_url(auth_url) +def test_redact_auth_from_url(auth_url, expected_url): + url = redact_auth_from_url(auth_url) assert url == expected_url From 987b45af9ec719ce2ded8615bb7177979e688184 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Tue, 27 Aug 2019 13:35:55 +0530 Subject: [PATCH 0159/3170] Move docstring to appropriately placed comment --- tests/functional/test_warning.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/test_warning.py b/tests/functional/test_warning.py index 2542aa80c0b..edde04429ca 100644 --- a/tests/functional/test_warning.py +++ b/tests/functional/test_warning.py @@ -2,7 +2,6 @@ def test_environ(script, tmpdir): - """$PYTHONWARNINGS was added in python2.7""" demo = tmpdir.joinpath('warnings_demo.py') demo.write_text(textwrap.dedent(''' from logging import basicConfig @@ -18,6 +17,7 @@ def test_environ(script, tmpdir): expected = 'WARNING:pip._internal.deprecations:DEPRECATION: deprecated!\n' assert result.stderr == expected + # $PYTHONWARNINGS was added in python2.7 script.environ['PYTHONWARNINGS'] = 'ignore' result = script.run('python', demo) assert result.stderr == '' From 0fc90d5d1fd950b81473a55e48d928c06d6b0f0d Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Tue, 27 Aug 2019 13:37:51 +0530 Subject: [PATCH 0160/3170] Improve flow of setting InstallRequirement.use_pep517 --- src/pip/_internal/req/req_install.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index d57804da188..339c2234810 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -521,11 +521,11 @@ def load_pyproject_toml(self): str(self) ) - self.use_pep517 = (pyproject_toml_data is not None) - - if not self.use_pep517: + if pyproject_toml_data is None: + self.use_pep517 = False return + self.use_pep517 = True requires, backend, check = pyproject_toml_data self.requirements_to_check = check self.pyproject_requires = requires From 506a08bc38ca059950ed311d337ec74ab89bfd91 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Tue, 27 Aug 2019 13:55:20 +0530 Subject: [PATCH 0161/3170] Rename `tox lint-py3` to `tox lint` Also update references to it, in Travis CI config and in documentation. --- .travis.yml | 2 +- docs/html/development/getting-started.rst | 4 ++-- tools/travis/run.sh | 4 ++-- tox.ini | 4 ++-- 4 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index c0aac7a4903..48c99a2208c 100644 --- a/.travis.yml +++ b/.travis.yml @@ -16,9 +16,9 @@ jobs: # Basic Checks - stage: primary env: TOXENV=docs + - env: TOXENV=lint - env: TOXENV=lint-py2 python: 2.7 - - env: TOXENV=lint-py3 - env: TOXENV=mypy - env: TOXENV=packaging # Latest CPython diff --git a/docs/html/development/getting-started.rst b/docs/html/development/getting-started.rst index 3cf23e8be1d..3fbd11de4d7 100644 --- a/docs/html/development/getting-started.rst +++ b/docs/html/development/getting-started.rst @@ -75,10 +75,10 @@ To use linters locally, run: .. code-block:: console + $ tox -e lint $ tox -e lint-py2 - $ tox -e lint-py3 -The above commands run the linters on Python 2 followed by Python 3. +The above commands run the linters on Python 3 followed by Python 2. .. note:: diff --git a/tools/travis/run.sh b/tools/travis/run.sh index 2e418d699e7..a7b15d0822a 100755 --- a/tools/travis/run.sh +++ b/tools/travis/run.sh @@ -2,8 +2,8 @@ set -e # Short circuit tests and linting jobs if there are no code changes involved. -if [[ $TOXENV != docs ]] && [[ $TOXENV != lint-py2 ]] && [[ $TOXENV != lint-py3 ]]; then - # Keep lint-py2 & lint-py3 for docs/conf.py +if [[ $TOXENV != docs ]] && [[ $TOXENV != lint-py2 ]] && [[ $TOXENV != lint ]]; then + # Keep lint and lint-py2, for docs/conf.py if [[ "$TRAVIS_PULL_REQUEST" == "false" ]] then echo "This is not a PR -- will do a complete build." diff --git a/tox.ini b/tox.ini index 608d491e3fe..0abf0b061bc 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] minversion = 3.4.0 envlist = - docs, packaging, lint-py2, lint-py3, mypy, + docs, packaging, mypy, lint, lint-py2, py27, py35, py36, py37, py38, pypy, pypy3 [helpers] @@ -63,7 +63,7 @@ commands = flake8 src tests isort --check-only --diff -[testenv:lint-py3] +[testenv:lint] skip_install = True basepython = python3 deps = {[lint]deps} From 7d84255929019e3a15c625896498002351ca39a6 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Tue, 27 Aug 2019 13:56:13 +0530 Subject: [PATCH 0162/3170] Switch order of lint and lint-py2 --- tox.ini | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/tox.ini b/tox.ini index 0abf0b061bc..c10d9eae77b 100644 --- a/tox.ini +++ b/tox.ini @@ -53,23 +53,23 @@ commands = [lint] deps = -r{toxinidir}/tools/requirements/lint.txt -[testenv:lint-py2] +[testenv:lint] skip_install = True -basepython = python2 +basepython = python3 deps = {[lint]deps} commands_pre = -# No need to flake8 docs, tools & tasks in py2 commands = - flake8 src tests + flake8 isort --check-only --diff -[testenv:lint] +[testenv:lint-py2] skip_install = True -basepython = python3 +basepython = python2 deps = {[lint]deps} commands_pre = +# No need to flake8 docs, tools & tasks in py2 commands = - flake8 + flake8 src tests isort --check-only --diff [testenv:mypy] From ea1bbd64e0aa4e1d919be829ebeb3b2a7cc753b9 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Tue, 27 Aug 2019 14:05:51 +0530 Subject: [PATCH 0163/3170] Only ignore 'build' and 'dist' in root directory --- .gitignore | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.gitignore b/.gitignore index fd367df9396..709c24b0369 100644 --- a/.gitignore +++ b/.gitignore @@ -4,8 +4,8 @@ __pycache__/ *$py.class # Distribution / packaging -build/ -dist/ +/build/ +/dist/ *.egg *.eggs *.egg-info/ From 6e3eae5e2a2ba6d0f49087695543c071de382922 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Tue, 27 Aug 2019 14:06:17 +0530 Subject: [PATCH 0164/3170] Ignore 'build' directory in linting tools --- setup.cfg | 2 ++ 1 file changed, 2 insertions(+) diff --git a/setup.cfg b/setup.cfg index cd5a2b256d6..4c6f7c5f142 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,5 +1,6 @@ [isort] skip = + ./build, .tox, .scratch, _vendor, @@ -15,6 +16,7 @@ include_trailing_comma = true [flake8] exclude = + ./build, .tox, .scratch, _vendor, From e9d8e1d3261a16b3234f208bc4ba4af1a4ce8e0e Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Sun, 4 Aug 2019 12:14:20 +0530 Subject: [PATCH 0165/3170] Move distributions/{source.py -> source/legacy.py} MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This would simplify for splitting up the build logic, in follow up commits. --- src/pip/_internal/distributions/source/__init__.py | 0 src/pip/_internal/distributions/{source.py => source/legacy.py} | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 src/pip/_internal/distributions/source/__init__.py rename src/pip/_internal/distributions/{source.py => source/legacy.py} (100%) diff --git a/src/pip/_internal/distributions/source/__init__.py b/src/pip/_internal/distributions/source/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/distributions/source.py b/src/pip/_internal/distributions/source/legacy.py similarity index 100% rename from src/pip/_internal/distributions/source.py rename to src/pip/_internal/distributions/source/legacy.py From 026fd7bbe83100a4df50f5a77be099ddc609ad14 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Tue, 27 Aug 2019 14:54:08 +0530 Subject: [PATCH 0166/3170] Switch to inline tables in pyproject.toml --- pyproject.toml | 44 +++++++++----------------------------------- 1 file changed, 9 insertions(+), 35 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index e3be2b3dc56..919965c27cc 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,38 +9,12 @@ directory = "news/" title_format = "{version} ({project_date})" issue_format = "`#{issue} `_" template = "news/_template.rst" - - [[tool.towncrier.type]] - directory = "process" - name = "Process" - showcontent = true - - [[tool.towncrier.type]] - directory = "removal" - name = "Deprecations and Removals" - showcontent = true - - [[tool.towncrier.type]] - directory = "feature" - name = "Features" - showcontent = true - - [[tool.towncrier.type]] - directory = "bugfix" - name = "Bug Fixes" - showcontent = true - - [[tool.towncrier.type]] - directory = "vendor" - name = "Vendored Libraries" - showcontent = true - - [[tool.towncrier.type]] - directory = "doc" - name = "Improved Documentation" - showcontent = true - - [[tool.towncrier.type]] - directory = "trivial" - name = "Trivial Changes" - showcontent = false +type = [ + { name = "Process", directory = "process", showcontent = true }, + { name = "Deprecations and Removals", directory = "removal", showcontent = true }, + { name = "Features", directory = "feature", showcontent = true }, + { name = "Bug Fixes", directory = "bugfix", showcontent = true }, + { name = "Vendored Libraries", directory = "vendor", showcontent = true }, + { name = "Improved Documentation", directory = "doc", showcontent = true }, + { name = "Trivial Changes", directory = "trivial", showcontent = false }, +] From b731b012e0e96fae4870770800208ecc038a3e51 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Tue, 27 Aug 2019 15:14:54 +0530 Subject: [PATCH 0167/3170] Update import statement --- src/pip/_internal/distributions/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/distributions/__init__.py b/src/pip/_internal/distributions/__init__.py index fdf332a8138..12b6831ee44 100644 --- a/src/pip/_internal/distributions/__init__.py +++ b/src/pip/_internal/distributions/__init__.py @@ -1,4 +1,4 @@ -from pip._internal.distributions.source import SourceDistribution +from pip._internal.distributions.source.legacy import SourceDistribution from pip._internal.distributions.wheel import WheelDistribution from pip._internal.utils.typing import MYPY_CHECK_RUNNING From 24bd7418b21d124f9c8f1d7e31f26efba9297788 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Sat, 31 Aug 2019 10:51:10 +0200 Subject: [PATCH 0168/3170] Add initial GH actions linter workflow --- .github/workflows/python-linters.yml | 46 ++++++++++++++++++++++++++++ 1 file changed, 46 insertions(+) create mode 100644 .github/workflows/python-linters.yml diff --git a/.github/workflows/python-linters.yml b/.github/workflows/python-linters.yml new file mode 100644 index 00000000000..367245642bf --- /dev/null +++ b/.github/workflows/python-linters.yml @@ -0,0 +1,46 @@ +name: Code quality + +on: + push: + pull_request: + schedule: + # Run every Friday at 18:02 UTC + # https://crontab.guru/#2_18_*_*_5 + - cron: 2 18 * * 5 + +jobs: + linters: + name: 🤖 + runs-on: ${{ matrix.os }} + strategy: + # max-parallel: 5 + matrix: + os: + - ubuntu-18.04 + env: + - TOXENV: docs + - TOXENV: lint + - TOXENV: lint-py2 + PYTHON_VERSION: 2.7 + - TOXENV: mypy + - TOXENV: packaging + steps: + - uses: actions/checkout@master + - name: Set up Python ${{ matrix.env.PYTHON_VERSION || 3.7 }} + uses: actions/setup-python@v1 + with: + version: ${{ matrix.env.PYTHON_VERSION || 3.7 }} + - name: Pre-configure global Git settings + run: >- + tools/travis/setup.sh + - name: Update setuptools and tox dependencies + run: >- + tools/travis/install.sh + - name: 'Initialize tox envs: ${{ matrix.env.TOXENV }}' + run: >- + python -m tox --parallel auto --notest + env: ${{ matrix.env }} + - name: Test with tox + run: >- + python -m tox --parallel 0 + env: ${{ matrix.env }} From a9d23fadbb678338b688dac18fff4f8a3f67b969 Mon Sep 17 00:00:00 2001 From: Albert Tugushev Date: Sat, 31 Aug 2019 18:13:44 +0300 Subject: [PATCH 0169/3170] Use pytest.param to skip certain parametrizations (#6944) --- news/revisit-test-clean-link.trivial | 1 + tests/unit/test_index.py | 38 +++++++++------------------- 2 files changed, 13 insertions(+), 26 deletions(-) create mode 100644 news/revisit-test-clean-link.trivial diff --git a/news/revisit-test-clean-link.trivial b/news/revisit-test-clean-link.trivial new file mode 100644 index 00000000000..341acee554a --- /dev/null +++ b/news/revisit-test-clean-link.trivial @@ -0,0 +1 @@ +Use pytest.param to skip certain parametrizations. diff --git a/tests/unit/test_index.py b/tests/unit/test_index.py index c27b3589d86..9f53c0b4af2 100644 --- a/tests/unit/test_index.py +++ b/tests/unit/test_index.py @@ -949,39 +949,25 @@ def test_request_retries(caplog): "https://localhost.localdomain/T%3A/path/"), # VCS URL containing revision string. ("git+ssh://example.com/path to/repo.git@1.0#egg=my-package-1.0", - "git+ssh://example.com/path%20to/repo.git@1.0#egg=my-package-1.0") - ] -) -def test_clean_link(url, clean_url): - assert(_clean_link(url) == clean_url) - - -@pytest.mark.parametrize( - ("url", "clean_url"), - [ + "git+ssh://example.com/path%20to/repo.git@1.0#egg=my-package-1.0"), # URL with Windows drive letter. The `:` after the drive # letter should not be quoted. The trailing `/` should be # removed. - ("file:///T:/path/with spaces/", - "file:///T:/path/with%20spaces") - ] -) -@pytest.mark.skipif("sys.platform != 'win32'") -def test_clean_link_windows(url, clean_url): - assert(_clean_link(url) == clean_url) - - -@pytest.mark.parametrize( - ("url", "clean_url"), - [ + pytest.param( + "file:///T:/path/with spaces/", + "file:///T:/path/with%20spaces", + marks=pytest.mark.skipif("sys.platform != 'win32'"), + ), # URL with Windows drive letter, running on non-windows # platform. The `:` after the drive should be quoted. - ("file:///T:/path/with spaces/", - "file:///T%3A/path/with%20spaces/") + pytest.param( + "file:///T:/path/with spaces/", + "file:///T%3A/path/with%20spaces/", + marks=pytest.mark.skipif("sys.platform == 'win32'"), + ), ] ) -@pytest.mark.skipif("sys.platform == 'win32'") -def test_clean_link_non_windows(url, clean_url): +def test_clean_link(url, clean_url): assert(_clean_link(url) == clean_url) From c674fabe954de919bc1d0950d86bb938fc056441 Mon Sep 17 00:00:00 2001 From: ofrinevo Date: Sat, 31 Aug 2019 21:44:41 +0300 Subject: [PATCH 0170/3170] Fix package name not being canonicalize in commands/show Add test to check if a capitalized name is being shown properly Add new stub package to support the test --- src/pip/_internal/commands/show.py | 3 ++- .../requires_capitalized/__init__.py | 1 + tests/data/src/requires_capitalized/setup.py | 6 ++++++ tests/functional/test_show.py | 19 ++++++++++++++++++- 4 files changed, 27 insertions(+), 2 deletions(-) create mode 100644 tests/data/src/requires_capitalized/requires_capitalized/__init__.py create mode 100644 tests/data/src/requires_capitalized/setup.py diff --git a/src/pip/_internal/commands/show.py b/src/pip/_internal/commands/show.py index 8ae27699bc9..0e7023e86fc 100644 --- a/src/pip/_internal/commands/show.py +++ b/src/pip/_internal/commands/show.py @@ -138,9 +138,10 @@ def print_results(distributions, list_files=False, verbose=False): write_output("---") name = dist.get('name', '') + canonical_name = canonicalize_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()] + if canonical_name in [required.name for required in pkg.requires()] ] write_output("Name: %s", name) diff --git a/tests/data/src/requires_capitalized/requires_capitalized/__init__.py b/tests/data/src/requires_capitalized/requires_capitalized/__init__.py new file mode 100644 index 00000000000..792d6005489 --- /dev/null +++ b/tests/data/src/requires_capitalized/requires_capitalized/__init__.py @@ -0,0 +1 @@ +# diff --git a/tests/data/src/requires_capitalized/setup.py b/tests/data/src/requires_capitalized/setup.py new file mode 100644 index 00000000000..39b75ba05b8 --- /dev/null +++ b/tests/data/src/requires_capitalized/setup.py @@ -0,0 +1,6 @@ +from setuptools import find_packages, setup + +setup(name='Requires_Capitalized', + version='0.1', + install_requires=['simple==1.0'] + ) diff --git a/tests/functional/test_show.py b/tests/functional/test_show.py index 8f63d3eca24..fb710089220 100644 --- a/tests/functional/test_show.py +++ b/tests/functional/test_show.py @@ -205,7 +205,7 @@ def test_package_name_is_canonicalized(script, data): assert underscore_upper_show_result.stdout == dash_show_result.stdout -def test_show_required_by_packages(script, data): +def test_show_required_by_packages_simple(script, data): """ Test that installed packages that depend on this package are shown """ @@ -219,3 +219,20 @@ def test_show_required_by_packages(script, data): assert 'Name: simple' in lines assert 'Required-by: requires-simple' in lines + + +def test_show_required_by_packages_capitalized(script, data): + """ + Test that installed packages that depend on this package are shown where the package has a capital letter + """ + editable_path = os.path.join(data.src, 'requires_capitalized') + script.pip( + 'install', '--no-index', '-f', data.find_links, editable_path + ) + + result = script.pip('show', 'simple') + lines = result.stdout.splitlines() + print(lines) + + assert 'Name: simple' in lines + assert 'Required-by: Requires-Capitalized' in lines From fa4fc6a6d470719f67789b8c15ed7eb2cdbbe408 Mon Sep 17 00:00:00 2001 From: ofrinevo Date: Sat, 31 Aug 2019 21:52:04 +0300 Subject: [PATCH 0171/3170] Add news file --- news/6947.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/6947.bugfix diff --git a/news/6947.bugfix b/news/6947.bugfix new file mode 100644 index 00000000000..ad121de7fdd --- /dev/null +++ b/news/6947.bugfix @@ -0,0 +1 @@ +Fix not displaying all dependent packages under "Required-by:" in pip show. \ No newline at end of file From ce17cf9c3280c4afc26cf409de5821bae7277b9b Mon Sep 17 00:00:00 2001 From: ofrinevo Date: Sat, 31 Aug 2019 22:09:41 +0300 Subject: [PATCH 0172/3170] Fix test_show_required_by_packages_capitalized documentation being too long --- tests/functional/test_show.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/functional/test_show.py b/tests/functional/test_show.py index fb710089220..b12a8449292 100644 --- a/tests/functional/test_show.py +++ b/tests/functional/test_show.py @@ -223,7 +223,8 @@ def test_show_required_by_packages_simple(script, data): def test_show_required_by_packages_capitalized(script, data): """ - Test that installed packages that depend on this package are shown where the package has a capital letter + Test that installed packages that depend on this package are shown + where the package has a capital letter """ editable_path = os.path.join(data.src, 'requires_capitalized') script.pip( From b951dd954e37ed0d86f63f6865c433bfe27052b7 Mon Sep 17 00:00:00 2001 From: ofrinevo Date: Sat, 31 Aug 2019 22:38:04 +0300 Subject: [PATCH 0173/3170] Remove a print command left by mistake in the test --- tests/functional/test_show.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/functional/test_show.py b/tests/functional/test_show.py index b12a8449292..d48c9196e65 100644 --- a/tests/functional/test_show.py +++ b/tests/functional/test_show.py @@ -233,7 +233,6 @@ def test_show_required_by_packages_capitalized(script, data): result = script.pip('show', 'simple') lines = result.stdout.splitlines() - print(lines) assert 'Name: simple' in lines assert 'Required-by: Requires-Capitalized' in lines From 9b6c0ea8ff2a9e8a0b998229d371b1dc567954dc Mon Sep 17 00:00:00 2001 From: ofrinevo Date: Sun, 1 Sep 2019 07:53:26 +0300 Subject: [PATCH 0174/3170] Extract the canonical_name from name itself instead of using dist.get twice --- src/pip/_internal/commands/show.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/commands/show.py b/src/pip/_internal/commands/show.py index 0e7023e86fc..195803b31c7 100644 --- a/src/pip/_internal/commands/show.py +++ b/src/pip/_internal/commands/show.py @@ -138,7 +138,7 @@ def print_results(distributions, list_files=False, verbose=False): write_output("---") name = dist.get('name', '') - canonical_name = canonicalize_name(dist.get('name', '')) + canonical_name = canonicalize_name(name) required_by = [ pkg.project_name for pkg in pkg_resources.working_set if canonical_name in [required.name for required in pkg.requires()] From a05de613fc9da33b7753b0893a2077e380cee834 Mon Sep 17 00:00:00 2001 From: ofrinevo Date: Sun, 1 Sep 2019 21:28:31 +0300 Subject: [PATCH 0175/3170] Add a test that checks for mixed upper and lower case letters in pip show Remove useless import in stubs for named tests --- .../__init__.py | 1 + .../required_by_mixed_capitalization/setup.py | 6 ++++++ tests/data/src/requires_capitalized/setup.py | 2 +- tests/functional/test_show.py | 18 ++++++++++++++++++ 4 files changed, 26 insertions(+), 1 deletion(-) create mode 100644 tests/data/src/required_by_mixed_capitalization/required_by_mixed_capitalization/__init__.py create mode 100644 tests/data/src/required_by_mixed_capitalization/setup.py diff --git a/tests/data/src/required_by_mixed_capitalization/required_by_mixed_capitalization/__init__.py b/tests/data/src/required_by_mixed_capitalization/required_by_mixed_capitalization/__init__.py new file mode 100644 index 00000000000..792d6005489 --- /dev/null +++ b/tests/data/src/required_by_mixed_capitalization/required_by_mixed_capitalization/__init__.py @@ -0,0 +1 @@ +# diff --git a/tests/data/src/required_by_mixed_capitalization/setup.py b/tests/data/src/required_by_mixed_capitalization/setup.py new file mode 100644 index 00000000000..01bcff4a458 --- /dev/null +++ b/tests/data/src/required_by_mixed_capitalization/setup.py @@ -0,0 +1,6 @@ +from setuptools import setup + +setup(name='requires_Capitalized', + version='0.1', + install_requires=['simple==1.0'] + ) diff --git a/tests/data/src/requires_capitalized/setup.py b/tests/data/src/requires_capitalized/setup.py index 39b75ba05b8..b3f37b919a6 100644 --- a/tests/data/src/requires_capitalized/setup.py +++ b/tests/data/src/requires_capitalized/setup.py @@ -1,4 +1,4 @@ -from setuptools import find_packages, setup +from setuptools import setup setup(name='Requires_Capitalized', version='0.1', diff --git a/tests/functional/test_show.py b/tests/functional/test_show.py index d48c9196e65..b5ea8fd8e58 100644 --- a/tests/functional/test_show.py +++ b/tests/functional/test_show.py @@ -236,3 +236,21 @@ def test_show_required_by_packages_capitalized(script, data): assert 'Name: simple' in lines assert 'Required-by: Requires-Capitalized' in lines + + +def test_show_required_by_with_mixed_capitalization(script, data): + """ + Test that installed packages that depend on this package are shown + where the package requires a name with a mix of + lower and upper case letters + """ + editable_path = os.path.join(data.src, 'required_by_mixed_capitalization') + script.pip( + 'install', '--no-index', '-f', data.find_links, editable_path + ) + + result = script.pip('show', 'Requires_CapitalizeD') + lines = result.stdout.splitlines() + + assert 'Name: Requires-Capitalized' in lines + assert 'Required-by: simple' in lines From 35c2161785c7aab7ce641b0d57d4f77e482a0e82 Mon Sep 17 00:00:00 2001 From: ofrinevo Date: Sun, 1 Sep 2019 21:32:00 +0300 Subject: [PATCH 0176/3170] Improve the documentation of the added tests --- tests/functional/test_show.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/functional/test_show.py b/tests/functional/test_show.py index b5ea8fd8e58..7709d6bb113 100644 --- a/tests/functional/test_show.py +++ b/tests/functional/test_show.py @@ -223,7 +223,7 @@ def test_show_required_by_packages_simple(script, data): def test_show_required_by_packages_capitalized(script, data): """ - Test that installed packages that depend on this package are shown + Test that the installed packages which depend on a package are shown where the package has a capital letter """ editable_path = os.path.join(data.src, 'requires_capitalized') @@ -240,8 +240,8 @@ def test_show_required_by_packages_capitalized(script, data): def test_show_required_by_with_mixed_capitalization(script, data): """ - Test that installed packages that depend on this package are shown - where the package requires a name with a mix of + Test that the installed packages which depend on a package are shown + where the package has a name with a mix of lower and upper case letters """ editable_path = os.path.join(data.src, 'required_by_mixed_capitalization') From 9f2ef615be2e255141592bcb49206f3f0eedb12f Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Sun, 1 Sep 2019 21:17:09 +0200 Subject: [PATCH 0177/3170] Add a news fragment --- news/6952-gh-actions--linters.trivial | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/6952-gh-actions--linters.trivial diff --git a/news/6952-gh-actions--linters.trivial b/news/6952-gh-actions--linters.trivial new file mode 100644 index 00000000000..194e39025c0 --- /dev/null +++ b/news/6952-gh-actions--linters.trivial @@ -0,0 +1 @@ +Add a GitHub Actions workflow running all linters. From b0a7d2b503a04c1cd7d3b505e009ad350e01ac23 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Sun, 21 Jul 2019 22:49:51 -0400 Subject: [PATCH 0178/3170] Refactor wheel.move_wheel_files to use updated distlib. --- src/pip/_internal/wheel.py | 115 +++++++++++++++++++------------------ tests/unit/test_wheel.py | 14 +++++ 2 files changed, 73 insertions(+), 56 deletions(-) diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index bc0cdd260b1..0f4c9070627 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -23,6 +23,7 @@ from pip._vendor import pkg_resources from pip._vendor.distlib.scripts import ScriptMaker +from pip._vendor.distlib.util import get_export_entry from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.six import StringIO @@ -311,6 +312,22 @@ def get_csv_rows_for_installed( return installed_rows +class MissingCallableSuffix(Exception): + pass + + +def _assert_valid_entrypoint(specification): + entry = get_export_entry(specification) + if entry is not None and entry.suffix is None: + raise MissingCallableSuffix(str(entry)) + + +class PipScriptMaker(ScriptMaker): + def make(self, specification, options=None): + _assert_valid_entrypoint(specification) + return super(PipScriptMaker, self).make(specification, options) + + def move_wheel_files( name, # type: str req, # type: Requirement @@ -473,7 +490,7 @@ def is_entrypoint_wrapper(name): dest = scheme[subdir] clobber(source, dest, False, fixer=fixer, filter=filter) - maker = ScriptMaker(None, scheme['scripts']) + maker = PipScriptMaker(None, scheme['scripts']) # Ensure old scripts are overwritten. # See https://github.com/pypa/pip/issues/1800 @@ -489,36 +506,7 @@ def is_entrypoint_wrapper(name): # See https://bitbucket.org/pypa/distlib/issue/32/ maker.set_mode = True - # Simplify the script and fix the fact that the default script swallows - # every single stack trace. - # See https://bitbucket.org/pypa/distlib/issue/34/ - # See https://bitbucket.org/pypa/distlib/issue/33/ - def _get_script_text(entry): - if entry.suffix is None: - raise InstallationError( - "Invalid script entry point: %s for req: %s - A callable " - "suffix is required. Cf https://packaging.python.org/en/" - "latest/distributing.html#console-scripts for more " - "information." % (entry, req) - ) - return maker.script_template % { - "module": entry.prefix, - "import_name": entry.suffix.split(".")[0], - "func": entry.suffix, - } - # ignore type, because mypy disallows assigning to a method, - # see https://github.com/python/mypy/issues/2427 - maker._get_script_text = _get_script_text # type: ignore - maker.script_template = r"""# -*- coding: utf-8 -*- -import re -import sys - -from %(module)s import %(import_name)s - -if __name__ == '__main__': - sys.argv[0] = re.sub(r'(-script\.pyw?|\.exe)?$', '', sys.argv[0]) - sys.exit(%(func)s()) -""" + scripts_to_generate = [] # Special case pip and setuptools to generate versioned wrappers # @@ -556,15 +544,16 @@ def _get_script_text(entry): pip_script = console.pop('pip', None) if pip_script: if "ENSUREPIP_OPTIONS" not in os.environ: - spec = 'pip = ' + pip_script - generated.extend(maker.make(spec)) + scripts_to_generate.append('pip = ' + pip_script) if os.environ.get("ENSUREPIP_OPTIONS", "") != "altinstall": - spec = 'pip%s = %s' % (sys.version_info[0], pip_script) - generated.extend(maker.make(spec)) + scripts_to_generate.append( + 'pip%s = %s' % (sys.version_info[0], pip_script) + ) - spec = 'pip%s = %s' % (get_major_minor_version(), pip_script) - generated.extend(maker.make(spec)) + scripts_to_generate.append( + 'pip%s = %s' % (get_major_minor_version(), pip_script) + ) # Delete any other versioned pip entry points pip_ep = [k for k in console if re.match(r'pip(\d(\.\d)?)?$', k)] for k in pip_ep: @@ -572,13 +561,15 @@ def _get_script_text(entry): easy_install_script = console.pop('easy_install', None) if easy_install_script: if "ENSUREPIP_OPTIONS" not in os.environ: - spec = 'easy_install = ' + easy_install_script - generated.extend(maker.make(spec)) + scripts_to_generate.append( + 'easy_install = ' + easy_install_script + ) - spec = 'easy_install-%s = %s' % ( - get_major_minor_version(), easy_install_script, + scripts_to_generate.append( + 'easy_install-%s = %s' % ( + get_major_minor_version(), easy_install_script + ) ) - generated.extend(maker.make(spec)) # Delete any other versioned easy_install entry points easy_install_ep = [ k for k in console if re.match(r'easy_install(-\d\.\d)?$', k) @@ -587,25 +578,37 @@ def _get_script_text(entry): del console[k] # Generate the console and GUI entry points specified in the wheel - if len(console) > 0: - generated_console_scripts = maker.make_multiple( - ['%s = %s' % kv for kv in console.items()] - ) - generated.extend(generated_console_scripts) + scripts_to_generate.extend( + '%s = %s' % kv for kv in console.items() + ) + + gui_scripts_to_generate = [ + '%s = %s' % kv for kv in gui.items() + ] + + generated_console_scripts = [] # type: List[str] - if warn_script_location: - msg = message_about_scripts_not_on_PATH(generated_console_scripts) - if msg is not None: - logger.warning(msg) + try: + generated_console_scripts = maker.make_multiple(scripts_to_generate) + generated.extend(generated_console_scripts) - if len(gui) > 0: generated.extend( - maker.make_multiple( - ['%s = %s' % kv for kv in gui.items()], - {'gui': True} - ) + maker.make_multiple(gui_scripts_to_generate, {'gui': True}) + ) + except MissingCallableSuffix as e: + entry = e.args[0] + raise InstallationError( + "Invalid script entry point: %s for req: %s - A callable " + "suffix is required. Cf https://packaging.python.org/en/" + "latest/distributing.html#console-scripts for more " + "information." % (entry, req) ) + if warn_script_location: + msg = message_about_scripts_not_on_PATH(generated_console_scripts) + if msg is not None: + logger.warning(msg) + # Record pip as the installer installer = os.path.join(info_dir[0], 'INSTALLER') temp_installer = os.path.join(info_dir[0], 'INSTALLER.pip') diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 449da28c199..d794aeab5ca 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -15,6 +15,7 @@ from pip._internal.req.req_install import InstallRequirement from pip._internal.utils.compat import WINDOWS from pip._internal.utils.misc import unpack_file +from pip._internal.wheel import MissingCallableSuffix, _assert_valid_entrypoint from tests.lib import DATA_DIR, assert_paths_equal @@ -265,6 +266,19 @@ def test_get_entrypoints(tmpdir, console_scripts): ) +def test_assert_valid_entrypoint_ok(): + _assert_valid_entrypoint("hello = hello:main") + + +@pytest.mark.parametrize("entrypoint", [ + "hello = hello", + "hello = hello:", +]) +def test_assert_valid_entrypoint_fail(entrypoint): + with pytest.raises(MissingCallableSuffix): + _assert_valid_entrypoint(entrypoint) + + @pytest.mark.parametrize("outrows, expected", [ ([ ('', '', 'a'), From ea2a889ce3dccee56999ee6f0959d0dfe9213050 Mon Sep 17 00:00:00 2001 From: abs51295 Date: Sun, 11 Aug 2019 12:57:06 +0530 Subject: [PATCH 0179/3170] Canonicalize FrozenRequirement name for correct comparison - Fixes #5716 --- news/5716.bugfix | 1 + src/pip/_internal/operations/freeze.py | 38 +++++++++++++++----------- tests/functional/test_freeze.py | 20 ++++++++++++-- 3 files changed, 40 insertions(+), 19 deletions(-) create mode 100644 news/5716.bugfix diff --git a/news/5716.bugfix b/news/5716.bugfix new file mode 100644 index 00000000000..1b705df6f92 --- /dev/null +++ b/news/5716.bugfix @@ -0,0 +1 @@ +Fix case sensitive comparison of pip freeze when used with -r option. \ No newline at end of file diff --git a/src/pip/_internal/operations/freeze.py b/src/pip/_internal/operations/freeze.py index 6eaa85b19ec..c54402fd8b5 100644 --- a/src/pip/_internal/operations/freeze.py +++ b/src/pip/_internal/operations/freeze.py @@ -79,7 +79,7 @@ def freeze( continue if exclude_editable and req.editable: continue - installations[req.name] = req + installations[req.canonical_name] = req if requirement: # the options that don't get turned into an InstallRequirement @@ -138,22 +138,27 @@ def freeze( " (add #egg=PackageName to the URL to avoid" " this warning)" ) - elif line_req.name not in installations: - # either it's not installed, or it is installed - # but has been processed already - if not req_files[line_req.name]: - logger.warning( - "Requirement file [%s] contains %s, but " - "package %r is not installed", - req_file_path, - COMMENT_RE.sub('', line).strip(), line_req.name - ) + else: + line_req_canonical_name = canonicalize_name( + line_req.name) + if line_req_canonical_name not in installations: + # either it's not installed, or it is installed + # but has been processed already + if not req_files[line_req.name]: + logger.warning( + "Requirement file [%s] contains %s, but " + "package %r is not installed", + req_file_path, + COMMENT_RE.sub('', line).strip(), + line_req.name + ) + else: + req_files[line_req.name].append(req_file_path) else: + yield str(installations[ + line_req_canonical_name]).rstrip() + del installations[line_req_canonical_name] req_files[line_req.name].append(req_file_path) - 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). @@ -168,7 +173,7 @@ def freeze( ) for installation in sorted( installations.values(), key=lambda x: x.name.lower()): - if canonicalize_name(installation.name) not in skip: + if installation.canonical_name not in skip: yield str(installation).rstrip() @@ -238,6 +243,7 @@ class FrozenRequirement(object): def __init__(self, name, req, editable, comments=()): # type: (str, Union[str, Requirement], bool, Iterable[str]) -> None self.name = name + self.canonical_name = canonicalize_name(name) self.req = req self.editable = editable self.comments = comments diff --git a/tests/functional/test_freeze.py b/tests/functional/test_freeze.py index 77f83796abc..9db9973c1c0 100644 --- a/tests/functional/test_freeze.py +++ b/tests/functional/test_freeze.py @@ -502,15 +502,20 @@ def test_freeze_with_requirement_option(script): """ - script.scratch_path.joinpath("hint.txt").write_text(textwrap.dedent("""\ + script.scratch_path.joinpath("hint1.txt").write_text(textwrap.dedent("""\ INITools==0.1 NoExist==4.2 # A comment that ensures end of line comments work. simple==3.0; python_version > '1.0' """) + _freeze_req_opts) + script.scratch_path.joinpath("hint2.txt").write_text(textwrap.dedent("""\ + iniTools==0.1 + Noexist==4.2 # A comment that ensures end of line comments work. + Simple==3.0; python_version > '1.0' + """) + _freeze_req_opts) result = script.pip_install_local('initools==0.2') result = script.pip_install_local('simple') result = script.pip( - 'freeze', '--requirement', 'hint.txt', + 'freeze', '--requirement', 'hint1.txt', expect_stderr=True, ) expected = textwrap.dedent("""\ @@ -521,9 +526,18 @@ def test_freeze_with_requirement_option(script): expected += "## The following requirements were added by pip freeze:..." _check_output(result.stdout, expected) assert ( - "Requirement file [hint.txt] contains NoExist==4.2, but package " + "Requirement file [hint1.txt] contains NoExist==4.2, but package " "'NoExist' is not installed" ) in result.stderr + result = script.pip( + 'freeze', '--requirement', 'hint2.txt', + expect_stderr=True, + ) + _check_output(result.stdout, expected) + assert ( + "Requirement file [hint2.txt] contains Noexist==4.2, but package " + "'Noexist' is not installed" + ) in result.stderr def test_freeze_with_requirement_option_multiple(script): From 8d83afb27e7822dde1859b58fe76ce19b96d8206 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Mon, 2 Sep 2019 16:10:58 +0200 Subject: [PATCH 0180/3170] Disable tox spinner --- .github/workflows/python-linters.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-linters.yml b/.github/workflows/python-linters.yml index 367245642bf..5e85522409d 100644 --- a/.github/workflows/python-linters.yml +++ b/.github/workflows/python-linters.yml @@ -38,9 +38,9 @@ jobs: tools/travis/install.sh - name: 'Initialize tox envs: ${{ matrix.env.TOXENV }}' run: >- - python -m tox --parallel auto --notest + TOX_PARALLEL_NO_SPINNER=1 python -m tox --parallel auto --notest env: ${{ matrix.env }} - name: Test with tox run: >- - python -m tox --parallel 0 + TOX_PARALLEL_NO_SPINNER=1 python -m tox --parallel auto env: ${{ matrix.env }} From 7d095a2b0637ee9b6065bb2481ef2bd9943ea9fb Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Mon, 2 Sep 2019 16:15:12 +0200 Subject: [PATCH 0181/3170] Don't skip missing interpreters --- .github/workflows/python-linters.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-linters.yml b/.github/workflows/python-linters.yml index 5e85522409d..d61a24669ea 100644 --- a/.github/workflows/python-linters.yml +++ b/.github/workflows/python-linters.yml @@ -38,7 +38,7 @@ jobs: tools/travis/install.sh - name: 'Initialize tox envs: ${{ matrix.env.TOXENV }}' run: >- - TOX_PARALLEL_NO_SPINNER=1 python -m tox --parallel auto --notest + TOX_PARALLEL_NO_SPINNER=1 python -m tox --parallel auto --notest --skip-missing-interpreters false env: ${{ matrix.env }} - name: Test with tox run: >- From bc65d55d8b4eda1598aaf7ed7d7f0f1a71f4fb4f Mon Sep 17 00:00:00 2001 From: ofrinevo Date: Mon, 2 Sep 2019 19:41:51 +0300 Subject: [PATCH 0182/3170] Fix all failing tests --- src/pip/_internal/commands/show.py | 2 +- .../required_by_capitalized/__init__.py | 1 + tests/data/src/required_by_capitalized/setup.py | 5 +++++ .../src/required_by_mixed_capitalization/setup.py | 6 +++--- tests/functional/test_show.py | 12 ++++++++---- 5 files changed, 18 insertions(+), 8 deletions(-) create mode 100644 tests/data/src/required_by_capitalized/required_by_capitalized/__init__.py create mode 100644 tests/data/src/required_by_capitalized/setup.py diff --git a/src/pip/_internal/commands/show.py b/src/pip/_internal/commands/show.py index 195803b31c7..275b9e4cc05 100644 --- a/src/pip/_internal/commands/show.py +++ b/src/pip/_internal/commands/show.py @@ -141,7 +141,7 @@ def print_results(distributions, list_files=False, verbose=False): canonical_name = canonicalize_name(name) required_by = [ pkg.project_name for pkg in pkg_resources.working_set - if canonical_name in [required.name for required in pkg.requires()] + if canonical_name in [canonicalize_name(required.name) for required in pkg.requires()] ] write_output("Name: %s", name) diff --git a/tests/data/src/required_by_capitalized/required_by_capitalized/__init__.py b/tests/data/src/required_by_capitalized/required_by_capitalized/__init__.py new file mode 100644 index 00000000000..792d6005489 --- /dev/null +++ b/tests/data/src/required_by_capitalized/required_by_capitalized/__init__.py @@ -0,0 +1 @@ +# diff --git a/tests/data/src/required_by_capitalized/setup.py b/tests/data/src/required_by_capitalized/setup.py new file mode 100644 index 00000000000..86fd71e6354 --- /dev/null +++ b/tests/data/src/required_by_capitalized/setup.py @@ -0,0 +1,5 @@ +from setuptools import setup + +setup(name='Required_By_Capitalized', + version='1.0', + ) diff --git a/tests/data/src/required_by_mixed_capitalization/setup.py b/tests/data/src/required_by_mixed_capitalization/setup.py index 01bcff4a458..9922ea386e5 100644 --- a/tests/data/src/required_by_mixed_capitalization/setup.py +++ b/tests/data/src/required_by_mixed_capitalization/setup.py @@ -1,6 +1,6 @@ from setuptools import setup -setup(name='requires_Capitalized', - version='0.1', - install_requires=['simple==1.0'] +setup(name='simple', + version='1.0', + install_requires=['required_by_Capitalized==1.0'] ) diff --git a/tests/functional/test_show.py b/tests/functional/test_show.py index 7709d6bb113..2695871fc50 100644 --- a/tests/functional/test_show.py +++ b/tests/functional/test_show.py @@ -205,7 +205,7 @@ def test_package_name_is_canonicalized(script, data): assert underscore_upper_show_result.stdout == dash_show_result.stdout -def test_show_required_by_packages_simple(script, data): +def test_show_required_by_packages_basic(script, data): """ Test that installed packages that depend on this package are shown """ @@ -238,19 +238,23 @@ def test_show_required_by_packages_capitalized(script, data): assert 'Required-by: Requires-Capitalized' in lines -def test_show_required_by_with_mixed_capitalization(script, data): +def test_show_required_by_packages_requiring_capitalized(script, data): """ Test that the installed packages which depend on a package are shown where the package has a name with a mix of lower and upper case letters """ + required_package_path = os.path.join(data.src, 'required_by_capitalized') + script.pip( + 'install', '--no-index', '-f', data.find_links, required_package_path + ) editable_path = os.path.join(data.src, 'required_by_mixed_capitalization') script.pip( 'install', '--no-index', '-f', data.find_links, editable_path ) - result = script.pip('show', 'Requires_CapitalizeD') + result = script.pip('show', 'Required_By_Capitalized') lines = result.stdout.splitlines() - assert 'Name: Requires-Capitalized' in lines + assert 'Name: Required-By-Capitalized' in lines assert 'Required-by: simple' in lines From 61ff014e6be5731aade5e192ba8d68e0858cd0fe Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko Date: Mon, 2 Sep 2019 20:00:03 +0200 Subject: [PATCH 0183/3170] Don't parallelize tox Co-Authored-By: Pradyun Gedam --- .github/workflows/python-linters.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-linters.yml b/.github/workflows/python-linters.yml index d61a24669ea..bf3308b0223 100644 --- a/.github/workflows/python-linters.yml +++ b/.github/workflows/python-linters.yml @@ -38,9 +38,9 @@ jobs: tools/travis/install.sh - name: 'Initialize tox envs: ${{ matrix.env.TOXENV }}' run: >- - TOX_PARALLEL_NO_SPINNER=1 python -m tox --parallel auto --notest --skip-missing-interpreters false + python -m tox --notest --skip-missing-interpreters false env: ${{ matrix.env }} - name: Test with tox run: >- - TOX_PARALLEL_NO_SPINNER=1 python -m tox --parallel auto + python -m tox env: ${{ matrix.env }} From bdd3781858bc8a7ffb22bb3391230a1adc8defc6 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Mon, 2 Sep 2019 14:36:14 -0400 Subject: [PATCH 0184/3170] Remove unused return value from `InstallRequirement.ensure_source_dir` --- src/pip/_internal/req/req_install.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index c3c2754e342..4948ddc3528 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -749,7 +749,7 @@ def assert_source_matches_version(self): # For both source distributions and editables def ensure_has_source_dir(self, parent_dir): - # type: (str) -> str + # type: (str) -> None """Ensure that a source_dir is set. This will create a temporary build dir if the name of the requirement @@ -761,7 +761,6 @@ def ensure_has_source_dir(self, parent_dir): """ if self.source_dir is None: self.source_dir = self.build_location(parent_dir) - return self.source_dir # For editable installations def install_editable( From 6da50d089d1d41b8eaebf7e702da525621a4e720 Mon Sep 17 00:00:00 2001 From: ofrinevo Date: Mon, 2 Sep 2019 22:14:22 +0300 Subject: [PATCH 0185/3170] Move required_by calculations from print_results to search_packages_info --- src/pip/_internal/commands/show.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/commands/show.py b/src/pip/_internal/commands/show.py index 275b9e4cc05..568d96271dc 100644 --- a/src/pip/_internal/commands/show.py +++ b/src/pip/_internal/commands/show.py @@ -67,12 +67,21 @@ def search_packages_info(query): if missing: logger.warning('Package(s) not found: %s', ', '.join(missing)) + def get_requiring_packages(package_name): + return [ + pkg.project_name for pkg in pkg_resources.working_set + if canonicalize_name(package_name) in + [canonicalize_name(required.name) for required in + pkg.requires()] + ] + for dist in [installed[pkg] for pkg in query_names if pkg in installed]: package = { 'name': dist.project_name, 'version': dist.version, 'location': dist.location, 'requires': [dep.project_name for dep in dist.requires()], + 'required_by': get_requiring_packages(dist.project_name) } file_list = None metadata = None @@ -137,14 +146,7 @@ def print_results(distributions, list_files=False, verbose=False): if i > 0: write_output("---") - name = dist.get('name', '') - canonical_name = canonicalize_name(name) - required_by = [ - pkg.project_name for pkg in pkg_resources.working_set - if canonical_name in [canonicalize_name(required.name) for required in pkg.requires()] - ] - - write_output("Name: %s", name) + write_output("Name: %s", dist.get('name', '')) write_output("Version: %s", dist.get('version', '')) write_output("Summary: %s", dist.get('summary', '')) write_output("Home-page: %s", dist.get('home-page', '')) @@ -153,7 +155,7 @@ def print_results(distributions, list_files=False, verbose=False): write_output("License: %s", dist.get('license', '')) write_output("Location: %s", dist.get('location', '')) write_output("Requires: %s", ', '.join(dist.get('requires', []))) - write_output("Required-by: %s", ', '.join(required_by)) + write_output("Required-by: %s", ', '.join(dist.get('required_by', []))) if verbose: write_output("Metadata-Version: %s", From 9dd7e0d58a619252dda609d2a4ef2be8a0375413 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Mon, 2 Sep 2019 15:17:16 -0400 Subject: [PATCH 0186/3170] Clean up venv check in `InstallRequirement.egg_info_path`. --- src/pip/_internal/req/req_install.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index c3c2754e342..1803162c717 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -648,6 +648,12 @@ def run_egg_info(self): @property def egg_info_path(self): # type: () -> str + def looks_like_virtual_env(path): + return ( + os.path.lexists(os.path.join(path, 'bin', 'python')) or + os.path.exists(os.path.join(path, 'Scripts', 'Python.exe')) + ) + if self._egg_info_path is None: if self.editable: base = self.source_dir @@ -664,17 +670,7 @@ def egg_info_path(self): # a list while iterating over it can cause trouble. # (See https://github.com/pypa/pip/pull/462.) for dir in list(dirs): - # Don't search in anything that looks like a virtualenv - # environment - if ( - os.path.lexists( - os.path.join(root, dir, 'bin', 'python') - ) or - os.path.exists( - os.path.join( - root, dir, 'Scripts', 'Python.exe' - ) - )): + if looks_like_virtual_env(os.path.join(root, dir)): dirs.remove(dir) # Also don't search through tests elif dir == 'test' or dir == 'tests': From d45a544b4774c47af4a595a7f1658efbe87726d7 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Mon, 2 Sep 2019 15:52:26 -0400 Subject: [PATCH 0187/3170] Remove duplicate line in comment. --- src/pip/_internal/operations/prepare.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 3cb09d83c68..ea7139e4f8e 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -140,8 +140,7 @@ def prepare_linked_requirement( # 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` + # 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" From b1978bc608818e878b16972c5336942bdf03c41d Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Mon, 2 Sep 2019 19:36:00 -0400 Subject: [PATCH 0188/3170] Fix indentation: move expanduser to constructor --- src/pip/_internal/operations/prepare.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 3cb09d83c68..dfafcc39d47 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -76,6 +76,8 @@ def __init__( # Where still packed archives should be written to. If None, they are # not saved, and are deleted immediately after unpacking. + if download_dir: + download_dir = expanduser(download_dir) self.download_dir = download_dir # Where still-packed .whl files should be written to. If None, they are @@ -100,7 +102,6 @@ def _download_should_save(self): # type: () -> bool # 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: From b719cc003379819e2dd1b77303b019d5e5580882 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Mon, 2 Sep 2019 19:36:57 -0400 Subject: [PATCH 0189/3170] Fix indentation: fix indentation --- src/pip/_internal/operations/prepare.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index dfafcc39d47..85834119956 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -100,16 +100,16 @@ def __init__( @property def _download_should_save(self): # type: () -> bool - # TODO: Modify to reduce indentation needed - if 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 + if not self.download_dir: + return False + + if os.path.exists(self.download_dir): + return True + + logger.critical('Could not find download directory') + raise InstallationError( + "Could not find or access download directory '%s'" + % display_path(self.download_dir)) def prepare_linked_requirement( self, From efd43fd0fe8656f5138fcbfd5b7ca550bd2981a3 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Mon, 2 Sep 2019 19:37:33 -0400 Subject: [PATCH 0190/3170] Minor comment typo. --- src/pip/_internal/operations/prepare.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 85834119956..e3062036435 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -74,7 +74,7 @@ def __init__( self.build_dir = build_dir self.req_tracker = req_tracker - # Where still packed archives should be written to. If None, they are + # Where still-packed archives should be written to. If None, they are # not saved, and are deleted immediately after unpacking. if download_dir: download_dir = expanduser(download_dir) From 1c4881157e3f1e821fd9a0b7b74e4706f775bde1 Mon Sep 17 00:00:00 2001 From: ofrinevo Date: Tue, 3 Sep 2019 20:46:28 +0300 Subject: [PATCH 0191/3170] Remove useless python packages from required_cap Rename stub package required_by_mixed_cap to requires_requires_cap Move the canonicalize_name calculation outside of a loop --- src/pip/_internal/commands/show.py | 3 ++- .../required_by_capitalized/__init__.py | 1 - tests/data/src/required_by_capitalized/setup.py | 5 ----- .../required_by_mixed_capitalization/__init__.py | 1 - .../src/required_by_mixed_capitalization/setup.py | 6 ------ .../requires_capitalized/__init__.py | 1 - tests/data/src/requires_requires_capitalized/setup.py | 6 ++++++ tests/functional/test_show.py | 11 ++++++----- 8 files changed, 14 insertions(+), 20 deletions(-) delete mode 100644 tests/data/src/required_by_capitalized/required_by_capitalized/__init__.py delete mode 100644 tests/data/src/required_by_capitalized/setup.py delete mode 100644 tests/data/src/required_by_mixed_capitalization/required_by_mixed_capitalization/__init__.py delete mode 100644 tests/data/src/required_by_mixed_capitalization/setup.py delete mode 100644 tests/data/src/requires_capitalized/requires_capitalized/__init__.py create mode 100644 tests/data/src/requires_requires_capitalized/setup.py diff --git a/src/pip/_internal/commands/show.py b/src/pip/_internal/commands/show.py index 568d96271dc..3baf785bd22 100644 --- a/src/pip/_internal/commands/show.py +++ b/src/pip/_internal/commands/show.py @@ -68,9 +68,10 @@ def search_packages_info(query): logger.warning('Package(s) not found: %s', ', '.join(missing)) def get_requiring_packages(package_name): + canonical_name = canonicalize_name(package_name) return [ pkg.project_name for pkg in pkg_resources.working_set - if canonicalize_name(package_name) in + if canonical_name in [canonicalize_name(required.name) for required in pkg.requires()] ] diff --git a/tests/data/src/required_by_capitalized/required_by_capitalized/__init__.py b/tests/data/src/required_by_capitalized/required_by_capitalized/__init__.py deleted file mode 100644 index 792d6005489..00000000000 --- a/tests/data/src/required_by_capitalized/required_by_capitalized/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# diff --git a/tests/data/src/required_by_capitalized/setup.py b/tests/data/src/required_by_capitalized/setup.py deleted file mode 100644 index 86fd71e6354..00000000000 --- a/tests/data/src/required_by_capitalized/setup.py +++ /dev/null @@ -1,5 +0,0 @@ -from setuptools import setup - -setup(name='Required_By_Capitalized', - version='1.0', - ) diff --git a/tests/data/src/required_by_mixed_capitalization/required_by_mixed_capitalization/__init__.py b/tests/data/src/required_by_mixed_capitalization/required_by_mixed_capitalization/__init__.py deleted file mode 100644 index 792d6005489..00000000000 --- a/tests/data/src/required_by_mixed_capitalization/required_by_mixed_capitalization/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# diff --git a/tests/data/src/required_by_mixed_capitalization/setup.py b/tests/data/src/required_by_mixed_capitalization/setup.py deleted file mode 100644 index 9922ea386e5..00000000000 --- a/tests/data/src/required_by_mixed_capitalization/setup.py +++ /dev/null @@ -1,6 +0,0 @@ -from setuptools import setup - -setup(name='simple', - version='1.0', - install_requires=['required_by_Capitalized==1.0'] - ) diff --git a/tests/data/src/requires_capitalized/requires_capitalized/__init__.py b/tests/data/src/requires_capitalized/requires_capitalized/__init__.py deleted file mode 100644 index 792d6005489..00000000000 --- a/tests/data/src/requires_capitalized/requires_capitalized/__init__.py +++ /dev/null @@ -1 +0,0 @@ -# diff --git a/tests/data/src/requires_requires_capitalized/setup.py b/tests/data/src/requires_requires_capitalized/setup.py new file mode 100644 index 00000000000..d124d072846 --- /dev/null +++ b/tests/data/src/requires_requires_capitalized/setup.py @@ -0,0 +1,6 @@ +from setuptools import setup + +setup(name='requires_requires_capitalized', + version='1.0', + install_requires=['requires_Capitalized==0.1'] + ) diff --git a/tests/functional/test_show.py b/tests/functional/test_show.py index 2695871fc50..4cbccc39b54 100644 --- a/tests/functional/test_show.py +++ b/tests/functional/test_show.py @@ -244,17 +244,18 @@ def test_show_required_by_packages_requiring_capitalized(script, data): where the package has a name with a mix of lower and upper case letters """ - required_package_path = os.path.join(data.src, 'required_by_capitalized') + required_package_path = os.path.join(data.src, 'requires_capitalized') script.pip( 'install', '--no-index', '-f', data.find_links, required_package_path ) - editable_path = os.path.join(data.src, 'required_by_mixed_capitalization') + editable_path = os.path.join(data.src, 'requires_requires_capitalized') script.pip( 'install', '--no-index', '-f', data.find_links, editable_path ) - result = script.pip('show', 'Required_By_Capitalized') + result = script.pip('show', 'Requires_Capitalized') lines = result.stdout.splitlines() + print(lines) - assert 'Name: Required-By-Capitalized' in lines - assert 'Required-by: simple' in lines + assert 'Name: Requires-Capitalized' in lines + assert 'Required-by: requires-requires-capitalized' in lines From 0a246b98a909838e173b2ec36e462952b79dfc12 Mon Sep 17 00:00:00 2001 From: Christopher Hunt Date: Wed, 4 Sep 2019 02:54:23 -0400 Subject: [PATCH 0192/3170] Update src/pip/_internal/operations/prepare.py Co-Authored-By: Pradyun Gedam --- src/pip/_internal/operations/prepare.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index ea7139e4f8e..7bdcb43fdf8 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -140,7 +140,7 @@ def prepare_linked_requirement( # 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" From 7e9f81244c0cf4ff42b93105f782020d9c0d2631 Mon Sep 17 00:00:00 2001 From: Albert Tugushev Date: Wed, 4 Sep 2019 00:07:48 +0300 Subject: [PATCH 0193/3170] Remove contradictory debug log --- news/4547.trivial | 1 + src/pip/_internal/download.py | 2 -- 2 files changed, 1 insertion(+), 2 deletions(-) create mode 100644 news/4547.trivial diff --git a/news/4547.trivial b/news/4547.trivial new file mode 100644 index 00000000000..482644a5756 --- /dev/null +++ b/news/4547.trivial @@ -0,0 +1 @@ +Remove contradictory debug log diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index b681ccb0681..2b1c147bf38 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -1003,8 +1003,6 @@ def written_chunks(chunks): else: logger.info("Downloading %s", url) - logger.debug('Downloading from URL %s', link) - downloaded_chunks = written_chunks( progress_indicator( resp_read(CONTENT_CHUNK_SIZE), From 85a5ae12d29d9e89da17bfad7d68c65a6cce4db1 Mon Sep 17 00:00:00 2001 From: Albert Tugushev Date: Wed, 4 Sep 2019 04:01:17 +0300 Subject: [PATCH 0194/3170] Fix rmtree_errorhandler to skip non-existing dirs --- news/4910.bugfix | 1 + src/pip/_internal/utils/misc.py | 8 ++++- tests/unit/test_utils.py | 60 +++++++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) create mode 100644 news/4910.bugfix diff --git a/news/4910.bugfix b/news/4910.bugfix new file mode 100644 index 00000000000..e829dfc7467 --- /dev/null +++ b/news/4910.bugfix @@ -0,0 +1 @@ +Fix ``rmtree_errorhandler`` to skip non-existing directories. diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index f6e6e291ca7..af7c9028442 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -181,8 +181,14 @@ def rmtree_errorhandler(func, path, exc_info): """On Windows, the files in .svn are read-only, so when rmtree() tries to remove them, an exception is thrown. We catch that here, remove the read-only attribute, and hopefully continue without problems.""" + try: + is_readonly = os.stat(path).st_mode & stat.S_IREAD + except (IOError, OSError): + # The path already removed, nothing to do + return + # if file type currently read only - if os.stat(path).st_mode & stat.S_IREAD: + if is_readonly: # convert to read/write os.chmod(path, stat.S_IWRITE) # use the original function to repeat the operation diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 6c1ad16f807..ee6851ccdaa 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -59,6 +59,7 @@ redact_netloc, remove_auth_from_url, rmtree, + rmtree_errorhandler, split_auth_from_netloc, split_auth_netloc_from_url, untar_file, @@ -386,6 +387,65 @@ def test_unpack_zip(self, data): self.confirm_files() +def test_rmtree_errorhandler_not_existing_directory(tmpdir): + """ + Test rmtree_errorhandler ignores the given non-existing directory. + """ + not_existing_path = str(tmpdir / 'foo') + mock_func = Mock() + rmtree_errorhandler(mock_func, not_existing_path, None) + mock_func.assert_not_called() + + +def test_rmtree_errorhandler_readonly_directory(tmpdir): + """ + Test rmtree_errorhandler makes the given read-only directory writable. + """ + # Create read only directory + path = str((tmpdir / 'subdir').mkdir()) + os.chmod(path, stat.S_IREAD) + + # Make sure mock_func is called with the given path + mock_func = Mock() + rmtree_errorhandler(mock_func, path, None) + mock_func.assert_called_with(path) + + # Make sure the path is become writable + assert os.stat(path).st_mode & stat.S_IWRITE + + +def test_rmtree_errorhandler_reraises_error(tmpdir): + """ + Test rmtree_errorhandler reraises an exception + by the given non-readonly directory. + """ + # Create directory without read permission + path = str((tmpdir / 'subdir').mkdir()) + os.chmod(path, ~stat.S_IREAD) + + mock_func = Mock() + try: + # Make sure the handler reraises an exception. + # Note that the raise statement without expression and + # active exception in the current scope throws + # the RuntimeError on Python3 and the TypeError on Python2. + with pytest.raises((RuntimeError, TypeError)): + rmtree_errorhandler(mock_func, path, None) + finally: + # Restore the read permission to let the pytest to clean up temp dirs + os.chmod(path, stat.S_IREAD) + + mock_func.assert_not_called() + + +def test_rmtree_skips_not_existing_directory(): + """ + Test wrapped rmtree doesn't raise an error + by the given non-existing directory. + """ + rmtree.__wrapped__('non-existing-subdir') + + class Failer: def __init__(self, duration=1): self.succeed_after = time.time() + duration From ec4f59c8aa926e6a31b6e011ebf0bb190680f7e4 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Wed, 4 Sep 2019 20:13:42 -0400 Subject: [PATCH 0195/3170] Change 'bdist_wheel' to 'wheel build'. --- src/pip/_internal/wheel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index bc0cdd260b1..fabfc2dae44 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -812,7 +812,7 @@ def should_use_ephemeral_cache( if "binary" not in format_control.get_allowed_formats( canonicalize_name(req.name)): logger.info( - "Skipping bdist_wheel for %s, due to binaries " + "Skipping wheel build for %s, due to binaries " "being disabled for it.", req.name, ) return None From a4102d179aeb3fadd8945edc911622722e55f4c9 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Wed, 4 Sep 2019 20:55:11 -0400 Subject: [PATCH 0196/3170] Don't check allowed format in wheel.py, pass function to do check. --- src/pip/_internal/commands/install.py | 18 ++++++++++++++++++ src/pip/_internal/wheel.py | 22 ++++++++++++++++++---- tests/unit/test_wheel.py | 9 +++++++-- 3 files changed, 43 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index bd8a2a26d8e..4637f030c17 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -15,6 +15,7 @@ from optparse import SUPPRESS_HELP from pip._vendor import pkg_resources +from pip._vendor.packaging.utils import canonicalize_name from pip._internal.cache import WheelCache from pip._internal.cli import cmdoptions @@ -46,7 +47,9 @@ from optparse import Values from typing import Any, List + from pip._internal.models.format_control import FormatControl from pip._internal.req.req_install import InstallRequirement + from pip._internal.wheel import BinaryAllowedPredicate logger = logging.getLogger(__name__) @@ -94,6 +97,17 @@ def build_wheels( return build_failures +def get_check_binary_allowed(format_control): + # type: (FormatControl) -> BinaryAllowedPredicate + def check_binary_allowed(req): + # type: (InstallRequirement) -> bool + canonical_name = canonicalize_name(req.name) + allowed_formats = format_control.get_allowed_formats(canonical_name) + return "binary" in allowed_formats + + return check_binary_allowed + + class InstallCommand(RequirementCommand): """ Install packages from: @@ -366,6 +380,9 @@ def run(self, options, args): modifying_pip=requirement_set.has_requirement("pip") ) + check_binary_allowed = get_check_binary_allowed( + finder.format_control + ) # Consider legacy and PEP517-using requirements separately legacy_requirements = [] pep517_requirements = [] @@ -378,6 +395,7 @@ def run(self, options, args): wheel_builder = WheelBuilder( finder, preparer, wheel_cache, build_options=[], global_options=[], + check_binary_allowed=check_binary_allowed, ) build_failures = build_wheels( diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index fabfc2dae44..27fb4bd73bd 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -53,7 +53,8 @@ if MYPY_CHECK_RUNNING: from typing import ( - Dict, List, Optional, Sequence, Mapping, Tuple, IO, Text, Any, Iterable + Dict, List, Optional, Sequence, Mapping, Tuple, IO, Text, Any, + Iterable, Callable, ) from pip._vendor.packaging.requirements import Requirement from pip._internal.req.req_install import InstallRequirement @@ -66,6 +67,8 @@ InstalledCSVRow = Tuple[str, ...] + BinaryAllowedPredicate = Callable[[InstallRequirement], bool] + VERSION_COMPATIBLE = (1, 0) @@ -777,7 +780,8 @@ def should_use_ephemeral_cache( req, # type: InstallRequirement format_control, # type: FormatControl should_unpack, # type: bool - cache_available # type: bool + cache_available, # type: bool + check_binary_allowed, # type: BinaryAllowedPredicate ): # type: (...) -> Optional[bool] """ @@ -809,8 +813,7 @@ def should_use_ephemeral_cache( if req.editable or not req.source_dir: return None - if "binary" not in format_control.get_allowed_formats( - canonicalize_name(req.name)): + if not check_binary_allowed(req): logger.info( "Skipping wheel build for %s, due to binaries " "being disabled for it.", req.name, @@ -887,6 +890,10 @@ def get_legacy_build_wheel_path( return os.path.join(temp_dir, names[0]) +def _always_true(_): + return True + + class WheelBuilder(object): """Build wheels from a RequirementSet.""" @@ -897,10 +904,15 @@ def __init__( wheel_cache, # type: WheelCache build_options=None, # type: Optional[List[str]] global_options=None, # type: Optional[List[str]] + check_binary_allowed=None, # type: Optional[BinaryAllowedPredicate] no_clean=False # type: bool ): # type: (...) -> None self.finder = finder + if check_binary_allowed is None: + # Binaries allowed by default. + check_binary_allowed = _always_true + self.preparer = preparer self.wheel_cache = wheel_cache @@ -908,6 +920,7 @@ def __init__( self.build_options = build_options or [] self.global_options = global_options or [] + self.check_binary_allowed = check_binary_allowed self.no_clean = no_clean def _build_one(self, req, output_dir, python_tag=None): @@ -1067,6 +1080,7 @@ def build( format_control=format_control, should_unpack=should_unpack, cache_available=cache_available, + check_binary_allowed=self.check_binary_allowed, ) if ephem_cache is None: continue diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 449da28c199..982529bd720 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -101,9 +101,12 @@ def test_should_use_ephemeral_cache__issue_6197( assert req.link.is_artifact format_control = FormatControl() + + always_true = Mock(return_value=True) + ephem_cache = wheel.should_use_ephemeral_cache( req, format_control=format_control, should_unpack=should_unpack, - cache_available=cache_available, + cache_available=cache_available, check_binary_allowed=always_true, ) assert ephem_cache is expected @@ -142,10 +145,12 @@ def test_should_use_ephemeral_cache__disallow_binaries_and_vcs_checkout( if disallow_binaries: format_control.disallow_binaries() + check_binary_allowed = Mock(return_value=not disallow_binaries) + # The cache_available value doesn't matter for this test. ephem_cache = wheel.should_use_ephemeral_cache( req, format_control=format_control, should_unpack=True, - cache_available=True, + cache_available=True, check_binary_allowed=check_binary_allowed, ) assert ephem_cache is expected From b63ea9cd08a5ced1f061aef36cac470ecddce815 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Wed, 4 Sep 2019 21:10:34 -0400 Subject: [PATCH 0197/3170] Remove now-unused finder and format_control from WheelBuilder. --- src/pip/_internal/commands/install.py | 2 +- src/pip/_internal/commands/wheel.py | 2 +- src/pip/_internal/wheel.py | 6 ------ tests/unit/test_wheel.py | 12 ++---------- 4 files changed, 4 insertions(+), 18 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 4637f030c17..b1acee94a56 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -393,7 +393,7 @@ def run(self, options, args): legacy_requirements.append(req) wheel_builder = WheelBuilder( - finder, preparer, wheel_cache, + preparer, wheel_cache, build_options=[], global_options=[], check_binary_allowed=check_binary_allowed, ) diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index 7b0c799b21f..635dedca67f 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -154,7 +154,7 @@ def run(self, options, args): # build wheels wb = WheelBuilder( - finder, preparer, wheel_cache, + preparer, wheel_cache, build_options=options.build_options or [], global_options=options.global_options or [], no_clean=options.no_clean, diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index 27fb4bd73bd..e90799a1dda 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -58,7 +58,6 @@ ) from pip._vendor.packaging.requirements import Requirement from pip._internal.req.req_install import InstallRequirement - from pip._internal.index import FormatControl, PackageFinder from pip._internal.operations.prepare import ( RequirementPreparer ) @@ -778,7 +777,6 @@ def _contains_egg_info( def should_use_ephemeral_cache( req, # type: InstallRequirement - format_control, # type: FormatControl should_unpack, # type: bool cache_available, # type: bool check_binary_allowed, # type: BinaryAllowedPredicate @@ -899,7 +897,6 @@ class WheelBuilder(object): def __init__( self, - finder, # type: PackageFinder preparer, # type: RequirementPreparer wheel_cache, # type: WheelCache build_options=None, # type: Optional[List[str]] @@ -908,7 +905,6 @@ def __init__( no_clean=False # type: bool ): # type: (...) -> None - self.finder = finder if check_binary_allowed is None: # Binaries allowed by default. check_binary_allowed = _always_true @@ -1071,13 +1067,11 @@ def build( ) buildset = [] - format_control = self.finder.format_control cache_available = bool(self.wheel_cache.cache_dir) for req in requirements: ephem_cache = should_use_ephemeral_cache( req, - format_control=format_control, should_unpack=should_unpack, cache_available=cache_available, check_binary_allowed=self.check_binary_allowed, diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 982529bd720..6d21980f05c 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -10,7 +10,6 @@ from pip._internal import pep425tags, wheel from pip._internal.exceptions import InvalidWheelFilename, UnsupportedWheel -from pip._internal.index import FormatControl from pip._internal.models.link import Link from pip._internal.req.req_install import InstallRequirement from pip._internal.utils.compat import WINDOWS @@ -100,12 +99,10 @@ def test_should_use_ephemeral_cache__issue_6197( assert not req.is_wheel assert req.link.is_artifact - format_control = FormatControl() - always_true = Mock(return_value=True) ephem_cache = wheel.should_use_ephemeral_cache( - req, format_control=format_control, should_unpack=should_unpack, + req, should_unpack=should_unpack, cache_available=cache_available, check_binary_allowed=always_true, ) assert ephem_cache is expected @@ -141,15 +138,11 @@ def test_should_use_ephemeral_cache__disallow_binaries_and_vcs_checkout( assert not req.is_wheel assert req.link.is_vcs - format_control = FormatControl() - if disallow_binaries: - format_control.disallow_binaries() - check_binary_allowed = Mock(return_value=not disallow_binaries) # The cache_available value doesn't matter for this test. ephem_cache = wheel.should_use_ephemeral_cache( - req, format_control=format_control, should_unpack=True, + req, should_unpack=True, cache_available=True, check_binary_allowed=check_binary_allowed, ) assert ephem_cache is expected @@ -701,7 +694,6 @@ def test_skip_building_wheels(self, caplog): as mock_build_one: wheel_req = Mock(is_wheel=True, editable=False, constraint=False) wb = wheel.WheelBuilder( - finder=Mock(), preparer=Mock(), wheel_cache=Mock(cache_dir=None), ) From 7ce0eafe53dfdd930377f80fb3d6fdc763cea251 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Wed, 4 Sep 2019 21:26:45 -0400 Subject: [PATCH 0198/3170] Combine separate editable conditions into one. --- src/pip/_internal/req/req_install.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index df6607c6e74..066932eeed6 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -657,10 +657,6 @@ def looks_like_virtual_env(path): if self._egg_info_path is None: if self.editable: base = self.source_dir - else: - base = os.path.join(self.setup_py_dir, 'pip-egg-info') - filenames = os.listdir(base) - if self.editable: filenames = [] for root, dirs, files in os.walk(base): for dir in vcs.dirnames: @@ -678,6 +674,9 @@ def looks_like_virtual_env(path): filenames.extend([os.path.join(root, dir) for dir in dirs]) filenames = [f for f in filenames if f.endswith('.egg-info')] + else: + base = os.path.join(self.setup_py_dir, 'pip-egg-info') + filenames = os.listdir(base) if not filenames: raise InstallationError( From 7b772e42d8c464776ba37a8cef8567e56128e6d3 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Wed, 4 Sep 2019 22:08:44 -0400 Subject: [PATCH 0199/3170] Simplify assertion. Checking `self._wheel_dir` is redundant with the existing assertion at the top of the function. --- src/pip/_internal/wheel.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index bc0cdd260b1..a9f96654f26 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -1078,10 +1078,8 @@ def build( # Is any wheel build not using the ephemeral cache? if any(not ephem_cache for _, ephem_cache in buildset): - have_directory_for_build = self._wheel_dir or ( - should_unpack and self.wheel_cache.cache_dir - ) - assert have_directory_for_build + if should_unpack: + assert self.wheel_cache.cache_dir # TODO by @pradyunsg # Should break up this method into 2 separate methods. From 3473a6736d4648b36da858bb2f3c9495244288de Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Wed, 4 Sep 2019 22:11:10 -0400 Subject: [PATCH 0200/3170] Simplify assertion 2. Per `should_use_ephemeral_cache`, we only use a non-ephemeral cache if `cache_available`, so there's no need to check it again here. --- src/pip/_internal/wheel.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index a9f96654f26..75042351e23 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -1076,11 +1076,6 @@ def build( if not buildset: return [] - # Is any wheel build not using the ephemeral cache? - if any(not ephem_cache for _, ephem_cache in buildset): - if should_unpack: - assert self.wheel_cache.cache_dir - # TODO by @pradyunsg # Should break up this method into 2 separate methods. From 907bc82d8b1d0ae30d3a780ebf0500d132583328 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Wed, 4 Sep 2019 22:17:19 -0400 Subject: [PATCH 0201/3170] Un-optimize conditional directory creation. --- src/pip/_internal/wheel.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index 75042351e23..dbf19d39310 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -1095,15 +1095,17 @@ def build( 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: - logger.warning("Building wheel for %s failed: %s", - req.name, e) - build_failure.append(req) - continue else: output_dir = self._wheel_dir + + try: + ensure_dir(output_dir) + except OSError as e: + logger.warning("Building wheel for %s failed: %s", + req.name, e) + build_failure.append(req) + continue + wheel_file = self._build_one( req, output_dir, python_tag=python_tag, From ebac7c012afdd71e5e1d7910e1ab23f583b14eaa Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Wed, 4 Sep 2019 22:21:40 -0400 Subject: [PATCH 0202/3170] Pull constant python_tag out of loop. --- src/pip/_internal/wheel.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index dbf19d39310..6b74c5899d8 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -1084,13 +1084,16 @@ def build( 'Building wheels for collected packages: %s', ', '.join([req.name for (req, _) in buildset]), ) + + python_tag = None + if should_unpack: + python_tag = pep425tags.implementation_tag + _cache = self.wheel_cache # shorter name with indent_log(): build_success, build_failure = [], [] for req, ephem in buildset: - python_tag = None if should_unpack: - python_tag = pep425tags.implementation_tag if ephem: output_dir = _cache.get_ephem_path_for_link(req.link) else: From 41f0ea076e33396f08afdb2eecfea599a29bc204 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Wed, 4 Sep 2019 22:27:37 -0400 Subject: [PATCH 0203/3170] Remove unneeded shorter alias. --- src/pip/_internal/wheel.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index 6b74c5899d8..328347263d0 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -1089,15 +1089,14 @@ def build( if should_unpack: python_tag = pep425tags.implementation_tag - _cache = self.wheel_cache # shorter name with indent_log(): build_success, build_failure = [], [] for req, ephem in buildset: if should_unpack: if ephem: - output_dir = _cache.get_ephem_path_for_link(req.link) + output_dir = self.wheel_cache.get_ephem_path_for_link(req.link) else: - output_dir = _cache.get_path_for_link(req.link) + output_dir = self.wheel_cache.get_path_for_link(req.link) else: output_dir = self._wheel_dir From aa90d4423925ed3d021da9e95d437304588d11d5 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Wed, 4 Sep 2019 22:29:16 -0400 Subject: [PATCH 0204/3170] Store output directory in build set instead of calculating in loop. --- src/pip/_internal/wheel.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index 328347263d0..ae8b9f0fb1c 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -1071,7 +1071,15 @@ def build( if ephem_cache is None: continue - buildset.append((req, ephem_cache)) + if should_unpack: + if ephem_cache: + output_dir = self.wheel_cache.get_ephem_path_for_link(req.link) + else: + output_dir = self.wheel_cache.get_path_for_link(req.link) + else: + output_dir = self._wheel_dir + + buildset.append((req, output_dir)) if not buildset: return [] @@ -1091,15 +1099,7 @@ def build( with indent_log(): build_success, build_failure = [], [] - for req, ephem in buildset: - if should_unpack: - if ephem: - output_dir = self.wheel_cache.get_ephem_path_for_link(req.link) - else: - output_dir = self.wheel_cache.get_path_for_link(req.link) - else: - output_dir = self._wheel_dir - + for req, output_dir in buildset: try: ensure_dir(output_dir) except OSError as e: From 4cbbec982b302c81d79f32cf665c9fbf492cea5d Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Wed, 4 Sep 2019 22:33:09 -0400 Subject: [PATCH 0205/3170] Shorten long line. --- src/pip/_internal/wheel.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index ae8b9f0fb1c..a44affd1556 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -1073,7 +1073,9 @@ def build( if should_unpack: if ephem_cache: - output_dir = self.wheel_cache.get_ephem_path_for_link(req.link) + output_dir = self.wheel_cache.get_ephem_path_for_link( + req.link + ) else: output_dir = self.wheel_cache.get_path_for_link(req.link) else: From 33b404dfdcd0b23a3c05df745d0457320c5cb9a8 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Thu, 5 Sep 2019 09:15:51 +0530 Subject: [PATCH 0206/3170] Reword a NEWS entry --- news/6947.bugfix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/6947.bugfix b/news/6947.bugfix index ad121de7fdd..f8d409e9eee 100644 --- a/news/6947.bugfix +++ b/news/6947.bugfix @@ -1 +1 @@ -Fix not displaying all dependent packages under "Required-by:" in pip show. \ No newline at end of file +Use canonical distribution names when computing ``Required-By`` in ``pip show``. From e31192cf4989c1cef481eb92d6a91ae99dd8e5f5 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Thu, 5 Sep 2019 09:34:16 +0530 Subject: [PATCH 0207/3170] Simplify conditional for choosing WheelDistribution --- src/pip/_internal/distributions/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/distributions/__init__.py b/src/pip/_internal/distributions/__init__.py index fdf332a8138..d583d7d3f8a 100644 --- a/src/pip/_internal/distributions/__init__.py +++ b/src/pip/_internal/distributions/__init__.py @@ -16,7 +16,7 @@ def make_distribution_for_install_requirement(install_req): if install_req.editable: return SourceDistribution(install_req) - if install_req.link and install_req.is_wheel: + if install_req.is_wheel: return WheelDistribution(install_req) # Otherwise, a SourceDistribution From fc338512de9a8407857e6f43e3706a1953df72ad Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Thu, 5 Sep 2019 09:35:15 +0530 Subject: [PATCH 0208/3170] Better comments in make_distribution_for_install_requirement() --- src/pip/_internal/distributions/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/distributions/__init__.py b/src/pip/_internal/distributions/__init__.py index d583d7d3f8a..decd377c011 100644 --- a/src/pip/_internal/distributions/__init__.py +++ b/src/pip/_internal/distributions/__init__.py @@ -12,10 +12,12 @@ def make_distribution_for_install_requirement(install_req): # type: (InstallRequirement) -> AbstractDistribution """Returns a Distribution for the given InstallRequirement """ - # If it's not an editable, is a wheel, it's a WheelDistribution + # Editable requirements will always be source distributions. They use the + # legacy logic until we create a modern standard for them. if install_req.editable: return SourceDistribution(install_req) + # If it's a wheel, it's a WheelDistribution if install_req.is_wheel: return WheelDistribution(install_req) From fe147309eade9092d93ff40b52da69c84d4eed29 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Tue, 30 Jul 2019 16:56:20 +0530 Subject: [PATCH 0209/3170] Appropriately ignore the .nox folder --- .gitignore | 2 +- setup.cfg | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index 709c24b0369..7af862f16a3 100644 --- a/.gitignore +++ b/.gitignore @@ -18,7 +18,7 @@ docs/build/ .mypy_cache/ # Unit test / coverage reports -.tox/ +.[nt]ox/ htmlcov/ .coverage .coverage.* diff --git a/setup.cfg b/setup.cfg index 4c6f7c5f142..81396f9ba3e 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,6 +1,7 @@ [isort] skip = ./build, + .nox, .tox, .scratch, _vendor, @@ -17,6 +18,7 @@ include_trailing_comma = true [flake8] exclude = ./build, + .nox, .tox, .scratch, _vendor, From ae4a22958eec7a578a93d9238386a7f33d8f37bd Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Tue, 30 Jul 2019 16:56:44 +0530 Subject: [PATCH 0210/3170] Move invoke.generate commands to nox --- noxfile.py | 52 ++++++++++++++++++++++++++++++++++++ tasks/__init__.py | 4 +-- tools/automation/generate.py | 47 -------------------------------- 3 files changed, 54 insertions(+), 49 deletions(-) create mode 100644 noxfile.py delete mode 100644 tools/automation/generate.py diff --git a/noxfile.py b/noxfile.py new file mode 100644 index 00000000000..9ae6b121957 --- /dev/null +++ b/noxfile.py @@ -0,0 +1,52 @@ +"""Release time helpers, executed using nox. +""" + +import nox + + +def get_author_list(): + """Get the list of authors from Git commits. + """ + # subprocess because session.run doesn't give us stdout + result = subprocess.run( + ["git", "log", "--use-mailmap", "--format=%aN <%aE>"], + capture_output=True, + encoding="utf-8", + ) + + # Create a unique list. + authors = [] + seen_authors = set() + for author in result.stdout.splitlines(): + author = author.strip() + if author.lower() not in seen_authors: + seen_authors.add(author.lower()) + authors.append(author) + + # Sort our list of Authors by their case insensitive name + return sorted(authors, key=lambda x: x.lower()) + + +# ----------------------------------------------------------------------------- +# Commands used during the release process +# ----------------------------------------------------------------------------- +@nox.session +def generate_authors(session): + # Get our list of authors + session.log("Collecting author names") + authors = get_author_list() + + # Write our authors to the AUTHORS file + session.log("Writing AUTHORS") + with io.open("AUTHORS.txt", "w", encoding="utf-8") as fp: + fp.write(u"\n".join(authors)) + fp.write(u"\n") + + +@nox.session +def generate_news(session): + session.log("Generating NEWS") + session.install("towncrier") + + # You can pass 2 possible arguments: --draft, --yes + session.run("towncrier", *session.posargs) diff --git a/tasks/__init__.py b/tasks/__init__.py index ecde439968e..6427f603726 100644 --- a/tasks/__init__.py +++ b/tasks/__init__.py @@ -1,5 +1,5 @@ import invoke -from tools.automation import generate, vendoring +from tools.automation import vendoring -ns = invoke.Collection(generate, vendoring) +ns = invoke.Collection(vendoring) diff --git a/tools/automation/generate.py b/tools/automation/generate.py deleted file mode 100644 index 2357897072d..00000000000 --- a/tools/automation/generate.py +++ /dev/null @@ -1,47 +0,0 @@ -import io - -import invoke - - -@invoke.task -def authors(ctx): - print("[generate.authors] Generating AUTHORS") - - # Get our list of authors - print("[generate.authors] Collecting author names") - - # Note that it's necessary to use double quotes in the - # --format"=%aN <%aE>" part of the command, as the Windows - # shell doesn't recognise single quotes here. - r = ctx.run('git log --use-mailmap --format"=%aN <%aE>"', - encoding="utf-8", hide=True) - - authors = [] - seen_authors = set() - for author in r.stdout.splitlines(): - author = author.strip() - if author.lower() not in seen_authors: - seen_authors.add(author.lower()) - authors.append(author) - - # Sort our list of Authors by their case insensitive name - authors = sorted(authors, key=lambda x: x.lower()) - - # Write our authors to the AUTHORS file - print("[generate.authors] Writing AUTHORS") - with io.open("AUTHORS.txt", "w", encoding="utf8") as fp: - fp.write(u"\n".join(authors)) - fp.write(u"\n") - - -@invoke.task -def news(ctx, draft=False, yes=False): - print("[generate.news] Generating NEWS") - - args = [] - if draft: - args.append("--draft") - if yes: - args.append("--yes") - - ctx.run("towncrier {}".format(" ".join(args))) From 79ce923e1a7cc87dc19b35dcd504c902b562fa2e Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Tue, 27 Aug 2019 16:05:35 +0530 Subject: [PATCH 0211/3170] Update documented invocations of generate commands --- .azure-pipelines/jobs/package.yml | 6 +++--- docs/html/development/release-process.rst | 7 +++---- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/.azure-pipelines/jobs/package.yml b/.azure-pipelines/jobs/package.yml index 7a3837df211..510fa3e4d68 100644 --- a/.azure-pipelines/jobs/package.yml +++ b/.azure-pipelines/jobs/package.yml @@ -15,13 +15,13 @@ jobs: inputs: versionSpec: '3' - - bash: pip install setuptools tox wheel invoke towncrier requests + - bash: pip install tox nox setuptools wheel displayName: Install dependencies - - bash: invoke generate.authors + - bash: nox -s generate_authors displayName: Generate AUTHORS.txt - - bash: invoke generate.news --yes + - bash: nox -s generate_news --yes displayName: Generate NEWS.rst - bash: tox -e packaging diff --git a/docs/html/development/release-process.rst b/docs/html/development/release-process.rst index a3de684595e..a01ce71dfd1 100644 --- a/docs/html/development/release-process.rst +++ b/docs/html/development/release-process.rst @@ -80,14 +80,13 @@ Creating a new release ---------------------- #. Checkout the current pip ``master`` branch. -#. Ensure you have the latest ``wheel``, ``setuptools``, ``twine``, ``invoke`` - and ``towncrier`` packages installed. -#. Generate a new ``AUTHORS.txt`` (``invoke generate.authors``) and commit the +#. Ensure you have the latest ``wheel``, ``setuptools``, ``twine`` and ``nox`` packages installed. +#. Generate a new ``AUTHORS.txt`` (``nox -s generate_authors``) and commit the results. #. Bump the version in ``pip/__init__.py`` to the release version and commit the results. Usually this involves dropping just the ``.devN`` suffix on the version. -#. Generate a new ``NEWS.rst`` (``invoke generate.news``) and commit the +#. Generate a new ``NEWS.rst`` (``nox -s generate_news``) and commit the results. #. Create a tag at the current commit, of the form ``YY.N`` (``git tag YY.N``). From b70d30e8cb14a3b46389adf3e933a27a2a66b6a4 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Thu, 5 Sep 2019 12:42:53 +0530 Subject: [PATCH 0212/3170] Exclude noxfile.py from the sdist --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index 747f84ad4ce..5bf20b0d98d 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -17,6 +17,7 @@ exclude .appveyor.yml exclude .travis.yml exclude .readthedocs.yml exclude tox.ini +exclude noxfile.py recursive-include src/pip/_vendor *.pem recursive-include docs Makefile *.rst *.py *.bat From b33280d92521191c706c0aa709b5e327d4630c3b Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Thu, 5 Sep 2019 12:45:28 +0530 Subject: [PATCH 0213/3170] Add missing imports --- noxfile.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/noxfile.py b/noxfile.py index 9ae6b121957..d6357b3dd7d 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1,6 +1,9 @@ """Release time helpers, executed using nox. """ +import io +import subprocess + import nox From 0c5721fe847a2f6232fb52529299efc18698f37a Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Mon, 2 Sep 2019 15:47:32 -0400 Subject: [PATCH 0214/3170] Simplify directory delete marker file check. --- src/pip/_internal/req/req_install.py | 8 +++++--- src/pip/_internal/utils/marker_files.py | 4 ++++ src/pip/_internal/wheel.py | 8 +++++--- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 0d686ca9476..1359badf749 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -28,7 +28,10 @@ from pip._internal.utils.compat import native_str from pip._internal.utils.hashes import Hashes from pip._internal.utils.logging import indent_log -from pip._internal.utils.marker_files import PIP_DELETE_MARKER_FILENAME +from pip._internal.utils.marker_files import ( + PIP_DELETE_MARKER_FILENAME, + has_delete_marker_file, +) from pip._internal.utils.misc import ( _make_build_dir, ask_path_exists, @@ -399,8 +402,7 @@ def remove_temporary_source(self): # type: () -> None """Remove the source files from this requirement, if they are marked for deletion""" - if self.source_dir and os.path.exists( - os.path.join(self.source_dir, PIP_DELETE_MARKER_FILENAME)): + if self.source_dir and has_delete_marker_file(self.source_dir): logger.debug('Removing source in %s', self.source_dir) rmtree(self.source_dir) self.source_dir = None diff --git a/src/pip/_internal/utils/marker_files.py b/src/pip/_internal/utils/marker_files.py index cb0c8ebc4e3..c0396951f94 100644 --- a/src/pip/_internal/utils/marker_files.py +++ b/src/pip/_internal/utils/marker_files.py @@ -10,6 +10,10 @@ PIP_DELETE_MARKER_FILENAME = 'pip-delete-this-directory.txt' +def has_delete_marker_file(directory): + return os.path.exists(os.path.join(directory, PIP_DELETE_MARKER_FILENAME)) + + def write_delete_marker_file(directory): # type: (str) -> None """ diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index e90799a1dda..99b9cb6fbba 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -36,7 +36,7 @@ from pip._internal.locations import distutils_scheme, get_major_minor_version from pip._internal.models.link import Link from pip._internal.utils.logging import indent_log -from pip._internal.utils.marker_files import PIP_DELETE_MARKER_FILENAME +from pip._internal.utils.marker_files import has_delete_marker_file from pip._internal.utils.misc import ( LOG_DIVIDER, call_subprocess, @@ -1131,8 +1131,10 @@ def build( # method. # The code below assumes temporary source dirs - # prevent it doing bad things. - if req.source_dir and not os.path.exists(os.path.join( - req.source_dir, PIP_DELETE_MARKER_FILENAME)): + if ( + req.source_dir and + not has_delete_marker_file(req.source_dir) + ): raise AssertionError( "bad source dir - missing marker") # Delete the source we built the wheel from From b5c1c7620335ccfbc855c50d92cc3e788a289a3a Mon Sep 17 00:00:00 2001 From: Albert Tugushev Date: Thu, 5 Sep 2019 17:49:34 +0300 Subject: [PATCH 0215/3170] Fix failing test and revisit is_readonly flag --- src/pip/_internal/utils/misc.py | 2 +- tests/unit/test_utils.py | 18 +++++++----------- 2 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index af7c9028442..78de2d83a17 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -182,7 +182,7 @@ def rmtree_errorhandler(func, path, exc_info): remove them, an exception is thrown. We catch that here, remove the read-only attribute, and hopefully continue without problems.""" try: - is_readonly = os.stat(path).st_mode & stat.S_IREAD + is_readonly = not (os.stat(path).st_mode & stat.S_IWRITE) except (IOError, OSError): # The path already removed, nothing to do return diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index ee6851ccdaa..4cf5c285421 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -421,19 +421,15 @@ def test_rmtree_errorhandler_reraises_error(tmpdir): """ # Create directory without read permission path = str((tmpdir / 'subdir').mkdir()) - os.chmod(path, ~stat.S_IREAD) + os.chmod(path, stat.S_IWRITE) mock_func = Mock() - try: - # Make sure the handler reraises an exception. - # Note that the raise statement without expression and - # active exception in the current scope throws - # the RuntimeError on Python3 and the TypeError on Python2. - with pytest.raises((RuntimeError, TypeError)): - rmtree_errorhandler(mock_func, path, None) - finally: - # Restore the read permission to let the pytest to clean up temp dirs - os.chmod(path, stat.S_IREAD) + # Make sure the handler reraises an exception. + # Note that the raise statement without expression and + # active exception in the current scope throws + # the RuntimeError on Python3 and the TypeError on Python2. + with pytest.raises((RuntimeError, TypeError)): + rmtree_errorhandler(mock_func, path, None) mock_func.assert_not_called() From db3320e4629ae4bf0bea4539a0a3a8b073f5ecec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= Date: Sat, 24 Aug 2019 23:04:01 +0200 Subject: [PATCH 0216/3170] Add symlink support to StashedUninstallPathSet --- src/pip/_internal/req/req_uninstall.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/req/req_uninstall.py b/src/pip/_internal/req/req_uninstall.py index e35e47615cd..6b551c91bec 100644 --- a/src/pip/_internal/req/req_uninstall.py +++ b/src/pip/_internal/req/req_uninstall.py @@ -267,14 +267,16 @@ def _get_file_stash(self, path): def stash(self, path): # type: (str) -> str """Stashes the directory or file and returns its new location. + Handle symlinks as files to avoid modifying the symlink targets. """ - if os.path.isdir(path): + path_is_dir = os.path.isdir(path) and not os.path.islink(path) + if path_is_dir: new_path = self._get_directory_stash(path) else: new_path = self._get_file_stash(path) self._moves.append((path, new_path)) - if os.path.isdir(path) and os.path.isdir(new_path): + if (path_is_dir and os.path.isdir(new_path)): # If we're moving a directory, we need to # remove the destination first or else it will be # moved to inside the existing directory. @@ -301,7 +303,7 @@ def rollback(self): for new_path, path in self._moves: try: logger.debug('Replacing %s from %s', new_path, path) - if os.path.isfile(new_path): + if os.path.isfile(new_path) or os.path.islink(new_path): os.unlink(new_path) elif os.path.isdir(new_path): rmtree(new_path) From 04bc6d090dc6d4a11b4af1c470a63f8986a0c8c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= Date: Fri, 6 Sep 2019 08:24:10 +0200 Subject: [PATCH 0217/3170] Remove non standard function from Path class --- tests/functional/test_uninstall.py | 3 ++- tests/lib/path.py | 4 ---- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/tests/functional/test_uninstall.py b/tests/functional/test_uninstall.py index 2e7330feee2..7d1984246ae 100644 --- a/tests/functional/test_uninstall.py +++ b/tests/functional/test_uninstall.py @@ -473,7 +473,8 @@ def test_uninstall_with_symlink(script, data, tmpdir): st_mode = symlink_target.stat().st_mode distinfo_path = script.site_packages_path / 'simple.dist-0.1.dist-info' record_path = distinfo_path / 'RECORD' - record_path.append_text("symlink,,\n") + with open(record_path, "a") as f: + f.write("symlink,,\n") uninstall_result = script.pip('uninstall', 'simple.dist', '-y') assert symlink_source in uninstall_result.files_deleted assert symlink_target.stat().st_mode == st_mode diff --git a/tests/lib/path.py b/tests/lib/path.py index d541468686f..b2676a2e1e0 100644 --- a/tests/lib/path.py +++ b/tests/lib/path.py @@ -203,10 +203,6 @@ def write_text(self, content): with open(self, "w") as fp: fp.write(content) - def append_text(self, content): - with open(self, "a") as fp: - fp.write(content) - def touch(self): with open(self, "a") as fp: path = fp.fileno() if os.utime in supports_fd else self From cdcacff8a1b3004de96bcf7b5cb75b17796bd273 Mon Sep 17 00:00:00 2001 From: Albert Tugushev Date: Thu, 5 Sep 2019 20:50:12 +0300 Subject: [PATCH 0218/3170] Address review comments Fix some comments and variable names. Execute rmtree_errorhandler in an except block to test whether it reraises an exception. --- src/pip/_internal/utils/misc.py | 7 +++---- tests/unit/test_utils.py | 29 +++++++++++++++-------------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 78de2d83a17..5d11640549e 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -182,13 +182,12 @@ def rmtree_errorhandler(func, path, exc_info): remove them, an exception is thrown. We catch that here, remove the read-only attribute, and hopefully continue without problems.""" try: - is_readonly = not (os.stat(path).st_mode & stat.S_IWRITE) + has_attr_readonly = not (os.stat(path).st_mode & stat.S_IWRITE) except (IOError, OSError): - # The path already removed, nothing to do + # it's equivalent to os.path.exists return - # if file type currently read only - if is_readonly: + if has_attr_readonly: # convert to read/write os.chmod(path, stat.S_IWRITE) # use the original function to repeat the operation diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 4cf5c285421..9f83fe4b144 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -387,13 +387,13 @@ def test_unpack_zip(self, data): self.confirm_files() -def test_rmtree_errorhandler_not_existing_directory(tmpdir): +def test_rmtree_errorhandler_nonexistent_directory(tmpdir): """ Test rmtree_errorhandler ignores the given non-existing directory. """ - not_existing_path = str(tmpdir / 'foo') + nonexistent_path = str(tmpdir / 'foo') mock_func = Mock() - rmtree_errorhandler(mock_func, not_existing_path, None) + rmtree_errorhandler(mock_func, nonexistent_path, None) mock_func.assert_not_called() @@ -410,36 +410,37 @@ def test_rmtree_errorhandler_readonly_directory(tmpdir): rmtree_errorhandler(mock_func, path, None) mock_func.assert_called_with(path) - # Make sure the path is become writable + # Make sure the path is now writable assert os.stat(path).st_mode & stat.S_IWRITE def test_rmtree_errorhandler_reraises_error(tmpdir): """ Test rmtree_errorhandler reraises an exception - by the given non-readonly directory. + by the given unreadable directory. """ # Create directory without read permission path = str((tmpdir / 'subdir').mkdir()) os.chmod(path, stat.S_IWRITE) mock_func = Mock() - # Make sure the handler reraises an exception. - # Note that the raise statement without expression and - # active exception in the current scope throws - # the RuntimeError on Python3 and the TypeError on Python2. - with pytest.raises((RuntimeError, TypeError)): - rmtree_errorhandler(mock_func, path, None) + + try: + raise RuntimeError('test message') + except RuntimeError: + # Make sure the handler reraises an exception + with pytest.raises(RuntimeError, match='test message'): + rmtree_errorhandler(mock_func, path, None) mock_func.assert_not_called() -def test_rmtree_skips_not_existing_directory(): +def test_rmtree_skips_nonexistent_directory(): """ Test wrapped rmtree doesn't raise an error - by the given non-existing directory. + by the given nonexistent directory. """ - rmtree.__wrapped__('non-existing-subdir') + rmtree.__wrapped__('nonexistent-subdir') class Failer: From a8427d5553ab7ce1e4def2934bbe6c58eea68776 Mon Sep 17 00:00:00 2001 From: Albert Tugushev Date: Mon, 26 Aug 2019 12:29:39 +0300 Subject: [PATCH 0219/3170] Run parametrized command in test_pep518_forkbombs Fixes copy-paste issue in `test_pep518_forkbombs`. --- news/fix-test-pep518-forkbombs.trivial | 1 + tests/functional/test_install.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 news/fix-test-pep518-forkbombs.trivial diff --git a/news/fix-test-pep518-forkbombs.trivial b/news/fix-test-pep518-forkbombs.trivial new file mode 100644 index 00000000000..13792675b3b --- /dev/null +++ b/news/fix-test-pep518-forkbombs.trivial @@ -0,0 +1 @@ +Fix copy-paste issue in `test_pep518_forkbombs`. diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 27d8b8ef68b..2fa45859647 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -162,7 +162,7 @@ def test_pep518_with_namespace_package(script, data, common_wheels): def test_pep518_forkbombs(script, data, common_wheels, command, package): package_source = next(data.packages.glob(package + '-[0-9]*.tar.gz')) result = script.pip( - 'wheel', '--no-index', '-v', + command, '--no-index', '-v', '-f', common_wheels, '-f', data.find_links, package, From a5840a9b8a7368be5eade83f3fb65fe6f590a7e3 Mon Sep 17 00:00:00 2001 From: Albert Tugushev Date: Fri, 6 Sep 2019 11:00:17 +0300 Subject: [PATCH 0220/3170] Address review comments Update news entry. --- news/6924.bugfix | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/news/6924.bugfix b/news/6924.bugfix index 4d8b4aa0e0d..d89652cba7b 100644 --- a/news/6924.bugfix +++ b/news/6924.bugfix @@ -1,2 +1 @@ -Fix an error when upgrade pip using ``pip.exe`` on Windows fails -if the requirement already satisfied. +Don't fail installation using pip.exe on Windows when pip wouldn't be upgraded. From d942a4ee65df67276d63af1f08098b072cc518ab Mon Sep 17 00:00:00 2001 From: Xavier Fernandez Date: Fri, 6 Sep 2019 10:29:34 +0200 Subject: [PATCH 0221/3170] tests: fix typos --- tests/functional/test_install_cleanup.py | 2 +- tests/unit/test_locations.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/functional/test_install_cleanup.py b/tests/functional/test_install_cleanup.py index bc51074eacc..8489b860b3a 100644 --- a/tests/functional/test_install_cleanup.py +++ b/tests/functional/test_install_cleanup.py @@ -72,7 +72,7 @@ def test_cleanup_after_install_from_local_directory(script, data): script.assert_no_temp() -def test_cleanup_req_satisifed_no_name(script, data): +def test_cleanup_req_satisfied_no_name(script, data): """ Test cleanup when req is already satisfied, and req has no 'name' """ diff --git a/tests/unit/test_locations.py b/tests/unit/test_locations.py index de83ef986be..d59d2d65a8d 100644 --- a/tests/unit/test_locations.py +++ b/tests/unit/test_locations.py @@ -76,7 +76,7 @@ def get_mock_getpwuid(self, uid): return result -class TestDisutilsScheme: +class TestDistutilsScheme: def test_root_modifies_appropriately(self, monkeypatch): # This deals with nt/posix path differences From 3bd099957ba632ef6f2388a3ecb5e07d7e04108d Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Fri, 6 Sep 2019 18:24:03 -0400 Subject: [PATCH 0222/3170] Use member function to make install reqs. --- src/pip/_internal/legacy_resolve.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/legacy_resolve.py b/src/pip/_internal/legacy_resolve.py index d2fcd9cc208..92b09237373 100644 --- a/src/pip/_internal/legacy_resolve.py +++ b/src/pip/_internal/legacy_resolve.py @@ -156,6 +156,7 @@ def __init__( self.ignore_requires_python = ignore_requires_python self.use_user_site = use_user_site self.use_pep517 = use_pep517 + self._make_install_req = install_req_from_req_string self._discovered_dependencies = \ defaultdict(list) # type: DefaultDict[str, List] @@ -381,7 +382,7 @@ def _resolve_one( more_reqs = [] # type: List[InstallRequirement] def add_req(subreq, extras_requested): - sub_install_req = install_req_from_req_string( + sub_install_req = self._make_install_req( str(subreq), req_to_install, isolated=self.isolated, From 7bb4cbc6cabfb3277d81e62f2f273d1ae15e4311 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Fri, 6 Sep 2019 18:26:38 -0400 Subject: [PATCH 0223/3170] Move arguments from call site to constructor. --- src/pip/_internal/legacy_resolve.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/legacy_resolve.py b/src/pip/_internal/legacy_resolve.py index 92b09237373..985ff4b5ca5 100644 --- a/src/pip/_internal/legacy_resolve.py +++ b/src/pip/_internal/legacy_resolve.py @@ -16,6 +16,7 @@ import logging import sys from collections import defaultdict +from functools import partial from itertools import chain from pip._vendor.packaging import specifiers @@ -156,7 +157,12 @@ def __init__( self.ignore_requires_python = ignore_requires_python self.use_user_site = use_user_site self.use_pep517 = use_pep517 - self._make_install_req = install_req_from_req_string + self._make_install_req = partial( + install_req_from_req_string, + isolated=self.isolated, + wheel_cache=self.wheel_cache, + use_pep517=self.use_pep517, + ) self._discovered_dependencies = \ defaultdict(list) # type: DefaultDict[str, List] @@ -385,9 +391,6 @@ def add_req(subreq, extras_requested): sub_install_req = self._make_install_req( str(subreq), req_to_install, - isolated=self.isolated, - wheel_cache=self.wheel_cache, - use_pep517=self.use_pep517 ) parent_req_name = req_to_install.name to_scan_again, add_to_parent = requirement_set.add_requirement( From 0ee1f9762b2a480d00da0251eeff038899c6df45 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Fri, 6 Sep 2019 18:45:20 -0400 Subject: [PATCH 0224/3170] Move `make_install_req` out of `Resolver`. --- src/pip/_internal/cli/req_command.py | 9 +++++++++ src/pip/_internal/legacy_resolve.py | 14 +++++++------- tests/unit/test_req.py | 9 +++++++++ 3 files changed, 25 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 361182abc07..15968767cc7 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -6,6 +6,7 @@ """ import os +from functools import partial from pip._internal.cli.base_command import Command from pip._internal.cli.cmdoptions import make_search_scope @@ -18,6 +19,7 @@ from pip._internal.req.constructors import ( install_req_from_editable, install_req_from_line, + install_req_from_req_string, ) from pip._internal.req.req_file import parse_requirements from pip._internal.utils.misc import normalize_path @@ -167,10 +169,17 @@ def make_resolver( """ Create a Resolver instance for the given parameters. """ + make_install_req = partial( + install_req_from_req_string, + isolated=options.isolated_mode, + wheel_cache=wheel_cache, + use_pep517=use_pep517, + ) return Resolver( preparer=preparer, session=session, finder=finder, + make_install_req=make_install_req, wheel_cache=wheel_cache, use_user_site=use_user_site, ignore_dependencies=options.ignore_dependencies, diff --git a/src/pip/_internal/legacy_resolve.py b/src/pip/_internal/legacy_resolve.py index 985ff4b5ca5..84cde374a27 100644 --- a/src/pip/_internal/legacy_resolve.py +++ b/src/pip/_internal/legacy_resolve.py @@ -42,7 +42,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import DefaultDict, List, Optional, Set, Tuple + from typing import Callable, DefaultDict, List, Optional, Set, Tuple from pip._vendor import pkg_resources from pip._internal.cache import WheelCache @@ -53,6 +53,10 @@ from pip._internal.req.req_install import InstallRequirement from pip._internal.req.req_set import RequirementSet + InstallRequirementProvider = Callable[ + [str, InstallRequirement], InstallRequirement + ] + logger = logging.getLogger(__name__) @@ -116,6 +120,7 @@ def __init__( preparer, # type: RequirementPreparer session, # type: PipSession finder, # type: PackageFinder + make_install_req, # type: InstallRequirementProvider wheel_cache, # type: Optional[WheelCache] use_user_site, # type: bool ignore_dependencies, # type: bool @@ -157,12 +162,7 @@ def __init__( self.ignore_requires_python = ignore_requires_python self.use_user_site = use_user_site self.use_pep517 = use_pep517 - self._make_install_req = partial( - install_req_from_req_string, - isolated=self.isolated, - wheel_cache=self.wheel_cache, - use_pep517=self.use_pep517, - ) + self._make_install_req = make_install_req self._discovered_dependencies = \ defaultdict(list) # type: DefaultDict[str, List] diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index 89442886d28..3436b398a24 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -2,6 +2,7 @@ import shutil import sys import tempfile +from functools import partial import pytest from mock import patch @@ -23,6 +24,7 @@ from pip._internal.req.constructors import ( install_req_from_editable, install_req_from_line, + install_req_from_req_string, parse_editable, ) from pip._internal.req.req_file import process_line @@ -61,8 +63,15 @@ def _basic_resolver(self, finder): build_isolation=True, req_tracker=RequirementTracker(), ) + make_install_req = partial( + install_req_from_req_string, + isolated=False, + wheel_cache=None, + use_pep517=None, + ) return Resolver( preparer=preparer, wheel_cache=None, + make_install_req=make_install_req, session=PipSession(), finder=finder, use_user_site=False, upgrade_strategy="to-satisfy-only", ignore_dependencies=False, ignore_installed=False, From 7514a50e1ddda1b54068e8800682b87c26431942 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Fri, 6 Sep 2019 18:53:35 -0400 Subject: [PATCH 0225/3170] Remove unused arguments from Resolver constructor. --- src/pip/_internal/cli/req_command.py | 3 --- src/pip/_internal/legacy_resolve.py | 12 ------------ tests/unit/test_req.py | 3 +-- 3 files changed, 1 insertion(+), 17 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 15968767cc7..62ffe4b149b 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -180,15 +180,12 @@ def make_resolver( session=session, finder=finder, make_install_req=make_install_req, - wheel_cache=wheel_cache, use_user_site=use_user_site, ignore_dependencies=options.ignore_dependencies, ignore_installed=ignore_installed, ignore_requires_python=ignore_requires_python, force_reinstall=force_reinstall, - isolated=options.isolated_mode, upgrade_strategy=upgrade_strategy, - use_pep517=use_pep517, py_version_info=py_version_info ) diff --git a/src/pip/_internal/legacy_resolve.py b/src/pip/_internal/legacy_resolve.py index 84cde374a27..8dbed9ac187 100644 --- a/src/pip/_internal/legacy_resolve.py +++ b/src/pip/_internal/legacy_resolve.py @@ -16,7 +16,6 @@ import logging import sys from collections import defaultdict -from functools import partial from itertools import chain from pip._vendor.packaging import specifiers @@ -28,7 +27,6 @@ HashErrors, UnsupportedPythonVersion, ) -from pip._internal.req.constructors import install_req_from_req_string from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import ( dist_in_usersite, @@ -45,7 +43,6 @@ from typing import Callable, DefaultDict, List, Optional, Set, Tuple from pip._vendor import pkg_resources - from pip._internal.cache import WheelCache from pip._internal.distributions import AbstractDistribution from pip._internal.download import PipSession from pip._internal.index import PackageFinder @@ -121,15 +118,12 @@ def __init__( session, # type: PipSession finder, # type: PackageFinder make_install_req, # type: InstallRequirementProvider - wheel_cache, # type: Optional[WheelCache] use_user_site, # type: bool ignore_dependencies, # type: bool ignore_installed, # type: bool ignore_requires_python, # type: bool force_reinstall, # type: bool - isolated, # type: bool upgrade_strategy, # type: str - use_pep517=None, # type: Optional[bool] py_version_info=None, # type: Optional[Tuple[int, ...]] ): # type: (...) -> None @@ -147,21 +141,15 @@ def __init__( 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 - # This is set in resolve self.require_hashes = None # type: Optional[bool] 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.use_user_site = use_user_site - self.use_pep517 = use_pep517 self._make_install_req = make_install_req self._discovered_dependencies = \ diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index 3436b398a24..479461e4838 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -70,13 +70,12 @@ def _basic_resolver(self, finder): use_pep517=None, ) return Resolver( - preparer=preparer, wheel_cache=None, + preparer=preparer, make_install_req=make_install_req, session=PipSession(), finder=finder, use_user_site=False, upgrade_strategy="to-satisfy-only", ignore_dependencies=False, ignore_installed=False, ignore_requires_python=False, force_reinstall=False, - isolated=False, ) def test_no_reuse_existing_build_dir(self, data): From 45283a332f45bd45ffb46f8af958d64eff5373c9 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Fri, 6 Sep 2019 19:40:37 -0400 Subject: [PATCH 0226/3170] Move delete marker file creation from unpack_url to caller. --- src/pip/_internal/download.py | 3 --- src/pip/_internal/operations/prepare.py | 4 ++++ 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index 2b1c147bf38..2f4b8260512 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -36,7 +36,6 @@ from pip._internal.utils.encoding import auto_decode from pip._internal.utils.filesystem import check_path_owner, copy2_fixed from pip._internal.utils.glibc import libc_ver -from pip._internal.utils.marker_files import write_delete_marker_file from pip._internal.utils.misc import ( ARCHIVE_EXTENSIONS, ask, @@ -1254,8 +1253,6 @@ def unpack_url( hashes=hashes, progress_bar=progress_bar ) - if only_download: - write_delete_marker_file(location) def sanitize_content_filename(filename): diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 745203cd4ea..4e3e4adf1fe 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -29,6 +29,7 @@ from pip._internal.utils.compat import expanduser from pip._internal.utils.hashes import MissingHashes from pip._internal.utils.logging import indent_log +from pip._internal.utils.marker_files import write_delete_marker_file from pip._internal.utils.misc import display_path, normalize_path from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -207,6 +208,9 @@ def prepare_linked_requirement( session=session, hashes=hashes, progress_bar=self.progress_bar ) + if autodelete_unpacked: + write_delete_marker_file(req.source_dir) + except requests.HTTPError as exc: logger.critical( 'Could not install requirement %s because of error %s', From aef01d25ccd42c9dfc94dc2e6a2c867abf5dbf10 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Fri, 6 Sep 2019 19:42:21 -0400 Subject: [PATCH 0227/3170] Remove unused argument from unpack_url. --- src/pip/_internal/download.py | 1 - src/pip/_internal/operations/prepare.py | 3 +-- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index 2f4b8260512..15d6fd56f08 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -1212,7 +1212,6 @@ def unpack_url( link, # type: Link location, # type: str download_dir=None, # type: Optional[str] - only_download=False, # type: bool session=None, # type: Optional[PipSession] hashes=None, # type: Optional[Hashes] progress_bar="on" # type: str diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 4e3e4adf1fe..592f6b0d3b3 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -203,8 +203,7 @@ def prepare_linked_requirement( # wheel. autodelete_unpacked = False unpack_url( - link, req.source_dir, - download_dir, autodelete_unpacked, + link, req.source_dir, download_dir, session=session, hashes=hashes, progress_bar=self.progress_bar ) From a20a06cd34ec17c88fc2c8354ca3bba7e01c1509 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Fri, 6 Sep 2019 19:46:15 -0400 Subject: [PATCH 0228/3170] Move unpack_url above irrelevant checks. --- src/pip/_internal/operations/prepare.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 592f6b0d3b3..2a14d1b9333 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -193,6 +193,13 @@ def prepare_linked_requirement( # when doing 'pip wheel` we download wheels to a # dedicated dir. download_dir = self.wheel_download_dir + + unpack_url( + link, req.source_dir, download_dir, + session=session, hashes=hashes, + progress_bar=self.progress_bar + ) + if link.is_wheel: if download_dir: # When downloading, we only unpack wheels to get @@ -202,11 +209,6 @@ def prepare_linked_requirement( # When installing a wheel, we use the unpacked # wheel. autodelete_unpacked = False - unpack_url( - link, req.source_dir, download_dir, - session=session, hashes=hashes, - progress_bar=self.progress_bar - ) if autodelete_unpacked: write_delete_marker_file(req.source_dir) From b72d9f1477185fbd9d0affe6c058d7d174c9b8e2 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Fri, 6 Sep 2019 19:47:50 -0400 Subject: [PATCH 0229/3170] Move autodelete_unpacked to more logical place. --- src/pip/_internal/operations/prepare.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 2a14d1b9333..5eee9e81266 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -187,8 +187,6 @@ def prepare_linked_requirement( try: download_dir = self.download_dir - # We always delete unpacked sdists after pip ran. - autodelete_unpacked = True if link.is_wheel and self.wheel_download_dir: # when doing 'pip wheel` we download wheels to a # dedicated dir. @@ -209,6 +207,9 @@ def prepare_linked_requirement( # When installing a wheel, we use the unpacked # wheel. autodelete_unpacked = False + else: + # We always delete unpacked sdists after pip runs. + autodelete_unpacked = True if autodelete_unpacked: write_delete_marker_file(req.source_dir) From 997c9b71b3c9cf1d7c9ac554eed223cc14de031e Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Fri, 6 Sep 2019 19:49:52 -0400 Subject: [PATCH 0230/3170] Move non-throwing code outside try/except. --- src/pip/_internal/operations/prepare.py | 43 ++++++++++++------------- 1 file changed, 21 insertions(+), 22 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 5eee9e81266..20e0301bed7 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -185,34 +185,18 @@ def prepare_linked_requirement( # showing the user what the hash should be. hashes = MissingHashes() - try: - download_dir = self.download_dir - if 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 + download_dir = self.download_dir + if 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 + try: unpack_url( link, req.source_dir, download_dir, session=session, hashes=hashes, progress_bar=self.progress_bar ) - - if 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 - else: - # We always delete unpacked sdists after pip runs. - autodelete_unpacked = True - if autodelete_unpacked: - write_delete_marker_file(req.source_dir) - except requests.HTTPError as exc: logger.critical( 'Could not install requirement %s because of error %s', @@ -225,6 +209,21 @@ def prepare_linked_requirement( (req, exc, link) ) + if 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 + else: + # We always delete unpacked sdists after pip runs. + autodelete_unpacked = True + if autodelete_unpacked: + write_delete_marker_file(req.source_dir) + abstract_dist = _get_prepared_distribution( req, self.req_tracker, finder, self.build_isolation, ) From 2963e4e2171f42c02d1ac898bb37ad7e05e11543 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Fri, 6 Sep 2019 21:05:15 -0400 Subject: [PATCH 0231/3170] Use ZipFile as context manager (Python 2.7+). --- src/pip/_internal/req/req_install.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 1359badf749..78073f0d3a9 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -898,11 +898,13 @@ def archive(self, build_dir): shutil.move(archive_path, dest_file) elif response == 'a': sys.exit(-1) - if create_archive: - zip = zipfile.ZipFile( - archive_path, 'w', zipfile.ZIP_DEFLATED, - allowZip64=True - ) + + if not create_archive: + return + + with zipfile.ZipFile( + archive_path, 'w', zipfile.ZIP_DEFLATED, allowZip64=True + ) as zip: dir = os.path.normcase(os.path.abspath(self.setup_py_dir)) for dirpath, dirnames, filenames in os.walk(dir): if 'pip-egg-info' in dirnames: @@ -922,8 +924,8 @@ def archive(self, build_dir): rootdir=dir) filename = os.path.join(dirpath, filename) zip.write(filename, file_arcname) - zip.close() - logger.info('Saved %s', display_path(archive_path)) + + logger.info('Saved %s', display_path(archive_path)) def install( self, From 4686e48e0fee85e082a47e4cc67d3f19f6cabe86 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Fri, 6 Sep 2019 21:18:10 -0400 Subject: [PATCH 0232/3170] Remove unused `update` parameter from `InstallRequirement`. --- src/pip/_internal/req/req_install.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 1359badf749..de68e0fb201 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -83,7 +83,6 @@ def __init__( source_dir=None, # type: Optional[str] editable=False, # type: bool link=None, # type: Optional[Link] - update=True, # type: bool markers=None, # type: Optional[Marker] use_pep517=None, # type: Optional[bool] isolated=False, # type: bool @@ -133,8 +132,6 @@ def __init__( # 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 # type: Optional[str] - # True if the editable should be updated: - self.update = update # Set to True after successful installation self.install_succeeded = None # type: Optional[bool] # UninstallPathSet of uninstalled distribution (for possible rollback) @@ -816,8 +813,6 @@ def update_editable(self, obtain=True): # Static paths don't get updated return assert '+' in self.link.url, "bad url: %r" % self.link.url - if not self.update: - return vc_type, url = self.link.url.split('+', 1) vcs_backend = vcs.get_backend(vc_type) if vcs_backend: From e5f05ce18c4fb6f2180ba418dc2167b6d4699815 Mon Sep 17 00:00:00 2001 From: AinsworthK Date: Sat, 7 Sep 2019 05:32:26 -0500 Subject: [PATCH 0233/3170] Directly import objects to be tested, from utils.outdated (#6966) --- tests/unit/test_unit_outdated.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/tests/unit/test_unit_outdated.py b/tests/unit/test_unit_outdated.py index 41081bbe7ae..092e7499205 100644 --- a/tests/unit/test_unit_outdated.py +++ b/tests/unit/test_unit_outdated.py @@ -11,6 +11,11 @@ from pip._internal.index import InstallationCandidate from pip._internal.utils import outdated +from pip._internal.utils.outdated import ( + SelfCheckState, + logger, + pip_version_check, +) from tests.lib.path import Path @@ -88,9 +93,9 @@ def test_pip_version_check(monkeypatch, stored_time, installed_ver, new_ver, monkeypatch.setattr(outdated, 'get_installed_version', lambda name: installed_ver) monkeypatch.setattr(outdated, 'PackageFinder', MockPackageFinder) - monkeypatch.setattr(outdated.logger, 'warning', + monkeypatch.setattr(logger, 'warning', pretend.call_recorder(lambda *a, **kw: None)) - monkeypatch.setattr(outdated.logger, 'debug', + monkeypatch.setattr(logger, 'debug', pretend.call_recorder(lambda s, exc_info=None: None)) monkeypatch.setattr(pkg_resources, 'get_distribution', lambda name: MockDistribution(installer)) @@ -111,7 +116,7 @@ def test_pip_version_check(monkeypatch, stored_time, installed_ver, new_ver, "pip._vendor.requests.packages.urllib3.packages.six.moves", ] ): - latest_pypi_version = outdated.pip_version_check(None, _options()) + latest_pypi_version = pip_version_check(None, _options()) # See we return None if not installed_version if not installed_ver: @@ -123,15 +128,15 @@ def test_pip_version_check(monkeypatch, stored_time, installed_ver, new_ver, ] else: # Make sure no Exceptions - assert not outdated.logger.debug.calls + assert not logger.debug.calls # See that save was not called assert fake_state.save.calls == [] # Ensure we warn the user or not if check_warn_logs: - assert len(outdated.logger.warning.calls) == 1 + assert len(logger.warning.calls) == 1 else: - assert len(outdated.logger.warning.calls) == 0 + assert len(logger.warning.calls) == 0 statefile_name_case_1 = ( @@ -185,7 +190,7 @@ def fake_lock(filename): key = 'pip_prefix' monkeypatch.setattr(sys, 'prefix', key) - state = outdated.SelfCheckState(cache_dir=cache_dir) + state = SelfCheckState(cache_dir=cache_dir) state.save('2.0', datetime.datetime.utcnow()) expected_path = _get_statefile_path(str(cache_dir), key) @@ -201,7 +206,7 @@ def fake_lock(filename): def test_self_check_state_no_cache_dir(): - state = outdated.SelfCheckState(cache_dir=False) + state = SelfCheckState(cache_dir=False) assert state.state == {} assert state.statefile_path is None From fcd1448f1d78bd82f574a8ce62ee9b35d15c5ee7 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Sat, 7 Sep 2019 16:57:51 +0530 Subject: [PATCH 0234/3170] Ignore "require_virtualenv" in `pip config` --- src/pip/_internal/commands/configuration.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pip/_internal/commands/configuration.py b/src/pip/_internal/commands/configuration.py index 4b3fc2baec4..6c3a0729523 100644 --- a/src/pip/_internal/commands/configuration.py +++ b/src/pip/_internal/commands/configuration.py @@ -34,6 +34,7 @@ class ConfigurationCommand(Command): default. """ + ignore_require_venv = True usage = """ %prog [] list %prog [] [--editor ] edit From 03117a415d3a46692e58bfd70b2db815da7bc167 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Sat, 7 Sep 2019 17:05:30 +0530 Subject: [PATCH 0235/3170] :newspaper: --- news/6991.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/6991.bugfix diff --git a/news/6991.bugfix b/news/6991.bugfix new file mode 100644 index 00000000000..db5904cdd42 --- /dev/null +++ b/news/6991.bugfix @@ -0,0 +1 @@ + Ignore "require_virtualenv" in `pip config` From 6ebee37dc4564e98f4d4bb52731d90531b1ce016 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Sat, 7 Sep 2019 09:39:32 -0400 Subject: [PATCH 0236/3170] Add comment, reformat logging statement. --- src/pip/_internal/wheel.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index a44affd1556..e7d66080fcd 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -1071,6 +1071,7 @@ def build( if ephem_cache is None: continue + # Determine where the wheel should go. if should_unpack: if ephem_cache: output_dir = self.wheel_cache.get_ephem_path_for_link( @@ -1105,8 +1106,10 @@ def build( try: ensure_dir(output_dir) except OSError as e: - logger.warning("Building wheel for %s failed: %s", - req.name, e) + logger.warning( + "Building wheel for %s failed: %s", + req.name, e, + ) build_failure.append(req) continue From 01fa93f715c6c329a7ddf76646e19af82bf94a55 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Mon, 2 Sep 2019 18:37:18 -0400 Subject: [PATCH 0237/3170] Add contextlib2 for ExitStack in Python 2. --- src/pip/_vendor/__init__.py | 1 + src/pip/_vendor/contextlib2.LICENSE | 120 ++++++++ src/pip/_vendor/contextlib2.py | 436 ++++++++++++++++++++++++++++ src/pip/_vendor/vendor.txt | 1 + 4 files changed, 558 insertions(+) create mode 100644 src/pip/_vendor/contextlib2.LICENSE create mode 100644 src/pip/_vendor/contextlib2.py diff --git a/src/pip/_vendor/__init__.py b/src/pip/_vendor/__init__.py index c1d9508dd4c..2f824556113 100644 --- a/src/pip/_vendor/__init__.py +++ b/src/pip/_vendor/__init__.py @@ -60,6 +60,7 @@ def vendored(modulename): # Actually alias all of our vendored dependencies. vendored("cachecontrol") vendored("colorama") + vendored("contextlib2") vendored("distlib") vendored("distro") vendored("html5lib") diff --git a/src/pip/_vendor/contextlib2.LICENSE b/src/pip/_vendor/contextlib2.LICENSE new file mode 100644 index 00000000000..c12b8ab2dd7 --- /dev/null +++ b/src/pip/_vendor/contextlib2.LICENSE @@ -0,0 +1,120 @@ +A. HISTORY OF THE SOFTWARE +========================== + +contextlib2 is a derivative of the contextlib module distributed by the PSF +as part of the Python standard library. According, it is itself redistributed +under the PSF license (reproduced in full below). As the contextlib module +was added only in Python 2.5, the licenses for earlier Python versions are +not applicable and have not been included. + +Python was created in the early 1990s by Guido van Rossum at Stichting +Mathematisch Centrum (CWI, see http://www.cwi.nl) in the Netherlands +as a successor of a language called ABC. Guido remains Python's +principal author, although it includes many contributions from others. + +In 1995, Guido continued his work on Python at the Corporation for +National Research Initiatives (CNRI, see http://www.cnri.reston.va.us) +in Reston, Virginia where he released several versions of the +software. + +In May 2000, Guido and the Python core development team moved to +BeOpen.com to form the BeOpen PythonLabs team. In October of the same +year, the PythonLabs team moved to Digital Creations (now Zope +Corporation, see http://www.zope.com). In 2001, the Python Software +Foundation (PSF, see http://www.python.org/psf/) was formed, a +non-profit organization created specifically to own Python-related +Intellectual Property. Zope Corporation is a sponsoring member of +the PSF. + +All Python releases are Open Source (see http://www.opensource.org for +the Open Source Definition). Historically, most, but not all, Python +releases have also been GPL-compatible; the table below summarizes +the various releases that included the contextlib module. + + Release Derived Year Owner GPL- + from compatible? (1) + + 2.5 2.4 2006 PSF yes + 2.5.1 2.5 2007 PSF yes + 2.5.2 2.5.1 2008 PSF yes + 2.5.3 2.5.2 2008 PSF yes + 2.6 2.5 2008 PSF yes + 2.6.1 2.6 2008 PSF yes + 2.6.2 2.6.1 2009 PSF yes + 2.6.3 2.6.2 2009 PSF yes + 2.6.4 2.6.3 2009 PSF yes + 2.6.5 2.6.4 2010 PSF yes + 3.0 2.6 2008 PSF yes + 3.0.1 3.0 2009 PSF yes + 3.1 3.0.1 2009 PSF yes + 3.1.1 3.1 2009 PSF yes + 3.1.2 3.1.1 2010 PSF yes + 3.1.3 3.1.2 2010 PSF yes + 3.1.4 3.1.3 2011 PSF yes + 3.2 3.1 2011 PSF yes + 3.2.1 3.2 2011 PSF yes + 3.2.2 3.2.1 2011 PSF yes + 3.3 3.2 2012 PSF yes + +Footnotes: + +(1) GPL-compatible doesn't mean that we're distributing Python under + the GPL. All Python licenses, unlike the GPL, let you distribute + a modified version without making your changes open source. The + GPL-compatible licenses make it possible to combine Python with + other software that is released under the GPL; the others don't. + +Thanks to the many outside volunteers who have worked under Guido's +direction to make these releases possible. + + +B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON +=============================================================== + +PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 +-------------------------------------------- + +1. This LICENSE AGREEMENT is between the Python Software Foundation +("PSF"), and the Individual or Organization ("Licensee") accessing and +otherwise using this software ("Python") in source or binary form and +its associated documentation. + +2. Subject to the terms and conditions of this License Agreement, PSF hereby +grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, +analyze, test, perform and/or display publicly, prepare derivative works, +distribute, and otherwise use Python alone or in any derivative version, +provided, however, that PSF's License Agreement and PSF's notice of copyright, +i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, +2011 Python Software Foundation; All Rights Reserved" are retained in Python +alone or in any derivative version prepared by Licensee. + +3. In the event Licensee prepares a derivative work that is based on +or incorporates Python or any part thereof, and wants to make +the derivative work available to others as provided herein, then +Licensee hereby agrees to include in any such work a brief summary of +the changes made to Python. + +4. PSF is making Python available to Licensee on an "AS IS" +basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR +IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND +DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS +FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT +INFRINGE ANY THIRD PARTY RIGHTS. + +5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON +FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS +A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, +OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. + +6. This License Agreement will automatically terminate upon a material +breach of its terms and conditions. + +7. Nothing in this License Agreement shall be deemed to create any +relationship of agency, partnership, or joint venture between PSF and +Licensee. This License Agreement does not grant permission to use PSF +trademarks or trade name in a trademark sense to endorse or promote +products or services of Licensee, or any third party. + +8. By copying, installing or otherwise using Python, Licensee +agrees to be bound by the terms and conditions of this License +Agreement. diff --git a/src/pip/_vendor/contextlib2.py b/src/pip/_vendor/contextlib2.py new file mode 100644 index 00000000000..f08df14ce54 --- /dev/null +++ b/src/pip/_vendor/contextlib2.py @@ -0,0 +1,436 @@ +"""contextlib2 - backports and enhancements to the contextlib module""" + +import sys +import warnings +from collections import deque +from functools import wraps + +__all__ = ["contextmanager", "closing", "ContextDecorator", "ExitStack", + "redirect_stdout", "redirect_stderr", "suppress"] + +# Backwards compatibility +__all__ += ["ContextStack"] + +class ContextDecorator(object): + "A base class or mixin that enables context managers to work as decorators." + + def refresh_cm(self): + """Returns the context manager used to actually wrap the call to the + decorated function. + + The default implementation just returns *self*. + + Overriding this method allows otherwise one-shot context managers + like _GeneratorContextManager to support use as decorators via + implicit recreation. + + DEPRECATED: refresh_cm was never added to the standard library's + ContextDecorator API + """ + warnings.warn("refresh_cm was never added to the standard library", + DeprecationWarning) + return self._recreate_cm() + + def _recreate_cm(self): + """Return a recreated instance of self. + + Allows an otherwise one-shot context manager like + _GeneratorContextManager to support use as + a decorator via implicit recreation. + + This is a private interface just for _GeneratorContextManager. + See issue #11647 for details. + """ + return self + + def __call__(self, func): + @wraps(func) + def inner(*args, **kwds): + with self._recreate_cm(): + return func(*args, **kwds) + return inner + + +class _GeneratorContextManager(ContextDecorator): + """Helper for @contextmanager decorator.""" + + def __init__(self, func, args, kwds): + self.gen = func(*args, **kwds) + self.func, self.args, self.kwds = func, args, kwds + # Issue 19330: ensure context manager instances have good docstrings + doc = getattr(func, "__doc__", None) + if doc is None: + doc = type(self).__doc__ + self.__doc__ = doc + # Unfortunately, this still doesn't provide good help output when + # inspecting the created context manager instances, since pydoc + # currently bypasses the instance docstring and shows the docstring + # for the class instead. + # See http://bugs.python.org/issue19404 for more details. + + def _recreate_cm(self): + # _GCM instances are one-shot context managers, so the + # CM must be recreated each time a decorated function is + # called + return self.__class__(self.func, self.args, self.kwds) + + def __enter__(self): + try: + return next(self.gen) + except StopIteration: + raise RuntimeError("generator didn't yield") + + def __exit__(self, type, value, traceback): + if type is None: + try: + next(self.gen) + except StopIteration: + return + else: + raise RuntimeError("generator didn't stop") + else: + if value is None: + # Need to force instantiation so we can reliably + # tell if we get the same exception back + value = type() + try: + self.gen.throw(type, value, traceback) + raise RuntimeError("generator didn't stop after throw()") + except StopIteration as exc: + # Suppress StopIteration *unless* it's the same exception that + # was passed to throw(). This prevents a StopIteration + # raised inside the "with" statement from being suppressed. + return exc is not value + except RuntimeError as exc: + # Don't re-raise the passed in exception + if exc is value: + return False + # Likewise, avoid suppressing if a StopIteration exception + # was passed to throw() and later wrapped into a RuntimeError + # (see PEP 479). + if _HAVE_EXCEPTION_CHAINING and exc.__cause__ is value: + return False + raise + except: + # only re-raise if it's *not* the exception that was + # passed to throw(), because __exit__() must not raise + # an exception unless __exit__() itself failed. But throw() + # has to raise the exception to signal propagation, so this + # fixes the impedance mismatch between the throw() protocol + # and the __exit__() protocol. + # + if sys.exc_info()[1] is not value: + raise + + +def contextmanager(func): + """@contextmanager decorator. + + Typical usage: + + @contextmanager + def some_generator(): + + try: + yield + finally: + + + This makes this: + + with some_generator() as : + + + equivalent to this: + + + try: + = + + finally: + + + """ + @wraps(func) + def helper(*args, **kwds): + return _GeneratorContextManager(func, args, kwds) + return helper + + +class closing(object): + """Context to automatically close something at the end of a block. + + Code like this: + + with closing(.open()) as f: + + + is equivalent to this: + + f = .open() + try: + + finally: + f.close() + + """ + def __init__(self, thing): + self.thing = thing + def __enter__(self): + return self.thing + def __exit__(self, *exc_info): + self.thing.close() + + +class _RedirectStream(object): + + _stream = None + + def __init__(self, new_target): + self._new_target = new_target + # We use a list of old targets to make this CM re-entrant + self._old_targets = [] + + def __enter__(self): + self._old_targets.append(getattr(sys, self._stream)) + setattr(sys, self._stream, self._new_target) + return self._new_target + + def __exit__(self, exctype, excinst, exctb): + setattr(sys, self._stream, self._old_targets.pop()) + + +class redirect_stdout(_RedirectStream): + """Context manager for temporarily redirecting stdout to another file. + + # How to send help() to stderr + with redirect_stdout(sys.stderr): + help(dir) + + # How to write help() to a file + with open('help.txt', 'w') as f: + with redirect_stdout(f): + help(pow) + """ + + _stream = "stdout" + + +class redirect_stderr(_RedirectStream): + """Context manager for temporarily redirecting stderr to another file.""" + + _stream = "stderr" + + +class suppress(object): + """Context manager to suppress specified exceptions + + After the exception is suppressed, execution proceeds with the next + statement following the with statement. + + with suppress(FileNotFoundError): + os.remove(somefile) + # Execution still resumes here if the file was already removed + """ + + def __init__(self, *exceptions): + self._exceptions = exceptions + + def __enter__(self): + pass + + def __exit__(self, exctype, excinst, exctb): + # Unlike isinstance and issubclass, CPython exception handling + # currently only looks at the concrete type hierarchy (ignoring + # the instance and subclass checking hooks). While Guido considers + # that a bug rather than a feature, it's a fairly hard one to fix + # due to various internal implementation details. suppress provides + # the simpler issubclass based semantics, rather than trying to + # exactly reproduce the limitations of the CPython interpreter. + # + # See http://bugs.python.org/issue12029 for more details + return exctype is not None and issubclass(exctype, self._exceptions) + + +# Context manipulation is Python 3 only +_HAVE_EXCEPTION_CHAINING = sys.version_info[0] >= 3 +if _HAVE_EXCEPTION_CHAINING: + def _make_context_fixer(frame_exc): + def _fix_exception_context(new_exc, old_exc): + # Context may not be correct, so find the end of the chain + while 1: + exc_context = new_exc.__context__ + if exc_context is old_exc: + # Context is already set correctly (see issue 20317) + return + if exc_context is None or exc_context is frame_exc: + break + new_exc = exc_context + # Change the end of the chain to point to the exception + # we expect it to reference + new_exc.__context__ = old_exc + return _fix_exception_context + + def _reraise_with_existing_context(exc_details): + try: + # bare "raise exc_details[1]" replaces our carefully + # set-up context + fixed_ctx = exc_details[1].__context__ + raise exc_details[1] + except BaseException: + exc_details[1].__context__ = fixed_ctx + raise +else: + # No exception context in Python 2 + def _make_context_fixer(frame_exc): + return lambda new_exc, old_exc: None + + # Use 3 argument raise in Python 2, + # but use exec to avoid SyntaxError in Python 3 + def _reraise_with_existing_context(exc_details): + exc_type, exc_value, exc_tb = exc_details + exec ("raise exc_type, exc_value, exc_tb") + +# Handle old-style classes if they exist +try: + from types import InstanceType +except ImportError: + # Python 3 doesn't have old-style classes + _get_type = type +else: + # Need to handle old-style context managers on Python 2 + def _get_type(obj): + obj_type = type(obj) + if obj_type is InstanceType: + return obj.__class__ # Old-style class + return obj_type # New-style class + +# Inspired by discussions on http://bugs.python.org/issue13585 +class ExitStack(object): + """Context manager for dynamic management of a stack of exit callbacks + + For example: + + with ExitStack() as stack: + files = [stack.enter_context(open(fname)) for fname in filenames] + # All opened files will automatically be closed at the end of + # the with statement, even if attempts to open files later + # in the list raise an exception + + """ + def __init__(self): + self._exit_callbacks = deque() + + def pop_all(self): + """Preserve the context stack by transferring it to a new instance""" + new_stack = type(self)() + new_stack._exit_callbacks = self._exit_callbacks + self._exit_callbacks = deque() + return new_stack + + def _push_cm_exit(self, cm, cm_exit): + """Helper to correctly register callbacks to __exit__ methods""" + def _exit_wrapper(*exc_details): + return cm_exit(cm, *exc_details) + _exit_wrapper.__self__ = cm + self.push(_exit_wrapper) + + def push(self, exit): + """Registers a callback with the standard __exit__ method signature + + Can suppress exceptions the same way __exit__ methods can. + + Also accepts any object with an __exit__ method (registering a call + to the method instead of the object itself) + """ + # We use an unbound method rather than a bound method to follow + # the standard lookup behaviour for special methods + _cb_type = _get_type(exit) + try: + exit_method = _cb_type.__exit__ + except AttributeError: + # Not a context manager, so assume its a callable + self._exit_callbacks.append(exit) + else: + self._push_cm_exit(exit, exit_method) + return exit # Allow use as a decorator + + def callback(self, callback, *args, **kwds): + """Registers an arbitrary callback and arguments. + + Cannot suppress exceptions. + """ + def _exit_wrapper(exc_type, exc, tb): + callback(*args, **kwds) + # We changed the signature, so using @wraps is not appropriate, but + # setting __wrapped__ may still help with introspection + _exit_wrapper.__wrapped__ = callback + self.push(_exit_wrapper) + return callback # Allow use as a decorator + + def enter_context(self, cm): + """Enters the supplied context manager + + If successful, also pushes its __exit__ method as a callback and + returns the result of the __enter__ method. + """ + # We look up the special methods on the type to match the with statement + _cm_type = _get_type(cm) + _exit = _cm_type.__exit__ + result = _cm_type.__enter__(cm) + self._push_cm_exit(cm, _exit) + return result + + def close(self): + """Immediately unwind the context stack""" + self.__exit__(None, None, None) + + def __enter__(self): + return self + + def __exit__(self, *exc_details): + received_exc = exc_details[0] is not None + + # We manipulate the exception state so it behaves as though + # we were actually nesting multiple with statements + frame_exc = sys.exc_info()[1] + _fix_exception_context = _make_context_fixer(frame_exc) + + # Callbacks are invoked in LIFO order to match the behaviour of + # nested context managers + suppressed_exc = False + pending_raise = False + while self._exit_callbacks: + cb = self._exit_callbacks.pop() + try: + if cb(*exc_details): + suppressed_exc = True + pending_raise = False + exc_details = (None, None, None) + except: + new_exc_details = sys.exc_info() + # simulate the stack of exceptions by setting the context + _fix_exception_context(new_exc_details[1], exc_details[1]) + pending_raise = True + exc_details = new_exc_details + if pending_raise: + _reraise_with_existing_context(exc_details) + return received_exc and suppressed_exc + +# Preserve backwards compatibility +class ContextStack(ExitStack): + """Backwards compatibility alias for ExitStack""" + + def __init__(self): + warnings.warn("ContextStack has been renamed to ExitStack", + DeprecationWarning) + super(ContextStack, self).__init__() + + def register_exit(self, callback): + return self.push(callback) + + def register(self, callback, *args, **kwds): + return self.callback(callback, *args, **kwds) + + def preserve(self): + return self.pop_all() diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index dbf7d664a58..84ff34ea1f1 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -1,6 +1,7 @@ appdirs==1.4.3 CacheControl==0.12.5 colorama==0.4.1 +contextlib2==0.5.5 distlib==0.2.9.post0 distro==1.4.0 html5lib==1.0.1 From 45fe22b3ec083e2f708b80dade34528f0ea443c6 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Mon, 2 Sep 2019 18:45:36 -0400 Subject: [PATCH 0238/3170] Manage overall application context in Command. --- src/pip/_internal/cli/base_command.py | 15 ++++++++++---- src/pip/_internal/cli/command_context.py | 26 ++++++++++++++++++++++++ 2 files changed, 37 insertions(+), 4 deletions(-) create mode 100644 src/pip/_internal/cli/command_context.py diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index 694527c1d65..dad08c24ea7 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -11,6 +11,7 @@ import traceback from pip._internal.cli import cmdoptions +from pip._internal.cli.command_context import CommandContextMixIn from pip._internal.cli.parser import ( ConfigOptionParser, UpdatingDefaultsHelpFormatter, @@ -44,12 +45,13 @@ logger = logging.getLogger(__name__) -class Command(object): +class Command(CommandContextMixIn): usage = None # type: str ignore_require_venv = False # type: bool def __init__(self, name, summary, isolated=False): # type: (str, str, bool) -> None + super(Command, self).__init__() parser_kw = { 'usage': self.usage, 'prog': '%s %s' % (get_prog(), name), @@ -95,6 +97,14 @@ def parse_args(self, args): return self.parser.parse_args(args) def main(self, args): + # type: (List[str]) -> int + try: + with self.main_context(): + return self._main(args) + finally: + logging.shutdown() + + def _main(self, args): # type: (List[str]) -> int options, args = self.parse_args(args) @@ -180,7 +190,4 @@ def main(self, args): finally: self.handle_pip_version_check(options) - # Shutdown the logging module - logging.shutdown() - return SUCCESS diff --git a/src/pip/_internal/cli/command_context.py b/src/pip/_internal/cli/command_context.py new file mode 100644 index 00000000000..c55f0116e19 --- /dev/null +++ b/src/pip/_internal/cli/command_context.py @@ -0,0 +1,26 @@ +from contextlib import contextmanager + +from pip._vendor.contextlib2 import ExitStack + + +class CommandContextMixIn(object): + def __init__(self): + super(CommandContextMixIn, self).__init__() + self._in_main_context = False + self._main_context = ExitStack() + + @contextmanager + def main_context(self): + assert not self._in_main_context + + self._in_main_context = True + try: + with self._main_context: + yield + finally: + self._in_main_context = False + + def enter_context(self, context_provider): + assert self._in_main_context + + return self._main_context.enter_context(context_provider) From 82be4ee76a4203cab7831f0e7b0b50d2db29dee2 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Mon, 2 Sep 2019 18:48:15 -0400 Subject: [PATCH 0239/3170] Use application context manager for PipSession. This removes a level of indentation from all commands without introducing any dummy functions. --- src/pip/_internal/cli/req_command.py | 15 +- src/pip/_internal/commands/download.py | 115 ++++----- src/pip/_internal/commands/install.py | 315 ++++++++++++------------ src/pip/_internal/commands/search.py | 14 +- src/pip/_internal/commands/uninstall.py | 57 ++--- src/pip/_internal/commands/wheel.py | 105 ++++---- tests/functional/test_search.py | 10 +- 7 files changed, 325 insertions(+), 306 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 62ffe4b149b..081ad190b62 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -10,6 +10,7 @@ from pip._internal.cli.base_command import Command from pip._internal.cli.cmdoptions import make_search_scope +from pip._internal.cli.command_context import CommandContextMixIn from pip._internal.download import PipSession from pip._internal.exceptions import CommandError from pip._internal.index import PackageFinder @@ -36,11 +37,14 @@ from pip._internal.utils.temp_dir import TempDirectory -class SessionCommandMixin(object): +class SessionCommandMixin(CommandContextMixIn): """ A class mixin for command classes needing _build_session(). """ + def __init__(self): + super(SessionCommandMixin, self).__init__() + self._session = None # Optional[PipSession] @classmethod def _get_index_urls(cls, options): @@ -56,6 +60,13 @@ def _get_index_urls(cls, options): # Return None rather than an empty list return index_urls or None + def get_default_session(self, options): + # type: (Values) -> PipSession + """Get a default-managed session.""" + if self._session is None: + self._session = self.enter_context(self._build_session(options)) + return self._session + def _build_session(self, options, retries=None, timeout=None): # type: (Values, Optional[int], Optional[int]) -> PipSession session = PipSession( @@ -95,7 +106,7 @@ def _build_session(self, options, retries=None, timeout=None): return session -class IndexGroupCommand(SessionCommandMixin, Command): +class IndexGroupCommand(Command, SessionCommandMixin): """ Abstract base class for commands with the index_group options. diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index 1f0cd7c7347..5068990481c 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -88,65 +88,66 @@ def run(self, options, args): ensure_dir(options.download_dir) - with self._build_session(options) as session: - target_python = make_target_python(options) - finder = self._build_package_finder( + session = self.get_default_session(options) + + target_python = make_target_python(options) + finder = self._build_package_finder( + options=options, + session=session, + target_python=target_python, + ) + build_delete = (not (options.no_clean or options.build_dir)) + if options.cache_dir and not check_path_owner(options.cache_dir): + logger.warning( + "The directory '%s' or its parent directory is not owned " + "by the current user and caching wheels has been " + "disabled. check the permissions and owner of that " + "directory. If executing pip with sudo, you may want " + "sudo's -H flag.", + options.cache_dir, + ) + options.cache_dir = None + + with RequirementTracker() as req_tracker, TempDirectory( + options.build_dir, delete=build_delete, kind="download" + ) as directory: + + requirement_set = RequirementSet( + require_hashes=options.require_hashes, + ) + self.populate_requirement_set( + requirement_set, + args, + options, + finder, + session, + None + ) + + preparer = self.make_requirement_preparer( + temp_directory=directory, options=options, + req_tracker=req_tracker, + download_dir=options.download_dir, + ) + + resolver = self.make_resolver( + preparer=preparer, + finder=finder, session=session, - target_python=target_python, + options=options, + py_version_info=options.python_version, ) - build_delete = (not (options.no_clean or options.build_dir)) - if options.cache_dir and not check_path_owner(options.cache_dir): - logger.warning( - "The directory '%s' or its parent directory is not owned " - "by the current user and caching wheels has been " - "disabled. check the permissions and owner of that " - "directory. If executing pip with sudo, you may want " - "sudo's -H flag.", - options.cache_dir, - ) - options.cache_dir = None - - with RequirementTracker() as req_tracker, TempDirectory( - options.build_dir, delete=build_delete, kind="download" - ) as directory: - - requirement_set = RequirementSet( - require_hashes=options.require_hashes, - ) - self.populate_requirement_set( - requirement_set, - args, - options, - finder, - session, - None - ) - - preparer = self.make_requirement_preparer( - temp_directory=directory, - options=options, - req_tracker=req_tracker, - download_dir=options.download_dir, - ) - - resolver = self.make_resolver( - preparer=preparer, - finder=finder, - session=session, - options=options, - py_version_info=options.python_version, - ) - resolver.resolve(requirement_set) - - downloaded = ' '.join([ - req.name for req in requirement_set.successfully_downloaded - ]) - if downloaded: - write_output('Successfully downloaded %s', downloaded) - - # Clean up - if not options.no_clean: - requirement_set.cleanup_files() + resolver.resolve(requirement_set) + + downloaded = ' '.join([ + req.name for req in requirement_set.successfully_downloaded + ]) + if downloaded: + write_output('Successfully downloaded %s', downloaded) + + # Clean up + if not options.no_clean: + requirement_set.cleanup_files() return requirement_set diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 27cc6e815bd..a50229a9a31 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -321,179 +321,180 @@ def run(self, options, args): global_options = options.global_options or [] - with self._build_session(options) as session: - target_python = make_target_python(options) - finder = self._build_package_finder( - options=options, - session=session, - target_python=target_python, - ignore_requires_python=options.ignore_requires_python, + session = self.get_default_session(options) + + target_python = make_target_python(options) + finder = self._build_package_finder( + options=options, + session=session, + target_python=target_python, + ignore_requires_python=options.ignore_requires_python, + ) + build_delete = (not (options.no_clean or options.build_dir)) + wheel_cache = WheelCache(options.cache_dir, options.format_control) + + if options.cache_dir and not check_path_owner(options.cache_dir): + logger.warning( + "The directory '%s' or its parent directory is not owned " + "by the current user and caching wheels has been " + "disabled. check the permissions and owner of that " + "directory. If executing pip with sudo, you may want " + "sudo's -H flag.", + options.cache_dir, + ) + options.cache_dir = None + + with RequirementTracker() as req_tracker, TempDirectory( + options.build_dir, delete=build_delete, kind="install" + ) as directory: + requirement_set = RequirementSet( + require_hashes=options.require_hashes, + check_supported_wheels=not options.target_dir, ) - 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, + + try: + self.populate_requirement_set( + requirement_set, args, options, finder, session, + wheel_cache + ) + preparer = self.make_requirement_preparer( + temp_directory=directory, + options=options, + req_tracker=req_tracker, ) - options.cache_dir = None - - with RequirementTracker() as req_tracker, TempDirectory( - options.build_dir, delete=build_delete, kind="install" - ) as directory: - requirement_set = RequirementSet( - require_hashes=options.require_hashes, - check_supported_wheels=not options.target_dir, + resolver = self.make_resolver( + preparer=preparer, + finder=finder, + session=session, + options=options, + wheel_cache=wheel_cache, + use_user_site=options.use_user_site, + ignore_installed=options.ignore_installed, + ignore_requires_python=options.ignore_requires_python, + force_reinstall=options.force_reinstall, + upgrade_strategy=upgrade_strategy, + use_pep517=options.use_pep517, ) + resolver.resolve(requirement_set) try: - self.populate_requirement_set( - requirement_set, args, options, finder, session, - wheel_cache - ) - preparer = self.make_requirement_preparer( - temp_directory=directory, - options=options, - req_tracker=req_tracker, - ) - resolver = self.make_resolver( - preparer=preparer, - finder=finder, - session=session, - options=options, - wheel_cache=wheel_cache, - use_user_site=options.use_user_site, - ignore_installed=options.ignore_installed, - ignore_requires_python=options.ignore_requires_python, - force_reinstall=options.force_reinstall, - upgrade_strategy=upgrade_strategy, - use_pep517=options.use_pep517, - ) - resolver.resolve(requirement_set) + pip_req = requirement_set.get_requirement("pip") + except KeyError: + modifying_pip = None + else: + # If we're not replacing an already installed pip, + # we're not modifying it. + modifying_pip = pip_req.satisfied_by is None + protect_pip_from_modification_on_windows( + modifying_pip=modifying_pip + ) - try: - pip_req = requirement_set.get_requirement("pip") - except KeyError: - modifying_pip = None + check_binary_allowed = get_check_binary_allowed( + finder.format_control + ) + # Consider legacy and PEP517-using requirements separately + legacy_requirements = [] + pep517_requirements = [] + for req in requirement_set.requirements.values(): + if req.use_pep517: + pep517_requirements.append(req) else: - # If we're not replacing an already installed pip, - # we're not modifying it. - modifying_pip = pip_req.satisfied_by is None - protect_pip_from_modification_on_windows( - modifying_pip=modifying_pip - ) + legacy_requirements.append(req) - check_binary_allowed = get_check_binary_allowed( - finder.format_control - ) - # Consider legacy and PEP517-using requirements separately - legacy_requirements = [] - pep517_requirements = [] - for req in requirement_set.requirements.values(): - if req.use_pep517: - pep517_requirements.append(req) - else: - legacy_requirements.append(req) + wheel_builder = WheelBuilder( + preparer, wheel_cache, + build_options=[], global_options=[], + check_binary_allowed=check_binary_allowed, + ) - wheel_builder = WheelBuilder( - preparer, wheel_cache, - build_options=[], global_options=[], - check_binary_allowed=check_binary_allowed, - ) + build_failures = build_wheels( + builder=wheel_builder, + pep517_requirements=pep517_requirements, + legacy_requirements=legacy_requirements, + ) - build_failures = build_wheels( - builder=wheel_builder, - pep517_requirements=pep517_requirements, - legacy_requirements=legacy_requirements, - ) + # If we're using PEP 517, we cannot do a direct install + # so we fail here. + if build_failures: + raise InstallationError( + "Could not build wheels for {} which use" + " PEP 517 and cannot be installed directly".format( + ", ".join(r.name for r in build_failures))) - # If we're using PEP 517, we cannot do a direct install - # so we fail here. - if build_failures: - raise InstallationError( - "Could not build wheels for {} which use" - " PEP 517 and cannot be installed directly".format( - ", ".join(r.name for r in build_failures))) + to_install = resolver.get_installation_order( + requirement_set + ) - 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, + ) - # 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, - ) + 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, + ) + working_set = pkg_resources.WorkingSet(lib_locations) - 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, - ) - working_set = pkg_resources.WorkingSet(lib_locations) - - reqs = sorted(installed, key=operator.attrgetter('name')) - items = [] - for req in reqs: - item = req.name - try: - installed_version = get_installed_version( - req.name, working_set=working_set - ) - if installed_version: - item += '-' + installed_version - except Exception: - pass - items.append(item) - installed_desc = ' '.join(items) - if installed_desc: - write_output( - 'Successfully installed %s', installed_desc, + reqs = sorted(installed, key=operator.attrgetter('name')) + items = [] + for req in reqs: + item = req.name + try: + installed_version = get_installed_version( + req.name, working_set=working_set ) - except EnvironmentError as error: - show_traceback = (self.verbosity >= 1) - - message = create_env_error_message( - error, show_traceback, options.use_user_site, + if installed_version: + item += '-' + installed_version + except Exception: + pass + items.append(item) + installed_desc = ' '.join(items) + if installed_desc: + write_output( + 'Successfully installed %s', installed_desc, ) - 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() + 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( diff --git a/src/pip/_internal/commands/search.py b/src/pip/_internal/commands/search.py index d0a36e83f65..ef698d0b7ec 100644 --- a/src/pip/_internal/commands/search.py +++ b/src/pip/_internal/commands/search.py @@ -24,7 +24,7 @@ logger = logging.getLogger(__name__) -class SearchCommand(SessionCommandMixin, Command): +class SearchCommand(Command, SessionCommandMixin): """Search for PyPI packages whose name or summary contains .""" usage = """ @@ -60,11 +60,13 @@ def run(self, options, args): def search(self, query, options): index_url = options.index - with self._build_session(options) as session: - transport = PipXmlrpcTransport(index_url, session) - pypi = xmlrpc_client.ServerProxy(index_url, transport) - hits = pypi.search({'name': query, 'summary': query}, 'or') - return hits + + session = self.get_default_session(options) + + transport = PipXmlrpcTransport(index_url, session) + pypi = xmlrpc_client.ServerProxy(index_url, transport) + hits = pypi.search({'name': query, 'summary': query}, 'or') + return hits def transform_hits(hits): diff --git a/src/pip/_internal/commands/uninstall.py b/src/pip/_internal/commands/uninstall.py index 6d72400e6b3..0d8e4662f09 100644 --- a/src/pip/_internal/commands/uninstall.py +++ b/src/pip/_internal/commands/uninstall.py @@ -10,7 +10,7 @@ from pip._internal.utils.misc import protect_pip_from_modification_on_windows -class UninstallCommand(SessionCommandMixin, Command): +class UninstallCommand(Command, SessionCommandMixin): """ Uninstall packages. @@ -45,34 +45,35 @@ def __init__(self, *args, **kw): self.parser.insert_option_group(0, self.cmd_opts) def run(self, options, args): - with self._build_session(options) as session: - reqs_to_uninstall = {} - for name in args: - req = install_req_from_line( - name, isolated=options.isolated_mode, - ) + session = self.get_default_session(options) + + reqs_to_uninstall = {} + for name in args: + req = install_req_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): 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): - 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) - ) - - protect_pip_from_modification_on_windows( - modifying_pip="pip" in reqs_to_uninstall + 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) ) - 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() + protect_pip_from_modification_on_windows( + modifying_pip="pip" in reqs_to_uninstall + ) + + 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() diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index 635dedca67f..a9b9507fe19 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -115,61 +115,62 @@ def run(self, options, args): options.src_dir = os.path.abspath(options.src_dir) - 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) + session = self.get_default_session(options) - with RequirementTracker() as req_tracker, TempDirectory( - options.build_dir, delete=build_delete, kind="wheel" - ) as directory: + 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) - requirement_set = RequirementSet( - require_hashes=options.require_hashes, - ) + with RequirementTracker() as req_tracker, TempDirectory( + options.build_dir, delete=build_delete, kind="wheel" + ) as directory: - try: - self.populate_requirement_set( - requirement_set, args, options, finder, session, - wheel_cache - ) + requirement_set = RequirementSet( + require_hashes=options.require_hashes, + ) - preparer = self.make_requirement_preparer( - temp_directory=directory, - options=options, - req_tracker=req_tracker, - wheel_download_dir=options.wheel_dir, - ) + try: + self.populate_requirement_set( + requirement_set, args, options, finder, session, + wheel_cache + ) - resolver = self.make_resolver( - preparer=preparer, - finder=finder, - session=session, - options=options, - wheel_cache=wheel_cache, - ignore_requires_python=options.ignore_requires_python, - use_pep517=options.use_pep517, - ) - resolver.resolve(requirement_set) - - # build wheels - wb = WheelBuilder( - preparer, wheel_cache, - build_options=options.build_options or [], - global_options=options.global_options or [], - no_clean=options.no_clean, - ) - build_failures = wb.build( - requirement_set.requirements.values(), + preparer = self.make_requirement_preparer( + temp_directory=directory, + options=options, + req_tracker=req_tracker, + wheel_download_dir=options.wheel_dir, + ) + + resolver = self.make_resolver( + preparer=preparer, + finder=finder, + session=session, + options=options, + wheel_cache=wheel_cache, + ignore_requires_python=options.ignore_requires_python, + use_pep517=options.use_pep517, + ) + resolver.resolve(requirement_set) + + # build wheels + wb = WheelBuilder( + preparer, wheel_cache, + build_options=options.build_options or [], + global_options=options.global_options or [], + no_clean=options.no_clean, + ) + build_failures = wb.build( + requirement_set.requirements.values(), + ) + if len(build_failures) != 0: + raise CommandError( + "Failed to build one or more wheels" ) - if len(build_failures) != 0: - raise CommandError( - "Failed to build one or more wheels" - ) - except PreviousBuildDirError: - options.no_clean = True - raise - finally: - if not options.no_clean: - requirement_set.cleanup_files() - wheel_cache.cleanup() + except PreviousBuildDirError: + options.no_clean = True + raise + finally: + if not options.no_clean: + requirement_set.cleanup_files() + wheel_cache.cleanup() diff --git a/tests/functional/test_search.py b/tests/functional/test_search.py index 70421de3091..fce6c5f819c 100644 --- a/tests/functional/test_search.py +++ b/tests/functional/test_search.py @@ -113,8 +113,9 @@ def test_run_method_should_return_success_when_find_packages(): """ command = create_command('search') cmdline = "--index=https://pypi.org/pypi pip" - options, args = command.parse_args(cmdline.split()) - status = command.run(options, args) + with command.main_context(): + options, args = command.parse_args(cmdline.split()) + status = command.run(options, args) assert status == SUCCESS @@ -125,8 +126,9 @@ def test_run_method_should_return_no_matches_found_when_does_not_find_pkgs(): """ command = create_command('search') cmdline = "--index=https://pypi.org/pypi nonexistentpackage" - options, args = command.parse_args(cmdline.split()) - status = command.run(options, args) + with command.main_context(): + options, args = command.parse_args(cmdline.split()) + status = command.run(options, args) assert status == NO_MATCHES_FOUND From 3da9a89b5b88afe951d785f831d4932e47d24564 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Sat, 7 Sep 2019 09:56:36 -0400 Subject: [PATCH 0240/3170] %-style to .format style string formatting. --- src/pip/_internal/wheel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index 0f4c9070627..131a1c4ab9e 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -598,10 +598,10 @@ def is_entrypoint_wrapper(name): except MissingCallableSuffix as e: entry = e.args[0] raise InstallationError( - "Invalid script entry point: %s for req: %s - A callable " + "Invalid script entry point: {} for req: {} - A callable " "suffix is required. Cf https://packaging.python.org/en/" "latest/distributing.html#console-scripts for more " - "information." % (entry, req) + "information.".format(entry, req) ) if warn_script_location: From f92961d99e453524822adeacd04c74b0d2316839 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Sat, 7 Sep 2019 09:59:41 -0400 Subject: [PATCH 0241/3170] More descriptive function name. --- src/pip/_internal/wheel.py | 4 ++-- tests/unit/test_wheel.py | 13 ++++++++----- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index 131a1c4ab9e..11525056b2d 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -316,7 +316,7 @@ class MissingCallableSuffix(Exception): pass -def _assert_valid_entrypoint(specification): +def _raise_for_invalid_entrypoint(specification): entry = get_export_entry(specification) if entry is not None and entry.suffix is None: raise MissingCallableSuffix(str(entry)) @@ -324,7 +324,7 @@ def _assert_valid_entrypoint(specification): class PipScriptMaker(ScriptMaker): def make(self, specification, options=None): - _assert_valid_entrypoint(specification) + _raise_for_invalid_entrypoint(specification) return super(PipScriptMaker, self).make(specification, options) diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index d794aeab5ca..2ae38d562cb 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -15,7 +15,10 @@ from pip._internal.req.req_install import InstallRequirement from pip._internal.utils.compat import WINDOWS from pip._internal.utils.misc import unpack_file -from pip._internal.wheel import MissingCallableSuffix, _assert_valid_entrypoint +from pip._internal.wheel import ( + MissingCallableSuffix, + _raise_for_invalid_entrypoint, +) from tests.lib import DATA_DIR, assert_paths_equal @@ -266,17 +269,17 @@ def test_get_entrypoints(tmpdir, console_scripts): ) -def test_assert_valid_entrypoint_ok(): - _assert_valid_entrypoint("hello = hello:main") +def test_raise_for_invalid_entrypoint_ok(): + _raise_for_invalid_entrypoint("hello = hello:main") @pytest.mark.parametrize("entrypoint", [ "hello = hello", "hello = hello:", ]) -def test_assert_valid_entrypoint_fail(entrypoint): +def test_raise_for_invalid_entrypoint_fail(entrypoint): with pytest.raises(MissingCallableSuffix): - _assert_valid_entrypoint(entrypoint) + _raise_for_invalid_entrypoint(entrypoint) @pytest.mark.parametrize("outrows, expected", [ From 11ec2a3b1735e3e41a441257286147efbb0b127a Mon Sep 17 00:00:00 2001 From: Hugo Date: Sun, 8 Sep 2019 11:53:58 +0300 Subject: [PATCH 0242/3170] Remove redundant check --- src/pip/_internal/utils/compat.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/utils/compat.py b/src/pip/_internal/utils/compat.py index 36398614fe9..3808ab057c1 100644 --- a/src/pip/_internal/utils/compat.py +++ b/src/pip/_internal/utils/compat.py @@ -9,7 +9,7 @@ import shutil import sys -from pip._vendor.six import PY2, PY3, text_type +from pip._vendor.six import PY3, text_type from pip._vendor.urllib3.util import IS_PYOPENSSL from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -72,9 +72,8 @@ # backslash replacement for all versions. def backslashreplace_decode_fn(err): raw_bytes = (err.object[i] for i in range(err.start, err.end)) - if PY2: - # Python 2 gave us characters - convert to numeric bytes - raw_bytes = (ord(b) for b in raw_bytes) + # 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", From 7256a10ccbc1f0518b4e57f27db388ade8466572 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Sun, 8 Sep 2019 14:49:40 +0530 Subject: [PATCH 0243/3170] Fix Azure Pipelines Package build --- .azure-pipelines/jobs/package.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.azure-pipelines/jobs/package.yml b/.azure-pipelines/jobs/package.yml index 510fa3e4d68..2070bc5d42b 100644 --- a/.azure-pipelines/jobs/package.yml +++ b/.azure-pipelines/jobs/package.yml @@ -21,7 +21,7 @@ jobs: - bash: nox -s generate_authors displayName: Generate AUTHORS.txt - - bash: nox -s generate_news --yes + - bash: nox -s generate_news -- --yes displayName: Generate NEWS.rst - bash: tox -e packaging From fd5ffb63464a3bc085cf9e71520ea3696b892a84 Mon Sep 17 00:00:00 2001 From: Hugo Date: Sun, 8 Sep 2019 14:47:19 +0300 Subject: [PATCH 0244/3170] Fix for Python 4: replace unsafe six.PY3 with PY2 --- src/pip/_internal/utils/compat.py | 39 ++++++++++++++++--------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/src/pip/_internal/utils/compat.py b/src/pip/_internal/utils/compat.py index 3808ab057c1..caa15c4c5dc 100644 --- a/src/pip/_internal/utils/compat.py +++ b/src/pip/_internal/utils/compat.py @@ -9,7 +9,7 @@ import shutil import sys -from pip._vendor.six import PY3, text_type +from pip._vendor.six import PY2, text_type from pip._vendor.urllib3.util import IS_PYOPENSSL from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -47,10 +47,7 @@ HAS_TLS = (ssl is not None) or IS_PYOPENSSL -if PY3: - uses_pycache = True - from importlib.util import cache_from_source -else: +if PY2: import imp try: @@ -60,11 +57,12 @@ cache_from_source = None uses_pycache = cache_from_source is not None +else: + uses_pycache = True + from importlib.util import cache_from_source -if PY3: - backslashreplace_decode = "backslashreplace" -else: +if PY2: # In version 3.4 and older, backslashreplace exists # but does not support use for decoding. # We implement our own replace handler for this @@ -80,6 +78,8 @@ def backslashreplace_decode_fn(err): backslashreplace_decode_fn, ) backslashreplace_decode = "backslashreplace_decode" +else: + backslashreplace_decode = "backslashreplace" def str_to_display(data, desc=None): @@ -155,19 +155,19 @@ def console_to_str(data): return str_to_display(data, desc='Subprocess output') -if PY3: +if PY2: def native_str(s, replace=False): # type: (str, bool) -> str - if isinstance(s, bytes): - return s.decode('utf-8', 'replace' if replace else 'strict') + # Replace is ignored -- unicode to UTF-8 can't fail + if isinstance(s, text_type): + return s.encode('utf-8') return s else: def native_str(s, replace=False): # type: (str, bool) -> str - # Replace is ignored -- unicode to UTF-8 can't fail - if isinstance(s, text_type): - return s.encode('utf-8') + if isinstance(s, bytes): + return s.decode('utf-8', 'replace' if replace else 'strict') return s @@ -201,16 +201,17 @@ def get_path_uid(path): return file_uid -if PY3: - from importlib.machinery import EXTENSION_SUFFIXES +if PY2: + from imp import get_suffixes def get_extension_suffixes(): - return EXTENSION_SUFFIXES + return [suffix[0] for suffix in get_suffixes()] + else: - from imp import get_suffixes + from importlib.machinery import EXTENSION_SUFFIXES def get_extension_suffixes(): - return [suffix[0] for suffix in get_suffixes()] + return EXTENSION_SUFFIXES def expanduser(path): From 6d20cbca23d15f7128516617fc45db374f781ce8 Mon Sep 17 00:00:00 2001 From: Hugo Date: Sun, 8 Sep 2019 15:19:03 +0300 Subject: [PATCH 0245/3170] Remove noqa workaround --- tests/functional/test_install.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index a8d3cf9628e..76e8b3ce4a8 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -5,9 +5,8 @@ import textwrap from os.path import curdir, join, pardir -# Ignore because flake8 can't detect the use inside skipif(). -import pip._vendor.six # noqa: F401 import pytest +from pip._vendor.six import PY2 from pip._internal import pep425tags from pip._internal.cli.status_codes import ERROR, SUCCESS @@ -21,6 +20,8 @@ from tests.lib.local_repos import local_checkout from tests.lib.path import Path +python2_only = pytest.mark.skipif(not PY2, reason="Python 2 only") + @pytest.mark.parametrize('command', ('install', 'wheel')) @pytest.mark.parametrize('variant', ('missing_setuptools', 'bad_setuptools')) @@ -530,7 +531,7 @@ def test_editable_install__local_dir_no_setup_py_with_pyproject( assert 'A "pyproject.toml" file was found' in msg -@pytest.mark.skipif("pip._vendor.six.PY3") +@python2_only @pytest.mark.xfail def test_install_argparse_shadowed(script): # When argparse is in the stdlib, we support installing it From af33759cb5ab632796f1f4a2f75ce57b4632bd46 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Sun, 8 Sep 2019 09:24:20 -0400 Subject: [PATCH 0246/3170] Add NEWS file. --- news/6763.bugfix | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 news/6763.bugfix diff --git a/news/6763.bugfix b/news/6763.bugfix new file mode 100644 index 00000000000..68d0b58fd64 --- /dev/null +++ b/news/6763.bugfix @@ -0,0 +1,2 @@ +Switch to new ``distlib`` wheel script template. This should be functionally +equivalent for end users. From b4bc7a62f435c0eb65c0a3e2f46f286fa2875165 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Thu, 15 Aug 2019 00:38:40 -0400 Subject: [PATCH 0247/3170] Remove lockfile-dependent test. --- tests/unit/test_unit_outdated.py | 46 +------------------------------- 1 file changed, 1 insertion(+), 45 deletions(-) diff --git a/tests/unit/test_unit_outdated.py b/tests/unit/test_unit_outdated.py index 092e7499205..d7230288b02 100644 --- a/tests/unit/test_unit_outdated.py +++ b/tests/unit/test_unit_outdated.py @@ -2,12 +2,11 @@ import json import os import sys -from contextlib import contextmanager import freezegun import pretend import pytest -from pip._vendor import lockfile, pkg_resources +from pip._vendor import pkg_resources from pip._internal.index import InstallationCandidate from pip._internal.utils import outdated @@ -162,49 +161,6 @@ def _get_statefile_path(cache_dir, key): ) -def test_self_check_state(monkeypatch, tmpdir): - CONTENT = '''{"key": "pip_prefix", "last_check": "1970-01-02T11:00:00Z", - "pypi_version": "1.0"}''' - fake_file = pretend.stub( - read=pretend.call_recorder(lambda: CONTENT), - write=pretend.call_recorder(lambda s: None), - ) - - @pretend.call_recorder - @contextmanager - def fake_open(filename, mode='r'): - yield fake_file - - monkeypatch.setattr(outdated, 'open', fake_open, raising=False) - - @pretend.call_recorder - @contextmanager - def fake_lock(filename): - yield - - monkeypatch.setattr(outdated, "check_path_owner", lambda p: True) - - monkeypatch.setattr(lockfile, 'LockFile', fake_lock) - - cache_dir = tmpdir / 'cache_dir' - key = 'pip_prefix' - monkeypatch.setattr(sys, 'prefix', key) - - state = SelfCheckState(cache_dir=cache_dir) - state.save('2.0', datetime.datetime.utcnow()) - - expected_path = _get_statefile_path(str(cache_dir), key) - assert fake_lock.calls == [pretend.call(expected_path)] - - assert fake_open.calls == [ - pretend.call(expected_path), - pretend.call(expected_path, 'w'), - ] - - # json.dumps will call this a number of times - assert len(fake_file.write.calls) - - def test_self_check_state_no_cache_dir(): state = SelfCheckState(cache_dir=False) assert state.state == {} From 6617d158387370c6890bd2d0562c84908187e646 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Thu, 15 Aug 2019 00:39:19 -0400 Subject: [PATCH 0248/3170] Write to tempfile and move instead of locking selfcheck file. --- src/pip/_internal/utils/filesystem.py | 47 +++++++++++++++++++++++++++ src/pip/_internal/utils/outdated.py | 20 ++++++++---- 2 files changed, 61 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/utils/filesystem.py b/src/pip/_internal/utils/filesystem.py index c5233ebbc71..56224e20ae6 100644 --- a/src/pip/_internal/utils/filesystem.py +++ b/src/pip/_internal/utils/filesystem.py @@ -2,8 +2,25 @@ import os.path import shutil import stat +from contextlib import contextmanager +from tempfile import NamedTemporaryFile + +# 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._internal.utils.compat import get_path_uid +from pip._internal.utils.misc import cast +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import BinaryIO, Iterator + + class NamedTemporaryFileResult(BinaryIO): + @property + def file(self): + # type: () -> BinaryIO + pass def check_path_owner(path): @@ -59,3 +76,33 @@ def copy2_fixed(src, dest): def is_socket(path): # type: (str) -> bool return stat.S_ISSOCK(os.lstat(path).st_mode) + + +@contextmanager +def adjacent_tmp_file(path): + # type: (str) -> Iterator[NamedTemporaryFileResult] + """Given a path to a file, open a temp file next to it securely and ensure + it is written to disk after the context reaches its end. + """ + with NamedTemporaryFile( + delete=False, + dir=os.path.dirname(path), + prefix=os.path.basename(path), + suffix='.tmp', + ) as f: + result = cast('NamedTemporaryFileResult', f) + try: + yield result + finally: + result.file.flush() + os.fsync(result.file.fileno()) + + +@retry(stop_max_delay=1000, wait_fixed=250) +def replace(src, dest): + # type: (str, str) -> None + try: + os.rename(src, dest) + except OSError: + os.remove(dest) + os.rename(src, dest) diff --git a/src/pip/_internal/utils/outdated.py b/src/pip/_internal/utils/outdated.py index e0c90e15c49..241599b274a 100644 --- a/src/pip/_internal/utils/outdated.py +++ b/src/pip/_internal/utils/outdated.py @@ -7,7 +7,7 @@ import os.path import sys -from pip._vendor import lockfile, pkg_resources +from pip._vendor import pkg_resources from pip._vendor.packaging import version as packaging_version from pip._vendor.six import ensure_binary @@ -15,7 +15,11 @@ from pip._internal.index import PackageFinder from pip._internal.models.selection_prefs import SelectionPreferences from pip._internal.utils.compat import WINDOWS -from pip._internal.utils.filesystem import check_path_owner +from pip._internal.utils.filesystem import ( + adjacent_tmp_file, + check_path_owner, + replace, +) from pip._internal.utils.misc import ensure_dir, get_installed_version from pip._internal.utils.packaging import get_installer from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -86,12 +90,16 @@ def save(self, pypi_version, current_time): text = json.dumps(state, sort_keys=True, separators=(",", ":")) - # Attempt to write out our version check file - with lockfile.LockFile(self.statefile_path): + with adjacent_tmp_file(self.statefile_path) as f: + f.write(ensure_binary(text)) + + try: # Since we have a prefix-specific state file, we can just # overwrite whatever is there, no need to check. - with open(self.statefile_path, "w") as statefile: - statefile.write(text) + replace(f.name, self.statefile_path) + except OSError: + # Best effort. + pass def was_installed_by_pip(pkg): From fc45975c8400e369a1e97176fbdec341cee71aa5 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Thu, 15 Aug 2019 01:06:59 -0400 Subject: [PATCH 0249/3170] Add news files. --- news/6954.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/6954.bugfix diff --git a/news/6954.bugfix b/news/6954.bugfix new file mode 100644 index 00000000000..8f6f67109cb --- /dev/null +++ b/news/6954.bugfix @@ -0,0 +1 @@ +Don't use hardlinks for locking selfcheck state file. From 68e6feb31a98b1327b01b17f63074c49ac0e2068 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Tue, 3 Sep 2019 20:08:58 -0400 Subject: [PATCH 0250/3170] Use os.replace in Python 3. --- src/pip/_internal/utils/filesystem.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/utils/filesystem.py b/src/pip/_internal/utils/filesystem.py index 56224e20ae6..f4a389cd92f 100644 --- a/src/pip/_internal/utils/filesystem.py +++ b/src/pip/_internal/utils/filesystem.py @@ -8,6 +8,7 @@ # 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._internal.utils.compat import get_path_uid from pip._internal.utils.misc import cast @@ -98,11 +99,17 @@ def adjacent_tmp_file(path): os.fsync(result.file.fileno()) -@retry(stop_max_delay=1000, wait_fixed=250) -def replace(src, dest): - # type: (str, str) -> None - try: - os.rename(src, dest) - except OSError: - os.remove(dest) - os.rename(src, dest) +_replace_retry = retry(stop_max_delay=1000, wait_fixed=250) + +if PY2: + @_replace_retry + def replace(src, dest): + # type: (str, str) -> None + try: + os.rename(src, dest) + except OSError: + os.remove(dest) + os.rename(src, dest) + +else: + replace = _replace_retry(os.replace) From 97f719cee245efe4cd5f4d18360507bdaea93285 Mon Sep 17 00:00:00 2001 From: Hugo Date: Sun, 8 Sep 2019 19:39:50 +0300 Subject: [PATCH 0251/3170] Explicitly refer to Python 2.7 instead of <=3.4 --- src/pip/_internal/utils/compat.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/utils/compat.py b/src/pip/_internal/utils/compat.py index caa15c4c5dc..ebaae3bd597 100644 --- a/src/pip/_internal/utils/compat.py +++ b/src/pip/_internal/utils/compat.py @@ -63,7 +63,7 @@ if PY2: - # In version 3.4 and older, backslashreplace exists + # In Python 2.7, backslashreplace exists # but does not support use for decoding. # We implement our own replace handler for this # situation, so that we can consistently use From 0f7921e1200adf4495c627adc38760f66254df94 Mon Sep 17 00:00:00 2001 From: Hugo Date: Sun, 8 Sep 2019 20:03:13 +0300 Subject: [PATCH 0252/3170] Add a @non_python2_only skipif --- tests/functional/test_install.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 76e8b3ce4a8..bcfbc7485e7 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -21,6 +21,7 @@ from tests.lib.path import Path python2_only = pytest.mark.skipif(not PY2, reason="Python 2 only") +non_python2_only = pytest.mark.skipif(PY2, reason="Non-Python 2 only") @pytest.mark.parametrize('command', ('install', 'wheel')) @@ -546,7 +547,7 @@ def test_install_argparse_shadowed(script): @pytest.mark.network -@pytest.mark.skipif("pip._vendor.six.PY2") +@non_python2_only def test_upgrade_argparse_shadowed(script): # If argparse is installed - even if shadowed for imported - we support # upgrading it and properly remove the older versions files. From 94b34c3e54e6627d35dd6853b79b443d44026436 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Sun, 8 Sep 2019 20:44:11 -0400 Subject: [PATCH 0253/3170] Remove FIXME tracked in issue tracker. --- src/pip/_internal/req/req_install.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index de68e0fb201..c153c9b127a 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -1000,7 +1000,6 @@ def prepend_root(path): self, ) # FIXME: put the record somewhere - # FIXME: should this be an error? return new_lines = [] with open(record_filename) as f: From 8f684d9eab9731342aad495c5de17e5ee2291ad7 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Sun, 8 Sep 2019 21:21:08 -0400 Subject: [PATCH 0254/3170] Remove unactionable FIXME in `InstallRequirement` `setup.py develop` doesn't accept `--install-headers`, so there's no need for this comment. --- src/pip/_internal/req/req_install.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index de68e0fb201..9920e6d07cc 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -786,7 +786,6 @@ def install_editable( no_user_config=self.isolated ) with indent_log(): - # FIXME: should we do --install-headers here too? with self.build_env: call_subprocess( base_cmd + From c46496fba69ab89416768a2787687af865190122 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Sun, 8 Sep 2019 19:27:27 -0400 Subject: [PATCH 0255/3170] Remove `InstallRequirement.uninstalled_pathset` Last actual usages of this were removed in #4642. --- src/pip/_internal/req/req_install.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 15f6ce7e371..173c72cd603 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -134,8 +134,6 @@ def __init__( self._ideal_build_dir = None # type: Optional[str] # Set to True after successful installation self.install_succeeded = None # type: Optional[bool] - # UninstallPathSet of uninstalled distribution (for possible rollback) - self.uninstalled_pathset = None self.options = options if options else {} # Set to True after successful preparation of this requirement self.prepared = False From 2b94acb9d82084abedd6cac5eb5d9e6767ca5dce Mon Sep 17 00:00:00 2001 From: Hugo Date: Mon, 9 Sep 2019 20:43:09 +0300 Subject: [PATCH 0256/3170] Rename for clarity --- tests/functional/test_install.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index bcfbc7485e7..8e4d4228378 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -20,8 +20,8 @@ from tests.lib.local_repos import local_checkout from tests.lib.path import Path -python2_only = pytest.mark.skipif(not PY2, reason="Python 2 only") -non_python2_only = pytest.mark.skipif(PY2, reason="Non-Python 2 only") +skip_if_python2 = pytest.mark.skipif(PY2, reason="Non-Python 2 only") +skip_if_not_python2 = pytest.mark.skipif(not PY2, reason="Python 2 only") @pytest.mark.parametrize('command', ('install', 'wheel')) @@ -532,7 +532,7 @@ def test_editable_install__local_dir_no_setup_py_with_pyproject( assert 'A "pyproject.toml" file was found' in msg -@python2_only +@skip_if_not_python2 @pytest.mark.xfail def test_install_argparse_shadowed(script): # When argparse is in the stdlib, we support installing it @@ -547,7 +547,7 @@ def test_install_argparse_shadowed(script): @pytest.mark.network -@non_python2_only +@skip_if_python2 def test_upgrade_argparse_shadowed(script): # If argparse is installed - even if shadowed for imported - we support # upgrading it and properly remove the older versions files. From 96a53d49eca9144c2e4a4e76fa11db219f4d1410 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Mon, 9 Sep 2019 19:45:33 -0400 Subject: [PATCH 0257/3170] Move archive description from TODO to docstring. This whole class will be refactored, so the TODO is implicit. --- src/pip/_internal/req/req_install.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index de68e0fb201..b88f2dc722a 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -866,10 +866,12 @@ def _get_archive_name(self, path, parentdir, rootdir): name = self._clean_zip_name(path, rootdir) return self.name + '/' + name - # TODO: Investigate if this should be kept in InstallRequirement - # Seems to be used only when VCS + downloads def archive(self, build_dir): # type: (str) -> None + """Saves archive to provided build_dir. + + Used for saving downloaded VCS requirements as part of `pip download`. + """ assert self.source_dir create_archive = True archive_name = '%s-%s.zip' % (self.name, self.metadata["version"]) From 9322b9fa22705ccf7d069a7dfc291bdf2746cf1f Mon Sep 17 00:00:00 2001 From: Ashwin Ramaswami Date: Tue, 10 Sep 2019 09:53:45 -0700 Subject: [PATCH 0258/3170] Fix grammar --- src/pip/_internal/utils/outdated.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/utils/outdated.py b/src/pip/_internal/utils/outdated.py index e0c90e15c49..70e8d83d3fe 100644 --- a/src/pip/_internal/utils/outdated.py +++ b/src/pip/_internal/utils/outdated.py @@ -180,7 +180,7 @@ def pip_version_check(session, options): else: pip_cmd = "pip" logger.warning( - "You are using pip version %s, however version %s is " + "You are using pip version %s; however, version %s is " "available.\nYou should consider upgrading via the " "'%s install --upgrade pip' command.", pip_version, pypi_version, pip_cmd From 9ae5f1a8df64cf20858a72b3c0b02127c32fa9bd Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 10 Sep 2019 09:32:09 -0700 Subject: [PATCH 0259/3170] Rename PackageFinder's _package_versions() to evaluate_links(). --- src/pip/_internal/index.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/index.py b/src/pip/_internal/index.py index ba44e7bb44c..3ab5cabf11d 100644 --- a/src/pip/_internal/index.py +++ b/src/pip/_internal/index.py @@ -1022,14 +1022,18 @@ def get_install_candidate(self, link_evaluator, link): version=str(result), ) - def _package_versions(self, link_evaluator, links): + def evaluate_links(self, link_evaluator, links): # type: (LinkEvaluator, Iterable[Link]) -> List[InstallationCandidate] - result = [] + """ + Convert links that are candidates to InstallationCandidate objects. + """ + candidates = [] for link in self._sort_links(links): candidate = self.get_install_candidate(link_evaluator, link) if candidate is not None: - result.append(candidate) - return result + candidates.append(candidate) + + return candidates def find_all_candidates(self, project_name): # type: (str) -> List[InstallationCandidate] @@ -1070,21 +1074,26 @@ def find_all_candidates(self, project_name): logger.debug('* %s', location) link_evaluator = self.make_link_evaluator(project_name) - find_links_versions = self._package_versions( + find_links_versions = self.evaluate_links( link_evaluator, # We trust every directly linked archive in find_links - (Link(url, '-f') for url in self.find_links), + links=(Link(url, '-f') for url in self.find_links), ) page_versions = [] for page in self._get_pages(url_locations, project_name): logger.debug('Analyzing links from page %s', page.url) with indent_log(): - page_versions.extend( - self._package_versions(link_evaluator, page.iter_links()) + new_versions = self.evaluate_links( + link_evaluator, + links=page.iter_links(), ) + page_versions.extend(new_versions) - file_versions = self._package_versions(link_evaluator, file_locations) + file_versions = self.evaluate_links( + link_evaluator, + links=file_locations, + ) if file_versions: file_versions.sort(reverse=True) logger.debug( From ed55cde68966e2c0b32e6fd543f4ce066609f516 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Wed, 21 Aug 2019 23:46:55 -0700 Subject: [PATCH 0260/3170] Add LinkCollector class to index.py. --- src/pip/_internal/index.py | 208 +++++++++++++++++++++++++----------- tests/unit/test_finder.py | 5 +- tests/unit/test_index.py | 22 ++-- tests/unit/test_req.py | 2 +- tests/unit/test_req_file.py | 2 +- 5 files changed, 166 insertions(+), 73 deletions(-) diff --git a/src/pip/_internal/index.py b/src/pip/_internal/index.py index 3ab5cabf11d..ed08e0272c2 100644 --- a/src/pip/_internal/index.py +++ b/src/pip/_internal/index.py @@ -48,8 +48,8 @@ if MYPY_CHECK_RUNNING: from typing import ( - Any, Callable, FrozenSet, Iterable, List, MutableMapping, Optional, - Sequence, Set, Text, Tuple, Union, + Any, Callable, Dict, FrozenSet, Iterable, List, MutableMapping, + Optional, Sequence, Set, Text, Tuple, Union, ) import xml.etree.ElementTree from pip._vendor.packaging.version import _BaseVersion @@ -312,6 +312,123 @@ def sort_path(path): return files, urls +class CollectedLinks(object): + + """ + Encapsulates all the Link objects collected by a call to + LinkCollector.collect_links(), stored separately as-- + + (1) links from the configured file locations, + (2) links from the configured find_links, and + (3) a dict mapping HTML page url to links from that page. + """ + + def __init__( + self, + files, # type: List[Link] + find_links, # type: List[Link] + pages, # type: Dict[str, List[Link]] + ): + # type: (...) -> None + """ + :param files: Links from file locations. + :param find_links: Links from find_links. + :param pages: A dict mapping HTML page url to links from that page. + """ + self.files = files + self.find_links = find_links + self.pages = pages + + +class LinkCollector(object): + + """ + Responsible for collecting Link objects from all configured locations, + making network requests as needed. + + The class's main method is its collect_links() method. + """ + + def __init__( + self, + session, # type: PipSession + search_scope, # type: SearchScope + ): + # type: (...) -> None + self.search_scope = search_scope + self.session = session + + @property + def find_links(self): + # type: () -> List[str] + return self.search_scope.find_links + + def _get_pages(self, locations, project_name): + # type: (Iterable[Link], str) -> Iterable[HTMLPage] + """ + Yields (page, page_url) from the given locations, skipping + locations that have errors. + """ + seen = set() # type: Set[Link] + for location in locations: + if location in seen: + continue + seen.add(location) + + page = _get_html_page(location, session=self.session) + if page is None: + continue + + yield page + + def collect_links(self, project_name): + # type: (str) -> CollectedLinks + """Find all available links for the given project name. + + :return: All the Link objects (unfiltered), as a CollectedLinks object. + """ + search_scope = self.search_scope + index_locations = search_scope.get_index_urls_locations(project_name) + index_file_loc, index_url_loc = group_locations(index_locations) + fl_file_loc, fl_url_loc = group_locations( + self.find_links, expand_dir=True, + ) + + file_links = [ + Link(url) for url in itertools.chain(index_file_loc, fl_file_loc) + ] + + # We trust every directly linked archive in find_links + find_link_links = [Link(url, '-f') for url in self.find_links] + + # We trust every url that the user has given us whether it was given + # via --index-url or --find-links. + # We want to filter out anything that does not have a secure origin. + url_locations = [ + link for link in itertools.chain( + (Link(url) for url in index_url_loc), + (Link(url) for url in fl_url_loc), + ) + if self.session.is_secure_origin(link) + ] + + logger.debug('%d location(s) to search for versions of %s:', + len(url_locations), project_name) + + for location in url_locations: + logger.debug('* %s', location) + + pages_links = {} + for page in self._get_pages(url_locations, project_name): + pages_links[page.url] = list(page.iter_links()) + + return CollectedLinks( + files=file_links, + find_links=find_link_links, + pages=pages_links, + ) + + def _check_link_requires_python( link, # type: Link version_info, # type: Tuple[int, int, int] @@ -853,8 +970,7 @@ class PackageFinder(object): def __init__( self, - search_scope, # type: SearchScope - session, # type: PipSession + link_collector, # type: LinkCollector target_python, # type: TargetPython allow_yanked, # type: bool format_control=None, # type: Optional[FormatControl] @@ -866,7 +982,6 @@ def __init__( This constructor is primarily meant to be used by the create() class method and from tests. - :param session: The Session to use to make requests. :param format_control: A FormatControl object, used to control the selection of source packages / binary packages when consulting the index and links. @@ -881,10 +996,9 @@ def __init__( self._allow_yanked = allow_yanked self._candidate_prefs = candidate_prefs self._ignore_requires_python = ignore_requires_python + self._link_collector = link_collector self._target_python = target_python - self.search_scope = search_scope - self.session = session self.format_control = format_control # These are boring links that have already been logged somehow. @@ -925,20 +1039,34 @@ def create( allow_all_prereleases=selection_prefs.allow_all_prereleases, ) + link_collector = LinkCollector( + session=session, + search_scope=search_scope, + ) + return cls( candidate_prefs=candidate_prefs, - search_scope=search_scope, - session=session, + link_collector=link_collector, target_python=target_python, allow_yanked=selection_prefs.allow_yanked, format_control=selection_prefs.format_control, ignore_requires_python=selection_prefs.ignore_requires_python, ) + @property + def search_scope(self): + # type: () -> SearchScope + return self._link_collector.search_scope + + @search_scope.setter + def search_scope(self, search_scope): + # type: (SearchScope) -> None + self._link_collector.search_scope = search_scope + @property def find_links(self): # type: () -> List[str] - return self.search_scope.find_links + return self._link_collector.find_links @property def index_urls(self): @@ -948,7 +1076,7 @@ def index_urls(self): @property def trusted_hosts(self): # type: () -> Iterable[str] - for host_port in self.session.pip_trusted_origins: + for host_port in self._link_collector.session.pip_trusted_origins: yield build_netloc(*host_port) @property @@ -1045,54 +1173,28 @@ def find_all_candidates(self, project_name): See LinkEvaluator.evaluate_link() for details on which files are accepted. """ - search_scope = self.search_scope - index_locations = search_scope.get_index_urls_locations(project_name) - index_file_loc, index_url_loc = group_locations(index_locations) - fl_file_loc, fl_url_loc = group_locations( - self.find_links, expand_dir=True, - ) - - file_locations = (Link(url) for url in itertools.chain( - index_file_loc, fl_file_loc, - )) - - # We trust every url that the user has given us whether it was given - # via --index-url or --find-links. - # We want to filter out any thing which does not have a secure origin. - url_locations = [ - link for link in itertools.chain( - (Link(url) for url in index_url_loc), - (Link(url) for url in fl_url_loc), - ) - if self.session.is_secure_origin(link) - ] - - logger.debug('%d location(s) to search for versions of %s:', - len(url_locations), project_name) - - for location in url_locations: - logger.debug('* %s', location) + collected_links = self._link_collector.collect_links(project_name) link_evaluator = self.make_link_evaluator(project_name) + find_links_versions = self.evaluate_links( link_evaluator, - # We trust every directly linked archive in find_links - links=(Link(url, '-f') for url in self.find_links), + links=collected_links.find_links, ) page_versions = [] - for page in self._get_pages(url_locations, project_name): - logger.debug('Analyzing links from page %s', page.url) + for page_url, page_links in collected_links.pages.items(): + logger.debug('Analyzing links from page %s', page_url) with indent_log(): new_versions = self.evaluate_links( link_evaluator, - links=page.iter_links(), + links=page_links, ) page_versions.extend(new_versions) file_versions = self.evaluate_links( link_evaluator, - links=file_locations, + links=collected_links.files, ) if file_versions: file_versions.sort(reverse=True) @@ -1228,24 +1330,6 @@ def _format_versions(cand_iter): ) return best_candidate.link - def _get_pages(self, locations, project_name): - # type: (Iterable[Link], str) -> Iterable[HTMLPage] - """ - Yields (page, page_url) from the given locations, skipping - locations that have errors. - """ - seen = set() # type: Set[Link] - for location in locations: - if location in seen: - continue - seen.add(location) - - page = _get_html_page(location, session=self.session) - if page is None: - continue - - yield page - def _find_name_version_sep(fragment, canonical_name): # type: (str, str) -> int diff --git a/tests/unit/test_finder.py b/tests/unit/test_finder.py index cc8fd8fc19b..fd68fcf8e27 100644 --- a/tests/unit/test_finder.py +++ b/tests/unit/test_finder.py @@ -35,8 +35,9 @@ def make_no_network_finder( find_links=find_links, allow_all_prereleases=allow_all_prereleases, ) - # Replace the PackageFinder object's _get_pages() with a no-op. - finder._get_pages = lambda locations, project_name: [] + # Replace the PackageFinder._link_collector's _get_pages() with a no-op. + link_collector = finder._link_collector + link_collector._get_pages = lambda locations, project_name: [] return finder diff --git a/tests/unit/test_index.py b/tests/unit/test_index.py index 9f53c0b4af2..26e9ea08c4a 100644 --- a/tests/unit/test_index.py +++ b/tests/unit/test_index.py @@ -12,7 +12,7 @@ CandidatePreferences, FormatControl, HTMLPage, - Link, + LinkCollector, LinkEvaluator, PackageFinder, _check_link_requires_python, @@ -25,6 +25,7 @@ group_locations, ) from pip._internal.models.candidate import InstallationCandidate +from pip._internal.models.link import Link from pip._internal.models.search_scope import SearchScope from pip._internal.models.selection_prefs import SelectionPreferences from pip._internal.models.target_python import TargetPython @@ -684,9 +685,14 @@ def test_make_link_evaluator( # Create a test TargetPython that we can check for. target_python = TargetPython(py_version_info=(3, 7)) format_control = FormatControl(set(), only_binary) - finder = PackageFinder( - search_scope=SearchScope([], []), + + link_collector = LinkCollector( session=PipSession(), + search_scope=SearchScope([], []), + ) + + finder = PackageFinder( + link_collector=link_collector, target_python=target_python, allow_yanked=allow_yanked, format_control=format_control, @@ -725,9 +731,12 @@ def test_make_candidate_evaluator( prefer_binary=prefer_binary, allow_all_prereleases=allow_all_prereleases, ) - finder = PackageFinder( - search_scope=SearchScope([], []), + link_collector = LinkCollector( session=PipSession(), + search_scope=SearchScope([], []), + ) + finder = PackageFinder( + link_collector=link_collector, target_python=target_python, allow_yanked=True, candidate_prefs=candidate_prefs, @@ -773,8 +782,7 @@ def test_group_locations__non_existing_path(): """ Test that a non-existing path is ignored. """ - files, urls = group_locations( - [os.path.join('this', 'doesnt', 'exist')]) + files, urls = group_locations([os.path.join('this', 'doesnt', 'exist')]) assert not urls and not files, "nothing should have been found" diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index 479461e4838..6085f37f122 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -193,7 +193,7 @@ def test_missing_hash_with_require_hashes_in_reqs_file(self, data, tmpdir): """ req_set = RequirementSet(require_hashes=False) finder = make_test_finder(find_links=[data.find_links]) - session = finder.session + session = finder._link_collector.session command = create_command('install') with requirements_file('--require-hashes', tmpdir) as reqs_file: options, args = command.parse_args(['-r', reqs_file]) diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py index 443a7605469..81a94a715fc 100644 --- a/tests/unit/test_req_file.py +++ b/tests/unit/test_req_file.py @@ -349,7 +349,7 @@ def test_set_finder_trusted_host(self, caplog, session, finder): "file.txt", 1, finder=finder, session=session, )) assert list(finder.trusted_hosts) == ['host1', 'host2:8080'] - session = finder.session + session = finder._link_collector.session assert session.adapters['https://host1/'] is session._insecure_adapter assert ( session.adapters['https://host2:8080/'] From b96ee36ba9c5d7e79592eab52c653ae9a37dbcb8 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Tue, 10 Sep 2019 22:23:00 -0400 Subject: [PATCH 0261/3170] Remove old comment in Resolver. `requirement_set` is no longer passed to `Resolver._check_skip_installed`, so we do not need to mention it. --- src/pip/_internal/legacy_resolve.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pip/_internal/legacy_resolve.py b/src/pip/_internal/legacy_resolve.py index 8dbed9ac187..7f39e257265 100644 --- a/src/pip/_internal/legacy_resolve.py +++ b/src/pip/_internal/legacy_resolve.py @@ -227,7 +227,6 @@ def _set_req_to_reinstall(self, req): 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): # type: (InstallRequirement) -> Optional[str] """Check if req_to_install should be skipped. From 81d8005bd13c356978784aea9052f4339f8f4c32 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Wed, 11 Sep 2019 19:03:57 -0400 Subject: [PATCH 0262/3170] Change argument name to better reflect purpose. Also assert on provided path, since it should have been created in all cases. --- src/pip/_internal/cli/req_command.py | 6 ++++-- src/pip/_internal/commands/download.py | 2 +- src/pip/_internal/commands/install.py | 2 +- src/pip/_internal/commands/wheel.py | 2 +- 4 files changed, 7 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 081ad190b62..e1fd495e4c9 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -141,7 +141,7 @@ class RequirementCommand(IndexGroupCommand): @staticmethod def make_requirement_preparer( - temp_directory, # type: TempDirectory + temp_build_dir, # type: TempDirectory options, # type: Values req_tracker, # type: RequirementTracker download_dir=None, # type: str @@ -151,8 +151,10 @@ def make_requirement_preparer( """ Create a RequirementPreparer instance for the given parameters. """ + temp_build_dir_path = temp_build_dir.path + assert temp_build_dir_path is not None return RequirementPreparer( - build_dir=temp_directory.path, + build_dir=temp_build_dir_path, src_dir=options.src_dir, download_dir=download_dir, wheel_download_dir=wheel_download_dir, diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index 5068990481c..4712a894d8a 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -125,7 +125,7 @@ def run(self, options, args): ) preparer = self.make_requirement_preparer( - temp_directory=directory, + temp_build_dir=directory, options=options, req_tracker=req_tracker, download_dir=options.download_dir, diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index a50229a9a31..356d674b91d 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -358,7 +358,7 @@ def run(self, options, args): wheel_cache ) preparer = self.make_requirement_preparer( - temp_directory=directory, + temp_build_dir=directory, options=options, req_tracker=req_tracker, ) diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index a9b9507fe19..b95732b0ce4 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -136,7 +136,7 @@ def run(self, options, args): ) preparer = self.make_requirement_preparer( - temp_directory=directory, + temp_build_dir=directory, options=options, req_tracker=req_tracker, wheel_download_dir=options.wheel_dir, From 12a27d0c9f10fc9efadfebf52126010db4593e63 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 10 Sep 2019 09:46:04 -0700 Subject: [PATCH 0263/3170] Add a couple tests. --- tests/lib/__init__.py | 21 ++++++-- tests/unit/test_index.py | 102 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 116 insertions(+), 7 deletions(-) diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index e895c7c1c3f..b7046b34be6 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -80,6 +80,21 @@ def create_file(path, contents=None): f.write("\n") +def make_test_search_scope( + find_links=None, # type: Optional[List[str]] + index_urls=None, # type: Optional[List[str]] +): + if find_links is None: + find_links = [] + if index_urls is None: + index_urls = [] + + return SearchScope.create( + find_links=find_links, + index_urls=index_urls, + ) + + def make_test_finder( find_links=None, # type: Optional[List[str]] index_urls=None, # type: Optional[List[str]] @@ -91,14 +106,10 @@ def make_test_finder( """ Create a PackageFinder for testing purposes. """ - if find_links is None: - find_links = [] - if index_urls is None: - index_urls = [] if session is None: session = PipSession() - search_scope = SearchScope.create( + search_scope = make_test_search_scope( find_links=find_links, index_urls=index_urls, ) diff --git a/tests/unit/test_index.py b/tests/unit/test_index.py index 26e9ea08c4a..d884fe52c08 100644 --- a/tests/unit/test_index.py +++ b/tests/unit/test_index.py @@ -1,8 +1,9 @@ import logging import os.path +from textwrap import dedent import pytest -from mock import Mock +from mock import Mock, patch from pip._vendor import html5lib, requests from pip._vendor.packaging.specifiers import SpecifierSet @@ -25,13 +26,94 @@ group_locations, ) from pip._internal.models.candidate import InstallationCandidate +from pip._internal.models.index import PyPI from pip._internal.models.link import Link from pip._internal.models.search_scope import SearchScope from pip._internal.models.selection_prefs import SelectionPreferences from pip._internal.models.target_python import TargetPython from pip._internal.pep425tags import get_supported from pip._internal.utils.hashes import Hashes -from tests.lib import CURRENT_PY_VERSION_INFO +from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from tests.lib import CURRENT_PY_VERSION_INFO, make_test_search_scope + +if MYPY_CHECK_RUNNING: + from typing import List, Optional + + +def make_fake_html_page(url): + html = dedent(u"""\ + + + abc-1.0.tar.gz + + """) + content = html.encode('utf-8') + headers = {} + return HTMLPage(content, url=url, headers=headers) + + +def make_test_link_collector( + find_links=None, # type: Optional[List[str]] +): + # type: (...) -> LinkCollector + """ + Create a LinkCollector object for testing purposes. + """ + session = PipSession() + search_scope = make_test_search_scope( + find_links=find_links, + index_urls=[PyPI.simple_url], + ) + + return LinkCollector( + session=session, + search_scope=search_scope, + ) + + +def check_links_include(links, names): + """ + Assert that the given list of Link objects includes, for each of the + given names, a link whose URL has a base name matching that name. + """ + for name in names: + assert any(link.url.endswith(name) for link in links), ( + 'name {!r} not among links: {}'.format(name, links) + ) + + +class TestLinkCollector(object): + + @patch('pip._internal.index._get_html_response') + def test_collect_links(self, mock_get_html_response, data): + expected_url = 'https://pypi.org/simple/twine/' + + fake_page = make_fake_html_page(expected_url) + mock_get_html_response.return_value = fake_page + + link_collector = make_test_link_collector( + find_links=[data.find_links] + ) + actual = link_collector.collect_links('twine') + + mock_get_html_response.assert_called_once_with( + expected_url, session=link_collector.session, + ) + + # Spot-check the CollectedLinks return value. + assert len(actual.files) > 20 + check_links_include(actual.files, names=['simple-1.0.tar.gz']) + + assert len(actual.find_links) == 1 + check_links_include(actual.find_links, names=['packages']) + + actual_pages = actual.pages + assert list(actual_pages) == [expected_url] + actual_page_links = actual_pages[expected_url] + assert len(actual_page_links) == 1 + assert actual_page_links[0].url == ( + 'https://pypi.org/abc-1.0.tar.gz#md5=000000000' + ) def make_mock_candidate(version, yanked_reason=None, hex_digest=None): @@ -586,6 +668,22 @@ def test_create__candidate_prefs( assert candidate_prefs.allow_all_prereleases == allow_all_prereleases assert candidate_prefs.prefer_binary == prefer_binary + def test_create__link_collector(self): + """ + Test that the _link_collector attribute is set correctly. + """ + search_scope = SearchScope([], []) + session = PipSession() + finder = PackageFinder.create( + search_scope=search_scope, + selection_prefs=SelectionPreferences(allow_yanked=True), + session=session, + ) + + actual_link_collector = finder._link_collector + assert actual_link_collector.search_scope is search_scope + assert actual_link_collector.session is session + def test_create__target_python(self): """ Test that the _target_python attribute is set correctly. From ca4fc9e741a06aee1ebc526abe32cbcb7e7f686f Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 10 Sep 2019 09:56:48 -0700 Subject: [PATCH 0264/3170] Update architecture/package-finding.rst. --- .../architecture/package-finding.rst | 47 ++++++++++++++++--- 1 file changed, 40 insertions(+), 7 deletions(-) diff --git a/docs/html/development/architecture/package-finding.rst b/docs/html/development/architecture/package-finding.rst index 1f17cb2c80e..11ad9cbc1a2 100644 --- a/docs/html/development/architecture/package-finding.rst +++ b/docs/html/development/architecture/package-finding.rst @@ -24,6 +24,8 @@ file to download for a package, given a requirement: is an HTML page of anchor links. 2. Collect together all of the links (e.g. by parsing the anchor links from the HTML pages) and create ``Link`` objects from each of these. + The :ref:`LinkCollector ` class is responsible + for both this step and the previous. 3. Determine which of the links are minimally relevant, using the :ref:`LinkEvaluator ` class. Create an ``InstallationCandidate`` object (aka candidate for install) for each @@ -39,6 +41,7 @@ The remainder of this section is organized by documenting some of the classes inside ``index.py``, in the following order: * the main :ref:`PackageFinder ` class, +* the :ref:`LinkCollector ` class, * the :ref:`LinkEvaluator ` class, * the :ref:`CandidateEvaluator ` class, * the :ref:`CandidatePreferences ` class, and @@ -95,18 +98,47 @@ links. One of ``PackageFinder``'s main top-level methods is ``find_best_candidate()``. This method does the following two things: -1. Calls its ``find_all_candidates()`` method, which reads and parses all the - index URL's provided by the user, constructs a :ref:`LinkEvaluator - ` object to filter out some of those links, and then - returns a list of ``InstallationCandidates`` (aka candidates for install). - This corresponds to steps 1-3 of the :ref:`Overview ` - above. +1. Calls its ``find_all_candidates()`` method, which gathers all + possible package links by reading and parsing the index URL's and + locations provided by the user (the :ref:`LinkCollector + ` class's ``collect_links()`` method), constructs a + :ref:`LinkEvaluator ` object to filter out some of + those links, and then returns a list of ``InstallationCandidates`` (aka + candidates for install). This corresponds to steps 1-3 of the + :ref:`Overview ` above. 2. Constructs a ``CandidateEvaluator`` object and uses that to determine the best candidate. It does this by calling the ``CandidateEvaluator`` class's ``compute_best_candidate()`` method on the return value of ``find_all_candidates()``. This corresponds to steps 4-5 of the Overview. +.. _link-collector-class: + +The ``LinkCollector`` class +*************************** + +The :ref:`LinkCollector ` class is the class +responsible for collecting the raw list of "links" to package files +(represented as ``Link`` objects). An instance of the class accesses the +various `PEP 503`_ HTML "simple repository" pages, parses their HTML, +extracts the links from the anchor elements, and creates ``Link`` objects +from that information. The ``LinkCollector`` class is "unintelligent" in that +it doesn't do any evaluation of whether the links are relevant to the +original requirement; it just collects them. + +The ``LinkCollector`` class takes into account the user's :ref:`--find-links +<--find-links>`, :ref:`--extra-index-url <--extra-index-url>`, and related +options when deciding which locations to collect links from. The class's main +method is the ``collect_links()`` method. The :ref:`PackageFinder +` class invokes this method as the first step of its +``find_all_candidates()`` method. + +The ``LinkCollector`` class is the only class in the ``index.py`` module that +makes network requests and is the only class in the module that depends +directly on ``PipSession``, which stores pip's configuration options and +state for making requests. + + .. _link-evaluator-class: The ``LinkEvaluator`` class @@ -191,7 +223,8 @@ The ``BestCandidateResult`` class The ``BestCandidateResult`` class is a convenience "container" class that encapsulates the result of finding the best candidate for a requirement. (By "container" we mean an object that simply contains data and has no -business logic or state-changing methods of its own.) +business logic or state-changing methods of its own.) It stores not just the +final result but also intermediate values used to determine the result. The class is the return type of both the ``CandidateEvaluator`` class's ``compute_best_candidate()`` method and the ``PackageFinder`` class's From 3303c1182874f8853c436709cb8bff383511185e Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Tue, 10 Sep 2019 03:13:16 -0700 Subject: [PATCH 0265/3170] Remove index.py and req/constructors.py's dependence on download.py. --- src/pip/_internal/download.py | 60 ++----------------------- src/pip/_internal/index.py | 4 +- src/pip/_internal/operations/prepare.py | 8 +--- src/pip/_internal/req/constructors.py | 20 +++++++-- src/pip/_internal/utils/urls.py | 42 +++++++++++++++++ src/pip/_internal/vcs/__init__.py | 2 +- src/pip/_internal/vcs/versioncontrol.py | 12 +++++ tests/unit/test_download.py | 44 ------------------ tests/unit/test_urls.py | 48 ++++++++++++++++++++ 9 files changed, 128 insertions(+), 112 deletions(-) create mode 100644 src/pip/_internal/utils/urls.py create mode 100644 tests/unit/test_urls.py diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index 15d6fd56f08..7190702cb9c 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -26,7 +26,6 @@ # 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 import pip from pip._internal.exceptions import HashMismatch, InstallationError @@ -37,7 +36,6 @@ from pip._internal.utils.filesystem import check_path_owner, copy2_fixed from pip._internal.utils.glibc import libc_ver from pip._internal.utils.misc import ( - ARCHIVE_EXTENSIONS, ask, ask_input, ask_password, @@ -61,6 +59,7 @@ from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.ui import DownloadProgressProvider +from pip._internal.utils.urls import get_url_scheme, url_to_path from pip._internal.vcs import vcs if MYPY_CHECK_RUNNING: @@ -101,8 +100,8 @@ __all__ = ['get_file_content', - 'is_url', 'url_to_path', 'path_to_url', - 'is_archive_file', 'unpack_vcs_link', + 'path_to_url', + 'unpack_vcs_link', 'unpack_file_url', 'is_file_url', 'unpack_http_url', 'unpack_url', 'parse_content_disposition', 'sanitize_content_filename'] @@ -787,7 +786,7 @@ def get_file_content(url, comes_from=None, session=None): "get_file_content() missing 1 required keyword argument: 'session'" ) - scheme = _get_url_scheme(url) + scheme = get_url_scheme(url) if scheme in ['http', 'https']: # FIXME: catch some errors @@ -824,57 +823,6 @@ def get_file_content(url, comes_from=None, session=None): _url_slash_drive_re = re.compile(r'/*([a-z])\|', re.I) -def _get_url_scheme(url): - # type: (Union[str, Text]) -> Optional[Text] - if ':' not in url: - return None - return url.split(':', 1)[0].lower() - - -def is_url(name): - # type: (Union[str, Text]) -> bool - """Returns true if the name looks like a URL""" - scheme = _get_url_scheme(name) - if scheme is None: - return False - return scheme in ['http', 'https', 'file', 'ftp'] + vcs.all_schemes - - -def url_to_path(url): - # type: (str) -> str - """ - Convert a file: URL to a path. - """ - assert url.startswith('file:'), ( - "You can only turn file: urls into filenames (not %r)" % url) - - _, netloc, path, _, _ = urllib_parse.urlsplit(url) - - if not netloc or netloc == 'localhost': - # According to RFC 8089, same as empty authority. - netloc = '' - elif sys.platform == 'win32': - # If we have a UNC path, prepend UNC share notation. - netloc = '\\\\' + netloc - else: - raise ValueError( - 'non-local file URIs are not supported on this platform: %r' - % url - ) - - path = urllib_request.url2pathname(netloc + path) - return path - - -def is_archive_file(name): - # type: (str) -> bool - """Return True if `name` is a considered as an archive file.""" - ext = splitext(name)[1].lower() - if ext in ARCHIVE_EXTENSIONS: - return True - return False - - def unpack_vcs_link(link, location): # type: (Link, str) -> None vcs_backend = _get_used_vcs_backend(link) diff --git a/src/pip/_internal/index.py b/src/pip/_internal/index.py index ed08e0272c2..dc9904eee8e 100644 --- a/src/pip/_internal/index.py +++ b/src/pip/_internal/index.py @@ -21,7 +21,6 @@ from pip._vendor.six.moves.urllib import parse as urllib_parse from pip._vendor.six.moves.urllib import request as urllib_request -from pip._internal.download import is_url, url_to_path from pip._internal.exceptions import ( BestVersionAlreadyInstalled, DistributionNotFound, @@ -44,6 +43,8 @@ ) from pip._internal.utils.packaging import check_requires_python from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from pip._internal.utils.urls import url_to_path +from pip._internal.vcs import is_url, vcs from pip._internal.wheel import Wheel if MYPY_CHECK_RUNNING: @@ -79,7 +80,6 @@ def _match_vcs_scheme(url): Returns the matched VCS scheme, or None if there's no match. """ - from pip._internal.vcs import vcs for scheme in vcs.schemes: if url.lower().startswith(scheme) and url[len(scheme)] in '+:': return scheme diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 20e0301bed7..9bcab9b9935 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -13,12 +13,7 @@ make_distribution_for_install_requirement, ) from pip._internal.distributions.installed import InstalledDistribution -from pip._internal.download import ( - is_dir_url, - is_file_url, - unpack_url, - url_to_path, -) +from pip._internal.download import is_dir_url, is_file_url, unpack_url from pip._internal.exceptions import ( DirectoryUrlHashUnsupported, HashUnpinned, @@ -32,6 +27,7 @@ from pip._internal.utils.marker_files import write_delete_marker_file from pip._internal.utils.misc import display_path, normalize_path from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from pip._internal.utils.urls import url_to_path if MYPY_CHECK_RUNNING: from typing import Optional diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index 28cef933221..0d910dbc494 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -20,15 +20,20 @@ from pip._vendor.packaging.specifiers import Specifier from pip._vendor.pkg_resources import RequirementParseError, parse_requirements -from pip._internal.download import is_archive_file, is_url, url_to_path from pip._internal.exceptions import InstallationError from pip._internal.models.index import PyPI, TestPyPI from pip._internal.models.link import Link from pip._internal.pyproject import make_pyproject_path from pip._internal.req.req_install import InstallRequirement -from pip._internal.utils.misc import is_installable_dir, path_to_url +from pip._internal.utils.misc import ( + ARCHIVE_EXTENSIONS, + is_installable_dir, + path_to_url, + splitext, +) from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from pip._internal.vcs import vcs +from pip._internal.utils.urls import url_to_path +from pip._internal.vcs import is_url, vcs from pip._internal.wheel import Wheel if MYPY_CHECK_RUNNING: @@ -47,6 +52,15 @@ operators = Specifier._operators.keys() +def is_archive_file(name): + # type: (str) -> bool + """Return True if `name` is a considered as an archive file.""" + ext = splitext(name)[1].lower() + if ext in ARCHIVE_EXTENSIONS: + return True + return False + + def _strip_extras(path): # type: (str) -> Tuple[str, Optional[str]] m = re.match(r'^(.+)(\[[^\]]+\])$', path) diff --git a/src/pip/_internal/utils/urls.py b/src/pip/_internal/utils/urls.py new file mode 100644 index 00000000000..9c5385044c7 --- /dev/null +++ b/src/pip/_internal/utils/urls.py @@ -0,0 +1,42 @@ +import sys + +from pip._vendor.six.moves.urllib import parse as urllib_parse +from pip._vendor.six.moves.urllib import request as urllib_request + +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import Optional, Text, Union + + +def get_url_scheme(url): + # type: (Union[str, Text]) -> Optional[Text] + if ':' not in url: + return None + return url.split(':', 1)[0].lower() + + +def url_to_path(url): + # type: (str) -> str + """ + Convert a file: URL to a path. + """ + assert url.startswith('file:'), ( + "You can only turn file: urls into filenames (not %r)" % url) + + _, netloc, path, _, _ = urllib_parse.urlsplit(url) + + if not netloc or netloc == 'localhost': + # According to RFC 8089, same as empty authority. + netloc = '' + elif sys.platform == 'win32': + # If we have a UNC path, prepend UNC share notation. + netloc = '\\\\' + netloc + else: + raise ValueError( + 'non-local file URIs are not supported on this platform: %r' + % url + ) + + path = urllib_request.url2pathname(netloc + path) + return path diff --git a/src/pip/_internal/vcs/__init__.py b/src/pip/_internal/vcs/__init__.py index cb573ab6dc2..75b5589c53d 100644 --- a/src/pip/_internal/vcs/__init__.py +++ b/src/pip/_internal/vcs/__init__.py @@ -3,7 +3,7 @@ # (The test directory and imports protected by MYPY_CHECK_RUNNING may # still need to import from a vcs sub-package.) from pip._internal.vcs.versioncontrol import ( # noqa: F401 - RemoteNotFoundError, make_vcs_requirement_url, vcs, + RemoteNotFoundError, is_url, make_vcs_requirement_url, vcs, ) # Import all vcs modules to register each VCS in the VcsSupport object. import pip._internal.vcs.bazaar diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index 40740e97867..27610602f16 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -22,6 +22,7 @@ rmtree, ) from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from pip._internal.utils.urls import get_url_scheme if MYPY_CHECK_RUNNING: from typing import ( @@ -39,6 +40,17 @@ logger = logging.getLogger(__name__) +def is_url(name): + # type: (Union[str, Text]) -> bool + """ + Return true if the name looks like a URL. + """ + scheme = get_url_scheme(name) + if scheme is None: + return False + return scheme in ['http', 'https', 'file', 'ftp'] + vcs.all_schemes + + def make_vcs_requirement_url(repo_url, rev, project_name, subdir=None): """ Return the URL for a VCS requirement. diff --git a/tests/unit/test_download.py b/tests/unit/test_download.py index 9c0ccc2cf30..07588d1f4bd 100644 --- a/tests/unit/test_download.py +++ b/tests/unit/test_download.py @@ -19,12 +19,10 @@ SafeFileCache, _copy_source_tree, _download_http_url, - _get_url_scheme, parse_content_disposition, sanitize_content_filename, unpack_file_url, unpack_http_url, - url_to_path, ) from pip._internal.exceptions import HashMismatch from pip._internal.models.link import Link @@ -301,48 +299,6 @@ def test_download_http_url__no_directory_traversal(tmpdir): assert actual == ['out_dir_file'] -@pytest.mark.parametrize("url,expected", [ - ('http://localhost:8080/', 'http'), - ('file:c:/path/to/file', 'file'), - ('file:/dev/null', 'file'), - ('', None), -]) -def test__get_url_scheme(url, expected): - assert _get_url_scheme(url) == expected - - -@pytest.mark.parametrize("url,win_expected,non_win_expected", [ - ('file:tmp', 'tmp', 'tmp'), - ('file:c:/path/to/file', r'C:\path\to\file', 'c:/path/to/file'), - ('file:/path/to/file', r'\path\to\file', '/path/to/file'), - ('file://localhost/tmp/file', r'\tmp\file', '/tmp/file'), - ('file://localhost/c:/tmp/file', r'C:\tmp\file', '/c:/tmp/file'), - ('file://somehost/tmp/file', r'\\somehost\tmp\file', None), - ('file:///tmp/file', r'\tmp\file', '/tmp/file'), - ('file:///c:/tmp/file', r'C:\tmp\file', '/c:/tmp/file'), -]) -def test_url_to_path(url, win_expected, non_win_expected): - if sys.platform == 'win32': - expected_path = win_expected - else: - expected_path = non_win_expected - - if expected_path is None: - with pytest.raises(ValueError): - url_to_path(url) - else: - assert url_to_path(url) == expected_path - - -@pytest.mark.skipif("sys.platform != 'win32'") -def test_url_to_path_path_to_url_symmetry_win(): - path = r'C:\tmp\file' - assert url_to_path(path_to_url(path)) == path - - unc_path = r'\\unc\share\path' - assert url_to_path(path_to_url(unc_path)) == unc_path - - @pytest.fixture def clean_project(tmpdir_factory, data): tmpdir = Path(str(tmpdir_factory.mktemp("clean_project"))) diff --git a/tests/unit/test_urls.py b/tests/unit/test_urls.py new file mode 100644 index 00000000000..68d544072f8 --- /dev/null +++ b/tests/unit/test_urls.py @@ -0,0 +1,48 @@ +import sys + +import pytest + +from pip._internal.utils.misc import path_to_url +from pip._internal.utils.urls import get_url_scheme, url_to_path + + +@pytest.mark.parametrize("url,expected", [ + ('http://localhost:8080/', 'http'), + ('file:c:/path/to/file', 'file'), + ('file:/dev/null', 'file'), + ('', None), +]) +def test_get_url_scheme(url, expected): + assert get_url_scheme(url) == expected + + +@pytest.mark.parametrize("url,win_expected,non_win_expected", [ + ('file:tmp', 'tmp', 'tmp'), + ('file:c:/path/to/file', r'C:\path\to\file', 'c:/path/to/file'), + ('file:/path/to/file', r'\path\to\file', '/path/to/file'), + ('file://localhost/tmp/file', r'\tmp\file', '/tmp/file'), + ('file://localhost/c:/tmp/file', r'C:\tmp\file', '/c:/tmp/file'), + ('file://somehost/tmp/file', r'\\somehost\tmp\file', None), + ('file:///tmp/file', r'\tmp\file', '/tmp/file'), + ('file:///c:/tmp/file', r'C:\tmp\file', '/c:/tmp/file'), +]) +def test_url_to_path(url, win_expected, non_win_expected): + if sys.platform == 'win32': + expected_path = win_expected + else: + expected_path = non_win_expected + + if expected_path is None: + with pytest.raises(ValueError): + url_to_path(url) + else: + assert url_to_path(url) == expected_path + + +@pytest.mark.skipif("sys.platform != 'win32'") +def test_url_to_path_path_to_url_symmetry_win(): + path = r'C:\tmp\file' + assert url_to_path(path_to_url(path)) == path + + unc_path = r'\\unc\share\path' + assert url_to_path(path_to_url(unc_path)) == unc_path From 9a765b8cab1cc4e2690081ac941ca3248a34b9e6 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 13 Sep 2019 09:18:07 -0700 Subject: [PATCH 0266/3170] Change local_checkout() to accept tmpdir. --- tests/functional/test_install.py | 9 +++---- tests/functional/test_install_cleanup.py | 5 +--- tests/functional/test_install_reqs.py | 3 +-- tests/functional/test_install_upgrade.py | 3 +-- tests/functional/test_install_user.py | 3 +-- tests/functional/test_install_vcs_git.py | 2 +- tests/functional/test_uninstall.py | 32 ++++++++---------------- tests/lib/local_repos.py | 20 ++++++++++++--- 8 files changed, 36 insertions(+), 41 deletions(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 036a087aff7..7a0e4a9cbee 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -299,8 +299,7 @@ def test_install_editable_uninstalls_existing(data, script, tmpdir): 'install', '-e', '%s#egg=pip-test-package' % local_checkout( - 'git+https://github.com/pypa/pip-test-package.git', - tmpdir.joinpath("cache"), + 'git+https://github.com/pypa/pip-test-package.git', tmpdir, ), ) result.assert_installed('pip-test-package', with_files=['.git']) @@ -374,7 +373,7 @@ def test_vcs_url_urlquote_normalization(script, tmpdir): local_checkout( 'bzr+http://bazaar.launchpad.net/%7Edjango-wikiapp/django-wikiapp' '/release-0.1', - tmpdir.joinpath("cache"), + tmpdir, ), ) @@ -652,7 +651,7 @@ def test_install_using_install_option_and_editable(script, tmpdir): url = 'git+git://github.com/pypa/pip-test-package' result = script.pip( 'install', '-e', '%s#egg=pip-test-package' % - local_checkout(url, tmpdir.joinpath("cache")), + local_checkout(url, tmpdir), '--install-option=--script-dir=%s' % folder, expect_stderr=True) script_file = ( @@ -671,7 +670,7 @@ def test_install_global_option_using_editable(script, tmpdir): url = 'hg+http://bitbucket.org/runeh/anyjson' result = script.pip( 'install', '--global-option=--version', '-e', - '%s@0.2.5#egg=anyjson' % local_checkout(url, tmpdir.joinpath("cache")), + '%s@0.2.5#egg=anyjson' % local_checkout(url, tmpdir), expect_stderr=True) assert 'Successfully installed anyjson' in result.stdout diff --git a/tests/functional/test_install_cleanup.py b/tests/functional/test_install_cleanup.py index 8489b860b3a..dc87bc3f76a 100644 --- a/tests/functional/test_install_cleanup.py +++ b/tests/functional/test_install_cleanup.py @@ -47,10 +47,7 @@ def test_cleanup_after_install_editable_from_hg(script, tmpdir): 'install', '-e', '%s#egg=ScriptTest' % - local_checkout( - 'hg+https://bitbucket.org/ianb/scripttest', - tmpdir.joinpath("cache"), - ), + local_checkout('hg+https://bitbucket.org/ianb/scripttest', tmpdir), ) build = script.venv_path / 'build' src = script.venv_path / 'src' diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index 63f163ca97a..b906c37b642 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -111,8 +111,7 @@ def test_multiple_requirements_files(script, tmpdir): """) % ( local_checkout( - 'svn+http://svn.colorstudy.com/INITools/trunk', - tmpdir.joinpath("cache"), + 'svn+http://svn.colorstudy.com/INITools/trunk', tmpdir, ), other_lib_name ), diff --git a/tests/functional/test_install_upgrade.py b/tests/functional/test_install_upgrade.py index 91f81e168df..36b518b2546 100644 --- a/tests/functional/test_install_upgrade.py +++ b/tests/functional/test_install_upgrade.py @@ -349,8 +349,7 @@ def test_install_with_ignoreinstalled_requested(script): def test_upgrade_vcs_req_with_no_dists_found(script, tmpdir): """It can upgrade a VCS requirement that has no distributions otherwise.""" req = "%s#egg=pip-test-package" % local_checkout( - "git+https://github.com/pypa/pip-test-package.git", - tmpdir.joinpath("cache"), + "git+https://github.com/pypa/pip-test-package.git", tmpdir, ) script.pip("install", req) result = script.pip("install", "-U", req) diff --git a/tests/functional/test_install_user.py b/tests/functional/test_install_user.py index b07c1c9cbbb..d668ed12cb0 100644 --- a/tests/functional/test_install_user.py +++ b/tests/functional/test_install_user.py @@ -52,8 +52,7 @@ def test_install_subversion_usersite_editable_with_distribute( 'install', '--user', '-e', '%s#egg=initools' % local_checkout( - 'svn+http://svn.colorstudy.com/INITools/trunk', - tmpdir.joinpath("cache"), + 'svn+http://svn.colorstudy.com/INITools/trunk', tmpdir, ) ) result.assert_installed('INITools', use_user_site=True) diff --git a/tests/functional/test_install_vcs_git.py b/tests/functional/test_install_vcs_git.py index 11b63a84488..feb9ef0401e 100644 --- a/tests/functional/test_install_vcs_git.py +++ b/tests/functional/test_install_vcs_git.py @@ -58,7 +58,7 @@ def _github_checkout(url_path, temp_dir, rev=None, egg=None, scheme=None): if scheme is None: scheme = 'https' url = 'git+{}://github.com/{}'.format(scheme, url_path) - local_url = local_checkout(url, temp_dir.joinpath('cache')) + local_url = local_checkout(url, temp_dir) if rev is not None: local_url += '@{}'.format(rev) if egg is not None: diff --git a/tests/functional/test_uninstall.py b/tests/functional/test_uninstall.py index 128d66102c0..8814c87c681 100644 --- a/tests/functional/test_uninstall.py +++ b/tests/functional/test_uninstall.py @@ -296,8 +296,7 @@ def test_uninstall_editable_from_svn(script, tmpdir): result = script.pip( 'install', '-e', '%s#egg=initools' % local_checkout( - 'svn+http://svn.colorstudy.com/INITools/trunk', - tmpdir.joinpath("cache"), + 'svn+http://svn.colorstudy.com/INITools/trunk', tmpdir, ), ) result.assert_installed('INITools') @@ -318,34 +317,29 @@ def test_uninstall_editable_from_svn(script, tmpdir): def test_uninstall_editable_with_source_outside_venv(script, tmpdir): """ Test uninstalling editable install from existing source outside the venv. - """ - cache_dir = tmpdir.joinpath("cache") - try: temp = mkdtemp() - tmpdir = join(temp, 'pip-test-package') + temp_pkg_dir = join(temp, 'pip-test-package') _test_uninstall_editable_with_source_outside_venv( script, tmpdir, - cache_dir, + temp_pkg_dir, ) finally: rmtree(temp) def _test_uninstall_editable_with_source_outside_venv( - script, tmpdir, cache_dir): + script, tmpdir, temp_pkg_dir, +): result = script.run( 'git', 'clone', - local_repo( - 'git+git://github.com/pypa/pip-test-package', - cache_dir, - ), - tmpdir, + local_repo('git+git://github.com/pypa/pip-test-package', tmpdir), + temp_pkg_dir, expect_stderr=True, ) - result2 = script.pip('install', '-e', tmpdir) + result2 = script.pip('install', '-e', temp_pkg_dir) assert join( script.site_packages, 'pip-test-package.egg-link' ) in result2.files_created, list(result2.files_created.keys()) @@ -370,10 +364,7 @@ def test_uninstall_from_reqs_file(script, tmpdir): # and something else to test out: PyLogo<0.4 """) % - local_checkout( - 'svn+http://svn.colorstudy.com/INITools/trunk', - tmpdir.joinpath("cache") - ) + local_checkout('svn+http://svn.colorstudy.com/INITools/trunk', tmpdir) ) result = script.pip('install', '-r', 'test-req.txt') script.scratch_path.joinpath("test-req.txt").write_text( @@ -387,10 +378,7 @@ def test_uninstall_from_reqs_file(script, tmpdir): # and something else to test out: PyLogo<0.4 """) % - local_checkout( - 'svn+http://svn.colorstudy.com/INITools/trunk', - tmpdir.joinpath("cache") - ) + local_checkout('svn+http://svn.colorstudy.com/INITools/trunk', tmpdir) ) result2 = script.pip('uninstall', '-r', 'test-req.txt', '-y') assert_all_changes( diff --git a/tests/lib/local_repos.py b/tests/lib/local_repos.py index 69c60adb3fb..40cb19b678a 100644 --- a/tests/lib/local_repos.py +++ b/tests/lib/local_repos.py @@ -6,9 +6,13 @@ from pip._vendor.six.moves.urllib import request as urllib_request from pip._internal.utils.misc import hide_url +from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.vcs import bazaar, git, mercurial, subversion from tests.lib import path_to_url +if MYPY_CHECK_RUNNING: + from tests.lib.path import Path + def _create_initools_repository(directory): subprocess.check_call('svnadmin create INITools'.split(), cwd=directory) @@ -68,7 +72,17 @@ def _get_vcs_and_checkout_url(remote_repository, directory): ) -def local_checkout(remote_repo, directory): +def local_checkout( + remote_repo, # type: str + temp_path, # type: Path +): + # type: (...) -> str + """ + :param temp_path: the return value of the tmpdir fixture, which is a + temp directory Path object unique to each test function invocation, + created as a sub directory of the base temp directory. + """ + directory = temp_path.joinpath('cache') if not os.path.exists(directory): os.mkdir(directory) # os.makedirs(directory) @@ -78,5 +92,5 @@ def local_checkout(remote_repo, directory): return _get_vcs_and_checkout_url(remote_repo, directory) -def local_repo(remote_repo, directory): - return local_checkout(remote_repo, directory).split('+', 1)[1] +def local_repo(remote_repo, temp_path): + return local_checkout(remote_repo, temp_path).split('+', 1)[1] From 1a42641762e1c06296aa27c3bcc19bbc903815d8 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 13 Sep 2019 23:47:58 -0700 Subject: [PATCH 0267/3170] Remove unused project_name argument from LinkCollector._get_pages(). --- src/pip/_internal/index.py | 6 +++--- tests/unit/test_finder.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/index.py b/src/pip/_internal/index.py index ed08e0272c2..7952b425149 100644 --- a/src/pip/_internal/index.py +++ b/src/pip/_internal/index.py @@ -363,8 +363,8 @@ def find_links(self): # type: () -> List[str] return self.search_scope.find_links - def _get_pages(self, locations, project_name): - # type: (Iterable[Link], str) -> Iterable[HTMLPage] + def _get_pages(self, locations): + # type: (Iterable[Link]) -> Iterable[HTMLPage] """ Yields (page, page_url) from the given locations, skipping locations that have errors. @@ -419,7 +419,7 @@ def collect_links(self, project_name): logger.debug('* %s', location) pages_links = {} - for page in self._get_pages(url_locations, project_name): + for page in self._get_pages(url_locations): pages_links[page.url] = list(page.iter_links()) return CollectedLinks( diff --git a/tests/unit/test_finder.py b/tests/unit/test_finder.py index fd68fcf8e27..13573447c27 100644 --- a/tests/unit/test_finder.py +++ b/tests/unit/test_finder.py @@ -37,7 +37,7 @@ def make_no_network_finder( ) # Replace the PackageFinder._link_collector's _get_pages() with a no-op. link_collector = finder._link_collector - link_collector._get_pages = lambda locations, project_name: [] + link_collector._get_pages = lambda locations: [] return finder From fe2509bd58b599ddbf28b98d19d775f64b72959c Mon Sep 17 00:00:00 2001 From: Chris Jerdonek Date: Fri, 13 Sep 2019 17:51:02 -0700 Subject: [PATCH 0268/3170] Move LinkCollector to a new collector.py module. --- src/pip/_internal/collector.py | 516 +++++++++++++++++++++++++++++ src/pip/_internal/index.py | 494 +-------------------------- src/pip/_internal/models/link.py | 2 +- tests/unit/test_collector.py | 452 +++++++++++++++++++++++++ tests/unit/test_finder.py | 2 +- tests/unit/test_index.py | 259 +-------------- tests/unit/test_index_html_page.py | 194 ----------- tests/unit/test_wheel.py | 2 + 8 files changed, 975 insertions(+), 946 deletions(-) create mode 100644 src/pip/_internal/collector.py create mode 100644 tests/unit/test_collector.py delete mode 100644 tests/unit/test_index_html_page.py diff --git a/src/pip/_internal/collector.py b/src/pip/_internal/collector.py new file mode 100644 index 00000000000..4f7a8d30989 --- /dev/null +++ b/src/pip/_internal/collector.py @@ -0,0 +1,516 @@ +""" +The main purpose of this module is to expose LinkCollector.collect_links(). +""" + +import cgi +import itertools +import logging +import mimetypes +import os + +from pip._vendor import html5lib, requests +from pip._vendor.distlib.compat import unescape +from pip._vendor.requests.exceptions import HTTPError, RetryError, SSLError +from pip._vendor.six.moves.urllib import parse as urllib_parse +from pip._vendor.six.moves.urllib import request as urllib_request + +from pip._internal.models.link import Link +from pip._internal.utils.misc import ( + ARCHIVE_EXTENSIONS, + path_to_url, + redact_auth_from_url, +) +from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from pip._internal.utils.urls import url_to_path +from pip._internal.vcs import is_url, vcs + +if MYPY_CHECK_RUNNING: + from typing import ( + Callable, Dict, Iterable, List, MutableMapping, Optional, Sequence, + Set, Tuple, Union, + ) + import xml.etree.ElementTree + + from pip._vendor.requests import Response + + from pip._internal.models.search_scope import SearchScope + from pip._internal.download import PipSession + + HTMLElement = xml.etree.ElementTree.Element + + +logger = logging.getLogger(__name__) + + +def _match_vcs_scheme(url): + # type: (str) -> Optional[str] + """Look for VCS schemes in the URL. + + Returns the matched VCS scheme, or None if there's no match. + """ + for scheme in vcs.schemes: + if url.lower().startswith(scheme) and url[len(scheme)] in '+:': + return scheme + return None + + +def _is_url_like_archive(url): + # type: (str) -> bool + """Return whether the URL looks like an archive. + """ + filename = Link(url).filename + for bad_ext in ARCHIVE_EXTENSIONS: + if filename.endswith(bad_ext): + return True + return False + + +class _NotHTML(Exception): + def __init__(self, content_type, request_desc): + # type: (str, str) -> None + super(_NotHTML, self).__init__(content_type, request_desc) + self.content_type = content_type + self.request_desc = request_desc + + +def _ensure_html_header(response): + # type: (Response) -> None + """Check the Content-Type header to ensure the response contains HTML. + + Raises `_NotHTML` if the content type is not text/html. + """ + content_type = response.headers.get("Content-Type", "") + if not content_type.lower().startswith("text/html"): + raise _NotHTML(content_type, response.request.method) + + +class _NotHTTP(Exception): + pass + + +def _ensure_html_response(url, session): + # type: (str, PipSession) -> None + """Send a HEAD request to the URL, and ensure the response contains HTML. + + Raises `_NotHTTP` if the URL is not available for a HEAD request, or + `_NotHTML` if the content type is not text/html. + """ + scheme, netloc, path, query, fragment = urllib_parse.urlsplit(url) + if scheme not in {'http', 'https'}: + raise _NotHTTP() + + resp = session.head(url, allow_redirects=True) + resp.raise_for_status() + + _ensure_html_header(resp) + + +def _get_html_response(url, session): + # type: (str, PipSession) -> Response + """Access an HTML page with GET, and return the response. + + This consists of three parts: + + 1. If the URL looks suspiciously like an archive, send a HEAD first to + check the Content-Type is HTML, to avoid downloading a large file. + Raise `_NotHTTP` if the content type cannot be determined, or + `_NotHTML` if it is not HTML. + 2. Actually perform the request. Raise HTTP exceptions on network failures. + 3. Check the Content-Type header to make sure we got HTML, and raise + `_NotHTML` otherwise. + """ + if _is_url_like_archive(url): + _ensure_html_response(url, session=session) + + logger.debug('Getting page %s', redact_auth_from_url(url)) + + resp = session.get( + url, + headers={ + "Accept": "text/html", + # We don't want to blindly returned cached data for + # /simple/, because authors generally expecting that + # twine upload && pip install will function, but if + # they've done a pip install in the last ~10 minutes + # it won't. Thus by setting this to zero we will not + # blindly use any cached data, however the benefit of + # using max-age=0 instead of no-cache, is that we will + # still support conditional requests, so we will still + # minimize traffic sent in cases where the page hasn't + # changed at all, we will just always incur the round + # trip for the conditional GET now instead of only + # once per 10 minutes. + # For more information, please see pypa/pip#5670. + "Cache-Control": "max-age=0", + }, + ) + resp.raise_for_status() + + # The check for archives above only works if the url ends with + # something that looks like an archive. However that is not a + # requirement of an url. Unless we issue a HEAD request on every + # url we cannot know ahead of time for sure if something is HTML + # or not. However we can check after we've downloaded it. + _ensure_html_header(resp) + + return resp + + +def _get_encoding_from_headers(headers): + """Determine if we have any encoding information in our headers. + """ + if headers and "Content-Type" in headers: + content_type, params = cgi.parse_header(headers["Content-Type"]) + if "charset" in params: + return params['charset'] + return None + + +def _determine_base_url(document, page_url): + """Determine the HTML document's base URL. + + This looks for a ```` tag in the HTML document. If present, its href + attribute denotes the base URL of anchor tags in the document. If there is + no such tag (or if it does not have a valid href attribute), the HTML + file's URL is used as the base URL. + + :param document: An HTML document representation. The current + implementation expects the result of ``html5lib.parse()``. + :param page_url: The URL of the HTML document. + """ + for base in document.findall(".//base"): + href = base.get("href") + if href is not None: + return href + return page_url + + +def _clean_link(url): + # type: (str) -> str + """Makes sure a link is fully encoded. That is, if a ' ' shows up in + the link, it will be rewritten to %20 (while not over-quoting + % or other characters).""" + # Split the URL into parts according to the general structure + # `scheme://netloc/path;parameters?query#fragment`. Note that the + # `netloc` can be empty and the URI will then refer to a local + # filesystem path. + result = urllib_parse.urlparse(url) + # In both cases below we unquote prior to quoting to make sure + # nothing is double quoted. + if result.netloc == "": + # On Windows the path part might contain a drive letter which + # should not be quoted. On Linux where drive letters do not + # exist, the colon should be quoted. We rely on urllib.request + # to do the right thing here. + path = urllib_request.pathname2url( + urllib_request.url2pathname(result.path)) + else: + # In addition to the `/` character we protect `@` so that + # revision strings in VCS URLs are properly parsed. + path = urllib_parse.quote(urllib_parse.unquote(result.path), safe="/@") + return urllib_parse.urlunparse(result._replace(path=path)) + + +def _create_link_from_element( + anchor, # type: HTMLElement + page_url, # type: str + base_url, # type: str +): + # type: (...) -> Optional[Link] + """ + Convert an anchor element in a simple repository page to a Link. + """ + href = anchor.get("href") + if not href: + return None + + url = _clean_link(urllib_parse.urljoin(base_url, href)) + pyrequire = anchor.get('data-requires-python') + pyrequire = unescape(pyrequire) if pyrequire else None + + yanked_reason = anchor.get('data-yanked') + if yanked_reason: + # This is a unicode string in Python 2 (and 3). + yanked_reason = unescape(yanked_reason) + + link = Link( + url, + comes_from=page_url, + requires_python=pyrequire, + yanked_reason=yanked_reason, + ) + + return link + + +class HTMLPage(object): + """Represents one page, along with its URL""" + + def __init__(self, content, url, headers=None): + # type: (bytes, str, MutableMapping[str, str]) -> None + self.content = content + self.url = url + self.headers = headers + + def __str__(self): + return redact_auth_from_url(self.url) + + def iter_links(self): + # type: () -> Iterable[Link] + """Yields all links in the page""" + document = html5lib.parse( + self.content, + transport_encoding=_get_encoding_from_headers(self.headers), + namespaceHTMLElements=False, + ) + base_url = _determine_base_url(document, self.url) + for anchor in document.findall(".//a"): + link = _create_link_from_element( + anchor, + page_url=self.url, + base_url=base_url, + ) + if link is None: + continue + yield link + + +def _handle_get_page_fail( + link, # type: Link + reason, # type: Union[str, Exception] + meth=None # type: Optional[Callable[..., None]] +): + # type: (...) -> None + if meth is None: + meth = logger.debug + meth("Could not fetch URL %s: %s - skipping", link, reason) + + +def _get_html_page(link, session=None): + # type: (Link, Optional[PipSession]) -> Optional[HTMLPage] + if session is None: + raise TypeError( + "_get_html_page() missing 1 required keyword argument: 'session'" + ) + + url = link.url.split('#', 1)[0] + + # Check for VCS schemes that do not support lookup as web pages. + vcs_scheme = _match_vcs_scheme(url) + if vcs_scheme: + logger.debug('Cannot look at %s URL %s', vcs_scheme, link) + return None + + # Tack index.html onto file:// URLs that point to directories + scheme, _, path, _, _, _ = urllib_parse.urlparse(url) + if (scheme == 'file' and os.path.isdir(urllib_request.url2pathname(path))): + # add trailing slash if not present so urljoin doesn't trim + # final segment + if not url.endswith('/'): + url += '/' + url = urllib_parse.urljoin(url, 'index.html') + logger.debug(' file: URL is directory, getting %s', url) + + try: + resp = _get_html_response(url, session=session) + except _NotHTTP: + logger.debug( + 'Skipping page %s because it looks like an archive, and cannot ' + 'be checked by HEAD.', link, + ) + except _NotHTML as exc: + logger.debug( + 'Skipping page %s because the %s request got Content-Type: %s', + link, exc.request_desc, exc.content_type, + ) + except HTTPError as exc: + _handle_get_page_fail(link, exc) + except RetryError as exc: + _handle_get_page_fail(link, exc) + except SSLError as exc: + reason = "There was a problem confirming the ssl certificate: " + reason += str(exc) + _handle_get_page_fail(link, reason, meth=logger.info) + except requests.ConnectionError as exc: + _handle_get_page_fail(link, "connection error: %s" % exc) + except requests.Timeout: + _handle_get_page_fail(link, "timed out") + else: + return HTMLPage(resp.content, resp.url, resp.headers) + return None + + +def group_locations(locations, expand_dir=False): + # type: (Sequence[str], bool) -> Tuple[List[str], List[str]] + """ + Divide a list of locations into two groups: "files" (archives) and "urls." + + :return: A pair of lists (files, urls). + """ + files = [] + urls = [] + + # puts the url for the given file path into the appropriate list + def sort_path(path): + url = path_to_url(path) + if mimetypes.guess_type(url, strict=False)[0] == 'text/html': + urls.append(url) + else: + files.append(url) + + for url in locations: + + is_local_path = os.path.exists(url) + is_file_url = url.startswith('file:') + + if is_local_path or is_file_url: + if is_local_path: + path = url + else: + path = url_to_path(url) + if os.path.isdir(path): + if expand_dir: + path = os.path.realpath(path) + for item in os.listdir(path): + sort_path(os.path.join(path, item)) + elif is_file_url: + urls.append(url) + else: + logger.warning( + "Path '{0}' is ignored: " + "it is a directory.".format(path), + ) + elif os.path.isfile(path): + sort_path(path) + else: + logger.warning( + "Url '%s' is ignored: it is neither a file " + "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, + ) + + return files, urls + + +class CollectedLinks(object): + + """ + Encapsulates all the Link objects collected by a call to + LinkCollector.collect_links(), stored separately as-- + + (1) links from the configured file locations, + (2) links from the configured find_links, and + (3) a dict mapping HTML page url to links from that page. + """ + + def __init__( + self, + files, # type: List[Link] + find_links, # type: List[Link] + pages, # type: Dict[str, List[Link]] + ): + # type: (...) -> None + """ + :param files: Links from file locations. + :param find_links: Links from find_links. + :param pages: A dict mapping HTML page url to links from that page. + """ + self.files = files + self.find_links = find_links + self.pages = pages + + +class LinkCollector(object): + + """ + Responsible for collecting Link objects from all configured locations, + making network requests as needed. + + The class's main method is its collect_links() method. + """ + + def __init__( + self, + session, # type: PipSession + search_scope, # type: SearchScope + ): + # type: (...) -> None + self.search_scope = search_scope + self.session = session + + @property + def find_links(self): + # type: () -> List[str] + return self.search_scope.find_links + + def _get_pages(self, locations): + # type: (Iterable[Link]) -> Iterable[HTMLPage] + """ + Yields (page, page_url) from the given locations, skipping + locations that have errors. + """ + seen = set() # type: Set[Link] + for location in locations: + if location in seen: + continue + seen.add(location) + + page = _get_html_page(location, session=self.session) + if page is None: + continue + + yield page + + def collect_links(self, project_name): + # type: (str) -> CollectedLinks + """Find all available links for the given project name. + + :return: All the Link objects (unfiltered), as a CollectedLinks object. + """ + search_scope = self.search_scope + index_locations = search_scope.get_index_urls_locations(project_name) + index_file_loc, index_url_loc = group_locations(index_locations) + fl_file_loc, fl_url_loc = group_locations( + self.find_links, expand_dir=True, + ) + + file_links = [ + Link(url) for url in itertools.chain(index_file_loc, fl_file_loc) + ] + + # We trust every directly linked archive in find_links + find_link_links = [Link(url, '-f') for url in self.find_links] + + # We trust every url that the user has given us whether it was given + # via --index-url or --find-links. + # We want to filter out anything that does not have a secure origin. + url_locations = [ + link for link in itertools.chain( + (Link(url) for url in index_url_loc), + (Link(url) for url in fl_url_loc), + ) + if self.session.is_secure_origin(link) + ] + + logger.debug('%d location(s) to search for versions of %s:', + len(url_locations), project_name) + + for location in url_locations: + logger.debug('* %s', location) + + pages_links = {} + for page in self._get_pages(url_locations): + pages_links[page.url] = list(page.iter_links()) + + return CollectedLinks( + files=file_links, + find_links=find_link_links, + pages=pages_links, + ) diff --git a/src/pip/_internal/index.py b/src/pip/_internal/index.py index e718d33eb4c..792feefb3b7 100644 --- a/src/pip/_internal/index.py +++ b/src/pip/_internal/index.py @@ -5,22 +5,14 @@ from __future__ import absolute_import -import cgi -import itertools import logging -import mimetypes -import os import re -from pip._vendor import html5lib, requests -from pip._vendor.distlib.compat import unescape from pip._vendor.packaging import specifiers from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.packaging.version import parse as parse_version -from pip._vendor.requests.exceptions import HTTPError, RetryError, SSLError -from pip._vendor.six.moves.urllib import parse as urllib_parse -from pip._vendor.six.moves.urllib import request as urllib_request +from pip._internal.collector import LinkCollector from pip._internal.exceptions import ( BestVersionAlreadyInstalled, DistributionNotFound, @@ -34,27 +26,20 @@ from pip._internal.models.target_python import TargetPython from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import ( - ARCHIVE_EXTENSIONS, SUPPORTED_EXTENSIONS, WHEEL_EXTENSION, build_netloc, - path_to_url, - redact_auth_from_url, ) from pip._internal.utils.packaging import check_requires_python from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.urls import url_to_path -from pip._internal.vcs import is_url, vcs from pip._internal.wheel import Wheel if MYPY_CHECK_RUNNING: from typing import ( - Any, Callable, Dict, FrozenSet, Iterable, List, MutableMapping, - Optional, Sequence, Set, Text, Tuple, Union, + Any, FrozenSet, Iterable, List, Optional, Set, Text, Tuple, ) - import xml.etree.ElementTree from pip._vendor.packaging.version import _BaseVersion - from pip._vendor.requests import Response from pip._internal.models.search_scope import SearchScope from pip._internal.req import InstallRequirement from pip._internal.download import PipSession @@ -65,7 +50,6 @@ CandidateSortingKey = ( Tuple[int, int, int, _BaseVersion, BuildTag, Optional[int]] ) - HTMLElement = xml.etree.ElementTree.Element __all__ = ['FormatControl', 'BestCandidateResult', 'PackageFinder'] @@ -74,361 +58,6 @@ logger = logging.getLogger(__name__) -def _match_vcs_scheme(url): - # type: (str) -> Optional[str] - """Look for VCS schemes in the URL. - - Returns the matched VCS scheme, or None if there's no match. - """ - for scheme in vcs.schemes: - if url.lower().startswith(scheme) and url[len(scheme)] in '+:': - return scheme - return None - - -def _is_url_like_archive(url): - # type: (str) -> bool - """Return whether the URL looks like an archive. - """ - filename = Link(url).filename - for bad_ext in ARCHIVE_EXTENSIONS: - if filename.endswith(bad_ext): - return True - return False - - -class _NotHTML(Exception): - def __init__(self, content_type, request_desc): - # type: (str, str) -> None - super(_NotHTML, self).__init__(content_type, request_desc) - self.content_type = content_type - self.request_desc = request_desc - - -def _ensure_html_header(response): - # type: (Response) -> None - """Check the Content-Type header to ensure the response contains HTML. - - Raises `_NotHTML` if the content type is not text/html. - """ - content_type = response.headers.get("Content-Type", "") - if not content_type.lower().startswith("text/html"): - raise _NotHTML(content_type, response.request.method) - - -class _NotHTTP(Exception): - pass - - -def _ensure_html_response(url, session): - # type: (str, PipSession) -> None - """Send a HEAD request to the URL, and ensure the response contains HTML. - - Raises `_NotHTTP` if the URL is not available for a HEAD request, or - `_NotHTML` if the content type is not text/html. - """ - scheme, netloc, path, query, fragment = urllib_parse.urlsplit(url) - if scheme not in {'http', 'https'}: - raise _NotHTTP() - - resp = session.head(url, allow_redirects=True) - resp.raise_for_status() - - _ensure_html_header(resp) - - -def _get_html_response(url, session): - # type: (str, PipSession) -> Response - """Access an HTML page with GET, and return the response. - - This consists of three parts: - - 1. If the URL looks suspiciously like an archive, send a HEAD first to - check the Content-Type is HTML, to avoid downloading a large file. - Raise `_NotHTTP` if the content type cannot be determined, or - `_NotHTML` if it is not HTML. - 2. Actually perform the request. Raise HTTP exceptions on network failures. - 3. Check the Content-Type header to make sure we got HTML, and raise - `_NotHTML` otherwise. - """ - if _is_url_like_archive(url): - _ensure_html_response(url, session=session) - - logger.debug('Getting page %s', redact_auth_from_url(url)) - - resp = session.get( - url, - headers={ - "Accept": "text/html", - # We don't want to blindly returned cached data for - # /simple/, because authors generally expecting that - # twine upload && pip install will function, but if - # they've done a pip install in the last ~10 minutes - # it won't. Thus by setting this to zero we will not - # blindly use any cached data, however the benefit of - # using max-age=0 instead of no-cache, is that we will - # still support conditional requests, so we will still - # minimize traffic sent in cases where the page hasn't - # changed at all, we will just always incur the round - # trip for the conditional GET now instead of only - # once per 10 minutes. - # For more information, please see pypa/pip#5670. - "Cache-Control": "max-age=0", - }, - ) - resp.raise_for_status() - - # The check for archives above only works if the url ends with - # something that looks like an archive. However that is not a - # requirement of an url. Unless we issue a HEAD request on every - # url we cannot know ahead of time for sure if something is HTML - # or not. However we can check after we've downloaded it. - _ensure_html_header(resp) - - return resp - - -def _handle_get_page_fail( - link, # type: Link - reason, # type: Union[str, Exception] - meth=None # type: Optional[Callable[..., None]] -): - # type: (...) -> None - if meth is None: - meth = logger.debug - meth("Could not fetch URL %s: %s - skipping", link, reason) - - -def _get_html_page(link, session=None): - # type: (Link, Optional[PipSession]) -> Optional[HTMLPage] - if session is None: - raise TypeError( - "_get_html_page() missing 1 required keyword argument: 'session'" - ) - - url = link.url.split('#', 1)[0] - - # Check for VCS schemes that do not support lookup as web pages. - vcs_scheme = _match_vcs_scheme(url) - if vcs_scheme: - logger.debug('Cannot look at %s URL %s', vcs_scheme, link) - return None - - # Tack index.html onto file:// URLs that point to directories - scheme, _, path, _, _, _ = urllib_parse.urlparse(url) - if (scheme == 'file' and os.path.isdir(urllib_request.url2pathname(path))): - # add trailing slash if not present so urljoin doesn't trim - # final segment - if not url.endswith('/'): - url += '/' - url = urllib_parse.urljoin(url, 'index.html') - logger.debug(' file: URL is directory, getting %s', url) - - try: - resp = _get_html_response(url, session=session) - except _NotHTTP: - logger.debug( - 'Skipping page %s because it looks like an archive, and cannot ' - 'be checked by HEAD.', link, - ) - except _NotHTML as exc: - logger.debug( - 'Skipping page %s because the %s request got Content-Type: %s', - link, exc.request_desc, exc.content_type, - ) - except HTTPError as exc: - _handle_get_page_fail(link, exc) - except RetryError as exc: - _handle_get_page_fail(link, exc) - except SSLError as exc: - reason = "There was a problem confirming the ssl certificate: " - reason += str(exc) - _handle_get_page_fail(link, reason, meth=logger.info) - except requests.ConnectionError as exc: - _handle_get_page_fail(link, "connection error: %s" % exc) - except requests.Timeout: - _handle_get_page_fail(link, "timed out") - else: - return HTMLPage(resp.content, resp.url, resp.headers) - return None - - -def group_locations(locations, expand_dir=False): - # type: (Sequence[str], bool) -> Tuple[List[str], List[str]] - """ - Divide a list of locations into two groups: "files" (archives) and "urls." - - :return: A pair of lists (files, urls). - """ - files = [] - urls = [] - - # puts the url for the given file path into the appropriate list - def sort_path(path): - url = path_to_url(path) - if mimetypes.guess_type(url, strict=False)[0] == 'text/html': - urls.append(url) - else: - files.append(url) - - for url in locations: - - is_local_path = os.path.exists(url) - is_file_url = url.startswith('file:') - - if is_local_path or is_file_url: - if is_local_path: - path = url - else: - path = url_to_path(url) - if os.path.isdir(path): - if expand_dir: - path = os.path.realpath(path) - for item in os.listdir(path): - sort_path(os.path.join(path, item)) - elif is_file_url: - urls.append(url) - else: - logger.warning( - "Path '{0}' is ignored: " - "it is a directory.".format(path), - ) - elif os.path.isfile(path): - sort_path(path) - else: - logger.warning( - "Url '%s' is ignored: it is neither a file " - "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, - ) - - return files, urls - - -class CollectedLinks(object): - - """ - Encapsulates all the Link objects collected by a call to - LinkCollector.collect_links(), stored separately as-- - - (1) links from the configured file locations, - (2) links from the configured find_links, and - (3) a dict mapping HTML page url to links from that page. - """ - - def __init__( - self, - files, # type: List[Link] - find_links, # type: List[Link] - pages, # type: Dict[str, List[Link]] - ): - # type: (...) -> None - """ - :param files: Links from file locations. - :param find_links: Links from find_links. - :param pages: A dict mapping HTML page url to links from that page. - """ - self.files = files - self.find_links = find_links - self.pages = pages - - -class LinkCollector(object): - - """ - Responsible for collecting Link objects from all configured locations, - making network requests as needed. - - The class's main method is its collect_links() method. - """ - - def __init__( - self, - session, # type: PipSession - search_scope, # type: SearchScope - ): - # type: (...) -> None - self.search_scope = search_scope - self.session = session - - @property - def find_links(self): - # type: () -> List[str] - return self.search_scope.find_links - - def _get_pages(self, locations): - # type: (Iterable[Link]) -> Iterable[HTMLPage] - """ - Yields (page, page_url) from the given locations, skipping - locations that have errors. - """ - seen = set() # type: Set[Link] - for location in locations: - if location in seen: - continue - seen.add(location) - - page = _get_html_page(location, session=self.session) - if page is None: - continue - - yield page - - def collect_links(self, project_name): - # type: (str) -> CollectedLinks - """Find all available links for the given project name. - - :return: All the Link objects (unfiltered), as a CollectedLinks object. - """ - search_scope = self.search_scope - index_locations = search_scope.get_index_urls_locations(project_name) - index_file_loc, index_url_loc = group_locations(index_locations) - fl_file_loc, fl_url_loc = group_locations( - self.find_links, expand_dir=True, - ) - - file_links = [ - Link(url) for url in itertools.chain(index_file_loc, fl_file_loc) - ] - - # We trust every directly linked archive in find_links - find_link_links = [Link(url, '-f') for url in self.find_links] - - # We trust every url that the user has given us whether it was given - # via --index-url or --find-links. - # We want to filter out anything that does not have a secure origin. - url_locations = [ - link for link in itertools.chain( - (Link(url) for url in index_url_loc), - (Link(url) for url in fl_url_loc), - ) - if self.session.is_secure_origin(link) - ] - - logger.debug('%d location(s) to search for versions of %s:', - len(url_locations), project_name) - - for location in url_locations: - logger.debug('* %s', location) - - pages_links = {} - for page in self._get_pages(url_locations): - pages_links[page.url] = list(page.iter_links()) - - return CollectedLinks( - files=file_links, - find_links=find_link_links, - pages=pages_links, - ) - - def _check_link_requires_python( link, # type: Link version_info, # type: Tuple[int, int, int] @@ -1375,122 +1004,3 @@ def _extract_version_from_fragment(fragment, canonical_name): if not version: return None return version - - -def _determine_base_url(document, page_url): - """Determine the HTML document's base URL. - - This looks for a ```` tag in the HTML document. If present, its href - attribute denotes the base URL of anchor tags in the document. If there is - no such tag (or if it does not have a valid href attribute), the HTML - file's URL is used as the base URL. - - :param document: An HTML document representation. The current - implementation expects the result of ``html5lib.parse()``. - :param page_url: The URL of the HTML document. - """ - for base in document.findall(".//base"): - href = base.get("href") - if href is not None: - return href - return page_url - - -def _get_encoding_from_headers(headers): - """Determine if we have any encoding information in our headers. - """ - if headers and "Content-Type" in headers: - content_type, params = cgi.parse_header(headers["Content-Type"]) - if "charset" in params: - return params['charset'] - return None - - -def _clean_link(url): - # type: (str) -> str - """Makes sure a link is fully encoded. That is, if a ' ' shows up in - the link, it will be rewritten to %20 (while not over-quoting - % or other characters).""" - # Split the URL into parts according to the general structure - # `scheme://netloc/path;parameters?query#fragment`. Note that the - # `netloc` can be empty and the URI will then refer to a local - # filesystem path. - result = urllib_parse.urlparse(url) - # In both cases below we unquote prior to quoting to make sure - # nothing is double quoted. - if result.netloc == "": - # On Windows the path part might contain a drive letter which - # should not be quoted. On Linux where drive letters do not - # exist, the colon should be quoted. We rely on urllib.request - # to do the right thing here. - path = urllib_request.pathname2url( - urllib_request.url2pathname(result.path)) - else: - # In addition to the `/` character we protect `@` so that - # revision strings in VCS URLs are properly parsed. - path = urllib_parse.quote(urllib_parse.unquote(result.path), safe="/@") - return urllib_parse.urlunparse(result._replace(path=path)) - - -def _create_link_from_element( - anchor, # type: HTMLElement - page_url, # type: str - base_url, # type: str -): - # type: (...) -> Optional[Link] - """ - Convert an anchor element in a simple repository page to a Link. - """ - href = anchor.get("href") - if not href: - return None - - url = _clean_link(urllib_parse.urljoin(base_url, href)) - pyrequire = anchor.get('data-requires-python') - pyrequire = unescape(pyrequire) if pyrequire else None - - yanked_reason = anchor.get('data-yanked') - if yanked_reason: - # This is a unicode string in Python 2 (and 3). - yanked_reason = unescape(yanked_reason) - - link = Link( - url, - comes_from=page_url, - requires_python=pyrequire, - yanked_reason=yanked_reason, - ) - - return link - - -class HTMLPage(object): - """Represents one page, along with its URL""" - - def __init__(self, content, url, headers=None): - # type: (bytes, str, MutableMapping[str, str]) -> None - self.content = content - self.url = url - self.headers = headers - - def __str__(self): - return redact_auth_from_url(self.url) - - def iter_links(self): - # type: () -> Iterable[Link] - """Yields all links in the page""" - document = html5lib.parse( - self.content, - transport_encoding=_get_encoding_from_headers(self.headers), - namespaceHTMLElements=False, - ) - base_url = _determine_base_url(document, self.url) - for anchor in document.findall(".//a"): - link = _create_link_from_element( - anchor, - page_url=self.url, - base_url=base_url, - ) - if link is None: - continue - yield link diff --git a/src/pip/_internal/models/link.py b/src/pip/_internal/models/link.py index 2a3c605e583..8c9e9e0fccb 100644 --- a/src/pip/_internal/models/link.py +++ b/src/pip/_internal/models/link.py @@ -15,7 +15,7 @@ if MYPY_CHECK_RUNNING: from typing import Optional, Text, Tuple, Union - from pip._internal.index import HTMLPage + from pip._internal.collector import HTMLPage from pip._internal.utils.hashes import Hashes diff --git a/tests/unit/test_collector.py b/tests/unit/test_collector.py new file mode 100644 index 00000000000..059ba40fd55 --- /dev/null +++ b/tests/unit/test_collector.py @@ -0,0 +1,452 @@ +import logging +import os.path +from textwrap import dedent + +import mock +import pytest +from mock import Mock, patch +from pip._vendor import html5lib, requests +from pip._vendor.six.moves.urllib import request as urllib_request + +from pip._internal.collector import ( + HTMLPage, + LinkCollector, + _clean_link, + _determine_base_url, + _get_html_page, + _get_html_response, + _NotHTML, + _NotHTTP, + group_locations, +) +from pip._internal.download import PipSession +from pip._internal.models.index import PyPI +from pip._internal.models.link import Link +from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from tests.lib import make_test_search_scope + +if MYPY_CHECK_RUNNING: + from typing import List, Optional + + +@pytest.mark.parametrize( + "url", + [ + "ftp://python.org/python-3.7.1.zip", + "file:///opt/data/pip-18.0.tar.gz", + ], +) +def test_get_html_response_archive_to_naive_scheme(url): + """ + `_get_html_response()` should error on an archive-like URL if the scheme + does not allow "poking" without getting data. + """ + with pytest.raises(_NotHTTP): + _get_html_response(url, session=mock.Mock(PipSession)) + + +@pytest.mark.parametrize( + "url, content_type", + [ + ("http://python.org/python-3.7.1.zip", "application/zip"), + ("https://pypi.org/pip-18.0.tar.gz", "application/gzip"), + ], +) +def test_get_html_response_archive_to_http_scheme(url, content_type): + """ + `_get_html_response()` should send a HEAD request on an archive-like URL + if the scheme supports it, and raise `_NotHTML` if the response isn't HTML. + """ + session = mock.Mock(PipSession) + session.head.return_value = mock.Mock(**{ + "request.method": "HEAD", + "headers": {"Content-Type": content_type}, + }) + + with pytest.raises(_NotHTML) as ctx: + _get_html_response(url, session=session) + + session.assert_has_calls([ + mock.call.head(url, allow_redirects=True), + ]) + assert ctx.value.args == (content_type, "HEAD") + + +@pytest.mark.parametrize( + "url", + [ + "http://python.org/python-3.7.1.zip", + "https://pypi.org/pip-18.0.tar.gz", + ], +) +def test_get_html_response_archive_to_http_scheme_is_html(url): + """ + `_get_html_response()` should work with archive-like URLs if the HEAD + request is responded with text/html. + """ + session = mock.Mock(PipSession) + session.head.return_value = mock.Mock(**{ + "request.method": "HEAD", + "headers": {"Content-Type": "text/html"}, + }) + session.get.return_value = mock.Mock(headers={"Content-Type": "text/html"}) + + resp = _get_html_response(url, session=session) + + assert resp is not None + assert session.mock_calls == [ + mock.call.head(url, allow_redirects=True), + mock.call.head().raise_for_status(), + mock.call.get(url, headers={ + "Accept": "text/html", "Cache-Control": "max-age=0", + }), + mock.call.get().raise_for_status(), + ] + + +@pytest.mark.parametrize( + "url", + [ + "https://pypi.org/simple/pip", + "https://pypi.org/simple/pip/", + "https://python.org/sitemap.xml", + ], +) +def test_get_html_response_no_head(url): + """ + `_get_html_response()` shouldn't send a HEAD request if the URL does not + look like an archive, only the GET request that retrieves data. + """ + session = mock.Mock(PipSession) + + # Mock the headers dict to ensure it is accessed. + session.get.return_value = mock.Mock(headers=mock.Mock(**{ + "get.return_value": "text/html", + })) + + resp = _get_html_response(url, session=session) + + assert resp is not None + assert session.head.call_count == 0 + assert session.get.mock_calls == [ + mock.call(url, headers={ + "Accept": "text/html", "Cache-Control": "max-age=0", + }), + mock.call().raise_for_status(), + mock.call().headers.get("Content-Type", ""), + ] + + +def test_get_html_response_dont_log_clear_text_password(caplog): + """ + `_get_html_response()` should redact the password from the index URL + in its DEBUG log message. + """ + session = mock.Mock(PipSession) + + # Mock the headers dict to ensure it is accessed. + session.get.return_value = mock.Mock(headers=mock.Mock(**{ + "get.return_value": "text/html", + })) + + caplog.set_level(logging.DEBUG) + + resp = _get_html_response( + "https://user:my_password@example.com/simple/", session=session + ) + + assert resp is not None + + assert len(caplog.records) == 1 + record = caplog.records[0] + assert record.levelname == 'DEBUG' + assert record.message.splitlines() == [ + "Getting page https://user:****@example.com/simple/", + ] + + +@pytest.mark.parametrize( + ("html", "url", "expected"), + [ + (b"", "https://example.com/", "https://example.com/"), + ( + b"" + b"" + b"", + "https://example.com/", + "https://foo.example.com/", + ), + ( + b"" + b"" + b"", + "https://example.com/", + "https://foo.example.com/", + ), + ], +) +def test_determine_base_url(html, url, expected): + document = html5lib.parse( + html, transport_encoding=None, namespaceHTMLElements=False, + ) + assert _determine_base_url(document, url) == expected + + +@pytest.mark.parametrize( + ("url", "clean_url"), + [ + # URL with hostname and port. Port separator should not be quoted. + ("https://localhost.localdomain:8181/path/with space/", + "https://localhost.localdomain:8181/path/with%20space/"), + # URL that is already properly quoted. The quoting `%` + # characters should not be quoted again. + ("https://localhost.localdomain:8181/path/with%20quoted%20space/", + "https://localhost.localdomain:8181/path/with%20quoted%20space/"), + # URL with IPv4 address and port. + ("https://127.0.0.1:8181/path/with space/", + "https://127.0.0.1:8181/path/with%20space/"), + # URL with IPv6 address and port. The `[]` brackets around the + # IPv6 address should not be quoted. + ("https://[fd00:0:0:236::100]:8181/path/with space/", + "https://[fd00:0:0:236::100]:8181/path/with%20space/"), + # URL with query. The leading `?` should not be quoted. + ("https://localhost.localdomain:8181/path/with/query?request=test", + "https://localhost.localdomain:8181/path/with/query?request=test"), + # URL with colon in the path portion. + ("https://localhost.localdomain:8181/path:/with:/colon", + "https://localhost.localdomain:8181/path%3A/with%3A/colon"), + # URL with something that looks like a drive letter, but is + # not. The `:` should be quoted. + ("https://localhost.localdomain/T:/path/", + "https://localhost.localdomain/T%3A/path/"), + # VCS URL containing revision string. + ("git+ssh://example.com/path to/repo.git@1.0#egg=my-package-1.0", + "git+ssh://example.com/path%20to/repo.git@1.0#egg=my-package-1.0"), + # URL with Windows drive letter. The `:` after the drive + # letter should not be quoted. The trailing `/` should be + # removed. + pytest.param( + "file:///T:/path/with spaces/", + "file:///T:/path/with%20spaces", + marks=pytest.mark.skipif("sys.platform != 'win32'"), + ), + # URL with Windows drive letter, running on non-windows + # platform. The `:` after the drive should be quoted. + pytest.param( + "file:///T:/path/with spaces/", + "file:///T%3A/path/with%20spaces/", + marks=pytest.mark.skipif("sys.platform == 'win32'"), + ), + ] +) +def test_clean_link(url, clean_url): + assert(_clean_link(url) == clean_url) + + +class TestHTMLPage: + + @pytest.mark.parametrize( + ('anchor_html, expected'), + [ + # Test not present. + ('', None), + # Test present with no value. + ('', ''), + # Test the empty string. + ('', ''), + # Test a non-empty string. + ('', 'error'), + # Test a value with an escaped character. + ('', + 'version < 1'), + # Test a yanked reason with a non-ascii character. + (u'', + u'curlyquote \u2018'), + ] + ) + def test_iter_links__yanked_reason(self, anchor_html, expected): + html = ( + # Mark this as a unicode string for Python 2 since anchor_html + # can contain non-ascii. + u'' + '{}' + ).format(anchor_html) + html_bytes = html.encode('utf-8') + page = HTMLPage(html_bytes, url='https://example.com/simple/') + links = list(page.iter_links()) + link, = links + actual = link.yanked_reason + assert actual == expected + + +def test_request_http_error(caplog): + caplog.set_level(logging.DEBUG) + link = Link('http://localhost') + session = Mock(PipSession) + session.get.return_value = resp = Mock() + resp.raise_for_status.side_effect = requests.HTTPError('Http error') + assert _get_html_page(link, session=session) is None + assert ( + 'Could not fetch URL http://localhost: Http error - skipping' + in caplog.text + ) + + +def test_request_retries(caplog): + caplog.set_level(logging.DEBUG) + link = Link('http://localhost') + session = Mock(PipSession) + session.get.side_effect = requests.exceptions.RetryError('Retry error') + assert _get_html_page(link, session=session) is None + assert ( + 'Could not fetch URL http://localhost: Retry error - skipping' + in caplog.text + ) + + +@pytest.mark.parametrize( + "url, vcs_scheme", + [ + ("svn+http://pypi.org/something", "svn"), + ("git+https://github.com/pypa/pip.git", "git"), + ], +) +def test_get_html_page_invalid_scheme(caplog, url, vcs_scheme): + """`_get_html_page()` should error if an invalid scheme is given. + + Only file:, http:, https:, and ftp: are allowed. + """ + with caplog.at_level(logging.DEBUG): + page = _get_html_page(Link(url), session=mock.Mock(PipSession)) + + assert page is None + assert caplog.record_tuples == [ + ( + "pip._internal.collector", + logging.DEBUG, + "Cannot look at {} URL {}".format(vcs_scheme, url), + ), + ] + + +def test_get_html_page_directory_append_index(tmpdir): + """`_get_html_page()` should append "index.html" to a directory URL. + """ + dirpath = tmpdir.mkdir("something") + dir_url = "file:///{}".format( + urllib_request.pathname2url(dirpath).lstrip("/"), + ) + + session = mock.Mock(PipSession) + with mock.patch("pip._internal.collector._get_html_response") as mock_func: + _get_html_page(Link(dir_url), session=session) + assert mock_func.mock_calls == [ + mock.call( + "{}/index.html".format(dir_url.rstrip("/")), + session=session, + ), + ] + + +def test_group_locations__file_expand_dir(data): + """ + Test that a file:// dir gets listdir run with expand_dir + """ + files, urls = group_locations([data.find_links], expand_dir=True) + assert files and not urls, ( + "files and not urls should have been found at find-links url: %s" % + data.find_links + ) + + +def test_group_locations__file_not_find_link(data): + """ + Test that a file:// url dir that's not a find-link, doesn't get a listdir + run + """ + files, urls = group_locations([data.index_url("empty_with_pkg")]) + assert urls and not files, "urls, but not files should have been found" + + +def test_group_locations__non_existing_path(): + """ + Test that a non-existing path is ignored. + """ + files, urls = group_locations([os.path.join('this', 'doesnt', 'exist')]) + assert not urls and not files, "nothing should have been found" + + +def make_fake_html_page(url): + html = dedent(u"""\ + + + abc-1.0.tar.gz + + """) + content = html.encode('utf-8') + headers = {} + return HTMLPage(content, url=url, headers=headers) + + +def make_test_link_collector( + find_links=None, # type: Optional[List[str]] +): + # type: (...) -> LinkCollector + """ + Create a LinkCollector object for testing purposes. + """ + session = PipSession() + search_scope = make_test_search_scope( + find_links=find_links, + index_urls=[PyPI.simple_url], + ) + + return LinkCollector( + session=session, + search_scope=search_scope, + ) + + +def check_links_include(links, names): + """ + Assert that the given list of Link objects includes, for each of the + given names, a link whose URL has a base name matching that name. + """ + for name in names: + assert any(link.url.endswith(name) for link in links), ( + 'name {!r} not among links: {}'.format(name, links) + ) + + +class TestLinkCollector(object): + + @patch('pip._internal.collector._get_html_response') + def test_collect_links(self, mock_get_html_response, data): + expected_url = 'https://pypi.org/simple/twine/' + + fake_page = make_fake_html_page(expected_url) + mock_get_html_response.return_value = fake_page + + link_collector = make_test_link_collector( + find_links=[data.find_links] + ) + actual = link_collector.collect_links('twine') + + mock_get_html_response.assert_called_once_with( + expected_url, session=link_collector.session, + ) + + # Spot-check the CollectedLinks return value. + assert len(actual.files) > 20 + check_links_include(actual.files, names=['simple-1.0.tar.gz']) + + assert len(actual.find_links) == 1 + check_links_include(actual.find_links, names=['packages']) + + actual_pages = actual.pages + assert list(actual_pages) == [expected_url] + actual_page_links = actual_pages[expected_url] + assert len(actual_page_links) == 1 + assert actual_page_links[0].url == ( + 'https://pypi.org/abc-1.0.tar.gz#md5=000000000' + ) diff --git a/tests/unit/test_finder.py b/tests/unit/test_finder.py index 13573447c27..1295ff0b059 100644 --- a/tests/unit/test_finder.py +++ b/tests/unit/test_finder.py @@ -62,7 +62,7 @@ def test_no_partial_name_match(data): def test_tilde(): """Finder can accept a path with ~ in it and will normalize it.""" - with patch('pip._internal.index.os.path.exists', return_value=True): + with patch('pip._internal.collector.os.path.exists', return_value=True): finder = make_test_finder(find_links=['~/python-pkgs']) req = install_req_from_line("gmpy") with pytest.raises(DistributionNotFound): diff --git a/tests/unit/test_index.py b/tests/unit/test_index.py index d884fe52c08..b6d8afa2674 100644 --- a/tests/unit/test_index.py +++ b/tests/unit/test_index.py @@ -1,10 +1,6 @@ import logging -import os.path -from textwrap import dedent import pytest -from mock import Mock, patch -from pip._vendor import html5lib, requests from pip._vendor.packaging.specifiers import SpecifierSet from pip._internal.download import PipSession @@ -12,108 +8,22 @@ CandidateEvaluator, CandidatePreferences, FormatControl, - HTMLPage, LinkCollector, LinkEvaluator, PackageFinder, _check_link_requires_python, - _clean_link, - _determine_base_url, _extract_version_from_fragment, _find_name_version_sep, - _get_html_page, filter_unallowed_hashes, - group_locations, ) from pip._internal.models.candidate import InstallationCandidate -from pip._internal.models.index import PyPI from pip._internal.models.link import Link from pip._internal.models.search_scope import SearchScope from pip._internal.models.selection_prefs import SelectionPreferences from pip._internal.models.target_python import TargetPython from pip._internal.pep425tags import get_supported from pip._internal.utils.hashes import Hashes -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from tests.lib import CURRENT_PY_VERSION_INFO, make_test_search_scope - -if MYPY_CHECK_RUNNING: - from typing import List, Optional - - -def make_fake_html_page(url): - html = dedent(u"""\ - - - abc-1.0.tar.gz - - """) - content = html.encode('utf-8') - headers = {} - return HTMLPage(content, url=url, headers=headers) - - -def make_test_link_collector( - find_links=None, # type: Optional[List[str]] -): - # type: (...) -> LinkCollector - """ - Create a LinkCollector object for testing purposes. - """ - session = PipSession() - search_scope = make_test_search_scope( - find_links=find_links, - index_urls=[PyPI.simple_url], - ) - - return LinkCollector( - session=session, - search_scope=search_scope, - ) - - -def check_links_include(links, names): - """ - Assert that the given list of Link objects includes, for each of the - given names, a link whose URL has a base name matching that name. - """ - for name in names: - assert any(link.url.endswith(name) for link in links), ( - 'name {!r} not among links: {}'.format(name, links) - ) - - -class TestLinkCollector(object): - - @patch('pip._internal.index._get_html_response') - def test_collect_links(self, mock_get_html_response, data): - expected_url = 'https://pypi.org/simple/twine/' - - fake_page = make_fake_html_page(expected_url) - mock_get_html_response.return_value = fake_page - - link_collector = make_test_link_collector( - find_links=[data.find_links] - ) - actual = link_collector.collect_links('twine') - - mock_get_html_response.assert_called_once_with( - expected_url, session=link_collector.session, - ) - - # Spot-check the CollectedLinks return value. - assert len(actual.files) > 20 - check_links_include(actual.files, names=['simple-1.0.tar.gz']) - - assert len(actual.find_links) == 1 - check_links_include(actual.find_links, names=['packages']) - - actual_pages = actual.pages - assert list(actual_pages) == [expected_url] - actual_page_links = actual_pages[expected_url] - assert len(actual_page_links) == 1 - assert actual_page_links[0].url == ( - 'https://pypi.org/abc-1.0.tar.gz#md5=000000000' - ) +from tests.lib import CURRENT_PY_VERSION_INFO def make_mock_candidate(version, yanked_reason=None, hex_digest=None): @@ -856,61 +766,6 @@ def test_make_candidate_evaluator( assert evaluator._supported_tags == [('py36', 'none', 'any')] -def test_group_locations__file_expand_dir(data): - """ - Test that a file:// dir gets listdir run with expand_dir - """ - files, urls = group_locations([data.find_links], expand_dir=True) - assert files and not urls, ( - "files and not urls should have been found at find-links url: %s" % - data.find_links - ) - - -def test_group_locations__file_not_find_link(data): - """ - Test that a file:// url dir that's not a find-link, doesn't get a listdir - run - """ - files, urls = group_locations([data.index_url("empty_with_pkg")]) - assert urls and not files, "urls, but not files should have been found" - - -def test_group_locations__non_existing_path(): - """ - Test that a non-existing path is ignored. - """ - files, urls = group_locations([os.path.join('this', 'doesnt', 'exist')]) - assert not urls and not files, "nothing should have been found" - - -@pytest.mark.parametrize( - ("html", "url", "expected"), - [ - (b"", "https://example.com/", "https://example.com/"), - ( - b"" - b"" - b"", - "https://example.com/", - "https://foo.example.com/", - ), - ( - b"" - b"" - b"", - "https://example.com/", - "https://foo.example.com/", - ), - ], -) -def test_determine_base_url(html, url, expected): - document = html5lib.parse( - html, transport_encoding=None, namespaceHTMLElements=False, - ) - assert _determine_base_url(document, url) == expected - - @pytest.mark.parametrize( ("fragment", "canonical_name", "expected"), [ @@ -999,115 +854,3 @@ def test_find_name_version_sep_failure(fragment, canonical_name): def test_extract_version_from_fragment(fragment, canonical_name, expected): version = _extract_version_from_fragment(fragment, canonical_name) assert version == expected - - -def test_request_http_error(caplog): - caplog.set_level(logging.DEBUG) - link = Link('http://localhost') - session = Mock(PipSession) - session.get.return_value = resp = Mock() - resp.raise_for_status.side_effect = requests.HTTPError('Http error') - assert _get_html_page(link, session=session) is None - assert ( - 'Could not fetch URL http://localhost: Http error - skipping' - in caplog.text - ) - - -def test_request_retries(caplog): - caplog.set_level(logging.DEBUG) - link = Link('http://localhost') - session = Mock(PipSession) - session.get.side_effect = requests.exceptions.RetryError('Retry error') - assert _get_html_page(link, session=session) is None - assert ( - 'Could not fetch URL http://localhost: Retry error - skipping' - in caplog.text - ) - - -@pytest.mark.parametrize( - ("url", "clean_url"), - [ - # URL with hostname and port. Port separator should not be quoted. - ("https://localhost.localdomain:8181/path/with space/", - "https://localhost.localdomain:8181/path/with%20space/"), - # URL that is already properly quoted. The quoting `%` - # characters should not be quoted again. - ("https://localhost.localdomain:8181/path/with%20quoted%20space/", - "https://localhost.localdomain:8181/path/with%20quoted%20space/"), - # URL with IPv4 address and port. - ("https://127.0.0.1:8181/path/with space/", - "https://127.0.0.1:8181/path/with%20space/"), - # URL with IPv6 address and port. The `[]` brackets around the - # IPv6 address should not be quoted. - ("https://[fd00:0:0:236::100]:8181/path/with space/", - "https://[fd00:0:0:236::100]:8181/path/with%20space/"), - # URL with query. The leading `?` should not be quoted. - ("https://localhost.localdomain:8181/path/with/query?request=test", - "https://localhost.localdomain:8181/path/with/query?request=test"), - # URL with colon in the path portion. - ("https://localhost.localdomain:8181/path:/with:/colon", - "https://localhost.localdomain:8181/path%3A/with%3A/colon"), - # URL with something that looks like a drive letter, but is - # not. The `:` should be quoted. - ("https://localhost.localdomain/T:/path/", - "https://localhost.localdomain/T%3A/path/"), - # VCS URL containing revision string. - ("git+ssh://example.com/path to/repo.git@1.0#egg=my-package-1.0", - "git+ssh://example.com/path%20to/repo.git@1.0#egg=my-package-1.0"), - # URL with Windows drive letter. The `:` after the drive - # letter should not be quoted. The trailing `/` should be - # removed. - pytest.param( - "file:///T:/path/with spaces/", - "file:///T:/path/with%20spaces", - marks=pytest.mark.skipif("sys.platform != 'win32'"), - ), - # URL with Windows drive letter, running on non-windows - # platform. The `:` after the drive should be quoted. - pytest.param( - "file:///T:/path/with spaces/", - "file:///T%3A/path/with%20spaces/", - marks=pytest.mark.skipif("sys.platform == 'win32'"), - ), - ] -) -def test_clean_link(url, clean_url): - assert(_clean_link(url) == clean_url) - - -class TestHTMLPage: - - @pytest.mark.parametrize( - ('anchor_html, expected'), - [ - # Test not present. - ('', None), - # Test present with no value. - ('', ''), - # Test the empty string. - ('', ''), - # Test a non-empty string. - ('', 'error'), - # Test a value with an escaped character. - ('', - 'version < 1'), - # Test a yanked reason with a non-ascii character. - (u'', - u'curlyquote \u2018'), - ] - ) - def test_iter_links__yanked_reason(self, anchor_html, expected): - html = ( - # Mark this as a unicode string for Python 2 since anchor_html - # can contain non-ascii. - u'' - '{}' - ).format(anchor_html) - html_bytes = html.encode('utf-8') - page = HTMLPage(html_bytes, url='https://example.com/simple/') - links = list(page.iter_links()) - link, = links - actual = link.yanked_reason - assert actual == expected diff --git a/tests/unit/test_index_html_page.py b/tests/unit/test_index_html_page.py deleted file mode 100644 index ec2a3950e7a..00000000000 --- a/tests/unit/test_index_html_page.py +++ /dev/null @@ -1,194 +0,0 @@ -import logging - -import mock -import pytest -from pip._vendor.six.moves.urllib import request as urllib_request - -from pip._internal.download import PipSession -from pip._internal.index import ( - Link, - _get_html_page, - _get_html_response, - _NotHTML, - _NotHTTP, -) - - -@pytest.mark.parametrize( - "url", - [ - "ftp://python.org/python-3.7.1.zip", - "file:///opt/data/pip-18.0.tar.gz", - ], -) -def test_get_html_response_archive_to_naive_scheme(url): - """ - `_get_html_response()` should error on an archive-like URL if the scheme - does not allow "poking" without getting data. - """ - with pytest.raises(_NotHTTP): - _get_html_response(url, session=mock.Mock(PipSession)) - - -@pytest.mark.parametrize( - "url, content_type", - [ - ("http://python.org/python-3.7.1.zip", "application/zip"), - ("https://pypi.org/pip-18.0.tar.gz", "application/gzip"), - ], -) -def test_get_html_response_archive_to_http_scheme(url, content_type): - """ - `_get_html_response()` should send a HEAD request on an archive-like URL - if the scheme supports it, and raise `_NotHTML` if the response isn't HTML. - """ - session = mock.Mock(PipSession) - session.head.return_value = mock.Mock(**{ - "request.method": "HEAD", - "headers": {"Content-Type": content_type}, - }) - - with pytest.raises(_NotHTML) as ctx: - _get_html_response(url, session=session) - - session.assert_has_calls([ - mock.call.head(url, allow_redirects=True), - ]) - assert ctx.value.args == (content_type, "HEAD") - - -@pytest.mark.parametrize( - "url", - [ - "http://python.org/python-3.7.1.zip", - "https://pypi.org/pip-18.0.tar.gz", - ], -) -def test_get_html_response_archive_to_http_scheme_is_html(url): - """ - `_get_html_response()` should work with archive-like URLs if the HEAD - request is responded with text/html. - """ - session = mock.Mock(PipSession) - session.head.return_value = mock.Mock(**{ - "request.method": "HEAD", - "headers": {"Content-Type": "text/html"}, - }) - session.get.return_value = mock.Mock(headers={"Content-Type": "text/html"}) - - resp = _get_html_response(url, session=session) - - assert resp is not None - assert session.mock_calls == [ - mock.call.head(url, allow_redirects=True), - mock.call.head().raise_for_status(), - mock.call.get(url, headers={ - "Accept": "text/html", "Cache-Control": "max-age=0", - }), - mock.call.get().raise_for_status(), - ] - - -@pytest.mark.parametrize( - "url", - [ - "https://pypi.org/simple/pip", - "https://pypi.org/simple/pip/", - "https://python.org/sitemap.xml", - ], -) -def test_get_html_response_no_head(url): - """ - `_get_html_response()` shouldn't send a HEAD request if the URL does not - look like an archive, only the GET request that retrieves data. - """ - session = mock.Mock(PipSession) - - # Mock the headers dict to ensure it is accessed. - session.get.return_value = mock.Mock(headers=mock.Mock(**{ - "get.return_value": "text/html", - })) - - resp = _get_html_response(url, session=session) - - assert resp is not None - assert session.head.call_count == 0 - assert session.get.mock_calls == [ - mock.call(url, headers={ - "Accept": "text/html", "Cache-Control": "max-age=0", - }), - mock.call().raise_for_status(), - mock.call().headers.get("Content-Type", ""), - ] - - -def test_get_html_response_dont_log_clear_text_password(caplog): - """ - `_get_html_response()` should redact the password from the index URL - in its DEBUG log message. - """ - session = mock.Mock(PipSession) - - # Mock the headers dict to ensure it is accessed. - session.get.return_value = mock.Mock(headers=mock.Mock(**{ - "get.return_value": "text/html", - })) - - caplog.set_level(logging.DEBUG) - - resp = _get_html_response( - "https://user:my_password@example.com/simple/", session=session - ) - - assert resp is not None - - assert len(caplog.records) == 1 - record = caplog.records[0] - assert record.levelname == 'DEBUG' - assert record.message.splitlines() == [ - "Getting page https://user:****@example.com/simple/", - ] - - -@pytest.mark.parametrize( - "url, vcs_scheme", - [ - ("svn+http://pypi.org/something", "svn"), - ("git+https://github.com/pypa/pip.git", "git"), - ], -) -def test_get_html_page_invalid_scheme(caplog, url, vcs_scheme): - """`_get_html_page()` should error if an invalid scheme is given. - - Only file:, http:, https:, and ftp: are allowed. - """ - with caplog.at_level(logging.DEBUG): - page = _get_html_page(Link(url), session=mock.Mock(PipSession)) - - assert page is None - assert caplog.record_tuples == [ - ( - "pip._internal.index", - logging.DEBUG, - "Cannot look at {} URL {}".format(vcs_scheme, url), - ), - ] - - -def test_get_html_page_directory_append_index(tmpdir): - """`_get_html_page()` should append "index.html" to a directory URL. - """ - dirpath = tmpdir.mkdir("something") - dir_url = "file:///{}".format( - urllib_request.pathname2url(dirpath).lstrip("/"), - ) - - session = mock.Mock(PipSession) - with mock.patch("pip._internal.index._get_html_response") as mock_func: - _get_html_page(Link(dir_url), session=session) - assert mock_func.mock_calls == [ - mock.call( - "{}/index.html".format(dir_url.rstrip("/")), - session=session, - ), - ] diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index c99b8f5d247..7ace1ea561a 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -218,6 +218,7 @@ def test_get_legacy_build_wheel_path(caplog): def test_get_legacy_build_wheel_path__no_names(caplog): + caplog.set_level(logging.INFO) actual = call_get_legacy_build_wheel_path(caplog, names=[]) assert actual is None assert len(caplog.records) == 1 @@ -231,6 +232,7 @@ def test_get_legacy_build_wheel_path__no_names(caplog): def test_get_legacy_build_wheel_path__multiple_names(caplog): + caplog.set_level(logging.INFO) # Deliberately pass the names in non-sorted order. actual = call_get_legacy_build_wheel_path( caplog, names=['name2', 'name1'], From ad68984ec69848c3dcab34949795c86476d5f436 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam Date: Sun, 15 Sep 2019 22:09:34 +0530 Subject: [PATCH 0269/3170] Rename {build_location -> ensure_build_location} --- src/pip/_internal/req/req_install.py | 6 +++--- src/pip/_internal/wheel.py | 2 +- tests/unit/test_req_install.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 10f547502fd..48e8804dce2 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -322,7 +322,7 @@ def from_path(self): s += '->' + comes_from return s - def build_location(self, build_dir): + def ensure_build_location(self, build_dir): # type: (str) -> str assert build_dir is not None if self._temp_build_dir.path is not None: @@ -370,7 +370,7 @@ def _correct_build_location(self): old_location = self._temp_build_dir.path self._temp_build_dir.path = None - new_location = self.build_location(self._ideal_build_dir) + new_location = self.ensure_build_location(self._ideal_build_dir) if os.path.exists(new_location): raise InstallationError( 'A package already exists in %s; please remove it to continue' @@ -763,7 +763,7 @@ def ensure_has_source_dir(self, parent_dir): :return: self.source_dir """ if self.source_dir is None: - self.source_dir = self.build_location(parent_dir) + self.source_dir = self.ensure_build_location(parent_dir) # For editable installations def install_editable( diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index 05747c8fe31..5ae85a6ec44 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -1146,7 +1146,7 @@ def build( req.remove_temporary_source() # set the build directory again - name is known from # the work prepare_files did. - req.source_dir = req.build_location( + req.source_dir = req.ensure_build_location( self.preparer.build_dir ) # Update the link for this. diff --git a/tests/unit/test_req_install.py b/tests/unit/test_req_install.py index a0c7711dcae..a8eae8249bb 100644 --- a/tests/unit/test_req_install.py +++ b/tests/unit/test_req_install.py @@ -20,7 +20,7 @@ def test_tmp_build_directory(self): # Make sure we're handling it correctly with real path. requirement = InstallRequirement(None, None) tmp_dir = tempfile.mkdtemp('-build', 'pip-') - tmp_build_dir = requirement.build_location(tmp_dir) + tmp_build_dir = requirement.ensure_build_location(tmp_dir) assert ( os.path.dirname(tmp_build_dir) == os.path.realpath(os.path.dirname(tmp_dir)) From 42cbd3c7370742115e5bbcb373a0df968fc035da Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Sun, 15 Sep 2019 13:20:20 -0400 Subject: [PATCH 0270/3170] Add triage guide document. Includes basic issue tracker info, as a first step towards the full document. --- docs/html/development/index.rst | 1 + docs/html/development/issue-triage.rst | 19 +++++++++++++++++++ 2 files changed, 20 insertions(+) create mode 100644 docs/html/development/issue-triage.rst diff --git a/docs/html/development/index.rst b/docs/html/development/index.rst index 7d10230e00c..53fefc9e186 100644 --- a/docs/html/development/index.rst +++ b/docs/html/development/index.rst @@ -14,6 +14,7 @@ or the `pypa-dev mailing list`_, to ask questions or get involved. getting-started contributing + issue-triage architecture/index release-process vendoring-policy diff --git a/docs/html/development/issue-triage.rst b/docs/html/development/issue-triage.rst new file mode 100644 index 00000000000..1aad9e8cbd8 --- /dev/null +++ b/docs/html/development/issue-triage.rst @@ -0,0 +1,19 @@ +============ +Issue Triage +============ + +This serves as an introduction to issue tracking in pip as well as +how to help triage reported issues. + + +Issue Tracker +************* + +The `pip issue tracker `__ is hosted on +GitHub alongside the project. + +Currently, the issue tracker is used for bugs, feature requests, and general +user support. + +In the pip issue tracker, we make use of labels and milestones to organize and +track work. From 1f412aca1eca54c6f95ebfddde751a8a7faeba9d Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Sun, 15 Sep 2019 16:35:08 -0400 Subject: [PATCH 0271/3170] Add types and explicit signatures to SafeFileCache. --- src/pip/_internal/download.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index 7190702cb9c..7bf724a87d4 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -542,21 +542,25 @@ class SafeFileCache(FileCache): not be accessible or writable. """ - def __init__(self, directory, *args, **kwargs): + def __init__(self, directory, use_dir_lock=False): + # type: (str, bool) -> None assert directory is not None, "Cache directory must not be None." - super(SafeFileCache, self).__init__(directory, *args, **kwargs) + super(SafeFileCache, self).__init__(directory, use_dir_lock) - def get(self, *args, **kwargs): + def get(self, key): + # type: (str) -> Optional[bytes] with suppressed_cache_errors(): - return super(SafeFileCache, self).get(*args, **kwargs) + return super(SafeFileCache, self).get(key) - def set(self, *args, **kwargs): + def set(self, key, value): + # type: (str, bytes) -> None with suppressed_cache_errors(): - return super(SafeFileCache, self).set(*args, **kwargs) + return super(SafeFileCache, self).set(key, value) - def delete(self, *args, **kwargs): + def delete(self, key): + # type: (str) -> None with suppressed_cache_errors(): - return super(SafeFileCache, self).delete(*args, **kwargs) + return super(SafeFileCache, self).delete(key) class InsecureHTTPAdapter(HTTPAdapter): From 80f092d22acf904c8cfc883c7f0194c169fff53e Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Sun, 15 Sep 2019 16:47:00 -0400 Subject: [PATCH 0272/3170] Copy function to get cache path. --- src/pip/_internal/download.py | 9 +++++++++ tests/unit/test_download.py | 6 ++++++ 2 files changed, 15 insertions(+) diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index 7bf724a87d4..1716ef1aba2 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -547,6 +547,15 @@ def __init__(self, directory, use_dir_lock=False): assert directory is not None, "Cache directory must not be None." super(SafeFileCache, self).__init__(directory, use_dir_lock) + def _get_cache_path(self, name): + # type: (str) -> str + # From cachecontrol.caches.file_cache.FileCache._fn, brought into our + # class for backwards-compatibility and to avoid using a non-public + # method. + hashed = FileCache.encode(name) + parts = list(hashed[:5]) + [hashed] + return os.path.join(self.directory, *parts) + def get(self, key): # type: (str) -> Optional[bytes] with suppressed_cache_errors(): diff --git a/tests/unit/test_download.py b/tests/unit/test_download.py index 07588d1f4bd..db2033bde53 100644 --- a/tests/unit/test_download.py +++ b/tests/unit/test_download.py @@ -10,6 +10,7 @@ import pytest from mock import Mock, patch +from pip._vendor.cachecontrol.caches import FileCache import pip from pip._internal.download import ( @@ -549,6 +550,11 @@ def test_safe_delete_no_perms(self, cache_tmpdir): cache = SafeFileCache(cache_tmpdir) cache.delete("foo") + def test_cache_hashes_are_same(self, cache_tmpdir): + cache = SafeFileCache(cache_tmpdir) + key = "test key" + assert cache._get_cache_path(key) == FileCache._fn(cache, key) + class TestPipSession: From 48be7eb5e3bd2ce4020b267bfcb5174611d1409c Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Sun, 15 Sep 2019 17:01:32 -0400 Subject: [PATCH 0273/3170] Implement cache methods using plain filesystem functions. --- src/pip/_internal/download.py | 29 +++++++++++++++++++++++------ tests/unit/test_download.py | 5 ++++- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index 1716ef1aba2..fb3ea5ffdcd 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -14,6 +14,7 @@ from pip._vendor import requests, six, urllib3 from pip._vendor.cachecontrol import CacheControlAdapter +from pip._vendor.cachecontrol.cache import BaseCache from pip._vendor.cachecontrol.caches import FileCache from pip._vendor.lockfile import LockError from pip._vendor.requests.adapters import BaseAdapter, HTTPAdapter @@ -33,7 +34,12 @@ # Import ssl from compat so the initial import occurs in only one place. from pip._internal.utils.compat import HAS_TLS, ipaddress, ssl from pip._internal.utils.encoding import auto_decode -from pip._internal.utils.filesystem import check_path_owner, copy2_fixed +from pip._internal.utils.filesystem import ( + adjacent_tmp_file, + check_path_owner, + copy2_fixed, + replace, +) from pip._internal.utils.glibc import libc_ver from pip._internal.utils.misc import ( ask, @@ -44,6 +50,7 @@ build_url_from_netloc, consume, display_path, + ensure_dir, format_size, get_installed_version, hide_url, @@ -536,7 +543,7 @@ def suppressed_cache_errors(): pass -class SafeFileCache(FileCache): +class SafeFileCache(BaseCache): """ A file based cache which is safe to use even when the target directory may not be accessible or writable. @@ -545,7 +552,8 @@ class SafeFileCache(FileCache): def __init__(self, directory, use_dir_lock=False): # type: (str, bool) -> None assert directory is not None, "Cache directory must not be None." - super(SafeFileCache, self).__init__(directory, use_dir_lock) + super(SafeFileCache, self).__init__() + self.directory = directory def _get_cache_path(self, name): # type: (str) -> str @@ -558,18 +566,27 @@ def _get_cache_path(self, name): def get(self, key): # type: (str) -> Optional[bytes] + path = self._get_cache_path(key) with suppressed_cache_errors(): - return super(SafeFileCache, self).get(key) + with open(path, 'rb') as f: + return f.read() def set(self, key, value): # type: (str, bytes) -> None + path = self._get_cache_path(key) with suppressed_cache_errors(): - return super(SafeFileCache, self).set(key, value) + ensure_dir(os.path.dirname(path)) + + with adjacent_tmp_file(path) as f: + f.write(value) + + replace(f.name, path) def delete(self, key): # type: (str) -> None + path = self._get_cache_path(key) with suppressed_cache_errors(): - return super(SafeFileCache, self).delete(key) + os.remove(path) class InsecureHTTPAdapter(HTTPAdapter): diff --git a/tests/unit/test_download.py b/tests/unit/test_download.py index db2033bde53..87bc6a4ad27 100644 --- a/tests/unit/test_download.py +++ b/tests/unit/test_download.py @@ -553,7 +553,10 @@ def test_safe_delete_no_perms(self, cache_tmpdir): def test_cache_hashes_are_same(self, cache_tmpdir): cache = SafeFileCache(cache_tmpdir) key = "test key" - assert cache._get_cache_path(key) == FileCache._fn(cache, key) + fake_cache = Mock( + FileCache, directory=cache.directory, encode=FileCache.encode + ) + assert cache._get_cache_path(key) == FileCache._fn(fake_cache, key) class TestPipSession: From fb7b6329e6f419acfee9f65cb0164719a32f2edb Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Sun, 15 Sep 2019 17:02:16 -0400 Subject: [PATCH 0274/3170] Remove unused argument. --- src/pip/_internal/download.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index fb3ea5ffdcd..455031bc010 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -549,8 +549,8 @@ class SafeFileCache(BaseCache): not be accessible or writable. """ - def __init__(self, directory, use_dir_lock=False): - # type: (str, bool) -> None + def __init__(self, directory): + # type: (str) -> None assert directory is not None, "Cache directory must not be None." super(SafeFileCache, self).__init__() self.directory = directory @@ -661,7 +661,7 @@ def __init__(self, *args, **kwargs): # require manual eviction from the cache to fix it. if cache: secure_adapter = CacheControlAdapter( - cache=SafeFileCache(cache, use_dir_lock=True), + cache=SafeFileCache(cache), max_retries=retries, ) else: From 46ec707544356a585446c7e88485a0b07085f030 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Sun, 15 Sep 2019 17:03:49 -0400 Subject: [PATCH 0275/3170] Remove unraised LockError. --- src/pip/_internal/download.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index 455031bc010..577f3fb4e8f 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -16,7 +16,6 @@ from pip._vendor.cachecontrol import CacheControlAdapter from pip._vendor.cachecontrol.cache import BaseCache from pip._vendor.cachecontrol.caches import FileCache -from pip._vendor.lockfile import LockError from pip._vendor.requests.adapters import BaseAdapter, HTTPAdapter from pip._vendor.requests.auth import AuthBase, HTTPBasicAuth from pip._vendor.requests.models import CONTENT_CHUNK_SIZE, Response @@ -539,7 +538,7 @@ def suppressed_cache_errors(): """ try: yield - except (LockError, OSError, IOError): + except (OSError, IOError): pass From 51d7a973854fb8e1e7b9b79dc2c43792b5b1c22a Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Sun, 15 Sep 2019 17:06:07 -0400 Subject: [PATCH 0276/3170] Remove Lockfile. --- news/lockfile.vendor | 1 + src/pip/_vendor/__init__.py | 1 - src/pip/_vendor/lockfile.pyi | 1 - src/pip/_vendor/lockfile/LICENSE | 21 -- src/pip/_vendor/lockfile/__init__.py | 347 -------------------- src/pip/_vendor/lockfile/linklockfile.py | 73 ---- src/pip/_vendor/lockfile/mkdirlockfile.py | 84 ----- src/pip/_vendor/lockfile/pidlockfile.py | 190 ----------- src/pip/_vendor/lockfile/sqlitelockfile.py | 156 --------- src/pip/_vendor/lockfile/symlinklockfile.py | 70 ---- src/pip/_vendor/vendor.txt | 1 - 11 files changed, 1 insertion(+), 944 deletions(-) create mode 100644 news/lockfile.vendor delete mode 100644 src/pip/_vendor/lockfile.pyi delete mode 100644 src/pip/_vendor/lockfile/LICENSE delete mode 100644 src/pip/_vendor/lockfile/__init__.py delete mode 100644 src/pip/_vendor/lockfile/linklockfile.py delete mode 100644 src/pip/_vendor/lockfile/mkdirlockfile.py delete mode 100644 src/pip/_vendor/lockfile/pidlockfile.py delete mode 100644 src/pip/_vendor/lockfile/sqlitelockfile.py delete mode 100644 src/pip/_vendor/lockfile/symlinklockfile.py diff --git a/news/lockfile.vendor b/news/lockfile.vendor new file mode 100644 index 00000000000..3d58fa13807 --- /dev/null +++ b/news/lockfile.vendor @@ -0,0 +1 @@ +Remove Lockfile as a vendored dependency. diff --git a/src/pip/_vendor/__init__.py b/src/pip/_vendor/__init__.py index 2f824556113..a0fcb8e2cc4 100644 --- a/src/pip/_vendor/__init__.py +++ b/src/pip/_vendor/__init__.py @@ -64,7 +64,6 @@ def vendored(modulename): vendored("distlib") vendored("distro") vendored("html5lib") - vendored("lockfile") vendored("six") vendored("six.moves") vendored("six.moves.urllib") diff --git a/src/pip/_vendor/lockfile.pyi b/src/pip/_vendor/lockfile.pyi deleted file mode 100644 index 6e577ca7d81..00000000000 --- a/src/pip/_vendor/lockfile.pyi +++ /dev/null @@ -1 +0,0 @@ -from lockfile import * \ No newline at end of file diff --git a/src/pip/_vendor/lockfile/LICENSE b/src/pip/_vendor/lockfile/LICENSE deleted file mode 100644 index 610c0793f71..00000000000 --- a/src/pip/_vendor/lockfile/LICENSE +++ /dev/null @@ -1,21 +0,0 @@ -This is the MIT license: http://www.opensource.org/licenses/mit-license.php - -Copyright (c) 2007 Skip Montanaro. - -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. diff --git a/src/pip/_vendor/lockfile/__init__.py b/src/pip/_vendor/lockfile/__init__.py deleted file mode 100644 index a6f44a55c63..00000000000 --- a/src/pip/_vendor/lockfile/__init__.py +++ /dev/null @@ -1,347 +0,0 @@ -# -*- coding: utf-8 -*- - -""" -lockfile.py - Platform-independent advisory file locks. - -Requires Python 2.5 unless you apply 2.4.diff -Locking is done on a per-thread basis instead of a per-process basis. - -Usage: - ->>> lock = LockFile('somefile') ->>> try: -... lock.acquire() -... except AlreadyLocked: -... print 'somefile', 'is locked already.' -... except LockFailed: -... print 'somefile', 'can\\'t be locked.' -... else: -... print 'got lock' -got lock ->>> print lock.is_locked() -True ->>> lock.release() - ->>> lock = LockFile('somefile') ->>> print lock.is_locked() -False ->>> with lock: -... print lock.is_locked() -True ->>> print lock.is_locked() -False - ->>> lock = LockFile('somefile') ->>> # It is okay to lock twice from the same thread... ->>> with lock: -... lock.acquire() -... ->>> # Though no counter is kept, so you can't unlock multiple times... ->>> print lock.is_locked() -False - -Exceptions: - - Error - base class for other exceptions - LockError - base class for all locking exceptions - AlreadyLocked - Another thread or process already holds the lock - LockFailed - Lock failed for some other reason - UnlockError - base class for all unlocking exceptions - AlreadyUnlocked - File was not locked. - NotMyLock - File was locked but not by the current thread/process -""" - -from __future__ import absolute_import - -import functools -import os -import socket -import threading -import warnings - -# Work with PEP8 and non-PEP8 versions of threading module. -if not hasattr(threading, "current_thread"): - threading.current_thread = threading.currentThread -if not hasattr(threading.Thread, "get_name"): - threading.Thread.get_name = threading.Thread.getName - -__all__ = ['Error', 'LockError', 'LockTimeout', 'AlreadyLocked', - 'LockFailed', 'UnlockError', 'NotLocked', 'NotMyLock', - 'LinkFileLock', 'MkdirFileLock', 'SQLiteFileLock', - 'LockBase', 'locked'] - - -class Error(Exception): - """ - Base class for other exceptions. - - >>> try: - ... raise Error - ... except Exception: - ... pass - """ - pass - - -class LockError(Error): - """ - Base class for error arising from attempts to acquire the lock. - - >>> try: - ... raise LockError - ... except Error: - ... pass - """ - pass - - -class LockTimeout(LockError): - """Raised when lock creation fails within a user-defined period of time. - - >>> try: - ... raise LockTimeout - ... except LockError: - ... pass - """ - pass - - -class AlreadyLocked(LockError): - """Some other thread/process is locking the file. - - >>> try: - ... raise AlreadyLocked - ... except LockError: - ... pass - """ - pass - - -class LockFailed(LockError): - """Lock file creation failed for some other reason. - - >>> try: - ... raise LockFailed - ... except LockError: - ... pass - """ - pass - - -class UnlockError(Error): - """ - Base class for errors arising from attempts to release the lock. - - >>> try: - ... raise UnlockError - ... except Error: - ... pass - """ - pass - - -class NotLocked(UnlockError): - """Raised when an attempt is made to unlock an unlocked file. - - >>> try: - ... raise NotLocked - ... except UnlockError: - ... pass - """ - pass - - -class NotMyLock(UnlockError): - """Raised when an attempt is made to unlock a file someone else locked. - - >>> try: - ... raise NotMyLock - ... except UnlockError: - ... pass - """ - pass - - -class _SharedBase(object): - def __init__(self, path): - self.path = path - - def acquire(self, timeout=None): - """ - Acquire the lock. - - * If timeout is omitted (or None), wait forever trying to lock the - file. - - * If timeout > 0, try to acquire the lock for that many seconds. If - the lock period expires and the file is still locked, raise - LockTimeout. - - * If timeout <= 0, raise AlreadyLocked immediately if the file is - already locked. - """ - raise NotImplemented("implement in subclass") - - def release(self): - """ - Release the lock. - - If the file is not locked, raise NotLocked. - """ - raise NotImplemented("implement in subclass") - - def __enter__(self): - """ - Context manager support. - """ - self.acquire() - return self - - def __exit__(self, *_exc): - """ - Context manager support. - """ - self.release() - - def __repr__(self): - return "<%s: %r>" % (self.__class__.__name__, self.path) - - -class LockBase(_SharedBase): - """Base class for platform-specific lock classes.""" - def __init__(self, path, threaded=True, timeout=None): - """ - >>> lock = LockBase('somefile') - >>> lock = LockBase('somefile', threaded=False) - """ - super(LockBase, self).__init__(path) - self.lock_file = os.path.abspath(path) + ".lock" - self.hostname = socket.gethostname() - self.pid = os.getpid() - if threaded: - t = threading.current_thread() - # Thread objects in Python 2.4 and earlier do not have ident - # attrs. Worm around that. - ident = getattr(t, "ident", hash(t)) - self.tname = "-%x" % (ident & 0xffffffff) - else: - self.tname = "" - dirname = os.path.dirname(self.lock_file) - - # unique name is mostly about the current process, but must - # also contain the path -- otherwise, two adjacent locked - # files conflict (one file gets locked, creating lock-file and - # unique file, the other one gets locked, creating lock-file - # and overwriting the already existing lock-file, then one - # gets unlocked, deleting both lock-file and unique file, - # finally the last lock errors out upon releasing. - self.unique_name = os.path.join(dirname, - "%s%s.%s%s" % (self.hostname, - self.tname, - self.pid, - hash(self.path))) - self.timeout = timeout - - def is_locked(self): - """ - Tell whether or not the file is locked. - """ - raise NotImplemented("implement in subclass") - - def i_am_locking(self): - """ - Return True if this object is locking the file. - """ - raise NotImplemented("implement in subclass") - - def break_lock(self): - """ - Remove a lock. Useful if a locking thread failed to unlock. - """ - raise NotImplemented("implement in subclass") - - def __repr__(self): - return "<%s: %r -- %r>" % (self.__class__.__name__, self.unique_name, - self.path) - - -def _fl_helper(cls, mod, *args, **kwds): - warnings.warn("Import from %s module instead of lockfile package" % mod, - DeprecationWarning, stacklevel=2) - # This is a bit funky, but it's only for awhile. The way the unit tests - # are constructed this function winds up as an unbound method, so it - # actually takes three args, not two. We want to toss out self. - if not isinstance(args[0], str): - # We are testing, avoid the first arg - args = args[1:] - if len(args) == 1 and not kwds: - kwds["threaded"] = True - return cls(*args, **kwds) - - -def LinkFileLock(*args, **kwds): - """Factory function provided for backwards compatibility. - - Do not use in new code. Instead, import LinkLockFile from the - lockfile.linklockfile module. - """ - from . import linklockfile - return _fl_helper(linklockfile.LinkLockFile, "lockfile.linklockfile", - *args, **kwds) - - -def MkdirFileLock(*args, **kwds): - """Factory function provided for backwards compatibility. - - Do not use in new code. Instead, import MkdirLockFile from the - lockfile.mkdirlockfile module. - """ - from . import mkdirlockfile - return _fl_helper(mkdirlockfile.MkdirLockFile, "lockfile.mkdirlockfile", - *args, **kwds) - - -def SQLiteFileLock(*args, **kwds): - """Factory function provided for backwards compatibility. - - Do not use in new code. Instead, import SQLiteLockFile from the - lockfile.mkdirlockfile module. - """ - from . import sqlitelockfile - return _fl_helper(sqlitelockfile.SQLiteLockFile, "lockfile.sqlitelockfile", - *args, **kwds) - - -def locked(path, timeout=None): - """Decorator which enables locks for decorated function. - - Arguments: - - path: path for lockfile. - - timeout (optional): Timeout for acquiring lock. - - Usage: - @locked('/var/run/myname', timeout=0) - def myname(...): - ... - """ - def decor(func): - @functools.wraps(func) - def wrapper(*args, **kwargs): - lock = FileLock(path, timeout=timeout) - lock.acquire() - try: - return func(*args, **kwargs) - finally: - lock.release() - return wrapper - return decor - - -if hasattr(os, "link"): - from . import linklockfile as _llf - LockFile = _llf.LinkLockFile -else: - from . import mkdirlockfile as _mlf - LockFile = _mlf.MkdirLockFile - -FileLock = LockFile diff --git a/src/pip/_vendor/lockfile/linklockfile.py b/src/pip/_vendor/lockfile/linklockfile.py deleted file mode 100644 index 2ca9be04235..00000000000 --- a/src/pip/_vendor/lockfile/linklockfile.py +++ /dev/null @@ -1,73 +0,0 @@ -from __future__ import absolute_import - -import time -import os - -from . import (LockBase, LockFailed, NotLocked, NotMyLock, LockTimeout, - AlreadyLocked) - - -class LinkLockFile(LockBase): - """Lock access to a file using atomic property of link(2). - - >>> lock = LinkLockFile('somefile') - >>> lock = LinkLockFile('somefile', threaded=False) - """ - - def acquire(self, timeout=None): - try: - open(self.unique_name, "wb").close() - except IOError: - raise LockFailed("failed to create %s" % self.unique_name) - - timeout = timeout if timeout is not None else self.timeout - end_time = time.time() - if timeout is not None and timeout > 0: - end_time += timeout - - while True: - # Try and create a hard link to it. - try: - os.link(self.unique_name, self.lock_file) - except OSError: - # Link creation failed. Maybe we've double-locked? - nlinks = os.stat(self.unique_name).st_nlink - if nlinks == 2: - # The original link plus the one I created == 2. We're - # good to go. - return - else: - # Otherwise the lock creation failed. - if timeout is not None and time.time() > end_time: - os.unlink(self.unique_name) - if timeout > 0: - raise LockTimeout("Timeout waiting to acquire" - " lock for %s" % - self.path) - else: - raise AlreadyLocked("%s is already locked" % - self.path) - time.sleep(timeout is not None and timeout / 10 or 0.1) - else: - # Link creation succeeded. We're good to go. - return - - def release(self): - if not self.is_locked(): - raise NotLocked("%s is not locked" % self.path) - elif not os.path.exists(self.unique_name): - raise NotMyLock("%s is locked, but not by me" % self.path) - os.unlink(self.unique_name) - os.unlink(self.lock_file) - - def is_locked(self): - return os.path.exists(self.lock_file) - - def i_am_locking(self): - return (self.is_locked() and - os.path.exists(self.unique_name) and - os.stat(self.unique_name).st_nlink == 2) - - def break_lock(self): - if os.path.exists(self.lock_file): - os.unlink(self.lock_file) diff --git a/src/pip/_vendor/lockfile/mkdirlockfile.py b/src/pip/_vendor/lockfile/mkdirlockfile.py deleted file mode 100644 index 05a8c96ca51..00000000000 --- a/src/pip/_vendor/lockfile/mkdirlockfile.py +++ /dev/null @@ -1,84 +0,0 @@ -from __future__ import absolute_import, division - -import time -import os -import sys -import errno - -from . import (LockBase, LockFailed, NotLocked, NotMyLock, LockTimeout, - AlreadyLocked) - - -class MkdirLockFile(LockBase): - """Lock file by creating a directory.""" - def __init__(self, path, threaded=True, timeout=None): - """ - >>> lock = MkdirLockFile('somefile') - >>> lock = MkdirLockFile('somefile', threaded=False) - """ - LockBase.__init__(self, path, threaded, timeout) - # Lock file itself is a directory. Place the unique file name into - # it. - self.unique_name = os.path.join(self.lock_file, - "%s.%s%s" % (self.hostname, - self.tname, - self.pid)) - - def acquire(self, timeout=None): - timeout = timeout if timeout is not None else self.timeout - end_time = time.time() - if timeout is not None and timeout > 0: - end_time += timeout - - if timeout is None: - wait = 0.1 - else: - wait = max(0, timeout / 10) - - while True: - try: - os.mkdir(self.lock_file) - except OSError: - err = sys.exc_info()[1] - if err.errno == errno.EEXIST: - # Already locked. - if os.path.exists(self.unique_name): - # Already locked by me. - return - if timeout is not None and time.time() > end_time: - if timeout > 0: - raise LockTimeout("Timeout waiting to acquire" - " lock for %s" % - self.path) - else: - # Someone else has the lock. - raise AlreadyLocked("%s is already locked" % - self.path) - time.sleep(wait) - else: - # Couldn't create the lock for some other reason - raise LockFailed("failed to create %s" % self.lock_file) - else: - open(self.unique_name, "wb").close() - return - - def release(self): - if not self.is_locked(): - raise NotLocked("%s is not locked" % self.path) - elif not os.path.exists(self.unique_name): - raise NotMyLock("%s is locked, but not by me" % self.path) - os.unlink(self.unique_name) - os.rmdir(self.lock_file) - - def is_locked(self): - return os.path.exists(self.lock_file) - - def i_am_locking(self): - return (self.is_locked() and - os.path.exists(self.unique_name)) - - def break_lock(self): - if os.path.exists(self.lock_file): - for name in os.listdir(self.lock_file): - os.unlink(os.path.join(self.lock_file, name)) - os.rmdir(self.lock_file) diff --git a/src/pip/_vendor/lockfile/pidlockfile.py b/src/pip/_vendor/lockfile/pidlockfile.py deleted file mode 100644 index 069e85b15bd..00000000000 --- a/src/pip/_vendor/lockfile/pidlockfile.py +++ /dev/null @@ -1,190 +0,0 @@ -# -*- coding: utf-8 -*- - -# pidlockfile.py -# -# Copyright © 2008–2009 Ben Finney -# -# This is free software: you may copy, modify, and/or distribute this work -# under the terms of the Python Software Foundation License, version 2 or -# later as published by the Python Software Foundation. -# No warranty expressed or implied. See the file LICENSE.PSF-2 for details. - -""" Lockfile behaviour implemented via Unix PID files. - """ - -from __future__ import absolute_import - -import errno -import os -import time - -from . import (LockBase, AlreadyLocked, LockFailed, NotLocked, NotMyLock, - LockTimeout) - - -class PIDLockFile(LockBase): - """ Lockfile implemented as a Unix PID file. - - The lock file is a normal file named by the attribute `path`. - A lock's PID file contains a single line of text, containing - the process ID (PID) of the process that acquired the lock. - - >>> lock = PIDLockFile('somefile') - >>> lock = PIDLockFile('somefile') - """ - - def __init__(self, path, threaded=False, timeout=None): - # pid lockfiles don't support threaded operation, so always force - # False as the threaded arg. - LockBase.__init__(self, path, False, timeout) - self.unique_name = self.path - - def read_pid(self): - """ Get the PID from the lock file. - """ - return read_pid_from_pidfile(self.path) - - def is_locked(self): - """ Test if the lock is currently held. - - The lock is held if the PID file for this lock exists. - - """ - return os.path.exists(self.path) - - def i_am_locking(self): - """ Test if the lock is held by the current process. - - Returns ``True`` if the current process ID matches the - number stored in the PID file. - """ - return self.is_locked() and os.getpid() == self.read_pid() - - def acquire(self, timeout=None): - """ Acquire the lock. - - Creates the PID file for this lock, or raises an error if - the lock could not be acquired. - """ - - timeout = timeout if timeout is not None else self.timeout - end_time = time.time() - if timeout is not None and timeout > 0: - end_time += timeout - - while True: - try: - write_pid_to_pidfile(self.path) - except OSError as exc: - if exc.errno == errno.EEXIST: - # The lock creation failed. Maybe sleep a bit. - if time.time() > end_time: - if timeout is not None and timeout > 0: - raise LockTimeout("Timeout waiting to acquire" - " lock for %s" % - self.path) - else: - raise AlreadyLocked("%s is already locked" % - self.path) - time.sleep(timeout is not None and timeout / 10 or 0.1) - else: - raise LockFailed("failed to create %s" % self.path) - else: - return - - def release(self): - """ Release the lock. - - Removes the PID file to release the lock, or raises an - error if the current process does not hold the lock. - - """ - if not self.is_locked(): - raise NotLocked("%s is not locked" % self.path) - if not self.i_am_locking(): - raise NotMyLock("%s is locked, but not by me" % self.path) - remove_existing_pidfile(self.path) - - def break_lock(self): - """ Break an existing lock. - - Removes the PID file if it already exists, otherwise does - nothing. - - """ - remove_existing_pidfile(self.path) - - -def read_pid_from_pidfile(pidfile_path): - """ Read the PID recorded in the named PID file. - - Read and return the numeric PID recorded as text in the named - PID file. If the PID file cannot be read, or if the content is - not a valid PID, return ``None``. - - """ - pid = None - try: - pidfile = open(pidfile_path, 'r') - except IOError: - pass - else: - # According to the FHS 2.3 section on PID files in /var/run: - # - # The file must consist of the process identifier in - # ASCII-encoded decimal, followed by a newline character. - # - # Programs that read PID files should be somewhat flexible - # in what they accept; i.e., they should ignore extra - # whitespace, leading zeroes, absence of the trailing - # newline, or additional lines in the PID file. - - line = pidfile.readline().strip() - try: - pid = int(line) - except ValueError: - pass - pidfile.close() - - return pid - - -def write_pid_to_pidfile(pidfile_path): - """ Write the PID in the named PID file. - - Get the numeric process ID (“PID”) of the current process - and write it to the named file as a line of text. - - """ - open_flags = (os.O_CREAT | os.O_EXCL | os.O_WRONLY) - open_mode = 0o644 - pidfile_fd = os.open(pidfile_path, open_flags, open_mode) - pidfile = os.fdopen(pidfile_fd, 'w') - - # According to the FHS 2.3 section on PID files in /var/run: - # - # The file must consist of the process identifier in - # ASCII-encoded decimal, followed by a newline character. For - # example, if crond was process number 25, /var/run/crond.pid - # would contain three characters: two, five, and newline. - - pid = os.getpid() - pidfile.write("%s\n" % pid) - pidfile.close() - - -def remove_existing_pidfile(pidfile_path): - """ Remove the named PID file if it exists. - - Removing a PID file that doesn't already exist puts us in the - desired state, so we ignore the condition if the file does not - exist. - - """ - try: - os.remove(pidfile_path) - except OSError as exc: - if exc.errno == errno.ENOENT: - pass - else: - raise diff --git a/src/pip/_vendor/lockfile/sqlitelockfile.py b/src/pip/_vendor/lockfile/sqlitelockfile.py deleted file mode 100644 index f997e2444e7..00000000000 --- a/src/pip/_vendor/lockfile/sqlitelockfile.py +++ /dev/null @@ -1,156 +0,0 @@ -from __future__ import absolute_import, division - -import time -import os - -try: - unicode -except NameError: - unicode = str - -from . import LockBase, NotLocked, NotMyLock, LockTimeout, AlreadyLocked - - -class SQLiteLockFile(LockBase): - "Demonstrate SQL-based locking." - - testdb = None - - def __init__(self, path, threaded=True, timeout=None): - """ - >>> lock = SQLiteLockFile('somefile') - >>> lock = SQLiteLockFile('somefile', threaded=False) - """ - LockBase.__init__(self, path, threaded, timeout) - self.lock_file = unicode(self.lock_file) - self.unique_name = unicode(self.unique_name) - - if SQLiteLockFile.testdb is None: - import tempfile - _fd, testdb = tempfile.mkstemp() - os.close(_fd) - os.unlink(testdb) - del _fd, tempfile - SQLiteLockFile.testdb = testdb - - import sqlite3 - self.connection = sqlite3.connect(SQLiteLockFile.testdb) - - c = self.connection.cursor() - try: - c.execute("create table locks" - "(" - " lock_file varchar(32)," - " unique_name varchar(32)" - ")") - except sqlite3.OperationalError: - pass - else: - self.connection.commit() - import atexit - atexit.register(os.unlink, SQLiteLockFile.testdb) - - def acquire(self, timeout=None): - timeout = timeout if timeout is not None else self.timeout - end_time = time.time() - if timeout is not None and timeout > 0: - end_time += timeout - - if timeout is None: - wait = 0.1 - elif timeout <= 0: - wait = 0 - else: - wait = timeout / 10 - - cursor = self.connection.cursor() - - while True: - if not self.is_locked(): - # Not locked. Try to lock it. - cursor.execute("insert into locks" - " (lock_file, unique_name)" - " values" - " (?, ?)", - (self.lock_file, self.unique_name)) - self.connection.commit() - - # Check to see if we are the only lock holder. - cursor.execute("select * from locks" - " where unique_name = ?", - (self.unique_name,)) - rows = cursor.fetchall() - if len(rows) > 1: - # Nope. Someone else got there. Remove our lock. - cursor.execute("delete from locks" - " where unique_name = ?", - (self.unique_name,)) - self.connection.commit() - else: - # Yup. We're done, so go home. - return - else: - # Check to see if we are the only lock holder. - cursor.execute("select * from locks" - " where unique_name = ?", - (self.unique_name,)) - rows = cursor.fetchall() - if len(rows) == 1: - # We're the locker, so go home. - return - - # Maybe we should wait a bit longer. - if timeout is not None and time.time() > end_time: - if timeout > 0: - # No more waiting. - raise LockTimeout("Timeout waiting to acquire" - " lock for %s" % - self.path) - else: - # Someone else has the lock and we are impatient.. - raise AlreadyLocked("%s is already locked" % self.path) - - # Well, okay. We'll give it a bit longer. - time.sleep(wait) - - def release(self): - if not self.is_locked(): - raise NotLocked("%s is not locked" % self.path) - if not self.i_am_locking(): - raise NotMyLock("%s is locked, but not by me (by %s)" % - (self.unique_name, self._who_is_locking())) - cursor = self.connection.cursor() - cursor.execute("delete from locks" - " where unique_name = ?", - (self.unique_name,)) - self.connection.commit() - - def _who_is_locking(self): - cursor = self.connection.cursor() - cursor.execute("select unique_name from locks" - " where lock_file = ?", - (self.lock_file,)) - return cursor.fetchone()[0] - - def is_locked(self): - cursor = self.connection.cursor() - cursor.execute("select * from locks" - " where lock_file = ?", - (self.lock_file,)) - rows = cursor.fetchall() - return not not rows - - def i_am_locking(self): - cursor = self.connection.cursor() - cursor.execute("select * from locks" - " where lock_file = ?" - " and unique_name = ?", - (self.lock_file, self.unique_name)) - return not not cursor.fetchall() - - def break_lock(self): - cursor = self.connection.cursor() - cursor.execute("delete from locks" - " where lock_file = ?", - (self.lock_file,)) - self.connection.commit() diff --git a/src/pip/_vendor/lockfile/symlinklockfile.py b/src/pip/_vendor/lockfile/symlinklockfile.py deleted file mode 100644 index 23b41f582b9..00000000000 --- a/src/pip/_vendor/lockfile/symlinklockfile.py +++ /dev/null @@ -1,70 +0,0 @@ -from __future__ import absolute_import - -import os -import time - -from . import (LockBase, NotLocked, NotMyLock, LockTimeout, - AlreadyLocked) - - -class SymlinkLockFile(LockBase): - """Lock access to a file using symlink(2).""" - - def __init__(self, path, threaded=True, timeout=None): - # super(SymlinkLockFile).__init(...) - LockBase.__init__(self, path, threaded, timeout) - # split it back! - self.unique_name = os.path.split(self.unique_name)[1] - - def acquire(self, timeout=None): - # Hopefully unnecessary for symlink. - # try: - # open(self.unique_name, "wb").close() - # except IOError: - # raise LockFailed("failed to create %s" % self.unique_name) - timeout = timeout if timeout is not None else self.timeout - end_time = time.time() - if timeout is not None and timeout > 0: - end_time += timeout - - while True: - # Try and create a symbolic link to it. - try: - os.symlink(self.unique_name, self.lock_file) - except OSError: - # Link creation failed. Maybe we've double-locked? - if self.i_am_locking(): - # Linked to out unique name. Proceed. - return - else: - # Otherwise the lock creation failed. - if timeout is not None and time.time() > end_time: - if timeout > 0: - raise LockTimeout("Timeout waiting to acquire" - " lock for %s" % - self.path) - else: - raise AlreadyLocked("%s is already locked" % - self.path) - time.sleep(timeout / 10 if timeout is not None else 0.1) - else: - # Link creation succeeded. We're good to go. - return - - def release(self): - if not self.is_locked(): - raise NotLocked("%s is not locked" % self.path) - elif not self.i_am_locking(): - raise NotMyLock("%s is locked, but not by me" % self.path) - os.unlink(self.lock_file) - - def is_locked(self): - return os.path.islink(self.lock_file) - - def i_am_locking(self): - return (os.path.islink(self.lock_file) - and os.readlink(self.lock_file) == self.unique_name) - - def break_lock(self): - if os.path.islink(self.lock_file): # exists && link - os.unlink(self.lock_file) diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 84ff34ea1f1..513f514aca4 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -6,7 +6,6 @@ distlib==0.2.9.post0 distro==1.4.0 html5lib==1.0.1 ipaddress==1.0.22 # Only needed on 2.6 and 2.7 -lockfile==0.12.2 msgpack==0.6.1 packaging==19.0 pep517==0.5.0 From e3a6ba63967f307261ad99a6b2992e927915fcb6 Mon Sep 17 00:00:00 2001 From: Albert Tugushev Date: Mon, 16 Sep 2019 02:51:24 +0300 Subject: [PATCH 0277/3170] Use normal fixture instead of yield_fixture It's been deprecated since pytest>=2.10. See https://docs.pytest.org/en/latest/historical-notes.html#pytest-yield-fixture-decorator --- news/deprecated-yield-fixture.trivial | 2 ++ tests/conftest.py | 6 +++--- 2 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 news/deprecated-yield-fixture.trivial diff --git a/news/deprecated-yield-fixture.trivial b/news/deprecated-yield-fixture.trivial new file mode 100644 index 00000000000..552f49c2ce1 --- /dev/null +++ b/news/deprecated-yield-fixture.trivial @@ -0,0 +1,2 @@ +Use normal ``fixture`` instead of ``yield_fixture``. +It's been deprecated in pytest since 2.10 version. diff --git a/tests/conftest.py b/tests/conftest.py index ac1559788d1..5982ebf9014 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -74,7 +74,7 @@ def tmpdir_factory(request, tmpdir_factory): tmpdir_factory.getbasetemp().remove(ignore_errors=True) -@pytest.yield_fixture +@pytest.fixture def tmpdir(request, tmpdir): """ Return a temporary directory path object which is unique to each test @@ -227,7 +227,7 @@ def install_egg_link(venv, project_name, egg_info_dir): fp.write(str(egg_info_dir) + '\n.') -@pytest.yield_fixture(scope='session') +@pytest.fixture(scope='session') def virtualenv_template(request, tmpdir_factory, pip_src, setuptools_install, common_wheels): @@ -268,7 +268,7 @@ def virtualenv_template(request, tmpdir_factory, pip_src, yield venv -@pytest.yield_fixture +@pytest.fixture def virtualenv(virtualenv_template, tmpdir, isolate): """ Return a virtual environment which is unique to each test function From ddfe2be3629efb543f70e88f89d6d986217e1148 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Mon, 16 Sep 2019 12:51:30 -0400 Subject: [PATCH 0278/3170] Fix unrelated test. --- tests/unit/test_index.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/test_index.py b/tests/unit/test_index.py index b6d8afa2674..2402629cd9a 100644 --- a/tests/unit/test_index.py +++ b/tests/unit/test_index.py @@ -533,6 +533,7 @@ def test_sort_best_candidate__best_yanked_but_not_all( """ Test the best candidates being yanked, but not all. """ + caplog.set_level(logging.INFO) candidates = [ make_mock_candidate('4.0', yanked_reason='bad metadata #4'), # Put the best candidate in the middle, to test sorting. From 459c1c7e40c48ca2c10227f9cbf3667173266d97 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Mon, 16 Sep 2019 20:52:46 -0400 Subject: [PATCH 0279/3170] Validate NEWS files with rstcheck. --- .github/workflows/python-linters.yml | 14 ++++++++++++++ noxfile.py | 13 +++++++++++++ pyproject.toml | 2 +- .../automation/news/template.rst | 0 4 files changed, 28 insertions(+), 1 deletion(-) rename news/_template.rst => tools/automation/news/template.rst (100%) diff --git a/.github/workflows/python-linters.yml b/.github/workflows/python-linters.yml index bf3308b0223..0f237e90ced 100644 --- a/.github/workflows/python-linters.yml +++ b/.github/workflows/python-linters.yml @@ -44,3 +44,17 @@ jobs: run: >- python -m tox env: ${{ matrix.env }} + + news_format: + name: Check NEWS format + runs-on: ubuntu-18.04 + steps: + - uses: actions/checkout@master + - name: Set up Python + uses: actions/setup-python@v1 + with: + version: 3.7 + - name: Install nox + run: pip install nox + - name: Check NEWS format + run: nox -s validate_news diff --git a/noxfile.py b/noxfile.py index d6357b3dd7d..205c50e9f98 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1,6 +1,7 @@ """Release time helpers, executed using nox. """ +import glob import io import subprocess @@ -30,6 +31,18 @@ def get_author_list(): return sorted(authors, key=lambda x: x.lower()) +# ----------------------------------------------------------------------------- +# Ad-hoc commands +# ----------------------------------------------------------------------------- +@nox.session +def validate_news(session): + session.install("rstcheck") + + news_files = sorted(glob.glob("news/*")) + + session.run("rstcheck", *news_files) + + # ----------------------------------------------------------------------------- # Commands used during the release process # ----------------------------------------------------------------------------- diff --git a/pyproject.toml b/pyproject.toml index 919965c27cc..aa798360c5b 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -8,7 +8,7 @@ filename = "NEWS.rst" directory = "news/" title_format = "{version} ({project_date})" issue_format = "`#{issue} `_" -template = "news/_template.rst" +template = "tools/automation/news/template.rst" type = [ { name = "Process", directory = "process", showcontent = true }, { name = "Deprecations and Removals", directory = "removal", showcontent = true }, diff --git a/news/_template.rst b/tools/automation/news/template.rst similarity index 100% rename from news/_template.rst rename to tools/automation/news/template.rst From ec8bf2cc2276c0945e0c3e47284486c0e38a3e42 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Sun, 15 Sep 2019 20:47:11 -0400 Subject: [PATCH 0280/3170] Clean up source location message creation. --- src/pip/_internal/req/constructors.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index 8142c22f3f6..129e3830326 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -336,6 +336,12 @@ def install_req_from_line( extras = Requirement("placeholder" + extras_as_string.lower()).extras else: extras = () + + def with_source(text): + if not line_source: + return text + return '{} (from {})'.format(text, line_source) + if req_as_string is not None: try: req = Requirement(req_as_string) @@ -348,12 +354,8 @@ def install_req_from_line( add_msg = "= is not a valid operator. Did you mean == ?" else: add_msg = '' - if line_source is None: - source = '' - else: - source = ' (from {})'.format(line_source) - msg = ( - 'Invalid requirement: {!r}{}'.format(req_as_string, source) + msg = with_source( + 'Invalid requirement: {!r}'.format(req_as_string) ) if add_msg: msg += '\nHint: {}'.format(add_msg) From 05552610874e139e224425c754c5276d099a3851 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Sun, 15 Sep 2019 20:49:45 -0400 Subject: [PATCH 0281/3170] Move extra conversion to function. --- src/pip/_internal/req/constructors.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index 129e3830326..6122f27e650 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -74,6 +74,14 @@ def _strip_extras(path): return path_no_extras, extras +def convert_extras(extras): + # type: (Optional[str]) -> Set[str] + if extras: + return Requirement("placeholder" + extras.lower()).extras + else: + return set() + + def parse_editable(editable_req): # type: (str) -> Tuple[Optional[str], str, Optional[Set[str]]] """Parses an editable requirement into: @@ -332,10 +340,7 @@ def install_req_from_line( else: req_as_string = name - if extras_as_string: - extras = Requirement("placeholder" + extras_as_string.lower()).extras - else: - extras = () + extras = convert_extras(extras_as_string) def with_source(text): if not line_source: From 5e785993b8bbb210713d16fa5f7e4e45d5ef4692 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Fri, 6 Sep 2019 21:12:21 -0400 Subject: [PATCH 0282/3170] Normalize style. --- src/pip/_internal/req/req_install.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 78073f0d3a9..4f2506d54d4 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -876,9 +876,11 @@ def _get_archive_name(self, path, parentdir, rootdir): def archive(self, build_dir): # type: (str) -> None assert self.source_dir + create_archive = True archive_name = '%s-%s.zip' % (self.name, self.metadata["version"]) archive_path = os.path.join(build_dir, archive_name) + if os.path.exists(archive_path): response = ask_path_exists( 'The file %s exists. (i)gnore, (w)ipe, (b)ackup, (a)bort ' % @@ -902,28 +904,29 @@ def archive(self, build_dir): if not create_archive: return - with zipfile.ZipFile( - archive_path, 'w', zipfile.ZIP_DEFLATED, allowZip64=True - ) as zip: + zip_output = zipfile.ZipFile( + archive_path, 'w', zipfile.ZIP_DEFLATED, allowZip64=True, + ) + with zip_output: dir = os.path.normcase(os.path.abspath(self.setup_py_dir)) for dirpath, dirnames, filenames in os.walk(dir): if 'pip-egg-info' in dirnames: dirnames.remove('pip-egg-info') for dirname in dirnames: - dir_arcname = self._get_archive_name(dirname, - parentdir=dirpath, - rootdir=dir) + dir_arcname = self._get_archive_name( + dirname, parentdir=dirpath, rootdir=dir, + ) zipdir = zipfile.ZipInfo(dir_arcname + '/') zipdir.external_attr = 0x1ED << 16 # 0o755 - zip.writestr(zipdir, '') + zip_output.writestr(zipdir, '') for filename in filenames: if filename == PIP_DELETE_MARKER_FILENAME: continue - file_arcname = self._get_archive_name(filename, - parentdir=dirpath, - rootdir=dir) + file_arcname = self._get_archive_name( + filename, parentdir=dirpath, rootdir=dir, + ) filename = os.path.join(dirpath, filename) - zip.write(filename, file_arcname) + zip_output.write(filename, file_arcname) logger.info('Saved %s', display_path(archive_path)) From 0689cf5db70a5326b4b16e5866db6d72a9308635 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Tue, 17 Sep 2019 20:18:52 -0400 Subject: [PATCH 0283/3170] Remove function-level imports in test. --- tests/unit/test_wheel.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 7ace1ea561a..7b325374dea 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -590,20 +590,11 @@ def test_support_index_min__none_supported(self): with pytest.raises(ValueError): w.support_index_min(tags=[]) - def test_unpack_wheel_no_flatten(self): - from pip._internal.utils import misc as utils - from tempfile import mkdtemp - from shutil import rmtree - + def test_unpack_wheel_no_flatten(self, tmpdir): filepath = os.path.join(DATA_DIR, 'packages', 'meta-1.0-py2.py3-none-any.whl') - try: - tmpdir = mkdtemp() - utils.unpack_file(filepath, tmpdir, 'application/zip', None) - assert os.path.isdir(os.path.join(tmpdir, 'meta-1.0.dist-info')) - finally: - rmtree(tmpdir) - pass + unpack_file(filepath, tmpdir, 'application/zip', None) + assert os.path.isdir(os.path.join(tmpdir, 'meta-1.0.dist-info')) def test_purelib_platlib(self, data): """ From 6a5bd723ec5333d7898fb2666a3decbe7ada48a0 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Tue, 17 Sep 2019 20:27:35 -0400 Subject: [PATCH 0284/3170] Move unpack_* from utils.misc to utils.unpacking. --- src/pip/_internal/download.py | 2 +- src/pip/_internal/utils/misc.py | 167 +--------------------- src/pip/_internal/utils/unpacking.py | 200 +++++++++++++++++++++++++++ tests/unit/test_utils.py | 86 ------------ tests/unit/test_utils_unpacking.py | 92 ++++++++++++ tests/unit/test_wheel.py | 2 +- 6 files changed, 295 insertions(+), 254 deletions(-) create mode 100644 src/pip/_internal/utils/unpacking.py create mode 100644 tests/unit/test_utils_unpacking.py diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index 7190702cb9c..eaf9e2e12ab 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -54,11 +54,11 @@ rmtree, split_auth_netloc_from_url, splitext, - unpack_file, ) from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.ui import DownloadProgressProvider +from pip._internal.utils.unpacking import unpack_file from pip._internal.utils.urls import get_url_scheme, url_to_path from pip._internal.vcs import vcs diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 7448504198d..40a284781b1 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -15,8 +15,6 @@ import stat import subprocess import sys -import tarfile -import zipfile from collections import deque from pip._vendor import pkg_resources @@ -61,7 +59,6 @@ Tuple, Union, cast, ) from pip._vendor.pkg_resources import Distribution - from pip._internal.models.link import Link from pip._internal.utils.ui import SpinnerInterface VersionInfo = Tuple[int, int, int] @@ -80,7 +77,7 @@ def cast(type_, value): # type: ignore 'split_leading_dir', 'has_leading_dir', 'normalize_path', 'renames', 'get_prog', - 'unzip_file', 'untar_file', 'unpack_file', 'call_subprocess', + 'call_subprocess', 'captured_stdout', 'ensure_dir', 'ARCHIVE_EXTENSIONS', 'SUPPORTED_EXTENSIONS', 'WHEEL_EXTENSION', 'get_installed_version', 'remove_auth_from_url'] @@ -612,168 +609,6 @@ def current_umask(): return mask -def unzip_file(filename, location, flatten=True): - # type: (str, str, bool) -> None - """ - Unzip the file (with path `filename`) to the destination `location`. All - files are written based on system defaults and umask (i.e. permissions are - not preserved), except that regular file members with any execute - permissions (user, group, or world) have "chmod +x" applied after being - written. Note that for windows, any execute changes using os.chmod are - no-ops per the python docs. - """ - ensure_dir(location) - zipfp = open(filename, 'rb') - try: - zip = zipfile.ZipFile(zipfp, allowZip64=True) - leading = has_leading_dir(zip.namelist()) and flatten - for info in zip.infolist(): - name = info.filename - fn = name - if leading: - fn = split_leading_dir(name)[1] - fn = os.path.join(location, fn) - dir = os.path.dirname(fn) - if fn.endswith('/') or fn.endswith('\\'): - # A directory - ensure_dir(fn) - else: - ensure_dir(dir) - # Don't use read() to avoid allocating an arbitrarily large - # chunk of memory for the file's content - fp = zip.open(name) - try: - with open(fn, 'wb') as destfp: - shutil.copyfileobj(fp, destfp) - finally: - fp.close() - mode = info.external_attr >> 16 - # if mode and regular file and any execute permissions for - # user/group/world? - if mode and stat.S_ISREG(mode) and mode & 0o111: - # make dest file have execute for user/group/world - # (chmod +x) no-op on windows per python docs - os.chmod(fn, (0o777 - current_umask() | 0o111)) - finally: - zipfp.close() - - -def untar_file(filename, location): - # type: (str, str) -> None - """ - Untar the file (with path `filename`) to the destination `location`. - All files are written based on system defaults and umask (i.e. permissions - are not preserved), except that regular file members with any execute - permissions (user, group, or world) have "chmod +x" applied after being - written. Note that for windows, any execute changes using os.chmod are - no-ops per the python docs. - """ - ensure_dir(location) - if filename.lower().endswith('.gz') or filename.lower().endswith('.tgz'): - mode = 'r:gz' - elif filename.lower().endswith(BZ2_EXTENSIONS): - mode = 'r:bz2' - elif filename.lower().endswith(XZ_EXTENSIONS): - mode = 'r:xz' - elif filename.lower().endswith('.tar'): - mode = 'r' - else: - logger.warning( - 'Cannot determine compression type for file %s', filename, - ) - mode = 'r:*' - tar = tarfile.open(filename, mode) - try: - leading = has_leading_dir([ - member.name for member in tar.getmembers() - ]) - for member in tar.getmembers(): - fn = member.name - if leading: - # https://github.com/python/mypy/issues/1174 - fn = split_leading_dir(fn)[1] # type: ignore - path = os.path.join(location, fn) - if member.isdir(): - ensure_dir(path) - elif member.issym(): - try: - # https://github.com/python/typeshed/issues/2673 - tar._extract_member(member, path) # type: ignore - except Exception as exc: - # Some corrupt tar files seem to produce this - # (specifically bad symlinks) - logger.warning( - 'In the tar file %s the member %s is invalid: %s', - filename, member.name, exc, - ) - continue - else: - try: - fp = tar.extractfile(member) - except (KeyError, AttributeError) as exc: - # Some corrupt tar files seem to produce this - # (specifically bad symlinks) - logger.warning( - 'In the tar file %s the member %s is invalid: %s', - filename, member.name, exc, - ) - continue - ensure_dir(os.path.dirname(path)) - with open(path, 'wb') as destfp: - shutil.copyfileobj(fp, destfp) - fp.close() - # Update the timestamp (useful for cython compiled files) - # https://github.com/python/typeshed/issues/2673 - tar.utime(member, path) # type: ignore - # member have any execute permissions for user/group/world? - if member.mode & 0o111: - # make dest file have execute for user/group/world - # no-op on windows per python docs - os.chmod(path, (0o777 - current_umask() | 0o111)) - finally: - tar.close() - - -def unpack_file( - filename, # type: str - location, # type: str - content_type, # type: Optional[str] - link # type: Optional[Link] -): - # type: (...) -> None - filename = os.path.realpath(filename) - if (content_type == 'application/zip' or - filename.lower().endswith(ZIP_EXTENSIONS) or - zipfile.is_zipfile(filename)): - unzip_file( - filename, - location, - flatten=not filename.endswith('.whl') - ) - elif (content_type == 'application/x-gzip' or - tarfile.is_tarfile(filename) or - filename.lower().endswith( - TAR_EXTENSIONS + BZ2_EXTENSIONS + XZ_EXTENSIONS)): - untar_file(filename, location) - elif (content_type and content_type.startswith('text/html') and - is_svn_page(file_contents(filename))): - # We don't really care about this - from pip._internal.vcs.subversion import Subversion - hidden_url = hide_url('svn+' + link.url) - Subversion().unpack(location, url=hidden_url) - else: - # FIXME: handle? - # FIXME: magic signatures? - logger.critical( - 'Cannot unpack file %s (downloaded from %s, content-type: %s); ' - 'cannot detect archive format', - filename, location, content_type, - ) - raise InstallationError( - 'Cannot determine archive format of %s' % location - ) - - def make_command(*args): # type: (Union[str, HiddenText, CommandArgs]) -> CommandArgs """ diff --git a/src/pip/_internal/utils/unpacking.py b/src/pip/_internal/utils/unpacking.py new file mode 100644 index 00000000000..94f7a28db1b --- /dev/null +++ b/src/pip/_internal/utils/unpacking.py @@ -0,0 +1,200 @@ +"""Utilities related archives. +""" + +# The following comment should be removed at some point in the future. +# mypy: strict-optional=False + +from __future__ import absolute_import + +import logging +import os +import shutil +import stat +import tarfile +import zipfile + +from pip._internal.exceptions import InstallationError +from pip._internal.utils.misc import ( + BZ2_EXTENSIONS, + TAR_EXTENSIONS, + XZ_EXTENSIONS, + ZIP_EXTENSIONS, + current_umask, + ensure_dir, + file_contents, + has_leading_dir, + hide_url, + is_svn_page, + split_leading_dir, +) +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import Optional + + from pip._internal.models.link import Link + + +logger = logging.getLogger(__name__) + + +def unzip_file(filename, location, flatten=True): + # type: (str, str, bool) -> None + """ + Unzip the file (with path `filename`) to the destination `location`. All + files are written based on system defaults and umask (i.e. permissions are + not preserved), except that regular file members with any execute + permissions (user, group, or world) have "chmod +x" applied after being + written. Note that for windows, any execute changes using os.chmod are + no-ops per the python docs. + """ + ensure_dir(location) + zipfp = open(filename, 'rb') + try: + zip = zipfile.ZipFile(zipfp, allowZip64=True) + leading = has_leading_dir(zip.namelist()) and flatten + for info in zip.infolist(): + name = info.filename + fn = name + if leading: + fn = split_leading_dir(name)[1] + fn = os.path.join(location, fn) + dir = os.path.dirname(fn) + if fn.endswith('/') or fn.endswith('\\'): + # A directory + ensure_dir(fn) + else: + ensure_dir(dir) + # Don't use read() to avoid allocating an arbitrarily large + # chunk of memory for the file's content + fp = zip.open(name) + try: + with open(fn, 'wb') as destfp: + shutil.copyfileobj(fp, destfp) + finally: + fp.close() + mode = info.external_attr >> 16 + # if mode and regular file and any execute permissions for + # user/group/world? + if mode and stat.S_ISREG(mode) and mode & 0o111: + # make dest file have execute for user/group/world + # (chmod +x) no-op on windows per python docs + os.chmod(fn, (0o777 - current_umask() | 0o111)) + finally: + zipfp.close() + + +def untar_file(filename, location): + # type: (str, str) -> None + """ + Untar the file (with path `filename`) to the destination `location`. + All files are written based on system defaults and umask (i.e. permissions + are not preserved), except that regular file members with any execute + permissions (user, group, or world) have "chmod +x" applied after being + written. Note that for windows, any execute changes using os.chmod are + no-ops per the python docs. + """ + ensure_dir(location) + if filename.lower().endswith('.gz') or filename.lower().endswith('.tgz'): + mode = 'r:gz' + elif filename.lower().endswith(BZ2_EXTENSIONS): + mode = 'r:bz2' + elif filename.lower().endswith(XZ_EXTENSIONS): + mode = 'r:xz' + elif filename.lower().endswith('.tar'): + mode = 'r' + else: + logger.warning( + 'Cannot determine compression type for file %s', filename, + ) + mode = 'r:*' + tar = tarfile.open(filename, mode) + try: + leading = has_leading_dir([ + member.name for member in tar.getmembers() + ]) + for member in tar.getmembers(): + fn = member.name + if leading: + # https://github.com/python/mypy/issues/1174 + fn = split_leading_dir(fn)[1] # type: ignore + path = os.path.join(location, fn) + if member.isdir(): + ensure_dir(path) + elif member.issym(): + try: + # https://github.com/python/typeshed/issues/2673 + tar._extract_member(member, path) # type: ignore + except Exception as exc: + # Some corrupt tar files seem to produce this + # (specifically bad symlinks) + logger.warning( + 'In the tar file %s the member %s is invalid: %s', + filename, member.name, exc, + ) + continue + else: + try: + fp = tar.extractfile(member) + except (KeyError, AttributeError) as exc: + # Some corrupt tar files seem to produce this + # (specifically bad symlinks) + logger.warning( + 'In the tar file %s the member %s is invalid: %s', + filename, member.name, exc, + ) + continue + ensure_dir(os.path.dirname(path)) + with open(path, 'wb') as destfp: + shutil.copyfileobj(fp, destfp) + fp.close() + # Update the timestamp (useful for cython compiled files) + # https://github.com/python/typeshed/issues/2673 + tar.utime(member, path) # type: ignore + # member have any execute permissions for user/group/world? + if member.mode & 0o111: + # make dest file have execute for user/group/world + # no-op on windows per python docs + os.chmod(path, (0o777 - current_umask() | 0o111)) + finally: + tar.close() + + +def unpack_file( + filename, # type: str + location, # type: str + content_type, # type: Optional[str] + link # type: Optional[Link] +): + # type: (...) -> None + filename = os.path.realpath(filename) + if (content_type == 'application/zip' or + filename.lower().endswith(ZIP_EXTENSIONS) or + zipfile.is_zipfile(filename)): + unzip_file( + filename, + location, + flatten=not filename.endswith('.whl') + ) + elif (content_type == 'application/x-gzip' or + tarfile.is_tarfile(filename) or + filename.lower().endswith( + TAR_EXTENSIONS + BZ2_EXTENSIONS + XZ_EXTENSIONS)): + untar_file(filename, location) + elif (content_type and content_type.startswith('text/html') and + is_svn_page(file_contents(filename))): + # We don't really care about this + from pip._internal.vcs.subversion import Subversion + hidden_url = hide_url('svn+' + link.url) + Subversion().unpack(location, url=hidden_url) + else: + # FIXME: handle? + # FIXME: magic signatures? + logger.critical( + 'Cannot unpack file %s (downloaded from %s, content-type: %s); ' + 'cannot detect archive format', + filename, location, content_type, + ) + raise InstallationError( + 'Cannot determine archive format of %s' % location + ) diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 9f83fe4b144..c34dea9165f 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -62,8 +62,6 @@ rmtree_errorhandler, split_auth_from_netloc, split_auth_netloc_from_url, - untar_file, - unzip_file, ) from pip._internal.utils.setuptools_build import make_setuptools_shim_args from pip._internal.utils.temp_dir import AdjacentTempDirectory, TempDirectory @@ -303,90 +301,6 @@ def test_freeze_excludes(self, mock_dist_is_editable, assert len(dists) == 0 -class TestUnpackArchives(object): - """ - test_tar.tgz/test_tar.zip have content as follows engineered to confirm 3 - things: - 1) confirm that reg files, dirs, and symlinks get unpacked - 2) permissions are not preserved (and go by the 022 umask) - 3) reg files with *any* execute perms, get chmod +x - - file.txt 600 regular file - symlink.txt 777 symlink to file.txt - script_owner.sh 700 script where owner can execute - script_group.sh 610 script where group can execute - script_world.sh 601 script where world can execute - dir 744 directory - dir/dirfile 622 regular file - 4) the file contents are extracted correctly (though the content of - each file isn't currently unique) - - """ - - def setup(self): - self.tempdir = tempfile.mkdtemp() - self.old_mask = os.umask(0o022) - self.symlink_expected_mode = None - - def teardown(self): - os.umask(self.old_mask) - shutil.rmtree(self.tempdir, ignore_errors=True) - - def mode(self, path): - return stat.S_IMODE(os.stat(path).st_mode) - - def confirm_files(self): - # expectations based on 022 umask set above and the unpack logic that - # sets execute permissions, not preservation - for fname, expected_mode, test, expected_contents in [ - ('file.txt', 0o644, os.path.isfile, b'file\n'), - # We don't test the "symlink.txt" contents for now. - ('symlink.txt', 0o644, os.path.isfile, None), - ('script_owner.sh', 0o755, os.path.isfile, b'file\n'), - ('script_group.sh', 0o755, os.path.isfile, b'file\n'), - ('script_world.sh', 0o755, os.path.isfile, b'file\n'), - ('dir', 0o755, os.path.isdir, None), - (os.path.join('dir', 'dirfile'), 0o644, os.path.isfile, b''), - ]: - path = os.path.join(self.tempdir, fname) - if path.endswith('symlink.txt') and sys.platform == 'win32': - # no symlinks created on windows - continue - assert test(path), path - if expected_contents is not None: - with open(path, mode='rb') as f: - contents = f.read() - assert contents == expected_contents, 'fname: {}'.format(fname) - if sys.platform == 'win32': - # the permissions tests below don't apply in windows - # due to os.chmod being a noop - continue - mode = self.mode(path) - assert mode == expected_mode, ( - "mode: %s, expected mode: %s" % (mode, expected_mode) - ) - - def test_unpack_tgz(self, data): - """ - Test unpacking a *.tgz, and setting execute permissions - """ - test_file = data.packages.joinpath("test_tar.tgz") - untar_file(test_file, self.tempdir) - self.confirm_files() - # Check the timestamp of an extracted file - file_txt_path = os.path.join(self.tempdir, 'file.txt') - mtime = time.gmtime(os.stat(file_txt_path).st_mtime) - assert mtime[0:6] == (2013, 8, 16, 5, 13, 37), mtime - - def test_unpack_zip(self, data): - """ - Test unpacking a *.zip, and setting execute permissions - """ - test_file = data.packages.joinpath("test_zip.zip") - unzip_file(test_file, self.tempdir) - self.confirm_files() - - def test_rmtree_errorhandler_nonexistent_directory(tmpdir): """ Test rmtree_errorhandler ignores the given non-existing directory. diff --git a/tests/unit/test_utils_unpacking.py b/tests/unit/test_utils_unpacking.py new file mode 100644 index 00000000000..96fcb99569f --- /dev/null +++ b/tests/unit/test_utils_unpacking.py @@ -0,0 +1,92 @@ +import os +import shutil +import stat +import sys +import tempfile +import time + +from pip._internal.utils.unpacking import untar_file, unzip_file + + +class TestUnpackArchives(object): + """ + test_tar.tgz/test_tar.zip have content as follows engineered to confirm 3 + things: + 1) confirm that reg files, dirs, and symlinks get unpacked + 2) permissions are not preserved (and go by the 022 umask) + 3) reg files with *any* execute perms, get chmod +x + + file.txt 600 regular file + symlink.txt 777 symlink to file.txt + script_owner.sh 700 script where owner can execute + script_group.sh 610 script where group can execute + script_world.sh 601 script where world can execute + dir 744 directory + dir/dirfile 622 regular file + 4) the file contents are extracted correctly (though the content of + each file isn't currently unique) + + """ + + def setup(self): + self.tempdir = tempfile.mkdtemp() + self.old_mask = os.umask(0o022) + self.symlink_expected_mode = None + + def teardown(self): + os.umask(self.old_mask) + shutil.rmtree(self.tempdir, ignore_errors=True) + + def mode(self, path): + return stat.S_IMODE(os.stat(path).st_mode) + + def confirm_files(self): + # expectations based on 022 umask set above and the unpack logic that + # sets execute permissions, not preservation + for fname, expected_mode, test, expected_contents in [ + ('file.txt', 0o644, os.path.isfile, b'file\n'), + # We don't test the "symlink.txt" contents for now. + ('symlink.txt', 0o644, os.path.isfile, None), + ('script_owner.sh', 0o755, os.path.isfile, b'file\n'), + ('script_group.sh', 0o755, os.path.isfile, b'file\n'), + ('script_world.sh', 0o755, os.path.isfile, b'file\n'), + ('dir', 0o755, os.path.isdir, None), + (os.path.join('dir', 'dirfile'), 0o644, os.path.isfile, b''), + ]: + path = os.path.join(self.tempdir, fname) + if path.endswith('symlink.txt') and sys.platform == 'win32': + # no symlinks created on windows + continue + assert test(path), path + if expected_contents is not None: + with open(path, mode='rb') as f: + contents = f.read() + assert contents == expected_contents, 'fname: {}'.format(fname) + if sys.platform == 'win32': + # the permissions tests below don't apply in windows + # due to os.chmod being a noop + continue + mode = self.mode(path) + assert mode == expected_mode, ( + "mode: %s, expected mode: %s" % (mode, expected_mode) + ) + + def test_unpack_tgz(self, data): + """ + Test unpacking a *.tgz, and setting execute permissions + """ + test_file = data.packages.joinpath("test_tar.tgz") + untar_file(test_file, self.tempdir) + self.confirm_files() + # Check the timestamp of an extracted file + file_txt_path = os.path.join(self.tempdir, 'file.txt') + mtime = time.gmtime(os.stat(file_txt_path).st_mtime) + assert mtime[0:6] == (2013, 8, 16, 5, 13, 37), mtime + + def test_unpack_zip(self, data): + """ + Test unpacking a *.zip, and setting execute permissions + """ + test_file = data.packages.joinpath("test_zip.zip") + unzip_file(test_file, self.tempdir) + self.confirm_files() diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 7b325374dea..f7b2e123b02 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -13,7 +13,7 @@ from pip._internal.models.link import Link from pip._internal.req.req_install import InstallRequirement from pip._internal.utils.compat import WINDOWS -from pip._internal.utils.misc import unpack_file +from pip._internal.utils.unpacking import unpack_file from pip._internal.wheel import ( MissingCallableSuffix, _raise_for_invalid_entrypoint, From 6a8b47d20be617be7b797e8dcc219c08e58695f9 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Tue, 17 Sep 2019 22:26:17 -0400 Subject: [PATCH 0285/3170] Move file type info to utils.filetypes. --- src/pip/_internal/collector.py | 7 ++----- src/pip/_internal/index.py | 7 ++----- src/pip/_internal/models/link.py | 2 +- src/pip/_internal/req/constructors.py | 8 ++------ src/pip/_internal/utils/filetypes.py | 11 +++++++++++ src/pip/_internal/utils/misc.py | 15 +++++++-------- src/pip/_internal/utils/unpacking.py | 4 +++- 7 files changed, 28 insertions(+), 26 deletions(-) create mode 100644 src/pip/_internal/utils/filetypes.py diff --git a/src/pip/_internal/collector.py b/src/pip/_internal/collector.py index 4f7a8d30989..33b8bc016eb 100644 --- a/src/pip/_internal/collector.py +++ b/src/pip/_internal/collector.py @@ -15,11 +15,8 @@ from pip._vendor.six.moves.urllib import request as urllib_request from pip._internal.models.link import Link -from pip._internal.utils.misc import ( - ARCHIVE_EXTENSIONS, - path_to_url, - redact_auth_from_url, -) +from pip._internal.utils.filetypes import ARCHIVE_EXTENSIONS +from pip._internal.utils.misc import path_to_url, redact_auth_from_url from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.urls import url_to_path from pip._internal.vcs import is_url, vcs diff --git a/src/pip/_internal/index.py b/src/pip/_internal/index.py index 792feefb3b7..1b45c5fbc8d 100644 --- a/src/pip/_internal/index.py +++ b/src/pip/_internal/index.py @@ -24,12 +24,9 @@ from pip._internal.models.link import Link from pip._internal.models.selection_prefs import SelectionPreferences from pip._internal.models.target_python import TargetPython +from pip._internal.utils.filetypes import WHEEL_EXTENSION from pip._internal.utils.logging import indent_log -from pip._internal.utils.misc import ( - SUPPORTED_EXTENSIONS, - WHEEL_EXTENSION, - build_netloc, -) +from pip._internal.utils.misc import SUPPORTED_EXTENSIONS, build_netloc from pip._internal.utils.packaging import check_requires_python from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.urls import url_to_path diff --git a/src/pip/_internal/models/link.py b/src/pip/_internal/models/link.py index 8c9e9e0fccb..b585c93c395 100644 --- a/src/pip/_internal/models/link.py +++ b/src/pip/_internal/models/link.py @@ -3,8 +3,8 @@ from pip._vendor.six.moves.urllib import parse as urllib_parse +from pip._internal.utils.filetypes import WHEEL_EXTENSION from pip._internal.utils.misc import ( - WHEEL_EXTENSION, path_to_url, redact_auth_from_url, split_auth_from_netloc, diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index 8142c22f3f6..e7bc94c0774 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -25,12 +25,8 @@ from pip._internal.models.link import Link from pip._internal.pyproject import make_pyproject_path from pip._internal.req.req_install import InstallRequirement -from pip._internal.utils.misc import ( - ARCHIVE_EXTENSIONS, - is_installable_dir, - path_to_url, - splitext, -) +from pip._internal.utils.filetypes import ARCHIVE_EXTENSIONS +from pip._internal.utils.misc import is_installable_dir, path_to_url, splitext from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.urls import url_to_path from pip._internal.vcs import is_url, vcs diff --git a/src/pip/_internal/utils/filetypes.py b/src/pip/_internal/utils/filetypes.py new file mode 100644 index 00000000000..251e3e933d3 --- /dev/null +++ b/src/pip/_internal/utils/filetypes.py @@ -0,0 +1,11 @@ +"""Filetype information. +""" + +WHEEL_EXTENSION = '.whl' +BZ2_EXTENSIONS = ('.tar.bz2', '.tbz') +XZ_EXTENSIONS = ('.tar.xz', '.txz', '.tlz', '.tar.lz', '.tar.lzma') +ZIP_EXTENSIONS = ('.zip', WHEEL_EXTENSION) +TAR_EXTENSIONS = ('.tar.gz', '.tgz', '.tar') +ARCHIVE_EXTENSIONS = ( + ZIP_EXTENSIONS + BZ2_EXTENSIONS + TAR_EXTENSIONS + XZ_EXTENSIONS +) diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 40a284781b1..ef4ef5dc469 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -41,6 +41,12 @@ stdlib_pkgs, str_to_display, ) +from pip._internal.utils.filetypes import ( + BZ2_EXTENSIONS, + TAR_EXTENSIONS, + XZ_EXTENSIONS, + ZIP_EXTENSIONS, +) from pip._internal.utils.marker_files import write_delete_marker_file from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.virtualenv import ( @@ -79,7 +85,7 @@ def cast(type_, value): # type: ignore 'renames', 'get_prog', 'call_subprocess', 'captured_stdout', 'ensure_dir', - 'ARCHIVE_EXTENSIONS', 'SUPPORTED_EXTENSIONS', 'WHEEL_EXTENSION', + 'SUPPORTED_EXTENSIONS', 'get_installed_version', 'remove_auth_from_url'] @@ -88,13 +94,6 @@ def cast(type_, value): # type: ignore LOG_DIVIDER = '----------------------------------------' -WHEEL_EXTENSION = '.whl' -BZ2_EXTENSIONS = ('.tar.bz2', '.tbz') -XZ_EXTENSIONS = ('.tar.xz', '.txz', '.tlz', '.tar.lz', '.tar.lzma') -ZIP_EXTENSIONS = ('.zip', WHEEL_EXTENSION) -TAR_EXTENSIONS = ('.tar.gz', '.tgz', '.tar') -ARCHIVE_EXTENSIONS = ( - ZIP_EXTENSIONS + BZ2_EXTENSIONS + TAR_EXTENSIONS + XZ_EXTENSIONS) SUPPORTED_EXTENSIONS = ZIP_EXTENSIONS + TAR_EXTENSIONS try: diff --git a/src/pip/_internal/utils/unpacking.py b/src/pip/_internal/utils/unpacking.py index 94f7a28db1b..0fabbb8ddd7 100644 --- a/src/pip/_internal/utils/unpacking.py +++ b/src/pip/_internal/utils/unpacking.py @@ -14,11 +14,13 @@ import zipfile from pip._internal.exceptions import InstallationError -from pip._internal.utils.misc import ( +from pip._internal.utils.filetypes import ( BZ2_EXTENSIONS, TAR_EXTENSIONS, XZ_EXTENSIONS, ZIP_EXTENSIONS, +) +from pip._internal.utils.misc import ( current_umask, ensure_dir, file_contents, From 565636411228edf05a7d99681683a4a42b535a89 Mon Sep 17 00:00:00 2001 From: Chris Hunt Date: Tue, 17 Sep 2019 22:34:54 -0400 Subject: [PATCH 0286/3170] Move supporting functions to utils.unpacking. --- src/pip/_internal/index.py | 3 +- src/pip/_internal/utils/misc.py | 77 +-------------------------- src/pip/_internal/utils/unpacking.py | 79 ++++++++++++++++++++++++---- 3 files changed, 72 insertions(+), 87 deletions(-) diff --git a/src/pip/_internal/index.py b/src/pip/_internal/index.py index 1b45c5fbc8d..697b9935140 100644 --- a/src/pip/_internal/index.py +++ b/src/pip/_internal/index.py @@ -26,9 +26,10 @@ from pip._internal.models.target_python import TargetPython from pip._internal.utils.filetypes import WHEEL_EXTENSION from pip._internal.utils.logging import indent_log -from pip._internal.utils.misc import SUPPORTED_EXTENSIONS, build_netloc +from pip._internal.utils.misc import build_netloc from pip._internal.utils.packaging import check_requires_python from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from pip._internal.utils.unpacking import SUPPORTED_EXTENSIONS from pip._internal.utils.urls import url_to_path from pip._internal.wheel import Wheel diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index ef4ef5dc469..5f13f975c29 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -10,7 +10,6 @@ import logging import os import posixpath -import re import shutil import stat import subprocess @@ -41,12 +40,6 @@ stdlib_pkgs, str_to_display, ) -from pip._internal.utils.filetypes import ( - BZ2_EXTENSIONS, - TAR_EXTENSIONS, - XZ_EXTENSIONS, - ZIP_EXTENSIONS, -) from pip._internal.utils.marker_files import write_delete_marker_file from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.virtualenv import ( @@ -61,7 +54,7 @@ if MYPY_CHECK_RUNNING: from typing import ( - Any, AnyStr, Container, Iterable, List, Mapping, Match, Optional, Text, + Any, AnyStr, Container, Iterable, List, Mapping, Optional, Text, Tuple, Union, cast, ) from pip._vendor.pkg_resources import Distribution @@ -79,13 +72,10 @@ def cast(type_, value): # type: ignore __all__ = ['rmtree', 'display_path', 'backup_dir', 'ask', 'splitext', 'format_size', 'is_installable_dir', - 'is_svn_page', 'file_contents', - 'split_leading_dir', 'has_leading_dir', 'normalize_path', 'renames', 'get_prog', 'call_subprocess', 'captured_stdout', 'ensure_dir', - 'SUPPORTED_EXTENSIONS', 'get_installed_version', 'remove_auth_from_url'] @@ -94,21 +84,6 @@ def cast(type_, value): # type: ignore LOG_DIVIDER = '----------------------------------------' -SUPPORTED_EXTENSIONS = ZIP_EXTENSIONS + TAR_EXTENSIONS - -try: - import bz2 # noqa - SUPPORTED_EXTENSIONS += BZ2_EXTENSIONS -except ImportError: - logger.debug('bz2 module is not available') - -try: - # Only for Python 3.3+ - import lzma # noqa - SUPPORTED_EXTENSIONS += XZ_EXTENSIONS -except ImportError: - logger.debug('lzma module is not available') - def get_pip_version(): # type: () -> str @@ -327,21 +302,6 @@ def is_installable_dir(path): return False -def is_svn_page(html): - # type: (Union[str, Text]) -> Optional[Match[Union[str, Text]]] - """ - Returns true if the page appears to be the index page of an svn repository - """ - return (re.search(r'[^<]*Revision \d+:', html) and - re.search(r'Powered by (?:<a[^>]*?>)?Subversion', html, re.I)) - - -def file_contents(filename): - # type: (str) -> Text - with open(filename, 'rb') as fp: - return fp.read().decode('utf-8') - - def read_chunks(file, size=io.DEFAULT_BUFFER_SIZE): """Yield pieces of data from a file-like object until EOF.""" while True: @@ -351,34 +311,6 @@ def read_chunks(file, size=io.DEFAULT_BUFFER_SIZE): yield chunk -def split_leading_dir(path): - # type: (Union[str, Text]) -> List[Union[str, Text]] - path = path.lstrip('/').lstrip('\\') - if '/' in path and (('\\' in path and path.find('/') < path.find('\\')) or - '\\' not in path): - return path.split('/', 1) - elif '\\' in path: - return path.split('\\', 1) - else: - return [path, ''] - - -def has_leading_dir(paths): - # type: (Iterable[Union[str, Text]]) -> bool - """Returns true if all the paths have the same leading path name - (i.e., everything is in one subdirectory in an archive)""" - common_prefix = None - for path in paths: - prefix, rest = split_leading_dir(path) - if not prefix: - return False - elif common_prefix is None: - common_prefix = prefix - elif prefix != common_prefix: - return False - return True - - def normalize_path(path, resolve_symlinks=True): # type: (str, bool) -> str """ @@ -601,13 +533,6 @@ def dist_location(dist): return normalize_path(dist.location) -def current_umask(): - """Get the current umask which involves having to set it temporarily.""" - mask = os.umask(0) - os.umask(mask) - return mask - - def make_command(*args): # type: (Union[str, HiddenText, CommandArgs]) -> CommandArgs """ diff --git a/src/pip/_internal/utils/unpacking.py b/src/pip/_internal/utils/unpacking.py index 0fabbb8ddd7..c027b8b23e9 100644 --- a/src/pip/_internal/utils/unpacking.py +++ b/src/pip/_internal/utils/unpacking.py @@ -8,6 +8,7 @@ import logging import os +import re import shutil import stat import tarfile @@ -20,19 +21,11 @@ XZ_EXTENSIONS, ZIP_EXTENSIONS, ) -from pip._internal.utils.misc import ( - current_umask, - ensure_dir, - file_contents, - has_leading_dir, - hide_url, - is_svn_page, - split_leading_dir, -) +from pip._internal.utils.misc import ensure_dir, hide_url from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Optional + from typing import Iterable, List, Optional, Match, Text, Union from pip._internal.models.link import Link @@ -40,6 +33,72 @@ logger = logging.getLogger(__name__) +SUPPORTED_EXTENSIONS = ZIP_EXTENSIONS + TAR_EXTENSIONS + +try: + import bz2 # noqa + SUPPORTED_EXTENSIONS += BZ2_EXTENSIONS +except ImportError: + logger.debug('bz2 module is not available') + +try: + # Only for Python 3.3+ + import lzma # noqa + SUPPORTED_EXTENSIONS += XZ_EXTENSIONS +except ImportError: + logger.debug('lzma module is not available') + + +def current_umask(): + """Get the current umask which involves having to set it temporarily.""" + mask = os.umask(0) + os.umask(mask) + return mask + + +def file_contents(filename): + # type: (str) -> Text + with open(filename, 'rb') as fp: + return fp.read().decode('utf-8') + + +def is_svn_page(html): + # type: (Union[str, Text]) -> Optional[Match[Union[str, Text]]] + """ + Returns true if the page appears to be the index page of an svn repository + """ + return (re.search(r'<title>[^<]*Revision \d+:', html) and + re.search(r'Powered by (?:<a[^>]*?>)?Subversion', html, re.I)) + + +def split_leading_dir(path): + # type: (Union[str, Text]) -> List[Union[str, Text]] + path = path.lstrip('/').lstrip('\\') + if '/' in path and (('\\' in path and path.find('/') < path.find('\\')) or + '\\' not in path): + return path.split('/', 1) + elif '\\' in path: + return path.split('\\', 1) + else: + return [path, ''] + + +def has_leading_dir(paths): + # type: (Iterable[Union[str, Text]]) -> bool + """Returns true if all the paths have the same leading path name + (i.e., everything is in one subdirectory in an archive)""" + common_prefix = None + for path in paths: + prefix, rest = split_leading_dir(path) + if not prefix: + return False + elif common_prefix is None: + common_prefix = prefix + elif prefix != common_prefix: + return False + return True + + def unzip_file(filename, location, flatten=True): # type: (str, str, bool) -> None """ From 5a759daaff70aa34347f0977beb695f9652b306f Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 17 Sep 2019 20:54:38 -0400 Subject: [PATCH 0287/3170] Make style consistent. --- src/pip/_internal/utils/unpacking.py | 41 ++++++++++++++++++---------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/src/pip/_internal/utils/unpacking.py b/src/pip/_internal/utils/unpacking.py index c027b8b23e9..62b196668f6 100644 --- a/src/pip/_internal/utils/unpacking.py +++ b/src/pip/_internal/utils/unpacking.py @@ -67,15 +67,21 @@ def is_svn_page(html): """ Returns true if the page appears to be the index page of an svn repository """ - return (re.search(r'<title>[^<]*Revision \d+:', html) and - re.search(r'Powered by (?:<a[^>]*?>)?Subversion', html, re.I)) + return ( + re.search(r'<title>[^<]*Revision \d+:', html) and + re.search(r'Powered by (?:<a[^>]*?>)?Subversion', html, re.I) + ) def split_leading_dir(path): # type: (Union[str, Text]) -> List[Union[str, Text]] path = path.lstrip('/').lstrip('\\') - if '/' in path and (('\\' in path and path.find('/') < path.find('\\')) or - '\\' not in path): + if ( + '/' in path and ( + ('\\' in path and path.find('/') < path.find('\\')) or + '\\' not in path + ) + ): return path.split('/', 1) elif '\\' in path: return path.split('\\', 1) @@ -229,21 +235,28 @@ def unpack_file( ): # type: (...) -> None filename = os.path.realpath(filename) - if (content_type == 'application/zip' or - filename.lower().endswith(ZIP_EXTENSIONS) or - zipfile.is_zipfile(filename)): + if ( + content_type == 'application/zip' or + filename.lower().endswith(ZIP_EXTENSIONS) or + zipfile.is_zipfile(filename) + ): unzip_file( filename, location, flatten=not filename.endswith('.whl') ) - elif (content_type == 'application/x-gzip' or - tarfile.is_tarfile(filename) or - filename.lower().endswith( - TAR_EXTENSIONS + BZ2_EXTENSIONS + XZ_EXTENSIONS)): + elif ( + content_type == 'application/x-gzip' or + tarfile.is_tarfile(filename) or + filename.lower().endswith( + TAR_EXTENSIONS + BZ2_EXTENSIONS + XZ_EXTENSIONS + ) + ): untar_file(filename, location) - elif (content_type and content_type.startswith('text/html') and - is_svn_page(file_contents(filename))): + elif ( + content_type and content_type.startswith('text/html') and + is_svn_page(file_contents(filename)) + ): # We don't really care about this from pip._internal.vcs.subversion import Subversion hidden_url = hide_url('svn+' + link.url) @@ -257,5 +270,5 @@ def unpack_file( filename, location, content_type, ) raise InstallationError( - 'Cannot determine archive format of %s' % location + 'Cannot determine archive format of {}'.format(location) ) From 6a55f0788b872ffc2f749f1ed8d47f981308d442 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek <chris.jerdonek@gmail.com> Date: Sat, 14 Sep 2019 10:38:05 -0700 Subject: [PATCH 0288/3170] Move make_search_scope() to outdated.py. --- src/pip/_internal/cli/cmdoptions.py | 26 ---------- src/pip/_internal/cli/req_command.py | 3 +- src/pip/_internal/commands/list.py | 2 +- src/pip/_internal/utils/outdated.py | 33 ++++++++++++- tests/unit/test_cmdoptions.py | 71 +--------------------------- tests/unit/test_unit_outdated.py | 64 +++++++++++++++++++++++++ 6 files changed, 98 insertions(+), 101 deletions(-) diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index 155211903b1..d8a6ebd54b6 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -24,10 +24,8 @@ from pip._internal.locations import USER_CACHE_DIR, get_src_prefix from pip._internal.models.format_control import FormatControl from pip._internal.models.index import PyPI -from pip._internal.models.search_scope import SearchScope from pip._internal.models.target_python import TargetPython from pip._internal.utils.hashes import STRONG_HASHES -from pip._internal.utils.misc import redact_auth_from_url from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.ui import BAR_TYPES @@ -359,30 +357,6 @@ def find_links(): ) -def make_search_scope(options, suppress_no_index=False): - # type: (Values, bool) -> SearchScope - """ - :param suppress_no_index: Whether to ignore the --no-index option - when constructing the SearchScope object. - """ - index_urls = [options.index_url] + options.extra_index_urls - if options.no_index and not suppress_no_index: - logger.debug( - 'Ignoring indexes: %s', - ','.join(redact_auth_from_url(url) for url in index_urls), - ) - index_urls = [] - - # Make sure find_links is a list before passing to create(). - find_links = options.find_links or [] - - search_scope = SearchScope.create( - find_links=find_links, index_urls=index_urls, - ) - - return search_scope - - def trusted_host(): # type: () -> Option return Option( diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index e1fd495e4c9..6f42a682494 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -9,7 +9,6 @@ from functools import partial from pip._internal.cli.base_command import Command -from pip._internal.cli.cmdoptions import make_search_scope from pip._internal.cli.command_context import CommandContextMixIn from pip._internal.download import PipSession from pip._internal.exceptions import CommandError @@ -24,7 +23,7 @@ ) from pip._internal.req.req_file import parse_requirements from pip._internal.utils.misc import normalize_path -from pip._internal.utils.outdated import pip_version_check +from pip._internal.utils.outdated import make_search_scope, pip_version_check from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index 6b80d58021e..0d3bf55d171 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -7,7 +7,6 @@ from pip._vendor.six.moves import zip_longest from pip._internal.cli import cmdoptions -from pip._internal.cli.cmdoptions import make_search_scope from pip._internal.cli.req_command import IndexGroupCommand from pip._internal.exceptions import CommandError from pip._internal.index import PackageFinder @@ -17,6 +16,7 @@ get_installed_distributions, write_output, ) +from pip._internal.utils.outdated import make_search_scope from pip._internal.utils.packaging import get_installer logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/utils/outdated.py b/src/pip/_internal/utils/outdated.py index 241599b274a..03d16fd2f41 100644 --- a/src/pip/_internal/utils/outdated.py +++ b/src/pip/_internal/utils/outdated.py @@ -11,8 +11,8 @@ from pip._vendor.packaging import version as packaging_version from pip._vendor.six import ensure_binary -from pip._internal.cli.cmdoptions import make_search_scope from pip._internal.index import PackageFinder +from pip._internal.models.search_scope import SearchScope from pip._internal.models.selection_prefs import SelectionPreferences from pip._internal.utils.compat import WINDOWS from pip._internal.utils.filesystem import ( @@ -20,12 +20,17 @@ check_path_owner, replace, ) -from pip._internal.utils.misc import ensure_dir, get_installed_version +from pip._internal.utils.misc import ( + ensure_dir, + get_installed_version, + redact_auth_from_url, +) from pip._internal.utils.packaging import get_installer from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: import optparse + from optparse import Values from typing import Any, Dict, Text, Union from pip._internal.download import PipSession @@ -36,6 +41,30 @@ logger = logging.getLogger(__name__) +def make_search_scope(options, suppress_no_index=False): + # type: (Values, bool) -> SearchScope + """ + :param suppress_no_index: Whether to ignore the --no-index option + when constructing the SearchScope object. + """ + index_urls = [options.index_url] + options.extra_index_urls + if options.no_index and not suppress_no_index: + logger.debug( + 'Ignoring indexes: %s', + ','.join(redact_auth_from_url(url) for url in index_urls), + ) + index_urls = [] + + # Make sure find_links is a list before passing to create(). + find_links = options.find_links or [] + + search_scope = SearchScope.create( + find_links=find_links, index_urls=index_urls, + ) + + return search_scope + + def _get_statefile_name(key): # type: (Union[str, Text]) -> str key_bytes = ensure_binary(key) diff --git a/tests/unit/test_cmdoptions.py b/tests/unit/test_cmdoptions.py index 3cf2ba8b9ab..150570e716e 100644 --- a/tests/unit/test_cmdoptions.py +++ b/tests/unit/test_cmdoptions.py @@ -1,75 +1,6 @@ -import os - -import pretend import pytest -from mock import patch - -from pip._internal.cli.cmdoptions import ( - _convert_python_version, - make_search_scope, -) - - -@pytest.mark.parametrize( - 'find_links, no_index, suppress_no_index, expected', [ - (['link1'], False, False, - (['link1'], ['default_url', 'url1', 'url2'])), - (['link1'], False, True, (['link1'], ['default_url', 'url1', 'url2'])), - (['link1'], True, False, (['link1'], [])), - # Passing suppress_no_index=True suppresses no_index=True. - (['link1'], True, True, (['link1'], ['default_url', 'url1', 'url2'])), - # Test options.find_links=False. - (False, False, False, ([], ['default_url', 'url1', 'url2'])), - ], -) -def test_make_search_scope(find_links, no_index, suppress_no_index, expected): - """ - :param expected: the expected (find_links, index_urls) values. - """ - expected_find_links, expected_index_urls = expected - options = pretend.stub( - find_links=find_links, - index_url='default_url', - extra_index_urls=['url1', 'url2'], - no_index=no_index, - ) - search_scope = make_search_scope( - options, suppress_no_index=suppress_no_index, - ) - assert search_scope.find_links == expected_find_links - assert search_scope.index_urls == expected_index_urls - - -@patch('pip._internal.utils.misc.expanduser') -def test_make_search_scope__find_links_expansion(mock_expanduser, tmpdir): - """ - Test "~" expansion in --find-links paths. - """ - # This is a mock version of expanduser() that expands "~" to the tmpdir. - def expand_path(path): - if path.startswith('~/'): - path = os.path.join(tmpdir, path[2:]) - return path - - mock_expanduser.side_effect = expand_path - - options = pretend.stub( - find_links=['~/temp1', '~/temp2'], - index_url='default_url', - extra_index_urls=[], - no_index=False, - ) - # Only create temp2 and not temp1 to test that "~" expansion only occurs - # when the directory exists. - temp2_dir = os.path.join(tmpdir, 'temp2') - os.mkdir(temp2_dir) - - search_scope = make_search_scope(options) - # Only ~/temp2 gets expanded. Also, the path is normalized when expanded. - expected_temp2_dir = os.path.normcase(temp2_dir) - assert search_scope.find_links == ['~/temp1', expected_temp2_dir] - assert search_scope.index_urls == ['default_url'] +from pip._internal.cli.cmdoptions import _convert_python_version @pytest.mark.parametrize('value, expected', [ diff --git a/tests/unit/test_unit_outdated.py b/tests/unit/test_unit_outdated.py index d7230288b02..d4f0524792d 100644 --- a/tests/unit/test_unit_outdated.py +++ b/tests/unit/test_unit_outdated.py @@ -6,6 +6,7 @@ import freezegun import pretend import pytest +from mock import patch from pip._vendor import pkg_resources from pip._internal.index import InstallationCandidate @@ -13,11 +14,74 @@ from pip._internal.utils.outdated import ( SelfCheckState, logger, + make_search_scope, pip_version_check, ) from tests.lib.path import Path +@pytest.mark.parametrize( + 'find_links, no_index, suppress_no_index, expected', [ + (['link1'], False, False, + (['link1'], ['default_url', 'url1', 'url2'])), + (['link1'], False, True, (['link1'], ['default_url', 'url1', 'url2'])), + (['link1'], True, False, (['link1'], [])), + # Passing suppress_no_index=True suppresses no_index=True. + (['link1'], True, True, (['link1'], ['default_url', 'url1', 'url2'])), + # Test options.find_links=False. + (False, False, False, ([], ['default_url', 'url1', 'url2'])), + ], +) +def test_make_search_scope(find_links, no_index, suppress_no_index, expected): + """ + :param expected: the expected (find_links, index_urls) values. + """ + expected_find_links, expected_index_urls = expected + options = pretend.stub( + find_links=find_links, + index_url='default_url', + extra_index_urls=['url1', 'url2'], + no_index=no_index, + ) + search_scope = make_search_scope( + options, suppress_no_index=suppress_no_index, + ) + assert search_scope.find_links == expected_find_links + assert search_scope.index_urls == expected_index_urls + + +@patch('pip._internal.utils.misc.expanduser') +def test_make_search_scope__find_links_expansion(mock_expanduser, tmpdir): + """ + Test "~" expansion in --find-links paths. + """ + # This is a mock version of expanduser() that expands "~" to the tmpdir. + def expand_path(path): + if path.startswith('~/'): + path = os.path.join(tmpdir, path[2:]) + return path + + mock_expanduser.side_effect = expand_path + + options = pretend.stub( + find_links=['~/temp1', '~/temp2'], + index_url='default_url', + extra_index_urls=[], + no_index=False, + ) + # Only create temp2 and not temp1 to test that "~" expansion only occurs + # when the directory exists. + temp2_dir = os.path.join(tmpdir, 'temp2') + os.mkdir(temp2_dir) + + search_scope = make_search_scope(options) + + # Only ~/temp2 gets expanded. Also, the path is normalized when expanded. + expected_temp2_dir = os.path.normcase(temp2_dir) + assert search_scope.find_links == ['~/temp1', expected_temp2_dir] + assert search_scope.index_urls == ['default_url'] + + class MockBestCandidateResult(object): def __init__(self, best): self.best_candidate = best From 6d94944efd254adf800f883d7ec127cd3850a2d8 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek <chris.jerdonek@gmail.com> Date: Sat, 14 Sep 2019 11:08:32 -0700 Subject: [PATCH 0289/3170] Change PackageFinder.create() to accept a LinkCollector. --- src/pip/_internal/cli/req_command.py | 7 ++-- src/pip/_internal/commands/list.py | 7 ++-- src/pip/_internal/index.py | 17 +-------- src/pip/_internal/utils/outdated.py | 23 ++++++++--- tests/lib/__init__.py | 29 ++++++++++---- tests/unit/test_build_env.py | 9 +++-- tests/unit/test_collector.py | 29 ++------------ tests/unit/test_index.py | 57 ++++++++++++++++++---------- tests/unit/test_unit_outdated.py | 22 ++++++++--- 9 files changed, 109 insertions(+), 91 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 6f42a682494..9a2d4196f6e 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -23,7 +23,7 @@ ) from pip._internal.req.req_file import parse_requirements from pip._internal.utils.misc import normalize_path -from pip._internal.utils.outdated import make_search_scope, pip_version_check +from pip._internal.utils.outdated import make_link_collector, pip_version_check from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: @@ -282,7 +282,7 @@ def _build_package_finder( :param ignore_requires_python: Whether to ignore incompatible "Requires-Python" values in links. Defaults to False. """ - search_scope = make_search_scope(options) + link_collector = make_link_collector(session, options=options) selection_prefs = SelectionPreferences( allow_yanked=True, format_control=options.format_control, @@ -292,8 +292,7 @@ def _build_package_finder( ) return PackageFinder.create( - search_scope=search_scope, + link_collector=link_collector, selection_prefs=selection_prefs, - session=session, target_python=target_python, ) diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index 0d3bf55d171..d6e38db8659 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -16,7 +16,7 @@ get_installed_distributions, write_output, ) -from pip._internal.utils.outdated import make_search_scope +from pip._internal.utils.outdated import make_link_collector from pip._internal.utils.packaging import get_installer logger = logging.getLogger(__name__) @@ -116,7 +116,7 @@ def _build_package_finder(self, options, session): """ Create a package finder appropriate to this list command. """ - search_scope = make_search_scope(options) + link_collector = make_link_collector(session, options=options) # Pass allow_yanked=False to ignore yanked versions. selection_prefs = SelectionPreferences( @@ -125,9 +125,8 @@ def _build_package_finder(self, options, session): ) return PackageFinder.create( - search_scope=search_scope, + link_collector=link_collector, selection_prefs=selection_prefs, - session=session, ) def run(self, options, args): diff --git a/src/pip/_internal/index.py b/src/pip/_internal/index.py index 697b9935140..4db14eb1aa8 100644 --- a/src/pip/_internal/index.py +++ b/src/pip/_internal/index.py @@ -12,7 +12,6 @@ from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.packaging.version import parse as parse_version -from pip._internal.collector import LinkCollector from pip._internal.exceptions import ( BestVersionAlreadyInstalled, DistributionNotFound, @@ -38,9 +37,9 @@ Any, FrozenSet, Iterable, List, Optional, Set, Text, Tuple, ) from pip._vendor.packaging.version import _BaseVersion + from pip._internal.collector import LinkCollector from pip._internal.models.search_scope import SearchScope from pip._internal.req import InstallRequirement - from pip._internal.download import PipSession from pip._internal.pep425tags import Pep425Tag from pip._internal.utils.hashes import Hashes @@ -638,9 +637,8 @@ def __init__( @classmethod def create( cls, - search_scope, # type: SearchScope + link_collector, # type: LinkCollector selection_prefs, # type: SelectionPreferences - session=None, # type: Optional[PipSession] target_python=None, # type: Optional[TargetPython] ): # type: (...) -> PackageFinder @@ -648,16 +646,10 @@ def create( :param selection_prefs: The candidate selection preferences, as a SelectionPreferences object. - :param session: The Session to use to make requests. :param target_python: The target Python interpreter to use when checking compatibility. If None (the default), a TargetPython object will be constructed from the running Python. """ - if session is None: - raise TypeError( - "PackageFinder.create() missing 1 required keyword argument: " - "'session'" - ) if target_python is None: target_python = TargetPython() @@ -666,11 +658,6 @@ def create( allow_all_prereleases=selection_prefs.allow_all_prereleases, ) - link_collector = LinkCollector( - session=session, - search_scope=search_scope, - ) - return cls( candidate_prefs=candidate_prefs, link_collector=link_collector, diff --git a/src/pip/_internal/utils/outdated.py b/src/pip/_internal/utils/outdated.py index 03d16fd2f41..9383fc2e059 100644 --- a/src/pip/_internal/utils/outdated.py +++ b/src/pip/_internal/utils/outdated.py @@ -11,6 +11,7 @@ from pip._vendor.packaging import version as packaging_version from pip._vendor.six import ensure_binary +from pip._internal.collector import LinkCollector from pip._internal.index import PackageFinder from pip._internal.models.search_scope import SearchScope from pip._internal.models.selection_prefs import SelectionPreferences @@ -41,9 +42,14 @@ logger = logging.getLogger(__name__) -def make_search_scope(options, suppress_no_index=False): - # type: (Values, bool) -> SearchScope +def make_link_collector( + session, # type: PipSession + options, # type: Values + suppress_no_index=False, # type: bool +): + # type: (...) -> LinkCollector """ + :param session: The Session to use to make requests. :param suppress_no_index: Whether to ignore the --no-index option when constructing the SearchScope object. """ @@ -62,7 +68,9 @@ def make_search_scope(options, suppress_no_index=False): find_links=find_links, index_urls=index_urls, ) - return search_scope + link_collector = LinkCollector(session=session, search_scope=search_scope) + + return link_collector def _get_statefile_name(key): @@ -176,7 +184,11 @@ def pip_version_check(session, options): # Refresh the version if we need to or just see if we need to warn if pypi_version is None: # Lets use PackageFinder to see what the latest pip version is - search_scope = make_search_scope(options, suppress_no_index=True) + link_collector = make_link_collector( + session, + options=options, + suppress_no_index=True, + ) # Pass allow_yanked=False so we don't suggest upgrading to a # yanked version. @@ -186,9 +198,8 @@ def pip_version_check(session, options): ) finder = PackageFinder.create( - search_scope=search_scope, + link_collector=link_collector, selection_prefs=selection_prefs, - session=session, ) best_candidate = finder.find_best_candidate("pip").best_candidate if best_candidate is None: diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index b7046b34be6..a1ddd23f1d0 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -13,6 +13,7 @@ import pytest from scripttest import FoundDir, TestFileEnvironment +from pip._internal.collector import LinkCollector from pip._internal.download import PipSession from pip._internal.index import PackageFinder from pip._internal.locations import get_major_minor_version @@ -89,11 +90,28 @@ def make_test_search_scope( if index_urls is None: index_urls = [] - return SearchScope.create( + return SearchScope.create(find_links=find_links, index_urls=index_urls) + + +def make_test_link_collector( + find_links=None, # type: Optional[List[str]] + index_urls=None, # type: Optional[List[str]] + session=None, # type: Optional[PipSession] +): + # type: (...) -> LinkCollector + """ + Create a LinkCollector object for testing purposes. + """ + if session is None: + session = PipSession() + + search_scope = make_test_search_scope( find_links=find_links, index_urls=index_urls, ) + return LinkCollector(session=session, search_scope=search_scope) + def make_test_finder( find_links=None, # type: Optional[List[str]] @@ -106,12 +124,10 @@ def make_test_finder( """ Create a PackageFinder for testing purposes. """ - if session is None: - session = PipSession() - - search_scope = make_test_search_scope( + link_collector = make_test_link_collector( find_links=find_links, index_urls=index_urls, + session=session, ) selection_prefs = SelectionPreferences( allow_yanked=True, @@ -119,9 +135,8 @@ def make_test_finder( ) return PackageFinder.create( - search_scope=search_scope, + link_collector=link_collector, selection_prefs=selection_prefs, - session=session, target_python=target_python, ) diff --git a/tests/unit/test_build_env.py b/tests/unit/test_build_env.py index 9bfd19aa281..3e3c7ce9fcb 100644 --- a/tests/unit/test_build_env.py +++ b/tests/unit/test_build_env.py @@ -22,6 +22,7 @@ def run_with_build_env(script, setup_script_contents, import sys from pip._internal.build_env import BuildEnvironment + from pip._internal.collector import LinkCollector from pip._internal.download import PipSession from pip._internal.index import PackageFinder from pip._internal.models.search_scope import SearchScope @@ -29,14 +30,16 @@ def run_with_build_env(script, setup_script_contents, SelectionPreferences ) - search_scope = SearchScope.create([%r], []) + link_collector = LinkCollector( + session=PipSession(), + search_scope=SearchScope.create([%r], []), + ) selection_prefs = SelectionPreferences( allow_yanked=True, ) finder = PackageFinder.create( - search_scope, + link_collector=link_collector, selection_prefs=selection_prefs, - session=PipSession(), ) build_env = BuildEnvironment() diff --git a/tests/unit/test_collector.py b/tests/unit/test_collector.py index 059ba40fd55..1e90948ebce 100644 --- a/tests/unit/test_collector.py +++ b/tests/unit/test_collector.py @@ -10,7 +10,6 @@ from pip._internal.collector import ( HTMLPage, - LinkCollector, _clean_link, _determine_base_url, _get_html_page, @@ -22,11 +21,7 @@ from pip._internal.download import PipSession from pip._internal.models.index import PyPI from pip._internal.models.link import Link -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from tests.lib import make_test_search_scope - -if MYPY_CHECK_RUNNING: - from typing import List, Optional +from tests.lib import make_test_link_collector @pytest.mark.parametrize( @@ -388,25 +383,6 @@ def make_fake_html_page(url): return HTMLPage(content, url=url, headers=headers) -def make_test_link_collector( - find_links=None, # type: Optional[List[str]] -): - # type: (...) -> LinkCollector - """ - Create a LinkCollector object for testing purposes. - """ - session = PipSession() - search_scope = make_test_search_scope( - find_links=find_links, - index_urls=[PyPI.simple_url], - ) - - return LinkCollector( - session=session, - search_scope=search_scope, - ) - - def check_links_include(links, names): """ Assert that the given list of Link objects includes, for each of the @@ -428,7 +404,8 @@ def test_collect_links(self, mock_get_html_response, data): mock_get_html_response.return_value = fake_page link_collector = make_test_link_collector( - find_links=[data.find_links] + find_links=[data.find_links], + index_urls=[PyPI.simple_url], ) actual = link_collector.collect_links('twine') diff --git a/tests/unit/test_index.py b/tests/unit/test_index.py index b6d8afa2674..bf1c457c43b 100644 --- a/tests/unit/test_index.py +++ b/tests/unit/test_index.py @@ -3,12 +3,12 @@ import pytest from pip._vendor.packaging.specifiers import SpecifierSet +from pip._internal.collector import LinkCollector from pip._internal.download import PipSession from pip._internal.index import ( CandidateEvaluator, CandidatePreferences, FormatControl, - LinkCollector, LinkEvaluator, PackageFinder, _check_link_requires_python, @@ -564,15 +564,18 @@ def test_create__candidate_prefs( """ Test that the _candidate_prefs attribute is set correctly. """ + link_collector = LinkCollector( + session=PipSession(), + search_scope=SearchScope([], []), + ) selection_prefs = SelectionPreferences( allow_yanked=True, allow_all_prereleases=allow_all_prereleases, prefer_binary=prefer_binary, ) finder = PackageFinder.create( - search_scope=SearchScope([], []), + link_collector=link_collector, selection_prefs=selection_prefs, - session=PipSession(), ) candidate_prefs = finder._candidate_prefs assert candidate_prefs.allow_all_prereleases == allow_all_prereleases @@ -582,27 +585,29 @@ def test_create__link_collector(self): """ Test that the _link_collector attribute is set correctly. """ - search_scope = SearchScope([], []) - session = PipSession() + link_collector = LinkCollector( + session=PipSession(), + search_scope=SearchScope([], []), + ) finder = PackageFinder.create( - search_scope=search_scope, + link_collector=link_collector, selection_prefs=SelectionPreferences(allow_yanked=True), - session=session, ) - actual_link_collector = finder._link_collector - assert actual_link_collector.search_scope is search_scope - assert actual_link_collector.session is session + assert finder._link_collector is link_collector def test_create__target_python(self): """ Test that the _target_python attribute is set correctly. """ + link_collector = LinkCollector( + session=PipSession(), + search_scope=SearchScope([], []), + ) target_python = TargetPython(py_version_info=(3, 7, 3)) finder = PackageFinder.create( - search_scope=SearchScope([], []), + link_collector=link_collector, selection_prefs=SelectionPreferences(allow_yanked=True), - session=PipSession(), target_python=target_python, ) actual_target_python = finder._target_python @@ -615,10 +620,13 @@ def test_create__target_python_none(self): """ Test passing target_python=None. """ - finder = PackageFinder.create( + link_collector = LinkCollector( + session=PipSession(), search_scope=SearchScope([], []), + ) + finder = PackageFinder.create( + link_collector=link_collector, selection_prefs=SelectionPreferences(allow_yanked=True), - session=PipSession(), target_python=None, ) # Spot-check the default TargetPython object. @@ -631,11 +639,14 @@ def test_create__allow_yanked(self, allow_yanked): """ Test that the _allow_yanked attribute is set correctly. """ + link_collector = LinkCollector( + session=PipSession(), + search_scope=SearchScope([], []), + ) selection_prefs = SelectionPreferences(allow_yanked=allow_yanked) finder = PackageFinder.create( - search_scope=SearchScope([], []), + link_collector=link_collector, selection_prefs=selection_prefs, - session=PipSession(), ) assert finder._allow_yanked == allow_yanked @@ -644,14 +655,17 @@ def test_create__ignore_requires_python(self, ignore_requires_python): """ Test that the _ignore_requires_python attribute is set correctly. """ + link_collector = LinkCollector( + session=PipSession(), + search_scope=SearchScope([], []), + ) selection_prefs = SelectionPreferences( allow_yanked=True, ignore_requires_python=ignore_requires_python, ) finder = PackageFinder.create( - search_scope=SearchScope([], []), + link_collector=link_collector, selection_prefs=selection_prefs, - session=PipSession(), ) assert finder._ignore_requires_python == ignore_requires_python @@ -659,15 +673,18 @@ def test_create__format_control(self): """ Test that the format_control attribute is set correctly. """ + link_collector = LinkCollector( + session=PipSession(), + search_scope=SearchScope([], []), + ) format_control = FormatControl(set(), {':all:'}) selection_prefs = SelectionPreferences( allow_yanked=True, format_control=format_control, ) finder = PackageFinder.create( - search_scope=SearchScope([], []), + link_collector=link_collector, selection_prefs=selection_prefs, - session=PipSession(), ) actual_format_control = finder.format_control assert actual_format_control is format_control diff --git a/tests/unit/test_unit_outdated.py b/tests/unit/test_unit_outdated.py index d4f0524792d..4dd5b16cb93 100644 --- a/tests/unit/test_unit_outdated.py +++ b/tests/unit/test_unit_outdated.py @@ -9,12 +9,13 @@ from mock import patch from pip._vendor import pkg_resources +from pip._internal.download import PipSession from pip._internal.index import InstallationCandidate from pip._internal.utils import outdated from pip._internal.utils.outdated import ( SelfCheckState, logger, - make_search_scope, + make_link_collector, pip_version_check, ) from tests.lib.path import Path @@ -32,26 +33,33 @@ (False, False, False, ([], ['default_url', 'url1', 'url2'])), ], ) -def test_make_search_scope(find_links, no_index, suppress_no_index, expected): +def test_make_link_collector( + find_links, no_index, suppress_no_index, expected, +): """ :param expected: the expected (find_links, index_urls) values. """ expected_find_links, expected_index_urls = expected + session = PipSession() options = pretend.stub( find_links=find_links, index_url='default_url', extra_index_urls=['url1', 'url2'], no_index=no_index, ) - search_scope = make_search_scope( - options, suppress_no_index=suppress_no_index, + link_collector = make_link_collector( + session, options=options, suppress_no_index=suppress_no_index, ) + + assert link_collector.session is session + + search_scope = link_collector.search_scope assert search_scope.find_links == expected_find_links assert search_scope.index_urls == expected_index_urls @patch('pip._internal.utils.misc.expanduser') -def test_make_search_scope__find_links_expansion(mock_expanduser, tmpdir): +def test_make_link_collector__find_links_expansion(mock_expanduser, tmpdir): """ Test "~" expansion in --find-links paths. """ @@ -63,6 +71,7 @@ def expand_path(path): mock_expanduser.side_effect = expand_path + session = PipSession() options = pretend.stub( find_links=['~/temp1', '~/temp2'], index_url='default_url', @@ -74,8 +83,9 @@ def expand_path(path): temp2_dir = os.path.join(tmpdir, 'temp2') os.mkdir(temp2_dir) - search_scope = make_search_scope(options) + link_collector = make_link_collector(session, options=options) + search_scope = link_collector.search_scope # Only ~/temp2 gets expanded. Also, the path is normalized when expanded. expected_temp2_dir = os.path.normcase(temp2_dir) assert search_scope.find_links == ['~/temp1', expected_temp2_dir] From 75c8bceb49603e9524573ee9a0fa1c9891733497 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek <chris.jerdonek@gmail.com> Date: Wed, 18 Sep 2019 21:39:02 -0700 Subject: [PATCH 0290/3170] Add parse_links(). --- src/pip/_internal/collector.py | 49 +++++++++++++++++++++++----------- 1 file changed, 34 insertions(+), 15 deletions(-) diff --git a/src/pip/_internal/collector.py b/src/pip/_internal/collector.py index 33b8bc016eb..443e22a7f5b 100644 --- a/src/pip/_internal/collector.py +++ b/src/pip/_internal/collector.py @@ -34,6 +34,7 @@ from pip._internal.download import PipSession HTMLElement = xml.etree.ElementTree.Element + ResponseHeaders = MutableMapping[str, str] logger = logging.getLogger(__name__) @@ -154,6 +155,7 @@ def _get_html_response(url, session): def _get_encoding_from_headers(headers): + # type: (Optional[ResponseHeaders]) -> Optional[str] """Determine if we have any encoding information in our headers. """ if headers and "Content-Type" in headers: @@ -164,6 +166,7 @@ def _get_encoding_from_headers(headers): def _determine_base_url(document, page_url): + # type: (HTMLElement, str) -> str """Determine the HTML document's base URL. This looks for a ``<base>`` tag in the HTML document. If present, its href @@ -240,11 +243,39 @@ def _create_link_from_element( return link +def parse_links( + html, # type: bytes + encoding, # type: Optional[str] + url, # type: str +): + # type: (...) -> Iterable[Link] + """ + Parse an HTML document, and yield its anchor elements as Link objects. + + :param url: the URL from which the HTML was downloaded. + """ + document = html5lib.parse( + html, + transport_encoding=encoding, + namespaceHTMLElements=False, + ) + base_url = _determine_base_url(document, url) + for anchor in document.findall(".//a"): + link = _create_link_from_element( + anchor, + page_url=url, + base_url=base_url, + ) + if link is None: + continue + yield link + + class HTMLPage(object): """Represents one page, along with its URL""" def __init__(self, content, url, headers=None): - # type: (bytes, str, MutableMapping[str, str]) -> None + # type: (bytes, str, ResponseHeaders) -> None self.content = content self.url = url self.headers = headers @@ -255,20 +286,8 @@ def __str__(self): def iter_links(self): # type: () -> Iterable[Link] """Yields all links in the page""" - document = html5lib.parse( - self.content, - transport_encoding=_get_encoding_from_headers(self.headers), - namespaceHTMLElements=False, - ) - base_url = _determine_base_url(document, self.url) - for anchor in document.findall(".//a"): - link = _create_link_from_element( - anchor, - page_url=self.url, - base_url=base_url, - ) - if link is None: - continue + encoding = _get_encoding_from_headers(self.headers) + for link in parse_links(self.content, encoding=encoding, url=self.url): yield link From 8648ff568f50d1a80c3f9236b2605b7ac4a7b4bb Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 19 Sep 2019 00:58:47 -0400 Subject: [PATCH 0291/3170] Removed unused Subversion().unpack in unpack_file `download.unpack_url` already calls `unpack_vcs_link`, it looks like this was leftover from before that change. --- src/pip/_internal/utils/unpacking.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/pip/_internal/utils/unpacking.py b/src/pip/_internal/utils/unpacking.py index 62b196668f6..c0379b6bf8e 100644 --- a/src/pip/_internal/utils/unpacking.py +++ b/src/pip/_internal/utils/unpacking.py @@ -21,7 +21,7 @@ XZ_EXTENSIONS, ZIP_EXTENSIONS, ) -from pip._internal.utils.misc import ensure_dir, hide_url +from pip._internal.utils.misc import ensure_dir from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: @@ -253,14 +253,6 @@ def unpack_file( ) ): untar_file(filename, location) - elif ( - content_type and content_type.startswith('text/html') and - is_svn_page(file_contents(filename)) - ): - # We don't really care about this - from pip._internal.vcs.subversion import Subversion - hidden_url = hide_url('svn+' + link.url) - Subversion().unpack(location, url=hidden_url) else: # FIXME: handle? # FIXME: magic signatures? From 89bbe7349831bedeb860435b9e8bfb00c58fd942 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 19 Sep 2019 01:03:28 -0400 Subject: [PATCH 0292/3170] Remove unused argument in unpacking.unpack_file --- src/pip/_internal/download.py | 4 ++-- src/pip/_internal/utils/unpacking.py | 3 --- tests/unit/test_wheel.py | 8 ++++---- 3 files changed, 6 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index eaf9e2e12ab..14dbb1d815f 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -1024,7 +1024,7 @@ def unpack_http_url( # 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_file(from_path, location, content_type) # a download dir is specified; let's copy the archive there if download_dir and not already_downloaded_path: @@ -1120,7 +1120,7 @@ def unpack_file_url( # 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_file(from_path, location, content_type) # a download dir is specified and not already downloaded if download_dir and not already_downloaded_path: diff --git a/src/pip/_internal/utils/unpacking.py b/src/pip/_internal/utils/unpacking.py index c0379b6bf8e..bcc6832fa39 100644 --- a/src/pip/_internal/utils/unpacking.py +++ b/src/pip/_internal/utils/unpacking.py @@ -27,8 +27,6 @@ if MYPY_CHECK_RUNNING: from typing import Iterable, List, Optional, Match, Text, Union - from pip._internal.models.link import Link - logger = logging.getLogger(__name__) @@ -231,7 +229,6 @@ def unpack_file( filename, # type: str location, # type: str content_type, # type: Optional[str] - link # type: Optional[Link] ): # type: (...) -> None filename = os.path.realpath(filename) diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index f7b2e123b02..3cf47d51de4 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -370,9 +370,9 @@ def test_wheel_version(tmpdir, data): future_version = (1, 9) unpack_file(data.packages.joinpath(future_wheel), - tmpdir + 'future', None, None) + tmpdir + 'future', None) unpack_file(data.packages.joinpath(broken_wheel), - tmpdir + 'broken', None, None) + tmpdir + 'broken', None) assert wheel.wheel_version(tmpdir + 'future') == future_version assert not wheel.wheel_version(tmpdir + 'broken') @@ -593,7 +593,7 @@ def test_support_index_min__none_supported(self): def test_unpack_wheel_no_flatten(self, tmpdir): filepath = os.path.join(DATA_DIR, 'packages', 'meta-1.0-py2.py3-none-any.whl') - unpack_file(filepath, tmpdir, 'application/zip', None) + unpack_file(filepath, tmpdir, 'application/zip') assert os.path.isdir(os.path.join(tmpdir, 'meta-1.0.dist-info')) def test_purelib_platlib(self, data): @@ -633,7 +633,7 @@ def prep(self, data, tmpdir): self.req = Requirement('sample') self.src = os.path.join(tmpdir, 'src') self.dest = os.path.join(tmpdir, 'dest') - unpack_file(self.wheelpath, self.src, None, None) + unpack_file(self.wheelpath, self.src, None) self.scheme = { 'scripts': os.path.join(self.dest, 'bin'), 'purelib': os.path.join(self.dest, 'lib'), From ea6eb1f898e27b0fff9a3b5fc9f7036054dfbacf Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 19 Sep 2019 01:38:05 -0400 Subject: [PATCH 0293/3170] Remove unused functions --- src/pip/_internal/utils/unpacking.py | 20 +------------------- 1 file changed, 1 insertion(+), 19 deletions(-) diff --git a/src/pip/_internal/utils/unpacking.py b/src/pip/_internal/utils/unpacking.py index bcc6832fa39..92424da5ad3 100644 --- a/src/pip/_internal/utils/unpacking.py +++ b/src/pip/_internal/utils/unpacking.py @@ -8,7 +8,6 @@ import logging import os -import re import shutil import stat import tarfile @@ -25,7 +24,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Iterable, List, Optional, Match, Text, Union + from typing import Iterable, List, Optional, Text, Union logger = logging.getLogger(__name__) @@ -54,23 +53,6 @@ def current_umask(): return mask -def file_contents(filename): - # type: (str) -> Text - with open(filename, 'rb') as fp: - return fp.read().decode('utf-8') - - -def is_svn_page(html): - # type: (Union[str, Text]) -> Optional[Match[Union[str, Text]]] - """ - Returns true if the page appears to be the index page of an svn repository - """ - return ( - re.search(r'<title>[^<]*Revision \d+:', html) and - re.search(r'Powered by (?:<a[^>]*?>)?Subversion', html, re.I) - ) - - def split_leading_dir(path): # type: (Union[str, Text]) -> List[Union[str, Text]] path = path.lstrip('/').lstrip('\\') From b6f7470185ab84453f4d847d98ba30f75678ed2a Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 19 Sep 2019 01:47:01 -0400 Subject: [PATCH 0294/3170] Add news --- news/7037.removal | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 news/7037.removal diff --git a/news/7037.removal b/news/7037.removal new file mode 100644 index 00000000000..577a02f5e46 --- /dev/null +++ b/news/7037.removal @@ -0,0 +1,2 @@ +Remove undocumented support for http:// requirements pointing to SVN +repositories. From 80339aab2c3c26f8971efb7256ffbdc76f7708c8 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 19 Sep 2019 01:53:02 -0400 Subject: [PATCH 0295/3170] Add WIP note --- docs/html/development/issue-triage.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/docs/html/development/issue-triage.rst b/docs/html/development/issue-triage.rst index 1aad9e8cbd8..621b9e6a453 100644 --- a/docs/html/development/issue-triage.rst +++ b/docs/html/development/issue-triage.rst @@ -1,3 +1,9 @@ +.. note:: + This section of the documentation is currently being written. pip + developers welcome your help to complete this documentation. If you're + interested in helping out, please let us know in the + `tracking issue <https://github.com/pypa/pip/issues/6583>`__. + ============ Issue Triage ============ From 061be18ed3121bc7a2a2032e187f3478336378da Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 19 Sep 2019 14:17:52 +0530 Subject: [PATCH 0296/3170] Cleanup RequirementSet str/repr computation --- src/pip/_internal/req/req_set.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/req/req_set.py b/src/pip/_internal/req/req_set.py index 269a045d592..a28018b3439 100644 --- a/src/pip/_internal/req/req_set.py +++ b/src/pip/_internal/req/req_set.py @@ -39,18 +39,25 @@ def __init__(self, require_hashes=False, check_supported_wheels=True): def __str__(self): # type: () -> str - 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]) + requirements = sorted( + (req for req in self.requirements.values() if not req.comes_from), + key=lambda req: req.name.lower(), + ) + return ' '.join(str(req.req) for req in requirements) def __repr__(self): # type: () -> str - 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)) + requirements = sorted( + self.requirements.values(), + key=lambda req: req.name.lower(), + ) + + format_string = '<{classname} object; {count} requirement(s): {reqs}>' + return format_string.format( + classname=self.__class__.__name__, + count=len(requirements), + reqs=', '.join(str(req.req) for req in requirements), + ) def add_unnamed_requirement(self, install_req): # type: (InstallRequirement) -> None From bd26e768c784026db7d290d49dc94624379efa7f Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 19 Sep 2019 14:20:58 +0530 Subject: [PATCH 0297/3170] Drop "name = install_req.name" It didn't help with formatting style and added just enough confusion when reading that I think this is a worthwhile change. --- src/pip/_internal/req/req_set.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/req/req_set.py b/src/pip/_internal/req/req_set.py index a28018b3439..9111808d2dd 100644 --- a/src/pip/_internal/req/req_set.py +++ b/src/pip/_internal/req/req_set.py @@ -95,13 +95,11 @@ def add_requirement( the requirement is not applicable, or [install_req] if the requirement is applicable and has just been added. """ - name = install_req.name - # If the markers do not match, ignore this requirement. if not install_req.match_markers(extras_requested): logger.info( "Ignoring %s: markers '%s' don't match your environment", - name, install_req.markers, + install_req.name, install_req.markers, ) return [], None @@ -126,12 +124,12 @@ def add_requirement( # Unnamed requirements are scanned again and the requirement won't be # added as a dependency until after scanning. - if not name: + if not install_req.name: self.add_unnamed_requirement(install_req) return [install_req], None try: - existing_req = self.get_requirement(name) + existing_req = self.get_requirement(install_req.name) except KeyError: existing_req = None @@ -145,7 +143,7 @@ def add_requirement( if has_conflicting_requirement: raise InstallationError( "Double requirement given: %s (already in %s, name=%r)" - % (install_req, existing_req, name) + % (install_req, existing_req, install_req.name) ) # When no existing requirement exists, add the requirement as a @@ -172,7 +170,7 @@ def add_requirement( raise InstallationError( "Could not satisfy constraints for '%s': " "installation from path or url cannot be " - "constrained to a version" % name, + "constrained to a version" % install_req.name, ) # If we're now installing a constraint, mark the existing # object for real installation. From 1bc6b94dc197a4e293c69e9ae2401824c8faeaad Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 19 Sep 2019 14:52:57 +0530 Subject: [PATCH 0298/3170] Canonicalize names stored in RequirementSet --- src/pip/_internal/req/req_set.py | 46 +++++++++++++++----------------- 1 file changed, 21 insertions(+), 25 deletions(-) diff --git a/src/pip/_internal/req/req_set.py b/src/pip/_internal/req/req_set.py index 9111808d2dd..b34a2bb11b8 100644 --- a/src/pip/_internal/req/req_set.py +++ b/src/pip/_internal/req/req_set.py @@ -6,6 +6,8 @@ import logging from collections import OrderedDict +from pip._vendor.packaging.utils import canonicalize_name + from pip._internal import pep425tags from pip._internal.exceptions import InstallationError from pip._internal.utils.logging import indent_log @@ -31,8 +33,6 @@ def __init__(self, require_hashes=False, check_supported_wheels=True): self.require_hashes = require_hashes self.check_supported_wheels = check_supported_wheels - # Mapping of alias: real_name - self.requirement_aliases = {} # type: Dict[str, str] self.unnamed_requirements = [] # type: List[InstallRequirement] self.successfully_downloaded = [] # type: List[InstallRequirement] self.reqs_to_cleanup = [] # type: List[InstallRequirement] @@ -41,7 +41,7 @@ def __str__(self): # type: () -> str requirements = sorted( (req for req in self.requirements.values() if not req.comes_from), - key=lambda req: req.name.lower(), + key=lambda req: canonicalize_name(req.name), ) return ' '.join(str(req.req) for req in requirements) @@ -49,7 +49,7 @@ def __repr__(self): # type: () -> str requirements = sorted( self.requirements.values(), - key=lambda req: req.name.lower(), + key=lambda req: canonicalize_name(req.name), ) format_string = '<{classname} object; {count} requirement(s): {reqs}>' @@ -67,12 +67,9 @@ def add_unnamed_requirement(self, install_req): def add_named_requirement(self, install_req): # type: (InstallRequirement) -> None assert install_req.name - name = install_req.name - self.requirements[name] = install_req - # FIXME: what about other normalizations? E.g., _ vs. -? - if name.lower() != name: - self.requirement_aliases[name.lower()] = name + project_name = canonicalize_name(install_req.name) + self.requirements[project_name] = install_req def add_requirement( self, @@ -186,24 +183,23 @@ def add_requirement( # scanning again. return [existing_req], existing_req - def has_requirement(self, project_name): + def has_requirement(self, name): # type: (str) -> bool - 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 - - def get_requirement(self, project_name): + project_name = canonicalize_name(name) + + return ( + project_name in self.requirements and + not self.requirements[project_name].constraint + ) + + def get_requirement(self, name): # type: (str) -> InstallRequirement - 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) + project_name = canonicalize_name(name) + + if project_name in self.requirements: + return self.requirements[project_name] + + raise KeyError("No project with the name %r" % name) def cleanup_files(self): # type: () -> None From 8dac3a046b11fa304e65b6ce04a0693ec230133f Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 19 Sep 2019 15:05:31 +0530 Subject: [PATCH 0299/3170] Move isolation setup code into a helper method --- src/pip/_internal/distributions/source/legacy.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/distributions/source/legacy.py b/src/pip/_internal/distributions/source/legacy.py index e5d9fd4bf80..a29f3d76ac4 100644 --- a/src/pip/_internal/distributions/source/legacy.py +++ b/src/pip/_internal/distributions/source/legacy.py @@ -29,7 +29,12 @@ def prepare_distribution_metadata(self, finder, build_isolation): self.req.load_pyproject_toml() should_isolate = self.req.use_pep517 and build_isolation + self._setup_isolation(should_isolate, finder) + self.req.prepare_metadata() + self.req.assert_source_matches_version() + + def _setup_isolation(self, should_isolate, finder): def _raise_conflicts(conflicting_with, conflicting_reqs): raise InstallationError( "Some build dependencies for %s conflict with %s: %s." % ( @@ -75,6 +80,3 @@ def _raise_conflicts(conflicting_with, conflicting_reqs): finder, missing, 'normal', "Installing backend dependencies" ) - - self.req.prepare_metadata() - self.req.assert_source_matches_version() From b22333e44e67c070eeb9b569ee5b898ae8b0a95a Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 19 Sep 2019 15:19:06 +0530 Subject: [PATCH 0300/3170] Linearize a nested-error-message-generation --- .../_internal/distributions/source/legacy.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/distributions/source/legacy.py b/src/pip/_internal/distributions/source/legacy.py index e5d9fd4bf80..45c0dde6b71 100644 --- a/src/pip/_internal/distributions/source/legacy.py +++ b/src/pip/_internal/distributions/source/legacy.py @@ -31,11 +31,19 @@ def prepare_distribution_metadata(self, finder, build_isolation): should_isolate = self.req.use_pep517 and build_isolation def _raise_conflicts(conflicting_with, conflicting_reqs): - raise InstallationError( - "Some build dependencies for %s conflict with %s: %s." % ( - self.req, conflicting_with, ', '.join( - '%s is incompatible with %s' % (installed, wanted) - for installed, wanted in sorted(conflicting)))) + format_string = ( + "Some build dependencies for {requirement} " + "conflict with {conflicting_with}: {description}." + ) + error_message = format_string.format( + requirement=self.req, + conflicting_with=conflicting_with, + description=', '.join( + '%s is incompatible with %s' % (installed, wanted) + for installed, wanted in sorted(conflicting) + ) + ) + raise InstallationError(error_message) if should_isolate: # Isolate in a BuildEnvironment and install the build-time From f051201d08218cec2948819b87c851d63e1240e5 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 19 Sep 2019 15:06:38 +0530 Subject: [PATCH 0301/3170] Invoke method only when build isolation is needed --- .../_internal/distributions/source/legacy.py | 76 +++++++++---------- 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/src/pip/_internal/distributions/source/legacy.py b/src/pip/_internal/distributions/source/legacy.py index a29f3d76ac4..17f3d169313 100644 --- a/src/pip/_internal/distributions/source/legacy.py +++ b/src/pip/_internal/distributions/source/legacy.py @@ -29,12 +29,13 @@ def prepare_distribution_metadata(self, finder, build_isolation): self.req.load_pyproject_toml() should_isolate = self.req.use_pep517 and build_isolation - self._setup_isolation(should_isolate, finder) + if should_isolate: + self._setup_isolation(finder) self.req.prepare_metadata() self.req.assert_source_matches_version() - def _setup_isolation(self, should_isolate, finder): + def _setup_isolation(self, finder): def _raise_conflicts(conflicting_with, conflicting_reqs): raise InstallationError( "Some build dependencies for %s conflict with %s: %s." % ( @@ -42,41 +43,40 @@ def _raise_conflicts(conflicting_with, conflicting_reqs): '%s is incompatible with %s' % (installed, wanted) for installed, wanted in sorted(conflicting)))) - if should_isolate: - # Isolate in a BuildEnvironment and install the build-time - # requirements. - self.req.build_env = BuildEnvironment() - self.req.build_env.install_requirements( - finder, self.req.pyproject_requires, 'overlay', - "Installing build dependencies" - ) - conflicting, missing = self.req.build_env.check_requirements( - self.req.requirements_to_check + # Isolate in a BuildEnvironment and install the build-time + # requirements. + self.req.build_env = BuildEnvironment() + self.req.build_env.install_requirements( + finder, self.req.pyproject_requires, 'overlay', + "Installing build dependencies" + ) + conflicting, missing = self.req.build_env.check_requirements( + self.req.requirements_to_check + ) + if conflicting: + _raise_conflicts("PEP 517/518 supported requirements", + conflicting) + if missing: + logger.warning( + "Missing build requirements in pyproject.toml for %s.", + self.req, ) - if conflicting: - _raise_conflicts("PEP 517/518 supported requirements", - conflicting) - if missing: - logger.warning( - "Missing build requirements in pyproject.toml for %s.", - self.req, - ) - logger.warning( - "The project does not specify a build backend, and " - "pip cannot fall back to setuptools without %s.", - " and ".join(map(repr, sorted(missing))) - ) - # Install any extra build dependencies that the backend requests. - # This must be done in a second pass, as the pyproject.toml - # dependencies must be installed before we can call the backend. - with self.req.build_env: - # We need to have the env active when calling the hook. - self.req.spin_message = "Getting requirements to build wheel" - reqs = self.req.pep517_backend.get_requires_for_build_wheel() - conflicting, missing = self.req.build_env.check_requirements(reqs) - if conflicting: - _raise_conflicts("the backend dependencies", conflicting) - self.req.build_env.install_requirements( - finder, missing, 'normal', - "Installing backend dependencies" + logger.warning( + "The project does not specify a build backend, and " + "pip cannot fall back to setuptools without %s.", + " and ".join(map(repr, sorted(missing))) ) + # Install any extra build dependencies that the backend requests. + # This must be done in a second pass, as the pyproject.toml + # dependencies must be installed before we can call the backend. + with self.req.build_env: + # We need to have the env active when calling the hook. + self.req.spin_message = "Getting requirements to build wheel" + reqs = self.req.pep517_backend.get_requires_for_build_wheel() + conflicting, missing = self.req.build_env.check_requirements(reqs) + if conflicting: + _raise_conflicts("the backend dependencies", conflicting) + self.req.build_env.install_requirements( + finder, missing, 'normal', + "Installing backend dependencies" + ) From 8d19b31b66b9981c92e5af77ce762d76ab851a0d Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 19 Sep 2019 19:28:54 -0400 Subject: [PATCH 0302/3170] Remove unnecessary else in convert_extras --- src/pip/_internal/req/constructors.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index 702f4392f96..d762141d0e8 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -72,10 +72,9 @@ def _strip_extras(path): def convert_extras(extras): # type: (Optional[str]) -> Set[str] - if extras: - return Requirement("placeholder" + extras.lower()).extras - else: + if not extras: return set() + return Requirement("placeholder" + extras.lower()).extras def parse_editable(editable_req): From 2aeed371e5bfba637a6970c0fc75d6379f2667d2 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 19 Sep 2019 20:02:47 -0400 Subject: [PATCH 0303/3170] Make content_type optional in unpack_file. --- src/pip/_internal/utils/unpacking.py | 2 +- tests/unit/test_wheel.py | 10 ++++------ 2 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/utils/unpacking.py b/src/pip/_internal/utils/unpacking.py index 92424da5ad3..dcfe5819e5b 100644 --- a/src/pip/_internal/utils/unpacking.py +++ b/src/pip/_internal/utils/unpacking.py @@ -210,7 +210,7 @@ def untar_file(filename, location): def unpack_file( filename, # type: str location, # type: str - content_type, # type: Optional[str] + content_type=None, # type: Optional[str] ): # type: (...) -> None filename = os.path.realpath(filename) diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 3cf47d51de4..2dee519be9e 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -369,10 +369,8 @@ def test_wheel_version(tmpdir, data): broken_wheel = 'brokenwheel-1.0-py2.py3-none-any.whl' future_version = (1, 9) - unpack_file(data.packages.joinpath(future_wheel), - tmpdir + 'future', None) - unpack_file(data.packages.joinpath(broken_wheel), - tmpdir + 'broken', None) + unpack_file(data.packages.joinpath(future_wheel), tmpdir + 'future') + unpack_file(data.packages.joinpath(broken_wheel), tmpdir + 'broken') assert wheel.wheel_version(tmpdir + 'future') == future_version assert not wheel.wheel_version(tmpdir + 'broken') @@ -593,7 +591,7 @@ def test_support_index_min__none_supported(self): def test_unpack_wheel_no_flatten(self, tmpdir): filepath = os.path.join(DATA_DIR, 'packages', 'meta-1.0-py2.py3-none-any.whl') - unpack_file(filepath, tmpdir, 'application/zip') + unpack_file(filepath, tmpdir) assert os.path.isdir(os.path.join(tmpdir, 'meta-1.0.dist-info')) def test_purelib_platlib(self, data): @@ -633,7 +631,7 @@ def prep(self, data, tmpdir): self.req = Requirement('sample') self.src = os.path.join(tmpdir, 'src') self.dest = os.path.join(tmpdir, 'dest') - unpack_file(self.wheelpath, self.src, None) + unpack_file(self.wheelpath, self.src) self.scheme = { 'scripts': os.path.join(self.dest, 'bin'), 'purelib': os.path.join(self.dest, 'lib'), From 767b0287242cfa42eb3d9c468b23261ae81b6858 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 19 Sep 2019 20:07:25 -0400 Subject: [PATCH 0304/3170] Drop usage of `url_without_fragment` in favor of `url` `url_to_path` discards fragment anyway (5th element of urllib_parse.urlsplit) --- src/pip/_internal/download.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index 763dc369ac3..7b9f65bd659 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -883,7 +883,7 @@ def is_dir_url(link): first. """ - link_path = url_to_path(link.url_without_fragment) + link_path = url_to_path(link.url) return os.path.isdir(link_path) @@ -1115,7 +1115,7 @@ def unpack_file_url( If download_dir is provided and link points to a file, make a copy of the link file inside download_dir. """ - link_path = url_to_path(link.url_without_fragment) + link_path = url_to_path(link.url) # If it's a url to a local directory if is_dir_url(link): if os.path.isdir(location): From 59b0a79bdeabe523cdbc621c7d8cb2d20b8f4c4c Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 19 Sep 2019 20:14:10 -0400 Subject: [PATCH 0305/3170] Add Link.file_path property --- src/pip/_internal/download.py | 4 ++-- src/pip/_internal/models/link.py | 6 ++++++ src/pip/_internal/operations/prepare.py | 2 +- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index 7b9f65bd659..a4a1d6856b3 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -883,7 +883,7 @@ def is_dir_url(link): first. """ - link_path = url_to_path(link.url) + link_path = link.file_path return os.path.isdir(link_path) @@ -1115,7 +1115,7 @@ def unpack_file_url( If download_dir is provided and link points to a file, make a copy of the link file inside download_dir. """ - link_path = url_to_path(link.url) + link_path = link.file_path # If it's a url to a local directory if is_dir_url(link): if os.path.isdir(location): diff --git a/src/pip/_internal/models/link.py b/src/pip/_internal/models/link.py index b585c93c395..23f549148a0 100644 --- a/src/pip/_internal/models/link.py +++ b/src/pip/_internal/models/link.py @@ -12,6 +12,7 @@ ) from pip._internal.utils.models import KeyBasedCompareMixin from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from pip._internal.utils.urls import url_to_path if MYPY_CHECK_RUNNING: from typing import Optional, Text, Tuple, Union @@ -96,6 +97,11 @@ def filename(self): assert name, ('URL %r produced no filename' % self._url) return name + @property + def file_path(self): + # type: () -> str + return url_to_path(self.url) + @property def scheme(self): # type: () -> str diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 9bcab9b9935..64717341b86 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -123,7 +123,7 @@ def prepare_linked_requirement( # TODO: Breakup into smaller functions if link.scheme == 'file': - path = url_to_path(link.url) + path = link.file_path logger.info('Processing %s', display_path(path)) else: logger.info('Collecting %s', req.req or req) From ee8cc0e6485163cceb38f5f9ff4ea3c38f258cb2 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 19 Sep 2019 20:19:24 -0400 Subject: [PATCH 0306/3170] Use unpack_file instead of unpack_file_url in wheel --- src/pip/_internal/operations/prepare.py | 1 - src/pip/_internal/wheel.py | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 64717341b86..f954c9859c3 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -27,7 +27,6 @@ from pip._internal.utils.marker_files import write_delete_marker_file from pip._internal.utils.misc import display_path, normalize_path from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from pip._internal.utils.urls import url_to_path if MYPY_CHECK_RUNNING: from typing import Optional diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index 5ae85a6ec44..cdf17c0ae1d 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -28,7 +28,6 @@ from pip._vendor.six import StringIO from pip._internal import pep425tags -from pip._internal.download import unpack_file_url from pip._internal.exceptions import ( InstallationError, InvalidWheelFilename, @@ -51,6 +50,7 @@ from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.ui import open_spinner +from pip._internal.utils.unpacking import unpack_file if MYPY_CHECK_RUNNING: from typing import ( @@ -1153,7 +1153,7 @@ def build( req.link = Link(path_to_url(wheel_file)) assert req.link.is_wheel # extract the wheel into the dir - unpack_file_url(link=req.link, location=req.source_dir) + unpack_file(req.link.file_path, req.source_dir) else: build_failure.append(req) From e9274f6548bcc2e1f5b9743ab8e074a42551909b Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 19 Sep 2019 20:48:09 -0400 Subject: [PATCH 0307/3170] Move keyring-related functions to new networking.auth module --- src/pip/_internal/download.py | 45 +------ src/pip/_internal/networking/__init__.py | 0 src/pip/_internal/networking/auth.py | 40 ++++++ tests/unit/test_download.py | 148 -------------------- tests/unit/test_networking_auth.py | 164 +++++++++++++++++++++++ 5 files changed, 208 insertions(+), 189 deletions(-) create mode 100644 src/pip/_internal/networking/__init__.py create mode 100644 src/pip/_internal/networking/auth.py create mode 100644 tests/unit/test_networking_auth.py diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index 763dc369ac3..964efac55c4 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -30,6 +30,7 @@ import pip from pip._internal.exceptions import HashMismatch, InstallationError from pip._internal.models.index import PyPI +from pip._internal.networking.auth import get_keyring_auth, keyring # Import ssl from compat so the initial import occurs in only one place. from pip._internal.utils.compat import HAS_TLS, ipaddress, ssl from pip._internal.utils.encoding import auto_decode @@ -116,16 +117,6 @@ logger = logging.getLogger(__name__) -try: - import keyring # noqa -except ImportError: - keyring = None -except Exception as exc: - logger.warning("Keyring is skipped due to an exception: %s", - str(exc)) - keyring = None - - SECURE_ORIGINS = [ # protocol, hostname, port # Taken from Chrome's list of secure origins (See: http://bit.ly/1qrySKC) @@ -248,34 +239,6 @@ def user_agent(): ) -def _get_keyring_auth(url, username): - """Return the tuple auth for a given url from keyring.""" - if not url or not keyring: - return None - - try: - try: - get_credential = keyring.get_credential - except AttributeError: - pass - else: - logger.debug("Getting credentials from keyring for %s", url) - cred = get_credential(url, username) - if cred is not None: - return cred.username, cred.password - return None - - if username: - logger.debug("Getting password from keyring for %s", url) - password = keyring.get_password(url, username) - if password: - return username, password - - except Exception as exc: - logger.warning("Keyring is skipped due to an exception: %s", - str(exc)) - - class MultiDomainBasicAuth(AuthBase): def __init__(self, prompting=True, index_urls=None): @@ -350,8 +313,8 @@ def _get_new_credentials(self, original_url, allow_netrc=True, # If we don't have a password and keyring is available, use it. if allow_keyring: # The index url is more specific than the netloc, so try it first - kr_auth = (_get_keyring_auth(index_url, username) or - _get_keyring_auth(netloc, username)) + kr_auth = (get_keyring_auth(index_url, username) or + get_keyring_auth(netloc, username)) if kr_auth: logger.debug("Found credentials in keyring for %s", netloc) return kr_auth @@ -419,7 +382,7 @@ def _prompt_for_password(self, netloc): username = ask_input("User for %s: " % netloc) if not username: return None, None - auth = _get_keyring_auth(netloc, username) + auth = get_keyring_auth(netloc, username) if auth: return auth[0], auth[1], False password = ask_password("Password: ") diff --git a/src/pip/_internal/networking/__init__.py b/src/pip/_internal/networking/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/networking/auth.py b/src/pip/_internal/networking/auth.py new file mode 100644 index 00000000000..3bc120f0048 --- /dev/null +++ b/src/pip/_internal/networking/auth.py @@ -0,0 +1,40 @@ +import logging + +logger = logging.getLogger(__name__) + +try: + import keyring # noqa +except ImportError: + keyring = None +except Exception as exc: + logger.warning("Keyring is skipped due to an exception: %s", + str(exc)) + keyring = None + + +def get_keyring_auth(url, username): + """Return the tuple auth for a given url from keyring.""" + if not url or not keyring: + return None + + try: + try: + get_credential = keyring.get_credential + except AttributeError: + pass + else: + logger.debug("Getting credentials from keyring for %s", url) + cred = get_credential(url, username) + if cred is not None: + return cred.username, cred.password + return None + + if username: + logger.debug("Getting password from keyring for %s", url) + password = keyring.get_password(url, username) + if password: + return username, password + + except Exception as exc: + logger.warning("Keyring is skipped due to an exception: %s", + str(exc)) diff --git a/tests/unit/test_download.py b/tests/unit/test_download.py index 87bc6a4ad27..643c7bfb8ae 100644 --- a/tests/unit/test_download.py +++ b/tests/unit/test_download.py @@ -791,151 +791,3 @@ def test_get_index_url_credentials(): # Check resolution of indexes assert get("http://example.com/path/path2") == ('foo', 'bar') assert get("http://example.com/path3/path2") == (None, None) - - -class KeyringModuleV1(object): - """Represents the supported API of keyring before get_credential - was added. - """ - - def __init__(self): - self.saved_passwords = [] - - def get_password(self, system, username): - if system == "example.com" and username: - return username + "!netloc" - if system == "http://example.com/path2" and username: - return username + "!url" - return None - - def set_password(self, system, username, password): - self.saved_passwords.append((system, username, password)) - - -@pytest.mark.parametrize('url, expect', ( - ("http://example.com/path1", (None, None)), - # path1 URLs will be resolved by netloc - ("http://user@example.com/path1", ("user", "user!netloc")), - ("http://user2@example.com/path1", ("user2", "user2!netloc")), - # path2 URLs will be resolved by index URL - ("http://example.com/path2/path3", (None, None)), - ("http://foo@example.com/path2/path3", ("foo", "foo!url")), -)) -def test_keyring_get_password(monkeypatch, url, expect): - monkeypatch.setattr('pip._internal.download.keyring', KeyringModuleV1()) - auth = MultiDomainBasicAuth(index_urls=["http://example.com/path2"]) - - actual = auth._get_new_credentials(url, allow_netrc=False, - allow_keyring=True) - assert actual == expect - - -def test_keyring_get_password_after_prompt(monkeypatch): - monkeypatch.setattr('pip._internal.download.keyring', KeyringModuleV1()) - auth = MultiDomainBasicAuth() - - def ask_input(prompt): - assert prompt == "User for example.com: " - return "user" - - monkeypatch.setattr('pip._internal.download.ask_input', ask_input) - actual = auth._prompt_for_password("example.com") - assert actual == ("user", "user!netloc", False) - - -def test_keyring_get_password_username_in_index(monkeypatch): - monkeypatch.setattr('pip._internal.download.keyring', KeyringModuleV1()) - auth = MultiDomainBasicAuth(index_urls=["http://user@example.com/path2"]) - get = functools.partial( - auth._get_new_credentials, - allow_netrc=False, - allow_keyring=True - ) - - assert get("http://example.com/path2/path3") == ("user", "user!url") - assert get("http://example.com/path4/path1") == (None, None) - - -@pytest.mark.parametrize("response_status, creds, expect_save", ( - (403, ("user", "pass", True), False), - (200, ("user", "pass", True), True,), - (200, ("user", "pass", False), False,), -)) -def test_keyring_set_password(monkeypatch, response_status, creds, - expect_save): - keyring = KeyringModuleV1() - monkeypatch.setattr('pip._internal.download.keyring', keyring) - auth = MultiDomainBasicAuth(prompting=True) - monkeypatch.setattr(auth, '_get_url_and_credentials', - lambda u: (u, None, None)) - monkeypatch.setattr(auth, '_prompt_for_password', lambda *a: creds) - if creds[2]: - # when _prompt_for_password indicates to save, we should save - def should_save_password_to_keyring(*a): - return True - else: - # when _prompt_for_password indicates not to save, we should - # never call this function - def should_save_password_to_keyring(*a): - assert False, ("_should_save_password_to_keyring should not be " + - "called") - monkeypatch.setattr(auth, '_should_save_password_to_keyring', - should_save_password_to_keyring) - - req = MockRequest("https://example.com") - resp = MockResponse(b"") - resp.url = req.url - connection = MockConnection() - - def _send(sent_req, **kwargs): - assert sent_req is req - assert "Authorization" in sent_req.headers - r = MockResponse(b"") - r.status_code = response_status - return r - - connection._send = _send - - resp.request = req - resp.status_code = 401 - resp.connection = connection - - auth.handle_401(resp) - - if expect_save: - assert keyring.saved_passwords == [("example.com", creds[0], creds[1])] - else: - assert keyring.saved_passwords == [] - - -class KeyringModuleV2(object): - """Represents the current supported API of keyring""" - - class Credential(object): - def __init__(self, username, password): - self.username = username - self.password = password - - def get_password(self, system, username): - assert False, "get_password should not ever be called" - - def get_credential(self, system, username): - if system == "http://example.com/path2": - return self.Credential("username", "url") - if system == "example.com": - return self.Credential("username", "netloc") - return None - - -@pytest.mark.parametrize('url, expect', ( - ("http://example.com/path1", ("username", "netloc")), - ("http://example.com/path2/path3", ("username", "url")), - ("http://user2@example.com/path2/path3", ("username", "url")), -)) -def test_keyring_get_credential(monkeypatch, url, expect): - monkeypatch.setattr(pip._internal.download, 'keyring', KeyringModuleV2()) - auth = MultiDomainBasicAuth(index_urls=["http://example.com/path2"]) - - assert auth._get_new_credentials(url, allow_netrc=False, - allow_keyring=True) \ - == expect diff --git a/tests/unit/test_networking_auth.py b/tests/unit/test_networking_auth.py new file mode 100644 index 00000000000..0999b7718c8 --- /dev/null +++ b/tests/unit/test_networking_auth.py @@ -0,0 +1,164 @@ +import functools + +import pytest + +import pip._internal.networking.auth +from pip._internal.download import MultiDomainBasicAuth +from tests.unit.test_download import MockConnection, MockRequest, MockResponse + + +class KeyringModuleV1(object): + """Represents the supported API of keyring before get_credential + was added. + """ + + def __init__(self): + self.saved_passwords = [] + + def get_password(self, system, username): + if system == "example.com" and username: + return username + "!netloc" + if system == "http://example.com/path2" and username: + return username + "!url" + return None + + def set_password(self, system, username, password): + self.saved_passwords.append((system, username, password)) + + +@pytest.mark.parametrize('url, expect', ( + ("http://example.com/path1", (None, None)), + # path1 URLs will be resolved by netloc + ("http://user@example.com/path1", ("user", "user!netloc")), + ("http://user2@example.com/path1", ("user2", "user2!netloc")), + # path2 URLs will be resolved by index URL + ("http://example.com/path2/path3", (None, None)), + ("http://foo@example.com/path2/path3", ("foo", "foo!url")), +)) +def test_keyring_get_password(monkeypatch, url, expect): + keyring = KeyringModuleV1() + monkeypatch.setattr('pip._internal.networking.auth.keyring', keyring) + monkeypatch.setattr('pip._internal.download.keyring', keyring) + auth = MultiDomainBasicAuth(index_urls=["http://example.com/path2"]) + + actual = auth._get_new_credentials(url, allow_netrc=False, + allow_keyring=True) + assert actual == expect + + +def test_keyring_get_password_after_prompt(monkeypatch): + keyring = KeyringModuleV1() + monkeypatch.setattr('pip._internal.networking.auth.keyring', keyring) + monkeypatch.setattr('pip._internal.download.keyring', keyring) + auth = MultiDomainBasicAuth() + + def ask_input(prompt): + assert prompt == "User for example.com: " + return "user" + + monkeypatch.setattr('pip._internal.download.ask_input', ask_input) + actual = auth._prompt_for_password("example.com") + assert actual == ("user", "user!netloc", False) + + +def test_keyring_get_password_username_in_index(monkeypatch): + keyring = KeyringModuleV1() + monkeypatch.setattr('pip._internal.networking.auth.keyring', keyring) + monkeypatch.setattr('pip._internal.download.keyring', keyring) + auth = MultiDomainBasicAuth(index_urls=["http://user@example.com/path2"]) + get = functools.partial( + auth._get_new_credentials, + allow_netrc=False, + allow_keyring=True + ) + + assert get("http://example.com/path2/path3") == ("user", "user!url") + assert get("http://example.com/path4/path1") == (None, None) + + +@pytest.mark.parametrize("response_status, creds, expect_save", ( + (403, ("user", "pass", True), False), + (200, ("user", "pass", True), True,), + (200, ("user", "pass", False), False,), +)) +def test_keyring_set_password(monkeypatch, response_status, creds, + expect_save): + keyring = KeyringModuleV1() + monkeypatch.setattr('pip._internal.networking.auth.keyring', keyring) + monkeypatch.setattr('pip._internal.download.keyring', keyring) + auth = MultiDomainBasicAuth(prompting=True) + monkeypatch.setattr(auth, '_get_url_and_credentials', + lambda u: (u, None, None)) + monkeypatch.setattr(auth, '_prompt_for_password', lambda *a: creds) + if creds[2]: + # when _prompt_for_password indicates to save, we should save + def should_save_password_to_keyring(*a): + return True + else: + # when _prompt_for_password indicates not to save, we should + # never call this function + def should_save_password_to_keyring(*a): + assert False, ("_should_save_password_to_keyring should not be " + + "called") + monkeypatch.setattr(auth, '_should_save_password_to_keyring', + should_save_password_to_keyring) + + req = MockRequest("https://example.com") + resp = MockResponse(b"") + resp.url = req.url + connection = MockConnection() + + def _send(sent_req, **kwargs): + assert sent_req is req + assert "Authorization" in sent_req.headers + r = MockResponse(b"") + r.status_code = response_status + return r + + connection._send = _send + + resp.request = req + resp.status_code = 401 + resp.connection = connection + + auth.handle_401(resp) + + if expect_save: + assert keyring.saved_passwords == [("example.com", creds[0], creds[1])] + else: + assert keyring.saved_passwords == [] + + +class KeyringModuleV2(object): + """Represents the current supported API of keyring""" + + class Credential(object): + def __init__(self, username, password): + self.username = username + self.password = password + + def get_password(self, system, username): + assert False, "get_password should not ever be called" + + def get_credential(self, system, username): + if system == "http://example.com/path2": + return self.Credential("username", "url") + if system == "example.com": + return self.Credential("username", "netloc") + return None + + +@pytest.mark.parametrize('url, expect', ( + ("http://example.com/path1", ("username", "netloc")), + ("http://example.com/path2/path3", ("username", "url")), + ("http://user2@example.com/path2/path3", ("username", "url")), +)) +def test_keyring_get_credential(monkeypatch, url, expect): + monkeypatch.setattr( + pip._internal.networking.auth, 'keyring', KeyringModuleV2() + ) + auth = MultiDomainBasicAuth(index_urls=["http://example.com/path2"]) + + assert auth._get_new_credentials( + url, allow_netrc=False, allow_keyring=True + ) == expect From f8d03e4baa0e65f8f758710b6f0b173272ba3306 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 19 Sep 2019 21:02:22 -0400 Subject: [PATCH 0308/3170] Move MultiDomainBasicAuth to networking.auth --- src/pip/_internal/download.py | 237 +------------------------- src/pip/_internal/networking/auth.py | 243 +++++++++++++++++++++++++++ tests/unit/test_download.py | 66 -------- tests/unit/test_networking_auth.py | 72 +++++++- 4 files changed, 312 insertions(+), 306 deletions(-) diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index 964efac55c4..907f6aa3992 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -17,10 +17,8 @@ from pip._vendor.cachecontrol.cache import BaseCache from pip._vendor.cachecontrol.caches import FileCache from pip._vendor.requests.adapters import BaseAdapter, HTTPAdapter -from pip._vendor.requests.auth import AuthBase, HTTPBasicAuth from pip._vendor.requests.models import CONTENT_CHUNK_SIZE, Response from pip._vendor.requests.structures import CaseInsensitiveDict -from pip._vendor.requests.utils import get_netrc_auth from pip._vendor.six import PY2 # NOTE: XMLRPC Client is not annotated in typeshed as on 2017-07-17, which is # why we ignore the type on this import @@ -30,7 +28,7 @@ import pip from pip._internal.exceptions import HashMismatch, InstallationError from pip._internal.models.index import PyPI -from pip._internal.networking.auth import get_keyring_auth, keyring +from pip._internal.networking.auth import MultiDomainBasicAuth # Import ssl from compat so the initial import occurs in only one place. from pip._internal.utils.compat import HAS_TLS, ipaddress, ssl from pip._internal.utils.encoding import auto_decode @@ -42,9 +40,6 @@ ) from pip._internal.utils.glibc import libc_ver from pip._internal.utils.misc import ( - ask, - ask_input, - ask_password, ask_path_exists, backup_dir, build_url_from_netloc, @@ -57,9 +52,7 @@ parse_netloc, path_to_display, path_to_url, - remove_auth_from_url, rmtree, - split_auth_netloc_from_url, splitext, ) from pip._internal.utils.temp_dir import TempDirectory @@ -71,17 +64,15 @@ if MYPY_CHECK_RUNNING: from typing import ( - IO, Callable, Dict, Iterator, List, Optional, Text, Tuple, Union, + IO, Callable, Iterator, List, Optional, Text, Tuple, Union, ) - from optparse import Values from mypy_extensions import TypedDict from pip._internal.models.link import Link from pip._internal.utils.hashes import Hashes - from pip._internal.vcs.versioncontrol import AuthInfo, VersionControl + from pip._internal.vcs.versioncontrol import VersionControl - Credentials = Tuple[str, str, str] SecureOrigin = Tuple[str, str, Optional[Union[int, str]]] if PY2: @@ -239,228 +230,6 @@ def user_agent(): ) -class MultiDomainBasicAuth(AuthBase): - - def __init__(self, prompting=True, index_urls=None): - # type: (bool, Optional[Values]) -> None - self.prompting = prompting - self.index_urls = index_urls - self.passwords = {} # type: Dict[str, AuthInfo] - # When the user is prompted to enter credentials and keyring is - # available, we will offer to save them. If the user accepts, - # this value is set to the credentials they entered. After the - # request authenticates, the caller should call - # ``save_credentials`` to save these. - self._credentials_to_save = None # type: Optional[Credentials] - - def _get_index_url(self, url): - """Return the original index URL matching the requested URL. - - Cached or dynamically generated credentials may work against - the original index URL rather than just the netloc. - - The provided url should have had its username and password - removed already. If the original index url had credentials then - they will be included in the return value. - - Returns None if no matching index was found, or if --no-index - was specified by the user. - """ - if not url or not self.index_urls: - return None - - for u in self.index_urls: - prefix = remove_auth_from_url(u).rstrip("/") + "/" - if url.startswith(prefix): - return u - - def _get_new_credentials(self, original_url, allow_netrc=True, - allow_keyring=True): - """Find and return credentials for the specified URL.""" - # Split the credentials and netloc from the url. - url, netloc, url_user_password = split_auth_netloc_from_url( - original_url) - - # Start with the credentials embedded in the url - username, password = url_user_password - if username is not None and password is not None: - logger.debug("Found credentials in url for %s", netloc) - return url_user_password - - # Find a matching index url for this request - index_url = self._get_index_url(url) - if index_url: - # Split the credentials from the url. - index_info = split_auth_netloc_from_url(index_url) - if index_info: - index_url, _, index_url_user_password = index_info - logger.debug("Found index url %s", index_url) - - # If an index URL was found, try its embedded credentials - if index_url and index_url_user_password[0] is not None: - username, password = index_url_user_password - if username is not None and password is not None: - logger.debug("Found credentials in index url for %s", netloc) - return index_url_user_password - - # Get creds from netrc if we still don't have them - if allow_netrc: - netrc_auth = get_netrc_auth(original_url) - if netrc_auth: - logger.debug("Found credentials in netrc for %s", netloc) - return netrc_auth - - # If we don't have a password and keyring is available, use it. - if allow_keyring: - # The index url is more specific than the netloc, so try it first - kr_auth = (get_keyring_auth(index_url, username) or - get_keyring_auth(netloc, username)) - if kr_auth: - logger.debug("Found credentials in keyring for %s", netloc) - return kr_auth - - return username, password - - def _get_url_and_credentials(self, original_url): - """Return the credentials to use for the provided URL. - - If allowed, netrc and keyring may be used to obtain the - correct credentials. - - Returns (url_without_credentials, username, password). Note - that even if the original URL contains credentials, this - function may return a different username and password. - """ - url, netloc, _ = split_auth_netloc_from_url(original_url) - - # Use any stored credentials that we have for this netloc - username, password = self.passwords.get(netloc, (None, None)) - - if username is None and password is None: - # No stored credentials. Acquire new credentials without prompting - # the user. (e.g. from netrc, keyring, or the URL itself) - username, password = self._get_new_credentials(original_url) - - if username is not None or password is not None: - # Convert the username and password if they're None, so that - # this netloc will show up as "cached" in the conditional above. - # Further, HTTPBasicAuth doesn't accept None, so it makes sense to - # cache the value that is going to be used. - username = username or "" - password = password or "" - - # Store any acquired credentials. - self.passwords[netloc] = (username, password) - - assert ( - # Credentials were found - (username is not None and password is not None) or - # Credentials were not found - (username is None and password is None) - ), "Could not load credentials from url: {}".format(original_url) - - return url, username, password - - def __call__(self, req): - # Get credentials for this request - url, username, password = self._get_url_and_credentials(req.url) - - # Set the url of the request to the url without any credentials - req.url = url - - if username is not None and password is not None: - # Send the basic auth with this request - req = HTTPBasicAuth(username, password)(req) - - # Attach a hook to handle 401 responses - req.register_hook("response", self.handle_401) - - return req - - # Factored out to allow for easy patching in tests - def _prompt_for_password(self, netloc): - username = ask_input("User for %s: " % netloc) - if not username: - return None, None - auth = get_keyring_auth(netloc, username) - if auth: - return auth[0], auth[1], False - password = ask_password("Password: ") - return username, password, True - - # Factored out to allow for easy patching in tests - def _should_save_password_to_keyring(self): - if not keyring: - return False - return ask("Save credentials to keyring [y/N]: ", ["y", "n"]) == "y" - - def handle_401(self, resp, **kwargs): - # We only care about 401 responses, anything else we want to just - # pass through the actual response - if resp.status_code != 401: - return resp - - # We are not able to prompt the user so simply return the response - if not self.prompting: - return resp - - parsed = urllib_parse.urlparse(resp.url) - - # Prompt the user for a new username and password - username, password, save = self._prompt_for_password(parsed.netloc) - - # Store the new username and password to use for future requests - self._credentials_to_save = None - if username is not None and password is not None: - self.passwords[parsed.netloc] = (username, password) - - # Prompt to save the password to keyring - if save and self._should_save_password_to_keyring(): - self._credentials_to_save = (parsed.netloc, username, password) - - # Consume content and release the original connection to allow our new - # request to reuse the same one. - resp.content - resp.raw.release_conn() - - # Add our new username and password to the request - req = HTTPBasicAuth(username or "", password or "")(resp.request) - req.register_hook("response", self.warn_on_401) - - # On successful request, save the credentials that were used to - # keyring. (Note that if the user responded "no" above, this member - # is not set and nothing will be saved.) - if self._credentials_to_save: - req.register_hook("response", self.save_credentials) - - # Send our new request - new_resp = resp.connection.send(req, **kwargs) - new_resp.history.append(resp) - - return new_resp - - def warn_on_401(self, resp, **kwargs): - """Response callback to warn about incorrect credentials.""" - if resp.status_code == 401: - logger.warning('401 Error, Credentials not correct for %s', - resp.request.url) - - def save_credentials(self, resp, **kwargs): - """Response callback to save credentials on success.""" - assert keyring is not None, "should never reach here without keyring" - if not keyring: - return - - creds = self._credentials_to_save - self._credentials_to_save = None - if creds and resp.status_code < 400: - try: - logger.info('Saving credentials to keyring') - keyring.set_password(*creds) - except Exception: - logger.exception('Failed to save credentials') - - class LocalFSAdapter(BaseAdapter): def send(self, request, stream=None, timeout=None, verify=None, cert=None, diff --git a/src/pip/_internal/networking/auth.py b/src/pip/_internal/networking/auth.py index 3bc120f0048..e0e2e61a987 100644 --- a/src/pip/_internal/networking/auth.py +++ b/src/pip/_internal/networking/auth.py @@ -1,5 +1,26 @@ import logging +from pip._vendor.requests.auth import AuthBase, HTTPBasicAuth +from pip._vendor.requests.utils import get_netrc_auth +from pip._vendor.six.moves.urllib import parse as urllib_parse + +from pip._internal.utils.misc import ( + ask, + ask_input, + ask_password, + remove_auth_from_url, + split_auth_netloc_from_url, +) +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from optparse import Values + from typing import Dict, Optional, Tuple + + from pip._internal.vcs.versioncontrol import AuthInfo + + Credentials = Tuple[str, str, str] + logger = logging.getLogger(__name__) try: @@ -38,3 +59,225 @@ def get_keyring_auth(url, username): except Exception as exc: logger.warning("Keyring is skipped due to an exception: %s", str(exc)) + + +class MultiDomainBasicAuth(AuthBase): + + def __init__(self, prompting=True, index_urls=None): + # type: (bool, Optional[Values]) -> None + self.prompting = prompting + self.index_urls = index_urls + self.passwords = {} # type: Dict[str, AuthInfo] + # When the user is prompted to enter credentials and keyring is + # available, we will offer to save them. If the user accepts, + # this value is set to the credentials they entered. After the + # request authenticates, the caller should call + # ``save_credentials`` to save these. + self._credentials_to_save = None # type: Optional[Credentials] + + def _get_index_url(self, url): + """Return the original index URL matching the requested URL. + + Cached or dynamically generated credentials may work against + the original index URL rather than just the netloc. + + The provided url should have had its username and password + removed already. If the original index url had credentials then + they will be included in the return value. + + Returns None if no matching index was found, or if --no-index + was specified by the user. + """ + if not url or not self.index_urls: + return None + + for u in self.index_urls: + prefix = remove_auth_from_url(u).rstrip("/") + "/" + if url.startswith(prefix): + return u + + def _get_new_credentials(self, original_url, allow_netrc=True, + allow_keyring=True): + """Find and return credentials for the specified URL.""" + # Split the credentials and netloc from the url. + url, netloc, url_user_password = split_auth_netloc_from_url( + original_url) + + # Start with the credentials embedded in the url + username, password = url_user_password + if username is not None and password is not None: + logger.debug("Found credentials in url for %s", netloc) + return url_user_password + + # Find a matching index url for this request + index_url = self._get_index_url(url) + if index_url: + # Split the credentials from the url. + index_info = split_auth_netloc_from_url(index_url) + if index_info: + index_url, _, index_url_user_password = index_info + logger.debug("Found index url %s", index_url) + + # If an index URL was found, try its embedded credentials + if index_url and index_url_user_password[0] is not None: + username, password = index_url_user_password + if username is not None and password is not None: + logger.debug("Found credentials in index url for %s", netloc) + return index_url_user_password + + # Get creds from netrc if we still don't have them + if allow_netrc: + netrc_auth = get_netrc_auth(original_url) + if netrc_auth: + logger.debug("Found credentials in netrc for %s", netloc) + return netrc_auth + + # If we don't have a password and keyring is available, use it. + if allow_keyring: + # The index url is more specific than the netloc, so try it first + kr_auth = (get_keyring_auth(index_url, username) or + get_keyring_auth(netloc, username)) + if kr_auth: + logger.debug("Found credentials in keyring for %s", netloc) + return kr_auth + + return username, password + + def _get_url_and_credentials(self, original_url): + """Return the credentials to use for the provided URL. + + If allowed, netrc and keyring may be used to obtain the + correct credentials. + + Returns (url_without_credentials, username, password). Note + that even if the original URL contains credentials, this + function may return a different username and password. + """ + url, netloc, _ = split_auth_netloc_from_url(original_url) + + # Use any stored credentials that we have for this netloc + username, password = self.passwords.get(netloc, (None, None)) + + if username is None and password is None: + # No stored credentials. Acquire new credentials without prompting + # the user. (e.g. from netrc, keyring, or the URL itself) + username, password = self._get_new_credentials(original_url) + + if username is not None or password is not None: + # Convert the username and password if they're None, so that + # this netloc will show up as "cached" in the conditional above. + # Further, HTTPBasicAuth doesn't accept None, so it makes sense to + # cache the value that is going to be used. + username = username or "" + password = password or "" + + # Store any acquired credentials. + self.passwords[netloc] = (username, password) + + assert ( + # Credentials were found + (username is not None and password is not None) or + # Credentials were not found + (username is None and password is None) + ), "Could not load credentials from url: {}".format(original_url) + + return url, username, password + + def __call__(self, req): + # Get credentials for this request + url, username, password = self._get_url_and_credentials(req.url) + + # Set the url of the request to the url without any credentials + req.url = url + + if username is not None and password is not None: + # Send the basic auth with this request + req = HTTPBasicAuth(username, password)(req) + + # Attach a hook to handle 401 responses + req.register_hook("response", self.handle_401) + + return req + + # Factored out to allow for easy patching in tests + def _prompt_for_password(self, netloc): + username = ask_input("User for %s: " % netloc) + if not username: + return None, None + auth = get_keyring_auth(netloc, username) + if auth: + return auth[0], auth[1], False + password = ask_password("Password: ") + return username, password, True + + # Factored out to allow for easy patching in tests + def _should_save_password_to_keyring(self): + if not keyring: + return False + return ask("Save credentials to keyring [y/N]: ", ["y", "n"]) == "y" + + def handle_401(self, resp, **kwargs): + # We only care about 401 responses, anything else we want to just + # pass through the actual response + if resp.status_code != 401: + return resp + + # We are not able to prompt the user so simply return the response + if not self.prompting: + return resp + + parsed = urllib_parse.urlparse(resp.url) + + # Prompt the user for a new username and password + username, password, save = self._prompt_for_password(parsed.netloc) + + # Store the new username and password to use for future requests + self._credentials_to_save = None + if username is not None and password is not None: + self.passwords[parsed.netloc] = (username, password) + + # Prompt to save the password to keyring + if save and self._should_save_password_to_keyring(): + self._credentials_to_save = (parsed.netloc, username, password) + + # Consume content and release the original connection to allow our new + # request to reuse the same one. + resp.content + resp.raw.release_conn() + + # Add our new username and password to the request + req = HTTPBasicAuth(username or "", password or "")(resp.request) + req.register_hook("response", self.warn_on_401) + + # On successful request, save the credentials that were used to + # keyring. (Note that if the user responded "no" above, this member + # is not set and nothing will be saved.) + if self._credentials_to_save: + req.register_hook("response", self.save_credentials) + + # Send our new request + new_resp = resp.connection.send(req, **kwargs) + new_resp.history.append(resp) + + return new_resp + + def warn_on_401(self, resp, **kwargs): + """Response callback to warn about incorrect credentials.""" + if resp.status_code == 401: + logger.warning('401 Error, Credentials not correct for %s', + resp.request.url) + + def save_credentials(self, resp, **kwargs): + """Response callback to save credentials on success.""" + assert keyring is not None, "should never reach here without keyring" + if not keyring: + return + + creds = self._credentials_to_save + self._credentials_to_save = None + if creds and resp.status_code < 400: + try: + logger.info('Saving credentials to keyring') + keyring.set_password(*creds) + except Exception: + logger.exception('Failed to save credentials') diff --git a/tests/unit/test_download.py b/tests/unit/test_download.py index 643c7bfb8ae..3d052619e46 100644 --- a/tests/unit/test_download.py +++ b/tests/unit/test_download.py @@ -1,4 +1,3 @@ -import functools import hashlib import logging import os @@ -15,7 +14,6 @@ import pip from pip._internal.download import ( CI_ENVIRONMENT_VARIABLES, - MultiDomainBasicAuth, PipSession, SafeFileCache, _copy_source_tree, @@ -727,67 +725,3 @@ def warning(self, *args, **kwargs): actual_level, actual_message = log_records[0] assert actual_level == 'WARNING' assert 'is not a trusted or secure host' in actual_message - - -@pytest.mark.parametrize(["input_url", "url", "username", "password"], [ - ( - "http://user%40email.com:password@example.com/path", - "http://example.com/path", - "user@email.com", - "password", - ), - ( - "http://username:password@example.com/path", - "http://example.com/path", - "username", - "password", - ), - ( - "http://token@example.com/path", - "http://example.com/path", - "token", - "", - ), - ( - "http://example.com/path", - "http://example.com/path", - None, - None, - ), -]) -def test_get_credentials_parses_correctly(input_url, url, username, password): - auth = MultiDomainBasicAuth() - get = auth._get_url_and_credentials - - # Check URL parsing - assert get(input_url) == (url, username, password) - assert ( - # There are no credentials in the URL - (username is None and password is None) or - # Credentials were found and "cached" appropriately - auth.passwords['example.com'] == (username, password) - ) - - -def test_get_credentials_uses_cached_credentials(): - auth = MultiDomainBasicAuth() - auth.passwords['example.com'] = ('user', 'pass') - - got = auth._get_url_and_credentials("http://foo:bar@example.com/path") - expected = ('http://example.com/path', 'user', 'pass') - assert got == expected - - -def test_get_index_url_credentials(): - auth = MultiDomainBasicAuth(index_urls=[ - "http://foo:bar@example.com/path" - ]) - get = functools.partial( - auth._get_new_credentials, - allow_netrc=False, - allow_keyring=False - ) - - # Check resolution of indexes - assert get("http://example.com/path/path2") == ('foo', 'bar') - assert get("http://example.com/path3/path2") == (None, None) diff --git a/tests/unit/test_networking_auth.py b/tests/unit/test_networking_auth.py index 0999b7718c8..1c128a76a3c 100644 --- a/tests/unit/test_networking_auth.py +++ b/tests/unit/test_networking_auth.py @@ -3,10 +3,74 @@ import pytest import pip._internal.networking.auth -from pip._internal.download import MultiDomainBasicAuth +from pip._internal.networking.auth import MultiDomainBasicAuth from tests.unit.test_download import MockConnection, MockRequest, MockResponse +@pytest.mark.parametrize(["input_url", "url", "username", "password"], [ + ( + "http://user%40email.com:password@example.com/path", + "http://example.com/path", + "user@email.com", + "password", + ), + ( + "http://username:password@example.com/path", + "http://example.com/path", + "username", + "password", + ), + ( + "http://token@example.com/path", + "http://example.com/path", + "token", + "", + ), + ( + "http://example.com/path", + "http://example.com/path", + None, + None, + ), +]) +def test_get_credentials_parses_correctly(input_url, url, username, password): + auth = MultiDomainBasicAuth() + get = auth._get_url_and_credentials + + # Check URL parsing + assert get(input_url) == (url, username, password) + assert ( + # There are no credentials in the URL + (username is None and password is None) or + # Credentials were found and "cached" appropriately + auth.passwords['example.com'] == (username, password) + ) + + +def test_get_credentials_uses_cached_credentials(): + auth = MultiDomainBasicAuth() + auth.passwords['example.com'] = ('user', 'pass') + + got = auth._get_url_and_credentials("http://foo:bar@example.com/path") + expected = ('http://example.com/path', 'user', 'pass') + assert got == expected + + +def test_get_index_url_credentials(): + auth = MultiDomainBasicAuth(index_urls=[ + "http://foo:bar@example.com/path" + ]) + get = functools.partial( + auth._get_new_credentials, + allow_netrc=False, + allow_keyring=False + ) + + # Check resolution of indexes + assert get("http://example.com/path/path2") == ('foo', 'bar') + assert get("http://example.com/path3/path2") == (None, None) + + class KeyringModuleV1(object): """Represents the supported API of keyring before get_credential was added. @@ -38,7 +102,6 @@ def set_password(self, system, username, password): def test_keyring_get_password(monkeypatch, url, expect): keyring = KeyringModuleV1() monkeypatch.setattr('pip._internal.networking.auth.keyring', keyring) - monkeypatch.setattr('pip._internal.download.keyring', keyring) auth = MultiDomainBasicAuth(index_urls=["http://example.com/path2"]) actual = auth._get_new_credentials(url, allow_netrc=False, @@ -49,14 +112,13 @@ def test_keyring_get_password(monkeypatch, url, expect): def test_keyring_get_password_after_prompt(monkeypatch): keyring = KeyringModuleV1() monkeypatch.setattr('pip._internal.networking.auth.keyring', keyring) - monkeypatch.setattr('pip._internal.download.keyring', keyring) auth = MultiDomainBasicAuth() def ask_input(prompt): assert prompt == "User for example.com: " return "user" - monkeypatch.setattr('pip._internal.download.ask_input', ask_input) + monkeypatch.setattr('pip._internal.networking.auth.ask_input', ask_input) actual = auth._prompt_for_password("example.com") assert actual == ("user", "user!netloc", False) @@ -64,7 +126,6 @@ def ask_input(prompt): def test_keyring_get_password_username_in_index(monkeypatch): keyring = KeyringModuleV1() monkeypatch.setattr('pip._internal.networking.auth.keyring', keyring) - monkeypatch.setattr('pip._internal.download.keyring', keyring) auth = MultiDomainBasicAuth(index_urls=["http://user@example.com/path2"]) get = functools.partial( auth._get_new_credentials, @@ -85,7 +146,6 @@ def test_keyring_set_password(monkeypatch, response_status, creds, expect_save): keyring = KeyringModuleV1() monkeypatch.setattr('pip._internal.networking.auth.keyring', keyring) - monkeypatch.setattr('pip._internal.download.keyring', keyring) auth = MultiDomainBasicAuth(prompting=True) monkeypatch.setattr(auth, '_get_url_and_credentials', lambda u: (u, None, None)) From fe7d0b0172c1e93eac18cdda1f307e2355b68a69 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 19 Sep 2019 21:11:16 -0400 Subject: [PATCH 0309/3170] Normalize style --- src/pip/_internal/networking/auth.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/networking/auth.py b/src/pip/_internal/networking/auth.py index e0e2e61a987..c864e24dd01 100644 --- a/src/pip/_internal/networking/auth.py +++ b/src/pip/_internal/networking/auth.py @@ -28,8 +28,9 @@ except ImportError: keyring = None except Exception as exc: - logger.warning("Keyring is skipped due to an exception: %s", - str(exc)) + logger.warning( + "Keyring is skipped due to an exception: %s", str(exc), + ) keyring = None @@ -57,8 +58,9 @@ def get_keyring_auth(url, username): return username, password except Exception as exc: - logger.warning("Keyring is skipped due to an exception: %s", - str(exc)) + logger.warning( + "Keyring is skipped due to an exception: %s", str(exc), + ) class MultiDomainBasicAuth(AuthBase): @@ -101,7 +103,8 @@ def _get_new_credentials(self, original_url, allow_netrc=True, """Find and return credentials for the specified URL.""" # Split the credentials and netloc from the url. url, netloc, url_user_password = split_auth_netloc_from_url( - original_url) + original_url, + ) # Start with the credentials embedded in the url username, password = url_user_password @@ -135,8 +138,10 @@ def _get_new_credentials(self, original_url, allow_netrc=True, # If we don't have a password and keyring is available, use it. if allow_keyring: # The index url is more specific than the netloc, so try it first - kr_auth = (get_keyring_auth(index_url, username) or - get_keyring_auth(netloc, username)) + kr_auth = ( + get_keyring_auth(index_url, username) or + get_keyring_auth(netloc, username) + ) if kr_auth: logger.debug("Found credentials in keyring for %s", netloc) return kr_auth @@ -264,8 +269,9 @@ def handle_401(self, resp, **kwargs): def warn_on_401(self, resp, **kwargs): """Response callback to warn about incorrect credentials.""" if resp.status_code == 401: - logger.warning('401 Error, Credentials not correct for %s', - resp.request.url) + logger.warning( + '401 Error, Credentials not correct for %s', resp.request.url, + ) def save_credentials(self, resp, **kwargs): """Response callback to save credentials on success.""" From 1ae28f6946ae4b86bd33064e5d594320c6558fb1 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 19 Sep 2019 21:23:13 -0400 Subject: [PATCH 0310/3170] Add RequirementParts to hold parsed requirement info --- src/pip/_internal/req/constructors.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index 702f4392f96..9dc065ff53a 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -273,6 +273,20 @@ def _get_url_from_path(path, name): return path_to_url(path) +class RequirementParts(object): + def __init__( + self, + requirement, # type: Optional[Requirement] + link, # type: Optional[Link] + markers, # type: Optional[Marker] + extras, # type: Set[str] + ): + self.requirement = requirement + self.link = link + self.markers = markers + self.extras = extras + + def install_req_from_line( name, # type: str comes_from=None, # type: Optional[Union[str, InstallRequirement]] @@ -364,13 +378,15 @@ def with_source(text): else: req = None + parts = RequirementParts(req, link, markers, extras) + return InstallRequirement( - req, comes_from, link=link, markers=markers, + parts.requirement, comes_from, link=parts.link, markers=parts.markers, use_pep517=use_pep517, isolated=isolated, options=options if options else {}, wheel_cache=wheel_cache, constraint=constraint, - extras=extras, + extras=parts.extras, ) From d0336be3ee7cacfc1880517271cbc6bfb55f04e6 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 19 Sep 2019 21:30:00 -0400 Subject: [PATCH 0311/3170] Move requirement parsing to separate function in req.constructors --- src/pip/_internal/req/constructors.py | 41 +++++++++++++++------------ 1 file changed, 23 insertions(+), 18 deletions(-) diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index 9dc065ff53a..50fdbc9b4ea 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -287,23 +287,8 @@ def __init__( self.extras = extras -def install_req_from_line( - name, # type: str - comes_from=None, # type: Optional[Union[str, InstallRequirement]] - use_pep517=None, # type: Optional[bool] - isolated=False, # type: bool - options=None, # type: Optional[Dict[str, Any]] - wheel_cache=None, # type: Optional[WheelCache] - constraint=False, # type: bool - line_source=None, # type: Optional[str] -): - # type: (...) -> InstallRequirement - """Creates an InstallRequirement from a name, which might be a - requirement, directory containing 'setup.py', filename, or URL. - - :param line_source: An optional string describing where the line is from, - for logging purposes in case of an error. - """ +def parse_req_from_line(name, line_source): + # type: (str, Optional[str]) -> RequirementParts if is_url(name): marker_sep = '; ' else: @@ -378,7 +363,27 @@ def with_source(text): else: req = None - parts = RequirementParts(req, link, markers, extras) + return RequirementParts(req, link, markers, extras) + + +def install_req_from_line( + name, # type: str + comes_from=None, # type: Optional[Union[str, InstallRequirement]] + use_pep517=None, # type: Optional[bool] + isolated=False, # type: bool + options=None, # type: Optional[Dict[str, Any]] + wheel_cache=None, # type: Optional[WheelCache] + constraint=False, # type: bool + line_source=None, # type: Optional[str] +): + # type: (...) -> InstallRequirement + """Creates an InstallRequirement from a name, which might be a + requirement, directory containing 'setup.py', filename, or URL. + + :param line_source: An optional string describing where the line is from, + for logging purposes in case of an error. + """ + parts = parse_req_from_line(name, line_source) return InstallRequirement( parts.requirement, comes_from, link=parts.link, markers=parts.markers, From 8b8f8abe45d81a36362d8ae5fec89a16f537cbba Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 19 Sep 2019 21:54:39 -0400 Subject: [PATCH 0312/3170] Unconditionally create TempDirectory in install --- src/pip/_internal/commands/install.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 356d674b91d..a14b1b38f45 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -45,7 +45,7 @@ if MYPY_CHECK_RUNNING: from optparse import Values - from typing import Any, List + from typing import Any, List, Optional from pip._internal.models.format_control import FormatControl from pip._internal.req.req_install import InstallRequirement @@ -304,7 +304,8 @@ def run(self, options, args): install_options.append('--user') install_options.append('--prefix=') - target_temp_dir = TempDirectory(kind="target") + target_temp_dir = None # type: Optional[TempDirectory] + target_temp_dir_path = None # type: Optional[str] if options.target_dir: options.ignore_installed = True options.target_dir = os.path.abspath(options.target_dir) @@ -316,8 +317,10 @@ def run(self, options, args): ) # Create a target directory for using with the target option + target_temp_dir = TempDirectory(kind="target") target_temp_dir.create() - install_options.append('--home=' + target_temp_dir.path) + target_temp_dir_path = target_temp_dir.path + install_options.append('--home=' + target_temp_dir_path) global_options = options.global_options or [] @@ -444,7 +447,7 @@ def run(self, options, args): install_options, global_options, root=options.root_path, - home=target_temp_dir.path, + home=target_temp_dir_path, prefix=options.prefix_path, pycompile=options.compile, warn_script_location=warn_script_location, @@ -453,7 +456,7 @@ def run(self, options, args): lib_locations = get_lib_location_guesses( user=options.use_user_site, - home=target_temp_dir.path, + home=target_temp_dir_path, root=options.root_path, prefix=options.prefix_path, isolated=options.isolated_mode, From fbdd02e2fdaa99250fd3494afa0c464315eee7c2 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 19 Sep 2019 22:09:01 -0400 Subject: [PATCH 0313/3170] Manage InstallRequirement._temp_build_dir itself instead of its path --- src/pip/_internal/req/req_install.py | 27 +++++++++++++++++---------- 1 file changed, 17 insertions(+), 10 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 48e8804dce2..3fbd1739e74 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -128,7 +128,7 @@ def __init__( # conflicts with another installed distribution: self.conflicts_with = None # Temporary build location - self._temp_build_dir = TempDirectory(kind="req-build") + self._temp_build_dir = None # type: Optional[TempDirectory] # 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 # type: Optional[str] @@ -325,7 +325,8 @@ def from_path(self): def ensure_build_location(self, build_dir): # type: (str) -> str assert build_dir is not None - if self._temp_build_dir.path is not None: + if self._temp_build_dir is not None: + assert self._temp_build_dir.path return self._temp_build_dir.path if self.req is None: # for requirement via a path to a directory: the name of the @@ -335,6 +336,7 @@ def ensure_build_location(self, build_dir): # Some systems have /tmp as a symlink which confuses custom # builds (such as numpy). Thus, we ensure that the real path # is returned. + self._temp_build_dir = TempDirectory(kind="req-build") self._temp_build_dir.create() self._ideal_build_dir = build_dir @@ -364,11 +366,11 @@ def _correct_build_location(self): if self.source_dir is not None: return assert self.req is not None - assert self._temp_build_dir.path + assert self._temp_build_dir assert (self._ideal_build_dir is not None and self._ideal_build_dir.path) # type: ignore - old_location = self._temp_build_dir.path - self._temp_build_dir.path = None + old_location = self._temp_build_dir + self._temp_build_dir = None new_location = self.ensure_build_location(self._ideal_build_dir) if os.path.exists(new_location): @@ -377,10 +379,13 @@ def _correct_build_location(self): % display_path(new_location)) logger.debug( 'Moving package %s from %s to new location %s', - self, display_path(old_location), display_path(new_location), + self, display_path(old_location.path), display_path(new_location), ) - shutil.move(old_location, new_location) - self._temp_build_dir.path = new_location + shutil.move(old_location.path, new_location) + self._temp_build_dir = TempDirectory( + path=new_location, kind="req-install", + ) + self._ideal_build_dir = None self.source_dir = os.path.normpath(os.path.abspath(new_location)) self._egg_info_path = None @@ -388,7 +393,7 @@ def _correct_build_location(self): # Correct the metadata directory, if it exists if self.metadata_directory: old_meta = self.metadata_directory - rel = os.path.relpath(old_meta, start=old_location) + rel = os.path.relpath(old_meta, start=old_location.path) new_meta = os.path.join(new_location, rel) new_meta = os.path.normpath(os.path.abspath(new_meta)) self.metadata_directory = new_meta @@ -401,7 +406,9 @@ def remove_temporary_source(self): logger.debug('Removing source in %s', self.source_dir) rmtree(self.source_dir) self.source_dir = None - self._temp_build_dir.cleanup() + if self._temp_build_dir: + self._temp_build_dir.cleanup() + self._temp_build_dir = None self.build_env.cleanup() def check_if_exists(self, use_user_site): From 92e974814791dd7316fe22c62e9cf92226c5a177 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 19 Sep 2019 22:16:00 -0400 Subject: [PATCH 0314/3170] Make TempDirectory._create return path instead of setting self.path --- src/pip/_internal/utils/temp_dir.py | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/utils/temp_dir.py b/src/pip/_internal/utils/temp_dir.py index 2c81ad554c8..56159d565e1 100644 --- a/src/pip/_internal/utils/temp_dir.py +++ b/src/pip/_internal/utils/temp_dir.py @@ -60,21 +60,25 @@ def __exit__(self, exc, value, tb): self.cleanup() def create(self): + self.path = self._create() + + def _create(self): """Create a temporary directory and store its path in self.path """ if self.path is not None: logger.debug( "Skipped creation of temporary directory: {}".format(self.path) ) - return + return self.path # 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( + path = os.path.realpath( tempfile.mkdtemp(prefix="pip-{}-".format(self.kind)) ) - logger.debug("Created temporary directory: {}".format(self.path)) + logger.debug("Created temporary directory: {}".format(path)) + return path def cleanup(self): """Remove the temporary directory created and reset state @@ -133,7 +137,7 @@ def _generate_names(cls, name): if new_name != name: yield new_name - def create(self): + def _create(self): root, name = os.path.split(self.original) for candidate in self._generate_names(name): path = os.path.join(root, candidate) @@ -144,12 +148,13 @@ def create(self): if ex.errno != errno.EEXIST: raise else: - self.path = os.path.realpath(path) + path = os.path.realpath(path) break - - if not self.path: + else: # Final fallback on the default behavior. - self.path = os.path.realpath( + path = os.path.realpath( tempfile.mkdtemp(prefix="pip-{}-".format(self.kind)) ) - logger.debug("Created temporary directory: {}".format(self.path)) + + logger.debug("Created temporary directory: {}".format(path)) + return path From 7e1d02226b0cabf6223bbcefa14414bebb6af3fb Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 19 Sep 2019 23:03:55 -0400 Subject: [PATCH 0315/3170] Rename networking to network --- src/pip/_internal/download.py | 2 +- .../{networking => network}/__init__.py | 0 .../_internal/{networking => network}/auth.py | 0 tests/unit/test_networking_auth.py | 16 ++++++++-------- 4 files changed, 9 insertions(+), 9 deletions(-) rename src/pip/_internal/{networking => network}/__init__.py (100%) rename src/pip/_internal/{networking => network}/auth.py (100%) diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index 907f6aa3992..ec70ac29ded 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -28,7 +28,7 @@ import pip from pip._internal.exceptions import HashMismatch, InstallationError from pip._internal.models.index import PyPI -from pip._internal.networking.auth import MultiDomainBasicAuth +from pip._internal.network.auth import MultiDomainBasicAuth # Import ssl from compat so the initial import occurs in only one place. from pip._internal.utils.compat import HAS_TLS, ipaddress, ssl from pip._internal.utils.encoding import auto_decode diff --git a/src/pip/_internal/networking/__init__.py b/src/pip/_internal/network/__init__.py similarity index 100% rename from src/pip/_internal/networking/__init__.py rename to src/pip/_internal/network/__init__.py diff --git a/src/pip/_internal/networking/auth.py b/src/pip/_internal/network/auth.py similarity index 100% rename from src/pip/_internal/networking/auth.py rename to src/pip/_internal/network/auth.py diff --git a/tests/unit/test_networking_auth.py b/tests/unit/test_networking_auth.py index 1c128a76a3c..0f0b6790ae1 100644 --- a/tests/unit/test_networking_auth.py +++ b/tests/unit/test_networking_auth.py @@ -2,8 +2,8 @@ import pytest -import pip._internal.networking.auth -from pip._internal.networking.auth import MultiDomainBasicAuth +import pip._internal.network.auth +from pip._internal.network.auth import MultiDomainBasicAuth from tests.unit.test_download import MockConnection, MockRequest, MockResponse @@ -101,7 +101,7 @@ def set_password(self, system, username, password): )) def test_keyring_get_password(monkeypatch, url, expect): keyring = KeyringModuleV1() - monkeypatch.setattr('pip._internal.networking.auth.keyring', keyring) + monkeypatch.setattr('pip._internal.network.auth.keyring', keyring) auth = MultiDomainBasicAuth(index_urls=["http://example.com/path2"]) actual = auth._get_new_credentials(url, allow_netrc=False, @@ -111,21 +111,21 @@ def test_keyring_get_password(monkeypatch, url, expect): def test_keyring_get_password_after_prompt(monkeypatch): keyring = KeyringModuleV1() - monkeypatch.setattr('pip._internal.networking.auth.keyring', keyring) + monkeypatch.setattr('pip._internal.network.auth.keyring', keyring) auth = MultiDomainBasicAuth() def ask_input(prompt): assert prompt == "User for example.com: " return "user" - monkeypatch.setattr('pip._internal.networking.auth.ask_input', ask_input) + monkeypatch.setattr('pip._internal.network.auth.ask_input', ask_input) actual = auth._prompt_for_password("example.com") assert actual == ("user", "user!netloc", False) def test_keyring_get_password_username_in_index(monkeypatch): keyring = KeyringModuleV1() - monkeypatch.setattr('pip._internal.networking.auth.keyring', keyring) + monkeypatch.setattr('pip._internal.network.auth.keyring', keyring) auth = MultiDomainBasicAuth(index_urls=["http://user@example.com/path2"]) get = functools.partial( auth._get_new_credentials, @@ -145,7 +145,7 @@ def test_keyring_get_password_username_in_index(monkeypatch): def test_keyring_set_password(monkeypatch, response_status, creds, expect_save): keyring = KeyringModuleV1() - monkeypatch.setattr('pip._internal.networking.auth.keyring', keyring) + monkeypatch.setattr('pip._internal.network.auth.keyring', keyring) auth = MultiDomainBasicAuth(prompting=True) monkeypatch.setattr(auth, '_get_url_and_credentials', lambda u: (u, None, None)) @@ -215,7 +215,7 @@ def get_credential(self, system, username): )) def test_keyring_get_credential(monkeypatch, url, expect): monkeypatch.setattr( - pip._internal.networking.auth, 'keyring', KeyringModuleV2() + pip._internal.network.auth, 'keyring', KeyringModuleV2() ) auth = MultiDomainBasicAuth(index_urls=["http://example.com/path2"]) From b5495b4d1eebfeb454c13f8fd708c9f8f413c592 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 19 Sep 2019 19:28:38 +0530 Subject: [PATCH 0316/3170] Introduce an operations.generate_metadata module As things stand, it'll completely delegate all the metadata generation to InstallRequirement's methods. Follow ups will move related code into this module. --- src/pip/_internal/operations/generate_metadata.py | 9 +++++++++ src/pip/_internal/req/req_install.py | 7 +++---- 2 files changed, 12 insertions(+), 4 deletions(-) create mode 100644 src/pip/_internal/operations/generate_metadata.py diff --git a/src/pip/_internal/operations/generate_metadata.py b/src/pip/_internal/operations/generate_metadata.py new file mode 100644 index 00000000000..07f35816dc0 --- /dev/null +++ b/src/pip/_internal/operations/generate_metadata.py @@ -0,0 +1,9 @@ +"""Metadata generation logic for source distributions. +""" + + +def get_metadata_generator(install_req): + if install_req.use_pep517: + return install_req.prepare_pep517_metadata + else: + return install_req.run_egg_info diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 48e8804dce2..8bff23c4004 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -23,6 +23,7 @@ from pip._internal.build_env import NoOpBuildEnvironment from pip._internal.exceptions import InstallationError from pip._internal.models.link import Link +from pip._internal.operations.generate_metadata import get_metadata_generator from pip._internal.pyproject import load_pyproject_toml, make_pyproject_path from pip._internal.req.req_uninstall import UninstallPathSet from pip._internal.utils.compat import native_str @@ -559,11 +560,9 @@ def prepare_metadata(self): """ assert self.source_dir + metadata_generator = get_metadata_generator(self) with indent_log(): - if self.use_pep517: - self.prepare_pep517_metadata() - else: - self.run_egg_info() + metadata_generator() if not self.req: if isinstance(parse_version(self.metadata["Version"]), Version): From b2e0a123631d326f06192a01758ebe581284dbdf Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 19 Sep 2019 19:29:41 +0530 Subject: [PATCH 0317/3170] Return early for legacy processes --- src/pip/_internal/operations/generate_metadata.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/operations/generate_metadata.py b/src/pip/_internal/operations/generate_metadata.py index 07f35816dc0..a8b5518d329 100644 --- a/src/pip/_internal/operations/generate_metadata.py +++ b/src/pip/_internal/operations/generate_metadata.py @@ -3,7 +3,7 @@ def get_metadata_generator(install_req): - if install_req.use_pep517: - return install_req.prepare_pep517_metadata - else: + if not install_req.use_pep517: return install_req.run_egg_info + + return install_req.prepare_pep517_metadata From 6d8c3fdd29113cf5d645c85338281797b651a714 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 19 Sep 2019 21:32:38 +0530 Subject: [PATCH 0318/3170] Pass self to the metadata generation function --- .../_internal/operations/generate_metadata.py | 21 +++++++++++++++++-- src/pip/_internal/req/req_install.py | 2 +- 2 files changed, 20 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/operations/generate_metadata.py b/src/pip/_internal/operations/generate_metadata.py index a8b5518d329..2151f855a1c 100644 --- a/src/pip/_internal/operations/generate_metadata.py +++ b/src/pip/_internal/operations/generate_metadata.py @@ -1,9 +1,26 @@ """Metadata generation logic for source distributions. """ +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import Callable + from pip._internal.req.req_install import InstallRequirement + def get_metadata_generator(install_req): + # type: (InstallRequirement) -> Callable[[InstallRequirement], None] if not install_req.use_pep517: - return install_req.run_egg_info + return _generate_metadata_legacy + + return _generate_metadata + + +def _generate_metadata_legacy(install_req): + # type: (InstallRequirement) -> None + install_req.run_egg_info() + - return install_req.prepare_pep517_metadata +def _generate_metadata(install_req): + # type: (InstallRequirement) -> None + install_req.prepare_pep517_metadata() diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 8bff23c4004..18c16547c61 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -562,7 +562,7 @@ def prepare_metadata(self): metadata_generator = get_metadata_generator(self) with indent_log(): - metadata_generator() + metadata_generator(self) if not self.req: if isinstance(parse_version(self.metadata["Version"]), Version): From 7423c8ab022037c07a5d4c2f33e4b249bc88ddb9 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 20 Sep 2019 19:46:40 -0400 Subject: [PATCH 0319/3170] Add documentation string to pip._internal.network.auth --- src/pip/_internal/network/__init__.py | 2 ++ src/pip/_internal/network/auth.py | 6 ++++++ 2 files changed, 8 insertions(+) diff --git a/src/pip/_internal/network/__init__.py b/src/pip/_internal/network/__init__.py index e69de29bb2d..b51bde91b2e 100644 --- a/src/pip/_internal/network/__init__.py +++ b/src/pip/_internal/network/__init__.py @@ -0,0 +1,2 @@ +"""Contains purely network-related utilities. +""" diff --git a/src/pip/_internal/network/auth.py b/src/pip/_internal/network/auth.py index c864e24dd01..cce79a521d2 100644 --- a/src/pip/_internal/network/auth.py +++ b/src/pip/_internal/network/auth.py @@ -1,3 +1,9 @@ +"""Network Authentication Helpers + +Contains interface (MultiDomainBasicAuth) and associated glue code for +providing credentials in the context of network requests. +""" + import logging from pip._vendor.requests.auth import AuthBase, HTTPBasicAuth From a8996f25e558de130d43df0b7df59074afadc24b Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 13 Aug 2019 01:43:04 -0400 Subject: [PATCH 0320/3170] Initial triage guide. --- docs/html/development/issue-triage.rst | 286 +++++++++++++++++++++++++ 1 file changed, 286 insertions(+) diff --git a/docs/html/development/issue-triage.rst b/docs/html/development/issue-triage.rst index 621b9e6a453..61378ec8b4b 100644 --- a/docs/html/development/issue-triage.rst +++ b/docs/html/development/issue-triage.rst @@ -23,3 +23,289 @@ user support. In the pip issue tracker, we make use of labels and milestones to organize and track work. + + +Labels +------ + +Issue labels are used to: + +#. Categorize issues +#. Provide status information for contributors and reporters +#. Help contributors find tasks to work on + +The current set of labels are divided into several categories identified by +prefix: + +**C - Category** + which area of ``pip`` functionality a feature request or issue is related to + +**K - Kind** + +**O - Operating System** + for issues that are OS-specific + +**P - Project/Platform** + related to something external to ``pip`` + +**R - Resolution** + no more discussion is really needed, an action has been identified and the + issue is waiting or closed + +**S - State** + for some automatic labels and other indicators that work is needed + +**type** + the role or flavor of an issue + +The specific labels falling into each category have a description that can be +seen on the `Labels <https://github.com/pypa/pip/labels>`__ page. + +In addition, there are several standalone labels: + +**good first issue** + this label marks an issue as beginner-friendly and shows up in banners that + GitHub displays for first-time visitors to the repository + +**triage** + default label given to issues when they are created + +**trivial** + special label that when used on a PR removes the news file requirement + +**needs rebase or merge** + this is a special label used by BrownTruck to mark PRs that have merge + conflicts + + +Automation +---------- + +There are several helpers to manage issues and pull requests. + +Issues created on the issue tracker are automatically given the +``triage`` label by the +`triage-new-issues <https://github.com/apps/triage-new-issues>`__ +bot. The label is automatically removed when another label is added. + +When an issue needs feedback from the author we can label it with +``S: awaiting response``. When the author responds, the +`no-response <https://github.com/apps/no-response>`__ bot removes the label. + +After an issue has been closed for 30 days, the +`lock <https://github.com/apps/lock>`__ bot locks the issue and adds the +``S: auto-locked`` label. This allows us to avoid monitoring existing closed +issues, but unfortunately prevents and references to issues from showing up as +links on the closed issue. + + +Triage Issues +************* + +Users can make issues for a number of reasons: + +#. Suggestions about pip features that could be added or improved +#. Problems using pip +#. Concerns about pip usability +#. General packaging problems to be solved with pip +#. Problems installing or using Python packages +#. Problems managing virtual environments +#. Problems managing Python installations + +To triage issues means to identify what kind of issue is happening and + +* confirm bugs +* provide support +* discuss and design around the uses of the tool + +Specifically, to address an issue: + +#. Read issue title +#. Scan issue description +#. Ask questions +#. If time is available, try to reproduce +#. Search for or remember related issues and link to them +#. Identify an appropriate area of concern (if applicable) + +Keep in mind that all communication is happening with other people and +should be done with respect per the +`Code of Conduct <https://www.pypa.io/en/latest/code-of-conduct/>`__. + +The lifecycle of an issue (bug or support) generally looks like: + +#. waiting for triage (marked with label ``triage``) +#. confirming issue - some discussion with the user, gathering + details, trying to reproduce the issue (may be marked with a specific + category, ``S: awaiting-respose``, ``S: discussion-needed``, or + ``S: need-repro``) +#. confirmed - the issue is pretty consistently reproducible in a + straightforward way, or a mechanism that could be causing the issue has been + identified +#. awaiting fix - the fix is identified and no real discussion on the issue + is needed, should be marked ``R: awaiting PR`` +#. closed - can be for several reasons + + * fixed + * could not be reproduced, no more details could be obtained, and no + progress can be made + * actual issue was with another project or related to system + configuration that pip cannot improve its behavior in the context + of + + +Requesting information +---------------------- + +Requesting more information to better understand the context and environment +that led to the issue. Examples of specific information that may be useful +depending on the situation: + +* pip version: ``pip -V`` +* Python version: ``python -VV`` +* Python path: ``python -c 'import sys; print(sys.executable)'`` +* ``python`` on ``PATH``: Unix: ``which python``; Windows: ``where python`` +* Python as resolved by the shell: ``type python`` +* Origin of pip (get-pip.py, OS-level package manager, ensurepip, manual + installation) +* Using a virtual environment (with `--system-site-packages`?) +* Using a conda environment +* ``PATH`` environment variable +* Network situation (e.g. airgapped environment, firewalls) +* ``--verbose`` output of a failing command +* (Unix) ``strace`` output from a failing command (be careful not to output + into the same directory as a package that's being installed, otherwise pip + will loop forever copying the log file...) +* (Windows) + `procmon <https://docs.microsoft.com/en-us/sysinternals/downloads/procmon>`__ + output during a failing command + (`example request <https://github.com/pypa/pip/issues/6814#issuecomment-516611389>`__) +* Listing of files relevant to the issue (e.g. ``ls -l venv/lib/pythonX.Y/problem-package.dist-info/``) +* whether the unexpected behavior ever worked as expected - if so then what + were the details of the setup (same information as above) + + +Generally, information is good to request if it can help confirm or rule out +possible sources of error. We shouldn't request information that does not +improve our understanding of the situation. + + +Reproducing issues +------------------ + +Whenever an issue happens and the cause isn't obvious, it is important +that we be able to reproduce it independently. This serves several purposes: + +#. If it is a pip bug, then any fix will need tests - a good reproducer + is most of the way towards that. +#. If it is not reproducible using the provided instructions, that helps + rule out a lot of possible causes. +#. A clear set of instructions is an easy way to get on the same page as + someone reporting an issue. + +The best way to reproduce an issue is with a script. + +A script can be copied into a file and executed, whereas shell output +has to be manually copied a line at a time. + +Scripts to reproduce issues should be: + +- portable (few/no assumptions about the system, other that it being Unix or Windows as applicable) +- non-destructive +- convenient +- require little/no setup on the part of the runner + +Examples: + +- creating and installing multiple wheels with different versions + (`link <https://github.com/pypa/pip/issues/4331#issuecomment-520156471>`__) +- using a small web server for authentication errors + (`link <https://github.com/pypa/pip/issues/2920#issuecomment-508953118>`__) +- using docker to test system or global configuration-related issues + (`link <https://github.com/pypa/pip/issues/5533#issuecomment-520159896>`__) +- using docker to test special filesystem permission/configurations + (`link <https://github.com/pypa/pip/issues/6364#issuecomment-507074729>`__) +- using docker for global installation with get-pip + (`link <https://github.com/pypa/pip/issues/6498#issuecomment-513501112>`__) +- get-pip on system with no ``/usr/lib64`` + (`link <https://github.com/pypa/pip/issues/5379#issuecomment-515270576>`__) +- reproducing with ``pip`` from master branch + (`link <https://github.com/pypa/pip/issues/6707#issue-467770959>`__) + + +Reaching resolution +------------------- + +Some user support questions are more related to system configuration than pip. +It's important to treat these issues with the same care and attention as +others, specifically: + +#. Unless the issue is very old and the user doesn't seem active, wait for + confirmation before closing the issue +#. Direct the user to the most appropriate forum for their questions: + + * For Ubuntu, `askubuntu <https://askubuntu.com/>`__ + * For Other linuxes/unixes, `serverfault <https://serverfault.com/>`__ + * For network connectivity issues, + `serverfault <https://serverfault.com/>`__ + +#. Just because a user support question is best solved using some other forum + doesn't mean that we can't make things easier. Try to extract and + understand from the user query how things could have been made easier for + them or you, for example with better warning or error messages. If an issue + does not exist covering that case then create one. If an issue does exist then + make sure to reference that issue before closing this one. +#. A user may be having trouble installing a package, where the package + ``setup.py`` or build-backend configuration is non-trivial. In these cases we + can help to troubleshoot but the best advice is going to be to direct them + to the support channels for the related projects. +#. Do not be hasty to assume it is one cause or another. What looks like + someone else's problem may still be an issue in pip or at least something + that could be improved. +#. For general discussion on Python packaging: + + * `pypa/packaging <https://github.com/pypa/packaging-problems>`__ + * `discuss.python.org/packaging <https://discuss.python.org/c/packaging>`__ + + +Closing issues +-------------- + +An issue may be considered resolved and closed when: + +- for each possible improvement or problem represented in the issue + discussion: + + - Consensus has been reached on a specific action and the actions + appear to be external to the project, with no follow up needed + in the project afterwards. + + - PEP updates (with a corresponding issue in + `python/peps <https://github.com/python/peps>`__) + - already tracked by another issue + + - A project-specific issue has been identified and the issue no + longer occurs as of the latest commit on the master branch. + +- An enhancement or feature request no longer has a proponent and the maintainers + don't think it's worth keeping open. +- An issue has been identified as a duplicate, and it is clearly a duplicate (i.e. the + original report was very good and points directly to the issue) +- The issue has been fixed, and can be independently validated as no longer being an + issue. If this is with code then the specific change/PR that led to it should be + identified and posted for tracking. + + +Common issues +************* + +#. network-related issues - any issue involving retries, address lookup, or + anything like that are typically network issues. +#. issues related to having multiple Python versions, or an OS package + manager-managed pip/python installation (specifically with Debian/Ubuntu). + These typically present themselves as: + + #. Not being able to find installed packages + #. basic libraries not able to be found, fundamental OS components missing + #. In these situations you will want to make sure that we know how they got + their Python and pip. Knowing the relevant package manager commands can + help, e.g. ``dpkg -S``. From 2a4cfcb0215785b5534f1790f9404a75033394b5 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 7 Sep 2019 10:20:34 -0400 Subject: [PATCH 0321/3170] Address review comments. --- docs/html/development/contributing.rst | 2 ++ docs/html/development/issue-triage.rst | 7 ++++--- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/html/development/contributing.rst b/docs/html/development/contributing.rst index 32b0ca864d6..75108f515a2 100644 --- a/docs/html/development/contributing.rst +++ b/docs/html/development/contributing.rst @@ -89,6 +89,8 @@ from a description of the feature/change in one or more paragraphs, each wrapped at 80 characters. Remember that a news entry is meant for end users and should only contain details relevant to an end user. +.. _`choosing-news-entry-type`: + Choosing the type of NEWS entry ------------------------------- diff --git a/docs/html/development/issue-triage.rst b/docs/html/development/issue-triage.rst index 61378ec8b4b..cf125ccdb57 100644 --- a/docs/html/development/issue-triage.rst +++ b/docs/html/development/issue-triage.rst @@ -71,7 +71,8 @@ In addition, there are several standalone labels: default label given to issues when they are created **trivial** - special label that when used on a PR removes the news file requirement + special label for pull requests that removes the + :ref:`news file requirement <choosing-news-entry-type>` **needs rebase or merge** this is a special label used by BrownTruck to mark PRs that have merge @@ -149,8 +150,7 @@ The lifecycle of an issue (bug or support) generally looks like: * could not be reproduced, no more details could be obtained, and no progress can be made * actual issue was with another project or related to system - configuration that pip cannot improve its behavior in the context - of + configuration and pip cannot (or will not) be adapted for it Requesting information @@ -160,6 +160,7 @@ Requesting more information to better understand the context and environment that led to the issue. Examples of specific information that may be useful depending on the situation: +* pip debug: ``pip debug`` * pip version: ``pip -V`` * Python version: ``python -VV`` * Python path: ``python -c 'import sys; print(sys.executable)'`` From 8a144447d7c51845721589a1ef11d4b98b62723d Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sat, 21 Sep 2019 11:11:10 +0530 Subject: [PATCH 0322/3170] Move run_egg_info logic into generate_metadata --- .../_internal/operations/generate_metadata.py | 39 ++++++++++++++++++- src/pip/_internal/req/req_install.py | 32 --------------- 2 files changed, 37 insertions(+), 34 deletions(-) diff --git a/src/pip/_internal/operations/generate_metadata.py b/src/pip/_internal/operations/generate_metadata.py index 2151f855a1c..bdff2e14a67 100644 --- a/src/pip/_internal/operations/generate_metadata.py +++ b/src/pip/_internal/operations/generate_metadata.py @@ -1,12 +1,19 @@ """Metadata generation logic for source distributions. """ +import logging +import os + +from pip._internal.utils.misc import call_subprocess, ensure_dir +from pip._internal.utils.setuptools_build import make_setuptools_shim_args from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Callable + from typing import Callable, List from pip._internal.req.req_install import InstallRequirement +logger = logging.getLogger(__name__) + def get_metadata_generator(install_req): # type: (InstallRequirement) -> Callable[[InstallRequirement], None] @@ -18,7 +25,35 @@ def get_metadata_generator(install_req): def _generate_metadata_legacy(install_req): # type: (InstallRequirement) -> None - install_req.run_egg_info() + if install_req.name: + logger.debug( + 'Running setup.py (path:%s) egg_info for package %s', + install_req.setup_py_path, install_req.name, + ) + else: + logger.debug( + 'Running setup.py (path:%s) egg_info for package from %s', + install_req.setup_py_path, install_req.link, + ) + + base_cmd = make_setuptools_shim_args(install_req.setup_py_path) + if install_req.isolated: + base_cmd += ["--no-user-cfg"] + egg_info_cmd = base_cmd + ['egg_info'] + # We can't put the .egg-info files at the root, because then the + # source code will be mistaken for an installed egg, causing + # problems + if install_req.editable: + egg_base_option = [] # type: List[str] + else: + egg_info_dir = os.path.join(install_req.setup_py_dir, 'pip-egg-info') + ensure_dir(egg_info_dir) + egg_base_option = ['--egg-base', 'pip-egg-info'] + with install_req.build_env: + call_subprocess( + egg_info_cmd + egg_base_option, + cwd=install_req.setup_py_dir, + command_desc='python setup.py egg_info') def _generate_metadata(install_req): diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 18c16547c61..5ce9fe81c96 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -620,38 +620,6 @@ def prepare_pep517_metadata(self): self.metadata_directory = os.path.join(metadata_dir, distinfo_dir) - def run_egg_info(self): - # type: () -> None - if self.name: - logger.debug( - 'Running setup.py (path:%s) egg_info for package %s', - self.setup_py_path, self.name, - ) - else: - logger.debug( - 'Running setup.py (path:%s) egg_info for package from %s', - self.setup_py_path, self.link, - ) - base_cmd = make_setuptools_shim_args( - self.setup_py_path, - no_user_config=self.isolated - ) - egg_info_cmd = base_cmd + ['egg_info'] - # We can't put the .egg-info files at the root, because then the - # source code will be mistaken for an installed egg, causing - # problems - if self.editable: - egg_base_option = [] # type: List[str] - else: - 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'] - with self.build_env: - call_subprocess( - egg_info_cmd + egg_base_option, - cwd=self.setup_py_dir, - command_desc='python setup.py egg_info') - @property def egg_info_path(self): # type: () -> str From 7ad5670c4e00c532184af437142fa2105c0b0136 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sat, 21 Sep 2019 11:30:49 +0530 Subject: [PATCH 0323/3170] Improve code flow of _generate_metadata_legacy Because it was a little difficult to follow. Because the comments weren't helpful unless you've built enough context. --- .../_internal/operations/generate_metadata.py | 39 +++++++++---------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/src/pip/_internal/operations/generate_metadata.py b/src/pip/_internal/operations/generate_metadata.py index bdff2e14a67..368ecdeb492 100644 --- a/src/pip/_internal/operations/generate_metadata.py +++ b/src/pip/_internal/operations/generate_metadata.py @@ -25,35 +25,34 @@ def get_metadata_generator(install_req): def _generate_metadata_legacy(install_req): # type: (InstallRequirement) -> None - if install_req.name: - logger.debug( - 'Running setup.py (path:%s) egg_info for package %s', - install_req.setup_py_path, install_req.name, - ) - else: - logger.debug( - 'Running setup.py (path:%s) egg_info for package from %s', - install_req.setup_py_path, install_req.link, - ) + req_details_str = install_req.name or "from {}".format(install_req.link) + logger.debug( + 'Running setup.py (path:%s) egg_info for package %s', + install_req.setup_py_path, req_details_str, + ) + # Compose arguments for subprocess call base_cmd = make_setuptools_shim_args(install_req.setup_py_path) if install_req.isolated: base_cmd += ["--no-user-cfg"] - egg_info_cmd = base_cmd + ['egg_info'] - # We can't put the .egg-info files at the root, because then the - # source code will be mistaken for an installed egg, causing - # problems - if install_req.editable: - egg_base_option = [] # type: List[str] - else: + + # For non-editable installed, don't put the .egg-info files at the root, + # to avoid confusion due to the source code being considered an installed + # egg. + egg_base_option = [] # type: List[str] + if not install_req.editable: egg_info_dir = os.path.join(install_req.setup_py_dir, 'pip-egg-info') + egg_base_option = ['--egg-base', egg_info_dir] + + # setuptools complains if the target directory does not exist. ensure_dir(egg_info_dir) - egg_base_option = ['--egg-base', 'pip-egg-info'] + with install_req.build_env: call_subprocess( - egg_info_cmd + egg_base_option, + base_cmd + ["egg_info"] + egg_base_option, cwd=install_req.setup_py_dir, - command_desc='python setup.py egg_info') + command_desc='python setup.py egg_info', + ) def _generate_metadata(install_req): From ab6d5505926e636f2a0bdab2b2e7e3ab5aa2e217 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sat, 21 Sep 2019 14:30:14 +0530 Subject: [PATCH 0324/3170] Remove link.is_artifact --- src/pip/_internal/models/link.py | 9 --------- src/pip/_internal/operations/prepare.py | 2 +- tests/unit/test_wheel.py | 2 +- 3 files changed, 2 insertions(+), 11 deletions(-) diff --git a/src/pip/_internal/models/link.py b/src/pip/_internal/models/link.py index 23f549148a0..c109804b8f3 100644 --- a/src/pip/_internal/models/link.py +++ b/src/pip/_internal/models/link.py @@ -192,15 +192,6 @@ def is_vcs(self): return self.scheme in vcs.all_schemes - @property - def is_artifact(self): - # type: () -> bool - """ - 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. - """ - return not self.is_vcs - @property def is_yanked(self): # type: () -> bool diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index f954c9859c3..64d41fa4e04 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -225,7 +225,7 @@ def prepare_linked_requirement( if self._download_should_save: # Make a .zip of the source_dir we already created. - if not link.is_artifact: + if link.is_vcs: req.archive(self.download_dir) return abstract_dist diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 2dee519be9e..2a824c7fd7b 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -101,7 +101,7 @@ def test_should_use_ephemeral_cache__issue_6197( """ req = make_test_install_req(base_name=base_name) assert not req.is_wheel - assert req.link.is_artifact + assert not req.link.is_vcs always_true = Mock(return_value=True) From 144051e4228ae88bf25988a4e4ef4a86cf208431 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek <chris.jerdonek@gmail.com> Date: Fri, 13 Sep 2019 10:20:34 -0700 Subject: [PATCH 0325/3170] Simplify local_repos.py. --- tests/lib/local_repos.py | 70 ++++++++++++++-------------------------- 1 file changed, 25 insertions(+), 45 deletions(-) diff --git a/tests/lib/local_repos.py b/tests/lib/local_repos.py index 40cb19b678a..5876f0de655 100644 --- a/tests/lib/local_repos.py +++ b/tests/lib/local_repos.py @@ -7,27 +7,32 @@ from pip._internal.utils.misc import hide_url from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from pip._internal.vcs import bazaar, git, mercurial, subversion +from pip._internal.vcs import vcs from tests.lib import path_to_url if MYPY_CHECK_RUNNING: from tests.lib.path import Path -def _create_initools_repository(directory): - subprocess.check_call('svnadmin create INITools'.split(), cwd=directory) +def _ensure_svn_initools_repo(directory): + """ + Create the SVN INITools repo if it doesn't already exist. + """ + initools_dir = os.path.join(directory, 'INITools') + repo_url_path = os.path.join(initools_dir, 'trunk') + if os.path.exists(initools_dir): + return repo_url_path + subprocess.check_call('svnadmin create INITools'.split(), cwd=directory) -def _dump_initools_repository(directory): filename, _ = urllib_request.urlretrieve( 'http://bitbucket.org/hltbra/pip-initools-dump/raw/8b55c908a320/' 'INITools_modified.dump' ) - initools_folder = os.path.join(directory, 'INITools') devnull = open(os.devnull, 'w') dump = open(filename) subprocess.check_call( - ['svnadmin', 'load', initools_folder], + ['svnadmin', 'load', initools_dir], stdin=dump, stdout=devnull, ) @@ -35,41 +40,7 @@ def _dump_initools_repository(directory): devnull.close() os.remove(filename) - -def _create_svn_repository_for_initools(directory): - if not os.path.exists(os.path.join(directory, 'INITools')): - _create_initools_repository(directory) - _dump_initools_repository(directory) - - -def _get_vcs_and_checkout_url(remote_repository, directory): - vcs_classes = {'svn': subversion.Subversion, - 'git': git.Git, - 'bzr': bazaar.Bazaar, - 'hg': mercurial.Mercurial} - default_vcs = 'svn' - if '+' not in remote_repository: - remote_repository = '%s+%s' % (default_vcs, remote_repository) - vcs, repository_path = remote_repository.split('+', 1) - vcs_class = vcs_classes[vcs] - branch = '' - if vcs == 'svn': - branch = os.path.basename(remote_repository) - # remove the slash - repository_name = os.path.basename( - remote_repository[:-len(branch) - 1] - ) - else: - repository_name = os.path.basename(remote_repository) - - destination_path = os.path.join(directory, repository_name) - if not os.path.exists(destination_path): - url = hide_url(remote_repository) - vcs_class().obtain(destination_path, url=url) - return '%s+%s' % ( - vcs, - path_to_url('/'.join([directory, repository_name, branch])), - ) + return repo_url_path def local_checkout( @@ -82,14 +53,23 @@ def local_checkout( temp directory Path object unique to each test function invocation, created as a sub directory of the base temp directory. """ + assert '+' in remote_repo + vcs_name, repository_path = remote_repo.split('+', 1) + directory = temp_path.joinpath('cache') if not os.path.exists(directory): os.mkdir(directory) - # os.makedirs(directory) - if remote_repo.startswith('svn'): - _create_svn_repository_for_initools(directory) - return _get_vcs_and_checkout_url(remote_repo, directory) + if vcs_name == 'svn': + assert remote_repo.endswith('/INITools/trunk') + repo_url_path = _ensure_svn_initools_repo(directory) + else: + repository_name = os.path.basename(remote_repo) + repo_url_path = os.path.join(directory, repository_name) + vcs_backend = vcs.get_backend(vcs_name) + vcs_backend.obtain(repo_url_path, url=hide_url(remote_repo)) + + return '{}+{}'.format(vcs_name, path_to_url(repo_url_path)) def local_repo(remote_repo, temp_path): From 1ca61e14ba43c1316acab3c5eacf903647a1d8cb Mon Sep 17 00:00:00 2001 From: Chris Jerdonek <chris.jerdonek@gmail.com> Date: Sat, 21 Sep 2019 02:29:38 -0700 Subject: [PATCH 0326/3170] Assert that repo_url_path doesn't exist. --- tests/functional/test_install_vcs_git.py | 7 ++++--- tests/lib/local_repos.py | 1 + 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/functional/test_install_vcs_git.py b/tests/functional/test_install_vcs_git.py index feb9ef0401e..e19745bb8a2 100644 --- a/tests/functional/test_install_vcs_git.py +++ b/tests/functional/test_install_vcs_git.py @@ -276,12 +276,13 @@ def test_git_with_tag_name_and_update(script, tmpdir): Test cloning a git repository and updating to a different version. """ url_path = 'pypa/pip-test-package.git' - local_url = _github_checkout(url_path, tmpdir, egg='pip-test-package') + base_local_url = _github_checkout(url_path, tmpdir) + + local_url = '{}#egg=pip-test-package'.format(base_local_url) result = script.pip('install', '-e', local_url) result.assert_installed('pip-test-package', with_files=['.git']) - new_local_url = _github_checkout(url_path, tmpdir) - new_local_url += '@0.1.2#egg=pip-test-package' + new_local_url = '{}@0.1.2#egg=pip-test-package'.format(base_local_url) result = script.pip( 'install', '--global-option=--version', '-e', new_local_url, ) diff --git a/tests/lib/local_repos.py b/tests/lib/local_repos.py index 5876f0de655..fe896f7f381 100644 --- a/tests/lib/local_repos.py +++ b/tests/lib/local_repos.py @@ -66,6 +66,7 @@ def local_checkout( else: repository_name = os.path.basename(remote_repo) repo_url_path = os.path.join(directory, repository_name) + assert not os.path.exists(repo_url_path) vcs_backend = vcs.get_backend(vcs_name) vcs_backend.obtain(repo_url_path, url=hide_url(remote_repo)) From d7709fa1061600e0606f8732f0fb1e4fb98fb874 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek <chris.jerdonek@gmail.com> Date: Thu, 19 Sep 2019 00:50:16 -0700 Subject: [PATCH 0327/3170] Assert that initools_dir doesn't already exist. --- tests/functional/test_uninstall.py | 10 ++++++---- tests/lib/local_repos.py | 12 +++++------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/tests/functional/test_uninstall.py b/tests/functional/test_uninstall.py index 8ea54224677..0ec1cd38bfc 100644 --- a/tests/functional/test_uninstall.py +++ b/tests/functional/test_uninstall.py @@ -358,13 +358,16 @@ def test_uninstall_from_reqs_file(script, tmpdir): Test uninstall from a requirements file. """ + local_svn_url = local_checkout( + 'svn+http://svn.colorstudy.com/INITools/trunk', + tmpdir, + ) script.scratch_path.joinpath("test-req.txt").write_text( textwrap.dedent(""" -e %s#egg=initools # and something else to test out: PyLogo<0.4 - """) % - local_checkout('svn+http://svn.colorstudy.com/INITools/trunk', tmpdir) + """) % local_svn_url ) result = script.pip('install', '-r', 'test-req.txt') script.scratch_path.joinpath("test-req.txt").write_text( @@ -377,8 +380,7 @@ def test_uninstall_from_reqs_file(script, tmpdir): -e %s#egg=initools # and something else to test out: PyLogo<0.4 - """) % - local_checkout('svn+http://svn.colorstudy.com/INITools/trunk', tmpdir) + """) % local_svn_url ) result2 = script.pip('uninstall', '-r', 'test-req.txt', '-y') assert_all_changes( diff --git a/tests/lib/local_repos.py b/tests/lib/local_repos.py index fe896f7f381..5b15b9b6516 100644 --- a/tests/lib/local_repos.py +++ b/tests/lib/local_repos.py @@ -14,14 +14,12 @@ from tests.lib.path import Path -def _ensure_svn_initools_repo(directory): +def _create_svn_initools_repo(directory): """ - Create the SVN INITools repo if it doesn't already exist. + Create the SVN INITools repo. """ initools_dir = os.path.join(directory, 'INITools') - repo_url_path = os.path.join(initools_dir, 'trunk') - if os.path.exists(initools_dir): - return repo_url_path + assert not os.path.exists(initools_dir) subprocess.check_call('svnadmin create INITools'.split(), cwd=directory) @@ -40,7 +38,7 @@ def _ensure_svn_initools_repo(directory): devnull.close() os.remove(filename) - return repo_url_path + return os.path.join(initools_dir, 'trunk') def local_checkout( @@ -62,7 +60,7 @@ def local_checkout( if vcs_name == 'svn': assert remote_repo.endswith('/INITools/trunk') - repo_url_path = _ensure_svn_initools_repo(directory) + repo_url_path = _create_svn_initools_repo(directory) else: repository_name = os.path.basename(remote_repo) repo_url_path = os.path.join(directory, repository_name) From 188ec6ff4e976ac9b82ac134a9925cf1ab5d88fd Mon Sep 17 00:00:00 2001 From: Chris Jerdonek <chris.jerdonek@gmail.com> Date: Sat, 21 Sep 2019 03:01:17 -0700 Subject: [PATCH 0328/3170] Don't pass "/trunk" when calling local_checkout() with svn. --- tests/functional/test_install_reqs.py | 4 +--- tests/functional/test_install_user.py | 4 +--- tests/functional/test_uninstall.py | 7 +++---- tests/lib/local_repos.py | 22 ++++++++++------------ 4 files changed, 15 insertions(+), 22 deletions(-) diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index b906c37b642..e0eed8715bb 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -110,9 +110,7 @@ def test_multiple_requirements_files(script, tmpdir): -r %s-req.txt """) % ( - local_checkout( - 'svn+http://svn.colorstudy.com/INITools/trunk', tmpdir, - ), + local_checkout('svn+http://svn.colorstudy.com/INITools', tmpdir), other_lib_name ), ) diff --git a/tests/functional/test_install_user.py b/tests/functional/test_install_user.py index d668ed12cb0..537f402769b 100644 --- a/tests/functional/test_install_user.py +++ b/tests/functional/test_install_user.py @@ -51,9 +51,7 @@ def test_install_subversion_usersite_editable_with_distribute( result = script.pip( 'install', '--user', '-e', '%s#egg=initools' % - local_checkout( - 'svn+http://svn.colorstudy.com/INITools/trunk', tmpdir, - ) + local_checkout('svn+http://svn.colorstudy.com/INITools', tmpdir) ) result.assert_installed('INITools', use_user_site=True) diff --git a/tests/functional/test_uninstall.py b/tests/functional/test_uninstall.py index 0ec1cd38bfc..13d18768edc 100644 --- a/tests/functional/test_uninstall.py +++ b/tests/functional/test_uninstall.py @@ -295,8 +295,8 @@ def test_uninstall_editable_from_svn(script, tmpdir): """ result = script.pip( 'install', '-e', - '%s#egg=initools' % local_checkout( - 'svn+http://svn.colorstudy.com/INITools/trunk', tmpdir, + '%s#egg=initools' % ( + local_checkout('svn+http://svn.colorstudy.com/INITools', tmpdir) ), ) result.assert_installed('INITools') @@ -359,8 +359,7 @@ def test_uninstall_from_reqs_file(script, tmpdir): """ local_svn_url = local_checkout( - 'svn+http://svn.colorstudy.com/INITools/trunk', - tmpdir, + 'svn+http://svn.colorstudy.com/INITools', tmpdir, ) script.scratch_path.joinpath("test-req.txt").write_text( textwrap.dedent(""" diff --git a/tests/lib/local_repos.py b/tests/lib/local_repos.py index 5b15b9b6516..2a41595f9f2 100644 --- a/tests/lib/local_repos.py +++ b/tests/lib/local_repos.py @@ -14,13 +14,11 @@ from tests.lib.path import Path -def _create_svn_initools_repo(directory): +def _create_svn_initools_repo(initools_dir): """ Create the SVN INITools repo. """ - initools_dir = os.path.join(directory, 'INITools') - assert not os.path.exists(initools_dir) - + directory = os.path.dirname(initools_dir) subprocess.check_call('svnadmin create INITools'.split(), cwd=directory) filename, _ = urllib_request.urlretrieve( @@ -38,8 +36,6 @@ def _create_svn_initools_repo(directory): devnull.close() os.remove(filename) - return os.path.join(initools_dir, 'trunk') - def local_checkout( remote_repo, # type: str @@ -52,19 +48,21 @@ def local_checkout( created as a sub directory of the base temp directory. """ assert '+' in remote_repo - vcs_name, repository_path = remote_repo.split('+', 1) + vcs_name = remote_repo.split('+', 1)[0] + repository_name = os.path.basename(remote_repo) directory = temp_path.joinpath('cache') + repo_url_path = os.path.join(directory, repository_name) + assert not os.path.exists(repo_url_path) + if not os.path.exists(directory): os.mkdir(directory) if vcs_name == 'svn': - assert remote_repo.endswith('/INITools/trunk') - repo_url_path = _create_svn_initools_repo(directory) + assert repository_name == 'INITools' + _create_svn_initools_repo(repo_url_path) + repo_url_path = os.path.join(repo_url_path, 'trunk') else: - repository_name = os.path.basename(remote_repo) - repo_url_path = os.path.join(directory, repository_name) - assert not os.path.exists(repo_url_path) vcs_backend = vcs.get_backend(vcs_name) vcs_backend.obtain(repo_url_path, url=hide_url(remote_repo)) From f5e5f403b6b185c690d8c87a4b66a92b8c8997fb Mon Sep 17 00:00:00 2001 From: Chris Jerdonek <chris.jerdonek@gmail.com> Date: Sat, 21 Sep 2019 10:10:53 -0700 Subject: [PATCH 0329/3170] Test logging in test_collect_links(). --- tests/unit/test_collector.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_collector.py b/tests/unit/test_collector.py index 1e90948ebce..82227a76304 100644 --- a/tests/unit/test_collector.py +++ b/tests/unit/test_collector.py @@ -397,7 +397,9 @@ def check_links_include(links, names): class TestLinkCollector(object): @patch('pip._internal.collector._get_html_response') - def test_collect_links(self, mock_get_html_response, data): + def test_collect_links(self, mock_get_html_response, caplog, data): + caplog.set_level(logging.DEBUG) + expected_url = 'https://pypi.org/simple/twine/' fake_page = make_fake_html_page(expected_url) @@ -405,7 +407,9 @@ def test_collect_links(self, mock_get_html_response, data): link_collector = make_test_link_collector( find_links=[data.find_links], - index_urls=[PyPI.simple_url], + # Include two copies of the URL to check that the second one + # is skipped. + index_urls=[PyPI.simple_url, PyPI.simple_url], ) actual = link_collector.collect_links('twine') @@ -427,3 +431,10 @@ def test_collect_links(self, mock_get_html_response, data): assert actual_page_links[0].url == ( 'https://pypi.org/abc-1.0.tar.gz#md5=000000000' ) + + actual = [record_tuple[1:] for record_tuple in caplog.record_tuples] + assert actual == [ + (logging.DEBUG, '2 location(s) to search for versions of twine:'), + (logging.DEBUG, '* https://pypi.org/simple/twine/'), + (logging.DEBUG, '* https://pypi.org/simple/twine/'), + ] From 7b7d16287063181679e2273daa7d702aed672e55 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek <chris.jerdonek@gmail.com> Date: Sat, 21 Sep 2019 10:26:17 -0700 Subject: [PATCH 0330/3170] Add _remove_duplicate_links(), and test. --- src/pip/_internal/collector.py | 10 ++++++++++ tests/unit/test_collector.py | 15 +++++++++++++++ 2 files changed, 25 insertions(+) diff --git a/src/pip/_internal/collector.py b/src/pip/_internal/collector.py index 443e22a7f5b..c88b7d3b8a9 100644 --- a/src/pip/_internal/collector.py +++ b/src/pip/_internal/collector.py @@ -7,6 +7,7 @@ import logging import mimetypes import os +from collections import OrderedDict from pip._vendor import html5lib, requests from pip._vendor.distlib.compat import unescape @@ -356,6 +357,15 @@ def _get_html_page(link, session=None): return None +def _remove_duplicate_links(links): + # type: (Iterable[Link]) -> List[Link] + """ + Return a list of links, with duplicates removed and ordering preserved. + """ + # We preserve the ordering when removing duplicates because we can. + return list(OrderedDict.fromkeys(links)) + + def group_locations(locations, expand_dir=False): # type: (Sequence[str], bool) -> Tuple[List[str], List[str]] """ diff --git a/tests/unit/test_collector.py b/tests/unit/test_collector.py index 82227a76304..a693c8cab50 100644 --- a/tests/unit/test_collector.py +++ b/tests/unit/test_collector.py @@ -16,6 +16,7 @@ _get_html_response, _NotHTML, _NotHTTP, + _remove_duplicate_links, group_locations, ) from pip._internal.download import PipSession @@ -343,6 +344,20 @@ def test_get_html_page_directory_append_index(tmpdir): ] +def test_remove_duplicate_links(): + links = [ + # We choose Links that will test that ordering is preserved. + Link('https://example.com/2'), + Link('https://example.com/1'), + Link('https://example.com/2'), + ] + actual = _remove_duplicate_links(links) + assert actual == [ + Link('https://example.com/2'), + Link('https://example.com/1'), + ] + + def test_group_locations__file_expand_dir(data): """ Test that a file:// dir gets listdir run with expand_dir From a2b2a245132aa6c926f8b7101f339ffa52734a7b Mon Sep 17 00:00:00 2001 From: Chris Jerdonek <chris.jerdonek@gmail.com> Date: Sat, 21 Sep 2019 10:29:55 -0700 Subject: [PATCH 0331/3170] Remove duplicate links before logging. --- src/pip/_internal/collector.py | 8 ++------ tests/unit/test_collector.py | 3 +-- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/collector.py b/src/pip/_internal/collector.py index c88b7d3b8a9..a1688d932b4 100644 --- a/src/pip/_internal/collector.py +++ b/src/pip/_internal/collector.py @@ -25,7 +25,7 @@ if MYPY_CHECK_RUNNING: from typing import ( Callable, Dict, Iterable, List, MutableMapping, Optional, Sequence, - Set, Tuple, Union, + Tuple, Union, ) import xml.etree.ElementTree @@ -482,12 +482,7 @@ def _get_pages(self, locations): Yields (page, page_url) from the given locations, skipping locations that have errors. """ - seen = set() # type: Set[Link] for location in locations: - if location in seen: - continue - seen.add(location) - page = _get_html_page(location, session=self.session) if page is None: continue @@ -525,6 +520,7 @@ def collect_links(self, project_name): if self.session.is_secure_origin(link) ] + url_locations = _remove_duplicate_links(url_locations) logger.debug('%d location(s) to search for versions of %s:', len(url_locations), project_name) diff --git a/tests/unit/test_collector.py b/tests/unit/test_collector.py index a693c8cab50..18871c85d29 100644 --- a/tests/unit/test_collector.py +++ b/tests/unit/test_collector.py @@ -449,7 +449,6 @@ def test_collect_links(self, mock_get_html_response, caplog, data): actual = [record_tuple[1:] for record_tuple in caplog.record_tuples] assert actual == [ - (logging.DEBUG, '2 location(s) to search for versions of twine:'), - (logging.DEBUG, '* https://pypi.org/simple/twine/'), + (logging.DEBUG, '1 location(s) to search for versions of twine:'), (logging.DEBUG, '* https://pypi.org/simple/twine/'), ] From 09fd200c599de4fadf2ff814a1bef855bc6d77e8 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 21 Sep 2019 15:21:23 -0400 Subject: [PATCH 0332/3170] Move `pip._internal:main` to its own module Moving content out of `__init__` is preferred in general because it avoids conflicts with module names and unnecessary imports. --- setup.py | 6 ++--- src/pip/__main__.py | 2 +- src/pip/_internal/__init__.py | 38 ------------------------------ src/pip/_internal/main.py | 44 +++++++++++++++++++++++++++++++++++ tests/conftest.py | 4 ++-- tests/unit/test_options.py | 2 +- 6 files changed, 51 insertions(+), 45 deletions(-) create mode 100644 src/pip/_internal/main.py diff --git a/setup.py b/setup.py index ed1f0109818..b05c1b8779b 100644 --- a/setup.py +++ b/setup.py @@ -71,9 +71,9 @@ def find_version(*file_paths): }, entry_points={ "console_scripts": [ - "pip=pip._internal:main", - "pip%s=pip._internal:main" % sys.version_info[:1], - "pip%s.%s=pip._internal:main" % sys.version_info[:2], + "pip=pip._internal.main:main", + "pip%s=pip._internal.main:main" % sys.version_info[:1], + "pip%s.%s=pip._internal.main:main" % sys.version_info[:2], ], }, diff --git a/src/pip/__main__.py b/src/pip/__main__.py index 0c223f8c187..49b6fdf71ca 100644 --- a/src/pip/__main__.py +++ b/src/pip/__main__.py @@ -13,7 +13,7 @@ path = os.path.dirname(os.path.dirname(__file__)) sys.path.insert(0, path) -from pip._internal import main as _main # isort:skip # noqa +from pip._internal.main import main as _main # isort:skip # noqa if __name__ == '__main__': sys.exit(_main()) diff --git a/src/pip/_internal/__init__.py b/src/pip/_internal/__init__.py index bb5cbff323c..e30d71ea3d4 100755 --- a/src/pip/_internal/__init__.py +++ b/src/pip/_internal/__init__.py @@ -1,10 +1,6 @@ #!/usr/bin/env python from __future__ import absolute_import -import locale -import logging -import os -import sys import warnings # We ignore certain warnings from urllib3, since they are not relevant to pip's @@ -16,10 +12,6 @@ import pip._internal.utils.inject_securetransport # noqa from pip._internal.cli.autocompletion import autocomplete -from pip._internal.cli.main_parser import parse_command -from pip._internal.commands import create_command -from pip._internal.exceptions import PipError -from pip._internal.utils import deprecation # Raised when using --trusted-host. warnings.filterwarnings("ignore", category=InsecureRequestWarning) @@ -27,33 +19,3 @@ # Barry Warsaw noted (on 2016-06-17) that this should be done before # importing pip.vcs, which has since moved to pip._internal.vcs. warnings.filterwarnings("ignore", category=DependencyWarning) - -logger = logging.getLogger(__name__) - - -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 = parse_command(args) - except PipError as exc: - sys.stderr.write("ERROR: %s" % exc) - sys.stderr.write(os.linesep) - sys.exit(1) - - # Needed for locale.getpreferredencoding(False) to work - # in pip._internal.utils.encoding.auto_decode - try: - locale.setlocale(locale.LC_ALL, '') - except locale.Error as e: - # setlocale can apparently crash if locale are uninitialized - logger.debug("Ignoring error %s when setting locale", e) - command = create_command(cmd_name, isolated=("--isolated" in cmd_args)) - - return command.main(cmd_args) diff --git a/src/pip/_internal/main.py b/src/pip/_internal/main.py new file mode 100644 index 00000000000..9b55d0f02de --- /dev/null +++ b/src/pip/_internal/main.py @@ -0,0 +1,44 @@ +"""Primary application entrypoint. +""" +from __future__ import absolute_import + +import locale +import logging +import os +import sys + +from pip._internal.cli.autocompletion import autocomplete +from pip._internal.cli.main_parser import parse_command +from pip._internal.commands import create_command +from pip._internal.exceptions import PipError +from pip._internal.utils import deprecation + +logger = logging.getLogger(__name__) + + +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 = parse_command(args) + except PipError as exc: + sys.stderr.write("ERROR: %s" % exc) + sys.stderr.write(os.linesep) + sys.exit(1) + + # Needed for locale.getpreferredencoding(False) to work + # in pip._internal.utils.encoding.auto_decode + try: + locale.setlocale(locale.LC_ALL, '') + except locale.Error as e: + # setlocale can apparently crash if locale are uninitialized + logger.debug("Ignoring error %s when setting locale", e) + command = create_command(cmd_name, isolated=("--isolated" in cmd_args)) + + return command.main(cmd_args) diff --git a/tests/conftest.py b/tests/conftest.py index 5982ebf9014..0e704f874fa 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,7 +10,7 @@ import six from setuptools.wheel import Wheel -import pip._internal +import pip._internal.main from tests.lib import DATA_DIR, SRC_DIR, TestData from tests.lib.path import Path from tests.lib.scripttest import PipTestEnvironment @@ -342,7 +342,7 @@ def pip(self, *args): stdout = io.BytesIO() sys.stdout = stdout try: - returncode = pip._internal.main(list(args)) + returncode = pip._internal.main.main(list(args)) except SystemExit as e: returncode = e.code or 0 finally: diff --git a/tests/unit/test_options.py b/tests/unit/test_options.py index 32509209869..c49801d99de 100644 --- a/tests/unit/test_options.py +++ b/tests/unit/test_options.py @@ -4,9 +4,9 @@ import pytest import pip._internal.configuration -from pip._internal import main from pip._internal.commands import create_command from pip._internal.exceptions import PipError +from pip._internal.main import main from tests.lib.options_helpers import AddFakeCommandMixin From b0eed267a62c54abb07b4fa62c6c1196b276b192 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 21 Sep 2019 15:27:27 -0400 Subject: [PATCH 0333/3170] Remove autocomplete import from pip._internal.__init__ --- src/pip/_internal/__init__.py | 1 - tests/functional/test_completion.py | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/__init__.py b/src/pip/_internal/__init__.py index e30d71ea3d4..b88a97c46c8 100755 --- a/src/pip/_internal/__init__.py +++ b/src/pip/_internal/__init__.py @@ -11,7 +11,6 @@ ) import pip._internal.utils.inject_securetransport # noqa -from pip._internal.cli.autocompletion import autocomplete # Raised when using --trusted-host. warnings.filterwarnings("ignore", category=InsecureRequestWarning) diff --git a/tests/functional/test_completion.py b/tests/functional/test_completion.py index 5491ce22d30..9280b5d6a8a 100644 --- a/tests/functional/test_completion.py +++ b/tests/functional/test_completion.py @@ -78,7 +78,9 @@ def setup_completion(script, words, cword, cwd=None): # expect_error is True because autocomplete exists with 1 status code result = script.run( - 'python', '-c', 'import pip._internal;pip._internal.autocomplete()', + 'python', '-c', + 'from pip._internal.cli.autocompletion import autocomplete;' + 'autocomplete()', expect_error=True, cwd=cwd, ) From 99c29af3cef67525f6a1e3d963c5c27da6620245 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 21 Sep 2019 16:04:46 -0400 Subject: [PATCH 0334/3170] Move SafeFileCache to network.cache. --- src/pip/_internal/download.py | 69 +--------------------------- src/pip/_internal/network/cache.py | 71 +++++++++++++++++++++++++++++ tests/unit/test_download.py | 57 ----------------------- tests/unit/test_networking_cache.py | 62 +++++++++++++++++++++++++ 4 files changed, 135 insertions(+), 124 deletions(-) create mode 100644 src/pip/_internal/network/cache.py create mode 100644 tests/unit/test_networking_cache.py diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index 53646149cc3..8367e2e98d7 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -10,12 +10,9 @@ import re import shutil import sys -from contextlib import contextmanager from pip._vendor import requests, six, urllib3 from pip._vendor.cachecontrol import CacheControlAdapter -from pip._vendor.cachecontrol.cache import BaseCache -from pip._vendor.cachecontrol.caches import FileCache from pip._vendor.requests.adapters import BaseAdapter, HTTPAdapter from pip._vendor.requests.models import CONTENT_CHUNK_SIZE, Response from pip._vendor.requests.structures import CaseInsensitiveDict @@ -29,15 +26,11 @@ from pip._internal.exceptions import HashMismatch, InstallationError from pip._internal.models.index import PyPI from pip._internal.network.auth import MultiDomainBasicAuth +from pip._internal.network.cache import SafeFileCache # Import ssl from compat so the initial import occurs in only one place. from pip._internal.utils.compat import HAS_TLS, ipaddress, ssl from pip._internal.utils.encoding import auto_decode -from pip._internal.utils.filesystem import ( - adjacent_tmp_file, - check_path_owner, - copy2_fixed, - replace, -) +from pip._internal.utils.filesystem import check_path_owner, copy2_fixed from pip._internal.utils.glibc import libc_ver from pip._internal.utils.misc import ( ask_path_exists, @@ -45,7 +38,6 @@ build_url_from_netloc, consume, display_path, - ensure_dir, format_size, get_installed_version, hide_url, @@ -263,63 +255,6 @@ def close(self): pass -@contextmanager -def suppressed_cache_errors(): - """If we can't access the cache then we can just skip caching and process - requests as if caching wasn't enabled. - """ - try: - yield - except (OSError, IOError): - pass - - -class SafeFileCache(BaseCache): - """ - A file based cache which is safe to use even when the target directory may - not be accessible or writable. - """ - - def __init__(self, directory): - # type: (str) -> None - assert directory is not None, "Cache directory must not be None." - super(SafeFileCache, self).__init__() - self.directory = directory - - def _get_cache_path(self, name): - # type: (str) -> str - # From cachecontrol.caches.file_cache.FileCache._fn, brought into our - # class for backwards-compatibility and to avoid using a non-public - # method. - hashed = FileCache.encode(name) - parts = list(hashed[:5]) + [hashed] - return os.path.join(self.directory, *parts) - - def get(self, key): - # type: (str) -> Optional[bytes] - path = self._get_cache_path(key) - with suppressed_cache_errors(): - with open(path, 'rb') as f: - return f.read() - - def set(self, key, value): - # type: (str, bytes) -> None - path = self._get_cache_path(key) - with suppressed_cache_errors(): - ensure_dir(os.path.dirname(path)) - - with adjacent_tmp_file(path) as f: - f.write(value) - - replace(f.name, path) - - def delete(self, key): - # type: (str) -> None - path = self._get_cache_path(key) - with suppressed_cache_errors(): - os.remove(path) - - class InsecureHTTPAdapter(HTTPAdapter): def cert_verify(self, conn, url, verify, cert): diff --git a/src/pip/_internal/network/cache.py b/src/pip/_internal/network/cache.py new file mode 100644 index 00000000000..9cd6403003e --- /dev/null +++ b/src/pip/_internal/network/cache.py @@ -0,0 +1,71 @@ +"""HTTP cache implementation. +""" +import os +from contextlib import contextmanager + +from pip._vendor.cachecontrol.cache import BaseCache +from pip._vendor.cachecontrol.caches import FileCache + +from pip._internal.utils.filesystem import adjacent_tmp_file, replace +from pip._internal.utils.misc import ensure_dir +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import Optional + + +@contextmanager +def suppressed_cache_errors(): + """If we can't access the cache then we can just skip caching and process + requests as if caching wasn't enabled. + """ + try: + yield + except (OSError, IOError): + pass + + +class SafeFileCache(BaseCache): + """ + A file based cache which is safe to use even when the target directory may + not be accessible or writable. + """ + + def __init__(self, directory): + # type: (str) -> None + assert directory is not None, "Cache directory must not be None." + super(SafeFileCache, self).__init__() + self.directory = directory + + def _get_cache_path(self, name): + # type: (str) -> str + # From cachecontrol.caches.file_cache.FileCache._fn, brought into our + # class for backwards-compatibility and to avoid using a non-public + # method. + hashed = FileCache.encode(name) + parts = list(hashed[:5]) + [hashed] + return os.path.join(self.directory, *parts) + + def get(self, key): + # type: (str) -> Optional[bytes] + path = self._get_cache_path(key) + with suppressed_cache_errors(): + with open(path, 'rb') as f: + return f.read() + + def set(self, key, value): + # type: (str, bytes) -> None + path = self._get_cache_path(key) + with suppressed_cache_errors(): + ensure_dir(os.path.dirname(path)) + + with adjacent_tmp_file(path) as f: + f.write(value) + + replace(f.name, path) + + def delete(self, key): + # type: (str) -> None + path = self._get_cache_path(key) + with suppressed_cache_errors(): + os.remove(path) diff --git a/tests/unit/test_download.py b/tests/unit/test_download.py index 3d052619e46..b0610556552 100644 --- a/tests/unit/test_download.py +++ b/tests/unit/test_download.py @@ -9,13 +9,11 @@ import pytest from mock import Mock, patch -from pip._vendor.cachecontrol.caches import FileCache import pip from pip._internal.download import ( CI_ENVIRONMENT_VARIABLES, PipSession, - SafeFileCache, _copy_source_tree, _download_http_url, parse_content_disposition, @@ -36,13 +34,6 @@ from tests.lib.path import Path -@pytest.fixture(scope="function") -def cache_tmpdir(tmpdir): - cache_dir = tmpdir.joinpath("cache") - cache_dir.mkdir(parents=True) - yield cache_dir - - def test_unpack_http_url_with_urllib_response_without_content_type(data): """ It should download and unpack files even if no Content-Type header exists @@ -509,54 +500,6 @@ def test_unpack_file_url_excludes_expected_dirs(tmpdir, exclude_dir): assert os.path.isdir(dst_included_dir) -class TestSafeFileCache: - """ - The no_perms test are useless on Windows since SafeFileCache uses - pip._internal.utils.filesystem.check_path_owner which is based on - os.geteuid which is absent on Windows. - """ - - def test_cache_roundtrip(self, cache_tmpdir): - - cache = SafeFileCache(cache_tmpdir) - assert cache.get("test key") is None - cache.set("test key", b"a test string") - assert cache.get("test key") == b"a test string" - cache.delete("test key") - assert cache.get("test key") is None - - @pytest.mark.skipif("sys.platform == 'win32'") - def test_safe_get_no_perms(self, cache_tmpdir, monkeypatch): - os.chmod(cache_tmpdir, 000) - - monkeypatch.setattr(os.path, "exists", lambda x: True) - - cache = SafeFileCache(cache_tmpdir) - cache.get("foo") - - @pytest.mark.skipif("sys.platform == 'win32'") - def test_safe_set_no_perms(self, cache_tmpdir): - os.chmod(cache_tmpdir, 000) - - cache = SafeFileCache(cache_tmpdir) - cache.set("foo", b"bar") - - @pytest.mark.skipif("sys.platform == 'win32'") - def test_safe_delete_no_perms(self, cache_tmpdir): - os.chmod(cache_tmpdir, 000) - - cache = SafeFileCache(cache_tmpdir) - cache.delete("foo") - - def test_cache_hashes_are_same(self, cache_tmpdir): - cache = SafeFileCache(cache_tmpdir) - key = "test key" - fake_cache = Mock( - FileCache, directory=cache.directory, encode=FileCache.encode - ) - assert cache._get_cache_path(key) == FileCache._fn(fake_cache, key) - - class TestPipSession: def test_cache_defaults_off(self): diff --git a/tests/unit/test_networking_cache.py b/tests/unit/test_networking_cache.py new file mode 100644 index 00000000000..5f1d0a0975a --- /dev/null +++ b/tests/unit/test_networking_cache.py @@ -0,0 +1,62 @@ +import os + +import pytest +from mock import Mock +from pip._vendor.cachecontrol.caches import FileCache + +from pip._internal.network.cache import SafeFileCache + + +@pytest.fixture(scope="function") +def cache_tmpdir(tmpdir): + cache_dir = tmpdir.joinpath("cache") + cache_dir.mkdir(parents=True) + yield cache_dir + + +class TestSafeFileCache: + """ + The no_perms test are useless on Windows since SafeFileCache uses + pip._internal.utils.filesystem.check_path_owner which is based on + os.geteuid which is absent on Windows. + """ + + def test_cache_roundtrip(self, cache_tmpdir): + + cache = SafeFileCache(cache_tmpdir) + assert cache.get("test key") is None + cache.set("test key", b"a test string") + assert cache.get("test key") == b"a test string" + cache.delete("test key") + assert cache.get("test key") is None + + @pytest.mark.skipif("sys.platform == 'win32'") + def test_safe_get_no_perms(self, cache_tmpdir, monkeypatch): + os.chmod(cache_tmpdir, 000) + + monkeypatch.setattr(os.path, "exists", lambda x: True) + + cache = SafeFileCache(cache_tmpdir) + cache.get("foo") + + @pytest.mark.skipif("sys.platform == 'win32'") + def test_safe_set_no_perms(self, cache_tmpdir): + os.chmod(cache_tmpdir, 000) + + cache = SafeFileCache(cache_tmpdir) + cache.set("foo", b"bar") + + @pytest.mark.skipif("sys.platform == 'win32'") + def test_safe_delete_no_perms(self, cache_tmpdir): + os.chmod(cache_tmpdir, 000) + + cache = SafeFileCache(cache_tmpdir) + cache.delete("foo") + + def test_cache_hashes_are_same(self, cache_tmpdir): + cache = SafeFileCache(cache_tmpdir) + key = "test key" + fake_cache = Mock( + FileCache, directory=cache.directory, encode=FileCache.encode + ) + assert cache._get_cache_path(key) == FileCache._fn(fake_cache, key) From 7c0031626804522a179045770793fb123cba1758 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek <chris.jerdonek@gmail.com> Date: Sat, 21 Sep 2019 10:39:25 -0700 Subject: [PATCH 0335/3170] Use a single log message. --- src/pip/_internal/collector.py | 13 ++++++++----- tests/unit/test_collector.py | 9 +++++---- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/collector.py b/src/pip/_internal/collector.py index a1688d932b4..557542384df 100644 --- a/src/pip/_internal/collector.py +++ b/src/pip/_internal/collector.py @@ -521,11 +521,14 @@ def collect_links(self, project_name): ] url_locations = _remove_duplicate_links(url_locations) - logger.debug('%d location(s) to search for versions of %s:', - len(url_locations), project_name) - - for location in url_locations: - logger.debug('* %s', location) + lines = [ + '{} location(s) to search for versions of {}:'.format( + len(url_locations), project_name, + ), + ] + for link in url_locations: + lines.append('* {}'.format(link)) + logger.debug('\n'.join(lines)) pages_links = {} for page in self._get_pages(url_locations): diff --git a/tests/unit/test_collector.py b/tests/unit/test_collector.py index 18871c85d29..c0f72313b3e 100644 --- a/tests/unit/test_collector.py +++ b/tests/unit/test_collector.py @@ -447,8 +447,9 @@ def test_collect_links(self, mock_get_html_response, caplog, data): 'https://pypi.org/abc-1.0.tar.gz#md5=000000000' ) - actual = [record_tuple[1:] for record_tuple in caplog.record_tuples] - assert actual == [ - (logging.DEBUG, '1 location(s) to search for versions of twine:'), - (logging.DEBUG, '* https://pypi.org/simple/twine/'), + expected_message = dedent("""\ + 1 location(s) to search for versions of twine: + * https://pypi.org/simple/twine/""") + assert caplog.record_tuples == [ + ('pip._internal.collector', logging.DEBUG, expected_message), ] From 85dcaa74bb9daa792a6ec77d94116d49d9bca948 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 19 Sep 2019 22:32:23 -0400 Subject: [PATCH 0336/3170] Unconditionally create TempDirectory.path --- src/pip/_internal/build_env.py | 1 - src/pip/_internal/cache.py | 1 - src/pip/_internal/commands/install.py | 1 - src/pip/_internal/req/req_install.py | 2 -- src/pip/_internal/req/req_tracker.py | 1 - src/pip/_internal/req/req_uninstall.py | 3 --- src/pip/_internal/utils/temp_dir.py | 37 +++++++++----------------- tests/unit/test_utils.py | 8 +++--- 8 files changed, 16 insertions(+), 38 deletions(-) diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py index 5decc91050e..f1d9b378b24 100644 --- a/src/pip/_internal/build_env.py +++ b/src/pip/_internal/build_env.py @@ -54,7 +54,6 @@ class BuildEnvironment(object): def __init__(self): # type: () -> None self._temp_dir = TempDirectory(kind="build-env") - self._temp_dir.create() self._prefixes = OrderedDict(( (name, _Prefix(os.path.join(self._temp_dir.path, name))) diff --git a/src/pip/_internal/cache.py b/src/pip/_internal/cache.py index 433b27bb9c2..222208f2caa 100644 --- a/src/pip/_internal/cache.py +++ b/src/pip/_internal/cache.py @@ -193,7 +193,6 @@ class EphemWheelCache(SimpleWheelCache): def __init__(self, format_control): # type: (FormatControl) -> None self._temp_dir = TempDirectory(kind="ephem-wheel-cache") - self._temp_dir.create() super(EphemWheelCache, self).__init__( self._temp_dir.path, format_control diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index a14b1b38f45..dc8bc3b8bdb 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -318,7 +318,6 @@ def run(self, options, args): # Create a target directory for using with the target option target_temp_dir = TempDirectory(kind="target") - target_temp_dir.create() target_temp_dir_path = target_temp_dir.path install_options.append('--home=' + target_temp_dir_path) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index c8b7466486b..78846aaee36 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -338,7 +338,6 @@ def ensure_build_location(self, build_dir): # builds (such as numpy). Thus, we ensure that the real path # is returned. self._temp_build_dir = TempDirectory(kind="req-build") - self._temp_build_dir.create() self._ideal_build_dir = build_dir return self._temp_build_dir.path @@ -606,7 +605,6 @@ def prepare_pep517_metadata(self): # NOTE: This needs to be refactored to stop using atexit self._temp_dir = TempDirectory(delete=False, kind="req-install") - self._temp_dir.create() metadata_dir = os.path.join( self._temp_dir.path, 'pip-wheel-metadata', diff --git a/src/pip/_internal/req/req_tracker.py b/src/pip/_internal/req/req_tracker.py index bef30f236af..aa57c799a69 100644 --- a/src/pip/_internal/req/req_tracker.py +++ b/src/pip/_internal/req/req_tracker.py @@ -28,7 +28,6 @@ def __init__(self): self._root = os.environ.get('PIP_REQ_TRACKER') if self._root is None: self._temp_dir = TempDirectory(delete=False, kind='req-tracker') - self._temp_dir.create() self._root = os.environ['PIP_REQ_TRACKER'] = self._temp_dir.path logger.debug('Created requirements tracker %r', self._root) else: diff --git a/src/pip/_internal/req/req_uninstall.py b/src/pip/_internal/req/req_uninstall.py index 6b551c91bec..3acde914ae0 100644 --- a/src/pip/_internal/req/req_uninstall.py +++ b/src/pip/_internal/req/req_uninstall.py @@ -227,10 +227,8 @@ def _get_directory_stash(self, path): try: save_dir = AdjacentTempDirectory(path) # type: TempDirectory - save_dir.create() except OSError: save_dir = TempDirectory(kind="uninstall") - save_dir.create() self._save_dirs[os.path.normcase(path)] = save_dir return save_dir.path @@ -256,7 +254,6 @@ def _get_file_stash(self, path): # Did not find any suitable root head = os.path.dirname(path) save_dir = TempDirectory(kind='uninstall') - save_dir.create() self._save_dirs[head] = save_dir relpath = os.path.relpath(path, head) diff --git a/src/pip/_internal/utils/temp_dir.py b/src/pip/_internal/utils/temp_dir.py index 56159d565e1..aee78b0242a 100644 --- a/src/pip/_internal/utils/temp_dir.py +++ b/src/pip/_internal/utils/temp_dir.py @@ -19,21 +19,17 @@ class TempDirectory(object): Attributes: path - Location to the created temporary directory or None + Location to the created temporary directory 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 + Deletes the temporary directory - 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. + When used as a context manager, if the delete attribute is True, on + exiting the context the temporary directory is deleted. """ def __init__(self, path=None, delete=None, kind="temp"): @@ -44,6 +40,9 @@ def __init__(self, path=None, delete=None, kind="temp"): # an explicit delete option, then we'll default to deleting. delete = True + if path is None: + path = self._create(kind) + self.path = path self.delete = delete self.kind = kind @@ -52,30 +51,21 @@ 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): - self.path = self._create() - - def _create(self): + def _create(self, kind): """Create a temporary directory and store its path in self.path """ - if self.path is not None: - logger.debug( - "Skipped creation of temporary directory: {}".format(self.path) - ) - return self.path # 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. path = os.path.realpath( - tempfile.mkdtemp(prefix="pip-{}-".format(self.kind)) + tempfile.mkdtemp(prefix="pip-{}-".format(kind)) ) logger.debug("Created temporary directory: {}".format(path)) return path @@ -83,9 +73,8 @@ def _create(self): def cleanup(self): """Remove the temporary directory created and reset state """ - if self.path is not None and os.path.exists(self.path): + if os.path.exists(self.path): rmtree(self.path) - self.path = None class AdjacentTempDirectory(TempDirectory): @@ -110,8 +99,8 @@ class AdjacentTempDirectory(TempDirectory): LEADING_CHARS = "-~.=%0123456789" def __init__(self, original, delete=None): - super(AdjacentTempDirectory, self).__init__(delete=delete) self.original = original.rstrip('/\\') + super(AdjacentTempDirectory, self).__init__(delete=delete) @classmethod def _generate_names(cls, name): @@ -137,7 +126,7 @@ def _generate_names(cls, name): if new_name != name: yield new_name - def _create(self): + def _create(self, kind): root, name = os.path.split(self.original) for candidate in self._generate_names(name): path = os.path.join(root, candidate) @@ -153,7 +142,7 @@ def _create(self): else: # Final fallback on the default behavior. path = os.path.realpath( - tempfile.mkdtemp(prefix="pip-{}-".format(self.kind)) + tempfile.mkdtemp(prefix="pip-{}-".format(kind)) ) logger.debug("Created temporary directory: {}".format(path)) diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index c34dea9165f..2ad09666075 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -588,19 +588,17 @@ def readonly_file(*args): create_file(tmp_dir.path, "subfolder", "readonly-file") readonly_file(tmp_dir.path, "subfolder", "readonly-file") - assert tmp_dir.path is None + assert not os.path.exists(tmp_dir.path) def test_create_and_cleanup_work(self): tmp_dir = TempDirectory() - assert tmp_dir.path is None - - tmp_dir.create() created_path = tmp_dir.path + assert tmp_dir.path is not None assert os.path.exists(created_path) tmp_dir.cleanup() - assert tmp_dir.path is None + assert tmp_dir.path is not None assert not os.path.exists(created_path) @pytest.mark.parametrize("name", [ From ebd286ebec895eb1818e2372962420c6fd994f68 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 19 Sep 2019 22:36:03 -0400 Subject: [PATCH 0337/3170] Add types to TempDirectory --- src/pip/_internal/utils/temp_dir.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/utils/temp_dir.py b/src/pip/_internal/utils/temp_dir.py index aee78b0242a..1ceadee8ae8 100644 --- a/src/pip/_internal/utils/temp_dir.py +++ b/src/pip/_internal/utils/temp_dir.py @@ -7,6 +7,11 @@ import tempfile from pip._internal.utils.misc import rmtree +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import Optional + logger = logging.getLogger(__name__) @@ -32,7 +37,12 @@ class TempDirectory(object): exiting the context the temporary directory is deleted. """ - def __init__(self, path=None, delete=None, kind="temp"): + def __init__( + self, + path=None, # type: Optional[str] + delete=None, # type: Optional[bool] + kind="temp" + ): super(TempDirectory, self).__init__() if path is None and delete is None: From 892b58f2442270f1748f31b9dc9faeba5b14a79c Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 20 Sep 2019 02:09:21 -0400 Subject: [PATCH 0338/3170] Move helper functions into Link --- src/pip/_internal/download.py | 23 +++-------------------- src/pip/_internal/models/link.py | 10 ++++++++++ src/pip/_internal/operations/prepare.py | 4 ++-- 3 files changed, 15 insertions(+), 22 deletions(-) diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index 53646149cc3..c31d8fc6cfb 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -100,7 +100,7 @@ __all__ = ['get_file_content', 'path_to_url', 'unpack_vcs_link', - 'unpack_file_url', 'is_file_url', + 'unpack_file_url', 'unpack_http_url', 'unpack_url', 'parse_content_disposition', 'sanitize_content_filename'] @@ -602,23 +602,6 @@ def _get_used_vcs_backend(link): return None -def is_file_url(link): - # type: (Link) -> bool - return link.url.lower().startswith('file:') - - -def is_dir_url(link): - # type: (Link) -> bool - """Return whether a file:// Link points to a directory. - - ``link`` must not have any other scheme but file://. Call is_file_url() - first. - - """ - link_path = link.file_path - return os.path.isdir(link_path) - - def _progress_indicator(iterable, *args, **kwargs): return iterable @@ -849,7 +832,7 @@ def unpack_file_url( """ link_path = link.file_path # If it's a url to a local directory - if is_dir_url(link): + if link.is_existing_dir(): if os.path.isdir(location): rmtree(location) _copy_source_tree(link_path, location) @@ -945,7 +928,7 @@ def unpack_url( unpack_vcs_link(link, location) # file urls - elif is_file_url(link): + elif link.is_file: unpack_file_url(link, location, download_dir, hashes=hashes) # http urls diff --git a/src/pip/_internal/models/link.py b/src/pip/_internal/models/link.py index c109804b8f3..a6ef39458ce 100644 --- a/src/pip/_internal/models/link.py +++ b/src/pip/_internal/models/link.py @@ -1,3 +1,4 @@ +import os import posixpath import re @@ -180,6 +181,15 @@ def show_url(self): # type: () -> Optional[str] return posixpath.basename(self._url.split('#', 1)[0].split('?', 1)[0]) + @property + def is_file(self): + # type: () -> bool + return self.scheme == 'file' + + def is_existing_dir(self): + # type: () -> bool + return self.is_file and os.path.isdir(self.file_path) + @property def is_wheel(self): # type: () -> bool diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 64d41fa4e04..43956bef9f6 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -13,7 +13,7 @@ make_distribution_for_install_requirement, ) from pip._internal.distributions.installed import InstalledDistribution -from pip._internal.download import is_dir_url, is_file_url, unpack_url +from pip._internal.download import unpack_url from pip._internal.exceptions import ( DirectoryUrlHashUnsupported, HashUnpinned, @@ -160,7 +160,7 @@ def prepare_linked_requirement( # hash provided. if link.is_vcs: raise VcsHashUnsupported() - elif is_file_url(link) and is_dir_url(link): + elif link.is_existing_dir(): raise DirectoryUrlHashUnsupported() if not req.original_link and not req.is_pinned: # Unpinned packages are asking for trouble when a new From e4714fbe8a8c4cd80ef6a70d77a7cfb91c8dc536 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 18 Sep 2019 22:32:17 -0400 Subject: [PATCH 0339/3170] Early return if download path does not exist. --- src/pip/_internal/download.py | 34 ++++++++++++++++++---------------- 1 file changed, 18 insertions(+), 16 deletions(-) diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index c31d8fc6cfb..9701f3546de 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -1038,19 +1038,21 @@ def _check_download_dir(link, download_dir, hashes): If a correct file is found return its path else None """ download_path = os.path.join(download_dir, link.filename) - if os.path.exists(download_path): - # If already downloaded, does its hash match? - logger.info('File was already downloaded %s', download_path) - if hashes: - try: - hashes.check_against_path(download_path) - except HashMismatch: - logger.warning( - 'Previously-downloaded file %s has bad hash. ' - 'Re-downloading.', - download_path - ) - os.unlink(download_path) - return None - return download_path - return None + + if not os.path.exists(download_path): + return None + + # If already downloaded, does its hash match? + logger.info('File was already downloaded %s', download_path) + if hashes: + try: + hashes.check_against_path(download_path) + except HashMismatch: + logger.warning( + 'Previously-downloaded file %s has bad hash. ' + 'Re-downloading.', + download_path + ) + os.unlink(download_path) + return None + return download_path From ab0322b8a82232acf9734c566836c78a120c2539 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 22 Sep 2019 11:54:21 +0530 Subject: [PATCH 0340/3170] Fix a typo --- src/pip/_internal/operations/generate_metadata.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/operations/generate_metadata.py b/src/pip/_internal/operations/generate_metadata.py index 368ecdeb492..5d64e018f69 100644 --- a/src/pip/_internal/operations/generate_metadata.py +++ b/src/pip/_internal/operations/generate_metadata.py @@ -36,7 +36,7 @@ def _generate_metadata_legacy(install_req): if install_req.isolated: base_cmd += ["--no-user-cfg"] - # For non-editable installed, don't put the .egg-info files at the root, + # For non-editable installs, don't put the .egg-info files at the root, # to avoid confusion due to the source code being considered an installed # egg. egg_base_option = [] # type: List[str] From 596b77c149142c46eebfbf8e5a37daa758030cde Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 22 Sep 2019 12:49:20 +0530 Subject: [PATCH 0341/3170] Better name for _correct_build_directory Why: the older name doesn't clearly signal what is happening. --- src/pip/_internal/req/req_install.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index c8b7466486b..80b12c15b72 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -131,7 +131,7 @@ def __init__( # Temporary build location self._temp_build_dir = None # type: Optional[TempDirectory] # Used to store the global directory where the _temp_build_dir should - # have been created. Cf _correct_build_location method. + # have been created. Cf move_to_correct_build_directory method. self._ideal_build_dir = None # type: Optional[str] # Set to True after successful installation self.install_succeeded = None # type: Optional[bool] @@ -332,8 +332,8 @@ def ensure_build_location(self, build_dir): if self.req is None: # for requirement via a path to a directory: the name of the # package is not available yet so we create a temp directory - # Once run_egg_info will have run, we'll be able - # to fix it via _correct_build_location + # Once run_egg_info will have run, we'll be able to fix it via + # move_to_correct_build_directory(). # Some systems have /tmp as a symlink which confuses custom # builds (such as numpy). Thus, we ensure that the real path # is returned. @@ -353,16 +353,16 @@ def ensure_build_location(self, build_dir): _make_build_dir(build_dir) return os.path.join(build_dir, name) - def _correct_build_location(self): + def move_to_correct_build_directory(self): # type: () -> None - """Move self._temp_build_dir to self._ideal_build_dir/self.req.name + """Move self._temp_build_dir to "self._ideal_build_dir/self.req.name" For some requirements (e.g. a path to a directory), the name of the package is not available until we run egg_info, so the build_location will return a temporary directory and store the _ideal_build_dir. - This is only called by self.run_egg_info to fix the temporary build - directory. + This is only called to "fix" the build directory after generating + metadata. """ if self.source_dir is not None: return @@ -583,7 +583,7 @@ def prepare_metadata(self): self.metadata["Version"], ]) ) - self._correct_build_location() + self.move_to_correct_build_directory() else: metadata_name = canonicalize_name(self.metadata["Name"]) if canonicalize_name(self.req.name) != metadata_name: From 903580cace64e6a9eaac5b1fd64ccca1d720da5e Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 22 Sep 2019 09:51:25 -0400 Subject: [PATCH 0342/3170] Use RequirementParts to populate editable InstallRequirement --- src/pip/_internal/req/constructors.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index f0203683d93..13d8502eb60 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -205,16 +205,23 @@ def install_req_from_editable( raise InstallationError("Invalid requirement: '%s'" % name) else: req = None + + link = Link(url) + + parts = RequirementParts(req, link, None, extras_override) + + source_dir = parts.link.file_path if parts.link.scheme == 'file' else None + return InstallRequirement( - req, comes_from, source_dir=source_dir, + parts.requirement, comes_from, source_dir=source_dir, editable=True, - link=Link(url), + link=parts.link, constraint=constraint, use_pep517=use_pep517, isolated=isolated, options=options if options else {}, wheel_cache=wheel_cache, - extras=extras_override or (), + extras=parts.extras, ) From 3f98ee42509679a5bfd53899c9b6127d6f52e44d Mon Sep 17 00:00:00 2001 From: Adam Tse <adam.tse@me.com> Date: Sun, 28 Oct 2018 11:44:03 +0000 Subject: [PATCH 0343/3170] Explicit support and tests for `hg+file` scheme for `pip install`. --- docs/html/reference/pip_install.rst | 2 +- news/4358.bugfix | 2 ++ src/pip/_internal/vcs/mercurial.py | 4 +++- tests/functional/test_install.py | 4 +++- 4 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 news/4358.bugfix diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index fa475c462c8..9fcf94676bf 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -418,7 +418,7 @@ Mercurial ~~~~~~~~~ The supported schemes are: ``hg+http``, ``hg+https``, -``hg+static-http`` and ``hg+ssh``. +``hg+static-http``, ``hg+ssh`` and ``hg+file``. Here are the supported forms:: diff --git a/news/4358.bugfix b/news/4358.bugfix new file mode 100644 index 00000000000..891d819debd --- /dev/null +++ b/news/4358.bugfix @@ -0,0 +1,2 @@ +Not a feature because the ``hg+file`` scheme was already functioning implicitly but inconsistently documented. +This change is to make the scheme support explicit, including tests and docs. \ No newline at end of file diff --git a/src/pip/_internal/vcs/mercurial.py b/src/pip/_internal/vcs/mercurial.py index 21697ff1584..e9906632dcd 100644 --- a/src/pip/_internal/vcs/mercurial.py +++ b/src/pip/_internal/vcs/mercurial.py @@ -22,7 +22,9 @@ class Mercurial(VersionControl): name = 'hg' dirname = '.hg' repo_name = 'clone' - schemes = ('hg', 'hg+http', 'hg+https', 'hg+ssh', 'hg+static-http') + schemes = ( + 'hg', 'hg+http', 'hg+https', 'hg+ssh', 'hg+static-http', 'hg+file' + ) @staticmethod def get_base_rev_args(rev): diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 7a0e4a9cbee..d11368b1171 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -334,11 +334,13 @@ def test_install_editable_uninstalls_existing_from_path(script, data): @need_mercurial def test_basic_install_editable_from_hg(script, tmpdir): - """Test cloning from Mercurial.""" + """Test cloning and hg+file install from Mercurial.""" pkg_path = _create_test_package(script, name='testpackage', vcs='hg') args = ['install', '-e', 'hg+%s#egg=testpackage' % path_to_url(pkg_path)] result = script.pip(*args) result.assert_installed('testpackage', with_files=['.hg']) + assert path_to_url(pkg_path).startswith("file://") + @need_mercurial From 30df34de98122c1c532e2039a6bf2da4143bcad6 Mon Sep 17 00:00:00 2001 From: atse <atse@users.noreply.github.com> Date: Sun, 28 Oct 2018 14:56:15 +0000 Subject: [PATCH 0344/3170] Update 4358.bugfix --- news/4358.bugfix | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/news/4358.bugfix b/news/4358.bugfix index 891d819debd..912083dc1d7 100644 --- a/news/4358.bugfix +++ b/news/4358.bugfix @@ -1,2 +1 @@ -Not a feature because the ``hg+file`` scheme was already functioning implicitly but inconsistently documented. -This change is to make the scheme support explicit, including tests and docs. \ No newline at end of file +Correct inconsistency related to the `hg+file` scheme. From 4022ac277fae64b4c514c3e36f64a6372bf56525 Mon Sep 17 00:00:00 2001 From: Adam Tse <adam.tse@me.com> Date: Sun, 28 Oct 2018 15:16:14 +0000 Subject: [PATCH 0345/3170] :fixed whitespace --- tests/functional/test_install.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index d11368b1171..59511bcddda 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -342,7 +342,6 @@ def test_basic_install_editable_from_hg(script, tmpdir): assert path_to_url(pkg_path).startswith("file://") - @need_mercurial def test_vcs_url_final_slash_normalization(script, tmpdir): """ From 658425a555f7fbdc53d02fd8aeb0a467bd377146 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 22 Sep 2019 11:06:07 -0400 Subject: [PATCH 0346/3170] Add explicit test for hg+file This was previously failing on the master branch. --- tests/unit/test_link.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/test_link.py b/tests/unit/test_link.py index 8fbafe082e8..a9e75e38bd3 100644 --- a/tests/unit/test_link.py +++ b/tests/unit/test_link.py @@ -131,6 +131,7 @@ def test_is_hash_allowed__none_hashes(self, hashes, expected): @pytest.mark.parametrize('url, expected', [ ('git+https://github.com/org/repo', True), ('bzr+http://bzr.myproject.org/MyProject/trunk/#egg=MyProject', True), + ('hg+file://hg.company.com/repo', True), ('https://example.com/some.whl', False), ('file://home/foo/some.whl', False), ]) From 05964ead48159cb92841e18dbb355b913267ca0f Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 22 Sep 2019 11:10:25 -0400 Subject: [PATCH 0347/3170] Address previous review comments --- docs/html/reference/pip_install.rst | 4 ++-- src/pip/_internal/vcs/mercurial.py | 2 +- tests/functional/test_install.py | 5 +++-- 3 files changed, 6 insertions(+), 5 deletions(-) diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index 9fcf94676bf..ca3ffb51e1a 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -417,8 +417,8 @@ making fewer network calls). Mercurial ~~~~~~~~~ -The supported schemes are: ``hg+http``, ``hg+https``, -``hg+static-http``, ``hg+ssh`` and ``hg+file``. +The supported schemes are: ``hg+file``, ``hg+http``, ``hg+https``, +``hg+static-http``, and ``hg+ssh``. Here are the supported forms:: diff --git a/src/pip/_internal/vcs/mercurial.py b/src/pip/_internal/vcs/mercurial.py index e9906632dcd..a4d007c764b 100644 --- a/src/pip/_internal/vcs/mercurial.py +++ b/src/pip/_internal/vcs/mercurial.py @@ -23,7 +23,7 @@ class Mercurial(VersionControl): dirname = '.hg' repo_name = 'clone' schemes = ( - 'hg', 'hg+http', 'hg+https', 'hg+ssh', 'hg+static-http', 'hg+file' + 'hg', 'hg+file', 'hg+http', 'hg+https', 'hg+ssh', 'hg+static-http', ) @staticmethod diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 59511bcddda..7f5f8cfdcbd 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -336,10 +336,11 @@ def test_install_editable_uninstalls_existing_from_path(script, data): def test_basic_install_editable_from_hg(script, tmpdir): """Test cloning and hg+file install from Mercurial.""" pkg_path = _create_test_package(script, name='testpackage', vcs='hg') - args = ['install', '-e', 'hg+%s#egg=testpackage' % path_to_url(pkg_path)] + url = 'hg+{}#egg=testpackage'.format(path_to_url(pkg_path)) + assert url.startswith('hg+file') + args = ['install', '-e', url] result = script.pip(*args) result.assert_installed('testpackage', with_files=['.hg']) - assert path_to_url(pkg_path).startswith("file://") @need_mercurial From 8fe2eb068a593db7995015234ac7c9a40983c530 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 22 Sep 2019 12:02:14 -0400 Subject: [PATCH 0348/3170] Factor out editable parsing into a function --- src/pip/_internal/req/constructors.py | 35 ++++++++++++++------------- 1 file changed, 18 insertions(+), 17 deletions(-) diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index 13d8502eb60..4c59ef92f14 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -28,7 +28,6 @@ from pip._internal.utils.filetypes import ARCHIVE_EXTENSIONS from pip._internal.utils.misc import is_installable_dir, path_to_url, splitext from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from pip._internal.utils.urls import url_to_path from pip._internal.vcs import is_url, vcs from pip._internal.wheel import Wheel @@ -179,6 +178,23 @@ def deduce_helpful_msg(req): return msg +def parse_req_from_editable(editable_req): + # type: (str) -> RequirementParts + name, url, extras_override = parse_editable(editable_req) + + if name is not None: + try: + req = Requirement(name) + except InvalidRequirement: + raise InstallationError("Invalid requirement: '%s'" % name) + else: + req = None + + link = Link(url) + + return RequirementParts(req, link, None, extras_override) + + # ---- The actual constructors follow ---- @@ -192,23 +208,8 @@ def install_req_from_editable( constraint=False # type: bool ): # type: (...) -> InstallRequirement - name, url, extras_override = parse_editable(editable_req) - if url.startswith('file:'): - source_dir = url_to_path(url) - else: - source_dir = None - - if name is not None: - try: - req = Requirement(name) - except InvalidRequirement: - raise InstallationError("Invalid requirement: '%s'" % name) - else: - req = None - - link = Link(url) - parts = RequirementParts(req, link, None, extras_override) + parts = parse_req_from_editable(editable_req) source_dir = parts.link.file_path if parts.link.scheme == 'file' else None From ad82dd09812bd61c7c8412e885f4c745ce4b7863 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 22 Sep 2019 12:03:47 -0400 Subject: [PATCH 0349/3170] Move RequirementParts above new function --- src/pip/_internal/req/constructors.py | 28 +++++++++++++-------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index 4c59ef92f14..bd16d4363b5 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -178,6 +178,20 @@ def deduce_helpful_msg(req): return msg +class RequirementParts(object): + def __init__( + self, + requirement, # type: Optional[Requirement] + link, # type: Optional[Link] + markers, # type: Optional[Marker] + extras, # type: Set[str] + ): + self.requirement = requirement + self.link = link + self.markers = markers + self.extras = extras + + def parse_req_from_editable(editable_req): # type: (str) -> RequirementParts name, url, extras_override = parse_editable(editable_req) @@ -280,20 +294,6 @@ def _get_url_from_path(path, name): return path_to_url(path) -class RequirementParts(object): - def __init__( - self, - requirement, # type: Optional[Requirement] - link, # type: Optional[Link] - markers, # type: Optional[Marker] - extras, # type: Set[str] - ): - self.requirement = requirement - self.link = link - self.markers = markers - self.extras = extras - - def parse_req_from_line(name, line_source): # type: (str, Optional[str]) -> RequirementParts if is_url(name): From e9ea3966e0bea469766941ba0e1ef98fac55dc7a Mon Sep 17 00:00:00 2001 From: Sebastian Jordan <sebastian.jordan.mail@googlemail.com> Date: Sun, 22 Sep 2019 21:24:43 +0200 Subject: [PATCH 0350/3170] Build pep 517 requirements when installing packages --- src/pip/_internal/commands/install.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index a14b1b38f45..09a6a5c6cc2 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -1,4 +1,3 @@ - # The following comment should be removed at some point in the future. # It's included for now because without it InstallCommand.run() has a # couple errors where we have to know req.name is str rather than @@ -103,7 +102,7 @@ def check_binary_allowed(req): # type: (InstallRequirement) -> bool canonical_name = canonicalize_name(req.name) allowed_formats = format_control.get_allowed_formats(canonical_name) - return "binary" in allowed_formats + return "binary" in allowed_formats or req.use_pep517 return check_binary_allowed @@ -392,9 +391,8 @@ def run(self, options, args): modifying_pip=modifying_pip ) - check_binary_allowed = get_check_binary_allowed( - finder.format_control - ) + check_binary_allowed = get_check_binary_allowed(finder.format_control) + # Consider legacy and PEP517-using requirements separately legacy_requirements = [] pep517_requirements = [] From 4879c8b2ebff6af72865ec665a16207559b5618b Mon Sep 17 00:00:00 2001 From: Sebastian Jordan <sebastian.jordan.mail@googlemail.com> Date: Sun, 22 Sep 2019 21:31:45 +0200 Subject: [PATCH 0351/3170] Linting --- src/pip/_internal/commands/install.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 09a6a5c6cc2..ce2ec016114 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -391,7 +391,9 @@ def run(self, options, args): modifying_pip=modifying_pip ) - check_binary_allowed = get_check_binary_allowed(finder.format_control) + check_binary_allowed = get_check_binary_allowed( + finder.format_control + ) # Consider legacy and PEP517-using requirements separately legacy_requirements = [] From ea91012bee36a83cb7e8a4795b4751691ad34fb7 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 22 Sep 2019 17:37:48 -0400 Subject: [PATCH 0352/3170] Update auto-svn+ removal wording --- news/7037.removal | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/news/7037.removal b/news/7037.removal index 577a02f5e46..4c606e4a2a0 100644 --- a/news/7037.removal +++ b/news/7037.removal @@ -1,2 +1,3 @@ -Remove undocumented support for http:// requirements pointing to SVN -repositories. +Remove undocumented support for un-prefixed URL requirements pointing +to SVN repositories. Users relying on this can get the original behavior +by prefixing their URL with ``svn+`` (which is backwards-compatible). From d30bc69073e1ccc2129ba7775a8146b4ec463120 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek <chris.jerdonek@gmail.com> Date: Mon, 23 Sep 2019 05:08:27 -0700 Subject: [PATCH 0353/3170] Move make_fake_html_page() elsewhere in the module. --- tests/unit/test_collector.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/tests/unit/test_collector.py b/tests/unit/test_collector.py index 1e90948ebce..ef7e8cf40d4 100644 --- a/tests/unit/test_collector.py +++ b/tests/unit/test_collector.py @@ -324,6 +324,18 @@ def test_get_html_page_invalid_scheme(caplog, url, vcs_scheme): ] +def make_fake_html_page(url): + html = dedent(u"""\ + <html><head><meta name="api-version" value="2" /></head> + <body> + <a href="/abc-1.0.tar.gz#md5=000000000">abc-1.0.tar.gz</a> + </body></html> + """) + content = html.encode('utf-8') + headers = {} + return HTMLPage(content, url=url, headers=headers) + + def test_get_html_page_directory_append_index(tmpdir): """`_get_html_page()` should append "index.html" to a directory URL. """ @@ -371,18 +383,6 @@ def test_group_locations__non_existing_path(): assert not urls and not files, "nothing should have been found" -def make_fake_html_page(url): - html = dedent(u"""\ - <html><head><meta name="api-version" value="2" /></head> - <body> - <a href="/abc-1.0.tar.gz#md5=000000000">abc-1.0.tar.gz</a> - </body></html> - """) - content = html.encode('utf-8') - headers = {} - return HTMLPage(content, url=url, headers=headers) - - def check_links_include(links, names): """ Assert that the given list of Link objects includes, for each of the From 4ad5a58376cd9bfc938362a1dc7aa3669b77f7ea Mon Sep 17 00:00:00 2001 From: Chris Jerdonek <chris.jerdonek@gmail.com> Date: Sat, 21 Sep 2019 01:19:00 -0700 Subject: [PATCH 0354/3170] Change HTMLPage.__init__ to accept an encoding. --- src/pip/_internal/collector.py | 21 +++++++++++---- tests/unit/test_collector.py | 48 ++++++++++++++++++++++++++-------- 2 files changed, 53 insertions(+), 16 deletions(-) diff --git a/src/pip/_internal/collector.py b/src/pip/_internal/collector.py index 443e22a7f5b..eda992fdf77 100644 --- a/src/pip/_internal/collector.py +++ b/src/pip/_internal/collector.py @@ -274,11 +274,16 @@ def parse_links( class HTMLPage(object): """Represents one page, along with its URL""" - def __init__(self, content, url, headers=None): - # type: (bytes, str, ResponseHeaders) -> None + def __init__( + self, + content, # type: bytes + encoding, # type: Optional[str] + url, # type: str + ): + # type: (...) -> None self.content = content + self.encoding = encoding self.url = url - self.headers = headers def __str__(self): return redact_auth_from_url(self.url) @@ -286,7 +291,7 @@ def __str__(self): def iter_links(self): # type: () -> Iterable[Link] """Yields all links in the page""" - encoding = _get_encoding_from_headers(self.headers) + encoding = self.encoding for link in parse_links(self.content, encoding=encoding, url=self.url): yield link @@ -302,6 +307,12 @@ def _handle_get_page_fail( meth("Could not fetch URL %s: %s - skipping", link, reason) +def _make_html_page(response): + # type: (Response) -> HTMLPage + encoding = _get_encoding_from_headers(response.headers) + return HTMLPage(response.content, encoding=encoding, url=response.url) + + def _get_html_page(link, session=None): # type: (Link, Optional[PipSession]) -> Optional[HTMLPage] if session is None: @@ -352,7 +363,7 @@ def _get_html_page(link, session=None): except requests.Timeout: _handle_get_page_fail(link, "timed out") else: - return HTMLPage(resp.content, resp.url, resp.headers) + return _make_html_page(resp) return None diff --git a/tests/unit/test_collector.py b/tests/unit/test_collector.py index ef7e8cf40d4..b628f206371 100644 --- a/tests/unit/test_collector.py +++ b/tests/unit/test_collector.py @@ -3,6 +3,7 @@ from textwrap import dedent import mock +import pretend import pytest from mock import Mock, patch from pip._vendor import html5lib, requests @@ -14,6 +15,7 @@ _determine_base_url, _get_html_page, _get_html_response, + _make_html_page, _NotHTML, _NotHTTP, group_locations, @@ -267,7 +269,11 @@ def test_iter_links__yanked_reason(self, anchor_html, expected): '<body>{}</body></html>' ).format(anchor_html) html_bytes = html.encode('utf-8') - page = HTMLPage(html_bytes, url='https://example.com/simple/') + page = HTMLPage( + html_bytes, + encoding=None, + url='https://example.com/simple/', + ) links = list(page.iter_links()) link, = links actual = link.yanked_reason @@ -299,6 +305,20 @@ def test_request_retries(caplog): ) +def test_make_html_page(): + headers = {'Content-Type': 'text/html; charset=UTF-8'} + response = pretend.stub( + content=b'<content>', + url='https://example.com/index.html', + headers=headers, + ) + + actual = _make_html_page(response) + assert actual.content == b'<content>' + assert actual.encoding == 'UTF-8' + assert actual.url == 'https://example.com/index.html' + + @pytest.mark.parametrize( "url, vcs_scheme", [ @@ -324,7 +344,10 @@ def test_get_html_page_invalid_scheme(caplog, url, vcs_scheme): ] -def make_fake_html_page(url): +def make_fake_html_response(url): + """ + Create a fake requests.Response object. + """ html = dedent(u"""\ <html><head><meta name="api-version" value="2" /></head> <body> @@ -332,8 +355,7 @@ def make_fake_html_page(url): </body></html> """) content = html.encode('utf-8') - headers = {} - return HTMLPage(content, url=url, headers=headers) + return pretend.stub(content=content, url=url, headers={}) def test_get_html_page_directory_append_index(tmpdir): @@ -343,16 +365,20 @@ def test_get_html_page_directory_append_index(tmpdir): dir_url = "file:///{}".format( urllib_request.pathname2url(dirpath).lstrip("/"), ) + expected_url = "{}/index.html".format(dir_url.rstrip("/")) session = mock.Mock(PipSession) + fake_response = make_fake_html_response(expected_url) with mock.patch("pip._internal.collector._get_html_response") as mock_func: - _get_html_page(Link(dir_url), session=session) + mock_func.return_value = fake_response + actual = _get_html_page(Link(dir_url), session=session) assert mock_func.mock_calls == [ - mock.call( - "{}/index.html".format(dir_url.rstrip("/")), - session=session, - ), - ] + mock.call(expected_url, session=session), + ], 'actual calls: {}'.format(mock_func.mock_calls) + + assert actual.content == fake_response.content + assert actual.encoding is None + assert actual.url == expected_url def test_group_locations__file_expand_dir(data): @@ -400,7 +426,7 @@ class TestLinkCollector(object): def test_collect_links(self, mock_get_html_response, data): expected_url = 'https://pypi.org/simple/twine/' - fake_page = make_fake_html_page(expected_url) + fake_page = make_fake_html_response(expected_url) mock_get_html_response.return_value = fake_page link_collector = make_test_link_collector( From 6c9e0c25364fc26d21daeb1ad38317f50fffe89f Mon Sep 17 00:00:00 2001 From: Chris Jerdonek <chris.jerdonek@gmail.com> Date: Sat, 21 Sep 2019 01:28:13 -0700 Subject: [PATCH 0355/3170] Remove HTMLPage.iter_links(). --- src/pip/_internal/collector.py | 29 ++++++-------- tests/unit/test_collector.py | 70 ++++++++++++++++------------------ 2 files changed, 44 insertions(+), 55 deletions(-) diff --git a/src/pip/_internal/collector.py b/src/pip/_internal/collector.py index eda992fdf77..703984646eb 100644 --- a/src/pip/_internal/collector.py +++ b/src/pip/_internal/collector.py @@ -243,22 +243,18 @@ def _create_link_from_element( return link -def parse_links( - html, # type: bytes - encoding, # type: Optional[str] - url, # type: str -): - # type: (...) -> Iterable[Link] +def parse_links(page): + # type: (HTMLPage) -> Iterable[Link] """ Parse an HTML document, and yield its anchor elements as Link objects. - - :param url: the URL from which the HTML was downloaded. """ document = html5lib.parse( - html, - transport_encoding=encoding, + page.content, + transport_encoding=page.encoding, namespaceHTMLElements=False, ) + + url = page.url base_url = _determine_base_url(document, url) for anchor in document.findall(".//a"): link = _create_link_from_element( @@ -281,6 +277,10 @@ def __init__( url, # type: str ): # type: (...) -> None + """ + :param encoding: the encoding to decode the given content. + :param url: the URL from which the HTML was downloaded. + """ self.content = content self.encoding = encoding self.url = url @@ -288,13 +288,6 @@ def __init__( def __str__(self): return redact_auth_from_url(self.url) - def iter_links(self): - # type: () -> Iterable[Link] - """Yields all links in the page""" - encoding = self.encoding - for link in parse_links(self.content, encoding=encoding, url=self.url): - yield link - def _handle_get_page_fail( link, # type: Link @@ -534,7 +527,7 @@ def collect_links(self, project_name): pages_links = {} for page in self._get_pages(url_locations): - pages_links[page.url] = list(page.iter_links()) + pages_links[page.url] = list(parse_links(page)) return CollectedLinks( files=file_links, diff --git a/tests/unit/test_collector.py b/tests/unit/test_collector.py index b628f206371..7abcc72ca33 100644 --- a/tests/unit/test_collector.py +++ b/tests/unit/test_collector.py @@ -19,6 +19,7 @@ _NotHTML, _NotHTTP, group_locations, + parse_links, ) from pip._internal.download import PipSession from pip._internal.models.index import PyPI @@ -240,44 +241,39 @@ def test_clean_link(url, clean_url): assert(_clean_link(url) == clean_url) -class TestHTMLPage: - - @pytest.mark.parametrize( - ('anchor_html, expected'), - [ - # Test not present. - ('<a href="/pkg1-1.0.tar.gz"></a>', None), - # Test present with no value. - ('<a href="/pkg2-1.0.tar.gz" data-yanked></a>', ''), - # Test the empty string. - ('<a href="/pkg3-1.0.tar.gz" data-yanked=""></a>', ''), - # Test a non-empty string. - ('<a href="/pkg4-1.0.tar.gz" data-yanked="error"></a>', 'error'), - # Test a value with an escaped character. - ('<a href="/pkg4-1.0.tar.gz" data-yanked="version < 1"></a>', - 'version < 1'), - # Test a yanked reason with a non-ascii character. - (u'<a href="/pkg-1.0.tar.gz" data-yanked="curlyquote \u2018"></a>', - u'curlyquote \u2018'), - ] +@pytest.mark.parametrize('anchor_html, expected', [ + # Test not present. + ('<a href="/pkg1-1.0.tar.gz"></a>', None), + # Test present with no value. + ('<a href="/pkg2-1.0.tar.gz" data-yanked></a>', ''), + # Test the empty string. + ('<a href="/pkg3-1.0.tar.gz" data-yanked=""></a>', ''), + # Test a non-empty string. + ('<a href="/pkg4-1.0.tar.gz" data-yanked="error"></a>', 'error'), + # Test a value with an escaped character. + ('<a href="/pkg4-1.0.tar.gz" data-yanked="version < 1"></a>', + 'version < 1'), + # Test a yanked reason with a non-ascii character. + (u'<a href="/pkg-1.0.tar.gz" data-yanked="curlyquote \u2018"></a>', + u'curlyquote \u2018'), +]) +def test_parse_links__yanked_reason(anchor_html, expected): + html = ( + # Mark this as a unicode string for Python 2 since anchor_html + # can contain non-ascii. + u'<html><head><meta charset="utf-8"><head>' + '<body>{}</body></html>' + ).format(anchor_html) + html_bytes = html.encode('utf-8') + page = HTMLPage( + html_bytes, + encoding=None, + url='https://example.com/simple/', ) - def test_iter_links__yanked_reason(self, anchor_html, expected): - html = ( - # Mark this as a unicode string for Python 2 since anchor_html - # can contain non-ascii. - u'<html><head><meta charset="utf-8"><head>' - '<body>{}</body></html>' - ).format(anchor_html) - html_bytes = html.encode('utf-8') - page = HTMLPage( - html_bytes, - encoding=None, - url='https://example.com/simple/', - ) - links = list(page.iter_links()) - link, = links - actual = link.yanked_reason - assert actual == expected + links = list(parse_links(page)) + link, = links + actual = link.yanked_reason + assert actual == expected def test_request_http_error(caplog): From ec892cec973e175da609ae42ce2a6c15fcedf6db Mon Sep 17 00:00:00 2001 From: Chris Jerdonek <chris.jerdonek@gmail.com> Date: Sat, 21 Sep 2019 01:33:25 -0700 Subject: [PATCH 0356/3170] Reflect that headers are non-optional in _get_encoding_from_headers(). --- src/pip/_internal/collector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/collector.py b/src/pip/_internal/collector.py index 703984646eb..befcf862a0b 100644 --- a/src/pip/_internal/collector.py +++ b/src/pip/_internal/collector.py @@ -155,7 +155,7 @@ def _get_html_response(url, session): def _get_encoding_from_headers(headers): - # type: (Optional[ResponseHeaders]) -> Optional[str] + # type: (ResponseHeaders) -> Optional[str] """Determine if we have any encoding information in our headers. """ if headers and "Content-Type" in headers: From 97ec73881eaa793420a8f054ee733868cd772afc Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Mon, 23 Sep 2019 18:41:31 -0400 Subject: [PATCH 0357/3170] Switch TempDirectory.path to property --- src/pip/_internal/utils/temp_dir.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/utils/temp_dir.py b/src/pip/_internal/utils/temp_dir.py index 1ceadee8ae8..b2cec05181a 100644 --- a/src/pip/_internal/utils/temp_dir.py +++ b/src/pip/_internal/utils/temp_dir.py @@ -53,10 +53,15 @@ def __init__( if path is None: path = self._create(kind) - self.path = path + self._path = path self.delete = delete self.kind = kind + @property + def path(self): + # type: () -> str + return self._path + def __repr__(self): return "<{} {!r}>".format(self.__class__.__name__, self.path) From 236fb82a1f8ad2d40bf533142364f40fccd2d292 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Mon, 23 Sep 2019 18:47:28 -0400 Subject: [PATCH 0358/3170] Move temp_dir tests to separate file --- tests/unit/test_utils.py | 164 ----------------------------- tests/unit/test_utils_temp_dir.py | 169 ++++++++++++++++++++++++++++++ 2 files changed, 169 insertions(+), 164 deletions(-) create mode 100644 tests/unit/test_utils_temp_dir.py diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 2ad09666075..db0ff19f193 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -5,13 +5,11 @@ """ import codecs -import itertools import locale import os import shutil import stat import sys -import tempfile import time import warnings from io import BytesIO @@ -42,7 +40,6 @@ build_url_from_netloc, call_subprocess, egg_link_path, - ensure_dir, format_command_args, get_installed_distributions, get_prog, @@ -64,7 +61,6 @@ split_auth_netloc_from_url, ) from pip._internal.utils.setuptools_build import make_setuptools_shim_args -from pip._internal.utils.temp_dir import AdjacentTempDirectory, TempDirectory from pip._internal.utils.ui import SpinnerInterface @@ -541,166 +537,6 @@ def test_all_encodings_are_valid(self, encoding): assert ''.encode(encoding).decode(encoding) == '' -class TestTempDirectory(object): - - # No need to test symlinked directories on Windows - @pytest.mark.skipif("sys.platform == 'win32'") - def test_symlinked_path(self): - with TempDirectory() as tmp_dir: - assert os.path.exists(tmp_dir.path) - - alt_tmp_dir = tempfile.mkdtemp(prefix="pip-test-") - assert ( - os.path.dirname(tmp_dir.path) == - os.path.dirname(os.path.realpath(alt_tmp_dir)) - ) - # are we on a system where /tmp is a symlink - if os.path.realpath(alt_tmp_dir) != os.path.abspath(alt_tmp_dir): - assert ( - os.path.dirname(tmp_dir.path) != - os.path.dirname(alt_tmp_dir) - ) - else: - assert ( - os.path.dirname(tmp_dir.path) == - os.path.dirname(alt_tmp_dir) - ) - os.rmdir(tmp_dir.path) - assert not os.path.exists(tmp_dir.path) - - def test_deletes_readonly_files(self): - def create_file(*args): - fpath = os.path.join(*args) - ensure_dir(os.path.dirname(fpath)) - with open(fpath, "w") as f: - f.write("Holla!") - - def readonly_file(*args): - fpath = os.path.join(*args) - os.chmod(fpath, stat.S_IREAD) - - with TempDirectory() as tmp_dir: - create_file(tmp_dir.path, "normal-file") - create_file(tmp_dir.path, "readonly-file") - readonly_file(tmp_dir.path, "readonly-file") - - create_file(tmp_dir.path, "subfolder", "normal-file") - create_file(tmp_dir.path, "subfolder", "readonly-file") - readonly_file(tmp_dir.path, "subfolder", "readonly-file") - - assert not os.path.exists(tmp_dir.path) - - def test_create_and_cleanup_work(self): - tmp_dir = TempDirectory() - created_path = tmp_dir.path - - assert tmp_dir.path is not None - assert os.path.exists(created_path) - - tmp_dir.cleanup() - assert tmp_dir.path is not None - assert not os.path.exists(created_path) - - @pytest.mark.parametrize("name", [ - "ABC", - "ABC.dist-info", - "_+-", - "_package", - "A......B", - "AB", - "A", - "2", - ]) - def test_adjacent_directory_names(self, name): - def names(): - return AdjacentTempDirectory._generate_names(name) - - chars = AdjacentTempDirectory.LEADING_CHARS - - # Ensure many names are unique - # (For long *name*, this sequence can be extremely long. - # However, since we're only ever going to take the first - # result that works, provided there are many of those - # and that shorter names result in totally unique sets, - # it's okay to skip part of the test.) - some_names = list(itertools.islice(names(), 1000)) - # We should always get at least 1000 names - assert len(some_names) == 1000 - - # Ensure original name does not appear early in the set - assert name not in some_names - - if len(name) > 2: - # Names should be at least 90% unique (given the infinite - # range of inputs, and the possibility that generated names - # may already exist on disk anyway, this is a much cheaper - # criteria to enforce than complete uniqueness). - assert len(some_names) > 0.9 * len(set(some_names)) - - # Ensure the first few names are the same length as the original - same_len = list(itertools.takewhile( - lambda x: len(x) == len(name), - some_names - )) - assert len(same_len) > 10 - - # Check the first group are correct - expected_names = ['~' + name[1:]] - expected_names.extend('~' + c + name[2:] for c in chars) - for x, y in zip(some_names, expected_names): - assert x == y - - else: - # All names are going to be longer than our original - assert min(len(x) for x in some_names) > 1 - - # All names are going to be unique - assert len(some_names) == len(set(some_names)) - - if len(name) == 2: - # All but the first name are going to end with our original - assert all(x.endswith(name) for x in some_names[1:]) - else: - # All names are going to end with our original - assert all(x.endswith(name) for x in some_names) - - @pytest.mark.parametrize("name", [ - "A", - "ABC", - "ABC.dist-info", - "_+-", - "_package", - ]) - def test_adjacent_directory_exists(self, name, tmpdir): - block_name, expect_name = itertools.islice( - AdjacentTempDirectory._generate_names(name), 2) - - original = os.path.join(tmpdir, name) - blocker = os.path.join(tmpdir, block_name) - - ensure_dir(original) - ensure_dir(blocker) - - with AdjacentTempDirectory(original) as atmp_dir: - assert expect_name == os.path.split(atmp_dir.path)[1] - - def test_adjacent_directory_permission_error(self, monkeypatch): - name = "ABC" - - def raising_mkdir(*args, **kwargs): - raise OSError("Unknown OSError") - - with TempDirectory() as tmp_dir: - original = os.path.join(tmp_dir.path, name) - - ensure_dir(original) - monkeypatch.setattr("os.mkdir", raising_mkdir) - - with pytest.raises(OSError): - with AdjacentTempDirectory(original): - pass - - def raises(error): raise error diff --git a/tests/unit/test_utils_temp_dir.py b/tests/unit/test_utils_temp_dir.py new file mode 100644 index 00000000000..8e5cfa62a37 --- /dev/null +++ b/tests/unit/test_utils_temp_dir.py @@ -0,0 +1,169 @@ +import itertools +import os +import stat +import tempfile + +import pytest + +from pip._internal.utils.misc import ensure_dir +from pip._internal.utils.temp_dir import AdjacentTempDirectory, TempDirectory + + +class TestTempDirectory(object): + + # No need to test symlinked directories on Windows + @pytest.mark.skipif("sys.platform == 'win32'") + def test_symlinked_path(self): + with TempDirectory() as tmp_dir: + assert os.path.exists(tmp_dir.path) + + alt_tmp_dir = tempfile.mkdtemp(prefix="pip-test-") + assert ( + os.path.dirname(tmp_dir.path) == + os.path.dirname(os.path.realpath(alt_tmp_dir)) + ) + # are we on a system where /tmp is a symlink + if os.path.realpath(alt_tmp_dir) != os.path.abspath(alt_tmp_dir): + assert ( + os.path.dirname(tmp_dir.path) != + os.path.dirname(alt_tmp_dir) + ) + else: + assert ( + os.path.dirname(tmp_dir.path) == + os.path.dirname(alt_tmp_dir) + ) + os.rmdir(tmp_dir.path) + assert not os.path.exists(tmp_dir.path) + + def test_deletes_readonly_files(self): + def create_file(*args): + fpath = os.path.join(*args) + ensure_dir(os.path.dirname(fpath)) + with open(fpath, "w") as f: + f.write("Holla!") + + def readonly_file(*args): + fpath = os.path.join(*args) + os.chmod(fpath, stat.S_IREAD) + + with TempDirectory() as tmp_dir: + create_file(tmp_dir.path, "normal-file") + create_file(tmp_dir.path, "readonly-file") + readonly_file(tmp_dir.path, "readonly-file") + + create_file(tmp_dir.path, "subfolder", "normal-file") + create_file(tmp_dir.path, "subfolder", "readonly-file") + readonly_file(tmp_dir.path, "subfolder", "readonly-file") + + assert not os.path.exists(tmp_dir.path) + + def test_create_and_cleanup_work(self): + tmp_dir = TempDirectory() + created_path = tmp_dir.path + + assert tmp_dir.path is not None + assert os.path.exists(created_path) + + tmp_dir.cleanup() + assert tmp_dir.path is not None + assert not os.path.exists(created_path) + + @pytest.mark.parametrize("name", [ + "ABC", + "ABC.dist-info", + "_+-", + "_package", + "A......B", + "AB", + "A", + "2", + ]) + def test_adjacent_directory_names(self, name): + def names(): + return AdjacentTempDirectory._generate_names(name) + + chars = AdjacentTempDirectory.LEADING_CHARS + + # Ensure many names are unique + # (For long *name*, this sequence can be extremely long. + # However, since we're only ever going to take the first + # result that works, provided there are many of those + # and that shorter names result in totally unique sets, + # it's okay to skip part of the test.) + some_names = list(itertools.islice(names(), 1000)) + # We should always get at least 1000 names + assert len(some_names) == 1000 + + # Ensure original name does not appear early in the set + assert name not in some_names + + if len(name) > 2: + # Names should be at least 90% unique (given the infinite + # range of inputs, and the possibility that generated names + # may already exist on disk anyway, this is a much cheaper + # criteria to enforce than complete uniqueness). + assert len(some_names) > 0.9 * len(set(some_names)) + + # Ensure the first few names are the same length as the original + same_len = list(itertools.takewhile( + lambda x: len(x) == len(name), + some_names + )) + assert len(same_len) > 10 + + # Check the first group are correct + expected_names = ['~' + name[1:]] + expected_names.extend('~' + c + name[2:] for c in chars) + for x, y in zip(some_names, expected_names): + assert x == y + + else: + # All names are going to be longer than our original + assert min(len(x) for x in some_names) > 1 + + # All names are going to be unique + assert len(some_names) == len(set(some_names)) + + if len(name) == 2: + # All but the first name are going to end with our original + assert all(x.endswith(name) for x in some_names[1:]) + else: + # All names are going to end with our original + assert all(x.endswith(name) for x in some_names) + + @pytest.mark.parametrize("name", [ + "A", + "ABC", + "ABC.dist-info", + "_+-", + "_package", + ]) + def test_adjacent_directory_exists(self, name, tmpdir): + block_name, expect_name = itertools.islice( + AdjacentTempDirectory._generate_names(name), 2) + + original = os.path.join(tmpdir, name) + blocker = os.path.join(tmpdir, block_name) + + ensure_dir(original) + ensure_dir(blocker) + + with AdjacentTempDirectory(original) as atmp_dir: + assert expect_name == os.path.split(atmp_dir.path)[1] + + def test_adjacent_directory_permission_error(self, monkeypatch): + name = "ABC" + + def raising_mkdir(*args, **kwargs): + raise OSError("Unknown OSError") + + with TempDirectory() as tmp_dir: + original = os.path.join(tmp_dir.path, name) + + ensure_dir(original) + monkeypatch.setattr("os.mkdir", raising_mkdir) + + with pytest.raises(OSError): + with AdjacentTempDirectory(original): + pass From 7ea1fcdb13a72462c26c5f2cb143f2ec0f15ffd0 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Mon, 23 Sep 2019 18:49:12 -0400 Subject: [PATCH 0359/3170] Remove outer class for temp_dir tests --- tests/unit/test_utils_temp_dir.py | 293 +++++++++++++++--------------- 1 file changed, 148 insertions(+), 145 deletions(-) diff --git a/tests/unit/test_utils_temp_dir.py b/tests/unit/test_utils_temp_dir.py index 8e5cfa62a37..9f7939c57d6 100644 --- a/tests/unit/test_utils_temp_dir.py +++ b/tests/unit/test_utils_temp_dir.py @@ -9,161 +9,164 @@ from pip._internal.utils.temp_dir import AdjacentTempDirectory, TempDirectory -class TestTempDirectory(object): - - # No need to test symlinked directories on Windows - @pytest.mark.skipif("sys.platform == 'win32'") - def test_symlinked_path(self): - with TempDirectory() as tmp_dir: - assert os.path.exists(tmp_dir.path) - - alt_tmp_dir = tempfile.mkdtemp(prefix="pip-test-") +# No need to test symlinked directories on Windows +@pytest.mark.skipif("sys.platform == 'win32'") +def test_symlinked_path(): + with TempDirectory() as tmp_dir: + assert os.path.exists(tmp_dir.path) + + alt_tmp_dir = tempfile.mkdtemp(prefix="pip-test-") + assert ( + os.path.dirname(tmp_dir.path) == + os.path.dirname(os.path.realpath(alt_tmp_dir)) + ) + # are we on a system where /tmp is a symlink + if os.path.realpath(alt_tmp_dir) != os.path.abspath(alt_tmp_dir): assert ( - os.path.dirname(tmp_dir.path) == - os.path.dirname(os.path.realpath(alt_tmp_dir)) + os.path.dirname(tmp_dir.path) != + os.path.dirname(alt_tmp_dir) ) - # are we on a system where /tmp is a symlink - if os.path.realpath(alt_tmp_dir) != os.path.abspath(alt_tmp_dir): - assert ( - os.path.dirname(tmp_dir.path) != - os.path.dirname(alt_tmp_dir) - ) - else: - assert ( - os.path.dirname(tmp_dir.path) == - os.path.dirname(alt_tmp_dir) - ) - os.rmdir(tmp_dir.path) - assert not os.path.exists(tmp_dir.path) - - def test_deletes_readonly_files(self): - def create_file(*args): - fpath = os.path.join(*args) - ensure_dir(os.path.dirname(fpath)) - with open(fpath, "w") as f: - f.write("Holla!") - - def readonly_file(*args): - fpath = os.path.join(*args) - os.chmod(fpath, stat.S_IREAD) - - with TempDirectory() as tmp_dir: - create_file(tmp_dir.path, "normal-file") - create_file(tmp_dir.path, "readonly-file") - readonly_file(tmp_dir.path, "readonly-file") - - create_file(tmp_dir.path, "subfolder", "normal-file") - create_file(tmp_dir.path, "subfolder", "readonly-file") - readonly_file(tmp_dir.path, "subfolder", "readonly-file") - + else: + assert ( + os.path.dirname(tmp_dir.path) == + os.path.dirname(alt_tmp_dir) + ) + os.rmdir(tmp_dir.path) assert not os.path.exists(tmp_dir.path) - def test_create_and_cleanup_work(self): - tmp_dir = TempDirectory() - created_path = tmp_dir.path - - assert tmp_dir.path is not None - assert os.path.exists(created_path) - - tmp_dir.cleanup() - assert tmp_dir.path is not None - assert not os.path.exists(created_path) - - @pytest.mark.parametrize("name", [ - "ABC", - "ABC.dist-info", - "_+-", - "_package", - "A......B", - "AB", - "A", - "2", - ]) - def test_adjacent_directory_names(self, name): - def names(): - return AdjacentTempDirectory._generate_names(name) - - chars = AdjacentTempDirectory.LEADING_CHARS - - # Ensure many names are unique - # (For long *name*, this sequence can be extremely long. - # However, since we're only ever going to take the first - # result that works, provided there are many of those - # and that shorter names result in totally unique sets, - # it's okay to skip part of the test.) - some_names = list(itertools.islice(names(), 1000)) - # We should always get at least 1000 names - assert len(some_names) == 1000 - - # Ensure original name does not appear early in the set - assert name not in some_names - - if len(name) > 2: - # Names should be at least 90% unique (given the infinite - # range of inputs, and the possibility that generated names - # may already exist on disk anyway, this is a much cheaper - # criteria to enforce than complete uniqueness). - assert len(some_names) > 0.9 * len(set(some_names)) - - # Ensure the first few names are the same length as the original - same_len = list(itertools.takewhile( - lambda x: len(x) == len(name), - some_names - )) - assert len(same_len) > 10 - - # Check the first group are correct - expected_names = ['~' + name[1:]] - expected_names.extend('~' + c + name[2:] for c in chars) - for x, y in zip(some_names, expected_names): - assert x == y +def test_deletes_readonly_files(): + def create_file(*args): + fpath = os.path.join(*args) + ensure_dir(os.path.dirname(fpath)) + with open(fpath, "w") as f: + f.write("Holla!") + + def readonly_file(*args): + fpath = os.path.join(*args) + os.chmod(fpath, stat.S_IREAD) + + with TempDirectory() as tmp_dir: + create_file(tmp_dir.path, "normal-file") + create_file(tmp_dir.path, "readonly-file") + readonly_file(tmp_dir.path, "readonly-file") + + create_file(tmp_dir.path, "subfolder", "normal-file") + create_file(tmp_dir.path, "subfolder", "readonly-file") + readonly_file(tmp_dir.path, "subfolder", "readonly-file") + + assert not os.path.exists(tmp_dir.path) + + +def test_create_and_cleanup_work(): + tmp_dir = TempDirectory() + created_path = tmp_dir.path + + assert tmp_dir.path is not None + assert os.path.exists(created_path) + + tmp_dir.cleanup() + assert tmp_dir.path is not None + assert not os.path.exists(created_path) + + +@pytest.mark.parametrize("name", [ + "ABC", + "ABC.dist-info", + "_+-", + "_package", + "A......B", + "AB", + "A", + "2", +]) +def test_adjacent_directory_names(name): + def names(): + return AdjacentTempDirectory._generate_names(name) + + chars = AdjacentTempDirectory.LEADING_CHARS + + # Ensure many names are unique + # (For long *name*, this sequence can be extremely long. + # However, since we're only ever going to take the first + # result that works, provided there are many of those + # and that shorter names result in totally unique sets, + # it's okay to skip part of the test.) + some_names = list(itertools.islice(names(), 1000)) + # We should always get at least 1000 names + assert len(some_names) == 1000 + + # Ensure original name does not appear early in the set + assert name not in some_names + + if len(name) > 2: + # Names should be at least 90% unique (given the infinite + # range of inputs, and the possibility that generated names + # may already exist on disk anyway, this is a much cheaper + # criteria to enforce than complete uniqueness). + assert len(some_names) > 0.9 * len(set(some_names)) + + # Ensure the first few names are the same length as the original + same_len = list(itertools.takewhile( + lambda x: len(x) == len(name), + some_names + )) + assert len(same_len) > 10 + + # Check the first group are correct + expected_names = ['~' + name[1:]] + expected_names.extend('~' + c + name[2:] for c in chars) + for x, y in zip(some_names, expected_names): + assert x == y + + else: + # All names are going to be longer than our original + assert min(len(x) for x in some_names) > 1 + + # All names are going to be unique + assert len(some_names) == len(set(some_names)) + + if len(name) == 2: + # All but the first name are going to end with our original + assert all(x.endswith(name) for x in some_names[1:]) else: - # All names are going to be longer than our original - assert min(len(x) for x in some_names) > 1 - - # All names are going to be unique - assert len(some_names) == len(set(some_names)) - - if len(name) == 2: - # All but the first name are going to end with our original - assert all(x.endswith(name) for x in some_names[1:]) - else: - # All names are going to end with our original - assert all(x.endswith(name) for x in some_names) - - @pytest.mark.parametrize("name", [ - "A", - "ABC", - "ABC.dist-info", - "_+-", - "_package", - ]) - def test_adjacent_directory_exists(self, name, tmpdir): - block_name, expect_name = itertools.islice( - AdjacentTempDirectory._generate_names(name), 2) - - original = os.path.join(tmpdir, name) - blocker = os.path.join(tmpdir, block_name) + # All names are going to end with our original + assert all(x.endswith(name) for x in some_names) - ensure_dir(original) - ensure_dir(blocker) - with AdjacentTempDirectory(original) as atmp_dir: - assert expect_name == os.path.split(atmp_dir.path)[1] +@pytest.mark.parametrize("name", [ + "A", + "ABC", + "ABC.dist-info", + "_+-", + "_package", +]) +def test_adjacent_directory_exists(name, tmpdir): + block_name, expect_name = itertools.islice( + AdjacentTempDirectory._generate_names(name), 2) + + original = os.path.join(tmpdir, name) + blocker = os.path.join(tmpdir, block_name) + + ensure_dir(original) + ensure_dir(blocker) - def test_adjacent_directory_permission_error(self, monkeypatch): - name = "ABC" + with AdjacentTempDirectory(original) as atmp_dir: + assert expect_name == os.path.split(atmp_dir.path)[1] - def raising_mkdir(*args, **kwargs): - raise OSError("Unknown OSError") - with TempDirectory() as tmp_dir: - original = os.path.join(tmp_dir.path, name) +def test_adjacent_directory_permission_error(monkeypatch): + name = "ABC" - ensure_dir(original) - monkeypatch.setattr("os.mkdir", raising_mkdir) + def raising_mkdir(*args, **kwargs): + raise OSError("Unknown OSError") + + with TempDirectory() as tmp_dir: + original = os.path.join(tmp_dir.path, name) + + ensure_dir(original) + monkeypatch.setattr("os.mkdir", raising_mkdir) - with pytest.raises(OSError): - with AdjacentTempDirectory(original): - pass + with pytest.raises(OSError): + with AdjacentTempDirectory(original): + pass From c0809f4183cbf40e3fd69245095537456d74b97f Mon Sep 17 00:00:00 2001 From: Tony Beswick <tonybeswick@orcon.net.nz> Date: Tue, 24 Sep 2019 11:46:02 +1200 Subject: [PATCH 0360/3170] Added a `controls_location` implementation to the `Mercurial` class, so it porperly detects that subdirectories are under mercurial control. --- src/pip/_internal/vcs/mercurial.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/pip/_internal/vcs/mercurial.py b/src/pip/_internal/vcs/mercurial.py index 21697ff1584..5af9c121e4b 100644 --- a/src/pip/_internal/vcs/mercurial.py +++ b/src/pip/_internal/vcs/mercurial.py @@ -9,6 +9,7 @@ from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.vcs.versioncontrol import VersionControl, vcs +from pip._internal.exceptions import BadCommand if MYPY_CHECK_RUNNING: from pip._internal.utils.misc import HiddenText @@ -111,5 +112,16 @@ def is_commit_id_equal(cls, dest, name): """Always assume the versions don't match""" return False + @classmethod + def controls_location(cls, location): + if super(Mercurial, cls).controls_location(location): + return True + try: + r = cls.run_command(['identify'], cwd=location, show_stdout=False, extra_ok_returncodes=[255]) + return not r.startswith('abort:') + except BadCommand: + logger.debug("could not determine if %s is under hg control " + "because hg is not available", location) + return False vcs.register(Mercurial) From e6bde63620f8c32b8277163ade100cf3bcaf8eda Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Mon, 23 Sep 2019 20:00:01 -0400 Subject: [PATCH 0361/3170] Assert TempDirectory.path has not been cleaned up on access --- src/pip/_internal/utils/temp_dir.py | 9 +++++++-- tests/unit/test_utils_temp_dir.py | 22 ++++++++++++++++++++-- 2 files changed, 27 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/utils/temp_dir.py b/src/pip/_internal/utils/temp_dir.py index b2cec05181a..87e0becaee3 100644 --- a/src/pip/_internal/utils/temp_dir.py +++ b/src/pip/_internal/utils/temp_dir.py @@ -54,12 +54,16 @@ def __init__( path = self._create(kind) self._path = path + self._deleted = False self.delete = delete self.kind = kind @property def path(self): # type: () -> str + assert not self._deleted, ( + "Attempted to access deleted path: {}".format(self._path) + ) return self._path def __repr__(self): @@ -88,8 +92,9 @@ def _create(self, kind): def cleanup(self): """Remove the temporary directory created and reset state """ - if os.path.exists(self.path): - rmtree(self.path) + self._deleted = True + if os.path.exists(self._path): + rmtree(self._path) class AdjacentTempDirectory(TempDirectory): diff --git a/tests/unit/test_utils_temp_dir.py b/tests/unit/test_utils_temp_dir.py index 9f7939c57d6..20a7852e77d 100644 --- a/tests/unit/test_utils_temp_dir.py +++ b/tests/unit/test_utils_temp_dir.py @@ -55,7 +55,26 @@ def readonly_file(*args): create_file(tmp_dir.path, "subfolder", "readonly-file") readonly_file(tmp_dir.path, "subfolder", "readonly-file") - assert not os.path.exists(tmp_dir.path) + +def test_path_access_after_context_raises(): + with TempDirectory() as tmp_dir: + path = tmp_dir.path + + with pytest.raises(AssertionError) as e: + _ = tmp_dir.path + + assert path in str(e.value) + + +def test_path_access_after_clean_raises(): + tmp_dir = TempDirectory() + path = tmp_dir.path + tmp_dir.cleanup() + + with pytest.raises(AssertionError) as e: + _ = tmp_dir.path + + assert path in str(e.value) def test_create_and_cleanup_work(): @@ -66,7 +85,6 @@ def test_create_and_cleanup_work(): assert os.path.exists(created_path) tmp_dir.cleanup() - assert tmp_dir.path is not None assert not os.path.exists(created_path) From 063b899c3bffa00c6ab69901ad8477d6a28a2bcf Mon Sep 17 00:00:00 2001 From: Tony Beswick <tonybeswick@orcon.net.nz> Date: Tue, 24 Sep 2019 12:20:53 +1200 Subject: [PATCH 0362/3170] Adding `get_subdirdctory` method to `Mercurial` class so it can detect a setup.py in a repo subdirectory. --- src/pip/_internal/vcs/mercurial.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/pip/_internal/vcs/mercurial.py b/src/pip/_internal/vcs/mercurial.py index 5af9c121e4b..7aa9b4ce1c0 100644 --- a/src/pip/_internal/vcs/mercurial.py +++ b/src/pip/_internal/vcs/mercurial.py @@ -9,6 +9,7 @@ from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.vcs.versioncontrol import VersionControl, vcs +from pip._internal.utils.compat import samefile from pip._internal.exceptions import BadCommand if MYPY_CHECK_RUNNING: @@ -112,6 +113,32 @@ def is_commit_id_equal(cls, dest, name): """Always assume the versions don't match""" return False + @classmethod + def get_subdirectory(cls, location): + # find the repo root + root_dir = cls.run_command(['root'], + show_stdout=False, cwd=location).strip() + if not os.path.isabs(root_dir): + root_dir = os.path.join(location, root_dir) + # find setup.py + orig_location = location + while not os.path.exists(os.path.join(location, 'setup.py')): + last_location = location + location = os.path.dirname(location) + if location == last_location: + # We've traversed up to the root of the filesystem without + # finding setup.py + logger.warning( + "Could not find setup.py for directory %s (tried all " + "parent directories)", + orig_location, + ) + return None + # relative path of setup.py to repo root + if samefile(root_dir, location): + return None + return os.path.relpath(location, root_dir) + @classmethod def controls_location(cls, location): if super(Mercurial, cls).controls_location(location): From 73a6342d55d22d2e1086008981d5e84aa57a999f Mon Sep 17 00:00:00 2001 From: Tony Beswick <tonybeswick@orcon.net.nz> Date: Tue, 24 Sep 2019 12:28:40 +1200 Subject: [PATCH 0363/3170] Added unit test for Mercurial to check for correct behaviour when either the source or setup.py is located in a subdirectory of the repo root. --- tests/functional/test_freeze.py | 41 +++++++++++++++++++++++++++++++++ 1 file changed, 41 insertions(+) diff --git a/tests/functional/test_freeze.py b/tests/functional/test_freeze.py index 77f83796abc..a06e81b7ae4 100644 --- a/tests/functional/test_freeze.py +++ b/tests/functional/test_freeze.py @@ -321,6 +321,47 @@ def test_freeze_git_clone_srcdir(script, tmpdir): ).strip() _check_output(result.stdout, expected) +@need_mercurial +def test_freeze_mrecurial_clone_srcdir(script, tmpdir): + """ + Test freezing a Mercurial clone where setup.py is in a subdirectory + relative to the repo root and the source code is in a subdirectory + relative to setup.py. + """ + # Returns path to a generated package called "version_pkg" + pkg_version = _create_test_package_with_srcdir(script, vcs='hg') + + result = script.run( + 'hg', 'clone', pkg_version, 'pip-test-package', + expect_stderr=True, + ) + repo_dir = script.scratch_path / 'pip-test-package' + result = script.run( + 'python', 'setup.py', 'develop', + cwd=repo_dir / 'subdir', + expect_stderr=True, + ) + result = script.pip('freeze', expect_stderr=True) + expected = textwrap.dedent( + """ + ...-e hg+...#egg=version_pkg&subdirectory=subdir + ... + """ + ).strip() + _check_output(result.stdout, expected) + + result = script.pip( + 'freeze', '-f', '%s#egg=pip_test_package' % repo_dir, + expect_stderr=True, + ) + expected = textwrap.dedent( + """ + -f %(repo)s#egg=pip_test_package... + -e hg+...#egg=version_pkg&subdirectory=subdir + ... + """ % {'repo': repo_dir}, + ).strip() + _check_output(result.stdout, expected) @pytest.mark.git def test_freeze_git_remote(script, tmpdir): From 356c6d20598aed809c1e3fa3582fec343a5e0a26 Mon Sep 17 00:00:00 2001 From: tbeswick <tbeswick@enphaseenergy.com> Date: Tue, 24 Sep 2019 16:33:39 +1200 Subject: [PATCH 0364/3170] fixed typo --- tests/functional/test_freeze.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/test_freeze.py b/tests/functional/test_freeze.py index a06e81b7ae4..39e7661e7eb 100644 --- a/tests/functional/test_freeze.py +++ b/tests/functional/test_freeze.py @@ -322,7 +322,7 @@ def test_freeze_git_clone_srcdir(script, tmpdir): _check_output(result.stdout, expected) @need_mercurial -def test_freeze_mrecurial_clone_srcdir(script, tmpdir): +def test_freeze_mercurial_clone_srcdir(script, tmpdir): """ Test freezing a Mercurial clone where setup.py is in a subdirectory relative to the repo root and the source code is in a subdirectory From b95a16e310be3caa18c24a430ad94161f486e733 Mon Sep 17 00:00:00 2001 From: tbeswick <tbeswick@enphaseenergy.com> Date: Tue, 24 Sep 2019 16:50:05 +1200 Subject: [PATCH 0365/3170] Added news --- news/7071.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/7071.bugfix diff --git a/news/7071.bugfix b/news/7071.bugfix new file mode 100644 index 00000000000..e4fa847d78f --- /dev/null +++ b/news/7071.bugfix @@ -0,0 +1 @@ +Fix `pip freeze` not showing correct entry for mercurial packages that use subdirectories. \ No newline at end of file From 0435316fde2c84f69ea067db475bac4f60339d48 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek <chris.jerdonek@gmail.com> Date: Tue, 24 Sep 2019 01:56:42 -0700 Subject: [PATCH 0366/3170] Move path_to_url() to utils/urls.py. --- src/pip/_internal/cache.py | 2 +- src/pip/_internal/collector.py | 4 ++-- src/pip/_internal/download.py | 2 -- src/pip/_internal/models/link.py | 3 +-- src/pip/_internal/req/constructors.py | 3 ++- src/pip/_internal/utils/misc.py | 12 ------------ src/pip/_internal/utils/urls.py | 12 ++++++++++++ src/pip/_internal/vcs/bazaar.py | 8 ++------ src/pip/_internal/vcs/mercurial.py | 3 ++- src/pip/_internal/wheel.py | 2 +- tests/unit/test_download.py | 2 +- tests/unit/test_req.py | 2 +- tests/unit/test_urls.py | 21 +++++++++++++++++++-- tests/unit/test_utils.py | 18 ------------------ 14 files changed, 44 insertions(+), 50 deletions(-) diff --git a/src/pip/_internal/cache.py b/src/pip/_internal/cache.py index 433b27bb9c2..3b02441e12d 100644 --- a/src/pip/_internal/cache.py +++ b/src/pip/_internal/cache.py @@ -13,9 +13,9 @@ from pip._internal.models.link import Link from pip._internal.utils.compat import expanduser -from pip._internal.utils.misc import path_to_url from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from pip._internal.utils.urls import path_to_url from pip._internal.wheel import InvalidWheelFilename, Wheel if MYPY_CHECK_RUNNING: diff --git a/src/pip/_internal/collector.py b/src/pip/_internal/collector.py index 8a1a0d7e418..c96cc711967 100644 --- a/src/pip/_internal/collector.py +++ b/src/pip/_internal/collector.py @@ -17,9 +17,9 @@ from pip._internal.models.link import Link from pip._internal.utils.filetypes import ARCHIVE_EXTENSIONS -from pip._internal.utils.misc import path_to_url, redact_auth_from_url +from pip._internal.utils.misc import redact_auth_from_url from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from pip._internal.utils.urls import url_to_path +from pip._internal.utils.urls import path_to_url, url_to_path from pip._internal.vcs import is_url, vcs if MYPY_CHECK_RUNNING: diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index 53646149cc3..647c0dbae08 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -51,7 +51,6 @@ hide_url, parse_netloc, path_to_display, - path_to_url, rmtree, splitext, ) @@ -98,7 +97,6 @@ __all__ = ['get_file_content', - 'path_to_url', 'unpack_vcs_link', 'unpack_file_url', 'is_file_url', 'unpack_http_url', 'unpack_url', diff --git a/src/pip/_internal/models/link.py b/src/pip/_internal/models/link.py index c109804b8f3..e1412b3c234 100644 --- a/src/pip/_internal/models/link.py +++ b/src/pip/_internal/models/link.py @@ -5,14 +5,13 @@ from pip._internal.utils.filetypes import WHEEL_EXTENSION from pip._internal.utils.misc import ( - path_to_url, redact_auth_from_url, split_auth_from_netloc, splitext, ) from pip._internal.utils.models import KeyBasedCompareMixin from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from pip._internal.utils.urls import url_to_path +from pip._internal.utils.urls import path_to_url, url_to_path if MYPY_CHECK_RUNNING: from typing import Optional, Text, Tuple, Union diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index bd16d4363b5..acb353e816a 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -26,8 +26,9 @@ from pip._internal.pyproject import make_pyproject_path from pip._internal.req.req_install import InstallRequirement from pip._internal.utils.filetypes import ARCHIVE_EXTENSIONS -from pip._internal.utils.misc import is_installable_dir, path_to_url, splitext +from pip._internal.utils.misc import is_installable_dir, splitext from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from pip._internal.utils.urls import path_to_url from pip._internal.vcs import is_url, vcs from pip._internal.wheel import Wheel diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 5f13f975c29..5b66a9c6dbe 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -23,7 +23,6 @@ from pip._vendor.six import PY2, text_type from pip._vendor.six.moves import input, shlex_quote 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 from pip import __version__ @@ -882,17 +881,6 @@ def enum(*sequential, **named): return type('Enum', (), enums) -def path_to_url(path): - # type: (Union[str, Text]) -> str - """ - Convert a path to a file: URL. The path will be made absolute and have - quoted path parts. - """ - path = os.path.normpath(os.path.abspath(path)) - url = urllib_parse.urljoin('file:', urllib_request.pathname2url(path)) - return url - - def build_netloc(host, port): # type: (str, Optional[int]) -> str """ diff --git a/src/pip/_internal/utils/urls.py b/src/pip/_internal/utils/urls.py index 9c5385044c7..9ad40feb345 100644 --- a/src/pip/_internal/utils/urls.py +++ b/src/pip/_internal/utils/urls.py @@ -1,3 +1,4 @@ +import os import sys from pip._vendor.six.moves.urllib import parse as urllib_parse @@ -16,6 +17,17 @@ def get_url_scheme(url): return url.split(':', 1)[0].lower() +def path_to_url(path): + # type: (Union[str, Text]) -> str + """ + Convert a path to a file: URL. The path will be made absolute and have + quoted path parts. + """ + path = os.path.normpath(os.path.abspath(path)) + url = urllib_parse.urljoin('file:', urllib_request.pathname2url(path)) + return url + + def url_to_path(url): # type: (str) -> str """ diff --git a/src/pip/_internal/vcs/bazaar.py b/src/pip/_internal/vcs/bazaar.py index c49eba50e53..325b378ecd2 100644 --- a/src/pip/_internal/vcs/bazaar.py +++ b/src/pip/_internal/vcs/bazaar.py @@ -5,13 +5,9 @@ from pip._vendor.six.moves.urllib import parse as urllib_parse -from pip._internal.utils.misc import ( - display_path, - make_command, - path_to_url, - rmtree, -) +from pip._internal.utils.misc import display_path, make_command, rmtree from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from pip._internal.utils.urls import path_to_url from pip._internal.vcs.versioncontrol import VersionControl, vcs if MYPY_CHECK_RUNNING: diff --git a/src/pip/_internal/vcs/mercurial.py b/src/pip/_internal/vcs/mercurial.py index 21697ff1584..4732d3863eb 100644 --- a/src/pip/_internal/vcs/mercurial.py +++ b/src/pip/_internal/vcs/mercurial.py @@ -5,9 +5,10 @@ from pip._vendor.six.moves import configparser -from pip._internal.utils.misc import display_path, make_command, path_to_url +from pip._internal.utils.misc import display_path, make_command from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from pip._internal.utils.urls import path_to_url from pip._internal.vcs.versioncontrol import VersionControl, vcs if MYPY_CHECK_RUNNING: diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index cdf17c0ae1d..1768e1ca84d 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -43,7 +43,6 @@ captured_stdout, ensure_dir, format_command_args, - path_to_url, read_chunks, ) from pip._internal.utils.setuptools_build import make_setuptools_shim_args @@ -51,6 +50,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.ui import open_spinner from pip._internal.utils.unpacking import unpack_file +from pip._internal.utils.urls import path_to_url if MYPY_CHECK_RUNNING: from typing import ( diff --git a/tests/unit/test_download.py b/tests/unit/test_download.py index 3d052619e46..a83c85cc96a 100644 --- a/tests/unit/test_download.py +++ b/tests/unit/test_download.py @@ -26,7 +26,7 @@ from pip._internal.exceptions import HashMismatch from pip._internal.models.link import Link from pip._internal.utils.hashes import Hashes -from pip._internal.utils.misc import path_to_url +from pip._internal.utils.urls import path_to_url from tests.lib import create_file from tests.lib.filesystem import ( get_filelist, diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index eef6ca373ef..2635a580024 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -31,7 +31,7 @@ ) from pip._internal.req.req_file import process_line from pip._internal.req.req_tracker import RequirementTracker -from pip._internal.utils.misc import path_to_url +from pip._internal.utils.urls import path_to_url from tests.lib import ( DATA_DIR, assert_raises_regexp, diff --git a/tests/unit/test_urls.py b/tests/unit/test_urls.py index 68d544072f8..7428cef9ebc 100644 --- a/tests/unit/test_urls.py +++ b/tests/unit/test_urls.py @@ -1,9 +1,10 @@ +import os import sys import pytest +from pip._vendor.six.moves.urllib import request as urllib_request -from pip._internal.utils.misc import path_to_url -from pip._internal.utils.urls import get_url_scheme, url_to_path +from pip._internal.utils.urls import get_url_scheme, path_to_url, url_to_path @pytest.mark.parametrize("url,expected", [ @@ -16,6 +17,22 @@ def test_get_url_scheme(url, expected): assert get_url_scheme(url) == expected +@pytest.mark.skipif("sys.platform == 'win32'") +def test_path_to_url_unix(): + assert path_to_url('/tmp/file') == 'file:///tmp/file' + path = os.path.join(os.getcwd(), 'file') + assert path_to_url('file') == 'file://' + urllib_request.pathname2url(path) + + +@pytest.mark.skipif("sys.platform != 'win32'") +def test_path_to_url_win(): + assert path_to_url('c:/tmp/file') == 'file:///C:/tmp/file' + assert path_to_url('c:\\tmp\\file') == 'file:///C:/tmp/file' + assert path_to_url(r'\\unc\as\path') == 'file://unc/as/path' + path = os.path.join(os.getcwd(), 'file') + assert path_to_url('file') == 'file:' + urllib_request.pathname2url(path) + + @pytest.mark.parametrize("url,win_expected,non_win_expected", [ ('file:tmp', 'tmp', 'tmp'), ('file:c:/path/to/file', r'C:\path\to\file', 'c:/path/to/file'), diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index c34dea9165f..8dd22a8bdc7 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -20,7 +20,6 @@ import pytest from mock import Mock, patch -from pip._vendor.six.moves.urllib import request as urllib_request from pip._internal.exceptions import ( HashMismatch, @@ -54,7 +53,6 @@ normalize_version_info, parse_netloc, path_to_display, - path_to_url, redact_auth_from_url, redact_netloc, remove_auth_from_url, @@ -1188,22 +1186,6 @@ def test_closes_stdin(self): ) -@pytest.mark.skipif("sys.platform == 'win32'") -def test_path_to_url_unix(): - assert path_to_url('/tmp/file') == 'file:///tmp/file' - path = os.path.join(os.getcwd(), 'file') - assert path_to_url('file') == 'file://' + urllib_request.pathname2url(path) - - -@pytest.mark.skipif("sys.platform != 'win32'") -def test_path_to_url_win(): - assert path_to_url('c:/tmp/file') == 'file:///C:/tmp/file' - assert path_to_url('c:\\tmp\\file') == 'file:///C:/tmp/file' - assert path_to_url(r'\\unc\as\path') == 'file://unc/as/path' - path = os.path.join(os.getcwd(), 'file') - assert path_to_url('file') == 'file:' + urllib_request.pathname2url(path) - - @pytest.mark.parametrize('host_port, expected_netloc', [ # Test domain name. (('example.com', None), 'example.com'), From 657a7cb0c287043809ecacefba67cf4294feafb6 Mon Sep 17 00:00:00 2001 From: tbeswick <tbeswick@enphaseenergy.com> Date: Tue, 24 Sep 2019 21:05:30 +1200 Subject: [PATCH 0367/3170] - Added missing argument to run_command: on_returncode='ignore' - fixed lint errors. --- src/pip/_internal/vcs/mercurial.py | 13 +++++++++---- tests/functional/test_freeze.py | 6 ++++-- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/vcs/mercurial.py b/src/pip/_internal/vcs/mercurial.py index 7aa9b4ce1c0..2b13a04cb0b 100644 --- a/src/pip/_internal/vcs/mercurial.py +++ b/src/pip/_internal/vcs/mercurial.py @@ -5,12 +5,12 @@ from pip._vendor.six.moves import configparser +from pip._internal.exceptions import BadCommand +from pip._internal.utils.compat import samefile from pip._internal.utils.misc import display_path, make_command, path_to_url from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.vcs.versioncontrol import VersionControl, vcs -from pip._internal.utils.compat import samefile -from pip._internal.exceptions import BadCommand if MYPY_CHECK_RUNNING: from pip._internal.utils.misc import HiddenText @@ -117,7 +117,7 @@ def is_commit_id_equal(cls, dest, name): def get_subdirectory(cls, location): # find the repo root root_dir = cls.run_command(['root'], - show_stdout=False, cwd=location).strip() + show_stdout=False, cwd=location).strip() if not os.path.isabs(root_dir): root_dir = os.path.join(location, root_dir) # find setup.py @@ -144,11 +144,16 @@ def controls_location(cls, location): if super(Mercurial, cls).controls_location(location): return True try: - r = cls.run_command(['identify'], cwd=location, show_stdout=False, extra_ok_returncodes=[255]) + r = cls.run_command(['identify'], + cwd=location, + show_stdout=False, + on_returncode='ignore', + extra_ok_returncodes=[255]) return not r.startswith('abort:') except BadCommand: logger.debug("could not determine if %s is under hg control " "because hg is not available", location) return False + vcs.register(Mercurial) diff --git a/tests/functional/test_freeze.py b/tests/functional/test_freeze.py index 39e7661e7eb..b3e06bfdc04 100644 --- a/tests/functional/test_freeze.py +++ b/tests/functional/test_freeze.py @@ -321,6 +321,7 @@ def test_freeze_git_clone_srcdir(script, tmpdir): ).strip() _check_output(result.stdout, expected) + @need_mercurial def test_freeze_mercurial_clone_srcdir(script, tmpdir): """ @@ -330,7 +331,7 @@ def test_freeze_mercurial_clone_srcdir(script, tmpdir): """ # Returns path to a generated package called "version_pkg" pkg_version = _create_test_package_with_srcdir(script, vcs='hg') - + result = script.run( 'hg', 'clone', pkg_version, 'pip-test-package', expect_stderr=True, @@ -349,7 +350,7 @@ def test_freeze_mercurial_clone_srcdir(script, tmpdir): """ ).strip() _check_output(result.stdout, expected) - + result = script.pip( 'freeze', '-f', '%s#egg=pip_test_package' % repo_dir, expect_stderr=True, @@ -363,6 +364,7 @@ def test_freeze_mercurial_clone_srcdir(script, tmpdir): ).strip() _check_output(result.stdout, expected) + @pytest.mark.git def test_freeze_git_remote(script, tmpdir): """ From 096cfb2de093a4d8331f8473aaf11ce51a6b3ed8 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 24 Sep 2019 16:02:51 +0530 Subject: [PATCH 0368/3170] Add initial pre-commit configuration Why: pip's linting requirements aren't special and there are benefits to using pre-commit, including a simpler workflow and access to the ecosystem of tooling around pre-commit. --- .pre-commit-config.yaml | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 .pre-commit-config.yaml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 00000000000..453036e37ed --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,38 @@ +exclude: 'src/pip/_vendor/' + +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v2.3.0 + hooks: + # - id: check-builtin-literals # TODO: enable when fixed. + - id: check-added-large-files + - id: check-case-conflict + - id: check-toml + - id: check-yaml + - id: debug-statements + # - id: end-of-file-fixer # TODO: enable when fixed. + - id: flake8 + exclude: tests/data + - id: forbid-new-submodules + # - id: trailing-whitespace # TODO: enable when fixed. + # exclude: .patch + +# TODO: enable when fixed. +# - repo: https://github.com/timothycrosley/isort +# rev: 4.3.21 +# hooks: +# - id: isort +# files: \.py$ + +- repo: https://github.com/pre-commit/mirrors-mypy + rev: v0.720 + hooks: + - id: mypy + exclude: docs|tests + +- repo: https://github.com/pre-commit/pygrep-hooks + rev: v1.4.1 + hooks: + - id: python-no-log-warn + - id: python-no-eval + # - id: rst-backticks # TODO: enable when fixed. From dc5d2b91c8a0bd2066bfb168c82345aa06062168 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 24 Sep 2019 16:51:05 +0530 Subject: [PATCH 0369/3170] Drop lint-py2 and mypy from tox environment Why: They would become redundant in following commits, which move the linting workloads over to pre-commit. --- tox.ini | 21 +-------------------- 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/tox.ini b/tox.ini index c10d9eae77b..1fb195b1104 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] minversion = 3.4.0 envlist = - docs, packaging, mypy, lint, lint-py2, + docs, packaging, lint, py27, py35, py36, py37, py38, pypy, pypy3 [helpers] @@ -61,22 +61,3 @@ commands_pre = commands = flake8 isort --check-only --diff - -[testenv:lint-py2] -skip_install = True -basepython = python2 -deps = {[lint]deps} -commands_pre = -# No need to flake8 docs, tools & tasks in py2 -commands = - flake8 src tests - isort --check-only --diff - -[testenv:mypy] -skip_install = True -basepython = python3 -deps = -r{toxinidir}/tools/requirements/mypy.txt -commands_pre = -commands = - mypy src - mypy src -2 From ea8026395871d9dbe076ab718ac7c00fda9a7d70 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 24 Sep 2019 16:51:34 +0530 Subject: [PATCH 0370/3170] Drop lint-py2 and mypy from Travis CI --- .travis.yml | 3 --- tools/travis/run.sh | 7 +++---- 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/.travis.yml b/.travis.yml index 48c99a2208c..4005a83dee3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,9 +17,6 @@ jobs: - stage: primary env: TOXENV=docs - env: TOXENV=lint - - env: TOXENV=lint-py2 - python: 2.7 - - env: TOXENV=mypy - env: TOXENV=packaging # Latest CPython - env: GROUP=1 diff --git a/tools/travis/run.sh b/tools/travis/run.sh index a7b15d0822a..3d64ec2bd8c 100755 --- a/tools/travis/run.sh +++ b/tools/travis/run.sh @@ -1,9 +1,8 @@ #!/bin/bash set -e -# Short circuit tests and linting jobs if there are no code changes involved. -if [[ $TOXENV != docs ]] && [[ $TOXENV != lint-py2 ]] && [[ $TOXENV != lint ]]; then - # Keep lint and lint-py2, for docs/conf.py +# Short circuit test runs if there are no code changes involved. +if ! [[ $TOXENV ~= ^(docs|lint|packaging)$ ]]; then if [[ "$TRAVIS_PULL_REQUEST" == "false" ]] then echo "This is not a PR -- will do a complete build." @@ -18,7 +17,7 @@ if [[ $TOXENV != docs ]] && [[ $TOXENV != lint-py2 ]] && [[ $TOXENV != lint ]]; echo "$changes" if ! echo "$changes" | grep -qvE '(\.rst$)|(^docs)|(^news)|(^\.github)' then - echo "Only Documentation was updated -- skipping build." + echo "Code was not changed -- skipping build." exit fi fi From 9eab9d1e95dc3da811979e7c3c2bad623cd03208 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 24 Sep 2019 16:51:59 +0530 Subject: [PATCH 0371/3170] Drop lint-py2 and mypy from GitHub Actions CI --- .github/workflows/python-linters.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/python-linters.yml b/.github/workflows/python-linters.yml index 0f237e90ced..5a842a619a3 100644 --- a/.github/workflows/python-linters.yml +++ b/.github/workflows/python-linters.yml @@ -20,9 +20,6 @@ jobs: env: - TOXENV: docs - TOXENV: lint - - TOXENV: lint-py2 - PYTHON_VERSION: 2.7 - - TOXENV: mypy - TOXENV: packaging steps: - uses: actions/checkout@master From d0df9d375f78379ae2cd626979c77c72549d87b8 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 24 Sep 2019 17:02:13 +0530 Subject: [PATCH 0372/3170] Drop references to lint-py2 and mypy in documentation --- docs/html/development/getting-started.rst | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/docs/html/development/getting-started.rst b/docs/html/development/getting-started.rst index 3fbd11de4d7..d2baeaa87aa 100644 --- a/docs/html/development/getting-started.rst +++ b/docs/html/development/getting-started.rst @@ -76,26 +76,6 @@ To use linters locally, run: .. code-block:: console $ tox -e lint - $ tox -e lint-py2 - -The above commands run the linters on Python 3 followed by Python 2. - -.. note:: - - Do not silence errors from flake8 with ``# noqa`` comments or otherwise. - -Running mypy ------------- - -pip uses :pypi:`mypy` to run static type analysis, which helps catch certain -kinds of bugs. The codebase uses `PEP 484 type-comments`_ due to compatibility -requirements with Python 2.7. - -To run the ``mypy`` type checker, run: - -.. code-block:: console - - $ tox -e mypy Building Documentation ---------------------- From 96fd7963344a5fe15651ff8d8bc765c2359e722e Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 24 Sep 2019 16:30:44 +0530 Subject: [PATCH 0373/3170] Some hand-holding to tell folks to install Python --- docs/html/development/getting-started.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/html/development/getting-started.rst b/docs/html/development/getting-started.rst index 3fbd11de4d7..164a6c10369 100644 --- a/docs/html/development/getting-started.rst +++ b/docs/html/development/getting-started.rst @@ -11,11 +11,11 @@ process, please `open an issue`_ about it on the issue tracker. Development Environment ----------------------- -pip uses :pypi:`tox` for testing against multiple different Python environments -and ensuring reproducible environments for linting and building documentation. +pip is a command line application written in Python. For developing pip, +you should `install Python`_ on your computer. -For developing pip, you need to install ``tox`` on your system. Often, you can -just do ``python -m pip install tox`` to install and use it. +For developing pip, you need to install :pypi:`tox`.Often, you can run +``python -m pip install tox`` to install and use it. Running pip From Source Tree ---------------------------- @@ -112,5 +112,6 @@ To build it locally, run: The built documentation can be found in the ``docs/build`` folder. .. _`open an issue`: https://github.com/pypa/pip/issues/new?title=Trouble+with+pip+development+environment +.. _`install Python`: https://realpython.com/installing-python/ .. _`PEP 484 type-comments`: https://www.python.org/dev/peps/pep-0484/#suggested-syntax-for-python-2-7-and-straddling-code .. _`rich CLI`: https://docs.pytest.org/en/latest/usage.html#specifying-tests-selecting-tests From 2bbbc0756ac5efc36399d74688757d0efa1461e7 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 24 Sep 2019 16:31:09 +0530 Subject: [PATCH 0374/3170] Oops! I'd missed a word. --- docs/html/development/getting-started.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/html/development/getting-started.rst b/docs/html/development/getting-started.rst index 164a6c10369..ba3266eb687 100644 --- a/docs/html/development/getting-started.rst +++ b/docs/html/development/getting-started.rst @@ -42,7 +42,7 @@ To run tests locally, run: The example above runs tests against Python 3.6. You can also use other versions like ``py27`` and ``pypy3``. -``tox`` has been configured to any additional arguments it is given to +``tox`` has been configured to forward any additional arguments it is given to ``pytest``. This enables the use of pytest's `rich CLI`_. As an example, you can select tests using the various ways that pytest provides: From 3b0f2c32ddcc2bbf5902d24fd5c4d7b4e1f5cd25 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 24 Sep 2019 16:31:58 +0530 Subject: [PATCH 0375/3170] Make the role of tox, in test running, clearer --- docs/html/development/getting-started.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/html/development/getting-started.rst b/docs/html/development/getting-started.rst index ba3266eb687..bccf1fd5d72 100644 --- a/docs/html/development/getting-started.rst +++ b/docs/html/development/getting-started.rst @@ -30,8 +30,9 @@ from the ``src`` directory: Running Tests ------------- -pip uses the :pypi:`pytest` test framework, :pypi:`mock` and :pypi:`pretend` -for testing. These are automatically installed by tox for running the tests. +pip's tests are written using the :pypi:`pytest` test framework, :pypi:`mock` +and :pypi:`pretend`. :pypi:`tox` is used to automate the setup and execution of +pip's tests. To run tests locally, run: From 7b005c2d7d8323331490351559cbbeee7c1b6894 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 24 Sep 2019 16:52:47 +0530 Subject: [PATCH 0376/3170] Switch to using pre-commit in `tox lint` --- tools/requirements/lint.txt | 2 -- tox.ini | 9 ++------- 2 files changed, 2 insertions(+), 9 deletions(-) delete mode 100644 tools/requirements/lint.txt diff --git a/tools/requirements/lint.txt b/tools/requirements/lint.txt deleted file mode 100644 index b437a2c4cb4..00000000000 --- a/tools/requirements/lint.txt +++ /dev/null @@ -1,2 +0,0 @@ -flake8 == 3.7.6 -isort == 4.3.4 diff --git a/tox.ini b/tox.ini index 1fb195b1104..2dd661ad168 100644 --- a/tox.ini +++ b/tox.ini @@ -50,14 +50,9 @@ commands = check-manifest python setup.py check -m -r -s -[lint] -deps = -r{toxinidir}/tools/requirements/lint.txt - [testenv:lint] skip_install = True -basepython = python3 -deps = {[lint]deps} commands_pre = +deps = pre-commit commands = - flake8 - isort --check-only --diff + pre-commit run --all-files --show-diff-on-failure From 0a25c7d4a62ba70762942031f5700e84e2b15281 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 24 Sep 2019 17:01:45 +0530 Subject: [PATCH 0377/3170] Document that pre-commit is used for linting --- docs/html/development/getting-started.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/html/development/getting-started.rst b/docs/html/development/getting-started.rst index d2baeaa87aa..11925eab6d5 100644 --- a/docs/html/development/getting-started.rst +++ b/docs/html/development/getting-started.rst @@ -67,9 +67,9 @@ tools, you can tell pip to skip those tests: Running Linters --------------- -pip uses :pypi:`flake8` and :pypi:`isort` for linting the codebase. These -ensure that the codebase is in compliance with :pep:`8` and the imports are -consistently ordered and styled. +pip uses :pypi:`pre-commit` for managing linting of the codebase. +``pre-commit`` performs various checks on all files in pip and uses tools that +help follow a consistent code style within the codebase. To use linters locally, run: From 40a0f189b90f0ca865e8ea34288672245c32da75 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 24 Sep 2019 17:15:19 +0530 Subject: [PATCH 0378/3170] Enable check-builtin-literals --- .pre-commit-config.yaml | 2 +- src/pip/_internal/index.py | 6 +++--- src/pip/_internal/operations/check.py | 4 ++-- tests/conftest.py | 8 ++++---- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 453036e37ed..abec3c89f60 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,7 +4,7 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v2.3.0 hooks: - # - id: check-builtin-literals # TODO: enable when fixed. + - id: check-builtin-literals - id: check-added-large-files - id: check-case-conflict - id: check-toml diff --git a/src/pip/_internal/index.py b/src/pip/_internal/index.py index 4db14eb1aa8..81bdd4ad546 100644 --- a/src/pip/_internal/index.py +++ b/src/pip/_internal/index.py @@ -34,7 +34,7 @@ if MYPY_CHECK_RUNNING: from typing import ( - Any, FrozenSet, Iterable, List, Optional, Set, Text, Tuple, + FrozenSet, Iterable, List, Optional, Set, Text, Tuple, Union, ) from pip._vendor.packaging.version import _BaseVersion from pip._internal.collector import LinkCollector @@ -43,7 +43,7 @@ from pip._internal.pep425tags import Pep425Tag from pip._internal.utils.hashes import Hashes - BuildTag = Tuple[Any, ...] # either empty tuple or Tuple[int, str] + BuildTag = Union[Tuple[()], Tuple[int, str]] CandidateSortingKey = ( Tuple[int, int, int, _BaseVersion, BuildTag, Optional[int]] ) @@ -511,7 +511,7 @@ def _sort_key(self, candidate): """ valid_tags = self._supported_tags support_num = len(valid_tags) - build_tag = tuple() # type: BuildTag + build_tag = () # type: BuildTag binary_preference = 0 link = candidate.link if link.is_wheel: diff --git a/src/pip/_internal/operations/check.py b/src/pip/_internal/operations/check.py index 3bc9f8107ea..9f7fe6a7f61 100644 --- a/src/pip/_internal/operations/check.py +++ b/src/pip/_internal/operations/check.py @@ -68,8 +68,8 @@ def check_package_set(package_set, should_ignore=None): def should_ignore(name): return False - missing = dict() - conflicting = dict() + missing = {} + conflicting = {} for package_name in package_set: # Info about dependencies of package_name diff --git a/tests/conftest.py b/tests/conftest.py index 5982ebf9014..e29f03e591d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -181,10 +181,10 @@ def not_code_files_and_folders(path, names): return to_ignore # Ignore all compiled files and egg-info. - ignored = list() - for pattern in ["__pycache__", "*.pyc", "pip.egg-info"]: - ignored.extend(fnmatch.filter(names, pattern)) - return set(ignored) + ignored = set() + for pattern in ("__pycache__", "*.pyc", "pip.egg-info"): + ignored.update(fnmatch.filter(names, pattern)) + return ignored pip_src = Path(str(tmpdir_factory.mktemp('pip_src'))).joinpath('pip_src') # Copy over our source tree so that each use is self contained From 6f8c29aa77e1993236c7db7ec942a446979009e2 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 24 Sep 2019 17:17:01 +0530 Subject: [PATCH 0379/3170] Enable end-of-file-fixer --- .azure-pipelines/steps/run-tests.yml | 2 +- .pre-commit-config.yaml | 3 ++- docs/html/development/architecture/overview.rst | 1 - docs/html/logic.rst | 1 - docs/html/reference/pip.rst | 1 - docs/html/reference/pip_uninstall.rst | 1 - news/6869.trivial | 2 +- news/6883.trivial | 2 +- news/6892.bugfix | 2 +- tests/data/indexes/datarequire/fakepackage/index.html | 1 - tests/data/src/prjwithdatafile/prjwithdatafile/README.txt | 2 +- tests/data/src/sample/data/data_file | 2 +- tests/data/src/sample/sample/package_data.dat | 2 +- 13 files changed, 9 insertions(+), 13 deletions(-) diff --git a/.azure-pipelines/steps/run-tests.yml b/.azure-pipelines/steps/run-tests.yml index 95e7b388f67..2682e085fe7 100644 --- a/.azure-pipelines/steps/run-tests.yml +++ b/.azure-pipelines/steps/run-tests.yml @@ -22,4 +22,4 @@ steps: inputs: testResultsFiles: junit/*.xml testRunTitle: 'Python $(python.version)' - condition: succeededOrFailed() \ No newline at end of file + condition: succeededOrFailed() diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index abec3c89f60..69faaee6d73 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -10,7 +10,8 @@ repos: - id: check-toml - id: check-yaml - id: debug-statements - # - id: end-of-file-fixer # TODO: enable when fixed. + - id: end-of-file-fixer + exclude: WHEEL - id: flake8 exclude: tests/data - id: forbid-new-submodules diff --git a/docs/html/development/architecture/overview.rst b/docs/html/development/architecture/overview.rst index bd0e7ece402..c83600b8b35 100644 --- a/docs/html/development/architecture/overview.rst +++ b/docs/html/development/architecture/overview.rst @@ -140,4 +140,3 @@ files on PyPI. It’s for getting all files of Flask.) .. _`tracking issue`: https://github.com/pypa/pip/issues/6831 .. _PyPI: https://pypi.org/ .. _PyPI Simple API: https://warehouse.readthedocs.io/api-reference/legacy/#simple-project-api - diff --git a/docs/html/logic.rst b/docs/html/logic.rst index 0c27d8ce5a4..79092629267 100644 --- a/docs/html/logic.rst +++ b/docs/html/logic.rst @@ -5,4 +5,3 @@ Internal Details ================ This content is now covered in the :doc:`Reference Guide <reference/index>` - diff --git a/docs/html/reference/pip.rst b/docs/html/reference/pip.rst index f7f1d3b2af2..62b60926752 100644 --- a/docs/html/reference/pip.rst +++ b/docs/html/reference/pip.rst @@ -234,4 +234,3 @@ General Options *************** .. pip-general-options:: - diff --git a/docs/html/reference/pip_uninstall.rst b/docs/html/reference/pip_uninstall.rst index f9a97589eb5..28f1bcd2414 100644 --- a/docs/html/reference/pip_uninstall.rst +++ b/docs/html/reference/pip_uninstall.rst @@ -34,4 +34,3 @@ Examples /home/me/env/lib/python2.7/site-packages/simplejson-2.2.1-py2.7.egg-info Proceed (y/n)? y Successfully uninstalled simplejson - diff --git a/news/6869.trivial b/news/6869.trivial index 1da3453fb4f..25d8bd6160e 100644 --- a/news/6869.trivial +++ b/news/6869.trivial @@ -1 +1 @@ -Clarify WheelBuilder.build() a bit \ No newline at end of file +Clarify WheelBuilder.build() a bit diff --git a/news/6883.trivial b/news/6883.trivial index e6731cdbef6..8d132ac30cd 100644 --- a/news/6883.trivial +++ b/news/6883.trivial @@ -1 +1 @@ -replace is_vcs_url function by is_vcs Link property \ No newline at end of file +replace is_vcs_url function by is_vcs Link property diff --git a/news/6892.bugfix b/news/6892.bugfix index 763f2520b47..3aaf7712495 100644 --- a/news/6892.bugfix +++ b/news/6892.bugfix @@ -1,2 +1,2 @@ Correctly uninstall symlinks that were installed in a virtualenv, -by tools such as ``flit install --symlink``. \ No newline at end of file +by tools such as ``flit install --symlink``. diff --git a/tests/data/indexes/datarequire/fakepackage/index.html b/tests/data/indexes/datarequire/fakepackage/index.html index 8f61127bc28..0ca8b9dc3a2 100644 --- a/tests/data/indexes/datarequire/fakepackage/index.html +++ b/tests/data/indexes/datarequire/fakepackage/index.html @@ -5,4 +5,3 @@ <a data-requires-python='>=3.3' href="/fakepackage-3.3.0.tar.gz#md5=00000000000000000000000000000000" rel="internal">fakepackage-3.3.0.tar.gz</a><br/> <a data-requires-python='><X.y.z' href="/fakepackage-9.9.9.tar.gz#md5=00000000000000000000000000000000" rel="internal">fakepackage-9.9.9.tar.gz</a><br/> </body></html> - diff --git a/tests/data/src/prjwithdatafile/prjwithdatafile/README.txt b/tests/data/src/prjwithdatafile/prjwithdatafile/README.txt index e8d92145675..1660ce36438 100755 --- a/tests/data/src/prjwithdatafile/prjwithdatafile/README.txt +++ b/tests/data/src/prjwithdatafile/prjwithdatafile/README.txt @@ -1,4 +1,4 @@ README ====== -Test project file \ No newline at end of file +Test project file diff --git a/tests/data/src/sample/data/data_file b/tests/data/src/sample/data/data_file index 7c0646bfd53..426863280ee 100644 --- a/tests/data/src/sample/data/data_file +++ b/tests/data/src/sample/data/data_file @@ -1 +1 @@ -some data \ No newline at end of file +some data diff --git a/tests/data/src/sample/sample/package_data.dat b/tests/data/src/sample/sample/package_data.dat index 7c0646bfd53..426863280ee 100644 --- a/tests/data/src/sample/sample/package_data.dat +++ b/tests/data/src/sample/sample/package_data.dat @@ -1 +1 @@ -some data \ No newline at end of file +some data From 7224a1155c2eff832310c16689490dcd37aca663 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 24 Sep 2019 17:17:40 +0530 Subject: [PATCH 0380/3170] Enable trailing-whitespace --- .pre-commit-config.yaml | 4 ++-- docs/html/development/architecture/anatomy.rst | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 69faaee6d73..857f57e2648 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -15,8 +15,8 @@ repos: - id: flake8 exclude: tests/data - id: forbid-new-submodules - # - id: trailing-whitespace # TODO: enable when fixed. - # exclude: .patch + - id: trailing-whitespace + exclude: .patch # TODO: enable when fixed. # - repo: https://github.com/timothycrosley/isort diff --git a/docs/html/development/architecture/anatomy.rst b/docs/html/development/architecture/anatomy.rst index a9e7ad6b5e4..25593600c5e 100644 --- a/docs/html/development/architecture/anatomy.rst +++ b/docs/html/development/architecture/anatomy.rst @@ -39,7 +39,7 @@ The ``README``, license, ``pyproject.toml``, ``setup.py``, and so on are in the * ``news/`` *[pip stores news fragments… Every time pip makes a user-facing change, a file is added to this directory (usually a short note referring to a GitHub issue) with the right extension & name so it gets included in release notes…. So every release the maintainers will be deleting old files in this directory? Yes - we use the towncrier automation to generate a NEWS file, and auto-delete old stuff. There’s more about this in the contributor documentation!]* - * ``template.rst`` *[template for release notes -- this is a file towncrier uses…. Is this jinja? I don’t know, check towncrier docs]* + * ``template.rst`` *[template for release notes -- this is a file towncrier uses…. Is this jinja? I don’t know, check towncrier docs]* * ``src/`` *[source; see below]* * ``tasks/`` *[invoke is a PyPI library which uses files in this directory to define automation commands that are used in pip’s development processes -- not discussing further right now. For instance, automating the release.]* From ea68afab939cf38055520bd83c291662c5c498a7 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 24 Sep 2019 17:19:29 +0530 Subject: [PATCH 0381/3170] Enable isort in pre-commit --- .pre-commit-config.yaml | 11 +++++------ src/pip/_internal/commands/__init__.py | 2 +- src/pip/_internal/distributions/__init__.py | 1 - src/pip/_internal/req/__init__.py | 7 ++++--- src/pip/_internal/vcs/__init__.py | 9 ++++++--- tasks/__init__.py | 1 - tests/data/src/pep517_setup_and_pyproject/setup.py | 1 + tests/data/src/pep517_setup_only/setup.py | 1 + .../src/pep518_with_namespace_package-1.0/setup.py | 1 - tests/data/src/simple_namespace/setup.py | 1 - tests/lib/__init__.py | 10 +++++----- tools/automation/vendoring/__init__.py | 2 +- 12 files changed, 24 insertions(+), 23 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 857f57e2648..edcc0c5f0e9 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -18,12 +18,11 @@ repos: - id: trailing-whitespace exclude: .patch -# TODO: enable when fixed. -# - repo: https://github.com/timothycrosley/isort -# rev: 4.3.21 -# hooks: -# - id: isort -# files: \.py$ +- repo: https://github.com/timothycrosley/isort + rev: 4.3.21 + hooks: + - id: isort + files: \.py$ - repo: https://github.com/pre-commit/mirrors-mypy rev: v0.720 diff --git a/src/pip/_internal/commands/__init__.py b/src/pip/_internal/commands/__init__.py index a5d454192d6..11d2a14c61d 100644 --- a/src/pip/_internal/commands/__init__.py +++ b/src/pip/_internal/commands/__init__.py @@ -4,7 +4,7 @@ from __future__ import absolute_import import importlib -from collections import namedtuple, OrderedDict +from collections import OrderedDict, namedtuple from pip._internal.utils.typing import MYPY_CHECK_RUNNING diff --git a/src/pip/_internal/distributions/__init__.py b/src/pip/_internal/distributions/__init__.py index cdb9ed9f5f5..bba02f26cd6 100644 --- a/src/pip/_internal/distributions/__init__.py +++ b/src/pip/_internal/distributions/__init__.py @@ -1,6 +1,5 @@ from pip._internal.distributions.source.legacy import SourceDistribution from pip._internal.distributions.wheel import WheelDistribution - from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: diff --git a/src/pip/_internal/req/__init__.py b/src/pip/_internal/req/__init__.py index 9955a716ce2..993f23a238a 100644 --- a/src/pip/_internal/req/__init__.py +++ b/src/pip/_internal/req/__init__.py @@ -5,12 +5,13 @@ import logging -from .req_install import InstallRequirement -from .req_set import RequirementSet -from .req_file import parse_requirements from pip._internal.utils.logging import indent_log from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from .req_file import parse_requirements +from .req_install import InstallRequirement +from .req_set import RequirementSet + if MYPY_CHECK_RUNNING: from typing import Any, List, Sequence diff --git a/src/pip/_internal/vcs/__init__.py b/src/pip/_internal/vcs/__init__.py index 75b5589c53d..2a4eb137576 100644 --- a/src/pip/_internal/vcs/__init__.py +++ b/src/pip/_internal/vcs/__init__.py @@ -2,11 +2,14 @@ # the vcs package don't need to import deeper than `pip._internal.vcs`. # (The test directory and imports protected by MYPY_CHECK_RUNNING may # still need to import from a vcs sub-package.) -from pip._internal.vcs.versioncontrol import ( # noqa: F401 - RemoteNotFoundError, is_url, make_vcs_requirement_url, vcs, -) # Import all vcs modules to register each VCS in the VcsSupport object. import pip._internal.vcs.bazaar import pip._internal.vcs.git import pip._internal.vcs.mercurial import pip._internal.vcs.subversion # noqa: F401 +from pip._internal.vcs.versioncontrol import ( # noqa: F401 + RemoteNotFoundError, + is_url, + make_vcs_requirement_url, + vcs, +) diff --git a/tasks/__init__.py b/tasks/__init__.py index 6427f603726..9591fb9ef05 100644 --- a/tasks/__init__.py +++ b/tasks/__init__.py @@ -1,5 +1,4 @@ import invoke - from tools.automation import vendoring ns = invoke.Collection(vendoring) diff --git a/tests/data/src/pep517_setup_and_pyproject/setup.py b/tests/data/src/pep517_setup_and_pyproject/setup.py index 3aac3034b67..7a211f90dbc 100644 --- a/tests/data/src/pep517_setup_and_pyproject/setup.py +++ b/tests/data/src/pep517_setup_and_pyproject/setup.py @@ -1,2 +1,3 @@ from setuptools import setup + setup(name="dummy", version="0.1") diff --git a/tests/data/src/pep517_setup_only/setup.py b/tests/data/src/pep517_setup_only/setup.py index 3aac3034b67..7a211f90dbc 100644 --- a/tests/data/src/pep517_setup_only/setup.py +++ b/tests/data/src/pep517_setup_only/setup.py @@ -1,2 +1,3 @@ from setuptools import setup + setup(name="dummy", version="0.1") diff --git a/tests/data/src/pep518_with_namespace_package-1.0/setup.py b/tests/data/src/pep518_with_namespace_package-1.0/setup.py index 7e58d0de60a..540ede4cf43 100644 --- a/tests/data/src/pep518_with_namespace_package-1.0/setup.py +++ b/tests/data/src/pep518_with_namespace_package-1.0/setup.py @@ -2,7 +2,6 @@ import simple_namespace.module - setup( name='pep518_with_namespace_package', version='1.0', diff --git a/tests/data/src/simple_namespace/setup.py b/tests/data/src/simple_namespace/setup.py index 20ee0044b7c..9a49d52b757 100644 --- a/tests/data/src/simple_namespace/setup.py +++ b/tests/data/src/simple_namespace/setup.py @@ -1,6 +1,5 @@ from setuptools import setup - setup( name='simple_namespace', version='1.0', diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index a1ddd23f1d0..7e59911b72e 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -1,14 +1,14 @@ from __future__ import absolute_import -from contextlib import contextmanager -from textwrap import dedent import os -import sys import re -import textwrap -import site import shutil +import site import subprocess +import sys +import textwrap +from contextlib import contextmanager +from textwrap import dedent import pytest from scripttest import FoundDir, TestFileEnvironment diff --git a/tools/automation/vendoring/__init__.py b/tools/automation/vendoring/__init__.py index 052be8efc69..3f0278838d2 100644 --- a/tools/automation/vendoring/__init__.py +++ b/tools/automation/vendoring/__init__.py @@ -1,11 +1,11 @@ """"Vendoring script, python 3.5 with requests needed""" -from pathlib import Path import os import re import shutil import tarfile import zipfile +from pathlib import Path import invoke import requests From c47e1b6e5d508d60349718907b81664e14b4f514 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 24 Sep 2019 17:31:33 +0530 Subject: [PATCH 0382/3170] Enable rst-backticks for ReST files and NEWS fragments --- .pre-commit-config.yaml | 7 ++++++- docs/html/installing.rst | 2 +- docs/html/reference/pip_install.rst | 6 +++--- docs/html/user_guide.rst | 28 +++++++++++++------------- news/6991.bugfix | 2 +- news/fix-test-pep518-forkbombs.trivial | 2 +- 6 files changed, 26 insertions(+), 21 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index edcc0c5f0e9..8b52992d027 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -35,4 +35,9 @@ repos: hooks: - id: python-no-log-warn - id: python-no-eval - # - id: rst-backticks # TODO: enable when fixed. + - id: rst-backticks + # Validate existing ReST files and NEWS fragments. + files: .*\.rst$|^news/.* + types: [file] + # The errors flagged in NEWS.rst are old. + exclude: NEWS.rst diff --git a/docs/html/installing.rst b/docs/html/installing.rst index c15a62721fb..6b50f929bc9 100644 --- a/docs/html/installing.rst +++ b/docs/html/installing.rst @@ -120,7 +120,7 @@ pip works on Unix/Linux, macOS, and Windows. ---- .. [1] "Secure" in this context means using a modern browser or a - tool like `curl` that verifies SSL certificates when downloading from + tool like ``curl`` that verifies SSL certificates when downloading from https URLs. .. [2] Beginning with pip v1.5.1, ``get-pip.py`` stopped requiring setuptools to diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index fa475c462c8..c23b051daaf 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -572,7 +572,7 @@ each sdist that wheels are built from and places the resulting wheels inside. Pip attempts to choose the best wheels from those built in preference to building a new wheel. Note that this means when a package has both optional -C extensions and builds `py` tagged wheels when the C extension can't be built +C extensions and builds ``py`` tagged wheels when the C extension can't be built that pip will not attempt to build a better wheel for Pythons that would have supported it, once any generic wheel is built. To correct this, make sure that the wheels are built with Python specific tags - e.g. pp on PyPy. @@ -826,7 +826,7 @@ Options Examples ******** -#. Install `SomePackage` and its dependencies from `PyPI`_ using :ref:`Requirement Specifiers` +#. Install ``SomePackage`` and its dependencies from `PyPI`_ using :ref:`Requirement Specifiers` :: @@ -842,7 +842,7 @@ Examples $ pip install -r requirements.txt -#. Upgrade an already installed `SomePackage` to the latest from PyPI. +#. Upgrade an already installed ``SomePackage`` to the latest from PyPI. :: diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 9b7e7ed4a0f..c0d09fb4db9 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -90,7 +90,7 @@ In practice, there are 4 common uses of Requirements files: 1. Requirements files are used to hold the result from :ref:`pip freeze` for the purpose of achieving :ref:`repeatable installations <Repeatability>`. In this case, your requirement file contains a pinned version of everything that - was installed when `pip freeze` was run. + was installed when ``pip freeze`` was run. :: @@ -100,11 +100,11 @@ In practice, there are 4 common uses of Requirements files: 2. Requirements files are used to force pip to properly resolve dependencies. As it is now, pip `doesn't have true dependency resolution <https://github.com/pypa/pip/issues/988>`_, but instead simply uses the first - specification it finds for a project. E.g. if `pkg1` requires `pkg3>=1.0` and - `pkg2` requires `pkg3>=1.0,<=2.0`, and if `pkg1` is resolved first, pip will - only use `pkg3>=1.0`, and could easily end up installing a version of `pkg3` - that conflicts with the needs of `pkg2`. To solve this problem, you can - place `pkg3>=1.0,<=2.0` (i.e. the correct specification) into your + specification it finds for a project. E.g. if ``pkg1`` requires ``pkg3>=1.0`` and + ``pkg2`` requires ``pkg3>=1.0,<=2.0``, and if ``pkg1`` is resolved first, pip will + only use ``pkg3>=1.0``, and could easily end up installing a version of ``pkg3`` + that conflicts with the needs of ``pkg2``. To solve this problem, you can + place ``pkg3>=1.0,<=2.0`` (i.e. the correct specification) into your requirements file directly along with the other top level requirements. Like so: @@ -115,8 +115,8 @@ In practice, there are 4 common uses of Requirements files: pkg3>=1.0,<=2.0 3. Requirements files are used to force pip to install an alternate version of a - sub-dependency. For example, suppose `ProjectA` in your requirements file - requires `ProjectB`, but the latest version (v1.3) has a bug, you can force + sub-dependency. For example, suppose ``ProjectA`` in your requirements file + requires ``ProjectB``, but the latest version (v1.3) has a bug, you can force pip to accept earlier versions like so: :: @@ -126,23 +126,23 @@ In practice, there are 4 common uses of Requirements files: 4. Requirements files are used to override a dependency with a local patch that lives in version control. For example, suppose a dependency, - `SomeDependency` from PyPI has a bug, and you can't wait for an upstream fix. + ``SomeDependency`` from PyPI has a bug, and you can't wait for an upstream fix. You could clone/copy the src, make the fix, and place it in VCS with the tag - `sometag`. You'd reference it in your requirements file with a line like so: + ``sometag``. You'd reference it in your requirements file with a line like so: :: git+https://myvcs.com/some_dependency@sometag#egg=SomeDependency - If `SomeDependency` was previously a top-level requirement in your + If ``SomeDependency`` was previously a top-level requirement in your requirements file, then **replace** that line with the new line. If - `SomeDependency` is a sub-dependency, then **add** the new line. + ``SomeDependency`` is a sub-dependency, then **add** the new line. It's important to be clear that pip determines package dependencies using `install_requires metadata <https://setuptools.readthedocs.io/en/latest/setuptools.html#declaring-dependencies>`_, -not by discovering `requirements.txt` files embedded in projects. +not by discovering ``requirements.txt`` files embedded in projects. See also: @@ -374,7 +374,7 @@ look like this: Each subcommand can be configured optionally in its own section so that every global setting with the same name will be overridden; e.g. decreasing the -``timeout`` to ``10`` seconds when running the `freeze` +``timeout`` to ``10`` seconds when running the ``freeze`` (`Freezing Requirements <./#freezing-requirements>`_) command and using ``60`` seconds for all other commands is possible with: diff --git a/news/6991.bugfix b/news/6991.bugfix index db5904cdd42..c6bf963b901 100644 --- a/news/6991.bugfix +++ b/news/6991.bugfix @@ -1 +1 @@ - Ignore "require_virtualenv" in `pip config` +Ignore "require_virtualenv" in ``pip config`` diff --git a/news/fix-test-pep518-forkbombs.trivial b/news/fix-test-pep518-forkbombs.trivial index 13792675b3b..d88e8c20055 100644 --- a/news/fix-test-pep518-forkbombs.trivial +++ b/news/fix-test-pep518-forkbombs.trivial @@ -1 +1 @@ -Fix copy-paste issue in `test_pep518_forkbombs`. +Fix copy-paste issue in ``test_pep518_forkbombs``. From ff53e6d7d324d0b0d620b97a09cb639ce93bf514 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 24 Sep 2019 17:41:07 +0530 Subject: [PATCH 0383/3170] Drop dedicated CI job for NEWS fragment check Why: This task is now handled as a part of pre-commit's checks. --- .github/workflows/python-linters.yml | 14 -------------- noxfile.py | 13 ------------- 2 files changed, 27 deletions(-) diff --git a/.github/workflows/python-linters.yml b/.github/workflows/python-linters.yml index 5a842a619a3..e547acf1118 100644 --- a/.github/workflows/python-linters.yml +++ b/.github/workflows/python-linters.yml @@ -41,17 +41,3 @@ jobs: run: >- python -m tox env: ${{ matrix.env }} - - news_format: - name: Check NEWS format - runs-on: ubuntu-18.04 - steps: - - uses: actions/checkout@master - - name: Set up Python - uses: actions/setup-python@v1 - with: - version: 3.7 - - name: Install nox - run: pip install nox - - name: Check NEWS format - run: nox -s validate_news diff --git a/noxfile.py b/noxfile.py index 205c50e9f98..d6357b3dd7d 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1,7 +1,6 @@ """Release time helpers, executed using nox. """ -import glob import io import subprocess @@ -31,18 +30,6 @@ def get_author_list(): return sorted(authors, key=lambda x: x.lower()) -# ----------------------------------------------------------------------------- -# Ad-hoc commands -# ----------------------------------------------------------------------------- -@nox.session -def validate_news(session): - session.install("rstcheck") - - news_files = sorted(glob.glob("news/*")) - - session.run("rstcheck", *news_files) - - # ----------------------------------------------------------------------------- # Commands used during the release process # ----------------------------------------------------------------------------- From 9a362abd72b1b5fc9000918dbfb55101cee36b6b Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 24 Sep 2019 18:22:27 +0530 Subject: [PATCH 0384/3170] Exclude .pre-commit-config.yaml from distributions --- MANIFEST.in | 1 + 1 file changed, 1 insertion(+) diff --git a/MANIFEST.in b/MANIFEST.in index 5bf20b0d98d..e16ea0c73c2 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -16,6 +16,7 @@ exclude .mailmap exclude .appveyor.yml exclude .travis.yml exclude .readthedocs.yml +exclude .pre-commit-config.yaml exclude tox.ini exclude noxfile.py From 8296b1a17e96428b35797b107d23e67d7385f791 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 24 Sep 2019 18:34:25 +0530 Subject: [PATCH 0385/3170] Do twine check in Azure Pipeline's Package job --- .azure-pipelines/jobs/package.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.azure-pipelines/jobs/package.yml b/.azure-pipelines/jobs/package.yml index 2070bc5d42b..ed885d8ef43 100644 --- a/.azure-pipelines/jobs/package.yml +++ b/.azure-pipelines/jobs/package.yml @@ -15,7 +15,7 @@ jobs: inputs: versionSpec: '3' - - bash: pip install tox nox setuptools wheel + - bash: pip install twine nox setuptools wheel displayName: Install dependencies - bash: nox -s generate_authors @@ -30,6 +30,9 @@ jobs: - bash: python setup.py sdist bdist_wheel displayName: Create sdist and wheel + - bash: twine check dist/* + displayName: Check distributions with twine + - task: PublishBuildArtifacts@1 displayName: 'Publish Artifact: dist' inputs: From 033a01333cf453ac6ab8f32d0251efc0354ed389 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 24 Sep 2019 18:35:53 +0530 Subject: [PATCH 0386/3170] Drop tox -e packaging from CIs --- .azure-pipelines/jobs/package.yml | 3 --- .github/workflows/python-linters.yml | 1 - .travis.yml | 1 - tools/travis/run.sh | 2 +- 4 files changed, 1 insertion(+), 6 deletions(-) diff --git a/.azure-pipelines/jobs/package.yml b/.azure-pipelines/jobs/package.yml index ed885d8ef43..5ab5be31f92 100644 --- a/.azure-pipelines/jobs/package.yml +++ b/.azure-pipelines/jobs/package.yml @@ -24,9 +24,6 @@ jobs: - bash: nox -s generate_news -- --yes displayName: Generate NEWS.rst - - bash: tox -e packaging - displayName: Run Tox packaging - - bash: python setup.py sdist bdist_wheel displayName: Create sdist and wheel diff --git a/.github/workflows/python-linters.yml b/.github/workflows/python-linters.yml index e547acf1118..99c632166e4 100644 --- a/.github/workflows/python-linters.yml +++ b/.github/workflows/python-linters.yml @@ -20,7 +20,6 @@ jobs: env: - TOXENV: docs - TOXENV: lint - - TOXENV: packaging steps: - uses: actions/checkout@master - name: Set up Python ${{ matrix.env.PYTHON_VERSION || 3.7 }} diff --git a/.travis.yml b/.travis.yml index 4005a83dee3..5ea346dac87 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,7 +17,6 @@ jobs: - stage: primary env: TOXENV=docs - env: TOXENV=lint - - env: TOXENV=packaging # Latest CPython - env: GROUP=1 python: 2.7 diff --git a/tools/travis/run.sh b/tools/travis/run.sh index 3d64ec2bd8c..aea29349c5f 100755 --- a/tools/travis/run.sh +++ b/tools/travis/run.sh @@ -2,7 +2,7 @@ set -e # Short circuit test runs if there are no code changes involved. -if ! [[ $TOXENV ~= ^(docs|lint|packaging)$ ]]; then +if [[ $TOXENV != docs ]] || [[ $TOXENV != lint ]]; then if [[ "$TRAVIS_PULL_REQUEST" == "false" ]] then echo "This is not a PR -- will do a complete build." From f646349e1b76a247c776b2aaf0915251b63adba7 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 24 Sep 2019 18:36:17 +0530 Subject: [PATCH 0387/3170] Add pre-commit hook for check-manifest Why: This brings back functionality removed in prior commits --- .pre-commit-config.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8b52992d027..d032d09ac12 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -41,3 +41,8 @@ repos: types: [file] # The errors flagged in NEWS.rst are old. exclude: NEWS.rst + +- repo: https://github.com/mgedmin/check-manifest + rev: '0.39' + hooks: + - id: check-manifest From 45bc66ae1a839ef6e21cd9029ca60a4d1cb0179d Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 24 Sep 2019 22:13:14 +0530 Subject: [PATCH 0388/3170] Add a space I missed. --- docs/html/development/getting-started.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/html/development/getting-started.rst b/docs/html/development/getting-started.rst index bccf1fd5d72..44577900031 100644 --- a/docs/html/development/getting-started.rst +++ b/docs/html/development/getting-started.rst @@ -14,7 +14,7 @@ Development Environment pip is a command line application written in Python. For developing pip, you should `install Python`_ on your computer. -For developing pip, you need to install :pypi:`tox`.Often, you can run +For developing pip, you need to install :pypi:`tox`. Often, you can run ``python -m pip install tox`` to install and use it. Running pip From Source Tree From 1d1e67269e664bf4f746bb0343224f1810907598 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 24 Sep 2019 22:20:19 +0530 Subject: [PATCH 0389/3170] Add invocation of mypy -2, to pre-commit --- .pre-commit-config.yaml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d032d09ac12..9a052ee9365 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,6 +29,11 @@ repos: hooks: - id: mypy exclude: docs|tests + args: [] + - id: mypy + name: mypy, for Py2 + args: ["-2"] + exclude: docs|tests - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.4.1 From b2bae6264120ed09526b5e56df10bd24c8a34e58 Mon Sep 17 00:00:00 2001 From: tbeswick <tbeswick@enphaseenergy.com> Date: Wed, 25 Sep 2019 11:40:38 +1200 Subject: [PATCH 0390/3170] Fixed problem where vcs commands were being called even if they're not installed, causing errors to be logged. --- src/pip/_internal/vcs/git.py | 4 ++-- src/pip/_internal/vcs/mercurial.py | 4 ++-- src/pip/_internal/vcs/versioncontrol.py | 17 ++++++++++++----- 3 files changed, 16 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/vcs/git.py b/src/pip/_internal/vcs/git.py index 65069af7b7b..e92af2d0593 100644 --- a/src/pip/_internal/vcs/git.py +++ b/src/pip/_internal/vcs/git.py @@ -362,8 +362,8 @@ def update_submodules(cls, location): @classmethod def controls_location(cls, location): - if super(Git, cls).controls_location(location): - return True + if not super(Git, cls).controls_location(location): + return False try: r = cls.run_command(['rev-parse'], cwd=location, diff --git a/src/pip/_internal/vcs/mercurial.py b/src/pip/_internal/vcs/mercurial.py index 2b13a04cb0b..de412a612ab 100644 --- a/src/pip/_internal/vcs/mercurial.py +++ b/src/pip/_internal/vcs/mercurial.py @@ -141,8 +141,8 @@ def get_subdirectory(cls, location): @classmethod def controls_location(cls, location): - if super(Mercurial, cls).controls_location(location): - return True + if not super(Mercurial, cls).controls_location(location): + return False try: r = cls.run_command(['identify'], cwd=location, diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index 27610602f16..610fb5a4347 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -621,10 +621,17 @@ def controls_location(cls, location): # type: (str) -> bool """ Check if a location is controlled by the vcs. - It is meant to be overridden to implement smarter detection - mechanisms for specific vcs. - This can do more than is_repository_directory() alone. For example, - the Git override checks that Git is actually available. + Searches up the filesystem and checks is_repository_directory(). + + It is meant to be extended to add smarter detection mechanisms for + specific vcs. For example, the Git override checks that Git is + actually available. """ - return cls.is_repository_directory(location) + while not cls.is_repository_directory(location): + last_location = location + location = os.path.dirname(location) + if location == last_location: + # We've traversed up to the root of the filesystem. + return False + return True From 3e98ee8464ab7e5b63c602bc315c8dea4a63a554 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 24 Sep 2019 21:36:16 -0400 Subject: [PATCH 0391/3170] Use from ... import ... style --- tests/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 0e704f874fa..134eddd7705 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,7 +10,7 @@ import six from setuptools.wheel import Wheel -import pip._internal.main +from pip._internal.main import main as pip_entry_point from tests.lib import DATA_DIR, SRC_DIR, TestData from tests.lib.path import Path from tests.lib.scripttest import PipTestEnvironment @@ -342,7 +342,7 @@ def pip(self, *args): stdout = io.BytesIO() sys.stdout = stdout try: - returncode = pip._internal.main.main(list(args)) + returncode = pip_entry_point(list(args)) except SystemExit as e: returncode = e.code or 0 finally: From 08a0eeb90cf8bb96e3d3507a7d724e04fde72503 Mon Sep 17 00:00:00 2001 From: Wilson Mo <wilsonfv@126.com> Date: Sun, 18 Aug 2019 22:23:52 +0800 Subject: [PATCH 0392/3170] Fix #3907 tar file placed outside of target location --- news/3907.bugfix | 1 + src/pip/_internal/utils/unpacking.py | 24 +++++++ tests/unit/test_utils_unpacking.py | 101 ++++++++++++++++++++++++++- 3 files changed, 125 insertions(+), 1 deletion(-) create mode 100644 news/3907.bugfix diff --git a/news/3907.bugfix b/news/3907.bugfix new file mode 100644 index 00000000000..c61975e7bf1 --- /dev/null +++ b/news/3907.bugfix @@ -0,0 +1 @@ +Abort the installation process and raise an exception if one of the tar/zip file will be placed outside of target location causing security issue. \ No newline at end of file diff --git a/src/pip/_internal/utils/unpacking.py b/src/pip/_internal/utils/unpacking.py index dcfe5819e5b..ed66187ae10 100644 --- a/src/pip/_internal/utils/unpacking.py +++ b/src/pip/_internal/utils/unpacking.py @@ -85,6 +85,18 @@ def has_leading_dir(paths): return True +def is_within_directory(directory, target): + # type: ((Union[str, Text]), (Union[str, Text])) -> bool + """ + Return true if the absolute path of target is within the directory + """ + abs_directory = os.path.abspath(directory) + abs_target = os.path.abspath(target) + + prefix = os.path.commonprefix([abs_directory, abs_target]) + return prefix == abs_directory + + def unzip_file(filename, location, flatten=True): # type: (str, str, bool) -> None """ @@ -107,6 +119,12 @@ def unzip_file(filename, location, flatten=True): fn = split_leading_dir(name)[1] fn = os.path.join(location, fn) dir = os.path.dirname(fn) + if not is_within_directory(location, fn): + raise InstallationError( + 'The zip file (%s) has a file (%s) trying to install ' + 'outside target directory (%s)' % + (filename, fn, location) + ) if fn.endswith('/') or fn.endswith('\\'): # A directory ensure_dir(fn) @@ -166,6 +184,12 @@ def untar_file(filename, location): # https://github.com/python/mypy/issues/1174 fn = split_leading_dir(fn)[1] # type: ignore path = os.path.join(location, fn) + if not is_within_directory(location, path): + raise InstallationError( + 'The tar file (%s) has a file (%s) trying to install ' + 'outside target directory (%s)' % + (filename, path, location) + ) if member.isdir(): ensure_dir(path) elif member.issym(): diff --git a/tests/unit/test_utils_unpacking.py b/tests/unit/test_utils_unpacking.py index 96fcb99569f..40440fb7c27 100644 --- a/tests/unit/test_utils_unpacking.py +++ b/tests/unit/test_utils_unpacking.py @@ -2,10 +2,19 @@ import shutil import stat import sys +import tarfile import tempfile import time +import zipfile -from pip._internal.utils.unpacking import untar_file, unzip_file +import pytest + +from pip._internal.exceptions import InstallationError +from pip._internal.utils.unpacking import ( + is_within_directory, + untar_file, + unzip_file, +) class TestUnpackArchives(object): @@ -71,6 +80,27 @@ def confirm_files(self): "mode: %s, expected mode: %s" % (mode, expected_mode) ) + def make_zip_file(self, filename, file_list): + """ + Create a zip file for test case + """ + test_zip = os.path.join(self.tempdir, filename) + with zipfile.ZipFile(test_zip, 'w') as myzip: + for item in file_list: + myzip.writestr(item, 'file content') + return test_zip + + def make_tar_file(self, filename, file_list): + """ + Create a tar file for test case + """ + test_tar = os.path.join(self.tempdir, filename) + with tarfile.open(test_tar, 'w') as mytar: + for item in file_list: + file_tarinfo = tarfile.TarInfo(item) + mytar.addfile(file_tarinfo, 'file content') + return test_tar + def test_unpack_tgz(self, data): """ Test unpacking a *.tgz, and setting execute permissions @@ -90,3 +120,72 @@ def test_unpack_zip(self, data): test_file = data.packages.joinpath("test_zip.zip") unzip_file(test_file, self.tempdir) self.confirm_files() + + def test_unpack_zip_failure(self): + """ + Test unpacking a *.zip with file containing .. path + and expect exception + """ + test_zip = self.make_zip_file('test_zip.zip', + ['regular_file.txt', + os.path.join('..', 'outside_file.txt')]) + with pytest.raises( + InstallationError, + match=r'.*trying to install outside target directory.*'): + unzip_file(test_zip, self.tempdir) + + def test_unpack_zip_success(self): + """ + Test unpacking a *.zip with regular files, + no file will be installed outside target directory after unpack + so no exception raised + """ + test_zip = self.make_zip_file( + 'test_zip.zip', + ['regular_file1.txt', + os.path.join('dir', 'dir_file1.txt'), + os.path.join('dir', '..', 'dir_file2.txt')]) + unzip_file(test_zip, self.tempdir) + + def test_unpack_tar_failure(self): + """ + Test unpacking a *.tar with file containing .. path + and expect exception + """ + test_tar = self.make_tar_file('test_tar.tar', + ['regular_file.txt', + os.path.join('..', 'outside_file.txt')]) + with pytest.raises( + InstallationError, + match=r'.*trying to install outside target directory.*'): + untar_file(test_tar, self.tempdir) + + def test_unpack_tar_success(self): + """ + Test unpacking a *.tar with regular files, + no file will be installed outside target directory after unpack + so no exception raised + """ + test_tar = self.make_tar_file( + 'test_tar.tar', + ['regular_file1.txt', + os.path.join('dir', 'dir_file1.txt'), + os.path.join('dir', '..', 'dir_file2.txt')]) + untar_file(test_tar, self.tempdir) + + +@pytest.mark.parametrize('args, expected', [ + # Test the second containing the first. + (('parent/sub', 'parent/'), False), + # Test the first not ending in a trailing slash. + (('parent', 'parent/foo'), True), + # Test target containing `..` but still inside the parent. + (('parent/', 'parent/foo/../bar'), True), + # Test target within the parent + (('parent/', 'parent/sub'), True), + # Test target outside parent + (('parent/', 'parent/../sub'), False), +]) +def test_is_within_directory(args, expected): + result = is_within_directory(*args) + assert result == expected From 10beeea0b0f0e35e2dc0fb67684714471ce4119d Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Wed, 25 Sep 2019 07:57:59 +0530 Subject: [PATCH 0393/3170] Allow passing hook-ids to `tox -e lint` Why: To allow running a specific hook when running locally. --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 2dd661ad168..63360619051 100644 --- a/tox.ini +++ b/tox.ini @@ -55,4 +55,4 @@ skip_install = True commands_pre = deps = pre-commit commands = - pre-commit run --all-files --show-diff-on-failure + pre-commit run [] --all-files --show-diff-on-failure From 14cbc5f8a420b8ed6a8a40e5489eb0656ce18750 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 22 Sep 2019 13:08:51 +0530 Subject: [PATCH 0394/3170] Return early from the function Why: Allows reducing indentation of the function body. --- src/pip/_internal/req/req_install.py | 75 +++++++++++++++------------- 1 file changed, 39 insertions(+), 36 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 03fc0dc1369..7a49049db6a 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -630,49 +630,52 @@ def prepare_pep517_metadata(self): @property def egg_info_path(self): # type: () -> str + if self._egg_info_path is not None: + return self._egg_info_path + def looks_like_virtual_env(path): return ( os.path.lexists(os.path.join(path, 'bin', 'python')) or os.path.exists(os.path.join(path, 'Scripts', 'Python.exe')) ) - if self._egg_info_path is None: - if self.editable: - base = self.source_dir - filenames = [] - for root, dirs, files in os.walk(base): - for dir in vcs.dirnames: - if dir in dirs: - dirs.remove(dir) - # Iterate over a copy of ``dirs``, since mutating - # a list while iterating over it can cause trouble. - # (See https://github.com/pypa/pip/pull/462.) - for dir in list(dirs): - if looks_like_virtual_env(os.path.join(root, dir)): - dirs.remove(dir) - # Also don't search through tests - elif dir == 'test' or dir == 'tests': - dirs.remove(dir) - filenames.extend([os.path.join(root, dir) - for dir in dirs]) - filenames = [f for f in filenames if f.endswith('.egg-info')] - else: - base = os.path.join(self.setup_py_dir, 'pip-egg-info') - filenames = os.listdir(base) + if self.editable: + base = self.source_dir + filenames = [] + for root, dirs, files in os.walk(base): + for dir in vcs.dirnames: + if dir in dirs: + dirs.remove(dir) + # Iterate over a copy of ``dirs``, since mutating + # a list while iterating over it can cause trouble. + # (See https://github.com/pypa/pip/pull/462.) + for dir in list(dirs): + if looks_like_virtual_env(os.path.join(root, dir)): + dirs.remove(dir) + # Also don't search through tests + elif dir == 'test' or dir == 'tests': + dirs.remove(dir) + filenames.extend([os.path.join(root, dir) + for dir in dirs]) + filenames = [f for f in filenames if f.endswith('.egg-info')] + else: + base = os.path.join(self.setup_py_dir, 'pip-egg-info') + filenames = os.listdir(base) + + if not filenames: + raise InstallationError( + "Files/directories not found in %s" % base + ) + # if we have more than one match, we pick the toplevel one. This + # can easily be the case if there is a dist folder which contains + # an extracted tarball for testing purposes. + if len(filenames) > 1: + filenames.sort( + key=lambda x: x.count(os.path.sep) + + (os.path.altsep and x.count(os.path.altsep) or 0) + ) + self._egg_info_path = os.path.join(base, filenames[0]) - if not filenames: - raise InstallationError( - "Files/directories not found in %s" % base - ) - # if we have more than one match, we pick the toplevel one. This - # can easily be the case if there is a dist folder which contains - # an extracted tarball for testing purposes. - if len(filenames) > 1: - filenames.sort( - key=lambda x: x.count(os.path.sep) + - (os.path.altsep and x.count(os.path.altsep) or 0) - ) - self._egg_info_path = os.path.join(base, filenames[0]) return self._egg_info_path @property From 72d5d140be51d2502f0e791548c66515c069efa1 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 22 Sep 2019 13:14:17 +0530 Subject: [PATCH 0395/3170] Define a function to use as sorting key Why: It makes it clearer what the funky expression means. --- src/pip/_internal/req/req_install.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 7a49049db6a..ee6ccfec749 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -639,6 +639,12 @@ def looks_like_virtual_env(path): os.path.exists(os.path.join(path, 'Scripts', 'Python.exe')) ) + def depth_of_directory(dir_): + return ( + dir_.count(os.path.sep) + + (os.path.altsep and dir_.count(os.path.altsep) or 0) + ) + if self.editable: base = self.source_dir filenames = [] @@ -666,16 +672,14 @@ def looks_like_virtual_env(path): raise InstallationError( "Files/directories not found in %s" % base ) - # if we have more than one match, we pick the toplevel one. This + + # If we have more than one match, we pick the toplevel one. This # can easily be the case if there is a dist folder which contains # an extracted tarball for testing purposes. if len(filenames) > 1: - filenames.sort( - key=lambda x: x.count(os.path.sep) + - (os.path.altsep and x.count(os.path.altsep) or 0) - ) - self._egg_info_path = os.path.join(base, filenames[0]) + filenames.sort(key=depth_of_directory) + self._egg_info_path = os.path.join(base, filenames[0]) return self._egg_info_path @property From 618714b1bf1ad26d1181844bed59150f43c3dc61 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 22 Sep 2019 13:18:25 +0530 Subject: [PATCH 0396/3170] Factor out logic for locating egg-info directories Why: A named function brings clarity to expressing what is happening. --- src/pip/_internal/req/req_install.py | 36 +++++++++++++++------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index ee6ccfec749..b734d2db863 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -639,6 +639,24 @@ def looks_like_virtual_env(path): os.path.exists(os.path.join(path, 'Scripts', 'Python.exe')) ) + def locate_editable_egg_info(base): + candidates = [] + for root, dirs, files in os.walk(base): + for dir_ in vcs.dirnames: + if dir_ in dirs: + dirs.remove(dir_) + # Iterate over a copy of ``dirs``, since mutating + # a list while iterating over it can cause trouble. + # (See https://github.com/pypa/pip/pull/462.) + for dir_ in list(dirs): + if looks_like_virtual_env(os.path.join(root, dir_)): + dirs.remove(dir_) + # Also don't search through tests + elif dir_ == 'test' or dir_ == 'tests': + dirs.remove(dir_) + candidates.extend(os.path.join(root, dir_) for dir_ in dirs) + return [f for f in candidates if f.endswith('.egg-info')] + def depth_of_directory(dir_): return ( dir_.count(os.path.sep) + @@ -647,23 +665,7 @@ def depth_of_directory(dir_): if self.editable: base = self.source_dir - filenames = [] - for root, dirs, files in os.walk(base): - for dir in vcs.dirnames: - if dir in dirs: - dirs.remove(dir) - # Iterate over a copy of ``dirs``, since mutating - # a list while iterating over it can cause trouble. - # (See https://github.com/pypa/pip/pull/462.) - for dir in list(dirs): - if looks_like_virtual_env(os.path.join(root, dir)): - dirs.remove(dir) - # Also don't search through tests - elif dir == 'test' or dir == 'tests': - dirs.remove(dir) - filenames.extend([os.path.join(root, dir) - for dir in dirs]) - filenames = [f for f in filenames if f.endswith('.egg-info')] + filenames = locate_editable_egg_info(base) else: base = os.path.join(self.setup_py_dir, 'pip-egg-info') filenames = os.listdir(base) From 8c94b703540de03a42394c23f45d45bcea484c66 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 24 Sep 2019 22:43:06 -0400 Subject: [PATCH 0397/3170] Address review comments --- news/3907.bugfix | 3 +- src/pip/_internal/utils/unpacking.py | 16 ++++++----- tests/unit/test_utils_unpacking.py | 42 +++++++++++++--------------- 3 files changed, 31 insertions(+), 30 deletions(-) diff --git a/news/3907.bugfix b/news/3907.bugfix index c61975e7bf1..24d711df482 100644 --- a/news/3907.bugfix +++ b/news/3907.bugfix @@ -1 +1,2 @@ -Abort the installation process and raise an exception if one of the tar/zip file will be placed outside of target location causing security issue. \ No newline at end of file +Abort installation if any archive contains a file which would be placed +outside the extraction location. diff --git a/src/pip/_internal/utils/unpacking.py b/src/pip/_internal/utils/unpacking.py index ed66187ae10..81a368f27ce 100644 --- a/src/pip/_internal/utils/unpacking.py +++ b/src/pip/_internal/utils/unpacking.py @@ -120,11 +120,11 @@ def unzip_file(filename, location, flatten=True): fn = os.path.join(location, fn) dir = os.path.dirname(fn) if not is_within_directory(location, fn): - raise InstallationError( - 'The zip file (%s) has a file (%s) trying to install ' - 'outside target directory (%s)' % - (filename, fn, location) + message = ( + 'The zip file ({}) has a file ({}) trying to install ' + 'outside target directory ({})' ) + raise InstallationError(message.format(filename, fn, location)) if fn.endswith('/') or fn.endswith('\\'): # A directory ensure_dir(fn) @@ -185,10 +185,12 @@ def untar_file(filename, location): fn = split_leading_dir(fn)[1] # type: ignore path = os.path.join(location, fn) if not is_within_directory(location, path): + message = ( + 'The tar file ({}) has a file ({}) trying to install ' + 'outside target directory ({})' + ) raise InstallationError( - 'The tar file (%s) has a file (%s) trying to install ' - 'outside target directory (%s)' % - (filename, path, location) + message.format(filename, path, location) ) if member.isdir(): ensure_dir(path) diff --git a/tests/unit/test_utils_unpacking.py b/tests/unit/test_utils_unpacking.py index 40440fb7c27..af9ae1c0e71 100644 --- a/tests/unit/test_utils_unpacking.py +++ b/tests/unit/test_utils_unpacking.py @@ -126,13 +126,11 @@ def test_unpack_zip_failure(self): Test unpacking a *.zip with file containing .. path and expect exception """ - test_zip = self.make_zip_file('test_zip.zip', - ['regular_file.txt', - os.path.join('..', 'outside_file.txt')]) - with pytest.raises( - InstallationError, - match=r'.*trying to install outside target directory.*'): + files = ['regular_file.txt', os.path.join('..', 'outside_file.txt')] + test_zip = self.make_zip_file('test_zip.zip', files) + with pytest.raises(InstallationError) as e: unzip_file(test_zip, self.tempdir) + assert 'trying to install outside target directory' in str(e.value) def test_unpack_zip_success(self): """ @@ -140,11 +138,12 @@ def test_unpack_zip_success(self): no file will be installed outside target directory after unpack so no exception raised """ - test_zip = self.make_zip_file( - 'test_zip.zip', - ['regular_file1.txt', - os.path.join('dir', 'dir_file1.txt'), - os.path.join('dir', '..', 'dir_file2.txt')]) + files = [ + 'regular_file1.txt', + os.path.join('dir', 'dir_file1.txt'), + os.path.join('dir', '..', 'dir_file2.txt'), + ] + test_zip = self.make_zip_file('test_zip.zip', files) unzip_file(test_zip, self.tempdir) def test_unpack_tar_failure(self): @@ -152,13 +151,11 @@ def test_unpack_tar_failure(self): Test unpacking a *.tar with file containing .. path and expect exception """ - test_tar = self.make_tar_file('test_tar.tar', - ['regular_file.txt', - os.path.join('..', 'outside_file.txt')]) - with pytest.raises( - InstallationError, - match=r'.*trying to install outside target directory.*'): + files = ['regular_file.txt', os.path.join('..', 'outside_file.txt')] + test_tar = self.make_tar_file('test_tar.tar', files) + with pytest.raises(InstallationError) as e: untar_file(test_tar, self.tempdir) + assert 'trying to install outside target directory' in str(e.value) def test_unpack_tar_success(self): """ @@ -166,11 +163,12 @@ def test_unpack_tar_success(self): no file will be installed outside target directory after unpack so no exception raised """ - test_tar = self.make_tar_file( - 'test_tar.tar', - ['regular_file1.txt', - os.path.join('dir', 'dir_file1.txt'), - os.path.join('dir', '..', 'dir_file2.txt')]) + files = [ + 'regular_file1.txt', + os.path.join('dir', 'dir_file1.txt'), + os.path.join('dir', '..', 'dir_file2.txt'), + ] + test_tar = self.make_tar_file('test_tar.tar', files) untar_file(test_tar, self.tempdir) From 9798d0fff23c0eb834927df6ffc172acd491bd19 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Wed, 25 Sep 2019 09:30:23 +0530 Subject: [PATCH 0398/3170] Dedent hooks in .pre-commit-config.yaml --- .pre-commit-config.yaml | 62 ++++++++++++++++++++--------------------- 1 file changed, 31 insertions(+), 31 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 9a052ee9365..bc145e993b5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -4,50 +4,50 @@ repos: - repo: https://github.com/pre-commit/pre-commit-hooks rev: v2.3.0 hooks: - - id: check-builtin-literals - - id: check-added-large-files - - id: check-case-conflict - - id: check-toml - - id: check-yaml - - id: debug-statements - - id: end-of-file-fixer - exclude: WHEEL - - id: flake8 - exclude: tests/data - - id: forbid-new-submodules - - id: trailing-whitespace - exclude: .patch + - id: check-builtin-literals + - id: check-added-large-files + - id: check-case-conflict + - id: check-toml + - id: check-yaml + - id: debug-statements + - id: end-of-file-fixer + exclude: WHEEL + - id: flake8 + exclude: tests/data + - id: forbid-new-submodules + - id: trailing-whitespace + exclude: .patch - repo: https://github.com/timothycrosley/isort rev: 4.3.21 hooks: - - id: isort - files: \.py$ + - id: isort + files: \.py$ - repo: https://github.com/pre-commit/mirrors-mypy rev: v0.720 hooks: - - id: mypy - exclude: docs|tests - args: [] - - id: mypy - name: mypy, for Py2 - args: ["-2"] - exclude: docs|tests + - id: mypy + exclude: docs|tests + args: [] + - id: mypy + name: mypy, for Py2 + exclude: docs|tests + args: ["-2"] - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.4.1 hooks: - - id: python-no-log-warn - - id: python-no-eval - - id: rst-backticks - # Validate existing ReST files and NEWS fragments. - files: .*\.rst$|^news/.* - types: [file] - # The errors flagged in NEWS.rst are old. - exclude: NEWS.rst + - id: python-no-log-warn + - id: python-no-eval + - id: rst-backticks + # Validate existing ReST files and NEWS fragments. + files: .*\.rst$|^news/.* + types: [file] + # The errors flagged in NEWS.rst are old. + exclude: NEWS.rst - repo: https://github.com/mgedmin/check-manifest rev: '0.39' hooks: - - id: check-manifest + - id: check-manifest From be6e198875a630e591f8e6f0a576175ebe5ce9b3 Mon Sep 17 00:00:00 2001 From: Sebastian Jordan <sebastian.jordan.mail@googlemail.com> Date: Wed, 25 Sep 2019 06:32:18 +0200 Subject: [PATCH 0399/3170] Implement functional test for installing PEP 517 packages with --no-binary :all: --- .../pep517_setup_and_pyproject/pyproject.toml | 3 +++ .../pep517_setup_and_pyproject/setup.cfg | 3 +++ .../pep517_setup_and_pyproject/setup.py | 3 +++ tests/functional/test_install.py | 17 +++++++++++++++++ 4 files changed, 26 insertions(+) create mode 100644 tests/data/packages/pep517_setup_and_pyproject/pyproject.toml create mode 100644 tests/data/packages/pep517_setup_and_pyproject/setup.cfg create mode 100644 tests/data/packages/pep517_setup_and_pyproject/setup.py diff --git a/tests/data/packages/pep517_setup_and_pyproject/pyproject.toml b/tests/data/packages/pep517_setup_and_pyproject/pyproject.toml new file mode 100644 index 00000000000..86df60002ac --- /dev/null +++ b/tests/data/packages/pep517_setup_and_pyproject/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = [ "setuptools" ] +build-backend = "setuptools.build_meta" \ No newline at end of file diff --git a/tests/data/packages/pep517_setup_and_pyproject/setup.cfg b/tests/data/packages/pep517_setup_and_pyproject/setup.cfg new file mode 100644 index 00000000000..88446933ec6 --- /dev/null +++ b/tests/data/packages/pep517_setup_and_pyproject/setup.cfg @@ -0,0 +1,3 @@ +[metadata] +name = pep517-setup-and-pyproject +version = 1.0 \ No newline at end of file diff --git a/tests/data/packages/pep517_setup_and_pyproject/setup.py b/tests/data/packages/pep517_setup_and_pyproject/setup.py new file mode 100644 index 00000000000..606849326a4 --- /dev/null +++ b/tests/data/packages/pep517_setup_and_pyproject/setup.py @@ -0,0 +1,3 @@ +from setuptools import setup + +setup() diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 7a0e4a9cbee..3cbd59b070a 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1268,6 +1268,23 @@ def test_install_no_binary_disables_building_wheels(script, data, with_wheel): assert "Running setup.py install for upper" in str(res), str(res) +def test_install_no_binary_builds_pep_517_wheel(script, data, with_wheel): + to_install = data.packages.joinpath('pep517_setup_and_pyproject') + res = script.pip( + 'install', '--no-binary=:all:', '-f', data.find_links, to_install + ) + expected = ("Successfully installed pep517-setup-and-pyproject") + # Must have installed the package + assert expected in str(res), str(res) + + assert "Building wheel for pep517-setup-and-pyproject" in str(res), str(res) + assert ( + "Running setup.py install for pep517-setup-and-pyproject" \ + not in str(res), + str(res) + ) + + def test_install_no_binary_disables_cached_wheels(script, data, with_wheel): # Seed the cache script.pip( From 58eb90ff80e4537175a481143992d7ee23ec2988 Mon Sep 17 00:00:00 2001 From: Sebastian Jordan <sebastian.jordan.mail@googlemail.com> Date: Wed, 25 Sep 2019 06:37:02 +0200 Subject: [PATCH 0400/3170] Fix code layout problems in test_install.py --- tests/functional/test_install.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 3cbd59b070a..7b65e3104d2 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1277,12 +1277,8 @@ def test_install_no_binary_builds_pep_517_wheel(script, data, with_wheel): # Must have installed the package assert expected in str(res), str(res) - assert "Building wheel for pep517-setup-and-pyproject" in str(res), str(res) - assert ( - "Running setup.py install for pep517-setup-and-pyproject" \ - not in str(res), - str(res) - ) + assert "Building wheel for pep517-setup" in str(res), str(res) + assert "Running setup.py install for pep517-set" not in str(res), str(res) def test_install_no_binary_disables_cached_wheels(script, data, with_wheel): From dd842fd71e1a4b85d51bf8485753bc09f587d113 Mon Sep 17 00:00:00 2001 From: Sebastian Jordan <sebastian.jordan.mail@googlemail.com> Date: Wed, 25 Sep 2019 06:54:00 +0200 Subject: [PATCH 0401/3170] Add news entry --- news/6606.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/6606.bugfix diff --git a/news/6606.bugfix b/news/6606.bugfix new file mode 100644 index 00000000000..990a7570121 --- /dev/null +++ b/news/6606.bugfix @@ -0,0 +1 @@ +Fix bug that prevented installation of PEP 517 packages without `setup.py` \ No newline at end of file From 7a0f7b607defc078a8c774c15d6814511a68d937 Mon Sep 17 00:00:00 2001 From: Sebastian Jordan <sebastian.jordan.mail@googlemail.com> Date: Wed, 25 Sep 2019 06:58:53 +0200 Subject: [PATCH 0402/3170] Improve check_binary_allowed via early return --- src/pip/_internal/commands/install.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index ce2ec016114..c91f6b8f1c3 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -100,9 +100,11 @@ def get_check_binary_allowed(format_control): # type: (FormatControl) -> BinaryAllowedPredicate def check_binary_allowed(req): # type: (InstallRequirement) -> bool + if req.use_pep517: + return True canonical_name = canonicalize_name(req.name) allowed_formats = format_control.get_allowed_formats(canonical_name) - return "binary" in allowed_formats or req.use_pep517 + return "binary" in allowed_formats return check_binary_allowed From cd0d6e82391545a86e2d906afad8bab289f1f9ad Mon Sep 17 00:00:00 2001 From: Sebastian Jordan <sebastian.jordan.mail@googlemail.com> Date: Wed, 25 Sep 2019 07:04:56 +0200 Subject: [PATCH 0403/3170] Delete unnecessary empty line addition --- src/pip/_internal/commands/install.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index c91f6b8f1c3..04e28e755ca 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -396,7 +396,6 @@ def run(self, options, args): check_binary_allowed = get_check_binary_allowed( finder.format_control ) - # Consider legacy and PEP517-using requirements separately legacy_requirements = [] pep517_requirements = [] From 9a2181a087340e537013f8223b39623662e5f3a0 Mon Sep 17 00:00:00 2001 From: Patrik Kopkan <pkopkan@redhat.com> Date: Wed, 25 Sep 2019 13:37:53 +0200 Subject: [PATCH 0404/3170] Add new option: pip wheel --save-wheel-names --- news/6340.feature | 1 + src/pip/_internal/commands/wheel.py | 40 ++++++++++++++++++ src/pip/_internal/wheel.py | 12 +++++- tests/functional/test_wheel.py | 63 +++++++++++++++++++++++++++++ tests/unit/test_wheel.py | 28 +++++++++++++ 5 files changed, 142 insertions(+), 2 deletions(-) create mode 100644 news/6340.feature diff --git a/news/6340.feature b/news/6340.feature new file mode 100644 index 00000000000..208501c6f93 --- /dev/null +++ b/news/6340.feature @@ -0,0 +1 @@ +Add a new option ``--save-wheel-names <filename>`` to ``pip wheel`` that writes the names of the resulting wheels to the given filename. \ No newline at end of file diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index b95732b0ce4..45f106e5550 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -98,6 +98,16 @@ def __init__(self, *args, **kw): cmd_opts.add_option(cmdoptions.no_clean()) cmd_opts.add_option(cmdoptions.require_hashes()) + cmd_opts.add_option( + '--save-wheel-names', + dest='path_to_wheelnames', + action='store', + metavar='path', + help=("Store the filenames of the built or downloaded wheels " + "in a new file of given path. Filenames are separated " + "by new line and file ends with new line"), + ) + index_opts = cmdoptions.make_option_group( cmdoptions.index_group, self.parser, @@ -106,6 +116,28 @@ def __init__(self, *args, **kw): self.parser.insert_option_group(0, index_opts) self.parser.insert_option_group(0, cmd_opts) + def save_wheelnames( + self, + links_filenames, + path_to_wheelnames, + wheel_filenames, + ): + if path_to_wheelnames is None: + return + + entries_to_save = wheel_filenames + links_filenames + entries_to_save = [ + filename + '\n' for filename in entries_to_save + if filename.endswith('whl') + ] + try: + with open(path_to_wheelnames, 'w') as f: + f.writelines(entries_to_save) + except EnvironmentError as e: + logger.error('Cannot write to the given path: %s\n%s' % + (path_to_wheelnames, e)) + raise + def run(self, options, args): # type: (Values, List[Any]) -> None cmdoptions.check_install_build_global(options) @@ -159,10 +191,18 @@ def run(self, options, args): build_options=options.build_options or [], global_options=options.global_options or [], no_clean=options.no_clean, + path_to_wheelnames=options.path_to_wheelnames ) build_failures = wb.build( requirement_set.requirements.values(), ) + self.save_wheelnames( + [req.link.filename for req in + requirement_set.successfully_downloaded + if req.link is not None], + wb.path_to_wheelnames, + wb.wheel_filenames, + ) if len(build_failures) != 0: raise CommandError( "Failed to build one or more wheels" diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index 1768e1ca84d..1b7ec265419 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -55,7 +55,7 @@ if MYPY_CHECK_RUNNING: from typing import ( Dict, List, Optional, Sequence, Mapping, Tuple, IO, Text, Any, - Iterable, Callable, + Iterable, Callable, Union, ) from pip._vendor.packaging.requirements import Requirement from pip._internal.req.req_install import InstallRequirement @@ -905,7 +905,8 @@ def __init__( build_options=None, # type: Optional[List[str]] global_options=None, # type: Optional[List[str]] check_binary_allowed=None, # type: Optional[BinaryAllowedPredicate] - no_clean=False # type: bool + no_clean=False, # type: bool + path_to_wheelnames=None, # type: Optional[Union[bytes, Text]] ): # type: (...) -> None if check_binary_allowed is None: @@ -921,6 +922,10 @@ def __init__( self.global_options = global_options or [] self.check_binary_allowed = check_binary_allowed self.no_clean = no_clean + # path where to save built names of built wheels + self.path_to_wheelnames = path_to_wheelnames + # file names of built wheel names + self.wheel_filenames = [] # type: List[Union[bytes, Text]] def _build_one(self, req, output_dir, python_tag=None): """Build one wheel. @@ -1130,6 +1135,9 @@ def build( ) if wheel_file: build_success.append(req) + self.wheel_filenames.append( + os.path.relpath(wheel_file, output_dir) + ) if should_unpack: # XXX: This is mildly duplicative with prepare_files, # but not close enough to pull out to a single common diff --git a/tests/functional/test_wheel.py b/tests/functional/test_wheel.py index 5ebc9ea4c21..92f2c9ef479 100644 --- a/tests/functional/test_wheel.py +++ b/tests/functional/test_wheel.py @@ -1,6 +1,7 @@ """'pip wheel' tests""" import os import re +import stat from os.path import exists import pytest @@ -255,3 +256,65 @@ def test_legacy_wheels_are_not_confused_with_other_files(script, tmpdir, data): wheel_file_name = 'simplewheel-1.0-py%s-none-any.whl' % pyversion[0] wheel_file_path = script.scratch / wheel_file_name assert wheel_file_path in result.files_created, result.stdout + + +def test_pip_option_save_wheel_name(script, data): + """Check if the option saves the filenames of built wheels + """ + script.pip( + 'wheel', '--no-index', '-f', data.find_links, + 'require_simple==1.0', + '--save-wheel-name', 'wheelnames', + ) + + wheel_file_names = [ + 'require_simple-1.0-py%s-none-any.whl' % pyversion[0], + 'simple-3.0-py%s-none-any.whl' % pyversion[0], + ] + wheelnames_path = script.scratch_path / 'wheelnames' + with open(wheelnames_path, 'r') as wheelnames_file: + wheelnames_entries = (wheelnames_file.read()).splitlines() + assert wheel_file_names == wheelnames_entries + + +def test_pip_option_save_wheel_name_Permission_error(script, data): + + temp_file = script.base_path / 'scratch' / 'wheelnames' + + wheel_file_names = [ + 'require_simple-1.0-py%s-none-any.whl' % pyversion[0], + 'simple-3.0-py%s-none-any.whl' % pyversion[0], + ] + + script.pip( + 'wheel', '--no-index', '-f', data.find_links, + 'require_simple==1.0', + '--save-wheel-name', 'wheelnames', + ) + os.chmod(temp_file, stat.S_IREAD) + result = script.pip( + 'wheel', '--no-index', '-f', data.find_links, + 'require_simple==1.0', + '--save-wheel-name', 'wheelnames', expect_error=True, + ) + os.chmod(temp_file, stat.S_IREAD | stat.S_IWRITE) + + assert "ERROR: Cannot write to the given path: wheelnames\n" \ + "[Errno 13] Permission denied: 'wheelnames'\n" in result.stderr + + with open(temp_file) as f: + result = f.read().splitlines() + # check that file stays same + assert result == wheel_file_names + + +def test_pip_option_save_wheel_name_error_during_build(script, data): + script.pip( + 'wheel', '--no-index', '--save-wheel-name', 'wheelnames', + '-f', data.find_links, 'wheelbroken==0.1', + expect_error=True, + ) + wheelnames_path = script.base_path / 'scratch' / 'wheelnames' + with open(wheelnames_path) as f: + wheelnames = f.read().splitlines() + assert wheelnames == [] diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 2a824c7fd7b..254986e7f44 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -9,6 +9,7 @@ from pip._vendor.packaging.requirements import Requirement from pip._internal import pep425tags, wheel +from pip._internal.commands.wheel import WheelCommand from pip._internal.exceptions import InvalidWheelFilename, UnsupportedWheel from pip._internal.models.link import Link from pip._internal.req.req_install import InstallRequirement @@ -848,3 +849,30 @@ def test_rehash(self, tmpdir): h, length = wheel.rehash(self.test_file) assert length == str(self.test_file_len) assert h == self.test_file_hash_encoded + + +class TestWheelCommand(object): + + def test_save_wheelnames(self, tmpdir): + wheel_filenames = ['Flask-1.1.dev0-py2.py3-none-any.whl'] + links_filenames = [ + 'flask', + 'Werkzeug-0.15.4-py2.py3-none-any.whl', + 'Jinja2-2.10.1-py2.py3-none-any.whl', + 'itsdangerous-1.1.0-py2.py3-none-any.whl', + 'Click-7.0-py2.py3-none-any.whl' + ] + + expected = wheel_filenames + links_filenames[1:] + expected = [filename + '\n' for filename in expected] + temp_file = tmpdir.joinpath('wheelfiles') + + WheelCommand('name', 'summary').save_wheelnames( + links_filenames, + temp_file, + wheel_filenames + ) + + with open(temp_file, 'r') as f: + test_content = f.readlines() + assert test_content == expected From 5713057db212cefb5e230bd57b3c018dd27f55a0 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 24 Sep 2019 17:37:54 +0530 Subject: [PATCH 0405/3170] Rewrap all of user_guide.rst at 80 characters --- docs/html/user_guide.rst | 145 +++++++++++++++++++++------------------ 1 file changed, 78 insertions(+), 67 deletions(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index c0d09fb4db9..ff500cb34de 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -100,15 +100,13 @@ In practice, there are 4 common uses of Requirements files: 2. Requirements files are used to force pip to properly resolve dependencies. As it is now, pip `doesn't have true dependency resolution <https://github.com/pypa/pip/issues/988>`_, but instead simply uses the first - specification it finds for a project. E.g. if ``pkg1`` requires ``pkg3>=1.0`` and - ``pkg2`` requires ``pkg3>=1.0,<=2.0``, and if ``pkg1`` is resolved first, pip will - only use ``pkg3>=1.0``, and could easily end up installing a version of ``pkg3`` - that conflicts with the needs of ``pkg2``. To solve this problem, you can - place ``pkg3>=1.0,<=2.0`` (i.e. the correct specification) into your - requirements file directly along with the other top level requirements. Like - so: - - :: + specification it finds for a project. E.g. if ``pkg1`` requires + ``pkg3>=1.0`` and ``pkg2`` requires ``pkg3>=1.0,<=2.0``, and if ``pkg1`` is + resolved first, pip will only use ``pkg3>=1.0``, and could easily end up + installing a version of ``pkg3`` that conflicts with the needs of ``pkg2``. + To solve this problem, you can place ``pkg3>=1.0,<=2.0`` (i.e. the correct + specification) into your requirements file directly along with the other top + level requirements. Like so:: pkg1 pkg2 @@ -117,20 +115,18 @@ In practice, there are 4 common uses of Requirements files: 3. Requirements files are used to force pip to install an alternate version of a sub-dependency. For example, suppose ``ProjectA`` in your requirements file requires ``ProjectB``, but the latest version (v1.3) has a bug, you can force - pip to accept earlier versions like so: - - :: + pip to accept earlier versions like so:: ProjectA ProjectB<1.3 4. Requirements files are used to override a dependency with a local patch that - lives in version control. For example, suppose a dependency, - ``SomeDependency`` from PyPI has a bug, and you can't wait for an upstream fix. + lives in version control. For example, suppose a dependency + ``SomeDependency`` from PyPI has a bug, and you can't wait for an upstream + fix. You could clone/copy the src, make the fix, and place it in VCS with the tag - ``sometag``. You'd reference it in your requirements file with a line like so: - - :: + ``sometag``. You'd reference it in your requirements file with a line like + so:: git+https://myvcs.com/some_dependency@sometag#egg=SomeDependency @@ -198,7 +194,8 @@ to building and installing from source archives. For more information, see the Pip prefers Wheels where they are available. To disable this, use the :ref:`--no-binary <install_--no-binary>` flag for :ref:`pip install`. -If no satisfactory wheels are found, pip will default to finding source archives. +If no satisfactory wheels are found, pip will default to finding source +archives. To install directly from a wheel archive: @@ -215,15 +212,16 @@ convenience, to build wheels for all your requirements and dependencies. <https://pypi.org/project/wheel/>`_ to be installed, which provides the "bdist_wheel" setuptools extension that it uses. -To build wheels for your requirements and all their dependencies to a local directory: +To build wheels for your requirements and all their dependencies to a local +directory: :: pip install wheel pip wheel --wheel-dir=/local/wheels -r requirements.txt - -And *then* to install those requirements just using your local directory of wheels (and not from PyPI): +And *then* to install those requirements just using your local directory of +wheels (and not from PyPI): :: @@ -447,16 +445,18 @@ is the same as calling:: .. note:: - Environment variables set to be empty string will not be treated as false. Please use ``no``, - ``false`` or ``0`` instead. + Environment variables set to be empty string will not be treated as false. + Please use ``no``, ``false`` or ``0`` instead. Config Precedence ----------------- -Command line options have precedence over environment variables, which have precedence over the config file. +Command line options have precedence over environment variables, which have +precedence over the config file. -Within the config file, command specific sections have precedence over the global section. +Within the config file, command specific sections have precedence over the +global section. Examples: @@ -483,8 +483,9 @@ To setup for fish:: $ pip completion --fish > ~/.config/fish/completions/pip.fish -Alternatively, you can use the result of the ``completion`` command -directly with the eval function of your shell, e.g. by adding the following to your startup file:: +Alternatively, you can use the result of the ``completion`` command directly +with the eval function of your shell, e.g. by adding the following to your +startup file:: eval "`pip completion --bash`" @@ -551,14 +552,16 @@ With Python 2.6 came the `"user scheme" for installation which means that all Python distributions support an alternative install location that is specific to a user. The default location for each OS is explained in the python documentation for the `site.USER_BASE -<https://docs.python.org/3/library/site.html#site.USER_BASE>`_ variable. This mode -of installation can be turned on by specifying the :ref:`--user +<https://docs.python.org/3/library/site.html#site.USER_BASE>`_ variable. +This mode of installation can be turned on by specifying the :ref:`--user <install_--user>` option to ``pip install``. Moreover, the "user scheme" can be customized by setting the -``PYTHONUSERBASE`` environment variable, which updates the value of ``site.USER_BASE``. +``PYTHONUSERBASE`` environment variable, which updates the value of +``site.USER_BASE``. -To install "SomePackage" into an environment with site.USER_BASE customized to '/myappenv', do the following:: +To install "SomePackage" into an environment with site.USER_BASE customized to +'/myappenv', do the following:: export PYTHONUSERBASE=/myappenv pip install --user SomePackage @@ -591,7 +594,8 @@ From within a ``--no-site-packages`` virtualenv (i.e. the default kind):: Can not perform a '--user' install. User site-packages are not visible in this virtualenv. -From within a ``--system-site-packages`` virtualenv where ``SomePackage==0.3`` is already installed in the virtualenv:: +From within a ``--system-site-packages`` virtualenv where ``SomePackage==0.3`` +is already installed in the virtualenv:: $ pip install --user SomePackage==0.4 Will not install to the user site because it will lack sys.path precedence @@ -604,7 +608,8 @@ From within a real python, where ``SomePackage`` is *not* installed globally:: Successfully installed SomePackage -From within a real python, where ``SomePackage`` *is* installed globally, but is *not* the latest version:: +From within a real python, where ``SomePackage`` *is* installed globally, but +is *not* the latest version:: $ pip install --user SomePackage [...] @@ -615,7 +620,8 @@ From within a real python, where ``SomePackage`` *is* installed globally, but is Successfully installed SomePackage -From within a real python, where ``SomePackage`` *is* installed globally, and is the latest version:: +From within a real python, where ``SomePackage`` *is* installed globally, and +is the latest version:: $ pip install --user SomePackage [...] @@ -679,7 +685,8 @@ requirements file for free). It can also substitute for a vendor library, providing easier upgrades and less VCS noise. It does not, of course, provide the availability benefits of a private index or a vendor library. -For more, see :ref:`pip install\'s discussion of hash-checking mode <hash-checking mode>`. +For more, see +:ref:`pip install\'s discussion of hash-checking mode <hash-checking mode>`. .. _`Installation Bundle`: @@ -720,50 +727,54 @@ archives are built with identical packages. Using pip from your program *************************** -As noted previously, pip is a command line program. While it is implemented in Python, -and so is available from your Python code via ``import pip``, you must not use pip's -internal APIs in this way. There are a number of reasons for this: +As noted previously, pip is a command line program. While it is implemented in +Python, and so is available from your Python code via ``import pip``, you must +not use pip's internal APIs in this way. There are a number of reasons for this: -#. The pip code assumes that is in sole control of the global state of the program. - Pip manages things like the logging system configuration, or the values of the - standard IO streams, without considering the possibility that user code might be - affected. +#. The pip code assumes that is in sole control of the global state of the + program. + pip manages things like the logging system configuration, or the values of + the standard IO streams, without considering the possibility that user code + might be affected. -#. Pip's code is *not* thread safe. If you were to run pip in a thread, there is no - guarantee that either your code or pip's would work as you expect. +#. pip's code is *not* thread safe. If you were to run pip in a thread, there + is no guarantee that either your code or pip's would work as you expect. -#. Pip assumes that once it has finished its work, the process will terminate. It - doesn't need to handle the possibility that other code will continue to run - after that point, so (for example) calling pip twice in the same process is - likely to have issues. +#. pip assumes that once it has finished its work, the process will terminate. + It doesn't need to handle the possibility that other code will continue to + run after that point, so (for example) calling pip twice in the same process + is likely to have issues. -This does not mean that the pip developers are opposed in principle to the idea that -pip could be used as a library - it's just that this isn't how it was written, and it -would be a lot of work to redesign the internals for use as a library, handling all -of the above issues, and designing a usable, robust and stable API that we could -guarantee would remain available across multiple releases of pip. And we simply don't -currently have the resources to even consider such a task. +This does not mean that the pip developers are opposed in principle to the idea +that pip could be used as a library - it's just that this isn't how it was +written, and it would be a lot of work to redesign the internals for use as a +library, handling all of the above issues, and designing a usable, robust and +stable API that we could guarantee would remain available across multiple +releases of pip. And we simply don't currently have the resources to even +consider such a task. What this means in practice is that everything inside of pip is considered an -implementation detail. Even the fact that the import name is ``pip`` is subject to -change without notice. While we do try not to break things as much as possible, all -the internal APIs can change at any time, for any reason. It also means that we -generally *won't* fix issues that are a result of using pip in an unsupported way. - -It should also be noted that installing packages into ``sys.path`` in a running Python -process is something that should only be done with care. The import system caches -certain data, and installing new packages while a program is running may not always -behave as expected. In practice, there is rarely an issue, but it is something to be -aware of. +implementation detail. Even the fact that the import name is ``pip`` is subject +to change without notice. While we do try not to break things as much as +possible, all the internal APIs can change at any time, for any reason. It also +means that we generally *won't* fix issues that are a result of using pip in an +unsupported way. + +It should also be noted that installing packages into ``sys.path`` in a running +Python process is something that should only be done with care. The import +system caches certain data, and installing new packages while a program is +running may not always behave as expected. In practice, there is rarely an +issue, but it is something to be aware of. Having said all of the above, it is worth covering the options available if you decide that you do want to run pip from within your program. The most reliable -approach, and the one that is fully supported, is to run pip in a subprocess. This -is easily done using the standard ``subprocess`` module:: +approach, and the one that is fully supported, is to run pip in a subprocess. +This is easily done using the standard ``subprocess`` module:: subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'my_package']) -If you want to process the output further, use one of the other APIs in the module:: +If you want to process the output further, use one of the other APIs in the +module:: reqs = subprocess.check_output([sys.executable, '-m', 'pip', 'freeze']) From c6a2f0aac74eed8634b4ccc8224d93ef9f784f93 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Fri, 27 Sep 2019 00:53:24 +0530 Subject: [PATCH 0406/3170] Rename {setup_py_dir -> unpacked_source_directory} Why: Because PEP 517 means you won't have a setup.py in your unpacked sources. --- .../_internal/operations/generate_metadata.py | 6 +++-- src/pip/_internal/pyproject.py | 4 ++-- src/pip/_internal/req/req_install.py | 23 ++++++++++--------- src/pip/_internal/wheel.py | 8 +++++-- 4 files changed, 24 insertions(+), 17 deletions(-) diff --git a/src/pip/_internal/operations/generate_metadata.py b/src/pip/_internal/operations/generate_metadata.py index 5d64e018f69..5788b73cd64 100644 --- a/src/pip/_internal/operations/generate_metadata.py +++ b/src/pip/_internal/operations/generate_metadata.py @@ -41,7 +41,9 @@ def _generate_metadata_legacy(install_req): # egg. egg_base_option = [] # type: List[str] if not install_req.editable: - egg_info_dir = os.path.join(install_req.setup_py_dir, 'pip-egg-info') + egg_info_dir = os.path.join( + install_req.unpacked_source_directory, 'pip-egg-info', + ) egg_base_option = ['--egg-base', egg_info_dir] # setuptools complains if the target directory does not exist. @@ -50,7 +52,7 @@ def _generate_metadata_legacy(install_req): with install_req.build_env: call_subprocess( base_cmd + ["egg_info"] + egg_base_option, - cwd=install_req.setup_py_dir, + cwd=install_req.unpacked_source_directory, command_desc='python setup.py egg_info', ) diff --git a/src/pip/_internal/pyproject.py b/src/pip/_internal/pyproject.py index 43efbed42be..98c20f7796d 100644 --- a/src/pip/_internal/pyproject.py +++ b/src/pip/_internal/pyproject.py @@ -21,9 +21,9 @@ def _is_list_of_str(obj): ) -def make_pyproject_path(setup_py_dir): +def make_pyproject_path(unpacked_source_directory): # type: (str) -> str - path = os.path.join(setup_py_dir, 'pyproject.toml') + path = os.path.join(unpacked_source_directory, 'pyproject.toml') # Python2 __file__ should not be unicode if six.PY2 and isinstance(path, six.text_type): diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 78d22eafd8e..659a494e94e 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -485,7 +485,7 @@ def move_wheel_files( # Things valid for sdists @property - def setup_py_dir(self): + def unpacked_source_directory(self): # type: () -> str return os.path.join( self.source_dir, @@ -495,8 +495,7 @@ def setup_py_dir(self): def setup_py_path(self): # type: () -> str assert self.source_dir, "No source dir for %s" % self - - setup_py = os.path.join(self.setup_py_dir, 'setup.py') + setup_py = os.path.join(self.unpacked_source_directory, 'setup.py') # Python2 __file__ should not be unicode if six.PY2 and isinstance(setup_py, six.text_type): @@ -508,8 +507,7 @@ def setup_py_path(self): def pyproject_toml_path(self): # type: () -> str assert self.source_dir, "No source dir for %s" % self - - return make_pyproject_path(self.setup_py_dir) + return make_pyproject_path(self.unpacked_source_directory) def load_pyproject_toml(self): # type: () -> None @@ -535,7 +533,9 @@ def load_pyproject_toml(self): requires, backend, check = pyproject_toml_data self.requirements_to_check = check self.pyproject_requires = requires - self.pep517_backend = Pep517HookCaller(self.setup_py_dir, backend) + self.pep517_backend = Pep517HookCaller( + self.unpacked_source_directory, backend + ) # Use a custom function to call subprocesses self.spin_message = "" @@ -665,7 +665,7 @@ def depth_of_directory(dir_): base = self.source_dir filenames = locate_editable_egg_info(base) else: - base = os.path.join(self.setup_py_dir, 'pip-egg-info') + base = os.path.join(self.unpacked_source_directory, 'pip-egg-info') filenames = os.listdir(base) if not filenames: @@ -770,8 +770,7 @@ def install_editable( base_cmd + ['develop', '--no-deps'] + list(install_options), - - cwd=self.setup_py_dir, + cwd=self.unpacked_source_directory, ) self.install_succeeded = True @@ -883,7 +882,9 @@ def archive(self, build_dir): archive_path, 'w', zipfile.ZIP_DEFLATED, allowZip64=True, ) with zip_output: - dir = os.path.normcase(os.path.abspath(self.setup_py_dir)) + dir = os.path.normcase( + os.path.abspath(self.unpacked_source_directory) + ) for dirpath, dirnames, filenames in os.walk(dir): if 'pip-egg-info' in dirnames: dirnames.remove('pip-egg-info') @@ -956,7 +957,7 @@ def install( with self.build_env: call_subprocess( install_args + install_options, - cwd=self.setup_py_dir, + cwd=self.unpacked_source_directory, spinner=spinner, ) diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index 1768e1ca84d..8d3c7aefdce 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -1019,12 +1019,16 @@ def _build_one_legacy(self, req, tempd, python_tag=None): wheel_args += ["--python-tag", python_tag] try: - output = call_subprocess(wheel_args, cwd=req.setup_py_dir, - spinner=spinner) + output = call_subprocess( + wheel_args, + cwd=req.unpacked_source_directory, + spinner=spinner, + ) except Exception: spinner.finish("error") logger.error('Failed building wheel for %s', req.name) return None + names = os.listdir(tempd) wheel_path = get_legacy_build_wheel_path( names=names, From 6ce7217a535e4efc56ab066e069ff2bdcd623c04 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 26 Sep 2019 18:20:58 -0400 Subject: [PATCH 0407/3170] Move PipSession helper functions to network.session This new module is a catch-all for PipSession helpers. --- src/pip/_internal/download.py | 135 +------------------------- src/pip/_internal/network/session.py | 137 +++++++++++++++++++++++++++ tests/unit/test_download.py | 45 --------- tests/unit/test_network_session.py | 48 ++++++++++ 4 files changed, 189 insertions(+), 176 deletions(-) create mode 100644 src/pip/_internal/network/session.py create mode 100644 tests/unit/test_network_session.py diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index 92d5ab501e3..ac5c393c785 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -2,11 +2,9 @@ import cgi import email.utils -import json import logging import mimetypes import os -import platform import re import shutil import sys @@ -22,16 +20,15 @@ from pip._vendor.six.moves import xmlrpc_client # type: ignore from pip._vendor.six.moves.urllib import parse as urllib_parse -import pip from pip._internal.exceptions import HashMismatch, InstallationError from pip._internal.models.index import PyPI from pip._internal.network.auth import MultiDomainBasicAuth from pip._internal.network.cache import SafeFileCache +from pip._internal.network.session import SECURE_ORIGINS, user_agent # Import ssl from compat so the initial import occurs in only one place. -from pip._internal.utils.compat import HAS_TLS, ipaddress, ssl +from pip._internal.utils.compat import ipaddress from pip._internal.utils.encoding import auto_decode from pip._internal.utils.filesystem import check_path_owner, copy2_fixed -from pip._internal.utils.glibc import libc_ver from pip._internal.utils.misc import ( ask_path_exists, backup_dir, @@ -39,7 +36,6 @@ consume, display_path, format_size, - get_installed_version, hide_url, parse_netloc, path_to_display, @@ -55,17 +51,16 @@ if MYPY_CHECK_RUNNING: from typing import ( - IO, Callable, Iterator, List, Optional, Text, Tuple, Union, + IO, Callable, Iterator, List, Optional, Text, Tuple, ) from mypy_extensions import TypedDict from pip._internal.models.link import Link + from pip._internal.network.requests import SecureOrigin from pip._internal.utils.hashes import Hashes from pip._internal.vcs.versioncontrol import VersionControl - SecureOrigin = Tuple[str, str, Optional[Union[int, str]]] - if PY2: CopytreeKwargs = TypedDict( 'CopytreeKwargs', @@ -98,128 +93,6 @@ logger = logging.getLogger(__name__) -SECURE_ORIGINS = [ - # protocol, hostname, port - # Taken from Chrome's list of secure origins (See: http://bit.ly/1qrySKC) - ("https", "*", "*"), - ("*", "localhost", "*"), - ("*", "127.0.0.0/8", "*"), - ("*", "::1/128", "*"), - ("file", "*", None), - # ssh is always secure. - ("ssh", "*", "*"), -] # type: List[SecureOrigin] - - -# These are environment variables present when running under various -# CI systems. For each variable, some CI systems that use the variable -# are indicated. The collection was chosen so that for each of a number -# of popular systems, at least one of the environment variables is used. -# This list is used to provide some indication of and lower bound for -# CI traffic to PyPI. Thus, it is okay if the list is not comprehensive. -# For more background, see: https://github.com/pypa/pip/issues/5499 -CI_ENVIRONMENT_VARIABLES = ( - # Azure Pipelines - 'BUILD_BUILDID', - # Jenkins - 'BUILD_ID', - # AppVeyor, CircleCI, Codeship, Gitlab CI, Shippable, Travis CI - 'CI', - # Explicit environment variable. - 'PIP_IS_CI', -) - - -def looks_like_ci(): - # type: () -> bool - """ - Return whether it looks like pip is running under CI. - """ - # We don't use the method of checking for a tty (e.g. using isatty()) - # because some CI systems mimic a tty (e.g. Travis CI). Thus that - # method doesn't provide definitive information in either direction. - return any(name in os.environ for name in CI_ENVIRONMENT_VARIABLES) - - -def user_agent(): - """ - Return a string representing the user agent. - """ - data = { - "installer": {"name": "pip", "version": pip.__version__}, - "python": platform.python_version(), - "implementation": { - "name": platform.python_implementation(), - }, - } - - if data["implementation"]["name"] == 'CPython': - data["implementation"]["version"] = platform.python_version() - elif data["implementation"]["name"] == 'PyPy': - if sys.pypy_version_info.releaselevel == 'final': - pypy_version_info = sys.pypy_version_info[:3] - else: - pypy_version_info = sys.pypy_version_info - data["implementation"]["version"] = ".".join( - [str(x) for x in pypy_version_info] - ) - elif data["implementation"]["name"] == 'Jython': - # Complete Guess - data["implementation"]["version"] = platform.python_version() - elif data["implementation"]["name"] == 'IronPython': - # Complete Guess - data["implementation"]["version"] = platform.python_version() - - if sys.platform.startswith("linux"): - from pip._vendor import distro - distro_infos = dict(filter( - lambda x: x[1], - zip(["name", "version", "id"], distro.linux_distribution()), - )) - libc = dict(filter( - lambda x: x[1], - zip(["lib", "version"], libc_ver()), - )) - if libc: - distro_infos["libc"] = libc - if distro_infos: - data["distro"] = distro_infos - - if sys.platform.startswith("darwin") and platform.mac_ver()[0]: - data["distro"] = {"name": "macOS", "version": platform.mac_ver()[0]} - - if platform.system(): - data.setdefault("system", {})["name"] = platform.system() - - if platform.release(): - data.setdefault("system", {})["release"] = platform.release() - - if platform.machine(): - data["cpu"] = platform.machine() - - 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 - - # Use None rather than False so as not to give the impression that - # pip knows it is not being run under CI. Rather, it is a null or - # inconclusive result. Also, we include some value rather than no - # value to make it easier to know that the check has been run. - data["ci"] = True if looks_like_ci() else None - - user_data = os.environ.get("PIP_USER_AGENT_USER_DATA") - if user_data is not None: - data["user_data"] = user_data - - return "{data[installer][name]}/{data[installer][version]} {json}".format( - data=data, - json=json.dumps(data, separators=(",", ":"), sort_keys=True), - ) - - class LocalFSAdapter(BaseAdapter): def send(self, request, stream=None, timeout=None, verify=None, cert=None, diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py new file mode 100644 index 00000000000..096c7a271e5 --- /dev/null +++ b/src/pip/_internal/network/session.py @@ -0,0 +1,137 @@ +import json +import os +import platform +import sys + +import pip +from pip._internal.utils.compat import HAS_TLS, ssl +from pip._internal.utils.glibc import libc_ver +from pip._internal.utils.misc import get_installed_version +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import List, Optional, Tuple, Union + + SecureOrigin = Tuple[str, str, Optional[Union[int, str]]] + + +SECURE_ORIGINS = [ + # protocol, hostname, port + # Taken from Chrome's list of secure origins (See: http://bit.ly/1qrySKC) + ("https", "*", "*"), + ("*", "localhost", "*"), + ("*", "127.0.0.0/8", "*"), + ("*", "::1/128", "*"), + ("file", "*", None), + # ssh is always secure. + ("ssh", "*", "*"), +] # type: List[SecureOrigin] + + +# These are environment variables present when running under various +# CI systems. For each variable, some CI systems that use the variable +# are indicated. The collection was chosen so that for each of a number +# of popular systems, at least one of the environment variables is used. +# This list is used to provide some indication of and lower bound for +# CI traffic to PyPI. Thus, it is okay if the list is not comprehensive. +# For more background, see: https://github.com/pypa/pip/issues/5499 +CI_ENVIRONMENT_VARIABLES = ( + # Azure Pipelines + 'BUILD_BUILDID', + # Jenkins + 'BUILD_ID', + # AppVeyor, CircleCI, Codeship, Gitlab CI, Shippable, Travis CI + 'CI', + # Explicit environment variable. + 'PIP_IS_CI', +) + + +def looks_like_ci(): + # type: () -> bool + """ + Return whether it looks like pip is running under CI. + """ + # We don't use the method of checking for a tty (e.g. using isatty()) + # because some CI systems mimic a tty (e.g. Travis CI). Thus that + # method doesn't provide definitive information in either direction. + return any(name in os.environ for name in CI_ENVIRONMENT_VARIABLES) + + +def user_agent(): + """ + Return a string representing the user agent. + """ + data = { + "installer": {"name": "pip", "version": pip.__version__}, + "python": platform.python_version(), + "implementation": { + "name": platform.python_implementation(), + }, + } + + if data["implementation"]["name"] == 'CPython': + data["implementation"]["version"] = platform.python_version() + elif data["implementation"]["name"] == 'PyPy': + if sys.pypy_version_info.releaselevel == 'final': + pypy_version_info = sys.pypy_version_info[:3] + else: + pypy_version_info = sys.pypy_version_info + data["implementation"]["version"] = ".".join( + [str(x) for x in pypy_version_info] + ) + elif data["implementation"]["name"] == 'Jython': + # Complete Guess + data["implementation"]["version"] = platform.python_version() + elif data["implementation"]["name"] == 'IronPython': + # Complete Guess + data["implementation"]["version"] = platform.python_version() + + if sys.platform.startswith("linux"): + from pip._vendor import distro + distro_infos = dict(filter( + lambda x: x[1], + zip(["name", "version", "id"], distro.linux_distribution()), + )) + libc = dict(filter( + lambda x: x[1], + zip(["lib", "version"], libc_ver()), + )) + if libc: + distro_infos["libc"] = libc + if distro_infos: + data["distro"] = distro_infos + + if sys.platform.startswith("darwin") and platform.mac_ver()[0]: + data["distro"] = {"name": "macOS", "version": platform.mac_ver()[0]} + + if platform.system(): + data.setdefault("system", {})["name"] = platform.system() + + if platform.release(): + data.setdefault("system", {})["release"] = platform.release() + + if platform.machine(): + data["cpu"] = platform.machine() + + 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 + + # Use None rather than False so as not to give the impression that + # pip knows it is not being run under CI. Rather, it is a null or + # inconclusive result. Also, we include some value rather than no + # value to make it easier to know that the check has been run. + data["ci"] = True if looks_like_ci() else None + + user_data = os.environ.get("PIP_USER_AGENT_USER_DATA") + if user_data is not None: + data["user_data"] = user_data + + return "{data[installer][name]}/{data[installer][version]} {json}".format( + data=data, + json=json.dumps(data, separators=(",", ":"), sort_keys=True), + ) diff --git a/tests/unit/test_download.py b/tests/unit/test_download.py index 4bbc20e0604..f2865bbae85 100644 --- a/tests/unit/test_download.py +++ b/tests/unit/test_download.py @@ -10,9 +10,7 @@ import pytest from mock import Mock, patch -import pip from pip._internal.download import ( - CI_ENVIRONMENT_VARIABLES, PipSession, _copy_source_tree, _download_http_url, @@ -65,49 +63,6 @@ def _fake_session_get(*args, **kwargs): rmtree(temp_dir) -def get_user_agent(): - return PipSession().headers["User-Agent"] - - -def test_user_agent(): - user_agent = get_user_agent() - - assert user_agent.startswith("pip/%s" % pip.__version__) - - -@pytest.mark.parametrize('name, expected_like_ci', [ - ('BUILD_BUILDID', True), - ('BUILD_ID', True), - ('CI', True), - ('PIP_IS_CI', True), - # Test a prefix substring of one of the variable names we use. - ('BUILD', False), -]) -def test_user_agent__ci(monkeypatch, name, expected_like_ci): - # Delete the variable names we use to check for CI to prevent the - # detection from always returning True in case the tests are being run - # under actual CI. It is okay to depend on CI_ENVIRONMENT_VARIABLES - # here (part of the code under test) because this setup step can only - # prevent false test failures. It can't cause a false test passage. - for ci_name in CI_ENVIRONMENT_VARIABLES: - monkeypatch.delenv(ci_name, raising=False) - - # Confirm the baseline before setting the environment variable. - user_agent = get_user_agent() - assert '"ci":null' in user_agent - assert '"ci":true' not in user_agent - - monkeypatch.setenv(name, 'true') - user_agent = get_user_agent() - assert ('"ci":true' in user_agent) == expected_like_ci - assert ('"ci":null' in user_agent) == (not expected_like_ci) - - -def test_user_agent_user_data(monkeypatch): - monkeypatch.setenv("PIP_USER_AGENT_USER_DATA", "some_string") - assert "some_string" in PipSession().headers["User-Agent"] - - class FakeStream(object): def __init__(self, contents): diff --git a/tests/unit/test_network_session.py b/tests/unit/test_network_session.py new file mode 100644 index 00000000000..8009fd9fcf7 --- /dev/null +++ b/tests/unit/test_network_session.py @@ -0,0 +1,48 @@ +import pytest + +import pip +from pip._internal.download import PipSession +from pip._internal.network.requests import CI_ENVIRONMENT_VARIABLES + + +def get_user_agent(): + return PipSession().headers["User-Agent"] + + +def test_user_agent(): + user_agent = get_user_agent() + + assert user_agent.startswith("pip/%s" % pip.__version__) + + +@pytest.mark.parametrize('name, expected_like_ci', [ + ('BUILD_BUILDID', True), + ('BUILD_ID', True), + ('CI', True), + ('PIP_IS_CI', True), + # Test a prefix substring of one of the variable names we use. + ('BUILD', False), +]) +def test_user_agent__ci(monkeypatch, name, expected_like_ci): + # Delete the variable names we use to check for CI to prevent the + # detection from always returning True in case the tests are being run + # under actual CI. It is okay to depend on CI_ENVIRONMENT_VARIABLES + # here (part of the code under test) because this setup step can only + # prevent false test failures. It can't cause a false test passage. + for ci_name in CI_ENVIRONMENT_VARIABLES: + monkeypatch.delenv(ci_name, raising=False) + + # Confirm the baseline before setting the environment variable. + user_agent = get_user_agent() + assert '"ci":null' in user_agent + assert '"ci":true' not in user_agent + + monkeypatch.setenv(name, 'true') + user_agent = get_user_agent() + assert ('"ci":true' in user_agent) == expected_like_ci + assert ('"ci":null' in user_agent) == (not expected_like_ci) + + +def test_user_agent_user_data(monkeypatch): + monkeypatch.setenv("PIP_USER_AGENT_USER_DATA", "some_string") + assert "some_string" in PipSession().headers["User-Agent"] From e7bd10115ecded1ce654774330119eff79558f20 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Fri, 27 Sep 2019 01:19:02 +0530 Subject: [PATCH 0408/3170] Start returning metadata directory from metadata generators Why: Since the result is a single directory and it's better for the resposibility of computing the exact location of the metadata directory should be with the generator, that generates it. --- .../_internal/operations/generate_metadata.py | 17 +++++++++++++---- src/pip/_internal/req/req_install.py | 3 ++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/operations/generate_metadata.py b/src/pip/_internal/operations/generate_metadata.py index 5788b73cd64..d0de351b63f 100644 --- a/src/pip/_internal/operations/generate_metadata.py +++ b/src/pip/_internal/operations/generate_metadata.py @@ -16,7 +16,13 @@ def get_metadata_generator(install_req): - # type: (InstallRequirement) -> Callable[[InstallRequirement], None] + # type: (InstallRequirement) -> Callable[[InstallRequirement], str] + """Return a callable metadata generator for this InstallRequirement. + + A metadata generator takes an InstallRequirement (install_req) as an input, + generates metadata via the appropriate process for that install_req and + returns the generated metadata directory. + """ if not install_req.use_pep517: return _generate_metadata_legacy @@ -24,7 +30,7 @@ def get_metadata_generator(install_req): def _generate_metadata_legacy(install_req): - # type: (InstallRequirement) -> None + # type: (InstallRequirement) -> str req_details_str = install_req.name or "from {}".format(install_req.link) logger.debug( 'Running setup.py (path:%s) egg_info for package %s', @@ -56,7 +62,10 @@ def _generate_metadata_legacy(install_req): command_desc='python setup.py egg_info', ) + # Return the metadata directory. + return install_req.egg_info_path + def _generate_metadata(install_req): - # type: (InstallRequirement) -> None - install_req.prepare_pep517_metadata() + # type: (InstallRequirement) -> str + return install_req.prepare_pep517_metadata() diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 659a494e94e..c7d8557ac61 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -600,7 +600,7 @@ def cleanup(self): self._temp_dir.cleanup() def prepare_pep517_metadata(self): - # type: () -> None + # type: () -> str assert self.pep517_backend is not None # NOTE: This needs to be refactored to stop using atexit @@ -624,6 +624,7 @@ def prepare_pep517_metadata(self): ) self.metadata_directory = os.path.join(metadata_dir, distinfo_dir) + return self.metadata_directory @property def egg_info_path(self): From 26fd8f24b060d50362e63c52a9dbc83da2bff644 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Fri, 27 Sep 2019 02:18:41 +0530 Subject: [PATCH 0409/3170] Always set InstallRequirement.metadata_directory Why: expecting metadata_directory to exist, once metadata is generated, is easier to reason about, than considering that some kinds of distributions set it while others do not. --- src/pip/_internal/operations/generate_metadata.py | 2 +- src/pip/_internal/req/req_install.py | 14 ++++---------- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/src/pip/_internal/operations/generate_metadata.py b/src/pip/_internal/operations/generate_metadata.py index d0de351b63f..9101bd77da8 100644 --- a/src/pip/_internal/operations/generate_metadata.py +++ b/src/pip/_internal/operations/generate_metadata.py @@ -63,7 +63,7 @@ def _generate_metadata_legacy(install_req): ) # Return the metadata directory. - return install_req.egg_info_path + return install_req.find_egg_info() def _generate_metadata(install_req): diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index c7d8557ac61..0da0af39fd6 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -568,7 +568,7 @@ def prepare_metadata(self): metadata_generator = get_metadata_generator(self) with indent_log(): - metadata_generator(self) + self.metadata_directory = metadata_generator(self) if not self.req: if isinstance(parse_version(self.metadata["Version"]), Version): @@ -623,15 +623,10 @@ def prepare_pep517_metadata(self): metadata_dir ) - self.metadata_directory = os.path.join(metadata_dir, distinfo_dir) - return self.metadata_directory + return os.path.join(metadata_dir, distinfo_dir) - @property - def egg_info_path(self): + def find_egg_info(self): # type: () -> str - if self._egg_info_path is not None: - return self._egg_info_path - def looks_like_virtual_env(path): return ( os.path.lexists(os.path.join(path, 'bin', 'python')) or @@ -680,8 +675,7 @@ def depth_of_directory(dir_): if len(filenames) > 1: filenames.sort(key=depth_of_directory) - self._egg_info_path = os.path.join(base, filenames[0]) - return self._egg_info_path + return os.path.join(base, filenames[0]) @property def metadata(self): From b01f301c759ec06d4f0d8e502c1e759ed9cee859 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Fri, 27 Sep 2019 02:22:25 +0530 Subject: [PATCH 0410/3170] Update get_dist to directly use metadata_directory --- src/pip/_internal/req/req_install.py | 16 ++++++++-------- tests/unit/test_req.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 0da0af39fd6..b14f8e7ccd7 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -688,16 +688,16 @@ def metadata(self): def get_dist(self): # type: () -> Distribution """Return a pkg_resources.Distribution for this requirement""" - if self.metadata_directory: - dist_dir = self.metadata_directory - dist_cls = pkg_resources.DistInfoDistribution + dist_dir = self.metadata_directory.rstrip(os.sep) + + # Determine the correct Distribution object type. + if dist_dir.endswith(".egg-info"): + dist_cls = pkg_resources.Distribution else: - dist_dir = self.egg_info_path.rstrip(os.path.sep) - # https://github.com/python/mypy/issues/1174 - dist_cls = pkg_resources.Distribution # type: ignore + assert dist_dir.endswith(".dist-info") + dist_cls = pkg_resources.DistInfoDistribution - # dist_dir_name can be of the form "<project>.dist-info" or - # e.g. "<project>.egg-info". + # Build a PathMetadata object, from path to metadata. :wink: base_dir, dist_dir_name = os.path.split(dist_dir) dist_name = os.path.splitext(dist_dir_name)[0] metadata = pkg_resources.PathMetadata(base_dir, dist_dir) diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index 2635a580024..54d689878aa 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -438,7 +438,7 @@ def test_url_preserved_editable_req(self): )) def test_get_dist(self, path): req = install_req_from_line('foo') - req._egg_info_path = path + req.metadata_directory = path dist = req.get_dist() assert isinstance(dist, pkg_resources.Distribution) assert dist.project_name == 'foo' From ec5e9d30d3809c66cdab075462cca86fc68f3d55 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Fri, 27 Sep 2019 02:25:35 +0530 Subject: [PATCH 0411/3170] Improve move_to_correct_build_directory Why: Changes made are IMO much clearer, with a step by step flow to this method that is easier to reason about. --- src/pip/_internal/req/req_install.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index b14f8e7ccd7..56680551f83 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -121,7 +121,6 @@ def __init__( markers = req.marker self.markers = markers - self._egg_info_path = None # type: Optional[str] # This holds the pkg_resources.Distribution object if this requirement # is already available: self.satisfied_by = None @@ -367,29 +366,34 @@ def move_to_correct_build_directory(self): return assert self.req is not None assert self._temp_build_dir - assert (self._ideal_build_dir is not None and - self._ideal_build_dir.path) # type: ignore + assert ( + self._ideal_build_dir is not None and + self._ideal_build_dir.path # type: ignore + ) old_location = self._temp_build_dir - self._temp_build_dir = None + self._temp_build_dir = None # checked inside ensure_build_location + # Figure out the correct place to put the files. new_location = self.ensure_build_location(self._ideal_build_dir) if os.path.exists(new_location): raise InstallationError( 'A package already exists in %s; please remove it to continue' - % display_path(new_location)) + % display_path(new_location) + ) + + # Move the files to the correct location. logger.debug( 'Moving package %s from %s to new location %s', self, display_path(old_location.path), display_path(new_location), ) shutil.move(old_location.path, new_location) + + # Update directory-tracking variables, to be in line with new_location + self.source_dir = os.path.normpath(os.path.abspath(new_location)) self._temp_build_dir = TempDirectory( path=new_location, kind="req-install", ) - self._ideal_build_dir = None - self.source_dir = os.path.normpath(os.path.abspath(new_location)) - self._egg_info_path = None - # Correct the metadata directory, if it exists if self.metadata_directory: old_meta = self.metadata_directory @@ -398,6 +402,11 @@ def move_to_correct_build_directory(self): new_meta = os.path.normpath(os.path.abspath(new_meta)) self.metadata_directory = new_meta + # Done with any "move built files" work, since have moved files to the + # "ideal" build location. Setting to None allows to clearly flag that + # no more moves are needed. + self._ideal_build_dir = None + def remove_temporary_source(self): # type: () -> None """Remove the source files from this requirement, if they are marked From cf7ebdbbc24f64ffacc63f6f808a1e240fe50df4 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 26 Sep 2019 18:39:53 -0400 Subject: [PATCH 0412/3170] Move PipSession to network.session --- src/pip/_internal/cli/req_command.py | 2 +- src/pip/_internal/collector.py | 2 +- src/pip/_internal/download.py | 267 +---------------------- src/pip/_internal/legacy_resolve.py | 2 +- src/pip/_internal/network/session.py | 278 +++++++++++++++++++++++- src/pip/_internal/operations/prepare.py | 2 +- src/pip/_internal/req/req_file.py | 2 +- src/pip/_internal/utils/outdated.py | 3 +- tests/lib/__init__.py | 2 +- tests/unit/test_collector.py | 2 +- tests/unit/test_download.py | 173 +-------------- tests/unit/test_index.py | 2 +- tests/unit/test_network_session.py | 175 ++++++++++++++- tests/unit/test_req.py | 2 +- tests/unit/test_req_file.py | 2 +- tests/unit/test_unit_outdated.py | 2 +- 16 files changed, 467 insertions(+), 451 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 9a2d4196f6e..01f67012dcd 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -10,11 +10,11 @@ from pip._internal.cli.base_command import Command from pip._internal.cli.command_context import CommandContextMixIn -from pip._internal.download import PipSession from pip._internal.exceptions import CommandError from pip._internal.index import PackageFinder from pip._internal.legacy_resolve import Resolver from pip._internal.models.selection_prefs import SelectionPreferences +from pip._internal.network.session import PipSession from pip._internal.operations.prepare import RequirementPreparer from pip._internal.req.constructors import ( install_req_from_editable, diff --git a/src/pip/_internal/collector.py b/src/pip/_internal/collector.py index c96cc711967..5b3c88eb5ac 100644 --- a/src/pip/_internal/collector.py +++ b/src/pip/_internal/collector.py @@ -32,7 +32,7 @@ from pip._vendor.requests import Response from pip._internal.models.search_scope import SearchScope - from pip._internal.download import PipSession + from pip._internal.network.session import PipSession HTMLElement = xml.etree.ElementTree.Element ResponseHeaders = MutableMapping[str, str] diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index ac5c393c785..53d53458619 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -1,7 +1,6 @@ from __future__ import absolute_import import cgi -import email.utils import logging import mimetypes import os @@ -9,11 +8,8 @@ import shutil import sys -from pip._vendor import requests, six, urllib3 -from pip._vendor.cachecontrol import CacheControlAdapter -from pip._vendor.requests.adapters import BaseAdapter, HTTPAdapter +from pip._vendor import requests from pip._vendor.requests.models import CONTENT_CHUNK_SIZE, Response -from pip._vendor.requests.structures import CaseInsensitiveDict from pip._vendor.six import PY2 # NOTE: XMLRPC Client is not annotated in typeshed as on 2017-07-17, which is # why we ignore the type on this import @@ -22,22 +18,16 @@ from pip._internal.exceptions import HashMismatch, InstallationError from pip._internal.models.index import PyPI -from pip._internal.network.auth import MultiDomainBasicAuth -from pip._internal.network.cache import SafeFileCache -from pip._internal.network.session import SECURE_ORIGINS, user_agent -# Import ssl from compat so the initial import occurs in only one place. -from pip._internal.utils.compat import ipaddress +from pip._internal.network.session import PipSession from pip._internal.utils.encoding import auto_decode -from pip._internal.utils.filesystem import check_path_owner, copy2_fixed +from pip._internal.utils.filesystem import copy2_fixed from pip._internal.utils.misc import ( ask_path_exists, backup_dir, - build_url_from_netloc, consume, display_path, format_size, hide_url, - parse_netloc, path_to_display, rmtree, splitext, @@ -46,18 +36,17 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.ui import DownloadProgressProvider from pip._internal.utils.unpacking import unpack_file -from pip._internal.utils.urls import get_url_scheme, url_to_path +from pip._internal.utils.urls import get_url_scheme from pip._internal.vcs import vcs if MYPY_CHECK_RUNNING: from typing import ( - IO, Callable, Iterator, List, Optional, Text, Tuple, + IO, Callable, List, Optional, Text, Tuple, ) from mypy_extensions import TypedDict from pip._internal.models.link import Link - from pip._internal.network.requests import SecureOrigin from pip._internal.utils.hashes import Hashes from pip._internal.vcs.versioncontrol import VersionControl @@ -93,252 +82,6 @@ logger = logging.getLogger(__name__) -class LocalFSAdapter(BaseAdapter): - - def send(self, request, stream=None, timeout=None, verify=None, cert=None, - proxies=None): - pathname = url_to_path(request.url) - - resp = Response() - resp.status_code = 200 - resp.url = request.url - - try: - stats = os.stat(pathname) - except OSError as exc: - resp.status_code = 404 - resp.raw = exc - else: - modified = email.utils.formatdate(stats.st_mtime, usegmt=True) - content_type = mimetypes.guess_type(pathname)[0] or "text/plain" - resp.headers = CaseInsensitiveDict({ - "Content-Type": content_type, - "Content-Length": stats.st_size, - "Last-Modified": modified, - }) - - resp.raw = open(pathname, "rb") - resp.close = resp.raw.close - - return resp - - def close(self): - pass - - -class InsecureHTTPAdapter(HTTPAdapter): - - def cert_verify(self, conn, url, verify, cert): - conn.cert_reqs = 'CERT_NONE' - conn.ca_certs = None - - -class PipSession(requests.Session): - - timeout = None # type: Optional[int] - - def __init__(self, *args, **kwargs): - """ - :param trusted_hosts: Domains not to emit warnings for when not using - HTTPS. - """ - retries = kwargs.pop("retries", 0) - cache = kwargs.pop("cache", None) - trusted_hosts = kwargs.pop("trusted_hosts", []) # type: List[str] - index_urls = kwargs.pop("index_urls", None) - - super(PipSession, self).__init__(*args, **kwargs) - - # Namespace the attribute with "pip_" just in case to prevent - # possible conflicts with the base class. - self.pip_trusted_origins = [] # type: List[Tuple[str, Optional[int]]] - - # Attach our User Agent to the request - self.headers["User-Agent"] = user_agent() - - # Attach our Authentication handler to the session - self.auth = MultiDomainBasicAuth(index_urls=index_urls) - - # Create our urllib3.Retry instance which will allow us to customize - # how we handle retries. - retries = urllib3.Retry( - # Set the total number of retries that a particular request can - # have. - total=retries, - - # A 503 error from PyPI typically means that the Fastly -> Origin - # 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. - # 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. - backoff_factor=0.25, - ) - - # Check to ensure that the directory containing our cache directory - # is owned by the user current executing pip. If it does not exist - # we will check the parent directory until we find one that does exist. - if cache and not check_path_owner(cache): - logger.warning( - "The directory '%s' or its parent directory is not owned by " - "the current user and the cache has been disabled. Please " - "check the permissions and owner of that directory. If " - "executing pip with sudo, you may want sudo's -H flag.", - cache, - ) - cache = None - - # We want to _only_ cache responses on securely fetched origins. We do - # this because we can't validate the response of an insecurely fetched - # origin, and we don't want someone to be able to poison the cache and - # require manual eviction from the cache to fix it. - if cache: - secure_adapter = CacheControlAdapter( - cache=SafeFileCache(cache), - max_retries=retries, - ) - else: - secure_adapter = HTTPAdapter(max_retries=retries) - - # Our Insecure HTTPAdapter disables HTTPS validation. It does not - # support caching (see above) so we'll use it for all http:// URLs as - # well as any https:// host that we've marked as ignoring TLS errors - # for. - insecure_adapter = InsecureHTTPAdapter(max_retries=retries) - # Save this for later use in add_insecure_host(). - self._insecure_adapter = insecure_adapter - - self.mount("https://", secure_adapter) - self.mount("http://", insecure_adapter) - - # Enable file:// urls - self.mount("file://", LocalFSAdapter()) - - for host in trusted_hosts: - self.add_trusted_host(host, suppress_logging=True) - - def add_trusted_host(self, host, source=None, suppress_logging=False): - # type: (str, Optional[str], bool) -> None - """ - :param host: It is okay to provide a host that has previously been - added. - :param source: An optional source string, for logging where the host - string came from. - """ - if not suppress_logging: - msg = 'adding trusted host: {!r}'.format(host) - if source is not None: - msg += ' (from {})'.format(source) - logger.info(msg) - - host_port = parse_netloc(host) - if host_port not in self.pip_trusted_origins: - self.pip_trusted_origins.append(host_port) - - self.mount(build_url_from_netloc(host) + '/', self._insecure_adapter) - if not host_port[1]: - # Mount wildcard ports for the same host. - self.mount( - build_url_from_netloc(host) + ':', - self._insecure_adapter - ) - - def iter_secure_origins(self): - # type: () -> Iterator[SecureOrigin] - for secure_origin in SECURE_ORIGINS: - yield secure_origin - for host, port in self.pip_trusted_origins: - yield ('*', host, '*' if port is None else port) - - def is_secure_origin(self, location): - # type: (Link) -> bool - # Determine if this url used a secure transport mechanism - parsed = urllib_parse.urlparse(str(location)) - origin_protocol, origin_host, origin_port = ( - parsed.scheme, parsed.hostname, parsed.port, - ) - - # The protocol to use to see if the protocol matches. - # Don't count the repository type as part of the protocol: in - # cases such as "git+ssh", only use "ssh". (I.e., Only verify against - # the last scheme.) - origin_protocol = origin_protocol.rsplit('+', 1)[-1] - - # Determine if our origin is a secure origin by looking through our - # hardcoded list of secure origins, as well as any additional ones - # configured on this PackageFinder instance. - for secure_origin in self.iter_secure_origins(): - secure_protocol, secure_host, secure_port = secure_origin - if origin_protocol != secure_protocol and secure_protocol != "*": - continue - - try: - # We need to do this decode dance to ensure that we have a - # unicode object, even on Python 2.x. - addr = ipaddress.ip_address( - origin_host - if ( - isinstance(origin_host, six.text_type) or - origin_host is None - ) - else origin_host.decode("utf8") - ) - network = ipaddress.ip_network( - secure_host - if isinstance(secure_host, six.text_type) - # setting secure_host to proper Union[bytes, str] - # creates problems in other places - else secure_host.decode("utf8") # type: ignore - ) - except ValueError: - # We don't have both a valid address or a valid network, so - # we'll check this origin against hostnames. - if (origin_host and - origin_host.lower() != secure_host.lower() and - secure_host != "*"): - continue - else: - # We have a valid address and network, so see if the address - # is contained within the network. - if addr not in network: - continue - - # Check to see if the port matches. - if (origin_port != secure_port and - secure_port != "*" and - secure_port is not None): - continue - - # If we've gotten here, then this origin matches the current - # secure origin and we should return True - return True - - # If we've gotten to this point, then the origin isn't secure and we - # will not accept it as a valid location to search. We will however - # 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 we " - "recommend you use HTTPS instead, otherwise you may silence " - "this warning and allow it anyway with '--trusted-host %s'.", - origin_host, - origin_host, - ) - - return False - - def request(self, method, url, *args, **kwargs): - # Allow setting a default timeout on a session - kwargs.setdefault("timeout", self.timeout) - - # Dispatch the actual request - return super(PipSession, self).request(method, url, *args, **kwargs) - - def get_file_content(url, comes_from=None, session=None): # type: (str, Optional[str], Optional[PipSession]) -> Tuple[str, Text] """Gets the content of a file; it may be a filename, file: URL, or diff --git a/src/pip/_internal/legacy_resolve.py b/src/pip/_internal/legacy_resolve.py index 7f39e257265..84855d73a39 100644 --- a/src/pip/_internal/legacy_resolve.py +++ b/src/pip/_internal/legacy_resolve.py @@ -44,7 +44,7 @@ from pip._vendor import pkg_resources from pip._internal.distributions import AbstractDistribution - from pip._internal.download import PipSession + from pip._internal.network.session import PipSession from pip._internal.index import PackageFinder from pip._internal.operations.prepare import RequirementPreparer from pip._internal.req.req_install import InstallRequirement diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py index 096c7a271e5..57c8eb01c99 100644 --- a/src/pip/_internal/network/session.py +++ b/src/pip/_internal/network/session.py @@ -1,20 +1,46 @@ +import email.utils import json +import logging +import mimetypes import os import platform import sys +from pip._vendor import requests, six, urllib3 +from pip._vendor.cachecontrol import CacheControlAdapter +from pip._vendor.requests.adapters import BaseAdapter, HTTPAdapter +from pip._vendor.requests.models import Response +from pip._vendor.requests.structures import CaseInsensitiveDict +from pip._vendor.six.moves.urllib import parse as urllib_parse + import pip -from pip._internal.utils.compat import HAS_TLS, ssl +from pip._internal.network.auth import MultiDomainBasicAuth +from pip._internal.network.cache import SafeFileCache +# Import ssl from compat so the initial import occurs in only one place. +from pip._internal.utils.compat import HAS_TLS, ipaddress, ssl +from pip._internal.utils.filesystem import check_path_owner from pip._internal.utils.glibc import libc_ver -from pip._internal.utils.misc import get_installed_version +from pip._internal.utils.misc import ( + build_url_from_netloc, + get_installed_version, + parse_netloc, +) from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from pip._internal.utils.urls import url_to_path if MYPY_CHECK_RUNNING: - from typing import List, Optional, Tuple, Union + from typing import ( + Iterator, List, Optional, Tuple, Union, + ) + + from pip._internal.models.link import Link SecureOrigin = Tuple[str, str, Optional[Union[int, str]]] +logger = logging.getLogger(__name__) + + SECURE_ORIGINS = [ # protocol, hostname, port # Taken from Chrome's list of secure origins (See: http://bit.ly/1qrySKC) @@ -135,3 +161,249 @@ def user_agent(): data=data, json=json.dumps(data, separators=(",", ":"), sort_keys=True), ) + + +class LocalFSAdapter(BaseAdapter): + + def send(self, request, stream=None, timeout=None, verify=None, cert=None, + proxies=None): + pathname = url_to_path(request.url) + + resp = Response() + resp.status_code = 200 + resp.url = request.url + + try: + stats = os.stat(pathname) + except OSError as exc: + resp.status_code = 404 + resp.raw = exc + else: + modified = email.utils.formatdate(stats.st_mtime, usegmt=True) + content_type = mimetypes.guess_type(pathname)[0] or "text/plain" + resp.headers = CaseInsensitiveDict({ + "Content-Type": content_type, + "Content-Length": stats.st_size, + "Last-Modified": modified, + }) + + resp.raw = open(pathname, "rb") + resp.close = resp.raw.close + + return resp + + def close(self): + pass + + +class InsecureHTTPAdapter(HTTPAdapter): + + def cert_verify(self, conn, url, verify, cert): + conn.cert_reqs = 'CERT_NONE' + conn.ca_certs = None + + +class PipSession(requests.Session): + + timeout = None # type: Optional[int] + + def __init__(self, *args, **kwargs): + """ + :param trusted_hosts: Domains not to emit warnings for when not using + HTTPS. + """ + retries = kwargs.pop("retries", 0) + cache = kwargs.pop("cache", None) + trusted_hosts = kwargs.pop("trusted_hosts", []) # type: List[str] + index_urls = kwargs.pop("index_urls", None) + + super(PipSession, self).__init__(*args, **kwargs) + + # Namespace the attribute with "pip_" just in case to prevent + # possible conflicts with the base class. + self.pip_trusted_origins = [] # type: List[Tuple[str, Optional[int]]] + + # Attach our User Agent to the request + self.headers["User-Agent"] = user_agent() + + # Attach our Authentication handler to the session + self.auth = MultiDomainBasicAuth(index_urls=index_urls) + + # Create our urllib3.Retry instance which will allow us to customize + # how we handle retries. + retries = urllib3.Retry( + # Set the total number of retries that a particular request can + # have. + total=retries, + + # A 503 error from PyPI typically means that the Fastly -> Origin + # 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. + # 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. + backoff_factor=0.25, + ) + + # Check to ensure that the directory containing our cache directory + # is owned by the user current executing pip. If it does not exist + # we will check the parent directory until we find one that does exist. + if cache and not check_path_owner(cache): + logger.warning( + "The directory '%s' or its parent directory is not owned by " + "the current user and the cache has been disabled. Please " + "check the permissions and owner of that directory. If " + "executing pip with sudo, you may want sudo's -H flag.", + cache, + ) + cache = None + + # We want to _only_ cache responses on securely fetched origins. We do + # this because we can't validate the response of an insecurely fetched + # origin, and we don't want someone to be able to poison the cache and + # require manual eviction from the cache to fix it. + if cache: + secure_adapter = CacheControlAdapter( + cache=SafeFileCache(cache), + max_retries=retries, + ) + else: + secure_adapter = HTTPAdapter(max_retries=retries) + + # Our Insecure HTTPAdapter disables HTTPS validation. It does not + # support caching (see above) so we'll use it for all http:// URLs as + # well as any https:// host that we've marked as ignoring TLS errors + # for. + insecure_adapter = InsecureHTTPAdapter(max_retries=retries) + # Save this for later use in add_insecure_host(). + self._insecure_adapter = insecure_adapter + + self.mount("https://", secure_adapter) + self.mount("http://", insecure_adapter) + + # Enable file:// urls + self.mount("file://", LocalFSAdapter()) + + for host in trusted_hosts: + self.add_trusted_host(host, suppress_logging=True) + + def add_trusted_host(self, host, source=None, suppress_logging=False): + # type: (str, Optional[str], bool) -> None + """ + :param host: It is okay to provide a host that has previously been + added. + :param source: An optional source string, for logging where the host + string came from. + """ + if not suppress_logging: + msg = 'adding trusted host: {!r}'.format(host) + if source is not None: + msg += ' (from {})'.format(source) + logger.info(msg) + + host_port = parse_netloc(host) + if host_port not in self.pip_trusted_origins: + self.pip_trusted_origins.append(host_port) + + self.mount(build_url_from_netloc(host) + '/', self._insecure_adapter) + if not host_port[1]: + # Mount wildcard ports for the same host. + self.mount( + build_url_from_netloc(host) + ':', + self._insecure_adapter + ) + + def iter_secure_origins(self): + # type: () -> Iterator[SecureOrigin] + for secure_origin in SECURE_ORIGINS: + yield secure_origin + for host, port in self.pip_trusted_origins: + yield ('*', host, '*' if port is None else port) + + def is_secure_origin(self, location): + # type: (Link) -> bool + # Determine if this url used a secure transport mechanism + parsed = urllib_parse.urlparse(str(location)) + origin_protocol, origin_host, origin_port = ( + parsed.scheme, parsed.hostname, parsed.port, + ) + + # The protocol to use to see if the protocol matches. + # Don't count the repository type as part of the protocol: in + # cases such as "git+ssh", only use "ssh". (I.e., Only verify against + # the last scheme.) + origin_protocol = origin_protocol.rsplit('+', 1)[-1] + + # Determine if our origin is a secure origin by looking through our + # hardcoded list of secure origins, as well as any additional ones + # configured on this PackageFinder instance. + for secure_origin in self.iter_secure_origins(): + secure_protocol, secure_host, secure_port = secure_origin + if origin_protocol != secure_protocol and secure_protocol != "*": + continue + + try: + # We need to do this decode dance to ensure that we have a + # unicode object, even on Python 2.x. + addr = ipaddress.ip_address( + origin_host + if ( + isinstance(origin_host, six.text_type) or + origin_host is None + ) + else origin_host.decode("utf8") + ) + network = ipaddress.ip_network( + secure_host + if isinstance(secure_host, six.text_type) + # setting secure_host to proper Union[bytes, str] + # creates problems in other places + else secure_host.decode("utf8") # type: ignore + ) + except ValueError: + # We don't have both a valid address or a valid network, so + # we'll check this origin against hostnames. + if (origin_host and + origin_host.lower() != secure_host.lower() and + secure_host != "*"): + continue + else: + # We have a valid address and network, so see if the address + # is contained within the network. + if addr not in network: + continue + + # Check to see if the port matches. + if (origin_port != secure_port and + secure_port != "*" and + secure_port is not None): + continue + + # If we've gotten here, then this origin matches the current + # secure origin and we should return True + return True + + # If we've gotten to this point, then the origin isn't secure and we + # will not accept it as a valid location to search. We will however + # 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 we " + "recommend you use HTTPS instead, otherwise you may silence " + "this warning and allow it anyway with '--trusted-host %s'.", + origin_host, + origin_host, + ) + + return False + + def request(self, method, url, *args, **kwargs): + # Allow setting a default timeout on a session + kwargs.setdefault("timeout", self.timeout) + + # Dispatch the actual request + return super(PipSession, self).request(method, url, *args, **kwargs) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 43956bef9f6..18ae249ee1c 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -32,8 +32,8 @@ from typing import Optional from pip._internal.distributions import AbstractDistribution - from pip._internal.download import PipSession from pip._internal.index import PackageFinder + from pip._internal.network.session import PipSession from pip._internal.req.req_install import InstallRequirement from pip._internal.req.req_tracker import RequirementTracker diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index 1e4aa689d57..83b3d344cbb 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -33,7 +33,7 @@ from pip._internal.req import InstallRequirement from pip._internal.cache import WheelCache from pip._internal.index import PackageFinder - from pip._internal.download import PipSession + from pip._internal.network.session import PipSession ReqFileLines = Iterator[Tuple[int, Text]] diff --git a/src/pip/_internal/utils/outdated.py b/src/pip/_internal/utils/outdated.py index ca219c618a9..42e27279382 100644 --- a/src/pip/_internal/utils/outdated.py +++ b/src/pip/_internal/utils/outdated.py @@ -33,7 +33,8 @@ import optparse from optparse import Values from typing import Any, Dict, Text, Union - from pip._internal.download import PipSession + + from pip._internal.network.session import PipSession SELFCHECK_DATE_FMT = "%Y-%m-%dT%H:%M:%SZ" diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index 7e59911b72e..576cc6edd7b 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -14,11 +14,11 @@ from scripttest import FoundDir, TestFileEnvironment from pip._internal.collector import LinkCollector -from pip._internal.download import PipSession from pip._internal.index import PackageFinder from pip._internal.locations import get_major_minor_version from pip._internal.models.search_scope import SearchScope from pip._internal.models.selection_prefs import SelectionPreferences +from pip._internal.network.session import PipSession from pip._internal.utils.deprecation import DEPRECATION_MSG_PREFIX from pip._internal.utils.typing import MYPY_CHECK_RUNNING from tests.lib.path import Path, curdir diff --git a/tests/unit/test_collector.py b/tests/unit/test_collector.py index a1bfdbb654a..c85c121268d 100644 --- a/tests/unit/test_collector.py +++ b/tests/unit/test_collector.py @@ -22,9 +22,9 @@ group_locations, parse_links, ) -from pip._internal.download import PipSession from pip._internal.models.index import PyPI from pip._internal.models.link import Link +from pip._internal.network.session import PipSession from tests.lib import make_test_link_collector diff --git a/tests/unit/test_download.py b/tests/unit/test_download.py index f2865bbae85..338b2bbad40 100644 --- a/tests/unit/test_download.py +++ b/tests/unit/test_download.py @@ -1,5 +1,4 @@ import hashlib -import logging import os import shutil import sys @@ -11,7 +10,6 @@ from mock import Mock, patch from pip._internal.download import ( - PipSession, _copy_source_tree, _download_http_url, parse_content_disposition, @@ -21,6 +19,7 @@ ) from pip._internal.exceptions import HashMismatch from pip._internal.models.link import Link +from pip._internal.network.session import PipSession from pip._internal.utils.hashes import Hashes from pip._internal.utils.urls import path_to_url from tests.lib import create_file @@ -453,173 +452,3 @@ def test_unpack_file_url_excludes_expected_dirs(tmpdir, exclude_dir): assert not os.path.isfile(dst_excluded_file) assert os.path.isfile(dst_included_file) assert os.path.isdir(dst_included_dir) - - -class TestPipSession: - - def test_cache_defaults_off(self): - session = PipSession() - - assert not hasattr(session.adapters["http://"], "cache") - assert not hasattr(session.adapters["https://"], "cache") - - def test_cache_is_enabled(self, tmpdir): - session = PipSession(cache=tmpdir.joinpath("test-cache")) - - assert hasattr(session.adapters["https://"], "cache") - - assert (session.adapters["https://"].cache.directory == - tmpdir.joinpath("test-cache")) - - def test_http_cache_is_not_enabled(self, tmpdir): - session = PipSession(cache=tmpdir.joinpath("test-cache")) - - assert not hasattr(session.adapters["http://"], "cache") - - def test_insecure_host_adapter(self, tmpdir): - session = PipSession( - cache=tmpdir.joinpath("test-cache"), - trusted_hosts=["example.com"], - ) - - assert "https://example.com/" in session.adapters - # Check that the "port wildcard" is present. - assert "https://example.com:" in session.adapters - # Check that the cache isn't enabled. - assert not hasattr(session.adapters["https://example.com/"], "cache") - - def test_add_trusted_host(self): - # Leave a gap to test how the ordering is affected. - trusted_hosts = ['host1', 'host3'] - session = PipSession(trusted_hosts=trusted_hosts) - insecure_adapter = session._insecure_adapter - prefix2 = 'https://host2/' - prefix3 = 'https://host3/' - prefix3_wildcard = 'https://host3:' - - # Confirm some initial conditions as a baseline. - assert session.pip_trusted_origins == [ - ('host1', None), ('host3', None) - ] - assert session.adapters[prefix3] is insecure_adapter - assert session.adapters[prefix3_wildcard] is insecure_adapter - - assert prefix2 not in session.adapters - - # Test adding a new host. - session.add_trusted_host('host2') - assert session.pip_trusted_origins == [ - ('host1', None), ('host3', None), ('host2', None) - ] - # Check that prefix3 is still present. - assert session.adapters[prefix3] is insecure_adapter - assert session.adapters[prefix2] is insecure_adapter - - # Test that adding the same host doesn't create a duplicate. - session.add_trusted_host('host3') - assert session.pip_trusted_origins == [ - ('host1', None), ('host3', None), ('host2', None) - ], 'actual: {}'.format(session.pip_trusted_origins) - - session.add_trusted_host('host4:8080') - prefix4 = 'https://host4:8080/' - assert session.pip_trusted_origins == [ - ('host1', None), ('host3', None), - ('host2', None), ('host4', 8080) - ] - assert session.adapters[prefix4] is insecure_adapter - - def test_add_trusted_host__logging(self, caplog): - """ - Test logging when add_trusted_host() is called. - """ - trusted_hosts = ['host0', 'host1'] - session = PipSession(trusted_hosts=trusted_hosts) - with caplog.at_level(logging.INFO): - # Test adding an existing host. - session.add_trusted_host('host1', source='somewhere') - session.add_trusted_host('host2') - # Test calling add_trusted_host() on the same host twice. - session.add_trusted_host('host2') - - actual = [(r.levelname, r.message) for r in caplog.records] - # Observe that "host0" isn't included in the logs. - expected = [ - ('INFO', "adding trusted host: 'host1' (from somewhere)"), - ('INFO', "adding trusted host: 'host2'"), - ('INFO', "adding trusted host: 'host2'"), - ] - assert actual == expected - - def test_iter_secure_origins(self): - trusted_hosts = ['host1', 'host2', 'host3:8080'] - session = PipSession(trusted_hosts=trusted_hosts) - - actual = list(session.iter_secure_origins()) - assert len(actual) == 9 - # Spot-check that SECURE_ORIGINS is included. - assert actual[0] == ('https', '*', '*') - assert actual[-3:] == [ - ('*', 'host1', '*'), - ('*', 'host2', '*'), - ('*', 'host3', 8080) - ] - - def test_iter_secure_origins__trusted_hosts_empty(self): - """ - Test iter_secure_origins() after passing trusted_hosts=[]. - """ - session = PipSession(trusted_hosts=[]) - - actual = list(session.iter_secure_origins()) - assert len(actual) == 6 - # Spot-check that SECURE_ORIGINS is included. - assert actual[0] == ('https', '*', '*') - - @pytest.mark.parametrize( - 'location, trusted, expected', - [ - ("http://pypi.org/something", [], False), - ("https://pypi.org/something", [], True), - ("git+http://pypi.org/something", [], False), - ("git+https://pypi.org/something", [], True), - ("git+ssh://git@pypi.org/something", [], True), - ("http://localhost", [], True), - ("http://127.0.0.1", [], True), - ("http://example.com/something/", [], False), - ("http://example.com/something/", ["example.com"], True), - # Try changing the case. - ("http://eXample.com/something/", ["example.cOm"], True), - # Test hosts with port. - ("http://example.com:8080/something/", ["example.com"], True), - # Test a trusted_host with a port. - ("http://example.com:8080/something/", ["example.com:8080"], True), - ("http://example.com/something/", ["example.com:8080"], False), - ( - "http://example.com:8888/something/", - ["example.com:8080"], - False - ), - ], - ) - def test_is_secure_origin(self, caplog, location, trusted, expected): - class MockLogger(object): - def __init__(self): - self.called = False - - def warning(self, *args, **kwargs): - self.called = True - - session = PipSession(trusted_hosts=trusted) - actual = session.is_secure_origin(location) - assert actual == expected - - log_records = [(r.levelname, r.message) for r in caplog.records] - if expected: - assert not log_records - return - - assert len(log_records) == 1 - actual_level, actual_message = log_records[0] - assert actual_level == 'WARNING' - assert 'is not a trusted or secure host' in actual_message diff --git a/tests/unit/test_index.py b/tests/unit/test_index.py index 47f3c67fcc8..63eac97b99e 100644 --- a/tests/unit/test_index.py +++ b/tests/unit/test_index.py @@ -4,7 +4,6 @@ from pip._vendor.packaging.specifiers import SpecifierSet from pip._internal.collector import LinkCollector -from pip._internal.download import PipSession from pip._internal.index import ( CandidateEvaluator, CandidatePreferences, @@ -21,6 +20,7 @@ from pip._internal.models.search_scope import SearchScope from pip._internal.models.selection_prefs import SelectionPreferences from pip._internal.models.target_python import TargetPython +from pip._internal.network.session import PipSession from pip._internal.pep425tags import get_supported from pip._internal.utils.hashes import Hashes from tests.lib import CURRENT_PY_VERSION_INFO diff --git a/tests/unit/test_network_session.py b/tests/unit/test_network_session.py index 8009fd9fcf7..c2cd15ad2ce 100644 --- a/tests/unit/test_network_session.py +++ b/tests/unit/test_network_session.py @@ -1,8 +1,9 @@ +import logging + import pytest import pip -from pip._internal.download import PipSession -from pip._internal.network.requests import CI_ENVIRONMENT_VARIABLES +from pip._internal.network.session import CI_ENVIRONMENT_VARIABLES, PipSession def get_user_agent(): @@ -46,3 +47,173 @@ def test_user_agent__ci(monkeypatch, name, expected_like_ci): def test_user_agent_user_data(monkeypatch): monkeypatch.setenv("PIP_USER_AGENT_USER_DATA", "some_string") assert "some_string" in PipSession().headers["User-Agent"] + + +class TestPipSession: + + def test_cache_defaults_off(self): + session = PipSession() + + assert not hasattr(session.adapters["http://"], "cache") + assert not hasattr(session.adapters["https://"], "cache") + + def test_cache_is_enabled(self, tmpdir): + session = PipSession(cache=tmpdir.joinpath("test-cache")) + + assert hasattr(session.adapters["https://"], "cache") + + assert (session.adapters["https://"].cache.directory == + tmpdir.joinpath("test-cache")) + + def test_http_cache_is_not_enabled(self, tmpdir): + session = PipSession(cache=tmpdir.joinpath("test-cache")) + + assert not hasattr(session.adapters["http://"], "cache") + + def test_insecure_host_adapter(self, tmpdir): + session = PipSession( + cache=tmpdir.joinpath("test-cache"), + trusted_hosts=["example.com"], + ) + + assert "https://example.com/" in session.adapters + # Check that the "port wildcard" is present. + assert "https://example.com:" in session.adapters + # Check that the cache isn't enabled. + assert not hasattr(session.adapters["https://example.com/"], "cache") + + def test_add_trusted_host(self): + # Leave a gap to test how the ordering is affected. + trusted_hosts = ['host1', 'host3'] + session = PipSession(trusted_hosts=trusted_hosts) + insecure_adapter = session._insecure_adapter + prefix2 = 'https://host2/' + prefix3 = 'https://host3/' + prefix3_wildcard = 'https://host3:' + + # Confirm some initial conditions as a baseline. + assert session.pip_trusted_origins == [ + ('host1', None), ('host3', None) + ] + assert session.adapters[prefix3] is insecure_adapter + assert session.adapters[prefix3_wildcard] is insecure_adapter + + assert prefix2 not in session.adapters + + # Test adding a new host. + session.add_trusted_host('host2') + assert session.pip_trusted_origins == [ + ('host1', None), ('host3', None), ('host2', None) + ] + # Check that prefix3 is still present. + assert session.adapters[prefix3] is insecure_adapter + assert session.adapters[prefix2] is insecure_adapter + + # Test that adding the same host doesn't create a duplicate. + session.add_trusted_host('host3') + assert session.pip_trusted_origins == [ + ('host1', None), ('host3', None), ('host2', None) + ], 'actual: {}'.format(session.pip_trusted_origins) + + session.add_trusted_host('host4:8080') + prefix4 = 'https://host4:8080/' + assert session.pip_trusted_origins == [ + ('host1', None), ('host3', None), + ('host2', None), ('host4', 8080) + ] + assert session.adapters[prefix4] is insecure_adapter + + def test_add_trusted_host__logging(self, caplog): + """ + Test logging when add_trusted_host() is called. + """ + trusted_hosts = ['host0', 'host1'] + session = PipSession(trusted_hosts=trusted_hosts) + with caplog.at_level(logging.INFO): + # Test adding an existing host. + session.add_trusted_host('host1', source='somewhere') + session.add_trusted_host('host2') + # Test calling add_trusted_host() on the same host twice. + session.add_trusted_host('host2') + + actual = [(r.levelname, r.message) for r in caplog.records] + # Observe that "host0" isn't included in the logs. + expected = [ + ('INFO', "adding trusted host: 'host1' (from somewhere)"), + ('INFO', "adding trusted host: 'host2'"), + ('INFO', "adding trusted host: 'host2'"), + ] + assert actual == expected + + def test_iter_secure_origins(self): + trusted_hosts = ['host1', 'host2', 'host3:8080'] + session = PipSession(trusted_hosts=trusted_hosts) + + actual = list(session.iter_secure_origins()) + assert len(actual) == 9 + # Spot-check that SECURE_ORIGINS is included. + assert actual[0] == ('https', '*', '*') + assert actual[-3:] == [ + ('*', 'host1', '*'), + ('*', 'host2', '*'), + ('*', 'host3', 8080) + ] + + def test_iter_secure_origins__trusted_hosts_empty(self): + """ + Test iter_secure_origins() after passing trusted_hosts=[]. + """ + session = PipSession(trusted_hosts=[]) + + actual = list(session.iter_secure_origins()) + assert len(actual) == 6 + # Spot-check that SECURE_ORIGINS is included. + assert actual[0] == ('https', '*', '*') + + @pytest.mark.parametrize( + 'location, trusted, expected', + [ + ("http://pypi.org/something", [], False), + ("https://pypi.org/something", [], True), + ("git+http://pypi.org/something", [], False), + ("git+https://pypi.org/something", [], True), + ("git+ssh://git@pypi.org/something", [], True), + ("http://localhost", [], True), + ("http://127.0.0.1", [], True), + ("http://example.com/something/", [], False), + ("http://example.com/something/", ["example.com"], True), + # Try changing the case. + ("http://eXample.com/something/", ["example.cOm"], True), + # Test hosts with port. + ("http://example.com:8080/something/", ["example.com"], True), + # Test a trusted_host with a port. + ("http://example.com:8080/something/", ["example.com:8080"], True), + ("http://example.com/something/", ["example.com:8080"], False), + ( + "http://example.com:8888/something/", + ["example.com:8080"], + False + ), + ], + ) + def test_is_secure_origin(self, caplog, location, trusted, expected): + class MockLogger(object): + def __init__(self): + self.called = False + + def warning(self, *args, **kwargs): + self.called = True + + session = PipSession(trusted_hosts=trusted) + actual = session.is_secure_origin(location) + assert actual == expected + + log_records = [(r.levelname, r.message) for r in caplog.records] + if expected: + assert not log_records + return + + assert len(log_records) == 1 + actual_level, actual_message = log_records[0] + assert actual_level == 'WARNING' + assert 'is not a trusted or secure host' in actual_message diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index 2635a580024..e86c7b5cb12 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -11,7 +11,6 @@ from pip._vendor.packaging.requirements import Requirement from pip._internal.commands import create_command -from pip._internal.download import PipSession from pip._internal.exceptions import ( HashErrors, InstallationError, @@ -19,6 +18,7 @@ PreviousBuildDirError, ) from pip._internal.legacy_resolve import Resolver +from pip._internal.network.session import PipSession from pip._internal.operations.prepare import RequirementPreparer from pip._internal.req import InstallRequirement, RequirementSet from pip._internal.req.constructors import ( diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py index 8584c11301a..0e0af2db2b8 100644 --- a/tests/unit/test_req_file.py +++ b/tests/unit/test_req_file.py @@ -8,12 +8,12 @@ from pretend import stub import pip._internal.index -from pip._internal.download import PipSession from pip._internal.exceptions import ( InstallationError, RequirementsFileParseError, ) from pip._internal.models.format_control import FormatControl +from pip._internal.network.session import PipSession from pip._internal.req.constructors import ( install_req_from_editable, install_req_from_line, diff --git a/tests/unit/test_unit_outdated.py b/tests/unit/test_unit_outdated.py index 4dd5b16cb93..7002e7b0504 100644 --- a/tests/unit/test_unit_outdated.py +++ b/tests/unit/test_unit_outdated.py @@ -9,8 +9,8 @@ from mock import patch from pip._vendor import pkg_resources -from pip._internal.download import PipSession from pip._internal.index import InstallationCandidate +from pip._internal.network.session import PipSession from pip._internal.utils import outdated from pip._internal.utils.outdated import ( SelfCheckState, From 3328e811d1b7e721383f62a920338e006c2f5c09 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 26 Sep 2019 18:44:20 -0400 Subject: [PATCH 0413/3170] Normalize style --- src/pip/_internal/network/session.py | 20 ++++++++++++-------- tests/unit/test_network_session.py | 12 +++++++----- 2 files changed, 19 insertions(+), 13 deletions(-) diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py index 57c8eb01c99..e63fc273af4 100644 --- a/src/pip/_internal/network/session.py +++ b/src/pip/_internal/network/session.py @@ -13,7 +13,7 @@ from pip._vendor.requests.structures import CaseInsensitiveDict from pip._vendor.six.moves.urllib import parse as urllib_parse -import pip +from pip import __version__ from pip._internal.network.auth import MultiDomainBasicAuth from pip._internal.network.cache import SafeFileCache # Import ssl from compat so the initial import occurs in only one place. @@ -89,7 +89,7 @@ def user_agent(): Return a string representing the user agent. """ data = { - "installer": {"name": "pip", "version": pip.__version__}, + "installer": {"name": "pip", "version": __version__}, "python": platform.python_version(), "implementation": { "name": platform.python_implementation(), @@ -367,9 +367,11 @@ def is_secure_origin(self, location): except ValueError: # We don't have both a valid address or a valid network, so # we'll check this origin against hostnames. - if (origin_host and - origin_host.lower() != secure_host.lower() and - secure_host != "*"): + if ( + origin_host and + origin_host.lower() != secure_host.lower() and + secure_host != "*" + ): continue else: # We have a valid address and network, so see if the address @@ -378,9 +380,11 @@ def is_secure_origin(self, location): continue # Check to see if the port matches. - if (origin_port != secure_port and - secure_port != "*" and - secure_port is not None): + if ( + origin_port != secure_port and + secure_port != "*" and + secure_port is not None + ): continue # If we've gotten here, then this origin matches the current diff --git a/tests/unit/test_network_session.py b/tests/unit/test_network_session.py index c2cd15ad2ce..159a4d4dea1 100644 --- a/tests/unit/test_network_session.py +++ b/tests/unit/test_network_session.py @@ -2,7 +2,7 @@ import pytest -import pip +from pip import __version__ from pip._internal.network.session import CI_ENVIRONMENT_VARIABLES, PipSession @@ -13,7 +13,7 @@ def get_user_agent(): def test_user_agent(): user_agent = get_user_agent() - assert user_agent.startswith("pip/%s" % pip.__version__) + assert user_agent.startswith("pip/{}".format(__version__)) @pytest.mark.parametrize('name, expected_like_ci', [ @@ -58,12 +58,14 @@ def test_cache_defaults_off(self): assert not hasattr(session.adapters["https://"], "cache") def test_cache_is_enabled(self, tmpdir): - session = PipSession(cache=tmpdir.joinpath("test-cache")) + cache_directory = tmpdir.joinpath("test-cache") + session = PipSession(cache=cache_directory) assert hasattr(session.adapters["https://"], "cache") - assert (session.adapters["https://"].cache.directory == - tmpdir.joinpath("test-cache")) + assert ( + session.adapters["https://"].cache.directory == cache_directory + ) def test_http_cache_is_not_enabled(self, tmpdir): session = PipSession(cache=tmpdir.joinpath("test-cache")) From c40331e824e827c80f1c03e9fb67d009a934d16f Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 26 Sep 2019 18:49:58 -0400 Subject: [PATCH 0414/3170] Add docstring to network.session --- src/pip/_internal/network/session.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py index e63fc273af4..0e9f44f562d 100644 --- a/src/pip/_internal/network/session.py +++ b/src/pip/_internal/network/session.py @@ -1,3 +1,6 @@ +"""PipSession and supporting code, containing all pip-specific +network request configuration and behavior. +""" import email.utils import json import logging From 87a3e754879207268ca64aebe4afcd77cfd75936 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Fri, 27 Sep 2019 10:44:57 +0530 Subject: [PATCH 0415/3170] Remove the testenv:packaging sections from tox.ini Why: Because we no longer need it and it's covered in tox -e lint now. --- tox.ini | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/tox.ini b/tox.ini index 63360619051..39ea5fc151e 100644 --- a/tox.ini +++ b/tox.ini @@ -40,16 +40,6 @@ commands = # That is why we have a "-c docs/html" in the next line. sphinx-build -W -d {envtmpdir}/doctrees/man -b man docs/man docs/build/man -c docs/html -[testenv:packaging] -skip_install = True -deps = - check-manifest - readme_renderer -commands_pre = -commands = - check-manifest - python setup.py check -m -r -s - [testenv:lint] skip_install = True commands_pre = From 9ba442362f05f7ec02d256b55580ab023a538135 Mon Sep 17 00:00:00 2001 From: NtaleGrey <Shadikntale@gmail.com> Date: Fri, 27 Sep 2019 14:27:08 +0300 Subject: [PATCH 0416/3170] remove DependencyWarning warning from pip._internal --- src/pip/_internal/__init__.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/pip/_internal/__init__.py b/src/pip/_internal/__init__.py index b88a97c46c8..08271db8672 100755 --- a/src/pip/_internal/__init__.py +++ b/src/pip/_internal/__init__.py @@ -6,7 +6,6 @@ # We ignore certain warnings from urllib3, since they are not relevant to pip's # usecases. from pip._vendor.urllib3.exceptions import ( - DependencyWarning, InsecureRequestWarning, ) @@ -14,7 +13,3 @@ # Raised when using --trusted-host. warnings.filterwarnings("ignore", category=InsecureRequestWarning) -# Raised since socks support depends on PySocks, which may not be installed. -# Barry Warsaw noted (on 2016-06-17) that this should be done before -# importing pip.vcs, which has since moved to pip._internal.vcs. -warnings.filterwarnings("ignore", category=DependencyWarning) From 76d4cce5a5b93fc65b5948138e5fc19f2e4c4710 Mon Sep 17 00:00:00 2001 From: NtaleGrey <Shadikntale@gmail.com> Date: Fri, 27 Sep 2019 14:40:46 +0300 Subject: [PATCH 0417/3170] add news file --- news/7094.removal | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/7094.removal diff --git a/news/7094.removal b/news/7094.removal new file mode 100644 index 00000000000..57a4ece077d --- /dev/null +++ b/news/7094.removal @@ -0,0 +1 @@ +Remove DependencyWarning warning from pip._internal \ No newline at end of file From c9bc36bdaee5d2e5509e4bb8bb3983b0610e6209 Mon Sep 17 00:00:00 2001 From: NtaleGrey <Shadikntale@gmail.com> Date: Fri, 27 Sep 2019 14:46:03 +0300 Subject: [PATCH 0418/3170] format code --- news/7094.removal | 2 +- src/pip/_internal/__init__.py | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/news/7094.removal b/news/7094.removal index 57a4ece077d..eff43441e35 100644 --- a/news/7094.removal +++ b/news/7094.removal @@ -1 +1 @@ -Remove DependencyWarning warning from pip._internal \ No newline at end of file +Remove DependencyWarning warning from pip._internal diff --git a/src/pip/_internal/__init__.py b/src/pip/_internal/__init__.py index 08271db8672..04238e289ad 100755 --- a/src/pip/_internal/__init__.py +++ b/src/pip/_internal/__init__.py @@ -5,9 +5,7 @@ # We ignore certain warnings from urllib3, since they are not relevant to pip's # usecases. -from pip._vendor.urllib3.exceptions import ( - InsecureRequestWarning, -) +from pip._vendor.urllib3.exceptions import InsecureRequestWarning import pip._internal.utils.inject_securetransport # noqa From cf5ce5f16fd6bbd5c8be54e2c53df8ea5ca70c9b Mon Sep 17 00:00:00 2001 From: NtaleGrey <Shadikntale@gmail.com> Date: Fri, 27 Sep 2019 17:54:11 +0300 Subject: [PATCH 0419/3170] Move PipXmlrpcTransport from pip._internal.download to pip._internal.network.xmlrpc --- news/7090.removal | 2 ++ src/pip/_internal/commands/search.py | 2 +- src/pip/_internal/download.py | 30 ----------------------- src/pip/_internal/network/xmlrpc.py | 36 ++++++++++++++++++++++++++++ tests/functional/test_search.py | 27 --------------------- tests/unit/test_network_xmlrpc.py | 30 +++++++++++++++++++++++ 6 files changed, 69 insertions(+), 58 deletions(-) create mode 100644 news/7090.removal create mode 100644 src/pip/_internal/network/xmlrpc.py create mode 100644 tests/unit/test_network_xmlrpc.py diff --git a/news/7090.removal b/news/7090.removal new file mode 100644 index 00000000000..01bdcf65459 --- /dev/null +++ b/news/7090.removal @@ -0,0 +1,2 @@ +Move PipXmlrpcTransport from pip._internal.download to pip._internal.network.xmlrpc +and move associated tests to tests.unit.test_network_xmlrpc diff --git a/src/pip/_internal/commands/search.py b/src/pip/_internal/commands/search.py index ef698d0b7ec..23e9ca8fee1 100644 --- a/src/pip/_internal/commands/search.py +++ b/src/pip/_internal/commands/search.py @@ -14,7 +14,7 @@ from pip._internal.cli.base_command import Command from pip._internal.cli.req_command import SessionCommandMixin from pip._internal.cli.status_codes import NO_MATCHES_FOUND, SUCCESS -from pip._internal.download import PipXmlrpcTransport +from pip._internal.network.xmlrpc import PipXmlrpcTransport from pip._internal.exceptions import CommandError from pip._internal.models.index import PyPI from pip._internal.utils.compat import get_terminal_size diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index 92d5ab501e3..9b813d3de15 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -19,7 +19,6 @@ from pip._vendor.six import PY2 # 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 import pip @@ -804,35 +803,6 @@ def unpack_file_url( _copy_file(from_path, download_dir, link) -class PipXmlrpcTransport(xmlrpc_client.Transport): - """Provide a `xmlrpclib.Transport` implementation via a `PipSession` - object. - """ - - def __init__(self, index_url, session, use_datetime=False): - xmlrpc_client.Transport.__init__(self, use_datetime) - index_parts = urllib_parse.urlparse(index_url) - self._scheme = index_parts.scheme - self._session = session - - def request(self, host, handler, request_body, verbose=False): - parts = (self._scheme, host, handler, None, None, None) - url = urllib_parse.urlunparse(parts) - try: - headers = {'Content-Type': 'text/xml'} - response = self._session.post(url, data=request_body, - headers=headers, stream=True) - response.raise_for_status() - self.verbose = verbose - return self.parse_response(response.raw) - except requests.HTTPError as exc: - logger.critical( - "HTTP error %s while getting %s", - exc.response.status_code, url, - ) - raise - - def unpack_url( link, # type: Link location, # type: str diff --git a/src/pip/_internal/network/xmlrpc.py b/src/pip/_internal/network/xmlrpc.py new file mode 100644 index 00000000000..042f3ff7526 --- /dev/null +++ b/src/pip/_internal/network/xmlrpc.py @@ -0,0 +1,36 @@ +import logging + +from pip._vendor import requests +from pip._vendor.six.moves import xmlrpc_client # type: ignore +from pip._vendor.six.moves.urllib import parse as urllib_parse + +logger = logging.getLogger(__name__) + + +class PipXmlrpcTransport(xmlrpc_client.Transport): + """Provide a `xmlrpclib.Transport` implementation via a `PipSession` + object. + """ + + def __init__(self, index_url, session, use_datetime=False): + xmlrpc_client.Transport.__init__(self, use_datetime) + index_parts = urllib_parse.urlparse(index_url) + self._scheme = index_parts.scheme + self._session = session + + def request(self, host, handler, request_body, verbose=False): + parts = (self._scheme, host, handler, None, None, None) + url = urllib_parse.urlunparse(parts) + try: + headers = {'Content-Type': 'text/xml'} + response = self._session.post(url, data=request_body, + headers=headers, stream=True) + response.raise_for_status() + self.verbose = verbose + return self.parse_response(response.raw) + except requests.HTTPError as exc: + logger.critical( + "HTTP error %s while getting %s", + exc.response.status_code, url, + ) + raise diff --git a/tests/functional/test_search.py b/tests/functional/test_search.py index fce6c5f819c..86c19b37cc3 100644 --- a/tests/functional/test_search.py +++ b/tests/functional/test_search.py @@ -4,7 +4,6 @@ import pytest from pip._internal.cli.status_codes import NO_MATCHES_FOUND, SUCCESS -from pip._internal.commands import create_command from pip._internal.commands.search import ( highest_version, print_results, @@ -106,32 +105,6 @@ def test_search_missing_argument(script): assert 'ERROR: Missing required argument (search query).' in result.stderr -@pytest.mark.network -def test_run_method_should_return_success_when_find_packages(): - """ - Test SearchCommand.run for found package - """ - command = create_command('search') - cmdline = "--index=https://pypi.org/pypi pip" - with command.main_context(): - options, args = command.parse_args(cmdline.split()) - status = command.run(options, args) - assert status == SUCCESS - - -@pytest.mark.network -def test_run_method_should_return_no_matches_found_when_does_not_find_pkgs(): - """ - Test SearchCommand.run for no matches - """ - command = create_command('search') - cmdline = "--index=https://pypi.org/pypi nonexistentpackage" - with command.main_context(): - options, args = command.parse_args(cmdline.split()) - status = command.run(options, args) - assert status == NO_MATCHES_FOUND - - @pytest.mark.network def test_search_should_exit_status_code_zero_when_find_packages(script): """ diff --git a/tests/unit/test_network_xmlrpc.py b/tests/unit/test_network_xmlrpc.py new file mode 100644 index 00000000000..58abb4f21b9 --- /dev/null +++ b/tests/unit/test_network_xmlrpc.py @@ -0,0 +1,30 @@ +import pytest + +from pip._internal.cli.status_codes import NO_MATCHES_FOUND, SUCCESS +from pip._internal.commands import create_command + + +@pytest.mark.network +def test_run_method_should_return_success_when_find_packages(): + """ + Test SearchCommand.run for found package + """ + command = create_command('search') + cmdline = "--index=https://pypi.org/pypi pip" + with command.main_context(): + options, args = command.parse_args(cmdline.split()) + status = command.run(options, args) + assert status == SUCCESS + + +@pytest.mark.network +def test_run_method_should_return_no_matches_found_when_does_not_find_pkgs(): + """ + Test SearchCommand.run for no matches + """ + command = create_command('search') + cmdline = "--index=https://pypi.org/pypi nonexistentpackage" + with command.main_context(): + options, args = command.parse_args(cmdline.split()) + status = command.run(options, args) + assert status == NO_MATCHES_FOUND From a0044856d7797ad8fb1bfed8a0860e591e5e1518 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Fri, 27 Sep 2019 22:31:12 +0530 Subject: [PATCH 0420/3170] No need to create a virtualenv for generate_authors --- noxfile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/noxfile.py b/noxfile.py index d6357b3dd7d..34214317ec0 100644 --- a/noxfile.py +++ b/noxfile.py @@ -31,9 +31,9 @@ def get_author_list(): # ----------------------------------------------------------------------------- -# Commands used during the release process +# Release Commands # ----------------------------------------------------------------------------- -@nox.session +@nox.session(python=False) def generate_authors(session): # Get our list of authors session.log("Collecting author names") From eb93a21b8b217dc97ca19fad62a9dfe4de1d967e Mon Sep 17 00:00:00 2001 From: NtaleGrey <Shadikntale@gmail.com> Date: Fri, 27 Sep 2019 20:18:08 +0300 Subject: [PATCH 0421/3170] rename news file --- news/{7094.removal => 7094.trivial} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename news/{7094.removal => 7094.trivial} (100%) diff --git a/news/7094.removal b/news/7094.trivial similarity index 100% rename from news/7094.removal rename to news/7094.trivial From a9ae6deacc793c32a78c68a6c38a1a65b361d13e Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Fri, 27 Sep 2019 22:41:49 +0530 Subject: [PATCH 0422/3170] Add development commands to nox Why: To enable using a single tool for release management as well as development automation. --- noxfile.py | 90 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 90 insertions(+) diff --git a/noxfile.py b/noxfile.py index 34214317ec0..d5db15f5707 100644 --- a/noxfile.py +++ b/noxfile.py @@ -2,10 +2,24 @@ """ import io +import os +import shutil import subprocess import nox +nox.options.reuse_existing_virtualenvs = True +nox.options.sessions = ["lint"] + +LOCATIONS = { + "common-wheels": "tests/data/common_wheels", + "protected-pip": "tools/tox_pip.py", +} +REQUIREMENTS = { + "tests": "tools/requirements/tests.txt", + "common-wheels": "tools/requirements/tests-common_wheels.txt", +} + def get_author_list(): """Get the list of authors from Git commits. @@ -30,6 +44,82 @@ def get_author_list(): return sorted(authors, key=lambda x: x.lower()) +def protected_pip(*arguments): + """Get arguments for session.run, that use a "protected" pip. + """ + return ("python", LOCATIONS["protected-pip"]) + arguments + + +def should_update_common_wheels(): + # If the cache hasn't been created, create it. + if not os.path.exists(LOCATIONS["common-wheels"]): + return True + + # If the requirements was updated after cache, we'll repopulate it. + cache_last_populated_at = os.path.getmtime(LOCATIONS["common-wheels"]) + requirements_updated_at = os.path.getmtime(REQUIREMENTS["common-wheels"]) + need_to_repopulate = requirements_updated_at > cache_last_populated_at + + # Clear the stale cache. + if need_to_repopulate: + shutil.remove(LOCATIONS["common-wheels"], ignore_errors=True) + + return need_to_repopulate + + +# ----------------------------------------------------------------------------- +# Development Commands +# ----------------------------------------------------------------------------- +@nox.session(python=["2.7", "3.5", "3.6", "3.7", "pypy"]) +def test(session): + # Get the common wheels. + if should_update_common_wheels(): + session.run(*protected_pip( + "wheel", + "-w", LOCATIONS["common-wheels"], + "-r", REQUIREMENTS["common-wheels"], + )) + + # Install sources and dependencies + session.run(*protected_pip("install", ".")) + session.run(*protected_pip("install", "-r", REQUIREMENTS["tests"])) + + # Run the tests + session.run("pytest", *session.posargs, env={"LC_CTYPE": "en_US.UTF-8"}) + + +@nox.session +def docs(session): + session.install(".") + session.install("-r", REQUIREMENTS["docs"]) + + def get_sphinx_build_command(kind): + return [ + "sphinx-build", + "-W", + "-c", "docs/html", + "-d", "docs/build/doctrees/" + kind, + "-b", kind, + "docs/" + kind, + "docs/build/" + kind, + ] + + session.run(*get_sphinx_build_command("html")) + session.run(*get_sphinx_build_command("man")) + + +@nox.session +def lint(session): + session.install("pre-commit") + + if session.posargs: + args = session.posargs + ["--all-files"] + else: + args = ["--all-files", "--show-diff-on-failure"] + + session.run("pre-commit", "run", *args) + + # ----------------------------------------------------------------------------- # Release Commands # ----------------------------------------------------------------------------- From 2f57f911104206c2d7be8ab7cdedd7866b8ed568 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Fri, 27 Sep 2019 22:43:32 +0530 Subject: [PATCH 0423/3170] More accurate docstring --- noxfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index d5db15f5707..f08dbb56b38 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1,4 +1,4 @@ -"""Release time helpers, executed using nox. +"""Automation using nox. """ import io From 1a3b84060dabdedc8abbe92baac237f2a4995de5 Mon Sep 17 00:00:00 2001 From: Albert Tugushev <albert@tugushev.ru> Date: Tue, 24 Sep 2019 19:32:01 +0300 Subject: [PATCH 0424/3170] Add functional tests for 'yanked' files --- news/6653.trivial | 1 + tests/data/indexes/yanked/simple/index.html | 7 +++++ tests/functional/test_install.py | 30 +++++++++++++++++++++ 3 files changed, 38 insertions(+) create mode 100644 news/6653.trivial create mode 100644 tests/data/indexes/yanked/simple/index.html diff --git a/news/6653.trivial b/news/6653.trivial new file mode 100644 index 00000000000..5ef02a00028 --- /dev/null +++ b/news/6653.trivial @@ -0,0 +1 @@ +Add functional tests for "yanked" files. diff --git a/tests/data/indexes/yanked/simple/index.html b/tests/data/indexes/yanked/simple/index.html new file mode 100644 index 00000000000..bf4994310be --- /dev/null +++ b/tests/data/indexes/yanked/simple/index.html @@ -0,0 +1,7 @@ +<html> + <body> + <a href="../../../packages/simple-1.0.tar.gz">simple-1.0.tar.gz</a> + <a href="../../../packages/simple-2.0.tar.gz">simple-2.0.tar.gz</a> + <a data-yanked="test reason message" href="../../../packages/simple-3.0.tar.gz">simple-3.0.tar.gz</a> + </body> +</html> diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 7a0e4a9cbee..8ce2c8499ec 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1657,3 +1657,33 @@ def test_install_pip_does_not_modify_pip_when_satisfied( 'pip', *install_args, use_module=use_module ) assert expected_message in result.stdout, str(result) + + +def test_ignore_yanked_file(script, data): + """ + Test ignore a "yanked" file. + """ + result = script.pip( + 'install', 'simple', + '--index-url', data.index_url('yanked'), + ) + # Make sure a "yanked" release ignored + assert 'Successfully installed simple-2.0\n' in result.stdout, str(result) + + +def test_install_yanked_file_and_print_warning(script, data): + """ + Test install a "yanked" file and warn a reason. + + Yanked files are always ignored, unless they are the only file that + matches a version specifier that "pins" to an exact version (PEP 592). + """ + result = script.pip( + 'install', 'simple==3.0', + '--index-url', data.index_url('yanked'), + expect_stderr=True, + ) + expected_warning = 'Reason for being yanked: test reason message' + assert expected_warning in result.stderr, str(result) + # Make sure a "yanked" release installed + assert 'Successfully installed simple-3.0\n' in result.stdout, str(result) From e41c5f868c830eead689cf04b01917a64d9168d5 Mon Sep 17 00:00:00 2001 From: Albert Tugushev <albert@tugushev.ru> Date: Thu, 26 Sep 2019 13:32:59 +0300 Subject: [PATCH 0425/3170] Address review comments Fix typos. Co-Authored-By: Christopher Hunt <chrahunt@gmail.com> --- tests/functional/test_install.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 8ce2c8499ec..27b797dfefe 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1667,13 +1667,13 @@ def test_ignore_yanked_file(script, data): 'install', 'simple', '--index-url', data.index_url('yanked'), ) - # Make sure a "yanked" release ignored + # Make sure a "yanked" release is ignored assert 'Successfully installed simple-2.0\n' in result.stdout, str(result) def test_install_yanked_file_and_print_warning(script, data): """ - Test install a "yanked" file and warn a reason. + Test install a "yanked" file and print a warning. Yanked files are always ignored, unless they are the only file that matches a version specifier that "pins" to an exact version (PEP 592). @@ -1685,5 +1685,5 @@ def test_install_yanked_file_and_print_warning(script, data): ) expected_warning = 'Reason for being yanked: test reason message' assert expected_warning in result.stderr, str(result) - # Make sure a "yanked" release installed + # Make sure a "yanked" release is installed assert 'Successfully installed simple-3.0\n' in result.stdout, str(result) From c6e9a9eab4686eba7fae0da64f6f140e05af8866 Mon Sep 17 00:00:00 2001 From: Albert Tugushev <albert@tugushev.ru> Date: Sat, 28 Sep 2019 01:44:11 +0300 Subject: [PATCH 0426/3170] Remove unused assignment --- news/remove-unused-assignment.trivial | 1 + src/pip/_internal/exceptions.py | 1 - 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 news/remove-unused-assignment.trivial diff --git a/news/remove-unused-assignment.trivial b/news/remove-unused-assignment.trivial new file mode 100644 index 00000000000..f4d91cabc28 --- /dev/null +++ b/news/remove-unused-assignment.trivial @@ -0,0 +1 @@ +Remove unused assignment. diff --git a/src/pip/_internal/exceptions.py b/src/pip/_internal/exceptions.py index 096adcd6c5a..78483d7d565 100644 --- a/src/pip/_internal/exceptions.py +++ b/src/pip/_internal/exceptions.py @@ -277,7 +277,6 @@ def hash_then_or(hash_name): for e in expecteds) lines.append(' Got %s\n' % self.gots[hash_name].hexdigest()) - prefix = ' or' return '\n'.join(lines) From 51dc60f8a7f021d1d0758bca51b8ffbc322ac33f Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Fri, 27 Sep 2019 23:25:32 +0530 Subject: [PATCH 0427/3170] Parallelize tests as much as possible, by default --- noxfile.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index f08dbb56b38..185f8c0ceda 100644 --- a/noxfile.py +++ b/noxfile.py @@ -84,8 +84,11 @@ def test(session): session.run(*protected_pip("install", ".")) session.run(*protected_pip("install", "-r", REQUIREMENTS["tests"])) + # Parallelize tests as much as possible, by default. + arguments = session.posargs or ["-n", "auto"] + # Run the tests - session.run("pytest", *session.posargs, env={"LC_CTYPE": "en_US.UTF-8"}) + session.run("pytest", *arguments, env={"LC_CTYPE": "en_US.UTF-8"}) @nox.session From 20b58c33fade37ad45a3a9518555d40544811a49 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sat, 28 Sep 2019 11:13:37 +0530 Subject: [PATCH 0428/3170] Bring in useful comments from tox.ini --- noxfile.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index 185f8c0ceda..90beffe16f8 100644 --- a/noxfile.py +++ b/noxfile.py @@ -46,6 +46,10 @@ def get_author_list(): def protected_pip(*arguments): """Get arguments for session.run, that use a "protected" pip. + + This invokes a wrapper script, that forwards calls to original virtualenv + (stable) version, and not the code being tested. This ensures pip being + used is not the code being tested. """ return ("python", LOCATIONS["protected-pip"]) + arguments @@ -88,6 +92,8 @@ def test(session): arguments = session.posargs or ["-n", "auto"] # Run the tests + # LC_CTYPE is set to get UTF-8 output inside of the subprocesses that our + # tests use. session.run("pytest", *arguments, env={"LC_CTYPE": "en_US.UTF-8"}) @@ -97,10 +103,14 @@ def docs(session): session.install("-r", REQUIREMENTS["docs"]) def get_sphinx_build_command(kind): + # Having the conf.py in the docs/html is weird but needed because we + # can not use a different configuration directory vs source directory + # on RTD currently. So, we'll pass "-c docs/html" here. + # See https://github.com/rtfd/readthedocs.org/issues/1543. return [ "sphinx-build", "-W", - "-c", "docs/html", + "-c", "docs/html", # see note above "-d", "docs/build/doctrees/" + kind, "-b", kind, "docs/" + kind, From 8ba9c1470dccd1c368a8321c0b68e3d95c176bab Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sat, 28 Sep 2019 11:15:13 +0530 Subject: [PATCH 0429/3170] Add a note that the nox commands are a "prototype" --- noxfile.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/noxfile.py b/noxfile.py index 90beffe16f8..7b19ee959fd 100644 --- a/noxfile.py +++ b/noxfile.py @@ -73,6 +73,9 @@ def should_update_common_wheels(): # ----------------------------------------------------------------------------- # Development Commands +# These are currently prototypes to evaluate whether we want to switch over +# completely to nox for all our automation. Contributors should prefer using +# `tox -e ...` until this note is removed. # ----------------------------------------------------------------------------- @nox.session(python=["2.7", "3.5", "3.6", "3.7", "pypy"]) def test(session): From f916fbe3331fa27a66fc4ecdf04b650ae410f80d Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sat, 28 Sep 2019 12:17:41 +0530 Subject: [PATCH 0430/3170] Simplify handling of PEP 517 metadata temp dir --- src/pip/_internal/req/req_install.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 659a494e94e..7172c5a5071 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -594,23 +594,18 @@ def prepare_metadata(self): ) self.req = Requirement(metadata_name) - def cleanup(self): - # type: () -> None - if self._temp_dir is not None: - self._temp_dir.cleanup() - def prepare_pep517_metadata(self): # type: () -> None assert self.pep517_backend is not None # NOTE: This needs to be refactored to stop using atexit - self._temp_dir = TempDirectory(delete=False, kind="req-install") + temp_dir = TempDirectory(kind="modern-metadata") + atexit.register(temp_dir.cleanup) + metadata_dir = os.path.join( - self._temp_dir.path, + temp_dir.path, 'pip-wheel-metadata', ) - atexit.register(self.cleanup) - ensure_dir(metadata_dir) with self.build_env: From f3c1519160cc3183fe807e05467e532d55b77b18 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 9 May 2019 11:47:27 -0400 Subject: [PATCH 0431/3170] Improve name and docstring --- tests/functional/test_warning.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/functional/test_warning.py b/tests/functional/test_warning.py index edde04429ca..100002e17ba 100644 --- a/tests/functional/test_warning.py +++ b/tests/functional/test_warning.py @@ -1,7 +1,7 @@ import textwrap -def test_environ(script, tmpdir): +def test_deprecation_warning_warns_correctly(script, tmpdir): demo = tmpdir.joinpath('warnings_demo.py') demo.write_text(textwrap.dedent(''' from logging import basicConfig @@ -17,7 +17,7 @@ def test_environ(script, tmpdir): expected = 'WARNING:pip._internal.deprecations:DEPRECATION: deprecated!\n' assert result.stderr == expected - # $PYTHONWARNINGS was added in python2.7 + # NOTE: PYTHONWARNINGS was added in 2.7 script.environ['PYTHONWARNINGS'] = 'ignore' result = script.run('python', demo) assert result.stderr == '' From e670f902195691929e276afb2fc01e6980a3b30b Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 9 May 2019 11:48:43 -0400 Subject: [PATCH 0432/3170] Generate temporary file in a fixture --- tests/functional/test_warning.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/functional/test_warning.py b/tests/functional/test_warning.py index 100002e17ba..3995328ff08 100644 --- a/tests/functional/test_warning.py +++ b/tests/functional/test_warning.py @@ -1,7 +1,9 @@ +import pytest import textwrap -def test_deprecation_warning_warns_correctly(script, tmpdir): +@pytest.fixture +def warnings_demo(tmpdir): demo = tmpdir.joinpath('warnings_demo.py') demo.write_text(textwrap.dedent(''' from logging import basicConfig @@ -12,12 +14,15 @@ def test_deprecation_warning_warns_correctly(script, tmpdir): deprecation.deprecated("deprecated!", replacement=None, gone_in=None) ''')) + return demo - result = script.run('python', demo, expect_stderr=True) + +def test_deprecation_warnings_are_correct(script, warnings_demo): + result = script.run('python', warnings_demo, expect_stderr=True) expected = 'WARNING:pip._internal.deprecations:DEPRECATION: deprecated!\n' assert result.stderr == expected # NOTE: PYTHONWARNINGS was added in 2.7 script.environ['PYTHONWARNINGS'] = 'ignore' - result = script.run('python', demo) + result = script.run('python', warnings_demo) assert result.stderr == '' From 3a77de3c7d863041bea1366c50a95293d1cd2f7a Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 9 May 2019 11:49:06 -0400 Subject: [PATCH 0433/3170] Split tests for different functionality --- tests/functional/test_warning.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/functional/test_warning.py b/tests/functional/test_warning.py index 3995328ff08..64ab79a184e 100644 --- a/tests/functional/test_warning.py +++ b/tests/functional/test_warning.py @@ -22,7 +22,8 @@ def test_deprecation_warnings_are_correct(script, warnings_demo): expected = 'WARNING:pip._internal.deprecations:DEPRECATION: deprecated!\n' assert result.stderr == expected - # NOTE: PYTHONWARNINGS was added in 2.7 + +def test_deprecation_warnings_can_be_silenced(script, warnings_demo): script.environ['PYTHONWARNINGS'] = 'ignore' result = script.run('python', warnings_demo) assert result.stderr == '' From 2bb8ee6ae30e233f28ea0ae0fb01c0e4a1f8d9f1 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 9 May 2019 13:35:40 -0400 Subject: [PATCH 0434/3170] Sort imports for the greater good --- tests/functional/test_warning.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/functional/test_warning.py b/tests/functional/test_warning.py index 64ab79a184e..4a329475012 100644 --- a/tests/functional/test_warning.py +++ b/tests/functional/test_warning.py @@ -1,6 +1,7 @@ -import pytest import textwrap +import pytest + @pytest.fixture def warnings_demo(tmpdir): From 88e9ce6e27a97c8d742f245762f576568125b86c Mon Sep 17 00:00:00 2001 From: NtaleGrey <Shadikntale@gmail.com> Date: Fri, 27 Sep 2019 20:11:19 +0300 Subject: [PATCH 0435/3170] implement feedback --- news/{7090.removal => 7090.trivial} | 0 src/pip/_internal/commands/search.py | 2 +- src/pip/_internal/download.py | 2 -- src/pip/_internal/network/xmlrpc.py | 4 ++++ tests/functional/test_search.py | 27 +++++++++++++++++++++++++ tests/unit/test_network_xmlrpc.py | 30 ---------------------------- 6 files changed, 32 insertions(+), 33 deletions(-) rename news/{7090.removal => 7090.trivial} (100%) delete mode 100644 tests/unit/test_network_xmlrpc.py diff --git a/news/7090.removal b/news/7090.trivial similarity index 100% rename from news/7090.removal rename to news/7090.trivial diff --git a/src/pip/_internal/commands/search.py b/src/pip/_internal/commands/search.py index 23e9ca8fee1..9ae1fb533d9 100644 --- a/src/pip/_internal/commands/search.py +++ b/src/pip/_internal/commands/search.py @@ -14,9 +14,9 @@ from pip._internal.cli.base_command import Command from pip._internal.cli.req_command import SessionCommandMixin from pip._internal.cli.status_codes import NO_MATCHES_FOUND, SUCCESS -from pip._internal.network.xmlrpc import PipXmlrpcTransport from pip._internal.exceptions import CommandError from pip._internal.models.index import PyPI +from pip._internal.network.xmlrpc import PipXmlrpcTransport from pip._internal.utils.compat import get_terminal_size from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import write_output diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index 9b813d3de15..6cf3e5e6431 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -17,8 +17,6 @@ from pip._vendor.requests.models import CONTENT_CHUNK_SIZE, Response from pip._vendor.requests.structures import CaseInsensitiveDict from pip._vendor.six import PY2 -# 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.urllib import parse as urllib_parse import pip diff --git a/src/pip/_internal/network/xmlrpc.py b/src/pip/_internal/network/xmlrpc.py index 042f3ff7526..40d20595a09 100644 --- a/src/pip/_internal/network/xmlrpc.py +++ b/src/pip/_internal/network/xmlrpc.py @@ -1,6 +1,10 @@ +"""xmlrpclib.Transport implementation +""" import logging from pip._vendor import requests +# 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 diff --git a/tests/functional/test_search.py b/tests/functional/test_search.py index 86c19b37cc3..fce6c5f819c 100644 --- a/tests/functional/test_search.py +++ b/tests/functional/test_search.py @@ -4,6 +4,7 @@ import pytest from pip._internal.cli.status_codes import NO_MATCHES_FOUND, SUCCESS +from pip._internal.commands import create_command from pip._internal.commands.search import ( highest_version, print_results, @@ -105,6 +106,32 @@ def test_search_missing_argument(script): assert 'ERROR: Missing required argument (search query).' in result.stderr +@pytest.mark.network +def test_run_method_should_return_success_when_find_packages(): + """ + Test SearchCommand.run for found package + """ + command = create_command('search') + cmdline = "--index=https://pypi.org/pypi pip" + with command.main_context(): + options, args = command.parse_args(cmdline.split()) + status = command.run(options, args) + assert status == SUCCESS + + +@pytest.mark.network +def test_run_method_should_return_no_matches_found_when_does_not_find_pkgs(): + """ + Test SearchCommand.run for no matches + """ + command = create_command('search') + cmdline = "--index=https://pypi.org/pypi nonexistentpackage" + with command.main_context(): + options, args = command.parse_args(cmdline.split()) + status = command.run(options, args) + assert status == NO_MATCHES_FOUND + + @pytest.mark.network def test_search_should_exit_status_code_zero_when_find_packages(script): """ diff --git a/tests/unit/test_network_xmlrpc.py b/tests/unit/test_network_xmlrpc.py deleted file mode 100644 index 58abb4f21b9..00000000000 --- a/tests/unit/test_network_xmlrpc.py +++ /dev/null @@ -1,30 +0,0 @@ -import pytest - -from pip._internal.cli.status_codes import NO_MATCHES_FOUND, SUCCESS -from pip._internal.commands import create_command - - -@pytest.mark.network -def test_run_method_should_return_success_when_find_packages(): - """ - Test SearchCommand.run for found package - """ - command = create_command('search') - cmdline = "--index=https://pypi.org/pypi pip" - with command.main_context(): - options, args = command.parse_args(cmdline.split()) - status = command.run(options, args) - assert status == SUCCESS - - -@pytest.mark.network -def test_run_method_should_return_no_matches_found_when_does_not_find_pkgs(): - """ - Test SearchCommand.run for no matches - """ - command = create_command('search') - cmdline = "--index=https://pypi.org/pypi nonexistentpackage" - with command.main_context(): - options, args = command.parse_args(cmdline.split()) - status = command.run(options, args) - assert status == NO_MATCHES_FOUND From 0f093c1f764cde20f2bc558e321677d7c1145768 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sat, 28 Sep 2019 19:55:38 +0530 Subject: [PATCH 0436/3170] Delete mypy.txt --- tools/requirements/mypy.txt | 1 - 1 file changed, 1 deletion(-) delete mode 100644 tools/requirements/mypy.txt diff --git a/tools/requirements/mypy.txt b/tools/requirements/mypy.txt deleted file mode 100644 index 50615d913be..00000000000 --- a/tools/requirements/mypy.txt +++ /dev/null @@ -1 +0,0 @@ -mypy == 0.720 From 64dacbf8881811c17e1dde60a0091d64974af139 Mon Sep 17 00:00:00 2001 From: Maxim Kurnikov <maxim.kurnikov@gmail.com> Date: Sat, 28 Sep 2019 17:42:27 +0300 Subject: [PATCH 0437/3170] update mypy to 0.730 --- .pre-commit-config.yaml | 2 +- src/pip/_internal/utils/appdirs.py | 5 +++-- src/pip/_internal/utils/filetypes.py | 13 +++++++++---- src/pip/_internal/wheel.py | 10 +++++----- 4 files changed, 18 insertions(+), 12 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index bc145e993b5..2cfdb2cb287 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: files: \.py$ - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.720 + rev: v0.730 hooks: - id: mypy exclude: docs|tests diff --git a/src/pip/_internal/utils/appdirs.py b/src/pip/_internal/utils/appdirs.py index dcd757c7e05..14e45b1978e 100644 --- a/src/pip/_internal/utils/appdirs.py +++ b/src/pip/_internal/utils/appdirs.py @@ -227,7 +227,8 @@ def _get_win_folder_with_ctypes(csidl_name): }[csidl_name] buf = ctypes.create_unicode_buffer(1024) - ctypes.windll.shell32.SHGetFolderPathW(None, csidl_const, None, 0, buf) + windll = ctypes.windll # type: ignore + windll.shell32.SHGetFolderPathW(None, csidl_const, None, 0, buf) # Downgrade to short path name if have highbit chars. See # <http://bugs.activestate.com/show_bug.cgi?id=85099>. @@ -238,7 +239,7 @@ def _get_win_folder_with_ctypes(csidl_name): break if has_high_char: buf2 = ctypes.create_unicode_buffer(1024) - if ctypes.windll.kernel32.GetShortPathNameW(buf.value, buf2, 1024): + if windll.kernel32.GetShortPathNameW(buf.value, buf2, 1024): buf = buf2 # The type: ignore is explained under the type annotation for this function diff --git a/src/pip/_internal/utils/filetypes.py b/src/pip/_internal/utils/filetypes.py index 251e3e933d3..daa0ca771b7 100644 --- a/src/pip/_internal/utils/filetypes.py +++ b/src/pip/_internal/utils/filetypes.py @@ -1,11 +1,16 @@ """Filetype information. """ +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import Tuple WHEEL_EXTENSION = '.whl' -BZ2_EXTENSIONS = ('.tar.bz2', '.tbz') -XZ_EXTENSIONS = ('.tar.xz', '.txz', '.tlz', '.tar.lz', '.tar.lzma') -ZIP_EXTENSIONS = ('.zip', WHEEL_EXTENSION) -TAR_EXTENSIONS = ('.tar.gz', '.tgz', '.tar') +BZ2_EXTENSIONS = ('.tar.bz2', '.tbz') # type: Tuple[str, ...] +XZ_EXTENSIONS = ('.tar.xz', '.txz', '.tlz', + '.tar.lz', '.tar.lzma') # type: Tuple[str, ...] +ZIP_EXTENSIONS = ('.zip', WHEEL_EXTENSION) # type: Tuple[str, ...] +TAR_EXTENSIONS = ('.tar.gz', '.tgz', '.tar') # type: Tuple[str, ...] ARCHIVE_EXTENSIONS = ( ZIP_EXTENSIONS + BZ2_EXTENSIONS + TAR_EXTENSIONS + XZ_EXTENSIONS ) diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index 8d3c7aefdce..02bc3f5f0f7 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -55,7 +55,7 @@ if MYPY_CHECK_RUNNING: from typing import ( Dict, List, Optional, Sequence, Mapping, Tuple, IO, Text, Any, - Iterable, Callable, + Iterable, Callable, Set, ) from pip._vendor.packaging.requirements import Requirement from pip._internal.req.req_install import InstallRequirement @@ -207,7 +207,7 @@ def message_about_scripts_not_on_PATH(scripts): return None # Group scripts by the path they were installed in - grouped_by_dir = collections.defaultdict(set) # type: Dict[str, set] + grouped_by_dir = collections.defaultdict(set) # type: Dict[str, Set[str]] for destfile in scripts: parent_dir = os.path.dirname(destfile) script_name = os.path.basename(destfile) @@ -224,14 +224,14 @@ def message_about_scripts_not_on_PATH(scripts): warn_for = { parent_dir: scripts for parent_dir, scripts in grouped_by_dir.items() if os.path.normcase(parent_dir) not in not_warn_dirs - } + } # type: Dict[str, Set[str]] if not warn_for: return None # Format a message msg_lines = [] - for parent_dir, scripts in warn_for.items(): - sorted_scripts = sorted(scripts) # type: List[str] + for parent_dir, dir_scripts in warn_for.items(): + sorted_scripts = sorted(dir_scripts) # type: List[str] if len(sorted_scripts) == 1: start_text = "script {} is".format(sorted_scripts[0]) else: From dc50948c3c039b876175f7eb80d81b714717b7c1 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 29 Sep 2019 11:27:06 -0400 Subject: [PATCH 0438/3170] Single backticks to double backticks in triage guide --- docs/html/development/issue-triage.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/html/development/issue-triage.rst b/docs/html/development/issue-triage.rst index cf125ccdb57..9fc54f544eb 100644 --- a/docs/html/development/issue-triage.rst +++ b/docs/html/development/issue-triage.rst @@ -168,7 +168,7 @@ depending on the situation: * Python as resolved by the shell: ``type python`` * Origin of pip (get-pip.py, OS-level package manager, ensurepip, manual installation) -* Using a virtual environment (with `--system-site-packages`?) +* Using a virtual environment (with ``--system-site-packages``?) * Using a conda environment * ``PATH`` environment variable * Network situation (e.g. airgapped environment, firewalls) From 3692097ccad1b86de657551077bc8aef9502583b Mon Sep 17 00:00:00 2001 From: Maxim Kurnikov <maxim.kurnikov@gmail.com> Date: Sat, 28 Sep 2019 21:12:49 +0300 Subject: [PATCH 0439/3170] add per-file disallow_untyped_defs=False, and set it to True globally --- noxfile.py | 3 +++ setup.cfg | 1 + setup.py | 3 +++ src/pip/_internal/build_env.py | 1 + src/pip/_internal/cli/autocompletion.py | 3 +++ src/pip/_internal/cli/cmdoptions.py | 1 + src/pip/_internal/cli/command_context.py | 3 +++ src/pip/_internal/cli/parser.py | 4 ++++ src/pip/_internal/cli/req_command.py | 3 +++ src/pip/_internal/collector.py | 3 +++ src/pip/_internal/commands/__init__.py | 4 ++++ src/pip/_internal/commands/check.py | 3 +++ src/pip/_internal/commands/completion.py | 3 +++ src/pip/_internal/commands/configuration.py | 3 +++ src/pip/_internal/commands/debug.py | 3 +++ src/pip/_internal/commands/download.py | 3 +++ src/pip/_internal/commands/freeze.py | 3 +++ src/pip/_internal/commands/hash.py | 3 +++ src/pip/_internal/commands/help.py | 3 +++ src/pip/_internal/commands/install.py | 1 + src/pip/_internal/commands/list.py | 3 +++ src/pip/_internal/commands/search.py | 3 +++ src/pip/_internal/commands/show.py | 3 +++ src/pip/_internal/commands/uninstall.py | 3 +++ src/pip/_internal/commands/wheel.py | 4 ++++ src/pip/_internal/configuration.py | 1 + src/pip/_internal/distributions/base.py | 3 +++ src/pip/_internal/distributions/installed.py | 3 +++ src/pip/_internal/distributions/source/legacy.py | 3 +++ src/pip/_internal/distributions/wheel.py | 3 +++ src/pip/_internal/download.py | 3 +++ src/pip/_internal/exceptions.py | 4 ++++ src/pip/_internal/index.py | 1 + src/pip/_internal/legacy_resolve.py | 1 + src/pip/_internal/locations.py | 1 + src/pip/_internal/main.py | 3 +++ src/pip/_internal/models/candidate.py | 3 +++ src/pip/_internal/models/format_control.py | 1 + src/pip/_internal/models/link.py | 3 +++ src/pip/_internal/models/search_scope.py | 3 +++ src/pip/_internal/network/auth.py | 3 +++ src/pip/_internal/network/cache.py | 4 ++++ src/pip/_internal/network/session.py | 4 ++++ src/pip/_internal/operations/check.py | 1 + src/pip/_internal/operations/freeze.py | 1 + src/pip/_internal/operations/prepare.py | 1 + src/pip/_internal/req/constructors.py | 1 + src/pip/_internal/req/req_install.py | 1 + src/pip/_internal/utils/appdirs.py | 4 ++++ src/pip/_internal/utils/compat.py | 4 ++++ src/pip/_internal/utils/deprecation.py | 4 ++++ src/pip/_internal/utils/hashes.py | 3 +++ src/pip/_internal/utils/logging.py | 3 +++ src/pip/_internal/utils/marker_files.py | 3 +++ src/pip/_internal/utils/misc.py | 1 + src/pip/_internal/utils/models.py | 2 ++ src/pip/_internal/utils/outdated.py | 3 +++ src/pip/_internal/utils/temp_dir.py | 3 +++ src/pip/_internal/utils/ui.py | 1 + src/pip/_internal/utils/unpacking.py | 1 + src/pip/_internal/vcs/bazaar.py | 3 +++ src/pip/_internal/vcs/git.py | 3 +++ src/pip/_internal/vcs/mercurial.py | 3 +++ src/pip/_internal/vcs/subversion.py | 3 +++ src/pip/_internal/vcs/versioncontrol.py | 4 ++++ src/pip/_internal/wheel.py | 1 + tools/automation/vendoring/__init__.py | 3 +++ tools/tox_pip.py | 3 +++ 68 files changed, 177 insertions(+) diff --git a/noxfile.py b/noxfile.py index 7b19ee959fd..2702fd306e4 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1,6 +1,9 @@ """Automation using nox. """ +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + import io import os import shutil diff --git a/setup.cfg b/setup.cfg index 81396f9ba3e..05e24a23741 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,6 +29,7 @@ ignore = W504 [mypy] follow_imports = silent ignore_missing_imports = True +disallow_untyped_defs = True [mypy-pip/_vendor/*] follow_imports = skip diff --git a/setup.py b/setup.py index b05c1b8779b..db4c55eeb6a 100644 --- a/setup.py +++ b/setup.py @@ -1,3 +1,6 @@ +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + import codecs import os import re diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py index f1d9b378b24..e91f71af755 100644 --- a/src/pip/_internal/build_env.py +++ b/src/pip/_internal/build_env.py @@ -3,6 +3,7 @@ # The following comment should be removed at some point in the future. # mypy: strict-optional=False +# mypy: disallow-untyped-defs=False import logging import os diff --git a/src/pip/_internal/cli/autocompletion.py b/src/pip/_internal/cli/autocompletion.py index 287e62c78a5..5440241b342 100644 --- a/src/pip/_internal/cli/autocompletion.py +++ b/src/pip/_internal/cli/autocompletion.py @@ -1,6 +1,9 @@ """Logic that powers autocompletion installed by ``pip completion``. """ +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + import optparse import os import sys diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index d8a6ebd54b6..ffed050f875 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -9,6 +9,7 @@ # The following comment should be removed at some point in the future. # mypy: strict-optional=False +# mypy: disallow-untyped-defs=False from __future__ import absolute_import diff --git a/src/pip/_internal/cli/command_context.py b/src/pip/_internal/cli/command_context.py index c55f0116e19..3ab255f558f 100644 --- a/src/pip/_internal/cli/command_context.py +++ b/src/pip/_internal/cli/command_context.py @@ -1,3 +1,6 @@ +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + from contextlib import contextmanager from pip._vendor.contextlib2 import ExitStack diff --git a/src/pip/_internal/cli/parser.py b/src/pip/_internal/cli/parser.py index e1eaac42042..c99456bae88 100644 --- a/src/pip/_internal/cli/parser.py +++ b/src/pip/_internal/cli/parser.py @@ -1,4 +1,8 @@ """Base option parser setup""" + +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + from __future__ import absolute_import import logging diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 01f67012dcd..4bfbbc83cd9 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -5,6 +5,9 @@ PackageFinder machinery and all its vendored dependencies, etc. """ +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + import os from functools import partial diff --git a/src/pip/_internal/collector.py b/src/pip/_internal/collector.py index 5b3c88eb5ac..e6ee598e806 100644 --- a/src/pip/_internal/collector.py +++ b/src/pip/_internal/collector.py @@ -2,6 +2,9 @@ The main purpose of this module is to expose LinkCollector.collect_links(). """ +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + import cgi import itertools import logging diff --git a/src/pip/_internal/commands/__init__.py b/src/pip/_internal/commands/__init__.py index 11d2a14c61d..2a311f8fc89 100644 --- a/src/pip/_internal/commands/__init__.py +++ b/src/pip/_internal/commands/__init__.py @@ -1,6 +1,10 @@ """ Package containing all pip commands """ + +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + from __future__ import absolute_import import importlib diff --git a/src/pip/_internal/commands/check.py b/src/pip/_internal/commands/check.py index c9c3e30f3a8..968944611ea 100644 --- a/src/pip/_internal/commands/check.py +++ b/src/pip/_internal/commands/check.py @@ -1,3 +1,6 @@ +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + import logging from pip._internal.cli.base_command import Command diff --git a/src/pip/_internal/commands/completion.py b/src/pip/_internal/commands/completion.py index f8975a4398d..c532806e386 100644 --- a/src/pip/_internal/commands/completion.py +++ b/src/pip/_internal/commands/completion.py @@ -1,3 +1,6 @@ +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + from __future__ import absolute_import import sys diff --git a/src/pip/_internal/commands/configuration.py b/src/pip/_internal/commands/configuration.py index 6c3a0729523..9b6eb1602da 100644 --- a/src/pip/_internal/commands/configuration.py +++ b/src/pip/_internal/commands/configuration.py @@ -1,3 +1,6 @@ +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + import logging import os import subprocess diff --git a/src/pip/_internal/commands/debug.py b/src/pip/_internal/commands/debug.py index df31c83bf51..5322c828bde 100644 --- a/src/pip/_internal/commands/debug.py +++ b/src/pip/_internal/commands/debug.py @@ -1,3 +1,6 @@ +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + from __future__ import absolute_import import locale diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index 4712a894d8a..a63019fbf50 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -1,3 +1,6 @@ +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + from __future__ import absolute_import import logging diff --git a/src/pip/_internal/commands/freeze.py b/src/pip/_internal/commands/freeze.py index 112a380439d..c59eb3960a6 100644 --- a/src/pip/_internal/commands/freeze.py +++ b/src/pip/_internal/commands/freeze.py @@ -1,3 +1,6 @@ +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + from __future__ import absolute_import import sys diff --git a/src/pip/_internal/commands/hash.py b/src/pip/_internal/commands/hash.py index f8194648940..1dc7fb0eac9 100644 --- a/src/pip/_internal/commands/hash.py +++ b/src/pip/_internal/commands/hash.py @@ -1,3 +1,6 @@ +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + from __future__ import absolute_import import hashlib diff --git a/src/pip/_internal/commands/help.py b/src/pip/_internal/commands/help.py index 270e82b28c6..75af999b41e 100644 --- a/src/pip/_internal/commands/help.py +++ b/src/pip/_internal/commands/help.py @@ -1,3 +1,6 @@ +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + from __future__ import absolute_import from pip._internal.cli.base_command import Command diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index dc8bc3b8bdb..85c06093ac2 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -4,6 +4,7 @@ # couple errors where we have to know req.name is str rather than # Optional[str] for the InstallRequirement req. # mypy: strict-optional=False +# mypy: disallow-untyped-defs=False from __future__ import absolute_import diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index d6e38db8659..08b621ff9ae 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -1,3 +1,6 @@ +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + from __future__ import absolute_import import json diff --git a/src/pip/_internal/commands/search.py b/src/pip/_internal/commands/search.py index 9ae1fb533d9..2e880eec224 100644 --- a/src/pip/_internal/commands/search.py +++ b/src/pip/_internal/commands/search.py @@ -1,3 +1,6 @@ +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + from __future__ import absolute_import import logging diff --git a/src/pip/_internal/commands/show.py b/src/pip/_internal/commands/show.py index 3baf785bd22..a46b08eeb3d 100644 --- a/src/pip/_internal/commands/show.py +++ b/src/pip/_internal/commands/show.py @@ -1,3 +1,6 @@ +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + from __future__ import absolute_import import logging diff --git a/src/pip/_internal/commands/uninstall.py b/src/pip/_internal/commands/uninstall.py index 0d8e4662f09..1bde414a6c1 100644 --- a/src/pip/_internal/commands/uninstall.py +++ b/src/pip/_internal/commands/uninstall.py @@ -1,3 +1,6 @@ +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + from __future__ import absolute_import from pip._vendor.packaging.utils import canonicalize_name diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index b95732b0ce4..7230470b7d7 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -1,4 +1,8 @@ # -*- coding: utf-8 -*- + +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + from __future__ import absolute_import import logging diff --git a/src/pip/_internal/configuration.py b/src/pip/_internal/configuration.py index 6843557855f..858c6602946 100644 --- a/src/pip/_internal/configuration.py +++ b/src/pip/_internal/configuration.py @@ -13,6 +13,7 @@ # The following comment should be removed at some point in the future. # mypy: strict-optional=False +# mypy: disallow-untyped-defs=False import locale import logging diff --git a/src/pip/_internal/distributions/base.py b/src/pip/_internal/distributions/base.py index b9af3f025a0..929bbefb858 100644 --- a/src/pip/_internal/distributions/base.py +++ b/src/pip/_internal/distributions/base.py @@ -1,3 +1,6 @@ +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + import abc from pip._vendor.six import add_metaclass diff --git a/src/pip/_internal/distributions/installed.py b/src/pip/_internal/distributions/installed.py index c4a64e7ca55..454fb48c2b4 100644 --- a/src/pip/_internal/distributions/installed.py +++ b/src/pip/_internal/distributions/installed.py @@ -1,3 +1,6 @@ +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + from pip._internal.distributions.base import AbstractDistribution diff --git a/src/pip/_internal/distributions/source/legacy.py b/src/pip/_internal/distributions/source/legacy.py index 8005b1fc00a..ae1d9b40b68 100644 --- a/src/pip/_internal/distributions/source/legacy.py +++ b/src/pip/_internal/distributions/source/legacy.py @@ -1,3 +1,6 @@ +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + import logging from pip._internal.build_env import BuildEnvironment diff --git a/src/pip/_internal/distributions/wheel.py b/src/pip/_internal/distributions/wheel.py index de7be38ee8f..128951ff6d1 100644 --- a/src/pip/_internal/distributions/wheel.py +++ b/src/pip/_internal/distributions/wheel.py @@ -1,3 +1,6 @@ +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + from pip._vendor import pkg_resources from pip._internal.distributions.base import AbstractDistribution diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index d3415c5b380..6567fc375a6 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -1,3 +1,6 @@ +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + from __future__ import absolute_import import cgi diff --git a/src/pip/_internal/exceptions.py b/src/pip/_internal/exceptions.py index 78483d7d565..dddec789ef4 100644 --- a/src/pip/_internal/exceptions.py +++ b/src/pip/_internal/exceptions.py @@ -1,4 +1,8 @@ """Exceptions used throughout package""" + +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + from __future__ import absolute_import from itertools import chain, groupby, repeat diff --git a/src/pip/_internal/index.py b/src/pip/_internal/index.py index 81bdd4ad546..897444aae3f 100644 --- a/src/pip/_internal/index.py +++ b/src/pip/_internal/index.py @@ -2,6 +2,7 @@ # The following comment should be removed at some point in the future. # mypy: strict-optional=False +# mypy: disallow-untyped-defs=False from __future__ import absolute_import diff --git a/src/pip/_internal/legacy_resolve.py b/src/pip/_internal/legacy_resolve.py index 84855d73a39..c24158f4d37 100644 --- a/src/pip/_internal/legacy_resolve.py +++ b/src/pip/_internal/legacy_resolve.py @@ -12,6 +12,7 @@ # The following comment should be removed at some point in the future. # mypy: strict-optional=False +# mypy: disallow-untyped-defs=False import logging import sys diff --git a/src/pip/_internal/locations.py b/src/pip/_internal/locations.py index e7e8057c6ee..1899c7d03c4 100644 --- a/src/pip/_internal/locations.py +++ b/src/pip/_internal/locations.py @@ -2,6 +2,7 @@ # The following comment should be removed at some point in the future. # mypy: strict-optional=False +# mypy: disallow-untyped-defs=False from __future__ import absolute_import diff --git a/src/pip/_internal/main.py b/src/pip/_internal/main.py index 9b55d0f02de..1e922402a0b 100644 --- a/src/pip/_internal/main.py +++ b/src/pip/_internal/main.py @@ -1,5 +1,8 @@ """Primary application entrypoint. """ +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + from __future__ import absolute_import import locale diff --git a/src/pip/_internal/models/candidate.py b/src/pip/_internal/models/candidate.py index 1b99690f22c..4d49604ddfc 100644 --- a/src/pip/_internal/models/candidate.py +++ b/src/pip/_internal/models/candidate.py @@ -1,3 +1,6 @@ +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + from pip._vendor.packaging.version import parse as parse_version from pip._internal.utils.models import KeyBasedCompareMixin diff --git a/src/pip/_internal/models/format_control.py b/src/pip/_internal/models/format_control.py index e1d36e4ee29..5489b51d076 100644 --- a/src/pip/_internal/models/format_control.py +++ b/src/pip/_internal/models/format_control.py @@ -1,5 +1,6 @@ # The following comment should be removed at some point in the future. # mypy: strict-optional=False +# mypy: disallow-untyped-defs=False from pip._vendor.packaging.utils import canonicalize_name diff --git a/src/pip/_internal/models/link.py b/src/pip/_internal/models/link.py index 416b4445973..2d50d17989f 100644 --- a/src/pip/_internal/models/link.py +++ b/src/pip/_internal/models/link.py @@ -1,3 +1,6 @@ +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + import os import posixpath import re diff --git a/src/pip/_internal/models/search_scope.py b/src/pip/_internal/models/search_scope.py index 5d667deef59..6e387068b63 100644 --- a/src/pip/_internal/models/search_scope.py +++ b/src/pip/_internal/models/search_scope.py @@ -1,3 +1,6 @@ +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + import itertools import logging import os diff --git a/src/pip/_internal/network/auth.py b/src/pip/_internal/network/auth.py index cce79a521d2..1e1da54ca59 100644 --- a/src/pip/_internal/network/auth.py +++ b/src/pip/_internal/network/auth.py @@ -4,6 +4,9 @@ providing credentials in the context of network requests. """ +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + import logging from pip._vendor.requests.auth import AuthBase, HTTPBasicAuth diff --git a/src/pip/_internal/network/cache.py b/src/pip/_internal/network/cache.py index 9cd6403003e..d23c0ffaf5a 100644 --- a/src/pip/_internal/network/cache.py +++ b/src/pip/_internal/network/cache.py @@ -1,5 +1,9 @@ """HTTP cache implementation. """ + +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + import os from contextlib import contextmanager diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py index 0e9f44f562d..52e324ad8dc 100644 --- a/src/pip/_internal/network/session.py +++ b/src/pip/_internal/network/session.py @@ -1,6 +1,10 @@ """PipSession and supporting code, containing all pip-specific network request configuration and behavior. """ + +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + import email.utils import json import logging diff --git a/src/pip/_internal/operations/check.py b/src/pip/_internal/operations/check.py index 9f7fe6a7f61..6bd18841a20 100644 --- a/src/pip/_internal/operations/check.py +++ b/src/pip/_internal/operations/check.py @@ -3,6 +3,7 @@ # The following comment should be removed at some point in the future. # mypy: strict-optional=False +# mypy: disallow-untyped-defs=False import logging from collections import namedtuple diff --git a/src/pip/_internal/operations/freeze.py b/src/pip/_internal/operations/freeze.py index 6eaa85b19ec..bfdddf97952 100644 --- a/src/pip/_internal/operations/freeze.py +++ b/src/pip/_internal/operations/freeze.py @@ -1,5 +1,6 @@ # The following comment should be removed at some point in the future. # mypy: strict-optional=False +# mypy: disallow-untyped-defs=False from __future__ import absolute_import diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 18ae249ee1c..d0930458d11 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -3,6 +3,7 @@ # The following comment should be removed at some point in the future. # mypy: strict-optional=False +# mypy: disallow-untyped-defs=False import logging import os diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index acb353e816a..03b51484b16 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -10,6 +10,7 @@ # The following comment should be removed at some point in the future. # mypy: strict-optional=False +# mypy: disallow-untyped-defs=False import logging import os diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index f6fd29d3bad..612e41f7416 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -1,5 +1,6 @@ # The following comment should be removed at some point in the future. # mypy: strict-optional=False +# mypy: disallow-untyped-defs=False from __future__ import absolute_import diff --git a/src/pip/_internal/utils/appdirs.py b/src/pip/_internal/utils/appdirs.py index 14e45b1978e..06cd8314a5c 100644 --- a/src/pip/_internal/utils/appdirs.py +++ b/src/pip/_internal/utils/appdirs.py @@ -2,6 +2,10 @@ This code was taken from https://github.com/ActiveState/appdirs and modified to suit our purposes. """ + +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + from __future__ import absolute_import import os diff --git a/src/pip/_internal/utils/compat.py b/src/pip/_internal/utils/compat.py index ebaae3bd597..dbd84487559 100644 --- a/src/pip/_internal/utils/compat.py +++ b/src/pip/_internal/utils/compat.py @@ -1,5 +1,9 @@ """Stuff that differs in different Python versions and platform distributions.""" + +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + from __future__ import absolute_import, division import codecs diff --git a/src/pip/_internal/utils/deprecation.py b/src/pip/_internal/utils/deprecation.py index b9359bddc5f..2f20cfd49d3 100644 --- a/src/pip/_internal/utils/deprecation.py +++ b/src/pip/_internal/utils/deprecation.py @@ -1,6 +1,10 @@ """ A module that implements tooling to enable easy warnings about deprecations. """ + +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + from __future__ import absolute_import import logging diff --git a/src/pip/_internal/utils/hashes.py b/src/pip/_internal/utils/hashes.py index 4f075b0918c..a0d87a41ea3 100644 --- a/src/pip/_internal/utils/hashes.py +++ b/src/pip/_internal/utils/hashes.py @@ -1,3 +1,6 @@ +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + from __future__ import absolute_import import hashlib diff --git a/src/pip/_internal/utils/logging.py b/src/pip/_internal/utils/logging.py index 3fbec712709..dd5ff9cd080 100644 --- a/src/pip/_internal/utils/logging.py +++ b/src/pip/_internal/utils/logging.py @@ -1,3 +1,6 @@ +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + from __future__ import absolute_import import contextlib diff --git a/src/pip/_internal/utils/marker_files.py b/src/pip/_internal/utils/marker_files.py index c0396951f94..734cba4c1d4 100644 --- a/src/pip/_internal/utils/marker_files.py +++ b/src/pip/_internal/utils/marker_files.py @@ -1,3 +1,6 @@ +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + import os.path DELETE_MARKER_MESSAGE = '''\ diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 5b66a9c6dbe..42e913cd893 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -1,5 +1,6 @@ # The following comment should be removed at some point in the future. # mypy: strict-optional=False +# mypy: disallow-untyped-defs=False from __future__ import absolute_import diff --git a/src/pip/_internal/utils/models.py b/src/pip/_internal/utils/models.py index fccaf5ddce7..29e1441153b 100644 --- a/src/pip/_internal/utils/models.py +++ b/src/pip/_internal/utils/models.py @@ -1,5 +1,7 @@ """Utilities for defining models """ +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False import operator diff --git a/src/pip/_internal/utils/outdated.py b/src/pip/_internal/utils/outdated.py index 42e27279382..3275dd062ee 100644 --- a/src/pip/_internal/utils/outdated.py +++ b/src/pip/_internal/utils/outdated.py @@ -1,3 +1,6 @@ +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + from __future__ import absolute_import import datetime diff --git a/src/pip/_internal/utils/temp_dir.py b/src/pip/_internal/utils/temp_dir.py index 87e0becaee3..77d40be6da3 100644 --- a/src/pip/_internal/utils/temp_dir.py +++ b/src/pip/_internal/utils/temp_dir.py @@ -1,3 +1,6 @@ +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + from __future__ import absolute_import import errno diff --git a/src/pip/_internal/utils/ui.py b/src/pip/_internal/utils/ui.py index 80cdab37912..f96ab54d01a 100644 --- a/src/pip/_internal/utils/ui.py +++ b/src/pip/_internal/utils/ui.py @@ -1,5 +1,6 @@ # The following comment should be removed at some point in the future. # mypy: strict-optional=False +# mypy: disallow-untyped-defs=False from __future__ import absolute_import, division diff --git a/src/pip/_internal/utils/unpacking.py b/src/pip/_internal/utils/unpacking.py index 81a368f27ce..7252dc217bf 100644 --- a/src/pip/_internal/utils/unpacking.py +++ b/src/pip/_internal/utils/unpacking.py @@ -3,6 +3,7 @@ # The following comment should be removed at some point in the future. # mypy: strict-optional=False +# mypy: disallow-untyped-defs=False from __future__ import absolute_import diff --git a/src/pip/_internal/vcs/bazaar.py b/src/pip/_internal/vcs/bazaar.py index 325b378ecd2..dc532294948 100644 --- a/src/pip/_internal/vcs/bazaar.py +++ b/src/pip/_internal/vcs/bazaar.py @@ -1,3 +1,6 @@ +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + from __future__ import absolute_import import logging diff --git a/src/pip/_internal/vcs/git.py b/src/pip/_internal/vcs/git.py index 65069af7b7b..c571405d471 100644 --- a/src/pip/_internal/vcs/git.py +++ b/src/pip/_internal/vcs/git.py @@ -1,3 +1,6 @@ +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + from __future__ import absolute_import import logging diff --git a/src/pip/_internal/vcs/mercurial.py b/src/pip/_internal/vcs/mercurial.py index 4732d3863eb..96298e2a0f3 100644 --- a/src/pip/_internal/vcs/mercurial.py +++ b/src/pip/_internal/vcs/mercurial.py @@ -1,3 +1,6 @@ +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + from __future__ import absolute_import import logging diff --git a/src/pip/_internal/vcs/subversion.py b/src/pip/_internal/vcs/subversion.py index 2d9ed9a100d..7f6fca3ee8c 100644 --- a/src/pip/_internal/vcs/subversion.py +++ b/src/pip/_internal/vcs/subversion.py @@ -1,3 +1,6 @@ +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + from __future__ import absolute_import import logging diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index 27610602f16..d405d27da6f 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -1,4 +1,8 @@ """Handles all VCS (version control) support""" + +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + from __future__ import absolute_import import errno diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index 02bc3f5f0f7..3d3af86a500 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -4,6 +4,7 @@ # The following comment should be removed at some point in the future. # mypy: strict-optional=False +# mypy: disallow-untyped-defs=False from __future__ import absolute_import diff --git a/tools/automation/vendoring/__init__.py b/tools/automation/vendoring/__init__.py index 3f0278838d2..ca6aee8e71c 100644 --- a/tools/automation/vendoring/__init__.py +++ b/tools/automation/vendoring/__init__.py @@ -1,5 +1,8 @@ """"Vendoring script, python 3.5 with requests needed""" +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + import os import re import shutil diff --git a/tools/tox_pip.py b/tools/tox_pip.py index 8d91fbf56b0..5996dade6d2 100644 --- a/tools/tox_pip.py +++ b/tools/tox_pip.py @@ -1,3 +1,6 @@ +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + import os import shutil import subprocess From daae7f96f5a7d3cb2807212abd4f64c36d9886c0 Mon Sep 17 00:00:00 2001 From: Xavier Fernandez <xavier.fernandez@polyconseil.fr> Date: Sun, 29 Sep 2019 23:37:46 +0200 Subject: [PATCH 0440/3170] Add missing disallow-untyped-defs=False comment --- src/pip/_internal/network/xmlrpc.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/pip/_internal/network/xmlrpc.py b/src/pip/_internal/network/xmlrpc.py index 40d20595a09..121edd93056 100644 --- a/src/pip/_internal/network/xmlrpc.py +++ b/src/pip/_internal/network/xmlrpc.py @@ -1,5 +1,9 @@ """xmlrpclib.Transport implementation """ + +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + import logging from pip._vendor import requests From d1bfeec18a2dfda95164d0361726883d63c70210 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 30 Sep 2019 11:15:06 +0530 Subject: [PATCH 0441/3170] Move subprocessing related utilities to utils.subprocess --- src/pip/_internal/utils/misc.py | 236 +----------------------- src/pip/_internal/utils/subprocess.py | 247 ++++++++++++++++++++++++++ 2 files changed, 250 insertions(+), 233 deletions(-) create mode 100644 src/pip/_internal/utils/subprocess.py diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 42e913cd893..67729649590 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -13,7 +13,6 @@ import posixpath import shutil import stat -import subprocess import sys from collections import deque @@ -22,12 +21,12 @@ # why we ignore the type on this import. from pip._vendor.retrying import retry # type: ignore from pip._vendor.six import PY2, text_type -from pip._vendor.six.moves import input, shlex_quote +from pip._vendor.six.moves import input from pip._vendor.six.moves.urllib import parse as urllib_parse from pip._vendor.six.moves.urllib.parse import unquote as urllib_unquote from pip import __version__ -from pip._internal.exceptions import CommandError, InstallationError +from pip._internal.exceptions import CommandError from pip._internal.locations import ( get_major_minor_version, site_packages, @@ -35,7 +34,6 @@ ) from pip._internal.utils.compat import ( WINDOWS, - console_to_str, expanduser, stdlib_pkgs, str_to_display, @@ -54,14 +52,12 @@ if MYPY_CHECK_RUNNING: from typing import ( - Any, AnyStr, Container, Iterable, List, Mapping, Optional, Text, + Any, AnyStr, Container, Iterable, List, Optional, Text, Tuple, Union, cast, ) from pip._vendor.pkg_resources import Distribution - from pip._internal.utils.ui import SpinnerInterface VersionInfo = Tuple[int, int, int] - CommandArgs = List[Union[str, 'HiddenText']] else: # typing's cast() is needed at runtime, but we don't want to import typing. # Thus, we use a dummy no-op version, which we tell mypy to ignore. @@ -74,15 +70,11 @@ def cast(type_, value): # type: ignore 'format_size', 'is_installable_dir', 'normalize_path', 'renames', 'get_prog', - 'call_subprocess', 'captured_stdout', 'ensure_dir', 'get_installed_version', 'remove_auth_from_url'] logger = logging.getLogger(__name__) -subprocess_logger = logging.getLogger('pip.subprocessor') - -LOG_DIVIDER = '----------------------------------------' def get_pip_version(): @@ -533,228 +525,6 @@ def dist_location(dist): return normalize_path(dist.location) -def make_command(*args): - # type: (Union[str, HiddenText, CommandArgs]) -> CommandArgs - """ - Create a CommandArgs object. - """ - command_args = [] # type: CommandArgs - for arg in args: - # Check for list instead of CommandArgs since CommandArgs is - # only known during type-checking. - if isinstance(arg, list): - command_args.extend(arg) - else: - # Otherwise, arg is str or HiddenText. - command_args.append(arg) - - return command_args - - -def format_command_args(args): - # type: (Union[List[str], CommandArgs]) -> str - """ - Format command arguments for display. - """ - # For HiddenText arguments, display the redacted form by calling str(). - # Also, we don't apply str() to arguments that aren't HiddenText since - # this can trigger a UnicodeDecodeError in Python 2 if the argument - # has type unicode and includes a non-ascii character. (The type - # checker doesn't ensure the annotations are correct in all cases.) - return ' '.join( - shlex_quote(str(arg)) if isinstance(arg, HiddenText) - else shlex_quote(arg) for arg in args - ) - - -def reveal_command_args(args): - # type: (Union[List[str], CommandArgs]) -> List[str] - """ - Return the arguments in their raw, unredacted form. - """ - return [ - arg.secret if isinstance(arg, HiddenText) else arg for arg in args - ] - - -def make_subprocess_output_error( - cmd_args, # type: Union[List[str], CommandArgs] - cwd, # type: Optional[str] - lines, # type: List[Text] - exit_status, # type: int -): - # type: (...) -> Text - """ - Create and return the error message to use to log a subprocess error - with command output. - - :param lines: A list of lines, each ending with a newline. - """ - command = format_command_args(cmd_args) - # Convert `command` and `cwd` to text (unicode in Python 2) so we can use - # them as arguments in the unicode format string below. This avoids - # "UnicodeDecodeError: 'ascii' codec can't decode byte ..." in Python 2 - # if either contains a non-ascii character. - command_display = str_to_display(command, desc='command bytes') - cwd_display = path_to_display(cwd) - - # We know the joined output value ends in a newline. - output = ''.join(lines) - msg = ( - # Use a unicode string to avoid "UnicodeEncodeError: 'ascii' - # codec can't encode character ..." in Python 2 when a format - # argument (e.g. `output`) has a non-ascii character. - u'Command errored out with exit status {exit_status}:\n' - ' command: {command_display}\n' - ' cwd: {cwd_display}\n' - 'Complete output ({line_count} lines):\n{output}{divider}' - ).format( - exit_status=exit_status, - command_display=command_display, - cwd_display=cwd_display, - line_count=len(lines), - output=output, - divider=LOG_DIVIDER, - ) - return msg - - -def call_subprocess( - cmd, # type: Union[List[str], CommandArgs] - show_stdout=False, # type: bool - cwd=None, # type: Optional[str] - on_returncode='raise', # type: str - extra_ok_returncodes=None, # type: Optional[Iterable[int]] - command_desc=None, # type: Optional[str] - extra_environ=None, # type: Optional[Mapping[str, Any]] - unset_environ=None, # type: Optional[Iterable[str]] - spinner=None # type: Optional[SpinnerInterface] -): - # type: (...) -> Text - """ - Args: - show_stdout: if true, use INFO to log the subprocess's stderr and - stdout streams. Otherwise, use DEBUG. Defaults to False. - extra_ok_returncodes: an iterable of integer return codes that are - acceptable, in addition to 0. Defaults to None, which means []. - unset_environ: an iterable of environment variable names to unset - prior to calling subprocess.Popen(). - """ - if extra_ok_returncodes is None: - extra_ok_returncodes = [] - if unset_environ is None: - unset_environ = [] - # Most places in pip use show_stdout=False. What this means is-- - # - # - We connect the child's output (combined stderr and stdout) to a - # single pipe, which we read. - # - We log this output to stderr at DEBUG level as it is received. - # - If DEBUG logging isn't enabled (e.g. if --verbose logging wasn't - # requested), then we show a spinner so the user can still see the - # subprocess is in progress. - # - If the subprocess exits with an error, we log the output to stderr - # at ERROR level if it hasn't already been displayed to the console - # (e.g. if --verbose logging wasn't enabled). This way we don't log - # the output to the console twice. - # - # If show_stdout=True, then the above is still done, but with DEBUG - # replaced by INFO. - if show_stdout: - # Then log the subprocess output at INFO level. - log_subprocess = subprocess_logger.info - used_level = logging.INFO - else: - # Then log the subprocess output using DEBUG. This also ensures - # it will be logged to the log file (aka user_log), if enabled. - log_subprocess = subprocess_logger.debug - used_level = logging.DEBUG - - # Whether the subprocess will be visible in the console. - showing_subprocess = subprocess_logger.getEffectiveLevel() <= used_level - - # Only use the spinner if we're not showing the subprocess output - # and we have a spinner. - use_spinner = not showing_subprocess and spinner is not None - - if command_desc is None: - command_desc = format_command_args(cmd) - - log_subprocess("Running command %s", command_desc) - env = os.environ.copy() - if extra_environ: - env.update(extra_environ) - for name in unset_environ: - env.pop(name, None) - try: - proc = subprocess.Popen( - # Convert HiddenText objects to the underlying str. - reveal_command_args(cmd), - stderr=subprocess.STDOUT, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, cwd=cwd, env=env, - ) - proc.stdin.close() - except Exception as exc: - subprocess_logger.critical( - "Error %s while executing command %s", exc, command_desc, - ) - raise - all_output = [] - while True: - # The "line" value is a unicode string in Python 2. - line = console_to_str(proc.stdout.readline()) - if not line: - break - line = line.rstrip() - all_output.append(line + '\n') - - # Show the line immediately. - log_subprocess(line) - # Update the spinner. - if use_spinner: - spinner.spin() - try: - proc.wait() - finally: - if proc.stdout: - proc.stdout.close() - proc_had_error = ( - proc.returncode and proc.returncode not in extra_ok_returncodes - ) - if use_spinner: - if proc_had_error: - spinner.finish("error") - else: - spinner.finish("done") - if proc_had_error: - if on_returncode == 'raise': - if not showing_subprocess: - # Then the subprocess streams haven't been logged to the - # console yet. - msg = make_subprocess_output_error( - cmd_args=cmd, - cwd=cwd, - lines=all_output, - exit_status=proc.returncode, - ) - subprocess_logger.error(msg) - exc_msg = ( - 'Command errored out with exit status {}: {} ' - 'Check the logs for full command output.' - ).format(proc.returncode, command_desc) - raise InstallationError(exc_msg) - elif on_returncode == 'warn': - subprocess_logger.warning( - 'Command "%s" had error code %s in %s', - command_desc, proc.returncode, cwd, - ) - elif on_returncode == 'ignore': - pass - else: - raise ValueError('Invalid value: on_returncode=%s' % - repr(on_returncode)) - return ''.join(all_output) - - def write_output(msg, *args): # type: (str, str) -> None logger.info(msg, *args) diff --git a/src/pip/_internal/utils/subprocess.py b/src/pip/_internal/utils/subprocess.py new file mode 100644 index 00000000000..135c9b09a5f --- /dev/null +++ b/src/pip/_internal/utils/subprocess.py @@ -0,0 +1,247 @@ +# The following comment should be removed at some point in the future. +# mypy: strict-optional=False + +from __future__ import absolute_import + +import logging +import os +import subprocess + +from pip._vendor.six.moves import shlex_quote + +from pip._internal.exceptions import InstallationError +from pip._internal.utils.compat import console_to_str, str_to_display +from pip._internal.utils.misc import HiddenText, path_to_display +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import Any, Iterable, List, Mapping, Optional, Text, Union + from pip._internal.utils.ui import SpinnerInterface + + CommandArgs = List[Union[str, HiddenText]] + + +subprocess_logger = logging.getLogger('pip.subprocessor') +LOG_DIVIDER = '----------------------------------------' + + +def make_command(*args): + # type: (Union[str, HiddenText, CommandArgs]) -> CommandArgs + """ + Create a CommandArgs object. + """ + command_args = [] # type: CommandArgs + for arg in args: + # Check for list instead of CommandArgs since CommandArgs is + # only known during type-checking. + if isinstance(arg, list): + command_args.extend(arg) + else: + # Otherwise, arg is str or HiddenText. + command_args.append(arg) + + return command_args + + +def format_command_args(args): + # type: (Union[List[str], CommandArgs]) -> str + """ + Format command arguments for display. + """ + # For HiddenText arguments, display the redacted form by calling str(). + # Also, we don't apply str() to arguments that aren't HiddenText since + # this can trigger a UnicodeDecodeError in Python 2 if the argument + # has type unicode and includes a non-ascii character. (The type + # checker doesn't ensure the annotations are correct in all cases.) + return ' '.join( + shlex_quote(str(arg)) if isinstance(arg, HiddenText) + else shlex_quote(arg) for arg in args + ) + + +def reveal_command_args(args): + # type: (Union[List[str], CommandArgs]) -> List[str] + """ + Return the arguments in their raw, unredacted form. + """ + return [ + arg.secret if isinstance(arg, HiddenText) else arg for arg in args + ] + + +def make_subprocess_output_error( + cmd_args, # type: Union[List[str], CommandArgs] + cwd, # type: Optional[str] + lines, # type: List[Text] + exit_status, # type: int +): + # type: (...) -> Text + """ + Create and return the error message to use to log a subprocess error + with command output. + + :param lines: A list of lines, each ending with a newline. + """ + command = format_command_args(cmd_args) + # Convert `command` and `cwd` to text (unicode in Python 2) so we can use + # them as arguments in the unicode format string below. This avoids + # "UnicodeDecodeError: 'ascii' codec can't decode byte ..." in Python 2 + # if either contains a non-ascii character. + command_display = str_to_display(command, desc='command bytes') + cwd_display = path_to_display(cwd) + + # We know the joined output value ends in a newline. + output = ''.join(lines) + msg = ( + # Use a unicode string to avoid "UnicodeEncodeError: 'ascii' + # codec can't encode character ..." in Python 2 when a format + # argument (e.g. `output`) has a non-ascii character. + u'Command errored out with exit status {exit_status}:\n' + ' command: {command_display}\n' + ' cwd: {cwd_display}\n' + 'Complete output ({line_count} lines):\n{output}{divider}' + ).format( + exit_status=exit_status, + command_display=command_display, + cwd_display=cwd_display, + line_count=len(lines), + output=output, + divider=LOG_DIVIDER, + ) + return msg + + +def call_subprocess( + cmd, # type: Union[List[str], CommandArgs] + show_stdout=False, # type: bool + cwd=None, # type: Optional[str] + on_returncode='raise', # type: str + extra_ok_returncodes=None, # type: Optional[Iterable[int]] + command_desc=None, # type: Optional[str] + extra_environ=None, # type: Optional[Mapping[str, Any]] + unset_environ=None, # type: Optional[Iterable[str]] + spinner=None # type: Optional[SpinnerInterface] +): + # type: (...) -> Text + """ + Args: + show_stdout: if true, use INFO to log the subprocess's stderr and + stdout streams. Otherwise, use DEBUG. Defaults to False. + extra_ok_returncodes: an iterable of integer return codes that are + acceptable, in addition to 0. Defaults to None, which means []. + unset_environ: an iterable of environment variable names to unset + prior to calling subprocess.Popen(). + """ + if extra_ok_returncodes is None: + extra_ok_returncodes = [] + if unset_environ is None: + unset_environ = [] + # Most places in pip use show_stdout=False. What this means is-- + # + # - We connect the child's output (combined stderr and stdout) to a + # single pipe, which we read. + # - We log this output to stderr at DEBUG level as it is received. + # - If DEBUG logging isn't enabled (e.g. if --verbose logging wasn't + # requested), then we show a spinner so the user can still see the + # subprocess is in progress. + # - If the subprocess exits with an error, we log the output to stderr + # at ERROR level if it hasn't already been displayed to the console + # (e.g. if --verbose logging wasn't enabled). This way we don't log + # the output to the console twice. + # + # If show_stdout=True, then the above is still done, but with DEBUG + # replaced by INFO. + if show_stdout: + # Then log the subprocess output at INFO level. + log_subprocess = subprocess_logger.info + used_level = logging.INFO + else: + # Then log the subprocess output using DEBUG. This also ensures + # it will be logged to the log file (aka user_log), if enabled. + log_subprocess = subprocess_logger.debug + used_level = logging.DEBUG + + # Whether the subprocess will be visible in the console. + showing_subprocess = subprocess_logger.getEffectiveLevel() <= used_level + + # Only use the spinner if we're not showing the subprocess output + # and we have a spinner. + use_spinner = not showing_subprocess and spinner is not None + + if command_desc is None: + command_desc = format_command_args(cmd) + + log_subprocess("Running command %s", command_desc) + env = os.environ.copy() + if extra_environ: + env.update(extra_environ) + for name in unset_environ: + env.pop(name, None) + try: + proc = subprocess.Popen( + # Convert HiddenText objects to the underlying str. + reveal_command_args(cmd), + stderr=subprocess.STDOUT, stdin=subprocess.PIPE, + stdout=subprocess.PIPE, cwd=cwd, env=env, + ) + proc.stdin.close() + except Exception as exc: + subprocess_logger.critical( + "Error %s while executing command %s", exc, command_desc, + ) + raise + all_output = [] + while True: + # The "line" value is a unicode string in Python 2. + line = console_to_str(proc.stdout.readline()) + if not line: + break + line = line.rstrip() + all_output.append(line + '\n') + + # Show the line immediately. + log_subprocess(line) + # Update the spinner. + if use_spinner: + spinner.spin() + try: + proc.wait() + finally: + if proc.stdout: + proc.stdout.close() + proc_had_error = ( + proc.returncode and proc.returncode not in extra_ok_returncodes + ) + if use_spinner: + if proc_had_error: + spinner.finish("error") + else: + spinner.finish("done") + if proc_had_error: + if on_returncode == 'raise': + if not showing_subprocess: + # Then the subprocess streams haven't been logged to the + # console yet. + msg = make_subprocess_output_error( + cmd_args=cmd, + cwd=cwd, + lines=all_output, + exit_status=proc.returncode, + ) + subprocess_logger.error(msg) + exc_msg = ( + 'Command errored out with exit status {}: {} ' + 'Check the logs for full command output.' + ).format(proc.returncode, command_desc) + raise InstallationError(exc_msg) + elif on_returncode == 'warn': + subprocess_logger.warning( + 'Command "%s" had error code %s in %s', + command_desc, proc.returncode, cwd, + ) + elif on_returncode == 'ignore': + pass + else: + raise ValueError('Invalid value: on_returncode=%s' % + repr(on_returncode)) + return ''.join(all_output) From 2eafb0218b4d1f10975bd49bbfe0f8533c82eadd Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 30 Sep 2019 11:15:24 +0530 Subject: [PATCH 0442/3170] Update all the imports as needed --- src/pip/_internal/build_env.py | 2 +- src/pip/_internal/operations/generate_metadata.py | 3 ++- src/pip/_internal/req/req_install.py | 2 +- src/pip/_internal/utils/logging.py | 3 ++- src/pip/_internal/vcs/bazaar.py | 3 ++- src/pip/_internal/vcs/git.py | 3 ++- src/pip/_internal/vcs/mercurial.py | 3 ++- src/pip/_internal/vcs/subversion.py | 5 +++-- src/pip/_internal/vcs/versioncontrol.py | 6 +++--- src/pip/_internal/wheel.py | 8 +++----- 10 files changed, 21 insertions(+), 17 deletions(-) diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py index e91f71af755..5e6dc4602e0 100644 --- a/src/pip/_internal/build_env.py +++ b/src/pip/_internal/build_env.py @@ -16,7 +16,7 @@ from pip._vendor.pkg_resources import Requirement, VersionConflict, WorkingSet from pip import __file__ as pip_location -from pip._internal.utils.misc import call_subprocess +from pip._internal.utils.subprocess import call_subprocess from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.ui import open_spinner diff --git a/src/pip/_internal/operations/generate_metadata.py b/src/pip/_internal/operations/generate_metadata.py index 9101bd77da8..4d06ca7afed 100644 --- a/src/pip/_internal/operations/generate_metadata.py +++ b/src/pip/_internal/operations/generate_metadata.py @@ -4,8 +4,9 @@ import logging import os -from pip._internal.utils.misc import call_subprocess, ensure_dir +from pip._internal.utils.misc import ensure_dir from pip._internal.utils.setuptools_build import make_setuptools_shim_args +from pip._internal.utils.subprocess import call_subprocess from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 612e41f7416..c578bf22593 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -38,7 +38,6 @@ _make_build_dir, ask_path_exists, backup_dir, - call_subprocess, display_path, dist_in_site_packages, dist_in_usersite, @@ -50,6 +49,7 @@ ) from pip._internal.utils.packaging import get_metadata from pip._internal.utils.setuptools_build import make_setuptools_shim_args +from pip._internal.utils.subprocess import call_subprocess from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.ui import open_spinner diff --git a/src/pip/_internal/utils/logging.py b/src/pip/_internal/utils/logging.py index dd5ff9cd080..03725b0c1dd 100644 --- a/src/pip/_internal/utils/logging.py +++ b/src/pip/_internal/utils/logging.py @@ -15,7 +15,8 @@ from pip._internal.utils.compat import WINDOWS from pip._internal.utils.deprecation import DEPRECATION_MSG_PREFIX -from pip._internal.utils.misc import ensure_dir, subprocess_logger +from pip._internal.utils.misc import ensure_dir +from pip._internal.utils.subprocess import subprocess_logger try: import threading diff --git a/src/pip/_internal/vcs/bazaar.py b/src/pip/_internal/vcs/bazaar.py index dc532294948..347c06f9dc7 100644 --- a/src/pip/_internal/vcs/bazaar.py +++ b/src/pip/_internal/vcs/bazaar.py @@ -8,7 +8,8 @@ from pip._vendor.six.moves.urllib import parse as urllib_parse -from pip._internal.utils.misc import display_path, make_command, rmtree +from pip._internal.utils.misc import display_path, rmtree +from pip._internal.utils.subprocess import make_command from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.urls import path_to_url from pip._internal.vcs.versioncontrol import VersionControl, vcs diff --git a/src/pip/_internal/vcs/git.py b/src/pip/_internal/vcs/git.py index c571405d471..9c2e87ea14f 100644 --- a/src/pip/_internal/vcs/git.py +++ b/src/pip/_internal/vcs/git.py @@ -13,7 +13,8 @@ from pip._internal.exceptions import BadCommand from pip._internal.utils.compat import samefile -from pip._internal.utils.misc import display_path, make_command +from pip._internal.utils.misc import display_path +from pip._internal.utils.subprocess import make_command from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.vcs.versioncontrol import ( diff --git a/src/pip/_internal/vcs/mercurial.py b/src/pip/_internal/vcs/mercurial.py index 96298e2a0f3..7679b8640ca 100644 --- a/src/pip/_internal/vcs/mercurial.py +++ b/src/pip/_internal/vcs/mercurial.py @@ -8,7 +8,8 @@ from pip._vendor.six.moves import configparser -from pip._internal.utils.misc import display_path, make_command +from pip._internal.utils.misc import display_path +from pip._internal.utils.subprocess import make_command from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.urls import path_to_url diff --git a/src/pip/_internal/vcs/subversion.py b/src/pip/_internal/vcs/subversion.py index 7f6fca3ee8c..7a2cd9f6def 100644 --- a/src/pip/_internal/vcs/subversion.py +++ b/src/pip/_internal/vcs/subversion.py @@ -11,10 +11,10 @@ from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import ( display_path, - make_command, rmtree, split_auth_from_netloc, ) +from pip._internal.utils.subprocess import make_command from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.vcs.versioncontrol import VersionControl, vcs @@ -26,7 +26,8 @@ if MYPY_CHECK_RUNNING: from typing import Optional, Tuple - from pip._internal.utils.misc import CommandArgs, HiddenText + from pip._internal.utils.subprocess import CommandArgs + from pip._internal.utils.misc import HiddenText from pip._internal.vcs.versioncontrol import AuthInfo, RevOptions diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index d405d27da6f..086b2ba850c 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -18,13 +18,12 @@ from pip._internal.utils.misc import ( ask_path_exists, backup_dir, - call_subprocess, display_path, hide_url, hide_value, - make_command, rmtree, ) +from pip._internal.utils.subprocess import call_subprocess, make_command from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.urls import get_url_scheme @@ -33,7 +32,8 @@ Any, Dict, Iterable, List, Mapping, Optional, Text, Tuple, Type, Union ) from pip._internal.utils.ui import SpinnerInterface - from pip._internal.utils.misc import CommandArgs, HiddenText + from pip._internal.utils.misc import HiddenText + from pip._internal.utils.subprocess import CommandArgs AuthInfo = Tuple[Optional[str], Optional[str]] diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index 3d3af86a500..7d6a4773dbf 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -38,15 +38,13 @@ from pip._internal.models.link import Link from pip._internal.utils.logging import indent_log from pip._internal.utils.marker_files import has_delete_marker_file -from pip._internal.utils.misc import ( +from pip._internal.utils.misc import captured_stdout, ensure_dir, read_chunks +from pip._internal.utils.setuptools_build import make_setuptools_shim_args +from pip._internal.utils.subprocess import ( LOG_DIVIDER, call_subprocess, - captured_stdout, - ensure_dir, format_command_args, - read_chunks, ) -from pip._internal.utils.setuptools_build import make_setuptools_shim_args from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.ui import open_spinner From e9d79db6c4f538814f741451d10d9378b6547908 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 30 Sep 2019 11:33:19 +0530 Subject: [PATCH 0443/3170] Move tests for utils.subprocess to dedicated file --- tests/unit/test_utils.py | 394 --------------------------- tests/unit/test_utils_subprocess.py | 403 ++++++++++++++++++++++++++++ 2 files changed, 403 insertions(+), 394 deletions(-) create mode 100644 tests/unit/test_utils_subprocess.py diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 8a7933eb4b7..a383f153141 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -5,7 +5,6 @@ """ import codecs -import locale import os import shutil import stat @@ -13,8 +12,6 @@ import time import warnings from io import BytesIO -from logging import DEBUG, ERROR, INFO, WARNING -from textwrap import dedent import pytest from mock import Mock, patch @@ -37,15 +34,11 @@ HiddenText, build_netloc, build_url_from_netloc, - call_subprocess, egg_link_path, - format_command_args, get_installed_distributions, get_prog, hide_url, hide_value, - make_command, - make_subprocess_output_error, normalize_path, normalize_version_info, parse_netloc, @@ -59,7 +52,6 @@ split_auth_netloc_from_url, ) from pip._internal.utils.setuptools_build import make_setuptools_shim_args -from pip._internal.utils.ui import SpinnerInterface class Tests_EgglinkPath: @@ -634,392 +626,6 @@ def test_get_prog(self, monkeypatch, argv, executable, expected): assert get_prog() == expected -@pytest.mark.parametrize('args, expected', [ - (['pip', 'list'], 'pip list'), - (['foo', 'space space', 'new\nline', 'double"quote', "single'quote"], - """foo 'space space' 'new\nline' 'double"quote' 'single'"'"'quote'"""), - # Test HiddenText arguments. - (make_command(hide_value('secret1'), 'foo', hide_value('secret2')), - "'****' foo '****'"), -]) -def test_format_command_args(args, expected): - actual = format_command_args(args) - assert actual == expected - - -def test_make_subprocess_output_error(): - cmd_args = ['test', 'has space'] - cwd = '/path/to/cwd' - lines = ['line1\n', 'line2\n', 'line3\n'] - actual = make_subprocess_output_error( - cmd_args=cmd_args, - cwd=cwd, - lines=lines, - exit_status=3, - ) - expected = dedent("""\ - Command errored out with exit status 3: - command: test 'has space' - cwd: /path/to/cwd - Complete output (3 lines): - line1 - line2 - line3 - ----------------------------------------""") - assert actual == expected, 'actual: {}'.format(actual) - - -def test_make_subprocess_output_error__non_ascii_command_arg(monkeypatch): - """ - Test a command argument with a non-ascii character. - """ - cmd_args = ['foo', 'déf'] - if sys.version_info[0] == 2: - # Check in Python 2 that the str (bytes object) with the non-ascii - # character has the encoding we expect. (This comes from the source - # code encoding at the top of the file.) - assert cmd_args[1].decode('utf-8') == u'déf' - - # We need to monkeypatch so the encoding will be correct on Windows. - monkeypatch.setattr(locale, 'getpreferredencoding', lambda: 'utf-8') - actual = make_subprocess_output_error( - cmd_args=cmd_args, - cwd='/path/to/cwd', - lines=[], - exit_status=1, - ) - expected = dedent(u"""\ - Command errored out with exit status 1: - command: foo 'déf' - cwd: /path/to/cwd - Complete output (0 lines): - ----------------------------------------""") - assert actual == expected, u'actual: {}'.format(actual) - - -@pytest.mark.skipif("sys.version_info < (3,)") -def test_make_subprocess_output_error__non_ascii_cwd_python_3(monkeypatch): - """ - Test a str (text) cwd with a non-ascii character in Python 3. - """ - cmd_args = ['test'] - cwd = '/path/to/cwd/déf' - actual = make_subprocess_output_error( - cmd_args=cmd_args, - cwd=cwd, - lines=[], - exit_status=1, - ) - expected = dedent("""\ - Command errored out with exit status 1: - command: test - cwd: /path/to/cwd/déf - Complete output (0 lines): - ----------------------------------------""") - assert actual == expected, 'actual: {}'.format(actual) - - -@pytest.mark.parametrize('encoding', [ - 'utf-8', - # Test a Windows encoding. - 'cp1252', -]) -@pytest.mark.skipif("sys.version_info >= (3,)") -def test_make_subprocess_output_error__non_ascii_cwd_python_2( - monkeypatch, encoding, -): - """ - Test a str (bytes object) cwd with a non-ascii character in Python 2. - """ - cmd_args = ['test'] - cwd = u'/path/to/cwd/déf'.encode(encoding) - monkeypatch.setattr(sys, 'getfilesystemencoding', lambda: encoding) - actual = make_subprocess_output_error( - cmd_args=cmd_args, - cwd=cwd, - lines=[], - exit_status=1, - ) - expected = dedent(u"""\ - Command errored out with exit status 1: - command: test - cwd: /path/to/cwd/déf - Complete output (0 lines): - ----------------------------------------""") - assert actual == expected, u'actual: {}'.format(actual) - - -# This test is mainly important for checking unicode in Python 2. -def test_make_subprocess_output_error__non_ascii_line(): - """ - Test a line with a non-ascii character. - """ - lines = [u'curly-quote: \u2018\n'] - actual = make_subprocess_output_error( - cmd_args=['test'], - cwd='/path/to/cwd', - lines=lines, - exit_status=1, - ) - expected = dedent(u"""\ - Command errored out with exit status 1: - command: test - cwd: /path/to/cwd - Complete output (1 lines): - curly-quote: \u2018 - ----------------------------------------""") - assert actual == expected, u'actual: {}'.format(actual) - - -class FakeSpinner(SpinnerInterface): - - def __init__(self): - self.spin_count = 0 - self.final_status = None - - def spin(self): - self.spin_count += 1 - - def finish(self, final_status): - self.final_status = final_status - - -class TestCallSubprocess(object): - - """ - Test call_subprocess(). - """ - - def check_result( - self, capfd, caplog, log_level, spinner, result, expected, - expected_spinner, - ): - """ - Check the result of calling call_subprocess(). - - :param log_level: the logging level that caplog was set to. - :param spinner: the FakeSpinner object passed to call_subprocess() - to be checked. - :param result: the call_subprocess() return value to be checked. - :param expected: a pair (expected_proc, expected_records), where - 1) `expected_proc` is the expected return value of - call_subprocess() as a list of lines, or None if the return - value is expected to be None; - 2) `expected_records` is the expected value of - caplog.record_tuples. - :param expected_spinner: a 2-tuple of the spinner's expected - (spin_count, final_status). - """ - expected_proc, expected_records = expected - - if expected_proc is None: - assert result is None - else: - assert result.splitlines() == expected_proc - - # Confirm that stdout and stderr haven't been written to. - captured = capfd.readouterr() - assert (captured.out, captured.err) == ('', '') - - records = caplog.record_tuples - if len(records) != len(expected_records): - raise RuntimeError('{} != {}'.format(records, expected_records)) - - for record, expected_record in zip(records, expected_records): - # Check the logger_name and log level parts exactly. - assert record[:2] == expected_record[:2] - # For the message portion, check only a substring. Also, we - # can't use startswith() since the order of stdout and stderr - # isn't guaranteed in cases where stderr is also present. - # For example, we observed the stderr lines coming before stdout - # in CI for PyPy 2.7 even though stdout happens first - # chronologically. - assert expected_record[2] in record[2] - - assert (spinner.spin_count, spinner.final_status) == expected_spinner - - def prepare_call(self, caplog, log_level, command=None): - if command is None: - command = 'print("Hello"); print("world")' - - caplog.set_level(log_level) - spinner = FakeSpinner() - args = [sys.executable, '-c', command] - - return (args, spinner) - - def test_debug_logging(self, capfd, caplog): - """ - Test DEBUG logging (and without passing show_stdout=True). - """ - log_level = DEBUG - args, spinner = self.prepare_call(caplog, log_level) - result = call_subprocess(args, spinner=spinner) - - expected = (['Hello', 'world'], [ - ('pip.subprocessor', DEBUG, 'Running command '), - ('pip.subprocessor', DEBUG, 'Hello'), - ('pip.subprocessor', DEBUG, 'world'), - ]) - # The spinner shouldn't spin in this case since the subprocess - # output is already being logged to the console. - self.check_result( - capfd, caplog, log_level, spinner, result, expected, - expected_spinner=(0, None), - ) - - def test_info_logging(self, capfd, caplog): - """ - Test INFO logging (and without passing show_stdout=True). - """ - log_level = INFO - args, spinner = self.prepare_call(caplog, log_level) - result = call_subprocess(args, spinner=spinner) - - expected = (['Hello', 'world'], []) - # The spinner should spin twice in this case since the subprocess - # output isn't being written to the console. - self.check_result( - capfd, caplog, log_level, spinner, result, expected, - expected_spinner=(2, 'done'), - ) - - def test_info_logging__subprocess_error(self, capfd, caplog): - """ - Test INFO logging of a subprocess with an error (and without passing - show_stdout=True). - """ - log_level = INFO - command = 'print("Hello"); print("world"); exit("fail")' - args, spinner = self.prepare_call(caplog, log_level, command=command) - - with pytest.raises(InstallationError) as exc: - call_subprocess(args, spinner=spinner) - result = None - exc_message = str(exc.value) - assert exc_message.startswith( - 'Command errored out with exit status 1: ' - ) - assert exc_message.endswith('Check the logs for full command output.') - - expected = (None, [ - ('pip.subprocessor', ERROR, 'Complete output (3 lines):\n'), - ]) - # The spinner should spin three times in this case since the - # subprocess output isn't being written to the console. - self.check_result( - capfd, caplog, log_level, spinner, result, expected, - expected_spinner=(3, 'error'), - ) - - # Do some further checking on the captured log records to confirm - # that the subprocess output was logged. - last_record = caplog.record_tuples[-1] - last_message = last_record[2] - lines = last_message.splitlines() - - # We have to sort before comparing the lines because we can't - # guarantee the order in which stdout and stderr will appear. - # For example, we observed the stderr lines coming before stdout - # in CI for PyPy 2.7 even though stdout happens first chronologically. - actual = sorted(lines) - # Test the "command" line separately because we can't test an - # exact match. - command_line = actual.pop(1) - assert actual == [ - ' cwd: None', - '----------------------------------------', - 'Command errored out with exit status 1:', - 'Complete output (3 lines):', - 'Hello', - 'fail', - 'world', - ], 'lines: {}'.format(actual) # Show the full output on failure. - - assert command_line.startswith(' command: ') - assert command_line.endswith('print("world"); exit("fail")\'') - - def test_info_logging_with_show_stdout_true(self, capfd, caplog): - """ - Test INFO logging with show_stdout=True. - """ - log_level = INFO - args, spinner = self.prepare_call(caplog, log_level) - result = call_subprocess(args, spinner=spinner, show_stdout=True) - - expected = (['Hello', 'world'], [ - ('pip.subprocessor', INFO, 'Running command '), - ('pip.subprocessor', INFO, 'Hello'), - ('pip.subprocessor', INFO, 'world'), - ]) - # The spinner shouldn't spin in this case since the subprocess - # output is already being written to the console. - self.check_result( - capfd, caplog, log_level, spinner, result, expected, - expected_spinner=(0, None), - ) - - @pytest.mark.parametrize(( - 'exit_status', 'show_stdout', 'extra_ok_returncodes', 'log_level', - 'expected'), - [ - # The spinner should show here because show_stdout=False means - # the subprocess should get logged at DEBUG level, but the passed - # log level is only INFO. - (0, False, None, INFO, (None, 'done', 2)), - # Test some cases where the spinner should not be shown. - (0, False, None, DEBUG, (None, None, 0)), - # Test show_stdout=True. - (0, True, None, DEBUG, (None, None, 0)), - (0, True, None, INFO, (None, None, 0)), - # The spinner should show here because show_stdout=True means - # the subprocess should get logged at INFO level, but the passed - # log level is only WARNING. - (0, True, None, WARNING, (None, 'done', 2)), - # Test a non-zero exit status. - (3, False, None, INFO, (InstallationError, 'error', 2)), - # Test a non-zero exit status also in extra_ok_returncodes. - (3, False, (3, ), INFO, (None, 'done', 2)), - ]) - def test_spinner_finish( - self, exit_status, show_stdout, extra_ok_returncodes, log_level, - caplog, expected, - ): - """ - Test that the spinner finishes correctly. - """ - expected_exc_type = expected[0] - expected_final_status = expected[1] - expected_spin_count = expected[2] - - command = ( - 'print("Hello"); print("world"); exit({})'.format(exit_status) - ) - args, spinner = self.prepare_call(caplog, log_level, command=command) - try: - call_subprocess( - args, - show_stdout=show_stdout, - extra_ok_returncodes=extra_ok_returncodes, - spinner=spinner, - ) - except Exception as exc: - exc_type = type(exc) - else: - exc_type = None - - assert exc_type == expected_exc_type - assert spinner.final_status == expected_final_status - assert spinner.spin_count == expected_spin_count - - def test_closes_stdin(self): - with pytest.raises(InstallationError): - call_subprocess( - [sys.executable, '-c', 'input()'], - show_stdout=True, - ) - - @pytest.mark.parametrize('host_port, expected_netloc', [ # Test domain name. (('example.com', None), 'example.com'), diff --git a/tests/unit/test_utils_subprocess.py b/tests/unit/test_utils_subprocess.py new file mode 100644 index 00000000000..44b8a7b3c45 --- /dev/null +++ b/tests/unit/test_utils_subprocess.py @@ -0,0 +1,403 @@ +# -*- coding: utf-8 -*- +import locale +import sys +from logging import DEBUG, ERROR, INFO, WARNING +from textwrap import dedent + +import pytest + +from pip._internal.exceptions import InstallationError +from pip._internal.utils.misc import hide_value +from pip._internal.utils.subprocess import ( + call_subprocess, + format_command_args, + make_command, + make_subprocess_output_error, +) +from pip._internal.utils.ui import SpinnerInterface + + +@pytest.mark.parametrize('args, expected', [ + (['pip', 'list'], 'pip list'), + (['foo', 'space space', 'new\nline', 'double"quote', "single'quote"], + """foo 'space space' 'new\nline' 'double"quote' 'single'"'"'quote'"""), + # Test HiddenText arguments. + (make_command(hide_value('secret1'), 'foo', hide_value('secret2')), + "'****' foo '****'"), +]) +def test_format_command_args(args, expected): + actual = format_command_args(args) + assert actual == expected + + +def test_make_subprocess_output_error(): + cmd_args = ['test', 'has space'] + cwd = '/path/to/cwd' + lines = ['line1\n', 'line2\n', 'line3\n'] + actual = make_subprocess_output_error( + cmd_args=cmd_args, + cwd=cwd, + lines=lines, + exit_status=3, + ) + expected = dedent("""\ + Command errored out with exit status 3: + command: test 'has space' + cwd: /path/to/cwd + Complete output (3 lines): + line1 + line2 + line3 + ----------------------------------------""") + assert actual == expected, 'actual: {}'.format(actual) + + +def test_make_subprocess_output_error__non_ascii_command_arg(monkeypatch): + """ + Test a command argument with a non-ascii character. + """ + cmd_args = ['foo', 'déf'] + if sys.version_info[0] == 2: + # Check in Python 2 that the str (bytes object) with the non-ascii + # character has the encoding we expect. (This comes from the source + # code encoding at the top of the file.) + assert cmd_args[1].decode('utf-8') == u'déf' + + # We need to monkeypatch so the encoding will be correct on Windows. + monkeypatch.setattr(locale, 'getpreferredencoding', lambda: 'utf-8') + actual = make_subprocess_output_error( + cmd_args=cmd_args, + cwd='/path/to/cwd', + lines=[], + exit_status=1, + ) + expected = dedent(u"""\ + Command errored out with exit status 1: + command: foo 'déf' + cwd: /path/to/cwd + Complete output (0 lines): + ----------------------------------------""") + assert actual == expected, u'actual: {}'.format(actual) + + +@pytest.mark.skipif("sys.version_info < (3,)") +def test_make_subprocess_output_error__non_ascii_cwd_python_3(monkeypatch): + """ + Test a str (text) cwd with a non-ascii character in Python 3. + """ + cmd_args = ['test'] + cwd = '/path/to/cwd/déf' + actual = make_subprocess_output_error( + cmd_args=cmd_args, + cwd=cwd, + lines=[], + exit_status=1, + ) + expected = dedent("""\ + Command errored out with exit status 1: + command: test + cwd: /path/to/cwd/déf + Complete output (0 lines): + ----------------------------------------""") + assert actual == expected, 'actual: {}'.format(actual) + + +@pytest.mark.parametrize('encoding', [ + 'utf-8', + # Test a Windows encoding. + 'cp1252', +]) +@pytest.mark.skipif("sys.version_info >= (3,)") +def test_make_subprocess_output_error__non_ascii_cwd_python_2( + monkeypatch, encoding, +): + """ + Test a str (bytes object) cwd with a non-ascii character in Python 2. + """ + cmd_args = ['test'] + cwd = u'/path/to/cwd/déf'.encode(encoding) + monkeypatch.setattr(sys, 'getfilesystemencoding', lambda: encoding) + actual = make_subprocess_output_error( + cmd_args=cmd_args, + cwd=cwd, + lines=[], + exit_status=1, + ) + expected = dedent(u"""\ + Command errored out with exit status 1: + command: test + cwd: /path/to/cwd/déf + Complete output (0 lines): + ----------------------------------------""") + assert actual == expected, u'actual: {}'.format(actual) + + +# This test is mainly important for checking unicode in Python 2. +def test_make_subprocess_output_error__non_ascii_line(): + """ + Test a line with a non-ascii character. + """ + lines = [u'curly-quote: \u2018\n'] + actual = make_subprocess_output_error( + cmd_args=['test'], + cwd='/path/to/cwd', + lines=lines, + exit_status=1, + ) + expected = dedent(u"""\ + Command errored out with exit status 1: + command: test + cwd: /path/to/cwd + Complete output (1 lines): + curly-quote: \u2018 + ----------------------------------------""") + assert actual == expected, u'actual: {}'.format(actual) + + +class FakeSpinner(SpinnerInterface): + + def __init__(self): + self.spin_count = 0 + self.final_status = None + + def spin(self): + self.spin_count += 1 + + def finish(self, final_status): + self.final_status = final_status + + +class TestCallSubprocess(object): + + """ + Test call_subprocess(). + """ + + def check_result( + self, capfd, caplog, log_level, spinner, result, expected, + expected_spinner, + ): + """ + Check the result of calling call_subprocess(). + + :param log_level: the logging level that caplog was set to. + :param spinner: the FakeSpinner object passed to call_subprocess() + to be checked. + :param result: the call_subprocess() return value to be checked. + :param expected: a pair (expected_proc, expected_records), where + 1) `expected_proc` is the expected return value of + call_subprocess() as a list of lines, or None if the return + value is expected to be None; + 2) `expected_records` is the expected value of + caplog.record_tuples. + :param expected_spinner: a 2-tuple of the spinner's expected + (spin_count, final_status). + """ + expected_proc, expected_records = expected + + if expected_proc is None: + assert result is None + else: + assert result.splitlines() == expected_proc + + # Confirm that stdout and stderr haven't been written to. + captured = capfd.readouterr() + assert (captured.out, captured.err) == ('', '') + + records = caplog.record_tuples + if len(records) != len(expected_records): + raise RuntimeError('{} != {}'.format(records, expected_records)) + + for record, expected_record in zip(records, expected_records): + # Check the logger_name and log level parts exactly. + assert record[:2] == expected_record[:2] + # For the message portion, check only a substring. Also, we + # can't use startswith() since the order of stdout and stderr + # isn't guaranteed in cases where stderr is also present. + # For example, we observed the stderr lines coming before stdout + # in CI for PyPy 2.7 even though stdout happens first + # chronologically. + assert expected_record[2] in record[2] + + assert (spinner.spin_count, spinner.final_status) == expected_spinner + + def prepare_call(self, caplog, log_level, command=None): + if command is None: + command = 'print("Hello"); print("world")' + + caplog.set_level(log_level) + spinner = FakeSpinner() + args = [sys.executable, '-c', command] + + return (args, spinner) + + def test_debug_logging(self, capfd, caplog): + """ + Test DEBUG logging (and without passing show_stdout=True). + """ + log_level = DEBUG + args, spinner = self.prepare_call(caplog, log_level) + result = call_subprocess(args, spinner=spinner) + + expected = (['Hello', 'world'], [ + ('pip.subprocessor', DEBUG, 'Running command '), + ('pip.subprocessor', DEBUG, 'Hello'), + ('pip.subprocessor', DEBUG, 'world'), + ]) + # The spinner shouldn't spin in this case since the subprocess + # output is already being logged to the console. + self.check_result( + capfd, caplog, log_level, spinner, result, expected, + expected_spinner=(0, None), + ) + + def test_info_logging(self, capfd, caplog): + """ + Test INFO logging (and without passing show_stdout=True). + """ + log_level = INFO + args, spinner = self.prepare_call(caplog, log_level) + result = call_subprocess(args, spinner=spinner) + + expected = (['Hello', 'world'], []) + # The spinner should spin twice in this case since the subprocess + # output isn't being written to the console. + self.check_result( + capfd, caplog, log_level, spinner, result, expected, + expected_spinner=(2, 'done'), + ) + + def test_info_logging__subprocess_error(self, capfd, caplog): + """ + Test INFO logging of a subprocess with an error (and without passing + show_stdout=True). + """ + log_level = INFO + command = 'print("Hello"); print("world"); exit("fail")' + args, spinner = self.prepare_call(caplog, log_level, command=command) + + with pytest.raises(InstallationError) as exc: + call_subprocess(args, spinner=spinner) + result = None + exc_message = str(exc.value) + assert exc_message.startswith( + 'Command errored out with exit status 1: ' + ) + assert exc_message.endswith('Check the logs for full command output.') + + expected = (None, [ + ('pip.subprocessor', ERROR, 'Complete output (3 lines):\n'), + ]) + # The spinner should spin three times in this case since the + # subprocess output isn't being written to the console. + self.check_result( + capfd, caplog, log_level, spinner, result, expected, + expected_spinner=(3, 'error'), + ) + + # Do some further checking on the captured log records to confirm + # that the subprocess output was logged. + last_record = caplog.record_tuples[-1] + last_message = last_record[2] + lines = last_message.splitlines() + + # We have to sort before comparing the lines because we can't + # guarantee the order in which stdout and stderr will appear. + # For example, we observed the stderr lines coming before stdout + # in CI for PyPy 2.7 even though stdout happens first chronologically. + actual = sorted(lines) + # Test the "command" line separately because we can't test an + # exact match. + command_line = actual.pop(1) + assert actual == [ + ' cwd: None', + '----------------------------------------', + 'Command errored out with exit status 1:', + 'Complete output (3 lines):', + 'Hello', + 'fail', + 'world', + ], 'lines: {}'.format(actual) # Show the full output on failure. + + assert command_line.startswith(' command: ') + assert command_line.endswith('print("world"); exit("fail")\'') + + def test_info_logging_with_show_stdout_true(self, capfd, caplog): + """ + Test INFO logging with show_stdout=True. + """ + log_level = INFO + args, spinner = self.prepare_call(caplog, log_level) + result = call_subprocess(args, spinner=spinner, show_stdout=True) + + expected = (['Hello', 'world'], [ + ('pip.subprocessor', INFO, 'Running command '), + ('pip.subprocessor', INFO, 'Hello'), + ('pip.subprocessor', INFO, 'world'), + ]) + # The spinner shouldn't spin in this case since the subprocess + # output is already being written to the console. + self.check_result( + capfd, caplog, log_level, spinner, result, expected, + expected_spinner=(0, None), + ) + + @pytest.mark.parametrize(( + 'exit_status', 'show_stdout', 'extra_ok_returncodes', 'log_level', + 'expected'), + [ + # The spinner should show here because show_stdout=False means + # the subprocess should get logged at DEBUG level, but the passed + # log level is only INFO. + (0, False, None, INFO, (None, 'done', 2)), + # Test some cases where the spinner should not be shown. + (0, False, None, DEBUG, (None, None, 0)), + # Test show_stdout=True. + (0, True, None, DEBUG, (None, None, 0)), + (0, True, None, INFO, (None, None, 0)), + # The spinner should show here because show_stdout=True means + # the subprocess should get logged at INFO level, but the passed + # log level is only WARNING. + (0, True, None, WARNING, (None, 'done', 2)), + # Test a non-zero exit status. + (3, False, None, INFO, (InstallationError, 'error', 2)), + # Test a non-zero exit status also in extra_ok_returncodes. + (3, False, (3, ), INFO, (None, 'done', 2)), + ]) + def test_spinner_finish( + self, exit_status, show_stdout, extra_ok_returncodes, log_level, + caplog, expected, + ): + """ + Test that the spinner finishes correctly. + """ + expected_exc_type = expected[0] + expected_final_status = expected[1] + expected_spin_count = expected[2] + + command = ( + 'print("Hello"); print("world"); exit({})'.format(exit_status) + ) + args, spinner = self.prepare_call(caplog, log_level, command=command) + try: + call_subprocess( + args, + show_stdout=show_stdout, + extra_ok_returncodes=extra_ok_returncodes, + spinner=spinner, + ) + except Exception as exc: + exc_type = type(exc) + else: + exc_type = None + + assert exc_type == expected_exc_type + assert spinner.final_status == expected_final_status + assert spinner.spin_count == expected_spin_count + + def test_closes_stdin(self): + with pytest.raises(InstallationError): + call_subprocess( + [sys.executable, '-c', 'input()'], + show_stdout=True, + ) From 27fdffbab6d5054ff4d6657874b5ff53a06f9f8a Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 29 Sep 2019 09:30:37 +0530 Subject: [PATCH 0444/3170] Remove dependency of utils.logging on utils.suprocess --- src/pip/_internal/utils/logging.py | 4 ++-- src/pip/_internal/utils/subprocess.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/utils/logging.py b/src/pip/_internal/utils/logging.py index 03725b0c1dd..7767111a6ba 100644 --- a/src/pip/_internal/utils/logging.py +++ b/src/pip/_internal/utils/logging.py @@ -9,14 +9,13 @@ import logging.handlers import os import sys -from logging import Filter +from logging import Filter, getLogger from pip._vendor.six import PY2 from pip._internal.utils.compat import WINDOWS from pip._internal.utils.deprecation import DEPRECATION_MSG_PREFIX from pip._internal.utils.misc import ensure_dir -from pip._internal.utils.subprocess import subprocess_logger try: import threading @@ -54,6 +53,7 @@ _log_state = threading.local() _log_state.indentation = 0 +subprocess_logger = getLogger('pip.subprocessor') class BrokenStdoutLoggingError(Exception): diff --git a/src/pip/_internal/utils/subprocess.py b/src/pip/_internal/utils/subprocess.py index 135c9b09a5f..3e2ad301cca 100644 --- a/src/pip/_internal/utils/subprocess.py +++ b/src/pip/_internal/utils/subprocess.py @@ -11,6 +11,7 @@ from pip._internal.exceptions import InstallationError from pip._internal.utils.compat import console_to_str, str_to_display +from pip._internal.utils.logging import subprocess_logger from pip._internal.utils.misc import HiddenText, path_to_display from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -21,7 +22,6 @@ CommandArgs = List[Union[str, HiddenText]] -subprocess_logger = logging.getLogger('pip.subprocessor') LOG_DIVIDER = '----------------------------------------' From 46bd454e3e6ea1739594b554082c991b565aa283 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sat, 28 Sep 2019 12:38:22 +0530 Subject: [PATCH 0445/3170] Use pep517.Pep517HookCaller.subprocess_runner Also, create a new utility function for showing a spinner when running a subprocess. Why: The subprocess_runner API was specifically added to make it possible for pip to stop monkey-patching Pep517HookCaller, while still maintaining its output style. The relevant monkeypatch will be removed in a follow up commit. --- .../_internal/distributions/source/legacy.py | 11 +++++-- src/pip/_internal/req/req_install.py | 16 ++++++---- src/pip/_internal/utils/subprocess.py | 30 ++++++++++++++++++- src/pip/_internal/wheel.py | 14 ++++++--- 4 files changed, 57 insertions(+), 14 deletions(-) diff --git a/src/pip/_internal/distributions/source/legacy.py b/src/pip/_internal/distributions/source/legacy.py index ae1d9b40b68..5651bc5c61b 100644 --- a/src/pip/_internal/distributions/source/legacy.py +++ b/src/pip/_internal/distributions/source/legacy.py @@ -6,6 +6,7 @@ from pip._internal.build_env import BuildEnvironment from pip._internal.distributions.base import AbstractDistribution from pip._internal.exceptions import InstallationError +from pip._internal.utils.subprocess import run_with_spinner_message logger = logging.getLogger(__name__) @@ -81,9 +82,13 @@ def _raise_conflicts(conflicting_with, conflicting_reqs): # This must be done in a second pass, as the pyproject.toml # dependencies must be installed before we can call the backend. with self.req.build_env: - # We need to have the env active when calling the hook. - self.req.spin_message = "Getting requirements to build wheel" - reqs = self.req.pep517_backend.get_requires_for_build_wheel() + runner = run_with_spinner_message( + "Getting requirements to build wheel" + ) + backend = self.req.pep517_backend + with backend.subprocess_runner(runner): + reqs = backend.get_requires_for_build_wheel() + conflicting, missing = self.req.build_env.check_requirements(reqs) if conflicting: _raise_conflicts("the backend dependencies", conflicting) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index c578bf22593..6f13bc05ded 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -49,7 +49,10 @@ ) from pip._internal.utils.packaging import get_metadata from pip._internal.utils.setuptools_build import make_setuptools_shim_args -from pip._internal.utils.subprocess import call_subprocess +from pip._internal.utils.subprocess import ( + call_subprocess, + run_with_spinner_message, +) from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.ui import open_spinner @@ -58,7 +61,7 @@ if MYPY_CHECK_RUNNING: from typing import ( - Any, Dict, Iterable, List, Mapping, Optional, Sequence, Union, + Any, Dict, Iterable, List, Optional, Sequence, Union, ) from pip._internal.build_env import BuildEnvironment from pip._internal.cache import WheelCache @@ -622,11 +625,12 @@ def prepare_pep517_metadata(self): # Note that Pep517HookCaller implements a fallback for # prepare_metadata_for_build_wheel, so we don't have to # consider the possibility that this hook doesn't exist. + runner = run_with_spinner_message("Preparing wheel metadata") backend = self.pep517_backend - self.spin_message = "Preparing wheel metadata" - distinfo_dir = backend.prepare_metadata_for_build_wheel( - metadata_dir - ) + with backend.subprocess_runner(runner): + distinfo_dir = backend.prepare_metadata_for_build_wheel( + metadata_dir + ) return os.path.join(metadata_dir, distinfo_dir) diff --git a/src/pip/_internal/utils/subprocess.py b/src/pip/_internal/utils/subprocess.py index 3e2ad301cca..7a0bedf16fc 100644 --- a/src/pip/_internal/utils/subprocess.py +++ b/src/pip/_internal/utils/subprocess.py @@ -14,9 +14,12 @@ from pip._internal.utils.logging import subprocess_logger from pip._internal.utils.misc import HiddenText, path_to_display from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from pip._internal.utils.ui import open_spinner if MYPY_CHECK_RUNNING: - from typing import Any, Iterable, List, Mapping, Optional, Text, Union + from typing import ( + Any, Callable, Iterable, List, Mapping, Optional, Text, Union, + ) from pip._internal.utils.ui import SpinnerInterface CommandArgs = List[Union[str, HiddenText]] @@ -245,3 +248,28 @@ def call_subprocess( raise ValueError('Invalid value: on_returncode=%s' % repr(on_returncode)) return ''.join(all_output) + + +def run_with_spinner_message(message): + # type: (str) -> Callable + """Provide a subprocess_runner that shows a spinner message. + + Intended for use with for pep517's Pep517HookCaller. Thus, the runner has + an API that matches what's expected by Pep517HookCaller.subprocess_runner. + """ + + def runner( + cmd, # type: List[str] + cwd=None, # type: Optional[str] + extra_environ=None # type: Optional[Mapping[str, Any]] + ): + # type: (...) -> None + with open_spinner(message) as spinner: + call_subprocess( + cmd, + cwd=cwd, + extra_environ=extra_environ, + spinner=spinner, + ) + + return runner diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index 7d6a4773dbf..7c37a70c031 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -44,6 +44,7 @@ LOG_DIVIDER, call_subprocess, format_command_args, + run_with_spinner_message, ) from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -979,12 +980,17 @@ def _build_one_pep517(self, req, tempd, python_tag=None): '--build-options is present' % (req.name,)) return None try: - req.spin_message = 'Building wheel for %s (PEP 517)' % (req.name,) logger.debug('Destination directory: %s', tempd) - wheel_name = req.pep517_backend.build_wheel( - tempd, - metadata_directory=req.metadata_directory + + runner = run_with_spinner_message( + 'Building wheel for {} (PEP 517)'.format(req.name) ) + backend = req.pep517_backend + with backend.subprocess_runner(runner): + wheel_name = backend.build_wheel( + tempd, + metadata_directory=req.metadata_directory, + ) if python_tag: # General PEP 517 backends don't necessarily support # a "--python-tag" option, so we rename the wheel From 489312e4d9e334abd38fdca97214540c4cd5599e Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sat, 28 Sep 2019 12:38:56 +0530 Subject: [PATCH 0446/3170] Stop monkeypatching InstallRequirement.pep517_backend --- src/pip/_internal/req/req_install.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 6f13bc05ded..8b86238063b 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -550,26 +550,6 @@ def load_pyproject_toml(self): self.unpacked_source_directory, backend ) - # Use a custom function to call subprocesses - self.spin_message = "" - - def runner( - cmd, # type: List[str] - cwd=None, # type: Optional[str] - extra_environ=None # type: Optional[Mapping[str, Any]] - ): - # type: (...) -> None - with open_spinner(self.spin_message) as spinner: - call_subprocess( - cmd, - cwd=cwd, - extra_environ=extra_environ, - spinner=spinner - ) - self.spin_message = "" - - self.pep517_backend._subprocess_runner = runner - def prepare_metadata(self): # type: () -> None """Ensure that project metadata is available. From 65621002fc596bba112ce0db5d768a10f5832a54 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 29 Sep 2019 11:25:01 +0530 Subject: [PATCH 0447/3170] Use run_with_spinner_message() for "setup.py install" --- src/pip/_internal/req/req_install.py | 19 +++++++++---------- 1 file changed, 9 insertions(+), 10 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 8b86238063b..4ef776eaa43 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -55,7 +55,6 @@ ) from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from pip._internal.utils.ui import open_spinner from pip._internal.utils.virtualenv import running_under_virtualenv from pip._internal.vcs import vcs @@ -935,15 +934,15 @@ def install( install_args = self.get_install_args( 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(): - with self.build_env: - call_subprocess( - install_args + install_options, - cwd=self.unpacked_source_directory, - spinner=spinner, - ) + + runner = run_with_spinner_message( + "Running setup.py install for {}".format(self.name) + ) + with indent_log(), self.build_env: + runner( + cmd=install_args + install_options, + cwd=self.unpacked_source_directory, + ) if not os.path.exists(record_filename): logger.debug('Record file %s not found', record_filename) From 1ad0495fdd61c3c1fa67645558c4dec5751c21e1 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 30 Sep 2019 12:06:05 +0530 Subject: [PATCH 0448/3170] Rename {run -> runner}_with_spinner_message --- src/pip/_internal/distributions/source/legacy.py | 4 ++-- src/pip/_internal/req/req_install.py | 6 +++--- src/pip/_internal/utils/subprocess.py | 2 +- src/pip/_internal/wheel.py | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/distributions/source/legacy.py b/src/pip/_internal/distributions/source/legacy.py index 5651bc5c61b..ab43afbec84 100644 --- a/src/pip/_internal/distributions/source/legacy.py +++ b/src/pip/_internal/distributions/source/legacy.py @@ -6,7 +6,7 @@ from pip._internal.build_env import BuildEnvironment from pip._internal.distributions.base import AbstractDistribution from pip._internal.exceptions import InstallationError -from pip._internal.utils.subprocess import run_with_spinner_message +from pip._internal.utils.subprocess import runner_with_spinner_message logger = logging.getLogger(__name__) @@ -82,7 +82,7 @@ def _raise_conflicts(conflicting_with, conflicting_reqs): # This must be done in a second pass, as the pyproject.toml # dependencies must be installed before we can call the backend. with self.req.build_env: - runner = run_with_spinner_message( + runner = runner_with_spinner_message( "Getting requirements to build wheel" ) backend = self.req.pep517_backend diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 4ef776eaa43..c59a4166ecd 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -51,7 +51,7 @@ from pip._internal.utils.setuptools_build import make_setuptools_shim_args from pip._internal.utils.subprocess import ( call_subprocess, - run_with_spinner_message, + runner_with_spinner_message, ) from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -604,7 +604,7 @@ def prepare_pep517_metadata(self): # Note that Pep517HookCaller implements a fallback for # prepare_metadata_for_build_wheel, so we don't have to # consider the possibility that this hook doesn't exist. - runner = run_with_spinner_message("Preparing wheel metadata") + runner = runner_with_spinner_message("Preparing wheel metadata") backend = self.pep517_backend with backend.subprocess_runner(runner): distinfo_dir = backend.prepare_metadata_for_build_wheel( @@ -935,7 +935,7 @@ def install( global_options, record_filename, root, prefix, pycompile, ) - runner = run_with_spinner_message( + runner = runner_with_spinner_message( "Running setup.py install for {}".format(self.name) ) with indent_log(), self.build_env: diff --git a/src/pip/_internal/utils/subprocess.py b/src/pip/_internal/utils/subprocess.py index 7a0bedf16fc..ddb418d2467 100644 --- a/src/pip/_internal/utils/subprocess.py +++ b/src/pip/_internal/utils/subprocess.py @@ -250,7 +250,7 @@ def call_subprocess( return ''.join(all_output) -def run_with_spinner_message(message): +def runner_with_spinner_message(message): # type: (str) -> Callable """Provide a subprocess_runner that shows a spinner message. diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index 7c37a70c031..8f9778c7d29 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -44,7 +44,7 @@ LOG_DIVIDER, call_subprocess, format_command_args, - run_with_spinner_message, + runner_with_spinner_message, ) from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -982,7 +982,7 @@ def _build_one_pep517(self, req, tempd, python_tag=None): try: logger.debug('Destination directory: %s', tempd) - runner = run_with_spinner_message( + runner = runner_with_spinner_message( 'Building wheel for {} (PEP 517)'.format(req.name) ) backend = req.pep517_backend From 6b57c1f5df358bf6af73262b8ace2f84a68831a0 Mon Sep 17 00:00:00 2001 From: John Paton <j.paton@catawiki.nl> Date: Sun, 29 Sep 2019 16:23:38 +0200 Subject: [PATCH 0449/3170] Move outdated outside utils and rename (pypa/pip#6532) --- .../architecture/package-finding.rst | 2 +- src/pip/_internal/cli/req_command.py | 7 +++- src/pip/_internal/commands/list.py | 2 +- .../outdated.py => self_outdated_check.py} | 2 +- src/pip/_vendor/README.rst | 2 +- tests/unit/test_commands.py | 9 +++-- ...ed.py => test_unit_self_outdated_check.py} | 39 ++++++++++--------- 7 files changed, 35 insertions(+), 28 deletions(-) rename src/pip/_internal/{utils/outdated.py => self_outdated_check.py} (99%) rename tests/unit/{test_unit_outdated.py => test_unit_self_outdated_check.py} (87%) diff --git a/docs/html/development/architecture/package-finding.rst b/docs/html/development/architecture/package-finding.rst index 11ad9cbc1a2..2954cb5d16b 100644 --- a/docs/html/development/architecture/package-finding.rst +++ b/docs/html/development/architecture/package-finding.rst @@ -86,7 +86,7 @@ difference may simply be historical and may not actually be necessary.) Each of these commands also uses the ``PackageFinder`` class for pip's "self-check," (i.e. to check whether a pip upgrade is available). In this case, the ``PackageFinder`` instance is created by the ``outdated.py`` -module's ``pip_version_check()`` function. +module's ``pip_self_version_check()`` function. The ``PackageFinder`` class is responsible for doing all of the things listed in the :ref:`Overview <index-py-overview>` section like fetching and parsing diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 4bfbbc83cd9..203e86a49cc 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -25,8 +25,11 @@ install_req_from_req_string, ) from pip._internal.req.req_file import parse_requirements +from pip._internal.self_outdated_check import ( + make_link_collector, + pip_self_version_check, +) from pip._internal.utils.misc import normalize_path -from pip._internal.utils.outdated import make_link_collector, pip_version_check from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: @@ -136,7 +139,7 @@ def handle_pip_version_check(self, options): timeout=min(5, options.timeout) ) with session: - pip_version_check(session, options) + pip_self_version_check(session, options) class RequirementCommand(IndexGroupCommand): diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index 08b621ff9ae..77a245b6d2d 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -14,12 +14,12 @@ from pip._internal.exceptions import CommandError from pip._internal.index import PackageFinder from pip._internal.models.selection_prefs import SelectionPreferences +from pip._internal.self_outdated_check import make_link_collector from pip._internal.utils.misc import ( dist_is_editable, get_installed_distributions, write_output, ) -from pip._internal.utils.outdated import make_link_collector from pip._internal.utils.packaging import get_installer logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/utils/outdated.py b/src/pip/_internal/self_outdated_check.py similarity index 99% rename from src/pip/_internal/utils/outdated.py rename to src/pip/_internal/self_outdated_check.py index 3275dd062ee..51ef3439ff1 100644 --- a/src/pip/_internal/utils/outdated.py +++ b/src/pip/_internal/self_outdated_check.py @@ -157,7 +157,7 @@ def was_installed_by_pip(pkg): return False -def pip_version_check(session, options): +def pip_self_version_check(session, options): # type: (PipSession, optparse.Values) -> None """Check for an update for pip. diff --git a/src/pip/_vendor/README.rst b/src/pip/_vendor/README.rst index 539634204cf..c8a5385da76 100644 --- a/src/pip/_vendor/README.rst +++ b/src/pip/_vendor/README.rst @@ -144,7 +144,7 @@ extra work on your end in order to solve the problems described above. ``pip/_vendor/``, then modify ``pip/_vendor/__init__.py`` so that the ``WHEEL_DIR`` variable points to the location you've placed them. -6. *(optional)* Update the ``pip_version_check`` logic to use the +6. *(optional)* Update the ``pip_self_version_check`` logic to use the appropriate logic for determining the latest available version of pip and prompt the user with the correct upgrade message. diff --git a/tests/unit/test_commands.py b/tests/unit/test_commands.py index be6c783524d..7fae427c697 100644 --- a/tests/unit/test_commands.py +++ b/tests/unit/test_commands.py @@ -73,7 +73,7 @@ def has_option_no_index(command): @pytest.mark.parametrize( 'disable_pip_version_check, no_index, expected_called', [ - # pip_version_check() is only called when both + # pip_self_version_check() is only called when both # disable_pip_version_check and no_index are False. (False, False, True), (False, True, False), @@ -81,14 +81,15 @@ def has_option_no_index(command): (True, True, False), ], ) -@patch('pip._internal.cli.req_command.pip_version_check') +@patch('pip._internal.cli.req_command.pip_self_version_check') def test_index_group_handle_pip_version_check( mock_version_check, command_name, disable_pip_version_check, no_index, expected_called, ): """ - Test whether pip_version_check() is called when handle_pip_version_check() - is called, for each of the IndexGroupCommand classes. + Test whether pip_self_version_check() is called when + handle_pip_version_check() is called, for each of the + IndexGroupCommand classes. """ command = create_command(command_name) options = command.parser.get_default_values() diff --git a/tests/unit/test_unit_outdated.py b/tests/unit/test_unit_self_outdated_check.py similarity index 87% rename from tests/unit/test_unit_outdated.py rename to tests/unit/test_unit_self_outdated_check.py index 7002e7b0504..e8c40224770 100644 --- a/tests/unit/test_unit_outdated.py +++ b/tests/unit/test_unit_self_outdated_check.py @@ -9,14 +9,14 @@ from mock import patch from pip._vendor import pkg_resources +from pip._internal import self_outdated_check from pip._internal.index import InstallationCandidate from pip._internal.network.session import PipSession -from pip._internal.utils import outdated -from pip._internal.utils.outdated import ( +from pip._internal.self_outdated_check import ( SelfCheckState, logger, make_link_collector, - pip_version_check, + pip_self_version_check, ) from tests.lib.path import Path @@ -133,7 +133,8 @@ def get_metadata_lines(self, name): def _options(): - ''' Some default options that we pass to outdated.pip_version_check ''' + ''' Some default options that we pass to + self_outdated_check.pip_self_version_check ''' return pretend.stub( find_links=[], index_url='default_url', extra_index_urls=[], no_index=False, pre=False, cache_dir='', @@ -160,12 +161,13 @@ def _options(): ('1970-01-9T10:00:00Z', '6.9.0', '6.9.0', 'pip', False, False), ] ) -def test_pip_version_check(monkeypatch, stored_time, installed_ver, new_ver, - installer, check_if_upgrade_required, - check_warn_logs): - monkeypatch.setattr(outdated, 'get_installed_version', +def test_pip_self_version_check(monkeypatch, stored_time, installed_ver, + new_ver, installer, + check_if_upgrade_required, check_warn_logs): + monkeypatch.setattr(self_outdated_check, 'get_installed_version', lambda name: installed_ver) - monkeypatch.setattr(outdated, 'PackageFinder', MockPackageFinder) + monkeypatch.setattr(self_outdated_check, 'PackageFinder', + MockPackageFinder) monkeypatch.setattr(logger, 'warning', pretend.call_recorder(lambda *a, **kw: None)) monkeypatch.setattr(logger, 'debug', @@ -178,7 +180,7 @@ def test_pip_version_check(monkeypatch, stored_time, installed_ver, new_ver, save=pretend.call_recorder(lambda v, t: None), ) monkeypatch.setattr( - outdated, 'SelfCheckState', lambda **kw: fake_state + self_outdated_check, 'SelfCheckState', lambda **kw: fake_state ) with freezegun.freeze_time( @@ -189,7 +191,7 @@ def test_pip_version_check(monkeypatch, stored_time, installed_ver, new_ver, "pip._vendor.requests.packages.urllib3.packages.six.moves", ] ): - latest_pypi_version = pip_version_check(None, _options()) + latest_pypi_version = pip_self_version_check(None, _options()) # See we return None if not installed_version if not installed_ver: @@ -226,12 +228,12 @@ def test_pip_version_check(monkeypatch, stored_time, installed_ver, new_ver, ("C:\\Users\\User\\Desktop\\venv", statefile_name_case_2), ]) def test_get_statefile_name_known_values(key, expected): - assert expected == outdated._get_statefile_name(key) + assert expected == self_outdated_check._get_statefile_name(key) def _get_statefile_path(cache_dir, key): return os.path.join( - cache_dir, "selfcheck", outdated._get_statefile_name(key) + cache_dir, "selfcheck", self_outdated_check._get_statefile_name(key) ) @@ -245,7 +247,7 @@ def test_self_check_state_key_uses_sys_prefix(monkeypatch): key = "helloworld" monkeypatch.setattr(sys, "prefix", key) - state = outdated.SelfCheckState("") + state = self_outdated_check.SelfCheckState("") assert state.key == key @@ -270,7 +272,7 @@ def test_self_check_state_reads_expected_statefile(monkeypatch, tmpdir): json.dump(content, f) monkeypatch.setattr(sys, "prefix", key) - state = outdated.SelfCheckState(str(cache_dir)) + state = self_outdated_check.SelfCheckState(str(cache_dir)) assert state.state["last_check"] == last_check assert state.state["pypi_version"] == pypi_version @@ -283,12 +285,12 @@ def test_self_check_state_writes_expected_statefile(monkeypatch, tmpdir): statefile_path = _get_statefile_path(str(cache_dir), key) last_check = datetime.datetime.strptime( - "1970-01-02T11:00:00Z", outdated.SELFCHECK_DATE_FMT + "1970-01-02T11:00:00Z", self_outdated_check.SELFCHECK_DATE_FMT ) pypi_version = "1.0" monkeypatch.setattr(sys, "prefix", key) - state = outdated.SelfCheckState(str(cache_dir)) + state = self_outdated_check.SelfCheckState(str(cache_dir)) state.save(pypi_version, last_check) with open(statefile_path) as f: @@ -296,7 +298,8 @@ def test_self_check_state_writes_expected_statefile(monkeypatch, tmpdir): expected = { "key": key, - "last_check": last_check.strftime(outdated.SELFCHECK_DATE_FMT), + "last_check": last_check.strftime( + self_outdated_check.SELFCHECK_DATE_FMT), "pypi_version": pypi_version, } assert expected == saved From d6128bebe51f99142cbb92f35fe9e970554840ac Mon Sep 17 00:00:00 2001 From: John Paton <j.paton@catawiki.nl> Date: Sun, 29 Sep 2019 16:27:42 +0200 Subject: [PATCH 0450/3170] change filename in docs --- docs/html/development/architecture/package-finding.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/html/development/architecture/package-finding.rst b/docs/html/development/architecture/package-finding.rst index 2954cb5d16b..dfbf5e7ae88 100644 --- a/docs/html/development/architecture/package-finding.rst +++ b/docs/html/development/architecture/package-finding.rst @@ -85,8 +85,8 @@ difference may simply be historical and may not actually be necessary.) Each of these commands also uses the ``PackageFinder`` class for pip's "self-check," (i.e. to check whether a pip upgrade is available). In this -case, the ``PackageFinder`` instance is created by the ``outdated.py`` -module's ``pip_self_version_check()`` function. +case, the ``PackageFinder`` instance is created by the +``self_outdated_check.py`` module's ``pip_self_version_check()`` function. The ``PackageFinder`` class is responsible for doing all of the things listed in the :ref:`Overview <index-py-overview>` section like fetching and parsing From 616a9b4b4e235d4ade5f62016029858c7fb845e0 Mon Sep 17 00:00:00 2001 From: John Paton <j.paton@catawiki.nl> Date: Sun, 29 Sep 2019 16:33:21 +0200 Subject: [PATCH 0451/3170] Add news snippet --- news/6532.trivial | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 news/6532.trivial diff --git a/news/6532.trivial b/news/6532.trivial new file mode 100644 index 00000000000..b49b59fd544 --- /dev/null +++ b/news/6532.trivial @@ -0,0 +1,3 @@ +Rename ``pip._internal.utils.outdated`` to +``pip._internal.self_outdated_check`` and rename rename +``pip_version_check`` to ``pip_self_version_check``. From 648f37735126e7e94edfc09b95bce8f51c6cf187 Mon Sep 17 00:00:00 2001 From: John Paton <j.paton@catawiki.nl> Date: Sun, 29 Sep 2019 16:51:10 +0200 Subject: [PATCH 0452/3170] Remove repeated word --- news/6532.trivial | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/news/6532.trivial b/news/6532.trivial index b49b59fd544..1414bd33d1c 100644 --- a/news/6532.trivial +++ b/news/6532.trivial @@ -1,3 +1,3 @@ Rename ``pip._internal.utils.outdated`` to -``pip._internal.self_outdated_check`` and rename rename -``pip_version_check`` to ``pip_self_version_check``. +``pip._internal.self_outdated_check`` and rename ``pip_version_check`` +to ``pip_self_version_check``. From ea4ac306741e1079012c84930474ce6f8db55be9 Mon Sep 17 00:00:00 2001 From: John Paton <j.paton@catawiki.nl> Date: Mon, 30 Sep 2019 11:35:57 +0200 Subject: [PATCH 0453/3170] rename test file --- ...st_unit_self_outdated_check.py => test_self_check_outdated.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/unit/{test_unit_self_outdated_check.py => test_self_check_outdated.py} (100%) diff --git a/tests/unit/test_unit_self_outdated_check.py b/tests/unit/test_self_check_outdated.py similarity index 100% rename from tests/unit/test_unit_self_outdated_check.py rename to tests/unit/test_self_check_outdated.py From e600aebe7da3792863f9fbc5d36806d5e9326de3 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sat, 28 Sep 2019 11:58:51 +0530 Subject: [PATCH 0454/3170] Move find_egg_info to operations.generate_metadata --- .../_internal/operations/generate_metadata.py | 61 ++++++++++++++++++- src/pip/_internal/req/req_install.py | 52 ---------------- 2 files changed, 59 insertions(+), 54 deletions(-) diff --git a/src/pip/_internal/operations/generate_metadata.py b/src/pip/_internal/operations/generate_metadata.py index 4d06ca7afed..9123c2bc7bf 100644 --- a/src/pip/_internal/operations/generate_metadata.py +++ b/src/pip/_internal/operations/generate_metadata.py @@ -4,10 +4,12 @@ import logging import os +from pip._internal.exceptions import InstallationError from pip._internal.utils.misc import ensure_dir from pip._internal.utils.setuptools_build import make_setuptools_shim_args from pip._internal.utils.subprocess import call_subprocess from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from pip._internal.vcs import vcs if MYPY_CHECK_RUNNING: from typing import Callable, List @@ -30,6 +32,61 @@ def get_metadata_generator(install_req): return _generate_metadata +def find_egg_info(install_req): + # type: (InstallRequirement) -> str + + def looks_like_virtual_env(path): + return ( + os.path.lexists(os.path.join(path, 'bin', 'python')) or + os.path.exists(os.path.join(path, 'Scripts', 'Python.exe')) + ) + + def locate_editable_egg_info(base): + candidates = [] + for root, dirs, files in os.walk(base): + for dir_ in vcs.dirnames: + if dir_ in dirs: + dirs.remove(dir_) + # Iterate over a copy of ``dirs``, since mutating + # a list while iterating over it can cause trouble. + # (See https://github.com/pypa/pip/pull/462.) + for dir_ in list(dirs): + if looks_like_virtual_env(os.path.join(root, dir_)): + dirs.remove(dir_) + # Also don't search through tests + elif dir_ == 'test' or dir_ == 'tests': + dirs.remove(dir_) + candidates.extend(os.path.join(root, dir_) for dir_ in dirs) + return [f for f in candidates if f.endswith('.egg-info')] + + def depth_of_directory(dir_): + return ( + dir_.count(os.path.sep) + + (os.path.altsep and dir_.count(os.path.altsep) or 0) + ) + + if install_req.editable: + base = install_req.source_dir + filenames = locate_editable_egg_info(base) + else: + dir_ = install_req.unpacked_source_directory + base = os.path.join(dir_, 'pip-egg-info') + filenames = os.listdir(base) + + if not filenames: + raise InstallationError( + "Files/directories not found in %s" % base + ) + + # If we have more than one match, we pick the toplevel one. This + # can easily be the case if there is a dist folder which contains + # an extracted tarball for testing purposes. + if len(filenames) > 1: + filenames.sort(key=depth_of_directory) + + return os.path.join(base, filenames[0]) + + def _generate_metadata_legacy(install_req): # type: (InstallRequirement) -> str req_details_str = install_req.name or "from {}".format(install_req.link) @@ -63,8 +120,8 @@ def _generate_metadata_legacy(install_req): command_desc='python setup.py egg_info', ) - # Return the metadata directory. - return install_req.find_egg_info() + # Return the .egg-info directory. + return find_egg_info(install_req) def _generate_metadata(install_req): diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index c59a4166ecd..5f1fdb74b1e 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -613,58 +613,6 @@ def prepare_pep517_metadata(self): return os.path.join(metadata_dir, distinfo_dir) - def find_egg_info(self): - # type: () -> str - def looks_like_virtual_env(path): - return ( - os.path.lexists(os.path.join(path, 'bin', 'python')) or - os.path.exists(os.path.join(path, 'Scripts', 'Python.exe')) - ) - - def locate_editable_egg_info(base): - candidates = [] - for root, dirs, files in os.walk(base): - for dir_ in vcs.dirnames: - if dir_ in dirs: - dirs.remove(dir_) - # Iterate over a copy of ``dirs``, since mutating - # a list while iterating over it can cause trouble. - # (See https://github.com/pypa/pip/pull/462.) - for dir_ in list(dirs): - if looks_like_virtual_env(os.path.join(root, dir_)): - dirs.remove(dir_) - # Also don't search through tests - elif dir_ == 'test' or dir_ == 'tests': - dirs.remove(dir_) - candidates.extend(os.path.join(root, dir_) for dir_ in dirs) - return [f for f in candidates if f.endswith('.egg-info')] - - def depth_of_directory(dir_): - return ( - dir_.count(os.path.sep) + - (os.path.altsep and dir_.count(os.path.altsep) or 0) - ) - - if self.editable: - base = self.source_dir - filenames = locate_editable_egg_info(base) - else: - base = os.path.join(self.unpacked_source_directory, 'pip-egg-info') - filenames = os.listdir(base) - - if not filenames: - raise InstallationError( - "Files/directories not found in %s" % base - ) - - # If we have more than one match, we pick the toplevel one. This - # can easily be the case if there is a dist folder which contains - # an extracted tarball for testing purposes. - if len(filenames) > 1: - filenames.sort(key=depth_of_directory) - - return os.path.join(base, filenames[0]) - @property def metadata(self): # type: () -> Any From 628dfd9fab07ec8c1403aee03ff87c640a7f1926 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 30 Sep 2019 13:00:36 +0530 Subject: [PATCH 0455/3170] Add type annotations in find_egg_info --- src/pip/_internal/operations/generate_metadata.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/operations/generate_metadata.py b/src/pip/_internal/operations/generate_metadata.py index 9123c2bc7bf..5a5510ded80 100644 --- a/src/pip/_internal/operations/generate_metadata.py +++ b/src/pip/_internal/operations/generate_metadata.py @@ -36,13 +36,15 @@ def find_egg_info(install_req): # type: (InstallRequirement) -> str def looks_like_virtual_env(path): + # type: (str) -> bool return ( os.path.lexists(os.path.join(path, 'bin', 'python')) or os.path.exists(os.path.join(path, 'Scripts', 'Python.exe')) ) def locate_editable_egg_info(base): - candidates = [] + # type: (str) -> List[str] + candidates = [] # type: List[str] for root, dirs, files in os.walk(base): for dir_ in vcs.dirnames: if dir_ in dirs: @@ -60,6 +62,7 @@ def locate_editable_egg_info(base): return [f for f in candidates if f.endswith('.egg-info')] def depth_of_directory(dir_): + # type: (str) -> int return ( dir_.count(os.path.sep) + (os.path.altsep and dir_.count(os.path.altsep) or 0) From 5307ed0c10f6a77843531ee087ef327c93d10555 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 30 Sep 2019 15:53:31 +0530 Subject: [PATCH 0456/3170] Don't use install_req.source_dir directly Why: This enables a simplification in a follow up and AFAICT, they'll be the same here. --- src/pip/_internal/operations/generate_metadata.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/operations/generate_metadata.py b/src/pip/_internal/operations/generate_metadata.py index 5a5510ded80..01158c5a948 100644 --- a/src/pip/_internal/operations/generate_metadata.py +++ b/src/pip/_internal/operations/generate_metadata.py @@ -68,12 +68,11 @@ def depth_of_directory(dir_): (os.path.altsep and dir_.count(os.path.altsep) or 0) ) + base = install_req.unpacked_source_directory if install_req.editable: - base = install_req.source_dir filenames = locate_editable_egg_info(base) else: - dir_ = install_req.unpacked_source_directory - base = os.path.join(dir_, 'pip-egg-info') + base = os.path.join(base, 'pip-egg-info') filenames = os.listdir(base) if not filenames: From 6d2ce844f55c94fd620a9b7afbdb65ff62a7deb8 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 30 Sep 2019 15:55:23 +0530 Subject: [PATCH 0457/3170] find_egg_info no longer needs InstallRequirement --- src/pip/_internal/operations/generate_metadata.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/operations/generate_metadata.py b/src/pip/_internal/operations/generate_metadata.py index 01158c5a948..1e184d1544a 100644 --- a/src/pip/_internal/operations/generate_metadata.py +++ b/src/pip/_internal/operations/generate_metadata.py @@ -32,8 +32,8 @@ def get_metadata_generator(install_req): return _generate_metadata -def find_egg_info(install_req): - # type: (InstallRequirement) -> str +def find_egg_info(source_directory, is_editable): + # type: (str, bool) -> str def looks_like_virtual_env(path): # type: (str) -> bool @@ -68,8 +68,8 @@ def depth_of_directory(dir_): (os.path.altsep and dir_.count(os.path.altsep) or 0) ) - base = install_req.unpacked_source_directory - if install_req.editable: + base = source_directory + if is_editable: filenames = locate_editable_egg_info(base) else: base = os.path.join(base, 'pip-egg-info') @@ -123,7 +123,10 @@ def _generate_metadata_legacy(install_req): ) # Return the .egg-info directory. - return find_egg_info(install_req) + return find_egg_info( + install_req.unpacked_source_directory, + install_req.editable, + ) def _generate_metadata(install_req): From 3f46081c54946494f74527fb188c15d29d3621e0 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 30 Sep 2019 15:56:38 +0530 Subject: [PATCH 0458/3170] Add an underscore, find_egg_info is an internal --- src/pip/_internal/operations/generate_metadata.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/operations/generate_metadata.py b/src/pip/_internal/operations/generate_metadata.py index 1e184d1544a..be4cf6c50e9 100644 --- a/src/pip/_internal/operations/generate_metadata.py +++ b/src/pip/_internal/operations/generate_metadata.py @@ -32,7 +32,7 @@ def get_metadata_generator(install_req): return _generate_metadata -def find_egg_info(source_directory, is_editable): +def _find_egg_info(source_directory, is_editable): # type: (str, bool) -> str def looks_like_virtual_env(path): @@ -123,7 +123,7 @@ def _generate_metadata_legacy(install_req): ) # Return the .egg-info directory. - return find_egg_info( + return _find_egg_info( install_req.unpacked_source_directory, install_req.editable, ) From b21f4c95eb0e4d298c822df048dbb79595d5d2c8 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 30 Sep 2019 16:07:14 +0530 Subject: [PATCH 0459/3170] Add a documentation string --- src/pip/_internal/operations/generate_metadata.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pip/_internal/operations/generate_metadata.py b/src/pip/_internal/operations/generate_metadata.py index be4cf6c50e9..984748d7fdd 100644 --- a/src/pip/_internal/operations/generate_metadata.py +++ b/src/pip/_internal/operations/generate_metadata.py @@ -34,6 +34,8 @@ def get_metadata_generator(install_req): def _find_egg_info(source_directory, is_editable): # type: (str, bool) -> str + """Find an .egg-info in `source_directory`, based on `is_editable`. + """ def looks_like_virtual_env(path): # type: (str) -> bool From 05a48404caaa1de33601cb6032ee38904713bf82 Mon Sep 17 00:00:00 2001 From: Emil Burzo <contact@emilburzo.com> Date: Mon, 30 Sep 2019 16:25:25 +0300 Subject: [PATCH 0460/3170] fix crash when sys.stdin is None --- src/pip/_internal/vcs/subversion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/vcs/subversion.py b/src/pip/_internal/vcs/subversion.py index 7a2cd9f6def..859b1a8fffa 100644 --- a/src/pip/_internal/vcs/subversion.py +++ b/src/pip/_internal/vcs/subversion.py @@ -188,7 +188,7 @@ def is_commit_id_equal(cls, dest, name): def __init__(self, use_interactive=None): # type: (bool) -> None if use_interactive is None: - use_interactive = sys.stdin.isatty() + use_interactive = sys.stdin and sys.stdin.isatty() self.use_interactive = use_interactive # This member is used to cache the fetched version of the current From f006a7b26259d24d9cc622d410b940cb7c81cc07 Mon Sep 17 00:00:00 2001 From: gpiks <gaurav.pikale@gmail.com> Date: Mon, 30 Sep 2019 22:17:31 -0400 Subject: [PATCH 0461/3170] Clarify `--no-binary` when enumerating packages and add an example Minor tweak to `--no-binary` documentation to specify that when enumerating packages, the colons are not needed. Additionally, add an example to demonstrate building a wheel from source. --- docs/html/reference/pip_wheel.rst | 6 ++++++ src/pip/_internal/cli/cmdoptions.py | 6 +++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/docs/html/reference/pip_wheel.rst b/docs/html/reference/pip_wheel.rst index 75ae51a21fb..942bb70b8b7 100644 --- a/docs/html/reference/pip_wheel.rst +++ b/docs/html/reference/pip_wheel.rst @@ -71,3 +71,9 @@ Examples $ pip wheel --wheel-dir=/tmp/wheelhouse SomePackage $ pip install --no-index --find-links=/tmp/wheelhouse SomePackage + +#. Build a wheel for a package from source + + :: + + $ pip wheel --no-binary SomePackage SomePackage diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index ffed050f875..d7c6e34b201 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -454,9 +454,9 @@ def no_binary(): help="Do not use binary packages. Can be supplied multiple times, and " "each time adds to the existing value. Accepts either :all: to " "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.", + "more package names with commas between them (no colons). Note " + "that some packages are tricky to compile and may fail to " + "install when this option is used on them.", ) From 617c94e57be011bca6102f32196f5ced4fa2d158 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 18 Feb 2019 02:06:37 +0530 Subject: [PATCH 0462/3170] Show output in YAML test failures --- tests/functional/test_yaml.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/functional/test_yaml.py b/tests/functional/test_yaml.py index ceac69fd81d..c95bb237f3e 100644 --- a/tests/functional/test_yaml.py +++ b/tests/functional/test_yaml.py @@ -56,10 +56,12 @@ def handle_install_request(script, requirement): result = script.pip( "install", "--no-index", "--find-links", path_to_url(script.scratch_path), - requirement + requirement, "--verbose", ) - retval = {} + retval = { + "_result_object": result, + } if result.returncode == 0: # Check which packages got installed retval["install"] = [] @@ -140,4 +142,7 @@ def test_yaml_based(script, case): # Perform the requested action effect = available_actions[action](script, request[action]) - assert effect == expected, "Fixture did not succeed." + result = effect["_result_object"] + del effect["_result_object"] + + assert effect == expected, str(result) From b20fd061c4ddc21baafc0ec06412c5e3ef5cf37f Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 1 Oct 2019 11:24:21 +0530 Subject: [PATCH 0463/3170] Allow errors in YAML test outputs --- tests/functional/test_yaml.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/functional/test_yaml.py b/tests/functional/test_yaml.py index c95bb237f3e..eab11e2a42a 100644 --- a/tests/functional/test_yaml.py +++ b/tests/functional/test_yaml.py @@ -57,6 +57,8 @@ def handle_install_request(script, requirement): "install", "--no-index", "--find-links", path_to_url(script.scratch_path), requirement, "--verbose", + allow_stderr_error=True, + allow_stderr_warning=True, ) retval = { From 64262aacee663b11520c1331c999cc999b111941 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 1 Oct 2019 11:43:47 +0530 Subject: [PATCH 0464/3170] Use the `repr` of version strings in wheels. --- tests/lib/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index 576cc6edd7b..6b5b283d05d 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -924,7 +924,7 @@ def create_basic_wheel_for_package(script, name, version, extras = {} files = { "{name}/__init__.py": """ - __version__ = {version} + __version__ = {version!r} def hello(): return "Hello From {name}" """, From be084e0fde322071a6f581db8f0fc927d2f23fca Mon Sep 17 00:00:00 2001 From: Emil Burzo <contact@emilburzo.com> Date: Tue, 1 Oct 2019 09:28:58 +0300 Subject: [PATCH 0465/3170] add news entries --- news/7118.bugfix | 1 + news/7119.bugfix | 1 + 2 files changed, 2 insertions(+) create mode 100644 news/7118.bugfix create mode 100644 news/7119.bugfix diff --git a/news/7118.bugfix b/news/7118.bugfix new file mode 100644 index 00000000000..602b9b578d4 --- /dev/null +++ b/news/7118.bugfix @@ -0,0 +1 @@ +Fix a crash when ``sys.stdin`` is set to ``None``, such as on AWS Lambda. \ No newline at end of file diff --git a/news/7119.bugfix b/news/7119.bugfix new file mode 100644 index 00000000000..602b9b578d4 --- /dev/null +++ b/news/7119.bugfix @@ -0,0 +1 @@ +Fix a crash when ``sys.stdin`` is set to ``None``, such as on AWS Lambda. \ No newline at end of file From 7e50674393a5c8f00bc79f7995675332f15bfed1 Mon Sep 17 00:00:00 2001 From: Emil Burzo <contact@emilburzo.com> Date: Tue, 1 Oct 2019 18:55:13 +0300 Subject: [PATCH 0466/3170] extract function to misc --- src/pip/_internal/utils/misc.py | 7 +++++++ src/pip/_internal/vcs/subversion.py | 3 ++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 67729649590..56339951048 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -864,3 +864,10 @@ def protect_pip_from_modification_on_windows(modifying_pip): 'To modify pip, please run the following command:\n{}' .format(" ".join(new_command)) ) + + +def is_console_interactive(): + # type: () -> bool + """Is this console interactive? + """ + return sys.stdin is not None and sys.stdin.isatty() diff --git a/src/pip/_internal/vcs/subversion.py b/src/pip/_internal/vcs/subversion.py index 859b1a8fffa..be49f6b70dd 100644 --- a/src/pip/_internal/vcs/subversion.py +++ b/src/pip/_internal/vcs/subversion.py @@ -13,6 +13,7 @@ display_path, rmtree, split_auth_from_netloc, + is_console_interactive ) from pip._internal.utils.subprocess import make_command from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -188,7 +189,7 @@ def is_commit_id_equal(cls, dest, name): def __init__(self, use_interactive=None): # type: (bool) -> None if use_interactive is None: - use_interactive = sys.stdin and sys.stdin.isatty() + use_interactive = is_console_interactive() self.use_interactive = use_interactive # This member is used to cache the fetched version of the current From 60d2ade74c6c64f89f94cd96ca8455a77c7c967d Mon Sep 17 00:00:00 2001 From: Emil Burzo <contact@emilburzo.com> Date: Tue, 1 Oct 2019 19:00:03 +0300 Subject: [PATCH 0467/3170] remove no longer used import --- src/pip/_internal/vcs/subversion.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pip/_internal/vcs/subversion.py b/src/pip/_internal/vcs/subversion.py index be49f6b70dd..4c2a281831a 100644 --- a/src/pip/_internal/vcs/subversion.py +++ b/src/pip/_internal/vcs/subversion.py @@ -6,7 +6,6 @@ import logging import os import re -import sys from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import ( From 6f7c3041671ccc3ba8bfb53e265fa14acc22d782 Mon Sep 17 00:00:00 2001 From: Emil Burzo <contact@emilburzo.com> Date: Wed, 2 Oct 2019 08:25:56 +0300 Subject: [PATCH 0468/3170] add tests --- tests/unit/test_utils.py | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index a383f153141..f401c122354 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -50,7 +50,7 @@ rmtree_errorhandler, split_auth_from_netloc, split_auth_netloc_from_url, -) + is_console_interactive) from pip._internal.utils.setuptools_build import make_setuptools_shim_args @@ -971,3 +971,29 @@ def test_make_setuptools_shim_args__unbuffered_output(unbuffered_output): unbuffered_output=unbuffered_output ) assert ('-u' in args) == unbuffered_output + + +def mock_stdin_isatty(monkeypatch, return_value): + monkeypatch.setattr(sys.stdin, 'isatty', Mock(return_value=return_value)) + + +def test_is_console_interactive_when_stdin_is_none_and_isatty_true(monkeypatch): + mock_stdin_isatty(monkeypatch, True) + monkeypatch.setattr(sys, 'stdin', None) + assert is_console_interactive() is False + + +def test_is_console_interactive_when_stdin_is_none_and_isatty_false(monkeypatch): + mock_stdin_isatty(monkeypatch, False) + monkeypatch.setattr(sys, 'stdin', None) + assert is_console_interactive() is False + + +def test_is_console_interactive_when_stdin_is_not_none_and_isatty_true(monkeypatch): + mock_stdin_isatty(monkeypatch, True) + assert is_console_interactive() is True + + +def test_is_console_interactive_when_stdin_is_not_none_and_isatty_false(monkeypatch): + mock_stdin_isatty(monkeypatch, False) + assert is_console_interactive() is False From 004103c0f5e66ca0534b59ee024217e5258bd640 Mon Sep 17 00:00:00 2001 From: Emil Burzo <contact@emilburzo.com> Date: Wed, 2 Oct 2019 08:33:41 +0300 Subject: [PATCH 0469/3170] fix code quality issues --- news/7118.bugfix | 2 +- news/7119.bugfix | 2 +- src/pip/_internal/vcs/subversion.py | 4 ++-- tests/unit/test_utils.py | 13 +++++++------ 4 files changed, 11 insertions(+), 10 deletions(-) diff --git a/news/7118.bugfix b/news/7118.bugfix index 602b9b578d4..8cca2e1bf46 100644 --- a/news/7118.bugfix +++ b/news/7118.bugfix @@ -1 +1 @@ -Fix a crash when ``sys.stdin`` is set to ``None``, such as on AWS Lambda. \ No newline at end of file +Fix a crash when ``sys.stdin`` is set to ``None``, such as on AWS Lambda. diff --git a/news/7119.bugfix b/news/7119.bugfix index 602b9b578d4..8cca2e1bf46 100644 --- a/news/7119.bugfix +++ b/news/7119.bugfix @@ -1 +1 @@ -Fix a crash when ``sys.stdin`` is set to ``None``, such as on AWS Lambda. \ No newline at end of file +Fix a crash when ``sys.stdin`` is set to ``None``, such as on AWS Lambda. diff --git a/src/pip/_internal/vcs/subversion.py b/src/pip/_internal/vcs/subversion.py index 4c2a281831a..16b0be855b8 100644 --- a/src/pip/_internal/vcs/subversion.py +++ b/src/pip/_internal/vcs/subversion.py @@ -10,9 +10,9 @@ from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import ( display_path, + is_console_interactive, rmtree, - split_auth_from_netloc, - is_console_interactive + split_auth_from_netloc ) from pip._internal.utils.subprocess import make_command from pip._internal.utils.typing import MYPY_CHECK_RUNNING diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index f401c122354..bcf0841165f 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -39,6 +39,7 @@ get_prog, hide_url, hide_value, + is_console_interactive, normalize_path, normalize_version_info, parse_netloc, @@ -49,8 +50,8 @@ rmtree, rmtree_errorhandler, split_auth_from_netloc, - split_auth_netloc_from_url, - is_console_interactive) + split_auth_netloc_from_url +) from pip._internal.utils.setuptools_build import make_setuptools_shim_args @@ -977,23 +978,23 @@ def mock_stdin_isatty(monkeypatch, return_value): monkeypatch.setattr(sys.stdin, 'isatty', Mock(return_value=return_value)) -def test_is_console_interactive_when_stdin_is_none_and_isatty_true(monkeypatch): +def test_is_console_interactive_when_stdin_none_and_isatty(monkeypatch): mock_stdin_isatty(monkeypatch, True) monkeypatch.setattr(sys, 'stdin', None) assert is_console_interactive() is False -def test_is_console_interactive_when_stdin_is_none_and_isatty_false(monkeypatch): +def test_is_console_interactive_when_stdin_none_and_not_isatty(monkeypatch): mock_stdin_isatty(monkeypatch, False) monkeypatch.setattr(sys, 'stdin', None) assert is_console_interactive() is False -def test_is_console_interactive_when_stdin_is_not_none_and_isatty_true(monkeypatch): +def test_is_console_interactive_when_stdin_isatty(monkeypatch): mock_stdin_isatty(monkeypatch, True) assert is_console_interactive() is True -def test_is_console_interactive_when_stdin_is_not_none_and_isatty_false(monkeypatch): +def test_is_console_interactive_when_stdin_not_isatty(monkeypatch): mock_stdin_isatty(monkeypatch, False) assert is_console_interactive() is False From c7e239c47ce33db7a832f7fda07c5e30183b0253 Mon Sep 17 00:00:00 2001 From: Emil Burzo <contact@emilburzo.com> Date: Wed, 2 Oct 2019 08:51:14 +0300 Subject: [PATCH 0470/3170] fix linter --- src/pip/_internal/vcs/subversion.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/vcs/subversion.py b/src/pip/_internal/vcs/subversion.py index 16b0be855b8..6c76d1ad435 100644 --- a/src/pip/_internal/vcs/subversion.py +++ b/src/pip/_internal/vcs/subversion.py @@ -12,7 +12,7 @@ display_path, is_console_interactive, rmtree, - split_auth_from_netloc + split_auth_from_netloc, ) from pip._internal.utils.subprocess import make_command from pip._internal.utils.typing import MYPY_CHECK_RUNNING From 5089b84c560873e3778a59cff7aa94a727fee00f Mon Sep 17 00:00:00 2001 From: Emil Burzo <contact@emilburzo.com> Date: Wed, 2 Oct 2019 08:51:32 +0300 Subject: [PATCH 0471/3170] rewrite tests using `pytest.mark.parametrize` --- tests/unit/test_utils.py | 35 ++++++++++++----------------------- 1 file changed, 12 insertions(+), 23 deletions(-) diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index bcf0841165f..25d39928f35 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -50,7 +50,7 @@ rmtree, rmtree_errorhandler, split_auth_from_netloc, - split_auth_netloc_from_url + split_auth_netloc_from_url, ) from pip._internal.utils.setuptools_build import make_setuptools_shim_args @@ -974,27 +974,16 @@ def test_make_setuptools_shim_args__unbuffered_output(unbuffered_output): assert ('-u' in args) == unbuffered_output -def mock_stdin_isatty(monkeypatch, return_value): - monkeypatch.setattr(sys.stdin, 'isatty', Mock(return_value=return_value)) - - -def test_is_console_interactive_when_stdin_none_and_isatty(monkeypatch): - mock_stdin_isatty(monkeypatch, True) - monkeypatch.setattr(sys, 'stdin', None) - assert is_console_interactive() is False - - -def test_is_console_interactive_when_stdin_none_and_not_isatty(monkeypatch): - mock_stdin_isatty(monkeypatch, False) - monkeypatch.setattr(sys, 'stdin', None) - assert is_console_interactive() is False - - -def test_is_console_interactive_when_stdin_isatty(monkeypatch): - mock_stdin_isatty(monkeypatch, True) - assert is_console_interactive() is True +@pytest.mark.parametrize('isatty,no_stdin,expected', [ + (True, False, True), + (False, False, False), + (True, True, False), + (False, True, False), +]) +def test_is_console_interactive(monkeypatch, isatty, no_stdin, expected): + monkeypatch.setattr(sys.stdin, 'isatty', Mock(return_value=isatty)) + if no_stdin: + monkeypatch.setattr(sys, 'stdin', None) -def test_is_console_interactive_when_stdin_not_isatty(monkeypatch): - mock_stdin_isatty(monkeypatch, False) - assert is_console_interactive() is False + assert is_console_interactive() is expected From 211fd5575f0e04f201c24317b0cbe84c0e01f426 Mon Sep 17 00:00:00 2001 From: Patrik Kopkan <pkopkan@redhat.com> Date: Wed, 2 Oct 2019 19:13:53 +0200 Subject: [PATCH 0472/3170] add newline --- news/6340.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/6340.feature b/news/6340.feature index 208501c6f93..9afba8bf24a 100644 --- a/news/6340.feature +++ b/news/6340.feature @@ -1 +1 @@ -Add a new option ``--save-wheel-names <filename>`` to ``pip wheel`` that writes the names of the resulting wheels to the given filename. \ No newline at end of file +Add a new option ``--save-wheel-names <filename>`` to ``pip wheel`` that writes the names of the resulting wheels to the given filename. From b38ce5d0e0fc41e31765ba1f401122b1efbb59c8 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Fri, 4 Oct 2019 15:46:18 +0530 Subject: [PATCH 0473/3170] Remove GitHub Actions CI Why: This seems to be causing 500 errors for @pradyunsg on PRs. https://twitter.com/AndersKaseorg/status/1179851971433385984 --- .github/workflows/python-linters.yml | 42 ---------------------------- 1 file changed, 42 deletions(-) delete mode 100644 .github/workflows/python-linters.yml diff --git a/.github/workflows/python-linters.yml b/.github/workflows/python-linters.yml deleted file mode 100644 index 99c632166e4..00000000000 --- a/.github/workflows/python-linters.yml +++ /dev/null @@ -1,42 +0,0 @@ -name: Code quality - -on: - push: - pull_request: - schedule: - # Run every Friday at 18:02 UTC - # https://crontab.guru/#2_18_*_*_5 - - cron: 2 18 * * 5 - -jobs: - linters: - name: 🤖 - runs-on: ${{ matrix.os }} - strategy: - # max-parallel: 5 - matrix: - os: - - ubuntu-18.04 - env: - - TOXENV: docs - - TOXENV: lint - steps: - - uses: actions/checkout@master - - name: Set up Python ${{ matrix.env.PYTHON_VERSION || 3.7 }} - uses: actions/setup-python@v1 - with: - version: ${{ matrix.env.PYTHON_VERSION || 3.7 }} - - name: Pre-configure global Git settings - run: >- - tools/travis/setup.sh - - name: Update setuptools and tox dependencies - run: >- - tools/travis/install.sh - - name: 'Initialize tox envs: ${{ matrix.env.TOXENV }}' - run: >- - python -m tox --notest --skip-missing-interpreters false - env: ${{ matrix.env }} - - name: Test with tox - run: >- - python -m tox - env: ${{ matrix.env }} From 5c8f299f7c1331581d2fd1ffc31c3e5c3e6c1f27 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Fri, 4 Oct 2019 11:59:21 +0530 Subject: [PATCH 0474/3170] Add a good-first-issue template This is based on Warehouse's template, adapted to use language we already use in our existing good-first-issues and with clearer instructions for what's needed in these kinds of issues. --- .github/ISSUE_TEMPLATE/~good-first-issue.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/~good-first-issue.md diff --git a/.github/ISSUE_TEMPLATE/~good-first-issue.md b/.github/ISSUE_TEMPLATE/~good-first-issue.md new file mode 100644 index 00000000000..4ff6d9f9b32 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/~good-first-issue.md @@ -0,0 +1,14 @@ +--- +name: (Maintainers Only) Good First Issue +about: For maintainers, to create an issue that is good for new contributors + +--- + +<!-- Write the issue below, provide clear instructions for resolution --> + +<!-- End of issue content. --> +<!-- Leave the following intact --> + +--- + +**Good First Issue**: This issue is a good starting point for first time contributors -- the process of fixing this should be a good introduction to pip's development workflow. If you've already contributed to pip, work on [another issue without this label](https://github.com/pypa/pip/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+-label%3A%22good+first+issue%22) instead. If there is not a corresponding pull request for this issue, it is up for grabs. For directions for getting set up, see our [Getting Started Guide](https://pip.pypa.io/en/latest/development/getting-started/). If you are working on this issue and have questions, feel free to ask them here, [`#pypa-dev` on Freenode](https://webchat.freenode.net/?channels=%23pypa-dev), or the [pypa-dev mailing list](https://groups.google.com/forum/#!forum/pypa-dev). From 3e86297c7000b1c5878a8517d07f0b3e399a9604 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Wed, 2 Oct 2019 17:20:17 +0530 Subject: [PATCH 0475/3170] Bump to latest sphinx --- tools/requirements/docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/requirements/docs.txt b/tools/requirements/docs.txt index d817f350d01..74fc71d96da 100644 --- a/tools/requirements/docs.txt +++ b/tools/requirements/docs.txt @@ -1,4 +1,4 @@ -sphinx == 2.0.1 +sphinx == 2.2.0 git+https://github.com/python/python-docs-theme.git#egg=python-docs-theme git+https://github.com/pypa/pypa-docs-theme.git#egg=pypa-docs-theme From 618583fc6b34188aa5073bda11af9a96b7b6517e Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sat, 5 Oct 2019 07:49:22 +0530 Subject: [PATCH 0476/3170] Revert "Remove GitHub Actions CI" --- .github/workflows/python-linters.yml | 42 ++++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) create mode 100644 .github/workflows/python-linters.yml diff --git a/.github/workflows/python-linters.yml b/.github/workflows/python-linters.yml new file mode 100644 index 00000000000..99c632166e4 --- /dev/null +++ b/.github/workflows/python-linters.yml @@ -0,0 +1,42 @@ +name: Code quality + +on: + push: + pull_request: + schedule: + # Run every Friday at 18:02 UTC + # https://crontab.guru/#2_18_*_*_5 + - cron: 2 18 * * 5 + +jobs: + linters: + name: 🤖 + runs-on: ${{ matrix.os }} + strategy: + # max-parallel: 5 + matrix: + os: + - ubuntu-18.04 + env: + - TOXENV: docs + - TOXENV: lint + steps: + - uses: actions/checkout@master + - name: Set up Python ${{ matrix.env.PYTHON_VERSION || 3.7 }} + uses: actions/setup-python@v1 + with: + version: ${{ matrix.env.PYTHON_VERSION || 3.7 }} + - name: Pre-configure global Git settings + run: >- + tools/travis/setup.sh + - name: Update setuptools and tox dependencies + run: >- + tools/travis/install.sh + - name: 'Initialize tox envs: ${{ matrix.env.TOXENV }}' + run: >- + python -m tox --notest --skip-missing-interpreters false + env: ${{ matrix.env }} + - name: Test with tox + run: >- + python -m tox + env: ${{ matrix.env }} From 0492e8081617dbb32cff8077d99e32e6ae6aa7f8 Mon Sep 17 00:00:00 2001 From: Anders Kaseorg <andersk@mit.edu> Date: Wed, 2 Oct 2019 20:35:13 -0700 Subject: [PATCH 0477/3170] Optimize upgrade of already-satisfied pinned requirement MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Example: after installing six 1.12.0, `pip install -Uv six==1.12.0` now returns immediately, instead of going to the index to check for a version that can’t possibly be considered better. This optimization is most significant when upgrading via a requirements file with many pinned versions and some non-pinned versions. Signed-off-by: Anders Kaseorg <andersk@mit.edu> --- news/7132.feature | 1 + src/pip/_internal/legacy_resolve.py | 4 ++++ 2 files changed, 5 insertions(+) create mode 100644 news/7132.feature diff --git a/news/7132.feature b/news/7132.feature new file mode 100644 index 00000000000..a873a669c56 --- /dev/null +++ b/news/7132.feature @@ -0,0 +1 @@ +Skip reaching out to the package index, if a pinned version is already satisfied. diff --git a/src/pip/_internal/legacy_resolve.py b/src/pip/_internal/legacy_resolve.py index c24158f4d37..866a508b885 100644 --- a/src/pip/_internal/legacy_resolve.py +++ b/src/pip/_internal/legacy_resolve.py @@ -267,6 +267,10 @@ def _check_skip_installed(self, req_to_install): # 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: + if req_to_install.is_pinned: + # No need to check the index for a better version. + return 'already satisfied' + try: self.finder.find_requirement(req_to_install, upgrade=True) except BestVersionAlreadyInstalled: From 10b94211934849fbfd58d05071797a942984568a Mon Sep 17 00:00:00 2001 From: Albert Tugushev <albert@tugushev.ru> Date: Fri, 4 Oct 2019 20:46:50 +0800 Subject: [PATCH 0478/3170] Fix a typo --- src/pip/_internal/commands/install.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 85c06093ac2..66071f6e819 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -118,7 +118,7 @@ class InstallCommand(RequirementCommand): - Local project directories. - Local or remote source archives. - pip also supports installing from "requirements files," which provide + pip also supports installing from "requirements files", which provide an easy way to specify a whole environment to be installed. """ From 79d7f54f523b32a1cfa1d78536182f646eb2b3ca Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 6 Oct 2019 02:03:14 +0530 Subject: [PATCH 0479/3170] Add requested labels key --- .github/ISSUE_TEMPLATE/~good-first-issue.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/ISSUE_TEMPLATE/~good-first-issue.md b/.github/ISSUE_TEMPLATE/~good-first-issue.md index 4ff6d9f9b32..b5ef71ae6f6 100644 --- a/.github/ISSUE_TEMPLATE/~good-first-issue.md +++ b/.github/ISSUE_TEMPLATE/~good-first-issue.md @@ -1,6 +1,7 @@ --- name: (Maintainers Only) Good First Issue about: For maintainers, to create an issue that is good for new contributors +labels: ["good first issue"] --- From 0548d207b6755a72e7440c71474aa6dfa82408a8 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 5 Oct 2019 22:16:22 -0400 Subject: [PATCH 0480/3170] Add NEWS file for new contextlib2 dependency --- news/contextlib2.vendor | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/contextlib2.vendor diff --git a/news/contextlib2.vendor b/news/contextlib2.vendor new file mode 100644 index 00000000000..c10c3c485b5 --- /dev/null +++ b/news/contextlib2.vendor @@ -0,0 +1 @@ +Add contextlib2 as a vendored dependency. From 1b542bd6c9ae91dac81eb0dfd0d0df88c38ba2bc Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 6 Oct 2019 22:34:42 +0530 Subject: [PATCH 0481/3170] Fix backticks in a NEWS fragment --- news/4358.bugfix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/4358.bugfix b/news/4358.bugfix index 912083dc1d7..a0378e6c50c 100644 --- a/news/4358.bugfix +++ b/news/4358.bugfix @@ -1 +1 @@ -Correct inconsistency related to the `hg+file` scheme. +Correct inconsistency related to the ``hg+file`` scheme. From f805f328d468b4d615267704b602c2eda0d39253 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 16 Aug 2019 21:34:17 -0400 Subject: [PATCH 0482/3170] Align interface of tests.lib.path.Path.mkdir with pathlib.Path.mkdir. --- tests/functional/test_install.py | 3 ++- tests/functional/test_pep517.py | 9 ++++++--- tests/functional/test_uninstall_user.py | 2 +- tests/lib/__init__.py | 5 +++-- tests/lib/path.py | 11 ++++++----- tests/lib/venv.py | 2 +- tests/unit/test_collector.py | 3 ++- tests/unit/test_compat.py | 6 ++++-- tests/unit/test_locations.py | 6 ++++-- tests/unit/test_utils.py | 8 ++++++-- 10 files changed, 35 insertions(+), 20 deletions(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 119f2d3bdbb..6907b99aced 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -551,7 +551,8 @@ def test_editable_install__local_dir_no_setup_py_with_pyproject( Test installing in editable mode from a local directory with no setup.py but that does have pyproject.toml. """ - local_dir = script.scratch_path.joinpath('temp').mkdir() + local_dir = script.scratch_path.joinpath('temp') + local_dir.mkdir() pyproject_path = local_dir.joinpath('pyproject.toml') pyproject_path.write_text('') diff --git a/tests/functional/test_pep517.py b/tests/functional/test_pep517.py index c71d1a40102..faff843dc80 100644 --- a/tests/functional/test_pep517.py +++ b/tests/functional/test_pep517.py @@ -7,7 +7,8 @@ def make_project(tmpdir, requires=[], backend=None): - project_dir = (tmpdir / 'project').mkdir() + project_dir = tmpdir / 'project' + project_dir.mkdir() buildsys = {'requires': requires} if backend: buildsys['build-backend'] = backend @@ -125,7 +126,8 @@ def test_pep517_install_with_no_cache_dir(script, tmpdir, data): def make_pyproject_with_setup(tmpdir, build_system=True, set_backend=True): - project_dir = (tmpdir / 'project').mkdir() + project_dir = tmpdir / 'project' + project_dir.mkdir() setup_script = ( 'from setuptools import setup\n' ) @@ -161,7 +163,8 @@ def make_pyproject_with_setup(tmpdir, build_system=True, set_backend=True): project_dir.joinpath('pyproject.toml').write_text(project_data) project_dir.joinpath('setup.py').write_text(setup_script) - package_dir = (project_dir / "pep517_test").mkdir() + package_dir = project_dir / "pep517_test" + package_dir.mkdir() package_dir.joinpath('__init__.py').write_text('__version__ = "0.1"') return project_dir, "pep517_test" diff --git a/tests/functional/test_uninstall_user.py b/tests/functional/test_uninstall_user.py index aa37c205f66..f99f3f21c7d 100644 --- a/tests/functional/test_uninstall_user.py +++ b/tests/functional/test_uninstall_user.py @@ -51,7 +51,7 @@ def test_uninstall_editable_from_usersite(self, script, data): """ Test uninstall editable local user install """ - script.user_site_path.mkdir(parents=True) + assert script.user_site_path.exists() # install to_install = data.packages.joinpath("FSPkg") diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index 6b5b283d05d..824ea153286 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -451,7 +451,8 @@ def __init__(self, base_path, *args, **kwargs): ) # Create a Directory to use as a scratch pad - self.scratch_path = base_path.joinpath("scratch").mkdir() + self.scratch_path = base_path.joinpath("scratch") + self.scratch_path.mkdir() # Set our default working directory kwargs.setdefault("cwd", self.scratch_path) @@ -988,7 +989,7 @@ def hello(): for fname in files: path = script.temp_path / fname - path.parent.mkdir() + path.parent.mkdir(exist_ok=True) path.write_text(files[fname]) retval = script.scratch_path / archive_name diff --git a/tests/lib/path.py b/tests/lib/path.py index b2676a2e1e0..736543b0459 100644 --- a/tests/lib/path.py +++ b/tests/lib/path.py @@ -154,18 +154,19 @@ def exists(self): """ return os.path.exists(self) - def mkdir(self, mode=0x1FF, parents=False): # 0o777 + def mkdir(self, mode=0x1FF, exist_ok=False, parents=False): # 0o777 """ Creates a directory, if it doesn't exist already. :param parents: Whether to create parent directories. """ - if self.exists(): - return self maker_func = os.makedirs if parents else os.mkdir - maker_func(self, mode) - return self + try: + maker_func(self, mode) + except OSError: + if not exist_ok or not os.path.isdir(self): + raise def unlink(self): """ diff --git a/tests/lib/venv.py b/tests/lib/venv.py index 5ef9adc8785..cc94e29f254 100644 --- a/tests/lib/venv.py +++ b/tests/lib/venv.py @@ -75,7 +75,7 @@ def _create(self, clear=False): context = builder.ensure_directories(self.location) builder.create_configuration(context) builder.setup_python(context) - self.site.mkdir(parents=True) + self.site.mkdir(parents=True, exist_ok=True) self.sitecustomize = self._sitecustomize self.user_site_packages = self._user_site_packages diff --git a/tests/unit/test_collector.py b/tests/unit/test_collector.py index c85c121268d..f602c956a04 100644 --- a/tests/unit/test_collector.py +++ b/tests/unit/test_collector.py @@ -358,7 +358,8 @@ def make_fake_html_response(url): def test_get_html_page_directory_append_index(tmpdir): """`_get_html_page()` should append "index.html" to a directory URL. """ - dirpath = tmpdir.mkdir("something") + dirpath = tmpdir / "something" + dirpath.mkdir() dir_url = "file:///{}".format( urllib_request.pathname2url(dirpath).lstrip("/"), ) diff --git a/tests/unit/test_compat.py b/tests/unit/test_compat.py index cf273da8697..c47b8c487ae 100644 --- a/tests/unit/test_compat.py +++ b/tests/unit/test_compat.py @@ -31,7 +31,8 @@ def test_get_path_uid_without_NOFOLLOW(monkeypatch): @pytest.mark.skipif("sys.platform == 'win32'") @pytest.mark.skipif("not hasattr(os, 'symlink')") def test_get_path_uid_symlink(tmpdir): - f = tmpdir.mkdir("symlink").joinpath("somefile") + f = tmpdir / "symlink" / "somefile" + f.parent.mkdir() f.write_text("content") fs = f + '_link' os.symlink(f, fs) @@ -43,7 +44,8 @@ def test_get_path_uid_symlink(tmpdir): @pytest.mark.skipif("not hasattr(os, 'symlink')") def test_get_path_uid_symlink_without_NOFOLLOW(tmpdir, monkeypatch): monkeypatch.delattr("os.O_NOFOLLOW") - f = tmpdir.mkdir("symlink").joinpath("somefile") + f = tmpdir / "symlink" / "somefile" + f.parent.mkdir() f.write_text("content") fs = f + '_link' os.symlink(f, fs) diff --git a/tests/unit/test_locations.py b/tests/unit/test_locations.py index d59d2d65a8d..3d24f717387 100644 --- a/tests/unit/test_locations.py +++ b/tests/unit/test_locations.py @@ -96,7 +96,8 @@ def test_distutils_config_file_read(self, tmpdir, monkeypatch): # This deals with nt/posix path differences install_scripts = os.path.normcase(os.path.abspath( os.path.join(os.path.sep, 'somewhere', 'else'))) - f = tmpdir.mkdir("config").joinpath("setup.cfg") + f = tmpdir / "config" / "setup.cfg" + f.parent.mkdir() f.write_text("[install]\ninstall-scripts=" + install_scripts) from distutils.dist import Distribution # patch the function that returns what config files are present @@ -116,7 +117,8 @@ def test_install_lib_takes_precedence(self, tmpdir, monkeypatch): # This deals with nt/posix path differences install_lib = os.path.normcase(os.path.abspath( os.path.join(os.path.sep, 'somewhere', 'else'))) - f = tmpdir.mkdir("config").joinpath("setup.cfg") + f = tmpdir / "config" / "setup.cfg" + f.parent.mkdir() f.write_text("[install]\ninstall-lib=" + install_lib) from distutils.dist import Distribution # patch the function that returns what config files are present diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 25d39928f35..bbd48c6af98 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -303,7 +303,9 @@ def test_rmtree_errorhandler_readonly_directory(tmpdir): Test rmtree_errorhandler makes the given read-only directory writable. """ # Create read only directory - path = str((tmpdir / 'subdir').mkdir()) + subdir_path = tmpdir / 'subdir' + subdir_path.mkdir() + path = str(subdir_path) os.chmod(path, stat.S_IREAD) # Make sure mock_func is called with the given path @@ -321,7 +323,9 @@ def test_rmtree_errorhandler_reraises_error(tmpdir): by the given unreadable directory. """ # Create directory without read permission - path = str((tmpdir / 'subdir').mkdir()) + subdir_path = tmpdir / 'subdir' + subdir_path.mkdir() + path = str(subdir_path) os.chmod(path, stat.S_IWRITE) mock_func = Mock() From f31567157278d7663d2fac6a304afd287a2d86c8 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 6 Oct 2019 22:31:49 -0400 Subject: [PATCH 0483/3170] Move urllib3 warning suppression to network.session This is only relevant for our usage of --trusted-host, so it is enough to initialize it in network.session. --- src/pip/_internal/__init__.py | 11 ----------- src/pip/_internal/network/session.py | 6 ++++++ 2 files changed, 6 insertions(+), 11 deletions(-) diff --git a/src/pip/_internal/__init__.py b/src/pip/_internal/__init__.py index 04238e289ad..8c0e4c585d1 100755 --- a/src/pip/_internal/__init__.py +++ b/src/pip/_internal/__init__.py @@ -1,13 +1,2 @@ #!/usr/bin/env python -from __future__ import absolute_import - -import warnings - -# We ignore certain warnings from urllib3, since they are not relevant to pip's -# usecases. -from pip._vendor.urllib3.exceptions import InsecureRequestWarning - import pip._internal.utils.inject_securetransport # noqa - -# Raised when using --trusted-host. -warnings.filterwarnings("ignore", category=InsecureRequestWarning) diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py index 52e324ad8dc..ac6e2622fc3 100644 --- a/src/pip/_internal/network/session.py +++ b/src/pip/_internal/network/session.py @@ -12,6 +12,7 @@ import os import platform import sys +import warnings from pip._vendor import requests, six, urllib3 from pip._vendor.cachecontrol import CacheControlAdapter @@ -19,6 +20,7 @@ from pip._vendor.requests.models import Response from pip._vendor.requests.structures import CaseInsensitiveDict from pip._vendor.six.moves.urllib import parse as urllib_parse +from pip._vendor.urllib3.exceptions import InsecureRequestWarning from pip import __version__ from pip._internal.network.auth import MultiDomainBasicAuth @@ -48,6 +50,10 @@ logger = logging.getLogger(__name__) +# Ignore warning raised when using --trusted-host. +warnings.filterwarnings("ignore", category=InsecureRequestWarning) + + SECURE_ORIGINS = [ # protocol, hostname, port # Taken from Chrome's list of secure origins (See: http://bit.ly/1qrySKC) From 959969840b16a84fef71c1adb4e6c86e7eefe260 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 7 Oct 2019 12:25:38 +0530 Subject: [PATCH 0484/3170] Remove subtraction from tests.lib.path.Path --- tests/lib/path.py | 17 ----------------- 1 file changed, 17 deletions(-) diff --git a/tests/lib/path.py b/tests/lib/path.py index b2676a2e1e0..0ef5c6239a4 100644 --- a/tests/lib/path.py +++ b/tests/lib/path.py @@ -66,23 +66,6 @@ def __idiv__(self, path): __itruediv__ = __idiv__ - def __sub__(self, path): - """ - Makes this path relative to another path. - - >>> path_obj - '/home/a' - >>> path_obj - path_obj2 - """ - return Path(os.path.relpath(self, path)) - - def __rsub__(self, path): - """ - Returns path relative to this path. - - >>> "/home/a" - path_obj - """ - return Path(os.path.relpath(path, self)) - def __add__(self, path): """ >>> Path('/home/a') + 'bc.d' From 7d72b3e034680f5734a0aa519efad0edb0ba8bf4 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 7 Oct 2019 12:38:22 +0530 Subject: [PATCH 0485/3170] Update uses of path subtraction to os.path.relpath --- tests/functional/test_install.py | 16 ++++++++++------ tests/functional/test_install_reqs.py | 5 ++++- tests/lib/__init__.py | 7 +++++-- 3 files changed, 19 insertions(+), 9 deletions(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 119f2d3bdbb..890027c84fe 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -407,7 +407,9 @@ def test_basic_install_relative_directory(script, data): package_folder = script.site_packages / 'fspkg' # Compute relative install path to FSPkg from scratch path. - full_rel_path = data.packages.joinpath('FSPkg') - script.scratch_path + full_rel_path = Path( + os.path.relpath(data.packages.joinpath('FSPkg'), script.scratch_path) + ) full_rel_url = ( 'file:' + full_rel_path.replace(os.path.sep, '/') + '#egg=FSPkg' ) @@ -1553,11 +1555,13 @@ def test_target_install_ignores_distutils_config_install_prefix(script): ''' % str(prefix))) target = script.scratch_path / 'target' result = script.pip_install_local('simplewheel', '-t', target) - assert ( - "Successfully installed simplewheel" in result.stdout and - (target - script.base_path) in result.files_created and - (prefix - script.base_path) not in result.files_created - ), str(result) + + assert "Successfully installed simplewheel" in result.stdout + + relative_target = os.path.relpath(target, script.base_path) + relative_script_base = os.path.relpath(target, script.base_path) + assert relative_target in result.files_created + assert relative_script_base not in result.files_created @pytest.mark.network diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index e0eed8715bb..c356b74ddc3 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -10,6 +10,7 @@ requirements_file, ) from tests.lib.local_repos import local_checkout +from tests.lib.path import Path @pytest.mark.network @@ -70,7 +71,9 @@ def test_relative_requirements_file(script, data): package_folder = script.site_packages / 'fspkg' # Compute relative install path to FSPkg from scratch path. - full_rel_path = data.packages.joinpath('FSPkg') - script.scratch_path + full_rel_path = Path( + os.path.relpath(data.packages.joinpath('FSPkg'), script.scratch_path) + ) full_rel_url = 'file:' + full_rel_path + '#egg=FSPkg' embedded_rel_path = script.scratch_path.joinpath(full_rel_path) diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index 6b5b283d05d..aa5eeeed10d 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -447,7 +447,7 @@ def __init__(self, base_path, *args, **kwargs): self.user_bin_path = scripts_base.joinpath('Scripts') else: self.user_bin_path = self.user_base_path.joinpath( - self.bin_path - self.venv_path + os.path.relpath(self.bin_path, self.venv_path) ) # Create a Directory to use as a scratch pad @@ -482,7 +482,10 @@ def __init__(self, base_path, *args, **kwargs): for name in ["base", "venv", "bin", "lib", "site_packages", "user_base", "user_site", "user_bin", "scratch"]: real_name = "%s_path" % name - setattr(self, name, getattr(self, real_name) - self.base_path) + relative_path = Path(os.path.relpath( + getattr(self, real_name), self.base_path + )) + setattr(self, name, relative_path) # Make sure temp_path is a Path object self.temp_path = Path(self.temp_path) From 3ff9061c4e126d7fccb9ac0152aeb0e1057ce7e3 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 7 Oct 2019 15:12:33 +0530 Subject: [PATCH 0486/3170] Fix a copy-paste error --- tests/functional/test_install.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 890027c84fe..11089867af6 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1559,7 +1559,7 @@ def test_target_install_ignores_distutils_config_install_prefix(script): assert "Successfully installed simplewheel" in result.stdout relative_target = os.path.relpath(target, script.base_path) - relative_script_base = os.path.relpath(target, script.base_path) + relative_script_base = os.path.relpath(prefix, script.base_path) assert relative_target in result.files_created assert relative_script_base not in result.files_created From 4048daeddfffb9d2ce570567a1ae82b412ee11a6 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 7 Oct 2019 15:41:42 +0530 Subject: [PATCH 0487/3170] nox: Better handle execution with protected pip --- noxfile.py | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/noxfile.py b/noxfile.py index 2702fd306e4..cad7ba077c6 100644 --- a/noxfile.py +++ b/noxfile.py @@ -47,14 +47,18 @@ def get_author_list(): return sorted(authors, key=lambda x: x.lower()) -def protected_pip(*arguments): - """Get arguments for session.run, that use a "protected" pip. +def run_with_protected_pip(session, *arguments): + """Do a session.run("pip", *arguments), using a "protected" pip. This invokes a wrapper script, that forwards calls to original virtualenv (stable) version, and not the code being tested. This ensures pip being used is not the code being tested. """ - return ("python", LOCATIONS["protected-pip"]) + arguments + env = {"VIRTUAL_ENV": session.virtualenv.location} + + command = ("python", LOCATIONS["protected-pip"]) + arguments + kwargs = {"env": env, "silent": True} + session.run(*command, **kwargs) def should_update_common_wheels(): @@ -84,15 +88,18 @@ def should_update_common_wheels(): def test(session): # Get the common wheels. if should_update_common_wheels(): - session.run(*protected_pip( + run_with_protected_pip( + session, "wheel", "-w", LOCATIONS["common-wheels"], "-r", REQUIREMENTS["common-wheels"], - )) + ) + + # Install sources + run_with_protected_pip(session, "install", ".") - # Install sources and dependencies - session.run(*protected_pip("install", ".")) - session.run(*protected_pip("install", "-r", REQUIREMENTS["tests"])) + # Install test dependencies + run_with_protected_pip(session, "install", "-r", REQUIREMENTS["tests"]) # Parallelize tests as much as possible, by default. arguments = session.posargs or ["-n", "auto"] From f6d690406b3f6d8040170a072644447f0c666cbb Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 7 Oct 2019 15:42:24 +0530 Subject: [PATCH 0488/3170] nox: Log details when reusing existing common-wheels --- noxfile.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/noxfile.py b/noxfile.py index cad7ba077c6..3244add5f36 100644 --- a/noxfile.py +++ b/noxfile.py @@ -94,6 +94,12 @@ def test(session): "-w", LOCATIONS["common-wheels"], "-r", REQUIREMENTS["common-wheels"], ) + else: + msg = ( + "Re-using existing common-wheels at {}." + .format(LOCATIONS["common-wheels"]) + ) + session.log(msg) # Install sources run_with_protected_pip(session, "install", ".") From db5432bf9c49704fd7de2ec0b3baf56037065cb2 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 7 Oct 2019 15:43:00 +0530 Subject: [PATCH 0489/3170] nox: Install from a source distribution --- noxfile.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/noxfile.py b/noxfile.py index 3244add5f36..3ddd0c8aaa1 100644 --- a/noxfile.py +++ b/noxfile.py @@ -101,8 +101,19 @@ def test(session): ) session.log(msg) - # Install sources - run_with_protected_pip(session, "install", ".") + # Build source distribution + sdist_dir = os.path.join(session.virtualenv.location, "sdist") + session.run( + "python", "setup.py", "sdist", + "--formats=zip", "--dist-dir", sdist_dir, + silent=True, + ) + generated_files = os.listdir(sdist_dir) + assert len(generated_files) == 1 + generated_sdist = os.path.join(sdist_dir, generated_files[0]) + + # Install source distribution + run_with_protected_pip(session, "install", generated_sdist) # Install test dependencies run_with_protected_pip(session, "install", "-r", REQUIREMENTS["tests"]) From 37bc629551f384825ea062a7ea843c23cc435dee Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 7 Oct 2019 18:00:59 +0530 Subject: [PATCH 0490/3170] Rename tests.lib.path.Path.{abspath -> resolve()} --- tests/conftest.py | 4 ++-- tests/functional/test_install.py | 6 +++--- tests/functional/test_install_reqs.py | 4 ++-- tests/lib/__init__.py | 8 ++++---- tests/lib/path.py | 3 +-- tests/unit/test_req.py | 2 +- tests/unit/test_req_file.py | 2 +- 7 files changed, 14 insertions(+), 15 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index d47c7b6fb1b..b832ab8ca86 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -190,7 +190,7 @@ def not_code_files_and_folders(path, names): # Copy over our source tree so that each use is self contained shutil.copytree( SRC_DIR, - pip_src.abspath, + pip_src.resolve(), ignore=not_code_files_and_folders, ) return pip_src @@ -222,7 +222,7 @@ def wheel_install(tmpdir_factory, common_wheels): def install_egg_link(venv, project_name, egg_info_dir): with open(venv.site / 'easy-install.pth', 'a') as fp: - fp.write(str(egg_info_dir.abspath) + '\n') + fp.write(str(egg_info_dir.resolve()) + '\n') with open(venv.site / (project_name + '.egg-link'), 'w') as fp: fp.write(str(egg_info_dir) + '\n.') diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index dccde8c7c3f..2e980d5402d 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -456,14 +456,14 @@ def test_hashed_install_success(script, data, tmpdir): """ file_url = path_to_url( - (data.packages / 'simple-1.0.tar.gz').abspath) + (data.packages / 'simple-1.0.tar.gz').resolve()) with requirements_file( 'simple2==1.0 --hash=sha256:9336af72ca661e6336eb87bc7de3e8844d853e' '3848c2b9bbd2e8bf01db88c2c7\n' '{simple} --hash=sha256:393043e672415891885c9a2a0929b1af95fb866d6c' 'a016b42d2e6ce53619b653'.format(simple=file_url), tmpdir) as reqs_file: - script.pip_install_local('-r', reqs_file.abspath, expect_error=False) + script.pip_install_local('-r', reqs_file.resolve(), expect_error=False) def test_hashed_install_failure(script, tmpdir): @@ -478,7 +478,7 @@ def test_hashed_install_failure(script, tmpdir): 'c7de3e8844d853e3848c2b9bbd2e8bf01db88c2c\n', tmpdir) as reqs_file: result = script.pip_install_local('-r', - reqs_file.abspath, + reqs_file.resolve(), expect_error=True) assert len(result.files_created) == 0 diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index c356b74ddc3..faa971eadb3 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -371,7 +371,7 @@ def test_double_install_spurious_hash_mismatch( # Install a package (and build its wheel): result = script.pip_install_local( '--find-links', data.find_links, - '-r', reqs_file.abspath, expect_error=False) + '-r', reqs_file.resolve(), expect_error=False) assert 'Successfully installed simple-1.0' in str(result) # Uninstall it: @@ -381,7 +381,7 @@ def test_double_install_spurious_hash_mismatch( # package should install happily. result = script.pip_install_local( '--find-links', data.find_links, - '-r', reqs_file.abspath, expect_error=False) + '-r', reqs_file.resolve(), expect_error=False) assert 'Successfully installed simple-1.0' in str(result) diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index d65b23a61dc..276c6e85a70 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -28,8 +28,8 @@ from pip._internal.models.target_python import TargetPython -DATA_DIR = Path(__file__).parent.parent.joinpath("data").abspath -SRC_DIR = Path(__file__).abspath.parent.parent.parent +DATA_DIR = Path(__file__).parent.parent.joinpath("data").resolve() +SRC_DIR = Path(__file__).resolve().parent.parent.parent pyversion = get_major_minor_version() pyversion_tuple = sys.version_info @@ -65,7 +65,7 @@ def _test_path_to_file_url(path): Args: path: a tests.lib.path.Path object. """ - return 'file://' + path.abspath.replace('\\', '/') + return 'file://' + path.resolve().replace('\\', '/') def create_file(path, contents=None): @@ -155,7 +155,7 @@ class TestData(object): def __init__(self, root, source=None): self.source = source or DATA_DIR - self.root = Path(root).abspath + self.root = Path(root).resolve() @classmethod def copy(cls, root): diff --git a/tests/lib/path.py b/tests/lib/path.py index 086571b90fb..5e1d924989c 100644 --- a/tests/lib/path.py +++ b/tests/lib/path.py @@ -107,8 +107,7 @@ def suffix(self): """ return Path(os.path.splitext(self)[1]) - @property - def abspath(self): + def resolve(self): """ './a/bc.d' -> '/home/a/bc.d' """ diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index 78a525afbce..dea73f368fb 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -270,7 +270,7 @@ def test_unpinned_hash_checking(self, data): def test_hash_mismatch(self, data): """A hash mismatch should raise an error.""" file_url = path_to_url( - (data.packages / 'simple-1.0.tar.gz').abspath) + (data.packages / 'simple-1.0.tar.gz').resolve()) reqset = RequirementSet(require_hashes=True) reqset.add_requirement(get_processed_req_from_line( '%s --hash=sha256:badbad' % file_url, lineno=1, diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py index 0e0af2db2b8..41e07e60f02 100644 --- a/tests/unit/test_req_file.py +++ b/tests/unit/test_req_file.py @@ -677,7 +677,7 @@ def test_install_requirements_with_options(self, tmpdir, finder, session, '''.format(global_option=global_option, install_option=install_option) with requirements_file(content, tmpdir) as reqs_file: - req = next(parse_requirements(reqs_file.abspath, + req = next(parse_requirements(reqs_file.resolve(), finder=finder, options=options, session=session)) From d82874c4174f836b92cc9e02dc0caf11be63ab71 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 7 Oct 2019 21:29:40 +0530 Subject: [PATCH 0491/3170] Revert "Optimize upgrade of already-satisfied pinned requirement" --- news/7132.feature | 1 - src/pip/_internal/legacy_resolve.py | 4 ---- 2 files changed, 5 deletions(-) delete mode 100644 news/7132.feature diff --git a/news/7132.feature b/news/7132.feature deleted file mode 100644 index a873a669c56..00000000000 --- a/news/7132.feature +++ /dev/null @@ -1 +0,0 @@ -Skip reaching out to the package index, if a pinned version is already satisfied. diff --git a/src/pip/_internal/legacy_resolve.py b/src/pip/_internal/legacy_resolve.py index 866a508b885..c24158f4d37 100644 --- a/src/pip/_internal/legacy_resolve.py +++ b/src/pip/_internal/legacy_resolve.py @@ -267,10 +267,6 @@ def _check_skip_installed(self, req_to_install): # 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: - if req_to_install.is_pinned: - # No need to check the index for a better version. - return 'already satisfied' - try: self.finder.find_requirement(req_to_install, upgrade=True) except BestVersionAlreadyInstalled: From df624d05dff3675bb7e805a73f0bb8cfedebc962 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 7 Oct 2019 21:55:05 +0530 Subject: [PATCH 0492/3170] Delete 1234.trival --- news/1234.trival | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 news/1234.trival diff --git a/news/1234.trival b/news/1234.trival deleted file mode 100644 index e69de29bb2d..00000000000 From 0eff60851d19cd9421e6833781c2cd539f7962d7 Mon Sep 17 00:00:00 2001 From: anatoly techtonik <techtonik@gmail.com> Date: Mon, 7 Oct 2019 21:01:20 +0300 Subject: [PATCH 0493/3170] Keep read() function according to review --- setup.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/setup.py b/setup.py index 15e08593d00..4d81c157eae 100644 --- a/setup.py +++ b/setup.py @@ -5,15 +5,16 @@ from setuptools import find_packages, setup -def codopen(rel_path): +def read(rel_path): here = os.path.abspath(os.path.dirname(__file__)) # intentionally *not* adding an encoding option to open, See: # https://github.com/pypa/virtualenv/issues/201#issuecomment-3145690 - return codecs.open(os.path.join(here, rel_path), 'r') + with codecs.open(os.path.join(here, rel_path), 'r') as fp: + return fp.read() def get_version(rel_path): - for line in codopen(rel_path): + for line in read(rel_path).splitlines(): if line.startswith('__version__'): # __version__ = "0.9" delim = '\"' if '\"' in line else '\'' @@ -22,7 +23,7 @@ def get_version(rel_path): raise RuntimeError("Unable to find version string.") -long_description = codopen('README.rst').read() +long_description = read('README.rst') setup( name="pip", From 3eda51fd346545c273de94211f18b578de055e3d Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 6 Oct 2019 22:42:40 -0400 Subject: [PATCH 0494/3170] Conditionally import ssl Saves >=10ms on irrelevant platforms. --- .../_internal/utils/inject_securetransport.py | 42 ++++++++++++------- 1 file changed, 27 insertions(+), 15 deletions(-) diff --git a/src/pip/_internal/utils/inject_securetransport.py b/src/pip/_internal/utils/inject_securetransport.py index f56731f6bf1..5b93b1d6730 100644 --- a/src/pip/_internal/utils/inject_securetransport.py +++ b/src/pip/_internal/utils/inject_securetransport.py @@ -7,18 +7,30 @@ old to handle TLSv1.2. """ -try: - import ssl -except ImportError: - pass -else: - import sys - - # Checks for OpenSSL 1.0.1 on MacOS - if sys.platform == "darwin" and ssl.OPENSSL_VERSION_NUMBER < 0x1000100f: - try: - from pip._vendor.urllib3.contrib import securetransport - except (ImportError, OSError): - pass - else: - securetransport.inject_into_urllib3() +import sys + + +def inject_securetransport(): + # type: () -> None + # Only relevant on macOS + if sys.platform != "darwin": + return + + try: + import ssl + except ImportError: + return + + # Checks for OpenSSL 1.0.1 + if ssl.OPENSSL_VERSION_NUMBER >= 0x1000100f: + return + + try: + from pip._vendor.urllib3.contrib import securetransport + except (ImportError, OSError): + return + + securetransport.inject_into_urllib3() + + +inject_securetransport() From c6906f1a38cc98667777438a747395599e247c04 Mon Sep 17 00:00:00 2001 From: tbeswick <tbeswick@enphaseenergy.com> Date: Tue, 8 Oct 2019 18:15:22 +1300 Subject: [PATCH 0495/3170] - Abstracted out common `get_subdirectory()` code in `Git` and `Mercurial`, adding `get_repo_root_dir()` for the vcs specific code. - Reverted behaviour of `Git.controls_location()` and `Mercurial.controls_location()` to call the vcs command if the base `VersionControl.controls_location()` doesn't detect the vcs directory. - Added `log_failed_cmd` argument `VcsSupport.run_command()` to allow vcs commands to be tried without logging errors if they aren't present. - Corrected indentation. - Removed `expect_stderr=True` in `test_freeze_mercurial_clone_srcdir` as its not required. --- src/pip/_internal/utils/misc.py | 13 +++--- src/pip/_internal/vcs/git.py | 30 +++----------- src/pip/_internal/vcs/mercurial.py | 49 +++++++--------------- src/pip/_internal/vcs/versioncontrol.py | 54 +++++++++++++++++++++++-- tests/functional/test_freeze.py | 11 ++--- 5 files changed, 82 insertions(+), 75 deletions(-) diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 5f13f975c29..915d925e620 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -628,7 +628,8 @@ def call_subprocess( command_desc=None, # type: Optional[str] extra_environ=None, # type: Optional[Mapping[str, Any]] unset_environ=None, # type: Optional[Iterable[str]] - spinner=None # type: Optional[SpinnerInterface] + spinner=None, # type: Optional[SpinnerInterface] + log_failed_cmd=True # type: Optional[bool] ): # type: (...) -> Text """ @@ -639,6 +640,7 @@ def call_subprocess( acceptable, in addition to 0. Defaults to None, which means []. unset_environ: an iterable of environment variable names to unset prior to calling subprocess.Popen(). + log_failed_cmd: if false, failed commands are not logged, only raised. """ if extra_ok_returncodes is None: extra_ok_returncodes = [] @@ -694,9 +696,10 @@ def call_subprocess( ) proc.stdin.close() except Exception as exc: - subprocess_logger.critical( - "Error %s while executing command %s", exc, command_desc, - ) + if log_failed_cmd: + subprocess_logger.critical( + "Error %s while executing command %s", exc, command_desc, + ) raise all_output = [] while True: @@ -727,7 +730,7 @@ def call_subprocess( spinner.finish("done") if proc_had_error: if on_returncode == 'raise': - if not showing_subprocess: + if not showing_subprocess and log_failed_cmd: # Then the subprocess streams haven't been logged to the # console yet. msg = make_subprocess_output_error( diff --git a/src/pip/_internal/vcs/git.py b/src/pip/_internal/vcs/git.py index e92af2d0593..c6e0f548e3b 100644 --- a/src/pip/_internal/vcs/git.py +++ b/src/pip/_internal/vcs/git.py @@ -9,7 +9,6 @@ from pip._vendor.six.moves.urllib import request as urllib_request from pip._internal.exceptions import BadCommand -from pip._internal.utils.compat import samefile from pip._internal.utils.misc import display_path, make_command from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -290,31 +289,13 @@ def get_revision(cls, location, rev=None): return current_rev.strip() @classmethod - def get_subdirectory(cls, location): - # find the repo root + def get_repo_root_dir(cls, location): git_dir = cls.run_command(['rev-parse', '--git-dir'], show_stdout=False, cwd=location).strip() if not os.path.isabs(git_dir): git_dir = os.path.join(location, git_dir) root_dir = os.path.join(git_dir, '..') - # find setup.py - orig_location = location - while not os.path.exists(os.path.join(location, 'setup.py')): - last_location = location - location = os.path.dirname(location) - if location == last_location: - # We've traversed up to the root of the filesystem without - # finding setup.py - logger.warning( - "Could not find setup.py for directory %s (tried all " - "parent directories)", - orig_location, - ) - return None - # relative path of setup.py to repo root - if samefile(root_dir, location): - return None - return os.path.relpath(location, root_dir) + return os.path.abspath(root_dir) @classmethod def get_url_rev_and_auth(cls, url): @@ -362,13 +343,14 @@ def update_submodules(cls, location): @classmethod def controls_location(cls, location): - if not super(Git, cls).controls_location(location): - return False + if super(Git, cls).controls_location(location): + return True try: r = cls.run_command(['rev-parse'], cwd=location, show_stdout=False, - on_returncode='ignore') + on_returncode='ignore', + log_failed_cmd=False) return not r except BadCommand: logger.debug("could not determine if %s is under git control " diff --git a/src/pip/_internal/vcs/mercurial.py b/src/pip/_internal/vcs/mercurial.py index de412a612ab..e82fdb6f6e6 100644 --- a/src/pip/_internal/vcs/mercurial.py +++ b/src/pip/_internal/vcs/mercurial.py @@ -5,8 +5,7 @@ from pip._vendor.six.moves import configparser -from pip._internal.exceptions import BadCommand -from pip._internal.utils.compat import samefile +from pip._internal.exceptions import BadCommand, InstallationError from pip._internal.utils.misc import display_path, make_command, path_to_url from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -114,45 +113,25 @@ def is_commit_id_equal(cls, dest, name): return False @classmethod - def get_subdirectory(cls, location): - # find the repo root - root_dir = cls.run_command(['root'], - show_stdout=False, cwd=location).strip() + def get_repo_root_dir(cls, location): + root_dir = cls.run_command( + ['root'], show_stdout=False, cwd=location).strip() if not os.path.isabs(root_dir): root_dir = os.path.join(location, root_dir) - # find setup.py - orig_location = location - while not os.path.exists(os.path.join(location, 'setup.py')): - last_location = location - location = os.path.dirname(location) - if location == last_location: - # We've traversed up to the root of the filesystem without - # finding setup.py - logger.warning( - "Could not find setup.py for directory %s (tried all " - "parent directories)", - orig_location, - ) - return None - # relative path of setup.py to repo root - if samefile(root_dir, location): - return None - return os.path.relpath(location, root_dir) + return os.path.abspath(root_dir) @classmethod def controls_location(cls, location): - if not super(Mercurial, cls).controls_location(location): - return False + if super(Mercurial, cls).controls_location(location): + return True try: - r = cls.run_command(['identify'], - cwd=location, - show_stdout=False, - on_returncode='ignore', - extra_ok_returncodes=[255]) - return not r.startswith('abort:') - except BadCommand: - logger.debug("could not determine if %s is under hg control " - "because hg is not available", location) + cls.run_command( + ['identify'], + cwd=location, + show_stdout=False, + on_returncode='raise', + log_failed_cmd=False) + except (BadCommand, InstallationError): return False diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index 610fb5a4347..ac5711286c8 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -11,6 +11,7 @@ from pip._vendor.six.moves.urllib import parse as urllib_parse from pip._internal.exceptions import BadCommand +from pip._internal.utils.compat import samefile from pip._internal.utils.misc import ( ask_path_exists, backup_dir, @@ -236,11 +237,54 @@ def should_add_vcs_url_prefix(cls, remote_url): return not remote_url.lower().startswith('{}:'.format(cls.name)) @classmethod - def get_subdirectory(cls, repo_dir): + def get_subdirectory(cls, location): """ Return the path to setup.py, relative to the repo root. + Return None if setup.py is in the repo root. """ - return None + # find the repo root + root_dir = cls.get_repo_root_dir(location) + if root_dir is None: + logger.warning( + "Repo root could not be detected for %s, " + "assuming it is the root.", + location) + return None + # find setup.py + orig_location = location + while not os.path.exists(os.path.join(location, 'setup.py')): + last_location = location + location = os.path.dirname(location) + if location == last_location: + # We've traversed up to the root of the filesystem without + # finding setup.py + logger.warning( + "Could not find setup.py for directory %s (tried all " + "parent directories)", + orig_location, + ) + return None + # relative path of setup.py to repo root + if samefile(root_dir, location): + return None + return os.path.relpath(location, root_dir) + + @classmethod + def get_repo_root_dir(cls, location): + """ + Return the absolute path to the repo root directory. + + Return None if not found. + This can be overridden by subclasses to interrogate the vcs tool to + find the repo root. + """ + while not cls.is_repository_directory(location): + last_location = location + location = os.path.dirname(location) + if location == last_location: + # We've traversed up to the root of the filesystem. + return None + return os.path.abspath(location) @classmethod def get_requirement_revision(cls, repo_dir): @@ -578,7 +622,8 @@ def run_command( extra_ok_returncodes=None, # type: Optional[Iterable[int]] command_desc=None, # type: Optional[str] extra_environ=None, # type: Optional[Mapping[str, Any]] - spinner=None # type: Optional[SpinnerInterface] + spinner=None, # type: Optional[SpinnerInterface] + log_failed_cmd=True ): # type: (...) -> Text """ @@ -594,7 +639,8 @@ def run_command( command_desc=command_desc, extra_environ=extra_environ, unset_environ=cls.unset_environ, - spinner=spinner) + spinner=spinner, + log_failed_cmd=log_failed_cmd) except OSError as e: # errno.ENOENT = no such file or directory # In other words, the VCS executable isn't available diff --git a/tests/functional/test_freeze.py b/tests/functional/test_freeze.py index b3e06bfdc04..546a4828d5c 100644 --- a/tests/functional/test_freeze.py +++ b/tests/functional/test_freeze.py @@ -333,16 +333,14 @@ def test_freeze_mercurial_clone_srcdir(script, tmpdir): pkg_version = _create_test_package_with_srcdir(script, vcs='hg') result = script.run( - 'hg', 'clone', pkg_version, 'pip-test-package', - expect_stderr=True, + 'hg', 'clone', pkg_version, 'pip-test-package' ) repo_dir = script.scratch_path / 'pip-test-package' result = script.run( 'python', 'setup.py', 'develop', - cwd=repo_dir / 'subdir', - expect_stderr=True, + cwd=repo_dir / 'subdir' ) - result = script.pip('freeze', expect_stderr=True) + result = script.pip('freeze') expected = textwrap.dedent( """ ...-e hg+...#egg=version_pkg&subdirectory=subdir @@ -352,8 +350,7 @@ def test_freeze_mercurial_clone_srcdir(script, tmpdir): _check_output(result.stdout, expected) result = script.pip( - 'freeze', '-f', '%s#egg=pip_test_package' % repo_dir, - expect_stderr=True, + 'freeze', '-f', '%s#egg=pip_test_package' % repo_dir ) expected = textwrap.dedent( """ From 74972037b578712c46397009a3f2d74e1c0c2fe4 Mon Sep 17 00:00:00 2001 From: Krishna Oza <krishoza15sep@gmail.com> Date: Tue, 8 Oct 2019 12:52:00 +0530 Subject: [PATCH 0496/3170] Simplify an interestingly written conditional (#7161) --- src/pip/_internal/utils/misc.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 56339951048..b84826350bc 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -491,12 +491,9 @@ def egg_link_path(dist): """ sites = [] if running_under_virtualenv(): - if virtualenv_no_global(): - sites.append(site_packages) - else: - sites.append(site_packages) - if user_site: - sites.append(user_site) + sites.append(site_packages) + if not virtualenv_no_global() and user_site: + sites.append(user_site) else: if user_site: sites.append(user_site) From a0cbe4c44087d95cff36e5fbfaf4da629464a866 Mon Sep 17 00:00:00 2001 From: tbeswick <tbeswick@enphaseenergy.com> Date: Wed, 9 Oct 2019 09:11:58 +1300 Subject: [PATCH 0497/3170] Fixing bad merge. --- src/pip/_internal/utils/misc.py | 225 -------------------------- src/pip/_internal/utils/subprocess.py | 13 +- src/pip/_internal/vcs/git.py | 3 +- 3 files changed, 10 insertions(+), 231 deletions(-) diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index c0c9120f214..56339951048 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -525,231 +525,6 @@ def dist_location(dist): return normalize_path(dist.location) -def make_command(*args): - # type: (Union[str, HiddenText, CommandArgs]) -> CommandArgs - """ - Create a CommandArgs object. - """ - command_args = [] # type: CommandArgs - for arg in args: - # Check for list instead of CommandArgs since CommandArgs is - # only known during type-checking. - if isinstance(arg, list): - command_args.extend(arg) - else: - # Otherwise, arg is str or HiddenText. - command_args.append(arg) - - return command_args - - -def format_command_args(args): - # type: (Union[List[str], CommandArgs]) -> str - """ - Format command arguments for display. - """ - # For HiddenText arguments, display the redacted form by calling str(). - # Also, we don't apply str() to arguments that aren't HiddenText since - # this can trigger a UnicodeDecodeError in Python 2 if the argument - # has type unicode and includes a non-ascii character. (The type - # checker doesn't ensure the annotations are correct in all cases.) - return ' '.join( - shlex_quote(str(arg)) if isinstance(arg, HiddenText) - else shlex_quote(arg) for arg in args - ) - - -def reveal_command_args(args): - # type: (Union[List[str], CommandArgs]) -> List[str] - """ - Return the arguments in their raw, unredacted form. - """ - return [ - arg.secret if isinstance(arg, HiddenText) else arg for arg in args - ] - - -def make_subprocess_output_error( - cmd_args, # type: Union[List[str], CommandArgs] - cwd, # type: Optional[str] - lines, # type: List[Text] - exit_status, # type: int -): - # type: (...) -> Text - """ - Create and return the error message to use to log a subprocess error - with command output. - - :param lines: A list of lines, each ending with a newline. - """ - command = format_command_args(cmd_args) - # Convert `command` and `cwd` to text (unicode in Python 2) so we can use - # them as arguments in the unicode format string below. This avoids - # "UnicodeDecodeError: 'ascii' codec can't decode byte ..." in Python 2 - # if either contains a non-ascii character. - command_display = str_to_display(command, desc='command bytes') - cwd_display = path_to_display(cwd) - - # We know the joined output value ends in a newline. - output = ''.join(lines) - msg = ( - # Use a unicode string to avoid "UnicodeEncodeError: 'ascii' - # codec can't encode character ..." in Python 2 when a format - # argument (e.g. `output`) has a non-ascii character. - u'Command errored out with exit status {exit_status}:\n' - ' command: {command_display}\n' - ' cwd: {cwd_display}\n' - 'Complete output ({line_count} lines):\n{output}{divider}' - ).format( - exit_status=exit_status, - command_display=command_display, - cwd_display=cwd_display, - line_count=len(lines), - output=output, - divider=LOG_DIVIDER, - ) - return msg - - -def call_subprocess( - cmd, # type: Union[List[str], CommandArgs] - show_stdout=False, # type: bool - cwd=None, # type: Optional[str] - on_returncode='raise', # type: str - extra_ok_returncodes=None, # type: Optional[Iterable[int]] - command_desc=None, # type: Optional[str] - extra_environ=None, # type: Optional[Mapping[str, Any]] - unset_environ=None, # type: Optional[Iterable[str]] - spinner=None, # type: Optional[SpinnerInterface] - log_failed_cmd=True # type: Optional[bool] -): - # type: (...) -> Text - """ - Args: - show_stdout: if true, use INFO to log the subprocess's stderr and - stdout streams. Otherwise, use DEBUG. Defaults to False. - extra_ok_returncodes: an iterable of integer return codes that are - acceptable, in addition to 0. Defaults to None, which means []. - unset_environ: an iterable of environment variable names to unset - prior to calling subprocess.Popen(). - log_failed_cmd: if false, failed commands are not logged, only raised. - """ - if extra_ok_returncodes is None: - extra_ok_returncodes = [] - if unset_environ is None: - unset_environ = [] - # Most places in pip use show_stdout=False. What this means is-- - # - # - We connect the child's output (combined stderr and stdout) to a - # single pipe, which we read. - # - We log this output to stderr at DEBUG level as it is received. - # - If DEBUG logging isn't enabled (e.g. if --verbose logging wasn't - # requested), then we show a spinner so the user can still see the - # subprocess is in progress. - # - If the subprocess exits with an error, we log the output to stderr - # at ERROR level if it hasn't already been displayed to the console - # (e.g. if --verbose logging wasn't enabled). This way we don't log - # the output to the console twice. - # - # If show_stdout=True, then the above is still done, but with DEBUG - # replaced by INFO. - if show_stdout: - # Then log the subprocess output at INFO level. - log_subprocess = subprocess_logger.info - used_level = logging.INFO - else: - # Then log the subprocess output using DEBUG. This also ensures - # it will be logged to the log file (aka user_log), if enabled. - log_subprocess = subprocess_logger.debug - used_level = logging.DEBUG - - # Whether the subprocess will be visible in the console. - showing_subprocess = subprocess_logger.getEffectiveLevel() <= used_level - - # Only use the spinner if we're not showing the subprocess output - # and we have a spinner. - use_spinner = not showing_subprocess and spinner is not None - - if command_desc is None: - command_desc = format_command_args(cmd) - - log_subprocess("Running command %s", command_desc) - env = os.environ.copy() - if extra_environ: - env.update(extra_environ) - for name in unset_environ: - env.pop(name, None) - try: - proc = subprocess.Popen( - # Convert HiddenText objects to the underlying str. - reveal_command_args(cmd), - stderr=subprocess.STDOUT, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, cwd=cwd, env=env, - ) - proc.stdin.close() - except Exception as exc: - if log_failed_cmd: - subprocess_logger.critical( - "Error %s while executing command %s", exc, command_desc, - ) - raise - all_output = [] - while True: - # The "line" value is a unicode string in Python 2. - line = console_to_str(proc.stdout.readline()) - if not line: - break - line = line.rstrip() - all_output.append(line + '\n') - - # Show the line immediately. - log_subprocess(line) - # Update the spinner. - if use_spinner: - spinner.spin() - try: - proc.wait() - finally: - if proc.stdout: - proc.stdout.close() - proc_had_error = ( - proc.returncode and proc.returncode not in extra_ok_returncodes - ) - if use_spinner: - if proc_had_error: - spinner.finish("error") - else: - spinner.finish("done") - if proc_had_error: - if on_returncode == 'raise': - if not showing_subprocess and log_failed_cmd: - # Then the subprocess streams haven't been logged to the - # console yet. - msg = make_subprocess_output_error( - cmd_args=cmd, - cwd=cwd, - lines=all_output, - exit_status=proc.returncode, - ) - subprocess_logger.error(msg) - exc_msg = ( - 'Command errored out with exit status {}: {} ' - 'Check the logs for full command output.' - ).format(proc.returncode, command_desc) - raise InstallationError(exc_msg) - elif on_returncode == 'warn': - subprocess_logger.warning( - 'Command "%s" had error code %s in %s', - command_desc, proc.returncode, cwd, - ) - elif on_returncode == 'ignore': - pass - else: - raise ValueError('Invalid value: on_returncode=%s' % - repr(on_returncode)) - return ''.join(all_output) - - def write_output(msg, *args): # type: (str, str) -> None logger.info(msg, *args) diff --git a/src/pip/_internal/utils/subprocess.py b/src/pip/_internal/utils/subprocess.py index ddb418d2467..2a0c5d1a655 100644 --- a/src/pip/_internal/utils/subprocess.py +++ b/src/pip/_internal/utils/subprocess.py @@ -123,7 +123,8 @@ def call_subprocess( command_desc=None, # type: Optional[str] extra_environ=None, # type: Optional[Mapping[str, Any]] unset_environ=None, # type: Optional[Iterable[str]] - spinner=None # type: Optional[SpinnerInterface] + spinner=None, # type: Optional[SpinnerInterface] + log_failed_cmd=True # type: Optional[bool] ): # type: (...) -> Text """ @@ -134,6 +135,7 @@ def call_subprocess( acceptable, in addition to 0. Defaults to None, which means []. unset_environ: an iterable of environment variable names to unset prior to calling subprocess.Popen(). + log_failed_cmd: if false, failed commands are not logged, only raised. """ if extra_ok_returncodes is None: extra_ok_returncodes = [] @@ -189,9 +191,10 @@ def call_subprocess( ) proc.stdin.close() except Exception as exc: - subprocess_logger.critical( - "Error %s while executing command %s", exc, command_desc, - ) + if log_failed_cmd: + subprocess_logger.critical( + "Error %s while executing command %s", exc, command_desc, + ) raise all_output = [] while True: @@ -222,7 +225,7 @@ def call_subprocess( spinner.finish("done") if proc_had_error: if on_returncode == 'raise': - if not showing_subprocess: + if not showing_subprocess and log_failed_cmd: # Then the subprocess streams haven't been logged to the # console yet. msg = make_subprocess_output_error( diff --git a/src/pip/_internal/vcs/git.py b/src/pip/_internal/vcs/git.py index 7f5407ddc39..d94a207fb99 100644 --- a/src/pip/_internal/vcs/git.py +++ b/src/pip/_internal/vcs/git.py @@ -12,8 +12,9 @@ from pip._vendor.six.moves.urllib import request as urllib_request from pip._internal.exceptions import BadCommand -from pip._internal.utils.misc import display_path, make_command from pip._internal.utils.compat import samefile +from pip._internal.utils.misc import display_path +from pip._internal.utils.subprocess import make_command from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.vcs.versioncontrol import ( From 5ad4291b06899c6b2ebbbb22c5c69990e5ad7950 Mon Sep 17 00:00:00 2001 From: tbeswick <tbeswick@enphaseenergy.com> Date: Wed, 9 Oct 2019 09:40:59 +1300 Subject: [PATCH 0498/3170] Added newline to end of news file. --- news/7071.bugfix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/7071.bugfix b/news/7071.bugfix index e4fa847d78f..f56ea39ffc7 100644 --- a/news/7071.bugfix +++ b/news/7071.bugfix @@ -1 +1 @@ -Fix `pip freeze` not showing correct entry for mercurial packages that use subdirectories. \ No newline at end of file +Fix `pip freeze` not showing correct entry for mercurial packages that use subdirectories. From f7b44a82fec455c27d21f7719c3a76523ebf4468 Mon Sep 17 00:00:00 2001 From: tbeswick <tbeswick@enphaseenergy.com> Date: Wed, 9 Oct 2019 09:45:41 +1300 Subject: [PATCH 0499/3170] Removed unused import. --- src/pip/_internal/vcs/git.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pip/_internal/vcs/git.py b/src/pip/_internal/vcs/git.py index d94a207fb99..65ff69877e3 100644 --- a/src/pip/_internal/vcs/git.py +++ b/src/pip/_internal/vcs/git.py @@ -12,7 +12,6 @@ from pip._vendor.six.moves.urllib import request as urllib_request from pip._internal.exceptions import BadCommand -from pip._internal.utils.compat import samefile from pip._internal.utils.misc import display_path from pip._internal.utils.subprocess import make_command from pip._internal.utils.temp_dir import TempDirectory From 1028e307824c26a8704ea3a929ae57de2f242053 Mon Sep 17 00:00:00 2001 From: tbeswick <tbeswick@enphaseenergy.com> Date: Wed, 9 Oct 2019 10:12:38 +1300 Subject: [PATCH 0500/3170] Fixed lint error in news. --- news/7071.bugfix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/7071.bugfix b/news/7071.bugfix index f56ea39ffc7..f0463ce3c19 100644 --- a/news/7071.bugfix +++ b/news/7071.bugfix @@ -1 +1 @@ -Fix `pip freeze` not showing correct entry for mercurial packages that use subdirectories. +Fix ``pip freeze`` not showing correct entry for mercurial packages that use subdirectories. From 042d4844e8fe8b664c7e29c0c66a4fe399c45581 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver <thomas@kluyver.me.uk> Date: Mon, 9 Sep 2019 15:22:08 +0100 Subject: [PATCH 0501/3170] Default to --user install in certain conditions --- src/pip/_internal/commands/install.py | 20 ++++++++++++-- src/pip/_internal/utils/filesystem.py | 40 +++++++++++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 66071f6e819..2cb9c055d7f 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -13,6 +13,7 @@ import operator import os import shutil +import site from optparse import SUPPRESS_HELP from pip._vendor import pkg_resources @@ -28,11 +29,11 @@ InstallationError, PreviousBuildDirError, ) -from pip._internal.locations import distutils_scheme +from pip._internal.locations import distutils_scheme, site_packages from pip._internal.operations.check import check_install_conflicts from pip._internal.req import RequirementSet, install_given_reqs from pip._internal.req.req_tracker import RequirementTracker -from pip._internal.utils.filesystem import check_path_owner +from pip._internal.utils.filesystem import check_path_owner, test_writable_dir from pip._internal.utils.misc import ( ensure_dir, get_installed_version, @@ -305,6 +306,17 @@ def run(self, options, args): install_options.append('--user') install_options.append('--prefix=') + elif options.use_user_site is None: + if options.prefix_path or options.target_dir: + options.use_user_site = False + elif site_packages_writable( + root=options.root_path, + isolated=options.isolated_mode + ): + options.use_user_site = False + elif site.ENABLE_USER_SITE: + options.use_user_site = True + target_temp_dir = None # type: Optional[TempDirectory] target_temp_dir_path = None # type: Optional[str] if options.target_dir: @@ -594,6 +606,10 @@ def get_lib_location_guesses(*args, **kwargs): return [scheme['purelib'], scheme['platlib']] +def site_packages_writable(**kwargs): + return all(test_writable_dir(d) for d in get_lib_location_guesses(**kwargs)) + + def create_env_error_message(error, show_traceback, using_user_site): """Format an error message for an EnvironmentError diff --git a/src/pip/_internal/utils/filesystem.py b/src/pip/_internal/utils/filesystem.py index f4a389cd92f..db099e4a64b 100644 --- a/src/pip/_internal/utils/filesystem.py +++ b/src/pip/_internal/utils/filesystem.py @@ -1,5 +1,6 @@ import os import os.path +import random import shutil import stat from contextlib import contextmanager @@ -113,3 +114,42 @@ def replace(src, dest): else: replace = _replace_retry(os.replace) + + +# test_writable_dir and _test_writable_dir_win are copied from Flit, +# with the author's agreement to also place them under pip's license. +def test_writable_dir(path): + """Check if a directory is writable. + + Uses os.access() on POSIX, tries creating files on Windows. + """ + if os.name == 'posix': + return os.access(path, os.W_OK) + + return _test_writable_dir_win(path) + + +def _test_writable_dir_win(path): + # os.access doesn't work on Windows: http://bugs.python.org/issue2528 + # and we can't use tempfile: http://bugs.python.org/issue22107 + basename = 'accesstest_deleteme_fishfingers_custard_' + alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789' + for i in range(10): + name = basename + ''.join(random.choice(alphabet) for _ in range(6)) + file = os.path.join(path, name) + try: + with open(file, mode='xb'): + pass + except FileExistsError: + continue + except PermissionError: + # This could be because there's a directory with the same name. + # But it's highly unlikely there's a directory called that, + # so we'll assume it's because the parent directory is not writable. + return False + else: + os.unlink(file) + return True + + # This should never be reached + raise EnvironmentError('Unexpected condition testing for writable directory') From 14e76d0eaf7f4aa59894403fa92eed4d7fea4074 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver <thomas@kluyver.me.uk> Date: Wed, 9 Oct 2019 08:38:58 +0100 Subject: [PATCH 0502/3170] Remove unused import --- src/pip/_internal/commands/install.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 2cb9c055d7f..b5180f5b514 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -29,7 +29,7 @@ InstallationError, PreviousBuildDirError, ) -from pip._internal.locations import distutils_scheme, site_packages +from pip._internal.locations import distutils_scheme from pip._internal.operations.check import check_install_conflicts from pip._internal.req import RequirementSet, install_given_reqs from pip._internal.req.req_tracker import RequirementTracker From 172274ac1dcb2839ec8a52bdf07a8d88dd6814d9 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver <thomas@kluyver.me.uk> Date: Wed, 9 Oct 2019 08:39:07 +0100 Subject: [PATCH 0503/3170] Add type annotation comments --- src/pip/_internal/utils/filesystem.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pip/_internal/utils/filesystem.py b/src/pip/_internal/utils/filesystem.py index db099e4a64b..6de6ea65d4d 100644 --- a/src/pip/_internal/utils/filesystem.py +++ b/src/pip/_internal/utils/filesystem.py @@ -119,6 +119,7 @@ def replace(src, dest): # test_writable_dir and _test_writable_dir_win are copied from Flit, # with the author's agreement to also place them under pip's license. def test_writable_dir(path): + # type: (str) -> bool """Check if a directory is writable. Uses os.access() on POSIX, tries creating files on Windows. @@ -130,6 +131,7 @@ def test_writable_dir(path): def _test_writable_dir_win(path): + # type: (str) -> bool # os.access doesn't work on Windows: http://bugs.python.org/issue2528 # and we can't use tempfile: http://bugs.python.org/issue22107 basename = 'accesstest_deleteme_fishfingers_custard_' From f7132d9c6d898c10e0c9146a572818f46be4581e Mon Sep 17 00:00:00 2001 From: Thomas Kluyver <thomas@kluyver.me.uk> Date: Wed, 9 Oct 2019 08:43:42 +0100 Subject: [PATCH 0504/3170] Fix OSError catching for Python 2 --- src/pip/_internal/utils/filesystem.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/utils/filesystem.py b/src/pip/_internal/utils/filesystem.py index 6de6ea65d4d..88d214a32ab 100644 --- a/src/pip/_internal/utils/filesystem.py +++ b/src/pip/_internal/utils/filesystem.py @@ -1,3 +1,4 @@ +import errno import os import os.path import random @@ -142,13 +143,15 @@ def _test_writable_dir_win(path): try: with open(file, mode='xb'): pass - except FileExistsError: - continue - except PermissionError: - # This could be because there's a directory with the same name. - # But it's highly unlikely there's a directory called that, - # so we'll assume it's because the parent directory is not writable. - return False + except OSError as e: + if e.errno == errno.EEXIST: + continue + if e.errno == errno.EPERM: + # This could be because there's a directory with the same name. + # But it's highly unlikely there's a directory called that, + # so we'll assume it's because the parent dir is not writable. + return False + raise else: os.unlink(file) return True From a6b13b637b1cd7208f478734817212e409ff8a17 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver <thomas@kluyver.me.uk> Date: Wed, 9 Oct 2019 08:43:58 +0100 Subject: [PATCH 0505/3170] Fix maximum line length --- src/pip/_internal/commands/install.py | 4 +++- src/pip/_internal/utils/filesystem.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index b5180f5b514..dfeb9c04ff6 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -607,7 +607,9 @@ def get_lib_location_guesses(*args, **kwargs): def site_packages_writable(**kwargs): - return all(test_writable_dir(d) for d in get_lib_location_guesses(**kwargs)) + return all( + test_writable_dir(d) for d in get_lib_location_guesses(**kwargs) + ) def create_env_error_message(error, show_traceback, using_user_site): diff --git a/src/pip/_internal/utils/filesystem.py b/src/pip/_internal/utils/filesystem.py index 88d214a32ab..90e344a1465 100644 --- a/src/pip/_internal/utils/filesystem.py +++ b/src/pip/_internal/utils/filesystem.py @@ -157,4 +157,6 @@ def _test_writable_dir_win(path): return True # This should never be reached - raise EnvironmentError('Unexpected condition testing for writable directory') + raise EnvironmentError( + 'Unexpected condition testing for writable directory' + ) From f2b1882b6a0438d4fead602ff3c3e08e9e8ea7ae Mon Sep 17 00:00:00 2001 From: Thomas Kluyver <thomas@kluyver.me.uk> Date: Wed, 9 Oct 2019 08:46:33 +0100 Subject: [PATCH 0506/3170] Add news file --- news/1668.feature | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 news/1668.feature diff --git a/news/1668.feature b/news/1668.feature new file mode 100644 index 00000000000..d200841ea7f --- /dev/null +++ b/news/1668.feature @@ -0,0 +1,2 @@ +Default to doing a user install (as if ``--user`` was passed) when the main +site-packages directory is not writeable and user site-packages are enabled. From a9405b22fcae2fc3f5dc7d2ef79e3bbcdea32363 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver <thomas@kluyver.me.uk> Date: Wed, 9 Oct 2019 08:48:33 +0100 Subject: [PATCH 0507/3170] Ensure necessary install_options are set for default user install --- src/pip/_internal/commands/install.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index dfeb9c04ff6..f7bbceaec37 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -316,6 +316,8 @@ def run(self, options, args): options.use_user_site = False elif site.ENABLE_USER_SITE: options.use_user_site = True + install_options.append('--user') + install_options.append('--prefix=') target_temp_dir = None # type: Optional[TempDirectory] target_temp_dir_path = None # type: Optional[str] From 086ab4c52aa1112f57acfabe166a4095681978df Mon Sep 17 00:00:00 2001 From: Thomas Kluyver <thomas@kluyver.me.uk> Date: Wed, 9 Oct 2019 09:46:12 +0100 Subject: [PATCH 0508/3170] Fix exclusive-mode open for Python 2 --- src/pip/_internal/utils/filesystem.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/utils/filesystem.py b/src/pip/_internal/utils/filesystem.py index 90e344a1465..f076f738d52 100644 --- a/src/pip/_internal/utils/filesystem.py +++ b/src/pip/_internal/utils/filesystem.py @@ -141,8 +141,7 @@ def _test_writable_dir_win(path): name = basename + ''.join(random.choice(alphabet) for _ in range(6)) file = os.path.join(path, name) try: - with open(file, mode='xb'): - pass + fd = os.open(file, os.O_RDWR | os.O_CREAT | os.O_EXCL) except OSError as e: if e.errno == errno.EEXIST: continue @@ -153,6 +152,7 @@ def _test_writable_dir_win(path): return False raise else: + os.close(fd) os.unlink(file) return True From 357c1322221290ee640454fe28fce7f7dfeae95d Mon Sep 17 00:00:00 2001 From: Thomas Kluyver <thomas@kluyver.me.uk> Date: Wed, 9 Oct 2019 10:13:29 +0100 Subject: [PATCH 0509/3170] Handle nonexistant directory when checking for write access --- src/pip/_internal/utils/filesystem.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/pip/_internal/utils/filesystem.py b/src/pip/_internal/utils/filesystem.py index f076f738d52..bedd662958a 100644 --- a/src/pip/_internal/utils/filesystem.py +++ b/src/pip/_internal/utils/filesystem.py @@ -125,6 +125,13 @@ def test_writable_dir(path): Uses os.access() on POSIX, tries creating files on Windows. """ + # If the directory doesn't exist, find the closest parent that does. + while not os.path.isdir(path): + parent = os.path.dirname(path) + if parent == path: + break # Should never get here, but infinite loops are bad + path = parent + if os.name == 'posix': return os.access(path, os.W_OK) From 4a4f1ca1cfbfa7d8a6ac4ae55f8aae8501eb2fbc Mon Sep 17 00:00:00 2001 From: Thomas Kluyver <thomas@kluyver.me.uk> Date: Wed, 9 Oct 2019 13:54:46 +0100 Subject: [PATCH 0510/3170] Relax failing tests due to changing site-packages mtime --- tests/functional/test_install.py | 1 - tests/functional/test_install_upgrade.py | 4 +++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 2e980d5402d..d6a2ce0ee9c 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -248,7 +248,6 @@ def test_basic_editable_install(script): in result.stderr ) assert not result.files_created - assert not result.files_updated @pytest.mark.svn diff --git a/tests/functional/test_install_upgrade.py b/tests/functional/test_install_upgrade.py index 36b518b2546..6d2eeb5dc4e 100644 --- a/tests/functional/test_install_upgrade.py +++ b/tests/functional/test_install_upgrade.py @@ -245,7 +245,9 @@ def test_upgrade_to_same_version_from_url(script): 'https://files.pythonhosted.org/packages/source/I/INITools/INITools-' '0.3.tar.gz', ) - assert not result2.files_updated, 'INITools 0.3 reinstalled same version' + assert script.site_packages / 'initools' not in result2.files_updated, ( + 'INITools 0.3 reinstalled same version' + ) result3 = script.pip('uninstall', 'initools', '-y') assert_all_changes(result, result3, [script.venv / 'build', 'cache']) From 5f1468274987348b569aa586eeca4363494d0357 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver <thomas@kluyver.me.uk> Date: Wed, 9 Oct 2019 17:34:16 +0100 Subject: [PATCH 0511/3170] Factor out code for handling the --user option & its default --- src/pip/_internal/commands/install.py | 70 ++++++++++++++++++--------- 1 file changed, 47 insertions(+), 23 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index f7bbceaec37..84feb023bc5 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -292,33 +292,19 @@ def run(self, options, args): options.src_dir = os.path.abspath(options.src_dir) install_options = options.install_options or [] + + options.use_user_site = decide_user_install( + options.use_user_site, + prefix_path=options.prefix_path, + target_dir=options.target_dir, + root_path=options.root_path, + isolated_mode=options.isolated_mode, + ) + 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=') - elif options.use_user_site is None: - if options.prefix_path or options.target_dir: - options.use_user_site = False - elif site_packages_writable( - root=options.root_path, - isolated=options.isolated_mode - ): - options.use_user_site = False - elif site.ENABLE_USER_SITE: - options.use_user_site = True - install_options.append('--user') - install_options.append('--prefix=') - target_temp_dir = None # type: Optional[TempDirectory] target_temp_dir_path = None # type: Optional[str] if options.target_dir: @@ -614,6 +600,44 @@ def site_packages_writable(**kwargs): ) +def decide_user_install( + use_user_site, + prefix_path, + target_dir, + root_path, + isolated_mode, +): + """Determine whether to do a user install based on the input options. + + If use_user_site is True/False, that is checked for compatibility with + other options. If None, the default behaviour depends on other options + and the environment. + """ + if use_user_site: + if prefix_path: + raise CommandError( + "Can not combine '--user' and '--prefix' as they imply " + "different installation locations" + ) + if virtualenv_no_global(): + raise InstallationError( + "Can not perform a '--user' install. User site-packages " + "are not visible in this virtualenv." + ) + if use_user_site in (True, False): + return use_user_site + + if prefix_path or target_dir: + return False # user install incompatible with --prefix/--target + + # Default behaviour: prefer non-user installation if that looks possible. + # If we don't have permission for that and user site-packages are visible, + # choose a user install. + return site.ENABLE_USER_SITE and not site_packages_writable( + root=root_path, isolated=isolated_mode + ) + + def create_env_error_message(error, show_traceback, using_user_site): """Format an error message for an EnvironmentError From 62d84a5aae229aa13c3ca41b6c1a91706e9daccd Mon Sep 17 00:00:00 2001 From: mayeut <mayeut@users.noreply.github.com> Date: Thu, 26 Sep 2019 00:03:39 +0200 Subject: [PATCH 0512/3170] Add manylinux2014 support Per PEP 599: https://www.python.org/dev/peps/pep-0599/ --- news/7102.feature | 4 ++ src/pip/_internal/pep425tags.py | 32 ++++++++++++++ tests/functional/test_download.py | 4 ++ tests/unit/test_pep425tags.py | 71 ++++++++++++++++++++++++++++--- 4 files changed, 104 insertions(+), 7 deletions(-) create mode 100644 news/7102.feature diff --git a/news/7102.feature b/news/7102.feature new file mode 100644 index 00000000000..4412649fcb3 --- /dev/null +++ b/news/7102.feature @@ -0,0 +1,4 @@ +Implement manylinux2014 platform tag support. manylinux2014 is the successor +to manylinux2010. It allows carefully compiled binary wheels to be installed +on compatible Linux platforms. The manylinux2014 platform tag definition can +be found in `PEP599 <https://www.python.org/dev/peps/pep-0599/>`_. diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index f60d7a63707..9225b1b9bed 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -200,6 +200,26 @@ def is_manylinux2010_compatible(): return pip._internal.utils.glibc.have_compatible_glibc(2, 12) +def is_manylinux2014_compatible(): + # type: () -> bool + # Only Linux and only x86-64, i686, aarch64, armv7l, ppc64, ppc64le, s390x + if get_platform() not in {"linux_x86_64", "linux_i686", "linux_aarch64", + "linux_armv7l", "linux_ppc64", "linux_ppc64le", + "linux_s390x"}: + return False + + # Check for presence of _manylinux module + try: + import _manylinux + return bool(_manylinux.manylinux2014_compatible) + except (ImportError, AttributeError): + # Fall through to heuristic check below + pass + + # Check glibc version. CentOS 7 uses glibc 2.17. + return pip._internal.utils.glibc.have_compatible_glibc(2, 17) + + def get_darwin_arches(major, minor, machine): # type: (int, int, str) -> List[str] """Return a list of supported arches (including group arches) for @@ -333,6 +353,16 @@ def get_supported( else: # arch pattern didn't match (?!) arches = [arch] + elif arch_prefix == 'manylinux2014': + arches = [arch] + # manylinux1/manylinux2010 wheels run on most manylinux2014 systems + # with the exception of wheels depending on ncurses. PEP 599 states + # manylinux1/manylinux2010 wheels should be considered + # manylinux2014 wheels: + # https://www.python.org/dev/peps/pep-0599/#backwards-compatibility-with-manylinux2010-wheels + if arch_suffix in {'i686', 'x86_64'}: + arches.append('manylinux2010' + arch_sep + arch_suffix) + arches.append('manylinux1' + arch_sep + arch_suffix) elif arch_prefix == 'manylinux2010': # manylinux1 wheels run on most manylinux2010 systems with the # exception of wheels depending on ncurses. PEP 571 states @@ -341,6 +371,8 @@ def get_supported( arches = [arch, 'manylinux1' + arch_sep + arch_suffix] elif platform is None: arches = [] + if is_manylinux2014_compatible(): + arches.append('manylinux2014' + arch_sep + arch_suffix) if is_manylinux2010_compatible(): arches.append('manylinux2010' + arch_sep + arch_suffix) if is_manylinux1_compatible(): diff --git a/tests/functional/test_download.py b/tests/functional/test_download.py index 7873e255b61..77f54e8ae22 100644 --- a/tests/functional/test_download.py +++ b/tests/functional/test_download.py @@ -331,6 +331,7 @@ class TestDownloadPlatformManylinuxes(object): "linux_x86_64", "manylinux1_x86_64", "manylinux2010_x86_64", + "manylinux2014_x86_64", ]) def test_download_universal(self, platform, script, data): """ @@ -353,6 +354,9 @@ def test_download_universal(self, platform, script, data): ("manylinux1_x86_64", "manylinux1_x86_64"), ("manylinux1_x86_64", "manylinux2010_x86_64"), ("manylinux2010_x86_64", "manylinux2010_x86_64"), + ("manylinux1_x86_64", "manylinux2014_x86_64"), + ("manylinux2010_x86_64", "manylinux2014_x86_64"), + ("manylinux2014_x86_64", "manylinux2014_x86_64"), ]) def test_download_compatible_manylinuxes( self, wheel_abi, platform, script, data, diff --git a/tests/unit/test_pep425tags.py b/tests/unit/test_pep425tags.py index a18f525a98f..c43843043aa 100644 --- a/tests/unit/test_pep425tags.py +++ b/tests/unit/test_pep425tags.py @@ -137,6 +137,7 @@ def test_manual_abi_dm_flags(self): @pytest.mark.parametrize('is_manylinux_compatible', [ pep425tags.is_manylinux1_compatible, pep425tags.is_manylinux2010_compatible, + pep425tags.is_manylinux2014_compatible, ]) class TestManylinuxTags(object): """ @@ -156,28 +157,28 @@ def test_manylinux_compatible_on_linux_x86_64(self, @patch('pip._internal.pep425tags.get_platform', lambda: 'linux_i686') @patch('pip._internal.utils.glibc.have_compatible_glibc', lambda major, minor: True) - def test_manylinux1_compatible_on_linux_i686(self, - is_manylinux_compatible): + def test_manylinux_compatible_on_linux_i686(self, + is_manylinux_compatible): """ - Test that manylinux1 is enabled on linux_i686 + Test that manylinuxes are enabled on linux_i686 """ assert is_manylinux_compatible() @patch('pip._internal.pep425tags.get_platform', lambda: 'linux_x86_64') @patch('pip._internal.utils.glibc.have_compatible_glibc', lambda major, minor: False) - def test_manylinux1_2(self, is_manylinux_compatible): + def test_manylinux_2(self, is_manylinux_compatible): """ - Test that manylinux1 is disabled with incompatible glibc + Test that manylinuxes are disabled with incompatible glibc """ assert not is_manylinux_compatible() @patch('pip._internal.pep425tags.get_platform', lambda: 'arm6vl') @patch('pip._internal.utils.glibc.have_compatible_glibc', lambda major, minor: True) - def test_manylinux1_3(self, is_manylinux_compatible): + def test_manylinux_3(self, is_manylinux_compatible): """ - Test that manylinux1 is disabled on arm6vl + Test that manylinuxes are disabled on arm6vl """ assert not is_manylinux_compatible() @@ -186,6 +187,8 @@ class TestManylinux1Tags(object): @patch('pip._internal.pep425tags.is_manylinux2010_compatible', lambda: False) + @patch('pip._internal.pep425tags.is_manylinux2014_compatible', + lambda: False) @patch('pip._internal.pep425tags.get_platform', lambda: 'linux_x86_64') @patch('pip._internal.utils.glibc.have_compatible_glibc', lambda major, minor: True) @@ -210,6 +213,8 @@ def test_manylinux1_tag_is_first(self): class TestManylinux2010Tags(object): + @patch('pip._internal.pep425tags.is_manylinux2014_compatible', + lambda: False) @patch('pip._internal.pep425tags.get_platform', lambda: 'linux_x86_64') @patch('pip._internal.utils.glibc.have_compatible_glibc', lambda major, minor: True) @@ -253,3 +258,55 @@ def test_manylinux2010_implies_manylinux1(self, manylinux2010, manylinux1): if arches == ['any']: continue assert arches[:2] == [manylinux2010, manylinux1] + + +class TestManylinux2014Tags(object): + + @patch('pip._internal.pep425tags.get_platform', lambda: 'linux_x86_64') + @patch('pip._internal.utils.glibc.have_compatible_glibc', + lambda major, minor: True) + @patch('sys.platform', 'linux2') + def test_manylinux2014_tag_is_first(self): + """ + Test that the more specific tag manylinux2014 comes first. + """ + groups = {} + for pyimpl, abi, arch in pep425tags.get_supported(): + groups.setdefault((pyimpl, abi), []).append(arch) + + for arches in groups.values(): + if arches == ['any']: + continue + # Expect the most specific arch first: + if len(arches) == 5: + assert arches == ['manylinux2014_x86_64', + 'manylinux2010_x86_64', + 'manylinux1_x86_64', + 'linux_x86_64', + 'any'] + else: + assert arches == ['manylinux2014_x86_64', + 'manylinux2010_x86_64', + 'manylinux1_x86_64', + 'linux_x86_64'] + + @pytest.mark.parametrize("manylinuxA,manylinuxB", [ + ("manylinux2014_x86_64", ["manylinux2010_x86_64", + "manylinux1_x86_64"]), + ("manylinux2014_i686", ["manylinux2010_i686", "manylinux1_i686"]), + ]) + def test_manylinuxA_implies_manylinuxB(self, manylinuxA, manylinuxB): + """ + Specifying manylinux2014 implies manylinux2010/manylinux1. + """ + groups = {} + supported = pep425tags.get_supported(platform=manylinuxA) + for pyimpl, abi, arch in supported: + groups.setdefault((pyimpl, abi), []).append(arch) + + expected_arches = [manylinuxA] + expected_arches.extend(manylinuxB) + for arches in groups.values(): + if arches == ['any']: + continue + assert arches[:3] == expected_arches From d3d3ccad47f605ac1c1f35f824d67e4afa8c8c13 Mon Sep 17 00:00:00 2001 From: mayeut <mayeut@users.noreply.github.com> Date: Wed, 2 Oct 2019 22:55:19 +0200 Subject: [PATCH 0513/3170] Detect armv7l hard-float ABI for manylinux2014 PEP 599 defines manylinux2014 armv7l to be compatible with centos7 altarch armv7 i.e. armhf using the gnueabihf ABI --- src/pip/_internal/pep425tags.py | 40 +++++++++++++++++++++++++++++---- 1 file changed, 36 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index 9225b1b9bed..042ba34b38f 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -164,6 +164,32 @@ def get_platform(): return result +def is_linux_armhf(): + # type: () -> bool + if get_platform() != "linux_armv7l": + return False + # hard-float ABI can be detected from the ELF header of the running + # process + try: + with open(sys.executable, 'rb') as f: + elf_header_raw = f.read(40) # read 40 first bytes of ELF header + except (IOError, OSError, TypeError): + return False + if elf_header_raw is None or len(elf_header_raw) < 40: + return False + if isinstance(elf_header_raw, str): + elf_header = [ord(c) for c in elf_header_raw] + else: + elf_header = [b for b in elf_header_raw] + result = elf_header[0:4] == [0x7f, 0x45, 0x4c, 0x46] # ELF magic number + result &= elf_header[4:5] == [1] # 32-bit ELF + result &= elf_header[5:6] == [1] # little-endian + result &= elf_header[18:20] == [0x28, 0] # ARM machine + result &= elf_header[39:40] == [5] # ARM EABIv5 + result &= (elf_header[37:38][0] & 4) == 4 # EF_ARM_ABI_FLOAT_HARD + return result + + def is_manylinux1_compatible(): # type: () -> bool # Only Linux, and only x86-64 / i686 @@ -202,10 +228,16 @@ def is_manylinux2010_compatible(): def is_manylinux2014_compatible(): # type: () -> bool - # Only Linux and only x86-64, i686, aarch64, armv7l, ppc64, ppc64le, s390x - if get_platform() not in {"linux_x86_64", "linux_i686", "linux_aarch64", - "linux_armv7l", "linux_ppc64", "linux_ppc64le", - "linux_s390x"}: + # Only Linux, and only supported architectures + platform = get_platform() + if platform not in {"linux_x86_64", "linux_i686", "linux_aarch64", + "linux_armv7l", "linux_ppc64", "linux_ppc64le", + "linux_s390x"}: + return False + + # check for hard-float ABI in case we're running linux_armv7l not to + # install hard-float ABI wheel in a soft-float ABI environment + if platform == "linux_armv7l" and not is_linux_armhf(): return False # Check for presence of _manylinux module From 3b22ce2fa46f3d1d90ad1ec9b276a8c1bbca0bc1 Mon Sep 17 00:00:00 2001 From: Xavier Fernandez <xavier.fernandez@polyconseil.fr> Date: Wed, 9 Oct 2019 17:05:48 +0200 Subject: [PATCH 0514/3170] Launch vendoring.update --- .../_vendor/cachecontrol/caches/file_cache.py | 4 +-- ...xtlib2.LICENSE => contextlib2.LICENSE.txt} | 2 ++ src/pip/_vendor/contextlib2.pyi | 1 + src/pip/_vendor/pytoml/LICENSE | 32 +++++++++---------- 4 files changed, 21 insertions(+), 18 deletions(-) rename src/pip/_vendor/{contextlib2.LICENSE => contextlib2.LICENSE.txt} (99%) create mode 100644 src/pip/_vendor/contextlib2.pyi diff --git a/src/pip/_vendor/cachecontrol/caches/file_cache.py b/src/pip/_vendor/cachecontrol/caches/file_cache.py index 1ba00806cc3..607b9452428 100644 --- a/src/pip/_vendor/cachecontrol/caches/file_cache.py +++ b/src/pip/_vendor/cachecontrol/caches/file_cache.py @@ -69,8 +69,8 @@ def __init__( raise ValueError("Cannot use use_dir_lock and lock_class together") try: - from pip._vendor.lockfile import LockFile - from pip._vendor.lockfile.mkdirlockfile import MkdirLockFile + from lockfile import LockFile + from lockfile.mkdirlockfile import MkdirLockFile except ImportError: notice = dedent( """ diff --git a/src/pip/_vendor/contextlib2.LICENSE b/src/pip/_vendor/contextlib2.LICENSE.txt similarity index 99% rename from src/pip/_vendor/contextlib2.LICENSE rename to src/pip/_vendor/contextlib2.LICENSE.txt index c12b8ab2dd7..5de20277df9 100644 --- a/src/pip/_vendor/contextlib2.LICENSE +++ b/src/pip/_vendor/contextlib2.LICENSE.txt @@ -1,3 +1,5 @@ + + A. HISTORY OF THE SOFTWARE ========================== diff --git a/src/pip/_vendor/contextlib2.pyi b/src/pip/_vendor/contextlib2.pyi new file mode 100644 index 00000000000..0d6d6bc4889 --- /dev/null +++ b/src/pip/_vendor/contextlib2.pyi @@ -0,0 +1 @@ +from contextlib2 import * \ No newline at end of file diff --git a/src/pip/_vendor/pytoml/LICENSE b/src/pip/_vendor/pytoml/LICENSE index 9739fc67c6d..da4b10cdc7e 100644 --- a/src/pip/_vendor/pytoml/LICENSE +++ b/src/pip/_vendor/pytoml/LICENSE @@ -1,16 +1,16 @@ -No-notice MIT License - -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. - -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. +No-notice MIT License + +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. + +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. From 79b48aab6ef4fea294977817f6506aa2b478d77b Mon Sep 17 00:00:00 2001 From: Xavier Fernandez <xavier.fernandez@polyconseil.fr> Date: Wed, 9 Oct 2019 17:07:06 +0200 Subject: [PATCH 0515/3170] Fix contextlib2 vendoring --- src/pip/_vendor/contextlib2.pyi | 1 - tools/automation/vendoring/__init__.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 src/pip/_vendor/contextlib2.pyi diff --git a/src/pip/_vendor/contextlib2.pyi b/src/pip/_vendor/contextlib2.pyi deleted file mode 100644 index 0d6d6bc4889..00000000000 --- a/src/pip/_vendor/contextlib2.pyi +++ /dev/null @@ -1 +0,0 @@ -from contextlib2 import * \ No newline at end of file diff --git a/tools/automation/vendoring/__init__.py b/tools/automation/vendoring/__init__.py index ca6aee8e71c..ae499ebecff 100644 --- a/tools/automation/vendoring/__init__.py +++ b/tools/automation/vendoring/__init__.py @@ -280,6 +280,7 @@ def update_stubs(ctx): ], # Some projects should not have stubs coz they're single file modules "appdirs": [], + "contextlib2": [], } for lib in vendored_libs: From af3062af411b1f4865a4a44c7891178c3d3fa92a Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 10 Oct 2019 02:58:00 +0530 Subject: [PATCH 0516/3170] Remove the deprecated pip config --venv option (#7163) --- news/7163.removal | 1 + src/pip/_internal/commands/configuration.py | 28 --------------------- tests/unit/test_options.py | 23 ----------------- 3 files changed, 1 insertion(+), 51 deletions(-) create mode 100644 news/7163.removal diff --git a/news/7163.removal b/news/7163.removal new file mode 100644 index 00000000000..e5c7edeefab --- /dev/null +++ b/news/7163.removal @@ -0,0 +1 @@ +Remove the deprecated ``--venv`` option from ``pip config``. diff --git a/src/pip/_internal/commands/configuration.py b/src/pip/_internal/commands/configuration.py index 9b6eb1602da..efcf5bb3699 100644 --- a/src/pip/_internal/commands/configuration.py +++ b/src/pip/_internal/commands/configuration.py @@ -13,9 +13,7 @@ kinds, ) from pip._internal.exceptions import PipError -from pip._internal.utils.deprecation import deprecated from pip._internal.utils.misc import get_prog, write_output -from pip._internal.utils.virtualenv import running_under_virtualenv logger = logging.getLogger(__name__) @@ -87,17 +85,6 @@ def __init__(self, *args, **kwargs): help='Use the current environment configuration file only' ) - self.cmd_opts.add_option( - '--venv', - dest='venv_file', - action='store_true', - default=False, - help=( - '[Deprecated] Use the current environment configuration ' - 'file in a virtual environment only' - ) - ) - self.parser.insert_option_group(0, self.cmd_opts) def run(self, options, args): @@ -144,21 +131,6 @@ def run(self, options, args): return SUCCESS def _determine_file(self, options, need_value): - # Convert legacy venv_file option to site_file or error - if options.venv_file and not options.site_file: - if running_under_virtualenv(): - options.site_file = True - deprecated( - "The --venv option has been deprecated.", - replacement="--site", - gone_in="19.3", - ) - else: - raise PipError( - "Legacy --venv option requires a virtual environment. " - "Use --site instead." - ) - file_options = [key for key, value in ( (kinds.USER, options.user_file), (kinds.GLOBAL, options.global_file), diff --git a/tests/unit/test_options.py b/tests/unit/test_options.py index c49801d99de..a67f34e83cb 100644 --- a/tests/unit/test_options.py +++ b/tests/unit/test_options.py @@ -421,26 +421,3 @@ def test_config_file_options(self, monkeypatch, args, expect): cmd._determine_file(options, need_value=False) else: assert expect == cmd._determine_file(options, need_value=False) - - def test_config_file_venv_option(self, monkeypatch): - cmd = create_command('config') - # Replace a handler with a no-op to avoid side effects - monkeypatch.setattr(cmd, "get_name", lambda *a: None) - - collected_warnings = [] - - def _warn(message, *a, **kw): - collected_warnings.append(message) - monkeypatch.setattr("warnings.warn", _warn) - - options, args = cmd.parser.parse_args(["--venv", "get", "name"]) - assert "site" == cmd._determine_file(options, need_value=False) - assert collected_warnings - assert "--site" in collected_warnings[0] - - # No warning or error if both "--venv" and "--site" are specified - collected_warnings[:] = [] - options, args = cmd.parser.parse_args(["--venv", "--site", "get", - "name"]) - assert "site" == cmd._determine_file(options, need_value=False) - assert not collected_warnings From 0cb85d5734f91b68dc590f386dfc6087f09c5837 Mon Sep 17 00:00:00 2001 From: Xavier Fernandez <xavier.fernandez@polyconseil.fr> Date: Wed, 9 Oct 2019 17:00:51 +0200 Subject: [PATCH 0517/3170] Bump contextlib2 to latest version 0.6.0 --- news/contextlib2.vendor | 2 +- src/pip/_vendor/contextlib2.py | 92 ++++++++++++++++++++++++++++++++-- src/pip/_vendor/vendor.txt | 2 +- 3 files changed, 89 insertions(+), 7 deletions(-) diff --git a/news/contextlib2.vendor b/news/contextlib2.vendor index c10c3c485b5..25a7f1b1f99 100644 --- a/news/contextlib2.vendor +++ b/news/contextlib2.vendor @@ -1 +1 @@ -Add contextlib2 as a vendored dependency. +Add contextlib2 0.6.0 as a vendored dependency. diff --git a/src/pip/_vendor/contextlib2.py b/src/pip/_vendor/contextlib2.py index f08df14ce54..3aae8f4117c 100644 --- a/src/pip/_vendor/contextlib2.py +++ b/src/pip/_vendor/contextlib2.py @@ -1,18 +1,77 @@ """contextlib2 - backports and enhancements to the contextlib module""" +import abc import sys import warnings from collections import deque from functools import wraps -__all__ = ["contextmanager", "closing", "ContextDecorator", "ExitStack", +__all__ = ["contextmanager", "closing", "nullcontext", + "AbstractContextManager", + "ContextDecorator", "ExitStack", "redirect_stdout", "redirect_stderr", "suppress"] # Backwards compatibility __all__ += ["ContextStack"] + +# Backport abc.ABC +if sys.version_info[:2] >= (3, 4): + _abc_ABC = abc.ABC +else: + _abc_ABC = abc.ABCMeta('ABC', (object,), {'__slots__': ()}) + + +# Backport classic class MRO +def _classic_mro(C, result): + if C in result: + return + result.append(C) + for B in C.__bases__: + _classic_mro(B, result) + return result + + +# Backport _collections_abc._check_methods +def _check_methods(C, *methods): + try: + mro = C.__mro__ + except AttributeError: + mro = tuple(_classic_mro(C, [])) + + for method in methods: + for B in mro: + if method in B.__dict__: + if B.__dict__[method] is None: + return NotImplemented + break + else: + return NotImplemented + return True + + +class AbstractContextManager(_abc_ABC): + """An abstract base class for context managers.""" + + def __enter__(self): + """Return `self` upon entering the runtime context.""" + return self + + @abc.abstractmethod + def __exit__(self, exc_type, exc_value, traceback): + """Raise any exception triggered within the runtime context.""" + return None + + @classmethod + def __subclasshook__(cls, C): + """Check whether subclass is considered a subclass of this ABC.""" + if cls is AbstractContextManager: + return _check_methods(C, "__enter__", "__exit__") + return NotImplemented + + class ContextDecorator(object): - "A base class or mixin that enables context managers to work as decorators." + """A base class or mixin that enables context managers to work as decorators.""" def refresh_cm(self): """Returns the context manager used to actually wrap the call to the @@ -176,8 +235,10 @@ class closing(object): """ def __init__(self, thing): self.thing = thing + def __enter__(self): return self.thing + def __exit__(self, *exc_info): self.thing.close() @@ -289,7 +350,7 @@ def _make_context_fixer(frame_exc): # but use exec to avoid SyntaxError in Python 3 def _reraise_with_existing_context(exc_details): exc_type, exc_value, exc_tb = exc_details - exec ("raise exc_type, exc_value, exc_tb") + exec("raise exc_type, exc_value, exc_tb") # Handle old-style classes if they exist try: @@ -302,8 +363,9 @@ def _reraise_with_existing_context(exc_details): def _get_type(obj): obj_type = type(obj) if obj_type is InstanceType: - return obj.__class__ # Old-style class - return obj_type # New-style class + return obj.__class__ # Old-style class + return obj_type # New-style class + # Inspired by discussions on http://bugs.python.org/issue13585 class ExitStack(object): @@ -417,6 +479,7 @@ def __exit__(self, *exc_details): _reraise_with_existing_context(exc_details) return received_exc and suppressed_exc + # Preserve backwards compatibility class ContextStack(ExitStack): """Backwards compatibility alias for ExitStack""" @@ -434,3 +497,22 @@ def register(self, callback, *args, **kwds): def preserve(self): return self.pop_all() + + +class nullcontext(AbstractContextManager): + """Context manager that does no additional processing. + Used as a stand-in for a normal context manager, when a particular + block of code is only sometimes used with a normal context manager: + cm = optional_cm if condition else nullcontext() + with cm: + # Perform operation, using optional_cm if condition is True + """ + + def __init__(self, enter_result=None): + self.enter_result = enter_result + + def __enter__(self): + return self.enter_result + + def __exit__(self, *excinfo): + pass diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 513f514aca4..5e04868231f 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -1,7 +1,7 @@ appdirs==1.4.3 CacheControl==0.12.5 colorama==0.4.1 -contextlib2==0.5.5 +contextlib2==0.6.0 distlib==0.2.9.post0 distro==1.4.0 html5lib==1.0.1 From c30bfa5a9ff9709fbdbd1554a4f06f1e649397a4 Mon Sep 17 00:00:00 2001 From: Xavier Fernandez <xavier.fernandez@polyconseil.fr> Date: Wed, 9 Oct 2019 17:44:54 +0200 Subject: [PATCH 0518/3170] Add vendoring target to heck that vendoring is up-to-date --- .travis.yml | 1 + tox.ini | 14 +++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5ea346dac87..c61e41702be 100644 --- a/.travis.yml +++ b/.travis.yml @@ -17,6 +17,7 @@ jobs: - stage: primary env: TOXENV=docs - env: TOXENV=lint + - env: TOXENV=vendoring # Latest CPython - env: GROUP=1 python: 2.7 diff --git a/tox.ini b/tox.ini index 39ea5fc151e..eb06dbddaea 100644 --- a/tox.ini +++ b/tox.ini @@ -1,7 +1,7 @@ [tox] minversion = 3.4.0 envlist = - docs, packaging, lint, + docs, packaging, lint, vendoring, py27, py35, py36, py37, py38, pypy, pypy3 [helpers] @@ -46,3 +46,15 @@ commands_pre = deps = pre-commit commands = pre-commit run [] --all-files --show-diff-on-failure + +[testenv:vendoring] +skip_install = True +commands_pre = +deps = + invoke + requests +whitelist_externals = git +commands = + # Check that the vendoring is up-to-date + invoke vendoring.update + git diff --exit-code From d9e27f0030cfd56451efaf320ad75867be01fd47 Mon Sep 17 00:00:00 2001 From: Xavier Fernandez <xavier.fernandez@polyconseil.fr> Date: Wed, 9 Oct 2019 16:16:37 +0200 Subject: [PATCH 0519/3170] Bump vendored libraries --- news/certifi.vendor | 1 + news/msgpack.vendor | 1 + news/packaging.vendor | 1 + news/pep517.vendor | 1 + news/pyparsing.vendor | 1 + news/pytoml.vendor | 1 + news/setuptools.vendor | 1 + news/urllib3.vendor | 1 + src/pip/_vendor/certifi/__init__.py | 2 +- src/pip/_vendor/certifi/cacert.pem | 60 - src/pip/_vendor/msgpack/__init__.py | 10 +- src/pip/_vendor/msgpack/_version.py | 2 +- src/pip/_vendor/msgpack/fallback.py | 4 +- src/pip/_vendor/packaging/__about__.py | 2 +- src/pip/_vendor/packaging/markers.py | 2 +- src/pip/_vendor/packaging/tags.py | 404 +++ src/pip/_vendor/pep517/__init__.py | 2 +- src/pip/_vendor/pep517/_in_process.py | 64 +- src/pip/_vendor/pep517/build.py | 80 +- src/pip/_vendor/pep517/check.py | 7 +- src/pip/_vendor/pep517/compat.py | 13 +- src/pip/_vendor/pep517/dirtools.py | 44 + src/pip/_vendor/pep517/envbuild.py | 29 +- src/pip/_vendor/pep517/meta.py | 92 + src/pip/_vendor/pep517/wrappers.py | 155 +- src/pip/_vendor/pkg_resources/__init__.py | 13 +- src/pip/_vendor/pyparsing.py | 2495 ++++++++++------- src/pip/_vendor/pytoml/parser.py | 7 +- src/pip/_vendor/pytoml/writer.py | 8 + src/pip/_vendor/urllib3/__init__.py | 55 +- src/pip/_vendor/urllib3/_collections.py | 37 +- src/pip/_vendor/urllib3/connection.py | 185 +- src/pip/_vendor/urllib3/connectionpool.py | 416 ++- .../urllib3/contrib/_appengine_environ.py | 20 +- .../contrib/_securetransport/bindings.py | 261 +- .../contrib/_securetransport/low_level.py | 52 +- src/pip/_vendor/urllib3/contrib/appengine.py | 118 +- src/pip/_vendor/urllib3/contrib/ntlmpool.py | 98 +- src/pip/_vendor/urllib3/contrib/pyopenssl.py | 129 +- .../urllib3/contrib/securetransport.py | 157 +- src/pip/_vendor/urllib3/contrib/socks.py | 101 +- src/pip/_vendor/urllib3/exceptions.py | 29 +- src/pip/_vendor/urllib3/fields.py | 93 +- src/pip/_vendor/urllib3/filepost.py | 14 +- src/pip/_vendor/urllib3/packages/__init__.py | 2 +- .../urllib3/packages/backports/makefile.py | 9 +- .../urllib3/packages/rfc3986/__init__.py | 56 - .../urllib3/packages/rfc3986/_mixin.py | 353 --- .../urllib3/packages/rfc3986/abnf_regexp.py | 267 -- .../_vendor/urllib3/packages/rfc3986/api.py | 106 - .../urllib3/packages/rfc3986/builder.py | 298 -- .../urllib3/packages/rfc3986/compat.py | 54 - .../urllib3/packages/rfc3986/exceptions.py | 118 - .../_vendor/urllib3/packages/rfc3986/iri.py | 147 - .../_vendor/urllib3/packages/rfc3986/misc.py | 124 - .../urllib3/packages/rfc3986/normalizers.py | 167 -- .../urllib3/packages/rfc3986/parseresult.py | 385 --- .../_vendor/urllib3/packages/rfc3986/uri.py | 153 - .../urllib3/packages/rfc3986/validators.py | 450 --- src/pip/_vendor/urllib3/packages/six.py | 321 ++- .../packages/ssl_match_hostname/__init__.py | 2 +- .../ssl_match_hostname/_implementation.py | 58 +- src/pip/_vendor/urllib3/poolmanager.py | 183 +- src/pip/_vendor/urllib3/request.py | 79 +- src/pip/_vendor/urllib3/response.py | 195 +- src/pip/_vendor/urllib3/util/__init__.py | 60 +- src/pip/_vendor/urllib3/util/connection.py | 16 +- src/pip/_vendor/urllib3/util/request.py | 52 +- src/pip/_vendor/urllib3/util/response.py | 9 +- src/pip/_vendor/urllib3/util/retry.py | 100 +- src/pip/_vendor/urllib3/util/ssl_.py | 175 +- src/pip/_vendor/urllib3/util/timeout.py | 79 +- src/pip/_vendor/urllib3/util/url.py | 372 ++- src/pip/_vendor/urllib3/util/wait.py | 3 + src/pip/_vendor/vendor.txt | 16 +- 75 files changed, 4377 insertions(+), 5300 deletions(-) create mode 100644 news/certifi.vendor create mode 100644 news/msgpack.vendor create mode 100644 news/packaging.vendor create mode 100644 news/pep517.vendor create mode 100644 news/pyparsing.vendor create mode 100644 news/pytoml.vendor create mode 100644 news/setuptools.vendor create mode 100644 news/urllib3.vendor create mode 100644 src/pip/_vendor/packaging/tags.py create mode 100644 src/pip/_vendor/pep517/dirtools.py create mode 100644 src/pip/_vendor/pep517/meta.py delete mode 100644 src/pip/_vendor/urllib3/packages/rfc3986/__init__.py delete mode 100644 src/pip/_vendor/urllib3/packages/rfc3986/_mixin.py delete mode 100644 src/pip/_vendor/urllib3/packages/rfc3986/abnf_regexp.py delete mode 100644 src/pip/_vendor/urllib3/packages/rfc3986/api.py delete mode 100644 src/pip/_vendor/urllib3/packages/rfc3986/builder.py delete mode 100644 src/pip/_vendor/urllib3/packages/rfc3986/compat.py delete mode 100644 src/pip/_vendor/urllib3/packages/rfc3986/exceptions.py delete mode 100644 src/pip/_vendor/urllib3/packages/rfc3986/iri.py delete mode 100644 src/pip/_vendor/urllib3/packages/rfc3986/misc.py delete mode 100644 src/pip/_vendor/urllib3/packages/rfc3986/normalizers.py delete mode 100644 src/pip/_vendor/urllib3/packages/rfc3986/parseresult.py delete mode 100644 src/pip/_vendor/urllib3/packages/rfc3986/uri.py delete mode 100644 src/pip/_vendor/urllib3/packages/rfc3986/validators.py diff --git a/news/certifi.vendor b/news/certifi.vendor new file mode 100644 index 00000000000..66e84cb207e --- /dev/null +++ b/news/certifi.vendor @@ -0,0 +1 @@ +Upgrade certifi to 2019.9.11 diff --git a/news/msgpack.vendor b/news/msgpack.vendor new file mode 100644 index 00000000000..1c101c68f59 --- /dev/null +++ b/news/msgpack.vendor @@ -0,0 +1 @@ +Upgrade msgpack to 0.6.2 diff --git a/news/packaging.vendor b/news/packaging.vendor new file mode 100644 index 00000000000..4076eb0cd2e --- /dev/null +++ b/news/packaging.vendor @@ -0,0 +1 @@ +Upgrade packaging to 19.2 diff --git a/news/pep517.vendor b/news/pep517.vendor new file mode 100644 index 00000000000..c2376b25913 --- /dev/null +++ b/news/pep517.vendor @@ -0,0 +1 @@ +Upgrade pep517 to 0.7.0 diff --git a/news/pyparsing.vendor b/news/pyparsing.vendor new file mode 100644 index 00000000000..90374a1ef91 --- /dev/null +++ b/news/pyparsing.vendor @@ -0,0 +1 @@ +Upgrade pyparsing to 2.4.2 diff --git a/news/pytoml.vendor b/news/pytoml.vendor new file mode 100644 index 00000000000..9916ed83e8b --- /dev/null +++ b/news/pytoml.vendor @@ -0,0 +1 @@ +Upgrade pytoml to 0.1.21 diff --git a/news/setuptools.vendor b/news/setuptools.vendor new file mode 100644 index 00000000000..c576a969da7 --- /dev/null +++ b/news/setuptools.vendor @@ -0,0 +1 @@ +Upgrade setuptools to 41.4.0 diff --git a/news/urllib3.vendor b/news/urllib3.vendor new file mode 100644 index 00000000000..80b98b44e10 --- /dev/null +++ b/news/urllib3.vendor @@ -0,0 +1 @@ +Upgrade urllib3 to 1.25.6 diff --git a/src/pip/_vendor/certifi/__init__.py b/src/pip/_vendor/certifi/__init__.py index 8ccb14e24ad..8e358e4c8f8 100644 --- a/src/pip/_vendor/certifi/__init__.py +++ b/src/pip/_vendor/certifi/__init__.py @@ -1,3 +1,3 @@ from .core import where -__version__ = "2019.06.16" +__version__ = "2019.09.11" diff --git a/src/pip/_vendor/certifi/cacert.pem b/src/pip/_vendor/certifi/cacert.pem index 9ca290f5eee..70fa91f6181 100644 --- a/src/pip/_vendor/certifi/cacert.pem +++ b/src/pip/_vendor/certifi/cacert.pem @@ -771,36 +771,6 @@ vEsXCS+0yx5DaMkHJ8HSXPfqIbloEpw8nL+e/IBcm2PN7EeqJSdnoDfzAIJ9VNep +OkuE6N36B9K -----END CERTIFICATE----- -# Issuer: CN=Class 2 Primary CA O=Certplus -# Subject: CN=Class 2 Primary CA O=Certplus -# Label: "Certplus Class 2 Primary CA" -# Serial: 177770208045934040241468760488327595043 -# MD5 Fingerprint: 88:2c:8c:52:b8:a2:3c:f3:f7:bb:03:ea:ae:ac:42:0b -# SHA1 Fingerprint: 74:20:74:41:72:9c:dd:92:ec:79:31:d8:23:10:8d:c2:81:92:e2:bb -# SHA256 Fingerprint: 0f:99:3c:8a:ef:97:ba:af:56:87:14:0e:d5:9a:d1:82:1b:b4:af:ac:f0:aa:9a:58:b5:d5:7a:33:8a:3a:fb:cb ------BEGIN CERTIFICATE----- -MIIDkjCCAnqgAwIBAgIRAIW9S/PY2uNp9pTXX8OlRCMwDQYJKoZIhvcNAQEFBQAw -PTELMAkGA1UEBhMCRlIxETAPBgNVBAoTCENlcnRwbHVzMRswGQYDVQQDExJDbGFz -cyAyIFByaW1hcnkgQ0EwHhcNOTkwNzA3MTcwNTAwWhcNMTkwNzA2MjM1OTU5WjA9 -MQswCQYDVQQGEwJGUjERMA8GA1UEChMIQ2VydHBsdXMxGzAZBgNVBAMTEkNsYXNz -IDIgUHJpbWFyeSBDQTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANxQ -ltAS+DXSCHh6tlJw/W/uz7kRy1134ezpfgSN1sxvc0NXYKwzCkTsA18cgCSR5aiR -VhKC9+Ar9NuuYS6JEI1rbLqzAr3VNsVINyPi8Fo3UjMXEuLRYE2+L0ER4/YXJQyL -kcAbmXuZVg2v7tK8R1fjeUl7NIknJITesezpWE7+Tt9avkGtrAjFGA7v0lPubNCd -EgETjdyAYveVqUSISnFOYFWe2yMZeVYHDD9jC1yw4r5+FfyUM1hBOHTE4Y+L3yas -H7WLO7dDWWuwJKZtkIvEcupdM5i3y95ee++U8Rs+yskhwcWYAqqi9lt3m/V+llU0 -HGdpwPFC40es/CgcZlUCAwEAAaOBjDCBiTAPBgNVHRMECDAGAQH/AgEKMAsGA1Ud -DwQEAwIBBjAdBgNVHQ4EFgQU43Mt38sOKAze3bOkynm4jrvoMIkwEQYJYIZIAYb4 -QgEBBAQDAgEGMDcGA1UdHwQwMC4wLKAqoCiGJmh0dHA6Ly93d3cuY2VydHBsdXMu -Y29tL0NSTC9jbGFzczIuY3JsMA0GCSqGSIb3DQEBBQUAA4IBAQCnVM+IRBnL39R/ -AN9WM2K191EBkOvDP9GIROkkXe/nFL0gt5o8AP5tn9uQ3Nf0YtaLcF3n5QRIqWh8 -yfFC82x/xXp8HVGIutIKPidd3i1RTtMTZGnkLuPT55sJmabglZvOGtd/vjzOUrMR -FcEPF80Du5wlFbqidon8BvEY0JNLDnyCt6X09l/+7UCmnYR0ObncHoUW2ikbhiMA -ybuJfm6AiB4vFLQDJKgybwOaRywwvlbGp0ICcBvqQNi6BQNwB6SW//1IMwrh3KWB -kJtN3X3n57LNXMhqlfil9o3EXXgIvnsG1knPGTZQIy4I5p4FTUcY1Rbpsda2ENW7 -l7+ijrRU ------END CERTIFICATE----- - # Issuer: CN=DST Root CA X3 O=Digital Signature Trust Co. # Subject: CN=DST Root CA X3 O=Digital Signature Trust Co. # Label: "DST Root CA X3" @@ -1219,36 +1189,6 @@ t0QmwCbAr1UwnjvVNioZBPRcHv/PLLf/0P2HQBHVESO7SMAhqaQoLf0V+LBOK/Qw WyH8EZE0vkHve52Xdf+XlcCWWC/qu0bXu+TZLg== -----END CERTIFICATE----- -# Issuer: CN=Deutsche Telekom Root CA 2 O=Deutsche Telekom AG OU=T-TeleSec Trust Center -# Subject: CN=Deutsche Telekom Root CA 2 O=Deutsche Telekom AG OU=T-TeleSec Trust Center -# Label: "Deutsche Telekom Root CA 2" -# Serial: 38 -# MD5 Fingerprint: 74:01:4a:91:b1:08:c4:58:ce:47:cd:f0:dd:11:53:08 -# SHA1 Fingerprint: 85:a4:08:c0:9c:19:3e:5d:51:58:7d:cd:d6:13:30:fd:8c:de:37:bf -# SHA256 Fingerprint: b6:19:1a:50:d0:c3:97:7f:7d:a9:9b:cd:aa:c8:6a:22:7d:ae:b9:67:9e:c7:0b:a3:b0:c9:d9:22:71:c1:70:d3 ------BEGIN CERTIFICATE----- -MIIDnzCCAoegAwIBAgIBJjANBgkqhkiG9w0BAQUFADBxMQswCQYDVQQGEwJERTEc -MBoGA1UEChMTRGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0GA1UECxMWVC1UZWxlU2Vj -IFRydXN0IENlbnRlcjEjMCEGA1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBSb290IENB -IDIwHhcNOTkwNzA5MTIxMTAwWhcNMTkwNzA5MjM1OTAwWjBxMQswCQYDVQQGEwJE -RTEcMBoGA1UEChMTRGV1dHNjaGUgVGVsZWtvbSBBRzEfMB0GA1UECxMWVC1UZWxl -U2VjIFRydXN0IENlbnRlcjEjMCEGA1UEAxMaRGV1dHNjaGUgVGVsZWtvbSBSb290 -IENBIDIwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCrC6M14IspFLEU -ha88EOQ5bzVdSq7d6mGNlUn0b2SjGmBmpKlAIoTZ1KXleJMOaAGtuU1cOs7TuKhC -QN/Po7qCWWqSG6wcmtoIKyUn+WkjR/Hg6yx6m/UTAtB+NHzCnjwAWav12gz1Mjwr -rFDa1sPeg5TKqAyZMg4ISFZbavva4VhYAUlfckE8FQYBjl2tqriTtM2e66foai1S -NNs671x1Udrb8zH57nGYMsRUFUQM+ZtV7a3fGAigo4aKSe5TBY8ZTNXeWHmb0moc -QqvF1afPaA+W5OFhmHZhyJF81j4A4pFQh+GdCuatl9Idxjp9y7zaAzTVjlsB9WoH -txa2bkp/AgMBAAGjQjBAMB0GA1UdDgQWBBQxw3kbuvVT1xfgiXotF2wKsyudMzAP -BgNVHRMECDAGAQH/AgEFMA4GA1UdDwEB/wQEAwIBBjANBgkqhkiG9w0BAQUFAAOC -AQEAlGRZrTlk5ynrE/5aw4sTV8gEJPB0d8Bg42f76Ymmg7+Wgnxu1MM9756Abrsp -tJh6sTtU6zkXR34ajgv8HzFZMQSyzhfzLMdiNlXiItiJVbSYSKpk+tYcNthEeFpa -IzpXl/V6ME+un2pMSyuOoAPjPuCp1NJ70rOo4nI8rZ7/gFnkm0W09juwzTkZmDLl -6iFhkOQxIY40sfcvNUqFENrnijchvllj4PKFiDFT1FQUhXB59C4Gdyd1Lx+4ivn+ -xbrYNuSD7Odlt79jWvNGr4GUN9RBjNYj1h7P9WgbRGOiWrqnNVmh5XAFmw4jV5mU -Cm26OWMohpLzGITY+9HPBVZkVw== ------END CERTIFICATE----- - # Issuer: CN=Cybertrust Global Root O=Cybertrust, Inc # Subject: CN=Cybertrust Global Root O=Cybertrust, Inc # Label: "Cybertrust Global Root" diff --git a/src/pip/_vendor/msgpack/__init__.py b/src/pip/_vendor/msgpack/__init__.py index b3265075580..4ad9c1a5e13 100644 --- a/src/pip/_vendor/msgpack/__init__.py +++ b/src/pip/_vendor/msgpack/__init__.py @@ -1,6 +1,6 @@ # coding: utf-8 -from pip._vendor.msgpack._version import version -from pip._vendor.msgpack.exceptions import * +from ._version import version +from .exceptions import * from collections import namedtuple @@ -19,12 +19,12 @@ def __new__(cls, code, data): import os if os.environ.get('MSGPACK_PUREPYTHON'): - from pip._vendor.msgpack.fallback import Packer, unpackb, Unpacker + from .fallback import Packer, unpackb, Unpacker else: try: - from pip._vendor.msgpack._cmsgpack import Packer, unpackb, Unpacker + from ._cmsgpack import Packer, unpackb, Unpacker except ImportError: - from pip._vendor.msgpack.fallback import Packer, unpackb, Unpacker + from .fallback import Packer, unpackb, Unpacker def pack(o, stream, **kwargs): diff --git a/src/pip/_vendor/msgpack/_version.py b/src/pip/_vendor/msgpack/_version.py index 926c5e7b02b..1e73a00f631 100644 --- a/src/pip/_vendor/msgpack/_version.py +++ b/src/pip/_vendor/msgpack/_version.py @@ -1 +1 @@ -version = (0, 6, 1) +version = (0, 6, 2) diff --git a/src/pip/_vendor/msgpack/fallback.py b/src/pip/_vendor/msgpack/fallback.py index 5b731dddab7..3836e830b8f 100644 --- a/src/pip/_vendor/msgpack/fallback.py +++ b/src/pip/_vendor/msgpack/fallback.py @@ -59,7 +59,7 @@ def getvalue(self): newlist_hint = lambda size: [] -from pip._vendor.msgpack.exceptions import ( +from .exceptions import ( BufferFull, OutOfData, ExtraData, @@ -67,7 +67,7 @@ def getvalue(self): StackError, ) -from pip._vendor.msgpack import ExtType +from . import ExtType EX_SKIP = 0 diff --git a/src/pip/_vendor/packaging/__about__.py b/src/pip/_vendor/packaging/__about__.py index 7481c9e2983..dc95138d049 100644 --- a/src/pip/_vendor/packaging/__about__.py +++ b/src/pip/_vendor/packaging/__about__.py @@ -18,7 +18,7 @@ __summary__ = "Core utilities for Python packages" __uri__ = "https://github.com/pypa/packaging" -__version__ = "19.0" +__version__ = "19.2" __author__ = "Donald Stufft and individual contributors" __email__ = "donald@stufft.io" diff --git a/src/pip/_vendor/packaging/markers.py b/src/pip/_vendor/packaging/markers.py index 548247681bc..b942e846e15 100644 --- a/src/pip/_vendor/packaging/markers.py +++ b/src/pip/_vendor/packaging/markers.py @@ -259,7 +259,7 @@ def default_environment(): "platform_version": platform.version(), "python_full_version": platform.python_version(), "platform_python_implementation": platform.python_implementation(), - "python_version": platform.python_version()[:3], + "python_version": ".".join(platform.python_version_tuple()[:2]), "sys_platform": sys.platform, } diff --git a/src/pip/_vendor/packaging/tags.py b/src/pip/_vendor/packaging/tags.py new file mode 100644 index 00000000000..ec9942f0f66 --- /dev/null +++ b/src/pip/_vendor/packaging/tags.py @@ -0,0 +1,404 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. + +from __future__ import absolute_import + +import distutils.util + +try: + from importlib.machinery import EXTENSION_SUFFIXES +except ImportError: # pragma: no cover + import imp + + EXTENSION_SUFFIXES = [x[0] for x in imp.get_suffixes()] + del imp +import platform +import re +import sys +import sysconfig +import warnings + + +INTERPRETER_SHORT_NAMES = { + "python": "py", # Generic. + "cpython": "cp", + "pypy": "pp", + "ironpython": "ip", + "jython": "jy", +} + + +_32_BIT_INTERPRETER = sys.maxsize <= 2 ** 32 + + +class Tag(object): + + __slots__ = ["_interpreter", "_abi", "_platform"] + + def __init__(self, interpreter, abi, platform): + self._interpreter = interpreter.lower() + self._abi = abi.lower() + self._platform = platform.lower() + + @property + def interpreter(self): + return self._interpreter + + @property + def abi(self): + return self._abi + + @property + def platform(self): + return self._platform + + def __eq__(self, other): + return ( + (self.platform == other.platform) + and (self.abi == other.abi) + and (self.interpreter == other.interpreter) + ) + + def __hash__(self): + return hash((self._interpreter, self._abi, self._platform)) + + def __str__(self): + return "{}-{}-{}".format(self._interpreter, self._abi, self._platform) + + def __repr__(self): + return "<{self} @ {self_id}>".format(self=self, self_id=id(self)) + + +def parse_tag(tag): + tags = set() + interpreters, abis, platforms = tag.split("-") + for interpreter in interpreters.split("."): + for abi in abis.split("."): + for platform_ in platforms.split("."): + tags.add(Tag(interpreter, abi, platform_)) + return frozenset(tags) + + +def _normalize_string(string): + return string.replace(".", "_").replace("-", "_") + + +def _cpython_interpreter(py_version): + # TODO: Is using py_version_nodot for interpreter version critical? + return "cp{major}{minor}".format(major=py_version[0], minor=py_version[1]) + + +def _cpython_abis(py_version): + abis = [] + version = "{}{}".format(*py_version[:2]) + debug = pymalloc = ucs4 = "" + with_debug = sysconfig.get_config_var("Py_DEBUG") + has_refcount = hasattr(sys, "gettotalrefcount") + # Windows doesn't set Py_DEBUG, so checking for support of debug-compiled + # extension modules is the best option. + # https://github.com/pypa/pip/issues/3383#issuecomment-173267692 + has_ext = "_d.pyd" in EXTENSION_SUFFIXES + if with_debug or (with_debug is None and (has_refcount or has_ext)): + debug = "d" + if py_version < (3, 8): + with_pymalloc = sysconfig.get_config_var("WITH_PYMALLOC") + if with_pymalloc or with_pymalloc is None: + pymalloc = "m" + if py_version < (3, 3): + unicode_size = sysconfig.get_config_var("Py_UNICODE_SIZE") + if unicode_size == 4 or ( + unicode_size is None and sys.maxunicode == 0x10FFFF + ): + ucs4 = "u" + elif debug: + # Debug builds can also load "normal" extension modules. + # We can also assume no UCS-4 or pymalloc requirement. + abis.append("cp{version}".format(version=version)) + abis.insert( + 0, + "cp{version}{debug}{pymalloc}{ucs4}".format( + version=version, debug=debug, pymalloc=pymalloc, ucs4=ucs4 + ), + ) + return abis + + +def _cpython_tags(py_version, interpreter, abis, platforms): + for abi in abis: + for platform_ in platforms: + yield Tag(interpreter, abi, platform_) + for tag in (Tag(interpreter, "abi3", platform_) for platform_ in platforms): + yield tag + for tag in (Tag(interpreter, "none", platform_) for platform_ in platforms): + yield tag + # PEP 384 was first implemented in Python 3.2. + for minor_version in range(py_version[1] - 1, 1, -1): + for platform_ in platforms: + interpreter = "cp{major}{minor}".format( + major=py_version[0], minor=minor_version + ) + yield Tag(interpreter, "abi3", platform_) + + +def _pypy_interpreter(): + return "pp{py_major}{pypy_major}{pypy_minor}".format( + py_major=sys.version_info[0], + pypy_major=sys.pypy_version_info.major, + pypy_minor=sys.pypy_version_info.minor, + ) + + +def _generic_abi(): + abi = sysconfig.get_config_var("SOABI") + if abi: + return _normalize_string(abi) + else: + return "none" + + +def _pypy_tags(py_version, interpreter, abi, platforms): + for tag in (Tag(interpreter, abi, platform) for platform in platforms): + yield tag + for tag in (Tag(interpreter, "none", platform) for platform in platforms): + yield tag + + +def _generic_tags(interpreter, py_version, abi, platforms): + for tag in (Tag(interpreter, abi, platform) for platform in platforms): + yield tag + if abi != "none": + tags = (Tag(interpreter, "none", platform_) for platform_ in platforms) + for tag in tags: + yield tag + + +def _py_interpreter_range(py_version): + """ + Yield Python versions in descending order. + + After the latest version, the major-only version will be yielded, and then + all following versions up to 'end'. + """ + yield "py{major}{minor}".format(major=py_version[0], minor=py_version[1]) + yield "py{major}".format(major=py_version[0]) + for minor in range(py_version[1] - 1, -1, -1): + yield "py{major}{minor}".format(major=py_version[0], minor=minor) + + +def _independent_tags(interpreter, py_version, platforms): + """ + Return the sequence of tags that are consistent across implementations. + + The tags consist of: + - py*-none-<platform> + - <interpreter>-none-any + - py*-none-any + """ + for version in _py_interpreter_range(py_version): + for platform_ in platforms: + yield Tag(version, "none", platform_) + yield Tag(interpreter, "none", "any") + for version in _py_interpreter_range(py_version): + yield Tag(version, "none", "any") + + +def _mac_arch(arch, is_32bit=_32_BIT_INTERPRETER): + if not is_32bit: + return arch + + if arch.startswith("ppc"): + return "ppc" + + return "i386" + + +def _mac_binary_formats(version, cpu_arch): + formats = [cpu_arch] + if cpu_arch == "x86_64": + if version < (10, 4): + return [] + formats.extend(["intel", "fat64", "fat32"]) + + elif cpu_arch == "i386": + if version < (10, 4): + return [] + formats.extend(["intel", "fat32", "fat"]) + + elif cpu_arch == "ppc64": + # TODO: Need to care about 32-bit PPC for ppc64 through 10.2? + if version > (10, 5) or version < (10, 4): + return [] + formats.append("fat64") + + elif cpu_arch == "ppc": + if version > (10, 6): + return [] + formats.extend(["fat32", "fat"]) + + formats.append("universal") + return formats + + +def _mac_platforms(version=None, arch=None): + version_str, _, cpu_arch = platform.mac_ver() + if version is None: + version = tuple(map(int, version_str.split(".")[:2])) + if arch is None: + arch = _mac_arch(cpu_arch) + platforms = [] + for minor_version in range(version[1], -1, -1): + compat_version = version[0], minor_version + binary_formats = _mac_binary_formats(compat_version, arch) + for binary_format in binary_formats: + platforms.append( + "macosx_{major}_{minor}_{binary_format}".format( + major=compat_version[0], + minor=compat_version[1], + binary_format=binary_format, + ) + ) + return platforms + + +# From PEP 513. +def _is_manylinux_compatible(name, glibc_version): + # Check for presence of _manylinux module. + try: + import _manylinux + + return bool(getattr(_manylinux, name + "_compatible")) + except (ImportError, AttributeError): + # Fall through to heuristic check below. + pass + + return _have_compatible_glibc(*glibc_version) + + +def _glibc_version_string(): + # Returns glibc version string, or None if not using glibc. + import ctypes + + # ctypes.CDLL(None) internally calls dlopen(NULL), and as the dlopen + # manpage says, "If filename is NULL, then the returned handle is for the + # main program". This way we can let the linker do the work to figure out + # which libc our process is actually using. + process_namespace = ctypes.CDLL(None) + try: + gnu_get_libc_version = process_namespace.gnu_get_libc_version + except AttributeError: + # Symbol doesn't exist -> therefore, we are not linked to + # glibc. + return None + + # Call gnu_get_libc_version, which returns a string like "2.5" + gnu_get_libc_version.restype = ctypes.c_char_p + version_str = gnu_get_libc_version() + # py2 / py3 compatibility: + if not isinstance(version_str, str): + version_str = version_str.decode("ascii") + + return version_str + + +# Separated out from have_compatible_glibc for easier unit testing. +def _check_glibc_version(version_str, required_major, minimum_minor): + # Parse string and check against requested version. + # + # We use a regexp instead of str.split because we want to discard any + # random junk that might come after the minor version -- this might happen + # in patched/forked versions of glibc (e.g. Linaro's version of glibc + # uses version strings like "2.20-2014.11"). See gh-3588. + m = re.match(r"(?P<major>[0-9]+)\.(?P<minor>[0-9]+)", version_str) + if not m: + warnings.warn( + "Expected glibc version with 2 components major.minor," + " got: %s" % version_str, + RuntimeWarning, + ) + return False + return ( + int(m.group("major")) == required_major + and int(m.group("minor")) >= minimum_minor + ) + + +def _have_compatible_glibc(required_major, minimum_minor): + version_str = _glibc_version_string() + if version_str is None: + return False + return _check_glibc_version(version_str, required_major, minimum_minor) + + +def _linux_platforms(is_32bit=_32_BIT_INTERPRETER): + linux = _normalize_string(distutils.util.get_platform()) + if linux == "linux_x86_64" and is_32bit: + linux = "linux_i686" + manylinux_support = ( + ("manylinux2014", (2, 17)), # CentOS 7 w/ glibc 2.17 (PEP 599) + ("manylinux2010", (2, 12)), # CentOS 6 w/ glibc 2.12 (PEP 571) + ("manylinux1", (2, 5)), # CentOS 5 w/ glibc 2.5 (PEP 513) + ) + manylinux_support_iter = iter(manylinux_support) + for name, glibc_version in manylinux_support_iter: + if _is_manylinux_compatible(name, glibc_version): + platforms = [linux.replace("linux", name)] + break + else: + platforms = [] + # Support for a later manylinux implies support for an earlier version. + platforms += [linux.replace("linux", name) for name, _ in manylinux_support_iter] + platforms.append(linux) + return platforms + + +def _generic_platforms(): + platform = _normalize_string(distutils.util.get_platform()) + return [platform] + + +def _interpreter_name(): + name = platform.python_implementation().lower() + return INTERPRETER_SHORT_NAMES.get(name) or name + + +def _generic_interpreter(name, py_version): + version = sysconfig.get_config_var("py_version_nodot") + if not version: + version = "".join(map(str, py_version[:2])) + return "{name}{version}".format(name=name, version=version) + + +def sys_tags(): + """ + Returns the sequence of tag triples for the running interpreter. + + The order of the sequence corresponds to priority order for the + interpreter, from most to least important. + """ + py_version = sys.version_info[:2] + interpreter_name = _interpreter_name() + if platform.system() == "Darwin": + platforms = _mac_platforms() + elif platform.system() == "Linux": + platforms = _linux_platforms() + else: + platforms = _generic_platforms() + + if interpreter_name == "cp": + interpreter = _cpython_interpreter(py_version) + abis = _cpython_abis(py_version) + for tag in _cpython_tags(py_version, interpreter, abis, platforms): + yield tag + elif interpreter_name == "pp": + interpreter = _pypy_interpreter() + abi = _generic_abi() + for tag in _pypy_tags(py_version, interpreter, abi, platforms): + yield tag + else: + interpreter = _generic_interpreter(interpreter_name, py_version) + abi = _generic_abi() + for tag in _generic_tags(interpreter, py_version, abi, platforms): + yield tag + for tag in _independent_tags(interpreter, py_version, platforms): + yield tag diff --git a/src/pip/_vendor/pep517/__init__.py b/src/pip/_vendor/pep517/__init__.py index 9c1a098f78c..38d8e63ca1d 100644 --- a/src/pip/_vendor/pep517/__init__.py +++ b/src/pip/_vendor/pep517/__init__.py @@ -1,4 +1,4 @@ """Wrappers to build Python packages using PEP 517 hooks """ -__version__ = '0.5.0' +__version__ = '0.7.0' diff --git a/src/pip/_vendor/pep517/_in_process.py b/src/pip/_vendor/pep517/_in_process.py index d6524b660a8..1589a6cac58 100644 --- a/src/pip/_vendor/pep517/_in_process.py +++ b/src/pip/_vendor/pep517/_in_process.py @@ -2,7 +2,9 @@ It expects: - Command line args: hook_name, control_dir -- Environment variable: PEP517_BUILD_BACKEND=entry.point:spec +- Environment variables: + PEP517_BUILD_BACKEND=entry.point:spec + PEP517_BACKEND_PATH=paths (separated with os.pathsep) - control_dir/input.json: - {"kwargs": {...}} @@ -13,10 +15,12 @@ from glob import glob from importlib import import_module import os +import os.path from os.path import join as pjoin import re import shutil import sys +import traceback # This is run as a script, not a module, so it can't do a relative import import compat @@ -24,16 +28,49 @@ class BackendUnavailable(Exception): """Raised if we cannot import the backend""" + def __init__(self, traceback): + self.traceback = traceback + + +class BackendInvalid(Exception): + """Raised if the backend is invalid""" + def __init__(self, message): + self.message = message + + +class HookMissing(Exception): + """Raised if a hook is missing and we are not executing the fallback""" + + +def contained_in(filename, directory): + """Test if a file is located within the given directory.""" + filename = os.path.normcase(os.path.abspath(filename)) + directory = os.path.normcase(os.path.abspath(directory)) + return os.path.commonprefix([filename, directory]) == directory def _build_backend(): """Find and load the build backend""" + # Add in-tree backend directories to the front of sys.path. + backend_path = os.environ.get('PEP517_BACKEND_PATH') + if backend_path: + extra_pathitems = backend_path.split(os.pathsep) + sys.path[:0] = extra_pathitems + ep = os.environ['PEP517_BUILD_BACKEND'] mod_path, _, obj_path = ep.partition(':') try: obj = import_module(mod_path) except ImportError: - raise BackendUnavailable + raise BackendUnavailable(traceback.format_exc()) + + if backend_path: + if not any( + contained_in(obj.__file__, path) + for path in extra_pathitems + ): + raise BackendInvalid("Backend was not loaded from backend-path") + if obj_path: for path_part in obj_path.split('.'): obj = getattr(obj, path_part) @@ -54,15 +91,19 @@ def get_requires_for_build_wheel(config_settings): return hook(config_settings) -def prepare_metadata_for_build_wheel(metadata_directory, config_settings): +def prepare_metadata_for_build_wheel( + metadata_directory, config_settings, _allow_fallback): """Invoke optional prepare_metadata_for_build_wheel - Implements a fallback by building a wheel if the hook isn't defined. + Implements a fallback by building a wheel if the hook isn't defined, + unless _allow_fallback is False in which case HookMissing is raised. """ backend = _build_backend() try: hook = backend.prepare_metadata_for_build_wheel except AttributeError: + if not _allow_fallback: + raise HookMissing() return _get_wheel_metadata_from_wheel(backend, metadata_directory, config_settings) else: @@ -161,6 +202,8 @@ class _DummyException(Exception): class GotUnsupportedOperation(Exception): """For internal use when backend raises UnsupportedOperation""" + def __init__(self, traceback): + self.traceback = traceback def build_sdist(sdist_directory, config_settings): @@ -169,7 +212,7 @@ def build_sdist(sdist_directory, config_settings): try: return backend.build_sdist(sdist_directory, config_settings) except getattr(backend, 'UnsupportedOperation', _DummyException): - raise GotUnsupportedOperation + raise GotUnsupportedOperation(traceback.format_exc()) HOOK_NAMES = { @@ -195,10 +238,17 @@ def main(): json_out = {'unsupported': False, 'return_val': None} try: json_out['return_val'] = hook(**hook_input['kwargs']) - except BackendUnavailable: + except BackendUnavailable as e: json_out['no_backend'] = True - except GotUnsupportedOperation: + json_out['traceback'] = e.traceback + except BackendInvalid as e: + json_out['backend_invalid'] = True + json_out['backend_error'] = e.message + except GotUnsupportedOperation as e: json_out['unsupported'] = True + json_out['traceback'] = e.traceback + except HookMissing: + json_out['hook_missing'] = True compat.write_json(json_out, pjoin(control_dir, 'output.json'), indent=2) diff --git a/src/pip/_vendor/pep517/build.py b/src/pip/_vendor/pep517/build.py index ac6c9495caa..7618c78c19b 100644 --- a/src/pip/_vendor/pep517/build.py +++ b/src/pip/_vendor/pep517/build.py @@ -3,25 +3,56 @@ import argparse import logging import os -import contextlib -from pip._vendor import pytoml +import toml import shutil -import errno -import tempfile from .envbuild import BuildEnvironment from .wrappers import Pep517HookCaller +from .dirtools import tempdir, mkdir_p +from .compat import FileNotFoundError log = logging.getLogger(__name__) -@contextlib.contextmanager -def tempdir(): - td = tempfile.mkdtemp() +def validate_system(system): + """ + Ensure build system has the requisite fields. + """ + required = {'requires', 'build-backend'} + if not (required <= set(system)): + message = "Missing required fields: {missing}".format( + missing=required-set(system), + ) + raise ValueError(message) + + +def load_system(source_dir): + """ + Load the build system from a source dir (pyproject.toml). + """ + pyproject = os.path.join(source_dir, 'pyproject.toml') + with open(pyproject) as f: + pyproject_data = toml.load(f) + return pyproject_data['build-system'] + + +def compat_system(source_dir): + """ + Given a source dir, attempt to get a build system backend + and requirements from pyproject.toml. Fallback to + setuptools but only if the file was not found or a build + system was not indicated. + """ try: - yield td - finally: - shutil.rmtree(td) + system = load_system(source_dir) + except (FileNotFoundError, KeyError): + system = {} + system.setdefault( + 'build-backend', + 'setuptools.build_meta:__legacy__', + ) + system.setdefault('requires', ['setuptools', 'wheel']) + return system def _do_build(hooks, env, dist, dest): @@ -42,33 +73,18 @@ def _do_build(hooks, env, dist, dest): shutil.move(source, os.path.join(dest, os.path.basename(filename))) -def mkdir_p(*args, **kwargs): - """Like `mkdir`, but does not raise an exception if the - directory already exists. - """ - try: - return os.mkdir(*args, **kwargs) - except OSError as exc: - if exc.errno != errno.EEXIST: - raise - - -def build(source_dir, dist, dest=None): - pyproject = os.path.join(source_dir, 'pyproject.toml') +def build(source_dir, dist, dest=None, system=None): + system = system or load_system(source_dir) dest = os.path.join(source_dir, dest or 'dist') mkdir_p(dest) - with open(pyproject) as f: - pyproject_data = pytoml.load(f) - # Ensure the mandatory data can be loaded - buildsys = pyproject_data['build-system'] - requires = buildsys['requires'] - backend = buildsys['build-backend'] - - hooks = Pep517HookCaller(source_dir, backend) + validate_system(system) + hooks = Pep517HookCaller( + source_dir, system['build-backend'], system.get('backend-path') + ) with BuildEnvironment() as env: - env.pip_install(requires) + env.pip_install(system['requires']) _do_build(hooks, env, dist, dest) diff --git a/src/pip/_vendor/pep517/check.py b/src/pip/_vendor/pep517/check.py index f4cdc6bec97..9e0c0682096 100644 --- a/src/pip/_vendor/pep517/check.py +++ b/src/pip/_vendor/pep517/check.py @@ -4,7 +4,7 @@ import logging import os from os.path import isfile, join as pjoin -from pip._vendor.pytoml import TomlError, load as toml_load +from toml import TomlDecodeError, load as toml_load import shutil from subprocess import CalledProcessError import sys @@ -147,12 +147,13 @@ def check(source_dir): buildsys = pyproject_data['build-system'] requires = buildsys['requires'] backend = buildsys['build-backend'] + backend_path = buildsys.get('backend-path') log.info('Loaded pyproject.toml') - except (TomlError, KeyError): + except (TomlDecodeError, KeyError): log.error("Invalid pyproject.toml", exc_info=True) return False - hooks = Pep517HookCaller(source_dir, backend) + hooks = Pep517HookCaller(source_dir, backend, backend_path) sdist_ok = check_build_sdist(hooks, requires) wheel_ok = check_build_wheel(hooks, requires) diff --git a/src/pip/_vendor/pep517/compat.py b/src/pip/_vendor/pep517/compat.py index 01c66fc7e41..8432acb7324 100644 --- a/src/pip/_vendor/pep517/compat.py +++ b/src/pip/_vendor/pep517/compat.py @@ -1,7 +1,10 @@ -"""Handle reading and writing JSON in UTF-8, on Python 3 and 2.""" +"""Python 2/3 compatibility""" import json import sys + +# Handle reading and writing JSON in UTF-8, on Python 3 and 2. + if sys.version_info[0] >= 3: # Python 3 def write_json(obj, path, **kwargs): @@ -21,3 +24,11 @@ def write_json(obj, path, **kwargs): def read_json(path): with open(path, 'rb') as f: return json.load(f) + + +# FileNotFoundError + +try: + FileNotFoundError = FileNotFoundError +except NameError: + FileNotFoundError = IOError diff --git a/src/pip/_vendor/pep517/dirtools.py b/src/pip/_vendor/pep517/dirtools.py new file mode 100644 index 00000000000..58c6ca0c56b --- /dev/null +++ b/src/pip/_vendor/pep517/dirtools.py @@ -0,0 +1,44 @@ +import os +import io +import contextlib +import tempfile +import shutil +import errno +import zipfile + + +@contextlib.contextmanager +def tempdir(): + """Create a temporary directory in a context manager.""" + td = tempfile.mkdtemp() + try: + yield td + finally: + shutil.rmtree(td) + + +def mkdir_p(*args, **kwargs): + """Like `mkdir`, but does not raise an exception if the + directory already exists. + """ + try: + return os.mkdir(*args, **kwargs) + except OSError as exc: + if exc.errno != errno.EEXIST: + raise + + +def dir_to_zipfile(root): + """Construct an in-memory zip file for a directory.""" + buffer = io.BytesIO() + zip_file = zipfile.ZipFile(buffer, 'w') + for root, dirs, files in os.walk(root): + for path in dirs: + fs_path = os.path.join(root, path) + rel_path = os.path.relpath(fs_path, root) + zip_file.writestr(rel_path + '/', '') + for path in files: + fs_path = os.path.join(root, path) + rel_path = os.path.relpath(fs_path, root) + zip_file.write(fs_path, rel_path) + return zip_file diff --git a/src/pip/_vendor/pep517/envbuild.py b/src/pip/_vendor/pep517/envbuild.py index f7ac5f46f7c..cacd2b12c01 100644 --- a/src/pip/_vendor/pep517/envbuild.py +++ b/src/pip/_vendor/pep517/envbuild.py @@ -3,23 +3,27 @@ import os import logging -from pip._vendor import pytoml +import toml import shutil from subprocess import check_call import sys from sysconfig import get_paths from tempfile import mkdtemp -from .wrappers import Pep517HookCaller +from .wrappers import Pep517HookCaller, LoggerWrapper log = logging.getLogger(__name__) def _load_pyproject(source_dir): with open(os.path.join(source_dir, 'pyproject.toml')) as f: - pyproject_data = pytoml.load(f) + pyproject_data = toml.load(f) buildsys = pyproject_data['build-system'] - return buildsys['requires'], buildsys['build-backend'] + return ( + buildsys['requires'], + buildsys['build-backend'], + buildsys.get('backend-path'), + ) class BuildEnvironment(object): @@ -90,9 +94,14 @@ def pip_install(self, reqs): if not reqs: return log.info('Calling pip to install %s', reqs) - check_call([ + cmd = [ sys.executable, '-m', 'pip', 'install', '--ignore-installed', - '--prefix', self.path] + list(reqs)) + '--prefix', self.path] + list(reqs) + check_call( + cmd, + stdout=LoggerWrapper(log, logging.INFO), + stderr=LoggerWrapper(log, logging.ERROR), + ) def __exit__(self, exc_type, exc_val, exc_tb): needs_cleanup = ( @@ -126,8 +135,8 @@ def build_wheel(source_dir, wheel_dir, config_settings=None): """ if config_settings is None: config_settings = {} - requires, backend = _load_pyproject(source_dir) - hooks = Pep517HookCaller(source_dir, backend) + requires, backend, backend_path = _load_pyproject(source_dir) + hooks = Pep517HookCaller(source_dir, backend, backend_path) with BuildEnvironment() as env: env.pip_install(requires) @@ -148,8 +157,8 @@ def build_sdist(source_dir, sdist_dir, config_settings=None): """ if config_settings is None: config_settings = {} - requires, backend = _load_pyproject(source_dir) - hooks = Pep517HookCaller(source_dir, backend) + requires, backend, backend_path = _load_pyproject(source_dir) + hooks = Pep517HookCaller(source_dir, backend, backend_path) with BuildEnvironment() as env: env.pip_install(requires) diff --git a/src/pip/_vendor/pep517/meta.py b/src/pip/_vendor/pep517/meta.py new file mode 100644 index 00000000000..d525de5c6c8 --- /dev/null +++ b/src/pip/_vendor/pep517/meta.py @@ -0,0 +1,92 @@ +"""Build metadata for a project using PEP 517 hooks. +""" +import argparse +import logging +import os +import shutil +import functools + +try: + import importlib.metadata as imp_meta +except ImportError: + import importlib_metadata as imp_meta + +try: + from zipfile import Path +except ImportError: + from zipp import Path + +from .envbuild import BuildEnvironment +from .wrappers import Pep517HookCaller, quiet_subprocess_runner +from .dirtools import tempdir, mkdir_p, dir_to_zipfile +from .build import validate_system, load_system, compat_system + +log = logging.getLogger(__name__) + + +def _prep_meta(hooks, env, dest): + reqs = hooks.get_requires_for_build_wheel({}) + log.info('Got build requires: %s', reqs) + + env.pip_install(reqs) + log.info('Installed dynamic build dependencies') + + with tempdir() as td: + log.info('Trying to build metadata in %s', td) + filename = hooks.prepare_metadata_for_build_wheel(td, {}) + source = os.path.join(td, filename) + shutil.move(source, os.path.join(dest, os.path.basename(filename))) + + +def build(source_dir='.', dest=None, system=None): + system = system or load_system(source_dir) + dest = os.path.join(source_dir, dest or 'dist') + mkdir_p(dest) + validate_system(system) + hooks = Pep517HookCaller( + source_dir, system['build-backend'], system.get('backend-path') + ) + + with hooks.subprocess_runner(quiet_subprocess_runner): + with BuildEnvironment() as env: + env.pip_install(system['requires']) + _prep_meta(hooks, env, dest) + + +def build_as_zip(builder=build): + with tempdir() as out_dir: + builder(dest=out_dir) + return dir_to_zipfile(out_dir) + + +def load(root): + """ + Given a source directory (root) of a package, + return an importlib.metadata.Distribution object + with metadata build from that package. + """ + root = os.path.expanduser(root) + system = compat_system(root) + builder = functools.partial(build, source_dir=root, system=system) + path = Path(build_as_zip(builder)) + return imp_meta.PathDistribution(path) + + +parser = argparse.ArgumentParser() +parser.add_argument( + 'source_dir', + help="A directory containing pyproject.toml", +) +parser.add_argument( + '--out-dir', '-o', + help="Destination in which to save the builds relative to source dir", +) + + +def main(): + args = parser.parse_args() + build(args.source_dir, args.out_dir) + + +if __name__ == '__main__': + main() diff --git a/src/pip/_vendor/pep517/wrappers.py b/src/pip/_vendor/pep517/wrappers.py index b14b899150a..ad9a4f8c32f 100644 --- a/src/pip/_vendor/pep517/wrappers.py +++ b/src/pip/_vendor/pep517/wrappers.py @@ -1,8 +1,9 @@ +import threading from contextlib import contextmanager import os from os.path import dirname, abspath, join as pjoin import shutil -from subprocess import check_call +from subprocess import check_call, check_output, STDOUT import sys from tempfile import mkdtemp @@ -22,10 +23,29 @@ def tempdir(): class BackendUnavailable(Exception): """Will be raised if the backend cannot be imported in the hook process.""" + def __init__(self, traceback): + self.traceback = traceback + + +class BackendInvalid(Exception): + """Will be raised if the backend is invalid.""" + def __init__(self, backend_name, backend_path, message): + self.backend_name = backend_name + self.backend_path = backend_path + self.message = message + + +class HookMissing(Exception): + """Will be raised on missing hooks.""" + def __init__(self, hook_name): + super(HookMissing, self).__init__(hook_name) + self.hook_name = hook_name class UnsupportedOperation(Exception): """May be raised by build_sdist if the backend indicates that it can't.""" + def __init__(self, traceback): + self.traceback = traceback def default_subprocess_runner(cmd, cwd=None, extra_environ=None): @@ -37,21 +57,82 @@ def default_subprocess_runner(cmd, cwd=None, extra_environ=None): check_call(cmd, cwd=cwd, env=env) +def quiet_subprocess_runner(cmd, cwd=None, extra_environ=None): + """A method of calling the wrapper subprocess while suppressing output.""" + env = os.environ.copy() + if extra_environ: + env.update(extra_environ) + + check_output(cmd, cwd=cwd, env=env, stderr=STDOUT) + + +def norm_and_check(source_tree, requested): + """Normalise and check a backend path. + + Ensure that the requested backend path is specified as a relative path, + and resolves to a location under the given source tree. + + Return an absolute version of the requested path. + """ + if os.path.isabs(requested): + raise ValueError("paths must be relative") + + abs_source = os.path.abspath(source_tree) + abs_requested = os.path.normpath(os.path.join(abs_source, requested)) + # We have to use commonprefix for Python 2.7 compatibility. So we + # normalise case to avoid problems because commonprefix is a character + # based comparison :-( + norm_source = os.path.normcase(abs_source) + norm_requested = os.path.normcase(abs_requested) + if os.path.commonprefix([norm_source, norm_requested]) != norm_source: + raise ValueError("paths must be inside source tree") + + return abs_requested + + class Pep517HookCaller(object): """A wrapper around a source directory to be built with a PEP 517 backend. source_dir : The path to the source directory, containing pyproject.toml. - backend : The build backend spec, as per PEP 517, from pyproject.toml. + build_backend : The build backend spec, as per PEP 517, from + pyproject.toml. + backend_path : The backend path, as per PEP 517, from pyproject.toml. + runner : A callable that invokes the wrapper subprocess. + + The 'runner', if provided, must expect the following: + cmd : a list of strings representing the command and arguments to + execute, as would be passed to e.g. 'subprocess.check_call'. + cwd : a string representing the working directory that must be + used for the subprocess. Corresponds to the provided source_dir. + extra_environ : a dict mapping environment variable names to values + which must be set for the subprocess execution. """ - def __init__(self, source_dir, build_backend): + def __init__( + self, + source_dir, + build_backend, + backend_path=None, + runner=None, + ): + if runner is None: + runner = default_subprocess_runner + self.source_dir = abspath(source_dir) self.build_backend = build_backend - self._subprocess_runner = default_subprocess_runner + if backend_path: + backend_path = [ + norm_and_check(self.source_dir, p) for p in backend_path + ] + self.backend_path = backend_path + self._subprocess_runner = runner # TODO: Is this over-engineered? Maybe frontends only need to # set this when creating the wrapper, not on every call. @contextmanager def subprocess_runner(self, runner): + """A context manager for temporarily overriding the default subprocess + runner. + """ prev = self._subprocess_runner self._subprocess_runner = runner yield @@ -72,18 +153,21 @@ def get_requires_for_build_wheel(self, config_settings=None): }) def prepare_metadata_for_build_wheel( - self, metadata_directory, config_settings=None): + self, metadata_directory, config_settings=None, + _allow_fallback=True): """Prepare a *.dist-info folder with metadata for this project. Returns the name of the newly created folder. If the build backend defines a hook with this name, it will be called in a subprocess. If not, the backend will be asked to build a wheel, - and the dist-info extracted from that. + and the dist-info extracted from that (unless _allow_fallback is + False). """ return self._call_hook('prepare_metadata_for_build_wheel', { 'metadata_directory': abspath(metadata_directory), 'config_settings': config_settings, + '_allow_fallback': _allow_fallback, }) def build_wheel( @@ -139,25 +223,76 @@ def _call_hook(self, hook_name, kwargs): # letters, digits and _, . and : characters, and will be used as a # Python identifier, so non-ASCII content is wrong on Python 2 in # any case). + # For backend_path, we use sys.getfilesystemencoding. if sys.version_info[0] == 2: build_backend = self.build_backend.encode('ASCII') else: build_backend = self.build_backend + extra_environ = {'PEP517_BUILD_BACKEND': build_backend} + + if self.backend_path: + backend_path = os.pathsep.join(self.backend_path) + if sys.version_info[0] == 2: + backend_path = backend_path.encode(sys.getfilesystemencoding()) + extra_environ['PEP517_BACKEND_PATH'] = backend_path with tempdir() as td: - compat.write_json({'kwargs': kwargs}, pjoin(td, 'input.json'), + hook_input = {'kwargs': kwargs} + compat.write_json(hook_input, pjoin(td, 'input.json'), indent=2) # Run the hook in a subprocess self._subprocess_runner( [sys.executable, _in_proc_script, hook_name, td], cwd=self.source_dir, - extra_environ={'PEP517_BUILD_BACKEND': build_backend} + extra_environ=extra_environ ) data = compat.read_json(pjoin(td, 'output.json')) if data.get('unsupported'): - raise UnsupportedOperation + raise UnsupportedOperation(data.get('traceback', '')) if data.get('no_backend'): - raise BackendUnavailable + raise BackendUnavailable(data.get('traceback', '')) + if data.get('backend_invalid'): + raise BackendInvalid( + backend_name=self.build_backend, + backend_path=self.backend_path, + message=data.get('backend_error', '') + ) + if data.get('hook_missing'): + raise HookMissing(hook_name) return data['return_val'] + + +class LoggerWrapper(threading.Thread): + """ + Read messages from a pipe and redirect them + to a logger (see python's logging module). + """ + + def __init__(self, logger, level): + threading.Thread.__init__(self) + self.daemon = True + + self.logger = logger + self.level = level + + # create the pipe and reader + self.fd_read, self.fd_write = os.pipe() + self.reader = os.fdopen(self.fd_read) + + self.start() + + def fileno(self): + return self.fd_write + + @staticmethod + def remove_newline(msg): + return msg[:-1] if msg.endswith(os.linesep) else msg + + def run(self): + for line in self.reader: + self._write(self.remove_newline(line)) + + def _write(self, message): + self.logger.log(self.level, message) diff --git a/src/pip/_vendor/pkg_resources/__init__.py b/src/pip/_vendor/pkg_resources/__init__.py index fdd40de4149..363a6309e55 100644 --- a/src/pip/_vendor/pkg_resources/__init__.py +++ b/src/pip/_vendor/pkg_resources/__init__.py @@ -1416,8 +1416,17 @@ def has_metadata(self, name): def get_metadata(self, name): if not self.egg_info: return "" - value = self._get(self._fn(self.egg_info, name)) - return value.decode('utf-8') if six.PY3 else value + path = self._get_metadata_path(name) + value = self._get(path) + if six.PY2: + return value + try: + return value.decode('utf-8') + except UnicodeDecodeError as exc: + # Include the path in the error message to simplify + # troubleshooting, and without changing the exception type. + exc.reason += ' in {} file at path: {}'.format(name, path) + raise def get_metadata_lines(self, name): return yield_lines(self.get_metadata(name)) diff --git a/src/pip/_vendor/pyparsing.py b/src/pip/_vendor/pyparsing.py index 9d6a01d5f37..dc1594d9dac 100644 --- a/src/pip/_vendor/pyparsing.py +++ b/src/pip/_vendor/pyparsing.py @@ -1,4 +1,4 @@ -#-*- coding: utf-8 -*- +# -*- coding: utf-8 -*- # module pyparsing.py # # Copyright (c) 2003-2019 Paul T. McGuire @@ -87,14 +87,16 @@ more complex ones - associate names with your parsed results using :class:`ParserElement.setResultsName` + - access the parsed data, which is returned as a :class:`ParseResults` + object - find some helpful expression short-cuts like :class:`delimitedList` and :class:`oneOf` - find more useful common expressions in the :class:`pyparsing_common` namespace class """ -__version__ = "2.4.0" -__versionTime__ = "07 Apr 2019 18:28 UTC" +__version__ = "2.4.2" +__versionTime__ = "29 Jul 2019 02:58 UTC" __author__ = "Paul McGuire <ptmcg@users.sourceforge.net>" import string @@ -109,6 +111,9 @@ import traceback import types from datetime import datetime +from operator import itemgetter +import itertools +from functools import wraps try: # Python 3 @@ -124,11 +129,11 @@ try: # Python 3 from collections.abc import Iterable - from collections.abc import MutableMapping + from collections.abc import MutableMapping, Mapping except ImportError: # Python 2.7 from collections import Iterable - from collections import MutableMapping + from collections import MutableMapping, Mapping try: from collections import OrderedDict as _OrderedDict @@ -146,40 +151,63 @@ class SimpleNamespace: pass # version compatibility configuration __compat__ = SimpleNamespace() __compat__.__doc__ = """ - A cross-version compatibility configuration for pyparsing features that will be - released in a future version. By setting values in this configuration to True, - those features can be enabled in prior versions for compatibility development + A cross-version compatibility configuration for pyparsing features that will be + released in a future version. By setting values in this configuration to True, + those features can be enabled in prior versions for compatibility development and testing. - + - collect_all_And_tokens - flag to enable fix for Issue #63 that fixes erroneous grouping - of results names when an And expression is nested within an Or or MatchFirst; set to - True to enable bugfix to be released in pyparsing 2.4 + of results names when an And expression is nested within an Or or MatchFirst; set to + True to enable bugfix released in pyparsing 2.3.0, or False to preserve + pre-2.3.0 handling of named results """ __compat__.collect_all_And_tokens = True - -#~ sys.stderr.write( "testing pyparsing module, version %s, %s\n" % (__version__,__versionTime__ ) ) - -__all__ = [ '__version__', '__versionTime__', '__author__', '__compat__', -'And', 'CaselessKeyword', 'CaselessLiteral', 'CharsNotIn', 'Combine', 'Dict', 'Each', 'Empty', -'FollowedBy', 'Forward', 'GoToColumn', 'Group', 'Keyword', 'LineEnd', 'LineStart', 'Literal', -'PrecededBy', 'MatchFirst', 'NoMatch', 'NotAny', 'OneOrMore', 'OnlyOnce', 'Optional', 'Or', -'ParseBaseException', 'ParseElementEnhance', 'ParseException', 'ParseExpression', 'ParseFatalException', -'ParseResults', 'ParseSyntaxException', 'ParserElement', 'QuotedString', 'RecursiveGrammarException', -'Regex', 'SkipTo', 'StringEnd', 'StringStart', 'Suppress', 'Token', 'TokenConverter', -'White', 'Word', 'WordEnd', 'WordStart', 'ZeroOrMore', 'Char', -'alphanums', 'alphas', 'alphas8bit', 'anyCloseTag', 'anyOpenTag', 'cStyleComment', 'col', -'commaSeparatedList', 'commonHTMLEntity', 'countedArray', 'cppStyleComment', 'dblQuotedString', -'dblSlashComment', 'delimitedList', 'dictOf', 'downcaseTokens', 'empty', 'hexnums', -'htmlComment', 'javaStyleComment', 'line', 'lineEnd', 'lineStart', 'lineno', -'makeHTMLTags', 'makeXMLTags', 'matchOnlyAtCol', 'matchPreviousExpr', 'matchPreviousLiteral', -'nestedExpr', 'nullDebugAction', 'nums', 'oneOf', 'opAssoc', 'operatorPrecedence', 'printables', -'punc8bit', 'pythonStyleComment', 'quotedString', 'removeQuotes', 'replaceHTMLEntity', -'replaceWith', 'restOfLine', 'sglQuotedString', 'srange', 'stringEnd', -'stringStart', 'traceParseAction', 'unicodeString', 'upcaseTokens', 'withAttribute', -'indentedBlock', 'originalTextFor', 'ungroup', 'infixNotation','locatedExpr', 'withClass', -'CloseMatch', 'tokenMap', 'pyparsing_common', 'pyparsing_unicode', 'unicode_set', -] +__diag__ = SimpleNamespace() +__diag__.__doc__ = """ +Diagnostic configuration (all default to False) + - warn_multiple_tokens_in_named_alternation - flag to enable warnings when a results + name is defined on a MatchFirst or Or expression with one or more And subexpressions + (only warns if __compat__.collect_all_And_tokens is False) + - warn_ungrouped_named_tokens_in_collection - flag to enable warnings when a results + name is defined on a containing expression with ungrouped subexpressions that also + have results names + - warn_name_set_on_empty_Forward - flag to enable warnings whan a Forward is defined + with a results name, but has no contents defined + - warn_on_multiple_string_args_to_oneof - flag to enable warnings whan oneOf is + incorrectly called with multiple str arguments + - enable_debug_on_named_expressions - flag to auto-enable debug on all subsequent + calls to ParserElement.setName() +""" +__diag__.warn_multiple_tokens_in_named_alternation = False +__diag__.warn_ungrouped_named_tokens_in_collection = False +__diag__.warn_name_set_on_empty_Forward = False +__diag__.warn_on_multiple_string_args_to_oneof = False +__diag__.enable_debug_on_named_expressions = False + +# ~ sys.stderr.write("testing pyparsing module, version %s, %s\n" % (__version__, __versionTime__)) + +__all__ = ['__version__', '__versionTime__', '__author__', '__compat__', '__diag__', + 'And', 'CaselessKeyword', 'CaselessLiteral', 'CharsNotIn', 'Combine', 'Dict', 'Each', 'Empty', + 'FollowedBy', 'Forward', 'GoToColumn', 'Group', 'Keyword', 'LineEnd', 'LineStart', 'Literal', + 'PrecededBy', 'MatchFirst', 'NoMatch', 'NotAny', 'OneOrMore', 'OnlyOnce', 'Optional', 'Or', + 'ParseBaseException', 'ParseElementEnhance', 'ParseException', 'ParseExpression', 'ParseFatalException', + 'ParseResults', 'ParseSyntaxException', 'ParserElement', 'QuotedString', 'RecursiveGrammarException', + 'Regex', 'SkipTo', 'StringEnd', 'StringStart', 'Suppress', 'Token', 'TokenConverter', + 'White', 'Word', 'WordEnd', 'WordStart', 'ZeroOrMore', 'Char', + 'alphanums', 'alphas', 'alphas8bit', 'anyCloseTag', 'anyOpenTag', 'cStyleComment', 'col', + 'commaSeparatedList', 'commonHTMLEntity', 'countedArray', 'cppStyleComment', 'dblQuotedString', + 'dblSlashComment', 'delimitedList', 'dictOf', 'downcaseTokens', 'empty', 'hexnums', + 'htmlComment', 'javaStyleComment', 'line', 'lineEnd', 'lineStart', 'lineno', + 'makeHTMLTags', 'makeXMLTags', 'matchOnlyAtCol', 'matchPreviousExpr', 'matchPreviousLiteral', + 'nestedExpr', 'nullDebugAction', 'nums', 'oneOf', 'opAssoc', 'operatorPrecedence', 'printables', + 'punc8bit', 'pythonStyleComment', 'quotedString', 'removeQuotes', 'replaceHTMLEntity', + 'replaceWith', 'restOfLine', 'sglQuotedString', 'srange', 'stringEnd', + 'stringStart', 'traceParseAction', 'unicodeString', 'upcaseTokens', 'withAttribute', + 'indentedBlock', 'originalTextFor', 'ungroup', 'infixNotation', 'locatedExpr', 'withClass', + 'CloseMatch', 'tokenMap', 'pyparsing_common', 'pyparsing_unicode', 'unicode_set', + 'conditionAsParseAction', + ] system_version = tuple(sys.version_info)[:3] PY_3 = system_version[0] == 3 @@ -204,7 +232,7 @@ def _ustr(obj): < returns the unicode object | encodes it with the default encoding | ... >. """ - if isinstance(obj,unicode): + if isinstance(obj, unicode): return obj try: @@ -222,9 +250,10 @@ def _ustr(obj): # build list of single arg builtins, tolerant of Python version, that can be used as parse actions singleArgBuiltins = [] import __builtin__ + for fname in "sum len sorted reversed list tuple set any all min max".split(): try: - singleArgBuiltins.append(getattr(__builtin__,fname)) + singleArgBuiltins.append(getattr(__builtin__, fname)) except AttributeError: continue @@ -235,23 +264,36 @@ def _xml_escape(data): # ampersand must be replaced first from_symbols = '&><"\'' - to_symbols = ('&'+s+';' for s in "amp gt lt quot apos".split()) - for from_,to_ in zip(from_symbols, to_symbols): + to_symbols = ('&' + s + ';' for s in "amp gt lt quot apos".split()) + for from_, to_ in zip(from_symbols, to_symbols): data = data.replace(from_, to_) return data -alphas = string.ascii_uppercase + string.ascii_lowercase -nums = "0123456789" -hexnums = nums + "ABCDEFabcdef" -alphanums = alphas + nums -_bslash = chr(92) +alphas = string.ascii_uppercase + string.ascii_lowercase +nums = "0123456789" +hexnums = nums + "ABCDEFabcdef" +alphanums = alphas + nums +_bslash = chr(92) printables = "".join(c for c in string.printable if c not in string.whitespace) + +def conditionAsParseAction(fn, message=None, fatal=False): + msg = message if message is not None else "failed user-defined condition" + exc_type = ParseFatalException if fatal else ParseException + fn = _trim_arity(fn) + + @wraps(fn) + def pa(s, l, t): + if not bool(fn(s, l, t)): + raise exc_type(s, l, msg) + + return pa + class ParseBaseException(Exception): """base exception class for all parsing runtime exceptions""" # Performance tuning: we construct a *lot* of these, so keep this # constructor as small and fast as possible - def __init__( self, pstr, loc=0, msg=None, elem=None ): + def __init__(self, pstr, loc=0, msg=None, elem=None): self.loc = loc if msg is None: self.msg = pstr @@ -270,27 +312,34 @@ def _from_exception(cls, pe): """ return cls(pe.pstr, pe.loc, pe.msg, pe.parserElement) - def __getattr__( self, aname ): + def __getattr__(self, aname): """supported attributes by name are: - lineno - returns the line number of the exception text - col - returns the column number of the exception text - line - returns the line containing the exception text """ - if( aname == "lineno" ): - return lineno( self.loc, self.pstr ) - elif( aname in ("col", "column") ): - return col( self.loc, self.pstr ) - elif( aname == "line" ): - return line( self.loc, self.pstr ) + if aname == "lineno": + return lineno(self.loc, self.pstr) + elif aname in ("col", "column"): + return col(self.loc, self.pstr) + elif aname == "line": + return line(self.loc, self.pstr) else: raise AttributeError(aname) - def __str__( self ): - return "%s (at char %d), (line:%d, col:%d)" % \ - ( self.msg, self.loc, self.lineno, self.column ) - def __repr__( self ): + def __str__(self): + if self.pstr: + if self.loc >= len(self.pstr): + foundstr = ', found end of text' + else: + foundstr = (', found %r' % self.pstr[self.loc:self.loc + 1]).replace(r'\\', '\\') + else: + foundstr = '' + return ("%s%s (at char %d), (line:%d, col:%d)" % + (self.msg, foundstr, self.loc, self.lineno, self.column)) + def __repr__(self): return _ustr(self) - def markInputline( self, markerString = ">!<" ): + def markInputline(self, markerString=">!<"): """Extracts the exception line from the input string, and marks the location of the exception with a special symbol. """ @@ -426,21 +475,21 @@ class RecursiveGrammarException(Exception): """exception thrown by :class:`ParserElement.validate` if the grammar could be improperly recursive """ - def __init__( self, parseElementList ): + def __init__(self, parseElementList): self.parseElementTrace = parseElementList - def __str__( self ): + def __str__(self): return "RecursiveGrammarException: %s" % self.parseElementTrace class _ParseResultsWithOffset(object): - def __init__(self,p1,p2): - self.tup = (p1,p2) - def __getitem__(self,i): + def __init__(self, p1, p2): + self.tup = (p1, p2) + def __getitem__(self, i): return self.tup[i] def __repr__(self): return repr(self.tup[0]) - def setOffset(self,i): - self.tup = (self.tup[0],i) + def setOffset(self, i): + self.tup = (self.tup[0], i) class ParseResults(object): """Structured parse results, to provide multiple means of access to @@ -485,7 +534,7 @@ def test(s, fn=repr): - month: 12 - year: 1999 """ - def __new__(cls, toklist=None, name=None, asList=True, modal=True ): + def __new__(cls, toklist=None, name=None, asList=True, modal=True): if isinstance(toklist, cls): return toklist retobj = object.__new__(cls) @@ -494,7 +543,7 @@ def __new__(cls, toklist=None, name=None, asList=True, modal=True ): # Performance tuning: we construct a *lot* of these, so keep this # constructor as small and fast as possible - def __init__( self, toklist=None, name=None, asList=True, modal=True, isinstance=isinstance ): + def __init__(self, toklist=None, name=None, asList=True, modal=True, isinstance=isinstance): if self.__doinit: self.__doinit = False self.__name = None @@ -515,85 +564,93 @@ def __init__( self, toklist=None, name=None, asList=True, modal=True, isinstance if name is not None and name: if not modal: self.__accumNames[name] = 0 - if isinstance(name,int): - name = _ustr(name) # will always return a str, but use _ustr for consistency + if isinstance(name, int): + name = _ustr(name) # will always return a str, but use _ustr for consistency self.__name = name - if not (isinstance(toklist, (type(None), basestring, list)) and toklist in (None,'',[])): - if isinstance(toklist,basestring): - toklist = [ toklist ] + if not (isinstance(toklist, (type(None), basestring, list)) and toklist in (None, '', [])): + if isinstance(toklist, basestring): + toklist = [toklist] if asList: - if isinstance(toklist,ParseResults): + if isinstance(toklist, ParseResults): self[name] = _ParseResultsWithOffset(ParseResults(toklist.__toklist), 0) else: - self[name] = _ParseResultsWithOffset(ParseResults(toklist[0]),0) + self[name] = _ParseResultsWithOffset(ParseResults(toklist[0]), 0) self[name].__name = name else: try: self[name] = toklist[0] - except (KeyError,TypeError,IndexError): + except (KeyError, TypeError, IndexError): self[name] = toklist - def __getitem__( self, i ): - if isinstance( i, (int,slice) ): + def __getitem__(self, i): + if isinstance(i, (int, slice)): return self.__toklist[i] else: if i not in self.__accumNames: return self.__tokdict[i][-1][0] else: - return ParseResults([ v[0] for v in self.__tokdict[i] ]) + return ParseResults([v[0] for v in self.__tokdict[i]]) - def __setitem__( self, k, v, isinstance=isinstance ): - if isinstance(v,_ParseResultsWithOffset): - self.__tokdict[k] = self.__tokdict.get(k,list()) + [v] + def __setitem__(self, k, v, isinstance=isinstance): + if isinstance(v, _ParseResultsWithOffset): + self.__tokdict[k] = self.__tokdict.get(k, list()) + [v] sub = v[0] - elif isinstance(k,(int,slice)): + elif isinstance(k, (int, slice)): self.__toklist[k] = v sub = v else: - self.__tokdict[k] = self.__tokdict.get(k,list()) + [_ParseResultsWithOffset(v,0)] + self.__tokdict[k] = self.__tokdict.get(k, list()) + [_ParseResultsWithOffset(v, 0)] sub = v - if isinstance(sub,ParseResults): + if isinstance(sub, ParseResults): sub.__parent = wkref(self) - def __delitem__( self, i ): - if isinstance(i,(int,slice)): - mylen = len( self.__toklist ) + def __delitem__(self, i): + if isinstance(i, (int, slice)): + mylen = len(self.__toklist) del self.__toklist[i] # convert int to slice if isinstance(i, int): if i < 0: i += mylen - i = slice(i, i+1) + i = slice(i, i + 1) # get removed indices removed = list(range(*i.indices(mylen))) removed.reverse() # fixup indices in token dictionary - for name,occurrences in self.__tokdict.items(): + for name, occurrences in self.__tokdict.items(): for j in removed: for k, (value, position) in enumerate(occurrences): occurrences[k] = _ParseResultsWithOffset(value, position - (position > j)) else: del self.__tokdict[i] - def __contains__( self, k ): + def __contains__(self, k): return k in self.__tokdict - def __len__( self ): return len( self.__toklist ) - def __bool__(self): return ( not not self.__toklist ) + def __len__(self): + return len(self.__toklist) + + def __bool__(self): + return (not not self.__toklist) __nonzero__ = __bool__ - def __iter__( self ): return iter( self.__toklist ) - def __reversed__( self ): return iter( self.__toklist[::-1] ) - def _iterkeys( self ): + + def __iter__(self): + return iter(self.__toklist) + + def __reversed__(self): + return iter(self.__toklist[::-1]) + + def _iterkeys(self): if hasattr(self.__tokdict, "iterkeys"): return self.__tokdict.iterkeys() else: return iter(self.__tokdict) - def _itervalues( self ): + def _itervalues(self): return (self[k] for k in self._iterkeys()) - def _iteritems( self ): + def _iteritems(self): return ((k, self[k]) for k in self._iterkeys()) if PY_3: @@ -616,24 +673,24 @@ def _iteritems( self ): iteritems = _iteritems """Returns an iterator of all named result key-value tuples (Python 2.x only).""" - def keys( self ): + def keys(self): """Returns all named result keys (as a list in Python 2.x, as an iterator in Python 3.x).""" return list(self.iterkeys()) - def values( self ): + def values(self): """Returns all named result values (as a list in Python 2.x, as an iterator in Python 3.x).""" return list(self.itervalues()) - def items( self ): + def items(self): """Returns all named result key-values (as a list of tuples in Python 2.x, as an iterator in Python 3.x).""" return list(self.iteritems()) - def haskeys( self ): + def haskeys(self): """Since keys() returns an iterator, this method is helpful in bypassing code that looks for the existence of any defined results names.""" return bool(self.__tokdict) - def pop( self, *args, **kwargs): + def pop(self, *args, **kwargs): """ Removes and returns item at specified index (default= ``last``). Supports both ``list`` and ``dict`` semantics for ``pop()``. If @@ -672,14 +729,14 @@ def remove_LABEL(tokens): """ if not args: args = [-1] - for k,v in kwargs.items(): + for k, v in kwargs.items(): if k == 'default': args = (args[0], v) else: raise TypeError("pop() got an unexpected keyword argument '%s'" % k) - if (isinstance(args[0], int) or - len(args) == 1 or - args[0] in self): + if (isinstance(args[0], int) + or len(args) == 1 + or args[0] in self): index = args[0] ret = self[index] del self[index] @@ -711,7 +768,7 @@ def get(self, key, defaultValue=None): else: return defaultValue - def insert( self, index, insStr ): + def insert(self, index, insStr): """ Inserts new element at location index in the list of parsed tokens. @@ -728,11 +785,11 @@ def insert_locn(locn, tokens): """ self.__toklist.insert(index, insStr) # fixup indices in token dictionary - for name,occurrences in self.__tokdict.items(): + for name, occurrences in self.__tokdict.items(): for k, (value, position) in enumerate(occurrences): occurrences[k] = _ParseResultsWithOffset(value, position + (position > index)) - def append( self, item ): + def append(self, item): """ Add single element to end of ParseResults list of elements. @@ -747,7 +804,7 @@ def append_sum(tokens): """ self.__toklist.append(item) - def extend( self, itemseq ): + def extend(self, itemseq): """ Add sequence of elements to end of ParseResults list of elements. @@ -766,74 +823,66 @@ def make_palindrome(tokens): else: self.__toklist.extend(itemseq) - def clear( self ): + def clear(self): """ Clear all elements and results names. """ del self.__toklist[:] self.__tokdict.clear() - def __getattr__( self, name ): + def __getattr__(self, name): try: return self[name] except KeyError: return "" - if name in self.__tokdict: - if name not in self.__accumNames: - return self.__tokdict[name][-1][0] - else: - return ParseResults([ v[0] for v in self.__tokdict[name] ]) - else: - return "" - - def __add__( self, other ): + def __add__(self, other): ret = self.copy() ret += other return ret - def __iadd__( self, other ): + def __iadd__(self, other): if other.__tokdict: offset = len(self.__toklist) - addoffset = lambda a: offset if a<0 else a+offset + addoffset = lambda a: offset if a < 0 else a + offset otheritems = other.__tokdict.items() - otherdictitems = [(k, _ParseResultsWithOffset(v[0],addoffset(v[1])) ) - for (k,vlist) in otheritems for v in vlist] - for k,v in otherdictitems: + otherdictitems = [(k, _ParseResultsWithOffset(v[0], addoffset(v[1]))) + for k, vlist in otheritems for v in vlist] + for k, v in otherdictitems: self[k] = v - if isinstance(v[0],ParseResults): + if isinstance(v[0], ParseResults): v[0].__parent = wkref(self) self.__toklist += other.__toklist - self.__accumNames.update( other.__accumNames ) + self.__accumNames.update(other.__accumNames) return self def __radd__(self, other): - if isinstance(other,int) and other == 0: + if isinstance(other, int) and other == 0: # useful for merging many ParseResults using sum() builtin return self.copy() else: # this may raise a TypeError - so be it return other + self - def __repr__( self ): - return "(%s, %s)" % ( repr( self.__toklist ), repr( self.__tokdict ) ) + def __repr__(self): + return "(%s, %s)" % (repr(self.__toklist), repr(self.__tokdict)) - def __str__( self ): + def __str__(self): return '[' + ', '.join(_ustr(i) if isinstance(i, ParseResults) else repr(i) for i in self.__toklist) + ']' - def _asStringList( self, sep='' ): + def _asStringList(self, sep=''): out = [] for item in self.__toklist: if out and sep: out.append(sep) - if isinstance( item, ParseResults ): + if isinstance(item, ParseResults): out += item._asStringList() else: - out.append( _ustr(item) ) + out.append(_ustr(item)) return out - def asList( self ): + def asList(self): """ Returns the parse results as a nested list of matching tokens, all converted to strings. @@ -848,9 +897,9 @@ def asList( self ): result_list = result.asList() print(type(result_list), result_list) # -> <class 'list'> ['sldkj', 'lsdkj', 'sldkj'] """ - return [res.asList() if isinstance(res,ParseResults) else res for res in self.__toklist] + return [res.asList() if isinstance(res, ParseResults) else res for res in self.__toklist] - def asDict( self ): + def asDict(self): """ Returns the named parse results as a nested dictionary. @@ -884,27 +933,27 @@ def toItem(obj): else: return obj - return dict((k,toItem(v)) for k,v in item_fn()) + return dict((k, toItem(v)) for k, v in item_fn()) - def copy( self ): + def copy(self): """ Returns a new copy of a :class:`ParseResults` object. """ - ret = ParseResults( self.__toklist ) + ret = ParseResults(self.__toklist) ret.__tokdict = dict(self.__tokdict.items()) ret.__parent = self.__parent - ret.__accumNames.update( self.__accumNames ) + ret.__accumNames.update(self.__accumNames) ret.__name = self.__name return ret - def asXML( self, doctag=None, namedItemsOnly=False, indent="", formatted=True ): + def asXML(self, doctag=None, namedItemsOnly=False, indent="", formatted=True): """ (Deprecated) Returns the parse results as XML. Tags are created for tokens and lists that have defined results names. """ nl = "\n" out = [] - namedItems = dict((v[1],k) for (k,vlist) in self.__tokdict.items() - for v in vlist) + namedItems = dict((v[1], k) for (k, vlist) in self.__tokdict.items() + for v in vlist) nextLevelIndent = indent + " " # collapse out indents if formatting is not desired @@ -926,20 +975,20 @@ def asXML( self, doctag=None, namedItemsOnly=False, indent="", formatted=True ): else: selfTag = "ITEM" - out += [ nl, indent, "<", selfTag, ">" ] + out += [nl, indent, "<", selfTag, ">"] - for i,res in enumerate(self.__toklist): - if isinstance(res,ParseResults): + for i, res in enumerate(self.__toklist): + if isinstance(res, ParseResults): if i in namedItems: - out += [ res.asXML(namedItems[i], - namedItemsOnly and doctag is None, - nextLevelIndent, - formatted)] + out += [res.asXML(namedItems[i], + namedItemsOnly and doctag is None, + nextLevelIndent, + formatted)] else: - out += [ res.asXML(None, - namedItemsOnly and doctag is None, - nextLevelIndent, - formatted)] + out += [res.asXML(None, + namedItemsOnly and doctag is None, + nextLevelIndent, + formatted)] else: # individual token, see if there is a name for it resTag = None @@ -951,16 +1000,16 @@ def asXML( self, doctag=None, namedItemsOnly=False, indent="", formatted=True ): else: resTag = "ITEM" xmlBodyText = _xml_escape(_ustr(res)) - out += [ nl, nextLevelIndent, "<", resTag, ">", - xmlBodyText, - "</", resTag, ">" ] + out += [nl, nextLevelIndent, "<", resTag, ">", + xmlBodyText, + "</", resTag, ">"] - out += [ nl, indent, "</", selfTag, ">" ] + out += [nl, indent, "</", selfTag, ">"] return "".join(out) - def __lookup(self,sub): - for k,vlist in self.__tokdict.items(): - for v,loc in vlist: + def __lookup(self, sub): + for k, vlist in self.__tokdict.items(): + for v, loc in vlist: if sub is v: return k return None @@ -998,14 +1047,14 @@ def getName(self): return par.__lookup(self) else: return None - elif (len(self) == 1 and - len(self.__tokdict) == 1 and - next(iter(self.__tokdict.values()))[0][1] in (0,-1)): + elif (len(self) == 1 + and len(self.__tokdict) == 1 + and next(iter(self.__tokdict.values()))[0][1] in (0, -1)): return next(iter(self.__tokdict.keys())) else: return None - def dump(self, indent='', depth=0, full=True): + def dump(self, indent='', full=True, include_list=True, _depth=0): """ Diagnostic method for listing out the contents of a :class:`ParseResults`. Accepts an optional ``indent`` argument so @@ -1028,28 +1077,45 @@ def dump(self, indent='', depth=0, full=True): """ out = [] NL = '\n' - out.append( indent+_ustr(self.asList()) ) + if include_list: + out.append(indent + _ustr(self.asList())) + else: + out.append('') + if full: if self.haskeys(): - items = sorted((str(k), v) for k,v in self.items()) - for k,v in items: + items = sorted((str(k), v) for k, v in self.items()) + for k, v in items: if out: out.append(NL) - out.append( "%s%s- %s: " % (indent,(' '*depth), k) ) - if isinstance(v,ParseResults): + out.append("%s%s- %s: " % (indent, (' ' * _depth), k)) + if isinstance(v, ParseResults): if v: - out.append( v.dump(indent,depth+1) ) + out.append(v.dump(indent=indent, full=full, include_list=include_list, _depth=_depth + 1)) else: out.append(_ustr(v)) else: out.append(repr(v)) - elif any(isinstance(vv,ParseResults) for vv in self): + elif any(isinstance(vv, ParseResults) for vv in self): v = self - for i,vv in enumerate(v): - if isinstance(vv,ParseResults): - out.append("\n%s%s[%d]:\n%s%s%s" % (indent,(' '*(depth)),i,indent,(' '*(depth+1)),vv.dump(indent,depth+1) )) + for i, vv in enumerate(v): + if isinstance(vv, ParseResults): + out.append("\n%s%s[%d]:\n%s%s%s" % (indent, + (' ' * (_depth)), + i, + indent, + (' ' * (_depth + 1)), + vv.dump(indent=indent, + full=full, + include_list=include_list, + _depth=_depth + 1))) else: - out.append("\n%s%s[%d]:\n%s%s%s" % (indent,(' '*(depth)),i,indent,(' '*(depth+1)),_ustr(vv))) + out.append("\n%s%s[%d]:\n%s%s%s" % (indent, + (' ' * (_depth)), + i, + indent, + (' ' * (_depth + 1)), + _ustr(vv))) return "".join(out) @@ -1082,18 +1148,15 @@ def pprint(self, *args, **kwargs): # add support for pickle protocol def __getstate__(self): - return ( self.__toklist, - ( self.__tokdict.copy(), - self.__parent is not None and self.__parent() or None, - self.__accumNames, - self.__name ) ) + return (self.__toklist, + (self.__tokdict.copy(), + self.__parent is not None and self.__parent() or None, + self.__accumNames, + self.__name)) - def __setstate__(self,state): + def __setstate__(self, state): self.__toklist = state[0] - (self.__tokdict, - par, - inAccumNames, - self.__name) = state[1] + self.__tokdict, par, inAccumNames, self.__name = state[1] self.__accumNames = {} self.__accumNames.update(inAccumNames) if par is not None: @@ -1105,11 +1168,39 @@ def __getnewargs__(self): return self.__toklist, self.__name, self.__asList, self.__modal def __dir__(self): - return (dir(type(self)) + list(self.keys())) + return dir(type(self)) + list(self.keys()) + + @classmethod + def from_dict(cls, other, name=None): + """ + Helper classmethod to construct a ParseResults from a dict, preserving the + name-value relations as results names. If an optional 'name' argument is + given, a nested ParseResults will be returned + """ + def is_iterable(obj): + try: + iter(obj) + except Exception: + return False + else: + if PY_3: + return not isinstance(obj, (str, bytes)) + else: + return not isinstance(obj, basestring) + + ret = cls([]) + for k, v in other.items(): + if isinstance(v, Mapping): + ret += cls.from_dict(v, name=k) + else: + ret += cls([v], name=k, asList=is_iterable(v)) + if name is not None: + ret = cls([ret], name=name) + return ret MutableMapping.register(ParseResults) -def col (loc,strg): +def col (loc, strg): """Returns current column within a string, counting newlines as line separators. The first column is number 1. @@ -1121,9 +1212,9 @@ def col (loc,strg): location, and line and column positions within the parsed string. """ s = strg - return 1 if 0<loc<len(s) and s[loc-1] == '\n' else loc - s.rfind("\n", 0, loc) + return 1 if 0 < loc < len(s) and s[loc-1] == '\n' else loc - s.rfind("\n", 0, loc) -def lineno(loc,strg): +def lineno(loc, strg): """Returns current line number within a string, counting newlines as line separators. The first line is number 1. @@ -1133,26 +1224,26 @@ def lineno(loc,strg): suggested methods to maintain a consistent view of the parsed string, the parse location, and line and column positions within the parsed string. """ - return strg.count("\n",0,loc) + 1 + return strg.count("\n", 0, loc) + 1 -def line( loc, strg ): +def line(loc, strg): """Returns the line of text containing loc within a string, counting newlines as line separators. """ lastCR = strg.rfind("\n", 0, loc) nextCR = strg.find("\n", loc) if nextCR >= 0: - return strg[lastCR+1:nextCR] + return strg[lastCR + 1:nextCR] else: - return strg[lastCR+1:] + return strg[lastCR + 1:] -def _defaultStartDebugAction( instring, loc, expr ): - print (("Match " + _ustr(expr) + " at loc " + _ustr(loc) + "(%d,%d)" % ( lineno(loc,instring), col(loc,instring) ))) +def _defaultStartDebugAction(instring, loc, expr): + print(("Match " + _ustr(expr) + " at loc " + _ustr(loc) + "(%d,%d)" % (lineno(loc, instring), col(loc, instring)))) -def _defaultSuccessDebugAction( instring, startloc, endloc, expr, toks ): - print ("Matched " + _ustr(expr) + " -> " + str(toks.asList())) +def _defaultSuccessDebugAction(instring, startloc, endloc, expr, toks): + print("Matched " + _ustr(expr) + " -> " + str(toks.asList())) -def _defaultExceptionDebugAction( instring, loc, expr, exc ): - print ("Exception raised:" + _ustr(exc)) +def _defaultExceptionDebugAction(instring, loc, expr, exc): + print("Exception raised:" + _ustr(exc)) def nullDebugAction(*args): """'Do-nothing' debug action, to suppress debugging output during parsing.""" @@ -1183,16 +1274,16 @@ def nullDebugAction(*args): 'decorator to trim function calls to match the arity of the target' def _trim_arity(func, maxargs=2): if func in singleArgBuiltins: - return lambda s,l,t: func(t) + return lambda s, l, t: func(t) limit = [0] foundArity = [False] # traceback return data structure changed in Py3.5 - normalize back to plain tuples - if system_version[:2] >= (3,5): + if system_version[:2] >= (3, 5): def extract_stack(limit=0): # special handling for Python 3.5.0 - extra deep call stack by 1 - offset = -3 if system_version == (3,5,0) else -2 - frame_summary = traceback.extract_stack(limit=-offset+limit-1)[offset] + offset = -3 if system_version == (3, 5, 0) else -2 + frame_summary = traceback.extract_stack(limit=-offset + limit - 1)[offset] return [frame_summary[:2]] def extract_tb(tb, limit=0): frames = traceback.extract_tb(tb, limit=limit) @@ -1209,7 +1300,7 @@ def extract_tb(tb, limit=0): # IF ANY CODE CHANGES, EVEN JUST COMMENTS OR BLANK LINES, BETWEEN THE NEXT LINE AND # THE CALL TO FUNC INSIDE WRAPPER, LINE_DIFF MUST BE MODIFIED!!!! this_line = extract_stack(limit=2)[-1] - pa_call_line_synth = (this_line[0], this_line[1]+LINE_DIFF) + pa_call_line_synth = (this_line[0], this_line[1] + LINE_DIFF) def wrapper(*args): while 1: @@ -1227,7 +1318,10 @@ def wrapper(*args): if not extract_tb(tb, limit=2)[-1][:2] == pa_call_line_synth: raise finally: - del tb + try: + del tb + except NameError: + pass if limit[0] <= maxargs: limit[0] += 1 @@ -1245,13 +1339,14 @@ def wrapper(*args): return wrapper + class ParserElement(object): """Abstract base level parser element class.""" DEFAULT_WHITE_CHARS = " \n\t\r" verbose_stacktrace = False @staticmethod - def setDefaultWhitespaceChars( chars ): + def setDefaultWhitespaceChars(chars): r""" Overrides the default whitespace chars @@ -1288,10 +1383,10 @@ def inlineLiteralsUsing(cls): """ ParserElement._literalStringClass = cls - def __init__( self, savelist=False ): + def __init__(self, savelist=False): self.parseAction = list() self.failAction = None - #~ self.name = "<unknown>" # don't define self.name, let subclasses try/except upcall + # ~ self.name = "<unknown>" # don't define self.name, let subclasses try/except upcall self.strRepr = None self.resultsName = None self.saveAsList = savelist @@ -1306,12 +1401,12 @@ def __init__( self, savelist=False ): self.mayIndexError = True # used to optimize exception handling for subclasses that don't advance parse index self.errmsg = "" self.modalResults = True # used to mark results names as modal (report only last) or cumulative (list all) - self.debugActions = ( None, None, None ) #custom debug actions + self.debugActions = (None, None, None) # custom debug actions self.re = None self.callPreparse = True # used to avoid redundant calls to preParse self.callDuringTry = False - def copy( self ): + def copy(self): """ Make a copy of this :class:`ParserElement`. Useful for defining different parse actions for the same parsing pattern, using copies of @@ -1320,8 +1415,8 @@ def copy( self ): Example:: integer = Word(nums).setParseAction(lambda toks: int(toks[0])) - integerK = integer.copy().addParseAction(lambda toks: toks[0]*1024) + Suppress("K") - integerM = integer.copy().addParseAction(lambda toks: toks[0]*1024*1024) + Suppress("M") + integerK = integer.copy().addParseAction(lambda toks: toks[0] * 1024) + Suppress("K") + integerM = integer.copy().addParseAction(lambda toks: toks[0] * 1024 * 1024) + Suppress("M") print(OneOrMore(integerK | integerM | integer).parseString("5K 100 640K 256M")) @@ -1331,16 +1426,16 @@ def copy( self ): Equivalent form of ``expr.copy()`` is just ``expr()``:: - integerM = integer().addParseAction(lambda toks: toks[0]*1024*1024) + Suppress("M") + integerM = integer().addParseAction(lambda toks: toks[0] * 1024 * 1024) + Suppress("M") """ - cpy = copy.copy( self ) + cpy = copy.copy(self) cpy.parseAction = self.parseAction[:] cpy.ignoreExprs = self.ignoreExprs[:] if self.copyDefaultWhiteChars: cpy.whiteChars = ParserElement.DEFAULT_WHITE_CHARS return cpy - def setName( self, name ): + def setName(self, name): """ Define name for this expression, makes debugging and exception messages clearer. @@ -1351,11 +1446,11 @@ def setName( self, name ): """ self.name = name self.errmsg = "Expected " + self.name - if hasattr(self,"exception"): - self.exception.msg = self.errmsg + if __diag__.enable_debug_on_named_expressions: + self.setDebug() return self - def setResultsName( self, name, listAllMatches=False ): + def setResultsName(self, name, listAllMatches=False): """ Define name for referencing matching tokens as a nested attribute of the returned parse results. @@ -1376,15 +1471,18 @@ def setResultsName( self, name, listAllMatches=False ): # equivalent form: date_str = integer("year") + '/' + integer("month") + '/' + integer("day") """ + return self._setResultsName(name, listAllMatches) + + def _setResultsName(self, name, listAllMatches=False): newself = self.copy() if name.endswith("*"): name = name[:-1] - listAllMatches=True + listAllMatches = True newself.resultsName = name newself.modalResults = not listAllMatches return newself - def setBreak(self,breakFlag = True): + def setBreak(self, breakFlag=True): """Method to invoke the Python pdb debugger when this element is about to be parsed. Set ``breakFlag`` to True to enable, False to disable. @@ -1393,20 +1491,21 @@ def setBreak(self,breakFlag = True): _parseMethod = self._parse def breaker(instring, loc, doActions=True, callPreParse=True): import pdb + # this call to pdb.set_trace() is intentional, not a checkin error pdb.set_trace() - return _parseMethod( instring, loc, doActions, callPreParse ) + return _parseMethod(instring, loc, doActions, callPreParse) breaker._originalParseMethod = _parseMethod self._parse = breaker else: - if hasattr(self._parse,"_originalParseMethod"): + if hasattr(self._parse, "_originalParseMethod"): self._parse = self._parse._originalParseMethod return self - def setParseAction( self, *fns, **kwargs ): + def setParseAction(self, *fns, **kwargs): """ Define one or more actions to perform when successfully matching parse element definition. - Parse action fn is a callable method with 0-3 arguments, called as ``fn(s,loc,toks)`` , - ``fn(loc,toks)`` , ``fn(toks)`` , or just ``fn()`` , where: + Parse action fn is a callable method with 0-3 arguments, called as ``fn(s, loc, toks)`` , + ``fn(loc, toks)`` , ``fn(toks)`` , or just ``fn()`` , where: - s = the original string being parsed (see note below) - loc = the location of the matching substring @@ -1416,8 +1515,11 @@ def setParseAction( self, *fns, **kwargs ): value from fn, and the modified list of tokens will replace the original. Otherwise, fn does not need to return any value. + If None is passed as the parse action, all previously added parse actions for this + expression are cleared. + Optional keyword arguments: - - callDuringTry = (default= ``False`` ) indicate if parse action should be run during lookaheads and alternate testing + - callDuringTry = (default= ``False``) indicate if parse action should be run during lookaheads and alternate testing Note: the default parsing behavior is to expand tabs in the input string before starting the parsing process. See :class:`parseString for more @@ -1439,11 +1541,16 @@ def setParseAction( self, *fns, **kwargs ): # note that integer fields are now ints, not strings date_str.parseString("1999/12/31") # -> [1999, '/', 12, '/', 31] """ - self.parseAction = list(map(_trim_arity, list(fns))) - self.callDuringTry = kwargs.get("callDuringTry", False) + if list(fns) == [None,]: + self.parseAction = [] + else: + if not all(callable(fn) for fn in fns): + raise TypeError("parse actions must be callable") + self.parseAction = list(map(_trim_arity, list(fns))) + self.callDuringTry = kwargs.get("callDuringTry", False) return self - def addParseAction( self, *fns, **kwargs ): + def addParseAction(self, *fns, **kwargs): """ Add one or more parse actions to expression's list of parse actions. See :class:`setParseAction`. @@ -1471,21 +1578,17 @@ def addCondition(self, *fns, **kwargs): result = date_str.parseString("1999/12/31") # -> Exception: Only support years 2000 and later (at char 0), (line:1, col:1) """ - msg = kwargs.get("message", "failed user-defined condition") - exc_type = ParseFatalException if kwargs.get("fatal", False) else ParseException for fn in fns: - fn = _trim_arity(fn) - def pa(s,l,t): - if not bool(fn(s,l,t)): - raise exc_type(s,l,msg) - self.parseAction.append(pa) + self.parseAction.append(conditionAsParseAction(fn, message=kwargs.get('message'), + fatal=kwargs.get('fatal', False))) + self.callDuringTry = self.callDuringTry or kwargs.get("callDuringTry", False) return self - def setFailAction( self, fn ): + def setFailAction(self, fn): """Define action to perform if parsing fails at this expression. Fail acton fn is a callable function that takes the arguments - ``fn(s,loc,expr,err)`` where: + ``fn(s, loc, expr, err)`` where: - s = string being parsed - loc = location where expression match was attempted and failed - expr = the parse expression that failed @@ -1495,22 +1598,22 @@ def setFailAction( self, fn ): self.failAction = fn return self - def _skipIgnorables( self, instring, loc ): + def _skipIgnorables(self, instring, loc): exprsFound = True while exprsFound: exprsFound = False for e in self.ignoreExprs: try: while 1: - loc,dummy = e._parse( instring, loc ) + loc, dummy = e._parse(instring, loc) exprsFound = True except ParseException: pass return loc - def preParse( self, instring, loc ): + def preParse(self, instring, loc): if self.ignoreExprs: - loc = self._skipIgnorables( instring, loc ) + loc = self._skipIgnorables(instring, loc) if self.skipWhitespace: wt = self.whiteChars @@ -1520,101 +1623,105 @@ def preParse( self, instring, loc ): return loc - def parseImpl( self, instring, loc, doActions=True ): + def parseImpl(self, instring, loc, doActions=True): return loc, [] - def postParse( self, instring, loc, tokenlist ): + def postParse(self, instring, loc, tokenlist): return tokenlist - #~ @profile - def _parseNoCache( self, instring, loc, doActions=True, callPreParse=True ): - debugging = ( self.debug ) #and doActions ) + # ~ @profile + def _parseNoCache(self, instring, loc, doActions=True, callPreParse=True): + TRY, MATCH, FAIL = 0, 1, 2 + debugging = (self.debug) # and doActions) if debugging or self.failAction: - #~ print ("Match",self,"at loc",loc,"(%d,%d)" % ( lineno(loc,instring), col(loc,instring) )) - if (self.debugActions[0] ): - self.debugActions[0]( instring, loc, self ) - if callPreParse and self.callPreparse: - preloc = self.preParse( instring, loc ) - else: - preloc = loc - tokensStart = preloc + # ~ print ("Match", self, "at loc", loc, "(%d, %d)" % (lineno(loc, instring), col(loc, instring))) + if self.debugActions[TRY]: + self.debugActions[TRY](instring, loc, self) try: - try: - loc,tokens = self.parseImpl( instring, preloc, doActions ) - except IndexError: - raise ParseException( instring, len(instring), self.errmsg, self ) - except ParseBaseException as err: - #~ print ("Exception raised:", err) - if self.debugActions[2]: - self.debugActions[2]( instring, tokensStart, self, err ) + if callPreParse and self.callPreparse: + preloc = self.preParse(instring, loc) + else: + preloc = loc + tokensStart = preloc + if self.mayIndexError or preloc >= len(instring): + try: + loc, tokens = self.parseImpl(instring, preloc, doActions) + except IndexError: + raise ParseException(instring, len(instring), self.errmsg, self) + else: + loc, tokens = self.parseImpl(instring, preloc, doActions) + except Exception as err: + # ~ print ("Exception raised:", err) + if self.debugActions[FAIL]: + self.debugActions[FAIL](instring, tokensStart, self, err) if self.failAction: - self.failAction( instring, tokensStart, self, err ) + self.failAction(instring, tokensStart, self, err) raise else: if callPreParse and self.callPreparse: - preloc = self.preParse( instring, loc ) + preloc = self.preParse(instring, loc) else: preloc = loc tokensStart = preloc if self.mayIndexError or preloc >= len(instring): try: - loc,tokens = self.parseImpl( instring, preloc, doActions ) + loc, tokens = self.parseImpl(instring, preloc, doActions) except IndexError: - raise ParseException( instring, len(instring), self.errmsg, self ) + raise ParseException(instring, len(instring), self.errmsg, self) else: - loc,tokens = self.parseImpl( instring, preloc, doActions ) + loc, tokens = self.parseImpl(instring, preloc, doActions) - tokens = self.postParse( instring, loc, tokens ) + tokens = self.postParse(instring, loc, tokens) - retTokens = ParseResults( tokens, self.resultsName, asList=self.saveAsList, modal=self.modalResults ) + retTokens = ParseResults(tokens, self.resultsName, asList=self.saveAsList, modal=self.modalResults) if self.parseAction and (doActions or self.callDuringTry): if debugging: try: for fn in self.parseAction: try: - tokens = fn( instring, tokensStart, retTokens ) + tokens = fn(instring, tokensStart, retTokens) except IndexError as parse_action_exc: exc = ParseException("exception raised in parse action") exc.__cause__ = parse_action_exc raise exc if tokens is not None and tokens is not retTokens: - retTokens = ParseResults( tokens, + retTokens = ParseResults(tokens, self.resultsName, - asList=self.saveAsList and isinstance(tokens,(ParseResults,list)), - modal=self.modalResults ) - except ParseBaseException as err: - #~ print "Exception raised in user parse action:", err - if (self.debugActions[2] ): - self.debugActions[2]( instring, tokensStart, self, err ) + asList=self.saveAsList and isinstance(tokens, (ParseResults, list)), + modal=self.modalResults) + except Exception as err: + # ~ print "Exception raised in user parse action:", err + if self.debugActions[FAIL]: + self.debugActions[FAIL](instring, tokensStart, self, err) raise else: for fn in self.parseAction: try: - tokens = fn( instring, tokensStart, retTokens ) + tokens = fn(instring, tokensStart, retTokens) except IndexError as parse_action_exc: exc = ParseException("exception raised in parse action") exc.__cause__ = parse_action_exc raise exc if tokens is not None and tokens is not retTokens: - retTokens = ParseResults( tokens, + retTokens = ParseResults(tokens, self.resultsName, - asList=self.saveAsList and isinstance(tokens,(ParseResults,list)), - modal=self.modalResults ) + asList=self.saveAsList and isinstance(tokens, (ParseResults, list)), + modal=self.modalResults) if debugging: - #~ print ("Matched",self,"->",retTokens.asList()) - if (self.debugActions[1] ): - self.debugActions[1]( instring, tokensStart, loc, self, retTokens ) + # ~ print ("Matched", self, "->", retTokens.asList()) + if self.debugActions[MATCH]: + self.debugActions[MATCH](instring, tokensStart, loc, self, retTokens) return loc, retTokens - def tryParse( self, instring, loc ): + def tryParse(self, instring, loc): try: - return self._parse( instring, loc, doActions=False )[0] + return self._parse(instring, loc, doActions=False)[0] except ParseFatalException: - raise ParseException( instring, loc, self.errmsg, self) + raise ParseException(instring, loc, self.errmsg, self) def canParseNext(self, instring, loc): try: @@ -1711,7 +1818,7 @@ def cache_len(self): # this method gets repeatedly called during backtracking with the same arguments - # we can cache these arguments and save ourselves the trouble of re-parsing the contained expression - def _parseCache( self, instring, loc, doActions=True, callPreParse=True ): + def _parseCache(self, instring, loc, doActions=True, callPreParse=True): HIT, MISS = 0, 1 lookup = (self, instring, loc, callPreParse, doActions) with ParserElement.packrat_cache_lock: @@ -1732,7 +1839,7 @@ def _parseCache( self, instring, loc, doActions=True, callPreParse=True ): ParserElement.packrat_cache_stats[HIT] += 1 if isinstance(value, Exception): raise value - return (value[0], value[1].copy()) + return value[0], value[1].copy() _parse = _parseNoCache @@ -1777,12 +1884,16 @@ def enablePackrat(cache_size_limit=128): ParserElement.packrat_cache = ParserElement._FifoCache(cache_size_limit) ParserElement._parse = ParserElement._parseCache - def parseString( self, instring, parseAll=False ): + def parseString(self, instring, parseAll=False): """ Execute the parse expression with the given string. This is the main interface to the client code, once the complete expression has been built. + Returns the parsed data as a :class:`ParseResults` object, which may be + accessed as a list, or as a dict or object with attributes if the given parser + includes results names. + If you want the grammar to require that the entire input string be successfully parsed, then set ``parseAll`` to True (equivalent to ending the grammar with ``StringEnd()``). @@ -1796,7 +1907,7 @@ def parseString( self, instring, parseAll=False ): - calling ``parseWithTabs`` on your grammar before calling ``parseString`` (see :class:`parseWithTabs`) - - define your parse action using the full ``(s,loc,toks)`` signature, and + - define your parse action using the full ``(s, loc, toks)`` signature, and reference the input string using the parse action's ``s`` argument - explictly expand the tabs in your input string before calling ``parseString`` @@ -1809,17 +1920,17 @@ def parseString( self, instring, parseAll=False ): ParserElement.resetCache() if not self.streamlined: self.streamline() - #~ self.saveAsList = True + # ~ self.saveAsList = True for e in self.ignoreExprs: e.streamline() if not self.keepTabs: instring = instring.expandtabs() try: - loc, tokens = self._parse( instring, 0 ) + loc, tokens = self._parse(instring, 0) if parseAll: - loc = self.preParse( instring, loc ) + loc = self.preParse(instring, loc) se = Empty() + StringEnd() - se._parse( instring, loc ) + se._parse(instring, loc) except ParseBaseException as exc: if ParserElement.verbose_stacktrace: raise @@ -1829,7 +1940,7 @@ def parseString( self, instring, parseAll=False ): else: return tokens - def scanString( self, instring, maxMatches=_MAX_INT, overlap=False ): + def scanString(self, instring, maxMatches=_MAX_INT, overlap=False): """ Scan the input string for expression matches. Each match will return the matching tokens, start location, and end location. May be called with optional @@ -1844,7 +1955,7 @@ def scanString( self, instring, maxMatches=_MAX_INT, overlap=False ): source = "sldjf123lsdjjkf345sldkjf879lkjsfd987" print(source) - for tokens,start,end in Word(alphas).scanString(source): + for tokens, start, end in Word(alphas).scanString(source): print(' '*start + '^'*(end-start)) print(' '*start + tokens[0]) @@ -1876,16 +1987,16 @@ def scanString( self, instring, maxMatches=_MAX_INT, overlap=False ): try: while loc <= instrlen and matches < maxMatches: try: - preloc = preparseFn( instring, loc ) - nextLoc,tokens = parseFn( instring, preloc, callPreParse=False ) + preloc = preparseFn(instring, loc) + nextLoc, tokens = parseFn(instring, preloc, callPreParse=False) except ParseException: - loc = preloc+1 + loc = preloc + 1 else: if nextLoc > loc: matches += 1 yield tokens, preloc, nextLoc if overlap: - nextloc = preparseFn( instring, loc ) + nextloc = preparseFn(instring, loc) if nextloc > loc: loc = nextLoc else: @@ -1893,7 +2004,7 @@ def scanString( self, instring, maxMatches=_MAX_INT, overlap=False ): else: loc = nextLoc else: - loc = preloc+1 + loc = preloc + 1 except ParseBaseException as exc: if ParserElement.verbose_stacktrace: raise @@ -1901,7 +2012,7 @@ def scanString( self, instring, maxMatches=_MAX_INT, overlap=False ): # catch and re-raise exception from here, clears out pyparsing internal stack trace raise exc - def transformString( self, instring ): + def transformString(self, instring): """ Extension to :class:`scanString`, to modify matching text with modified tokens that may be returned from a parse action. To use ``transformString``, define a grammar and @@ -1927,19 +2038,19 @@ def transformString( self, instring ): # keep string locs straight between transformString and scanString self.keepTabs = True try: - for t,s,e in self.scanString( instring ): - out.append( instring[lastE:s] ) + for t, s, e in self.scanString(instring): + out.append(instring[lastE:s]) if t: - if isinstance(t,ParseResults): + if isinstance(t, ParseResults): out += t.asList() - elif isinstance(t,list): + elif isinstance(t, list): out += t else: out.append(t) lastE = e out.append(instring[lastE:]) out = [o for o in out if o] - return "".join(map(_ustr,_flatten(out))) + return "".join(map(_ustr, _flatten(out))) except ParseBaseException as exc: if ParserElement.verbose_stacktrace: raise @@ -1947,7 +2058,7 @@ def transformString( self, instring ): # catch and re-raise exception from here, clears out pyparsing internal stack trace raise exc - def searchString( self, instring, maxMatches=_MAX_INT ): + def searchString(self, instring, maxMatches=_MAX_INT): """ Another extension to :class:`scanString`, simplifying the access to the tokens found to match the given parse expression. May be called with optional @@ -1969,7 +2080,7 @@ def searchString( self, instring, maxMatches=_MAX_INT ): ['More', 'Iron', 'Lead', 'Gold', 'I', 'Electricity'] """ try: - return ParseResults([ t for t,s,e in self.scanString( instring, maxMatches ) ]) + return ParseResults([t for t, s, e in self.scanString(instring, maxMatches)]) except ParseBaseException as exc: if ParserElement.verbose_stacktrace: raise @@ -1995,14 +2106,14 @@ def split(self, instring, maxsplit=_MAX_INT, includeSeparators=False): """ splits = 0 last = 0 - for t,s,e in self.scanString(instring, maxMatches=maxsplit): + for t, s, e in self.scanString(instring, maxMatches=maxsplit): yield instring[last:s] if includeSeparators: yield t[0] last = e yield instring[last:] - def __add__(self, other ): + def __add__(self, other): """ Implementation of + operator - returns :class:`And`. Adding strings to a ParserElement converts them to :class:`Literal`s by default. @@ -2016,24 +2127,42 @@ def __add__(self, other ): prints:: Hello, World! -> ['Hello', ',', 'World', '!'] + + ``...`` may be used as a parse expression as a short form of :class:`SkipTo`. + + Literal('start') + ... + Literal('end') + + is equivalent to: + + Literal('start') + SkipTo('end')("_skipped*") + Literal('end') + + Note that the skipped text is returned with '_skipped' as a results name, + and to support having multiple skips in the same parser, the value returned is + a list of all skipped text. """ - if isinstance( other, basestring ): - other = ParserElement._literalStringClass( other ) - if not isinstance( other, ParserElement ): + if other is Ellipsis: + return _PendingSkip(self) + + if isinstance(other, basestring): + other = self._literalStringClass(other) + if not isinstance(other, ParserElement): warnings.warn("Cannot combine element of type %s with ParserElement" % type(other), - SyntaxWarning, stacklevel=2) + SyntaxWarning, stacklevel=2) return None - return And( [ self, other ] ) + return And([self, other]) - def __radd__(self, other ): + def __radd__(self, other): """ Implementation of + operator when left operand is not a :class:`ParserElement` """ - if isinstance( other, basestring ): - other = ParserElement._literalStringClass( other ) - if not isinstance( other, ParserElement ): + if other is Ellipsis: + return SkipTo(self)("_skipped*") + self + + if isinstance(other, basestring): + other = self._literalStringClass(other) + if not isinstance(other, ParserElement): warnings.warn("Cannot combine element of type %s with ParserElement" % type(other), - SyntaxWarning, stacklevel=2) + SyntaxWarning, stacklevel=2) return None return other + self @@ -2041,64 +2170,70 @@ def __sub__(self, other): """ Implementation of - operator, returns :class:`And` with error stop """ - if isinstance( other, basestring ): - other = ParserElement._literalStringClass( other ) - if not isinstance( other, ParserElement ): + if isinstance(other, basestring): + other = self._literalStringClass(other) + if not isinstance(other, ParserElement): warnings.warn("Cannot combine element of type %s with ParserElement" % type(other), - SyntaxWarning, stacklevel=2) + SyntaxWarning, stacklevel=2) return None return self + And._ErrorStop() + other - def __rsub__(self, other ): + def __rsub__(self, other): """ Implementation of - operator when left operand is not a :class:`ParserElement` """ - if isinstance( other, basestring ): - other = ParserElement._literalStringClass( other ) - if not isinstance( other, ParserElement ): + if isinstance(other, basestring): + other = self._literalStringClass(other) + if not isinstance(other, ParserElement): warnings.warn("Cannot combine element of type %s with ParserElement" % type(other), - SyntaxWarning, stacklevel=2) + SyntaxWarning, stacklevel=2) return None return other - self - def __mul__(self,other): + def __mul__(self, other): """ Implementation of * operator, allows use of ``expr * 3`` in place of ``expr + expr + expr``. Expressions may also me multiplied by a 2-integer - tuple, similar to ``{min,max}`` multipliers in regular expressions. Tuples + tuple, similar to ``{min, max}`` multipliers in regular expressions. Tuples may also include ``None`` as in: - - ``expr*(n,None)`` or ``expr*(n,)`` is equivalent + - ``expr*(n, None)`` or ``expr*(n, )`` is equivalent to ``expr*n + ZeroOrMore(expr)`` (read as "at least n instances of ``expr``") - - ``expr*(None,n)`` is equivalent to ``expr*(0,n)`` + - ``expr*(None, n)`` is equivalent to ``expr*(0, n)`` (read as "0 to n instances of ``expr``") - - ``expr*(None,None)`` is equivalent to ``ZeroOrMore(expr)`` - - ``expr*(1,None)`` is equivalent to ``OneOrMore(expr)`` + - ``expr*(None, None)`` is equivalent to ``ZeroOrMore(expr)`` + - ``expr*(1, None)`` is equivalent to ``OneOrMore(expr)`` - Note that ``expr*(None,n)`` does not raise an exception if + Note that ``expr*(None, n)`` does not raise an exception if more than n exprs exist in the input stream; that is, - ``expr*(None,n)`` does not enforce a maximum number of expr + ``expr*(None, n)`` does not enforce a maximum number of expr occurrences. If this behavior is desired, then write - ``expr*(None,n) + ~expr`` + ``expr*(None, n) + ~expr`` """ - if isinstance(other,int): - minElements, optElements = other,0 - elif isinstance(other,tuple): + if other is Ellipsis: + other = (0, None) + elif isinstance(other, tuple) and other[:1] == (Ellipsis,): + other = ((0, ) + other[1:] + (None,))[:2] + + if isinstance(other, int): + minElements, optElements = other, 0 + elif isinstance(other, tuple): + other = tuple(o if o is not Ellipsis else None for o in other) other = (other + (None, None))[:2] if other[0] is None: other = (0, other[1]) - if isinstance(other[0],int) and other[1] is None: + if isinstance(other[0], int) and other[1] is None: if other[0] == 0: return ZeroOrMore(self) if other[0] == 1: return OneOrMore(self) else: - return self*other[0] + ZeroOrMore(self) - elif isinstance(other[0],int) and isinstance(other[1],int): + return self * other[0] + ZeroOrMore(self) + elif isinstance(other[0], int) and isinstance(other[1], int): minElements, optElements = other optElements -= minElements else: - raise TypeError("cannot multiply 'ParserElement' and ('%s','%s') objects", type(other[0]),type(other[1])) + raise TypeError("cannot multiply 'ParserElement' and ('%s', '%s') objects", type(other[0]), type(other[1])) else: raise TypeError("cannot multiply 'ParserElement' and '%s' objects", type(other)) @@ -2107,108 +2242,152 @@ def __mul__(self,other): if optElements < 0: raise ValueError("second tuple value must be greater or equal to first tuple value") if minElements == optElements == 0: - raise ValueError("cannot multiply ParserElement by 0 or (0,0)") + raise ValueError("cannot multiply ParserElement by 0 or (0, 0)") - if (optElements): + if optElements: def makeOptionalList(n): - if n>1: - return Optional(self + makeOptionalList(n-1)) + if n > 1: + return Optional(self + makeOptionalList(n - 1)) else: return Optional(self) if minElements: if minElements == 1: ret = self + makeOptionalList(optElements) else: - ret = And([self]*minElements) + makeOptionalList(optElements) + ret = And([self] * minElements) + makeOptionalList(optElements) else: ret = makeOptionalList(optElements) else: if minElements == 1: ret = self else: - ret = And([self]*minElements) + ret = And([self] * minElements) return ret def __rmul__(self, other): return self.__mul__(other) - def __or__(self, other ): + def __or__(self, other): """ Implementation of | operator - returns :class:`MatchFirst` """ - if isinstance( other, basestring ): - other = ParserElement._literalStringClass( other ) - if not isinstance( other, ParserElement ): + if other is Ellipsis: + return _PendingSkip(self, must_skip=True) + + if isinstance(other, basestring): + other = self._literalStringClass(other) + if not isinstance(other, ParserElement): warnings.warn("Cannot combine element of type %s with ParserElement" % type(other), - SyntaxWarning, stacklevel=2) + SyntaxWarning, stacklevel=2) return None - return MatchFirst( [ self, other ] ) + return MatchFirst([self, other]) - def __ror__(self, other ): + def __ror__(self, other): """ Implementation of | operator when left operand is not a :class:`ParserElement` """ - if isinstance( other, basestring ): - other = ParserElement._literalStringClass( other ) - if not isinstance( other, ParserElement ): + if isinstance(other, basestring): + other = self._literalStringClass(other) + if not isinstance(other, ParserElement): warnings.warn("Cannot combine element of type %s with ParserElement" % type(other), - SyntaxWarning, stacklevel=2) + SyntaxWarning, stacklevel=2) return None return other | self - def __xor__(self, other ): + def __xor__(self, other): """ Implementation of ^ operator - returns :class:`Or` """ - if isinstance( other, basestring ): - other = ParserElement._literalStringClass( other ) - if not isinstance( other, ParserElement ): + if isinstance(other, basestring): + other = self._literalStringClass(other) + if not isinstance(other, ParserElement): warnings.warn("Cannot combine element of type %s with ParserElement" % type(other), - SyntaxWarning, stacklevel=2) + SyntaxWarning, stacklevel=2) return None - return Or( [ self, other ] ) + return Or([self, other]) - def __rxor__(self, other ): + def __rxor__(self, other): """ Implementation of ^ operator when left operand is not a :class:`ParserElement` """ - if isinstance( other, basestring ): - other = ParserElement._literalStringClass( other ) - if not isinstance( other, ParserElement ): + if isinstance(other, basestring): + other = self._literalStringClass(other) + if not isinstance(other, ParserElement): warnings.warn("Cannot combine element of type %s with ParserElement" % type(other), - SyntaxWarning, stacklevel=2) + SyntaxWarning, stacklevel=2) return None return other ^ self - def __and__(self, other ): + def __and__(self, other): """ Implementation of & operator - returns :class:`Each` """ - if isinstance( other, basestring ): - other = ParserElement._literalStringClass( other ) - if not isinstance( other, ParserElement ): + if isinstance(other, basestring): + other = self._literalStringClass(other) + if not isinstance(other, ParserElement): warnings.warn("Cannot combine element of type %s with ParserElement" % type(other), - SyntaxWarning, stacklevel=2) + SyntaxWarning, stacklevel=2) return None - return Each( [ self, other ] ) + return Each([self, other]) - def __rand__(self, other ): + def __rand__(self, other): """ Implementation of & operator when left operand is not a :class:`ParserElement` """ - if isinstance( other, basestring ): - other = ParserElement._literalStringClass( other ) - if not isinstance( other, ParserElement ): + if isinstance(other, basestring): + other = self._literalStringClass(other) + if not isinstance(other, ParserElement): warnings.warn("Cannot combine element of type %s with ParserElement" % type(other), - SyntaxWarning, stacklevel=2) + SyntaxWarning, stacklevel=2) return None return other & self - def __invert__( self ): + def __invert__(self): """ Implementation of ~ operator - returns :class:`NotAny` """ - return NotAny( self ) + return NotAny(self) + + def __iter__(self): + # must implement __iter__ to override legacy use of sequential access to __getitem__ to + # iterate over a sequence + raise TypeError('%r object is not iterable' % self.__class__.__name__) + + def __getitem__(self, key): + """ + use ``[]`` indexing notation as a short form for expression repetition: + - ``expr[n]`` is equivalent to ``expr*n`` + - ``expr[m, n]`` is equivalent to ``expr*(m, n)`` + - ``expr[n, ...]`` or ``expr[n,]`` is equivalent + to ``expr*n + ZeroOrMore(expr)`` + (read as "at least n instances of ``expr``") + - ``expr[..., n]`` is equivalent to ``expr*(0, n)`` + (read as "0 to n instances of ``expr``") + - ``expr[...]`` and ``expr[0, ...]`` are equivalent to ``ZeroOrMore(expr)`` + - ``expr[1, ...]`` is equivalent to ``OneOrMore(expr)`` + ``None`` may be used in place of ``...``. + + Note that ``expr[..., n]`` and ``expr[m, n]``do not raise an exception + if more than ``n`` ``expr``s exist in the input stream. If this behavior is + desired, then write ``expr[..., n] + ~expr``. + """ + + # convert single arg keys to tuples + try: + if isinstance(key, str): + key = (key,) + iter(key) + except TypeError: + key = (key, key) + + if len(key) > 2: + warnings.warn("only 1 or 2 index arguments supported ({0}{1})".format(key[:5], + '... [{0}]'.format(len(key)) + if len(key) > 5 else '')) + + # clip to 2 elements + ret = self * tuple(key[:2]) + return ret def __call__(self, name=None): """ @@ -2222,22 +2401,22 @@ def __call__(self, name=None): Example:: # these are equivalent - userdata = Word(alphas).setResultsName("name") + Word(nums+"-").setResultsName("socsecno") - userdata = Word(alphas)("name") + Word(nums+"-")("socsecno") + userdata = Word(alphas).setResultsName("name") + Word(nums + "-").setResultsName("socsecno") + userdata = Word(alphas)("name") + Word(nums + "-")("socsecno") """ if name is not None: - return self.setResultsName(name) + return self._setResultsName(name) else: return self.copy() - def suppress( self ): + def suppress(self): """ Suppresses the output of this :class:`ParserElement`; useful to keep punctuation from cluttering up returned output. """ - return Suppress( self ) + return Suppress(self) - def leaveWhitespace( self ): + def leaveWhitespace(self): """ Disables the skipping of whitespace before matching the characters in the :class:`ParserElement`'s defined pattern. This is normally only used internally by @@ -2246,7 +2425,7 @@ def leaveWhitespace( self ): self.skipWhitespace = False return self - def setWhitespaceChars( self, chars ): + def setWhitespaceChars(self, chars): """ Overrides the default whitespace chars """ @@ -2255,7 +2434,7 @@ def setWhitespaceChars( self, chars ): self.copyDefaultWhiteChars = False return self - def parseWithTabs( self ): + def parseWithTabs(self): """ Overrides default behavior to expand ``<TAB>``s to spaces before parsing the input string. Must be called before ``parseString`` when the input grammar contains elements that @@ -2264,7 +2443,7 @@ def parseWithTabs( self ): self.keepTabs = True return self - def ignore( self, other ): + def ignore(self, other): """ Define expression to be ignored (e.g., comments) while doing pattern matching; may be called repeatedly, to define multiple comment or other @@ -2281,14 +2460,14 @@ def ignore( self, other ): if isinstance(other, basestring): other = Suppress(other) - if isinstance( other, Suppress ): + if isinstance(other, Suppress): if other not in self.ignoreExprs: self.ignoreExprs.append(other) else: - self.ignoreExprs.append( Suppress( other.copy() ) ) + self.ignoreExprs.append(Suppress(other.copy())) return self - def setDebugActions( self, startAction, successAction, exceptionAction ): + def setDebugActions(self, startAction, successAction, exceptionAction): """ Enable display of debugging messages while doing pattern matching. """ @@ -2298,7 +2477,7 @@ def setDebugActions( self, startAction, successAction, exceptionAction ): self.debug = True return self - def setDebug( self, flag=True ): + def setDebug(self, flag=True): """ Enable display of debugging messages while doing pattern matching. Set ``flag`` to True to enable, False to disable. @@ -2336,32 +2515,32 @@ def setDebug( self, flag=True ): name created for the :class:`Word` expression without calling ``setName`` is ``"W:(ABCD...)"``. """ if flag: - self.setDebugActions( _defaultStartDebugAction, _defaultSuccessDebugAction, _defaultExceptionDebugAction ) + self.setDebugActions(_defaultStartDebugAction, _defaultSuccessDebugAction, _defaultExceptionDebugAction) else: self.debug = False return self - def __str__( self ): + def __str__(self): return self.name - def __repr__( self ): + def __repr__(self): return _ustr(self) - def streamline( self ): + def streamline(self): self.streamlined = True self.strRepr = None return self - def checkRecursion( self, parseElementList ): + def checkRecursion(self, parseElementList): pass - def validate( self, validateTrace=[] ): + def validate(self, validateTrace=None): """ Check defined expressions for valid structure, check for infinite recursive definitions. """ - self.checkRecursion( [] ) + self.checkRecursion([]) - def parseFile( self, file_or_filename, parseAll=False ): + def parseFile(self, file_or_filename, parseAll=False): """ Execute the parse expression on the given file or filename. If a filename is specified (instead of a file object), @@ -2381,24 +2560,27 @@ def parseFile( self, file_or_filename, parseAll=False ): # catch and re-raise exception from here, clears out pyparsing internal stack trace raise exc - def __eq__(self,other): + def __eq__(self, other): if isinstance(other, ParserElement): - return self is other or vars(self) == vars(other) + if PY_3: + self is other or super(ParserElement, self).__eq__(other) + else: + return self is other or vars(self) == vars(other) elif isinstance(other, basestring): return self.matches(other) else: - return super(ParserElement,self)==other + return super(ParserElement, self) == other - def __ne__(self,other): + def __ne__(self, other): return not (self == other) def __hash__(self): - return hash(id(self)) + return id(self) - def __req__(self,other): + def __req__(self, other): return self == other - def __rne__(self,other): + def __rne__(self, other): return not (self == other) def matches(self, testString, parseAll=True): @@ -2422,7 +2604,8 @@ def matches(self, testString, parseAll=True): return False def runTests(self, tests, parseAll=True, comment='#', - fullDump=True, printResults=True, failureTests=False, postParse=None): + fullDump=True, printResults=True, failureTests=False, postParse=None, + file=None): """ Execute the parse expression on a series of test strings, showing each test, the parsed results or where the parse failed. Quick and easy way to @@ -2439,6 +2622,8 @@ def runTests(self, tests, parseAll=True, comment='#', - failureTests - (default= ``False``) indicates if these tests are expected to fail parsing - postParse - (default= ``None``) optional callback for successful parse results; called as `fn(test_string, parse_results)` and returns a string to be added to the test output + - file - (default=``None``) optional file-like object to which test output will be written; + if None, will default to ``sys.stdout`` Returns: a (success, results) tuple, where success indicates that all tests succeeded (or failed if ``failureTests`` is True), and the results contain a list of lines of each @@ -2518,9 +2703,15 @@ def runTests(self, tests, parseAll=True, comment='#', tests = list(map(str.strip, tests.rstrip().splitlines())) if isinstance(comment, basestring): comment = Literal(comment) + if file is None: + file = sys.stdout + print_ = file.write + allResults = [] comments = [] success = True + NL = Literal(r'\n').addParseAction(replaceWith('\n')).ignore(quotedString) + BOM = u'\ufeff' for t in tests: if comment is not None and comment.matches(t, False) or comments and not t: comments.append(t) @@ -2531,26 +2722,15 @@ def runTests(self, tests, parseAll=True, comment='#', comments = [] try: # convert newline marks to actual newlines, and strip leading BOM if present - NL = Literal(r'\n').addParseAction(replaceWith('\n')).ignore(quotedString) - BOM = '\ufeff' t = NL.transformString(t.lstrip(BOM)) result = self.parseString(t, parseAll=parseAll) - out.append(result.dump(full=fullDump)) - success = success and not failureTests - if postParse is not None: - try: - pp_value = postParse(t, result) - if pp_value is not None: - out.append(str(pp_value)) - except Exception as e: - out.append("{0} failed: {1}: {2}".format(postParse.__name__, type(e).__name__, e)) except ParseBaseException as pe: fatal = "(FATAL)" if isinstance(pe, ParseFatalException) else "" if '\n' in t: out.append(line(pe.loc, t)) - out.append(' '*(col(pe.loc,t)-1) + '^' + fatal) + out.append(' ' * (col(pe.loc, t) - 1) + '^' + fatal) else: - out.append(' '*pe.loc + '^' + fatal) + out.append(' ' * pe.loc + '^' + fatal) out.append("FAIL: " + str(pe)) success = success and failureTests result = pe @@ -2558,30 +2738,80 @@ def runTests(self, tests, parseAll=True, comment='#', out.append("FAIL-EXCEPTION: " + str(exc)) success = success and failureTests result = exc + else: + success = success and not failureTests + if postParse is not None: + try: + pp_value = postParse(t, result) + if pp_value is not None: + if isinstance(pp_value, ParseResults): + out.append(pp_value.dump()) + else: + out.append(str(pp_value)) + else: + out.append(result.dump()) + except Exception as e: + out.append(result.dump(full=fullDump)) + out.append("{0} failed: {1}: {2}".format(postParse.__name__, type(e).__name__, e)) + else: + out.append(result.dump(full=fullDump)) if printResults: if fullDump: out.append('') - print('\n'.join(out)) + print_('\n'.join(out)) allResults.append((t, result)) return success, allResults +class _PendingSkip(ParserElement): + # internal placeholder class to hold a place were '...' is added to a parser element, + # once another ParserElement is added, this placeholder will be replaced with a SkipTo + def __init__(self, expr, must_skip=False): + super(_PendingSkip, self).__init__() + self.strRepr = str(expr + Empty()).replace('Empty', '...') + self.name = self.strRepr + self.anchor = expr + self.must_skip = must_skip + + def __add__(self, other): + skipper = SkipTo(other).setName("...")("_skipped*") + if self.must_skip: + def must_skip(t): + if not t._skipped or t._skipped.asList() == ['']: + del t[0] + t.pop("_skipped", None) + def show_skip(t): + if t._skipped.asList()[-1:] == ['']: + skipped = t.pop('_skipped') + t['_skipped'] = 'missing <' + repr(self.anchor) + '>' + return (self.anchor + skipper().addParseAction(must_skip) + | skipper().addParseAction(show_skip)) + other + + return self.anchor + skipper + other + + def __repr__(self): + return self.strRepr + + def parseImpl(self, *args): + raise Exception("use of `...` expression without following SkipTo target expression") + + class Token(ParserElement): """Abstract :class:`ParserElement` subclass, for defining atomic matching patterns. """ - def __init__( self ): - super(Token,self).__init__( savelist=False ) + def __init__(self): + super(Token, self).__init__(savelist=False) class Empty(Token): """An empty token, will always match. """ - def __init__( self ): - super(Empty,self).__init__() + def __init__(self): + super(Empty, self).__init__() self.name = "Empty" self.mayReturnEmpty = True self.mayIndexError = False @@ -2590,14 +2820,14 @@ def __init__( self ): class NoMatch(Token): """A token that will never match. """ - def __init__( self ): - super(NoMatch,self).__init__() + def __init__(self): + super(NoMatch, self).__init__() self.name = "NoMatch" self.mayReturnEmpty = True self.mayIndexError = False self.errmsg = "Unmatchable token" - def parseImpl( self, instring, loc, doActions=True ): + def parseImpl(self, instring, loc, doActions=True): raise ParseException(instring, loc, self.errmsg, self) @@ -2615,8 +2845,8 @@ class Literal(Token): For keyword matching (force word break before and after the matched string), use :class:`Keyword` or :class:`CaselessKeyword`. """ - def __init__( self, matchString ): - super(Literal,self).__init__() + def __init__(self, matchString): + super(Literal, self).__init__() self.match = matchString self.matchLen = len(matchString) try: @@ -2630,15 +2860,22 @@ def __init__( self, matchString ): self.mayReturnEmpty = False self.mayIndexError = False - # Performance tuning: this routine gets called a *lot* - # if this is a single character match string and the first character matches, - # short-circuit as quickly as possible, and avoid calling startswith - #~ @profile - def parseImpl( self, instring, loc, doActions=True ): - if (instring[loc] == self.firstMatchChar and - (self.matchLen==1 or instring.startswith(self.match,loc)) ): - return loc+self.matchLen, self.match + # Performance tuning: modify __class__ to select + # a parseImpl optimized for single-character check + if self.matchLen == 1 and type(self) is Literal: + self.__class__ = _SingleCharLiteral + + def parseImpl(self, instring, loc, doActions=True): + if instring[loc] == self.firstMatchChar and instring.startswith(self.match, loc): + return loc + self.matchLen, self.match + raise ParseException(instring, loc, self.errmsg, self) + +class _SingleCharLiteral(Literal): + def parseImpl(self, instring, loc, doActions=True): + if instring[loc] == self.firstMatchChar: + return loc + 1, self.match raise ParseException(instring, loc, self.errmsg, self) + _L = Literal ParserElement._literalStringClass = Literal @@ -2667,10 +2904,10 @@ class Keyword(Token): For case-insensitive matching, use :class:`CaselessKeyword`. """ - DEFAULT_KEYWORD_CHARS = alphanums+"_$" + DEFAULT_KEYWORD_CHARS = alphanums + "_$" - def __init__( self, matchString, identChars=None, caseless=False ): - super(Keyword,self).__init__() + def __init__(self, matchString, identChars=None, caseless=False): + super(Keyword, self).__init__() if identChars is None: identChars = Keyword.DEFAULT_KEYWORD_CHARS self.match = matchString @@ -2679,7 +2916,7 @@ def __init__( self, matchString, identChars=None, caseless=False ): self.firstMatchChar = matchString[0] except IndexError: warnings.warn("null string passed to Keyword; use Empty() instead", - SyntaxWarning, stacklevel=2) + SyntaxWarning, stacklevel=2) self.name = '"%s"' % self.match self.errmsg = "Expected " + self.name self.mayReturnEmpty = False @@ -2690,27 +2927,32 @@ def __init__( self, matchString, identChars=None, caseless=False ): identChars = identChars.upper() self.identChars = set(identChars) - def parseImpl( self, instring, loc, doActions=True ): + def parseImpl(self, instring, loc, doActions=True): if self.caseless: - if ( (instring[ loc:loc+self.matchLen ].upper() == self.caselessmatch) and - (loc >= len(instring)-self.matchLen or instring[loc+self.matchLen].upper() not in self.identChars) and - (loc == 0 or instring[loc-1].upper() not in self.identChars) ): - return loc+self.matchLen, self.match + if ((instring[loc:loc + self.matchLen].upper() == self.caselessmatch) + and (loc >= len(instring) - self.matchLen + or instring[loc + self.matchLen].upper() not in self.identChars) + and (loc == 0 + or instring[loc - 1].upper() not in self.identChars)): + return loc + self.matchLen, self.match + else: - if (instring[loc] == self.firstMatchChar and - (self.matchLen==1 or instring.startswith(self.match,loc)) and - (loc >= len(instring)-self.matchLen or instring[loc+self.matchLen] not in self.identChars) and - (loc == 0 or instring[loc-1] not in self.identChars) ): - return loc+self.matchLen, self.match + if instring[loc] == self.firstMatchChar: + if ((self.matchLen == 1 or instring.startswith(self.match, loc)) + and (loc >= len(instring) - self.matchLen + or instring[loc + self.matchLen] not in self.identChars) + and (loc == 0 or instring[loc - 1] not in self.identChars)): + return loc + self.matchLen, self.match + raise ParseException(instring, loc, self.errmsg, self) def copy(self): - c = super(Keyword,self).copy() + c = super(Keyword, self).copy() c.identChars = Keyword.DEFAULT_KEYWORD_CHARS return c @staticmethod - def setDefaultKeywordChars( chars ): + def setDefaultKeywordChars(chars): """Overrides the default Keyword chars """ Keyword.DEFAULT_KEYWORD_CHARS = chars @@ -2726,16 +2968,16 @@ class CaselessLiteral(Literal): (Contrast with example for :class:`CaselessKeyword`.) """ - def __init__( self, matchString ): - super(CaselessLiteral,self).__init__( matchString.upper() ) + def __init__(self, matchString): + super(CaselessLiteral, self).__init__(matchString.upper()) # Preserve the defining literal. self.returnString = matchString self.name = "'%s'" % self.returnString self.errmsg = "Expected " + self.name - def parseImpl( self, instring, loc, doActions=True ): - if instring[ loc:loc+self.matchLen ].upper() == self.match: - return loc+self.matchLen, self.returnString + def parseImpl(self, instring, loc, doActions=True): + if instring[loc:loc + self.matchLen].upper() == self.match: + return loc + self.matchLen, self.returnString raise ParseException(instring, loc, self.errmsg, self) class CaselessKeyword(Keyword): @@ -2748,8 +2990,8 @@ class CaselessKeyword(Keyword): (Contrast with example for :class:`CaselessLiteral`.) """ - def __init__( self, matchString, identChars=None ): - super(CaselessKeyword,self).__init__( matchString, identChars, caseless=True ) + def __init__(self, matchString, identChars=None): + super(CaselessKeyword, self).__init__(matchString, identChars, caseless=True) class CloseMatch(Token): """A variation on :class:`Literal` which matches "close" matches, @@ -2785,7 +3027,7 @@ class CloseMatch(Token): patt.parseString("ATCAXCGAAXGGA") # -> (['ATCAXCGAAXGGA'], {'mismatches': [[4, 9]], 'original': ['ATCATCGAATGGA']}) """ def __init__(self, match_string, maxMismatches=1): - super(CloseMatch,self).__init__() + super(CloseMatch, self).__init__() self.name = match_string self.match_string = match_string self.maxMismatches = maxMismatches @@ -2793,7 +3035,7 @@ def __init__(self, match_string, maxMismatches=1): self.mayIndexError = False self.mayReturnEmpty = False - def parseImpl( self, instring, loc, doActions=True ): + def parseImpl(self, instring, loc, doActions=True): start = loc instrlen = len(instring) maxloc = start + len(self.match_string) @@ -2804,8 +3046,8 @@ def parseImpl( self, instring, loc, doActions=True ): mismatches = [] maxMismatches = self.maxMismatches - for match_stringloc,s_m in enumerate(zip(instring[loc:maxloc], self.match_string)): - src,mat = s_m + for match_stringloc, s_m in enumerate(zip(instring[loc:maxloc], match_string)): + src, mat = s_m if src != mat: mismatches.append(match_stringloc) if len(mismatches) > maxMismatches: @@ -2813,7 +3055,7 @@ def parseImpl( self, instring, loc, doActions=True ): else: loc = match_stringloc + 1 results = ParseResults([instring[start:loc]]) - results['original'] = self.match_string + results['original'] = match_string results['mismatches'] = mismatches return loc, results @@ -2865,7 +3107,7 @@ class Word(Token): capital_word = Word(alphas.upper(), alphas.lower()) # hostnames are alphanumeric, with leading alpha, and '-' - hostname = Word(alphas, alphanums+'-') + hostname = Word(alphas, alphanums + '-') # roman numeral (not a strict parser, accepts invalid mix of characters) roman = Word("IVXLCDM") @@ -2873,8 +3115,8 @@ class Word(Token): # any string of non-whitespace characters, except for ',' csv_value = Word(printables, excludeChars=",") """ - def __init__( self, initChars, bodyChars=None, min=1, max=0, exact=0, asKeyword=False, excludeChars=None ): - super(Word,self).__init__() + def __init__(self, initChars, bodyChars=None, min=1, max=0, exact=0, asKeyword=False, excludeChars=None): + super(Word, self).__init__() if excludeChars: excludeChars = set(excludeChars) initChars = ''.join(c for c in initChars if c not in excludeChars) @@ -2882,7 +3124,7 @@ def __init__( self, initChars, bodyChars=None, min=1, max=0, exact=0, asKeyword= bodyChars = ''.join(c for c in bodyChars if c not in excludeChars) self.initCharsOrig = initChars self.initChars = set(initChars) - if bodyChars : + if bodyChars: self.bodyCharsOrig = bodyChars self.bodyChars = set(bodyChars) else: @@ -2910,33 +3152,27 @@ def __init__( self, initChars, bodyChars=None, min=1, max=0, exact=0, asKeyword= self.mayIndexError = False self.asKeyword = asKeyword - if ' ' not in self.initCharsOrig+self.bodyCharsOrig and (min==1 and max==0 and exact==0): + if ' ' not in self.initCharsOrig + self.bodyCharsOrig and (min == 1 and max == 0 and exact == 0): if self.bodyCharsOrig == self.initCharsOrig: self.reString = "[%s]+" % _escapeRegexRangeChars(self.initCharsOrig) elif len(self.initCharsOrig) == 1: - self.reString = "%s[%s]*" % \ - (re.escape(self.initCharsOrig), - _escapeRegexRangeChars(self.bodyCharsOrig),) + self.reString = "%s[%s]*" % (re.escape(self.initCharsOrig), + _escapeRegexRangeChars(self.bodyCharsOrig),) else: - self.reString = "[%s][%s]*" % \ - (_escapeRegexRangeChars(self.initCharsOrig), - _escapeRegexRangeChars(self.bodyCharsOrig),) + self.reString = "[%s][%s]*" % (_escapeRegexRangeChars(self.initCharsOrig), + _escapeRegexRangeChars(self.bodyCharsOrig),) if self.asKeyword: - self.reString = r"\b"+self.reString+r"\b" + self.reString = r"\b" + self.reString + r"\b" + try: - self.re = re.compile( self.reString ) + self.re = re.compile(self.reString) except Exception: self.re = None + else: + self.re_match = self.re.match + self.__class__ = _WordRegex - def parseImpl( self, instring, loc, doActions=True ): - if self.re: - result = self.re.match(instring,loc) - if not result: - raise ParseException(instring, loc, self.errmsg, self) - - loc = result.end() - return loc, result.group() - + def parseImpl(self, instring, loc, doActions=True): if instring[loc] not in self.initChars: raise ParseException(instring, loc, self.errmsg, self) @@ -2945,7 +3181,7 @@ def parseImpl( self, instring, loc, doActions=True ): instrlen = len(instring) bodychars = self.bodyChars maxloc = start + self.maxLen - maxloc = min( maxloc, instrlen ) + maxloc = min(maxloc, instrlen) while loc < maxloc and instring[loc] in bodychars: loc += 1 @@ -2955,7 +3191,8 @@ def parseImpl( self, instring, loc, doActions=True ): elif self.maxSpecified and loc < instrlen and instring[loc] in bodychars: throwException = True elif self.asKeyword: - if (start>0 and instring[start-1] in bodychars) or (loc<instrlen and instring[loc] in bodychars): + if (start > 0 and instring[start - 1] in bodychars + or loc < instrlen and instring[loc] in bodychars): throwException = True if throwException: @@ -2963,38 +3200,49 @@ def parseImpl( self, instring, loc, doActions=True ): return loc, instring[start:loc] - def __str__( self ): + def __str__(self): try: - return super(Word,self).__str__() + return super(Word, self).__str__() except Exception: pass - if self.strRepr is None: def charsAsStr(s): - if len(s)>4: - return s[:4]+"..." + if len(s) > 4: + return s[:4] + "..." else: return s - if ( self.initCharsOrig != self.bodyCharsOrig ): - self.strRepr = "W:(%s,%s)" % ( charsAsStr(self.initCharsOrig), charsAsStr(self.bodyCharsOrig) ) + if self.initCharsOrig != self.bodyCharsOrig: + self.strRepr = "W:(%s, %s)" % (charsAsStr(self.initCharsOrig), charsAsStr(self.bodyCharsOrig)) else: self.strRepr = "W:(%s)" % charsAsStr(self.initCharsOrig) return self.strRepr +class _WordRegex(Word): + def parseImpl(self, instring, loc, doActions=True): + result = self.re_match(instring, loc) + if not result: + raise ParseException(instring, loc, self.errmsg, self) + + loc = result.end() + return loc, result.group() + -class Char(Word): +class Char(_WordRegex): """A short-cut class for defining ``Word(characters, exact=1)``, when defining a match of any single character in a string of characters. """ def __init__(self, charset, asKeyword=False, excludeChars=None): super(Char, self).__init__(charset, exact=1, asKeyword=asKeyword, excludeChars=excludeChars) - self.reString = "[%s]" % _escapeRegexRangeChars(self.initCharsOrig) - self.re = re.compile( self.reString ) + self.reString = "[%s]" % _escapeRegexRangeChars(''.join(self.initChars)) + if asKeyword: + self.reString = r"\b%s\b" % self.reString + self.re = re.compile(self.reString) + self.re_match = self.re.match class Regex(Token): @@ -3012,18 +3260,18 @@ class Regex(Token): roman = Regex(r"M{0,4}(CM|CD|D?{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})") """ compiledREtype = type(re.compile("[A-Z]")) - def __init__( self, pattern, flags=0, asGroupList=False, asMatch=False): + def __init__(self, pattern, flags=0, asGroupList=False, asMatch=False): """The parameters ``pattern`` and ``flags`` are passed to the ``re.compile()`` function as-is. See the Python `re module <https://docs.python.org/3/library/re.html>`_ module for an explanation of the acceptable patterns and flags. """ - super(Regex,self).__init__() + super(Regex, self).__init__() if isinstance(pattern, basestring): if not pattern: warnings.warn("null string passed to Regex; use Empty() instead", - SyntaxWarning, stacklevel=2) + SyntaxWarning, stacklevel=2) self.pattern = pattern self.flags = flags @@ -3033,18 +3281,19 @@ def __init__( self, pattern, flags=0, asGroupList=False, asMatch=False): self.reString = self.pattern except sre_constants.error: warnings.warn("invalid pattern (%s) passed to Regex" % pattern, - SyntaxWarning, stacklevel=2) + SyntaxWarning, stacklevel=2) raise elif isinstance(pattern, Regex.compiledREtype): self.re = pattern - self.pattern = \ - self.reString = str(pattern) + self.pattern = self.reString = str(pattern) self.flags = flags else: raise ValueError("Regex may only be constructed with a string or a compiled RE object") + self.re_match = self.re.match + self.name = _ustr(self) self.errmsg = "Expected " + self.name self.mayIndexError = False @@ -3057,7 +3306,7 @@ def __init__( self, pattern, flags=0, asGroupList=False, asMatch=False): self.parseImpl = self.parseImplAsMatch def parseImpl(self, instring, loc, doActions=True): - result = self.re.match(instring,loc) + result = self.re_match(instring, loc) if not result: raise ParseException(instring, loc, self.errmsg, self) @@ -3070,7 +3319,7 @@ def parseImpl(self, instring, loc, doActions=True): return loc, ret def parseImplAsGroupList(self, instring, loc, doActions=True): - result = self.re.match(instring,loc) + result = self.re_match(instring, loc) if not result: raise ParseException(instring, loc, self.errmsg, self) @@ -3079,7 +3328,7 @@ def parseImplAsGroupList(self, instring, loc, doActions=True): return loc, ret def parseImplAsMatch(self, instring, loc, doActions=True): - result = self.re.match(instring,loc) + result = self.re_match(instring, loc) if not result: raise ParseException(instring, loc, self.errmsg, self) @@ -3087,9 +3336,9 @@ def parseImplAsMatch(self, instring, loc, doActions=True): ret = result return loc, ret - def __str__( self ): + def __str__(self): try: - return super(Regex,self).__str__() + return super(Regex, self).__str__() except Exception: pass @@ -3111,12 +3360,12 @@ def sub(self, repl): """ if self.asGroupList: warnings.warn("cannot use sub() with Regex(asGroupList=True)", - SyntaxWarning, stacklevel=2) + SyntaxWarning, stacklevel=2) raise SyntaxError() if self.asMatch and callable(repl): warnings.warn("cannot use sub() with a callable with Regex(asMatch=True)", - SyntaxWarning, stacklevel=2) + SyntaxWarning, stacklevel=2) raise SyntaxError() if self.asMatch: @@ -3136,20 +3385,20 @@ class QuotedString(Token): - quoteChar - string of one or more characters defining the quote delimiting string - escChar - character to escape quotes, typically backslash - (default= ``None`` ) + (default= ``None``) - escQuote - special quote sequence to escape an embedded quote string (such as SQL's ``""`` to escape an embedded ``"``) - (default= ``None`` ) + (default= ``None``) - multiline - boolean indicating whether quotes can span - multiple lines (default= ``False`` ) + multiple lines (default= ``False``) - unquoteResults - boolean indicating whether the matched text - should be unquoted (default= ``True`` ) + should be unquoted (default= ``True``) - endQuoteChar - string of one or more characters defining the end of the quote delimited string (default= ``None`` => same as quoteChar) - convertWhitespaceEscapes - convert escaped whitespace (``'\t'``, ``'\n'``, etc.) to actual whitespace - (default= ``True`` ) + (default= ``True``) Example:: @@ -3166,13 +3415,14 @@ class QuotedString(Token): [['This is the "quote"']] [['This is the quote with "embedded" quotes']] """ - def __init__( self, quoteChar, escChar=None, escQuote=None, multiline=False, unquoteResults=True, endQuoteChar=None, convertWhitespaceEscapes=True): - super(QuotedString,self).__init__() + def __init__(self, quoteChar, escChar=None, escQuote=None, multiline=False, + unquoteResults=True, endQuoteChar=None, convertWhitespaceEscapes=True): + super(QuotedString, self).__init__() # remove white space from quote chars - wont work anyway quoteChar = quoteChar.strip() if not quoteChar: - warnings.warn("quoteChar cannot be the empty string",SyntaxWarning,stacklevel=2) + warnings.warn("quoteChar cannot be the empty string", SyntaxWarning, stacklevel=2) raise SyntaxError() if endQuoteChar is None: @@ -3180,7 +3430,7 @@ def __init__( self, quoteChar, escChar=None, escQuote=None, multiline=False, unq else: endQuoteChar = endQuoteChar.strip() if not endQuoteChar: - warnings.warn("endQuoteChar cannot be the empty string",SyntaxWarning,stacklevel=2) + warnings.warn("endQuoteChar cannot be the empty string", SyntaxWarning, stacklevel=2) raise SyntaxError() self.quoteChar = quoteChar @@ -3195,35 +3445,34 @@ def __init__( self, quoteChar, escChar=None, escQuote=None, multiline=False, unq if multiline: self.flags = re.MULTILINE | re.DOTALL - self.pattern = r'%s(?:[^%s%s]' % \ - ( re.escape(self.quoteChar), - _escapeRegexRangeChars(self.endQuoteChar[0]), - (escChar is not None and _escapeRegexRangeChars(escChar) or '') ) + self.pattern = r'%s(?:[^%s%s]' % (re.escape(self.quoteChar), + _escapeRegexRangeChars(self.endQuoteChar[0]), + (escChar is not None and _escapeRegexRangeChars(escChar) or '')) else: self.flags = 0 - self.pattern = r'%s(?:[^%s\n\r%s]' % \ - ( re.escape(self.quoteChar), - _escapeRegexRangeChars(self.endQuoteChar[0]), - (escChar is not None and _escapeRegexRangeChars(escChar) or '') ) + self.pattern = r'%s(?:[^%s\n\r%s]' % (re.escape(self.quoteChar), + _escapeRegexRangeChars(self.endQuoteChar[0]), + (escChar is not None and _escapeRegexRangeChars(escChar) or '')) if len(self.endQuoteChar) > 1: self.pattern += ( '|(?:' + ')|(?:'.join("%s[^%s]" % (re.escape(self.endQuoteChar[:i]), - _escapeRegexRangeChars(self.endQuoteChar[i])) - for i in range(len(self.endQuoteChar)-1,0,-1)) + ')' - ) + _escapeRegexRangeChars(self.endQuoteChar[i])) + for i in range(len(self.endQuoteChar) - 1, 0, -1)) + ')') + if escQuote: self.pattern += (r'|(?:%s)' % re.escape(escQuote)) if escChar: self.pattern += (r'|(?:%s.)' % re.escape(escChar)) - self.escCharReplacePattern = re.escape(self.escChar)+"(.)" + self.escCharReplacePattern = re.escape(self.escChar) + "(.)" self.pattern += (r')*%s' % re.escape(self.endQuoteChar)) try: self.re = re.compile(self.pattern, self.flags) self.reString = self.pattern + self.re_match = self.re.match except sre_constants.error: warnings.warn("invalid pattern (%s) passed to Regex" % self.pattern, - SyntaxWarning, stacklevel=2) + SyntaxWarning, stacklevel=2) raise self.name = _ustr(self) @@ -3231,8 +3480,8 @@ def __init__( self, quoteChar, escChar=None, escQuote=None, multiline=False, unq self.mayIndexError = False self.mayReturnEmpty = True - def parseImpl( self, instring, loc, doActions=True ): - result = instring[loc] == self.firstQuoteChar and self.re.match(instring,loc) or None + def parseImpl(self, instring, loc, doActions=True): + result = instring[loc] == self.firstQuoteChar and self.re_match(instring, loc) or None if not result: raise ParseException(instring, loc, self.errmsg, self) @@ -3242,18 +3491,18 @@ def parseImpl( self, instring, loc, doActions=True ): if self.unquoteResults: # strip off quotes - ret = ret[self.quoteCharLen:-self.endQuoteCharLen] + ret = ret[self.quoteCharLen: -self.endQuoteCharLen] - if isinstance(ret,basestring): + if isinstance(ret, basestring): # replace escaped whitespace if '\\' in ret and self.convertWhitespaceEscapes: ws_map = { - r'\t' : '\t', - r'\n' : '\n', - r'\f' : '\f', - r'\r' : '\r', + r'\t': '\t', + r'\n': '\n', + r'\f': '\f', + r'\r': '\r', } - for wslit,wschar in ws_map.items(): + for wslit, wschar in ws_map.items(): ret = ret.replace(wslit, wschar) # replace escaped characters @@ -3266,9 +3515,9 @@ def parseImpl( self, instring, loc, doActions=True ): return loc, ret - def __str__( self ): + def __str__(self): try: - return super(QuotedString,self).__str__() + return super(QuotedString, self).__str__() except Exception: pass @@ -3298,15 +3547,14 @@ class CharsNotIn(Token): ['dkls', 'lsdkjf', 's12 34', '@!#', '213'] """ - def __init__( self, notChars, min=1, max=0, exact=0 ): - super(CharsNotIn,self).__init__() + def __init__(self, notChars, min=1, max=0, exact=0): + super(CharsNotIn, self).__init__() self.skipWhitespace = False self.notChars = notChars if min < 1: - raise ValueError( - "cannot specify a minimum length < 1; use " + - "Optional(CharsNotIn()) if zero-length char group is permitted") + raise ValueError("cannot specify a minimum length < 1; use " + "Optional(CharsNotIn()) if zero-length char group is permitted") self.minLen = min @@ -3321,19 +3569,18 @@ def __init__( self, notChars, min=1, max=0, exact=0 ): self.name = _ustr(self) self.errmsg = "Expected " + self.name - self.mayReturnEmpty = ( self.minLen == 0 ) + self.mayReturnEmpty = (self.minLen == 0) self.mayIndexError = False - def parseImpl( self, instring, loc, doActions=True ): + def parseImpl(self, instring, loc, doActions=True): if instring[loc] in self.notChars: raise ParseException(instring, loc, self.errmsg, self) start = loc loc += 1 notchars = self.notChars - maxlen = min( start+self.maxLen, len(instring) ) - while loc < maxlen and \ - (instring[loc] not in notchars): + maxlen = min(start + self.maxLen, len(instring)) + while loc < maxlen and instring[loc] not in notchars: loc += 1 if loc - start < self.minLen: @@ -3341,7 +3588,7 @@ def parseImpl( self, instring, loc, doActions=True ): return loc, instring[start:loc] - def __str__( self ): + def __str__(self): try: return super(CharsNotIn, self).__str__() except Exception: @@ -3390,10 +3637,10 @@ class White(Token): 'u\3000': '<IDEOGRAPHIC_SPACE>', } def __init__(self, ws=" \t\r\n", min=1, max=0, exact=0): - super(White,self).__init__() + super(White, self).__init__() self.matchWhite = ws - self.setWhitespaceChars( "".join(c for c in self.whiteChars if c not in self.matchWhite) ) - #~ self.leaveWhitespace() + self.setWhitespaceChars("".join(c for c in self.whiteChars if c not in self.matchWhite)) + # ~ self.leaveWhitespace() self.name = ("".join(White.whiteStrs[c] for c in self.matchWhite)) self.mayReturnEmpty = True self.errmsg = "Expected " + self.name @@ -3409,13 +3656,13 @@ def __init__(self, ws=" \t\r\n", min=1, max=0, exact=0): self.maxLen = exact self.minLen = exact - def parseImpl( self, instring, loc, doActions=True ): + def parseImpl(self, instring, loc, doActions=True): if instring[loc] not in self.matchWhite: raise ParseException(instring, loc, self.errmsg, self) start = loc loc += 1 maxloc = start + self.maxLen - maxloc = min( maxloc, len(instring) ) + maxloc = min(maxloc, len(instring)) while loc < maxloc and instring[loc] in self.matchWhite: loc += 1 @@ -3426,9 +3673,9 @@ def parseImpl( self, instring, loc, doActions=True ): class _PositionToken(Token): - def __init__( self ): - super(_PositionToken,self).__init__() - self.name=self.__class__.__name__ + def __init__(self): + super(_PositionToken, self).__init__() + self.name = self.__class__.__name__ self.mayReturnEmpty = True self.mayIndexError = False @@ -3436,25 +3683,25 @@ class GoToColumn(_PositionToken): """Token to advance to a specific column of input text; useful for tabular report scraping. """ - def __init__( self, colno ): - super(GoToColumn,self).__init__() + def __init__(self, colno): + super(GoToColumn, self).__init__() self.col = colno - def preParse( self, instring, loc ): - if col(loc,instring) != self.col: + def preParse(self, instring, loc): + if col(loc, instring) != self.col: instrlen = len(instring) if self.ignoreExprs: - loc = self._skipIgnorables( instring, loc ) - while loc < instrlen and instring[loc].isspace() and col( loc, instring ) != self.col : + loc = self._skipIgnorables(instring, loc) + while loc < instrlen and instring[loc].isspace() and col(loc, instring) != self.col: loc += 1 return loc - def parseImpl( self, instring, loc, doActions=True ): - thiscol = col( loc, instring ) + def parseImpl(self, instring, loc, doActions=True): + thiscol = col(loc, instring) if thiscol > self.col: - raise ParseException( instring, loc, "Text not in expected column", self ) + raise ParseException(instring, loc, "Text not in expected column", self) newloc = loc + self.col - thiscol - ret = instring[ loc: newloc ] + ret = instring[loc: newloc] return newloc, ret @@ -3480,11 +3727,11 @@ class LineStart(_PositionToken): ['AAA', ' and this line'] """ - def __init__( self ): - super(LineStart,self).__init__() + def __init__(self): + super(LineStart, self).__init__() self.errmsg = "Expected start of line" - def parseImpl( self, instring, loc, doActions=True ): + def parseImpl(self, instring, loc, doActions=True): if col(loc, instring) == 1: return loc, [] raise ParseException(instring, loc, self.errmsg, self) @@ -3493,19 +3740,19 @@ class LineEnd(_PositionToken): """Matches if current position is at the end of a line within the parse string """ - def __init__( self ): - super(LineEnd,self).__init__() - self.setWhitespaceChars( ParserElement.DEFAULT_WHITE_CHARS.replace("\n","") ) + def __init__(self): + super(LineEnd, self).__init__() + self.setWhitespaceChars(ParserElement.DEFAULT_WHITE_CHARS.replace("\n", "")) self.errmsg = "Expected end of line" - def parseImpl( self, instring, loc, doActions=True ): - if loc<len(instring): + def parseImpl(self, instring, loc, doActions=True): + if loc < len(instring): if instring[loc] == "\n": - return loc+1, "\n" + return loc + 1, "\n" else: raise ParseException(instring, loc, self.errmsg, self) elif loc == len(instring): - return loc+1, [] + return loc + 1, [] else: raise ParseException(instring, loc, self.errmsg, self) @@ -3513,29 +3760,29 @@ class StringStart(_PositionToken): """Matches if current position is at the beginning of the parse string """ - def __init__( self ): - super(StringStart,self).__init__() + def __init__(self): + super(StringStart, self).__init__() self.errmsg = "Expected start of text" - def parseImpl( self, instring, loc, doActions=True ): + def parseImpl(self, instring, loc, doActions=True): if loc != 0: # see if entire string up to here is just whitespace and ignoreables - if loc != self.preParse( instring, 0 ): + if loc != self.preParse(instring, 0): raise ParseException(instring, loc, self.errmsg, self) return loc, [] class StringEnd(_PositionToken): """Matches if current position is at the end of the parse string """ - def __init__( self ): - super(StringEnd,self).__init__() + def __init__(self): + super(StringEnd, self).__init__() self.errmsg = "Expected end of text" - def parseImpl( self, instring, loc, doActions=True ): + def parseImpl(self, instring, loc, doActions=True): if loc < len(instring): raise ParseException(instring, loc, self.errmsg, self) elif loc == len(instring): - return loc+1, [] + return loc + 1, [] elif loc > len(instring): return loc, [] else: @@ -3550,15 +3797,15 @@ class WordStart(_PositionToken): the beginning of the string being parsed, or at the beginning of a line. """ - def __init__(self, wordChars = printables): - super(WordStart,self).__init__() + def __init__(self, wordChars=printables): + super(WordStart, self).__init__() self.wordChars = set(wordChars) self.errmsg = "Not at the start of a word" - def parseImpl(self, instring, loc, doActions=True ): + def parseImpl(self, instring, loc, doActions=True): if loc != 0: - if (instring[loc-1] in self.wordChars or - instring[loc] not in self.wordChars): + if (instring[loc - 1] in self.wordChars + or instring[loc] not in self.wordChars): raise ParseException(instring, loc, self.errmsg, self) return loc, [] @@ -3570,17 +3817,17 @@ class WordEnd(_PositionToken): will also match at the end of the string being parsed, or at the end of a line. """ - def __init__(self, wordChars = printables): - super(WordEnd,self).__init__() + def __init__(self, wordChars=printables): + super(WordEnd, self).__init__() self.wordChars = set(wordChars) self.skipWhitespace = False self.errmsg = "Not at the end of a word" - def parseImpl(self, instring, loc, doActions=True ): + def parseImpl(self, instring, loc, doActions=True): instrlen = len(instring) - if instrlen>0 and loc<instrlen: + if instrlen > 0 and loc < instrlen: if (instring[loc] in self.wordChars or - instring[loc-1] not in self.wordChars): + instring[loc - 1] not in self.wordChars): raise ParseException(instring, loc, self.errmsg, self) return loc, [] @@ -3589,90 +3836,89 @@ class ParseExpression(ParserElement): """Abstract subclass of ParserElement, for combining and post-processing parsed tokens. """ - def __init__( self, exprs, savelist = False ): - super(ParseExpression,self).__init__(savelist) - if isinstance( exprs, _generatorType ): + def __init__(self, exprs, savelist=False): + super(ParseExpression, self).__init__(savelist) + if isinstance(exprs, _generatorType): exprs = list(exprs) - if isinstance( exprs, basestring ): - self.exprs = [ ParserElement._literalStringClass( exprs ) ] - elif isinstance( exprs, Iterable ): + if isinstance(exprs, basestring): + self.exprs = [self._literalStringClass(exprs)] + elif isinstance(exprs, ParserElement): + self.exprs = [exprs] + elif isinstance(exprs, Iterable): exprs = list(exprs) # if sequence of strings provided, wrap with Literal - if all(isinstance(expr, basestring) for expr in exprs): - exprs = map(ParserElement._literalStringClass, exprs) + if any(isinstance(expr, basestring) for expr in exprs): + exprs = (self._literalStringClass(e) if isinstance(e, basestring) else e for e in exprs) self.exprs = list(exprs) else: try: - self.exprs = list( exprs ) + self.exprs = list(exprs) except TypeError: - self.exprs = [ exprs ] + self.exprs = [exprs] self.callPreparse = False - def __getitem__( self, i ): - return self.exprs[i] - - def append( self, other ): - self.exprs.append( other ) + def append(self, other): + self.exprs.append(other) self.strRepr = None return self - def leaveWhitespace( self ): + def leaveWhitespace(self): """Extends ``leaveWhitespace`` defined in base class, and also invokes ``leaveWhitespace`` on all contained expressions.""" self.skipWhitespace = False - self.exprs = [ e.copy() for e in self.exprs ] + self.exprs = [e.copy() for e in self.exprs] for e in self.exprs: e.leaveWhitespace() return self - def ignore( self, other ): - if isinstance( other, Suppress ): + def ignore(self, other): + if isinstance(other, Suppress): if other not in self.ignoreExprs: - super( ParseExpression, self).ignore( other ) + super(ParseExpression, self).ignore(other) for e in self.exprs: - e.ignore( self.ignoreExprs[-1] ) + e.ignore(self.ignoreExprs[-1]) else: - super( ParseExpression, self).ignore( other ) + super(ParseExpression, self).ignore(other) for e in self.exprs: - e.ignore( self.ignoreExprs[-1] ) + e.ignore(self.ignoreExprs[-1]) return self - def __str__( self ): + def __str__(self): try: - return super(ParseExpression,self).__str__() + return super(ParseExpression, self).__str__() except Exception: pass if self.strRepr is None: - self.strRepr = "%s:(%s)" % ( self.__class__.__name__, _ustr(self.exprs) ) + self.strRepr = "%s:(%s)" % (self.__class__.__name__, _ustr(self.exprs)) return self.strRepr - def streamline( self ): - super(ParseExpression,self).streamline() + def streamline(self): + super(ParseExpression, self).streamline() for e in self.exprs: e.streamline() - # collapse nested And's of the form And( And( And( a,b), c), d) to And( a,b,c,d ) + # collapse nested And's of the form And(And(And(a, b), c), d) to And(a, b, c, d) # but only if there are no parse actions or resultsNames on the nested And's # (likewise for Or's and MatchFirst's) - if ( len(self.exprs) == 2 ): + if len(self.exprs) == 2: other = self.exprs[0] - if ( isinstance( other, self.__class__ ) and - not(other.parseAction) and - other.resultsName is None and - not other.debug ): - self.exprs = other.exprs[:] + [ self.exprs[1] ] + if (isinstance(other, self.__class__) + and not other.parseAction + and other.resultsName is None + and not other.debug): + self.exprs = other.exprs[:] + [self.exprs[1]] self.strRepr = None self.mayReturnEmpty |= other.mayReturnEmpty self.mayIndexError |= other.mayIndexError other = self.exprs[-1] - if ( isinstance( other, self.__class__ ) and - not(other.parseAction) and - other.resultsName is None and - not other.debug ): + if (isinstance(other, self.__class__) + and not other.parseAction + and other.resultsName is None + and not other.debug): self.exprs = self.exprs[:-1] + other.exprs[:] self.strRepr = None self.mayReturnEmpty |= other.mayReturnEmpty @@ -3682,17 +3928,31 @@ def streamline( self ): return self - def validate( self, validateTrace=[] ): - tmp = validateTrace[:]+[self] + def validate(self, validateTrace=None): + tmp = (validateTrace if validateTrace is not None else [])[:] + [self] for e in self.exprs: e.validate(tmp) - self.checkRecursion( [] ) + self.checkRecursion([]) def copy(self): - ret = super(ParseExpression,self).copy() + ret = super(ParseExpression, self).copy() ret.exprs = [e.copy() for e in self.exprs] return ret + def _setResultsName(self, name, listAllMatches=False): + if __diag__.warn_ungrouped_named_tokens_in_collection: + for e in self.exprs: + if isinstance(e, ParserElement) and e.resultsName: + warnings.warn("{0}: setting results name {1!r} on {2} expression " + "collides with {3!r} on contained expression".format("warn_ungrouped_named_tokens_in_collection", + name, + type(self).__name__, + e.resultsName), + stacklevel=3) + + return super(ParseExpression, self)._setResultsName(name, listAllMatches) + + class And(ParseExpression): """ Requires all given :class:`ParseExpression` s to be found in the given order. @@ -3706,33 +3966,58 @@ class And(ParseExpression): integer = Word(nums) name_expr = OneOrMore(Word(alphas)) - expr = And([integer("id"),name_expr("name"),integer("age")]) + expr = And([integer("id"), name_expr("name"), integer("age")]) # more easily written as: expr = integer("id") + name_expr("name") + integer("age") """ class _ErrorStop(Empty): def __init__(self, *args, **kwargs): - super(And._ErrorStop,self).__init__(*args, **kwargs) + super(And._ErrorStop, self).__init__(*args, **kwargs) self.name = '-' self.leaveWhitespace() - def __init__( self, exprs, savelist = True ): - super(And,self).__init__(exprs, savelist) + def __init__(self, exprs, savelist=True): + if exprs and Ellipsis in exprs: + tmp = [] + for i, expr in enumerate(exprs): + if expr is Ellipsis: + if i < len(exprs) - 1: + skipto_arg = (Empty() + exprs[i + 1]).exprs[-1] + tmp.append(SkipTo(skipto_arg)("_skipped*")) + else: + raise Exception("cannot construct And with sequence ending in ...") + else: + tmp.append(expr) + exprs[:] = tmp + super(And, self).__init__(exprs, savelist) self.mayReturnEmpty = all(e.mayReturnEmpty for e in self.exprs) - self.setWhitespaceChars( self.exprs[0].whiteChars ) + self.setWhitespaceChars(self.exprs[0].whiteChars) self.skipWhitespace = self.exprs[0].skipWhitespace self.callPreparse = True def streamline(self): + # collapse any _PendingSkip's + if self.exprs: + if any(isinstance(e, ParseExpression) and e.exprs and isinstance(e.exprs[-1], _PendingSkip) + for e in self.exprs[:-1]): + for i, e in enumerate(self.exprs[:-1]): + if e is None: + continue + if (isinstance(e, ParseExpression) + and e.exprs and isinstance(e.exprs[-1], _PendingSkip)): + e.exprs[-1] = e.exprs[-1] + self.exprs[i + 1] + self.exprs[i + 1] = None + self.exprs = [e for e in self.exprs if e is not None] + super(And, self).streamline() self.mayReturnEmpty = all(e.mayReturnEmpty for e in self.exprs) return self - def parseImpl( self, instring, loc, doActions=True ): + def parseImpl(self, instring, loc, doActions=True): # pass False as last arg to _parse for first element, since we already # pre-parsed the string as part of our And pre-parsing - loc, resultlist = self.exprs[0]._parse( instring, loc, doActions, callPreParse=False ) + loc, resultlist = self.exprs[0]._parse(instring, loc, doActions, callPreParse=False) errorStop = False for e in self.exprs[1:]: if isinstance(e, And._ErrorStop): @@ -3740,7 +4025,7 @@ def parseImpl( self, instring, loc, doActions=True ): continue if errorStop: try: - loc, exprtokens = e._parse( instring, loc, doActions ) + loc, exprtokens = e._parse(instring, loc, doActions) except ParseSyntaxException: raise except ParseBaseException as pe: @@ -3749,25 +4034,25 @@ def parseImpl( self, instring, loc, doActions=True ): except IndexError: raise ParseSyntaxException(instring, len(instring), self.errmsg, self) else: - loc, exprtokens = e._parse( instring, loc, doActions ) + loc, exprtokens = e._parse(instring, loc, doActions) if exprtokens or exprtokens.haskeys(): resultlist += exprtokens return loc, resultlist - def __iadd__(self, other ): - if isinstance( other, basestring ): - other = ParserElement._literalStringClass( other ) - return self.append( other ) #And( [ self, other ] ) + def __iadd__(self, other): + if isinstance(other, basestring): + other = self._literalStringClass(other) + return self.append(other) # And([self, other]) - def checkRecursion( self, parseElementList ): - subRecCheckList = parseElementList[:] + [ self ] + def checkRecursion(self, parseElementList): + subRecCheckList = parseElementList[:] + [self] for e in self.exprs: - e.checkRecursion( subRecCheckList ) + e.checkRecursion(subRecCheckList) if not e.mayReturnEmpty: break - def __str__( self ): - if hasattr(self,"name"): + def __str__(self): + if hasattr(self, "name"): return self.name if self.strRepr is None: @@ -3793,8 +4078,8 @@ class Or(ParseExpression): [['123'], ['3.1416'], ['789']] """ - def __init__( self, exprs, savelist = False ): - super(Or,self).__init__(exprs, savelist) + def __init__(self, exprs, savelist=False): + super(Or, self).__init__(exprs, savelist) if self.exprs: self.mayReturnEmpty = any(e.mayReturnEmpty for e in self.exprs) else: @@ -3806,13 +4091,13 @@ def streamline(self): self.saveAsList = any(e.saveAsList for e in self.exprs) return self - def parseImpl( self, instring, loc, doActions=True ): + def parseImpl(self, instring, loc, doActions=True): maxExcLoc = -1 maxException = None matches = [] for e in self.exprs: try: - loc2 = e.tryParse( instring, loc ) + loc2 = e.tryParse(instring, loc) except ParseException as err: err.__traceback__ = None if err.loc > maxExcLoc: @@ -3820,22 +4105,45 @@ def parseImpl( self, instring, loc, doActions=True ): maxExcLoc = err.loc except IndexError: if len(instring) > maxExcLoc: - maxException = ParseException(instring,len(instring),e.errmsg,self) + maxException = ParseException(instring, len(instring), e.errmsg, self) maxExcLoc = len(instring) else: # save match among all matches, to retry longest to shortest matches.append((loc2, e)) if matches: - matches.sort(key=lambda x: -x[0]) - for _,e in matches: + # re-evaluate all matches in descending order of length of match, in case attached actions + # might change whether or how much they match of the input. + matches.sort(key=itemgetter(0), reverse=True) + + if not doActions: + # no further conditions or parse actions to change the selection of + # alternative, so the first match will be the best match + best_expr = matches[0][1] + return best_expr._parse(instring, loc, doActions) + + longest = -1, None + for loc1, expr1 in matches: + if loc1 <= longest[0]: + # already have a longer match than this one will deliver, we are done + return longest + try: - return e._parse( instring, loc, doActions ) + loc2, toks = expr1._parse(instring, loc, doActions) except ParseException as err: err.__traceback__ = None if err.loc > maxExcLoc: maxException = err maxExcLoc = err.loc + else: + if loc2 >= loc1: + return loc2, toks + # didn't match as much as before + elif loc2 > longest[0]: + longest = loc2, toks + + if longest != (-1, None): + return longest if maxException is not None: maxException.msg = self.errmsg @@ -3844,13 +4152,13 @@ def parseImpl( self, instring, loc, doActions=True ): raise ParseException(instring, loc, "no defined alternatives to match", self) - def __ixor__(self, other ): - if isinstance( other, basestring ): - other = ParserElement._literalStringClass( other ) - return self.append( other ) #Or( [ self, other ] ) + def __ixor__(self, other): + if isinstance(other, basestring): + other = self._literalStringClass(other) + return self.append(other) # Or([self, other]) - def __str__( self ): - if hasattr(self,"name"): + def __str__(self): + if hasattr(self, "name"): return self.name if self.strRepr is None: @@ -3858,10 +4166,22 @@ def __str__( self ): return self.strRepr - def checkRecursion( self, parseElementList ): - subRecCheckList = parseElementList[:] + [ self ] + def checkRecursion(self, parseElementList): + subRecCheckList = parseElementList[:] + [self] for e in self.exprs: - e.checkRecursion( subRecCheckList ) + e.checkRecursion(subRecCheckList) + + def _setResultsName(self, name, listAllMatches=False): + if (not __compat__.collect_all_And_tokens + and __diag__.warn_multiple_tokens_in_named_alternation): + if any(isinstance(e, And) for e in self.exprs): + warnings.warn("{0}: setting results name {1!r} on {2} expression " + "may only return a single token for an And alternative, " + "in future will return the full list of tokens".format( + "warn_multiple_tokens_in_named_alternation", name, type(self).__name__), + stacklevel=3) + + return super(Or, self)._setResultsName(name, listAllMatches) class MatchFirst(ParseExpression): @@ -3881,8 +4201,8 @@ class MatchFirst(ParseExpression): number = Combine(Word(nums) + '.' + Word(nums)) | Word(nums) print(number.searchString("123 3.1416 789")) # Better -> [['123'], ['3.1416'], ['789']] """ - def __init__( self, exprs, savelist = False ): - super(MatchFirst,self).__init__(exprs, savelist) + def __init__(self, exprs, savelist=False): + super(MatchFirst, self).__init__(exprs, savelist) if self.exprs: self.mayReturnEmpty = any(e.mayReturnEmpty for e in self.exprs) else: @@ -3894,12 +4214,12 @@ def streamline(self): self.saveAsList = any(e.saveAsList for e in self.exprs) return self - def parseImpl( self, instring, loc, doActions=True ): + def parseImpl(self, instring, loc, doActions=True): maxExcLoc = -1 maxException = None for e in self.exprs: try: - ret = e._parse( instring, loc, doActions ) + ret = e._parse(instring, loc, doActions) return ret except ParseException as err: if err.loc > maxExcLoc: @@ -3907,7 +4227,7 @@ def parseImpl( self, instring, loc, doActions=True ): maxExcLoc = err.loc except IndexError: if len(instring) > maxExcLoc: - maxException = ParseException(instring,len(instring),e.errmsg,self) + maxException = ParseException(instring, len(instring), e.errmsg, self) maxExcLoc = len(instring) # only got here if no expression matched, raise exception for match that made it the furthest @@ -3918,13 +4238,13 @@ def parseImpl( self, instring, loc, doActions=True ): else: raise ParseException(instring, loc, "no defined alternatives to match", self) - def __ior__(self, other ): - if isinstance( other, basestring ): - other = ParserElement._literalStringClass( other ) - return self.append( other ) #MatchFirst( [ self, other ] ) + def __ior__(self, other): + if isinstance(other, basestring): + other = self._literalStringClass(other) + return self.append(other) # MatchFirst([self, other]) - def __str__( self ): - if hasattr(self,"name"): + def __str__(self): + if hasattr(self, "name"): return self.name if self.strRepr is None: @@ -3932,10 +4252,22 @@ def __str__( self ): return self.strRepr - def checkRecursion( self, parseElementList ): - subRecCheckList = parseElementList[:] + [ self ] + def checkRecursion(self, parseElementList): + subRecCheckList = parseElementList[:] + [self] for e in self.exprs: - e.checkRecursion( subRecCheckList ) + e.checkRecursion(subRecCheckList) + + def _setResultsName(self, name, listAllMatches=False): + if (not __compat__.collect_all_And_tokens + and __diag__.warn_multiple_tokens_in_named_alternation): + if any(isinstance(e, And) for e in self.exprs): + warnings.warn("{0}: setting results name {1!r} on {2} expression " + "may only return a single token for an And alternative, " + "in future will return the full list of tokens".format( + "warn_multiple_tokens_in_named_alternation", name, type(self).__name__), + stacklevel=3) + + return super(MatchFirst, self)._setResultsName(name, listAllMatches) class Each(ParseExpression): @@ -3995,8 +4327,8 @@ class Each(ParseExpression): - shape: TRIANGLE - size: 20 """ - def __init__( self, exprs, savelist = True ): - super(Each,self).__init__(exprs, savelist) + def __init__(self, exprs, savelist=True): + super(Each, self).__init__(exprs, savelist) self.mayReturnEmpty = all(e.mayReturnEmpty for e in self.exprs) self.skipWhitespace = True self.initExprGroups = True @@ -4007,15 +4339,15 @@ def streamline(self): self.mayReturnEmpty = all(e.mayReturnEmpty for e in self.exprs) return self - def parseImpl( self, instring, loc, doActions=True ): + def parseImpl(self, instring, loc, doActions=True): if self.initExprGroups: - self.opt1map = dict((id(e.expr),e) for e in self.exprs if isinstance(e,Optional)) - opt1 = [ e.expr for e in self.exprs if isinstance(e,Optional) ] - opt2 = [ e for e in self.exprs if e.mayReturnEmpty and not isinstance(e,Optional)] + self.opt1map = dict((id(e.expr), e) for e in self.exprs if isinstance(e, Optional)) + opt1 = [e.expr for e in self.exprs if isinstance(e, Optional)] + opt2 = [e for e in self.exprs if e.mayReturnEmpty and not isinstance(e, Optional)] self.optionals = opt1 + opt2 - self.multioptionals = [ e.expr for e in self.exprs if isinstance(e,ZeroOrMore) ] - self.multirequired = [ e.expr for e in self.exprs if isinstance(e,OneOrMore) ] - self.required = [ e for e in self.exprs if not isinstance(e,(Optional,ZeroOrMore,OneOrMore)) ] + self.multioptionals = [e.expr for e in self.exprs if isinstance(e, ZeroOrMore)] + self.multirequired = [e.expr for e in self.exprs if isinstance(e, OneOrMore)] + self.required = [e for e in self.exprs if not isinstance(e, (Optional, ZeroOrMore, OneOrMore))] self.required += self.multirequired self.initExprGroups = False tmpLoc = loc @@ -4029,11 +4361,11 @@ def parseImpl( self, instring, loc, doActions=True ): failed = [] for e in tmpExprs: try: - tmpLoc = e.tryParse( instring, tmpLoc ) + tmpLoc = e.tryParse(instring, tmpLoc) except ParseException: failed.append(e) else: - matchOrder.append(self.opt1map.get(id(e),e)) + matchOrder.append(self.opt1map.get(id(e), e)) if e in tmpReqd: tmpReqd.remove(e) elif e in tmpOpt: @@ -4043,21 +4375,21 @@ def parseImpl( self, instring, loc, doActions=True ): if tmpReqd: missing = ", ".join(_ustr(e) for e in tmpReqd) - raise ParseException(instring,loc,"Missing one or more required elements (%s)" % missing ) + raise ParseException(instring, loc, "Missing one or more required elements (%s)" % missing) # add any unmatched Optionals, in case they have default values defined - matchOrder += [e for e in self.exprs if isinstance(e,Optional) and e.expr in tmpOpt] + matchOrder += [e for e in self.exprs if isinstance(e, Optional) and e.expr in tmpOpt] resultlist = [] for e in matchOrder: - loc,results = e._parse(instring,loc,doActions) + loc, results = e._parse(instring, loc, doActions) resultlist.append(results) finalResults = sum(resultlist, ParseResults([])) return loc, finalResults - def __str__( self ): - if hasattr(self,"name"): + def __str__(self): + if hasattr(self, "name"): return self.name if self.strRepr is None: @@ -4065,86 +4397,88 @@ def __str__( self ): return self.strRepr - def checkRecursion( self, parseElementList ): - subRecCheckList = parseElementList[:] + [ self ] + def checkRecursion(self, parseElementList): + subRecCheckList = parseElementList[:] + [self] for e in self.exprs: - e.checkRecursion( subRecCheckList ) + e.checkRecursion(subRecCheckList) class ParseElementEnhance(ParserElement): """Abstract subclass of :class:`ParserElement`, for combining and post-processing parsed tokens. """ - def __init__( self, expr, savelist=False ): - super(ParseElementEnhance,self).__init__(savelist) - if isinstance( expr, basestring ): - if issubclass(ParserElement._literalStringClass, Token): - expr = ParserElement._literalStringClass(expr) + def __init__(self, expr, savelist=False): + super(ParseElementEnhance, self).__init__(savelist) + if isinstance(expr, basestring): + if issubclass(self._literalStringClass, Token): + expr = self._literalStringClass(expr) else: - expr = ParserElement._literalStringClass(Literal(expr)) + expr = self._literalStringClass(Literal(expr)) self.expr = expr self.strRepr = None if expr is not None: self.mayIndexError = expr.mayIndexError self.mayReturnEmpty = expr.mayReturnEmpty - self.setWhitespaceChars( expr.whiteChars ) + self.setWhitespaceChars(expr.whiteChars) self.skipWhitespace = expr.skipWhitespace self.saveAsList = expr.saveAsList self.callPreparse = expr.callPreparse self.ignoreExprs.extend(expr.ignoreExprs) - def parseImpl( self, instring, loc, doActions=True ): + def parseImpl(self, instring, loc, doActions=True): if self.expr is not None: - return self.expr._parse( instring, loc, doActions, callPreParse=False ) + return self.expr._parse(instring, loc, doActions, callPreParse=False) else: - raise ParseException("",loc,self.errmsg,self) + raise ParseException("", loc, self.errmsg, self) - def leaveWhitespace( self ): + def leaveWhitespace(self): self.skipWhitespace = False self.expr = self.expr.copy() if self.expr is not None: self.expr.leaveWhitespace() return self - def ignore( self, other ): - if isinstance( other, Suppress ): + def ignore(self, other): + if isinstance(other, Suppress): if other not in self.ignoreExprs: - super( ParseElementEnhance, self).ignore( other ) + super(ParseElementEnhance, self).ignore(other) if self.expr is not None: - self.expr.ignore( self.ignoreExprs[-1] ) + self.expr.ignore(self.ignoreExprs[-1]) else: - super( ParseElementEnhance, self).ignore( other ) + super(ParseElementEnhance, self).ignore(other) if self.expr is not None: - self.expr.ignore( self.ignoreExprs[-1] ) + self.expr.ignore(self.ignoreExprs[-1]) return self - def streamline( self ): - super(ParseElementEnhance,self).streamline() + def streamline(self): + super(ParseElementEnhance, self).streamline() if self.expr is not None: self.expr.streamline() return self - def checkRecursion( self, parseElementList ): + def checkRecursion(self, parseElementList): if self in parseElementList: - raise RecursiveGrammarException( parseElementList+[self] ) - subRecCheckList = parseElementList[:] + [ self ] + raise RecursiveGrammarException(parseElementList + [self]) + subRecCheckList = parseElementList[:] + [self] if self.expr is not None: - self.expr.checkRecursion( subRecCheckList ) + self.expr.checkRecursion(subRecCheckList) - def validate( self, validateTrace=[] ): - tmp = validateTrace[:]+[self] + def validate(self, validateTrace=None): + if validateTrace is None: + validateTrace = [] + tmp = validateTrace[:] + [self] if self.expr is not None: self.expr.validate(tmp) - self.checkRecursion( [] ) + self.checkRecursion([]) - def __str__( self ): + def __str__(self): try: - return super(ParseElementEnhance,self).__str__() + return super(ParseElementEnhance, self).__str__() except Exception: pass if self.strRepr is None and self.expr is not None: - self.strRepr = "%s:(%s)" % ( self.__class__.__name__, _ustr(self.expr) ) + self.strRepr = "%s:(%s)" % (self.__class__.__name__, _ustr(self.expr)) return self.strRepr @@ -4170,13 +4504,16 @@ class FollowedBy(ParseElementEnhance): [['shape', 'SQUARE'], ['color', 'BLACK'], ['posn', 'upper left']] """ - def __init__( self, expr ): - super(FollowedBy,self).__init__(expr) + def __init__(self, expr): + super(FollowedBy, self).__init__(expr) self.mayReturnEmpty = True - def parseImpl( self, instring, loc, doActions=True ): + def parseImpl(self, instring, loc, doActions=True): + # by using self._expr.parse and deleting the contents of the returned ParseResults list + # we keep any named results that were defined in the FollowedBy expression _, ret = self.expr._parse(instring, loc, doActions=doActions) del ret[:] + return loc, ret @@ -4241,9 +4578,9 @@ def parseImpl(self, instring, loc=0, doActions=True): test_expr = self.expr + StringEnd() instring_slice = instring[:loc] last_expr = ParseException(instring, loc, self.errmsg) - for offset in range(1, min(loc, self.retreat+1)): + for offset in range(1, min(loc, self.retreat + 1)): try: - _, ret = test_expr._parse(instring_slice, loc-offset) + _, ret = test_expr._parse(instring_slice, loc - offset) except ParseBaseException as pbe: last_expr = pbe else: @@ -4278,20 +4615,20 @@ class NotAny(ParseElementEnhance): # integers that are followed by "." are actually floats integer = Word(nums) + ~Char(".") """ - def __init__( self, expr ): - super(NotAny,self).__init__(expr) - #~ self.leaveWhitespace() + def __init__(self, expr): + super(NotAny, self).__init__(expr) + # ~ self.leaveWhitespace() self.skipWhitespace = False # do NOT use self.leaveWhitespace(), don't want to propagate to exprs self.mayReturnEmpty = True - self.errmsg = "Found unwanted token, "+_ustr(self.expr) + self.errmsg = "Found unwanted token, " + _ustr(self.expr) - def parseImpl( self, instring, loc, doActions=True ): + def parseImpl(self, instring, loc, doActions=True): if self.expr.canParseNext(instring, loc): raise ParseException(instring, loc, self.errmsg, self) return loc, [] - def __str__( self ): - if hasattr(self,"name"): + def __str__(self): + if hasattr(self, "name"): return self.name if self.strRepr is None: @@ -4300,15 +4637,21 @@ def __str__( self ): return self.strRepr class _MultipleMatch(ParseElementEnhance): - def __init__( self, expr, stopOn=None): + def __init__(self, expr, stopOn=None): super(_MultipleMatch, self).__init__(expr) self.saveAsList = True ender = stopOn if isinstance(ender, basestring): - ender = ParserElement._literalStringClass(ender) + ender = self._literalStringClass(ender) + self.stopOn(ender) + + def stopOn(self, ender): + if isinstance(ender, basestring): + ender = self._literalStringClass(ender) self.not_ender = ~ender if ender is not None else None + return self - def parseImpl( self, instring, loc, doActions=True ): + def parseImpl(self, instring, loc, doActions=True): self_expr_parse = self.expr._parse self_skip_ignorables = self._skipIgnorables check_ender = self.not_ender is not None @@ -4319,24 +4662,38 @@ def parseImpl( self, instring, loc, doActions=True ): # if so, fail) if check_ender: try_not_ender(instring, loc) - loc, tokens = self_expr_parse( instring, loc, doActions, callPreParse=False ) + loc, tokens = self_expr_parse(instring, loc, doActions, callPreParse=False) try: hasIgnoreExprs = (not not self.ignoreExprs) while 1: if check_ender: try_not_ender(instring, loc) if hasIgnoreExprs: - preloc = self_skip_ignorables( instring, loc ) + preloc = self_skip_ignorables(instring, loc) else: preloc = loc - loc, tmptokens = self_expr_parse( instring, preloc, doActions ) + loc, tmptokens = self_expr_parse(instring, preloc, doActions) if tmptokens or tmptokens.haskeys(): tokens += tmptokens - except (ParseException,IndexError): + except (ParseException, IndexError): pass return loc, tokens + def _setResultsName(self, name, listAllMatches=False): + if __diag__.warn_ungrouped_named_tokens_in_collection: + for e in [self.expr] + getattr(self.expr, 'exprs', []): + if isinstance(e, ParserElement) and e.resultsName: + warnings.warn("{0}: setting results name {1!r} on {2} expression " + "collides with {3!r} on contained expression".format("warn_ungrouped_named_tokens_in_collection", + name, + type(self).__name__, + e.resultsName), + stacklevel=3) + + return super(_MultipleMatch, self)._setResultsName(name, listAllMatches) + + class OneOrMore(_MultipleMatch): """Repetition of one or more of the given expression. @@ -4363,8 +4720,8 @@ class OneOrMore(_MultipleMatch): (attr_expr * (1,)).parseString(text).pprint() """ - def __str__( self ): - if hasattr(self,"name"): + def __str__(self): + if hasattr(self, "name"): return self.name if self.strRepr is None: @@ -4383,18 +4740,18 @@ class ZeroOrMore(_MultipleMatch): Example: similar to :class:`OneOrMore` """ - def __init__( self, expr, stopOn=None): - super(ZeroOrMore,self).__init__(expr, stopOn=stopOn) + def __init__(self, expr, stopOn=None): + super(ZeroOrMore, self).__init__(expr, stopOn=stopOn) self.mayReturnEmpty = True - def parseImpl( self, instring, loc, doActions=True ): + def parseImpl(self, instring, loc, doActions=True): try: return super(ZeroOrMore, self).parseImpl(instring, loc, doActions) - except (ParseException,IndexError): + except (ParseException, IndexError): return loc, [] - def __str__( self ): - if hasattr(self,"name"): + def __str__(self): + if hasattr(self, "name"): return self.name if self.strRepr is None: @@ -4402,6 +4759,7 @@ def __str__( self ): return self.strRepr + class _NullToken(object): def __bool__(self): return False @@ -4409,7 +4767,6 @@ def __bool__(self): def __str__(self): return "" -_optionalNotMatched = _NullToken() class Optional(ParseElementEnhance): """Optional matching of the given expression. @@ -4447,28 +4804,30 @@ class Optional(ParseElementEnhance): ^ FAIL: Expected end of text (at char 5), (line:1, col:6) """ - def __init__( self, expr, default=_optionalNotMatched ): - super(Optional,self).__init__( expr, savelist=False ) + __optionalNotMatched = _NullToken() + + def __init__(self, expr, default=__optionalNotMatched): + super(Optional, self).__init__(expr, savelist=False) self.saveAsList = self.expr.saveAsList self.defaultValue = default self.mayReturnEmpty = True - def parseImpl( self, instring, loc, doActions=True ): + def parseImpl(self, instring, loc, doActions=True): try: - loc, tokens = self.expr._parse( instring, loc, doActions, callPreParse=False ) - except (ParseException,IndexError): - if self.defaultValue is not _optionalNotMatched: + loc, tokens = self.expr._parse(instring, loc, doActions, callPreParse=False) + except (ParseException, IndexError): + if self.defaultValue is not self.__optionalNotMatched: if self.expr.resultsName: - tokens = ParseResults([ self.defaultValue ]) + tokens = ParseResults([self.defaultValue]) tokens[self.expr.resultsName] = self.defaultValue else: - tokens = [ self.defaultValue ] + tokens = [self.defaultValue] else: tokens = [] return loc, tokens - def __str__( self ): - if hasattr(self,"name"): + def __str__(self): + if hasattr(self, "name"): return self.name if self.strRepr is None: @@ -4534,20 +4893,20 @@ class SkipTo(ParseElementEnhance): - issue_num: 79 - sev: Minor """ - def __init__( self, other, include=False, ignore=None, failOn=None ): - super( SkipTo, self ).__init__( other ) + def __init__(self, other, include=False, ignore=None, failOn=None): + super(SkipTo, self).__init__(other) self.ignoreExpr = ignore self.mayReturnEmpty = True self.mayIndexError = False self.includeMatch = include self.saveAsList = False if isinstance(failOn, basestring): - self.failOn = ParserElement._literalStringClass(failOn) + self.failOn = self._literalStringClass(failOn) else: self.failOn = failOn - self.errmsg = "No match found for "+_ustr(self.expr) + self.errmsg = "No match found for " + _ustr(self.expr) - def parseImpl( self, instring, loc, doActions=True ): + def parseImpl(self, instring, loc, doActions=True): startloc = loc instrlen = len(instring) expr = self.expr @@ -4589,7 +4948,7 @@ def parseImpl( self, instring, loc, doActions=True ): skipresult = ParseResults(skiptext) if self.includeMatch: - loc, mat = expr_parse(instring,loc,doActions,callPreParse=False) + loc, mat = expr_parse(instring, loc, doActions, callPreParse=False) skipresult += mat return loc, skipresult @@ -4621,17 +4980,17 @@ class Forward(ParseElementEnhance): See :class:`ParseResults.pprint` for an example of a recursive parser created using ``Forward``. """ - def __init__( self, other=None ): - super(Forward,self).__init__( other, savelist=False ) + def __init__(self, other=None): + super(Forward, self).__init__(other, savelist=False) - def __lshift__( self, other ): - if isinstance( other, basestring ): - other = ParserElement._literalStringClass(other) + def __lshift__(self, other): + if isinstance(other, basestring): + other = self._literalStringClass(other) self.expr = other self.strRepr = None self.mayIndexError = self.expr.mayIndexError self.mayReturnEmpty = self.expr.mayReturnEmpty - self.setWhitespaceChars( self.expr.whiteChars ) + self.setWhitespaceChars(self.expr.whiteChars) self.skipWhitespace = self.expr.skipWhitespace self.saveAsList = self.expr.saveAsList self.ignoreExprs.extend(self.expr.ignoreExprs) @@ -4640,55 +4999,72 @@ def __lshift__( self, other ): def __ilshift__(self, other): return self << other - def leaveWhitespace( self ): + def leaveWhitespace(self): self.skipWhitespace = False return self - def streamline( self ): + def streamline(self): if not self.streamlined: self.streamlined = True if self.expr is not None: self.expr.streamline() return self - def validate( self, validateTrace=[] ): + def validate(self, validateTrace=None): + if validateTrace is None: + validateTrace = [] + if self not in validateTrace: - tmp = validateTrace[:]+[self] + tmp = validateTrace[:] + [self] if self.expr is not None: self.expr.validate(tmp) self.checkRecursion([]) - def __str__( self ): - if hasattr(self,"name"): + def __str__(self): + if hasattr(self, "name"): return self.name + if self.strRepr is not None: + return self.strRepr - # Avoid infinite recursion by setting a temporary name - self.name = self.__class__.__name__ + ": ..." + # Avoid infinite recursion by setting a temporary strRepr + self.strRepr = ": ..." # Use the string representation of main expression. + retString = '...' try: if self.expr is not None: - retString = _ustr(self.expr) + retString = _ustr(self.expr)[:1000] else: retString = "None" finally: - del self.name - return self.__class__.__name__ + ": " + retString + self.strRepr = self.__class__.__name__ + ": " + retString + return self.strRepr def copy(self): if self.expr is not None: - return super(Forward,self).copy() + return super(Forward, self).copy() else: ret = Forward() ret <<= self return ret + def _setResultsName(self, name, listAllMatches=False): + if __diag__.warn_name_set_on_empty_Forward: + if self.expr is None: + warnings.warn("{0}: setting results name {0!r} on {1} expression " + "that has no contained expression".format("warn_name_set_on_empty_Forward", + name, + type(self).__name__), + stacklevel=3) + + return super(Forward, self)._setResultsName(name, listAllMatches) + class TokenConverter(ParseElementEnhance): """ Abstract subclass of :class:`ParseExpression`, for converting parsed results. """ - def __init__( self, expr, savelist=False ): - super(TokenConverter,self).__init__( expr )#, savelist ) + def __init__(self, expr, savelist=False): + super(TokenConverter, self).__init__(expr) # , savelist) self.saveAsList = False class Combine(TokenConverter): @@ -4709,8 +5085,8 @@ class Combine(TokenConverter): # no match when there are internal spaces print(real.parseString('3. 1416')) # -> Exception: Expected W:(0123...) """ - def __init__( self, expr, joinString="", adjacent=True ): - super(Combine,self).__init__( expr ) + def __init__(self, expr, joinString="", adjacent=True): + super(Combine, self).__init__(expr) # suppress whitespace-stripping in contained parse expressions, but re-enable it on the Combine itself if adjacent: self.leaveWhitespace() @@ -4719,20 +5095,20 @@ def __init__( self, expr, joinString="", adjacent=True ): self.joinString = joinString self.callPreparse = True - def ignore( self, other ): + def ignore(self, other): if self.adjacent: ParserElement.ignore(self, other) else: - super( Combine, self).ignore( other ) + super(Combine, self).ignore(other) return self - def postParse( self, instring, loc, tokenlist ): + def postParse(self, instring, loc, tokenlist): retToks = tokenlist.copy() del retToks[:] - retToks += ParseResults([ "".join(tokenlist._asStringList(self.joinString)) ], modal=self.modalResults) + retToks += ParseResults(["".join(tokenlist._asStringList(self.joinString))], modal=self.modalResults) if self.resultsName and retToks.haskeys(): - return [ retToks ] + return [retToks] else: return retToks @@ -4746,17 +5122,17 @@ class Group(TokenConverter): num = Word(nums) term = ident | num func = ident + Optional(delimitedList(term)) - print(func.parseString("fn a,b,100")) # -> ['fn', 'a', 'b', '100'] + print(func.parseString("fn a, b, 100")) # -> ['fn', 'a', 'b', '100'] func = ident + Group(Optional(delimitedList(term))) - print(func.parseString("fn a,b,100")) # -> ['fn', ['a', 'b', '100']] + print(func.parseString("fn a, b, 100")) # -> ['fn', ['a', 'b', '100']] """ - def __init__( self, expr ): - super(Group,self).__init__( expr ) + def __init__(self, expr): + super(Group, self).__init__(expr) self.saveAsList = True - def postParse( self, instring, loc, tokenlist ): - return [ tokenlist ] + def postParse(self, instring, loc, tokenlist): + return [tokenlist] class Dict(TokenConverter): """Converter to return a repetitive expression as a list, but also @@ -4797,31 +5173,31 @@ class Dict(TokenConverter): See more examples at :class:`ParseResults` of accessing fields by results name. """ - def __init__( self, expr ): - super(Dict,self).__init__( expr ) + def __init__(self, expr): + super(Dict, self).__init__(expr) self.saveAsList = True - def postParse( self, instring, loc, tokenlist ): - for i,tok in enumerate(tokenlist): + def postParse(self, instring, loc, tokenlist): + for i, tok in enumerate(tokenlist): if len(tok) == 0: continue ikey = tok[0] - if isinstance(ikey,int): + if isinstance(ikey, int): ikey = _ustr(tok[0]).strip() - if len(tok)==1: - tokenlist[ikey] = _ParseResultsWithOffset("",i) - elif len(tok)==2 and not isinstance(tok[1],ParseResults): - tokenlist[ikey] = _ParseResultsWithOffset(tok[1],i) + if len(tok) == 1: + tokenlist[ikey] = _ParseResultsWithOffset("", i) + elif len(tok) == 2 and not isinstance(tok[1], ParseResults): + tokenlist[ikey] = _ParseResultsWithOffset(tok[1], i) else: - dictvalue = tok.copy() #ParseResults(i) + dictvalue = tok.copy() # ParseResults(i) del dictvalue[0] - if len(dictvalue)!= 1 or (isinstance(dictvalue,ParseResults) and dictvalue.haskeys()): - tokenlist[ikey] = _ParseResultsWithOffset(dictvalue,i) + if len(dictvalue) != 1 or (isinstance(dictvalue, ParseResults) and dictvalue.haskeys()): + tokenlist[ikey] = _ParseResultsWithOffset(dictvalue, i) else: - tokenlist[ikey] = _ParseResultsWithOffset(dictvalue[0],i) + tokenlist[ikey] = _ParseResultsWithOffset(dictvalue[0], i) if self.resultsName: - return [ tokenlist ] + return [tokenlist] else: return tokenlist @@ -4848,10 +5224,10 @@ class Suppress(TokenConverter): (See also :class:`delimitedList`.) """ - def postParse( self, instring, loc, tokenlist ): + def postParse(self, instring, loc, tokenlist): return [] - def suppress( self ): + def suppress(self): return self @@ -4861,12 +5237,12 @@ class OnlyOnce(object): def __init__(self, methodCall): self.callable = _trim_arity(methodCall) self.called = False - def __call__(self,s,l,t): + def __call__(self, s, l, t): if not self.called: - results = self.callable(s,l,t) + results = self.callable(s, l, t) self.called = True return results - raise ParseException(s,l,"") + raise ParseException(s, l, "") def reset(self): self.called = False @@ -4898,16 +5274,16 @@ def remove_duplicate_chars(tokens): f = _trim_arity(f) def z(*paArgs): thisFunc = f.__name__ - s,l,t = paArgs[-3:] - if len(paArgs)>3: + s, l, t = paArgs[-3:] + if len(paArgs) > 3: thisFunc = paArgs[0].__class__.__name__ + '.' + thisFunc - sys.stderr.write( ">>entering %s(line: '%s', %d, %r)\n" % (thisFunc,line(l,s),l,t) ) + sys.stderr.write(">>entering %s(line: '%s', %d, %r)\n" % (thisFunc, line(l, s), l, t)) try: ret = f(*paArgs) except Exception as exc: - sys.stderr.write( "<<leaving %s (exception: %s)\n" % (thisFunc,exc) ) + sys.stderr.write("<<leaving %s (exception: %s)\n" % (thisFunc, exc)) raise - sys.stderr.write( "<<leaving %s (ret: %r)\n" % (thisFunc,ret) ) + sys.stderr.write("<<leaving %s (ret: %r)\n" % (thisFunc, ret)) return ret try: z.__name__ = f.__name__ @@ -4918,7 +5294,7 @@ def z(*paArgs): # # global helpers # -def delimitedList( expr, delim=",", combine=False ): +def delimitedList(expr, delim=",", combine=False): """Helper to define a delimited list of expressions - the delimiter defaults to ','. By default, the list elements and delimiters can have intervening whitespace, and comments, but this can be @@ -4933,13 +5309,13 @@ def delimitedList( expr, delim=",", combine=False ): delimitedList(Word(alphas)).parseString("aa,bb,cc") # -> ['aa', 'bb', 'cc'] delimitedList(Word(hexnums), delim=':', combine=True).parseString("AA:BB:CC:DD:EE") # -> ['AA:BB:CC:DD:EE'] """ - dlName = _ustr(expr)+" ["+_ustr(delim)+" "+_ustr(expr)+"]..." + dlName = _ustr(expr) + " [" + _ustr(delim) + " " + _ustr(expr) + "]..." if combine: - return Combine( expr + ZeroOrMore( delim + expr ) ).setName(dlName) + return Combine(expr + ZeroOrMore(delim + expr)).setName(dlName) else: - return ( expr + ZeroOrMore( Suppress( delim ) + expr ) ).setName(dlName) + return (expr + ZeroOrMore(Suppress(delim) + expr)).setName(dlName) -def countedArray( expr, intExpr=None ): +def countedArray(expr, intExpr=None): """Helper to define a counted list of expressions. This helper defines a pattern of the form:: @@ -4963,22 +5339,22 @@ def countedArray( expr, intExpr=None ): countedArray(Word(alphas), intExpr=binaryConstant).parseString('10 ab cd ef') # -> ['ab', 'cd'] """ arrayExpr = Forward() - def countFieldParseAction(s,l,t): + def countFieldParseAction(s, l, t): n = t[0] - arrayExpr << (n and Group(And([expr]*n)) or Group(empty)) + arrayExpr << (n and Group(And([expr] * n)) or Group(empty)) return [] if intExpr is None: - intExpr = Word(nums).setParseAction(lambda t:int(t[0])) + intExpr = Word(nums).setParseAction(lambda t: int(t[0])) else: intExpr = intExpr.copy() intExpr.setName("arrayLen") intExpr.addParseAction(countFieldParseAction, callDuringTry=True) - return ( intExpr + arrayExpr ).setName('(len) ' + _ustr(expr) + '...') + return (intExpr + arrayExpr).setName('(len) ' + _ustr(expr) + '...') def _flatten(L): ret = [] for i in L: - if isinstance(i,list): + if isinstance(i, list): ret.extend(_flatten(i)) else: ret.append(i) @@ -5000,7 +5376,7 @@ def matchPreviousLiteral(expr): enabled. """ rep = Forward() - def copyTokenToRepeater(s,l,t): + def copyTokenToRepeater(s, l, t): if t: if len(t) == 1: rep << t[0] @@ -5032,26 +5408,26 @@ def matchPreviousExpr(expr): rep = Forward() e2 = expr.copy() rep <<= e2 - def copyTokenToRepeater(s,l,t): + def copyTokenToRepeater(s, l, t): matchTokens = _flatten(t.asList()) - def mustMatchTheseTokens(s,l,t): + def mustMatchTheseTokens(s, l, t): theseTokens = _flatten(t.asList()) - if theseTokens != matchTokens: - raise ParseException("",0,"") - rep.setParseAction( mustMatchTheseTokens, callDuringTry=True ) + if theseTokens != matchTokens: + raise ParseException('', 0, '') + rep.setParseAction(mustMatchTheseTokens, callDuringTry=True) expr.addParseAction(copyTokenToRepeater, callDuringTry=True) rep.setName('(prev) ' + _ustr(expr)) return rep def _escapeRegexRangeChars(s): - #~ escape these chars: ^-] + # ~ escape these chars: ^-] for c in r"\^-]": - s = s.replace(c,_bslash+c) - s = s.replace("\n",r"\n") - s = s.replace("\t",r"\t") + s = s.replace(c, _bslash + c) + s = s.replace("\n", r"\n") + s = s.replace("\t", r"\t") return _ustr(s) -def oneOf( strs, caseless=False, useRegex=True ): +def oneOf(strs, caseless=False, useRegex=True, asKeyword=False): """Helper to quickly define a set of alternative Literals, and makes sure to do longest-first testing when there is a conflict, regardless of the input order, but returns @@ -5065,8 +5441,10 @@ def oneOf( strs, caseless=False, useRegex=True ): caseless - useRegex - (default= ``True``) - as an optimization, will generate a Regex object; otherwise, will generate - a :class:`MatchFirst` object (if ``caseless=True``, or if + a :class:`MatchFirst` object (if ``caseless=True`` or ``asKeyword=True``, or if creating a :class:`Regex` raises an exception) + - asKeyword - (default=``False``) - enforce Keyword-style matching on the + generated expressions Example:: @@ -5081,57 +5459,62 @@ def oneOf( strs, caseless=False, useRegex=True ): [['B', '=', '12'], ['AA', '=', '23'], ['B', '<=', 'AA'], ['AA', '>', '12']] """ + if isinstance(caseless, basestring): + warnings.warn("More than one string argument passed to oneOf, pass " + "choices as a list or space-delimited string", stacklevel=2) + if caseless: - isequal = ( lambda a,b: a.upper() == b.upper() ) - masks = ( lambda a,b: b.upper().startswith(a.upper()) ) - parseElementClass = CaselessLiteral + isequal = (lambda a, b: a.upper() == b.upper()) + masks = (lambda a, b: b.upper().startswith(a.upper())) + parseElementClass = CaselessKeyword if asKeyword else CaselessLiteral else: - isequal = ( lambda a,b: a == b ) - masks = ( lambda a,b: b.startswith(a) ) - parseElementClass = Literal + isequal = (lambda a, b: a == b) + masks = (lambda a, b: b.startswith(a)) + parseElementClass = Keyword if asKeyword else Literal symbols = [] - if isinstance(strs,basestring): + if isinstance(strs, basestring): symbols = strs.split() elif isinstance(strs, Iterable): symbols = list(strs) else: warnings.warn("Invalid argument to oneOf, expected string or iterable", - SyntaxWarning, stacklevel=2) + SyntaxWarning, stacklevel=2) if not symbols: return NoMatch() - i = 0 - while i < len(symbols)-1: - cur = symbols[i] - for j,other in enumerate(symbols[i+1:]): - if ( isequal(other, cur) ): - del symbols[i+j+1] - break - elif ( masks(cur, other) ): - del symbols[i+j+1] - symbols.insert(i,other) - cur = other - break - else: - i += 1 + if not asKeyword: + # if not producing keywords, need to reorder to take care to avoid masking + # longer choices with shorter ones + i = 0 + while i < len(symbols) - 1: + cur = symbols[i] + for j, other in enumerate(symbols[i + 1:]): + if isequal(other, cur): + del symbols[i + j + 1] + break + elif masks(cur, other): + del symbols[i + j + 1] + symbols.insert(i, other) + break + else: + i += 1 - if not caseless and useRegex: - #~ print (strs,"->", "|".join( [ _escapeRegexChars(sym) for sym in symbols] )) + if not (caseless or asKeyword) and useRegex: + # ~ print (strs, "->", "|".join([_escapeRegexChars(sym) for sym in symbols])) try: - if len(symbols)==len("".join(symbols)): - return Regex( "[%s]" % "".join(_escapeRegexRangeChars(sym) for sym in symbols) ).setName(' | '.join(symbols)) + if len(symbols) == len("".join(symbols)): + return Regex("[%s]" % "".join(_escapeRegexRangeChars(sym) for sym in symbols)).setName(' | '.join(symbols)) else: - return Regex( "|".join(re.escape(sym) for sym in symbols) ).setName(' | '.join(symbols)) + return Regex("|".join(re.escape(sym) for sym in symbols)).setName(' | '.join(symbols)) except Exception: warnings.warn("Exception creating Regex for oneOf, building MatchFirst", SyntaxWarning, stacklevel=2) - # last resort, just use MatchFirst return MatchFirst(parseElementClass(sym) for sym in symbols).setName(' | '.join(symbols)) -def dictOf( key, value ): +def dictOf(key, value): """Helper to easily and clearly define a dictionary by specifying the respective patterns for the key and value. Takes care of defining the :class:`Dict`, :class:`ZeroOrMore`, and @@ -5189,8 +5572,8 @@ def originalTextFor(expr, asString=True): Example:: src = "this is test <b> bold <i>text</i> </b> normal text " - for tag in ("b","i"): - opener,closer = makeHTMLTags(tag) + for tag in ("b", "i"): + opener, closer = makeHTMLTags(tag) patt = originalTextFor(opener + SkipTo(closer) + closer) print(patt.searchString(src)[0]) @@ -5199,14 +5582,14 @@ def originalTextFor(expr, asString=True): ['<b> bold <i>text</i> </b>'] ['<i>text</i>'] """ - locMarker = Empty().setParseAction(lambda s,loc,t: loc) + locMarker = Empty().setParseAction(lambda s, loc, t: loc) endlocMarker = locMarker.copy() endlocMarker.callPreparse = False matchExpr = locMarker("_original_start") + expr + endlocMarker("_original_end") if asString: - extractText = lambda s,l,t: s[t._original_start:t._original_end] + extractText = lambda s, l, t: s[t._original_start: t._original_end] else: - def extractText(s,l,t): + def extractText(s, l, t): t[:] = [s[t.pop('_original_start'):t.pop('_original_end')]] matchExpr.setParseAction(extractText) matchExpr.ignoreExprs = expr.ignoreExprs @@ -5216,7 +5599,7 @@ def ungroup(expr): """Helper to undo pyparsing's default grouping of And expressions, even if all but one are non-empty. """ - return TokenConverter(expr).addParseAction(lambda t:t[0]) + return TokenConverter(expr).addParseAction(lambda t: t[0]) def locatedExpr(expr): """Helper to decorate a returned token with its starting and ending @@ -5243,7 +5626,7 @@ def locatedExpr(expr): [[8, 'lksdjjf', 15]] [[18, 'lkkjj', 23]] """ - locator = Empty().setParseAction(lambda s,l,t: l) + locator = Empty().setParseAction(lambda s, l, t: l) return Group(locator("locn_start") + expr("value") + locator.copy().leaveWhitespace()("locn_end")) @@ -5254,12 +5637,12 @@ def locatedExpr(expr): stringStart = StringStart().setName("stringStart") stringEnd = StringEnd().setName("stringEnd") -_escapedPunc = Word( _bslash, r"\[]-*.$+^?()~ ", exact=2 ).setParseAction(lambda s,l,t:t[0][1]) -_escapedHexChar = Regex(r"\\0?[xX][0-9a-fA-F]+").setParseAction(lambda s,l,t:unichr(int(t[0].lstrip(r'\0x'),16))) -_escapedOctChar = Regex(r"\\0[0-7]+").setParseAction(lambda s,l,t:unichr(int(t[0][1:],8))) +_escapedPunc = Word(_bslash, r"\[]-*.$+^?()~ ", exact=2).setParseAction(lambda s, l, t: t[0][1]) +_escapedHexChar = Regex(r"\\0?[xX][0-9a-fA-F]+").setParseAction(lambda s, l, t: unichr(int(t[0].lstrip(r'\0x'), 16))) +_escapedOctChar = Regex(r"\\0[0-7]+").setParseAction(lambda s, l, t: unichr(int(t[0][1:], 8))) _singleChar = _escapedPunc | _escapedHexChar | _escapedOctChar | CharsNotIn(r'\]', exact=1) _charRange = Group(_singleChar + Suppress("-") + _singleChar) -_reBracketExpr = Literal("[") + Optional("^").setResultsName("negate") + Group( OneOrMore( _charRange | _singleChar ) ).setResultsName("body") + "]" +_reBracketExpr = Literal("[") + Optional("^").setResultsName("negate") + Group(OneOrMore(_charRange | _singleChar)).setResultsName("body") + "]" def srange(s): r"""Helper to easily define string ranges for use in Word @@ -5287,7 +5670,7 @@ def srange(s): - any combination of the above (``'aeiouy'``, ``'a-zA-Z0-9_$'``, etc.) """ - _expanded = lambda p: p if not isinstance(p,ParseResults) else ''.join(unichr(c) for c in range(ord(p[0]),ord(p[1])+1)) + _expanded = lambda p: p if not isinstance(p, ParseResults) else ''.join(unichr(c) for c in range(ord(p[0]), ord(p[1]) + 1)) try: return "".join(_expanded(part) for part in _reBracketExpr.parseString(s).body) except Exception: @@ -5297,9 +5680,9 @@ def matchOnlyAtCol(n): """Helper method for defining parse actions that require matching at a specific column in the input text. """ - def verifyCol(strg,locn,toks): - if col(locn,strg) != n: - raise ParseException(strg,locn,"matched token not at column %d" % n) + def verifyCol(strg, locn, toks): + if col(locn, strg) != n: + raise ParseException(strg, locn, "matched token not at column %d" % n) return verifyCol def replaceWith(replStr): @@ -5315,9 +5698,9 @@ def replaceWith(replStr): OneOrMore(term).parseString("324 234 N/A 234") # -> [324, 234, nan, 234] """ - return lambda s,l,t: [replStr] + return lambda s, l, t: [replStr] -def removeQuotes(s,l,t): +def removeQuotes(s, l, t): """Helper parse action for removing quotation marks from parsed quoted strings. @@ -5368,7 +5751,7 @@ def tokenMap(func, *args): now is the winter of our discontent made glorious summer by this sun of york ['Now Is The Winter Of Our Discontent Made Glorious Summer By This Sun Of York'] """ - def pa(s,l,t): + def pa(s, l, t): return [func(tokn, *args) for tokn in t] try: @@ -5392,34 +5775,34 @@ def _makeTags(tagStr, xml, suppress_LT=Suppress("<"), suppress_GT=Suppress(">")): """Internal helper to construct opening and closing tag expressions, given a tag name""" - if isinstance(tagStr,basestring): + if isinstance(tagStr, basestring): resname = tagStr tagStr = Keyword(tagStr, caseless=not xml) else: resname = tagStr.name - tagAttrName = Word(alphas,alphanums+"_-:") - if (xml): - tagAttrValue = dblQuotedString.copy().setParseAction( removeQuotes ) + tagAttrName = Word(alphas, alphanums + "_-:") + if xml: + tagAttrValue = dblQuotedString.copy().setParseAction(removeQuotes) openTag = (suppress_LT + tagStr("tag") - + Dict(ZeroOrMore(Group(tagAttrName + Suppress("=") + tagAttrValue ))) - + Optional("/", default=[False])("empty").setParseAction(lambda s,l,t:t[0]=='/') + + Dict(ZeroOrMore(Group(tagAttrName + Suppress("=") + tagAttrValue))) + + Optional("/", default=[False])("empty").setParseAction(lambda s, l, t: t[0] == '/') + suppress_GT) else: - tagAttrValue = quotedString.copy().setParseAction( removeQuotes ) | Word(printables, excludeChars=">") + tagAttrValue = quotedString.copy().setParseAction(removeQuotes) | Word(printables, excludeChars=">") openTag = (suppress_LT + tagStr("tag") + Dict(ZeroOrMore(Group(tagAttrName.setParseAction(downcaseTokens) + Optional(Suppress("=") + tagAttrValue)))) - + Optional("/",default=[False])("empty").setParseAction(lambda s,l,t:t[0]=='/') + + Optional("/", default=[False])("empty").setParseAction(lambda s, l, t: t[0] == '/') + suppress_GT) closeTag = Combine(_L("</") + tagStr + ">", adjacent=False) openTag.setName("<%s>" % resname) # add start<tagname> results name in parse action now that ungrouped names are not reported at two levels - openTag.addParseAction(lambda t: t.__setitem__("start"+"".join(resname.replace(":"," ").title().split()), t.copy())) - closeTag = closeTag("end"+"".join(resname.replace(":"," ").title().split())).setName("</%s>" % resname) + openTag.addParseAction(lambda t: t.__setitem__("start" + "".join(resname.replace(":", " ").title().split()), t.copy())) + closeTag = closeTag("end" + "".join(resname.replace(":", " ").title().split())).setName("</%s>" % resname) openTag.tag = resname closeTag.tag = resname openTag.tag_body = SkipTo(closeTag()) @@ -5435,7 +5818,7 @@ def makeHTMLTags(tagStr): text = '<td>More info at the <a href="https://github.com/pyparsing/pyparsing/wiki">pyparsing</a> wiki page</td>' # makeHTMLTags returns pyparsing expressions for the opening and # closing tags as a 2-tuple - a,a_end = makeHTMLTags("A") + a, a_end = makeHTMLTags("A") link_expr = a + SkipTo(a_end)("link_text") + a_end for link in link_expr.searchString(text): @@ -5447,7 +5830,7 @@ def makeHTMLTags(tagStr): pyparsing -> https://github.com/pyparsing/pyparsing/wiki """ - return _makeTags( tagStr, False ) + return _makeTags(tagStr, False) def makeXMLTags(tagStr): """Helper to construct opening and closing tag expressions for XML, @@ -5455,9 +5838,9 @@ def makeXMLTags(tagStr): Example: similar to :class:`makeHTMLTags` """ - return _makeTags( tagStr, True ) + return _makeTags(tagStr, True) -def withAttribute(*args,**attrDict): +def withAttribute(*args, **attrDict): """Helper to create a validating parse action to be used with start tags created with :class:`makeXMLTags` or :class:`makeHTMLTags`. Use ``withAttribute`` to qualify @@ -5470,7 +5853,7 @@ def withAttribute(*args,**attrDict): - keyword arguments, as in ``(align="right")``, or - as an explicit dict with ``**`` operator, when an attribute name is also a Python reserved word, as in ``**{"class":"Customer", "align":"right"}`` - - a list of name-value tuples, as in ``(("ns1:class", "Customer"), ("ns2:align","right"))`` + - a list of name-value tuples, as in ``(("ns1:class", "Customer"), ("ns2:align", "right"))`` For attribute names with a namespace prefix, you must use the second form. Attribute names are matched insensitive to upper/lower case. @@ -5517,13 +5900,13 @@ def withAttribute(*args,**attrDict): attrs = args[:] else: attrs = attrDict.items() - attrs = [(k,v) for k,v in attrs] - def pa(s,l,tokens): - for attrName,attrValue in attrs: + attrs = [(k, v) for k, v in attrs] + def pa(s, l, tokens): + for attrName, attrValue in attrs: if attrName not in tokens: - raise ParseException(s,l,"no matching attribute " + attrName) + raise ParseException(s, l, "no matching attribute " + attrName) if attrValue != withAttribute.ANY_VALUE and tokens[attrName] != attrValue: - raise ParseException(s,l,"attribute '%s' has value '%s', must be '%s'" % + raise ParseException(s, l, "attribute '%s' has value '%s', must be '%s'" % (attrName, tokens[attrName], attrValue)) return pa withAttribute.ANY_VALUE = object() @@ -5564,13 +5947,13 @@ def withClass(classname, namespace=''): 1,3 2,3 1,1 """ classattr = "%s:class" % namespace if namespace else "class" - return withAttribute(**{classattr : classname}) + return withAttribute(**{classattr: classname}) opAssoc = SimpleNamespace() opAssoc.LEFT = object() opAssoc.RIGHT = object() -def infixNotation( baseExpr, opList, lpar=Suppress('('), rpar=Suppress(')') ): +def infixNotation(baseExpr, opList, lpar=Suppress('('), rpar=Suppress(')')): """Helper method for constructing grammars of expressions made up of operators working in a precedence hierarchy. Operators may be unary or binary, left- or right-associative. Parse actions can also be @@ -5648,9 +6031,9 @@ def parseImpl(self, instring, loc, doActions=True): return loc, [] ret = Forward() - lastExpr = baseExpr | ( lpar + ret + rpar ) - for i,operDef in enumerate(opList): - opExpr,arity,rightLeftAssoc,pa = (operDef + (None,))[:4] + lastExpr = baseExpr | (lpar + ret + rpar) + for i, operDef in enumerate(opList): + opExpr, arity, rightLeftAssoc, pa = (operDef + (None, ))[:4] termName = "%s term" % opExpr if arity < 3 else "%s%s term" % opExpr if arity == 3: if opExpr is None or len(opExpr) != 2: @@ -5660,15 +6043,15 @@ def parseImpl(self, instring, loc, doActions=True): thisExpr = Forward().setName(termName) if rightLeftAssoc == opAssoc.LEFT: if arity == 1: - matchExpr = _FB(lastExpr + opExpr) + Group( lastExpr + OneOrMore( opExpr ) ) + matchExpr = _FB(lastExpr + opExpr) + Group(lastExpr + OneOrMore(opExpr)) elif arity == 2: if opExpr is not None: - matchExpr = _FB(lastExpr + opExpr + lastExpr) + Group( lastExpr + OneOrMore( opExpr + lastExpr ) ) + matchExpr = _FB(lastExpr + opExpr + lastExpr) + Group(lastExpr + OneOrMore(opExpr + lastExpr)) else: - matchExpr = _FB(lastExpr+lastExpr) + Group( lastExpr + OneOrMore(lastExpr) ) + matchExpr = _FB(lastExpr + lastExpr) + Group(lastExpr + OneOrMore(lastExpr)) elif arity == 3: - matchExpr = _FB(lastExpr + opExpr1 + lastExpr + opExpr2 + lastExpr) + \ - Group( lastExpr + opExpr1 + lastExpr + opExpr2 + lastExpr ) + matchExpr = (_FB(lastExpr + opExpr1 + lastExpr + opExpr2 + lastExpr) + + Group(lastExpr + opExpr1 + lastExpr + opExpr2 + lastExpr)) else: raise ValueError("operator must be unary (1), binary (2), or ternary (3)") elif rightLeftAssoc == opAssoc.RIGHT: @@ -5676,15 +6059,15 @@ def parseImpl(self, instring, loc, doActions=True): # try to avoid LR with this extra test if not isinstance(opExpr, Optional): opExpr = Optional(opExpr) - matchExpr = _FB(opExpr.expr + thisExpr) + Group( opExpr + thisExpr ) + matchExpr = _FB(opExpr.expr + thisExpr) + Group(opExpr + thisExpr) elif arity == 2: if opExpr is not None: - matchExpr = _FB(lastExpr + opExpr + thisExpr) + Group( lastExpr + OneOrMore( opExpr + thisExpr ) ) + matchExpr = _FB(lastExpr + opExpr + thisExpr) + Group(lastExpr + OneOrMore(opExpr + thisExpr)) else: - matchExpr = _FB(lastExpr + thisExpr) + Group( lastExpr + OneOrMore( thisExpr ) ) + matchExpr = _FB(lastExpr + thisExpr) + Group(lastExpr + OneOrMore(thisExpr)) elif arity == 3: - matchExpr = _FB(lastExpr + opExpr1 + thisExpr + opExpr2 + thisExpr) + \ - Group( lastExpr + opExpr1 + thisExpr + opExpr2 + thisExpr ) + matchExpr = (_FB(lastExpr + opExpr1 + thisExpr + opExpr2 + thisExpr) + + Group(lastExpr + opExpr1 + thisExpr + opExpr2 + thisExpr)) else: raise ValueError("operator must be unary (1), binary (2), or ternary (3)") else: @@ -5694,7 +6077,7 @@ def parseImpl(self, instring, loc, doActions=True): matchExpr.setParseAction(*pa) else: matchExpr.setParseAction(pa) - thisExpr <<= ( matchExpr.setName(termName) | lastExpr ) + thisExpr <<= (matchExpr.setName(termName) | lastExpr) lastExpr = thisExpr ret <<= lastExpr return ret @@ -5703,10 +6086,10 @@ def parseImpl(self, instring, loc, doActions=True): """(Deprecated) Former name of :class:`infixNotation`, will be dropped in a future release.""" -dblQuotedString = Combine(Regex(r'"(?:[^"\n\r\\]|(?:"")|(?:\\(?:[^x]|x[0-9a-fA-F]+)))*')+'"').setName("string enclosed in double quotes") -sglQuotedString = Combine(Regex(r"'(?:[^'\n\r\\]|(?:'')|(?:\\(?:[^x]|x[0-9a-fA-F]+)))*")+"'").setName("string enclosed in single quotes") -quotedString = Combine(Regex(r'"(?:[^"\n\r\\]|(?:"")|(?:\\(?:[^x]|x[0-9a-fA-F]+)))*')+'"'| - Regex(r"'(?:[^'\n\r\\]|(?:'')|(?:\\(?:[^x]|x[0-9a-fA-F]+)))*")+"'").setName("quotedString using single or double quotes") +dblQuotedString = Combine(Regex(r'"(?:[^"\n\r\\]|(?:"")|(?:\\(?:[^x]|x[0-9a-fA-F]+)))*') + '"').setName("string enclosed in double quotes") +sglQuotedString = Combine(Regex(r"'(?:[^'\n\r\\]|(?:'')|(?:\\(?:[^x]|x[0-9a-fA-F]+)))*") + "'").setName("string enclosed in single quotes") +quotedString = Combine(Regex(r'"(?:[^"\n\r\\]|(?:"")|(?:\\(?:[^x]|x[0-9a-fA-F]+)))*') + '"' + | Regex(r"'(?:[^'\n\r\\]|(?:'')|(?:\\(?:[^x]|x[0-9a-fA-F]+)))*") + "'").setName("quotedString using single or double quotes") unicodeString = Combine(_L('u') + quotedString.copy()).setName("unicode string literal") def nestedExpr(opener="(", closer=")", content=None, ignoreExpr=quotedString.copy()): @@ -5742,7 +6125,7 @@ def nestedExpr(opener="(", closer=")", content=None, ignoreExpr=quotedString.cop ident = Word(alphas+'_', alphanums+'_') number = pyparsing_common.number arg = Group(decl_data_type + ident) - LPAR,RPAR = map(Suppress, "()") + LPAR, RPAR = map(Suppress, "()") code_body = nestedExpr('{', '}', ignoreExpr=(quotedString | cStyleComment)) @@ -5777,33 +6160,40 @@ def nestedExpr(opener="(", closer=")", content=None, ignoreExpr=quotedString.cop if opener == closer: raise ValueError("opening and closing strings cannot be the same") if content is None: - if isinstance(opener,basestring) and isinstance(closer,basestring): - if len(opener) == 1 and len(closer)==1: + if isinstance(opener, basestring) and isinstance(closer, basestring): + if len(opener) == 1 and len(closer) == 1: if ignoreExpr is not None: - content = (Combine(OneOrMore(~ignoreExpr + - CharsNotIn(opener+closer+ParserElement.DEFAULT_WHITE_CHARS,exact=1)) - ).setParseAction(lambda t:t[0].strip())) + content = (Combine(OneOrMore(~ignoreExpr + + CharsNotIn(opener + + closer + + ParserElement.DEFAULT_WHITE_CHARS, exact=1) + ) + ).setParseAction(lambda t: t[0].strip())) else: - content = (empty.copy()+CharsNotIn(opener+closer+ParserElement.DEFAULT_WHITE_CHARS - ).setParseAction(lambda t:t[0].strip())) + content = (empty.copy() + CharsNotIn(opener + + closer + + ParserElement.DEFAULT_WHITE_CHARS + ).setParseAction(lambda t: t[0].strip())) else: if ignoreExpr is not None: - content = (Combine(OneOrMore(~ignoreExpr + - ~Literal(opener) + ~Literal(closer) + - CharsNotIn(ParserElement.DEFAULT_WHITE_CHARS,exact=1)) - ).setParseAction(lambda t:t[0].strip())) + content = (Combine(OneOrMore(~ignoreExpr + + ~Literal(opener) + + ~Literal(closer) + + CharsNotIn(ParserElement.DEFAULT_WHITE_CHARS, exact=1)) + ).setParseAction(lambda t: t[0].strip())) else: - content = (Combine(OneOrMore(~Literal(opener) + ~Literal(closer) + - CharsNotIn(ParserElement.DEFAULT_WHITE_CHARS,exact=1)) - ).setParseAction(lambda t:t[0].strip())) + content = (Combine(OneOrMore(~Literal(opener) + + ~Literal(closer) + + CharsNotIn(ParserElement.DEFAULT_WHITE_CHARS, exact=1)) + ).setParseAction(lambda t: t[0].strip())) else: raise ValueError("opening and closing arguments must be strings if no content expression is given") ret = Forward() if ignoreExpr is not None: - ret <<= Group( Suppress(opener) + ZeroOrMore( ignoreExpr | ret | content ) + Suppress(closer) ) + ret <<= Group(Suppress(opener) + ZeroOrMore(ignoreExpr | ret | content) + Suppress(closer)) else: - ret <<= Group( Suppress(opener) + ZeroOrMore( ret | content ) + Suppress(closer) ) - ret.setName('nested %s%s expression' % (opener,closer)) + ret <<= Group(Suppress(opener) + ZeroOrMore(ret | content) + Suppress(closer)) + ret.setName('nested %s%s expression' % (opener, closer)) return ret def indentedBlock(blockStatementExpr, indentStack, indent=True): @@ -5818,7 +6208,7 @@ def indentedBlock(blockStatementExpr, indentStack, indent=True): (multiple statementWithIndentedBlock expressions within a single grammar should share a common indentStack) - indent - boolean indicating whether block must be indented beyond - the the current level; set to False for block of left-most + the current level; set to False for block of left-most statements (default= ``True``) A valid block must contain at least one ``blockStatement``. @@ -5851,15 +6241,15 @@ def eggs(z): stmt = Forward() identifier = Word(alphas, alphanums) - funcDecl = ("def" + identifier + Group( "(" + Optional( delimitedList(identifier) ) + ")" ) + ":") + funcDecl = ("def" + identifier + Group("(" + Optional(delimitedList(identifier)) + ")") + ":") func_body = indentedBlock(stmt, indentStack) - funcDef = Group( funcDecl + func_body ) + funcDef = Group(funcDecl + func_body) rvalue = Forward() funcCall = Group(identifier + "(" + Optional(delimitedList(rvalue)) + ")") rvalue << (funcCall | identifier | Word(nums)) assignment = Group(identifier + "=" + rvalue) - stmt << ( funcDef | assignment | identifier ) + stmt << (funcDef | assignment | identifier) module_body = OneOrMore(stmt) @@ -5892,39 +6282,42 @@ def eggs(z): def reset_stack(): indentStack[:] = backup_stack - def checkPeerIndent(s,l,t): + def checkPeerIndent(s, l, t): if l >= len(s): return - curCol = col(l,s) + curCol = col(l, s) if curCol != indentStack[-1]: if curCol > indentStack[-1]: - raise ParseException(s,l,"illegal nesting") - raise ParseException(s,l,"not a peer entry") + raise ParseException(s, l, "illegal nesting") + raise ParseException(s, l, "not a peer entry") - def checkSubIndent(s,l,t): - curCol = col(l,s) + def checkSubIndent(s, l, t): + curCol = col(l, s) if curCol > indentStack[-1]: - indentStack.append( curCol ) + indentStack.append(curCol) else: - raise ParseException(s,l,"not a subentry") + raise ParseException(s, l, "not a subentry") - def checkUnindent(s,l,t): + def checkUnindent(s, l, t): if l >= len(s): return - curCol = col(l,s) - if not(indentStack and curCol < indentStack[-1] and curCol <= indentStack[-2]): - raise ParseException(s,l,"not an unindent") - indentStack.pop() + curCol = col(l, s) + if not(indentStack and curCol in indentStack): + raise ParseException(s, l, "not an unindent") + if curCol < indentStack[-1]: + indentStack.pop() NL = OneOrMore(LineEnd().setWhitespaceChars("\t ").suppress()) INDENT = (Empty() + Empty().setParseAction(checkSubIndent)).setName('INDENT') PEER = Empty().setParseAction(checkPeerIndent).setName('') UNDENT = Empty().setParseAction(checkUnindent).setName('UNINDENT') if indent: - smExpr = Group( Optional(NL) + - #~ FollowedBy(blockStatementExpr) + - INDENT + (OneOrMore( PEER + Group(blockStatementExpr) + Optional(NL) )) + UNDENT) + smExpr = Group(Optional(NL) + + INDENT + + OneOrMore(PEER + Group(blockStatementExpr) + Optional(NL)) + + UNDENT) else: - smExpr = Group( Optional(NL) + - (OneOrMore( PEER + Group(blockStatementExpr) + Optional(NL) )) ) + smExpr = Group(Optional(NL) + + OneOrMore(PEER + Group(blockStatementExpr) + Optional(NL)) + + UNDENT) smExpr.setFailAction(lambda a, b, c, d: reset_stack()) blockStatementExpr.ignore(_bslash + LineEnd()) return smExpr.setName('indented block') @@ -5932,8 +6325,8 @@ def checkUnindent(s,l,t): alphas8bit = srange(r"[\0xc0-\0xd6\0xd8-\0xf6\0xf8-\0xff]") punc8bit = srange(r"[\0xa1-\0xbf\0xd7\0xf7]") -anyOpenTag,anyCloseTag = makeHTMLTags(Word(alphas,alphanums+"_:").setName('any tag')) -_htmlEntityMap = dict(zip("gt lt amp nbsp quot apos".split(),'><& "\'')) +anyOpenTag, anyCloseTag = makeHTMLTags(Word(alphas, alphanums + "_:").setName('any tag')) +_htmlEntityMap = dict(zip("gt lt amp nbsp quot apos".split(), '><& "\'')) commonHTMLEntity = Regex('&(?P<entity>' + '|'.join(_htmlEntityMap.keys()) +");").setName("common HTML entity") def replaceHTMLEntity(t): """Helper parser action to replace common HTML entities with their special characters""" @@ -5950,7 +6343,7 @@ def replaceHTMLEntity(t): dblSlashComment = Regex(r"//(?:\\\n|[^\n])*").setName("// comment") "Comment of the form ``// ... (to end of line)``" -cppStyleComment = Combine(Regex(r"/\*(?:[^*]|\*(?!/))*") + '*/'| dblSlashComment).setName("C++ style comment") +cppStyleComment = Combine(Regex(r"/\*(?:[^*]|\*(?!/))*") + '*/' | dblSlashComment).setName("C++ style comment") "Comment of either form :class:`cStyleComment` or :class:`dblSlashComment`" javaStyleComment = cppStyleComment @@ -5959,10 +6352,10 @@ def replaceHTMLEntity(t): pythonStyleComment = Regex(r"#.*").setName("Python style comment") "Comment of the form ``# ... (to end of line)``" -_commasepitem = Combine(OneOrMore(Word(printables, excludeChars=',') + - Optional( Word(" \t") + - ~Literal(",") + ~LineEnd() ) ) ).streamline().setName("commaItem") -commaSeparatedList = delimitedList( Optional( quotedString.copy() | _commasepitem, default="") ).setName("commaSeparatedList") +_commasepitem = Combine(OneOrMore(Word(printables, excludeChars=',') + + Optional(Word(" \t") + + ~Literal(",") + ~LineEnd()))).streamline().setName("commaItem") +commaSeparatedList = delimitedList(Optional(quotedString.copy() | _commasepitem, default="")).setName("commaSeparatedList") """(Deprecated) Predefined expression of 1 or more printable words or quoted strings, separated by commas. @@ -6128,7 +6521,7 @@ class pyparsing_common: integer = Word(nums).setName("integer").setParseAction(convertToInteger) """expression that parses an unsigned integer, returns an int""" - hex_integer = Word(hexnums).setName("hex integer").setParseAction(tokenMap(int,16)) + hex_integer = Word(hexnums).setName("hex integer").setParseAction(tokenMap(int, 16)) """expression that parses a hexadecimal integer, returns an int""" signed_integer = Regex(r'[+-]?\d+').setName("signed integer").setParseAction(convertToInteger) @@ -6142,10 +6535,10 @@ class pyparsing_common: """mixed integer of the form 'integer - fraction', with optional leading integer, returns float""" mixed_integer.addParseAction(sum) - real = Regex(r'[+-]?\d+\.\d*').setName("real number").setParseAction(convertToFloat) + real = Regex(r'[+-]?(:?\d+\.\d*|\.\d+)').setName("real number").setParseAction(convertToFloat) """expression that parses a floating point number and returns a float""" - sci_real = Regex(r'[+-]?\d+([eE][+-]?\d+|\.\d*([eE][+-]?\d+)?)').setName("real number with scientific notation").setParseAction(convertToFloat) + sci_real = Regex(r'[+-]?(:?\d+(:?[eE][+-]?\d+)|(:?\d+\.\d*|\.\d+)(:?[eE][+-]?\d+)?)').setName("real number with scientific notation").setParseAction(convertToFloat) """expression that parses a floating point number with optional scientific notation and returns a float""" @@ -6156,15 +6549,18 @@ class pyparsing_common: fnumber = Regex(r'[+-]?\d+\.?\d*([eE][+-]?\d+)?').setName("fnumber").setParseAction(convertToFloat) """any int or real number, returned as float""" - identifier = Word(alphas+'_', alphanums+'_').setName("identifier") + identifier = Word(alphas + '_', alphanums + '_').setName("identifier") """typical code identifier (leading alpha or '_', followed by 0 or more alphas, nums, or '_')""" ipv4_address = Regex(r'(25[0-5]|2[0-4][0-9]|1?[0-9]{1,2})(\.(25[0-5]|2[0-4][0-9]|1?[0-9]{1,2})){3}').setName("IPv4 address") "IPv4 address (``0.0.0.0 - 255.255.255.255``)" _ipv6_part = Regex(r'[0-9a-fA-F]{1,4}').setName("hex_integer") - _full_ipv6_address = (_ipv6_part + (':' + _ipv6_part)*7).setName("full IPv6 address") - _short_ipv6_address = (Optional(_ipv6_part + (':' + _ipv6_part)*(0,6)) + "::" + Optional(_ipv6_part + (':' + _ipv6_part)*(0,6))).setName("short IPv6 address") + _full_ipv6_address = (_ipv6_part + (':' + _ipv6_part) * 7).setName("full IPv6 address") + _short_ipv6_address = (Optional(_ipv6_part + (':' + _ipv6_part) * (0, 6)) + + "::" + + Optional(_ipv6_part + (':' + _ipv6_part) * (0, 6)) + ).setName("short IPv6 address") _short_ipv6_address.addCondition(lambda t: sum(1 for tt in t if pyparsing_common._ipv6_part.matches(tt)) < 8) _mixed_ipv6_address = ("::ffff:" + ipv4_address).setName("mixed IPv6 address") ipv6_address = Combine((_full_ipv6_address | _mixed_ipv6_address | _short_ipv6_address).setName("IPv6 address")).setName("IPv6 address") @@ -6191,7 +6587,7 @@ def convertToDate(fmt="%Y-%m-%d"): [datetime.date(1999, 12, 31)] """ - def cvt_fn(s,l,t): + def cvt_fn(s, l, t): try: return datetime.strptime(t[0], fmt).date() except ValueError as ve: @@ -6216,7 +6612,7 @@ def convertToDatetime(fmt="%Y-%m-%dT%H:%M:%S.%f"): [datetime.datetime(1999, 12, 31, 23, 59, 59, 999000)] """ - def cvt_fn(s,l,t): + def cvt_fn(s, l, t): try: return datetime.strptime(t[0], fmt) except ValueError as ve: @@ -6241,7 +6637,7 @@ def stripHTMLTags(s, l, tokens): # strip HTML links from normal text text = '<td>More info at the <a href="https://github.com/pyparsing/pyparsing/wiki">pyparsing</a> wiki page</td>' - td,td_end = makeHTMLTags("TD") + td, td_end = makeHTMLTags("TD") table_text = td + SkipTo(td_end).setParseAction(pyparsing_common.stripHTMLTags)("body") + td_end print(table_text.parseString(text).body) @@ -6251,9 +6647,13 @@ def stripHTMLTags(s, l, tokens): """ return pyparsing_common._html_stripper.transformString(tokens[0]) - _commasepitem = Combine(OneOrMore(~Literal(",") + ~LineEnd() + Word(printables, excludeChars=',') - + Optional( White(" \t") ) ) ).streamline().setName("commaItem") - comma_separated_list = delimitedList( Optional( quotedString.copy() | _commasepitem, default="") ).setName("comma separated list") + _commasepitem = Combine(OneOrMore(~Literal(",") + + ~LineEnd() + + Word(printables, excludeChars=',') + + Optional(White(" \t")))).streamline().setName("commaItem") + comma_separated_list = delimitedList(Optional(quotedString.copy() + | _commasepitem, default='') + ).setName("comma separated list") """Predefined expression of 1 or more printable words or quoted strings, separated by commas.""" upcaseTokens = staticmethod(tokenMap(lambda t: _ustr(t).upper())) @@ -6272,7 +6672,8 @@ def __init__(self, fn): def __get__(self, obj, cls): if cls is None: cls = type(obj) - if not hasattr(cls, '_intern') or any(cls._intern is getattr(superclass, '_intern', []) for superclass in cls.__mro__[1:]): + if not hasattr(cls, '_intern') or any(cls._intern is getattr(superclass, '_intern', []) + for superclass in cls.__mro__[1:]): cls._intern = {} attrname = self.fn.__name__ if attrname not in cls._intern: @@ -6303,7 +6704,7 @@ def _get_chars_for_ranges(cls): if cc is unicode_set: break for rr in cc._ranges: - ret.extend(range(rr[0], rr[-1]+1)) + ret.extend(range(rr[0], rr[-1] + 1)) return [unichr(c) for c in sorted(set(ret))] @_lazyclassproperty @@ -6359,27 +6760,27 @@ class Cyrillic(unicode_set): class Chinese(unicode_set): "Unicode set for Chinese Unicode Character Range" - _ranges = [(0x4e00, 0x9fff), (0x3000, 0x303f), ] + _ranges = [(0x4e00, 0x9fff), (0x3000, 0x303f),] class Japanese(unicode_set): "Unicode set for Japanese Unicode Character Range, combining Kanji, Hiragana, and Katakana ranges" - _ranges = [ ] + _ranges = [] class Kanji(unicode_set): "Unicode set for Kanji Unicode Character Range" - _ranges = [(0x4E00, 0x9Fbf), (0x3000, 0x303f), ] + _ranges = [(0x4E00, 0x9Fbf), (0x3000, 0x303f),] class Hiragana(unicode_set): "Unicode set for Hiragana Unicode Character Range" - _ranges = [(0x3040, 0x309f), ] + _ranges = [(0x3040, 0x309f),] class Katakana(unicode_set): "Unicode set for Katakana Unicode Character Range" - _ranges = [(0x30a0, 0x30ff), ] + _ranges = [(0x30a0, 0x30ff),] class Korean(unicode_set): "Unicode set for Korean Unicode Character Range" - _ranges = [(0xac00, 0xd7af), (0x1100, 0x11ff), (0x3130, 0x318f), (0xa960, 0xa97f), (0xd7b0, 0xd7ff), (0x3000, 0x303f), ] + _ranges = [(0xac00, 0xd7af), (0x1100, 0x11ff), (0x3130, 0x318f), (0xa960, 0xa97f), (0xd7b0, 0xd7ff), (0x3000, 0x303f),] class CJK(Chinese, Japanese, Korean): "Unicode set for combined Chinese, Japanese, and Korean (CJK) Unicode Character Range" @@ -6387,15 +6788,15 @@ class CJK(Chinese, Japanese, Korean): class Thai(unicode_set): "Unicode set for Thai Unicode Character Range" - _ranges = [(0x0e01, 0x0e3a), (0x0e3f, 0x0e5b), ] + _ranges = [(0x0e01, 0x0e3a), (0x0e3f, 0x0e5b),] class Arabic(unicode_set): "Unicode set for Arabic Unicode Character Range" - _ranges = [(0x0600, 0x061b), (0x061e, 0x06ff), (0x0700, 0x077f), ] + _ranges = [(0x0600, 0x061b), (0x061e, 0x06ff), (0x0700, 0x077f),] class Hebrew(unicode_set): "Unicode set for Hebrew Unicode Character Range" - _ranges = [(0x0590, 0x05ff), ] + _ranges = [(0x0590, 0x05ff),] class Devanagari(unicode_set): "Unicode set for Devanagari Unicode Character Range" @@ -6407,18 +6808,18 @@ class Devanagari(unicode_set): # define ranges in language character sets if PY_3: - setattr(pyparsing_unicode, "العربية", pyparsing_unicode.Arabic) - setattr(pyparsing_unicode, "中文", pyparsing_unicode.Chinese) - setattr(pyparsing_unicode, "кириллица", pyparsing_unicode.Cyrillic) - setattr(pyparsing_unicode, "Ελληνικά", pyparsing_unicode.Greek) - setattr(pyparsing_unicode, "עִברִית", pyparsing_unicode.Hebrew) - setattr(pyparsing_unicode, "日本語", pyparsing_unicode.Japanese) - setattr(pyparsing_unicode.Japanese, "漢字", pyparsing_unicode.Japanese.Kanji) - setattr(pyparsing_unicode.Japanese, "カタカナ", pyparsing_unicode.Japanese.Katakana) - setattr(pyparsing_unicode.Japanese, "ひらがな", pyparsing_unicode.Japanese.Hiragana) - setattr(pyparsing_unicode, "한국어", pyparsing_unicode.Korean) - setattr(pyparsing_unicode, "ไทย", pyparsing_unicode.Thai) - setattr(pyparsing_unicode, "देवनागरी", pyparsing_unicode.Devanagari) + setattr(pyparsing_unicode, u"العربية", pyparsing_unicode.Arabic) + setattr(pyparsing_unicode, u"中文", pyparsing_unicode.Chinese) + setattr(pyparsing_unicode, u"кириллица", pyparsing_unicode.Cyrillic) + setattr(pyparsing_unicode, u"Ελληνικά", pyparsing_unicode.Greek) + setattr(pyparsing_unicode, u"עִברִית", pyparsing_unicode.Hebrew) + setattr(pyparsing_unicode, u"日本語", pyparsing_unicode.Japanese) + setattr(pyparsing_unicode.Japanese, u"漢字", pyparsing_unicode.Japanese.Kanji) + setattr(pyparsing_unicode.Japanese, u"カタカナ", pyparsing_unicode.Japanese.Katakana) + setattr(pyparsing_unicode.Japanese, u"ひらがな", pyparsing_unicode.Japanese.Hiragana) + setattr(pyparsing_unicode, u"한국어", pyparsing_unicode.Korean) + setattr(pyparsing_unicode, u"ไทย", pyparsing_unicode.Thai) + setattr(pyparsing_unicode, u"देवनागरी", pyparsing_unicode.Devanagari) if __name__ == "__main__": diff --git a/src/pip/_vendor/pytoml/parser.py b/src/pip/_vendor/pytoml/parser.py index 3493aa644ca..f074317ff0d 100644 --- a/src/pip/_vendor/pytoml/parser.py +++ b/src/pip/_vendor/pytoml/parser.py @@ -1,4 +1,4 @@ -import string, re, sys, datetime +import re, sys from .core import TomlError from .utils import rfc3339_re, parse_rfc3339_re @@ -28,8 +28,6 @@ def error(msg): def process_value(v, object_pairs_hook): kind, text, value, pos = v - if kind == 'str' and value.startswith('\n'): - value = value[1:] if kind == 'array': if value and any(k != value[0][0] for k, t, v, p in value[1:]): error('array-type-mismatch') @@ -215,6 +213,7 @@ def _p_key(s): return r if s.consume('\''): if s.consume('\'\''): + s.consume('\n') r = s.expect_re(_litstr_ml_re).group(0) s.expect('\'\'\'') else: @@ -238,6 +237,7 @@ def _p_value(s, object_pairs_hook): if s.consume('"'): if s.consume('""'): + s.consume('\n') r = _p_basicstr_content(s, _basicstr_ml_re) s.expect('"""') else: @@ -247,6 +247,7 @@ def _p_value(s, object_pairs_hook): if s.consume('\''): if s.consume('\'\''): + s.consume('\n') r = s.expect_re(_litstr_ml_re).group(0) s.expect('\'\'\'') else: diff --git a/src/pip/_vendor/pytoml/writer.py b/src/pip/_vendor/pytoml/writer.py index 73b5089c241..d2e849f6196 100644 --- a/src/pip/_vendor/pytoml/writer.py +++ b/src/pip/_vendor/pytoml/writer.py @@ -3,6 +3,12 @@ from .utils import format_rfc3339 +try: + from pathlib import PurePath as _path_types +except ImportError: + _path_types = () + + if sys.version_info[0] == 3: long = int unicode = str @@ -66,6 +72,8 @@ def _format_value(v): return '[{0}]'.format(', '.join(_format_value(obj) for obj in v)) elif isinstance(v, dict): return '{{{0}}}'.format(', '.join('{} = {}'.format(_escape_id(k), _format_value(obj)) for k, obj in v.items())) + elif isinstance(v, _path_types): + return _escape_string(str(v)) else: raise RuntimeError(v) diff --git a/src/pip/_vendor/urllib3/__init__.py b/src/pip/_vendor/urllib3/__init__.py index c4c0dde54ab..8f5a21f3462 100644 --- a/src/pip/_vendor/urllib3/__init__.py +++ b/src/pip/_vendor/urllib3/__init__.py @@ -4,11 +4,7 @@ from __future__ import absolute_import import warnings -from .connectionpool import ( - HTTPConnectionPool, - HTTPSConnectionPool, - connection_from_url -) +from .connectionpool import HTTPConnectionPool, HTTPSConnectionPool, connection_from_url from . import exceptions from .filepost import encode_multipart_formdata @@ -24,25 +20,25 @@ import logging from logging import NullHandler -__author__ = 'Andrey Petrov (andrey.petrov@shazow.net)' -__license__ = 'MIT' -__version__ = '1.25.3' +__author__ = "Andrey Petrov (andrey.petrov@shazow.net)" +__license__ = "MIT" +__version__ = "1.25.6" __all__ = ( - 'HTTPConnectionPool', - 'HTTPSConnectionPool', - 'PoolManager', - 'ProxyManager', - 'HTTPResponse', - 'Retry', - 'Timeout', - 'add_stderr_logger', - 'connection_from_url', - 'disable_warnings', - 'encode_multipart_formdata', - 'get_host', - 'make_headers', - 'proxy_from_url', + "HTTPConnectionPool", + "HTTPSConnectionPool", + "PoolManager", + "ProxyManager", + "HTTPResponse", + "Retry", + "Timeout", + "add_stderr_logger", + "connection_from_url", + "disable_warnings", + "encode_multipart_formdata", + "get_host", + "make_headers", + "proxy_from_url", ) logging.getLogger(__name__).addHandler(NullHandler()) @@ -59,10 +55,10 @@ def add_stderr_logger(level=logging.DEBUG): # even if urllib3 is vendored within another package. logger = logging.getLogger(__name__) handler = logging.StreamHandler() - handler.setFormatter(logging.Formatter('%(asctime)s %(levelname)s %(message)s')) + handler.setFormatter(logging.Formatter("%(asctime)s %(levelname)s %(message)s")) logger.addHandler(handler) logger.setLevel(level) - logger.debug('Added a stderr logging handler to logger: %s', __name__) + logger.debug("Added a stderr logging handler to logger: %s", __name__) return handler @@ -74,18 +70,17 @@ def add_stderr_logger(level=logging.DEBUG): # shouldn't be: otherwise, it's very hard for users to use most Python # mechanisms to silence them. # SecurityWarning's always go off by default. -warnings.simplefilter('always', exceptions.SecurityWarning, append=True) +warnings.simplefilter("always", exceptions.SecurityWarning, append=True) # SubjectAltNameWarning's should go off once per host -warnings.simplefilter('default', exceptions.SubjectAltNameWarning, append=True) +warnings.simplefilter("default", exceptions.SubjectAltNameWarning, append=True) # InsecurePlatformWarning's don't vary between requests, so we keep it default. -warnings.simplefilter('default', exceptions.InsecurePlatformWarning, - append=True) +warnings.simplefilter("default", exceptions.InsecurePlatformWarning, append=True) # SNIMissingWarnings should go off only once. -warnings.simplefilter('default', exceptions.SNIMissingWarning, append=True) +warnings.simplefilter("default", exceptions.SNIMissingWarning, append=True) def disable_warnings(category=exceptions.HTTPWarning): """ Helper for quickly disabling all urllib3 warnings. """ - warnings.simplefilter('ignore', category) + warnings.simplefilter("ignore", category) diff --git a/src/pip/_vendor/urllib3/_collections.py b/src/pip/_vendor/urllib3/_collections.py index 34f23811c62..019d1511d56 100644 --- a/src/pip/_vendor/urllib3/_collections.py +++ b/src/pip/_vendor/urllib3/_collections.py @@ -1,4 +1,5 @@ from __future__ import absolute_import + try: from collections.abc import Mapping, MutableMapping except ImportError: @@ -6,6 +7,7 @@ try: from threading import RLock except ImportError: # Platform-specific: No threads available + class RLock: def __enter__(self): pass @@ -19,7 +21,7 @@ def __exit__(self, exc_type, exc_value, traceback): from .packages.six import iterkeys, itervalues, PY3 -__all__ = ['RecentlyUsedContainer', 'HTTPHeaderDict'] +__all__ = ["RecentlyUsedContainer", "HTTPHeaderDict"] _Null = object() @@ -82,7 +84,9 @@ def __len__(self): return len(self._container) def __iter__(self): - raise NotImplementedError('Iteration over this class is unlikely to be threadsafe.') + raise NotImplementedError( + "Iteration over this class is unlikely to be threadsafe." + ) def clear(self): with self.lock: @@ -150,7 +154,7 @@ def __setitem__(self, key, val): def __getitem__(self, key): val = self._container[key.lower()] - return ', '.join(val[1:]) + return ", ".join(val[1:]) def __delitem__(self, key): del self._container[key.lower()] @@ -159,12 +163,13 @@ def __contains__(self, key): return key.lower() in self._container def __eq__(self, other): - if not isinstance(other, Mapping) and not hasattr(other, 'keys'): + if not isinstance(other, Mapping) and not hasattr(other, "keys"): return False if not isinstance(other, type(self)): other = type(self)(other) - return (dict((k.lower(), v) for k, v in self.itermerged()) == - dict((k.lower(), v) for k, v in other.itermerged())) + return dict((k.lower(), v) for k, v in self.itermerged()) == dict( + (k.lower(), v) for k, v in other.itermerged() + ) def __ne__(self, other): return not self.__eq__(other) @@ -184,9 +189,9 @@ def __iter__(self): yield vals[0] def pop(self, key, default=__marker): - '''D.pop(k[,d]) -> v, remove specified key and return the corresponding value. + """D.pop(k[,d]) -> v, remove specified key and return the corresponding value. If key is not found, d is returned if given, otherwise KeyError is raised. - ''' + """ # Using the MutableMapping function directly fails due to the private marker. # Using ordinary dict.pop would expose the internal structures. # So let's reinvent the wheel. @@ -228,8 +233,10 @@ def extend(self, *args, **kwargs): with self.add instead of self.__setitem__ """ if len(args) > 1: - raise TypeError("extend() takes at most 1 positional " - "arguments ({0} given)".format(len(args))) + raise TypeError( + "extend() takes at most 1 positional " + "arguments ({0} given)".format(len(args)) + ) other = args[0] if len(args) >= 1 else () if isinstance(other, HTTPHeaderDict): @@ -295,7 +302,7 @@ def itermerged(self): """Iterate over all headers, merging duplicate ones together.""" for key in self: val = self._container[key.lower()] - yield val[0], ', '.join(val[1:]) + yield val[0], ", ".join(val[1:]) def items(self): return list(self.iteritems()) @@ -306,7 +313,7 @@ def from_httplib(cls, message): # Python 2 # python2.7 does not expose a proper API for exporting multiheaders # efficiently. This function re-reads raw lines from the message # object and extracts the multiheaders properly. - obs_fold_continued_leaders = (' ', '\t') + obs_fold_continued_leaders = (" ", "\t") headers = [] for line in message.headers: @@ -316,14 +323,14 @@ def from_httplib(cls, message): # Python 2 # in RFC-7230 S3.2.4. This indicates a multiline header, but # there exists no previous header to which we can attach it. raise InvalidHeader( - 'Header continuation with no previous header: %s' % line + "Header continuation with no previous header: %s" % line ) else: key, value = headers[-1] - headers[-1] = (key, value + ' ' + line.strip()) + headers[-1] = (key, value + " " + line.strip()) continue - key, value = line.split(':', 1) + key, value = line.split(":", 1) headers.append((key, value.strip())) return cls(headers) diff --git a/src/pip/_vendor/urllib3/connection.py b/src/pip/_vendor/urllib3/connection.py index 57c58fedb7b..3eeb1af58ed 100644 --- a/src/pip/_vendor/urllib3/connection.py +++ b/src/pip/_vendor/urllib3/connection.py @@ -11,6 +11,7 @@ try: # Compiled with SSL? import ssl + BaseSSLError = ssl.SSLError except (ImportError, AttributeError): # Platform-specific: No SSL. ssl = None @@ -41,7 +42,7 @@ class ConnectionError(Exception): resolve_ssl_version, assert_fingerprint, create_urllib3_context, - ssl_wrap_socket + ssl_wrap_socket, ) @@ -51,20 +52,16 @@ class ConnectionError(Exception): log = logging.getLogger(__name__) -port_by_scheme = { - 'http': 80, - 'https': 443, -} +port_by_scheme = {"http": 80, "https": 443} -# When updating RECENT_DATE, move it to within two years of the current date, -# and not less than 6 months ago. -# Example: if Today is 2018-01-01, then RECENT_DATE should be any date on or -# after 2016-01-01 (today - 2 years) AND before 2017-07-01 (today - 6 months) -RECENT_DATE = datetime.date(2017, 6, 30) +# When it comes time to update this value as a part of regular maintenance +# (ie test_recent_date is failing) update it to ~6 months before the current date. +RECENT_DATE = datetime.date(2019, 1, 1) class DummyConnection(object): """Used to detect a failed ConnectionCls import.""" + pass @@ -92,7 +89,7 @@ class HTTPConnection(_HTTPConnection, object): Or you may want to disable the defaults by passing an empty list (e.g., ``[]``). """ - default_port = port_by_scheme['http'] + default_port = port_by_scheme["http"] #: Disable Nagle's algorithm by default. #: ``[(socket.IPPROTO_TCP, socket.TCP_NODELAY, 1)]`` @@ -102,15 +99,15 @@ class HTTPConnection(_HTTPConnection, object): is_verified = False def __init__(self, *args, **kw): - if six.PY3: - kw.pop('strict', None) + if not six.PY2: + kw.pop("strict", None) # Pre-set source_address. - self.source_address = kw.get('source_address') + self.source_address = kw.get("source_address") #: The socket options provided by the user. If no options are #: provided, we use the default options. - self.socket_options = kw.pop('socket_options', self.default_socket_options) + self.socket_options = kw.pop("socket_options", self.default_socket_options) _HTTPConnection.__init__(self, *args, **kw) @@ -131,7 +128,7 @@ def host(self): those cases where it's appropriate (i.e., when doing DNS lookup to establish the actual TCP connection across which we're going to send HTTP requests). """ - return self._dns_host.rstrip('.') + return self._dns_host.rstrip(".") @host.setter def host(self, value): @@ -150,30 +147,34 @@ def _new_conn(self): """ extra_kw = {} if self.source_address: - extra_kw['source_address'] = self.source_address + extra_kw["source_address"] = self.source_address if self.socket_options: - extra_kw['socket_options'] = self.socket_options + extra_kw["socket_options"] = self.socket_options try: conn = connection.create_connection( - (self._dns_host, self.port), self.timeout, **extra_kw) + (self._dns_host, self.port), self.timeout, **extra_kw + ) except SocketTimeout: raise ConnectTimeoutError( - self, "Connection to %s timed out. (connect timeout=%s)" % - (self.host, self.timeout)) + self, + "Connection to %s timed out. (connect timeout=%s)" + % (self.host, self.timeout), + ) except SocketError as e: raise NewConnectionError( - self, "Failed to establish a new connection: %s" % e) + self, "Failed to establish a new connection: %s" % e + ) return conn def _prepare_conn(self, conn): self.sock = conn # Google App Engine's httplib does not define _tunnel_host - if getattr(self, '_tunnel_host', None): + if getattr(self, "_tunnel_host", None): # TODO: Fix tunnel so it doesn't depend on self.sock state. self._tunnel() # Mark this connection as not reusable @@ -189,18 +190,15 @@ def request_chunked(self, method, url, body=None, headers=None): body with chunked encoding and not as one block """ headers = HTTPHeaderDict(headers if headers is not None else {}) - skip_accept_encoding = 'accept-encoding' in headers - skip_host = 'host' in headers + skip_accept_encoding = "accept-encoding" in headers + skip_host = "host" in headers self.putrequest( - method, - url, - skip_accept_encoding=skip_accept_encoding, - skip_host=skip_host + method, url, skip_accept_encoding=skip_accept_encoding, skip_host=skip_host ) for header, value in headers.items(): self.putheader(header, value) - if 'transfer-encoding' not in headers: - self.putheader('Transfer-Encoding', 'chunked') + if "transfer-encoding" not in headers: + self.putheader("Transfer-Encoding", "chunked") self.endheaders() if body is not None: @@ -211,29 +209,37 @@ def request_chunked(self, method, url, body=None, headers=None): if not chunk: continue if not isinstance(chunk, bytes): - chunk = chunk.encode('utf8') + chunk = chunk.encode("utf8") len_str = hex(len(chunk))[2:] - self.send(len_str.encode('utf-8')) - self.send(b'\r\n') + self.send(len_str.encode("utf-8")) + self.send(b"\r\n") self.send(chunk) - self.send(b'\r\n') + self.send(b"\r\n") # After the if clause, to always have a closed body - self.send(b'0\r\n\r\n') + self.send(b"0\r\n\r\n") class HTTPSConnection(HTTPConnection): - default_port = port_by_scheme['https'] + default_port = port_by_scheme["https"] ssl_version = None - def __init__(self, host, port=None, key_file=None, cert_file=None, - key_password=None, strict=None, - timeout=socket._GLOBAL_DEFAULT_TIMEOUT, - ssl_context=None, server_hostname=None, **kw): - - HTTPConnection.__init__(self, host, port, strict=strict, - timeout=timeout, **kw) + def __init__( + self, + host, + port=None, + key_file=None, + cert_file=None, + key_password=None, + strict=None, + timeout=socket._GLOBAL_DEFAULT_TIMEOUT, + ssl_context=None, + server_hostname=None, + **kw + ): + + HTTPConnection.__init__(self, host, port, strict=strict, timeout=timeout, **kw) self.key_file = key_file self.cert_file = cert_file @@ -243,7 +249,7 @@ def __init__(self, host, port=None, key_file=None, cert_file=None, # Required property for Google AppEngine 1.9.0 which otherwise causes # HTTPS requests to go out as HTTP. (See Issue #356) - self._protocol = 'https' + self._protocol = "https" def connect(self): conn = self._new_conn() @@ -262,8 +268,12 @@ def connect(self): # Try to load OS default certs if none are given. # Works well on Windows (requires Python3.4+) context = self.ssl_context - if (not self.ca_certs and not self.ca_cert_dir and default_ssl_context - and hasattr(context, 'load_default_certs')): + if ( + not self.ca_certs + and not self.ca_cert_dir + and default_ssl_context + and hasattr(context, "load_default_certs") + ): context.load_default_certs() self.sock = ssl_wrap_socket( @@ -272,7 +282,7 @@ def connect(self): certfile=self.cert_file, key_password=self.key_password, ssl_context=self.ssl_context, - server_hostname=self.server_hostname + server_hostname=self.server_hostname, ) @@ -281,16 +291,24 @@ class VerifiedHTTPSConnection(HTTPSConnection): Based on httplib.HTTPSConnection but wraps the socket with SSL certification. """ + cert_reqs = None ca_certs = None ca_cert_dir = None ssl_version = None assert_fingerprint = None - def set_cert(self, key_file=None, cert_file=None, - cert_reqs=None, key_password=None, ca_certs=None, - assert_hostname=None, assert_fingerprint=None, - ca_cert_dir=None): + def set_cert( + self, + key_file=None, + cert_file=None, + cert_reqs=None, + key_password=None, + ca_certs=None, + assert_hostname=None, + assert_fingerprint=None, + ca_cert_dir=None, + ): """ This method should only be called once, before the connection is used. """ @@ -317,7 +335,7 @@ def connect(self): hostname = self.host # Google App Engine's httplib does not define _tunnel_host - if getattr(self, '_tunnel_host', None): + if getattr(self, "_tunnel_host", None): self.sock = conn # Calls self._set_hostport(), so self.host is # self._tunnel_host below. @@ -334,10 +352,12 @@ def connect(self): is_time_off = datetime.date.today() < RECENT_DATE if is_time_off: - warnings.warn(( - 'System time is way off (before {0}). This will probably ' - 'lead to SSL verification errors').format(RECENT_DATE), - SystemTimeWarning + warnings.warn( + ( + "System time is way off (before {0}). This will probably " + "lead to SSL verification errors" + ).format(RECENT_DATE), + SystemTimeWarning, ) # Wrap socket using verification with the root certs in @@ -355,8 +375,12 @@ def connect(self): # Try to load OS default certs if none are given. # Works well on Windows (requires Python3.4+) - if (not self.ca_certs and not self.ca_cert_dir and default_ssl_context - and hasattr(context, 'load_default_certs')): + if ( + not self.ca_certs + and not self.ca_cert_dir + and default_ssl_context + and hasattr(context, "load_default_certs") + ): context.load_default_certs() self.sock = ssl_wrap_socket( @@ -367,31 +391,37 @@ def connect(self): ca_certs=self.ca_certs, ca_cert_dir=self.ca_cert_dir, server_hostname=server_hostname, - ssl_context=context) + ssl_context=context, + ) if self.assert_fingerprint: - assert_fingerprint(self.sock.getpeercert(binary_form=True), - self.assert_fingerprint) - elif context.verify_mode != ssl.CERT_NONE \ - and not getattr(context, 'check_hostname', False) \ - and self.assert_hostname is not False: + assert_fingerprint( + self.sock.getpeercert(binary_form=True), self.assert_fingerprint + ) + elif ( + context.verify_mode != ssl.CERT_NONE + and not getattr(context, "check_hostname", False) + and self.assert_hostname is not False + ): # While urllib3 attempts to always turn off hostname matching from # the TLS library, this cannot always be done. So we check whether # the TLS Library still thinks it's matching hostnames. cert = self.sock.getpeercert() - if not cert.get('subjectAltName', ()): - warnings.warn(( - 'Certificate for {0} has no `subjectAltName`, falling back to check for a ' - '`commonName` for now. This feature is being removed by major browsers and ' - 'deprecated by RFC 2818. (See https://github.com/shazow/urllib3/issues/497 ' - 'for details.)'.format(hostname)), - SubjectAltNameWarning + if not cert.get("subjectAltName", ()): + warnings.warn( + ( + "Certificate for {0} has no `subjectAltName`, falling back to check for a " + "`commonName` for now. This feature is being removed by major browsers and " + "deprecated by RFC 2818. (See https://github.com/shazow/urllib3/issues/497 " + "for details.)".format(hostname) + ), + SubjectAltNameWarning, ) _match_hostname(cert, self.assert_hostname or server_hostname) self.is_verified = ( - context.verify_mode == ssl.CERT_REQUIRED or - self.assert_fingerprint is not None + context.verify_mode == ssl.CERT_REQUIRED + or self.assert_fingerprint is not None ) @@ -399,9 +429,10 @@ def _match_hostname(cert, asserted_hostname): try: match_hostname(cert, asserted_hostname) except CertificateError as e: - log.error( - 'Certificate did not match expected hostname: %s. ' - 'Certificate: %s', asserted_hostname, cert + log.warning( + "Certificate did not match expected hostname: %s. " "Certificate: %s", + asserted_hostname, + cert, ) # Add cert to exception and reraise so client code can inspect # the cert when catching the exception, if they want to diff --git a/src/pip/_vendor/urllib3/connectionpool.py b/src/pip/_vendor/urllib3/connectionpool.py index 157568a3951..e73fa57a427 100644 --- a/src/pip/_vendor/urllib3/connectionpool.py +++ b/src/pip/_vendor/urllib3/connectionpool.py @@ -26,12 +26,14 @@ from .packages.ssl_match_hostname import CertificateError from .packages import six from .packages.six.moves import queue -from .packages.rfc3986.normalizers import normalize_host from .connection import ( port_by_scheme, DummyConnection, - HTTPConnection, HTTPSConnection, VerifiedHTTPSConnection, - HTTPException, BaseSSLError, + HTTPConnection, + HTTPSConnection, + VerifiedHTTPSConnection, + HTTPException, + BaseSSLError, ) from .request import RequestMethods from .response import HTTPResponse @@ -41,7 +43,13 @@ from .util.response import assert_header_parsing from .util.retry import Retry from .util.timeout import Timeout -from .util.url import get_host, Url, NORMALIZABLE_SCHEMES +from .util.url import ( + get_host, + parse_url, + Url, + _normalize_host as normalize_host, + _encode_target, +) from .util.queue import LifoQueue @@ -71,8 +79,7 @@ def __init__(self, host, port=None): self.port = port def __str__(self): - return '%s(host=%r, port=%r)' % (type(self).__name__, - self.host, self.port) + return "%s(host=%r, port=%r)" % (type(self).__name__, self.host, self.port) def __enter__(self): return self @@ -153,15 +160,24 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods): :class:`urllib3.connection.HTTPSConnection` instances. """ - scheme = 'http' + scheme = "http" ConnectionCls = HTTPConnection ResponseCls = HTTPResponse - def __init__(self, host, port=None, strict=False, - timeout=Timeout.DEFAULT_TIMEOUT, maxsize=1, block=False, - headers=None, retries=None, - _proxy=None, _proxy_headers=None, - **conn_kw): + def __init__( + self, + host, + port=None, + strict=False, + timeout=Timeout.DEFAULT_TIMEOUT, + maxsize=1, + block=False, + headers=None, + retries=None, + _proxy=None, + _proxy_headers=None, + **conn_kw + ): ConnectionPool.__init__(self, host, port) RequestMethods.__init__(self, headers) @@ -195,19 +211,27 @@ def __init__(self, host, port=None, strict=False, # Enable Nagle's algorithm for proxies, to avoid packet fragmentation. # We cannot know if the user has added default socket options, so we cannot replace the # list. - self.conn_kw.setdefault('socket_options', []) + self.conn_kw.setdefault("socket_options", []) def _new_conn(self): """ Return a fresh :class:`HTTPConnection`. """ self.num_connections += 1 - log.debug("Starting new HTTP connection (%d): %s:%s", - self.num_connections, self.host, self.port or "80") - - conn = self.ConnectionCls(host=self.host, port=self.port, - timeout=self.timeout.connect_timeout, - strict=self.strict, **self.conn_kw) + log.debug( + "Starting new HTTP connection (%d): %s:%s", + self.num_connections, + self.host, + self.port or "80", + ) + + conn = self.ConnectionCls( + host=self.host, + port=self.port, + timeout=self.timeout.connect_timeout, + strict=self.strict, + **self.conn_kw + ) return conn def _get_conn(self, timeout=None): @@ -231,16 +255,17 @@ def _get_conn(self, timeout=None): except queue.Empty: if self.block: - raise EmptyPoolError(self, - "Pool reached maximum size and no more " - "connections are allowed.") + raise EmptyPoolError( + self, + "Pool reached maximum size and no more " "connections are allowed.", + ) pass # Oh well, we'll create a new connection then # If this is a persistent connection, check if it got disconnected if conn and is_connection_dropped(conn): log.debug("Resetting dropped connection: %s", self.host) conn.close() - if getattr(conn, 'auto_open', 1) == 0: + if getattr(conn, "auto_open", 1) == 0: # This is a proxied connection that has been mutated by # httplib._tunnel() and cannot be reused (since it would # attempt to bypass the proxy) @@ -270,9 +295,7 @@ def _put_conn(self, conn): pass except queue.Full: # This should never happen if self.block == True - log.warning( - "Connection pool is full, discarding connection: %s", - self.host) + log.warning("Connection pool is full, discarding connection: %s", self.host) # Connection never got put back into the pool, close it. if conn: @@ -304,21 +327,30 @@ def _raise_timeout(self, err, url, timeout_value): """Is the error actually a timeout? Will raise a ReadTimeout or pass""" if isinstance(err, SocketTimeout): - raise ReadTimeoutError(self, url, "Read timed out. (read timeout=%s)" % timeout_value) + raise ReadTimeoutError( + self, url, "Read timed out. (read timeout=%s)" % timeout_value + ) # See the above comment about EAGAIN in Python 3. In Python 2 we have # to specifically catch it and throw the timeout error - if hasattr(err, 'errno') and err.errno in _blocking_errnos: - raise ReadTimeoutError(self, url, "Read timed out. (read timeout=%s)" % timeout_value) + if hasattr(err, "errno") and err.errno in _blocking_errnos: + raise ReadTimeoutError( + self, url, "Read timed out. (read timeout=%s)" % timeout_value + ) # Catch possible read timeouts thrown as SSL errors. If not the # case, rethrow the original. We need to do this because of: # http://bugs.python.org/issue10272 - if 'timed out' in str(err) or 'did not complete (read)' in str(err): # Python < 2.7.4 - raise ReadTimeoutError(self, url, "Read timed out. (read timeout=%s)" % timeout_value) - - def _make_request(self, conn, method, url, timeout=_Default, chunked=False, - **httplib_request_kw): + if "timed out" in str(err) or "did not complete (read)" in str( + err + ): # Python < 2.7.4 + raise ReadTimeoutError( + self, url, "Read timed out. (read timeout=%s)" % timeout_value + ) + + def _make_request( + self, conn, method, url, timeout=_Default, chunked=False, **httplib_request_kw + ): """ Perform a request on a given urllib connection object taken from our pool. @@ -358,7 +390,7 @@ def _make_request(self, conn, method, url, timeout=_Default, chunked=False, read_timeout = timeout_obj.read_timeout # App Engine doesn't have a sock attr - if getattr(conn, 'sock', None): + if getattr(conn, "sock", None): # In Python 3 socket.py will catch EAGAIN and return None when you # try and read into the file pointer created by http.client, which # instead raises a BadStatusLine exception. Instead of catching @@ -366,7 +398,8 @@ def _make_request(self, conn, method, url, timeout=_Default, chunked=False, # timeouts, check for a zero timeout before making the request. if read_timeout == 0: raise ReadTimeoutError( - self, url, "Read timed out. (read timeout=%s)" % read_timeout) + self, url, "Read timed out. (read timeout=%s)" % read_timeout + ) if read_timeout is Timeout.DEFAULT_TIMEOUT: conn.sock.settimeout(socket.getdefaulttimeout()) else: # None or a value @@ -381,26 +414,38 @@ def _make_request(self, conn, method, url, timeout=_Default, chunked=False, # Python 3 try: httplib_response = conn.getresponse() - except Exception as e: - # Remove the TypeError from the exception chain in Python 3; - # otherwise it looks like a programming error was the cause. + except BaseException as e: + # Remove the TypeError from the exception chain in + # Python 3 (including for exceptions like SystemExit). + # Otherwise it looks like a bug in the code. six.raise_from(e, None) except (SocketTimeout, BaseSSLError, SocketError) as e: self._raise_timeout(err=e, url=url, timeout_value=read_timeout) raise # AppEngine doesn't have a version attr. - http_version = getattr(conn, '_http_vsn_str', 'HTTP/?') - log.debug("%s://%s:%s \"%s %s %s\" %s %s", self.scheme, self.host, self.port, - method, url, http_version, httplib_response.status, - httplib_response.length) + http_version = getattr(conn, "_http_vsn_str", "HTTP/?") + log.debug( + '%s://%s:%s "%s %s %s" %s %s', + self.scheme, + self.host, + self.port, + method, + url, + http_version, + httplib_response.status, + httplib_response.length, + ) try: assert_header_parsing(httplib_response.msg) except (HeaderParsingError, TypeError) as hpe: # Platform-specific: Python 3 log.warning( - 'Failed to parse headers (url=%s): %s', - self._absolute_url(url), hpe, exc_info=True) + "Failed to parse headers (url=%s): %s", + self._absolute_url(url), + hpe, + exc_info=True, + ) return httplib_response @@ -430,7 +475,7 @@ def is_same_host(self, url): Check if the given ``url`` is a member of the same host as this connection pool. """ - if url.startswith('/'): + if url.startswith("/"): return True # TODO: Add optional support for socket.gethostbyname checking. @@ -446,10 +491,22 @@ def is_same_host(self, url): return (scheme, host, port) == (self.scheme, self.host, self.port) - def urlopen(self, method, url, body=None, headers=None, retries=None, - redirect=True, assert_same_host=True, timeout=_Default, - pool_timeout=None, release_conn=None, chunked=False, - body_pos=None, **response_kw): + def urlopen( + self, + method, + url, + body=None, + headers=None, + retries=None, + redirect=True, + assert_same_host=True, + timeout=_Default, + pool_timeout=None, + release_conn=None, + chunked=False, + body_pos=None, + **response_kw + ): """ Get a connection from the pool and perform an HTTP request. This is the lowest level call for making a request, so you'll need to specify all @@ -547,12 +604,18 @@ def urlopen(self, method, url, body=None, headers=None, retries=None, retries = Retry.from_int(retries, redirect=redirect, default=self.retries) if release_conn is None: - release_conn = response_kw.get('preload_content', True) + release_conn = response_kw.get("preload_content", True) # Check host if assert_same_host and not self.is_same_host(url): raise HostChangedError(self, url, retries) + # Ensure that the URL we're connecting to is properly encoded + if url.startswith("/"): + url = six.ensure_str(_encode_target(url)) + else: + url = six.ensure_str(parse_url(url).url) + conn = None # Track whether `conn` needs to be released before @@ -569,7 +632,7 @@ def urlopen(self, method, url, body=None, headers=None, retries=None, # Merge the proxy headers. Only do this in HTTP. We have to copy the # headers dict so we can safely change it without those changes being # reflected in anyone else's copy. - if self.scheme == 'http': + if self.scheme == "http": headers = headers.copy() headers.update(self.proxy_headers) @@ -592,15 +655,22 @@ def urlopen(self, method, url, body=None, headers=None, retries=None, conn.timeout = timeout_obj.connect_timeout - is_new_proxy_conn = self.proxy is not None and not getattr(conn, 'sock', None) + is_new_proxy_conn = self.proxy is not None and not getattr( + conn, "sock", None + ) if is_new_proxy_conn: self._prepare_proxy(conn) # Make the request on the httplib connection object. - httplib_response = self._make_request(conn, method, url, - timeout=timeout_obj, - body=body, headers=headers, - chunked=chunked) + httplib_response = self._make_request( + conn, + method, + url, + timeout=timeout_obj, + body=body, + headers=headers, + chunked=chunked, + ) # If we're going to release the connection in ``finally:``, then # the response doesn't need to know about the connection. Otherwise @@ -609,14 +679,16 @@ def urlopen(self, method, url, body=None, headers=None, retries=None, response_conn = conn if not release_conn else None # Pass method to Response for length checking - response_kw['request_method'] = method + response_kw["request_method"] = method # Import httplib's response into our own wrapper object - response = self.ResponseCls.from_httplib(httplib_response, - pool=self, - connection=response_conn, - retries=retries, - **response_kw) + response = self.ResponseCls.from_httplib( + httplib_response, + pool=self, + connection=response_conn, + retries=retries, + **response_kw + ) # Everything went great! clean_exit = True @@ -625,20 +697,28 @@ def urlopen(self, method, url, body=None, headers=None, retries=None, # Timed out by queue. raise EmptyPoolError(self, "No pool connections are available.") - except (TimeoutError, HTTPException, SocketError, ProtocolError, - BaseSSLError, SSLError, CertificateError) as e: + except ( + TimeoutError, + HTTPException, + SocketError, + ProtocolError, + BaseSSLError, + SSLError, + CertificateError, + ) as e: # Discard the connection for these exceptions. It will be # replaced during the next _get_conn() call. clean_exit = False if isinstance(e, (BaseSSLError, CertificateError)): e = SSLError(e) elif isinstance(e, (SocketError, NewConnectionError)) and self.proxy: - e = ProxyError('Cannot connect to proxy.', e) + e = ProxyError("Cannot connect to proxy.", e) elif isinstance(e, (SocketError, HTTPException)): - e = ProtocolError('Connection aborted.', e) + e = ProtocolError("Connection aborted.", e) - retries = retries.increment(method, url, error=e, _pool=self, - _stacktrace=sys.exc_info()[2]) + retries = retries.increment( + method, url, error=e, _pool=self, _stacktrace=sys.exc_info()[2] + ) retries.sleep() # Keep track of the error for the retry warning. @@ -661,28 +741,47 @@ def urlopen(self, method, url, body=None, headers=None, retries=None, if not conn: # Try again - log.warning("Retrying (%r) after connection " - "broken by '%r': %s", retries, err, url) - return self.urlopen(method, url, body, headers, retries, - redirect, assert_same_host, - timeout=timeout, pool_timeout=pool_timeout, - release_conn=release_conn, body_pos=body_pos, - **response_kw) + log.warning( + "Retrying (%r) after connection " "broken by '%r': %s", + retries, + err, + url, + ) + return self.urlopen( + method, + url, + body, + headers, + retries, + redirect, + assert_same_host, + timeout=timeout, + pool_timeout=pool_timeout, + release_conn=release_conn, + body_pos=body_pos, + **response_kw + ) def drain_and_release_conn(response): try: # discard any remaining response body, the connection will be # released back to the pool once the entire response is read response.read() - except (TimeoutError, HTTPException, SocketError, ProtocolError, - BaseSSLError, SSLError): + except ( + TimeoutError, + HTTPException, + SocketError, + ProtocolError, + BaseSSLError, + SSLError, + ): pass # Handle redirect? redirect_location = redirect and response.get_redirect_location() if redirect_location: if response.status == 303: - method = 'GET' + method = "GET" try: retries = retries.increment(method, url, response=response, _pool=self) @@ -700,15 +799,22 @@ def drain_and_release_conn(response): retries.sleep_for_retry(response) log.debug("Redirecting %s -> %s", url, redirect_location) return self.urlopen( - method, redirect_location, body, headers, - retries=retries, redirect=redirect, + method, + redirect_location, + body, + headers, + retries=retries, + redirect=redirect, assert_same_host=assert_same_host, - timeout=timeout, pool_timeout=pool_timeout, - release_conn=release_conn, body_pos=body_pos, - **response_kw) + timeout=timeout, + pool_timeout=pool_timeout, + release_conn=release_conn, + body_pos=body_pos, + **response_kw + ) # Check if we should retry the HTTP response. - has_retry_after = bool(response.getheader('Retry-After')) + has_retry_after = bool(response.getheader("Retry-After")) if retries.is_retry(method, response.status, has_retry_after): try: retries = retries.increment(method, url, response=response, _pool=self) @@ -726,12 +832,19 @@ def drain_and_release_conn(response): retries.sleep(response) log.debug("Retry: %s", url) return self.urlopen( - method, url, body, headers, - retries=retries, redirect=redirect, + method, + url, + body, + headers, + retries=retries, + redirect=redirect, assert_same_host=assert_same_host, - timeout=timeout, pool_timeout=pool_timeout, + timeout=timeout, + pool_timeout=pool_timeout, release_conn=release_conn, - body_pos=body_pos, **response_kw) + body_pos=body_pos, + **response_kw + ) return response @@ -754,21 +867,47 @@ class HTTPSConnectionPool(HTTPConnectionPool): the connection socket into an SSL socket. """ - scheme = 'https' + scheme = "https" ConnectionCls = HTTPSConnection - def __init__(self, host, port=None, - strict=False, timeout=Timeout.DEFAULT_TIMEOUT, maxsize=1, - block=False, headers=None, retries=None, - _proxy=None, _proxy_headers=None, - key_file=None, cert_file=None, cert_reqs=None, - key_password=None, ca_certs=None, ssl_version=None, - assert_hostname=None, assert_fingerprint=None, - ca_cert_dir=None, **conn_kw): - - HTTPConnectionPool.__init__(self, host, port, strict, timeout, maxsize, - block, headers, retries, _proxy, _proxy_headers, - **conn_kw) + def __init__( + self, + host, + port=None, + strict=False, + timeout=Timeout.DEFAULT_TIMEOUT, + maxsize=1, + block=False, + headers=None, + retries=None, + _proxy=None, + _proxy_headers=None, + key_file=None, + cert_file=None, + cert_reqs=None, + key_password=None, + ca_certs=None, + ssl_version=None, + assert_hostname=None, + assert_fingerprint=None, + ca_cert_dir=None, + **conn_kw + ): + + HTTPConnectionPool.__init__( + self, + host, + port, + strict, + timeout, + maxsize, + block, + headers, + retries, + _proxy, + _proxy_headers, + **conn_kw + ) self.key_file = key_file self.cert_file = cert_file @@ -787,14 +926,16 @@ def _prepare_conn(self, conn): """ if isinstance(conn, VerifiedHTTPSConnection): - conn.set_cert(key_file=self.key_file, - key_password=self.key_password, - cert_file=self.cert_file, - cert_reqs=self.cert_reqs, - ca_certs=self.ca_certs, - ca_cert_dir=self.ca_cert_dir, - assert_hostname=self.assert_hostname, - assert_fingerprint=self.assert_fingerprint) + conn.set_cert( + key_file=self.key_file, + key_password=self.key_password, + cert_file=self.cert_file, + cert_reqs=self.cert_reqs, + ca_certs=self.ca_certs, + ca_cert_dir=self.ca_cert_dir, + assert_hostname=self.assert_hostname, + assert_fingerprint=self.assert_fingerprint, + ) conn.ssl_version = self.ssl_version return conn @@ -811,12 +952,17 @@ def _new_conn(self): Return a fresh :class:`httplib.HTTPSConnection`. """ self.num_connections += 1 - log.debug("Starting new HTTPS connection (%d): %s:%s", - self.num_connections, self.host, self.port or "443") + log.debug( + "Starting new HTTPS connection (%d): %s:%s", + self.num_connections, + self.host, + self.port or "443", + ) if not self.ConnectionCls or self.ConnectionCls is DummyConnection: - raise SSLError("Can't connect to HTTPS URL because the SSL " - "module is not available.") + raise SSLError( + "Can't connect to HTTPS URL because the SSL " "module is not available." + ) actual_host = self.host actual_port = self.port @@ -824,11 +970,16 @@ def _new_conn(self): actual_host = self.proxy.host actual_port = self.proxy.port - conn = self.ConnectionCls(host=actual_host, port=actual_port, - timeout=self.timeout.connect_timeout, - strict=self.strict, cert_file=self.cert_file, - key_file=self.key_file, key_password=self.key_password, - **self.conn_kw) + conn = self.ConnectionCls( + host=actual_host, + port=actual_port, + timeout=self.timeout.connect_timeout, + strict=self.strict, + cert_file=self.cert_file, + key_file=self.key_file, + key_password=self.key_password, + **self.conn_kw + ) return self._prepare_conn(conn) @@ -839,16 +990,19 @@ def _validate_conn(self, conn): super(HTTPSConnectionPool, self)._validate_conn(conn) # Force connect early to allow us to validate the connection. - if not getattr(conn, 'sock', None): # AppEngine might not have `.sock` + if not getattr(conn, "sock", None): # AppEngine might not have `.sock` conn.connect() if not conn.is_verified: - warnings.warn(( - 'Unverified HTTPS request is being made. ' - 'Adding certificate verification is strongly advised. See: ' - 'https://urllib3.readthedocs.io/en/latest/advanced-usage.html' - '#ssl-warnings'), - InsecureRequestWarning) + warnings.warn( + ( + "Unverified HTTPS request is being made. " + "Adding certificate verification is strongly advised. See: " + "https://urllib3.readthedocs.io/en/latest/advanced-usage.html" + "#ssl-warnings" + ), + InsecureRequestWarning, + ) def connection_from_url(url, **kw): @@ -873,7 +1027,7 @@ def connection_from_url(url, **kw): """ scheme, host, port = get_host(url) port = port or port_by_scheme.get(scheme, 80) - if scheme == 'https': + if scheme == "https": return HTTPSConnectionPool(host, port=port, **kw) else: return HTTPConnectionPool(host, port=port, **kw) @@ -884,14 +1038,14 @@ def _normalize_host(host, scheme): Normalize hosts for comparisons and use with sockets. """ + host = normalize_host(host, scheme) + # httplib doesn't like it when we include brackets in IPv6 addresses # Specifically, if we include brackets but also pass the port then # httplib crazily doubles up the square brackets on the Host header. # Instead, we need to make sure we never pass ``None`` as the port. # However, for backward compatibility reasons we can't actually # *assert* that. See http://bugs.python.org/issue28539 - if host.startswith('[') and host.endswith(']'): - host = host.strip('[]') - if scheme in NORMALIZABLE_SCHEMES: - host = normalize_host(host) + if host.startswith("[") and host.endswith("]"): + host = host[1:-1] return host diff --git a/src/pip/_vendor/urllib3/contrib/_appengine_environ.py b/src/pip/_vendor/urllib3/contrib/_appengine_environ.py index f3e00942cb9..c909010bf27 100644 --- a/src/pip/_vendor/urllib3/contrib/_appengine_environ.py +++ b/src/pip/_vendor/urllib3/contrib/_appengine_environ.py @@ -6,9 +6,7 @@ def is_appengine(): - return (is_local_appengine() or - is_prod_appengine() or - is_prod_appengine_mvms()) + return is_local_appengine() or is_prod_appengine() or is_prod_appengine_mvms() def is_appengine_sandbox(): @@ -16,15 +14,19 @@ def is_appengine_sandbox(): def is_local_appengine(): - return ('APPENGINE_RUNTIME' in os.environ and - 'Development/' in os.environ['SERVER_SOFTWARE']) + return ( + "APPENGINE_RUNTIME" in os.environ + and "Development/" in os.environ["SERVER_SOFTWARE"] + ) def is_prod_appengine(): - return ('APPENGINE_RUNTIME' in os.environ and - 'Google App Engine/' in os.environ['SERVER_SOFTWARE'] and - not is_prod_appengine_mvms()) + return ( + "APPENGINE_RUNTIME" in os.environ + and "Google App Engine/" in os.environ["SERVER_SOFTWARE"] + and not is_prod_appengine_mvms() + ) def is_prod_appengine_mvms(): - return os.environ.get('GAE_VM', False) == 'true' + return os.environ.get("GAE_VM", False) == "true" diff --git a/src/pip/_vendor/urllib3/contrib/_securetransport/bindings.py b/src/pip/_vendor/urllib3/contrib/_securetransport/bindings.py index be342153590..b46e1e3b5de 100644 --- a/src/pip/_vendor/urllib3/contrib/_securetransport/bindings.py +++ b/src/pip/_vendor/urllib3/contrib/_securetransport/bindings.py @@ -34,29 +34,35 @@ import platform from ctypes.util import find_library from ctypes import ( - c_void_p, c_int32, c_char_p, c_size_t, c_byte, c_uint32, c_ulong, c_long, - c_bool + c_void_p, + c_int32, + c_char_p, + c_size_t, + c_byte, + c_uint32, + c_ulong, + c_long, + c_bool, ) from ctypes import CDLL, POINTER, CFUNCTYPE -security_path = find_library('Security') +security_path = find_library("Security") if not security_path: - raise ImportError('The library Security could not be found') + raise ImportError("The library Security could not be found") -core_foundation_path = find_library('CoreFoundation') +core_foundation_path = find_library("CoreFoundation") if not core_foundation_path: - raise ImportError('The library CoreFoundation could not be found') + raise ImportError("The library CoreFoundation could not be found") version = platform.mac_ver()[0] -version_info = tuple(map(int, version.split('.'))) +version_info = tuple(map(int, version.split("."))) if version_info < (10, 8): raise OSError( - 'Only OS X 10.8 and newer are supported, not %s.%s' % ( - version_info[0], version_info[1] - ) + "Only OS X 10.8 and newer are supported, not %s.%s" + % (version_info[0], version_info[1]) ) Security = CDLL(security_path, use_errno=True) @@ -129,27 +135,19 @@ Security.SecKeyGetTypeID.argtypes = [] Security.SecKeyGetTypeID.restype = CFTypeID - Security.SecCertificateCreateWithData.argtypes = [ - CFAllocatorRef, - CFDataRef - ] + Security.SecCertificateCreateWithData.argtypes = [CFAllocatorRef, CFDataRef] Security.SecCertificateCreateWithData.restype = SecCertificateRef - Security.SecCertificateCopyData.argtypes = [ - SecCertificateRef - ] + Security.SecCertificateCopyData.argtypes = [SecCertificateRef] Security.SecCertificateCopyData.restype = CFDataRef - Security.SecCopyErrorMessageString.argtypes = [ - OSStatus, - c_void_p - ] + Security.SecCopyErrorMessageString.argtypes = [OSStatus, c_void_p] Security.SecCopyErrorMessageString.restype = CFStringRef Security.SecIdentityCreateWithCertificate.argtypes = [ CFTypeRef, SecCertificateRef, - POINTER(SecIdentityRef) + POINTER(SecIdentityRef), ] Security.SecIdentityCreateWithCertificate.restype = OSStatus @@ -159,201 +157,126 @@ c_void_p, Boolean, c_void_p, - POINTER(SecKeychainRef) + POINTER(SecKeychainRef), ] Security.SecKeychainCreate.restype = OSStatus - Security.SecKeychainDelete.argtypes = [ - SecKeychainRef - ] + Security.SecKeychainDelete.argtypes = [SecKeychainRef] Security.SecKeychainDelete.restype = OSStatus Security.SecPKCS12Import.argtypes = [ CFDataRef, CFDictionaryRef, - POINTER(CFArrayRef) + POINTER(CFArrayRef), ] Security.SecPKCS12Import.restype = OSStatus SSLReadFunc = CFUNCTYPE(OSStatus, SSLConnectionRef, c_void_p, POINTER(c_size_t)) - SSLWriteFunc = CFUNCTYPE(OSStatus, SSLConnectionRef, POINTER(c_byte), POINTER(c_size_t)) + SSLWriteFunc = CFUNCTYPE( + OSStatus, SSLConnectionRef, POINTER(c_byte), POINTER(c_size_t) + ) - Security.SSLSetIOFuncs.argtypes = [ - SSLContextRef, - SSLReadFunc, - SSLWriteFunc - ] + Security.SSLSetIOFuncs.argtypes = [SSLContextRef, SSLReadFunc, SSLWriteFunc] Security.SSLSetIOFuncs.restype = OSStatus - Security.SSLSetPeerID.argtypes = [ - SSLContextRef, - c_char_p, - c_size_t - ] + Security.SSLSetPeerID.argtypes = [SSLContextRef, c_char_p, c_size_t] Security.SSLSetPeerID.restype = OSStatus - Security.SSLSetCertificate.argtypes = [ - SSLContextRef, - CFArrayRef - ] + Security.SSLSetCertificate.argtypes = [SSLContextRef, CFArrayRef] Security.SSLSetCertificate.restype = OSStatus - Security.SSLSetCertificateAuthorities.argtypes = [ - SSLContextRef, - CFTypeRef, - Boolean - ] + Security.SSLSetCertificateAuthorities.argtypes = [SSLContextRef, CFTypeRef, Boolean] Security.SSLSetCertificateAuthorities.restype = OSStatus - Security.SSLSetConnection.argtypes = [ - SSLContextRef, - SSLConnectionRef - ] + Security.SSLSetConnection.argtypes = [SSLContextRef, SSLConnectionRef] Security.SSLSetConnection.restype = OSStatus - Security.SSLSetPeerDomainName.argtypes = [ - SSLContextRef, - c_char_p, - c_size_t - ] + Security.SSLSetPeerDomainName.argtypes = [SSLContextRef, c_char_p, c_size_t] Security.SSLSetPeerDomainName.restype = OSStatus - Security.SSLHandshake.argtypes = [ - SSLContextRef - ] + Security.SSLHandshake.argtypes = [SSLContextRef] Security.SSLHandshake.restype = OSStatus - Security.SSLRead.argtypes = [ - SSLContextRef, - c_char_p, - c_size_t, - POINTER(c_size_t) - ] + Security.SSLRead.argtypes = [SSLContextRef, c_char_p, c_size_t, POINTER(c_size_t)] Security.SSLRead.restype = OSStatus - Security.SSLWrite.argtypes = [ - SSLContextRef, - c_char_p, - c_size_t, - POINTER(c_size_t) - ] + Security.SSLWrite.argtypes = [SSLContextRef, c_char_p, c_size_t, POINTER(c_size_t)] Security.SSLWrite.restype = OSStatus - Security.SSLClose.argtypes = [ - SSLContextRef - ] + Security.SSLClose.argtypes = [SSLContextRef] Security.SSLClose.restype = OSStatus - Security.SSLGetNumberSupportedCiphers.argtypes = [ - SSLContextRef, - POINTER(c_size_t) - ] + Security.SSLGetNumberSupportedCiphers.argtypes = [SSLContextRef, POINTER(c_size_t)] Security.SSLGetNumberSupportedCiphers.restype = OSStatus Security.SSLGetSupportedCiphers.argtypes = [ SSLContextRef, POINTER(SSLCipherSuite), - POINTER(c_size_t) + POINTER(c_size_t), ] Security.SSLGetSupportedCiphers.restype = OSStatus Security.SSLSetEnabledCiphers.argtypes = [ SSLContextRef, POINTER(SSLCipherSuite), - c_size_t + c_size_t, ] Security.SSLSetEnabledCiphers.restype = OSStatus - Security.SSLGetNumberEnabledCiphers.argtype = [ - SSLContextRef, - POINTER(c_size_t) - ] + Security.SSLGetNumberEnabledCiphers.argtype = [SSLContextRef, POINTER(c_size_t)] Security.SSLGetNumberEnabledCiphers.restype = OSStatus Security.SSLGetEnabledCiphers.argtypes = [ SSLContextRef, POINTER(SSLCipherSuite), - POINTER(c_size_t) + POINTER(c_size_t), ] Security.SSLGetEnabledCiphers.restype = OSStatus - Security.SSLGetNegotiatedCipher.argtypes = [ - SSLContextRef, - POINTER(SSLCipherSuite) - ] + Security.SSLGetNegotiatedCipher.argtypes = [SSLContextRef, POINTER(SSLCipherSuite)] Security.SSLGetNegotiatedCipher.restype = OSStatus Security.SSLGetNegotiatedProtocolVersion.argtypes = [ SSLContextRef, - POINTER(SSLProtocol) + POINTER(SSLProtocol), ] Security.SSLGetNegotiatedProtocolVersion.restype = OSStatus - Security.SSLCopyPeerTrust.argtypes = [ - SSLContextRef, - POINTER(SecTrustRef) - ] + Security.SSLCopyPeerTrust.argtypes = [SSLContextRef, POINTER(SecTrustRef)] Security.SSLCopyPeerTrust.restype = OSStatus - Security.SecTrustSetAnchorCertificates.argtypes = [ - SecTrustRef, - CFArrayRef - ] + Security.SecTrustSetAnchorCertificates.argtypes = [SecTrustRef, CFArrayRef] Security.SecTrustSetAnchorCertificates.restype = OSStatus - Security.SecTrustSetAnchorCertificatesOnly.argstypes = [ - SecTrustRef, - Boolean - ] + Security.SecTrustSetAnchorCertificatesOnly.argstypes = [SecTrustRef, Boolean] Security.SecTrustSetAnchorCertificatesOnly.restype = OSStatus - Security.SecTrustEvaluate.argtypes = [ - SecTrustRef, - POINTER(SecTrustResultType) - ] + Security.SecTrustEvaluate.argtypes = [SecTrustRef, POINTER(SecTrustResultType)] Security.SecTrustEvaluate.restype = OSStatus - Security.SecTrustGetCertificateCount.argtypes = [ - SecTrustRef - ] + Security.SecTrustGetCertificateCount.argtypes = [SecTrustRef] Security.SecTrustGetCertificateCount.restype = CFIndex - Security.SecTrustGetCertificateAtIndex.argtypes = [ - SecTrustRef, - CFIndex - ] + Security.SecTrustGetCertificateAtIndex.argtypes = [SecTrustRef, CFIndex] Security.SecTrustGetCertificateAtIndex.restype = SecCertificateRef Security.SSLCreateContext.argtypes = [ CFAllocatorRef, SSLProtocolSide, - SSLConnectionType + SSLConnectionType, ] Security.SSLCreateContext.restype = SSLContextRef - Security.SSLSetSessionOption.argtypes = [ - SSLContextRef, - SSLSessionOption, - Boolean - ] + Security.SSLSetSessionOption.argtypes = [SSLContextRef, SSLSessionOption, Boolean] Security.SSLSetSessionOption.restype = OSStatus - Security.SSLSetProtocolVersionMin.argtypes = [ - SSLContextRef, - SSLProtocol - ] + Security.SSLSetProtocolVersionMin.argtypes = [SSLContextRef, SSLProtocol] Security.SSLSetProtocolVersionMin.restype = OSStatus - Security.SSLSetProtocolVersionMax.argtypes = [ - SSLContextRef, - SSLProtocol - ] + Security.SSLSetProtocolVersionMax.argtypes = [SSLContextRef, SSLProtocol] Security.SSLSetProtocolVersionMax.restype = OSStatus - Security.SecCopyErrorMessageString.argtypes = [ - OSStatus, - c_void_p - ] + Security.SecCopyErrorMessageString.argtypes = [OSStatus, c_void_p] Security.SecCopyErrorMessageString.restype = CFStringRef Security.SSLReadFunc = SSLReadFunc @@ -369,64 +292,47 @@ Security.OSStatus = OSStatus Security.kSecImportExportPassphrase = CFStringRef.in_dll( - Security, 'kSecImportExportPassphrase' + Security, "kSecImportExportPassphrase" ) Security.kSecImportItemIdentity = CFStringRef.in_dll( - Security, 'kSecImportItemIdentity' + Security, "kSecImportItemIdentity" ) # CoreFoundation time! - CoreFoundation.CFRetain.argtypes = [ - CFTypeRef - ] + CoreFoundation.CFRetain.argtypes = [CFTypeRef] CoreFoundation.CFRetain.restype = CFTypeRef - CoreFoundation.CFRelease.argtypes = [ - CFTypeRef - ] + CoreFoundation.CFRelease.argtypes = [CFTypeRef] CoreFoundation.CFRelease.restype = None - CoreFoundation.CFGetTypeID.argtypes = [ - CFTypeRef - ] + CoreFoundation.CFGetTypeID.argtypes = [CFTypeRef] CoreFoundation.CFGetTypeID.restype = CFTypeID CoreFoundation.CFStringCreateWithCString.argtypes = [ CFAllocatorRef, c_char_p, - CFStringEncoding + CFStringEncoding, ] CoreFoundation.CFStringCreateWithCString.restype = CFStringRef - CoreFoundation.CFStringGetCStringPtr.argtypes = [ - CFStringRef, - CFStringEncoding - ] + CoreFoundation.CFStringGetCStringPtr.argtypes = [CFStringRef, CFStringEncoding] CoreFoundation.CFStringGetCStringPtr.restype = c_char_p CoreFoundation.CFStringGetCString.argtypes = [ CFStringRef, c_char_p, CFIndex, - CFStringEncoding + CFStringEncoding, ] CoreFoundation.CFStringGetCString.restype = c_bool - CoreFoundation.CFDataCreate.argtypes = [ - CFAllocatorRef, - c_char_p, - CFIndex - ] + CoreFoundation.CFDataCreate.argtypes = [CFAllocatorRef, c_char_p, CFIndex] CoreFoundation.CFDataCreate.restype = CFDataRef - CoreFoundation.CFDataGetLength.argtypes = [ - CFDataRef - ] + CoreFoundation.CFDataGetLength.argtypes = [CFDataRef] CoreFoundation.CFDataGetLength.restype = CFIndex - CoreFoundation.CFDataGetBytePtr.argtypes = [ - CFDataRef - ] + CoreFoundation.CFDataGetBytePtr.argtypes = [CFDataRef] CoreFoundation.CFDataGetBytePtr.restype = c_void_p CoreFoundation.CFDictionaryCreate.argtypes = [ @@ -435,14 +341,11 @@ POINTER(CFTypeRef), CFIndex, CFDictionaryKeyCallBacks, - CFDictionaryValueCallBacks + CFDictionaryValueCallBacks, ] CoreFoundation.CFDictionaryCreate.restype = CFDictionaryRef - CoreFoundation.CFDictionaryGetValue.argtypes = [ - CFDictionaryRef, - CFTypeRef - ] + CoreFoundation.CFDictionaryGetValue.argtypes = [CFDictionaryRef, CFTypeRef] CoreFoundation.CFDictionaryGetValue.restype = CFTypeRef CoreFoundation.CFArrayCreate.argtypes = [ @@ -456,36 +359,30 @@ CoreFoundation.CFArrayCreateMutable.argtypes = [ CFAllocatorRef, CFIndex, - CFArrayCallBacks + CFArrayCallBacks, ] CoreFoundation.CFArrayCreateMutable.restype = CFMutableArrayRef - CoreFoundation.CFArrayAppendValue.argtypes = [ - CFMutableArrayRef, - c_void_p - ] + CoreFoundation.CFArrayAppendValue.argtypes = [CFMutableArrayRef, c_void_p] CoreFoundation.CFArrayAppendValue.restype = None - CoreFoundation.CFArrayGetCount.argtypes = [ - CFArrayRef - ] + CoreFoundation.CFArrayGetCount.argtypes = [CFArrayRef] CoreFoundation.CFArrayGetCount.restype = CFIndex - CoreFoundation.CFArrayGetValueAtIndex.argtypes = [ - CFArrayRef, - CFIndex - ] + CoreFoundation.CFArrayGetValueAtIndex.argtypes = [CFArrayRef, CFIndex] CoreFoundation.CFArrayGetValueAtIndex.restype = c_void_p CoreFoundation.kCFAllocatorDefault = CFAllocatorRef.in_dll( - CoreFoundation, 'kCFAllocatorDefault' + CoreFoundation, "kCFAllocatorDefault" + ) + CoreFoundation.kCFTypeArrayCallBacks = c_void_p.in_dll( + CoreFoundation, "kCFTypeArrayCallBacks" ) - CoreFoundation.kCFTypeArrayCallBacks = c_void_p.in_dll(CoreFoundation, 'kCFTypeArrayCallBacks') CoreFoundation.kCFTypeDictionaryKeyCallBacks = c_void_p.in_dll( - CoreFoundation, 'kCFTypeDictionaryKeyCallBacks' + CoreFoundation, "kCFTypeDictionaryKeyCallBacks" ) CoreFoundation.kCFTypeDictionaryValueCallBacks = c_void_p.in_dll( - CoreFoundation, 'kCFTypeDictionaryValueCallBacks' + CoreFoundation, "kCFTypeDictionaryValueCallBacks" ) CoreFoundation.CFTypeRef = CFTypeRef @@ -494,7 +391,7 @@ CoreFoundation.CFDictionaryRef = CFDictionaryRef except (AttributeError): - raise ImportError('Error initializing ctypes') + raise ImportError("Error initializing ctypes") class CFConst(object): @@ -502,6 +399,7 @@ class CFConst(object): A class object that acts as essentially a namespace for CoreFoundation constants. """ + kCFStringEncodingUTF8 = CFStringEncoding(0x08000100) @@ -509,6 +407,7 @@ class SecurityConst(object): """ A class object that acts as essentially a namespace for Security constants. """ + kSSLSessionOptionBreakOnServerAuth = 0 kSSLProtocol2 = 1 diff --git a/src/pip/_vendor/urllib3/contrib/_securetransport/low_level.py b/src/pip/_vendor/urllib3/contrib/_securetransport/low_level.py index b13cd9e72c6..e60168cac14 100644 --- a/src/pip/_vendor/urllib3/contrib/_securetransport/low_level.py +++ b/src/pip/_vendor/urllib3/contrib/_securetransport/low_level.py @@ -66,22 +66,18 @@ def _cf_string_to_unicode(value): value_as_void_p = ctypes.cast(value, ctypes.POINTER(ctypes.c_void_p)) string = CoreFoundation.CFStringGetCStringPtr( - value_as_void_p, - CFConst.kCFStringEncodingUTF8 + value_as_void_p, CFConst.kCFStringEncodingUTF8 ) if string is None: buffer = ctypes.create_string_buffer(1024) result = CoreFoundation.CFStringGetCString( - value_as_void_p, - buffer, - 1024, - CFConst.kCFStringEncodingUTF8 + value_as_void_p, buffer, 1024, CFConst.kCFStringEncodingUTF8 ) if not result: - raise OSError('Error copying C string from CFStringRef') + raise OSError("Error copying C string from CFStringRef") string = buffer.value if string is not None: - string = string.decode('utf-8') + string = string.decode("utf-8") return string @@ -97,8 +93,8 @@ def _assert_no_error(error, exception_class=None): output = _cf_string_to_unicode(cf_error_string) CoreFoundation.CFRelease(cf_error_string) - if output is None or output == u'': - output = u'OSStatus %s' % error + if output is None or output == u"": + output = u"OSStatus %s" % error if exception_class is None: exception_class = ssl.SSLError @@ -115,8 +111,7 @@ def _cert_array_from_pem(pem_bundle): pem_bundle = pem_bundle.replace(b"\r\n", b"\n") der_certs = [ - base64.b64decode(match.group(1)) - for match in _PEM_CERTS_RE.finditer(pem_bundle) + base64.b64decode(match.group(1)) for match in _PEM_CERTS_RE.finditer(pem_bundle) ] if not der_certs: raise ssl.SSLError("No root certificates specified") @@ -124,7 +119,7 @@ def _cert_array_from_pem(pem_bundle): cert_array = CoreFoundation.CFArrayCreateMutable( CoreFoundation.kCFAllocatorDefault, 0, - ctypes.byref(CoreFoundation.kCFTypeArrayCallBacks) + ctypes.byref(CoreFoundation.kCFTypeArrayCallBacks), ) if not cert_array: raise ssl.SSLError("Unable to allocate memory!") @@ -186,21 +181,16 @@ def _temporary_keychain(): # some random bytes to password-protect the keychain we're creating, so we # ask for 40 random bytes. random_bytes = os.urandom(40) - filename = base64.b16encode(random_bytes[:8]).decode('utf-8') + filename = base64.b16encode(random_bytes[:8]).decode("utf-8") password = base64.b16encode(random_bytes[8:]) # Must be valid UTF-8 tempdirectory = tempfile.mkdtemp() - keychain_path = os.path.join(tempdirectory, filename).encode('utf-8') + keychain_path = os.path.join(tempdirectory, filename).encode("utf-8") # We now want to create the keychain itself. keychain = Security.SecKeychainRef() status = Security.SecKeychainCreate( - keychain_path, - len(password), - password, - False, - None, - ctypes.byref(keychain) + keychain_path, len(password), password, False, None, ctypes.byref(keychain) ) _assert_no_error(status) @@ -219,14 +209,12 @@ def _load_items_from_file(keychain, path): identities = [] result_array = None - with open(path, 'rb') as f: + with open(path, "rb") as f: raw_filedata = f.read() try: filedata = CoreFoundation.CFDataCreate( - CoreFoundation.kCFAllocatorDefault, - raw_filedata, - len(raw_filedata) + CoreFoundation.kCFAllocatorDefault, raw_filedata, len(raw_filedata) ) result_array = CoreFoundation.CFArrayRef() result = Security.SecItemImport( @@ -237,7 +225,7 @@ def _load_items_from_file(keychain, path): 0, # import flags None, # key params, can include passphrase in the future keychain, # The keychain to insert into - ctypes.byref(result_array) # Results + ctypes.byref(result_array), # Results ) _assert_no_error(result) @@ -247,9 +235,7 @@ def _load_items_from_file(keychain, path): # keychain already has them! result_count = CoreFoundation.CFArrayGetCount(result_array) for index in range(result_count): - item = CoreFoundation.CFArrayGetValueAtIndex( - result_array, index - ) + item = CoreFoundation.CFArrayGetValueAtIndex(result_array, index) item = ctypes.cast(item, CoreFoundation.CFTypeRef) if _is_cert(item): @@ -307,9 +293,7 @@ def _load_client_cert_chain(keychain, *paths): try: for file_path in paths: - new_identities, new_certs = _load_items_from_file( - keychain, file_path - ) + new_identities, new_certs = _load_items_from_file(keychain, file_path) identities.extend(new_identities) certificates.extend(new_certs) @@ -318,9 +302,7 @@ def _load_client_cert_chain(keychain, *paths): if not identities: new_identity = Security.SecIdentityRef() status = Security.SecIdentityCreateWithCertificate( - keychain, - certificates[0], - ctypes.byref(new_identity) + keychain, certificates[0], ctypes.byref(new_identity) ) _assert_no_error(status) identities.append(new_identity) diff --git a/src/pip/_vendor/urllib3/contrib/appengine.py b/src/pip/_vendor/urllib3/contrib/appengine.py index 9b42952d7bf..886b3a7bc0f 100644 --- a/src/pip/_vendor/urllib3/contrib/appengine.py +++ b/src/pip/_vendor/urllib3/contrib/appengine.py @@ -50,7 +50,7 @@ MaxRetryError, ProtocolError, TimeoutError, - SSLError + SSLError, ) from ..request import RequestMethods @@ -96,23 +96,31 @@ class AppEngineManager(RequestMethods): Beyond those cases, it will raise normal urllib3 errors. """ - def __init__(self, headers=None, retries=None, validate_certificate=True, - urlfetch_retries=True): + def __init__( + self, + headers=None, + retries=None, + validate_certificate=True, + urlfetch_retries=True, + ): if not urlfetch: raise AppEnginePlatformError( - "URLFetch is not available in this environment.") + "URLFetch is not available in this environment." + ) if is_prod_appengine_mvms(): raise AppEnginePlatformError( "Use normal urllib3.PoolManager instead of AppEngineManager" "on Managed VMs, as using URLFetch is not necessary in " - "this environment.") + "this environment." + ) warnings.warn( "urllib3 is using URLFetch on Google App Engine sandbox instead " "of sockets. To use sockets directly instead of URLFetch see " "https://urllib3.readthedocs.io/en/latest/reference/urllib3.contrib.html.", - AppEnginePlatformWarning) + AppEnginePlatformWarning, + ) RequestMethods.__init__(self, headers) self.validate_certificate = validate_certificate @@ -127,17 +135,22 @@ def __exit__(self, exc_type, exc_val, exc_tb): # Return False to re-raise any potential exceptions return False - def urlopen(self, method, url, body=None, headers=None, - retries=None, redirect=True, timeout=Timeout.DEFAULT_TIMEOUT, - **response_kw): + def urlopen( + self, + method, + url, + body=None, + headers=None, + retries=None, + redirect=True, + timeout=Timeout.DEFAULT_TIMEOUT, + **response_kw + ): retries = self._get_retries(retries, redirect) try: - follow_redirects = ( - redirect and - retries.redirect != 0 and - retries.total) + follow_redirects = redirect and retries.redirect != 0 and retries.total response = urlfetch.fetch( url, payload=body, @@ -152,44 +165,52 @@ def urlopen(self, method, url, body=None, headers=None, raise TimeoutError(self, e) except urlfetch.InvalidURLError as e: - if 'too large' in str(e): + if "too large" in str(e): raise AppEnginePlatformError( "URLFetch request too large, URLFetch only " - "supports requests up to 10mb in size.", e) + "supports requests up to 10mb in size.", + e, + ) raise ProtocolError(e) except urlfetch.DownloadError as e: - if 'Too many redirects' in str(e): + if "Too many redirects" in str(e): raise MaxRetryError(self, url, reason=e) raise ProtocolError(e) except urlfetch.ResponseTooLargeError as e: raise AppEnginePlatformError( "URLFetch response too large, URLFetch only supports" - "responses up to 32mb in size.", e) + "responses up to 32mb in size.", + e, + ) except urlfetch.SSLCertificateError as e: raise SSLError(e) except urlfetch.InvalidMethodError as e: raise AppEnginePlatformError( - "URLFetch does not support method: %s" % method, e) + "URLFetch does not support method: %s" % method, e + ) http_response = self._urlfetch_response_to_http_response( - response, retries=retries, **response_kw) + response, retries=retries, **response_kw + ) # Handle redirect? redirect_location = redirect and http_response.get_redirect_location() if redirect_location: # Check for redirect response - if (self.urlfetch_retries and retries.raise_on_redirect): + if self.urlfetch_retries and retries.raise_on_redirect: raise MaxRetryError(self, url, "too many redirects") else: if http_response.status == 303: - method = 'GET' + method = "GET" try: - retries = retries.increment(method, url, response=http_response, _pool=self) + retries = retries.increment( + method, url, response=http_response, _pool=self + ) except MaxRetryError: if retries.raise_on_redirect: raise MaxRetryError(self, url, "too many redirects") @@ -199,22 +220,32 @@ def urlopen(self, method, url, body=None, headers=None, log.debug("Redirecting %s -> %s", url, redirect_location) redirect_url = urljoin(url, redirect_location) return self.urlopen( - method, redirect_url, body, headers, - retries=retries, redirect=redirect, - timeout=timeout, **response_kw) + method, + redirect_url, + body, + headers, + retries=retries, + redirect=redirect, + timeout=timeout, + **response_kw + ) # Check if we should retry the HTTP response. - has_retry_after = bool(http_response.getheader('Retry-After')) + has_retry_after = bool(http_response.getheader("Retry-After")) if retries.is_retry(method, http_response.status, has_retry_after): - retries = retries.increment( - method, url, response=http_response, _pool=self) + retries = retries.increment(method, url, response=http_response, _pool=self) log.debug("Retry: %s", url) retries.sleep(http_response) return self.urlopen( - method, url, - body=body, headers=headers, - retries=retries, redirect=redirect, - timeout=timeout, **response_kw) + method, + url, + body=body, + headers=headers, + retries=retries, + redirect=redirect, + timeout=timeout, + **response_kw + ) return http_response @@ -223,18 +254,18 @@ def _urlfetch_response_to_http_response(self, urlfetch_resp, **response_kw): if is_prod_appengine(): # Production GAE handles deflate encoding automatically, but does # not remove the encoding header. - content_encoding = urlfetch_resp.headers.get('content-encoding') + content_encoding = urlfetch_resp.headers.get("content-encoding") - if content_encoding == 'deflate': - del urlfetch_resp.headers['content-encoding'] + if content_encoding == "deflate": + del urlfetch_resp.headers["content-encoding"] - transfer_encoding = urlfetch_resp.headers.get('transfer-encoding') + transfer_encoding = urlfetch_resp.headers.get("transfer-encoding") # We have a full response's content, # so let's make sure we don't report ourselves as chunked data. - if transfer_encoding == 'chunked': + if transfer_encoding == "chunked": encodings = transfer_encoding.split(",") - encodings.remove('chunked') - urlfetch_resp.headers['transfer-encoding'] = ','.join(encodings) + encodings.remove("chunked") + urlfetch_resp.headers["transfer-encoding"] = ",".join(encodings) original_response = HTTPResponse( # In order for decoding to work, we must present the content as @@ -262,20 +293,21 @@ def _get_absolute_timeout(self, timeout): warnings.warn( "URLFetch does not support granular timeout settings, " "reverting to total or default URLFetch timeout.", - AppEnginePlatformWarning) + AppEnginePlatformWarning, + ) return timeout.total return timeout def _get_retries(self, retries, redirect): if not isinstance(retries, Retry): - retries = Retry.from_int( - retries, redirect=redirect, default=self.retries) + retries = Retry.from_int(retries, redirect=redirect, default=self.retries) if retries.connect or retries.read or retries.redirect: warnings.warn( "URLFetch only supports total retries and does not " "recognize connect, read, or redirect retry parameters.", - AppEnginePlatformWarning) + AppEnginePlatformWarning, + ) return retries diff --git a/src/pip/_vendor/urllib3/contrib/ntlmpool.py b/src/pip/_vendor/urllib3/contrib/ntlmpool.py index 8ea127c5833..9c96be29d8a 100644 --- a/src/pip/_vendor/urllib3/contrib/ntlmpool.py +++ b/src/pip/_vendor/urllib3/contrib/ntlmpool.py @@ -20,7 +20,7 @@ class NTLMConnectionPool(HTTPSConnectionPool): Implements an NTLM authentication version of an urllib3 connection pool """ - scheme = 'https' + scheme = "https" def __init__(self, user, pw, authurl, *args, **kwargs): """ @@ -31,7 +31,7 @@ def __init__(self, user, pw, authurl, *args, **kwargs): super(NTLMConnectionPool, self).__init__(*args, **kwargs) self.authurl = authurl self.rawuser = user - user_parts = user.split('\\', 1) + user_parts = user.split("\\", 1) self.domain = user_parts[0].upper() self.user = user_parts[1] self.pw = pw @@ -40,72 +40,84 @@ def _new_conn(self): # Performs the NTLM handshake that secures the connection. The socket # must be kept open while requests are performed. self.num_connections += 1 - log.debug('Starting NTLM HTTPS connection no. %d: https://%s%s', - self.num_connections, self.host, self.authurl) + log.debug( + "Starting NTLM HTTPS connection no. %d: https://%s%s", + self.num_connections, + self.host, + self.authurl, + ) - headers = {'Connection': 'Keep-Alive'} - req_header = 'Authorization' - resp_header = 'www-authenticate' + headers = {"Connection": "Keep-Alive"} + req_header = "Authorization" + resp_header = "www-authenticate" conn = HTTPSConnection(host=self.host, port=self.port) # Send negotiation message - headers[req_header] = ( - 'NTLM %s' % ntlm.create_NTLM_NEGOTIATE_MESSAGE(self.rawuser)) - log.debug('Request headers: %s', headers) - conn.request('GET', self.authurl, None, headers) + headers[req_header] = "NTLM %s" % ntlm.create_NTLM_NEGOTIATE_MESSAGE( + self.rawuser + ) + log.debug("Request headers: %s", headers) + conn.request("GET", self.authurl, None, headers) res = conn.getresponse() reshdr = dict(res.getheaders()) - log.debug('Response status: %s %s', res.status, res.reason) - log.debug('Response headers: %s', reshdr) - log.debug('Response data: %s [...]', res.read(100)) + log.debug("Response status: %s %s", res.status, res.reason) + log.debug("Response headers: %s", reshdr) + log.debug("Response data: %s [...]", res.read(100)) # Remove the reference to the socket, so that it can not be closed by # the response object (we want to keep the socket open) res.fp = None # Server should respond with a challenge message - auth_header_values = reshdr[resp_header].split(', ') + auth_header_values = reshdr[resp_header].split(", ") auth_header_value = None for s in auth_header_values: - if s[:5] == 'NTLM ': + if s[:5] == "NTLM ": auth_header_value = s[5:] if auth_header_value is None: - raise Exception('Unexpected %s response header: %s' % - (resp_header, reshdr[resp_header])) + raise Exception( + "Unexpected %s response header: %s" % (resp_header, reshdr[resp_header]) + ) # Send authentication message - ServerChallenge, NegotiateFlags = \ - ntlm.parse_NTLM_CHALLENGE_MESSAGE(auth_header_value) - auth_msg = ntlm.create_NTLM_AUTHENTICATE_MESSAGE(ServerChallenge, - self.user, - self.domain, - self.pw, - NegotiateFlags) - headers[req_header] = 'NTLM %s' % auth_msg - log.debug('Request headers: %s', headers) - conn.request('GET', self.authurl, None, headers) + ServerChallenge, NegotiateFlags = ntlm.parse_NTLM_CHALLENGE_MESSAGE( + auth_header_value + ) + auth_msg = ntlm.create_NTLM_AUTHENTICATE_MESSAGE( + ServerChallenge, self.user, self.domain, self.pw, NegotiateFlags + ) + headers[req_header] = "NTLM %s" % auth_msg + log.debug("Request headers: %s", headers) + conn.request("GET", self.authurl, None, headers) res = conn.getresponse() - log.debug('Response status: %s %s', res.status, res.reason) - log.debug('Response headers: %s', dict(res.getheaders())) - log.debug('Response data: %s [...]', res.read()[:100]) + log.debug("Response status: %s %s", res.status, res.reason) + log.debug("Response headers: %s", dict(res.getheaders())) + log.debug("Response data: %s [...]", res.read()[:100]) if res.status != 200: if res.status == 401: - raise Exception('Server rejected request: wrong ' - 'username or password') - raise Exception('Wrong server response: %s %s' % - (res.status, res.reason)) + raise Exception( + "Server rejected request: wrong " "username or password" + ) + raise Exception("Wrong server response: %s %s" % (res.status, res.reason)) res.fp = None - log.debug('Connection established') + log.debug("Connection established") return conn - def urlopen(self, method, url, body=None, headers=None, retries=3, - redirect=True, assert_same_host=True): + def urlopen( + self, + method, + url, + body=None, + headers=None, + retries=3, + redirect=True, + assert_same_host=True, + ): if headers is None: headers = {} - headers['Connection'] = 'Keep-Alive' - return super(NTLMConnectionPool, self).urlopen(method, url, body, - headers, retries, - redirect, - assert_same_host) + headers["Connection"] = "Keep-Alive" + return super(NTLMConnectionPool, self).urlopen( + method, url, body, headers, retries, redirect, assert_same_host + ) diff --git a/src/pip/_vendor/urllib3/contrib/pyopenssl.py b/src/pip/_vendor/urllib3/contrib/pyopenssl.py index abfc3191325..fc99d34bd4c 100644 --- a/src/pip/_vendor/urllib3/contrib/pyopenssl.py +++ b/src/pip/_vendor/urllib3/contrib/pyopenssl.py @@ -47,6 +47,7 @@ from cryptography import x509 from cryptography.hazmat.backends.openssl import backend as openssl_backend from cryptography.hazmat.backends.openssl.x509 import _Certificate + try: from cryptography.x509 import UnsupportedExtension except ImportError: @@ -54,6 +55,7 @@ class UnsupportedExtension(Exception): pass + from socket import timeout, error as SocketError from io import BytesIO @@ -71,7 +73,7 @@ class UnsupportedExtension(Exception): from .. import util -__all__ = ['inject_into_urllib3', 'extract_from_urllib3'] +__all__ = ["inject_into_urllib3", "extract_from_urllib3"] # SNI always works. HAS_SNI = True @@ -82,25 +84,23 @@ class UnsupportedExtension(Exception): ssl.PROTOCOL_TLSv1: OpenSSL.SSL.TLSv1_METHOD, } -if hasattr(ssl, 'PROTOCOL_SSLv3') and hasattr(OpenSSL.SSL, 'SSLv3_METHOD'): +if hasattr(ssl, "PROTOCOL_SSLv3") and hasattr(OpenSSL.SSL, "SSLv3_METHOD"): _openssl_versions[ssl.PROTOCOL_SSLv3] = OpenSSL.SSL.SSLv3_METHOD -if hasattr(ssl, 'PROTOCOL_TLSv1_1') and hasattr(OpenSSL.SSL, 'TLSv1_1_METHOD'): +if hasattr(ssl, "PROTOCOL_TLSv1_1") and hasattr(OpenSSL.SSL, "TLSv1_1_METHOD"): _openssl_versions[ssl.PROTOCOL_TLSv1_1] = OpenSSL.SSL.TLSv1_1_METHOD -if hasattr(ssl, 'PROTOCOL_TLSv1_2') and hasattr(OpenSSL.SSL, 'TLSv1_2_METHOD'): +if hasattr(ssl, "PROTOCOL_TLSv1_2") and hasattr(OpenSSL.SSL, "TLSv1_2_METHOD"): _openssl_versions[ssl.PROTOCOL_TLSv1_2] = OpenSSL.SSL.TLSv1_2_METHOD _stdlib_to_openssl_verify = { ssl.CERT_NONE: OpenSSL.SSL.VERIFY_NONE, ssl.CERT_OPTIONAL: OpenSSL.SSL.VERIFY_PEER, - ssl.CERT_REQUIRED: - OpenSSL.SSL.VERIFY_PEER + OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT, + ssl.CERT_REQUIRED: OpenSSL.SSL.VERIFY_PEER + + OpenSSL.SSL.VERIFY_FAIL_IF_NO_PEER_CERT, } -_openssl_to_stdlib_verify = dict( - (v, k) for k, v in _stdlib_to_openssl_verify.items() -) +_openssl_to_stdlib_verify = dict((v, k) for k, v in _stdlib_to_openssl_verify.items()) # OpenSSL will only write 16K at a time SSL_WRITE_BLOCKSIZE = 16384 @@ -113,7 +113,7 @@ class UnsupportedExtension(Exception): def inject_into_urllib3(): - 'Monkey-patch urllib3 with PyOpenSSL-backed SSL-support.' + "Monkey-patch urllib3 with PyOpenSSL-backed SSL-support." _validate_dependencies_met() @@ -126,7 +126,7 @@ def inject_into_urllib3(): def extract_from_urllib3(): - 'Undo monkey-patching by :func:`inject_into_urllib3`.' + "Undo monkey-patching by :func:`inject_into_urllib3`." util.SSLContext = orig_util_SSLContext util.ssl_.SSLContext = orig_util_SSLContext @@ -143,17 +143,23 @@ def _validate_dependencies_met(): """ # Method added in `cryptography==1.1`; not available in older versions from cryptography.x509.extensions import Extensions + if getattr(Extensions, "get_extension_for_class", None) is None: - raise ImportError("'cryptography' module missing required functionality. " - "Try upgrading to v1.3.4 or newer.") + raise ImportError( + "'cryptography' module missing required functionality. " + "Try upgrading to v1.3.4 or newer." + ) # pyOpenSSL 0.14 and above use cryptography for OpenSSL bindings. The _x509 # attribute is only present on those versions. from OpenSSL.crypto import X509 + x509 = X509() if getattr(x509, "_x509", None) is None: - raise ImportError("'pyOpenSSL' module missing required functionality. " - "Try upgrading to v0.14 or newer.") + raise ImportError( + "'pyOpenSSL' module missing required functionality. " + "Try upgrading to v0.14 or newer." + ) def _dnsname_to_stdlib(name): @@ -169,6 +175,7 @@ def _dnsname_to_stdlib(name): If the name cannot be idna-encoded then we return None signalling that the name given should be skipped. """ + def idna_encode(name): """ Borrowed wholesale from the Python Cryptography Project. It turns out @@ -178,23 +185,23 @@ def idna_encode(name): from pip._vendor import idna try: - for prefix in [u'*.', u'.']: + for prefix in [u"*.", u"."]: if name.startswith(prefix): - name = name[len(prefix):] - return prefix.encode('ascii') + idna.encode(name) + name = name[len(prefix) :] + return prefix.encode("ascii") + idna.encode(name) return idna.encode(name) except idna.core.IDNAError: return None # Don't send IPv6 addresses through the IDNA encoder. - if ':' in name: + if ":" in name: return name name = idna_encode(name) if name is None: return None elif sys.version_info >= (3, 0): - name = name.decode('utf-8') + name = name.decode("utf-8") return name @@ -213,14 +220,16 @@ def get_subj_alt_name(peer_cert): # We want to find the SAN extension. Ask Cryptography to locate it (it's # faster than looping in Python) try: - ext = cert.extensions.get_extension_for_class( - x509.SubjectAlternativeName - ).value + ext = cert.extensions.get_extension_for_class(x509.SubjectAlternativeName).value except x509.ExtensionNotFound: # No such extension, return the empty list. return [] - except (x509.DuplicateExtension, UnsupportedExtension, - x509.UnsupportedGeneralNameType, UnicodeError) as e: + except ( + x509.DuplicateExtension, + UnsupportedExtension, + x509.UnsupportedGeneralNameType, + UnicodeError, + ) as e: # A problem has been found with the quality of the certificate. Assume # no SAN field is present. log.warning( @@ -239,23 +248,23 @@ def get_subj_alt_name(peer_cert): # does with certificates, and so we need to attempt to do the same. # We also want to skip over names which cannot be idna encoded. names = [ - ('DNS', name) for name in map(_dnsname_to_stdlib, ext.get_values_for_type(x509.DNSName)) + ("DNS", name) + for name in map(_dnsname_to_stdlib, ext.get_values_for_type(x509.DNSName)) if name is not None ] names.extend( - ('IP Address', str(name)) - for name in ext.get_values_for_type(x509.IPAddress) + ("IP Address", str(name)) for name in ext.get_values_for_type(x509.IPAddress) ) return names class WrappedSocket(object): - '''API-compatibility wrapper for Python OpenSSL's Connection-class. + """API-compatibility wrapper for Python OpenSSL's Connection-class. Note: _makefile_refs, _drop() and _reuse() are needed for the garbage collector of pypy. - ''' + """ def __init__(self, connection, socket, suppress_ragged_eofs=True): self.connection = connection @@ -278,18 +287,18 @@ def recv(self, *args, **kwargs): try: data = self.connection.recv(*args, **kwargs) except OpenSSL.SSL.SysCallError as e: - if self.suppress_ragged_eofs and e.args == (-1, 'Unexpected EOF'): - return b'' + if self.suppress_ragged_eofs and e.args == (-1, "Unexpected EOF"): + return b"" else: raise SocketError(str(e)) except OpenSSL.SSL.ZeroReturnError: if self.connection.get_shutdown() == OpenSSL.SSL.RECEIVED_SHUTDOWN: - return b'' + return b"" else: raise except OpenSSL.SSL.WantReadError: if not util.wait_for_read(self.socket, self.socket.gettimeout()): - raise timeout('The read operation timed out') + raise timeout("The read operation timed out") else: return self.recv(*args, **kwargs) @@ -303,7 +312,7 @@ def recv_into(self, *args, **kwargs): try: return self.connection.recv_into(*args, **kwargs) except OpenSSL.SSL.SysCallError as e: - if self.suppress_ragged_eofs and e.args == (-1, 'Unexpected EOF'): + if self.suppress_ragged_eofs and e.args == (-1, "Unexpected EOF"): return 0 else: raise SocketError(str(e)) @@ -314,7 +323,7 @@ def recv_into(self, *args, **kwargs): raise except OpenSSL.SSL.WantReadError: if not util.wait_for_read(self.socket, self.socket.gettimeout()): - raise timeout('The read operation timed out') + raise timeout("The read operation timed out") else: return self.recv_into(*args, **kwargs) @@ -339,7 +348,9 @@ def _send_until_done(self, data): def sendall(self, data): total_sent = 0 while total_sent < len(data): - sent = self._send_until_done(data[total_sent:total_sent + SSL_WRITE_BLOCKSIZE]) + sent = self._send_until_done( + data[total_sent : total_sent + SSL_WRITE_BLOCKSIZE] + ) total_sent += sent def shutdown(self): @@ -363,15 +374,11 @@ def getpeercert(self, binary_form=False): return x509 if binary_form: - return OpenSSL.crypto.dump_certificate( - OpenSSL.crypto.FILETYPE_ASN1, - x509) + return OpenSSL.crypto.dump_certificate(OpenSSL.crypto.FILETYPE_ASN1, x509) return { - 'subject': ( - (('commonName', x509.get_subject().CN),), - ), - 'subjectAltName': get_subj_alt_name(x509) + "subject": ((("commonName", x509.get_subject().CN),),), + "subjectAltName": get_subj_alt_name(x509), } def version(self): @@ -388,9 +395,12 @@ def _drop(self): if _fileobject: # Platform-specific: Python 2 + def makefile(self, mode, bufsize=-1): self._makefile_refs += 1 return _fileobject(self, mode, bufsize, close=True) + + else: # Platform-specific: Python 3 makefile = backport_makefile @@ -403,6 +413,7 @@ class PyOpenSSLContext(object): for translating the interface of the standard library ``SSLContext`` object to calls into PyOpenSSL. """ + def __init__(self, protocol): self.protocol = _openssl_versions[protocol] self._ctx = OpenSSL.SSL.Context(self.protocol) @@ -424,24 +435,21 @@ def verify_mode(self): @verify_mode.setter def verify_mode(self, value): - self._ctx.set_verify( - _stdlib_to_openssl_verify[value], - _verify_callback - ) + self._ctx.set_verify(_stdlib_to_openssl_verify[value], _verify_callback) def set_default_verify_paths(self): self._ctx.set_default_verify_paths() def set_ciphers(self, ciphers): if isinstance(ciphers, six.text_type): - ciphers = ciphers.encode('utf-8') + ciphers = ciphers.encode("utf-8") self._ctx.set_cipher_list(ciphers) def load_verify_locations(self, cafile=None, capath=None, cadata=None): if cafile is not None: - cafile = cafile.encode('utf-8') + cafile = cafile.encode("utf-8") if capath is not None: - capath = capath.encode('utf-8') + capath = capath.encode("utf-8") self._ctx.load_verify_locations(cafile, capath) if cadata is not None: self._ctx.load_verify_locations(BytesIO(cadata)) @@ -450,17 +458,22 @@ def load_cert_chain(self, certfile, keyfile=None, password=None): self._ctx.use_certificate_chain_file(certfile) if password is not None: if not isinstance(password, six.binary_type): - password = password.encode('utf-8') + password = password.encode("utf-8") self._ctx.set_passwd_cb(lambda *_: password) self._ctx.use_privatekey_file(keyfile or certfile) - def wrap_socket(self, sock, server_side=False, - do_handshake_on_connect=True, suppress_ragged_eofs=True, - server_hostname=None): + def wrap_socket( + self, + sock, + server_side=False, + do_handshake_on_connect=True, + suppress_ragged_eofs=True, + server_hostname=None, + ): cnx = OpenSSL.SSL.Connection(self._ctx, sock) if isinstance(server_hostname, six.text_type): # Platform-specific: Python 3 - server_hostname = server_hostname.encode('utf-8') + server_hostname = server_hostname.encode("utf-8") if server_hostname is not None: cnx.set_tlsext_host_name(server_hostname) @@ -472,10 +485,10 @@ def wrap_socket(self, sock, server_side=False, cnx.do_handshake() except OpenSSL.SSL.WantReadError: if not util.wait_for_read(sock, sock.gettimeout()): - raise timeout('select timed out') + raise timeout("select timed out") continue except OpenSSL.SSL.Error as e: - raise ssl.SSLError('bad handshake: %r' % e) + raise ssl.SSLError("bad handshake: %r" % e) break return WrappedSocket(cnx, sock) diff --git a/src/pip/_vendor/urllib3/contrib/securetransport.py b/src/pip/_vendor/urllib3/contrib/securetransport.py index 4dc48484167..24e6b5c4d9e 100644 --- a/src/pip/_vendor/urllib3/contrib/securetransport.py +++ b/src/pip/_vendor/urllib3/contrib/securetransport.py @@ -62,12 +62,12 @@ import weakref from .. import util -from ._securetransport.bindings import ( - Security, SecurityConst, CoreFoundation -) +from ._securetransport.bindings import Security, SecurityConst, CoreFoundation from ._securetransport.low_level import ( - _assert_no_error, _cert_array_from_pem, _temporary_keychain, - _load_client_cert_chain + _assert_no_error, + _cert_array_from_pem, + _temporary_keychain, + _load_client_cert_chain, ) try: # Platform-specific: Python 2 @@ -76,7 +76,7 @@ _fileobject = None from ..packages.backports.makefile import backport_makefile -__all__ = ['inject_into_urllib3', 'extract_from_urllib3'] +__all__ = ["inject_into_urllib3", "extract_from_urllib3"] # SNI always works HAS_SNI = True @@ -147,28 +147,36 @@ # TLSv1 and a high of TLSv1.3. For everything else, we pin to that version. # TLSv1 to 1.2 are supported on macOS 10.8+ and TLSv1.3 is macOS 10.13+ _protocol_to_min_max = { - util.PROTOCOL_TLS: (SecurityConst.kTLSProtocol1, SecurityConst.kTLSProtocolMaxSupported), + util.PROTOCOL_TLS: ( + SecurityConst.kTLSProtocol1, + SecurityConst.kTLSProtocolMaxSupported, + ) } if hasattr(ssl, "PROTOCOL_SSLv2"): _protocol_to_min_max[ssl.PROTOCOL_SSLv2] = ( - SecurityConst.kSSLProtocol2, SecurityConst.kSSLProtocol2 + SecurityConst.kSSLProtocol2, + SecurityConst.kSSLProtocol2, ) if hasattr(ssl, "PROTOCOL_SSLv3"): _protocol_to_min_max[ssl.PROTOCOL_SSLv3] = ( - SecurityConst.kSSLProtocol3, SecurityConst.kSSLProtocol3 + SecurityConst.kSSLProtocol3, + SecurityConst.kSSLProtocol3, ) if hasattr(ssl, "PROTOCOL_TLSv1"): _protocol_to_min_max[ssl.PROTOCOL_TLSv1] = ( - SecurityConst.kTLSProtocol1, SecurityConst.kTLSProtocol1 + SecurityConst.kTLSProtocol1, + SecurityConst.kTLSProtocol1, ) if hasattr(ssl, "PROTOCOL_TLSv1_1"): _protocol_to_min_max[ssl.PROTOCOL_TLSv1_1] = ( - SecurityConst.kTLSProtocol11, SecurityConst.kTLSProtocol11 + SecurityConst.kTLSProtocol11, + SecurityConst.kTLSProtocol11, ) if hasattr(ssl, "PROTOCOL_TLSv1_2"): _protocol_to_min_max[ssl.PROTOCOL_TLSv1_2] = ( - SecurityConst.kTLSProtocol12, SecurityConst.kTLSProtocol12 + SecurityConst.kTLSProtocol12, + SecurityConst.kTLSProtocol12, ) @@ -218,7 +226,7 @@ def _read_callback(connection_id, data_buffer, data_length_pointer): while read_count < requested_length: if timeout is None or timeout >= 0: if not util.wait_for_read(base_socket, timeout): - raise socket.error(errno.EAGAIN, 'timed out') + raise socket.error(errno.EAGAIN, "timed out") remaining = requested_length - read_count buffer = (ctypes.c_char * remaining).from_address( @@ -274,7 +282,7 @@ def _write_callback(connection_id, data_buffer, data_length_pointer): while sent < bytes_to_write: if timeout is None or timeout >= 0: if not util.wait_for_write(base_socket, timeout): - raise socket.error(errno.EAGAIN, 'timed out') + raise socket.error(errno.EAGAIN, "timed out") chunk_sent = base_socket.send(data) sent += chunk_sent @@ -316,6 +324,7 @@ class WrappedSocket(object): Note: _makefile_refs, _drop(), and _reuse() are needed for the garbage collector of PyPy. """ + def __init__(self, socket): self.socket = socket self.context = None @@ -380,7 +389,7 @@ def _custom_validate(self, verify, trust_bundle): # We want data in memory, so load it up. if os.path.isfile(trust_bundle): - with open(trust_bundle, 'rb') as f: + with open(trust_bundle, "rb") as f: trust_bundle = f.read() cert_array = None @@ -394,9 +403,7 @@ def _custom_validate(self, verify, trust_bundle): # created for this connection, shove our CAs into it, tell ST to # ignore everything else it knows, and then ask if it can build a # chain. This is a buuuunch of code. - result = Security.SSLCopyPeerTrust( - self.context, ctypes.byref(trust) - ) + result = Security.SSLCopyPeerTrust(self.context, ctypes.byref(trust)) _assert_no_error(result) if not trust: raise ssl.SSLError("Failed to copy trust reference") @@ -408,9 +415,7 @@ def _custom_validate(self, verify, trust_bundle): _assert_no_error(result) trust_result = Security.SecTrustResultType() - result = Security.SecTrustEvaluate( - trust, ctypes.byref(trust_result) - ) + result = Security.SecTrustEvaluate(trust, ctypes.byref(trust_result)) _assert_no_error(result) finally: if trust: @@ -422,23 +427,24 @@ def _custom_validate(self, verify, trust_bundle): # Ok, now we can look at what the result was. successes = ( SecurityConst.kSecTrustResultUnspecified, - SecurityConst.kSecTrustResultProceed + SecurityConst.kSecTrustResultProceed, ) if trust_result.value not in successes: raise ssl.SSLError( - "certificate verify failed, error code: %d" % - trust_result.value + "certificate verify failed, error code: %d" % trust_result.value ) - def handshake(self, - server_hostname, - verify, - trust_bundle, - min_version, - max_version, - client_cert, - client_key, - client_key_passphrase): + def handshake( + self, + server_hostname, + verify, + trust_bundle, + min_version, + max_version, + client_cert, + client_key, + client_key_passphrase, + ): """ Actually performs the TLS handshake. This is run automatically by wrapped socket, and shouldn't be needed in user code. @@ -468,7 +474,7 @@ def handshake(self, # If we have a server hostname, we should set that too. if server_hostname: if not isinstance(server_hostname, bytes): - server_hostname = server_hostname.encode('utf-8') + server_hostname = server_hostname.encode("utf-8") result = Security.SSLSetPeerDomainName( self.context, server_hostname, len(server_hostname) @@ -488,7 +494,9 @@ def handshake(self, # was added in macOS 10.13 along with kTLSProtocol13. result = Security.SSLSetProtocolVersionMax(self.context, max_version) if result != 0 and max_version == SecurityConst.kTLSProtocolMaxSupported: - result = Security.SSLSetProtocolVersionMax(self.context, SecurityConst.kTLSProtocol12) + result = Security.SSLSetProtocolVersionMax( + self.context, SecurityConst.kTLSProtocol12 + ) _assert_no_error(result) # If there's a trust DB, we need to use it. We do that by telling @@ -497,9 +505,7 @@ def handshake(self, # authing in that case. if not verify or trust_bundle is not None: result = Security.SSLSetSessionOption( - self.context, - SecurityConst.kSSLSessionOptionBreakOnServerAuth, - True + self.context, SecurityConst.kSSLSessionOptionBreakOnServerAuth, True ) _assert_no_error(result) @@ -509,9 +515,7 @@ def handshake(self, self._client_cert_chain = _load_client_cert_chain( self._keychain, client_cert, client_key ) - result = Security.SSLSetCertificate( - self.context, self._client_cert_chain - ) + result = Security.SSLSetCertificate(self.context, self._client_cert_chain) _assert_no_error(result) while True: @@ -562,7 +566,7 @@ def recv_into(self, buffer, nbytes=None): # There are some result codes that we want to treat as "not always # errors". Specifically, those are errSSLWouldBlock, # errSSLClosedGraceful, and errSSLClosedNoNotify. - if (result == SecurityConst.errSSLWouldBlock): + if result == SecurityConst.errSSLWouldBlock: # If we didn't process any bytes, then this was just a time out. # However, we can get errSSLWouldBlock in situations when we *did* # read some data, and in those cases we should just read "short" @@ -570,7 +574,10 @@ def recv_into(self, buffer, nbytes=None): if processed_bytes.value == 0: # Timed out, no data read. raise socket.timeout("recv timed out") - elif result in (SecurityConst.errSSLClosedGraceful, SecurityConst.errSSLClosedNoNotify): + elif result in ( + SecurityConst.errSSLClosedGraceful, + SecurityConst.errSSLClosedNoNotify, + ): # The remote peer has closed this connection. We should do so as # well. Note that we don't actually return here because in # principle this could actually be fired along with return data. @@ -609,7 +616,7 @@ def send(self, data): def sendall(self, data): total_sent = 0 while total_sent < len(data): - sent = self.send(data[total_sent:total_sent + SSL_WRITE_BLOCKSIZE]) + sent = self.send(data[total_sent : total_sent + SSL_WRITE_BLOCKSIZE]) total_sent += sent def shutdown(self): @@ -656,18 +663,14 @@ def getpeercert(self, binary_form=False): # instead to just flag to urllib3 that it shouldn't do its own hostname # validation when using SecureTransport. if not binary_form: - raise ValueError( - "SecureTransport only supports dumping binary certs" - ) + raise ValueError("SecureTransport only supports dumping binary certs") trust = Security.SecTrustRef() certdata = None der_bytes = None try: # Grab the trust store. - result = Security.SSLCopyPeerTrust( - self.context, ctypes.byref(trust) - ) + result = Security.SSLCopyPeerTrust(self.context, ctypes.byref(trust)) _assert_no_error(result) if not trust: # Probably we haven't done the handshake yet. No biggie. @@ -699,22 +702,24 @@ def getpeercert(self, binary_form=False): def version(self): protocol = Security.SSLProtocol() - result = Security.SSLGetNegotiatedProtocolVersion(self.context, ctypes.byref(protocol)) + result = Security.SSLGetNegotiatedProtocolVersion( + self.context, ctypes.byref(protocol) + ) _assert_no_error(result) if protocol.value == SecurityConst.kTLSProtocol13: - return 'TLSv1.3' + return "TLSv1.3" elif protocol.value == SecurityConst.kTLSProtocol12: - return 'TLSv1.2' + return "TLSv1.2" elif protocol.value == SecurityConst.kTLSProtocol11: - return 'TLSv1.1' + return "TLSv1.1" elif protocol.value == SecurityConst.kTLSProtocol1: - return 'TLSv1' + return "TLSv1" elif protocol.value == SecurityConst.kSSLProtocol3: - return 'SSLv3' + return "SSLv3" elif protocol.value == SecurityConst.kSSLProtocol2: - return 'SSLv2' + return "SSLv2" else: - raise ssl.SSLError('Unknown TLS version: %r' % protocol) + raise ssl.SSLError("Unknown TLS version: %r" % protocol) def _reuse(self): self._makefile_refs += 1 @@ -727,16 +732,21 @@ def _drop(self): if _fileobject: # Platform-specific: Python 2 + def makefile(self, mode, bufsize=-1): self._makefile_refs += 1 return _fileobject(self, mode, bufsize, close=True) + + else: # Platform-specific: Python 3 + def makefile(self, mode="r", buffering=None, *args, **kwargs): # We disable buffering with SecureTransport because it conflicts with # the buffering that ST does internally (see issue #1153 for more). buffering = 0 return backport_makefile(self, mode, buffering, *args, **kwargs) + WrappedSocket.makefile = makefile @@ -746,6 +756,7 @@ class SecureTransportContext(object): interface of the standard library ``SSLContext`` object to calls into SecureTransport. """ + def __init__(self, protocol): self._min_version, self._max_version = _protocol_to_min_max[protocol] self._options = 0 @@ -812,16 +823,12 @@ def load_default_certs(self): def set_ciphers(self, ciphers): # For now, we just require the default cipher string. if ciphers != util.ssl_.DEFAULT_CIPHERS: - raise ValueError( - "SecureTransport doesn't support custom cipher strings" - ) + raise ValueError("SecureTransport doesn't support custom cipher strings") def load_verify_locations(self, cafile=None, capath=None, cadata=None): # OK, we only really support cadata and cafile. if capath is not None: - raise ValueError( - "SecureTransport does not support cert directories" - ) + raise ValueError("SecureTransport does not support cert directories") self._trust_bundle = cafile or cadata @@ -830,9 +837,14 @@ def load_cert_chain(self, certfile, keyfile=None, password=None): self._client_key = keyfile self._client_cert_passphrase = password - def wrap_socket(self, sock, server_side=False, - do_handshake_on_connect=True, suppress_ragged_eofs=True, - server_hostname=None): + def wrap_socket( + self, + sock, + server_side=False, + do_handshake_on_connect=True, + suppress_ragged_eofs=True, + server_hostname=None, + ): # So, what do we do here? Firstly, we assert some properties. This is a # stripped down shim, so there is some functionality we don't support. # See PEP 543 for the real deal. @@ -846,8 +858,13 @@ def wrap_socket(self, sock, server_side=False, # Now we can handshake wrapped_socket.handshake( - server_hostname, self._verify, self._trust_bundle, - self._min_version, self._max_version, self._client_cert, - self._client_key, self._client_key_passphrase + server_hostname, + self._verify, + self._trust_bundle, + self._min_version, + self._max_version, + self._client_cert, + self._client_key, + self._client_key_passphrase, ) return wrapped_socket diff --git a/src/pip/_vendor/urllib3/contrib/socks.py b/src/pip/_vendor/urllib3/contrib/socks.py index 636d261fb03..9e97f7aa98f 100644 --- a/src/pip/_vendor/urllib3/contrib/socks.py +++ b/src/pip/_vendor/urllib3/contrib/socks.py @@ -42,23 +42,20 @@ import warnings from ..exceptions import DependencyWarning - warnings.warn(( - 'SOCKS support in urllib3 requires the installation of optional ' - 'dependencies: specifically, PySocks. For more information, see ' - 'https://urllib3.readthedocs.io/en/latest/contrib.html#socks-proxies' + warnings.warn( + ( + "SOCKS support in urllib3 requires the installation of optional " + "dependencies: specifically, PySocks. For more information, see " + "https://urllib3.readthedocs.io/en/latest/contrib.html#socks-proxies" ), - DependencyWarning + DependencyWarning, ) raise from socket import error as SocketError, timeout as SocketTimeout -from ..connection import ( - HTTPConnection, HTTPSConnection -) -from ..connectionpool import ( - HTTPConnectionPool, HTTPSConnectionPool -) +from ..connection import HTTPConnection, HTTPSConnection +from ..connectionpool import HTTPConnectionPool, HTTPSConnectionPool from ..exceptions import ConnectTimeoutError, NewConnectionError from ..poolmanager import PoolManager from ..util.url import parse_url @@ -73,8 +70,9 @@ class SOCKSConnection(HTTPConnection): """ A plain-text HTTP connection that connects via a SOCKS proxy. """ + def __init__(self, *args, **kwargs): - self._socks_options = kwargs.pop('_socks_options') + self._socks_options = kwargs.pop("_socks_options") super(SOCKSConnection, self).__init__(*args, **kwargs) def _new_conn(self): @@ -83,28 +81,30 @@ def _new_conn(self): """ extra_kw = {} if self.source_address: - extra_kw['source_address'] = self.source_address + extra_kw["source_address"] = self.source_address if self.socket_options: - extra_kw['socket_options'] = self.socket_options + extra_kw["socket_options"] = self.socket_options try: conn = socks.create_connection( (self.host, self.port), - proxy_type=self._socks_options['socks_version'], - proxy_addr=self._socks_options['proxy_host'], - proxy_port=self._socks_options['proxy_port'], - proxy_username=self._socks_options['username'], - proxy_password=self._socks_options['password'], - proxy_rdns=self._socks_options['rdns'], + proxy_type=self._socks_options["socks_version"], + proxy_addr=self._socks_options["proxy_host"], + proxy_port=self._socks_options["proxy_port"], + proxy_username=self._socks_options["username"], + proxy_password=self._socks_options["password"], + proxy_rdns=self._socks_options["rdns"], timeout=self.timeout, **extra_kw ) except SocketTimeout: raise ConnectTimeoutError( - self, "Connection to %s timed out. (connect timeout=%s)" % - (self.host, self.timeout)) + self, + "Connection to %s timed out. (connect timeout=%s)" + % (self.host, self.timeout), + ) except socks.ProxyError as e: # This is fragile as hell, but it seems to be the only way to raise @@ -114,23 +114,22 @@ def _new_conn(self): if isinstance(error, SocketTimeout): raise ConnectTimeoutError( self, - "Connection to %s timed out. (connect timeout=%s)" % - (self.host, self.timeout) + "Connection to %s timed out. (connect timeout=%s)" + % (self.host, self.timeout), ) else: raise NewConnectionError( - self, - "Failed to establish a new connection: %s" % error + self, "Failed to establish a new connection: %s" % error ) else: raise NewConnectionError( - self, - "Failed to establish a new connection: %s" % e + self, "Failed to establish a new connection: %s" % e ) except SocketError as e: # Defensive: PySocks should catch all these. raise NewConnectionError( - self, "Failed to establish a new connection: %s" % e) + self, "Failed to establish a new connection: %s" % e + ) return conn @@ -156,47 +155,53 @@ class SOCKSProxyManager(PoolManager): A version of the urllib3 ProxyManager that routes connections via the defined SOCKS proxy. """ + pool_classes_by_scheme = { - 'http': SOCKSHTTPConnectionPool, - 'https': SOCKSHTTPSConnectionPool, + "http": SOCKSHTTPConnectionPool, + "https": SOCKSHTTPSConnectionPool, } - def __init__(self, proxy_url, username=None, password=None, - num_pools=10, headers=None, **connection_pool_kw): + def __init__( + self, + proxy_url, + username=None, + password=None, + num_pools=10, + headers=None, + **connection_pool_kw + ): parsed = parse_url(proxy_url) if username is None and password is None and parsed.auth is not None: - split = parsed.auth.split(':') + split = parsed.auth.split(":") if len(split) == 2: username, password = split - if parsed.scheme == 'socks5': + if parsed.scheme == "socks5": socks_version = socks.PROXY_TYPE_SOCKS5 rdns = False - elif parsed.scheme == 'socks5h': + elif parsed.scheme == "socks5h": socks_version = socks.PROXY_TYPE_SOCKS5 rdns = True - elif parsed.scheme == 'socks4': + elif parsed.scheme == "socks4": socks_version = socks.PROXY_TYPE_SOCKS4 rdns = False - elif parsed.scheme == 'socks4a': + elif parsed.scheme == "socks4a": socks_version = socks.PROXY_TYPE_SOCKS4 rdns = True else: - raise ValueError( - "Unable to determine SOCKS version from %s" % proxy_url - ) + raise ValueError("Unable to determine SOCKS version from %s" % proxy_url) self.proxy_url = proxy_url socks_options = { - 'socks_version': socks_version, - 'proxy_host': parsed.host, - 'proxy_port': parsed.port, - 'username': username, - 'password': password, - 'rdns': rdns + "socks_version": socks_version, + "proxy_host": parsed.host, + "proxy_port": parsed.port, + "username": username, + "password": password, + "rdns": rdns, } - connection_pool_kw['_socks_options'] = socks_options + connection_pool_kw["_socks_options"] = socks_options super(SOCKSProxyManager, self).__init__( num_pools, headers, **connection_pool_kw diff --git a/src/pip/_vendor/urllib3/exceptions.py b/src/pip/_vendor/urllib3/exceptions.py index 7bbaa9871f5..93d93fba7d1 100644 --- a/src/pip/_vendor/urllib3/exceptions.py +++ b/src/pip/_vendor/urllib3/exceptions.py @@ -1,7 +1,6 @@ from __future__ import absolute_import -from .packages.six.moves.http_client import ( - IncompleteRead as httplib_IncompleteRead -) +from .packages.six.moves.http_client import IncompleteRead as httplib_IncompleteRead + # Base Exceptions @@ -17,6 +16,7 @@ class HTTPWarning(Warning): class PoolError(HTTPError): "Base exception for errors caused within a pool." + def __init__(self, pool, message): self.pool = pool HTTPError.__init__(self, "%s: %s" % (pool, message)) @@ -28,6 +28,7 @@ def __reduce__(self): class RequestError(PoolError): "Base exception for PoolErrors that have associated URLs." + def __init__(self, pool, url, message): self.url = url PoolError.__init__(self, pool, message) @@ -63,6 +64,7 @@ class ProtocolError(HTTPError): # Leaf Exceptions + class MaxRetryError(RequestError): """Raised when the maximum number of retries is exceeded. @@ -76,8 +78,7 @@ class MaxRetryError(RequestError): def __init__(self, pool, url, reason=None): self.reason = reason - message = "Max retries exceeded with url: %s (Caused by %r)" % ( - url, reason) + message = "Max retries exceeded with url: %s (Caused by %r)" % (url, reason) RequestError.__init__(self, pool, url, message) @@ -93,6 +94,7 @@ def __init__(self, pool, url, retries=3): class TimeoutStateError(HTTPError): """ Raised when passing an invalid state to a timeout """ + pass @@ -102,6 +104,7 @@ class TimeoutError(HTTPError): Catching this error will catch both :exc:`ReadTimeoutErrors <ReadTimeoutError>` and :exc:`ConnectTimeoutErrors <ConnectTimeoutError>`. """ + pass @@ -149,8 +152,8 @@ def __init__(self, location): class ResponseError(HTTPError): "Used as a container for an error reason supplied in a MaxRetryError." - GENERIC_ERROR = 'too many error responses' - SPECIFIC_ERROR = 'too many {status_code} error responses' + GENERIC_ERROR = "too many error responses" + SPECIFIC_ERROR = "too many {status_code} error responses" class SecurityWarning(HTTPWarning): @@ -188,6 +191,7 @@ class DependencyWarning(HTTPWarning): Warned when an attempt is made to import a module with missing optional dependencies. """ + pass @@ -201,6 +205,7 @@ class BodyNotHttplibCompatible(HTTPError): Body should be httplib.HTTPResponse like (have an fp attribute which returns raw chunks) for read_chunked(). """ + pass @@ -212,12 +217,15 @@ class IncompleteRead(HTTPError, httplib_IncompleteRead): for `partial` to avoid creating large objects on streamed reads. """ + def __init__(self, partial, expected): super(IncompleteRead, self).__init__(partial, expected) def __repr__(self): - return ('IncompleteRead(%i bytes read, ' - '%i more expected)' % (self.partial, self.expected)) + return "IncompleteRead(%i bytes read, " "%i more expected)" % ( + self.partial, + self.expected, + ) class InvalidHeader(HTTPError): @@ -236,8 +244,9 @@ def __init__(self, scheme): class HeaderParsingError(HTTPError): "Raised by assert_header_parsing, but we convert it to a log.warning statement." + def __init__(self, defects, unparsed_data): - message = '%s, unparsed data: %r' % (defects or 'Unknown', unparsed_data) + message = "%s, unparsed data: %r" % (defects or "Unknown", unparsed_data) super(HeaderParsingError, self).__init__(message) diff --git a/src/pip/_vendor/urllib3/fields.py b/src/pip/_vendor/urllib3/fields.py index 6a9a5a7f562..8715b2202b0 100644 --- a/src/pip/_vendor/urllib3/fields.py +++ b/src/pip/_vendor/urllib3/fields.py @@ -6,7 +6,7 @@ from .packages import six -def guess_content_type(filename, default='application/octet-stream'): +def guess_content_type(filename, default="application/octet-stream"): """ Guess the "Content-Type" of a file. @@ -41,22 +41,22 @@ def format_header_param_rfc2231(name, value): if not any(ch in value for ch in '"\\\r\n'): result = u'%s="%s"' % (name, value) try: - result.encode('ascii') + result.encode("ascii") except (UnicodeEncodeError, UnicodeDecodeError): pass else: return result - if not six.PY3: # Python 2: - value = value.encode('utf-8') + if six.PY2: # Python 2: + value = value.encode("utf-8") # encode_rfc2231 accepts an encoded string and returns an ascii-encoded # string in Python 2 but accepts and returns unicode strings in Python 3 - value = email.utils.encode_rfc2231(value, 'utf-8') - value = '%s*=%s' % (name, value) + value = email.utils.encode_rfc2231(value, "utf-8") + value = "%s*=%s" % (name, value) - if not six.PY3: # Python 2: - value = value.decode('utf-8') + if six.PY2: # Python 2: + value = value.decode("utf-8") return value @@ -69,23 +69,21 @@ def format_header_param_rfc2231(name, value): } # All control characters from 0x00 to 0x1F *except* 0x1B. -_HTML5_REPLACEMENTS.update({ - six.unichr(cc): u"%{:02X}".format(cc) - for cc - in range(0x00, 0x1F+1) - if cc not in (0x1B,) -}) +_HTML5_REPLACEMENTS.update( + { + six.unichr(cc): u"%{:02X}".format(cc) + for cc in range(0x00, 0x1F + 1) + if cc not in (0x1B,) + } +) def _replace_multiple(value, needles_and_replacements): - def replacer(match): return needles_and_replacements[match.group(0)] pattern = re.compile( - r"|".join([ - re.escape(needle) for needle in needles_and_replacements.keys() - ]) + r"|".join([re.escape(needle) for needle in needles_and_replacements.keys()]) ) result = pattern.sub(replacer, value) @@ -140,13 +138,15 @@ class RequestField(object): An optional callable that is used to encode and format the headers. By default, this is :func:`format_header_param_html5`. """ + def __init__( - self, - name, - data, - filename=None, - headers=None, - header_formatter=format_header_param_html5): + self, + name, + data, + filename=None, + headers=None, + header_formatter=format_header_param_html5, + ): self._name = name self._filename = filename self.data = data @@ -156,11 +156,7 @@ def __init__( self.header_formatter = header_formatter @classmethod - def from_tuples( - cls, - fieldname, - value, - header_formatter=format_header_param_html5): + def from_tuples(cls, fieldname, value, header_formatter=format_header_param_html5): """ A :class:`~urllib3.fields.RequestField` factory from old-style tuple parameters. @@ -189,7 +185,8 @@ def from_tuples( data = value request_param = cls( - fieldname, data, filename=filename, header_formatter=header_formatter) + fieldname, data, filename=filename, header_formatter=header_formatter + ) request_param.make_multipart(content_type=content_type) return request_param @@ -227,7 +224,7 @@ def _render_parts(self, header_parts): if value is not None: parts.append(self._render_part(name, value)) - return u'; '.join(parts) + return u"; ".join(parts) def render_headers(self): """ @@ -235,21 +232,22 @@ def render_headers(self): """ lines = [] - sort_keys = ['Content-Disposition', 'Content-Type', 'Content-Location'] + sort_keys = ["Content-Disposition", "Content-Type", "Content-Location"] for sort_key in sort_keys: if self.headers.get(sort_key, False): - lines.append(u'%s: %s' % (sort_key, self.headers[sort_key])) + lines.append(u"%s: %s" % (sort_key, self.headers[sort_key])) for header_name, header_value in self.headers.items(): if header_name not in sort_keys: if header_value: - lines.append(u'%s: %s' % (header_name, header_value)) + lines.append(u"%s: %s" % (header_name, header_value)) - lines.append(u'\r\n') - return u'\r\n'.join(lines) + lines.append(u"\r\n") + return u"\r\n".join(lines) - def make_multipart(self, content_disposition=None, content_type=None, - content_location=None): + def make_multipart( + self, content_disposition=None, content_type=None, content_location=None + ): """ Makes this request field into a multipart request field. @@ -262,11 +260,14 @@ def make_multipart(self, content_disposition=None, content_type=None, The 'Content-Location' of the request body. """ - self.headers['Content-Disposition'] = content_disposition or u'form-data' - self.headers['Content-Disposition'] += u'; '.join([ - u'', self._render_parts( - ((u'name', self._name), (u'filename', self._filename)) - ) - ]) - self.headers['Content-Type'] = content_type - self.headers['Content-Location'] = content_location + self.headers["Content-Disposition"] = content_disposition or u"form-data" + self.headers["Content-Disposition"] += u"; ".join( + [ + u"", + self._render_parts( + ((u"name", self._name), (u"filename", self._filename)) + ), + ] + ) + self.headers["Content-Type"] = content_type + self.headers["Content-Location"] = content_location diff --git a/src/pip/_vendor/urllib3/filepost.py b/src/pip/_vendor/urllib3/filepost.py index 78f1e19b0ed..b7b00992c65 100644 --- a/src/pip/_vendor/urllib3/filepost.py +++ b/src/pip/_vendor/urllib3/filepost.py @@ -9,7 +9,7 @@ from .packages.six import b from .fields import RequestField -writer = codecs.lookup('utf-8')[3] +writer = codecs.lookup("utf-8")[3] def choose_boundary(): @@ -17,8 +17,8 @@ def choose_boundary(): Our embarrassingly-simple replacement for mimetools.choose_boundary. """ boundary = binascii.hexlify(os.urandom(16)) - if six.PY3: - boundary = boundary.decode('ascii') + if not six.PY2: + boundary = boundary.decode("ascii") return boundary @@ -76,7 +76,7 @@ def encode_multipart_formdata(fields, boundary=None): boundary = choose_boundary() for field in iter_field_objects(fields): - body.write(b('--%s\r\n' % (boundary))) + body.write(b("--%s\r\n" % (boundary))) writer(body).write(field.render_headers()) data = field.data @@ -89,10 +89,10 @@ def encode_multipart_formdata(fields, boundary=None): else: body.write(data) - body.write(b'\r\n') + body.write(b"\r\n") - body.write(b('--%s--\r\n' % (boundary))) + body.write(b("--%s--\r\n" % (boundary))) - content_type = str('multipart/form-data; boundary=%s' % boundary) + content_type = str("multipart/form-data; boundary=%s" % boundary) return body.getvalue(), content_type diff --git a/src/pip/_vendor/urllib3/packages/__init__.py b/src/pip/_vendor/urllib3/packages/__init__.py index 170e974c157..fce4caa65d2 100644 --- a/src/pip/_vendor/urllib3/packages/__init__.py +++ b/src/pip/_vendor/urllib3/packages/__init__.py @@ -2,4 +2,4 @@ from . import ssl_match_hostname -__all__ = ('ssl_match_hostname', ) +__all__ = ("ssl_match_hostname",) diff --git a/src/pip/_vendor/urllib3/packages/backports/makefile.py b/src/pip/_vendor/urllib3/packages/backports/makefile.py index 740db377d99..a3156a69c08 100644 --- a/src/pip/_vendor/urllib3/packages/backports/makefile.py +++ b/src/pip/_vendor/urllib3/packages/backports/makefile.py @@ -11,15 +11,14 @@ from socket import SocketIO -def backport_makefile(self, mode="r", buffering=None, encoding=None, - errors=None, newline=None): +def backport_makefile( + self, mode="r", buffering=None, encoding=None, errors=None, newline=None +): """ Backport of ``socket.makefile`` from Python 3.5. """ if not set(mode) <= {"r", "w", "b"}: - raise ValueError( - "invalid mode %r (only r, w, b allowed)" % (mode,) - ) + raise ValueError("invalid mode %r (only r, w, b allowed)" % (mode,)) writing = "w" in mode reading = "r" in mode or not writing assert reading or writing diff --git a/src/pip/_vendor/urllib3/packages/rfc3986/__init__.py b/src/pip/_vendor/urllib3/packages/rfc3986/__init__.py deleted file mode 100644 index 371c6dd5193..00000000000 --- a/src/pip/_vendor/urllib3/packages/rfc3986/__init__.py +++ /dev/null @@ -1,56 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2014 Rackspace -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. -# See the License for the specific language governing permissions and -# limitations under the License. - -""" -An implementation of semantics and validations described in RFC 3986. - -See http://rfc3986.readthedocs.io/ for detailed documentation. - -:copyright: (c) 2014 Rackspace -:license: Apache v2.0, see LICENSE for details -""" - -from .api import iri_reference -from .api import IRIReference -from .api import is_valid_uri -from .api import normalize_uri -from .api import uri_reference -from .api import URIReference -from .api import urlparse -from .parseresult import ParseResult - -__title__ = 'rfc3986' -__author__ = 'Ian Stapleton Cordasco' -__author_email__ = 'graffatcolmingov@gmail.com' -__license__ = 'Apache v2.0' -__copyright__ = 'Copyright 2014 Rackspace' -__version__ = '1.3.2' - -__all__ = ( - 'ParseResult', - 'URIReference', - 'IRIReference', - 'is_valid_uri', - 'normalize_uri', - 'uri_reference', - 'iri_reference', - 'urlparse', - '__title__', - '__author__', - '__author_email__', - '__license__', - '__copyright__', - '__version__', -) diff --git a/src/pip/_vendor/urllib3/packages/rfc3986/_mixin.py b/src/pip/_vendor/urllib3/packages/rfc3986/_mixin.py deleted file mode 100644 index 543925cdbc4..00000000000 --- a/src/pip/_vendor/urllib3/packages/rfc3986/_mixin.py +++ /dev/null @@ -1,353 +0,0 @@ -"""Module containing the implementation of the URIMixin class.""" -import warnings - -from . import exceptions as exc -from . import misc -from . import normalizers -from . import validators - - -class URIMixin(object): - """Mixin with all shared methods for URIs and IRIs.""" - - __hash__ = tuple.__hash__ - - def authority_info(self): - """Return a dictionary with the ``userinfo``, ``host``, and ``port``. - - If the authority is not valid, it will raise a - :class:`~rfc3986.exceptions.InvalidAuthority` Exception. - - :returns: - ``{'userinfo': 'username:password', 'host': 'www.example.com', - 'port': '80'}`` - :rtype: dict - :raises rfc3986.exceptions.InvalidAuthority: - If the authority is not ``None`` and can not be parsed. - """ - if not self.authority: - return {'userinfo': None, 'host': None, 'port': None} - - match = self._match_subauthority() - - if match is None: - # In this case, we have an authority that was parsed from the URI - # Reference, but it cannot be further parsed by our - # misc.SUBAUTHORITY_MATCHER. In this case it must not be a valid - # authority. - raise exc.InvalidAuthority(self.authority.encode(self.encoding)) - - # We had a match, now let's ensure that it is actually a valid host - # address if it is IPv4 - matches = match.groupdict() - host = matches.get('host') - - if (host and misc.IPv4_MATCHER.match(host) and not - validators.valid_ipv4_host_address(host)): - # If we have a host, it appears to be IPv4 and it does not have - # valid bytes, it is an InvalidAuthority. - raise exc.InvalidAuthority(self.authority.encode(self.encoding)) - - return matches - - def _match_subauthority(self): - return misc.SUBAUTHORITY_MATCHER.match(self.authority) - - @property - def host(self): - """If present, a string representing the host.""" - try: - authority = self.authority_info() - except exc.InvalidAuthority: - return None - return authority['host'] - - @property - def port(self): - """If present, the port extracted from the authority.""" - try: - authority = self.authority_info() - except exc.InvalidAuthority: - return None - return authority['port'] - - @property - def userinfo(self): - """If present, the userinfo extracted from the authority.""" - try: - authority = self.authority_info() - except exc.InvalidAuthority: - return None - return authority['userinfo'] - - def is_absolute(self): - """Determine if this URI Reference is an absolute URI. - - See http://tools.ietf.org/html/rfc3986#section-4.3 for explanation. - - :returns: ``True`` if it is an absolute URI, ``False`` otherwise. - :rtype: bool - """ - return bool(misc.ABSOLUTE_URI_MATCHER.match(self.unsplit())) - - def is_valid(self, **kwargs): - """Determine if the URI is valid. - - .. deprecated:: 1.1.0 - - Use the :class:`~rfc3986.validators.Validator` object instead. - - :param bool require_scheme: Set to ``True`` if you wish to require the - presence of the scheme component. - :param bool require_authority: Set to ``True`` if you wish to require - the presence of the authority component. - :param bool require_path: Set to ``True`` if you wish to require the - presence of the path component. - :param bool require_query: Set to ``True`` if you wish to require the - presence of the query component. - :param bool require_fragment: Set to ``True`` if you wish to require - the presence of the fragment component. - :returns: ``True`` if the URI is valid. ``False`` otherwise. - :rtype: bool - """ - warnings.warn("Please use rfc3986.validators.Validator instead. " - "This method will be eventually removed.", - DeprecationWarning) - validators = [ - (self.scheme_is_valid, kwargs.get('require_scheme', False)), - (self.authority_is_valid, kwargs.get('require_authority', False)), - (self.path_is_valid, kwargs.get('require_path', False)), - (self.query_is_valid, kwargs.get('require_query', False)), - (self.fragment_is_valid, kwargs.get('require_fragment', False)), - ] - return all(v(r) for v, r in validators) - - def authority_is_valid(self, require=False): - """Determine if the authority component is valid. - - .. deprecated:: 1.1.0 - - Use the :class:`~rfc3986.validators.Validator` object instead. - - :param bool require: - Set to ``True`` to require the presence of this component. - :returns: - ``True`` if the authority is valid. ``False`` otherwise. - :rtype: - bool - """ - warnings.warn("Please use rfc3986.validators.Validator instead. " - "This method will be eventually removed.", - DeprecationWarning) - try: - self.authority_info() - except exc.InvalidAuthority: - return False - - return validators.authority_is_valid( - self.authority, - host=self.host, - require=require, - ) - - def scheme_is_valid(self, require=False): - """Determine if the scheme component is valid. - - .. deprecated:: 1.1.0 - - Use the :class:`~rfc3986.validators.Validator` object instead. - - :param str require: Set to ``True`` to require the presence of this - component. - :returns: ``True`` if the scheme is valid. ``False`` otherwise. - :rtype: bool - """ - warnings.warn("Please use rfc3986.validators.Validator instead. " - "This method will be eventually removed.", - DeprecationWarning) - return validators.scheme_is_valid(self.scheme, require) - - def path_is_valid(self, require=False): - """Determine if the path component is valid. - - .. deprecated:: 1.1.0 - - Use the :class:`~rfc3986.validators.Validator` object instead. - - :param str require: Set to ``True`` to require the presence of this - component. - :returns: ``True`` if the path is valid. ``False`` otherwise. - :rtype: bool - """ - warnings.warn("Please use rfc3986.validators.Validator instead. " - "This method will be eventually removed.", - DeprecationWarning) - return validators.path_is_valid(self.path, require) - - def query_is_valid(self, require=False): - """Determine if the query component is valid. - - .. deprecated:: 1.1.0 - - Use the :class:`~rfc3986.validators.Validator` object instead. - - :param str require: Set to ``True`` to require the presence of this - component. - :returns: ``True`` if the query is valid. ``False`` otherwise. - :rtype: bool - """ - warnings.warn("Please use rfc3986.validators.Validator instead. " - "This method will be eventually removed.", - DeprecationWarning) - return validators.query_is_valid(self.query, require) - - def fragment_is_valid(self, require=False): - """Determine if the fragment component is valid. - - .. deprecated:: 1.1.0 - - Use the Validator object instead. - - :param str require: Set to ``True`` to require the presence of this - component. - :returns: ``True`` if the fragment is valid. ``False`` otherwise. - :rtype: bool - """ - warnings.warn("Please use rfc3986.validators.Validator instead. " - "This method will be eventually removed.", - DeprecationWarning) - return validators.fragment_is_valid(self.fragment, require) - - def normalized_equality(self, other_ref): - """Compare this URIReference to another URIReference. - - :param URIReference other_ref: (required), The reference with which - we're comparing. - :returns: ``True`` if the references are equal, ``False`` otherwise. - :rtype: bool - """ - return tuple(self.normalize()) == tuple(other_ref.normalize()) - - def resolve_with(self, base_uri, strict=False): - """Use an absolute URI Reference to resolve this relative reference. - - Assuming this is a relative reference that you would like to resolve, - use the provided base URI to resolve it. - - See http://tools.ietf.org/html/rfc3986#section-5 for more information. - - :param base_uri: Either a string or URIReference. It must be an - absolute URI or it will raise an exception. - :returns: A new URIReference which is the result of resolving this - reference using ``base_uri``. - :rtype: :class:`URIReference` - :raises rfc3986.exceptions.ResolutionError: - If the ``base_uri`` is not an absolute URI. - """ - if not isinstance(base_uri, URIMixin): - base_uri = type(self).from_string(base_uri) - - if not base_uri.is_absolute(): - raise exc.ResolutionError(base_uri) - - # This is optional per - # http://tools.ietf.org/html/rfc3986#section-5.2.1 - base_uri = base_uri.normalize() - - # The reference we're resolving - resolving = self - - if not strict and resolving.scheme == base_uri.scheme: - resolving = resolving.copy_with(scheme=None) - - # http://tools.ietf.org/html/rfc3986#page-32 - if resolving.scheme is not None: - target = resolving.copy_with( - path=normalizers.normalize_path(resolving.path) - ) - else: - if resolving.authority is not None: - target = resolving.copy_with( - scheme=base_uri.scheme, - path=normalizers.normalize_path(resolving.path) - ) - else: - if resolving.path is None: - if resolving.query is not None: - query = resolving.query - else: - query = base_uri.query - target = resolving.copy_with( - scheme=base_uri.scheme, - authority=base_uri.authority, - path=base_uri.path, - query=query - ) - else: - if resolving.path.startswith('/'): - path = normalizers.normalize_path(resolving.path) - else: - path = normalizers.normalize_path( - misc.merge_paths(base_uri, resolving.path) - ) - target = resolving.copy_with( - scheme=base_uri.scheme, - authority=base_uri.authority, - path=path, - query=resolving.query - ) - return target - - def unsplit(self): - """Create a URI string from the components. - - :returns: The URI Reference reconstituted as a string. - :rtype: str - """ - # See http://tools.ietf.org/html/rfc3986#section-5.3 - result_list = [] - if self.scheme: - result_list.extend([self.scheme, ':']) - if self.authority: - result_list.extend(['//', self.authority]) - if self.path: - result_list.append(self.path) - if self.query is not None: - result_list.extend(['?', self.query]) - if self.fragment is not None: - result_list.extend(['#', self.fragment]) - return ''.join(result_list) - - def copy_with(self, scheme=misc.UseExisting, authority=misc.UseExisting, - path=misc.UseExisting, query=misc.UseExisting, - fragment=misc.UseExisting): - """Create a copy of this reference with the new components. - - :param str scheme: - (optional) The scheme to use for the new reference. - :param str authority: - (optional) The authority to use for the new reference. - :param str path: - (optional) The path to use for the new reference. - :param str query: - (optional) The query to use for the new reference. - :param str fragment: - (optional) The fragment to use for the new reference. - :returns: - New URIReference with provided components. - :rtype: - URIReference - """ - attributes = { - 'scheme': scheme, - 'authority': authority, - 'path': path, - 'query': query, - 'fragment': fragment, - } - for key, value in list(attributes.items()): - if value is misc.UseExisting: - del attributes[key] - uri = self._replace(**attributes) - uri.encoding = self.encoding - return uri diff --git a/src/pip/_vendor/urllib3/packages/rfc3986/abnf_regexp.py b/src/pip/_vendor/urllib3/packages/rfc3986/abnf_regexp.py deleted file mode 100644 index 24c9c3d00ad..00000000000 --- a/src/pip/_vendor/urllib3/packages/rfc3986/abnf_regexp.py +++ /dev/null @@ -1,267 +0,0 @@ -# -*- coding: utf-8 -*- -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Module for the regular expressions crafted from ABNF.""" - -import sys - -# https://tools.ietf.org/html/rfc3986#page-13 -GEN_DELIMS = GENERIC_DELIMITERS = ":/?#[]@" -GENERIC_DELIMITERS_SET = set(GENERIC_DELIMITERS) -# https://tools.ietf.org/html/rfc3986#page-13 -SUB_DELIMS = SUB_DELIMITERS = "!$&'()*+,;=" -SUB_DELIMITERS_SET = set(SUB_DELIMITERS) -# Escape the '*' for use in regular expressions -SUB_DELIMITERS_RE = r"!$&'()\*+,;=" -RESERVED_CHARS_SET = GENERIC_DELIMITERS_SET.union(SUB_DELIMITERS_SET) -ALPHA = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz' -DIGIT = '0123456789' -# https://tools.ietf.org/html/rfc3986#section-2.3 -UNRESERVED = UNRESERVED_CHARS = ALPHA + DIGIT + r'._!-' -UNRESERVED_CHARS_SET = set(UNRESERVED_CHARS) -NON_PCT_ENCODED_SET = RESERVED_CHARS_SET.union(UNRESERVED_CHARS_SET) -# We need to escape the '-' in this case: -UNRESERVED_RE = r'A-Za-z0-9._~\-' - -# Percent encoded character values -PERCENT_ENCODED = PCT_ENCODED = '%[A-Fa-f0-9]{2}' -PCHAR = '([' + UNRESERVED_RE + SUB_DELIMITERS_RE + ':@]|%s)' % PCT_ENCODED - -# NOTE(sigmavirus24): We're going to use more strict regular expressions -# than appear in Appendix B for scheme. This will prevent over-eager -# consuming of items that aren't schemes. -SCHEME_RE = '[a-zA-Z][a-zA-Z0-9+.-]*' -_AUTHORITY_RE = '[^/?#]*' -_PATH_RE = '[^?#]*' -_QUERY_RE = '[^#]*' -_FRAGMENT_RE = '.*' - -# Extracted from http://tools.ietf.org/html/rfc3986#appendix-B -COMPONENT_PATTERN_DICT = { - 'scheme': SCHEME_RE, - 'authority': _AUTHORITY_RE, - 'path': _PATH_RE, - 'query': _QUERY_RE, - 'fragment': _FRAGMENT_RE, -} - -# See http://tools.ietf.org/html/rfc3986#appendix-B -# In this case, we name each of the important matches so we can use -# SRE_Match#groupdict to parse the values out if we so choose. This is also -# modified to ignore other matches that are not important to the parsing of -# the reference so we can also simply use SRE_Match#groups. -URL_PARSING_RE = ( - r'(?:(?P<scheme>{scheme}):)?(?://(?P<authority>{authority}))?' - r'(?P<path>{path})(?:\?(?P<query>{query}))?' - r'(?:#(?P<fragment>{fragment}))?' -).format(**COMPONENT_PATTERN_DICT) - - -# ######################### -# Authority Matcher Section -# ######################### - -# Host patterns, see: http://tools.ietf.org/html/rfc3986#section-3.2.2 -# The pattern for a regular name, e.g., www.google.com, api.github.com -REGULAR_NAME_RE = REG_NAME = '((?:{0}|[{1}])*)'.format( - '%[0-9A-Fa-f]{2}', SUB_DELIMITERS_RE + UNRESERVED_RE -) -# The pattern for an IPv4 address, e.g., 192.168.255.255, 127.0.0.1, -IPv4_RE = r'([0-9]{1,3}\.){3}[0-9]{1,3}' -# Hexadecimal characters used in each piece of an IPv6 address -HEXDIG_RE = '[0-9A-Fa-f]{1,4}' -# Least-significant 32 bits of an IPv6 address -LS32_RE = '({hex}:{hex}|{ipv4})'.format(hex=HEXDIG_RE, ipv4=IPv4_RE) -# Substitutions into the following patterns for IPv6 patterns defined -# http://tools.ietf.org/html/rfc3986#page-20 -_subs = {'hex': HEXDIG_RE, 'ls32': LS32_RE} - -# Below: h16 = hexdig, see: https://tools.ietf.org/html/rfc5234 for details -# about ABNF (Augmented Backus-Naur Form) use in the comments -variations = [ - # 6( h16 ":" ) ls32 - '(%(hex)s:){6}%(ls32)s' % _subs, - # "::" 5( h16 ":" ) ls32 - '::(%(hex)s:){5}%(ls32)s' % _subs, - # [ h16 ] "::" 4( h16 ":" ) ls32 - '(%(hex)s)?::(%(hex)s:){4}%(ls32)s' % _subs, - # [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32 - '((%(hex)s:)?%(hex)s)?::(%(hex)s:){3}%(ls32)s' % _subs, - # [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32 - '((%(hex)s:){0,2}%(hex)s)?::(%(hex)s:){2}%(ls32)s' % _subs, - # [ *3( h16 ":" ) h16 ] "::" h16 ":" ls32 - '((%(hex)s:){0,3}%(hex)s)?::%(hex)s:%(ls32)s' % _subs, - # [ *4( h16 ":" ) h16 ] "::" ls32 - '((%(hex)s:){0,4}%(hex)s)?::%(ls32)s' % _subs, - # [ *5( h16 ":" ) h16 ] "::" h16 - '((%(hex)s:){0,5}%(hex)s)?::%(hex)s' % _subs, - # [ *6( h16 ":" ) h16 ] "::" - '((%(hex)s:){0,6}%(hex)s)?::' % _subs, -] - -IPv6_RE = '(({0})|({1})|({2})|({3})|({4})|({5})|({6})|({7})|({8}))'.format( - *variations -) - -IPv_FUTURE_RE = r'v[0-9A-Fa-f]+\.[%s]+' % ( - UNRESERVED_RE + SUB_DELIMITERS_RE + ':' -) - -# RFC 6874 Zone ID ABNF -ZONE_ID = '(?:[' + UNRESERVED_RE + ']|' + PCT_ENCODED + ')+' - -IPv6_ADDRZ_RFC4007_RE = IPv6_RE + '(?:(?:%25|%)' + ZONE_ID + ')?' -IPv6_ADDRZ_RE = IPv6_RE + '(?:%25' + ZONE_ID + ')?' - -IP_LITERAL_RE = r'\[({0}|{1})\]'.format( - IPv6_ADDRZ_RFC4007_RE, - IPv_FUTURE_RE, -) - -# Pattern for matching the host piece of the authority -HOST_RE = HOST_PATTERN = '({0}|{1}|{2})'.format( - REG_NAME, - IPv4_RE, - IP_LITERAL_RE, -) -USERINFO_RE = '^([' + UNRESERVED_RE + SUB_DELIMITERS_RE + ':]|%s)+' % ( - PCT_ENCODED -) -PORT_RE = '[0-9]{1,5}' - -# #################### -# Path Matcher Section -# #################### - -# See http://tools.ietf.org/html/rfc3986#section-3.3 for more information -# about the path patterns defined below. -segments = { - 'segment': PCHAR + '*', - # Non-zero length segment - 'segment-nz': PCHAR + '+', - # Non-zero length segment without ":" - 'segment-nz-nc': PCHAR.replace(':', '') + '+' -} - -# Path types taken from Section 3.3 (linked above) -PATH_EMPTY = '^$' -PATH_ROOTLESS = '%(segment-nz)s(/%(segment)s)*' % segments -PATH_NOSCHEME = '%(segment-nz-nc)s(/%(segment)s)*' % segments -PATH_ABSOLUTE = '/(%s)?' % PATH_ROOTLESS -PATH_ABEMPTY = '(/%(segment)s)*' % segments -PATH_RE = '^(%s|%s|%s|%s|%s)$' % ( - PATH_ABEMPTY, PATH_ABSOLUTE, PATH_NOSCHEME, PATH_ROOTLESS, PATH_EMPTY -) - -FRAGMENT_RE = QUERY_RE = ( - '^([/?:@' + UNRESERVED_RE + SUB_DELIMITERS_RE + ']|%s)*$' % PCT_ENCODED -) - -# ########################## -# Relative reference matcher -# ########################## - -# See http://tools.ietf.org/html/rfc3986#section-4.2 for details -RELATIVE_PART_RE = '(//%s%s|%s|%s|%s)' % ( - COMPONENT_PATTERN_DICT['authority'], - PATH_ABEMPTY, - PATH_ABSOLUTE, - PATH_NOSCHEME, - PATH_EMPTY, -) - -# See http://tools.ietf.org/html/rfc3986#section-3 for definition -HIER_PART_RE = '(//%s%s|%s|%s|%s)' % ( - COMPONENT_PATTERN_DICT['authority'], - PATH_ABEMPTY, - PATH_ABSOLUTE, - PATH_ROOTLESS, - PATH_EMPTY, -) - -# ############### -# IRIs / RFC 3987 -# ############### - -# Only wide-unicode gets the high-ranges of UCSCHAR -if sys.maxunicode > 0xFFFF: # pragma: no cover - IPRIVATE = u'\uE000-\uF8FF\U000F0000-\U000FFFFD\U00100000-\U0010FFFD' - UCSCHAR_RE = ( - u'\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF' - u'\U00010000-\U0001FFFD\U00020000-\U0002FFFD' - u'\U00030000-\U0003FFFD\U00040000-\U0004FFFD' - u'\U00050000-\U0005FFFD\U00060000-\U0006FFFD' - u'\U00070000-\U0007FFFD\U00080000-\U0008FFFD' - u'\U00090000-\U0009FFFD\U000A0000-\U000AFFFD' - u'\U000B0000-\U000BFFFD\U000C0000-\U000CFFFD' - u'\U000D0000-\U000DFFFD\U000E1000-\U000EFFFD' - ) -else: # pragma: no cover - IPRIVATE = u'\uE000-\uF8FF' - UCSCHAR_RE = ( - u'\u00A0-\uD7FF\uF900-\uFDCF\uFDF0-\uFFEF' - ) - -IUNRESERVED_RE = u'A-Za-z0-9\\._~\\-' + UCSCHAR_RE -IPCHAR = u'([' + IUNRESERVED_RE + SUB_DELIMITERS_RE + u':@]|%s)' % PCT_ENCODED - -isegments = { - 'isegment': IPCHAR + u'*', - # Non-zero length segment - 'isegment-nz': IPCHAR + u'+', - # Non-zero length segment without ":" - 'isegment-nz-nc': IPCHAR.replace(':', '') + u'+' -} - -IPATH_ROOTLESS = u'%(isegment-nz)s(/%(isegment)s)*' % isegments -IPATH_NOSCHEME = u'%(isegment-nz-nc)s(/%(isegment)s)*' % isegments -IPATH_ABSOLUTE = u'/(?:%s)?' % IPATH_ROOTLESS -IPATH_ABEMPTY = u'(?:/%(isegment)s)*' % isegments -IPATH_RE = u'^(?:%s|%s|%s|%s|%s)$' % ( - IPATH_ABEMPTY, IPATH_ABSOLUTE, IPATH_NOSCHEME, IPATH_ROOTLESS, PATH_EMPTY -) - -IREGULAR_NAME_RE = IREG_NAME = u'(?:{0}|[{1}])*'.format( - u'%[0-9A-Fa-f]{2}', SUB_DELIMITERS_RE + IUNRESERVED_RE -) - -IHOST_RE = IHOST_PATTERN = u'({0}|{1}|{2})'.format( - IREG_NAME, - IPv4_RE, - IP_LITERAL_RE, -) - -IUSERINFO_RE = u'^(?:[' + IUNRESERVED_RE + SUB_DELIMITERS_RE + u':]|%s)+' % ( - PCT_ENCODED -) - -IFRAGMENT_RE = (u'^(?:[/?:@' + IUNRESERVED_RE + SUB_DELIMITERS_RE - + u']|%s)*$' % PCT_ENCODED) -IQUERY_RE = (u'^(?:[/?:@' + IUNRESERVED_RE + SUB_DELIMITERS_RE - + IPRIVATE + u']|%s)*$' % PCT_ENCODED) - -IRELATIVE_PART_RE = u'(//%s%s|%s|%s|%s)' % ( - COMPONENT_PATTERN_DICT['authority'], - IPATH_ABEMPTY, - IPATH_ABSOLUTE, - IPATH_NOSCHEME, - PATH_EMPTY, -) - -IHIER_PART_RE = u'(//%s%s|%s|%s|%s)' % ( - COMPONENT_PATTERN_DICT['authority'], - IPATH_ABEMPTY, - IPATH_ABSOLUTE, - IPATH_ROOTLESS, - PATH_EMPTY, -) diff --git a/src/pip/_vendor/urllib3/packages/rfc3986/api.py b/src/pip/_vendor/urllib3/packages/rfc3986/api.py deleted file mode 100644 index ddc4a1cd283..00000000000 --- a/src/pip/_vendor/urllib3/packages/rfc3986/api.py +++ /dev/null @@ -1,106 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2014 Rackspace -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -Module containing the simple and functional API for rfc3986. - -This module defines functions and provides access to the public attributes -and classes of rfc3986. -""" - -from .iri import IRIReference -from .parseresult import ParseResult -from .uri import URIReference - - -def uri_reference(uri, encoding='utf-8'): - """Parse a URI string into a URIReference. - - This is a convenience function. You could achieve the same end by using - ``URIReference.from_string(uri)``. - - :param str uri: The URI which needs to be parsed into a reference. - :param str encoding: The encoding of the string provided - :returns: A parsed URI - :rtype: :class:`URIReference` - """ - return URIReference.from_string(uri, encoding) - - -def iri_reference(iri, encoding='utf-8'): - """Parse a IRI string into an IRIReference. - - This is a convenience function. You could achieve the same end by using - ``IRIReference.from_string(iri)``. - - :param str iri: The IRI which needs to be parsed into a reference. - :param str encoding: The encoding of the string provided - :returns: A parsed IRI - :rtype: :class:`IRIReference` - """ - return IRIReference.from_string(iri, encoding) - - -def is_valid_uri(uri, encoding='utf-8', **kwargs): - """Determine if the URI given is valid. - - This is a convenience function. You could use either - ``uri_reference(uri).is_valid()`` or - ``URIReference.from_string(uri).is_valid()`` to achieve the same result. - - :param str uri: The URI to be validated. - :param str encoding: The encoding of the string provided - :param bool require_scheme: Set to ``True`` if you wish to require the - presence of the scheme component. - :param bool require_authority: Set to ``True`` if you wish to require the - presence of the authority component. - :param bool require_path: Set to ``True`` if you wish to require the - presence of the path component. - :param bool require_query: Set to ``True`` if you wish to require the - presence of the query component. - :param bool require_fragment: Set to ``True`` if you wish to require the - presence of the fragment component. - :returns: ``True`` if the URI is valid, ``False`` otherwise. - :rtype: bool - """ - return URIReference.from_string(uri, encoding).is_valid(**kwargs) - - -def normalize_uri(uri, encoding='utf-8'): - """Normalize the given URI. - - This is a convenience function. You could use either - ``uri_reference(uri).normalize().unsplit()`` or - ``URIReference.from_string(uri).normalize().unsplit()`` instead. - - :param str uri: The URI to be normalized. - :param str encoding: The encoding of the string provided - :returns: The normalized URI. - :rtype: str - """ - normalized_reference = URIReference.from_string(uri, encoding).normalize() - return normalized_reference.unsplit() - - -def urlparse(uri, encoding='utf-8'): - """Parse a given URI and return a ParseResult. - - This is a partial replacement of the standard library's urlparse function. - - :param str uri: The URI to be parsed. - :param str encoding: The encoding of the string provided. - :returns: A parsed URI - :rtype: :class:`~rfc3986.parseresult.ParseResult` - """ - return ParseResult.from_string(uri, encoding, strict=False) diff --git a/src/pip/_vendor/urllib3/packages/rfc3986/builder.py b/src/pip/_vendor/urllib3/packages/rfc3986/builder.py deleted file mode 100644 index 7934279995c..00000000000 --- a/src/pip/_vendor/urllib3/packages/rfc3986/builder.py +++ /dev/null @@ -1,298 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2017 Ian Stapleton Cordasco -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Module containing the logic for the URIBuilder object.""" -from . import compat -from . import normalizers -from . import uri - - -class URIBuilder(object): - """Object to aid in building up a URI Reference from parts. - - .. note:: - - This object should be instantiated by the user, but it's recommended - that it is not provided with arguments. Instead, use the available - method to populate the fields. - - """ - - def __init__(self, scheme=None, userinfo=None, host=None, port=None, - path=None, query=None, fragment=None): - """Initialize our URI builder. - - :param str scheme: - (optional) - :param str userinfo: - (optional) - :param str host: - (optional) - :param int port: - (optional) - :param str path: - (optional) - :param str query: - (optional) - :param str fragment: - (optional) - """ - self.scheme = scheme - self.userinfo = userinfo - self.host = host - self.port = port - self.path = path - self.query = query - self.fragment = fragment - - def __repr__(self): - """Provide a convenient view of our builder object.""" - formatstr = ('URIBuilder(scheme={b.scheme}, userinfo={b.userinfo}, ' - 'host={b.host}, port={b.port}, path={b.path}, ' - 'query={b.query}, fragment={b.fragment})') - return formatstr.format(b=self) - - def add_scheme(self, scheme): - """Add a scheme to our builder object. - - After normalizing, this will generate a new URIBuilder instance with - the specified scheme and all other attributes the same. - - .. code-block:: python - - >>> URIBuilder().add_scheme('HTTPS') - URIBuilder(scheme='https', userinfo=None, host=None, port=None, - path=None, query=None, fragment=None) - - """ - scheme = normalizers.normalize_scheme(scheme) - return URIBuilder( - scheme=scheme, - userinfo=self.userinfo, - host=self.host, - port=self.port, - path=self.path, - query=self.query, - fragment=self.fragment, - ) - - def add_credentials(self, username, password): - """Add credentials as the userinfo portion of the URI. - - .. code-block:: python - - >>> URIBuilder().add_credentials('root', 's3crete') - URIBuilder(scheme=None, userinfo='root:s3crete', host=None, - port=None, path=None, query=None, fragment=None) - - >>> URIBuilder().add_credentials('root', None) - URIBuilder(scheme=None, userinfo='root', host=None, - port=None, path=None, query=None, fragment=None) - """ - if username is None: - raise ValueError('Username cannot be None') - userinfo = normalizers.normalize_username(username) - - if password is not None: - userinfo = '{}:{}'.format( - userinfo, - normalizers.normalize_password(password), - ) - - return URIBuilder( - scheme=self.scheme, - userinfo=userinfo, - host=self.host, - port=self.port, - path=self.path, - query=self.query, - fragment=self.fragment, - ) - - def add_host(self, host): - """Add hostname to the URI. - - .. code-block:: python - - >>> URIBuilder().add_host('google.com') - URIBuilder(scheme=None, userinfo=None, host='google.com', - port=None, path=None, query=None, fragment=None) - - """ - return URIBuilder( - scheme=self.scheme, - userinfo=self.userinfo, - host=normalizers.normalize_host(host), - port=self.port, - path=self.path, - query=self.query, - fragment=self.fragment, - ) - - def add_port(self, port): - """Add port to the URI. - - .. code-block:: python - - >>> URIBuilder().add_port(80) - URIBuilder(scheme=None, userinfo=None, host=None, port='80', - path=None, query=None, fragment=None) - - >>> URIBuilder().add_port(443) - URIBuilder(scheme=None, userinfo=None, host=None, port='443', - path=None, query=None, fragment=None) - - """ - port_int = int(port) - if port_int < 0: - raise ValueError( - 'ports are not allowed to be negative. You provided {}'.format( - port_int, - ) - ) - if port_int > 65535: - raise ValueError( - 'ports are not allowed to be larger than 65535. ' - 'You provided {}'.format( - port_int, - ) - ) - - return URIBuilder( - scheme=self.scheme, - userinfo=self.userinfo, - host=self.host, - port='{}'.format(port_int), - path=self.path, - query=self.query, - fragment=self.fragment, - ) - - def add_path(self, path): - """Add a path to the URI. - - .. code-block:: python - - >>> URIBuilder().add_path('sigmavirus24/rfc3985') - URIBuilder(scheme=None, userinfo=None, host=None, port=None, - path='/sigmavirus24/rfc3986', query=None, fragment=None) - - >>> URIBuilder().add_path('/checkout.php') - URIBuilder(scheme=None, userinfo=None, host=None, port=None, - path='/checkout.php', query=None, fragment=None) - - """ - if not path.startswith('/'): - path = '/{}'.format(path) - - return URIBuilder( - scheme=self.scheme, - userinfo=self.userinfo, - host=self.host, - port=self.port, - path=normalizers.normalize_path(path), - query=self.query, - fragment=self.fragment, - ) - - def add_query_from(self, query_items): - """Generate and add a query a dictionary or list of tuples. - - .. code-block:: python - - >>> URIBuilder().add_query_from({'a': 'b c'}) - URIBuilder(scheme=None, userinfo=None, host=None, port=None, - path=None, query='a=b+c', fragment=None) - - >>> URIBuilder().add_query_from([('a', 'b c')]) - URIBuilder(scheme=None, userinfo=None, host=None, port=None, - path=None, query='a=b+c', fragment=None) - - """ - query = normalizers.normalize_query(compat.urlencode(query_items)) - - return URIBuilder( - scheme=self.scheme, - userinfo=self.userinfo, - host=self.host, - port=self.port, - path=self.path, - query=query, - fragment=self.fragment, - ) - - def add_query(self, query): - """Add a pre-formated query string to the URI. - - .. code-block:: python - - >>> URIBuilder().add_query('a=b&c=d') - URIBuilder(scheme=None, userinfo=None, host=None, port=None, - path=None, query='a=b&c=d', fragment=None) - - """ - return URIBuilder( - scheme=self.scheme, - userinfo=self.userinfo, - host=self.host, - port=self.port, - path=self.path, - query=normalizers.normalize_query(query), - fragment=self.fragment, - ) - - def add_fragment(self, fragment): - """Add a fragment to the URI. - - .. code-block:: python - - >>> URIBuilder().add_fragment('section-2.6.1') - URIBuilder(scheme=None, userinfo=None, host=None, port=None, - path=None, query=None, fragment='section-2.6.1') - - """ - return URIBuilder( - scheme=self.scheme, - userinfo=self.userinfo, - host=self.host, - port=self.port, - path=self.path, - query=self.query, - fragment=normalizers.normalize_fragment(fragment), - ) - - def finalize(self): - """Create a URIReference from our builder. - - .. code-block:: python - - >>> URIBuilder().add_scheme('https').add_host('github.com' - ... ).add_path('sigmavirus24/rfc3986').finalize().unsplit() - 'https://github.com/sigmavirus24/rfc3986' - - >>> URIBuilder().add_scheme('https').add_host('github.com' - ... ).add_path('sigmavirus24/rfc3986').add_credentials( - ... 'sigmavirus24', 'not-re@l').finalize().unsplit() - 'https://sigmavirus24:not-re%40l@github.com/sigmavirus24/rfc3986' - - """ - return uri.URIReference( - self.scheme, - normalizers.normalize_authority( - (self.userinfo, self.host, self.port) - ), - self.path, - self.query, - self.fragment, - ) diff --git a/src/pip/_vendor/urllib3/packages/rfc3986/compat.py b/src/pip/_vendor/urllib3/packages/rfc3986/compat.py deleted file mode 100644 index 8968c384373..00000000000 --- a/src/pip/_vendor/urllib3/packages/rfc3986/compat.py +++ /dev/null @@ -1,54 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2014 Rackspace -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Compatibility module for Python 2 and 3 support.""" -import sys - -try: - from urllib.parse import quote as urlquote -except ImportError: # Python 2.x - from urllib import quote as urlquote - -try: - from urllib.parse import urlencode -except ImportError: # Python 2.x - from urllib import urlencode - -__all__ = ( - 'to_bytes', - 'to_str', - 'urlquote', - 'urlencode', -) - -PY3 = (3, 0) <= sys.version_info < (4, 0) -PY2 = (2, 6) <= sys.version_info < (2, 8) - - -if PY3: - unicode = str # Python 3.x - - -def to_str(b, encoding='utf-8'): - """Ensure that b is text in the specified encoding.""" - if hasattr(b, 'decode') and not isinstance(b, unicode): - b = b.decode(encoding) - return b - - -def to_bytes(s, encoding='utf-8'): - """Ensure that s is converted to bytes from the encoding.""" - if hasattr(s, 'encode') and not isinstance(s, bytes): - s = s.encode(encoding) - return s diff --git a/src/pip/_vendor/urllib3/packages/rfc3986/exceptions.py b/src/pip/_vendor/urllib3/packages/rfc3986/exceptions.py deleted file mode 100644 index da8ca7cb1f3..00000000000 --- a/src/pip/_vendor/urllib3/packages/rfc3986/exceptions.py +++ /dev/null @@ -1,118 +0,0 @@ -# -*- coding: utf-8 -*- -"""Exceptions module for rfc3986.""" - -from . import compat - - -class RFC3986Exception(Exception): - """Base class for all rfc3986 exception classes.""" - - pass - - -class InvalidAuthority(RFC3986Exception): - """Exception when the authority string is invalid.""" - - def __init__(self, authority): - """Initialize the exception with the invalid authority.""" - super(InvalidAuthority, self).__init__( - u"The authority ({0}) is not valid.".format( - compat.to_str(authority))) - - -class InvalidPort(RFC3986Exception): - """Exception when the port is invalid.""" - - def __init__(self, port): - """Initialize the exception with the invalid port.""" - super(InvalidPort, self).__init__( - 'The port ("{0}") is not valid.'.format(port)) - - -class ResolutionError(RFC3986Exception): - """Exception to indicate a failure to resolve a URI.""" - - def __init__(self, uri): - """Initialize the error with the failed URI.""" - super(ResolutionError, self).__init__( - "{0} is not an absolute URI.".format(uri.unsplit())) - - -class ValidationError(RFC3986Exception): - """Exception raised during Validation of a URI.""" - - pass - - -class MissingComponentError(ValidationError): - """Exception raised when a required component is missing.""" - - def __init__(self, uri, *component_names): - """Initialize the error with the missing component name.""" - verb = 'was' - if len(component_names) > 1: - verb = 'were' - - self.uri = uri - self.components = sorted(component_names) - components = ', '.join(self.components) - super(MissingComponentError, self).__init__( - "{} {} required but missing".format(components, verb), - uri, - self.components, - ) - - -class UnpermittedComponentError(ValidationError): - """Exception raised when a component has an unpermitted value.""" - - def __init__(self, component_name, component_value, allowed_values): - """Initialize the error with the unpermitted component.""" - super(UnpermittedComponentError, self).__init__( - "{} was required to be one of {!r} but was {!r}".format( - component_name, list(sorted(allowed_values)), component_value, - ), - component_name, - component_value, - allowed_values, - ) - self.component_name = component_name - self.component_value = component_value - self.allowed_values = allowed_values - - -class PasswordForbidden(ValidationError): - """Exception raised when a URL has a password in the userinfo section.""" - - def __init__(self, uri): - """Initialize the error with the URI that failed validation.""" - unsplit = getattr(uri, 'unsplit', lambda: uri) - super(PasswordForbidden, self).__init__( - '"{}" contained a password when validation forbade it'.format( - unsplit() - ) - ) - self.uri = uri - - -class InvalidComponentsError(ValidationError): - """Exception raised when one or more components are invalid.""" - - def __init__(self, uri, *component_names): - """Initialize the error with the invalid component name(s).""" - verb = 'was' - if len(component_names) > 1: - verb = 'were' - - self.uri = uri - self.components = sorted(component_names) - components = ', '.join(self.components) - super(InvalidComponentsError, self).__init__( - "{} {} found to be invalid".format(components, verb), - uri, - self.components, - ) - - -class MissingDependencyError(RFC3986Exception): - """Exception raised when an IRI is encoded without the 'idna' module.""" diff --git a/src/pip/_vendor/urllib3/packages/rfc3986/iri.py b/src/pip/_vendor/urllib3/packages/rfc3986/iri.py deleted file mode 100644 index 416cae4a715..00000000000 --- a/src/pip/_vendor/urllib3/packages/rfc3986/iri.py +++ /dev/null @@ -1,147 +0,0 @@ -"""Module containing the implementation of the IRIReference class.""" -# -*- coding: utf-8 -*- -# Copyright (c) 2014 Rackspace -# Copyright (c) 2015 Ian Stapleton Cordasco -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from collections import namedtuple - -from . import compat -from . import exceptions -from . import misc -from . import normalizers -from . import uri - - -try: - from pip._vendor import idna -except ImportError: # pragma: no cover - idna = None - - -class IRIReference(namedtuple('IRIReference', misc.URI_COMPONENTS), - uri.URIMixin): - """Immutable object representing a parsed IRI Reference. - - Can be encoded into an URIReference object via the procedure - specified in RFC 3987 Section 3.1 - - .. note:: - The IRI submodule is a new interface and may possibly change in - the future. Check for changes to the interface when upgrading. - """ - - slots = () - - def __new__(cls, scheme, authority, path, query, fragment, - encoding='utf-8'): - """Create a new IRIReference.""" - ref = super(IRIReference, cls).__new__( - cls, - scheme or None, - authority or None, - path or None, - query, - fragment) - ref.encoding = encoding - return ref - - def __eq__(self, other): - """Compare this reference to another.""" - other_ref = other - if isinstance(other, tuple): - other_ref = self.__class__(*other) - elif not isinstance(other, IRIReference): - try: - other_ref = self.__class__.from_string(other) - except TypeError: - raise TypeError( - 'Unable to compare {0}() to {1}()'.format( - type(self).__name__, type(other).__name__)) - - # See http://tools.ietf.org/html/rfc3986#section-6.2 - return tuple(self) == tuple(other_ref) - - def _match_subauthority(self): - return misc.ISUBAUTHORITY_MATCHER.match(self.authority) - - @classmethod - def from_string(cls, iri_string, encoding='utf-8'): - """Parse a IRI reference from the given unicode IRI string. - - :param str iri_string: Unicode IRI to be parsed into a reference. - :param str encoding: The encoding of the string provided - :returns: :class:`IRIReference` or subclass thereof - """ - iri_string = compat.to_str(iri_string, encoding) - - split_iri = misc.IRI_MATCHER.match(iri_string).groupdict() - return cls( - split_iri['scheme'], split_iri['authority'], - normalizers.encode_component(split_iri['path'], encoding), - normalizers.encode_component(split_iri['query'], encoding), - normalizers.encode_component(split_iri['fragment'], encoding), - encoding, - ) - - def encode(self, idna_encoder=None): # noqa: C901 - """Encode an IRIReference into a URIReference instance. - - If the ``idna`` module is installed or the ``rfc3986[idna]`` - extra is used then unicode characters in the IRI host - component will be encoded with IDNA2008. - - :param idna_encoder: - Function that encodes each part of the host component - If not given will raise an exception if the IRI - contains a host component. - :rtype: uri.URIReference - :returns: A URI reference - """ - authority = self.authority - if authority: - if idna_encoder is None: - if idna is None: # pragma: no cover - raise exceptions.MissingDependencyError( - "Could not import the 'idna' module " - "and the IRI hostname requires encoding" - ) - - def idna_encoder(name): - if any(ord(c) > 128 for c in name): - try: - return idna.encode(name.lower(), - strict=True, - std3_rules=True) - except idna.IDNAError: - raise exceptions.InvalidAuthority(self.authority) - return name - - authority = "" - if self.host: - authority = ".".join([compat.to_str(idna_encoder(part)) - for part in self.host.split(".")]) - - if self.userinfo is not None: - authority = (normalizers.encode_component( - self.userinfo, self.encoding) + '@' + authority) - - if self.port is not None: - authority += ":" + str(self.port) - - return uri.URIReference(self.scheme, - authority, - path=self.path, - query=self.query, - fragment=self.fragment, - encoding=self.encoding) diff --git a/src/pip/_vendor/urllib3/packages/rfc3986/misc.py b/src/pip/_vendor/urllib3/packages/rfc3986/misc.py deleted file mode 100644 index b735e04402c..00000000000 --- a/src/pip/_vendor/urllib3/packages/rfc3986/misc.py +++ /dev/null @@ -1,124 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2014 Rackspace -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. -# See the License for the specific language governing permissions and -# limitations under the License. -""" -Module containing compiled regular expressions and constants. - -This module contains important constants, patterns, and compiled regular -expressions for parsing and validating URIs and their components. -""" - -import re - -from . import abnf_regexp - -# These are enumerated for the named tuple used as a superclass of -# URIReference -URI_COMPONENTS = ['scheme', 'authority', 'path', 'query', 'fragment'] - -important_characters = { - 'generic_delimiters': abnf_regexp.GENERIC_DELIMITERS, - 'sub_delimiters': abnf_regexp.SUB_DELIMITERS, - # We need to escape the '*' in this case - 're_sub_delimiters': abnf_regexp.SUB_DELIMITERS_RE, - 'unreserved_chars': abnf_regexp.UNRESERVED_CHARS, - # We need to escape the '-' in this case: - 're_unreserved': abnf_regexp.UNRESERVED_RE, -} - -# For details about delimiters and reserved characters, see: -# http://tools.ietf.org/html/rfc3986#section-2.2 -GENERIC_DELIMITERS = abnf_regexp.GENERIC_DELIMITERS_SET -SUB_DELIMITERS = abnf_regexp.SUB_DELIMITERS_SET -RESERVED_CHARS = abnf_regexp.RESERVED_CHARS_SET -# For details about unreserved characters, see: -# http://tools.ietf.org/html/rfc3986#section-2.3 -UNRESERVED_CHARS = abnf_regexp.UNRESERVED_CHARS_SET -NON_PCT_ENCODED = abnf_regexp.NON_PCT_ENCODED_SET - -URI_MATCHER = re.compile(abnf_regexp.URL_PARSING_RE) - -SUBAUTHORITY_MATCHER = re.compile(( - '^(?:(?P<userinfo>{0})@)?' # userinfo - '(?P<host>{1})' # host - ':?(?P<port>{2})?$' # port - ).format(abnf_regexp.USERINFO_RE, - abnf_regexp.HOST_PATTERN, - abnf_regexp.PORT_RE)) - - -HOST_MATCHER = re.compile('^' + abnf_regexp.HOST_RE + '$') -IPv4_MATCHER = re.compile('^' + abnf_regexp.IPv4_RE + '$') -IPv6_MATCHER = re.compile(r'^\[' + abnf_regexp.IPv6_ADDRZ_RFC4007_RE + r'\]$') - -# Used by host validator -IPv6_NO_RFC4007_MATCHER = re.compile(r'^\[%s\]$' % ( - abnf_regexp.IPv6_ADDRZ_RE -)) - -# Matcher used to validate path components -PATH_MATCHER = re.compile(abnf_regexp.PATH_RE) - - -# ################################## -# Query and Fragment Matcher Section -# ################################## - -QUERY_MATCHER = re.compile(abnf_regexp.QUERY_RE) - -FRAGMENT_MATCHER = QUERY_MATCHER - -# Scheme validation, see: http://tools.ietf.org/html/rfc3986#section-3.1 -SCHEME_MATCHER = re.compile('^{0}$'.format(abnf_regexp.SCHEME_RE)) - -RELATIVE_REF_MATCHER = re.compile(r'^%s(\?%s)?(#%s)?$' % ( - abnf_regexp.RELATIVE_PART_RE, - abnf_regexp.QUERY_RE, - abnf_regexp.FRAGMENT_RE, -)) - -# See http://tools.ietf.org/html/rfc3986#section-4.3 -ABSOLUTE_URI_MATCHER = re.compile(r'^%s:%s(\?%s)?$' % ( - abnf_regexp.COMPONENT_PATTERN_DICT['scheme'], - abnf_regexp.HIER_PART_RE, - abnf_regexp.QUERY_RE[1:-1], -)) - -# ############### -# IRIs / RFC 3987 -# ############### - -IRI_MATCHER = re.compile(abnf_regexp.URL_PARSING_RE, re.UNICODE) - -ISUBAUTHORITY_MATCHER = re.compile(( - u'^(?:(?P<userinfo>{0})@)?' # iuserinfo - u'(?P<host>{1})' # ihost - u':?(?P<port>{2})?$' # port - ).format(abnf_regexp.IUSERINFO_RE, - abnf_regexp.IHOST_RE, - abnf_regexp.PORT_RE), re.UNICODE) - - -# Path merger as defined in http://tools.ietf.org/html/rfc3986#section-5.2.3 -def merge_paths(base_uri, relative_path): - """Merge a base URI's path with a relative URI's path.""" - if base_uri.path is None and base_uri.authority is not None: - return '/' + relative_path - else: - path = base_uri.path or '' - index = path.rfind('/') - return path[:index] + '/' + relative_path - - -UseExisting = object() diff --git a/src/pip/_vendor/urllib3/packages/rfc3986/normalizers.py b/src/pip/_vendor/urllib3/packages/rfc3986/normalizers.py deleted file mode 100644 index 2eb1bb36f74..00000000000 --- a/src/pip/_vendor/urllib3/packages/rfc3986/normalizers.py +++ /dev/null @@ -1,167 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2014 Rackspace -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Module with functions to normalize components.""" -import re - -from . import compat -from . import misc - - -def normalize_scheme(scheme): - """Normalize the scheme component.""" - return scheme.lower() - - -def normalize_authority(authority): - """Normalize an authority tuple to a string.""" - userinfo, host, port = authority - result = '' - if userinfo: - result += normalize_percent_characters(userinfo) + '@' - if host: - result += normalize_host(host) - if port: - result += ':' + port - return result - - -def normalize_username(username): - """Normalize a username to make it safe to include in userinfo.""" - return compat.urlquote(username) - - -def normalize_password(password): - """Normalize a password to make safe for userinfo.""" - return compat.urlquote(password) - - -def normalize_host(host): - """Normalize a host string.""" - if misc.IPv6_MATCHER.match(host): - percent = host.find('%') - if percent != -1: - percent_25 = host.find('%25') - - # Replace RFC 4007 IPv6 Zone ID delimiter '%' with '%25' - # from RFC 6874. If the host is '[<IPv6 addr>%25]' then we - # assume RFC 4007 and normalize to '[<IPV6 addr>%2525]' - if percent_25 == -1 or percent < percent_25 or \ - (percent == percent_25 and percent_25 == len(host) - 4): - host = host.replace('%', '%25', 1) - - # Don't normalize the casing of the Zone ID - return host[:percent].lower() + host[percent:] - - return host.lower() - - -def normalize_path(path): - """Normalize the path string.""" - if not path: - return path - - path = normalize_percent_characters(path) - return remove_dot_segments(path) - - -def normalize_query(query): - """Normalize the query string.""" - if not query: - return query - return normalize_percent_characters(query) - - -def normalize_fragment(fragment): - """Normalize the fragment string.""" - if not fragment: - return fragment - return normalize_percent_characters(fragment) - - -PERCENT_MATCHER = re.compile('%[A-Fa-f0-9]{2}') - - -def normalize_percent_characters(s): - """All percent characters should be upper-cased. - - For example, ``"%3afoo%DF%ab"`` should be turned into ``"%3Afoo%DF%AB"``. - """ - matches = set(PERCENT_MATCHER.findall(s)) - for m in matches: - if not m.isupper(): - s = s.replace(m, m.upper()) - return s - - -def remove_dot_segments(s): - """Remove dot segments from the string. - - See also Section 5.2.4 of :rfc:`3986`. - """ - # See http://tools.ietf.org/html/rfc3986#section-5.2.4 for pseudo-code - segments = s.split('/') # Turn the path into a list of segments - output = [] # Initialize the variable to use to store output - - for segment in segments: - # '.' is the current directory, so ignore it, it is superfluous - if segment == '.': - continue - # Anything other than '..', should be appended to the output - elif segment != '..': - output.append(segment) - # In this case segment == '..', if we can, we should pop the last - # element - elif output: - output.pop() - - # If the path starts with '/' and the output is empty or the first string - # is non-empty - if s.startswith('/') and (not output or output[0]): - output.insert(0, '') - - # If the path starts with '/.' or '/..' ensure we add one more empty - # string to add a trailing '/' - if s.endswith(('/.', '/..')): - output.append('') - - return '/'.join(output) - - -def encode_component(uri_component, encoding): - """Encode the specific component in the provided encoding.""" - if uri_component is None: - return uri_component - - # Try to see if the component we're encoding is already percent-encoded - # so we can skip all '%' characters but still encode all others. - percent_encodings = len(PERCENT_MATCHER.findall( - compat.to_str(uri_component, encoding))) - - uri_bytes = compat.to_bytes(uri_component, encoding) - is_percent_encoded = percent_encodings == uri_bytes.count(b'%') - - encoded_uri = bytearray() - - for i in range(0, len(uri_bytes)): - # Will return a single character bytestring on both Python 2 & 3 - byte = uri_bytes[i:i+1] - byte_ord = ord(byte) - if ((is_percent_encoded and byte == b'%') - or (byte_ord < 128 and byte.decode() in misc.NON_PCT_ENCODED)): - encoded_uri.extend(byte) - continue - encoded_uri.extend('%{0:02x}'.format(byte_ord).encode().upper()) - - return encoded_uri.decode(encoding) diff --git a/src/pip/_vendor/urllib3/packages/rfc3986/parseresult.py b/src/pip/_vendor/urllib3/packages/rfc3986/parseresult.py deleted file mode 100644 index 0a734566939..00000000000 --- a/src/pip/_vendor/urllib3/packages/rfc3986/parseresult.py +++ /dev/null @@ -1,385 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2015 Ian Stapleton Cordasco -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Module containing the urlparse compatibility logic.""" -from collections import namedtuple - -from . import compat -from . import exceptions -from . import misc -from . import normalizers -from . import uri - -__all__ = ('ParseResult', 'ParseResultBytes') - -PARSED_COMPONENTS = ('scheme', 'userinfo', 'host', 'port', 'path', 'query', - 'fragment') - - -class ParseResultMixin(object): - def _generate_authority(self, attributes): - # I swear I did not align the comparisons below. That's just how they - # happened to align based on pep8 and attribute lengths. - userinfo, host, port = (attributes[p] - for p in ('userinfo', 'host', 'port')) - if (self.userinfo != userinfo or - self.host != host or - self.port != port): - if port: - port = '{0}'.format(port) - return normalizers.normalize_authority( - (compat.to_str(userinfo, self.encoding), - compat.to_str(host, self.encoding), - port) - ) - return self.authority - - def geturl(self): - """Shim to match the standard library method.""" - return self.unsplit() - - @property - def hostname(self): - """Shim to match the standard library.""" - return self.host - - @property - def netloc(self): - """Shim to match the standard library.""" - return self.authority - - @property - def params(self): - """Shim to match the standard library.""" - return self.query - - -class ParseResult(namedtuple('ParseResult', PARSED_COMPONENTS), - ParseResultMixin): - """Implementation of urlparse compatibility class. - - This uses the URIReference logic to handle compatibility with the - urlparse.ParseResult class. - """ - - slots = () - - def __new__(cls, scheme, userinfo, host, port, path, query, fragment, - uri_ref, encoding='utf-8'): - """Create a new ParseResult.""" - parse_result = super(ParseResult, cls).__new__( - cls, - scheme or None, - userinfo or None, - host, - port or None, - path or None, - query, - fragment) - parse_result.encoding = encoding - parse_result.reference = uri_ref - return parse_result - - @classmethod - def from_parts(cls, scheme=None, userinfo=None, host=None, port=None, - path=None, query=None, fragment=None, encoding='utf-8'): - """Create a ParseResult instance from its parts.""" - authority = '' - if userinfo is not None: - authority += userinfo + '@' - if host is not None: - authority += host - if port is not None: - authority += ':{0}'.format(port) - uri_ref = uri.URIReference(scheme=scheme, - authority=authority, - path=path, - query=query, - fragment=fragment, - encoding=encoding).normalize() - userinfo, host, port = authority_from(uri_ref, strict=True) - return cls(scheme=uri_ref.scheme, - userinfo=userinfo, - host=host, - port=port, - path=uri_ref.path, - query=uri_ref.query, - fragment=uri_ref.fragment, - uri_ref=uri_ref, - encoding=encoding) - - @classmethod - def from_string(cls, uri_string, encoding='utf-8', strict=True, - lazy_normalize=True): - """Parse a URI from the given unicode URI string. - - :param str uri_string: Unicode URI to be parsed into a reference. - :param str encoding: The encoding of the string provided - :param bool strict: Parse strictly according to :rfc:`3986` if True. - If False, parse similarly to the standard library's urlparse - function. - :returns: :class:`ParseResult` or subclass thereof - """ - reference = uri.URIReference.from_string(uri_string, encoding) - if not lazy_normalize: - reference = reference.normalize() - userinfo, host, port = authority_from(reference, strict) - - return cls(scheme=reference.scheme, - userinfo=userinfo, - host=host, - port=port, - path=reference.path, - query=reference.query, - fragment=reference.fragment, - uri_ref=reference, - encoding=encoding) - - @property - def authority(self): - """Return the normalized authority.""" - return self.reference.authority - - def copy_with(self, scheme=misc.UseExisting, userinfo=misc.UseExisting, - host=misc.UseExisting, port=misc.UseExisting, - path=misc.UseExisting, query=misc.UseExisting, - fragment=misc.UseExisting): - """Create a copy of this instance replacing with specified parts.""" - attributes = zip(PARSED_COMPONENTS, - (scheme, userinfo, host, port, path, query, fragment)) - attrs_dict = {} - for name, value in attributes: - if value is misc.UseExisting: - value = getattr(self, name) - attrs_dict[name] = value - authority = self._generate_authority(attrs_dict) - ref = self.reference.copy_with(scheme=attrs_dict['scheme'], - authority=authority, - path=attrs_dict['path'], - query=attrs_dict['query'], - fragment=attrs_dict['fragment']) - return ParseResult(uri_ref=ref, encoding=self.encoding, **attrs_dict) - - def encode(self, encoding=None): - """Convert to an instance of ParseResultBytes.""" - encoding = encoding or self.encoding - attrs = dict( - zip(PARSED_COMPONENTS, - (attr.encode(encoding) if hasattr(attr, 'encode') else attr - for attr in self))) - return ParseResultBytes( - uri_ref=self.reference, - encoding=encoding, - **attrs - ) - - def unsplit(self, use_idna=False): - """Create a URI string from the components. - - :returns: The parsed URI reconstituted as a string. - :rtype: str - """ - parse_result = self - if use_idna and self.host: - hostbytes = self.host.encode('idna') - host = hostbytes.decode(self.encoding) - parse_result = self.copy_with(host=host) - return parse_result.reference.unsplit() - - -class ParseResultBytes(namedtuple('ParseResultBytes', PARSED_COMPONENTS), - ParseResultMixin): - """Compatibility shim for the urlparse.ParseResultBytes object.""" - - def __new__(cls, scheme, userinfo, host, port, path, query, fragment, - uri_ref, encoding='utf-8', lazy_normalize=True): - """Create a new ParseResultBytes instance.""" - parse_result = super(ParseResultBytes, cls).__new__( - cls, - scheme or None, - userinfo or None, - host, - port or None, - path or None, - query or None, - fragment or None) - parse_result.encoding = encoding - parse_result.reference = uri_ref - parse_result.lazy_normalize = lazy_normalize - return parse_result - - @classmethod - def from_parts(cls, scheme=None, userinfo=None, host=None, port=None, - path=None, query=None, fragment=None, encoding='utf-8', - lazy_normalize=True): - """Create a ParseResult instance from its parts.""" - authority = '' - if userinfo is not None: - authority += userinfo + '@' - if host is not None: - authority += host - if port is not None: - authority += ':{0}'.format(int(port)) - uri_ref = uri.URIReference(scheme=scheme, - authority=authority, - path=path, - query=query, - fragment=fragment, - encoding=encoding) - if not lazy_normalize: - uri_ref = uri_ref.normalize() - to_bytes = compat.to_bytes - userinfo, host, port = authority_from(uri_ref, strict=True) - return cls(scheme=to_bytes(scheme, encoding), - userinfo=to_bytes(userinfo, encoding), - host=to_bytes(host, encoding), - port=port, - path=to_bytes(path, encoding), - query=to_bytes(query, encoding), - fragment=to_bytes(fragment, encoding), - uri_ref=uri_ref, - encoding=encoding, - lazy_normalize=lazy_normalize) - - @classmethod - def from_string(cls, uri_string, encoding='utf-8', strict=True, - lazy_normalize=True): - """Parse a URI from the given unicode URI string. - - :param str uri_string: Unicode URI to be parsed into a reference. - :param str encoding: The encoding of the string provided - :param bool strict: Parse strictly according to :rfc:`3986` if True. - If False, parse similarly to the standard library's urlparse - function. - :returns: :class:`ParseResultBytes` or subclass thereof - """ - reference = uri.URIReference.from_string(uri_string, encoding) - if not lazy_normalize: - reference = reference.normalize() - userinfo, host, port = authority_from(reference, strict) - - to_bytes = compat.to_bytes - return cls(scheme=to_bytes(reference.scheme, encoding), - userinfo=to_bytes(userinfo, encoding), - host=to_bytes(host, encoding), - port=port, - path=to_bytes(reference.path, encoding), - query=to_bytes(reference.query, encoding), - fragment=to_bytes(reference.fragment, encoding), - uri_ref=reference, - encoding=encoding, - lazy_normalize=lazy_normalize) - - @property - def authority(self): - """Return the normalized authority.""" - return self.reference.authority.encode(self.encoding) - - def copy_with(self, scheme=misc.UseExisting, userinfo=misc.UseExisting, - host=misc.UseExisting, port=misc.UseExisting, - path=misc.UseExisting, query=misc.UseExisting, - fragment=misc.UseExisting, lazy_normalize=True): - """Create a copy of this instance replacing with specified parts.""" - attributes = zip(PARSED_COMPONENTS, - (scheme, userinfo, host, port, path, query, fragment)) - attrs_dict = {} - for name, value in attributes: - if value is misc.UseExisting: - value = getattr(self, name) - if not isinstance(value, bytes) and hasattr(value, 'encode'): - value = value.encode(self.encoding) - attrs_dict[name] = value - authority = self._generate_authority(attrs_dict) - to_str = compat.to_str - ref = self.reference.copy_with( - scheme=to_str(attrs_dict['scheme'], self.encoding), - authority=to_str(authority, self.encoding), - path=to_str(attrs_dict['path'], self.encoding), - query=to_str(attrs_dict['query'], self.encoding), - fragment=to_str(attrs_dict['fragment'], self.encoding) - ) - if not lazy_normalize: - ref = ref.normalize() - return ParseResultBytes( - uri_ref=ref, - encoding=self.encoding, - lazy_normalize=lazy_normalize, - **attrs_dict - ) - - def unsplit(self, use_idna=False): - """Create a URI bytes object from the components. - - :returns: The parsed URI reconstituted as a string. - :rtype: bytes - """ - parse_result = self - if use_idna and self.host: - # self.host is bytes, to encode to idna, we need to decode it - # first - host = self.host.decode(self.encoding) - hostbytes = host.encode('idna') - parse_result = self.copy_with(host=hostbytes) - if self.lazy_normalize: - parse_result = parse_result.copy_with(lazy_normalize=False) - uri = parse_result.reference.unsplit() - return uri.encode(self.encoding) - - -def split_authority(authority): - # Initialize our expected return values - userinfo = host = port = None - # Initialize an extra var we may need to use - extra_host = None - # Set-up rest in case there is no userinfo portion - rest = authority - - if '@' in authority: - userinfo, rest = authority.rsplit('@', 1) - - # Handle IPv6 host addresses - if rest.startswith('['): - host, rest = rest.split(']', 1) - host += ']' - - if ':' in rest: - extra_host, port = rest.split(':', 1) - elif not host and rest: - host = rest - - if extra_host and not host: - host = extra_host - - return userinfo, host, port - - -def authority_from(reference, strict): - try: - subauthority = reference.authority_info() - except exceptions.InvalidAuthority: - if strict: - raise - userinfo, host, port = split_authority(reference.authority) - else: - # Thanks to Richard Barrell for this idea: - # https://twitter.com/0x2ba22e11/status/617338811975139328 - userinfo, host, port = (subauthority.get(p) - for p in ('userinfo', 'host', 'port')) - - if port: - try: - port = int(port) - except ValueError: - raise exceptions.InvalidPort(port) - return userinfo, host, port diff --git a/src/pip/_vendor/urllib3/packages/rfc3986/uri.py b/src/pip/_vendor/urllib3/packages/rfc3986/uri.py deleted file mode 100644 index d1d71505e2a..00000000000 --- a/src/pip/_vendor/urllib3/packages/rfc3986/uri.py +++ /dev/null @@ -1,153 +0,0 @@ -"""Module containing the implementation of the URIReference class.""" -# -*- coding: utf-8 -*- -# Copyright (c) 2014 Rackspace -# Copyright (c) 2015 Ian Stapleton Cordasco -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. -# See the License for the specific language governing permissions and -# limitations under the License. -from collections import namedtuple - -from . import compat -from . import misc -from . import normalizers -from ._mixin import URIMixin - - -class URIReference(namedtuple('URIReference', misc.URI_COMPONENTS), URIMixin): - """Immutable object representing a parsed URI Reference. - - .. note:: - - This class is not intended to be directly instantiated by the user. - - This object exposes attributes for the following components of a - URI: - - - scheme - - authority - - path - - query - - fragment - - .. attribute:: scheme - - The scheme that was parsed for the URI Reference. For example, - ``http``, ``https``, ``smtp``, ``imap``, etc. - - .. attribute:: authority - - Component of the URI that contains the user information, host, - and port sub-components. For example, - ``google.com``, ``127.0.0.1:5000``, ``username@[::1]``, - ``username:password@example.com:443``, etc. - - .. attribute:: path - - The path that was parsed for the given URI Reference. For example, - ``/``, ``/index.php``, etc. - - .. attribute:: query - - The query component for a given URI Reference. For example, ``a=b``, - ``a=b%20c``, ``a=b+c``, ``a=b,c=d,e=%20f``, etc. - - .. attribute:: fragment - - The fragment component of a URI. For example, ``section-3.1``. - - This class also provides extra attributes for easier access to information - like the subcomponents of the authority component. - - .. attribute:: userinfo - - The user information parsed from the authority. - - .. attribute:: host - - The hostname, IPv4, or IPv6 adddres parsed from the authority. - - .. attribute:: port - - The port parsed from the authority. - """ - - slots = () - - def __new__(cls, scheme, authority, path, query, fragment, - encoding='utf-8'): - """Create a new URIReference.""" - ref = super(URIReference, cls).__new__( - cls, - scheme or None, - authority or None, - path or None, - query, - fragment) - ref.encoding = encoding - return ref - - __hash__ = tuple.__hash__ - - def __eq__(self, other): - """Compare this reference to another.""" - other_ref = other - if isinstance(other, tuple): - other_ref = URIReference(*other) - elif not isinstance(other, URIReference): - try: - other_ref = URIReference.from_string(other) - except TypeError: - raise TypeError( - 'Unable to compare URIReference() to {0}()'.format( - type(other).__name__)) - - # See http://tools.ietf.org/html/rfc3986#section-6.2 - naive_equality = tuple(self) == tuple(other_ref) - return naive_equality or self.normalized_equality(other_ref) - - def normalize(self): - """Normalize this reference as described in Section 6.2.2. - - This is not an in-place normalization. Instead this creates a new - URIReference. - - :returns: A new reference object with normalized components. - :rtype: URIReference - """ - # See http://tools.ietf.org/html/rfc3986#section-6.2.2 for logic in - # this method. - return URIReference(normalizers.normalize_scheme(self.scheme or ''), - normalizers.normalize_authority( - (self.userinfo, self.host, self.port)), - normalizers.normalize_path(self.path or ''), - normalizers.normalize_query(self.query), - normalizers.normalize_fragment(self.fragment), - self.encoding) - - @classmethod - def from_string(cls, uri_string, encoding='utf-8'): - """Parse a URI reference from the given unicode URI string. - - :param str uri_string: Unicode URI to be parsed into a reference. - :param str encoding: The encoding of the string provided - :returns: :class:`URIReference` or subclass thereof - """ - uri_string = compat.to_str(uri_string, encoding) - - split_uri = misc.URI_MATCHER.match(uri_string).groupdict() - return cls( - split_uri['scheme'], split_uri['authority'], - normalizers.encode_component(split_uri['path'], encoding), - normalizers.encode_component(split_uri['query'], encoding), - normalizers.encode_component(split_uri['fragment'], encoding), - encoding, - ) diff --git a/src/pip/_vendor/urllib3/packages/rfc3986/validators.py b/src/pip/_vendor/urllib3/packages/rfc3986/validators.py deleted file mode 100644 index 7fc97215b1f..00000000000 --- a/src/pip/_vendor/urllib3/packages/rfc3986/validators.py +++ /dev/null @@ -1,450 +0,0 @@ -# -*- coding: utf-8 -*- -# Copyright (c) 2017 Ian Stapleton Cordasco -# Licensed under the Apache License, Version 2.0 (the "License"); -# you may not use this file except in compliance with the License. -# You may obtain a copy of the License at -# -# http://www.apache.org/licenses/LICENSE-2.0 -# -# Unless required by applicable law or agreed to in writing, software -# distributed under the License is distributed on an "AS IS" BASIS, -# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or -# implied. -# See the License for the specific language governing permissions and -# limitations under the License. -"""Module containing the validation logic for rfc3986.""" -from . import exceptions -from . import misc -from . import normalizers - - -class Validator(object): - """Object used to configure validation of all objects in rfc3986. - - .. versionadded:: 1.0 - - Example usage:: - - >>> from rfc3986 import api, validators - >>> uri = api.uri_reference('https://github.com/') - >>> validator = validators.Validator().require_presence_of( - ... 'scheme', 'host', 'path', - ... ).allow_schemes( - ... 'http', 'https', - ... ).allow_hosts( - ... '127.0.0.1', 'github.com', - ... ) - >>> validator.validate(uri) - >>> invalid_uri = rfc3986.uri_reference('imap://mail.google.com') - >>> validator.validate(invalid_uri) - Traceback (most recent call last): - ... - rfc3986.exceptions.MissingComponentError: ('path was required but - missing', URIReference(scheme=u'imap', authority=u'mail.google.com', - path=None, query=None, fragment=None), ['path']) - - """ - - COMPONENT_NAMES = frozenset([ - 'scheme', - 'userinfo', - 'host', - 'port', - 'path', - 'query', - 'fragment', - ]) - - def __init__(self): - """Initialize our default validations.""" - self.allowed_schemes = set() - self.allowed_hosts = set() - self.allowed_ports = set() - self.allow_password = True - self.required_components = { - 'scheme': False, - 'userinfo': False, - 'host': False, - 'port': False, - 'path': False, - 'query': False, - 'fragment': False, - } - self.validated_components = self.required_components.copy() - - def allow_schemes(self, *schemes): - """Require the scheme to be one of the provided schemes. - - .. versionadded:: 1.0 - - :param schemes: - Schemes, without ``://`` that are allowed. - :returns: - The validator instance. - :rtype: - Validator - """ - for scheme in schemes: - self.allowed_schemes.add(normalizers.normalize_scheme(scheme)) - return self - - def allow_hosts(self, *hosts): - """Require the host to be one of the provided hosts. - - .. versionadded:: 1.0 - - :param hosts: - Hosts that are allowed. - :returns: - The validator instance. - :rtype: - Validator - """ - for host in hosts: - self.allowed_hosts.add(normalizers.normalize_host(host)) - return self - - def allow_ports(self, *ports): - """Require the port to be one of the provided ports. - - .. versionadded:: 1.0 - - :param ports: - Ports that are allowed. - :returns: - The validator instance. - :rtype: - Validator - """ - for port in ports: - port_int = int(port, base=10) - if 0 <= port_int <= 65535: - self.allowed_ports.add(port) - return self - - def allow_use_of_password(self): - """Allow passwords to be present in the URI. - - .. versionadded:: 1.0 - - :returns: - The validator instance. - :rtype: - Validator - """ - self.allow_password = True - return self - - def forbid_use_of_password(self): - """Prevent passwords from being included in the URI. - - .. versionadded:: 1.0 - - :returns: - The validator instance. - :rtype: - Validator - """ - self.allow_password = False - return self - - def check_validity_of(self, *components): - """Check the validity of the components provided. - - This can be specified repeatedly. - - .. versionadded:: 1.1 - - :param components: - Names of components from :attr:`Validator.COMPONENT_NAMES`. - :returns: - The validator instance. - :rtype: - Validator - """ - components = [c.lower() for c in components] - for component in components: - if component not in self.COMPONENT_NAMES: - raise ValueError( - '"{}" is not a valid component'.format(component) - ) - self.validated_components.update({ - component: True for component in components - }) - return self - - def require_presence_of(self, *components): - """Require the components provided. - - This can be specified repeatedly. - - .. versionadded:: 1.0 - - :param components: - Names of components from :attr:`Validator.COMPONENT_NAMES`. - :returns: - The validator instance. - :rtype: - Validator - """ - components = [c.lower() for c in components] - for component in components: - if component not in self.COMPONENT_NAMES: - raise ValueError( - '"{}" is not a valid component'.format(component) - ) - self.required_components.update({ - component: True for component in components - }) - return self - - def validate(self, uri): - """Check a URI for conditions specified on this validator. - - .. versionadded:: 1.0 - - :param uri: - Parsed URI to validate. - :type uri: - rfc3986.uri.URIReference - :raises MissingComponentError: - When a required component is missing. - :raises UnpermittedComponentError: - When a component is not one of those allowed. - :raises PasswordForbidden: - When a password is present in the userinfo component but is - not permitted by configuration. - :raises InvalidComponentsError: - When a component was found to be invalid. - """ - if not self.allow_password: - check_password(uri) - - required_components = [ - component - for component, required in self.required_components.items() - if required - ] - validated_components = [ - component - for component, required in self.validated_components.items() - if required - ] - if required_components: - ensure_required_components_exist(uri, required_components) - if validated_components: - ensure_components_are_valid(uri, validated_components) - - ensure_one_of(self.allowed_schemes, uri, 'scheme') - ensure_one_of(self.allowed_hosts, uri, 'host') - ensure_one_of(self.allowed_ports, uri, 'port') - - -def check_password(uri): - """Assert that there is no password present in the uri.""" - userinfo = uri.userinfo - if not userinfo: - return - credentials = userinfo.split(':', 1) - if len(credentials) <= 1: - return - raise exceptions.PasswordForbidden(uri) - - -def ensure_one_of(allowed_values, uri, attribute): - """Assert that the uri's attribute is one of the allowed values.""" - value = getattr(uri, attribute) - if value is not None and allowed_values and value not in allowed_values: - raise exceptions.UnpermittedComponentError( - attribute, value, allowed_values, - ) - - -def ensure_required_components_exist(uri, required_components): - """Assert that all required components are present in the URI.""" - missing_components = sorted([ - component - for component in required_components - if getattr(uri, component) is None - ]) - if missing_components: - raise exceptions.MissingComponentError(uri, *missing_components) - - -def is_valid(value, matcher, require): - """Determine if a value is valid based on the provided matcher. - - :param str value: - Value to validate. - :param matcher: - Compiled regular expression to use to validate the value. - :param require: - Whether or not the value is required. - """ - if require: - return (value is not None - and matcher.match(value)) - - # require is False and value is not None - return value is None or matcher.match(value) - - -def authority_is_valid(authority, host=None, require=False): - """Determine if the authority string is valid. - - :param str authority: - The authority to validate. - :param str host: - (optional) The host portion of the authority to validate. - :param bool require: - (optional) Specify if authority must not be None. - :returns: - ``True`` if valid, ``False`` otherwise - :rtype: - bool - """ - validated = is_valid(authority, misc.SUBAUTHORITY_MATCHER, require) - if validated and host is not None: - return host_is_valid(host, require) - return validated - - -def host_is_valid(host, require=False): - """Determine if the host string is valid. - - :param str host: - The host to validate. - :param bool require: - (optional) Specify if host must not be None. - :returns: - ``True`` if valid, ``False`` otherwise - :rtype: - bool - """ - validated = is_valid(host, misc.HOST_MATCHER, require) - if validated and host is not None and misc.IPv4_MATCHER.match(host): - return valid_ipv4_host_address(host) - elif validated and host is not None and misc.IPv6_MATCHER.match(host): - return misc.IPv6_NO_RFC4007_MATCHER.match(host) is not None - return validated - - -def scheme_is_valid(scheme, require=False): - """Determine if the scheme is valid. - - :param str scheme: - The scheme string to validate. - :param bool require: - (optional) Set to ``True`` to require the presence of a scheme. - :returns: - ``True`` if the scheme is valid. ``False`` otherwise. - :rtype: - bool - """ - return is_valid(scheme, misc.SCHEME_MATCHER, require) - - -def path_is_valid(path, require=False): - """Determine if the path component is valid. - - :param str path: - The path string to validate. - :param bool require: - (optional) Set to ``True`` to require the presence of a path. - :returns: - ``True`` if the path is valid. ``False`` otherwise. - :rtype: - bool - """ - return is_valid(path, misc.PATH_MATCHER, require) - - -def query_is_valid(query, require=False): - """Determine if the query component is valid. - - :param str query: - The query string to validate. - :param bool require: - (optional) Set to ``True`` to require the presence of a query. - :returns: - ``True`` if the query is valid. ``False`` otherwise. - :rtype: - bool - """ - return is_valid(query, misc.QUERY_MATCHER, require) - - -def fragment_is_valid(fragment, require=False): - """Determine if the fragment component is valid. - - :param str fragment: - The fragment string to validate. - :param bool require: - (optional) Set to ``True`` to require the presence of a fragment. - :returns: - ``True`` if the fragment is valid. ``False`` otherwise. - :rtype: - bool - """ - return is_valid(fragment, misc.FRAGMENT_MATCHER, require) - - -def valid_ipv4_host_address(host): - """Determine if the given host is a valid IPv4 address.""" - # If the host exists, and it might be IPv4, check each byte in the - # address. - return all([0 <= int(byte, base=10) <= 255 for byte in host.split('.')]) - - -_COMPONENT_VALIDATORS = { - 'scheme': scheme_is_valid, - 'path': path_is_valid, - 'query': query_is_valid, - 'fragment': fragment_is_valid, -} - -_SUBAUTHORITY_VALIDATORS = set(['userinfo', 'host', 'port']) - - -def subauthority_component_is_valid(uri, component): - """Determine if the userinfo, host, and port are valid.""" - try: - subauthority_dict = uri.authority_info() - except exceptions.InvalidAuthority: - return False - - # If we can parse the authority into sub-components and we're not - # validating the port, we can assume it's valid. - if component == 'host': - return host_is_valid(subauthority_dict['host']) - elif component != 'port': - return True - - try: - port = int(subauthority_dict['port']) - except TypeError: - # If the port wasn't provided it'll be None and int(None) raises a - # TypeError - return True - - return (0 <= port <= 65535) - - -def ensure_components_are_valid(uri, validated_components): - """Assert that all components are valid in the URI.""" - invalid_components = set([]) - for component in validated_components: - if component in _SUBAUTHORITY_VALIDATORS: - if not subauthority_component_is_valid(uri, component): - invalid_components.add(component) - # Python's peephole optimizer means that while this continue *is* - # actually executed, coverage.py cannot detect that. See also, - # https://bitbucket.org/ned/coveragepy/issues/198/continue-marked-as-not-covered - continue # nocov: Python 2.7, 3.3, 3.4 - - validator = _COMPONENT_VALIDATORS[component] - if not validator(getattr(uri, component)): - invalid_components.add(component) - - if invalid_components: - raise exceptions.InvalidComponentsError(uri, *invalid_components) diff --git a/src/pip/_vendor/urllib3/packages/six.py b/src/pip/_vendor/urllib3/packages/six.py index 190c0239cd7..314424099f6 100644 --- a/src/pip/_vendor/urllib3/packages/six.py +++ b/src/pip/_vendor/urllib3/packages/six.py @@ -1,6 +1,4 @@ -"""Utilities for writing code that runs on Python 2 and 3""" - -# Copyright (c) 2010-2015 Benjamin Peterson +# Copyright (c) 2010-2019 Benjamin Peterson # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -20,6 +18,8 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. +"""Utilities for writing code that runs on Python 2 and 3""" + from __future__ import absolute_import import functools @@ -29,7 +29,7 @@ import types __author__ = "Benjamin Peterson <benjamin@python.org>" -__version__ = "1.10.0" +__version__ = "1.12.0" # Useful for very coarse version differentiation. @@ -38,15 +38,15 @@ PY34 = sys.version_info[0:2] >= (3, 4) if PY3: - string_types = str, - integer_types = int, - class_types = type, + string_types = (str,) + integer_types = (int,) + class_types = (type,) text_type = str binary_type = bytes MAXSIZE = sys.maxsize else: - string_types = basestring, + string_types = (basestring,) integer_types = (int, long) class_types = (type, types.ClassType) text_type = unicode @@ -58,9 +58,9 @@ else: # It's possible to have sizeof(long) != sizeof(Py_ssize_t). class X(object): - def __len__(self): return 1 << 31 + try: len(X()) except OverflowError: @@ -84,7 +84,6 @@ def _import_module(name): class _LazyDescr(object): - def __init__(self, name): self.name = name @@ -101,7 +100,6 @@ def __get__(self, obj, tp): class MovedModule(_LazyDescr): - def __init__(self, name, old, new=None): super(MovedModule, self).__init__(name) if PY3: @@ -122,7 +120,6 @@ def __getattr__(self, attr): class _LazyModule(types.ModuleType): - def __init__(self, name): super(_LazyModule, self).__init__(name) self.__doc__ = self.__class__.__doc__ @@ -137,7 +134,6 @@ def __dir__(self): class MovedAttribute(_LazyDescr): - def __init__(self, name, old_mod, new_mod, old_attr=None, new_attr=None): super(MovedAttribute, self).__init__(name) if PY3: @@ -221,28 +217,36 @@ def get_code(self, fullname): Required, if is_package is implemented""" self.__get_module(fullname) # eventually raises ImportError return None + get_source = get_code # same as get_code + _importer = _SixMetaPathImporter(__name__) class _MovedItems(_LazyModule): """Lazy loading of moved objects""" + __path__ = [] # mark as package _moved_attributes = [ MovedAttribute("cStringIO", "cStringIO", "io", "StringIO"), MovedAttribute("filter", "itertools", "builtins", "ifilter", "filter"), - MovedAttribute("filterfalse", "itertools", "itertools", "ifilterfalse", "filterfalse"), + MovedAttribute( + "filterfalse", "itertools", "itertools", "ifilterfalse", "filterfalse" + ), MovedAttribute("input", "__builtin__", "builtins", "raw_input", "input"), MovedAttribute("intern", "__builtin__", "sys"), MovedAttribute("map", "itertools", "builtins", "imap", "map"), MovedAttribute("getcwd", "os", "os", "getcwdu", "getcwd"), MovedAttribute("getcwdb", "os", "os", "getcwd", "getcwdb"), + MovedAttribute("getoutput", "commands", "subprocess"), MovedAttribute("range", "__builtin__", "builtins", "xrange", "range"), - MovedAttribute("reload_module", "__builtin__", "importlib" if PY34 else "imp", "reload"), + MovedAttribute( + "reload_module", "__builtin__", "importlib" if PY34 else "imp", "reload" + ), MovedAttribute("reduce", "__builtin__", "functools"), MovedAttribute("shlex_quote", "pipes", "shlex", "quote"), MovedAttribute("StringIO", "StringIO", "io"), @@ -251,7 +255,9 @@ class _MovedItems(_LazyModule): MovedAttribute("UserString", "UserString", "collections"), MovedAttribute("xrange", "__builtin__", "builtins", "xrange", "range"), MovedAttribute("zip", "itertools", "builtins", "izip", "zip"), - MovedAttribute("zip_longest", "itertools", "itertools", "izip_longest", "zip_longest"), + MovedAttribute( + "zip_longest", "itertools", "itertools", "izip_longest", "zip_longest" + ), MovedModule("builtins", "__builtin__"), MovedModule("configparser", "ConfigParser"), MovedModule("copyreg", "copy_reg"), @@ -262,10 +268,13 @@ class _MovedItems(_LazyModule): MovedModule("html_entities", "htmlentitydefs", "html.entities"), MovedModule("html_parser", "HTMLParser", "html.parser"), MovedModule("http_client", "httplib", "http.client"), + MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"), + MovedModule("email_mime_image", "email.MIMEImage", "email.mime.image"), MovedModule("email_mime_multipart", "email.MIMEMultipart", "email.mime.multipart"), - MovedModule("email_mime_nonmultipart", "email.MIMENonMultipart", "email.mime.nonmultipart"), + MovedModule( + "email_mime_nonmultipart", "email.MIMENonMultipart", "email.mime.nonmultipart" + ), MovedModule("email_mime_text", "email.MIMEText", "email.mime.text"), - MovedModule("email_mime_base", "email.MIMEBase", "email.mime.base"), MovedModule("BaseHTTPServer", "BaseHTTPServer", "http.server"), MovedModule("CGIHTTPServer", "CGIHTTPServer", "http.server"), MovedModule("SimpleHTTPServer", "SimpleHTTPServer", "http.server"), @@ -283,15 +292,12 @@ class _MovedItems(_LazyModule): MovedModule("tkinter_ttk", "ttk", "tkinter.ttk"), MovedModule("tkinter_constants", "Tkconstants", "tkinter.constants"), MovedModule("tkinter_dnd", "Tkdnd", "tkinter.dnd"), - MovedModule("tkinter_colorchooser", "tkColorChooser", - "tkinter.colorchooser"), - MovedModule("tkinter_commondialog", "tkCommonDialog", - "tkinter.commondialog"), + MovedModule("tkinter_colorchooser", "tkColorChooser", "tkinter.colorchooser"), + MovedModule("tkinter_commondialog", "tkCommonDialog", "tkinter.commondialog"), MovedModule("tkinter_tkfiledialog", "tkFileDialog", "tkinter.filedialog"), MovedModule("tkinter_font", "tkFont", "tkinter.font"), MovedModule("tkinter_messagebox", "tkMessageBox", "tkinter.messagebox"), - MovedModule("tkinter_tksimpledialog", "tkSimpleDialog", - "tkinter.simpledialog"), + MovedModule("tkinter_tksimpledialog", "tkSimpleDialog", "tkinter.simpledialog"), MovedModule("urllib_parse", __name__ + ".moves.urllib_parse", "urllib.parse"), MovedModule("urllib_error", __name__ + ".moves.urllib_error", "urllib.error"), MovedModule("urllib", __name__ + ".moves.urllib", __name__ + ".moves.urllib"), @@ -301,9 +307,7 @@ class _MovedItems(_LazyModule): ] # Add windows specific modules. if sys.platform == "win32": - _moved_attributes += [ - MovedModule("winreg", "_winreg"), - ] + _moved_attributes += [MovedModule("winreg", "_winreg")] for attr in _moved_attributes: setattr(_MovedItems, attr.name, attr) @@ -337,10 +341,14 @@ class Module_six_moves_urllib_parse(_LazyModule): MovedAttribute("quote_plus", "urllib", "urllib.parse"), MovedAttribute("unquote", "urllib", "urllib.parse"), MovedAttribute("unquote_plus", "urllib", "urllib.parse"), + MovedAttribute( + "unquote_to_bytes", "urllib", "urllib.parse", "unquote", "unquote_to_bytes" + ), MovedAttribute("urlencode", "urllib", "urllib.parse"), MovedAttribute("splitquery", "urllib", "urllib.parse"), MovedAttribute("splittag", "urllib", "urllib.parse"), MovedAttribute("splituser", "urllib", "urllib.parse"), + MovedAttribute("splitvalue", "urllib", "urllib.parse"), MovedAttribute("uses_fragment", "urlparse", "urllib.parse"), MovedAttribute("uses_netloc", "urlparse", "urllib.parse"), MovedAttribute("uses_params", "urlparse", "urllib.parse"), @@ -353,8 +361,11 @@ class Module_six_moves_urllib_parse(_LazyModule): Module_six_moves_urllib_parse._moved_attributes = _urllib_parse_moved_attributes -_importer._add_module(Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse"), - "moves.urllib_parse", "moves.urllib.parse") +_importer._add_module( + Module_six_moves_urllib_parse(__name__ + ".moves.urllib_parse"), + "moves.urllib_parse", + "moves.urllib.parse", +) class Module_six_moves_urllib_error(_LazyModule): @@ -373,8 +384,11 @@ class Module_six_moves_urllib_error(_LazyModule): Module_six_moves_urllib_error._moved_attributes = _urllib_error_moved_attributes -_importer._add_module(Module_six_moves_urllib_error(__name__ + ".moves.urllib.error"), - "moves.urllib_error", "moves.urllib.error") +_importer._add_module( + Module_six_moves_urllib_error(__name__ + ".moves.urllib.error"), + "moves.urllib_error", + "moves.urllib.error", +) class Module_six_moves_urllib_request(_LazyModule): @@ -416,6 +430,8 @@ class Module_six_moves_urllib_request(_LazyModule): MovedAttribute("URLopener", "urllib", "urllib.request"), MovedAttribute("FancyURLopener", "urllib", "urllib.request"), MovedAttribute("proxy_bypass", "urllib", "urllib.request"), + MovedAttribute("parse_http_list", "urllib2", "urllib.request"), + MovedAttribute("parse_keqv_list", "urllib2", "urllib.request"), ] for attr in _urllib_request_moved_attributes: setattr(Module_six_moves_urllib_request, attr.name, attr) @@ -423,8 +439,11 @@ class Module_six_moves_urllib_request(_LazyModule): Module_six_moves_urllib_request._moved_attributes = _urllib_request_moved_attributes -_importer._add_module(Module_six_moves_urllib_request(__name__ + ".moves.urllib.request"), - "moves.urllib_request", "moves.urllib.request") +_importer._add_module( + Module_six_moves_urllib_request(__name__ + ".moves.urllib.request"), + "moves.urllib_request", + "moves.urllib.request", +) class Module_six_moves_urllib_response(_LazyModule): @@ -444,8 +463,11 @@ class Module_six_moves_urllib_response(_LazyModule): Module_six_moves_urllib_response._moved_attributes = _urllib_response_moved_attributes -_importer._add_module(Module_six_moves_urllib_response(__name__ + ".moves.urllib.response"), - "moves.urllib_response", "moves.urllib.response") +_importer._add_module( + Module_six_moves_urllib_response(__name__ + ".moves.urllib.response"), + "moves.urllib_response", + "moves.urllib.response", +) class Module_six_moves_urllib_robotparser(_LazyModule): @@ -454,21 +476,27 @@ class Module_six_moves_urllib_robotparser(_LazyModule): _urllib_robotparser_moved_attributes = [ - MovedAttribute("RobotFileParser", "robotparser", "urllib.robotparser"), + MovedAttribute("RobotFileParser", "robotparser", "urllib.robotparser") ] for attr in _urllib_robotparser_moved_attributes: setattr(Module_six_moves_urllib_robotparser, attr.name, attr) del attr -Module_six_moves_urllib_robotparser._moved_attributes = _urllib_robotparser_moved_attributes +Module_six_moves_urllib_robotparser._moved_attributes = ( + _urllib_robotparser_moved_attributes +) -_importer._add_module(Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser"), - "moves.urllib_robotparser", "moves.urllib.robotparser") +_importer._add_module( + Module_six_moves_urllib_robotparser(__name__ + ".moves.urllib.robotparser"), + "moves.urllib_robotparser", + "moves.urllib.robotparser", +) class Module_six_moves_urllib(types.ModuleType): """Create a six.moves.urllib namespace that resembles the Python 3 namespace""" + __path__ = [] # mark as package parse = _importer._get_module("moves.urllib_parse") error = _importer._get_module("moves.urllib_error") @@ -477,10 +505,12 @@ class Module_six_moves_urllib(types.ModuleType): robotparser = _importer._get_module("moves.urllib_robotparser") def __dir__(self): - return ['parse', 'error', 'request', 'response', 'robotparser'] + return ["parse", "error", "request", "response", "robotparser"] + -_importer._add_module(Module_six_moves_urllib(__name__ + ".moves.urllib"), - "moves.urllib") +_importer._add_module( + Module_six_moves_urllib(__name__ + ".moves.urllib"), "moves.urllib" +) def add_move(move): @@ -520,19 +550,24 @@ def remove_move(name): try: advance_iterator = next except NameError: + def advance_iterator(it): return it.next() + + next = advance_iterator try: callable = callable except NameError: + def callable(obj): return any("__call__" in klass.__dict__ for klass in type(obj).__mro__) if PY3: + def get_unbound_function(unbound): return unbound @@ -543,6 +578,7 @@ def create_unbound_method(func, cls): Iterator = object else: + def get_unbound_function(unbound): return unbound.im_func @@ -553,13 +589,13 @@ def create_unbound_method(func, cls): return types.MethodType(func, None, cls) class Iterator(object): - def next(self): return type(self).__next__(self) callable = callable -_add_doc(get_unbound_function, - """Get the function out of a possibly unbound function""") +_add_doc( + get_unbound_function, """Get the function out of a possibly unbound function""" +) get_method_function = operator.attrgetter(_meth_func) @@ -571,6 +607,7 @@ def next(self): if PY3: + def iterkeys(d, **kw): return iter(d.keys(**kw)) @@ -589,6 +626,7 @@ def iterlists(d, **kw): viewitems = operator.methodcaller("items") else: + def iterkeys(d, **kw): return d.iterkeys(**kw) @@ -609,28 +647,33 @@ def iterlists(d, **kw): _add_doc(iterkeys, "Return an iterator over the keys of a dictionary.") _add_doc(itervalues, "Return an iterator over the values of a dictionary.") -_add_doc(iteritems, - "Return an iterator over the (key, value) pairs of a dictionary.") -_add_doc(iterlists, - "Return an iterator over the (key, [values]) pairs of a dictionary.") +_add_doc(iteritems, "Return an iterator over the (key, value) pairs of a dictionary.") +_add_doc( + iterlists, "Return an iterator over the (key, [values]) pairs of a dictionary." +) if PY3: + def b(s): return s.encode("latin-1") def u(s): return s + unichr = chr import struct + int2byte = struct.Struct(">B").pack del struct byte2int = operator.itemgetter(0) indexbytes = operator.getitem iterbytes = iter import io + StringIO = io.StringIO BytesIO = io.BytesIO + del io _assertCountEqual = "assertCountEqual" if sys.version_info[1] <= 1: _assertRaisesRegex = "assertRaisesRegexp" @@ -639,12 +682,15 @@ def u(s): _assertRaisesRegex = "assertRaisesRegex" _assertRegex = "assertRegex" else: + def b(s): return s + # Workaround for standalone backslash def u(s): - return unicode(s.replace(r'\\', r'\\\\'), "unicode_escape") + return unicode(s.replace(r"\\", r"\\\\"), "unicode_escape") + unichr = unichr int2byte = chr @@ -653,8 +699,10 @@ def byte2int(bs): def indexbytes(buf, i): return ord(buf[i]) + iterbytes = functools.partial(itertools.imap, ord) import StringIO + StringIO = BytesIO = StringIO.StringIO _assertCountEqual = "assertItemsEqual" _assertRaisesRegex = "assertRaisesRegexp" @@ -679,13 +727,19 @@ def assertRegex(self, *args, **kwargs): exec_ = getattr(moves.builtins, "exec") def reraise(tp, value, tb=None): - if value is None: - value = tp() - if value.__traceback__ is not tb: - raise value.with_traceback(tb) - raise value + try: + if value is None: + value = tp() + if value.__traceback__ is not tb: + raise value.with_traceback(tb) + raise value + finally: + value = None + tb = None + else: + def exec_(_code_, _globs_=None, _locs_=None): """Execute code in a namespace.""" if _globs_ is None: @@ -698,28 +752,45 @@ def exec_(_code_, _globs_=None, _locs_=None): _locs_ = _globs_ exec("""exec _code_ in _globs_, _locs_""") - exec_("""def reraise(tp, value, tb=None): - raise tp, value, tb -""") + exec_( + """def reraise(tp, value, tb=None): + try: + raise tp, value, tb + finally: + tb = None +""" + ) if sys.version_info[:2] == (3, 2): - exec_("""def raise_from(value, from_value): - if from_value is None: - raise value - raise value from from_value -""") + exec_( + """def raise_from(value, from_value): + try: + if from_value is None: + raise value + raise value from from_value + finally: + value = None +""" + ) elif sys.version_info[:2] > (3, 2): - exec_("""def raise_from(value, from_value): - raise value from from_value -""") + exec_( + """def raise_from(value, from_value): + try: + raise value from from_value + finally: + value = None +""" + ) else: + def raise_from(value, from_value): raise value print_ = getattr(moves.builtins, "print", None) if print_ is None: + def print_(*args, **kwargs): """The new-style print function for Python 2.4 and 2.5.""" fp = kwargs.pop("file", sys.stdout) @@ -730,14 +801,17 @@ def write(data): if not isinstance(data, basestring): data = str(data) # If the file has an encoding, encode unicode with it. - if (isinstance(fp, file) and - isinstance(data, unicode) and - fp.encoding is not None): + if ( + isinstance(fp, file) + and isinstance(data, unicode) + and fp.encoding is not None + ): errors = getattr(fp, "errors", None) if errors is None: errors = "strict" data = data.encode(fp.encoding, errors) fp.write(data) + want_unicode = False sep = kwargs.pop("sep", None) if sep is not None: @@ -773,6 +847,8 @@ def write(data): write(sep) write(arg) write(end) + + if sys.version_info[:2] < (3, 3): _print = print_ @@ -783,16 +859,24 @@ def print_(*args, **kwargs): if flush and fp is not None: fp.flush() + _add_doc(reraise, """Reraise an exception.""") if sys.version_info[0:2] < (3, 4): - def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS, - updated=functools.WRAPPER_UPDATES): + + def wraps( + wrapped, + assigned=functools.WRAPPER_ASSIGNMENTS, + updated=functools.WRAPPER_UPDATES, + ): def wrapper(f): f = functools.wraps(wrapped, assigned, updated)(f) f.__wrapped__ = wrapped return f + return wrapper + + else: wraps = functools.wraps @@ -802,29 +886,95 @@ def with_metaclass(meta, *bases): # This requires a bit of explanation: the basic idea is to make a dummy # metaclass for one level of class instantiation that replaces itself with # the actual metaclass. - class metaclass(meta): - + class metaclass(type): def __new__(cls, name, this_bases, d): return meta(name, bases, d) - return type.__new__(metaclass, 'temporary_class', (), {}) + + @classmethod + def __prepare__(cls, name, this_bases): + return meta.__prepare__(name, bases) + + return type.__new__(metaclass, "temporary_class", (), {}) def add_metaclass(metaclass): """Class decorator for creating a class with a metaclass.""" + def wrapper(cls): orig_vars = cls.__dict__.copy() - slots = orig_vars.get('__slots__') + slots = orig_vars.get("__slots__") if slots is not None: if isinstance(slots, str): slots = [slots] for slots_var in slots: orig_vars.pop(slots_var) - orig_vars.pop('__dict__', None) - orig_vars.pop('__weakref__', None) + orig_vars.pop("__dict__", None) + orig_vars.pop("__weakref__", None) + if hasattr(cls, "__qualname__"): + orig_vars["__qualname__"] = cls.__qualname__ return metaclass(cls.__name__, cls.__bases__, orig_vars) + return wrapper +def ensure_binary(s, encoding="utf-8", errors="strict"): + """Coerce **s** to six.binary_type. + + For Python 2: + - `unicode` -> encoded to `str` + - `str` -> `str` + + For Python 3: + - `str` -> encoded to `bytes` + - `bytes` -> `bytes` + """ + if isinstance(s, text_type): + return s.encode(encoding, errors) + elif isinstance(s, binary_type): + return s + else: + raise TypeError("not expecting type '%s'" % type(s)) + + +def ensure_str(s, encoding="utf-8", errors="strict"): + """Coerce *s* to `str`. + + For Python 2: + - `unicode` -> encoded to `str` + - `str` -> `str` + + For Python 3: + - `str` -> `str` + - `bytes` -> decoded to `str` + """ + if not isinstance(s, (text_type, binary_type)): + raise TypeError("not expecting type '%s'" % type(s)) + if PY2 and isinstance(s, text_type): + s = s.encode(encoding, errors) + elif PY3 and isinstance(s, binary_type): + s = s.decode(encoding, errors) + return s + + +def ensure_text(s, encoding="utf-8", errors="strict"): + """Coerce *s* to six.text_type. + + For Python 2: + - `unicode` -> `unicode` + - `str` -> `unicode` + + For Python 3: + - `str` -> `str` + - `bytes` -> decoded to `str` + """ + if isinstance(s, binary_type): + return s.decode(encoding, errors) + elif isinstance(s, text_type): + return s + else: + raise TypeError("not expecting type '%s'" % type(s)) + + def python_2_unicode_compatible(klass): """ A decorator that defines __unicode__ and __str__ methods under Python 2. @@ -834,12 +984,13 @@ def python_2_unicode_compatible(klass): returning text and apply this decorator to the class. """ if PY2: - if '__str__' not in klass.__dict__: - raise ValueError("@python_2_unicode_compatible cannot be applied " - "to %s because it doesn't define __str__()." % - klass.__name__) + if "__str__" not in klass.__dict__: + raise ValueError( + "@python_2_unicode_compatible cannot be applied " + "to %s because it doesn't define __str__()." % klass.__name__ + ) klass.__unicode__ = klass.__str__ - klass.__str__ = lambda self: self.__unicode__().encode('utf-8') + klass.__str__ = lambda self: self.__unicode__().encode("utf-8") return klass @@ -859,8 +1010,10 @@ def python_2_unicode_compatible(klass): # be floating around. Therefore, we can't use isinstance() to check for # the six meta path importer, since the other six instance will have # inserted an importer with different class. - if (type(importer).__name__ == "_SixMetaPathImporter" and - importer.name == __name__): + if ( + type(importer).__name__ == "_SixMetaPathImporter" + and importer.name == __name__ + ): del sys.meta_path[i] break del i, importer diff --git a/src/pip/_vendor/urllib3/packages/ssl_match_hostname/__init__.py b/src/pip/_vendor/urllib3/packages/ssl_match_hostname/__init__.py index d6594eb264d..75b6bb1cf0d 100644 --- a/src/pip/_vendor/urllib3/packages/ssl_match_hostname/__init__.py +++ b/src/pip/_vendor/urllib3/packages/ssl_match_hostname/__init__.py @@ -16,4 +16,4 @@ from ._implementation import CertificateError, match_hostname # Not needed, but documenting what we provide. -__all__ = ('CertificateError', 'match_hostname') +__all__ = ("CertificateError", "match_hostname") diff --git a/src/pip/_vendor/urllib3/packages/ssl_match_hostname/_implementation.py b/src/pip/_vendor/urllib3/packages/ssl_match_hostname/_implementation.py index 970cf653e7f..89543225241 100644 --- a/src/pip/_vendor/urllib3/packages/ssl_match_hostname/_implementation.py +++ b/src/pip/_vendor/urllib3/packages/ssl_match_hostname/_implementation.py @@ -15,7 +15,7 @@ except ImportError: ipaddress = None -__version__ = '3.5.0.1' +__version__ = "3.5.0.1" class CertificateError(ValueError): @@ -33,18 +33,19 @@ def _dnsname_match(dn, hostname, max_wildcards=1): # Ported from python3-syntax: # leftmost, *remainder = dn.split(r'.') - parts = dn.split(r'.') + parts = dn.split(r".") leftmost = parts[0] remainder = parts[1:] - wildcards = leftmost.count('*') + wildcards = leftmost.count("*") if wildcards > max_wildcards: # Issue #17980: avoid denials of service by refusing more # than one wildcard per fragment. A survey of established # policy among SSL implementations showed it to be a # reasonable choice. raise CertificateError( - "too many wildcards in certificate DNS name: " + repr(dn)) + "too many wildcards in certificate DNS name: " + repr(dn) + ) # speed up common case w/o wildcards if not wildcards: @@ -53,11 +54,11 @@ def _dnsname_match(dn, hostname, max_wildcards=1): # RFC 6125, section 6.4.3, subitem 1. # The client SHOULD NOT attempt to match a presented identifier in which # the wildcard character comprises a label other than the left-most label. - if leftmost == '*': + if leftmost == "*": # When '*' is a fragment by itself, it matches a non-empty dotless # fragment. - pats.append('[^.]+') - elif leftmost.startswith('xn--') or hostname.startswith('xn--'): + pats.append("[^.]+") + elif leftmost.startswith("xn--") or hostname.startswith("xn--"): # RFC 6125, section 6.4.3, subitem 3. # The client SHOULD NOT attempt to match a presented identifier # where the wildcard character is embedded within an A-label or @@ -65,21 +66,22 @@ def _dnsname_match(dn, hostname, max_wildcards=1): pats.append(re.escape(leftmost)) else: # Otherwise, '*' matches any dotless string, e.g. www* - pats.append(re.escape(leftmost).replace(r'\*', '[^.]*')) + pats.append(re.escape(leftmost).replace(r"\*", "[^.]*")) # add the remaining fragments, ignore any wildcards for frag in remainder: pats.append(re.escape(frag)) - pat = re.compile(r'\A' + r'\.'.join(pats) + r'\Z', re.IGNORECASE) + pat = re.compile(r"\A" + r"\.".join(pats) + r"\Z", re.IGNORECASE) return pat.match(hostname) def _to_unicode(obj): if isinstance(obj, str) and sys.version_info < (3,): - obj = unicode(obj, encoding='ascii', errors='strict') + obj = unicode(obj, encoding="ascii", errors="strict") return obj + def _ipaddress_match(ipname, host_ip): """Exact matching of IP addresses. @@ -101,9 +103,11 @@ def match_hostname(cert, hostname): returns nothing. """ if not cert: - raise ValueError("empty or no certificate, match_hostname needs a " - "SSL socket or SSL context with either " - "CERT_OPTIONAL or CERT_REQUIRED") + raise ValueError( + "empty or no certificate, match_hostname needs a " + "SSL socket or SSL context with either " + "CERT_OPTIONAL or CERT_REQUIRED" + ) try: # Divergence from upstream: ipaddress can't handle byte str host_ip = ipaddress.ip_address(_to_unicode(hostname)) @@ -122,35 +126,37 @@ def match_hostname(cert, hostname): else: raise dnsnames = [] - san = cert.get('subjectAltName', ()) + san = cert.get("subjectAltName", ()) for key, value in san: - if key == 'DNS': + if key == "DNS": if host_ip is None and _dnsname_match(value, hostname): return dnsnames.append(value) - elif key == 'IP Address': + elif key == "IP Address": if host_ip is not None and _ipaddress_match(value, host_ip): return dnsnames.append(value) if not dnsnames: # The subject is only checked when there is no dNSName entry # in subjectAltName - for sub in cert.get('subject', ()): + for sub in cert.get("subject", ()): for key, value in sub: # XXX according to RFC 2818, the most specific Common Name # must be used. - if key == 'commonName': + if key == "commonName": if _dnsname_match(value, hostname): return dnsnames.append(value) if len(dnsnames) > 1: - raise CertificateError("hostname %r " - "doesn't match either of %s" - % (hostname, ', '.join(map(repr, dnsnames)))) + raise CertificateError( + "hostname %r " + "doesn't match either of %s" % (hostname, ", ".join(map(repr, dnsnames))) + ) elif len(dnsnames) == 1: - raise CertificateError("hostname %r " - "doesn't match %r" - % (hostname, dnsnames[0])) + raise CertificateError( + "hostname %r " "doesn't match %r" % (hostname, dnsnames[0]) + ) else: - raise CertificateError("no appropriate commonName or " - "subjectAltName fields were found") + raise CertificateError( + "no appropriate commonName or " "subjectAltName fields were found" + ) diff --git a/src/pip/_vendor/urllib3/poolmanager.py b/src/pip/_vendor/urllib3/poolmanager.py index a6ade6e9056..242a2f8203f 100644 --- a/src/pip/_vendor/urllib3/poolmanager.py +++ b/src/pip/_vendor/urllib3/poolmanager.py @@ -14,48 +14,55 @@ from .util.retry import Retry -__all__ = ['PoolManager', 'ProxyManager', 'proxy_from_url'] +__all__ = ["PoolManager", "ProxyManager", "proxy_from_url"] log = logging.getLogger(__name__) -SSL_KEYWORDS = ('key_file', 'cert_file', 'cert_reqs', 'ca_certs', - 'ssl_version', 'ca_cert_dir', 'ssl_context', - 'key_password') +SSL_KEYWORDS = ( + "key_file", + "cert_file", + "cert_reqs", + "ca_certs", + "ssl_version", + "ca_cert_dir", + "ssl_context", + "key_password", +) # All known keyword arguments that could be provided to the pool manager, its # pools, or the underlying connections. This is used to construct a pool key. _key_fields = ( - 'key_scheme', # str - 'key_host', # str - 'key_port', # int - 'key_timeout', # int or float or Timeout - 'key_retries', # int or Retry - 'key_strict', # bool - 'key_block', # bool - 'key_source_address', # str - 'key_key_file', # str - 'key_key_password', # str - 'key_cert_file', # str - 'key_cert_reqs', # str - 'key_ca_certs', # str - 'key_ssl_version', # str - 'key_ca_cert_dir', # str - 'key_ssl_context', # instance of ssl.SSLContext or urllib3.util.ssl_.SSLContext - 'key_maxsize', # int - 'key_headers', # dict - 'key__proxy', # parsed proxy url - 'key__proxy_headers', # dict - 'key_socket_options', # list of (level (int), optname (int), value (int or str)) tuples - 'key__socks_options', # dict - 'key_assert_hostname', # bool or string - 'key_assert_fingerprint', # str - 'key_server_hostname', # str + "key_scheme", # str + "key_host", # str + "key_port", # int + "key_timeout", # int or float or Timeout + "key_retries", # int or Retry + "key_strict", # bool + "key_block", # bool + "key_source_address", # str + "key_key_file", # str + "key_key_password", # str + "key_cert_file", # str + "key_cert_reqs", # str + "key_ca_certs", # str + "key_ssl_version", # str + "key_ca_cert_dir", # str + "key_ssl_context", # instance of ssl.SSLContext or urllib3.util.ssl_.SSLContext + "key_maxsize", # int + "key_headers", # dict + "key__proxy", # parsed proxy url + "key__proxy_headers", # dict + "key_socket_options", # list of (level (int), optname (int), value (int or str)) tuples + "key__socks_options", # dict + "key_assert_hostname", # bool or string + "key_assert_fingerprint", # str + "key_server_hostname", # str ) #: The namedtuple class used to construct keys for the connection pool. #: All custom key schemes should include the fields in this key at a minimum. -PoolKey = collections.namedtuple('PoolKey', _key_fields) +PoolKey = collections.namedtuple("PoolKey", _key_fields) def _default_key_normalizer(key_class, request_context): @@ -80,24 +87,24 @@ def _default_key_normalizer(key_class, request_context): """ # Since we mutate the dictionary, make a copy first context = request_context.copy() - context['scheme'] = context['scheme'].lower() - context['host'] = context['host'].lower() + context["scheme"] = context["scheme"].lower() + context["host"] = context["host"].lower() # These are both dictionaries and need to be transformed into frozensets - for key in ('headers', '_proxy_headers', '_socks_options'): + for key in ("headers", "_proxy_headers", "_socks_options"): if key in context and context[key] is not None: context[key] = frozenset(context[key].items()) # The socket_options key may be a list and needs to be transformed into a # tuple. - socket_opts = context.get('socket_options') + socket_opts = context.get("socket_options") if socket_opts is not None: - context['socket_options'] = tuple(socket_opts) + context["socket_options"] = tuple(socket_opts) # Map the kwargs to the names in the namedtuple - this is necessary since # namedtuples can't have fields starting with '_'. for key in list(context.keys()): - context['key_' + key] = context.pop(key) + context["key_" + key] = context.pop(key) # Default to ``None`` for keys missing from the context for field in key_class._fields: @@ -112,14 +119,11 @@ def _default_key_normalizer(key_class, request_context): #: Each PoolManager makes a copy of this dictionary so they can be configured #: globally here, or individually on the instance. key_fn_by_scheme = { - 'http': functools.partial(_default_key_normalizer, PoolKey), - 'https': functools.partial(_default_key_normalizer, PoolKey), + "http": functools.partial(_default_key_normalizer, PoolKey), + "https": functools.partial(_default_key_normalizer, PoolKey), } -pool_classes_by_scheme = { - 'http': HTTPConnectionPool, - 'https': HTTPSConnectionPool, -} +pool_classes_by_scheme = {"http": HTTPConnectionPool, "https": HTTPSConnectionPool} class PoolManager(RequestMethods): @@ -155,8 +159,7 @@ class PoolManager(RequestMethods): def __init__(self, num_pools=10, headers=None, **connection_pool_kw): RequestMethods.__init__(self, headers) self.connection_pool_kw = connection_pool_kw - self.pools = RecentlyUsedContainer(num_pools, - dispose_func=lambda p: p.close()) + self.pools = RecentlyUsedContainer(num_pools, dispose_func=lambda p: p.close()) # Locally set the pool classes and keys so other PoolManagers can # override them. @@ -189,10 +192,10 @@ def _new_pool(self, scheme, host, port, request_context=None): # this function has historically only used the scheme, host, and port # in the positional args. When an API change is acceptable these can # be removed. - for key in ('scheme', 'host', 'port'): + for key in ("scheme", "host", "port"): request_context.pop(key, None) - if scheme == 'http': + if scheme == "http": for kw in SSL_KEYWORDS: request_context.pop(kw, None) @@ -207,7 +210,7 @@ def clear(self): """ self.pools.clear() - def connection_from_host(self, host, port=None, scheme='http', pool_kwargs=None): + def connection_from_host(self, host, port=None, scheme="http", pool_kwargs=None): """ Get a :class:`ConnectionPool` based on the host, port, and scheme. @@ -222,11 +225,11 @@ def connection_from_host(self, host, port=None, scheme='http', pool_kwargs=None) raise LocationValueError("No host specified.") request_context = self._merge_pool_kwargs(pool_kwargs) - request_context['scheme'] = scheme or 'http' + request_context["scheme"] = scheme or "http" if not port: - port = port_by_scheme.get(request_context['scheme'].lower(), 80) - request_context['port'] = port - request_context['host'] = host + port = port_by_scheme.get(request_context["scheme"].lower(), 80) + request_context["port"] = port + request_context["host"] = host return self.connection_from_context(request_context) @@ -237,7 +240,7 @@ def connection_from_context(self, request_context): ``request_context`` must at least contain the ``scheme`` key and its value must be a key in ``key_fn_by_scheme`` instance variable. """ - scheme = request_context['scheme'].lower() + scheme = request_context["scheme"].lower() pool_key_constructor = self.key_fn_by_scheme[scheme] pool_key = pool_key_constructor(request_context) @@ -259,9 +262,9 @@ def connection_from_pool_key(self, pool_key, request_context=None): return pool # Make a fresh ConnectionPool of the desired type - scheme = request_context['scheme'] - host = request_context['host'] - port = request_context['port'] + scheme = request_context["scheme"] + host = request_context["host"] + port = request_context["port"] pool = self._new_pool(scheme, host, port, request_context=request_context) self.pools[pool_key] = pool @@ -279,8 +282,9 @@ def connection_from_url(self, url, pool_kwargs=None): not used. """ u = parse_url(url) - return self.connection_from_host(u.host, port=u.port, scheme=u.scheme, - pool_kwargs=pool_kwargs) + return self.connection_from_host( + u.host, port=u.port, scheme=u.scheme, pool_kwargs=pool_kwargs + ) def _merge_pool_kwargs(self, override): """ @@ -314,11 +318,11 @@ def urlopen(self, method, url, redirect=True, **kw): u = parse_url(url) conn = self.connection_from_host(u.host, port=u.port, scheme=u.scheme) - kw['assert_same_host'] = False - kw['redirect'] = False + kw["assert_same_host"] = False + kw["redirect"] = False - if 'headers' not in kw: - kw['headers'] = self.headers.copy() + if "headers" not in kw: + kw["headers"] = self.headers.copy() if self.proxy is not None and u.scheme == "http": response = conn.urlopen(method, url, **kw) @@ -334,21 +338,22 @@ def urlopen(self, method, url, redirect=True, **kw): # RFC 7231, Section 6.4.4 if response.status == 303: - method = 'GET' + method = "GET" - retries = kw.get('retries') + retries = kw.get("retries") if not isinstance(retries, Retry): retries = Retry.from_int(retries, redirect=redirect) # Strip headers marked as unsafe to forward to the redirected location. # Check remove_headers_on_redirect to avoid a potential network call within # conn.is_same_host() which may use socket.gethostbyname() in the future. - if (retries.remove_headers_on_redirect - and not conn.is_same_host(redirect_location)): - headers = list(six.iterkeys(kw['headers'])) + if retries.remove_headers_on_redirect and not conn.is_same_host( + redirect_location + ): + headers = list(six.iterkeys(kw["headers"])) for header in headers: if header.lower() in retries.remove_headers_on_redirect: - kw['headers'].pop(header, None) + kw["headers"].pop(header, None) try: retries = retries.increment(method, url, response=response, _pool=conn) @@ -357,8 +362,8 @@ def urlopen(self, method, url, redirect=True, **kw): raise return response - kw['retries'] = retries - kw['redirect'] = redirect + kw["retries"] = retries + kw["redirect"] = redirect log.info("Redirecting %s -> %s", url, redirect_location) return self.urlopen(method, redirect_location, **kw) @@ -391,12 +396,21 @@ class ProxyManager(PoolManager): """ - def __init__(self, proxy_url, num_pools=10, headers=None, - proxy_headers=None, **connection_pool_kw): + def __init__( + self, + proxy_url, + num_pools=10, + headers=None, + proxy_headers=None, + **connection_pool_kw + ): if isinstance(proxy_url, HTTPConnectionPool): - proxy_url = '%s://%s:%i' % (proxy_url.scheme, proxy_url.host, - proxy_url.port) + proxy_url = "%s://%s:%i" % ( + proxy_url.scheme, + proxy_url.host, + proxy_url.port, + ) proxy = parse_url(proxy_url) if not proxy.port: port = port_by_scheme.get(proxy.scheme, 80) @@ -408,30 +422,31 @@ def __init__(self, proxy_url, num_pools=10, headers=None, self.proxy = proxy self.proxy_headers = proxy_headers or {} - connection_pool_kw['_proxy'] = self.proxy - connection_pool_kw['_proxy_headers'] = self.proxy_headers + connection_pool_kw["_proxy"] = self.proxy + connection_pool_kw["_proxy_headers"] = self.proxy_headers - super(ProxyManager, self).__init__( - num_pools, headers, **connection_pool_kw) + super(ProxyManager, self).__init__(num_pools, headers, **connection_pool_kw) - def connection_from_host(self, host, port=None, scheme='http', pool_kwargs=None): + def connection_from_host(self, host, port=None, scheme="http", pool_kwargs=None): if scheme == "https": return super(ProxyManager, self).connection_from_host( - host, port, scheme, pool_kwargs=pool_kwargs) + host, port, scheme, pool_kwargs=pool_kwargs + ) return super(ProxyManager, self).connection_from_host( - self.proxy.host, self.proxy.port, self.proxy.scheme, pool_kwargs=pool_kwargs) + self.proxy.host, self.proxy.port, self.proxy.scheme, pool_kwargs=pool_kwargs + ) def _set_proxy_headers(self, url, headers=None): """ Sets headers needed by proxies: specifically, the Accept and Host headers. Only sets headers not provided by the user. """ - headers_ = {'Accept': '*/*'} + headers_ = {"Accept": "*/*"} netloc = parse_url(url).netloc if netloc: - headers_['Host'] = netloc + headers_["Host"] = netloc if headers: headers_.update(headers) @@ -445,8 +460,8 @@ def urlopen(self, method, url, redirect=True, **kw): # For proxied HTTPS requests, httplib sets the necessary headers # on the CONNECT to the proxy. For HTTP, we'll definitely # need to set 'Host' at the very least. - headers = kw.get('headers', self.headers) - kw['headers'] = self._set_proxy_headers(url, headers) + headers = kw.get("headers", self.headers) + kw["headers"] = self._set_proxy_headers(url, headers) return super(ProxyManager, self).urlopen(method, url, redirect=redirect, **kw) diff --git a/src/pip/_vendor/urllib3/request.py b/src/pip/_vendor/urllib3/request.py index 8f2f44bb211..55f160bbf10 100644 --- a/src/pip/_vendor/urllib3/request.py +++ b/src/pip/_vendor/urllib3/request.py @@ -4,7 +4,7 @@ from .packages.six.moves.urllib.parse import urlencode -__all__ = ['RequestMethods'] +__all__ = ["RequestMethods"] class RequestMethods(object): @@ -36,16 +36,25 @@ class RequestMethods(object): explicitly. """ - _encode_url_methods = {'DELETE', 'GET', 'HEAD', 'OPTIONS'} + _encode_url_methods = {"DELETE", "GET", "HEAD", "OPTIONS"} def __init__(self, headers=None): self.headers = headers or {} - def urlopen(self, method, url, body=None, headers=None, - encode_multipart=True, multipart_boundary=None, - **kw): # Abstract - raise NotImplementedError("Classes extending RequestMethods must implement " - "their own ``urlopen`` method.") + def urlopen( + self, + method, + url, + body=None, + headers=None, + encode_multipart=True, + multipart_boundary=None, + **kw + ): # Abstract + raise NotImplementedError( + "Classes extending RequestMethods must implement " + "their own ``urlopen`` method." + ) def request(self, method, url, fields=None, headers=None, **urlopen_kw): """ @@ -60,19 +69,18 @@ def request(self, method, url, fields=None, headers=None, **urlopen_kw): """ method = method.upper() - urlopen_kw['request_url'] = url + urlopen_kw["request_url"] = url if method in self._encode_url_methods: - return self.request_encode_url(method, url, fields=fields, - headers=headers, - **urlopen_kw) + return self.request_encode_url( + method, url, fields=fields, headers=headers, **urlopen_kw + ) else: - return self.request_encode_body(method, url, fields=fields, - headers=headers, - **urlopen_kw) + return self.request_encode_body( + method, url, fields=fields, headers=headers, **urlopen_kw + ) - def request_encode_url(self, method, url, fields=None, headers=None, - **urlopen_kw): + def request_encode_url(self, method, url, fields=None, headers=None, **urlopen_kw): """ Make a request using :meth:`urlopen` with the ``fields`` encoded in the url. This is useful for request methods like GET, HEAD, DELETE, etc. @@ -80,17 +88,24 @@ def request_encode_url(self, method, url, fields=None, headers=None, if headers is None: headers = self.headers - extra_kw = {'headers': headers} + extra_kw = {"headers": headers} extra_kw.update(urlopen_kw) if fields: - url += '?' + urlencode(fields) + url += "?" + urlencode(fields) return self.urlopen(method, url, **extra_kw) - def request_encode_body(self, method, url, fields=None, headers=None, - encode_multipart=True, multipart_boundary=None, - **urlopen_kw): + def request_encode_body( + self, + method, + url, + fields=None, + headers=None, + encode_multipart=True, + multipart_boundary=None, + **urlopen_kw + ): """ Make a request using :meth:`urlopen` with the ``fields`` encoded in the body. This is useful for request methods like POST, PUT, PATCH, etc. @@ -129,22 +144,28 @@ def request_encode_body(self, method, url, fields=None, headers=None, if headers is None: headers = self.headers - extra_kw = {'headers': {}} + extra_kw = {"headers": {}} if fields: - if 'body' in urlopen_kw: + if "body" in urlopen_kw: raise TypeError( - "request got values for both 'fields' and 'body', can only specify one.") + "request got values for both 'fields' and 'body', can only specify one." + ) if encode_multipart: - body, content_type = encode_multipart_formdata(fields, boundary=multipart_boundary) + body, content_type = encode_multipart_formdata( + fields, boundary=multipart_boundary + ) else: - body, content_type = urlencode(fields), 'application/x-www-form-urlencoded' + body, content_type = ( + urlencode(fields), + "application/x-www-form-urlencoded", + ) - extra_kw['body'] = body - extra_kw['headers'] = {'Content-Type': content_type} + extra_kw["body"] = body + extra_kw["headers"] = {"Content-Type": content_type} - extra_kw['headers'].update(headers) + extra_kw["headers"].update(headers) extra_kw.update(urlopen_kw) return self.urlopen(method, url, **extra_kw) diff --git a/src/pip/_vendor/urllib3/response.py b/src/pip/_vendor/urllib3/response.py index 4f857932c54..adc321e713b 100644 --- a/src/pip/_vendor/urllib3/response.py +++ b/src/pip/_vendor/urllib3/response.py @@ -13,8 +13,13 @@ from ._collections import HTTPHeaderDict from .exceptions import ( - BodyNotHttplibCompatible, ProtocolError, DecodeError, ReadTimeoutError, - ResponseNotChunked, IncompleteRead, InvalidHeader + BodyNotHttplibCompatible, + ProtocolError, + DecodeError, + ReadTimeoutError, + ResponseNotChunked, + IncompleteRead, + InvalidHeader, ) from .packages.six import string_types as basestring, PY3 from .packages.six.moves import http_client as httplib @@ -25,10 +30,9 @@ class DeflateDecoder(object): - def __init__(self): self._first_try = True - self._data = b'' + self._data = b"" self._obj = zlib.decompressobj() def __getattr__(self, name): @@ -65,7 +69,6 @@ class GzipDecoderState(object): class GzipDecoder(object): - def __init__(self): self._obj = zlib.decompressobj(16 + zlib.MAX_WBITS) self._state = GzipDecoderState.FIRST_MEMBER @@ -96,6 +99,7 @@ def decompress(self, data): if brotli is not None: + class BrotliDecoder(object): # Supports both 'brotlipy' and 'Brotli' packages # since they share an import name. The top branches @@ -104,14 +108,14 @@ def __init__(self): self._obj = brotli.Decompressor() def decompress(self, data): - if hasattr(self._obj, 'decompress'): + if hasattr(self._obj, "decompress"): return self._obj.decompress(data) return self._obj.process(data) def flush(self): - if hasattr(self._obj, 'flush'): + if hasattr(self._obj, "flush"): return self._obj.flush() - return b'' + return b"" class MultiDecoder(object): @@ -124,7 +128,7 @@ class MultiDecoder(object): """ def __init__(self, modes): - self._decoders = [_get_decoder(m.strip()) for m in modes.split(',')] + self._decoders = [_get_decoder(m.strip()) for m in modes.split(",")] def flush(self): return self._decoders[0].flush() @@ -136,13 +140,13 @@ def decompress(self, data): def _get_decoder(mode): - if ',' in mode: + if "," in mode: return MultiDecoder(mode) - if mode == 'gzip': + if mode == "gzip": return GzipDecoder() - if brotli is not None and mode == 'br': + if brotli is not None and mode == "br": return BrotliDecoder() return DeflateDecoder() @@ -181,16 +185,31 @@ class is also compatible with the Python standard library's :mod:`io` value of Content-Length header, if present. Otherwise, raise error. """ - CONTENT_DECODERS = ['gzip', 'deflate'] + CONTENT_DECODERS = ["gzip", "deflate"] if brotli is not None: - CONTENT_DECODERS += ['br'] + CONTENT_DECODERS += ["br"] REDIRECT_STATUSES = [301, 302, 303, 307, 308] - def __init__(self, body='', headers=None, status=0, version=0, reason=None, - strict=0, preload_content=True, decode_content=True, - original_response=None, pool=None, connection=None, msg=None, - retries=None, enforce_content_length=False, - request_method=None, request_url=None): + def __init__( + self, + body="", + headers=None, + status=0, + version=0, + reason=None, + strict=0, + preload_content=True, + decode_content=True, + original_response=None, + pool=None, + connection=None, + msg=None, + retries=None, + enforce_content_length=False, + request_method=None, + request_url=None, + auto_close=True, + ): if isinstance(headers, HTTPHeaderDict): self.headers = headers @@ -203,6 +222,7 @@ def __init__(self, body='', headers=None, status=0, version=0, reason=None, self.decode_content = decode_content self.retries = retries self.enforce_content_length = enforce_content_length + self.auto_close = auto_close self._decoder = None self._body = None @@ -218,13 +238,13 @@ def __init__(self, body='', headers=None, status=0, version=0, reason=None, self._pool = pool self._connection = connection - if hasattr(body, 'read'): + if hasattr(body, "read"): self._fp = body # Are we using the chunked-style of transfer encoding? self.chunked = False self.chunk_left = None - tr_enc = self.headers.get('transfer-encoding', '').lower() + tr_enc = self.headers.get("transfer-encoding", "").lower() # Don't incur the penalty of creating a list and then discarding it encodings = (enc.strip() for enc in tr_enc.split(",")) if "chunked" in encodings: @@ -246,7 +266,7 @@ def get_redirect_location(self): location. ``False`` if not a redirect status code. """ if self.status in self.REDIRECT_STATUSES: - return self.headers.get('location') + return self.headers.get("location") return False @@ -285,18 +305,20 @@ def _init_length(self, request_method): """ Set initial length value for Response content if available. """ - length = self.headers.get('content-length') + length = self.headers.get("content-length") if length is not None: if self.chunked: # This Response will fail with an IncompleteRead if it can't be # received as chunked. This method falls back to attempt reading # the response before raising an exception. - log.warning("Received response with both Content-Length and " - "Transfer-Encoding set. This is expressly forbidden " - "by RFC 7230 sec 3.3.2. Ignoring Content-Length and " - "attempting to process response as Transfer-Encoding: " - "chunked.") + log.warning( + "Received response with both Content-Length and " + "Transfer-Encoding set. This is expressly forbidden " + "by RFC 7230 sec 3.3.2. Ignoring Content-Length and " + "attempting to process response as Transfer-Encoding: " + "chunked." + ) return None try: @@ -305,10 +327,12 @@ def _init_length(self, request_method): # (e.g. Content-Length: 42, 42). This line ensures the values # are all valid ints and that as long as the `set` length is 1, # all values are the same. Otherwise, the header is invalid. - lengths = set([int(val) for val in length.split(',')]) + lengths = set([int(val) for val in length.split(",")]) if len(lengths) > 1: - raise InvalidHeader("Content-Length contained multiple " - "unmatching values (%s)" % length) + raise InvalidHeader( + "Content-Length contained multiple " + "unmatching values (%s)" % length + ) length = lengths.pop() except ValueError: length = None @@ -324,7 +348,7 @@ def _init_length(self, request_method): status = 0 # Check for responses that shouldn't include a body - if status in (204, 304) or 100 <= status < 200 or request_method == 'HEAD': + if status in (204, 304) or 100 <= status < 200 or request_method == "HEAD": length = 0 return length @@ -335,14 +359,16 @@ def _init_decoder(self): """ # Note: content-encoding value should be case-insensitive, per RFC 7230 # Section 3.2 - content_encoding = self.headers.get('content-encoding', '').lower() + content_encoding = self.headers.get("content-encoding", "").lower() if self._decoder is None: if content_encoding in self.CONTENT_DECODERS: self._decoder = _get_decoder(content_encoding) - elif ',' in content_encoding: + elif "," in content_encoding: encodings = [ - e.strip() for e in content_encoding.split(',') - if e.strip() in self.CONTENT_DECODERS] + e.strip() + for e in content_encoding.split(",") + if e.strip() in self.CONTENT_DECODERS + ] if len(encodings): self._decoder = _get_decoder(content_encoding) @@ -361,10 +387,12 @@ def _decode(self, data, decode_content, flush_decoder): if self._decoder: data = self._decoder.decompress(data) except self.DECODER_ERROR_CLASSES as e: - content_encoding = self.headers.get('content-encoding', '').lower() + content_encoding = self.headers.get("content-encoding", "").lower() raise DecodeError( "Received response with content-encoding: %s, but " - "failed to decode it." % content_encoding, e) + "failed to decode it." % content_encoding, + e, + ) if flush_decoder: data += self._flush_decoder() @@ -376,10 +404,10 @@ def _flush_decoder(self): being used. """ if self._decoder: - buf = self._decoder.decompress(b'') + buf = self._decoder.decompress(b"") return buf + self._decoder.flush() - return b'' + return b"" @contextmanager def _error_catcher(self): @@ -399,20 +427,20 @@ def _error_catcher(self): except SocketTimeout: # FIXME: Ideally we'd like to include the url in the ReadTimeoutError but # there is yet no clean way to get at it from this context. - raise ReadTimeoutError(self._pool, None, 'Read timed out.') + raise ReadTimeoutError(self._pool, None, "Read timed out.") except BaseSSLError as e: # FIXME: Is there a better way to differentiate between SSLErrors? - if 'read operation timed out' not in str(e): # Defensive: + if "read operation timed out" not in str(e): # Defensive: # This shouldn't happen but just in case we're missing an edge # case, let's avoid swallowing SSL errors. raise - raise ReadTimeoutError(self._pool, None, 'Read timed out.') + raise ReadTimeoutError(self._pool, None, "Read timed out.") except (HTTPException, SocketError) as e: # This includes IncompleteRead. - raise ProtocolError('Connection broken: %r' % e, e) + raise ProtocolError("Connection broken: %r" % e, e) # If no exception is thrown, we should avoid cleaning up # unnecessarily. @@ -467,17 +495,19 @@ def read(self, amt=None, decode_content=None, cache_content=False): return flush_decoder = False - data = None + fp_closed = getattr(self._fp, "closed", False) with self._error_catcher(): if amt is None: # cStringIO doesn't like amt=None - data = self._fp.read() + data = self._fp.read() if not fp_closed else b"" flush_decoder = True else: cache_content = False - data = self._fp.read(amt) - if amt != 0 and not data: # Platform-specific: Buggy versions of Python. + data = self._fp.read(amt) if not fp_closed else b"" + if ( + amt != 0 and not data + ): # Platform-specific: Buggy versions of Python. # Close the connection when no data is returned # # This is redundant to what httplib/http.client _should_ @@ -487,7 +517,10 @@ def read(self, amt=None, decode_content=None, cache_content=False): # no harm in redundantly calling close. self._fp.close() flush_decoder = True - if self.enforce_content_length and self.length_remaining not in (0, None): + if self.enforce_content_length and self.length_remaining not in ( + 0, + None, + ): # This is an edge case that httplib failed to cover due # to concerns of backward compatibility. We're # addressing it here to make sure IncompleteRead is @@ -507,7 +540,7 @@ def read(self, amt=None, decode_content=None, cache_content=False): return data - def stream(self, amt=2**16, decode_content=None): + def stream(self, amt=2 ** 16, decode_content=None): """ A generator wrapper for the read() method. A call will block until ``amt`` bytes have been read from the connection or until the @@ -552,15 +585,17 @@ def from_httplib(ResponseCls, r, **response_kw): headers = HTTPHeaderDict.from_httplib(headers) # HTTPResponse objects in Python 3 don't have a .strict attribute - strict = getattr(r, 'strict', 0) - resp = ResponseCls(body=r, - headers=headers, - status=r.status, - version=r.version, - reason=r.reason, - strict=strict, - original_response=r, - **response_kw) + strict = getattr(r, "strict", 0) + resp = ResponseCls( + body=r, + headers=headers, + status=r.status, + version=r.version, + reason=r.reason, + strict=strict, + original_response=r, + **response_kw + ) return resp # Backwards-compatibility methods for httplib.HTTPResponse @@ -582,13 +617,18 @@ def close(self): if self._connection: self._connection.close() + if not self.auto_close: + io.IOBase.close(self) + @property def closed(self): - if self._fp is None: + if not self.auto_close: + return io.IOBase.closed.__get__(self) + elif self._fp is None: return True - elif hasattr(self._fp, 'isclosed'): + elif hasattr(self._fp, "isclosed"): return self._fp.isclosed() - elif hasattr(self._fp, 'closed'): + elif hasattr(self._fp, "closed"): return self._fp.closed else: return True @@ -599,11 +639,17 @@ def fileno(self): elif hasattr(self._fp, "fileno"): return self._fp.fileno() else: - raise IOError("The file-like object this HTTPResponse is wrapped " - "around has no file descriptor") + raise IOError( + "The file-like object this HTTPResponse is wrapped " + "around has no file descriptor" + ) def flush(self): - if self._fp is not None and hasattr(self._fp, 'flush'): + if ( + self._fp is not None + and hasattr(self._fp, "flush") + and not getattr(self._fp, "closed", False) + ): return self._fp.flush() def readable(self): @@ -616,7 +662,7 @@ def readinto(self, b): if len(temp) == 0: return 0 else: - b[:len(temp)] = temp + b[: len(temp)] = temp return len(temp) def supports_chunked_reads(self): @@ -626,7 +672,7 @@ def supports_chunked_reads(self): attribute. If it is present we assume it returns raw chunks as processed by read_chunked(). """ - return hasattr(self._fp, 'fp') + return hasattr(self._fp, "fp") def _update_chunk_length(self): # First, we'll figure out length of a chunk and then @@ -634,7 +680,7 @@ def _update_chunk_length(self): if self.chunk_left is not None: return line = self._fp.fp.readline() - line = line.split(b';', 1)[0] + line = line.split(b";", 1)[0] try: self.chunk_left = int(line, 16) except ValueError: @@ -683,11 +729,13 @@ def read_chunked(self, amt=None, decode_content=None): if not self.chunked: raise ResponseNotChunked( "Response is not chunked. " - "Header 'transfer-encoding: chunked' is missing.") + "Header 'transfer-encoding: chunked' is missing." + ) if not self.supports_chunked_reads(): raise BodyNotHttplibCompatible( "Body should be httplib.HTTPResponse like. " - "It should have have an fp attribute which returns raw chunks.") + "It should have have an fp attribute which returns raw chunks." + ) with self._error_catcher(): # Don't bother reading the body of a HEAD request. @@ -705,8 +753,9 @@ def read_chunked(self, amt=None, decode_content=None): if self.chunk_left == 0: break chunk = self._handle_chunk(amt) - decoded = self._decode(chunk, decode_content=decode_content, - flush_decoder=False) + decoded = self._decode( + chunk, decode_content=decode_content, flush_decoder=False + ) if decoded: yield decoded @@ -724,7 +773,7 @@ def read_chunked(self, amt=None, decode_content=None): if not line: # Some sites may not end with '\r\n'. break - if line == b'\r\n': + if line == b"\r\n": break # We read everything; close the "file". diff --git a/src/pip/_vendor/urllib3/util/__init__.py b/src/pip/_vendor/urllib3/util/__init__.py index 2914bb468be..a96c73a9d85 100644 --- a/src/pip/_vendor/urllib3/util/__init__.py +++ b/src/pip/_vendor/urllib3/util/__init__.py @@ -1,4 +1,5 @@ from __future__ import absolute_import + # For backwards compatibility, provide imports that used to be here. from .connection import is_connection_dropped from .request import make_headers @@ -14,43 +15,32 @@ ssl_wrap_socket, PROTOCOL_TLS, ) -from .timeout import ( - current_time, - Timeout, -) +from .timeout import current_time, Timeout from .retry import Retry -from .url import ( - get_host, - parse_url, - split_first, - Url, -) -from .wait import ( - wait_for_read, - wait_for_write -) +from .url import get_host, parse_url, split_first, Url +from .wait import wait_for_read, wait_for_write __all__ = ( - 'HAS_SNI', - 'IS_PYOPENSSL', - 'IS_SECURETRANSPORT', - 'SSLContext', - 'PROTOCOL_TLS', - 'Retry', - 'Timeout', - 'Url', - 'assert_fingerprint', - 'current_time', - 'is_connection_dropped', - 'is_fp_closed', - 'get_host', - 'parse_url', - 'make_headers', - 'resolve_cert_reqs', - 'resolve_ssl_version', - 'split_first', - 'ssl_wrap_socket', - 'wait_for_read', - 'wait_for_write' + "HAS_SNI", + "IS_PYOPENSSL", + "IS_SECURETRANSPORT", + "SSLContext", + "PROTOCOL_TLS", + "Retry", + "Timeout", + "Url", + "assert_fingerprint", + "current_time", + "is_connection_dropped", + "is_fp_closed", + "get_host", + "parse_url", + "make_headers", + "resolve_cert_reqs", + "resolve_ssl_version", + "split_first", + "ssl_wrap_socket", + "wait_for_read", + "wait_for_write", ) diff --git a/src/pip/_vendor/urllib3/util/connection.py b/src/pip/_vendor/urllib3/util/connection.py index 5ad70b2f1ca..0e1112628e5 100644 --- a/src/pip/_vendor/urllib3/util/connection.py +++ b/src/pip/_vendor/urllib3/util/connection.py @@ -14,7 +14,7 @@ def is_connection_dropped(conn): # Platform-specific Note: For platforms like AppEngine, this will always return ``False`` to let the platform handle connection recycling transparently for us. """ - sock = getattr(conn, 'sock', False) + sock = getattr(conn, "sock", False) if sock is False: # Platform-specific: AppEngine return False if sock is None: # Connection already closed (such as by httplib). @@ -30,8 +30,12 @@ def is_connection_dropped(conn): # Platform-specific # library test suite. Added to its signature is only `socket_options`. # One additional modification is that we avoid binding to IPv6 servers # discovered in DNS if the system doesn't have IPv6 functionality. -def create_connection(address, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, - source_address=None, socket_options=None): +def create_connection( + address, + timeout=socket._GLOBAL_DEFAULT_TIMEOUT, + source_address=None, + socket_options=None, +): """Connect to *address* and return the socket object. Convenience function. Connect to *address* (a 2-tuple ``(host, @@ -45,8 +49,8 @@ def create_connection(address, timeout=socket._GLOBAL_DEFAULT_TIMEOUT, """ host, port = address - if host.startswith('['): - host = host.strip('[]') + if host.startswith("["): + host = host.strip("[]") err = None # Using the value from allowed_gai_family() in the context of getaddrinfo lets @@ -131,4 +135,4 @@ def _has_ipv6(host): return has_ipv6 -HAS_IPV6 = _has_ipv6('::1') +HAS_IPV6 = _has_ipv6("::1") diff --git a/src/pip/_vendor/urllib3/util/request.py b/src/pip/_vendor/urllib3/util/request.py index 280b8530c60..262a6d61854 100644 --- a/src/pip/_vendor/urllib3/util/request.py +++ b/src/pip/_vendor/urllib3/util/request.py @@ -4,19 +4,25 @@ from ..packages.six import b, integer_types from ..exceptions import UnrewindableBodyError -ACCEPT_ENCODING = 'gzip,deflate' +ACCEPT_ENCODING = "gzip,deflate" try: import brotli as _unused_module_brotli # noqa: F401 except ImportError: pass else: - ACCEPT_ENCODING += ',br' + ACCEPT_ENCODING += ",br" _FAILEDTELL = object() -def make_headers(keep_alive=None, accept_encoding=None, user_agent=None, - basic_auth=None, proxy_basic_auth=None, disable_cache=None): +def make_headers( + keep_alive=None, + accept_encoding=None, + user_agent=None, + basic_auth=None, + proxy_basic_auth=None, + disable_cache=None, +): """ Shortcuts for generating request headers. @@ -56,27 +62,27 @@ def make_headers(keep_alive=None, accept_encoding=None, user_agent=None, if isinstance(accept_encoding, str): pass elif isinstance(accept_encoding, list): - accept_encoding = ','.join(accept_encoding) + accept_encoding = ",".join(accept_encoding) else: accept_encoding = ACCEPT_ENCODING - headers['accept-encoding'] = accept_encoding + headers["accept-encoding"] = accept_encoding if user_agent: - headers['user-agent'] = user_agent + headers["user-agent"] = user_agent if keep_alive: - headers['connection'] = 'keep-alive' + headers["connection"] = "keep-alive" if basic_auth: - headers['authorization'] = 'Basic ' + \ - b64encode(b(basic_auth)).decode('utf-8') + headers["authorization"] = "Basic " + b64encode(b(basic_auth)).decode("utf-8") if proxy_basic_auth: - headers['proxy-authorization'] = 'Basic ' + \ - b64encode(b(proxy_basic_auth)).decode('utf-8') + headers["proxy-authorization"] = "Basic " + b64encode( + b(proxy_basic_auth) + ).decode("utf-8") if disable_cache: - headers['cache-control'] = 'no-cache' + headers["cache-control"] = "no-cache" return headers @@ -88,7 +94,7 @@ def set_file_position(body, pos): """ if pos is not None: rewind_body(body, pos) - elif getattr(body, 'tell', None) is not None: + elif getattr(body, "tell", None) is not None: try: pos = body.tell() except (IOError, OSError): @@ -110,16 +116,20 @@ def rewind_body(body, body_pos): :param int pos: Position to seek to in file. """ - body_seek = getattr(body, 'seek', None) + body_seek = getattr(body, "seek", None) if body_seek is not None and isinstance(body_pos, integer_types): try: body_seek(body_pos) except (IOError, OSError): - raise UnrewindableBodyError("An error occurred when rewinding request " - "body for redirect/retry.") + raise UnrewindableBodyError( + "An error occurred when rewinding request " "body for redirect/retry." + ) elif body_pos is _FAILEDTELL: - raise UnrewindableBodyError("Unable to record file position for rewinding " - "request body during a redirect/retry.") + raise UnrewindableBodyError( + "Unable to record file position for rewinding " + "request body during a redirect/retry." + ) else: - raise ValueError("body_pos must be of type integer, " - "instead it was %s." % type(body_pos)) + raise ValueError( + "body_pos must be of type integer, " "instead it was %s." % type(body_pos) + ) diff --git a/src/pip/_vendor/urllib3/util/response.py b/src/pip/_vendor/urllib3/util/response.py index 3d5486485a6..715868dd100 100644 --- a/src/pip/_vendor/urllib3/util/response.py +++ b/src/pip/_vendor/urllib3/util/response.py @@ -52,11 +52,10 @@ def assert_header_parsing(headers): # This will fail silently if we pass in the wrong kind of parameter. # To make debugging easier add an explicit check. if not isinstance(headers, httplib.HTTPMessage): - raise TypeError('expected httplib.Message, got {0}.'.format( - type(headers))) + raise TypeError("expected httplib.Message, got {0}.".format(type(headers))) - defects = getattr(headers, 'defects', None) - get_payload = getattr(headers, 'get_payload', None) + defects = getattr(headers, "defects", None) + get_payload = getattr(headers, "get_payload", None) unparsed_data = None if get_payload: @@ -84,4 +83,4 @@ def is_response_to_head(response): method = response._method if isinstance(method, int): # Platform-specific: Appengine return method == 3 - return method.upper() == 'HEAD' + return method.upper() == "HEAD" diff --git a/src/pip/_vendor/urllib3/util/retry.py b/src/pip/_vendor/urllib3/util/retry.py index 02429ee8e48..5a049fe65e0 100644 --- a/src/pip/_vendor/urllib3/util/retry.py +++ b/src/pip/_vendor/urllib3/util/retry.py @@ -21,8 +21,9 @@ # Data structure for representing the metadata of requests that result in a retry. -RequestHistory = namedtuple('RequestHistory', ["method", "url", "error", - "status", "redirect_location"]) +RequestHistory = namedtuple( + "RequestHistory", ["method", "url", "error", "status", "redirect_location"] +) class Retry(object): @@ -146,21 +147,33 @@ class Retry(object): request. """ - DEFAULT_METHOD_WHITELIST = frozenset([ - 'HEAD', 'GET', 'PUT', 'DELETE', 'OPTIONS', 'TRACE']) + DEFAULT_METHOD_WHITELIST = frozenset( + ["HEAD", "GET", "PUT", "DELETE", "OPTIONS", "TRACE"] + ) RETRY_AFTER_STATUS_CODES = frozenset([413, 429, 503]) - DEFAULT_REDIRECT_HEADERS_BLACKLIST = frozenset(['Authorization']) + DEFAULT_REDIRECT_HEADERS_BLACKLIST = frozenset(["Authorization"]) #: Maximum backoff time. BACKOFF_MAX = 120 - def __init__(self, total=10, connect=None, read=None, redirect=None, status=None, - method_whitelist=DEFAULT_METHOD_WHITELIST, status_forcelist=None, - backoff_factor=0, raise_on_redirect=True, raise_on_status=True, - history=None, respect_retry_after_header=True, - remove_headers_on_redirect=DEFAULT_REDIRECT_HEADERS_BLACKLIST): + def __init__( + self, + total=10, + connect=None, + read=None, + redirect=None, + status=None, + method_whitelist=DEFAULT_METHOD_WHITELIST, + status_forcelist=None, + backoff_factor=0, + raise_on_redirect=True, + raise_on_status=True, + history=None, + respect_retry_after_header=True, + remove_headers_on_redirect=DEFAULT_REDIRECT_HEADERS_BLACKLIST, + ): self.total = total self.connect = connect @@ -179,20 +192,25 @@ def __init__(self, total=10, connect=None, read=None, redirect=None, status=None self.raise_on_status = raise_on_status self.history = history or tuple() self.respect_retry_after_header = respect_retry_after_header - self.remove_headers_on_redirect = frozenset([ - h.lower() for h in remove_headers_on_redirect]) + self.remove_headers_on_redirect = frozenset( + [h.lower() for h in remove_headers_on_redirect] + ) def new(self, **kw): params = dict( total=self.total, - connect=self.connect, read=self.read, redirect=self.redirect, status=self.status, + connect=self.connect, + read=self.read, + redirect=self.redirect, + status=self.status, method_whitelist=self.method_whitelist, status_forcelist=self.status_forcelist, backoff_factor=self.backoff_factor, raise_on_redirect=self.raise_on_redirect, raise_on_status=self.raise_on_status, history=self.history, - remove_headers_on_redirect=self.remove_headers_on_redirect + remove_headers_on_redirect=self.remove_headers_on_redirect, + respect_retry_after_header=self.respect_retry_after_header, ) params.update(kw) return type(self)(**params) @@ -217,8 +235,11 @@ def get_backoff_time(self): :rtype: float """ # We want to consider only the last consecutive errors sequence (Ignore redirects). - consecutive_errors_len = len(list(takewhile(lambda x: x.redirect_location is None, - reversed(self.history)))) + consecutive_errors_len = len( + list( + takewhile(lambda x: x.redirect_location is None, reversed(self.history)) + ) + ) if consecutive_errors_len <= 1: return 0 @@ -274,7 +295,7 @@ def sleep(self, response=None): this method will return immediately. """ - if response: + if self.respect_retry_after_header and response: slept = self.sleep_for_retry(response) if slept: return @@ -315,8 +336,12 @@ def is_retry(self, method, status_code, has_retry_after=False): if self.status_forcelist and status_code in self.status_forcelist: return True - return (self.total and self.respect_retry_after_header and - has_retry_after and (status_code in self.RETRY_AFTER_STATUS_CODES)) + return ( + self.total + and self.respect_retry_after_header + and has_retry_after + and (status_code in self.RETRY_AFTER_STATUS_CODES) + ) def is_exhausted(self): """ Are we out of retries? """ @@ -327,8 +352,15 @@ def is_exhausted(self): return min(retry_counts) < 0 - def increment(self, method=None, url=None, response=None, error=None, - _pool=None, _stacktrace=None): + def increment( + self, + method=None, + url=None, + response=None, + error=None, + _pool=None, + _stacktrace=None, + ): """ Return a new Retry object with incremented retry counters. :param response: A response object, or None, if the server did not @@ -351,7 +383,7 @@ def increment(self, method=None, url=None, response=None, error=None, read = self.read redirect = self.redirect status_count = self.status - cause = 'unknown' + cause = "unknown" status = None redirect_location = None @@ -373,7 +405,7 @@ def increment(self, method=None, url=None, response=None, error=None, # Redirect retry? if redirect is not None: redirect -= 1 - cause = 'too many redirects' + cause = "too many redirects" redirect_location = response.get_redirect_location() status = response.status @@ -384,16 +416,21 @@ def increment(self, method=None, url=None, response=None, error=None, if response and response.status: if status_count is not None: status_count -= 1 - cause = ResponseError.SPECIFIC_ERROR.format( - status_code=response.status) + cause = ResponseError.SPECIFIC_ERROR.format(status_code=response.status) status = response.status - history = self.history + (RequestHistory(method, url, error, status, redirect_location),) + history = self.history + ( + RequestHistory(method, url, error, status, redirect_location), + ) new_retry = self.new( total=total, - connect=connect, read=read, redirect=redirect, status=status_count, - history=history) + connect=connect, + read=read, + redirect=redirect, + status=status_count, + history=history, + ) if new_retry.is_exhausted(): raise MaxRetryError(_pool, url, error or ResponseError(cause)) @@ -403,9 +440,10 @@ def increment(self, method=None, url=None, response=None, error=None, return new_retry def __repr__(self): - return ('{cls.__name__}(total={self.total}, connect={self.connect}, ' - 'read={self.read}, redirect={self.redirect}, status={self.status})').format( - cls=type(self), self=self) + return ( + "{cls.__name__}(total={self.total}, connect={self.connect}, " + "read={self.read}, redirect={self.redirect}, status={self.status})" + ).format(cls=type(self), self=self) # For backwards compatibility (equivalent to pre-v1.9): diff --git a/src/pip/_vendor/urllib3/util/ssl_.py b/src/pip/_vendor/urllib3/util/ssl_.py index fbdef65d76a..e5739fb6757 100644 --- a/src/pip/_vendor/urllib3/util/ssl_.py +++ b/src/pip/_vendor/urllib3/util/ssl_.py @@ -2,14 +2,14 @@ import errno import warnings import hmac -import re +import sys from binascii import hexlify, unhexlify from hashlib import md5, sha1, sha256 +from .url import IPV4_RE, BRACELESS_IPV6_ADDRZ_RE from ..exceptions import SSLError, InsecurePlatformWarning, SNIMissingWarning from ..packages import six -from ..packages.rfc3986 import abnf_regexp SSLContext = None @@ -18,11 +18,7 @@ IS_SECURETRANSPORT = False # Maps the length of a digest to a possible hash function producing this digest -HASHFUNC_MAP = { - 32: md5, - 40: sha1, - 64: sha256, -} +HASHFUNC_MAP = {32: md5, 40: sha1, 64: sha256} def _const_compare_digest_backport(a, b): @@ -38,18 +34,7 @@ def _const_compare_digest_backport(a, b): return result == 0 -_const_compare_digest = getattr(hmac, 'compare_digest', - _const_compare_digest_backport) - -# Borrow rfc3986's regular expressions for IPv4 -# and IPv6 addresses for use in is_ipaddress() -_IP_ADDRESS_REGEX = re.compile( - r'^(?:%s|%s|%s)$' % ( - abnf_regexp.IPv4_RE, - abnf_regexp.IPv6_RE, - abnf_regexp.IPv6_ADDRZ_RFC4007_RE - ) -) +_const_compare_digest = getattr(hmac, "compare_digest", _const_compare_digest_backport) try: # Test for SSL features import ssl @@ -60,10 +45,12 @@ def _const_compare_digest_backport(a, b): try: # Platform-specific: Python 3.6 from ssl import PROTOCOL_TLS + PROTOCOL_SSLv23 = PROTOCOL_TLS except ImportError: try: from ssl import PROTOCOL_SSLv23 as PROTOCOL_TLS + PROTOCOL_SSLv23 = PROTOCOL_TLS except ImportError: PROTOCOL_SSLv23 = PROTOCOL_TLS = 2 @@ -93,26 +80,29 @@ def _const_compare_digest_backport(a, b): # insecure ciphers for security reasons. # - NOTE: TLS 1.3 cipher suites are managed through a different interface # not exposed by CPython (yet!) and are enabled by default if they're available. -DEFAULT_CIPHERS = ':'.join([ - 'ECDHE+AESGCM', - 'ECDHE+CHACHA20', - 'DHE+AESGCM', - 'DHE+CHACHA20', - 'ECDH+AESGCM', - 'DH+AESGCM', - 'ECDH+AES', - 'DH+AES', - 'RSA+AESGCM', - 'RSA+AES', - '!aNULL', - '!eNULL', - '!MD5', - '!DSS', -]) +DEFAULT_CIPHERS = ":".join( + [ + "ECDHE+AESGCM", + "ECDHE+CHACHA20", + "DHE+AESGCM", + "DHE+CHACHA20", + "ECDH+AESGCM", + "DH+AESGCM", + "ECDH+AES", + "DH+AES", + "RSA+AESGCM", + "RSA+AES", + "!aNULL", + "!eNULL", + "!MD5", + "!DSS", + ] +) try: from ssl import SSLContext # Modern SSL? except ImportError: + class SSLContext(object): # Platform-specific: Python 2 def __init__(self, protocol_version): self.protocol = protocol_version @@ -140,21 +130,21 @@ def set_ciphers(self, cipher_suite): def wrap_socket(self, socket, server_hostname=None, server_side=False): warnings.warn( - 'A true SSLContext object is not available. This prevents ' - 'urllib3 from configuring SSL appropriately and may cause ' - 'certain SSL connections to fail. You can upgrade to a newer ' - 'version of Python to solve this. For more information, see ' - 'https://urllib3.readthedocs.io/en/latest/advanced-usage.html' - '#ssl-warnings', - InsecurePlatformWarning + "A true SSLContext object is not available. This prevents " + "urllib3 from configuring SSL appropriately and may cause " + "certain SSL connections to fail. You can upgrade to a newer " + "version of Python to solve this. For more information, see " + "https://urllib3.readthedocs.io/en/latest/advanced-usage.html" + "#ssl-warnings", + InsecurePlatformWarning, ) kwargs = { - 'keyfile': self.keyfile, - 'certfile': self.certfile, - 'ca_certs': self.ca_certs, - 'cert_reqs': self.verify_mode, - 'ssl_version': self.protocol, - 'server_side': server_side, + "keyfile": self.keyfile, + "certfile": self.certfile, + "ca_certs": self.ca_certs, + "cert_reqs": self.verify_mode, + "ssl_version": self.protocol, + "server_side": server_side, } return wrap_socket(socket, ciphers=self.ciphers, **kwargs) @@ -169,12 +159,11 @@ def assert_fingerprint(cert, fingerprint): Fingerprint as string of hexdigits, can be interspersed by colons. """ - fingerprint = fingerprint.replace(':', '').lower() + fingerprint = fingerprint.replace(":", "").lower() digest_length = len(fingerprint) hashfunc = HASHFUNC_MAP.get(digest_length) if not hashfunc: - raise SSLError( - 'Fingerprint of invalid length: {0}'.format(fingerprint)) + raise SSLError("Fingerprint of invalid length: {0}".format(fingerprint)) # We need encode() here for py32; works on py2 and p33. fingerprint_bytes = unhexlify(fingerprint.encode()) @@ -182,8 +171,11 @@ def assert_fingerprint(cert, fingerprint): cert_digest = hashfunc(cert).digest() if not _const_compare_digest(cert_digest, fingerprint_bytes): - raise SSLError('Fingerprints did not match. Expected "{0}", got "{1}".' - .format(fingerprint, hexlify(cert_digest))) + raise SSLError( + 'Fingerprints did not match. Expected "{0}", got "{1}".'.format( + fingerprint, hexlify(cert_digest) + ) + ) def resolve_cert_reqs(candidate): @@ -203,7 +195,7 @@ def resolve_cert_reqs(candidate): if isinstance(candidate, str): res = getattr(ssl, candidate, None) if res is None: - res = getattr(ssl, 'CERT_' + candidate) + res = getattr(ssl, "CERT_" + candidate) return res return candidate @@ -219,14 +211,15 @@ def resolve_ssl_version(candidate): if isinstance(candidate, str): res = getattr(ssl, candidate, None) if res is None: - res = getattr(ssl, 'PROTOCOL_' + candidate) + res = getattr(ssl, "PROTOCOL_" + candidate) return res return candidate -def create_urllib3_context(ssl_version=None, cert_reqs=None, - options=None, ciphers=None): +def create_urllib3_context( + ssl_version=None, cert_reqs=None, options=None, ciphers=None +): """All arguments have the same meaning as ``ssl_wrap_socket``. By default, this function does a lot of the same work that @@ -279,18 +272,40 @@ def create_urllib3_context(ssl_version=None, cert_reqs=None, context.options |= options + # Enable post-handshake authentication for TLS 1.3, see GH #1634. PHA is + # necessary for conditional client cert authentication with TLS 1.3. + # The attribute is None for OpenSSL <= 1.1.0 or does not exist in older + # versions of Python. We only enable on Python 3.7.4+ or if certificate + # verification is enabled to work around Python issue #37428 + # See: https://bugs.python.org/issue37428 + if (cert_reqs == ssl.CERT_REQUIRED or sys.version_info >= (3, 7, 4)) and getattr( + context, "post_handshake_auth", None + ) is not None: + context.post_handshake_auth = True + context.verify_mode = cert_reqs - if getattr(context, 'check_hostname', None) is not None: # Platform-specific: Python 3.2 + if ( + getattr(context, "check_hostname", None) is not None + ): # Platform-specific: Python 3.2 # We do our own verification, including fingerprints and alternative # hostnames. So disable it here context.check_hostname = False return context -def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None, - ca_certs=None, server_hostname=None, - ssl_version=None, ciphers=None, ssl_context=None, - ca_cert_dir=None, key_password=None): +def ssl_wrap_socket( + sock, + keyfile=None, + certfile=None, + cert_reqs=None, + ca_certs=None, + server_hostname=None, + ssl_version=None, + ciphers=None, + ssl_context=None, + ca_cert_dir=None, + key_password=None, +): """ All arguments except for server_hostname, ssl_context, and ca_cert_dir have the same meaning as they do when using :func:`ssl.wrap_socket`. @@ -314,8 +329,7 @@ def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None, # Note: This branch of code and all the variables in it are no longer # used by urllib3 itself. We should consider deprecating and removing # this code. - context = create_urllib3_context(ssl_version, cert_reqs, - ciphers=ciphers) + context = create_urllib3_context(ssl_version, cert_reqs, ciphers=ciphers) if ca_certs or ca_cert_dir: try: @@ -329,7 +343,7 @@ def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None, raise SSLError(e) raise - elif ssl_context is None and hasattr(context, 'load_default_certs'): + elif ssl_context is None and hasattr(context, "load_default_certs"): # try to load OS default certs; works well on Windows (require Python3.4+) context.load_default_certs() @@ -349,20 +363,21 @@ def ssl_wrap_socket(sock, keyfile=None, certfile=None, cert_reqs=None, # extension should not be used according to RFC3546 Section 3.1 # We shouldn't warn the user if SNI isn't available but we would # not be using SNI anyways due to IP address for server_hostname. - if ((server_hostname is not None and not is_ipaddress(server_hostname)) - or IS_SECURETRANSPORT): + if ( + server_hostname is not None and not is_ipaddress(server_hostname) + ) or IS_SECURETRANSPORT: if HAS_SNI and server_hostname is not None: return context.wrap_socket(sock, server_hostname=server_hostname) warnings.warn( - 'An HTTPS request has been made, but the SNI (Server Name ' - 'Indication) extension to TLS is not available on this platform. ' - 'This may cause the server to present an incorrect TLS ' - 'certificate, which can cause validation failures. You can upgrade to ' - 'a newer version of Python to solve this. For more information, see ' - 'https://urllib3.readthedocs.io/en/latest/advanced-usage.html' - '#ssl-warnings', - SNIMissingWarning + "An HTTPS request has been made, but the SNI (Server Name " + "Indication) extension to TLS is not available on this platform. " + "This may cause the server to present an incorrect TLS " + "certificate, which can cause validation failures. You can upgrade to " + "a newer version of Python to solve this. For more information, see " + "https://urllib3.readthedocs.io/en/latest/advanced-usage.html" + "#ssl-warnings", + SNIMissingWarning, ) return context.wrap_socket(sock) @@ -375,18 +390,18 @@ def is_ipaddress(hostname): :param str hostname: Hostname to examine. :return: True if the hostname is an IP address, False otherwise. """ - if six.PY3 and isinstance(hostname, bytes): + if not six.PY2 and isinstance(hostname, bytes): # IDN A-label bytes are ASCII compatible. - hostname = hostname.decode('ascii') - return _IP_ADDRESS_REGEX.match(hostname) is not None + hostname = hostname.decode("ascii") + return bool(IPV4_RE.match(hostname) or BRACELESS_IPV6_ADDRZ_RE.match(hostname)) def _is_key_file_encrypted(key_file): """Detects if a key file is encrypted or not.""" - with open(key_file, 'r') as f: + with open(key_file, "r") as f: for line in f: # Look for Proc-Type: 4,ENCRYPTED - if 'ENCRYPTED' in line: + if "ENCRYPTED" in line: return True return False diff --git a/src/pip/_vendor/urllib3/util/timeout.py b/src/pip/_vendor/urllib3/util/timeout.py index a4d004a848c..c1dc1e97126 100644 --- a/src/pip/_vendor/urllib3/util/timeout.py +++ b/src/pip/_vendor/urllib3/util/timeout.py @@ -1,4 +1,5 @@ from __future__ import absolute_import + # The default socket timeout, used by httplib to indicate that no timeout was # specified by the user from socket import _GLOBAL_DEFAULT_TIMEOUT @@ -45,19 +46,20 @@ class Timeout(object): :type total: integer, float, or None :param connect: - The maximum amount of time to wait for a connection attempt to a server - to succeed. Omitting the parameter will default the connect timeout to - the system default, probably `the global default timeout in socket.py + The maximum amount of time (in seconds) to wait for a connection + attempt to a server to succeed. Omitting the parameter will default the + connect timeout to the system default, probably `the global default + timeout in socket.py <http://hg.python.org/cpython/file/603b4d593758/Lib/socket.py#l535>`_. None will set an infinite timeout for connection attempts. :type connect: integer, float, or None :param read: - The maximum amount of time to wait between consecutive - read operations for a response from the server. Omitting - the parameter will default the read timeout to the system - default, probably `the global default timeout in socket.py + The maximum amount of time (in seconds) to wait between consecutive + read operations for a response from the server. Omitting the parameter + will default the read timeout to the system default, probably `the + global default timeout in socket.py <http://hg.python.org/cpython/file/603b4d593758/Lib/socket.py#l535>`_. None will set an infinite timeout. @@ -91,14 +93,18 @@ class Timeout(object): DEFAULT_TIMEOUT = _GLOBAL_DEFAULT_TIMEOUT def __init__(self, total=None, connect=_Default, read=_Default): - self._connect = self._validate_timeout(connect, 'connect') - self._read = self._validate_timeout(read, 'read') - self.total = self._validate_timeout(total, 'total') + self._connect = self._validate_timeout(connect, "connect") + self._read = self._validate_timeout(read, "read") + self.total = self._validate_timeout(total, "total") self._start_connect = None def __str__(self): - return '%s(connect=%r, read=%r, total=%r)' % ( - type(self).__name__, self._connect, self._read, self.total) + return "%s(connect=%r, read=%r, total=%r)" % ( + type(self).__name__, + self._connect, + self._read, + self.total, + ) @classmethod def _validate_timeout(cls, value, name): @@ -118,23 +124,31 @@ def _validate_timeout(cls, value, name): return value if isinstance(value, bool): - raise ValueError("Timeout cannot be a boolean value. It must " - "be an int, float or None.") + raise ValueError( + "Timeout cannot be a boolean value. It must " + "be an int, float or None." + ) try: float(value) except (TypeError, ValueError): - raise ValueError("Timeout value %s was %s, but it must be an " - "int, float or None." % (name, value)) + raise ValueError( + "Timeout value %s was %s, but it must be an " + "int, float or None." % (name, value) + ) try: if value <= 0: - raise ValueError("Attempted to set %s timeout to %s, but the " - "timeout cannot be set to a value less " - "than or equal to 0." % (name, value)) + raise ValueError( + "Attempted to set %s timeout to %s, but the " + "timeout cannot be set to a value less " + "than or equal to 0." % (name, value) + ) except TypeError: # Python 3 - raise ValueError("Timeout value %s was %s, but it must be an " - "int, float or None." % (name, value)) + raise ValueError( + "Timeout value %s was %s, but it must be an " + "int, float or None." % (name, value) + ) return value @@ -166,8 +180,7 @@ def clone(self): # We can't use copy.deepcopy because that will also create a new object # for _GLOBAL_DEFAULT_TIMEOUT, which socket.py uses as a sentinel to # detect the user default. - return Timeout(connect=self._connect, read=self._read, - total=self.total) + return Timeout(connect=self._connect, read=self._read, total=self.total) def start_connect(self): """ Start the timeout clock, used during a connect() attempt @@ -183,14 +196,15 @@ def start_connect(self): def get_connect_duration(self): """ Gets the time elapsed since the call to :meth:`start_connect`. - :return: Elapsed time. + :return: Elapsed time in seconds. :rtype: float :raises urllib3.exceptions.TimeoutStateError: if you attempt to get duration for a timer that hasn't been started. """ if self._start_connect is None: - raise TimeoutStateError("Can't get connect duration for timer " - "that has not started.") + raise TimeoutStateError( + "Can't get connect duration for timer " "that has not started." + ) return current_time() - self._start_connect @property @@ -228,15 +242,16 @@ def read_timeout(self): :raises urllib3.exceptions.TimeoutStateError: If :meth:`start_connect` has not yet been called on this object. """ - if (self.total is not None and - self.total is not self.DEFAULT_TIMEOUT and - self._read is not None and - self._read is not self.DEFAULT_TIMEOUT): + if ( + self.total is not None + and self.total is not self.DEFAULT_TIMEOUT + and self._read is not None + and self._read is not self.DEFAULT_TIMEOUT + ): # In case the connect timeout has not yet been established. if self._start_connect is None: return self._read - return max(0, min(self.total - self.get_connect_duration(), - self._read)) + return max(0, min(self.total - self.get_connect_duration(), self._read)) elif self.total is not None and self.total is not self.DEFAULT_TIMEOUT: return max(0, self.total - self.get_connect_duration()) else: diff --git a/src/pip/_vendor/urllib3/util/url.py b/src/pip/_vendor/urllib3/util/url.py index aefa119b590..5fe37a72dfe 100644 --- a/src/pip/_vendor/urllib3/util/url.py +++ b/src/pip/_vendor/urllib3/util/url.py @@ -3,41 +3,108 @@ from collections import namedtuple from ..exceptions import LocationParseError -from ..packages import six, rfc3986 -from ..packages.rfc3986.exceptions import RFC3986Exception, ValidationError -from ..packages.rfc3986.validators import Validator -from ..packages.rfc3986 import abnf_regexp, normalizers, compat, misc +from ..packages import six -url_attrs = ['scheme', 'auth', 'host', 'port', 'path', 'query', 'fragment'] +url_attrs = ["scheme", "auth", "host", "port", "path", "query", "fragment"] # We only want to normalize urls with an HTTP(S) scheme. # urllib3 infers URLs without a scheme (None) to be http. -NORMALIZABLE_SCHEMES = ('http', 'https', None) - -# Regex for detecting URLs with schemes. RFC 3986 Section 3.1 -SCHEME_REGEX = re.compile(r"^(?:[a-zA-Z][a-zA-Z0-9+\-]*:|/)") - -PATH_CHARS = abnf_regexp.UNRESERVED_CHARS_SET | abnf_regexp.SUB_DELIMITERS_SET | {':', '@', '/'} -QUERY_CHARS = FRAGMENT_CHARS = PATH_CHARS | {'?'} - - -class Url(namedtuple('Url', url_attrs)): +NORMALIZABLE_SCHEMES = ("http", "https", None) + +# Almost all of these patterns were derived from the +# 'rfc3986' module: https://github.com/python-hyper/rfc3986 +PERCENT_RE = re.compile(r"%[a-fA-F0-9]{2}") +SCHEME_RE = re.compile(r"^(?:[a-zA-Z][a-zA-Z0-9+-]*:|/)") +URI_RE = re.compile( + r"^(?:([a-zA-Z][a-zA-Z0-9+.-]*):)?" + r"(?://([^/?#]*))?" + r"([^?#]*)" + r"(?:\?([^#]*))?" + r"(?:#(.*))?$", + re.UNICODE | re.DOTALL, +) + +IPV4_PAT = r"(?:[0-9]{1,3}\.){3}[0-9]{1,3}" +HEX_PAT = "[0-9A-Fa-f]{1,4}" +LS32_PAT = "(?:{hex}:{hex}|{ipv4})".format(hex=HEX_PAT, ipv4=IPV4_PAT) +_subs = {"hex": HEX_PAT, "ls32": LS32_PAT} +_variations = [ + # 6( h16 ":" ) ls32 + "(?:%(hex)s:){6}%(ls32)s", + # "::" 5( h16 ":" ) ls32 + "::(?:%(hex)s:){5}%(ls32)s", + # [ h16 ] "::" 4( h16 ":" ) ls32 + "(?:%(hex)s)?::(?:%(hex)s:){4}%(ls32)s", + # [ *1( h16 ":" ) h16 ] "::" 3( h16 ":" ) ls32 + "(?:(?:%(hex)s:)?%(hex)s)?::(?:%(hex)s:){3}%(ls32)s", + # [ *2( h16 ":" ) h16 ] "::" 2( h16 ":" ) ls32 + "(?:(?:%(hex)s:){0,2}%(hex)s)?::(?:%(hex)s:){2}%(ls32)s", + # [ *3( h16 ":" ) h16 ] "::" h16 ":" ls32 + "(?:(?:%(hex)s:){0,3}%(hex)s)?::%(hex)s:%(ls32)s", + # [ *4( h16 ":" ) h16 ] "::" ls32 + "(?:(?:%(hex)s:){0,4}%(hex)s)?::%(ls32)s", + # [ *5( h16 ":" ) h16 ] "::" h16 + "(?:(?:%(hex)s:){0,5}%(hex)s)?::%(hex)s", + # [ *6( h16 ":" ) h16 ] "::" + "(?:(?:%(hex)s:){0,6}%(hex)s)?::", +] + +UNRESERVED_PAT = r"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._!\-~" +IPV6_PAT = "(?:" + "|".join([x % _subs for x in _variations]) + ")" +ZONE_ID_PAT = "(?:%25|%)(?:[" + UNRESERVED_PAT + "]|%[a-fA-F0-9]{2})+" +IPV6_ADDRZ_PAT = r"\[" + IPV6_PAT + r"(?:" + ZONE_ID_PAT + r")?\]" +REG_NAME_PAT = r"(?:[^\[\]%:/?#]|%[a-fA-F0-9]{2})*" +TARGET_RE = re.compile(r"^(/[^?]*)(?:\?([^#]+))?(?:#(.*))?$") + +IPV4_RE = re.compile("^" + IPV4_PAT + "$") +IPV6_RE = re.compile("^" + IPV6_PAT + "$") +IPV6_ADDRZ_RE = re.compile("^" + IPV6_ADDRZ_PAT + "$") +BRACELESS_IPV6_ADDRZ_RE = re.compile("^" + IPV6_ADDRZ_PAT[2:-2] + "$") +ZONE_ID_RE = re.compile("(" + ZONE_ID_PAT + r")\]$") + +SUBAUTHORITY_PAT = (u"^(?:(.*)@)?(%s|%s|%s)(?::([0-9]{0,5}))?$") % ( + REG_NAME_PAT, + IPV4_PAT, + IPV6_ADDRZ_PAT, +) +SUBAUTHORITY_RE = re.compile(SUBAUTHORITY_PAT, re.UNICODE | re.DOTALL) + +UNRESERVED_CHARS = set( + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789._-~" +) +SUB_DELIM_CHARS = set("!$&'()*+,;=") +USERINFO_CHARS = UNRESERVED_CHARS | SUB_DELIM_CHARS | {":"} +PATH_CHARS = USERINFO_CHARS | {"@", "/"} +QUERY_CHARS = FRAGMENT_CHARS = PATH_CHARS | {"?"} + + +class Url(namedtuple("Url", url_attrs)): """ Data structure for representing an HTTP URL. Used as a return value for :func:`parse_url`. Both the scheme and host are normalized as they are both case-insensitive according to RFC 3986. """ + __slots__ = () - def __new__(cls, scheme=None, auth=None, host=None, port=None, path=None, - query=None, fragment=None): - if path and not path.startswith('/'): - path = '/' + path + def __new__( + cls, + scheme=None, + auth=None, + host=None, + port=None, + path=None, + query=None, + fragment=None, + ): + if path and not path.startswith("/"): + path = "/" + path if scheme is not None: scheme = scheme.lower() - return super(Url, cls).__new__(cls, scheme, auth, host, port, path, - query, fragment) + return super(Url, cls).__new__( + cls, scheme, auth, host, port, path, query, fragment + ) @property def hostname(self): @@ -47,10 +114,10 @@ def hostname(self): @property def request_uri(self): """Absolute path including the query string.""" - uri = self.path or '/' + uri = self.path or "/" if self.query is not None: - uri += '?' + self.query + uri += "?" + self.query return uri @@ -58,7 +125,7 @@ def request_uri(self): def netloc(self): """Network location including host and port""" if self.port: - return '%s:%d' % (self.host, self.port) + return "%s:%d" % (self.host, self.port) return self.host @property @@ -81,23 +148,23 @@ def url(self): 'http://username:password@host.com:80/path?query#fragment' """ scheme, auth, host, port, path, query, fragment = self - url = u'' + url = u"" # We use "is not None" we want things to happen with empty strings (or 0 port) if scheme is not None: - url += scheme + u'://' + url += scheme + u"://" if auth is not None: - url += auth + u'@' + url += auth + u"@" if host is not None: url += host if port is not None: - url += u':' + str(port) + url += u":" + str(port) if path is not None: url += path if query is not None: - url += u'?' + query + url += u"?" + query if fragment is not None: - url += u'#' + fragment + url += u"#" + fragment return url @@ -135,48 +202,149 @@ def split_first(s, delims): min_delim = d if min_idx is None or min_idx < 0: - return s, '', None + return s, "", None - return s[:min_idx], s[min_idx + 1:], min_delim + return s[:min_idx], s[min_idx + 1 :], min_delim -def _encode_invalid_chars(component, allowed_chars, encoding='utf-8'): +def _encode_invalid_chars(component, allowed_chars, encoding="utf-8"): """Percent-encodes a URI component without reapplying - onto an already percent-encoded component. Based on - rfc3986.normalizers.encode_component() + onto an already percent-encoded component. """ if component is None: return component + component = six.ensure_text(component) + # Try to see if the component we're encoding is already percent-encoded # so we can skip all '%' characters but still encode all others. - percent_encodings = len(normalizers.PERCENT_MATCHER.findall( - compat.to_str(component, encoding))) + percent_encodings = PERCENT_RE.findall(component) - uri_bytes = component.encode('utf-8', 'surrogatepass') - is_percent_encoded = percent_encodings == uri_bytes.count(b'%') + # Normalize existing percent-encoded bytes. + for enc in percent_encodings: + if not enc.isupper(): + component = component.replace(enc, enc.upper()) + + uri_bytes = component.encode("utf-8", "surrogatepass") + is_percent_encoded = len(percent_encodings) == uri_bytes.count(b"%") encoded_component = bytearray() for i in range(0, len(uri_bytes)): # Will return a single character bytestring on both Python 2 & 3 - byte = uri_bytes[i:i+1] + byte = uri_bytes[i : i + 1] byte_ord = ord(byte) - if ((is_percent_encoded and byte == b'%') - or (byte_ord < 128 and byte.decode() in allowed_chars)): + if (is_percent_encoded and byte == b"%") or ( + byte_ord < 128 and byte.decode() in allowed_chars + ): encoded_component.extend(byte) continue - encoded_component.extend('%{0:02x}'.format(byte_ord).encode().upper()) + encoded_component.extend(b"%" + (hex(byte_ord)[2:].encode().zfill(2).upper())) return encoded_component.decode(encoding) +def _remove_path_dot_segments(path): + # See http://tools.ietf.org/html/rfc3986#section-5.2.4 for pseudo-code + segments = path.split("/") # Turn the path into a list of segments + output = [] # Initialize the variable to use to store output + + for segment in segments: + # '.' is the current directory, so ignore it, it is superfluous + if segment == ".": + continue + # Anything other than '..', should be appended to the output + elif segment != "..": + output.append(segment) + # In this case segment == '..', if we can, we should pop the last + # element + elif output: + output.pop() + + # If the path starts with '/' and the output is empty or the first string + # is non-empty + if path.startswith("/") and (not output or output[0]): + output.insert(0, "") + + # If the path starts with '/.' or '/..' ensure we add one more empty + # string to add a trailing '/' + if path.endswith(("/.", "/..")): + output.append("") + + return "/".join(output) + + +def _normalize_host(host, scheme): + if host: + if isinstance(host, six.binary_type): + host = six.ensure_str(host) + + if scheme in NORMALIZABLE_SCHEMES: + is_ipv6 = IPV6_ADDRZ_RE.match(host) + if is_ipv6: + match = ZONE_ID_RE.search(host) + if match: + start, end = match.span(1) + zone_id = host[start:end] + + if zone_id.startswith("%25") and zone_id != "%25": + zone_id = zone_id[3:] + else: + zone_id = zone_id[1:] + zone_id = "%" + _encode_invalid_chars(zone_id, UNRESERVED_CHARS) + return host[:start].lower() + zone_id + host[end:] + else: + return host.lower() + elif not IPV4_RE.match(host): + return six.ensure_str( + b".".join([_idna_encode(label) for label in host.split(".")]) + ) + return host + + +def _idna_encode(name): + if name and any([ord(x) > 128 for x in name]): + try: + from pip._vendor import idna + except ImportError: + six.raise_from( + LocationParseError("Unable to parse URL without the 'idna' module"), + None, + ) + try: + return idna.encode(name.lower(), strict=True, std3_rules=True) + except idna.IDNAError: + six.raise_from( + LocationParseError(u"Name '%s' is not a valid IDNA label" % name), None + ) + return name.lower().encode("ascii") + + +def _encode_target(target): + """Percent-encodes a request target so that there are no invalid characters""" + if not target.startswith("/"): + return target + + path, query, fragment = TARGET_RE.match(target).groups() + target = _encode_invalid_chars(path, PATH_CHARS) + query = _encode_invalid_chars(query, QUERY_CHARS) + fragment = _encode_invalid_chars(fragment, FRAGMENT_CHARS) + if query is not None: + target += "?" + query + if fragment is not None: + target += "#" + target + return target + + def parse_url(url): """ Given a url, return a parsed :class:`.Url` namedtuple. Best-effort is performed to parse incomplete urls. Fields not provided will be None. This parser is RFC 3986 compliant. + The parser logic and helper functions are based heavily on + work done in the ``rfc3986`` module. + :param str url: URL to parse into a :class:`.Url` namedtuple. Partly backwards-compatible with :mod:`urlparse`. @@ -194,90 +362,72 @@ def parse_url(url): # Empty return Url() - is_string = not isinstance(url, six.binary_type) - - # RFC 3986 doesn't like URLs that have a host but don't start - # with a scheme and we support URLs like that so we need to - # detect that problem and add an empty scheme indication. - # We don't get hurt on path-only URLs here as it's stripped - # off and given an empty scheme anyways. - if not SCHEME_REGEX.search(url): + source_url = url + if not SCHEME_RE.search(url): url = "//" + url - def idna_encode(name): - if name and any([ord(x) > 128 for x in name]): - try: - from pip._vendor import idna - except ImportError: - raise LocationParseError("Unable to parse URL without the 'idna' module") - try: - return idna.encode(name.lower(), strict=True, std3_rules=True) - except idna.IDNAError: - raise LocationParseError(u"Name '%s' is not a valid IDNA label" % name) - return name - - try: - split_iri = misc.IRI_MATCHER.match(compat.to_str(url)).groupdict() - iri_ref = rfc3986.IRIReference( - split_iri['scheme'], split_iri['authority'], - _encode_invalid_chars(split_iri['path'], PATH_CHARS), - _encode_invalid_chars(split_iri['query'], QUERY_CHARS), - _encode_invalid_chars(split_iri['fragment'], FRAGMENT_CHARS) - ) - has_authority = iri_ref.authority is not None - uri_ref = iri_ref.encode(idna_encoder=idna_encode) - except (ValueError, RFC3986Exception): - return six.raise_from(LocationParseError(url), None) - - # rfc3986 strips the authority if it's invalid - if has_authority and uri_ref.authority is None: - raise LocationParseError(url) - - # Only normalize schemes we understand to not break http+unix - # or other schemes that don't follow RFC 3986. - if uri_ref.scheme is None or uri_ref.scheme.lower() in NORMALIZABLE_SCHEMES: - uri_ref = uri_ref.normalize() - - # Validate all URIReference components and ensure that all - # components that were set before are still set after - # normalization has completed. - validator = Validator() try: - validator.check_validity_of( - *validator.COMPONENT_NAMES - ).validate(uri_ref) - except ValidationError: - return six.raise_from(LocationParseError(url), None) + scheme, authority, path, query, fragment = URI_RE.match(url).groups() + normalize_uri = scheme is None or scheme.lower() in NORMALIZABLE_SCHEMES + + if scheme: + scheme = scheme.lower() + + if authority: + auth, host, port = SUBAUTHORITY_RE.match(authority).groups() + if auth and normalize_uri: + auth = _encode_invalid_chars(auth, USERINFO_CHARS) + if port == "": + port = None + else: + auth, host, port = None, None, None + + if port is not None: + port = int(port) + if not (0 <= port <= 65535): + raise LocationParseError(url) + + host = _normalize_host(host, scheme) + + if normalize_uri and path: + path = _remove_path_dot_segments(path) + path = _encode_invalid_chars(path, PATH_CHARS) + if normalize_uri and query: + query = _encode_invalid_chars(query, QUERY_CHARS) + if normalize_uri and fragment: + fragment = _encode_invalid_chars(fragment, FRAGMENT_CHARS) + + except (ValueError, AttributeError): + return six.raise_from(LocationParseError(source_url), None) # For the sake of backwards compatibility we put empty # string values for path if there are any defined values # beyond the path in the URL. # TODO: Remove this when we break backwards compatibility. - path = uri_ref.path if not path: - if (uri_ref.query is not None - or uri_ref.fragment is not None): + if query is not None or fragment is not None: path = "" else: path = None # Ensure that each part of the URL is a `str` for # backwards compatibility. - def to_input_type(x): - if x is None: - return None - elif not is_string and not isinstance(x, six.binary_type): - return x.encode('utf-8') - return x + if isinstance(url, six.text_type): + ensure_func = six.ensure_text + else: + ensure_func = six.ensure_str + + def ensure_type(x): + return x if x is None else ensure_func(x) return Url( - scheme=to_input_type(uri_ref.scheme), - auth=to_input_type(uri_ref.userinfo), - host=to_input_type(uri_ref.host), - port=int(uri_ref.port) if uri_ref.port is not None else None, - path=to_input_type(path), - query=to_input_type(uri_ref.query), - fragment=to_input_type(uri_ref.fragment) + scheme=ensure_type(scheme), + auth=ensure_type(auth), + host=ensure_type(host), + port=port, + path=ensure_type(path), + query=ensure_type(query), + fragment=ensure_type(fragment), ) @@ -286,4 +436,4 @@ def get_host(url): Deprecated. Use :func:`parse_url` instead. """ p = parse_url(url) - return p.scheme or 'http', p.hostname, p.port + return p.scheme or "http", p.hostname, p.port diff --git a/src/pip/_vendor/urllib3/util/wait.py b/src/pip/_vendor/urllib3/util/wait.py index 4db71bafd87..d71d2fd722b 100644 --- a/src/pip/_vendor/urllib3/util/wait.py +++ b/src/pip/_vendor/urllib3/util/wait.py @@ -2,6 +2,7 @@ from functools import partial import select import sys + try: from time import monotonic except ImportError: @@ -40,6 +41,8 @@ class NoWayToWaitForSocketError(Exception): # Modern Python, that retries syscalls by default def _retry_on_intr(fn, timeout): return fn(timeout) + + else: # Old and broken Pythons. def _retry_on_intr(fn, timeout): diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 5e04868231f..aadd35261a1 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -6,18 +6,18 @@ distlib==0.2.9.post0 distro==1.4.0 html5lib==1.0.1 ipaddress==1.0.22 # Only needed on 2.6 and 2.7 -msgpack==0.6.1 -packaging==19.0 -pep517==0.5.0 +msgpack==0.6.2 +packaging==19.2 +pep517==0.7.0 progress==1.5 -pyparsing==2.4.0 -pytoml==0.1.20 +pyparsing==2.4.2 +pytoml==0.1.21 requests==2.22.0 - certifi==2019.6.16 + certifi==2019.9.11 chardet==3.0.4 idna==2.8 - urllib3==1.25.3 + urllib3==1.25.6 retrying==1.3.3 -setuptools==41.0.1 +setuptools==41.4.0 six==1.12.0 webencodings==0.5.1 From f84d9c810b5c1520b6102fe7f93b182c449fc1f0 Mon Sep 17 00:00:00 2001 From: Pachwenko <32424503+Pachwenko@users.noreply.github.com> Date: Wed, 9 Oct 2019 23:28:40 -0500 Subject: [PATCH 0520/3170] update global install option test --- news/2578.trivial | 0 tests/functional/test_install.py | 1 + 2 files changed, 1 insertion(+) create mode 100644 news/2578.trivial diff --git a/news/2578.trivial b/news/2578.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 2e980d5402d..f07dcd4eff0 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -635,6 +635,7 @@ def test_install_global_option(script): 'install', '--global-option=--version', "INITools==0.1", expect_stderr=True) assert 'INITools==0.1\n' in result.stdout + assert not result.files_created def test_install_with_hacked_egg_info(script, data): From f81f3c902c181b416cf15a78c9bdc1466d732253 Mon Sep 17 00:00:00 2001 From: tbeswick <tbeswick@enphaseenergy.com> Date: Fri, 11 Oct 2019 12:27:17 +1300 Subject: [PATCH 0521/3170] Removed `get_repo_root_dir()`, this functionality is now included in `Git.get_subdirectory()` and `Mercurial.get_subdirectory()`. Added `find_path_to_setup_from_repo_root()` function to perform the common parts of `get_subdirectory()`. --- src/pip/_internal/vcs/git.py | 737 ++++++------ src/pip/_internal/vcs/mercurial.py | 299 ++--- src/pip/_internal/vcs/versioncontrol.py | 1359 +++++++++++------------ 3 files changed, 1198 insertions(+), 1197 deletions(-) diff --git a/src/pip/_internal/vcs/git.py b/src/pip/_internal/vcs/git.py index 65ff69877e3..6b804114002 100644 --- a/src/pip/_internal/vcs/git.py +++ b/src/pip/_internal/vcs/git.py @@ -1,365 +1,372 @@ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - -from __future__ import absolute_import - -import logging -import os.path -import re - -from pip._vendor.packaging.version import parse as parse_version -from pip._vendor.six.moves.urllib import parse as urllib_parse -from pip._vendor.six.moves.urllib import request as urllib_request - -from pip._internal.exceptions import BadCommand -from pip._internal.utils.misc import display_path -from pip._internal.utils.subprocess import make_command -from pip._internal.utils.temp_dir import TempDirectory -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from pip._internal.vcs.versioncontrol import ( - RemoteNotFoundError, - VersionControl, - vcs, -) - -if MYPY_CHECK_RUNNING: - from typing import Optional, Tuple - from pip._internal.utils.misc import HiddenText - from pip._internal.vcs.versioncontrol import AuthInfo, RevOptions - - -urlsplit = urllib_parse.urlsplit -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' - repo_name = 'clone' - 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' - - @staticmethod - def get_base_rev_args(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):].split()[0] - else: - version = '' - # get first 3 positions of the git version because - # on windows it is x.y.z.windows.t, and this parses as - # LegacyVersion which always smaller than a Version. - version = '.'.join(version.split('.')[:3]) - return parse_version(version) - - @classmethod - def get_current_branch(cls, location): - """ - Return the current branch, or None if HEAD isn't at a branch - (e.g. detached HEAD). - """ - # git-symbolic-ref exits with empty stdout if "HEAD" is a detached - # HEAD rather than a symbolic ref. In addition, the -q causes the - # command to exit with status code 1 instead of 128 in this case - # and to suppress the message to stderr. - args = ['symbolic-ref', '-q', 'HEAD'] - output = cls.run_command( - args, extra_ok_returncodes=(1, ), show_stdout=False, cwd=location, - ) - ref = output.strip() - - if ref.startswith('refs/heads/'): - return ref[len('refs/heads/'):] - - return None - - def export(self, location, url): - # type: (str, HiddenText) -> None - """Export the Git repository at the url to the destination location""" - if not location.endswith('/'): - location = location + '/' - - with TempDirectory(kind="export") as temp_dir: - self.unpack(temp_dir.path, url=url) - self.run_command( - ['checkout-index', '-a', '-f', '--prefix', location], - show_stdout=False, cwd=temp_dir.path - ) - - @classmethod - def get_revision_sha(cls, dest, rev): - """ - Return (sha_or_none, is_branch), where sha_or_none is a commit hash - if the revision names a remote branch or tag, otherwise None. - - Args: - dest: the repository directory. - rev: the revision name. - """ - # Pass rev to pre-filter the list. - output = cls.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) - - sha = refs.get(branch_ref) - if sha is not None: - return (sha, True) - - sha = refs.get(tag_ref) - - return (sha, False) - - @classmethod - def resolve_revision(cls, dest, url, rev_options): - # type: (str, HiddenText, RevOptions) -> RevOptions - """ - Resolve a revision to a new RevOptions object with the SHA1 of the - branch, tag, or ref if found. - - Args: - rev_options: a RevOptions object. - """ - rev = rev_options.arg_rev - # The arg_rev property's implementation for Git ensures that the - # rev return value is always non-None. - assert rev is not None - - sha, is_branch = cls.get_revision_sha(dest, rev) - - if sha is not None: - rev_options = rev_options.make_new(sha) - rev_options.branch_name = rev if is_branch else None - - return rev_options - - # 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, - ) - - if not rev.startswith('refs/'): - return rev_options - - # If it looks like a ref, we have to fetch it explicitly. - cls.run_command( - make_command('fetch', '-q', url, rev_options.to_args()), - cwd=dest, - ) - # Change the revision to the SHA of the ref we fetched - sha = cls.get_revision(dest, rev='FETCH_HEAD') - rev_options = rev_options.make_new(sha) - - return rev_options - - @classmethod - def is_commit_id_equal(cls, 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 cls.get_revision(dest) == name - - def fetch_new(self, dest, url, rev_options): - # type: (str, HiddenText, RevOptions) -> None - rev_display = rev_options.to_display() - logger.info('Cloning %s%s to %s', url, rev_display, display_path(dest)) - self.run_command(make_command('clone', '-q', url, dest)) - - if rev_options.rev: - # Then a specific revision was requested. - rev_options = self.resolve_revision(dest, url, rev_options) - branch_name = getattr(rev_options, 'branch_name', None) - if branch_name is None: - # 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): - cmd_args = make_command( - 'checkout', '-q', rev_options.to_args(), - ) - self.run_command(cmd_args, cwd=dest) - elif self.get_current_branch(dest) != branch_name: - # Then a specific branch was requested, and that branch - # is not yet checked out. - track_branch = 'origin/{}'.format(branch_name) - cmd_args = [ - 'checkout', '-b', branch_name, '--track', track_branch, - ] - self.run_command(cmd_args, cwd=dest) - - #: repo may contain submodules - self.update_submodules(dest) - - def switch(self, dest, url, rev_options): - # type: (str, HiddenText, RevOptions) -> None - self.run_command( - make_command('config', 'remote.origin.url', url), - cwd=dest, - ) - cmd_args = make_command('checkout', '-q', rev_options.to_args()) - self.run_command(cmd_args, cwd=dest) - - self.update_submodules(dest) - - def update(self, dest, url, rev_options): - # type: (str, HiddenText, RevOptions) -> None - # First fetch changes from the default remote - if self.get_git_version() >= parse_version('1.9.0'): - # fetch tags in addition to everything else - self.run_command(['fetch', '-q', '--tags'], cwd=dest) - else: - self.run_command(['fetch', '-q'], cwd=dest) - # Then reset to wanted revision (maybe even origin/master) - rev_options = self.resolve_revision(dest, url, rev_options) - cmd_args = make_command('reset', '--hard', '-q', rev_options.to_args()) - self.run_command(cmd_args, cwd=dest) - #: update submodules - self.update_submodules(dest) - - @classmethod - def get_remote_url(cls, location): - """ - Return URL of the first remote encountered. - - Raises RemoteNotFoundError if the repository does not have a remote - url configured. - """ - # We need to pass 1 for extra_ok_returncodes since the command - # exits with return code 1 if there are no matching lines. - stdout = cls.run_command( - ['config', '--get-regexp', r'remote\..*\.url'], - extra_ok_returncodes=(1, ), show_stdout=False, cwd=location, - ) - remotes = stdout.splitlines() - try: - found_remote = remotes[0] - except IndexError: - raise RemoteNotFoundError - - for remote in remotes: - if remote.startswith('remote.origin.url '): - found_remote = remote - break - url = found_remote.split(' ')[1] - return url.strip() - - @classmethod - def get_revision(cls, location, rev=None): - if rev is None: - rev = 'HEAD' - current_rev = cls.run_command( - ['rev-parse', rev], show_stdout=False, cwd=location, - ) - return current_rev.strip() - - @classmethod - def get_repo_root_dir(cls, location): - git_dir = cls.run_command(['rev-parse', '--git-dir'], - show_stdout=False, cwd=location).strip() - if not os.path.isabs(git_dir): - git_dir = os.path.join(location, git_dir) - root_dir = os.path.join(git_dir, '..') - return os.path.abspath(root_dir) - - @classmethod - def get_url_rev_and_auth(cls, url): - # type: (str) -> Tuple[str, Optional[str], AuthInfo] - """ - Prefixes stub URLs like 'user@hostname:user/repo.git' with 'ssh://'. - That's required because although they use SSH they sometimes don't - work with a ssh:// scheme (e.g. GitHub). But we need a scheme for - parsing. Hence we remove it again afterwards and return it as a stub. - """ - # Works around an apparent Git bug - # (see https://article.gmane.org/gmane.comp.version-control.git/146500) - scheme, netloc, path, query, fragment = urlsplit(url) - if scheme.endswith('file'): - initial_slashes = path[:-len(path.lstrip('/'))] - newpath = ( - initial_slashes + - urllib_request.url2pathname(path) - .replace('\\', '/').lstrip('/') - ) - url = urlunsplit((scheme, netloc, newpath, query, fragment)) - after_plus = scheme.find('+') + 1 - url = scheme[:after_plus] + urlunsplit( - (scheme[after_plus:], netloc, newpath, query, fragment), - ) - - if '://' not in url: - assert 'file:' not in url - url = url.replace('git+', 'git+ssh://') - url, rev, user_pass = super(Git, cls).get_url_rev_and_auth(url) - url = url.replace('ssh://', '') - else: - url, rev, user_pass = super(Git, cls).get_url_rev_and_auth(url) - - return url, rev, user_pass - - @classmethod - def update_submodules(cls, location): - if not os.path.exists(os.path.join(location, '.gitmodules')): - return - cls.run_command( - ['submodule', 'update', '--init', '--recursive', '-q'], - cwd=location, - ) - - @classmethod - def controls_location(cls, location): - if super(Git, cls).controls_location(location): - return True - try: - r = cls.run_command(['rev-parse'], - cwd=location, - show_stdout=False, - on_returncode='ignore', - log_failed_cmd=False) - return not r - except BadCommand: - logger.debug("could not determine if %s is under git control " - "because git is not available", location) - return False - - -vcs.register(Git) +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + +from __future__ import absolute_import + +import logging +import os.path +import re + +from pip._vendor.packaging.version import parse as parse_version +from pip._vendor.six.moves.urllib import parse as urllib_parse +from pip._vendor.six.moves.urllib import request as urllib_request + +from pip._internal.exceptions import BadCommand +from pip._internal.utils.misc import display_path +from pip._internal.utils.subprocess import make_command +from pip._internal.utils.temp_dir import TempDirectory +from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from pip._internal.vcs.versioncontrol import ( + RemoteNotFoundError, + VersionControl, + find_path_to_setup_from_repo_root, + vcs, +) + +if MYPY_CHECK_RUNNING: + from typing import Optional, Tuple + from pip._internal.utils.misc import HiddenText + from pip._internal.vcs.versioncontrol import AuthInfo, RevOptions + + +urlsplit = urllib_parse.urlsplit +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' + repo_name = 'clone' + 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' + + @staticmethod + def get_base_rev_args(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):].split()[0] + else: + version = '' + # get first 3 positions of the git version because + # on windows it is x.y.z.windows.t, and this parses as + # LegacyVersion which always smaller than a Version. + version = '.'.join(version.split('.')[:3]) + return parse_version(version) + + @classmethod + def get_current_branch(cls, location): + """ + Return the current branch, or None if HEAD isn't at a branch + (e.g. detached HEAD). + """ + # git-symbolic-ref exits with empty stdout if "HEAD" is a detached + # HEAD rather than a symbolic ref. In addition, the -q causes the + # command to exit with status code 1 instead of 128 in this case + # and to suppress the message to stderr. + args = ['symbolic-ref', '-q', 'HEAD'] + output = cls.run_command( + args, extra_ok_returncodes=(1, ), show_stdout=False, cwd=location, + ) + ref = output.strip() + + if ref.startswith('refs/heads/'): + return ref[len('refs/heads/'):] + + return None + + def export(self, location, url): + # type: (str, HiddenText) -> None + """Export the Git repository at the url to the destination location""" + if not location.endswith('/'): + location = location + '/' + + with TempDirectory(kind="export") as temp_dir: + self.unpack(temp_dir.path, url=url) + self.run_command( + ['checkout-index', '-a', '-f', '--prefix', location], + show_stdout=False, cwd=temp_dir.path + ) + + @classmethod + def get_revision_sha(cls, dest, rev): + """ + Return (sha_or_none, is_branch), where sha_or_none is a commit hash + if the revision names a remote branch or tag, otherwise None. + + Args: + dest: the repository directory. + rev: the revision name. + """ + # Pass rev to pre-filter the list. + output = cls.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) + + sha = refs.get(branch_ref) + if sha is not None: + return (sha, True) + + sha = refs.get(tag_ref) + + return (sha, False) + + @classmethod + def resolve_revision(cls, dest, url, rev_options): + # type: (str, HiddenText, RevOptions) -> RevOptions + """ + Resolve a revision to a new RevOptions object with the SHA1 of the + branch, tag, or ref if found. + + Args: + rev_options: a RevOptions object. + """ + rev = rev_options.arg_rev + # The arg_rev property's implementation for Git ensures that the + # rev return value is always non-None. + assert rev is not None + + sha, is_branch = cls.get_revision_sha(dest, rev) + + if sha is not None: + rev_options = rev_options.make_new(sha) + rev_options.branch_name = rev if is_branch else None + + return rev_options + + # 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, + ) + + if not rev.startswith('refs/'): + return rev_options + + # If it looks like a ref, we have to fetch it explicitly. + cls.run_command( + make_command('fetch', '-q', url, rev_options.to_args()), + cwd=dest, + ) + # Change the revision to the SHA of the ref we fetched + sha = cls.get_revision(dest, rev='FETCH_HEAD') + rev_options = rev_options.make_new(sha) + + return rev_options + + @classmethod + def is_commit_id_equal(cls, 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 cls.get_revision(dest) == name + + def fetch_new(self, dest, url, rev_options): + # type: (str, HiddenText, RevOptions) -> None + rev_display = rev_options.to_display() + logger.info('Cloning %s%s to %s', url, rev_display, display_path(dest)) + self.run_command(make_command('clone', '-q', url, dest)) + + if rev_options.rev: + # Then a specific revision was requested. + rev_options = self.resolve_revision(dest, url, rev_options) + branch_name = getattr(rev_options, 'branch_name', None) + if branch_name is None: + # 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): + cmd_args = make_command( + 'checkout', '-q', rev_options.to_args(), + ) + self.run_command(cmd_args, cwd=dest) + elif self.get_current_branch(dest) != branch_name: + # Then a specific branch was requested, and that branch + # is not yet checked out. + track_branch = 'origin/{}'.format(branch_name) + cmd_args = [ + 'checkout', '-b', branch_name, '--track', track_branch, + ] + self.run_command(cmd_args, cwd=dest) + + #: repo may contain submodules + self.update_submodules(dest) + + def switch(self, dest, url, rev_options): + # type: (str, HiddenText, RevOptions) -> None + self.run_command( + make_command('config', 'remote.origin.url', url), + cwd=dest, + ) + cmd_args = make_command('checkout', '-q', rev_options.to_args()) + self.run_command(cmd_args, cwd=dest) + + self.update_submodules(dest) + + def update(self, dest, url, rev_options): + # type: (str, HiddenText, RevOptions) -> None + # First fetch changes from the default remote + if self.get_git_version() >= parse_version('1.9.0'): + # fetch tags in addition to everything else + self.run_command(['fetch', '-q', '--tags'], cwd=dest) + else: + self.run_command(['fetch', '-q'], cwd=dest) + # Then reset to wanted revision (maybe even origin/master) + rev_options = self.resolve_revision(dest, url, rev_options) + cmd_args = make_command('reset', '--hard', '-q', rev_options.to_args()) + self.run_command(cmd_args, cwd=dest) + #: update submodules + self.update_submodules(dest) + + @classmethod + def get_remote_url(cls, location): + """ + Return URL of the first remote encountered. + + Raises RemoteNotFoundError if the repository does not have a remote + url configured. + """ + # We need to pass 1 for extra_ok_returncodes since the command + # exits with return code 1 if there are no matching lines. + stdout = cls.run_command( + ['config', '--get-regexp', r'remote\..*\.url'], + extra_ok_returncodes=(1, ), show_stdout=False, cwd=location, + ) + remotes = stdout.splitlines() + try: + found_remote = remotes[0] + except IndexError: + raise RemoteNotFoundError + + for remote in remotes: + if remote.startswith('remote.origin.url '): + found_remote = remote + break + url = found_remote.split(' ')[1] + return url.strip() + + @classmethod + def get_revision(cls, location, rev=None): + if rev is None: + rev = 'HEAD' + current_rev = cls.run_command( + ['rev-parse', rev], show_stdout=False, cwd=location, + ) + return current_rev.strip() + + @classmethod + def get_subdirectory(cls, location): + """ + Return the path to setup.py, relative to the repo root. + Return None if setup.py is in the repo root. + """ + # find the repo root + git_dir = cls.run_command( + ['rev-parse', '--git-dir'], + show_stdout=False, cwd=location).strip() + if not os.path.isabs(git_dir): + git_dir = os.path.join(location, git_dir) + repo_root = os.path.abspath(os.path.join(git_dir, '..')) + return find_path_to_setup_from_repo_root(location, repo_root) + + @classmethod + def get_url_rev_and_auth(cls, url): + # type: (str) -> Tuple[str, Optional[str], AuthInfo] + """ + Prefixes stub URLs like 'user@hostname:user/repo.git' with 'ssh://'. + That's required because although they use SSH they sometimes don't + work with a ssh:// scheme (e.g. GitHub). But we need a scheme for + parsing. Hence we remove it again afterwards and return it as a stub. + """ + # Works around an apparent Git bug + # (see https://article.gmane.org/gmane.comp.version-control.git/146500) + scheme, netloc, path, query, fragment = urlsplit(url) + if scheme.endswith('file'): + initial_slashes = path[:-len(path.lstrip('/'))] + newpath = ( + initial_slashes + + urllib_request.url2pathname(path) + .replace('\\', '/').lstrip('/') + ) + url = urlunsplit((scheme, netloc, newpath, query, fragment)) + after_plus = scheme.find('+') + 1 + url = scheme[:after_plus] + urlunsplit( + (scheme[after_plus:], netloc, newpath, query, fragment), + ) + + if '://' not in url: + assert 'file:' not in url + url = url.replace('git+', 'git+ssh://') + url, rev, user_pass = super(Git, cls).get_url_rev_and_auth(url) + url = url.replace('ssh://', '') + else: + url, rev, user_pass = super(Git, cls).get_url_rev_and_auth(url) + + return url, rev, user_pass + + @classmethod + def update_submodules(cls, location): + if not os.path.exists(os.path.join(location, '.gitmodules')): + return + cls.run_command( + ['submodule', 'update', '--init', '--recursive', '-q'], + cwd=location, + ) + + @classmethod + def controls_location(cls, location): + if super(Git, cls).controls_location(location): + return True + try: + r = cls.run_command(['rev-parse'], + cwd=location, + show_stdout=False, + on_returncode='ignore', + log_failed_cmd=False) + return not r + except BadCommand: + logger.debug("could not determine if %s is under git control " + "because git is not available", location) + return False + + +vcs.register(Git) diff --git a/src/pip/_internal/vcs/mercurial.py b/src/pip/_internal/vcs/mercurial.py index d1036d699dc..dd04b8bc687 100644 --- a/src/pip/_internal/vcs/mercurial.py +++ b/src/pip/_internal/vcs/mercurial.py @@ -1,145 +1,154 @@ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - -from __future__ import absolute_import - -import logging -import os - -from pip._vendor.six.moves import configparser - -from pip._internal.exceptions import BadCommand, InstallationError -from pip._internal.utils.misc import display_path -from pip._internal.utils.subprocess import make_command -from pip._internal.utils.temp_dir import TempDirectory -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from pip._internal.utils.urls import path_to_url -from pip._internal.vcs.versioncontrol import VersionControl, vcs - -if MYPY_CHECK_RUNNING: - from pip._internal.utils.misc import HiddenText - from pip._internal.vcs.versioncontrol import RevOptions - - -logger = logging.getLogger(__name__) - - -class Mercurial(VersionControl): - name = 'hg' - dirname = '.hg' - repo_name = 'clone' - schemes = ( - 'hg', 'hg+file', 'hg+http', 'hg+https', 'hg+ssh', 'hg+static-http', - ) - - @staticmethod - def get_base_rev_args(rev): - return [rev] - - def export(self, location, url): - # type: (str, HiddenText) -> None - """Export the Hg repository at the url to the destination location""" - with TempDirectory(kind="export") as temp_dir: - self.unpack(temp_dir.path, url=url) - - self.run_command( - ['archive', location], show_stdout=False, cwd=temp_dir.path - ) - - def fetch_new(self, dest, url, rev_options): - # type: (str, HiddenText, RevOptions) -> None - rev_display = rev_options.to_display() - logger.info( - 'Cloning hg %s%s to %s', - url, - rev_display, - display_path(dest), - ) - self.run_command(make_command('clone', '--noupdate', '-q', url, dest)) - self.run_command( - make_command('update', '-q', rev_options.to_args()), - cwd=dest, - ) - - def switch(self, dest, url, rev_options): - # type: (str, HiddenText, RevOptions) -> None - repo_config = os.path.join(dest, self.dirname, 'hgrc') - config = configparser.RawConfigParser() - try: - config.read(repo_config) - config.set('paths', 'default', url.secret) - with open(repo_config, 'w') as config_file: - config.write(config_file) - except (OSError, configparser.NoSectionError) as exc: - logger.warning( - 'Could not switch Mercurial repository to %s: %s', url, exc, - ) - else: - cmd_args = make_command('update', '-q', rev_options.to_args()) - self.run_command(cmd_args, cwd=dest) - - def update(self, dest, url, rev_options): - # type: (str, HiddenText, RevOptions) -> None - self.run_command(['pull', '-q'], cwd=dest) - cmd_args = make_command('update', '-q', rev_options.to_args()) - self.run_command(cmd_args, cwd=dest) - - @classmethod - def get_remote_url(cls, location): - url = cls.run_command( - ['showconfig', 'paths.default'], - show_stdout=False, cwd=location).strip() - if cls._is_local_repository(url): - url = path_to_url(url) - return url.strip() - - @classmethod - def get_revision(cls, location): - """ - Return the repository-local changeset revision number, as an integer. - """ - current_revision = cls.run_command( - ['parents', '--template={rev}'], - show_stdout=False, cwd=location).strip() - return current_revision - - @classmethod - def get_requirement_revision(cls, location): - """ - Return the changeset identification hash, as a 40-character - hexadecimal string - """ - current_rev_hash = cls.run_command( - ['parents', '--template={node}'], - show_stdout=False, cwd=location).strip() - return current_rev_hash - - @classmethod - def is_commit_id_equal(cls, dest, name): - """Always assume the versions don't match""" - return False - - @classmethod - def get_repo_root_dir(cls, location): - root_dir = cls.run_command( - ['root'], show_stdout=False, cwd=location).strip() - if not os.path.isabs(root_dir): - root_dir = os.path.join(location, root_dir) - return os.path.abspath(root_dir) - - @classmethod - def controls_location(cls, location): - if super(Mercurial, cls).controls_location(location): - return True - try: - cls.run_command( - ['identify'], - cwd=location, - show_stdout=False, - on_returncode='raise', - log_failed_cmd=False) - except (BadCommand, InstallationError): - return False - - -vcs.register(Mercurial) +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + +from __future__ import absolute_import + +import logging +import os + +from pip._vendor.six.moves import configparser + +from pip._internal.exceptions import BadCommand, InstallationError +from pip._internal.utils.misc import display_path +from pip._internal.utils.subprocess import make_command +from pip._internal.utils.temp_dir import TempDirectory +from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from pip._internal.utils.urls import path_to_url +from pip._internal.vcs.versioncontrol import ( + VersionControl, + find_path_to_setup_from_repo_root, + vcs, +) + +if MYPY_CHECK_RUNNING: + from pip._internal.utils.misc import HiddenText + from pip._internal.vcs.versioncontrol import RevOptions + + +logger = logging.getLogger(__name__) + + +class Mercurial(VersionControl): + name = 'hg' + dirname = '.hg' + repo_name = 'clone' + schemes = ( + 'hg', 'hg+file', 'hg+http', 'hg+https', 'hg+ssh', 'hg+static-http', + ) + + @staticmethod + def get_base_rev_args(rev): + return [rev] + + def export(self, location, url): + # type: (str, HiddenText) -> None + """Export the Hg repository at the url to the destination location""" + with TempDirectory(kind="export") as temp_dir: + self.unpack(temp_dir.path, url=url) + + self.run_command( + ['archive', location], show_stdout=False, cwd=temp_dir.path + ) + + def fetch_new(self, dest, url, rev_options): + # type: (str, HiddenText, RevOptions) -> None + rev_display = rev_options.to_display() + logger.info( + 'Cloning hg %s%s to %s', + url, + rev_display, + display_path(dest), + ) + self.run_command(make_command('clone', '--noupdate', '-q', url, dest)) + self.run_command( + make_command('update', '-q', rev_options.to_args()), + cwd=dest, + ) + + def switch(self, dest, url, rev_options): + # type: (str, HiddenText, RevOptions) -> None + repo_config = os.path.join(dest, self.dirname, 'hgrc') + config = configparser.RawConfigParser() + try: + config.read(repo_config) + config.set('paths', 'default', url.secret) + with open(repo_config, 'w') as config_file: + config.write(config_file) + except (OSError, configparser.NoSectionError) as exc: + logger.warning( + 'Could not switch Mercurial repository to %s: %s', url, exc, + ) + else: + cmd_args = make_command('update', '-q', rev_options.to_args()) + self.run_command(cmd_args, cwd=dest) + + def update(self, dest, url, rev_options): + # type: (str, HiddenText, RevOptions) -> None + self.run_command(['pull', '-q'], cwd=dest) + cmd_args = make_command('update', '-q', rev_options.to_args()) + self.run_command(cmd_args, cwd=dest) + + @classmethod + def get_remote_url(cls, location): + url = cls.run_command( + ['showconfig', 'paths.default'], + show_stdout=False, cwd=location).strip() + if cls._is_local_repository(url): + url = path_to_url(url) + return url.strip() + + @classmethod + def get_revision(cls, location): + """ + Return the repository-local changeset revision number, as an integer. + """ + current_revision = cls.run_command( + ['parents', '--template={rev}'], + show_stdout=False, cwd=location).strip() + return current_revision + + @classmethod + def get_requirement_revision(cls, location): + """ + Return the changeset identification hash, as a 40-character + hexadecimal string + """ + current_rev_hash = cls.run_command( + ['parents', '--template={node}'], + show_stdout=False, cwd=location).strip() + return current_rev_hash + + @classmethod + def is_commit_id_equal(cls, dest, name): + """Always assume the versions don't match""" + return False + + @classmethod + def get_subdirectory(cls, location): + """ + Return the path to setup.py, relative to the repo root. + Return None if setup.py is in the repo root. + """ + # find the repo root + repo_root = cls.run_command( + ['root'], show_stdout=False, cwd=location).strip() + if not os.path.isabs(repo_root): + repo_root = os.path.abspath(os.path.join(location, repo_root)) + return find_path_to_setup_from_repo_root(location, repo_root) + + @classmethod + def controls_location(cls, location): + if super(Mercurial, cls).controls_location(location): + return True + try: + cls.run_command( + ['identify'], + cwd=location, + show_stdout=False, + on_returncode='raise', + log_failed_cmd=False) + except (BadCommand, InstallationError): + return False + + +vcs.register(Mercurial) diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index 03e69f59802..4519f29f78f 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -1,687 +1,672 @@ -"""Handles all VCS (version control) support""" - -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - -from __future__ import absolute_import - -import errno -import logging -import os -import shutil -import sys - -from pip._vendor import pkg_resources -from pip._vendor.six.moves.urllib import parse as urllib_parse - -from pip._internal.exceptions import BadCommand -from pip._internal.utils.compat import samefile -from pip._internal.utils.misc import ( - ask_path_exists, - backup_dir, - display_path, - hide_url, - hide_value, - rmtree, -) -from pip._internal.utils.subprocess import call_subprocess, make_command -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from pip._internal.utils.urls import get_url_scheme - -if MYPY_CHECK_RUNNING: - from typing import ( - Any, Dict, Iterable, List, Mapping, Optional, Text, Tuple, Type, Union - ) - from pip._internal.utils.ui import SpinnerInterface - from pip._internal.utils.misc import HiddenText - from pip._internal.utils.subprocess import CommandArgs - - AuthInfo = Tuple[Optional[str], Optional[str]] - - -__all__ = ['vcs'] - - -logger = logging.getLogger(__name__) - - -def is_url(name): - # type: (Union[str, Text]) -> bool - """ - Return true if the name looks like a URL. - """ - scheme = get_url_scheme(name) - if scheme is None: - return False - return scheme in ['http', 'https', 'file', 'ftp'] + vcs.all_schemes - - -def make_vcs_requirement_url(repo_url, rev, project_name, subdir=None): - """ - Return the URL for a VCS requirement. - - Args: - repo_url: the remote VCS url, with any needed VCS prefix (e.g. "git+"). - project_name: the (unescaped) project name. - """ - egg_project_name = pkg_resources.to_filename(project_name) - req = '{}@{}#egg={}'.format(repo_url, rev, egg_project_name) - if subdir: - req += '&subdirectory={}'.format(subdir) - - return req - - -class RemoteNotFoundError(Exception): - pass - - -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, - vc_class, # type: Type[VersionControl] - rev=None, # type: Optional[str] - extra_args=None, # type: Optional[CommandArgs] - ): - # type: (...) -> None - """ - Args: - vc_class: a VersionControl subclass. - 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.vc_class = vc_class - self.branch_name = None # type: Optional[str] - - def __repr__(self): - return '<RevOptions {}: rev={!r}>'.format(self.vc_class.name, self.rev) - - @property - def arg_rev(self): - # type: () -> Optional[str] - if self.rev is None: - return self.vc_class.default_arg_rev - - return self.rev - - def to_args(self): - # type: () -> CommandArgs - """ - Return the VCS-specific command arguments. - """ - args = [] # type: CommandArgs - rev = self.arg_rev - if rev is not None: - args += self.vc_class.get_base_rev_args(rev) - args += self.extra_args - - return args - - def to_display(self): - # type: () -> str - if not self.rev: - return '' - - return ' (to revision {})'.format(self.rev) - - def make_new(self, rev): - # type: (str) -> RevOptions - """ - 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.vc_class.make_rev_options(rev, extra_args=self.extra_args) - - -class VcsSupport(object): - _registry = {} # type: Dict[str, VersionControl] - schemes = ['ssh', 'git', 'hg', 'bzr', 'sftp', 'svn'] - - def __init__(self): - # type: () -> None - # Register more schemes with urlparse for various version control - # systems - urllib_parse.uses_netloc.extend(self.schemes) - # Python >= 2.7.4, 3.3 doesn't have uses_fragment - if getattr(urllib_parse, 'uses_fragment', None): - urllib_parse.uses_fragment.extend(self.schemes) - super(VcsSupport, self).__init__() - - def __iter__(self): - return self._registry.__iter__() - - @property - def backends(self): - # type: () -> List[VersionControl] - return list(self._registry.values()) - - @property - def dirnames(self): - # type: () -> List[str] - return [backend.dirname for backend in self.backends] - - @property - def all_schemes(self): - # type: () -> List[str] - schemes = [] # type: List[str] - for backend in self.backends: - schemes.extend(backend.schemes) - return schemes - - def register(self, cls): - # type: (Type[VersionControl]) -> None - if not hasattr(cls, 'name'): - logger.warning('Cannot register VCS %s', cls.__name__) - return - if cls.name not in self._registry: - self._registry[cls.name] = cls() - logger.debug('Registered VCS backend: %s', cls.name) - - def unregister(self, name): - # type: (str) -> None - if name in self._registry: - del self._registry[name] - - def get_backend_for_dir(self, location): - # type: (str) -> Optional[VersionControl] - """ - Return a VersionControl object if a repository of that type is found - at the given directory. - """ - for vcs_backend in self._registry.values(): - if vcs_backend.controls_location(location): - logger.debug('Determine that %s uses VCS: %s', - location, vcs_backend.name) - return vcs_backend - return None - - def get_backend(self, name): - # type: (str) -> Optional[VersionControl] - """ - Return a VersionControl object or None. - """ - name = name.lower() - return self._registry.get(name) - - -vcs = VcsSupport() - - -class VersionControl(object): - name = '' - dirname = '' - repo_name = '' - # List of supported schemes for this Version Control - 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] - - @classmethod - def should_add_vcs_url_prefix(cls, remote_url): - """ - Return whether the vcs prefix (e.g. "git+") should be added to a - repository's remote url when used in a requirement. - """ - return not remote_url.lower().startswith('{}:'.format(cls.name)) - - @classmethod - def get_subdirectory(cls, location): - """ - Return the path to setup.py, relative to the repo root. - Return None if setup.py is in the repo root. - """ - # find the repo root - root_dir = cls.get_repo_root_dir(location) - if root_dir is None: - logger.warning( - "Repo root could not be detected for %s, " - "assuming it is the root.", - location) - return None - # find setup.py - orig_location = location - while not os.path.exists(os.path.join(location, 'setup.py')): - last_location = location - location = os.path.dirname(location) - if location == last_location: - # We've traversed up to the root of the filesystem without - # finding setup.py - logger.warning( - "Could not find setup.py for directory %s (tried all " - "parent directories)", - orig_location, - ) - return None - # relative path of setup.py to repo root - if samefile(root_dir, location): - return None - return os.path.relpath(location, root_dir) - - @classmethod - def get_repo_root_dir(cls, location): - """ - Return the absolute path to the repo root directory. - - Return None if not found. - This can be overridden by subclasses to interrogate the vcs tool to - find the repo root. - """ - while not cls.is_repository_directory(location): - last_location = location - location = os.path.dirname(location) - if location == last_location: - # We've traversed up to the root of the filesystem. - return None - return os.path.abspath(location) - - @classmethod - def get_requirement_revision(cls, repo_dir): - """ - Return the revision string that should be used in a requirement. - """ - return cls.get_revision(repo_dir) - - @classmethod - def get_src_requirement(cls, repo_dir, project_name): - """ - Return the requirement string to use to redownload the files - currently at the given repository directory. - - Args: - project_name: the (unescaped) project name. - - The return value has a form similar to the following: - - {repository_url}@{revision}#egg={project_name} - """ - repo_url = cls.get_remote_url(repo_dir) - if repo_url is None: - return None - - if cls.should_add_vcs_url_prefix(repo_url): - repo_url = '{}+{}'.format(cls.name, repo_url) - - revision = cls.get_requirement_revision(repo_dir) - subdir = cls.get_subdirectory(repo_dir) - req = make_vcs_requirement_url(repo_url, revision, project_name, - subdir=subdir) - - return req - - @staticmethod - def get_base_rev_args(rev): - """ - Return the base revision arguments for a vcs command. - - Args: - rev: the name of a revision to install. Cannot be None. - """ - raise NotImplementedError - - @classmethod - def make_rev_options(cls, rev=None, extra_args=None): - # type: (Optional[str], Optional[CommandArgs]) -> RevOptions - """ - Return a RevOptions object. - - Args: - rev: the name of a revision to install. - extra_args: a list of extra options. - """ - return RevOptions(cls, rev, extra_args=extra_args) - - @classmethod - def _is_local_repository(cls, repo): - # type: (str) -> bool - """ - posix absolute paths start with os.path.sep, - win32 ones start with drive (like c:\\folder) - """ - drive, tail = os.path.splitdrive(repo) - return repo.startswith(os.path.sep) or bool(drive) - - def export(self, location, url): - # type: (str, HiddenText) -> None - """ - Export the repository at the url to the destination location - i.e. only download the files, without vcs informations - - :param url: the repository URL starting with a vcs prefix. - """ - raise NotImplementedError - - @classmethod - def get_netloc_and_auth(cls, netloc, scheme): - """ - Parse the repository URL's netloc, and return the new netloc to use - along with auth information. - - Args: - netloc: the original repository URL netloc. - scheme: the repository URL's scheme without the vcs prefix. - - This is mainly for the Subversion class to override, so that auth - information can be provided via the --username and --password options - instead of through the URL. For other subclasses like Git without - such an option, auth information must stay in the URL. - - Returns: (netloc, (username, password)). - """ - return netloc, (None, None) - - @classmethod - def get_url_rev_and_auth(cls, url): - # type: (str) -> Tuple[str, Optional[str], AuthInfo] - """ - Parse the repository URL to use, and return the URL, revision, - and auth info to use. - - Returns: (url, rev, (username, password)). - """ - scheme, netloc, path, query, frag = urllib_parse.urlsplit(url) - if '+' not in scheme: - raise ValueError( - "Sorry, {!r} is a malformed VCS url. " - "The format is <vcs>+<protocol>://<url>, " - "e.g. svn+http://myrepo/svn/MyApp#egg=MyApp".format(url) - ) - # Remove the vcs prefix. - scheme = scheme.split('+', 1)[1] - netloc, user_pass = cls.get_netloc_and_auth(netloc, scheme) - rev = None - if '@' in path: - path, rev = path.rsplit('@', 1) - url = urllib_parse.urlunsplit((scheme, netloc, path, query, '')) - return url, rev, user_pass - - @staticmethod - def make_rev_args(username, password): - # type: (Optional[str], Optional[HiddenText]) -> CommandArgs - """ - Return the RevOptions "extra arguments" to use in obtain(). - """ - return [] - - def get_url_rev_options(self, url): - # type: (HiddenText) -> Tuple[HiddenText, RevOptions] - """ - Return the URL and RevOptions object to use in obtain() and in - some cases export(), as a tuple (url, rev_options). - """ - secret_url, rev, user_pass = self.get_url_rev_and_auth(url.secret) - username, secret_password = user_pass - password = None # type: Optional[HiddenText] - if secret_password is not None: - password = hide_value(secret_password) - extra_args = self.make_rev_args(username, password) - rev_options = self.make_rev_options(rev, extra_args=extra_args) - - return hide_url(secret_url), rev_options - - @staticmethod - def normalize_url(url): - # type: (str) -> str - """ - Normalize a URL for comparison by unquoting it and removing any - trailing slash. - """ - return urllib_parse.unquote(url).rstrip('/') - - @classmethod - def compare_urls(cls, url1, url2): - # type: (str, str) -> bool - """ - Compare two repo URLs for identity, ignoring incidental differences. - """ - return (cls.normalize_url(url1) == cls.normalize_url(url2)) - - def fetch_new(self, dest, url, rev_options): - # type: (str, HiddenText, RevOptions) -> None - """ - Fetch a revision from a repository, in the case that this is the - first fetch from the repository. - - Args: - dest: the directory to fetch the repository to. - rev_options: a RevOptions object. - """ - raise NotImplementedError - - def switch(self, dest, url, rev_options): - # type: (str, HiddenText, RevOptions) -> None - """ - Switch the repo at ``dest`` to point to ``URL``. - - Args: - rev_options: a RevOptions object. - """ - raise NotImplementedError - - def update(self, dest, url, rev_options): - # type: (str, HiddenText, RevOptions) -> None - """ - Update an already-existing repo to the given ``rev_options``. - - Args: - rev_options: a RevOptions object. - """ - raise NotImplementedError - - @classmethod - def is_commit_id_equal(cls, dest, name): - """ - Return whether the id of the current commit equals the given name. - - Args: - dest: the repository directory. - name: a string name. - """ - raise NotImplementedError - - def obtain(self, dest, url): - # type: (str, HiddenText) -> None - """ - Install or update in editable mode the package represented by this - VersionControl object. - - :param dest: the repository directory in which to install or update. - :param url: the repository URL starting with a vcs prefix. - """ - url, rev_options = self.get_url_rev_options(url) - - if not os.path.exists(dest): - self.fetch_new(dest, url, rev_options) - return - - rev_display = rev_options.to_display() - if self.is_repository_directory(dest): - existing_url = self.get_remote_url(dest) - if self.compare_urls(existing_url, url.secret): - logger.debug( - '%s in %s exists, and has correct URL (%s)', - self.repo_name.title(), - display_path(dest), - url, - ) - if not self.is_commit_id_equal(dest, rev_options.rev): - logger.info( - 'Updating %s %s%s', - display_path(dest), - self.repo_name, - rev_display, - ) - self.update(dest, url, rev_options) - else: - logger.info('Skipping because already up-to-date.') - return - - logger.warning( - '%s %s in %s exists with URL %s', - self.name, - self.repo_name, - display_path(dest), - existing_url, - ) - prompt = ('(s)witch, (i)gnore, (w)ipe, (b)ackup ', - ('s', 'i', 'w', 'b')) - else: - logger.warning( - 'Directory %s already exists, and is not a %s %s.', - dest, - self.name, - self.repo_name, - ) - # https://github.com/python/mypy/issues/1174 - prompt = ('(i)gnore, (w)ipe, (b)ackup ', # type: ignore - ('i', 'w', 'b')) - - logger.warning( - 'The plan is to install the %s repository %s', - self.name, - url, - ) - response = ask_path_exists('What to do? %s' % prompt[0], prompt[1]) - - if response == 'a': - sys.exit(-1) - - if response == 'w': - logger.warning('Deleting %s', display_path(dest)) - rmtree(dest) - self.fetch_new(dest, url, rev_options) - return - - if response == 'b': - dest_dir = backup_dir(dest) - logger.warning( - 'Backing up %s to %s', display_path(dest), dest_dir, - ) - shutil.move(dest, dest_dir) - self.fetch_new(dest, url, rev_options) - return - - # Do nothing if the response is "i". - if response == 's': - logger.info( - 'Switching %s %s to %s%s', - self.repo_name, - display_path(dest), - url, - rev_display, - ) - self.switch(dest, url, rev_options) - - def unpack(self, location, url): - # type: (str, HiddenText) -> None - """ - Clean up current location and download the url repository - (and vcs infos) into location - - :param url: the repository URL starting with a vcs prefix. - """ - if os.path.exists(location): - rmtree(location) - self.obtain(location, url=url) - - @classmethod - def get_remote_url(cls, location): - """ - Return the url used at location - - Raises RemoteNotFoundError if the repository does not have a remote - url configured. - """ - raise NotImplementedError - - @classmethod - def get_revision(cls, location): - """ - Return the current commit id of the files at the given location. - """ - raise NotImplementedError - - @classmethod - def run_command( - cls, - cmd, # type: Union[List[str], CommandArgs] - show_stdout=True, # type: bool - cwd=None, # type: Optional[str] - on_returncode='raise', # type: str - extra_ok_returncodes=None, # type: Optional[Iterable[int]] - command_desc=None, # type: Optional[str] - extra_environ=None, # type: Optional[Mapping[str, Any]] - spinner=None, # type: Optional[SpinnerInterface] - log_failed_cmd=True - ): - # type: (...) -> Text - """ - Run a VCS subcommand - This is simply a wrapper around call_subprocess that adds the VCS - command name, and checks that the VCS is available - """ - cmd = make_command(cls.name, *cmd) - try: - return call_subprocess(cmd, show_stdout, cwd, - on_returncode=on_returncode, - extra_ok_returncodes=extra_ok_returncodes, - command_desc=command_desc, - extra_environ=extra_environ, - unset_environ=cls.unset_environ, - spinner=spinner, - log_failed_cmd=log_failed_cmd) - 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 - do you have ' - '%r installed and in your ' - 'PATH?' % (cls.name, cls.name)) - else: - raise # re-raise exception if a different error occurred - - @classmethod - def is_repository_directory(cls, path): - # type: (str) -> bool - """ - Return whether a directory path is a repository directory. - """ - logger.debug('Checking in %s for %s (%s)...', - path, cls.dirname, cls.name) - return os.path.exists(os.path.join(path, cls.dirname)) - - @classmethod - def controls_location(cls, location): - # type: (str) -> bool - """ - Check if a location is controlled by the vcs. - - Searches up the filesystem and checks is_repository_directory(). - - It is meant to be extended to add smarter detection mechanisms for - specific vcs. For example, the Git override checks that Git is - actually available. - """ - while not cls.is_repository_directory(location): - last_location = location - location = os.path.dirname(location) - if location == last_location: - # We've traversed up to the root of the filesystem. - return False - return True +"""Handles all VCS (version control) support""" + +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + +from __future__ import absolute_import + +import errno +import logging +import os +import shutil +import sys + +from pip._vendor import pkg_resources +from pip._vendor.six.moves.urllib import parse as urllib_parse + +from pip._internal.exceptions import BadCommand +from pip._internal.utils.compat import samefile +from pip._internal.utils.misc import ( + ask_path_exists, + backup_dir, + display_path, + hide_url, + hide_value, + rmtree, +) +from pip._internal.utils.subprocess import call_subprocess, make_command +from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from pip._internal.utils.urls import get_url_scheme + +if MYPY_CHECK_RUNNING: + from typing import ( + Any, Dict, Iterable, List, Mapping, Optional, Text, Tuple, Type, Union + ) + from pip._internal.utils.ui import SpinnerInterface + from pip._internal.utils.misc import HiddenText + from pip._internal.utils.subprocess import CommandArgs + + AuthInfo = Tuple[Optional[str], Optional[str]] + + +__all__ = ['vcs'] + + +logger = logging.getLogger(__name__) + + +def is_url(name): + # type: (Union[str, Text]) -> bool + """ + Return true if the name looks like a URL. + """ + scheme = get_url_scheme(name) + if scheme is None: + return False + return scheme in ['http', 'https', 'file', 'ftp'] + vcs.all_schemes + + +def make_vcs_requirement_url(repo_url, rev, project_name, subdir=None): + """ + Return the URL for a VCS requirement. + + Args: + repo_url: the remote VCS url, with any needed VCS prefix (e.g. "git+"). + project_name: the (unescaped) project name. + """ + egg_project_name = pkg_resources.to_filename(project_name) + req = '{}@{}#egg={}'.format(repo_url, rev, egg_project_name) + if subdir: + req += '&subdirectory={}'.format(subdir) + + return req + + +def find_path_to_setup_from_repo_root(location, repo_root): + """ + Find the path to `setup.py` by searching up the filesystem from `location`. + Return the path to `setup.py` relative to `repo_root`. + Return None if `setup.py` is in `repo_root` or cannot be found. + """ + # find setup.py + orig_location = location + while not os.path.exists(os.path.join(location, 'setup.py')): + last_location = location + location = os.path.dirname(location) + if location == last_location: + # We've traversed up to the root of the filesystem without + # finding setup.py + logger.warning( + "Could not find setup.py for directory %s (tried all " + "parent directories)", + orig_location, + ) + return None + + if samefile(repo_root, location): + return None + + return os.path.relpath(location, repo_root) + + +class RemoteNotFoundError(Exception): + pass + + +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, + vc_class, # type: Type[VersionControl] + rev=None, # type: Optional[str] + extra_args=None, # type: Optional[CommandArgs] + ): + # type: (...) -> None + """ + Args: + vc_class: a VersionControl subclass. + 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.vc_class = vc_class + self.branch_name = None # type: Optional[str] + + def __repr__(self): + return '<RevOptions {}: rev={!r}>'.format(self.vc_class.name, self.rev) + + @property + def arg_rev(self): + # type: () -> Optional[str] + if self.rev is None: + return self.vc_class.default_arg_rev + + return self.rev + + def to_args(self): + # type: () -> CommandArgs + """ + Return the VCS-specific command arguments. + """ + args = [] # type: CommandArgs + rev = self.arg_rev + if rev is not None: + args += self.vc_class.get_base_rev_args(rev) + args += self.extra_args + + return args + + def to_display(self): + # type: () -> str + if not self.rev: + return '' + + return ' (to revision {})'.format(self.rev) + + def make_new(self, rev): + # type: (str) -> RevOptions + """ + 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.vc_class.make_rev_options(rev, extra_args=self.extra_args) + + +class VcsSupport(object): + _registry = {} # type: Dict[str, VersionControl] + schemes = ['ssh', 'git', 'hg', 'bzr', 'sftp', 'svn'] + + def __init__(self): + # type: () -> None + # Register more schemes with urlparse for various version control + # systems + urllib_parse.uses_netloc.extend(self.schemes) + # Python >= 2.7.4, 3.3 doesn't have uses_fragment + if getattr(urllib_parse, 'uses_fragment', None): + urllib_parse.uses_fragment.extend(self.schemes) + super(VcsSupport, self).__init__() + + def __iter__(self): + return self._registry.__iter__() + + @property + def backends(self): + # type: () -> List[VersionControl] + return list(self._registry.values()) + + @property + def dirnames(self): + # type: () -> List[str] + return [backend.dirname for backend in self.backends] + + @property + def all_schemes(self): + # type: () -> List[str] + schemes = [] # type: List[str] + for backend in self.backends: + schemes.extend(backend.schemes) + return schemes + + def register(self, cls): + # type: (Type[VersionControl]) -> None + if not hasattr(cls, 'name'): + logger.warning('Cannot register VCS %s', cls.__name__) + return + if cls.name not in self._registry: + self._registry[cls.name] = cls() + logger.debug('Registered VCS backend: %s', cls.name) + + def unregister(self, name): + # type: (str) -> None + if name in self._registry: + del self._registry[name] + + def get_backend_for_dir(self, location): + # type: (str) -> Optional[VersionControl] + """ + Return a VersionControl object if a repository of that type is found + at the given directory. + """ + for vcs_backend in self._registry.values(): + if vcs_backend.controls_location(location): + logger.debug('Determine that %s uses VCS: %s', + location, vcs_backend.name) + return vcs_backend + return None + + def get_backend(self, name): + # type: (str) -> Optional[VersionControl] + """ + Return a VersionControl object or None. + """ + name = name.lower() + return self._registry.get(name) + + +vcs = VcsSupport() + + +class VersionControl(object): + name = '' + dirname = '' + repo_name = '' + # List of supported schemes for this Version Control + 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] + + @classmethod + def should_add_vcs_url_prefix(cls, remote_url): + """ + Return whether the vcs prefix (e.g. "git+") should be added to a + repository's remote url when used in a requirement. + """ + return not remote_url.lower().startswith('{}:'.format(cls.name)) + + @classmethod + def get_subdirectory(cls, location): + """ + Return the path to setup.py, relative to the repo root. + Return None if setup.py is in the repo root. + """ + return None + + @classmethod + def get_requirement_revision(cls, repo_dir): + """ + Return the revision string that should be used in a requirement. + """ + return cls.get_revision(repo_dir) + + @classmethod + def get_src_requirement(cls, repo_dir, project_name): + """ + Return the requirement string to use to redownload the files + currently at the given repository directory. + + Args: + project_name: the (unescaped) project name. + + The return value has a form similar to the following: + + {repository_url}@{revision}#egg={project_name} + """ + repo_url = cls.get_remote_url(repo_dir) + if repo_url is None: + return None + + if cls.should_add_vcs_url_prefix(repo_url): + repo_url = '{}+{}'.format(cls.name, repo_url) + + revision = cls.get_requirement_revision(repo_dir) + subdir = cls.get_subdirectory(repo_dir) + req = make_vcs_requirement_url(repo_url, revision, project_name, + subdir=subdir) + + return req + + @staticmethod + def get_base_rev_args(rev): + """ + Return the base revision arguments for a vcs command. + + Args: + rev: the name of a revision to install. Cannot be None. + """ + raise NotImplementedError + + @classmethod + def make_rev_options(cls, rev=None, extra_args=None): + # type: (Optional[str], Optional[CommandArgs]) -> RevOptions + """ + Return a RevOptions object. + + Args: + rev: the name of a revision to install. + extra_args: a list of extra options. + """ + return RevOptions(cls, rev, extra_args=extra_args) + + @classmethod + def _is_local_repository(cls, repo): + # type: (str) -> bool + """ + posix absolute paths start with os.path.sep, + win32 ones start with drive (like c:\\folder) + """ + drive, tail = os.path.splitdrive(repo) + return repo.startswith(os.path.sep) or bool(drive) + + def export(self, location, url): + # type: (str, HiddenText) -> None + """ + Export the repository at the url to the destination location + i.e. only download the files, without vcs informations + + :param url: the repository URL starting with a vcs prefix. + """ + raise NotImplementedError + + @classmethod + def get_netloc_and_auth(cls, netloc, scheme): + """ + Parse the repository URL's netloc, and return the new netloc to use + along with auth information. + + Args: + netloc: the original repository URL netloc. + scheme: the repository URL's scheme without the vcs prefix. + + This is mainly for the Subversion class to override, so that auth + information can be provided via the --username and --password options + instead of through the URL. For other subclasses like Git without + such an option, auth information must stay in the URL. + + Returns: (netloc, (username, password)). + """ + return netloc, (None, None) + + @classmethod + def get_url_rev_and_auth(cls, url): + # type: (str) -> Tuple[str, Optional[str], AuthInfo] + """ + Parse the repository URL to use, and return the URL, revision, + and auth info to use. + + Returns: (url, rev, (username, password)). + """ + scheme, netloc, path, query, frag = urllib_parse.urlsplit(url) + if '+' not in scheme: + raise ValueError( + "Sorry, {!r} is a malformed VCS url. " + "The format is <vcs>+<protocol>://<url>, " + "e.g. svn+http://myrepo/svn/MyApp#egg=MyApp".format(url) + ) + # Remove the vcs prefix. + scheme = scheme.split('+', 1)[1] + netloc, user_pass = cls.get_netloc_and_auth(netloc, scheme) + rev = None + if '@' in path: + path, rev = path.rsplit('@', 1) + url = urllib_parse.urlunsplit((scheme, netloc, path, query, '')) + return url, rev, user_pass + + @staticmethod + def make_rev_args(username, password): + # type: (Optional[str], Optional[HiddenText]) -> CommandArgs + """ + Return the RevOptions "extra arguments" to use in obtain(). + """ + return [] + + def get_url_rev_options(self, url): + # type: (HiddenText) -> Tuple[HiddenText, RevOptions] + """ + Return the URL and RevOptions object to use in obtain() and in + some cases export(), as a tuple (url, rev_options). + """ + secret_url, rev, user_pass = self.get_url_rev_and_auth(url.secret) + username, secret_password = user_pass + password = None # type: Optional[HiddenText] + if secret_password is not None: + password = hide_value(secret_password) + extra_args = self.make_rev_args(username, password) + rev_options = self.make_rev_options(rev, extra_args=extra_args) + + return hide_url(secret_url), rev_options + + @staticmethod + def normalize_url(url): + # type: (str) -> str + """ + Normalize a URL for comparison by unquoting it and removing any + trailing slash. + """ + return urllib_parse.unquote(url).rstrip('/') + + @classmethod + def compare_urls(cls, url1, url2): + # type: (str, str) -> bool + """ + Compare two repo URLs for identity, ignoring incidental differences. + """ + return (cls.normalize_url(url1) == cls.normalize_url(url2)) + + def fetch_new(self, dest, url, rev_options): + # type: (str, HiddenText, RevOptions) -> None + """ + Fetch a revision from a repository, in the case that this is the + first fetch from the repository. + + Args: + dest: the directory to fetch the repository to. + rev_options: a RevOptions object. + """ + raise NotImplementedError + + def switch(self, dest, url, rev_options): + # type: (str, HiddenText, RevOptions) -> None + """ + Switch the repo at ``dest`` to point to ``URL``. + + Args: + rev_options: a RevOptions object. + """ + raise NotImplementedError + + def update(self, dest, url, rev_options): + # type: (str, HiddenText, RevOptions) -> None + """ + Update an already-existing repo to the given ``rev_options``. + + Args: + rev_options: a RevOptions object. + """ + raise NotImplementedError + + @classmethod + def is_commit_id_equal(cls, dest, name): + """ + Return whether the id of the current commit equals the given name. + + Args: + dest: the repository directory. + name: a string name. + """ + raise NotImplementedError + + def obtain(self, dest, url): + # type: (str, HiddenText) -> None + """ + Install or update in editable mode the package represented by this + VersionControl object. + + :param dest: the repository directory in which to install or update. + :param url: the repository URL starting with a vcs prefix. + """ + url, rev_options = self.get_url_rev_options(url) + + if not os.path.exists(dest): + self.fetch_new(dest, url, rev_options) + return + + rev_display = rev_options.to_display() + if self.is_repository_directory(dest): + existing_url = self.get_remote_url(dest) + if self.compare_urls(existing_url, url.secret): + logger.debug( + '%s in %s exists, and has correct URL (%s)', + self.repo_name.title(), + display_path(dest), + url, + ) + if not self.is_commit_id_equal(dest, rev_options.rev): + logger.info( + 'Updating %s %s%s', + display_path(dest), + self.repo_name, + rev_display, + ) + self.update(dest, url, rev_options) + else: + logger.info('Skipping because already up-to-date.') + return + + logger.warning( + '%s %s in %s exists with URL %s', + self.name, + self.repo_name, + display_path(dest), + existing_url, + ) + prompt = ('(s)witch, (i)gnore, (w)ipe, (b)ackup ', + ('s', 'i', 'w', 'b')) + else: + logger.warning( + 'Directory %s already exists, and is not a %s %s.', + dest, + self.name, + self.repo_name, + ) + # https://github.com/python/mypy/issues/1174 + prompt = ('(i)gnore, (w)ipe, (b)ackup ', # type: ignore + ('i', 'w', 'b')) + + logger.warning( + 'The plan is to install the %s repository %s', + self.name, + url, + ) + response = ask_path_exists('What to do? %s' % prompt[0], prompt[1]) + + if response == 'a': + sys.exit(-1) + + if response == 'w': + logger.warning('Deleting %s', display_path(dest)) + rmtree(dest) + self.fetch_new(dest, url, rev_options) + return + + if response == 'b': + dest_dir = backup_dir(dest) + logger.warning( + 'Backing up %s to %s', display_path(dest), dest_dir, + ) + shutil.move(dest, dest_dir) + self.fetch_new(dest, url, rev_options) + return + + # Do nothing if the response is "i". + if response == 's': + logger.info( + 'Switching %s %s to %s%s', + self.repo_name, + display_path(dest), + url, + rev_display, + ) + self.switch(dest, url, rev_options) + + def unpack(self, location, url): + # type: (str, HiddenText) -> None + """ + Clean up current location and download the url repository + (and vcs infos) into location + + :param url: the repository URL starting with a vcs prefix. + """ + if os.path.exists(location): + rmtree(location) + self.obtain(location, url=url) + + @classmethod + def get_remote_url(cls, location): + """ + Return the url used at location + + Raises RemoteNotFoundError if the repository does not have a remote + url configured. + """ + raise NotImplementedError + + @classmethod + def get_revision(cls, location): + """ + Return the current commit id of the files at the given location. + """ + raise NotImplementedError + + @classmethod + def run_command( + cls, + cmd, # type: Union[List[str], CommandArgs] + show_stdout=True, # type: bool + cwd=None, # type: Optional[str] + on_returncode='raise', # type: str + extra_ok_returncodes=None, # type: Optional[Iterable[int]] + command_desc=None, # type: Optional[str] + extra_environ=None, # type: Optional[Mapping[str, Any]] + spinner=None, # type: Optional[SpinnerInterface] + log_failed_cmd=True + ): + # type: (...) -> Text + """ + Run a VCS subcommand + This is simply a wrapper around call_subprocess that adds the VCS + command name, and checks that the VCS is available + """ + cmd = make_command(cls.name, *cmd) + try: + return call_subprocess(cmd, show_stdout, cwd, + on_returncode=on_returncode, + extra_ok_returncodes=extra_ok_returncodes, + command_desc=command_desc, + extra_environ=extra_environ, + unset_environ=cls.unset_environ, + spinner=spinner, + log_failed_cmd=log_failed_cmd) + 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 - do you have ' + '%r installed and in your ' + 'PATH?' % (cls.name, cls.name)) + else: + raise # re-raise exception if a different error occurred + + @classmethod + def is_repository_directory(cls, path): + # type: (str) -> bool + """ + Return whether a directory path is a repository directory. + """ + logger.debug('Checking in %s for %s (%s)...', + path, cls.dirname, cls.name) + return os.path.exists(os.path.join(path, cls.dirname)) + + @classmethod + def controls_location(cls, location): + # type: (str) -> bool + """ + Check if a location is controlled by the vcs. + + Searches up the filesystem and checks is_repository_directory(). + + It is meant to be extended to add smarter detection mechanisms for + specific vcs. For example, the Git override checks that Git is + actually available. + """ + while not cls.is_repository_directory(location): + last_location = location + location = os.path.dirname(location) + if location == last_location: + # We've traversed up to the root of the filesystem. + return False + return True From f197479dc8b496e236b1db5321a79be97a37bfdf Mon Sep 17 00:00:00 2001 From: tbeswick <tbeswick@enphaseenergy.com> Date: Fri, 11 Oct 2019 14:30:30 +1300 Subject: [PATCH 0522/3170] Fixed LF getting converted to CRLF in last commit. --- src/pip/_internal/vcs/git.py | 744 ++++++------- src/pip/_internal/vcs/mercurial.py | 308 +++--- src/pip/_internal/vcs/versioncontrol.py | 1344 +++++++++++------------ 3 files changed, 1198 insertions(+), 1198 deletions(-) diff --git a/src/pip/_internal/vcs/git.py b/src/pip/_internal/vcs/git.py index 6b804114002..92b84571406 100644 --- a/src/pip/_internal/vcs/git.py +++ b/src/pip/_internal/vcs/git.py @@ -1,372 +1,372 @@ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - -from __future__ import absolute_import - -import logging -import os.path -import re - -from pip._vendor.packaging.version import parse as parse_version -from pip._vendor.six.moves.urllib import parse as urllib_parse -from pip._vendor.six.moves.urllib import request as urllib_request - -from pip._internal.exceptions import BadCommand -from pip._internal.utils.misc import display_path -from pip._internal.utils.subprocess import make_command -from pip._internal.utils.temp_dir import TempDirectory -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from pip._internal.vcs.versioncontrol import ( - RemoteNotFoundError, - VersionControl, - find_path_to_setup_from_repo_root, - vcs, -) - -if MYPY_CHECK_RUNNING: - from typing import Optional, Tuple - from pip._internal.utils.misc import HiddenText - from pip._internal.vcs.versioncontrol import AuthInfo, RevOptions - - -urlsplit = urllib_parse.urlsplit -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' - repo_name = 'clone' - 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' - - @staticmethod - def get_base_rev_args(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):].split()[0] - else: - version = '' - # get first 3 positions of the git version because - # on windows it is x.y.z.windows.t, and this parses as - # LegacyVersion which always smaller than a Version. - version = '.'.join(version.split('.')[:3]) - return parse_version(version) - - @classmethod - def get_current_branch(cls, location): - """ - Return the current branch, or None if HEAD isn't at a branch - (e.g. detached HEAD). - """ - # git-symbolic-ref exits with empty stdout if "HEAD" is a detached - # HEAD rather than a symbolic ref. In addition, the -q causes the - # command to exit with status code 1 instead of 128 in this case - # and to suppress the message to stderr. - args = ['symbolic-ref', '-q', 'HEAD'] - output = cls.run_command( - args, extra_ok_returncodes=(1, ), show_stdout=False, cwd=location, - ) - ref = output.strip() - - if ref.startswith('refs/heads/'): - return ref[len('refs/heads/'):] - - return None - - def export(self, location, url): - # type: (str, HiddenText) -> None - """Export the Git repository at the url to the destination location""" - if not location.endswith('/'): - location = location + '/' - - with TempDirectory(kind="export") as temp_dir: - self.unpack(temp_dir.path, url=url) - self.run_command( - ['checkout-index', '-a', '-f', '--prefix', location], - show_stdout=False, cwd=temp_dir.path - ) - - @classmethod - def get_revision_sha(cls, dest, rev): - """ - Return (sha_or_none, is_branch), where sha_or_none is a commit hash - if the revision names a remote branch or tag, otherwise None. - - Args: - dest: the repository directory. - rev: the revision name. - """ - # Pass rev to pre-filter the list. - output = cls.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) - - sha = refs.get(branch_ref) - if sha is not None: - return (sha, True) - - sha = refs.get(tag_ref) - - return (sha, False) - - @classmethod - def resolve_revision(cls, dest, url, rev_options): - # type: (str, HiddenText, RevOptions) -> RevOptions - """ - Resolve a revision to a new RevOptions object with the SHA1 of the - branch, tag, or ref if found. - - Args: - rev_options: a RevOptions object. - """ - rev = rev_options.arg_rev - # The arg_rev property's implementation for Git ensures that the - # rev return value is always non-None. - assert rev is not None - - sha, is_branch = cls.get_revision_sha(dest, rev) - - if sha is not None: - rev_options = rev_options.make_new(sha) - rev_options.branch_name = rev if is_branch else None - - return rev_options - - # 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, - ) - - if not rev.startswith('refs/'): - return rev_options - - # If it looks like a ref, we have to fetch it explicitly. - cls.run_command( - make_command('fetch', '-q', url, rev_options.to_args()), - cwd=dest, - ) - # Change the revision to the SHA of the ref we fetched - sha = cls.get_revision(dest, rev='FETCH_HEAD') - rev_options = rev_options.make_new(sha) - - return rev_options - - @classmethod - def is_commit_id_equal(cls, 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 cls.get_revision(dest) == name - - def fetch_new(self, dest, url, rev_options): - # type: (str, HiddenText, RevOptions) -> None - rev_display = rev_options.to_display() - logger.info('Cloning %s%s to %s', url, rev_display, display_path(dest)) - self.run_command(make_command('clone', '-q', url, dest)) - - if rev_options.rev: - # Then a specific revision was requested. - rev_options = self.resolve_revision(dest, url, rev_options) - branch_name = getattr(rev_options, 'branch_name', None) - if branch_name is None: - # 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): - cmd_args = make_command( - 'checkout', '-q', rev_options.to_args(), - ) - self.run_command(cmd_args, cwd=dest) - elif self.get_current_branch(dest) != branch_name: - # Then a specific branch was requested, and that branch - # is not yet checked out. - track_branch = 'origin/{}'.format(branch_name) - cmd_args = [ - 'checkout', '-b', branch_name, '--track', track_branch, - ] - self.run_command(cmd_args, cwd=dest) - - #: repo may contain submodules - self.update_submodules(dest) - - def switch(self, dest, url, rev_options): - # type: (str, HiddenText, RevOptions) -> None - self.run_command( - make_command('config', 'remote.origin.url', url), - cwd=dest, - ) - cmd_args = make_command('checkout', '-q', rev_options.to_args()) - self.run_command(cmd_args, cwd=dest) - - self.update_submodules(dest) - - def update(self, dest, url, rev_options): - # type: (str, HiddenText, RevOptions) -> None - # First fetch changes from the default remote - if self.get_git_version() >= parse_version('1.9.0'): - # fetch tags in addition to everything else - self.run_command(['fetch', '-q', '--tags'], cwd=dest) - else: - self.run_command(['fetch', '-q'], cwd=dest) - # Then reset to wanted revision (maybe even origin/master) - rev_options = self.resolve_revision(dest, url, rev_options) - cmd_args = make_command('reset', '--hard', '-q', rev_options.to_args()) - self.run_command(cmd_args, cwd=dest) - #: update submodules - self.update_submodules(dest) - - @classmethod - def get_remote_url(cls, location): - """ - Return URL of the first remote encountered. - - Raises RemoteNotFoundError if the repository does not have a remote - url configured. - """ - # We need to pass 1 for extra_ok_returncodes since the command - # exits with return code 1 if there are no matching lines. - stdout = cls.run_command( - ['config', '--get-regexp', r'remote\..*\.url'], - extra_ok_returncodes=(1, ), show_stdout=False, cwd=location, - ) - remotes = stdout.splitlines() - try: - found_remote = remotes[0] - except IndexError: - raise RemoteNotFoundError - - for remote in remotes: - if remote.startswith('remote.origin.url '): - found_remote = remote - break - url = found_remote.split(' ')[1] - return url.strip() - - @classmethod - def get_revision(cls, location, rev=None): - if rev is None: - rev = 'HEAD' - current_rev = cls.run_command( - ['rev-parse', rev], show_stdout=False, cwd=location, - ) - return current_rev.strip() - - @classmethod - def get_subdirectory(cls, location): - """ - Return the path to setup.py, relative to the repo root. - Return None if setup.py is in the repo root. - """ - # find the repo root - git_dir = cls.run_command( - ['rev-parse', '--git-dir'], - show_stdout=False, cwd=location).strip() - if not os.path.isabs(git_dir): - git_dir = os.path.join(location, git_dir) - repo_root = os.path.abspath(os.path.join(git_dir, '..')) - return find_path_to_setup_from_repo_root(location, repo_root) - - @classmethod - def get_url_rev_and_auth(cls, url): - # type: (str) -> Tuple[str, Optional[str], AuthInfo] - """ - Prefixes stub URLs like 'user@hostname:user/repo.git' with 'ssh://'. - That's required because although they use SSH they sometimes don't - work with a ssh:// scheme (e.g. GitHub). But we need a scheme for - parsing. Hence we remove it again afterwards and return it as a stub. - """ - # Works around an apparent Git bug - # (see https://article.gmane.org/gmane.comp.version-control.git/146500) - scheme, netloc, path, query, fragment = urlsplit(url) - if scheme.endswith('file'): - initial_slashes = path[:-len(path.lstrip('/'))] - newpath = ( - initial_slashes + - urllib_request.url2pathname(path) - .replace('\\', '/').lstrip('/') - ) - url = urlunsplit((scheme, netloc, newpath, query, fragment)) - after_plus = scheme.find('+') + 1 - url = scheme[:after_plus] + urlunsplit( - (scheme[after_plus:], netloc, newpath, query, fragment), - ) - - if '://' not in url: - assert 'file:' not in url - url = url.replace('git+', 'git+ssh://') - url, rev, user_pass = super(Git, cls).get_url_rev_and_auth(url) - url = url.replace('ssh://', '') - else: - url, rev, user_pass = super(Git, cls).get_url_rev_and_auth(url) - - return url, rev, user_pass - - @classmethod - def update_submodules(cls, location): - if not os.path.exists(os.path.join(location, '.gitmodules')): - return - cls.run_command( - ['submodule', 'update', '--init', '--recursive', '-q'], - cwd=location, - ) - - @classmethod - def controls_location(cls, location): - if super(Git, cls).controls_location(location): - return True - try: - r = cls.run_command(['rev-parse'], - cwd=location, - show_stdout=False, - on_returncode='ignore', - log_failed_cmd=False) - return not r - except BadCommand: - logger.debug("could not determine if %s is under git control " - "because git is not available", location) - return False - - -vcs.register(Git) +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + +from __future__ import absolute_import + +import logging +import os.path +import re + +from pip._vendor.packaging.version import parse as parse_version +from pip._vendor.six.moves.urllib import parse as urllib_parse +from pip._vendor.six.moves.urllib import request as urllib_request + +from pip._internal.exceptions import BadCommand +from pip._internal.utils.misc import display_path +from pip._internal.utils.subprocess import make_command +from pip._internal.utils.temp_dir import TempDirectory +from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from pip._internal.vcs.versioncontrol import ( + RemoteNotFoundError, + VersionControl, + find_path_to_setup_from_repo_root, + vcs, +) + +if MYPY_CHECK_RUNNING: + from typing import Optional, Tuple + from pip._internal.utils.misc import HiddenText + from pip._internal.vcs.versioncontrol import AuthInfo, RevOptions + + +urlsplit = urllib_parse.urlsplit +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' + repo_name = 'clone' + 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' + + @staticmethod + def get_base_rev_args(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):].split()[0] + else: + version = '' + # get first 3 positions of the git version because + # on windows it is x.y.z.windows.t, and this parses as + # LegacyVersion which always smaller than a Version. + version = '.'.join(version.split('.')[:3]) + return parse_version(version) + + @classmethod + def get_current_branch(cls, location): + """ + Return the current branch, or None if HEAD isn't at a branch + (e.g. detached HEAD). + """ + # git-symbolic-ref exits with empty stdout if "HEAD" is a detached + # HEAD rather than a symbolic ref. In addition, the -q causes the + # command to exit with status code 1 instead of 128 in this case + # and to suppress the message to stderr. + args = ['symbolic-ref', '-q', 'HEAD'] + output = cls.run_command( + args, extra_ok_returncodes=(1, ), show_stdout=False, cwd=location, + ) + ref = output.strip() + + if ref.startswith('refs/heads/'): + return ref[len('refs/heads/'):] + + return None + + def export(self, location, url): + # type: (str, HiddenText) -> None + """Export the Git repository at the url to the destination location""" + if not location.endswith('/'): + location = location + '/' + + with TempDirectory(kind="export") as temp_dir: + self.unpack(temp_dir.path, url=url) + self.run_command( + ['checkout-index', '-a', '-f', '--prefix', location], + show_stdout=False, cwd=temp_dir.path + ) + + @classmethod + def get_revision_sha(cls, dest, rev): + """ + Return (sha_or_none, is_branch), where sha_or_none is a commit hash + if the revision names a remote branch or tag, otherwise None. + + Args: + dest: the repository directory. + rev: the revision name. + """ + # Pass rev to pre-filter the list. + output = cls.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) + + sha = refs.get(branch_ref) + if sha is not None: + return (sha, True) + + sha = refs.get(tag_ref) + + return (sha, False) + + @classmethod + def resolve_revision(cls, dest, url, rev_options): + # type: (str, HiddenText, RevOptions) -> RevOptions + """ + Resolve a revision to a new RevOptions object with the SHA1 of the + branch, tag, or ref if found. + + Args: + rev_options: a RevOptions object. + """ + rev = rev_options.arg_rev + # The arg_rev property's implementation for Git ensures that the + # rev return value is always non-None. + assert rev is not None + + sha, is_branch = cls.get_revision_sha(dest, rev) + + if sha is not None: + rev_options = rev_options.make_new(sha) + rev_options.branch_name = rev if is_branch else None + + return rev_options + + # 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, + ) + + if not rev.startswith('refs/'): + return rev_options + + # If it looks like a ref, we have to fetch it explicitly. + cls.run_command( + make_command('fetch', '-q', url, rev_options.to_args()), + cwd=dest, + ) + # Change the revision to the SHA of the ref we fetched + sha = cls.get_revision(dest, rev='FETCH_HEAD') + rev_options = rev_options.make_new(sha) + + return rev_options + + @classmethod + def is_commit_id_equal(cls, 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 cls.get_revision(dest) == name + + def fetch_new(self, dest, url, rev_options): + # type: (str, HiddenText, RevOptions) -> None + rev_display = rev_options.to_display() + logger.info('Cloning %s%s to %s', url, rev_display, display_path(dest)) + self.run_command(make_command('clone', '-q', url, dest)) + + if rev_options.rev: + # Then a specific revision was requested. + rev_options = self.resolve_revision(dest, url, rev_options) + branch_name = getattr(rev_options, 'branch_name', None) + if branch_name is None: + # 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): + cmd_args = make_command( + 'checkout', '-q', rev_options.to_args(), + ) + self.run_command(cmd_args, cwd=dest) + elif self.get_current_branch(dest) != branch_name: + # Then a specific branch was requested, and that branch + # is not yet checked out. + track_branch = 'origin/{}'.format(branch_name) + cmd_args = [ + 'checkout', '-b', branch_name, '--track', track_branch, + ] + self.run_command(cmd_args, cwd=dest) + + #: repo may contain submodules + self.update_submodules(dest) + + def switch(self, dest, url, rev_options): + # type: (str, HiddenText, RevOptions) -> None + self.run_command( + make_command('config', 'remote.origin.url', url), + cwd=dest, + ) + cmd_args = make_command('checkout', '-q', rev_options.to_args()) + self.run_command(cmd_args, cwd=dest) + + self.update_submodules(dest) + + def update(self, dest, url, rev_options): + # type: (str, HiddenText, RevOptions) -> None + # First fetch changes from the default remote + if self.get_git_version() >= parse_version('1.9.0'): + # fetch tags in addition to everything else + self.run_command(['fetch', '-q', '--tags'], cwd=dest) + else: + self.run_command(['fetch', '-q'], cwd=dest) + # Then reset to wanted revision (maybe even origin/master) + rev_options = self.resolve_revision(dest, url, rev_options) + cmd_args = make_command('reset', '--hard', '-q', rev_options.to_args()) + self.run_command(cmd_args, cwd=dest) + #: update submodules + self.update_submodules(dest) + + @classmethod + def get_remote_url(cls, location): + """ + Return URL of the first remote encountered. + + Raises RemoteNotFoundError if the repository does not have a remote + url configured. + """ + # We need to pass 1 for extra_ok_returncodes since the command + # exits with return code 1 if there are no matching lines. + stdout = cls.run_command( + ['config', '--get-regexp', r'remote\..*\.url'], + extra_ok_returncodes=(1, ), show_stdout=False, cwd=location, + ) + remotes = stdout.splitlines() + try: + found_remote = remotes[0] + except IndexError: + raise RemoteNotFoundError + + for remote in remotes: + if remote.startswith('remote.origin.url '): + found_remote = remote + break + url = found_remote.split(' ')[1] + return url.strip() + + @classmethod + def get_revision(cls, location, rev=None): + if rev is None: + rev = 'HEAD' + current_rev = cls.run_command( + ['rev-parse', rev], show_stdout=False, cwd=location, + ) + return current_rev.strip() + + @classmethod + def get_subdirectory(cls, location): + """ + Return the path to setup.py, relative to the repo root. + Return None if setup.py is in the repo root. + """ + # find the repo root + git_dir = cls.run_command( + ['rev-parse', '--git-dir'], + show_stdout=False, cwd=location).strip() + if not os.path.isabs(git_dir): + git_dir = os.path.join(location, git_dir) + repo_root = os.path.abspath(os.path.join(git_dir, '..')) + return find_path_to_setup_from_repo_root(location, repo_root) + + @classmethod + def get_url_rev_and_auth(cls, url): + # type: (str) -> Tuple[str, Optional[str], AuthInfo] + """ + Prefixes stub URLs like 'user@hostname:user/repo.git' with 'ssh://'. + That's required because although they use SSH they sometimes don't + work with a ssh:// scheme (e.g. GitHub). But we need a scheme for + parsing. Hence we remove it again afterwards and return it as a stub. + """ + # Works around an apparent Git bug + # (see https://article.gmane.org/gmane.comp.version-control.git/146500) + scheme, netloc, path, query, fragment = urlsplit(url) + if scheme.endswith('file'): + initial_slashes = path[:-len(path.lstrip('/'))] + newpath = ( + initial_slashes + + urllib_request.url2pathname(path) + .replace('\\', '/').lstrip('/') + ) + url = urlunsplit((scheme, netloc, newpath, query, fragment)) + after_plus = scheme.find('+') + 1 + url = scheme[:after_plus] + urlunsplit( + (scheme[after_plus:], netloc, newpath, query, fragment), + ) + + if '://' not in url: + assert 'file:' not in url + url = url.replace('git+', 'git+ssh://') + url, rev, user_pass = super(Git, cls).get_url_rev_and_auth(url) + url = url.replace('ssh://', '') + else: + url, rev, user_pass = super(Git, cls).get_url_rev_and_auth(url) + + return url, rev, user_pass + + @classmethod + def update_submodules(cls, location): + if not os.path.exists(os.path.join(location, '.gitmodules')): + return + cls.run_command( + ['submodule', 'update', '--init', '--recursive', '-q'], + cwd=location, + ) + + @classmethod + def controls_location(cls, location): + if super(Git, cls).controls_location(location): + return True + try: + r = cls.run_command(['rev-parse'], + cwd=location, + show_stdout=False, + on_returncode='ignore', + log_failed_cmd=False) + return not r + except BadCommand: + logger.debug("could not determine if %s is under git control " + "because git is not available", location) + return False + + +vcs.register(Git) diff --git a/src/pip/_internal/vcs/mercurial.py b/src/pip/_internal/vcs/mercurial.py index dd04b8bc687..bc4b19c7294 100644 --- a/src/pip/_internal/vcs/mercurial.py +++ b/src/pip/_internal/vcs/mercurial.py @@ -1,154 +1,154 @@ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - -from __future__ import absolute_import - -import logging -import os - -from pip._vendor.six.moves import configparser - -from pip._internal.exceptions import BadCommand, InstallationError -from pip._internal.utils.misc import display_path -from pip._internal.utils.subprocess import make_command -from pip._internal.utils.temp_dir import TempDirectory -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from pip._internal.utils.urls import path_to_url -from pip._internal.vcs.versioncontrol import ( - VersionControl, - find_path_to_setup_from_repo_root, - vcs, -) - -if MYPY_CHECK_RUNNING: - from pip._internal.utils.misc import HiddenText - from pip._internal.vcs.versioncontrol import RevOptions - - -logger = logging.getLogger(__name__) - - -class Mercurial(VersionControl): - name = 'hg' - dirname = '.hg' - repo_name = 'clone' - schemes = ( - 'hg', 'hg+file', 'hg+http', 'hg+https', 'hg+ssh', 'hg+static-http', - ) - - @staticmethod - def get_base_rev_args(rev): - return [rev] - - def export(self, location, url): - # type: (str, HiddenText) -> None - """Export the Hg repository at the url to the destination location""" - with TempDirectory(kind="export") as temp_dir: - self.unpack(temp_dir.path, url=url) - - self.run_command( - ['archive', location], show_stdout=False, cwd=temp_dir.path - ) - - def fetch_new(self, dest, url, rev_options): - # type: (str, HiddenText, RevOptions) -> None - rev_display = rev_options.to_display() - logger.info( - 'Cloning hg %s%s to %s', - url, - rev_display, - display_path(dest), - ) - self.run_command(make_command('clone', '--noupdate', '-q', url, dest)) - self.run_command( - make_command('update', '-q', rev_options.to_args()), - cwd=dest, - ) - - def switch(self, dest, url, rev_options): - # type: (str, HiddenText, RevOptions) -> None - repo_config = os.path.join(dest, self.dirname, 'hgrc') - config = configparser.RawConfigParser() - try: - config.read(repo_config) - config.set('paths', 'default', url.secret) - with open(repo_config, 'w') as config_file: - config.write(config_file) - except (OSError, configparser.NoSectionError) as exc: - logger.warning( - 'Could not switch Mercurial repository to %s: %s', url, exc, - ) - else: - cmd_args = make_command('update', '-q', rev_options.to_args()) - self.run_command(cmd_args, cwd=dest) - - def update(self, dest, url, rev_options): - # type: (str, HiddenText, RevOptions) -> None - self.run_command(['pull', '-q'], cwd=dest) - cmd_args = make_command('update', '-q', rev_options.to_args()) - self.run_command(cmd_args, cwd=dest) - - @classmethod - def get_remote_url(cls, location): - url = cls.run_command( - ['showconfig', 'paths.default'], - show_stdout=False, cwd=location).strip() - if cls._is_local_repository(url): - url = path_to_url(url) - return url.strip() - - @classmethod - def get_revision(cls, location): - """ - Return the repository-local changeset revision number, as an integer. - """ - current_revision = cls.run_command( - ['parents', '--template={rev}'], - show_stdout=False, cwd=location).strip() - return current_revision - - @classmethod - def get_requirement_revision(cls, location): - """ - Return the changeset identification hash, as a 40-character - hexadecimal string - """ - current_rev_hash = cls.run_command( - ['parents', '--template={node}'], - show_stdout=False, cwd=location).strip() - return current_rev_hash - - @classmethod - def is_commit_id_equal(cls, dest, name): - """Always assume the versions don't match""" - return False - - @classmethod - def get_subdirectory(cls, location): - """ - Return the path to setup.py, relative to the repo root. - Return None if setup.py is in the repo root. - """ - # find the repo root - repo_root = cls.run_command( - ['root'], show_stdout=False, cwd=location).strip() - if not os.path.isabs(repo_root): - repo_root = os.path.abspath(os.path.join(location, repo_root)) - return find_path_to_setup_from_repo_root(location, repo_root) - - @classmethod - def controls_location(cls, location): - if super(Mercurial, cls).controls_location(location): - return True - try: - cls.run_command( - ['identify'], - cwd=location, - show_stdout=False, - on_returncode='raise', - log_failed_cmd=False) - except (BadCommand, InstallationError): - return False - - -vcs.register(Mercurial) +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + +from __future__ import absolute_import + +import logging +import os + +from pip._vendor.six.moves import configparser + +from pip._internal.exceptions import BadCommand, InstallationError +from pip._internal.utils.misc import display_path +from pip._internal.utils.subprocess import make_command +from pip._internal.utils.temp_dir import TempDirectory +from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from pip._internal.utils.urls import path_to_url +from pip._internal.vcs.versioncontrol import ( + VersionControl, + find_path_to_setup_from_repo_root, + vcs, +) + +if MYPY_CHECK_RUNNING: + from pip._internal.utils.misc import HiddenText + from pip._internal.vcs.versioncontrol import RevOptions + + +logger = logging.getLogger(__name__) + + +class Mercurial(VersionControl): + name = 'hg' + dirname = '.hg' + repo_name = 'clone' + schemes = ( + 'hg', 'hg+file', 'hg+http', 'hg+https', 'hg+ssh', 'hg+static-http', + ) + + @staticmethod + def get_base_rev_args(rev): + return [rev] + + def export(self, location, url): + # type: (str, HiddenText) -> None + """Export the Hg repository at the url to the destination location""" + with TempDirectory(kind="export") as temp_dir: + self.unpack(temp_dir.path, url=url) + + self.run_command( + ['archive', location], show_stdout=False, cwd=temp_dir.path + ) + + def fetch_new(self, dest, url, rev_options): + # type: (str, HiddenText, RevOptions) -> None + rev_display = rev_options.to_display() + logger.info( + 'Cloning hg %s%s to %s', + url, + rev_display, + display_path(dest), + ) + self.run_command(make_command('clone', '--noupdate', '-q', url, dest)) + self.run_command( + make_command('update', '-q', rev_options.to_args()), + cwd=dest, + ) + + def switch(self, dest, url, rev_options): + # type: (str, HiddenText, RevOptions) -> None + repo_config = os.path.join(dest, self.dirname, 'hgrc') + config = configparser.RawConfigParser() + try: + config.read(repo_config) + config.set('paths', 'default', url.secret) + with open(repo_config, 'w') as config_file: + config.write(config_file) + except (OSError, configparser.NoSectionError) as exc: + logger.warning( + 'Could not switch Mercurial repository to %s: %s', url, exc, + ) + else: + cmd_args = make_command('update', '-q', rev_options.to_args()) + self.run_command(cmd_args, cwd=dest) + + def update(self, dest, url, rev_options): + # type: (str, HiddenText, RevOptions) -> None + self.run_command(['pull', '-q'], cwd=dest) + cmd_args = make_command('update', '-q', rev_options.to_args()) + self.run_command(cmd_args, cwd=dest) + + @classmethod + def get_remote_url(cls, location): + url = cls.run_command( + ['showconfig', 'paths.default'], + show_stdout=False, cwd=location).strip() + if cls._is_local_repository(url): + url = path_to_url(url) + return url.strip() + + @classmethod + def get_revision(cls, location): + """ + Return the repository-local changeset revision number, as an integer. + """ + current_revision = cls.run_command( + ['parents', '--template={rev}'], + show_stdout=False, cwd=location).strip() + return current_revision + + @classmethod + def get_requirement_revision(cls, location): + """ + Return the changeset identification hash, as a 40-character + hexadecimal string + """ + current_rev_hash = cls.run_command( + ['parents', '--template={node}'], + show_stdout=False, cwd=location).strip() + return current_rev_hash + + @classmethod + def is_commit_id_equal(cls, dest, name): + """Always assume the versions don't match""" + return False + + @classmethod + def get_subdirectory(cls, location): + """ + Return the path to setup.py, relative to the repo root. + Return None if setup.py is in the repo root. + """ + # find the repo root + repo_root = cls.run_command( + ['root'], show_stdout=False, cwd=location).strip() + if not os.path.isabs(repo_root): + repo_root = os.path.abspath(os.path.join(location, repo_root)) + return find_path_to_setup_from_repo_root(location, repo_root) + + @classmethod + def controls_location(cls, location): + if super(Mercurial, cls).controls_location(location): + return True + try: + cls.run_command( + ['identify'], + cwd=location, + show_stdout=False, + on_returncode='raise', + log_failed_cmd=False) + except (BadCommand, InstallationError): + return False + + +vcs.register(Mercurial) diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index 4519f29f78f..55afa908c19 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -1,672 +1,672 @@ -"""Handles all VCS (version control) support""" - -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - -from __future__ import absolute_import - -import errno -import logging -import os -import shutil -import sys - -from pip._vendor import pkg_resources -from pip._vendor.six.moves.urllib import parse as urllib_parse - -from pip._internal.exceptions import BadCommand -from pip._internal.utils.compat import samefile -from pip._internal.utils.misc import ( - ask_path_exists, - backup_dir, - display_path, - hide_url, - hide_value, - rmtree, -) -from pip._internal.utils.subprocess import call_subprocess, make_command -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from pip._internal.utils.urls import get_url_scheme - -if MYPY_CHECK_RUNNING: - from typing import ( - Any, Dict, Iterable, List, Mapping, Optional, Text, Tuple, Type, Union - ) - from pip._internal.utils.ui import SpinnerInterface - from pip._internal.utils.misc import HiddenText - from pip._internal.utils.subprocess import CommandArgs - - AuthInfo = Tuple[Optional[str], Optional[str]] - - -__all__ = ['vcs'] - - -logger = logging.getLogger(__name__) - - -def is_url(name): - # type: (Union[str, Text]) -> bool - """ - Return true if the name looks like a URL. - """ - scheme = get_url_scheme(name) - if scheme is None: - return False - return scheme in ['http', 'https', 'file', 'ftp'] + vcs.all_schemes - - -def make_vcs_requirement_url(repo_url, rev, project_name, subdir=None): - """ - Return the URL for a VCS requirement. - - Args: - repo_url: the remote VCS url, with any needed VCS prefix (e.g. "git+"). - project_name: the (unescaped) project name. - """ - egg_project_name = pkg_resources.to_filename(project_name) - req = '{}@{}#egg={}'.format(repo_url, rev, egg_project_name) - if subdir: - req += '&subdirectory={}'.format(subdir) - - return req - - -def find_path_to_setup_from_repo_root(location, repo_root): - """ - Find the path to `setup.py` by searching up the filesystem from `location`. - Return the path to `setup.py` relative to `repo_root`. - Return None if `setup.py` is in `repo_root` or cannot be found. - """ - # find setup.py - orig_location = location - while not os.path.exists(os.path.join(location, 'setup.py')): - last_location = location - location = os.path.dirname(location) - if location == last_location: - # We've traversed up to the root of the filesystem without - # finding setup.py - logger.warning( - "Could not find setup.py for directory %s (tried all " - "parent directories)", - orig_location, - ) - return None - - if samefile(repo_root, location): - return None - - return os.path.relpath(location, repo_root) - - -class RemoteNotFoundError(Exception): - pass - - -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, - vc_class, # type: Type[VersionControl] - rev=None, # type: Optional[str] - extra_args=None, # type: Optional[CommandArgs] - ): - # type: (...) -> None - """ - Args: - vc_class: a VersionControl subclass. - 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.vc_class = vc_class - self.branch_name = None # type: Optional[str] - - def __repr__(self): - return '<RevOptions {}: rev={!r}>'.format(self.vc_class.name, self.rev) - - @property - def arg_rev(self): - # type: () -> Optional[str] - if self.rev is None: - return self.vc_class.default_arg_rev - - return self.rev - - def to_args(self): - # type: () -> CommandArgs - """ - Return the VCS-specific command arguments. - """ - args = [] # type: CommandArgs - rev = self.arg_rev - if rev is not None: - args += self.vc_class.get_base_rev_args(rev) - args += self.extra_args - - return args - - def to_display(self): - # type: () -> str - if not self.rev: - return '' - - return ' (to revision {})'.format(self.rev) - - def make_new(self, rev): - # type: (str) -> RevOptions - """ - 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.vc_class.make_rev_options(rev, extra_args=self.extra_args) - - -class VcsSupport(object): - _registry = {} # type: Dict[str, VersionControl] - schemes = ['ssh', 'git', 'hg', 'bzr', 'sftp', 'svn'] - - def __init__(self): - # type: () -> None - # Register more schemes with urlparse for various version control - # systems - urllib_parse.uses_netloc.extend(self.schemes) - # Python >= 2.7.4, 3.3 doesn't have uses_fragment - if getattr(urllib_parse, 'uses_fragment', None): - urllib_parse.uses_fragment.extend(self.schemes) - super(VcsSupport, self).__init__() - - def __iter__(self): - return self._registry.__iter__() - - @property - def backends(self): - # type: () -> List[VersionControl] - return list(self._registry.values()) - - @property - def dirnames(self): - # type: () -> List[str] - return [backend.dirname for backend in self.backends] - - @property - def all_schemes(self): - # type: () -> List[str] - schemes = [] # type: List[str] - for backend in self.backends: - schemes.extend(backend.schemes) - return schemes - - def register(self, cls): - # type: (Type[VersionControl]) -> None - if not hasattr(cls, 'name'): - logger.warning('Cannot register VCS %s', cls.__name__) - return - if cls.name not in self._registry: - self._registry[cls.name] = cls() - logger.debug('Registered VCS backend: %s', cls.name) - - def unregister(self, name): - # type: (str) -> None - if name in self._registry: - del self._registry[name] - - def get_backend_for_dir(self, location): - # type: (str) -> Optional[VersionControl] - """ - Return a VersionControl object if a repository of that type is found - at the given directory. - """ - for vcs_backend in self._registry.values(): - if vcs_backend.controls_location(location): - logger.debug('Determine that %s uses VCS: %s', - location, vcs_backend.name) - return vcs_backend - return None - - def get_backend(self, name): - # type: (str) -> Optional[VersionControl] - """ - Return a VersionControl object or None. - """ - name = name.lower() - return self._registry.get(name) - - -vcs = VcsSupport() - - -class VersionControl(object): - name = '' - dirname = '' - repo_name = '' - # List of supported schemes for this Version Control - 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] - - @classmethod - def should_add_vcs_url_prefix(cls, remote_url): - """ - Return whether the vcs prefix (e.g. "git+") should be added to a - repository's remote url when used in a requirement. - """ - return not remote_url.lower().startswith('{}:'.format(cls.name)) - - @classmethod - def get_subdirectory(cls, location): - """ - Return the path to setup.py, relative to the repo root. - Return None if setup.py is in the repo root. - """ - return None - - @classmethod - def get_requirement_revision(cls, repo_dir): - """ - Return the revision string that should be used in a requirement. - """ - return cls.get_revision(repo_dir) - - @classmethod - def get_src_requirement(cls, repo_dir, project_name): - """ - Return the requirement string to use to redownload the files - currently at the given repository directory. - - Args: - project_name: the (unescaped) project name. - - The return value has a form similar to the following: - - {repository_url}@{revision}#egg={project_name} - """ - repo_url = cls.get_remote_url(repo_dir) - if repo_url is None: - return None - - if cls.should_add_vcs_url_prefix(repo_url): - repo_url = '{}+{}'.format(cls.name, repo_url) - - revision = cls.get_requirement_revision(repo_dir) - subdir = cls.get_subdirectory(repo_dir) - req = make_vcs_requirement_url(repo_url, revision, project_name, - subdir=subdir) - - return req - - @staticmethod - def get_base_rev_args(rev): - """ - Return the base revision arguments for a vcs command. - - Args: - rev: the name of a revision to install. Cannot be None. - """ - raise NotImplementedError - - @classmethod - def make_rev_options(cls, rev=None, extra_args=None): - # type: (Optional[str], Optional[CommandArgs]) -> RevOptions - """ - Return a RevOptions object. - - Args: - rev: the name of a revision to install. - extra_args: a list of extra options. - """ - return RevOptions(cls, rev, extra_args=extra_args) - - @classmethod - def _is_local_repository(cls, repo): - # type: (str) -> bool - """ - posix absolute paths start with os.path.sep, - win32 ones start with drive (like c:\\folder) - """ - drive, tail = os.path.splitdrive(repo) - return repo.startswith(os.path.sep) or bool(drive) - - def export(self, location, url): - # type: (str, HiddenText) -> None - """ - Export the repository at the url to the destination location - i.e. only download the files, without vcs informations - - :param url: the repository URL starting with a vcs prefix. - """ - raise NotImplementedError - - @classmethod - def get_netloc_and_auth(cls, netloc, scheme): - """ - Parse the repository URL's netloc, and return the new netloc to use - along with auth information. - - Args: - netloc: the original repository URL netloc. - scheme: the repository URL's scheme without the vcs prefix. - - This is mainly for the Subversion class to override, so that auth - information can be provided via the --username and --password options - instead of through the URL. For other subclasses like Git without - such an option, auth information must stay in the URL. - - Returns: (netloc, (username, password)). - """ - return netloc, (None, None) - - @classmethod - def get_url_rev_and_auth(cls, url): - # type: (str) -> Tuple[str, Optional[str], AuthInfo] - """ - Parse the repository URL to use, and return the URL, revision, - and auth info to use. - - Returns: (url, rev, (username, password)). - """ - scheme, netloc, path, query, frag = urllib_parse.urlsplit(url) - if '+' not in scheme: - raise ValueError( - "Sorry, {!r} is a malformed VCS url. " - "The format is <vcs>+<protocol>://<url>, " - "e.g. svn+http://myrepo/svn/MyApp#egg=MyApp".format(url) - ) - # Remove the vcs prefix. - scheme = scheme.split('+', 1)[1] - netloc, user_pass = cls.get_netloc_and_auth(netloc, scheme) - rev = None - if '@' in path: - path, rev = path.rsplit('@', 1) - url = urllib_parse.urlunsplit((scheme, netloc, path, query, '')) - return url, rev, user_pass - - @staticmethod - def make_rev_args(username, password): - # type: (Optional[str], Optional[HiddenText]) -> CommandArgs - """ - Return the RevOptions "extra arguments" to use in obtain(). - """ - return [] - - def get_url_rev_options(self, url): - # type: (HiddenText) -> Tuple[HiddenText, RevOptions] - """ - Return the URL and RevOptions object to use in obtain() and in - some cases export(), as a tuple (url, rev_options). - """ - secret_url, rev, user_pass = self.get_url_rev_and_auth(url.secret) - username, secret_password = user_pass - password = None # type: Optional[HiddenText] - if secret_password is not None: - password = hide_value(secret_password) - extra_args = self.make_rev_args(username, password) - rev_options = self.make_rev_options(rev, extra_args=extra_args) - - return hide_url(secret_url), rev_options - - @staticmethod - def normalize_url(url): - # type: (str) -> str - """ - Normalize a URL for comparison by unquoting it and removing any - trailing slash. - """ - return urllib_parse.unquote(url).rstrip('/') - - @classmethod - def compare_urls(cls, url1, url2): - # type: (str, str) -> bool - """ - Compare two repo URLs for identity, ignoring incidental differences. - """ - return (cls.normalize_url(url1) == cls.normalize_url(url2)) - - def fetch_new(self, dest, url, rev_options): - # type: (str, HiddenText, RevOptions) -> None - """ - Fetch a revision from a repository, in the case that this is the - first fetch from the repository. - - Args: - dest: the directory to fetch the repository to. - rev_options: a RevOptions object. - """ - raise NotImplementedError - - def switch(self, dest, url, rev_options): - # type: (str, HiddenText, RevOptions) -> None - """ - Switch the repo at ``dest`` to point to ``URL``. - - Args: - rev_options: a RevOptions object. - """ - raise NotImplementedError - - def update(self, dest, url, rev_options): - # type: (str, HiddenText, RevOptions) -> None - """ - Update an already-existing repo to the given ``rev_options``. - - Args: - rev_options: a RevOptions object. - """ - raise NotImplementedError - - @classmethod - def is_commit_id_equal(cls, dest, name): - """ - Return whether the id of the current commit equals the given name. - - Args: - dest: the repository directory. - name: a string name. - """ - raise NotImplementedError - - def obtain(self, dest, url): - # type: (str, HiddenText) -> None - """ - Install or update in editable mode the package represented by this - VersionControl object. - - :param dest: the repository directory in which to install or update. - :param url: the repository URL starting with a vcs prefix. - """ - url, rev_options = self.get_url_rev_options(url) - - if not os.path.exists(dest): - self.fetch_new(dest, url, rev_options) - return - - rev_display = rev_options.to_display() - if self.is_repository_directory(dest): - existing_url = self.get_remote_url(dest) - if self.compare_urls(existing_url, url.secret): - logger.debug( - '%s in %s exists, and has correct URL (%s)', - self.repo_name.title(), - display_path(dest), - url, - ) - if not self.is_commit_id_equal(dest, rev_options.rev): - logger.info( - 'Updating %s %s%s', - display_path(dest), - self.repo_name, - rev_display, - ) - self.update(dest, url, rev_options) - else: - logger.info('Skipping because already up-to-date.') - return - - logger.warning( - '%s %s in %s exists with URL %s', - self.name, - self.repo_name, - display_path(dest), - existing_url, - ) - prompt = ('(s)witch, (i)gnore, (w)ipe, (b)ackup ', - ('s', 'i', 'w', 'b')) - else: - logger.warning( - 'Directory %s already exists, and is not a %s %s.', - dest, - self.name, - self.repo_name, - ) - # https://github.com/python/mypy/issues/1174 - prompt = ('(i)gnore, (w)ipe, (b)ackup ', # type: ignore - ('i', 'w', 'b')) - - logger.warning( - 'The plan is to install the %s repository %s', - self.name, - url, - ) - response = ask_path_exists('What to do? %s' % prompt[0], prompt[1]) - - if response == 'a': - sys.exit(-1) - - if response == 'w': - logger.warning('Deleting %s', display_path(dest)) - rmtree(dest) - self.fetch_new(dest, url, rev_options) - return - - if response == 'b': - dest_dir = backup_dir(dest) - logger.warning( - 'Backing up %s to %s', display_path(dest), dest_dir, - ) - shutil.move(dest, dest_dir) - self.fetch_new(dest, url, rev_options) - return - - # Do nothing if the response is "i". - if response == 's': - logger.info( - 'Switching %s %s to %s%s', - self.repo_name, - display_path(dest), - url, - rev_display, - ) - self.switch(dest, url, rev_options) - - def unpack(self, location, url): - # type: (str, HiddenText) -> None - """ - Clean up current location and download the url repository - (and vcs infos) into location - - :param url: the repository URL starting with a vcs prefix. - """ - if os.path.exists(location): - rmtree(location) - self.obtain(location, url=url) - - @classmethod - def get_remote_url(cls, location): - """ - Return the url used at location - - Raises RemoteNotFoundError if the repository does not have a remote - url configured. - """ - raise NotImplementedError - - @classmethod - def get_revision(cls, location): - """ - Return the current commit id of the files at the given location. - """ - raise NotImplementedError - - @classmethod - def run_command( - cls, - cmd, # type: Union[List[str], CommandArgs] - show_stdout=True, # type: bool - cwd=None, # type: Optional[str] - on_returncode='raise', # type: str - extra_ok_returncodes=None, # type: Optional[Iterable[int]] - command_desc=None, # type: Optional[str] - extra_environ=None, # type: Optional[Mapping[str, Any]] - spinner=None, # type: Optional[SpinnerInterface] - log_failed_cmd=True - ): - # type: (...) -> Text - """ - Run a VCS subcommand - This is simply a wrapper around call_subprocess that adds the VCS - command name, and checks that the VCS is available - """ - cmd = make_command(cls.name, *cmd) - try: - return call_subprocess(cmd, show_stdout, cwd, - on_returncode=on_returncode, - extra_ok_returncodes=extra_ok_returncodes, - command_desc=command_desc, - extra_environ=extra_environ, - unset_environ=cls.unset_environ, - spinner=spinner, - log_failed_cmd=log_failed_cmd) - 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 - do you have ' - '%r installed and in your ' - 'PATH?' % (cls.name, cls.name)) - else: - raise # re-raise exception if a different error occurred - - @classmethod - def is_repository_directory(cls, path): - # type: (str) -> bool - """ - Return whether a directory path is a repository directory. - """ - logger.debug('Checking in %s for %s (%s)...', - path, cls.dirname, cls.name) - return os.path.exists(os.path.join(path, cls.dirname)) - - @classmethod - def controls_location(cls, location): - # type: (str) -> bool - """ - Check if a location is controlled by the vcs. - - Searches up the filesystem and checks is_repository_directory(). - - It is meant to be extended to add smarter detection mechanisms for - specific vcs. For example, the Git override checks that Git is - actually available. - """ - while not cls.is_repository_directory(location): - last_location = location - location = os.path.dirname(location) - if location == last_location: - # We've traversed up to the root of the filesystem. - return False - return True +"""Handles all VCS (version control) support""" + +# The following comment should be removed at some point in the future. +# mypy: disallow-untyped-defs=False + +from __future__ import absolute_import + +import errno +import logging +import os +import shutil +import sys + +from pip._vendor import pkg_resources +from pip._vendor.six.moves.urllib import parse as urllib_parse + +from pip._internal.exceptions import BadCommand +from pip._internal.utils.compat import samefile +from pip._internal.utils.misc import ( + ask_path_exists, + backup_dir, + display_path, + hide_url, + hide_value, + rmtree, +) +from pip._internal.utils.subprocess import call_subprocess, make_command +from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from pip._internal.utils.urls import get_url_scheme + +if MYPY_CHECK_RUNNING: + from typing import ( + Any, Dict, Iterable, List, Mapping, Optional, Text, Tuple, Type, Union + ) + from pip._internal.utils.ui import SpinnerInterface + from pip._internal.utils.misc import HiddenText + from pip._internal.utils.subprocess import CommandArgs + + AuthInfo = Tuple[Optional[str], Optional[str]] + + +__all__ = ['vcs'] + + +logger = logging.getLogger(__name__) + + +def is_url(name): + # type: (Union[str, Text]) -> bool + """ + Return true if the name looks like a URL. + """ + scheme = get_url_scheme(name) + if scheme is None: + return False + return scheme in ['http', 'https', 'file', 'ftp'] + vcs.all_schemes + + +def make_vcs_requirement_url(repo_url, rev, project_name, subdir=None): + """ + Return the URL for a VCS requirement. + + Args: + repo_url: the remote VCS url, with any needed VCS prefix (e.g. "git+"). + project_name: the (unescaped) project name. + """ + egg_project_name = pkg_resources.to_filename(project_name) + req = '{}@{}#egg={}'.format(repo_url, rev, egg_project_name) + if subdir: + req += '&subdirectory={}'.format(subdir) + + return req + + +def find_path_to_setup_from_repo_root(location, repo_root): + """ + Find the path to `setup.py` by searching up the filesystem from `location`. + Return the path to `setup.py` relative to `repo_root`. + Return None if `setup.py` is in `repo_root` or cannot be found. + """ + # find setup.py + orig_location = location + while not os.path.exists(os.path.join(location, 'setup.py')): + last_location = location + location = os.path.dirname(location) + if location == last_location: + # We've traversed up to the root of the filesystem without + # finding setup.py + logger.warning( + "Could not find setup.py for directory %s (tried all " + "parent directories)", + orig_location, + ) + return None + + if samefile(repo_root, location): + return None + + return os.path.relpath(location, repo_root) + + +class RemoteNotFoundError(Exception): + pass + + +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, + vc_class, # type: Type[VersionControl] + rev=None, # type: Optional[str] + extra_args=None, # type: Optional[CommandArgs] + ): + # type: (...) -> None + """ + Args: + vc_class: a VersionControl subclass. + 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.vc_class = vc_class + self.branch_name = None # type: Optional[str] + + def __repr__(self): + return '<RevOptions {}: rev={!r}>'.format(self.vc_class.name, self.rev) + + @property + def arg_rev(self): + # type: () -> Optional[str] + if self.rev is None: + return self.vc_class.default_arg_rev + + return self.rev + + def to_args(self): + # type: () -> CommandArgs + """ + Return the VCS-specific command arguments. + """ + args = [] # type: CommandArgs + rev = self.arg_rev + if rev is not None: + args += self.vc_class.get_base_rev_args(rev) + args += self.extra_args + + return args + + def to_display(self): + # type: () -> str + if not self.rev: + return '' + + return ' (to revision {})'.format(self.rev) + + def make_new(self, rev): + # type: (str) -> RevOptions + """ + 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.vc_class.make_rev_options(rev, extra_args=self.extra_args) + + +class VcsSupport(object): + _registry = {} # type: Dict[str, VersionControl] + schemes = ['ssh', 'git', 'hg', 'bzr', 'sftp', 'svn'] + + def __init__(self): + # type: () -> None + # Register more schemes with urlparse for various version control + # systems + urllib_parse.uses_netloc.extend(self.schemes) + # Python >= 2.7.4, 3.3 doesn't have uses_fragment + if getattr(urllib_parse, 'uses_fragment', None): + urllib_parse.uses_fragment.extend(self.schemes) + super(VcsSupport, self).__init__() + + def __iter__(self): + return self._registry.__iter__() + + @property + def backends(self): + # type: () -> List[VersionControl] + return list(self._registry.values()) + + @property + def dirnames(self): + # type: () -> List[str] + return [backend.dirname for backend in self.backends] + + @property + def all_schemes(self): + # type: () -> List[str] + schemes = [] # type: List[str] + for backend in self.backends: + schemes.extend(backend.schemes) + return schemes + + def register(self, cls): + # type: (Type[VersionControl]) -> None + if not hasattr(cls, 'name'): + logger.warning('Cannot register VCS %s', cls.__name__) + return + if cls.name not in self._registry: + self._registry[cls.name] = cls() + logger.debug('Registered VCS backend: %s', cls.name) + + def unregister(self, name): + # type: (str) -> None + if name in self._registry: + del self._registry[name] + + def get_backend_for_dir(self, location): + # type: (str) -> Optional[VersionControl] + """ + Return a VersionControl object if a repository of that type is found + at the given directory. + """ + for vcs_backend in self._registry.values(): + if vcs_backend.controls_location(location): + logger.debug('Determine that %s uses VCS: %s', + location, vcs_backend.name) + return vcs_backend + return None + + def get_backend(self, name): + # type: (str) -> Optional[VersionControl] + """ + Return a VersionControl object or None. + """ + name = name.lower() + return self._registry.get(name) + + +vcs = VcsSupport() + + +class VersionControl(object): + name = '' + dirname = '' + repo_name = '' + # List of supported schemes for this Version Control + 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] + + @classmethod + def should_add_vcs_url_prefix(cls, remote_url): + """ + Return whether the vcs prefix (e.g. "git+") should be added to a + repository's remote url when used in a requirement. + """ + return not remote_url.lower().startswith('{}:'.format(cls.name)) + + @classmethod + def get_subdirectory(cls, location): + """ + Return the path to setup.py, relative to the repo root. + Return None if setup.py is in the repo root. + """ + return None + + @classmethod + def get_requirement_revision(cls, repo_dir): + """ + Return the revision string that should be used in a requirement. + """ + return cls.get_revision(repo_dir) + + @classmethod + def get_src_requirement(cls, repo_dir, project_name): + """ + Return the requirement string to use to redownload the files + currently at the given repository directory. + + Args: + project_name: the (unescaped) project name. + + The return value has a form similar to the following: + + {repository_url}@{revision}#egg={project_name} + """ + repo_url = cls.get_remote_url(repo_dir) + if repo_url is None: + return None + + if cls.should_add_vcs_url_prefix(repo_url): + repo_url = '{}+{}'.format(cls.name, repo_url) + + revision = cls.get_requirement_revision(repo_dir) + subdir = cls.get_subdirectory(repo_dir) + req = make_vcs_requirement_url(repo_url, revision, project_name, + subdir=subdir) + + return req + + @staticmethod + def get_base_rev_args(rev): + """ + Return the base revision arguments for a vcs command. + + Args: + rev: the name of a revision to install. Cannot be None. + """ + raise NotImplementedError + + @classmethod + def make_rev_options(cls, rev=None, extra_args=None): + # type: (Optional[str], Optional[CommandArgs]) -> RevOptions + """ + Return a RevOptions object. + + Args: + rev: the name of a revision to install. + extra_args: a list of extra options. + """ + return RevOptions(cls, rev, extra_args=extra_args) + + @classmethod + def _is_local_repository(cls, repo): + # type: (str) -> bool + """ + posix absolute paths start with os.path.sep, + win32 ones start with drive (like c:\\folder) + """ + drive, tail = os.path.splitdrive(repo) + return repo.startswith(os.path.sep) or bool(drive) + + def export(self, location, url): + # type: (str, HiddenText) -> None + """ + Export the repository at the url to the destination location + i.e. only download the files, without vcs informations + + :param url: the repository URL starting with a vcs prefix. + """ + raise NotImplementedError + + @classmethod + def get_netloc_and_auth(cls, netloc, scheme): + """ + Parse the repository URL's netloc, and return the new netloc to use + along with auth information. + + Args: + netloc: the original repository URL netloc. + scheme: the repository URL's scheme without the vcs prefix. + + This is mainly for the Subversion class to override, so that auth + information can be provided via the --username and --password options + instead of through the URL. For other subclasses like Git without + such an option, auth information must stay in the URL. + + Returns: (netloc, (username, password)). + """ + return netloc, (None, None) + + @classmethod + def get_url_rev_and_auth(cls, url): + # type: (str) -> Tuple[str, Optional[str], AuthInfo] + """ + Parse the repository URL to use, and return the URL, revision, + and auth info to use. + + Returns: (url, rev, (username, password)). + """ + scheme, netloc, path, query, frag = urllib_parse.urlsplit(url) + if '+' not in scheme: + raise ValueError( + "Sorry, {!r} is a malformed VCS url. " + "The format is <vcs>+<protocol>://<url>, " + "e.g. svn+http://myrepo/svn/MyApp#egg=MyApp".format(url) + ) + # Remove the vcs prefix. + scheme = scheme.split('+', 1)[1] + netloc, user_pass = cls.get_netloc_and_auth(netloc, scheme) + rev = None + if '@' in path: + path, rev = path.rsplit('@', 1) + url = urllib_parse.urlunsplit((scheme, netloc, path, query, '')) + return url, rev, user_pass + + @staticmethod + def make_rev_args(username, password): + # type: (Optional[str], Optional[HiddenText]) -> CommandArgs + """ + Return the RevOptions "extra arguments" to use in obtain(). + """ + return [] + + def get_url_rev_options(self, url): + # type: (HiddenText) -> Tuple[HiddenText, RevOptions] + """ + Return the URL and RevOptions object to use in obtain() and in + some cases export(), as a tuple (url, rev_options). + """ + secret_url, rev, user_pass = self.get_url_rev_and_auth(url.secret) + username, secret_password = user_pass + password = None # type: Optional[HiddenText] + if secret_password is not None: + password = hide_value(secret_password) + extra_args = self.make_rev_args(username, password) + rev_options = self.make_rev_options(rev, extra_args=extra_args) + + return hide_url(secret_url), rev_options + + @staticmethod + def normalize_url(url): + # type: (str) -> str + """ + Normalize a URL for comparison by unquoting it and removing any + trailing slash. + """ + return urllib_parse.unquote(url).rstrip('/') + + @classmethod + def compare_urls(cls, url1, url2): + # type: (str, str) -> bool + """ + Compare two repo URLs for identity, ignoring incidental differences. + """ + return (cls.normalize_url(url1) == cls.normalize_url(url2)) + + def fetch_new(self, dest, url, rev_options): + # type: (str, HiddenText, RevOptions) -> None + """ + Fetch a revision from a repository, in the case that this is the + first fetch from the repository. + + Args: + dest: the directory to fetch the repository to. + rev_options: a RevOptions object. + """ + raise NotImplementedError + + def switch(self, dest, url, rev_options): + # type: (str, HiddenText, RevOptions) -> None + """ + Switch the repo at ``dest`` to point to ``URL``. + + Args: + rev_options: a RevOptions object. + """ + raise NotImplementedError + + def update(self, dest, url, rev_options): + # type: (str, HiddenText, RevOptions) -> None + """ + Update an already-existing repo to the given ``rev_options``. + + Args: + rev_options: a RevOptions object. + """ + raise NotImplementedError + + @classmethod + def is_commit_id_equal(cls, dest, name): + """ + Return whether the id of the current commit equals the given name. + + Args: + dest: the repository directory. + name: a string name. + """ + raise NotImplementedError + + def obtain(self, dest, url): + # type: (str, HiddenText) -> None + """ + Install or update in editable mode the package represented by this + VersionControl object. + + :param dest: the repository directory in which to install or update. + :param url: the repository URL starting with a vcs prefix. + """ + url, rev_options = self.get_url_rev_options(url) + + if not os.path.exists(dest): + self.fetch_new(dest, url, rev_options) + return + + rev_display = rev_options.to_display() + if self.is_repository_directory(dest): + existing_url = self.get_remote_url(dest) + if self.compare_urls(existing_url, url.secret): + logger.debug( + '%s in %s exists, and has correct URL (%s)', + self.repo_name.title(), + display_path(dest), + url, + ) + if not self.is_commit_id_equal(dest, rev_options.rev): + logger.info( + 'Updating %s %s%s', + display_path(dest), + self.repo_name, + rev_display, + ) + self.update(dest, url, rev_options) + else: + logger.info('Skipping because already up-to-date.') + return + + logger.warning( + '%s %s in %s exists with URL %s', + self.name, + self.repo_name, + display_path(dest), + existing_url, + ) + prompt = ('(s)witch, (i)gnore, (w)ipe, (b)ackup ', + ('s', 'i', 'w', 'b')) + else: + logger.warning( + 'Directory %s already exists, and is not a %s %s.', + dest, + self.name, + self.repo_name, + ) + # https://github.com/python/mypy/issues/1174 + prompt = ('(i)gnore, (w)ipe, (b)ackup ', # type: ignore + ('i', 'w', 'b')) + + logger.warning( + 'The plan is to install the %s repository %s', + self.name, + url, + ) + response = ask_path_exists('What to do? %s' % prompt[0], prompt[1]) + + if response == 'a': + sys.exit(-1) + + if response == 'w': + logger.warning('Deleting %s', display_path(dest)) + rmtree(dest) + self.fetch_new(dest, url, rev_options) + return + + if response == 'b': + dest_dir = backup_dir(dest) + logger.warning( + 'Backing up %s to %s', display_path(dest), dest_dir, + ) + shutil.move(dest, dest_dir) + self.fetch_new(dest, url, rev_options) + return + + # Do nothing if the response is "i". + if response == 's': + logger.info( + 'Switching %s %s to %s%s', + self.repo_name, + display_path(dest), + url, + rev_display, + ) + self.switch(dest, url, rev_options) + + def unpack(self, location, url): + # type: (str, HiddenText) -> None + """ + Clean up current location and download the url repository + (and vcs infos) into location + + :param url: the repository URL starting with a vcs prefix. + """ + if os.path.exists(location): + rmtree(location) + self.obtain(location, url=url) + + @classmethod + def get_remote_url(cls, location): + """ + Return the url used at location + + Raises RemoteNotFoundError if the repository does not have a remote + url configured. + """ + raise NotImplementedError + + @classmethod + def get_revision(cls, location): + """ + Return the current commit id of the files at the given location. + """ + raise NotImplementedError + + @classmethod + def run_command( + cls, + cmd, # type: Union[List[str], CommandArgs] + show_stdout=True, # type: bool + cwd=None, # type: Optional[str] + on_returncode='raise', # type: str + extra_ok_returncodes=None, # type: Optional[Iterable[int]] + command_desc=None, # type: Optional[str] + extra_environ=None, # type: Optional[Mapping[str, Any]] + spinner=None, # type: Optional[SpinnerInterface] + log_failed_cmd=True + ): + # type: (...) -> Text + """ + Run a VCS subcommand + This is simply a wrapper around call_subprocess that adds the VCS + command name, and checks that the VCS is available + """ + cmd = make_command(cls.name, *cmd) + try: + return call_subprocess(cmd, show_stdout, cwd, + on_returncode=on_returncode, + extra_ok_returncodes=extra_ok_returncodes, + command_desc=command_desc, + extra_environ=extra_environ, + unset_environ=cls.unset_environ, + spinner=spinner, + log_failed_cmd=log_failed_cmd) + 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 - do you have ' + '%r installed and in your ' + 'PATH?' % (cls.name, cls.name)) + else: + raise # re-raise exception if a different error occurred + + @classmethod + def is_repository_directory(cls, path): + # type: (str) -> bool + """ + Return whether a directory path is a repository directory. + """ + logger.debug('Checking in %s for %s (%s)...', + path, cls.dirname, cls.name) + return os.path.exists(os.path.join(path, cls.dirname)) + + @classmethod + def controls_location(cls, location): + # type: (str) -> bool + """ + Check if a location is controlled by the vcs. + + Searches up the filesystem and checks is_repository_directory(). + + It is meant to be extended to add smarter detection mechanisms for + specific vcs. For example, the Git override checks that Git is + actually available. + """ + while not cls.is_repository_directory(location): + last_location = location + location = os.path.dirname(location) + if location == last_location: + # We've traversed up to the root of the filesystem. + return False + return True From 24a2be8af684885720ac376477fc864584e95bb2 Mon Sep 17 00:00:00 2001 From: tbeswick <tbeswick@enphaseenergy.com> Date: Fri, 11 Oct 2019 16:44:45 +1300 Subject: [PATCH 0523/3170] Reverting `VersionControl.controls_location()` to pre PR state. Its an optimization that belongs in another PR. --- src/pip/_internal/vcs/versioncontrol.py | 17 +++++------------ 1 file changed, 5 insertions(+), 12 deletions(-) diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index 55afa908c19..9038ace8052 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -656,17 +656,10 @@ def controls_location(cls, location): # type: (str) -> bool """ Check if a location is controlled by the vcs. + It is meant to be overridden to implement smarter detection + mechanisms for specific vcs. - Searches up the filesystem and checks is_repository_directory(). - - It is meant to be extended to add smarter detection mechanisms for - specific vcs. For example, the Git override checks that Git is - actually available. + This can do more than is_repository_directory() alone. For example, + the Git override checks that Git is actually available. """ - while not cls.is_repository_directory(location): - last_location = location - location = os.path.dirname(location) - if location == last_location: - # We've traversed up to the root of the filesystem. - return False - return True + return cls.is_repository_directory(location) From 7ebc54171c9612b78e59d8012c0d8c1b70f78431 Mon Sep 17 00:00:00 2001 From: tbeswick <tbeswick@enphaseenergy.com> Date: Fri, 11 Oct 2019 17:15:35 +1300 Subject: [PATCH 0524/3170] Fixed missing return statement in `Mercurial.controls_location()`, it only got found after reverting `VersionControl.controls_location()` --- src/pip/_internal/vcs/mercurial.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pip/_internal/vcs/mercurial.py b/src/pip/_internal/vcs/mercurial.py index bc4b19c7294..d9b58cfe9a4 100644 --- a/src/pip/_internal/vcs/mercurial.py +++ b/src/pip/_internal/vcs/mercurial.py @@ -147,6 +147,7 @@ def controls_location(cls, location): show_stdout=False, on_returncode='raise', log_failed_cmd=False) + return True except (BadCommand, InstallationError): return False From 7e11e25bfbb7edaf6dce1736fd47fc0351cfbc51 Mon Sep 17 00:00:00 2001 From: Xavier Fernandez <xavier.fernandez@polyconseil.fr> Date: Fri, 11 Oct 2019 11:14:09 +0200 Subject: [PATCH 0525/3170] Update AUTHORS.txt --- AUTHORS.txt | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/AUTHORS.txt b/AUTHORS.txt index f0d346e1436..fff9c3595e2 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -7,6 +7,8 @@ Adam Tse <adam.tse@me.com> Adam Tse <atse@users.noreply.github.com> Adam Wentz <awentz@theonion.com> Adrien Morison <adrien.morison@gmail.com> +ahayrapetyan <ahayrapetya2@bloomberg.net> +AinsworthK <yat626@yahoo.com.hk> Alan Yee <alyee@ucsd.edu> Albert Tugushev <albert@tugushev.ru> Albert-Guan <albert.guan94@gmail.com> @@ -25,6 +27,7 @@ Alexey Popravka <alexey.popravka@horsedevel.com> Alli <alzeih@users.noreply.github.com> Ami Fischman <ami@fischman.org> Anatoly Techtonik <techtonik@gmail.com> +Anders Kaseorg <andersk@mit.edu> Andreas Lutro <anlutro@gmail.com> Andrei Geacar <andrei.geacar@gmail.com> Andrew Gaul <andrew@gaul.org> @@ -34,6 +37,7 @@ Andrés Delfino <adelfino@gmail.com> Andy Freeland <andy.freeland@redjack.com> Andy Freeland <andy@andyfreeland.net> Andy Kluger <AndydeCleyre@users.noreply.github.com> +Ani Hayrapetyan <ahayrapetya2@bloomberg.net> Anish Tambe <anish.tambe@yahoo.in> Anrs Hu <anrs@douban.com> Anthony Sottile <asottile@umich.edu> @@ -51,6 +55,8 @@ Arindam Choudhury <arindam@live.com> Armin Ronacher <armin.ronacher@active-4.com> Artem <duketemon@users.noreply.github.com> Ashley Manton <ajd.manton@googlemail.com> +Ashwin Ramaswami <aramaswamis@gmail.com> +atse <atse@users.noreply.github.com> Atsushi Odagiri <aodagx@gmail.com> Avner Cohen <israbirding@gmail.com> Baptiste Mispelon <bmispelon@gmail.com> @@ -163,6 +169,7 @@ ekristina <panacejja@gmail.com> elainechan <elaine.chan@outlook.com> Eli Schwartz <eschwartz93@gmail.com> Eli Schwartz <eschwartz@archlinux.org> +Emil Burzo <contact@emilburzo.com> Emil Styrke <emil.styrke@gmail.com> Endoh Takanao <djmchl@gmail.com> enoch <lanxenet@gmail.com> @@ -183,6 +190,7 @@ Florian Briand <ownerfrance+github@hotmail.com> Florian Rathgeber <florian.rathgeber@gmail.com> Francesco <f.guerrieri@gmail.com> Francesco Montesano <franz.bergesund@gmail.com> +Frost Ming <mianghong@gmail.com> Gabriel Curio <g.curio@gmail.com> Gabriel de Perthuis <g2p.code@gmail.com> Garry Polley <garrympolley@gmail.com> @@ -195,6 +203,7 @@ Giftlin Rajaiah <giftlin.rgn@gmail.com> gizmoguy1 <gizmoguy1@gmail.com> gkdoc <40815324+gkdoc@users.noreply.github.com> GOTO Hayato <3532528+gh640@users.noreply.github.com> +gpiks <gaurav.pikale@gmail.com> Guilherme Espada <porcariadagata@gmail.com> Guy Rozendorn <guy@rzn.co.il> gzpan123 <gzpan123@gmail.com> @@ -217,6 +226,7 @@ Ilya Baryshev <baryshev@gmail.com> INADA Naoki <songofacandy@gmail.com> Ionel Cristian Mărieș <contact@ionelmc.ro> Ionel Maries Cristian <ionel.mc@gmail.com> +Ivan Pozdeev <vano@mail.mipt.ru> jakirkham <jakirkham@gmail.com> Jakub Stasiak <kuba.stasiak@gmail.com> Jakub Vysoky <jakub@borka.cz> @@ -238,6 +248,7 @@ Jeremy Stanley <fungi@yuggoth.org> Jeremy Zafran <jzafran@users.noreply.github.com> Jim Garrison <jim@garrison.cc> Jivan Amara <Development@JivanAmara.net> +John Paton <j.paton@catawiki.nl> John-Scott Atlakson <john.scott.atlakson@gmail.com> johnthagen <johnthagen@gmail.com> johnthagen <johnthagen@users.noreply.github.com> @@ -272,6 +283,7 @@ Kevin R Patterson <kevin.r.patterson@intel.com> Kexuan Sun <me@kianasun.com> Kit Randel <kit@nocturne.net.nz> kpinc <kop@meme.com> +Krishna Oza <krishoza15sep@gmail.com> Kumar McMillan <kumar.mcmillan@gmail.com> Kyle Persohn <kyle.persohn@gmail.com> lakshmanaram <lakshmanaram.n@gmail.com> @@ -317,6 +329,7 @@ Matthias Bussonnier <bussonniermatthias@gmail.com> mattip <matti.picus@gmail.com> Maxim Kurnikov <maxim.kurnikov@gmail.com> Maxime Rouyrre <rouyrre+git@gmail.com> +mayeut <mayeut@users.noreply.github.com> mbaluna <44498973+mbaluna@users.noreply.github.com> memoselyk <memoselyk@gmail.com> Michael <michael-k@users.noreply.github.com> @@ -328,6 +341,8 @@ michaelpacer <michaelpacer@gmail.com> Mickaël Schoentgen <mschoentgen@nuxeo.com> Miguel Araujo Perez <miguel.araujo.perez@gmail.com> Mihir Singh <git.service@mihirsingh.com> +Mike <mikeh@blur.com> +Mike Hendricks <mikeh@blur.com> Min RK <benjaminrk@gmail.com> MinRK <benjaminrk@gmail.com> Miro Hrončok <miro@hroncok.cz> @@ -344,17 +359,21 @@ Nicolas Bock <nicolasbock@gmail.com> Nikhil Benesch <nikhil.benesch@gmail.com> Nitesh Sharma <nbsharma@outlook.com> Nowell Strite <nowell@strite.org> +NtaleGrey <Shadikntale@gmail.com> nvdv <modestdev@gmail.com> Ofekmeister <ofekmeister@gmail.com> +ofrinevo <ofrine@gmail.com> Oliver Jeeves <oliver.jeeves@ocado.com> Oliver Tonnhofer <olt@bogosoft.com> Olivier Girardot <ssaboum@gmail.com> Olivier Grisel <olivier.grisel@ensta.org> Ollie Rutherfurd <orutherfurd@gmail.com> OMOTO Kenji <k-omoto@m3.com> +Omry Yadan <omry@fb.com> Oren Held <orenhe@il.ibm.com> Oscar Benjamin <oscar.j.benjamin@gmail.com> Oz N Tiram <oz.tiram@gmail.com> +Pachwenko <32424503+Pachwenko@users.noreply.github.com> Patrick Dubroy <pdubroy@gmail.com> Patrick Jenkins <patrick@socialgrowthtechnologies.com> Patrick Lawson <pl@foursquare.com> @@ -394,6 +413,7 @@ R. David Murray <rdmurray@bitdance.com> Rafael Caricio <rafael.jacinto@gmail.com> Ralf Schmitt <ralf@systemexit.de> Razzi Abuissa <razzi53@gmail.com> +rdb <rdb@users.noreply.github.com> Remi Rampin <remirampin@gmail.com> Rene Dudfield <renesd@gmail.com> Riccardo Magliocchetti <riccardo.magliocchetti@gmail.com> @@ -445,6 +465,9 @@ stonebig <stonebig34@gmail.com> Stéphane Bidoul (ACSONE) <stephane.bidoul@acsone.eu> Stéphane Bidoul <stephane.bidoul@acsone.eu> Stéphane Klein <contact@stephane-klein.info> +Sumana Harihareswara <sh@changeset.nyc> +Sviatoslav Sydorenko <wk.cvs.github@sydorenko.org.ua> +Sviatoslav Sydorenko <wk@sydorenko.org.ua> Takayuki SHIMIZUKAWA <shimizukawa@gmail.com> Thijs Triemstra <info@collab.nl> Thomas Fenzl <thomas.fenzl@gmail.com> @@ -473,6 +496,7 @@ Viktor Szépe <viktor@szepe.net> Ville Skyttä <ville.skytta@iki.fi> Vinay Sajip <vinay_sajip@yahoo.co.uk> Vincent Philippon <sindaewoh@gmail.com> +Vinicyus Macedo <7549205+vinicyusmacedo@users.noreply.github.com> Vitaly Babiy <vbabiy86@gmail.com> Vladimir Rutsky <rutsky@users.noreply.github.com> W. Trevor King <wking@drexel.edu> @@ -480,6 +504,7 @@ Wil Tan <wil@dready.org> Wilfred Hughes <me@wilfred.me.uk> William ML Leslie <william.leslie.ttg@gmail.com> William T Olson <trevor@heytrevor.com> +Wilson Mo <wilsonfv@126.com> wim glenn <wim.glenn@gmail.com> Wolfgang Maier <wolfgang.maier@biologie.uni-freiburg.de> Xavier Fernandez <xav.fernandez@gmail.com> From 4f7da85e2c644d79429f9991b2dacce9e405dc02 Mon Sep 17 00:00:00 2001 From: anatoly techtonik <techtonik@gmail.com> Date: Fri, 11 Oct 2019 18:11:05 +0300 Subject: [PATCH 0526/3170] Rename news item to .trivial per review --- news/{6004.removal => 6004.trivial} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename news/{6004.removal => 6004.trivial} (100%) diff --git a/news/6004.removal b/news/6004.trivial similarity index 100% rename from news/6004.removal rename to news/6004.trivial From e8b71ea33ee0fd4ab06d96bf5411d5958999b04c Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 11 Oct 2019 19:15:51 -0400 Subject: [PATCH 0527/3170] Simplify constructing editable install args --- src/pip/_internal/req/req_install.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 5f1fdb74b1e..517d7d009c2 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -687,20 +687,23 @@ def install_editable( # type: (...) -> None logger.info('Running setup.py develop for %s', self.name) - if prefix: - prefix_param = ['--prefix={}'.format(prefix)] - install_options = list(install_options) + prefix_param - base_cmd = make_setuptools_shim_args( + args = make_setuptools_shim_args( self.setup_py_path, global_options=global_options, no_user_config=self.isolated ) + + args.extend(["develop", "--no-deps"]) + + args.extend(install_options) + + if prefix: + args.extend(["--prefix", prefix]) + with indent_log(): with self.build_env: call_subprocess( - base_cmd + - ['develop', '--no-deps'] + - list(install_options), + args, cwd=self.unpacked_source_directory, ) From c202ae9d7e9bdc471a9c9c718bfd384bdbd3b6f9 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 11 Oct 2019 19:29:26 -0400 Subject: [PATCH 0528/3170] Make editable install args in setuptools_build --- src/pip/_internal/req/req_install.py | 18 +++++++------- src/pip/_internal/utils/setuptools_build.py | 26 ++++++++++++++++++++- 2 files changed, 33 insertions(+), 11 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 517d7d009c2..4c07a9bc93b 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -48,7 +48,10 @@ rmtree, ) from pip._internal.utils.packaging import get_metadata -from pip._internal.utils.setuptools_build import make_setuptools_shim_args +from pip._internal.utils.setuptools_build import ( + make_setuptools_develop_args, + make_setuptools_shim_args, +) from pip._internal.utils.subprocess import ( call_subprocess, runner_with_spinner_message, @@ -687,19 +690,14 @@ def install_editable( # type: (...) -> None logger.info('Running setup.py develop for %s', self.name) - args = make_setuptools_shim_args( + args = make_setuptools_develop_args( self.setup_py_path, global_options=global_options, - no_user_config=self.isolated + install_options=install_options, + no_user_config=self.isolated, + prefix=prefix, ) - args.extend(["develop", "--no-deps"]) - - args.extend(install_options) - - if prefix: - args.extend(["--prefix", prefix]) - with indent_log(): with self.build_env: call_subprocess( diff --git a/src/pip/_internal/utils/setuptools_build.py b/src/pip/_internal/utils/setuptools_build.py index 12d866e00a0..4d8b0e1dda0 100644 --- a/src/pip/_internal/utils/setuptools_build.py +++ b/src/pip/_internal/utils/setuptools_build.py @@ -3,7 +3,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import List, Sequence + from typing import List, Optional, Sequence # Shim to wrap setup.py invocation with setuptools # @@ -45,3 +45,27 @@ def make_setuptools_shim_args( if no_user_config: args.append('--no-user-cfg') return args + + +def make_setuptools_develop_args( + setup_py_path, # type: str + global_options, # type: Sequence[str] + install_options, # type: Sequence[str] + no_user_config, # type: bool + prefix, # type: Optional[str] +): + # type: (...) -> List[str] + args = make_setuptools_shim_args( + setup_py_path, + global_options=global_options, + no_user_config=no_user_config, + ) + + args.extend(["develop", "--no-deps"]) + + args.extend(install_options) + + if prefix: + args.extend(["--prefix", prefix]) + + return args From 1a6d4036903727946c4cb738ad0fd764873b43fc Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 11 Oct 2019 20:09:39 -0400 Subject: [PATCH 0529/3170] Parameterize member access before moving function --- src/pip/_internal/req/req_install.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 4c07a9bc93b..cd3c8f03e36 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -881,7 +881,13 @@ def install( with TempDirectory(kind="record") as temp_dir: record_filename = os.path.join(temp_dir.path, 'install-record.txt') install_args = self.get_install_args( - global_options, record_filename, root, prefix, pycompile, + self.setup_py_path, + global_options=global_options, + record_filename=record_filename, + root=root, + prefix=prefix, + no_user_config=self.isolated, + pycompile=pycompile, ) runner = runner_with_spinner_message( @@ -936,17 +942,19 @@ def prepend_root(path): def get_install_args( self, + setup_py_path, # type: str global_options, # type: Sequence[str] record_filename, # type: str root, # type: Optional[str] prefix, # type: Optional[str] + no_user_config, # type: bool pycompile # type: bool ): # type: (...) -> List[str] install_args = make_setuptools_shim_args( - self.setup_py_path, + setup_py_path, global_options=global_options, - no_user_config=self.isolated, + no_user_config=no_user_config, unbuffered_output=True ) install_args += ['install', '--record', record_filename] From dcd3509eea842179d01f22ad77aeba5452d2d705 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 11 Oct 2019 20:16:26 -0400 Subject: [PATCH 0530/3170] Finish extracting InstallRequirement.get_install_args --- src/pip/_internal/req/req_install.py | 22 ++++++++++++++++------ 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index cd3c8f03e36..1f07f77e4e8 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -878,14 +878,23 @@ def install( install_options = list(install_options) + \ self.options.get('install_options', []) + header_dir = None # type: Optional[str] + if running_under_virtualenv(): + py_ver_str = 'python' + sysconfig.get_python_version() + header_dir = os.path.join( + sys.prefix, 'include', 'site', py_ver_str, self.name + ) + with TempDirectory(kind="record") as temp_dir: record_filename = os.path.join(temp_dir.path, 'install-record.txt') install_args = self.get_install_args( self.setup_py_path, global_options=global_options, + install_options=install_options, record_filename=record_filename, root=root, prefix=prefix, + header_dir=header_dir, no_user_config=self.isolated, pycompile=pycompile, ) @@ -895,7 +904,7 @@ def install( ) with indent_log(), self.build_env: runner( - cmd=install_args + install_options, + cmd=install_args, cwd=self.unpacked_source_directory, ) @@ -944,9 +953,11 @@ def get_install_args( self, setup_py_path, # type: str global_options, # type: Sequence[str] + install_options, # type: Sequence[str] record_filename, # type: str root, # type: Optional[str] prefix, # type: Optional[str] + header_dir, # type: Optional[str] no_user_config, # type: bool pycompile # type: bool ): @@ -970,10 +981,9 @@ def get_install_args( else: install_args += ["--no-compile"] - if running_under_virtualenv(): - py_ver_str = 'python' + sysconfig.get_python_version() - install_args += ['--install-headers', - os.path.join(sys.prefix, 'include', 'site', - py_ver_str, self.name)] + if header_dir: + install_args += ['--install-headers', header_dir] + + install_args += install_options return install_args From c69d194d642546df95187b940f39ff892584effb Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 11 Oct 2019 20:25:50 -0400 Subject: [PATCH 0531/3170] Move install args construction to setuptools_build --- src/pip/_internal/req/req_install.py | 43 +-------------------- src/pip/_internal/utils/setuptools_build.py | 39 +++++++++++++++++++ 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 1f07f77e4e8..e520d167c8e 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -50,7 +50,7 @@ from pip._internal.utils.packaging import get_metadata from pip._internal.utils.setuptools_build import ( make_setuptools_develop_args, - make_setuptools_shim_args, + make_setuptools_install_args, ) from pip._internal.utils.subprocess import ( call_subprocess, @@ -887,7 +887,7 @@ def install( with TempDirectory(kind="record") as temp_dir: record_filename = os.path.join(temp_dir.path, 'install-record.txt') - install_args = self.get_install_args( + install_args = make_setuptools_install_args( self.setup_py_path, global_options=global_options, install_options=install_options, @@ -948,42 +948,3 @@ def prepend_root(path): inst_files_path = os.path.join(egg_info_dir, 'installed-files.txt') with open(inst_files_path, 'w') as f: f.write('\n'.join(new_lines) + '\n') - - def get_install_args( - self, - setup_py_path, # type: str - global_options, # type: Sequence[str] - install_options, # type: Sequence[str] - record_filename, # type: str - root, # type: Optional[str] - prefix, # type: Optional[str] - header_dir, # type: Optional[str] - no_user_config, # type: bool - pycompile # type: bool - ): - # type: (...) -> List[str] - install_args = make_setuptools_shim_args( - setup_py_path, - global_options=global_options, - no_user_config=no_user_config, - unbuffered_output=True - ) - install_args += ['install', '--record', record_filename] - install_args += ['--single-version-externally-managed'] - - if root is not None: - install_args += ['--root', root] - if prefix is not None: - install_args += ['--prefix', prefix] - - if pycompile: - install_args += ["--compile"] - else: - install_args += ["--no-compile"] - - if header_dir: - install_args += ['--install-headers', header_dir] - - install_args += install_options - - return install_args diff --git a/src/pip/_internal/utils/setuptools_build.py b/src/pip/_internal/utils/setuptools_build.py index 4d8b0e1dda0..495a703e9a0 100644 --- a/src/pip/_internal/utils/setuptools_build.py +++ b/src/pip/_internal/utils/setuptools_build.py @@ -69,3 +69,42 @@ def make_setuptools_develop_args( args.extend(["--prefix", prefix]) return args + + +def make_setuptools_install_args( + setup_py_path, # type: str + global_options, # type: Sequence[str] + install_options, # type: Sequence[str] + record_filename, # type: str + root, # type: Optional[str] + prefix, # type: Optional[str] + header_dir, # type: Optional[str] + no_user_config, # type: bool + pycompile # type: bool +): + # type: (...) -> List[str] + install_args = make_setuptools_shim_args( + setup_py_path, + global_options=global_options, + no_user_config=no_user_config, + unbuffered_output=True + ) + install_args += ['install', '--record', record_filename] + install_args += ['--single-version-externally-managed'] + + if root is not None: + install_args += ['--root', root] + if prefix is not None: + install_args += ['--prefix', prefix] + + if pycompile: + install_args += ["--compile"] + else: + install_args += ["--no-compile"] + + if header_dir: + install_args += ['--install-headers', header_dir] + + install_args += install_options + + return install_args From 578de7d863d22c769aba98a6d7e07c1c78f35546 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 11 Oct 2019 21:31:35 -0400 Subject: [PATCH 0532/3170] Rename wheel install function --- src/pip/_internal/req/req_install.py | 2 +- src/pip/_internal/wheel.py | 2 +- tests/unit/test_wheel.py | 8 ++++---- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 5f1fdb74b1e..3603f0666d1 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -484,7 +484,7 @@ def move_wheel_files( pycompile=True # type: bool ): # type: (...) -> None - wheel.move_wheel_files( + wheel.install_unpacked_wheel( self.name, self.req, wheeldir, user=use_user_site, home=home, diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index 8f9778c7d29..b8f8721a537 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -330,7 +330,7 @@ def make(self, specification, options=None): return super(PipScriptMaker, self).make(specification, options) -def move_wheel_files( +def install_unpacked_wheel( name, # type: str req, # type: Requirement wheeldir, # type: str diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 2a824c7fd7b..b78cd280fc7 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -619,7 +619,7 @@ def test_version_underscore_conversion(self): assert w.version == '0.1-1' -class TestMoveWheelFiles(object): +class TestInstallUnpackedWheel(object): """ Tests for moving files from wheel src to scheme paths """ @@ -659,14 +659,14 @@ def assert_installed(self): def test_std_install(self, data, tmpdir): self.prep(data, tmpdir) - wheel.move_wheel_files( + wheel.install_unpacked_wheel( self.name, self.req, self.src, scheme=self.scheme) self.assert_installed() def test_install_prefix(self, data, tmpdir): prefix = os.path.join(os.path.sep, 'some', 'path') self.prep(data, tmpdir) - wheel.move_wheel_files( + wheel.install_unpacked_wheel( self.name, self.req, self.src, @@ -688,7 +688,7 @@ def test_dist_info_contains_empty_dir(self, data, tmpdir): self.src_dist_info, 'empty_dir', 'empty_dir') os.makedirs(src_empty_dir) assert os.path.isdir(src_empty_dir) - wheel.move_wheel_files( + wheel.install_unpacked_wheel( self.name, self.req, self.src, scheme=self.scheme) self.assert_installed() assert not os.path.isdir( From 4682f3cb9b3d9d1430429f0917ed9da42868f26b Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 11 Oct 2019 21:49:39 -0400 Subject: [PATCH 0533/3170] Pass scheme to install_unpacked_wheel This reduces the number of required arguments and helps establish a convention for install_* functions, which should take a scheme instead of the individual components. --- src/pip/_internal/req/req_install.py | 27 ++++++++++++++------------- src/pip/_internal/wheel.py | 15 ++------------- tests/unit/test_wheel.py | 12 ++++++++++-- 3 files changed, 26 insertions(+), 28 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 3603f0666d1..39485c09eb7 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -23,6 +23,7 @@ from pip._internal import pep425tags, wheel from pip._internal.build_env import NoOpBuildEnvironment from pip._internal.exceptions import InstallationError +from pip._internal.locations import distutils_scheme from pip._internal.models.link import Link from pip._internal.operations.generate_metadata import get_metadata_generator from pip._internal.pyproject import load_pyproject_toml, make_pyproject_path @@ -60,7 +61,7 @@ if MYPY_CHECK_RUNNING: from typing import ( - Any, Dict, Iterable, List, Optional, Sequence, Union, + Any, Dict, Iterable, List, Mapping, Optional, Sequence, Union, ) from pip._internal.build_env import BuildEnvironment from pip._internal.cache import WheelCache @@ -476,22 +477,17 @@ def is_wheel(self): def move_wheel_files( self, wheeldir, # type: str - root=None, # type: Optional[str] - home=None, # type: Optional[str] - prefix=None, # type: Optional[str] + scheme, # type: Mapping[str, str] warn_script_location=True, # type: bool - use_user_site=False, # type: bool pycompile=True # type: bool ): # type: (...) -> None wheel.install_unpacked_wheel( - self.name, self.req, wheeldir, - user=use_user_site, - home=home, - root=root, - prefix=prefix, + self.name, + self.req, + wheeldir, + scheme=scheme, pycompile=pycompile, - isolated=self.isolated, warn_script_location=warn_script_location, ) @@ -859,10 +855,15 @@ def install( version = wheel.wheel_version(self.source_dir) wheel.check_compatibility(version, self.name) + scheme = distutils_scheme( + self.name, user=use_user_site, home=home, root=root, + isolated=self.isolated, prefix=prefix, + ) self.move_wheel_files( - self.source_dir, root=root, prefix=prefix, home=home, + self.source_dir, + scheme=scheme, warn_script_location=warn_script_location, - use_user_site=use_user_site, pycompile=pycompile, + pycompile=pycompile, ) self.install_succeeded = True return diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index b8f8721a537..342e111f29e 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -34,7 +34,7 @@ InvalidWheelFilename, UnsupportedWheel, ) -from pip._internal.locations import distutils_scheme, get_major_minor_version +from pip._internal.locations import get_major_minor_version from pip._internal.models.link import Link from pip._internal.utils.logging import indent_log from pip._internal.utils.marker_files import has_delete_marker_file @@ -334,13 +334,8 @@ def install_unpacked_wheel( name, # type: str req, # type: Requirement wheeldir, # type: str - user=False, # type: bool - home=None, # type: Optional[str] - root=None, # type: Optional[str] + scheme, # type: Mapping[str, str] pycompile=True, # type: bool - scheme=None, # type: Optional[Mapping[str, str]] - isolated=False, # type: bool - prefix=None, # type: Optional[str] warn_script_location=True # type: bool ): # type: (...) -> None @@ -349,12 +344,6 @@ def install_unpacked_wheel( # TODO: Look into moving this into a dedicated class for representing an # installation. - if not scheme: - scheme = distutils_scheme( - name, user=user, home=home, root=root, isolated=isolated, - prefix=prefix, - ) - if root_is_purelib(name, wheeldir): lib_dir = scheme['purelib'] else: diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index b78cd280fc7..946e2b4f7f1 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -10,6 +10,7 @@ from pip._internal import pep425tags, wheel from pip._internal.exceptions import InvalidWheelFilename, UnsupportedWheel +from pip._internal.locations import distutils_scheme from pip._internal.models.link import Link from pip._internal.req.req_install import InstallRequirement from pip._internal.utils.compat import WINDOWS @@ -666,12 +667,19 @@ def test_std_install(self, data, tmpdir): def test_install_prefix(self, data, tmpdir): prefix = os.path.join(os.path.sep, 'some', 'path') self.prep(data, tmpdir) + scheme = distutils_scheme( + self.name, + user=False, + home=None, + root=tmpdir, + isolated=False, + prefix=prefix, + ) wheel.install_unpacked_wheel( self.name, self.req, self.src, - root=tmpdir, - prefix=prefix, + scheme=scheme, ) bin_dir = 'Scripts' if WINDOWS else 'bin' From 39572ddd126ccfae546231c9d1d2dfcdcea73fab Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 11 Oct 2019 21:58:58 -0400 Subject: [PATCH 0534/3170] Don't pass InstallRequirement to install_unpacked_wheel We are only using this value for logging. Passing a string reduces coupling between InstallRequirement and this function. --- src/pip/_internal/req/req_install.py | 2 +- src/pip/_internal/wheel.py | 4 ++-- tests/unit/test_wheel.py | 6 +++--- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 39485c09eb7..b23f447b5d0 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -484,7 +484,7 @@ def move_wheel_files( # type: (...) -> None wheel.install_unpacked_wheel( self.name, - self.req, + str(self.req), wheeldir, scheme=scheme, pycompile=pycompile, diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index 342e111f29e..28bcd14c5c8 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -332,7 +332,7 @@ def make(self, specification, options=None): def install_unpacked_wheel( name, # type: str - req, # type: Requirement + req, # type: str wheeldir, # type: str scheme, # type: Mapping[str, str] pycompile=True, # type: bool @@ -393,7 +393,7 @@ def clobber(source, dest, is_base, fixer=None, filter=None): elif (is_base and s.endswith('.dist-info') and canonicalize_name(s).startswith( - canonicalize_name(req.name))): + canonicalize_name(name))): assert not info_dir, ('Multiple .dist-info directories: ' + destsubdir + ', ' + ', '.join(info_dir)) diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 946e2b4f7f1..fbb492d70af 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -661,7 +661,7 @@ def assert_installed(self): def test_std_install(self, data, tmpdir): self.prep(data, tmpdir) wheel.install_unpacked_wheel( - self.name, self.req, self.src, scheme=self.scheme) + self.name, str(self.req), self.src, scheme=self.scheme) self.assert_installed() def test_install_prefix(self, data, tmpdir): @@ -677,7 +677,7 @@ def test_install_prefix(self, data, tmpdir): ) wheel.install_unpacked_wheel( self.name, - self.req, + str(self.req), self.src, scheme=scheme, ) @@ -697,7 +697,7 @@ def test_dist_info_contains_empty_dir(self, data, tmpdir): os.makedirs(src_empty_dir) assert os.path.isdir(src_empty_dir) wheel.install_unpacked_wheel( - self.name, self.req, self.src, scheme=self.scheme) + self.name, str(self.req), self.src, scheme=self.scheme) self.assert_installed() assert not os.path.isdir( os.path.join(self.dest_dist_info, 'empty_dir')) From 913f85673929612d3f9c6fdfcec094297caa65ed Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 11 Oct 2019 22:24:50 -0400 Subject: [PATCH 0535/3170] Cleanup arguments, add docstring --- src/pip/_internal/req/req_install.py | 2 +- src/pip/_internal/wheel.py | 38 +++++++++++++++++++--------- tests/unit/test_wheel.py | 14 +++++++--- 3 files changed, 38 insertions(+), 16 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index b23f447b5d0..fd198078c9c 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -484,9 +484,9 @@ def move_wheel_files( # type: (...) -> None wheel.install_unpacked_wheel( self.name, - str(self.req), wheeldir, scheme=scheme, + req_description=str(self.req), pycompile=pycompile, warn_script_location=warn_script_location, ) diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index 28bcd14c5c8..70d52d29d9d 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -57,7 +57,6 @@ Dict, List, Optional, Sequence, Mapping, Tuple, IO, Text, Any, Iterable, Callable, Set, ) - from pip._vendor.packaging.requirements import Requirement from pip._internal.req.req_install import InstallRequirement from pip._internal.operations.prepare import ( RequirementPreparer @@ -332,14 +331,24 @@ def make(self, specification, options=None): def install_unpacked_wheel( name, # type: str - req, # type: str wheeldir, # type: str scheme, # type: Mapping[str, str] + req_description, # type: str pycompile=True, # type: bool warn_script_location=True # type: bool ): # type: (...) -> None - """Install a wheel""" + """Install a wheel. + + :param name: Name of the project to install + :param wheeldir: Base directory of the unpacked wheel + :param scheme: Distutils scheme dictating the install directories + :param req_description: String used in place of the requirement, for + logging + :param pycompile: Whether to byte-compile installed Python files + :param warn_script_location: Whether to check that scripts are installed + into a directory on PATH + """ # TODO: Investigate and break this up. # TODO: Look into moving this into a dedicated class for representing an # installation. @@ -390,13 +399,16 @@ def clobber(source, dest, is_base, fixer=None, filter=None): if is_base and basedir == '' and destsubdir.endswith('.data'): data_dirs.append(s) continue - elif (is_base and - s.endswith('.dist-info') and - canonicalize_name(s).startswith( - canonicalize_name(name))): - assert not info_dir, ('Multiple .dist-info directories: ' + - destsubdir + ', ' + - ', '.join(info_dir)) + elif ( + is_base and + s.endswith('.dist-info') and + canonicalize_name(s).startswith(canonicalize_name(name)) + ): + assert not info_dir, ( + 'Multiple .dist-info directories: {}, '.format( + destsubdir + ) + ', '.join(info_dir) + ) info_dir.append(destsubdir) for f in files: # Skip unwanted files @@ -449,7 +461,9 @@ def clobber(source, dest, is_base, fixer=None, filter=None): clobber(source, lib_dir, True) - assert info_dir, "%s .dist-info directory not found" % req + assert info_dir, "{} .dist-info directory not found".format( + req_description + ) # Get the defined entry points ep_file = os.path.join(info_dir[0], 'entry_points.txt') @@ -592,7 +606,7 @@ def is_entrypoint_wrapper(name): "Invalid script entry point: {} for req: {} - A callable " "suffix is required. Cf https://packaging.python.org/en/" "latest/distributing.html#console-scripts for more " - "information.".format(entry, req) + "information.".format(entry, req_description) ) if warn_script_location: diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index fbb492d70af..cd658cca007 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -661,7 +661,11 @@ def assert_installed(self): def test_std_install(self, data, tmpdir): self.prep(data, tmpdir) wheel.install_unpacked_wheel( - self.name, str(self.req), self.src, scheme=self.scheme) + self.name, + self.src, + scheme=self.scheme, + req_description=str(self.req), + ) self.assert_installed() def test_install_prefix(self, data, tmpdir): @@ -677,9 +681,9 @@ def test_install_prefix(self, data, tmpdir): ) wheel.install_unpacked_wheel( self.name, - str(self.req), self.src, scheme=scheme, + req_description=str(self.req), ) bin_dir = 'Scripts' if WINDOWS else 'bin' @@ -697,7 +701,11 @@ def test_dist_info_contains_empty_dir(self, data, tmpdir): os.makedirs(src_empty_dir) assert os.path.isdir(src_empty_dir) wheel.install_unpacked_wheel( - self.name, str(self.req), self.src, scheme=self.scheme) + self.name, + self.src, + scheme=self.scheme, + req_description=str(self.req), + ) self.assert_installed() assert not os.path.isdir( os.path.join(self.dest_dist_info, 'empty_dir')) From 28f3dcc64142d9ae427442710931049aae7c6adc Mon Sep 17 00:00:00 2001 From: Sebastian Jordan <sebastian.jordan.mail@googlemail.com> Date: Sat, 12 Oct 2019 12:19:52 +0200 Subject: [PATCH 0536/3170] Add missing newline characters in pep517_setup_and_pyproject test data --- tests/data/packages/pep517_setup_and_pyproject/pyproject.toml | 2 +- tests/data/packages/pep517_setup_and_pyproject/setup.cfg | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/data/packages/pep517_setup_and_pyproject/pyproject.toml b/tests/data/packages/pep517_setup_and_pyproject/pyproject.toml index 86df60002ac..5a561e83317 100644 --- a/tests/data/packages/pep517_setup_and_pyproject/pyproject.toml +++ b/tests/data/packages/pep517_setup_and_pyproject/pyproject.toml @@ -1,3 +1,3 @@ [build-system] requires = [ "setuptools" ] -build-backend = "setuptools.build_meta" \ No newline at end of file +build-backend = "setuptools.build_meta" diff --git a/tests/data/packages/pep517_setup_and_pyproject/setup.cfg b/tests/data/packages/pep517_setup_and_pyproject/setup.cfg index 88446933ec6..7eae9c0111e 100644 --- a/tests/data/packages/pep517_setup_and_pyproject/setup.cfg +++ b/tests/data/packages/pep517_setup_and_pyproject/setup.cfg @@ -1,3 +1,3 @@ [metadata] name = pep517-setup-and-pyproject -version = 1.0 \ No newline at end of file +version = 1.0 From 483daaba439cc1bdad76b706ed547a61d4c19667 Mon Sep 17 00:00:00 2001 From: Sebastian Jordan <sebastian.jordan.mail@googlemail.com> Date: Sat, 12 Oct 2019 13:17:43 +0200 Subject: [PATCH 0537/3170] Add trailing newline in news/6606.bugfix --- news/6606.bugfix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/6606.bugfix b/news/6606.bugfix index 990a7570121..3fbf7262f14 100644 --- a/news/6606.bugfix +++ b/news/6606.bugfix @@ -1 +1 @@ -Fix bug that prevented installation of PEP 517 packages without `setup.py` \ No newline at end of file +Fix bug that prevented installation of PEP 517 packages without ``setup.py``. From 8c66447bad7ceff2cac171c59b17df4801e41005 Mon Sep 17 00:00:00 2001 From: Albert Tugushev <albert@tugushev.ru> Date: Sat, 12 Oct 2019 11:02:26 +0700 Subject: [PATCH 0538/3170] Use python-version instead of deprecated version It's deprecated since https://github.com/actions/setup-python/commit/6f6fcee. --- .github/workflows/python-linters.yml | 2 +- news/fix-deprecated-version-key.trivial | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 news/fix-deprecated-version-key.trivial diff --git a/.github/workflows/python-linters.yml b/.github/workflows/python-linters.yml index 99c632166e4..705dea44db2 100644 --- a/.github/workflows/python-linters.yml +++ b/.github/workflows/python-linters.yml @@ -25,7 +25,7 @@ jobs: - name: Set up Python ${{ matrix.env.PYTHON_VERSION || 3.7 }} uses: actions/setup-python@v1 with: - version: ${{ matrix.env.PYTHON_VERSION || 3.7 }} + python-version: ${{ matrix.env.PYTHON_VERSION || 3.7 }} - name: Pre-configure global Git settings run: >- tools/travis/setup.sh diff --git a/news/fix-deprecated-version-key.trivial b/news/fix-deprecated-version-key.trivial new file mode 100644 index 00000000000..fc0f23a827a --- /dev/null +++ b/news/fix-deprecated-version-key.trivial @@ -0,0 +1 @@ +Use ``python-version`` instead of deprecated ``version``. From 440d4c2002275457663d657befd6525a2d785e30 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 11 Oct 2019 20:34:40 -0400 Subject: [PATCH 0539/3170] Clean up egg_info arg construction --- src/pip/_internal/operations/generate_metadata.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/operations/generate_metadata.py b/src/pip/_internal/operations/generate_metadata.py index 984748d7fdd..f37d0c7da2d 100644 --- a/src/pip/_internal/operations/generate_metadata.py +++ b/src/pip/_internal/operations/generate_metadata.py @@ -12,7 +12,8 @@ from pip._internal.vcs import vcs if MYPY_CHECK_RUNNING: - from typing import Callable, List + from typing import Callable, List, Optional + from pip._internal.req.req_install import InstallRequirement logger = logging.getLogger(__name__) @@ -104,22 +105,26 @@ def _generate_metadata_legacy(install_req): if install_req.isolated: base_cmd += ["--no-user-cfg"] + base_cmd += ["egg_info"] + + egg_info_dir = None # type: Optional[str] # For non-editable installs, don't put the .egg-info files at the root, # to avoid confusion due to the source code being considered an installed # egg. - egg_base_option = [] # type: List[str] if not install_req.editable: egg_info_dir = os.path.join( install_req.unpacked_source_directory, 'pip-egg-info', ) - egg_base_option = ['--egg-base', egg_info_dir] # setuptools complains if the target directory does not exist. ensure_dir(egg_info_dir) + if egg_info_dir: + base_cmd += ['--egg-base', egg_info_dir] + with install_req.build_env: call_subprocess( - base_cmd + ["egg_info"] + egg_base_option, + base_cmd, cwd=install_req.unpacked_source_directory, command_desc='python setup.py egg_info', ) From f00d7a13749c96e8a18df5b59481a77fc0da55d9 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 11 Oct 2019 20:40:52 -0400 Subject: [PATCH 0540/3170] Move egg_info args construction to setuptools_build --- .../_internal/operations/generate_metadata.py | 18 +++++++----------- src/pip/_internal/utils/setuptools_build.py | 18 ++++++++++++++++++ 2 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/pip/_internal/operations/generate_metadata.py b/src/pip/_internal/operations/generate_metadata.py index f37d0c7da2d..bfdeae517a1 100644 --- a/src/pip/_internal/operations/generate_metadata.py +++ b/src/pip/_internal/operations/generate_metadata.py @@ -6,7 +6,7 @@ from pip._internal.exceptions import InstallationError from pip._internal.utils.misc import ensure_dir -from pip._internal.utils.setuptools_build import make_setuptools_shim_args +from pip._internal.utils.setuptools_build import make_setuptools_egg_info_args from pip._internal.utils.subprocess import call_subprocess from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.vcs import vcs @@ -100,13 +100,6 @@ def _generate_metadata_legacy(install_req): install_req.setup_py_path, req_details_str, ) - # Compose arguments for subprocess call - base_cmd = make_setuptools_shim_args(install_req.setup_py_path) - if install_req.isolated: - base_cmd += ["--no-user-cfg"] - - base_cmd += ["egg_info"] - egg_info_dir = None # type: Optional[str] # For non-editable installs, don't put the .egg-info files at the root, # to avoid confusion due to the source code being considered an installed @@ -119,12 +112,15 @@ def _generate_metadata_legacy(install_req): # setuptools complains if the target directory does not exist. ensure_dir(egg_info_dir) - if egg_info_dir: - base_cmd += ['--egg-base', egg_info_dir] + args = make_setuptools_egg_info_args( + install_req.setup_py_path, + egg_info_dir=egg_info_dir, + no_user_config=install_req.isolated, + ) with install_req.build_env: call_subprocess( - base_cmd, + args, cwd=install_req.unpacked_source_directory, command_desc='python setup.py egg_info', ) diff --git a/src/pip/_internal/utils/setuptools_build.py b/src/pip/_internal/utils/setuptools_build.py index 495a703e9a0..fc4f06c79e5 100644 --- a/src/pip/_internal/utils/setuptools_build.py +++ b/src/pip/_internal/utils/setuptools_build.py @@ -71,6 +71,24 @@ def make_setuptools_develop_args( return args +def make_setuptools_egg_info_args( + setup_py_path, # type: str + egg_info_dir, # type: Optional[str] + no_user_config, # type: bool +): + # type: (...) -> List[str] + base_cmd = make_setuptools_shim_args(setup_py_path) + if no_user_config: + base_cmd += ["--no-user-cfg"] + + base_cmd += ["egg_info"] + + if egg_info_dir: + base_cmd += ['--egg-base', egg_info_dir] + + return base_cmd + + def make_setuptools_install_args( setup_py_path, # type: str global_options, # type: Sequence[str] From 8af0dc2e02590895d359f45a6bbe83fee1012047 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 11 Oct 2019 20:50:01 -0400 Subject: [PATCH 0541/3170] Normalize style --- src/pip/_internal/utils/setuptools_build.py | 52 ++++++++++----------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/src/pip/_internal/utils/setuptools_build.py b/src/pip/_internal/utils/setuptools_build.py index fc4f06c79e5..866d660a6f7 100644 --- a/src/pip/_internal/utils/setuptools_build.py +++ b/src/pip/_internal/utils/setuptools_build.py @@ -21,10 +21,10 @@ def make_setuptools_shim_args( - setup_py_path, # type: str - global_options=None, # type: Sequence[str] - no_user_config=False, # type: bool - unbuffered_output=False # type: bool + setup_py_path, # type: str + global_options=None, # type: Sequence[str] + no_user_config=False, # type: bool + unbuffered_output=False # type: bool ): # type: (...) -> List[str] """ @@ -38,12 +38,12 @@ def make_setuptools_shim_args( """ args = [sys.executable] if unbuffered_output: - args.append('-u') - args.extend(['-c', _SETUPTOOLS_SHIM.format(setup_py_path)]) + args += ["-u"] + args += ["-c", _SETUPTOOLS_SHIM.format(setup_py_path)] if global_options: - args.extend(global_options) + args += global_options if no_user_config: - args.append('--no-user-cfg') + args += ["--no-user-cfg"] return args @@ -61,12 +61,12 @@ def make_setuptools_develop_args( no_user_config=no_user_config, ) - args.extend(["develop", "--no-deps"]) + args += ["develop", "--no-deps"] - args.extend(install_options) + args += install_options if prefix: - args.extend(["--prefix", prefix]) + args += ["--prefix", prefix] return args @@ -77,16 +77,16 @@ def make_setuptools_egg_info_args( no_user_config, # type: bool ): # type: (...) -> List[str] - base_cmd = make_setuptools_shim_args(setup_py_path) + args = make_setuptools_shim_args(setup_py_path) if no_user_config: - base_cmd += ["--no-user-cfg"] + args += ["--no-user-cfg"] - base_cmd += ["egg_info"] + args += ["egg_info"] if egg_info_dir: - base_cmd += ['--egg-base', egg_info_dir] + args += ["--egg-base", egg_info_dir] - return base_cmd + return args def make_setuptools_install_args( @@ -101,28 +101,28 @@ def make_setuptools_install_args( pycompile # type: bool ): # type: (...) -> List[str] - install_args = make_setuptools_shim_args( + args = make_setuptools_shim_args( setup_py_path, global_options=global_options, no_user_config=no_user_config, unbuffered_output=True ) - install_args += ['install', '--record', record_filename] - install_args += ['--single-version-externally-managed'] + args += ["install", "--record", record_filename] + args += ["--single-version-externally-managed"] if root is not None: - install_args += ['--root', root] + args += ["--root", root] if prefix is not None: - install_args += ['--prefix', prefix] + args += ["--prefix", prefix] if pycompile: - install_args += ["--compile"] + args += ["--compile"] else: - install_args += ["--no-compile"] + args += ["--no-compile"] if header_dir: - install_args += ['--install-headers', header_dir] + args += ["--install-headers", header_dir] - install_args += install_options + args += install_options - return install_args + return args From 7e4e8eea40ffd3205f402d93a850ef11be3a86ae Mon Sep 17 00:00:00 2001 From: Desetude <harry@desetude.com> Date: Sat, 12 Oct 2019 17:08:10 +0100 Subject: [PATCH 0542/3170] Replace %-format with str#format --- src/pip/_internal/operations/prepare.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index d0930458d11..93484392a75 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -105,8 +105,8 @@ def _download_should_save(self): logger.critical('Could not find download directory') raise InstallationError( - "Could not find or access download directory '%s'" - % display_path(self.download_dir)) + "Could not find or access download directory '{}'" + .format(self.download_dir)) def prepare_linked_requirement( self, @@ -141,12 +141,12 @@ def prepare_linked_requirement( # 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 " + "pip can't proceed with requirements '{}' due to a" + " pre-existing build directory ({}). 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) + .format(req, req.source_dir) ) # Now that we have the real link, we can tell what kind of @@ -200,9 +200,8 @@ def prepare_linked_requirement( exc, ) raise InstallationError( - 'Could not install requirement %s because of HTTP ' - 'error %s for URL %s' % - (req, exc, link) + 'Could not install requirement {} because of HTTP ' + 'error {} for URL {}'.format(req, exc, link) ) if link.is_wheel: @@ -247,9 +246,9 @@ def prepare_editable_requirement( with indent_log(): if require_hashes: raise InstallationError( - 'The editable requirement %s cannot be installed when ' + 'The editable requirement {} cannot be installed when ' 'requiring hashes, because there is no single file to ' - 'hash.' % req + 'hash.'.format(req) ) req.ensure_has_source_dir(self.src_dir) req.update_editable(not self._download_should_save) @@ -276,7 +275,7 @@ def prepare_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,) + "is set to {}".format(req.satisfied_by) ) logger.info( 'Requirement %s: %s (%s)', From ed942ae3aadb1c9a2bde5bbacd6121127d9b4948 Mon Sep 17 00:00:00 2001 From: Desetude <harry@desetude.com> Date: Sat, 12 Oct 2019 17:14:47 +0100 Subject: [PATCH 0543/3170] Add #7178 trivial news file --- news/7178.trivial | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 news/7178.trivial diff --git a/news/7178.trivial b/news/7178.trivial new file mode 100644 index 00000000000..e69de29bb2d From d25dc68942a7889fca3a63cbf80ef8c1f376ae7d Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 12 Oct 2019 20:03:09 -0400 Subject: [PATCH 0544/3170] Factor get_dist implementation out of InstallRequirement With it outside of the class body and taking primitive values, this should be easier to move into a separate module. --- src/pip/_internal/req/req_install.py | 47 ++++++++++++++++------------ 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 5f1fdb74b1e..e525f103bfa 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -73,6 +73,32 @@ logger = logging.getLogger(__name__) +def _get_dist(metadata_directory): + # type: (str) -> Distribution + """Return a pkg_resources.Distribution for the provided + metadata directory. + """ + dist_dir = metadata_directory.rstrip(os.sep) + + # Determine the correct Distribution object type. + if dist_dir.endswith(".egg-info"): + dist_cls = pkg_resources.Distribution + else: + assert dist_dir.endswith(".dist-info") + dist_cls = pkg_resources.DistInfoDistribution + + # Build a PathMetadata object, from path to metadata. :wink: + base_dir, dist_dir_name = os.path.split(dist_dir) + dist_name = os.path.splitext(dist_dir_name)[0] + metadata = pkg_resources.PathMetadata(base_dir, dist_dir) + + return dist_cls( + base_dir, + project_name=dist_name, + metadata=metadata, + ) + + class InstallRequirement(object): """ Represents something that may be installed later on, may have information @@ -623,26 +649,7 @@ def metadata(self): def get_dist(self): # type: () -> Distribution - """Return a pkg_resources.Distribution for this requirement""" - dist_dir = self.metadata_directory.rstrip(os.sep) - - # Determine the correct Distribution object type. - if dist_dir.endswith(".egg-info"): - dist_cls = pkg_resources.Distribution - else: - assert dist_dir.endswith(".dist-info") - dist_cls = pkg_resources.DistInfoDistribution - - # Build a PathMetadata object, from path to metadata. :wink: - base_dir, dist_dir_name = os.path.split(dist_dir) - dist_name = os.path.splitext(dist_dir_name)[0] - metadata = pkg_resources.PathMetadata(base_dir, dist_dir) - - return dist_cls( - base_dir, - project_name=dist_name, - metadata=metadata, - ) + return _get_dist(self.metadata_directory) def assert_source_matches_version(self): # type: () -> None From c451bce2596ac6468d3ce5d1899cdeb0e4252cf7 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 12 Oct 2019 21:00:38 -0400 Subject: [PATCH 0545/3170] Separate functions for constructing setuptools args To prepare for factoring these out into a separate module. --- src/pip/_internal/wheel.py | 24 +++++++++++++++--------- 1 file changed, 15 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index 8f9778c7d29..029e486e223 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -957,7 +957,7 @@ def _build_one_inside_env(self, req, output_dir, python_tag=None): self._clean_one(req) return None - def _base_setup_args(self, req): + def _make_setuptools_bdist_wheel_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 @@ -968,6 +968,13 @@ def _base_setup_args(self, req): unbuffered_output=True ) + def _make_setuptools_clean_args(self, req): + return make_setuptools_shim_args( + req.setup_py_path, + global_options=self.global_options, + unbuffered_output=True + ) + def _build_one_pep517(self, req, tempd, python_tag=None): """Build one InstallRequirement using the PEP 517 build process. @@ -1012,16 +1019,15 @@ def _build_one_legacy(self, req, tempd, python_tag=None): Returns path to wheel if successfully built. Otherwise, returns None. """ - base_args = self._base_setup_args(req) + wheel_args = self._make_setuptools_bdist_wheel_args(req) + wheel_args += ['bdist_wheel', '-d', tempd] + wheel_args += self.build_options + if python_tag is not None: + wheel_args += ["--python-tag", python_tag] spin_message = 'Building wheel for %s (setup.py)' % (req.name,) with open_spinner(spin_message) as spinner: logger.debug('Destination directory: %s', tempd) - wheel_args = base_args + ['bdist_wheel', '-d', tempd] \ - + self.build_options - - if python_tag is not None: - wheel_args += ["--python-tag", python_tag] try: output = call_subprocess( @@ -1045,10 +1051,10 @@ def _build_one_legacy(self, req, tempd, python_tag=None): return wheel_path def _clean_one(self, req): - base_args = self._base_setup_args(req) + clean_args = self._make_setuptools_clean_args(req) + clean_args += ['clean', '--all'] logger.info('Running setup.py clean for %s', req.name) - clean_args = base_args + ['clean', '--all'] try: call_subprocess(clean_args, cwd=req.source_dir) return True From 3d931388dc4437e6e163db401f8d5cdfb93e2529 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 12 Oct 2019 21:12:32 -0400 Subject: [PATCH 0546/3170] Parameterize bdist_wheel and clean args functions --- src/pip/_internal/wheel.py | 54 +++++++++++++++++++++++++++----------- 1 file changed, 39 insertions(+), 15 deletions(-) diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index 029e486e223..bd2b836f881 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -957,23 +957,43 @@ def _build_one_inside_env(self, req, output_dir, python_tag=None): self._clean_one(req) return None - def _make_setuptools_bdist_wheel_args(self, req): + def _make_setuptools_bdist_wheel_args( + self, + setup_py_path, # type: str + global_options, # type: Sequence[str] + build_options, # type: Sequence[str] + destination_dir, # type: str + python_tag, # type: Optional[str] + ): + # type: (...) -> List[str] # NOTE: Eventually, we'd want to also -S to the flags here, when we're # isolating. Currently, it breaks Python in virtualenvs, because it # relies on site.py to find parts of the standard library outside the # virtualenv. - return make_setuptools_shim_args( - req.setup_py_path, - global_options=self.global_options, + args = make_setuptools_shim_args( + setup_py_path, + global_options=global_options, unbuffered_output=True ) + args += ["bdist_wheel", "-d", destination_dir] + args += build_options + if python_tag is not None: + args += ["--python-tag", python_tag] + return args - def _make_setuptools_clean_args(self, req): - return make_setuptools_shim_args( - req.setup_py_path, - global_options=self.global_options, + def _make_setuptools_clean_args( + self, + setup_py_path, # type: str + global_options, # type: Sequence[str] + ): + # type: (...) -> List[str] + args = make_setuptools_shim_args( + setup_py_path, + global_options=global_options, unbuffered_output=True ) + args += ["clean", "--all"] + return args def _build_one_pep517(self, req, tempd, python_tag=None): """Build one InstallRequirement using the PEP 517 build process. @@ -1019,11 +1039,13 @@ def _build_one_legacy(self, req, tempd, python_tag=None): Returns path to wheel if successfully built. Otherwise, returns None. """ - wheel_args = self._make_setuptools_bdist_wheel_args(req) - wheel_args += ['bdist_wheel', '-d', tempd] - wheel_args += self.build_options - if python_tag is not None: - wheel_args += ["--python-tag", python_tag] + wheel_args = self._make_setuptools_bdist_wheel_args( + req.setup_py_path, + global_options=self.global_options, + build_options=self.build_options, + destination_dir=tempd, + python_tag=python_tag, + ) spin_message = 'Building wheel for %s (setup.py)' % (req.name,) with open_spinner(spin_message) as spinner: @@ -1051,8 +1073,10 @@ def _build_one_legacy(self, req, tempd, python_tag=None): return wheel_path def _clean_one(self, req): - clean_args = self._make_setuptools_clean_args(req) - clean_args += ['clean', '--all'] + clean_args = self._make_setuptools_clean_args( + req.setup_py_path, + global_options=self.global_options, + ) logger.info('Running setup.py clean for %s', req.name) try: From 66c3e6010d95bcabae8095a4176f4aa31bae2f3a Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 12 Oct 2019 21:20:10 -0400 Subject: [PATCH 0547/3170] Move arg builder functions to setuptools_build --- src/pip/_internal/utils/setuptools_build.py | 38 +++++++++++++++++ src/pip/_internal/wheel.py | 47 +++------------------ 2 files changed, 44 insertions(+), 41 deletions(-) diff --git a/src/pip/_internal/utils/setuptools_build.py b/src/pip/_internal/utils/setuptools_build.py index 866d660a6f7..1bf416eb34d 100644 --- a/src/pip/_internal/utils/setuptools_build.py +++ b/src/pip/_internal/utils/setuptools_build.py @@ -47,6 +47,44 @@ def make_setuptools_shim_args( return args +def make_setuptools_bdist_wheel_args( + setup_py_path, # type: str + global_options, # type: Sequence[str] + build_options, # type: Sequence[str] + destination_dir, # type: str + python_tag, # type: Optional[str] +): + # type: (...) -> List[str] + # NOTE: Eventually, we'd want to also -S to the flags here, when we're + # isolating. Currently, it breaks Python in virtualenvs, because it + # relies on site.py to find parts of the standard library outside the + # virtualenv. + args = make_setuptools_shim_args( + setup_py_path, + global_options=global_options, + unbuffered_output=True + ) + args += ["bdist_wheel", "-d", destination_dir] + args += build_options + if python_tag is not None: + args += ["--python-tag", python_tag] + return args + + +def make_setuptools_clean_args( + setup_py_path, # type: str + global_options, # type: Sequence[str] +): + # type: (...) -> List[str] + args = make_setuptools_shim_args( + setup_py_path, + global_options=global_options, + unbuffered_output=True + ) + args += ["clean", "--all"] + return args + + def make_setuptools_develop_args( setup_py_path, # type: str global_options, # type: Sequence[str] diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index bd2b836f881..d524701bdac 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -39,7 +39,10 @@ from pip._internal.utils.logging import indent_log from pip._internal.utils.marker_files import has_delete_marker_file from pip._internal.utils.misc import captured_stdout, ensure_dir, read_chunks -from pip._internal.utils.setuptools_build import make_setuptools_shim_args +from pip._internal.utils.setuptools_build import ( + make_setuptools_bdist_wheel_args, + make_setuptools_clean_args, +) from pip._internal.utils.subprocess import ( LOG_DIVIDER, call_subprocess, @@ -957,44 +960,6 @@ def _build_one_inside_env(self, req, output_dir, python_tag=None): self._clean_one(req) return None - def _make_setuptools_bdist_wheel_args( - self, - setup_py_path, # type: str - global_options, # type: Sequence[str] - build_options, # type: Sequence[str] - destination_dir, # type: str - python_tag, # type: Optional[str] - ): - # type: (...) -> List[str] - # NOTE: Eventually, we'd want to also -S to the flags here, when we're - # isolating. Currently, it breaks Python in virtualenvs, because it - # relies on site.py to find parts of the standard library outside the - # virtualenv. - args = make_setuptools_shim_args( - setup_py_path, - global_options=global_options, - unbuffered_output=True - ) - args += ["bdist_wheel", "-d", destination_dir] - args += build_options - if python_tag is not None: - args += ["--python-tag", python_tag] - return args - - def _make_setuptools_clean_args( - self, - setup_py_path, # type: str - global_options, # type: Sequence[str] - ): - # type: (...) -> List[str] - args = make_setuptools_shim_args( - setup_py_path, - global_options=global_options, - unbuffered_output=True - ) - args += ["clean", "--all"] - return args - def _build_one_pep517(self, req, tempd, python_tag=None): """Build one InstallRequirement using the PEP 517 build process. @@ -1039,7 +1004,7 @@ def _build_one_legacy(self, req, tempd, python_tag=None): Returns path to wheel if successfully built. Otherwise, returns None. """ - wheel_args = self._make_setuptools_bdist_wheel_args( + wheel_args = make_setuptools_bdist_wheel_args( req.setup_py_path, global_options=self.global_options, build_options=self.build_options, @@ -1073,7 +1038,7 @@ def _build_one_legacy(self, req, tempd, python_tag=None): return wheel_path def _clean_one(self, req): - clean_args = self._make_setuptools_clean_args( + clean_args = make_setuptools_clean_args( req.setup_py_path, global_options=self.global_options, ) From e41b5bc6cc7c5bb8596a2f17149f7fae20c52fc2 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 13 Oct 2019 13:12:58 -0400 Subject: [PATCH 0548/3170] Parameterize require_hashes within Resolver Removes the only dynamic state from Resolver, at the cost of passing an extra variable to 2 methods. --- src/pip/_internal/legacy_resolve.py | 34 ++++++++++++++--------------- 1 file changed, 16 insertions(+), 18 deletions(-) diff --git a/src/pip/_internal/legacy_resolve.py b/src/pip/_internal/legacy_resolve.py index c24158f4d37..012089d63f7 100644 --- a/src/pip/_internal/legacy_resolve.py +++ b/src/pip/_internal/legacy_resolve.py @@ -142,9 +142,6 @@ def __init__( self.finder = finder self.session = session - # This is set in resolve - self.require_hashes = None # type: Optional[bool] - self.upgrade_strategy = upgrade_strategy self.force_reinstall = force_reinstall self.ignore_dependencies = ignore_dependencies @@ -178,8 +175,10 @@ def resolve(self, requirement_set): requirement_set.unnamed_requirements + list(requirement_set.requirements.values()) ) - self.require_hashes = ( - requirement_set.require_hashes or + + require_hashes_option = requirement_set.require_hashes + require_hashes = ( + require_hashes_option or any(req.has_hash_options for req in root_reqs) ) @@ -198,7 +197,7 @@ def resolve(self, requirement_set): for req in chain(root_reqs, discovered_reqs): try: discovered_reqs.extend( - self._resolve_one(requirement_set, req) + self._resolve_one(requirement_set, req, require_hashes) ) except HashError as exc: exc.req = req @@ -281,18 +280,14 @@ def _check_skip_installed(self, req_to_install): self._set_req_to_reinstall(req_to_install) return None - def _get_abstract_dist_for(self, req): - # type: (InstallRequirement) -> AbstractDistribution + def _get_abstract_dist_for(self, req, require_hashes): + # type: (InstallRequirement, bool) -> AbstractDistribution """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, + req, require_hashes, self.use_user_site, self.finder, ) # satisfied_by is only evaluated by calling _check_skip_installed, @@ -302,15 +297,15 @@ def _get_abstract_dist_for(self, req): if req.satisfied_by: return self.preparer.prepare_installed_requirement( - req, self.require_hashes, skip_reason + req, require_hashes, skip_reason ) upgrade_allowed = self._is_upgrade_allowed(req) # We eagerly populate the link, since that's our "legacy" behavior. - req.populate_link(self.finder, upgrade_allowed, self.require_hashes) + req.populate_link(self.finder, upgrade_allowed, require_hashes) abstract_dist = self.preparer.prepare_linked_requirement( - req, self.session, self.finder, self.require_hashes + req, self.session, self.finder, require_hashes ) # NOTE @@ -344,7 +339,8 @@ def _get_abstract_dist_for(self, req): def _resolve_one( self, requirement_set, # type: RequirementSet - req_to_install # type: InstallRequirement + req_to_install, # type: InstallRequirement + require_hashes, # type: bool ): # type: (...) -> List[InstallRequirement] """Prepare a single requirements file. @@ -362,7 +358,9 @@ def _resolve_one( # 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) + abstract_dist = self._get_abstract_dist_for( + req_to_install, require_hashes + ) # Parse and return dependencies dist = abstract_dist.get_pkg_resources_distribution() From bbc29f0c6cb028f11e0df058e885edade9d0e929 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 22 Sep 2019 18:42:51 -0400 Subject: [PATCH 0549/3170] Pass require_hashes directly to Resolver This removes some of the dependence of the Resolver on our specific RequirementSet implementation. --- src/pip/_internal/cli/req_command.py | 3 ++- src/pip/_internal/legacy_resolve.py | 6 ++++-- tests/unit/test_req.py | 5 +++-- 3 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 203e86a49cc..e9e9d6a9906 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -204,7 +204,8 @@ def make_resolver( ignore_requires_python=ignore_requires_python, force_reinstall=force_reinstall, upgrade_strategy=upgrade_strategy, - py_version_info=py_version_info + py_version_info=py_version_info, + require_hashes=options.require_hashes, ) def populate_requirement_set( diff --git a/src/pip/_internal/legacy_resolve.py b/src/pip/_internal/legacy_resolve.py index 012089d63f7..bc07ae4d9ae 100644 --- a/src/pip/_internal/legacy_resolve.py +++ b/src/pip/_internal/legacy_resolve.py @@ -126,6 +126,7 @@ def __init__( force_reinstall, # type: bool upgrade_strategy, # type: str py_version_info=None, # type: Optional[Tuple[int, ...]] + require_hashes=False, # type: bool ): # type: (...) -> None super(Resolver, self).__init__() @@ -142,6 +143,8 @@ def __init__( self.finder = finder self.session = session + self.require_hashes_option = require_hashes + self.upgrade_strategy = upgrade_strategy self.force_reinstall = force_reinstall self.ignore_dependencies = ignore_dependencies @@ -176,9 +179,8 @@ def resolve(self, requirement_set): list(requirement_set.requirements.values()) ) - require_hashes_option = requirement_set.require_hashes require_hashes = ( - require_hashes_option or + self.require_hashes_option or any(req.has_hash_options for req in root_reqs) ) diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index dea73f368fb..9f4e643d319 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -55,7 +55,7 @@ def setup(self): def teardown(self): shutil.rmtree(self.tempdir, ignore_errors=True) - def _basic_resolver(self, finder): + def _basic_resolver(self, finder, require_hashes=False): preparer = RequirementPreparer( build_dir=os.path.join(self.tempdir, 'build'), src_dir=os.path.join(self.tempdir, 'src'), @@ -78,6 +78,7 @@ def _basic_resolver(self, finder): use_user_site=False, upgrade_strategy="to-satisfy-only", ignore_dependencies=False, ignore_installed=False, ignore_requires_python=False, force_reinstall=False, + require_hashes=require_hashes, ) def test_no_reuse_existing_build_dir(self, data): @@ -177,7 +178,7 @@ def test_missing_hash_with_require_hashes(self, data): )) finder = make_test_finder(find_links=[data.find_links]) - resolver = self._basic_resolver(finder) + resolver = self._basic_resolver(finder, require_hashes=True) assert_raises_regexp( HashErrors, From b8fb97a815d5af2b66add55a591cf66afbb9d6f3 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 22 Sep 2019 18:49:34 -0400 Subject: [PATCH 0550/3170] Remove unused RequirementSet.require_hashes --- src/pip/_internal/cli/req_command.py | 3 --- src/pip/_internal/commands/download.py | 4 +--- src/pip/_internal/commands/install.py | 1 - src/pip/_internal/commands/wheel.py | 4 +--- src/pip/_internal/req/req_set.py | 5 ++--- tests/unit/test_req.py | 10 +++++----- 6 files changed, 9 insertions(+), 18 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index e9e9d6a9906..b4df549c292 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -259,9 +259,6 @@ def populate_requirement_set( use_pep517=options.use_pep517): req_to_add.is_direct = True requirement_set.add_requirement(req_to_add) - # If --require-hashes was a line in a requirements file, tell - # RequirementSet about it: - requirement_set.require_hashes = options.require_hashes if not (args or options.editables or options.requirements): opts = {'name': self.name} diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index a63019fbf50..865a5af1bbb 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -115,9 +115,7 @@ def run(self, options, args): options.build_dir, delete=build_delete, kind="download" ) as directory: - requirement_set = RequirementSet( - require_hashes=options.require_hashes, - ) + requirement_set = RequirementSet() self.populate_requirement_set( requirement_set, args, diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 66071f6e819..596da17e637 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -351,7 +351,6 @@ def run(self, options, args): options.build_dir, delete=build_delete, kind="install" ) as directory: requirement_set = RequirementSet( - require_hashes=options.require_hashes, check_supported_wheels=not options.target_dir, ) diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index 7230470b7d7..1328d9650c8 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -129,9 +129,7 @@ def run(self, options, args): options.build_dir, delete=build_delete, kind="wheel" ) as directory: - requirement_set = RequirementSet( - require_hashes=options.require_hashes, - ) + requirement_set = RequirementSet() try: self.populate_requirement_set( diff --git a/src/pip/_internal/req/req_set.py b/src/pip/_internal/req/req_set.py index b34a2bb11b8..f1ad97fc768 100644 --- a/src/pip/_internal/req/req_set.py +++ b/src/pip/_internal/req/req_set.py @@ -24,13 +24,12 @@ class RequirementSet(object): - def __init__(self, require_hashes=False, check_supported_wheels=True): - # type: (bool, bool) -> None + def __init__(self, check_supported_wheels=True): + # type: (bool) -> None """Create a RequirementSet. """ self.requirements = OrderedDict() # type: Dict[str, InstallRequirement] # noqa: E501 - self.require_hashes = require_hashes self.check_supported_wheels = check_supported_wheels self.unnamed_requirements = [] # type: List[InstallRequirement] diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index 9f4e643d319..d06f0bbed5f 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -172,7 +172,7 @@ def test_missing_hash_with_require_hashes(self, data): """Setting --require-hashes explicitly should raise errors if hashes are missing. """ - reqset = RequirementSet(require_hashes=True) + reqset = RequirementSet() reqset.add_requirement(get_processed_req_from_line( 'simple==1.0', lineno=1 )) @@ -194,7 +194,7 @@ def test_missing_hash_with_require_hashes_in_reqs_file(self, data, tmpdir): """--require-hashes in a requirements file should make its way to the RequirementSet. """ - req_set = RequirementSet(require_hashes=False) + req_set = RequirementSet() finder = make_test_finder(find_links=[data.find_links]) session = finder._link_collector.session command = create_command('install') @@ -203,7 +203,7 @@ def test_missing_hash_with_require_hashes_in_reqs_file(self, data, tmpdir): command.populate_requirement_set( req_set, args, options, finder, session, wheel_cache=None, ) - assert req_set.require_hashes + assert options.require_hashes def test_unsupported_hashes(self, data): """VCS and dir links should raise errors when --require-hashes is @@ -213,7 +213,7 @@ def test_unsupported_hashes(self, data): should trump the presence or absence of a hash. """ - reqset = RequirementSet(require_hashes=True) + reqset = RequirementSet() reqset.add_requirement(get_processed_req_from_line( 'git+git://github.com/pypa/pip-test-package --hash=sha256:123', lineno=1, @@ -272,7 +272,7 @@ def test_hash_mismatch(self, data): """A hash mismatch should raise an error.""" file_url = path_to_url( (data.packages / 'simple-1.0.tar.gz').resolve()) - reqset = RequirementSet(require_hashes=True) + reqset = RequirementSet() reqset.add_requirement(get_processed_req_from_line( '%s --hash=sha256:badbad' % file_url, lineno=1, )) From a0b75cc46042f031366d4206e8613dc336a9767f Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 12 Oct 2019 20:08:27 -0400 Subject: [PATCH 0551/3170] Remove intermediate pip-wheel-metadata dir Previously this was located in the source directory itself, but now that we're using a temporary directory, there's no need for pip-wheel-metadata. --- src/pip/_internal/req/req_install.py | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 5f1fdb74b1e..5a8c0dc14d1 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -591,14 +591,10 @@ def prepare_pep517_metadata(self): assert self.pep517_backend is not None # NOTE: This needs to be refactored to stop using atexit - temp_dir = TempDirectory(kind="modern-metadata") - atexit.register(temp_dir.cleanup) + metadata_tmpdir = TempDirectory(kind="modern-metadata") + atexit.register(metadata_tmpdir.cleanup) - metadata_dir = os.path.join( - temp_dir.path, - 'pip-wheel-metadata', - ) - ensure_dir(metadata_dir) + metadata_dir = metadata_tmpdir.path with self.build_env: # Note that Pep517HookCaller implements a fallback for From c265ce1b4850191c142886ca37f59ecbf16db33c Mon Sep 17 00:00:00 2001 From: Akash Srivastava <akashsrivastava4927@gmail.com> Date: Mon, 14 Oct 2019 12:30:43 +0530 Subject: [PATCH 0552/3170] (Fix Tests): Change method from shutil.remove to shutil.rmtree - remove was not a member of shutil and tests were failing because of it. To fix it, shutil.rmtree is used. Signed-off-by: Akash Srivastava <akashsrivastava4927@gmail.com> --- noxfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index 3ddd0c8aaa1..949dacb77c4 100644 --- a/noxfile.py +++ b/noxfile.py @@ -73,7 +73,7 @@ def should_update_common_wheels(): # Clear the stale cache. if need_to_repopulate: - shutil.remove(LOCATIONS["common-wheels"], ignore_errors=True) + shutil.rmtree(LOCATIONS["common-wheels"], ignore_errors=True) return need_to_repopulate From a6e098a526cb2b18b9acd320fc954a79622b018e Mon Sep 17 00:00:00 2001 From: Akash Srivastava <akashsrivastava4927@gmail.com> Date: Mon, 14 Oct 2019 13:07:22 +0530 Subject: [PATCH 0553/3170] Add newsfile for the PR Signed-off-by: Akash Srivastava <akashsrivastava4927@gmail.com> --- news/7191.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/7191.bugfix diff --git a/news/7191.bugfix b/news/7191.bugfix new file mode 100644 index 00000000000..06b3b01eff4 --- /dev/null +++ b/news/7191.bugfix @@ -0,0 +1 @@ +Change method from shutil.remove to shutil.rmtree in noxfile.py. From 4f1b88375a0d84b535fe743847822ebcbe8e22d6 Mon Sep 17 00:00:00 2001 From: Harsh Vardhan <harsh59v@gmail.com> Date: Mon, 14 Oct 2019 12:56:10 +0530 Subject: [PATCH 0554/3170] Skip running svn tests when svn isn't installed Signed-off-by: Harsh Vardhan <harsh59v@gmail.com> --- tests/lib/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index 276c6e85a70..32580739f36 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -1037,6 +1037,12 @@ def need_bzr(fn): )(fn)) +def need_svn(fn): + return pytest.mark.svn(need_executable( + 'Subversion', ('svn', '--version') + )(fn)) + + def need_mercurial(fn): return pytest.mark.mercurial(need_executable( 'Mercurial', ('hg', 'version') From 2f1a419393b40d520c1f2147dd94f3c34596e129 Mon Sep 17 00:00:00 2001 From: Harsh Vardhan <harsh59v@gmail.com> Date: Mon, 14 Oct 2019 13:11:30 +0530 Subject: [PATCH 0555/3170] Add news file for the change Signed-off-by: Harsh Vardhan <harsh59v@gmail.com> --- news/7193.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/7193.bugfix diff --git a/news/7193.bugfix b/news/7193.bugfix new file mode 100644 index 00000000000..a871b63031d --- /dev/null +++ b/news/7193.bugfix @@ -0,0 +1 @@ +Skip running tests which require subversion, when svn isn't installed From 1e46e6c48d486fa5721ea320555f66d7d03517c4 Mon Sep 17 00:00:00 2001 From: Aniruddha Basak <codewithaniruddha@gmail.com> Date: Mon, 14 Oct 2019 17:21:24 +0530 Subject: [PATCH 0556/3170] Added description for basic auth credentials --- docs/html/user_guide.rst | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index ff500cb34de..fe35a31ad37 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -49,6 +49,15 @@ For more information and examples, see the :ref:`pip install` reference. .. _PyPI: https://pypi.org/ +Basic Authentication Credentials +******************************** + +pip support basic auhentication credentials. Basically, in the url there is +a username and password separated by ``:``. + +``https://[username[:password]]@pypi.company.com/simple`` + + Using a Proxy Server ******************** From 8df9329396cca52c300ae9c1c3c84477bf3aac66 Mon Sep 17 00:00:00 2001 From: Xavier Fernandez <xavier.fernandez@polyconseil.fr> Date: Sun, 13 Oct 2019 23:32:00 +0200 Subject: [PATCH 0557/3170] Add release target --- .pre-commit-config.yaml | 2 +- docs/html/development/release-process.rst | 6 +++ noxfile.py | 57 ++++++++++++++++++++++- tests/conftest.py | 6 ++- 4 files changed, 68 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 2cfdb2cb287..0e327e88a25 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,7 +32,7 @@ repos: args: [] - id: mypy name: mypy, for Py2 - exclude: docs|tests + exclude: noxfile.py|docs|tests args: ["-2"] - repo: https://github.com/pre-commit/pygrep-hooks diff --git a/docs/html/development/release-process.rst b/docs/html/development/release-process.rst index a01ce71dfd1..fc73657bddc 100644 --- a/docs/html/development/release-process.rst +++ b/docs/html/development/release-process.rst @@ -101,6 +101,12 @@ Creating a new release setuptools) to ``Lib/ensurepip/_bundled``, removing the existing version, and adjusting the versions listed in ``Lib/ensurepip/__init__.py``. + +.. note:: + + Steps 3 to 6 are automated in ``nox -s release -- YY.N`` command. + + Creating a bug-fix release -------------------------- diff --git a/noxfile.py b/noxfile.py index 3ddd0c8aaa1..d990fa571b5 100644 --- a/noxfile.py +++ b/noxfile.py @@ -23,6 +23,9 @@ "common-wheels": "tools/requirements/tests-common_wheels.txt", } +AUTHORS_FILE = "AUTHORS.txt" +VERSION_FILE = "src/pip/__init__.py" + def get_author_list(): """Get the list of authors from Git commits. @@ -78,6 +81,11 @@ def should_update_common_wheels(): return need_to_repopulate +def update_version_file(new_version): + with open(VERSION_FILE, "w", encoding="utf-8") as f: + f.write('__version__ = "{}"\n'.format(new_version)) + + # ----------------------------------------------------------------------------- # Development Commands # These are currently prototypes to evaluate whether we want to switch over @@ -174,7 +182,7 @@ def generate_authors(session): # Write our authors to the AUTHORS file session.log("Writing AUTHORS") - with io.open("AUTHORS.txt", "w", encoding="utf-8") as fp: + with io.open(AUTHORS_FILE, "w", encoding="utf-8") as fp: fp.write(u"\n".join(authors)) fp.write(u"\n") @@ -186,3 +194,50 @@ def generate_news(session): # You can pass 2 possible arguments: --draft, --yes session.run("towncrier", *session.posargs) + + +@nox.session +def release(session): + assert len(session.posargs) == 1, "A version number is expected" + new_version = session.posargs[0] + parts = new_version.split('.') + # Expect YY.N or YY.N.P + assert 2 <= len(parts) <= 3, parts + # Only integers + parts = list(map(int, parts)) + session.log("Generating commits for version {}".format(new_version)) + + session.log("Checking that nothing is staged") + # Non-zero exit code means that something is already staged + session.run("git", "diff", "--staged", "--exit-code", external=True) + + session.log(f"Updating {AUTHORS_FILE}") + generate_authors(session) + if subprocess.run(["git", "diff", "--exit-code"]).returncode: + session.run("git", "add", AUTHORS_FILE, external=True) + session.run( + "git", "commit", "-m", f"Updating {AUTHORS_FILE}", + external=True, + ) + else: + session.log(f"No update needed for {AUTHORS_FILE}") + + session.log("Generating NEWS") + session.install("towncrier") + session.run("towncrier", "--yes", "--version", new_version) + + session.log("Updating version") + update_version_file(new_version) + session.run("git", "add", VERSION_FILE, external=True) + session.run("git", "commit", "-m", f"Release {new_version}", external=True) + + session.log("Tagging release") + session.run( + "git", "tag", "-m", f"Release {new_version}", new_version, + external=True, + ) + + next_dev_version = f"{parts[0]}.{parts[1] + 1}.dev0" + update_version_file(next_dev_version) + session.run("git", "add", VERSION_FILE, external=True) + session.run("git", "commit", "-m", "Back to development", external=True) diff --git a/tests/conftest.py b/tests/conftest.py index b832ab8ca86..7a54373b8ec 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -2,6 +2,7 @@ import fnmatch import io import os +import re import shutil import subprocess import sys @@ -246,7 +247,10 @@ def virtualenv_template(request, tmpdir_factory, pip_src, install_egg_link(venv, 'setuptools', setuptools_install) pip_editable = Path(str(tmpdir_factory.mktemp('pip'))) / 'pip' shutil.copytree(pip_src, pip_editable, symlinks=True) - assert compileall.compile_dir(str(pip_editable), quiet=1) + # noxfile.py is Python 3 only + assert compileall.compile_dir( + str(pip_editable), quiet=1, rx=re.compile("noxfile.py$"), + ) subprocess.check_call([venv.bin / 'python', 'setup.py', '-q', 'develop'], cwd=pip_editable) From 707fe2171e5ba96029d0870fce1a4e203ed1a2fe Mon Sep 17 00:00:00 2001 From: Xavier Fernandez <xavier.fernandez@polyconseil.fr> Date: Mon, 14 Oct 2019 14:24:27 +0200 Subject: [PATCH 0558/3170] Updating AUTHORS.txt --- AUTHORS.txt | 3 +++ 1 file changed, 3 insertions(+) diff --git a/AUTHORS.txt b/AUTHORS.txt index fff9c3595e2..e06e07b132a 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -469,6 +469,7 @@ Sumana Harihareswara <sh@changeset.nyc> Sviatoslav Sydorenko <wk.cvs.github@sydorenko.org.ua> Sviatoslav Sydorenko <wk@sydorenko.org.ua> Takayuki SHIMIZUKAWA <shimizukawa@gmail.com> +tbeswick <tbeswick@enphaseenergy.com> Thijs Triemstra <info@collab.nl> Thomas Fenzl <thomas.fenzl@gmail.com> Thomas Grainger <tagrain@gmail.com> @@ -486,7 +487,9 @@ Tom Forbes <tom@tomforb.es> Tom Freudenheim <tom.freudenheim@onepeloton.com> Tom V <tom@viner.tv> Tomer Chachamu <tomer.chachamu@gmail.com> +Tony Beswick <tonybeswick@orcon.net.nz> Tony Zhaocheng Tan <tony@tonytan.io> +TonyBeswick <TonyBeswick@users.noreply.github.com> Toshio Kuratomi <toshio@fedoraproject.org> Travis Swicegood <development@domain51.com> Tzu-ping Chung <uranusjr@gmail.com> From 39086e9c952d4176c5ef984355936bbd5d8cfd55 Mon Sep 17 00:00:00 2001 From: Xavier Fernandez <xavier.fernandez@polyconseil.fr> Date: Mon, 14 Oct 2019 14:24:27 +0200 Subject: [PATCH 0559/3170] Back to development --- src/pip/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/__init__.py b/src/pip/__init__.py index a24cb60dd22..c0496d674b6 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -1 +1 @@ -__version__ = "19.3" +__version__ = "19.4.dev0" From afcb3e7eaf468450609f79d744918b6a37bca262 Mon Sep 17 00:00:00 2001 From: Xavier Fernandez <xavier.fernandez@polyconseil.fr> Date: Mon, 14 Oct 2019 14:24:27 +0200 Subject: [PATCH 0560/3170] Release 19.3 --- NEWS.rst | 79 +++++++++++++++++++ ...0c4888-abed-11e9-a614-b72e6663bd8a.trivial | 0 news/2578.trivial | 0 news/3191.feature | 2 - news/3907.bugfix | 2 - news/3942.bugfix | 1 - news/4358.bugfix | 1 - news/4547.trivial | 1 - news/4910.bugfix | 1 - news/5306.bugfix | 1 - news/6202.bugfix | 2 - news/6213.bugfix | 1 - news/6516.doc | 1 - news/6532.trivial | 3 - news/6637.doc | 1 - news/6653.trivial | 1 - news/6705.bugfix | 2 - news/6763.bugfix | 2 - news/6770.bugfix | 1 - news/6794.doc | 1 - news/6795.bugfix | 1 - news/6804.bugfix | 2 - news/6841.bugfix | 1 - news/6858.feature | 1 - news/6869.trivial | 1 - news/6883.trivial | 1 - news/6885.bugfix | 1 - news/6886.feature | 1 - news/6890.bugfix | 2 - news/6891.feature | 1 - news/6892.bugfix | 2 - news/6924.bugfix | 1 - news/6947.bugfix | 1 - news/6952-gh-actions--linters.trivial | 1 - news/6954.bugfix | 1 - news/6991.bugfix | 1 - news/7037.removal | 3 - news/7071.bugfix | 1 - news/7090.trivial | 2 - news/7094.trivial | 1 - news/7102.feature | 4 - news/7118.bugfix | 1 - news/7119.bugfix | 1 - news/7163.removal | 1 - news/certifi.vendor | 1 - news/contextlib2.vendor | 1 - news/deprecated-yield-fixture.trivial | 2 - news/fix-deprecated-version-key.trivial | 1 - news/fix-test-pep518-forkbombs.trivial | 1 - news/lockfile.vendor | 1 - news/msgpack.vendor | 1 - news/packaging.vendor | 1 - news/pep517.vendor | 1 - news/pyparsing.vendor | 1 - news/pytoml.vendor | 1 - news/remove-unused-assignment.trivial | 1 - news/revisit-test-clean-link.trivial | 1 - news/setuptools.vendor | 1 - news/update-marker-test.trivial | 0 news/urllib3.vendor | 1 - src/pip/__init__.py | 2 +- 61 files changed, 80 insertions(+), 74 deletions(-) delete mode 100644 news/080c4888-abed-11e9-a614-b72e6663bd8a.trivial delete mode 100644 news/2578.trivial delete mode 100644 news/3191.feature delete mode 100644 news/3907.bugfix delete mode 100644 news/3942.bugfix delete mode 100644 news/4358.bugfix delete mode 100644 news/4547.trivial delete mode 100644 news/4910.bugfix delete mode 100644 news/5306.bugfix delete mode 100644 news/6202.bugfix delete mode 100644 news/6213.bugfix delete mode 100644 news/6516.doc delete mode 100644 news/6532.trivial delete mode 100644 news/6637.doc delete mode 100644 news/6653.trivial delete mode 100644 news/6705.bugfix delete mode 100644 news/6763.bugfix delete mode 100644 news/6770.bugfix delete mode 100644 news/6794.doc delete mode 100644 news/6795.bugfix delete mode 100644 news/6804.bugfix delete mode 100644 news/6841.bugfix delete mode 100644 news/6858.feature delete mode 100644 news/6869.trivial delete mode 100644 news/6883.trivial delete mode 100644 news/6885.bugfix delete mode 100644 news/6886.feature delete mode 100644 news/6890.bugfix delete mode 100644 news/6891.feature delete mode 100644 news/6892.bugfix delete mode 100644 news/6924.bugfix delete mode 100644 news/6947.bugfix delete mode 100644 news/6952-gh-actions--linters.trivial delete mode 100644 news/6954.bugfix delete mode 100644 news/6991.bugfix delete mode 100644 news/7037.removal delete mode 100644 news/7071.bugfix delete mode 100644 news/7090.trivial delete mode 100644 news/7094.trivial delete mode 100644 news/7102.feature delete mode 100644 news/7118.bugfix delete mode 100644 news/7119.bugfix delete mode 100644 news/7163.removal delete mode 100644 news/certifi.vendor delete mode 100644 news/contextlib2.vendor delete mode 100644 news/deprecated-yield-fixture.trivial delete mode 100644 news/fix-deprecated-version-key.trivial delete mode 100644 news/fix-test-pep518-forkbombs.trivial delete mode 100644 news/lockfile.vendor delete mode 100644 news/msgpack.vendor delete mode 100644 news/packaging.vendor delete mode 100644 news/pep517.vendor delete mode 100644 news/pyparsing.vendor delete mode 100644 news/pytoml.vendor delete mode 100644 news/remove-unused-assignment.trivial delete mode 100644 news/revisit-test-clean-link.trivial delete mode 100644 news/setuptools.vendor delete mode 100644 news/update-marker-test.trivial delete mode 100644 news/urllib3.vendor diff --git a/NEWS.rst b/NEWS.rst index e1c47d88c01..859b25d997a 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -7,6 +7,85 @@ .. towncrier release notes start +19.3 (2019-10-14) +================= + +Deprecations and Removals +------------------------- + +- Remove undocumented support for un-prefixed URL requirements pointing + to SVN repositories. Users relying on this can get the original behavior + by prefixing their URL with ``svn+`` (which is backwards-compatible). (`#7037 <https://github.com/pypa/pip/issues/7037>`_) +- Remove the deprecated ``--venv`` option from ``pip config``. (`#7163 <https://github.com/pypa/pip/issues/7163>`_) + +Features +-------- + +- Print a better error message when ``--no-binary`` or ``--only-binary`` is given + an argument starting with ``-``. (`#3191 <https://github.com/pypa/pip/issues/3191>`_) +- Make ``pip show`` warn about packages not found. (`#6858 <https://github.com/pypa/pip/issues/6858>`_) +- Support including a port number in ``--trusted-host`` for both HTTP and HTTPS. (`#6886 <https://github.com/pypa/pip/issues/6886>`_) +- Redact single-part login credentials from URLs in log messages. (`#6891 <https://github.com/pypa/pip/issues/6891>`_) +- Implement manylinux2014 platform tag support. manylinux2014 is the successor + to manylinux2010. It allows carefully compiled binary wheels to be installed + on compatible Linux platforms. The manylinux2014 platform tag definition can + be found in `PEP599 <https://www.python.org/dev/peps/pep-0599/>`_. (`#7102 <https://github.com/pypa/pip/issues/7102>`_) + +Bug Fixes +--------- + +- Abort installation if any archive contains a file which would be placed + outside the extraction location. (`#3907 <https://github.com/pypa/pip/issues/3907>`_) +- pip's CLI completion code no longer prints a Traceback if it is interrupted. (`#3942 <https://github.com/pypa/pip/issues/3942>`_) +- Correct inconsistency related to the ``hg+file`` scheme. (`#4358 <https://github.com/pypa/pip/issues/4358>`_) +- Fix ``rmtree_errorhandler`` to skip non-existing directories. (`#4910 <https://github.com/pypa/pip/issues/4910>`_) +- Ignore errors copying socket files for local source installs (in Python 3). (`#5306 <https://github.com/pypa/pip/issues/5306>`_) +- Fix requirement line parser to correctly handle PEP 440 requirements with a URL + pointing to an archive file. (`#6202 <https://github.com/pypa/pip/issues/6202>`_) +- The ``pip-wheel-metadata`` directory does not need to persist between invocations of pip, use a temporary directory instead of the current ``setup.py`` directory. (`#6213 <https://github.com/pypa/pip/issues/6213>`_) +- Fix ``--trusted-host`` processing under HTTPS to trust any port number used + with the host. (`#6705 <https://github.com/pypa/pip/issues/6705>`_) +- Switch to new ``distlib`` wheel script template. This should be functionally + equivalent for end users. (`#6763 <https://github.com/pypa/pip/issues/6763>`_) +- Skip copying .tox and .nox directories to temporary build directories (`#6770 <https://github.com/pypa/pip/issues/6770>`_) +- Fix handling of tokens (single part credentials) in URLs. (`#6795 <https://github.com/pypa/pip/issues/6795>`_) +- Fix a regression that caused ``~`` expansion not to occur in ``--find-links`` + paths. (`#6804 <https://github.com/pypa/pip/issues/6804>`_) +- Fix bypassed pip upgrade warning on Windows. (`#6841 <https://github.com/pypa/pip/issues/6841>`_) +- Fix 'm' flag erroneously being appended to ABI tag in Python 3.8 on platforms that do not provide SOABI (`#6885 <https://github.com/pypa/pip/issues/6885>`_) +- Hide security-sensitive strings like passwords in log messages related to + version control system (aka VCS) command invocations. (`#6890 <https://github.com/pypa/pip/issues/6890>`_) +- Correctly uninstall symlinks that were installed in a virtualenv, + by tools such as ``flit install --symlink``. (`#6892 <https://github.com/pypa/pip/issues/6892>`_) +- Don't fail installation using pip.exe on Windows when pip wouldn't be upgraded. (`#6924 <https://github.com/pypa/pip/issues/6924>`_) +- Use canonical distribution names when computing ``Required-By`` in ``pip show``. (`#6947 <https://github.com/pypa/pip/issues/6947>`_) +- Don't use hardlinks for locking selfcheck state file. (`#6954 <https://github.com/pypa/pip/issues/6954>`_) +- Ignore "require_virtualenv" in ``pip config`` (`#6991 <https://github.com/pypa/pip/issues/6991>`_) +- Fix ``pip freeze`` not showing correct entry for mercurial packages that use subdirectories. (`#7071 <https://github.com/pypa/pip/issues/7071>`_) +- Fix a crash when ``sys.stdin`` is set to ``None``, such as on AWS Lambda. (`#7118 <https://github.com/pypa/pip/issues/7118>`_, `#7119 <https://github.com/pypa/pip/issues/7119>`_) + +Vendored Libraries +------------------ + +- Upgrade certifi to 2019.9.11 +- Add contextlib2 0.6.0 as a vendored dependency. +- Remove Lockfile as a vendored dependency. +- Upgrade msgpack to 0.6.2 +- Upgrade packaging to 19.2 +- Upgrade pep517 to 0.7.0 +- Upgrade pyparsing to 2.4.2 +- Upgrade pytoml to 0.1.21 +- Upgrade setuptools to 41.4.0 +- Upgrade urllib3 to 1.25.6 + +Improved Documentation +---------------------- + +- Document caveats for UNC paths in uninstall and add .pth unit tests. (`#6516 <https://github.com/pypa/pip/issues/6516>`_) +- Add architectural overview documentation. (`#6637 <https://github.com/pypa/pip/issues/6637>`_) +- Document that ``--ignore-installed`` is dangerous. (`#6794 <https://github.com/pypa/pip/issues/6794>`_) + + 19.2.3 (2019-08-25) =================== diff --git a/news/080c4888-abed-11e9-a614-b72e6663bd8a.trivial b/news/080c4888-abed-11e9-a614-b72e6663bd8a.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/2578.trivial b/news/2578.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/3191.feature b/news/3191.feature deleted file mode 100644 index 7dedafd421a..00000000000 --- a/news/3191.feature +++ /dev/null @@ -1,2 +0,0 @@ -Print a better error message when ``--no-binary`` or ``--only-binary`` is given -an argument starting with ``-``. diff --git a/news/3907.bugfix b/news/3907.bugfix deleted file mode 100644 index 24d711df482..00000000000 --- a/news/3907.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -Abort installation if any archive contains a file which would be placed -outside the extraction location. diff --git a/news/3942.bugfix b/news/3942.bugfix deleted file mode 100644 index a15077cc378..00000000000 --- a/news/3942.bugfix +++ /dev/null @@ -1 +0,0 @@ -pip's CLI completion code no longer prints a Traceback if it is interrupted. diff --git a/news/4358.bugfix b/news/4358.bugfix deleted file mode 100644 index a0378e6c50c..00000000000 --- a/news/4358.bugfix +++ /dev/null @@ -1 +0,0 @@ -Correct inconsistency related to the ``hg+file`` scheme. diff --git a/news/4547.trivial b/news/4547.trivial deleted file mode 100644 index 482644a5756..00000000000 --- a/news/4547.trivial +++ /dev/null @@ -1 +0,0 @@ -Remove contradictory debug log diff --git a/news/4910.bugfix b/news/4910.bugfix deleted file mode 100644 index e829dfc7467..00000000000 --- a/news/4910.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix ``rmtree_errorhandler`` to skip non-existing directories. diff --git a/news/5306.bugfix b/news/5306.bugfix deleted file mode 100644 index bf040a95fcf..00000000000 --- a/news/5306.bugfix +++ /dev/null @@ -1 +0,0 @@ -Ignore errors copying socket files for local source installs (in Python 3). diff --git a/news/6202.bugfix b/news/6202.bugfix deleted file mode 100644 index 03184fa8d93..00000000000 --- a/news/6202.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -Fix requirement line parser to correctly handle PEP 440 requirements with a URL -pointing to an archive file. diff --git a/news/6213.bugfix b/news/6213.bugfix deleted file mode 100644 index 08a2483fc80..00000000000 --- a/news/6213.bugfix +++ /dev/null @@ -1 +0,0 @@ -The ``pip-wheel-metadata`` directory does not need to persist between invocations of pip, use a temporary directory instead of the current ``setup.py`` directory. diff --git a/news/6516.doc b/news/6516.doc deleted file mode 100644 index e8b880ef4b4..00000000000 --- a/news/6516.doc +++ /dev/null @@ -1 +0,0 @@ -Document caveats for UNC paths in uninstall and add .pth unit tests. diff --git a/news/6532.trivial b/news/6532.trivial deleted file mode 100644 index 1414bd33d1c..00000000000 --- a/news/6532.trivial +++ /dev/null @@ -1,3 +0,0 @@ -Rename ``pip._internal.utils.outdated`` to -``pip._internal.self_outdated_check`` and rename ``pip_version_check`` -to ``pip_self_version_check``. diff --git a/news/6637.doc b/news/6637.doc deleted file mode 100644 index f79d729bea1..00000000000 --- a/news/6637.doc +++ /dev/null @@ -1 +0,0 @@ -Add architectural overview documentation. diff --git a/news/6653.trivial b/news/6653.trivial deleted file mode 100644 index 5ef02a00028..00000000000 --- a/news/6653.trivial +++ /dev/null @@ -1 +0,0 @@ -Add functional tests for "yanked" files. diff --git a/news/6705.bugfix b/news/6705.bugfix deleted file mode 100644 index e8f67ff3868..00000000000 --- a/news/6705.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -Fix ``--trusted-host`` processing under HTTPS to trust any port number used -with the host. diff --git a/news/6763.bugfix b/news/6763.bugfix deleted file mode 100644 index 68d0b58fd64..00000000000 --- a/news/6763.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -Switch to new ``distlib`` wheel script template. This should be functionally -equivalent for end users. diff --git a/news/6770.bugfix b/news/6770.bugfix deleted file mode 100644 index c0ab57ee109..00000000000 --- a/news/6770.bugfix +++ /dev/null @@ -1 +0,0 @@ -Skip copying .tox and .nox directories to temporary build directories diff --git a/news/6794.doc b/news/6794.doc deleted file mode 100644 index 55bc01404dd..00000000000 --- a/news/6794.doc +++ /dev/null @@ -1 +0,0 @@ -Document that ``--ignore-installed`` is dangerous. diff --git a/news/6795.bugfix b/news/6795.bugfix deleted file mode 100644 index f80bd9b4b2f..00000000000 --- a/news/6795.bugfix +++ /dev/null @@ -1 +0,0 @@ - Fix handling of tokens (single part credentials) in URLs. diff --git a/news/6804.bugfix b/news/6804.bugfix deleted file mode 100644 index f9599f9fda6..00000000000 --- a/news/6804.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -Fix a regression that caused ``~`` expansion not to occur in ``--find-links`` -paths. diff --git a/news/6841.bugfix b/news/6841.bugfix deleted file mode 100644 index 278caa64e54..00000000000 --- a/news/6841.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix bypassed pip upgrade warning on Windows. diff --git a/news/6858.feature b/news/6858.feature deleted file mode 100644 index be01bc82652..00000000000 --- a/news/6858.feature +++ /dev/null @@ -1 +0,0 @@ -Make ``pip show`` warn about packages not found. diff --git a/news/6869.trivial b/news/6869.trivial deleted file mode 100644 index 25d8bd6160e..00000000000 --- a/news/6869.trivial +++ /dev/null @@ -1 +0,0 @@ -Clarify WheelBuilder.build() a bit diff --git a/news/6883.trivial b/news/6883.trivial deleted file mode 100644 index 8d132ac30cd..00000000000 --- a/news/6883.trivial +++ /dev/null @@ -1 +0,0 @@ -replace is_vcs_url function by is_vcs Link property diff --git a/news/6885.bugfix b/news/6885.bugfix deleted file mode 100644 index 1eedfec9376..00000000000 --- a/news/6885.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix 'm' flag erroneously being appended to ABI tag in Python 3.8 on platforms that do not provide SOABI diff --git a/news/6886.feature b/news/6886.feature deleted file mode 100644 index b4f500b1b22..00000000000 --- a/news/6886.feature +++ /dev/null @@ -1 +0,0 @@ -Support including a port number in ``--trusted-host`` for both HTTP and HTTPS. diff --git a/news/6890.bugfix b/news/6890.bugfix deleted file mode 100644 index 3da0d5bb2fa..00000000000 --- a/news/6890.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -Hide security-sensitive strings like passwords in log messages related to -version control system (aka VCS) command invocations. diff --git a/news/6891.feature b/news/6891.feature deleted file mode 100644 index 4d08eedfbfc..00000000000 --- a/news/6891.feature +++ /dev/null @@ -1 +0,0 @@ -Redact single-part login credentials from URLs in log messages. diff --git a/news/6892.bugfix b/news/6892.bugfix deleted file mode 100644 index 3aaf7712495..00000000000 --- a/news/6892.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -Correctly uninstall symlinks that were installed in a virtualenv, -by tools such as ``flit install --symlink``. diff --git a/news/6924.bugfix b/news/6924.bugfix deleted file mode 100644 index d89652cba7b..00000000000 --- a/news/6924.bugfix +++ /dev/null @@ -1 +0,0 @@ -Don't fail installation using pip.exe on Windows when pip wouldn't be upgraded. diff --git a/news/6947.bugfix b/news/6947.bugfix deleted file mode 100644 index f8d409e9eee..00000000000 --- a/news/6947.bugfix +++ /dev/null @@ -1 +0,0 @@ -Use canonical distribution names when computing ``Required-By`` in ``pip show``. diff --git a/news/6952-gh-actions--linters.trivial b/news/6952-gh-actions--linters.trivial deleted file mode 100644 index 194e39025c0..00000000000 --- a/news/6952-gh-actions--linters.trivial +++ /dev/null @@ -1 +0,0 @@ -Add a GitHub Actions workflow running all linters. diff --git a/news/6954.bugfix b/news/6954.bugfix deleted file mode 100644 index 8f6f67109cb..00000000000 --- a/news/6954.bugfix +++ /dev/null @@ -1 +0,0 @@ -Don't use hardlinks for locking selfcheck state file. diff --git a/news/6991.bugfix b/news/6991.bugfix deleted file mode 100644 index c6bf963b901..00000000000 --- a/news/6991.bugfix +++ /dev/null @@ -1 +0,0 @@ -Ignore "require_virtualenv" in ``pip config`` diff --git a/news/7037.removal b/news/7037.removal deleted file mode 100644 index 4c606e4a2a0..00000000000 --- a/news/7037.removal +++ /dev/null @@ -1,3 +0,0 @@ -Remove undocumented support for un-prefixed URL requirements pointing -to SVN repositories. Users relying on this can get the original behavior -by prefixing their URL with ``svn+`` (which is backwards-compatible). diff --git a/news/7071.bugfix b/news/7071.bugfix deleted file mode 100644 index f0463ce3c19..00000000000 --- a/news/7071.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix ``pip freeze`` not showing correct entry for mercurial packages that use subdirectories. diff --git a/news/7090.trivial b/news/7090.trivial deleted file mode 100644 index 01bdcf65459..00000000000 --- a/news/7090.trivial +++ /dev/null @@ -1,2 +0,0 @@ -Move PipXmlrpcTransport from pip._internal.download to pip._internal.network.xmlrpc -and move associated tests to tests.unit.test_network_xmlrpc diff --git a/news/7094.trivial b/news/7094.trivial deleted file mode 100644 index eff43441e35..00000000000 --- a/news/7094.trivial +++ /dev/null @@ -1 +0,0 @@ -Remove DependencyWarning warning from pip._internal diff --git a/news/7102.feature b/news/7102.feature deleted file mode 100644 index 4412649fcb3..00000000000 --- a/news/7102.feature +++ /dev/null @@ -1,4 +0,0 @@ -Implement manylinux2014 platform tag support. manylinux2014 is the successor -to manylinux2010. It allows carefully compiled binary wheels to be installed -on compatible Linux platforms. The manylinux2014 platform tag definition can -be found in `PEP599 <https://www.python.org/dev/peps/pep-0599/>`_. diff --git a/news/7118.bugfix b/news/7118.bugfix deleted file mode 100644 index 8cca2e1bf46..00000000000 --- a/news/7118.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a crash when ``sys.stdin`` is set to ``None``, such as on AWS Lambda. diff --git a/news/7119.bugfix b/news/7119.bugfix deleted file mode 100644 index 8cca2e1bf46..00000000000 --- a/news/7119.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a crash when ``sys.stdin`` is set to ``None``, such as on AWS Lambda. diff --git a/news/7163.removal b/news/7163.removal deleted file mode 100644 index e5c7edeefab..00000000000 --- a/news/7163.removal +++ /dev/null @@ -1 +0,0 @@ -Remove the deprecated ``--venv`` option from ``pip config``. diff --git a/news/certifi.vendor b/news/certifi.vendor deleted file mode 100644 index 66e84cb207e..00000000000 --- a/news/certifi.vendor +++ /dev/null @@ -1 +0,0 @@ -Upgrade certifi to 2019.9.11 diff --git a/news/contextlib2.vendor b/news/contextlib2.vendor deleted file mode 100644 index 25a7f1b1f99..00000000000 --- a/news/contextlib2.vendor +++ /dev/null @@ -1 +0,0 @@ -Add contextlib2 0.6.0 as a vendored dependency. diff --git a/news/deprecated-yield-fixture.trivial b/news/deprecated-yield-fixture.trivial deleted file mode 100644 index 552f49c2ce1..00000000000 --- a/news/deprecated-yield-fixture.trivial +++ /dev/null @@ -1,2 +0,0 @@ -Use normal ``fixture`` instead of ``yield_fixture``. -It's been deprecated in pytest since 2.10 version. diff --git a/news/fix-deprecated-version-key.trivial b/news/fix-deprecated-version-key.trivial deleted file mode 100644 index fc0f23a827a..00000000000 --- a/news/fix-deprecated-version-key.trivial +++ /dev/null @@ -1 +0,0 @@ -Use ``python-version`` instead of deprecated ``version``. diff --git a/news/fix-test-pep518-forkbombs.trivial b/news/fix-test-pep518-forkbombs.trivial deleted file mode 100644 index d88e8c20055..00000000000 --- a/news/fix-test-pep518-forkbombs.trivial +++ /dev/null @@ -1 +0,0 @@ -Fix copy-paste issue in ``test_pep518_forkbombs``. diff --git a/news/lockfile.vendor b/news/lockfile.vendor deleted file mode 100644 index 3d58fa13807..00000000000 --- a/news/lockfile.vendor +++ /dev/null @@ -1 +0,0 @@ -Remove Lockfile as a vendored dependency. diff --git a/news/msgpack.vendor b/news/msgpack.vendor deleted file mode 100644 index 1c101c68f59..00000000000 --- a/news/msgpack.vendor +++ /dev/null @@ -1 +0,0 @@ -Upgrade msgpack to 0.6.2 diff --git a/news/packaging.vendor b/news/packaging.vendor deleted file mode 100644 index 4076eb0cd2e..00000000000 --- a/news/packaging.vendor +++ /dev/null @@ -1 +0,0 @@ -Upgrade packaging to 19.2 diff --git a/news/pep517.vendor b/news/pep517.vendor deleted file mode 100644 index c2376b25913..00000000000 --- a/news/pep517.vendor +++ /dev/null @@ -1 +0,0 @@ -Upgrade pep517 to 0.7.0 diff --git a/news/pyparsing.vendor b/news/pyparsing.vendor deleted file mode 100644 index 90374a1ef91..00000000000 --- a/news/pyparsing.vendor +++ /dev/null @@ -1 +0,0 @@ -Upgrade pyparsing to 2.4.2 diff --git a/news/pytoml.vendor b/news/pytoml.vendor deleted file mode 100644 index 9916ed83e8b..00000000000 --- a/news/pytoml.vendor +++ /dev/null @@ -1 +0,0 @@ -Upgrade pytoml to 0.1.21 diff --git a/news/remove-unused-assignment.trivial b/news/remove-unused-assignment.trivial deleted file mode 100644 index f4d91cabc28..00000000000 --- a/news/remove-unused-assignment.trivial +++ /dev/null @@ -1 +0,0 @@ -Remove unused assignment. diff --git a/news/revisit-test-clean-link.trivial b/news/revisit-test-clean-link.trivial deleted file mode 100644 index 341acee554a..00000000000 --- a/news/revisit-test-clean-link.trivial +++ /dev/null @@ -1 +0,0 @@ -Use pytest.param to skip certain parametrizations. diff --git a/news/setuptools.vendor b/news/setuptools.vendor deleted file mode 100644 index c576a969da7..00000000000 --- a/news/setuptools.vendor +++ /dev/null @@ -1 +0,0 @@ -Upgrade setuptools to 41.4.0 diff --git a/news/update-marker-test.trivial b/news/update-marker-test.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/urllib3.vendor b/news/urllib3.vendor deleted file mode 100644 index 80b98b44e10..00000000000 --- a/news/urllib3.vendor +++ /dev/null @@ -1 +0,0 @@ -Upgrade urllib3 to 1.25.6 diff --git a/src/pip/__init__.py b/src/pip/__init__.py index 9c2e2a3bb48..a24cb60dd22 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -1 +1 @@ -__version__ = "19.3.dev0" +__version__ = "19.3" From 3e326a017b616c3daa090147f76d66b2cbccece5 Mon Sep 17 00:00:00 2001 From: Aniruddha Basak <codewithaniruddha@gmail.com> Date: Tue, 15 Oct 2019 12:09:42 +0530 Subject: [PATCH 0561/3170] Fix typo --- docs/html/user_guide.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index fe35a31ad37..d35403be6d9 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -52,7 +52,7 @@ For more information and examples, see the :ref:`pip install` reference. Basic Authentication Credentials ******************************** -pip support basic auhentication credentials. Basically, in the url there is +pip supports basic authentication credentials. Basically, in the url there is a username and password separated by ``:``. ``https://[username[:password]]@pypi.company.com/simple`` From 5fb94298cfe45e7738bf8fd262670f2690b2efbd Mon Sep 17 00:00:00 2001 From: Aniruddha Basak <codewithaniruddha@gmail.com> Date: Tue, 15 Oct 2019 12:22:47 +0530 Subject: [PATCH 0562/3170] Fix whitespace --- docs/html/user_guide.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index d35403be6d9..e088e419a00 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -52,7 +52,7 @@ For more information and examples, see the :ref:`pip install` reference. Basic Authentication Credentials ******************************** -pip supports basic authentication credentials. Basically, in the url there is +pip supports basic authentication credentials. Basically, in the url there is a username and password separated by ``:``. ``https://[username[:password]]@pypi.company.com/simple`` From 571fccf047a3ebfd6949bee52be39ce3b3b7acc2 Mon Sep 17 00:00:00 2001 From: Aniruddha Basak <codewithaniruddha@gmail.com> Date: Tue, 15 Oct 2019 12:50:53 +0530 Subject: [PATCH 0563/3170] Add 7201.doc file in news directory --- news/7201.doc | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/7201.doc diff --git a/news/7201.doc b/news/7201.doc new file mode 100644 index 00000000000..7eca4599754 --- /dev/null +++ b/news/7201.doc @@ -0,0 +1 @@ +Describe basic authentication credentials works in urls. \ No newline at end of file From 86087d32e209e51ef686436d07f4bfc9fda5f264 Mon Sep 17 00:00:00 2001 From: Aniruddha Basak <codewithaniruddha@gmail.com> Date: Tue, 15 Oct 2019 13:20:40 +0530 Subject: [PATCH 0564/3170] Fix lint issue --- news/7201.doc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/7201.doc b/news/7201.doc index 7eca4599754..839259e362c 100644 --- a/news/7201.doc +++ b/news/7201.doc @@ -1 +1 @@ -Describe basic authentication credentials works in urls. \ No newline at end of file +Describe basic authentication credentials works in urls. From 7cfc16d016906e1437580acff42e503d3b2fa188 Mon Sep 17 00:00:00 2001 From: admin <admin@admins-MacBook-Pro.local> Date: Tue, 15 Oct 2019 13:44:43 +0530 Subject: [PATCH 0565/3170] Change %s to .format --- src/pip/_internal/distributions/source/legacy.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/distributions/source/legacy.py b/src/pip/_internal/distributions/source/legacy.py index ab43afbec84..6d2f53ebc65 100644 --- a/src/pip/_internal/distributions/source/legacy.py +++ b/src/pip/_internal/distributions/source/legacy.py @@ -49,7 +49,7 @@ def _raise_conflicts(conflicting_with, conflicting_reqs): requirement=self.req, conflicting_with=conflicting_with, description=', '.join( - '%s is incompatible with %s' % (installed, wanted) + '{} is incompatible with {}'.format(installed, wanted) for installed, wanted in sorted(conflicting) ) ) From e2a6fea7fac98e93c0f70b319212ca22fcd6cc21 Mon Sep 17 00:00:00 2001 From: Neil Botelho <neil.botelho321@gmail.com> Date: Tue, 15 Oct 2019 14:54:34 +0530 Subject: [PATCH 0566/3170] Add CA cert info to debug, and corresponding tests Added CA cert information to debug.py Made the corresponding changes to tests/functional/test_debug.py --- src/pip/_internal/commands/debug.py | 32 +++++++++++++++++++++++++++-- tests/functional/test_debug.py | 4 ++++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/commands/debug.py b/src/pip/_internal/commands/debug.py index 5322c828bde..5e62e737c18 100644 --- a/src/pip/_internal/commands/debug.py +++ b/src/pip/_internal/commands/debug.py @@ -5,8 +5,11 @@ import locale import logging +import os import sys +from pip._vendor.certifi import where + from pip._internal.cli import cmdoptions from pip._internal.cli.base_command import Command from pip._internal.cli.cmdoptions import make_target_python @@ -17,14 +20,14 @@ from pip._internal.wheel import format_tag if MYPY_CHECK_RUNNING: - from typing import Any, List + from typing import Any, List, Optional from optparse import Values logger = logging.getLogger(__name__) def show_value(name, value): - # type: (str, str) -> None + # type: (str, Optional[str]) -> None logger.info('{}: {}'.format(name, value)) @@ -75,6 +78,25 @@ def show_tags(options): logger.info(msg) +def ca_bundle_info(config): + levels = set() + for key, value in config.items(): + levels.add(key.split('.')[0]) + + if not levels: + return "Not specified" + + levels_that_override_global = ['install', 'wheel', 'download'] + global_overriding_level = [ + level for level in levels if level in levels_that_override_global + ] + if not global_overriding_level: + return 'global' + + levels.remove('global') + return ", ".join(levels) + + class DebugCommand(Command): """ Display debug information. @@ -90,6 +112,7 @@ def __init__(self, *args, **kw): cmd_opts = self.cmd_opts cmdoptions.add_target_python_options(cmd_opts) self.parser.insert_option_group(0, cmd_opts) + self.parser.config.load() def run(self, options, args): # type: (Values, List[Any]) -> int @@ -110,6 +133,11 @@ def run(self, options, args): show_value('sys.platform', sys.platform) show_sys_implementation() + show_value("REQUESTS_CA_BUNDLE", os.environ.get('REQUESTS_CA_BUNDLE')) + show_value("CURL_CA_BUNDLE", os.environ.get('CURL_CA_BUNDLE')) + show_value("pip._vendor.certifi.where()", where()) + show_value("'cert' config value", ca_bundle_info(self.parser.config)) + show_tags(options) return SUCCESS diff --git a/tests/functional/test_debug.py b/tests/functional/test_debug.py index 785ff3380a5..20d34feb983 100644 --- a/tests/functional/test_debug.py +++ b/tests/functional/test_debug.py @@ -10,6 +10,10 @@ 'locale.getpreferredencoding: ', 'sys.platform: ', 'sys.implementation:', + 'REQUESTS_CA_BUNDLE: ', + 'CURL_CA_BUNDLE: ', + 'pip._vendor.certifi.where(): ', + '\'cert\' config value: ', ]) def test_debug(script, expected_text): """ From e51299d128ee4e38f32148c00a85b957f1f6e0cb Mon Sep 17 00:00:00 2001 From: Neil Botelho <neil.botelho321@gmail.com> Date: Tue, 15 Oct 2019 14:55:35 +0530 Subject: [PATCH 0567/3170] Add news file fragment --- news/7146.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/7146.feature diff --git a/news/7146.feature b/news/7146.feature new file mode 100644 index 00000000000..db57345ede5 --- /dev/null +++ b/news/7146.feature @@ -0,0 +1 @@ +Display CA information in ``pip debug``. From 0e6ac42c93383ab5f604ffae39d30df5406a403c Mon Sep 17 00:00:00 2001 From: Harsh Vardhan <harsh59v@gmail.com> Date: Mon, 14 Oct 2019 15:27:57 +0530 Subject: [PATCH 0568/3170] Add need_svn decorator for tests which require svn Signed-off-by: Harsh Vardhan <harsh59v@gmail.com> --- tests/functional/test_freeze.py | 3 ++- tests/functional/test_install.py | 3 ++- tests/functional/test_install_reqs.py | 3 ++- tests/functional/test_install_user.py | 4 ++-- tests/functional/test_uninstall.py | 9 +++++++-- tests/unit/test_vcs.py | 4 ++-- 6 files changed, 17 insertions(+), 9 deletions(-) diff --git a/tests/functional/test_freeze.py b/tests/functional/test_freeze.py index 546a4828d5c..5a10b93b7b5 100644 --- a/tests/functional/test_freeze.py +++ b/tests/functional/test_freeze.py @@ -12,6 +12,7 @@ _git_commit, need_bzr, need_mercurial, + need_svn, path_to_url, ) @@ -169,7 +170,7 @@ def test_freeze_editable_git_with_no_remote(script, tmpdir, deprecated_python): _check_output(result.stdout, expected) -@pytest.mark.svn +@need_svn def test_freeze_svn(script, tmpdir): """Test freezing a svn checkout""" diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index f07dcd4eff0..f8f814a092d 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -21,6 +21,7 @@ create_test_package_with_setup, need_bzr, need_mercurial, + need_svn, path_to_url, pyversion, pyversion_tuple, @@ -251,7 +252,7 @@ def test_basic_editable_install(script): assert not result.files_updated -@pytest.mark.svn +@need_svn def test_basic_install_editable_from_svn(script): """ Test checking out from svn. diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index faa971eadb3..a653e0b2fb2 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -5,6 +5,7 @@ from tests.lib import ( _create_test_package_with_subdirectory, + need_svn, path_to_url, pyversion, requirements_file, @@ -100,7 +101,7 @@ def test_relative_requirements_file(script, data): @pytest.mark.network -@pytest.mark.svn +@need_svn def test_multiple_requirements_files(script, tmpdir): """ Test installing from multiple nested requirements files. diff --git a/tests/functional/test_install_user.py b/tests/functional/test_install_user.py index 537f402769b..1ac644609e9 100644 --- a/tests/functional/test_install_user.py +++ b/tests/functional/test_install_user.py @@ -6,7 +6,7 @@ import pytest -from tests.lib import pyversion +from tests.lib import need_svn, pyversion from tests.lib.local_repos import local_checkout @@ -41,7 +41,7 @@ def test_reset_env_system_site_packages_usersite(self, script): assert 'INITools' == project_name, project_name @pytest.mark.network - @pytest.mark.svn + @need_svn def test_install_subversion_usersite_editable_with_distribute( self, script, tmpdir): """ diff --git a/tests/functional/test_uninstall.py b/tests/functional/test_uninstall.py index 13d18768edc..84436aca098 100644 --- a/tests/functional/test_uninstall.py +++ b/tests/functional/test_uninstall.py @@ -13,7 +13,11 @@ from pip._internal.req.constructors import install_req_from_line from pip._internal.utils.misc import rmtree -from tests.lib import assert_all_changes, create_test_package_with_setup +from tests.lib import ( + assert_all_changes, + create_test_package_with_setup, + need_svn, +) from tests.lib.local_repos import local_checkout, local_repo @@ -289,6 +293,7 @@ def test_uninstall_easy_installed_console_scripts(script): @pytest.mark.network +@need_svn def test_uninstall_editable_from_svn(script, tmpdir): """ Test uninstalling an editable installation from svn. @@ -352,7 +357,7 @@ def _test_uninstall_editable_with_source_outside_venv( @pytest.mark.network -@pytest.mark.svn +@need_svn def test_uninstall_from_reqs_file(script, tmpdir): """ Test uninstall from a requirements file. diff --git a/tests/unit/test_vcs.py b/tests/unit/test_vcs.py index d4110fd61ac..42fc43d6855 100644 --- a/tests/unit/test_vcs.py +++ b/tests/unit/test_vcs.py @@ -13,7 +13,7 @@ from pip._internal.vcs.mercurial import Mercurial from pip._internal.vcs.subversion import Subversion from pip._internal.vcs.versioncontrol import RevOptions, VersionControl -from tests.lib import is_svn_installed, pyversion +from tests.lib import is_svn_installed, need_svn, pyversion if pyversion >= '3': VERBOSE_FALSE = False @@ -411,7 +411,7 @@ def test_subversion__init_use_interactive( assert svn.use_interactive == expected -@pytest.mark.svn +@need_svn def test_subversion__call_vcs_version(): """ Test Subversion.call_vcs_version() against local ``svn``. From f235e675c0a208487cc2f6c7be230a905dbfcc51 Mon Sep 17 00:00:00 2001 From: Hugo <hugovk@users.noreply.github.com> Date: Tue, 15 Oct 2019 18:40:43 +0300 Subject: [PATCH 0569/3170] Declare support for Python 3.8 --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index db4c55eeb6a..31100ed5b84 100644 --- a/setup.py +++ b/setup.py @@ -52,6 +52,7 @@ def find_version(*file_paths): "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ], From d65bf5021a6d160fc1396d9be57880de1144f7bf Mon Sep 17 00:00:00 2001 From: hugovk <hugovk@users.noreply.github.com> Date: Tue, 15 Oct 2019 20:06:29 +0300 Subject: [PATCH 0570/3170] Update docs about Python 3.8 --- docs/html/installing.rst | 2 +- news/7219.feature | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 news/7219.feature diff --git a/docs/html/installing.rst b/docs/html/installing.rst index 6b50f929bc9..cfcdf7edabc 100644 --- a/docs/html/installing.rst +++ b/docs/html/installing.rst @@ -109,7 +109,7 @@ On Windows [4]_:: Python and OS Compatibility --------------------------- -pip works with CPython versions 2.7, 3.5, 3.6, 3.7 and also PyPy. +pip works with CPython versions 2.7, 3.5, 3.6, 3.7, 3.8 and also PyPy. This means pip works on the latest patch version of each of these minor versions. Previous patch versions are supported on a best effort approach. diff --git a/news/7219.feature b/news/7219.feature new file mode 100644 index 00000000000..ba0e9370489 --- /dev/null +++ b/news/7219.feature @@ -0,0 +1 @@ +Document Python 3.8 support. From 9f6ee74d1a562caf91ee804e3ae7cca2b4e0fd97 Mon Sep 17 00:00:00 2001 From: hugovk <hugovk@users.noreply.github.com> Date: Tue, 15 Oct 2019 22:07:03 +0300 Subject: [PATCH 0571/3170] Test on Python 3.8 final --- .travis.yml | 14 +++++--------- tox.ini | 2 +- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/.travis.yml b/.travis.yml index c61e41702be..97520682ab7 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: python cache: pip dist: xenial -python: 3.7 +python: 3.8 addons: apt: packages: @@ -38,6 +38,10 @@ jobs: - env: GROUP=2 python: pypy2.7-6.0 # Other Supported CPython + - env: GROUP=1 + python: 3.7 + - env: GROUP=2 + python: 3.7 - env: GROUP=1 python: 3.6 - env: GROUP=2 @@ -47,15 +51,7 @@ jobs: - env: GROUP=2 python: 3.5 - - env: GROUP=1 - python: 3.8-dev - - env: GROUP=2 - python: 3.8-dev - fast_finish: true - # It's okay to fail on the in-development CPython version. - allow_failures: - - python: 3.8-dev before_install: tools/travis/setup.sh install: travis_retry tools/travis/install.sh diff --git a/tox.ini b/tox.ini index eb06dbddaea..30eb193dd3d 100644 --- a/tox.ini +++ b/tox.ini @@ -31,7 +31,7 @@ commands = pytest --timeout 300 --cov=pip --cov-report=term-missing --cov-report [testenv:docs] # Don't skip install here since pip_sphinxext uses pip's internals. deps = -r{toxinidir}/tools/requirements/docs.txt -basepython = python3.7 +basepython = python3.8 commands = sphinx-build -W -d {envtmpdir}/doctrees/html -b html docs/html docs/build/html # Having the conf.py in the docs/html is weird but needed because we From 852d996c40d2c57b5b95459630b3882cc0fed327 Mon Sep 17 00:00:00 2001 From: Shovan Maity <shovan.maity@mayadata.io> Date: Wed, 16 Oct 2019 02:20:38 +0530 Subject: [PATCH 0572/3170] (improve doc): add command to run test in parallel (#7189) --- docs/html/development/getting-started.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/html/development/getting-started.rst b/docs/html/development/getting-started.rst index fc37952d379..41bf88b0f6f 100644 --- a/docs/html/development/getting-started.rst +++ b/docs/html/development/getting-started.rst @@ -40,6 +40,13 @@ To run tests locally, run: $ tox -e py36 +Generally, it can take a long time to run pip's test suite. To run tests in parallel, +which is faster, run: + +.. code-block:: console + + $ tox -e py36 -- -n auto + The example above runs tests against Python 3.6. You can also use other versions like ``py27`` and ``pypy3``. From 03cdc205a58feda8de87cf16290138af9df216ce Mon Sep 17 00:00:00 2001 From: Gopinath M <31352222+mgopi1990@users.noreply.github.com> Date: Wed, 16 Oct 2019 02:34:03 +0530 Subject: [PATCH 0573/3170] Remove single use variable show_url (#7213) --- src/pip/_internal/download.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index 6567fc375a6..2799e38812c 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -180,8 +180,6 @@ def _download_url( else: show_progress = False - show_url = link.show_url - def resp_read(chunk_size): try: # Special case for urllib3. @@ -227,7 +225,7 @@ def written_chunks(chunks): progress_indicator = _progress_indicator if link.netloc == PyPI.netloc: - url = show_url + url = link.show_url else: url = link.url_without_fragment From 6727972508ac2056bf53620ea85e13d4c29e6e61 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 15 Oct 2019 21:44:58 +0530 Subject: [PATCH 0574/3170] Do not show full URLs when downloading from PyPI --- src/pip/_internal/download.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index 2799e38812c..9845b79c0c6 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -224,7 +224,7 @@ def written_chunks(chunks): progress_indicator = _progress_indicator - if link.netloc == PyPI.netloc: + if link.netloc == PyPI.file_storage_domain: url = link.show_url else: url = link.url_without_fragment From 33d7d65da19389c489cc8f1ed91d83fe72a7508c Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 15 Oct 2019 23:21:45 +0530 Subject: [PATCH 0575/3170] Update test to verify that full URL is not printed --- tests/functional/test_install.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index f8f814a092d..823b2c1a1e9 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -226,7 +226,7 @@ def test_basic_install_from_pypi(script): """ Test installing a package from PyPI. """ - result = script.pip('install', '-vvv', 'INITools==0.2') + result = script.pip('install', 'INITools==0.2') egg_info_folder = ( script.site_packages / 'INITools-0.2-py%s.egg-info' % pyversion ) @@ -238,6 +238,13 @@ def test_basic_install_from_pypi(script): assert "Looking in indexes: " not in result.stdout assert "Looking in links: " not in result.stdout + # Ensure that we don't print the full URL. + # The URL should be trimmed to only the last part of the path in it, + # when installing from PyPI. The assertion here only checks for + # `https://` since that's likely to show up if we're not trimming in + # the correct circumstances. + assert "https://" not in result.stdout + def test_basic_editable_install(script): """ From af0c5b5b7b9dc6d59b3b055d155faef163eca90c Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Wed, 16 Oct 2019 12:40:51 +0530 Subject: [PATCH 0576/3170] :newspaper: --- news/7225.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/7225.feature diff --git a/news/7225.feature b/news/7225.feature new file mode 100644 index 00000000000..1678a80f4fd --- /dev/null +++ b/news/7225.feature @@ -0,0 +1 @@ +Show only the filename (instead of full URL), when downloading from PyPI. From 3b8e2f3749129b41a277acfbedc543ad7d770466 Mon Sep 17 00:00:00 2001 From: everdimension <everdimension@gmail.com> Date: Wed, 16 Oct 2019 03:09:07 +0300 Subject: [PATCH 0577/3170] Rephrase installation instructions Make it a bit more clear that the goal is to just download the get-pip.py file and that the curl command is not required --- docs/html/installing.rst | 8 +++++--- news/7222.doc | 1 + 2 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 news/7222.doc diff --git a/docs/html/installing.rst b/docs/html/installing.rst index 6b50f929bc9..5107afa9272 100644 --- a/docs/html/installing.rst +++ b/docs/html/installing.rst @@ -18,12 +18,14 @@ Just make sure to :ref:`upgrade pip <Upgrading pip>`. Installing with get-pip.py -------------------------- -To install pip, securely download `get-pip.py -<https://bootstrap.pypa.io/get-pip.py>`_. [1]_:: +To install pip, securely [1]_ download ``get-pip.py`` by following +this link: `get-pip.py +<https://bootstrap.pypa.io/get-pip.py>`_. Alternatively, use ``curl``:: curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py -Then run the following:: +Then run the following command in the folder where you +have downloaded ``get-pip.py``:: python get-pip.py diff --git a/news/7222.doc b/news/7222.doc new file mode 100644 index 00000000000..5d68f707a20 --- /dev/null +++ b/news/7222.doc @@ -0,0 +1 @@ +Add more clear installation instructions From d2a19aee7f827f606bbb214380ca2c3c52ea939d Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 13 Oct 2019 18:31:01 -0400 Subject: [PATCH 0578/3170] Move download.get_file_content to req.req_file --- src/pip/_internal/download.py | 62 ++----------------------------- src/pip/_internal/req/req_file.py | 59 ++++++++++++++++++++++++++++- 2 files changed, 60 insertions(+), 61 deletions(-) diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py index 9845b79c0c6..7306f98ab36 100644 --- a/src/pip/_internal/download.py +++ b/src/pip/_internal/download.py @@ -7,19 +7,16 @@ import logging import mimetypes import os -import re import shutil import sys from pip._vendor import requests from pip._vendor.requests.models import CONTENT_CHUNK_SIZE, Response from pip._vendor.six import PY2 -from pip._vendor.six.moves.urllib import parse as urllib_parse -from pip._internal.exceptions import HashMismatch, InstallationError +from pip._internal.exceptions import HashMismatch from pip._internal.models.index import PyPI from pip._internal.network.session import PipSession -from pip._internal.utils.encoding import auto_decode from pip._internal.utils.filesystem import copy2_fixed from pip._internal.utils.misc import ( ask_path_exists, @@ -36,12 +33,11 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.ui import DownloadProgressProvider from pip._internal.utils.unpacking import unpack_file -from pip._internal.utils.urls import get_url_scheme from pip._internal.vcs import vcs if MYPY_CHECK_RUNNING: from typing import ( - IO, Callable, List, Optional, Text, Tuple, + Callable, IO, List, Optional, Tuple, ) from mypy_extensions import TypedDict @@ -72,8 +68,7 @@ ) -__all__ = ['get_file_content', - 'unpack_vcs_link', +__all__ = ['unpack_vcs_link', 'unpack_file_url', 'unpack_http_url', 'unpack_url', 'parse_content_disposition', 'sanitize_content_filename'] @@ -82,57 +77,6 @@ logger = logging.getLogger(__name__) -def get_file_content(url, comes_from=None, session=None): - # type: (str, Optional[str], Optional[PipSession]) -> Tuple[str, Text] - """Gets the content of a file; it may be a filename, file: URL, or - http: URL. Returns (location, content). Content is unicode. - - :param url: File path or url. - :param comes_from: Origin description of requirements. - :param session: Instance of pip.download.PipSession. - """ - if session is None: - raise TypeError( - "get_file_content() missing 1 required keyword argument: 'session'" - ) - - scheme = get_url_scheme(url) - - if scheme in ['http', 'https']: - # FIXME: catch some errors - resp = session.get(url) - resp.raise_for_status() - return resp.url, resp.text - - elif scheme == 'file': - if comes_from and comes_from.startswith('http'): - raise InstallationError( - 'Requirements file %s references URL %s, which is local' - % (comes_from, url)) - - path = url.split(':', 1)[1] - path = path.replace('\\', '/') - match = _url_slash_drive_re.match(path) - if match: - path = match.group(1) + ':' + path.split('|', 1)[1] - path = urllib_parse.unquote(path) - if path.startswith('/'): - path = '/' + path.lstrip('/') - url = path - - try: - with open(url, 'rb') as f: - content = auto_decode(f.read()) - except IOError as exc: - raise InstallationError( - 'Could not open requirements file: %s' % str(exc) - ) - return url, content - - -_url_slash_drive_re = re.compile(r'/*([a-z])\|', re.I) - - def unpack_vcs_link(link, location): # type: (Link, str) -> None vcs_backend = _get_used_vcs_backend(link) diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index 83b3d344cbb..da75ad62813 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -17,14 +17,18 @@ from pip._vendor.six.moves.urllib import parse as urllib_parse from pip._internal.cli import cmdoptions -from pip._internal.download import get_file_content -from pip._internal.exceptions import RequirementsFileParseError +from pip._internal.exceptions import ( + InstallationError, + RequirementsFileParseError, +) from pip._internal.models.search_scope import SearchScope from pip._internal.req.constructors import ( install_req_from_editable, install_req_from_line, ) +from pip._internal.utils.encoding import auto_decode from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from pip._internal.utils.urls import get_url_scheme if MYPY_CHECK_RUNNING: from typing import ( @@ -401,3 +405,54 @@ def expand_env_variables(lines_enum): line = line.replace(env_var, value) yield line_number, line + + +def get_file_content(url, comes_from=None, session=None): + # type: (str, Optional[str], Optional[PipSession]) -> Tuple[str, Text] + """Gets the content of a file; it may be a filename, file: URL, or + http: URL. Returns (location, content). Content is unicode. + + :param url: File path or url. + :param comes_from: Origin description of requirements. + :param session: Instance of pip.download.PipSession. + """ + if session is None: + raise TypeError( + "get_file_content() missing 1 required keyword argument: 'session'" + ) + + scheme = get_url_scheme(url) + + if scheme in ['http', 'https']: + # FIXME: catch some errors + resp = session.get(url) + resp.raise_for_status() + return resp.url, resp.text + + elif scheme == 'file': + if comes_from and comes_from.startswith('http'): + raise InstallationError( + 'Requirements file %s references URL %s, which is local' + % (comes_from, url)) + + path = url.split(':', 1)[1] + path = path.replace('\\', '/') + match = _url_slash_drive_re.match(path) + if match: + path = match.group(1) + ':' + path.split('|', 1)[1] + path = urllib_parse.unquote(path) + if path.startswith('/'): + path = '/' + path.lstrip('/') + url = path + + try: + with open(url, 'rb') as f: + content = auto_decode(f.read()) + except IOError as exc: + raise InstallationError( + 'Could not open requirements file: %s' % str(exc) + ) + return url, content + + +_url_slash_drive_re = re.compile(r'/*([a-z])\|', re.I) From 6eb83c6d3abc3b0b86f8dad2ebffb6ea9e0e4e31 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 13 Oct 2019 18:44:23 -0400 Subject: [PATCH 0579/3170] Move remaining functions in download to operations.prepare The only user of this module is operations.prepare.RequirementPreparer. Moving the functionality to the single using module means that refactoring will be easier (since all the mess is in one place). This also removes a mis-named module from the top-level of the repository. --- src/pip/_internal/download.py | 520 ------------------------ src/pip/_internal/operations/prepare.py | 502 ++++++++++++++++++++++- tests/unit/test_build_env.py | 2 +- tests/unit/test_download.py | 10 +- 4 files changed, 504 insertions(+), 530 deletions(-) delete mode 100644 src/pip/_internal/download.py diff --git a/src/pip/_internal/download.py b/src/pip/_internal/download.py deleted file mode 100644 index 7306f98ab36..00000000000 --- a/src/pip/_internal/download.py +++ /dev/null @@ -1,520 +0,0 @@ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - -from __future__ import absolute_import - -import cgi -import logging -import mimetypes -import os -import shutil -import sys - -from pip._vendor import requests -from pip._vendor.requests.models import CONTENT_CHUNK_SIZE, Response -from pip._vendor.six import PY2 - -from pip._internal.exceptions import HashMismatch -from pip._internal.models.index import PyPI -from pip._internal.network.session import PipSession -from pip._internal.utils.filesystem import copy2_fixed -from pip._internal.utils.misc import ( - ask_path_exists, - backup_dir, - consume, - display_path, - format_size, - hide_url, - path_to_display, - rmtree, - splitext, -) -from pip._internal.utils.temp_dir import TempDirectory -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from pip._internal.utils.ui import DownloadProgressProvider -from pip._internal.utils.unpacking import unpack_file -from pip._internal.vcs import vcs - -if MYPY_CHECK_RUNNING: - from typing import ( - Callable, IO, List, Optional, Tuple, - ) - - from mypy_extensions import TypedDict - - from pip._internal.models.link import Link - from pip._internal.utils.hashes import Hashes - from pip._internal.vcs.versioncontrol import VersionControl - - if PY2: - CopytreeKwargs = TypedDict( - 'CopytreeKwargs', - { - 'ignore': Callable[[str, List[str]], List[str]], - 'symlinks': bool, - }, - total=False, - ) - else: - CopytreeKwargs = TypedDict( - 'CopytreeKwargs', - { - 'copy_function': Callable[[str, str], None], - 'ignore': Callable[[str, List[str]], List[str]], - 'ignore_dangling_symlinks': bool, - 'symlinks': bool, - }, - total=False, - ) - - -__all__ = ['unpack_vcs_link', - 'unpack_file_url', - 'unpack_http_url', 'unpack_url', - 'parse_content_disposition', 'sanitize_content_filename'] - - -logger = logging.getLogger(__name__) - - -def unpack_vcs_link(link, location): - # type: (Link, str) -> None - vcs_backend = _get_used_vcs_backend(link) - assert vcs_backend is not None - vcs_backend.unpack(location, url=hide_url(link.url)) - - -def _get_used_vcs_backend(link): - # type: (Link) -> Optional[VersionControl] - """ - Return a VersionControl object or None. - """ - for vcs_backend in vcs.backends: - if link.scheme in vcs_backend.schemes: - return vcs_backend - return None - - -def _progress_indicator(iterable, *args, **kwargs): - return iterable - - -def _download_url( - resp, # type: Response - link, # type: Link - content_file, # type: IO - hashes, # type: Optional[Hashes] - progress_bar # type: str -): - # type: (...) -> None - try: - total_length = int(resp.headers['content-length']) - except (ValueError, KeyError, TypeError): - total_length = 0 - - cached_resp = getattr(resp, "from_cache", False) - if logger.getEffectiveLevel() > logging.INFO: - show_progress = False - elif cached_resp: - show_progress = False - elif total_length > (40 * 1000): - show_progress = True - elif not total_length: - show_progress = True - else: - show_progress = False - - def resp_read(chunk_size): - try: - # Special case for urllib3. - for chunk in resp.raw.stream( - chunk_size, - # We use decode_content=False here because we don't - # want urllib3 to mess with the raw bytes we get - # from the server. If we decompress inside of - # urllib3 then we cannot verify the checksum - # because the checksum will be of the compressed - # file. This breakage will only occur if the - # server adds a Content-Encoding header, which - # depends on how the server was configured: - # - Some servers will notice that the file isn't a - # compressible file and will leave the file alone - # and with an empty Content-Encoding - # - Some servers will notice that the file is - # already compressed and will leave the file - # alone and will add a Content-Encoding: gzip - # header - # - Some servers won't notice anything at all and - # will take a file that's already been compressed - # and compress it again and set the - # Content-Encoding: gzip header - # - # By setting this not to decode automatically we - # hope to eliminate problems with the second case. - decode_content=False): - yield chunk - except AttributeError: - # Standard file-like object. - while True: - chunk = resp.raw.read(chunk_size) - if not chunk: - break - yield chunk - - def written_chunks(chunks): - for chunk in chunks: - content_file.write(chunk) - yield chunk - - progress_indicator = _progress_indicator - - if link.netloc == PyPI.file_storage_domain: - url = link.show_url - else: - url = link.url_without_fragment - - if show_progress: # We don't show progress on cached responses - progress_indicator = DownloadProgressProvider(progress_bar, - max=total_length) - if total_length: - logger.info("Downloading %s (%s)", url, format_size(total_length)) - else: - logger.info("Downloading %s", url) - elif cached_resp: - logger.info("Using cached %s", url) - else: - logger.info("Downloading %s", url) - - downloaded_chunks = written_chunks( - progress_indicator( - resp_read(CONTENT_CHUNK_SIZE), - CONTENT_CHUNK_SIZE - ) - ) - if hashes: - hashes.check_against_chunks(downloaded_chunks) - else: - consume(downloaded_chunks) - - -def _copy_file(filename, location, link): - copy = True - download_location = os.path.join(location, link.filename) - if os.path.exists(download_location): - response = ask_path_exists( - 'The file %s exists. (i)gnore, (w)ipe, (b)ackup, (a)abort' % - display_path(download_location), ('i', 'w', 'b', 'a')) - if response == 'i': - copy = False - elif response == 'w': - logger.warning('Deleting %s', display_path(download_location)) - os.remove(download_location) - elif response == 'b': - dest_file = backup_dir(download_location) - logger.warning( - 'Backing up %s to %s', - display_path(download_location), - display_path(dest_file), - ) - shutil.move(download_location, dest_file) - elif response == 'a': - sys.exit(-1) - if copy: - shutil.copy(filename, download_location) - logger.info('Saved %s', display_path(download_location)) - - -def unpack_http_url( - link, # type: Link - location, # type: str - download_dir=None, # type: Optional[str] - session=None, # type: Optional[PipSession] - hashes=None, # type: Optional[Hashes] - progress_bar="on" # type: str -): - # type: (...) -> None - if session is None: - raise TypeError( - "unpack_http_url() missing 1 required keyword argument: 'session'" - ) - - with TempDirectory(kind="unpack") as temp_dir: - # If a download dir is specified, is the file already downloaded there? - already_downloaded_path = None - if download_dir: - already_downloaded_path = _check_download_dir(link, - download_dir, - hashes) - - if already_downloaded_path: - from_path = already_downloaded_path - content_type = mimetypes.guess_type(from_path)[0] - else: - # let's download to a tmp dir - from_path, content_type = _download_http_url(link, - session, - temp_dir.path, - hashes, - progress_bar) - - # unpack the archive to the build dir location. even when only - # downloading archives, they have to be unpacked to parse dependencies - unpack_file(from_path, location, content_type) - - # a download dir is specified; let's copy the archive there - if download_dir and not already_downloaded_path: - _copy_file(from_path, download_dir, link) - - if not already_downloaded_path: - os.unlink(from_path) - - -def _copy2_ignoring_special_files(src, dest): - # type: (str, str) -> None - """Copying special files is not supported, but as a convenience to users - we skip errors copying them. This supports tools that may create e.g. - socket files in the project source directory. - """ - try: - copy2_fixed(src, dest) - except shutil.SpecialFileError as e: - # SpecialFileError may be raised due to either the source or - # destination. If the destination was the cause then we would actually - # care, but since the destination directory is deleted prior to - # copy we ignore all of them assuming it is caused by the source. - logger.warning( - "Ignoring special file error '%s' encountered copying %s to %s.", - str(e), - path_to_display(src), - path_to_display(dest), - ) - - -def _copy_source_tree(source, target): - # type: (str, str) -> None - def ignore(d, names): - # Pulling in those directories can potentially be very slow, - # exclude the following directories if they appear in the top - # level dir (and only it). - # See discussion at https://github.com/pypa/pip/pull/6770 - return ['.tox', '.nox'] if d == source else [] - - kwargs = dict(ignore=ignore, symlinks=True) # type: CopytreeKwargs - - if not PY2: - # Python 2 does not support copy_function, so we only ignore - # errors on special file copy in Python 3. - kwargs['copy_function'] = _copy2_ignoring_special_files - - shutil.copytree(source, target, **kwargs) - - -def unpack_file_url( - link, # type: Link - location, # type: str - download_dir=None, # type: Optional[str] - hashes=None # type: Optional[Hashes] -): - # type: (...) -> None - """Unpack link into location. - - If download_dir is provided and link points to a file, make a copy - of the link file inside download_dir. - """ - link_path = link.file_path - # If it's a url to a local directory - if link.is_existing_dir(): - if os.path.isdir(location): - rmtree(location) - _copy_source_tree(link_path, location) - if download_dir: - logger.info('Link is a directory, ignoring download_dir') - return - - # If --require-hashes is off, `hashes` is either empty, the - # link's embedded hash, or MissingHashes; it is required to - # match. If --require-hashes is on, we are satisfied by any - # hash in `hashes` matching: a URL-based or an option-based - # one; no internet-sourced hash will be in `hashes`. - if hashes: - hashes.check_against_path(link_path) - - # If a download dir is specified, is the file already there and valid? - already_downloaded_path = None - if download_dir: - already_downloaded_path = _check_download_dir(link, - download_dir, - hashes) - - if already_downloaded_path: - from_path = already_downloaded_path - else: - from_path = link_path - - content_type = mimetypes.guess_type(from_path)[0] - - # unpack the archive to the build dir location. even when only downloading - # archives, they have to be unpacked to parse dependencies - unpack_file(from_path, location, content_type) - - # a download dir is specified and not already downloaded - if download_dir and not already_downloaded_path: - _copy_file(from_path, download_dir, link) - - -def unpack_url( - link, # type: Link - location, # type: str - download_dir=None, # type: Optional[str] - session=None, # type: Optional[PipSession] - hashes=None, # type: Optional[Hashes] - progress_bar="on" # type: str -): - # type: (...) -> None - """Unpack link. - If link is a VCS link: - if only_download, export into download_dir and ignore location - else unpack into location - for other types of link: - - unpack into location - - if download_dir, copy the file into download_dir - - if only_download, mark location for deletion - - :param hashes: A Hashes object, one of whose embedded hashes must match, - or HashMismatch will be raised. If the Hashes is empty, no matches are - required, and unhashable types of requirements (like VCS ones, which - would ordinarily raise HashUnsupported) are allowed. - """ - # non-editable vcs urls - if link.is_vcs: - unpack_vcs_link(link, location) - - # file urls - elif link.is_file: - unpack_file_url(link, location, download_dir, hashes=hashes) - - # http urls - else: - if session is None: - session = PipSession() - - unpack_http_url( - link, - location, - download_dir, - session, - hashes=hashes, - progress_bar=progress_bar - ) - - -def sanitize_content_filename(filename): - # type: (str) -> str - """ - Sanitize the "filename" value from a Content-Disposition header. - """ - return os.path.basename(filename) - - -def parse_content_disposition(content_disposition, default_filename): - # type: (str, str) -> str - """ - Parse the "filename" value from a Content-Disposition header, and - return the default filename if the result is empty. - """ - _type, params = cgi.parse_header(content_disposition) - filename = params.get('filename') - if filename: - # We need to sanitize the filename to prevent directory traversal - # in case the filename contains ".." path parts. - filename = sanitize_content_filename(filename) - return filename or default_filename - - -def _download_http_url( - link, # type: Link - session, # type: PipSession - temp_dir, # type: str - hashes, # type: Optional[Hashes] - progress_bar # type: str -): - # type: (...) -> Tuple[str, str] - """Download link url into temp_dir using provided session""" - target_url = link.url.split('#', 1)[0] - try: - resp = session.get( - target_url, - # We use Accept-Encoding: identity here because requests - # defaults to accepting compressed responses. This breaks in - # a variety of ways depending on how the server is configured. - # - Some servers will notice that the file isn't a compressible - # file and will leave the file alone and with an empty - # Content-Encoding - # - Some servers will notice that the file is already - # compressed and will leave the file alone and will add a - # Content-Encoding: gzip header - # - Some servers won't notice anything at all and will take - # a file that's already been compressed and compress it again - # and set the Content-Encoding: gzip header - # By setting this to request only the identity encoding We're - # hoping to eliminate the third case. Hopefully there does not - # exist a server which when given a file will notice it is - # already compressed and that you're not asking for a - # compressed file and will then decompress it before sending - # because if that's the case I don't think it'll ever be - # possible to make this work. - headers={"Accept-Encoding": "identity"}, - stream=True, - ) - resp.raise_for_status() - except requests.HTTPError as exc: - logger.critical( - "HTTP error %s while getting %s", exc.response.status_code, link, - ) - raise - - content_type = resp.headers.get('content-type', '') - filename = link.filename # fallback - # Have a look at the Content-Disposition header for a better guess - content_disposition = resp.headers.get('content-disposition') - if content_disposition: - filename = parse_content_disposition(content_disposition, filename) - ext = splitext(filename)[1] # type: Optional[str] - if not ext: - ext = mimetypes.guess_extension(content_type) - if ext: - filename += ext - if not ext and link.url != resp.url: - ext = os.path.splitext(resp.url)[1] - if ext: - filename += ext - file_path = os.path.join(temp_dir, filename) - with open(file_path, 'wb') as content_file: - _download_url(resp, link, content_file, hashes, progress_bar) - return file_path, content_type - - -def _check_download_dir(link, download_dir, hashes): - # type: (Link, str, Optional[Hashes]) -> Optional[str] - """ Check download_dir for previously downloaded file with correct hash - If a correct file is found return its path else None - """ - download_path = os.path.join(download_dir, link.filename) - - if not os.path.exists(download_path): - return None - - # If already downloaded, does its hash match? - logger.info('File was already downloaded %s', download_path) - if hashes: - try: - hashes.check_against_path(download_path) - except HashMismatch: - logger.warning( - 'Previously-downloaded file %s has bad hash. ' - 'Re-downloading.', - download_path - ) - os.unlink(download_path) - return None - return download_path diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index d0930458d11..73a20e31999 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -5,38 +5,89 @@ # mypy: strict-optional=False # mypy: disallow-untyped-defs=False +import cgi import logging +import mimetypes import os +import shutil +import sys from pip._vendor import requests +from pip._vendor.requests.models import CONTENT_CHUNK_SIZE, Response +from pip._vendor.six import PY2 from pip._internal.distributions import ( make_distribution_for_install_requirement, ) from pip._internal.distributions.installed import InstalledDistribution -from pip._internal.download import unpack_url from pip._internal.exceptions import ( DirectoryUrlHashUnsupported, + HashMismatch, HashUnpinned, InstallationError, PreviousBuildDirError, VcsHashUnsupported, ) +from pip._internal.models.index import PyPI +from pip._internal.network.session import PipSession from pip._internal.utils.compat import expanduser +from pip._internal.utils.filesystem import copy2_fixed from pip._internal.utils.hashes import MissingHashes from pip._internal.utils.logging import indent_log from pip._internal.utils.marker_files import write_delete_marker_file -from pip._internal.utils.misc import display_path, normalize_path +from pip._internal.utils.misc import ( + ask_path_exists, + backup_dir, + consume, + display_path, + format_size, + hide_url, + normalize_path, + path_to_display, + rmtree, + splitext, +) +from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from pip._internal.utils.ui import DownloadProgressProvider +from pip._internal.utils.unpacking import unpack_file +from pip._internal.vcs import vcs if MYPY_CHECK_RUNNING: - from typing import Optional + from typing import ( + Callable, IO, List, Optional, Tuple, + ) + + from mypy_extensions import TypedDict from pip._internal.distributions import AbstractDistribution from pip._internal.index import PackageFinder - from pip._internal.network.session import PipSession + from pip._internal.models.link import Link from pip._internal.req.req_install import InstallRequirement from pip._internal.req.req_tracker import RequirementTracker + from pip._internal.utils.hashes import Hashes + from pip._internal.vcs.versioncontrol import VersionControl + + if PY2: + CopytreeKwargs = TypedDict( + 'CopytreeKwargs', + { + 'ignore': Callable[[str, List[str]], List[str]], + 'symlinks': bool, + }, + total=False, + ) + else: + CopytreeKwargs = TypedDict( + 'CopytreeKwargs', + { + 'copy_function': Callable[[str, str], None], + 'ignore': Callable[[str, List[str]], List[str]], + 'ignore_dangling_symlinks': bool, + 'symlinks': bool, + }, + total=False, + ) logger = logging.getLogger(__name__) @@ -50,6 +101,449 @@ def _get_prepared_distribution(req, req_tracker, finder, build_isolation): return abstract_dist +def unpack_vcs_link(link, location): + # type: (Link, str) -> None + vcs_backend = _get_used_vcs_backend(link) + assert vcs_backend is not None + vcs_backend.unpack(location, url=hide_url(link.url)) + + +def _get_used_vcs_backend(link): + # type: (Link) -> Optional[VersionControl] + """ + Return a VersionControl object or None. + """ + for vcs_backend in vcs.backends: + if link.scheme in vcs_backend.schemes: + return vcs_backend + return None + + +def _progress_indicator(iterable, *args, **kwargs): + return iterable + + +def _download_url( + resp, # type: Response + link, # type: Link + content_file, # type: IO + hashes, # type: Optional[Hashes] + progress_bar # type: str +): + # type: (...) -> None + try: + total_length = int(resp.headers['content-length']) + except (ValueError, KeyError, TypeError): + total_length = 0 + + cached_resp = getattr(resp, "from_cache", False) + if logger.getEffectiveLevel() > logging.INFO: + show_progress = False + elif cached_resp: + show_progress = False + elif total_length > (40 * 1000): + show_progress = True + elif not total_length: + show_progress = True + else: + show_progress = False + + def resp_read(chunk_size): + try: + # Special case for urllib3. + for chunk in resp.raw.stream( + chunk_size, + # We use decode_content=False here because we don't + # want urllib3 to mess with the raw bytes we get + # from the server. If we decompress inside of + # urllib3 then we cannot verify the checksum + # because the checksum will be of the compressed + # file. This breakage will only occur if the + # server adds a Content-Encoding header, which + # depends on how the server was configured: + # - Some servers will notice that the file isn't a + # compressible file and will leave the file alone + # and with an empty Content-Encoding + # - Some servers will notice that the file is + # already compressed and will leave the file + # alone and will add a Content-Encoding: gzip + # header + # - Some servers won't notice anything at all and + # will take a file that's already been compressed + # and compress it again and set the + # Content-Encoding: gzip header + # + # By setting this not to decode automatically we + # hope to eliminate problems with the second case. + decode_content=False): + yield chunk + except AttributeError: + # Standard file-like object. + while True: + chunk = resp.raw.read(chunk_size) + if not chunk: + break + yield chunk + + def written_chunks(chunks): + for chunk in chunks: + content_file.write(chunk) + yield chunk + + progress_indicator = _progress_indicator + + if link.netloc == PyPI.file_storage_domain: + url = link.show_url + else: + url = link.url_without_fragment + + if show_progress: # We don't show progress on cached responses + progress_indicator = DownloadProgressProvider(progress_bar, + max=total_length) + if total_length: + logger.info("Downloading %s (%s)", url, format_size(total_length)) + else: + logger.info("Downloading %s", url) + elif cached_resp: + logger.info("Using cached %s", url) + else: + logger.info("Downloading %s", url) + + downloaded_chunks = written_chunks( + progress_indicator( + resp_read(CONTENT_CHUNK_SIZE), + CONTENT_CHUNK_SIZE + ) + ) + if hashes: + hashes.check_against_chunks(downloaded_chunks) + else: + consume(downloaded_chunks) + + +def _copy_file(filename, location, link): + copy = True + download_location = os.path.join(location, link.filename) + if os.path.exists(download_location): + response = ask_path_exists( + 'The file %s exists. (i)gnore, (w)ipe, (b)ackup, (a)abort' % + display_path(download_location), ('i', 'w', 'b', 'a')) + if response == 'i': + copy = False + elif response == 'w': + logger.warning('Deleting %s', display_path(download_location)) + os.remove(download_location) + elif response == 'b': + dest_file = backup_dir(download_location) + logger.warning( + 'Backing up %s to %s', + display_path(download_location), + display_path(dest_file), + ) + shutil.move(download_location, dest_file) + elif response == 'a': + sys.exit(-1) + if copy: + shutil.copy(filename, download_location) + logger.info('Saved %s', display_path(download_location)) + + +def unpack_http_url( + link, # type: Link + location, # type: str + download_dir=None, # type: Optional[str] + session=None, # type: Optional[PipSession] + hashes=None, # type: Optional[Hashes] + progress_bar="on" # type: str +): + # type: (...) -> None + if session is None: + raise TypeError( + "unpack_http_url() missing 1 required keyword argument: 'session'" + ) + + with TempDirectory(kind="unpack") as temp_dir: + # If a download dir is specified, is the file already downloaded there? + already_downloaded_path = None + if download_dir: + already_downloaded_path = _check_download_dir(link, + download_dir, + hashes) + + if already_downloaded_path: + from_path = already_downloaded_path + content_type = mimetypes.guess_type(from_path)[0] + else: + # let's download to a tmp dir + from_path, content_type = _download_http_url(link, + session, + temp_dir.path, + hashes, + progress_bar) + + # unpack the archive to the build dir location. even when only + # downloading archives, they have to be unpacked to parse dependencies + unpack_file(from_path, location, content_type) + + # a download dir is specified; let's copy the archive there + if download_dir and not already_downloaded_path: + _copy_file(from_path, download_dir, link) + + if not already_downloaded_path: + os.unlink(from_path) + + +def _copy2_ignoring_special_files(src, dest): + # type: (str, str) -> None + """Copying special files is not supported, but as a convenience to users + we skip errors copying them. This supports tools that may create e.g. + socket files in the project source directory. + """ + try: + copy2_fixed(src, dest) + except shutil.SpecialFileError as e: + # SpecialFileError may be raised due to either the source or + # destination. If the destination was the cause then we would actually + # care, but since the destination directory is deleted prior to + # copy we ignore all of them assuming it is caused by the source. + logger.warning( + "Ignoring special file error '%s' encountered copying %s to %s.", + str(e), + path_to_display(src), + path_to_display(dest), + ) + + +def _copy_source_tree(source, target): + # type: (str, str) -> None + def ignore(d, names): + # Pulling in those directories can potentially be very slow, + # exclude the following directories if they appear in the top + # level dir (and only it). + # See discussion at https://github.com/pypa/pip/pull/6770 + return ['.tox', '.nox'] if d == source else [] + + kwargs = dict(ignore=ignore, symlinks=True) # type: CopytreeKwargs + + if not PY2: + # Python 2 does not support copy_function, so we only ignore + # errors on special file copy in Python 3. + kwargs['copy_function'] = _copy2_ignoring_special_files + + shutil.copytree(source, target, **kwargs) + + +def unpack_file_url( + link, # type: Link + location, # type: str + download_dir=None, # type: Optional[str] + hashes=None # type: Optional[Hashes] +): + # type: (...) -> None + """Unpack link into location. + + If download_dir is provided and link points to a file, make a copy + of the link file inside download_dir. + """ + link_path = link.file_path + # If it's a url to a local directory + if link.is_existing_dir(): + if os.path.isdir(location): + rmtree(location) + _copy_source_tree(link_path, location) + if download_dir: + logger.info('Link is a directory, ignoring download_dir') + return + + # If --require-hashes is off, `hashes` is either empty, the + # link's embedded hash, or MissingHashes; it is required to + # match. If --require-hashes is on, we are satisfied by any + # hash in `hashes` matching: a URL-based or an option-based + # one; no internet-sourced hash will be in `hashes`. + if hashes: + hashes.check_against_path(link_path) + + # If a download dir is specified, is the file already there and valid? + already_downloaded_path = None + if download_dir: + already_downloaded_path = _check_download_dir(link, + download_dir, + hashes) + + if already_downloaded_path: + from_path = already_downloaded_path + else: + from_path = link_path + + content_type = mimetypes.guess_type(from_path)[0] + + # unpack the archive to the build dir location. even when only downloading + # archives, they have to be unpacked to parse dependencies + unpack_file(from_path, location, content_type) + + # a download dir is specified and not already downloaded + if download_dir and not already_downloaded_path: + _copy_file(from_path, download_dir, link) + + +def unpack_url( + link, # type: Link + location, # type: str + download_dir=None, # type: Optional[str] + session=None, # type: Optional[PipSession] + hashes=None, # type: Optional[Hashes] + progress_bar="on" # type: str +): + # type: (...) -> None + """Unpack link. + If link is a VCS link: + if only_download, export into download_dir and ignore location + else unpack into location + for other types of link: + - unpack into location + - if download_dir, copy the file into download_dir + - if only_download, mark location for deletion + + :param hashes: A Hashes object, one of whose embedded hashes must match, + or HashMismatch will be raised. If the Hashes is empty, no matches are + required, and unhashable types of requirements (like VCS ones, which + would ordinarily raise HashUnsupported) are allowed. + """ + # non-editable vcs urls + if link.is_vcs: + unpack_vcs_link(link, location) + + # file urls + elif link.is_file: + unpack_file_url(link, location, download_dir, hashes=hashes) + + # http urls + else: + if session is None: + session = PipSession() + + unpack_http_url( + link, + location, + download_dir, + session, + hashes=hashes, + progress_bar=progress_bar + ) + + +def sanitize_content_filename(filename): + # type: (str) -> str + """ + Sanitize the "filename" value from a Content-Disposition header. + """ + return os.path.basename(filename) + + +def parse_content_disposition(content_disposition, default_filename): + # type: (str, str) -> str + """ + Parse the "filename" value from a Content-Disposition header, and + return the default filename if the result is empty. + """ + _type, params = cgi.parse_header(content_disposition) + filename = params.get('filename') + if filename: + # We need to sanitize the filename to prevent directory traversal + # in case the filename contains ".." path parts. + filename = sanitize_content_filename(filename) + return filename or default_filename + + +def _download_http_url( + link, # type: Link + session, # type: PipSession + temp_dir, # type: str + hashes, # type: Optional[Hashes] + progress_bar # type: str +): + # type: (...) -> Tuple[str, str] + """Download link url into temp_dir using provided session""" + target_url = link.url.split('#', 1)[0] + try: + resp = session.get( + target_url, + # We use Accept-Encoding: identity here because requests + # defaults to accepting compressed responses. This breaks in + # a variety of ways depending on how the server is configured. + # - Some servers will notice that the file isn't a compressible + # file and will leave the file alone and with an empty + # Content-Encoding + # - Some servers will notice that the file is already + # compressed and will leave the file alone and will add a + # Content-Encoding: gzip header + # - Some servers won't notice anything at all and will take + # a file that's already been compressed and compress it again + # and set the Content-Encoding: gzip header + # By setting this to request only the identity encoding We're + # hoping to eliminate the third case. Hopefully there does not + # exist a server which when given a file will notice it is + # already compressed and that you're not asking for a + # compressed file and will then decompress it before sending + # because if that's the case I don't think it'll ever be + # possible to make this work. + headers={"Accept-Encoding": "identity"}, + stream=True, + ) + resp.raise_for_status() + except requests.HTTPError as exc: + logger.critical( + "HTTP error %s while getting %s", exc.response.status_code, link, + ) + raise + + content_type = resp.headers.get('content-type', '') + filename = link.filename # fallback + # Have a look at the Content-Disposition header for a better guess + content_disposition = resp.headers.get('content-disposition') + if content_disposition: + filename = parse_content_disposition(content_disposition, filename) + ext = splitext(filename)[1] # type: Optional[str] + if not ext: + ext = mimetypes.guess_extension(content_type) + if ext: + filename += ext + if not ext and link.url != resp.url: + ext = os.path.splitext(resp.url)[1] + if ext: + filename += ext + file_path = os.path.join(temp_dir, filename) + with open(file_path, 'wb') as content_file: + _download_url(resp, link, content_file, hashes, progress_bar) + return file_path, content_type + + +def _check_download_dir(link, download_dir, hashes): + # type: (Link, str, Optional[Hashes]) -> Optional[str] + """ Check download_dir for previously downloaded file with correct hash + If a correct file is found return its path else None + """ + download_path = os.path.join(download_dir, link.filename) + + if not os.path.exists(download_path): + return None + + # If already downloaded, does its hash match? + logger.info('File was already downloaded %s', download_path) + if hashes: + try: + hashes.check_against_path(download_path) + except HashMismatch: + logger.warning( + 'Previously-downloaded file %s has bad hash. ' + 'Re-downloading.', + download_path + ) + os.unlink(download_path) + return None + return download_path + + class RequirementPreparer(object): """Prepares a Requirement """ diff --git a/tests/unit/test_build_env.py b/tests/unit/test_build_env.py index 3e3c7ce9fcb..bcc241bbedf 100644 --- a/tests/unit/test_build_env.py +++ b/tests/unit/test_build_env.py @@ -23,12 +23,12 @@ def run_with_build_env(script, setup_script_contents, from pip._internal.build_env import BuildEnvironment from pip._internal.collector import LinkCollector - from pip._internal.download import PipSession from pip._internal.index import PackageFinder from pip._internal.models.search_scope import SearchScope from pip._internal.models.selection_prefs import ( SelectionPreferences ) + from pip._internal.network.session import PipSession link_collector = LinkCollector( session=PipSession(), diff --git a/tests/unit/test_download.py b/tests/unit/test_download.py index 338b2bbad40..9241f9ee2ca 100644 --- a/tests/unit/test_download.py +++ b/tests/unit/test_download.py @@ -9,7 +9,10 @@ import pytest from mock import Mock, patch -from pip._internal.download import ( +from pip._internal.exceptions import HashMismatch +from pip._internal.models.link import Link +from pip._internal.network.session import PipSession +from pip._internal.operations.prepare import ( _copy_source_tree, _download_http_url, parse_content_disposition, @@ -17,9 +20,6 @@ unpack_file_url, unpack_http_url, ) -from pip._internal.exceptions import HashMismatch -from pip._internal.models.link import Link -from pip._internal.network.session import PipSession from pip._internal.utils.hashes import Hashes from pip._internal.utils.urls import path_to_url from tests.lib import create_file @@ -116,7 +116,7 @@ def register_hook(self, event_name, callback): self.hooks.setdefault(event_name, []).append(callback) -@patch('pip._internal.download.unpack_file') +@patch('pip._internal.operations.prepare.unpack_file') def test_unpack_http_url_bad_downloaded_checksum(mock_unpack_file): """ If already-downloaded file has bad checksum, re-download. From 5c5c6eca83dd7acf3b3af135fb170abbd7f89180 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 13 Oct 2019 18:46:54 -0400 Subject: [PATCH 0580/3170] Rename test file to align with moved functions --- tests/unit/test_networking_auth.py | 6 +++++- tests/unit/{test_download.py => test_operations_prepare.py} | 0 2 files changed, 5 insertions(+), 1 deletion(-) rename tests/unit/{test_download.py => test_operations_prepare.py} (100%) diff --git a/tests/unit/test_networking_auth.py b/tests/unit/test_networking_auth.py index 0f0b6790ae1..45702f27b67 100644 --- a/tests/unit/test_networking_auth.py +++ b/tests/unit/test_networking_auth.py @@ -4,7 +4,11 @@ import pip._internal.network.auth from pip._internal.network.auth import MultiDomainBasicAuth -from tests.unit.test_download import MockConnection, MockRequest, MockResponse +from tests.unit.test_operations_prepare import ( + MockConnection, + MockRequest, + MockResponse, +) @pytest.mark.parametrize(["input_url", "url", "username", "password"], [ diff --git a/tests/unit/test_download.py b/tests/unit/test_operations_prepare.py similarity index 100% rename from tests/unit/test_download.py rename to tests/unit/test_operations_prepare.py From 144611ca1ed2464f58ec759cbfccd9c6725bea84 Mon Sep 17 00:00:00 2001 From: Neil Botelho <neil.botelho321@gmail.com> Date: Thu, 17 Oct 2019 12:31:49 +0530 Subject: [PATCH 0581/3170] Show cert config value first Move the cert config show_value statement to before the REQUESTS_CA_BUNDLE statement. Removed lines used for testing. --- src/pip/_internal/commands/debug.py | 2 +- tests/functional/test_debug.py | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/commands/debug.py b/src/pip/_internal/commands/debug.py index 5e62e737c18..2773dfd52d0 100644 --- a/src/pip/_internal/commands/debug.py +++ b/src/pip/_internal/commands/debug.py @@ -133,10 +133,10 @@ def run(self, options, args): show_value('sys.platform', sys.platform) show_sys_implementation() + show_value("'cert' config value", ca_bundle_info(self.parser.config)) show_value("REQUESTS_CA_BUNDLE", os.environ.get('REQUESTS_CA_BUNDLE')) show_value("CURL_CA_BUNDLE", os.environ.get('CURL_CA_BUNDLE')) show_value("pip._vendor.certifi.where()", where()) - show_value("'cert' config value", ca_bundle_info(self.parser.config)) show_tags(options) diff --git a/tests/functional/test_debug.py b/tests/functional/test_debug.py index 20d34feb983..1d3509c7b72 100644 --- a/tests/functional/test_debug.py +++ b/tests/functional/test_debug.py @@ -10,10 +10,11 @@ 'locale.getpreferredencoding: ', 'sys.platform: ', 'sys.implementation:', + '\'cert\' config value: ', 'REQUESTS_CA_BUNDLE: ', 'CURL_CA_BUNDLE: ', 'pip._vendor.certifi.where(): ', - '\'cert\' config value: ', + ]) def test_debug(script, expected_text): """ From 58dd0c3491df95b000693bfea4539d2fb4246ae5 Mon Sep 17 00:00:00 2001 From: robin elisha robinson <elisha.rob@gmail.com> Date: Thu, 17 Oct 2019 14:53:19 +0530 Subject: [PATCH 0582/3170] Change %s string formatting to str.format in setup.py (#7199) --- news/7199.trivial | 1 + setup.py | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 news/7199.trivial diff --git a/news/7199.trivial b/news/7199.trivial new file mode 100644 index 00000000000..0f7f2fea83b --- /dev/null +++ b/news/7199.trivial @@ -0,0 +1 @@ +adding line in trivial file to avoid linter issues. diff --git a/setup.py b/setup.py index db4c55eeb6a..6da704c3d67 100644 --- a/setup.py +++ b/setup.py @@ -75,8 +75,8 @@ def find_version(*file_paths): entry_points={ "console_scripts": [ "pip=pip._internal.main:main", - "pip%s=pip._internal.main:main" % sys.version_info[:1], - "pip%s.%s=pip._internal.main:main" % sys.version_info[:2], + "pip{}=pip._internal.main:main".format(sys.version_info[0]), + "pip{}.{}=pip._internal.main:main".format(*sys.version_info[:2]), ], }, From 4bc72977fcdb89ceecffadb42de14f7463ce2d62 Mon Sep 17 00:00:00 2001 From: Christopher Hunt <chrahunt@gmail.com> Date: Thu, 17 Oct 2019 09:22:03 -0400 Subject: [PATCH 0583/3170] Merge pull request #7219 from hugovk/add-3.8 Declare support for Python 3.8 --- docs/html/installing.rst | 2 +- news/7219.feature | 1 + setup.py | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 news/7219.feature diff --git a/docs/html/installing.rst b/docs/html/installing.rst index 6b50f929bc9..cfcdf7edabc 100644 --- a/docs/html/installing.rst +++ b/docs/html/installing.rst @@ -109,7 +109,7 @@ On Windows [4]_:: Python and OS Compatibility --------------------------- -pip works with CPython versions 2.7, 3.5, 3.6, 3.7 and also PyPy. +pip works with CPython versions 2.7, 3.5, 3.6, 3.7, 3.8 and also PyPy. This means pip works on the latest patch version of each of these minor versions. Previous patch versions are supported on a best effort approach. diff --git a/news/7219.feature b/news/7219.feature new file mode 100644 index 00000000000..ba0e9370489 --- /dev/null +++ b/news/7219.feature @@ -0,0 +1 @@ +Document Python 3.8 support. diff --git a/setup.py b/setup.py index db4c55eeb6a..31100ed5b84 100644 --- a/setup.py +++ b/setup.py @@ -52,6 +52,7 @@ def find_version(*file_paths): "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", + "Programming Language :: Python :: 3.8", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ], From 6ee768a1d4361bb70ce495521d78018cac18fce8 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 17 Oct 2019 22:18:03 +0530 Subject: [PATCH 0584/3170] Fix PEP 517 builds for packages without setup.py (#6606) --- news/6606.bugfix | 1 + src/pip/_internal/commands/install.py | 3 ++- .../pep517_setup_and_pyproject/pyproject.toml | 3 +++ .../packages/pep517_setup_and_pyproject/setup.cfg | 3 +++ .../packages/pep517_setup_and_pyproject/setup.py | 3 +++ tests/functional/test_install.py | 13 +++++++++++++ 6 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 news/6606.bugfix create mode 100644 tests/data/packages/pep517_setup_and_pyproject/pyproject.toml create mode 100644 tests/data/packages/pep517_setup_and_pyproject/setup.cfg create mode 100644 tests/data/packages/pep517_setup_and_pyproject/setup.py diff --git a/news/6606.bugfix b/news/6606.bugfix new file mode 100644 index 00000000000..3fbf7262f14 --- /dev/null +++ b/news/6606.bugfix @@ -0,0 +1 @@ +Fix bug that prevented installation of PEP 517 packages without ``setup.py``. diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 66071f6e819..5842d18da1b 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -1,4 +1,3 @@ - # The following comment should be removed at some point in the future. # It's included for now because without it InstallCommand.run() has a # couple errors where we have to know req.name is str rather than @@ -102,6 +101,8 @@ def get_check_binary_allowed(format_control): # type: (FormatControl) -> BinaryAllowedPredicate def check_binary_allowed(req): # type: (InstallRequirement) -> bool + if req.use_pep517: + return True canonical_name = canonicalize_name(req.name) allowed_formats = format_control.get_allowed_formats(canonical_name) return "binary" in allowed_formats diff --git a/tests/data/packages/pep517_setup_and_pyproject/pyproject.toml b/tests/data/packages/pep517_setup_and_pyproject/pyproject.toml new file mode 100644 index 00000000000..5a561e83317 --- /dev/null +++ b/tests/data/packages/pep517_setup_and_pyproject/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = [ "setuptools" ] +build-backend = "setuptools.build_meta" diff --git a/tests/data/packages/pep517_setup_and_pyproject/setup.cfg b/tests/data/packages/pep517_setup_and_pyproject/setup.cfg new file mode 100644 index 00000000000..7eae9c0111e --- /dev/null +++ b/tests/data/packages/pep517_setup_and_pyproject/setup.cfg @@ -0,0 +1,3 @@ +[metadata] +name = pep517-setup-and-pyproject +version = 1.0 diff --git a/tests/data/packages/pep517_setup_and_pyproject/setup.py b/tests/data/packages/pep517_setup_and_pyproject/setup.py new file mode 100644 index 00000000000..606849326a4 --- /dev/null +++ b/tests/data/packages/pep517_setup_and_pyproject/setup.py @@ -0,0 +1,3 @@ +from setuptools import setup + +setup() diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index f07dcd4eff0..0bea0543f8b 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1274,6 +1274,19 @@ def test_install_no_binary_disables_building_wheels(script, data, with_wheel): assert "Running setup.py install for upper" in str(res), str(res) +def test_install_no_binary_builds_pep_517_wheel(script, data, with_wheel): + to_install = data.packages.joinpath('pep517_setup_and_pyproject') + res = script.pip( + 'install', '--no-binary=:all:', '-f', data.find_links, to_install + ) + expected = ("Successfully installed pep517-setup-and-pyproject") + # Must have installed the package + assert expected in str(res), str(res) + + assert "Building wheel for pep517-setup" in str(res), str(res) + assert "Running setup.py install for pep517-set" not in str(res), str(res) + + def test_install_no_binary_disables_cached_wheels(script, data, with_wheel): # Seed the cache script.pip( From 6500e595601b8cb4ce597b9f7a71aaf82441c1f8 Mon Sep 17 00:00:00 2001 From: Xavier Fernandez <xavier.fernandez@polyconseil.fr> Date: Thu, 17 Oct 2019 21:32:34 +0200 Subject: [PATCH 0585/3170] Release 19.3.1 --- NEWS.rst | 14 ++++++++++++++ news/6606.bugfix | 1 - news/7219.feature | 1 - src/pip/__init__.py | 2 +- 4 files changed, 15 insertions(+), 3 deletions(-) delete mode 100644 news/6606.bugfix delete mode 100644 news/7219.feature diff --git a/NEWS.rst b/NEWS.rst index 859b25d997a..d440dce3181 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -7,6 +7,20 @@ .. towncrier release notes start +19.3.1 (2019-10-17) +=================== + +Features +-------- + +- Document Python 3.8 support. (`#7219 <https://github.com/pypa/pip/issues/7219>`_) + +Bug Fixes +--------- + +- Fix bug that prevented installation of PEP 517 packages without ``setup.py``. (`#6606 <https://github.com/pypa/pip/issues/6606>`_) + + 19.3 (2019-10-14) ================= diff --git a/news/6606.bugfix b/news/6606.bugfix deleted file mode 100644 index 3fbf7262f14..00000000000 --- a/news/6606.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix bug that prevented installation of PEP 517 packages without ``setup.py``. diff --git a/news/7219.feature b/news/7219.feature deleted file mode 100644 index ba0e9370489..00000000000 --- a/news/7219.feature +++ /dev/null @@ -1 +0,0 @@ -Document Python 3.8 support. diff --git a/src/pip/__init__.py b/src/pip/__init__.py index a24cb60dd22..a487794a9ba 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -1 +1 @@ -__version__ = "19.3" +__version__ = "19.3.1" From 00e3f9f09883b18ff1edc414278932582571fe45 Mon Sep 17 00:00:00 2001 From: Xavier Fernandez <xavier.fernandez@polyconseil.fr> Date: Thu, 17 Oct 2019 21:32:34 +0200 Subject: [PATCH 0586/3170] Back to development --- src/pip/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/__init__.py b/src/pip/__init__.py index a487794a9ba..c0496d674b6 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -1 +1 @@ -__version__ = "19.3.1" +__version__ = "19.4.dev0" From 4bb13dad143e0bbba35c9bc427c19cf44119e6bb Mon Sep 17 00:00:00 2001 From: Prabhu Marappan <prabhum.794@gmail.com> Date: Fri, 18 Oct 2019 05:16:20 +0530 Subject: [PATCH 0587/3170] Use str.format Instead of %s (#7196) Co-Authored-By: Pradyun Gedam <pradyunsg@gmail.com> --- news/7334.trivial | 0 src/pip/_internal/operations/generate_metadata.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 news/7334.trivial diff --git a/news/7334.trivial b/news/7334.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/operations/generate_metadata.py b/src/pip/_internal/operations/generate_metadata.py index 984748d7fdd..e944e54ee5a 100644 --- a/src/pip/_internal/operations/generate_metadata.py +++ b/src/pip/_internal/operations/generate_metadata.py @@ -79,7 +79,7 @@ def depth_of_directory(dir_): if not filenames: raise InstallationError( - "Files/directories not found in %s" % base + "Files/directories not found in {}".format(base) ) # If we have more than one match, we pick the toplevel one. This From 5098203271af5b340d6628217596e6db297ad4b4 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 18 Oct 2019 08:48:17 -0400 Subject: [PATCH 0588/3170] Add newline to news file --- news/5716.bugfix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/5716.bugfix b/news/5716.bugfix index 1b705df6f92..2659657cb61 100644 --- a/news/5716.bugfix +++ b/news/5716.bugfix @@ -1 +1 @@ -Fix case sensitive comparison of pip freeze when used with -r option. \ No newline at end of file +Fix case sensitive comparison of pip freeze when used with -r option. From e94e7877ab09ad11c7eb53ccad50097faa0ebede Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sat, 19 Oct 2019 11:23:27 +0100 Subject: [PATCH 0589/3170] Polish & clarify decide_user_install() function --- src/pip/_internal/commands/install.py | 45 +++++++++++++++++---------- 1 file changed, 28 insertions(+), 17 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 84feb023bc5..f6a5ac53e6a 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -601,19 +601,25 @@ def site_packages_writable(**kwargs): def decide_user_install( - use_user_site, - prefix_path, - target_dir, - root_path, - isolated_mode, + use_user_site, # type: Optional[bool] + prefix_path, # type: Optional[str] + target_dir, # type: Optional[str] + root_path, # type: Optional[str] + isolated_mode, # type: bool ): + # type: (...) -> bool """Determine whether to do a user install based on the input options. - If use_user_site is True/False, that is checked for compatibility with - other options. If None, the default behaviour depends on other options - and the environment. + If use_user_site is False, no additional checks are done. + If use_user_site is True, it is checked for compatibility with other + options. + If use_user_site is None, the default behaviour depends on the environment, + which is provided by the other arguments. """ - if use_user_site: + if use_user_site is False: + return False + + if use_user_site is True: if prefix_path: raise CommandError( "Can not combine '--user' and '--prefix' as they imply " @@ -624,17 +630,22 @@ def decide_user_install( "Can not perform a '--user' install. User site-packages " "are not visible in this virtualenv." ) - if use_user_site in (True, False): - return use_user_site + return True + + # If we are here, user installs have not been explicitly requested/avoided + assert use_user_site is None + # user install incompatible with --prefix/--target if prefix_path or target_dir: - return False # user install incompatible with --prefix/--target + return False + + # If user installs are not enabled, choose a non-user install + if not site.ENABLE_USER_SITE: + return False - # Default behaviour: prefer non-user installation if that looks possible. - # If we don't have permission for that and user site-packages are visible, - # choose a user install. - return site.ENABLE_USER_SITE and not site_packages_writable( - root=root_path, isolated=isolated_mode + # If we don't have permissions for a non-user install, choose a user install + return not site_packages_writable( + root=root_path, isolated=isolated_mode, ) From ecac7a17b51dbfb59a554633a165e4889baac776 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver <thomas@kluyver.me.uk> Date: Sat, 19 Oct 2019 11:28:51 +0100 Subject: [PATCH 0590/3170] Line length --- src/pip/_internal/commands/install.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index f6a5ac53e6a..cb7b972ee17 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -643,7 +643,7 @@ def decide_user_install( if not site.ENABLE_USER_SITE: return False - # If we don't have permissions for a non-user install, choose a user install + # If we don't have permission for a non-user install, choose a user install return not site_packages_writable( root=root_path, isolated=isolated_mode, ) From 4d968f5ff31fd21265f2d489fec7a49d0be4f7f4 Mon Sep 17 00:00:00 2001 From: Aniruddha Basak <codewithaniruddha@gmail.com> Date: Sat, 19 Oct 2019 22:06:11 +0530 Subject: [PATCH 0591/3170] Add url with special charecter and single part token --- docs/html/user_guide.rst | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index e088e419a00..8b1c8e1ea37 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -55,7 +55,14 @@ Basic Authentication Credentials pip supports basic authentication credentials. Basically, in the url there is a username and password separated by ``:``. -``https://[username[:password]]@pypi.company.com/simple`` + :: + + https://[username[:password]]@pypi.company.com/simple + 0123456789abcdef@pypi.company.com + https://aniruddha%24basak:gdg%24js%5Ejf%26l@pypi.company.com + +`Here <https://en.wikipedia.org/wiki/Percent-encoding>`_ you can find more about +percent encoding. Using a Proxy Server From f7a00656364ad9b8e157f3fcf0fb6392da887fcf Mon Sep 17 00:00:00 2001 From: Aniruddha Basak <codewithaniruddha@gmail.com> Date: Sat, 19 Oct 2019 22:21:59 +0530 Subject: [PATCH 0592/3170] Add block in the code snippet --- docs/html/user_guide.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 8b1c8e1ea37..e2aec8a19ff 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -57,9 +57,9 @@ a username and password separated by ``:``. :: - https://[username[:password]]@pypi.company.com/simple - 0123456789abcdef@pypi.company.com - https://aniruddha%24basak:gdg%24js%5Ejf%26l@pypi.company.com + https://[username[:password]]@pypi.company.com/simple + 0123456789abcdef@pypi.company.com + https://aniruddha%24basak:gdg%24js%5Ejf%26l@pypi.company.com `Here <https://en.wikipedia.org/wiki/Percent-encoding>`_ you can find more about percent encoding. From 23ab63e2f36dec54cf8c98e8c24e1dff3b91e4e4 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 6 Oct 2019 22:18:47 +0530 Subject: [PATCH 0593/3170] Import FormatControl more directly --- src/pip/_internal/cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/cache.py b/src/pip/_internal/cache.py index 8ebecbbd5a6..c5431e14d14 100644 --- a/src/pip/_internal/cache.py +++ b/src/pip/_internal/cache.py @@ -20,7 +20,7 @@ if MYPY_CHECK_RUNNING: from typing import Optional, Set, List, Any - from pip._internal.index import FormatControl + from pip._internal.models.format_control import FormatControl from pip._internal.pep425tags import Pep425Tag logger = logging.getLogger(__name__) From c18e912b2ac62e374feb02c2a430f95fe5b84dfc Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 6 Oct 2019 22:21:43 +0530 Subject: [PATCH 0594/3170] Import InstallationCandidate more directly --- tests/unit/test_self_check_outdated.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_self_check_outdated.py b/tests/unit/test_self_check_outdated.py index e8c40224770..72fa6929357 100644 --- a/tests/unit/test_self_check_outdated.py +++ b/tests/unit/test_self_check_outdated.py @@ -10,7 +10,7 @@ from pip._vendor import pkg_resources from pip._internal import self_outdated_check -from pip._internal.index import InstallationCandidate +from pip._internal.models.candidate import InstallationCandidate from pip._internal.network.session import PipSession from pip._internal.self_outdated_check import ( SelfCheckState, From 66e9b44f15bbfc7335d05c3c7121a2e305443882 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 6 Oct 2019 22:27:09 +0530 Subject: [PATCH 0595/3170] Import req_file more directly --- tests/unit/test_req_file.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py index 41e07e60f02..ca86a191b29 100644 --- a/tests/unit/test_req_file.py +++ b/tests/unit/test_req_file.py @@ -7,7 +7,7 @@ from mock import Mock, patch from pretend import stub -import pip._internal.index +import pip._internal.req.req_file # this will be monkeypatched from pip._internal.exceptions import ( InstallationError, RequirementsFileParseError, @@ -272,7 +272,6 @@ def test_yield_editable_constraint(self): def test_nested_requirements_file(self, monkeypatch): line = '-r another_file' req = install_req_from_line('SomeProject') - import pip._internal.req.req_file def stub_parse_requirements(req_url, finder, comes_from, options, session, wheel_cache, constraint): @@ -285,7 +284,6 @@ def stub_parse_requirements(req_url, finder, comes_from, options, def test_nested_constraints_file(self, monkeypatch): line = '-c another_file' req = install_req_from_line('SomeProject') - import pip._internal.req.req_file def stub_parse_requirements(req_url, finder, comes_from, options, session, wheel_cache, constraint): From d766c448e1f8ce84ed757d7c49bcaa8dfcce8812 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 6 Oct 2019 22:20:47 +0530 Subject: [PATCH 0596/3170] Move index interaction code into pip._internal.index Why: To better group code that handles index interactions. Renames pip._internal.{collector -> index.collector} and pip._internal.{index -> index.package_finder} --- src/pip/_internal/index/__init__.py | 2 ++ src/pip/_internal/{ => index}/collector.py | 0 src/pip/_internal/{index.py => index/package_finder.py} | 0 3 files changed, 2 insertions(+) create mode 100644 src/pip/_internal/index/__init__.py rename src/pip/_internal/{ => index}/collector.py (100%) rename src/pip/_internal/{index.py => index/package_finder.py} (100%) diff --git a/src/pip/_internal/index/__init__.py b/src/pip/_internal/index/__init__.py new file mode 100644 index 00000000000..7a17b7b3b6a --- /dev/null +++ b/src/pip/_internal/index/__init__.py @@ -0,0 +1,2 @@ +"""Index interaction code +""" diff --git a/src/pip/_internal/collector.py b/src/pip/_internal/index/collector.py similarity index 100% rename from src/pip/_internal/collector.py rename to src/pip/_internal/index/collector.py diff --git a/src/pip/_internal/index.py b/src/pip/_internal/index/package_finder.py similarity index 100% rename from src/pip/_internal/index.py rename to src/pip/_internal/index/package_finder.py From 6ea84c6145c3795f1053e69dcb1a69e9bb3ffc40 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 6 Oct 2019 22:28:24 +0530 Subject: [PATCH 0597/3170] Update docs to refer to index as a sub-package --- .../development/architecture/package-finding.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/html/development/architecture/package-finding.rst b/docs/html/development/architecture/package-finding.rst index dfbf5e7ae88..d214c9b02b1 100644 --- a/docs/html/development/architecture/package-finding.rst +++ b/docs/html/development/architecture/package-finding.rst @@ -1,10 +1,10 @@ -Finding and choosing files (``index.py`` and ``PackageFinder``) +Finding and choosing files (``index`` and ``PackageFinder``) --------------------------------------------------------------- -The ``index.py`` module is a top-level module in pip responsible for deciding +The ``pip._internal.index`` sub-package in pip responsible for deciding what file to download and from where, given a requirement for a project. The -module's functionality is largely exposed through and coordinated by the -module's ``PackageFinder`` class. +package's functionality is largely exposed through and coordinated by the +package's ``PackageFinder`` class. .. _index-py-overview: @@ -133,8 +133,8 @@ method is the ``collect_links()`` method. The :ref:`PackageFinder <package-finder-class>` class invokes this method as the first step of its ``find_all_candidates()`` method. -The ``LinkCollector`` class is the only class in the ``index.py`` module that -makes network requests and is the only class in the module that depends +The ``LinkCollector`` class is the only class in the ``index`` sub-package that +makes network requests and is the only class in the sub-package that depends directly on ``PipSession``, which stores pip's configuration options and state for making requests. From 2db6f428bf3770209680ba06378c133aa0d6041f Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 6 Oct 2019 22:29:05 +0530 Subject: [PATCH 0598/3170] Update imports to {index -> index.package_finder} --- src/pip/_internal/build_env.py | 2 +- src/pip/_internal/cli/req_command.py | 2 +- src/pip/_internal/commands/list.py | 2 +- src/pip/_internal/legacy_resolve.py | 2 +- src/pip/_internal/operations/prepare.py | 2 +- src/pip/_internal/req/req_file.py | 2 +- src/pip/_internal/req/req_install.py | 2 +- src/pip/_internal/self_outdated_check.py | 2 +- tests/lib/__init__.py | 4 ++-- tests/unit/test_build_env.py | 2 +- tests/unit/test_finder.py | 2 +- tests/unit/test_index.py | 2 +- 12 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py index 5e6dc4602e0..f55f0e6b8d9 100644 --- a/src/pip/_internal/build_env.py +++ b/src/pip/_internal/build_env.py @@ -23,7 +23,7 @@ if MYPY_CHECK_RUNNING: from typing import Tuple, Set, Iterable, Optional, List - from pip._internal.index import PackageFinder + from pip._internal.index.package_finder import PackageFinder logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 203e86a49cc..88088b94894 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -14,7 +14,7 @@ from pip._internal.cli.base_command import Command from pip._internal.cli.command_context import CommandContextMixIn from pip._internal.exceptions import CommandError -from pip._internal.index import PackageFinder +from pip._internal.index.package_finder import PackageFinder from pip._internal.legacy_resolve import Resolver from pip._internal.models.selection_prefs import SelectionPreferences from pip._internal.network.session import PipSession diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index 77a245b6d2d..cce470a6051 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -12,7 +12,7 @@ from pip._internal.cli import cmdoptions from pip._internal.cli.req_command import IndexGroupCommand from pip._internal.exceptions import CommandError -from pip._internal.index import PackageFinder +from pip._internal.index.package_finder import PackageFinder from pip._internal.models.selection_prefs import SelectionPreferences from pip._internal.self_outdated_check import make_link_collector from pip._internal.utils.misc import ( diff --git a/src/pip/_internal/legacy_resolve.py b/src/pip/_internal/legacy_resolve.py index c24158f4d37..be67bdac1b7 100644 --- a/src/pip/_internal/legacy_resolve.py +++ b/src/pip/_internal/legacy_resolve.py @@ -46,7 +46,7 @@ from pip._internal.distributions import AbstractDistribution from pip._internal.network.session import PipSession - from pip._internal.index import PackageFinder + from pip._internal.index.package_finder import PackageFinder from pip._internal.operations.prepare import RequirementPreparer from pip._internal.req.req_install import InstallRequirement from pip._internal.req.req_set import RequirementSet diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 73a20e31999..9993f884186 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -61,7 +61,7 @@ from mypy_extensions import TypedDict from pip._internal.distributions import AbstractDistribution - from pip._internal.index import PackageFinder + from pip._internal.index.package_finder import PackageFinder from pip._internal.models.link import Link from pip._internal.req.req_install import InstallRequirement from pip._internal.req.req_tracker import RequirementTracker diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index da75ad62813..47486fa1fbc 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -36,7 +36,7 @@ ) from pip._internal.req import InstallRequirement from pip._internal.cache import WheelCache - from pip._internal.index import PackageFinder + from pip._internal.index.package_finder import PackageFinder from pip._internal.network.session import PipSession ReqFileLines = Iterator[Tuple[int, Text]] diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 763a2f54415..f3e5234d6ec 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -67,7 +67,7 @@ ) from pip._internal.build_env import BuildEnvironment from pip._internal.cache import WheelCache - from pip._internal.index import PackageFinder + from pip._internal.index.package_finder import PackageFinder from pip._vendor.pkg_resources import Distribution from pip._vendor.packaging.specifiers import SpecifierSet from pip._vendor.packaging.markers import Marker diff --git a/src/pip/_internal/self_outdated_check.py b/src/pip/_internal/self_outdated_check.py index 51ef3439ff1..c85fcc635fb 100644 --- a/src/pip/_internal/self_outdated_check.py +++ b/src/pip/_internal/self_outdated_check.py @@ -15,7 +15,7 @@ from pip._vendor.six import ensure_binary from pip._internal.collector import LinkCollector -from pip._internal.index import PackageFinder +from pip._internal.index.package_finder import PackageFinder from pip._internal.models.search_scope import SearchScope from pip._internal.models.selection_prefs import SelectionPreferences from pip._internal.utils.compat import WINDOWS diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index 32580739f36..3e3f7e44fc9 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -13,8 +13,8 @@ import pytest from scripttest import FoundDir, TestFileEnvironment -from pip._internal.collector import LinkCollector -from pip._internal.index import PackageFinder +from pip._internal.index.collector import LinkCollector +from pip._internal.index.package_finder import PackageFinder from pip._internal.locations import get_major_minor_version from pip._internal.models.search_scope import SearchScope from pip._internal.models.selection_prefs import SelectionPreferences diff --git a/tests/unit/test_build_env.py b/tests/unit/test_build_env.py index bcc241bbedf..372c83556c3 100644 --- a/tests/unit/test_build_env.py +++ b/tests/unit/test_build_env.py @@ -23,7 +23,7 @@ def run_with_build_env(script, setup_script_contents, from pip._internal.build_env import BuildEnvironment from pip._internal.collector import LinkCollector - from pip._internal.index import PackageFinder + from pip._internal.index.package_finder import PackageFinder from pip._internal.models.search_scope import SearchScope from pip._internal.models.selection_prefs import ( SelectionPreferences diff --git a/tests/unit/test_finder.py b/tests/unit/test_finder.py index 1295ff0b059..ac17cdf9d9a 100644 --- a/tests/unit/test_finder.py +++ b/tests/unit/test_finder.py @@ -12,7 +12,7 @@ BestVersionAlreadyInstalled, DistributionNotFound, ) -from pip._internal.index import ( +from pip._internal.index.package_finder import ( CandidateEvaluator, InstallationCandidate, Link, diff --git a/tests/unit/test_index.py b/tests/unit/test_index.py index 63eac97b99e..fc3fa0c15be 100644 --- a/tests/unit/test_index.py +++ b/tests/unit/test_index.py @@ -4,7 +4,7 @@ from pip._vendor.packaging.specifiers import SpecifierSet from pip._internal.collector import LinkCollector -from pip._internal.index import ( +from pip._internal.index.package_finder import ( CandidateEvaluator, CandidatePreferences, FormatControl, From 611fc6069b8fab73bcf7a0076b5423eae796f37e Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 6 Oct 2019 23:41:01 +0530 Subject: [PATCH 0599/3170] Update references to collector.py --- src/pip/_internal/index/package_finder.py | 2 +- src/pip/_internal/models/link.py | 2 +- src/pip/_internal/self_outdated_check.py | 2 +- tests/unit/test_build_env.py | 2 +- tests/unit/test_collector.py | 11 ++++++----- tests/unit/test_finder.py | 5 ++++- tests/unit/test_index.py | 2 +- 7 files changed, 15 insertions(+), 11 deletions(-) diff --git a/src/pip/_internal/index/package_finder.py b/src/pip/_internal/index/package_finder.py index 897444aae3f..9b338e693be 100644 --- a/src/pip/_internal/index/package_finder.py +++ b/src/pip/_internal/index/package_finder.py @@ -38,7 +38,7 @@ FrozenSet, Iterable, List, Optional, Set, Text, Tuple, Union, ) from pip._vendor.packaging.version import _BaseVersion - from pip._internal.collector import LinkCollector + from pip._internal.index.collector import LinkCollector from pip._internal.models.search_scope import SearchScope from pip._internal.req import InstallRequirement from pip._internal.pep425tags import Pep425Tag diff --git a/src/pip/_internal/models/link.py b/src/pip/_internal/models/link.py index 2d50d17989f..ff808f5e9e2 100644 --- a/src/pip/_internal/models/link.py +++ b/src/pip/_internal/models/link.py @@ -19,7 +19,7 @@ if MYPY_CHECK_RUNNING: from typing import Optional, Text, Tuple, Union - from pip._internal.collector import HTMLPage + from pip._internal.index.collector import HTMLPage from pip._internal.utils.hashes import Hashes diff --git a/src/pip/_internal/self_outdated_check.py b/src/pip/_internal/self_outdated_check.py index c85fcc635fb..38f1d815f39 100644 --- a/src/pip/_internal/self_outdated_check.py +++ b/src/pip/_internal/self_outdated_check.py @@ -14,7 +14,7 @@ from pip._vendor.packaging import version as packaging_version from pip._vendor.six import ensure_binary -from pip._internal.collector import LinkCollector +from pip._internal.index.collector import LinkCollector from pip._internal.index.package_finder import PackageFinder from pip._internal.models.search_scope import SearchScope from pip._internal.models.selection_prefs import SelectionPreferences diff --git a/tests/unit/test_build_env.py b/tests/unit/test_build_env.py index 372c83556c3..9db08a124dd 100644 --- a/tests/unit/test_build_env.py +++ b/tests/unit/test_build_env.py @@ -22,7 +22,7 @@ def run_with_build_env(script, setup_script_contents, import sys from pip._internal.build_env import BuildEnvironment - from pip._internal.collector import LinkCollector + from pip._internal.index.collector import LinkCollector from pip._internal.index.package_finder import PackageFinder from pip._internal.models.search_scope import SearchScope from pip._internal.models.selection_prefs import ( diff --git a/tests/unit/test_collector.py b/tests/unit/test_collector.py index f602c956a04..cf709c99bcf 100644 --- a/tests/unit/test_collector.py +++ b/tests/unit/test_collector.py @@ -9,7 +9,7 @@ from pip._vendor import html5lib, requests from pip._vendor.six.moves.urllib import request as urllib_request -from pip._internal.collector import ( +from pip._internal.index.collector import ( HTMLPage, _clean_link, _determine_base_url, @@ -334,7 +334,7 @@ def test_get_html_page_invalid_scheme(caplog, url, vcs_scheme): assert page is None assert caplog.record_tuples == [ ( - "pip._internal.collector", + "pip._internal.index.collector", logging.DEBUG, "Cannot look at {} URL {}".format(vcs_scheme, url), ), @@ -367,7 +367,8 @@ def test_get_html_page_directory_append_index(tmpdir): session = mock.Mock(PipSession) fake_response = make_fake_html_response(expected_url) - with mock.patch("pip._internal.collector._get_html_response") as mock_func: + mock_func = mock.patch("pip._internal.index.collector._get_html_response") + with mock_func as mock_func: mock_func.return_value = fake_response actual = _get_html_page(Link(dir_url), session=session) assert mock_func.mock_calls == [ @@ -434,7 +435,7 @@ def check_links_include(links, names): class TestLinkCollector(object): - @patch('pip._internal.collector._get_html_response') + @patch('pip._internal.index.collector._get_html_response') def test_collect_links(self, mock_get_html_response, caplog, data): caplog.set_level(logging.DEBUG) @@ -474,5 +475,5 @@ def test_collect_links(self, mock_get_html_response, caplog, data): 1 location(s) to search for versions of twine: * https://pypi.org/simple/twine/""") assert caplog.record_tuples == [ - ('pip._internal.collector', logging.DEBUG, expected_message), + ('pip._internal.index.collector', logging.DEBUG, expected_message), ] diff --git a/tests/unit/test_finder.py b/tests/unit/test_finder.py index ac17cdf9d9a..e7bf13e02bb 100644 --- a/tests/unit/test_finder.py +++ b/tests/unit/test_finder.py @@ -62,7 +62,10 @@ def test_no_partial_name_match(data): def test_tilde(): """Finder can accept a path with ~ in it and will normalize it.""" - with patch('pip._internal.collector.os.path.exists', return_value=True): + patched_exists = patch( + 'pip._internal.index.collector.os.path.exists', return_value=True + ) + with patched_exists: finder = make_test_finder(find_links=['~/python-pkgs']) req = install_req_from_line("gmpy") with pytest.raises(DistributionNotFound): diff --git a/tests/unit/test_index.py b/tests/unit/test_index.py index fc3fa0c15be..bc86398eecf 100644 --- a/tests/unit/test_index.py +++ b/tests/unit/test_index.py @@ -3,7 +3,7 @@ import pytest from pip._vendor.packaging.specifiers import SpecifierSet -from pip._internal.collector import LinkCollector +from pip._internal.index.collector import LinkCollector from pip._internal.index.package_finder import ( CandidateEvaluator, CandidatePreferences, From 3846da7f713b32a698e007d4abe3e12f53998064 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 8 Oct 2019 08:22:00 +0530 Subject: [PATCH 0600/3170] Update docs/html/development/architecture/package-finding.rst Co-Authored-By: Ellen Marie Dash <the@smallest.dog> --- docs/html/development/architecture/package-finding.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/html/development/architecture/package-finding.rst b/docs/html/development/architecture/package-finding.rst index d214c9b02b1..153b917af09 100644 --- a/docs/html/development/architecture/package-finding.rst +++ b/docs/html/development/architecture/package-finding.rst @@ -1,7 +1,7 @@ Finding and choosing files (``index`` and ``PackageFinder``) --------------------------------------------------------------- -The ``pip._internal.index`` sub-package in pip responsible for deciding +The ``pip._internal.index`` sub-package in pip is responsible for deciding what file to download and from where, given a requirement for a project. The package's functionality is largely exposed through and coordinated by the package's ``PackageFinder`` class. From 2399a26cb16f1d75b8a904674f1289e44e14cb8f Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sat, 19 Oct 2019 22:29:19 +0530 Subject: [PATCH 0601/3170] Remove remaining references to index.py --- docs/html/development/architecture/package-finding.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/html/development/architecture/package-finding.rst b/docs/html/development/architecture/package-finding.rst index 153b917af09..221af940597 100644 --- a/docs/html/development/architecture/package-finding.rst +++ b/docs/html/development/architecture/package-finding.rst @@ -38,7 +38,7 @@ file to download for a package, given a requirement: <candidate-evaluator-class>` class). The remainder of this section is organized by documenting some of the -classes inside ``index.py``, in the following order: +classes inside the ``index`` package, in the following order: * the main :ref:`PackageFinder <package-finder-class>` class, * the :ref:`LinkCollector <link-collector-class>` class, @@ -54,7 +54,7 @@ The ``PackageFinder`` class *************************** The ``PackageFinder`` class is the primary way through which code in pip -interacts with ``index.py``. It is an umbrella class that encapsulates and +interacts with ``index`` package. It is an umbrella class that encapsulates and groups together various package-finding functionality. The ``PackageFinder`` class is responsible for searching the network and file From 6d96b85388a9bcc3c087abf5b6f8a7c943cced9b Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sat, 19 Oct 2019 22:31:45 +0530 Subject: [PATCH 0602/3170] Update the URL fragment to not include "py" I'm making this change since it's unlikely that folks would've linked to this specific section, and if they did, this isn't exactly user facing documentation, so I wouldn't hold this to the same standard. --- docs/html/development/architecture/package-finding.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/html/development/architecture/package-finding.rst b/docs/html/development/architecture/package-finding.rst index 221af940597..53ece59f8d9 100644 --- a/docs/html/development/architecture/package-finding.rst +++ b/docs/html/development/architecture/package-finding.rst @@ -7,7 +7,7 @@ package's functionality is largely exposed through and coordinated by the package's ``PackageFinder`` class. -.. _index-py-overview: +.. _index-overview: Overview ******** @@ -89,7 +89,7 @@ case, the ``PackageFinder`` instance is created by the ``self_outdated_check.py`` module's ``pip_self_version_check()`` function. The ``PackageFinder`` class is responsible for doing all of the things listed -in the :ref:`Overview <index-py-overview>` section like fetching and parsing +in the :ref:`Overview <index-overview>` section like fetching and parsing `PEP 503`_ simple repository HTML pages, evaluating which links in the simple repository pages are relevant for each requirement, and further filtering and sorting by preference the candidates for install coming from the relevant @@ -105,7 +105,7 @@ One of ``PackageFinder``'s main top-level methods is :ref:`LinkEvaluator <link-evaluator-class>` object to filter out some of those links, and then returns a list of ``InstallationCandidates`` (aka candidates for install). This corresponds to steps 1-3 of the - :ref:`Overview <index-py-overview>` above. + :ref:`Overview <index-overview>` above. 2. Constructs a ``CandidateEvaluator`` object and uses that to determine the best candidate. It does this by calling the ``CandidateEvaluator`` class's ``compute_best_candidate()`` method on the return value of From e21e28f4ebfefac5f7e5cb7c99e4301a304895a1 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sat, 19 Oct 2019 23:07:17 +0530 Subject: [PATCH 0603/3170] Move prepare_pep517_metadata to generate_metadata Why: So that InstallRequirement is no longer responsible for this task. --- .../_internal/operations/generate_metadata.py | 21 ++++++++++++++++- src/pip/_internal/req/req_install.py | 23 ------------------- 2 files changed, 20 insertions(+), 24 deletions(-) diff --git a/src/pip/_internal/operations/generate_metadata.py b/src/pip/_internal/operations/generate_metadata.py index d0bb7c8e940..5772dd83d69 100644 --- a/src/pip/_internal/operations/generate_metadata.py +++ b/src/pip/_internal/operations/generate_metadata.py @@ -134,4 +134,23 @@ def _generate_metadata_legacy(install_req): def _generate_metadata(install_req): # type: (InstallRequirement) -> str - return install_req.prepare_pep517_metadata() + assert install_req.pep517_backend is not None + + # NOTE: This needs to be refactored to stop using atexit + metadata_tmpdir = TempDirectory(kind="modern-metadata") + atexit.register(metadata_tmpdir.cleanup) + + metadata_dir = metadata_tmpdir.path + + with install_req.build_env: + # Note that Pep517HookCaller implements a fallback for + # prepare_metadata_for_build_wheel, so we don't have to + # consider the possibility that this hook doesn't exist. + runner = runner_with_spinner_message("Preparing wheel metadata") + backend = install_req.pep517_backend + with backend.subprocess_runner(runner): + distinfo_dir = backend.prepare_metadata_for_build_wheel( + metadata_dir + ) + + return os.path.join(metadata_dir, distinfo_dir) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 763a2f54415..97d48a69ba1 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -615,29 +615,6 @@ def prepare_metadata(self): ) self.req = Requirement(metadata_name) - def prepare_pep517_metadata(self): - # type: () -> str - assert self.pep517_backend is not None - - # NOTE: This needs to be refactored to stop using atexit - metadata_tmpdir = TempDirectory(kind="modern-metadata") - atexit.register(metadata_tmpdir.cleanup) - - metadata_dir = metadata_tmpdir.path - - with self.build_env: - # Note that Pep517HookCaller implements a fallback for - # prepare_metadata_for_build_wheel, so we don't have to - # consider the possibility that this hook doesn't exist. - runner = runner_with_spinner_message("Preparing wheel metadata") - backend = self.pep517_backend - with backend.subprocess_runner(runner): - distinfo_dir = backend.prepare_metadata_for_build_wheel( - metadata_dir - ) - - return os.path.join(metadata_dir, distinfo_dir) - @property def metadata(self): # type: () -> Any From e6875ec7cc52a010f668e9b9fa0a00d557bb1702 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sat, 19 Oct 2019 23:10:18 +0530 Subject: [PATCH 0604/3170] refactor: Unpack from install_req eagerly --- src/pip/_internal/operations/generate_metadata.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/operations/generate_metadata.py b/src/pip/_internal/operations/generate_metadata.py index 5772dd83d69..2d2b01ae651 100644 --- a/src/pip/_internal/operations/generate_metadata.py +++ b/src/pip/_internal/operations/generate_metadata.py @@ -135,6 +135,8 @@ def _generate_metadata_legacy(install_req): def _generate_metadata(install_req): # type: (InstallRequirement) -> str assert install_req.pep517_backend is not None + build_env = install_req.build_env + backend = install_req.pep517_backend # NOTE: This needs to be refactored to stop using atexit metadata_tmpdir = TempDirectory(kind="modern-metadata") @@ -142,12 +144,11 @@ def _generate_metadata(install_req): metadata_dir = metadata_tmpdir.path - with install_req.build_env: + with build_env: # Note that Pep517HookCaller implements a fallback for # prepare_metadata_for_build_wheel, so we don't have to # consider the possibility that this hook doesn't exist. runner = runner_with_spinner_message("Preparing wheel metadata") - backend = install_req.pep517_backend with backend.subprocess_runner(runner): distinfo_dir = backend.prepare_metadata_for_build_wheel( metadata_dir From 1875573dafdaec7a7a18071c06f8f514d2f5eabf Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 20 Oct 2019 00:12:37 +0530 Subject: [PATCH 0605/3170] Ah the imports. --- src/pip/_internal/operations/generate_metadata.py | 7 ++++++- src/pip/_internal/req/req_install.py | 1 - 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/operations/generate_metadata.py b/src/pip/_internal/operations/generate_metadata.py index 2d2b01ae651..04e5ace1236 100644 --- a/src/pip/_internal/operations/generate_metadata.py +++ b/src/pip/_internal/operations/generate_metadata.py @@ -1,13 +1,18 @@ """Metadata generation logic for source distributions. """ +import atexit import logging import os from pip._internal.exceptions import InstallationError from pip._internal.utils.misc import ensure_dir from pip._internal.utils.setuptools_build import make_setuptools_egg_info_args -from pip._internal.utils.subprocess import call_subprocess +from pip._internal.utils.subprocess import ( + call_subprocess, + runner_with_spinner_message, +) +from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.vcs import vcs diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 97d48a69ba1..79fe1716217 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -4,7 +4,6 @@ from __future__ import absolute_import -import atexit import logging import os import shutil From cb8564c6e142cf7446cf853176c6cb35cb83009c Mon Sep 17 00:00:00 2001 From: Xavier Fernandez <xavier.fernandez@polyconseil.fr> Date: Sat, 19 Oct 2019 22:52:45 +0200 Subject: [PATCH 0606/3170] docs: add notes regarding ``get-pip.py`` during the release --- docs/html/development/release-process.rst | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/html/development/release-process.rst b/docs/html/development/release-process.rst index fc73657bddc..b03b042f7b6 100644 --- a/docs/html/development/release-process.rst +++ b/docs/html/development/release-process.rst @@ -107,6 +107,23 @@ Creating a new release Steps 3 to 6 are automated in ``nox -s release -- YY.N`` command. +.. note:: + + If the release dropped the support of an obsolete Python version ``M.m``, + a new ``M.m/get-pip.py`` needs to be published: update the ``all`` task from + ``tasks/generate.py`` in `get-pip repository`_ and make a pull request to + `psf-salt repository`_ to add the new ``get-pip.py`` (and its directory) to + ``salt/pypa/bootstrap/init.sls``. + + +.. note:: + If the ``get-pip.py`` script needs to be updated due to changes in pip internals + and if the last ``M.m/get-pip.py`` published still uses the default template, make + sure to first duplicate ``templates/default.py`` as ``templates/pre-YY.N.py`` + before updating it and specify in ``tasks/generate.py`` that ``M.m/get-pip.py`` + now needs to use ``templates/pre-YY.N.py``. + + Creating a bug-fix release -------------------------- @@ -125,4 +142,5 @@ order to create one of these the changes should already be merged into the the above release process starting with step 4. .. _`get-pip repository`: https://github.com/pypa/get-pip +.. _`psf-salt repository`: https://github.com/python/psf-salt .. _`CPython`: https://github.com/pypa/cpython From 7c218c9a800da9a121f1ce9bebbe8eb8f14d78ee Mon Sep 17 00:00:00 2001 From: Xavier Fernandez <xavier.fernandez@polyconseil.fr> Date: Sat, 19 Oct 2019 22:53:29 +0200 Subject: [PATCH 0607/3170] Update the patch release process --- docs/html/development/release-process.rst | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/docs/html/development/release-process.rst b/docs/html/development/release-process.rst index b03b042f7b6..63f4c5e461b 100644 --- a/docs/html/development/release-process.rst +++ b/docs/html/development/release-process.rst @@ -134,8 +134,11 @@ order to create one of these the changes should already be merged into the #. Create a new ``release/YY.N.Z+1`` branch off of the ``YY.N`` tag using the command ``git checkout -b release/YY.N.Z+1 YY.N``. #. Cherry pick the fixed commits off of the ``master`` branch, fixing any - conflicts and moving any changelog entries from the development version's - changelog section to the ``YY.N.Z+1`` section. + conflicts. +#. Follow the steps 3 to 6 from above (or call ``nox -s release -- YY.N.Z+1``) +#. Merge master into your release branch and drop the news files that have been + included in your release (otherwise they would also appear in the ``YY.N+1`` + changelog) #. Push the ``release/YY.N.Z+1`` branch to github and submit a PR for it against the ``master`` branch and wait for the tests to run. #. Once tests run, merge the ``release/YY.N.Z+1`` branch into master, and follow From bf032e36bb23a63bd28bbc5446c490fba0332613 Mon Sep 17 00:00:00 2001 From: Aniruddha Basak <codewithaniruddha@gmail.com> Date: Sun, 20 Oct 2019 09:36:55 +0530 Subject: [PATCH 0608/3170] Change the 7201.doc file --- news/7201.doc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/7201.doc b/news/7201.doc index 839259e362c..95df888c48a 100644 --- a/news/7201.doc +++ b/news/7201.doc @@ -1 +1 @@ -Describe basic authentication credentials works in urls. +Describe how basic authentication credentials in URLs work. From 618e97520d7517e28787179df783d6ed72321795 Mon Sep 17 00:00:00 2001 From: Jacob Kim <me@thejacobkim.com> Date: Sun, 20 Oct 2019 22:14:50 +0900 Subject: [PATCH 0609/3170] Respect docstring conventions (#7230) Respect PEP 257 -- Docstring Conventions in wheel.py --- news/7230.trivial | 1 + src/pip/_internal/wheel.py | 53 +++++++++++++------------------------- 2 files changed, 19 insertions(+), 35 deletions(-) create mode 100644 news/7230.trivial diff --git a/news/7230.trivial b/news/7230.trivial new file mode 100644 index 00000000000..2e63419b14c --- /dev/null +++ b/news/7230.trivial @@ -0,0 +1 @@ +Change ``pip._internal.wheel`` to respect docstring conventions. diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index d45cb927cc9..72168e5d831 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -1,5 +1,4 @@ -""" -Support for installing and building the "wheel" binary package format. +"""Support for installing and building the "wheel" binary package format. """ # The following comment should be removed at some point in the future. @@ -118,8 +117,7 @@ def open_for_csv(name, mode): def replace_python_tag(wheelname, new_tag): # type: (str, str) -> str - """Replace the Python tag in a wheel file name with a new value. - """ + """Replace the Python tag in a wheel file name with a new value.""" parts = wheelname.split('-') parts[-3] = new_tag return '-'.join(parts) @@ -128,7 +126,8 @@ def replace_python_tag(wheelname, new_tag): def fix_script(path): # type: (str) -> Optional[bool] """Replace #!python with #!/path/to/python - Return True if file was changed.""" + Return True if file was changed. + """ # XXX RECORD hashes will need to be updated if os.path.isfile(path): with open(path, 'rb') as script: @@ -151,9 +150,7 @@ def fix_script(path): def root_is_purelib(name, wheeldir): # type: (str, str) -> bool - """ - Return True if the extracted wheel in wheeldir should go into purelib. - """ + """True if the extracted wheel in wheeldir should go into purelib.""" name_folded = name.replace("-", "_") for item in os.listdir(wheeldir): match = dist_info_re.match(item) @@ -188,8 +185,9 @@ def get_entrypoints(filename): gui = entry_points.get('gui_scripts', {}) def _split_ep(s): - """get the string representation of EntryPoint, remove space and split - on '='""" + """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 @@ -201,7 +199,6 @@ def _split_ep(s): def message_about_scripts_not_on_PATH(scripts): # type: (Sequence[str]) -> Optional[str] """Determine if any scripts are not on PATH and format a warning. - Returns a warning message if one or more scripts are not on PATH, otherwise None. """ @@ -261,8 +258,7 @@ def message_about_scripts_not_on_PATH(scripts): def sorted_outrows(outrows): # type: (Iterable[InstalledCSVRow]) -> List[InstalledCSVRow] - """ - Return the given rows of a RECORD file in sorted order. + """Return the given rows of a RECORD file in sorted order. Each row is a 3-tuple (path, hash, size) and corresponds to a record of a RECORD file (see PEP 376 and PEP 427 for details). For the rows @@ -644,9 +640,7 @@ def is_entrypoint_wrapper(name): def wheel_version(source_dir): # type: (Optional[str]) -> Optional[Tuple[int, ...]] - """ - Return the Wheel-Version of an extracted wheel, if possible. - + """Return the Wheel-Version of an extracted wheel, if possible. Otherwise, return None if we couldn't parse / extract it. """ try: @@ -664,8 +658,7 @@ def wheel_version(source_dir): def check_compatibility(version, name): # type: (Optional[Tuple[int, ...]], str) -> None - """ - Raises errors or warns if called with an incompatible Wheel-Version. + """Raises errors or warns if called with an incompatible Wheel-Version. Pip should refuse to install a Wheel-Version that's a major series ahead of what it's compatible with (e.g 2.0 > 1.1); and warn when @@ -694,8 +687,7 @@ def check_compatibility(version, name): def format_tag(file_tag): # type: (Tuple[str, ...]) -> str - """ - Format three tags in the form "<python_tag>-<abi_tag>-<platform_tag>". + """Format three tags in the form "<python_tag>-<abi_tag>-<platform_tag>". :param file_tag: A 3-tuple of tags (python_tag, abi_tag, platform_tag). """ @@ -743,15 +735,12 @@ def __init__(self, filename): def get_formatted_file_tags(self): # type: () -> List[str] - """ - Return the wheel's tags as a sorted list of strings. - """ + """Return the wheel's tags as a sorted list of strings.""" return sorted(format_tag(tag) for tag in self.file_tags) def support_index_min(self, tags): # type: (List[Pep425Tag]) -> int - """ - Return the lowest index that one of the wheel's file_tag combinations + """Return the lowest index that one of the wheel's file_tag combinations achieves in the given list of supported tags. For example, if there are 8 supported tags and one of the file tags @@ -767,8 +756,7 @@ def support_index_min(self, tags): def supported(self, tags): # type: (List[Pep425Tag]) -> bool - """ - Return whether the wheel is compatible with one of the given tags. + """Return whether the wheel is compatible with one of the given tags. :param tags: the PEP 425 tags to check the wheel against. """ @@ -791,8 +779,7 @@ def should_use_ephemeral_cache( check_binary_allowed, # type: BinaryAllowedPredicate ): # type: (...) -> Optional[bool] - """ - Return whether to build an InstallRequirement object using the + """Return whether to build an InstallRequirement object using the ephemeral cache. :param cache_available: whether a cache directory is available for the @@ -847,9 +834,7 @@ def format_command_result( command_output, # type: str ): # type: (...) -> str - """ - Format command information for logging. - """ + """Format command information for logging.""" command_desc = format_command_args(command_args) text = 'Command arguments: {}\n'.format(command_desc) @@ -873,9 +858,7 @@ def get_legacy_build_wheel_path( command_output, # type: str ): # type: (...) -> Optional[str] - """ - Return the path to the wheel in the temporary build directory. - """ + """Return the path to the wheel in the temporary build directory.""" # Sort for determinism. names = sorted(names) if not names: From 6645530952e50ed13ac51fc012ee420cff64dfaa Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 20 Oct 2019 12:03:37 -0400 Subject: [PATCH 0610/3170] Do not create line-specific parsers for requirements files This clears the way and for us to create our parser outside the function next. --- src/pip/_internal/req/req_file.py | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index 47486fa1fbc..4397538aa14 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -167,7 +167,7 @@ def process_line( :param constraint: If True, parsing a constraints file. :param options: OptionParser options that we may update """ - parser = build_parser(line) + parser = build_parser() defaults = parser.get_default_values() defaults.index_url = None if finder: @@ -177,9 +177,14 @@ def process_line( if sys.version_info < (2, 7, 3): # https://github.com/python/mypy/issues/1174 options_str = options_str.encode('utf8') # type: ignore - # https://github.com/python/mypy/issues/1174 - opts, _ = parser.parse_args( - shlex.split(options_str), defaults) # type: ignore + try: + # https://github.com/python/mypy/issues/1174 + opts, _ = parser.parse_args( + shlex.split(options_str), defaults) # type: ignore + except OptionParsingError as e: + # add offending line + msg = 'Invalid requirement: %s\n%s' % (line, e.msg) + raise RequirementsFileParseError(msg) # preserve for the nested code path line_comes_from = '%s %s (line %s)' % ( @@ -297,8 +302,14 @@ def break_args_options(line): return ' '.join(args), ' '.join(options) # type: ignore -def build_parser(line): - # type: (Text) -> optparse.OptionParser +class OptionParsingError(Exception): + def __init__(self, msg): + # type: (str) -> None + self.msg = msg + + +def build_parser(): + # type: () -> optparse.OptionParser """ Return a parser for parsing requirement lines """ @@ -313,9 +324,7 @@ def build_parser(line): # that in our own exception. def parser_exit(self, msg): # type: (Any, str) -> NoReturn - # add offending line - msg = 'Invalid requirement: %s\n%s' % (line, msg) - raise RequirementsFileParseError(msg) + raise OptionParsingError(msg) # NOTE: mypy disallows assigning to a method # https://github.com/python/mypy/issues/2427 parser.exit = parser_exit # type: ignore From f0b20f19aed51c451c529c7ec354f4f3ed0c0fc0 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 20 Oct 2019 12:24:24 -0400 Subject: [PATCH 0611/3170] Hide line parsing details behind a line parser Simplifies reading the code that actually processes the line. --- src/pip/_internal/req/req_file.py | 45 ++++++++++++++++++++++--------- 1 file changed, 32 insertions(+), 13 deletions(-) diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index 4397538aa14..79fd883ab33 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -31,9 +31,11 @@ from pip._internal.utils.urls import get_url_scheme if MYPY_CHECK_RUNNING: + from optparse import Values from typing import ( Any, Callable, Iterator, List, NoReturn, Optional, Text, Tuple, ) + from pip._internal.req import InstallRequirement from pip._internal.cache import WheelCache from pip._internal.index.package_finder import PackageFinder @@ -41,6 +43,9 @@ ReqFileLines = Iterator[Tuple[int, Text]] + LineParser = Callable[[Text], Tuple[str, Values]] + + __all__ = ['parse_requirements'] SCHEME_RE = re.compile(r'^(http|https|file):', re.I) @@ -167,20 +172,9 @@ def process_line( :param constraint: If True, parsing a constraints file. :param options: OptionParser options that we may update """ - parser = build_parser() - defaults = parser.get_default_values() - defaults.index_url = None - if finder: - defaults.format_control = finder.format_control - args_str, options_str = break_args_options(line) - # Prior to 2.7.3, shlex cannot deal with unicode entries - if sys.version_info < (2, 7, 3): - # https://github.com/python/mypy/issues/1174 - options_str = options_str.encode('utf8') # type: ignore + line_parser = get_line_parser(finder) try: - # https://github.com/python/mypy/issues/1174 - opts, _ = parser.parse_args( - shlex.split(options_str), defaults) # type: ignore + args_str, opts = line_parser(line) except OptionParsingError as e: # add offending line msg = 'Invalid requirement: %s\n%s' % (line, e.msg) @@ -284,6 +278,31 @@ def process_line( session.add_trusted_host(host, source=source) +def get_line_parser(finder): + # type: (Optional[PackageFinder]) -> LineParser + parser = build_parser() + defaults = parser.get_default_values() + defaults.index_url = None + if finder: + defaults.format_control = finder.format_control + + def parse_line(line): + # type: (Text) -> Tuple[str, Values] + args_str, options_str = break_args_options(line) + # Prior to 2.7.3, shlex cannot deal with unicode entries + if sys.version_info < (2, 7, 3): + # https://github.com/python/mypy/issues/1174 + options_str = options_str.encode('utf8') # type: ignore + + # https://github.com/python/mypy/issues/1174 + opts, _ = parser.parse_args( + shlex.split(options_str), defaults) # type: ignore + + return args_str, opts + + return parse_line + + def break_args_options(line): # type: (Text) -> Tuple[str, Text] """Break up the line into an args and options string. We only want to shlex From 4d7fc272b42a9be9fac176113342788024d20189 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 20 Oct 2019 12:45:10 -0400 Subject: [PATCH 0612/3170] Remove no-action TODO comes_from is only used in get_file_content, which expects to see a URL or path, so there is no need to decorate it. --- src/pip/_internal/req/req_file.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index 79fd883ab33..2b57bd07f2e 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -232,7 +232,6 @@ def process_line( elif not SCHEME_RE.search(req_path): # do a join so relative paths work req_path = os.path.join(os.path.dirname(filename), req_path) - # TODO: Why not use `comes_from='-r {} (line {})'` here as well? parsed_reqs = parse_requirements( req_path, finder, comes_from, options, session, constraint=nested_constraint, wheel_cache=wheel_cache From c8307614f23ddc160e812ab76fa492c9432a5db6 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 20 Oct 2019 12:50:07 -0400 Subject: [PATCH 0613/3170] Do requirement file recursion first This change makes factoring out the parsing more obvious. --- src/pip/_internal/req/req_file.py | 51 +++++++++++++++++-------------- 1 file changed, 28 insertions(+), 23 deletions(-) diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index 2b57bd07f2e..f2a69c280ff 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -180,6 +180,34 @@ def process_line( msg = 'Invalid requirement: %s\n%s' % (line, e.msg) raise RequirementsFileParseError(msg) + # parse a nested requirements file + if ( + not args_str and + not opts.editable and + (opts.requirements or opts.constraints) + ): + if opts.requirements: + req_path = opts.requirements[0] + nested_constraint = False + else: + req_path = opts.constraints[0] + nested_constraint = True + # original file is over http + if SCHEME_RE.search(filename): + # do a url join so relative paths work + req_path = urllib_parse.urljoin(filename, req_path) + # original file and nested file are paths + elif not SCHEME_RE.search(req_path): + # do a join so relative paths work + req_path = os.path.join(os.path.dirname(filename), req_path) + parsed_reqs = parse_requirements( + req_path, finder, comes_from, options, session, + constraint=nested_constraint, wheel_cache=wheel_cache + ) + for req in parsed_reqs: + yield req + return + # preserve for the nested code path line_comes_from = '%s %s (line %s)' % ( '-c' if constraint else '-r', filename, line_number, @@ -216,29 +244,6 @@ def process_line( constraint=constraint, isolated=isolated, wheel_cache=wheel_cache ) - # parse a nested requirements file - elif opts.requirements or opts.constraints: - if opts.requirements: - req_path = opts.requirements[0] - nested_constraint = False - else: - req_path = opts.constraints[0] - nested_constraint = True - # original file is over http - if SCHEME_RE.search(filename): - # do a url join so relative paths work - req_path = urllib_parse.urljoin(filename, req_path) - # original file and nested file are paths - elif not SCHEME_RE.search(req_path): - # do a join so relative paths work - req_path = os.path.join(os.path.dirname(filename), req_path) - parsed_reqs = parse_requirements( - req_path, finder, comes_from, options, session, - constraint=nested_constraint, wheel_cache=wheel_cache - ) - for req in parsed_reqs: - yield req - # percolate hash-checking option upward elif opts.require_hashes: options.require_hashes = opts.require_hashes From a5d53eab0a2374de8730d51109efad88ee2b3d5a Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 20 Oct 2019 13:16:42 -0400 Subject: [PATCH 0614/3170] Simplify skip_requirements_regex option handling Decouples `process_lines` from our CLI options. --- src/pip/_internal/req/req_file.py | 26 ++++++++++++++------------ tests/unit/test_req_file.py | 25 ++++++++++--------------- 2 files changed, 24 insertions(+), 27 deletions(-) diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index f2a69c280ff..bbc12555a96 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -117,7 +117,10 @@ def parse_requirements( filename, comes_from=comes_from, session=session ) - lines_enum = preprocess(content, options) + skip_requirements_regex = ( + options.skip_requirements_regex if options else None + ) + lines_enum = preprocess(content, skip_requirements_regex) for line_number, line in lines_enum: req_iter = process_line(line, filename, line_number, finder, @@ -127,8 +130,8 @@ def parse_requirements( yield req -def preprocess(content, options): - # type: (Text, Optional[optparse.Values]) -> ReqFileLines +def preprocess(content, skip_requirements_regex): + # type: (Text, Optional[str]) -> ReqFileLines """Split, filter, and join lines, and return a line iterator :param content: the content of the requirements file @@ -137,7 +140,8 @@ def preprocess(content, options): lines_enum = enumerate(content.splitlines(), start=1) # type: ReqFileLines lines_enum = join_lines(lines_enum) lines_enum = ignore_comments(lines_enum) - lines_enum = skip_regex(lines_enum, options) + if skip_requirements_regex: + lines_enum = skip_regex(lines_enum, skip_requirements_regex) lines_enum = expand_env_variables(lines_enum) return lines_enum @@ -183,7 +187,7 @@ def process_line( # parse a nested requirements file if ( not args_str and - not opts.editable and + not opts.editables and (opts.requirements or opts.constraints) ): if opts.requirements: @@ -397,17 +401,15 @@ def ignore_comments(lines_enum): yield line_number, line -def skip_regex(lines_enum, options): - # type: (ReqFileLines, Optional[optparse.Values]) -> ReqFileLines +def skip_regex(lines_enum, pattern): + # type: (ReqFileLines, str) -> ReqFileLines """ - Skip lines that match '--skip-requirements-regex' pattern + Skip lines that match the provided pattern Note: the regex pattern is only built once """ - skip_regex = options.skip_requirements_regex if options else None - if skip_regex: - pattern = re.compile(skip_regex) - lines_enum = filterfalse(lambda e: pattern.search(e[1]), lines_enum) + matcher = re.compile(pattern) + lines_enum = filterfalse(lambda e: matcher.search(e[1]), lines_enum) return lines_enum diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py index ca86a191b29..29145680f62 100644 --- a/tests/unit/test_req_file.py +++ b/tests/unit/test_req_file.py @@ -83,8 +83,8 @@ def test_skip_regex_after_joining_case1(self, options): ern line2 """) - options.skip_requirements_regex = 'pattern' - result = preprocess(content, options) + skip_requirements_regex = 'pattern' + result = preprocess(content, skip_requirements_regex) assert list(result) == [(3, 'line2')] def test_skip_regex_after_joining_case2(self, options): @@ -93,8 +93,8 @@ def test_skip_regex_after_joining_case2(self, options): line2 line3 """) - options.skip_requirements_regex = 'pattern' - result = preprocess(content, options) + skip_requirements_regex = 'pattern' + result = preprocess(content, skip_requirements_regex) assert list(result) == [(3, 'line3')] @@ -154,24 +154,19 @@ class TestSkipRegex(object): """tests for `skip_reqex``""" def test_skip_regex_pattern_match(self): - options = stub(skip_requirements_regex='.*Bad.*') + pattern = '.*Bad.*' line = '--extra-index-url Bad' - assert [] == list(skip_regex(enumerate([line]), options)) + assert [] == list(skip_regex(enumerate([line]), pattern)) def test_skip_regex_pattern_not_match(self): - options = stub(skip_requirements_regex='.*Bad.*') + pattern = '.*Bad.*' line = '--extra-index-url Good' - assert [(0, line)] == list(skip_regex(enumerate([line]), options)) + assert [(0, line)] == list(skip_regex(enumerate([line]), pattern)) def test_skip_regex_no_options(self): - options = None + pattern = None line = '--extra-index-url Good' - assert [(0, line)] == list(skip_regex(enumerate([line]), options)) - - def test_skip_regex_no_skip_option(self): - options = stub(skip_requirements_regex=None) - line = '--extra-index-url Good' - assert [(0, line)] == list(skip_regex(enumerate([line]), options)) + assert [(1, line)] == list(preprocess(line, pattern)) class TestProcessLine(object): From 79f05916300a0fe41c70b227eadc8fcda4eda170 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver <thomas@kluyver.me.uk> Date: Sun, 20 Oct 2019 21:23:49 +0100 Subject: [PATCH 0615/3170] Don't check write access on the same path twice --- src/pip/_internal/commands/install.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index cb7b972ee17..851a9537974 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -596,7 +596,7 @@ def get_lib_location_guesses(*args, **kwargs): def site_packages_writable(**kwargs): return all( - test_writable_dir(d) for d in get_lib_location_guesses(**kwargs) + test_writable_dir(d) for d in set(get_lib_location_guesses(**kwargs)) ) From 16174f4ad07664d71be72276997f6b0e3a0fd6d4 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver <thomas@kluyver.me.uk> Date: Sun, 20 Oct 2019 21:28:56 +0100 Subject: [PATCH 0616/3170] Add logging for decide_user_install --- src/pip/_internal/commands/install.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 851a9537974..ead9f0fe9c7 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -617,6 +617,7 @@ def decide_user_install( which is provided by the other arguments. """ if use_user_site is False: + logger.debug("Non-user install by explicit request") return False if use_user_site is True: @@ -630,6 +631,7 @@ def decide_user_install( "Can not perform a '--user' install. User site-packages " "are not visible in this virtualenv." ) + logger.debug("User install by explicit request") return True # If we are here, user installs have not been explicitly requested/avoided @@ -637,16 +639,22 @@ def decide_user_install( # user install incompatible with --prefix/--target if prefix_path or target_dir: + logger.debug("Non-user install due to --prefix or --target option") return False # If user installs are not enabled, choose a non-user install if not site.ENABLE_USER_SITE: + logger.debug("Non-user install because user site-packages disabled") return False # If we don't have permission for a non-user install, choose a user install - return not site_packages_writable( - root=root_path, isolated=isolated_mode, - ) + if site_packages_writable(root=root_path, isolated=isolated_mode): + logger.debug("Non-user install because site-packages writeable") + return False + + logger.info("Defaulting to user installation because normal site-packages " + "is not writeable") + return True def create_env_error_message(error, show_traceback, using_user_site): From fbc0588c0135574f6ad61928bd9ca70564bf4ef4 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver <thomas@kluyver.me.uk> Date: Sun, 20 Oct 2019 21:48:18 +0100 Subject: [PATCH 0617/3170] Add unit tests of decide_user_install() --- src/pip/_internal/commands/install.py | 8 +++--- tests/unit/test_command_install.py | 36 ++++++++++++++++++++++++++- 2 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index ead9f0fe9c7..3f19ade8927 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -602,10 +602,10 @@ def site_packages_writable(**kwargs): def decide_user_install( use_user_site, # type: Optional[bool] - prefix_path, # type: Optional[str] - target_dir, # type: Optional[str] - root_path, # type: Optional[str] - isolated_mode, # type: bool + prefix_path=None, # type: Optional[str] + target_dir=None, # type: Optional[str] + root_path=None, # type: Optional[str] + isolated_mode=False, # type: bool ): # type: (...) -> bool """Determine whether to do a user install based on the input options. diff --git a/tests/unit/test_command_install.py b/tests/unit/test_command_install.py index 1a3fee5c9ae..34635cd6df9 100644 --- a/tests/unit/test_command_install.py +++ b/tests/unit/test_command_install.py @@ -1,6 +1,6 @@ from mock import Mock, call, patch -from pip._internal.commands.install import build_wheels +from pip._internal.commands.install import build_wheels, decide_user_install class TestWheelCache: @@ -61,3 +61,37 @@ def test_build_wheels__wheel_not_installed(self, is_wheel_installed): ] assert build_failures == ['a'] + + +class TestDecideUserInstall: + @patch('site.ENABLE_USER_SITE', True) + @patch('pip._internal.commands.install.site_packages_writable') + def test_prefix_and_target(self, sp_writable): + sp_writable.return_value = False + + assert decide_user_install( + use_user_site=None, prefix_path='foo' + ) is False + + assert decide_user_install( + use_user_site=None, target_dir='bar' + ) is False + + @patch('pip._internal.commands.install.site_packages_writable') + def test_user_site_enabled(self, sp_writable): + sp_writable.return_value = False + + with patch('site.ENABLE_USER_SITE', True): + assert decide_user_install(use_user_site=None) is True + + with patch('site.ENABLE_USER_SITE', False): + assert decide_user_install(use_user_site=None) is False + + @patch('site.ENABLE_USER_SITE', True) + @patch('pip._internal.commands.install.site_packages_writable') + def test_site_packages_access(self, sp_writable): + sp_writable.return_value = True + assert decide_user_install(use_user_site=None) is False + + sp_writable.return_value = False + assert decide_user_install(use_user_site=None) is True From c4d92bbb4ea704069de8381e3629348db32c3818 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver <thomas@kluyver.me.uk> Date: Mon, 21 Oct 2019 08:52:44 +0100 Subject: [PATCH 0618/3170] Use pytest parametrize for decide_user_install tests --- tests/unit/test_command_install.py | 35 +++++++++++++++--------------- 1 file changed, 17 insertions(+), 18 deletions(-) diff --git a/tests/unit/test_command_install.py b/tests/unit/test_command_install.py index 34635cd6df9..13d644a8587 100644 --- a/tests/unit/test_command_install.py +++ b/tests/unit/test_command_install.py @@ -1,3 +1,4 @@ +import pytest from mock import Mock, call, patch from pip._internal.commands.install import build_wheels, decide_user_install @@ -77,21 +78,19 @@ def test_prefix_and_target(self, sp_writable): use_user_site=None, target_dir='bar' ) is False - @patch('pip._internal.commands.install.site_packages_writable') - def test_user_site_enabled(self, sp_writable): - sp_writable.return_value = False - - with patch('site.ENABLE_USER_SITE', True): - assert decide_user_install(use_user_site=None) is True - - with patch('site.ENABLE_USER_SITE', False): - assert decide_user_install(use_user_site=None) is False - - @patch('site.ENABLE_USER_SITE', True) - @patch('pip._internal.commands.install.site_packages_writable') - def test_site_packages_access(self, sp_writable): - sp_writable.return_value = True - assert decide_user_install(use_user_site=None) is False - - sp_writable.return_value = False - assert decide_user_install(use_user_site=None) is True + @pytest.mark.parametrize( + "enable_user_site,site_packages_writable,result", [ + (True, True, False), + (True, False, True), + (False, True, False), + (False, False, False), + ]) + def test_most_cases( + self, enable_user_site, site_packages_writable, result, monkeypatch, + ): + monkeypatch.setattr('site.ENABLE_USER_SITE', enable_user_site) + monkeypatch.setattr( + 'pip._internal.commands.install.site_packages_writable', + lambda **kw: site_packages_writable + ) + assert decide_user_install(use_user_site=None) is result From 42b1ef3537343fa1787480b45daa353241b4705a Mon Sep 17 00:00:00 2001 From: Thomas Kluyver <thomas@kluyver.me.uk> Date: Mon, 21 Oct 2019 13:52:50 +0100 Subject: [PATCH 0619/3170] Clarify comment --- src/pip/_internal/commands/install.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 3f19ade8927..12d58f6e056 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -647,7 +647,8 @@ def decide_user_install( logger.debug("Non-user install because user site-packages disabled") return False - # If we don't have permission for a non-user install, choose a user install + # If we have permission for a non-user install, do that, + # otherwise do a user install. if site_packages_writable(root=root_path, isolated=isolated_mode): logger.debug("Non-user install because site-packages writeable") return False From 4c6052d2d6a0ff7c2cc659367c027568ad81cb30 Mon Sep 17 00:00:00 2001 From: jenix21 <devfrog@gmail.com> Date: Wed, 23 Oct 2019 01:00:08 +0900 Subject: [PATCH 0620/3170] Add tests for create_env_error_message function --- tests/unit/test_command_install.py | 47 +++++++++++++++++++++++++++++- 1 file changed, 46 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_command_install.py b/tests/unit/test_command_install.py index 13d644a8587..c7dd43b85b0 100644 --- a/tests/unit/test_command_install.py +++ b/tests/unit/test_command_install.py @@ -1,7 +1,13 @@ +import errno + import pytest from mock import Mock, call, patch -from pip._internal.commands.install import build_wheels, decide_user_install +from pip._internal.commands.install import ( + build_wheels, + create_env_error_message, + decide_user_install, +) class TestWheelCache: @@ -94,3 +100,42 @@ def test_most_cases( lambda **kw: site_packages_writable ) assert decide_user_install(use_user_site=None) is result + + +def error_creation_helper(with_errno=False): + env_error = EnvironmentError("No file permission") + if with_errno: + env_error.errno = errno.EACCES + return env_error + + +@pytest.mark.parametrize('error, show_traceback, using_user_site, expected', [ + # show_traceback = True, using_user_site = True + (error_creation_helper(), True, True, 'Could not install packages due to' + ' an EnvironmentError.\n'), + (error_creation_helper(True), True, True, 'Could not install packages due' + ' to an EnvironmentError.\nCheck the permissions.\n'), + # show_traceback = True, using_user_site = False + (error_creation_helper(), True, False, 'Could not install packages due to' + ' an EnvironmentError.\n'), + (error_creation_helper(True), True, False, 'Could not install packages due' + ' to an EnvironmentError.\nConsider using the `--user` option or check' + ' the permissions.\n'), + # show_traceback = False, using_user_site = True + (error_creation_helper(), False, True, 'Could not install packages due to' + ' an EnvironmentError: No file permission\n'), + (error_creation_helper(True), False, True, 'Could not install packages due' + ' to an EnvironmentError: No file permission\nCheck the' + ' permissions.\n'), + # show_traceback = False, using_user_site = False + (error_creation_helper(), False, False, 'Could not install packages due to' + ' an EnvironmentError: No file permission\n'), + (error_creation_helper(True), False, False, 'Could not install packages' + ' due to an EnvironmentError: No file permission\nConsider using the' + ' `--user` option or check the permissions.\n'), +]) +def test_create_env_error_message( + error, show_traceback, using_user_site, expected +): + msg = create_env_error_message(error, show_traceback, using_user_site) + assert msg == expected From d631a9355c3469d43e5fcf8d8093276906abca7f Mon Sep 17 00:00:00 2001 From: Thomas Kluyver <thomas@kluyver.me.uk> Date: Thu, 24 Oct 2019 11:59:22 +0100 Subject: [PATCH 0621/3170] Allow for use_user_site being set to an integer This can come from tox config, see: https://github.com/pypa/pip/pull/7002#issuecomment-545108292 --- src/pip/_internal/commands/install.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index e50155c2709..9bc9f1e9b37 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -616,11 +616,13 @@ def decide_user_install( If use_user_site is None, the default behaviour depends on the environment, which is provided by the other arguments. """ - if use_user_site is False: + # In some cases (config from tox), use_user_site can be set to an integer + # rather than a bool. Comparing == True/False instead of 'is' allows this. + if use_user_site == False: # noqa: E712 logger.debug("Non-user install by explicit request") return False - if use_user_site is True: + if use_user_site == True: # noqa: E712 if prefix_path: raise CommandError( "Can not combine '--user' and '--prefix' as they imply " From 17e9f4c4a36c29f8ce4cd984fd3689e8bc75a092 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver <thomas@kluyver.me.uk> Date: Thu, 24 Oct 2019 12:01:36 +0100 Subject: [PATCH 0622/3170] Mark as trivial change --- news/284c23de-df0b-4aaa-8454-4569829768fc.trivial | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 news/284c23de-df0b-4aaa-8454-4569829768fc.trivial diff --git a/news/284c23de-df0b-4aaa-8454-4569829768fc.trivial b/news/284c23de-df0b-4aaa-8454-4569829768fc.trivial new file mode 100644 index 00000000000..e69de29bb2d From fb8eb15b9ab3a5b2baaf8ea3ef9f7b28c72ea31d Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 20 Oct 2019 14:35:42 +0530 Subject: [PATCH 0623/3170] Handle unnamed requirements within move_to_correct_build_directory Why: This is only called in one situation, so it makes sense to inline the relevant bit of code from that method to here. --- src/pip/_internal/req/req_install.py | 38 +++++++++++++++++----------- 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 20cd76502a7..ef0938e270d 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -162,7 +162,7 @@ def __init__( # Temporary build location self._temp_build_dir = None # type: Optional[TempDirectory] # Used to store the global directory where the _temp_build_dir should - # have been created. Cf move_to_correct_build_directory method. + # have been created. See move_to_correct_build_directory(). self._ideal_build_dir = None # type: Optional[str] # Set to True after successful installation self.install_succeeded = None # type: Optional[bool] @@ -385,7 +385,7 @@ def ensure_build_location(self, build_dir): def move_to_correct_build_directory(self): # type: () -> None - """Move self._temp_build_dir to "self._ideal_build_dir/self.req.name" + """Move self._temp_build_dir to "self._ideal_build_dir/{metadata name}" For some requirements (e.g. a path to a directory), the name of the package is not available until we run egg_info, so the build_location @@ -394,14 +394,33 @@ def move_to_correct_build_directory(self): This is only called to "fix" the build directory after generating metadata. """ + assert self.req is None + assert self.metadata is not None + + # Construct a Requirement object from the generated metadata + if isinstance(parse_version(self.metadata["Version"]), Version): + op = "==" + else: + op = "===" + + self.req = Requirement( + "".join([ + self.metadata["Name"], + op, + self.metadata["Version"], + ]) + ) + if self.source_dir is not None: return - assert self.req is not None + assert self._temp_build_dir assert ( self._ideal_build_dir is not None and self._ideal_build_dir.path # type: ignore ) + + # Backup directory for later use. old_location = self._temp_build_dir self._temp_build_dir = None # checked inside ensure_build_location @@ -586,18 +605,7 @@ def prepare_metadata(self): with indent_log(): self.metadata_directory = metadata_generator(self) - if not self.req: - if isinstance(parse_version(self.metadata["Version"]), Version): - op = "==" - else: - op = "===" - self.req = Requirement( - "".join([ - self.metadata["Name"], - op, - self.metadata["Version"], - ]) - ) + if not self.name: self.move_to_correct_build_directory() else: metadata_name = canonicalize_name(self.metadata["Name"]) From 219a9e9d2370da0ea83a9af3ea09ddffc326f01d Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 20 Oct 2019 14:36:54 +0530 Subject: [PATCH 0624/3170] Drop a conditional Why: The metadata directory is always going to be available here. The method is called *after* metadata generation. --- src/pip/_internal/req/req_install.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index ef0938e270d..370c2de8d26 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -445,13 +445,12 @@ def move_to_correct_build_directory(self): path=new_location, kind="req-install", ) - # Correct the metadata directory, if it exists - if self.metadata_directory: - old_meta = self.metadata_directory - rel = os.path.relpath(old_meta, start=old_location.path) - new_meta = os.path.join(new_location, rel) - new_meta = os.path.normpath(os.path.abspath(new_meta)) - self.metadata_directory = new_meta + # Correct the metadata directory + old_meta = self.metadata_directory + rel = os.path.relpath(old_meta, start=old_location.path) + new_meta = os.path.join(new_location, rel) + new_meta = os.path.normpath(os.path.abspath(new_meta)) + self.metadata_directory = new_meta # Done with any "move built files" work, since have moved files to the # "ideal" build location. Setting to None allows to clearly flag that From 2c18e1e50002b6afb853b7b346081fb91d2820d5 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 20 Oct 2019 14:37:34 +0530 Subject: [PATCH 0625/3170] Move warning on mismatching name to a method --- src/pip/_internal/req/req_install.py | 21 ++++++++++++--------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 370c2de8d26..a2a86d4c4e6 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -457,6 +457,17 @@ def move_to_correct_build_directory(self): # no more moves are needed. self._ideal_build_dir = None + def warn_on_mismatching_name(self): + metadata_name = canonicalize_name(self.metadata["Name"]) + if canonicalize_name(self.req.name) != metadata_name: + logger.warning( + 'Generating metadata for package %s ' + 'produced metadata for project name %s. Fix your ' + '#egg=%s fragments.', + self.name, metadata_name, self.name + ) + self.req = Requirement(metadata_name) + def remove_temporary_source(self): # type: () -> None """Remove the source files from this requirement, if they are marked @@ -607,15 +618,7 @@ def prepare_metadata(self): if not self.name: self.move_to_correct_build_directory() else: - metadata_name = canonicalize_name(self.metadata["Name"]) - if canonicalize_name(self.req.name) != metadata_name: - logger.warning( - 'Generating metadata for package %s ' - 'produced metadata for project name %s. Fix your ' - '#egg=%s fragments.', - self.name, metadata_name, self.name - ) - self.req = Requirement(metadata_name) + self.warn_on_mismatching_name() @property def metadata(self): From c88da892dba52d3f8f90982ea2aea889fcde933d Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 20 Oct 2019 14:39:01 +0530 Subject: [PATCH 0626/3170] Reduce indentation in warn_on_mismatching_name Why: Makes it easier to read IMO. --- src/pip/_internal/req/req_install.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index a2a86d4c4e6..f47bbbf0f45 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -459,14 +459,18 @@ def move_to_correct_build_directory(self): def warn_on_mismatching_name(self): metadata_name = canonicalize_name(self.metadata["Name"]) - if canonicalize_name(self.req.name) != metadata_name: - logger.warning( - 'Generating metadata for package %s ' - 'produced metadata for project name %s. Fix your ' - '#egg=%s fragments.', - self.name, metadata_name, self.name - ) - self.req = Requirement(metadata_name) + if canonicalize_name(self.req.name) == metadata_name: + # Everything is fine. + return + + # If we're here, there's a mismatch. Log a warning about it. + logger.warning( + 'Generating metadata for package %s ' + 'produced metadata for project name %s. Fix your ' + '#egg=%s fragments.', + self.name, metadata_name, self.name + ) + self.req = Requirement(metadata_name) def remove_temporary_source(self): # type: () -> None From 23bac8f574d721ffac779b7d738de9a0a9452739 Mon Sep 17 00:00:00 2001 From: Aniruddha Basak <codewithaniruddha@gmail.com> Date: Fri, 25 Oct 2019 00:51:40 +0530 Subject: [PATCH 0627/3170] Add https:// in the login token example --- docs/html/user_guide.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index e2aec8a19ff..c37857ac976 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -58,7 +58,7 @@ a username and password separated by ``:``. :: https://[username[:password]]@pypi.company.com/simple - 0123456789abcdef@pypi.company.com + https://0123456789abcdef@pypi.company.com https://aniruddha%24basak:gdg%24js%5Ejf%26l@pypi.company.com `Here <https://en.wikipedia.org/wiki/Percent-encoding>`_ you can find more about From 1310fc276a6a22eac7d2ffd8251065a9daac9375 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver <thomas@kluyver.me.uk> Date: Fri, 25 Oct 2019 08:26:11 +0100 Subject: [PATCH 0628/3170] Use truthiness of everything but None for use_user_site --- src/pip/_internal/commands/install.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 9bc9f1e9b37..bf4fbe3c777 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -617,12 +617,12 @@ def decide_user_install( which is provided by the other arguments. """ # In some cases (config from tox), use_user_site can be set to an integer - # rather than a bool. Comparing == True/False instead of 'is' allows this. - if use_user_site == False: # noqa: E712 + # rather than a bool, which 'use_user_site is False' wouldn't catch. + if (use_user_site is not None) and (not use_user_site): logger.debug("Non-user install by explicit request") return False - if use_user_site == True: # noqa: E712 + if use_user_site: if prefix_path: raise CommandError( "Can not combine '--user' and '--prefix' as they imply " From 1ba1b6ca373f33a36441544dce70d4994d747215 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 20 Oct 2019 12:55:26 +0530 Subject: [PATCH 0629/3170] Simplify RequirementTracker.cleanup How: Utilize early returns and minor duplication for easier reading. --- src/pip/_internal/req/req_tracker.py | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/req/req_tracker.py b/src/pip/_internal/req/req_tracker.py index aa57c799a69..a5b5803b217 100644 --- a/src/pip/_internal/req/req_tracker.py +++ b/src/pip/_internal/req/req_tracker.py @@ -83,12 +83,16 @@ def cleanup(self): # type: () -> None for req in set(self._entries): self.remove(req) - remove = self._temp_dir is not None - if remove: - self._temp_dir.cleanup() - logger.debug('%s build tracker %r', - 'Removed' if remove else 'Cleaned', - self._root) + + if self._temp_dir is None: + # Did not setup the directory. No action needed. + logger.debug("Cleaned build tracker: %r", self._root) + return + + # Cleanup the directory. + self._temp_dir.cleanup() + del os.environ['PIP_REQ_TRACKER'] + logger.debug("Removed build tracker: %r", self._root) @contextlib.contextmanager def track(self, req): From 69e0178a543e0f28e74ecf03bd3f54930ac884de Mon Sep 17 00:00:00 2001 From: Aniruddha Basak <codewithaniruddha@gmail.com> Date: Fri, 25 Oct 2019 16:40:05 +0530 Subject: [PATCH 0630/3170] Add description about the examples --- docs/html/user_guide.rst | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index c37857ac976..f359b0d016d 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -55,11 +55,20 @@ Basic Authentication Credentials pip supports basic authentication credentials. Basically, in the url there is a username and password separated by ``:``. - :: +``https://[username[:password]]@pypi.company.com/simple`` + +Certain special characters are not valid in the authentication part of URLs. +If the user or password part of your login credentials contain any of these +special characters here then they must be percent-encoded. For example, for a +user with username "user" and password "he//o" accessing a repository at +pypi.company.com, the index URL with credentials would look like: + +``https://user:he%2F%2Fo@pypi.company.com`` + +For indexes that only require single-part authentication tokens, provide the token +as the "username" and do not provide a password, for example - - https://[username[:password]]@pypi.company.com/simple - https://0123456789abcdef@pypi.company.com - https://aniruddha%24basak:gdg%24js%5Ejf%26l@pypi.company.com +``https://0123456789abcdef@pypi.company.com`` `Here <https://en.wikipedia.org/wiki/Percent-encoding>`_ you can find more about percent encoding. From 631807c10f8ccbb75ce2016171d5b814fdcfa1da Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 25 Oct 2019 17:52:17 -0400 Subject: [PATCH 0631/3170] Represent parsed lines with ParsedLine class Also separate logic that handles normal requirement lines from those that include other requirement/constraint files. --- src/pip/_internal/req/req_file.py | 136 ++++++++++++++++++++---------- 1 file changed, 90 insertions(+), 46 deletions(-) diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index bbc12555a96..093ae215663 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -84,6 +84,25 @@ SUPPORTED_OPTIONS_REQ_DEST = [str(o().dest) for o in SUPPORTED_OPTIONS_REQ] +class ParsedLine(object): + def __init__( + self, + filename, # type: str + lineno, # type: int + comes_from, # type: str + args, # type: str + opts, # type: Values + constraint, # type: bool + ): + # type: (...) -> None + self.filename = filename + self.lineno = lineno + self.comes_from = comes_from + self.args = args + self.opts = opts + self.constraint = constraint + + def parse_requirements( filename, # type: str finder=None, # type: Optional[PackageFinder] @@ -127,7 +146,8 @@ def parse_requirements( comes_from, options, session, wheel_cache, use_pep517=use_pep517, constraint=constraint) for req in req_iter: - yield req + if req is not None: + yield req def preprocess(content, skip_requirements_regex): @@ -158,22 +178,8 @@ def process_line( use_pep517=None, # type: Optional[bool] constraint=False, # type: bool ): - # type: (...) -> Iterator[InstallRequirement] - """Process a single requirements line; This can result in creating/yielding - requirements, or updating the finder. - - For lines that contain requirements, the only options that have an effect - are from SUPPORTED_OPTIONS_REQ, and they are scoped to the - requirement. Other options from SUPPORTED_OPTIONS may be present, but are - ignored. - - For lines that do not contain requirements, the only options that have an - effect are from SUPPORTED_OPTIONS. Options from SUPPORTED_OPTIONS_REQ may - be present, but are ignored. These lines may contain multiple options - (although our docs imply only one is supported), and all our parsed and - affect the finder. - - :param constraint: If True, parsing a constraints file. + # type: (...) -> Iterator[Optional[InstallRequirement]] + """ :param options: OptionParser options that we may update """ line_parser = get_line_parser(finder) @@ -212,62 +218,96 @@ def process_line( yield req return + parsed_line = ParsedLine( + filename, line_number, comes_from, args_str, opts, constraint + ) + + yield handle_line( + parsed_line, finder, options, session, wheel_cache, use_pep517 + ) + + +def handle_line( + line, # type: ParsedLine + finder=None, # type: Optional[PackageFinder] + options=None, # type: Optional[optparse.Values] + session=None, # type: Optional[PipSession] + wheel_cache=None, # type: Optional[WheelCache] + use_pep517=None, # type: Optional[bool] +): + # type: (...) -> Optional[InstallRequirement] + """Handle a single parsed requirements line; This can result in + creating/yielding requirements, or updating the finder. + + For lines that contain requirements, the only options that have an effect + are from SUPPORTED_OPTIONS_REQ, and they are scoped to the + requirement. Other options from SUPPORTED_OPTIONS may be present, but are + ignored. + + For lines that do not contain requirements, the only options that have an + effect are from SUPPORTED_OPTIONS. Options from SUPPORTED_OPTIONS_REQ may + be present, but are ignored. These lines may contain multiple options + (although our docs imply only one is supported), and all our parsed and + affect the finder. + """ + # preserve for the nested code path line_comes_from = '%s %s (line %s)' % ( - '-c' if constraint else '-r', filename, line_number, + '-c' if line.constraint else '-r', line.filename, line.lineno, ) - # yield a line requirement - if args_str: + # return a line requirement + if line.args: isolated = options.isolated_mode if options else False if options: - cmdoptions.check_install_build_global(options, opts) + cmdoptions.check_install_build_global(options, line.opts) # get the options that apply to requirements req_options = {} for dest in SUPPORTED_OPTIONS_REQ_DEST: - if dest in opts.__dict__ and opts.__dict__[dest]: - req_options[dest] = opts.__dict__[dest] - line_source = 'line {} of {}'.format(line_number, filename) - yield install_req_from_line( - args_str, + if dest in line.opts.__dict__ and line.opts.__dict__[dest]: + req_options[dest] = line.opts.__dict__[dest] + line_source = 'line {} of {}'.format(line.lineno, line.filename) + return install_req_from_line( + line.args, comes_from=line_comes_from, use_pep517=use_pep517, isolated=isolated, options=req_options, wheel_cache=wheel_cache, - constraint=constraint, + constraint=line.constraint, line_source=line_source, ) - # yield an editable requirement - elif opts.editables: + # return an editable requirement + elif line.opts.editables: isolated = options.isolated_mode if options else False - yield install_req_from_editable( - opts.editables[0], comes_from=line_comes_from, + return install_req_from_editable( + line.opts.editables[0], comes_from=line_comes_from, use_pep517=use_pep517, - constraint=constraint, isolated=isolated, wheel_cache=wheel_cache + constraint=line.constraint, isolated=isolated, + wheel_cache=wheel_cache ) # percolate hash-checking option upward - elif opts.require_hashes: - options.require_hashes = opts.require_hashes + elif line.opts.require_hashes: + options.require_hashes = line.opts.require_hashes # set finder options elif finder: find_links = finder.find_links index_urls = finder.index_urls - if opts.index_url: - index_urls = [opts.index_url] - if opts.no_index is True: + if line.opts.index_url: + index_urls = [line.opts.index_url] + if line.opts.no_index is True: index_urls = [] - if opts.extra_index_urls: - index_urls.extend(opts.extra_index_urls) - if opts.find_links: + if line.opts.extra_index_urls: + index_urls.extend(line.opts.extra_index_urls) + if line.opts.find_links: # FIXME: it would be nice to keep track of the source # of the find_links: support a find-links local path # relative to a requirements file. - value = opts.find_links[0] - req_dir = os.path.dirname(os.path.abspath(filename)) + value = line.opts.find_links[0] + req_dir = os.path.dirname(os.path.abspath(line.filename)) relative_to_reqs_file = os.path.join(req_dir, value) if os.path.exists(relative_to_reqs_file): value = relative_to_reqs_file @@ -279,11 +319,15 @@ def process_line( ) finder.search_scope = search_scope - if opts.pre: + if line.opts.pre: finder.set_allow_all_prereleases() - for host in opts.trusted_hosts or []: - source = 'line {} of {}'.format(line_number, filename) - session.add_trusted_host(host, source=source) + + if session: + for host in line.opts.trusted_hosts or []: + source = 'line {} of {}'.format(line.lineno, line.filename) + session.add_trusted_host(host, source=source) + + return None def get_line_parser(finder): From 68454d29a57db9c3db6e2dcdb66ab355983f5aef Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 25 Oct 2019 18:53:54 -0400 Subject: [PATCH 0632/3170] Make req file parsing unit tests higher-level Decoupling the tests from the implementation makes it possible to refactor the interface exposed by process_line. --- tests/unit/test_req_file.py | 161 +++++++++++++++++++++++------------- 1 file changed, 103 insertions(+), 58 deletions(-) diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py index 29145680f62..9013c686048 100644 --- a/tests/unit/test_req_file.py +++ b/tests/unit/test_req_file.py @@ -5,6 +5,7 @@ import pytest from mock import Mock, patch +from pip._vendor.six import PY2 from pretend import stub import pip._internal.req.req_file # this will be monkeypatched @@ -169,98 +170,133 @@ def test_skip_regex_no_options(self): assert [(1, line)] == list(preprocess(line, pattern)) +@pytest.fixture +def line_processor( + monkeypatch, + tmpdir, +): + def process_line( + line, + filename, + line_number, + finder=None, + options=None, + session=None, + constraint=False, + ): + if session is None: + session = PipSession() + + prefix = '\n' * (line_number - 1) + path = tmpdir.joinpath(filename) + path.parent.mkdir(exist_ok=True) + path.write_text(prefix + line) + monkeypatch.chdir(str(tmpdir)) + return list(parse_requirements( + filename, + finder=finder, + options=options, + session=session, + constraint=constraint, + )) + + return process_line + + class TestProcessLine(object): """tests for `process_line`""" - def test_parser_error(self): + def test_parser_error(self, line_processor): with pytest.raises(RequirementsFileParseError): - list(process_line("--bogus", "file", 1)) + line_processor("--bogus", "file", 1) - def test_parser_offending_line(self): + def test_parser_offending_line(self, line_processor): line = 'pkg==1.0.0 --hash=somehash' with pytest.raises(RequirementsFileParseError) as err: - list(process_line(line, 'file', 1)) + line_processor(line, 'file', 1) assert line in str(err.value) - def test_parser_non_offending_line(self): + def test_parser_non_offending_line(self, line_processor): try: - list(process_line('pkg==1.0.0 --hash=sha256:somehash', 'file', 1)) + line_processor('pkg==1.0.0 --hash=sha256:somehash', 'file', 1) except RequirementsFileParseError: pytest.fail('Reported offending line where it should not.') - def test_only_one_req_per_line(self): + def test_only_one_req_per_line(self, line_processor): # pkg_resources raises the ValueError with pytest.raises(InstallationError): - list(process_line("req1 req2", "file", 1)) + line_processor("req1 req2", "file", 1) - def test_error_message(self): + def test_error_message(self, line_processor): """ Test the error message if a parsing error occurs (all of path, line number, and hint). """ - iterator = process_line( - 'my-package=1.0', - filename='path/requirements.txt', - line_number=3 - ) with pytest.raises(InstallationError) as exc: - list(iterator) + line_processor( + 'my-package=1.0', + filename='path/requirements.txt', + line_number=3 + ) + package_name = "u'my-package=1.0'" if PY2 else "'my-package=1.0'" expected = ( - "Invalid requirement: 'my-package=1.0' " + "Invalid requirement: {} " '(from line 3 of path/requirements.txt)\n' 'Hint: = is not a valid operator. Did you mean == ?' - ) + ).format(package_name) assert str(exc.value) == expected - def test_yield_line_requirement(self): + def test_yield_line_requirement(self, line_processor): line = 'SomeProject' filename = 'filename' comes_from = '-r %s (line %s)' % (filename, 1) req = install_req_from_line(line, comes_from=comes_from) - assert repr(list(process_line(line, filename, 1))[0]) == repr(req) + assert repr(line_processor(line, filename, 1)[0]) == repr(req) - def test_yield_pep440_line_requirement(self): + def test_yield_pep440_line_requirement(self, line_processor): line = 'SomeProject @ https://url/SomeProject-py2-py3-none-any.whl' filename = 'filename' comes_from = '-r %s (line %s)' % (filename, 1) req = install_req_from_line(line, comes_from=comes_from) - assert repr(list(process_line(line, filename, 1))[0]) == repr(req) + assert repr(line_processor(line, filename, 1)[0]) == repr(req) - def test_yield_line_constraint(self): + def test_yield_line_constraint(self, line_processor): line = 'SomeProject' filename = 'filename' comes_from = '-c %s (line %s)' % (filename, 1) req = install_req_from_line( line, comes_from=comes_from, constraint=True) - found_req = list(process_line(line, filename, 1, constraint=True))[0] + found_req = line_processor(line, filename, 1, constraint=True)[0] assert repr(found_req) == repr(req) assert found_req.constraint is True - def test_yield_line_requirement_with_spaces_in_specifier(self): + def test_yield_line_requirement_with_spaces_in_specifier( + self, line_processor + ): line = 'SomeProject >= 2' filename = 'filename' comes_from = '-r %s (line %s)' % (filename, 1) req = install_req_from_line(line, comes_from=comes_from) - assert repr(list(process_line(line, filename, 1))[0]) == repr(req) + assert repr(line_processor(line, filename, 1)[0]) == repr(req) assert str(req.req.specifier) == '>=2' - def test_yield_editable_requirement(self): + def test_yield_editable_requirement(self, line_processor): url = 'git+https://url#egg=SomeProject' line = '-e %s' % url filename = 'filename' comes_from = '-r %s (line %s)' % (filename, 1) req = install_req_from_editable(url, comes_from=comes_from) - assert repr(list(process_line(line, filename, 1))[0]) == repr(req) + assert repr(line_processor(line, filename, 1)[0]) == repr(req) - def test_yield_editable_constraint(self): + def test_yield_editable_constraint(self, line_processor): url = 'git+https://url#egg=SomeProject' line = '-e %s' % url filename = 'filename' comes_from = '-c %s (line %s)' % (filename, 1) req = install_req_from_editable( url, comes_from=comes_from, constraint=True) - found_req = list(process_line(line, filename, 1, constraint=True))[0] + found_req = line_processor(line, filename, 1, constraint=True)[0] assert repr(found_req) == repr(req) assert found_req.constraint is True @@ -288,16 +324,16 @@ def stub_parse_requirements(req_url, finder, comes_from, options, parse_requirements_stub.call) assert list(process_line(line, 'filename', 1)) == [(req, True)] - def test_options_on_a_requirement_line(self): + def test_options_on_a_requirement_line(self, line_processor): line = 'SomeProject --install-option=yo1 --install-option yo2 '\ '--global-option="yo3" --global-option "yo4"' filename = 'filename' - req = list(process_line(line, filename, 1))[0] + req = line_processor(line, filename, 1)[0] assert req.options == { 'global_options': ['yo3', 'yo4'], 'install_options': ['yo1', 'yo2']} - def test_hash_options(self): + def test_hash_options(self, line_processor): """Test the --hash option: mostly its value storage. Make sure it reads and preserve multiple hashes. @@ -310,7 +346,7 @@ def test_hash_options(self): '--hash=sha256:486ea46224d1bb4fb680f34f7c9ad96a8f24ec88be73ea8' 'e5a6c65260e9cb8a7') filename = 'filename' - req = list(process_line(line, filename, 1))[0] + req = line_processor(line, filename, 1)[0] assert req.options == {'hashes': { 'sha256': ['2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e730433' '62938b9824', @@ -319,35 +355,37 @@ def test_hash_options(self): 'sha384': ['59e1748777448c69de6b800d7a33bbfb9ff1b463e44354c3553bcd' 'b9c666fa90125a3c79f90397bdf5f6a13de828684f']}} - def test_set_isolated(self, options): + def test_set_isolated(self, line_processor, options): line = 'SomeProject' filename = 'filename' options.isolated_mode = True - result = process_line(line, filename, 1, options=options) - assert list(result)[0].isolated + result = line_processor(line, filename, 1, options=options) + assert result[0].isolated - def test_set_finder_no_index(self, finder): - list(process_line("--no-index", "file", 1, finder=finder)) + def test_set_finder_no_index(self, line_processor, finder): + line_processor("--no-index", "file", 1, finder=finder) assert finder.index_urls == [] - def test_set_finder_index_url(self, finder): - list(process_line("--index-url=url", "file", 1, finder=finder)) + def test_set_finder_index_url(self, line_processor, finder): + line_processor("--index-url=url", "file", 1, finder=finder) assert finder.index_urls == ['url'] - def test_set_finder_find_links(self, finder): - list(process_line("--find-links=url", "file", 1, finder=finder)) + def test_set_finder_find_links(self, line_processor, finder): + line_processor("--find-links=url", "file", 1, finder=finder) assert finder.find_links == ['url'] - def test_set_finder_extra_index_urls(self, finder): - list(process_line("--extra-index-url=url", "file", 1, finder=finder)) + def test_set_finder_extra_index_urls(self, line_processor, finder): + line_processor("--extra-index-url=url", "file", 1, finder=finder) assert finder.index_urls == ['url'] - def test_set_finder_trusted_host(self, caplog, session, finder): + def test_set_finder_trusted_host( + self, line_processor, caplog, session, finder + ): with caplog.at_level(logging.INFO): - list(process_line( + line_processor( "--trusted-host=host1 --trusted-host=host2:8080", "file.txt", 1, finder=finder, session=session, - )) + ) assert list(finder.trusted_hosts) == ['host1', 'host2:8080'] session = finder._link_collector.session assert session.adapters['https://host1/'] is session._insecure_adapter @@ -363,23 +401,30 @@ def test_set_finder_trusted_host(self, caplog, session, finder): ) assert expected in actual - def test_noop_always_unzip(self, finder): + def test_noop_always_unzip(self, line_processor, finder): # noop, but confirm it can be set - list(process_line("--always-unzip", "file", 1, finder=finder)) + line_processor("--always-unzip", "file", 1, finder=finder) - def test_set_finder_allow_all_prereleases(self, finder): - list(process_line("--pre", "file", 1, finder=finder)) + def test_set_finder_allow_all_prereleases(self, line_processor, finder): + line_processor("--pre", "file", 1, finder=finder) assert finder.allow_all_prereleases - def test_relative_local_find_links(self, finder, monkeypatch): + def test_relative_local_find_links( + self, line_processor, finder, monkeypatch, tmpdir + ): """ Test a relative find_links path is joined with the req file directory """ + base_path = tmpdir / 'path' + + def normalize(path): + return os.path.normcase( + os.path.abspath(os.path.normpath(str(path))) + ) + # Make sure the test also passes on windows - req_file = os.path.normcase(os.path.abspath( - os.path.normpath('/path/req_file.txt'))) - nested_link = os.path.normcase(os.path.abspath( - os.path.normpath('/path/rel_path'))) + req_file = normalize(base_path / 'req_file.txt') + nested_link = normalize(base_path / 'rel_path') exists_ = os.path.exists def exists(path): @@ -387,9 +432,9 @@ def exists(path): return True else: exists_(path) + monkeypatch.setattr(os.path, 'exists', exists) - list(process_line("--find-links=rel_path", req_file, 1, - finder=finder)) + line_processor("--find-links=rel_path", req_file, 1, finder=finder) assert finder.find_links == [nested_link] def test_relative_http_nested_req_files(self, finder, monkeypatch): From 4d4c4f4a0248ef877268b90b898a9e5bb687292b Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 25 Oct 2019 19:45:27 -0400 Subject: [PATCH 0633/3170] Extract requirements file parsing from line handling By using a separate entity to parse lines and recurse into other requirements files, we can more easily use different strategies for handling the incoming requirements info. --- src/pip/_internal/req/req_file.py | 117 +++++++++++++++++++++++++----- 1 file changed, 99 insertions(+), 18 deletions(-) diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index 093ae215663..a5d3695f2d5 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -132,22 +132,20 @@ def parse_requirements( "'session'" ) - _, content = get_file_content( - filename, comes_from=comes_from, session=session - ) - skip_requirements_regex = ( options.skip_requirements_regex if options else None ) - lines_enum = preprocess(content, skip_requirements_regex) + line_parser = get_line_parser(finder) + parser = RequirementsFileParser( + session, line_parser, comes_from, skip_requirements_regex + ) - for line_number, line in lines_enum: - req_iter = process_line(line, filename, line_number, finder, - comes_from, options, session, wheel_cache, - use_pep517=use_pep517, constraint=constraint) - for req in req_iter: - if req is not None: - yield req + for parsed_line in parser.parse(filename, constraint): + req = handle_line( + parsed_line, finder, options, session, wheel_cache, use_pep517 + ) + if req is not None: + yield req def preprocess(content, skip_requirements_regex): @@ -330,16 +328,99 @@ def handle_line( return None +class RequirementsFileParser(object): + def __init__( + self, + session, # type: PipSession + line_parser, # type: LineParser + comes_from, # type: str + skip_requirements_regex, # type: Optional[str] + ): + # type: (...) -> None + self._session = session + self._line_parser = line_parser + self._comes_from = comes_from + self._skip_requirements_regex = skip_requirements_regex + + def parse(self, filename, constraint): + # type: (str, bool) -> Iterator[ParsedLine] + """Parse a given file, yielding parsed lines. + """ + for line in self._parse_and_recurse(filename, constraint): + yield line + + def _parse_and_recurse(self, filename, constraint): + # type: (str, bool) -> Iterator[ParsedLine] + for line in self._parse_file(filename, constraint): + if ( + not line.args and + not line.opts.editables and + (line.opts.requirements or line.opts.constraints) + ): + # parse a nested requirements file + if line.opts.requirements: + req_path = line.opts.requirements[0] + nested_constraint = False + else: + req_path = line.opts.constraints[0] + nested_constraint = True + + # original file is over http + if SCHEME_RE.search(filename): + # do a url join so relative paths work + req_path = urllib_parse.urljoin(filename, req_path) + # original file and nested file are paths + elif not SCHEME_RE.search(req_path): + # do a join so relative paths work + req_path = os.path.join( + os.path.dirname(filename), req_path, + ) + + for inner_line in self._parse_and_recurse( + req_path, nested_constraint, + ): + yield inner_line + else: + yield line + + def _parse_file(self, filename, constraint): + # type: (str, bool) -> Iterator[ParsedLine] + _, content = get_file_content( + filename, comes_from=self._comes_from, session=self._session + ) + + lines_enum = preprocess(content, self._skip_requirements_regex) + + for line_number, line in lines_enum: + try: + args_str, opts = self._line_parser(line) + except OptionParsingError as e: + # add offending line + msg = 'Invalid requirement: %s\n%s' % (line, e.msg) + raise RequirementsFileParseError(msg) + + yield ParsedLine( + filename, + line_number, + self._comes_from, + args_str, + opts, + constraint, + ) + + def get_line_parser(finder): # type: (Optional[PackageFinder]) -> LineParser - parser = build_parser() - defaults = parser.get_default_values() - defaults.index_url = None - if finder: - defaults.format_control = finder.format_control - def parse_line(line): # type: (Text) -> Tuple[str, Values] + # Build new parser for each line since it accumulates appendable + # options. + parser = build_parser() + defaults = parser.get_default_values() + defaults.index_url = None + if finder: + defaults.format_control = finder.format_control + args_str, options_str = break_args_options(line) # Prior to 2.7.3, shlex cannot deal with unicode entries if sys.version_info < (2, 7, 3): From 85918afc5e1c3df2d46b441f9a3cd80fbf7c80d0 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 26 Oct 2019 11:45:57 -0400 Subject: [PATCH 0634/3170] Remove req_file.process_line and update tests The behavior that was in process_line was moved to RequirementsFileParser so it's no longer needed, we just had to move the remaining tests to use the higher-level parse_requirements interface. --- src/pip/_internal/req/req_file.py | 61 ---------- tests/unit/test_req.py | 15 ++- tests/unit/test_req_file.py | 188 +++++++++++++++++------------- 3 files changed, 117 insertions(+), 147 deletions(-) diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index a5d3695f2d5..b4c22475fa4 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -164,67 +164,6 @@ def preprocess(content, skip_requirements_regex): return lines_enum -def process_line( - line, # type: Text - filename, # type: str - line_number, # type: int - finder=None, # type: Optional[PackageFinder] - comes_from=None, # type: Optional[str] - options=None, # type: Optional[optparse.Values] - session=None, # type: Optional[PipSession] - wheel_cache=None, # type: Optional[WheelCache] - use_pep517=None, # type: Optional[bool] - constraint=False, # type: bool -): - # type: (...) -> Iterator[Optional[InstallRequirement]] - """ - :param options: OptionParser options that we may update - """ - line_parser = get_line_parser(finder) - try: - args_str, opts = line_parser(line) - except OptionParsingError as e: - # add offending line - msg = 'Invalid requirement: %s\n%s' % (line, e.msg) - raise RequirementsFileParseError(msg) - - # parse a nested requirements file - if ( - not args_str and - not opts.editables and - (opts.requirements or opts.constraints) - ): - if opts.requirements: - req_path = opts.requirements[0] - nested_constraint = False - else: - req_path = opts.constraints[0] - nested_constraint = True - # original file is over http - if SCHEME_RE.search(filename): - # do a url join so relative paths work - req_path = urllib_parse.urljoin(filename, req_path) - # original file and nested file are paths - elif not SCHEME_RE.search(req_path): - # do a join so relative paths work - req_path = os.path.join(os.path.dirname(filename), req_path) - parsed_reqs = parse_requirements( - req_path, finder, comes_from, options, session, - constraint=nested_constraint, wheel_cache=wheel_cache - ) - for req in parsed_reqs: - yield req - return - - parsed_line = ParsedLine( - filename, line_number, comes_from, args_str, opts, constraint - ) - - yield handle_line( - parsed_line, finder, options, session, wheel_cache, use_pep517 - ) - - def handle_line( line, # type: ParsedLine finder=None, # type: Optional[PackageFinder] diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index d06f0bbed5f..4dcdc311780 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -29,7 +29,7 @@ install_req_from_req_string, parse_editable, ) -from pip._internal.req.req_file import process_line +from pip._internal.req.req_file import ParsedLine, get_line_parser, handle_line from pip._internal.req.req_tracker import RequirementTracker from pip._internal.utils.urls import path_to_url from tests.lib import ( @@ -41,7 +41,18 @@ def get_processed_req_from_line(line, fname='file', lineno=1): - req = list(process_line(line, fname, lineno))[0] + line_parser = get_line_parser(None) + args_str, opts = line_parser(line) + parsed_line = ParsedLine( + fname, + lineno, + fname, + args_str, + opts, + False, + ) + req = handle_line(parsed_line) + assert req is not None req.is_direct = True return req diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py index 9013c686048..83dab2111d0 100644 --- a/tests/unit/test_req_file.py +++ b/tests/unit/test_req_file.py @@ -4,7 +4,7 @@ import textwrap import pytest -from mock import Mock, patch +from mock import patch from pip._vendor.six import PY2 from pretend import stub @@ -25,7 +25,6 @@ join_lines, parse_requirements, preprocess, - process_line, skip_regex, ) from tests.lib import make_test_finder, requirements_file @@ -300,29 +299,21 @@ def test_yield_editable_constraint(self, line_processor): assert repr(found_req) == repr(req) assert found_req.constraint is True - def test_nested_requirements_file(self, monkeypatch): - line = '-r another_file' - req = install_req_from_line('SomeProject') - - def stub_parse_requirements(req_url, finder, comes_from, options, - session, wheel_cache, constraint): - return [(req, constraint)] - parse_requirements_stub = stub(call=stub_parse_requirements) - monkeypatch.setattr(pip._internal.req.req_file, 'parse_requirements', - parse_requirements_stub.call) - assert list(process_line(line, 'filename', 1)) == [(req, False)] - - def test_nested_constraints_file(self, monkeypatch): - line = '-c another_file' - req = install_req_from_line('SomeProject') - - def stub_parse_requirements(req_url, finder, comes_from, options, - session, wheel_cache, constraint): - return [(req, constraint)] - parse_requirements_stub = stub(call=stub_parse_requirements) - monkeypatch.setattr(pip._internal.req.req_file, 'parse_requirements', - parse_requirements_stub.call) - assert list(process_line(line, 'filename', 1)) == [(req, True)] + def test_nested_constraints_file(self, monkeypatch, tmpdir): + req_name = 'hello' + req_file = tmpdir / 'parent' / 'req_file.txt' + req_file.parent.mkdir() + req_file.write_text('-c reqs.txt') + req_file.parent.joinpath('reqs.txt').write_text(req_name) + + monkeypatch.chdir(str(tmpdir)) + + reqs = list( + parse_requirements('./parent/req_file.txt', session=session) + ) + assert len(reqs) == 1 + assert reqs[0].name == req_name + assert reqs[0].constraint def test_options_on_a_requirement_line(self, line_processor): line = 'SomeProject --install-option=yo1 --install-option yo2 '\ @@ -437,70 +428,99 @@ def exists(path): line_processor("--find-links=rel_path", req_file, 1, finder=finder) assert finder.find_links == [nested_link] - def test_relative_http_nested_req_files(self, finder, monkeypatch): + def test_relative_http_nested_req_files( + self, finder, session, monkeypatch + ): """ Test a relative nested req file path is joined with the req file url """ + req_name = 'hello' req_file = 'http://me.com/me/req_file.txt' - def parse(*args, **kwargs): - return iter([]) - mock_parse = Mock() - mock_parse.side_effect = parse - monkeypatch.setattr(pip._internal.req.req_file, 'parse_requirements', - mock_parse) - list(process_line("-r reqs.txt", req_file, 1, finder=finder)) - call = mock_parse.mock_calls[0] - assert call[1][0] == 'http://me.com/me/reqs.txt' - - def test_relative_local_nested_req_files(self, finder, monkeypatch): + def get_file_content(filename, *args, **kwargs): + if filename == req_file: + return None, '-r reqs.txt' + elif filename == 'http://me.com/me/reqs.txt': + return None, req_name + assert False, 'Unexpected file requested {}'.format(filename) + + monkeypatch.setattr( + pip._internal.req.req_file, 'get_file_content', get_file_content + ) + + result = list(parse_requirements(req_file, session=session)) + assert len(result) == 1 + assert result[0].name == req_name + assert not result[0].constraint + + def test_relative_local_nested_req_files( + self, session, monkeypatch, tmpdir + ): """ Test a relative nested req file path is joined with the req file dir """ - req_file = os.path.normpath('/path/req_file.txt') - - def parse(*args, **kwargs): - return iter([]) - mock_parse = Mock() - mock_parse.side_effect = parse - monkeypatch.setattr(pip._internal.req.req_file, 'parse_requirements', - mock_parse) - list(process_line("-r reqs.txt", req_file, 1, finder=finder)) - call = mock_parse.mock_calls[0] - assert call[1][0] == os.path.normpath('/path/reqs.txt') - - def test_absolute_local_nested_req_files(self, finder, monkeypatch): + req_name = 'hello' + req_file = tmpdir / 'parent' / 'req_file.txt' + req_file.parent.mkdir() + req_file.write_text('-r reqs.txt') + req_file.parent.joinpath('reqs.txt').write_text(req_name) + + monkeypatch.chdir(str(tmpdir)) + + reqs = list( + parse_requirements('./parent/req_file.txt', session=session) + ) + assert len(reqs) == 1 + assert reqs[0].name == req_name + assert not reqs[0].constraint + + def test_absolute_local_nested_req_files( + self, session, monkeypatch, tmpdir + ): """ Test an absolute nested req file path """ - req_file = '/path/req_file.txt' - - def parse(*args, **kwargs): - return iter([]) - mock_parse = Mock() - mock_parse.side_effect = parse - monkeypatch.setattr(pip._internal.req.req_file, 'parse_requirements', - mock_parse) - list(process_line("-r /other/reqs.txt", req_file, 1, finder=finder)) - call = mock_parse.mock_calls[0] - assert call[1][0] == '/other/reqs.txt' - - def test_absolute_http_nested_req_file_in_local(self, finder, monkeypatch): + req_name = 'hello' + req_file = tmpdir / 'parent' / 'req_file.txt' + req_file.parent.mkdir() + other_req_file = tmpdir / 'other' / 'reqs.txt' + other_req_file.parent.mkdir() + # POSIX-ify the path, since Windows backslashes aren't supported. + other_req_file_str = str(other_req_file).replace('\\', '/') + + req_file.write_text('-r {}'.format(other_req_file_str)) + other_req_file.write_text(req_name) + + reqs = list(parse_requirements(str(req_file), session=session)) + assert len(reqs) == 1 + assert reqs[0].name == req_name + assert not reqs[0].constraint + + def test_absolute_http_nested_req_file_in_local( + self, session, monkeypatch, tmpdir + ): """ Test a nested req file url in a local req file """ - req_file = '/path/req_file.txt' + req_name = 'hello' + req_file = tmpdir / 'req_file.txt' + nested_req_file = 'http://me.com/me/req_file.txt' + + def get_file_content(filename, *args, **kwargs): + if filename == str(req_file): + return None, '-r {}'.format(nested_req_file) + elif filename == nested_req_file: + return None, req_name + assert False, 'Unexpected file requested {}'.format(filename) + + monkeypatch.setattr( + pip._internal.req.req_file, 'get_file_content', get_file_content + ) - def parse(*args, **kwargs): - return iter([]) - mock_parse = Mock() - mock_parse.side_effect = parse - monkeypatch.setattr(pip._internal.req.req_file, 'parse_requirements', - mock_parse) - list(process_line("-r http://me.com/me/reqs.txt", req_file, 1, - finder=finder)) - call = mock_parse.mock_calls[0] - assert call[1][0] == 'http://me.com/me/reqs.txt' + result = list(parse_requirements(req_file, session=session)) + assert len(result) == 1 + assert result[0].name == req_name + assert not result[0].constraint class TestBreakOptionsArgs(object): @@ -524,24 +544,24 @@ class TestOptionVariants(object): # this suite is really just testing optparse, but added it anyway - def test_variant1(self, finder): - list(process_line("-i url", "file", 1, finder=finder)) + def test_variant1(self, line_processor, finder): + line_processor("-i url", "file", 1, finder=finder) assert finder.index_urls == ['url'] - def test_variant2(self, finder): - list(process_line("-i 'url'", "file", 1, finder=finder)) + def test_variant2(self, line_processor, finder): + line_processor("-i 'url'", "file", 1, finder=finder) assert finder.index_urls == ['url'] - def test_variant3(self, finder): - list(process_line("--index-url=url", "file", 1, finder=finder)) + def test_variant3(self, line_processor, finder): + line_processor("--index-url=url", "file", 1, finder=finder) assert finder.index_urls == ['url'] - def test_variant4(self, finder): - list(process_line("--index-url url", "file", 1, finder=finder)) + def test_variant4(self, line_processor, finder): + line_processor("--index-url url", "file", 1, finder=finder) assert finder.index_urls == ['url'] - def test_variant5(self, finder): - list(process_line("--index-url='url'", "file", 1, finder=finder)) + def test_variant5(self, line_processor, finder): + line_processor("--index-url='url'", "file", 1, finder=finder) assert finder.index_urls == ['url'] From d150b75f5b280fbd0a68aa4ca90d0580fd399a97 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 20 Oct 2019 13:05:59 +0530 Subject: [PATCH 0635/3170] Simplify RequirementTracker.add(req) - utilize try-except-else structure better - enables reducing a level of indentation - add comments to describe the flow of the method - drop once-or-twice use variables - --- src/pip/_internal/req/req_tracker.py | 34 +++++++++++++++++++--------- 1 file changed, 23 insertions(+), 11 deletions(-) diff --git a/src/pip/_internal/req/req_tracker.py b/src/pip/_internal/req/req_tracker.py index a5b5803b217..9d3d2c3077b 100644 --- a/src/pip/_internal/req/req_tracker.py +++ b/src/pip/_internal/req/req_tracker.py @@ -55,22 +55,34 @@ def _entry_path(self, link): def add(self, req): # type: (InstallRequirement) -> None - link = req.link - info = str(req) - entry_path = self._entry_path(link) + """Add an InstallRequirement to build tracking. + """ + + # Get the file to write information about this requirement. + entry_path = self._entry_path(req.link) + + # Try reading from the file. If it exists and can be read from, a build + # is already in progress, so a LookupError is raised. try: with open(entry_path) as fp: - # Error, these's already a build in progress. - raise LookupError('%s is already being built: %s' - % (link, fp.read())) + contents = fp.read() except IOError as e: + # if the error is anything other than "file does not exist", raise. if e.errno != errno.ENOENT: raise - assert req not in self._entries - with open(entry_path, 'w') as fp: - fp.write(info) - self._entries.add(req) - logger.debug('Added %s to build tracker %r', req, self._root) + else: + message = '%s is already being built: %s' % (req.link, contents) + raise LookupError(message) + + # If we're here, req should really not be building already. + assert req not in self._entries + + # Start tracking this requirement. + with open(entry_path, 'w') as fp: + fp.write(str(req)) + self._entries.add(req) + + logger.debug('Added %s to build tracker %r', req, self._root) def remove(self, req): # type: (InstallRequirement) -> None From 6837fc68471348562b97bcb89c497454daf9b3a1 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 20 Oct 2019 13:07:51 +0530 Subject: [PATCH 0636/3170] Polish up RequirementTracker.remove - add a docstring - drop a single-use variable - do file-system operations before in-memory operations (like add) --- src/pip/_internal/req/req_tracker.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/req/req_tracker.py b/src/pip/_internal/req/req_tracker.py index 9d3d2c3077b..ed4457d59b2 100644 --- a/src/pip/_internal/req/req_tracker.py +++ b/src/pip/_internal/req/req_tracker.py @@ -86,9 +86,13 @@ def add(self, req): def remove(self, req): # type: (InstallRequirement) -> None - link = req.link + """Remove an InstallRequirement from build tracking. + """ + + # Delete the created file and the corresponding entries. + os.unlink(self._entry_path(req.link)) self._entries.remove(req) - os.unlink(self._entry_path(link)) + logger.debug('Removed %s from build tracker %r', req, self._root) def cleanup(self): From f92efc022b38806b1dda7ed6c7ad0c7fcc44e2e3 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver <thomas@kluyver.me.uk> Date: Sun, 27 Oct 2019 10:09:15 +0000 Subject: [PATCH 0637/3170] Test that setting user in the config file works --- tests/functional/test_install.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 00a972f9652..f203ea43ce5 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1586,6 +1586,20 @@ def test_target_install_ignores_distutils_config_install_prefix(script): assert relative_script_base not in result.files_created +def test_user_config_accepted(script): + # user set in the config file is parsed as 0/1 instead of True/False. + # Check that this doesn't cause a problem. + config_file = script.scratch_path / 'pip.conf' + script.environ['PIP_CONFIG_FILE'] = str(config_file) + config_file.write_text("[install]\nuser = true") + result = script.pip_install_local('simplewheel') + + assert "Successfully installed simplewheel" in result.stdout + + relative_user = os.path.relpath(script.user_site_path, script.base_path) + assert join(relative_user, 'simplewheel') in result.files_created + + @pytest.mark.network @pytest.mark.skipif("sys.platform != 'win32'") @pytest.mark.parametrize('pip_name', [ From aaa4237adfcbab90373e4fdb1cf5597d7fe1f405 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Sun, 27 Oct 2019 17:52:19 +0100 Subject: [PATCH 0638/3170] refactor should_use_ephemeral_cache --- src/pip/_internal/wheel.py | 89 +++++++++++++++++++++++++++----------- tests/unit/test_wheel.py | 84 +++++++++++++++++++++++++++++++++++ 2 files changed, 147 insertions(+), 26 deletions(-) diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index 72168e5d831..79091e54b92 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -772,61 +772,98 @@ def _contains_egg_info( return bool(_egg_info_re.search(s)) -def should_use_ephemeral_cache( +def should_build( req, # type: InstallRequirement - should_unpack, # type: bool - cache_available, # type: bool + need_wheel, # type: bool check_binary_allowed, # type: BinaryAllowedPredicate ): # type: (...) -> Optional[bool] - """Return whether to build an InstallRequirement object using the - ephemeral cache. - - :param cache_available: whether a cache directory is available for the - should_unpack=True case. - - :return: True or False to build the requirement with ephem_cache=True - or False, respectively; or None not to build the requirement. - """ + """Return whether an InstallRequirement should be built into a wheel.""" if req.constraint: # never build requirements that are merely constraints - return None + return False if req.is_wheel: - if not should_unpack: + if need_wheel: logger.info( 'Skipping %s, due to already being wheel.', req.name, ) - return None - if not should_unpack: - # i.e. pip wheel, not pip install; - # return False, knowing that the caller will never cache - # in this case anyway, so this return merely means "build it". - # TODO improve this behavior return False + if need_wheel: + # i.e. pip wheel, not pip install + return True + if req.editable or not req.source_dir: - return None + return False if not check_binary_allowed(req): logger.info( "Skipping wheel build for %s, due to binaries " "being disabled for it.", req.name, ) - return None + return False + + return True + + +def should_cache( + req, # type: InstallRequirement + check_binary_allowed, # type: BinaryAllowedPredicate +): + # type: (...) -> Optional[bool] + """ + Return whether a built InstallRequirement can be stored in the persistent + wheel cache, assuming the wheel cache is available, and should_build() + has determined a wheel needs to be built. + """ + if req.editable or not req.source_dir: + return False + + if not check_binary_allowed(req): + return False if req.link and req.link.is_vcs: # VCS checkout. Build wheel just for this run. - return True + return False link = req.link base, ext = link.splitext() - if cache_available and _contains_egg_info(base): - return False + if _contains_egg_info(base): + return True # Otherwise, build the wheel just for this run using the ephemeral # cache since we are either in the case of e.g. a local directory, or # no cache directory is available to use. - return True + return False + + +def should_use_ephemeral_cache( + req, # type: InstallRequirement + should_unpack, # type: bool + cache_available, # type: bool + check_binary_allowed, # type: BinaryAllowedPredicate +): + # type: (...) -> Optional[bool] + """Return whether to build an InstallRequirement object using the + ephemeral cache. + + :param cache_available: whether a cache directory is available for the + should_unpack=True case. + + :return: True or False to build the requirement with ephem_cache=True + or False, respectively; or None not to build the requirement. + """ + if not should_build(req, not should_unpack, check_binary_allowed): + return None + if not should_unpack: + # i.e. pip wheel, not pip install; + # return False, knowing that the caller will never cache + # in this case anyway, so this return merely means "build it". + # TODO improve this behavior + return False + if not cache_available: + return True + return not should_cache(req, check_binary_allowed) def format_command_result( diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 707451d92d2..b530bcb6920 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -23,6 +23,25 @@ from tests.lib import DATA_DIR, assert_paths_equal +class ReqMock: + + def __init__( + self, + name="pendulum", + is_wheel=False, + editable=False, + link=None, + constraint=False, + source_dir="/tmp/pip-install-123/pendulum", + ): + self.name = name + self.is_wheel = is_wheel + self.editable = editable + self.link = link + self.constraint = constraint + self.source_dir = source_dir + + @pytest.mark.parametrize( "s, expected", [ @@ -82,6 +101,71 @@ def test_format_tag(file_tag, expected): assert actual == expected +@pytest.mark.parametrize( + "req, need_wheel, disallow_binaries, expected", + [ + # pip wheel (need_wheel=True) + (ReqMock(), True, False, True), + (ReqMock(), True, True, True), + (ReqMock(constraint=True), True, False, False), + (ReqMock(is_wheel=True), True, False, False), + (ReqMock(editable=True), True, False, True), + (ReqMock(source_dir=None), True, False, True), + (ReqMock(link=Link("git+https://g.c/org/repo")), True, False, True), + (ReqMock(link=Link("git+https://g.c/org/repo")), True, True, True), + # pip install (need_wheel=False) + (ReqMock(), False, False, True), + (ReqMock(), False, True, False), + (ReqMock(constraint=True), False, False, False), + (ReqMock(is_wheel=True), False, False, False), + (ReqMock(editable=True), False, False, False), + (ReqMock(source_dir=None), False, False, False), + # By default (i.e. when binaries are allowed), VCS requirements + # should be built in install mode. + (ReqMock(link=Link("git+https://g.c/org/repo")), False, False, True), + # Disallowing binaries, however, should cause them not to be built. + (ReqMock(link=Link("git+https://g.c/org/repo")), False, True, False), + ], +) +def test_should_build(req, need_wheel, disallow_binaries, expected): + should_build = wheel.should_build( + req, + need_wheel, + check_binary_allowed=lambda req: not disallow_binaries, + ) + assert should_build is expected + + +@pytest.mark.parametrize( + "req, disallow_binaries, expected", + [ + (ReqMock(editable=True), False, False), + (ReqMock(source_dir=None), False, False), + (ReqMock(link=Link("git+https://g.c/org/repo")), False, False), + (ReqMock(link=Link("https://g.c/dist.tgz")), False, False), + (ReqMock(link=Link("https://g.c/dist-2.0.4.tgz")), False, True), + (ReqMock(editable=True), True, False), + (ReqMock(source_dir=None), True, False), + (ReqMock(link=Link("git+https://g.c/org/repo")), True, False), + (ReqMock(link=Link("https://g.c/dist.tgz")), True, False), + (ReqMock(link=Link("https://g.c/dist-2.0.4.tgz")), True, False), + ], +) +def test_should_cache( + req, disallow_binaries, expected +): + def check_binary_allowed(req): + return not disallow_binaries + + should_cache = wheel.should_cache(req, check_binary_allowed) + if not wheel.should_build( + req, need_wheel=False, check_binary_allowed=check_binary_allowed + ): + # never cache if pip install (need_wheel=False) would not have built) + assert not should_cache + assert should_cache is expected + + @pytest.mark.parametrize( "base_name, should_unpack, cache_available, expected", [ From e638e24e10e53a1d30d5ab89dd0b226edc2aacaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Sun, 27 Oct 2019 18:16:47 +0100 Subject: [PATCH 0639/3170] do not repeat tests in should_cache --- src/pip/_internal/wheel.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index 79091e54b92..451ca57e8b2 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -816,10 +816,11 @@ def should_cache( wheel cache, assuming the wheel cache is available, and should_build() has determined a wheel needs to be built. """ - if req.editable or not req.source_dir: - return False - - if not check_binary_allowed(req): + if not should_build( + req, need_wheel=False, check_binary_allowed=check_binary_allowed + ): + # never cache if pip install (need_wheel=False) would not have built + # (editable mode, etc) return False if req.link and req.link.is_vcs: From ceaf75b9ede9a9c25bcee84fe512fa6774889685 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Mon, 28 Oct 2019 00:16:42 -0400 Subject: [PATCH 0640/3170] Put Temp on RAM Disk for Azure Pipelines tests Profiling on Azure Pipelines indicates that the majority of our time is spent waiting for filesystem operations to complete. As a quick way to improve our test speed in this area, we can do operations on a RAM disk instead of the default SSDs. --- .azure-pipelines/scripts/New-RAMDisk.ps1 | 74 ++++++++++++++++++++ .azure-pipelines/steps/run-tests-windows.yml | 22 +++++- 2 files changed, 93 insertions(+), 3 deletions(-) create mode 100644 .azure-pipelines/scripts/New-RAMDisk.ps1 diff --git a/.azure-pipelines/scripts/New-RAMDisk.ps1 b/.azure-pipelines/scripts/New-RAMDisk.ps1 new file mode 100644 index 00000000000..21b1a573a49 --- /dev/null +++ b/.azure-pipelines/scripts/New-RAMDisk.ps1 @@ -0,0 +1,74 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory=$true, + HelpMessage="Drive letter to use for the RAMDisk")] + [String]$drive, + [Parameter(HelpMessage="Size to allocate to the RAMDisk")] + [UInt64]$size=1GB +) + +$ErrorActionPreference = "Stop" +Set-StrictMode -Version Latest + +Write-Output "Installing FS-iSCSITarget-Server" +Install-WindowsFeature -Name FS-iSCSITarget-Server + +Write-Output "Starting MSiSCSI" +Start-Service MSiSCSI +$retry = 10 +do { + $service = Get-Service MSiSCSI + if ($service.Status -eq "Running") { + break; + } + $retry-- + Start-Sleep -Milliseconds 500 +} until ($retry -eq 0) + +$service = Get-Service MSiSCSI +if ($service.Status -ne "Running") { + throw "MSiSCSI is not running" +} + +Write-Output "Configuring Firewall" +Get-NetFirewallServiceFilter -Service MSiSCSI | Enable-NetFirewallRule + +Write-Output "Configuring RAMDisk" +# Must use external-facing IP address, otherwise New-IscsiTargetPortal is +# unable to connect. +$ip = ( + Get-NetIPAddress -AddressFamily IPv4 | + Where-Object {$_.IPAddress -ne "127.0.0.1"} +)[0].IPAddress +if ( + -not (Get-IscsiServerTarget -ComputerName localhost | Where-Object {$_.TargetName -eq "ramdisks"}) +) { + New-IscsiServerTarget ` + -ComputerName localhost ` + -TargetName ramdisks ` + -InitiatorId IPAddress:$ip +} + +$newVirtualDisk = New-IscsiVirtualDisk ` + -ComputerName localhost ` + -Path ramdisk:local$drive.vhdx ` + -Size $size +Add-IscsiVirtualDiskTargetMapping ` + -ComputerName localhost ` + -TargetName ramdisks ` + -Path ramdisk:local$drive.vhdx + +Write-Output "Connecting to iSCSI" +New-IscsiTargetPortal -TargetPortalAddress $ip +Get-IscsiTarget | Where-Object {!$_.IsConnected} | Connect-IscsiTarget + +Write-Output "Configuring disk" +$newDisk = Get-IscsiConnection | + Get-Disk | + Where-Object {$_.SerialNumber -eq $newVirtualDisk.SerialNumber} + +Set-Disk -InputObject $newDisk -IsOffline $false +Initialize-Disk -InputObject $newDisk -PartitionStyle MBR +New-Partition -InputObject $newDisk -UseMaximumSize -DriveLetter $drive + +Format-Volume -DriveLetter $drive -NewFileSystemLabel Temp -FileSystem NTFS diff --git a/.azure-pipelines/steps/run-tests-windows.yml b/.azure-pipelines/steps/run-tests-windows.yml index 6ce5d1cc010..9a992f46f81 100644 --- a/.azure-pipelines/steps/run-tests-windows.yml +++ b/.azure-pipelines/steps/run-tests-windows.yml @@ -8,10 +8,28 @@ steps: versionSpec: '$(python.version)' architecture: '$(python.architecture)' +- task: PowerShell@2 + inputs: + filePath: .azure-pipelines/scripts/New-RAMDisk.ps1 + arguments: "-Drive R -Size 1GB" + displayName: Setup RAMDisk + +- powershell: | + mkdir R:\Temp + $acl = Get-Acl "R:\Temp" + $rule = New-Object System.Security.AccessControl.FileSystemAccessRule( + "Everyone", "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow" + ) + $acl.AddAccessRule($rule) + Set-Acl "R:\Temp" $acl + displayName: Set RAMDisk Permissions + - bash: pip install --upgrade setuptools tox displayName: Install Tox - script: tox -e py -- -m unit -n 3 --junit-xml=junit/unit-test.xml + env: + TEMP: "R:\\Temp" displayName: Tox run unit tests - ${{ if eq(parameters.runIntegrationTests, 'true') }}: @@ -23,9 +41,7 @@ steps: # Shorten paths to get under MAX_PATH or else integration tests will fail # https://bugs.python.org/issue18199 - subst T: $env:TEMP - $env:TEMP = "T:\" - $env:TMP = "T:\" + $env:TEMP = "R:\Temp" tox -e py -- -m integration -n 3 --duration=5 --junit-xml=junit/integration-test.xml displayName: Tox run integration tests From a7edcc730e2df40cbe42627ff2622fbec253d5db Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 29 Oct 2019 02:04:55 -0400 Subject: [PATCH 0641/3170] Normalize parallelization parameter. --- .appveyor.yml | 4 ++-- .azure-pipelines/steps/run-tests-windows.yml | 4 ++-- .azure-pipelines/steps/run-tests.yml | 6 +++--- tools/travis/run.sh | 6 +++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index bc76d6919ed..3852a82327e 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -64,8 +64,8 @@ test_script: subst T: $env:TEMP $env:TEMP = "T:\" $env:TMP = "T:\" - tox -e py -- -m unit + tox -e py -- -m unit -n auto if ($LastExitCode -eq 0 -and $env:RUN_INTEGRATION_TESTS -eq "True") { - tox -e py -- --use-venv -m integration -n2 --durations=20 + tox -e py -- --use-venv -m integration -n auto --durations=20 } } diff --git a/.azure-pipelines/steps/run-tests-windows.yml b/.azure-pipelines/steps/run-tests-windows.yml index 9a992f46f81..9926754b0aa 100644 --- a/.azure-pipelines/steps/run-tests-windows.yml +++ b/.azure-pipelines/steps/run-tests-windows.yml @@ -27,7 +27,7 @@ steps: - bash: pip install --upgrade setuptools tox displayName: Install Tox -- script: tox -e py -- -m unit -n 3 --junit-xml=junit/unit-test.xml +- script: tox -e py -- -m unit -n auto --junit-xml=junit/unit-test.xml env: TEMP: "R:\\Temp" displayName: Tox run unit tests @@ -43,7 +43,7 @@ steps: # https://bugs.python.org/issue18199 $env:TEMP = "R:\Temp" - tox -e py -- -m integration -n 3 --duration=5 --junit-xml=junit/integration-test.xml + tox -e py -- -m integration -n auto --duration=5 --junit-xml=junit/integration-test.xml displayName: Tox run integration tests - task: PublishTestResults@2 diff --git a/.azure-pipelines/steps/run-tests.yml b/.azure-pipelines/steps/run-tests.yml index 2682e085fe7..64163a5be8b 100644 --- a/.azure-pipelines/steps/run-tests.yml +++ b/.azure-pipelines/steps/run-tests.yml @@ -7,14 +7,14 @@ steps: - bash: pip install --upgrade setuptools tox displayName: Install Tox -- script: tox -e py -- -m unit --junit-xml=junit/unit-test.xml +- script: tox -e py -- -m unit -n auto --junit-xml=junit/unit-test.xml displayName: Tox run unit tests # Run integration tests in two groups so we will fail faster if there is a failure in the first group -- script: tox -e py -- -m integration -n 4 --duration=5 -k "not test_install" --junit-xml=junit/integration-test-group0.xml +- script: tox -e py -- -m integration -n auto --duration=5 -k "not test_install" --junit-xml=junit/integration-test-group0.xml displayName: Tox run Group 0 integration tests -- script: tox -e py -- -m integration -n 4 --duration=5 -k "test_install" --junit-xml=junit/integration-test-group1.xml +- script: tox -e py -- -m integration -n auto --duration=5 -k "test_install" --junit-xml=junit/integration-test-group1.xml displayName: Tox run Group 1 integration tests - task: PublishTestResults@2 diff --git a/tools/travis/run.sh b/tools/travis/run.sh index aea29349c5f..86f975f4f16 100755 --- a/tools/travis/run.sh +++ b/tools/travis/run.sh @@ -41,12 +41,12 @@ echo "TOXENV=${TOXENV}" set -x if [[ "$GROUP" == "1" ]]; then # Unit tests - tox -- --use-venv -m unit + tox -- --use-venv -m unit -n auto # Integration tests (not the ones for 'pip install') - tox -- --use-venv -m integration -n 4 --duration=5 -k "not test_install" + tox -- --use-venv -m integration -n auto --duration=5 -k "not test_install" elif [[ "$GROUP" == "2" ]]; then # Separate Job for running integration tests for 'pip install' - tox -- --use-venv -m integration -n 4 --duration=5 -k "test_install" + tox -- --use-venv -m integration -n auto --duration=5 -k "test_install" else # Non-Testing Jobs should run once tox From cdf09bfc4a6181ab2d39a612add7ad089c8045e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Tue, 29 Oct 2019 09:01:05 +0100 Subject: [PATCH 0642/3170] remove should_use_ephemeral_cache --- news/7268.trivial | 1 + src/pip/_internal/wheel.py | 46 +++++------------------- tests/unit/test_wheel.py | 72 -------------------------------------- 3 files changed, 10 insertions(+), 109 deletions(-) create mode 100644 news/7268.trivial diff --git a/news/7268.trivial b/news/7268.trivial new file mode 100644 index 00000000000..052c8279189 --- /dev/null +++ b/news/7268.trivial @@ -0,0 +1 @@ +refactoring: remove should_use_ephemeral_cache diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index 451ca57e8b2..d95ff66a474 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -838,35 +838,6 @@ def should_cache( return False -def should_use_ephemeral_cache( - req, # type: InstallRequirement - should_unpack, # type: bool - cache_available, # type: bool - check_binary_allowed, # type: BinaryAllowedPredicate -): - # type: (...) -> Optional[bool] - """Return whether to build an InstallRequirement object using the - ephemeral cache. - - :param cache_available: whether a cache directory is available for the - should_unpack=True case. - - :return: True or False to build the requirement with ephem_cache=True - or False, respectively; or None not to build the requirement. - """ - if not should_build(req, not should_unpack, check_binary_allowed): - return None - if not should_unpack: - # i.e. pip wheel, not pip install; - # return False, knowing that the caller will never cache - # in this case anyway, so this return merely means "build it". - # TODO improve this behavior - return False - if not cache_available: - return True - return not should_cache(req, check_binary_allowed) - - def format_command_result( command_args, # type: List[str] command_output, # type: str @@ -1106,23 +1077,24 @@ def build( cache_available = bool(self.wheel_cache.cache_dir) for req in requirements: - ephem_cache = should_use_ephemeral_cache( + if not should_build( req, - should_unpack=should_unpack, - cache_available=cache_available, + need_wheel=not should_unpack, check_binary_allowed=self.check_binary_allowed, - ) - if ephem_cache is None: + ): continue # Determine where the wheel should go. if should_unpack: - if ephem_cache: + if ( + cache_available and + should_cache(req, self.check_binary_allowed) + ): + output_dir = self.wheel_cache.get_path_for_link(req.link) + else: output_dir = self.wheel_cache.get_ephem_path_for_link( req.link ) - else: - output_dir = self.wheel_cache.get_path_for_link(req.link) else: output_dir = self._wheel_dir diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index b530bcb6920..05413f9074e 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -166,78 +166,6 @@ def check_binary_allowed(req): assert should_cache is expected -@pytest.mark.parametrize( - "base_name, should_unpack, cache_available, expected", - [ - ('pendulum-2.0.4', False, False, False), - # The following cases test should_unpack=True. - # Test _contains_egg_info() returning True. - ('pendulum-2.0.4', True, True, False), - ('pendulum-2.0.4', True, False, True), - # Test _contains_egg_info() returning False. - ('pendulum', True, True, True), - ('pendulum', True, False, True), - ], -) -def test_should_use_ephemeral_cache__issue_6197( - base_name, should_unpack, cache_available, expected, -): - """ - Regression test for: https://github.com/pypa/pip/issues/6197 - """ - req = make_test_install_req(base_name=base_name) - assert not req.is_wheel - assert not req.link.is_vcs - - always_true = Mock(return_value=True) - - ephem_cache = wheel.should_use_ephemeral_cache( - req, should_unpack=should_unpack, - cache_available=cache_available, check_binary_allowed=always_true, - ) - assert ephem_cache is expected - - -@pytest.mark.parametrize( - "disallow_binaries, expected", - [ - # By default (i.e. when binaries are allowed), VCS requirements - # should be built. - (False, True), - # Disallowing binaries, however, should cause them not to be built. - (True, None), - ], -) -def test_should_use_ephemeral_cache__disallow_binaries_and_vcs_checkout( - disallow_binaries, expected, -): - """ - Test that disallowing binaries (e.g. from passing --global-option) - causes should_use_ephemeral_cache() to return None for VCS checkouts. - """ - req = Requirement('pendulum') - link = Link(url='git+https://git.example.com/pendulum.git') - req = InstallRequirement( - req=req, - comes_from=None, - constraint=False, - editable=False, - link=link, - source_dir='/tmp/pip-install-9py5m2z1/pendulum', - ) - assert not req.is_wheel - assert req.link.is_vcs - - check_binary_allowed = Mock(return_value=not disallow_binaries) - - # The cache_available value doesn't matter for this test. - ephem_cache = wheel.should_use_ephemeral_cache( - req, should_unpack=True, - cache_available=True, check_binary_allowed=check_binary_allowed, - ) - assert ephem_cache is expected - - def test_format_command_result__INFO(caplog): caplog.set_level(logging.INFO) actual = wheel.format_command_result( From d8567d3b6601049934fdd0a780e8ff56a1504b70 Mon Sep 17 00:00:00 2001 From: jenix21 <devfrog@gmail.com> Date: Wed, 30 Oct 2019 00:33:15 +0900 Subject: [PATCH 0643/3170] address review comment --- tests/unit/test_command_install.py | 49 ++++++++++++++---------------- 1 file changed, 23 insertions(+), 26 deletions(-) diff --git a/tests/unit/test_command_install.py b/tests/unit/test_command_install.py index c7dd43b85b0..6e10fd73a58 100644 --- a/tests/unit/test_command_install.py +++ b/tests/unit/test_command_install.py @@ -102,37 +102,34 @@ def test_most_cases( assert decide_user_install(use_user_site=None) is result -def error_creation_helper(with_errno=False): - env_error = EnvironmentError("No file permission") - if with_errno: - env_error.errno = errno.EACCES - return env_error - - @pytest.mark.parametrize('error, show_traceback, using_user_site, expected', [ # show_traceback = True, using_user_site = True - (error_creation_helper(), True, True, 'Could not install packages due to' - ' an EnvironmentError.\n'), - (error_creation_helper(True), True, True, 'Could not install packages due' - ' to an EnvironmentError.\nCheck the permissions.\n'), + (EnvironmentError("Illegal byte sequence"), True, True, 'Could not install' + ' packages due to an EnvironmentError.\n'), + (EnvironmentError(errno.EACCES, "No file permission"), True, True, 'Could' + ' not install packages due to an EnvironmentError.\nCheck the' + ' permissions.\n'), # show_traceback = True, using_user_site = False - (error_creation_helper(), True, False, 'Could not install packages due to' - ' an EnvironmentError.\n'), - (error_creation_helper(True), True, False, 'Could not install packages due' - ' to an EnvironmentError.\nConsider using the `--user` option or check' - ' the permissions.\n'), + (EnvironmentError("Illegal byte sequence"), True, False, 'Could not' + ' install packages due to an EnvironmentError.\n'), + (EnvironmentError(errno.EACCES, "No file permission"), True, False, 'Could' + ' not install packages due to an EnvironmentError.\nConsider using the' + ' `--user` option or check the permissions.\n'), # show_traceback = False, using_user_site = True - (error_creation_helper(), False, True, 'Could not install packages due to' - ' an EnvironmentError: No file permission\n'), - (error_creation_helper(True), False, True, 'Could not install packages due' - ' to an EnvironmentError: No file permission\nCheck the' - ' permissions.\n'), + (EnvironmentError("Illegal byte sequence"), False, True, 'Could not' + ' install packages due to an EnvironmentError: Illegal byte' + ' sequence\n'), + (EnvironmentError(errno.EACCES, "No file permission"), False, True, 'Could' + ' not install packages due to an EnvironmentError: [Errno 13] No file' + ' permission\nCheck the permissions.\n'), # show_traceback = False, using_user_site = False - (error_creation_helper(), False, False, 'Could not install packages due to' - ' an EnvironmentError: No file permission\n'), - (error_creation_helper(True), False, False, 'Could not install packages' - ' due to an EnvironmentError: No file permission\nConsider using the' - ' `--user` option or check the permissions.\n'), + (EnvironmentError("Illegal byte sequence"), False, False, 'Could not' + ' install packages due to an EnvironmentError: Illegal byte sequence' + '\n'), + (EnvironmentError(errno.EACCES, "No file permission"), False, False, + 'Could not install packages due to an EnvironmentError: [Errno 13] No' + ' file permission\nConsider using the `--user` option or check the' + ' permissions.\n'), ]) def test_create_env_error_message( error, show_traceback, using_user_site, expected From 0597a6d9fb0163f27ebcc97b1890636139f5e372 Mon Sep 17 00:00:00 2001 From: Xavier Fernandez <xavier.fernandez@polyconseil.fr> Date: Tue, 29 Oct 2019 14:17:25 +0100 Subject: [PATCH 0644/3170] CI: move Python35-x64 from Appveyor to Azure Since Appveyor provides less runner and is often the bottleneck --- .appveyor.yml | 2 -- .azure-pipelines/jobs/test-windows.yml | 5 ++++- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/.appveyor.yml b/.appveyor.yml index 3852a82327e..0da6202b2cd 100644 --- a/.appveyor.yml +++ b/.appveyor.yml @@ -3,8 +3,6 @@ environment: # Unit and integration tests. - PYTHON: "C:\\Python27-x64" RUN_INTEGRATION_TESTS: "True" - - PYTHON: "C:\\Python35-x64" - RUN_INTEGRATION_TESTS: "True" - PYTHON: "C:\\Python36-x64" RUN_INTEGRATION_TESTS: "True" # Unit tests only. diff --git a/.azure-pipelines/jobs/test-windows.yml b/.azure-pipelines/jobs/test-windows.yml index ee869c87c89..3d25587de41 100644 --- a/.azure-pipelines/jobs/test-windows.yml +++ b/.azure-pipelines/jobs/test-windows.yml @@ -12,10 +12,13 @@ jobs: Python27-x86: python.version: '2.7' python.architecture: x86 + Python35-x64: + python.version: '3.5' + python.architecture: x64 Python37-x64: python.version: '3.7' python.architecture: x64 - maxParallel: 2 + maxParallel: 3 steps: - template: ../steps/run-tests-windows.yml From 3d3f6637012cfdf92415b2206e58a0cb7b601e57 Mon Sep 17 00:00:00 2001 From: Maxim Kurnikov <maxim.kurnikov@gmail.com> Date: Sat, 28 Sep 2019 18:17:53 +0300 Subject: [PATCH 0645/3170] set disallow_untyped_defs=True for pip._internal.wheel --- src/pip/_internal/wheel.py | 60 +++++++++++++++++++++++++++++++------- 1 file changed, 50 insertions(+), 10 deletions(-) diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index 451ca57e8b2..a8d0857c906 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -3,7 +3,6 @@ # The following comment should be removed at some point in the future. # mypy: strict-optional=False -# mypy: disallow-untyped-defs=False from __future__ import absolute_import @@ -57,7 +56,7 @@ if MYPY_CHECK_RUNNING: from typing import ( Dict, List, Optional, Sequence, Mapping, Tuple, IO, Text, Any, - Iterable, Callable, Set, Union, + Iterable, Callable, Set, Pattern, Union, ) from pip._internal.req.req_install import InstallRequirement from pip._internal.operations.prepare import ( @@ -78,6 +77,7 @@ def normpath(src, p): + # type: (str, str) -> str return os.path.relpath(src, p).replace(os.path.sep, '/') @@ -185,10 +185,12 @@ def get_entrypoints(filename): gui = entry_points.get('gui_scripts', {}) def _split_ep(s): + # type: (pkg_resources.EntryPoint) -> Tuple[str, str] """get the string representation of EntryPoint, remove space and split on '=' """ - return str(s).replace(" ", "").split("=") + split_parts = str(s).replace(" ", "").split("=") + return split_parts[0], split_parts[1] # convert the EntryPoint objects into strings with module:function console = dict(_split_ep(v) for v in console.values()) @@ -317,6 +319,7 @@ class MissingCallableSuffix(Exception): def _raise_for_invalid_entrypoint(specification): + # type: (str) -> None entry = get_export_entry(specification) if entry is not None and entry.suffix is None: raise MissingCallableSuffix(str(entry)) @@ -324,6 +327,7 @@ def _raise_for_invalid_entrypoint(specification): class PipScriptMaker(ScriptMaker): def make(self, specification, options=None): + # type: (str, Dict[str, Any]) -> List[str] _raise_for_invalid_entrypoint(specification) return super(PipScriptMaker, self).make(specification, options) @@ -378,6 +382,7 @@ def install_unpacked_wheel( logger.debug(stdout.getvalue()) def record_installed(srcfile, destfile, modified=False): + # type: (str, str, bool) -> None """Map archive RECORD paths to installation RECORD paths.""" oldpath = normpath(srcfile, wheeldir) newpath = normpath(destfile, lib_dir) @@ -385,7 +390,14 @@ def record_installed(srcfile, destfile, modified=False): if modified: changed.add(destfile) - def clobber(source, dest, is_base, fixer=None, filter=None): + def clobber( + source, # type: str + dest, # type: str + is_base, # type: bool + fixer=None, # type: Optional[Callable[[str], Any]] + filter=None # type: Optional[Callable[[str], bool]] + ): + # type: (...) -> None ensure_dir(dest) # common for the 'include' path for dir, subdirs, files in os.walk(source): @@ -469,6 +481,7 @@ def clobber(source, dest, is_base, fixer=None, filter=None): console, gui = get_entrypoints(ep_file) def is_entrypoint_wrapper(name): + # type: (str) -> bool # EP, EP.exe and EP-script.py are scripts generated for # entry point EP by setuptools if name.lower().endswith('.exe'): @@ -765,6 +778,7 @@ def supported(self, tags): def _contains_egg_info( s, _egg_info_re=re.compile(r'([a-z0-9_.]+)-([a-z0-9_.!+-]+)', re.I)): + # type: (str, Pattern) -> bool """Determine whether the string looks like an egg_info. :param s: The string to parse. E.g. foo-2.1 @@ -869,7 +883,7 @@ def should_use_ephemeral_cache( def format_command_result( command_args, # type: List[str] - command_output, # type: str + command_output, # type: Text ): # type: (...) -> str """Format command information for logging.""" @@ -893,7 +907,7 @@ def get_legacy_build_wheel_path( temp_dir, # type: str req, # type: InstallRequirement command_args, # type: List[str] - command_output, # type: str + command_output, # type: Text ): # type: (...) -> Optional[str] """Return the path to the wheel in the temporary build directory.""" @@ -919,6 +933,7 @@ def get_legacy_build_wheel_path( def _always_true(_): + # type: (Any) -> bool return True @@ -954,7 +969,13 @@ def __init__( # file names of built wheel names self.wheel_filenames = [] # type: List[Union[bytes, Text]] - def _build_one(self, req, output_dir, python_tag=None): + def _build_one( + self, + req, # type: InstallRequirement + output_dir, # type: str + python_tag=None, # type: Optional[str] + ): + # type: (...) -> Optional[str] """Build one wheel. :return: The filename of the built wheel, or None if the build failed. @@ -964,7 +985,13 @@ def _build_one(self, req, output_dir, python_tag=None): 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): + def _build_one_inside_env( + self, + req, # type: InstallRequirement + output_dir, # type: str + python_tag=None, # type: Optional[str] + ): + # type: (...) -> Optional[str] with TempDirectory(kind="wheel") as temp_dir: if req.use_pep517: builder = self._build_one_pep517 @@ -989,7 +1016,13 @@ def _build_one_inside_env(self, req, output_dir, python_tag=None): self._clean_one(req) return None - def _build_one_pep517(self, req, tempd, python_tag=None): + def _build_one_pep517( + self, + req, # type: InstallRequirement + tempd, # type: str + python_tag=None, # type: Optional[str] + ): + # type: (...) -> Optional[str] """Build one InstallRequirement using the PEP 517 build process. Returns path to wheel if successfully built. Otherwise, returns None. @@ -1028,7 +1061,13 @@ def _build_one_pep517(self, req, tempd, python_tag=None): return None return os.path.join(tempd, wheel_name) - def _build_one_legacy(self, req, tempd, python_tag=None): + def _build_one_legacy( + self, + req, # type: InstallRequirement + tempd, # type: str + python_tag=None, # type: Optional[str] + ): + # type: (...) -> Optional[str] """Build one InstallRequirement using the "legacy" build process. Returns path to wheel if successfully built. Otherwise, returns None. @@ -1067,6 +1106,7 @@ def _build_one_legacy(self, req, tempd, python_tag=None): return wheel_path def _clean_one(self, req): + # type: (InstallRequirement) -> bool clean_args = make_setuptools_clean_args( req.setup_py_path, global_options=self.global_options, From bc20cac8482b424ed512d25ab946f9c7c517dd38 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 29 Oct 2019 19:08:36 -0400 Subject: [PATCH 0646/3170] Update pypy on Travis --- .travis.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.travis.yml b/.travis.yml index c61e41702be..0c539b6e19a 100644 --- a/.travis.yml +++ b/.travis.yml @@ -30,13 +30,13 @@ jobs: # PyPy - stage: secondary env: GROUP=1 - python: pypy3.5-6.0 + python: pypy3.5-7.0.0 - env: GROUP=2 - python: pypy3.5-6.0 + python: pypy3.5-7.0.0 - env: GROUP=1 - python: pypy2.7-6.0 + python: pypy2.7-7.1.1 - env: GROUP=2 - python: pypy2.7-6.0 + python: pypy2.7-7.1.1 # Other Supported CPython - env: GROUP=1 python: 3.6 From 9dc065972fd3d3851988c9e4cecc4faad11e144c Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 29 Oct 2019 21:20:19 -0400 Subject: [PATCH 0647/3170] Return InstallationResult from requirement installation --- src/pip/_internal/commands/install.py | 8 ++++---- src/pip/_internal/req/__init__.py | 18 ++++++++++++++++-- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index bf4fbe3c777..1e8741ac471 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -463,13 +463,13 @@ def run(self, options, args): ) working_set = pkg_resources.WorkingSet(lib_locations) - reqs = sorted(installed, key=operator.attrgetter('name')) + installed.sort(key=operator.attrgetter('name')) items = [] - for req in reqs: - item = req.name + for result in installed: + item = result.name try: installed_version = get_installed_version( - req.name, working_set=working_set + result.name, working_set=working_set ) if installed_version: item += '-' + installed_version diff --git a/src/pip/_internal/req/__init__.py b/src/pip/_internal/req/__init__.py index 993f23a238a..e5176dae5e4 100644 --- a/src/pip/_internal/req/__init__.py +++ b/src/pip/_internal/req/__init__.py @@ -23,6 +23,16 @@ logger = logging.getLogger(__name__) +class InstallationResult(object): + def __init__(self, name): + # type: (str) -> None + self.name = name + + def __repr__(self): + # type: () -> str + return "InstallationResult(name={!r})".format(self.name) + + def install_given_reqs( to_install, # type: List[InstallRequirement] install_options, # type: List[str] @@ -30,7 +40,7 @@ def install_given_reqs( *args, # type: Any **kwargs # type: Any ): - # type: (...) -> List[InstallRequirement] + # type: (...) -> List[InstallationResult] """ Install everything in the given list. @@ -43,6 +53,8 @@ def install_given_reqs( ', '.join([req.name for req in to_install]), ) + installed = [] + with indent_log(): for requirement in to_install: if requirement.conflicts_with: @@ -79,4 +91,6 @@ def install_given_reqs( uninstalled_pathset.commit() requirement.remove_temporary_source() - return to_install + installed.append(InstallationResult(requirement.name)) + + return installed From ae4ad85e5f890798303f3d5a1c6c437173957a69 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 29 Oct 2019 23:41:34 -0400 Subject: [PATCH 0648/3170] Make virtualenv/script fixture factories --- tests/conftest.py | 69 +++++++++++++++++++++++++++++------------------ 1 file changed, 43 insertions(+), 26 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 7a54373b8ec..816a08877bd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -272,16 +272,23 @@ def virtualenv_template(request, tmpdir_factory, pip_src, yield venv +@pytest.fixture(scope="session") +def virtualenv_factory(virtualenv_template): + def factory(tmpdir): + return VirtualEnvironment(tmpdir, virtualenv_template) + + return factory + + @pytest.fixture -def virtualenv(virtualenv_template, tmpdir, isolate): +def virtualenv(virtualenv_factory, tmpdir): """ Return a virtual environment which is unique to each test function invocation created inside of a sub directory of the test function's temporary directory. The returned object is a ``tests.lib.venv.VirtualEnvironment`` object. """ - venv_location = tmpdir.joinpath("workspace", "venv") - yield VirtualEnvironment(venv_location, virtualenv_template) + yield virtualenv_factory(tmpdir.joinpath("workspace", "venv")) @pytest.fixture @@ -289,35 +296,45 @@ def with_wheel(virtualenv, wheel_install): install_egg_link(virtualenv, 'wheel', wheel_install) +@pytest.fixture(scope="session") +def script_factory(virtualenv_factory, deprecated_python): + def factory(tmpdir, virtualenv=None): + if virtualenv is None: + virtualenv = virtualenv_factory(tmpdir.joinpath("venv")) + return PipTestEnvironment( + # The base location for our test environment + tmpdir, + + # Tell the Test Environment where our virtualenv is located + virtualenv=virtualenv, + + # Do not ignore hidden files, they need to be checked as well + ignore_hidden=False, + + # We are starting with an already empty directory + start_clear=False, + + # We want to ensure no temporary files are left behind, so the + # PipTestEnvironment needs to capture and assert against temp + capture_temp=True, + assert_no_temp=True, + + # Deprecated python versions produce an extra deprecation warning + pip_expect_warning=deprecated_python, + ) + + return factory + + @pytest.fixture -def script(tmpdir, virtualenv, deprecated_python): +def script(tmpdir, virtualenv, script_factory): """ Return a PipTestEnvironment which is unique to each test function and will execute all commands inside of the unique virtual environment for this test function. The returned object is a ``tests.lib.scripttest.PipTestEnvironment``. """ - return PipTestEnvironment( - # The base location for our test environment - tmpdir.joinpath("workspace"), - - # Tell the Test Environment where our virtualenv is located - virtualenv=virtualenv, - - # Do not ignore hidden files, they need to be checked as well - ignore_hidden=False, - - # We are starting with an already empty directory - start_clear=False, - - # We want to ensure no temporary files are left behind, so the - # PipTestEnvironment needs to capture and assert against temp - capture_temp=True, - assert_no_temp=True, - - # Deprecated python versions produce an extra deprecation warning - pip_expect_warning=deprecated_python, - ) + return script_factory(tmpdir.joinpath("workspace"), virtualenv) @pytest.fixture(scope="session") @@ -359,7 +376,7 @@ def in_memory_pip(): return InMemoryPip() -@pytest.fixture +@pytest.fixture(scope="session") def deprecated_python(): """Used to indicate whether pip deprecated this python version""" return sys.version_info[:2] in [(2, 7)] From cc73b2b933125952c7c9145a11a9c7bbad4d010e Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 30 Oct 2019 01:00:08 -0400 Subject: [PATCH 0649/3170] Use shared autocomplete_script for tests --- tests/functional/test_completion.py | 24 ++++++++++++++++++------ 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/tests/functional/test_completion.py b/tests/functional/test_completion.py index 9280b5d6a8a..2bb53ff2f0f 100644 --- a/tests/functional/test_completion.py +++ b/tests/functional/test_completion.py @@ -3,6 +3,8 @@ import pytest +from tests.lib.path import Path + COMPLETION_FOR_SUPPORTED_SHELLS_TESTS = ( ('bash', """\ _pip_completion() @@ -52,20 +54,28 @@ def test_completion_for_supported_shells(script, pip_src, common_wheels, assert completion in result.stdout, str(result.stdout) -def test_completion_for_unknown_shell(script): +@pytest.fixture(scope="session") +def autocomplete_script(tmpdir_factory, script_factory): + tmpdir = Path(str(tmpdir_factory.mktemp("autocomplete_script"))) + return script_factory(tmpdir.joinpath("workspace")) + + +def test_completion_for_unknown_shell(autocomplete_script): """ Test getting completion for an unknown shell """ error_msg = 'no such option: --myfooshell' - result = script.pip('completion', '--myfooshell', expect_error=True) + result = autocomplete_script.pip( + 'completion', '--myfooshell', expect_error=True + ) assert error_msg in result.stderr, 'tests for an unknown shell failed' -def test_completion_alone(script): +def test_completion_alone(autocomplete_script): """ Test getting completion for none shell, just pip completion """ - result = script.pip('completion', allow_stderr_error=True) + result = autocomplete_script.pip('completion', allow_stderr_error=True) assert 'ERROR: You must pass --bash or --fish or --zsh' in result.stderr, \ 'completion alone failed -- ' + result.stderr @@ -285,10 +295,12 @@ def test_completion_path_after_option(script, data): @pytest.mark.parametrize('flag', ['--bash', '--zsh', '--fish']) -def test_completion_uses_same_executable_name(script, flag, deprecated_python): +def test_completion_uses_same_executable_name( + autocomplete_script, flag, deprecated_python +): executable_name = 'pip{}'.format(sys.version_info[0]) # Deprecated python versions produce an extra deprecation warning - result = script.run( + result = autocomplete_script.run( executable_name, 'completion', flag, expect_stderr=deprecated_python, ) assert executable_name in result.stdout From ab6b17b2a6e7296cb4e822987b54868726aa97ac Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 30 Oct 2019 01:01:03 -0400 Subject: [PATCH 0650/3170] Refactor autocomplete test helper to use shared script --- tests/functional/test_completion.py | 91 ++++++++++++++--------------- 1 file changed, 45 insertions(+), 46 deletions(-) diff --git a/tests/functional/test_completion.py b/tests/functional/test_completion.py index 2bb53ff2f0f..a043bf56c60 100644 --- a/tests/functional/test_completion.py +++ b/tests/functional/test_completion.py @@ -60,6 +60,27 @@ def autocomplete_script(tmpdir_factory, script_factory): return script_factory(tmpdir.joinpath("workspace")) +@pytest.fixture +def autocomplete(autocomplete_script, monkeypatch): + monkeypatch.setattr(autocomplete_script, 'environ', os.environ.copy()) + autocomplete_script.environ['PIP_AUTO_COMPLETE'] = '1' + + def do_autocomplete(words, cword, cwd=None): + autocomplete_script.environ['COMP_WORDS'] = words + autocomplete_script.environ['COMP_CWORD'] = cword + result = autocomplete_script.run( + 'python', '-c', + 'from pip._internal.cli.autocompletion import autocomplete;' + 'autocomplete()', + expect_error=True, + cwd=cwd, + ) + + return result, autocomplete_script + + return do_autocomplete + + def test_completion_for_unknown_shell(autocomplete_script): """ Test getting completion for an unknown shell @@ -80,83 +101,64 @@ def test_completion_alone(autocomplete_script): 'completion alone failed -- ' + result.stderr -def setup_completion(script, words, cword, cwd=None): - script.environ = os.environ.copy() - script.environ['PIP_AUTO_COMPLETE'] = '1' - script.environ['COMP_WORDS'] = words - script.environ['COMP_CWORD'] = cword - - # expect_error is True because autocomplete exists with 1 status code - result = script.run( - 'python', '-c', - 'from pip._internal.cli.autocompletion import autocomplete;' - 'autocomplete()', - expect_error=True, - cwd=cwd, - ) - - return result, script - - -def test_completion_for_un_snippet(script): +def test_completion_for_un_snippet(autocomplete): """ Test getting completion for ``un`` should return uninstall """ - res, env = setup_completion(script, 'pip un', '1') + res, env = autocomplete('pip un', '1') assert res.stdout.strip().split() == ['uninstall'], res.stdout -def test_completion_for_default_parameters(script): +def test_completion_for_default_parameters(autocomplete): """ Test getting completion for ``--`` should contain --help """ - res, env = setup_completion(script, 'pip --', '1') + res, env = autocomplete('pip --', '1') assert '--help' in res.stdout,\ "autocomplete function could not complete ``--``" -def test_completion_option_for_command(script): +def test_completion_option_for_command(autocomplete): """ Test getting completion for ``--`` in command (e.g. ``pip search --``) """ - res, env = setup_completion(script, 'pip search --', '2') + res, env = autocomplete('pip search --', '2') assert '--help' in res.stdout,\ "autocomplete function could not complete ``--``" -def test_completion_short_option(script): +def test_completion_short_option(autocomplete): """ Test getting completion for short options after ``-`` (eg. pip -) """ - res, env = setup_completion(script, 'pip -', '1') + res, env = autocomplete('pip -', '1') assert '-h' in res.stdout.split(),\ "autocomplete function could not complete short options after ``-``" -def test_completion_short_option_for_command(script): +def test_completion_short_option_for_command(autocomplete): """ Test getting completion for short options after ``-`` in command (eg. pip search -) """ - res, env = setup_completion(script, 'pip search -', '2') + res, env = autocomplete('pip search -', '2') assert '-h' in res.stdout.split(),\ "autocomplete function could not complete short options after ``-``" -def test_completion_files_after_option(script, data): +def test_completion_files_after_option(autocomplete, data): """ Test getting completion for <file> or <dir> after options in command (e.g. ``pip install -r``) """ - res, env = setup_completion( - script=script, + res, env = autocomplete( words=('pip install -r r'), cword='3', cwd=data.completion_paths, @@ -186,13 +188,12 @@ def test_completion_files_after_option(script, data): ) -def test_completion_not_files_after_option(script, data): +def test_completion_not_files_after_option(autocomplete, data): """ Test not getting completion files after options which not applicable (e.g. ``pip install``) """ - res, env = setup_completion( - script=script, + res, env = autocomplete( words=('pip install r'), cword='2', cwd=data.completion_paths, @@ -210,13 +211,14 @@ def test_completion_not_files_after_option(script, data): @pytest.mark.parametrize("cl_opts", ["-U", "--user", "-h"]) -def test_completion_not_files_after_nonexpecting_option(script, data, cl_opts): +def test_completion_not_files_after_nonexpecting_option( + autocomplete, data, cl_opts +): """ Test not getting completion files after options which not applicable (e.g. ``pip install``) """ - res, env = setup_completion( - script=script, + res, env = autocomplete( words=('pip install %s r' % cl_opts), cword='2', cwd=data.completion_paths, @@ -233,13 +235,12 @@ def test_completion_not_files_after_nonexpecting_option(script, data, cl_opts): ) -def test_completion_directories_after_option(script, data): +def test_completion_directories_after_option(autocomplete, data): """ Test getting completion <dir> after options in command (e.g. ``pip --cache-dir``) """ - res, env = setup_completion( - script=script, + res, env = autocomplete( words=('pip --cache-dir r'), cword='2', cwd=data.completion_paths, @@ -258,13 +259,12 @@ def test_completion_directories_after_option(script, data): ) -def test_completion_subdirectories_after_option(script, data): +def test_completion_subdirectories_after_option(autocomplete, data): """ Test getting completion <dir> after options in command given path of a directory """ - res, env = setup_completion( - script=script, + res, env = autocomplete( words=('pip --cache-dir ' + os.path.join('resources', '')), cword='2', cwd=data.completion_paths, @@ -276,13 +276,12 @@ def test_completion_subdirectories_after_option(script, data): ) -def test_completion_path_after_option(script, data): +def test_completion_path_after_option(autocomplete, data): """ Test getting completion <path> after options in command given absolute path """ - res, env = setup_completion( - script=script, + res, env = autocomplete( words=('pip install -e ' + os.path.join(data.completion_paths, 'R')), cword='3', ) From 1f9028851d1ded51df1e56d838ea1c9a9fd274e1 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 30 Oct 2019 01:02:12 -0400 Subject: [PATCH 0651/3170] Make shared script with launchers installed --- tests/functional/test_completion.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/tests/functional/test_completion.py b/tests/functional/test_completion.py index a043bf56c60..4eaf36b4c85 100644 --- a/tests/functional/test_completion.py +++ b/tests/functional/test_completion.py @@ -37,20 +37,31 @@ ) +@pytest.fixture(scope="session") +def script_with_launchers( + tmpdir_factory, script_factory, common_wheels, pip_src +): + tmpdir = Path(str(tmpdir_factory.mktemp("script_with_launchers"))) + script = script_factory(tmpdir.joinpath("workspace")) + # Re-install pip so we get the launchers. + script.pip_install_local('-f', common_wheels, pip_src) + return script + + @pytest.mark.parametrize( 'shell, completion', COMPLETION_FOR_SUPPORTED_SHELLS_TESTS, ids=[t[0] for t in COMPLETION_FOR_SUPPORTED_SHELLS_TESTS], ) -def test_completion_for_supported_shells(script, pip_src, common_wheels, - shell, completion): +def test_completion_for_supported_shells( + script_with_launchers, shell, completion +): """ Test getting completion for bash shell """ - # Re-install pip so we get the launchers. - script.pip_install_local('-f', common_wheels, pip_src) - - result = script.pip('completion', '--' + shell, use_module=False) + result = script_with_launchers.pip( + 'completion', '--' + shell, use_module=False + ) assert completion in result.stdout, str(result.stdout) From 6cd98526269dbababf81ff620bc1b09eb58203d7 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 30 Oct 2019 01:38:13 -0400 Subject: [PATCH 0652/3170] Use shared script in pip list tests --- tests/conftest.py | 5 +++ tests/functional/test_list.py | 75 +++++++++++++++-------------------- 2 files changed, 36 insertions(+), 44 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 816a08877bd..baffc6bf181 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -343,6 +343,11 @@ def common_wheels(): return DATA_DIR.joinpath('common_wheels') +@pytest.fixture(scope="session") +def shared_data(tmpdir_factory): + return TestData.copy(Path(tmpdir_factory.mktemp("data"))) + + @pytest.fixture def data(tmpdir): return TestData.copy(tmpdir.joinpath("data")) diff --git a/tests/functional/test_list.py b/tests/functional/test_list.py index 764d326f7b8..4866b01e4b0 100644 --- a/tests/functional/test_list.py +++ b/tests/functional/test_list.py @@ -3,30 +3,35 @@ import pytest +from tests.lib.path import Path -def test_basic_list(script, data): - """ - Test default behavior of list command without format specifier. - """ +@pytest.fixture(scope="session") +def simple_script(tmpdir_factory, script_factory, shared_data): + tmpdir = Path(str(tmpdir_factory.mktemp("pip_test_package"))) + script = script_factory(tmpdir.joinpath("workspace")) script.pip( - 'install', '-f', data.find_links, '--no-index', 'simple==1.0', + 'install', '-f', shared_data.find_links, '--no-index', 'simple==1.0', 'simple2==3.0', ) - result = script.pip('list') + return script + + +def test_basic_list(simple_script): + """ + Test default behavior of list command without format specifier. + + """ + result = simple_script.pip('list') assert 'simple 1.0' in result.stdout, str(result) assert 'simple2 3.0' in result.stdout, str(result) -def test_verbose_flag(script, data): +def test_verbose_flag(simple_script): """ Test the list command with the '-v' option """ - script.pip( - 'install', '-f', data.find_links, '--no-index', 'simple==1.0', - 'simple2==3.0', - ) - result = script.pip('list', '-v', '--format=columns') + result = simple_script.pip('list', '-v', '--format=columns') assert 'Package' in result.stdout, str(result) assert 'Version' in result.stdout, str(result) assert 'Location' in result.stdout, str(result) @@ -35,15 +40,11 @@ def test_verbose_flag(script, data): assert 'simple2 3.0' in result.stdout, str(result) -def test_columns_flag(script, data): +def test_columns_flag(simple_script): """ Test the list command with the '--format=columns' option """ - script.pip( - 'install', '-f', data.find_links, '--no-index', 'simple==1.0', - 'simple2==3.0', - ) - result = script.pip('list', '--format=columns') + result = simple_script.pip('list', '--format=columns') assert 'Package' in result.stdout, str(result) assert 'Version' in result.stdout, str(result) assert 'simple (1.0)' not in result.stdout, str(result) @@ -51,22 +52,18 @@ def test_columns_flag(script, data): assert 'simple2 3.0' in result.stdout, str(result) -def test_format_priority(script, data): +def test_format_priority(simple_script): """ Test that latest format has priority over previous ones. """ - script.pip( - 'install', '-f', data.find_links, '--no-index', 'simple==1.0', - 'simple2==3.0', - ) - result = script.pip('list', '--format=columns', '--format=freeze', - expect_stderr=True) + result = simple_script.pip('list', '--format=columns', '--format=freeze', + expect_stderr=True) assert 'simple==1.0' in result.stdout, str(result) assert 'simple2==3.0' in result.stdout, str(result) assert 'simple 1.0' not in result.stdout, str(result) assert 'simple2 3.0' not in result.stdout, str(result) - result = script.pip('list', '--format=freeze', '--format=columns') + result = simple_script.pip('list', '--format=freeze', '--format=columns') assert 'Package' in result.stdout, str(result) assert 'Version' in result.stdout, str(result) assert 'simple==1.0' not in result.stdout, str(result) @@ -75,23 +72,21 @@ def test_format_priority(script, data): assert 'simple2 3.0' in result.stdout, str(result) -def test_local_flag(script, data): +def test_local_flag(simple_script): """ Test the behavior of --local flag in the list command """ - script.pip('install', '-f', data.find_links, '--no-index', 'simple==1.0') - result = script.pip('list', '--local', '--format=json') + result = simple_script.pip('list', '--local', '--format=json') assert {"name": "simple", "version": "1.0"} in json.loads(result.stdout) -def test_local_columns_flag(script, data): +def test_local_columns_flag(simple_script): """ Test the behavior of --local --format=columns flags in the list command """ - script.pip('install', '-f', data.find_links, '--no-index', 'simple==1.0') - result = script.pip('list', '--local', '--format=columns') + result = simple_script.pip('list', '--local', '--format=columns') assert 'Package' in result.stdout assert 'Version' in result.stdout assert 'simple (1.0)' not in result.stdout @@ -479,30 +474,22 @@ def test_not_required_flag(script, data): assert 'TopoRequires3 ' not in result.stdout -def test_list_freeze(script, data): +def test_list_freeze(simple_script): """ Test freeze formatting of list command """ - script.pip( - 'install', '-f', data.find_links, '--no-index', 'simple==1.0', - 'simple2==3.0', - ) - result = script.pip('list', '--format=freeze') + result = simple_script.pip('list', '--format=freeze') assert 'simple==1.0' in result.stdout, str(result) assert 'simple2==3.0' in result.stdout, str(result) -def test_list_json(script, data): +def test_list_json(simple_script): """ Test json formatting of list command """ - script.pip( - 'install', '-f', data.find_links, '--no-index', 'simple==1.0', - 'simple2==3.0', - ) - result = script.pip('list', '--format=json') + result = simple_script.pip('list', '--format=json') data = json.loads(result.stdout) assert {'name': 'simple', 'version': '1.0'} in data assert {'name': 'simple2', 'version': '3.0'} in data From 0c4625b2bac57815f652e28e93324ec95e087872 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 30 Oct 2019 01:39:02 -0400 Subject: [PATCH 0653/3170] Use another shared script in pip list tests --- tests/conftest.py | 2 +- tests/functional/test_list.py | 61 ++++++++++++++++------------------- 2 files changed, 28 insertions(+), 35 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index baffc6bf181..6a05ca5ea00 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -345,7 +345,7 @@ def common_wheels(): @pytest.fixture(scope="session") def shared_data(tmpdir_factory): - return TestData.copy(Path(tmpdir_factory.mktemp("data"))) + return TestData.copy(Path(str(tmpdir_factory.mktemp("data")))) @pytest.fixture diff --git a/tests/functional/test_list.py b/tests/functional/test_list.py index 4866b01e4b0..a863c42c91a 100644 --- a/tests/functional/test_list.py +++ b/tests/functional/test_list.py @@ -242,50 +242,53 @@ def test_outdated_columns_flag(script, data): assert 'simple2' not in result.stdout, str(result) # 3.0 is latest +@pytest.fixture(scope="session") +def pip_test_package_script(tmpdir_factory, script_factory, shared_data): + tmpdir = Path(str(tmpdir_factory.mktemp("pip_test_package"))) + script = script_factory(tmpdir.joinpath("workspace")) + script.pip( + 'install', '-f', shared_data.find_links, '--no-index', 'simple==1.0' + ) + script.pip( + 'install', '-e', + 'git+https://github.com/pypa/pip-test-package.git#egg=pip-test-package' + ) + return script + + @pytest.mark.network -def test_editables_flag(script, data): +def test_editables_flag(pip_test_package_script): """ Test the behavior of --editables flag in the list command """ - script.pip('install', '-f', data.find_links, '--no-index', 'simple==1.0') - result = script.pip( - 'install', '-e', - 'git+https://github.com/pypa/pip-test-package.git#egg=pip-test-package' - ) - result = script.pip('list', '--editable', '--format=json') - result2 = script.pip('list', '--editable') + result = pip_test_package_script.pip('list', '--editable', '--format=json') + result2 = pip_test_package_script.pip('list', '--editable') assert {"name": "simple", "version": "1.0"} \ not in json.loads(result.stdout) assert os.path.join('src', 'pip-test-package') in result2.stdout @pytest.mark.network -def test_exclude_editable_flag(script, data): +def test_exclude_editable_flag(pip_test_package_script): """ Test the behavior of --editables flag in the list command """ - script.pip('install', '-f', data.find_links, '--no-index', 'simple==1.0') - result = script.pip( - 'install', '-e', - 'git+https://github.com/pypa/pip-test-package.git#egg=pip-test-package' + result = pip_test_package_script.pip( + 'list', '--exclude-editable', '--format=json' ) - result = script.pip('list', '--exclude-editable', '--format=json') assert {"name": "simple", "version": "1.0"} in json.loads(result.stdout) assert "pip-test-package" \ not in {p["name"] for p in json.loads(result.stdout)} @pytest.mark.network -def test_editables_columns_flag(script, data): +def test_editables_columns_flag(pip_test_package_script): """ Test the behavior of --editables flag in the list command """ - script.pip('install', '-f', data.find_links, '--no-index', 'simple==1.0') - result = script.pip( - 'install', '-e', - 'git+https://github.com/pypa/pip-test-package.git#egg=pip-test-package' + result = pip_test_package_script.pip( + 'list', '--editable', '--format=columns' ) - result = script.pip('list', '--editable', '--format=columns') assert 'Package' in result.stdout assert 'Version' in result.stdout assert 'Location' in result.stdout @@ -295,16 +298,11 @@ def test_editables_columns_flag(script, data): @pytest.mark.network -def test_uptodate_editables_flag(script, data): +def test_uptodate_editables_flag(pip_test_package_script, data): """ test the behavior of --editable --uptodate flag in the list command """ - script.pip('install', '-f', data.find_links, '--no-index', 'simple==1.0') - result = script.pip( - 'install', '-e', - 'git+https://github.com/pypa/pip-test-package.git#egg=pip-test-package' - ) - result = script.pip( + result = pip_test_package_script.pip( 'list', '-f', data.find_links, '--no-index', '--editable', '--uptodate', ) @@ -315,17 +313,12 @@ def test_uptodate_editables_flag(script, data): @pytest.mark.network -def test_uptodate_editables_columns_flag(script, data): +def test_uptodate_editables_columns_flag(pip_test_package_script, data): """ test the behavior of --editable --uptodate --format=columns flag in the list command """ - script.pip('install', '-f', data.find_links, '--no-index', 'simple==1.0') - result = script.pip( - 'install', '-e', - 'git+https://github.com/pypa/pip-test-package.git#egg=pip-test-package' - ) - result = script.pip( + result = pip_test_package_script.pip( 'list', '-f', data.find_links, '--no-index', '--editable', '--uptodate', '--format=columns', ) From 01c953b95dfe9d82262c4c545e1567c6faa64203 Mon Sep 17 00:00:00 2001 From: Xavier Fernandez <xavier.fernandez@polyconseil.fr> Date: Thu, 17 Oct 2019 21:26:21 +0200 Subject: [PATCH 0654/3170] docs: Add basic documentation about our CI setup --- docs/html/development/ci.rst | 179 ++++++++++++++++++++++++++++++++ docs/html/development/index.rst | 1 + 2 files changed, 180 insertions(+) create mode 100644 docs/html/development/ci.rst diff --git a/docs/html/development/ci.rst b/docs/html/development/ci.rst new file mode 100644 index 00000000000..2b7632a0c57 --- /dev/null +++ b/docs/html/development/ci.rst @@ -0,0 +1,179 @@ +.. note:: + This section of the documentation is currently being written. pip + developers welcome your help to complete this documentation. If you're + interested in helping out, please let us know in the `tracking issue`_. + +.. _`tracking issue`: https://github.com/pypa/pip/issues/7279 + +********************** +Continuous Integration +********************** + +Supported interpreters +====================== + +pip support a variety of Python interpreters: + + - CPython 2.7 + - CPython 3.5 + - CPython 3.6 + - CPython 3.7 + - CPython 3.8 + - Latest PyPy + - Latest PyPy3 + +on different operating systems: + + - Linux + - Windows + - MacOS + +and on different architectures: + + - x64 + - x86 + +so 42 hypothetical interpreters. + + +Checks +====== + +``pip`` CI runs different kind of tests: + +- lint (defined in ``.pre-commit-config.yaml``) +- docs +- vendoring (is the ``src/_internal/_vendor`` directory cleanly vendored) +- unit tests (present in ``tests/unit``) +- "integration" tests (mostly present in ``tests/functional``) +- package (test the packaging steps) + +Since lint, docs, vendoring and package tests only need to run on a pip +developer/contributor machine, they only need to be tested on the x64 variant +of the 3 different operating systems, and when an interpreter needs to be +specified it's ok to require the latest CPython interpreter. + +So only unit tests and integration tests would need to be run with the different +interpreters. + +Services +======== + +pip test suite and checks are distributed on four different platforms that +provides free executors for open source packages: + + - `Travis CI`_ (Used for Linux) + - `Appveyor CI`_ (Windows only) + - `Azure DevOps CI`_ (Linux, MacOS & Windows tests) + - `GitHub Actions`_ (Linux, MacOS & Windows tests) + +.. _`Travis CI`: https://travis-ci.org/ +.. _`Appveyor CI`: https://www.appveyor.com/ +.. _`Azure DevOps CI`: https://dev.azure.com/ +.. _`GitHub Actions`: https://github.com/features/actions + + +Current run tests +================= + +Developer tasks +--------------- + +======== =============== ================ =========== ============ + OS docs lint vendoring packages +======== =============== ================ =========== ============ +Linux Travis, Github Travis, Github Travis Azure +Windows Azure +MacOS Azure +======== =============== ================ =========== ============ + +Actual testing +-------------- + ++------------------------------+---------------+-----------------+ +| **interpreter** | **unit** | **integration** | ++-----------+----------+-------+---------------+-----------------+ +| | | CP2.7 | Azure | Azure | +| | +-------+---------------+-----------------+ +| | | CP3.5 | Azure | | +| | +-------+---------------+-----------------+ +| | | CP3.6 | Azure | | +| | +-------+---------------+-----------------+ +| | x86 | CP3.7 | Azure | | +| | +-------+---------------+-----------------+ +| | | CP3.8 | | | +| | +-------+---------------+-----------------+ +| | | PyPy | | | +| | +-------+---------------+-----------------+ +| | | PyPy3 | | | +| Windows +----------+-------+---------------+-----------------+ +| | | CP2.7 | Appveyor | Appveyor | +| | +-------+---------------+-----------------+ +| | | CP3.5 | Azure | Azure | +| | +-------+---------------+-----------------+ +| | | CP3.6 | Appveyor | Appveyor | +| | +-------+---------------+-----------------+ +| | x64 | CP3.7 | Azure | Azure | +| | +-------+---------------+-----------------+ +| | | CP3.8 | | | +| | +-------+---------------+-----------------+ +| | | PyPy | | | +| | +-------+---------------+-----------------+ +| | | PyPy3 | | | ++-----------+----------+-------+---------------+-----------------+ +| | | CP2.7 | | | +| | +-------+---------------+-----------------+ +| | | CP3.5 | | | +| | +-------+---------------+-----------------+ +| | | CP3.6 | | | +| | +-------+---------------+-----------------+ +| | x86 | CP3.7 | | | +| | +-------+---------------+-----------------+ +| | | CP3.8 | | | +| | +-------+---------------+-----------------+ +| | | PyPy | | | +| | +-------+---------------+-----------------+ +| | | PyPy3 | | | +| Linux +----------+-------+---------------+-----------------+ +| | | CP2.7 | Travis,Azure | Travis,Azure | +| | +-------+---------------+-----------------+ +| | | CP3.5 | Travis,Azure | Travis,Azure | +| | +-------+---------------+-----------------+ +| | | CP3.6 | Travis,Azure | Travis,Azure | +| | +-------+---------------+-----------------+ +| | x64 | CP3.7 | Travis,Azure | Travis,Azure | +| | +-------+---------------+-----------------+ +| | | CP3.8 | Travis | Travis | +| | +-------+---------------+-----------------+ +| | | PyPy | Travis | Travis | +| | +-------+---------------+-----------------+ +| | | PyPy3 | Travis | Travis | ++-----------+----------+-------+---------------+-----------------+ +| | | CP2.7 | | | +| | +-------+---------------+-----------------+ +| | | CP3.5 | | | +| | +-------+---------------+-----------------+ +| | | CP3.6 | | | +| | +-------+---------------+-----------------+ +| | x86 | CP3.7 | | | +| | +-------+---------------+-----------------+ +| | | CP3.8 | | | +| | +-------+---------------+-----------------+ +| | | PyPy | | | +| | +-------+---------------+-----------------+ +| | | PyPy3 | | | +| MacOS +----------+-------+---------------+-----------------+ +| | | CP2.7 | Azure | Azure | +| | +-------+---------------+-----------------+ +| | | CP3.5 | Azure | Azure | +| | +-------+---------------+-----------------+ +| | | CP3.6 | Azure | Azure | +| | +-------+---------------+-----------------+ +| | x64 | CP3.7 | Azure | Azure | +| | +-------+---------------+-----------------+ +| | | CP3.8 | | | +| | +-------+---------------+-----------------+ +| | | PyPy | | | +| | +-------+---------------+-----------------+ +| | | PyPy3 | | | ++-----------+----------+-------+---------------+-----------------+ diff --git a/docs/html/development/index.rst b/docs/html/development/index.rst index 53fefc9e186..ffaa3c07b09 100644 --- a/docs/html/development/index.rst +++ b/docs/html/development/index.rst @@ -14,6 +14,7 @@ or the `pypa-dev mailing list`_, to ask questions or get involved. getting-started contributing + ci issue-triage architecture/index release-process From bb047aa2633f6f07b16a3a0c8af66f0052f50b52 Mon Sep 17 00:00:00 2001 From: Ananya Maiti <ananyoevo@gmail.com> Date: Mon, 14 Oct 2019 13:54:55 +0530 Subject: [PATCH 0655/3170] Document that "coding: utf-8" is supported in requirements.txt Fixes #7182 --- docs/html/reference/pip_install.rst | 3 +++ news/7182.doc | 1 + src/pip/_internal/req/req_file.py | 1 + 3 files changed, 5 insertions(+) create mode 100644 news/7182.doc diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index ac37455ccad..1418e0f58f8 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -148,6 +148,9 @@ and the newline following it is effectively ignored. Comments are stripped *before* line continuations are processed. +To interpret the requirements file in UTF-8 format add a comment +``# -*- coding: utf-8 -*-`` to the first or second line of the file. + The following options are supported: * :ref:`-i, --index-url <--index-url>` diff --git a/news/7182.doc b/news/7182.doc new file mode 100644 index 00000000000..f55c6ee7eef --- /dev/null +++ b/news/7182.doc @@ -0,0 +1 @@ +Document that "coding: utf-8" is supported in requirements.txt diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index b4c22475fa4..c8b6156e3b5 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -509,6 +509,7 @@ def get_file_content(url, comes_from=None, session=None): # type: (str, Optional[str], Optional[PipSession]) -> Tuple[str, Text] """Gets the content of a file; it may be a filename, file: URL, or http: URL. Returns (location, content). Content is unicode. + Respects # -*- coding: declarations on the retrieved files. :param url: File path or url. :param comes_from: Origin description of requirements. From 18a58815a105ae61b68e8e83373fb4c15dde4392 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Fri, 1 Nov 2019 12:24:22 +0100 Subject: [PATCH 0656/3170] Refactor _get_used_vcs_backend --- news/7281.trivial | 1 + src/pip/_internal/operations/prepare.py | 14 +------------- src/pip/_internal/vcs/versioncontrol.py | 10 ++++++++++ tests/functional/test_vcs_git.py | 5 +++++ 4 files changed, 17 insertions(+), 13 deletions(-) create mode 100644 news/7281.trivial diff --git a/news/7281.trivial b/news/7281.trivial new file mode 100644 index 00000000000..8fce9ce5107 --- /dev/null +++ b/news/7281.trivial @@ -0,0 +1 @@ +refactor _get_used_vcs_backend diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 9993f884186..1152c336f84 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -66,7 +66,6 @@ from pip._internal.req.req_install import InstallRequirement from pip._internal.req.req_tracker import RequirementTracker from pip._internal.utils.hashes import Hashes - from pip._internal.vcs.versioncontrol import VersionControl if PY2: CopytreeKwargs = TypedDict( @@ -103,22 +102,11 @@ def _get_prepared_distribution(req, req_tracker, finder, build_isolation): def unpack_vcs_link(link, location): # type: (Link, str) -> None - vcs_backend = _get_used_vcs_backend(link) + vcs_backend = vcs.get_backend_for_scheme(link.scheme) assert vcs_backend is not None vcs_backend.unpack(location, url=hide_url(link.url)) -def _get_used_vcs_backend(link): - # type: (Link) -> Optional[VersionControl] - """ - Return a VersionControl object or None. - """ - for vcs_backend in vcs.backends: - if link.scheme in vcs_backend.schemes: - return vcs_backend - return None - - def _progress_indicator(iterable, *args, **kwargs): return iterable diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index 9038ace8052..0eccf436a76 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -237,6 +237,16 @@ def get_backend_for_dir(self, location): return vcs_backend return None + def get_backend_for_scheme(self, scheme): + # type: (str) -> Optional[VersionControl] + """ + Return a VersionControl object or None. + """ + for vcs_backend in self._registry.values(): + if scheme in vcs_backend.schemes: + return vcs_backend + return None + def get_backend(self, name): # type: (str) -> Optional[VersionControl] """ diff --git a/tests/functional/test_vcs_git.py b/tests/functional/test_vcs_git.py index ac94b99aac9..606cbe1ef7c 100644 --- a/tests/functional/test_vcs_git.py +++ b/tests/functional/test_vcs_git.py @@ -6,10 +6,15 @@ import pytest +from pip._internal.vcs import vcs from pip._internal.vcs.git import Git, RemoteNotFoundError from tests.lib import _create_test_package, _git_commit, _test_path_to_file_url +def test_get_backend_for_scheme(): + assert vcs.get_backend_for_scheme("git+https") is vcs.get_backend("Git") + + def get_head_sha(script, dest): """Return the HEAD sha.""" result = script.run('git', 'rev-parse', 'HEAD', cwd=dest) From 2b5e87cd3796b7d8ab575eacf3f817f7cea04d31 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sat, 2 Nov 2019 20:11:30 +0530 Subject: [PATCH 0657/3170] Move operations.{generate_metadata -> build.metadata} --- src/pip/_internal/operations/build/__init__.py | 0 .../operations/{generate_metadata.py => build/metadata.py} | 0 src/pip/_internal/req/req_install.py | 2 +- 3 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 src/pip/_internal/operations/build/__init__.py rename src/pip/_internal/operations/{generate_metadata.py => build/metadata.py} (100%) diff --git a/src/pip/_internal/operations/build/__init__.py b/src/pip/_internal/operations/build/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/operations/generate_metadata.py b/src/pip/_internal/operations/build/metadata.py similarity index 100% rename from src/pip/_internal/operations/generate_metadata.py rename to src/pip/_internal/operations/build/metadata.py diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index f47bbbf0f45..168204e0754 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -24,7 +24,7 @@ from pip._internal.exceptions import InstallationError from pip._internal.locations import distutils_scheme from pip._internal.models.link import Link -from pip._internal.operations.generate_metadata import get_metadata_generator +from pip._internal.operations.build.metadata import get_metadata_generator from pip._internal.pyproject import load_pyproject_toml, make_pyproject_path from pip._internal.req.req_uninstall import UninstallPathSet from pip._internal.utils.compat import native_str From 3f76f5702b1a0387eb1c14ab9663af3dc4a1997e Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sat, 2 Nov 2019 20:19:44 +0530 Subject: [PATCH 0658/3170] Move logic and imports for legacy metadata generation --- .../_internal/operations/build/metadata.py | 111 +--------------- .../operations/build/metadata_legacy.py | 120 ++++++++++++++++++ 2 files changed, 124 insertions(+), 107 deletions(-) create mode 100644 src/pip/_internal/operations/build/metadata_legacy.py diff --git a/src/pip/_internal/operations/build/metadata.py b/src/pip/_internal/operations/build/metadata.py index 04e5ace1236..6bb132f81a8 100644 --- a/src/pip/_internal/operations/build/metadata.py +++ b/src/pip/_internal/operations/build/metadata.py @@ -5,19 +5,15 @@ import logging import os -from pip._internal.exceptions import InstallationError -from pip._internal.utils.misc import ensure_dir -from pip._internal.utils.setuptools_build import make_setuptools_egg_info_args -from pip._internal.utils.subprocess import ( - call_subprocess, - runner_with_spinner_message, +from pip._internal.operations.build.metadata_legacy import ( + _generate_metadata_legacy, ) +from pip._internal.utils.subprocess import runner_with_spinner_message from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from pip._internal.vcs import vcs if MYPY_CHECK_RUNNING: - from typing import Callable, List, Optional + from typing import Callable from pip._internal.req.req_install import InstallRequirement @@ -38,105 +34,6 @@ def get_metadata_generator(install_req): return _generate_metadata -def _find_egg_info(source_directory, is_editable): - # type: (str, bool) -> str - """Find an .egg-info in `source_directory`, based on `is_editable`. - """ - - def looks_like_virtual_env(path): - # type: (str) -> bool - return ( - os.path.lexists(os.path.join(path, 'bin', 'python')) or - os.path.exists(os.path.join(path, 'Scripts', 'Python.exe')) - ) - - def locate_editable_egg_info(base): - # type: (str) -> List[str] - candidates = [] # type: List[str] - for root, dirs, files in os.walk(base): - for dir_ in vcs.dirnames: - if dir_ in dirs: - dirs.remove(dir_) - # Iterate over a copy of ``dirs``, since mutating - # a list while iterating over it can cause trouble. - # (See https://github.com/pypa/pip/pull/462.) - for dir_ in list(dirs): - if looks_like_virtual_env(os.path.join(root, dir_)): - dirs.remove(dir_) - # Also don't search through tests - elif dir_ == 'test' or dir_ == 'tests': - dirs.remove(dir_) - candidates.extend(os.path.join(root, dir_) for dir_ in dirs) - return [f for f in candidates if f.endswith('.egg-info')] - - def depth_of_directory(dir_): - # type: (str) -> int - return ( - dir_.count(os.path.sep) + - (os.path.altsep and dir_.count(os.path.altsep) or 0) - ) - - base = source_directory - if is_editable: - filenames = locate_editable_egg_info(base) - else: - base = os.path.join(base, 'pip-egg-info') - filenames = os.listdir(base) - - if not filenames: - raise InstallationError( - "Files/directories not found in {}".format(base) - ) - - # If we have more than one match, we pick the toplevel one. This - # can easily be the case if there is a dist folder which contains - # an extracted tarball for testing purposes. - if len(filenames) > 1: - filenames.sort(key=depth_of_directory) - - return os.path.join(base, filenames[0]) - - -def _generate_metadata_legacy(install_req): - # type: (InstallRequirement) -> str - req_details_str = install_req.name or "from {}".format(install_req.link) - logger.debug( - 'Running setup.py (path:%s) egg_info for package %s', - install_req.setup_py_path, req_details_str, - ) - - egg_info_dir = None # type: Optional[str] - # For non-editable installs, don't put the .egg-info files at the root, - # to avoid confusion due to the source code being considered an installed - # egg. - if not install_req.editable: - egg_info_dir = os.path.join( - install_req.unpacked_source_directory, 'pip-egg-info', - ) - - # setuptools complains if the target directory does not exist. - ensure_dir(egg_info_dir) - - args = make_setuptools_egg_info_args( - install_req.setup_py_path, - egg_info_dir=egg_info_dir, - no_user_config=install_req.isolated, - ) - - with install_req.build_env: - call_subprocess( - args, - cwd=install_req.unpacked_source_directory, - command_desc='python setup.py egg_info', - ) - - # Return the .egg-info directory. - return _find_egg_info( - install_req.unpacked_source_directory, - install_req.editable, - ) - - def _generate_metadata(install_req): # type: (InstallRequirement) -> str assert install_req.pep517_backend is not None diff --git a/src/pip/_internal/operations/build/metadata_legacy.py b/src/pip/_internal/operations/build/metadata_legacy.py new file mode 100644 index 00000000000..4d8d1cf23f7 --- /dev/null +++ b/src/pip/_internal/operations/build/metadata_legacy.py @@ -0,0 +1,120 @@ +"""Metadata generation logic for legacy source distributions. +""" + +import logging +import os + +from pip._internal.exceptions import InstallationError +from pip._internal.utils.misc import ensure_dir +from pip._internal.utils.setuptools_build import make_setuptools_egg_info_args +from pip._internal.utils.subprocess import call_subprocess +from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from pip._internal.vcs import vcs + +if MYPY_CHECK_RUNNING: + from typing import List, Optional + + from pip._internal.req.req_install import InstallRequirement + +logger = logging.getLogger(__name__) + + +def _find_egg_info(source_directory, is_editable): + # type: (str, bool) -> str + """Find an .egg-info in `source_directory`, based on `is_editable`. + """ + + def looks_like_virtual_env(path): + # type: (str) -> bool + return ( + os.path.lexists(os.path.join(path, 'bin', 'python')) or + os.path.exists(os.path.join(path, 'Scripts', 'Python.exe')) + ) + + def locate_editable_egg_info(base): + # type: (str) -> List[str] + candidates = [] # type: List[str] + for root, dirs, files in os.walk(base): + for dir_ in vcs.dirnames: + if dir_ in dirs: + dirs.remove(dir_) + # Iterate over a copy of ``dirs``, since mutating + # a list while iterating over it can cause trouble. + # (See https://github.com/pypa/pip/pull/462.) + for dir_ in list(dirs): + if looks_like_virtual_env(os.path.join(root, dir_)): + dirs.remove(dir_) + # Also don't search through tests + elif dir_ == 'test' or dir_ == 'tests': + dirs.remove(dir_) + candidates.extend(os.path.join(root, dir_) for dir_ in dirs) + return [f for f in candidates if f.endswith('.egg-info')] + + def depth_of_directory(dir_): + # type: (str) -> int + return ( + dir_.count(os.path.sep) + + (os.path.altsep and dir_.count(os.path.altsep) or 0) + ) + + base = source_directory + if is_editable: + filenames = locate_editable_egg_info(base) + else: + base = os.path.join(base, 'pip-egg-info') + filenames = os.listdir(base) + + if not filenames: + raise InstallationError( + "Files/directories not found in {}".format(base) + ) + + # If we have more than one match, we pick the toplevel one. This + # can easily be the case if there is a dist folder which contains + # an extracted tarball for testing purposes. + if len(filenames) > 1: + filenames.sort(key=depth_of_directory) + + return os.path.join(base, filenames[0]) + + +def _generate_metadata_legacy(install_req): + # type: (InstallRequirement) -> str + assert install_req.unpacked_source_directory + + req_details_str = install_req.name or "from {}".format(install_req.link) + logger.debug( + 'Running setup.py (path:%s) egg_info for package %s', + install_req.setup_py_path, req_details_str, + ) + + egg_info_dir = None # type: Optional[str] + # For non-editable installs, don't put the .egg-info files at the root, + # to avoid confusion due to the source code being considered an installed + # egg. + if not install_req.editable: + egg_info_dir = os.path.join( + install_req.unpacked_source_directory, 'pip-egg-info', + ) + + # setuptools complains if the target directory does not exist. + ensure_dir(egg_info_dir) + + args = make_setuptools_egg_info_args( + install_req.setup_py_path, + egg_info_dir=egg_info_dir, + no_user_config=install_req.isolated, + ) + + with install_req.build_env: + call_subprocess( + args, + cwd=install_req.unpacked_source_directory, + command_desc='python setup.py egg_info', + ) + + # Return the .egg-info directory. + return _find_egg_info( + install_req.unpacked_source_directory, + install_req.editable, + ) From bebd69173b2eec1ecc263a12e6fefefa45d76fa4 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sat, 2 Nov 2019 20:21:01 +0530 Subject: [PATCH 0659/3170] Rename _generate_metadata_legacy -> generate_metadata Why: To avoid importing an _name. --- src/pip/_internal/operations/build/metadata.py | 5 ++--- src/pip/_internal/operations/build/metadata_legacy.py | 2 +- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/operations/build/metadata.py b/src/pip/_internal/operations/build/metadata.py index 6bb132f81a8..66b59eb5129 100644 --- a/src/pip/_internal/operations/build/metadata.py +++ b/src/pip/_internal/operations/build/metadata.py @@ -5,9 +5,8 @@ import logging import os -from pip._internal.operations.build.metadata_legacy import ( - _generate_metadata_legacy, -) +from pip._internal.operations.build.metadata_legacy import \ + generate_metadata as _generate_metadata_legacy from pip._internal.utils.subprocess import runner_with_spinner_message from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING diff --git a/src/pip/_internal/operations/build/metadata_legacy.py b/src/pip/_internal/operations/build/metadata_legacy.py index 4d8d1cf23f7..ba6265db791 100644 --- a/src/pip/_internal/operations/build/metadata_legacy.py +++ b/src/pip/_internal/operations/build/metadata_legacy.py @@ -78,7 +78,7 @@ def depth_of_directory(dir_): return os.path.join(base, filenames[0]) -def _generate_metadata_legacy(install_req): +def generate_metadata(install_req): # type: (InstallRequirement) -> str assert install_req.unpacked_source_directory From 7256207df7cd7b9cf31efc90b6d45672e58bea90 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sat, 2 Nov 2019 20:55:52 +0530 Subject: [PATCH 0660/3170] Move _clean_zip_name into a nested function Why: Better models how it's used --- src/pip/_internal/req/req_install.py | 21 +++++++++++---------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index f47bbbf0f45..4437d1eac5d 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -749,19 +749,20 @@ def uninstall(self, auto_confirm=False, verbose=False, uninstalled_pathset.remove(auto_confirm, verbose) return uninstalled_pathset - def _clean_zip_name(self, name, prefix): # only used by archive. - # type: (str, str) -> str - assert name.startswith(prefix + os.path.sep), ( - "name %r doesn't start with prefix %r" % (name, prefix) - ) - name = name[len(prefix) + 1:] - name = name.replace(os.path.sep, '/') - return name - def _get_archive_name(self, path, parentdir, rootdir): # type: (str, str, str) -> str + + def _clean_zip_name(name, prefix): + # type: (str, str) -> str + assert name.startswith(prefix + os.path.sep), ( + "name %r doesn't start with prefix %r" % (name, prefix) + ) + name = name[len(prefix) + 1:] + name = name.replace(os.path.sep, '/') + return name + path = os.path.join(parentdir, path) - name = self._clean_zip_name(path, rootdir) + name = _clean_zip_name(path, rootdir) return self.name + '/' + name def archive(self, build_dir): From 9e3e82e081b1197c56912450bbd3d23db39efd3b Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 3 Nov 2019 00:36:57 +0530 Subject: [PATCH 0661/3170] Rewrite release preparation automation --- noxfile.py | 123 +++++++-------------------- tools/automation/release/__init__.py | 104 ++++++++++++++++++++++ 2 files changed, 136 insertions(+), 91 deletions(-) create mode 100644 tools/automation/release/__init__.py diff --git a/noxfile.py b/noxfile.py index 0799fc5cc53..21ee63a3db3 100644 --- a/noxfile.py +++ b/noxfile.py @@ -4,13 +4,16 @@ # The following comment should be removed at some point in the future. # mypy: disallow-untyped-defs=False -import io import os import shutil -import subprocess +import sys import nox +sys.path.append(".") +from tools.automation import release # isort:skip # noqa +sys.path.pop() + nox.options.reuse_existing_virtualenvs = True nox.options.sessions = ["lint"] @@ -27,29 +30,6 @@ VERSION_FILE = "src/pip/__init__.py" -def get_author_list(): - """Get the list of authors from Git commits. - """ - # subprocess because session.run doesn't give us stdout - result = subprocess.run( - ["git", "log", "--use-mailmap", "--format=%aN <%aE>"], - capture_output=True, - encoding="utf-8", - ) - - # Create a unique list. - authors = [] - seen_authors = set() - for author in result.stdout.splitlines(): - author = author.strip() - if author.lower() not in seen_authors: - seen_authors.add(author.lower()) - authors.append(author) - - # Sort our list of Authors by their case insensitive name - return sorted(authors, key=lambda x: x.lower()) - - def run_with_protected_pip(session, *arguments): """Do a session.run("pip", *arguments), using a "protected" pip. @@ -81,11 +61,6 @@ def should_update_common_wheels(): return need_to_repopulate -def update_version_file(new_version): - with open(VERSION_FILE, "w", encoding="utf-8") as f: - f.write('__version__ = "{}"\n'.format(new_version)) - - # ----------------------------------------------------------------------------- # Development Commands # These are currently prototypes to evaluate whether we want to switch over @@ -174,70 +149,36 @@ def lint(session): # ----------------------------------------------------------------------------- # Release Commands # ----------------------------------------------------------------------------- -@nox.session(python=False) -def generate_authors(session): - # Get our list of authors - session.log("Collecting author names") - authors = get_author_list() - - # Write our authors to the AUTHORS file - session.log("Writing AUTHORS") - with io.open(AUTHORS_FILE, "w", encoding="utf-8") as fp: - fp.write(u"\n".join(authors)) - fp.write(u"\n") - - -@nox.session -def generate_news(session): - session.log("Generating NEWS") - session.install("towncrier") - - # You can pass 2 possible arguments: --draft, --yes - session.run("towncrier", *session.posargs) - - -@nox.session -def release(session): - assert len(session.posargs) == 1, "A version number is expected" - new_version = session.posargs[0] - parts = new_version.split('.') - # Expect YY.N or YY.N.P - assert 2 <= len(parts) <= 3, parts - # Only integers - parts = list(map(int, parts)) - session.log("Generating commits for version {}".format(new_version)) - - session.log("Checking that nothing is staged") - # Non-zero exit code means that something is already staged - session.run("git", "diff", "--staged", "--exit-code", external=True) - - session.log(f"Updating {AUTHORS_FILE}") - generate_authors(session) - if subprocess.run(["git", "diff", "--exit-code"]).returncode: - session.run("git", "add", AUTHORS_FILE, external=True) - session.run( - "git", "commit", "-m", f"Updating {AUTHORS_FILE}", - external=True, +@nox.session(name="prepare-release") +def prepare_release(session): + version = release.get_version_from_arguments(session.posargs) + if not version: + session.error("Usage: nox -s prepare-release -- YY.N[.P]") + + session.log("# Ensure nothing is staged") + if release.modified_files_in_git("--staged"): + session.error("There are files staged in git") + + session.log(f"# Updating {AUTHORS_FILE}") + release.generate_authors(AUTHORS_FILE) + if release.modified_files_in_git(): + release.commit_file( + session, AUTHORS_FILE, message=f"Update {AUTHORS_FILE}", ) else: - session.log(f"No update needed for {AUTHORS_FILE}") + session.log(f"# No changes to {AUTHORS_FILE}") - session.log("Generating NEWS") - session.install("towncrier") - session.run("towncrier", "--yes", "--version", new_version) + session.log("# Generating NEWS") + release.generate_news(session, version) - session.log("Updating version") - update_version_file(new_version) - session.run("git", "add", VERSION_FILE, external=True) - session.run("git", "commit", "-m", f"Release {new_version}", external=True) + session.log(f"# Bumping for release {version}") + release.update_version_file(version, VERSION_FILE) + release.commit_file(session, VERSION_FILE, message="Bump for release") - session.log("Tagging release") - session.run( - "git", "tag", "-m", f"Release {new_version}", new_version, - external=True, - ) + session.log("# Tagging release") + release.create_git_tag(session, version, message=f"Release {version}") - next_dev_version = f"{parts[0]}.{parts[1] + 1}.dev0" - update_version_file(next_dev_version) - session.run("git", "add", VERSION_FILE, external=True) - session.run("git", "commit", "-m", "Back to development", external=True) + session.log("# Bumping for development") + next_dev_version = release.get_next_development_version(version) + release.update_version_file(next_dev_version, VERSION_FILE) + release.commit_file(session, VERSION_FILE, message="Bump for development") diff --git a/tools/automation/release/__init__.py b/tools/automation/release/__init__.py new file mode 100644 index 00000000000..e97d18008cc --- /dev/null +++ b/tools/automation/release/__init__.py @@ -0,0 +1,104 @@ +"""Helpers for release automation. + +These are written according to the order they are called in. +""" + +import io +import subprocess + + +def get_version_from_arguments(arguments): + """Checks the arguments passed to `nox -s release`. + + If there is only 1 argument that looks like a pip version, returns that. + Otherwise, returns None. + """ + if len(arguments) != 1: + return None + + version = arguments[0] + + parts = version.split('.') + if not 2 <= len(parts) <= 3: + # Not of the form: YY.N or YY.N.P + return None + + if not all(part.isdigit() for part in parts): + # Not all segments are integers. + return None + + # All is good. + return version + + +def modified_files_in_git(*args): + return subprocess.run( + ["git", "diff", "--no-patch", "--exit-code", *args], + capture_output=True, + ).returncode + + +def get_author_list(): + """Get the list of authors from Git commits. + """ + # subprocess because session.run doesn't give us stdout + result = subprocess.run( + ["git", "log", "--use-mailmap", "--format=%aN <%aE>"], + capture_output=True, + encoding="utf-8", + ) + + # Create a unique list. + authors = [] + seen_authors = set() + for author in result.stdout.splitlines(): + author = author.strip() + if author.lower() not in seen_authors: + seen_authors.add(author.lower()) + authors.append(author) + + # Sort our list of Authors by their case insensitive name + return sorted(authors, key=lambda x: x.lower()) + + +def generate_authors(filename: str) -> None: + # Get our list of authors + authors = get_author_list() + + # Write our authors to the AUTHORS file + with io.open(filename, "w", encoding="utf-8") as fp: + fp.write(u"\n".join(authors)) + fp.write(u"\n") + + +def commit_file(session, filename, *, message): + session.run("git", "add", filename, external=True, silent=True) + session.run("git", "commit", "-m", message, external=True, silent=True) + + +def generate_news(session, version): + session.install("towncrier") + session.run("towncrier", "--yes", "--version", version, silent=True) + + +def update_version_file(new_version, filepath): + with open(filepath, "w", encoding="utf-8") as f: + f.write('__version__ = "{}"\n'.format(new_version)) + + +def create_git_tag(session, tag_name, *, message): + session.run("git", "tag", "-m", message, tag_name, external=True, silent=True) + + +def get_next_development_version(version): + major, minor, *_ = map(int, version.split(".")) + + # We have at most 4 releases, starting with 0. Once we reach 3, we'd want + # to roll-over to the next year's release numbers. + if minor == 3: + major += 1 + minor = 0 + else: + minor += 1 + + return f"{major}.{minor}.dev0" From db66b6eaf5a4d1b81ee292474a87504afdf30beb Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 3 Nov 2019 01:22:53 +0530 Subject: [PATCH 0662/3170] Add a nox command to build release artifacts --- noxfile.py | 27 +++++++++++++++++++++++++++ tools/automation/release/__init__.py | 7 +++++++ 2 files changed, 34 insertions(+) diff --git a/noxfile.py b/noxfile.py index 21ee63a3db3..e03dbbd1d4c 100644 --- a/noxfile.py +++ b/noxfile.py @@ -4,6 +4,7 @@ # The following comment should be removed at some point in the future. # mypy: disallow-untyped-defs=False +import glob import os import shutil import sys @@ -182,3 +183,29 @@ def prepare_release(session): next_dev_version = release.get_next_development_version(version) release.update_version_file(next_dev_version, VERSION_FILE) release.commit_file(session, VERSION_FILE, message="Bump for development") + + +@nox.session(name="build-release") +def build_release(session): + version = release.get_version_from_arguments(session.posargs) + if not version: + session.error("Usage: nox -s upload-release -- YY.N[.P]") + + session.log("# Ensure no files in dist/") + if release.have_files_in_folder("dist"): + session.error("There are files in dist/. Remove them and try again") + + session.log("# Install dependencies") + session.install("setuptools", "wheel", "twine") + + session.log("# Checkout the tag") + session.run("git", "checkout", version, external=True, silent=True) + + session.log("# Build distributions") + session.run("python", "setup.py", "sdist", "bdist_wheel", silent=True) + + session.log("# Verify distributions") + session.run("twine", "check", *glob.glob("dist/*"), silent=True) + + session.log("# Checkout the master branch") + session.run("git", "checkout", "master", external=True, silent=True) diff --git a/tools/automation/release/__init__.py b/tools/automation/release/__init__.py index e97d18008cc..7717ccb51fd 100644 --- a/tools/automation/release/__init__.py +++ b/tools/automation/release/__init__.py @@ -4,6 +4,7 @@ """ import io +import os import subprocess @@ -102,3 +103,9 @@ def get_next_development_version(version): minor += 1 return f"{major}.{minor}.dev0" + + +def have_files_in_folder(folder_name): + if not os.path.exists(folder_name): + return False + return bool(os.listdir(folder_name)) From ca53a8bc561a5ea41ab6c0d3893325f88bca3b4d Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 3 Nov 2019 01:43:49 +0530 Subject: [PATCH 0663/3170] Document the simplified release process --- docs/html/development/release-process.rst | 29 +++++++---------------- 1 file changed, 9 insertions(+), 20 deletions(-) diff --git a/docs/html/development/release-process.rst b/docs/html/development/release-process.rst index 63f4c5e461b..fb954b0dff2 100644 --- a/docs/html/development/release-process.rst +++ b/docs/html/development/release-process.rst @@ -80,20 +80,13 @@ Creating a new release ---------------------- #. Checkout the current pip ``master`` branch. -#. Ensure you have the latest ``wheel``, ``setuptools``, ``twine`` and ``nox`` packages installed. -#. Generate a new ``AUTHORS.txt`` (``nox -s generate_authors``) and commit the - results. -#. Bump the version in ``pip/__init__.py`` to the release version and commit - the results. Usually this involves dropping just the ``.devN`` suffix on the - version. -#. Generate a new ``NEWS.rst`` (``nox -s generate_news``) and commit the - results. -#. Create a tag at the current commit, of the form ``YY.N`` - (``git tag YY.N``). -#. Checkout the tag (``git checkout YY.N``). -#. Create the distribution files (``python setup.py sdist bdist_wheel``). -#. Upload the distribution files to PyPI using twine - (``twine upload dist/*``). +#. Ensure you have the latest ``nox`` and ``twine`` installed. +#. Prepare for release using ``nox -s prepare-release -- YY.N``. + This will update the relevant files and tag the correct commit. +#. Build the release artifacts using ``nox -s build-release -- YY.N``. + This will checkout the tag, generate the distribution files to be + uploaded and checkout the master branch again. +#. Upload the distribution files to PyPI using ``twine upload dist/*``. #. Push all of the changes including the tag. #. Regenerate the ``get-pip.py`` script in the `get-pip repository`_ (as documented there) and commit the results. @@ -102,11 +95,6 @@ Creating a new release adjusting the versions listed in ``Lib/ensurepip/__init__.py``. -.. note:: - - Steps 3 to 6 are automated in ``nox -s release -- YY.N`` command. - - .. note:: If the release dropped the support of an obsolete Python version ``M.m``, @@ -117,6 +105,7 @@ Creating a new release .. note:: + If the ``get-pip.py`` script needs to be updated due to changes in pip internals and if the last ``M.m/get-pip.py`` published still uses the default template, make sure to first duplicate ``templates/default.py`` as ``templates/pre-YY.N.py`` @@ -135,7 +124,7 @@ order to create one of these the changes should already be merged into the command ``git checkout -b release/YY.N.Z+1 YY.N``. #. Cherry pick the fixed commits off of the ``master`` branch, fixing any conflicts. -#. Follow the steps 3 to 6 from above (or call ``nox -s release -- YY.N.Z+1``) +#. Run ``nox -s prepare-release -- YY.N.Z+1``. #. Merge master into your release branch and drop the news files that have been included in your release (otherwise they would also appear in the ``YY.N+1`` changelog) From 1a28d31002fb5db9e8692e60047ba4fd48561c4e Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 3 Nov 2019 02:01:11 +0530 Subject: [PATCH 0664/3170] Type Annotations! --- .pre-commit-config.yaml | 2 +- tools/automation/release/__init__.py | 29 ++++++++++++++++------------ 2 files changed, 18 insertions(+), 13 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0e327e88a25..04f655d08ee 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,7 +32,7 @@ repos: args: [] - id: mypy name: mypy, for Py2 - exclude: noxfile.py|docs|tests + exclude: noxfile.py|tools/automation/release|docs|tests args: ["-2"] - repo: https://github.com/pre-commit/pygrep-hooks diff --git a/tools/automation/release/__init__.py b/tools/automation/release/__init__.py index 7717ccb51fd..518ac522360 100644 --- a/tools/automation/release/__init__.py +++ b/tools/automation/release/__init__.py @@ -6,9 +6,12 @@ import io import os import subprocess +from typing import List, Optional, Set +from nox.sessions import Session -def get_version_from_arguments(arguments): + +def get_version_from_arguments(arguments: List[str]) -> Optional[str]: """Checks the arguments passed to `nox -s release`. If there is only 1 argument that looks like a pip version, returns that. @@ -32,14 +35,14 @@ def get_version_from_arguments(arguments): return version -def modified_files_in_git(*args): +def modified_files_in_git(*args: str) -> int: return subprocess.run( ["git", "diff", "--no-patch", "--exit-code", *args], capture_output=True, ).returncode -def get_author_list(): +def get_author_list() -> List[str]: """Get the list of authors from Git commits. """ # subprocess because session.run doesn't give us stdout @@ -51,7 +54,7 @@ def get_author_list(): # Create a unique list. authors = [] - seen_authors = set() + seen_authors: Set[str] = set() for author in result.stdout.splitlines(): author = author.strip() if author.lower() not in seen_authors: @@ -72,26 +75,28 @@ def generate_authors(filename: str) -> None: fp.write(u"\n") -def commit_file(session, filename, *, message): +def commit_file(session: Session, filename: str, *, message: str) -> None: session.run("git", "add", filename, external=True, silent=True) session.run("git", "commit", "-m", message, external=True, silent=True) -def generate_news(session, version): +def generate_news(session: Session, version: str) -> None: session.install("towncrier") session.run("towncrier", "--yes", "--version", version, silent=True) -def update_version_file(new_version, filepath): +def update_version_file(version: str, filepath: str) -> None: with open(filepath, "w", encoding="utf-8") as f: - f.write('__version__ = "{}"\n'.format(new_version)) + f.write('__version__ = "{}"\n'.format(version)) -def create_git_tag(session, tag_name, *, message): - session.run("git", "tag", "-m", message, tag_name, external=True, silent=True) +def create_git_tag(session: Session, tag_name: str, *, message: str) -> None: + session.run( + "git", "tag", "-m", message, tag_name, external=True, silent=True, + ) -def get_next_development_version(version): +def get_next_development_version(version: str) -> str: major, minor, *_ = map(int, version.split(".")) # We have at most 4 releases, starting with 0. Once we reach 3, we'd want @@ -105,7 +110,7 @@ def get_next_development_version(version): return f"{major}.{minor}.dev0" -def have_files_in_folder(folder_name): +def have_files_in_folder(folder_name: str) -> bool: if not os.path.exists(folder_name): return False return bool(os.listdir(folder_name)) From fca613b3812a42da9f3b15ce6b485279c0148515 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 3 Nov 2019 09:45:47 +0530 Subject: [PATCH 0665/3170] Build a dummy release on Azure Pipelines --- .azure-pipelines/jobs/package.yml | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/.azure-pipelines/jobs/package.yml b/.azure-pipelines/jobs/package.yml index 5ab5be31f92..8663720de9c 100644 --- a/.azure-pipelines/jobs/package.yml +++ b/.azure-pipelines/jobs/package.yml @@ -15,20 +15,19 @@ jobs: inputs: versionSpec: '3' - - bash: pip install twine nox setuptools wheel - displayName: Install dependencies - - - bash: nox -s generate_authors - displayName: Generate AUTHORS.txt + - bash: | + git config --global user.email "pypa-dev@googlegroups.com" + git config --global user.name "pip" + displayName: Setup Git credentials - - bash: nox -s generate_news -- --yes - displayName: Generate NEWS.rst + - bash: pip install nox + displayName: Install dependencies - - bash: python setup.py sdist bdist_wheel - displayName: Create sdist and wheel + - bash: nox -s prepare-release -- 99.9 + displayName: Prepare dummy release - - bash: twine check dist/* - displayName: Check distributions with twine + - bash: nox -s build-release -- 99.9 + displayName: Generate distributions for the dummy release - task: PublishBuildArtifacts@1 displayName: 'Publish Artifact: dist' From 44cc3aeb0ac17b2282410d90b533f8a8c5cbf4aa Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 3 Nov 2019 14:26:47 -0500 Subject: [PATCH 0666/3170] Use session from RequirePreparer, not Resolver --- src/pip/_internal/cli/req_command.py | 2 ++ src/pip/_internal/commands/download.py | 1 + src/pip/_internal/commands/install.py | 1 + src/pip/_internal/commands/wheel.py | 1 + src/pip/_internal/legacy_resolve.py | 2 +- src/pip/_internal/operations/prepare.py | 7 ++++--- tests/unit/test_req.py | 1 + 7 files changed, 11 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index dd51a44b4ba..2d0b299b27d 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -149,6 +149,7 @@ def make_requirement_preparer( temp_build_dir, # type: TempDirectory options, # type: Values req_tracker, # type: RequirementTracker + session, # type: PipSession download_dir=None, # type: str wheel_download_dir=None, # type: str ): @@ -166,6 +167,7 @@ def make_requirement_preparer( progress_bar=options.progress_bar, build_isolation=options.build_isolation, req_tracker=req_tracker, + session=session, ) @staticmethod diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index 865a5af1bbb..e71498b1946 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -129,6 +129,7 @@ def run(self, options, args): temp_build_dir=directory, options=options, req_tracker=req_tracker, + session=session, download_dir=options.download_dir, ) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 1e8741ac471..a7260fb0168 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -364,6 +364,7 @@ def run(self, options, args): temp_build_dir=directory, options=options, req_tracker=req_tracker, + session=session, ) resolver = self.make_resolver( preparer=preparer, diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index 3b656a3cc9b..2c78d221fc9 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -173,6 +173,7 @@ def run(self, options, args): temp_build_dir=directory, options=options, req_tracker=req_tracker, + session=session, wheel_download_dir=options.wheel_dir, ) diff --git a/src/pip/_internal/legacy_resolve.py b/src/pip/_internal/legacy_resolve.py index bd3c19dca68..bd991984ff5 100644 --- a/src/pip/_internal/legacy_resolve.py +++ b/src/pip/_internal/legacy_resolve.py @@ -307,7 +307,7 @@ def _get_abstract_dist_for(self, req, require_hashes): # We eagerly populate the link, since that's our "legacy" behavior. req.populate_link(self.finder, upgrade_allowed, require_hashes) abstract_dist = self.preparer.prepare_linked_requirement( - req, self.session, self.finder, require_hashes + req, self.finder, require_hashes ) # NOTE diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 1cb8178e319..0bd7386efcc 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -544,7 +544,8 @@ def __init__( wheel_download_dir, # type: Optional[str] progress_bar, # type: str build_isolation, # type: bool - req_tracker # type: RequirementTracker + req_tracker, # type: RequirementTracker + session, # type: PipSession ): # type: (...) -> None super(RequirementPreparer, self).__init__() @@ -552,6 +553,7 @@ def __init__( self.src_dir = src_dir self.build_dir = build_dir self.req_tracker = req_tracker + self.session = session # Where still-packed archives should be written to. If None, they are # not saved, and are deleted immediately after unpacking. @@ -593,7 +595,6 @@ def _download_should_save(self): def prepare_linked_requirement( self, req, # type: InstallRequirement - session, # type: PipSession finder, # type: PackageFinder require_hashes, # type: bool ): @@ -672,7 +673,7 @@ def prepare_linked_requirement( try: unpack_url( link, req.source_dir, download_dir, - session=session, hashes=hashes, + session=self.session, hashes=hashes, progress_bar=self.progress_bar ) except requests.HTTPError as exc: diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index 4dcdc311780..47ffaf9adaf 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -75,6 +75,7 @@ def _basic_resolver(self, finder, require_hashes=False): progress_bar="on", build_isolation=True, req_tracker=RequirementTracker(), + session=PipSession(), ) make_install_req = partial( install_req_from_req_string, From 3076c39f255353bf39de59ff8aec71ffb7fecaac Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 3 Nov 2019 14:31:38 -0500 Subject: [PATCH 0667/3170] Remove unused Resolver.session --- src/pip/_internal/cli/req_command.py | 2 -- src/pip/_internal/commands/download.py | 1 - src/pip/_internal/commands/install.py | 1 - src/pip/_internal/commands/wheel.py | 1 - src/pip/_internal/legacy_resolve.py | 3 --- tests/unit/test_req.py | 2 +- 6 files changed, 1 insertion(+), 9 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 2d0b299b27d..5240dd86614 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -173,7 +173,6 @@ def make_requirement_preparer( @staticmethod def make_resolver( preparer, # type: RequirementPreparer - session, # type: PipSession finder, # type: PackageFinder options, # type: Values wheel_cache=None, # type: Optional[WheelCache] @@ -197,7 +196,6 @@ def make_resolver( ) return Resolver( preparer=preparer, - session=session, finder=finder, make_install_req=make_install_req, use_user_site=use_user_site, diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index e71498b1946..b952bb22515 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -136,7 +136,6 @@ def run(self, options, args): resolver = self.make_resolver( preparer=preparer, finder=finder, - session=session, options=options, py_version_info=options.python_version, ) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index a7260fb0168..1e1d273fe9a 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -369,7 +369,6 @@ def run(self, options, args): resolver = self.make_resolver( preparer=preparer, finder=finder, - session=session, options=options, wheel_cache=wheel_cache, use_user_site=options.use_user_site, diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index 2c78d221fc9..a6912717fc2 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -180,7 +180,6 @@ def run(self, options, args): resolver = self.make_resolver( preparer=preparer, finder=finder, - session=session, options=options, wheel_cache=wheel_cache, ignore_requires_python=options.ignore_requires_python, diff --git a/src/pip/_internal/legacy_resolve.py b/src/pip/_internal/legacy_resolve.py index bd991984ff5..43c3b80c641 100644 --- a/src/pip/_internal/legacy_resolve.py +++ b/src/pip/_internal/legacy_resolve.py @@ -45,7 +45,6 @@ from pip._vendor import pkg_resources from pip._internal.distributions import AbstractDistribution - from pip._internal.network.session import PipSession from pip._internal.index.package_finder import PackageFinder from pip._internal.operations.prepare import RequirementPreparer from pip._internal.req.req_install import InstallRequirement @@ -116,7 +115,6 @@ class Resolver(object): def __init__( self, preparer, # type: RequirementPreparer - session, # type: PipSession finder, # type: PackageFinder make_install_req, # type: InstallRequirementProvider use_user_site, # type: bool @@ -141,7 +139,6 @@ def __init__( self.preparer = preparer self.finder = finder - self.session = session self.require_hashes_option = require_hashes diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index 47ffaf9adaf..fcee96509aa 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -86,7 +86,7 @@ def _basic_resolver(self, finder, require_hashes=False): return Resolver( preparer=preparer, make_install_req=make_install_req, - session=PipSession(), finder=finder, + finder=finder, use_user_site=False, upgrade_strategy="to-satisfy-only", ignore_dependencies=False, ignore_installed=False, ignore_requires_python=False, force_reinstall=False, From 5da6abf60de0897d97742b9dcd68f4223d9d244e Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 3 Nov 2019 15:16:42 -0500 Subject: [PATCH 0668/3170] Move resp_chunks to network.utils This generic piece of code only clutters operations.prepare, making it harder to refactor. --- src/pip/_internal/network/utils.py | 48 +++++++++++++++++++++++++ src/pip/_internal/operations/prepare.py | 40 ++------------------- 2 files changed, 50 insertions(+), 38 deletions(-) create mode 100644 src/pip/_internal/network/utils.py diff --git a/src/pip/_internal/network/utils.py b/src/pip/_internal/network/utils.py new file mode 100644 index 00000000000..a19050b0f70 --- /dev/null +++ b/src/pip/_internal/network/utils.py @@ -0,0 +1,48 @@ +from pip._vendor.requests.models import CONTENT_CHUNK_SIZE, Response + +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import Iterator + + +def response_chunks(response, chunk_size=CONTENT_CHUNK_SIZE): + # type: (Response, int) -> Iterator[bytes] + """Given a requests Response, provide the data chunks. + """ + try: + # Special case for urllib3. + for chunk in response.raw.stream( + chunk_size, + # We use decode_content=False here because we don't + # want urllib3 to mess with the raw bytes we get + # from the server. If we decompress inside of + # urllib3 then we cannot verify the checksum + # because the checksum will be of the compressed + # file. This breakage will only occur if the + # server adds a Content-Encoding header, which + # depends on how the server was configured: + # - Some servers will notice that the file isn't a + # compressible file and will leave the file alone + # and with an empty Content-Encoding + # - Some servers will notice that the file is + # already compressed and will leave the file + # alone and will add a Content-Encoding: gzip + # header + # - Some servers won't notice anything at all and + # will take a file that's already been compressed + # and compress it again and set the + # Content-Encoding: gzip header + # + # By setting this not to decode automatically we + # hope to eliminate problems with the second case. + decode_content=False, + ): + yield chunk + except AttributeError: + # Standard file-like object. + while True: + chunk = response.raw.read(chunk_size) + if not chunk: + break + yield chunk diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 1cb8178e319..6c3790eb79a 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -30,6 +30,7 @@ ) from pip._internal.models.index import PyPI from pip._internal.network.session import PipSession +from pip._internal.network.utils import response_chunks from pip._internal.utils.compat import expanduser from pip._internal.utils.filesystem import copy2_fixed from pip._internal.utils.hashes import MissingHashes @@ -136,43 +137,6 @@ def _download_url( else: show_progress = False - def resp_read(chunk_size): - try: - # Special case for urllib3. - for chunk in resp.raw.stream( - chunk_size, - # We use decode_content=False here because we don't - # want urllib3 to mess with the raw bytes we get - # from the server. If we decompress inside of - # urllib3 then we cannot verify the checksum - # because the checksum will be of the compressed - # file. This breakage will only occur if the - # server adds a Content-Encoding header, which - # depends on how the server was configured: - # - Some servers will notice that the file isn't a - # compressible file and will leave the file alone - # and with an empty Content-Encoding - # - Some servers will notice that the file is - # already compressed and will leave the file - # alone and will add a Content-Encoding: gzip - # header - # - Some servers won't notice anything at all and - # will take a file that's already been compressed - # and compress it again and set the - # Content-Encoding: gzip header - # - # By setting this not to decode automatically we - # hope to eliminate problems with the second case. - decode_content=False): - yield chunk - except AttributeError: - # Standard file-like object. - while True: - chunk = resp.raw.read(chunk_size) - if not chunk: - break - yield chunk - def written_chunks(chunks): for chunk in chunks: content_file.write(chunk) @@ -199,7 +163,7 @@ def written_chunks(chunks): downloaded_chunks = written_chunks( progress_indicator( - resp_read(CONTENT_CHUNK_SIZE), + response_chunks(resp, CONTENT_CHUNK_SIZE), CONTENT_CHUNK_SIZE ) ) From e8976af4e1ba90b7756939ef80c4a0530de1f8a0 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 3 Nov 2019 15:25:59 -0500 Subject: [PATCH 0669/3170] Show download size if available This simplifies the logic for displaying output. --- src/pip/_internal/operations/prepare.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 6c3790eb79a..661b44dc37e 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -152,10 +152,9 @@ def written_chunks(chunks): if show_progress: # We don't show progress on cached responses progress_indicator = DownloadProgressProvider(progress_bar, max=total_length) - if total_length: - logger.info("Downloading %s (%s)", url, format_size(total_length)) - else: - logger.info("Downloading %s", url) + + if total_length: + logger.info("Downloading %s (%s)", url, format_size(total_length)) elif cached_resp: logger.info("Using cached %s", url) else: From b873df49b0a2bd589fb8edb9cc3e68af98af755d Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 3 Nov 2019 15:29:19 -0500 Subject: [PATCH 0670/3170] Factor from_cache check into separate function --- src/pip/_internal/network/cache.py | 6 ++++++ src/pip/_internal/operations/prepare.py | 6 +++--- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/network/cache.py b/src/pip/_internal/network/cache.py index d23c0ffaf5a..c9386e17360 100644 --- a/src/pip/_internal/network/cache.py +++ b/src/pip/_internal/network/cache.py @@ -9,6 +9,7 @@ from pip._vendor.cachecontrol.cache import BaseCache from pip._vendor.cachecontrol.caches import FileCache +from pip._vendor.requests.models import Response from pip._internal.utils.filesystem import adjacent_tmp_file, replace from pip._internal.utils.misc import ensure_dir @@ -18,6 +19,11 @@ from typing import Optional +def is_from_cache(response): + # type: (Response) -> bool + return getattr(response, "from_cache", False) + + @contextmanager def suppressed_cache_errors(): """If we can't access the cache then we can just skip caching and process diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 661b44dc37e..26a5636ec82 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -29,6 +29,7 @@ VcsHashUnsupported, ) from pip._internal.models.index import PyPI +from pip._internal.network.cache import is_from_cache from pip._internal.network.session import PipSession from pip._internal.network.utils import response_chunks from pip._internal.utils.compat import expanduser @@ -125,10 +126,9 @@ def _download_url( 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: + elif is_from_cache(resp): show_progress = False elif total_length > (40 * 1000): show_progress = True @@ -155,7 +155,7 @@ def written_chunks(chunks): if total_length: logger.info("Downloading %s (%s)", url, format_size(total_length)) - elif cached_resp: + elif is_from_cache(resp): logger.info("Using cached %s", url) else: logger.info("Downloading %s", url) From ba60397dd4dd2dfa3dda01210c0588f99ba8fc8d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Sat, 2 Nov 2019 19:07:30 +0100 Subject: [PATCH 0671/3170] Make pip wheel cache what it built Just like pip install. --- news/6852.feature | 5 ++++ src/pip/_internal/wheel.py | 45 +++++++++++++++++++++------------- tests/functional/test_wheel.py | 24 ++++++++++++++++++ 3 files changed, 57 insertions(+), 17 deletions(-) create mode 100644 news/6852.feature diff --git a/news/6852.feature b/news/6852.feature new file mode 100644 index 00000000000..30097e82a8f --- /dev/null +++ b/news/6852.feature @@ -0,0 +1,5 @@ +Cache wheels that ``pip wheel`` built locally, matching what +``pip install`` does. This particularly helps performance in workflows where +``pip wheel`` is used for `building before installing +<https://pip.pypa.io/en/stable/user_guide/#installing-from-local-packages>`_. +Users desiring the original behavior can use ``pip wheel --no-cache-dir``. diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index 71d9765ce42..ac4bf029f14 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -1102,7 +1102,7 @@ def build( :param should_unpack: If True, after building the wheel, unpack it and replace the sdist with the unpacked version in preparation for installation. - :return: True if all the wheels built correctly. + :return: The list of InstallRequirement that failed to build. """ # pip install uses should_unpack=True. # pip install never provides a _wheel_dir. @@ -1124,19 +1124,15 @@ def build( ): continue - # Determine where the wheel should go. - if should_unpack: - if ( - cache_available and - should_cache(req, self.check_binary_allowed) - ): - output_dir = self.wheel_cache.get_path_for_link(req.link) - else: - output_dir = self.wheel_cache.get_ephem_path_for_link( - req.link - ) + if ( + cache_available and + should_cache(req, self.check_binary_allowed) + ): + output_dir = self.wheel_cache.get_path_for_link(req.link) else: - output_dir = self._wheel_dir + output_dir = self.wheel_cache.get_ephem_path_for_link( + req.link + ) buildset.append((req, output_dir)) @@ -1174,10 +1170,6 @@ def build( python_tag=python_tag, ) if wheel_file: - build_success.append(req) - self.wheel_filenames.append( - os.path.relpath(wheel_file, output_dir) - ) if should_unpack: # XXX: This is mildly duplicative with prepare_files, # but not close enough to pull out to a single common @@ -1202,6 +1194,25 @@ def build( assert req.link.is_wheel # extract the wheel into the dir unpack_file(req.link.file_path, req.source_dir) + else: + # copy from cache to target directory + try: + ensure_dir(self._wheel_dir) + shutil.copy( + os.path.join(output_dir, wheel_file), + self._wheel_dir, + ) + except OSError as e: + logger.warning( + "Building wheel for %s failed: %s", + req.name, e, + ) + build_failure.append(req) + continue + self.wheel_filenames.append( + os.path.relpath(wheel_file, output_dir) + ) + build_success.append(req) else: build_failure.append(req) diff --git a/tests/functional/test_wheel.py b/tests/functional/test_wheel.py index 92f2c9ef479..b123d0693cd 100644 --- a/tests/functional/test_wheel.py +++ b/tests/functional/test_wheel.py @@ -62,6 +62,30 @@ def test_pip_wheel_success(script, data): assert "Successfully built simple" in result.stdout, result.stdout +def test_pip_wheel_build_cache(script, data): + """ + Test 'pip wheel' builds and caches. + """ + result = script.pip( + 'wheel', '--no-index', '-f', data.find_links, + 'simple==3.0', + ) + wheel_file_name = 'simple-3.0-py%s-none-any.whl' % pyversion[0] + wheel_file_path = script.scratch / wheel_file_name + assert wheel_file_path in result.files_created, result.stdout + assert "Successfully built simple" in result.stdout, result.stdout + # remove target file + (script.scratch_path / wheel_file_name).unlink() + # pip wheel again and test that no build occurs since + # we get the wheel from cache + result = script.pip( + 'wheel', '--no-index', '-f', data.find_links, + 'simple==3.0', + ) + assert wheel_file_path in result.files_created, result.stdout + assert "Successfully built simple" not in result.stdout, result.stdout + + def test_basic_pip_wheel_downloads_wheels(script, data): """ Test 'pip wheel' downloads wheels From 54efa9dbdb2514211ef7dd83dba2c24c4f5d75a6 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 3 Nov 2019 16:26:37 -0500 Subject: [PATCH 0672/3170] Reorder operations.prepare._download_url --- src/pip/_internal/operations/prepare.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 26a5636ec82..664cbc048ee 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -126,6 +126,18 @@ def _download_url( except (ValueError, KeyError, TypeError): total_length = 0 + if link.netloc == PyPI.file_storage_domain: + url = link.show_url + else: + url = link.url_without_fragment + + if total_length: + logger.info("Downloading %s (%s)", url, format_size(total_length)) + elif is_from_cache(resp): + logger.info("Using cached %s", url) + else: + logger.info("Downloading %s", url) + if logger.getEffectiveLevel() > logging.INFO: show_progress = False elif is_from_cache(resp): @@ -144,22 +156,10 @@ def written_chunks(chunks): progress_indicator = _progress_indicator - if link.netloc == PyPI.file_storage_domain: - url = link.show_url - else: - url = link.url_without_fragment - if show_progress: # We don't show progress on cached responses progress_indicator = DownloadProgressProvider(progress_bar, max=total_length) - if total_length: - logger.info("Downloading %s (%s)", url, format_size(total_length)) - elif is_from_cache(resp): - logger.info("Using cached %s", url) - else: - logger.info("Downloading %s", url) - downloaded_chunks = written_chunks( progress_indicator( response_chunks(resp, CONTENT_CHUNK_SIZE), From f5eba4f726294120b3cdd3a0d1303f821536a3ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Sat, 2 Nov 2019 15:14:39 +0100 Subject: [PATCH 0673/3170] Add tests for git sha wheel caching --- tests/functional/test_install_vcs_git.py | 41 ++++++++++++++++++++++-- tests/unit/test_wheel.py | 17 +++++++++- 2 files changed, 55 insertions(+), 3 deletions(-) diff --git a/tests/functional/test_install_vcs_git.py b/tests/functional/test_install_vcs_git.py index e19745bb8a2..01fec7a8bd4 100644 --- a/tests/functional/test_install_vcs_git.py +++ b/tests/functional/test_install_vcs_git.py @@ -67,7 +67,7 @@ def _github_checkout(url_path, temp_dir, rev=None, egg=None, scheme=None): return local_url -def _make_version_pkg_url(path, rev=None): +def _make_version_pkg_url(path, rev=None, name="version_pkg"): """ Return a "git+file://" URL to the version_pkg test package. @@ -78,7 +78,7 @@ def _make_version_pkg_url(path, rev=None): """ file_url = _test_path_to_file_url(path) url_rev = '' if rev is None else '@{}'.format(rev) - url = 'git+{}{}#egg=version_pkg'.format(file_url, url_rev) + url = 'git+{}{}#egg={}'.format(file_url, url_rev, name) return url @@ -476,3 +476,40 @@ def test_check_submodule_addition(script): script.venv / 'src/version-pkg/testpkg/static/testfile2' in update_result.files_created ) + + +def test_install_git_branch_not_cached(script, with_wheel): + """ + Installing git urls with a branch revision does not cause wheel caching. + """ + PKG = "gitbranchnotcached" + repo_dir = _create_test_package(script, name=PKG) + url = _make_version_pkg_url(repo_dir, rev="master", name=PKG) + result = script.pip("install", url, "--only-binary=:all:") + assert "Successfully built {}".format(PKG) in result.stdout, result.stdout + script.pip("uninstall", "-y", PKG) + # build occurs on the second install too because it is not cached + result = script.pip("install", url) + assert ( + "Successfully built {}".format(PKG) in result.stdout + ), result.stdout + + +def test_install_git_sha_cached(script, with_wheel): + """ + Installing git urls with a sha revision does cause wheel caching. + """ + PKG = "gitshacached" + repo_dir = _create_test_package(script, name=PKG) + commit = script.run( + 'git', 'rev-parse', 'HEAD', cwd=repo_dir + ).stdout.strip() + url = _make_version_pkg_url(repo_dir, rev=commit, name=PKG) + result = script.pip("install", url) + assert "Successfully built {}".format(PKG) in result.stdout, result.stdout + script.pip("uninstall", "-y", PKG) + # build does not occur on the second install because it is cached + result = script.pip("install", url) + assert ( + "Successfully built {}".format(PKG) not in result.stdout + ), result.stdout diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 05413f9074e..fc3a7ddb382 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -20,7 +20,7 @@ MissingCallableSuffix, _raise_for_invalid_entrypoint, ) -from tests.lib import DATA_DIR, assert_paths_equal +from tests.lib import DATA_DIR, _create_test_package, assert_paths_equal class ReqMock: @@ -166,6 +166,21 @@ def check_binary_allowed(req): assert should_cache is expected +def test_should_cache_git_sha(script, tmpdir): + repo_path = _create_test_package(script, name="mypkg") + commit = script.run( + "git", "rev-parse", "HEAD", cwd=repo_path + ).stdout.strip() + # a link referencing a sha should be cached + url = "git+https://g.c/o/r@" + commit + "#egg=mypkg" + req = ReqMock(link=Link(url), source_dir=repo_path) + assert wheel.should_cache(req, check_binary_allowed=lambda r: True) + # a link not referencing a sha should not be cached + url = "git+https://g.c/o/r@master#egg=mypkg" + req = ReqMock(link=Link(url), source_dir=repo_path) + assert not wheel.should_cache(req, check_binary_allowed=lambda r: True) + + def test_format_command_result__INFO(caplog): caplog.set_level(logging.INFO) actual = wheel.format_command_result( From 9cad519521211c0a5af7bde34ff55e140e2856d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Sat, 10 Aug 2019 12:54:16 +0200 Subject: [PATCH 0674/3170] Cache wheels built from immutable Git requirements Cache wheels that are built from Git requirements that contain an immutable revision (i.e. a sha). --- docs/html/reference/pip_install.rst | 3 +++ news/6640.feature | 2 ++ src/pip/_internal/vcs/git.py | 9 ++++++++- src/pip/_internal/vcs/versioncontrol.py | 14 ++++++++++++++ src/pip/_internal/wheel.py | 11 ++++++++++- tests/functional/test_vcs_git.py | 17 +++++++++++++++++ 6 files changed, 54 insertions(+), 2 deletions(-) create mode 100644 news/6640.feature diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index 1418e0f58f8..5cbb139064b 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -573,6 +573,9 @@ and use any packages found there. This is disabled via the same of that is not part of the pip API. As of 7.0, pip makes a subdirectory for each sdist that wheels are built from and places the resulting wheels inside. +As of version 20.0, pip also caches wheels when building from an immutable Git +reference (i.e. a commit hash). + Pip attempts to choose the best wheels from those built in preference to building a new wheel. Note that this means when a package has both optional C extensions and builds ``py`` tagged wheels when the C extension can't be built diff --git a/news/6640.feature b/news/6640.feature new file mode 100644 index 00000000000..cb7e939dabb --- /dev/null +++ b/news/6640.feature @@ -0,0 +1,2 @@ +Cache wheels built from Git requirements that are considered immutable, +because they point to a commit hash. diff --git a/src/pip/_internal/vcs/git.py b/src/pip/_internal/vcs/git.py index 92b84571406..28a4fbae545 100644 --- a/src/pip/_internal/vcs/git.py +++ b/src/pip/_internal/vcs/git.py @@ -12,7 +12,7 @@ from pip._vendor.six.moves.urllib import request as urllib_request from pip._internal.exceptions import BadCommand -from pip._internal.utils.misc import display_path +from pip._internal.utils.misc import display_path, hide_url from pip._internal.utils.subprocess import make_command from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -59,6 +59,13 @@ class Git(VersionControl): def get_base_rev_args(rev): return [rev] + def is_immutable_rev_checkout(self, url, dest): + # type: (str, str) -> bool + _, rev_options = self.get_url_rev_options(hide_url(url)) + if not rev_options.rev: + return False + return self.is_commit_id_equal(dest, rev_options.rev) + def get_git_version(self): VERSION_PFX = 'git version ' version = self.run_command(['version'], show_stdout=False) diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index 0eccf436a76..a1742cfc814 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -329,6 +329,20 @@ def get_base_rev_args(rev): """ raise NotImplementedError + def is_immutable_rev_checkout(self, url, dest): + # type: (str, str) -> bool + """ + Return true if the commit hash checked out at dest matches + the revision in url. + + Always return False, if the VCS does not support immutable commit + hashes. + + This method does not check if there are local uncommitted changes + in dest after checkout, as pip currently has no use case for that. + """ + return False + @classmethod def make_rev_options(cls, rev=None, extra_args=None): # type: (Optional[str], Optional[CommandArgs]) -> RevOptions diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index ac4bf029f14..0af58056d38 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -52,6 +52,7 @@ from pip._internal.utils.ui import open_spinner from pip._internal.utils.unpacking import unpack_file from pip._internal.utils.urls import path_to_url +from pip._internal.vcs import vcs if MYPY_CHECK_RUNNING: from typing import ( @@ -838,7 +839,15 @@ def should_cache( return False if req.link and req.link.is_vcs: - # VCS checkout. Build wheel just for this run. + # VCS checkout. Build wheel just for this run + # unless it points to an immutable commit hash in which + # case it can be cached. + assert not req.editable + assert req.source_dir + vcs_backend = vcs.get_backend_for_scheme(req.link.scheme) + assert vcs_backend + if vcs_backend.is_immutable_rev_checkout(req.link.url, req.source_dir): + return True return False link = req.link diff --git a/tests/functional/test_vcs_git.py b/tests/functional/test_vcs_git.py index 606cbe1ef7c..c3b23afa022 100644 --- a/tests/functional/test_vcs_git.py +++ b/tests/functional/test_vcs_git.py @@ -221,3 +221,20 @@ def test_is_commit_id_equal(script): assert not Git.is_commit_id_equal(version_pkg_path, 'abc123') # Also check passing a None value. assert not Git.is_commit_id_equal(version_pkg_path, None) + + +def test_is_immutable_rev_checkout(script): + version_pkg_path = _create_test_package(script) + commit = script.run( + 'git', 'rev-parse', 'HEAD', + cwd=version_pkg_path + ).stdout.strip() + assert Git().is_immutable_rev_checkout( + "git+https://g.c/o/r@" + commit, version_pkg_path + ) + assert not Git().is_immutable_rev_checkout( + "git+https://g.c/o/r", version_pkg_path + ) + assert not Git().is_immutable_rev_checkout( + "git+https://g.c/o/r@master", version_pkg_path + ) From a20395d0dba8621907ff80e4d679585a31fb5a30 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Sat, 2 Nov 2019 15:54:30 +0100 Subject: [PATCH 0675/3170] Be extra safe in caching Git urls Do not cache in case a branch/tag has the same name as a commit sha. --- src/pip/_internal/vcs/git.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/vcs/git.py b/src/pip/_internal/vcs/git.py index 28a4fbae545..7483303a94b 100644 --- a/src/pip/_internal/vcs/git.py +++ b/src/pip/_internal/vcs/git.py @@ -64,7 +64,17 @@ def is_immutable_rev_checkout(self, url, dest): _, rev_options = self.get_url_rev_options(hide_url(url)) if not rev_options.rev: return False - return self.is_commit_id_equal(dest, rev_options.rev) + if not self.is_commit_id_equal(dest, rev_options.rev): + # the current commit is different from rev, + # which means rev was something else than a commit hash + return False + # return False in the rare case rev is both a commit hash + # and a tag or a branch; we don't want to cache in that case + # because that branch/tag could point to something else in the future + is_tag_or_branch = bool( + self.get_revision_sha(dest, rev_options.rev)[0] + ) + return not is_tag_or_branch def get_git_version(self): VERSION_PFX = 'git version ' From 204a0043776cc3e0cc74841c051c8da831c2ca4b Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 3 Nov 2019 13:09:18 -0500 Subject: [PATCH 0676/3170] Add test helpers for HTTP(S) server and certs --- tests/conftest.py | 19 ++++ tests/lib/certs.py | 53 ++++++++++ tests/lib/server.py | 181 +++++++++++++++++++++++++++++++++++ tools/requirements/tests.txt | 2 + 4 files changed, 255 insertions(+) create mode 100644 tests/lib/certs.py create mode 100644 tests/lib/server.py diff --git a/tests/conftest.py b/tests/conftest.py index 6a05ca5ea00..46213a127bc 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,6 +13,7 @@ from pip._internal.main import main as pip_entry_point from tests.lib import DATA_DIR, SRC_DIR, TestData +from tests.lib.certs import make_tls_cert, serialize_cert, serialize_key from tests.lib.path import Path from tests.lib.scripttest import PipTestEnvironment from tests.lib.venv import VirtualEnvironment @@ -385,3 +386,21 @@ def in_memory_pip(): def deprecated_python(): """Used to indicate whether pip deprecated this python version""" return sys.version_info[:2] in [(2, 7)] + + +@pytest.fixture(scope="session") +def cert_factory(tmpdir_factory): + def factory(): + # type: () -> str + """Returns path to cert/key file. + """ + output_path = Path(str(tmpdir_factory.mktemp("certs"))) / "cert.pem" + # Must be Text on PY2. + cert, key = make_tls_cert(u"localhost") + with open(str(output_path), "wb") as f: + f.write(serialize_cert(cert)) + f.write(serialize_key(key)) + + return str(output_path) + + return factory diff --git a/tests/lib/certs.py b/tests/lib/certs.py new file mode 100644 index 00000000000..7d86ee4c04e --- /dev/null +++ b/tests/lib/certs.py @@ -0,0 +1,53 @@ +from datetime import datetime, timedelta + +from cryptography import x509 +from cryptography.hazmat.backends import default_backend +from cryptography.hazmat.primitives import hashes, serialization +from cryptography.hazmat.primitives.asymmetric import rsa +from cryptography.x509.oid import NameOID + +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import Text, Tuple + + +def make_tls_cert(hostname): + # type: (Text) -> Tuple[x509.Certificate, rsa.RSAPrivateKey] + key = rsa.generate_private_key( + public_exponent=65537, + key_size=2048, + backend=default_backend() + ) + subject = issuer = x509.Name([ + x509.NameAttribute(NameOID.COMMON_NAME, hostname), + ]) + cert = ( + x509.CertificateBuilder() + .subject_name(subject) + .issuer_name(issuer) + .public_key(key.public_key()) + .serial_number(x509.random_serial_number()) + .not_valid_before(datetime.utcnow()) + .not_valid_after(datetime.utcnow() + timedelta(days=10)) + .add_extension( + x509.SubjectAlternativeName([x509.DNSName(hostname)]), + critical=False, + ) + .sign(key, hashes.SHA256(), default_backend()) + ) + return cert, key + + +def serialize_key(key): + # type: (rsa.RSAPrivateKey) -> bytes + return key.private_bytes( + encoding=serialization.Encoding.PEM, + format=serialization.PrivateFormat.TraditionalOpenSSL, + encryption_algorithm=serialization.NoEncryption(), + ) + + +def serialize_cert(cert): + # type: (x509.Certificate) -> bytes + return cert.public_bytes(serialization.Encoding.PEM) diff --git a/tests/lib/server.py b/tests/lib/server.py new file mode 100644 index 00000000000..0745abaee6b --- /dev/null +++ b/tests/lib/server.py @@ -0,0 +1,181 @@ +import os +import signal +import ssl +import threading +from contextlib import contextmanager +from textwrap import dedent + +from mock import Mock +from pip._vendor.contextlib2 import nullcontext +from werkzeug.serving import WSGIRequestHandler +from werkzeug.serving import make_server as _make_server + +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from types import TracebackType + from typing import ( + Any, Callable, Dict, Iterable, List, Optional, Text, Tuple, Type, Union + ) + + from werkzeug.serving import BaseWSGIServer + + Environ = Dict[str, str] + Status = str + Headers = Iterable[Tuple[str, str]] + ExcInfo = Tuple[Type[BaseException], BaseException, TracebackType] + Write = Callable[[bytes], None] + StartResponse = Callable[[Status, Headers, Optional[ExcInfo]], Write] + Body = List[bytes] + Responder = Callable[[Environ, StartResponse], Body] + + class MockServer(BaseWSGIServer): + mock = Mock() # type: Mock + +# Applies on Python 2 and Windows. +if not hasattr(signal, "pthread_sigmask"): + # We're not relying on this behavior anywhere currently, it's just best + # practice. + blocked_signals = nullcontext +else: + @contextmanager + def blocked_signals(): + """Block all signals for e.g. starting a worker thread. + """ + old_mask = signal.pthread_sigmask( + signal.SIG_SETMASK, range(1, signal.NSIG) + ) + try: + yield + finally: + signal.pthread_sigmask(signal.SIG_SETMASK, old_mask) + + +class RequestHandler(WSGIRequestHandler): + def make_environ(self): + environ = super(RequestHandler, self).make_environ() + + # From pallets/werkzeug#1469, will probably be in release after + # 0.16.0. + try: + # binary_form=False gives nicer information, but wouldn't be + # compatible with what Nginx or Apache could return. + peer_cert = self.connection.getpeercert(binary_form=True) + if peer_cert is not None: + # Nginx and Apache use PEM format. + environ["SSL_CLIENT_CERT"] = ssl.DER_cert_to_PEM_cert( + peer_cert, + ) + except ValueError: + # SSL handshake hasn't finished. + self.server.log("error", "Cannot fetch SSL peer certificate info") + except AttributeError: + # Not using TLS, the socket will not have getpeercert(). + pass + + return environ + + +def mock_wsgi_adapter(mock): + # type: (Callable[[Environ, StartResponse], Responder]) -> Responder + """Uses a mock to record function arguments and provide + the actual function that should respond. + """ + def adapter(environ, start_response): + # type: (Environ, StartResponse) -> Body + responder = mock(environ, start_response) + return responder(environ, start_response) + + return adapter + + +def make_mock_server(**kwargs): + # type: (Any) -> MockServer + kwargs.setdefault("request_handler", RequestHandler) + + mock = Mock() + app = mock_wsgi_adapter(mock) + server = _make_server("localhost", 0, app=app, **kwargs) + server.mock = mock + return server + + +@contextmanager +def server_running(server): + # type: (BaseWSGIServer) -> None + thread = threading.Thread(target=server.serve_forever) + thread.daemon = True + with blocked_signals(): + thread.start() + try: + yield + finally: + server.shutdown() + thread.join() + + +# Helper functions for making responses in a declarative way. + + +def text_html_response(text): + # type: (Text) -> Responder + def responder(environ, start_response): + # type: (Environ, StartResponse) -> Body + start_response("200 OK", [ + ("Content-Type", "text/html; charset=UTF-8"), + ]) + return [text.encode('utf-8')] + + return responder + + +def html5_page(text): + # type: (Union[Text, str]) -> Text + return dedent(u""" + <!DOCTYPE html> + <html> + <body> + {} + </body> + </html> + """).strip().format(text) + + +def index_page(spec): + # type: (Dict[str, str]) -> Responder + def link(name, value): + return '<a href="{}">{}</a>'.format( + value, name + ) + + links = ''.join(link(*kv) for kv in spec.items()) + return text_html_response(html5_page(links)) + + +def package_page(spec): + # type: (Dict[str, str]) -> Responder + def link(name, value): + return '<a href="{}">{}</a>'.format( + value, name + ) + + links = ''.join(link(*kv) for kv in spec.items()) + return text_html_response(html5_page(links)) + + +def file_response(path): + # type: (str) -> Responder + def responder(environ, start_response): + # type: (Environ, StartResponse) -> Body + size = os.stat(path).st_size + start_response( + "200 OK", [ + ("Content-Type", "application/octet-stream"), + ("Content-Length", str(size)), + ], + ) + + with open(path, 'rb') as f: + return [f.read()] + + return responder diff --git a/tools/requirements/tests.txt b/tools/requirements/tests.txt index 53601160b22..747ea321fb6 100644 --- a/tools/requirements/tests.txt +++ b/tools/requirements/tests.txt @@ -1,3 +1,4 @@ +cryptography==2.8 freezegun mock pretend @@ -12,4 +13,5 @@ pyyaml setuptools>=39.2.0 # Needed for `setuptools.wheel.Wheel` support. scripttest https://github.com/pypa/virtualenv/archive/master.zip#egg=virtualenv +werkzeug==0.16.0 wheel From d9854794fa05cc9a58ebaf81142f2c6bfe6ab716 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 3 Nov 2019 13:10:19 -0500 Subject: [PATCH 0677/3170] Add failing test for missing client cert when using --trusted-host --- tests/functional/test_install.py | 43 ++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index f203ea43ce5..3194b165384 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -2,6 +2,7 @@ import glob import os import shutil +import ssl import sys import textwrap from os.path import curdir, join, pardir @@ -30,6 +31,12 @@ from tests.lib.filesystem import make_socket_file from tests.lib.local_repos import local_checkout from tests.lib.path import Path +from tests.lib.server import ( + file_response, + make_mock_server, + package_page, + server_running, +) skip_if_python2 = pytest.mark.skipif(PY2, reason="Non-Python 2 only") skip_if_not_python2 = pytest.mark.skipif(not PY2, reason="Python 2 only") @@ -1729,3 +1736,39 @@ def test_install_yanked_file_and_print_warning(script, data): assert expected_warning in result.stderr, str(result) # Make sure a "yanked" release is installed assert 'Successfully installed simple-3.0\n' in result.stdout, str(result) + + +@pytest.mark.parametrize("install_args", [ + (), + ("--trusted-host", "localhost"), +]) +def test_install_sends_client_cert(install_args, script, cert_factory, data): + cert_path = cert_factory() + ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + ctx.load_cert_chain(cert_path, cert_path) + ctx.load_verify_locations(cafile=cert_path) + ctx.verify_mode = ssl.CERT_REQUIRED + + server = make_mock_server(ssl_context=ctx) + server.mock.side_effect = [ + package_page({ + "simple-3.0.tar.gz": "/files/simple-3.0.tar.gz", + }), + file_response(str(data.packages / "simple-3.0.tar.gz")), + ] + + url = "https://{}:{}/simple".format(server.host, server.port) + + args = ["install", "-vvv", "--cert", cert_path, "--client-cert", cert_path] + args.extend(["--index-url", url]) + args.extend(install_args) + args.append("simple") + + with server_running(server): + script.pip(*args) + + assert server.mock.call_count == 2 + for call_args in server.mock.call_args_list: + environ, _ = call_args.args + assert "SSL_CLIENT_CERT" in environ + assert environ["SSL_CLIENT_CERT"] From 3125c326965ba42e105f37a0974180aa7400741b Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 3 Nov 2019 13:10:47 -0500 Subject: [PATCH 0678/3170] Send client cert when using InsecureHTTPAdapter --- news/7207.bugfix | 1 + src/pip/_internal/network/session.py | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 news/7207.bugfix diff --git a/news/7207.bugfix b/news/7207.bugfix new file mode 100644 index 00000000000..014f979c712 --- /dev/null +++ b/news/7207.bugfix @@ -0,0 +1 @@ +Fix not sending client certificates when using ``--trusted-host``. diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py index ac6e2622fc3..4fa57c8683e 100644 --- a/src/pip/_internal/network/session.py +++ b/src/pip/_internal/network/session.py @@ -212,8 +212,9 @@ def close(self): class InsecureHTTPAdapter(HTTPAdapter): def cert_verify(self, conn, url, verify, cert): - conn.cert_reqs = 'CERT_NONE' - conn.ca_certs = None + super(InsecureHTTPAdapter, self).cert_verify( + conn=conn, url=url, verify=False, cert=cert + ) class PipSession(requests.Session): From df42c80ff6117e35da88099b51963419038fb2eb Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 3 Nov 2019 20:49:11 -0500 Subject: [PATCH 0679/3170] Make session required in pip._internal.req.req_file functions --- src/pip/_internal/req/req_file.py | 23 ++++++----------------- tests/unit/test_req_file.py | 12 ++++++++---- 2 files changed, 14 insertions(+), 21 deletions(-) diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index c8b6156e3b5..d2cf6eb28d1 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -105,10 +105,10 @@ def __init__( def parse_requirements( filename, # type: str + session, # type: PipSession finder=None, # type: Optional[PackageFinder] comes_from=None, # type: Optional[str] options=None, # type: Optional[optparse.Values] - session=None, # type: Optional[PipSession] constraint=False, # type: bool wheel_cache=None, # type: Optional[WheelCache] use_pep517=None # type: Optional[bool] @@ -117,21 +117,15 @@ def parse_requirements( """Parse a requirements file and yield InstallRequirement instances. :param filename: Path or url of requirements file. + :param session: PipSession instance. :param finder: Instance of pip.index.PackageFinder. :param comes_from: Origin description of requirements. :param options: cli options. - :param session: Instance of pip.download.PipSession. :param constraint: If true, parsing a constraint file rather than requirements file. :param wheel_cache: Instance of pip.wheel.WheelCache :param use_pep517: Value of the --use-pep517 option. """ - if session is None: - raise TypeError( - "parse_requirements() missing 1 required keyword argument: " - "'session'" - ) - skip_requirements_regex = ( options.skip_requirements_regex if options else None ) @@ -325,7 +319,7 @@ def _parse_and_recurse(self, filename, constraint): def _parse_file(self, filename, constraint): # type: (str, bool) -> Iterator[ParsedLine] _, content = get_file_content( - filename, comes_from=self._comes_from, session=self._session + filename, self._session, comes_from=self._comes_from ) lines_enum = preprocess(content, self._skip_requirements_regex) @@ -505,21 +499,16 @@ def expand_env_variables(lines_enum): yield line_number, line -def get_file_content(url, comes_from=None, session=None): - # type: (str, Optional[str], Optional[PipSession]) -> Tuple[str, Text] +def get_file_content(url, session, comes_from=None): + # type: (str, PipSession, Optional[str]) -> Tuple[str, Text] """Gets the content of a file; it may be a filename, file: URL, or http: URL. Returns (location, content). Content is unicode. Respects # -*- coding: declarations on the retrieved files. :param url: File path or url. + :param session: PipSession instance. :param comes_from: Origin description of requirements. - :param session: Instance of pip.download.PipSession. """ - if session is None: - raise TypeError( - "get_file_content() missing 1 required keyword argument: 'session'" - ) - scheme = get_url_scheme(url) if scheme in ['http', 'https']: diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py index 83dab2111d0..4f4843d8f76 100644 --- a/tests/unit/test_req_file.py +++ b/tests/unit/test_req_file.py @@ -665,7 +665,8 @@ def test_join_lines(self, tmpdir, finder): def test_req_file_parse_no_only_binary(self, data, finder): list(parse_requirements( - data.reqfiles.joinpath("supported_options2.txt"), finder, + data.reqfiles.joinpath("supported_options2.txt"), + finder=finder, session=PipSession())) expected = FormatControl({'fred'}, {'wilma'}) assert finder.format_control == expected @@ -677,7 +678,8 @@ def test_req_file_parse_comment_start_of_line(self, tmpdir, finder): with open(tmpdir.joinpath("req1.txt"), "w") as fp: fp.write("# Comment ") - reqs = list(parse_requirements(tmpdir.joinpath("req1.txt"), finder, + reqs = list(parse_requirements(tmpdir.joinpath("req1.txt"), + finder=finder, session=PipSession())) assert not reqs @@ -689,7 +691,8 @@ def test_req_file_parse_comment_end_of_line_with_url(self, tmpdir, finder): with open(tmpdir.joinpath("req1.txt"), "w") as fp: fp.write("https://example.com/foo.tar.gz # Comment ") - reqs = list(parse_requirements(tmpdir.joinpath("req1.txt"), finder, + reqs = list(parse_requirements(tmpdir.joinpath("req1.txt"), + finder=finder, session=PipSession())) assert len(reqs) == 1 @@ -702,7 +705,8 @@ def test_req_file_parse_egginfo_end_of_line_with_url(self, tmpdir, finder): with open(tmpdir.joinpath("req1.txt"), "w") as fp: fp.write("https://example.com/foo.tar.gz#egg=wat") - reqs = list(parse_requirements(tmpdir.joinpath("req1.txt"), finder, + reqs = list(parse_requirements(tmpdir.joinpath("req1.txt"), + finder=finder, session=PipSession())) assert len(reqs) == 1 From c1817d8b148c5f2f0a4b689399e014ad47ad9056 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 4 Nov 2019 11:14:13 +0530 Subject: [PATCH 0680/3170] Remove an accidentally added line --- src/pip/_internal/req/req_tracker.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pip/_internal/req/req_tracker.py b/src/pip/_internal/req/req_tracker.py index ed4457d59b2..63044dd0ad5 100644 --- a/src/pip/_internal/req/req_tracker.py +++ b/src/pip/_internal/req/req_tracker.py @@ -107,7 +107,6 @@ def cleanup(self): # Cleanup the directory. self._temp_dir.cleanup() - del os.environ['PIP_REQ_TRACKER'] logger.debug("Removed build tracker: %r", self._root) @contextlib.contextmanager From da9a432576eb94e460658c408e3524ffabaed3a3 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 3 Nov 2019 18:37:30 +0530 Subject: [PATCH 0681/3170] Move WheelBuilder and friends to a dedicated module --- src/pip/_internal/commands/install.py | 4 +- src/pip/_internal/commands/wheel.py | 2 +- src/pip/_internal/wheel.py | 515 +------------------------ src/pip/_internal/wheel_builder.py | 530 ++++++++++++++++++++++++++ tests/unit/test_wheel.py | 28 +- 5 files changed, 552 insertions(+), 527 deletions(-) create mode 100644 src/pip/_internal/wheel_builder.py diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 1e1d273fe9a..7dd1dc7b7a0 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -42,7 +42,7 @@ from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.virtualenv import virtualenv_no_global -from pip._internal.wheel import WheelBuilder +from pip._internal.wheel_builder import WheelBuilder if MYPY_CHECK_RUNNING: from optparse import Values @@ -50,7 +50,7 @@ from pip._internal.models.format_control import FormatControl from pip._internal.req.req_install import InstallRequirement - from pip._internal.wheel import BinaryAllowedPredicate + from pip._internal.wheel_builder import BinaryAllowedPredicate logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index a6912717fc2..e1d59ee4c2b 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -16,7 +16,7 @@ from pip._internal.req.req_tracker import RequirementTracker from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from pip._internal.wheel import WheelBuilder +from pip._internal.wheel_builder import WheelBuilder if MYPY_CHECK_RUNNING: from optparse import Values diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index 0af58056d38..274d9aa7362 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -9,7 +9,6 @@ import collections import compileall import csv -import hashlib import logging import os.path import re @@ -26,50 +25,25 @@ from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.six import StringIO -from pip._internal import pep425tags from pip._internal.exceptions import ( InstallationError, InvalidWheelFilename, UnsupportedWheel, ) from pip._internal.locations import get_major_minor_version -from pip._internal.models.link import Link -from pip._internal.utils.logging import indent_log -from pip._internal.utils.marker_files import has_delete_marker_file -from pip._internal.utils.misc import captured_stdout, ensure_dir, read_chunks -from pip._internal.utils.setuptools_build import ( - make_setuptools_bdist_wheel_args, - make_setuptools_clean_args, -) -from pip._internal.utils.subprocess import ( - LOG_DIVIDER, - call_subprocess, - format_command_args, - runner_with_spinner_message, -) -from pip._internal.utils.temp_dir import TempDirectory +from pip._internal.utils.misc import captured_stdout, ensure_dir from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from pip._internal.utils.ui import open_spinner -from pip._internal.utils.unpacking import unpack_file -from pip._internal.utils.urls import path_to_url -from pip._internal.vcs import vcs +from pip._internal.wheel_builder import hash_file if MYPY_CHECK_RUNNING: from typing import ( Dict, List, Optional, Sequence, Mapping, Tuple, IO, Text, Any, - Iterable, Callable, Set, Pattern, Union, - ) - from pip._internal.req.req_install import InstallRequirement - from pip._internal.operations.prepare import ( - RequirementPreparer + Iterable, Callable, Set, ) - from pip._internal.cache import WheelCache from pip._internal.pep425tags import Pep425Tag InstalledCSVRow = Tuple[str, ...] - BinaryAllowedPredicate = Callable[[InstallRequirement], bool] - VERSION_COMPATIBLE = (1, 0) @@ -82,18 +56,6 @@ def normpath(src, p): return os.path.relpath(src, p).replace(os.path.sep, '/') -def hash_file(path, blocksize=1 << 20): - # type: (str, int) -> Tuple[Any, int] - """Return (hash, length) for path using hashlib.sha256()""" - h = hashlib.sha256() - length = 0 - with open(path, 'rb') as f: - for block in read_chunks(f, size=blocksize): - length += len(block) - h.update(block) - return (h, length) # type: ignore - - def rehash(path, blocksize=1 << 20): # type: (str, int) -> Tuple[str, str] """Return (encoded_digest, length) for path using hashlib.sha256()""" @@ -116,14 +78,6 @@ def open_for_csv(name, mode): return open(name, mode + bin, **nl) -def replace_python_tag(wheelname, new_tag): - # type: (str, str) -> str - """Replace the Python tag in a wheel file name with a new value.""" - parts = wheelname.split('-') - parts[-3] = new_tag - return '-'.join(parts) - - def fix_script(path): # type: (str) -> Optional[bool] """Replace #!python with #!/path/to/python @@ -775,466 +729,3 @@ def supported(self, tags): :param tags: the PEP 425 tags to check the wheel against. """ return not self.file_tags.isdisjoint(tags) - - -def _contains_egg_info( - s, _egg_info_re=re.compile(r'([a-z0-9_.]+)-([a-z0-9_.!+-]+)', re.I)): - # type: (str, Pattern) -> bool - """Determine whether the string looks like an egg_info. - - :param s: The string to parse. E.g. foo-2.1 - """ - return bool(_egg_info_re.search(s)) - - -def should_build( - req, # type: InstallRequirement - need_wheel, # type: bool - check_binary_allowed, # type: BinaryAllowedPredicate -): - # type: (...) -> Optional[bool] - """Return whether an InstallRequirement should be built into a wheel.""" - if req.constraint: - # never build requirements that are merely constraints - return False - if req.is_wheel: - if need_wheel: - logger.info( - 'Skipping %s, due to already being wheel.', req.name, - ) - return False - - if need_wheel: - # i.e. pip wheel, not pip install - return True - - if req.editable or not req.source_dir: - return False - - if not check_binary_allowed(req): - logger.info( - "Skipping wheel build for %s, due to binaries " - "being disabled for it.", req.name, - ) - return False - - return True - - -def should_cache( - req, # type: InstallRequirement - check_binary_allowed, # type: BinaryAllowedPredicate -): - # type: (...) -> Optional[bool] - """ - Return whether a built InstallRequirement can be stored in the persistent - wheel cache, assuming the wheel cache is available, and should_build() - has determined a wheel needs to be built. - """ - if not should_build( - req, need_wheel=False, check_binary_allowed=check_binary_allowed - ): - # never cache if pip install (need_wheel=False) would not have built - # (editable mode, etc) - return False - - if req.link and req.link.is_vcs: - # VCS checkout. Build wheel just for this run - # unless it points to an immutable commit hash in which - # case it can be cached. - assert not req.editable - assert req.source_dir - vcs_backend = vcs.get_backend_for_scheme(req.link.scheme) - assert vcs_backend - if vcs_backend.is_immutable_rev_checkout(req.link.url, req.source_dir): - return True - return False - - link = req.link - base, ext = link.splitext() - if _contains_egg_info(base): - return True - - # Otherwise, build the wheel just for this run using the ephemeral - # cache since we are either in the case of e.g. a local directory, or - # no cache directory is available to use. - return False - - -def format_command_result( - command_args, # type: List[str] - command_output, # type: Text -): - # type: (...) -> str - """Format command information for logging.""" - command_desc = format_command_args(command_args) - text = 'Command arguments: {}\n'.format(command_desc) - - if not command_output: - text += 'Command output: None' - elif logger.getEffectiveLevel() > logging.DEBUG: - text += 'Command output: [use --verbose to show]' - else: - if not command_output.endswith('\n'): - command_output += '\n' - text += 'Command output:\n{}{}'.format(command_output, LOG_DIVIDER) - - return text - - -def get_legacy_build_wheel_path( - names, # type: List[str] - temp_dir, # type: str - req, # type: InstallRequirement - command_args, # type: List[str] - command_output, # type: Text -): - # type: (...) -> Optional[str] - """Return the path to the wheel in the temporary build directory.""" - # Sort for determinism. - names = sorted(names) - if not names: - msg = ( - 'Legacy build of wheel for {!r} created no files.\n' - ).format(req.name) - msg += format_command_result(command_args, command_output) - logger.warning(msg) - return None - - if len(names) > 1: - msg = ( - 'Legacy build of wheel for {!r} created more than one file.\n' - 'Filenames (choosing first): {}\n' - ).format(req.name, names) - msg += format_command_result(command_args, command_output) - logger.warning(msg) - - return os.path.join(temp_dir, names[0]) - - -def _always_true(_): - # type: (Any) -> bool - return True - - -class WheelBuilder(object): - """Build wheels from a RequirementSet.""" - - def __init__( - self, - preparer, # type: RequirementPreparer - wheel_cache, # type: WheelCache - build_options=None, # type: Optional[List[str]] - global_options=None, # type: Optional[List[str]] - check_binary_allowed=None, # type: Optional[BinaryAllowedPredicate] - no_clean=False, # type: bool - path_to_wheelnames=None, # type: Optional[Union[bytes, Text]] - ): - # type: (...) -> None - if check_binary_allowed is None: - # Binaries allowed by default. - check_binary_allowed = _always_true - - 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.check_binary_allowed = check_binary_allowed - self.no_clean = no_clean - # path where to save built names of built wheels - self.path_to_wheelnames = path_to_wheelnames - # file names of built wheel names - self.wheel_filenames = [] # type: List[Union[bytes, Text]] - - def _build_one( - self, - req, # type: InstallRequirement - output_dir, # type: str - python_tag=None, # type: Optional[str] - ): - # type: (...) -> Optional[str] - """Build one wheel. - - :return: The filename of the built wheel, or None if the build failed. - """ - # 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, # type: InstallRequirement - output_dir, # type: str - python_tag=None, # type: Optional[str] - ): - # type: (...) -> Optional[str] - with TempDirectory(kind="wheel") as temp_dir: - if req.use_pep517: - builder = self._build_one_pep517 - else: - builder = self._build_one_legacy - wheel_path = builder(req, temp_dir.path, python_tag=python_tag) - if wheel_path is not None: - wheel_name = os.path.basename(wheel_path) - dest_path = os.path.join(output_dir, wheel_name) - try: - wheel_hash, length = hash_file(wheel_path) - shutil.move(wheel_path, dest_path) - logger.info('Created wheel for %s: ' - 'filename=%s size=%d sha256=%s', - req.name, wheel_name, length, - wheel_hash.hexdigest()) - logger.info('Stored in directory: %s', output_dir) - return dest_path - except Exception: - pass - # Ignore return, we can't do anything else useful. - self._clean_one(req) - return None - - def _build_one_pep517( - self, - req, # type: InstallRequirement - tempd, # type: str - python_tag=None, # type: Optional[str] - ): - # type: (...) -> Optional[str] - """Build one InstallRequirement using the PEP 517 build process. - - Returns path to wheel if successfully built. Otherwise, returns None. - """ - assert req.metadata_directory is not None - if self.build_options: - # PEP 517 does not support --build-options - logger.error('Cannot build wheel for %s using PEP 517 when ' - '--build-options is present' % (req.name,)) - return None - try: - logger.debug('Destination directory: %s', tempd) - - runner = runner_with_spinner_message( - 'Building wheel for {} (PEP 517)'.format(req.name) - ) - backend = req.pep517_backend - with backend.subprocess_runner(runner): - wheel_name = backend.build_wheel( - tempd, - metadata_directory=req.metadata_directory, - ) - if python_tag: - # General PEP 517 backends don't necessarily support - # a "--python-tag" option, so we rename the wheel - # file directly. - new_name = replace_python_tag(wheel_name, python_tag) - os.rename( - os.path.join(tempd, wheel_name), - os.path.join(tempd, new_name) - ) - # Reassign to simplify the return at the end of function - wheel_name = new_name - except Exception: - logger.error('Failed building wheel for %s', req.name) - return None - return os.path.join(tempd, wheel_name) - - def _build_one_legacy( - self, - req, # type: InstallRequirement - tempd, # type: str - python_tag=None, # type: Optional[str] - ): - # type: (...) -> Optional[str] - """Build one InstallRequirement using the "legacy" build process. - - Returns path to wheel if successfully built. Otherwise, returns None. - """ - wheel_args = make_setuptools_bdist_wheel_args( - req.setup_py_path, - global_options=self.global_options, - build_options=self.build_options, - destination_dir=tempd, - python_tag=python_tag, - ) - - spin_message = 'Building wheel for %s (setup.py)' % (req.name,) - with open_spinner(spin_message) as spinner: - logger.debug('Destination directory: %s', tempd) - - try: - output = call_subprocess( - wheel_args, - cwd=req.unpacked_source_directory, - spinner=spinner, - ) - except Exception: - spinner.finish("error") - logger.error('Failed building wheel for %s', req.name) - return None - - names = os.listdir(tempd) - wheel_path = get_legacy_build_wheel_path( - names=names, - temp_dir=tempd, - req=req, - command_args=wheel_args, - command_output=output, - ) - return wheel_path - - def _clean_one(self, req): - # type: (InstallRequirement) -> bool - clean_args = make_setuptools_clean_args( - req.setup_py_path, - global_options=self.global_options, - ) - - logger.info('Running setup.py clean for %s', req.name) - try: - call_subprocess(clean_args, cwd=req.source_dir) - return True - except Exception: - logger.error('Failed cleaning build dir for %s', req.name) - return False - - def build( - self, - requirements, # type: Iterable[InstallRequirement] - should_unpack=False # type: bool - ): - # type: (...) -> List[InstallRequirement] - """Build wheels. - - :param should_unpack: If True, after building the wheel, unpack it - and replace the sdist with the unpacked version in preparation - for installation. - :return: The list of InstallRequirement that failed to build. - """ - # pip install uses should_unpack=True. - # pip install never provides a _wheel_dir. - # pip wheel uses should_unpack=False. - # pip wheel always provides a _wheel_dir (via the preparer). - assert ( - (should_unpack and not self._wheel_dir) or - (not should_unpack and self._wheel_dir) - ) - - buildset = [] - cache_available = bool(self.wheel_cache.cache_dir) - - for req in requirements: - if not should_build( - req, - need_wheel=not should_unpack, - check_binary_allowed=self.check_binary_allowed, - ): - continue - - if ( - cache_available and - should_cache(req, self.check_binary_allowed) - ): - output_dir = self.wheel_cache.get_path_for_link(req.link) - else: - output_dir = self.wheel_cache.get_ephem_path_for_link( - req.link - ) - - buildset.append((req, output_dir)) - - if not buildset: - return [] - - # TODO by @pradyunsg - # Should break up this method into 2 separate methods. - - # Build the wheels. - logger.info( - 'Building wheels for collected packages: %s', - ', '.join([req.name for (req, _) in buildset]), - ) - - python_tag = None - if should_unpack: - python_tag = pep425tags.implementation_tag - - with indent_log(): - build_success, build_failure = [], [] - for req, output_dir in buildset: - try: - ensure_dir(output_dir) - except OSError as e: - logger.warning( - "Building wheel for %s failed: %s", - req.name, e, - ) - build_failure.append(req) - continue - - wheel_file = self._build_one( - req, output_dir, - python_tag=python_tag, - ) - if wheel_file: - if should_unpack: - # XXX: This is mildly duplicative with prepare_files, - # but not close enough to pull out to a single common - # method. - # The code below assumes temporary source dirs - - # prevent it doing bad things. - if ( - req.source_dir and - not has_delete_marker_file(req.source_dir) - ): - raise AssertionError( - "bad source dir - missing marker") - # Delete the source we built the wheel from - req.remove_temporary_source() - # set the build directory again - name is known from - # the work prepare_files did. - req.source_dir = req.ensure_build_location( - self.preparer.build_dir - ) - # Update the link for this. - req.link = Link(path_to_url(wheel_file)) - assert req.link.is_wheel - # extract the wheel into the dir - unpack_file(req.link.file_path, req.source_dir) - else: - # copy from cache to target directory - try: - ensure_dir(self._wheel_dir) - shutil.copy( - os.path.join(output_dir, wheel_file), - self._wheel_dir, - ) - except OSError as e: - logger.warning( - "Building wheel for %s failed: %s", - req.name, e, - ) - build_failure.append(req) - continue - self.wheel_filenames.append( - os.path.relpath(wheel_file, output_dir) - ) - build_success.append(req) - else: - build_failure.append(req) - - # notify success/failure - if build_success: - logger.info( - 'Successfully built %s', - ' '.join([req.name for req in build_success]), - ) - if build_failure: - logger.info( - 'Failed to build %s', - ' '.join([req.name for req in build_failure]), - ) - # Return a list of requirements that failed to build - return build_failure diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py new file mode 100644 index 00000000000..188f97fadc9 --- /dev/null +++ b/src/pip/_internal/wheel_builder.py @@ -0,0 +1,530 @@ +"""Orchestrator for building wheels from InstallRequirements. +""" + +# The following comment should be removed at some point in the future. +# mypy: strict-optional=False + +import logging +import os.path +import re +import shutil + +from pip._internal import pep425tags +from pip._internal.models.link import Link +from pip._internal.utils.logging import indent_log +from pip._internal.utils.marker_files import has_delete_marker_file +from pip._internal.utils.misc import ensure_dir, read_chunks +from pip._internal.utils.setuptools_build import ( + make_setuptools_bdist_wheel_args, + make_setuptools_clean_args, +) +from pip._internal.utils.subprocess import ( + LOG_DIVIDER, + call_subprocess, + format_command_args, + runner_with_spinner_message, +) +from pip._internal.utils.temp_dir import TempDirectory +from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from pip._internal.utils.ui import open_spinner +from pip._internal.utils.unpacking import unpack_file +from pip._internal.utils.urls import path_to_url +from pip._internal.vcs import vcs + +if MYPY_CHECK_RUNNING: + from typing import ( + Any, Callable, Iterable, List, Optional, Pattern, Text, Tuple, Union, + ) + + from pip._internal.cache import WheelCache + from pip._internal.operations.prepare import ( + RequirementPreparer + ) + from pip._internal.req.req_install import InstallRequirement + + BinaryAllowedPredicate = Callable[[InstallRequirement], bool] + +logger = logging.getLogger(__name__) + + +def hash_file(path, blocksize=1 << 20): + # type: (str, int) -> Tuple[Any, int] + """Return (hash, length) for path using hashlib.sha256()""" + h = hashlib.sha256() + length = 0 + with open(path, 'rb') as f: + for block in read_chunks(f, size=blocksize): + length += len(block) + h.update(block) + return (h, length) # type: ignore + + +def replace_python_tag(wheelname, new_tag): + # type: (str, str) -> str + """Replace the Python tag in a wheel file name with a new value.""" + parts = wheelname.split('-') + parts[-3] = new_tag + return '-'.join(parts) + + +def _contains_egg_info( + s, _egg_info_re=re.compile(r'([a-z0-9_.]+)-([a-z0-9_.!+-]+)', re.I)): + # type: (str, Pattern) -> bool + """Determine whether the string looks like an egg_info. + + :param s: The string to parse. E.g. foo-2.1 + """ + return bool(_egg_info_re.search(s)) + + +def should_build( + req, # type: InstallRequirement + need_wheel, # type: bool + check_binary_allowed, # type: BinaryAllowedPredicate +): + # type: (...) -> Optional[bool] + """Return whether an InstallRequirement should be built into a wheel.""" + if req.constraint: + # never build requirements that are merely constraints + return False + if req.is_wheel: + if need_wheel: + logger.info( + 'Skipping %s, due to already being wheel.', req.name, + ) + return False + + if need_wheel: + # i.e. pip wheel, not pip install + return True + + if req.editable or not req.source_dir: + return False + + if not check_binary_allowed(req): + logger.info( + "Skipping wheel build for %s, due to binaries " + "being disabled for it.", req.name, + ) + return False + + return True + + +def should_cache( + req, # type: InstallRequirement + check_binary_allowed, # type: BinaryAllowedPredicate +): + # type: (...) -> Optional[bool] + """ + Return whether a built InstallRequirement can be stored in the persistent + wheel cache, assuming the wheel cache is available, and should_build() + has determined a wheel needs to be built. + """ + if not should_build( + req, need_wheel=False, check_binary_allowed=check_binary_allowed + ): + # never cache if pip install (need_wheel=False) would not have built + # (editable mode, etc) + return False + + if req.link and req.link.is_vcs: + # VCS checkout. Build wheel just for this run + # unless it points to an immutable commit hash in which + # case it can be cached. + assert not req.editable + assert req.source_dir + vcs_backend = vcs.get_backend_for_scheme(req.link.scheme) + assert vcs_backend + if vcs_backend.is_immutable_rev_checkout(req.link.url, req.source_dir): + return True + return False + + link = req.link + base, ext = link.splitext() + if _contains_egg_info(base): + return True + + # Otherwise, build the wheel just for this run using the ephemeral + # cache since we are either in the case of e.g. a local directory, or + # no cache directory is available to use. + return False + + +def format_command_result( + command_args, # type: List[str] + command_output, # type: Text +): + # type: (...) -> str + """Format command information for logging.""" + command_desc = format_command_args(command_args) + text = 'Command arguments: {}\n'.format(command_desc) + + if not command_output: + text += 'Command output: None' + elif logger.getEffectiveLevel() > logging.DEBUG: + text += 'Command output: [use --verbose to show]' + else: + if not command_output.endswith('\n'): + command_output += '\n' + text += 'Command output:\n{}{}'.format(command_output, LOG_DIVIDER) + + return text + + +def get_legacy_build_wheel_path( + names, # type: List[str] + temp_dir, # type: str + req, # type: InstallRequirement + command_args, # type: List[str] + command_output, # type: Text +): + # type: (...) -> Optional[str] + """Return the path to the wheel in the temporary build directory.""" + # Sort for determinism. + names = sorted(names) + if not names: + msg = ( + 'Legacy build of wheel for {!r} created no files.\n' + ).format(req.name) + msg += format_command_result(command_args, command_output) + logger.warning(msg) + return None + + if len(names) > 1: + msg = ( + 'Legacy build of wheel for {!r} created more than one file.\n' + 'Filenames (choosing first): {}\n' + ).format(req.name, names) + msg += format_command_result(command_args, command_output) + logger.warning(msg) + + return os.path.join(temp_dir, names[0]) + + +def _always_true(_): + # type: (Any) -> bool + return True + + +class WheelBuilder(object): + """Build wheels from a RequirementSet.""" + + def __init__( + self, + preparer, # type: RequirementPreparer + wheel_cache, # type: WheelCache + build_options=None, # type: Optional[List[str]] + global_options=None, # type: Optional[List[str]] + check_binary_allowed=None, # type: Optional[BinaryAllowedPredicate] + no_clean=False, # type: bool + path_to_wheelnames=None, # type: Optional[Union[bytes, Text]] + ): + # type: (...) -> None + if check_binary_allowed is None: + # Binaries allowed by default. + check_binary_allowed = _always_true + + 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.check_binary_allowed = check_binary_allowed + self.no_clean = no_clean + # path where to save built names of built wheels + self.path_to_wheelnames = path_to_wheelnames + # file names of built wheel names + self.wheel_filenames = [] # type: List[Union[bytes, Text]] + + def _build_one( + self, + req, # type: InstallRequirement + output_dir, # type: str + python_tag=None, # type: Optional[str] + ): + # type: (...) -> Optional[str] + """Build one wheel. + + :return: The filename of the built wheel, or None if the build failed. + """ + # 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, # type: InstallRequirement + output_dir, # type: str + python_tag=None, # type: Optional[str] + ): + # type: (...) -> Optional[str] + with TempDirectory(kind="wheel") as temp_dir: + if req.use_pep517: + builder = self._build_one_pep517 + else: + builder = self._build_one_legacy + wheel_path = builder(req, temp_dir.path, python_tag=python_tag) + if wheel_path is not None: + wheel_name = os.path.basename(wheel_path) + dest_path = os.path.join(output_dir, wheel_name) + try: + wheel_hash, length = hash_file(wheel_path) + shutil.move(wheel_path, dest_path) + logger.info('Created wheel for %s: ' + 'filename=%s size=%d sha256=%s', + req.name, wheel_name, length, + wheel_hash.hexdigest()) + logger.info('Stored in directory: %s', output_dir) + return dest_path + except Exception: + pass + # Ignore return, we can't do anything else useful. + self._clean_one(req) + return None + + def _build_one_pep517( + self, + req, # type: InstallRequirement + tempd, # type: str + python_tag=None, # type: Optional[str] + ): + # type: (...) -> Optional[str] + """Build one InstallRequirement using the PEP 517 build process. + + Returns path to wheel if successfully built. Otherwise, returns None. + """ + assert req.metadata_directory is not None + if self.build_options: + # PEP 517 does not support --build-options + logger.error('Cannot build wheel for %s using PEP 517 when ' + '--build-options is present' % (req.name,)) + return None + try: + logger.debug('Destination directory: %s', tempd) + + runner = runner_with_spinner_message( + 'Building wheel for {} (PEP 517)'.format(req.name) + ) + backend = req.pep517_backend + with backend.subprocess_runner(runner): + wheel_name = backend.build_wheel( + tempd, + metadata_directory=req.metadata_directory, + ) + if python_tag: + # General PEP 517 backends don't necessarily support + # a "--python-tag" option, so we rename the wheel + # file directly. + new_name = replace_python_tag(wheel_name, python_tag) + os.rename( + os.path.join(tempd, wheel_name), + os.path.join(tempd, new_name) + ) + # Reassign to simplify the return at the end of function + wheel_name = new_name + except Exception: + logger.error('Failed building wheel for %s', req.name) + return None + return os.path.join(tempd, wheel_name) + + def _build_one_legacy( + self, + req, # type: InstallRequirement + tempd, # type: str + python_tag=None, # type: Optional[str] + ): + # type: (...) -> Optional[str] + """Build one InstallRequirement using the "legacy" build process. + + Returns path to wheel if successfully built. Otherwise, returns None. + """ + wheel_args = make_setuptools_bdist_wheel_args( + req.setup_py_path, + global_options=self.global_options, + build_options=self.build_options, + destination_dir=tempd, + python_tag=python_tag, + ) + + spin_message = 'Building wheel for %s (setup.py)' % (req.name,) + with open_spinner(spin_message) as spinner: + logger.debug('Destination directory: %s', tempd) + + try: + output = call_subprocess( + wheel_args, + cwd=req.unpacked_source_directory, + spinner=spinner, + ) + except Exception: + spinner.finish("error") + logger.error('Failed building wheel for %s', req.name) + return None + + names = os.listdir(tempd) + wheel_path = get_legacy_build_wheel_path( + names=names, + temp_dir=tempd, + req=req, + command_args=wheel_args, + command_output=output, + ) + return wheel_path + + def _clean_one(self, req): + # type: (InstallRequirement) -> bool + clean_args = make_setuptools_clean_args( + req.setup_py_path, + global_options=self.global_options, + ) + + logger.info('Running setup.py clean for %s', req.name) + try: + call_subprocess(clean_args, cwd=req.source_dir) + return True + except Exception: + logger.error('Failed cleaning build dir for %s', req.name) + return False + + def build( + self, + requirements, # type: Iterable[InstallRequirement] + should_unpack=False # type: bool + ): + # type: (...) -> List[InstallRequirement] + """Build wheels. + + :param should_unpack: If True, after building the wheel, unpack it + and replace the sdist with the unpacked version in preparation + for installation. + :return: The list of InstallRequirement that failed to build. + """ + # pip install uses should_unpack=True. + # pip install never provides a _wheel_dir. + # pip wheel uses should_unpack=False. + # pip wheel always provides a _wheel_dir (via the preparer). + assert ( + (should_unpack and not self._wheel_dir) or + (not should_unpack and self._wheel_dir) + ) + + buildset = [] + cache_available = bool(self.wheel_cache.cache_dir) + + for req in requirements: + if not should_build( + req, + need_wheel=not should_unpack, + check_binary_allowed=self.check_binary_allowed, + ): + continue + + if ( + cache_available and + should_cache(req, self.check_binary_allowed) + ): + output_dir = self.wheel_cache.get_path_for_link(req.link) + else: + output_dir = self.wheel_cache.get_ephem_path_for_link( + req.link + ) + + buildset.append((req, output_dir)) + + if not buildset: + return [] + + # TODO by @pradyunsg + # Should break up this method into 2 separate methods. + + # Build the wheels. + logger.info( + 'Building wheels for collected packages: %s', + ', '.join([req.name for (req, _) in buildset]), + ) + + python_tag = None + if should_unpack: + python_tag = pep425tags.implementation_tag + + with indent_log(): + build_success, build_failure = [], [] + for req, output_dir in buildset: + try: + ensure_dir(output_dir) + except OSError as e: + logger.warning( + "Building wheel for %s failed: %s", + req.name, e, + ) + build_failure.append(req) + continue + + wheel_file = self._build_one( + req, output_dir, + python_tag=python_tag, + ) + if wheel_file: + if should_unpack: + # XXX: This is mildly duplicative with prepare_files, + # but not close enough to pull out to a single common + # method. + # The code below assumes temporary source dirs - + # prevent it doing bad things. + if ( + req.source_dir and + not has_delete_marker_file(req.source_dir) + ): + raise AssertionError( + "bad source dir - missing marker") + # Delete the source we built the wheel from + req.remove_temporary_source() + # set the build directory again - name is known from + # the work prepare_files did. + req.source_dir = req.ensure_build_location( + self.preparer.build_dir + ) + # Update the link for this. + req.link = Link(path_to_url(wheel_file)) + assert req.link.is_wheel + # extract the wheel into the dir + unpack_file(req.link.file_path, req.source_dir) + else: + # copy from cache to target directory + try: + ensure_dir(self._wheel_dir) + shutil.copy( + os.path.join(output_dir, wheel_file), + self._wheel_dir, + ) + except OSError as e: + logger.warning( + "Building wheel for %s failed: %s", + req.name, e, + ) + build_failure.append(req) + continue + self.wheel_filenames.append( + os.path.relpath(wheel_file, output_dir) + ) + build_success.append(req) + else: + build_failure.append(req) + + # notify success/failure + if build_success: + logger.info( + 'Successfully built %s', + ' '.join([req.name for req in build_success]), + ) + if build_failure: + logger.info( + 'Failed to build %s', + ' '.join([req.name for req in build_failure]), + ) + # Return a list of requirements that failed to build + return build_failure diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index fc3a7ddb382..1d0c8e209ae 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -8,6 +8,7 @@ from mock import Mock, patch from pip._vendor.packaging.requirements import Requirement +import pip._internal.wheel_builder from pip._internal import pep425tags, wheel from pip._internal.commands.wheel import WheelCommand from pip._internal.exceptions import InvalidWheelFilename, UnsupportedWheel @@ -58,7 +59,7 @@ def __init__( ], ) def test_contains_egg_info(s, expected): - result = wheel._contains_egg_info(s) + result = pip._internal.wheel_builder._contains_egg_info(s) assert result == expected @@ -128,7 +129,7 @@ def test_format_tag(file_tag, expected): ], ) def test_should_build(req, need_wheel, disallow_binaries, expected): - should_build = wheel.should_build( + should_build = pip._internal.wheel_builder.should_build( req, need_wheel, check_binary_allowed=lambda req: not disallow_binaries, @@ -157,8 +158,10 @@ def test_should_cache( def check_binary_allowed(req): return not disallow_binaries - should_cache = wheel.should_cache(req, check_binary_allowed) - if not wheel.should_build( + should_cache = pip._internal.wheel_builder.should_cache( + req, check_binary_allowed + ) + if not pip._internal.wheel_builder.should_build( req, need_wheel=False, check_binary_allowed=check_binary_allowed ): # never cache if pip install (need_wheel=False) would not have built) @@ -183,7 +186,7 @@ def test_should_cache_git_sha(script, tmpdir): def test_format_command_result__INFO(caplog): caplog.set_level(logging.INFO) - actual = wheel.format_command_result( + actual = pip._internal.wheel_builder.format_command_result( # Include an argument with a space to test argument quoting. command_args=['arg1', 'second arg'], command_output='output line 1\noutput line 2\n', @@ -202,7 +205,7 @@ def test_format_command_result__INFO(caplog): ]) def test_format_command_result__DEBUG(caplog, command_output): caplog.set_level(logging.DEBUG) - actual = wheel.format_command_result( + actual = pip._internal.wheel_builder.format_command_result( command_args=['arg1', 'arg2'], command_output=command_output, ) @@ -218,7 +221,7 @@ def test_format_command_result__DEBUG(caplog, command_output): @pytest.mark.parametrize('log_level', ['DEBUG', 'INFO']) def test_format_command_result__empty_output(caplog, log_level): caplog.set_level(log_level) - actual = wheel.format_command_result( + actual = pip._internal.wheel_builder.format_command_result( command_args=['arg1', 'arg2'], command_output='', ) @@ -230,7 +233,7 @@ def test_format_command_result__empty_output(caplog, log_level): def call_get_legacy_build_wheel_path(caplog, names): req = make_test_install_req() - wheel_path = wheel.get_legacy_build_wheel_path( + wheel_path = pip._internal.wheel_builder.get_legacy_build_wheel_path( names=names, temp_dir='/tmp/abcd', req=req, @@ -416,8 +419,9 @@ def test_python_tag(): 'simplewheel-1.0-py37-none-any.whl', 'simplewheel-2.0-1-py37-none-any.whl', ] - for name, new in zip(wheelnames, newnames): - assert wheel.replace_python_tag(name, 'py37') == new + for name, expected in zip(wheelnames, newnames): + result = pip._internal.wheel_builder.replace_python_tag(name, 'py37') + assert result == expected def test_check_compatibility(): @@ -745,7 +749,7 @@ def test_skip_building_wheels(self, caplog): with patch('pip._internal.wheel.WheelBuilder._build_one') \ as mock_build_one: wheel_req = Mock(is_wheel=True, editable=False, constraint=False) - wb = wheel.WheelBuilder( + wb = pip._internal.wheel_builder.WheelBuilder( preparer=Mock(), wheel_cache=Mock(cache_dir=None), ) @@ -883,7 +887,7 @@ def prep(self, tmpdir): def test_hash_file(self, tmpdir): self.prep(tmpdir) - h, length = wheel.hash_file(self.test_file) + h, length = pip._internal.wheel_builder.hash_file(self.test_file) assert length == self.test_file_len assert h.hexdigest() == self.test_file_hash From 647d30ec77d550bc021ea2e510c7fac6e019528e Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 3 Nov 2019 18:54:11 +0530 Subject: [PATCH 0682/3170] Move hash_file to utils.misc Why: Allows for better code reuse, without introducing dependency between wheel->wheel_builder. --- src/pip/_internal/utils/misc.py | 15 +++++++++++++++ src/pip/_internal/wheel.py | 3 +-- src/pip/_internal/wheel_builder.py | 16 ++-------------- tests/unit/test_wheel.py | 3 ++- 4 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index b84826350bc..c84177cbd2c 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -7,6 +7,7 @@ import contextlib import errno import getpass +import hashlib import io import logging import os @@ -868,3 +869,17 @@ def is_console_interactive(): """Is this console interactive? """ return sys.stdin is not None and sys.stdin.isatty() + + +def hash_file(path, blocksize=1 << 20): + # type: (str, int) -> Tuple[Any, int] + """Return (hash, length) for path using hashlib.sha256() + """ + + h = hashlib.sha256() + length = 0 + with open(path, 'rb') as f: + for block in read_chunks(f, size=blocksize): + length += len(block) + h.update(block) + return (h, length) # type: ignore diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index 274d9aa7362..85a9e2b60e4 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -31,9 +31,8 @@ UnsupportedWheel, ) from pip._internal.locations import get_major_minor_version -from pip._internal.utils.misc import captured_stdout, ensure_dir +from pip._internal.utils.misc import captured_stdout, ensure_dir, hash_file from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from pip._internal.wheel_builder import hash_file if MYPY_CHECK_RUNNING: from typing import ( diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index 188f97fadc9..ba93933edf8 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -13,7 +13,7 @@ from pip._internal.models.link import Link from pip._internal.utils.logging import indent_log from pip._internal.utils.marker_files import has_delete_marker_file -from pip._internal.utils.misc import ensure_dir, read_chunks +from pip._internal.utils.misc import ensure_dir, hash_file from pip._internal.utils.setuptools_build import ( make_setuptools_bdist_wheel_args, make_setuptools_clean_args, @@ -33,7 +33,7 @@ if MYPY_CHECK_RUNNING: from typing import ( - Any, Callable, Iterable, List, Optional, Pattern, Text, Tuple, Union, + Any, Callable, Iterable, List, Optional, Pattern, Text, Union, ) from pip._internal.cache import WheelCache @@ -47,18 +47,6 @@ logger = logging.getLogger(__name__) -def hash_file(path, blocksize=1 << 20): - # type: (str, int) -> Tuple[Any, int] - """Return (hash, length) for path using hashlib.sha256()""" - h = hashlib.sha256() - length = 0 - with open(path, 'rb') as f: - for block in read_chunks(f, size=blocksize): - length += len(block) - h.update(block) - return (h, length) # type: ignore - - def replace_python_tag(wheelname, new_tag): # type: (str, str) -> str """Replace the Python tag in a wheel file name with a new value.""" diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 1d0c8e209ae..6ef988ecfce 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -16,6 +16,7 @@ from pip._internal.models.link import Link from pip._internal.req.req_install import InstallRequirement from pip._internal.utils.compat import WINDOWS +from pip._internal.utils.misc import hash_file from pip._internal.utils.unpacking import unpack_file from pip._internal.wheel import ( MissingCallableSuffix, @@ -887,7 +888,7 @@ def prep(self, tmpdir): def test_hash_file(self, tmpdir): self.prep(tmpdir) - h, length = pip._internal.wheel_builder.hash_file(self.test_file) + h, length = hash_file(self.test_file) assert length == self.test_file_len assert h.hexdigest() == self.test_file_hash From 9435050c2d18bed9ca844fcf3ca6c7f67c77cf80 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 3 Nov 2019 20:01:05 +0530 Subject: [PATCH 0683/3170] Move tests for WheelBuilder and friends --- tests/unit/test_wheel.py | 208 +----------------------------- tests/unit/test_wheel_builder.py | 209 +++++++++++++++++++++++++++++++ 2 files changed, 213 insertions(+), 204 deletions(-) create mode 100644 tests/unit/test_wheel_builder.py diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 6ef988ecfce..3153f2a822a 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -5,10 +5,9 @@ import textwrap import pytest -from mock import Mock, patch +from mock import patch from pip._vendor.packaging.requirements import Requirement -import pip._internal.wheel_builder from pip._internal import pep425tags, wheel from pip._internal.commands.wheel import WheelCommand from pip._internal.exceptions import InvalidWheelFilename, UnsupportedWheel @@ -22,46 +21,8 @@ MissingCallableSuffix, _raise_for_invalid_entrypoint, ) -from tests.lib import DATA_DIR, _create_test_package, assert_paths_equal - - -class ReqMock: - - def __init__( - self, - name="pendulum", - is_wheel=False, - editable=False, - link=None, - constraint=False, - source_dir="/tmp/pip-install-123/pendulum", - ): - self.name = name - self.is_wheel = is_wheel - self.editable = editable - self.link = link - self.constraint = constraint - self.source_dir = source_dir - - -@pytest.mark.parametrize( - "s, expected", - [ - # Trivial. - ("pip-18.0", True), - - # Ambiguous. - ("foo-2-2", True), - ("im-valid", True), - - # Invalid. - ("invalid", False), - ("im_invalid", False), - ], -) -def test_contains_egg_info(s, expected): - result = pip._internal.wheel_builder._contains_egg_info(s) - assert result == expected +from pip._internal.wheel_builder import get_legacy_build_wheel_path +from tests.lib import DATA_DIR, assert_paths_equal def make_test_install_req(base_name=None): @@ -103,138 +64,9 @@ def test_format_tag(file_tag, expected): assert actual == expected -@pytest.mark.parametrize( - "req, need_wheel, disallow_binaries, expected", - [ - # pip wheel (need_wheel=True) - (ReqMock(), True, False, True), - (ReqMock(), True, True, True), - (ReqMock(constraint=True), True, False, False), - (ReqMock(is_wheel=True), True, False, False), - (ReqMock(editable=True), True, False, True), - (ReqMock(source_dir=None), True, False, True), - (ReqMock(link=Link("git+https://g.c/org/repo")), True, False, True), - (ReqMock(link=Link("git+https://g.c/org/repo")), True, True, True), - # pip install (need_wheel=False) - (ReqMock(), False, False, True), - (ReqMock(), False, True, False), - (ReqMock(constraint=True), False, False, False), - (ReqMock(is_wheel=True), False, False, False), - (ReqMock(editable=True), False, False, False), - (ReqMock(source_dir=None), False, False, False), - # By default (i.e. when binaries are allowed), VCS requirements - # should be built in install mode. - (ReqMock(link=Link("git+https://g.c/org/repo")), False, False, True), - # Disallowing binaries, however, should cause them not to be built. - (ReqMock(link=Link("git+https://g.c/org/repo")), False, True, False), - ], -) -def test_should_build(req, need_wheel, disallow_binaries, expected): - should_build = pip._internal.wheel_builder.should_build( - req, - need_wheel, - check_binary_allowed=lambda req: not disallow_binaries, - ) - assert should_build is expected - - -@pytest.mark.parametrize( - "req, disallow_binaries, expected", - [ - (ReqMock(editable=True), False, False), - (ReqMock(source_dir=None), False, False), - (ReqMock(link=Link("git+https://g.c/org/repo")), False, False), - (ReqMock(link=Link("https://g.c/dist.tgz")), False, False), - (ReqMock(link=Link("https://g.c/dist-2.0.4.tgz")), False, True), - (ReqMock(editable=True), True, False), - (ReqMock(source_dir=None), True, False), - (ReqMock(link=Link("git+https://g.c/org/repo")), True, False), - (ReqMock(link=Link("https://g.c/dist.tgz")), True, False), - (ReqMock(link=Link("https://g.c/dist-2.0.4.tgz")), True, False), - ], -) -def test_should_cache( - req, disallow_binaries, expected -): - def check_binary_allowed(req): - return not disallow_binaries - - should_cache = pip._internal.wheel_builder.should_cache( - req, check_binary_allowed - ) - if not pip._internal.wheel_builder.should_build( - req, need_wheel=False, check_binary_allowed=check_binary_allowed - ): - # never cache if pip install (need_wheel=False) would not have built) - assert not should_cache - assert should_cache is expected - - -def test_should_cache_git_sha(script, tmpdir): - repo_path = _create_test_package(script, name="mypkg") - commit = script.run( - "git", "rev-parse", "HEAD", cwd=repo_path - ).stdout.strip() - # a link referencing a sha should be cached - url = "git+https://g.c/o/r@" + commit + "#egg=mypkg" - req = ReqMock(link=Link(url), source_dir=repo_path) - assert wheel.should_cache(req, check_binary_allowed=lambda r: True) - # a link not referencing a sha should not be cached - url = "git+https://g.c/o/r@master#egg=mypkg" - req = ReqMock(link=Link(url), source_dir=repo_path) - assert not wheel.should_cache(req, check_binary_allowed=lambda r: True) - - -def test_format_command_result__INFO(caplog): - caplog.set_level(logging.INFO) - actual = pip._internal.wheel_builder.format_command_result( - # Include an argument with a space to test argument quoting. - command_args=['arg1', 'second arg'], - command_output='output line 1\noutput line 2\n', - ) - assert actual.splitlines() == [ - "Command arguments: arg1 'second arg'", - 'Command output: [use --verbose to show]', - ] - - -@pytest.mark.parametrize('command_output', [ - # Test trailing newline. - 'output line 1\noutput line 2\n', - # Test no trailing newline. - 'output line 1\noutput line 2', -]) -def test_format_command_result__DEBUG(caplog, command_output): - caplog.set_level(logging.DEBUG) - actual = pip._internal.wheel_builder.format_command_result( - command_args=['arg1', 'arg2'], - command_output=command_output, - ) - assert actual.splitlines() == [ - "Command arguments: arg1 arg2", - 'Command output:', - 'output line 1', - 'output line 2', - '----------------------------------------', - ] - - -@pytest.mark.parametrize('log_level', ['DEBUG', 'INFO']) -def test_format_command_result__empty_output(caplog, log_level): - caplog.set_level(log_level) - actual = pip._internal.wheel_builder.format_command_result( - command_args=['arg1', 'arg2'], - command_output='', - ) - assert actual.splitlines() == [ - "Command arguments: arg1 arg2", - 'Command output: None', - ] - - def call_get_legacy_build_wheel_path(caplog, names): req = make_test_install_req() - wheel_path = pip._internal.wheel_builder.get_legacy_build_wheel_path( + wheel_path = get_legacy_build_wheel_path( names=names, temp_dir='/tmp/abcd', req=req, @@ -409,22 +241,6 @@ def test_wheel_version(tmpdir, data): assert not wheel.wheel_version(tmpdir + 'broken') -def test_python_tag(): - wheelnames = [ - 'simplewheel-1.0-py2.py3-none-any.whl', - 'simplewheel-1.0-py27-none-any.whl', - 'simplewheel-2.0-1-py2.py3-none-any.whl', - ] - newnames = [ - 'simplewheel-1.0-py37-none-any.whl', - 'simplewheel-1.0-py37-none-any.whl', - 'simplewheel-2.0-1-py37-none-any.whl', - ] - for name, expected in zip(wheelnames, newnames): - result = pip._internal.wheel_builder.replace_python_tag(name, 'py37') - assert result == expected - - def test_check_compatibility(): name = 'test' vc = wheel.VERSION_COMPATIBLE @@ -744,22 +560,6 @@ def test_dist_info_contains_empty_dir(self, data, tmpdir): os.path.join(self.dest_dist_info, 'empty_dir')) -class TestWheelBuilder(object): - - def test_skip_building_wheels(self, caplog): - with patch('pip._internal.wheel.WheelBuilder._build_one') \ - as mock_build_one: - wheel_req = Mock(is_wheel=True, editable=False, constraint=False) - wb = pip._internal.wheel_builder.WheelBuilder( - preparer=Mock(), - wheel_cache=Mock(cache_dir=None), - ) - with caplog.at_level(logging.INFO): - wb.build([wheel_req]) - assert "due to already being wheel" in caplog.text - assert mock_build_one.mock_calls == [] - - class TestMessageAboutScriptsNotOnPATH(object): def _template(self, paths, scripts): diff --git a/tests/unit/test_wheel_builder.py b/tests/unit/test_wheel_builder.py new file mode 100644 index 00000000000..da6749a3b66 --- /dev/null +++ b/tests/unit/test_wheel_builder.py @@ -0,0 +1,209 @@ +import logging + +import pytest +from mock import Mock + +from pip._internal import wheel_builder +from pip._internal.models.link import Link +from tests.lib import _create_test_package + + +@pytest.mark.parametrize( + "s, expected", + [ + # Trivial. + ("pip-18.0", True), + + # Ambiguous. + ("foo-2-2", True), + ("im-valid", True), + + # Invalid. + ("invalid", False), + ("im_invalid", False), + ], +) +def test_contains_egg_info(s, expected): + result = wheel_builder._contains_egg_info(s) + assert result == expected + + +class ReqMock: + + def __init__( + self, + name="pendulum", + is_wheel=False, + editable=False, + link=None, + constraint=False, + source_dir="/tmp/pip-install-123/pendulum", + ): + self.name = name + self.is_wheel = is_wheel + self.editable = editable + self.link = link + self.constraint = constraint + self.source_dir = source_dir + + +@pytest.mark.parametrize( + "req, need_wheel, disallow_binaries, expected", + [ + # pip wheel (need_wheel=True) + (ReqMock(), True, False, True), + (ReqMock(), True, True, True), + (ReqMock(constraint=True), True, False, False), + (ReqMock(is_wheel=True), True, False, False), + (ReqMock(editable=True), True, False, True), + (ReqMock(source_dir=None), True, False, True), + (ReqMock(link=Link("git+https://g.c/org/repo")), True, False, True), + (ReqMock(link=Link("git+https://g.c/org/repo")), True, True, True), + # pip install (need_wheel=False) + (ReqMock(), False, False, True), + (ReqMock(), False, True, False), + (ReqMock(constraint=True), False, False, False), + (ReqMock(is_wheel=True), False, False, False), + (ReqMock(editable=True), False, False, False), + (ReqMock(source_dir=None), False, False, False), + # By default (i.e. when binaries are allowed), VCS requirements + # should be built in install mode. + (ReqMock(link=Link("git+https://g.c/org/repo")), False, False, True), + # Disallowing binaries, however, should cause them not to be built. + (ReqMock(link=Link("git+https://g.c/org/repo")), False, True, False), + ], +) +def test_should_build(req, need_wheel, disallow_binaries, expected): + should_build = wheel_builder.should_build( + req, + need_wheel, + check_binary_allowed=lambda req: not disallow_binaries, + ) + assert should_build is expected + + +@pytest.mark.parametrize( + "req, disallow_binaries, expected", + [ + (ReqMock(editable=True), False, False), + (ReqMock(source_dir=None), False, False), + (ReqMock(link=Link("git+https://g.c/org/repo")), False, False), + (ReqMock(link=Link("https://g.c/dist.tgz")), False, False), + (ReqMock(link=Link("https://g.c/dist-2.0.4.tgz")), False, True), + (ReqMock(editable=True), True, False), + (ReqMock(source_dir=None), True, False), + (ReqMock(link=Link("git+https://g.c/org/repo")), True, False), + (ReqMock(link=Link("https://g.c/dist.tgz")), True, False), + (ReqMock(link=Link("https://g.c/dist-2.0.4.tgz")), True, False), + ], +) +def test_should_cache( + req, disallow_binaries, expected +): + def check_binary_allowed(req): + return not disallow_binaries + + should_cache = wheel_builder.should_cache( + req, check_binary_allowed + ) + if not wheel_builder.should_build( + req, need_wheel=False, check_binary_allowed=check_binary_allowed + ): + # never cache if pip install (need_wheel=False) would not have built) + assert not should_cache + assert should_cache is expected + + +def test_should_cache_git_sha(script, tmpdir): + repo_path = _create_test_package(script, name="mypkg") + commit = script.run( + "git", "rev-parse", "HEAD", cwd=repo_path + ).stdout.strip() + # a link referencing a sha should be cached + url = "git+https://g.c/o/r@" + commit + "#egg=mypkg" + req = ReqMock(link=Link(url), source_dir=repo_path) + assert wheel_builder.should_cache(req, check_binary_allowed=lambda r: True) + # a link not referencing a sha should not be cached + url = "git+https://g.c/o/r@master#egg=mypkg" + req = ReqMock(link=Link(url), source_dir=repo_path) + assert not wheel_builder.should_cache(req, check_binary_allowed=lambda r: True) + + +def test_format_command_result__INFO(caplog): + caplog.set_level(logging.INFO) + actual = wheel_builder.format_command_result( + # Include an argument with a space to test argument quoting. + command_args=['arg1', 'second arg'], + command_output='output line 1\noutput line 2\n', + ) + assert actual.splitlines() == [ + "Command arguments: arg1 'second arg'", + 'Command output: [use --verbose to show]', + ] + + +@pytest.mark.parametrize('command_output', [ + # Test trailing newline. + 'output line 1\noutput line 2\n', + # Test no trailing newline. + 'output line 1\noutput line 2', +]) +def test_format_command_result__DEBUG(caplog, command_output): + caplog.set_level(logging.DEBUG) + actual = wheel_builder.format_command_result( + command_args=['arg1', 'arg2'], + command_output=command_output, + ) + assert actual.splitlines() == [ + "Command arguments: arg1 arg2", + 'Command output:', + 'output line 1', + 'output line 2', + '----------------------------------------', + ] + + +@pytest.mark.parametrize('log_level', ['DEBUG', 'INFO']) +def test_format_command_result__empty_output(caplog, log_level): + caplog.set_level(log_level) + actual = wheel_builder.format_command_result( + command_args=['arg1', 'arg2'], + command_output='', + ) + assert actual.splitlines() == [ + "Command arguments: arg1 arg2", + 'Command output: None', + ] + + +def test_python_tag(): + wheelnames = [ + 'simplewheel-1.0-py2.py3-none-any.whl', + 'simplewheel-1.0-py27-none-any.whl', + 'simplewheel-2.0-1-py2.py3-none-any.whl', + ] + newnames = [ + 'simplewheel-1.0-py37-none-any.whl', + 'simplewheel-1.0-py37-none-any.whl', + 'simplewheel-2.0-1-py37-none-any.whl', + ] + for name, expected in zip(wheelnames, newnames): + result = wheel_builder.replace_python_tag(name, 'py37') + assert result == expected + + +class TestWheelBuilder(object): + + def test_skip_building_wheels(self, caplog): + wb = wheel_builder.WheelBuilder( + preparer=Mock(), + wheel_cache=Mock(cache_dir=None), + ) + wb._build_one = mock_build_one = Mock() + + wheel_req = Mock(is_wheel=True, editable=False, constraint=False) + with caplog.at_level(logging.INFO): + wb.build([wheel_req]) + + assert "due to already being wheel" in caplog.text + assert mock_build_one.mock_calls == [] From b1f2f747f90b44f948eb485ecbab0590741ec6e9 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 4 Nov 2019 11:50:29 +0530 Subject: [PATCH 0684/3170] :art: a test --- tests/unit/test_wheel_builder.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_wheel_builder.py b/tests/unit/test_wheel_builder.py index da6749a3b66..8db535dadc2 100644 --- a/tests/unit/test_wheel_builder.py +++ b/tests/unit/test_wheel_builder.py @@ -119,14 +119,20 @@ def test_should_cache_git_sha(script, tmpdir): commit = script.run( "git", "rev-parse", "HEAD", cwd=repo_path ).stdout.strip() + # a link referencing a sha should be cached url = "git+https://g.c/o/r@" + commit + "#egg=mypkg" req = ReqMock(link=Link(url), source_dir=repo_path) - assert wheel_builder.should_cache(req, check_binary_allowed=lambda r: True) + assert wheel_builder.should_cache( + req, check_binary_allowed=lambda r: True, + ) + # a link not referencing a sha should not be cached url = "git+https://g.c/o/r@master#egg=mypkg" req = ReqMock(link=Link(url), source_dir=repo_path) - assert not wheel_builder.should_cache(req, check_binary_allowed=lambda r: True) + assert not wheel_builder.should_cache( + req, check_binary_allowed=lambda r: True, + ) def test_format_command_result__INFO(caplog): From 911722173ec3b7ed3b1c3ff5a2637a7340c904e4 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 3 Nov 2019 15:25:49 +0530 Subject: [PATCH 0685/3170] Move distributions.{source.legacy -> source} Why: Based on some more experience from refactoring metadata generation, it became clear to me that the separation of legacy vs modern codepaths should happen at lower level than this abstraction. --- src/pip/_internal/distributions/__init__.py | 2 +- .../_internal/distributions/{source/legacy.py => source.py} | 5 ----- src/pip/_internal/distributions/source/__init__.py | 0 3 files changed, 1 insertion(+), 6 deletions(-) rename src/pip/_internal/distributions/{source/legacy.py => source.py} (93%) delete mode 100644 src/pip/_internal/distributions/source/__init__.py diff --git a/src/pip/_internal/distributions/__init__.py b/src/pip/_internal/distributions/__init__.py index bba02f26cd6..20ffe2b32b5 100644 --- a/src/pip/_internal/distributions/__init__.py +++ b/src/pip/_internal/distributions/__init__.py @@ -1,4 +1,4 @@ -from pip._internal.distributions.source.legacy import SourceDistribution +from pip._internal.distributions.source import SourceDistribution from pip._internal.distributions.wheel import WheelDistribution from pip._internal.utils.typing import MYPY_CHECK_RUNNING diff --git a/src/pip/_internal/distributions/source/legacy.py b/src/pip/_internal/distributions/source.py similarity index 93% rename from src/pip/_internal/distributions/source/legacy.py rename to src/pip/_internal/distributions/source.py index 6d2f53ebc65..a4bbe838d19 100644 --- a/src/pip/_internal/distributions/source/legacy.py +++ b/src/pip/_internal/distributions/source.py @@ -16,11 +16,6 @@ class SourceDistribution(AbstractDistribution): The preparation step for these needs metadata for the packages to be generated, either using PEP 517 or using the legacy `setup.py egg_info`. - - NOTE from @pradyunsg (14 June 2019) - I expect SourceDistribution class will need to be split into - `legacy_source` (setup.py based) and `source` (PEP 517 based) when we start - bringing logic for preparation out of InstallRequirement into this class. """ def get_pkg_resources_distribution(self): diff --git a/src/pip/_internal/distributions/source/__init__.py b/src/pip/_internal/distributions/source/__init__.py deleted file mode 100644 index e69de29bb2d..00000000000 From 33ccea2e0c4a7e0a07d41fc056dd06b33896dfdb Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 3 Nov 2019 16:25:35 +0530 Subject: [PATCH 0686/3170] Reduce operations.build.metadata's API to 1 function Why: There isn't any state being maintained, or multiple uses of the metadata generator for a single requirement. The additional complexity does not help in any significant manner. --- src/pip/_internal/operations/build/metadata.py | 17 ++++++----------- src/pip/_internal/req/req_install.py | 5 ++--- 2 files changed, 8 insertions(+), 14 deletions(-) diff --git a/src/pip/_internal/operations/build/metadata.py b/src/pip/_internal/operations/build/metadata.py index 66b59eb5129..43c47813884 100644 --- a/src/pip/_internal/operations/build/metadata.py +++ b/src/pip/_internal/operations/build/metadata.py @@ -12,25 +12,20 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Callable - from pip._internal.req.req_install import InstallRequirement logger = logging.getLogger(__name__) -def get_metadata_generator(install_req): - # type: (InstallRequirement) -> Callable[[InstallRequirement], str] - """Return a callable metadata generator for this InstallRequirement. - - A metadata generator takes an InstallRequirement (install_req) as an input, - generates metadata via the appropriate process for that install_req and - returns the generated metadata directory. +def generate_metadata(install_req): + # type: (InstallRequirement) -> str + """Generate metadata and return the metadata directory. """ + func = _generate_metadata if not install_req.use_pep517: - return _generate_metadata_legacy + func = _generate_metadata_legacy - return _generate_metadata + return func(install_req) def _generate_metadata(install_req): diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index fe0f8b4c76d..20c29685c43 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -24,7 +24,7 @@ from pip._internal.exceptions import InstallationError from pip._internal.locations import distutils_scheme from pip._internal.models.link import Link -from pip._internal.operations.build.metadata import get_metadata_generator +from pip._internal.operations.build.metadata import generate_metadata from pip._internal.pyproject import load_pyproject_toml, make_pyproject_path from pip._internal.req.req_uninstall import UninstallPathSet from pip._internal.utils.compat import native_str @@ -615,9 +615,8 @@ def prepare_metadata(self): """ assert self.source_dir - metadata_generator = get_metadata_generator(self) with indent_log(): - self.metadata_directory = metadata_generator(self) + self.metadata_directory = generate_metadata(self) if not self.name: self.move_to_correct_build_directory() From 54fc70d212d445618c6e2ba94ddce63add7461d7 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 3 Nov 2019 19:35:44 +0530 Subject: [PATCH 0687/3170] Stop generating metadata as part of a unit test --- tests/unit/test_req.py | 25 ++++++++++--------------- 1 file changed, 10 insertions(+), 15 deletions(-) diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index fcee96509aa..a7477b33c76 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -32,12 +32,7 @@ from pip._internal.req.req_file import ParsedLine, get_line_parser, handle_line from pip._internal.req.req_tracker import RequirementTracker from pip._internal.utils.urls import path_to_url -from tests.lib import ( - DATA_DIR, - assert_raises_regexp, - make_test_finder, - requirements_file, -) +from tests.lib import assert_raises_regexp, make_test_finder, requirements_file def get_processed_req_from_line(line, fname='file', lineno=1): @@ -664,17 +659,17 @@ def test_exclusive_environment_markers(): assert req_set.has_requirement('Django') -def test_mismatched_versions(caplog, tmpdir): - original_source = os.path.join(DATA_DIR, 'src', 'simplewheel-1.0') - source_dir = os.path.join(tmpdir, 'simplewheel') - shutil.copytree(original_source, source_dir) - req = InstallRequirement(req=Requirement('simplewheel==2.0'), - comes_from=None, source_dir=source_dir) - req.prepare_metadata() +def test_mismatched_versions(caplog): + req = InstallRequirement( + req=Requirement('simplewheel==2.0'), + comes_from=None, + source_dir="/tmp/somewhere", + ) + # Monkeypatch! + req._metadata = {"name": "simplewheel", "version": "1.0"} req.assert_source_matches_version() assert caplog.records[-1].message == ( - 'Requested simplewheel==2.0, ' - 'but installing version 1.0' + 'Requested simplewheel==2.0, but installing version 1.0' ) From 67ae8fdc28bfc54ff2f67a852b889d460ac78861 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 3 Nov 2019 16:26:52 +0530 Subject: [PATCH 0688/3170] Move call to assert_source_matches_version --- src/pip/_internal/distributions/source.py | 1 - src/pip/_internal/req/req_install.py | 2 ++ 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/distributions/source.py b/src/pip/_internal/distributions/source.py index a4bbe838d19..a4fad1f5472 100644 --- a/src/pip/_internal/distributions/source.py +++ b/src/pip/_internal/distributions/source.py @@ -32,7 +32,6 @@ def prepare_distribution_metadata(self, finder, build_isolation): self._setup_isolation(finder) self.req.prepare_metadata() - self.req.assert_source_matches_version() def _setup_isolation(self, finder): def _raise_conflicts(conflicting_with, conflicting_reqs): diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 20c29685c43..9e7fe83b3f2 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -623,6 +623,8 @@ def prepare_metadata(self): else: self.warn_on_mismatching_name() + self.assert_source_matches_version() + @property def metadata(self): # type: () -> Any From 528d27a2fe82b77ad88a98cd5112e2890dcb1494 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 3 Nov 2019 16:35:31 +0530 Subject: [PATCH 0689/3170] Nicer comments in prepare_distribution_metadata --- src/pip/_internal/distributions/source.py | 7 +++---- src/pip/_internal/req/req_install.py | 1 + 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/distributions/source.py b/src/pip/_internal/distributions/source.py index a4fad1f5472..f74ec5acc82 100644 --- a/src/pip/_internal/distributions/source.py +++ b/src/pip/_internal/distributions/source.py @@ -22,11 +22,10 @@ def get_pkg_resources_distribution(self): return self.req.get_dist() def prepare_distribution_metadata(self, finder, build_isolation): - # Prepare for building. We need to: - # 1. Load pyproject.toml (if it exists) - # 2. Set up the build environment - + # Load pyproject.toml, to determine whether PEP 517 is to be used self.req.load_pyproject_toml() + + # Set up the build isolation, if this requirement should be isolated should_isolate = self.req.use_pep517 and build_isolation if should_isolate: self._setup_isolation(finder) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 9e7fe83b3f2..aa8599d618c 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -618,6 +618,7 @@ def prepare_metadata(self): with indent_log(): self.metadata_directory = generate_metadata(self) + # Act on the newly generated metadata, based on the name and version. if not self.name: self.move_to_correct_build_directory() else: From f137aef12ed56279de4d6754164af883f9417ae0 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 4 Nov 2019 15:24:09 +0530 Subject: [PATCH 0690/3170] Choose metadata generator in prepare_metadata --- src/pip/_internal/operations/build/metadata.py | 15 +++------------ .../_internal/operations/build/metadata_legacy.py | 4 ++++ src/pip/_internal/req/req_install.py | 8 +++++++- 3 files changed, 14 insertions(+), 13 deletions(-) diff --git a/src/pip/_internal/operations/build/metadata.py b/src/pip/_internal/operations/build/metadata.py index 43c47813884..43c3590c0ef 100644 --- a/src/pip/_internal/operations/build/metadata.py +++ b/src/pip/_internal/operations/build/metadata.py @@ -5,8 +5,6 @@ import logging import os -from pip._internal.operations.build.metadata_legacy import \ - generate_metadata as _generate_metadata_legacy from pip._internal.utils.subprocess import runner_with_spinner_message from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -19,17 +17,10 @@ def generate_metadata(install_req): # type: (InstallRequirement) -> str - """Generate metadata and return the metadata directory. - """ - func = _generate_metadata - if not install_req.use_pep517: - func = _generate_metadata_legacy - - return func(install_req) - + """Generate metadata using mechanisms described in PEP 517. -def _generate_metadata(install_req): - # type: (InstallRequirement) -> str + Returns the generated metadata directory. + """ assert install_req.pep517_backend is not None build_env = install_req.build_env backend = install_req.pep517_backend diff --git a/src/pip/_internal/operations/build/metadata_legacy.py b/src/pip/_internal/operations/build/metadata_legacy.py index ba6265db791..d817504764b 100644 --- a/src/pip/_internal/operations/build/metadata_legacy.py +++ b/src/pip/_internal/operations/build/metadata_legacy.py @@ -80,6 +80,10 @@ def depth_of_directory(dir_): def generate_metadata(install_req): # type: (InstallRequirement) -> str + """Generate metadata using setup.py-based defacto mechanisms.ArithmeticError + + Returns the generated metadata directory. + """ assert install_req.unpacked_source_directory req_details_str = install_req.name or "from {}".format(install_req.link) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index aa8599d618c..4bc341fb97d 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -25,6 +25,8 @@ from pip._internal.locations import distutils_scheme from pip._internal.models.link import Link from pip._internal.operations.build.metadata import generate_metadata +from pip._internal.operations.build.metadata_legacy import \ + generate_metadata as generate_metadata_legacy from pip._internal.pyproject import load_pyproject_toml, make_pyproject_path from pip._internal.req.req_uninstall import UninstallPathSet from pip._internal.utils.compat import native_str @@ -615,8 +617,12 @@ def prepare_metadata(self): """ assert self.source_dir + metadata_generator = generate_metadata + if not self.use_pep517: + metadata_generator = generate_metadata_legacy + with indent_log(): - self.metadata_directory = generate_metadata(self) + self.metadata_directory = metadata_generator(self) # Act on the newly generated metadata, based on the name and version. if not self.name: From 125f483675dc0619da4f751d3e41c04bc43339c9 Mon Sep 17 00:00:00 2001 From: Hugo <hugovk@users.noreply.github.com> Date: Mon, 4 Nov 2019 23:16:06 +0200 Subject: [PATCH 0691/3170] Test Python 3.8 on GHA --- .github/workflows/python-linters.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/python-linters.yml b/.github/workflows/python-linters.yml index 705dea44db2..6e14c5e9dd9 100644 --- a/.github/workflows/python-linters.yml +++ b/.github/workflows/python-linters.yml @@ -22,10 +22,10 @@ jobs: - TOXENV: lint steps: - uses: actions/checkout@master - - name: Set up Python ${{ matrix.env.PYTHON_VERSION || 3.7 }} + - name: Set up Python ${{ matrix.env.PYTHON_VERSION || 3.8 }} uses: actions/setup-python@v1 with: - python-version: ${{ matrix.env.PYTHON_VERSION || 3.7 }} + python-version: ${{ matrix.env.PYTHON_VERSION || 3.8 }} - name: Pre-configure global Git settings run: >- tools/travis/setup.sh From a47fef71c2c17aba18a838cdcff759eb0644f3ec Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Mon, 4 Nov 2019 18:00:42 -0500 Subject: [PATCH 0692/3170] Deprecate --skip-requirements-regex --- news/7297.removal | 1 + src/pip/_internal/cli/base_command.py | 11 +++++++++++ 2 files changed, 12 insertions(+) create mode 100644 news/7297.removal diff --git a/news/7297.removal b/news/7297.removal new file mode 100644 index 00000000000..663fd9ad609 --- /dev/null +++ b/news/7297.removal @@ -0,0 +1 @@ +Deprecate undocumented ``--skip-requirements-regex`` option. diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index dad08c24ea7..6b404954044 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -131,6 +131,17 @@ def _main(self, args): ) + message deprecated(message, replacement=None, gone_in=None) + if options.skip_requirements_regex: + deprecated( + "--skip-requirements-regex is unsupported and will be removed", + replacement=( + "manage requirements/constraints files explicitly, " + "possibly generating them from metadata" + ), + gone_in="20.1", + issue=7297, + ) + # TODO: Try to get these passing down from the command? # without resorting to os.environ to hold these. # This also affects isolated builds and it should. From 3ed3f41c3ec91f03407e09b85adefa8a48cd7d39 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Mon, 4 Nov 2019 19:34:40 -0500 Subject: [PATCH 0693/3170] Make InstallRequirement.install_editable arguments mandatory --- src/pip/_internal/req/req_install.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index fe0f8b4c76d..6278f375683 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -673,8 +673,8 @@ def ensure_has_source_dir(self, parent_dir): def install_editable( self, install_options, # type: List[str] - global_options=(), # type: Sequence[str] - prefix=None # type: Optional[str] + global_options, # type: Sequence[str] + prefix, # type: Optional[str] ): # type: (...) -> None logger.info('Running setup.py develop for %s', self.name) From b28d05bb50007680fe21868d54ad63421ea4256a Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Mon, 4 Nov 2019 19:40:00 -0500 Subject: [PATCH 0694/3170] Set user and prefix in setuptools args functions Previously we were adding arguments to install_options in response to command-line parameters, which leads to spooky action at a distance. Now we provide the arguments explicitly. --- src/pip/_internal/commands/install.py | 4 ---- src/pip/_internal/req/req_install.py | 8 +++++++- src/pip/_internal/utils/setuptools_build.py | 12 ++++++++++++ 3 files changed, 19 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 1e1d273fe9a..8e5d07e200b 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -302,10 +302,6 @@ def run(self, options, args): isolated_mode=options.isolated_mode, ) - if options.use_user_site: - install_options.append('--user') - install_options.append('--prefix=') - target_temp_dir = None # type: Optional[TempDirectory] target_temp_dir_path = None # type: Optional[str] if options.target_dir: diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 6278f375683..c9957552e01 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -675,6 +675,7 @@ def install_editable( install_options, # type: List[str] global_options, # type: Sequence[str] prefix, # type: Optional[str] + use_user_site, # type: bool ): # type: (...) -> None logger.info('Running setup.py develop for %s', self.name) @@ -685,6 +686,7 @@ def install_editable( install_options=install_options, no_user_config=self.isolated, prefix=prefix, + use_user_site=use_user_site, ) with indent_log(): @@ -843,7 +845,10 @@ def install( 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, + use_user_site=use_user_site, ) return if self.is_wheel: @@ -890,6 +895,7 @@ def install( root=root, prefix=prefix, header_dir=header_dir, + use_user_site=use_user_site, no_user_config=self.isolated, pycompile=pycompile, ) diff --git a/src/pip/_internal/utils/setuptools_build.py b/src/pip/_internal/utils/setuptools_build.py index 1bf416eb34d..b68d16a4f1f 100644 --- a/src/pip/_internal/utils/setuptools_build.py +++ b/src/pip/_internal/utils/setuptools_build.py @@ -91,8 +91,11 @@ def make_setuptools_develop_args( install_options, # type: Sequence[str] no_user_config, # type: bool prefix, # type: Optional[str] + use_user_site, # type: bool ): # type: (...) -> List[str] + assert not (use_user_site and prefix) + args = make_setuptools_shim_args( setup_py_path, global_options=global_options, @@ -106,6 +109,9 @@ def make_setuptools_develop_args( if prefix: args += ["--prefix", prefix] + if use_user_site: + args += ["--user", "--prefix="] + return args @@ -135,10 +141,14 @@ def make_setuptools_install_args( root, # type: Optional[str] prefix, # type: Optional[str] header_dir, # type: Optional[str] + use_user_site, # type: bool no_user_config, # type: bool pycompile # type: bool ): # type: (...) -> List[str] + assert not (use_user_site and prefix) + assert not (use_user_site and root) + args = make_setuptools_shim_args( setup_py_path, global_options=global_options, @@ -152,6 +162,8 @@ def make_setuptools_install_args( args += ["--root", root] if prefix is not None: args += ["--prefix", prefix] + if use_user_site: + args += ["--user", "--prefix="] if pycompile: args += ["--compile"] From 431727a4e4abe8ec6bcb4c1549b98cee41255624 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Mon, 4 Nov 2019 19:48:24 -0500 Subject: [PATCH 0695/3170] Set home in setuptools args functions --- src/pip/_internal/commands/install.py | 1 - src/pip/_internal/req/req_install.py | 4 ++++ src/pip/_internal/utils/setuptools_build.py | 6 ++++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 8e5d07e200b..000c5da739a 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -317,7 +317,6 @@ def run(self, options, args): # Create a target directory for using with the target option target_temp_dir = TempDirectory(kind="target") target_temp_dir_path = target_temp_dir.path - install_options.append('--home=' + target_temp_dir_path) global_options = options.global_options or [] diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index c9957552e01..fdd85a1e243 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -675,6 +675,7 @@ def install_editable( install_options, # type: List[str] global_options, # type: Sequence[str] prefix, # type: Optional[str] + home, # type: Optional[str] use_user_site, # type: bool ): # type: (...) -> None @@ -686,6 +687,7 @@ def install_editable( install_options=install_options, no_user_config=self.isolated, prefix=prefix, + home=home, use_user_site=use_user_site, ) @@ -848,6 +850,7 @@ def install( install_options, global_options, prefix=prefix, + home=home, use_user_site=use_user_site, ) return @@ -895,6 +898,7 @@ def install( root=root, prefix=prefix, header_dir=header_dir, + home=home, use_user_site=use_user_site, no_user_config=self.isolated, pycompile=pycompile, diff --git a/src/pip/_internal/utils/setuptools_build.py b/src/pip/_internal/utils/setuptools_build.py index b68d16a4f1f..497b0eb4939 100644 --- a/src/pip/_internal/utils/setuptools_build.py +++ b/src/pip/_internal/utils/setuptools_build.py @@ -91,6 +91,7 @@ def make_setuptools_develop_args( install_options, # type: Sequence[str] no_user_config, # type: bool prefix, # type: Optional[str] + home, # type: Optional[str] use_user_site, # type: bool ): # type: (...) -> List[str] @@ -108,6 +109,8 @@ def make_setuptools_develop_args( if prefix: args += ["--prefix", prefix] + if home is not None: + args += ["--home", home] if use_user_site: args += ["--user", "--prefix="] @@ -141,6 +144,7 @@ def make_setuptools_install_args( root, # type: Optional[str] prefix, # type: Optional[str] header_dir, # type: Optional[str] + home, # type: Optional[str] use_user_site, # type: bool no_user_config, # type: bool pycompile # type: bool @@ -162,6 +166,8 @@ def make_setuptools_install_args( args += ["--root", root] if prefix is not None: args += ["--prefix", prefix] + if home is not None: + args += ["--home", home] if use_user_site: args += ["--user", "--prefix="] From 93ffb10a239fabc48b6c516e630029f67bd773de Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Mon, 4 Nov 2019 20:01:09 -0500 Subject: [PATCH 0696/3170] Remove old type workaround comments --- src/pip/_internal/locations.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/pip/_internal/locations.py b/src/pip/_internal/locations.py index 1899c7d03c4..67c19caf536 100644 --- a/src/pip/_internal/locations.py +++ b/src/pip/_internal/locations.py @@ -106,7 +106,6 @@ def distutils_scheme(dist_name, user=False, home=None, root=None, dist_args.update(extra_dist_args) d = Distribution(dist_args) - # Ignoring, typeshed issue reported python/typeshed/issues/2567 d.parse_config_files() # NOTE: Ignoring type since mypy can't find attributes on 'Command' i = d.get_command_obj('install', create=True) # type: Any @@ -131,9 +130,7 @@ def distutils_scheme(dist_name, user=False, home=None, root=None, # platlib). Note, i.install_lib is *always* set after # finalize_options(); we only want to override here if the user # has explicitly requested it hence going back to the config - - # Ignoring, typeshed issue reported python/typeshed/issues/2567 - if 'install_lib' in d.get_option_dict('install'): # type: ignore + if 'install_lib' in d.get_option_dict('install'): scheme.update(dict(purelib=i.install_lib, platlib=i.install_lib)) if running_under_virtualenv(): From 0339983b0f720823cefcbbe691fcd84ab218584a Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Mon, 4 Nov 2019 20:02:28 -0500 Subject: [PATCH 0697/3170] Remove intermediate args variable --- src/pip/_internal/locations.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/locations.py b/src/pip/_internal/locations.py index 67c19caf536..1ce7910dfe9 100644 --- a/src/pip/_internal/locations.py +++ b/src/pip/_internal/locations.py @@ -96,14 +96,9 @@ def distutils_scheme(dist_name, user=False, home=None, root=None, """ from distutils.dist import Distribution - scheme = {} - - if isolated: - extra_dist_args = {"script_args": ["--no-user-cfg"]} - else: - extra_dist_args = {} dist_args = {'name': dist_name} # type: Dict[str, Union[str, List[str]]] - dist_args.update(extra_dist_args) + if isolated: + dist_args["script_args"] = ["--no-user-cfg"] d = Distribution(dist_args) d.parse_config_files() @@ -122,6 +117,8 @@ def distutils_scheme(dist_name, user=False, home=None, root=None, i.home = home or i.home i.root = root or i.root i.finalize_options() + + scheme = {} for key in SCHEME_KEYS: scheme[key] = getattr(i, 'install_' + key) From 912f5763c6e5e22417da3748efb25848fef013b4 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Mon, 4 Nov 2019 20:16:36 -0500 Subject: [PATCH 0698/3170] Move utils.misc.cast to avoid circular import --- src/pip/_internal/utils/filesystem.py | 3 +-- src/pip/_internal/utils/misc.py | 9 ++------- src/pip/_internal/utils/typing.py | 9 +++++++++ 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/utils/filesystem.py b/src/pip/_internal/utils/filesystem.py index bedd662958a..bce2058bcf1 100644 --- a/src/pip/_internal/utils/filesystem.py +++ b/src/pip/_internal/utils/filesystem.py @@ -13,8 +13,7 @@ from pip._vendor.six import PY2 from pip._internal.utils.compat import get_path_uid -from pip._internal.utils.misc import cast -from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from pip._internal.utils.typing import MYPY_CHECK_RUNNING, cast if MYPY_CHECK_RUNNING: from typing import BinaryIO, Iterator diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index b84826350bc..3e9c2c44e62 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -39,7 +39,7 @@ str_to_display, ) from pip._internal.utils.marker_files import write_delete_marker_file -from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from pip._internal.utils.typing import MYPY_CHECK_RUNNING, cast from pip._internal.utils.virtualenv import ( running_under_virtualenv, virtualenv_no_global, @@ -53,16 +53,11 @@ if MYPY_CHECK_RUNNING: from typing import ( Any, AnyStr, Container, Iterable, List, Optional, Text, - Tuple, Union, cast, + Tuple, Union, ) from pip._vendor.pkg_resources import Distribution VersionInfo = Tuple[int, int, int] -else: - # typing's cast() is needed at runtime, but we don't want to import typing. - # Thus, we use a dummy no-op version, which we tell mypy to ignore. - def cast(type_, value): # type: ignore - return value __all__ = ['rmtree', 'display_path', 'backup_dir', diff --git a/src/pip/_internal/utils/typing.py b/src/pip/_internal/utils/typing.py index 10170ce296b..8505a29b15d 100644 --- a/src/pip/_internal/utils/typing.py +++ b/src/pip/_internal/utils/typing.py @@ -27,3 +27,12 @@ """ MYPY_CHECK_RUNNING = False + + +if MYPY_CHECK_RUNNING: + from typing import cast +else: + # typing's cast() is needed at runtime, but we don't want to import typing. + # Thus, we use a dummy no-op version, which we tell mypy to ignore. + def cast(type_, value): # type: ignore + return value From 92a9183f47eca82ae2e8884af68f58db7072911b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Mon, 4 Nov 2019 20:14:02 +0100 Subject: [PATCH 0699/3170] Remove unused WheelBuilder.no_clean --- src/pip/_internal/commands/wheel.py | 1 - src/pip/_internal/wheel_builder.py | 2 -- 2 files changed, 3 deletions(-) diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index e1d59ee4c2b..1bf2bbecb83 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -192,7 +192,6 @@ def run(self, options, args): preparer, wheel_cache, build_options=options.build_options or [], global_options=options.global_options or [], - no_clean=options.no_clean, path_to_wheelnames=options.path_to_wheelnames ) build_failures = wb.build( diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index ba93933edf8..9b2db949c27 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -205,7 +205,6 @@ def __init__( build_options=None, # type: Optional[List[str]] global_options=None, # type: Optional[List[str]] check_binary_allowed=None, # type: Optional[BinaryAllowedPredicate] - no_clean=False, # type: bool path_to_wheelnames=None, # type: Optional[Union[bytes, Text]] ): # type: (...) -> None @@ -221,7 +220,6 @@ def __init__( self.build_options = build_options or [] self.global_options = global_options or [] self.check_binary_allowed = check_binary_allowed - self.no_clean = no_clean # path where to save built names of built wheels self.path_to_wheelnames = path_to_wheelnames # file names of built wheel names From 1b9c9e2901252d21f208eb86b062cb0f7bcba428 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 7 Oct 2019 12:50:12 +0530 Subject: [PATCH 0700/3170] Factor out running_under_virtualenv conditionals Why: This would allow for use in an updated `virtualenv_no_global` that supports PEP 405 virtual environments. --- src/pip/_internal/utils/virtualenv.py | 28 ++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/utils/virtualenv.py b/src/pip/_internal/utils/virtualenv.py index 380db1c3281..a83b3e951c9 100644 --- a/src/pip/_internal/utils/virtualenv.py +++ b/src/pip/_internal/utils/virtualenv.py @@ -3,20 +3,30 @@ import sys -def running_under_virtualenv(): +def _running_under_venv(): # type: () -> bool + """Checks if sys.base_prefix and sys.prefix match. + + This handles PEP 405 compliant virtual environments. """ - Return True if we're running inside a virtualenv, False otherwise. + return sys.prefix != getattr(sys, "base_prefix", sys.prefix) + +def _running_under_regular_virtualenv(): + # type: () -> bool + """Checks if sys.real_prefix is set. + + This handles virtual environments created with pypa's virtualenv. """ - if hasattr(sys, 'real_prefix'): - # pypa/virtualenv case - return True - elif sys.prefix != getattr(sys, "base_prefix", sys.prefix): - # PEP 405 venv - return True + # pypa/virtualenv case + return hasattr(sys, 'real_prefix') + - return False +def running_under_virtualenv(): + # type: () -> bool + """Return a boolean, whether running under a virtual environment. + """ + return _running_under_venv() or _running_under_regular_virtualenv() def virtualenv_no_global(): From fd7c9b7ce4cf8c82499d53d401d995e71f108748 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 7 Oct 2019 13:39:01 +0530 Subject: [PATCH 0701/3170] Refactor virtualenv_no_global Why: This change makes it easier to introduce handling for PEP 405 based virtual environment's global site-package exclusion logic --- src/pip/_internal/utils/virtualenv.py | 35 ++++++++++++++++++--------- tests/unit/test_utils_virtualenv.py | 18 +++++++++----- 2 files changed, 36 insertions(+), 17 deletions(-) diff --git a/src/pip/_internal/utils/virtualenv.py b/src/pip/_internal/utils/virtualenv.py index a83b3e951c9..3bc7cd1e382 100644 --- a/src/pip/_internal/utils/virtualenv.py +++ b/src/pip/_internal/utils/virtualenv.py @@ -1,7 +1,10 @@ -import os.path +import logging +import os import site import sys +logger = logging.getLogger(__name__) + def _running_under_venv(): # type: () -> bool @@ -29,16 +32,26 @@ def running_under_virtualenv(): return _running_under_venv() or _running_under_regular_virtualenv() -def virtualenv_no_global(): +def _no_global_under_regular_virtualenv(): # type: () -> bool + """Check if "no-global-site-packages.txt" exists beside site.py + + This mirrors logic in pypa/virtualenv for determining whether system + site-packages are visible in the virtual environment. """ - Return True if in a venv and no system site packages. - """ - # this mirrors the logic in virtualenv.py for locating the - # no-global-site-packages.txt file site_mod_dir = os.path.dirname(os.path.abspath(site.__file__)) - no_global_file = os.path.join(site_mod_dir, 'no-global-site-packages.txt') - if running_under_virtualenv() and os.path.isfile(no_global_file): - return True - else: - return False + no_global_site_packages_file = os.path.join( + site_mod_dir, 'no-global-site-packages.txt', + ) + return os.path.exists(no_global_site_packages_file) + + +def virtualenv_no_global(): + # type: () -> bool + """Returns a boolean, whether running in venv with no system site-packages. + """ + + if _running_under_regular_virtualenv(): + return _no_global_under_regular_virtualenv() + + return False diff --git a/tests/unit/test_utils_virtualenv.py b/tests/unit/test_utils_virtualenv.py index 80e30404bda..98071e82d6f 100644 --- a/tests/unit/test_utils_virtualenv.py +++ b/tests/unit/test_utils_virtualenv.py @@ -32,20 +32,26 @@ def test_running_under_virtualenv( @pytest.mark.parametrize( - "running_under_virtualenv, no_global_file, expected", [ + "under_virtualenv, no_global_file, expected", [ (False, False, False), (False, True, False), (True, False, False), (True, True, True), ], ) -def test_virtualenv_no_global( - monkeypatch, tmpdir, - running_under_virtualenv, no_global_file, expected): +def test_virtualenv_no_global_with_regular_virtualenv( + monkeypatch, + tmpdir, + under_virtualenv, + no_global_file, + expected, +): monkeypatch.setattr(site, '__file__', tmpdir / 'site.py') monkeypatch.setattr( - virtualenv, 'running_under_virtualenv', - lambda: running_under_virtualenv) + virtualenv, '_running_under_regular_virtualenv', + lambda: under_virtualenv, + ) if no_global_file: (tmpdir / 'no-global-site-packages.txt').touch() + assert virtualenv.virtualenv_no_global() == expected From 1aee0eb5cbaa48b5a193b942e38a15c4cf376c05 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 7 Oct 2019 15:36:29 +0530 Subject: [PATCH 0702/3170] Correctly ignore system site-packages for venv Why: PEP 405 virtual environments have a different mechanism for ignoring system site-packages in virtual environments. --- src/pip/_internal/utils/virtualenv.py | 48 +++++++++++++++++ tests/unit/test_utils_virtualenv.py | 78 +++++++++++++++++++++++++++ 2 files changed, 126 insertions(+) diff --git a/src/pip/_internal/utils/virtualenv.py b/src/pip/_internal/utils/virtualenv.py index 3bc7cd1e382..1be69a96d72 100644 --- a/src/pip/_internal/utils/virtualenv.py +++ b/src/pip/_internal/utils/virtualenv.py @@ -3,6 +3,11 @@ import site import sys +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import Optional, List + logger = logging.getLogger(__name__) @@ -32,6 +37,46 @@ def running_under_virtualenv(): return _running_under_venv() or _running_under_regular_virtualenv() +def _get_pyvenv_cfg_lines(): + # type: () -> Optional[List[str]] + """Reads {sys.prefix}/pyvenv.cfg and returns its contents as list of lines + + Returns None, if it could not read/access the file. + """ + pyvenv_cfg_file = os.path.join(sys.prefix, 'pyvenv.cfg') + try: + with open(pyvenv_cfg_file) as f: + return f.read().splitlines() # avoids trailing newlines + except OSError: + return None + + +def _no_global_under_venv(): + # type: () -> bool + """Check `{sys.prefix}/pyvenv.cfg` for system site-packages inclusion + + PEP 405 specifies that when system site-packages are not supposed to be + visible from a virtual environment, `pyvenv.cfg` must contain the following + line: + + include-system-site-packages = false + + Additionally, log a warning if accessing the file fails. + """ + cfg_lines = _get_pyvenv_cfg_lines() + if cfg_lines is None: + # We're not in a "sane" venv, so assume there is no system + # site-packages access (since that's PEP 405's default state). + logger.warning( + "Could not access 'pyvenv.cfg' despite a virtual environment " + "being active. Assuming global site-packages is not accessible " + "in this environment." + ) + return True + + return "include-system-site-packages = false" in cfg_lines + + def _no_global_under_regular_virtualenv(): # type: () -> bool """Check if "no-global-site-packages.txt" exists beside site.py @@ -54,4 +99,7 @@ def virtualenv_no_global(): if _running_under_regular_virtualenv(): return _no_global_under_regular_virtualenv() + if _running_under_venv(): + return _no_global_under_venv() + return False diff --git a/tests/unit/test_utils_virtualenv.py b/tests/unit/test_utils_virtualenv.py index 98071e82d6f..625539d7617 100644 --- a/tests/unit/test_utils_virtualenv.py +++ b/tests/unit/test_utils_virtualenv.py @@ -1,3 +1,4 @@ +import logging import site import sys @@ -46,6 +47,8 @@ def test_virtualenv_no_global_with_regular_virtualenv( no_global_file, expected, ): + monkeypatch.setattr(virtualenv, '_running_under_venv', lambda: False) + monkeypatch.setattr(site, '__file__', tmpdir / 'site.py') monkeypatch.setattr( virtualenv, '_running_under_regular_virtualenv', @@ -55,3 +58,78 @@ def test_virtualenv_no_global_with_regular_virtualenv( (tmpdir / 'no-global-site-packages.txt').touch() assert virtualenv.virtualenv_no_global() == expected + + +@pytest.mark.parametrize( + "pyvenv_cfg_lines, under_venv, expected, expect_warning", [ + (None, False, False, False), + (None, True, True, True), # this has a warning. + ( + [ + "home = <we do not care>", + "include-system-site-packages = true", + "version = <we do not care>", + ], + True, + False, + False, + ), + ( + [ + "home = <we do not care>", + "include-system-site-packages = false", + "version = <we do not care>", + ], + True, + True, + False, + ), + ], +) +def test_virtualenv_no_global_with_pep_405_virtual_environment( + monkeypatch, + caplog, + pyvenv_cfg_lines, + under_venv, + expected, + expect_warning, +): + monkeypatch.setattr( + virtualenv, '_running_under_regular_virtualenv', lambda: False + ) + monkeypatch.setattr( + virtualenv, '_get_pyvenv_cfg_lines', lambda: pyvenv_cfg_lines + ) + monkeypatch.setattr(virtualenv, '_running_under_venv', lambda: under_venv) + + with caplog.at_level(logging.WARNING): + assert virtualenv.virtualenv_no_global() == expected + + if expect_warning: + assert caplog.records + + # Check for basic information + message = caplog.records[-1].getMessage().lower() + assert "could not access 'pyvenv.cfg'" in message + assert "assuming global site-packages is not accessible" in message + + +@pytest.mark.parametrize( + "contents, expected", [ + (None, None), + ("", []), + ("a = b\nc = d\n", ["a = b", "c = d"]), + ("a = b\nc = d", ["a = b", "c = d"]), # no trailing newlines + ] +) +def test_get_pyvenv_cfg_lines_for_pep_405_virtual_environment( + monkeypatch, + tmpdir, + contents, + expected, +): + monkeypatch.setattr(sys, 'prefix', str(tmpdir)) + if contents is not None: + tmpdir.joinpath('pyvenv.cfg').write_text(contents) + + assert virtualenv._get_pyvenv_cfg_lines() == expected From 762ffd5b5ea527c9fcd4d74e8993ca27a9a3197c Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 7 Oct 2019 15:56:20 +0530 Subject: [PATCH 0703/3170] :newspaper: --- news/5702.bugfix | 1 + news/7155.bugfix | 1 + 2 files changed, 2 insertions(+) create mode 100644 news/5702.bugfix create mode 100644 news/7155.bugfix diff --git a/news/5702.bugfix b/news/5702.bugfix new file mode 100644 index 00000000000..2541d745ed7 --- /dev/null +++ b/news/5702.bugfix @@ -0,0 +1 @@ +Correctly handle system site-packages, in virtual environments created with venv (PEP 405). diff --git a/news/7155.bugfix b/news/7155.bugfix new file mode 100644 index 00000000000..2541d745ed7 --- /dev/null +++ b/news/7155.bugfix @@ -0,0 +1 @@ +Correctly handle system site-packages, in virtual environments created with venv (PEP 405). From fe9ae3ba7552e230431cc39a34fa5e5c1e042b25 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 7 Oct 2019 16:00:59 +0530 Subject: [PATCH 0704/3170] Did someone ask why I dislike Python 2? --- src/pip/_internal/utils/virtualenv.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/utils/virtualenv.py b/src/pip/_internal/utils/virtualenv.py index 1be69a96d72..8a195123bdc 100644 --- a/src/pip/_internal/utils/virtualenv.py +++ b/src/pip/_internal/utils/virtualenv.py @@ -1,3 +1,5 @@ +from __future__ import absolute_import + import logging import os import site @@ -47,7 +49,7 @@ def _get_pyvenv_cfg_lines(): try: with open(pyvenv_cfg_file) as f: return f.read().splitlines() # avoids trailing newlines - except OSError: + except IOError: return None From 5332ec57c6673609c2758e268c88df6a6454c635 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 8 Oct 2019 12:59:08 +0530 Subject: [PATCH 0705/3170] Apply suggestions from code review --- src/pip/_internal/utils/virtualenv.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/utils/virtualenv.py b/src/pip/_internal/utils/virtualenv.py index 8a195123bdc..ffc401ad104 100644 --- a/src/pip/_internal/utils/virtualenv.py +++ b/src/pip/_internal/utils/virtualenv.py @@ -8,7 +8,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Optional, List + from typing import List, Optional logger = logging.getLogger(__name__) @@ -34,7 +34,7 @@ def _running_under_regular_virtualenv(): def running_under_virtualenv(): # type: () -> bool - """Return a boolean, whether running under a virtual environment. + """Return True if we're running inside a virtualenv, False otherwise. """ return _running_under_venv() or _running_under_regular_virtualenv() From 57d34e0d9b41d14ae91942b97ac8d1eb1cd6d162 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 5 Nov 2019 14:54:52 +0530 Subject: [PATCH 0706/3170] Allow whitespace in pyvenv.cfg --- src/pip/_internal/utils/virtualenv.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/utils/virtualenv.py b/src/pip/_internal/utils/virtualenv.py index ffc401ad104..d81e6ac54bb 100644 --- a/src/pip/_internal/utils/virtualenv.py +++ b/src/pip/_internal/utils/virtualenv.py @@ -2,6 +2,7 @@ import logging import os +import re import site import sys @@ -11,6 +12,9 @@ from typing import List, Optional logger = logging.getLogger(__name__) +_INCLUDE_SYSTEM_SITE_PACKAGES_REGEX = re.compile( + r"include-system-site-packages\s*=\s*(?P<value>true|false)" +) def _running_under_venv(): @@ -76,7 +80,11 @@ def _no_global_under_venv(): ) return True - return "include-system-site-packages = false" in cfg_lines + for line in cfg_lines: + match = _INCLUDE_SYSTEM_SITE_PACKAGES_REGEX.match(line) + if match is not None and match.group('value') == 'false': + return True + return False def _no_global_under_regular_virtualenv(): From 8981895b5e34de1be2a73e5fff77879c45908700 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 5 Nov 2019 21:05:08 +0530 Subject: [PATCH 0707/3170] Skip all failing tests when using venv Our isolation logic for venv isn't correct and that is causing these tests to fail. The culprits for this are: tests/lib/venv.py::VirtualEnvironment.user_site_packages tests/lib/venv.py::VirtualEnvironment.sitecustomize Both these together are supposed to create an environment to isolate the tests. However, they were written for virtualenv and make assumptions that are not true for environments created with venv. Until we can fix VirtualEnvironment to properly isolate the test from the underlying test environment when using venv, these tests will continue to fail. This is blocking an important bugfix for users facing issues with since pip is installing packages into `--user` when run in a venv, even when `--user` isn't visible from that environment. As a temporary band-aid for this problem, I'm skipping these tests to unblock us from shipping the bugfix for the aforementioned issue. The test isolation logic should be fixed to work for venv. Once such a fix is made, this commit should be reverted. --- tests/functional/test_freeze.py | 2 ++ tests/functional/test_install.py | 2 ++ tests/functional/test_install_reqs.py | 1 + tests/functional/test_install_user.py | 9 ++++++++- tests/functional/test_install_wheel.py | 1 + tests/functional/test_list.py | 3 +++ tests/functional/test_uninstall_user.py | 1 + tests/unit/test_build_env.py | 1 + 8 files changed, 19 insertions(+), 1 deletion(-) diff --git a/tests/functional/test_freeze.py b/tests/functional/test_freeze.py index a4ff73d5c61..d13c931d0ef 100644 --- a/tests/functional/test_freeze.py +++ b/tests/functional/test_freeze.py @@ -703,6 +703,7 @@ def test_freeze_with_requirement_option_package_repeated_multi_file(script): @pytest.mark.network +@pytest.mark.incompatible_with_test_venv def test_freeze_user(script, virtualenv, data): """ Testing freeze with --user, first we have to install some stuff. @@ -733,6 +734,7 @@ def test_freeze_path(tmpdir, script, data): _check_output(result.stdout, expected) +@pytest.mark.incompatible_with_test_venv def test_freeze_path_exclude_user(tmpdir, script, data): """ Test freeze with --path and make sure packages from --user are not picked diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 3194b165384..364893eeca6 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -124,6 +124,7 @@ def test_pep518_allows_missing_requires(script, data, common_wheels): assert result.files_created +@pytest.mark.incompatible_with_test_venv def test_pep518_with_user_pip(script, pip_src, data, common_wheels): """ Check that build dependencies are installed into the build @@ -1593,6 +1594,7 @@ def test_target_install_ignores_distutils_config_install_prefix(script): assert relative_script_base not in result.files_created +@pytest.mark.incompatible_with_test_venv def test_user_config_accepted(script): # user set in the config file is parsed as 0/1 instead of True/False. # Check that this doesn't cause a problem. diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index a653e0b2fb2..2022e1fee53 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -227,6 +227,7 @@ def test_install_local_with_subdirectory(script): result.assert_installed('version_subpkg.py', editable=False) +@pytest.mark.incompatible_with_test_venv def test_wheel_user_with_prefix_in_pydistutils_cfg( script, data, with_wheel): if os.name == 'posix': diff --git a/tests/functional/test_install_user.py b/tests/functional/test_install_user.py index 1ac644609e9..03725fc3229 100644 --- a/tests/functional/test_install_user.py +++ b/tests/functional/test_install_user.py @@ -27,6 +27,7 @@ def dist_in_site_packages(dist): class Tests_UserSite: @pytest.mark.network + @pytest.mark.incompatible_with_test_venv def test_reset_env_system_site_packages_usersite(self, script): """ Check user site works as expected. @@ -42,6 +43,7 @@ def test_reset_env_system_site_packages_usersite(self, script): @pytest.mark.network @need_svn + @pytest.mark.incompatible_with_test_venv def test_install_subversion_usersite_editable_with_distribute( self, script, tmpdir): """ @@ -55,6 +57,7 @@ def test_install_subversion_usersite_editable_with_distribute( ) result.assert_installed('INITools', use_user_site=True) + @pytest.mark.incompatible_with_test_venv def test_install_from_current_directory_into_usersite( self, script, data, with_wheel): """ @@ -75,7 +78,6 @@ def test_install_from_current_directory_into_usersite( ) assert dist_info_folder in result.files_created - @pytest.mark.incompatible_with_test_venv def test_install_user_venv_nositepkgs_fails(self, virtualenv, script, data): """ @@ -96,6 +98,7 @@ def test_install_user_venv_nositepkgs_fails(self, virtualenv, ) @pytest.mark.network + @pytest.mark.incompatible_with_test_venv def test_install_user_conflict_in_usersite(self, script): """ Test user install with conflict in usersite updates usersite. @@ -119,6 +122,7 @@ def test_install_user_conflict_in_usersite(self, script): assert not isfile(initools_v3_file), initools_v3_file @pytest.mark.network + @pytest.mark.incompatible_with_test_venv def test_install_user_conflict_in_globalsite(self, virtualenv, script): """ Test user install with conflict in global site ignores site and @@ -149,6 +153,7 @@ def test_install_user_conflict_in_globalsite(self, virtualenv, script): assert isdir(initools_folder) @pytest.mark.network + @pytest.mark.incompatible_with_test_venv def test_upgrade_user_conflict_in_globalsite(self, virtualenv, script): """ Test user install/upgrade with conflict in global site ignores site and @@ -178,6 +183,7 @@ def test_upgrade_user_conflict_in_globalsite(self, virtualenv, script): assert isdir(initools_folder) @pytest.mark.network + @pytest.mark.incompatible_with_test_venv def test_install_user_conflict_in_globalsite_and_usersite( self, virtualenv, script): """ @@ -214,6 +220,7 @@ def test_install_user_conflict_in_globalsite_and_usersite( assert isdir(initools_folder) @pytest.mark.network + @pytest.mark.incompatible_with_test_venv def test_install_user_in_global_virtualenv_with_conflict_fails( self, script): """ diff --git a/tests/functional/test_install_wheel.py b/tests/functional/test_install_wheel.py index 2dfec01eedb..e218d6e7f17 100644 --- a/tests/functional/test_install_wheel.py +++ b/tests/functional/test_install_wheel.py @@ -233,6 +233,7 @@ def test_wheel_record_lines_in_deterministic_order(script, data): assert record_lines == sorted(record_lines) +@pytest.mark.incompatible_with_test_venv def test_install_user_wheel(script, data, with_wheel): """ Test user install from wheel (that has a script) diff --git a/tests/functional/test_list.py b/tests/functional/test_list.py index a863c42c91a..53f4152c2b7 100644 --- a/tests/functional/test_list.py +++ b/tests/functional/test_list.py @@ -94,6 +94,7 @@ def test_local_columns_flag(simple_script): @pytest.mark.network +@pytest.mark.incompatible_with_test_venv def test_user_flag(script, data): """ Test the behavior of --user flag in the list command @@ -110,6 +111,7 @@ def test_user_flag(script, data): @pytest.mark.network +@pytest.mark.incompatible_with_test_venv def test_user_columns_flag(script, data): """ Test the behavior of --user --format=columns flags in the list command @@ -502,6 +504,7 @@ def test_list_path(tmpdir, script, data): assert {'name': 'simple', 'version': '2.0'} in json_result +@pytest.mark.incompatible_with_test_venv def test_list_path_exclude_user(tmpdir, script, data): """ Test list with --path and make sure packages from --user are not picked diff --git a/tests/functional/test_uninstall_user.py b/tests/functional/test_uninstall_user.py index f99f3f21c7d..58079a293a8 100644 --- a/tests/functional/test_uninstall_user.py +++ b/tests/functional/test_uninstall_user.py @@ -9,6 +9,7 @@ from tests.lib import assert_all_changes, pyversion +@pytest.mark.incompatible_with_test_venv class Tests_UninstallUserSite: @pytest.mark.network diff --git a/tests/unit/test_build_env.py b/tests/unit/test_build_env.py index 9db08a124dd..ff3b2e90cef 100644 --- a/tests/unit/test_build_env.py +++ b/tests/unit/test_build_env.py @@ -162,6 +162,7 @@ def test_build_env_overlay_prefix_has_priority(script): assert result.stdout.strip() == '2.0', str(result) +@pytest.mark.incompatible_with_test_venv def test_build_env_isolation(script): # Create dummy `pkg` wheel. From a790c92d2e2a2bbf310be77c31c7e98693e125c6 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Mon, 4 Nov 2019 20:08:30 -0500 Subject: [PATCH 0708/3170] Type-safer distutils install command handling --- src/pip/_internal/locations.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/locations.py b/src/pip/_internal/locations.py index 1ce7910dfe9..6b35071c2cf 100644 --- a/src/pip/_internal/locations.py +++ b/src/pip/_internal/locations.py @@ -14,14 +14,17 @@ import sysconfig from distutils import sysconfig as distutils_sysconfig from distutils.command.install import SCHEME_KEYS # type: ignore +from distutils.command.install import install as distutils_install_command from pip._internal.utils import appdirs from pip._internal.utils.compat import WINDOWS -from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from pip._internal.utils.typing import MYPY_CHECK_RUNNING, cast from pip._internal.utils.virtualenv import running_under_virtualenv if MYPY_CHECK_RUNNING: - from typing import Any, Union, Dict, List, Optional + from typing import Dict, List, Optional, Union + + from distutils.cmd import Command as DistutilsCommand # Application Directories @@ -102,9 +105,10 @@ def distutils_scheme(dist_name, user=False, home=None, root=None, d = Distribution(dist_args) d.parse_config_files() - # NOTE: Ignoring type since mypy can't find attributes on 'Command' - i = d.get_command_obj('install', create=True) # type: Any - assert i is not None + obj = None # type: Optional[DistutilsCommand] + obj = d.get_command_obj('install', create=True) + assert obj is not None + i = cast(distutils_install_command, obj) # NOTE: setting user or home has the side-effect of creating the home dir # or user base for installations during finalize_options() # ideally, we'd prefer a scheme class that has no side-effects. From d709966c72920be6f28ef01544bd8ae1a6fab17d Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Mon, 4 Nov 2019 20:10:53 -0500 Subject: [PATCH 0709/3170] Normalize style in pip._internal.locations --- src/pip/_internal/locations.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/locations.py b/src/pip/_internal/locations.py index 6b35071c2cf..5dd10e8141d 100644 --- a/src/pip/_internal/locations.py +++ b/src/pip/_internal/locations.py @@ -91,8 +91,9 @@ def get_src_prefix(): bin_py = '/usr/local/bin' -def distutils_scheme(dist_name, user=False, home=None, root=None, - isolated=False, prefix=None): +def distutils_scheme( + dist_name, user=False, home=None, root=None, isolated=False, prefix=None +): # type:(str, bool, str, str, bool, str) -> dict """ Return a distutils install scheme From 2520ba716a965813d5790669efbf0dc65970706b Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 5 Nov 2019 19:36:53 -0500 Subject: [PATCH 0710/3170] Remove unused noarch flag in pep425tags.get_supported --- src/pip/_internal/pep425tags.py | 112 ++++++++++++++++---------------- 1 file changed, 55 insertions(+), 57 deletions(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index 042ba34b38f..c9425cdf823 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -327,7 +327,6 @@ def get_all_minor_versions_as_strings(version_info): def get_supported( versions=None, # type: Optional[List[str]] - noarch=False, # type: bool platform=None, # type: Optional[str] impl=None, # type: Optional[str] abi=None # type: Optional[str] @@ -369,67 +368,66 @@ def get_supported( abis.append('none') - if not noarch: - arch = platform or get_platform() - arch_prefix, arch_sep, arch_suffix = arch.partition('_') - if arch.startswith('macosx'): - # support macosx-10.6-intel on macosx-10.9-x86_64 - match = _osx_arch_pat.match(arch) - if match: - name, major, minor, actual_arch = match.groups() - tpl = '{}_{}_%i_%s'.format(name, major) - arches = [] - for m in reversed(range(int(minor) + 1)): - for a in get_darwin_arches(int(major), m, actual_arch): - arches.append(tpl % (m, a)) - else: - # arch pattern didn't match (?!) - arches = [arch] - elif arch_prefix == 'manylinux2014': - arches = [arch] - # manylinux1/manylinux2010 wheels run on most manylinux2014 systems - # with the exception of wheels depending on ncurses. PEP 599 states - # manylinux1/manylinux2010 wheels should be considered - # manylinux2014 wheels: - # https://www.python.org/dev/peps/pep-0599/#backwards-compatibility-with-manylinux2010-wheels - if arch_suffix in {'i686', 'x86_64'}: - arches.append('manylinux2010' + arch_sep + arch_suffix) - arches.append('manylinux1' + arch_sep + arch_suffix) - elif arch_prefix == 'manylinux2010': - # manylinux1 wheels run on most manylinux2010 systems with the - # exception of wheels depending on ncurses. PEP 571 states - # manylinux1 wheels should be considered manylinux2010 wheels: - # https://www.python.org/dev/peps/pep-0571/#backwards-compatibility-with-manylinux1-wheels - arches = [arch, 'manylinux1' + arch_sep + arch_suffix] - elif platform is None: + arch = platform or get_platform() + arch_prefix, arch_sep, arch_suffix = arch.partition('_') + if arch.startswith('macosx'): + # support macosx-10.6-intel on macosx-10.9-x86_64 + match = _osx_arch_pat.match(arch) + if match: + name, major, minor, actual_arch = match.groups() + tpl = '{}_{}_%i_%s'.format(name, major) arches = [] - if is_manylinux2014_compatible(): - arches.append('manylinux2014' + arch_sep + arch_suffix) - if is_manylinux2010_compatible(): - arches.append('manylinux2010' + arch_sep + arch_suffix) - if is_manylinux1_compatible(): - arches.append('manylinux1' + arch_sep + arch_suffix) - arches.append(arch) + for m in reversed(range(int(minor) + 1)): + for a in get_darwin_arches(int(major), m, actual_arch): + arches.append(tpl % (m, a)) else: + # arch pattern didn't match (?!) arches = [arch] + elif arch_prefix == 'manylinux2014': + arches = [arch] + # manylinux1/manylinux2010 wheels run on most manylinux2014 systems + # with the exception of wheels depending on ncurses. PEP 599 states + # manylinux1/manylinux2010 wheels should be considered + # manylinux2014 wheels: + # https://www.python.org/dev/peps/pep-0599/#backwards-compatibility-with-manylinux2010-wheels + if arch_suffix in {'i686', 'x86_64'}: + arches.append('manylinux2010' + arch_sep + arch_suffix) + arches.append('manylinux1' + arch_sep + arch_suffix) + elif arch_prefix == 'manylinux2010': + # manylinux1 wheels run on most manylinux2010 systems with the + # exception of wheels depending on ncurses. PEP 571 states + # manylinux1 wheels should be considered manylinux2010 wheels: + # https://www.python.org/dev/peps/pep-0571/#backwards-compatibility-with-manylinux1-wheels + arches = [arch, 'manylinux1' + arch_sep + arch_suffix] + elif platform is None: + arches = [] + if is_manylinux2014_compatible(): + arches.append('manylinux2014' + arch_sep + arch_suffix) + if is_manylinux2010_compatible(): + arches.append('manylinux2010' + arch_sep + arch_suffix) + if is_manylinux1_compatible(): + arches.append('manylinux1' + arch_sep + arch_suffix) + arches.append(arch) + else: + arches = [arch] - # Current version, current API (built specifically for our Python): - for abi in abis: - for arch in arches: - supported.append(('%s%s' % (impl, versions[0]), abi, arch)) - - # abi3 modules compatible with older version of Python - for version in versions[1:]: - # abi3 was introduced in Python 3.2 - if version in {'31', '30'}: - break - for abi in abi3s: # empty set if not Python 3 - for arch in arches: - supported.append(("%s%s" % (impl, version), abi, arch)) - - # Has binaries, does not use the Python API: + # Current version, current API (built specifically for our Python): + for abi in abis: for arch in arches: - supported.append(('py%s' % (versions[0][0]), 'none', arch)) + supported.append(('%s%s' % (impl, versions[0]), abi, arch)) + + # abi3 modules compatible with older version of Python + for version in versions[1:]: + # abi3 was introduced in Python 3.2 + if version in {'31', '30'}: + break + for abi in abi3s: # empty set if not Python 3 + for arch in arches: + supported.append(("%s%s" % (impl, version), abi, arch)) + + # Has binaries, does not use the Python API: + for arch in arches: + supported.append(('py%s' % (versions[0][0]), 'none', arch)) # No abi / arch, but requires our implementation: supported.append(('%s%s' % (impl, versions[0]), 'none', 'any')) From f305f66eb2b4f356482754ccd30885c88041d934 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Mon, 4 Nov 2019 20:45:37 -0500 Subject: [PATCH 0711/3170] Trace common finder info outside resolver There's no reason for Resolver to trace this information about our PackageFinder, we just need a common one-time function to trace useful information. --- src/pip/_internal/cli/req_command.py | 15 +++++++++++++++ src/pip/_internal/commands/download.py | 3 +++ src/pip/_internal/commands/install.py | 3 +++ src/pip/_internal/commands/wheel.py | 3 +++ src/pip/_internal/legacy_resolve.py | 6 ------ 5 files changed, 24 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 5240dd86614..573bd76701d 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -8,6 +8,7 @@ # The following comment should be removed at some point in the future. # mypy: disallow-untyped-defs=False +import logging import os from functools import partial @@ -41,6 +42,8 @@ from pip._internal.req.req_tracker import RequirementTracker from pip._internal.utils.temp_dir import TempDirectory +logger = logging.getLogger(__name__) + class SessionCommandMixin(CommandContextMixIn): @@ -272,6 +275,18 @@ def populate_requirement_set( 'You must give at least one requirement to %(name)s ' '(see "pip help %(name)s")' % opts) + @staticmethod + def trace_basic_info(finder): + # type: (PackageFinder) -> None + """ + Trace basic information about the provided objects. + """ + # Display where finder is looking for packages + search_scope = finder.search_scope + locations = search_scope.get_formatted_locations() + if locations: + logger.info(locations) + def _build_package_finder( self, options, # type: Values diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index b952bb22515..cfbcbbe3525 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -139,6 +139,9 @@ def run(self, options, args): options=options, py_version_info=options.python_version, ) + + self.trace_basic_info(finder) + resolver.resolve(requirement_set) downloaded = ' '.join([ diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 1d9c7d87cdd..e7a5d826487 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -373,6 +373,9 @@ def run(self, options, args): upgrade_strategy=upgrade_strategy, use_pep517=options.use_pep517, ) + + self.trace_basic_info(finder) + resolver.resolve(requirement_set) try: diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index e1d59ee4c2b..f8bf284a624 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -185,6 +185,9 @@ def run(self, options, args): ignore_requires_python=options.ignore_requires_python, use_pep517=options.use_pep517, ) + + self.trace_basic_info(finder) + resolver.resolve(requirement_set) # build wheels diff --git a/src/pip/_internal/legacy_resolve.py b/src/pip/_internal/legacy_resolve.py index 43c3b80c641..af1d2ef68e1 100644 --- a/src/pip/_internal/legacy_resolve.py +++ b/src/pip/_internal/legacy_resolve.py @@ -181,12 +181,6 @@ def resolve(self, requirement_set): any(req.has_hash_options for req in root_reqs) ) - # Display where finder is looking for packages - search_scope = self.finder.search_scope - locations = search_scope.get_formatted_locations() - if locations: - logger.info(locations) - # Actually prepare the files, and collect any exceptions. Most hash # exceptions cannot be checked ahead of time, because # req.populate_link() needs to be called before we can make decisions From deac2343dc42accb207de13bcf1ad9e5ca0c35b8 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Mon, 4 Nov 2019 21:50:19 -0500 Subject: [PATCH 0712/3170] Don't pass PackageFinder to Preparer from Resolver Preparer's overall responsibilities align more with having its own reference to finder, which will help us remove it from resolver later. --- src/pip/_internal/cli/req_command.py | 2 ++ src/pip/_internal/commands/download.py | 1 + src/pip/_internal/commands/install.py | 1 + src/pip/_internal/commands/wheel.py | 1 + src/pip/_internal/legacy_resolve.py | 4 ++-- src/pip/_internal/operations/prepare.py | 8 ++++---- tests/unit/test_req.py | 1 + 7 files changed, 12 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 573bd76701d..38a394b7032 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -153,6 +153,7 @@ def make_requirement_preparer( options, # type: Values req_tracker, # type: RequirementTracker session, # type: PipSession + finder, # type: PackageFinder download_dir=None, # type: str wheel_download_dir=None, # type: str ): @@ -171,6 +172,7 @@ def make_requirement_preparer( build_isolation=options.build_isolation, req_tracker=req_tracker, session=session, + finder=finder, ) @staticmethod diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index cfbcbbe3525..8cffc6a7f5e 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -130,6 +130,7 @@ def run(self, options, args): options=options, req_tracker=req_tracker, session=session, + finder=finder, download_dir=options.download_dir, ) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index e7a5d826487..67d7204f476 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -360,6 +360,7 @@ def run(self, options, args): options=options, req_tracker=req_tracker, session=session, + finder=finder, ) resolver = self.make_resolver( preparer=preparer, diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index f8bf284a624..e4d627a6a7f 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -174,6 +174,7 @@ def run(self, options, args): options=options, req_tracker=req_tracker, session=session, + finder=finder, wheel_download_dir=options.wheel_dir, ) diff --git a/src/pip/_internal/legacy_resolve.py b/src/pip/_internal/legacy_resolve.py index af1d2ef68e1..9acfa8bb9dd 100644 --- a/src/pip/_internal/legacy_resolve.py +++ b/src/pip/_internal/legacy_resolve.py @@ -280,7 +280,7 @@ def _get_abstract_dist_for(self, req, require_hashes): """ if req.editable: return self.preparer.prepare_editable_requirement( - req, require_hashes, self.use_user_site, self.finder, + req, require_hashes, self.use_user_site, ) # satisfied_by is only evaluated by calling _check_skip_installed, @@ -298,7 +298,7 @@ def _get_abstract_dist_for(self, req, require_hashes): # We eagerly populate the link, since that's our "legacy" behavior. req.populate_link(self.finder, upgrade_allowed, require_hashes) abstract_dist = self.preparer.prepare_linked_requirement( - req, self.finder, require_hashes + req, require_hashes ) # NOTE diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index d8359b2f0f3..b145e65fa6f 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -509,6 +509,7 @@ def __init__( build_isolation, # type: bool req_tracker, # type: RequirementTracker session, # type: PipSession + finder, # type: PackageFinder ): # type: (...) -> None super(RequirementPreparer, self).__init__() @@ -517,6 +518,7 @@ def __init__( self.build_dir = build_dir self.req_tracker = req_tracker self.session = session + self.finder = finder # Where still-packed archives should be written to. If None, they are # not saved, and are deleted immediately after unpacking. @@ -558,7 +560,6 @@ def _download_should_save(self): def prepare_linked_requirement( self, req, # type: InstallRequirement - finder, # type: PackageFinder require_hashes, # type: bool ): # type: (...) -> AbstractDistribution @@ -666,7 +667,7 @@ def prepare_linked_requirement( write_delete_marker_file(req.source_dir) abstract_dist = _get_prepared_distribution( - req, self.req_tracker, finder, self.build_isolation, + req, self.req_tracker, self.finder, self.build_isolation, ) if self._download_should_save: @@ -680,7 +681,6 @@ def prepare_editable_requirement( req, # type: InstallRequirement require_hashes, # type: bool use_user_site, # type: bool - finder # type: PackageFinder ): # type: (...) -> AbstractDistribution """Prepare an editable requirement @@ -700,7 +700,7 @@ def prepare_editable_requirement( req.update_editable(not self._download_should_save) abstract_dist = _get_prepared_distribution( - req, self.req_tracker, finder, self.build_isolation, + req, self.req_tracker, self.finder, self.build_isolation, ) if self._download_should_save: diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index fcee96509aa..0ebd80527fa 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -76,6 +76,7 @@ def _basic_resolver(self, finder, require_hashes=False): build_isolation=True, req_tracker=RequirementTracker(), session=PipSession(), + finder=finder, ) make_install_req = partial( install_req_from_req_string, From b8de2154db804b8dbabc9f9106c28f2af422e1b4 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 7 Nov 2019 21:59:53 +0530 Subject: [PATCH 0713/3170] Bump to the correct development version --- src/pip/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/__init__.py b/src/pip/__init__.py index c0496d674b6..b3b1ff0b7d1 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -1 +1 @@ -__version__ = "19.4.dev0" +__version__ = "20.0.dev0" From 16684fbc28b17eff558e20813e06dc546780eea6 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Mon, 4 Nov 2019 17:41:32 -0500 Subject: [PATCH 0714/3170] Create Scheme model and wrap distutils_scheme Now we have a nicely-typed interface to the calculated scheme, and some more documentation for the same. --- src/pip/_internal/locations.py | 39 ++++++++++++++++++++++++++++++ src/pip/_internal/models/scheme.py | 25 +++++++++++++++++++ 2 files changed, 64 insertions(+) create mode 100644 src/pip/_internal/models/scheme.py diff --git a/src/pip/_internal/locations.py b/src/pip/_internal/locations.py index 5dd10e8141d..b54b46bbe8a 100644 --- a/src/pip/_internal/locations.py +++ b/src/pip/_internal/locations.py @@ -16,6 +16,7 @@ from distutils.command.install import SCHEME_KEYS # type: ignore from distutils.command.install import install as distutils_install_command +from pip._internal.models.scheme import Scheme from pip._internal.utils import appdirs from pip._internal.utils.compat import WINDOWS from pip._internal.utils.typing import MYPY_CHECK_RUNNING, cast @@ -153,3 +154,41 @@ def distutils_scheme( ) return scheme + + +def get_scheme( + dist_name, # type: str + user=False, # type: bool + home=None, # type: Optional[str] + root=None, # type: Optional[str] + isolated=False, # type: bool + prefix=None, # type: Optional[str] +): + # type: (...) -> Scheme + """ + Get the "scheme" corresponding to the input parameters. The distutils + documentation provides the context for the available schemes: + https://docs.python.org/3/install/index.html#alternate-installation + + :param dist_name: the name of the package to retrieve the scheme for, used + in the headers scheme path + :param user: indicates to use the "user" scheme + :param home: indicates to use the "home" scheme and provides the base + directory for the same + :param root: root under which other directories are re-based + :param isolated: equivalent to --no-user-cfg, i.e. do not consider + ~/.pydistutils.cfg (posix) or ~/pydistutils.cfg (non-posix) for + scheme paths + :param prefix: indicates to use the "prefix" scheme and provides the + base directory for the same + """ + scheme = distutils_scheme( + dist_name, user, home, root, isolated, prefix + ) + return Scheme( + platlib=scheme["platlib"], + purelib=scheme["purelib"], + headers=scheme["headers"], + scripts=scheme["scripts"], + data=scheme["data"], + ) diff --git a/src/pip/_internal/models/scheme.py b/src/pip/_internal/models/scheme.py new file mode 100644 index 00000000000..af07b4078f9 --- /dev/null +++ b/src/pip/_internal/models/scheme.py @@ -0,0 +1,25 @@ +""" +For types associated with installation schemes. + +For a general overview of available schemes and their context, see +https://docs.python.org/3/install/index.html#alternate-installation. +""" + + +class Scheme(object): + """A Scheme holds paths which are used as the base directories for + artifacts associated with a Python package. + """ + def __init__( + self, + platlib, # type: str + purelib, # type: str + headers, # type: str + scripts, # type: str + data, # type: str + ): + self.platlib = platlib + self.purelib = purelib + self.headers = headers + self.scripts = scripts + self.data = data From 9de8a6d598b100071e212c7d8b40643223cf05ec Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 6 Nov 2019 20:54:47 -0500 Subject: [PATCH 0715/3170] Create scheme unconditionally on install --- src/pip/_internal/req/req_install.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 8033c69a4b6..fb378d28f72 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -852,6 +852,15 @@ def install( pycompile=True # type: bool ): # type: (...) -> None + scheme = distutils_scheme( + self.name, + user=use_user_site, + home=home, + root=root, + isolated=self.isolated, + prefix=prefix, + ) + global_options = global_options if global_options is not None else [] if self.editable: self.install_editable( @@ -862,14 +871,11 @@ def install( use_user_site=use_user_site, ) return + if self.is_wheel: version = wheel.wheel_version(self.source_dir) wheel.check_compatibility(version, self.name) - scheme = distutils_scheme( - self.name, user=use_user_site, home=home, root=root, - isolated=self.isolated, prefix=prefix, - ) self.move_wheel_files( self.source_dir, scheme=scheme, From e03a71aff8c1b6dfaaacdd4abc14386a43f6b52a Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 6 Nov 2019 21:10:11 -0500 Subject: [PATCH 0716/3170] Use new Scheme model for wheel installation --- src/pip/_internal/req/req_install.py | 9 +++++---- src/pip/_internal/wheel.py | 14 ++++++++------ tests/unit/test_wheel.py | 25 ++++++++++++++----------- 3 files changed, 27 insertions(+), 21 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index fb378d28f72..5a86532ccc0 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -22,7 +22,7 @@ from pip._internal import pep425tags, wheel from pip._internal.build_env import NoOpBuildEnvironment from pip._internal.exceptions import InstallationError -from pip._internal.locations import distutils_scheme +from pip._internal.locations import get_scheme from pip._internal.models.link import Link from pip._internal.operations.build.metadata import generate_metadata from pip._internal.operations.build.metadata_legacy import \ @@ -65,11 +65,12 @@ if MYPY_CHECK_RUNNING: from typing import ( - Any, Dict, Iterable, List, Mapping, Optional, Sequence, Union, + Any, Dict, Iterable, List, Optional, Sequence, Union, ) from pip._internal.build_env import BuildEnvironment from pip._internal.cache import WheelCache from pip._internal.index.package_finder import PackageFinder + from pip._internal.models.scheme import Scheme from pip._vendor.pkg_resources import Distribution from pip._vendor.packaging.specifiers import SpecifierSet from pip._vendor.packaging.markers import Marker @@ -540,7 +541,7 @@ def is_wheel(self): def move_wheel_files( self, wheeldir, # type: str - scheme, # type: Mapping[str, str] + scheme, # type: Scheme warn_script_location=True, # type: bool pycompile=True # type: bool ): @@ -852,7 +853,7 @@ def install( pycompile=True # type: bool ): # type: (...) -> None - scheme = distutils_scheme( + scheme = get_scheme( self.name, user=use_user_site, home=home, diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index 85a9e2b60e4..682a37e49b8 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -36,9 +36,11 @@ if MYPY_CHECK_RUNNING: from typing import ( - Dict, List, Optional, Sequence, Mapping, Tuple, IO, Text, Any, + Dict, List, Optional, Sequence, Tuple, IO, Text, Any, Iterable, Callable, Set, ) + + from pip._internal.models.scheme import Scheme from pip._internal.pep425tags import Pep425Tag InstalledCSVRow = Tuple[str, ...] @@ -289,7 +291,7 @@ def make(self, specification, options=None): def install_unpacked_wheel( name, # type: str wheeldir, # type: str - scheme, # type: Mapping[str, str] + scheme, # type: Scheme req_description, # type: str pycompile=True, # type: bool warn_script_location=True # type: bool @@ -311,9 +313,9 @@ def install_unpacked_wheel( # installation. if root_is_purelib(name, wheeldir): - lib_dir = scheme['purelib'] + lib_dir = scheme.purelib else: - lib_dir = scheme['platlib'] + lib_dir = scheme.platlib info_dir = [] # type: List[str] data_dirs = [] @@ -458,10 +460,10 @@ def is_entrypoint_wrapper(name): fixer = fix_script filter = is_entrypoint_wrapper source = os.path.join(wheeldir, datadir, subdir) - dest = scheme[subdir] + dest = getattr(scheme, subdir) clobber(source, dest, False, fixer=fixer, filter=filter) - maker = PipScriptMaker(None, scheme['scripts']) + maker = PipScriptMaker(None, scheme.scripts) # Ensure old scripts are overwritten. # See https://github.com/pypa/pip/issues/1800 diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 3153f2a822a..ea145d72274 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -11,8 +11,9 @@ from pip._internal import pep425tags, wheel from pip._internal.commands.wheel import WheelCommand from pip._internal.exceptions import InvalidWheelFilename, UnsupportedWheel -from pip._internal.locations import distutils_scheme +from pip._internal.locations import get_scheme from pip._internal.models.link import Link +from pip._internal.models.scheme import Scheme from pip._internal.req.req_install import InstallRequirement from pip._internal.utils.compat import WINDOWS from pip._internal.utils.misc import hash_file @@ -482,29 +483,31 @@ def prep(self, data, tmpdir): self.src = os.path.join(tmpdir, 'src') self.dest = os.path.join(tmpdir, 'dest') unpack_file(self.wheelpath, self.src) - self.scheme = { - 'scripts': os.path.join(self.dest, 'bin'), - 'purelib': os.path.join(self.dest, 'lib'), - 'data': os.path.join(self.dest, 'data'), - } + self.scheme = Scheme( + purelib=os.path.join(self.dest, 'lib'), + platlib=os.path.join(self.dest, 'lib'), + headers=os.path.join(self.dest, 'headers'), + scripts=os.path.join(self.dest, 'bin'), + data=os.path.join(self.dest, 'data'), + ) self.src_dist_info = os.path.join( self.src, 'sample-1.2.0.dist-info') self.dest_dist_info = os.path.join( - self.scheme['purelib'], 'sample-1.2.0.dist-info') + self.scheme.purelib, 'sample-1.2.0.dist-info') def assert_installed(self): # lib assert os.path.isdir( - os.path.join(self.scheme['purelib'], 'sample')) + os.path.join(self.scheme.purelib, 'sample')) # dist-info metadata = os.path.join(self.dest_dist_info, 'METADATA') assert os.path.isfile(metadata) # data files - data_file = os.path.join(self.scheme['data'], 'my_data', 'data_file') + data_file = os.path.join(self.scheme.data, 'my_data', 'data_file') assert os.path.isfile(data_file) # package data pkg_data = os.path.join( - self.scheme['purelib'], 'sample', 'package_data.dat') + self.scheme.purelib, 'sample', 'package_data.dat') assert os.path.isfile(pkg_data) def test_std_install(self, data, tmpdir): @@ -520,7 +523,7 @@ def test_std_install(self, data, tmpdir): def test_install_prefix(self, data, tmpdir): prefix = os.path.join(os.path.sep, 'some', 'path') self.prep(data, tmpdir) - scheme = distutils_scheme( + scheme = get_scheme( self.name, user=False, home=None, From 6fa64a6b285ae8e5daaccbf346fb922bd023a494 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 6 Nov 2019 21:10:19 -0500 Subject: [PATCH 0717/3170] Calculate header directories in one place This was already happening in locations.distutils_scheme, we're just reusing the existing work instead of doing it again. --- src/pip/_internal/req/req_install.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 5a86532ccc0..674545c9f53 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -8,7 +8,6 @@ import os import shutil import sys -import sysconfig import zipfile from distutils.util import change_root @@ -896,12 +895,7 @@ def install( install_options = list(install_options) + \ self.options.get('install_options', []) - header_dir = None # type: Optional[str] - if running_under_virtualenv(): - py_ver_str = 'python' + sysconfig.get_python_version() - header_dir = os.path.join( - sys.prefix, 'include', 'site', py_ver_str, self.name - ) + header_dir = scheme.headers with TempDirectory(kind="record") as temp_dir: record_filename = os.path.join(temp_dir.path, 'install-record.txt') From 89f6e4fcd581ecb38c6194a372c6acdf39081c8f Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 5 Nov 2019 21:17:19 -0500 Subject: [PATCH 0718/3170] Deprecate setup.py-based builds that do not make .egg-info --- news/6998.removal | 1 + src/pip/_internal/req/req_install.py | 20 ++++++++++++++++---- 2 files changed, 17 insertions(+), 4 deletions(-) create mode 100644 news/6998.removal diff --git a/news/6998.removal b/news/6998.removal new file mode 100644 index 00000000000..7c38a48fd11 --- /dev/null +++ b/news/6998.removal @@ -0,0 +1 @@ +Deprecate setup.py-based builds that do not generate an ``.egg-info`` directory. diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index fdd85a1e243..1029b8cf76b 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -28,6 +28,7 @@ from pip._internal.pyproject import load_pyproject_toml, make_pyproject_path from pip._internal.req.req_uninstall import UninstallPathSet from pip._internal.utils.compat import native_str +from pip._internal.utils.deprecation import deprecated from pip._internal.utils.hashes import Hashes from pip._internal.utils.logging import indent_log from pip._internal.utils.marker_files import ( @@ -932,10 +933,21 @@ def prepend_root(path): egg_info_dir = prepend_root(directory) break else: - logger.warning( - 'Could not find .egg-info directory in install record' - ' for %s', - self, + deprecated( + reason=( + "{} did not indicate that it installed an " + ".egg-info directory. Only setup.py projects " + "generating .egg-info directories are supported." + ).format(self), + replacement=( + "for maintainers: updating the setup.py of {0}. " + "For users: contact the maintainers of {0} to let " + "them know to update their setup.py.".format( + self.name + ) + ), + gone_in="20.2", + issue=6998, ) # FIXME: put the record somewhere return From d56e488f78c9e85981c74fc45f9a3d1926113034 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 8 Nov 2019 21:19:57 -0500 Subject: [PATCH 0719/3170] Assert originally unnamed requirements are direct --- src/pip/_internal/legacy_resolve.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/legacy_resolve.py b/src/pip/_internal/legacy_resolve.py index 9acfa8bb9dd..ffefab2398c 100644 --- a/src/pip/_internal/legacy_resolve.py +++ b/src/pip/_internal/legacy_resolve.py @@ -388,7 +388,9 @@ def add_req(subreq, extras_requested): # can refer to it when adding dependencies. if not requirement_set.has_requirement(req_to_install.name): # 'unnamed' requirements will get added here - req_to_install.is_direct = True + # 'unnamed' requirements can only come from being directly + # provided by the user. + assert req_to_install.is_direct requirement_set.add_requirement( req_to_install, parent_req_name=None, ) From 0fff5bec932e2be1540288d2a9f93315c3bd40cc Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 7 Nov 2019 21:29:15 -0500 Subject: [PATCH 0720/3170] Separate calls to wheel builder functions --- src/pip/_internal/wheel_builder.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index 9b2db949c27..86886394668 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -250,10 +250,14 @@ def _build_one_inside_env( # type: (...) -> Optional[str] with TempDirectory(kind="wheel") as temp_dir: if req.use_pep517: - builder = self._build_one_pep517 + wheel_path = self._build_one_pep517( + req, temp_dir.path, python_tag=python_tag + ) else: - builder = self._build_one_legacy - wheel_path = builder(req, temp_dir.path, python_tag=python_tag) + wheel_path = self._build_one_legacy( + req, temp_dir.path, python_tag=python_tag + ) + if wheel_path is not None: wheel_name = os.path.basename(wheel_path) dest_path = os.path.join(output_dir, wheel_name) From 8963f6398987effe737fbab9d253aa23eaea9e86 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 7 Nov 2019 21:32:54 -0500 Subject: [PATCH 0721/3170] Parameterize requirement name for logging --- src/pip/_internal/wheel_builder.py | 8 +++---- tests/unit/test_wheel.py | 35 +----------------------------- 2 files changed, 5 insertions(+), 38 deletions(-) diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index 86886394668..b06e75323dd 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -163,7 +163,7 @@ def format_command_result( def get_legacy_build_wheel_path( names, # type: List[str] temp_dir, # type: str - req, # type: InstallRequirement + name, # type: str command_args, # type: List[str] command_output, # type: Text ): @@ -174,7 +174,7 @@ def get_legacy_build_wheel_path( if not names: msg = ( 'Legacy build of wheel for {!r} created no files.\n' - ).format(req.name) + ).format(name) msg += format_command_result(command_args, command_output) logger.warning(msg) return None @@ -183,7 +183,7 @@ def get_legacy_build_wheel_path( msg = ( 'Legacy build of wheel for {!r} created more than one file.\n' 'Filenames (choosing first): {}\n' - ).format(req.name, names) + ).format(name, names) msg += format_command_result(command_args, command_output) logger.warning(msg) @@ -359,7 +359,7 @@ def _build_one_legacy( wheel_path = get_legacy_build_wheel_path( names=names, temp_dir=tempd, - req=req, + name=req.name, command_args=wheel_args, command_output=output, ) diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index ea145d72274..c48ef4220e9 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -12,9 +12,7 @@ from pip._internal.commands.wheel import WheelCommand from pip._internal.exceptions import InvalidWheelFilename, UnsupportedWheel from pip._internal.locations import get_scheme -from pip._internal.models.link import Link from pip._internal.models.scheme import Scheme -from pip._internal.req.req_install import InstallRequirement from pip._internal.utils.compat import WINDOWS from pip._internal.utils.misc import hash_file from pip._internal.utils.unpacking import unpack_file @@ -26,36 +24,6 @@ from tests.lib import DATA_DIR, assert_paths_equal -def make_test_install_req(base_name=None): - """ - Return an InstallRequirement object for testing purposes. - """ - if base_name is None: - base_name = 'pendulum-2.0.4' - - req = Requirement('pendulum') - link_url = ( - 'https://files.pythonhosted.org/packages/aa/{base_name}.tar.gz' - '#sha256=cf535d36c063575d4752af36df928882b2e0e31541b4482c97d637527' - '85f9fcb' - ).format(base_name=base_name) - link = Link( - url=link_url, - comes_from='https://pypi.org/simple/pendulum/', - requires_python='>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*', - ) - req = InstallRequirement( - req=req, - comes_from=None, - constraint=False, - editable=False, - link=link, - source_dir='/tmp/pip-install-9py5m2z1/pendulum', - ) - - return req - - @pytest.mark.parametrize('file_tag, expected', [ (('py27', 'none', 'any'), 'py27-none-any'), (('cp33', 'cp32dmu', 'linux_x86_64'), 'cp33-cp32dmu-linux_x86_64'), @@ -66,11 +34,10 @@ def test_format_tag(file_tag, expected): def call_get_legacy_build_wheel_path(caplog, names): - req = make_test_install_req() wheel_path = get_legacy_build_wheel_path( names=names, temp_dir='/tmp/abcd', - req=req, + name='pendulum', command_args=['arg1', 'arg2'], command_output='output line 1\noutput line 2\n', ) From d8382456ba098548de252fa367a1591dec4b6e9e Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 7 Nov 2019 21:34:50 -0500 Subject: [PATCH 0722/3170] Don't pass req or use members in WheelBuilder._build_one_legacy --- src/pip/_internal/wheel_builder.py | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index b06e75323dd..b12ce3eadc6 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -255,7 +255,13 @@ def _build_one_inside_env( ) else: wheel_path = self._build_one_legacy( - req, temp_dir.path, python_tag=python_tag + name=req.name, + setup_py_path=req.setup_py_path, + source_dir=req.unpacked_source_directory, + global_options=self.global_options, + build_options=self.build_options, + tempd=temp_dir.path, + python_tag=python_tag, ) if wheel_path is not None: @@ -323,7 +329,11 @@ def _build_one_pep517( def _build_one_legacy( self, - req, # type: InstallRequirement + name, # type: str + setup_py_path, # type: str + source_dir, # type: str + global_options, # type: List[str] + build_options, # type: List[str] tempd, # type: str python_tag=None, # type: Optional[str] ): @@ -333,33 +343,33 @@ def _build_one_legacy( Returns path to wheel if successfully built. Otherwise, returns None. """ wheel_args = make_setuptools_bdist_wheel_args( - req.setup_py_path, - global_options=self.global_options, - build_options=self.build_options, + setup_py_path, + global_options=global_options, + build_options=build_options, destination_dir=tempd, python_tag=python_tag, ) - spin_message = 'Building wheel for %s (setup.py)' % (req.name,) + spin_message = 'Building wheel for %s (setup.py)' % (name,) with open_spinner(spin_message) as spinner: logger.debug('Destination directory: %s', tempd) try: output = call_subprocess( wheel_args, - cwd=req.unpacked_source_directory, + cwd=source_dir, spinner=spinner, ) except Exception: spinner.finish("error") - logger.error('Failed building wheel for %s', req.name) + logger.error('Failed building wheel for %s', name) return None names = os.listdir(tempd) wheel_path = get_legacy_build_wheel_path( names=names, temp_dir=tempd, - name=req.name, + name=name, command_args=wheel_args, command_output=output, ) From 88b3367b8c04b9b471c82fd4701ae7347c79111b Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 7 Nov 2019 21:38:38 -0500 Subject: [PATCH 0723/3170] Move legacy wheel build method to standalone function --- src/pip/_internal/wheel_builder.py | 98 +++++++++++++++--------------- 1 file changed, 49 insertions(+), 49 deletions(-) diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index b12ce3eadc6..68780cda237 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -190,6 +190,54 @@ def get_legacy_build_wheel_path( return os.path.join(temp_dir, names[0]) +def _build_wheel_legacy( + name, # type: str + setup_py_path, # type: str + source_dir, # type: str + global_options, # type: List[str] + build_options, # type: List[str] + tempd, # type: str + python_tag=None, # type: Optional[str] +): + # type: (...) -> Optional[str] + """Build one InstallRequirement using the "legacy" build process. + + Returns path to wheel if successfully built. Otherwise, returns None. + """ + wheel_args = make_setuptools_bdist_wheel_args( + setup_py_path, + global_options=global_options, + build_options=build_options, + destination_dir=tempd, + python_tag=python_tag, + ) + + spin_message = 'Building wheel for %s (setup.py)' % (name,) + with open_spinner(spin_message) as spinner: + logger.debug('Destination directory: %s', tempd) + + try: + output = call_subprocess( + wheel_args, + cwd=source_dir, + spinner=spinner, + ) + except Exception: + spinner.finish("error") + logger.error('Failed building wheel for %s', name) + return None + + names = os.listdir(tempd) + wheel_path = get_legacy_build_wheel_path( + names=names, + temp_dir=tempd, + name=name, + command_args=wheel_args, + command_output=output, + ) + return wheel_path + + def _always_true(_): # type: (Any) -> bool return True @@ -254,7 +302,7 @@ def _build_one_inside_env( req, temp_dir.path, python_tag=python_tag ) else: - wheel_path = self._build_one_legacy( + wheel_path = _build_wheel_legacy( name=req.name, setup_py_path=req.setup_py_path, source_dir=req.unpacked_source_directory, @@ -327,54 +375,6 @@ def _build_one_pep517( return None return os.path.join(tempd, wheel_name) - def _build_one_legacy( - self, - name, # type: str - setup_py_path, # type: str - source_dir, # type: str - global_options, # type: List[str] - build_options, # type: List[str] - tempd, # type: str - python_tag=None, # type: Optional[str] - ): - # type: (...) -> Optional[str] - """Build one InstallRequirement using the "legacy" build process. - - Returns path to wheel if successfully built. Otherwise, returns None. - """ - wheel_args = make_setuptools_bdist_wheel_args( - setup_py_path, - global_options=global_options, - build_options=build_options, - destination_dir=tempd, - python_tag=python_tag, - ) - - spin_message = 'Building wheel for %s (setup.py)' % (name,) - with open_spinner(spin_message) as spinner: - logger.debug('Destination directory: %s', tempd) - - try: - output = call_subprocess( - wheel_args, - cwd=source_dir, - spinner=spinner, - ) - except Exception: - spinner.finish("error") - logger.error('Failed building wheel for %s', name) - return None - - names = os.listdir(tempd) - wheel_path = get_legacy_build_wheel_path( - names=names, - temp_dir=tempd, - name=name, - command_args=wheel_args, - command_output=output, - ) - return wheel_path - def _clean_one(self, req): # type: (InstallRequirement) -> bool clean_args = make_setuptools_clean_args( From bd646446fe5534acff04afabdf705b04587338ac Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 7 Nov 2019 21:44:21 -0500 Subject: [PATCH 0724/3170] Update comment to reflect new arguments --- src/pip/_internal/wheel_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index 68780cda237..f1383a46627 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -200,7 +200,7 @@ def _build_wheel_legacy( python_tag=None, # type: Optional[str] ): # type: (...) -> Optional[str] - """Build one InstallRequirement using the "legacy" build process. + """Build one unpacked package using the "legacy" build process. Returns path to wheel if successfully built. Otherwise, returns None. """ From 975553109455b54270f2c85d9f976719fab1c932 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sat, 9 Nov 2019 10:02:55 +0530 Subject: [PATCH 0725/3170] Make no-response bot, wait for less time --- .github/no-response.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/no-response.yml b/.github/no-response.yml index 808756a122d..47afde800e0 100644 --- a/.github/no-response.yml +++ b/.github/no-response.yml @@ -1,5 +1,5 @@ # Number of days of inactivity before issue is closed for lack of response -daysUntilClose: 30 +daysUntilClose: 15 # Label requiring a response responseRequiredLabel: "S: awaiting response" # Comment to post when closing an Issue for lack of response. Set to `false` to disable From 4e7867d0a8c3904dd6dc6e030f90511f89e33b9f Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sat, 9 Nov 2019 12:05:28 +0530 Subject: [PATCH 0726/3170] Compute require_hashes in populate_requirement_set --- src/pip/_internal/cli/req_command.py | 12 +++++++++--- src/pip/_internal/legacy_resolve.py | 9 +++------ 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 38a394b7032..05df1b16287 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -226,9 +226,6 @@ def populate_requirement_set( """ Marshal cmd line args into a requirement set. """ - # NOTE: As a side-effect, options.require_hashes and - # requirement_set.require_hashes may be updated - for filename in options.constraints: for req_to_add in parse_requirements( filename, @@ -256,6 +253,7 @@ def populate_requirement_set( req_to_add.is_direct = True requirement_set.add_requirement(req_to_add) + # NOTE: options.require_hashes may be set if --require-hashes is True for filename in options.requirements: for req_to_add in parse_requirements( filename, @@ -265,6 +263,14 @@ def populate_requirement_set( req_to_add.is_direct = True requirement_set.add_requirement(req_to_add) + # If any requirement has hash options, enable hash checking. + requirements = ( + requirement_set.unnamed_requirements + + list(requirement_set.requirements.values()) + ) + if any(req.has_hash_options for req in requirements): + options.require_hashes = True + if not (args or options.editables or options.requirements): opts = {'name': self.name} if options.find_links: diff --git a/src/pip/_internal/legacy_resolve.py b/src/pip/_internal/legacy_resolve.py index 9acfa8bb9dd..d276fbe250c 100644 --- a/src/pip/_internal/legacy_resolve.py +++ b/src/pip/_internal/legacy_resolve.py @@ -176,11 +176,6 @@ def resolve(self, requirement_set): list(requirement_set.requirements.values()) ) - require_hashes = ( - self.require_hashes_option or - any(req.has_hash_options for req in root_reqs) - ) - # 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 @@ -190,7 +185,9 @@ def resolve(self, requirement_set): for req in chain(root_reqs, discovered_reqs): try: discovered_reqs.extend( - self._resolve_one(requirement_set, req, require_hashes) + self._resolve_one( + requirement_set, req, self.require_hashes_option + ) ) except HashError as exc: exc.req = req From c0afe5c81da4b79c223b7879140c11e3204708a5 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sat, 9 Nov 2019 12:13:29 +0530 Subject: [PATCH 0727/3170] Add require_hashes to RequirementPreparer --- src/pip/_internal/cli/req_command.py | 1 + src/pip/_internal/operations/prepare.py | 14 +++++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 05df1b16287..243cf33d13f 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -173,6 +173,7 @@ def make_requirement_preparer( req_tracker=req_tracker, session=session, finder=finder, + require_hashes=options.require_hashes, ) @staticmethod diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index b145e65fa6f..220072d3cf2 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -510,6 +510,7 @@ def __init__( req_tracker, # type: RequirementTracker session, # type: PipSession finder, # type: PackageFinder + require_hashes, # type: bool ): # type: (...) -> None super(RequirementPreparer, self).__init__() @@ -543,6 +544,9 @@ def __init__( # Is build isolation allowed? self.build_isolation = build_isolation + # Should hash-checking be required? + self.require_hashes = require_hashes + @property def _download_should_save(self): # type: () -> bool @@ -600,7 +604,7 @@ def prepare_linked_requirement( # requirements we have and raise some more informative errors # than otherwise. (For example, we can raise VcsHashUnsupported # for a VCS URL rather than HashMissing.) - if require_hashes: + if self.require_hashes: # We could check these first 2 conditions inside # unpack_url and save repetition of conditions, but then # we would report less-useful error messages for @@ -620,8 +624,8 @@ def prepare_linked_requirement( # about them not being pinned. raise HashUnpinned() - hashes = req.hashes(trust_internet=not require_hashes) - if require_hashes and not hashes: + hashes = req.hashes(trust_internet=not self.require_hashes) + if self.require_hashes and not hashes: # Known-good hashes are missing for this requirement, so # shim it with a facade object that will provoke hash # computation and then raise a HashMissing exception @@ -690,7 +694,7 @@ def prepare_editable_requirement( logger.info('Obtaining %s', req) with indent_log(): - if require_hashes: + if self.require_hashes: raise InstallationError( 'The editable requirement {} cannot be installed when ' 'requiring hashes, because there is no single file to ' @@ -728,7 +732,7 @@ def prepare_installed_requirement( skip_reason, req, req.satisfied_by.version ) with indent_log(): - if require_hashes: + if self.require_hashes: logger.debug( 'Since it is already installed, we are trusting this ' 'package without checking its hash. To ensure a ' From 0d0ee9b4075523d0e65aa3f113ab92583e03698e Mon Sep 17 00:00:00 2001 From: Aniruddha Basak <codewithaniruddha@gmail.com> Date: Sat, 9 Nov 2019 15:34:58 +0530 Subject: [PATCH 0728/3170] Modify URL and give information of enterprise environments supporting older Pythons and pips --- docs/html/user_guide.rst | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index f359b0d016d..40c6590188d 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -55,7 +55,7 @@ Basic Authentication Credentials pip supports basic authentication credentials. Basically, in the url there is a username and password separated by ``:``. -``https://[username[:password]]@pypi.company.com/simple`` +``https://[username[:password]@]pypi.company.com/simple`` Certain special characters are not valid in the authentication part of URLs. If the user or password part of your login credentials contain any of these @@ -65,6 +65,11 @@ pypi.company.com, the index URL with credentials would look like: ``https://user:he%2F%2Fo@pypi.company.com`` +Support for percent-encoded authentication in index URLs was added in pip 10.0.0 +(in `#3236 <https://github.com/pypa/pip/issues/3236>`). Users that must use authentication +for their Python repository on systems with older pip versions should make the latest +get-pip.py available in their environment to bootstrap pip to a recent-enough version. + For indexes that only require single-part authentication tokens, provide the token as the "username" and do not provide a password, for example - From 7f910251a4ac5f0f13925e5b3a55ab838558c3ca Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 9 Nov 2019 10:30:44 -0500 Subject: [PATCH 0729/3170] Remove unused sys.platform patching from test_wheel --- tests/unit/test_wheel.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index ea145d72274..57912a9814d 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -331,7 +331,6 @@ def test_not_supported_version(self): w = wheel.Wheel('simple-0.1-py2-none-any.whl') assert not w.supported(tags=[('py1', 'none', 'any')]) - @patch('sys.platform', 'darwin') @patch('pip._internal.pep425tags.get_abbr_impl', lambda: 'cp') @patch('pip._internal.pep425tags.get_platform', lambda: 'macosx_10_9_intel') @@ -345,7 +344,6 @@ def test_supported_osx_version(self): w = wheel.Wheel('simple-0.1-cp27-none-macosx_10_9_intel.whl') assert w.supported(tags=tags) - @patch('sys.platform', 'darwin') @patch('pip._internal.pep425tags.get_abbr_impl', lambda: 'cp') @patch('pip._internal.pep425tags.get_platform', lambda: 'macosx_10_6_intel') @@ -357,7 +355,6 @@ def test_not_supported_osx_version(self): w = wheel.Wheel('simple-0.1-cp27-none-macosx_10_9_intel.whl') assert not w.supported(tags=tags) - @patch('sys.platform', 'darwin') @patch('pip._internal.pep425tags.get_abbr_impl', lambda: 'cp') def test_supported_multiarch_darwin(self): """ @@ -397,7 +394,6 @@ def test_supported_multiarch_darwin(self): assert w.supported(tags=ppc) assert w.supported(tags=ppc64) - @patch('sys.platform', 'darwin') @patch('pip._internal.pep425tags.get_abbr_impl', lambda: 'cp') def test_not_supported_multiarch_darwin(self): """ From 94dbbe2556adeded573b9ac0f84067218c8917e8 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 9 Nov 2019 10:41:21 -0500 Subject: [PATCH 0730/3170] Remove patching of pep425tags internals in wheel tests --- tests/unit/test_wheel.py | 64 +++++++++++++++++++--------------------- 1 file changed, 30 insertions(+), 34 deletions(-) diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 57912a9814d..6b554d52974 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -331,53 +331,50 @@ def test_not_supported_version(self): w = wheel.Wheel('simple-0.1-py2-none-any.whl') assert not w.supported(tags=[('py1', 'none', 'any')]) - @patch('pip._internal.pep425tags.get_abbr_impl', lambda: 'cp') - @patch('pip._internal.pep425tags.get_platform', - lambda: 'macosx_10_9_intel') def test_supported_osx_version(self): """ Wheels built for macOS 10.6 are supported on 10.9 """ - tags = pep425tags.get_supported(['27'], False) + tags = pep425tags.get_supported( + ['27'], platform='macosx_10_9_intel', impl='cp' + ) w = wheel.Wheel('simple-0.1-cp27-none-macosx_10_6_intel.whl') assert w.supported(tags=tags) w = wheel.Wheel('simple-0.1-cp27-none-macosx_10_9_intel.whl') assert w.supported(tags=tags) - @patch('pip._internal.pep425tags.get_abbr_impl', lambda: 'cp') - @patch('pip._internal.pep425tags.get_platform', - lambda: 'macosx_10_6_intel') def test_not_supported_osx_version(self): """ Wheels built for macOS 10.9 are not supported on 10.6 """ - tags = pep425tags.get_supported(['27'], False) + tags = pep425tags.get_supported( + ['27'], platform='macosx_10_6_intel', impl='cp' + ) w = wheel.Wheel('simple-0.1-cp27-none-macosx_10_9_intel.whl') assert not w.supported(tags=tags) - @patch('pip._internal.pep425tags.get_abbr_impl', lambda: 'cp') def test_supported_multiarch_darwin(self): """ Multi-arch wheels (intel) are supported on components (i386, x86_64) """ - with patch('pip._internal.pep425tags.get_platform', - lambda: 'macosx_10_5_universal'): - universal = pep425tags.get_supported(['27'], False) - with patch('pip._internal.pep425tags.get_platform', - lambda: 'macosx_10_5_intel'): - intel = pep425tags.get_supported(['27'], False) - with patch('pip._internal.pep425tags.get_platform', - lambda: 'macosx_10_5_x86_64'): - x64 = pep425tags.get_supported(['27'], False) - with patch('pip._internal.pep425tags.get_platform', - lambda: 'macosx_10_5_i386'): - i386 = pep425tags.get_supported(['27'], False) - with patch('pip._internal.pep425tags.get_platform', - lambda: 'macosx_10_5_ppc'): - ppc = pep425tags.get_supported(['27'], False) - with patch('pip._internal.pep425tags.get_platform', - lambda: 'macosx_10_5_ppc64'): - ppc64 = pep425tags.get_supported(['27'], False) + universal = pep425tags.get_supported( + ['27'], platform='macosx_10_5_universal', impl='cp' + ) + intel = pep425tags.get_supported( + ['27'], platform='macosx_10_5_intel', impl='cp' + ) + x64 = pep425tags.get_supported( + ['27'], platform='macosx_10_5_x86_64', impl='cp' + ) + i386 = pep425tags.get_supported( + ['27'], platform='macosx_10_5_i386', impl='cp' + ) + ppc = pep425tags.get_supported( + ['27'], platform='macosx_10_5_ppc', impl='cp' + ) + ppc64 = pep425tags.get_supported( + ['27'], platform='macosx_10_5_ppc64', impl='cp' + ) w = wheel.Wheel('simple-0.1-cp27-none-macosx_10_5_intel.whl') assert w.supported(tags=intel) @@ -394,17 +391,16 @@ def test_supported_multiarch_darwin(self): assert w.supported(tags=ppc) assert w.supported(tags=ppc64) - @patch('pip._internal.pep425tags.get_abbr_impl', lambda: 'cp') def test_not_supported_multiarch_darwin(self): """ Single-arch wheels (x86_64) are not supported on multi-arch (intel) """ - with patch('pip._internal.pep425tags.get_platform', - lambda: 'macosx_10_5_universal'): - universal = pep425tags.get_supported(['27'], False) - with patch('pip._internal.pep425tags.get_platform', - lambda: 'macosx_10_5_intel'): - intel = pep425tags.get_supported(['27'], False) + universal = pep425tags.get_supported( + ['27'], platform='macosx_10_5_universal', impl='cp' + ) + intel = pep425tags.get_supported( + ['27'], platform='macosx_10_5_intel', impl='cp' + ) w = wheel.Wheel('simple-0.1-cp27-none-macosx_10_5_i386.whl') assert not w.supported(tags=intel) From 0612685e7e4451c53f4e78d17724fc251ec47ca5 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sat, 9 Nov 2019 12:14:37 +0530 Subject: [PATCH 0731/3170] Stop passing require_hashes to Resolver directly --- src/pip/_internal/cli/req_command.py | 1 - src/pip/_internal/legacy_resolve.py | 27 ++++++++----------------- src/pip/_internal/operations/prepare.py | 3 --- 3 files changed, 8 insertions(+), 23 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 243cf33d13f..affa8aed406 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -211,7 +211,6 @@ def make_resolver( force_reinstall=force_reinstall, upgrade_strategy=upgrade_strategy, py_version_info=py_version_info, - require_hashes=options.require_hashes, ) def populate_requirement_set( diff --git a/src/pip/_internal/legacy_resolve.py b/src/pip/_internal/legacy_resolve.py index d276fbe250c..0487b3019df 100644 --- a/src/pip/_internal/legacy_resolve.py +++ b/src/pip/_internal/legacy_resolve.py @@ -124,7 +124,6 @@ def __init__( force_reinstall, # type: bool upgrade_strategy, # type: str py_version_info=None, # type: Optional[Tuple[int, ...]] - require_hashes=False, # type: bool ): # type: (...) -> None super(Resolver, self).__init__() @@ -140,8 +139,6 @@ def __init__( self.preparer = preparer self.finder = finder - self.require_hashes_option = require_hashes - self.upgrade_strategy = upgrade_strategy self.force_reinstall = force_reinstall self.ignore_dependencies = ignore_dependencies @@ -184,11 +181,7 @@ def resolve(self, requirement_set): hash_errors = HashErrors() for req in chain(root_reqs, discovered_reqs): try: - discovered_reqs.extend( - self._resolve_one( - requirement_set, req, self.require_hashes_option - ) - ) + discovered_reqs.extend(self._resolve_one(requirement_set, req)) except HashError as exc: exc.req = req hash_errors.append(exc) @@ -270,14 +263,14 @@ def _check_skip_installed(self, req_to_install): self._set_req_to_reinstall(req_to_install) return None - def _get_abstract_dist_for(self, req, require_hashes): - # type: (InstallRequirement, bool) -> AbstractDistribution + def _get_abstract_dist_for(self, req): + # type: (InstallRequirement) -> AbstractDistribution """Takes a InstallRequirement and returns a single AbstractDist \ representing a prepared variant of the same. """ if req.editable: return self.preparer.prepare_editable_requirement( - req, require_hashes, self.use_user_site, + req, self.use_user_site ) # satisfied_by is only evaluated by calling _check_skip_installed, @@ -287,16 +280,15 @@ def _get_abstract_dist_for(self, req, require_hashes): if req.satisfied_by: return self.preparer.prepare_installed_requirement( - req, require_hashes, skip_reason + req, skip_reason ) upgrade_allowed = self._is_upgrade_allowed(req) # We eagerly populate the link, since that's our "legacy" behavior. + require_hashes = self.preparer.require_hashes req.populate_link(self.finder, upgrade_allowed, require_hashes) - abstract_dist = self.preparer.prepare_linked_requirement( - req, require_hashes - ) + abstract_dist = self.preparer.prepare_linked_requirement(req) # NOTE # The following portion is for determining if a certain package is @@ -330,7 +322,6 @@ def _resolve_one( self, requirement_set, # type: RequirementSet req_to_install, # type: InstallRequirement - require_hashes, # type: bool ): # type: (...) -> List[InstallRequirement] """Prepare a single requirements file. @@ -348,9 +339,7 @@ def _resolve_one( # 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, require_hashes - ) + abstract_dist = self._get_abstract_dist_for(req_to_install) # Parse and return dependencies dist = abstract_dist.get_pkg_resources_distribution() diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 220072d3cf2..4795c489639 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -564,7 +564,6 @@ def _download_should_save(self): def prepare_linked_requirement( self, req, # type: InstallRequirement - require_hashes, # type: bool ): # type: (...) -> AbstractDistribution """Prepare a requirement that would be obtained from req.link @@ -683,7 +682,6 @@ def prepare_linked_requirement( def prepare_editable_requirement( self, req, # type: InstallRequirement - require_hashes, # type: bool use_user_site, # type: bool ): # type: (...) -> AbstractDistribution @@ -716,7 +714,6 @@ def prepare_editable_requirement( def prepare_installed_requirement( self, req, # type: InstallRequirement - require_hashes, # type: bool skip_reason # type: str ): # type: (...) -> AbstractDistribution From 7f8c25dbbb8ac31a7b67231858d92c699fa23b20 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 9 Nov 2019 16:11:43 -0500 Subject: [PATCH 0732/3170] Fix call to Resolver/Preparer in tests --- tests/unit/test_req.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index 64e7adb0021..ac93720b250 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -72,6 +72,7 @@ def _basic_resolver(self, finder, require_hashes=False): req_tracker=RequirementTracker(), session=PipSession(), finder=finder, + require_hashes=require_hashes, ) make_install_req = partial( install_req_from_req_string, @@ -86,7 +87,6 @@ def _basic_resolver(self, finder, require_hashes=False): use_user_site=False, upgrade_strategy="to-satisfy-only", ignore_dependencies=False, ignore_installed=False, ignore_requires_python=False, force_reinstall=False, - require_hashes=require_hashes, ) def test_no_reuse_existing_build_dir(self, data): From e4a7276ea0173c87697bffe01a35865d2c5b4df7 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 9 Nov 2019 16:12:05 -0500 Subject: [PATCH 0733/3170] Explicitly require hashes for tests that expect it The purpose of these tests is not to check implicit hash-checking mode, so we can be explicit. --- tests/unit/test_req.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index ac93720b250..20d5035c744 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -232,7 +232,7 @@ def test_unsupported_hashes(self, data): lineno=2, )) finder = make_test_finder(find_links=[data.find_links]) - resolver = self._basic_resolver(finder) + resolver = self._basic_resolver(finder, require_hashes=True) sep = os.path.sep if sep == '\\': sep = '\\\\' # This needs to be escaped for the regex @@ -266,7 +266,7 @@ def test_unpinned_hash_checking(self, data): lineno=2, )) finder = make_test_finder(find_links=[data.find_links]) - resolver = self._basic_resolver(finder) + resolver = self._basic_resolver(finder, require_hashes=True) assert_raises_regexp( HashErrors, # Make sure all failing requirements are listed: @@ -285,7 +285,7 @@ def test_hash_mismatch(self, data): '%s --hash=sha256:badbad' % file_url, lineno=1, )) finder = make_test_finder(find_links=[data.find_links]) - resolver = self._basic_resolver(finder) + resolver = self._basic_resolver(finder, require_hashes=True) assert_raises_regexp( HashErrors, r'THESE PACKAGES DO NOT MATCH THE HASHES.*\n' @@ -301,7 +301,7 @@ def test_unhashed_deps_on_require_hashes(self, data): dependencies get complained about when --require-hashes is on.""" reqset = RequirementSet() finder = make_test_finder(find_links=[data.find_links]) - resolver = self._basic_resolver(finder) + resolver = self._basic_resolver(finder, require_hashes=True) reqset.add_requirement(get_processed_req_from_line( 'TopoRequires2==0.0.1 ' # requires TopoRequires '--hash=sha256:eaf9a01242c9f2f42cf2bd82a6a848cd' From 9775387e415ddf8518b10c60c9ad50fba33065d6 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 9 Nov 2019 16:30:44 -0500 Subject: [PATCH 0734/3170] Move unit test to functional test for implicit hash checking --- tests/functional/test_install.py | 36 +++++++++++++++++++++++++ tests/unit/test_req.py | 45 -------------------------------- 2 files changed, 36 insertions(+), 45 deletions(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 364893eeca6..a9eff081de2 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1,6 +1,7 @@ import distutils import glob import os +import re import shutil import ssl import sys @@ -498,6 +499,41 @@ def test_hashed_install_failure(script, tmpdir): assert len(result.files_created) == 0 +def assert_re_match(pattern, text): + assert re.search(pattern, text), ( + "Could not find {!r} in {!r}".format(pattern, text) + ) + + +@pytest.mark.network +def test_hashed_install_failure_later_flag(script, tmpdir): + with requirements_file( + "blessings==1.0\n" + "tracefront==0.1 --hash=sha256:somehash\n" + "https://files.pythonhosted.org/packages/source/m/more-itertools/" + "more-itertools-1.0.tar.gz#md5=b21850c3cfa7efbb70fd662ab5413bdd\n" + "https://files.pythonhosted.org/" + "packages/source/p/peep/peep-3.1.1.tar.gz\n", + tmpdir, + ) as reqs_file: + result = script.pip( + "install", "-r", reqs_file.resolve(), expect_error=True + ) + + assert_re_match( + r'Hashes are required in --require-hashes mode, but they are ' + r'missing .*\n' + r' https://files\.pythonhosted\.org/packages/source/p/peep/peep' + r'-3\.1\.1\.tar\.gz --hash=sha256:[0-9a-f]+\n' + r' blessings==1.0 --hash=sha256:[0-9a-f]+\n' + r'THESE PACKAGES DO NOT MATCH THE HASHES.*\n' + r' tracefront==0.1 .*:\n' + r' Expected sha256 somehash\n' + r' Got [0-9a-f]+', + result.stderr, + ) + + def test_install_from_local_directory_with_symlinks_to_directories( script, data): """ diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index 20d5035c744..b84d38052c6 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -131,51 +131,6 @@ def test_environment_marker_extras(self, data): else: assert not reqset.has_requirement('simple') - @pytest.mark.network - def test_missing_hash_checking(self): - """Make sure prepare_files() raises an error when a requirement has no - hash in implicit hash-checking mode. - """ - reqset = RequirementSet() - # No flags here. This tests that detection of later flags nonetheless - # requires earlier packages to have hashes: - reqset.add_requirement(get_processed_req_from_line( - 'blessings==1.0', lineno=1 - )) - # This flag activates --require-hashes mode: - reqset.add_requirement(get_processed_req_from_line( - 'tracefront==0.1 --hash=sha256:somehash', lineno=2, - )) - # This hash should be accepted because it came from the reqs file, not - # from the internet: - reqset.add_requirement(get_processed_req_from_line( - 'https://files.pythonhosted.org/packages/source/m/more-itertools/' - 'more-itertools-1.0.tar.gz#md5=b21850c3cfa7efbb70fd662ab5413bdd', - lineno=3, - )) - # The error text should list this as a URL and not `peep==3.1.1`: - reqset.add_requirement(get_processed_req_from_line( - 'https://files.pythonhosted.org/' - 'packages/source/p/peep/peep-3.1.1.tar.gz', - lineno=4, - )) - finder = make_test_finder(index_urls=['https://pypi.org/simple/']) - resolver = self._basic_resolver(finder) - assert_raises_regexp( - HashErrors, - r'Hashes are required in --require-hashes mode, but they are ' - r'missing .*\n' - r' https://files\.pythonhosted\.org/packages/source/p/peep/peep' - r'-3\.1\.1\.tar\.gz --hash=sha256:[0-9a-f]+\n' - r' blessings==1.0 --hash=sha256:[0-9a-f]+\n' - r'THESE PACKAGES DO NOT MATCH THE HASHES.*\n' - r' tracefront==0.1 .*:\n' - r' Expected sha256 somehash\n' - r' Got [0-9a-f]+$', - resolver.resolve, - reqset - ) - def test_missing_hash_with_require_hashes(self, data): """Setting --require-hashes explicitly should raise errors if hashes are missing. From eb3701f74917323fbc2149e0bbabe08af849bc1d Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 9 Nov 2019 20:47:20 -0500 Subject: [PATCH 0735/3170] Use shared_data to avoid copying the data directory --- tests/unit/test_pep517.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/unit/test_pep517.py b/tests/unit/test_pep517.py index 31f2481a9fe..daa12ecedbb 100644 --- a/tests/unit/test_pep517.py +++ b/tests/unit/test_pep517.py @@ -9,11 +9,11 @@ ("pep517_setup_only", False), ("pep517_pyproject_only", True), ]) -def test_use_pep517(data, source, expected): +def test_use_pep517(shared_data, source, expected): """ Test that we choose correctly between PEP 517 and legacy code paths """ - src = data.src.joinpath(source) + src = shared_data.src.joinpath(source) req = InstallRequirement(None, None, source_dir=src) req.load_pyproject_toml() assert req.use_pep517 is expected @@ -23,11 +23,11 @@ def test_use_pep517(data, source, expected): ("pep517_setup_and_pyproject", "specifies a build backend"), ("pep517_pyproject_only", "does not have a setup.py"), ]) -def test_disabling_pep517_invalid(data, source, msg): +def test_disabling_pep517_invalid(shared_data, source, msg): """ Test that we fail if we try to disable PEP 517 when it's not acceptable """ - src = data.src.joinpath(source) + src = shared_data.src.joinpath(source) req = InstallRequirement(None, None, source_dir=src) # Simulate --no-use-pep517 From f64f15b6d92b76ef111b6346b03e55fb2c2ba53c Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 9 Nov 2019 20:58:27 -0500 Subject: [PATCH 0736/3170] Enforce requirement format in build-system.requires --- news/6410.bugfix | 2 ++ src/pip/_internal/pyproject.py | 16 ++++++++++++++++ tests/unit/test_pep517.py | 22 ++++++++++++++++++++++ 3 files changed, 40 insertions(+) create mode 100644 news/6410.bugfix diff --git a/news/6410.bugfix b/news/6410.bugfix new file mode 100644 index 00000000000..c96fba8bdb1 --- /dev/null +++ b/news/6410.bugfix @@ -0,0 +1,2 @@ +Enforce PEP 508 requirement format in ``pyproject.toml`` +``build-system.requires``. diff --git a/src/pip/_internal/pyproject.py b/src/pip/_internal/pyproject.py index 98c20f7796d..459d9872715 100644 --- a/src/pip/_internal/pyproject.py +++ b/src/pip/_internal/pyproject.py @@ -5,6 +5,7 @@ import sys from pip._vendor import pytoml, six +from pip._vendor.packaging.requirements import InvalidRequirement, Requirement from pip._internal.exceptions import InstallationError from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -150,6 +151,21 @@ def load_pyproject_toml( reason="'build-system.requires' is not a list of strings.", )) + # Each requirement must be valid as per PEP 508 + for requirement in requires: + try: + Requirement(requirement) + except InvalidRequirement: + raise InstallationError( + error_template.format( + package=req_name, + reason=( + "'build-system.requires' contains an invalid " + "requirement: {!r}".format(requirement) + ), + ) + ) + backend = build_system.get("build-backend") check = [] # type: List[str] if backend is None: diff --git a/tests/unit/test_pep517.py b/tests/unit/test_pep517.py index daa12ecedbb..c961a7f2f97 100644 --- a/tests/unit/test_pep517.py +++ b/tests/unit/test_pep517.py @@ -1,3 +1,5 @@ +from textwrap import dedent + import pytest from pip._internal.exceptions import InstallationError @@ -39,3 +41,23 @@ def test_disabling_pep517_invalid(shared_data, source, msg): err_msg = e.value.args[0] assert "Disabling PEP 517 processing is invalid" in err_msg assert msg in err_msg + + +@pytest.mark.parametrize( + ("spec",), [("./foo",), ("git+https://example.com/pkg@dev#egg=myproj",)] +) +def test_pep517_parsing_checks_requirements(tmpdir, spec): + tmpdir.joinpath("pyproject.toml").write_text(dedent( + """ + [build-system] + requires = [{!r}] + build-backend = "foo" + """.format(spec) + )) + req = InstallRequirement(None, None, source_dir=tmpdir) + + with pytest.raises(InstallationError) as e: + req.load_pyproject_toml() + + err_msg = e.value.args[0] + assert "contains an invalid requirement" in err_msg From 3dfa9e420ff9c65c229a5429029d1838d26df4e5 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 10 Nov 2019 11:48:13 +0530 Subject: [PATCH 0737/3170] nox: Remove pre-existing source distributions --- noxfile.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/noxfile.py b/noxfile.py index e03dbbd1d4c..9788c7b8b7c 100644 --- a/noxfile.py +++ b/noxfile.py @@ -87,6 +87,8 @@ def test(session): # Build source distribution sdist_dir = os.path.join(session.virtualenv.location, "sdist") + if os.path.exists(sdist_dir): + shutil.rmtree(sdist_dir, ignore_errors=True) session.run( "python", "setup.py", "sdist", "--formats=zip", "--dist-dir", sdist_dir, From 08a7f501e833066236404fd57708daa293b988c9 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 10 Nov 2019 11:48:34 +0530 Subject: [PATCH 0738/3170] nox: Update interpreters available for testing --- noxfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index 9788c7b8b7c..60c4a4a0bda 100644 --- a/noxfile.py +++ b/noxfile.py @@ -68,7 +68,7 @@ def should_update_common_wheels(): # completely to nox for all our automation. Contributors should prefer using # `tox -e ...` until this note is removed. # ----------------------------------------------------------------------------- -@nox.session(python=["2.7", "3.5", "3.6", "3.7", "pypy"]) +@nox.session(python=["2.7", "3.5", "3.6", "3.7", "3.8", "pypy", "pypy3"]) def test(session): # Get the common wheels. if should_update_common_wheels(): From b94df0b57022891a1e2d5368a7532ead994d1a2b Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 10 Nov 2019 12:13:08 +0530 Subject: [PATCH 0739/3170] nox: Log the correct command name in error --- noxfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index e03dbbd1d4c..8ca2ce94e8d 100644 --- a/noxfile.py +++ b/noxfile.py @@ -189,7 +189,7 @@ def prepare_release(session): def build_release(session): version = release.get_version_from_arguments(session.posargs) if not version: - session.error("Usage: nox -s upload-release -- YY.N[.P]") + session.error("Usage: nox -s build-release -- YY.N[.P]") session.log("# Ensure no files in dist/") if release.have_files_in_folder("dist"): From 22cdfd6ad1d18670199db147099d25016d7f9d31 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 10 Nov 2019 12:16:02 +0530 Subject: [PATCH 0740/3170] nox: Add a command for uploading releases --- noxfile.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/noxfile.py b/noxfile.py index 8ca2ce94e8d..e5222d9785d 100644 --- a/noxfile.py +++ b/noxfile.py @@ -209,3 +209,36 @@ def build_release(session): session.log("# Checkout the master branch") session.run("git", "checkout", "master", external=True, silent=True) + + +@nox.session(name="upload-release") +def upload_release(session): + version = release.get_version_from_arguments(session.posargs) + if not version: + session.error("Usage: nox -s upload-release -- YY.N[.P]") + + session.log("# Install dependencies") + session.install("twine") + + distribution_files = glob.glob("dist/*") + session.log(f"# Distribution files: {distribution_files}") + + # Sanity check: Make sure there's 2 distribution files. + count = len(distribution_files) + if count != 2: + session.error( + f"Expected 2 distribution files for upload, got {count}. " + f"Remove dist/ and run 'nox -s build-release -- {version}'" + ) + # Sanity check: Make sure the files are correctly named. + expected_distribution_files = [ + f"pip-{version}-py2.py3-none-any.whl", + f"pip-{version}.tar.gz", + ] + if sorted(distribution_files) != sorted(expected_distribution_files): + session.error( + f"Distribution files do not seem to be for {version} release." + ) + + session.log("# Upload distributions") + session.run("twine", "upload", *distribution_files) From 130827072338375f552a97c59f29493f53861156 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 10 Nov 2019 12:16:16 +0530 Subject: [PATCH 0741/3170] Switch to using just nox commands in the release process --- docs/html/development/release-process.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/html/development/release-process.rst b/docs/html/development/release-process.rst index fb954b0dff2..9e9248c327b 100644 --- a/docs/html/development/release-process.rst +++ b/docs/html/development/release-process.rst @@ -80,13 +80,13 @@ Creating a new release ---------------------- #. Checkout the current pip ``master`` branch. -#. Ensure you have the latest ``nox`` and ``twine`` installed. +#. Ensure you have the latest ``nox`` installed. #. Prepare for release using ``nox -s prepare-release -- YY.N``. This will update the relevant files and tag the correct commit. #. Build the release artifacts using ``nox -s build-release -- YY.N``. This will checkout the tag, generate the distribution files to be uploaded and checkout the master branch again. -#. Upload the distribution files to PyPI using ``twine upload dist/*``. +#. Upload the release to PyPI using ``nox -s upload-release -- YY.N``. #. Push all of the changes including the tag. #. Regenerate the ``get-pip.py`` script in the `get-pip repository`_ (as documented there) and commit the results. From 4a97d91a39ea94c4ef285bc870d5872aa283be8b Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 27 Oct 2019 14:01:14 +0530 Subject: [PATCH 0742/3170] Add "get_requirement_tracker" --- src/pip/_internal/req/req_tracker.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/pip/_internal/req/req_tracker.py b/src/pip/_internal/req/req_tracker.py index 63044dd0ad5..61ca0c1ec1c 100644 --- a/src/pip/_internal/req/req_tracker.py +++ b/src/pip/_internal/req/req_tracker.py @@ -21,6 +21,19 @@ logger = logging.getLogger(__name__) +@contextlib.contextmanager +def get_requirement_tracker(): + root = os.environ.get('PIP_REQ_TRACKER') + with contextlib2.ExitStack() as ctx: + if root is None: + root = ctx.enter_context( + TempDirectory(kind='req-tracker') + ).path + ctx.enter_context(update_env_context_manager(PIP_REQ_TRACKER=root)) + + yield RequirementTracker(root) + + class RequirementTracker(object): def __init__(self): From fcf1168a40a55f39fa683e93a5fd84162bba3997 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Wed, 30 Oct 2019 22:12:33 +0530 Subject: [PATCH 0743/3170] Add context manager for handling environment details --- src/pip/_internal/req/req_tracker.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/pip/_internal/req/req_tracker.py b/src/pip/_internal/req/req_tracker.py index 61ca0c1ec1c..dba10d0569b 100644 --- a/src/pip/_internal/req/req_tracker.py +++ b/src/pip/_internal/req/req_tracker.py @@ -9,6 +9,8 @@ import logging import os +from pip._vendor import contextlib2 + from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -21,6 +23,31 @@ logger = logging.getLogger(__name__) +@contextlib.contextmanager +def update_env_context_manager(**changes): + target = os.environ + + # Save values from the target and change them. + non_existent_marker = object() + saved_values = {} + for name, value in changes.items(): + try: + saved_values[name] = target[name] + except KeyError: + saved_values[name] = non_existent_marker + target[name] = value + + try: + yield + finally: + # Restore original values in the target. + for name, value in saved_values.items(): + if value is non_existent_marker: + del target[name] + else: + target[name] = value + + @contextlib.contextmanager def get_requirement_tracker(): root = os.environ.get('PIP_REQ_TRACKER') From b953ef4d18b3a86e19213685c6572732b32a034b Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Wed, 30 Oct 2019 22:13:03 +0530 Subject: [PATCH 0744/3170] Drop environment and directory management from RequirementTracker --- src/pip/_internal/req/req_tracker.py | 20 +++----------------- 1 file changed, 3 insertions(+), 17 deletions(-) diff --git a/src/pip/_internal/req/req_tracker.py b/src/pip/_internal/req/req_tracker.py index dba10d0569b..61d538d5b6b 100644 --- a/src/pip/_internal/req/req_tracker.py +++ b/src/pip/_internal/req/req_tracker.py @@ -63,16 +63,9 @@ def get_requirement_tracker(): class RequirementTracker(object): - def __init__(self): - # type: () -> None - self._root = os.environ.get('PIP_REQ_TRACKER') - if self._root is None: - self._temp_dir = TempDirectory(delete=False, kind='req-tracker') - self._root = os.environ['PIP_REQ_TRACKER'] = self._temp_dir.path - logger.debug('Created requirements tracker %r', self._root) - else: - self._temp_dir = None - logger.debug('Re-using requirements tracker %r', self._root) + def __init__(self, root): + # type: (str) -> None + self._root = root self._entries = set() # type: Set[InstallRequirement] def __enter__(self): @@ -140,13 +133,6 @@ def cleanup(self): for req in set(self._entries): self.remove(req) - if self._temp_dir is None: - # Did not setup the directory. No action needed. - logger.debug("Cleaned build tracker: %r", self._root) - return - - # Cleanup the directory. - self._temp_dir.cleanup() logger.debug("Removed build tracker: %r", self._root) @contextlib.contextmanager From c40a9d332d52c37b087c96bb21faf6bb8426f753 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Wed, 30 Oct 2019 22:37:13 +0530 Subject: [PATCH 0745/3170] Update callsites for RequirementTracker --- src/pip/_internal/commands/download.py | 4 ++-- src/pip/_internal/commands/install.py | 4 ++-- src/pip/_internal/commands/wheel.py | 4 ++-- tests/unit/test_req.py | 4 ++-- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index 8cffc6a7f5e..cf5ea1c44ff 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -10,7 +10,7 @@ from pip._internal.cli.cmdoptions import make_target_python from pip._internal.cli.req_command import RequirementCommand from pip._internal.req import RequirementSet -from pip._internal.req.req_tracker import RequirementTracker +from pip._internal.req.req_tracker import get_requirement_tracker from pip._internal.utils.filesystem import check_path_owner from pip._internal.utils.misc import ensure_dir, normalize_path, write_output from pip._internal.utils.temp_dir import TempDirectory @@ -111,7 +111,7 @@ def run(self, options, args): ) options.cache_dir = None - with RequirementTracker() as req_tracker, TempDirectory( + with get_requirement_tracker() as req_tracker, TempDirectory( options.build_dir, delete=build_delete, kind="download" ) as directory: diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 67d7204f476..59b69bdd5ae 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -31,7 +31,7 @@ from pip._internal.locations import distutils_scheme from pip._internal.operations.check import check_install_conflicts from pip._internal.req import RequirementSet, install_given_reqs -from pip._internal.req.req_tracker import RequirementTracker +from pip._internal.req.req_tracker import get_requirement_tracker from pip._internal.utils.filesystem import check_path_owner, test_writable_dir from pip._internal.utils.misc import ( ensure_dir, @@ -343,7 +343,7 @@ def run(self, options, args): ) options.cache_dir = None - with RequirementTracker() as req_tracker, TempDirectory( + with get_requirement_tracker() as req_tracker, TempDirectory( options.build_dir, delete=build_delete, kind="install" ) as directory: requirement_set = RequirementSet( diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index 5217703d309..adc0252c358 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -13,7 +13,7 @@ from pip._internal.cli.req_command import RequirementCommand from pip._internal.exceptions import CommandError, PreviousBuildDirError from pip._internal.req import RequirementSet -from pip._internal.req.req_tracker import RequirementTracker +from pip._internal.req.req_tracker import get_requirement_tracker from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.wheel_builder import WheelBuilder @@ -157,7 +157,7 @@ def run(self, options, args): build_delete = (not (options.no_clean or options.build_dir)) wheel_cache = WheelCache(options.cache_dir, options.format_control) - with RequirementTracker() as req_tracker, TempDirectory( + with get_requirement_tracker() as req_tracker, TempDirectory( options.build_dir, delete=build_delete, kind="wheel" ) as directory: diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index b84d38052c6..ef3d0cc6c5d 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -30,7 +30,7 @@ parse_editable, ) from pip._internal.req.req_file import ParsedLine, get_line_parser, handle_line -from pip._internal.req.req_tracker import RequirementTracker +from pip._internal.req.req_tracker import get_requirement_tracker from pip._internal.utils.urls import path_to_url from tests.lib import assert_raises_regexp, make_test_finder, requirements_file @@ -69,7 +69,7 @@ def _basic_resolver(self, finder, require_hashes=False): wheel_download_dir=None, progress_bar="on", build_isolation=True, - req_tracker=RequirementTracker(), + req_tracker=get_requirement_tracker(), session=PipSession(), finder=finder, require_hashes=require_hashes, From c06d0aad996db50a645fd03a3d8388bfb9b29042 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Wed, 30 Oct 2019 22:35:12 +0530 Subject: [PATCH 0746/3170] Make mypy happy --- src/pip/_internal/req/req_tracker.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/req/req_tracker.py b/src/pip/_internal/req/req_tracker.py index 61d538d5b6b..6ae9e6da7fb 100644 --- a/src/pip/_internal/req/req_tracker.py +++ b/src/pip/_internal/req/req_tracker.py @@ -16,7 +16,7 @@ if MYPY_CHECK_RUNNING: from types import TracebackType - from typing import Iterator, Optional, Set, Type + from typing import Dict, Iterator, Optional, Set, Type, Union from pip._internal.req.req_install import InstallRequirement from pip._internal.models.link import Link @@ -25,31 +25,34 @@ @contextlib.contextmanager def update_env_context_manager(**changes): + # type: (str) -> Iterator[None] target = os.environ # Save values from the target and change them. non_existent_marker = object() - saved_values = {} - for name, value in changes.items(): + saved_values = {} # type: Dict[str, Union[object, str]] + for name, new_value in changes.items(): try: saved_values[name] = target[name] except KeyError: saved_values[name] = non_existent_marker - target[name] = value + target[name] = new_value try: yield finally: # Restore original values in the target. - for name, value in saved_values.items(): - if value is non_existent_marker: + for name, original_value in saved_values.items(): + if original_value is non_existent_marker: del target[name] else: - target[name] = value + assert isinstance(original_value, str) # for mypy + target[name] = original_value @contextlib.contextmanager def get_requirement_tracker(): + # type: () -> Iterator[RequirementTracker] root = os.environ.get('PIP_REQ_TRACKER') with contextlib2.ExitStack() as ctx: if root is None: From 573d89a6fd7619312a8578f8f5c815764ab50b4a Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Wed, 6 Nov 2019 15:38:40 +0530 Subject: [PATCH 0747/3170] Enter the RequirementTracker context in get_requirement_tracker --- src/pip/_internal/req/req_tracker.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/req/req_tracker.py b/src/pip/_internal/req/req_tracker.py index 6ae9e6da7fb..2e3b5be64ef 100644 --- a/src/pip/_internal/req/req_tracker.py +++ b/src/pip/_internal/req/req_tracker.py @@ -61,7 +61,8 @@ def get_requirement_tracker(): ).path ctx.enter_context(update_env_context_manager(PIP_REQ_TRACKER=root)) - yield RequirementTracker(root) + with RequirementTracker(root) as tracker: + yield tracker class RequirementTracker(object): From bb197e12bf0f2e5c758b57412b236c0df62c7ab4 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Wed, 6 Nov 2019 15:15:41 +0530 Subject: [PATCH 0748/3170] Move make_install_req definition to top of function Why: Makes the diff for the next commit cleaner --- tests/unit/test_req.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index ef3d0cc6c5d..934234fc5b1 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -62,6 +62,12 @@ def teardown(self): shutil.rmtree(self.tempdir, ignore_errors=True) def _basic_resolver(self, finder, require_hashes=False): + make_install_req = partial( + install_req_from_req_string, + isolated=False, + wheel_cache=None, + use_pep517=None, + ) preparer = RequirementPreparer( build_dir=os.path.join(self.tempdir, 'build'), src_dir=os.path.join(self.tempdir, 'src'), @@ -74,12 +80,6 @@ def _basic_resolver(self, finder, require_hashes=False): finder=finder, require_hashes=require_hashes, ) - make_install_req = partial( - install_req_from_req_string, - isolated=False, - wheel_cache=None, - use_pep517=None, - ) return Resolver( preparer=preparer, make_install_req=make_install_req, From f39ce3439b00e24703c95cc05eeb0f9059b1c75f Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Wed, 6 Nov 2019 15:38:02 +0530 Subject: [PATCH 0749/3170] Update tests to correctly enter RequirementTracker context --- tests/unit/test_req.py | 173 ++++++++++++++++++++++------------------- 1 file changed, 92 insertions(+), 81 deletions(-) diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index 934234fc5b1..c817ad9a68e 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -1,3 +1,4 @@ +import contextlib import os import shutil import sys @@ -61,6 +62,7 @@ def setup(self): def teardown(self): shutil.rmtree(self.tempdir, ignore_errors=True) + @contextlib.contextmanager def _basic_resolver(self, finder, require_hashes=False): make_install_req = partial( install_req_from_req_string, @@ -68,26 +70,28 @@ def _basic_resolver(self, finder, require_hashes=False): wheel_cache=None, use_pep517=None, ) - preparer = RequirementPreparer( - build_dir=os.path.join(self.tempdir, 'build'), - src_dir=os.path.join(self.tempdir, 'src'), - download_dir=None, - wheel_download_dir=None, - progress_bar="on", - build_isolation=True, - req_tracker=get_requirement_tracker(), - session=PipSession(), - finder=finder, - require_hashes=require_hashes, - ) - return Resolver( - preparer=preparer, - make_install_req=make_install_req, - finder=finder, - use_user_site=False, upgrade_strategy="to-satisfy-only", - ignore_dependencies=False, ignore_installed=False, - ignore_requires_python=False, force_reinstall=False, - ) + + with get_requirement_tracker() as tracker: + preparer = RequirementPreparer( + build_dir=os.path.join(self.tempdir, 'build'), + src_dir=os.path.join(self.tempdir, 'src'), + download_dir=None, + wheel_download_dir=None, + progress_bar="on", + build_isolation=True, + req_tracker=tracker, + session=PipSession(), + finder=finder, + require_hashes=require_hashes, + ) + yield Resolver( + preparer=preparer, + make_install_req=make_install_req, + finder=finder, + use_user_site=False, upgrade_strategy="to-satisfy-only", + ignore_dependencies=False, ignore_installed=False, + ignore_requires_python=False, force_reinstall=False, + ) def test_no_reuse_existing_build_dir(self, data): """Test prepare_files raise exception with previous build dir""" @@ -101,14 +105,14 @@ def test_no_reuse_existing_build_dir(self, data): req.is_direct = True reqset.add_requirement(req) finder = make_test_finder(find_links=[data.find_links]) - resolver = self._basic_resolver(finder) - assert_raises_regexp( - PreviousBuildDirError, - r"pip can't proceed with [\s\S]*%s[\s\S]*%s" % - (req, build_dir.replace('\\', '\\\\')), - resolver.resolve, - reqset, - ) + with self._basic_resolver(finder) as resolver: + assert_raises_regexp( + PreviousBuildDirError, + r"pip can't proceed with [\s\S]*%s[\s\S]*%s" % + (req, build_dir.replace('\\', '\\\\')), + resolver.resolve, + reqset, + ) # TODO: Update test when Python 2.7 is dropped. def test_environment_marker_extras(self, data): @@ -123,8 +127,8 @@ def test_environment_marker_extras(self, data): req.is_direct = True reqset.add_requirement(req) finder = make_test_finder(find_links=[data.find_links]) - resolver = self._basic_resolver(finder) - resolver.resolve(reqset) + with self._basic_resolver(finder) as resolver: + resolver.resolve(reqset) # This is hacky but does test both case in py2 and py3 if sys.version_info[:2] == (2, 7): assert reqset.has_requirement('simple') @@ -141,17 +145,17 @@ def test_missing_hash_with_require_hashes(self, data): )) finder = make_test_finder(find_links=[data.find_links]) - resolver = self._basic_resolver(finder, require_hashes=True) - - assert_raises_regexp( - HashErrors, - r'Hashes are required in --require-hashes mode, but they are ' - r'missing .*\n' - r' simple==1.0 --hash=sha256:393043e672415891885c9a2a0929b1af95' - r'fb866d6ca016b42d2e6ce53619b653$', - resolver.resolve, - reqset - ) + + with self._basic_resolver(finder, require_hashes=True) as resolver: + assert_raises_regexp( + HashErrors, + r'Hashes are required in --require-hashes mode, but they are ' + r'missing .*\n' + r' simple==1.0 --hash=sha256:393043e672415891885c9a2a0929b1af95' + r'fb866d6ca016b42d2e6ce53619b653$', + resolver.resolve, + reqset + ) def test_missing_hash_with_require_hashes_in_reqs_file(self, data, tmpdir): """--require-hashes in a requirements file should make its way to the @@ -187,22 +191,25 @@ def test_unsupported_hashes(self, data): lineno=2, )) finder = make_test_finder(find_links=[data.find_links]) - resolver = self._basic_resolver(finder, require_hashes=True) + sep = os.path.sep if sep == '\\': sep = '\\\\' # This needs to be escaped for the regex - assert_raises_regexp( - HashErrors, - r"Can't verify hashes for these requirements because we don't " - r"have a way to hash version control repositories:\n" - r" git\+git://github\.com/pypa/pip-test-package \(from -r file " - r"\(line 1\)\)\n" - r"Can't verify hashes for these file:// requirements because they " - r"point to directories:\n" - r" file://.*{sep}data{sep}packages{sep}FSPkg " - r"\(from -r file \(line 2\)\)".format(sep=sep), - resolver.resolve, - reqset) + + with self._basic_resolver(finder, require_hashes=True) as resolver: + assert_raises_regexp( + HashErrors, + r"Can't verify hashes for these requirements because we don't " + r"have a way to hash version control repositories:\n" + r" git\+git://github\.com/pypa/pip-test-package \(from -r file " + r"\(line 1\)\)\n" + r"Can't verify hashes for these file:// requirements because they " + r"point to directories:\n" + r" file://.*{sep}data{sep}packages{sep}FSPkg " + r"\(from -r file \(line 2\)\)".format(sep=sep), + resolver.resolve, + reqset, + ) def test_unpinned_hash_checking(self, data): """Make sure prepare_files() raises an error when a requirement is not @@ -221,15 +228,16 @@ def test_unpinned_hash_checking(self, data): lineno=2, )) finder = make_test_finder(find_links=[data.find_links]) - resolver = self._basic_resolver(finder, require_hashes=True) - assert_raises_regexp( - HashErrors, - # Make sure all failing requirements are listed: - r'versions pinned with ==. These do not:\n' - r' simple .* \(from -r file \(line 1\)\)\n' - r' simple2>1.0 .* \(from -r file \(line 2\)\)', - resolver.resolve, - reqset) + with self._basic_resolver(finder, require_hashes=True) as resolver: + assert_raises_regexp( + HashErrors, + # Make sure all failing requirements are listed: + r'versions pinned with ==. These do not:\n' + r' simple .* \(from -r file \(line 1\)\)\n' + r' simple2>1.0 .* \(from -r file \(line 2\)\)', + resolver.resolve, + reqset, + ) def test_hash_mismatch(self, data): """A hash mismatch should raise an error.""" @@ -240,36 +248,39 @@ def test_hash_mismatch(self, data): '%s --hash=sha256:badbad' % file_url, lineno=1, )) finder = make_test_finder(find_links=[data.find_links]) - resolver = self._basic_resolver(finder, require_hashes=True) - assert_raises_regexp( - HashErrors, - r'THESE PACKAGES DO NOT MATCH THE HASHES.*\n' - r' file:///.*/data/packages/simple-1\.0\.tar\.gz .*:\n' - r' Expected sha256 badbad\n' - r' Got 393043e672415891885c9a2a0929b1af95fb866d' - r'6ca016b42d2e6ce53619b653$', - resolver.resolve, - reqset) + with self._basic_resolver(finder, require_hashes=True) as resolver: + assert_raises_regexp( + HashErrors, + r'THESE PACKAGES DO NOT MATCH THE HASHES.*\n' + r' file:///.*/data/packages/simple-1\.0\.tar\.gz .*:\n' + r' Expected sha256 badbad\n' + r' Got 393043e672415891885c9a2a0929b1af95fb866d' + r'6ca016b42d2e6ce53619b653$', + resolver.resolve, + reqset, + ) def test_unhashed_deps_on_require_hashes(self, data): """Make sure unhashed, unpinned, or otherwise unrepeatable dependencies get complained about when --require-hashes is on.""" reqset = RequirementSet() finder = make_test_finder(find_links=[data.find_links]) - resolver = self._basic_resolver(finder, require_hashes=True) reqset.add_requirement(get_processed_req_from_line( 'TopoRequires2==0.0.1 ' # requires TopoRequires '--hash=sha256:eaf9a01242c9f2f42cf2bd82a6a848cd' 'e3591d14f7896bdbefcf48543720c970', lineno=1 )) - assert_raises_regexp( - HashErrors, - r'In --require-hashes mode, all requirements must have their ' - r'versions pinned.*\n' - r' TopoRequires from .*$', - resolver.resolve, - reqset) + + with self._basic_resolver(finder, require_hashes=True) as resolver: + assert_raises_regexp( + HashErrors, + r'In --require-hashes mode, all requirements must have their ' + r'versions pinned.*\n' + r' TopoRequires from .*$', + resolver.resolve, + reqset, + ) def test_hashed_deps_on_require_hashes(self): """Make sure hashed dependencies get installed when --require-hashes From 1b07d3e7977bbdae87d60e63ec33438f7b0ef3a4 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 20 Oct 2019 12:31:41 +0530 Subject: [PATCH 0750/3170] Rewrap strings to 80 characters --- tests/unit/test_req.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index c817ad9a68e..afd276a5950 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -151,8 +151,8 @@ def test_missing_hash_with_require_hashes(self, data): HashErrors, r'Hashes are required in --require-hashes mode, but they are ' r'missing .*\n' - r' simple==1.0 --hash=sha256:393043e672415891885c9a2a0929b1af95' - r'fb866d6ca016b42d2e6ce53619b653$', + r' simple==1.0 --hash=sha256:393043e672415891885c9a2a0929b1' + r'af95fb866d6ca016b42d2e6ce53619b653$', resolver.resolve, reqset ) @@ -201,10 +201,10 @@ def test_unsupported_hashes(self, data): HashErrors, r"Can't verify hashes for these requirements because we don't " r"have a way to hash version control repositories:\n" - r" git\+git://github\.com/pypa/pip-test-package \(from -r file " - r"\(line 1\)\)\n" - r"Can't verify hashes for these file:// requirements because they " - r"point to directories:\n" + r" git\+git://github\.com/pypa/pip-test-package \(from -r " + r"file \(line 1\)\)\n" + r"Can't verify hashes for these file:// requirements because " + r"they point to directories:\n" r" file://.*{sep}data{sep}packages{sep}FSPkg " r"\(from -r file \(line 2\)\)".format(sep=sep), resolver.resolve, @@ -254,8 +254,8 @@ def test_hash_mismatch(self, data): r'THESE PACKAGES DO NOT MATCH THE HASHES.*\n' r' file:///.*/data/packages/simple-1\.0\.tar\.gz .*:\n' r' Expected sha256 badbad\n' - r' Got 393043e672415891885c9a2a0929b1af95fb866d' - r'6ca016b42d2e6ce53619b653$', + r' Got 393043e672415891885c9a2a0929b1af95fb' + r'866d6ca016b42d2e6ce53619b653$', resolver.resolve, reqset, ) From 031ed90f45494db56b06a504e4b5b7b80a485e7e Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 10 Nov 2019 12:42:22 +0530 Subject: [PATCH 0751/3170] Add some useful log lines --- src/pip/_internal/req/req_tracker.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pip/_internal/req/req_tracker.py b/src/pip/_internal/req/req_tracker.py index 2e3b5be64ef..84e0c0419fc 100644 --- a/src/pip/_internal/req/req_tracker.py +++ b/src/pip/_internal/req/req_tracker.py @@ -60,6 +60,7 @@ def get_requirement_tracker(): TempDirectory(kind='req-tracker') ).path ctx.enter_context(update_env_context_manager(PIP_REQ_TRACKER=root)) + logger.debug("Initialized build tracking at %s", root) with RequirementTracker(root) as tracker: yield tracker @@ -71,9 +72,11 @@ def __init__(self, root): # type: (str) -> None self._root = root self._entries = set() # type: Set[InstallRequirement] + logger.debug("Created build tracker: %s", self._root) def __enter__(self): # type: () -> RequirementTracker + logger.debug("Entered build tracker: %s", self._root) return self def __exit__( From c9606b86e16f3deb9488a153aa3da9badbb023f0 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 10 Nov 2019 12:54:17 +0530 Subject: [PATCH 0752/3170] Add use_user_site to RequirementPreparer --- src/pip/_internal/cli/req_command.py | 2 ++ src/pip/_internal/commands/download.py | 1 + src/pip/_internal/commands/install.py | 1 + src/pip/_internal/commands/wheel.py | 1 + src/pip/_internal/operations/prepare.py | 4 ++++ tests/unit/test_req.py | 1 + 6 files changed, 10 insertions(+) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index affa8aed406..5f0752ff40a 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -154,6 +154,7 @@ def make_requirement_preparer( req_tracker, # type: RequirementTracker session, # type: PipSession finder, # type: PackageFinder + use_user_site, # type: bool download_dir=None, # type: str wheel_download_dir=None, # type: str ): @@ -174,6 +175,7 @@ def make_requirement_preparer( session=session, finder=finder, require_hashes=options.require_hashes, + use_user_site=use_user_site, ) @staticmethod diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index 8cffc6a7f5e..3e405119f71 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -132,6 +132,7 @@ def run(self, options, args): session=session, finder=finder, download_dir=options.download_dir, + use_user_site=False, ) resolver = self.make_resolver( diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 67d7204f476..cdddf8487e6 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -361,6 +361,7 @@ def run(self, options, args): req_tracker=req_tracker, session=session, finder=finder, + use_user_site=options.use_user_site, ) resolver = self.make_resolver( preparer=preparer, diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index 5217703d309..59cf6495bc9 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -176,6 +176,7 @@ def run(self, options, args): session=session, finder=finder, wheel_download_dir=options.wheel_dir, + use_user_site=False, ) resolver = self.make_resolver( diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 4795c489639..26f383053ee 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -511,6 +511,7 @@ def __init__( session, # type: PipSession finder, # type: PackageFinder require_hashes, # type: bool + use_user_site, # type: bool ): # type: (...) -> None super(RequirementPreparer, self).__init__() @@ -547,6 +548,9 @@ def __init__( # Should hash-checking be required? self.require_hashes = require_hashes + # Should install in user site-packages? + self.use_user_site = use_user_site + @property def _download_should_save(self): # type: () -> bool diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index b84d38052c6..4c2bd7979fd 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -73,6 +73,7 @@ def _basic_resolver(self, finder, require_hashes=False): session=PipSession(), finder=finder, require_hashes=require_hashes, + use_user_site=False, ) make_install_req = partial( install_req_from_req_string, From 11b3fc22652f27c8fe51c291d5c31522ce34255f Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 10 Nov 2019 12:55:14 +0530 Subject: [PATCH 0753/3170] Drop use_user_site argument from prepare_editable_requirement --- src/pip/_internal/legacy_resolve.py | 4 +--- src/pip/_internal/operations/prepare.py | 3 +-- 2 files changed, 2 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/legacy_resolve.py b/src/pip/_internal/legacy_resolve.py index f1d69941339..75d6076a588 100644 --- a/src/pip/_internal/legacy_resolve.py +++ b/src/pip/_internal/legacy_resolve.py @@ -269,9 +269,7 @@ def _get_abstract_dist_for(self, req): representing a prepared variant of the same. """ if req.editable: - return self.preparer.prepare_editable_requirement( - req, self.use_user_site - ) + return self.preparer.prepare_editable_requirement(req) # satisfied_by is only evaluated by calling _check_skip_installed, # so it must be None here. diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 26f383053ee..c564d64a7e9 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -686,7 +686,6 @@ def prepare_linked_requirement( def prepare_editable_requirement( self, req, # type: InstallRequirement - use_user_site, # type: bool ): # type: (...) -> AbstractDistribution """Prepare an editable requirement @@ -711,7 +710,7 @@ def prepare_editable_requirement( if self._download_should_save: req.archive(self.download_dir) - req.check_if_exists(use_user_site) + req.check_if_exists(self.use_user_site) return abstract_dist From 0f03a05207b8988b10eb4545ff8821b4a54ca13a Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 10 Nov 2019 14:00:59 +0530 Subject: [PATCH 0754/3170] Sort applicable candidates before computing best candidate --- src/pip/_internal/index/package_finder.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/index/package_finder.py b/src/pip/_internal/index/package_finder.py index 9b338e693be..74e3299d611 100644 --- a/src/pip/_internal/index/package_finder.py +++ b/src/pip/_internal/index/package_finder.py @@ -473,12 +473,14 @@ def get_applicable_candidates( c for c in candidates if str(c.version) in versions ] - return filter_unallowed_hashes( + filtered_applicable_candidates = filter_unallowed_hashes( candidates=applicable_candidates, hashes=self._hashes, project_name=self._project_name, ) + return sorted(filtered_applicable_candidates, key=self._sort_key) + def _sort_key(self, candidate): # type: (InstallationCandidate) -> CandidateSortingKey """ From 997db009b2541cd425b7e52598cc0be001f4337a Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 10 Nov 2019 14:12:56 +0530 Subject: [PATCH 0755/3170] Update documentation for package finding --- docs/html/development/architecture/package-finding.rst | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/docs/html/development/architecture/package-finding.rst b/docs/html/development/architecture/package-finding.rst index 53ece59f8d9..4923c19965f 100644 --- a/docs/html/development/architecture/package-finding.rst +++ b/docs/html/development/architecture/package-finding.rst @@ -182,12 +182,11 @@ user, and other user preferences, etc. Specifically, the class has a ``get_applicable_candidates()`` method. This accepts the ``InstallationCandidate`` objects resulting from the links -accepted by the ``LinkEvaluator`` class's ``evaluate_link()`` method, and -it further filters them to a list of "applicable" candidates. +accepted by the ``LinkEvaluator`` class's ``evaluate_link()`` method, filters +them to a list of "applicable" candidates and orders them by preference. The ``CandidateEvaluator`` class also has a ``sort_best_candidate()`` method -that orders the applicable candidates by preference, and then returns the -best (i.e. most preferred). +that returns the best (i.e. most preferred) candidate. Finally, the class has a ``compute_best_candidate()`` method that calls ``get_applicable_candidates()`` followed by ``sort_best_candidate()``, and From 419ddf4d7cb87e95330ec0dba2c39538be7c26cd Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 10 Nov 2019 15:37:14 +0530 Subject: [PATCH 0756/3170] Get rid of TASK_NAME --- tools/automation/vendoring/__init__.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tools/automation/vendoring/__init__.py b/tools/automation/vendoring/__init__.py index ae499ebecff..e5ee2132ef1 100644 --- a/tools/automation/vendoring/__init__.py +++ b/tools/automation/vendoring/__init__.py @@ -13,8 +13,6 @@ import invoke import requests -TASK_NAME = 'update' - FILE_WHITE_LIST = ( 'Makefile', 'vendor.txt', @@ -49,7 +47,7 @@ def remove_all(paths): def log(msg): - print('[vendoring.%s] %s' % (TASK_NAME, msg)) + print('[vendoring.update] ' + msg) def _get_vendor_dir(ctx): @@ -299,7 +297,7 @@ def update_stubs(ctx): f_path.write_text("from %s import *" % selector) -@invoke.task(name=TASK_NAME, post=[update_stubs]) +@invoke.task(name="update", post=[update_stubs]) def main(ctx): vendor_dir = _get_vendor_dir(ctx) log('Using vendor dir: %s' % vendor_dir) From d8d948de64ae60153fe3d00e416d32b1293d94a6 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 10 Nov 2019 15:39:49 +0530 Subject: [PATCH 0757/3170] Make it clearer where licenses are coming from --- tools/automation/vendoring/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/automation/vendoring/__init__.py b/tools/automation/vendoring/__init__.py index e5ee2132ef1..873eaa98ea7 100644 --- a/tools/automation/vendoring/__init__.py +++ b/tools/automation/vendoring/__init__.py @@ -27,10 +27,10 @@ } # from time to time, remove the no longer needed ones +_github_license = "https://github.com/{}/raw/master/LICENSE" HARDCODED_LICENSE_URLS = { - 'pytoml': 'https://github.com/avakar/pytoml/raw/master/LICENSE', - 'webencodings': 'https://github.com/SimonSapin/python-webencodings/raw/' - 'master/LICENSE', + 'pytoml': _github_license.format('avakar/pytoml'), + 'webencodings': _github_license.format('SimonSapin/python-webencodings') } From 25d98529dd1e08af590db1258cf08818d533d929 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 10 Nov 2019 16:53:30 +0530 Subject: [PATCH 0758/3170] Move logic for generating stubs into dedicated module --- tools/automation/vendoring/__init__.py | 30 ++------------------ tools/automation/vendoring/typing.py | 38 ++++++++++++++++++++++++++ 2 files changed, 41 insertions(+), 27 deletions(-) create mode 100644 tools/automation/vendoring/typing.py diff --git a/tools/automation/vendoring/__init__.py b/tools/automation/vendoring/__init__.py index 873eaa98ea7..3544d4251b3 100644 --- a/tools/automation/vendoring/__init__.py +++ b/tools/automation/vendoring/__init__.py @@ -10,6 +10,8 @@ import zipfile from pathlib import Path +from .typing import generate_stubs + import invoke import requests @@ -268,33 +270,7 @@ def update_stubs(ctx): vendored_libs = detect_vendored_libs(vendor_dir) print("[vendoring.update_stubs] Add mypy stubs") - - extra_stubs_needed = { - # Some projects need stubs other than a simple <name>.pyi - "six": [ - "six.__init__", - "six.moves.__init__", - "six.moves.configparser", - ], - # Some projects should not have stubs coz they're single file modules - "appdirs": [], - "contextlib2": [], - } - - 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) + generate_stubs(vendor_dir, vendored_libs) @invoke.task(name="update", post=[update_stubs]) diff --git a/tools/automation/vendoring/typing.py b/tools/automation/vendoring/typing.py new file mode 100644 index 00000000000..f5bde0fb82e --- /dev/null +++ b/tools/automation/vendoring/typing.py @@ -0,0 +1,38 @@ +"""Logic for adding static typing related stubs of vendored dependencies. + +We autogenerate `.pyi` stub files for the vendored modules, when vendoring. +These .pyi files are not distributed (thanks to MANIFEST.in). The stub files +are merely `from ... import *` but they do what they're supposed to and mypy +is able to find the correct declarations using these files. +""" + +import os + +extra_stubs_needed = { + # Some projects need stubs other than a simple <name>.pyi + "six": [ + "six.__init__", + "six.moves.__init__", + "six.moves.configparser", + ], + # Some projects should not have stubs coz they're single file modules + "appdirs": [], + "contextlib2": [], +} + + +def generate_stubs(vendor_dir, vendored_libs): + 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) From ef53667aebd912b305fdfe54172a1c9f35368127 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 10 Nov 2019 16:56:07 +0530 Subject: [PATCH 0759/3170] Rework mypy stub generation --- tools/automation/vendoring/__init__.py | 5 ++- tools/automation/vendoring/typing.py | 49 ++++++++++++++++++-------- 2 files changed, 37 insertions(+), 17 deletions(-) diff --git a/tools/automation/vendoring/__init__.py b/tools/automation/vendoring/__init__.py index 3544d4251b3..01ee4ae713f 100644 --- a/tools/automation/vendoring/__init__.py +++ b/tools/automation/vendoring/__init__.py @@ -3,18 +3,17 @@ # The following comment should be removed at some point in the future. # mypy: disallow-untyped-defs=False -import os import re import shutil import tarfile import zipfile from pathlib import Path -from .typing import generate_stubs - import invoke import requests +from .typing import generate_stubs + FILE_WHITE_LIST = ( 'Makefile', 'vendor.txt', diff --git a/tools/automation/vendoring/typing.py b/tools/automation/vendoring/typing.py index f5bde0fb82e..1cdc326f430 100644 --- a/tools/automation/vendoring/typing.py +++ b/tools/automation/vendoring/typing.py @@ -7,6 +7,8 @@ """ import os +from pathlib import Path +from typing import Dict, Iterable, List, Tuple extra_stubs_needed = { # Some projects need stubs other than a simple <name>.pyi @@ -18,21 +20,40 @@ # Some projects should not have stubs coz they're single file modules "appdirs": [], "contextlib2": [], -} +} # type: Dict[str, List[str]] -def generate_stubs(vendor_dir, vendored_libs): - for lib in vendored_libs: - if lib not in extra_stubs_needed: - (vendor_dir / (lib + ".pyi")).write_text("from %s import *" % lib) - continue +def determine_stub_files(lib): + # type: (str) -> Iterable[Tuple[str, str]] + # There's no special handling needed -- a <libname>.pyi file is good enough + if lib not in extra_stubs_needed: + yield lib + ".pyi", lib + return - for selector in extra_stubs_needed[lib]: - fname = selector.replace(".", os.sep) + ".pyi" - if selector.endswith(".__init__"): - selector = selector[:-9] + # Need to generate the given stubs, with the correct import names + for import_name in extra_stubs_needed[lib]: + rel_location = import_name.replace(".", os.sep) + ".pyi" - f_path = vendor_dir / fname - if not f_path.parent.exists(): - f_path.parent.mkdir() - f_path.write_text("from %s import *" % selector) + # Writing an __init__.pyi file -> don't import from `pkg.__init__` + if import_name.endswith(".__init__"): + import_name = import_name[:-9] + + yield rel_location, import_name + + +def write_stub(destination, import_name): + # type: (Path, str) -> None + # Create the parent directories if needed. + if not destination.parent.exists(): + destination.parent.mkdir() + + # Write `from ... import *` in the stub file. + destination.write_text("from %s import *" % import_name) + + +def generate_stubs(vendor_dir, libraries): + # type: (Path, List[str]) -> None + for lib in libraries: + for rel_location, import_name in determine_stub_files(lib): + destination = vendor_dir / rel_location + write_stub(destination, import_name) From e3400fda03e77b1ab405c1eccf0f7fc036b3dbec Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 10 Nov 2019 16:56:41 +0530 Subject: [PATCH 0760/3170] Use ALL_CAPS for global variable --- tools/automation/vendoring/typing.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/automation/vendoring/typing.py b/tools/automation/vendoring/typing.py index 1cdc326f430..e424bac87c1 100644 --- a/tools/automation/vendoring/typing.py +++ b/tools/automation/vendoring/typing.py @@ -10,7 +10,7 @@ from pathlib import Path from typing import Dict, Iterable, List, Tuple -extra_stubs_needed = { +EXTRA_STUBS_NEEDED = { # Some projects need stubs other than a simple <name>.pyi "six": [ "six.__init__", @@ -26,12 +26,12 @@ def determine_stub_files(lib): # type: (str) -> Iterable[Tuple[str, str]] # There's no special handling needed -- a <libname>.pyi file is good enough - if lib not in extra_stubs_needed: + if lib not in EXTRA_STUBS_NEEDED: yield lib + ".pyi", lib return # Need to generate the given stubs, with the correct import names - for import_name in extra_stubs_needed[lib]: + for import_name in EXTRA_STUBS_NEEDED[lib]: rel_location = import_name.replace(".", os.sep) + ".pyi" # Writing an __init__.pyi file -> don't import from `pkg.__init__` From 85083f008b63ab01d7a67edc40e9643ea8e69cf9 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 10 Nov 2019 16:57:09 +0530 Subject: [PATCH 0761/3170] :art: nicer grammer in a comment --- tools/automation/vendoring/typing.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/automation/vendoring/typing.py b/tools/automation/vendoring/typing.py index e424bac87c1..35f7e0bfff0 100644 --- a/tools/automation/vendoring/typing.py +++ b/tools/automation/vendoring/typing.py @@ -17,7 +17,7 @@ "six.moves.__init__", "six.moves.configparser", ], - # Some projects should not have stubs coz they're single file modules + # Some projects should not have stubs because they're a single module "appdirs": [], "contextlib2": [], } # type: Dict[str, List[str]] From 143608bd149823ab3e986b2a116eb6c45d7e5678 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 10 Nov 2019 17:56:59 -0500 Subject: [PATCH 0762/3170] Remove manual decoding in network.session in favor of six method --- src/pip/_internal/network/session.py | 17 ++++------------- 1 file changed, 4 insertions(+), 13 deletions(-) diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py index 4fa57c8683e..8eb4d88349c 100644 --- a/src/pip/_internal/network/session.py +++ b/src/pip/_internal/network/session.py @@ -361,22 +361,13 @@ def is_secure_origin(self, location): continue try: - # We need to do this decode dance to ensure that we have a - # unicode object, even on Python 2.x. addr = ipaddress.ip_address( - origin_host - if ( - isinstance(origin_host, six.text_type) or - origin_host is None - ) - else origin_host.decode("utf8") + None + if origin_host is None + else six.ensure_text(origin_host) ) network = ipaddress.ip_network( - secure_host - if isinstance(secure_host, six.text_type) - # setting secure_host to proper Union[bytes, str] - # creates problems in other places - else secure_host.decode("utf8") # type: ignore + six.ensure_text(secure_host) ) except ValueError: # We don't have both a valid address or a valid network, so From 19397d0d2e60f559ea695e4e54a0d2681c38d913 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 10 Nov 2019 18:05:10 -0500 Subject: [PATCH 0763/3170] Prefer six.ensure_str over our own version --- src/pip/_internal/req/req_install.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index c184384eab4..1376c892b9f 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -28,7 +28,6 @@ generate_metadata as generate_metadata_legacy from pip._internal.pyproject import load_pyproject_toml, make_pyproject_path from pip._internal.req.req_uninstall import UninstallPathSet -from pip._internal.utils.compat import native_str from pip._internal.utils.deprecation import deprecated from pip._internal.utils.hashes import Hashes from pip._internal.utils.logging import indent_log @@ -272,7 +271,7 @@ def name(self): # type: () -> Optional[str] if self.req is None: return None - return native_str(pkg_resources.safe_name(self.req.name)) + return six.ensure_str(pkg_resources.safe_name(self.req.name)) @property def specifier(self): From 0a6305a39813234ff0aff62524eb0609c5ee1a55 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 10 Nov 2019 18:05:25 -0500 Subject: [PATCH 0764/3170] Remove unused compat.native_str --- src/pip/_internal/utils/compat.py | 18 +----------------- tests/unit/test_compat.py | 8 -------- 2 files changed, 1 insertion(+), 25 deletions(-) diff --git a/src/pip/_internal/utils/compat.py b/src/pip/_internal/utils/compat.py index dbd84487559..0468b2b14ea 100644 --- a/src/pip/_internal/utils/compat.py +++ b/src/pip/_internal/utils/compat.py @@ -41,7 +41,7 @@ __all__ = [ - "ipaddress", "uses_pycache", "console_to_str", "native_str", + "ipaddress", "uses_pycache", "console_to_str", "get_path_uid", "stdlib_pkgs", "WINDOWS", "samefile", "get_terminal_size", "get_extension_suffixes", ] @@ -159,22 +159,6 @@ def console_to_str(data): return str_to_display(data, desc='Subprocess output') -if PY2: - def native_str(s, replace=False): - # type: (str, bool) -> str - # Replace is ignored -- unicode to UTF-8 can't fail - if isinstance(s, text_type): - return s.encode('utf-8') - return s - -else: - def native_str(s, replace=False): - # type: (str, bool) -> str - if isinstance(s, bytes): - return s.decode('utf-8', 'replace' if replace else 'strict') - return s - - def get_path_uid(path): # type: (str) -> int """ diff --git a/tests/unit/test_compat.py b/tests/unit/test_compat.py index c47b8c487ae..634aacc40c2 100644 --- a/tests/unit/test_compat.py +++ b/tests/unit/test_compat.py @@ -10,7 +10,6 @@ console_to_str, expanduser, get_path_uid, - native_str, str_to_display, ) @@ -128,13 +127,6 @@ def check_warning(msg, *args, **kwargs): console_to_str(some_bytes) -def test_to_native_str_type(): - some_bytes = b"test\xE9 et approuv\xC3\xE9" - some_unicode = b"test\xE9 et approuv\xE9".decode('iso-8859-15') - assert isinstance(native_str(some_bytes, True), str) - assert isinstance(native_str(some_unicode, True), str) - - @pytest.mark.parametrize("home,path,expanded", [ ("/Users/test", "~", "/Users/test"), ("/Users/test", "~/.cache", "/Users/test/.cache"), From dd0ba3d18540fb6227aefb36b15c651d0ed024ed Mon Sep 17 00:00:00 2001 From: Kai Chen <kaichen120@gmail.com> Date: Sun, 31 Mar 2019 19:56:13 -0700 Subject: [PATCH 0765/3170] Document enabling --no-warn-script-location with falsy values Closes https://github.com/pypa/pip/issues/6209 --- docs/html/user_guide.rst | 5 +++-- news/31044E84-3F3C-48A8-84B2-6028E21FEBF1.trivial | 0 2 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 news/31044E84-3F3C-48A8-84B2-6028E21FEBF1.trivial diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 9b7e7ed4a0f..4d0477d0d20 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -396,8 +396,8 @@ set like this: ignore-installed = true no-dependencies = yes -To enable the boolean options ``--no-compile`` and ``--no-cache-dir``, falsy -values have to be used: +To enable the boolean options ``--no-compile``, ``--no-warn-script-location`` +and ``--no-cache-dir``, falsy values have to be used: .. code-block:: ini @@ -406,6 +406,7 @@ values have to be used: [install] no-compile = no + no-warn-script-location = false Appending options like ``--find-links`` can be written on multiple lines: diff --git a/news/31044E84-3F3C-48A8-84B2-6028E21FEBF1.trivial b/news/31044E84-3F3C-48A8-84B2-6028E21FEBF1.trivial new file mode 100644 index 00000000000..e69de29bb2d From 024038cf1059899503a5e7e57ce3b00bf6d8add9 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek <chris.jerdonek@gmail.com> Date: Mon, 23 Sep 2019 06:20:03 -0700 Subject: [PATCH 0766/3170] Add LinkCollector.fetch_page(). --- src/pip/_internal/index/collector.py | 9 ++++++++- tests/unit/test_collector.py | 25 +++++++++++++++++++++++-- 2 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/index/collector.py b/src/pip/_internal/index/collector.py index e6ee598e806..abf45000247 100644 --- a/src/pip/_internal/index/collector.py +++ b/src/pip/_internal/index/collector.py @@ -483,6 +483,13 @@ def find_links(self): # type: () -> List[str] return self.search_scope.find_links + def fetch_page(self, location): + # type: (Link) -> Optional[HTMLPage] + """ + Fetch an HTML page containing package links. + """ + return _get_html_page(location, session=self.session) + def _get_pages(self, locations): # type: (Iterable[Link]) -> Iterable[HTMLPage] """ @@ -490,7 +497,7 @@ def _get_pages(self, locations): locations that have errors. """ for location in locations: - page = _get_html_page(location, session=self.session) + page = self.fetch_page(location) if page is None: continue diff --git a/tests/unit/test_collector.py b/tests/unit/test_collector.py index cf709c99bcf..f95ebd8ba2a 100644 --- a/tests/unit/test_collector.py +++ b/tests/unit/test_collector.py @@ -435,14 +435,35 @@ def check_links_include(links, names): class TestLinkCollector(object): + @patch('pip._internal.index.collector._get_html_response') + def test_fetch_page(self, mock_get_html_response): + url = 'https://pypi.org/simple/twine/' + + fake_response = make_fake_html_response(url) + mock_get_html_response.return_value = fake_response + + location = Link(url) + link_collector = make_test_link_collector() + actual = link_collector.fetch_page(location) + + assert actual.content == fake_response.content + assert actual.encoding is None + assert actual.url == url + + # Also check that the right session object was passed to + # _get_html_response(). + mock_get_html_response.assert_called_once_with( + url, session=link_collector.session, + ) + @patch('pip._internal.index.collector._get_html_response') def test_collect_links(self, mock_get_html_response, caplog, data): caplog.set_level(logging.DEBUG) expected_url = 'https://pypi.org/simple/twine/' - fake_page = make_fake_html_response(expected_url) - mock_get_html_response.return_value = fake_page + fake_response = make_fake_html_response(expected_url) + mock_get_html_response.return_value = fake_response link_collector = make_test_link_collector( find_links=[data.find_links], From bab1e4f8a1a3ba5f5f08207c83a4e0a7a87ea615 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek <chris.jerdonek@gmail.com> Date: Mon, 23 Sep 2019 06:53:58 -0700 Subject: [PATCH 0767/3170] Change CollectedLinks to store project_urls. --- src/pip/_internal/index/collector.py | 46 +++++++++-------------- src/pip/_internal/index/package_finder.py | 16 ++++++-- tests/functional/test_install_config.py | 6 +-- tests/unit/test_collector.py | 20 +--------- 4 files changed, 36 insertions(+), 52 deletions(-) diff --git a/src/pip/_internal/index/collector.py b/src/pip/_internal/index/collector.py index abf45000247..b390cdc8227 100644 --- a/src/pip/_internal/index/collector.py +++ b/src/pip/_internal/index/collector.py @@ -27,8 +27,8 @@ if MYPY_CHECK_RUNNING: from typing import ( - Callable, Dict, Iterable, List, MutableMapping, Optional, Sequence, - Tuple, Union, + Callable, Iterable, List, MutableMapping, Optional, Sequence, Tuple, + Union, ) import xml.etree.ElementTree @@ -435,29 +435,36 @@ def sort_path(path): class CollectedLinks(object): """ - Encapsulates all the Link objects collected by a call to - LinkCollector.collect_links(), stored separately as-- + Encapsulates the return value of a call to LinkCollector.collect_links(). + + The return value includes both URLs to project pages containing package + links, as well as individual package Link objects collected from other + sources. + + This info is stored separately as: (1) links from the configured file locations, (2) links from the configured find_links, and - (3) a dict mapping HTML page url to links from that page. + (3) urls to HTML project pages, as described by the PEP 503 simple + repository API. """ def __init__( self, - files, # type: List[Link] - find_links, # type: List[Link] - pages, # type: Dict[str, List[Link]] + files, # type: List[Link] + find_links, # type: List[Link] + project_urls, # type: List[Link] ): # type: (...) -> None """ :param files: Links from file locations. :param find_links: Links from find_links. - :param pages: A dict mapping HTML page url to links from that page. + :param project_urls: URLs to HTML project pages, as described by + the PEP 503 simple repository API. """ self.files = files self.find_links = find_links - self.pages = pages + self.project_urls = project_urls class LinkCollector(object): @@ -490,19 +497,6 @@ def fetch_page(self, location): """ return _get_html_page(location, session=self.session) - def _get_pages(self, locations): - # type: (Iterable[Link]) -> Iterable[HTMLPage] - """ - Yields (page, page_url) from the given locations, skipping - locations that have errors. - """ - for location in locations: - page = self.fetch_page(location) - if page is None: - continue - - yield page - def collect_links(self, project_name): # type: (str) -> CollectedLinks """Find all available links for the given project name. @@ -544,12 +538,8 @@ def collect_links(self, project_name): lines.append('* {}'.format(link)) logger.debug('\n'.join(lines)) - pages_links = {} - for page in self._get_pages(url_locations): - pages_links[page.url] = list(parse_links(page)) - return CollectedLinks( files=file_links, find_links=find_link_links, - pages=pages_links, + project_urls=url_locations, ) diff --git a/src/pip/_internal/index/package_finder.py b/src/pip/_internal/index/package_finder.py index 9b338e693be..36cf91893ea 100644 --- a/src/pip/_internal/index/package_finder.py +++ b/src/pip/_internal/index/package_finder.py @@ -19,6 +19,7 @@ InvalidWheelFilename, UnsupportedWheel, ) +from pip._internal.index.collector import parse_links from pip._internal.models.candidate import InstallationCandidate from pip._internal.models.format_control import FormatControl from pip._internal.models.link import Link @@ -788,7 +789,8 @@ def find_all_candidates(self, project_name): See LinkEvaluator.evaluate_link() for details on which files are accepted. """ - collected_links = self._link_collector.collect_links(project_name) + link_collector = self._link_collector + collected_links = link_collector.collect_links(project_name) link_evaluator = self.make_link_evaluator(project_name) @@ -798,8 +800,16 @@ def find_all_candidates(self, project_name): ) page_versions = [] - for page_url, page_links in collected_links.pages.items(): - logger.debug('Analyzing links from page %s', page_url) + for project_url in collected_links.project_urls: + logger.debug( + 'Fetching project page and analyzing links: %s', project_url, + ) + html_page = link_collector.fetch_page(project_url) + if html_page is None: + continue + + page_links = list(parse_links(html_page)) + with indent_log(): new_versions = self.evaluate_links( link_evaluator, diff --git a/tests/functional/test_install_config.py b/tests/functional/test_install_config.py index bcf83f163a8..176976c4e46 100644 --- a/tests/functional/test_install_config.py +++ b/tests/functional/test_install_config.py @@ -91,7 +91,7 @@ def test_command_line_append_flags(script, virtualenv, data): 'test.pypi.org', ) assert ( - "Analyzing links from page https://test.pypi.org" + "Fetching project page and analyzing links: https://test.pypi.org" in result.stdout ), str(result) virtualenv.clear() @@ -100,7 +100,7 @@ def test_command_line_append_flags(script, virtualenv, data): '--trusted-host', 'test.pypi.org', ) assert ( - "Analyzing links from page https://test.pypi.org" + "Fetching project page and analyzing links: https://test.pypi.org" in result.stdout ) assert ( @@ -124,7 +124,7 @@ def test_command_line_appends_correctly(script, data): ) assert ( - "Analyzing links from page https://test.pypi.org" + "Fetching project page and analyzing links: https://test.pypi.org" in result.stdout ), result.stdout assert ( diff --git a/tests/unit/test_collector.py b/tests/unit/test_collector.py index f95ebd8ba2a..e266ea16342 100644 --- a/tests/unit/test_collector.py +++ b/tests/unit/test_collector.py @@ -456,15 +456,9 @@ def test_fetch_page(self, mock_get_html_response): url, session=link_collector.session, ) - @patch('pip._internal.index.collector._get_html_response') - def test_collect_links(self, mock_get_html_response, caplog, data): + def test_collect_links(self, caplog, data): caplog.set_level(logging.DEBUG) - expected_url = 'https://pypi.org/simple/twine/' - - fake_response = make_fake_html_response(expected_url) - mock_get_html_response.return_value = fake_response - link_collector = make_test_link_collector( find_links=[data.find_links], # Include two copies of the URL to check that the second one @@ -473,10 +467,6 @@ def test_collect_links(self, mock_get_html_response, caplog, data): ) actual = link_collector.collect_links('twine') - mock_get_html_response.assert_called_once_with( - expected_url, session=link_collector.session, - ) - # Spot-check the CollectedLinks return value. assert len(actual.files) > 20 check_links_include(actual.files, names=['simple-1.0.tar.gz']) @@ -484,13 +474,7 @@ def test_collect_links(self, mock_get_html_response, caplog, data): assert len(actual.find_links) == 1 check_links_include(actual.find_links, names=['packages']) - actual_pages = actual.pages - assert list(actual_pages) == [expected_url] - actual_page_links = actual_pages[expected_url] - assert len(actual_page_links) == 1 - assert actual_page_links[0].url == ( - 'https://pypi.org/abc-1.0.tar.gz#md5=000000000' - ) + assert actual.project_urls == [Link('https://pypi.org/simple/twine/')] expected_message = dedent("""\ 1 location(s) to search for versions of twine: From f4cad3d403e0b449be229d023f1655ea1a038136 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek <chris.jerdonek@gmail.com> Date: Mon, 23 Sep 2019 07:29:15 -0700 Subject: [PATCH 0768/3170] Add PackageFinder.process_project_url(), and test. --- src/pip/_internal/index/package_finder.py | 39 +++++++++++++---------- tests/unit/test_finder.py | 16 ++++++++++ 2 files changed, 39 insertions(+), 16 deletions(-) diff --git a/src/pip/_internal/index/package_finder.py b/src/pip/_internal/index/package_finder.py index 36cf91893ea..5be1c14881e 100644 --- a/src/pip/_internal/index/package_finder.py +++ b/src/pip/_internal/index/package_finder.py @@ -779,6 +779,25 @@ def evaluate_links(self, link_evaluator, links): return candidates + def process_project_url(self, project_url, link_evaluator): + # type: (Link, LinkEvaluator) -> List[InstallationCandidate] + logger.debug( + 'Fetching project page and analyzing links: %s', project_url, + ) + html_page = self._link_collector.fetch_page(project_url) + if html_page is None: + return [] + + page_links = list(parse_links(html_page)) + + with indent_log(): + package_links = self.evaluate_links( + link_evaluator, + links=page_links, + ) + + return package_links + def find_all_candidates(self, project_name): # type: (str) -> List[InstallationCandidate] """Find all available InstallationCandidate for project_name @@ -789,8 +808,7 @@ def find_all_candidates(self, project_name): See LinkEvaluator.evaluate_link() for details on which files are accepted. """ - link_collector = self._link_collector - collected_links = link_collector.collect_links(project_name) + collected_links = self._link_collector.collect_links(project_name) link_evaluator = self.make_link_evaluator(project_name) @@ -801,21 +819,10 @@ def find_all_candidates(self, project_name): page_versions = [] for project_url in collected_links.project_urls: - logger.debug( - 'Fetching project page and analyzing links: %s', project_url, + package_links = self.process_project_url( + project_url, link_evaluator=link_evaluator, ) - html_page = link_collector.fetch_page(project_url) - if html_page is None: - continue - - page_links = list(parse_links(html_page)) - - with indent_log(): - new_versions = self.evaluate_links( - link_evaluator, - links=page_links, - ) - page_versions.extend(new_versions) + page_versions.extend(package_links) file_versions = self.evaluate_links( link_evaluator, diff --git a/tests/unit/test_finder.py b/tests/unit/test_finder.py index e7bf13e02bb..a3b0da39ae2 100644 --- a/tests/unit/test_finder.py +++ b/tests/unit/test_finder.py @@ -473,6 +473,22 @@ def test_evaluate_link__substring_fails(self, url, expected_msg): assert actual == (False, expected_msg) +def test_process_project_url(data): + project_name = 'simple' + index_url = data.index_url('simple') + project_url = Link('{}/{}'.format(index_url, project_name)) + finder = make_test_finder(index_urls=[index_url]) + link_evaluator = finder.make_link_evaluator(project_name) + actual = finder.process_project_url( + project_url, link_evaluator=link_evaluator, + ) + + assert len(actual) == 1 + package_link = actual[0] + assert package_link.project == 'simple' + assert str(package_link.version) == '1.0' + + def test_find_all_candidates_nothing(): """Find nothing without anything""" finder = make_test_finder() From 9bd0db2945be86bf9a9acc761cf6afec4f86df63 Mon Sep 17 00:00:00 2001 From: Chris Jerdonek <chris.jerdonek@gmail.com> Date: Tue, 24 Sep 2019 00:18:35 -0700 Subject: [PATCH 0769/3170] Update the PackageFinder architecture document. --- .../architecture/package-finding.rst | 39 +++++++++++-------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/docs/html/development/architecture/package-finding.rst b/docs/html/development/architecture/package-finding.rst index 53ece59f8d9..de8b1e8b1d6 100644 --- a/docs/html/development/architecture/package-finding.rst +++ b/docs/html/development/architecture/package-finding.rst @@ -15,17 +15,16 @@ Overview Here is a rough description of the process that pip uses to choose what file to download for a package, given a requirement: -1. Access the various network and file system locations configured for pip - that contain package files. These locations can include, for example, - pip's :ref:`--index-url <--index-url>` (with default - https://pypi.org/simple/ ) and any configured - :ref:`--extra-index-url <--extra-index-url>` locations. - Each of these locations is a `PEP 503`_ "simple repository" page, which - is an HTML page of anchor links. -2. Collect together all of the links (e.g. by parsing the anchor links - from the HTML pages) and create ``Link`` objects from each of these. - The :ref:`LinkCollector <link-collector-class>` class is responsible - for both this step and the previous. +1. Collect together the various network and file system locations containing + project package files. These locations are derived, for example, from pip's + :ref:`--index-url <--index-url>` (with default https://pypi.org/simple/ ) + setting and any configured :ref:`--extra-index-url <--extra-index-url>` + locations. Each of the project page URL's is an HTML page of anchor links, + as defined in `PEP 503`_, the "Simple Repository API." +2. For each project page URL, fetch the HTML and parse out the anchor links, + creating a ``Link`` object from each one. The :ref:`LinkCollector + <link-collector-class>` class is responsible for both the previous step + and fetching the HTML over the network. 3. Determine which of the links are minimally relevant, using the :ref:`LinkEvaluator <link-evaluator-class>` class. Create an ``InstallationCandidate`` object (aka candidate for install) for each @@ -111,6 +110,12 @@ One of ``PackageFinder``'s main top-level methods is class's ``compute_best_candidate()`` method on the return value of ``find_all_candidates()``. This corresponds to steps 4-5 of the Overview. +``PackageFinder`` also has a ``process_project_url()`` method (called by +``find_best_candidate()``) to process a `PEP 503`_ "simple repository" +project page. This method fetches and parses the HTML from a PEP 503 project +page URL, extracts the anchor elements and creates ``Link`` objects from +them, and then evaluates those links. + .. _link-collector-class: @@ -119,12 +124,8 @@ The ``LinkCollector`` class The :ref:`LinkCollector <link-collector-class>` class is the class responsible for collecting the raw list of "links" to package files -(represented as ``Link`` objects). An instance of the class accesses the -various `PEP 503`_ HTML "simple repository" pages, parses their HTML, -extracts the links from the anchor elements, and creates ``Link`` objects -from that information. The ``LinkCollector`` class is "unintelligent" in that -it doesn't do any evaluation of whether the links are relevant to the -original requirement; it just collects them. +(represented as ``Link`` objects) from file system locations, as well as the +`PEP 503`_ project page URL's that ``PackageFinder`` should access. The ``LinkCollector`` class takes into account the user's :ref:`--find-links <--find-links>`, :ref:`--extra-index-url <--extra-index-url>`, and related @@ -133,6 +134,10 @@ method is the ``collect_links()`` method. The :ref:`PackageFinder <package-finder-class>` class invokes this method as the first step of its ``find_all_candidates()`` method. +``LinkCollector`` also has a ``fetch_page()`` method to fetch the HTML from a +project page URL. This method is "unintelligent" in that it doesn't parse the +HTML. + The ``LinkCollector`` class is the only class in the ``index`` sub-package that makes network requests and is the only class in the sub-package that depends directly on ``PipSession``, which stores pip's configuration options and From b7706798ae714898deba22c958b5b36e160821bd Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 11 Nov 2019 11:25:01 +0530 Subject: [PATCH 0770/3170] Revert "Make it clearer where licenses are coming from" This reverts commit d8d948de64ae60153fe3d00e416d32b1293d94a6. --- tools/automation/vendoring/__init__.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tools/automation/vendoring/__init__.py b/tools/automation/vendoring/__init__.py index 01ee4ae713f..1c2ae3a6948 100644 --- a/tools/automation/vendoring/__init__.py +++ b/tools/automation/vendoring/__init__.py @@ -28,10 +28,10 @@ } # from time to time, remove the no longer needed ones -_github_license = "https://github.com/{}/raw/master/LICENSE" HARDCODED_LICENSE_URLS = { - 'pytoml': _github_license.format('avakar/pytoml'), - 'webencodings': _github_license.format('SimonSapin/python-webencodings') + 'pytoml': 'https://github.com/avakar/pytoml/raw/master/LICENSE', + 'webencodings': 'https://github.com/SimonSapin/python-webencodings/raw/' + 'master/LICENSE', } From 9b25cd512b0c3c2611c280bb0bcd4202e81246e1 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sat, 9 Nov 2019 11:47:03 +0530 Subject: [PATCH 0771/3170] Rename InstallationCandidate.{project -> name} --- src/pip/_internal/index/package_finder.py | 2 +- src/pip/_internal/models/candidate.py | 13 ++++++------- tests/unit/test_finder.py | 2 +- tests/unit/test_models.py | 4 ++-- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/pip/_internal/index/package_finder.py b/src/pip/_internal/index/package_finder.py index 5be1c14881e..188f571c7ab 100644 --- a/src/pip/_internal/index/package_finder.py +++ b/src/pip/_internal/index/package_finder.py @@ -759,7 +759,7 @@ def get_install_candidate(self, link_evaluator, link): return None return InstallationCandidate( - project=link_evaluator.project_name, + name=link_evaluator.project_name, link=link, # Convert the Text result to str since InstallationCandidate # accepts str. diff --git a/src/pip/_internal/models/candidate.py b/src/pip/_internal/models/candidate.py index 4d49604ddfc..850825f6aba 100644 --- a/src/pip/_internal/models/candidate.py +++ b/src/pip/_internal/models/candidate.py @@ -9,31 +9,30 @@ if MYPY_CHECK_RUNNING: from pip._vendor.packaging.version import _BaseVersion from pip._internal.models.link import Link - from typing import Any class InstallationCandidate(KeyBasedCompareMixin): """Represents a potential "candidate" for installation. """ - def __init__(self, project, version, link): - # type: (Any, str, Link) -> None - self.project = project + def __init__(self, name, version, link): + # type: (str, str, Link) -> None + self.name = name self.version = parse_version(version) # type: _BaseVersion self.link = link super(InstallationCandidate, self).__init__( - key=(self.project, self.version, self.link), + key=(self.name, self.version, self.link), defining_class=InstallationCandidate ) def __repr__(self): # type: () -> str return "<InstallationCandidate({!r}, {!r}, {!r})>".format( - self.project, self.version, self.link, + self.name, self.version, self.link, ) def __str__(self): return '{!r} candidate (version {} at {})'.format( - self.project, self.version, self.link, + self.name, self.version, self.link, ) diff --git a/tests/unit/test_finder.py b/tests/unit/test_finder.py index a3b0da39ae2..ddfcc7860ed 100644 --- a/tests/unit/test_finder.py +++ b/tests/unit/test_finder.py @@ -485,7 +485,7 @@ def test_process_project_url(data): assert len(actual) == 1 package_link = actual[0] - assert package_link.project == 'simple' + assert package_link.name == 'simple' assert str(package_link.version) == '1.0' diff --git a/tests/unit/test_models.py b/tests/unit/test_models.py index c922dc773cb..f6363367755 100644 --- a/tests/unit/test_models.py +++ b/tests/unit/test_models.py @@ -47,7 +47,7 @@ def test_sets_correct_variables(self): obj = candidate.InstallationCandidate( "A", "1.0.0", "https://somewhere.com/path/A-1.0.0.tar.gz" ) - assert obj.project == "A" + assert obj.name == "A" assert obj.version == parse_version("1.0.0") assert obj.link == "https://somewhere.com/path/A-1.0.0.tar.gz" @@ -57,4 +57,4 @@ def test_sets_the_right_key(self): obj = candidate.InstallationCandidate( "A", "1.0.0", "https://somewhere.com/path/A-1.0.0.tar.gz" ) - assert obj._compare_key == (obj.project, obj.version, obj.link) + assert obj._compare_key == (obj.name, obj.version, obj.link) From 4127beeb502db3a05e1c7dd916b1a69ca0592361 Mon Sep 17 00:00:00 2001 From: Hugo <hugovk@users.noreply.github.com> Date: Sat, 9 Nov 2019 14:22:41 +0200 Subject: [PATCH 0772/3170] Test Linux and macOS on Python 3.8 --- .azure-pipelines/jobs/test.yml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.azure-pipelines/jobs/test.yml b/.azure-pipelines/jobs/test.yml index cadf99a51f7..68b6e5268e1 100644 --- a/.azure-pipelines/jobs/test.yml +++ b/.azure-pipelines/jobs/test.yml @@ -35,7 +35,10 @@ jobs: Python37: python.version: '3.7' python.architecture: x64 - maxParallel: 3 + Python38: + python.version: '3.8' + python.architecture: x64 + maxParallel: 4 steps: - template: ../steps/run-tests.yml From f2bde8038c3556ae421a34fc2df225f7ff16ab98 Mon Sep 17 00:00:00 2001 From: Hanjun Kim <hallazzang@gmail.com> Date: Mon, 11 Nov 2019 23:08:35 +0900 Subject: [PATCH 0773/3170] Fix typo in warning message Option names in the message should match with actual arguments. --- src/pip/_internal/cli/cmdoptions.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index d7c6e34b201..6e4d0eac506 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -83,8 +83,8 @@ def getname(n): control = options.format_control control.disallow_binaries() warnings.warn( - 'Disabling all use of wheels due to the use of --build-options ' - '/ --global-options / --install-options.', stacklevel=2, + 'Disabling all use of wheels due to the use of --build-option ' + '/ --global-option / --install-option.', stacklevel=2, ) From 12b58ed8e0be0fe97bc1ff9479797ebce2b20d84 Mon Sep 17 00:00:00 2001 From: Hanjun Kim <hallazzang@gmail.com> Date: Mon, 11 Nov 2019 23:15:46 +0900 Subject: [PATCH 0774/3170] create news entry --- news/7340.bugfix | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 news/7340.bugfix diff --git a/news/7340.bugfix b/news/7340.bugfix new file mode 100644 index 00000000000..ca6332d1712 --- /dev/null +++ b/news/7340.bugfix @@ -0,0 +1,2 @@ +Fix typo in warning message when any of ``--build-option``, ``--global-option`` +and ``--install-option`` is used in requirements.txt From 530c1d73441f4a70e1f2dbed52223c30b1203174 Mon Sep 17 00:00:00 2001 From: Jiashuo Li <jiasli@microsoft.com> Date: Tue, 12 Nov 2019 10:32:21 +0800 Subject: [PATCH 0775/3170] Add double quotes to pip install from VCS --- docs/html/reference/pip_install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index 5cbb139064b..ceb5302c2ed 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -387,7 +387,7 @@ So if your repository layout is: - some_file - some_other_file -You'll need to use ``pip install -e vcs+protocol://repo_url/#egg=pkg&subdirectory=pkg_dir``. +You'll need to use ``pip install -e "vcs+protocol://repo_url/#egg=pkg&subdirectory=pkg_dir"``. Git From 94afac6d31d5a665c1da566ecad1899b34777d71 Mon Sep 17 00:00:00 2001 From: Jiashuo Li <jiasli@microsoft.com> Date: Tue, 12 Nov 2019 10:50:26 +0800 Subject: [PATCH 0776/3170] trivial --- news/77a2e30d-d448-43fd-9223-81dff5ae5001.trivial | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 news/77a2e30d-d448-43fd-9223-81dff5ae5001.trivial diff --git a/news/77a2e30d-d448-43fd-9223-81dff5ae5001.trivial b/news/77a2e30d-d448-43fd-9223-81dff5ae5001.trivial new file mode 100644 index 00000000000..e69de29bb2d From 95375a5213ab6abaa2496f26c298642589f7055d Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Mon, 11 Nov 2019 22:34:52 -0500 Subject: [PATCH 0777/3170] Make session a required parameter in operations.prepare --- src/pip/_internal/operations/prepare.py | 18 +++++------------- tests/unit/test_operations_prepare.py | 4 ++-- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index c564d64a7e9..3d153e2fc1f 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -202,17 +202,12 @@ def _copy_file(filename, location, link): def unpack_http_url( link, # type: Link location, # type: str + session, # type: PipSession download_dir=None, # type: Optional[str] - session=None, # type: Optional[PipSession] hashes=None, # type: Optional[Hashes] progress_bar="on" # type: str ): # type: (...) -> None - if session is None: - raise TypeError( - "unpack_http_url() missing 1 required keyword argument: 'session'" - ) - with TempDirectory(kind="unpack") as temp_dir: # If a download dir is specified, is the file already downloaded there? already_downloaded_path = None @@ -340,8 +335,8 @@ def unpack_file_url( def unpack_url( link, # type: Link location, # type: str + session, # type: PipSession download_dir=None, # type: Optional[str] - session=None, # type: Optional[PipSession] hashes=None, # type: Optional[Hashes] progress_bar="on" # type: str ): @@ -370,14 +365,11 @@ def unpack_url( # http urls else: - if session is None: - session = PipSession() - unpack_http_url( link, location, - download_dir, session, + download_dir, hashes=hashes, progress_bar=progress_bar ) @@ -643,8 +635,8 @@ def prepare_linked_requirement( try: unpack_url( - link, req.source_dir, download_dir, - session=self.session, hashes=hashes, + link, req.source_dir, self.session, download_dir, + hashes=hashes, progress_bar=self.progress_bar ) except requests.HTTPError as exc: diff --git a/tests/unit/test_operations_prepare.py b/tests/unit/test_operations_prepare.py index 9241f9ee2ca..d9b3781e9c2 100644 --- a/tests/unit/test_operations_prepare.py +++ b/tests/unit/test_operations_prepare.py @@ -52,8 +52,8 @@ def _fake_session_get(*args, **kwargs): unpack_http_url( link, temp_dir, - download_dir=None, session=session, + download_dir=None, ) assert set(os.listdir(temp_dir)) == { 'PKG-INFO', 'setup.cfg', 'setup.py', 'simple', 'simple.egg-info' @@ -140,8 +140,8 @@ def test_unpack_http_url_bad_downloaded_checksum(mock_unpack_file): unpack_http_url( link, 'location', - download_dir=download_dir, session=session, + download_dir=download_dir, hashes=Hashes({'sha1': [download_hash.hexdigest()]}) ) From 16ffd1a9c45cd36e130c6303cde006be07ba776b Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Mon, 11 Nov 2019 22:36:46 -0500 Subject: [PATCH 0778/3170] Normalize style --- src/pip/_internal/operations/prepare.py | 32 +++++++++++++------------ 1 file changed, 17 insertions(+), 15 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 3d153e2fc1f..b508ebe82c5 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -157,8 +157,9 @@ def written_chunks(chunks): progress_indicator = _progress_indicator if show_progress: # We don't show progress on cached responses - progress_indicator = DownloadProgressProvider(progress_bar, - max=total_length) + progress_indicator = DownloadProgressProvider( + progress_bar, max=total_length + ) downloaded_chunks = written_chunks( progress_indicator( @@ -177,8 +178,11 @@ def _copy_file(filename, location, link): download_location = os.path.join(location, link.filename) if os.path.exists(download_location): response = ask_path_exists( - 'The file %s exists. (i)gnore, (w)ipe, (b)ackup, (a)abort' % - display_path(download_location), ('i', 'w', 'b', 'a')) + 'The file {} exists. (i)gnore, (w)ipe, (b)ackup, (a)abort'.format( + display_path(download_location) + ), + ('i', 'w', 'b', 'a'), + ) if response == 'i': copy = False elif response == 'w': @@ -212,20 +216,18 @@ def unpack_http_url( # If a download dir is specified, is the file already downloaded there? already_downloaded_path = None if download_dir: - already_downloaded_path = _check_download_dir(link, - download_dir, - hashes) + already_downloaded_path = _check_download_dir( + link, download_dir, hashes + ) if already_downloaded_path: from_path = already_downloaded_path 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) + from_path, content_type = _download_http_url( + link, session, temp_dir.path, hashes, progress_bar + ) # unpack the archive to the build dir location. even when only # downloading archives, they have to be unpacked to parse dependencies @@ -312,9 +314,9 @@ def unpack_file_url( # If a download dir is specified, is the file already there and valid? already_downloaded_path = None if download_dir: - already_downloaded_path = _check_download_dir(link, - download_dir, - hashes) + already_downloaded_path = _check_download_dir( + link, download_dir, hashes + ) if already_downloaded_path: from_path = already_downloaded_path From 613cc01c0b2a4fbb2d2c399a20003ec07d91474a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Sun, 10 Nov 2019 21:41:41 +0100 Subject: [PATCH 0779/3170] Include subdirectory URL fragment in the cache key --- news/7333.bugfix | 1 + src/pip/_internal/cache.py | 10 +++++++++ tests/unit/test_cache.py | 45 ++++++++++++++++++++++++++++++++------ 3 files changed, 49 insertions(+), 7 deletions(-) create mode 100644 news/7333.bugfix diff --git a/news/7333.bugfix b/news/7333.bugfix new file mode 100644 index 00000000000..8ddcba76a39 --- /dev/null +++ b/news/7333.bugfix @@ -0,0 +1 @@ +Include ``subdirectory`` URL fragments in cache keys. diff --git a/src/pip/_internal/cache.py b/src/pip/_internal/cache.py index c5431e14d14..b3327ea9a57 100644 --- a/src/pip/_internal/cache.py +++ b/src/pip/_internal/cache.py @@ -58,6 +58,10 @@ def _get_cache_path_parts(self, link): 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])) + if link.subdirectory_fragment: + key_parts.append( + "=".join(["subdirectory", link.subdirectory_fragment]) + ) key_url = "#".join(key_parts) # Encode our key url with sha224, we'll use this because it has similar @@ -168,11 +172,17 @@ def get( # type: (...) -> Link candidates = [] + canonical_package_name = None + if package_name: + canonical_package_name = canonicalize_name(package_name) for wheel_name in self._get_candidates(link, package_name): try: wheel = Wheel(wheel_name) except InvalidWheelFilename: continue + assert canonical_package_name + if wheel.name != canonical_package_name: + continue if not wheel.supported(supported_tags): # Built for a different python/arch/etc continue diff --git a/tests/unit/test_cache.py b/tests/unit/test_cache.py index d75cd2c654f..79e4f624d19 100644 --- a/tests/unit/test_cache.py +++ b/tests/unit/test_cache.py @@ -1,13 +1,44 @@ +import os + from pip._internal.cache import WheelCache +from pip._internal.models.format_control import FormatControl +from pip._internal.models.link import Link from pip._internal.utils.compat import expanduser +from pip._internal.utils.misc import ensure_dir + + +def test_expands_path(): + wc = WheelCache("~/.foo/", None) + assert wc.cache_dir == expanduser("~/.foo/") + + +def test_falsey_path_none(): + wc = WheelCache(False, None) + assert wc.cache_dir is None -class TestWheelCache: +def test_subdirectory_fragment(): + """ + Test the subdirectory URL fragment is part of the cache key. + """ + wc = WheelCache("~/.foo/", None) + link1 = Link("git+https://g.c/o/r#subdirectory=d1") + link2 = Link("git+https://g.c/o/r#subdirectory=d2") + assert wc.get_path_for_link(link1) != wc.get_path_for_link(link2) - def test_expands_path(self): - wc = WheelCache("~/.foo/", None) - assert wc.cache_dir == expanduser("~/.foo/") - def test_falsey_path_none(self): - wc = WheelCache(False, None) - assert wc.cache_dir is None +def test_wheel_name_filter(tmpdir): + """ + Test the wheel cache filters on wheel name when several wheels + for different package are stored under the same cache directory. + """ + wc = WheelCache(tmpdir, FormatControl()) + link = Link("https://g.c/package.tar.gz") + cache_path = wc.get_path_for_link(link) + ensure_dir(cache_path) + with open(os.path.join(cache_path, "package-1.0-py3-none-any.whl"), "w"): + pass + # package matches wheel name + assert wc.get(link, "package", [("py3", "none", "any")]) is not link + # package2 does not match wheel name + assert wc.get(link, "package2", [("py3", "none", "any")]) is link From 0363420239d7a053242e054044182542d0ffa63c Mon Sep 17 00:00:00 2001 From: Hanjun Kim <hallazzang@gmail.com> Date: Tue, 12 Nov 2019 20:03:01 +0900 Subject: [PATCH 0780/3170] update test condition --- tests/functional/test_pep517.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/test_pep517.py b/tests/functional/test_pep517.py index faff843dc80..37ef54decee 100644 --- a/tests/functional/test_pep517.py +++ b/tests/functional/test_pep517.py @@ -214,4 +214,4 @@ def test_pep517_and_build_options(script, tmpdir, data, common_wheels): expect_error=True ) assert 'Cannot build wheel' in result.stderr - assert 'when --build-options is present' in result.stderr + assert 'when --build-option is present' in result.stderr From b94725de4cb711d5faa00b9761fcca4d479417d0 Mon Sep 17 00:00:00 2001 From: Hanjun Kim <hallazzang@gmail.com> Date: Tue, 12 Nov 2019 20:05:06 +0900 Subject: [PATCH 0781/3170] change error message --- src/pip/_internal/wheel_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index f1383a46627..73e178bdae0 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -345,7 +345,7 @@ def _build_one_pep517( if self.build_options: # PEP 517 does not support --build-options logger.error('Cannot build wheel for %s using PEP 517 when ' - '--build-options is present' % (req.name,)) + '--build-option is present' % (req.name,)) return None try: logger.debug('Destination directory: %s', tempd) From cc57a185482e322aa9fb62a466b8ac084654f628 Mon Sep 17 00:00:00 2001 From: Aniruddha Basak <codewithaniruddha@gmail.com> Date: Tue, 12 Nov 2019 18:30:51 +0530 Subject: [PATCH 0782/3170] Correct the link referring the issue 3236 --- docs/html/user_guide.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 40c6590188d..10dccdda2c3 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -66,7 +66,7 @@ pypi.company.com, the index URL with credentials would look like: ``https://user:he%2F%2Fo@pypi.company.com`` Support for percent-encoded authentication in index URLs was added in pip 10.0.0 -(in `#3236 <https://github.com/pypa/pip/issues/3236>`). Users that must use authentication +(in `#3236 <https://github.com/pypa/pip/issues/3236>`_). Users that must use authentication for their Python repository on systems with older pip versions should make the latest get-pip.py available in their environment to bootstrap pip to a recent-enough version. From d1452ffabdfc566ecae56a38384e96f5445658c6 Mon Sep 17 00:00:00 2001 From: Maxim Kurnikov <maxim.kurnikov@gmail.com> Date: Tue, 12 Nov 2019 21:08:48 +0300 Subject: [PATCH 0783/3170] add global disallow_any_generics=True --- setup.cfg | 1 + src/pip/_internal/cli/base_command.py | 2 +- src/pip/_internal/index/package_finder.py | 2 +- src/pip/_internal/legacy_resolve.py | 3 ++- src/pip/_internal/locations.py | 2 +- src/pip/_internal/models/format_control.py | 6 +++--- src/pip/_internal/operations/prepare.py | 4 ++-- src/pip/_internal/req/req_uninstall.py | 2 +- src/pip/_internal/utils/subprocess.py | 2 +- src/pip/_internal/utils/ui.py | 2 +- src/pip/_internal/wheel.py | 4 ++-- src/pip/_internal/wheel_builder.py | 2 +- 12 files changed, 17 insertions(+), 15 deletions(-) diff --git a/setup.cfg b/setup.cfg index 05e24a23741..617e8b5673f 100644 --- a/setup.cfg +++ b/setup.cfg @@ -30,6 +30,7 @@ ignore = W504 follow_imports = silent ignore_missing_imports = True disallow_untyped_defs = True +disallow_any_generics = True [mypy-pip/_vendor/*] follow_imports = skip diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index 6b404954044..31ee09e86aa 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -92,7 +92,7 @@ def run(self, options, args): raise NotImplementedError def parse_args(self, args): - # type: (List[str]) -> Tuple + # type: (List[str]) -> Tuple[Any, Any] # factored out for testability return self.parser.parse_args(args) diff --git a/src/pip/_internal/index/package_finder.py b/src/pip/_internal/index/package_finder.py index 1ede4e42df1..3e034ba91b0 100644 --- a/src/pip/_internal/index/package_finder.py +++ b/src/pip/_internal/index/package_finder.py @@ -116,7 +116,7 @@ def __init__( self, project_name, # type: str canonical_name, # type: str - formats, # type: FrozenSet + formats, # type: FrozenSet[str] target_python, # type: TargetPython allow_yanked, # type: bool ignore_requires_python=None, # type: Optional[bool] diff --git a/src/pip/_internal/legacy_resolve.py b/src/pip/_internal/legacy_resolve.py index 75d6076a588..d94447f8798 100644 --- a/src/pip/_internal/legacy_resolve.py +++ b/src/pip/_internal/legacy_resolve.py @@ -53,6 +53,7 @@ InstallRequirementProvider = Callable[ [str, InstallRequirement], InstallRequirement ] + DiscoveredDependencies = DefaultDict[str, List[InstallRequirement]] logger = logging.getLogger(__name__) @@ -148,7 +149,7 @@ def __init__( self._make_install_req = make_install_req self._discovered_dependencies = \ - defaultdict(list) # type: DefaultDict[str, List] + defaultdict(list) # type: DiscoveredDependencies def resolve(self, requirement_set): # type: (RequirementSet) -> None diff --git a/src/pip/_internal/locations.py b/src/pip/_internal/locations.py index b54b46bbe8a..be47ab17c7e 100644 --- a/src/pip/_internal/locations.py +++ b/src/pip/_internal/locations.py @@ -95,7 +95,7 @@ def get_src_prefix(): def distutils_scheme( dist_name, user=False, home=None, root=None, isolated=False, prefix=None ): - # type:(str, bool, str, str, bool, str) -> dict + # type:(str, bool, str, str, bool, str) -> Dict[str, str] """ Return a distutils install scheme """ diff --git a/src/pip/_internal/models/format_control.py b/src/pip/_internal/models/format_control.py index 5489b51d076..3f4a219e257 100644 --- a/src/pip/_internal/models/format_control.py +++ b/src/pip/_internal/models/format_control.py @@ -16,7 +16,7 @@ class FormatControl(object): """ def __init__(self, no_binary=None, only_binary=None): - # type: (Optional[Set], Optional[Set]) -> None + # type: (Optional[Set[str]], Optional[Set[str]]) -> None if no_binary is None: no_binary = set() if only_binary is None: @@ -40,7 +40,7 @@ def __repr__(self): @staticmethod def handle_mutual_excludes(value, target, other): - # type: (str, Optional[Set], Optional[Set]) -> None + # type: (str, Optional[Set[str]], Optional[Set[str]]) -> None if value.startswith('-'): raise CommandError( "--no-binary / --only-binary option requires 1 argument." @@ -63,7 +63,7 @@ def handle_mutual_excludes(value, target, other): target.add(name) def get_allowed_formats(self, canonical_name): - # type: (str) -> FrozenSet + # type: (str) -> FrozenSet[str] result = {"binary", "source"} if canonical_name in self.only_binary: result.discard('source') diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index c564d64a7e9..d3e646966c7 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -57,7 +57,7 @@ if MYPY_CHECK_RUNNING: from typing import ( - Callable, IO, List, Optional, Tuple, + Any, Callable, IO, List, Optional, Tuple, ) from mypy_extensions import TypedDict @@ -116,7 +116,7 @@ def _progress_indicator(iterable, *args, **kwargs): def _download_url( resp, # type: Response link, # type: Link - content_file, # type: IO + content_file, # type: IO[Any] hashes, # type: Optional[Hashes] progress_bar # type: str ): diff --git a/src/pip/_internal/req/req_uninstall.py b/src/pip/_internal/req/req_uninstall.py index 3acde914ae0..544f296592f 100644 --- a/src/pip/_internal/req/req_uninstall.py +++ b/src/pip/_internal/req/req_uninstall.py @@ -59,7 +59,7 @@ def _script_names(dist, script_name, is_gui): def _unique(fn): - # type: (Callable) -> Callable[..., Iterator[Any]] + # type: (Callable[..., Iterator[Any]]) -> Callable[..., Iterator[Any]] @functools.wraps(fn) def unique(*args, **kw): # type: (Any, Any) -> Iterator[Any] diff --git a/src/pip/_internal/utils/subprocess.py b/src/pip/_internal/utils/subprocess.py index 2a0c5d1a655..ea0176d341e 100644 --- a/src/pip/_internal/utils/subprocess.py +++ b/src/pip/_internal/utils/subprocess.py @@ -254,7 +254,7 @@ def call_subprocess( def runner_with_spinner_message(message): - # type: (str) -> Callable + # type: (str) -> Callable[..., None] """Provide a subprocess_runner that shows a spinner message. Intended for use with for pep517's Pep517HookCaller. Thus, the runner has diff --git a/src/pip/_internal/utils/ui.py b/src/pip/_internal/utils/ui.py index f96ab54d01a..45e509b6bcb 100644 --- a/src/pip/_internal/utils/ui.py +++ b/src/pip/_internal/utils/ui.py @@ -279,7 +279,7 @@ def DownloadProgressProvider(progress_bar, max=None): @contextlib.contextmanager def hidden_cursor(file): - # type: (IO) -> Iterator[None] + # type: (IO[Any]) -> Iterator[None] # The Windows terminal does not support the hide/show cursor ANSI codes, # even via colorama. So don't even try. if WINDOWS: diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index 682a37e49b8..ab2abba7b23 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -69,7 +69,7 @@ def rehash(path, blocksize=1 << 20): def open_for_csv(name, mode): - # type: (str, Text) -> IO + # type: (str, Text) -> IO[Any] if sys.version_info[0] < 3: nl = {} # type: Dict[str, Any] bin = 'b' @@ -237,7 +237,7 @@ def sorted_outrows(outrows): def get_csv_rows_for_installed( old_csv_rows, # type: Iterable[List[str]] installed, # type: Dict[str, str] - changed, # type: set + changed, # type: Set[str] generated, # type: List[str] lib_dir, # type: str ): diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index f1383a46627..6e0d1f69ce6 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -57,7 +57,7 @@ def replace_python_tag(wheelname, new_tag): def _contains_egg_info( s, _egg_info_re=re.compile(r'([a-z0-9_.]+)-([a-z0-9_.!+-]+)', re.I)): - # type: (str, Pattern) -> bool + # type: (str, Pattern[str]) -> bool """Determine whether the string looks like an egg_info. :param s: The string to parse. E.g. foo-2.1 From 72e8510f3bf1d8ab30442abc16aef47008bc6dab Mon Sep 17 00:00:00 2001 From: sinoroc <sinoroc.code+git@gmail.com> Date: Tue, 12 Nov 2019 23:54:43 +0100 Subject: [PATCH 0784/3170] Fix documentation links for index options In the documentation the links for the pip commands index options are mixed up. The index options are common to multiple commands, but in the documentation they should be specific to a command for the links to point to the right chapter. GitHub: #7347 --- .../development/architecture/package-finding.rst | 15 ++++++++------- docs/html/reference/pip_download.rst | 2 +- docs/html/reference/pip_install.rst | 16 ++++++++-------- docs/html/reference/pip_list.rst | 2 +- docs/html/reference/pip_wheel.rst | 2 +- docs/html/user_guide.rst | 2 +- docs/pip_sphinxext.py | 6 +++++- news/7347.doc | 1 + 8 files changed, 26 insertions(+), 20 deletions(-) create mode 100644 news/7347.doc diff --git a/docs/html/development/architecture/package-finding.rst b/docs/html/development/architecture/package-finding.rst index 852be3b5d38..77eab584736 100644 --- a/docs/html/development/architecture/package-finding.rst +++ b/docs/html/development/architecture/package-finding.rst @@ -17,10 +17,11 @@ file to download for a package, given a requirement: 1. Collect together the various network and file system locations containing project package files. These locations are derived, for example, from pip's - :ref:`--index-url <--index-url>` (with default https://pypi.org/simple/ ) - setting and any configured :ref:`--extra-index-url <--extra-index-url>` - locations. Each of the project page URL's is an HTML page of anchor links, - as defined in `PEP 503`_, the "Simple Repository API." + :ref:`--index-url <install_--index-url>` (with default + https://pypi.org/simple/ ) setting and any configured + :ref:`--extra-index-url <install_--extra-index-url>` locations. Each of the + project page URL's is an HTML page of anchor links, as defined in + `PEP 503`_, the "Simple Repository API." 2. For each project page URL, fetch the HTML and parse out the anchor links, creating a ``Link`` object from each one. The :ref:`LinkCollector <link-collector-class>` class is responsible for both the previous step @@ -128,9 +129,9 @@ responsible for collecting the raw list of "links" to package files `PEP 503`_ project page URL's that ``PackageFinder`` should access. The ``LinkCollector`` class takes into account the user's :ref:`--find-links -<--find-links>`, :ref:`--extra-index-url <--extra-index-url>`, and related -options when deciding which locations to collect links from. The class's main -method is the ``collect_links()`` method. The :ref:`PackageFinder +<install_--find-links>`, :ref:`--extra-index-url <install_--extra-index-url>`, +and related options when deciding which locations to collect links from. The +class's main method is the ``collect_links()`` method. The :ref:`PackageFinder <package-finder-class>` class invokes this method as the first step of its ``find_all_candidates()`` method. diff --git a/docs/html/reference/pip_download.rst b/docs/html/reference/pip_download.rst index cc02accf595..2432d889457 100644 --- a/docs/html/reference/pip_download.rst +++ b/docs/html/reference/pip_download.rst @@ -47,7 +47,7 @@ Options .. pip-command-options:: download -.. pip-index-options:: +.. pip-index-options:: download Examples diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index 5cbb139064b..de1eb100c35 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -153,17 +153,17 @@ To interpret the requirements file in UTF-8 format add a comment The following options are supported: - * :ref:`-i, --index-url <--index-url>` - * :ref:`--extra-index-url <--extra-index-url>` - * :ref:`--no-index <--no-index>` - * :ref:`-f, --find-links <--find-links>` + * :ref:`-i, --index-url <install_--index-url>` + * :ref:`--extra-index-url <install_--extra-index-url>` + * :ref:`--no-index <install_--no-index>` + * :ref:`-f, --find-links <install_--find-links>` * :ref:`--no-binary <install_--no-binary>` * :ref:`--only-binary <install_--only-binary>` - * :ref:`--require-hashes <--require-hashes>` + * :ref:`--require-hashes <install_--require-hashes>` * :ref:`--trusted-host <--trusted-host>` -For example, to specify :ref:`--no-index <--no-index>` and two -:ref:`--find-links <--find-links>` locations: +For example, to specify :ref:`--no-index <install_--no-index>` and two +:ref:`--find-links <install_--find-links>` locations: :: @@ -824,7 +824,7 @@ Options .. pip-command-options:: install -.. pip-index-options:: +.. pip-index-options:: install .. _`pip install Examples`: diff --git a/docs/html/reference/pip_list.rst b/docs/html/reference/pip_list.rst index c459b7a3bb1..f7f7dd0529f 100644 --- a/docs/html/reference/pip_list.rst +++ b/docs/html/reference/pip_list.rst @@ -20,7 +20,7 @@ Options .. pip-command-options:: list -.. pip-index-options:: +.. pip-index-options:: list Examples diff --git a/docs/html/reference/pip_wheel.rst b/docs/html/reference/pip_wheel.rst index 942bb70b8b7..2b8cee946f8 100644 --- a/docs/html/reference/pip_wheel.rst +++ b/docs/html/reference/pip_wheel.rst @@ -59,7 +59,7 @@ Options .. pip-command-options:: wheel -.. pip-index-options:: +.. pip-index-options:: wheel Examples diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 21c2ca4acaa..57f7d281ff8 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -515,7 +515,7 @@ $ pip wheel --wheel-dir DIR -r requirements.txt Then, to install from local only, you'll be using :ref:`--find-links -<--find-links>` and :ref:`--no-index <--no-index>` like so:: +<install_--find-links>` and :ref:`--no-index <install_--no-index>` like so:: $ pip install --no-index --find-links=DIR -r requirements.txt diff --git a/docs/pip_sphinxext.py b/docs/pip_sphinxext.py index c34c457e394..bfc5a6b4aef 100644 --- a/docs/pip_sphinxext.py +++ b/docs/pip_sphinxext.py @@ -86,9 +86,13 @@ def process_options(self): class PipIndexOptions(PipOptions): + required_arguments = 1 + def process_options(self): + cmd_name = self.arguments[0] self._format_options( - [o() for o in cmdoptions.index_group['options']] + [o() for o in cmdoptions.index_group['options']], + cmd_name=cmd_name, ) diff --git a/news/7347.doc b/news/7347.doc new file mode 100644 index 00000000000..bc62c56cdb9 --- /dev/null +++ b/news/7347.doc @@ -0,0 +1 @@ +Fix documentation links for index options From 7d27bff2990ffca8b03aaf990c6b69ad491031ca Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Mon, 11 Nov 2019 21:10:05 -0500 Subject: [PATCH 0785/3170] Make versions argument singular for pep425tags.get_supported() This simplifies the interface of this function in preparation for switching to packaging.tags. --- src/pip/_internal/models/target_python.py | 6 +++--- src/pip/_internal/pep425tags.py | 10 ++++++---- tests/unit/test_target_python.py | 20 ++++++++++---------- tests/unit/test_wheel.py | 20 ++++++++++---------- 4 files changed, 29 insertions(+), 27 deletions(-) diff --git a/src/pip/_internal/models/target_python.py b/src/pip/_internal/models/target_python.py index a23b79c4e4e..cd08c917899 100644 --- a/src/pip/_internal/models/target_python.py +++ b/src/pip/_internal/models/target_python.py @@ -91,12 +91,12 @@ def get_tags(self): # versions=None uses special default logic. py_version_info = self._given_py_version_info if py_version_info is None: - versions = None + version = None else: - versions = [version_info_to_nodot(py_version_info)] + version = version_info_to_nodot(py_version_info) tags = get_supported( - versions=versions, + version=version, platform=self.platform, abi=self.abi, impl=self.implementation, diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index c9425cdf823..5e1b238e2e5 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -326,7 +326,7 @@ def get_all_minor_versions_as_strings(version_info): def get_supported( - versions=None, # type: Optional[List[str]] + version=None, # type: Optional[str] platform=None, # type: Optional[str] impl=None, # type: Optional[str] abi=None # type: Optional[str] @@ -335,8 +335,8 @@ def get_supported( """Return a list of supported tags for each version specified in `versions`. - :param versions: a list of string versions, of the form ["33", "32"], - or None. The first version will be assumed to support our ABI. + :param version: a string version, of the form "33" or "32", + or None. The version will be assumed to support our ABI. :param platform: specify the exact platform you want valid tags for, or None. If None, use the local system platform. :param impl: specify the exact implementation you want valid @@ -347,9 +347,11 @@ def get_supported( supported = [] # Versions must be given with respect to the preference - if versions is None: + if version is None: version_info = get_impl_version_info() versions = get_all_minor_versions_as_strings(version_info) + else: + versions = [version] impl = impl or get_abbr_impl() diff --git a/tests/unit/test_target_python.py b/tests/unit/test_target_python.py index 9c08427ccb6..0dc2af22bd0 100644 --- a/tests/unit/test_target_python.py +++ b/tests/unit/test_target_python.py @@ -64,20 +64,20 @@ def test_format_given(self, kwargs, expected): actual = target_python.format_given() assert actual == expected - @pytest.mark.parametrize('py_version_info, expected_versions', [ - ((), ['']), - ((2, ), ['2']), - ((3, ), ['3']), - ((3, 7), ['37']), - ((3, 7, 3), ['37']), + @pytest.mark.parametrize('py_version_info, expected_version', [ + ((), ''), + ((2, ), '2'), + ((3, ), '3'), + ((3, 7), '37'), + ((3, 7, 3), '37'), # Check a minor version with two digits. - ((3, 10, 1), ['310']), + ((3, 10, 1), '310'), # Check that versions=None is passed to get_tags(). (None, None), ]) @patch('pip._internal.models.target_python.get_supported') def test_get_tags( - self, mock_get_supported, py_version_info, expected_versions, + self, mock_get_supported, py_version_info, expected_version, ): mock_get_supported.return_value = ['tag-1', 'tag-2'] @@ -85,8 +85,8 @@ def test_get_tags( actual = target_python.get_tags() assert actual == ['tag-1', 'tag-2'] - actual = mock_get_supported.call_args[1]['versions'] - assert actual == expected_versions + actual = mock_get_supported.call_args[1]['version'] + assert actual == expected_version # Check that the value was cached. assert target_python._valid_tags == ['tag-1', 'tag-2'] diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 0df9bd8351d..0eccac90567 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -303,7 +303,7 @@ def test_supported_osx_version(self): Wheels built for macOS 10.6 are supported on 10.9 """ tags = pep425tags.get_supported( - ['27'], platform='macosx_10_9_intel', impl='cp' + '27', platform='macosx_10_9_intel', impl='cp' ) w = wheel.Wheel('simple-0.1-cp27-none-macosx_10_6_intel.whl') assert w.supported(tags=tags) @@ -315,7 +315,7 @@ def test_not_supported_osx_version(self): Wheels built for macOS 10.9 are not supported on 10.6 """ tags = pep425tags.get_supported( - ['27'], platform='macosx_10_6_intel', impl='cp' + '27', platform='macosx_10_6_intel', impl='cp' ) w = wheel.Wheel('simple-0.1-cp27-none-macosx_10_9_intel.whl') assert not w.supported(tags=tags) @@ -325,22 +325,22 @@ def test_supported_multiarch_darwin(self): Multi-arch wheels (intel) are supported on components (i386, x86_64) """ universal = pep425tags.get_supported( - ['27'], platform='macosx_10_5_universal', impl='cp' + '27', platform='macosx_10_5_universal', impl='cp' ) intel = pep425tags.get_supported( - ['27'], platform='macosx_10_5_intel', impl='cp' + '27', platform='macosx_10_5_intel', impl='cp' ) x64 = pep425tags.get_supported( - ['27'], platform='macosx_10_5_x86_64', impl='cp' + '27', platform='macosx_10_5_x86_64', impl='cp' ) i386 = pep425tags.get_supported( - ['27'], platform='macosx_10_5_i386', impl='cp' + '27', platform='macosx_10_5_i386', impl='cp' ) ppc = pep425tags.get_supported( - ['27'], platform='macosx_10_5_ppc', impl='cp' + '27', platform='macosx_10_5_ppc', impl='cp' ) ppc64 = pep425tags.get_supported( - ['27'], platform='macosx_10_5_ppc64', impl='cp' + '27', platform='macosx_10_5_ppc64', impl='cp' ) w = wheel.Wheel('simple-0.1-cp27-none-macosx_10_5_intel.whl') @@ -363,10 +363,10 @@ def test_not_supported_multiarch_darwin(self): Single-arch wheels (x86_64) are not supported on multi-arch (intel) """ universal = pep425tags.get_supported( - ['27'], platform='macosx_10_5_universal', impl='cp' + '27', platform='macosx_10_5_universal', impl='cp' ) intel = pep425tags.get_supported( - ['27'], platform='macosx_10_5_intel', impl='cp' + '27', platform='macosx_10_5_intel', impl='cp' ) w = wheel.Wheel('simple-0.1-cp27-none-macosx_10_5_i386.whl') From 8f60e8835a58dcb6b0852547fc6ca2fb019e602b Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 12 Nov 2019 21:05:42 -0500 Subject: [PATCH 0786/3170] Move URL in basic auth docs --- docs/html/user_guide.rst | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 10dccdda2c3..8f4e8fd05a3 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -58,8 +58,10 @@ a username and password separated by ``:``. ``https://[username[:password]@]pypi.company.com/simple`` Certain special characters are not valid in the authentication part of URLs. -If the user or password part of your login credentials contain any of these -special characters here then they must be percent-encoded. For example, for a +If the user or password part of your login credentials contain any of the +special characters +`here <https://en.wikipedia.org/wiki/Percent-encoding#Percent-encoding_reserved_characters>`_ +then they must be percent-encoded. For example, for a user with username "user" and password "he//o" accessing a repository at pypi.company.com, the index URL with credentials would look like: @@ -75,9 +77,6 @@ as the "username" and do not provide a password, for example - ``https://0123456789abcdef@pypi.company.com`` -`Here <https://en.wikipedia.org/wiki/Percent-encoding>`_ you can find more about -percent encoding. - Using a Proxy Server ******************** From dcb8669f215cd488200c0d5e8ca1026977a4a5af Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Wed, 13 Nov 2019 11:43:56 +0100 Subject: [PATCH 0787/3170] Simplify SimpleWheelCache.get in case package_name is falsy --- src/pip/_internal/cache.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/cache.py b/src/pip/_internal/cache.py index b3327ea9a57..432b652ec1e 100644 --- a/src/pip/_internal/cache.py +++ b/src/pip/_internal/cache.py @@ -172,15 +172,15 @@ def get( # type: (...) -> Link candidates = [] - canonical_package_name = None - if package_name: - canonical_package_name = canonicalize_name(package_name) + if not package_name: + return link + + canonical_package_name = canonicalize_name(package_name) for wheel_name in self._get_candidates(link, package_name): try: wheel = Wheel(wheel_name) except InvalidWheelFilename: continue - assert canonical_package_name if wheel.name != canonical_package_name: continue if not wheel.supported(supported_tags): From a4d12f807fb4be7998b7f83572f020a447fc9956 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Wed, 13 Nov 2019 11:57:30 +0100 Subject: [PATCH 0788/3170] Avoid double package name canonicalization --- src/pip/_internal/cache.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/cache.py b/src/pip/_internal/cache.py index 432b652ec1e..5173a070fed 100644 --- a/src/pip/_internal/cache.py +++ b/src/pip/_internal/cache.py @@ -77,19 +77,18 @@ def _get_cache_path_parts(self, link): return parts - def _get_candidates(self, link, package_name): + def _get_candidates(self, link, canonical_package_name): # type: (Link, Optional[str]) -> List[Any] can_not_cache = ( not self.cache_dir or - not package_name or + not canonical_package_name or not link ) if can_not_cache: return [] - canonical_name = canonicalize_name(package_name) formats = self.format_control.get_allowed_formats( - canonical_name + canonical_package_name ) if not self.allowed_formats.intersection(formats): return [] @@ -176,12 +175,12 @@ def get( return link canonical_package_name = canonicalize_name(package_name) - for wheel_name in self._get_candidates(link, package_name): + for wheel_name in self._get_candidates(link, canonical_package_name): try: wheel = Wheel(wheel_name) except InvalidWheelFilename: continue - if wheel.name != canonical_package_name: + if canonicalize_name(wheel.name) != canonical_package_name: continue if not wheel.supported(supported_tags): # Built for a different python/arch/etc From 47ae034da8f10b854c5f6febb54a7794deea86e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Wed, 13 Nov 2019 15:13:04 +0100 Subject: [PATCH 0789/3170] Debug logging in case of unexpected wheel name in cache --- src/pip/_internal/cache.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/pip/_internal/cache.py b/src/pip/_internal/cache.py index 5173a070fed..7aa72b9a774 100644 --- a/src/pip/_internal/cache.py +++ b/src/pip/_internal/cache.py @@ -181,6 +181,12 @@ def get( except InvalidWheelFilename: continue if canonicalize_name(wheel.name) != canonical_package_name: + logger.debug( + "Ignoring cached wheel {} for {} as it " + "does not match the expected distribution name {}.".format( + wheel_name, link, package_name + ) + ) continue if not wheel.supported(supported_tags): # Built for a different python/arch/etc From d4f2c9f962989a25dc7c055d5b2573708e2c37ee Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 14 Nov 2019 20:14:31 -0500 Subject: [PATCH 0790/3170] Remove interpreter-specific major version tag As mentioned in https://snarky.ca/the-challenges-in-designing-a-library-for-pep-425/ this tag doesn't make much sense + it impedes our usage of packaging.tags. In terms of backwards-compatibility, we attest to try to match compatible wheels as best as possible, and this tag doesn't represent that. --- src/pip/_internal/pep425tags.py | 3 --- tests/functional/test_download.py | 14 -------------- 2 files changed, 17 deletions(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index 5e1b238e2e5..1335e323e03 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -433,9 +433,6 @@ def get_supported( # No abi / arch, but requires our implementation: supported.append(('%s%s' % (impl, versions[0]), 'none', 'any')) - # Tagged specifically as being cross-version compatible - # (with just the major version specified) - supported.append(('%s%s' % (impl, versions[0][0]), 'none', 'any')) # No abi / arch, generic Python for i, version in enumerate(versions): diff --git a/tests/functional/test_download.py b/tests/functional/test_download.py index 77f54e8ae22..e19c9e5548f 100644 --- a/tests/functional/test_download.py +++ b/tests/functional/test_download.py @@ -620,20 +620,6 @@ def test_download_specify_implementation(script, data): in result.files_created ) - data.reset() - fake_wheel(data, 'fake-1.0-fk2.fk3-none-any.whl') - result = script.pip( - 'download', '--no-index', '--find-links', data.find_links, - '--only-binary=:all:', - '--dest', '.', - '--implementation', 'fk', - 'fake' - ) - assert ( - Path('scratch') / 'fake-1.0-fk2.fk3-none-any.whl' - in result.files_created - ) - data.reset() fake_wheel(data, 'fake-1.0-fk3-none-any.whl') result = script.pip( From e5212281299d18cdd56ebc137cc18f31c9a7605c Mon Sep 17 00:00:00 2001 From: Tomas Orsava <torsava@redhat.com> Date: Fri, 15 Nov 2019 19:44:54 +0100 Subject: [PATCH 0791/3170] Mark 6 tests as network tests =================================== FAILURES =================================== _______________________________ test_freeze_path _______________________________ tmpdir = Path('/tmp/pytest-of-mockbuild/pytest-0/test_freeze_path0') script = <tests.lib.PipTestEnvironment object at 0x7fe950a4caf0> data = <tests.lib.TestData object at 0x7fe950a4cc10> def test_freeze_path(tmpdir, script, data): """ Test freeze with --path. """ > script.pip('install', '--find-links', data.find_links, '--target', tmpdir, 'simple==2.0') tests/functional/test_freeze.py:712: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ tests/lib/__init__.py:593: in run _check_stderr( _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ stderr = "WARNING: Retrying (Retry(total=4, connect=None, read=None, redirect=None, status=None)) after connection broken by 'N...t at 0x7fe6435ef280>: Failed to establish a new connection: [Errno -2] Name or service not known')': /simple/simple/\n" allow_stderr_warning = False, allow_stderr_error = False def _check_stderr( stderr, allow_stderr_warning, allow_stderr_error, ): """ Check the given stderr for logged warnings and errors. :param stderr: stderr output as a string. :param allow_stderr_warning: whether a logged warning (or deprecation message) is allowed. Must be True if allow_stderr_error is True. :param allow_stderr_error: whether a logged error is allowed. """ assert not (allow_stderr_error and not allow_stderr_warning) lines = stderr.splitlines() for line in lines: # First check for logging errors, which we don't allow during # tests even if allow_stderr_error=True (since a logging error # would signal a bug in pip's code). # Unlike errors logged with logger.error(), these errors are # sent directly to stderr and so bypass any configured log formatter. # The "--- Logging error ---" string is used in Python 3.4+, and # "Logged from file " is used in Python 2. if (line.startswith('--- Logging error ---') or line.startswith('Logged from file ')): reason = 'stderr has a logging error, which is never allowed' msg = make_check_stderr_message(stderr, line=line, reason=reason) raise RuntimeError(msg) if allow_stderr_error: continue if line.startswith('ERROR: '): reason = ( 'stderr has an unexpected error ' '(pass allow_stderr_error=True to permit this)' ) msg = make_check_stderr_message(stderr, line=line, reason=reason) raise RuntimeError(msg) if allow_stderr_warning: continue if (line.startswith('WARNING: ') or line.startswith(DEPRECATION_MSG_PREFIX)): reason = ( 'stderr has an unexpected warning ' '(pass allow_stderr_warning=True to permit this)' ) msg = make_check_stderr_message(stderr, line=line, reason=reason) > raise RuntimeError(msg) E RuntimeError: stderr has an unexpected warning (pass allow_stderr_warning=True to permit this): E Caused by line: "WARNING: Retrying (Retry(total=4, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<pip._vendor.urllib3.connection.VerifiedHTTPSConnection object at 0x7fe64364c850>: Failed to establish a new connection: [Errno -2] Name or service not known')': /simple/simple/" E Complete stderr: WARNING: Retrying (Retry(total=4, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<pip._vendor.urllib3.connection.VerifiedHTTPSConnection object at 0x7fe64364c850>: Failed to establish a new connection: [Errno -2] Name or service not known')': /simple/simple/ E WARNING: Retrying (Retry(total=3, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<pip._vendor.urllib3.connection.VerifiedHTTPSConnection object at 0x7fe64364cdc0>: Failed to establish a new connection: [Errno -2] Name or service not known')': /simple/simple/ E WARNING: Retrying (Retry(total=2, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<pip._vendor.urllib3.connection.VerifiedHTTPSConnection object at 0x7fe64364cf70>: Failed to establish a new connection: [Errno -2] Name or service not known')': /simple/simple/ E WARNING: Retrying (Retry(total=1, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<pip._vendor.urllib3.connection.VerifiedHTTPSConnection object at 0x7fe6435ef130>: Failed to establish a new connection: [Errno -2] Name or service not known')': /simple/simple/ E WARNING: Retrying (Retry(total=0, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<pip._vendor.urllib3.connection.VerifiedHTTPSConnection object at 0x7fe6435ef280>: Failed to establish a new connection: [Errno -2] Name or service not known')': /simple/simple/ tests/lib/__init__.py:404: RuntimeError ________________________ test_freeze_path_exclude_user _________________________ tmpdir = Path('/tmp/pytest-of-mockbuild/pytest-0/test_freeze_path_exclude_user0') script = <tests.lib.PipTestEnvironment object at 0x7fe950ec8fa0> data = <tests.lib.TestData object at 0x7fe950ec8a30> def test_freeze_path_exclude_user(tmpdir, script, data): """ Test freeze with --path and make sure packages from --user are not picked up. """ script.pip_install_local('--find-links', data.find_links, '--user', 'simple2') > script.pip('install', '--find-links', data.find_links, '--target', tmpdir, 'simple==1.0') tests/functional/test_freeze.py:728: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ tests/lib/__init__.py:593: in run _check_stderr( _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ stderr = "WARNING: Retrying (Retry(total=4, connect=None, read=None, redirect=None, status=None)) after connection broken by 'N...t at 0x7f87ae751310>: Failed to establish a new connection: [Errno -2] Name or service not known')': /simple/simple/\n" allow_stderr_warning = False, allow_stderr_error = False def _check_stderr( stderr, allow_stderr_warning, allow_stderr_error, ): """ Check the given stderr for logged warnings and errors. :param stderr: stderr output as a string. :param allow_stderr_warning: whether a logged warning (or deprecation message) is allowed. Must be True if allow_stderr_error is True. :param allow_stderr_error: whether a logged error is allowed. """ assert not (allow_stderr_error and not allow_stderr_warning) lines = stderr.splitlines() for line in lines: # First check for logging errors, which we don't allow during # tests even if allow_stderr_error=True (since a logging error # would signal a bug in pip's code). # Unlike errors logged with logger.error(), these errors are # sent directly to stderr and so bypass any configured log formatter. # The "--- Logging error ---" string is used in Python 3.4+, and # "Logged from file " is used in Python 2. if (line.startswith('--- Logging error ---') or line.startswith('Logged from file ')): reason = 'stderr has a logging error, which is never allowed' msg = make_check_stderr_message(stderr, line=line, reason=reason) raise RuntimeError(msg) if allow_stderr_error: continue if line.startswith('ERROR: '): reason = ( 'stderr has an unexpected error ' '(pass allow_stderr_error=True to permit this)' ) msg = make_check_stderr_message(stderr, line=line, reason=reason) raise RuntimeError(msg) if allow_stderr_warning: continue if (line.startswith('WARNING: ') or line.startswith(DEPRECATION_MSG_PREFIX)): reason = ( 'stderr has an unexpected warning ' '(pass allow_stderr_warning=True to permit this)' ) msg = make_check_stderr_message(stderr, line=line, reason=reason) > raise RuntimeError(msg) E RuntimeError: stderr has an unexpected warning (pass allow_stderr_warning=True to permit this): E Caused by line: "WARNING: Retrying (Retry(total=4, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<pip._vendor.urllib3.connection.VerifiedHTTPSConnection object at 0x7f87ae7aa8e0>: Failed to establish a new connection: [Errno -2] Name or service not known')': /simple/simple/" E Complete stderr: WARNING: Retrying (Retry(total=4, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<pip._vendor.urllib3.connection.VerifiedHTTPSConnection object at 0x7f87ae7aa8e0>: Failed to establish a new connection: [Errno -2] Name or service not known')': /simple/simple/ E WARNING: Retrying (Retry(total=3, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<pip._vendor.urllib3.connection.VerifiedHTTPSConnection object at 0x7f87ae7aae50>: Failed to establish a new connection: [Errno -2] Name or service not known')': /simple/simple/ E WARNING: Retrying (Retry(total=2, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<pip._vendor.urllib3.connection.VerifiedHTTPSConnection object at 0x7f87ae751040>: Failed to establish a new connection: [Errno -2] Name or service not known')': /simple/simple/ E WARNING: Retrying (Retry(total=1, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<pip._vendor.urllib3.connection.VerifiedHTTPSConnection object at 0x7f87ae7511c0>: Failed to establish a new connection: [Errno -2] Name or service not known')': /simple/simple/ E WARNING: Retrying (Retry(total=0, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<pip._vendor.urllib3.connection.VerifiedHTTPSConnection object at 0x7f87ae751310>: Failed to establish a new connection: [Errno -2] Name or service not known')': /simple/simple/ tests/lib/__init__.py:404: RuntimeError __________________________ test_freeze_path_multiple ___________________________ tmpdir = Path('/tmp/pytest-of-mockbuild/pytest-0/test_freeze_path_multiple0') script = <tests.lib.PipTestEnvironment object at 0x7fe950b43fd0> data = <tests.lib.TestData object at 0x7fe950b43df0> def test_freeze_path_multiple(tmpdir, script, data): """ Test freeze with multiple --path arguments. """ path1 = tmpdir / "path1" os.mkdir(path1) path2 = tmpdir / "path2" os.mkdir(path2) > script.pip('install', '--find-links', data.find_links, '--target', path1, 'simple==2.0') tests/functional/test_freeze.py:750: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ tests/lib/__init__.py:593: in run _check_stderr( _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ stderr = "WARNING: Retrying (Retry(total=4, connect=None, read=None, redirect=None, status=None)) after connection broken by 'N...t at 0x7f07e6253280>: Failed to establish a new connection: [Errno -2] Name or service not known')': /simple/simple/\n" allow_stderr_warning = False, allow_stderr_error = False def _check_stderr( stderr, allow_stderr_warning, allow_stderr_error, ): """ Check the given stderr for logged warnings and errors. :param stderr: stderr output as a string. :param allow_stderr_warning: whether a logged warning (or deprecation message) is allowed. Must be True if allow_stderr_error is True. :param allow_stderr_error: whether a logged error is allowed. """ assert not (allow_stderr_error and not allow_stderr_warning) lines = stderr.splitlines() for line in lines: # First check for logging errors, which we don't allow during # tests even if allow_stderr_error=True (since a logging error # would signal a bug in pip's code). # Unlike errors logged with logger.error(), these errors are # sent directly to stderr and so bypass any configured log formatter. # The "--- Logging error ---" string is used in Python 3.4+, and # "Logged from file " is used in Python 2. if (line.startswith('--- Logging error ---') or line.startswith('Logged from file ')): reason = 'stderr has a logging error, which is never allowed' msg = make_check_stderr_message(stderr, line=line, reason=reason) raise RuntimeError(msg) if allow_stderr_error: continue if line.startswith('ERROR: '): reason = ( 'stderr has an unexpected error ' '(pass allow_stderr_error=True to permit this)' ) msg = make_check_stderr_message(stderr, line=line, reason=reason) raise RuntimeError(msg) if allow_stderr_warning: continue if (line.startswith('WARNING: ') or line.startswith(DEPRECATION_MSG_PREFIX)): reason = ( 'stderr has an unexpected warning ' '(pass allow_stderr_warning=True to permit this)' ) msg = make_check_stderr_message(stderr, line=line, reason=reason) > raise RuntimeError(msg) E RuntimeError: stderr has an unexpected warning (pass allow_stderr_warning=True to permit this): E Caused by line: "WARNING: Retrying (Retry(total=4, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<pip._vendor.urllib3.connection.VerifiedHTTPSConnection object at 0x7f07e62ae850>: Failed to establish a new connection: [Errno -2] Name or service not known')': /simple/simple/" E Complete stderr: WARNING: Retrying (Retry(total=4, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<pip._vendor.urllib3.connection.VerifiedHTTPSConnection object at 0x7f07e62ae850>: Failed to establish a new connection: [Errno -2] Name or service not known')': /simple/simple/ E WARNING: Retrying (Retry(total=3, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<pip._vendor.urllib3.connection.VerifiedHTTPSConnection object at 0x7f07e62aedc0>: Failed to establish a new connection: [Errno -2] Name or service not known')': /simple/simple/ E WARNING: Retrying (Retry(total=2, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<pip._vendor.urllib3.connection.VerifiedHTTPSConnection object at 0x7f07e62aef70>: Failed to establish a new connection: [Errno -2] Name or service not known')': /simple/simple/ E WARNING: Retrying (Retry(total=1, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<pip._vendor.urllib3.connection.VerifiedHTTPSConnection object at 0x7f07e6253130>: Failed to establish a new connection: [Errno -2] Name or service not known')': /simple/simple/ E WARNING: Retrying (Retry(total=0, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<pip._vendor.urllib3.connection.VerifiedHTTPSConnection object at 0x7f07e6253280>: Failed to establish a new connection: [Errno -2] Name or service not known')': /simple/simple/ tests/lib/__init__.py:404: RuntimeError _________________ test_install_no_binary_builds_pep_517_wheel __________________ script = <tests.lib.PipTestEnvironment object at 0x7fe9509f4e20> data = <tests.lib.TestData object at 0x7fe9509f4640>, with_wheel = None def test_install_no_binary_builds_pep_517_wheel(script, data, with_wheel): to_install = data.packages.joinpath('pep517_setup_and_pyproject') > res = script.pip( 'install', '--no-binary=:all:', '-f', data.find_links, to_install ) tests/functional/test_install.py:1279: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <tests.lib.PipTestEnvironment object at 0x7fe9509f4e20> args = ('python', '-m', 'pip', 'install', '--no-binary=:all:', '-f', ...) kw = {'expect_stderr': True} cwd = Path('/tmp/pytest-of-mockbuild/pytest-0/test_install_no_binary_builds_0/workspace/scratch') run_from = None, allow_stderr_error = False, allow_stderr_warning = False expect_error = None def run(self, *args, **kw): """ :param allow_stderr_error: whether a logged error is allowed in stderr. Passing True for this argument implies `allow_stderr_warning` since warnings are weaker than errors. :param allow_stderr_warning: whether a logged warning (or deprecation message) is allowed in stderr. :param expect_error: if False (the default), asserts that the command exits with 0. Otherwise, asserts that the command exits with a non-zero exit code. Passing True also implies allow_stderr_error and allow_stderr_warning. :param expect_stderr: whether to allow warnings in stderr (equivalent to `allow_stderr_warning`). This argument is an abbreviated version of `allow_stderr_warning` and is also kept for backwards compatibility. """ if self.verbose: print('>> running %s %s' % (args, kw)) cwd = kw.pop('cwd', None) run_from = kw.pop('run_from', None) assert not cwd or not run_from, "Don't use run_from; it's going away" cwd = cwd or run_from or self.cwd if sys.platform == 'win32': # Partial fix for ScriptTest.run using `shell=True` on Windows. args = [str(a).replace('^', '^^').replace('&', '^&') for a in args] # Remove `allow_stderr_error` and `allow_stderr_warning` before # calling run() because PipTestEnvironment doesn't support them. allow_stderr_error = kw.pop('allow_stderr_error', None) allow_stderr_warning = kw.pop('allow_stderr_warning', None) # Propagate default values. expect_error = kw.get('expect_error') if expect_error: # Then default to allowing logged errors. if allow_stderr_error is not None and not allow_stderr_error: raise RuntimeError( 'cannot pass allow_stderr_error=False with ' 'expect_error=True' ) allow_stderr_error = True elif kw.get('expect_stderr'): # Then default to allowing logged warnings. if allow_stderr_warning is not None and not allow_stderr_warning: raise RuntimeError( 'cannot pass allow_stderr_warning=False with ' 'expect_stderr=True' ) allow_stderr_warning = True if allow_stderr_error: if allow_stderr_warning is not None and not allow_stderr_warning: raise RuntimeError( 'cannot pass allow_stderr_warning=False with ' 'allow_stderr_error=True' ) # Default values if not set. if allow_stderr_error is None: allow_stderr_error = False if allow_stderr_warning is None: allow_stderr_warning = allow_stderr_error # Pass expect_stderr=True to allow any stderr. We do this because # we do our checking of stderr further on in check_stderr(). kw['expect_stderr'] = True > result = super(PipTestEnvironment, self).run(cwd=cwd, *args, **kw) E AssertionError: Script returned code: 1 tests/lib/__init__.py:586: AssertionError ----------------------------- Captured stdout call ----------------------------- Script result: python -m pip install --no-binary=:all: -f file:///tmp/pytest-of-mockbuild/pytest-0/test_install_no_binary_builds_0/data/packages /tmp/pytest-of-mockbuild/pytest-0/test_install_no_binary_builds_0/data/packages/pep517_setup_and_pyproject return code: 1 -- stderr: -------------------- ERROR: Command errored out with exit status 1: command: /tmp/pytest-of-mockbuild/pytest-0/test_install_no_binary_builds_0/workspace/venv/bin/python /builddir/build/BUILDROOT/python-pip-19.3.1-1.fc32.noarch/usr/lib/python3.8/site-packages/pip install --ignore-installed --no-user --prefix /tmp/pytest-of-mockbuild/pytest-0/test_install_no_binary_builds_0/workspace/tmp/pip-build-env-ntp1m4dh/overlay --no-warn-script-location --no-binary :all: --only-binary :none: -i https://pypi.org/simple --find-links file:///tmp/pytest-of-mockbuild/pytest-0/test_install_no_binary_builds_0/data/packages -- setuptools cwd: None Complete output (28 lines): Looking in links: file:///tmp/pytest-of-mockbuild/pytest-0/test_install_no_binary_builds_0/data/packages WARNING: Retrying (Retry(total=4, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<pip._vendor.urllib3.connection.VerifiedHTTPSConnection object at 0x7f7234ef1e50>: Failed to establish a new connection: [Errno -2] Name or service not known')': /simple/setuptools/ WARNING: Retrying (Retry(total=3, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<pip._vendor.urllib3.connection.VerifiedHTTPSConnection object at 0x7f7234e92040>: Failed to establish a new connection: [Errno -2] Name or service not known')': /simple/setuptools/ WARNING: Retrying (Retry(total=2, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<pip._vendor.urllib3.connection.VerifiedHTTPSConnection object at 0x7f7234e921c0>: Failed to establish a new connection: [Errno -2] Name or service not known')': /simple/setuptools/ WARNING: Retrying (Retry(total=1, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<pip._vendor.urllib3.connection.VerifiedHTTPSConnection object at 0x7f7234e92340>: Failed to establish a new connection: [Errno -2] Name or service not known')': /simple/setuptools/ WARNING: Retrying (Retry(total=0, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<pip._vendor.urllib3.connection.VerifiedHTTPSConnection object at 0x7f7234e924c0>: Failed to establish a new connection: [Errno -2] Name or service not known')': /simple/setuptools/ Processing /tmp/pytest-of-mockbuild/pytest-0/test_install_no_binary_builds_0/data/packages/setuptools-0.9.6.tar.gz ERROR: Command errored out with exit status 1: command: /tmp/pytest-of-mockbuild/pytest-0/test_install_no_binary_builds_0/workspace/venv/bin/python -c 'import sys, setuptools, tokenize; sys.argv[0] = '"'"'/tmp/pytest-of-mockbuild/pytest-0/test_install_no_binary_builds_0/workspace/tmp/pip-install-b_6lf4z6/setuptools/setup.py'"'"'; __file__='"'"'/tmp/pytest-of-mockbuild/pytest-0/test_install_no_binary_builds_0/workspace/tmp/pip-install-b_6lf4z6/setuptools/setup.py'"'"';f=getattr(tokenize, '"'"'open'"'"', open)(__file__);code=f.read().replace('"'"'\r\n'"'"', '"'"'\n'"'"');f.close();exec(compile(code, __file__, '"'"'exec'"'"'))' egg_info --egg-base /tmp/pytest-of-mockbuild/pytest-0/test_install_no_binary_builds_0/workspace/tmp/pip-install-b_6lf4z6/setuptools/pip-egg-info cwd: /tmp/pytest-of-mockbuild/pytest-0/test_install_no_binary_builds_0/workspace/tmp/pip-install-b_6lf4z6/setuptools/ Complete output (15 lines): Traceback (most recent call last): File "<string>", line 1, in <module> File "/tmp/pytest-of-mockbuild/pytest-0/test_install_no_binary_builds_0/workspace/tmp/pip-install-b_6lf4z6/setuptools/setuptools/__init__.py", line 2, in <module> from setuptools.extension import Extension, Library File "/tmp/pytest-of-mockbuild/pytest-0/test_install_no_binary_builds_0/workspace/tmp/pip-install-b_6lf4z6/setuptools/setuptools/extension.py", line 5, in <module> from setuptools.dist import _get_unpatched File "/tmp/pytest-of-mockbuild/pytest-0/test_install_no_binary_builds_0/workspace/tmp/pip-install-b_6lf4z6/setuptools/setuptools/dist.py", line 7, in <module> from setuptools.command.install import install File "/tmp/pytest-of-mockbuild/pytest-0/test_install_no_binary_builds_0/workspace/tmp/pip-install-b_6lf4z6/setuptools/setuptools/command/__init__.py", line 8, in <module> from setuptools.command import install_scripts File "/tmp/pytest-of-mockbuild/pytest-0/test_install_no_binary_builds_0/workspace/tmp/pip-install-b_6lf4z6/setuptools/setuptools/command/install_scripts.py", line 3, in <module> from pkg_resources import Distribution, PathMetadata, ensure_directory File "/tmp/pytest-of-mockbuild/pytest-0/test_install_no_binary_builds_0/workspace/tmp/pip-install-b_6lf4z6/setuptools/pkg_resources.py", line 1545, in <module> register_loader_type(importlib_bootstrap.SourceFileLoader, DefaultProvider) AttributeError: module 'importlib._bootstrap' has no attribute 'SourceFileLoader' ---------------------------------------- ERROR: Command errored out with exit status 1: python setup.py egg_info Check the logs for full command output. ---------------------------------------- ERROR: Command errored out with exit status 1: /tmp/pytest-of-mockbuild/pytest-0/test_install_no_binary_builds_0/workspace/venv/bin/python /builddir/build/BUILDROOT/python-pip-19.3.1-1.fc32.noarch/usr/lib/python3.8/site-packages/pip install --ignore-installed --no-user --prefix /tmp/pytest-of-mockbuild/pytest-0/test_install_no_binary_builds_0/workspace/tmp/pip-build-env-ntp1m4dh/overlay --no-warn-script-location --no-binary :all: --only-binary :none: -i https://pypi.org/simple --find-links file:///tmp/pytest-of-mockbuild/pytest-0/test_install_no_binary_builds_0/data/packages -- setuptools Check the logs for full command output. -- stdout: -------------------- Looking in links: file:///tmp/pytest-of-mockbuild/pytest-0/test_install_no_binary_builds_0/data/packages Processing /tmp/pytest-of-mockbuild/pytest-0/test_install_no_binary_builds_0/data/packages/pep517_setup_and_pyproject Installing build dependencies: started Installing build dependencies: finished with status 'error' _______________________ test_config_file_override_stack ________________________ script = <tests.lib.PipTestEnvironment object at 0x7fe950d9b7f0> virtualenv = <VirtualEnvironment /tmp/pytest-of-mockbuild/pytest-0/test_config_file_override_stac0/workspace/venv> def test_config_file_override_stack(script, virtualenv): """ Test config files (global, overriding a global config with a local, overriding all with a command line flag). """ fd, config_file = tempfile.mkstemp('-pip.cfg', 'test-') try: > _test_config_file_override_stack(script, virtualenv, config_file) tests/functional/test_install_config.py:144: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ tests/functional/test_install_config.py:172: in _test_config_file_override_stack result = script.pip( _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <tests.lib.PipTestEnvironment object at 0x7fe950d9b7f0> args = ('python', '-m', 'pip', 'install', '-vvv', '--index-url', ...) kw = {'expect_stderr': True} cwd = Path('/tmp/pytest-of-mockbuild/pytest-0/test_config_file_override_stac0/workspace/scratch') run_from = None, allow_stderr_error = False, allow_stderr_warning = False expect_error = None def run(self, *args, **kw): """ :param allow_stderr_error: whether a logged error is allowed in stderr. Passing True for this argument implies `allow_stderr_warning` since warnings are weaker than errors. :param allow_stderr_warning: whether a logged warning (or deprecation message) is allowed in stderr. :param expect_error: if False (the default), asserts that the command exits with 0. Otherwise, asserts that the command exits with a non-zero exit code. Passing True also implies allow_stderr_error and allow_stderr_warning. :param expect_stderr: whether to allow warnings in stderr (equivalent to `allow_stderr_warning`). This argument is an abbreviated version of `allow_stderr_warning` and is also kept for backwards compatibility. """ if self.verbose: print('>> running %s %s' % (args, kw)) cwd = kw.pop('cwd', None) run_from = kw.pop('run_from', None) assert not cwd or not run_from, "Don't use run_from; it's going away" cwd = cwd or run_from or self.cwd if sys.platform == 'win32': # Partial fix for ScriptTest.run using `shell=True` on Windows. args = [str(a).replace('^', '^^').replace('&', '^&') for a in args] # Remove `allow_stderr_error` and `allow_stderr_warning` before # calling run() because PipTestEnvironment doesn't support them. allow_stderr_error = kw.pop('allow_stderr_error', None) allow_stderr_warning = kw.pop('allow_stderr_warning', None) # Propagate default values. expect_error = kw.get('expect_error') if expect_error: # Then default to allowing logged errors. if allow_stderr_error is not None and not allow_stderr_error: raise RuntimeError( 'cannot pass allow_stderr_error=False with ' 'expect_error=True' ) allow_stderr_error = True elif kw.get('expect_stderr'): # Then default to allowing logged warnings. if allow_stderr_warning is not None and not allow_stderr_warning: raise RuntimeError( 'cannot pass allow_stderr_warning=False with ' 'expect_stderr=True' ) allow_stderr_warning = True if allow_stderr_error: if allow_stderr_warning is not None and not allow_stderr_warning: raise RuntimeError( 'cannot pass allow_stderr_warning=False with ' 'allow_stderr_error=True' ) # Default values if not set. if allow_stderr_error is None: allow_stderr_error = False if allow_stderr_warning is None: allow_stderr_warning = allow_stderr_error # Pass expect_stderr=True to allow any stderr. We do this because # we do our checking of stderr further on in check_stderr(). kw['expect_stderr'] = True > result = super(PipTestEnvironment, self).run(cwd=cwd, *args, **kw) E AssertionError: Script returned code: 1 tests/lib/__init__.py:586: AssertionError ----------------------------- Captured stdout call ----------------------------- Script result: python -m pip install -vvv --index-url https://pypi.org/simple/ INITools return code: 1 -- stderr: -------------------- WARNING: Retrying (Retry(total=4, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<pip._vendor.urllib3.connection.VerifiedHTTPSConnection object at 0x7f9669c3d8b0>: Failed to establish a new connection: [Errno -2] Name or service not known')': /simple/initools/ WARNING: Retrying (Retry(total=3, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<pip._vendor.urllib3.connection.VerifiedHTTPSConnection object at 0x7f9669c3da60>: Failed to establish a new connection: [Errno -2] Name or service not known')': /simple/initools/ WARNING: Retrying (Retry(total=2, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<pip._vendor.urllib3.connection.VerifiedHTTPSConnection object at 0x7f9669c3dbe0>: Failed to establish a new connection: [Errno -2] Name or service not known')': /simple/initools/ WARNING: Retrying (Retry(total=1, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<pip._vendor.urllib3.connection.VerifiedHTTPSConnection object at 0x7f9669c3dd60>: Failed to establish a new connection: [Errno -2] Name or service not known')': /simple/initools/ WARNING: Retrying (Retry(total=0, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<pip._vendor.urllib3.connection.VerifiedHTTPSConnection object at 0x7f966900f490>: Failed to establish a new connection: [Errno -2] Name or service not known')': /simple/initools/ ERROR: Could not find a version that satisfies the requirement INITools (from versions: none) ERROR: No matching distribution found for INITools -- stdout: -------------------- Created temporary directory: /tmp/pytest-of-mockbuild/pytest-0/test_config_file_override_stac0/workspace/tmp/pip-ephem-wheel-cache-6gj33ens Created temporary directory: /tmp/pytest-of-mockbuild/pytest-0/test_config_file_override_stac0/workspace/tmp/pip-req-tracker-s7_2cwgc Created requirements tracker '/tmp/pytest-of-mockbuild/pytest-0/test_config_file_override_stac0/workspace/tmp/pip-req-tracker-s7_2cwgc' Created temporary directory: /tmp/pytest-of-mockbuild/pytest-0/test_config_file_override_stac0/workspace/tmp/pip-install-_91mh3df Looking in indexes: https://pypi.org/simple/ 1 location(s) to search for versions of INITools: * https://pypi.org/simple/initools/ Getting page https://pypi.org/simple/initools/ Found index url https://pypi.org/simple/ Looking up "https://pypi.org/simple/initools/" in the cache Request header has "max_age" as 0, cache bypassed Starting new HTTPS connection (1): pypi.org:443 Incremented Retry for (url='/simple/initools/'): Retry(total=4, connect=None, read=None, redirect=None, status=None) Starting new HTTPS connection (2): pypi.org:443 Incremented Retry for (url='/simple/initools/'): Retry(total=3, connect=None, read=None, redirect=None, status=None) Starting new HTTPS connection (3): pypi.org:443 Incremented Retry for (url='/simple/initools/'): Retry(total=2, connect=None, read=None, redirect=None, status=None) Starting new HTTPS connection (4): pypi.org:443 Incremented Retry for (url='/simple/initools/'): Retry(total=1, connect=None, read=None, redirect=None, status=None) Starting new HTTPS connection (5): pypi.org:443 Incremented Retry for (url='/simple/initools/'): Retry(total=0, connect=None, read=None, redirect=None, status=None) Starting new HTTPS connection (6): pypi.org:443 Could not fetch URL https://pypi.org/simple/initools/: connection error: HTTPSConnectionPool(host='pypi.org', port=443): Max retries exceeded with url: /simple/initools/ (Caused by NewConnectionError('<pip._vendor.urllib3.connection.VerifiedHTTPSConnection object at 0x7f9669c15b50>: Failed to establish a new connection: [Errno -2] Name or service not known')) - skipping Given no hashes to check 0 links for project 'INITools': discarding no candidates Cleaning up... Removed build tracker '/tmp/pytest-of-mockbuild/pytest-0/test_config_file_override_stac0/workspace/tmp/pip-req-tracker-s7_2cwgc' Exception information: Traceback (most recent call last): File "/builddir/build/BUILDROOT/python-pip-19.3.1-1.fc32.noarch/usr/lib/python3.8/site-packages/pip/_internal/cli/base_command.py", line 153, in _main status = self.run(options, args) File "/builddir/build/BUILDROOT/python-pip-19.3.1-1.fc32.noarch/usr/lib/python3.8/site-packages/pip/_internal/commands/install.py", line 401, in run resolver.resolve(requirement_set) File "/builddir/build/BUILDROOT/python-pip-19.3.1-1.fc32.noarch/usr/lib/python3.8/site-packages/pip/_internal/legacy_resolve.py", line 202, in resolve self._resolve_one(requirement_set, req) File "/builddir/build/BUILDROOT/python-pip-19.3.1-1.fc32.noarch/usr/lib/python3.8/site-packages/pip/_internal/legacy_resolve.py", line 368, in _resolve_one abstract_dist = self._get_abstract_dist_for(req_to_install) File "/builddir/build/BUILDROOT/python-pip-19.3.1-1.fc32.noarch/usr/lib/python3.8/site-packages/pip/_internal/legacy_resolve.py", line 314, in _get_abstract_dist_for req.populate_link(self.finder, upgrade_allowed, self.require_hashes) File "/builddir/build/BUILDROOT/python-pip-19.3.1-1.fc32.noarch/usr/lib/python3.8/site-packages/pip/_internal/req/req_install.py", line 226, in populate_link self.link = finder.find_requirement(self, upgrade) File "/builddir/build/BUILDROOT/python-pip-19.3.1-1.fc32.noarch/usr/lib/python3.8/site-packages/pip/_internal/index.py", line 905, in find_requirement raise DistributionNotFound( pip._internal.exceptions.DistributionNotFound: No matching distribution found for INITools _______________________ test_no_upgrade_unless_requested _______________________ script = <tests.lib.PipTestEnvironment object at 0x7fe950d86070> def test_no_upgrade_unless_requested(script): """ No upgrade if not specifically requested. """ > script.pip('install', 'INITools==0.1') tests/functional/test_install_upgrade.py:16: _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ self = <tests.lib.PipTestEnvironment object at 0x7fe950d86070> args = ('python', '-m', 'pip', 'install', 'INITools==0.1') kw = {'expect_stderr': True} cwd = Path('/tmp/pytest-of-mockbuild/pytest-0/test_no_upgrade_unless_request0/workspace/scratch') run_from = None, allow_stderr_error = False, allow_stderr_warning = False expect_error = None def run(self, *args, **kw): """ :param allow_stderr_error: whether a logged error is allowed in stderr. Passing True for this argument implies `allow_stderr_warning` since warnings are weaker than errors. :param allow_stderr_warning: whether a logged warning (or deprecation message) is allowed in stderr. :param expect_error: if False (the default), asserts that the command exits with 0. Otherwise, asserts that the command exits with a non-zero exit code. Passing True also implies allow_stderr_error and allow_stderr_warning. :param expect_stderr: whether to allow warnings in stderr (equivalent to `allow_stderr_warning`). This argument is an abbreviated version of `allow_stderr_warning` and is also kept for backwards compatibility. """ if self.verbose: print('>> running %s %s' % (args, kw)) cwd = kw.pop('cwd', None) run_from = kw.pop('run_from', None) assert not cwd or not run_from, "Don't use run_from; it's going away" cwd = cwd or run_from or self.cwd if sys.platform == 'win32': # Partial fix for ScriptTest.run using `shell=True` on Windows. args = [str(a).replace('^', '^^').replace('&', '^&') for a in args] # Remove `allow_stderr_error` and `allow_stderr_warning` before # calling run() because PipTestEnvironment doesn't support them. allow_stderr_error = kw.pop('allow_stderr_error', None) allow_stderr_warning = kw.pop('allow_stderr_warning', None) # Propagate default values. expect_error = kw.get('expect_error') if expect_error: # Then default to allowing logged errors. if allow_stderr_error is not None and not allow_stderr_error: raise RuntimeError( 'cannot pass allow_stderr_error=False with ' 'expect_error=True' ) allow_stderr_error = True elif kw.get('expect_stderr'): # Then default to allowing logged warnings. if allow_stderr_warning is not None and not allow_stderr_warning: raise RuntimeError( 'cannot pass allow_stderr_warning=False with ' 'expect_stderr=True' ) allow_stderr_warning = True if allow_stderr_error: if allow_stderr_warning is not None and not allow_stderr_warning: raise RuntimeError( 'cannot pass allow_stderr_warning=False with ' 'allow_stderr_error=True' ) # Default values if not set. if allow_stderr_error is None: allow_stderr_error = False if allow_stderr_warning is None: allow_stderr_warning = allow_stderr_error # Pass expect_stderr=True to allow any stderr. We do this because # we do our checking of stderr further on in check_stderr(). kw['expect_stderr'] = True > result = super(PipTestEnvironment, self).run(cwd=cwd, *args, **kw) E AssertionError: Script returned code: 1 tests/lib/__init__.py:586: AssertionError ----------------------------- Captured stdout call ----------------------------- Script result: python -m pip install INITools==0.1 return code: 1 -- stderr: -------------------- WARNING: Retrying (Retry(total=4, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<pip._vendor.urllib3.connection.VerifiedHTTPSConnection object at 0x7fd66cc36700>: Failed to establish a new connection: [Errno -2] Name or service not known')': /simple/initools/ WARNING: Retrying (Retry(total=3, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<pip._vendor.urllib3.connection.VerifiedHTTPSConnection object at 0x7fd66cc36c40>: Failed to establish a new connection: [Errno -2] Name or service not known')': /simple/initools/ WARNING: Retrying (Retry(total=2, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<pip._vendor.urllib3.connection.VerifiedHTTPSConnection object at 0x7fd66cc36dc0>: Failed to establish a new connection: [Errno -2] Name or service not known')': /simple/initools/ WARNING: Retrying (Retry(total=1, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<pip._vendor.urllib3.connection.VerifiedHTTPSConnection object at 0x7fd66cc36f40>: Failed to establish a new connection: [Errno -2] Name or service not known')': /simple/initools/ WARNING: Retrying (Retry(total=0, connect=None, read=None, redirect=None, status=None)) after connection broken by 'NewConnectionError('<pip._vendor.urllib3.connection.VerifiedHTTPSConnection object at 0x7fd66be48100>: Failed to establish a new connection: [Errno -2] Name or service not known')': /simple/initools/ ERROR: Could not find a version that satisfies the requirement INITools==0.1 (from versions: none) ERROR: No matching distribution found for INITools==0.1 --- news/7359.trivial | 0 tests/functional/test_freeze.py | 3 +++ tests/functional/test_install.py | 1 + tests/functional/test_install_config.py | 1 + tests/functional/test_install_upgrade.py | 1 + 5 files changed, 6 insertions(+) create mode 100644 news/7359.trivial diff --git a/news/7359.trivial b/news/7359.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/functional/test_freeze.py b/tests/functional/test_freeze.py index d13c931d0ef..2e35de3e686 100644 --- a/tests/functional/test_freeze.py +++ b/tests/functional/test_freeze.py @@ -721,6 +721,7 @@ def test_freeze_user(script, virtualenv, data): assert 'simple2' not in result.stdout +@pytest.mark.network def test_freeze_path(tmpdir, script, data): """ Test freeze with --path. @@ -734,6 +735,7 @@ def test_freeze_path(tmpdir, script, data): _check_output(result.stdout, expected) +@pytest.mark.network @pytest.mark.incompatible_with_test_venv def test_freeze_path_exclude_user(tmpdir, script, data): """ @@ -756,6 +758,7 @@ def test_freeze_path_exclude_user(tmpdir, script, data): _check_output(result.stdout, expected) +@pytest.mark.network def test_freeze_path_multiple(tmpdir, script, data): """ Test freeze with multiple --path arguments. diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index a9eff081de2..ba7a0a55c3c 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1325,6 +1325,7 @@ def test_install_no_binary_disables_building_wheels(script, data, with_wheel): assert "Running setup.py install for upper" in str(res), str(res) +@pytest.mark.network def test_install_no_binary_builds_pep_517_wheel(script, data, with_wheel): to_install = data.packages.joinpath('pep517_setup_and_pyproject') res = script.pip( diff --git a/tests/functional/test_install_config.py b/tests/functional/test_install_config.py index 176976c4e46..3082639288e 100644 --- a/tests/functional/test_install_config.py +++ b/tests/functional/test_install_config.py @@ -133,6 +133,7 @@ def test_command_line_appends_correctly(script, data): ), 'stdout: {}'.format(result.stdout) +@pytest.mark.network def test_config_file_override_stack(script, virtualenv): """ Test config files (global, overriding a global config with a diff --git a/tests/functional/test_install_upgrade.py b/tests/functional/test_install_upgrade.py index 6d2eeb5dc4e..0024de4d438 100644 --- a/tests/functional/test_install_upgrade.py +++ b/tests/functional/test_install_upgrade.py @@ -8,6 +8,7 @@ from tests.lib.local_repos import local_checkout +@pytest.mark.network def test_no_upgrade_unless_requested(script): """ No upgrade if not specifically requested. From f6aaba9fd99ecae2d90b97048dc227da9e2e3dbd Mon Sep 17 00:00:00 2001 From: mdebi <17590103+mdebi@users.noreply.github.com> Date: Sat, 16 Nov 2019 04:46:41 +0530 Subject: [PATCH 0792/3170] Warn if a path in PATH starts with tilde during install --- news/6414.feature | 1 + src/pip/_internal/wheel.py | 11 +++++++++ tests/unit/test_wheel.py | 46 ++++++++++++++++++++++++++++++++++++++ 3 files changed, 58 insertions(+) create mode 100644 news/6414.feature diff --git a/news/6414.feature b/news/6414.feature new file mode 100644 index 00000000000..5a72befdcfc --- /dev/null +++ b/news/6414.feature @@ -0,0 +1 @@ +Warn if a path in PATH starts with tilde during ``pip install``. diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index ab2abba7b23..a38ff38982e 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -210,6 +210,17 @@ def message_about_scripts_not_on_PATH(scripts): else: msg_lines.append(last_line_fmt.format("these directories")) + # Add a note if any directory starts with ~ + warn_for_tilde = any( + i[0] == "~" for i in os.environ.get("PATH", "").split(os.pathsep) if i + ) + if warn_for_tilde: + tilde_warning_msg = ( + "NOTE: The current PATH contains path(s) starting with `~`, " + "which may not be expanded by all applications." + ) + msg_lines.append(tilde_warning_msg) + # Returns the formatted multiline message return "\n".join(msg_lines) diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 0eccac90567..5292514cd10 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -524,6 +524,11 @@ def test_dist_info_contains_empty_dir(self, data, tmpdir): class TestMessageAboutScriptsNotOnPATH(object): + tilde_warning_msg = ( + "NOTE: The current PATH contains path(s) starting with `~`, " + "which may not be expanded by all applications." + ) + def _template(self, paths, scripts): with patch.dict('os.environ', {'PATH': os.pathsep.join(paths)}): return wheel.message_about_scripts_not_on_PATH(scripts) @@ -543,6 +548,7 @@ def test_single_script__single_dir_not_on_PATH(self): assert retval is not None assert "--no-warn-script-location" in retval assert "foo is installed in '/c/d'" in retval + assert self.tilde_warning_msg not in retval def test_two_script__single_dir_not_on_PATH(self): retval = self._template( @@ -552,6 +558,7 @@ def test_two_script__single_dir_not_on_PATH(self): assert retval is not None assert "--no-warn-script-location" in retval assert "baz and foo are installed in '/c/d'" in retval + assert self.tilde_warning_msg not in retval def test_multi_script__multi_dir_not_on_PATH(self): retval = self._template( @@ -562,6 +569,7 @@ def test_multi_script__multi_dir_not_on_PATH(self): assert "--no-warn-script-location" in retval assert "bar, baz and foo are installed in '/c/d'" in retval assert "spam is installed in '/a/b/c'" in retval + assert self.tilde_warning_msg not in retval def test_multi_script_all__multi_dir_not_on_PATH(self): retval = self._template( @@ -575,6 +583,7 @@ def test_multi_script_all__multi_dir_not_on_PATH(self): assert "--no-warn-script-location" in retval assert "bar, baz and foo are installed in '/c/d'" in retval assert "eggs and spam are installed in '/a/b/c'" in retval + assert self.tilde_warning_msg not in retval def test_two_script__single_dir_on_PATH(self): retval = self._template( @@ -613,6 +622,7 @@ def test_PATH_check_case_insensitive_on_windows(self): assert retval is None else: assert retval is not None + assert self.tilde_warning_msg not in retval def test_trailing_ossep_removal(self): retval = self._template( @@ -634,6 +644,42 @@ def test_missing_PATH_env_treated_as_empty_PATH_env(self): assert retval_missing == retval_empty + def test_no_script_tilde_in_path(self): + retval = self._template( + paths=['/a/b', '/c/d/bin', '~/e', '/f/g~g'], + scripts=[] + ) + assert retval is None + + def test_multi_script_all_tilde__multi_dir_not_on_PATH(self): + retval = self._template( + paths=['/a/b', '/c/d/bin', '~e/f'], + scripts=[ + '/c/d/foo', '/c/d/bar', '/c/d/baz', + '/a/b/c/spam', '/a/b/c/eggs', '/e/f/tilde' + ] + ) + assert retval is not None + assert "--no-warn-script-location" in retval + assert "bar, baz and foo are installed in '/c/d'" in retval + assert "eggs and spam are installed in '/a/b/c'" in retval + assert "tilde is installed in '/e/f'" in retval + assert self.tilde_warning_msg in retval + + def test_multi_script_all_tilde_not_at_start__multi_dir_not_on_PATH(self): + retval = self._template( + paths=['/e/f~f', '/c/d/bin'], + scripts=[ + '/c/d/foo', '/c/d/bar', '/c/d/baz', + '/e/f~f/c/spam', '/e/f~f/c/eggs' + ] + ) + assert retval is not None + assert "--no-warn-script-location" in retval + assert "bar, baz and foo are installed in '/c/d'" in retval + assert "eggs and spam are installed in '/e/f~f/c'" in retval + assert self.tilde_warning_msg not in retval + class TestWheelHashCalculators(object): From a2d473ff528f314bfc93dfc8768fffea67565c3b Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 15 Nov 2019 20:38:52 -0500 Subject: [PATCH 0793/3170] Add NEWS --- news/7355.removal | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 news/7355.removal diff --git a/news/7355.removal b/news/7355.removal new file mode 100644 index 00000000000..3f61b40112c --- /dev/null +++ b/news/7355.removal @@ -0,0 +1,4 @@ +Remove interpreter-specific major version tag e.g. ``cp3-none-any`` +from consideration. This behavior was not documented strictly, and this +tag in particular is `not useful <https://snarky.ca/the-challenges-in-designing-a-library-for-pep-425/>`_. +Anyone with a use case can create an issue with pypa/packaging. From f777ac727665b433d53c9a16fc70dcbe045b59cd Mon Sep 17 00:00:00 2001 From: Maxim Kurnikov <maxim.kurnikov@gmail.com> Date: Thu, 14 Nov 2019 19:52:34 +0300 Subject: [PATCH 0794/3170] remove disallow_untyped_defs=False from models.format_control --- src/pip/_internal/models/format_control.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/models/format_control.py b/src/pip/_internal/models/format_control.py index 3f4a219e257..2e13727ca00 100644 --- a/src/pip/_internal/models/format_control.py +++ b/src/pip/_internal/models/format_control.py @@ -1,6 +1,5 @@ # The following comment should be removed at some point in the future. # mypy: strict-optional=False -# mypy: disallow-untyped-defs=False from pip._vendor.packaging.utils import canonicalize_name @@ -26,12 +25,15 @@ def __init__(self, no_binary=None, only_binary=None): self.only_binary = only_binary def __eq__(self, other): + # type: (object) -> bool return self.__dict__ == other.__dict__ def __ne__(self, other): + # type: (object) -> bool return not self.__eq__(other) def __repr__(self): + # type: () -> str return "{}({}, {})".format( self.__class__.__name__, self.no_binary, From e9fcd75cab8453526e8f52bd9b5f0bcc20d73710 Mon Sep 17 00:00:00 2001 From: Maxim Kurnikov <maxim.kurnikov@gmail.com> Date: Sat, 16 Nov 2019 16:53:44 +0300 Subject: [PATCH 0795/3170] add disallow_untyped_defs=True for pip._internal.collector --- src/pip/_internal/index/collector.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/index/collector.py b/src/pip/_internal/index/collector.py index b390cdc8227..8330793171a 100644 --- a/src/pip/_internal/index/collector.py +++ b/src/pip/_internal/index/collector.py @@ -2,9 +2,6 @@ The main purpose of this module is to expose LinkCollector.collect_links(). """ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - import cgi import itertools import logging @@ -290,6 +287,7 @@ def __init__( self.url = url def __str__(self): + # type: () -> str return redact_auth_from_url(self.url) @@ -385,6 +383,7 @@ def group_locations(locations, expand_dir=False): # puts the url for the given file path into the appropriate list def sort_path(path): + # type: (str) -> None url = path_to_url(path) if mimetypes.guess_type(url, strict=False)[0] == 'text/html': urls.append(url) From f5f1761aba125c66996bb3be4aa384837abbb29b Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 16 Nov 2019 11:47:42 -0500 Subject: [PATCH 0796/3170] Inline content-type retrieval This makes it easier to factor out the filename calculation. --- src/pip/_internal/operations/prepare.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 017afef65ca..c8546261e67 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -442,7 +442,6 @@ def _download_http_url( ) raise - content_type = resp.headers.get('content-type', '') filename = link.filename # fallback # Have a look at the Content-Disposition header for a better guess content_disposition = resp.headers.get('content-disposition') @@ -450,7 +449,9 @@ def _download_http_url( filename = parse_content_disposition(content_disposition, filename) ext = splitext(filename)[1] # type: Optional[str] if not ext: - ext = mimetypes.guess_extension(content_type) + ext = mimetypes.guess_extension( + resp.headers.get('content-type', '') + ) if ext: filename += ext if not ext and link.url != resp.url: @@ -460,7 +461,7 @@ def _download_http_url( file_path = os.path.join(temp_dir, filename) with open(file_path, 'wb') as content_file: _download_url(resp, link, content_file, hashes, progress_bar) - return file_path, content_type + return file_path, resp.headers.get('content-type', '') def _check_download_dir(link, download_dir, hashes): From dd08330ac3a9bb66d8dd2cb7c8f2e94b1b734fb9 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 16 Nov 2019 11:50:14 -0500 Subject: [PATCH 0797/3170] Construct download name in separate function Later, we will use this to make a dedicated object that represents our download. --- src/pip/_internal/operations/prepare.py | 41 +++++++++++++++---------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index c8546261e67..e58a4442233 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -400,6 +400,30 @@ def parse_content_disposition(content_disposition, default_filename): return filename or default_filename +def _get_http_response_filename(resp, link): + # type: (Response, Link) -> str + """Get an ideal filename from the given HTTP response, falling back to + the link filename if not provided. + """ + filename = link.filename # fallback + # Have a look at the Content-Disposition header for a better guess + content_disposition = resp.headers.get('content-disposition') + if content_disposition: + filename = parse_content_disposition(content_disposition, filename) + ext = splitext(filename)[1] # type: Optional[str] + if not ext: + ext = mimetypes.guess_extension( + resp.headers.get('content-type', '') + ) + if ext: + filename += ext + if not ext and link.url != resp.url: + ext = os.path.splitext(resp.url)[1] + if ext: + filename += ext + return filename + + def _download_http_url( link, # type: Link session, # type: PipSession @@ -442,22 +466,7 @@ def _download_http_url( ) raise - filename = link.filename # fallback - # Have a look at the Content-Disposition header for a better guess - content_disposition = resp.headers.get('content-disposition') - if content_disposition: - filename = parse_content_disposition(content_disposition, filename) - ext = splitext(filename)[1] # type: Optional[str] - if not ext: - ext = mimetypes.guess_extension( - resp.headers.get('content-type', '') - ) - if ext: - filename += ext - if not ext and link.url != resp.url: - ext = os.path.splitext(resp.url)[1] - if ext: - filename += ext + filename = _get_http_response_filename(resp, link) file_path = os.path.join(temp_dir, filename) with open(file_path, 'wb') as content_file: _download_url(resp, link, content_file, hashes, progress_bar) From 6e68c8d9bac6c417b1d52e91aa5d6fd8b32f65f9 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 16 Nov 2019 11:56:54 -0500 Subject: [PATCH 0798/3170] Retrieve HTTP response size in separate function Also use `None` as the "no size available" indicator value, which is already compatible with all of its current uses. --- src/pip/_internal/operations/prepare.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index e58a4442233..7118e7dda4e 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -113,6 +113,14 @@ def _progress_indicator(iterable, *args, **kwargs): return iterable +def _get_http_response_size(resp): + # type: (Response) -> Optional[int] + try: + return int(resp.headers['content-length']) + except (ValueError, KeyError, TypeError): + return None + + def _download_url( resp, # type: Response link, # type: Link @@ -121,10 +129,7 @@ def _download_url( progress_bar # type: str ): # type: (...) -> None - try: - total_length = int(resp.headers['content-length']) - except (ValueError, KeyError, TypeError): - total_length = 0 + total_length = _get_http_response_size(resp) if link.netloc == PyPI.file_storage_domain: url = link.show_url @@ -142,10 +147,10 @@ def _download_url( show_progress = False elif is_from_cache(resp): show_progress = False - elif total_length > (40 * 1000): - show_progress = True elif not total_length: show_progress = True + elif total_length > (40 * 1000): + show_progress = True else: show_progress = False From a9997127b93a2bf1eaf290d1125a9e9e9d0ec53f Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 16 Nov 2019 12:16:57 -0500 Subject: [PATCH 0799/3170] Don't pass explicit step size to progress indicator Now the place we construct the progress indicator doesn't need to know about our strategy for consuming the response, freeing us to extract the chunk iterator construction into the caller. --- src/pip/_internal/operations/prepare.py | 3 +-- src/pip/_internal/utils/ui.py | 4 ++-- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 7118e7dda4e..ce739aedd50 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -168,8 +168,7 @@ def written_chunks(chunks): downloaded_chunks = written_chunks( progress_indicator( - response_chunks(resp, CONTENT_CHUNK_SIZE), - CONTENT_CHUNK_SIZE + response_chunks(resp, CONTENT_CHUNK_SIZE) ) ) if hashes: diff --git a/src/pip/_internal/utils/ui.py b/src/pip/_internal/utils/ui.py index 45e509b6bcb..87782aa641d 100644 --- a/src/pip/_internal/utils/ui.py +++ b/src/pip/_internal/utils/ui.py @@ -156,10 +156,10 @@ def pretty_eta(self): return "eta %s" % self.eta_td return "" - def iter(self, it, n=1): + def iter(self, it): for x in it: yield x - self.next(n) + self.next(len(x)) self.finish() From 09b19dbbb81a43b14afbacfb121e6948f83c64a8 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 16 Nov 2019 12:22:45 -0500 Subject: [PATCH 0800/3170] Move actual session.get call into separate function By moving this call (and its long explanation) to a separate function, diffs for later refactoring will be easier to review. --- src/pip/_internal/operations/prepare.py | 58 ++++++++++++++----------- 1 file changed, 32 insertions(+), 26 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index ce739aedd50..e2da45cb25c 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -428,6 +428,37 @@ def _get_http_response_filename(resp, link): return filename +def _http_get_download(session, link): + # type: (PipSession, Link) -> Response + target_url = link.url.split('#', 1)[0] + resp = session.get( + target_url, + # We use Accept-Encoding: identity here because requests + # defaults to accepting compressed responses. This breaks in + # a variety of ways depending on how the server is configured. + # - Some servers will notice that the file isn't a compressible + # file and will leave the file alone and with an empty + # Content-Encoding + # - Some servers will notice that the file is already + # compressed and will leave the file alone and will add a + # Content-Encoding: gzip header + # - Some servers won't notice anything at all and will take + # a file that's already been compressed and compress it again + # and set the Content-Encoding: gzip header + # By setting this to request only the identity encoding We're + # hoping to eliminate the third case. Hopefully there does not + # exist a server which when given a file will notice it is + # already compressed and that you're not asking for a + # compressed file and will then decompress it before sending + # because if that's the case I don't think it'll ever be + # possible to make this work. + headers={"Accept-Encoding": "identity"}, + stream=True, + ) + resp.raise_for_status() + return resp + + def _download_http_url( link, # type: Link session, # type: PipSession @@ -437,33 +468,8 @@ def _download_http_url( ): # type: (...) -> Tuple[str, str] """Download link url into temp_dir using provided session""" - target_url = link.url.split('#', 1)[0] try: - resp = session.get( - target_url, - # We use Accept-Encoding: identity here because requests - # defaults to accepting compressed responses. This breaks in - # a variety of ways depending on how the server is configured. - # - Some servers will notice that the file isn't a compressible - # file and will leave the file alone and with an empty - # Content-Encoding - # - Some servers will notice that the file is already - # compressed and will leave the file alone and will add a - # Content-Encoding: gzip header - # - Some servers won't notice anything at all and will take - # a file that's already been compressed and compress it again - # and set the Content-Encoding: gzip header - # By setting this to request only the identity encoding We're - # hoping to eliminate the third case. Hopefully there does not - # exist a server which when given a file will notice it is - # already compressed and that you're not asking for a - # compressed file and will then decompress it before sending - # because if that's the case I don't think it'll ever be - # possible to make this work. - headers={"Accept-Encoding": "identity"}, - stream=True, - ) - resp.raise_for_status() + resp = _http_get_download(session, link) except requests.HTTPError as exc: logger.critical( "HTTP error %s while getting %s", exc.response.status_code, link, From 5051c74d5ef9bf7840560faab5e2836000c86b9b Mon Sep 17 00:00:00 2001 From: Ahilya <ahilya16009@iiitd.ac.in> Date: Sun, 17 Nov 2019 16:49:43 +0530 Subject: [PATCH 0801/3170] Redact passwords from index-url during download Made changes in _download_url so that it relies on redact_auth_from_url from misc to redact passwords. Closes https://github.com/pypa/pip/issues/6783 --- news/6783.bugfix | 2 ++ src/pip/_internal/operations/prepare.py | 11 ++++++++--- 2 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 news/6783.bugfix diff --git a/news/6783.bugfix b/news/6783.bugfix new file mode 100644 index 00000000000..239d962f2ba --- /dev/null +++ b/news/6783.bugfix @@ -0,0 +1,2 @@ +Fix passwords being visible in the index-url in +"Downloading <url>" message. diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 017afef65ca..44b62b1fff9 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -46,6 +46,7 @@ hide_url, normalize_path, path_to_display, + redact_auth_from_url, rmtree, splitext, ) @@ -131,12 +132,16 @@ def _download_url( else: url = link.url_without_fragment + redacted_url = redact_auth_from_url(url) + if total_length: - logger.info("Downloading %s (%s)", url, format_size(total_length)) + logger.info( + "Downloading %s (%s)", redacted_url, format_size(total_length) + ) elif is_from_cache(resp): - logger.info("Using cached %s", url) + logger.info("Using cached %s", redacted_url) else: - logger.info("Downloading %s", url) + logger.info("Downloading %s", redacted_url) if logger.getEffectiveLevel() > logging.INFO: show_progress = False From 31f49023bd0a055ec5e2dd63e4244fff5c861df1 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 17 Nov 2019 15:43:45 -0500 Subject: [PATCH 0802/3170] Add mock_server documentation --- tests/lib/server.py | 41 ++++++++++++++++++++++++++++++++++++----- 1 file changed, 36 insertions(+), 5 deletions(-) diff --git a/tests/lib/server.py b/tests/lib/server.py index 0745abaee6b..bb423a2d867 100644 --- a/tests/lib/server.py +++ b/tests/lib/server.py @@ -51,9 +51,9 @@ def blocked_signals(): signal.pthread_sigmask(signal.SIG_SETMASK, old_mask) -class RequestHandler(WSGIRequestHandler): +class _RequestHandler(WSGIRequestHandler): def make_environ(self): - environ = super(RequestHandler, self).make_environ() + environ = super(_RequestHandler, self).make_environ() # From pallets/werkzeug#1469, will probably be in release after # 0.16.0. @@ -76,7 +76,7 @@ def make_environ(self): return environ -def mock_wsgi_adapter(mock): +def _mock_wsgi_adapter(mock): # type: (Callable[[Environ, StartResponse], Responder]) -> Responder """Uses a mock to record function arguments and provide the actual function that should respond. @@ -91,10 +91,39 @@ def adapter(environ, start_response): def make_mock_server(**kwargs): # type: (Any) -> MockServer - kwargs.setdefault("request_handler", RequestHandler) + """Creates a mock HTTP(S) server listening on a random port on localhost. + + The `mock` property of the returned server provides and records all WSGI + interactions, so one approach to testing could be + + server = make_mock_server() + server.mock.side_effects = [ + page1, + page2, + ] + + with server_running(server): + # ... use server... + ... + + assert server.mock.call_count > 0 + call_args_list = server.mock.call_args_list + + # `environ` is a dictionary defined as per PEP 3333 with the associated + # contents. Additional properties may be added by werkzeug. + environ, _ = call_args_list[0].args + assert environ["PATH_INFO"].startswith("/hello/simple") + + Note that the server interactions take place in a different thread, so you + do not want to touch the server.mock within the `server_running` block. + + Note also for pip interactions that "localhost" is a "secure origin", so + be careful using this for failure tests of `--trusted-host`. + """ + kwargs.setdefault("request_handler", _RequestHandler) mock = Mock() - app = mock_wsgi_adapter(mock) + app = _mock_wsgi_adapter(mock) server = _make_server("localhost", 0, app=app, **kwargs) server.mock = mock return server @@ -103,6 +132,8 @@ def make_mock_server(**kwargs): @contextmanager def server_running(server): # type: (BaseWSGIServer) -> None + """Context manager for running the provided server in a separate thread. + """ thread = threading.Thread(target=server.serve_forever) thread.daemon = True with blocked_signals(): From bf7ad4a2a15cbce04362445ddea95879875aca17 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 17 Nov 2019 15:50:10 -0500 Subject: [PATCH 0803/3170] Remove unnecessary wrapping of tests The cited issue https://bugs.python.org/issue3210 is fixed in all the versions of Python that we test against. --- tests/functional/test_install_config.py | 35 +++++-------------------- 1 file changed, 7 insertions(+), 28 deletions(-) diff --git a/tests/functional/test_install_config.py b/tests/functional/test_install_config.py index 3082639288e..021a12d66b9 100644 --- a/tests/functional/test_install_config.py +++ b/tests/functional/test_install_config.py @@ -44,26 +44,15 @@ def test_command_line_options_override_env_vars(script, virtualenv): def test_env_vars_override_config_file(script, virtualenv): """ Test that environmental variables override settings in config files. - """ - fd, config_file = tempfile.mkstemp('-pip.cfg', 'test-') - try: - _test_env_vars_override_config_file(script, virtualenv, config_file) - finally: - # `os.close` is a workaround for a bug in subprocess - # https://bugs.python.org/issue3210 - os.close(fd) - os.remove(config_file) - - -def _test_env_vars_override_config_file(script, virtualenv, config_file): + config_file = script.scratch_path / "test-pip.cfg" # set this to make pip load it - script.environ['PIP_CONFIG_FILE'] = config_file + script.environ['PIP_CONFIG_FILE'] = str(config_file) # It's important that we test this particular config value ('no-index') # because there is/was a bug which only shows up in cases in which # 'config-item' and 'config_item' hash to the same value modulo the size # of the config dictionary. - (script.scratch_path / config_file).write_text(textwrap.dedent("""\ + config_file.write_text(textwrap.dedent("""\ [global] no-index = 1 """)) @@ -138,22 +127,12 @@ def test_config_file_override_stack(script, virtualenv): """ Test config files (global, overriding a global config with a local, overriding all with a command line flag). - """ - fd, config_file = tempfile.mkstemp('-pip.cfg', 'test-') - try: - _test_config_file_override_stack(script, virtualenv, config_file) - finally: - # `os.close` is a workaround for a bug in subprocess - # https://bugs.python.org/issue3210 - os.close(fd) - os.remove(config_file) - + config_file = script.scratch_path / "test-pip.cfg" -def _test_config_file_override_stack(script, virtualenv, config_file): # set this to make pip load it - script.environ['PIP_CONFIG_FILE'] = config_file - (script.scratch_path / config_file).write_text(textwrap.dedent("""\ + script.environ['PIP_CONFIG_FILE'] = str(config_file) + config_file.write_text(textwrap.dedent("""\ [global] index-url = https://download.zope.org/ppix """)) @@ -162,7 +141,7 @@ def _test_config_file_override_stack(script, virtualenv, config_file): "Getting page https://download.zope.org/ppix/initools" in result.stdout ) virtualenv.clear() - (script.scratch_path / config_file).write_text(textwrap.dedent("""\ + config_file.write_text(textwrap.dedent("""\ [global] index-url = https://download.zope.org/ppix [install] From 1daa8b2fd96b4965fbc9c6d02f7f642168fc848a Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 17 Nov 2019 17:36:36 -0500 Subject: [PATCH 0804/3170] Add mock server wrapper and replace network calls in a test --- tests/conftest.py | 66 +++++++++++++++++++++++++ tests/functional/test_install_config.py | 57 ++++++++++++--------- 2 files changed, 100 insertions(+), 23 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 46213a127bc..34609ab569d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,18 +6,27 @@ import shutil import subprocess import sys +from contextlib import contextmanager import pytest import six +from pip._vendor.contextlib2 import ExitStack from setuptools.wheel import Wheel from pip._internal.main import main as pip_entry_point +from pip._internal.utils.typing import MYPY_CHECK_RUNNING from tests.lib import DATA_DIR, SRC_DIR, TestData from tests.lib.certs import make_tls_cert, serialize_cert, serialize_key from tests.lib.path import Path from tests.lib.scripttest import PipTestEnvironment +from tests.lib.server import make_mock_server, server_running from tests.lib.venv import VirtualEnvironment +if MYPY_CHECK_RUNNING: + from typing import Dict, Iterable + + from tests.lib.server import MockServer as _MockServer, Responder + def pytest_addoption(parser): parser.addoption( @@ -404,3 +413,60 @@ def factory(): return str(output_path) return factory + + +class MockServer(object): + def __init__(self, server): + # type: (_MockServer) -> None + self._server = server + self._running = False + self.context = ExitStack() + + @property + def port(self): + return self._server.port + + @property + def host(self): + return self._server.host + + def set_responses(self, responses): + # type: (Iterable[Responder]) -> None + assert not self._running, "responses cannot be set on running server" + self._server.mock.side_effect = responses + + def start(self): + # type: () -> None + assert not self._running, "running server cannot be started" + self.context.enter_context(server_running(self._server)) + self.context.enter_context(self._set_running()) + + @contextmanager + def _set_running(self): + self._running = True + try: + yield + finally: + self._running = False + + def stop(self): + # type: () -> None + assert self._running, "idle server cannot be stopped" + self.context.close() + + def get_requests(self): + # type: () -> Dict[str, str] + """Get environ for each received request. + """ + assert not self._running, "cannot get mock from running server" + return [ + call.args[0] for call in self._server.mock.call_args_list + ] + + +@pytest.fixture +def mock_server(): + server = make_mock_server() + test_server = MockServer(server) + with test_server.context: + yield test_server diff --git a/tests/functional/test_install_config.py b/tests/functional/test_install_config.py index 021a12d66b9..2b76e81cd25 100644 --- a/tests/functional/test_install_config.py +++ b/tests/functional/test_install_config.py @@ -4,6 +4,8 @@ import pytest +from tests.lib.server import file_response, package_page + def test_options_from_env_vars(script): """ @@ -122,46 +124,55 @@ def test_command_line_appends_correctly(script, data): ), 'stdout: {}'.format(result.stdout) -@pytest.mark.network -def test_config_file_override_stack(script, virtualenv): +def test_config_file_override_stack( + script, virtualenv, mock_server, shared_data +): """ Test config files (global, overriding a global config with a local, overriding all with a command line flag). """ + mock_server.set_responses([ + package_page({}), + package_page({}), + package_page({"INITools-0.2.tar.gz": "/files/INITools-0.2.tar.gz"}), + file_response(shared_data.packages.joinpath("INITools-0.2.tar.gz")), + ]) + mock_server.start() + base_address = "http://{}:{}".format(mock_server.host, mock_server.port) + config_file = script.scratch_path / "test-pip.cfg" # set this to make pip load it script.environ['PIP_CONFIG_FILE'] = str(config_file) + config_file.write_text(textwrap.dedent("""\ [global] - index-url = https://download.zope.org/ppix - """)) - result = script.pip('install', '-vvv', 'INITools', expect_error=True) - assert ( - "Getting page https://download.zope.org/ppix/initools" in result.stdout - ) + index-url = {}/simple1 + """.format(base_address))) + script.pip('install', '-vvv', 'INITools', expect_error=True) virtualenv.clear() + config_file.write_text(textwrap.dedent("""\ [global] - index-url = https://download.zope.org/ppix + index-url = {address}/simple1 [install] - index-url = https://pypi.gocept.com/ - """)) - result = script.pip('install', '-vvv', 'INITools', expect_error=True) - assert "Getting page https://pypi.gocept.com/initools" in result.stdout - result = script.pip( - 'install', '-vvv', '--index-url', 'https://pypi.org/simple/', - 'INITools', + index-url = {address}/simple2 + """.format(address=base_address)) ) - assert ( - "Getting page http://download.zope.org/ppix/INITools" - not in result.stdout - ) - assert "Getting page https://pypi.gocept.com/INITools" not in result.stdout - assert ( - "Getting page https://pypi.org/simple/initools" in result.stdout + script.pip('install', '-vvv', 'INITools', expect_error=True) + script.pip( + 'install', '-vvv', '--index-url', "{}/simple3".format(base_address), + 'INITools', ) + mock_server.stop() + requests = mock_server.get_requests() + assert len(requests) == 4 + assert requests[0]["PATH_INFO"] == "/simple1/initools/" + assert requests[1]["PATH_INFO"] == "/simple2/initools/" + assert requests[2]["PATH_INFO"] == "/simple3/initools/" + assert requests[3]["PATH_INFO"] == "/files/INITools-0.2.tar.gz" + def test_options_from_venv_config(script, virtualenv): """ From 2b6ed33f4cb27934c47b02aa815d9af85b44ea1d Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 17 Nov 2019 20:08:56 -0500 Subject: [PATCH 0805/3170] Remove extra escapes --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 4d81c157eae..1693edaffd2 100644 --- a/setup.py +++ b/setup.py @@ -17,7 +17,7 @@ def get_version(rel_path): for line in read(rel_path).splitlines(): if line.startswith('__version__'): # __version__ = "0.9" - delim = '\"' if '\"' in line else '\'' + delim = '"' if '"' in line else "'" return line.split(delim)[1] else: raise RuntimeError("Unable to find version string.") From ffe0e27c2b8e803a1517a30da0edfe7e6c0f21f7 Mon Sep 17 00:00:00 2001 From: Yeray Diaz Diaz <yeraydiazdiaz@gmail.com> Date: Mon, 18 Nov 2019 01:42:55 +0000 Subject: [PATCH 0806/3170] Update link to peer review PDF (#7360) --- docs/html/development/contributing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/html/development/contributing.rst b/docs/html/development/contributing.rst index 75108f515a2..85485dc1713 100644 --- a/docs/html/development/contributing.rst +++ b/docs/html/development/contributing.rst @@ -247,7 +247,7 @@ and they will initiate a vote among the existing maintainers. - CI Administration capabilities - ReadTheDocs Administration capabilities -.. _`Studies have shown`: https://smartbear.com/smartbear/media/pdfs/wp-cc-11-best-practices-of-peer-code-review.pdf +.. _`Studies have shown`: https://www.kessler.de/prd/smartbear/BestPracticesForPeerCodeReview.pdf .. _`resolve merge conflicts`: https://help.github.com/articles/resolving-a-merge-conflict-using-the-command-line/ .. _`Travis CI`: https://travis-ci.org/ .. _`Appveyor CI`: https://www.appveyor.com/ From 844fcc1cc54b3d1fe14bb91d26a02fc343be1e6a Mon Sep 17 00:00:00 2001 From: Swat009 <swatantra.kumar8@gmail.com> Date: Mon, 18 Nov 2019 07:15:35 +0530 Subject: [PATCH 0807/3170] Updated info about pip support for url_req portion of PEP508 (#6768) --- docs/html/reference/pip_install.rst | 3 ++- news/5860.trivial | 1 + 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 news/5860.trivial diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index 7e9e200e52e..37f74e3321b 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -247,7 +247,8 @@ pip supports installing from a package index using a :term:`requirement specifier <pypug:Requirement Specifier>`. Generally speaking, a requirement specifier is composed of a project name followed by optional :term:`version specifiers <pypug:Version Specifier>`. :pep:`508` contains a full specification -of the format of a requirement. +of the format of a requirement. Since version 18.1 pip supports the +``url_req``-form specification. Some examples: diff --git a/news/5860.trivial b/news/5860.trivial new file mode 100644 index 00000000000..7f77d3fd7ab --- /dev/null +++ b/news/5860.trivial @@ -0,0 +1 @@ +Updated info about pip support for url_req portion of PEP508 in doc. From a3bcaa4ea0192952ee0ba4b155135db5807b18c3 Mon Sep 17 00:00:00 2001 From: Aakanksha Agrawal <11389424+rasponic@users.noreply.github.com> Date: Mon, 18 Nov 2019 07:16:19 +0530 Subject: [PATCH 0808/3170] Explain how to get source code in getting started (#7197) --- docs/html/development/getting-started.rst | 13 +++++++++++++ news/7197.doc | 1 + 2 files changed, 14 insertions(+) create mode 100644 news/7197.doc diff --git a/docs/html/development/getting-started.rst b/docs/html/development/getting-started.rst index 41bf88b0f6f..66fae0d2775 100644 --- a/docs/html/development/getting-started.rst +++ b/docs/html/development/getting-started.rst @@ -8,6 +8,18 @@ This document is meant to get you setup to work on pip and to act as a guide and reference to the the development setup. If you face any issues during this process, please `open an issue`_ about it on the issue tracker. +Get the source code +------------------- + +To work on pip, you first need to get the source code of pip. The source code is +available on `GitHub`_. + +.. code-block:: console + + $ git clone https://github.com/pypa/pip + $ cd pip + + Development Environment ----------------------- @@ -103,3 +115,4 @@ The built documentation can be found in the ``docs/build`` folder. .. _`install Python`: https://realpython.com/installing-python/ .. _`PEP 484 type-comments`: https://www.python.org/dev/peps/pep-0484/#suggested-syntax-for-python-2-7-and-straddling-code .. _`rich CLI`: https://docs.pytest.org/en/latest/usage.html#specifying-tests-selecting-tests +.. _`GitHub`: https://github.com/pypa/pip diff --git a/news/7197.doc b/news/7197.doc new file mode 100644 index 00000000000..9168b21c813 --- /dev/null +++ b/news/7197.doc @@ -0,0 +1 @@ +Explain to to get source code in getting started From b802331fd2f34d5a0f359d98c9dbb18619fd1cf7 Mon Sep 17 00:00:00 2001 From: Christopher Hunt <chrahunt@gmail.com> Date: Tue, 19 Nov 2019 08:24:04 +0800 Subject: [PATCH 0809/3170] Simplify abi3 usage in pep425tags (#7367) abi3 refers to the CPython stable ABI, so we should only ensure it applies in that particular case. This simplifies the logic in get_platforms() and makes us more compatible with the behavior of packaging.tags, which only includes abi3 for cpython_tags() and only the literal "abi3". --- news/7327.removal | 2 ++ src/pip/_internal/pep425tags.py | 25 ++++++++++++------------- src/pip/_internal/utils/compat.py | 14 -------------- 3 files changed, 14 insertions(+), 27 deletions(-) create mode 100644 news/7327.removal diff --git a/news/7327.removal b/news/7327.removal new file mode 100644 index 00000000000..b35f58e0fd2 --- /dev/null +++ b/news/7327.removal @@ -0,0 +1,2 @@ +Use literal "abi3" for wheel tag on CPython 3.x, to align with PEP 384 +which only defines it for this platform. diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index 1335e323e03..2d1fb1eb98b 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -10,13 +10,14 @@ import warnings from collections import OrderedDict +from pip._vendor.six import PY2 + import pip._internal.utils.glibc -from pip._internal.utils.compat import get_extension_suffixes from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: from typing import ( - Tuple, Callable, List, Optional, Union, Dict, Set + Tuple, Callable, List, Optional, Union, Dict ) Pep425Tag = Tuple[str, str, str] @@ -361,12 +362,10 @@ def get_supported( if abi: abis[0:0] = [abi] - abi3s = set() # type: Set[str] - for suffix in get_extension_suffixes(): - if suffix.startswith('.abi'): - abi3s.add(suffix.split('.', 2)[1]) + supports_abi3 = not PY2 and impl == "cp" - abis.extend(sorted(list(abi3s))) + if supports_abi3: + abis.append("abi3") abis.append('none') @@ -419,13 +418,13 @@ def get_supported( supported.append(('%s%s' % (impl, versions[0]), abi, arch)) # abi3 modules compatible with older version of Python - for version in versions[1:]: - # abi3 was introduced in Python 3.2 - if version in {'31', '30'}: - break - for abi in abi3s: # empty set if not Python 3 + if supports_abi3: + for version in versions[1:]: + # abi3 was introduced in Python 3.2 + if version in {'31', '30'}: + break for arch in arches: - supported.append(("%s%s" % (impl, version), abi, arch)) + supported.append(("%s%s" % (impl, version), "abi3", arch)) # Has binaries, does not use the Python API: for arch in arches: diff --git a/src/pip/_internal/utils/compat.py b/src/pip/_internal/utils/compat.py index 0468b2b14ea..26f6b0ea5d7 100644 --- a/src/pip/_internal/utils/compat.py +++ b/src/pip/_internal/utils/compat.py @@ -43,7 +43,6 @@ __all__ = [ "ipaddress", "uses_pycache", "console_to_str", "get_path_uid", "stdlib_pkgs", "WINDOWS", "samefile", "get_terminal_size", - "get_extension_suffixes", ] @@ -189,19 +188,6 @@ def get_path_uid(path): return file_uid -if PY2: - from imp import get_suffixes - - def get_extension_suffixes(): - return [suffix[0] for suffix in get_suffixes()] - -else: - from importlib.machinery import EXTENSION_SUFFIXES - - def get_extension_suffixes(): - return EXTENSION_SUFFIXES - - def expanduser(path): # type: (str) -> str """ From 83babc7629a2de31f8fa2f84503e7084fad4ea7d Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 16 Nov 2019 18:38:41 -0500 Subject: [PATCH 0810/3170] Move mac arch calculation to separate function --- src/pip/_internal/pep425tags.py | 29 +++++++++++++++++------------ 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index 2d1fb1eb98b..d219f9ce6a2 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -326,6 +326,22 @@ def get_all_minor_versions_as_strings(version_info): return versions +def _mac_platforms(arch): + # type: (str) -> List[str] + match = _osx_arch_pat.match(arch) + if match: + name, major, minor, actual_arch = match.groups() + tpl = '{}_{}_%i_%s'.format(name, major) + arches = [] + for m in reversed(range(int(minor) + 1)): + for a in get_darwin_arches(int(major), m, actual_arch): + arches.append(tpl % (m, a)) + else: + # arch pattern didn't match (?!) + arches = [arch] + return arches + + def get_supported( version=None, # type: Optional[str] platform=None, # type: Optional[str] @@ -372,18 +388,7 @@ def get_supported( arch = platform or get_platform() arch_prefix, arch_sep, arch_suffix = arch.partition('_') if arch.startswith('macosx'): - # support macosx-10.6-intel on macosx-10.9-x86_64 - match = _osx_arch_pat.match(arch) - if match: - name, major, minor, actual_arch = match.groups() - tpl = '{}_{}_%i_%s'.format(name, major) - arches = [] - for m in reversed(range(int(minor) + 1)): - for a in get_darwin_arches(int(major), m, actual_arch): - arches.append(tpl % (m, a)) - else: - # arch pattern didn't match (?!) - arches = [arch] + arches = _mac_platforms(arch) elif arch_prefix == 'manylinux2014': arches = [arch] # manylinux1/manylinux2010 wheels run on most manylinux2014 systems From 49549fcd69f01ff36f2e3ed9d2bd0f9386292b23 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 16 Nov 2019 18:42:31 -0500 Subject: [PATCH 0811/3170] Move manylinux arch backfilling into separate function --- src/pip/_internal/pep425tags.py | 40 ++++++++++++++++++++------------- 1 file changed, 24 insertions(+), 16 deletions(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index d219f9ce6a2..fa9b3b0c383 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -342,6 +342,28 @@ def _mac_platforms(arch): return arches +def _custom_manylinux_platforms(arch): + # type: (str) -> List[str] + arches = [arch] + arch_prefix, arch_sep, arch_suffix = arch.partition('_') + if arch_prefix == 'manylinux2014': + # manylinux1/manylinux2010 wheels run on most manylinux2014 systems + # with the exception of wheels depending on ncurses. PEP 599 states + # manylinux1/manylinux2010 wheels should be considered + # manylinux2014 wheels: + # https://www.python.org/dev/peps/pep-0599/#backwards-compatibility-with-manylinux2010-wheels + if arch_suffix in {'i686', 'x86_64'}: + arches.append('manylinux2010' + arch_sep + arch_suffix) + arches.append('manylinux1' + arch_sep + arch_suffix) + elif arch_prefix == 'manylinux2010': + # manylinux1 wheels run on most manylinux2010 systems with the + # exception of wheels depending on ncurses. PEP 571 states + # manylinux1 wheels should be considered manylinux2010 wheels: + # https://www.python.org/dev/peps/pep-0571/#backwards-compatibility-with-manylinux1-wheels + arches.append('manylinux1' + arch_sep + arch_suffix) + return arches + + def get_supported( version=None, # type: Optional[str] platform=None, # type: Optional[str] @@ -389,22 +411,8 @@ def get_supported( arch_prefix, arch_sep, arch_suffix = arch.partition('_') if arch.startswith('macosx'): arches = _mac_platforms(arch) - elif arch_prefix == 'manylinux2014': - arches = [arch] - # manylinux1/manylinux2010 wheels run on most manylinux2014 systems - # with the exception of wheels depending on ncurses. PEP 599 states - # manylinux1/manylinux2010 wheels should be considered - # manylinux2014 wheels: - # https://www.python.org/dev/peps/pep-0599/#backwards-compatibility-with-manylinux2010-wheels - if arch_suffix in {'i686', 'x86_64'}: - arches.append('manylinux2010' + arch_sep + arch_suffix) - arches.append('manylinux1' + arch_sep + arch_suffix) - elif arch_prefix == 'manylinux2010': - # manylinux1 wheels run on most manylinux2010 systems with the - # exception of wheels depending on ncurses. PEP 571 states - # manylinux1 wheels should be considered manylinux2010 wheels: - # https://www.python.org/dev/peps/pep-0571/#backwards-compatibility-with-manylinux1-wheels - arches = [arch, 'manylinux1' + arch_sep + arch_suffix] + elif arch_prefix in ['manylinux2014', 'manylinux2010']: + arches = _custom_manylinux_platforms(arch) elif platform is None: arches = [] if is_manylinux2014_compatible(): From 2af6112751940544c51655b84540d861c670513e Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 16 Nov 2019 18:56:02 -0500 Subject: [PATCH 0812/3170] Cleanup pep425tags version handling --- src/pip/_internal/pep425tags.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index fa9b3b0c383..f77f231b833 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -391,6 +391,8 @@ def get_supported( versions = get_all_minor_versions_as_strings(version_info) else: versions = [version] + current_version = versions[0] + other_versions = versions[1:] impl = impl or get_abbr_impl() @@ -428,11 +430,11 @@ def get_supported( # Current version, current API (built specifically for our Python): for abi in abis: for arch in arches: - supported.append(('%s%s' % (impl, versions[0]), abi, arch)) + supported.append(('%s%s' % (impl, current_version), abi, arch)) # abi3 modules compatible with older version of Python if supports_abi3: - for version in versions[1:]: + for version in other_versions: # abi3 was introduced in Python 3.2 if version in {'31', '30'}: break @@ -441,16 +443,16 @@ def get_supported( # Has binaries, does not use the Python API: for arch in arches: - supported.append(('py%s' % (versions[0][0]), 'none', arch)) + supported.append(('py%s' % (current_version[0]), 'none', arch)) # No abi / arch, but requires our implementation: - supported.append(('%s%s' % (impl, versions[0]), 'none', 'any')) + supported.append(('%s%s' % (impl, current_version), 'none', 'any')) # No abi / arch, generic Python - for i, version in enumerate(versions): + supported.append(('py%s' % (current_version,), 'none', 'any')) + supported.append(('py%s' % (current_version[0]), 'none', 'any')) + for version in other_versions: supported.append(('py%s' % (version,), 'none', 'any')) - if i == 0: - supported.append(('py%s' % (version[0]), 'none', 'any')) return supported From 1b4c0866ab1108162ee00bd38a0fb5657b9e9aea Mon Sep 17 00:00:00 2001 From: Christopher Hunt <chrahunt@gmail.com> Date: Tue, 19 Nov 2019 11:46:26 +0800 Subject: [PATCH 0813/3170] Remove untyped defs (#7382) --- src/pip/_internal/configuration.py | 2 +- src/pip/_internal/locations.py | 2 +- src/pip/_internal/req/constructors.py | 2 +- src/pip/_internal/req/req_install.py | 2 +- src/pip/_internal/vcs/versioncontrol.py | 21 ++++++++++++++++----- 5 files changed, 20 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/configuration.py b/src/pip/_internal/configuration.py index 858c6602946..f09a1ae25c2 100644 --- a/src/pip/_internal/configuration.py +++ b/src/pip/_internal/configuration.py @@ -13,7 +13,6 @@ # The following comment should be removed at some point in the future. # mypy: strict-optional=False -# mypy: disallow-untyped-defs=False import locale import logging @@ -78,6 +77,7 @@ def _disassemble_key(name): def get_configuration_files(): + # type: () -> Dict[Kind, List[str]] global_config_files = [ os.path.join(path, CONFIG_BASENAME) for path in appdirs.site_config_dirs('pip') diff --git a/src/pip/_internal/locations.py b/src/pip/_internal/locations.py index be47ab17c7e..0c115531911 100644 --- a/src/pip/_internal/locations.py +++ b/src/pip/_internal/locations.py @@ -2,7 +2,6 @@ # The following comment should be removed at some point in the future. # mypy: strict-optional=False -# mypy: disallow-untyped-defs=False from __future__ import absolute_import @@ -42,6 +41,7 @@ def get_major_minor_version(): def get_src_prefix(): + # type: () -> str if running_under_virtualenv(): src_prefix = os.path.join(sys.prefix, 'src') else: diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index 03b51484b16..70fb5e0860f 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -10,7 +10,6 @@ # The following comment should be removed at some point in the future. # mypy: strict-optional=False -# mypy: disallow-untyped-defs=False import logging import os @@ -347,6 +346,7 @@ def parse_req_from_line(name, line_source): extras = convert_extras(extras_as_string) def with_source(text): + # type: (str) -> str if not line_source: return text return '{} (from {})'.format(text, line_source) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 1376c892b9f..a38a8ab4dad 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -1,6 +1,5 @@ # The following comment should be removed at some point in the future. # mypy: strict-optional=False -# mypy: disallow-untyped-defs=False from __future__ import absolute_import @@ -460,6 +459,7 @@ def move_to_correct_build_directory(self): self._ideal_build_dir = None def warn_on_mismatching_name(self): + # type: () -> None metadata_name = canonicalize_name(self.metadata["Name"]) if canonicalize_name(self.req.name) == metadata_name: # Everything is fine. diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index a1742cfc814..7cfd568829f 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -1,8 +1,5 @@ """Handles all VCS (version control) support""" -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - from __future__ import absolute_import import errno @@ -30,7 +27,8 @@ if MYPY_CHECK_RUNNING: from typing import ( - Any, Dict, Iterable, List, Mapping, Optional, Text, Tuple, Type, Union + Any, Dict, Iterable, Iterator, List, Mapping, Optional, Text, Tuple, + Type, Union ) from pip._internal.utils.ui import SpinnerInterface from pip._internal.utils.misc import HiddenText @@ -57,6 +55,7 @@ def is_url(name): def make_vcs_requirement_url(repo_url, rev, project_name, subdir=None): + # type: (str, str, str, Optional[str]) -> str """ Return the URL for a VCS requirement. @@ -73,6 +72,7 @@ def make_vcs_requirement_url(repo_url, rev, project_name, subdir=None): def find_path_to_setup_from_repo_root(location, repo_root): + # type: (str, str) -> Optional[str] """ Find the path to `setup.py` by searching up the filesystem from `location`. Return the path to `setup.py` relative to `repo_root`. @@ -134,6 +134,7 @@ def __init__( self.branch_name = None # type: Optional[str] def __repr__(self): + # type: () -> str return '<RevOptions {}: rev={!r}>'.format(self.vc_class.name, self.rev) @property @@ -190,6 +191,7 @@ def __init__(self): super(VcsSupport, self).__init__() def __iter__(self): + # type: () -> Iterator[str] return self._registry.__iter__() @property @@ -271,6 +273,7 @@ class VersionControl(object): @classmethod def should_add_vcs_url_prefix(cls, remote_url): + # type: (str) -> bool """ Return whether the vcs prefix (e.g. "git+") should be added to a repository's remote url when used in a requirement. @@ -279,6 +282,7 @@ def should_add_vcs_url_prefix(cls, remote_url): @classmethod def get_subdirectory(cls, location): + # type: (str) -> Optional[str] """ Return the path to setup.py, relative to the repo root. Return None if setup.py is in the repo root. @@ -287,6 +291,7 @@ def get_subdirectory(cls, location): @classmethod def get_requirement_revision(cls, repo_dir): + # type: (str) -> str """ Return the revision string that should be used in a requirement. """ @@ -294,6 +299,7 @@ def get_requirement_revision(cls, repo_dir): @classmethod def get_src_requirement(cls, repo_dir, project_name): + # type: (str, str) -> Optional[str] """ Return the requirement string to use to redownload the files currently at the given repository directory. @@ -321,6 +327,7 @@ def get_src_requirement(cls, repo_dir, project_name): @staticmethod def get_base_rev_args(rev): + # type: (str) -> List[str] """ Return the base revision arguments for a vcs command. @@ -377,6 +384,7 @@ def export(self, location, url): @classmethod def get_netloc_and_auth(cls, netloc, scheme): + # type: (str, str) -> Tuple[str, Tuple[Optional[str], Optional[str]]] """ Parse the repository URL's netloc, and return the new netloc to use along with auth information. @@ -494,6 +502,7 @@ def update(self, dest, url, rev_options): @classmethod def is_commit_id_equal(cls, dest, name): + # type: (str, Optional[str]) -> bool """ Return whether the id of the current commit equals the given name. @@ -610,6 +619,7 @@ def unpack(self, location, url): @classmethod def get_remote_url(cls, location): + # type: (str) -> str """ Return the url used at location @@ -620,6 +630,7 @@ def get_remote_url(cls, location): @classmethod def get_revision(cls, location): + # type: (str) -> str """ Return the current commit id of the files at the given location. """ @@ -636,7 +647,7 @@ def run_command( command_desc=None, # type: Optional[str] extra_environ=None, # type: Optional[Mapping[str, Any]] spinner=None, # type: Optional[SpinnerInterface] - log_failed_cmd=True + log_failed_cmd=True # type: bool ): # type: (...) -> Text """ From dca556fdaa5366473323f932403f4dc3032e40a5 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 17 Nov 2019 18:45:42 -0500 Subject: [PATCH 0814/3170] Check hashes after download is complete Instead of computing hashes on-the-fly we do it after fully downloading the file. This step will let us move hash checking to a higher-level function without introducing a lot of complexity. --- src/pip/_internal/operations/prepare.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 9f419952f70..32cc9aa14b8 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -126,7 +126,6 @@ def _download_url( resp, # type: Response link, # type: Link content_file, # type: IO[Any] - hashes, # type: Optional[Hashes] progress_bar # type: str ): # type: (...) -> None @@ -176,10 +175,7 @@ def written_chunks(chunks): response_chunks(resp, CONTENT_CHUNK_SIZE) ) ) - if hashes: - hashes.check_against_chunks(downloaded_chunks) - else: - consume(downloaded_chunks) + consume(downloaded_chunks) def _copy_file(filename, location, link): @@ -484,7 +480,11 @@ def _download_http_url( filename = _get_http_response_filename(resp, link) file_path = os.path.join(temp_dir, filename) with open(file_path, 'wb') as content_file: - _download_url(resp, link, content_file, hashes, progress_bar) + _download_url(resp, link, content_file, progress_bar) + + if hashes: + hashes.check_against_path(file_path) + return file_path, resp.headers.get('content-type', '') From e706af20fe9e73cab3482b1f34b5510e88909506 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 17 Nov 2019 18:58:32 -0500 Subject: [PATCH 0815/3170] Simplify writing file chunks A plain loop is easier to follow than chained generators consumed by a helper function, and reduces the number of objects being passed around just to download a file. --- src/pip/_internal/operations/prepare.py | 23 +++++++---------------- 1 file changed, 7 insertions(+), 16 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 32cc9aa14b8..3eee00a8a5c 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -40,7 +40,6 @@ from pip._internal.utils.misc import ( ask_path_exists, backup_dir, - consume, display_path, format_size, hide_url, @@ -58,7 +57,7 @@ if MYPY_CHECK_RUNNING: from typing import ( - Any, Callable, IO, List, Optional, Tuple, + Callable, Iterable, List, Optional, Tuple, ) from mypy_extensions import TypedDict @@ -122,13 +121,12 @@ def _get_http_response_size(resp): return None -def _download_url( +def _prepare_download( resp, # type: Response link, # type: Link - content_file, # type: IO[Any] progress_bar # type: str ): - # type: (...) -> None + # type: (...) -> Iterable[bytes] total_length = _get_http_response_size(resp) if link.netloc == PyPI.file_storage_domain: @@ -158,11 +156,6 @@ def _download_url( else: show_progress = False - def written_chunks(chunks): - for chunk in chunks: - content_file.write(chunk) - yield chunk - progress_indicator = _progress_indicator if show_progress: # We don't show progress on cached responses @@ -170,12 +163,9 @@ def written_chunks(chunks): progress_bar, max=total_length ) - downloaded_chunks = written_chunks( - progress_indicator( - response_chunks(resp, CONTENT_CHUNK_SIZE) - ) + return progress_indicator( + response_chunks(resp, CONTENT_CHUNK_SIZE) ) - consume(downloaded_chunks) def _copy_file(filename, location, link): @@ -480,7 +470,8 @@ def _download_http_url( filename = _get_http_response_filename(resp, link) file_path = os.path.join(temp_dir, filename) with open(file_path, 'wb') as content_file: - _download_url(resp, link, content_file, progress_bar) + for chunk in _prepare_download(resp, link, progress_bar): + content_file.write(chunk) if hashes: hashes.check_against_path(file_path) From b7a4b022d66d734fe69e16a4c715e96863b5c847 Mon Sep 17 00:00:00 2001 From: Keith Maxwell <keith.maxwell@gmail.com> Date: Wed, 20 Nov 2019 04:45:21 +0000 Subject: [PATCH 0816/3170] Better document the requirements file format (#7386) Change the documentation for the requirements file format so that it matches the implementation [0]. Also change the order of the options in the implementation so that the documentation reads better. Before this change the documentation included an incomplete list of supported options. This change adds the missing options and changes the order to match, so that the two locations are easier to keep in sync. After this change the list in the documentation matches SUPPORTED_OPTIONS in src/pip/_internal/req/req_file.py [0]: https://github.com/pypa/pip/blob/master/src/pip/_internal/req/req_file.py#L60 --- docs/html/reference/pip_install.rst | 4 ++++ news/7385.doc | 1 + src/pip/_internal/req/req_file.py | 12 ++++++------ 3 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 news/7385.doc diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index 37f74e3321b..2b627cbee64 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -156,10 +156,14 @@ The following options are supported: * :ref:`-i, --index-url <install_--index-url>` * :ref:`--extra-index-url <install_--extra-index-url>` * :ref:`--no-index <install_--no-index>` + * :ref:`-c, --constraint <install_--constraint>` + * :ref:`-r, --requirement <install_--requirement>` + * :ref:`-e, --editable <install_--editable>` * :ref:`-f, --find-links <install_--find-links>` * :ref:`--no-binary <install_--no-binary>` * :ref:`--only-binary <install_--only-binary>` * :ref:`--require-hashes <install_--require-hashes>` + * :ref:`--pre <install_--pre>` * :ref:`--trusted-host <--trusted-host>` For example, to specify :ref:`--no-index <install_--no-index>` and two diff --git a/news/7385.doc b/news/7385.doc new file mode 100644 index 00000000000..ec8c4a4a3cb --- /dev/null +++ b/news/7385.doc @@ -0,0 +1 @@ +Better document the requirements file format diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index d2cf6eb28d1..8c7810481ee 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -58,19 +58,19 @@ ENV_VAR_RE = re.compile(r'(?P<var>\$\{(?P<name>[A-Z0-9_]+)\})') SUPPORTED_OPTIONS = [ + cmdoptions.index_url, + cmdoptions.extra_index_url, + cmdoptions.no_index, cmdoptions.constraints, - cmdoptions.editable, cmdoptions.requirements, - cmdoptions.no_index, - cmdoptions.index_url, + cmdoptions.editable, cmdoptions.find_links, - cmdoptions.extra_index_url, - cmdoptions.always_unzip, cmdoptions.no_binary, cmdoptions.only_binary, + cmdoptions.require_hashes, cmdoptions.pre, cmdoptions.trusted_host, - cmdoptions.require_hashes, + cmdoptions.always_unzip, # Deprecated ] # type: List[Callable[..., optparse.Option]] # options to be passed to requirements From 46c20698c6bf37d9589729a675e01ce10faa1ba5 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 21 Nov 2019 11:04:32 +0530 Subject: [PATCH 0817/3170] Improve a NEWS fragment --- news/7197.doc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/7197.doc b/news/7197.doc index 9168b21c813..47b6d7c4add 100644 --- a/news/7197.doc +++ b/news/7197.doc @@ -1 +1 @@ -Explain to to get source code in getting started +Explain how to get pip's source code in `Getting Started <https://pip.pypa.io/en/stable/development/getting-started/>`_ From e4d2d34fe13d89bdc585f8a1b9c434886dd87e5f Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 17 Nov 2019 19:14:21 -0500 Subject: [PATCH 0818/3170] Represent download as its own type This abstraction will let us hide details about the actual downloading and display of progress from most of operations.prepare. --- src/pip/_internal/operations/prepare.py | 26 +++++++++++++++++++++---- 1 file changed, 22 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 3eee00a8a5c..ca2ce34f8a7 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -450,6 +450,19 @@ def _http_get_download(session, link): return resp +class Download(object): + def __init__( + self, + response, # type: Response + filename, # type: str + chunks, # type: Iterable[bytes] + ): + # type: (...) -> None + self.response = response + self.filename = filename + self.chunks = chunks + + def _download_http_url( link, # type: Link session, # type: PipSession @@ -467,16 +480,21 @@ def _download_http_url( ) raise - filename = _get_http_response_filename(resp, link) - file_path = os.path.join(temp_dir, filename) + download = Download( + resp, + _get_http_response_filename(resp, link), + _prepare_download(resp, link, progress_bar), + ) + + file_path = os.path.join(temp_dir, download.filename) with open(file_path, 'wb') as content_file: - for chunk in _prepare_download(resp, link, progress_bar): + for chunk in download.chunks: content_file.write(chunk) if hashes: hashes.check_against_path(file_path) - return file_path, resp.headers.get('content-type', '') + return file_path, download.response.headers.get('content-type', '') def _check_download_dir(link, download_dir, hashes): From 3ce317890eb0970a478d979783e931ea546ce3bd Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 17 Nov 2019 19:22:55 -0500 Subject: [PATCH 0819/3170] Introduce Downloader for progress-showing downloads --- src/pip/_internal/operations/prepare.py | 41 +++++++++++++++++-------- 1 file changed, 29 insertions(+), 12 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index ca2ce34f8a7..4f69e7e8afe 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -463,6 +463,33 @@ def __init__( self.chunks = chunks +class Downloader(object): + def __init__( + self, + session, # type: PipSession + progress_bar, # type: str + ): + # type: (...) -> None + self._session = session + self._progress_bar = progress_bar + + def __call__(self, link): + # type: (Link) -> Download + try: + resp = _http_get_download(self._session, link) + except requests.HTTPError as e: + logger.critical( + "HTTP error %s while getting %s", e.response.status_code, link + ) + raise + + return Download( + resp, + _get_http_response_filename(resp, link), + _prepare_download(resp, link, self._progress_bar), + ) + + def _download_http_url( link, # type: Link session, # type: PipSession @@ -472,19 +499,9 @@ def _download_http_url( ): # type: (...) -> Tuple[str, str] """Download link url into temp_dir using provided session""" - try: - resp = _http_get_download(session, link) - except requests.HTTPError as exc: - logger.critical( - "HTTP error %s while getting %s", exc.response.status_code, link, - ) - raise + downloader = Downloader(session, progress_bar) - download = Download( - resp, - _get_http_response_filename(resp, link), - _prepare_download(resp, link, progress_bar), - ) + download = downloader(link) file_path = os.path.join(temp_dir, download.filename) with open(file_path, 'wb') as content_file: From 4be0a5445ae83be6b6903bdb28979bbf2b1b8bff Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 17 Nov 2019 19:28:08 -0500 Subject: [PATCH 0820/3170] Move Downloader up out of _download_http_url Reduces exposure of session and progress_bar. --- src/pip/_internal/operations/prepare.py | 8 +++----- tests/unit/test_operations_prepare.py | 5 +++-- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 4f69e7e8afe..51c626118c2 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -219,9 +219,10 @@ def unpack_http_url( from_path = already_downloaded_path content_type = mimetypes.guess_type(from_path)[0] else: + downloader = Downloader(session, progress_bar) # let's download to a tmp dir from_path, content_type = _download_http_url( - link, session, temp_dir.path, hashes, progress_bar + link, downloader, temp_dir.path, hashes ) # unpack the archive to the build dir location. even when only @@ -492,15 +493,12 @@ def __call__(self, link): def _download_http_url( link, # type: Link - session, # type: PipSession + downloader, # type: Downloader temp_dir, # type: str hashes, # type: Optional[Hashes] - progress_bar # type: str ): # type: (...) -> Tuple[str, str] """Download link url into temp_dir using provided session""" - downloader = Downloader(session, progress_bar) - download = downloader(link) file_path = os.path.join(temp_dir, download.filename) diff --git a/tests/unit/test_operations_prepare.py b/tests/unit/test_operations_prepare.py index d9b3781e9c2..fea0cd0a94c 100644 --- a/tests/unit/test_operations_prepare.py +++ b/tests/unit/test_operations_prepare.py @@ -13,6 +13,7 @@ from pip._internal.models.link import Link from pip._internal.network.session import PipSession from pip._internal.operations.prepare import ( + Downloader, _copy_source_tree, _download_http_url, parse_content_disposition, @@ -228,15 +229,15 @@ def test_download_http_url__no_directory_traversal(tmpdir): 'content-disposition': 'attachment;filename="../out_dir_file"' } session.get.return_value = resp + downloader = Downloader(session, progress_bar="on") download_dir = tmpdir.joinpath('download') os.mkdir(download_dir) file_path, content_type = _download_http_url( link, - session, + downloader, download_dir, hashes=None, - progress_bar='on', ) # The file should be downloaded to download_dir. actual = os.listdir(download_dir) From 19806574e5256d065bb0209427e5d3e9c162d65a Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 17 Nov 2019 19:31:20 -0500 Subject: [PATCH 0821/3170] Move Downloader out of unpack_http_url Reduces scope of session and progress_bar. --- src/pip/_internal/operations/prepare.py | 9 ++++----- tests/unit/test_operations_prepare.py | 6 ++++-- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 51c626118c2..63086e23957 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -201,10 +201,9 @@ def _copy_file(filename, location, link): def unpack_http_url( link, # type: Link location, # type: str - session, # type: PipSession + downloader, # type: Downloader download_dir=None, # type: Optional[str] hashes=None, # type: Optional[Hashes] - progress_bar="on" # type: str ): # type: (...) -> None with TempDirectory(kind="unpack") as temp_dir: @@ -219,7 +218,6 @@ def unpack_http_url( from_path = already_downloaded_path content_type = mimetypes.guess_type(from_path)[0] else: - downloader = Downloader(session, progress_bar) # let's download to a tmp dir from_path, content_type = _download_http_url( link, downloader, temp_dir.path, hashes @@ -363,13 +361,14 @@ def unpack_url( # http urls else: + downloader = Downloader(session, progress_bar) + unpack_http_url( link, location, - session, + downloader, download_dir, hashes=hashes, - progress_bar=progress_bar ) diff --git a/tests/unit/test_operations_prepare.py b/tests/unit/test_operations_prepare.py index fea0cd0a94c..cdcc1400437 100644 --- a/tests/unit/test_operations_prepare.py +++ b/tests/unit/test_operations_prepare.py @@ -45,6 +45,7 @@ def _fake_session_get(*args, **kwargs): session = Mock() session.get = _fake_session_get + downloader = Downloader(session, progress_bar="on") uri = path_to_url(data.packages.joinpath("simple-1.0.tar.gz")) link = Link(uri) @@ -53,7 +54,7 @@ def _fake_session_get(*args, **kwargs): unpack_http_url( link, temp_dir, - session=session, + downloader=downloader, download_dir=None, ) assert set(os.listdir(temp_dir)) == { @@ -132,6 +133,7 @@ def test_unpack_http_url_bad_downloaded_checksum(mock_unpack_file): response = session.get.return_value = MockResponse(contents) response.headers = {'content-type': 'application/x-tar'} response.url = base_url + downloader = Downloader(session, progress_bar="on") download_dir = mkdtemp() try: @@ -141,7 +143,7 @@ def test_unpack_http_url_bad_downloaded_checksum(mock_unpack_file): unpack_http_url( link, 'location', - session=session, + downloader=downloader, download_dir=download_dir, hashes=Hashes({'sha1': [download_hash.hexdigest()]}) ) From 3c8be92c320f9b824659b029dd346144e11e1dab Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 17 Nov 2019 19:34:00 -0500 Subject: [PATCH 0822/3170] Move Downloader out of unpack_url This simplifies the work done in the operations.prepare helper functions and also opens up the door to remove session and progress_bar from RequirementPreparer itself. --- src/pip/_internal/operations/prepare.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 63086e23957..c4c7526f68a 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -331,10 +331,9 @@ def unpack_file_url( def unpack_url( link, # type: Link location, # type: str - session, # type: PipSession + downloader, # type: Downloader download_dir=None, # type: Optional[str] hashes=None, # type: Optional[Hashes] - progress_bar="on" # type: str ): # type: (...) -> None """Unpack link. @@ -361,8 +360,6 @@ def unpack_url( # http urls else: - downloader = Downloader(session, progress_bar) - unpack_http_url( link, location, @@ -683,11 +680,12 @@ def prepare_linked_requirement( # dedicated dir. download_dir = self.wheel_download_dir + downloader = Downloader(self.session, self.progress_bar) + try: unpack_url( - link, req.source_dir, self.session, download_dir, + link, req.source_dir, downloader, download_dir, hashes=hashes, - progress_bar=self.progress_bar ) except requests.HTTPError as exc: logger.critical( From 59f22068d464d8f50f0772f9c4c6fe9b680fd852 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 17 Nov 2019 19:41:05 -0500 Subject: [PATCH 0823/3170] Move Downloader construction to RequirementPreparer.__init__ Just one step away from pulling this out of operations.prepare altogether. --- src/pip/_internal/operations/prepare.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index c4c7526f68a..eb9e32b8109 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -558,7 +558,7 @@ def __init__( self.src_dir = src_dir self.build_dir = build_dir self.req_tracker = req_tracker - self.session = session + self.downloader = Downloader(session, progress_bar) self.finder = finder # Where still-packed archives should be written to. If None, they are @@ -579,8 +579,6 @@ def __init__( # 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 @@ -680,11 +678,9 @@ def prepare_linked_requirement( # dedicated dir. download_dir = self.wheel_download_dir - downloader = Downloader(self.session, self.progress_bar) - try: unpack_url( - link, req.source_dir, downloader, download_dir, + link, req.source_dir, self.downloader, download_dir, hashes=hashes, ) except requests.HTTPError as exc: From dd7b1ee5c3ef04b5bece55c38d7cf8c789356fa8 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver <thomas@kluyver.me.uk> Date: Sat, 23 Nov 2019 14:38:27 +0000 Subject: [PATCH 0824/3170] Fix building packages with backend-path in pyproject.toml Closes gh-6599 --- news/6599.bugfix | 1 + src/pip/_internal/pyproject.py | 7 ++++-- src/pip/_internal/req/req_install.py | 4 ++-- tests/functional/test_pep517.py | 32 +++++++++++++++++++++++++++- 4 files changed, 39 insertions(+), 5 deletions(-) create mode 100644 news/6599.bugfix diff --git a/news/6599.bugfix b/news/6599.bugfix new file mode 100644 index 00000000000..38bd89e2e03 --- /dev/null +++ b/news/6599.bugfix @@ -0,0 +1 @@ +Fix building packages which specify ``backend-path`` in pyproject.toml. diff --git a/src/pip/_internal/pyproject.py b/src/pip/_internal/pyproject.py index 459d9872715..3d978c8b654 100644 --- a/src/pip/_internal/pyproject.py +++ b/src/pip/_internal/pyproject.py @@ -39,7 +39,7 @@ def load_pyproject_toml( setup_py, # type: str req_name # type: str ): - # type: (...) -> Optional[Tuple[List[str], str, List[str]]] + # type: (...) -> Optional[Tuple[List[str], str, List[str], List[str]]] """Load the pyproject.toml file. Parameters: @@ -57,6 +57,8 @@ def load_pyproject_toml( name of PEP 517 backend, requirements we should check are installed after setting up the build environment + directory paths to import the backend from (backend-path), + relative to the project root. ) """ has_pyproject = os.path.isfile(pyproject_toml) @@ -167,6 +169,7 @@ def load_pyproject_toml( ) backend = build_system.get("build-backend") + backend_path = build_system.get("backend-path", []) check = [] # type: List[str] if backend is None: # If the user didn't specify a backend, we assume they want to use @@ -184,4 +187,4 @@ def load_pyproject_toml( backend = "setuptools.build_meta:__legacy__" check = ["setuptools>=40.8.0", "wheel"] - return (requires, backend, check) + return (requires, backend, check, backend_path) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index a38a8ab4dad..ddc7bb897bd 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -601,11 +601,11 @@ def load_pyproject_toml(self): return self.use_pep517 = True - requires, backend, check = pyproject_toml_data + requires, backend, check, backend_path = pyproject_toml_data self.requirements_to_check = check self.pyproject_requires = requires self.pep517_backend = Pep517HookCaller( - self.unpacked_source_directory, backend + self.unpacked_source_directory, backend, backend_path=backend_path, ) def prepare_metadata(self): diff --git a/tests/functional/test_pep517.py b/tests/functional/test_pep517.py index 37ef54decee..09b38c3f7c6 100644 --- a/tests/functional/test_pep517.py +++ b/tests/functional/test_pep517.py @@ -6,12 +6,14 @@ from tests.lib import make_test_finder, path_to_url -def make_project(tmpdir, requires=[], backend=None): +def make_project(tmpdir, requires=[], backend=None, backend_path=[]): project_dir = tmpdir / 'project' project_dir.mkdir() buildsys = {'requires': requires} if backend: buildsys['build-backend'] = backend + if backend_path: + buildsys['backend-path'] = backend_path data = pytoml.dumps({'build-system': buildsys}) project_dir.joinpath('pyproject.toml').write_text(data) return project_dir @@ -32,6 +34,34 @@ def test_backend(tmpdir, data): assert req.pep517_backend.build_wheel("dir") == "Backend called" +dummy_backend_code = """\ +def build_wheel( + wheel_directory, + config_settings=None, + metadata_directory=None +): + return "Backend called" +""" + + +def test_backend_path(tmpdir, data): + """Check we can call a backend inside the project""" + project_dir = make_project( + tmpdir, backend="dummy_backend", backend_path=['.'] + ) + (project_dir / 'dummy_backend.py').write_text(dummy_backend_code) + print(project_dir) + import os + print(os.listdir(project_dir)) + req = InstallRequirement(None, None, source_dir=project_dir) + req.load_pyproject_toml() + + env = BuildEnvironment() + assert hasattr(req.pep517_backend, 'build_wheel') + with env: + assert req.pep517_backend.build_wheel("dir") == "Backend called" + + def test_pep517_install(script, tmpdir, data): """Check we can build with a custom backend""" project_dir = make_project( From 6f55872bf391ded9a9e929a2c522b35137fbb112 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver <thomas@kluyver.me.uk> Date: Sat, 23 Nov 2019 15:29:30 +0000 Subject: [PATCH 0825/3170] Convert tuple return type to a named tuple --- src/pip/_internal/pyproject.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/pyproject.py b/src/pip/_internal/pyproject.py index 3d978c8b654..6f4454e6508 100644 --- a/src/pip/_internal/pyproject.py +++ b/src/pip/_internal/pyproject.py @@ -3,6 +3,7 @@ import io import os import sys +from collections import namedtuple from pip._vendor import pytoml, six from pip._vendor.packaging.requirements import InvalidRequirement, Requirement @@ -11,7 +12,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Any, Tuple, Optional, List + from typing import Any, Optional, List def _is_list_of_str(obj): @@ -33,13 +34,18 @@ def make_pyproject_path(unpacked_source_directory): return path +BuildsystemDetails = namedtuple('BuildsystemDetails', [ + 'requires', 'backend', 'check', 'backend_path' +]) + + def load_pyproject_toml( use_pep517, # type: Optional[bool] pyproject_toml, # type: str setup_py, # type: str req_name # type: str ): - # type: (...) -> Optional[Tuple[List[str], str, List[str], List[str]]] + # type: (...) -> Optional[BuildsystemDetails] """Load the pyproject.toml file. Parameters: @@ -187,4 +193,4 @@ def load_pyproject_toml( backend = "setuptools.build_meta:__legacy__" check = ["setuptools>=40.8.0", "wheel"] - return (requires, backend, check, backend_path) + return BuildsystemDetails(requires, backend, check, backend_path) From da0ff0db76046cce7f5959d30520f40329df26ba Mon Sep 17 00:00:00 2001 From: Thomas Kluyver <thomas@kluyver.me.uk> Date: Sat, 23 Nov 2019 16:46:40 +0000 Subject: [PATCH 0826/3170] Capital S in BuildSystemDetails --- src/pip/_internal/pyproject.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/pyproject.py b/src/pip/_internal/pyproject.py index 6f4454e6508..cf614fd6eaf 100644 --- a/src/pip/_internal/pyproject.py +++ b/src/pip/_internal/pyproject.py @@ -34,7 +34,7 @@ def make_pyproject_path(unpacked_source_directory): return path -BuildsystemDetails = namedtuple('BuildsystemDetails', [ +BuildSystemDetails = namedtuple('BuildSystemDetails', [ 'requires', 'backend', 'check', 'backend_path' ]) @@ -45,7 +45,7 @@ def load_pyproject_toml( setup_py, # type: str req_name # type: str ): - # type: (...) -> Optional[BuildsystemDetails] + # type: (...) -> Optional[BuildSystemDetails] """Load the pyproject.toml file. Parameters: @@ -193,4 +193,4 @@ def load_pyproject_toml( backend = "setuptools.build_meta:__legacy__" check = ["setuptools>=40.8.0", "wheel"] - return BuildsystemDetails(requires, backend, check, backend_path) + return BuildSystemDetails(requires, backend, check, backend_path) From 81f6ba3fc1eb41ca591d347e2eab1a2fe48f0485 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver <thomas@kluyver.me.uk> Date: Sat, 23 Nov 2019 16:52:24 +0000 Subject: [PATCH 0827/3170] Default None instead of [] --- tests/functional/test_pep517.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/test_pep517.py b/tests/functional/test_pep517.py index 09b38c3f7c6..826c022e875 100644 --- a/tests/functional/test_pep517.py +++ b/tests/functional/test_pep517.py @@ -6,7 +6,7 @@ from tests.lib import make_test_finder, path_to_url -def make_project(tmpdir, requires=[], backend=None, backend_path=[]): +def make_project(tmpdir, requires=[], backend=None, backend_path=None): project_dir = tmpdir / 'project' project_dir.mkdir() buildsys = {'requires': requires} From edc525add95a749a3cf5497147be2d3ab96c6aa5 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver <thomas@kluyver.me.uk> Date: Sat, 23 Nov 2019 16:53:01 +0000 Subject: [PATCH 0828/3170] Remove leftover debugging code --- tests/functional/test_pep517.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/functional/test_pep517.py b/tests/functional/test_pep517.py index 826c022e875..d1819d47ea4 100644 --- a/tests/functional/test_pep517.py +++ b/tests/functional/test_pep517.py @@ -50,9 +50,6 @@ def test_backend_path(tmpdir, data): tmpdir, backend="dummy_backend", backend_path=['.'] ) (project_dir / 'dummy_backend.py').write_text(dummy_backend_code) - print(project_dir) - import os - print(os.listdir(project_dir)) req = InstallRequirement(None, None, source_dir=project_dir) req.load_pyproject_toml() From 400cf8d71e742f67c3e38d58c6f67b942295fbb8 Mon Sep 17 00:00:00 2001 From: Thomas Kluyver <thomas@kluyver.me.uk> Date: Sat, 23 Nov 2019 16:57:24 +0000 Subject: [PATCH 0829/3170] Add a test combining local PEP 517 backend with external dependency --- tests/functional/test_pep517.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/functional/test_pep517.py b/tests/functional/test_pep517.py index d1819d47ea4..b16e692c5ac 100644 --- a/tests/functional/test_pep517.py +++ b/tests/functional/test_pep517.py @@ -59,6 +59,25 @@ def test_backend_path(tmpdir, data): assert req.pep517_backend.build_wheel("dir") == "Backend called" +def test_backend_path_and_dep(tmpdir, data): + """Check we can call a requirement's backend successfully""" + project_dir = make_project( + tmpdir, backend="dummy_internal_backend", backend_path=['.'] + ) + (project_dir / 'dummy_internal_backend.py').write_text( + "from dummy_backend import build_wheel" + ) + req = InstallRequirement(None, None, source_dir=project_dir) + req.load_pyproject_toml() + env = BuildEnvironment() + finder = make_test_finder(find_links=[data.backends]) + env.install_requirements(finder, ["dummy_backend"], 'normal', "Installing") + + assert hasattr(req.pep517_backend, 'build_wheel') + with env: + assert req.pep517_backend.build_wheel("dir") == "Backend called" + + def test_pep517_install(script, tmpdir, data): """Check we can build with a custom backend""" project_dir = make_project( From 59550aaec3a21b127c914ca5db6dcebe2636cc4a Mon Sep 17 00:00:00 2001 From: Thomas Kluyver <thomas@kluyver.me.uk> Date: Sat, 23 Nov 2019 18:45:09 +0000 Subject: [PATCH 0830/3170] Add an integration test of installing a project with a local PEP 517 backend --- .../pep517_wrapper_buildsys/mybuildsys.py | 15 +++++++++++++++ .../pep517_wrapper_buildsys/pyproject.toml | 4 ++++ .../packages/pep517_wrapper_buildsys/setup.cfg | 3 +++ .../packages/pep517_wrapper_buildsys/setup.py | 3 +++ tests/functional/test_install.py | 15 +++++++++++++++ 5 files changed, 40 insertions(+) create mode 100644 tests/data/packages/pep517_wrapper_buildsys/mybuildsys.py create mode 100644 tests/data/packages/pep517_wrapper_buildsys/pyproject.toml create mode 100644 tests/data/packages/pep517_wrapper_buildsys/setup.cfg create mode 100644 tests/data/packages/pep517_wrapper_buildsys/setup.py diff --git a/tests/data/packages/pep517_wrapper_buildsys/mybuildsys.py b/tests/data/packages/pep517_wrapper_buildsys/mybuildsys.py new file mode 100644 index 00000000000..df391e96a2f --- /dev/null +++ b/tests/data/packages/pep517_wrapper_buildsys/mybuildsys.py @@ -0,0 +1,15 @@ +import os + +from setuptools.build_meta import build_sdist +from setuptools.build_meta import build_wheel as setuptools_build_wheel +from setuptools.build_meta import (get_requires_for_build_sdist, + get_requires_for_build_wheel, + prepare_metadata_for_build_wheel) + + +def build_wheel(*a, **kw): + # Create the marker file to record that the hook was called + with open(os.environ['PIP_TEST_MARKER_FILE'], 'wb'): + pass + + return setuptools_build_wheel(*a, **kw) diff --git a/tests/data/packages/pep517_wrapper_buildsys/pyproject.toml b/tests/data/packages/pep517_wrapper_buildsys/pyproject.toml new file mode 100644 index 00000000000..94cbfb52d25 --- /dev/null +++ b/tests/data/packages/pep517_wrapper_buildsys/pyproject.toml @@ -0,0 +1,4 @@ +[build-system] +requires = [ "setuptools" ] +build-backend = "mybuildsys" # setuptools.build_meta +backend-path = ["."] diff --git a/tests/data/packages/pep517_wrapper_buildsys/setup.cfg b/tests/data/packages/pep517_wrapper_buildsys/setup.cfg new file mode 100644 index 00000000000..c8336a1c0b6 --- /dev/null +++ b/tests/data/packages/pep517_wrapper_buildsys/setup.cfg @@ -0,0 +1,3 @@ +[metadata] +name = pep517-wrapper-buildsys +version = 1.0 diff --git a/tests/data/packages/pep517_wrapper_buildsys/setup.py b/tests/data/packages/pep517_wrapper_buildsys/setup.py new file mode 100644 index 00000000000..606849326a4 --- /dev/null +++ b/tests/data/packages/pep517_wrapper_buildsys/setup.py @@ -0,0 +1,3 @@ +from setuptools import setup + +setup() diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index ba7a0a55c3c..ffc3736ba06 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1339,6 +1339,21 @@ def test_install_no_binary_builds_pep_517_wheel(script, data, with_wheel): assert "Running setup.py install for pep517-set" not in str(res), str(res) +@pytest.mark.network +def test_install_no_binary_uses_local_backend( + script, data, with_wheel, tmpdir): + to_install = data.packages.joinpath('pep517_wrapper_buildsys') + script.environ['PIP_TEST_MARKER_FILE'] = marker = str(tmpdir / 'marker') + res = script.pip( + 'install', '--no-binary=:all:', '-f', data.find_links, to_install + ) + expected = "Successfully installed pep517-wrapper-buildsys" + # Must have installed the package + assert expected in str(res), str(res) + + assert os.path.isfile(marker), "Local PEP 517 backend not used" + + def test_install_no_binary_disables_cached_wheels(script, data, with_wheel): # Seed the cache script.pip( From 8d38b3705265b08d11ae0bb149128afd3a386243 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 23 Nov 2019 16:17:35 -0500 Subject: [PATCH 0831/3170] Ignore WARNING emitted by setuptools during test --- tests/functional/test_uninstall.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/functional/test_uninstall.py b/tests/functional/test_uninstall.py index 84436aca098..42f84fa1585 100644 --- a/tests/functional/test_uninstall.py +++ b/tests/functional/test_uninstall.py @@ -276,7 +276,9 @@ def test_uninstall_easy_installed_console_scripts(script): """ Test uninstalling package with console_scripts that is easy_installed. """ - result = script.easy_install('discover') + # setuptools >= 42.0.0 deprecates easy_install and prints a warning when + # used + result = script.easy_install('discover', allow_stderr_warning=True) assert script.bin / 'discover' + script.exe in result.files_created, ( sorted(result.files_created.keys()) ) From 8114003e62b1ae99b36cc7f37a08ffb9c0f075c9 Mon Sep 17 00:00:00 2001 From: Xavier Fernandez <xavier.fernandez@polyconseil.fr> Date: Sun, 24 Nov 2019 22:49:35 +0100 Subject: [PATCH 0832/3170] Add basic tests for format_size --- tests/unit/test_utils.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index bbd48c6af98..a7bdf164eff 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -35,6 +35,7 @@ build_netloc, build_url_from_netloc, egg_link_path, + format_size, get_installed_distributions, get_prog, hide_url, @@ -991,3 +992,13 @@ def test_is_console_interactive(monkeypatch, isatty, no_stdin, expected): monkeypatch.setattr(sys, 'stdin', None) assert is_console_interactive() is expected + + +@pytest.mark.parametrize('size,expected', [ + (123, "123bytes"), + (1234, "1.2kB"), + (123456, "123kB"), + (1234567890, "1234.6MB"), +]) +def test_format_size(size, expected): + assert format_size(size) == expected From 83a9a12f96db21ac45ecbfcc4d7ff648fc42ecc2 Mon Sep 17 00:00:00 2001 From: Albert Tugushev <albert@tugushev.ru> Date: Tue, 26 Nov 2019 04:02:53 +0700 Subject: [PATCH 0833/3170] Cache pre-commit in GitHib Actions (#7400) See https://pre-commit.com/#github-actions-example --- .github/workflows/python-linters.yml | 6 ++++++ news/pre-commit-gha-cache.trivial | 0 2 files changed, 6 insertions(+) create mode 100644 news/pre-commit-gha-cache.trivial diff --git a/.github/workflows/python-linters.yml b/.github/workflows/python-linters.yml index 705dea44db2..5f2990a0dbb 100644 --- a/.github/workflows/python-linters.yml +++ b/.github/workflows/python-linters.yml @@ -26,6 +26,12 @@ jobs: uses: actions/setup-python@v1 with: python-version: ${{ matrix.env.PYTHON_VERSION || 3.7 }} + - name: set PY + run: echo "::set-env name=PY::$(python -VV | sha256sum | cut -d' ' -f1)" + - uses: actions/cache@v1 + with: + path: ~/.cache/pre-commit + key: pre-commit|${{ env.PY }}|${{ hashFiles('.pre-commit-config.yaml') }} - name: Pre-configure global Git settings run: >- tools/travis/setup.sh diff --git a/news/pre-commit-gha-cache.trivial b/news/pre-commit-gha-cache.trivial new file mode 100644 index 00000000000..e69de29bb2d From 95576102c56f53177512642ee761dcaf798061fc Mon Sep 17 00:00:00 2001 From: Xavier Fernandez <xav.fernandez@gmail.com> Date: Mon, 25 Nov 2019 22:09:10 +0100 Subject: [PATCH 0834/3170] Tweak format_size utility function to include a space (#7399) --- src/pip/_internal/utils/misc.py | 8 ++++---- tests/unit/test_utils.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 18cba7234b1..5db3b9df579 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -266,13 +266,13 @@ def ask_password(message): def format_size(bytes): # type: (float) -> str if bytes > 1000 * 1000: - return '%.1fMB' % (bytes / 1000.0 / 1000) + return '%.1f MB' % (bytes / 1000.0 / 1000) elif bytes > 10 * 1000: - return '%ikB' % (bytes / 1000) + return '%i kB' % (bytes / 1000) elif bytes > 1000: - return '%.1fkB' % (bytes / 1000.0) + return '%.1f kB' % (bytes / 1000.0) else: - return '%ibytes' % bytes + return '%i bytes' % bytes def is_installable_dir(path): diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index a7bdf164eff..65b1a9a3ff6 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -995,10 +995,10 @@ def test_is_console_interactive(monkeypatch, isatty, no_stdin, expected): @pytest.mark.parametrize('size,expected', [ - (123, "123bytes"), - (1234, "1.2kB"), - (123456, "123kB"), - (1234567890, "1234.6MB"), + (123, "123 bytes"), + (1234, "1.2 kB"), + (123456, "123 kB"), + (1234567890, "1234.6 MB"), ]) def test_format_size(size, expected): assert format_size(size) == expected From db1a2754bb2aa4f92db781933661c170100f154f Mon Sep 17 00:00:00 2001 From: Xavier Fernandez <xavier.fernandez@polyconseil.fr> Date: Fri, 22 Nov 2019 16:00:52 +0100 Subject: [PATCH 0835/3170] Fix logging of cached response Cached responses often (always ?) provide a length meaning they were never logged as such. --- news/7393.bugfix | 1 + src/pip/_internal/operations/prepare.py | 13 +++++----- tests/unit/test_operations_prepare.py | 32 +++++++++++++++++++++++++ 3 files changed, 39 insertions(+), 7 deletions(-) create mode 100644 news/7393.bugfix diff --git a/news/7393.bugfix b/news/7393.bugfix new file mode 100644 index 00000000000..a7b88d2e09a --- /dev/null +++ b/news/7393.bugfix @@ -0,0 +1 @@ +Fix the logging of cached HTTP response shown as downloading. diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index eb9e32b8109..2b02bf3c115 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -134,16 +134,15 @@ def _prepare_download( else: url = link.url_without_fragment - redacted_url = redact_auth_from_url(url) + logged_url = redact_auth_from_url(url) if total_length: - logger.info( - "Downloading %s (%s)", redacted_url, format_size(total_length) - ) - elif is_from_cache(resp): - logger.info("Using cached %s", redacted_url) + logged_url = '{} ({})'.format(logged_url, format_size(total_length)) + + if is_from_cache(resp): + logger.info("Using cached %s", logged_url) else: - logger.info("Downloading %s", redacted_url) + logger.info("Downloading %s", logged_url) if logger.getEffectiveLevel() > logging.INFO: show_progress = False diff --git a/tests/unit/test_operations_prepare.py b/tests/unit/test_operations_prepare.py index cdcc1400437..80ad5ce29f1 100644 --- a/tests/unit/test_operations_prepare.py +++ b/tests/unit/test_operations_prepare.py @@ -1,4 +1,5 @@ import hashlib +import logging import os import shutil import sys @@ -16,6 +17,7 @@ Downloader, _copy_source_tree, _download_http_url, + _prepare_download, parse_content_disposition, sanitize_content_filename, unpack_file_url, @@ -246,6 +248,36 @@ def test_download_http_url__no_directory_traversal(tmpdir): assert actual == ['out_dir_file'] +@pytest.mark.parametrize("url, headers, from_cache, expected", [ + ('http://example.com/foo.tgz', {}, False, + "Downloading http://example.com/foo.tgz"), + ('http://example.com/foo.tgz', {'content-length': 2}, False, + "Downloading http://example.com/foo.tgz (2 bytes)"), + ('http://example.com/foo.tgz', {'content-length': 2}, True, + "Using cached http://example.com/foo.tgz (2 bytes)"), + ('https://files.pythonhosted.org/foo.tgz', {}, False, + "Downloading foo.tgz"), + ('https://files.pythonhosted.org/foo.tgz', {'content-length': 2}, False, + "Downloading foo.tgz (2 bytes)"), + ('https://files.pythonhosted.org/foo.tgz', {'content-length': 2}, True, + "Using cached foo.tgz"), +]) +def test_prepare_download__log(caplog, url, headers, from_cache, expected): + caplog.set_level(logging.INFO) + resp = MockResponse(b'') + resp.url = url + resp.headers = headers + if from_cache: + resp.from_cache = from_cache + link = Link(url) + _prepare_download(resp, link, progress_bar="on") + + assert len(caplog.records) == 1 + record = caplog.records[0] + assert record.levelname == 'INFO' + assert expected in record.message + + @pytest.fixture def clean_project(tmpdir_factory, data): tmpdir = Path(str(tmpdir_factory.mktemp("clean_project"))) From 5ba702894a169e44640916f0616243a6632f95e1 Mon Sep 17 00:00:00 2001 From: Christopher Hunt <chrahunt@gmail.com> Date: Tue, 26 Nov 2019 11:58:46 +0800 Subject: [PATCH 0836/3170] Remove old workaround for Debian Python patch (#7401) --- src/pip/_internal/pep425tags.py | 7 +------ tests/unit/test_pep425tags.py | 15 --------------- 2 files changed, 1 insertion(+), 21 deletions(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index f77f231b833..8fe6bc58bf0 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -7,7 +7,6 @@ import re import sys import sysconfig -import warnings from collections import OrderedDict from pip._vendor.six import PY2 @@ -29,11 +28,7 @@ def get_config_var(var): # type: (str) -> Optional[str] - try: - return sysconfig.get_config_var(var) - except IOError as e: # Issue #1074 - warnings.warn("{}".format(e), RuntimeWarning) - return None + return sysconfig.get_config_var(var) def get_abbr_impl(): diff --git a/tests/unit/test_pep425tags.py b/tests/unit/test_pep425tags.py index c43843043aa..ec4034999ef 100644 --- a/tests/unit/test_pep425tags.py +++ b/tests/unit/test_pep425tags.py @@ -77,21 +77,6 @@ def abi_tag_unicode(self, flags, config_vars): abi_tag = pip._internal.pep425tags.get_abi_tag() assert abi_tag == base + flags - def test_broken_sysconfig(self): - """ - Test that pep425tags still works when sysconfig is broken. - Can be a problem on Python 2.7 - Issue #1074. - """ - import pip._internal.pep425tags - - def raises_ioerror(var): - raise IOError("I have the wrong path!") - - with patch('pip._internal.pep425tags.sysconfig.get_config_var', - raises_ioerror): - assert len(pip._internal.pep425tags.get_supported()) - def test_no_hyphen_tag(self): """ Test that no tag contains a hyphen. From 9886e8dea1c0861dc589e1dfe1ed9709bceec8b4 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 26 Nov 2019 18:39:19 -0500 Subject: [PATCH 0837/3170] Assert that we aren't using most of "move_to_correct_build_directory" Since self._ideal_build_dir is only either None or a string, we must have always been returning from this function. This goes back to pip 10.0.0, so this code has been dead for some time. --- src/pip/_internal/req/req_install.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index ddc7bb897bd..ad7e8b53ee3 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -412,8 +412,9 @@ def move_to_correct_build_directory(self): ]) ) - if self.source_dir is not None: - return + assert self.source_dir is not None + + return assert self._temp_build_dir assert ( From 99c7598eb962487d6257c737f54384677c6bb564 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 26 Nov 2019 18:41:25 -0500 Subject: [PATCH 0838/3170] Remove dead code from move_to_correct_build_directory --- src/pip/_internal/req/req_install.py | 48 +--------------------------- 1 file changed, 1 insertion(+), 47 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index ad7e8b53ee3..ba2627ef06f 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -397,6 +397,7 @@ def move_to_correct_build_directory(self): """ assert self.req is None assert self.metadata is not None + assert self.source_dir is not None # Construct a Requirement object from the generated metadata if isinstance(parse_version(self.metadata["Version"]), Version): @@ -412,53 +413,6 @@ def move_to_correct_build_directory(self): ]) ) - assert self.source_dir is not None - - return - - assert self._temp_build_dir - assert ( - self._ideal_build_dir is not None and - self._ideal_build_dir.path # type: ignore - ) - - # Backup directory for later use. - old_location = self._temp_build_dir - self._temp_build_dir = None # checked inside ensure_build_location - - # Figure out the correct place to put the files. - new_location = self.ensure_build_location(self._ideal_build_dir) - if os.path.exists(new_location): - raise InstallationError( - 'A package already exists in %s; please remove it to continue' - % display_path(new_location) - ) - - # Move the files to the correct location. - logger.debug( - 'Moving package %s from %s to new location %s', - self, display_path(old_location.path), display_path(new_location), - ) - shutil.move(old_location.path, new_location) - - # Update directory-tracking variables, to be in line with new_location - self.source_dir = os.path.normpath(os.path.abspath(new_location)) - self._temp_build_dir = TempDirectory( - path=new_location, kind="req-install", - ) - - # Correct the metadata directory - old_meta = self.metadata_directory - rel = os.path.relpath(old_meta, start=old_location.path) - new_meta = os.path.join(new_location, rel) - new_meta = os.path.normpath(os.path.abspath(new_meta)) - self.metadata_directory = new_meta - - # Done with any "move built files" work, since have moved files to the - # "ideal" build location. Setting to None allows to clearly flag that - # no more moves are needed. - self._ideal_build_dir = None - def warn_on_mismatching_name(self): # type: () -> None metadata_name = canonicalize_name(self.metadata["Name"]) From dbd80ec2c0d7b5c3f55d51177168847e0ae40320 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 26 Nov 2019 18:47:12 -0500 Subject: [PATCH 0839/3170] Remove unused code related to moving build directory --- src/pip/_internal/req/req_install.py | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index ba2627ef06f..c0c15ce387c 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -162,9 +162,6 @@ def __init__( self.conflicts_with = None # Temporary build location self._temp_build_dir = None # type: Optional[TempDirectory] - # Used to store the global directory where the _temp_build_dir should - # have been created. See move_to_correct_build_directory(). - self._ideal_build_dir = None # type: Optional[str] # Set to True after successful installation self.install_succeeded = None # type: Optional[bool] self.options = options if options else {} @@ -362,15 +359,10 @@ def ensure_build_location(self, build_dir): assert self._temp_build_dir.path return self._temp_build_dir.path if self.req is None: - # for requirement via a path to a directory: the name of the - # package is not available yet so we create a temp directory - # Once run_egg_info will have run, we'll be able to fix it via - # move_to_correct_build_directory(). # Some systems have /tmp as a symlink which confuses custom # builds (such as numpy). Thus, we ensure that the real path # is returned. self._temp_build_dir = TempDirectory(kind="req-build") - self._ideal_build_dir = build_dir return self._temp_build_dir.path if self.editable: @@ -384,16 +376,9 @@ def ensure_build_location(self, build_dir): _make_build_dir(build_dir) return os.path.join(build_dir, name) - def move_to_correct_build_directory(self): + def _set_requirement(self): # type: () -> None - """Move self._temp_build_dir to "self._ideal_build_dir/{metadata name}" - - For some requirements (e.g. a path to a directory), the name of the - package is not available until we run egg_info, so the build_location - will return a temporary directory and store the _ideal_build_dir. - - This is only called to "fix" the build directory after generating - metadata. + """Set requirement after generating metadata. """ assert self.req is None assert self.metadata is not None @@ -581,7 +566,7 @@ def prepare_metadata(self): # Act on the newly generated metadata, based on the name and version. if not self.name: - self.move_to_correct_build_directory() + self._set_requirement() else: self.warn_on_mismatching_name() From 8ab7d239fadaa8bd35980b6412000643f5a39232 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 26 Nov 2019 21:36:40 -0500 Subject: [PATCH 0840/3170] Use pip 19.3.1+ for vendoring task --- tox.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tox.ini b/tox.ini index 30eb193dd3d..de1263fc47c 100644 --- a/tox.ini +++ b/tox.ini @@ -53,6 +53,9 @@ commands_pre = deps = invoke requests + # Required, otherwise we interpret --no-binary :all: as + # "do not build wheels", which fails for PEP 517 requirements + pip>=19.3.1 whitelist_externals = git commands = # Check that the vendoring is up-to-date From 4659fe16bd8bbd5ccee9e0466b5e46d96ca7e482 Mon Sep 17 00:00:00 2001 From: BorisZZZ <BorisZZZ@users.noreply.github.com> Date: Thu, 28 Nov 2019 06:05:17 +0200 Subject: [PATCH 0841/3170] #6426 Cannot install packages on Docker in Ubuntu in WSL (Windows). (#6427) --- news/6426.bugfix | 1 + src/pip/_internal/utils/misc.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 news/6426.bugfix diff --git a/news/6426.bugfix b/news/6426.bugfix new file mode 100644 index 00000000000..25512b3c808 --- /dev/null +++ b/news/6426.bugfix @@ -0,0 +1 @@ +Make ``ensure_dir()`` also ignore ``ENOTEMPTY`` as seen on Windows. diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 5db3b9df579..85acab856b0 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -111,7 +111,8 @@ def ensure_dir(path): try: os.makedirs(path) except OSError as e: - if e.errno != errno.EEXIST: + # Windows can raise spurious ENOTEMPTY errors. See #6426. + if e.errno != errno.EEXIST and e.errno != errno.ENOTEMPTY: raise From 46adcc89a68e330b1799476c9c9aa2948741c63d Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 29 Nov 2019 10:21:24 -0500 Subject: [PATCH 0842/3170] Remove outdated comment in operations.prepare There are several reasons why we don't trace progress as indicated a few lines above. --- src/pip/_internal/operations/prepare.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 2b02bf3c115..5642fa9b17d 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -157,7 +157,7 @@ def _prepare_download( progress_indicator = _progress_indicator - if show_progress: # We don't show progress on cached responses + if show_progress: progress_indicator = DownloadProgressProvider( progress_bar, max=total_length ) From d279807a0e7f82dceb31e636da5d48618a34f70e Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 29 Nov 2019 10:24:56 -0500 Subject: [PATCH 0843/3170] Extract chunks into separate variable --- src/pip/_internal/operations/prepare.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 5642fa9b17d..e0ce088f634 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -155,6 +155,8 @@ def _prepare_download( else: show_progress = False + chunks = response_chunks(resp, CONTENT_CHUNK_SIZE) + progress_indicator = _progress_indicator if show_progress: @@ -162,9 +164,7 @@ def _prepare_download( progress_bar, max=total_length ) - return progress_indicator( - response_chunks(resp, CONTENT_CHUNK_SIZE) - ) + return progress_indicator(chunks) def _copy_file(filename, location, link): From aaabdfc2f7319633fbec6b77dcc0ee07d8b47cfe Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 29 Nov 2019 10:26:37 -0500 Subject: [PATCH 0844/3170] Switch to early-return in _prepare_download Makes this function simpler by removing a local variable and reduces overall lines of code. --- src/pip/_internal/operations/prepare.py | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index e0ce088f634..50f15ef7b3e 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -109,10 +109,6 @@ def unpack_vcs_link(link, location): vcs_backend.unpack(location, url=hide_url(link.url)) -def _progress_indicator(iterable, *args, **kwargs): - return iterable - - def _get_http_response_size(resp): # type: (Response) -> Optional[int] try: @@ -157,14 +153,12 @@ def _prepare_download( chunks = response_chunks(resp, CONTENT_CHUNK_SIZE) - progress_indicator = _progress_indicator - - if show_progress: - progress_indicator = DownloadProgressProvider( - progress_bar, max=total_length - ) + if not show_progress: + return chunks - return progress_indicator(chunks) + return DownloadProgressProvider( + progress_bar, max=total_length + )(chunks) def _copy_file(filename, location, link): From e13c1f1e8f829b94220af33a783a54f84393dd04 Mon Sep 17 00:00:00 2001 From: Christopher Hunt <chrahunt@gmail.com> Date: Sun, 1 Dec 2019 05:39:57 +0800 Subject: [PATCH 0845/3170] Minimize scope of try block (#7415) This more clearly separates the flow control in the function, which will make refactoring easier. --- src/pip/_internal/req/req_install.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index c0c15ce387c..2f82ab8c98e 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -435,20 +435,14 @@ def check_if_exists(self, use_user_site): """ if self.req is None: return False + # get_distribution() will resolve the entire list of requirements + # anyway, and we've already determined that we need the requirement + # in question, so strip the marker so that we don't try to + # evaluate it. + no_marker = Requirement(str(self.req)) + no_marker.marker = None try: - # get_distribution() will resolve the entire list of requirements - # anyway, and we've already determined that we need the requirement - # in question, so strip the marker so that we don't try to - # evaluate it. - no_marker = Requirement(str(self.req)) - no_marker.marker = None self.satisfied_by = pkg_resources.get_distribution(str(no_marker)) - if self.editable and self.satisfied_by: - self.conflicts_with = self.satisfied_by - # when installing editables, nothing pre-existing should ever - # satisfy - self.satisfied_by = None - return True except pkg_resources.DistributionNotFound: return False except pkg_resources.VersionConflict: @@ -467,6 +461,13 @@ def check_if_exists(self, use_user_site): ) else: self.conflicts_with = existing_dist + else: + if self.editable and self.satisfied_by: + self.conflicts_with = self.satisfied_by + # when installing editables, nothing pre-existing should ever + # satisfy + self.satisfied_by = None + return True return True # Things valid for wheels From 55a943e55639bd52c86167a9243d8dd887b68c28 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 16 Nov 2019 18:00:34 -0500 Subject: [PATCH 0846/3170] Add distutils args helpers The utils.distutils_args.parse_args function can recognize distutils arguments so we can deprecate their usage. --- src/pip/_internal/utils/distutils_args.py | 48 ++++++++++++++++++ tests/unit/test_utils_distutils_args.py | 60 +++++++++++++++++++++++ 2 files changed, 108 insertions(+) create mode 100644 src/pip/_internal/utils/distutils_args.py create mode 100644 tests/unit/test_utils_distutils_args.py diff --git a/src/pip/_internal/utils/distutils_args.py b/src/pip/_internal/utils/distutils_args.py new file mode 100644 index 00000000000..e38e402d733 --- /dev/null +++ b/src/pip/_internal/utils/distutils_args.py @@ -0,0 +1,48 @@ +from distutils.errors import DistutilsArgError +from distutils.fancy_getopt import FancyGetopt + +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import Dict, List + + +_options = [ + ("exec-prefix=", None, ""), + ("home=", None, ""), + ("install-base=", None, ""), + ("install-data=", None, ""), + ("install-headers=", None, ""), + ("install-lib=", None, ""), + ("install-platlib=", None, ""), + ("install-purelib=", None, ""), + ("install-scripts=", None, ""), + ("prefix=", None, ""), + ("root=", None, ""), + ("user", None, ""), +] + + +# typeshed doesn't permit Tuple[str, None, str], see python/typeshed#3469. +_distutils_getopt = FancyGetopt(_options) # type: ignore + + +def parse_distutils_args(args): + # type: (List[str]) -> Dict[str, str] + """Parse provided arguments, returning an object that has the + matched arguments. + + Any unknown arguments are ignored. + """ + result = {} + for arg in args: + try: + _, match = _distutils_getopt.getopt(args=[arg]) + except DistutilsArgError: + # We don't care about any other options, which here may be + # considered unrecognized since our option list is not + # exhaustive. + pass + else: + result.update(match.__dict__) + return result diff --git a/tests/unit/test_utils_distutils_args.py b/tests/unit/test_utils_distutils_args.py new file mode 100644 index 00000000000..5bca65018ec --- /dev/null +++ b/tests/unit/test_utils_distutils_args.py @@ -0,0 +1,60 @@ +import pytest + +from pip._internal.utils.distutils_args import parse_distutils_args + + +def test_unknown_option_is_ok(): + result = parse_distutils_args(["--foo"]) + assert not result + + +def test_option_is_returned(): + result = parse_distutils_args(["--prefix=hello"]) + assert result["prefix"] == "hello" + + +def test_options_are_clobbered(): + # Matches the current setuptools behavior that the last argument + # wins. + result = parse_distutils_args(["--prefix=hello", "--prefix=world"]) + assert result["prefix"] == "world" + + +def test_multiple_options_work(): + result = parse_distutils_args(["--prefix=hello", "--root=world"]) + assert result["prefix"] == "hello" + assert result["root"] == "world" + + +def test_multiple_invocations_do_not_keep_options(): + result = parse_distutils_args(["--prefix=hello1"]) + assert len(result) == 1 + assert result["prefix"] == "hello1" + + result = parse_distutils_args(["--root=world1"]) + assert len(result) == 1 + assert result["root"] == "world1" + + +@pytest.mark.parametrize("name,value", [ + ("exec-prefix", "1"), + ("home", "2"), + ("install-base", "3"), + ("install-data", "4"), + ("install-headers", "5"), + ("install-lib", "6"), + ("install-platlib", "7"), + ("install-purelib", "8"), + ("install-scripts", "9"), + ("prefix", "10"), + ("root", "11"), +]) +def test_all_value_options_work(name, value): + result = parse_distutils_args(["--{}={}".format(name, value)]) + key_name = name.replace("-", "_") + assert result[key_name] == value + + +def test_user_option_works(): + result = parse_distutils_args(["--user"]) + assert result["user"] == 1 From b8f626ace6c7f9d0c2f4c52bafe977c1c47bc395 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 16 Nov 2019 20:42:27 -0500 Subject: [PATCH 0847/3170] Deprecate install-location-related options in --install-option --- news/7309.removal | 1 + src/pip/_internal/commands/install.py | 64 ++++++++++++++++++++++++++- tests/functional/test_install_reqs.py | 1 + tests/unit/test_command_install.py | 42 ++++++++++++++++++ 4 files changed, 107 insertions(+), 1 deletion(-) create mode 100644 news/7309.removal diff --git a/news/7309.removal b/news/7309.removal new file mode 100644 index 00000000000..54c2f062656 --- /dev/null +++ b/news/7309.removal @@ -0,0 +1 @@ +Deprecate passing install-location-related options via ``--install-option``. diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 92bfa3f6b10..d62534de39a 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -32,6 +32,8 @@ from pip._internal.operations.check import check_install_conflicts from pip._internal.req import RequirementSet, install_given_reqs from pip._internal.req.req_tracker import get_requirement_tracker +from pip._internal.utils.deprecation import deprecated +from pip._internal.utils.distutils_args import parse_distutils_args from pip._internal.utils.filesystem import check_path_owner, test_writable_dir from pip._internal.utils.misc import ( ensure_dir, @@ -46,7 +48,7 @@ if MYPY_CHECK_RUNNING: from optparse import Values - from typing import Any, List, Optional + from typing import Any, Iterable, List, Optional from pip._internal.models.format_control import FormatControl from pip._internal.req.req_install import InstallRequirement @@ -355,6 +357,11 @@ def run(self, options, args): requirement_set, args, options, finder, session, wheel_cache ) + + warn_deprecated_install_options( + requirement_set, options.install_options + ) + preparer = self.make_requirement_preparer( temp_build_dir=directory, options=options, @@ -660,6 +667,61 @@ def decide_user_install( return True +def warn_deprecated_install_options(requirement_set, options): + # type: (RequirementSet, Optional[List[str]]) -> None + """If any location-changing --install-option arguments were passed for + requirements or on the command-line, then show a deprecation warning. + """ + def format_options(option_names): + # type: (Iterable[str]) -> List[str] + return ["--{}".format(name.replace("_", "-")) for name in option_names] + + requirements = ( + requirement_set.unnamed_requirements + + list(requirement_set.requirements.values()) + ) + + offenders = [] + + for requirement in requirements: + install_options = requirement.options.get("install_options", []) + location_options = parse_distutils_args(install_options) + if location_options: + offenders.append( + "{!r} from {}".format( + format_options(location_options.keys()), requirement + ) + ) + + if options: + location_options = parse_distutils_args(options) + if location_options: + offenders.append( + "{!r} from command line".format( + format_options(location_options.keys()) + ) + ) + + if not offenders: + return + + deprecated( + reason=( + "Location-changing options found in --install-option: {}. " + "This configuration may cause unexpected behavior and is " + "unsupported.".format( + "; ".join(offenders) + ) + ), + replacement=( + "using pip-level options like --user, --prefix, --root, and " + "--target" + ), + gone_in="20.2", + issue=7309, + ) + + def create_env_error_message(error, show_traceback, using_user_site): """Format an error message for an EnvironmentError diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index 2022e1fee53..55c51664366 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -536,6 +536,7 @@ def test_install_options_local_to_package(script, data): 'install', '--no-index', '-f', data.find_links, '-r', reqs_file, + expect_stderr=True, ) simple = test_simple / 'lib' / 'python' / 'simple' diff --git a/tests/unit/test_command_install.py b/tests/unit/test_command_install.py index 6e10fd73a58..7f7154c94f1 100644 --- a/tests/unit/test_command_install.py +++ b/tests/unit/test_command_install.py @@ -2,12 +2,16 @@ import pytest from mock import Mock, call, patch +from pip._vendor.packaging.requirements import Requirement from pip._internal.commands.install import ( build_wheels, create_env_error_message, decide_user_install, + warn_deprecated_install_options, ) +from pip._internal.req.req_install import InstallRequirement +from pip._internal.req.req_set import RequirementSet class TestWheelCache: @@ -102,6 +106,44 @@ def test_most_cases( assert decide_user_install(use_user_site=None) is result +def test_deprecation_notice_for_pip_install_options(recwarn): + install_options = ["--prefix=/hello"] + req_set = RequirementSet() + warn_deprecated_install_options(req_set, install_options) + + assert len(recwarn) == 1 + message = recwarn[0].message.args[0] + assert "['--prefix'] from command line" in message + + +def test_deprecation_notice_for_requirement_options(recwarn): + install_options = [] + req_set = RequirementSet() + + bad_named_req_options = {"install_options": ["--home=/wow"]} + bad_named_req = InstallRequirement( + Requirement("hello"), "requirements.txt", options=bad_named_req_options + ) + req_set.add_named_requirement(bad_named_req) + + bad_unnamed_req_options = {"install_options": ["--install-lib=/lib"]} + bad_unnamed_req = InstallRequirement( + None, "requirements2.txt", options=bad_unnamed_req_options + ) + req_set.add_unnamed_requirement(bad_unnamed_req) + + warn_deprecated_install_options(req_set, install_options) + + assert len(recwarn) == 1 + message = recwarn[0].message.args[0] + + assert ( + "['--install-lib'] from <InstallRequirement> (from requirements2.txt)" + in message + ) + assert "['--home'] from hello (from requirements.txt)" in message + + @pytest.mark.parametrize('error, show_traceback, using_user_site, expected', [ # show_traceback = True, using_user_site = True (EnvironmentError("Illegal byte sequence"), True, True, 'Could not install' From ccc231e4de8bb04e3c03552fef39543b0bd51a0d Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 1 Dec 2019 16:26:02 -0500 Subject: [PATCH 0848/3170] Remove old TODO in wheel.Wheel Moving installation into this class would add complexity but only to service a single caller (InstallRequirement). Better to keep the Wheel representation, which is used in several places, small and focused and installation can be moved to its own module. --- src/pip/_internal/wheel.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index a38ff38982e..4791cbbbbc4 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -678,7 +678,6 @@ class Wheel(object): """A wheel file""" # TODO: Maybe move the class into the models sub-package - # TODO: Maybe move the install code into this class wheel_file_re = re.compile( r"""^(?P<namever>(?P<name>.+?)-(?P<ver>.*?)) From 1abf978e8843b02c6e0952309e971d953abfa6ae Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 1 Dec 2019 16:41:47 -0500 Subject: [PATCH 0849/3170] Move format_tag to pep425tags This is a more appropriate place for the function, since it is more related to tags than wheels, and will make it easier to refactor Wheel into its own module. --- src/pip/_internal/commands/debug.py | 2 +- src/pip/_internal/pep425tags.py | 9 +++++++++ src/pip/_internal/wheel.py | 10 +--------- tests/unit/test_pep425tags.py | 9 +++++++++ tests/unit/test_wheel.py | 9 --------- 5 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/pip/_internal/commands/debug.py b/src/pip/_internal/commands/debug.py index 2773dfd52d0..2a262e759eb 100644 --- a/src/pip/_internal/commands/debug.py +++ b/src/pip/_internal/commands/debug.py @@ -14,10 +14,10 @@ from pip._internal.cli.base_command import Command from pip._internal.cli.cmdoptions import make_target_python from pip._internal.cli.status_codes import SUCCESS +from pip._internal.pep425tags import format_tag from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import get_pip_version from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from pip._internal.wheel import format_tag if MYPY_CHECK_RUNNING: from typing import Any, List, Optional diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index 8fe6bc58bf0..44b2d7ee0f8 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -26,6 +26,15 @@ _osx_arch_pat = re.compile(r'(.+)_(\d+)_(\d+)_(.+)') +def format_tag(file_tag): + # type: (Tuple[str, ...]) -> str + """Format three tags in the form "<python_tag>-<abi_tag>-<platform_tag>". + + :param file_tag: A 3-tuple of tags (python_tag, abi_tag, platform_tag). + """ + return '-'.join(file_tag) + + def get_config_var(var): # type: (str) -> Optional[str] return sysconfig.get_config_var(var) diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index 4791cbbbbc4..e7b8c6bb6b4 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -31,6 +31,7 @@ UnsupportedWheel, ) from pip._internal.locations import get_major_minor_version +from pip._internal.pep425tags import format_tag from pip._internal.utils.misc import captured_stdout, ensure_dir, hash_file from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -665,15 +666,6 @@ def check_compatibility(version, name): ) -def format_tag(file_tag): - # type: (Tuple[str, ...]) -> str - """Format three tags in the form "<python_tag>-<abi_tag>-<platform_tag>". - - :param file_tag: A 3-tuple of tags (python_tag, abi_tag, platform_tag). - """ - return '-'.join(file_tag) - - class Wheel(object): """A wheel file""" diff --git a/tests/unit/test_pep425tags.py b/tests/unit/test_pep425tags.py index ec4034999ef..6de10b9d079 100644 --- a/tests/unit/test_pep425tags.py +++ b/tests/unit/test_pep425tags.py @@ -6,6 +6,15 @@ from pip._internal import pep425tags +@pytest.mark.parametrize('file_tag, expected', [ + (('py27', 'none', 'any'), 'py27-none-any'), + (('cp33', 'cp32dmu', 'linux_x86_64'), 'cp33-cp32dmu-linux_x86_64'), +]) +def test_format_tag(file_tag, expected): + actual = pep425tags.format_tag(file_tag) + assert actual == expected + + @pytest.mark.parametrize('version_info, expected', [ ((2,), '2'), ((2, 8), '28'), diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 5292514cd10..95563d84128 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -24,15 +24,6 @@ from tests.lib import DATA_DIR, assert_paths_equal -@pytest.mark.parametrize('file_tag, expected', [ - (('py27', 'none', 'any'), 'py27-none-any'), - (('cp33', 'cp32dmu', 'linux_x86_64'), 'cp33-cp32dmu-linux_x86_64'), -]) -def test_format_tag(file_tag, expected): - actual = wheel.format_tag(file_tag) - assert actual == expected - - def call_get_legacy_build_wheel_path(caplog, names): wheel_path = get_legacy_build_wheel_path( names=names, From bf26185d55f10dc4db6a0966ceb9f64b3da009fa Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 1 Dec 2019 16:45:47 -0500 Subject: [PATCH 0850/3170] Import Wheel in tests directly --- tests/unit/test_wheel.py | 43 ++++++++++++++++++++-------------------- 1 file changed, 22 insertions(+), 21 deletions(-) diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 95563d84128..9fb5f8de90c 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -18,6 +18,7 @@ from pip._internal.utils.unpacking import unpack_file from pip._internal.wheel import ( MissingCallableSuffix, + Wheel, _raise_for_invalid_entrypoint, ) from pip._internal.wheel_builder import get_legacy_build_wheel_path @@ -227,7 +228,7 @@ def test_check_compatibility(): class TestWheelFile(object): def test_std_wheel_pattern(self): - w = wheel.Wheel('simple-1.1.1-py2-none-any.whl') + w = Wheel('simple-1.1.1-py2-none-any.whl') assert w.name == 'simple' assert w.version == '1.1.1' assert w.pyversions == ['py2'] @@ -235,7 +236,7 @@ def test_std_wheel_pattern(self): assert w.plats == ['any'] def test_wheel_pattern_multi_values(self): - w = wheel.Wheel('simple-1.1-py2.py3-abi1.abi2-any.whl') + w = Wheel('simple-1.1-py2.py3-abi1.abi2-any.whl') assert w.name == 'simple' assert w.version == '1.1' assert w.pyversions == ['py2', 'py3'] @@ -245,7 +246,7 @@ def test_wheel_pattern_multi_values(self): def test_wheel_with_build_tag(self): # pip doesn't do anything with build tags, but theoretically, we might # see one, in this case the build tag = '4' - w = wheel.Wheel('simple-1.1-4-py2-none-any.whl') + w = Wheel('simple-1.1-4-py2-none-any.whl') assert w.name == 'simple' assert w.version == '1.1' assert w.pyversions == ['py2'] @@ -253,40 +254,40 @@ def test_wheel_with_build_tag(self): assert w.plats == ['any'] def test_single_digit_version(self): - w = wheel.Wheel('simple-1-py2-none-any.whl') + w = Wheel('simple-1-py2-none-any.whl') assert w.version == '1' def test_non_pep440_version(self): - w = wheel.Wheel('simple-_invalid_-py2-none-any.whl') + w = Wheel('simple-_invalid_-py2-none-any.whl') assert w.version == '-invalid-' def test_missing_version_raises(self): with pytest.raises(InvalidWheelFilename): - wheel.Wheel('Cython-cp27-none-linux_x86_64.whl') + Wheel('Cython-cp27-none-linux_x86_64.whl') def test_invalid_filename_raises(self): with pytest.raises(InvalidWheelFilename): - wheel.Wheel('invalid.whl') + Wheel('invalid.whl') def test_supported_single_version(self): """ Test single-version wheel is known to be supported """ - w = wheel.Wheel('simple-0.1-py2-none-any.whl') + w = Wheel('simple-0.1-py2-none-any.whl') assert w.supported(tags=[('py2', 'none', 'any')]) def test_supported_multi_version(self): """ Test multi-version wheel is known to be supported """ - w = wheel.Wheel('simple-0.1-py2.py3-none-any.whl') + w = Wheel('simple-0.1-py2.py3-none-any.whl') assert w.supported(tags=[('py3', 'none', 'any')]) def test_not_supported_version(self): """ Test unsupported wheel is known to be unsupported """ - w = wheel.Wheel('simple-0.1-py2-none-any.whl') + w = Wheel('simple-0.1-py2-none-any.whl') assert not w.supported(tags=[('py1', 'none', 'any')]) def test_supported_osx_version(self): @@ -296,9 +297,9 @@ def test_supported_osx_version(self): tags = pep425tags.get_supported( '27', platform='macosx_10_9_intel', impl='cp' ) - w = wheel.Wheel('simple-0.1-cp27-none-macosx_10_6_intel.whl') + w = Wheel('simple-0.1-cp27-none-macosx_10_6_intel.whl') assert w.supported(tags=tags) - w = wheel.Wheel('simple-0.1-cp27-none-macosx_10_9_intel.whl') + w = Wheel('simple-0.1-cp27-none-macosx_10_9_intel.whl') assert w.supported(tags=tags) def test_not_supported_osx_version(self): @@ -308,7 +309,7 @@ def test_not_supported_osx_version(self): tags = pep425tags.get_supported( '27', platform='macosx_10_6_intel', impl='cp' ) - w = wheel.Wheel('simple-0.1-cp27-none-macosx_10_9_intel.whl') + w = Wheel('simple-0.1-cp27-none-macosx_10_9_intel.whl') assert not w.supported(tags=tags) def test_supported_multiarch_darwin(self): @@ -334,14 +335,14 @@ def test_supported_multiarch_darwin(self): '27', platform='macosx_10_5_ppc64', impl='cp' ) - w = wheel.Wheel('simple-0.1-cp27-none-macosx_10_5_intel.whl') + w = Wheel('simple-0.1-cp27-none-macosx_10_5_intel.whl') assert w.supported(tags=intel) assert w.supported(tags=x64) assert w.supported(tags=i386) assert not w.supported(tags=universal) assert not w.supported(tags=ppc) assert not w.supported(tags=ppc64) - w = wheel.Wheel('simple-0.1-cp27-none-macosx_10_5_universal.whl') + w = Wheel('simple-0.1-cp27-none-macosx_10_5_universal.whl') assert w.supported(tags=universal) assert w.supported(tags=intel) assert w.supported(tags=x64) @@ -360,10 +361,10 @@ def test_not_supported_multiarch_darwin(self): '27', platform='macosx_10_5_intel', impl='cp' ) - w = wheel.Wheel('simple-0.1-cp27-none-macosx_10_5_i386.whl') + w = Wheel('simple-0.1-cp27-none-macosx_10_5_i386.whl') assert not w.supported(tags=intel) assert not w.supported(tags=universal) - w = wheel.Wheel('simple-0.1-cp27-none-macosx_10_5_x86_64.whl') + w = Wheel('simple-0.1-cp27-none-macosx_10_5_x86_64.whl') assert not w.supported(tags=intel) assert not w.supported(tags=universal) @@ -376,16 +377,16 @@ def test_support_index_min(self): ('py2', 'TEST', 'any'), ('py2', 'none', 'any'), ] - w = wheel.Wheel('simple-0.1-py2-none-any.whl') + w = Wheel('simple-0.1-py2-none-any.whl') assert w.support_index_min(tags=tags) == 2 - w = wheel.Wheel('simple-0.1-py2-none-TEST.whl') + w = Wheel('simple-0.1-py2-none-TEST.whl') assert w.support_index_min(tags=tags) == 0 def test_support_index_min__none_supported(self): """ Test a wheel not supported by the given tags. """ - w = wheel.Wheel('simple-0.1-py2-none-any.whl') + w = Wheel('simple-0.1-py2-none-any.whl') with pytest.raises(ValueError): w.support_index_min(tags=[]) @@ -416,7 +417,7 @@ def test_version_underscore_conversion(self): Test that we convert '_' to '-' for versions parsed out of wheel filenames """ - w = wheel.Wheel('simple-0.1_1-py2-none-any.whl') + w = Wheel('simple-0.1_1-py2-none-any.whl') assert w.version == '0.1-1' From 60f6ed9387b9ef885739671e85386460e19e84c6 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 1 Dec 2019 16:55:47 -0500 Subject: [PATCH 0851/3170] Move wheel.Wheel to models.wheel.Wheel This aligns more closely with how the class is used and makes it easier to move the rest of the wheel module to a dedicated module for installation. --- src/pip/_internal/cache.py | 3 +- src/pip/_internal/index/package_finder.py | 2 +- src/pip/_internal/models/wheel.py | 79 +++++++++++++++++++++++ src/pip/_internal/req/constructors.py | 2 +- src/pip/_internal/req/req_set.py | 2 +- src/pip/_internal/wheel.py | 76 +--------------------- tests/unit/test_wheel.py | 2 +- 7 files changed, 86 insertions(+), 80 deletions(-) create mode 100644 src/pip/_internal/models/wheel.py diff --git a/src/pip/_internal/cache.py b/src/pip/_internal/cache.py index 7aa72b9a774..7a431f9a2ed 100644 --- a/src/pip/_internal/cache.py +++ b/src/pip/_internal/cache.py @@ -11,12 +11,13 @@ from pip._vendor.packaging.utils import canonicalize_name +from pip._internal.exceptions import InvalidWheelFilename from pip._internal.models.link import Link +from pip._internal.models.wheel import Wheel from pip._internal.utils.compat import expanduser from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.urls import path_to_url -from pip._internal.wheel import InvalidWheelFilename, Wheel if MYPY_CHECK_RUNNING: from typing import Optional, Set, List, Any diff --git a/src/pip/_internal/index/package_finder.py b/src/pip/_internal/index/package_finder.py index 3e034ba91b0..3c98de2c5fc 100644 --- a/src/pip/_internal/index/package_finder.py +++ b/src/pip/_internal/index/package_finder.py @@ -25,6 +25,7 @@ from pip._internal.models.link import Link from pip._internal.models.selection_prefs import SelectionPreferences from pip._internal.models.target_python import TargetPython +from pip._internal.models.wheel import Wheel from pip._internal.utils.filetypes import WHEEL_EXTENSION from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import build_netloc @@ -32,7 +33,6 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.unpacking import SUPPORTED_EXTENSIONS from pip._internal.utils.urls import url_to_path -from pip._internal.wheel import Wheel if MYPY_CHECK_RUNNING: from typing import ( diff --git a/src/pip/_internal/models/wheel.py b/src/pip/_internal/models/wheel.py new file mode 100644 index 00000000000..4c7a8af78fa --- /dev/null +++ b/src/pip/_internal/models/wheel.py @@ -0,0 +1,79 @@ +"""Represents a wheel file and provides access to the various parts of the +name that have meaning. +""" +import re + +from pip._internal.exceptions import InvalidWheelFilename +from pip._internal.pep425tags import format_tag +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import List + + from pip._internal.pep425tags import Pep425Tag + + +class Wheel(object): + """A wheel file""" + + wheel_file_re = re.compile( + r"""^(?P<namever>(?P<name>.+?)-(?P<ver>.*?)) + ((-(?P<build>\d[^-]*?))?-(?P<pyver>.+?)-(?P<abi>.+?)-(?P<plat>.+?) + \.whl|\.dist-info)$""", + re.VERBOSE + ) + + def __init__(self, filename): + # type: (str) -> None + """ + :raises InvalidWheelFilename: when the filename is invalid for a wheel + """ + wheel_info = self.wheel_file_re.match(filename) + if not wheel_info: + raise InvalidWheelFilename( + "%s is not a valid wheel filename." % filename + ) + self.filename = filename + self.name = wheel_info.group('name').replace('_', '-') + # we'll assume "_" means "-" due to wheel naming scheme + # (https://github.com/pypa/pip/issues/1150) + self.version = wheel_info.group('ver').replace('_', '-') + self.build_tag = wheel_info.group('build') + self.pyversions = wheel_info.group('pyver').split('.') + self.abis = wheel_info.group('abi').split('.') + self.plats = wheel_info.group('plat').split('.') + + # All the tag combinations from this file + self.file_tags = { + (x, y, z) for x in self.pyversions + for y in self.abis for z in self.plats + } + + def get_formatted_file_tags(self): + # type: () -> List[str] + """Return the wheel's tags as a sorted list of strings.""" + return sorted(format_tag(tag) for tag in self.file_tags) + + def support_index_min(self, tags): + # type: (List[Pep425Tag]) -> int + """Return the lowest index that one of the wheel's file_tag combinations + achieves in the given list of supported tags. + + For example, if there are 8 supported tags and one of the file tags + is first in the list, then return 0. + + :param tags: the PEP 425 tags to check the wheel against, in order + with most preferred first. + + :raises ValueError: If none of the wheel's file tags match one of + the supported tags. + """ + return min(tags.index(tag) for tag in self.file_tags if tag in tags) + + def supported(self, tags): + # type: (List[Pep425Tag]) -> bool + """Return whether the wheel is compatible with one of the given tags. + + :param tags: the PEP 425 tags to check the wheel against. + """ + return not self.file_tags.isdisjoint(tags) diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index 70fb5e0860f..1f3cd8a104c 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -23,6 +23,7 @@ from pip._internal.exceptions import InstallationError from pip._internal.models.index import PyPI, TestPyPI from pip._internal.models.link import Link +from pip._internal.models.wheel import Wheel from pip._internal.pyproject import make_pyproject_path from pip._internal.req.req_install import InstallRequirement from pip._internal.utils.filetypes import ARCHIVE_EXTENSIONS @@ -30,7 +31,6 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.urls import path_to_url from pip._internal.vcs import is_url, vcs -from pip._internal.wheel import Wheel if MYPY_CHECK_RUNNING: from typing import ( diff --git a/src/pip/_internal/req/req_set.py b/src/pip/_internal/req/req_set.py index f1ad97fc768..087ac5925f5 100644 --- a/src/pip/_internal/req/req_set.py +++ b/src/pip/_internal/req/req_set.py @@ -10,9 +10,9 @@ from pip._internal import pep425tags from pip._internal.exceptions import InstallationError +from pip._internal.models.wheel import Wheel from pip._internal.utils.logging import indent_log from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from pip._internal.wheel import Wheel if MYPY_CHECK_RUNNING: from typing import Dict, Iterable, List, Optional, Tuple diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index e7b8c6bb6b4..98432427c28 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -25,13 +25,8 @@ from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.six import StringIO -from pip._internal.exceptions import ( - InstallationError, - InvalidWheelFilename, - UnsupportedWheel, -) +from pip._internal.exceptions import InstallationError, UnsupportedWheel from pip._internal.locations import get_major_minor_version -from pip._internal.pep425tags import format_tag from pip._internal.utils.misc import captured_stdout, ensure_dir, hash_file from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -42,7 +37,6 @@ ) from pip._internal.models.scheme import Scheme - from pip._internal.pep425tags import Pep425Tag InstalledCSVRow = Tuple[str, ...] @@ -664,71 +658,3 @@ def check_compatibility(version, name): 'Installing from a newer Wheel-Version (%s)', '.'.join(map(str, version)), ) - - -class Wheel(object): - """A wheel file""" - - # TODO: Maybe move the class into the models sub-package - - wheel_file_re = re.compile( - r"""^(?P<namever>(?P<name>.+?)-(?P<ver>.*?)) - ((-(?P<build>\d[^-]*?))?-(?P<pyver>.+?)-(?P<abi>.+?)-(?P<plat>.+?) - \.whl|\.dist-info)$""", - re.VERBOSE - ) - - def __init__(self, filename): - # type: (str) -> None - """ - :raises InvalidWheelFilename: when the filename is invalid for a wheel - """ - wheel_info = self.wheel_file_re.match(filename) - if not wheel_info: - raise InvalidWheelFilename( - "%s is not a valid wheel filename." % filename - ) - self.filename = filename - self.name = wheel_info.group('name').replace('_', '-') - # we'll assume "_" means "-" due to wheel naming scheme - # (https://github.com/pypa/pip/issues/1150) - self.version = wheel_info.group('ver').replace('_', '-') - self.build_tag = wheel_info.group('build') - self.pyversions = wheel_info.group('pyver').split('.') - self.abis = wheel_info.group('abi').split('.') - self.plats = wheel_info.group('plat').split('.') - - # All the tag combinations from this file - self.file_tags = { - (x, y, z) for x in self.pyversions - for y in self.abis for z in self.plats - } - - def get_formatted_file_tags(self): - # type: () -> List[str] - """Return the wheel's tags as a sorted list of strings.""" - return sorted(format_tag(tag) for tag in self.file_tags) - - def support_index_min(self, tags): - # type: (List[Pep425Tag]) -> int - """Return the lowest index that one of the wheel's file_tag combinations - achieves in the given list of supported tags. - - For example, if there are 8 supported tags and one of the file tags - is first in the list, then return 0. - - :param tags: the PEP 425 tags to check the wheel against, in order - with most preferred first. - - :raises ValueError: If none of the wheel's file tags match one of - the supported tags. - """ - return min(tags.index(tag) for tag in self.file_tags if tag in tags) - - def supported(self, tags): - # type: (List[Pep425Tag]) -> bool - """Return whether the wheel is compatible with one of the given tags. - - :param tags: the PEP 425 tags to check the wheel against. - """ - return not self.file_tags.isdisjoint(tags) diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 9fb5f8de90c..ce426566865 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -13,12 +13,12 @@ from pip._internal.exceptions import InvalidWheelFilename, UnsupportedWheel from pip._internal.locations import get_scheme from pip._internal.models.scheme import Scheme +from pip._internal.models.wheel import Wheel from pip._internal.utils.compat import WINDOWS from pip._internal.utils.misc import hash_file from pip._internal.utils.unpacking import unpack_file from pip._internal.wheel import ( MissingCallableSuffix, - Wheel, _raise_for_invalid_entrypoint, ) from pip._internal.wheel_builder import get_legacy_build_wheel_path From b8c16a0dc86519e283a94b14591f9ddae27f9c55 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 1 Dec 2019 17:07:00 -0500 Subject: [PATCH 0852/3170] Move Wheel tests --- tests/unit/test_models_wheel.py | 179 ++++++++++++++++++++++++++++++++ tests/unit/test_wheel.py | 176 +------------------------------ 2 files changed, 181 insertions(+), 174 deletions(-) create mode 100644 tests/unit/test_models_wheel.py diff --git a/tests/unit/test_models_wheel.py b/tests/unit/test_models_wheel.py new file mode 100644 index 00000000000..8b351356d83 --- /dev/null +++ b/tests/unit/test_models_wheel.py @@ -0,0 +1,179 @@ +import pytest + +from pip._internal import pep425tags +from pip._internal.exceptions import InvalidWheelFilename +from pip._internal.models.wheel import Wheel + + +class TestWheelFile(object): + + def test_std_wheel_pattern(self): + w = Wheel('simple-1.1.1-py2-none-any.whl') + assert w.name == 'simple' + assert w.version == '1.1.1' + assert w.pyversions == ['py2'] + assert w.abis == ['none'] + assert w.plats == ['any'] + + def test_wheel_pattern_multi_values(self): + w = Wheel('simple-1.1-py2.py3-abi1.abi2-any.whl') + assert w.name == 'simple' + assert w.version == '1.1' + assert w.pyversions == ['py2', 'py3'] + assert w.abis == ['abi1', 'abi2'] + assert w.plats == ['any'] + + def test_wheel_with_build_tag(self): + # pip doesn't do anything with build tags, but theoretically, we might + # see one, in this case the build tag = '4' + w = Wheel('simple-1.1-4-py2-none-any.whl') + assert w.name == 'simple' + assert w.version == '1.1' + assert w.pyversions == ['py2'] + assert w.abis == ['none'] + assert w.plats == ['any'] + + def test_single_digit_version(self): + w = Wheel('simple-1-py2-none-any.whl') + assert w.version == '1' + + def test_non_pep440_version(self): + w = Wheel('simple-_invalid_-py2-none-any.whl') + assert w.version == '-invalid-' + + def test_missing_version_raises(self): + with pytest.raises(InvalidWheelFilename): + Wheel('Cython-cp27-none-linux_x86_64.whl') + + def test_invalid_filename_raises(self): + with pytest.raises(InvalidWheelFilename): + Wheel('invalid.whl') + + def test_supported_single_version(self): + """ + Test single-version wheel is known to be supported + """ + w = Wheel('simple-0.1-py2-none-any.whl') + assert w.supported(tags=[('py2', 'none', 'any')]) + + def test_supported_multi_version(self): + """ + Test multi-version wheel is known to be supported + """ + w = Wheel('simple-0.1-py2.py3-none-any.whl') + assert w.supported(tags=[('py3', 'none', 'any')]) + + def test_not_supported_version(self): + """ + Test unsupported wheel is known to be unsupported + """ + w = Wheel('simple-0.1-py2-none-any.whl') + assert not w.supported(tags=[('py1', 'none', 'any')]) + + def test_supported_osx_version(self): + """ + Wheels built for macOS 10.6 are supported on 10.9 + """ + tags = pep425tags.get_supported( + '27', platform='macosx_10_9_intel', impl='cp' + ) + w = Wheel('simple-0.1-cp27-none-macosx_10_6_intel.whl') + assert w.supported(tags=tags) + w = Wheel('simple-0.1-cp27-none-macosx_10_9_intel.whl') + assert w.supported(tags=tags) + + def test_not_supported_osx_version(self): + """ + Wheels built for macOS 10.9 are not supported on 10.6 + """ + tags = pep425tags.get_supported( + '27', platform='macosx_10_6_intel', impl='cp' + ) + w = Wheel('simple-0.1-cp27-none-macosx_10_9_intel.whl') + assert not w.supported(tags=tags) + + def test_supported_multiarch_darwin(self): + """ + Multi-arch wheels (intel) are supported on components (i386, x86_64) + """ + universal = pep425tags.get_supported( + '27', platform='macosx_10_5_universal', impl='cp' + ) + intel = pep425tags.get_supported( + '27', platform='macosx_10_5_intel', impl='cp' + ) + x64 = pep425tags.get_supported( + '27', platform='macosx_10_5_x86_64', impl='cp' + ) + i386 = pep425tags.get_supported( + '27', platform='macosx_10_5_i386', impl='cp' + ) + ppc = pep425tags.get_supported( + '27', platform='macosx_10_5_ppc', impl='cp' + ) + ppc64 = pep425tags.get_supported( + '27', platform='macosx_10_5_ppc64', impl='cp' + ) + + w = Wheel('simple-0.1-cp27-none-macosx_10_5_intel.whl') + assert w.supported(tags=intel) + assert w.supported(tags=x64) + assert w.supported(tags=i386) + assert not w.supported(tags=universal) + assert not w.supported(tags=ppc) + assert not w.supported(tags=ppc64) + w = Wheel('simple-0.1-cp27-none-macosx_10_5_universal.whl') + assert w.supported(tags=universal) + assert w.supported(tags=intel) + assert w.supported(tags=x64) + assert w.supported(tags=i386) + assert w.supported(tags=ppc) + assert w.supported(tags=ppc64) + + def test_not_supported_multiarch_darwin(self): + """ + Single-arch wheels (x86_64) are not supported on multi-arch (intel) + """ + universal = pep425tags.get_supported( + '27', platform='macosx_10_5_universal', impl='cp' + ) + intel = pep425tags.get_supported( + '27', platform='macosx_10_5_intel', impl='cp' + ) + + w = Wheel('simple-0.1-cp27-none-macosx_10_5_i386.whl') + assert not w.supported(tags=intel) + assert not w.supported(tags=universal) + w = Wheel('simple-0.1-cp27-none-macosx_10_5_x86_64.whl') + assert not w.supported(tags=intel) + assert not w.supported(tags=universal) + + def test_support_index_min(self): + """ + Test results from `support_index_min` + """ + tags = [ + ('py2', 'none', 'TEST'), + ('py2', 'TEST', 'any'), + ('py2', 'none', 'any'), + ] + w = Wheel('simple-0.1-py2-none-any.whl') + assert w.support_index_min(tags=tags) == 2 + w = Wheel('simple-0.1-py2-none-TEST.whl') + assert w.support_index_min(tags=tags) == 0 + + def test_support_index_min__none_supported(self): + """ + Test a wheel not supported by the given tags. + """ + w = Wheel('simple-0.1-py2-none-any.whl') + with pytest.raises(ValueError): + w.support_index_min(tags=[]) + + def test_version_underscore_conversion(self): + """ + Test that we convert '_' to '-' for versions parsed out of wheel + filenames + """ + w = Wheel('simple-0.1_1-py2-none-any.whl') + assert w.version == '0.1-1' diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index ce426566865..b1bed86704f 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -8,12 +8,11 @@ from mock import patch from pip._vendor.packaging.requirements import Requirement -from pip._internal import pep425tags, wheel +from pip._internal import wheel from pip._internal.commands.wheel import WheelCommand -from pip._internal.exceptions import InvalidWheelFilename, UnsupportedWheel +from pip._internal.exceptions import UnsupportedWheel from pip._internal.locations import get_scheme from pip._internal.models.scheme import Scheme -from pip._internal.models.wheel import Wheel from pip._internal.utils.compat import WINDOWS from pip._internal.utils.misc import hash_file from pip._internal.utils.unpacking import unpack_file @@ -227,169 +226,6 @@ def test_check_compatibility(): class TestWheelFile(object): - def test_std_wheel_pattern(self): - w = Wheel('simple-1.1.1-py2-none-any.whl') - assert w.name == 'simple' - assert w.version == '1.1.1' - assert w.pyversions == ['py2'] - assert w.abis == ['none'] - assert w.plats == ['any'] - - def test_wheel_pattern_multi_values(self): - w = Wheel('simple-1.1-py2.py3-abi1.abi2-any.whl') - assert w.name == 'simple' - assert w.version == '1.1' - assert w.pyversions == ['py2', 'py3'] - assert w.abis == ['abi1', 'abi2'] - assert w.plats == ['any'] - - def test_wheel_with_build_tag(self): - # pip doesn't do anything with build tags, but theoretically, we might - # see one, in this case the build tag = '4' - w = Wheel('simple-1.1-4-py2-none-any.whl') - assert w.name == 'simple' - assert w.version == '1.1' - assert w.pyversions == ['py2'] - assert w.abis == ['none'] - assert w.plats == ['any'] - - def test_single_digit_version(self): - w = Wheel('simple-1-py2-none-any.whl') - assert w.version == '1' - - def test_non_pep440_version(self): - w = Wheel('simple-_invalid_-py2-none-any.whl') - assert w.version == '-invalid-' - - def test_missing_version_raises(self): - with pytest.raises(InvalidWheelFilename): - Wheel('Cython-cp27-none-linux_x86_64.whl') - - def test_invalid_filename_raises(self): - with pytest.raises(InvalidWheelFilename): - Wheel('invalid.whl') - - def test_supported_single_version(self): - """ - Test single-version wheel is known to be supported - """ - w = Wheel('simple-0.1-py2-none-any.whl') - assert w.supported(tags=[('py2', 'none', 'any')]) - - def test_supported_multi_version(self): - """ - Test multi-version wheel is known to be supported - """ - w = Wheel('simple-0.1-py2.py3-none-any.whl') - assert w.supported(tags=[('py3', 'none', 'any')]) - - def test_not_supported_version(self): - """ - Test unsupported wheel is known to be unsupported - """ - w = Wheel('simple-0.1-py2-none-any.whl') - assert not w.supported(tags=[('py1', 'none', 'any')]) - - def test_supported_osx_version(self): - """ - Wheels built for macOS 10.6 are supported on 10.9 - """ - tags = pep425tags.get_supported( - '27', platform='macosx_10_9_intel', impl='cp' - ) - w = Wheel('simple-0.1-cp27-none-macosx_10_6_intel.whl') - assert w.supported(tags=tags) - w = Wheel('simple-0.1-cp27-none-macosx_10_9_intel.whl') - assert w.supported(tags=tags) - - def test_not_supported_osx_version(self): - """ - Wheels built for macOS 10.9 are not supported on 10.6 - """ - tags = pep425tags.get_supported( - '27', platform='macosx_10_6_intel', impl='cp' - ) - w = Wheel('simple-0.1-cp27-none-macosx_10_9_intel.whl') - assert not w.supported(tags=tags) - - def test_supported_multiarch_darwin(self): - """ - Multi-arch wheels (intel) are supported on components (i386, x86_64) - """ - universal = pep425tags.get_supported( - '27', platform='macosx_10_5_universal', impl='cp' - ) - intel = pep425tags.get_supported( - '27', platform='macosx_10_5_intel', impl='cp' - ) - x64 = pep425tags.get_supported( - '27', platform='macosx_10_5_x86_64', impl='cp' - ) - i386 = pep425tags.get_supported( - '27', platform='macosx_10_5_i386', impl='cp' - ) - ppc = pep425tags.get_supported( - '27', platform='macosx_10_5_ppc', impl='cp' - ) - ppc64 = pep425tags.get_supported( - '27', platform='macosx_10_5_ppc64', impl='cp' - ) - - w = Wheel('simple-0.1-cp27-none-macosx_10_5_intel.whl') - assert w.supported(tags=intel) - assert w.supported(tags=x64) - assert w.supported(tags=i386) - assert not w.supported(tags=universal) - assert not w.supported(tags=ppc) - assert not w.supported(tags=ppc64) - w = Wheel('simple-0.1-cp27-none-macosx_10_5_universal.whl') - assert w.supported(tags=universal) - assert w.supported(tags=intel) - assert w.supported(tags=x64) - assert w.supported(tags=i386) - assert w.supported(tags=ppc) - assert w.supported(tags=ppc64) - - def test_not_supported_multiarch_darwin(self): - """ - Single-arch wheels (x86_64) are not supported on multi-arch (intel) - """ - universal = pep425tags.get_supported( - '27', platform='macosx_10_5_universal', impl='cp' - ) - intel = pep425tags.get_supported( - '27', platform='macosx_10_5_intel', impl='cp' - ) - - w = Wheel('simple-0.1-cp27-none-macosx_10_5_i386.whl') - assert not w.supported(tags=intel) - assert not w.supported(tags=universal) - w = Wheel('simple-0.1-cp27-none-macosx_10_5_x86_64.whl') - assert not w.supported(tags=intel) - assert not w.supported(tags=universal) - - def test_support_index_min(self): - """ - Test results from `support_index_min` - """ - tags = [ - ('py2', 'none', 'TEST'), - ('py2', 'TEST', 'any'), - ('py2', 'none', 'any'), - ] - w = Wheel('simple-0.1-py2-none-any.whl') - assert w.support_index_min(tags=tags) == 2 - w = Wheel('simple-0.1-py2-none-TEST.whl') - assert w.support_index_min(tags=tags) == 0 - - def test_support_index_min__none_supported(self): - """ - Test a wheel not supported by the given tags. - """ - w = Wheel('simple-0.1-py2-none-any.whl') - with pytest.raises(ValueError): - w.support_index_min(tags=[]) - def test_unpack_wheel_no_flatten(self, tmpdir): filepath = os.path.join(DATA_DIR, 'packages', 'meta-1.0-py2.py3-none-any.whl') @@ -412,14 +248,6 @@ def test_purelib_platlib(self, data): for name, path, expected in packages: assert wheel.root_is_purelib(name, path) == expected - def test_version_underscore_conversion(self): - """ - Test that we convert '_' to '-' for versions parsed out of wheel - filenames - """ - w = Wheel('simple-0.1_1-py2-none-any.whl') - assert w.version == '0.1-1' - class TestInstallUnpackedWheel(object): """ From 178cd3f2446632c779285f975991667a5c189f98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Sat, 9 Nov 2019 12:26:51 +0100 Subject: [PATCH 0853/3170] Better workaround for cache poisoning #3025 Make sure ``pip wheel`` never outputs pure python wheels with a python implementation tag. Better fix/workaround for `#3025 <https://github.com/pypa/pip/issues/3025>`_ by using a per-implementation wheel cache instead of caching pure python wheels with an implementation tag in their name. Fixes #7296 --- news/7296.bugfix | 5 +++++ src/pip/_internal/cache.py | 16 +++++++++++++++- src/pip/_internal/utils/misc.py | 11 +++++++++++ src/pip/_internal/wheel_builder.py | 10 +--------- tests/functional/test_install.py | 5 ++--- 5 files changed, 34 insertions(+), 13 deletions(-) create mode 100644 news/7296.bugfix diff --git a/news/7296.bugfix b/news/7296.bugfix new file mode 100644 index 00000000000..5d617bf75db --- /dev/null +++ b/news/7296.bugfix @@ -0,0 +1,5 @@ +Make sure ``pip wheel`` never outputs pure python wheels with a +python implementation tag. Better fix/workaround for +`#3025 <https://github.com/pypa/pip/issues/3025>`_ by +using a per-implementation wheel cache instead of caching pure python +wheels with an implementation tag in their name. diff --git a/src/pip/_internal/cache.py b/src/pip/_internal/cache.py index 7a431f9a2ed..1ed7d584cb1 100644 --- a/src/pip/_internal/cache.py +++ b/src/pip/_internal/cache.py @@ -8,6 +8,7 @@ import hashlib import logging import os +import sys from pip._vendor.packaging.utils import canonicalize_name @@ -15,6 +16,7 @@ from pip._internal.models.link import Link from pip._internal.models.wheel import Wheel from pip._internal.utils.compat import expanduser +from pip._internal.utils.misc import interpreter_name from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.urls import path_to_url @@ -65,11 +67,23 @@ def _get_cache_path_parts(self, link): ) key_url = "#".join(key_parts) + # Include interpreter name, major and minor version in cache key + # to cope with ill-behaved sdists that build a different wheel + # depending on the python version their setup.py is being run on, + # and don't encode the difference in compatibility tags. + # https://github.com/pypa/pip/issues/7296 + key = "{}-{}.{} {}".format( + interpreter_name(), + sys.version_info[0], + sys.version_info[1], + key_url, + ) + # 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() + hashed = hashlib.sha224(key.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 diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 85acab856b0..9a802556269 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -11,6 +11,7 @@ import io import logging import os +import platform import posixpath import shutil import stat @@ -879,3 +880,13 @@ def hash_file(path, blocksize=1 << 20): length += len(block) h.update(block) return (h, length) # type: ignore + + +def interpreter_name(): + # type: () -> str + try: + name = sys.implementation.name # type: ignore + except AttributeError: # pragma: no cover + # Python 2.7 compatibility. + name = platform.python_implementation().lower() + return name diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index 994b2f1a3dc..9a408a3dbf8 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -9,7 +9,6 @@ import re import shutil -from pip._internal import pep425tags from pip._internal.models.link import Link from pip._internal.utils.logging import indent_log from pip._internal.utils.marker_files import has_delete_marker_file @@ -447,10 +446,6 @@ def build( ', '.join([req.name for (req, _) in buildset]), ) - python_tag = None - if should_unpack: - python_tag = pep425tags.implementation_tag - with indent_log(): build_success, build_failure = [], [] for req, output_dir in buildset: @@ -464,10 +459,7 @@ def build( build_failure.append(req) continue - wheel_file = self._build_one( - req, output_dir, - python_tag=python_tag, - ) + wheel_file = self._build_one(req, output_dir) if wheel_file: if should_unpack: # XXX: This is mildly duplicative with prepare_files, diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index ffc3736ba06..6b62d863545 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -12,7 +12,6 @@ from pip._vendor.six import PY2 from pip import __version__ as pip_current_version -from pip._internal import pep425tags from pip._internal.cli.status_codes import ERROR, SUCCESS from pip._internal.models.index import PyPI, TestPyPI from pip._internal.utils.misc import rmtree @@ -1297,9 +1296,9 @@ def test_install_builds_wheels(script, data, with_wheel): assert "Running setup.py install for requir" not in str(res), str(res) # wheelbroken has to run install assert "Running setup.py install for wheelb" in str(res), str(res) - # We want to make sure we used the correct implementation tag + # We want to make sure pure python wheels do not have an implementation tag assert wheels == [ - "Upper-2.0-{}-none-any.whl".format(pep425tags.implementation_tag), + "Upper-2.0-py{}-none-any.whl".format(sys.version_info[0]), ] From b14b37545a371afde6e09e02fbf940121853bd1e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Sun, 10 Nov 2019 11:28:33 +0100 Subject: [PATCH 0854/3170] Remove unused pep425tags.implementation_tag --- src/pip/_internal/pep425tags.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index 44b2d7ee0f8..b8aba326c71 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -83,14 +83,6 @@ def get_impl_version_info(): return sys.version_info[0], sys.version_info[1] -def get_impl_tag(): - # type: () -> str - """ - Returns the Tag for this specific implementation. - """ - return "{}{}".format(get_abbr_impl(), get_impl_ver()) - - def get_flag(var, fallback, expected=True, warn=True): # type: (str, Callable[..., bool], Union[bool, int], bool) -> bool """Use a fallback method for determining SOABI flags if the needed config @@ -459,6 +451,3 @@ def get_supported( supported.append(('py%s' % (version,), 'none', 'any')) return supported - - -implementation_tag = get_impl_tag() From e0165e7b300bd166cdf355221da90d0c28dcff52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Sun, 10 Nov 2019 11:33:41 +0100 Subject: [PATCH 0855/3170] Remove unused wheel_builder python_tag argument --- src/pip/_internal/utils/setuptools_build.py | 3 -- src/pip/_internal/wheel_builder.py | 32 ++------------------- tests/unit/test_wheel_builder.py | 16 ----------- 3 files changed, 2 insertions(+), 49 deletions(-) diff --git a/src/pip/_internal/utils/setuptools_build.py b/src/pip/_internal/utils/setuptools_build.py index 497b0eb4939..4147a650dca 100644 --- a/src/pip/_internal/utils/setuptools_build.py +++ b/src/pip/_internal/utils/setuptools_build.py @@ -52,7 +52,6 @@ def make_setuptools_bdist_wheel_args( global_options, # type: Sequence[str] build_options, # type: Sequence[str] destination_dir, # type: str - python_tag, # type: Optional[str] ): # type: (...) -> List[str] # NOTE: Eventually, we'd want to also -S to the flags here, when we're @@ -66,8 +65,6 @@ def make_setuptools_bdist_wheel_args( ) args += ["bdist_wheel", "-d", destination_dir] args += build_options - if python_tag is not None: - args += ["--python-tag", python_tag] return args diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index 9a408a3dbf8..e67b18781dd 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -46,14 +46,6 @@ logger = logging.getLogger(__name__) -def replace_python_tag(wheelname, new_tag): - # type: (str, str) -> str - """Replace the Python tag in a wheel file name with a new value.""" - parts = wheelname.split('-') - parts[-3] = new_tag - return '-'.join(parts) - - def _contains_egg_info( s, _egg_info_re=re.compile(r'([a-z0-9_.]+)-([a-z0-9_.!+-]+)', re.I)): # type: (str, Pattern[str]) -> bool @@ -196,7 +188,6 @@ def _build_wheel_legacy( global_options, # type: List[str] build_options, # type: List[str] tempd, # type: str - python_tag=None, # type: Optional[str] ): # type: (...) -> Optional[str] """Build one unpacked package using the "legacy" build process. @@ -208,7 +199,6 @@ def _build_wheel_legacy( global_options=global_options, build_options=build_options, destination_dir=tempd, - python_tag=python_tag, ) spin_message = 'Building wheel for %s (setup.py)' % (name,) @@ -276,7 +266,6 @@ def _build_one( self, req, # type: InstallRequirement output_dir, # type: str - python_tag=None, # type: Optional[str] ): # type: (...) -> Optional[str] """Build one wheel. @@ -285,21 +274,17 @@ def _build_one( """ # 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) + return self._build_one_inside_env(req, output_dir) def _build_one_inside_env( self, req, # type: InstallRequirement output_dir, # type: str - python_tag=None, # type: Optional[str] ): # type: (...) -> Optional[str] with TempDirectory(kind="wheel") as temp_dir: if req.use_pep517: - wheel_path = self._build_one_pep517( - req, temp_dir.path, python_tag=python_tag - ) + wheel_path = self._build_one_pep517(req, temp_dir.path) else: wheel_path = _build_wheel_legacy( name=req.name, @@ -308,7 +293,6 @@ def _build_one_inside_env( global_options=self.global_options, build_options=self.build_options, tempd=temp_dir.path, - python_tag=python_tag, ) if wheel_path is not None: @@ -333,7 +317,6 @@ def _build_one_pep517( self, req, # type: InstallRequirement tempd, # type: str - python_tag=None, # type: Optional[str] ): # type: (...) -> Optional[str] """Build one InstallRequirement using the PEP 517 build process. @@ -358,17 +341,6 @@ def _build_one_pep517( tempd, metadata_directory=req.metadata_directory, ) - if python_tag: - # General PEP 517 backends don't necessarily support - # a "--python-tag" option, so we rename the wheel - # file directly. - new_name = replace_python_tag(wheel_name, python_tag) - os.rename( - os.path.join(tempd, wheel_name), - os.path.join(tempd, new_name) - ) - # Reassign to simplify the return at the end of function - wheel_name = new_name except Exception: logger.error('Failed building wheel for %s', req.name) return None diff --git a/tests/unit/test_wheel_builder.py b/tests/unit/test_wheel_builder.py index 8db535dadc2..3e0fc0490ef 100644 --- a/tests/unit/test_wheel_builder.py +++ b/tests/unit/test_wheel_builder.py @@ -182,22 +182,6 @@ def test_format_command_result__empty_output(caplog, log_level): ] -def test_python_tag(): - wheelnames = [ - 'simplewheel-1.0-py2.py3-none-any.whl', - 'simplewheel-1.0-py27-none-any.whl', - 'simplewheel-2.0-1-py2.py3-none-any.whl', - ] - newnames = [ - 'simplewheel-1.0-py37-none-any.whl', - 'simplewheel-1.0-py37-none-any.whl', - 'simplewheel-2.0-1-py37-none-any.whl', - ] - for name, expected in zip(wheelnames, newnames): - result = wheel_builder.replace_python_tag(name, 'py37') - assert result == expected - - class TestWheelBuilder(object): def test_skip_building_wheels(self, caplog): From c4ef6163e547961aded5a894e770c0b191088572 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Sat, 16 Nov 2019 11:58:53 +0100 Subject: [PATCH 0856/3170] New cache key generation algorithm Instead of building an URL-ish string that could be complex to describe and reproduce, generate a dictionary that is hashed with a simple algorithm. --- src/pip/_internal/cache.py | 28 ++++++++++++++++++---------- tests/unit/test_cache.py | 9 ++++++++- 2 files changed, 26 insertions(+), 11 deletions(-) diff --git a/src/pip/_internal/cache.py b/src/pip/_internal/cache.py index 1ed7d584cb1..502de4d0bb3 100644 --- a/src/pip/_internal/cache.py +++ b/src/pip/_internal/cache.py @@ -22,13 +22,25 @@ from pip._internal.utils.urls import path_to_url if MYPY_CHECK_RUNNING: - from typing import Optional, Set, List, Any + from typing import Optional, Set, List, Any, Dict from pip._internal.models.format_control import FormatControl from pip._internal.pep425tags import Pep425Tag logger = logging.getLogger(__name__) +def _hash_dict(d): + # type: (Dict[str, str]) -> str + """Return a sha224 of a dictionary where keys and values are strings.""" + h = hashlib.new('sha224') + for k in sorted(d.keys()): + h.update(k.encode()) + h.update("=".encode()) + h.update(d[k].encode()) + h.update(b"\0") + return h.hexdigest() + + class Cache(object): """An abstract class - provides cache directories for data from links @@ -58,32 +70,28 @@ def _get_cache_path_parts(self, link): # 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] + key_parts = {"url": 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_parts[link.hash_name] = link.hash if link.subdirectory_fragment: - key_parts.append( - "=".join(["subdirectory", link.subdirectory_fragment]) - ) - key_url = "#".join(key_parts) + key_parts["subdirectory"] = link.subdirectory_fragment # Include interpreter name, major and minor version in cache key # to cope with ill-behaved sdists that build a different wheel # depending on the python version their setup.py is being run on, # and don't encode the difference in compatibility tags. # https://github.com/pypa/pip/issues/7296 - key = "{}-{}.{} {}".format( + key_parts["interpreter"] = "{}-{}.{}".format( interpreter_name(), sys.version_info[0], sys.version_info[1], - key_url, ) # 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.encode()).hexdigest() + hashed = _hash_dict(key_parts) # We want to nest the directories some to prevent having a ton of top # level directories where we might run out of sub directories on some diff --git a/tests/unit/test_cache.py b/tests/unit/test_cache.py index 79e4f624d19..e11b3e78a08 100644 --- a/tests/unit/test_cache.py +++ b/tests/unit/test_cache.py @@ -1,6 +1,6 @@ import os -from pip._internal.cache import WheelCache +from pip._internal.cache import WheelCache, _hash_dict from pip._internal.models.format_control import FormatControl from pip._internal.models.link import Link from pip._internal.utils.compat import expanduser @@ -42,3 +42,10 @@ def test_wheel_name_filter(tmpdir): assert wc.get(link, "package", [("py3", "none", "any")]) is not link # package2 does not match wheel name assert wc.get(link, "package2", [("py3", "none", "any")]) is link + + +def test_cache_hash(): + h = _hash_dict({"url": "https://g.c/o/r"}) + assert h == "c7d60d08b1079254d236e983501fa26c016d58d16010725b27ed0af2" + h = _hash_dict({"url": "https://g.c/o/r", "subdirectory": "sd"}) + assert h == "9cba35d4ccf04b7cde751b44db347fd0f21fa47d1276e32f9d47864c" From 66ba51ca7d2ae42b1ab8d17aa3a00e158baf6541 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Sun, 17 Nov 2019 11:44:20 +0100 Subject: [PATCH 0857/3170] Use legacy cache entries when they exist. Pip 20 changes the cache key format to include the interpreter name. To avoid invalidating all existing caches, we continue using existing cache entries that were computed with the legacy algorithm. This should not regress issue #3025 because wheel cached in such legacy entries should have the python implementation tag set. --- src/pip/_internal/cache.py | 58 ++++++++++++++++++++++++++++++++------ tests/unit/test_cache.py | 20 +++++++++++++ 2 files changed, 70 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/cache.py b/src/pip/_internal/cache.py index 502de4d0bb3..e8495e82515 100644 --- a/src/pip/_internal/cache.py +++ b/src/pip/_internal/cache.py @@ -4,7 +4,6 @@ # The following comment should be removed at some point in the future. # mypy: strict-optional=False -import errno import hashlib import logging import os @@ -62,6 +61,34 @@ def __init__(self, cache_dir, format_control, allowed_formats): _valid_formats = {"source", "binary"} assert self.allowed_formats.union(_valid_formats) == _valid_formats + def _get_cache_path_parts_legacy(self, link): + # type: (Link) -> List[str] + """Get parts of part that must be os.path.joined with cache_dir + + Legacy cache key (pip < 20) for compatibility with older caches. + """ + + # We want to generate an url to use as our cache key, we don't want to + # 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_cache_path_parts(self, link): # type: (Link) -> List[str] """Get parts of part that must be os.path.joined with cache_dir @@ -116,13 +143,19 @@ def _get_candidates(self, link, canonical_package_name): if not self.allowed_formats.intersection(formats): return [] - root = self.get_path_for_link(link) - try: - return os.listdir(root) - except OSError as err: - if err.errno in {errno.ENOENT, errno.ENOTDIR}: - return [] - raise + candidates = [] + path = self.get_path_for_link(link) + if os.path.isdir(path): + candidates.extend(os.listdir(path)) + # TODO remove legacy path lookup in pip>=21 + legacy_path = self.get_path_for_link_legacy(link) + if os.path.isdir(legacy_path): + candidates.extend(os.listdir(legacy_path)) + return candidates + + def get_path_for_link_legacy(self, link): + # type: (Link) -> str + raise NotImplementedError() def get_path_for_link(self, link): # type: (Link) -> str @@ -164,6 +197,11 @@ def __init__(self, cache_dir, format_control): cache_dir, format_control, {"binary"} ) + def get_path_for_link_legacy(self, link): + # type: (Link) -> str + parts = self._get_cache_path_parts_legacy(link) + return os.path.join(self.cache_dir, "wheels", *parts) + def get_path_for_link(self, link): # type: (Link) -> str """Return a directory to store cached wheels for link @@ -256,6 +294,10 @@ def __init__(self, cache_dir, format_control): self._wheel_cache = SimpleWheelCache(cache_dir, format_control) self._ephem_cache = EphemWheelCache(format_control) + def get_path_for_link_legacy(self, link): + # type: (Link) -> str + return self._wheel_cache.get_path_for_link_legacy(link) + def get_path_for_link(self, link): # type: (Link) -> str return self._wheel_cache.get_path_for_link(link) diff --git a/tests/unit/test_cache.py b/tests/unit/test_cache.py index e11b3e78a08..a9baf7e95c3 100644 --- a/tests/unit/test_cache.py +++ b/tests/unit/test_cache.py @@ -49,3 +49,23 @@ def test_cache_hash(): assert h == "c7d60d08b1079254d236e983501fa26c016d58d16010725b27ed0af2" h = _hash_dict({"url": "https://g.c/o/r", "subdirectory": "sd"}) assert h == "9cba35d4ccf04b7cde751b44db347fd0f21fa47d1276e32f9d47864c" + + +def test_get_path_for_link_legacy(tmpdir): + """ + Test that an existing cache entry that was created with the legacy hashing + mechanism is used. + """ + wc = WheelCache(tmpdir, FormatControl()) + link = Link("https://g.c/o/r") + path = wc.get_path_for_link(link) + legacy_path = wc.get_path_for_link_legacy(link) + assert path != legacy_path + ensure_dir(path) + with open(os.path.join(path, "test-pyz-none-any.whl"), "w"): + pass + ensure_dir(legacy_path) + with open(os.path.join(legacy_path, "test-pyx-none-any.whl"), "w"): + pass + expected_candidates = {"test-pyx-none-any.whl", "test-pyz-none-any.whl"} + assert set(wc._get_candidates(link, "test")) == expected_candidates From bfb7db2f68dc707db8e4c4de219da09318ed017a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Mon, 18 Nov 2019 14:45:50 +0100 Subject: [PATCH 0858/3170] Advertise the new cache structure in a news file --- news/7296.removal | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 news/7296.removal diff --git a/news/7296.removal b/news/7296.removal new file mode 100644 index 00000000000..bd261c1f33f --- /dev/null +++ b/news/7296.removal @@ -0,0 +1,3 @@ +The pip>=20 wheel cache is not retro-compatible with previous versions. Until +version 21, pip will still be able to take advantage of existing legacy cache +entries. From 824dca1060027c2675fc442a98701469841f7aae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Mon, 18 Nov 2019 17:26:28 +0100 Subject: [PATCH 0859/3170] Better support for unicode cache entries --- src/pip/_internal/cache.py | 12 ++++-------- tests/unit/test_cache.py | 6 ++++-- 2 files changed, 8 insertions(+), 10 deletions(-) diff --git a/src/pip/_internal/cache.py b/src/pip/_internal/cache.py index e8495e82515..930205f6461 100644 --- a/src/pip/_internal/cache.py +++ b/src/pip/_internal/cache.py @@ -5,6 +5,7 @@ # mypy: strict-optional=False import hashlib +import json import logging import os import sys @@ -30,14 +31,9 @@ def _hash_dict(d): # type: (Dict[str, str]) -> str - """Return a sha224 of a dictionary where keys and values are strings.""" - h = hashlib.new('sha224') - for k in sorted(d.keys()): - h.update(k.encode()) - h.update("=".encode()) - h.update(d[k].encode()) - h.update(b"\0") - return h.hexdigest() + """Return a stable sha224 of a dictionary.""" + s = json.dumps(d, sort_keys=True, separators=(",", ":"), ensure_ascii=True) + return hashlib.sha224(s.encode("ascii")).hexdigest() class Cache(object): diff --git a/tests/unit/test_cache.py b/tests/unit/test_cache.py index a9baf7e95c3..8926f3af84e 100644 --- a/tests/unit/test_cache.py +++ b/tests/unit/test_cache.py @@ -46,9 +46,11 @@ def test_wheel_name_filter(tmpdir): def test_cache_hash(): h = _hash_dict({"url": "https://g.c/o/r"}) - assert h == "c7d60d08b1079254d236e983501fa26c016d58d16010725b27ed0af2" + assert h == "72aa79d3315c181d2cc23239d7109a782de663b6f89982624d8c1e86" h = _hash_dict({"url": "https://g.c/o/r", "subdirectory": "sd"}) - assert h == "9cba35d4ccf04b7cde751b44db347fd0f21fa47d1276e32f9d47864c" + assert h == "8b13391b6791bf7f3edeabb41ea4698d21bcbdbba7f9c7dc9339750d" + h = _hash_dict({"subdirectory": u"/\xe9e"}) + assert h == "f83b32dfa27a426dec08c21bf006065dd003d0aac78e7fc493d9014d" def test_get_path_for_link_legacy(tmpdir): From ab0593659df4e51748636e2e0068224f83a9b1e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Sun, 24 Nov 2019 19:08:51 +0100 Subject: [PATCH 0860/3170] Use interpreter_name and _version in cache keys --- src/pip/_internal/cache.py | 10 +++------- src/pip/_internal/pep425tags.py | 6 ++++++ src/pip/_internal/utils/misc.py | 11 ----------- 3 files changed, 9 insertions(+), 18 deletions(-) diff --git a/src/pip/_internal/cache.py b/src/pip/_internal/cache.py index 930205f6461..8c2041527c1 100644 --- a/src/pip/_internal/cache.py +++ b/src/pip/_internal/cache.py @@ -8,15 +8,14 @@ import json import logging import os -import sys from pip._vendor.packaging.utils import canonicalize_name from pip._internal.exceptions import InvalidWheelFilename from pip._internal.models.link import Link from pip._internal.models.wheel import Wheel +from pip._internal.pep425tags import interpreter_name, interpreter_version from pip._internal.utils.compat import expanduser -from pip._internal.utils.misc import interpreter_name from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.urls import path_to_url @@ -104,11 +103,8 @@ def _get_cache_path_parts(self, link): # depending on the python version their setup.py is being run on, # and don't encode the difference in compatibility tags. # https://github.com/pypa/pip/issues/7296 - key_parts["interpreter"] = "{}-{}.{}".format( - interpreter_name(), - sys.version_info[0], - sys.version_info[1], - ) + key_parts["interpreter_name"] = interpreter_name() + key_parts["interpreter_version"] = interpreter_version() # Encode our key url with sha224, we'll use this because it has similar # security properties to sha256, but with a shorter total output (and diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index b8aba326c71..3a291ebaeb1 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -54,6 +54,9 @@ def get_abbr_impl(): return pyimpl +interpreter_name = get_abbr_impl + + def version_info_to_nodot(version_info): # type: (Tuple[int, ...]) -> str # Only use up to the first two numbers. @@ -69,6 +72,9 @@ def get_impl_ver(): return impl_ver +interpreter_version = get_impl_ver + + def get_impl_version_info(): # type: () -> Tuple[int, ...] """Return sys.version_info-like tuple for use in decrementing the minor diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 9a802556269..85acab856b0 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -11,7 +11,6 @@ import io import logging import os -import platform import posixpath import shutil import stat @@ -880,13 +879,3 @@ def hash_file(path, blocksize=1 << 20): length += len(block) h.update(block) return (h, length) # type: ignore - - -def interpreter_name(): - # type: () -> str - try: - name = sys.implementation.name # type: ignore - except AttributeError: # pragma: no cover - # Python 2.7 compatibility. - name = platform.python_implementation().lower() - return name From e3c1ca137f2557f88e4bfb68d971f56531c7ff0a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Mon, 2 Dec 2019 12:06:32 +0100 Subject: [PATCH 0861/3170] Update news/7296.removal Co-Authored-By: Pradyun Gedam <pradyunsg@gmail.com> --- news/7296.removal | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/7296.removal b/news/7296.removal index bd261c1f33f..ef0e5f7495f 100644 --- a/news/7296.removal +++ b/news/7296.removal @@ -1,3 +1,3 @@ The pip>=20 wheel cache is not retro-compatible with previous versions. Until -version 21, pip will still be able to take advantage of existing legacy cache +pip 21.0, pip will continue to take advantage of existing legacy cache entries. From ca43dac401bff076a8f5a1d71ed55ff0bfee4e87 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 1 Dec 2019 17:20:34 -0500 Subject: [PATCH 0862/3170] Move wheel version check to install_unpacked_wheel This helps in several ways: 1. makes it easier to test the correct behavior of wheel installation via `install_unpacked_wheel` independent of `InstallRequirement` 2. is easier to understand, since `install_unpacked_wheel` itself should know which Wheel version(s) it supports 3. reduces the scope of `check_compatibility` and `wheel_version`, which will make it easier to move `wheel` to `operations.install.wheel` --- src/pip/_internal/req/req_install.py | 3 --- src/pip/_internal/wheel.py | 5 +++++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 2f82ab8c98e..2fc3c675a17 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -814,9 +814,6 @@ def install( return if self.is_wheel: - version = wheel.wheel_version(self.source_dir) - wheel.check_compatibility(version, self.name) - self.move_wheel_files( self.source_dir, scheme=scheme, diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/wheel.py index 98432427c28..6b835b527c2 100644 --- a/src/pip/_internal/wheel.py +++ b/src/pip/_internal/wheel.py @@ -313,11 +313,16 @@ def install_unpacked_wheel( :param pycompile: Whether to byte-compile installed Python files :param warn_script_location: Whether to check that scripts are installed into a directory on PATH + :raises UnsupportedWheel: when the directory holds an unpacked wheel with + incompatible Wheel-Version """ # TODO: Investigate and break this up. # TODO: Look into moving this into a dedicated class for representing an # installation. + version = wheel_version(wheeldir) + check_compatibility(version, name) + if root_is_purelib(name, wheeldir): lib_dir = scheme.purelib else: From 830e29e1891d6f4ddd543e287aec6bb0737fe896 Mon Sep 17 00:00:00 2001 From: Christopher Hunt <chrahunt@gmail.com> Date: Wed, 4 Dec 2019 06:02:39 +0800 Subject: [PATCH 0863/3170] Move wheel to operations.install.wheel (#7421) * Update documentation For now just fixing the paths and adding a sub-package docstring. --- docs/html/development/architecture/anatomy.rst | 6 +++++- src/pip/_internal/operations/install/__init__.py | 2 ++ src/pip/_internal/{ => operations/install}/wheel.py | 0 src/pip/_internal/req/req_install.py | 5 +++-- tests/unit/test_finder.py | 1 - tests/unit/test_wheel.py | 10 +++++----- 6 files changed, 15 insertions(+), 9 deletions(-) create mode 100644 src/pip/_internal/operations/install/__init__.py rename src/pip/_internal/{ => operations/install}/wheel.py (100%) diff --git a/docs/html/development/architecture/anatomy.rst b/docs/html/development/architecture/anatomy.rst index 25593600c5e..9613460b0cb 100644 --- a/docs/html/development/architecture/anatomy.rst +++ b/docs/html/development/architecture/anatomy.rst @@ -91,13 +91,17 @@ Within ``src/``: * ``locations.py`` * ``models/`` *[in-process refactoring! Goal: improve how pip internally models representations it has for data -- data representation. General overall cleanup. Data reps are spread throughout codebase….link is defined in a class in 1 file, and then another file imports Link from that file. Sometimes cyclic dependency?!?! To prevent future situations like this, etc., Pradyun started moving these into a models directory.]* * ``operations/`` -- a bit of a weird directory….. ``Freeze.py`` used to be in there. Freeze is an operation -- there was an operations.freeze. Then “prepare” got added (the operation of preparing a pkg). Then “check” got added for checking the state of an env.] [what’s a command vs an operation? Command is on CLI; an operation would be an internal bit of code that actually does some subset of the operation the command says. ``install`` command uses bits of ``check`` and ``prepare``, for instance. In the long run, Pradyun’s goal: ``prepare.py`` goes away (gets refactored into other files) such that ``operations`` is just ``check`` and ``freeze``..... … Pradyun plans to refactor this. [how does this compare to ``utils``?] + + * ``install/`` -- for modules related to installing projects of various kinds + + * ``wheel.py`` is a file that manages installation of a wheel file. This handles unpacking wheels -- “unpack and spread”. There is a package on PyPI called ``wheel`` that builds wheels -- do not confuse it with this. + * ``pep425tags.py`` -- getting refactored into packaging.tags (a library on PyPI) which is external to pip (but vendored by pip). :pep:`425` tags: turns out lots of people want this! Compatibility tags for built distributions -> e.g., platform, Python version, etc. * ``pyproject.py`` -- ``pyproject.toml`` is a new standard (:pep:`518` and :pep:`517`). This file reads pyproject.toml and passes that info elsewhere. The rest of the processing happens in a different file. All the handling for 517 and 518 is in a different file. * ``req/`` *[*\ **A DIRECTORY THAT NEEDS REFACTORING. A LOT**\ *\ …… Remember Step 3? Dependency resolution etc.? This is that step! Each file represents … have the entire flow of installing & uninstalling, getting info about packages…. Some files here are more than 1,000 lines long! (used to be longer?!) Refactor will deeply improve developer experience.]* * ``resolve.py`` -- This is where the current dependency resolution algorithm sits. Pradyun is `improving the pip dependency resolver`_. Pradyun will get rid of this file and replace it with a directory called “resolution”. (this work is in git master…. There is further work that is going to be in a branch soon) * ``utils/`` *[everything that is not “operationally” pip ….. Misc functions and files get dumped. There’s some organization here. There’s a models.py here which needs refactoring. Deprecation.py is useful, as are other things, but some things do not belong here. There ought to be some GitHub issues for refactoring some things here. Maybe a few issues with checkbox lists.]* * ``vcs/`` *[stands for Version Control System. Where pip handles all version control stuff -- one of the ``pip install`` arguments you can use is a version control link. Are any of these commands vendored? No, via subprocesses. For performance, it makes sense (we think) to do this instead of pygitlib2 or similar -- and has to be pure Python, can’t include C libraries, because you can’t include compiled C stuff, because you might not have it for the platform you are running on.]* - * ``wheel.py`` is a file that manages installation of a wheel file. This handles unpacking wheels -- “unpack and spread”. There is a package on PyPI called ``wheel`` that builds wheels -- do not confuse it with this. * ``_vendor/`` *[code from other packages -- pip’s own dependencies…. Has them in its own source tree, because pip cannot depend on pip being installed on the machine already!]* diff --git a/src/pip/_internal/operations/install/__init__.py b/src/pip/_internal/operations/install/__init__.py new file mode 100644 index 00000000000..24d6a5dd31f --- /dev/null +++ b/src/pip/_internal/operations/install/__init__.py @@ -0,0 +1,2 @@ +"""For modules related to installing packages. +""" diff --git a/src/pip/_internal/wheel.py b/src/pip/_internal/operations/install/wheel.py similarity index 100% rename from src/pip/_internal/wheel.py rename to src/pip/_internal/operations/install/wheel.py diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 2fc3c675a17..a821e86d535 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -17,7 +17,7 @@ from pip._vendor.packaging.version import parse as parse_version from pip._vendor.pep517.wrappers import Pep517HookCaller -from pip._internal import pep425tags, wheel +from pip._internal import pep425tags from pip._internal.build_env import NoOpBuildEnvironment from pip._internal.exceptions import InstallationError from pip._internal.locations import get_scheme @@ -25,6 +25,7 @@ from pip._internal.operations.build.metadata import generate_metadata from pip._internal.operations.build.metadata_legacy import \ generate_metadata as generate_metadata_legacy +from pip._internal.operations.install.wheel import install_unpacked_wheel from pip._internal.pyproject import load_pyproject_toml, make_pyproject_path from pip._internal.req.req_uninstall import UninstallPathSet from pip._internal.utils.deprecation import deprecated @@ -486,7 +487,7 @@ def move_wheel_files( pycompile=True # type: bool ): # type: (...) -> None - wheel.install_unpacked_wheel( + install_unpacked_wheel( self.name, wheeldir, scheme=scheme, diff --git a/tests/unit/test_finder.py b/tests/unit/test_finder.py index ddfcc7860ed..b8e179c115e 100644 --- a/tests/unit/test_finder.py +++ b/tests/unit/test_finder.py @@ -7,7 +7,6 @@ from pkg_resources import parse_version import pip._internal.pep425tags -import pip._internal.wheel from pip._internal.exceptions import ( BestVersionAlreadyInstalled, DistributionNotFound, diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index b1bed86704f..9e7e98b1f1c 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -8,18 +8,18 @@ from mock import patch from pip._vendor.packaging.requirements import Requirement -from pip._internal import wheel from pip._internal.commands.wheel import WheelCommand from pip._internal.exceptions import UnsupportedWheel from pip._internal.locations import get_scheme from pip._internal.models.scheme import Scheme -from pip._internal.utils.compat import WINDOWS -from pip._internal.utils.misc import hash_file -from pip._internal.utils.unpacking import unpack_file -from pip._internal.wheel import ( +from pip._internal.operations.install import wheel +from pip._internal.operations.install.wheel import ( MissingCallableSuffix, _raise_for_invalid_entrypoint, ) +from pip._internal.utils.compat import WINDOWS +from pip._internal.utils.misc import hash_file +from pip._internal.utils.unpacking import unpack_file from pip._internal.wheel_builder import get_legacy_build_wheel_path from tests.lib import DATA_DIR, assert_paths_equal From d3419a4b7edc770fdccf4c826f106293f7bd6704 Mon Sep 17 00:00:00 2001 From: Christopher Hunt <chrahunt@gmail.com> Date: Wed, 4 Dec 2019 07:20:00 +0800 Subject: [PATCH 0864/3170] Don't use check_if_exists in InstallRequirement.uninstall (#7422) Previously InstallRequirement.uninstall was using InstallRequirement.check_if_exists, which is a very overloaded function with several callers that operate at different phases in pip processing, not to mention that it mutates InstallRequirement itself. Now we don't use that function for InstallRequirement.uninstall. There should be no behavior change here. --- src/pip/_internal/req/req_install.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index a821e86d535..328a43cf6f5 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -679,9 +679,8 @@ def update_editable(self, obtain=True): % (self.link, vc_type)) # Top-level Actions - def uninstall(self, auto_confirm=False, verbose=False, - use_user_site=False): - # type: (bool, bool, bool) -> Optional[UninstallPathSet] + def uninstall(self, auto_confirm=False, verbose=False): + # type: (bool, bool) -> Optional[UninstallPathSet] """ Uninstall the distribution currently satisfying this requirement. @@ -694,10 +693,12 @@ def uninstall(self, auto_confirm=False, verbose=False, linked to global site-packages. """ - if not self.check_if_exists(use_user_site): + assert self.req + try: + dist = pkg_resources.get_distribution(self.req.name) + except pkg_resources.DistributionNotFound: logger.warning("Skipping %s as it is not installed.", self.name) return None - dist = self.satisfied_by or self.conflicts_with uninstalled_pathset = UninstallPathSet.from_dist(dist) uninstalled_pathset.remove(auto_confirm, verbose) From e3cd8922f20737a75b353dbe00d390624e82c670 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 3 Dec 2019 18:54:22 -0500 Subject: [PATCH 0865/3170] Remove unused return value from check_if_exists After refactoring InstallRequirement.uninstall to not call this function, none of the remaining callers require the return value. --- src/pip/_internal/req/req_install.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 328a43cf6f5..96406ee619c 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -429,13 +429,13 @@ def remove_temporary_source(self): self.build_env.cleanup() def check_if_exists(self, use_user_site): - # type: (bool) -> bool + # type: (bool) -> None """Find an installed distribution that satisfies or conflicts with this requirement, and set self.satisfied_by or self.conflicts_with appropriately. """ if self.req is None: - return False + return # get_distribution() will resolve the entire list of requirements # anyway, and we've already determined that we need the requirement # in question, so strip the marker so that we don't try to @@ -445,7 +445,7 @@ def check_if_exists(self, use_user_site): try: self.satisfied_by = pkg_resources.get_distribution(str(no_marker)) except pkg_resources.DistributionNotFound: - return False + pass except pkg_resources.VersionConflict: existing_dist = pkg_resources.get_distribution( self.req.name @@ -468,8 +468,6 @@ def check_if_exists(self, use_user_site): # when installing editables, nothing pre-existing should ever # satisfy self.satisfied_by = None - return True - return True # Things valid for wheels @property From c718994c5d704a48e4dfcdbfcf2a8454c590578e Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 3 Dec 2019 19:45:55 -0500 Subject: [PATCH 0866/3170] Move install_succeeded for editable installs This aligns it with wheel installation and reinforces that it is the very last thing that happens before we return (so we can potentially refactor it out later). --- src/pip/_internal/req/req_install.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 328a43cf6f5..2c5883d097f 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -648,8 +648,6 @@ def install_editable( cwd=self.unpacked_source_directory, ) - self.install_succeeded = True - def update_editable(self, obtain=True): # type: (bool) -> None if not self.link: @@ -813,6 +811,7 @@ def install( home=home, use_user_site=use_user_site, ) + self.install_succeeded = True return if self.is_wheel: From b714904285618a0204802464a30352fc2d97ffd3 Mon Sep 17 00:00:00 2001 From: Christopher Hunt <chrahunt@gmail.com> Date: Wed, 4 Dec 2019 21:31:24 +0800 Subject: [PATCH 0867/3170] Update src/pip/_internal/req/req_install.py --- src/pip/_internal/req/req_install.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 96406ee619c..bc466a1f384 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -445,7 +445,7 @@ def check_if_exists(self, use_user_site): try: self.satisfied_by = pkg_resources.get_distribution(str(no_marker)) except pkg_resources.DistributionNotFound: - pass + return except pkg_resources.VersionConflict: existing_dist = pkg_resources.get_distribution( self.req.name From db766f3c95fc62068ab0d7fbc1df10ac1878bdc5 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 29 Nov 2019 11:03:48 -0500 Subject: [PATCH 0868/3170] Move helper mock classes to dedicated module When we factor out tests these will be needed in both sets, and it's easier to refactor tests later if we avoid creating a dependency between test files. --- tests/lib/requests_mocks.py | 58 +++++++++++++++++++++++++++ tests/unit/test_networking_auth.py | 6 +-- tests/unit/test_operations_prepare.py | 56 +------------------------- 3 files changed, 60 insertions(+), 60 deletions(-) create mode 100644 tests/lib/requests_mocks.py diff --git a/tests/lib/requests_mocks.py b/tests/lib/requests_mocks.py new file mode 100644 index 00000000000..baaf77ecc25 --- /dev/null +++ b/tests/lib/requests_mocks.py @@ -0,0 +1,58 @@ +"""Helper classes as mocks for requests objects. +""" + +from io import BytesIO + + +class FakeStream(object): + + def __init__(self, contents): + self._io = BytesIO(contents) + + def read(self, size, decode_content=None): + return self._io.read(size) + + def stream(self, size, decode_content=None): + yield self._io.read(size) + + def release_conn(self): + pass + + +class MockResponse(object): + + def __init__(self, contents): + self.raw = FakeStream(contents) + self.content = contents + self.request = None + self.status_code = 200 + self.connection = None + self.url = None + self.headers = {} + self.history = [] + + def raise_for_status(self): + pass + + +class MockConnection(object): + + def _send(self, req, **kwargs): + raise NotImplementedError("_send must be overridden for tests") + + def send(self, req, **kwargs): + resp = self._send(req, **kwargs) + for cb in req.hooks.get("response", []): + cb(resp) + return resp + + +class MockRequest(object): + + def __init__(self, url): + self.url = url + self.headers = {} + self.hooks = {} + + def register_hook(self, event_name, callback): + self.hooks.setdefault(event_name, []).append(callback) diff --git a/tests/unit/test_networking_auth.py b/tests/unit/test_networking_auth.py index 45702f27b67..1fadd1db348 100644 --- a/tests/unit/test_networking_auth.py +++ b/tests/unit/test_networking_auth.py @@ -4,11 +4,7 @@ import pip._internal.network.auth from pip._internal.network.auth import MultiDomainBasicAuth -from tests.unit.test_operations_prepare import ( - MockConnection, - MockRequest, - MockResponse, -) +from tests.lib.requests_mocks import MockConnection, MockRequest, MockResponse @pytest.mark.parametrize(["input_url", "url", "username", "password"], [ diff --git a/tests/unit/test_operations_prepare.py b/tests/unit/test_operations_prepare.py index 80ad5ce29f1..3d5d0fef116 100644 --- a/tests/unit/test_operations_prepare.py +++ b/tests/unit/test_operations_prepare.py @@ -3,7 +3,6 @@ import os import shutil import sys -from io import BytesIO from shutil import copy, rmtree from tempfile import mkdtemp @@ -32,6 +31,7 @@ make_unreadable_file, ) from tests.lib.path import Path +from tests.lib.requests_mocks import MockResponse def test_unpack_http_url_with_urllib_response_without_content_type(data): @@ -66,60 +66,6 @@ def _fake_session_get(*args, **kwargs): rmtree(temp_dir) -class FakeStream(object): - - def __init__(self, contents): - self._io = BytesIO(contents) - - def read(self, size, decode_content=None): - return self._io.read(size) - - def stream(self, size, decode_content=None): - yield self._io.read(size) - - def release_conn(self): - pass - - -class MockResponse(object): - - def __init__(self, contents): - self.raw = FakeStream(contents) - self.content = contents - self.request = None - self.status_code = 200 - self.connection = None - self.url = None - self.headers = {} - self.history = [] - - def raise_for_status(self): - pass - - -class MockConnection(object): - - def _send(self, req, **kwargs): - raise NotImplementedError("_send must be overridden for tests") - - def send(self, req, **kwargs): - resp = self._send(req, **kwargs) - for cb in req.hooks.get("response", []): - cb(resp) - return resp - - -class MockRequest(object): - - def __init__(self, url): - self.url = url - self.headers = {} - self.hooks = {} - - def register_hook(self, event_name, callback): - self.hooks.setdefault(event_name, []).append(callback) - - @patch('pip._internal.operations.prepare.unpack_file') def test_unpack_http_url_bad_downloaded_checksum(mock_unpack_file): """ From 864f78bd96b9990a5add51c8d0f1b06cec00dd2f Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 29 Nov 2019 11:24:26 -0500 Subject: [PATCH 0869/3170] Add network.download module This will be home to Dowloader, Download, and associated helper functions. Since this is an abstraction over PipSession, it makes sense to keep these functions in a separate module. Also move a helper function here from operations.prepare. --- src/pip/_internal/network/download.py | 16 ++++++++++++++++ src/pip/_internal/operations/prepare.py | 9 +-------- 2 files changed, 17 insertions(+), 8 deletions(-) create mode 100644 src/pip/_internal/network/download.py diff --git a/src/pip/_internal/network/download.py b/src/pip/_internal/network/download.py new file mode 100644 index 00000000000..5825e386226 --- /dev/null +++ b/src/pip/_internal/network/download.py @@ -0,0 +1,16 @@ +"""Download files with progress indicators. +""" +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import Optional + + from pip._vendor.requests.models import Response + + +def _get_http_response_size(resp): + # type: (Response) -> Optional[int] + try: + return int(resp.headers['content-length']) + except (ValueError, KeyError, TypeError): + return None diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 50f15ef7b3e..8810e6f1c13 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -30,6 +30,7 @@ ) from pip._internal.models.index import PyPI from pip._internal.network.cache import is_from_cache +from pip._internal.network.download import _get_http_response_size from pip._internal.network.session import PipSession from pip._internal.network.utils import response_chunks from pip._internal.utils.compat import expanduser @@ -109,14 +110,6 @@ def unpack_vcs_link(link, location): vcs_backend.unpack(location, url=hide_url(link.url)) -def _get_http_response_size(resp): - # type: (Response) -> Optional[int] - try: - return int(resp.headers['content-length']) - except (ValueError, KeyError, TypeError): - return None - - def _prepare_download( resp, # type: Response link, # type: Link From 05b327ca2fcb1f05e544945197d2524ab1d8da72 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 29 Nov 2019 11:37:28 -0500 Subject: [PATCH 0870/3170] Remove Optional from Link.show_url Since we're moving functions to a file with stricter typing, this prevents mypy from complaining that our string is possibly None. --- src/pip/_internal/models/link.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/models/link.py b/src/pip/_internal/models/link.py index ff808f5e9e2..d0445fd54e1 100644 --- a/src/pip/_internal/models/link.py +++ b/src/pip/_internal/models/link.py @@ -180,7 +180,7 @@ def hash_name(self): @property def show_url(self): - # type: () -> Optional[str] + # type: () -> str return posixpath.basename(self._url.split('#', 1)[0].split('?', 1)[0]) @property From 3fbc991f0cbb8de3fe186f87f241f36ddf6ddc0c Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 29 Nov 2019 11:41:14 -0500 Subject: [PATCH 0871/3170] Move _prepare_download to network.download --- src/pip/_internal/network/download.py | 59 ++++++++++++++++++++++++- src/pip/_internal/operations/prepare.py | 54 +--------------------- tests/unit/test_network_download.py | 36 +++++++++++++++ tests/unit/test_operations_prepare.py | 32 -------------- 4 files changed, 96 insertions(+), 85 deletions(-) create mode 100644 tests/unit/test_network_download.py diff --git a/src/pip/_internal/network/download.py b/src/pip/_internal/network/download.py index 5825e386226..d5c6d424f53 100644 --- a/src/pip/_internal/network/download.py +++ b/src/pip/_internal/network/download.py @@ -1,12 +1,25 @@ """Download files with progress indicators. """ +import logging + +from pip._vendor.requests.models import CONTENT_CHUNK_SIZE + +from pip._internal.models.index import PyPI +from pip._internal.network.cache import is_from_cache +from pip._internal.network.utils import response_chunks +from pip._internal.utils.misc import format_size, redact_auth_from_url from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from pip._internal.utils.ui import DownloadProgressProvider if MYPY_CHECK_RUNNING: - from typing import Optional + from typing import Iterable, Optional from pip._vendor.requests.models import Response + from pip._internal.models.link import Link + +logger = logging.getLogger(__name__) + def _get_http_response_size(resp): # type: (Response) -> Optional[int] @@ -14,3 +27,47 @@ def _get_http_response_size(resp): return int(resp.headers['content-length']) except (ValueError, KeyError, TypeError): return None + + +def _prepare_download( + resp, # type: Response + link, # type: Link + progress_bar # type: str +): + # type: (...) -> Iterable[bytes] + total_length = _get_http_response_size(resp) + + if link.netloc == PyPI.file_storage_domain: + url = link.show_url + else: + url = link.url_without_fragment + + logged_url = redact_auth_from_url(url) + + if total_length: + logged_url = '{} ({})'.format(logged_url, format_size(total_length)) + + if is_from_cache(resp): + logger.info("Using cached %s", logged_url) + else: + logger.info("Downloading %s", logged_url) + + if logger.getEffectiveLevel() > logging.INFO: + show_progress = False + elif is_from_cache(resp): + show_progress = False + elif not total_length: + show_progress = True + elif total_length > (40 * 1000): + show_progress = True + else: + show_progress = False + + chunks = response_chunks(resp, CONTENT_CHUNK_SIZE) + + if not show_progress: + return chunks + + return DownloadProgressProvider( + progress_bar, max=total_length + )(chunks) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 8810e6f1c13..ae1cdbd5134 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -13,7 +13,7 @@ import sys from pip._vendor import requests -from pip._vendor.requests.models import CONTENT_CHUNK_SIZE, Response +from pip._vendor.requests.models import Response from pip._vendor.six import PY2 from pip._internal.distributions import ( @@ -28,11 +28,8 @@ PreviousBuildDirError, VcsHashUnsupported, ) -from pip._internal.models.index import PyPI -from pip._internal.network.cache import is_from_cache -from pip._internal.network.download import _get_http_response_size +from pip._internal.network.download import _prepare_download from pip._internal.network.session import PipSession -from pip._internal.network.utils import response_chunks from pip._internal.utils.compat import expanduser from pip._internal.utils.filesystem import copy2_fixed from pip._internal.utils.hashes import MissingHashes @@ -42,17 +39,14 @@ ask_path_exists, backup_dir, display_path, - format_size, hide_url, normalize_path, path_to_display, - redact_auth_from_url, rmtree, splitext, ) from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from pip._internal.utils.ui import DownloadProgressProvider from pip._internal.utils.unpacking import unpack_file from pip._internal.vcs import vcs @@ -110,50 +104,6 @@ def unpack_vcs_link(link, location): vcs_backend.unpack(location, url=hide_url(link.url)) -def _prepare_download( - resp, # type: Response - link, # type: Link - progress_bar # type: str -): - # type: (...) -> Iterable[bytes] - total_length = _get_http_response_size(resp) - - if link.netloc == PyPI.file_storage_domain: - url = link.show_url - else: - url = link.url_without_fragment - - logged_url = redact_auth_from_url(url) - - if total_length: - logged_url = '{} ({})'.format(logged_url, format_size(total_length)) - - if is_from_cache(resp): - logger.info("Using cached %s", logged_url) - else: - logger.info("Downloading %s", logged_url) - - if logger.getEffectiveLevel() > logging.INFO: - show_progress = False - elif is_from_cache(resp): - show_progress = False - elif not total_length: - show_progress = True - elif total_length > (40 * 1000): - show_progress = True - else: - show_progress = False - - chunks = response_chunks(resp, CONTENT_CHUNK_SIZE) - - if not show_progress: - return chunks - - return DownloadProgressProvider( - progress_bar, max=total_length - )(chunks) - - def _copy_file(filename, location, link): copy = True download_location = os.path.join(location, link.filename) diff --git a/tests/unit/test_network_download.py b/tests/unit/test_network_download.py new file mode 100644 index 00000000000..352d657f717 --- /dev/null +++ b/tests/unit/test_network_download.py @@ -0,0 +1,36 @@ +import pytest +import logging + +from pip._internal.models.link import Link +from pip._internal.network.download import _prepare_download +from tests.lib.requests_mocks import MockResponse + + +@pytest.mark.parametrize("url, headers, from_cache, expected", [ + ('http://example.com/foo.tgz', {}, False, + "Downloading http://example.com/foo.tgz"), + ('http://example.com/foo.tgz', {'content-length': 2}, False, + "Downloading http://example.com/foo.tgz (2 bytes)"), + ('http://example.com/foo.tgz', {'content-length': 2}, True, + "Using cached http://example.com/foo.tgz (2 bytes)"), + ('https://files.pythonhosted.org/foo.tgz', {}, False, + "Downloading foo.tgz"), + ('https://files.pythonhosted.org/foo.tgz', {'content-length': 2}, False, + "Downloading foo.tgz (2 bytes)"), + ('https://files.pythonhosted.org/foo.tgz', {'content-length': 2}, True, + "Using cached foo.tgz"), +]) +def test_prepare_download__log(caplog, url, headers, from_cache, expected): + caplog.set_level(logging.INFO) + resp = MockResponse(b'') + resp.url = url + resp.headers = headers + if from_cache: + resp.from_cache = from_cache + link = Link(url) + _prepare_download(resp, link, progress_bar="on") + + assert len(caplog.records) == 1 + record = caplog.records[0] + assert record.levelname == 'INFO' + assert expected in record.message diff --git a/tests/unit/test_operations_prepare.py b/tests/unit/test_operations_prepare.py index 3d5d0fef116..7d736dc2eed 100644 --- a/tests/unit/test_operations_prepare.py +++ b/tests/unit/test_operations_prepare.py @@ -1,5 +1,4 @@ import hashlib -import logging import os import shutil import sys @@ -16,7 +15,6 @@ Downloader, _copy_source_tree, _download_http_url, - _prepare_download, parse_content_disposition, sanitize_content_filename, unpack_file_url, @@ -194,36 +192,6 @@ def test_download_http_url__no_directory_traversal(tmpdir): assert actual == ['out_dir_file'] -@pytest.mark.parametrize("url, headers, from_cache, expected", [ - ('http://example.com/foo.tgz', {}, False, - "Downloading http://example.com/foo.tgz"), - ('http://example.com/foo.tgz', {'content-length': 2}, False, - "Downloading http://example.com/foo.tgz (2 bytes)"), - ('http://example.com/foo.tgz', {'content-length': 2}, True, - "Using cached http://example.com/foo.tgz (2 bytes)"), - ('https://files.pythonhosted.org/foo.tgz', {}, False, - "Downloading foo.tgz"), - ('https://files.pythonhosted.org/foo.tgz', {'content-length': 2}, False, - "Downloading foo.tgz (2 bytes)"), - ('https://files.pythonhosted.org/foo.tgz', {'content-length': 2}, True, - "Using cached foo.tgz"), -]) -def test_prepare_download__log(caplog, url, headers, from_cache, expected): - caplog.set_level(logging.INFO) - resp = MockResponse(b'') - resp.url = url - resp.headers = headers - if from_cache: - resp.from_cache = from_cache - link = Link(url) - _prepare_download(resp, link, progress_bar="on") - - assert len(caplog.records) == 1 - record = caplog.records[0] - assert record.levelname == 'INFO' - assert expected in record.message - - @pytest.fixture def clean_project(tmpdir_factory, data): tmpdir = Path(str(tmpdir_factory.mktemp("clean_project"))) From 32b0fc23ab55f2a3211d696479729e04676c1b0a Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 29 Nov 2019 11:43:40 -0500 Subject: [PATCH 0872/3170] Move sanitize_content_filename to network.download --- src/pip/_internal/network/download.py | 9 +++++ src/pip/_internal/operations/prepare.py | 13 +++---- tests/unit/test_network_download.py | 47 +++++++++++++++++++++++-- tests/unit/test_operations_prepare.py | 40 --------------------- 4 files changed, 58 insertions(+), 51 deletions(-) diff --git a/src/pip/_internal/network/download.py b/src/pip/_internal/network/download.py index d5c6d424f53..f0ef2f86388 100644 --- a/src/pip/_internal/network/download.py +++ b/src/pip/_internal/network/download.py @@ -1,6 +1,7 @@ """Download files with progress indicators. """ import logging +import os from pip._vendor.requests.models import CONTENT_CHUNK_SIZE @@ -71,3 +72,11 @@ def _prepare_download( return DownloadProgressProvider( progress_bar, max=total_length )(chunks) + + +def sanitize_content_filename(filename): + # type: (str) -> str + """ + Sanitize the "filename" value from a Content-Disposition header. + """ + return os.path.basename(filename) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index ae1cdbd5134..2048ae51011 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -28,7 +28,10 @@ PreviousBuildDirError, VcsHashUnsupported, ) -from pip._internal.network.download import _prepare_download +from pip._internal.network.download import ( + _prepare_download, + sanitize_content_filename, +) from pip._internal.network.session import PipSession from pip._internal.utils.compat import expanduser from pip._internal.utils.filesystem import copy2_fixed @@ -305,14 +308,6 @@ def unpack_url( ) -def sanitize_content_filename(filename): - # type: (str) -> str - """ - Sanitize the "filename" value from a Content-Disposition header. - """ - return os.path.basename(filename) - - def parse_content_disposition(content_disposition, default_filename): # type: (str, str) -> str """ diff --git a/tests/unit/test_network_download.py b/tests/unit/test_network_download.py index 352d657f717..87fe7e5072f 100644 --- a/tests/unit/test_network_download.py +++ b/tests/unit/test_network_download.py @@ -1,8 +1,13 @@ -import pytest import logging +import sys + +import pytest from pip._internal.models.link import Link -from pip._internal.network.download import _prepare_download +from pip._internal.network.download import ( + _prepare_download, + sanitize_content_filename, +) from tests.lib.requests_mocks import MockResponse @@ -34,3 +39,41 @@ def test_prepare_download__log(caplog, url, headers, from_cache, expected): record = caplog.records[0] assert record.levelname == 'INFO' assert expected in record.message + + +@pytest.mark.parametrize("filename, expected", [ + ('dir/file', 'file'), + ('../file', 'file'), + ('../../file', 'file'), + ('../', ''), + ('../..', '..'), + ('/', ''), +]) +def test_sanitize_content_filename(filename, expected): + """ + Test inputs where the result is the same for Windows and non-Windows. + """ + assert sanitize_content_filename(filename) == expected + + +@pytest.mark.parametrize("filename, win_expected, non_win_expected", [ + ('dir\\file', 'file', 'dir\\file'), + ('..\\file', 'file', '..\\file'), + ('..\\..\\file', 'file', '..\\..\\file'), + ('..\\', '', '..\\'), + ('..\\..', '..', '..\\..'), + ('\\', '', '\\'), +]) +def test_sanitize_content_filename__platform_dependent( + filename, + win_expected, + non_win_expected +): + """ + Test inputs where the result is different for Windows and non-Windows. + """ + if sys.platform == 'win32': + expected = win_expected + else: + expected = non_win_expected + assert sanitize_content_filename(filename) == expected diff --git a/tests/unit/test_operations_prepare.py b/tests/unit/test_operations_prepare.py index 7d736dc2eed..bef7792c9d6 100644 --- a/tests/unit/test_operations_prepare.py +++ b/tests/unit/test_operations_prepare.py @@ -1,7 +1,6 @@ import hashlib import os import shutil -import sys from shutil import copy, rmtree from tempfile import mkdtemp @@ -16,7 +15,6 @@ _copy_source_tree, _download_http_url, parse_content_disposition, - sanitize_content_filename, unpack_file_url, unpack_http_url, ) @@ -108,44 +106,6 @@ def test_unpack_http_url_bad_downloaded_checksum(mock_unpack_file): rmtree(download_dir) -@pytest.mark.parametrize("filename, expected", [ - ('dir/file', 'file'), - ('../file', 'file'), - ('../../file', 'file'), - ('../', ''), - ('../..', '..'), - ('/', ''), -]) -def test_sanitize_content_filename(filename, expected): - """ - Test inputs where the result is the same for Windows and non-Windows. - """ - assert sanitize_content_filename(filename) == expected - - -@pytest.mark.parametrize("filename, win_expected, non_win_expected", [ - ('dir\\file', 'file', 'dir\\file'), - ('..\\file', 'file', '..\\file'), - ('..\\..\\file', 'file', '..\\..\\file'), - ('..\\', '', '..\\'), - ('..\\..', '..', '..\\..'), - ('\\', '', '\\'), -]) -def test_sanitize_content_filename__platform_dependent( - filename, - win_expected, - non_win_expected -): - """ - Test inputs where the result is different for Windows and non-Windows. - """ - if sys.platform == 'win32': - expected = win_expected - else: - expected = non_win_expected - assert sanitize_content_filename(filename) == expected - - @pytest.mark.parametrize("content_disposition, default_filename, expected", [ ('attachment;filename="../file"', 'df', 'file'), ]) From 762e4a08170e1e60e748d332d77366e08109e21e Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 29 Nov 2019 11:48:40 -0500 Subject: [PATCH 0873/3170] Move parse_content_disposition to network.download --- src/pip/_internal/network/download.py | 16 ++++++++++++++++ src/pip/_internal/operations/prepare.py | 18 +----------------- tests/unit/test_network_download.py | 13 +++++++++++++ tests/unit/test_operations_prepare.py | 13 ------------- 4 files changed, 30 insertions(+), 30 deletions(-) diff --git a/src/pip/_internal/network/download.py b/src/pip/_internal/network/download.py index f0ef2f86388..a1c4bc79247 100644 --- a/src/pip/_internal/network/download.py +++ b/src/pip/_internal/network/download.py @@ -1,5 +1,6 @@ """Download files with progress indicators. """ +import cgi import logging import os @@ -80,3 +81,18 @@ def sanitize_content_filename(filename): Sanitize the "filename" value from a Content-Disposition header. """ return os.path.basename(filename) + + +def parse_content_disposition(content_disposition, default_filename): + # type: (str, str) -> str + """ + Parse the "filename" value from a Content-Disposition header, and + return the default filename if the result is empty. + """ + _type, params = cgi.parse_header(content_disposition) + filename = params.get('filename') + if filename: + # We need to sanitize the filename to prevent directory traversal + # in case the filename contains ".." path parts. + filename = sanitize_content_filename(filename) + return filename or default_filename diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 2048ae51011..96c702a66c2 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -5,7 +5,6 @@ # mypy: strict-optional=False # mypy: disallow-untyped-defs=False -import cgi import logging import mimetypes import os @@ -30,7 +29,7 @@ ) from pip._internal.network.download import ( _prepare_download, - sanitize_content_filename, + parse_content_disposition, ) from pip._internal.network.session import PipSession from pip._internal.utils.compat import expanduser @@ -308,21 +307,6 @@ def unpack_url( ) -def parse_content_disposition(content_disposition, default_filename): - # type: (str, str) -> str - """ - Parse the "filename" value from a Content-Disposition header, and - return the default filename if the result is empty. - """ - _type, params = cgi.parse_header(content_disposition) - filename = params.get('filename') - if filename: - # We need to sanitize the filename to prevent directory traversal - # in case the filename contains ".." path parts. - filename = sanitize_content_filename(filename) - return filename or default_filename - - def _get_http_response_filename(resp, link): # type: (Response, Link) -> str """Get an ideal filename from the given HTTP response, falling back to diff --git a/tests/unit/test_network_download.py b/tests/unit/test_network_download.py index 87fe7e5072f..20f5513a2df 100644 --- a/tests/unit/test_network_download.py +++ b/tests/unit/test_network_download.py @@ -6,6 +6,7 @@ from pip._internal.models.link import Link from pip._internal.network.download import ( _prepare_download, + parse_content_disposition, sanitize_content_filename, ) from tests.lib.requests_mocks import MockResponse @@ -77,3 +78,15 @@ def test_sanitize_content_filename__platform_dependent( else: expected = non_win_expected assert sanitize_content_filename(filename) == expected + + +@pytest.mark.parametrize("content_disposition, default_filename, expected", [ + ('attachment;filename="../file"', 'df', 'file'), +]) +def test_parse_content_disposition( + content_disposition, + default_filename, + expected +): + actual = parse_content_disposition(content_disposition, default_filename) + assert actual == expected diff --git a/tests/unit/test_operations_prepare.py b/tests/unit/test_operations_prepare.py index bef7792c9d6..7845e6ce116 100644 --- a/tests/unit/test_operations_prepare.py +++ b/tests/unit/test_operations_prepare.py @@ -14,7 +14,6 @@ Downloader, _copy_source_tree, _download_http_url, - parse_content_disposition, unpack_file_url, unpack_http_url, ) @@ -106,18 +105,6 @@ def test_unpack_http_url_bad_downloaded_checksum(mock_unpack_file): rmtree(download_dir) -@pytest.mark.parametrize("content_disposition, default_filename, expected", [ - ('attachment;filename="../file"', 'df', 'file'), -]) -def test_parse_content_disposition( - content_disposition, - default_filename, - expected -): - actual = parse_content_disposition(content_disposition, default_filename) - assert actual == expected - - def test_download_http_url__no_directory_traversal(tmpdir): """ Test that directory traversal doesn't happen on download when the From e354b728dc4aa42ef3ec31528d2a99fc3b82a6dd Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 29 Nov 2019 11:51:42 -0500 Subject: [PATCH 0874/3170] Move _get_http_response_filename to network.download --- src/pip/_internal/network/download.py | 31 ++++++++++++++++++++++++- src/pip/_internal/operations/prepare.py | 27 +-------------------- 2 files changed, 31 insertions(+), 27 deletions(-) diff --git a/src/pip/_internal/network/download.py b/src/pip/_internal/network/download.py index a1c4bc79247..d9ba1d17765 100644 --- a/src/pip/_internal/network/download.py +++ b/src/pip/_internal/network/download.py @@ -2,6 +2,7 @@ """ import cgi import logging +import mimetypes import os from pip._vendor.requests.models import CONTENT_CHUNK_SIZE @@ -9,7 +10,11 @@ from pip._internal.models.index import PyPI from pip._internal.network.cache import is_from_cache from pip._internal.network.utils import response_chunks -from pip._internal.utils.misc import format_size, redact_auth_from_url +from pip._internal.utils.misc import ( + format_size, + redact_auth_from_url, + splitext, +) from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.ui import DownloadProgressProvider @@ -96,3 +101,27 @@ def parse_content_disposition(content_disposition, default_filename): # in case the filename contains ".." path parts. filename = sanitize_content_filename(filename) return filename or default_filename + + +def _get_http_response_filename(resp, link): + # type: (Response, Link) -> str + """Get an ideal filename from the given HTTP response, falling back to + the link filename if not provided. + """ + filename = link.filename # fallback + # Have a look at the Content-Disposition header for a better guess + content_disposition = resp.headers.get('content-disposition') + if content_disposition: + filename = parse_content_disposition(content_disposition, filename) + ext = splitext(filename)[1] # type: Optional[str] + if not ext: + ext = mimetypes.guess_extension( + resp.headers.get('content-type', '') + ) + if ext: + filename += ext + if not ext and link.url != resp.url: + ext = os.path.splitext(resp.url)[1] + if ext: + filename += ext + return filename diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 96c702a66c2..e0d03d0aeaa 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -28,8 +28,8 @@ VcsHashUnsupported, ) from pip._internal.network.download import ( + _get_http_response_filename, _prepare_download, - parse_content_disposition, ) from pip._internal.network.session import PipSession from pip._internal.utils.compat import expanduser @@ -45,7 +45,6 @@ normalize_path, path_to_display, rmtree, - splitext, ) from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -307,30 +306,6 @@ def unpack_url( ) -def _get_http_response_filename(resp, link): - # type: (Response, Link) -> str - """Get an ideal filename from the given HTTP response, falling back to - the link filename if not provided. - """ - filename = link.filename # fallback - # Have a look at the Content-Disposition header for a better guess - content_disposition = resp.headers.get('content-disposition') - if content_disposition: - filename = parse_content_disposition(content_disposition, filename) - ext = splitext(filename)[1] # type: Optional[str] - if not ext: - ext = mimetypes.guess_extension( - resp.headers.get('content-type', '') - ) - if ext: - filename += ext - if not ext and link.url != resp.url: - ext = os.path.splitext(resp.url)[1] - if ext: - filename += ext - return filename - - def _http_get_download(session, link): # type: (PipSession, Link) -> Response target_url = link.url.split('#', 1)[0] From 3a2ff979def45baeaca58128b0f93946a380d097 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 29 Nov 2019 11:53:59 -0500 Subject: [PATCH 0875/3170] Move _http_get_download to network.download --- src/pip/_internal/network/download.py | 32 +++++++++++++++++++++++ src/pip/_internal/operations/prepare.py | 34 ++----------------------- 2 files changed, 34 insertions(+), 32 deletions(-) diff --git a/src/pip/_internal/network/download.py b/src/pip/_internal/network/download.py index d9ba1d17765..2fe717b8231 100644 --- a/src/pip/_internal/network/download.py +++ b/src/pip/_internal/network/download.py @@ -24,6 +24,7 @@ from pip._vendor.requests.models import Response from pip._internal.models.link import Link + from pip._internal.network.session import PipSession logger = logging.getLogger(__name__) @@ -125,3 +126,34 @@ def _get_http_response_filename(resp, link): if ext: filename += ext return filename + + +def _http_get_download(session, link): + # type: (PipSession, Link) -> Response + target_url = link.url.split('#', 1)[0] + resp = session.get( + target_url, + # We use Accept-Encoding: identity here because requests + # defaults to accepting compressed responses. This breaks in + # a variety of ways depending on how the server is configured. + # - Some servers will notice that the file isn't a compressible + # file and will leave the file alone and with an empty + # Content-Encoding + # - Some servers will notice that the file is already + # compressed and will leave the file alone and will add a + # Content-Encoding: gzip header + # - Some servers won't notice anything at all and will take + # a file that's already been compressed and compress it again + # and set the Content-Encoding: gzip header + # By setting this to request only the identity encoding We're + # hoping to eliminate the third case. Hopefully there does not + # exist a server which when given a file will notice it is + # already compressed and that you're not asking for a + # compressed file and will then decompress it before sending + # because if that's the case I don't think it'll ever be + # possible to make this work. + headers={"Accept-Encoding": "identity"}, + stream=True, + ) + resp.raise_for_status() + return resp diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index e0d03d0aeaa..d9e5ace6f57 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -29,9 +29,9 @@ ) from pip._internal.network.download import ( _get_http_response_filename, + _http_get_download, _prepare_download, ) -from pip._internal.network.session import PipSession from pip._internal.utils.compat import expanduser from pip._internal.utils.filesystem import copy2_fixed from pip._internal.utils.hashes import MissingHashes @@ -61,6 +61,7 @@ from pip._internal.distributions import AbstractDistribution from pip._internal.index.package_finder import PackageFinder from pip._internal.models.link import Link + from pip._internal.network.session import PipSession from pip._internal.req.req_install import InstallRequirement from pip._internal.req.req_tracker import RequirementTracker from pip._internal.utils.hashes import Hashes @@ -306,37 +307,6 @@ def unpack_url( ) -def _http_get_download(session, link): - # type: (PipSession, Link) -> Response - target_url = link.url.split('#', 1)[0] - resp = session.get( - target_url, - # We use Accept-Encoding: identity here because requests - # defaults to accepting compressed responses. This breaks in - # a variety of ways depending on how the server is configured. - # - Some servers will notice that the file isn't a compressible - # file and will leave the file alone and with an empty - # Content-Encoding - # - Some servers will notice that the file is already - # compressed and will leave the file alone and will add a - # Content-Encoding: gzip header - # - Some servers won't notice anything at all and will take - # a file that's already been compressed and compress it again - # and set the Content-Encoding: gzip header - # By setting this to request only the identity encoding We're - # hoping to eliminate the third case. Hopefully there does not - # exist a server which when given a file will notice it is - # already compressed and that you're not asking for a - # compressed file and will then decompress it before sending - # because if that's the case I don't think it'll ever be - # possible to make this work. - headers={"Accept-Encoding": "identity"}, - stream=True, - ) - resp.raise_for_status() - return resp - - class Download(object): def __init__( self, From 78a221cf71d060109dadf085962dae72808bc964 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 29 Nov 2019 11:56:18 -0500 Subject: [PATCH 0876/3170] Move Downloader to network.download This will help us move Downloader construction out of RequirementPreparer, reducing its concerns and making it easier to test in isolation. --- src/pip/_internal/network/download.py | 41 +++++++++++++++++++++ src/pip/_internal/operations/prepare.py | 49 +------------------------ 2 files changed, 43 insertions(+), 47 deletions(-) diff --git a/src/pip/_internal/network/download.py b/src/pip/_internal/network/download.py index 2fe717b8231..c90c4bf42cf 100644 --- a/src/pip/_internal/network/download.py +++ b/src/pip/_internal/network/download.py @@ -5,6 +5,7 @@ import mimetypes import os +from pip._vendor import requests from pip._vendor.requests.models import CONTENT_CHUNK_SIZE from pip._internal.models.index import PyPI @@ -157,3 +158,43 @@ def _http_get_download(session, link): ) resp.raise_for_status() return resp + + +class Download(object): + def __init__( + self, + response, # type: Response + filename, # type: str + chunks, # type: Iterable[bytes] + ): + # type: (...) -> None + self.response = response + self.filename = filename + self.chunks = chunks + + +class Downloader(object): + def __init__( + self, + session, # type: PipSession + progress_bar, # type: str + ): + # type: (...) -> None + self._session = session + self._progress_bar = progress_bar + + def __call__(self, link): + # type: (Link) -> Download + try: + resp = _http_get_download(self._session, link) + except requests.HTTPError as e: + logger.critical( + "HTTP error %s while getting %s", e.response.status_code, link + ) + raise + + return Download( + resp, + _get_http_response_filename(resp, link), + _prepare_download(resp, link, self._progress_bar), + ) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index d9e5ace6f57..6a7ae749bc4 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -12,7 +12,6 @@ import sys from pip._vendor import requests -from pip._vendor.requests.models import Response from pip._vendor.six import PY2 from pip._internal.distributions import ( @@ -27,11 +26,7 @@ PreviousBuildDirError, VcsHashUnsupported, ) -from pip._internal.network.download import ( - _get_http_response_filename, - _http_get_download, - _prepare_download, -) +from pip._internal.network.download import Downloader from pip._internal.utils.compat import expanduser from pip._internal.utils.filesystem import copy2_fixed from pip._internal.utils.hashes import MissingHashes @@ -53,7 +48,7 @@ if MYPY_CHECK_RUNNING: from typing import ( - Callable, Iterable, List, Optional, Tuple, + Callable, List, Optional, Tuple, ) from mypy_extensions import TypedDict @@ -307,46 +302,6 @@ def unpack_url( ) -class Download(object): - def __init__( - self, - response, # type: Response - filename, # type: str - chunks, # type: Iterable[bytes] - ): - # type: (...) -> None - self.response = response - self.filename = filename - self.chunks = chunks - - -class Downloader(object): - def __init__( - self, - session, # type: PipSession - progress_bar, # type: str - ): - # type: (...) -> None - self._session = session - self._progress_bar = progress_bar - - def __call__(self, link): - # type: (Link) -> Download - try: - resp = _http_get_download(self._session, link) - except requests.HTTPError as e: - logger.critical( - "HTTP error %s while getting %s", e.response.status_code, link - ) - raise - - return Download( - resp, - _get_http_response_filename(resp, link), - _prepare_download(resp, link, self._progress_bar), - ) - - def _download_http_url( link, # type: Link downloader, # type: Downloader From a4fc8d0e7e91ff9da3e86b35a032fe833f91d0ee Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 4 Dec 2019 22:03:10 -0500 Subject: [PATCH 0877/3170] Parameterize InstallRequirement.install_editable This makes it easier to extract the function without keeping references to InstallRequirement. --- src/pip/_internal/req/req_install.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index ee12a4de6cd..d5713ffdacc 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -625,25 +625,30 @@ def install_editable( prefix, # type: Optional[str] home, # type: Optional[str] use_user_site, # type: bool + name, # type: str + setup_py_path, # type: str + isolated, # type: bool + build_env, # type: BuildEnvironment + unpacked_source_directory, # type: str ): # type: (...) -> None - logger.info('Running setup.py develop for %s', self.name) + logger.info('Running setup.py develop for %s', name) args = make_setuptools_develop_args( - self.setup_py_path, + setup_py_path, global_options=global_options, install_options=install_options, - no_user_config=self.isolated, + no_user_config=isolated, prefix=prefix, home=home, use_user_site=use_user_site, ) with indent_log(): - with self.build_env: + with build_env: call_subprocess( args, - cwd=self.unpacked_source_directory, + cwd=unpacked_source_directory, ) def update_editable(self, obtain=True): @@ -808,6 +813,11 @@ def install( prefix=prefix, home=home, use_user_site=use_user_site, + name=self.name, + setup_py_path=self.setup_py_path, + isolated=self.isolated, + build_env=self.build_env, + unpacked_source_directory=self.unpacked_source_directory, ) self.install_succeeded = True return From 83b2acf032d3feff6ea742068d786c6bd5544403 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 4 Dec 2019 22:13:20 -0500 Subject: [PATCH 0878/3170] Move InstallRequirement.install_editable into operations.install This makes InstallRequirement simpler and overall makes it easier to track how the parts of InstallRequirement are being used for the phases of package processing. --- .../operations/install/editable_legacy.py | 47 +++++++++++++++++++ src/pip/_internal/req/req_install.py | 47 ++----------------- 2 files changed, 52 insertions(+), 42 deletions(-) create mode 100644 src/pip/_internal/operations/install/editable_legacy.py diff --git a/src/pip/_internal/operations/install/editable_legacy.py b/src/pip/_internal/operations/install/editable_legacy.py new file mode 100644 index 00000000000..1a20a279feb --- /dev/null +++ b/src/pip/_internal/operations/install/editable_legacy.py @@ -0,0 +1,47 @@ +import logging + +from pip._internal.utils.logging import indent_log +from pip._internal.utils.setuptools_build import make_setuptools_develop_args +from pip._internal.utils.subprocess import call_subprocess +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import List, Optional, Sequence + + from pip._internal.build_env import BuildEnvironment + + +logger = logging.getLogger(__name__) + + +def install( + install_options, # type: List[str] + global_options, # type: Sequence[str] + prefix, # type: Optional[str] + home, # type: Optional[str] + use_user_site, # type: bool + name, # type: str + setup_py_path, # type: str + isolated, # type: bool + build_env, # type: BuildEnvironment + unpacked_source_directory, # type: str +): + # type: (...) -> None + logger.info('Running setup.py develop for %s', name) + + args = make_setuptools_develop_args( + setup_py_path, + global_options=global_options, + install_options=install_options, + no_user_config=isolated, + prefix=prefix, + home=home, + use_user_site=use_user_site, + ) + + with indent_log(): + with build_env: + call_subprocess( + args, + cwd=unpacked_source_directory, + ) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index d5713ffdacc..3a76ef3aec7 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -25,6 +25,8 @@ from pip._internal.operations.build.metadata import generate_metadata from pip._internal.operations.build.metadata_legacy import \ generate_metadata as generate_metadata_legacy +from pip._internal.operations.install.editable_legacy import \ + install as install_editable_legacy from pip._internal.operations.install.wheel import install_unpacked_wheel from pip._internal.pyproject import load_pyproject_toml, make_pyproject_path from pip._internal.req.req_uninstall import UninstallPathSet @@ -49,14 +51,8 @@ rmtree, ) from pip._internal.utils.packaging import get_metadata -from pip._internal.utils.setuptools_build import ( - make_setuptools_develop_args, - make_setuptools_install_args, -) -from pip._internal.utils.subprocess import ( - call_subprocess, - runner_with_spinner_message, -) +from pip._internal.utils.setuptools_build import make_setuptools_install_args +from pip._internal.utils.subprocess import runner_with_spinner_message from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.virtualenv import running_under_virtualenv @@ -618,39 +614,6 @@ def ensure_has_source_dir(self, parent_dir): self.source_dir = self.ensure_build_location(parent_dir) # For editable installations - def install_editable( - self, - install_options, # type: List[str] - global_options, # type: Sequence[str] - prefix, # type: Optional[str] - home, # type: Optional[str] - use_user_site, # type: bool - name, # type: str - setup_py_path, # type: str - isolated, # type: bool - build_env, # type: BuildEnvironment - unpacked_source_directory, # type: str - ): - # type: (...) -> None - logger.info('Running setup.py develop for %s', name) - - args = make_setuptools_develop_args( - setup_py_path, - global_options=global_options, - install_options=install_options, - no_user_config=isolated, - prefix=prefix, - home=home, - use_user_site=use_user_site, - ) - - with indent_log(): - with build_env: - call_subprocess( - args, - cwd=unpacked_source_directory, - ) - def update_editable(self, obtain=True): # type: (bool) -> None if not self.link: @@ -807,7 +770,7 @@ def install( global_options = global_options if global_options is not None else [] if self.editable: - self.install_editable( + install_editable_legacy( install_options, global_options, prefix=prefix, From 27fee83065a775690986ad5b9406839d30d8dba1 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 4 Dec 2019 22:20:04 -0500 Subject: [PATCH 0879/3170] Add docstrings --- src/pip/_internal/operations/install/editable_legacy.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/pip/_internal/operations/install/editable_legacy.py b/src/pip/_internal/operations/install/editable_legacy.py index 1a20a279feb..b3124a8b013 100644 --- a/src/pip/_internal/operations/install/editable_legacy.py +++ b/src/pip/_internal/operations/install/editable_legacy.py @@ -1,3 +1,5 @@ +"""Legacy installation process, i.e. `setup.py develop`. +""" import logging from pip._internal.utils.logging import indent_log @@ -27,6 +29,9 @@ def install( unpacked_source_directory, # type: str ): # type: (...) -> None + """Install a package in editable mode. Most arguments are pass-through + to setuptools. + """ logger.info('Running setup.py develop for %s', name) args = make_setuptools_develop_args( From 331716a4392c50a608e662e89c69e9a22c149d13 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 4 Dec 2019 22:43:33 -0500 Subject: [PATCH 0880/3170] Don't use conflicts_with in logging InstallRequirement.uninstall doesn't actually use conflicts_with, so don't log it. Instead we output the requirement name so we still have something that looks like a heading before indenting the log, and log what we actually uninstall inside the function. This will also enable us to get rid of conflicts_with. --- src/pip/_internal/req/__init__.py | 5 +---- src/pip/_internal/req/req_install.py | 2 ++ 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/req/__init__.py b/src/pip/_internal/req/__init__.py index e5176dae5e4..657ee0cb0a4 100644 --- a/src/pip/_internal/req/__init__.py +++ b/src/pip/_internal/req/__init__.py @@ -58,10 +58,7 @@ def install_given_reqs( with indent_log(): for requirement in to_install: if requirement.conflicts_with: - logger.info( - 'Found existing installation: %s', - requirement.conflicts_with, - ) + logger.info('Attempting uninstall: %s', requirement.name) with indent_log(): uninstalled_pathset = requirement.uninstall( auto_confirm=True diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index ee12a4de6cd..81e01bbdcbd 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -695,6 +695,8 @@ def uninstall(self, auto_confirm=False, verbose=False): except pkg_resources.DistributionNotFound: logger.warning("Skipping %s as it is not installed.", self.name) return None + else: + logger.info('Found existing installation: %s', dist) uninstalled_pathset = UninstallPathSet.from_dist(dist) uninstalled_pathset.remove(auto_confirm, verbose) From d098f27d3fe90d46841f4f58f8533357c039f923 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 4 Dec 2019 22:53:58 -0500 Subject: [PATCH 0881/3170] Use should_reinstall flag instead of conflicts_with A boolean flag is simpler to reason about than a complex type like `conflicts_with`. In all of these situations `conflicts_with` was being assigned a non-None value and only being checked for truthyness, so a bool is sufficient to capture the required usages. --- src/pip/_internal/legacy_resolve.py | 1 + src/pip/_internal/req/__init__.py | 6 +++--- src/pip/_internal/req/req_install.py | 6 ++++++ 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/legacy_resolve.py b/src/pip/_internal/legacy_resolve.py index d94447f8798..4939309c3fb 100644 --- a/src/pip/_internal/legacy_resolve.py +++ b/src/pip/_internal/legacy_resolve.py @@ -209,6 +209,7 @@ def _set_req_to_reinstall(self, req): # conflict is not a user install. if not self.use_user_site or dist_in_usersite(req.satisfied_by): req.conflicts_with = req.satisfied_by + req.should_reinstall = True req.satisfied_by = None def _check_skip_installed(self, req_to_install): diff --git a/src/pip/_internal/req/__init__.py b/src/pip/_internal/req/__init__.py index 657ee0cb0a4..827b6cbc229 100644 --- a/src/pip/_internal/req/__init__.py +++ b/src/pip/_internal/req/__init__.py @@ -57,7 +57,7 @@ def install_given_reqs( with indent_log(): for requirement in to_install: - if requirement.conflicts_with: + if requirement.should_reinstall: logger.info('Attempting uninstall: %s', requirement.name) with indent_log(): uninstalled_pathset = requirement.uninstall( @@ -72,7 +72,7 @@ def install_given_reqs( ) except Exception: should_rollback = ( - requirement.conflicts_with and + requirement.should_reinstall and not requirement.install_succeeded ) # if install did not succeed, rollback previous uninstall @@ -81,7 +81,7 @@ def install_given_reqs( raise else: should_commit = ( - requirement.conflicts_with and + requirement.should_reinstall and requirement.install_succeeded ) if should_commit: diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 81e01bbdcbd..435d1e8e49d 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -161,6 +161,9 @@ def __init__( # This hold the pkg_resources.Distribution object if this requirement # conflicts with another installed distribution: self.conflicts_with = None + # Whether the installation process should try to uninstall an existing + # distribution before installing this requirement. + self.should_reinstall = False # Temporary build location self._temp_build_dir = None # type: Optional[TempDirectory] # Set to True after successful installation @@ -453,6 +456,7 @@ def check_if_exists(self, use_user_site): if use_user_site: if dist_in_usersite(existing_dist): self.conflicts_with = existing_dist + self.should_reinstall = True elif (running_under_virtualenv() and dist_in_site_packages(existing_dist)): raise InstallationError( @@ -462,9 +466,11 @@ def check_if_exists(self, use_user_site): ) else: self.conflicts_with = existing_dist + self.should_reinstall = True else: if self.editable and self.satisfied_by: self.conflicts_with = self.satisfied_by + self.should_reinstall = True # when installing editables, nothing pre-existing should ever # satisfy self.satisfied_by = None From ce42b132ba0c5ad4d6a5681b09d605e729536377 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 4 Dec 2019 22:57:05 -0500 Subject: [PATCH 0882/3170] Remove unused member InstallRequirement.conflicts_with --- src/pip/_internal/legacy_resolve.py | 1 - src/pip/_internal/req/req_install.py | 8 +------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/src/pip/_internal/legacy_resolve.py b/src/pip/_internal/legacy_resolve.py index 4939309c3fb..3fa93d3dced 100644 --- a/src/pip/_internal/legacy_resolve.py +++ b/src/pip/_internal/legacy_resolve.py @@ -208,7 +208,6 @@ def _set_req_to_reinstall(self, req): # Don't uninstall the conflict if doing a user install and the # conflict is not a user install. if not self.use_user_site or dist_in_usersite(req.satisfied_by): - req.conflicts_with = req.satisfied_by req.should_reinstall = True req.satisfied_by = None diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 435d1e8e49d..f6c9474fa99 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -158,9 +158,6 @@ def __init__( # This holds the pkg_resources.Distribution object if this requirement # is already available: self.satisfied_by = None - # This hold the pkg_resources.Distribution object if this requirement - # conflicts with another installed distribution: - self.conflicts_with = None # Whether the installation process should try to uninstall an existing # distribution before installing this requirement. self.should_reinstall = False @@ -435,7 +432,7 @@ def check_if_exists(self, use_user_site): # type: (bool) -> None """Find an installed distribution that satisfies or conflicts with this requirement, and set self.satisfied_by or - self.conflicts_with appropriately. + self.should_reinstall appropriately. """ if self.req is None: return @@ -455,7 +452,6 @@ def check_if_exists(self, use_user_site): ) if use_user_site: if dist_in_usersite(existing_dist): - self.conflicts_with = existing_dist self.should_reinstall = True elif (running_under_virtualenv() and dist_in_site_packages(existing_dist)): @@ -465,11 +461,9 @@ def check_if_exists(self, use_user_site): (existing_dist.project_name, existing_dist.location) ) else: - self.conflicts_with = existing_dist self.should_reinstall = True else: if self.editable and self.satisfied_by: - self.conflicts_with = self.satisfied_by self.should_reinstall = True # when installing editables, nothing pre-existing should ever # satisfy From 48ddd0b1115916b8fc38982dbc94364d48192082 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 4 Dec 2019 21:52:13 -0500 Subject: [PATCH 0883/3170] Inline InstallRequirement.move_wheel_files This makes InstallRequirement simpler and will make it easier to refactor InstallRequirement.install. --- src/pip/_internal/req/req_install.py | 24 ++++-------------------- 1 file changed, 4 insertions(+), 20 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index ee12a4de6cd..1a733675b9c 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -69,7 +69,6 @@ from pip._internal.build_env import BuildEnvironment from pip._internal.cache import WheelCache from pip._internal.index.package_finder import PackageFinder - from pip._internal.models.scheme import Scheme from pip._vendor.pkg_resources import Distribution from pip._vendor.packaging.specifiers import SpecifierSet from pip._vendor.packaging.markers import Marker @@ -477,23 +476,6 @@ def is_wheel(self): return False return self.link.is_wheel - def move_wheel_files( - self, - wheeldir, # type: str - scheme, # type: Scheme - warn_script_location=True, # type: bool - pycompile=True # type: bool - ): - # type: (...) -> None - install_unpacked_wheel( - self.name, - wheeldir, - scheme=scheme, - req_description=str(self.req), - pycompile=pycompile, - warn_script_location=warn_script_location, - ) - # Things valid for sdists @property def unpacked_source_directory(self): @@ -813,11 +795,13 @@ def install( return if self.is_wheel: - self.move_wheel_files( + install_unpacked_wheel( + self.name, self.source_dir, scheme=scheme, - warn_script_location=warn_script_location, + req_description=str(self.req), pycompile=pycompile, + warn_script_location=warn_script_location, ) self.install_succeeded = True return From 65ac79571b764f6664d65900ac93e04176bed720 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 4 Dec 2019 23:08:53 -0500 Subject: [PATCH 0884/3170] Justify lack of encoding when reading record file --- src/pip/_internal/req/req_install.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 1a733675b9c..cb6747dd3df 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -855,6 +855,9 @@ def prepend_root(path): else: return change_root(root, path) + # We intentionally do not use any encoding to read the file because + # setuptools writes the file using distutils.file_util.write_file, + # which does not specify an encoding. with open(record_filename) as f: for line in f: directory = os.path.dirname(line) From 7db57478a28ee88d1e168077853d879a2154435a Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 5 Dec 2019 20:26:53 -0500 Subject: [PATCH 0885/3170] Construct Downloader outside RequirementPreparer Reduces RequirementPreparer responsibilities, and will let us get rid of some constructor arguments. --- src/pip/_internal/cli/req_command.py | 5 +++++ src/pip/_internal/operations/prepare.py | 5 +++-- tests/unit/test_operations_prepare.py | 2 +- tests/unit/test_req.py | 2 ++ 4 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 5f0752ff40a..39409ca0689 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -18,6 +18,7 @@ from pip._internal.index.package_finder import PackageFinder from pip._internal.legacy_resolve import Resolver from pip._internal.models.selection_prefs import SelectionPreferences +from pip._internal.network.download import Downloader from pip._internal.network.session import PipSession from pip._internal.operations.prepare import RequirementPreparer from pip._internal.req.constructors import ( @@ -162,8 +163,11 @@ def make_requirement_preparer( """ Create a RequirementPreparer instance for the given parameters. """ + downloader = Downloader(session, progress_bar=options.progress_bar) + temp_build_dir_path = temp_build_dir.path assert temp_build_dir_path is not None + return RequirementPreparer( build_dir=temp_build_dir_path, src_dir=options.src_dir, @@ -173,6 +177,7 @@ def make_requirement_preparer( build_isolation=options.build_isolation, req_tracker=req_tracker, session=session, + downloader=downloader, finder=finder, require_hashes=options.require_hashes, use_user_site=use_user_site, diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 6a7ae749bc4..1709d60f8f6 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -26,7 +26,6 @@ PreviousBuildDirError, VcsHashUnsupported, ) -from pip._internal.network.download import Downloader from pip._internal.utils.compat import expanduser from pip._internal.utils.filesystem import copy2_fixed from pip._internal.utils.hashes import MissingHashes @@ -56,6 +55,7 @@ from pip._internal.distributions import AbstractDistribution from pip._internal.index.package_finder import PackageFinder from pip._internal.models.link import Link + from pip._internal.network.download import Downloader from pip._internal.network.session import PipSession from pip._internal.req.req_install import InstallRequirement from pip._internal.req.req_tracker import RequirementTracker @@ -363,6 +363,7 @@ def __init__( build_isolation, # type: bool req_tracker, # type: RequirementTracker session, # type: PipSession + downloader, # type: Downloader finder, # type: PackageFinder require_hashes, # type: bool use_user_site, # type: bool @@ -373,7 +374,7 @@ def __init__( self.src_dir = src_dir self.build_dir = build_dir self.req_tracker = req_tracker - self.downloader = Downloader(session, progress_bar) + self.downloader = downloader self.finder = finder # Where still-packed archives should be written to. If None, they are diff --git a/tests/unit/test_operations_prepare.py b/tests/unit/test_operations_prepare.py index 7845e6ce116..adec8e020d7 100644 --- a/tests/unit/test_operations_prepare.py +++ b/tests/unit/test_operations_prepare.py @@ -9,9 +9,9 @@ from pip._internal.exceptions import HashMismatch from pip._internal.models.link import Link +from pip._internal.network.download import Downloader from pip._internal.network.session import PipSession from pip._internal.operations.prepare import ( - Downloader, _copy_source_tree, _download_http_url, unpack_file_url, diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index 361e2893e2f..2f234a8d5e3 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -19,6 +19,7 @@ PreviousBuildDirError, ) from pip._internal.legacy_resolve import Resolver +from pip._internal.network.download import Downloader from pip._internal.network.session import PipSession from pip._internal.operations.prepare import RequirementPreparer from pip._internal.req import InstallRequirement, RequirementSet @@ -81,6 +82,7 @@ def _basic_resolver(self, finder, require_hashes=False): build_isolation=True, req_tracker=tracker, session=PipSession(), + downloader=Downloader(PipSession(), progress_bar="on"), finder=finder, require_hashes=require_hashes, use_user_site=False, From 24d2f1e719499f7e53e29bd12c6b8f3004bf9268 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 5 Dec 2019 20:27:38 -0500 Subject: [PATCH 0886/3170] Remove unused arguments --- src/pip/_internal/cli/req_command.py | 2 -- src/pip/_internal/operations/prepare.py | 3 --- tests/unit/test_req.py | 2 -- 3 files changed, 7 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 39409ca0689..e5d1e8b2b0f 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -173,10 +173,8 @@ def make_requirement_preparer( src_dir=options.src_dir, download_dir=download_dir, wheel_download_dir=wheel_download_dir, - progress_bar=options.progress_bar, build_isolation=options.build_isolation, req_tracker=req_tracker, - session=session, downloader=downloader, finder=finder, require_hashes=options.require_hashes, diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 1709d60f8f6..8dff48914e3 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -56,7 +56,6 @@ from pip._internal.index.package_finder import PackageFinder from pip._internal.models.link import Link from pip._internal.network.download import Downloader - from pip._internal.network.session import PipSession from pip._internal.req.req_install import InstallRequirement from pip._internal.req.req_tracker import RequirementTracker from pip._internal.utils.hashes import Hashes @@ -359,10 +358,8 @@ def __init__( download_dir, # type: Optional[str] src_dir, # type: str wheel_download_dir, # type: Optional[str] - progress_bar, # type: str build_isolation, # type: bool req_tracker, # type: RequirementTracker - session, # type: PipSession downloader, # type: Downloader finder, # type: PackageFinder require_hashes, # type: bool diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index 2f234a8d5e3..0d5790831c6 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -78,10 +78,8 @@ def _basic_resolver(self, finder, require_hashes=False): src_dir=os.path.join(self.tempdir, 'src'), download_dir=None, wheel_download_dir=None, - progress_bar="on", build_isolation=True, req_tracker=tracker, - session=PipSession(), downloader=Downloader(PipSession(), progress_bar="on"), finder=finder, require_hashes=require_hashes, From cc96315273935e590ea6c8fa31f7beba67760cda Mon Sep 17 00:00:00 2001 From: Preet Thakkar <preet.thakkar@students.iiit.ac.in> Date: Fri, 6 Dec 2019 15:10:33 +0530 Subject: [PATCH 0887/3170] Added note about # noqa in getting-started.rst --- docs/html/development/getting-started.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/html/development/getting-started.rst b/docs/html/development/getting-started.rst index 66fae0d2775..5b4cb88de16 100644 --- a/docs/html/development/getting-started.rst +++ b/docs/html/development/getting-started.rst @@ -97,6 +97,13 @@ To use linters locally, run: $ tox -e lint +.. note:: + + Avoid using ``# noqa`` comments to suppress linter warnings - wherever + possible, warnings should be fixed instead. ``# noqa`` comments are + reserved for rare cases where the recommended style causes severe + readability problems. + Building Documentation ---------------------- From 682eafe546c0b95aaed4edbc18edb83fdc89065c Mon Sep 17 00:00:00 2001 From: Preet Thakkar <preet.thakkar@students.iiit.ac.in> Date: Fri, 6 Dec 2019 15:23:39 +0530 Subject: [PATCH 0888/3170] added news file --- news/7411.trivial | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/7411.trivial diff --git a/news/7411.trivial b/news/7411.trivial new file mode 100644 index 00000000000..62644961e78 --- /dev/null +++ b/news/7411.trivial @@ -0,0 +1 @@ +Added a note about # noqa comments in the Getting Started Guide \ No newline at end of file From 4abe2f708f865ab7f191bb47d0d8a4f48a093743 Mon Sep 17 00:00:00 2001 From: Preet Thakkar <preet.thakkar@students.iiit.ac.in> Date: Fri, 6 Dec 2019 15:29:36 +0530 Subject: [PATCH 0889/3170] added newline at end of news file --- news/7411.trivial | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/7411.trivial b/news/7411.trivial index 62644961e78..9181e01abf5 100644 --- a/news/7411.trivial +++ b/news/7411.trivial @@ -1 +1 @@ -Added a note about # noqa comments in the Getting Started Guide \ No newline at end of file +Added a note about # noqa comments in the Getting Started Guide From 0c65a9e2a3aed12c7df0eab266261fa190d1e8e0 Mon Sep 17 00:00:00 2001 From: Hugo <hugovk@users.noreply.github.com> Date: Sat, 7 Dec 2019 11:20:17 +0200 Subject: [PATCH 0890/3170] The next pip will be released after Python 2.7's EOL --- src/pip/_internal/cli/base_command.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index 31ee09e86aa..b1b989ae2cf 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -125,9 +125,9 @@ def _main(self, args): ) if platform.python_implementation() == "CPython": message = ( - "Python 2.7 will reach the end of its life on January " + "Python 2.7 reached the end of its life on January " "1st, 2020. Please upgrade your Python as Python 2.7 " - "won't be maintained after that date. " + "is no longer maintained. " ) + message deprecated(message, replacement=None, gone_in=None) From 201375352594a879ddf0b0a612c2f0f817db6645 Mon Sep 17 00:00:00 2001 From: Christopher Hunt <chrahunt@gmail.com> Date: Mon, 9 Dec 2019 04:55:51 +0800 Subject: [PATCH 0891/3170] Read record file once during legacy install (#7452) --- src/pip/_internal/req/req_install.py | 89 ++++++++++++++-------------- 1 file changed, 45 insertions(+), 44 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index d6ecc70455e..916ccc7e78f 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -823,52 +823,53 @@ def install( return self.install_succeeded = True - def prepend_root(path): - # type: (str) -> str - if root is None or not os.path.isabs(path): - return path - else: - return change_root(root, path) - # We intentionally do not use any encoding to read the file because # setuptools writes the file using distutils.file_util.write_file, # which does not specify an encoding. with open(record_filename) as f: - for line in f: - directory = os.path.dirname(line) - if directory.endswith('.egg-info'): - egg_info_dir = prepend_root(directory) - break - else: - deprecated( - reason=( - "{} did not indicate that it installed an " - ".egg-info directory. Only setup.py projects " - "generating .egg-info directories are supported." - ).format(self), - replacement=( - "for maintainers: updating the setup.py of {0}. " - "For users: contact the maintainers of {0} to let " - "them know to update their setup.py.".format( - self.name - ) - ), - gone_in="20.2", - issue=6998, - ) - # FIXME: put the record somewhere - return - new_lines = [] - with open(record_filename) as f: - for line in f: - filename = line.strip() - if os.path.isdir(filename): - filename += os.path.sep - new_lines.append( - os.path.relpath(prepend_root(filename), egg_info_dir) + record_lines = f.read().splitlines() + + def prepend_root(path): + # type: (str) -> str + if root is None or not os.path.isabs(path): + return path + else: + return change_root(root, path) + + for line in record_lines: + directory = os.path.dirname(line) + if directory.endswith('.egg-info'): + egg_info_dir = prepend_root(directory) + break + else: + deprecated( + reason=( + "{} did not indicate that it installed an " + ".egg-info directory. Only setup.py projects " + "generating .egg-info directories are supported." + ).format(self), + replacement=( + "for maintainers: updating the setup.py of {0}. " + "For users: contact the maintainers of {0} to let " + "them know to update their setup.py.".format( + self.name ) - 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') + ), + gone_in="20.2", + issue=6998, + ) + # FIXME: put the record somewhere + return + new_lines = [] + for line in record_lines: + filename = line.strip() + if os.path.isdir(filename): + filename += os.path.sep + new_lines.append( + os.path.relpath(prepend_root(filename), egg_info_dir) + ) + new_lines.sort() + ensure_dir(egg_info_dir) + inst_files_path = os.path.join(egg_info_dir, 'installed-files.txt') + with open(inst_files_path, 'w') as f: + f.write('\n'.join(new_lines) + '\n') From cf743dd245f01e766f76fe74bf6d694af2127545 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 7 Dec 2019 21:56:14 -0500 Subject: [PATCH 0892/3170] Remove unnecessary unlink in unpack_http_url The file is downloaded into a TempDirectory which will get cleaned up at the end of the `with` block, so no need to explicitly unlink the file. --- src/pip/_internal/operations/prepare.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 8dff48914e3..7933ec0924d 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -163,9 +163,6 @@ def unpack_http_url( if download_dir and not already_downloaded_path: _copy_file(from_path, download_dir, link) - if not already_downloaded_path: - os.unlink(from_path) - def _copy2_ignoring_special_files(src, dest): # type: (str, str) -> None From 0457826bd0ab408296cc63808b28eb818fdf3ffa Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 7 Dec 2019 21:08:26 -0500 Subject: [PATCH 0893/3170] Add global TempDirectory manager In cases where there is not a clear scope, or where enforcing a scope and passing a temp directory to callees creates unnecessary coupling between components, this will let us tie the lifetime of temporary directories to the lifetime of the application without using e.g. atexit or finalizers. This has the benefit of being easier to test and reason about. --- src/pip/_internal/utils/temp_dir.py | 27 +++++++++++++++++++++++++-- tests/unit/test_utils_temp_dir.py | 21 ++++++++++++++++++++- 2 files changed, 45 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/utils/temp_dir.py b/src/pip/_internal/utils/temp_dir.py index 77d40be6da3..ffd0dcfb2c1 100644 --- a/src/pip/_internal/utils/temp_dir.py +++ b/src/pip/_internal/utils/temp_dir.py @@ -8,17 +8,35 @@ import logging import os.path import tempfile +from contextlib import contextmanager + +from pip._vendor.contextlib2 import ExitStack from pip._internal.utils.misc import rmtree from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Optional + from typing import Iterator, Optional logger = logging.getLogger(__name__) +_tempdir_manager = None # type: Optional[ExitStack] + + +@contextmanager +def global_tempdir_manager(): + # type: () -> Iterator[None] + global _tempdir_manager + with ExitStack() as stack: + old_tempdir_manager, _tempdir_manager = _tempdir_manager, stack + try: + yield + finally: + _tempdir_manager = old_tempdir_manager + + class TempDirectory(object): """Helper class that owns and cleans up a temporary directory. @@ -44,7 +62,8 @@ def __init__( self, path=None, # type: Optional[str] delete=None, # type: Optional[bool] - kind="temp" + kind="temp", # type: str + globally_managed=False, # type: bool ): super(TempDirectory, self).__init__() @@ -61,6 +80,10 @@ def __init__( self.delete = delete self.kind = kind + if globally_managed: + assert _tempdir_manager is not None + _tempdir_manager.enter_context(self) + @property def path(self): # type: () -> str diff --git a/tests/unit/test_utils_temp_dir.py b/tests/unit/test_utils_temp_dir.py index 20a7852e77d..9b45a75b9e2 100644 --- a/tests/unit/test_utils_temp_dir.py +++ b/tests/unit/test_utils_temp_dir.py @@ -5,8 +5,13 @@ import pytest +from pip._internal.utils import temp_dir from pip._internal.utils.misc import ensure_dir -from pip._internal.utils.temp_dir import AdjacentTempDirectory, TempDirectory +from pip._internal.utils.temp_dir import ( + AdjacentTempDirectory, + TempDirectory, + global_tempdir_manager, +) # No need to test symlinked directories on Windows @@ -188,3 +193,17 @@ def raising_mkdir(*args, **kwargs): with pytest.raises(OSError): with AdjacentTempDirectory(original): pass + + +def test_global_tempdir_manager(): + with global_tempdir_manager(): + d = TempDirectory(globally_managed=True) + path = d.path + assert os.path.exists(path) + assert not os.path.exists(path) + + +def test_tempdirectory_asserts_global_tempdir(monkeypatch): + monkeypatch.setattr(temp_dir, "_tempdir_manager", None) + with pytest.raises(AssertionError): + TempDirectory(globally_managed=True) From d6b509d7c625016a6ae5514a8b4d0e3dd6d6a9f4 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 7 Dec 2019 21:27:00 -0500 Subject: [PATCH 0894/3170] Set up global tempdir manager in BaseCommand This ensures that the resource is available for the lifetime of the program. --- src/pip/_internal/cli/base_command.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index b1b989ae2cf..47c6926e46a 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -33,6 +33,7 @@ from pip._internal.utils.deprecation import deprecated from pip._internal.utils.logging import BrokenStdoutLoggingError, setup_logging from pip._internal.utils.misc import get_prog +from pip._internal.utils.temp_dir import global_tempdir_manager from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.virtualenv import running_under_virtualenv @@ -106,6 +107,10 @@ def main(self, args): def _main(self, args): # type: (List[str]) -> int + # Intentionally set as early as possible so globally-managed temporary + # directories are available to the rest of the code. + self.enter_context(global_tempdir_manager()) + options, args = self.parse_args(args) # Set verbosity so that it can be used elsewhere. From 6343e8093fbbb471e8a35d0c8982cf4897215a7f Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 7 Dec 2019 21:28:38 -0500 Subject: [PATCH 0895/3170] Replace atexit with globally-managed tempdir --- src/pip/_internal/operations/build/metadata.py | 7 +++---- tests/conftest.py | 12 ++++++++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/operations/build/metadata.py b/src/pip/_internal/operations/build/metadata.py index 43c3590c0ef..4d3f1306a5a 100644 --- a/src/pip/_internal/operations/build/metadata.py +++ b/src/pip/_internal/operations/build/metadata.py @@ -1,7 +1,6 @@ """Metadata generation logic for source distributions. """ -import atexit import logging import os @@ -25,9 +24,9 @@ def generate_metadata(install_req): build_env = install_req.build_env backend = install_req.pep517_backend - # NOTE: This needs to be refactored to stop using atexit - metadata_tmpdir = TempDirectory(kind="modern-metadata") - atexit.register(metadata_tmpdir.cleanup) + metadata_tmpdir = TempDirectory( + kind="modern-metadata", globally_managed=True + ) metadata_dir = metadata_tmpdir.path diff --git a/tests/conftest.py b/tests/conftest.py index 34609ab569d..bce70651b6b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -14,6 +14,7 @@ from setuptools.wheel import Wheel from pip._internal.main import main as pip_entry_point +from pip._internal.utils.temp_dir import global_tempdir_manager from pip._internal.utils.typing import MYPY_CHECK_RUNNING from tests.lib import DATA_DIR, SRC_DIR, TestData from tests.lib.certs import make_tls_cert, serialize_cert, serialize_key @@ -177,6 +178,17 @@ def isolate(tmpdir): ) +@pytest.fixture(autouse=True) +def scoped_global_tempdir_manager(): + """Make unit tests with globally-managed tempdirs easier + + Each test function gets its own individual scope for globally-managed + temporary directories in the application. + """ + with global_tempdir_manager(): + yield + + @pytest.fixture(scope='session') def pip_src(tmpdir_factory): def not_code_files_and_folders(path, names): From f4e40d7a095768d30fee1ba46ea48d77babe4b8f Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 7 Dec 2019 22:52:53 -0500 Subject: [PATCH 0896/3170] Wrap HAS_TLS in a function Next we'll refactor the value to be computed lazily. --- src/pip/_internal/models/search_scope.py | 4 ++-- src/pip/_internal/network/session.py | 4 ++-- src/pip/_internal/utils/compat.py | 5 +++++ 3 files changed, 9 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/models/search_scope.py b/src/pip/_internal/models/search_scope.py index 6e387068b63..45d6b557ead 100644 --- a/src/pip/_internal/models/search_scope.py +++ b/src/pip/_internal/models/search_scope.py @@ -10,7 +10,7 @@ from pip._vendor.six.moves.urllib import parse as urllib_parse from pip._internal.models.index import PyPI -from pip._internal.utils.compat import HAS_TLS +from pip._internal.utils.compat import has_tls from pip._internal.utils.misc import normalize_path, redact_auth_from_url from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -52,7 +52,7 @@ def create( # If we don't have TLS enabled, then WARN if anyplace we're looking # relies on TLS. - if not HAS_TLS: + if not has_tls(): for link in itertools.chain(index_urls, built_find_links): parsed = urllib_parse.urlparse(link) if parsed.scheme == 'https': diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py index 8eb4d88349c..2f980bf9ab8 100644 --- a/src/pip/_internal/network/session.py +++ b/src/pip/_internal/network/session.py @@ -26,7 +26,7 @@ from pip._internal.network.auth import MultiDomainBasicAuth from pip._internal.network.cache import SafeFileCache # Import ssl from compat so the initial import occurs in only one place. -from pip._internal.utils.compat import HAS_TLS, ipaddress, ssl +from pip._internal.utils.compat import has_tls, ipaddress, ssl from pip._internal.utils.filesystem import check_path_owner from pip._internal.utils.glibc import libc_ver from pip._internal.utils.misc import ( @@ -153,7 +153,7 @@ def user_agent(): if platform.machine(): data["cpu"] = platform.machine() - if HAS_TLS: + if has_tls(): data["openssl_version"] = ssl.OPENSSL_VERSION setuptools_version = get_installed_version("setuptools") diff --git a/src/pip/_internal/utils/compat.py b/src/pip/_internal/utils/compat.py index 26f6b0ea5d7..aa07628d70a 100644 --- a/src/pip/_internal/utils/compat.py +++ b/src/pip/_internal/utils/compat.py @@ -85,6 +85,11 @@ def backslashreplace_decode_fn(err): backslashreplace_decode = "backslashreplace" +def has_tls(): + # type: () -> bool + return HAS_TLS + + def str_to_display(data, desc=None): # type: (Union[bytes, Text], Optional[str]) -> Text """ From 355c303e126247873308467dc39aa9a754881c77 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 7 Dec 2019 22:58:17 -0500 Subject: [PATCH 0897/3170] Lazy evaluate has_tls result This avoids an unnecessary unconditional import of ssl and urllib3 in compat. --- src/pip/_internal/network/session.py | 3 ++- src/pip/_internal/utils/compat.py | 20 ++++++++------------ 2 files changed, 10 insertions(+), 13 deletions(-) diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py index 2f980bf9ab8..2d208cb65c3 100644 --- a/src/pip/_internal/network/session.py +++ b/src/pip/_internal/network/session.py @@ -26,7 +26,7 @@ from pip._internal.network.auth import MultiDomainBasicAuth from pip._internal.network.cache import SafeFileCache # Import ssl from compat so the initial import occurs in only one place. -from pip._internal.utils.compat import has_tls, ipaddress, ssl +from pip._internal.utils.compat import has_tls, ipaddress from pip._internal.utils.filesystem import check_path_owner from pip._internal.utils.glibc import libc_ver from pip._internal.utils.misc import ( @@ -154,6 +154,7 @@ def user_agent(): data["cpu"] = platform.machine() if has_tls(): + import _ssl as ssl data["openssl_version"] = ssl.OPENSSL_VERSION setuptools_version = get_installed_version("setuptools") diff --git a/src/pip/_internal/utils/compat.py b/src/pip/_internal/utils/compat.py index aa07628d70a..d347b73d98d 100644 --- a/src/pip/_internal/utils/compat.py +++ b/src/pip/_internal/utils/compat.py @@ -14,21 +14,12 @@ import sys from pip._vendor.six import PY2, text_type -from pip._vendor.urllib3.util import IS_PYOPENSSL from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: from typing import Optional, Text, Tuple, Union -try: - import _ssl # noqa -except ImportError: - ssl = None -else: - # This additional assignment was needed to prevent a mypy error. - ssl = _ssl - try: import ipaddress except ImportError: @@ -48,8 +39,6 @@ logger = logging.getLogger(__name__) -HAS_TLS = (ssl is not None) or IS_PYOPENSSL - if PY2: import imp @@ -87,7 +76,14 @@ def backslashreplace_decode_fn(err): def has_tls(): # type: () -> bool - return HAS_TLS + try: + import _ssl # noqa: F401 # ignore unused + return True + except ImportError: + pass + + from pip._vendor.urllib3.util import IS_PYOPENSSL + return IS_PYOPENSSL def str_to_display(data, desc=None): From 17075cc17fc4a9d37b826e09cbdb7ba52d8e3735 Mon Sep 17 00:00:00 2001 From: toonarmycaptain <toonarmycaptain@hotmail.com> Date: Tue, 10 Dec 2019 15:04:36 -0600 Subject: [PATCH 0898/3170] Update README.rst correcting grammar. `there is several` should be `there are several` --- src/pip/_vendor/README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_vendor/README.rst b/src/pip/_vendor/README.rst index c8a5385da76..39f168636a5 100644 --- a/src/pip/_vendor/README.rst +++ b/src/pip/_vendor/README.rst @@ -34,7 +34,7 @@ typical benefits of reusing libraries instead of reinventing the wheel like higher quality and more battle tested code, centralization of bug fixes (particularly security sensitive ones), and better/more features for less work. -However, there is several issues with having dependencies in the traditional +However, there are several issues with having dependencies in the traditional way (via ``install_requires``) for pip. These issues are: * **Fragility.** When pip depends on another library to function then if for From 04d8841ace46d49a1443ef56f1205a70019a1a2f Mon Sep 17 00:00:00 2001 From: Remi Rampin <r@remirampin.com> Date: Tue, 10 Dec 2019 17:18:38 -0500 Subject: [PATCH 0899/3170] Update links in docs --- README.rst | 2 +- docs/html/conf.py | 2 +- docs/html/development/ci.rst | 2 +- docs/html/development/contributing.rst | 2 +- docs/html/development/release-process.rst | 2 +- docs/html/index.rst | 2 +- docs/html/installing.rst | 2 +- docs/html/user_guide.rst | 2 +- news/7463.trivial | 0 src/pip/_internal/operations/install/wheel.py | 4 ++-- 10 files changed, 10 insertions(+), 10 deletions(-) create mode 100644 news/7463.trivial diff --git a/README.rst b/README.rst index d10caa63037..6d1bf585bb8 100644 --- a/README.rst +++ b/README.rst @@ -38,7 +38,7 @@ Code of Conduct Everyone interacting in the pip project's codebases, issue trackers, chat rooms, and mailing lists is expected to follow the `PyPA Code of Conduct`_. -.. _package installer: https://packaging.python.org/en/latest/current/ +.. _package installer: https://packaging.python.org/guides/tool-recommendations/ .. _Python Package Index: https://pypi.org .. _Installation: https://pip.pypa.io/en/stable/installing.html .. _Usage: https://pip.pypa.io/en/stable/ diff --git a/docs/html/conf.py b/docs/html/conf.py index 55eb722d5a9..bb30f75fb05 100644 --- a/docs/html/conf.py +++ b/docs/html/conf.py @@ -127,7 +127,7 @@ extlinks = { 'issue': ('https://github.com/pypa/pip/issues/%s', '#'), 'pull': ('https://github.com/pypa/pip/pull/%s', 'PR #'), - 'pypi': ('https://pypi.org/project/%s', ''), + 'pypi': ('https://pypi.org/project/%s/', ''), } # -- Options for HTML output -------------------------------------------------- diff --git a/docs/html/development/ci.rst b/docs/html/development/ci.rst index 2b7632a0c57..620aa528cf5 100644 --- a/docs/html/development/ci.rst +++ b/docs/html/development/ci.rst @@ -69,7 +69,7 @@ provides free executors for open source packages: .. _`Travis CI`: https://travis-ci.org/ .. _`Appveyor CI`: https://www.appveyor.com/ -.. _`Azure DevOps CI`: https://dev.azure.com/ +.. _`Azure DevOps CI`: https://azure.microsoft.com/en-us/services/devops/ .. _`GitHub Actions`: https://github.com/features/actions diff --git a/docs/html/development/contributing.rst b/docs/html/development/contributing.rst index 85485dc1713..a82420da464 100644 --- a/docs/html/development/contributing.rst +++ b/docs/html/development/contributing.rst @@ -248,7 +248,7 @@ and they will initiate a vote among the existing maintainers. - ReadTheDocs Administration capabilities .. _`Studies have shown`: https://www.kessler.de/prd/smartbear/BestPracticesForPeerCodeReview.pdf -.. _`resolve merge conflicts`: https://help.github.com/articles/resolving-a-merge-conflict-using-the-command-line/ +.. _`resolve merge conflicts`: https://help.github.com/articles/resolving-a-merge-conflict-using-the-command-line .. _`Travis CI`: https://travis-ci.org/ .. _`Appveyor CI`: https://www.appveyor.com/ .. _`.travis.yml`: https://github.com/pypa/pip/blob/master/.travis.yml diff --git a/docs/html/development/release-process.rst b/docs/html/development/release-process.rst index 9e9248c327b..f41f3cb803b 100644 --- a/docs/html/development/release-process.rst +++ b/docs/html/development/release-process.rst @@ -135,4 +135,4 @@ order to create one of these the changes should already be merged into the .. _`get-pip repository`: https://github.com/pypa/get-pip .. _`psf-salt repository`: https://github.com/python/psf-salt -.. _`CPython`: https://github.com/pypa/cpython +.. _`CPython`: https://github.com/python/cpython diff --git a/docs/html/index.rst b/docs/html/index.rst index 5ce442de086..a8fab2bd3a9 100644 --- a/docs/html/index.rst +++ b/docs/html/index.rst @@ -33,7 +33,7 @@ Code of Conduct Everyone interacting in the pip project's codebases, issue trackers, chat rooms, and mailing lists is expected to follow the `PyPA Code of Conduct`_. -.. _package installer: https://packaging.python.org/en/latest/current/ +.. _package installer: https://packaging.python.org/guides/tool-recommendations/ .. _Python Package Index: https://pypi.org .. _Installation: https://pip.pypa.io/en/stable/installing.html .. _Documentation: https://pip.pypa.io/en/stable/ diff --git a/docs/html/installing.rst b/docs/html/installing.rst index 3d346e2d22c..3ffeef5caae 100644 --- a/docs/html/installing.rst +++ b/docs/html/installing.rst @@ -89,7 +89,7 @@ Using Linux Package Managers See :ref:`pypug:Installing pip/setuptools/wheel with Linux Package Managers` in the `Python Packaging User Guide -<https://packaging.python.org/en/latest/current/>`_. +<https://packaging.python.org/guides/tool-recommendations/>`_. .. _`Upgrading pip`: diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 780e5a79089..ee827f0890b 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -402,7 +402,7 @@ look like this: Each subcommand can be configured optionally in its own section so that every global setting with the same name will be overridden; e.g. decreasing the ``timeout`` to ``10`` seconds when running the ``freeze`` -(`Freezing Requirements <./#freezing-requirements>`_) command and using +(:ref:`pip freeze`) command and using ``60`` seconds for all other commands is possible with: .. code-block:: ini diff --git a/news/7463.trivial b/news/7463.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 6b835b527c2..028ed3df7c6 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -583,8 +583,8 @@ def is_entrypoint_wrapper(name): entry = e.args[0] raise InstallationError( "Invalid script entry point: {} for req: {} - A callable " - "suffix is required. Cf https://packaging.python.org/en/" - "latest/distributing.html#console-scripts for more " + "suffix is required. Cf https://packaging.python.org/" + "specifications/entry-points/#use-for-scripts for more " "information.".format(entry, req_description) ) From 8b87f8728aa341e5a765f1eb52181660e2936868 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Thu, 12 Dec 2019 09:10:51 +0000 Subject: [PATCH 0900/3170] Add a comment showing how to call main using runpy --- ...a054cec-e4d6-4494-a554-90a2c0bee837.trivial | 0 src/pip/_internal/main.py | 18 ++++++++++++++++++ 2 files changed, 18 insertions(+) create mode 100644 news/6a054cec-e4d6-4494-a554-90a2c0bee837.trivial diff --git a/news/6a054cec-e4d6-4494-a554-90a2c0bee837.trivial b/news/6a054cec-e4d6-4494-a554-90a2c0bee837.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/main.py b/src/pip/_internal/main.py index 1e922402a0b..fe0bcf09ff0 100644 --- a/src/pip/_internal/main.py +++ b/src/pip/_internal/main.py @@ -19,6 +19,24 @@ logger = logging.getLogger(__name__) +# Do not run this directly! Running pip in-process is unsupported and +# unsafe. +# +# Also, the location of this function may change, so calling it directly +# is not portable across different pip versions. If you have to call +# this function, and understand and accept the implications of doing so, +# the best approach is to use runpy as follows: +# +# sys.argv = ["pip", your, args, here] +# runpy.run_module("pip", run_name="__main__") +# +# Note that this will exit the process after running, unlike a direct +# call to main. +# +# This still has all of the issues with running pip in-process, but +# ensures that you don’t rely on the (internal) name of the main +# function. + def main(args=None): if args is None: args = sys.argv[1:] From 80836a4cae328d1b302a3a49e8679406b10bd441 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Thu, 12 Dec 2019 09:31:13 +0000 Subject: [PATCH 0901/3170] Remove non-ASCII character in comment --- src/pip/_internal/main.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/main.py b/src/pip/_internal/main.py index fe0bcf09ff0..ff1df6d6b62 100644 --- a/src/pip/_internal/main.py +++ b/src/pip/_internal/main.py @@ -34,7 +34,7 @@ # call to main. # # This still has all of the issues with running pip in-process, but -# ensures that you don’t rely on the (internal) name of the main +# ensures that you don't rely on the (internal) name of the main # function. def main(args=None): From 38fd95aa6ae92dadcb38e2f76e4c2abe28eb74db Mon Sep 17 00:00:00 2001 From: Maxim Kurnikov <maxim.kurnikov@gmail.com> Date: Thu, 12 Dec 2019 06:44:35 +0300 Subject: [PATCH 0902/3170] update to mypy==0.750 --- .pre-commit-config.yaml | 2 +- src/pip/_internal/utils/misc.py | 5 ++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 04f655d08ee..c3c944ff8f3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,7 @@ repos: files: \.py$ - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.730 + rev: v0.750 hooks: - id: mypy exclude: docs|tests diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 85acab856b0..a53ce0dc9f9 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -457,8 +457,7 @@ def editables_only_test(d): def user_test(d): return True - # because of pkg_resources vendoring, mypy cannot find stub in typeshed - return [d for d in working_set # type: ignore + return [d for d in working_set if local_test(d) and d.key not in skip and editable_test(d) and @@ -878,4 +877,4 @@ def hash_file(path, blocksize=1 << 20): for block in read_chunks(f, size=blocksize): length += len(block) h.update(block) - return (h, length) # type: ignore + return h, length From 241a3cb6f2730ba829ec5d375962c5190d49727e Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Thu, 12 Dec 2019 10:50:58 +0000 Subject: [PATCH 0903/3170] Update wording based on review comments --- src/pip/_internal/main.py | 32 ++++++++++++++++++++------------ 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/pip/_internal/main.py b/src/pip/_internal/main.py index ff1df6d6b62..855569f3101 100644 --- a/src/pip/_internal/main.py +++ b/src/pip/_internal/main.py @@ -19,23 +19,31 @@ logger = logging.getLogger(__name__) -# Do not run this directly! Running pip in-process is unsupported and -# unsafe. -# -# Also, the location of this function may change, so calling it directly -# is not portable across different pip versions. If you have to call -# this function, and understand and accept the implications of doing so, -# the best approach is to use runpy as follows: +# Do not import and use main() directly! Using it directly is actively +# discouraged by pip's maintainers. The name, location and behavior of +# this function is subject to change, so calling it directly is not +# portable across different pip versions. + +# In addition, running pip in-process is unsupported and unsafe. This is +# elaborated in detail at +# https://pip.pypa.io/en/stable/user_guide/#using-pip-from-your-program. +# That document also provides suggestions that should work for nearly +# all users that are considering importing and using main() directly. + +# However, we know that certain users will still want to invoke pip +# in-process. If you understand and accept the implications of using pip +# in an unsupported manner, the best approach is to use runpy to avoid +# depending on the exact location of this entry point. + +# The following example shows how to use runpy to invoke pip in that +# case: # # sys.argv = ["pip", your, args, here] # runpy.run_module("pip", run_name="__main__") # # Note that this will exit the process after running, unlike a direct -# call to main. -# -# This still has all of the issues with running pip in-process, but -# ensures that you don't rely on the (internal) name of the main -# function. +# call to main. As it is not safe to do any processing after calling +# main, this should not be an issue in practice. def main(args=None): if args is None: From a046c4f0c3c7125bd0201fca6c64c893f41234c4 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 11 Dec 2019 20:00:22 -0500 Subject: [PATCH 0904/3170] Handle making --src-dir absolute at option declaration This will let us reduce duplication of this option normalization in a few of our commands. --- src/pip/_internal/cli/cmdoptions.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index 6e4d0eac506..8ea79d620ee 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -14,6 +14,7 @@ from __future__ import absolute_import import logging +import os import textwrap import warnings from distutils.util import strtobool @@ -410,12 +411,21 @@ def editable(): ) +def _handle_src(option, opt_str, value, parser): + # type: (Option, str, str, OptionParser) -> None + value = os.path.abspath(value) + setattr(parser.values, option.dest, value) + + src = partial( Option, '--src', '--source', '--source-dir', '--source-directory', dest='src_dir', + type='str', metavar='dir', default=get_src_prefix(), + action='callback', + callback=_handle_src, help='Directory to check out editable projects into. ' 'The default in a virtualenv is "<venv path>/src". ' 'The default for global installs is "<current dir>/src".' From b6e007c6a83446f61ef1ca6ec68b71fe7cf158d0 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 11 Dec 2019 20:05:18 -0500 Subject: [PATCH 0905/3170] Remove redundant src_dir normalization --- src/pip/_internal/commands/download.py | 1 - src/pip/_internal/commands/install.py | 1 - src/pip/_internal/commands/wheel.py | 2 -- 3 files changed, 4 deletions(-) diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index b6b6b7d27eb..23ee11a5b23 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -86,7 +86,6 @@ def run(self, options, args): cmdoptions.check_dist_restriction(options) - options.src_dir = os.path.abspath(options.src_dir) options.download_dir = normalize_path(options.download_dir) ensure_dir(options.download_dir) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index d62534de39a..e56dfa47dea 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -293,7 +293,6 @@ def run(self, options, args): cmdoptions.check_dist_restriction(options, check_target=True) - options.src_dir = os.path.abspath(options.src_dir) install_options = options.install_options or [] options.use_user_site = decide_user_install( diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index c4c9f7f5b9f..2cb430f445f 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -149,8 +149,6 @@ def run(self, options, args): if options.build_dir: options.build_dir = os.path.abspath(options.build_dir) - options.src_dir = os.path.abspath(options.src_dir) - session = self.get_default_session(options) finder = self._build_package_finder(options, session) From 95c3f632e3f3f9f1f0340889201d619c576df520 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 11 Dec 2019 20:07:08 -0500 Subject: [PATCH 0906/3170] Handle build dir normalization in cmdoptions Technically this changes behavior in DownloadCommand, which does not make build_dir absolute, but given that it is used the same way as in InstallCommand and WheelCommand (which do make it absolute), it should not have any visible impact. --- src/pip/_internal/cli/cmdoptions.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index 8ea79d620ee..4e3144746f4 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -679,11 +679,22 @@ def _handle_no_cache_dir(option, opt, value, parser): help="Don't install package dependencies.", ) # type: Callable[..., Option] + +def _handle_build_dir(option, opt, value, parser): + # type: (Option, str, str, OptionParser) -> None + if value: + value = os.path.abspath(value) + setattr(parser.values, option.dest, value) + + build_dir = partial( Option, '-b', '--build', '--build-dir', '--build-directory', dest='build_dir', + type='str', metavar='dir', + action='callback', + callback=_handle_build_dir, help='Directory to unpack packages into and build in. Note that ' 'an initial build still takes place in a temporary directory. ' 'The location of temporary directories can be controlled by setting ' From fe320a3da02c5ee64ce56c79d570c48c1659ce9b Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 11 Dec 2019 20:10:17 -0500 Subject: [PATCH 0907/3170] Remove redundant build_dir normalization --- src/pip/_internal/commands/install.py | 3 --- src/pip/_internal/commands/wheel.py | 3 --- 2 files changed, 6 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index e56dfa47dea..c7dcf28df8a 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -288,9 +288,6 @@ def run(self, options, args): if options.upgrade: upgrade_strategy = options.upgrade_strategy - if options.build_dir: - options.build_dir = os.path.abspath(options.build_dir) - cmdoptions.check_dist_restriction(options, check_target=True) install_options = options.install_options or [] diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index 2cb430f445f..0a9aa5f5271 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -146,9 +146,6 @@ def run(self, options, args): # type: (Values, List[Any]) -> None cmdoptions.check_install_build_global(options) - if options.build_dir: - options.build_dir = os.path.abspath(options.build_dir) - session = self.get_default_session(options) finder = self._build_package_finder(options, session) From b69560661b03a928680f5f481cec43d42126f635 Mon Sep 17 00:00:00 2001 From: Maxim Kurnikov <maxim.kurnikov@gmail.com> Date: Thu, 12 Dec 2019 16:00:44 +0300 Subject: [PATCH 0908/3170] remove disallow_untyped_defs=False for pip._internal.distributions, pip._internal.operations.prepare --- src/pip/_internal/distributions/base.py | 15 +++++++++++--- src/pip/_internal/distributions/installed.py | 12 ++++++++--- src/pip/_internal/distributions/source.py | 21 ++++++++++++++++---- src/pip/_internal/distributions/wheel.py | 10 +++++++--- src/pip/_internal/operations/prepare.py | 11 ++++++++-- src/pip/_internal/req/req_install.py | 2 +- 6 files changed, 55 insertions(+), 16 deletions(-) diff --git a/src/pip/_internal/distributions/base.py b/src/pip/_internal/distributions/base.py index 929bbefb858..b836b98d162 100644 --- a/src/pip/_internal/distributions/base.py +++ b/src/pip/_internal/distributions/base.py @@ -1,10 +1,16 @@ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - import abc from pip._vendor.six import add_metaclass +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import Optional + + from pip._vendor.pkg_resources import Distribution + from pip._internal.req import InstallRequirement + from pip._internal.index.package_finder import PackageFinder + @add_metaclass(abc.ABCMeta) class AbstractDistribution(object): @@ -24,13 +30,16 @@ class AbstractDistribution(object): """ def __init__(self, req): + # type: (InstallRequirement) -> None super(AbstractDistribution, self).__init__() self.req = req @abc.abstractmethod def get_pkg_resources_distribution(self): + # type: () -> Optional[Distribution] raise NotImplementedError() @abc.abstractmethod def prepare_distribution_metadata(self, finder, build_isolation): + # type: (PackageFinder, bool) -> None raise NotImplementedError() diff --git a/src/pip/_internal/distributions/installed.py b/src/pip/_internal/distributions/installed.py index 454fb48c2b4..0d15bf42405 100644 --- a/src/pip/_internal/distributions/installed.py +++ b/src/pip/_internal/distributions/installed.py @@ -1,7 +1,11 @@ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - from pip._internal.distributions.base import AbstractDistribution +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import Optional + + from pip._vendor.pkg_resources import Distribution + from pip._internal.index.package_finder import PackageFinder class InstalledDistribution(AbstractDistribution): @@ -12,7 +16,9 @@ class InstalledDistribution(AbstractDistribution): """ def get_pkg_resources_distribution(self): + # type: () -> Optional[Distribution] return self.req.satisfied_by def prepare_distribution_metadata(self, finder, build_isolation): + # type: (PackageFinder, bool) -> None pass diff --git a/src/pip/_internal/distributions/source.py b/src/pip/_internal/distributions/source.py index f74ec5acc82..be3d7d97a1c 100644 --- a/src/pip/_internal/distributions/source.py +++ b/src/pip/_internal/distributions/source.py @@ -1,12 +1,17 @@ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - import logging from pip._internal.build_env import BuildEnvironment from pip._internal.distributions.base import AbstractDistribution from pip._internal.exceptions import InstallationError from pip._internal.utils.subprocess import runner_with_spinner_message +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import Set, Tuple + + from pip._vendor.pkg_resources import Distribution + from pip._internal.index.package_finder import PackageFinder + logger = logging.getLogger(__name__) @@ -19,9 +24,11 @@ class SourceDistribution(AbstractDistribution): """ def get_pkg_resources_distribution(self): + # type: () -> Distribution return self.req.get_dist() def prepare_distribution_metadata(self, finder, build_isolation): + # type: (PackageFinder, bool) -> None # Load pyproject.toml, to determine whether PEP 517 is to be used self.req.load_pyproject_toml() @@ -33,7 +40,9 @@ def prepare_distribution_metadata(self, finder, build_isolation): self.req.prepare_metadata() def _setup_isolation(self, finder): + # type: (PackageFinder) -> None def _raise_conflicts(conflicting_with, conflicting_reqs): + # type: (str, Set[Tuple[str, str]]) -> None format_string = ( "Some build dependencies for {requirement} " "conflict with {conflicting_with}: {description}." @@ -50,9 +59,12 @@ def _raise_conflicts(conflicting_with, conflicting_reqs): # Isolate in a BuildEnvironment and install the build-time # requirements. + pyproject_requires = self.req.pyproject_requires + assert pyproject_requires is not None + self.req.build_env = BuildEnvironment() self.req.build_env.install_requirements( - finder, self.req.pyproject_requires, 'overlay', + finder, pyproject_requires, 'overlay', "Installing build dependencies" ) conflicting, missing = self.req.build_env.check_requirements( @@ -79,6 +91,7 @@ def _raise_conflicts(conflicting_with, conflicting_reqs): "Getting requirements to build wheel" ) backend = self.req.pep517_backend + assert backend is not None with backend.subprocess_runner(runner): reqs = backend.get_requires_for_build_wheel() diff --git a/src/pip/_internal/distributions/wheel.py b/src/pip/_internal/distributions/wheel.py index 128951ff6d1..c609ef4e348 100644 --- a/src/pip/_internal/distributions/wheel.py +++ b/src/pip/_internal/distributions/wheel.py @@ -1,9 +1,11 @@ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - from pip._vendor import pkg_resources from pip._internal.distributions.base import AbstractDistribution +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from pip._vendor.pkg_resources import Distribution + from pip._internal.index.package_finder import PackageFinder class WheelDistribution(AbstractDistribution): @@ -13,8 +15,10 @@ class WheelDistribution(AbstractDistribution): """ def get_pkg_resources_distribution(self): + # type: () -> Distribution return list(pkg_resources.find_distributions( self.req.source_dir))[0] def prepare_distribution_metadata(self, finder, build_isolation): + # type: (PackageFinder, bool) -> None pass diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 7933ec0924d..e3e04b44dae 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -3,7 +3,6 @@ # The following comment should be removed at some point in the future. # mypy: strict-optional=False -# mypy: disallow-untyped-defs=False import logging import mimetypes @@ -84,7 +83,13 @@ logger = logging.getLogger(__name__) -def _get_prepared_distribution(req, req_tracker, finder, build_isolation): +def _get_prepared_distribution( + req, # type: InstallRequirement + req_tracker, # type: RequirementTracker + finder, # type: PackageFinder + build_isolation # type: bool +): + # type: (...) -> AbstractDistribution """Prepare a distribution for installation. """ abstract_dist = make_distribution_for_install_requirement(req) @@ -101,6 +106,7 @@ def unpack_vcs_link(link, location): def _copy_file(filename, location, link): + # type: (str, str, Link) -> None copy = True download_location = os.path.join(location, link.filename) if os.path.exists(download_location): @@ -188,6 +194,7 @@ def _copy2_ignoring_special_files(src, dest): def _copy_source_tree(source, target): # type: (str, str) -> None def ignore(d, names): + # type: (str, List[str]) -> List[str] # Pulling in those directories can potentially be very slow, # exclude the following directories if they appear in the top # level dir (and only it). diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 916ccc7e78f..059dc04e881 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -152,7 +152,7 @@ def __init__( # This holds the pkg_resources.Distribution object if this requirement # is already available: - self.satisfied_by = None + self.satisfied_by = None # type: Optional[Distribution] # Whether the installation process should try to uninstall an existing # distribution before installing this requirement. self.should_reinstall = False From deb322d7b7fdeb74654ebb1762fbe0e5e6026625 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Thu, 12 Dec 2019 23:15:22 +0100 Subject: [PATCH 0909/3170] Move pep517 wheel build method to standalone function --- src/pip/_internal/wheel_builder.py | 73 ++++++++++++++++-------------- 1 file changed, 39 insertions(+), 34 deletions(-) diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index e67b18781dd..90a1a29cb5c 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -227,6 +227,40 @@ def _build_wheel_legacy( return wheel_path +def _build_wheel_pep517( + req, # type: InstallRequirement + build_options, # type: List[str] + tempd, # type: str +): + # type: (...) -> Optional[str] + """Build one InstallRequirement using the PEP 517 build process. + + Returns path to wheel if successfully built. Otherwise, returns None. + """ + assert req.metadata_directory is not None + if build_options: + # PEP 517 does not support --build-options + logger.error('Cannot build wheel for %s using PEP 517 when ' + '--build-option is present' % (req.name,)) + return None + try: + logger.debug('Destination directory: %s', tempd) + + runner = runner_with_spinner_message( + 'Building wheel for {} (PEP 517)'.format(req.name) + ) + backend = req.pep517_backend + with backend.subprocess_runner(runner): + wheel_name = backend.build_wheel( + tempd, + metadata_directory=req.metadata_directory, + ) + except Exception: + logger.error('Failed building wheel for %s', req.name) + return None + return os.path.join(tempd, wheel_name) + + def _always_true(_): # type: (Any) -> bool return True @@ -284,7 +318,11 @@ def _build_one_inside_env( # type: (...) -> Optional[str] with TempDirectory(kind="wheel") as temp_dir: if req.use_pep517: - wheel_path = self._build_one_pep517(req, temp_dir.path) + wheel_path = _build_wheel_pep517( + req, + build_options=self.build_options, + tempd=temp_dir.path, + ) else: wheel_path = _build_wheel_legacy( name=req.name, @@ -313,39 +351,6 @@ def _build_one_inside_env( self._clean_one(req) return None - def _build_one_pep517( - self, - req, # type: InstallRequirement - tempd, # type: str - ): - # type: (...) -> Optional[str] - """Build one InstallRequirement using the PEP 517 build process. - - Returns path to wheel if successfully built. Otherwise, returns None. - """ - assert req.metadata_directory is not None - if self.build_options: - # PEP 517 does not support --build-options - logger.error('Cannot build wheel for %s using PEP 517 when ' - '--build-option is present' % (req.name,)) - return None - try: - logger.debug('Destination directory: %s', tempd) - - runner = runner_with_spinner_message( - 'Building wheel for {} (PEP 517)'.format(req.name) - ) - backend = req.pep517_backend - with backend.subprocess_runner(runner): - wheel_name = backend.build_wheel( - tempd, - metadata_directory=req.metadata_directory, - ) - except Exception: - logger.error('Failed building wheel for %s', req.name) - return None - return os.path.join(tempd, wheel_name) - def _clean_one(self, req): # type: (InstallRequirement) -> bool clean_args = make_setuptools_clean_args( From 2eff06e7cc21c3c56be12a95d30817e61d789673 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Thu, 12 Dec 2019 23:26:51 +0100 Subject: [PATCH 0910/3170] Make WheelBuilder should_unpack argument explicit --- src/pip/_internal/commands/wheel.py | 1 + src/pip/_internal/wheel_builder.py | 2 +- tests/unit/test_wheel_builder.py | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index 0a9aa5f5271..dfd0a84d9ba 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -196,6 +196,7 @@ def run(self, options, args): ) build_failures = wb.build( requirement_set.requirements.values(), + should_unpack=False, ) self.save_wheelnames( [req.link.filename for req in diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index 90a1a29cb5c..0fab0ae9e24 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -369,7 +369,7 @@ def _clean_one(self, req): def build( self, requirements, # type: Iterable[InstallRequirement] - should_unpack=False # type: bool + should_unpack, # type: bool ): # type: (...) -> List[InstallRequirement] """Build wheels. diff --git a/tests/unit/test_wheel_builder.py b/tests/unit/test_wheel_builder.py index 3e0fc0490ef..bd1b0aa9d82 100644 --- a/tests/unit/test_wheel_builder.py +++ b/tests/unit/test_wheel_builder.py @@ -193,7 +193,7 @@ def test_skip_building_wheels(self, caplog): wheel_req = Mock(is_wheel=True, editable=False, constraint=False) with caplog.at_level(logging.INFO): - wb.build([wheel_req]) + wb.build([wheel_req], should_unpack=False) assert "due to already being wheel" in caplog.text assert mock_build_one.mock_calls == [] From 339e061e6b95a6b934242f9a0b5cbf33b18ec881 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Thu, 12 Dec 2019 23:44:59 +0100 Subject: [PATCH 0911/3170] Don't pass req to _build_wheel_pep517 --- src/pip/_internal/wheel_builder.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index 0fab0ae9e24..d1d01fe4865 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -40,6 +40,7 @@ RequirementPreparer ) from pip._internal.req.req_install import InstallRequirement + from pip._vendor.pep517.wrappers import Pep517HookCaller BinaryAllowedPredicate = Callable[[InstallRequirement], bool] @@ -228,7 +229,9 @@ def _build_wheel_legacy( def _build_wheel_pep517( - req, # type: InstallRequirement + name, # type: str + backend, # type: Pep517HookCaller + metadata_directory, # type: str build_options, # type: List[str] tempd, # type: str ): @@ -237,26 +240,25 @@ def _build_wheel_pep517( Returns path to wheel if successfully built. Otherwise, returns None. """ - assert req.metadata_directory is not None + assert metadata_directory is not None if build_options: # PEP 517 does not support --build-options logger.error('Cannot build wheel for %s using PEP 517 when ' - '--build-option is present' % (req.name,)) + '--build-option is present' % (name,)) return None try: logger.debug('Destination directory: %s', tempd) runner = runner_with_spinner_message( - 'Building wheel for {} (PEP 517)'.format(req.name) + 'Building wheel for {} (PEP 517)'.format(name) ) - backend = req.pep517_backend with backend.subprocess_runner(runner): wheel_name = backend.build_wheel( tempd, - metadata_directory=req.metadata_directory, + metadata_directory=metadata_directory, ) except Exception: - logger.error('Failed building wheel for %s', req.name) + logger.error('Failed building wheel for %s', name) return None return os.path.join(tempd, wheel_name) @@ -319,7 +321,9 @@ def _build_one_inside_env( with TempDirectory(kind="wheel") as temp_dir: if req.use_pep517: wheel_path = _build_wheel_pep517( - req, + name=req.name, + backend=req.pep517_backend, + metadata_directory=req.metadata_directory, build_options=self.build_options, tempd=temp_dir.path, ) From d7eaede434fb962add409b04cb29c8329fa6c9e2 Mon Sep 17 00:00:00 2001 From: Christopher Hunt <chrahunt@gmail.com> Date: Fri, 13 Dec 2019 07:57:05 +0800 Subject: [PATCH 0912/3170] Revert "Add new option: pip wheel --save-wheel-names (#6377)" (#7420) This reverts commit bcad1b1cb59f1071af44f39188eb488f694304fd, reversing changes made to f86490317ac8e35221c09f6bf360896b0734aa0c. As discussed, we should rethink the interface of this command output as part of larger CLI usability review. In the interim, the same functionality can be achieved using straightforward shell commands. --- news/6340.feature | 1 - src/pip/_internal/commands/wheel.py | 40 ------------------ src/pip/_internal/wheel_builder.py | 6 --- tests/functional/test_wheel.py | 63 ----------------------------- tests/unit/test_wheel.py | 28 ------------- 5 files changed, 138 deletions(-) delete mode 100644 news/6340.feature diff --git a/news/6340.feature b/news/6340.feature deleted file mode 100644 index 9afba8bf24a..00000000000 --- a/news/6340.feature +++ /dev/null @@ -1 +0,0 @@ -Add a new option ``--save-wheel-names <filename>`` to ``pip wheel`` that writes the names of the resulting wheels to the given filename. diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index 0a9aa5f5271..06efc90adb2 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -102,16 +102,6 @@ def __init__(self, *args, **kw): cmd_opts.add_option(cmdoptions.no_clean()) cmd_opts.add_option(cmdoptions.require_hashes()) - cmd_opts.add_option( - '--save-wheel-names', - dest='path_to_wheelnames', - action='store', - metavar='path', - help=("Store the filenames of the built or downloaded wheels " - "in a new file of given path. Filenames are separated " - "by new line and file ends with new line"), - ) - index_opts = cmdoptions.make_option_group( cmdoptions.index_group, self.parser, @@ -120,28 +110,6 @@ def __init__(self, *args, **kw): self.parser.insert_option_group(0, index_opts) self.parser.insert_option_group(0, cmd_opts) - def save_wheelnames( - self, - links_filenames, - path_to_wheelnames, - wheel_filenames, - ): - if path_to_wheelnames is None: - return - - entries_to_save = wheel_filenames + links_filenames - entries_to_save = [ - filename + '\n' for filename in entries_to_save - if filename.endswith('whl') - ] - try: - with open(path_to_wheelnames, 'w') as f: - f.writelines(entries_to_save) - except EnvironmentError as e: - logger.error('Cannot write to the given path: %s\n%s' % - (path_to_wheelnames, e)) - raise - def run(self, options, args): # type: (Values, List[Any]) -> None cmdoptions.check_install_build_global(options) @@ -192,18 +160,10 @@ def run(self, options, args): preparer, wheel_cache, build_options=options.build_options or [], global_options=options.global_options or [], - path_to_wheelnames=options.path_to_wheelnames ) build_failures = wb.build( requirement_set.requirements.values(), ) - self.save_wheelnames( - [req.link.filename for req in - requirement_set.successfully_downloaded - if req.link is not None], - wb.path_to_wheelnames, - wb.wheel_filenames, - ) if len(build_failures) != 0: raise CommandError( "Failed to build one or more wheels" diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index e67b18781dd..948fb7dbf9e 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -242,7 +242,6 @@ def __init__( build_options=None, # type: Optional[List[str]] global_options=None, # type: Optional[List[str]] check_binary_allowed=None, # type: Optional[BinaryAllowedPredicate] - path_to_wheelnames=None, # type: Optional[Union[bytes, Text]] ): # type: (...) -> None if check_binary_allowed is None: @@ -257,8 +256,6 @@ def __init__( self.build_options = build_options or [] self.global_options = global_options or [] self.check_binary_allowed = check_binary_allowed - # path where to save built names of built wheels - self.path_to_wheelnames = path_to_wheelnames # file names of built wheel names self.wheel_filenames = [] # type: List[Union[bytes, Text]] @@ -472,9 +469,6 @@ def build( ) build_failure.append(req) continue - self.wheel_filenames.append( - os.path.relpath(wheel_file, output_dir) - ) build_success.append(req) else: build_failure.append(req) diff --git a/tests/functional/test_wheel.py b/tests/functional/test_wheel.py index b123d0693cd..00959f65dd6 100644 --- a/tests/functional/test_wheel.py +++ b/tests/functional/test_wheel.py @@ -1,7 +1,6 @@ """'pip wheel' tests""" import os import re -import stat from os.path import exists import pytest @@ -280,65 +279,3 @@ def test_legacy_wheels_are_not_confused_with_other_files(script, tmpdir, data): wheel_file_name = 'simplewheel-1.0-py%s-none-any.whl' % pyversion[0] wheel_file_path = script.scratch / wheel_file_name assert wheel_file_path in result.files_created, result.stdout - - -def test_pip_option_save_wheel_name(script, data): - """Check if the option saves the filenames of built wheels - """ - script.pip( - 'wheel', '--no-index', '-f', data.find_links, - 'require_simple==1.0', - '--save-wheel-name', 'wheelnames', - ) - - wheel_file_names = [ - 'require_simple-1.0-py%s-none-any.whl' % pyversion[0], - 'simple-3.0-py%s-none-any.whl' % pyversion[0], - ] - wheelnames_path = script.scratch_path / 'wheelnames' - with open(wheelnames_path, 'r') as wheelnames_file: - wheelnames_entries = (wheelnames_file.read()).splitlines() - assert wheel_file_names == wheelnames_entries - - -def test_pip_option_save_wheel_name_Permission_error(script, data): - - temp_file = script.base_path / 'scratch' / 'wheelnames' - - wheel_file_names = [ - 'require_simple-1.0-py%s-none-any.whl' % pyversion[0], - 'simple-3.0-py%s-none-any.whl' % pyversion[0], - ] - - script.pip( - 'wheel', '--no-index', '-f', data.find_links, - 'require_simple==1.0', - '--save-wheel-name', 'wheelnames', - ) - os.chmod(temp_file, stat.S_IREAD) - result = script.pip( - 'wheel', '--no-index', '-f', data.find_links, - 'require_simple==1.0', - '--save-wheel-name', 'wheelnames', expect_error=True, - ) - os.chmod(temp_file, stat.S_IREAD | stat.S_IWRITE) - - assert "ERROR: Cannot write to the given path: wheelnames\n" \ - "[Errno 13] Permission denied: 'wheelnames'\n" in result.stderr - - with open(temp_file) as f: - result = f.read().splitlines() - # check that file stays same - assert result == wheel_file_names - - -def test_pip_option_save_wheel_name_error_during_build(script, data): - script.pip( - 'wheel', '--no-index', '--save-wheel-name', 'wheelnames', - '-f', data.find_links, 'wheelbroken==0.1', - expect_error=True, - ) - wheelnames_path = script.base_path / 'scratch' / 'wheelnames' - with open(wheelnames_path) as f: - wheelnames = f.read().splitlines() - assert wheelnames == [] diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 9e7e98b1f1c..4bcbcbee2a2 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -8,7 +8,6 @@ from mock import patch from pip._vendor.packaging.requirements import Requirement -from pip._internal.commands.wheel import WheelCommand from pip._internal.exceptions import UnsupportedWheel from pip._internal.locations import get_scheme from pip._internal.models.scheme import Scheme @@ -525,30 +524,3 @@ def test_rehash(self, tmpdir): h, length = wheel.rehash(self.test_file) assert length == str(self.test_file_len) assert h == self.test_file_hash_encoded - - -class TestWheelCommand(object): - - def test_save_wheelnames(self, tmpdir): - wheel_filenames = ['Flask-1.1.dev0-py2.py3-none-any.whl'] - links_filenames = [ - 'flask', - 'Werkzeug-0.15.4-py2.py3-none-any.whl', - 'Jinja2-2.10.1-py2.py3-none-any.whl', - 'itsdangerous-1.1.0-py2.py3-none-any.whl', - 'Click-7.0-py2.py3-none-any.whl' - ] - - expected = wheel_filenames + links_filenames[1:] - expected = [filename + '\n' for filename in expected] - temp_file = tmpdir.joinpath('wheelfiles') - - WheelCommand('name', 'summary').save_wheelnames( - links_filenames, - temp_file, - wheel_filenames - ) - - with open(temp_file, 'r') as f: - test_content = f.readlines() - assert test_content == expected From 6b0a79501cec65ec6c713e7ffafa6a886a102288 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 12 Dec 2019 21:18:03 -0500 Subject: [PATCH 0913/3170] Assert that target file does not exist _check_download_dir will only return a falsy value if either: * the provided path does not exist * the hash does not match - in which case the file is unlinked so the file cannot exist at either of these points. --- src/pip/_internal/operations/prepare.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index e3e04b44dae..15c08de3e53 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -167,6 +167,9 @@ def unpack_http_url( # a download dir is specified; let's copy the archive there if download_dir and not already_downloaded_path: + assert not os.path.exists( + os.path.join(download_dir, link.filename) + ) _copy_file(from_path, download_dir, link) @@ -261,6 +264,7 @@ def unpack_file_url( # a download dir is specified and not already downloaded if download_dir and not already_downloaded_path: + assert not os.path.exists(os.path.join(download_dir, link.filename)) _copy_file(from_path, download_dir, link) From 7dea92e5b3aaed176ac1173696901045dde4fe97 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 12 Dec 2019 22:27:13 -0500 Subject: [PATCH 0914/3170] Replace already_downloaded_path with existence check These statements are equivalent, so we exchange them. This will make it easier to factor the download_dir concerns out of these functions. --- src/pip/_internal/operations/prepare.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 15c08de3e53..1e1d8fc3e4e 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -166,10 +166,9 @@ def unpack_http_url( unpack_file(from_path, location, content_type) # a download dir is specified; let's copy the archive there - if download_dir and not already_downloaded_path: - assert not os.path.exists( - os.path.join(download_dir, link.filename) - ) + if download_dir and not os.path.exists( + os.path.join(download_dir, link.filename) + ): _copy_file(from_path, download_dir, link) @@ -263,8 +262,9 @@ def unpack_file_url( unpack_file(from_path, location, content_type) # a download dir is specified and not already downloaded - if download_dir and not already_downloaded_path: - assert not os.path.exists(os.path.join(download_dir, link.filename)) + if download_dir and not os.path.exists( + os.path.join(download_dir, link.filename) + ): _copy_file(from_path, download_dir, link) From 58e2a99ccf96266b5a93872cac540b82441f9186 Mon Sep 17 00:00:00 2001 From: Maxim Kurnikov <maxim.kurnikov@gmail.com> Date: Fri, 13 Dec 2019 09:35:35 +0300 Subject: [PATCH 0915/3170] remove disallow_untyped_defs=False for most of pip._internal.cli modules --- src/pip/_internal/cli/autocompletion.py | 43 ++++++++++++++---------- src/pip/_internal/cli/cmdoptions.py | 4 ++- src/pip/_internal/cli/command_context.py | 13 +++++-- src/pip/_internal/cli/req_command.py | 9 +++-- 4 files changed, 45 insertions(+), 24 deletions(-) diff --git a/src/pip/_internal/cli/autocompletion.py b/src/pip/_internal/cli/autocompletion.py index 5440241b342..329de602513 100644 --- a/src/pip/_internal/cli/autocompletion.py +++ b/src/pip/_internal/cli/autocompletion.py @@ -1,19 +1,22 @@ """Logic that powers autocompletion installed by ``pip completion``. """ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - import optparse import os import sys +from itertools import chain from pip._internal.cli.main_parser import create_main_parser from pip._internal.commands import commands_dict, create_command from pip._internal.utils.misc import get_installed_distributions +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import Any, Iterable, List, Optional def autocomplete(): + # type: () -> None """Entry Point for completion of main and subcommand options. """ # Don't complete if user hasn't sourced bash_completion file. @@ -26,17 +29,18 @@ def autocomplete(): except IndexError: current = '' + parser = create_main_parser() subcommands = list(commands_dict) options = [] - # subcommand - try: - subcommand_name = [w for w in cwords if w in subcommands][0] - except IndexError: - subcommand_name = None - parser = create_main_parser() + # subcommand + subcommand_name = None # type: Optional[str] + for word in cwords: + if word in subcommands: + subcommand_name = word + break # subcommand options - if subcommand_name: + if subcommand_name is not None: # special case: 'help' subcommand has no options if subcommand_name == 'help': sys.exit(1) @@ -76,8 +80,8 @@ def autocomplete(): # get completion files and directories if ``completion_type`` is # ``<file>``, ``<dir>`` or ``<path>`` if completion_type: - options = auto_complete_paths(current, completion_type) - options = ((opt, 0) for opt in options) + paths = auto_complete_paths(current, completion_type) + options = [(path, 0) for path in paths] for option in options: opt_label = option[0] # append '=' to options which require args @@ -89,22 +93,25 @@ def autocomplete(): opts = [i.option_list for i in parser.option_groups] opts.append(parser.option_list) - opts = (o for it in opts for o in it) + flattened_opts = chain.from_iterable(opts) if current.startswith('-'): - for opt in opts: + for opt in flattened_opts: if opt.help != optparse.SUPPRESS_HELP: subcommands += opt._long_opts + opt._short_opts else: # get completion type given cwords and all available options - completion_type = get_path_completion_type(cwords, cword, opts) + completion_type = get_path_completion_type(cwords, cword, + flattened_opts) if completion_type: - subcommands = auto_complete_paths(current, completion_type) + subcommands = list(auto_complete_paths(current, + completion_type)) print(' '.join([x for x in subcommands if x.startswith(current)])) sys.exit(1) def get_path_completion_type(cwords, cword, opts): + # type: (List[str], int, Iterable[Any]) -> Optional[str] """Get the type of path completion (``file``, ``dir``, ``path`` or None) :param cwords: same as the environmental variable ``COMP_WORDS`` @@ -113,7 +120,7 @@ def get_path_completion_type(cwords, cword, opts): :return: path completion type (``file``, ``dir``, ``path`` or None) """ if cword < 2 or not cwords[cword - 2].startswith('-'): - return + return None for opt in opts: if opt.help == optparse.SUPPRESS_HELP: continue @@ -123,9 +130,11 @@ def get_path_completion_type(cwords, cword, opts): x in ('path', 'file', 'dir') for x in opt.metavar.split('/')): return opt.metavar + return None def auto_complete_paths(current, completion_type): + # type: (str, str) -> Iterable[str] """If ``completion_type`` is ``file`` or ``path``, list all regular files and directories starting with ``current``; otherwise only list directories starting with ``current``. diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index 4e3144746f4..f14c7d7eeca 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -9,7 +9,6 @@ # The following comment should be removed at some point in the future. # mypy: strict-optional=False -# mypy: disallow-untyped-defs=False from __future__ import absolute_import @@ -40,6 +39,7 @@ def raise_option_error(parser, option, msg): + # type: (OptionParser, Option, str) -> None """ Raise an option parsing error using parser.error(). @@ -78,6 +78,7 @@ def check_install_build_global(options, check_options=None): check_options = options def getname(n): + # type: (str) -> Optional[Any] return getattr(check_options, n, None) names = ["build_options", "global_options", "install_options"] if any(map(getname, names)): @@ -323,6 +324,7 @@ def exists_action(): def extra_index_url(): + # type: () -> Option return Option( '--extra-index-url', dest='extra_index_urls', diff --git a/src/pip/_internal/cli/command_context.py b/src/pip/_internal/cli/command_context.py index 3ab255f558f..d1a64a77606 100644 --- a/src/pip/_internal/cli/command_context.py +++ b/src/pip/_internal/cli/command_context.py @@ -1,19 +1,25 @@ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - from contextlib import contextmanager from pip._vendor.contextlib2 import ExitStack +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import Iterator, ContextManager, TypeVar + + _T = TypeVar('_T', covariant=True) + class CommandContextMixIn(object): def __init__(self): + # type: () -> None super(CommandContextMixIn, self).__init__() self._in_main_context = False self._main_context = ExitStack() @contextmanager def main_context(self): + # type: () -> Iterator[None] assert not self._in_main_context self._in_main_context = True @@ -24,6 +30,7 @@ def main_context(self): self._in_main_context = False def enter_context(self, context_provider): + # type: (ContextManager[_T]) -> _T assert self._in_main_context return self._main_context.enter_context(context_provider) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index e5d1e8b2b0f..cf6e101f78a 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -5,9 +5,6 @@ PackageFinder machinery and all its vendored dependencies, etc. """ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - import logging import os from functools import partial @@ -52,11 +49,13 @@ class SessionCommandMixin(CommandContextMixIn): A class mixin for command classes needing _build_session(). """ def __init__(self): + # type: () -> None super(SessionCommandMixin, self).__init__() self._session = None # Optional[PipSession] @classmethod def _get_index_urls(cls, options): + # type: (Values) -> Optional[List[str]] """Return a list of index urls from user-provided options.""" index_urls = [] if not getattr(options, "no_index", False): @@ -74,6 +73,10 @@ def get_default_session(self, options): """Get a default-managed session.""" if self._session is None: self._session = self.enter_context(self._build_session(options)) + # there's no type annotation on requests.Session, so it's + # automatically ContextManager[Any] and self._session becomes Any, + # then https://github.com/python/mypy/issues/7696 kicks in + assert self._session is not None return self._session def _build_session(self, options, retries=None, timeout=None): From 3cb30385d88f71f1889541f2bde44bf911dac7d2 Mon Sep 17 00:00:00 2001 From: Maxim Kurnikov <maxim.kurnikov@gmail.com> Date: Fri, 13 Dec 2019 10:22:21 +0300 Subject: [PATCH 0916/3170] remove disallow_untyped_defs=False for more modules --- src/pip/_internal/index/package_finder.py | 2 +- src/pip/_internal/main.py | 8 +++++--- src/pip/_internal/models/candidate.py | 4 +--- src/pip/_internal/models/link.py | 6 +++--- src/pip/_internal/models/search_scope.py | 4 +--- src/pip/_internal/utils/hashes.py | 4 +--- src/pip/_internal/utils/marker_files.py | 4 +--- src/pip/_internal/utils/temp_dir.py | 15 +++++++++++---- 8 files changed, 24 insertions(+), 23 deletions(-) diff --git a/src/pip/_internal/index/package_finder.py b/src/pip/_internal/index/package_finder.py index 3c98de2c5fc..d9ee8b5a877 100644 --- a/src/pip/_internal/index/package_finder.py +++ b/src/pip/_internal/index/package_finder.py @@ -2,7 +2,6 @@ # The following comment should be removed at some point in the future. # mypy: strict-optional=False -# mypy: disallow-untyped-defs=False from __future__ import absolute_import @@ -904,6 +903,7 @@ def find_requirement(self, req, upgrade): installed_version = parse_version(req.satisfied_by.version) def _format_versions(cand_iter): + # type: (Iterable[InstallationCandidate]) -> str # This repeated parse_version and str() conversion is needed to # handle different vendoring sources from pip and pkg_resources. # If we stop using the pkg_resources provided specifier and start diff --git a/src/pip/_internal/main.py b/src/pip/_internal/main.py index 855569f3101..5e97a5103f6 100644 --- a/src/pip/_internal/main.py +++ b/src/pip/_internal/main.py @@ -1,8 +1,5 @@ """Primary application entrypoint. """ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - from __future__ import absolute_import import locale @@ -15,6 +12,10 @@ from pip._internal.commands import create_command from pip._internal.exceptions import PipError from pip._internal.utils import deprecation +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import List, Optional logger = logging.getLogger(__name__) @@ -46,6 +47,7 @@ # main, this should not be an issue in practice. def main(args=None): + # type: (Optional[List[str]]) -> int if args is None: args = sys.argv[1:] diff --git a/src/pip/_internal/models/candidate.py b/src/pip/_internal/models/candidate.py index 850825f6aba..1dc1a576eea 100644 --- a/src/pip/_internal/models/candidate.py +++ b/src/pip/_internal/models/candidate.py @@ -1,6 +1,3 @@ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - from pip._vendor.packaging.version import parse as parse_version from pip._internal.utils.models import KeyBasedCompareMixin @@ -33,6 +30,7 @@ def __repr__(self): ) def __str__(self): + # type: () -> str return '{!r} candidate (version {} at {})'.format( self.name, self.version, self.link, ) diff --git a/src/pip/_internal/models/link.py b/src/pip/_internal/models/link.py index d0445fd54e1..34fbcbfe7e4 100644 --- a/src/pip/_internal/models/link.py +++ b/src/pip/_internal/models/link.py @@ -1,6 +1,3 @@ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - import os import posixpath import re @@ -67,6 +64,7 @@ def __init__( super(Link, self).__init__(key=url, defining_class=Link) def __str__(self): + # type: () -> str if self.requires_python: rp = ' (requires-python:%s)' % self.requires_python else: @@ -78,6 +76,7 @@ def __str__(self): return redact_auth_from_url(str(self._url)) def __repr__(self): + # type: () -> str return '<Link %s>' % self @property @@ -211,6 +210,7 @@ def is_yanked(self): @property def has_hash(self): + # type: () -> bool return self.hash_name is not None def is_hash_allowed(self, hashes): diff --git a/src/pip/_internal/models/search_scope.py b/src/pip/_internal/models/search_scope.py index 45d6b557ead..138d1b6eedf 100644 --- a/src/pip/_internal/models/search_scope.py +++ b/src/pip/_internal/models/search_scope.py @@ -1,6 +1,3 @@ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - import itertools import logging import os @@ -101,6 +98,7 @@ def get_index_urls_locations(self, project_name): """ def mkurl_pypi_url(url): + # type: (str) -> str loc = posixpath.join( url, urllib_parse.quote(canonicalize_name(project_name))) diff --git a/src/pip/_internal/utils/hashes.py b/src/pip/_internal/utils/hashes.py index a0d87a41ea3..4c41551a255 100644 --- a/src/pip/_internal/utils/hashes.py +++ b/src/pip/_internal/utils/hashes.py @@ -1,6 +1,3 @@ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - from __future__ import absolute_import import hashlib @@ -59,6 +56,7 @@ def is_hash_allowed( hash_name, # type: str hex_digest, # type: str ): + # type: (...) -> bool """Return whether the given hex digest is allowed.""" return hex_digest in self._allowed.get(hash_name, []) diff --git a/src/pip/_internal/utils/marker_files.py b/src/pip/_internal/utils/marker_files.py index 734cba4c1d4..42ea8140508 100644 --- a/src/pip/_internal/utils/marker_files.py +++ b/src/pip/_internal/utils/marker_files.py @@ -1,6 +1,3 @@ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - import os.path DELETE_MARKER_MESSAGE = '''\ @@ -14,6 +11,7 @@ def has_delete_marker_file(directory): + # type: (str) -> bool return os.path.exists(os.path.join(directory, PIP_DELETE_MARKER_FILENAME)) diff --git a/src/pip/_internal/utils/temp_dir.py b/src/pip/_internal/utils/temp_dir.py index ffd0dcfb2c1..0be0ff785cd 100644 --- a/src/pip/_internal/utils/temp_dir.py +++ b/src/pip/_internal/utils/temp_dir.py @@ -1,6 +1,3 @@ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - from __future__ import absolute_import import errno @@ -16,7 +13,9 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Iterator, Optional + from typing import Any, Iterator, Optional, TypeVar + + _T = TypeVar('_T', bound='TempDirectory') logger = logging.getLogger(__name__) @@ -93,16 +92,20 @@ def path(self): return self._path def __repr__(self): + # type: () -> str return "<{} {!r}>".format(self.__class__.__name__, self.path) def __enter__(self): + # type: (_T) -> _T return self def __exit__(self, exc, value, tb): + # type: (Any, Any, Any) -> None if self.delete: self.cleanup() def _create(self, kind): + # type: (str) -> str """Create a temporary directory and store its path in self.path """ # We realpath here because some systems have their default tmpdir @@ -116,6 +119,7 @@ def _create(self, kind): return path def cleanup(self): + # type: () -> None """Remove the temporary directory created and reset state """ self._deleted = True @@ -145,11 +149,13 @@ class AdjacentTempDirectory(TempDirectory): LEADING_CHARS = "-~.=%0123456789" def __init__(self, original, delete=None): + # type: (str, Optional[bool]) -> None self.original = original.rstrip('/\\') super(AdjacentTempDirectory, self).__init__(delete=delete) @classmethod def _generate_names(cls, name): + # type: (str) -> Iterator[str] """Generates a series of temporary names. The algorithm replaces the leading characters in the name @@ -173,6 +179,7 @@ def _generate_names(cls, name): yield new_name def _create(self, kind): + # type: (str) -> str root, name = os.path.split(self.original) for candidate in self._generate_names(name): path = os.path.join(root, candidate) From 9e737fe511892de905549877cc0446700429c3c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Fri, 13 Dec 2019 21:42:06 +0100 Subject: [PATCH 0917/3170] Extract buildset collection from WheelBuilder.build Towards splitting the build method in two for pip wheel and pip install cases. --- src/pip/_internal/wheel_builder.py | 55 +++++++++++++++++------------- 1 file changed, 31 insertions(+), 24 deletions(-) diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index 4e191061bb3..d3a6472c762 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -32,7 +32,7 @@ if MYPY_CHECK_RUNNING: from typing import ( - Any, Callable, Iterable, List, Optional, Pattern, Text, Union, + Any, Callable, Iterable, List, Optional, Pattern, Text, Tuple, Union, ) from pip._internal.cache import WheelCache @@ -367,39 +367,21 @@ def _clean_one(self, req): logger.error('Failed cleaning build dir for %s', req.name) return False - def build( + def _collect_buildset( self, requirements, # type: Iterable[InstallRequirement] - should_unpack, # type: bool + need_wheel, # type: bool ): - # type: (...) -> List[InstallRequirement] - """Build wheels. - - :param should_unpack: If True, after building the wheel, unpack it - and replace the sdist with the unpacked version in preparation - for installation. - :return: The list of InstallRequirement that failed to build. - """ - # pip install uses should_unpack=True. - # pip install never provides a _wheel_dir. - # pip wheel uses should_unpack=False. - # pip wheel always provides a _wheel_dir (via the preparer). - assert ( - (should_unpack and not self._wheel_dir) or - (not should_unpack and self._wheel_dir) - ) - + # type: (...) -> List[Tuple[InstallRequirement, str]] buildset = [] cache_available = bool(self.wheel_cache.cache_dir) - for req in requirements: if not should_build( req, - need_wheel=not should_unpack, + need_wheel=need_wheel, check_binary_allowed=self.check_binary_allowed, ): continue - if ( cache_available and should_cache(req, self.check_binary_allowed) @@ -409,9 +391,34 @@ def build( output_dir = self.wheel_cache.get_ephem_path_for_link( req.link ) - buildset.append((req, output_dir)) + return buildset + def build( + self, + requirements, # type: Iterable[InstallRequirement] + should_unpack, # type: bool + ): + # type: (...) -> List[InstallRequirement] + """Build wheels. + + :param should_unpack: If True, after building the wheel, unpack it + and replace the sdist with the unpacked version in preparation + for installation. + :return: The list of InstallRequirement that failed to build. + """ + # pip install uses should_unpack=True. + # pip install never provides a _wheel_dir. + # pip wheel uses should_unpack=False. + # pip wheel always provides a _wheel_dir (via the preparer). + assert ( + (should_unpack and not self._wheel_dir) or + (not should_unpack and self._wheel_dir) + ) + + buildset = self._collect_buildset( + requirements, need_wheel=not should_unpack + ) if not buildset: return [] From 98f6ff86613c32e29872fb84a6d117899d0aceaf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Fri, 13 Dec 2019 22:01:54 +0100 Subject: [PATCH 0918/3170] Rename output_dir to cache_dir in WheelBuilder for clarity --- src/pip/_internal/wheel_builder.py | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index d3a6472c762..48208f99088 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -373,6 +373,10 @@ def _collect_buildset( need_wheel, # type: bool ): # type: (...) -> List[Tuple[InstallRequirement, str]] + """Return the list of InstallRequirement that need to be built, + with the persistent or temporary cache directory where the built + wheel needs to be stored. + """ buildset = [] cache_available = bool(self.wheel_cache.cache_dir) for req in requirements: @@ -386,12 +390,10 @@ def _collect_buildset( cache_available and should_cache(req, self.check_binary_allowed) ): - output_dir = self.wheel_cache.get_path_for_link(req.link) + cache_dir = self.wheel_cache.get_path_for_link(req.link) else: - output_dir = self.wheel_cache.get_ephem_path_for_link( - req.link - ) - buildset.append((req, output_dir)) + cache_dir = self.wheel_cache.get_ephem_path_for_link(req.link) + buildset.append((req, cache_dir)) return buildset def build( @@ -433,9 +435,9 @@ def build( with indent_log(): build_success, build_failure = [], [] - for req, output_dir in buildset: + for req, cache_dir in buildset: try: - ensure_dir(output_dir) + ensure_dir(cache_dir) except OSError as e: logger.warning( "Building wheel for %s failed: %s", @@ -444,7 +446,7 @@ def build( build_failure.append(req) continue - wheel_file = self._build_one(req, output_dir) + wheel_file = self._build_one(req, cache_dir) if wheel_file: if should_unpack: # XXX: This is mildly duplicative with prepare_files, @@ -475,7 +477,7 @@ def build( try: ensure_dir(self._wheel_dir) shutil.copy( - os.path.join(output_dir, wheel_file), + os.path.join(cache_dir, wheel_file), self._wheel_dir, ) except OSError as e: From 50efb55072ffdc90ab61532f7ff902fec75a224f Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 13 Dec 2019 19:32:35 -0500 Subject: [PATCH 0919/3170] Add tests.lib.path.Path.read_bytes To be used in some upcoming tests. --- tests/lib/path.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/lib/path.py b/tests/lib/path.py index 5e1d924989c..2f8fdd24218 100644 --- a/tests/lib/path.py +++ b/tests/lib/path.py @@ -178,6 +178,11 @@ def joinpath(self, *parts): def join(self, *parts): raise RuntimeError('Path.join is invalid, use joinpath instead.') + def read_bytes(self): + # type: () -> bytes + with open(self, "rb") as fp: + return fp.read() + def read_text(self): with open(self, "r") as fp: return fp.read() From 30c2d979b0481e34ccb8dcf044f48c09b927da4f Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 13 Dec 2019 19:35:42 -0500 Subject: [PATCH 0920/3170] Make unpack_file_url download test to functional This and the next several changes will uncouple the tests from the current implementation, allowing us to factor the actual file download out of `unpack_file_url` and `unpack_http_url`. --- tests/functional/test_download.py | 27 +++++++++++++++++++++++++++ tests/unit/test_operations_prepare.py | 7 ------- 2 files changed, 27 insertions(+), 7 deletions(-) diff --git a/tests/functional/test_download.py b/tests/functional/test_download.py index e19c9e5548f..a25118cfccc 100644 --- a/tests/functional/test_download.py +++ b/tests/functional/test_download.py @@ -5,6 +5,7 @@ import pytest from pip._internal.cli.status_codes import ERROR +from pip._internal.utils.urls import path_to_url from tests.lib.path import Path @@ -720,3 +721,29 @@ def test_download_prefer_binary_when_only_tarball_exists(script, data): Path('scratch') / 'source-1.0.tar.gz' in result.files_created ) + + +@pytest.fixture(scope="session") +def shared_script(tmpdir_factory, script_factory): + tmpdir = Path(str(tmpdir_factory.mktemp("download_shared_script"))) + script = script_factory(tmpdir.joinpath("workspace")) + return script + + +def test_download_file_url(shared_script, shared_data, tmpdir): + download_dir = tmpdir / 'download' + download_dir.mkdir() + downloaded_path = download_dir / 'simple-1.0.tar.gz' + + simple_pkg = shared_data.packages / 'simple-1.0.tar.gz' + + shared_script.pip( + 'download', + '-d', + str(download_dir), + '--no-index', + path_to_url(str(simple_pkg)), + ) + + assert downloaded_path.exists() + assert simple_pkg.read_bytes() == downloaded_path.read_bytes() diff --git a/tests/unit/test_operations_prepare.py b/tests/unit/test_operations_prepare.py index adec8e020d7..25d344f4e01 100644 --- a/tests/unit/test_operations_prepare.py +++ b/tests/unit/test_operations_prepare.py @@ -239,13 +239,6 @@ def test_unpack_file_url_no_download(self, tmpdir, data): assert not os.path.isfile( os.path.join(self.download_dir, self.dist_file)) - def test_unpack_file_url_and_download(self, tmpdir, data): - self.prep(tmpdir, data) - unpack_file_url(self.dist_url, self.build_dir, - download_dir=self.download_dir) - assert os.path.isdir(os.path.join(self.build_dir, 'simple')) - assert os.path.isfile(os.path.join(self.download_dir, self.dist_file)) - def test_unpack_file_url_download_already_exists(self, tmpdir, data, monkeypatch): self.prep(tmpdir, data) From d30f406c37a30b0425869f1faa386d62458a3711 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 13 Dec 2019 19:41:26 -0500 Subject: [PATCH 0921/3170] Check file actually used against hashes This makes the behavior of this function easier to test, since we can use a different file to distinguish the already-downloaded case from the existing-file-hash-failed case. --- src/pip/_internal/operations/prepare.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 1e1d8fc3e4e..e529577d8f5 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -235,14 +235,6 @@ def unpack_file_url( logger.info('Link is a directory, ignoring download_dir') return - # If --require-hashes is off, `hashes` is either empty, the - # link's embedded hash, or MissingHashes; it is required to - # match. If --require-hashes is on, we are satisfied by any - # hash in `hashes` matching: a URL-based or an option-based - # one; no internet-sourced hash will be in `hashes`. - if hashes: - hashes.check_against_path(link_path) - # If a download dir is specified, is the file already there and valid? already_downloaded_path = None if download_dir: @@ -255,6 +247,14 @@ def unpack_file_url( else: from_path = link_path + # If --require-hashes is off, `hashes` is either empty, the + # link's embedded hash, or MissingHashes; it is required to + # match. If --require-hashes is on, we are satisfied by any + # hash in `hashes` matching: a URL-based or an option-based + # one; no internet-sourced hash will be in `hashes`. + if hashes: + hashes.check_against_path(from_path) + content_type = mimetypes.guess_type(from_path)[0] # unpack the archive to the build dir location. even when only downloading From fe24c7fc16d4915e0fb2f06d472a827bd0a1b802 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 13 Dec 2019 19:49:35 -0500 Subject: [PATCH 0922/3170] Make unpack_file_url existing matching file test functional Reduces coupling between tests and code. --- tests/functional/test_download.py | 20 ++++++++++++++++++++ tests/unit/test_operations_prepare.py | 16 ---------------- 2 files changed, 20 insertions(+), 16 deletions(-) diff --git a/tests/functional/test_download.py b/tests/functional/test_download.py index a25118cfccc..d1581a33b78 100644 --- a/tests/functional/test_download.py +++ b/tests/functional/test_download.py @@ -1,6 +1,7 @@ import os.path import shutil import textwrap +from hashlib import sha256 import pytest @@ -747,3 +748,22 @@ def test_download_file_url(shared_script, shared_data, tmpdir): assert downloaded_path.exists() assert simple_pkg.read_bytes() == downloaded_path.read_bytes() + + +def test_download_file_url_existing_ok_download( + shared_script, shared_data, tmpdir +): + download_dir = tmpdir / 'download' + download_dir.mkdir() + downloaded_path = download_dir / 'simple-1.0.tar.gz' + fake_existing_package = shared_data.packages / 'simple-2.0.tar.gz' + shutil.copy(str(fake_existing_package), str(downloaded_path)) + downloaded_path_bytes = downloaded_path.read_bytes() + digest = sha256(downloaded_path_bytes).hexdigest() + + simple_pkg = shared_data.packages / 'simple-1.0.tar.gz' + url = "{}#sha256={}".format(path_to_url(simple_pkg), digest) + + shared_script.pip('download', '-d', str(download_dir), url) + + assert downloaded_path_bytes == downloaded_path.read_bytes() diff --git a/tests/unit/test_operations_prepare.py b/tests/unit/test_operations_prepare.py index 25d344f4e01..178e00fcb48 100644 --- a/tests/unit/test_operations_prepare.py +++ b/tests/unit/test_operations_prepare.py @@ -239,22 +239,6 @@ def test_unpack_file_url_no_download(self, tmpdir, data): assert not os.path.isfile( os.path.join(self.download_dir, self.dist_file)) - def test_unpack_file_url_download_already_exists(self, tmpdir, - data, monkeypatch): - self.prep(tmpdir, data) - # add in previous download (copy simple-2.0 as simple-1.0) - # so we can tell it didn't get overwritten - dest_file = os.path.join(self.download_dir, self.dist_file) - copy(self.dist_path2, dest_file) - with open(self.dist_path2, 'rb') as f: - dist_path2_md5 = hashlib.md5(f.read()).hexdigest() - - unpack_file_url(self.dist_url, self.build_dir, - download_dir=self.download_dir) - # our hash should be the same, i.e. not overwritten by simple-1.0 hash - with open(dest_file, 'rb') as f: - assert dist_path2_md5 == hashlib.md5(f.read()).hexdigest() - def test_unpack_file_url_bad_hash(self, tmpdir, data, monkeypatch): """ From d3920f299f58a786143dbe1148f5e17bfeb439e2 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 13 Dec 2019 19:53:40 -0500 Subject: [PATCH 0923/3170] Make unpack_file_url existing bad file test functional Reduces coupling between tests and code. --- tests/functional/test_download.py | 19 +++++++++++++++ tests/unit/test_operations_prepare.py | 33 +-------------------------- 2 files changed, 20 insertions(+), 32 deletions(-) diff --git a/tests/functional/test_download.py b/tests/functional/test_download.py index d1581a33b78..87f62c36354 100644 --- a/tests/functional/test_download.py +++ b/tests/functional/test_download.py @@ -767,3 +767,22 @@ def test_download_file_url_existing_ok_download( shared_script.pip('download', '-d', str(download_dir), url) assert downloaded_path_bytes == downloaded_path.read_bytes() + + +def test_download_file_url_existing_bad_download( + shared_script, shared_data, tmpdir +): + download_dir = tmpdir / 'download' + download_dir.mkdir() + downloaded_path = download_dir / 'simple-1.0.tar.gz' + fake_existing_package = shared_data.packages / 'simple-2.0.tar.gz' + shutil.copy(str(fake_existing_package), str(downloaded_path)) + + simple_pkg = shared_data.packages / 'simple-1.0.tar.gz' + simple_pkg_bytes = simple_pkg.read_bytes() + digest = sha256(simple_pkg_bytes).hexdigest() + url = "{}#sha256={}".format(path_to_url(simple_pkg), digest) + + shared_script.pip('download', '-d', str(download_dir), url) + + assert simple_pkg_bytes == downloaded_path.read_bytes() diff --git a/tests/unit/test_operations_prepare.py b/tests/unit/test_operations_prepare.py index 178e00fcb48..b38cafa7f1c 100644 --- a/tests/unit/test_operations_prepare.py +++ b/tests/unit/test_operations_prepare.py @@ -1,7 +1,7 @@ import hashlib import os import shutil -from shutil import copy, rmtree +from shutil import rmtree from tempfile import mkdtemp import pytest @@ -252,37 +252,6 @@ def test_unpack_file_url_bad_hash(self, tmpdir, data, self.build_dir, hashes=Hashes({'md5': ['bogus']})) - def test_unpack_file_url_download_bad_hash(self, tmpdir, data, - monkeypatch): - """ - Test when existing download has different hash from the file url - fragment - """ - self.prep(tmpdir, data) - - # add in previous download (copy simple-2.0 as simple-1.0 so it's wrong - # hash) - dest_file = os.path.join(self.download_dir, self.dist_file) - copy(self.dist_path2, dest_file) - - with open(self.dist_path, 'rb') as f: - dist_path_md5 = hashlib.md5(f.read()).hexdigest() - with open(dest_file, 'rb') as f: - dist_path2_md5 = hashlib.md5(f.read()).hexdigest() - - assert dist_path_md5 != dist_path2_md5 - - url = '{}#md5={}'.format(self.dist_url.url, dist_path_md5) - dist_url = Link(url) - unpack_file_url(dist_url, self.build_dir, - download_dir=self.download_dir, - hashes=Hashes({'md5': [dist_path_md5]})) - - # confirm hash is for simple1-1.0 - # the previous bad download has been removed - with open(dest_file, 'rb') as f: - assert hashlib.md5(f.read()).hexdigest() == dist_path_md5 - def test_unpack_file_url_thats_a_dir(self, tmpdir, data): self.prep(tmpdir, data) dist_path = data.packages.joinpath("FSPkg") From 9faa9aef2939ddb1fb4023ed15a209f59f123690 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 13 Dec 2019 20:15:44 -0500 Subject: [PATCH 0924/3170] Make download_http_url existing bad file test functional Reduces coupling between tests and code. --- tests/functional/test_download.py | 31 +++++++++++++++++ tests/unit/test_operations_prepare.py | 48 +-------------------------- 2 files changed, 32 insertions(+), 47 deletions(-) diff --git a/tests/functional/test_download.py b/tests/functional/test_download.py index 87f62c36354..4064221ecdc 100644 --- a/tests/functional/test_download.py +++ b/tests/functional/test_download.py @@ -8,6 +8,7 @@ from pip._internal.cli.status_codes import ERROR from pip._internal.utils.urls import path_to_url from tests.lib.path import Path +from tests.lib.server import file_response def fake_wheel(data, wheel_path): @@ -786,3 +787,33 @@ def test_download_file_url_existing_bad_download( shared_script.pip('download', '-d', str(download_dir), url) assert simple_pkg_bytes == downloaded_path.read_bytes() + + +def test_download_http_url_bad_hash( + shared_script, shared_data, tmpdir, mock_server +): + download_dir = tmpdir / 'download' + download_dir.mkdir() + downloaded_path = download_dir / 'simple-1.0.tar.gz' + fake_existing_package = shared_data.packages / 'simple-2.0.tar.gz' + shutil.copy(str(fake_existing_package), str(downloaded_path)) + + simple_pkg = shared_data.packages / 'simple-1.0.tar.gz' + simple_pkg_bytes = simple_pkg.read_bytes() + digest = sha256(simple_pkg_bytes).hexdigest() + mock_server.set_responses([ + file_response(simple_pkg) + ]) + mock_server.start() + base_address = 'http://{}:{}'.format(mock_server.host, mock_server.port) + url = "{}/simple-1.0.tar.gz#sha256={}".format(base_address, digest) + + shared_script.pip('download', '-d', str(download_dir), url) + + assert simple_pkg_bytes == downloaded_path.read_bytes() + + mock_server.stop() + requests = mock_server.get_requests() + assert len(requests) == 1 + assert requests[0]['PATH_INFO'] == '/simple-1.0.tar.gz' + assert requests[0]['HTTP_ACCEPT_ENCODING'] == 'identity' diff --git a/tests/unit/test_operations_prepare.py b/tests/unit/test_operations_prepare.py index b38cafa7f1c..6040db26ec0 100644 --- a/tests/unit/test_operations_prepare.py +++ b/tests/unit/test_operations_prepare.py @@ -1,11 +1,10 @@ -import hashlib import os import shutil from shutil import rmtree from tempfile import mkdtemp import pytest -from mock import Mock, patch +from mock import Mock from pip._internal.exceptions import HashMismatch from pip._internal.models.link import Link @@ -19,7 +18,6 @@ ) from pip._internal.utils.hashes import Hashes from pip._internal.utils.urls import path_to_url -from tests.lib import create_file from tests.lib.filesystem import ( get_filelist, make_socket_file, @@ -61,50 +59,6 @@ def _fake_session_get(*args, **kwargs): rmtree(temp_dir) -@patch('pip._internal.operations.prepare.unpack_file') -def test_unpack_http_url_bad_downloaded_checksum(mock_unpack_file): - """ - If already-downloaded file has bad checksum, re-download. - """ - base_url = 'http://www.example.com/somepackage.tgz' - contents = b'downloaded' - download_hash = hashlib.new('sha1', contents) - link = Link(base_url + '#sha1=' + download_hash.hexdigest()) - - session = Mock() - session.get = Mock() - response = session.get.return_value = MockResponse(contents) - response.headers = {'content-type': 'application/x-tar'} - response.url = base_url - downloader = Downloader(session, progress_bar="on") - - download_dir = mkdtemp() - try: - downloaded_file = os.path.join(download_dir, 'somepackage.tgz') - create_file(downloaded_file, 'some contents') - - unpack_http_url( - link, - 'location', - downloader=downloader, - download_dir=download_dir, - hashes=Hashes({'sha1': [download_hash.hexdigest()]}) - ) - - # despite existence of downloaded file with bad hash, downloaded again - session.get.assert_called_once_with( - 'http://www.example.com/somepackage.tgz', - headers={"Accept-Encoding": "identity"}, - stream=True, - ) - # cached file is replaced with newly downloaded file - with open(downloaded_file) as fh: - assert fh.read() == 'downloaded' - - finally: - rmtree(download_dir) - - def test_download_http_url__no_directory_traversal(tmpdir): """ Test that directory traversal doesn't happen on download when the From 32cabbf716bdea6829f7cd9e2779df91aac594d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Sat, 14 Dec 2019 12:45:04 +0100 Subject: [PATCH 0925/3170] Add test for pip wheel with non-absolute cache dir --- tests/functional/test_wheel.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/functional/test_wheel.py b/tests/functional/test_wheel.py index 00959f65dd6..87083958196 100644 --- a/tests/functional/test_wheel.py +++ b/tests/functional/test_wheel.py @@ -98,6 +98,18 @@ def test_basic_pip_wheel_downloads_wheels(script, data): assert "Saved" in result.stdout, result.stdout +def test_pip_wheel_build_relative_cachedir(script, data): + """ + Test 'pip wheel' builds and caches with a non-absolute cache directory. + """ + result = script.pip( + 'wheel', '--no-index', '-f', data.find_links, + '--cache-dir', './cache', + 'simple==3.0', + ) + assert result.returncode == 0 + + def test_pip_wheel_builds_when_no_binary_set(script, data): data.packages.joinpath('simple-3.0-py2.py3-none-any.whl').touch() # Check that the wheel package is ignored From b9faa61c8f55223a769284f0ccb06c338ae2c2f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Fri, 13 Dec 2019 22:06:32 +0100 Subject: [PATCH 0926/3170] Remove incorrect os.path.join in WheelBuilder The join is done in _build_one_inside_env. The bug as undetected because the cache directory is absolute most of the time. --- src/pip/_internal/wheel_builder.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index 48208f99088..9665cb2b4e4 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -476,10 +476,7 @@ def build( # copy from cache to target directory try: ensure_dir(self._wheel_dir) - shutil.copy( - os.path.join(cache_dir, wheel_file), - self._wheel_dir, - ) + shutil.copy(wheel_file, self._wheel_dir) except OSError as e: logger.warning( "Building wheel for %s failed: %s", From c0d05dc6a32ee8636b7f1f3cdb6378ca6374f0f1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Fri, 13 Dec 2019 22:10:58 +0100 Subject: [PATCH 0927/3170] Make _collect_buildset a standalone function --- src/pip/_internal/wheel_builder.py | 67 ++++++++++++++++-------------- 1 file changed, 36 insertions(+), 31 deletions(-) diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index 9665cb2b4e4..94691c0cb50 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -263,6 +263,37 @@ def _build_wheel_pep517( return os.path.join(tempd, wheel_name) +def _collect_buildset( + requirements, # type: Iterable[InstallRequirement] + wheel_cache, # type: WheelCache + check_binary_allowed, # type: BinaryAllowedPredicate + need_wheel, # type: bool +): + # type: (...) -> List[Tuple[InstallRequirement, str]] + """Return the list of InstallRequirement that need to be built, + with the persistent or temporary cache directory where the built + wheel needs to be stored. + """ + buildset = [] + cache_available = bool(wheel_cache.cache_dir) + for req in requirements: + if not should_build( + req, + need_wheel=need_wheel, + check_binary_allowed=check_binary_allowed, + ): + continue + if ( + cache_available and + should_cache(req, check_binary_allowed) + ): + cache_dir = wheel_cache.get_path_for_link(req.link) + else: + cache_dir = wheel_cache.get_ephem_path_for_link(req.link) + buildset.append((req, cache_dir)) + return buildset + + def _always_true(_): # type: (Any) -> bool return True @@ -367,35 +398,6 @@ def _clean_one(self, req): logger.error('Failed cleaning build dir for %s', req.name) return False - def _collect_buildset( - self, - requirements, # type: Iterable[InstallRequirement] - need_wheel, # type: bool - ): - # type: (...) -> List[Tuple[InstallRequirement, str]] - """Return the list of InstallRequirement that need to be built, - with the persistent or temporary cache directory where the built - wheel needs to be stored. - """ - buildset = [] - cache_available = bool(self.wheel_cache.cache_dir) - for req in requirements: - if not should_build( - req, - need_wheel=need_wheel, - check_binary_allowed=self.check_binary_allowed, - ): - continue - if ( - cache_available and - should_cache(req, self.check_binary_allowed) - ): - cache_dir = self.wheel_cache.get_path_for_link(req.link) - else: - cache_dir = self.wheel_cache.get_ephem_path_for_link(req.link) - buildset.append((req, cache_dir)) - return buildset - def build( self, requirements, # type: Iterable[InstallRequirement] @@ -418,8 +420,11 @@ def build( (not should_unpack and self._wheel_dir) ) - buildset = self._collect_buildset( - requirements, need_wheel=not should_unpack + buildset = _collect_buildset( + requirements, + wheel_cache=self.wheel_cache, + check_binary_allowed=self.check_binary_allowed, + need_wheel=not should_unpack, ) if not buildset: return [] From 510970968f741b2fca103037855faaf83d0a85b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Fri, 13 Dec 2019 22:15:53 +0100 Subject: [PATCH 0928/3170] Remove unused WheelBuilder attribute Remnant of a reverted feature. --- src/pip/_internal/wheel_builder.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index 94691c0cb50..fd850c17a23 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -32,7 +32,7 @@ if MYPY_CHECK_RUNNING: from typing import ( - Any, Callable, Iterable, List, Optional, Pattern, Text, Tuple, Union, + Any, Callable, Iterable, List, Optional, Pattern, Text, Tuple, ) from pip._internal.cache import WheelCache @@ -323,8 +323,6 @@ def __init__( self.build_options = build_options or [] self.global_options = global_options or [] self.check_binary_allowed = check_binary_allowed - # file names of built wheel names - self.wheel_filenames = [] # type: List[Union[bytes, Text]] def _build_one( self, From fedde5fa2cbafc70441ebf23a9b50c33843d3d26 Mon Sep 17 00:00:00 2001 From: Christopher Hunt <chrahunt@gmail.com> Date: Sat, 14 Dec 2019 23:19:25 +0800 Subject: [PATCH 0929/3170] Do not cleanup archive download tempdir immediately (#7479) * Do not cleanup download tempdir immediately The previous logic forced us to handle populating the download directory in this function right next to the download and hash checking. By extending the lifetime of the directory we can more easily separate the code. This also allows for additional optimizations later: by using metadata from wheels directly instead of unpacking them, we can avoid extracting wheels unnecessarily. Unpacked files can be easily 3x larger than the archives themselves, so this should reduce disk utilization and general IO significantly. --- src/pip/_internal/operations/prepare.py | 46 ++++++++++++------------- 1 file changed, 23 insertions(+), 23 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 1e1d8fc3e4e..47c8717830e 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -144,32 +144,32 @@ def unpack_http_url( hashes=None, # type: Optional[Hashes] ): # type: (...) -> None - 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 - ) + temp_dir = TempDirectory(kind="unpack", globally_managed=True) + # If a download dir is specified, is the file already downloaded there? + already_downloaded_path = None + if download_dir: + already_downloaded_path = _check_download_dir( + link, download_dir, hashes + ) - if already_downloaded_path: - from_path = already_downloaded_path - content_type = mimetypes.guess_type(from_path)[0] - else: - # let's download to a tmp dir - from_path, content_type = _download_http_url( - link, downloader, temp_dir.path, hashes - ) + if already_downloaded_path: + from_path = already_downloaded_path + content_type = mimetypes.guess_type(from_path)[0] + else: + # let's download to a tmp dir + from_path, content_type = _download_http_url( + link, downloader, temp_dir.path, hashes + ) - # unpack the archive to the build dir location. even when only - # downloading archives, they have to be unpacked to parse dependencies - unpack_file(from_path, location, content_type) + # unpack the archive to the build dir location. even when only + # downloading archives, they have to be unpacked to parse dependencies + unpack_file(from_path, location, content_type) - # a download dir is specified; let's copy the archive there - if download_dir and not os.path.exists( - os.path.join(download_dir, link.filename) - ): - _copy_file(from_path, download_dir, link) + # a download dir is specified; let's copy the archive there + if download_dir and not os.path.exists( + os.path.join(download_dir, link.filename) + ): + _copy_file(from_path, download_dir, link) def _copy2_ignoring_special_files(src, dest): From 3f912ad48e06915723619f6395074ee8bd6c88f5 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 14 Dec 2019 11:10:36 -0500 Subject: [PATCH 0930/3170] Add function to install directly from wheel files This will help us avoid some complicated directory-changing logic in WheelBuilder. --- src/pip/_internal/operations/install/wheel.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 028ed3df7c6..5893ad64d2e 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -28,7 +28,9 @@ from pip._internal.exceptions import InstallationError, UnsupportedWheel from pip._internal.locations import get_major_minor_version from pip._internal.utils.misc import captured_stdout, ensure_dir, hash_file +from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from pip._internal.utils.unpacking import unpack_file if MYPY_CHECK_RUNNING: from typing import ( @@ -618,6 +620,27 @@ def is_entrypoint_wrapper(name): shutil.move(temp_record, record) +def install_wheel( + name, # type: str + wheel_path, # type: str + scheme, # type: Scheme + req_description, # type: str + pycompile=True, # type: bool + warn_script_location=True, # type: bool +): + # type: (...) -> None + with TempDirectory(kind="unpacked-wheel") as unpacked_dir: + unpack_file(wheel_path, unpacked_dir) + install_unpacked_wheel( + name=name, + wheeldir=unpacked_dir, + scheme=scheme, + req_description=req_description, + pycompile=pycompile, + warn_script_location=warn_script_location, + ) + + def wheel_version(source_dir): # type: (Optional[str]) -> Optional[Tuple[int, ...]] """Return the Wheel-Version of an extracted wheel, if possible. From c565d7a1b2e74a229edfc11087d2b8f5f1a86f3a Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 14 Dec 2019 11:21:00 -0500 Subject: [PATCH 0931/3170] Switch to install_wheel in unit tests Since it tests install_unpacked_wheel, the coverage should be the same. --- src/pip/_internal/operations/install/wheel.py | 9 ++++++--- tests/unit/test_wheel.py | 14 +++++++------- 2 files changed, 13 insertions(+), 10 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 5893ad64d2e..627cc4a32a6 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -627,13 +627,16 @@ def install_wheel( req_description, # type: str pycompile=True, # type: bool warn_script_location=True, # type: bool + _temp_dir_for_testing=None, # type: Optional[str] ): # type: (...) -> None - with TempDirectory(kind="unpacked-wheel") as unpacked_dir: - unpack_file(wheel_path, unpacked_dir) + with TempDirectory( + path=_temp_dir_for_testing, kind="unpacked-wheel" + ) as unpacked_dir: + unpack_file(wheel_path, unpacked_dir.path) install_unpacked_wheel( name=name, - wheeldir=unpacked_dir, + wheeldir=unpacked_dir.path, scheme=scheme, req_description=req_description, pycompile=pycompile, diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 4bcbcbee2a2..52e4e15aaed 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -260,7 +260,6 @@ def prep(self, data, tmpdir): self.req = Requirement('sample') self.src = os.path.join(tmpdir, 'src') self.dest = os.path.join(tmpdir, 'dest') - unpack_file(self.wheelpath, self.src) self.scheme = Scheme( purelib=os.path.join(self.dest, 'lib'), platlib=os.path.join(self.dest, 'lib'), @@ -290,9 +289,9 @@ def assert_installed(self): def test_std_install(self, data, tmpdir): self.prep(data, tmpdir) - wheel.install_unpacked_wheel( + wheel.install_wheel( self.name, - self.src, + self.wheelpath, scheme=self.scheme, req_description=str(self.req), ) @@ -309,9 +308,9 @@ def test_install_prefix(self, data, tmpdir): isolated=False, prefix=prefix, ) - wheel.install_unpacked_wheel( + wheel.install_wheel( self.name, - self.src, + self.wheelpath, scheme=scheme, req_description=str(self.req), ) @@ -330,11 +329,12 @@ def test_dist_info_contains_empty_dir(self, data, tmpdir): self.src_dist_info, 'empty_dir', 'empty_dir') os.makedirs(src_empty_dir) assert os.path.isdir(src_empty_dir) - wheel.install_unpacked_wheel( + wheel.install_wheel( self.name, - self.src, + self.wheelpath, scheme=self.scheme, req_description=str(self.req), + _temp_dir_for_testing=self.src, ) self.assert_installed() assert not os.path.isdir( From 327c295554568af4e897c096190623fc1c198f85 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 14 Dec 2019 11:22:00 -0500 Subject: [PATCH 0932/3170] Keep path to downloaded archive on InstallRequirement Now we'll be able to transition other parts of the code to use pre-existing archives directly instead of relying on unpacked sources. --- src/pip/_internal/operations/prepare.py | 24 +++++++++++++++++------- src/pip/_internal/req/req_install.py | 4 ++++ src/pip/_internal/wheel_builder.py | 1 + 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 47c8717830e..f2fc26d58f6 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -143,7 +143,7 @@ def unpack_http_url( download_dir=None, # type: Optional[str] hashes=None, # type: Optional[Hashes] ): - # type: (...) -> None + # type: (...) -> str temp_dir = TempDirectory(kind="unpack", globally_managed=True) # If a download dir is specified, is the file already downloaded there? already_downloaded_path = None @@ -171,6 +171,8 @@ def unpack_http_url( ): _copy_file(from_path, download_dir, link) + return from_path + def _copy2_ignoring_special_files(src, dest): # type: (str, str) -> None @@ -219,7 +221,7 @@ def unpack_file_url( download_dir=None, # type: Optional[str] hashes=None # type: Optional[Hashes] ): - # type: (...) -> None + # type: (...) -> Optional[str] """Unpack link into location. If download_dir is provided and link points to a file, make a copy @@ -233,7 +235,7 @@ def unpack_file_url( _copy_source_tree(link_path, location) if download_dir: logger.info('Link is a directory, ignoring download_dir') - return + return None # If --require-hashes is off, `hashes` is either empty, the # link's embedded hash, or MissingHashes; it is required to @@ -267,6 +269,8 @@ def unpack_file_url( ): _copy_file(from_path, download_dir, link) + return from_path + def unpack_url( link, # type: Link @@ -275,7 +279,7 @@ def unpack_url( download_dir=None, # type: Optional[str] hashes=None, # type: Optional[Hashes] ): - # type: (...) -> None + # type: (...) -> Optional[str] """Unpack link. If link is a VCS link: if only_download, export into download_dir and ignore location @@ -293,14 +297,15 @@ def unpack_url( # non-editable vcs urls if link.is_vcs: unpack_vcs_link(link, location) + return None # file urls elif link.is_file: - unpack_file_url(link, location, download_dir, hashes=hashes) + return unpack_file_url(link, location, download_dir, hashes=hashes) # http urls else: - unpack_http_url( + return unpack_http_url( link, location, downloader, @@ -500,7 +505,7 @@ def prepare_linked_requirement( download_dir = self.wheel_download_dir try: - unpack_url( + local_path = unpack_url( link, req.source_dir, self.downloader, download_dir, hashes=hashes, ) @@ -515,6 +520,11 @@ def prepare_linked_requirement( 'error {} for URL {}'.format(req, exc, link) ) + # For use in later processing, preserve the file path on the + # requirement. + if local_path: + req.local_file_path = local_path + if link.is_wheel: if download_dir: # When downloading, we only unpack wheels to get diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 059dc04e881..0671d7eb8e4 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -137,6 +137,10 @@ def __init__( # PEP 508 URL requirement link = Link(req.url) self.link = self.original_link = link + # Path to any downloaded or already-existing package. + self.local_file_path = None # type: Optional[str] + if self.link and self.link.is_file: + self.local_file_path = self.link.file_path if extras: self.extras = extras diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index 4e191061bb3..8a51c727582 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -460,6 +460,7 @@ def build( ) # Update the link for this. req.link = Link(path_to_url(wheel_file)) + req.local_file_path = req.link.file_path assert req.link.is_wheel # extract the wheel into the dir unpack_file(req.link.file_path, req.source_dir) From 11472e1b12f8c883d9326fecd5c9d4e0373c9b7e Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 14 Dec 2019 11:23:19 -0500 Subject: [PATCH 0933/3170] Switch to install_wheel in InstallRequirement This removes one of usages of the overloaded `source_dir` member. We know the local_file_path must be set at this point because it is set in the only 3 places that wheels are added to InstallRequirement. --- src/pip/_internal/req/req_install.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 0671d7eb8e4..cd0563c529e 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -27,7 +27,7 @@ generate_metadata as generate_metadata_legacy from pip._internal.operations.install.editable_legacy import \ install as install_editable_legacy -from pip._internal.operations.install.wheel import install_unpacked_wheel +from pip._internal.operations.install.wheel import install_wheel from pip._internal.pyproject import load_pyproject_toml, make_pyproject_path from pip._internal.req.req_uninstall import UninstallPathSet from pip._internal.utils.deprecation import deprecated @@ -774,9 +774,10 @@ def install( return if self.is_wheel: - install_unpacked_wheel( + assert self.local_file_path + install_wheel( self.name, - self.source_dir, + self.local_file_path, scheme=scheme, req_description=str(self.req), pycompile=pycompile, From f60d15ca87ad791e5d6f74c608d2a1b0a254f87e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Sat, 14 Dec 2019 13:25:28 +0100 Subject: [PATCH 0934/3170] Add missing error report Use the same error message as in WheelBuilder.build(), include the exception in the message. --- src/pip/_internal/wheel_builder.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index fd850c17a23..eaea7bcf9b0 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -375,7 +375,11 @@ def _build_one_inside_env( wheel_hash.hexdigest()) logger.info('Stored in directory: %s', output_dir) return dest_path - except Exception: + except Exception as e: + logger.warning( + "Building wheel for %s failed: %s", + req.name, e, + ) pass # Ignore return, we can't do anything else useful. self._clean_one(req) From 5de1d5161039d0e7ae51a488d41491d40d4f74ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Sat, 14 Dec 2019 13:27:57 +0100 Subject: [PATCH 0935/3170] Ensure output directory is present inside _build_one --- src/pip/_internal/wheel_builder.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index eaea7bcf9b0..87452f6949d 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -367,6 +367,7 @@ def _build_one_inside_env( wheel_name = os.path.basename(wheel_path) dest_path = os.path.join(output_dir, wheel_name) try: + ensure_dir(output_dir) wheel_hash, length = hash_file(wheel_path) shutil.move(wheel_path, dest_path) logger.info('Created wheel for %s: ' From 2f125342e04c3a984beff21b2b2a108b4ed267cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Sat, 14 Dec 2019 13:29:04 +0100 Subject: [PATCH 0936/3170] Remove now redundant ensure_dir Since _build_one ensure the output dir is present and does proper error logging, we can remove ensure_dir from build(), further simplifying that method. --- src/pip/_internal/wheel_builder.py | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index 87452f6949d..2dc4313d4d9 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -444,16 +444,6 @@ def build( with indent_log(): build_success, build_failure = [], [] for req, cache_dir in buildset: - try: - ensure_dir(cache_dir) - except OSError as e: - logger.warning( - "Building wheel for %s failed: %s", - req.name, e, - ) - build_failure.append(req) - continue - wheel_file = self._build_one(req, cache_dir) if wheel_file: if should_unpack: From cdee9e2c331f8f16118e2e2d0ea403cdce8b8c27 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 15 Dec 2019 04:00:07 +0530 Subject: [PATCH 0937/3170] Add configuration for using vendoring --- pyproject.toml | 39 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/pyproject.toml b/pyproject.toml index aa798360c5b..645d54a7550 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -18,3 +18,42 @@ type = [ { name = "Improved Documentation", directory = "doc", showcontent = true }, { name = "Trivial Changes", directory = "trivial", showcontent = false }, ] + +[tool.vendoring] +destination = "src/pip/_vendor/" +requirements = "src/pip/_vendor/vendor.txt" +namespace = "pip._vendor" + +protected-files = ["__init__.py", "README.rst", "vendor.txt"] +patches-dir = "tools/automation/vendoring/patches" + +[tool.vendoring.transformations] +substitute = [ + # pkg_resource's vendored packages are directly vendored in pip. + { match='pkg_resources\.extern', replace='pip._vendor' }, + { match='from \.extern', replace='from pip._vendor' }, +] +drop = [ + # contains unnecessary scripts + "bin/", + # interpreter and OS specific msgpack libs + "msgpack/*.so", + # unneeded parts of setuptools + "easy_install.py", + "setuptools", + "pkg_resources/_vendor/", + "pkg_resources/extern/", +] + +[tool.vendoring.typing-stubs] +six = ["six.__init__", "six.moves.__init__", "six.moves.configparser"] +appdirs = [] +contextlib2 = [] + +[tool.vendoring.license.directories] +setuptools = "pkg_resources" +msgpack-python = "msgpack" + +[tool.vendoring.license.fallback-urls] +pytoml = "https://github.com/avakar/pytoml/raw/master/LICENSE" +webencodings = "https://github.com/SimonSapin/python-webencodings/raw/master/LICENSE" From 248f6b2fc6be8fec63bdbce7dd7882bbafc57b20 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 15 Dec 2019 04:00:25 +0530 Subject: [PATCH 0938/3170] Switch tox -e vendoring, to use vendoring --- tox.ini | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tox.ini b/tox.ini index de1263fc47c..5f9b6f4284b 100644 --- a/tox.ini +++ b/tox.ini @@ -48,16 +48,16 @@ commands = pre-commit run [] --all-files --show-diff-on-failure [testenv:vendoring] +basepython = python3.8 skip_install = True commands_pre = deps = - invoke - requests + vendoring == 0.2.2 # Required, otherwise we interpret --no-binary :all: as # "do not build wheels", which fails for PEP 517 requirements pip>=19.3.1 whitelist_externals = git commands = # Check that the vendoring is up-to-date - invoke vendoring.update + vendoring sync . -v git diff --exit-code From 53aaa3e40bcdddf3d4b816de5365aa7534ceefdd Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 15 Dec 2019 04:01:33 +0530 Subject: [PATCH 0939/3170] Drop invoke task for vendoring dependencies This is no longer needed, since `vendoring` does what we need here. --- tasks/__init__.py | 4 - tools/automation/vendoring/__init__.py | 282 ------------------------- tools/automation/vendoring/typing.py | 59 ------ 3 files changed, 345 deletions(-) delete mode 100644 tasks/__init__.py delete mode 100644 tools/automation/vendoring/__init__.py delete mode 100644 tools/automation/vendoring/typing.py diff --git a/tasks/__init__.py b/tasks/__init__.py deleted file mode 100644 index 9591fb9ef05..00000000000 --- a/tasks/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -import invoke -from tools.automation import vendoring - -ns = invoke.Collection(vendoring) diff --git a/tools/automation/vendoring/__init__.py b/tools/automation/vendoring/__init__.py deleted file mode 100644 index 1c2ae3a6948..00000000000 --- a/tools/automation/vendoring/__init__.py +++ /dev/null @@ -1,282 +0,0 @@ -""""Vendoring script, python 3.5 with requests needed""" - -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - -import re -import shutil -import tarfile -import zipfile -from pathlib import Path - -import invoke -import requests - -from .typing import generate_stubs - -FILE_WHITE_LIST = ( - 'Makefile', - 'vendor.txt', - '__init__.py', - 'README.rst', -) - -# libraries that have directories with different names -LIBRARY_DIRNAMES = { - 'setuptools': 'pkg_resources', - 'msgpack-python': 'msgpack', -} - -# from time to time, remove the no longer needed ones -HARDCODED_LICENSE_URLS = { - 'pytoml': 'https://github.com/avakar/pytoml/raw/master/LICENSE', - 'webencodings': 'https://github.com/SimonSapin/python-webencodings/raw/' - 'master/LICENSE', -} - - -def drop_dir(path, **kwargs): - shutil.rmtree(str(path), **kwargs) - - -def remove_all(paths): - for path in paths: - if path.is_dir(): - drop_dir(path) - else: - path.unlink() - - -def log(msg): - print('[vendoring.update] ' + 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')) - log('Cleaning %s' % vendor_dir) - for item in vendor_dir.iterdir(): - if item.is_dir(): - shutil.rmtree(str(item)) - elif item.name not in FILE_WHITE_LIST: - item.unlink() - else: - 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 "LICENSE" in item.name or "COPYING" in item.name: - 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(): - rewrite_imports(item, vendored_libs) - elif item.name.endswith('.py'): - rewrite_file_imports(item, vendored_libs) - - -def rewrite_file_imports(item, vendored_libs): - """Rewrite 'import xxx' and 'from xxx import' for vendored_libs""" - text = item.read_text(encoding='utf-8') - # Revendor pkg_resources.extern first - text = re.sub(r'pkg_resources\.extern', r'pip._vendor', text) - text = re.sub(r'from \.extern', r'from pip._vendor', text) - for lib in vendored_libs: - text = re.sub( - r'(\n\s*|^)import %s(\n\s*)' % lib, - r'\1from pip._vendor import %s\2' % lib, - text, - ) - text = re.sub( - r'(\n\s*|^)from %s(\.|\s+)' % lib, - r'\1from pip._vendor.%s\2' % lib, - text, - ) - item.write_text(text, encoding='utf-8') - - -def apply_patch(ctx, patch_file_path): - log('Applying patch %s' % patch_file_path.name) - ctx.run('git apply --verbose %s' % patch_file_path) - - -def vendor(ctx, vendor_dir): - log('Reinstalling vendored libraries') - # We use --no-deps because we want to ensure that all of our dependencies - # are added to vendor.txt, this includes all dependencies recursively up - # the chain. - ctx.run( - 'pip install -t {0} -r {0}/vendor.txt --no-compile --no-deps'.format( - str(vendor_dir), - ) - ) - remove_all(vendor_dir.glob('*.dist-info')) - remove_all(vendor_dir.glob('*.egg-info')) - - # Cleanup setuptools unneeded parts - (vendor_dir / 'easy_install.py').unlink() - drop_dir(vendor_dir / 'setuptools') - drop_dir(vendor_dir / 'pkg_resources' / '_vendor') - drop_dir(vendor_dir / 'pkg_resources' / 'extern') - - # Drop the bin directory (contains easy_install, distro, chardetect etc.) - # Might not appear on all OSes, so ignoring errors - drop_dir(vendor_dir / 'bin', ignore_errors=True) - - # Drop interpreter and OS specific msgpack libs. - # Pip will rely on the python-only fallback instead. - remove_all(vendor_dir.glob('msgpack/*.so')) - - # Detect the vendored packages/modules - vendored_libs = detect_vendored_libs(vendor_dir) - log("Detected vendored libraries: %s" % ", ".join(vendored_libs)) - - # Global import rewrites - log("Rewriting all imports related to vendored libs") - for item in vendor_dir.iterdir(): - if item.is_dir(): - rewrite_imports(item, vendored_libs) - elif item.name not in FILE_WHITE_LIST: - rewrite_file_imports(item, vendored_libs) - - # Special cases: apply stored patches - log("Apply patches") - patch_dir = Path(__file__).parent / 'patches' - for patch in patch_dir.glob('*.patch'): - apply_patch(ctx, patch) - - -def download_licenses(ctx, vendor_dir): - log('Downloading licenses') - tmp_dir = vendor_dir / '__tmp__' - ctx.run( - 'pip download -r {0}/vendor.txt --no-binary ' - ':all: --no-deps -d {1}'.format( - str(vendor_dir), - str(tmp_dir), - ) - ) - for sdist in tmp_dir.iterdir(): - extract_license(vendor_dir, sdist) - drop_dir(tmp_dir) - - -def extract_license(vendor_dir, sdist): - if sdist.suffixes[-2] == '.tar': - ext = sdist.suffixes[-1][1:] - with tarfile.open(sdist, mode='r:{}'.format(ext)) as tar: - found = find_and_extract_license(vendor_dir, tar, tar.getmembers()) - elif sdist.suffixes[-1] == '.zip': - with zipfile.ZipFile(sdist) as zip: - found = find_and_extract_license(vendor_dir, zip, zip.infolist()) - else: - raise NotImplementedError('new sdist type!') - - if not found: - log('License not found in {}, will download'.format(sdist.name)) - license_fallback(vendor_dir, sdist.name) - - -def find_and_extract_license(vendor_dir, tar, members): - found = False - for member in members: - try: - name = member.name - except AttributeError: # zipfile - name = member.filename - if 'LICENSE' in name or 'COPYING' in name: - if '/test' in name: - # some testing licenses in html5lib and distlib - log('Ignoring {}'.format(name)) - continue - found = True - extract_license_member(vendor_dir, tar, member, name) - return found - - -def license_fallback(vendor_dir, sdist_name): - """Hardcoded license URLs. Check when updating if those are still needed""" - libname = libname_from_dir(sdist_name) - if libname not in HARDCODED_LICENSE_URLS: - raise ValueError('No hardcoded URL for {} license'.format(libname)) - - url = HARDCODED_LICENSE_URLS[libname] - _, _, name = url.rpartition('/') - dest = license_destination(vendor_dir, libname, name) - log('Downloading {}'.format(url)) - r = requests.get(url, allow_redirects=True) - r.raise_for_status() - dest.write_bytes(r.content) - - -def libname_from_dir(dirname): - """Reconstruct the library name without it's version""" - parts = [] - for part in dirname.split('-'): - if part[0].isdigit(): - break - parts.append(part) - return '-'.join(parts) - - -def license_destination(vendor_dir, libname, filename): - """Given the (reconstructed) library name, find appropriate destination""" - normal = vendor_dir / libname - if normal.is_dir(): - return normal / filename - lowercase = vendor_dir / libname.lower() - if lowercase.is_dir(): - return lowercase / filename - if libname in LIBRARY_DIRNAMES: - return vendor_dir / LIBRARY_DIRNAMES[libname] / filename - # fallback to libname.LICENSE (used for nondirs) - return vendor_dir / '{}.{}'.format(libname, filename) - - -def extract_license_member(vendor_dir, tar, member, name): - mpath = Path(name) # relative path inside the sdist - dirname = list(mpath.parents)[-2].name # -1 is . - libname = libname_from_dir(dirname) - dest = license_destination(vendor_dir, libname, mpath.name) - dest_relative = dest.relative_to(Path.cwd()) - log('Extracting {} into {}'.format(name, dest_relative)) - try: - fileobj = tar.extractfile(member) - dest.write_bytes(fileobj.read()) - except AttributeError: # zipfile - dest.write_bytes(tar.read(member)) - - -@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") - generate_stubs(vendor_dir, vendored_libs) - - -@invoke.task(name="update", post=[update_stubs]) -def main(ctx): - vendor_dir = _get_vendor_dir(ctx) - log('Using vendor dir: %s' % vendor_dir) - clean_vendor(ctx, vendor_dir) - vendor(ctx, vendor_dir) - download_licenses(ctx, vendor_dir) - log('Revendoring complete') diff --git a/tools/automation/vendoring/typing.py b/tools/automation/vendoring/typing.py deleted file mode 100644 index 35f7e0bfff0..00000000000 --- a/tools/automation/vendoring/typing.py +++ /dev/null @@ -1,59 +0,0 @@ -"""Logic for adding static typing related stubs of vendored dependencies. - -We autogenerate `.pyi` stub files for the vendored modules, when vendoring. -These .pyi files are not distributed (thanks to MANIFEST.in). The stub files -are merely `from ... import *` but they do what they're supposed to and mypy -is able to find the correct declarations using these files. -""" - -import os -from pathlib import Path -from typing import Dict, Iterable, List, Tuple - -EXTRA_STUBS_NEEDED = { - # Some projects need stubs other than a simple <name>.pyi - "six": [ - "six.__init__", - "six.moves.__init__", - "six.moves.configparser", - ], - # Some projects should not have stubs because they're a single module - "appdirs": [], - "contextlib2": [], -} # type: Dict[str, List[str]] - - -def determine_stub_files(lib): - # type: (str) -> Iterable[Tuple[str, str]] - # There's no special handling needed -- a <libname>.pyi file is good enough - if lib not in EXTRA_STUBS_NEEDED: - yield lib + ".pyi", lib - return - - # Need to generate the given stubs, with the correct import names - for import_name in EXTRA_STUBS_NEEDED[lib]: - rel_location = import_name.replace(".", os.sep) + ".pyi" - - # Writing an __init__.pyi file -> don't import from `pkg.__init__` - if import_name.endswith(".__init__"): - import_name = import_name[:-9] - - yield rel_location, import_name - - -def write_stub(destination, import_name): - # type: (Path, str) -> None - # Create the parent directories if needed. - if not destination.parent.exists(): - destination.parent.mkdir() - - # Write `from ... import *` in the stub file. - destination.write_text("from %s import *" % import_name) - - -def generate_stubs(vendor_dir, libraries): - # type: (Path, List[str]) -> None - for lib in libraries: - for rel_location, import_name in determine_stub_files(lib): - destination = vendor_dir / rel_location - write_stub(destination, import_name) From 81805a5776767b17533afc934121968f3c03a4d9 Mon Sep 17 00:00:00 2001 From: victorvpaulo <victorvpaulo@gmail.com> Date: Sat, 14 Dec 2019 22:59:34 -0300 Subject: [PATCH 0940/3170] Add option to silence warnings related to deprecation of Python versions (#6739) * Add option to silence warnings related to deprecation of Python versions * Move skip_if_python2 and skip_if_not_python2 decorator declaratios to test/lib/__init__.py and use them in test_warning.py * Add tests to ensure that python version deprecation warning is shown correctly and can be silenced by a flag. * Add new test to ensure that --no-python-version-warning flag does nothing if python version is not 2 --- news/6673.feature | 2 ++ src/pip/_internal/cli/base_command.py | 5 +++- src/pip/_internal/cli/cmdoptions.py | 11 ++++++++ tests/functional/test_install.py | 6 ++--- tests/functional/test_warning.py | 39 +++++++++++++++++++++++++++ tests/lib/__init__.py | 5 ++++ 6 files changed, 63 insertions(+), 5 deletions(-) create mode 100644 news/6673.feature diff --git a/news/6673.feature b/news/6673.feature new file mode 100644 index 00000000000..829bb232859 --- /dev/null +++ b/news/6673.feature @@ -0,0 +1,2 @@ +Add option ``--no-python-version-warning`` to silence warnings +related to deprecation of Python versions. diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index 47c6926e46a..6fbc26c0f55 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -122,7 +122,10 @@ def _main(self, args): user_log_file=options.log, ) - if sys.version_info[:2] == (2, 7): + if ( + sys.version_info[:2] == (2, 7) and + not options.no_python_version_warning + ): message = ( "A future version of pip will drop support for Python 2.7. " "More details about Python 2 support in pip, can be found at " diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index f14c7d7eeca..b3db456e4e3 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -891,6 +891,16 @@ def check_list_path_option(options): ) +no_python_version_warning = partial( + Option, + '--no-python-version-warning', + dest='no_python_version_warning', + action='store_true', + default=False, + help='Silence deprecation warnings for upcoming unsupported Pythons.', +) # type: Callable[..., Option] + + ########## # groups # ########## @@ -918,6 +928,7 @@ def check_list_path_option(options): no_cache, disable_pip_version_check, no_color, + no_python_version_warning, ] } # type: Dict[str, Any] diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 6b62d863545..8ae64319d1e 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -9,7 +9,6 @@ from os.path import curdir, join, pardir import pytest -from pip._vendor.six import PY2 from pip import __version__ as pip_current_version from pip._internal.cli.status_codes import ERROR, SUCCESS @@ -27,6 +26,8 @@ pyversion, pyversion_tuple, requirements_file, + skip_if_not_python2, + skip_if_python2, ) from tests.lib.filesystem import make_socket_file from tests.lib.local_repos import local_checkout @@ -38,9 +39,6 @@ server_running, ) -skip_if_python2 = pytest.mark.skipif(PY2, reason="Non-Python 2 only") -skip_if_not_python2 = pytest.mark.skipif(not PY2, reason="Python 2 only") - @pytest.mark.parametrize('command', ('install', 'wheel')) @pytest.mark.parametrize('variant', ('missing_setuptools', 'bad_setuptools')) diff --git a/tests/functional/test_warning.py b/tests/functional/test_warning.py index 4a329475012..ff228421e66 100644 --- a/tests/functional/test_warning.py +++ b/tests/functional/test_warning.py @@ -1,7 +1,10 @@ +import platform import textwrap import pytest +from tests.lib import skip_if_not_python2, skip_if_python2 + @pytest.fixture def warnings_demo(tmpdir): @@ -28,3 +31,39 @@ def test_deprecation_warnings_can_be_silenced(script, warnings_demo): script.environ['PYTHONWARNINGS'] = 'ignore' result = script.run('python', warnings_demo) assert result.stderr == '' + + +DEPRECATION_TEXT = "drop support for Python 2.7" +CPYTHON_DEPRECATION_TEXT = "January 1st, 2020" + + +@skip_if_python2 +def test_version_warning_is_not_shown_if_python_version_is_not_2(script): + result = script.pip("debug", allow_stderr_warning=True) + assert DEPRECATION_TEXT not in result.stderr, str(result) + assert CPYTHON_DEPRECATION_TEXT not in result.stderr, str(result) + + +@skip_if_python2 +def test_flag_does_nothing_if_python_version_is_not_2(script): + script.pip("list", "--no-python-version-warning") + + +@skip_if_not_python2 +def test_version_warning_is_shown_if_python_version_is_2(script): + result = script.pip("debug", allow_stderr_warning=True) + assert DEPRECATION_TEXT in result.stderr, str(result) + if platform.python_implementation() == 'CPython': + assert CPYTHON_DEPRECATION_TEXT in result.stderr, str(result) + else: + assert CPYTHON_DEPRECATION_TEXT not in result.stderr, str(result) + + +@skip_if_not_python2 +def test_version_warning_is_not_shown_when_flag_is_passed(script): + result = script.pip( + "debug", "--no-python-version-warning", allow_stderr_warning=True + ) + assert DEPRECATION_TEXT not in result.stderr, str(result) + assert CPYTHON_DEPRECATION_TEXT not in result.stderr, str(result) + assert "--no-python-version-warning" not in result.stderr diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index 3e3f7e44fc9..07cc0ed5f76 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -11,6 +11,7 @@ from textwrap import dedent import pytest +from pip._vendor.six import PY2 from scripttest import FoundDir, TestFileEnvironment from pip._internal.index.collector import LinkCollector @@ -1047,3 +1048,7 @@ def need_mercurial(fn): return pytest.mark.mercurial(need_executable( 'Mercurial', ('hg', 'version') )(fn)) + + +skip_if_python2 = pytest.mark.skipif(PY2, reason="Non-Python 2 only") +skip_if_not_python2 = pytest.mark.skipif(not PY2, reason="Python 2 only") From 618b2d839391e9041cd9e5430a7ba4d0d6bad073 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 15 Dec 2019 12:34:35 +0530 Subject: [PATCH 0941/3170] :newspaper: --- news/4785.process | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/4785.process diff --git a/news/4785.process b/news/4785.process new file mode 100644 index 00000000000..022510f4b18 --- /dev/null +++ b/news/4785.process @@ -0,0 +1 @@ +Switch to a dedicated CLI tool for vendoring dependencies. From 6929cc6d6ba1b6bda4252832749790ed10e55a71 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 15 Dec 2019 21:22:04 +0530 Subject: [PATCH 0942/3170] Address review comments These were all nitpicks but hey, that's good news. :) --- pyproject.toml | 4 ++-- tox.ini | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 645d54a7550..01fae701523 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,8 +30,8 @@ patches-dir = "tools/automation/vendoring/patches" [tool.vendoring.transformations] substitute = [ # pkg_resource's vendored packages are directly vendored in pip. - { match='pkg_resources\.extern', replace='pip._vendor' }, - { match='from \.extern', replace='from pip._vendor' }, + { match='pkg_resources\.extern', replace="pip._vendor" }, + { match='from \.extern', replace="from pip._vendor" }, ] drop = [ # contains unnecessary scripts diff --git a/tox.ini b/tox.ini index 5f9b6f4284b..6be97232065 100644 --- a/tox.ini +++ b/tox.ini @@ -52,7 +52,7 @@ basepython = python3.8 skip_install = True commands_pre = deps = - vendoring == 0.2.2 + vendoring==0.2.2 # Required, otherwise we interpret --no-binary :all: as # "do not build wheels", which fails for PEP 517 requirements pip>=19.3.1 From 9c74c0214a0a081962273ce1bf0e9931dfa5d6da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Sun, 15 Dec 2019 18:17:27 +0100 Subject: [PATCH 0943/3170] Add failing test for legacy cache keys --- tests/unit/test_cache.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/unit/test_cache.py b/tests/unit/test_cache.py index 8926f3af84e..03382ef6d31 100644 --- a/tests/unit/test_cache.py +++ b/tests/unit/test_cache.py @@ -71,3 +71,21 @@ def test_get_path_for_link_legacy(tmpdir): pass expected_candidates = {"test-pyx-none-any.whl", "test-pyz-none-any.whl"} assert set(wc._get_candidates(link, "test")) == expected_candidates + + +def test_get_with_legacy_entry_only(tmpdir): + """ + Test that an existing cache entry that was created with the legacy hashing + mechanism is actually returned in WheelCache.get(). + """ + wc = WheelCache(tmpdir, FormatControl()) + link = Link("https://g.c/o/r") + legacy_path = wc.get_path_for_link_legacy(link) + ensure_dir(legacy_path) + with open(os.path.join(legacy_path, "test-1.0.0-py3-none-any.whl"), "w"): + pass + cached_link = wc.get(link, "test", [("py3", "none", "any")]) + assert ( + os.path.normcase(os.path.dirname(cached_link.file_path)) == + os.path.normcase(legacy_path) + ) From 80bfba330267a1d12f144454266d228b61697642 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Sun, 15 Dec 2019 18:19:35 +0100 Subject: [PATCH 0944/3170] Improve WheelCache.get test --- tests/unit/test_cache.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_cache.py b/tests/unit/test_cache.py index 03382ef6d31..165e016500e 100644 --- a/tests/unit/test_cache.py +++ b/tests/unit/test_cache.py @@ -39,7 +39,9 @@ def test_wheel_name_filter(tmpdir): with open(os.path.join(cache_path, "package-1.0-py3-none-any.whl"), "w"): pass # package matches wheel name - assert wc.get(link, "package", [("py3", "none", "any")]) is not link + cached_link = wc.get(link, "package", [("py3", "none", "any")]) + assert cached_link is not link + assert os.path.exists(cached_link.file_path) # package2 does not match wheel name assert wc.get(link, "package2", [("py3", "none", "any")]) is link From 36ff8846732a37d99c62ce6baeadcf4f8b0580ea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Sun, 15 Dec 2019 18:20:03 +0100 Subject: [PATCH 0945/3170] Fix WheelCache.get in presence of legacy cache keys --- news/7490.trivial | 1 + src/pip/_internal/cache.py | 27 +++++++++++++++------------ tests/unit/test_cache.py | 13 ++++++++----- 3 files changed, 24 insertions(+), 17 deletions(-) create mode 100644 news/7490.trivial diff --git a/news/7490.trivial b/news/7490.trivial new file mode 100644 index 00000000000..25619f36b8e --- /dev/null +++ b/news/7490.trivial @@ -0,0 +1 @@ +Fix unrelease bug from #7319. diff --git a/src/pip/_internal/cache.py b/src/pip/_internal/cache.py index 8c2041527c1..cde54637b85 100644 --- a/src/pip/_internal/cache.py +++ b/src/pip/_internal/cache.py @@ -138,11 +138,15 @@ def _get_candidates(self, link, canonical_package_name): candidates = [] path = self.get_path_for_link(link) if os.path.isdir(path): - candidates.extend(os.listdir(path)) + for candidate in os.listdir(path): + candidates.append((candidate, os.path.join(path, candidate))) # TODO remove legacy path lookup in pip>=21 legacy_path = self.get_path_for_link_legacy(link) if os.path.isdir(legacy_path): - candidates.extend(os.listdir(legacy_path)) + for candidate in os.listdir(legacy_path): + candidates.append( + (candidate, os.path.join(legacy_path, candidate)) + ) return candidates def get_path_for_link_legacy(self, link): @@ -167,13 +171,6 @@ def get( """ raise NotImplementedError() - def _link_for_candidate(self, link, candidate): - # type: (Link, str) -> Link - root = self.get_path_for_link(link) - path = os.path.join(root, candidate) - - return Link(path_to_url(path)) - def cleanup(self): # type: () -> None pass @@ -228,7 +225,9 @@ def get( return link canonical_package_name = canonicalize_name(package_name) - for wheel_name in self._get_candidates(link, canonical_package_name): + for wheel_name, wheel_path in self._get_candidates( + link, canonical_package_name + ): try: wheel = Wheel(wheel_name) except InvalidWheelFilename: @@ -245,13 +244,17 @@ def get( # Built for a different python/arch/etc continue candidates.append( - (wheel.support_index_min(supported_tags), wheel_name) + ( + wheel.support_index_min(supported_tags), + wheel_name, + wheel_path, + ) ) if not candidates: return link - return self._link_for_candidate(link, min(candidates)[1]) + return Link(path_to_url(min(candidates)[2])) class EphemWheelCache(SimpleWheelCache): diff --git a/tests/unit/test_cache.py b/tests/unit/test_cache.py index 165e016500e..5d81fc01d14 100644 --- a/tests/unit/test_cache.py +++ b/tests/unit/test_cache.py @@ -58,7 +58,7 @@ def test_cache_hash(): def test_get_path_for_link_legacy(tmpdir): """ Test that an existing cache entry that was created with the legacy hashing - mechanism is used. + mechanism is returned by WheelCache._get_candidates(). """ wc = WheelCache(tmpdir, FormatControl()) link = Link("https://g.c/o/r") @@ -66,13 +66,16 @@ def test_get_path_for_link_legacy(tmpdir): legacy_path = wc.get_path_for_link_legacy(link) assert path != legacy_path ensure_dir(path) - with open(os.path.join(path, "test-pyz-none-any.whl"), "w"): + with open(os.path.join(path, "test-1.0.0-pyz-none-any.whl"), "w"): pass ensure_dir(legacy_path) - with open(os.path.join(legacy_path, "test-pyx-none-any.whl"), "w"): + with open(os.path.join(legacy_path, "test-1.0.0-pyx-none-any.whl"), "w"): pass - expected_candidates = {"test-pyx-none-any.whl", "test-pyz-none-any.whl"} - assert set(wc._get_candidates(link, "test")) == expected_candidates + expected_candidates = { + "test-1.0.0-pyx-none-any.whl", "test-1.0.0-pyz-none-any.whl" + } + candidates = {c[0] for c in wc._get_candidates(link, "test")} + assert candidates == expected_candidates def test_get_with_legacy_entry_only(tmpdir): From 45222a2b3672b04dd8ebe30774970caa17f7f5b7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Sun, 15 Dec 2019 12:51:28 +0100 Subject: [PATCH 0946/3170] ensure cache dir is present before building --- src/pip/_internal/wheel_builder.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index 2dc4313d4d9..1937219d07c 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -334,6 +334,15 @@ def _build_one( :return: The filename of the built wheel, or None if the build failed. """ + try: + ensure_dir(output_dir) + except OSError as e: + logger.warning( + "Building wheel for %s failed: %s", + req.name, e, + ) + return None + # Install build deps into temporary directory (PEP 518) with req.build_env: return self._build_one_inside_env(req, output_dir) @@ -367,7 +376,6 @@ def _build_one_inside_env( wheel_name = os.path.basename(wheel_path) dest_path = os.path.join(output_dir, wheel_name) try: - ensure_dir(output_dir) wheel_hash, length = hash_file(wheel_path) shutil.move(wheel_path, dest_path) logger.info('Created wheel for %s: ' From 17428d9eabc8eddc4d55a81ea7eda070a9474389 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 16 Dec 2019 16:48:57 +0530 Subject: [PATCH 0947/3170] Add an InstallRequirement._generate_metadata method --- src/pip/_internal/req/req_install.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index cd0563c529e..fcff69c5bb4 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -530,6 +530,14 @@ def load_pyproject_toml(self): self.unpacked_source_directory, backend, backend_path=backend_path, ) + def _generate_metadata(self): + # type: () -> str + """Invokes metadata generator functions, with the required arguments. + """ + if not self.use_pep517: + return generate_metadata_legacy(self) + return generate_metadata(self) + def prepare_metadata(self): # type: () -> None """Ensure that project metadata is available. @@ -539,12 +547,8 @@ def prepare_metadata(self): """ assert self.source_dir - metadata_generator = generate_metadata - if not self.use_pep517: - metadata_generator = generate_metadata_legacy - with indent_log(): - self.metadata_directory = metadata_generator(self) + self.metadata_directory = self._generate_metadata() # Act on the newly generated metadata, based on the name and version. if not self.name: From 00bc7d1727aabf0990913a3c649541e60d42a317 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 16 Dec 2019 17:07:26 +0530 Subject: [PATCH 0948/3170] Expand the arguments for legacy metadata generation --- .../operations/build/metadata_legacy.py | 40 +++++++++---------- src/pip/_internal/req/req_install.py | 9 ++++- 2 files changed, 28 insertions(+), 21 deletions(-) diff --git a/src/pip/_internal/operations/build/metadata_legacy.py b/src/pip/_internal/operations/build/metadata_legacy.py index d817504764b..02492c98e8c 100644 --- a/src/pip/_internal/operations/build/metadata_legacy.py +++ b/src/pip/_internal/operations/build/metadata_legacy.py @@ -14,7 +14,7 @@ if MYPY_CHECK_RUNNING: from typing import List, Optional - from pip._internal.req.req_install import InstallRequirement + from pip._internal.build_env import BuildEnvironment logger = logging.getLogger(__name__) @@ -78,47 +78,47 @@ def depth_of_directory(dir_): return os.path.join(base, filenames[0]) -def generate_metadata(install_req): - # type: (InstallRequirement) -> str - """Generate metadata using setup.py-based defacto mechanisms.ArithmeticError +def generate_metadata( + build_env, # type: BuildEnvironment + setup_py_path, # type: str + source_dir, # type: str + editable, # type: bool + isolated, # type: bool + details, # type: str +): + # type: (...) -> str + """Generate metadata using setup.py-based defacto mechanisms. Returns the generated metadata directory. """ - assert install_req.unpacked_source_directory + assert source_dir - req_details_str = install_req.name or "from {}".format(install_req.link) logger.debug( 'Running setup.py (path:%s) egg_info for package %s', - install_req.setup_py_path, req_details_str, + setup_py_path, details, ) egg_info_dir = None # type: Optional[str] # For non-editable installs, don't put the .egg-info files at the root, # to avoid confusion due to the source code being considered an installed # egg. - if not install_req.editable: - egg_info_dir = os.path.join( - install_req.unpacked_source_directory, 'pip-egg-info', - ) - + if not editable: + egg_info_dir = os.path.join(source_dir, 'pip-egg-info') # setuptools complains if the target directory does not exist. ensure_dir(egg_info_dir) args = make_setuptools_egg_info_args( - install_req.setup_py_path, + setup_py_path, egg_info_dir=egg_info_dir, - no_user_config=install_req.isolated, + no_user_config=isolated, ) - with install_req.build_env: + with build_env: call_subprocess( args, - cwd=install_req.unpacked_source_directory, + cwd=source_dir, command_desc='python setup.py egg_info', ) # Return the .egg-info directory. - return _find_egg_info( - install_req.unpacked_source_directory, - install_req.editable, - ) + return _find_egg_info(source_dir, editable) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index fcff69c5bb4..8d2a8ca8498 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -535,7 +535,14 @@ def _generate_metadata(self): """Invokes metadata generator functions, with the required arguments. """ if not self.use_pep517: - return generate_metadata_legacy(self) + return generate_metadata_legacy( + build_env=self.build_env, + setup_py_path=self.setup_py_path, + source_dir=self.unpacked_source_directory, + editable=self.editable, + isolated=self.isolated, + details=self.name or "from {}".format(self.link) + ) return generate_metadata(self) def prepare_metadata(self): From 350a5986c9485a8e4458e55ce6949baf19914201 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 16 Dec 2019 17:22:20 +0530 Subject: [PATCH 0949/3170] Expand the arguments for modern metadata generation --- src/pip/_internal/operations/build/metadata.py | 11 +++++------ src/pip/_internal/req/req_install.py | 6 +++++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/operations/build/metadata.py b/src/pip/_internal/operations/build/metadata.py index 4d3f1306a5a..05ef83e0ebd 100644 --- a/src/pip/_internal/operations/build/metadata.py +++ b/src/pip/_internal/operations/build/metadata.py @@ -9,20 +9,19 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from pip._internal.req.req_install import InstallRequirement + from pip._internal.build_env import BuildEnvironment + from pip._vendor.pep517.wrappers import Pep517HookCaller logger = logging.getLogger(__name__) -def generate_metadata(install_req): - # type: (InstallRequirement) -> str +def generate_metadata(build_env, backend): + # type: (BuildEnvironment, Pep517HookCaller) -> str """Generate metadata using mechanisms described in PEP 517. Returns the generated metadata directory. """ - assert install_req.pep517_backend is not None - build_env = install_req.build_env - backend = install_req.pep517_backend + assert backend is not None metadata_tmpdir = TempDirectory( kind="modern-metadata", globally_managed=True diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 8d2a8ca8498..0554279b698 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -543,7 +543,11 @@ def _generate_metadata(self): isolated=self.isolated, details=self.name or "from {}".format(self.link) ) - return generate_metadata(self) + + return generate_metadata( + build_env=self.build_env, + backend=self.pep517_backend, + ) def prepare_metadata(self): # type: () -> None From 74c2837adfeb3cf9721966ef6b40268189dd29c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Mon, 16 Dec 2019 17:39:25 +0100 Subject: [PATCH 0950/3170] Simplify handling of cache candidate directories --- src/pip/_internal/cache.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/cache.py b/src/pip/_internal/cache.py index cde54637b85..53c3aed927e 100644 --- a/src/pip/_internal/cache.py +++ b/src/pip/_internal/cache.py @@ -139,14 +139,12 @@ def _get_candidates(self, link, canonical_package_name): path = self.get_path_for_link(link) if os.path.isdir(path): for candidate in os.listdir(path): - candidates.append((candidate, os.path.join(path, candidate))) + candidates.append((candidate, path)) # TODO remove legacy path lookup in pip>=21 legacy_path = self.get_path_for_link_legacy(link) if os.path.isdir(legacy_path): for candidate in os.listdir(legacy_path): - candidates.append( - (candidate, os.path.join(legacy_path, candidate)) - ) + candidates.append((candidate, legacy_path)) return candidates def get_path_for_link_legacy(self, link): @@ -225,7 +223,7 @@ def get( return link canonical_package_name = canonicalize_name(package_name) - for wheel_name, wheel_path in self._get_candidates( + for wheel_name, wheel_dir in self._get_candidates( link, canonical_package_name ): try: @@ -247,14 +245,15 @@ def get( ( wheel.support_index_min(supported_tags), wheel_name, - wheel_path, + wheel_dir, ) ) if not candidates: return link - return Link(path_to_url(min(candidates)[2])) + _, wheel_name, wheel_dir = min(candidates) + return Link(path_to_url(os.path.join(wheel_dir, wheel_name))) class EphemWheelCache(SimpleWheelCache): From 2a1fb915e2d2a579fa733359f0723c977a8eaf6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Mon, 16 Dec 2019 17:44:34 +0100 Subject: [PATCH 0951/3170] Remove useless pass statement Co-Authored-By: Christopher Hunt <chrahunt@gmail.com> --- src/pip/_internal/wheel_builder.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index 1937219d07c..e3212ded72b 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -389,7 +389,6 @@ def _build_one_inside_env( "Building wheel for %s failed: %s", req.name, e, ) - pass # Ignore return, we can't do anything else useful. self._clean_one(req) return None From 75f7f06ca7a42bcb2ecc382187e34c9fec65f7a8 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Wed, 18 Dec 2019 11:53:34 +0530 Subject: [PATCH 0952/3170] Move assertions to calling functions --- src/pip/_internal/operations/build/metadata.py | 2 -- src/pip/_internal/operations/build/metadata_legacy.py | 2 -- src/pip/_internal/req/req_install.py | 4 ++++ 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/operations/build/metadata.py b/src/pip/_internal/operations/build/metadata.py index 05ef83e0ebd..b13fbdef933 100644 --- a/src/pip/_internal/operations/build/metadata.py +++ b/src/pip/_internal/operations/build/metadata.py @@ -21,8 +21,6 @@ def generate_metadata(build_env, backend): Returns the generated metadata directory. """ - assert backend is not None - metadata_tmpdir = TempDirectory( kind="modern-metadata", globally_managed=True ) diff --git a/src/pip/_internal/operations/build/metadata_legacy.py b/src/pip/_internal/operations/build/metadata_legacy.py index 02492c98e8c..b6813f89ba7 100644 --- a/src/pip/_internal/operations/build/metadata_legacy.py +++ b/src/pip/_internal/operations/build/metadata_legacy.py @@ -91,8 +91,6 @@ def generate_metadata( Returns the generated metadata directory. """ - assert source_dir - logger.debug( 'Running setup.py (path:%s) egg_info for package %s', setup_py_path, details, diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 0554279b698..dc4b43730a0 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -535,6 +535,8 @@ def _generate_metadata(self): """Invokes metadata generator functions, with the required arguments. """ if not self.use_pep517: + assert self.unpacked_source_directory + return generate_metadata_legacy( build_env=self.build_env, setup_py_path=self.setup_py_path, @@ -544,6 +546,8 @@ def _generate_metadata(self): details=self.name or "from {}".format(self.link) ) + assert self.pep517_backend is not None + return generate_metadata( build_env=self.build_env, backend=self.pep517_backend, From 2e41be817ace5700c7f4032ef5321328fb9f0ade Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Wed, 18 Dec 2019 15:47:45 +0530 Subject: [PATCH 0953/3170] Clarify that editable_legacy does editable installs --- src/pip/_internal/operations/install/editable_legacy.py | 4 ++-- src/pip/_internal/req/req_install.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/operations/install/editable_legacy.py b/src/pip/_internal/operations/install/editable_legacy.py index b3124a8b013..a668a61dc60 100644 --- a/src/pip/_internal/operations/install/editable_legacy.py +++ b/src/pip/_internal/operations/install/editable_legacy.py @@ -1,4 +1,4 @@ -"""Legacy installation process, i.e. `setup.py develop`. +"""Legacy editable installation process, i.e. `setup.py develop`. """ import logging @@ -16,7 +16,7 @@ logger = logging.getLogger(__name__) -def install( +def install_editable( install_options, # type: List[str] global_options, # type: Sequence[str] prefix, # type: Optional[str] diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index dc4b43730a0..3c2008b9c6b 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -26,7 +26,7 @@ from pip._internal.operations.build.metadata_legacy import \ generate_metadata as generate_metadata_legacy from pip._internal.operations.install.editable_legacy import \ - install as install_editable_legacy + install_editable as install_editable_legacy from pip._internal.operations.install.wheel import install_wheel from pip._internal.pyproject import load_pyproject_toml, make_pyproject_path from pip._internal.req.req_uninstall import UninstallPathSet From b375580132394187f69f37d78d459f4f05767120 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Wed, 18 Dec 2019 16:11:12 +0530 Subject: [PATCH 0954/3170] Move logic for legacy installs to dedicated module --- .../_internal/operations/install/legacy.py | 129 ++++++++++++++++++ src/pip/_internal/req/req_install.py | 109 ++------------- 2 files changed, 141 insertions(+), 97 deletions(-) create mode 100644 src/pip/_internal/operations/install/legacy.py diff --git a/src/pip/_internal/operations/install/legacy.py b/src/pip/_internal/operations/install/legacy.py new file mode 100644 index 00000000000..2d4adc4f62c --- /dev/null +++ b/src/pip/_internal/operations/install/legacy.py @@ -0,0 +1,129 @@ +"""Legacy installation process, i.e. `setup.py install`. +""" + +import logging +import os +from distutils.util import change_root + +from pip._internal.utils.deprecation import deprecated +from pip._internal.utils.logging import indent_log +from pip._internal.utils.misc import ensure_dir +from pip._internal.utils.setuptools_build import make_setuptools_install_args +from pip._internal.utils.subprocess import runner_with_spinner_message +from pip._internal.utils.temp_dir import TempDirectory +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import List, Optional, Sequence + + from pip._internal.models.scheme import Scheme + from pip._internal.req.req_install import InstallRequirement + + +logger = logging.getLogger(__name__) + + +def install( + install_req, # type: InstallRequirement + install_options, # type: List[str] + global_options, # type: Sequence[str] + root, # type: Optional[str] + home, # type: Optional[str] + prefix, # type: Optional[str] + use_user_site, # type: bool + pycompile, # type: bool + scheme, # type: Scheme +): + # type: (...) -> None + # Extend the list of global and install options passed on to + # the setup.py call with the ones from the requirements file. + # Options specified in requirements file override those + # specified on the command line, since the last option given + # to setup.py is the one that is used. + global_options = list(global_options) + \ + install_req.options.get('global_options', []) + install_options = list(install_options) + \ + install_req.options.get('install_options', []) + + header_dir = scheme.headers + + with TempDirectory(kind="record") as temp_dir: + record_filename = os.path.join(temp_dir.path, 'install-record.txt') + install_args = make_setuptools_install_args( + install_req.setup_py_path, + global_options=global_options, + install_options=install_options, + record_filename=record_filename, + root=root, + prefix=prefix, + header_dir=header_dir, + home=home, + use_user_site=use_user_site, + no_user_config=install_req.isolated, + pycompile=pycompile, + ) + + runner = runner_with_spinner_message( + "Running setup.py install for {}".format(install_req.name) + ) + with indent_log(), install_req.build_env: + runner( + cmd=install_args, + cwd=install_req.unpacked_source_directory, + ) + + if not os.path.exists(record_filename): + logger.debug('Record file %s not found', record_filename) + return + install_req.install_succeeded = True + + # We intentionally do not use any encoding to read the file because + # setuptools writes the file using distutils.file_util.write_file, + # which does not specify an encoding. + with open(record_filename) as f: + record_lines = f.read().splitlines() + + def prepend_root(path): + # type: (str) -> str + if root is None or not os.path.isabs(path): + return path + else: + return change_root(root, path) + + for line in record_lines: + directory = os.path.dirname(line) + if directory.endswith('.egg-info'): + egg_info_dir = prepend_root(directory) + break + else: + deprecated( + reason=( + "{} did not indicate that it installed an " + ".egg-info directory. Only setup.py projects " + "generating .egg-info directories are supported." + ).format(install_req), + replacement=( + "for maintainers: updating the setup.py of {0}. " + "For users: contact the maintainers of {0} to let " + "them know to update their setup.py.".format( + install_req.name + ) + ), + gone_in="20.2", + issue=6998, + ) + # FIXME: put the record somewhere + return + new_lines = [] + for line in record_lines: + filename = line.strip() + if os.path.isdir(filename): + filename += os.path.sep + new_lines.append( + os.path.relpath(prepend_root(filename), egg_info_dir) + ) + new_lines.sort() + ensure_dir(egg_info_dir) + inst_files_path = os.path.join(egg_info_dir, 'installed-files.txt') + with open(inst_files_path, 'w') as f: + f.write('\n'.join(new_lines) + '\n') diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 3c2008b9c6b..80f40307290 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -8,7 +8,6 @@ import shutil import sys import zipfile -from distutils.util import change_root from pip._vendor import pkg_resources, six from pip._vendor.packaging.requirements import Requirement @@ -27,10 +26,10 @@ generate_metadata as generate_metadata_legacy from pip._internal.operations.install.editable_legacy import \ install_editable as install_editable_legacy +from pip._internal.operations.install.legacy import install as install_legacy from pip._internal.operations.install.wheel import install_wheel from pip._internal.pyproject import load_pyproject_toml, make_pyproject_path from pip._internal.req.req_uninstall import UninstallPathSet -from pip._internal.utils.deprecation import deprecated from pip._internal.utils.hashes import Hashes from pip._internal.utils.logging import indent_log from pip._internal.utils.marker_files import ( @@ -44,15 +43,12 @@ display_path, dist_in_site_packages, dist_in_usersite, - ensure_dir, get_installed_version, hide_url, redact_auth_from_url, rmtree, ) from pip._internal.utils.packaging import get_metadata -from pip._internal.utils.setuptools_build import make_setuptools_install_args -from pip._internal.utils.subprocess import runner_with_spinner_message from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.virtualenv import running_under_virtualenv @@ -805,95 +801,14 @@ def install( self.install_succeeded = True return - # Extend the list of global and install options passed on to - # the setup.py call with the ones from the requirements file. - # Options specified in requirements file override those - # specified on the command line, since the last option given - # to setup.py is the one that is used. - global_options = list(global_options) + \ - self.options.get('global_options', []) - install_options = list(install_options) + \ - self.options.get('install_options', []) - - header_dir = scheme.headers - - with TempDirectory(kind="record") as temp_dir: - record_filename = os.path.join(temp_dir.path, 'install-record.txt') - install_args = make_setuptools_install_args( - self.setup_py_path, - global_options=global_options, - install_options=install_options, - record_filename=record_filename, - root=root, - prefix=prefix, - header_dir=header_dir, - home=home, - use_user_site=use_user_site, - no_user_config=self.isolated, - pycompile=pycompile, - ) - - runner = runner_with_spinner_message( - "Running setup.py install for {}".format(self.name) - ) - with indent_log(), self.build_env: - runner( - cmd=install_args, - cwd=self.unpacked_source_directory, - ) - - if not os.path.exists(record_filename): - logger.debug('Record file %s not found', record_filename) - return - self.install_succeeded = True - - # We intentionally do not use any encoding to read the file because - # setuptools writes the file using distutils.file_util.write_file, - # which does not specify an encoding. - with open(record_filename) as f: - record_lines = f.read().splitlines() - - def prepend_root(path): - # type: (str) -> str - if root is None or not os.path.isabs(path): - return path - else: - return change_root(root, path) - - for line in record_lines: - directory = os.path.dirname(line) - if directory.endswith('.egg-info'): - egg_info_dir = prepend_root(directory) - break - else: - deprecated( - reason=( - "{} did not indicate that it installed an " - ".egg-info directory. Only setup.py projects " - "generating .egg-info directories are supported." - ).format(self), - replacement=( - "for maintainers: updating the setup.py of {0}. " - "For users: contact the maintainers of {0} to let " - "them know to update their setup.py.".format( - self.name - ) - ), - gone_in="20.2", - issue=6998, - ) - # FIXME: put the record somewhere - return - new_lines = [] - for line in record_lines: - filename = line.strip() - if os.path.isdir(filename): - filename += os.path.sep - new_lines.append( - os.path.relpath(prepend_root(filename), egg_info_dir) - ) - new_lines.sort() - ensure_dir(egg_info_dir) - inst_files_path = os.path.join(egg_info_dir, 'installed-files.txt') - with open(inst_files_path, 'w') as f: - f.write('\n'.join(new_lines) + '\n') + install_legacy( + self, + install_options=install_options, + global_options=global_options, + root=root, + home=home, + prefix=prefix, + use_user_site=use_user_site, + pycompile=pycompile, + scheme=scheme, + ) From 2472a6e51dc40170b933017640d0776a987a396d Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 18 Dec 2019 19:09:54 +0800 Subject: [PATCH 0955/3170] Fix path in _vendor/README.rst to match reality --- src/pip/_vendor/README.rst | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/pip/_vendor/README.rst b/src/pip/_vendor/README.rst index 39f168636a5..38c306aab8e 100644 --- a/src/pip/_vendor/README.rst +++ b/src/pip/_vendor/README.rst @@ -18,7 +18,7 @@ Vendoring Policy * Any modifications made to libraries **MUST** be noted in ``pip/_vendor/README.rst`` and their corresponding patches **MUST** be - included ``tasks/vendoring/patches``. + included ``tools/automation/vendoring/patches``. * Vendored libraries should have corresponding ``vendored()`` entries in ``pip/_vendor/__init__.py``. @@ -109,11 +109,10 @@ Modifications Automatic Vendoring ------------------- -Vendoring is automated via the ``vendoring.update`` task (defined in -``tasks/vendoring/__init__.py``) from the content of +Vendoring is automated via the ``vendoring`` tool from the content of ``pip/_vendor/vendor.txt`` and the different patches in -``tasks/vendoring/patches/``. -Launch it via ``invoke vendoring.update`` (requires ``invoke>=0.13.0``). +``tools/automation/vendoring/patches``. +Launch it via ``vendoring sync . -v`` (requires ``vendoring>=0.2.2``). Debundling From 82a2651f930c8b5ae9fd5b5d1f9b34de6b57f512 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 18 Dec 2019 20:21:22 -0500 Subject: [PATCH 0956/3170] Move pip._internal.main to cli submodule Moving this out of the way gives us the flexibility to define wrappers that will redirect requests to our old entrypoints to our new one. --- setup.py | 8 +++++--- src/pip/__main__.py | 2 +- src/pip/_internal/{ => cli}/main.py | 0 tests/conftest.py | 2 +- tests/unit/test_options.py | 2 +- 5 files changed, 8 insertions(+), 6 deletions(-) rename src/pip/_internal/{ => cli}/main.py (100%) diff --git a/setup.py b/setup.py index 7e62de57349..13e919c3fc7 100644 --- a/setup.py +++ b/setup.py @@ -70,9 +70,11 @@ def get_version(rel_path): }, entry_points={ "console_scripts": [ - "pip=pip._internal.main:main", - "pip{}=pip._internal.main:main".format(sys.version_info[0]), - "pip{}.{}=pip._internal.main:main".format(*sys.version_info[:2]), + "pip=pip._internal.cli.main:main", + "pip{}=pip._internal.cli.main:main".format(sys.version_info[0]), + "pip{}.{}=pip._internal.cli.main:main".format( + *sys.version_info[:2] + ), ], }, diff --git a/src/pip/__main__.py b/src/pip/__main__.py index 49b6fdf71ca..e83b9e056b3 100644 --- a/src/pip/__main__.py +++ b/src/pip/__main__.py @@ -13,7 +13,7 @@ path = os.path.dirname(os.path.dirname(__file__)) sys.path.insert(0, path) -from pip._internal.main import main as _main # isort:skip # noqa +from pip._internal.cli.main import main as _main # isort:skip # noqa if __name__ == '__main__': sys.exit(_main()) diff --git a/src/pip/_internal/main.py b/src/pip/_internal/cli/main.py similarity index 100% rename from src/pip/_internal/main.py rename to src/pip/_internal/cli/main.py diff --git a/tests/conftest.py b/tests/conftest.py index bce70651b6b..0b8496856f5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -13,7 +13,7 @@ from pip._vendor.contextlib2 import ExitStack from setuptools.wheel import Wheel -from pip._internal.main import main as pip_entry_point +from pip._internal.cli.main import main as pip_entry_point from pip._internal.utils.temp_dir import global_tempdir_manager from pip._internal.utils.typing import MYPY_CHECK_RUNNING from tests.lib import DATA_DIR, SRC_DIR, TestData diff --git a/tests/unit/test_options.py b/tests/unit/test_options.py index a67f34e83cb..78816ef1aa7 100644 --- a/tests/unit/test_options.py +++ b/tests/unit/test_options.py @@ -4,9 +4,9 @@ import pytest import pip._internal.configuration +from pip._internal.cli.main import main from pip._internal.commands import create_command from pip._internal.exceptions import PipError -from pip._internal.main import main from tests.lib.options_helpers import AddFakeCommandMixin From 204887da03f1683d6338e5f79257f9892506f79f Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 19 Dec 2019 15:54:58 +0800 Subject: [PATCH 0957/3170] Apply changes from bundled appdirs to vendored * Convert Windows app data directory values to bytes on Python 2, so the output type is consistent across platforms (pypa/pip#4000) * Also look in ~/.config for user config on macOS (pypa/pip#4100) * Remove pywin32 dependency, only use ctypes and winreg for directory lookup on Windows (pypa/pip#2467) * Always use os.path.join() instead of os.sep.join() so cross-platform tests work as expected (pypa/pip#3275) --- ...0cda98-2240-11ea-9951-00e04c3600d8.trivial | 0 src/pip/_internal/utils/appdirs.py | 265 +----------------- src/pip/_vendor/appdirs.py | 36 ++- tests/unit/test_appdirs.py | 57 ++-- .../vendoring/patches/appdirs.patch | 88 +++++- 5 files changed, 160 insertions(+), 286 deletions(-) create mode 100644 news/050cda98-2240-11ea-9951-00e04c3600d8.trivial diff --git a/news/050cda98-2240-11ea-9951-00e04c3600d8.trivial b/news/050cda98-2240-11ea-9951-00e04c3600d8.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/utils/appdirs.py b/src/pip/_internal/utils/appdirs.py index 06cd8314a5c..cce1e293c00 100644 --- a/src/pip/_internal/utils/appdirs.py +++ b/src/pip/_internal/utils/appdirs.py @@ -1,19 +1,17 @@ """ -This code was taken from https://github.com/ActiveState/appdirs and modified -to suit our purposes. -""" +This code wraps the vendored appdirs module to so the return values are +compatible for the current pip code base. -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False +The intention is to rewrite current usages guradually, keeping the tests pass, +and eventually drop this after all usages are changed. +""" from __future__ import absolute_import import os -import sys -from pip._vendor.six import PY2, text_type +from pip._vendor import appdirs as _appdirs -from pip._internal.utils.compat import WINDOWS, expanduser from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: @@ -22,255 +20,22 @@ def user_cache_dir(appname): # type: (str) -> str - r""" - Return full path to the user-specific cache dir for this application. - - "appname" is the name of application. - - Typical user cache directories are: - macOS: ~/Library/Caches/<AppName> - Unix: ~/.cache/<AppName> (XDG default) - Windows: C:\Users\<username>\AppData\Local\<AppName>\Cache - - On Windows the only suggestion in the MSDN docs is that local settings go - in the `CSIDL_LOCAL_APPDATA` directory. This is identical to the - non-roaming app data dir (the default returned by `user_data_dir`). Apps - typically put cache data somewhere *under* the given dir here. Some - examples: - ...\Mozilla\Firefox\Profiles\<ProfileName>\Cache - ...\Acme\SuperApp\Cache\1.0 - - OPINION: This function appends "Cache" to the `CSIDL_LOCAL_APPDATA` value. - """ - if WINDOWS: - # Get the base path - path = os.path.normpath(_get_win_folder("CSIDL_LOCAL_APPDATA")) - - # When using Python 2, return paths as bytes on Windows like we do on - # other operating systems. See helper function docs for more details. - if PY2 and isinstance(path, text_type): - path = _win_path_to_bytes(path) - - # Add our app name and Cache directory to it - path = os.path.join(path, appname, "Cache") - elif sys.platform == "darwin": - # Get the base path - path = expanduser("~/Library/Caches") - - # Add our app name to it - path = os.path.join(path, appname) - else: - # Get the base path - path = os.getenv("XDG_CACHE_HOME", expanduser("~/.cache")) - - # Add our app name to it - path = os.path.join(path, appname) - - return path - - -def user_data_dir(appname, roaming=False): - # type: (str, bool) -> str - r""" - Return full path to the user-specific data dir for this application. - - "appname" is the name of application. - If None, just the system directory is returned. - "roaming" (boolean, default False) can be set True to use the Windows - roaming appdata directory. That means that for users on a Windows - network setup for roaming profiles, this user data will be - sync'd on login. See - <http://technet.microsoft.com/en-us/library/cc766489(WS.10).aspx> - for a discussion of issues. - - Typical user data directories are: - macOS: ~/Library/Application Support/<AppName> - if it exists, else ~/.config/<AppName> - Unix: ~/.local/share/<AppName> # or in - $XDG_DATA_HOME, if defined - Win XP (not roaming): C:\Documents and Settings\<username>\ ... - ...Application Data\<AppName> - Win XP (roaming): C:\Documents and Settings\<username>\Local ... - ...Settings\Application Data\<AppName> - Win 7 (not roaming): C:\\Users\<username>\AppData\Local\<AppName> - Win 7 (roaming): C:\\Users\<username>\AppData\Roaming\<AppName> - - For Unix, we follow the XDG spec and support $XDG_DATA_HOME. - That means, by default "~/.local/share/<AppName>". - """ - if WINDOWS: - const = roaming and "CSIDL_APPDATA" or "CSIDL_LOCAL_APPDATA" - path = os.path.join(os.path.normpath(_get_win_folder(const)), appname) - elif sys.platform == "darwin": - path = os.path.join( - expanduser('~/Library/Application Support/'), - appname, - ) if os.path.isdir(os.path.join( - expanduser('~/Library/Application Support/'), - appname, - ) - ) else os.path.join( - expanduser('~/.config/'), - appname, - ) - else: - path = os.path.join( - os.getenv('XDG_DATA_HOME', expanduser("~/.local/share")), - appname, - ) - - return path + return _appdirs.user_cache_dir(appname, appauthor=False) def user_config_dir(appname, roaming=True): # type: (str, bool) -> str - """Return full path to the user-specific config dir for this application. - - "appname" is the name of application. - If None, just the system directory is returned. - "roaming" (boolean, default True) can be set False to not use the - Windows roaming appdata directory. That means that for users on a - Windows network setup for roaming profiles, this user data will be - sync'd on login. See - <http://technet.microsoft.com/en-us/library/cc766489(WS.10).aspx> - for a discussion of issues. - - Typical user data directories are: - macOS: same as user_data_dir - Unix: ~/.config/<AppName> - Win *: same as user_data_dir + return _appdirs.user_config_dir(appname, appauthor=False, roaming=roaming) - For Unix, we follow the XDG spec and support $XDG_CONFIG_HOME. - That means, by default "~/.config/<AppName>". - """ - if WINDOWS: - path = user_data_dir(appname, roaming=roaming) - elif sys.platform == "darwin": - path = user_data_dir(appname) - else: - path = os.getenv('XDG_CONFIG_HOME', expanduser("~/.config")) - path = os.path.join(path, appname) - return path +def user_data_dir(appname, roaming=False): + # type: (str, bool) -> str + return _appdirs.user_data_dir(appname, appauthor=False, roaming=roaming) -# for the discussion regarding site_config_dirs locations -# see <https://github.com/pypa/pip/issues/1733> def site_config_dirs(appname): # type: (str) -> List[str] - r"""Return a list of potential user-shared config dirs for this application. - - "appname" is the name of application. - - Typical user config directories are: - macOS: /Library/Application Support/<AppName>/ - Unix: /etc or $XDG_CONFIG_DIRS[i]/<AppName>/ for each value in - $XDG_CONFIG_DIRS - Win XP: C:\Documents and Settings\All Users\Application ... - ...Data\<AppName>\ - Vista: (Fail! "C:\ProgramData" is a hidden *system* directory - on Vista.) - Win 7: Hidden, but writeable on Win 7: - C:\ProgramData\<AppName>\ - """ - if WINDOWS: - path = os.path.normpath(_get_win_folder("CSIDL_COMMON_APPDATA")) - pathlist = [os.path.join(path, appname)] - elif sys.platform == 'darwin': - pathlist = [os.path.join('/Library/Application Support', appname)] - else: - # try looking in $XDG_CONFIG_DIRS - xdg_config_dirs = os.getenv('XDG_CONFIG_DIRS', '/etc/xdg') - if xdg_config_dirs: - pathlist = [ - os.path.join(expanduser(x), appname) - for x in xdg_config_dirs.split(os.pathsep) - ] - else: - pathlist = [] - - # always look in /etc directly as well - pathlist.append('/etc') - - return pathlist - - -# -- Windows support functions -- - -def _get_win_folder_from_registry(csidl_name): - # type: (str) -> str - """ - This is a fallback technique at best. I'm not sure if using the - registry for this guarantees us the correct answer for all CSIDL_* - names. - """ - import _winreg - - shell_folder_name = { - "CSIDL_APPDATA": "AppData", - "CSIDL_COMMON_APPDATA": "Common AppData", - "CSIDL_LOCAL_APPDATA": "Local AppData", - }[csidl_name] - - key = _winreg.OpenKey( - _winreg.HKEY_CURRENT_USER, - r"Software\Microsoft\Windows\CurrentVersion\Explorer\Shell Folders" - ) - directory, _type = _winreg.QueryValueEx(key, shell_folder_name) - return directory - - -def _get_win_folder_with_ctypes(csidl_name): - # type: (str) -> str - # On Python 2, ctypes.create_unicode_buffer().value returns "unicode", - # which isn't the same as str in the annotation above. - csidl_const = { - "CSIDL_APPDATA": 26, - "CSIDL_COMMON_APPDATA": 35, - "CSIDL_LOCAL_APPDATA": 28, - }[csidl_name] - - buf = ctypes.create_unicode_buffer(1024) - windll = ctypes.windll # type: ignore - windll.shell32.SHGetFolderPathW(None, csidl_const, None, 0, buf) - - # Downgrade to short path name if have highbit chars. See - # <http://bugs.activestate.com/show_bug.cgi?id=85099>. - has_high_char = False - for c in buf: - if ord(c) > 255: - has_high_char = True - break - if has_high_char: - buf2 = ctypes.create_unicode_buffer(1024) - if windll.kernel32.GetShortPathNameW(buf.value, buf2, 1024): - buf = buf2 - - # The type: ignore is explained under the type annotation for this function - return buf.value # type: ignore - - -if WINDOWS: - try: - import ctypes - _get_win_folder = _get_win_folder_with_ctypes - except ImportError: - _get_win_folder = _get_win_folder_from_registry - - -def _win_path_to_bytes(path): - """Encode Windows paths to bytes. Only used on Python 2. - - Motivation is to be consistent with other operating systems where paths - are also returned as bytes. This avoids problems mixing bytes and Unicode - elsewhere in the codebase. For more details and discussion see - <https://github.com/pypa/pip/issues/3463>. - - If encoding using ASCII and MBCS fails, return the original Unicode path. - """ - for encoding in ('ASCII', 'MBCS'): - try: - return path.encode(encoding) - except (UnicodeEncodeError, LookupError): - pass - return path + dirval = _appdirs.site_config_dir(appname, appauthor=False, multipath=True) + if _appdirs.system == "linux2": + return dirval.split(os.pathsep) + return [dirval] diff --git a/src/pip/_vendor/appdirs.py b/src/pip/_vendor/appdirs.py index 2bd39110281..92b80251e14 100644 --- a/src/pip/_vendor/appdirs.py +++ b/src/pip/_vendor/appdirs.py @@ -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: - Mac OS X: ~/Library/Application Support/<AppName> + Mac OS X: ~/Library/Application Support/<AppName> # or ~/.config/<AppName>, if the other does not exist Unix: ~/.local/share/<AppName> # or in $XDG_DATA_HOME, if defined Win XP (not roaming): C:\Documents and Settings\<username>\Application Data\<AppAuthor>\<AppName> Win XP (roaming): C:\Documents and Settings\<username>\Local Settings\Application Data\<AppAuthor>\<AppName> @@ -88,6 +88,10 @@ def user_data_dir(appname=None, appauthor=None, version=None, roaming=False): path = os.path.expanduser('~/Library/Application Support/') if appname: path = os.path.join(path, appname) + if not os.path.isdir(path): + path = os.path.expanduser('~/.config/') + if appname: + path = os.path.join(path, appname) else: path = os.getenv('XDG_DATA_HOME', os.path.expanduser("~/.local/share")) if appname: @@ -150,7 +154,7 @@ def site_data_dir(appname=None, appauthor=None, version=None, multipath=False): if appname: if version: appname = os.path.join(appname, version) - pathlist = [os.sep.join([x, appname]) for x in pathlist] + pathlist = [os.path.join(x, appname) for x in pathlist] if multipath: path = os.pathsep.join(pathlist) @@ -203,6 +207,8 @@ def user_config_dir(appname=None, appauthor=None, version=None, roaming=False): return path +# for the discussion regarding site_config_dir locations +# see <https://github.com/pypa/pip/issues/1733> def site_config_dir(appname=None, appauthor=None, version=None, multipath=False): r"""Return full path to the user-shared data dir for this application. @@ -245,7 +251,9 @@ def site_config_dir(appname=None, appauthor=None, version=None, multipath=False) if appname: if version: appname = os.path.join(appname, version) - pathlist = [os.sep.join([x, appname]) for x in pathlist] + pathlist = [os.path.join(x, appname) for x in pathlist] + # always look in /etc directly as well + pathlist.append('/etc') if multipath: path = os.pathsep.join(pathlist) @@ -291,6 +299,10 @@ def user_cache_dir(appname=None, appauthor=None, version=None, opinion=True): if appauthor is None: appauthor = appname path = os.path.normpath(_get_win_folder("CSIDL_LOCAL_APPDATA")) + # When using Python 2, return paths as bytes on Windows like we do on + # other operating systems. See helper function docs for more details. + if not PY3 and isinstance(path, unicode): + path = _win_path_to_bytes(path) if appname: if appauthor is not False: path = os.path.join(path, appauthor, appname) @@ -567,6 +579,24 @@ def _get_win_folder_with_jna(csidl_name): _get_win_folder = _get_win_folder_from_registry +def _win_path_to_bytes(path): + """Encode Windows paths to bytes. Only used on Python 2. + + Motivation is to be consistent with other operating systems where paths + are also returned as bytes. This avoids problems mixing bytes and Unicode + elsewhere in the codebase. For more details and discussion see + <https://github.com/pypa/pip/issues/3463>. + + If encoding using ASCII and MBCS fails, return the original Unicode path. + """ + for encoding in ('ASCII', 'MBCS'): + try: + return path.encode(encoding) + except (UnicodeEncodeError, LookupError): + pass + return path + + #---- self test code if __name__ == "__main__": diff --git a/tests/unit/test_appdirs.py b/tests/unit/test_appdirs.py index 1ee68ef2f72..1a01464174f 100644 --- a/tests/unit/test_appdirs.py +++ b/tests/unit/test_appdirs.py @@ -4,6 +4,7 @@ import sys import pretend +from pip._vendor import appdirs as _appdirs from pip._internal.utils import appdirs @@ -16,12 +17,12 @@ def _get_win_folder(base): return "C:\\Users\\test\\AppData\\Local" monkeypatch.setattr( - appdirs, + _appdirs, "_get_win_folder", _get_win_folder, raising=False, ) - monkeypatch.setattr(appdirs, "WINDOWS", True) + monkeypatch.setattr(_appdirs, "system", "win32") monkeypatch.setattr(os, "path", ntpath) assert (appdirs.user_cache_dir("pip") == @@ -29,7 +30,7 @@ def _get_win_folder(base): assert _get_win_folder.calls == [pretend.call("CSIDL_LOCAL_APPDATA")] def test_user_cache_dir_osx(self, monkeypatch): - monkeypatch.setattr(appdirs, "WINDOWS", False) + monkeypatch.setattr(_appdirs, "system", "darwin") monkeypatch.setattr(os, "path", posixpath) monkeypatch.setenv("HOME", "/home/test") monkeypatch.setattr(sys, "platform", "darwin") @@ -37,7 +38,7 @@ def test_user_cache_dir_osx(self, monkeypatch): assert appdirs.user_cache_dir("pip") == "/home/test/Library/Caches/pip" def test_user_cache_dir_linux(self, monkeypatch): - monkeypatch.setattr(appdirs, "WINDOWS", False) + monkeypatch.setattr(_appdirs, "system", "linux2") monkeypatch.setattr(os, "path", posixpath) monkeypatch.delenv("XDG_CACHE_HOME", raising=False) monkeypatch.setenv("HOME", "/home/test") @@ -46,7 +47,7 @@ def test_user_cache_dir_linux(self, monkeypatch): assert appdirs.user_cache_dir("pip") == "/home/test/.cache/pip" def test_user_cache_dir_linux_override(self, monkeypatch): - monkeypatch.setattr(appdirs, "WINDOWS", False) + monkeypatch.setattr(_appdirs, "system", "linux2") monkeypatch.setattr(os, "path", posixpath) monkeypatch.setenv("XDG_CACHE_HOME", "/home/test/.other-cache") monkeypatch.setenv("HOME", "/home/test") @@ -55,7 +56,7 @@ def test_user_cache_dir_linux_override(self, monkeypatch): assert appdirs.user_cache_dir("pip") == "/home/test/.other-cache/pip" def test_user_cache_dir_linux_home_slash(self, monkeypatch): - monkeypatch.setattr(appdirs, "WINDOWS", False) + monkeypatch.setattr(_appdirs, "system", "linux2") monkeypatch.setattr(os, "path", posixpath) # Verify that we are not affected by https://bugs.python.org/issue14768 monkeypatch.delenv("XDG_CACHE_HOME", raising=False) @@ -71,7 +72,7 @@ def test_user_cache_dir_unicode(self, monkeypatch): def my_get_win_folder(csidl_name): return u"\u00DF\u00E4\u03B1\u20AC" - monkeypatch.setattr(appdirs, "_get_win_folder", my_get_win_folder) + monkeypatch.setattr(_appdirs, "_get_win_folder", my_get_win_folder) # Do not use the isinstance expression directly in the # assert statement, as the Unicode characters in the result @@ -92,19 +93,19 @@ def _get_win_folder(base): return "C:\\ProgramData" monkeypatch.setattr( - appdirs, + _appdirs, "_get_win_folder", _get_win_folder, raising=False, ) - monkeypatch.setattr(appdirs, "WINDOWS", True) + monkeypatch.setattr(_appdirs, "system", "win32") monkeypatch.setattr(os, "path", ntpath) assert appdirs.site_config_dirs("pip") == ["C:\\ProgramData\\pip"] assert _get_win_folder.calls == [pretend.call("CSIDL_COMMON_APPDATA")] def test_site_config_dirs_osx(self, monkeypatch): - monkeypatch.setattr(appdirs, "WINDOWS", False) + monkeypatch.setattr(_appdirs, "system", "darwin") monkeypatch.setattr(os, "path", posixpath) monkeypatch.setenv("HOME", "/home/test") monkeypatch.setattr(sys, "platform", "darwin") @@ -113,7 +114,7 @@ def test_site_config_dirs_osx(self, monkeypatch): ["/Library/Application Support/pip"] def test_site_config_dirs_linux(self, monkeypatch): - monkeypatch.setattr(appdirs, "WINDOWS", False) + monkeypatch.setattr(_appdirs, "system", "linux2") monkeypatch.setattr(os, "path", posixpath) monkeypatch.delenv("XDG_CONFIG_DIRS", raising=False) monkeypatch.setattr(sys, "platform", "linux2") @@ -124,7 +125,7 @@ def test_site_config_dirs_linux(self, monkeypatch): ] def test_site_config_dirs_linux_override(self, monkeypatch): - monkeypatch.setattr(appdirs, "WINDOWS", False) + monkeypatch.setattr(_appdirs, "system", "linux2") monkeypatch.setattr(os, "path", posixpath) monkeypatch.setattr(os, "pathsep", ':') monkeypatch.setenv("XDG_CONFIG_DIRS", "/spam:/etc:/etc/xdg") @@ -146,12 +147,12 @@ def _get_win_folder(base): return "C:\\Users\\test\\AppData\\Local" monkeypatch.setattr( - appdirs, + _appdirs, "_get_win_folder", _get_win_folder, raising=False, ) - monkeypatch.setattr(appdirs, "WINDOWS", True) + monkeypatch.setattr(_appdirs, "system", "win32") monkeypatch.setattr(os, "path", ntpath) assert (appdirs.user_data_dir("pip") == @@ -164,12 +165,12 @@ def _get_win_folder(base): return "C:\\Users\\test\\AppData\\Roaming" monkeypatch.setattr( - appdirs, + _appdirs, "_get_win_folder", _get_win_folder, raising=False, ) - monkeypatch.setattr(appdirs, "WINDOWS", True) + monkeypatch.setattr(_appdirs, "system", "win32") monkeypatch.setattr(os, "path", ntpath) assert ( @@ -179,7 +180,7 @@ def _get_win_folder(base): assert _get_win_folder.calls == [pretend.call("CSIDL_APPDATA")] def test_user_data_dir_osx(self, monkeypatch): - monkeypatch.setattr(appdirs, "WINDOWS", False) + monkeypatch.setattr(_appdirs, "system", "darwin") monkeypatch.setattr(os, "path", posixpath) monkeypatch.setenv("HOME", "/home/test") monkeypatch.setattr(sys, "platform", "darwin") @@ -192,7 +193,7 @@ def test_user_data_dir_osx(self, monkeypatch): "/home/test/.config/pip") def test_user_data_dir_linux(self, monkeypatch): - monkeypatch.setattr(appdirs, "WINDOWS", False) + monkeypatch.setattr(_appdirs, "system", "linux2") monkeypatch.setattr(os, "path", posixpath) monkeypatch.delenv("XDG_DATA_HOME", raising=False) monkeypatch.setenv("HOME", "/home/test") @@ -201,7 +202,7 @@ def test_user_data_dir_linux(self, monkeypatch): assert appdirs.user_data_dir("pip") == "/home/test/.local/share/pip" def test_user_data_dir_linux_override(self, monkeypatch): - monkeypatch.setattr(appdirs, "WINDOWS", False) + monkeypatch.setattr(_appdirs, "system", "linux2") monkeypatch.setattr(os, "path", posixpath) monkeypatch.setenv("XDG_DATA_HOME", "/home/test/.other-share") monkeypatch.setenv("HOME", "/home/test") @@ -210,7 +211,7 @@ def test_user_data_dir_linux_override(self, monkeypatch): assert appdirs.user_data_dir("pip") == "/home/test/.other-share/pip" def test_user_data_dir_linux_home_slash(self, monkeypatch): - monkeypatch.setattr(appdirs, "WINDOWS", False) + monkeypatch.setattr(_appdirs, "system", "linux2") monkeypatch.setattr(os, "path", posixpath) # Verify that we are not affected by https://bugs.python.org/issue14768 monkeypatch.delenv("XDG_DATA_HOME", raising=False) @@ -228,12 +229,12 @@ def _get_win_folder(base): return "C:\\Users\\test\\AppData\\Local" monkeypatch.setattr( - appdirs, + _appdirs, "_get_win_folder", _get_win_folder, raising=False, ) - monkeypatch.setattr(appdirs, "WINDOWS", True) + monkeypatch.setattr(_appdirs, "system", "win32") monkeypatch.setattr(os, "path", ntpath) assert ( @@ -248,12 +249,12 @@ def _get_win_folder(base): return "C:\\Users\\test\\AppData\\Roaming" monkeypatch.setattr( - appdirs, + _appdirs, "_get_win_folder", _get_win_folder, raising=False, ) - monkeypatch.setattr(appdirs, "WINDOWS", True) + monkeypatch.setattr(_appdirs, "system", "win32") monkeypatch.setattr(os, "path", ntpath) assert (appdirs.user_config_dir("pip") == @@ -261,7 +262,7 @@ def _get_win_folder(base): assert _get_win_folder.calls == [pretend.call("CSIDL_APPDATA")] def test_user_config_dir_osx(self, monkeypatch): - monkeypatch.setattr(appdirs, "WINDOWS", False) + monkeypatch.setattr(_appdirs, "system", "darwin") monkeypatch.setattr(os, "path", posixpath) monkeypatch.setenv("HOME", "/home/test") monkeypatch.setattr(sys, "platform", "darwin") @@ -274,7 +275,7 @@ def test_user_config_dir_osx(self, monkeypatch): "/home/test/.config/pip") def test_user_config_dir_linux(self, monkeypatch): - monkeypatch.setattr(appdirs, "WINDOWS", False) + monkeypatch.setattr(_appdirs, "system", "linux2") monkeypatch.setattr(os, "path", posixpath) monkeypatch.delenv("XDG_CONFIG_HOME", raising=False) monkeypatch.setenv("HOME", "/home/test") @@ -283,7 +284,7 @@ def test_user_config_dir_linux(self, monkeypatch): assert appdirs.user_config_dir("pip") == "/home/test/.config/pip" def test_user_config_dir_linux_override(self, monkeypatch): - monkeypatch.setattr(appdirs, "WINDOWS", False) + monkeypatch.setattr(_appdirs, "system", "linux2") monkeypatch.setattr(os, "path", posixpath) monkeypatch.setenv("XDG_CONFIG_HOME", "/home/test/.other-config") monkeypatch.setenv("HOME", "/home/test") @@ -292,7 +293,7 @@ def test_user_config_dir_linux_override(self, monkeypatch): assert appdirs.user_config_dir("pip") == "/home/test/.other-config/pip" def test_user_config_dir_linux_home_slash(self, monkeypatch): - monkeypatch.setattr(appdirs, "WINDOWS", False) + monkeypatch.setattr(_appdirs, "system", "linux2") monkeypatch.setattr(os, "path", posixpath) # Verify that we are not affected by https://bugs.python.org/issue14768 monkeypatch.delenv("XDG_CONFIG_HOME", raising=False) diff --git a/tools/automation/vendoring/patches/appdirs.patch b/tools/automation/vendoring/patches/appdirs.patch index 73f9f2b743a..136f34b4898 100644 --- a/tools/automation/vendoring/patches/appdirs.patch +++ b/tools/automation/vendoring/patches/appdirs.patch @@ -1,9 +1,69 @@ diff --git a/src/pip/_vendor/appdirs.py b/src/pip/_vendor/appdirs.py -index ae67001a..2bd39110 100644 +index ae67001a..92b80251 100644 --- a/src/pip/_vendor/appdirs.py +++ b/src/pip/_vendor/appdirs.py -@@ -557,18 +557,14 @@ def _get_win_folder_with_jna(csidl_name): - +@@ -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: +- Mac OS X: ~/Library/Application Support/<AppName> ++ Mac OS X: ~/Library/Application Support/<AppName> # or ~/.config/<AppName>, if the other does not exist + Unix: ~/.local/share/<AppName> # or in $XDG_DATA_HOME, if defined + Win XP (not roaming): C:\Documents and Settings\<username>\Application Data\<AppAuthor>\<AppName> + Win XP (roaming): C:\Documents and Settings\<username>\Local Settings\Application Data\<AppAuthor>\<AppName> +@@ -88,6 +88,10 @@ def user_data_dir(appname=None, appauthor=None, version=None, roaming=False): + path = os.path.expanduser('~/Library/Application Support/') + if appname: + path = os.path.join(path, appname) ++ if not os.path.isdir(path): ++ path = os.path.expanduser('~/.config/') ++ if appname: ++ path = os.path.join(path, appname) + else: + path = os.getenv('XDG_DATA_HOME', os.path.expanduser("~/.local/share")) + if appname: +@@ -150,7 +154,7 @@ def site_data_dir(appname=None, appauthor=None, version=None, multipath=False): + if appname: + if version: + appname = os.path.join(appname, version) +- pathlist = [os.sep.join([x, appname]) for x in pathlist] ++ pathlist = [os.path.join(x, appname) for x in pathlist] + + if multipath: + path = os.pathsep.join(pathlist) +@@ -203,6 +207,8 @@ def user_config_dir(appname=None, appauthor=None, version=None, roaming=False): + return path + + ++# for the discussion regarding site_config_dir locations ++# see <https://github.com/pypa/pip/issues/1733> + def site_config_dir(appname=None, appauthor=None, version=None, multipath=False): + r"""Return full path to the user-shared data dir for this application. + +@@ -245,7 +251,9 @@ def site_config_dir(appname=None, appauthor=None, version=None, multipath=False) + if appname: + if version: + appname = os.path.join(appname, version) +- pathlist = [os.sep.join([x, appname]) for x in pathlist] ++ pathlist = [os.path.join(x, appname) for x in pathlist] ++ # always look in /etc directly as well ++ pathlist.append('/etc') + + if multipath: + path = os.pathsep.join(pathlist) +@@ -291,6 +299,10 @@ def user_cache_dir(appname=None, appauthor=None, version=None, opinion=True): + if appauthor is None: + appauthor = appname + path = os.path.normpath(_get_win_folder("CSIDL_LOCAL_APPDATA")) ++ # When using Python 2, return paths as bytes on Windows like we do on ++ # other operating systems. See helper function docs for more details. ++ if not PY3 and isinstance(path, unicode): ++ path = _win_path_to_bytes(path) + if appname: + if appauthor is not False: + path = os.path.join(path, appauthor, appname) +@@ -557,18 +569,32 @@ def _get_win_folder_with_jna(csidl_name): + if system == "win32": try: - import win32com.shell @@ -23,6 +83,24 @@ index ae67001a..2bd39110 100644 - except ImportError: - _get_win_folder = _get_win_folder_from_registry + _get_win_folder = _get_win_folder_from_registry - - ++ ++ ++def _win_path_to_bytes(path): ++ """Encode Windows paths to bytes. Only used on Python 2. ++ ++ Motivation is to be consistent with other operating systems where paths ++ are also returned as bytes. This avoids problems mixing bytes and Unicode ++ elsewhere in the codebase. For more details and discussion see ++ <https://github.com/pypa/pip/issues/3463>. ++ ++ If encoding using ASCII and MBCS fails, return the original Unicode path. ++ """ ++ for encoding in ('ASCII', 'MBCS'): ++ try: ++ return path.encode(encoding) ++ except (UnicodeEncodeError, LookupError): ++ pass ++ return path + + #---- self test code From 241679e6c28dc3195a753240cd4d1613c3640acb Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 18 Dec 2019 21:00:18 -0500 Subject: [PATCH 0958/3170] Setup old entrypoints in pip module This should make everything "just work" with respect to combinations of PATH, sys.path, and multiple Python installs. Later we can add a warning here to help guide users to better understanding. --- src/pip/__init__.py | 17 +++++++++++++ src/pip/_internal/__init__.py | 16 +++++++++++++ src/pip/_internal/main.py | 16 +++++++++++++ src/pip/_internal/utils/entrypoints.py | 21 ++++++++++++++++ tests/functional/test_cli.py | 33 ++++++++++++++++++++++++++ 5 files changed, 103 insertions(+) create mode 100644 src/pip/_internal/main.py create mode 100644 src/pip/_internal/utils/entrypoints.py create mode 100644 tests/functional/test_cli.py diff --git a/src/pip/__init__.py b/src/pip/__init__.py index b3b1ff0b7d1..c1e42c80fc1 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -1 +1,18 @@ +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import List, Optional + + __version__ = "20.0.dev0" + + +def main(args=None): + # type: (Optional[List[str]]) -> int + """This is an internal API only meant for use by pip's own console scripts. + + For additional details, see https://github.com/pypa/pip/issues/7498. + """ + from pip._internal.utils.entrypoints import _wrapper + + return _wrapper(args) diff --git a/src/pip/_internal/__init__.py b/src/pip/_internal/__init__.py index 8c0e4c585d1..3aa8a4693ff 100755 --- a/src/pip/_internal/__init__.py +++ b/src/pip/_internal/__init__.py @@ -1,2 +1,18 @@ #!/usr/bin/env python import pip._internal.utils.inject_securetransport # noqa +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import Optional, List + + +def main(args=None): + # type: (Optional[List[str]]) -> int + """This is preserved for old console scripts that may still be referencing + it. + + For additional details, see https://github.com/pypa/pip/issues/7498. + """ + from pip._internal.utils.entrypoints import _wrapper + + return _wrapper(args) diff --git a/src/pip/_internal/main.py b/src/pip/_internal/main.py new file mode 100644 index 00000000000..3208d5b8820 --- /dev/null +++ b/src/pip/_internal/main.py @@ -0,0 +1,16 @@ +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import Optional, List + + +def main(args=None): + # type: (Optional[List[str]]) -> int + """This is preserved for old console scripts that may still be referencing + it. + + For additional details, see https://github.com/pypa/pip/issues/7498. + """ + from pip._internal.utils.entrypoints import _wrapper + + return _wrapper(args) diff --git a/src/pip/_internal/utils/entrypoints.py b/src/pip/_internal/utils/entrypoints.py new file mode 100644 index 00000000000..308832163eb --- /dev/null +++ b/src/pip/_internal/utils/entrypoints.py @@ -0,0 +1,21 @@ +from pip._internal.cli.main import main +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import Optional, List + + +def _wrapper(args=None): + # type: (Optional[List[str]]) -> int + """Central wrapper for all old entrypoints. + + Historically pip has had several entrypoints defined. Because of issues + arising from PATH, sys.path, multiple Pythons, their interactions, and most + of them having a pip installed, users suffer every time an entrypoint gets + moved. + + To alleviate this pain, and provide a mechanism for warning users and + directing them to an appropriate place for help, we now define all of + our old entrypoints as wrappers for the current one. + """ + return main(args) diff --git a/tests/functional/test_cli.py b/tests/functional/test_cli.py new file mode 100644 index 00000000000..0c51938297f --- /dev/null +++ b/tests/functional/test_cli.py @@ -0,0 +1,33 @@ +"""Basic CLI functionality checks. +""" +from textwrap import dedent + +import pytest + + +@pytest.mark.parametrize("entrypoint", [ + ("fake_pip = pip._internal.main:main",), + ("fake_pip = pip._internal:main",), + ("fake_pip = pip:main",), +]) +def test_entrypoints_work(entrypoint, script): + fake_pkg = script.temp_path / "fake_pkg" + fake_pkg.mkdir() + fake_pkg.joinpath("setup.py").write_text(dedent(""" + from setuptools import setup + + setup( + name="fake-pip", + version="0.1.0", + entry_points={{ + "console_scripts": [ + {!r} + ] + }} + ) + """.format(entrypoint))) + + script.pip("install", "-vvv", str(fake_pkg)) + result = script.pip("-V") + result2 = script.run("fake_pip", "-V") + assert result.stdout == result2.stdout From 973cb349ba18327839fd10825cb40baac6b08ffd Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 18 Dec 2019 21:13:15 -0500 Subject: [PATCH 0959/3170] Add news --- news/7498.feature | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 news/7498.feature diff --git a/news/7498.feature b/news/7498.feature new file mode 100644 index 00000000000..52f0c57a327 --- /dev/null +++ b/news/7498.feature @@ -0,0 +1,2 @@ +Define all old pip console script entrypoints to prevent import issues in +stale wrapper scripts. From 82b456e043397e212959ec5ac3e13c93e867c1cb Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 20 Dec 2019 14:15:52 +0800 Subject: [PATCH 0960/3170] Fix typo in docstring Co-Authored-By: Christopher Hunt <chrahunt@gmail.com> --- src/pip/_internal/utils/appdirs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/utils/appdirs.py b/src/pip/_internal/utils/appdirs.py index cce1e293c00..56850d38629 100644 --- a/src/pip/_internal/utils/appdirs.py +++ b/src/pip/_internal/utils/appdirs.py @@ -2,7 +2,7 @@ This code wraps the vendored appdirs module to so the return values are compatible for the current pip code base. -The intention is to rewrite current usages guradually, keeping the tests pass, +The intention is to rewrite current usages gradually, keeping the tests pass, and eventually drop this after all usages are changed. """ From 368c811467ed851d317ca89559c1c2799f45ff43 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 20 Dec 2019 14:13:42 +0800 Subject: [PATCH 0961/3170] Treat Windows an macOS as special case in appdirs --- src/pip/_internal/utils/appdirs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/utils/appdirs.py b/src/pip/_internal/utils/appdirs.py index 56850d38629..251c5fd59a5 100644 --- a/src/pip/_internal/utils/appdirs.py +++ b/src/pip/_internal/utils/appdirs.py @@ -36,6 +36,6 @@ def user_data_dir(appname, roaming=False): def site_config_dirs(appname): # type: (str) -> List[str] dirval = _appdirs.site_config_dir(appname, appauthor=False, multipath=True) - if _appdirs.system == "linux2": + if _appdirs.system not in ["win32", "darwin"]: return dirval.split(os.pathsep) return [dirval] From 2ccc5c055db7247b4529569e9ddf3185b538060d Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 20 Dec 2019 14:36:30 +0800 Subject: [PATCH 0962/3170] Match site_config_dirs for empty XDG_CONFIG_DIRS --- src/pip/_vendor/appdirs.py | 2 +- tests/unit/test_appdirs.py | 8 ++++++++ tools/automation/vendoring/patches/appdirs.patch | 9 +++++++-- 3 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/pip/_vendor/appdirs.py b/src/pip/_vendor/appdirs.py index 92b80251e14..e9ff1aa4f0f 100644 --- a/src/pip/_vendor/appdirs.py +++ b/src/pip/_vendor/appdirs.py @@ -247,7 +247,7 @@ def site_config_dir(appname=None, appauthor=None, version=None, multipath=False) # XDG default for $XDG_CONFIG_DIRS # only first, if multipath is False path = os.getenv('XDG_CONFIG_DIRS', '/etc/xdg') - pathlist = [os.path.expanduser(x.rstrip(os.sep)) for x in path.split(os.pathsep)] + pathlist = [os.path.expanduser(x.rstrip(os.sep)) for x in path.split(os.pathsep) if x] if appname: if version: appname = os.path.join(appname, version) diff --git a/tests/unit/test_appdirs.py b/tests/unit/test_appdirs.py index 1a01464174f..402d0db311b 100644 --- a/tests/unit/test_appdirs.py +++ b/tests/unit/test_appdirs.py @@ -138,6 +138,14 @@ def test_site_config_dirs_linux_override(self, monkeypatch): '/etc' ] + def test_site_config_dirs_linux_empty(self, monkeypatch): + monkeypatch.setattr(_appdirs, "system", "linux2") + monkeypatch.setattr(os, "path", posixpath) + monkeypatch.setattr(os, "pathsep", ':') + monkeypatch.setenv("XDG_CONFIG_DIRS", "") + monkeypatch.setattr(sys, "platform", "linux2") + assert appdirs.site_config_dirs("pip") == ['/etc'] + class TestUserDataDir: diff --git a/tools/automation/vendoring/patches/appdirs.patch b/tools/automation/vendoring/patches/appdirs.patch index 136f34b4898..6c6f37b45c5 100644 --- a/tools/automation/vendoring/patches/appdirs.patch +++ b/tools/automation/vendoring/patches/appdirs.patch @@ -1,5 +1,5 @@ diff --git a/src/pip/_vendor/appdirs.py b/src/pip/_vendor/appdirs.py -index ae67001a..92b80251 100644 +index ae67001a..e9ff1aa4 100644 --- a/src/pip/_vendor/appdirs.py +++ b/src/pip/_vendor/appdirs.py @@ -64,7 +64,7 @@ def user_data_dir(appname=None, appauthor=None, version=None, roaming=False): @@ -40,7 +40,12 @@ index ae67001a..92b80251 100644 def site_config_dir(appname=None, appauthor=None, version=None, multipath=False): r"""Return full path to the user-shared data dir for this application. -@@ -245,7 +251,9 @@ def site_config_dir(appname=None, appauthor=None, version=None, multipath=False) +@@ -241,11 +247,13 @@ def site_config_dir(appname=None, appauthor=None, version=None, multipath=False) + # XDG default for $XDG_CONFIG_DIRS + # only first, if multipath is False + path = os.getenv('XDG_CONFIG_DIRS', '/etc/xdg') +- pathlist = [os.path.expanduser(x.rstrip(os.sep)) for x in path.split(os.pathsep)] ++ pathlist = [os.path.expanduser(x.rstrip(os.sep)) for x in path.split(os.pathsep) if x] if appname: if version: appname = os.path.join(appname, version) From f6afa1a15423566c7e16ce8de97e32bdc9b8843d Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 20 Dec 2019 15:21:34 +0800 Subject: [PATCH 0963/3170] Make appdirs detect IronPython Windows --- src/pip/_vendor/appdirs.py | 4 ++ .../vendoring/patches/appdirs.patch | 45 ++++++++++++------- 2 files changed, 32 insertions(+), 17 deletions(-) diff --git a/src/pip/_vendor/appdirs.py b/src/pip/_vendor/appdirs.py index e9ff1aa4f0f..c3db25de30d 100644 --- a/src/pip/_vendor/appdirs.py +++ b/src/pip/_vendor/appdirs.py @@ -37,6 +37,10 @@ # are actually checked for and the rest of the module expects # *sys.platform* style strings. system = 'linux2' +elif sys.platform == 'cli' and os.name == 'nt': + # Detect Windows in IronPython to match pip._internal.utils.compat.WINDOWS + # Discussion: <https://github.com/pypa/pip/pull/7501> + system = 'win32' else: system = sys.platform diff --git a/tools/automation/vendoring/patches/appdirs.patch b/tools/automation/vendoring/patches/appdirs.patch index 6c6f37b45c5..1c8d087af70 100644 --- a/tools/automation/vendoring/patches/appdirs.patch +++ b/tools/automation/vendoring/patches/appdirs.patch @@ -1,17 +1,28 @@ diff --git a/src/pip/_vendor/appdirs.py b/src/pip/_vendor/appdirs.py -index ae67001a..e9ff1aa4 100644 +index ae67001a..87a1e0a6 100644 --- a/src/pip/_vendor/appdirs.py +++ b/src/pip/_vendor/appdirs.py -@@ -64,7 +64,7 @@ def user_data_dir(appname=None, appauthor=None, version=None, roaming=False): +@@ -37,6 +37,10 @@ if sys.platform.startswith('java'): + # are actually checked for and the rest of the module expects + # *sys.platform* style strings. + system = 'linux2' ++elif sys.platform == 'cli' and os.name == 'nt': ++ # Detect Windows in IronPython to match pip._internal.utils.compat.WINDOWS ++ # Discussion: <https://github.com/pypa/pip/pull/7501> ++ system = 'win32' + else: + system = sys.platform + +@@ -64,7 +68,7 @@ def user_data_dir(appname=None, appauthor=None, version=None, roaming=False): for a discussion of issues. - + Typical user data directories are: - Mac OS X: ~/Library/Application Support/<AppName> + Mac OS X: ~/Library/Application Support/<AppName> # or ~/.config/<AppName>, if the other does not exist Unix: ~/.local/share/<AppName> # or in $XDG_DATA_HOME, if defined Win XP (not roaming): C:\Documents and Settings\<username>\Application Data\<AppAuthor>\<AppName> Win XP (roaming): C:\Documents and Settings\<username>\Local Settings\Application Data\<AppAuthor>\<AppName> -@@ -88,6 +88,10 @@ def user_data_dir(appname=None, appauthor=None, version=None, roaming=False): +@@ -88,6 +92,10 @@ def user_data_dir(appname=None, appauthor=None, version=None, roaming=False): path = os.path.expanduser('~/Library/Application Support/') if appname: path = os.path.join(path, appname) @@ -22,25 +33,25 @@ index ae67001a..e9ff1aa4 100644 else: path = os.getenv('XDG_DATA_HOME', os.path.expanduser("~/.local/share")) if appname: -@@ -150,7 +154,7 @@ def site_data_dir(appname=None, appauthor=None, version=None, multipath=False): +@@ -150,7 +158,7 @@ def site_data_dir(appname=None, appauthor=None, version=None, multipath=False): if appname: if version: appname = os.path.join(appname, version) - pathlist = [os.sep.join([x, appname]) for x in pathlist] + pathlist = [os.path.join(x, appname) for x in pathlist] - + if multipath: path = os.pathsep.join(pathlist) -@@ -203,6 +207,8 @@ def user_config_dir(appname=None, appauthor=None, version=None, roaming=False): +@@ -203,6 +211,8 @@ def user_config_dir(appname=None, appauthor=None, version=None, roaming=False): return path - - + + +# for the discussion regarding site_config_dir locations +# see <https://github.com/pypa/pip/issues/1733> def site_config_dir(appname=None, appauthor=None, version=None, multipath=False): r"""Return full path to the user-shared data dir for this application. - -@@ -241,11 +247,13 @@ def site_config_dir(appname=None, appauthor=None, version=None, multipath=False) + +@@ -241,11 +251,13 @@ def site_config_dir(appname=None, appauthor=None, version=None, multipath=False) # XDG default for $XDG_CONFIG_DIRS # only first, if multipath is False path = os.getenv('XDG_CONFIG_DIRS', '/etc/xdg') @@ -53,10 +64,10 @@ index ae67001a..e9ff1aa4 100644 + pathlist = [os.path.join(x, appname) for x in pathlist] + # always look in /etc directly as well + pathlist.append('/etc') - + if multipath: path = os.pathsep.join(pathlist) -@@ -291,6 +299,10 @@ def user_cache_dir(appname=None, appauthor=None, version=None, opinion=True): +@@ -291,6 +303,10 @@ def user_cache_dir(appname=None, appauthor=None, version=None, opinion=True): if appauthor is None: appauthor = appname path = os.path.normpath(_get_win_folder("CSIDL_LOCAL_APPDATA")) @@ -67,8 +78,8 @@ index ae67001a..e9ff1aa4 100644 if appname: if appauthor is not False: path = os.path.join(path, appauthor, appname) -@@ -557,18 +569,32 @@ def _get_win_folder_with_jna(csidl_name): - +@@ -557,18 +573,32 @@ def _get_win_folder_with_jna(csidl_name): + if system == "win32": try: - import win32com.shell @@ -106,6 +117,6 @@ index ae67001a..e9ff1aa4 100644 + except (UnicodeEncodeError, LookupError): + pass + return path - - + + #---- self test code From 38585adaecd88e386fe6fb434e37e4501a0cd45a Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 19 Dec 2019 22:17:14 -0500 Subject: [PATCH 0964/3170] Add warning when using old console script wrappers We use sys.stderr instead of our `deprecated` helper because logging is not set up at the time this is executed. --- src/pip/_internal/utils/entrypoints.py | 10 ++++++++++ tests/functional/test_cli.py | 3 ++- 2 files changed, 12 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/utils/entrypoints.py b/src/pip/_internal/utils/entrypoints.py index 308832163eb..befd01c8901 100644 --- a/src/pip/_internal/utils/entrypoints.py +++ b/src/pip/_internal/utils/entrypoints.py @@ -1,3 +1,5 @@ +import sys + from pip._internal.cli.main import main from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -18,4 +20,12 @@ def _wrapper(args=None): directing them to an appropriate place for help, we now define all of our old entrypoints as wrappers for the current one. """ + sys.stderr.write( + "WARNING: pip is being invoked by an old script wrapper. This will " + "fail in a future version of pip.\n" + "Please see https://github.com/pypa/pip/issues/5599 for advice on " + "fixing the underlying issue.\n" + "To avoid this problem you can invoke Python with '-m pip' instead of " + "running pip directly.\n" + ) return main(args) diff --git a/tests/functional/test_cli.py b/tests/functional/test_cli.py index 0c51938297f..e416315125f 100644 --- a/tests/functional/test_cli.py +++ b/tests/functional/test_cli.py @@ -29,5 +29,6 @@ def test_entrypoints_work(entrypoint, script): script.pip("install", "-vvv", str(fake_pkg)) result = script.pip("-V") - result2 = script.run("fake_pip", "-V") + result2 = script.run("fake_pip", "-V", allow_stderr_warning=True) assert result.stdout == result2.stdout + assert "old script wrapper" in result2.stderr From de217b54afe35eb3216d7bfdc6706618ca87b353 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Mon, 16 Dec 2019 18:24:14 -0500 Subject: [PATCH 0965/3170] Only allow one top-level .dist-info directory in wheels --- src/pip/_internal/operations/install/wheel.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 627cc4a32a6..01b48059c0a 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -22,7 +22,6 @@ from pip._vendor import pkg_resources from pip._vendor.distlib.scripts import ScriptMaker from pip._vendor.distlib.util import get_export_entry -from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.six import StringIO from pip._internal.exceptions import InstallationError, UnsupportedWheel @@ -381,8 +380,7 @@ def clobber( continue elif ( is_base and - s.endswith('.dist-info') and - canonicalize_name(s).startswith(canonicalize_name(name)) + s.endswith('.dist-info') ): assert not info_dir, ( 'Multiple .dist-info directories: {}, '.format( From 0d865d8fe3277b9091beec6b3fbe05abb2cd322b Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 20 Dec 2019 22:38:12 -0500 Subject: [PATCH 0966/3170] Add test for multiple .dist-info in wheel --- tests/functional/test_install_wheel.py | 19 +++++++++++++++++++ tests/lib/__init__.py | 8 ++++++-- 2 files changed, 25 insertions(+), 2 deletions(-) diff --git a/tests/functional/test_install_wheel.py b/tests/functional/test_install_wheel.py index e218d6e7f17..2b4e50fe004 100644 --- a/tests/functional/test_install_wheel.py +++ b/tests/functional/test_install_wheel.py @@ -4,6 +4,7 @@ import pytest +from tests.lib import create_basic_wheel_for_package from tests.lib.path import Path @@ -432,3 +433,21 @@ def test_wheel_install_with_no_cache_dir(script, tmpdir, data): package = data.packages.joinpath("simple.dist-0.1-py2.py3-none-any.whl") result = script.pip('install', '--no-cache-dir', '--no-index', package) result.assert_installed('simpledist', editable=False) + + +def test_wheel_install_fails_with_extra_dist_info(script): + package = create_basic_wheel_for_package( + script, + "simple", + "0.1.0", + extra_files={ + "unrelated-2.0.0.dist-info/WHEEL": "Wheel-Version: 1.0", + "unrelated-2.0.0.dist-info/METADATA": ( + "Name: unrelated\nVersion: 2.0.0\n" + ), + }, + ) + result = script.pip( + "install", "--no-cache-dir", "--no-index", package, expect_error=True + ) + assert "Multiple .dist-info directories" in result.stderr diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index 07cc0ed5f76..af80078df87 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -921,8 +921,9 @@ def create_test_package_with_setup(script, **setup_kwargs): return pkg_path -def create_basic_wheel_for_package(script, name, version, - depends=None, extras=None): +def create_basic_wheel_for_package( + script, name, version, depends=None, extras=None, extra_files=None +): if depends is None: depends = [] if extras is None: @@ -966,6 +967,9 @@ def hello(): "{dist_info}/RECORD": "" } + if extra_files: + files.update(extra_files) + # Some useful shorthands archive_name = "{name}-{version}-py2.py3-none-any.whl".format( name=name, version=version From e5495cf6a1c576362835f72d41263d7af43001ba Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 21 Dec 2019 11:14:50 -0500 Subject: [PATCH 0967/3170] Raise UnsupportedWheel when .dist-info doesn't match name This aligns with the previous behavior that would have enforced the found .dist-info directory starting with the name of the package. We raise UnsupportedWheel because it looks better in output than the AssertionError (which includes traceback). --- src/pip/_internal/operations/install/wheel.py | 16 ++++++++++++-- tests/functional/test_install_wheel.py | 21 +++++++++++++++++++ 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 01b48059c0a..4b10a9c009f 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -22,6 +22,7 @@ from pip._vendor import pkg_resources from pip._vendor.distlib.scripts import ScriptMaker from pip._vendor.distlib.util import get_export_entry +from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.six import StringIO from pip._internal.exceptions import InstallationError, UnsupportedWheel @@ -314,8 +315,10 @@ def install_unpacked_wheel( :param pycompile: Whether to byte-compile installed Python files :param warn_script_location: Whether to check that scripts are installed into a directory on PATH - :raises UnsupportedWheel: when the directory holds an unpacked wheel with - incompatible Wheel-Version + :raises UnsupportedWheel: + * when the directory holds an unpacked wheel with incompatible + Wheel-Version + * when the .dist-info dir does not match the wheel """ # TODO: Investigate and break this up. # TODO: Look into moving this into a dedicated class for representing an @@ -443,6 +446,15 @@ def clobber( req_description ) + info_dir_name = canonicalize_name(os.path.basename(info_dir[0])) + canonical_name = canonicalize_name(name) + if not info_dir_name.startswith(canonical_name): + raise UnsupportedWheel( + "{} .dist-info directory {!r} does not start with {!r}".format( + req_description, os.path.basename(info_dir[0]), canonical_name + ) + ) + # Get the defined entry points ep_file = os.path.join(info_dir[0], 'entry_points.txt') console, gui = get_entrypoints(ep_file) diff --git a/tests/functional/test_install_wheel.py b/tests/functional/test_install_wheel.py index 2b4e50fe004..4c1a9285406 100644 --- a/tests/functional/test_install_wheel.py +++ b/tests/functional/test_install_wheel.py @@ -1,6 +1,7 @@ import distutils import glob import os +import shutil import pytest @@ -451,3 +452,23 @@ def test_wheel_install_fails_with_extra_dist_info(script): "install", "--no-cache-dir", "--no-index", package, expect_error=True ) assert "Multiple .dist-info directories" in result.stderr + + +def test_wheel_install_fails_with_unrelated_dist_info(script): + package = create_basic_wheel_for_package(script, "simple", "0.1.0") + new_name = "unrelated-2.0.0-py2.py3-none-any.whl" + new_package = os.path.join(os.path.dirname(package), new_name) + shutil.move(package, new_package) + + result = script.pip( + "install", + "--no-cache-dir", + "--no-index", + new_package, + expect_error=True, + ) + + assert ( + "'simple-0.1.0.dist-info' does not start with 'unrelated'" + in result.stderr + ) From 649a4f3fb0241045af6282e62efc7b8be189eb31 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 21 Dec 2019 11:22:33 -0500 Subject: [PATCH 0968/3170] Add news --- news/7487.removal | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 news/7487.removal diff --git a/news/7487.removal b/news/7487.removal new file mode 100644 index 00000000000..aa89bf42ad0 --- /dev/null +++ b/news/7487.removal @@ -0,0 +1,2 @@ +Wheel processing no longer permits wheels containing more than one top-level +.dist-info directory. From c98c0ad79c5f4530cb0bbb33358d414884f0a27a Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Mon, 23 Dec 2019 14:27:44 +0800 Subject: [PATCH 0969/3170] Default to /etc/xdg if XDG_CONFIG_DIRS if empty --- src/pip/_vendor/appdirs.py | 5 ++- tests/unit/test_appdirs.py | 2 +- .../vendoring/patches/appdirs.patch | 38 +++++++++++-------- 3 files changed, 26 insertions(+), 19 deletions(-) diff --git a/src/pip/_vendor/appdirs.py b/src/pip/_vendor/appdirs.py index c3db25de30d..3a52b75846b 100644 --- a/src/pip/_vendor/appdirs.py +++ b/src/pip/_vendor/appdirs.py @@ -248,9 +248,10 @@ def site_config_dir(appname=None, appauthor=None, version=None, multipath=False) if appname and version: path = os.path.join(path, version) else: - # XDG default for $XDG_CONFIG_DIRS + # XDG default for $XDG_CONFIG_DIRS (missing or empty) + # see <https://github.com/pypa/pip/pull/7501#discussion_r360624829> # only first, if multipath is False - path = os.getenv('XDG_CONFIG_DIRS', '/etc/xdg') + path = os.getenv('XDG_CONFIG_DIRS') or '/etc/xdg' pathlist = [os.path.expanduser(x.rstrip(os.sep)) for x in path.split(os.pathsep) if x] if appname: if version: diff --git a/tests/unit/test_appdirs.py b/tests/unit/test_appdirs.py index 402d0db311b..6b2ca8ff196 100644 --- a/tests/unit/test_appdirs.py +++ b/tests/unit/test_appdirs.py @@ -144,7 +144,7 @@ def test_site_config_dirs_linux_empty(self, monkeypatch): monkeypatch.setattr(os, "pathsep", ':') monkeypatch.setenv("XDG_CONFIG_DIRS", "") monkeypatch.setattr(sys, "platform", "linux2") - assert appdirs.site_config_dirs("pip") == ['/etc'] + assert appdirs.site_config_dirs("pip") == ['/etc/xdg/pip', '/etc'] class TestUserDataDir: diff --git a/tools/automation/vendoring/patches/appdirs.patch b/tools/automation/vendoring/patches/appdirs.patch index 1c8d087af70..a6135c35f9e 100644 --- a/tools/automation/vendoring/patches/appdirs.patch +++ b/tools/automation/vendoring/patches/appdirs.patch @@ -1,5 +1,5 @@ diff --git a/src/pip/_vendor/appdirs.py b/src/pip/_vendor/appdirs.py -index ae67001a..87a1e0a6 100644 +index ae67001a..3a52b758 100644 --- a/src/pip/_vendor/appdirs.py +++ b/src/pip/_vendor/appdirs.py @@ -37,6 +37,10 @@ if sys.platform.startswith('java'): @@ -12,10 +12,10 @@ index ae67001a..87a1e0a6 100644 + system = 'win32' else: system = sys.platform - + @@ -64,7 +68,7 @@ def user_data_dir(appname=None, appauthor=None, version=None, roaming=False): for a discussion of issues. - + Typical user data directories are: - Mac OS X: ~/Library/Application Support/<AppName> + Mac OS X: ~/Library/Application Support/<AppName> # or ~/.config/<AppName>, if the other does not exist @@ -39,23 +39,29 @@ index ae67001a..87a1e0a6 100644 appname = os.path.join(appname, version) - pathlist = [os.sep.join([x, appname]) for x in pathlist] + pathlist = [os.path.join(x, appname) for x in pathlist] - + if multipath: path = os.pathsep.join(pathlist) @@ -203,6 +211,8 @@ def user_config_dir(appname=None, appauthor=None, version=None, roaming=False): return path - - + + +# for the discussion regarding site_config_dir locations +# see <https://github.com/pypa/pip/issues/1733> def site_config_dir(appname=None, appauthor=None, version=None, multipath=False): r"""Return full path to the user-shared data dir for this application. - -@@ -241,11 +251,13 @@ def site_config_dir(appname=None, appauthor=None, version=None, multipath=False) - # XDG default for $XDG_CONFIG_DIRS + +@@ -238,14 +248,17 @@ def site_config_dir(appname=None, appauthor=None, version=None, multipath=False) + if appname and version: + path = os.path.join(path, version) + else: +- # XDG default for $XDG_CONFIG_DIRS ++ # XDG default for $XDG_CONFIG_DIRS (missing or empty) ++ # see <https://github.com/pypa/pip/pull/7501#discussion_r360624829> # only first, if multipath is False - path = os.getenv('XDG_CONFIG_DIRS', '/etc/xdg') +- path = os.getenv('XDG_CONFIG_DIRS', '/etc/xdg') - pathlist = [os.path.expanduser(x.rstrip(os.sep)) for x in path.split(os.pathsep)] ++ path = os.getenv('XDG_CONFIG_DIRS') or '/etc/xdg' + pathlist = [os.path.expanduser(x.rstrip(os.sep)) for x in path.split(os.pathsep) if x] if appname: if version: @@ -64,10 +70,10 @@ index ae67001a..87a1e0a6 100644 + pathlist = [os.path.join(x, appname) for x in pathlist] + # always look in /etc directly as well + pathlist.append('/etc') - + if multipath: path = os.pathsep.join(pathlist) -@@ -291,6 +303,10 @@ def user_cache_dir(appname=None, appauthor=None, version=None, opinion=True): +@@ -291,6 +304,10 @@ def user_cache_dir(appname=None, appauthor=None, version=None, opinion=True): if appauthor is None: appauthor = appname path = os.path.normpath(_get_win_folder("CSIDL_LOCAL_APPDATA")) @@ -78,8 +84,8 @@ index ae67001a..87a1e0a6 100644 if appname: if appauthor is not False: path = os.path.join(path, appauthor, appname) -@@ -557,18 +573,32 @@ def _get_win_folder_with_jna(csidl_name): - +@@ -557,18 +574,32 @@ def _get_win_folder_with_jna(csidl_name): + if system == "win32": try: - import win32com.shell @@ -117,6 +123,6 @@ index ae67001a..87a1e0a6 100644 + except (UnicodeEncodeError, LookupError): + pass + return path - - + + #---- self test code From 554db9230d035117dd36d39b3c6953006bf69577 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 24 Dec 2019 16:56:24 -0500 Subject: [PATCH 0970/3170] Move directory requirement download logging out of unpack_file_url One less use of `download_dir` in `unpack_file_url`, which will make it easier to factor out. --- src/pip/_internal/operations/prepare.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 23f2e2f1982..78144476866 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -233,8 +233,6 @@ def unpack_file_url( if os.path.isdir(location): rmtree(location) _copy_source_tree(link_path, location) - if download_dir: - logger.info('Link is a directory, ignoring download_dir') return None # If a download dir is specified, is the file already there and valid? @@ -544,6 +542,10 @@ def prepare_linked_requirement( req, self.req_tracker, self.finder, self.build_isolation, ) + if link.is_existing_dir(): + if download_dir: + logger.info('Link is a directory, ignoring download_dir') + if self._download_should_save: # Make a .zip of the source_dir we already created. if link.is_vcs: From 5c901454621ba2958a85bd9eb76731a27bd88035 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 24 Dec 2019 16:59:24 -0500 Subject: [PATCH 0971/3170] Switch conditions --- src/pip/_internal/operations/prepare.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 78144476866..11afbd93db8 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -542,8 +542,8 @@ def prepare_linked_requirement( req, self.req_tracker, self.finder, self.build_isolation, ) - if link.is_existing_dir(): - if download_dir: + if download_dir: + if link.is_existing_dir(): logger.info('Link is a directory, ignoring download_dir') if self._download_should_save: From 37f3fd70874d846661d7caed279beed69118a616 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Wed, 25 Dec 2019 17:37:58 +0530 Subject: [PATCH 0972/3170] Move legacy wheel build process --- .../operations/build/wheel_legacy.py | 115 ++++++++++++++++++ src/pip/_internal/wheel_builder.py | 110 +---------------- tests/unit/test_wheel.py | 4 +- tests/unit/test_wheel_builder.py | 7 +- 4 files changed, 126 insertions(+), 110 deletions(-) create mode 100644 src/pip/_internal/operations/build/wheel_legacy.py diff --git a/src/pip/_internal/operations/build/wheel_legacy.py b/src/pip/_internal/operations/build/wheel_legacy.py new file mode 100644 index 00000000000..3ebd9fe444b --- /dev/null +++ b/src/pip/_internal/operations/build/wheel_legacy.py @@ -0,0 +1,115 @@ +import logging +import os.path + +from pip._internal.utils.setuptools_build import ( + make_setuptools_bdist_wheel_args, +) +from pip._internal.utils.subprocess import ( + LOG_DIVIDER, + call_subprocess, + format_command_args, +) +from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from pip._internal.utils.ui import open_spinner + +if MYPY_CHECK_RUNNING: + from typing import List, Optional, Text + +logger = logging.getLogger(__name__) + + +def format_command_result( + command_args, # type: List[str] + command_output, # type: Text +): + # type: (...) -> str + """Format command information for logging.""" + command_desc = format_command_args(command_args) + text = 'Command arguments: {}\n'.format(command_desc) + + if not command_output: + text += 'Command output: None' + elif logger.getEffectiveLevel() > logging.DEBUG: + text += 'Command output: [use --verbose to show]' + else: + if not command_output.endswith('\n'): + command_output += '\n' + text += 'Command output:\n{}{}'.format(command_output, LOG_DIVIDER) + + return text + + +def get_legacy_build_wheel_path( + names, # type: List[str] + temp_dir, # type: str + name, # type: str + command_args, # type: List[str] + command_output, # type: Text +): + # type: (...) -> Optional[str] + """Return the path to the wheel in the temporary build directory.""" + # Sort for determinism. + names = sorted(names) + if not names: + msg = ( + 'Legacy build of wheel for {!r} created no files.\n' + ).format(name) + msg += format_command_result(command_args, command_output) + logger.warning(msg) + return None + + if len(names) > 1: + msg = ( + 'Legacy build of wheel for {!r} created more than one file.\n' + 'Filenames (choosing first): {}\n' + ).format(name, names) + msg += format_command_result(command_args, command_output) + logger.warning(msg) + + return os.path.join(temp_dir, names[0]) + + +def build_wheel_legacy( + name, # type: str + setup_py_path, # type: str + source_dir, # type: str + global_options, # type: List[str] + build_options, # type: List[str] + tempd, # type: str +): + # type: (...) -> Optional[str] + """Build one unpacked package using the "legacy" build process. + + Returns path to wheel if successfully built. Otherwise, returns None. + """ + wheel_args = make_setuptools_bdist_wheel_args( + setup_py_path, + global_options=global_options, + build_options=build_options, + destination_dir=tempd, + ) + + spin_message = 'Building wheel for %s (setup.py)' % (name,) + with open_spinner(spin_message) as spinner: + logger.debug('Destination directory: %s', tempd) + + try: + output = call_subprocess( + wheel_args, + cwd=source_dir, + spinner=spinner, + ) + except Exception: + spinner.finish("error") + logger.error('Failed building wheel for %s', name) + return None + + names = os.listdir(tempd) + wheel_path = get_legacy_build_wheel_path( + names=names, + temp_dir=tempd, + name=name, + command_args=wheel_args, + command_output=output, + ) + return wheel_path diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index f099fd040d2..8928f637be9 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -10,29 +10,24 @@ import shutil from pip._internal.models.link import Link +from pip._internal.operations.build.wheel_legacy import build_wheel_legacy from pip._internal.utils.logging import indent_log from pip._internal.utils.marker_files import has_delete_marker_file from pip._internal.utils.misc import ensure_dir, hash_file -from pip._internal.utils.setuptools_build import ( - make_setuptools_bdist_wheel_args, - make_setuptools_clean_args, -) +from pip._internal.utils.setuptools_build import make_setuptools_clean_args from pip._internal.utils.subprocess import ( - LOG_DIVIDER, call_subprocess, - format_command_args, runner_with_spinner_message, ) from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from pip._internal.utils.ui import open_spinner from pip._internal.utils.unpacking import unpack_file from pip._internal.utils.urls import path_to_url from pip._internal.vcs import vcs if MYPY_CHECK_RUNNING: from typing import ( - Any, Callable, Iterable, List, Optional, Pattern, Text, Tuple, + Any, Callable, Iterable, List, Optional, Pattern, Tuple, ) from pip._internal.cache import WheelCache @@ -131,103 +126,6 @@ def should_cache( return False -def format_command_result( - command_args, # type: List[str] - command_output, # type: Text -): - # type: (...) -> str - """Format command information for logging.""" - command_desc = format_command_args(command_args) - text = 'Command arguments: {}\n'.format(command_desc) - - if not command_output: - text += 'Command output: None' - elif logger.getEffectiveLevel() > logging.DEBUG: - text += 'Command output: [use --verbose to show]' - else: - if not command_output.endswith('\n'): - command_output += '\n' - text += 'Command output:\n{}{}'.format(command_output, LOG_DIVIDER) - - return text - - -def get_legacy_build_wheel_path( - names, # type: List[str] - temp_dir, # type: str - name, # type: str - command_args, # type: List[str] - command_output, # type: Text -): - # type: (...) -> Optional[str] - """Return the path to the wheel in the temporary build directory.""" - # Sort for determinism. - names = sorted(names) - if not names: - msg = ( - 'Legacy build of wheel for {!r} created no files.\n' - ).format(name) - msg += format_command_result(command_args, command_output) - logger.warning(msg) - return None - - if len(names) > 1: - msg = ( - 'Legacy build of wheel for {!r} created more than one file.\n' - 'Filenames (choosing first): {}\n' - ).format(name, names) - msg += format_command_result(command_args, command_output) - logger.warning(msg) - - return os.path.join(temp_dir, names[0]) - - -def _build_wheel_legacy( - name, # type: str - setup_py_path, # type: str - source_dir, # type: str - global_options, # type: List[str] - build_options, # type: List[str] - tempd, # type: str -): - # type: (...) -> Optional[str] - """Build one unpacked package using the "legacy" build process. - - Returns path to wheel if successfully built. Otherwise, returns None. - """ - wheel_args = make_setuptools_bdist_wheel_args( - setup_py_path, - global_options=global_options, - build_options=build_options, - destination_dir=tempd, - ) - - spin_message = 'Building wheel for %s (setup.py)' % (name,) - with open_spinner(spin_message) as spinner: - logger.debug('Destination directory: %s', tempd) - - try: - output = call_subprocess( - wheel_args, - cwd=source_dir, - spinner=spinner, - ) - except Exception: - spinner.finish("error") - logger.error('Failed building wheel for %s', name) - return None - - names = os.listdir(tempd) - wheel_path = get_legacy_build_wheel_path( - names=names, - temp_dir=tempd, - name=name, - command_args=wheel_args, - command_output=output, - ) - return wheel_path - - def _build_wheel_pep517( name, # type: str backend, # type: Pep517HookCaller @@ -363,7 +261,7 @@ def _build_one_inside_env( tempd=temp_dir.path, ) else: - wheel_path = _build_wheel_legacy( + wheel_path = build_wheel_legacy( name=req.name, setup_py_path=req.setup_py_path, source_dir=req.unpacked_source_directory, diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 52e4e15aaed..8656d842aa7 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -11,6 +11,9 @@ from pip._internal.exceptions import UnsupportedWheel from pip._internal.locations import get_scheme from pip._internal.models.scheme import Scheme +from pip._internal.operations.build.wheel_legacy import ( + get_legacy_build_wheel_path, +) from pip._internal.operations.install import wheel from pip._internal.operations.install.wheel import ( MissingCallableSuffix, @@ -19,7 +22,6 @@ from pip._internal.utils.compat import WINDOWS from pip._internal.utils.misc import hash_file from pip._internal.utils.unpacking import unpack_file -from pip._internal.wheel_builder import get_legacy_build_wheel_path from tests.lib import DATA_DIR, assert_paths_equal diff --git a/tests/unit/test_wheel_builder.py b/tests/unit/test_wheel_builder.py index bd1b0aa9d82..9d0953518a3 100644 --- a/tests/unit/test_wheel_builder.py +++ b/tests/unit/test_wheel_builder.py @@ -5,6 +5,7 @@ from pip._internal import wheel_builder from pip._internal.models.link import Link +from pip._internal.operations.build.wheel_legacy import format_command_result from tests.lib import _create_test_package @@ -137,7 +138,7 @@ def test_should_cache_git_sha(script, tmpdir): def test_format_command_result__INFO(caplog): caplog.set_level(logging.INFO) - actual = wheel_builder.format_command_result( + actual = format_command_result( # Include an argument with a space to test argument quoting. command_args=['arg1', 'second arg'], command_output='output line 1\noutput line 2\n', @@ -156,7 +157,7 @@ def test_format_command_result__INFO(caplog): ]) def test_format_command_result__DEBUG(caplog, command_output): caplog.set_level(logging.DEBUG) - actual = wheel_builder.format_command_result( + actual = format_command_result( command_args=['arg1', 'arg2'], command_output=command_output, ) @@ -172,7 +173,7 @@ def test_format_command_result__DEBUG(caplog, command_output): @pytest.mark.parametrize('log_level', ['DEBUG', 'INFO']) def test_format_command_result__empty_output(caplog, log_level): caplog.set_level(log_level) - actual = wheel_builder.format_command_result( + actual = format_command_result( command_args=['arg1', 'arg2'], command_output='', ) From ce9ddbb60015de29f7bf4c0d2b24387f5ff9e540 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 24 Dec 2019 17:03:14 -0500 Subject: [PATCH 0973/3170] Move download file copying out of unpacking functions Now our "unpacking" functions aren't also for sometimes populating the download directory. --- src/pip/_internal/operations/prepare.py | 28 +++++-------------------- 1 file changed, 5 insertions(+), 23 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 11afbd93db8..3b6e225a31c 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -165,12 +165,6 @@ def unpack_http_url( # downloading archives, they have to be unpacked to parse dependencies unpack_file(from_path, location, content_type) - # a download dir is specified; let's copy the archive there - if download_dir and not os.path.exists( - os.path.join(download_dir, link.filename) - ): - _copy_file(from_path, download_dir, link) - return from_path @@ -223,9 +217,6 @@ def unpack_file_url( ): # type: (...) -> Optional[str] """Unpack link into location. - - If download_dir is provided and link points to a file, make a copy - of the link file inside download_dir. """ link_path = link.file_path # If it's a url to a local directory @@ -261,12 +252,6 @@ def unpack_file_url( # archives, they have to be unpacked to parse dependencies unpack_file(from_path, location, content_type) - # a download dir is specified and not already downloaded - if download_dir and not os.path.exists( - os.path.join(download_dir, link.filename) - ): - _copy_file(from_path, download_dir, link) - return from_path @@ -278,14 +263,7 @@ def unpack_url( hashes=None, # type: Optional[Hashes] ): # type: (...) -> Optional[str] - """Unpack link. - If link is a VCS link: - if only_download, export into download_dir and ignore location - else unpack into location - for other types of link: - - unpack into location - - if download_dir, copy the file into download_dir - - if only_download, mark location for deletion + """Unpack link into location, downloading if required. :param hashes: A Hashes object, one of whose embedded hashes must match, or HashMismatch will be raised. If the Hashes is empty, no matches are @@ -545,6 +523,10 @@ def prepare_linked_requirement( if download_dir: if link.is_existing_dir(): logger.info('Link is a directory, ignoring download_dir') + elif local_path and not os.path.exists( + os.path.join(download_dir, link.filename) + ): + _copy_file(local_path, download_dir, link) if self._download_should_save: # Make a .zip of the source_dir we already created. From db42a03ee2fa664d275321b7cb2e86f2220508e2 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 26 Dec 2019 14:46:05 -0500 Subject: [PATCH 0974/3170] Remove unnecessary InstallRequirement cleanup in install_given_reqs install_given_reqs is only called from InstallCommand.run, which calls RequirementSet.cleanup_files, which calls InstallRequirement.remove_temporary_source for each InstallRequirement, so the call here was not necessary. We have test coverage affirming this still works as expected in tests/functional/test_install_cleanup.py. --- src/pip/_internal/req/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pip/_internal/req/__init__.py b/src/pip/_internal/req/__init__.py index 827b6cbc229..d2d027adeec 100644 --- a/src/pip/_internal/req/__init__.py +++ b/src/pip/_internal/req/__init__.py @@ -86,7 +86,6 @@ def install_given_reqs( ) if should_commit: uninstalled_pathset.commit() - requirement.remove_temporary_source() installed.append(InstallationResult(requirement.name)) From a4d06aecaa3f018e9604725798f2e8b9bb22024a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Thu, 26 Dec 2019 21:31:55 +0100 Subject: [PATCH 0975/3170] wheel builder: unconditionally update req.link (#7515) We unconditionally update the requirement link with the build wheel (in cache), so when build() will return build success as well as build failure, the caller can obtain the built wheel by looking at req.local_file_path --- src/pip/_internal/wheel_builder.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index f099fd040d2..627cba30496 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -453,6 +453,10 @@ def build( for req, cache_dir in buildset: wheel_file = self._build_one(req, cache_dir) if wheel_file: + # Update the link for this. + req.link = Link(path_to_url(wheel_file)) + req.local_file_path = req.link.file_path + assert req.link.is_wheel if should_unpack: # XXX: This is mildly duplicative with prepare_files, # but not close enough to pull out to a single common @@ -472,10 +476,6 @@ def build( req.source_dir = req.ensure_build_location( self.preparer.build_dir ) - # Update the link for this. - req.link = Link(path_to_url(wheel_file)) - req.local_file_path = req.link.file_path - assert req.link.is_wheel # extract the wheel into the dir unpack_file(req.link.file_path, req.source_dir) else: From cf21401fd73fa74f690dac71386b09b6a8814760 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Thu, 26 Dec 2019 21:11:39 +0100 Subject: [PATCH 0976/3170] Make wheelbuilder.build return successes too Also, pluralize variable names for readability and consistency with similar variables in callers. --- src/pip/_internal/commands/install.py | 2 +- src/pip/_internal/commands/wheel.py | 2 +- src/pip/_internal/wheel_builder.py | 23 ++++++++++++----------- tests/unit/test_command_install.py | 2 +- 4 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index c7dcf28df8a..747d243537d 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -83,7 +83,7 @@ def build_wheels( should_build_legacy = is_wheel_installed() # Always build PEP 517 requirements - build_failures = builder.build( + _, build_failures = builder.build( pep517_requirements, should_unpack=True, ) diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index 8b3ddd0dccf..af50b214f96 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -161,7 +161,7 @@ def run(self, options, args): build_options=options.build_options or [], global_options=options.global_options or [], ) - build_failures = wb.build( + _, build_failures = wb.build( requirement_set.requirements.values(), should_unpack=False, ) diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index 627cba30496..c61be727d09 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -43,6 +43,7 @@ from pip._vendor.pep517.wrappers import Pep517HookCaller BinaryAllowedPredicate = Callable[[InstallRequirement], bool] + BuildResult = Tuple[List[InstallRequirement], List[InstallRequirement]] logger = logging.getLogger(__name__) @@ -413,7 +414,7 @@ def build( requirements, # type: Iterable[InstallRequirement] should_unpack, # type: bool ): - # type: (...) -> List[InstallRequirement] + # type: (...) -> BuildResult """Build wheels. :param should_unpack: If True, after building the wheel, unpack it @@ -437,7 +438,7 @@ def build( need_wheel=not should_unpack, ) if not buildset: - return [] + return [], [] # TODO by @pradyunsg # Should break up this method into 2 separate methods. @@ -449,7 +450,7 @@ def build( ) with indent_log(): - build_success, build_failure = [], [] + build_successes, build_failures = [], [] for req, cache_dir in buildset: wheel_file = self._build_one(req, cache_dir) if wheel_file: @@ -488,22 +489,22 @@ def build( "Building wheel for %s failed: %s", req.name, e, ) - build_failure.append(req) + build_failures.append(req) continue - build_success.append(req) + build_successes.append(req) else: - build_failure.append(req) + build_failures.append(req) # notify success/failure - if build_success: + if build_successes: logger.info( 'Successfully built %s', - ' '.join([req.name for req in build_success]), + ' '.join([req.name for req in build_successes]), ) - if build_failure: + if build_failures: logger.info( 'Failed to build %s', - ' '.join([req.name for req in build_failure]), + ' '.join([req.name for req in build_failures]), ) # Return a list of requirements that failed to build - return build_failure + return build_successes, build_failures diff --git a/tests/unit/test_command_install.py b/tests/unit/test_command_install.py index 7f7154c94f1..9d862792bba 100644 --- a/tests/unit/test_command_install.py +++ b/tests/unit/test_command_install.py @@ -26,7 +26,7 @@ def check_build_wheels( """ def build(reqs, **kwargs): # Fail the first requirement. - return [reqs[0]] + return ([], [reqs[0]]) builder = Mock() builder.build.side_effect = build From 9909b4069a41a74893d324e67e6ffabc18308d3c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Thu, 26 Dec 2019 21:24:32 +0100 Subject: [PATCH 0977/3170] Move final copy operation from wheel_builder to wheel command --- src/pip/_internal/commands/wheel.py | 17 ++++++++++++++++- src/pip/_internal/wheel_builder.py | 12 ------------ 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index af50b214f96..d3fe3430886 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -7,6 +7,7 @@ import logging import os +import shutil from pip._internal.cache import WheelCache from pip._internal.cli import cmdoptions @@ -14,6 +15,7 @@ from pip._internal.exceptions import CommandError, PreviousBuildDirError from pip._internal.req import RequirementSet from pip._internal.req.req_tracker import get_requirement_tracker +from pip._internal.utils.misc import ensure_dir from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.wheel_builder import WheelBuilder @@ -161,10 +163,23 @@ def run(self, options, args): build_options=options.build_options or [], global_options=options.global_options or [], ) - _, build_failures = wb.build( + build_successes, build_failures = wb.build( requirement_set.requirements.values(), should_unpack=False, ) + for req in build_successes: + assert req.link and req.link.is_wheel + assert req.local_file_path + # copy from cache to target directory + try: + ensure_dir(options.wheel_dir) + shutil.copy(req.local_file_path, options.wheel_dir) + except OSError as e: + logger.warning( + "Building wheel for %s failed: %s", + req.name, e, + ) + build_failures.append(req) if len(build_failures) != 0: raise CommandError( "Failed to build one or more wheels" diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index c61be727d09..3f3123cb081 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -479,18 +479,6 @@ def build( ) # extract the wheel into the dir unpack_file(req.link.file_path, req.source_dir) - else: - # copy from cache to target directory - try: - ensure_dir(self._wheel_dir) - shutil.copy(wheel_file, self._wheel_dir) - except OSError as e: - logger.warning( - "Building wheel for %s failed: %s", - req.name, e, - ) - build_failures.append(req) - continue build_successes.append(req) else: build_failures.append(req) From 158ae67910f3e769acd0663fd52b6a1647882034 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Thu, 26 Dec 2019 21:26:48 +0100 Subject: [PATCH 0978/3170] Remove unused _wheel_dir in WheelBuilder --- src/pip/_internal/wheel_builder.py | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index 3f3123cb081..a3692e49787 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -319,8 +319,6 @@ def __init__( 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.check_binary_allowed = check_binary_allowed @@ -422,15 +420,6 @@ def build( for installation. :return: The list of InstallRequirement that failed to build. """ - # pip install uses should_unpack=True. - # pip install never provides a _wheel_dir. - # pip wheel uses should_unpack=False. - # pip wheel always provides a _wheel_dir (via the preparer). - assert ( - (should_unpack and not self._wheel_dir) or - (not should_unpack and self._wheel_dir) - ) - buildset = _collect_buildset( requirements, wheel_cache=self.wheel_cache, From 865539bdad1d64bc2b5f433f2fc8ad496cec4eb2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Sun, 15 Dec 2019 12:51:14 +0100 Subject: [PATCH 0979/3170] Test that pip wheel succeeds when cache dir is not writeable --- tests/functional/test_wheel.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/functional/test_wheel.py b/tests/functional/test_wheel.py index 87083958196..7a66a78fdf9 100644 --- a/tests/functional/test_wheel.py +++ b/tests/functional/test_wheel.py @@ -120,6 +120,22 @@ def test_pip_wheel_builds_when_no_binary_set(script, data): assert "Building wheel for simple" in str(res), str(res) +def test_pip_wheel_readonly_cache(script, data, tmpdir): + cache_dir = tmpdir / "cache" + cache_dir.mkdir() + os.chmod(cache_dir, 0o400) # read-only cache + # Check that the wheel package is ignored + res = script.pip( + 'wheel', '--no-index', + '-f', data.find_links, + '--cache-dir', cache_dir, + 'simple==3.0', + allow_stderr_warning=True, + ) + assert res.returncode == 0 + assert "caching wheels has been disabled" in str(res), str(res) + + def test_pip_wheel_builds_editable_deps(script, data): """ Test 'pip wheel' finds and builds dependencies of editables From 1ee270a8d485ab4deafd2f7d7b950ce31d474807 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Sun, 15 Dec 2019 13:55:48 +0100 Subject: [PATCH 0980/3170] Check that the cache is writable in _main() This avoid code duplication (for the wheel and http cache) and repeated warnings. --- news/7488.bugfix | 2 ++ src/pip/_internal/cli/base_command.py | 13 +++++++++++++ src/pip/_internal/commands/download.py | 11 ----------- src/pip/_internal/commands/install.py | 13 +------------ src/pip/_internal/network/session.py | 14 -------------- tests/functional/test_wheel.py | 3 ++- 6 files changed, 18 insertions(+), 38 deletions(-) create mode 100644 news/7488.bugfix diff --git a/news/7488.bugfix b/news/7488.bugfix new file mode 100644 index 00000000000..047a8c1fc8d --- /dev/null +++ b/news/7488.bugfix @@ -0,0 +1,2 @@ +Effectively disable the wheel cache when it is not writable, as is the +case with the http cache. diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index 6fbc26c0f55..74dcb0b3fc0 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -31,6 +31,7 @@ UninstallationError, ) from pip._internal.utils.deprecation import deprecated +from pip._internal.utils.filesystem import check_path_owner from pip._internal.utils.logging import BrokenStdoutLoggingError, setup_logging from pip._internal.utils.misc import get_prog from pip._internal.utils.temp_dir import global_tempdir_manager @@ -168,6 +169,18 @@ def _main(self, args): ) sys.exit(VIRTUALENV_NOT_FOUND) + if options.cache_dir: + if not check_path_owner(options.cache_dir): + logger.warning( + "The directory '%s' or its parent directory is not owned " + "or is not writable by the current user. The cache " + "has been disabled. Check the permissions and owner of " + "that directory. If executing pip with sudo, you may want " + "sudo's -H flag.", + options.cache_dir, + ) + options.cache_dir = None + try: status = self.run(options, args) # FIXME: all commands should return an exit status diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index 23ee11a5b23..24da3eb2a26 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -11,7 +11,6 @@ from pip._internal.cli.req_command import RequirementCommand from pip._internal.req import RequirementSet from pip._internal.req.req_tracker import get_requirement_tracker -from pip._internal.utils.filesystem import check_path_owner from pip._internal.utils.misc import ensure_dir, normalize_path, write_output from pip._internal.utils.temp_dir import TempDirectory @@ -99,16 +98,6 @@ def run(self, options, args): target_python=target_python, ) build_delete = (not (options.no_clean or options.build_dir)) - if options.cache_dir and not check_path_owner(options.cache_dir): - logger.warning( - "The directory '%s' or its parent directory is not owned " - "by the current user and caching wheels has been " - "disabled. check the permissions and owner of that " - "directory. If executing pip with sudo, you may want " - "sudo's -H flag.", - options.cache_dir, - ) - options.cache_dir = None with get_requirement_tracker() as req_tracker, TempDirectory( options.build_dir, delete=build_delete, kind="download" diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index c7dcf28df8a..32b814800f6 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -34,7 +34,7 @@ from pip._internal.req.req_tracker import get_requirement_tracker from pip._internal.utils.deprecation import deprecated from pip._internal.utils.distutils_args import parse_distutils_args -from pip._internal.utils.filesystem import check_path_owner, test_writable_dir +from pip._internal.utils.filesystem import test_writable_dir from pip._internal.utils.misc import ( ensure_dir, get_installed_version, @@ -330,17 +330,6 @@ def run(self, options, args): 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 get_requirement_tracker() as req_tracker, TempDirectory( options.build_dir, delete=build_delete, kind="install" ) as directory: diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py index 2d208cb65c3..f5eb15ef2f6 100644 --- a/src/pip/_internal/network/session.py +++ b/src/pip/_internal/network/session.py @@ -27,7 +27,6 @@ from pip._internal.network.cache import SafeFileCache # Import ssl from compat so the initial import occurs in only one place. from pip._internal.utils.compat import has_tls, ipaddress -from pip._internal.utils.filesystem import check_path_owner from pip._internal.utils.glibc import libc_ver from pip._internal.utils.misc import ( build_url_from_netloc, @@ -264,19 +263,6 @@ def __init__(self, *args, **kwargs): backoff_factor=0.25, ) - # Check to ensure that the directory containing our cache directory - # is owned by the user current executing pip. If it does not exist - # we will check the parent directory until we find one that does exist. - if cache and not check_path_owner(cache): - logger.warning( - "The directory '%s' or its parent directory is not owned by " - "the current user and the cache has been disabled. Please " - "check the permissions and owner of that directory. If " - "executing pip with sudo, you may want sudo's -H flag.", - cache, - ) - cache = None - # We want to _only_ cache responses on securely fetched origins. We do # this because we can't validate the response of an insecurely fetched # origin, and we don't want someone to be able to poison the cache and diff --git a/tests/functional/test_wheel.py b/tests/functional/test_wheel.py index 7a66a78fdf9..792f5fe6c10 100644 --- a/tests/functional/test_wheel.py +++ b/tests/functional/test_wheel.py @@ -120,6 +120,7 @@ def test_pip_wheel_builds_when_no_binary_set(script, data): assert "Building wheel for simple" in str(res), str(res) +@pytest.mark.skipif("sys.platform == 'win32'") def test_pip_wheel_readonly_cache(script, data, tmpdir): cache_dir = tmpdir / "cache" cache_dir.mkdir() @@ -133,7 +134,7 @@ def test_pip_wheel_readonly_cache(script, data, tmpdir): allow_stderr_warning=True, ) assert res.returncode == 0 - assert "caching wheels has been disabled" in str(res), str(res) + assert "The cache has been disabled." in str(res), str(res) def test_pip_wheel_builds_editable_deps(script, data): From 7c2c58442f5de276e96cbaa66a86d798c5086de2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Fri, 27 Dec 2019 09:40:02 +0100 Subject: [PATCH 0981/3170] Update docstring --- src/pip/_internal/wheel_builder.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index a3692e49787..200e70fc986 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -418,7 +418,8 @@ def build( :param should_unpack: If True, after building the wheel, unpack it and replace the sdist with the unpacked version in preparation for installation. - :return: The list of InstallRequirement that failed to build. + :return: The list of InstallRequirement that succeeded to build and + the list of InstallRequirement that failed to build. """ buildset = _collect_buildset( requirements, From 1f39950f3a8021c1fadbe291042d4b31f56caa48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Sat, 28 Dec 2019 16:12:58 +0100 Subject: [PATCH 0982/3170] Add news file explaining the new pip wheel behavior --- news/7517.feature | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 news/7517.feature diff --git a/news/7517.feature b/news/7517.feature new file mode 100644 index 00000000000..089fbc38781 --- /dev/null +++ b/news/7517.feature @@ -0,0 +1,4 @@ +The build step of ``pip wheel`` now builds all wheels to a cache first, +then copies them to the wheel directory all at once. +Before, it built them to a temporary direcory and moved +them to the wheel directory one by one. From 93900e119947560b95b817badaff96b6df76d5e1 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 28 Dec 2019 15:04:24 -0500 Subject: [PATCH 0983/3170] Only check for .dist-info directories at the top-level Previously we were restricting to a single .dist-info directory anywhere in the unpacked wheel directory. That was incorrect since only a top-level .dist-info directory indicates a contained "package". Now we limit our restriction to top-level .dist-info directories. --- src/pip/_internal/operations/install/wheel.py | 1 + tests/functional/test_install_wheel.py | 17 +++++++++++++++++ tests/lib/__init__.py | 2 +- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 4b10a9c009f..c749d6dd3eb 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -383,6 +383,7 @@ def clobber( continue elif ( is_base and + basedir == '' and s.endswith('.dist-info') ): assert not info_dir, ( diff --git a/tests/functional/test_install_wheel.py b/tests/functional/test_install_wheel.py index 4c1a9285406..316e9ef365c 100644 --- a/tests/functional/test_install_wheel.py +++ b/tests/functional/test_install_wheel.py @@ -472,3 +472,20 @@ def test_wheel_install_fails_with_unrelated_dist_info(script): "'simple-0.1.0.dist-info' does not start with 'unrelated'" in result.stderr ) + + +def test_wheel_installs_ok_with_nested_dist_info(script): + package = create_basic_wheel_for_package( + script, + "simple", + "0.1.0", + extra_files={ + "subdir/unrelated-2.0.0.dist-info/WHEEL": "Wheel-Version: 1.0", + "subdir/unrelated-2.0.0.dist-info/METADATA": ( + "Name: unrelated\nVersion: 2.0.0\n" + ), + }, + ) + script.pip( + "install", "--no-cache-dir", "--no-index", package + ) diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index af80078df87..394a469d17d 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -997,7 +997,7 @@ def hello(): for fname in files: path = script.temp_path / fname - path.parent.mkdir(exist_ok=True) + path.parent.mkdir(exist_ok=True, parents=True) path.write_text(files[fname]) retval = script.scratch_path / archive_name From 4d1fd08d4545c97f218b4c021ec1040e709f835e Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 28 Dec 2019 17:26:36 -0500 Subject: [PATCH 0984/3170] Run linters/docs on Windows and macOS This ensures that contributors on any of the major platforms can run our linting and doc building checks without any changes. --- .github/workflows/python-linters.yml | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/.github/workflows/python-linters.yml b/.github/workflows/python-linters.yml index ed2c7c5c061..81a87d46346 100644 --- a/.github/workflows/python-linters.yml +++ b/.github/workflows/python-linters.yml @@ -17,6 +17,8 @@ jobs: matrix: os: - ubuntu-18.04 + - windows-latest + - macos-latest env: - TOXENV: docs - TOXENV: lint @@ -33,11 +35,14 @@ jobs: path: ~/.cache/pre-commit key: pre-commit|${{ env.PY }}|${{ hashFiles('.pre-commit-config.yaml') }} - name: Pre-configure global Git settings - run: >- - tools/travis/setup.sh + run: | + git config --global user.email "pypa-dev@googlegroups.com" + git config --global user.name "pip" - name: Update setuptools and tox dependencies - run: >- - tools/travis/install.sh + run: | + python -m pip install --upgrade setuptools + python -m pip install --upgrade tox tox-venv + python -m pip freeze --all - name: 'Initialize tox envs: ${{ matrix.env.TOXENV }}' run: >- python -m tox --notest --skip-missing-interpreters false From 98543b2caab83aa1eadd2bb8837cf061fdb91eca Mon Sep 17 00:00:00 2001 From: Eitan Adler <lists@eitanadler.com> Date: Sat, 28 Dec 2019 15:05:43 -0800 Subject: [PATCH 0985/3170] getting_started: remove a a duplicate word --- docs/html/development/getting-started.rst | 2 +- news/7521.trivial | 0 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 news/7521.trivial diff --git a/docs/html/development/getting-started.rst b/docs/html/development/getting-started.rst index 5b4cb88de16..ed25f968125 100644 --- a/docs/html/development/getting-started.rst +++ b/docs/html/development/getting-started.rst @@ -5,7 +5,7 @@ Getting Started We’re pleased that you are interested in working on pip. This document is meant to get you setup to work on pip and to act as a guide and -reference to the the development setup. If you face any issues during this +reference to the development setup. If you face any issues during this process, please `open an issue`_ about it on the issue tracker. Get the source code diff --git a/news/7521.trivial b/news/7521.trivial new file mode 100644 index 00000000000..e69de29bb2d From c9ab34a9451dba538ef0cc68a2724052beddc1a2 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 28 Dec 2019 18:21:46 -0500 Subject: [PATCH 0986/3170] Fix docs build on Windows Sphinx expects the "document name" field to be normalized with respect to slashes (all /), so we now do this in a portable way for Windows. --- docs/html/conf.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/docs/html/conf.py b/docs/html/conf.py index bb30f75fb05..aae7ab12df1 100644 --- a/docs/html/conf.py +++ b/docs/html/conf.py @@ -273,17 +273,26 @@ ) ] + +def to_document_name(path, base_dir): + """Convert a provided path to a Sphinx "document name". + """ + relative_path = os.path.relpath(path, base_dir) + root, _ = os.path.splitext(relative_path) + return root.replace(os.sep, '/') + + # Here, we crawl the entire man/commands/ directory and list every file with # appropriate name and details -man_dir = os.path.join(docs_dir, 'man/') +man_dir = os.path.join(docs_dir, 'man') raw_subcommands = glob.glob(os.path.join(man_dir, 'commands/*.rst')) if not raw_subcommands: raise FileNotFoundError( 'The individual subcommand manpages could not be found!' ) for fname in raw_subcommands: - fname_base = fname[len(man_dir):-4] - outname = 'pip-' + fname_base[9:] + fname_base = to_document_name(fname, man_dir) + outname = 'pip-' + fname_base.split('/')[1] description = u'description of {} command'.format( outname.replace('-', ' ') ) From 8dc6919875271c1cecc07701c6dc0411e67e0258 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 28 Dec 2019 18:36:19 -0500 Subject: [PATCH 0987/3170] Fix mypy checks on Windows Previously we were making unguarded calls to non-Windows-only APIs. Mypy only automatically excludes these from platform-specific checks when inside conditions. --- src/pip/_internal/utils/compat.py | 13 +++++++------ src/pip/_internal/utils/filesystem.py | 3 ++- src/pip/_internal/utils/glibc.py | 3 +++ tests/unit/test_utils.py | 2 ++ 4 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/utils/compat.py b/src/pip/_internal/utils/compat.py index d347b73d98d..6efa52ad2b8 100644 --- a/src/pip/_internal/utils/compat.py +++ b/src/pip/_internal/utils/compat.py @@ -257,12 +257,13 @@ def ioctl_GWINSZ(fd): return cr cr = ioctl_GWINSZ(0) or ioctl_GWINSZ(1) or ioctl_GWINSZ(2) if not cr: - try: - fd = os.open(os.ctermid(), os.O_RDONLY) - cr = ioctl_GWINSZ(fd) - os.close(fd) - except Exception: - pass + if sys.platform != "win32": + try: + fd = os.open(os.ctermid(), os.O_RDONLY) + cr = ioctl_GWINSZ(fd) + os.close(fd) + except Exception: + pass if not cr: cr = (os.environ.get('LINES', 25), os.environ.get('COLUMNS', 80)) return int(cr[1]), int(cr[0]) diff --git a/src/pip/_internal/utils/filesystem.py b/src/pip/_internal/utils/filesystem.py index bce2058bcf1..7e1e3c8c7a5 100644 --- a/src/pip/_internal/utils/filesystem.py +++ b/src/pip/_internal/utils/filesystem.py @@ -4,6 +4,7 @@ import random import shutil import stat +import sys from contextlib import contextmanager from tempfile import NamedTemporaryFile @@ -29,7 +30,7 @@ def check_path_owner(path): # type: (str) -> bool # If we don't have a way to check the effective uid of this process, then # we'll just assume that we own the directory. - if not hasattr(os, "geteuid"): + if sys.platform == "win32" or not hasattr(os, "geteuid"): return True previous = None diff --git a/src/pip/_internal/utils/glibc.py b/src/pip/_internal/utils/glibc.py index 544b4c2792b..42b1d3919a3 100644 --- a/src/pip/_internal/utils/glibc.py +++ b/src/pip/_internal/utils/glibc.py @@ -5,6 +5,7 @@ import os import re +import sys import warnings from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -26,6 +27,8 @@ def glibc_version_string_confstr(): # to be broken or missing. This strategy is used in the standard library # platform module: # https://github.com/python/cpython/blob/fcf1d003bf4f0100c9d0921ff3d70e1127ca1b71/Lib/platform.py#L175-L183 + if sys.platform == "win32": + return None try: # os.confstr("CS_GNU_LIBC_VERSION") returns a string like "glibc 2.17": _, version = os.confstr("CS_GNU_LIBC_VERSION").split() diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 65b1a9a3ff6..64c8aabf505 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -570,12 +570,14 @@ def test_manylinux_check_glibc_version(self): # Didn't find the warning we were expecting assert False + @pytest.mark.skipif("sys.platform == 'win32'") def test_glibc_version_string(self, monkeypatch): monkeypatch.setattr( os, "confstr", lambda x: "glibc 2.20", raising=False, ) assert glibc_version_string() == "2.20" + @pytest.mark.skipif("sys.platform == 'win32'") def test_glibc_version_string_confstr(self, monkeypatch): monkeypatch.setattr( os, "confstr", lambda x: "glibc 2.20", raising=False, From ccfef670163f79ceeb3cafe3ca683761b3e51c89 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 26 Dec 2019 19:00:47 +0530 Subject: [PATCH 0988/3170] Move PEP-517 wheel build logic into operations.build --- src/pip/_internal/operations/build/wheel.py | 46 +++++++++++++++++++++ src/pip/_internal/wheel_builder.py | 44 ++------------------ 2 files changed, 49 insertions(+), 41 deletions(-) create mode 100644 src/pip/_internal/operations/build/wheel.py diff --git a/src/pip/_internal/operations/build/wheel.py b/src/pip/_internal/operations/build/wheel.py new file mode 100644 index 00000000000..1266ce05c6f --- /dev/null +++ b/src/pip/_internal/operations/build/wheel.py @@ -0,0 +1,46 @@ +import logging +import os + +from pip._internal.utils.subprocess import runner_with_spinner_message +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import List, Optional + from pip._vendor.pep517.wrappers import Pep517HookCaller + +logger = logging.getLogger(__name__) + + +def build_wheel_pep517( + name, # type: str + backend, # type: Pep517HookCaller + metadata_directory, # type: str + build_options, # type: List[str] + tempd, # type: str +): + # type: (...) -> Optional[str] + """Build one InstallRequirement using the PEP 517 build process. + + Returns path to wheel if successfully built. Otherwise, returns None. + """ + assert metadata_directory is not None + if build_options: + # PEP 517 does not support --build-options + logger.error('Cannot build wheel for %s using PEP 517 when ' + '--build-option is present' % (name,)) + return None + try: + logger.debug('Destination directory: %s', tempd) + + runner = runner_with_spinner_message( + 'Building wheel for {} (PEP 517)'.format(name) + ) + with backend.subprocess_runner(runner): + wheel_name = backend.build_wheel( + tempd, + metadata_directory=metadata_directory, + ) + except Exception: + logger.error('Failed building wheel for %s', name) + return None + return os.path.join(tempd, wheel_name) diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index 0d758aac179..1e425ad9b2a 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -10,15 +10,13 @@ import shutil from pip._internal.models.link import Link +from pip._internal.operations.build.wheel import build_wheel_pep517 from pip._internal.operations.build.wheel_legacy import build_wheel_legacy from pip._internal.utils.logging import indent_log from pip._internal.utils.marker_files import has_delete_marker_file from pip._internal.utils.misc import ensure_dir, hash_file from pip._internal.utils.setuptools_build import make_setuptools_clean_args -from pip._internal.utils.subprocess import ( - call_subprocess, - runner_with_spinner_message, -) +from pip._internal.utils.subprocess import call_subprocess from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.unpacking import unpack_file @@ -35,7 +33,6 @@ RequirementPreparer ) from pip._internal.req.req_install import InstallRequirement - from pip._vendor.pep517.wrappers import Pep517HookCaller BinaryAllowedPredicate = Callable[[InstallRequirement], bool] BuildResult = Tuple[List[InstallRequirement], List[InstallRequirement]] @@ -127,41 +124,6 @@ def should_cache( return False -def _build_wheel_pep517( - name, # type: str - backend, # type: Pep517HookCaller - metadata_directory, # type: str - build_options, # type: List[str] - tempd, # type: str -): - # type: (...) -> Optional[str] - """Build one InstallRequirement using the PEP 517 build process. - - Returns path to wheel if successfully built. Otherwise, returns None. - """ - assert metadata_directory is not None - if build_options: - # PEP 517 does not support --build-options - logger.error('Cannot build wheel for %s using PEP 517 when ' - '--build-option is present' % (name,)) - return None - try: - logger.debug('Destination directory: %s', tempd) - - runner = runner_with_spinner_message( - 'Building wheel for {} (PEP 517)'.format(name) - ) - with backend.subprocess_runner(runner): - wheel_name = backend.build_wheel( - tempd, - metadata_directory=metadata_directory, - ) - except Exception: - logger.error('Failed building wheel for %s', name) - return None - return os.path.join(tempd, wheel_name) - - def _collect_buildset( requirements, # type: Iterable[InstallRequirement] wheel_cache, # type: WheelCache @@ -252,7 +214,7 @@ def _build_one_inside_env( # type: (...) -> Optional[str] with TempDirectory(kind="wheel") as temp_dir: if req.use_pep517: - wheel_path = _build_wheel_pep517( + wheel_path = build_wheel_pep517( name=req.name, backend=req.pep517_backend, metadata_directory=req.metadata_directory, From 3828699ddce7e02312488c58a5392e0f56c1b74b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Sun, 29 Dec 2019 12:19:08 +0100 Subject: [PATCH 0989/3170] Move build options from WheelBuilder to build function --- src/pip/_internal/commands/install.py | 19 +++++++---- src/pip/_internal/commands/wheel.py | 8 ++--- src/pip/_internal/wheel_builder.py | 46 +++++++++++++++------------ tests/unit/test_command_install.py | 23 +++++++------- tests/unit/test_wheel_builder.py | 7 +++- 5 files changed, 59 insertions(+), 44 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 9e4fec70bdd..5c012069f31 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -74,6 +74,9 @@ def build_wheels( builder, # type: WheelBuilder pep517_requirements, # type: List[InstallRequirement] legacy_requirements, # type: List[InstallRequirement] + build_options, # type: List[str] + global_options, # type: List[str] + check_binary_allowed, # type: BinaryAllowedPredicate ): # type: (...) -> List[InstallRequirement] """ @@ -86,6 +89,9 @@ def build_wheels( _, build_failures = builder.build( pep517_requirements, should_unpack=True, + build_options=build_options, + global_options=global_options, + check_binary_allowed=check_binary_allowed, ) if should_build_legacy: @@ -95,6 +101,9 @@ def build_wheels( builder.build( legacy_requirements, should_unpack=True, + build_options=build_options, + global_options=global_options, + check_binary_allowed=check_binary_allowed, ) return build_failures @@ -396,16 +405,14 @@ def run(self, options, args): else: legacy_requirements.append(req) - wheel_builder = WheelBuilder( - preparer, wheel_cache, - build_options=[], global_options=[], - check_binary_allowed=check_binary_allowed, - ) - + wheel_builder = WheelBuilder(preparer, wheel_cache) build_failures = build_wheels( builder=wheel_builder, pep517_requirements=pep517_requirements, legacy_requirements=legacy_requirements, + build_options=[], + global_options=[], + check_binary_allowed=check_binary_allowed, ) # If we're using PEP 517, we cannot do a direct install diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index d3fe3430886..5fb4d219cfa 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -158,14 +158,12 @@ def run(self, options, args): resolver.resolve(requirement_set) # build wheels - wb = WheelBuilder( - preparer, wheel_cache, - build_options=options.build_options or [], - global_options=options.global_options or [], - ) + wb = WheelBuilder(preparer, wheel_cache) build_successes, build_failures = wb.build( requirement_set.requirements.values(), should_unpack=False, + build_options=options.build_options or [], + global_options=options.global_options or [], ) for req in build_successes: assert req.link and req.link.is_wheel diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index 1e425ad9b2a..aaf48b9ed7b 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -167,26 +167,17 @@ def __init__( self, preparer, # type: RequirementPreparer wheel_cache, # type: WheelCache - build_options=None, # type: Optional[List[str]] - global_options=None, # type: Optional[List[str]] - check_binary_allowed=None, # type: Optional[BinaryAllowedPredicate] ): # type: (...) -> None - if check_binary_allowed is None: - # Binaries allowed by default. - check_binary_allowed = _always_true - self.preparer = preparer self.wheel_cache = wheel_cache - self.build_options = build_options or [] - self.global_options = global_options or [] - self.check_binary_allowed = check_binary_allowed - def _build_one( self, req, # type: InstallRequirement output_dir, # type: str + build_options, # type: List[str] + global_options, # type: List[str] ): # type: (...) -> Optional[str] """Build one wheel. @@ -204,12 +195,16 @@ def _build_one( # Install build deps into temporary directory (PEP 518) with req.build_env: - return self._build_one_inside_env(req, output_dir) + return self._build_one_inside_env( + req, output_dir, build_options, global_options + ) def _build_one_inside_env( self, req, # type: InstallRequirement output_dir, # type: str + build_options, # type: List[str] + global_options, # type: List[str] ): # type: (...) -> Optional[str] with TempDirectory(kind="wheel") as temp_dir: @@ -218,7 +213,7 @@ def _build_one_inside_env( name=req.name, backend=req.pep517_backend, metadata_directory=req.metadata_directory, - build_options=self.build_options, + build_options=build_options, tempd=temp_dir.path, ) else: @@ -226,8 +221,8 @@ def _build_one_inside_env( name=req.name, setup_py_path=req.setup_py_path, source_dir=req.unpacked_source_directory, - global_options=self.global_options, - build_options=self.build_options, + global_options=global_options, + build_options=build_options, tempd=temp_dir.path, ) @@ -249,14 +244,14 @@ def _build_one_inside_env( req.name, e, ) # Ignore return, we can't do anything else useful. - self._clean_one(req) + self._clean_one(req, global_options) return None - def _clean_one(self, req): - # type: (InstallRequirement) -> bool + def _clean_one(self, req, global_options): + # type: (InstallRequirement, List[str]) -> bool clean_args = make_setuptools_clean_args( req.setup_py_path, - global_options=self.global_options, + global_options=global_options, ) logger.info('Running setup.py clean for %s', req.name) @@ -271,6 +266,9 @@ def build( self, requirements, # type: Iterable[InstallRequirement] should_unpack, # type: bool + build_options, # type: List[str] + global_options, # type: List[str] + check_binary_allowed=None, # type: Optional[BinaryAllowedPredicate] ): # type: (...) -> BuildResult """Build wheels. @@ -281,10 +279,14 @@ def build( :return: The list of InstallRequirement that succeeded to build and the list of InstallRequirement that failed to build. """ + if check_binary_allowed is None: + # Binaries allowed by default. + check_binary_allowed = _always_true + buildset = _collect_buildset( requirements, wheel_cache=self.wheel_cache, - check_binary_allowed=self.check_binary_allowed, + check_binary_allowed=check_binary_allowed, need_wheel=not should_unpack, ) if not buildset: @@ -302,7 +304,9 @@ def build( with indent_log(): build_successes, build_failures = [], [] for req, cache_dir in buildset: - wheel_file = self._build_one(req, cache_dir) + wheel_file = self._build_one( + req, cache_dir, build_options, global_options + ) if wheel_file: # Update the link for this. req.link = Link(path_to_url(wheel_file)) diff --git a/tests/unit/test_command_install.py b/tests/unit/test_command_install.py index 9d862792bba..5d53a89a8f9 100644 --- a/tests/unit/test_command_install.py +++ b/tests/unit/test_command_install.py @@ -1,7 +1,7 @@ import errno import pytest -from mock import Mock, call, patch +from mock import Mock, patch from pip._vendor.packaging.requirements import Requirement from pip._internal.commands.install import ( @@ -24,8 +24,11 @@ def check_build_wheels( """ Return: (mock_calls, return_value). """ + built_reqs = [] + def build(reqs, **kwargs): # Fail the first requirement. + built_reqs.append(reqs) return ([], [reqs[0]]) builder = Mock() @@ -35,24 +38,24 @@ def build(reqs, **kwargs): builder=builder, pep517_requirements=pep517_requirements, legacy_requirements=legacy_requirements, + build_options=[], + global_options=[], + check_binary_allowed=None, ) - return (builder.build.mock_calls, build_failures) + return (built_reqs, build_failures) @patch('pip._internal.commands.install.is_wheel_installed') def test_build_wheels__wheel_installed(self, is_wheel_installed): is_wheel_installed.return_value = True - mock_calls, build_failures = self.check_build_wheels( + built_reqs, build_failures = self.check_build_wheels( pep517_requirements=['a', 'b'], legacy_requirements=['c', 'd'], ) # Legacy requirements were built. - assert mock_calls == [ - call(['a', 'b'], should_unpack=True), - call(['c', 'd'], should_unpack=True), - ] + assert built_reqs == [['a', 'b'], ['c', 'd']] # Legacy build failures are not included in the return value. assert build_failures == ['a'] @@ -61,15 +64,13 @@ def test_build_wheels__wheel_installed(self, is_wheel_installed): def test_build_wheels__wheel_not_installed(self, is_wheel_installed): is_wheel_installed.return_value = False - mock_calls, build_failures = self.check_build_wheels( + built_reqs, build_failures = self.check_build_wheels( pep517_requirements=['a', 'b'], legacy_requirements=['c', 'd'], ) # Legacy requirements were not built. - assert mock_calls == [ - call(['a', 'b'], should_unpack=True), - ] + assert built_reqs == [['a', 'b']] assert build_failures == ['a'] diff --git a/tests/unit/test_wheel_builder.py b/tests/unit/test_wheel_builder.py index 9d0953518a3..0eb77c19987 100644 --- a/tests/unit/test_wheel_builder.py +++ b/tests/unit/test_wheel_builder.py @@ -194,7 +194,12 @@ def test_skip_building_wheels(self, caplog): wheel_req = Mock(is_wheel=True, editable=False, constraint=False) with caplog.at_level(logging.INFO): - wb.build([wheel_req], should_unpack=False) + wb.build( + [wheel_req], + should_unpack=False, + build_options=[], + global_options=[], + ) assert "due to already being wheel" in caplog.text assert mock_build_one.mock_calls == [] From c8d42775350d32572010345fcb6eda0a5c7a7fa0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Sun, 29 Dec 2019 12:42:08 +0100 Subject: [PATCH 0990/3170] Convert _build_one from method to function It does not rely on WheelBuilder anymore. --- src/pip/_internal/wheel_builder.py | 183 +++++++++++++++-------------- 1 file changed, 92 insertions(+), 91 deletions(-) diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index aaf48b9ed7b..6a8a27c33eb 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -160,6 +160,97 @@ def _always_true(_): return True +def _build_one( + req, # type: InstallRequirement + output_dir, # type: str + build_options, # type: List[str] + global_options, # type: List[str] +): + # type: (...) -> Optional[str] + """Build one wheel. + + :return: The filename of the built wheel, or None if the build failed. + """ + try: + ensure_dir(output_dir) + except OSError as e: + logger.warning( + "Building wheel for %s failed: %s", + req.name, e, + ) + return None + + # Install build deps into temporary directory (PEP 518) + with req.build_env: + return _build_one_inside_env( + req, output_dir, build_options, global_options + ) + + +def _build_one_inside_env( + req, # type: InstallRequirement + output_dir, # type: str + build_options, # type: List[str] + global_options, # type: List[str] +): + # type: (...) -> Optional[str] + with TempDirectory(kind="wheel") as temp_dir: + if req.use_pep517: + wheel_path = build_wheel_pep517( + name=req.name, + backend=req.pep517_backend, + metadata_directory=req.metadata_directory, + build_options=build_options, + tempd=temp_dir.path, + ) + else: + wheel_path = build_wheel_legacy( + name=req.name, + setup_py_path=req.setup_py_path, + source_dir=req.unpacked_source_directory, + global_options=global_options, + build_options=build_options, + tempd=temp_dir.path, + ) + + if wheel_path is not None: + wheel_name = os.path.basename(wheel_path) + dest_path = os.path.join(output_dir, wheel_name) + try: + wheel_hash, length = hash_file(wheel_path) + shutil.move(wheel_path, dest_path) + logger.info('Created wheel for %s: ' + 'filename=%s size=%d sha256=%s', + req.name, wheel_name, length, + wheel_hash.hexdigest()) + logger.info('Stored in directory: %s', output_dir) + return dest_path + except Exception as e: + logger.warning( + "Building wheel for %s failed: %s", + req.name, e, + ) + # Ignore return, we can't do anything else useful. + _clean_one(req, global_options) + return None + + +def _clean_one(req, global_options): + # type: (InstallRequirement, List[str]) -> bool + clean_args = make_setuptools_clean_args( + req.setup_py_path, + global_options=global_options, + ) + + logger.info('Running setup.py clean for %s', req.name) + try: + call_subprocess(clean_args, cwd=req.source_dir) + return True + except Exception: + logger.error('Failed cleaning build dir for %s', req.name) + return False + + class WheelBuilder(object): """Build wheels from a RequirementSet.""" @@ -172,96 +263,6 @@ def __init__( self.preparer = preparer self.wheel_cache = wheel_cache - def _build_one( - self, - req, # type: InstallRequirement - output_dir, # type: str - build_options, # type: List[str] - global_options, # type: List[str] - ): - # type: (...) -> Optional[str] - """Build one wheel. - - :return: The filename of the built wheel, or None if the build failed. - """ - try: - ensure_dir(output_dir) - except OSError as e: - logger.warning( - "Building wheel for %s failed: %s", - req.name, e, - ) - return None - - # Install build deps into temporary directory (PEP 518) - with req.build_env: - return self._build_one_inside_env( - req, output_dir, build_options, global_options - ) - - def _build_one_inside_env( - self, - req, # type: InstallRequirement - output_dir, # type: str - build_options, # type: List[str] - global_options, # type: List[str] - ): - # type: (...) -> Optional[str] - with TempDirectory(kind="wheel") as temp_dir: - if req.use_pep517: - wheel_path = build_wheel_pep517( - name=req.name, - backend=req.pep517_backend, - metadata_directory=req.metadata_directory, - build_options=build_options, - tempd=temp_dir.path, - ) - else: - wheel_path = build_wheel_legacy( - name=req.name, - setup_py_path=req.setup_py_path, - source_dir=req.unpacked_source_directory, - global_options=global_options, - build_options=build_options, - tempd=temp_dir.path, - ) - - if wheel_path is not None: - wheel_name = os.path.basename(wheel_path) - dest_path = os.path.join(output_dir, wheel_name) - try: - wheel_hash, length = hash_file(wheel_path) - shutil.move(wheel_path, dest_path) - logger.info('Created wheel for %s: ' - 'filename=%s size=%d sha256=%s', - req.name, wheel_name, length, - wheel_hash.hexdigest()) - logger.info('Stored in directory: %s', output_dir) - return dest_path - except Exception as e: - logger.warning( - "Building wheel for %s failed: %s", - req.name, e, - ) - # Ignore return, we can't do anything else useful. - self._clean_one(req, global_options) - return None - - def _clean_one(self, req, global_options): - # type: (InstallRequirement, List[str]) -> bool - clean_args = make_setuptools_clean_args( - req.setup_py_path, - global_options=global_options, - ) - - logger.info('Running setup.py clean for %s', req.name) - try: - call_subprocess(clean_args, cwd=req.source_dir) - return True - except Exception: - logger.error('Failed cleaning build dir for %s', req.name) - return False - def build( self, requirements, # type: Iterable[InstallRequirement] @@ -304,7 +305,7 @@ def build( with indent_log(): build_successes, build_failures = [], [] for req, cache_dir in buildset: - wheel_file = self._build_one( + wheel_file = _build_one( req, cache_dir, build_options, global_options ) if wheel_file: From 261c286de9814942609a6e8ef972005329e95a79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Sun, 29 Dec 2019 12:47:28 +0100 Subject: [PATCH 0991/3170] Make wheel_cache an argument of build() --- src/pip/_internal/commands/install.py | 6 +++++- src/pip/_internal/commands/wheel.py | 3 ++- src/pip/_internal/wheel_builder.py | 5 ++--- tests/unit/test_command_install.py | 1 + tests/unit/test_wheel_builder.py | 6 ++---- 5 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 5c012069f31..0f39b90d812 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -74,6 +74,7 @@ def build_wheels( builder, # type: WheelBuilder pep517_requirements, # type: List[InstallRequirement] legacy_requirements, # type: List[InstallRequirement] + wheel_cache, # type: WheelCache build_options, # type: List[str] global_options, # type: List[str] check_binary_allowed, # type: BinaryAllowedPredicate @@ -89,6 +90,7 @@ def build_wheels( _, build_failures = builder.build( pep517_requirements, should_unpack=True, + wheel_cache=wheel_cache, build_options=build_options, global_options=global_options, check_binary_allowed=check_binary_allowed, @@ -101,6 +103,7 @@ def build_wheels( builder.build( legacy_requirements, should_unpack=True, + wheel_cache=wheel_cache, build_options=build_options, global_options=global_options, check_binary_allowed=check_binary_allowed, @@ -405,11 +408,12 @@ def run(self, options, args): else: legacy_requirements.append(req) - wheel_builder = WheelBuilder(preparer, wheel_cache) + wheel_builder = WheelBuilder(preparer) build_failures = build_wheels( builder=wheel_builder, pep517_requirements=pep517_requirements, legacy_requirements=legacy_requirements, + wheel_cache=wheel_cache, build_options=[], global_options=[], check_binary_allowed=check_binary_allowed, diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index 5fb4d219cfa..edf006714ba 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -158,10 +158,11 @@ def run(self, options, args): resolver.resolve(requirement_set) # build wheels - wb = WheelBuilder(preparer, wheel_cache) + wb = WheelBuilder(preparer) build_successes, build_failures = wb.build( requirement_set.requirements.values(), should_unpack=False, + wheel_cache=wheel_cache, build_options=options.build_options or [], global_options=options.global_options or [], ) diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index 6a8a27c33eb..b992bd676ba 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -257,16 +257,15 @@ class WheelBuilder(object): def __init__( self, preparer, # type: RequirementPreparer - wheel_cache, # type: WheelCache ): # type: (...) -> None self.preparer = preparer - self.wheel_cache = wheel_cache def build( self, requirements, # type: Iterable[InstallRequirement] should_unpack, # type: bool + wheel_cache, # type: WheelCache build_options, # type: List[str] global_options, # type: List[str] check_binary_allowed=None, # type: Optional[BinaryAllowedPredicate] @@ -286,7 +285,7 @@ def build( buildset = _collect_buildset( requirements, - wheel_cache=self.wheel_cache, + wheel_cache=wheel_cache, check_binary_allowed=check_binary_allowed, need_wheel=not should_unpack, ) diff --git a/tests/unit/test_command_install.py b/tests/unit/test_command_install.py index 5d53a89a8f9..36b3ca73fe6 100644 --- a/tests/unit/test_command_install.py +++ b/tests/unit/test_command_install.py @@ -38,6 +38,7 @@ def build(reqs, **kwargs): builder=builder, pep517_requirements=pep517_requirements, legacy_requirements=legacy_requirements, + wheel_cache=Mock(cache_dir=None), build_options=[], global_options=[], check_binary_allowed=None, diff --git a/tests/unit/test_wheel_builder.py b/tests/unit/test_wheel_builder.py index 0eb77c19987..9d2c803039e 100644 --- a/tests/unit/test_wheel_builder.py +++ b/tests/unit/test_wheel_builder.py @@ -186,10 +186,7 @@ def test_format_command_result__empty_output(caplog, log_level): class TestWheelBuilder(object): def test_skip_building_wheels(self, caplog): - wb = wheel_builder.WheelBuilder( - preparer=Mock(), - wheel_cache=Mock(cache_dir=None), - ) + wb = wheel_builder.WheelBuilder(preparer=Mock()) wb._build_one = mock_build_one = Mock() wheel_req = Mock(is_wheel=True, editable=False, constraint=False) @@ -197,6 +194,7 @@ def test_skip_building_wheels(self, caplog): wb.build( [wheel_req], should_unpack=False, + wheel_cache=Mock(cache_dir=None), build_options=[], global_options=[], ) From d8374b86f9f4ed954c94badc151ef0abe4336f18 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Sun, 29 Dec 2019 13:49:25 +0100 Subject: [PATCH 0992/3170] wheel: ensure wheel dir is present earlier Similar to what is done for download_dir in the download command --- src/pip/_internal/commands/wheel.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index d3fe3430886..9bd85b172dd 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -122,6 +122,8 @@ def run(self, options, args): build_delete = (not (options.no_clean or options.build_dir)) wheel_cache = WheelCache(options.cache_dir, options.format_control) + ensure_dir(options.wheel_dir) + with get_requirement_tracker() as req_tracker, TempDirectory( options.build_dir, delete=build_delete, kind="wheel" ) as directory: @@ -172,7 +174,6 @@ def run(self, options, args): assert req.local_file_path # copy from cache to target directory try: - ensure_dir(options.wheel_dir) shutil.copy(req.local_file_path, options.wheel_dir) except OSError as e: logger.warning( From 9da9f6050bab84b23ed097aac6defb1d2bf5101f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Sun, 29 Dec 2019 13:54:14 +0100 Subject: [PATCH 0993/3170] In Resolver, assume wheel_download_dir exists This is ensured at the beginning of the wheel command, which is the only command that sets wheel_download_dir. This is also similar to what is done for download_dir in the download command. --- src/pip/_internal/legacy_resolve.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/pip/_internal/legacy_resolve.py b/src/pip/_internal/legacy_resolve.py index 3fa93d3dced..ca269121b60 100644 --- a/src/pip/_internal/legacy_resolve.py +++ b/src/pip/_internal/legacy_resolve.py @@ -29,11 +29,7 @@ UnsupportedPythonVersion, ) from pip._internal.utils.logging import indent_log -from pip._internal.utils.misc import ( - dist_in_usersite, - ensure_dir, - normalize_version_info, -) +from pip._internal.utils.misc import dist_in_usersite, normalize_version_info from pip._internal.utils.packaging import ( check_requires_python, get_requires_python, @@ -163,10 +159,6 @@ def resolve(self, requirement_set): 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 = ( From f55819787d924b083ad12b2a683f263ba0e55f2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Sun, 29 Dec 2019 14:04:41 +0100 Subject: [PATCH 0994/3170] Normalize wheel dir at the beginning of wheel command Similar to what is done in the download command. --- src/pip/_internal/commands/wheel.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index 9bd85b172dd..f85d473e1f3 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -15,7 +15,7 @@ from pip._internal.exceptions import CommandError, PreviousBuildDirError from pip._internal.req import RequirementSet from pip._internal.req.req_tracker import get_requirement_tracker -from pip._internal.utils.misc import ensure_dir +from pip._internal.utils.misc import ensure_dir, normalize_path from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.wheel_builder import WheelBuilder @@ -122,6 +122,7 @@ def run(self, options, args): build_delete = (not (options.no_clean or options.build_dir)) wheel_cache = WheelCache(options.cache_dir, options.format_control) + options.wheel_dir = normalize_path(options.wheel_dir) ensure_dir(options.wheel_dir) with get_requirement_tracker() as req_tracker, TempDirectory( From 8c888a2287c27b6b41e16162316c1712a4ad4603 Mon Sep 17 00:00:00 2001 From: Christopher Hunt <chrahunt@gmail.com> Date: Mon, 30 Dec 2019 23:59:23 +0800 Subject: [PATCH 0995/3170] Simplify getting info directories for wheel installation (#7526) * Edit subdirs of top-level instead of checking in each directory Previously, we were checking whether the top of the relative path ended with .data. Now, we do not recurse into those directories, so there's no need to check every time. * Store info_dir in separate variable Instead of working with a list everywhere, we use the single info_dir. * Separate variables for info_dir and the destination path * Use destination .dist-info dir only when needed By initially storing just the name of the folder we ensure our code is agnostic to the destination, so it'll be easier to install from a zip later. * Use os.listdir instead of os.walk for wheel dir population Since we only execute any code when basedir == '', we only need the top-level directories. * Inline data_dirs calculation * Inline info_dirs calculation --- src/pip/_internal/operations/install/wheel.py | 51 +++++++++---------- tests/functional/test_install_wheel.py | 2 +- 2 files changed, 24 insertions(+), 29 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index c749d6dd3eb..fee0358bd6e 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -332,9 +332,10 @@ def install_unpacked_wheel( else: lib_dir = scheme.platlib - info_dir = [] # type: List[str] - data_dirs = [] source = wheeldir.rstrip(os.path.sep) + os.path.sep + subdirs = os.listdir(source) + info_dirs = [s for s in subdirs if s.endswith('.dist-info')] + data_dirs = [s for s in subdirs if s.endswith('.data')] # Record details of the files moved # installed = files copied from the wheel to the destination @@ -374,24 +375,8 @@ def clobber( for dir, subdirs, files in os.walk(source): basedir = dir[len(source):].lstrip(os.path.sep) destdir = os.path.join(dest, basedir) - if is_base and basedir.split(os.path.sep, 1)[0].endswith('.data'): - continue - for s in subdirs: - destsubdir = os.path.join(dest, basedir, s) - if is_base and basedir == '' and destsubdir.endswith('.data'): - data_dirs.append(s) - continue - elif ( - is_base and - basedir == '' and - s.endswith('.dist-info') - ): - assert not info_dir, ( - 'Multiple .dist-info directories: {}, '.format( - destsubdir - ) + ', '.join(info_dir) - ) - info_dir.append(destsubdir) + if is_base and basedir == '': + subdirs[:] = [s for s in subdirs if not s.endswith('.data')] for f in files: # Skip unwanted files if filter and filter(f): @@ -443,21 +428,31 @@ def clobber( clobber(source, lib_dir, True) - assert info_dir, "{} .dist-info directory not found".format( + assert info_dirs, "{} .dist-info directory not found".format( req_description ) - info_dir_name = canonicalize_name(os.path.basename(info_dir[0])) + assert len(info_dirs) == 1, ( + '{} multiple .dist-info directories found: {}'.format( + req_description, ', '.join(info_dirs) + ) + ) + + info_dir = info_dirs[0] + + info_dir_name = canonicalize_name(info_dir) canonical_name = canonicalize_name(name) if not info_dir_name.startswith(canonical_name): raise UnsupportedWheel( "{} .dist-info directory {!r} does not start with {!r}".format( - req_description, os.path.basename(info_dir[0]), canonical_name + req_description, info_dir, canonical_name ) ) + dest_info_dir = os.path.join(lib_dir, info_dir) + # Get the defined entry points - ep_file = os.path.join(info_dir[0], 'entry_points.txt') + ep_file = os.path.join(dest_info_dir, 'entry_points.txt') console, gui = get_entrypoints(ep_file) def is_entrypoint_wrapper(name): @@ -607,16 +602,16 @@ def is_entrypoint_wrapper(name): logger.warning(msg) # Record pip as the installer - installer = os.path.join(info_dir[0], 'INSTALLER') - temp_installer = os.path.join(info_dir[0], 'INSTALLER.pip') + installer = os.path.join(dest_info_dir, 'INSTALLER') + temp_installer = os.path.join(dest_info_dir, 'INSTALLER.pip') with open(temp_installer, 'wb') as installer_file: installer_file.write(b'pip\n') shutil.move(temp_installer, installer) generated.append(installer) # Record details of all files installed - record = os.path.join(info_dir[0], 'RECORD') - temp_record = os.path.join(info_dir[0], 'RECORD.pip') + record = os.path.join(dest_info_dir, 'RECORD') + temp_record = os.path.join(dest_info_dir, 'RECORD.pip') with open_for_csv(record, 'r') as record_in: with open_for_csv(temp_record, 'w+') as record_out: reader = csv.reader(record_in) diff --git a/tests/functional/test_install_wheel.py b/tests/functional/test_install_wheel.py index 316e9ef365c..e11664aa8f1 100644 --- a/tests/functional/test_install_wheel.py +++ b/tests/functional/test_install_wheel.py @@ -451,7 +451,7 @@ def test_wheel_install_fails_with_extra_dist_info(script): result = script.pip( "install", "--no-cache-dir", "--no-index", package, expect_error=True ) - assert "Multiple .dist-info directories" in result.stderr + assert "multiple .dist-info directories" in result.stderr def test_wheel_install_fails_with_unrelated_dist_info(script): From a6d212383b8e98cefcf000b322c86d5c95980526 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Tue, 31 Dec 2019 05:32:42 +0100 Subject: [PATCH 0996/3170] Remove redundant path normalization in preparer wheel_download_dir is normalized at the beginning of the wheel command --- src/pip/_internal/operations/prepare.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 3b6e225a31c..73f4c1294b7 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -35,7 +35,6 @@ backup_dir, display_path, hide_url, - normalize_path, path_to_display, rmtree, ) @@ -372,8 +371,6 @@ def __init__( # 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 From 69143848b0ea78e326d88a5ecec7cf225f8d6934 Mon Sep 17 00:00:00 2001 From: Christopher Hunt <chrahunt@gmail.com> Date: Tue, 31 Dec 2019 23:17:41 +0800 Subject: [PATCH 0997/3170] Make WHEEL file errors more explicit (#7529) * Raise exception on exception in finding wheel dist We plan to replace this code with direct extraction from a zip, so no point catching anything more precise. * Raise exception if no dist is found in wheel_version * Catch file read errors when reading WHEEL get_metadata delegates to the underlying implementation which tries to locate and read the file, throwing an IOError (Python 2) or OSError subclass on any errors. Since the new explicit test checks the same case as brokenwheel in test_wheel_version we remove the redundant test. * Check for WHEEL decoding errors explicitly This was the last error that could be thrown by get_metadata, so we can also remove the catch-all except block. * Move WHEEL parsing outside try...except This API does not raise an exception, but returns any errors on the message object itself. We are preserving the original behavior, and can decide later whether to start warning or raising our own exception. * Raise explicit error if Wheel-Version is missing `email.message.Message.__getitem__` returns None on missing values, so we have to check for ourselves explicitly. * Raise explicit exception on failure to parse Wheel-Version This is also the last exception that can be raised, so we remove `except Exception`. * Remove dead code Since wheel_version never returns None, this exception will never be raised. --- src/pip/_internal/operations/install/wheel.py | 57 +++++++++++---- tests/lib/path.py | 5 ++ tests/unit/test_wheel.py | 73 +++++++++++++++++-- 3 files changed, 114 insertions(+), 21 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index fee0358bd6e..438f96303cf 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -324,7 +324,13 @@ def install_unpacked_wheel( # TODO: Look into moving this into a dedicated class for representing an # installation. - version = wheel_version(wheeldir) + try: + version = wheel_version(wheeldir) + except UnsupportedWheel as e: + raise UnsupportedWheel( + "{} has an invalid wheel, {}".format(name, str(e)) + ) + check_compatibility(version, name) if root_is_purelib(name, wheeldir): @@ -651,25 +657,48 @@ def install_wheel( def wheel_version(source_dir): - # type: (Optional[str]) -> Optional[Tuple[int, ...]] + # type: (Optional[str]) -> Tuple[int, ...] """Return the Wheel-Version of an extracted wheel, if possible. - Otherwise, return None if we couldn't parse / extract it. + Otherwise, raise UnsupportedWheel if we couldn't parse / extract it. """ try: - dist = [d for d in pkg_resources.find_on_path(None, source_dir)][0] + dists = [d for d in pkg_resources.find_on_path(None, source_dir)] + except Exception as e: + raise UnsupportedWheel( + "could not find a contained distribution due to: {!r}".format(e) + ) - wheel_data = dist.get_metadata('WHEEL') - wheel_data = Parser().parsestr(wheel_data) + if not dists: + raise UnsupportedWheel("no contained distribution found") - version = wheel_data['Wheel-Version'].strip() - version = tuple(map(int, version.split('.'))) - return version - except Exception: - return None + dist = dists[0] + + try: + wheel_text = dist.get_metadata('WHEEL') + except (IOError, OSError) as e: + raise UnsupportedWheel("could not read WHEEL file: {!r}".format(e)) + except UnicodeDecodeError as e: + raise UnsupportedWheel("error decoding WHEEL: {!r}".format(e)) + + # FeedParser (used by Parser) does not raise any exceptions. The returned + # message may have .defects populated, but for backwards-compatibility we + # currently ignore them. + wheel_data = Parser().parsestr(wheel_text) + + version_text = wheel_data["Wheel-Version"] + if version_text is None: + raise UnsupportedWheel("WHEEL is missing Wheel-Version") + + version = version_text.strip() + + try: + return tuple(map(int, version.split('.'))) + except ValueError: + raise UnsupportedWheel("invalid Wheel-Version: {!r}".format(version)) def check_compatibility(version, name): - # type: (Optional[Tuple[int, ...]], str) -> None + # type: (Tuple[int, ...], str) -> None """Raises errors or warns if called with an incompatible Wheel-Version. Pip should refuse to install a Wheel-Version that's a major series @@ -681,10 +710,6 @@ def check_compatibility(version, name): :raises UnsupportedWheel: when an incompatible Wheel-Version is given """ - if not version: - raise UnsupportedWheel( - "%s is in an unsupported or invalid wheel" % name - ) if version[0] > VERSION_COMPATIBLE[0]: raise UnsupportedWheel( "%s's Wheel-Version (%s) is not compatible with this version " diff --git a/tests/lib/path.py b/tests/lib/path.py index 2f8fdd24218..35eefa6bb0a 100644 --- a/tests/lib/path.py +++ b/tests/lib/path.py @@ -183,6 +183,11 @@ def read_bytes(self): with open(self, "rb") as fp: return fp.read() + def write_bytes(self, content): + # type: (bytes) -> None + with open(self, "wb") as f: + f.write(content) + def read_text(self): with open(self, "r") as fp: return fp.read() diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 8656d842aa7..9689c0ec173 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -5,7 +5,8 @@ import textwrap import pytest -from mock import patch +from mock import Mock, patch +from pip._vendor import pkg_resources from pip._vendor.packaging.requirements import Requirement from pip._internal.exceptions import UnsupportedWheel @@ -22,7 +23,7 @@ from pip._internal.utils.compat import WINDOWS from pip._internal.utils.misc import hash_file from pip._internal.utils.unpacking import unpack_file -from tests.lib import DATA_DIR, assert_paths_equal +from tests.lib import DATA_DIR, assert_paths_equal, skip_if_python2 def call_get_legacy_build_wheel_path(caplog, names): @@ -191,14 +192,76 @@ def test_get_csv_rows_for_installed__long_lines(tmpdir, caplog): def test_wheel_version(tmpdir, data): future_wheel = 'futurewheel-1.9-py2.py3-none-any.whl' - broken_wheel = 'brokenwheel-1.0-py2.py3-none-any.whl' future_version = (1, 9) unpack_file(data.packages.joinpath(future_wheel), tmpdir + 'future') - unpack_file(data.packages.joinpath(broken_wheel), tmpdir + 'broken') assert wheel.wheel_version(tmpdir + 'future') == future_version - assert not wheel.wheel_version(tmpdir + 'broken') + + +def test_wheel_version_fails_on_error(monkeypatch): + err = RuntimeError("example") + monkeypatch.setattr(pkg_resources, "find_on_path", Mock(side_effect=err)) + with pytest.raises(UnsupportedWheel) as e: + wheel.wheel_version(".") + assert repr(err) in str(e.value) + + +def test_wheel_version_fails_no_dists(tmpdir): + with pytest.raises(UnsupportedWheel) as e: + wheel.wheel_version(str(tmpdir)) + assert "no contained distribution found" in str(e.value) + + +def test_wheel_version_fails_missing_wheel(tmpdir): + dist_info_dir = tmpdir / "simple-0.1.0.dist-info" + dist_info_dir.mkdir() + dist_info_dir.joinpath("METADATA").touch() + + with pytest.raises(UnsupportedWheel) as e: + wheel.wheel_version(str(tmpdir)) + assert "could not read WHEEL file" in str(e.value) + + +@skip_if_python2 +def test_wheel_version_fails_on_bad_encoding(tmpdir): + dist_info_dir = tmpdir / "simple-0.1.0.dist-info" + dist_info_dir.mkdir() + dist_info_dir.joinpath("METADATA").touch() + dist_info_dir.joinpath("WHEEL").write_bytes(b"\xff") + + with pytest.raises(UnsupportedWheel) as e: + wheel.wheel_version(str(tmpdir)) + assert "error decoding WHEEL" in str(e.value) + + +def test_wheel_version_fails_on_no_wheel_version(tmpdir): + dist_info_dir = tmpdir / "simple-0.1.0.dist-info" + dist_info_dir.mkdir() + dist_info_dir.joinpath("METADATA").touch() + dist_info_dir.joinpath("WHEEL").touch() + + with pytest.raises(UnsupportedWheel) as e: + wheel.wheel_version(str(tmpdir)) + assert "missing Wheel-Version" in str(e.value) + + +@pytest.mark.parametrize("version", [ + ("",), + ("1.b",), + ("1.",), +]) +def test_wheel_version_fails_on_bad_wheel_version(tmpdir, version): + dist_info_dir = tmpdir / "simple-0.1.0.dist-info" + dist_info_dir.mkdir() + dist_info_dir.joinpath("METADATA").touch() + dist_info_dir.joinpath("WHEEL").write_text( + "Wheel-Version: {}".format(version) + ) + + with pytest.raises(UnsupportedWheel) as e: + wheel.wheel_version(str(tmpdir)) + assert "invalid Wheel-Version" in str(e.value) def test_check_compatibility(): From 918b154518e0e6b545991dee7170616ac3694355 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 31 Dec 2019 12:12:17 -0500 Subject: [PATCH 0998/3170] Move info_dir calculation up This will let us use the value for later processing. --- src/pip/_internal/operations/install/wheel.py | 49 ++++++++++--------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 438f96303cf..745a6d34210 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -324,6 +324,31 @@ def install_unpacked_wheel( # TODO: Look into moving this into a dedicated class for representing an # installation. + source = wheeldir.rstrip(os.path.sep) + os.path.sep + subdirs = os.listdir(source) + info_dirs = [s for s in subdirs if s.endswith('.dist-info')] + + assert info_dirs, "{} .dist-info directory not found".format( + req_description + ) + + assert len(info_dirs) == 1, ( + '{} multiple .dist-info directories found: {}'.format( + req_description, ', '.join(info_dirs) + ) + ) + + info_dir = info_dirs[0] + + info_dir_name = canonicalize_name(info_dir) + canonical_name = canonicalize_name(name) + if not info_dir_name.startswith(canonical_name): + raise UnsupportedWheel( + "{} .dist-info directory {!r} does not start with {!r}".format( + req_description, info_dir, canonical_name + ) + ) + try: version = wheel_version(wheeldir) except UnsupportedWheel as e: @@ -338,9 +363,6 @@ def install_unpacked_wheel( else: lib_dir = scheme.platlib - source = wheeldir.rstrip(os.path.sep) + os.path.sep - subdirs = os.listdir(source) - info_dirs = [s for s in subdirs if s.endswith('.dist-info')] data_dirs = [s for s in subdirs if s.endswith('.data')] # Record details of the files moved @@ -434,27 +456,6 @@ def clobber( clobber(source, lib_dir, True) - assert info_dirs, "{} .dist-info directory not found".format( - req_description - ) - - assert len(info_dirs) == 1, ( - '{} multiple .dist-info directories found: {}'.format( - req_description, ', '.join(info_dirs) - ) - ) - - info_dir = info_dirs[0] - - info_dir_name = canonicalize_name(info_dir) - canonical_name = canonicalize_name(name) - if not info_dir_name.startswith(canonical_name): - raise UnsupportedWheel( - "{} .dist-info directory {!r} does not start with {!r}".format( - req_description, info_dir, canonical_name - ) - ) - dest_info_dir = os.path.join(lib_dir, info_dir) # Get the defined entry points From d66bc398be1e87281951d17fcc2375516afa025d Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 31 Dec 2019 12:28:26 -0500 Subject: [PATCH 0999/3170] Split wheel_metadata from wheel_version This will let us re-use the wheel_metadata for other parts of processing, and by parameterizing checks in terms of metadata we will be able to substitute in metadata derived directly from the zip. --- src/pip/_internal/operations/install/wheel.py | 20 ++++++++++----- tests/unit/test_wheel.py | 25 +++++++++++-------- 2 files changed, 28 insertions(+), 17 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 745a6d34210..667a139c7a9 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -33,6 +33,7 @@ from pip._internal.utils.unpacking import unpack_file if MYPY_CHECK_RUNNING: + from email.message import Message from typing import ( Dict, List, Optional, Sequence, Tuple, IO, Text, Any, Iterable, Callable, Set, @@ -350,7 +351,8 @@ def install_unpacked_wheel( ) try: - version = wheel_version(wheeldir) + metadata = wheel_metadata(wheeldir) + version = wheel_version(metadata) except UnsupportedWheel as e: raise UnsupportedWheel( "{} has an invalid wheel, {}".format(name, str(e)) @@ -657,10 +659,10 @@ def install_wheel( ) -def wheel_version(source_dir): - # type: (Optional[str]) -> Tuple[int, ...] - """Return the Wheel-Version of an extracted wheel, if possible. - Otherwise, raise UnsupportedWheel if we couldn't parse / extract it. +def wheel_metadata(source_dir): + # type: (Optional[str]) -> Message + """Return the WHEEL metadata of an extracted wheel, if possible. + Otherwise, raise UnsupportedWheel. """ try: dists = [d for d in pkg_resources.find_on_path(None, source_dir)] @@ -684,8 +686,14 @@ def wheel_version(source_dir): # FeedParser (used by Parser) does not raise any exceptions. The returned # message may have .defects populated, but for backwards-compatibility we # currently ignore them. - wheel_data = Parser().parsestr(wheel_text) + return Parser().parsestr(wheel_text) + +def wheel_version(wheel_data): + # type: (Message) -> Tuple[int, ...] + """Given WHEEL metadata, return the parsed Wheel-Version. + Otherwise, raise UnsupportedWheel. + """ version_text = wheel_data["Wheel-Version"] if version_text is None: raise UnsupportedWheel("WHEEL is missing Wheel-Version") diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 9689c0ec173..4c941dc5921 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -196,42 +196,43 @@ def test_wheel_version(tmpdir, data): unpack_file(data.packages.joinpath(future_wheel), tmpdir + 'future') - assert wheel.wheel_version(tmpdir + 'future') == future_version + metadata = wheel.wheel_metadata(tmpdir + 'future') + assert wheel.wheel_version(metadata) == future_version -def test_wheel_version_fails_on_error(monkeypatch): +def test_wheel_metadata_fails_on_error(monkeypatch): err = RuntimeError("example") monkeypatch.setattr(pkg_resources, "find_on_path", Mock(side_effect=err)) with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_version(".") + wheel.wheel_metadata(".") assert repr(err) in str(e.value) -def test_wheel_version_fails_no_dists(tmpdir): +def test_wheel_metadata_fails_no_dists(tmpdir): with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_version(str(tmpdir)) + wheel.wheel_metadata(str(tmpdir)) assert "no contained distribution found" in str(e.value) -def test_wheel_version_fails_missing_wheel(tmpdir): +def test_wheel_metadata_fails_missing_wheel(tmpdir): dist_info_dir = tmpdir / "simple-0.1.0.dist-info" dist_info_dir.mkdir() dist_info_dir.joinpath("METADATA").touch() with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_version(str(tmpdir)) + wheel.wheel_metadata(str(tmpdir)) assert "could not read WHEEL file" in str(e.value) @skip_if_python2 -def test_wheel_version_fails_on_bad_encoding(tmpdir): +def test_wheel_metadata_fails_on_bad_encoding(tmpdir): dist_info_dir = tmpdir / "simple-0.1.0.dist-info" dist_info_dir.mkdir() dist_info_dir.joinpath("METADATA").touch() dist_info_dir.joinpath("WHEEL").write_bytes(b"\xff") with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_version(str(tmpdir)) + wheel.wheel_metadata(str(tmpdir)) assert "error decoding WHEEL" in str(e.value) @@ -241,8 +242,9 @@ def test_wheel_version_fails_on_no_wheel_version(tmpdir): dist_info_dir.joinpath("METADATA").touch() dist_info_dir.joinpath("WHEEL").touch() + metadata = wheel.wheel_metadata(str(tmpdir)) with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_version(str(tmpdir)) + wheel.wheel_version(metadata) assert "missing Wheel-Version" in str(e.value) @@ -259,8 +261,9 @@ def test_wheel_version_fails_on_bad_wheel_version(tmpdir, version): "Wheel-Version: {}".format(version) ) + metadata = wheel.wheel_metadata(str(tmpdir)) with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_version(str(tmpdir)) + wheel.wheel_version(metadata) assert "invalid Wheel-Version" in str(e.value) From ca729c89de909a64f76bd191a9d5f43db9e9d93b Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 31 Dec 2019 12:32:11 -0500 Subject: [PATCH 1000/3170] Simplify wheel_version tests --- tests/unit/test_wheel.py | 25 +++++++------------------ 1 file changed, 7 insertions(+), 18 deletions(-) diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 4c941dc5921..fa3f7eeea4d 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -3,6 +3,7 @@ import logging import os import textwrap +from email import message_from_string import pytest from mock import Mock, patch @@ -236,15 +237,9 @@ def test_wheel_metadata_fails_on_bad_encoding(tmpdir): assert "error decoding WHEEL" in str(e.value) -def test_wheel_version_fails_on_no_wheel_version(tmpdir): - dist_info_dir = tmpdir / "simple-0.1.0.dist-info" - dist_info_dir.mkdir() - dist_info_dir.joinpath("METADATA").touch() - dist_info_dir.joinpath("WHEEL").touch() - - metadata = wheel.wheel_metadata(str(tmpdir)) +def test_wheel_version_fails_on_no_wheel_version(): with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_version(metadata) + wheel.wheel_version(message_from_string("")) assert "missing Wheel-Version" in str(e.value) @@ -253,17 +248,11 @@ def test_wheel_version_fails_on_no_wheel_version(tmpdir): ("1.b",), ("1.",), ]) -def test_wheel_version_fails_on_bad_wheel_version(tmpdir, version): - dist_info_dir = tmpdir / "simple-0.1.0.dist-info" - dist_info_dir.mkdir() - dist_info_dir.joinpath("METADATA").touch() - dist_info_dir.joinpath("WHEEL").write_text( - "Wheel-Version: {}".format(version) - ) - - metadata = wheel.wheel_metadata(str(tmpdir)) +def test_wheel_version_fails_on_bad_wheel_version(version): with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_version(metadata) + wheel.wheel_version( + message_from_string("Wheel-Version: {}".format(version)) + ) assert "invalid Wheel-Version" in str(e.value) From 42c6dd78b935d73b0f94bce06bf937b2af564501 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 31 Dec 2019 12:45:13 -0500 Subject: [PATCH 1001/3170] Use WHEEL metadata to determine Root-Is-Purelib --- src/pip/_internal/operations/install/wheel.py | 7 ++++++- tests/unit/test_wheel.py | 12 ++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 667a139c7a9..9b7d62cd3c3 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -117,6 +117,11 @@ def root_is_purelib(name, wheeldir): return False +def wheel_root_is_purelib(metadata): + # type: (Message) -> bool + return metadata.get("Root-Is-Purelib", "").lower() == "true" + + def get_entrypoints(filename): # type: (str) -> Tuple[Dict[str, str], Dict[str, str]] if not os.path.exists(filename): @@ -360,7 +365,7 @@ def install_unpacked_wheel( check_compatibility(version, name) - if root_is_purelib(name, wheeldir): + if wheel_root_is_purelib(metadata): lib_dir = scheme.purelib else: lib_dir = scheme.platlib diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index fa3f7eeea4d..d40878eedd0 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -256,6 +256,18 @@ def test_wheel_version_fails_on_bad_wheel_version(version): assert "invalid Wheel-Version" in str(e.value) +@pytest.mark.parametrize("text,expected", [ + ("Root-Is-Purelib: true", True), + ("Root-Is-Purelib: false", False), + ("Root-Is-Purelib: hello", False), + ("", False), + ("root-is-purelib: true", True), + ("root-is-purelib: True", True), +]) +def test_wheel_root_is_purelib(text, expected): + assert wheel.wheel_root_is_purelib(message_from_string(text)) == expected + + def test_check_compatibility(): name = 'test' vc = wheel.VERSION_COMPATIBLE From c9b0742508568616f1ce3af77f6d5e3737e5ba88 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 31 Dec 2019 12:49:08 -0500 Subject: [PATCH 1002/3170] Remove old root_is_purelib The _invalidversion_ tests are not applicable to the new function since we do not use a regex to find the applicable folder. --- src/pip/_internal/operations/install/wheel.py | 19 ------------------- .../plat_wheel-1.7.dist-info/WHEEL | 4 ---- .../WHEEL | 4 ---- .../pure_wheel-1.7.dist-info/WHEEL | 4 ---- .../WHEEL | 4 ---- tests/unit/test_wheel.py | 16 ---------------- 6 files changed, 51 deletions(-) delete mode 100644 tests/data/packages/plat_wheel-1.7/plat_wheel-1.7.dist-info/WHEEL delete mode 100644 tests/data/packages/plat_wheel-_invalidversion_/plat_wheel-_invalidversion_.dist-info/WHEEL delete mode 100644 tests/data/packages/pure_wheel-1.7/pure_wheel-1.7.dist-info/WHEEL delete mode 100644 tests/data/packages/pure_wheel-_invalidversion_/pure_wheel-_invalidversion_.dist-info/WHEEL diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 9b7d62cd3c3..13ff0b3c460 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -98,25 +98,6 @@ def fix_script(path): return None -dist_info_re = re.compile(r"""^(?P<namever>(?P<name>.+?)(-(?P<ver>.+?))?) - \.dist-info$""", re.VERBOSE) - - -def root_is_purelib(name, wheeldir): - # type: (str, str) -> bool - """True if the extracted wheel in wheeldir should go into purelib.""" - name_folded = name.replace("-", "_") - for item in os.listdir(wheeldir): - match = dist_info_re.match(item) - if match and match.group('name') == name_folded: - with open(os.path.join(wheeldir, item, 'WHEEL')) as wheel: - for line in wheel: - line = line.lower().rstrip() - if line == "root-is-purelib: true": - return True - return False - - def wheel_root_is_purelib(metadata): # type: (Message) -> bool return metadata.get("Root-Is-Purelib", "").lower() == "true" diff --git a/tests/data/packages/plat_wheel-1.7/plat_wheel-1.7.dist-info/WHEEL b/tests/data/packages/plat_wheel-1.7/plat_wheel-1.7.dist-info/WHEEL deleted file mode 100644 index 0a698cb6bf9..00000000000 --- a/tests/data/packages/plat_wheel-1.7/plat_wheel-1.7.dist-info/WHEEL +++ /dev/null @@ -1,4 +0,0 @@ -Wheel-Version: 0.1 -Packager: bdist_wheel -Root-Is-Purelib: false - diff --git a/tests/data/packages/plat_wheel-_invalidversion_/plat_wheel-_invalidversion_.dist-info/WHEEL b/tests/data/packages/plat_wheel-_invalidversion_/plat_wheel-_invalidversion_.dist-info/WHEEL deleted file mode 100644 index 0a698cb6bf9..00000000000 --- a/tests/data/packages/plat_wheel-_invalidversion_/plat_wheel-_invalidversion_.dist-info/WHEEL +++ /dev/null @@ -1,4 +0,0 @@ -Wheel-Version: 0.1 -Packager: bdist_wheel -Root-Is-Purelib: false - diff --git a/tests/data/packages/pure_wheel-1.7/pure_wheel-1.7.dist-info/WHEEL b/tests/data/packages/pure_wheel-1.7/pure_wheel-1.7.dist-info/WHEEL deleted file mode 100644 index 782313d161b..00000000000 --- a/tests/data/packages/pure_wheel-1.7/pure_wheel-1.7.dist-info/WHEEL +++ /dev/null @@ -1,4 +0,0 @@ -Wheel-Version: 0.1 -Packager: bdist_wheel -Root-Is-Purelib: true - diff --git a/tests/data/packages/pure_wheel-_invalidversion_/pure_wheel-_invalidversion_.dist-info/WHEEL b/tests/data/packages/pure_wheel-_invalidversion_/pure_wheel-_invalidversion_.dist-info/WHEEL deleted file mode 100644 index 782313d161b..00000000000 --- a/tests/data/packages/pure_wheel-_invalidversion_/pure_wheel-_invalidversion_.dist-info/WHEEL +++ /dev/null @@ -1,4 +0,0 @@ -Wheel-Version: 0.1 -Packager: bdist_wheel -Root-Is-Purelib: true - diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index d40878eedd0..a9aea152d9f 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -300,22 +300,6 @@ def test_unpack_wheel_no_flatten(self, tmpdir): unpack_file(filepath, tmpdir) assert os.path.isdir(os.path.join(tmpdir, 'meta-1.0.dist-info')) - def test_purelib_platlib(self, data): - """ - Test the "wheel is purelib/platlib" code. - """ - packages = [ - ("pure_wheel", data.packages.joinpath("pure_wheel-1.7"), True), - ("plat_wheel", data.packages.joinpath("plat_wheel-1.7"), False), - ("pure_wheel", data.packages.joinpath( - "pure_wheel-_invalidversion_"), True), - ("plat_wheel", data.packages.joinpath( - "plat_wheel-_invalidversion_"), False), - ] - - for name, path, expected in packages: - assert wheel.root_is_purelib(name, path) == expected - class TestInstallUnpackedWheel(object): """ From ed40706534093d995e4ad840b965cddfe4deef63 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 31 Dec 2019 15:45:35 +0800 Subject: [PATCH 1003/3170] Use sys.executable to format upgrade message --- news/7376.feature | 2 ++ src/pip/_internal/self_outdated_check.py | 12 +++++------- 2 files changed, 7 insertions(+), 7 deletions(-) create mode 100644 news/7376.feature diff --git a/news/7376.feature b/news/7376.feature new file mode 100644 index 00000000000..43ed51345db --- /dev/null +++ b/news/7376.feature @@ -0,0 +1,2 @@ +Suggest a more robust command to upgrade pip itself to avoid confusion when the +current pip command is not available as ``pip``. diff --git a/src/pip/_internal/self_outdated_check.py b/src/pip/_internal/self_outdated_check.py index 38f1d815f39..7f49331d0cb 100644 --- a/src/pip/_internal/self_outdated_check.py +++ b/src/pip/_internal/self_outdated_check.py @@ -18,7 +18,6 @@ from pip._internal.index.package_finder import PackageFinder from pip._internal.models.search_scope import SearchScope from pip._internal.models.selection_prefs import SelectionPreferences -from pip._internal.utils.compat import WINDOWS from pip._internal.utils.filesystem import ( adjacent_tmp_file, check_path_owner, @@ -225,12 +224,11 @@ def pip_self_version_check(session, options): if not local_version_is_older: return - # Advise "python -m pip" on Windows to avoid issues - # with overwriting pip.exe. - if WINDOWS: - pip_cmd = "python -m pip" - else: - pip_cmd = "pip" + # We cannot tell how the current pip is available in the current + # command context, so be pragmatic here and suggest the command + # that's always available. This doea not accomodate spaces in + # `sys.executable`. + pip_cmd = "{} -m pip".format(sys.executable) logger.warning( "You are using pip version %s; however, version %s is " "available.\nYou should consider upgrading via the " From 6c2ffcd0ce7f1d48a14017e61c9cd9cd0a0ba3dc Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 2 Jan 2020 00:00:59 +0800 Subject: [PATCH 1004/3170] Update src/pip/_internal/self_outdated_check.py Co-Authored-By: Christopher Hunt <chrahunt@gmail.com> --- src/pip/_internal/self_outdated_check.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/self_outdated_check.py b/src/pip/_internal/self_outdated_check.py index 7f49331d0cb..8fc3c594acf 100644 --- a/src/pip/_internal/self_outdated_check.py +++ b/src/pip/_internal/self_outdated_check.py @@ -226,7 +226,7 @@ def pip_self_version_check(session, options): # We cannot tell how the current pip is available in the current # command context, so be pragmatic here and suggest the command - # that's always available. This doea not accomodate spaces in + # that's always available. This does not accommodate spaces in # `sys.executable`. pip_cmd = "{} -m pip".format(sys.executable) logger.warning( From c51f020b9834ec56874feac1626d43e4bfdc1d40 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 31 Dec 2019 13:00:57 -0500 Subject: [PATCH 1005/3170] Read WHEEL from .dist-info instead of using has_metadata This will make it easier to transition to the already-determined dist-info directory and reduces some of our dependence on pkg_resources. Despite the name, the `egg_info` member is also populated for .dist-info dirs. ensure_str uses encoding='utf-8' and errors='strict' for Python 3 by default, which matches the behavior in `pkg_resources.NullProvider.get_metadata`. --- src/pip/_internal/operations/install/wheel.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 13ff0b3c460..ee9508bd569 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -23,7 +23,7 @@ from pip._vendor.distlib.scripts import ScriptMaker from pip._vendor.distlib.util import get_export_entry from pip._vendor.packaging.utils import canonicalize_name -from pip._vendor.six import StringIO +from pip._vendor.six import StringIO, ensure_str from pip._internal.exceptions import InstallationError, UnsupportedWheel from pip._internal.locations import get_major_minor_version @@ -662,8 +662,11 @@ def wheel_metadata(source_dir): dist = dists[0] + dist_info_dir = os.path.basename(dist.egg_info) + try: - wheel_text = dist.get_metadata('WHEEL') + with open(os.path.join(source_dir, dist_info_dir, "WHEEL"), "rb") as f: + wheel_text = ensure_str(f.read()) except (IOError, OSError) as e: raise UnsupportedWheel("could not read WHEEL file: {!r}".format(e)) except UnicodeDecodeError as e: From 32115b55afa18301e30ed23899c1ae5d418337bc Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 31 Dec 2019 13:23:48 -0500 Subject: [PATCH 1006/3170] Extract getting wheel .dist-info directory into a separate function --- src/pip/_internal/operations/install/wheel.py | 59 +++++++++++-------- tests/unit/test_wheel.py | 27 +++++++++ 2 files changed, 63 insertions(+), 23 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index ee9508bd569..ecba5e8bccb 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -312,29 +312,7 @@ def install_unpacked_wheel( # installation. source = wheeldir.rstrip(os.path.sep) + os.path.sep - subdirs = os.listdir(source) - info_dirs = [s for s in subdirs if s.endswith('.dist-info')] - - assert info_dirs, "{} .dist-info directory not found".format( - req_description - ) - - assert len(info_dirs) == 1, ( - '{} multiple .dist-info directories found: {}'.format( - req_description, ', '.join(info_dirs) - ) - ) - - info_dir = info_dirs[0] - - info_dir_name = canonicalize_name(info_dir) - canonical_name = canonicalize_name(name) - if not info_dir_name.startswith(canonical_name): - raise UnsupportedWheel( - "{} .dist-info directory {!r} does not start with {!r}".format( - req_description, info_dir, canonical_name - ) - ) + info_dir = wheel_dist_info_dir(source, req_description, name) try: metadata = wheel_metadata(wheeldir) @@ -351,6 +329,7 @@ def install_unpacked_wheel( else: lib_dir = scheme.platlib + subdirs = os.listdir(source) data_dirs = [s for s in subdirs if s.endswith('.data')] # Record details of the files moved @@ -645,6 +624,40 @@ def install_wheel( ) +def wheel_dist_info_dir(source, req_description, name): + # type: (str, str, str) -> str + """Returns the name of the contained .dist-info directory. + + Raises AssertionError or UnsupportedWheel if not found, >1 found, or + it doesn't match the provided name. + """ + subdirs = os.listdir(source) + info_dirs = [s for s in subdirs if s.endswith('.dist-info')] + + assert info_dirs, "{} .dist-info directory not found".format( + req_description + ) + + assert len(info_dirs) == 1, ( + '{} multiple .dist-info directories found: {}'.format( + req_description, ', '.join(info_dirs) + ) + ) + + info_dir = info_dirs[0] + + info_dir_name = canonicalize_name(info_dir) + canonical_name = canonicalize_name(name) + if not info_dir_name.startswith(canonical_name): + raise UnsupportedWheel( + "{} .dist-info directory {!r} does not start with {!r}".format( + req_description, info_dir, canonical_name + ) + ) + + return info_dir + + def wheel_metadata(source_dir): # type: (Optional[str]) -> Message """Return the WHEEL metadata of an extracted wheel, if possible. diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index a9aea152d9f..436af6a9659 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -191,6 +191,33 @@ def test_get_csv_rows_for_installed__long_lines(tmpdir, caplog): assert messages == expected +def test_wheel_dist_info_dir_found(tmpdir): + expected = "simple-0.1.dist-info" + tmpdir.joinpath(expected).mkdir() + assert wheel.wheel_dist_info_dir(str(tmpdir), "", "simple") == expected + + +def test_wheel_dist_info_dir_multiple(tmpdir): + tmpdir.joinpath("simple-0.1.dist-info").mkdir() + tmpdir.joinpath("unrelated-0.1.dist-info").mkdir() + with pytest.raises(AssertionError) as e: + wheel.wheel_dist_info_dir(str(tmpdir), "", "simple") + assert "multiple .dist-info directories found" in str(e.value) + + +def test_wheel_dist_info_dir_none(tmpdir): + with pytest.raises(AssertionError) as e: + wheel.wheel_dist_info_dir(str(tmpdir), "", "simple") + assert "directory not found" in str(e.value) + + +def test_wheel_dist_info_dir_wrong_name(tmpdir): + tmpdir.joinpath("unrelated-0.1.dist-info").mkdir() + with pytest.raises(UnsupportedWheel) as e: + wheel.wheel_dist_info_dir(str(tmpdir), "", "simple") + assert "does not start with 'simple'" in str(e.value) + + def test_wheel_version(tmpdir, data): future_wheel = 'futurewheel-1.9-py2.py3-none-any.whl' future_version = (1, 9) From a539c8dfe0e67181c60d19725ef7278876fb47b3 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 31 Dec 2019 13:39:40 -0500 Subject: [PATCH 1007/3170] Normalize exception message for dist_info_dir check --- src/pip/_internal/operations/install/wheel.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index ecba5e8bccb..b24b29874dc 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -312,9 +312,9 @@ def install_unpacked_wheel( # installation. source = wheeldir.rstrip(os.path.sep) + os.path.sep - info_dir = wheel_dist_info_dir(source, req_description, name) try: + info_dir = wheel_dist_info_dir(source, req_description, name) metadata = wheel_metadata(wheeldir) version = wheel_version(metadata) except UnsupportedWheel as e: @@ -650,8 +650,8 @@ def wheel_dist_info_dir(source, req_description, name): canonical_name = canonicalize_name(name) if not info_dir_name.startswith(canonical_name): raise UnsupportedWheel( - "{} .dist-info directory {!r} does not start with {!r}".format( - req_description, info_dir, canonical_name + ".dist-info directory {!r} does not start with {!r}".format( + info_dir, canonical_name ) ) From e1c7de8861687d93b11dba50e9cbeec066a2a13f Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 31 Dec 2019 13:43:19 -0500 Subject: [PATCH 1008/3170] Raise UnsupportedWheel instead of AssertionError, and let caller add name --- src/pip/_internal/operations/install/wheel.py | 14 +++++++------- tests/unit/test_wheel.py | 4 ++-- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index b24b29874dc..f6ca9629779 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -634,15 +634,15 @@ def wheel_dist_info_dir(source, req_description, name): subdirs = os.listdir(source) info_dirs = [s for s in subdirs if s.endswith('.dist-info')] - assert info_dirs, "{} .dist-info directory not found".format( - req_description - ) + if not info_dirs: + raise UnsupportedWheel(".dist-info directory not found") - assert len(info_dirs) == 1, ( - '{} multiple .dist-info directories found: {}'.format( - req_description, ', '.join(info_dirs) + if len(info_dirs) > 1: + raise UnsupportedWheel( + "multiple .dist-info directories found: {}".format( + ", ".join(info_dirs) + ) ) - ) info_dir = info_dirs[0] diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 436af6a9659..3acf0fe635c 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -200,13 +200,13 @@ def test_wheel_dist_info_dir_found(tmpdir): def test_wheel_dist_info_dir_multiple(tmpdir): tmpdir.joinpath("simple-0.1.dist-info").mkdir() tmpdir.joinpath("unrelated-0.1.dist-info").mkdir() - with pytest.raises(AssertionError) as e: + with pytest.raises(UnsupportedWheel) as e: wheel.wheel_dist_info_dir(str(tmpdir), "", "simple") assert "multiple .dist-info directories found" in str(e.value) def test_wheel_dist_info_dir_none(tmpdir): - with pytest.raises(AssertionError) as e: + with pytest.raises(UnsupportedWheel) as e: wheel.wheel_dist_info_dir(str(tmpdir), "", "simple") assert "directory not found" in str(e.value) From 6c56557fbedc7b978e3c2422e089149ca75c47a1 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 31 Dec 2019 13:44:20 -0500 Subject: [PATCH 1009/3170] Remove unused req_description from wheel_dist_info_dir --- src/pip/_internal/operations/install/wheel.py | 6 +++--- tests/unit/test_wheel.py | 8 ++++---- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index f6ca9629779..99fc50cb47c 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -314,7 +314,7 @@ def install_unpacked_wheel( source = wheeldir.rstrip(os.path.sep) + os.path.sep try: - info_dir = wheel_dist_info_dir(source, req_description, name) + info_dir = wheel_dist_info_dir(source, name) metadata = wheel_metadata(wheeldir) version = wheel_version(metadata) except UnsupportedWheel as e: @@ -624,8 +624,8 @@ def install_wheel( ) -def wheel_dist_info_dir(source, req_description, name): - # type: (str, str, str) -> str +def wheel_dist_info_dir(source, name): + # type: (str, str) -> str """Returns the name of the contained .dist-info directory. Raises AssertionError or UnsupportedWheel if not found, >1 found, or diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 3acf0fe635c..31dc064fddd 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -194,27 +194,27 @@ def test_get_csv_rows_for_installed__long_lines(tmpdir, caplog): def test_wheel_dist_info_dir_found(tmpdir): expected = "simple-0.1.dist-info" tmpdir.joinpath(expected).mkdir() - assert wheel.wheel_dist_info_dir(str(tmpdir), "", "simple") == expected + assert wheel.wheel_dist_info_dir(str(tmpdir), "simple") == expected def test_wheel_dist_info_dir_multiple(tmpdir): tmpdir.joinpath("simple-0.1.dist-info").mkdir() tmpdir.joinpath("unrelated-0.1.dist-info").mkdir() with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_dist_info_dir(str(tmpdir), "", "simple") + wheel.wheel_dist_info_dir(str(tmpdir), "simple") assert "multiple .dist-info directories found" in str(e.value) def test_wheel_dist_info_dir_none(tmpdir): with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_dist_info_dir(str(tmpdir), "", "simple") + wheel.wheel_dist_info_dir(str(tmpdir), "simple") assert "directory not found" in str(e.value) def test_wheel_dist_info_dir_wrong_name(tmpdir): tmpdir.joinpath("unrelated-0.1.dist-info").mkdir() with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_dist_info_dir(str(tmpdir), "", "simple") + wheel.wheel_dist_info_dir(str(tmpdir), "simple") assert "does not start with 'simple'" in str(e.value) From fc48a172066a4aa3e56bafc6b303d87faaa17d03 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 31 Dec 2019 13:56:19 -0500 Subject: [PATCH 1010/3170] Simplify positive wheel_version unit test --- tests/unit/test_wheel.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 31dc064fddd..88ca0edfb47 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -218,14 +218,10 @@ def test_wheel_dist_info_dir_wrong_name(tmpdir): assert "does not start with 'simple'" in str(e.value) -def test_wheel_version(tmpdir, data): - future_wheel = 'futurewheel-1.9-py2.py3-none-any.whl' - future_version = (1, 9) - - unpack_file(data.packages.joinpath(future_wheel), tmpdir + 'future') - - metadata = wheel.wheel_metadata(tmpdir + 'future') - assert wheel.wheel_version(metadata) == future_version +def test_wheel_version_ok(tmpdir, data): + assert wheel.wheel_version( + message_from_string("Wheel-Version: 1.9") + ) == (1, 9) def test_wheel_metadata_fails_on_error(monkeypatch): From 010c24d64c2f6efe1ad5d6d6ff888297549a0e0b Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 31 Dec 2019 14:00:16 -0500 Subject: [PATCH 1011/3170] Take .dist-info dir directly in wheel_metadata Since retrieval of the .dist-info dir already ensures that a distribution is found, this reduces responsibility on wheel_metadata and lets us remove a few tests already covered by the tests for test_wheel_dist_info_dir_*. --- src/pip/_internal/operations/install/wheel.py | 22 ++++--------------- tests/unit/test_wheel.py | 21 +++--------------- 2 files changed, 7 insertions(+), 36 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 99fc50cb47c..200d7fe0ab7 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -315,7 +315,7 @@ def install_unpacked_wheel( try: info_dir = wheel_dist_info_dir(source, name) - metadata = wheel_metadata(wheeldir) + metadata = wheel_metadata(source, info_dir) version = wheel_version(metadata) except UnsupportedWheel as e: raise UnsupportedWheel( @@ -658,27 +658,13 @@ def wheel_dist_info_dir(source, name): return info_dir -def wheel_metadata(source_dir): - # type: (Optional[str]) -> Message +def wheel_metadata(source, dist_info_dir): + # type: (str, str) -> Message """Return the WHEEL metadata of an extracted wheel, if possible. Otherwise, raise UnsupportedWheel. """ try: - dists = [d for d in pkg_resources.find_on_path(None, source_dir)] - except Exception as e: - raise UnsupportedWheel( - "could not find a contained distribution due to: {!r}".format(e) - ) - - if not dists: - raise UnsupportedWheel("no contained distribution found") - - dist = dists[0] - - dist_info_dir = os.path.basename(dist.egg_info) - - try: - with open(os.path.join(source_dir, dist_info_dir, "WHEEL"), "rb") as f: + with open(os.path.join(source, dist_info_dir, "WHEEL"), "rb") as f: wheel_text = ensure_str(f.read()) except (IOError, OSError) as e: raise UnsupportedWheel("could not read WHEEL file: {!r}".format(e)) diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 88ca0edfb47..f6344b8ef04 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -6,8 +6,7 @@ from email import message_from_string import pytest -from mock import Mock, patch -from pip._vendor import pkg_resources +from mock import patch from pip._vendor.packaging.requirements import Requirement from pip._internal.exceptions import UnsupportedWheel @@ -224,27 +223,13 @@ def test_wheel_version_ok(tmpdir, data): ) == (1, 9) -def test_wheel_metadata_fails_on_error(monkeypatch): - err = RuntimeError("example") - monkeypatch.setattr(pkg_resources, "find_on_path", Mock(side_effect=err)) - with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_metadata(".") - assert repr(err) in str(e.value) - - -def test_wheel_metadata_fails_no_dists(tmpdir): - with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_metadata(str(tmpdir)) - assert "no contained distribution found" in str(e.value) - - def test_wheel_metadata_fails_missing_wheel(tmpdir): dist_info_dir = tmpdir / "simple-0.1.0.dist-info" dist_info_dir.mkdir() dist_info_dir.joinpath("METADATA").touch() with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_metadata(str(tmpdir)) + wheel.wheel_metadata(str(tmpdir), dist_info_dir.name) assert "could not read WHEEL file" in str(e.value) @@ -256,7 +241,7 @@ def test_wheel_metadata_fails_on_bad_encoding(tmpdir): dist_info_dir.joinpath("WHEEL").write_bytes(b"\xff") with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_metadata(str(tmpdir)) + wheel.wheel_metadata(str(tmpdir), dist_info_dir.name) assert "error decoding WHEEL" in str(e.value) From 68e49b9613fe1a87974a8ea648342c7e0391d780 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Tue, 31 Dec 2019 05:40:51 +0100 Subject: [PATCH 1012/3170] Remove redundant expanduser for download_dir in preparer Since - download_dir is only set by the download command - download_dir is normalized at the beginning of the download command - path normalization includes expanduser Therefore expanduser in the preparer is redundant --- src/pip/_internal/operations/prepare.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 73f4c1294b7..5f5505cd71c 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -25,7 +25,6 @@ PreviousBuildDirError, VcsHashUnsupported, ) -from pip._internal.utils.compat import expanduser from pip._internal.utils.filesystem import copy2_fixed from pip._internal.utils.hashes import MissingHashes from pip._internal.utils.logging import indent_log @@ -364,8 +363,6 @@ def __init__( # Where still-packed archives should be written to. If None, they are # not saved, and are deleted immediately after unpacking. - if download_dir: - download_dir = expanduser(download_dir) self.download_dir = download_dir # Where still-packed .whl files should be written to. If None, they are From 513a3162de0b2b62ec617856243daf4b6a033b68 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 1 Jan 2020 12:29:45 -0500 Subject: [PATCH 1013/3170] Pass open zip file for wheel into install_unpacked_wheel With this parameter we can incrementally update code to rely on the zip file instead of the unpacked source directory. --- src/pip/_internal/operations/install/wheel.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 200d7fe0ab7..bcadd3c9dd7 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -18,6 +18,7 @@ import warnings from base64 import urlsafe_b64encode from email.parser import Parser +from zipfile import ZipFile from pip._vendor import pkg_resources from pip._vendor.distlib.scripts import ScriptMaker @@ -286,6 +287,7 @@ def make(self, specification, options=None): def install_unpacked_wheel( name, # type: str wheeldir, # type: str + wheel_zip, # type: ZipFile scheme, # type: Scheme req_description, # type: str pycompile=True, # type: bool @@ -296,6 +298,7 @@ def install_unpacked_wheel( :param name: Name of the project to install :param wheeldir: Base directory of the unpacked wheel + :param wheel_zip: open ZipFile for wheel being installed :param scheme: Distutils scheme dictating the install directories :param req_description: String used in place of the requirement, for logging @@ -612,11 +615,12 @@ def install_wheel( # type: (...) -> None with TempDirectory( path=_temp_dir_for_testing, kind="unpacked-wheel" - ) as unpacked_dir: + ) as unpacked_dir, ZipFile(wheel_path, allowZip64=True) as z: unpack_file(wheel_path, unpacked_dir.path) install_unpacked_wheel( name=name, wheeldir=unpacked_dir.path, + wheel_zip=z, scheme=scheme, req_description=req_description, pycompile=pycompile, From 452acd5e7613d5d869fdf58bd6dba2dbf723056e Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 1 Jan 2020 13:01:56 -0500 Subject: [PATCH 1014/3170] Add files in .dist-info directory tests Since zips don't typically contain directory entries, we want to only operate on files. Adding files to the .dist-info tests means we will be able to reuse them for both cases, while they coexist. --- tests/unit/test_wheel.py | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index f6344b8ef04..c9a62270f1e 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -192,13 +192,19 @@ def test_get_csv_rows_for_installed__long_lines(tmpdir, caplog): def test_wheel_dist_info_dir_found(tmpdir): expected = "simple-0.1.dist-info" - tmpdir.joinpath(expected).mkdir() + dist_info_dir = tmpdir / expected + dist_info_dir.mkdir() + dist_info_dir.joinpath("WHEEL").touch() assert wheel.wheel_dist_info_dir(str(tmpdir), "simple") == expected def test_wheel_dist_info_dir_multiple(tmpdir): - tmpdir.joinpath("simple-0.1.dist-info").mkdir() - tmpdir.joinpath("unrelated-0.1.dist-info").mkdir() + dist_info_dir_1 = tmpdir / "simple-0.1.dist-info" + dist_info_dir_1.mkdir() + dist_info_dir_1.joinpath("WHEEL").touch() + dist_info_dir_2 = tmpdir / "unrelated-0.1.dist-info" + dist_info_dir_2.mkdir() + dist_info_dir_2.joinpath("WHEEL").touch() with pytest.raises(UnsupportedWheel) as e: wheel.wheel_dist_info_dir(str(tmpdir), "simple") assert "multiple .dist-info directories found" in str(e.value) @@ -211,7 +217,9 @@ def test_wheel_dist_info_dir_none(tmpdir): def test_wheel_dist_info_dir_wrong_name(tmpdir): - tmpdir.joinpath("unrelated-0.1.dist-info").mkdir() + dist_info_dir = tmpdir / "unrelated-0.1.dist-info" + dist_info_dir.mkdir() + dist_info_dir.joinpath("WHEEL").touch() with pytest.raises(UnsupportedWheel) as e: wheel.wheel_dist_info_dir(str(tmpdir), "simple") assert "does not start with 'simple'" in str(e.value) From cd5400ceb97c6170474fa4d037813595e22143d7 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 1 Jan 2020 13:21:47 -0500 Subject: [PATCH 1015/3170] Determine .dist-info directory from wheel file directly First example of transitioning a directory-aware function to using a zipfile directly. Since we will not need to maintain the unpacked dir going forward, we don't need to worry about making wheel_dist_info_dir "generic", just that the same tests pass for both cases at each commit. To do this neatly we use pytest.fixture(params=[...]), which generates a test for each param. Once we've transitioned the necessary functions we only need to replace the fixture name and remove the dead code. --- src/pip/_internal/operations/install/wheel.py | 17 ++++-- tests/unit/test_wheel.py | 60 ++++++++++++++++--- 2 files changed, 64 insertions(+), 13 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index bcadd3c9dd7..7e3b78ee6c9 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -37,7 +37,7 @@ from email.message import Message from typing import ( Dict, List, Optional, Sequence, Tuple, IO, Text, Any, - Iterable, Callable, Set, + Iterable, Callable, Set, Union, ) from pip._internal.models.scheme import Scheme @@ -317,7 +317,7 @@ def install_unpacked_wheel( source = wheeldir.rstrip(os.path.sep) + os.path.sep try: - info_dir = wheel_dist_info_dir(source, name) + info_dir = wheel_dist_info_dir(wheel_zip, name) metadata = wheel_metadata(source, info_dir) version = wheel_version(metadata) except UnsupportedWheel as e: @@ -629,13 +629,18 @@ def install_wheel( def wheel_dist_info_dir(source, name): - # type: (str, str) -> str + # type: (Union[str, ZipFile], str) -> str """Returns the name of the contained .dist-info directory. Raises AssertionError or UnsupportedWheel if not found, >1 found, or it doesn't match the provided name. """ - subdirs = os.listdir(source) + if isinstance(source, ZipFile): + # Zip file path separators must be / + subdirs = list(set(p.split("/")[0] for p in source.namelist())) + else: + subdirs = os.listdir(source) + info_dirs = [s for s in subdirs if s.endswith('.dist-info')] if not info_dirs: @@ -659,7 +664,9 @@ def wheel_dist_info_dir(source, name): ) ) - return info_dir + # Zip file paths can be unicode or str depending on the zip entry flags, + # so normalize it. + return ensure_str(info_dir) def wheel_metadata(source, dist_info_dir): diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index c9a62270f1e..bdfcd724a52 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -4,9 +4,12 @@ import os import textwrap from email import message_from_string +from io import BytesIO +from zipfile import ZipFile import pytest from mock import patch +from pip._vendor.contextlib2 import ExitStack from pip._vendor.packaging.requirements import Requirement from pip._internal.exceptions import UnsupportedWheel @@ -22,9 +25,15 @@ ) from pip._internal.utils.compat import WINDOWS from pip._internal.utils.misc import hash_file +from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.unpacking import unpack_file from tests.lib import DATA_DIR, assert_paths_equal, skip_if_python2 +if MYPY_CHECK_RUNNING: + from typing import Union + + from tests.lib.path import Path + def call_get_legacy_build_wheel_path(caplog, names): wheel_path = get_legacy_build_wheel_path( @@ -190,15 +199,50 @@ def test_get_csv_rows_for_installed__long_lines(tmpdir, caplog): assert messages == expected -def test_wheel_dist_info_dir_found(tmpdir): +@pytest.fixture +def zip_dir(): + def make_zip(path): + # type: (Path) -> ZipFile + buf = BytesIO() + with ZipFile(buf, "w", allowZip64=True) as z: + for dirpath, dirnames, filenames in os.walk(path): + for filename in filenames: + file_path = os.path.join(path, dirpath, filename) + # Zip files must always have / as path separator + archive_path = os.path.relpath(file_path, path).replace( + os.pathsep, "/" + ) + z.write(file_path, archive_path) + + return stack.enter_context(ZipFile(buf, "r", allowZip64=True)) + + stack = ExitStack() + with stack: + yield make_zip + + +@pytest.fixture(params=[True, False]) +def zip_or_dir(request, zip_dir): + """Test both with directory and zip file representing directory. + """ + def get_zip_or_dir(path): + # type: (Path) -> Union[str, ZipFile] + if request.param: + return zip_dir(path) + return str(path) + + return get_zip_or_dir + + +def test_wheel_dist_info_dir_found(tmpdir, zip_or_dir): expected = "simple-0.1.dist-info" dist_info_dir = tmpdir / expected dist_info_dir.mkdir() dist_info_dir.joinpath("WHEEL").touch() - assert wheel.wheel_dist_info_dir(str(tmpdir), "simple") == expected + assert wheel.wheel_dist_info_dir(zip_or_dir(tmpdir), "simple") == expected -def test_wheel_dist_info_dir_multiple(tmpdir): +def test_wheel_dist_info_dir_multiple(tmpdir, zip_or_dir): dist_info_dir_1 = tmpdir / "simple-0.1.dist-info" dist_info_dir_1.mkdir() dist_info_dir_1.joinpath("WHEEL").touch() @@ -206,22 +250,22 @@ def test_wheel_dist_info_dir_multiple(tmpdir): dist_info_dir_2.mkdir() dist_info_dir_2.joinpath("WHEEL").touch() with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_dist_info_dir(str(tmpdir), "simple") + wheel.wheel_dist_info_dir(zip_or_dir(tmpdir), "simple") assert "multiple .dist-info directories found" in str(e.value) -def test_wheel_dist_info_dir_none(tmpdir): +def test_wheel_dist_info_dir_none(tmpdir, zip_or_dir): with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_dist_info_dir(str(tmpdir), "simple") + wheel.wheel_dist_info_dir(zip_or_dir(tmpdir), "simple") assert "directory not found" in str(e.value) -def test_wheel_dist_info_dir_wrong_name(tmpdir): +def test_wheel_dist_info_dir_wrong_name(tmpdir, zip_or_dir): dist_info_dir = tmpdir / "unrelated-0.1.dist-info" dist_info_dir.mkdir() dist_info_dir.joinpath("WHEEL").touch() with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_dist_info_dir(str(tmpdir), "simple") + wheel.wheel_dist_info_dir(zip_or_dir(tmpdir), "simple") assert "does not start with 'simple'" in str(e.value) From 62721eb5da9f3b242ac439024ad286d255189714 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 1 Jan 2020 14:45:10 -0500 Subject: [PATCH 1016/3170] Remove dead code Since we don't pass the source dir anymore, we can simplify the tests and implementation for wheel_dist_info_dir. --- src/pip/_internal/operations/install/wheel.py | 9 +++------ tests/unit/test_wheel.py | 16 ++++++++-------- 2 files changed, 11 insertions(+), 14 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 7e3b78ee6c9..a377761c723 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -629,17 +629,14 @@ def install_wheel( def wheel_dist_info_dir(source, name): - # type: (Union[str, ZipFile], str) -> str + # type: (ZipFile, str) -> str """Returns the name of the contained .dist-info directory. Raises AssertionError or UnsupportedWheel if not found, >1 found, or it doesn't match the provided name. """ - if isinstance(source, ZipFile): - # Zip file path separators must be / - subdirs = list(set(p.split("/")[0] for p in source.namelist())) - else: - subdirs = os.listdir(source) + # Zip file path separators must be / + subdirs = list(set(p.split("/")[0] for p in source.namelist())) info_dirs = [s for s in subdirs if s.endswith('.dist-info')] diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index bdfcd724a52..b6b53e3bec7 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -234,15 +234,15 @@ def get_zip_or_dir(path): return get_zip_or_dir -def test_wheel_dist_info_dir_found(tmpdir, zip_or_dir): +def test_wheel_dist_info_dir_found(tmpdir, zip_dir): expected = "simple-0.1.dist-info" dist_info_dir = tmpdir / expected dist_info_dir.mkdir() dist_info_dir.joinpath("WHEEL").touch() - assert wheel.wheel_dist_info_dir(zip_or_dir(tmpdir), "simple") == expected + assert wheel.wheel_dist_info_dir(zip_dir(tmpdir), "simple") == expected -def test_wheel_dist_info_dir_multiple(tmpdir, zip_or_dir): +def test_wheel_dist_info_dir_multiple(tmpdir, zip_dir): dist_info_dir_1 = tmpdir / "simple-0.1.dist-info" dist_info_dir_1.mkdir() dist_info_dir_1.joinpath("WHEEL").touch() @@ -250,22 +250,22 @@ def test_wheel_dist_info_dir_multiple(tmpdir, zip_or_dir): dist_info_dir_2.mkdir() dist_info_dir_2.joinpath("WHEEL").touch() with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_dist_info_dir(zip_or_dir(tmpdir), "simple") + wheel.wheel_dist_info_dir(zip_dir(tmpdir), "simple") assert "multiple .dist-info directories found" in str(e.value) -def test_wheel_dist_info_dir_none(tmpdir, zip_or_dir): +def test_wheel_dist_info_dir_none(tmpdir, zip_dir): with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_dist_info_dir(zip_or_dir(tmpdir), "simple") + wheel.wheel_dist_info_dir(zip_dir(tmpdir), "simple") assert "directory not found" in str(e.value) -def test_wheel_dist_info_dir_wrong_name(tmpdir, zip_or_dir): +def test_wheel_dist_info_dir_wrong_name(tmpdir, zip_dir): dist_info_dir = tmpdir / "unrelated-0.1.dist-info" dist_info_dir.mkdir() dist_info_dir.joinpath("WHEEL").touch() with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_dist_info_dir(zip_or_dir(tmpdir), "simple") + wheel.wheel_dist_info_dir(zip_dir(tmpdir), "simple") assert "does not start with 'simple'" in str(e.value) From 2519e9740c26d8b16a81f401eea6b70bc37ef64d Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 1 Jan 2020 15:33:11 -0500 Subject: [PATCH 1017/3170] Split try block in wheel_metadata Makes it simpler to substitute in zip-derived WHEEL extraction. --- src/pip/_internal/operations/install/wheel.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index a377761c723..d3ae1bb12f3 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -673,9 +673,12 @@ def wheel_metadata(source, dist_info_dir): """ try: with open(os.path.join(source, dist_info_dir, "WHEEL"), "rb") as f: - wheel_text = ensure_str(f.read()) + wheel_contents = f.read() except (IOError, OSError) as e: raise UnsupportedWheel("could not read WHEEL file: {!r}".format(e)) + + try: + wheel_text = ensure_str(wheel_contents) except UnicodeDecodeError as e: raise UnsupportedWheel("error decoding WHEEL: {!r}".format(e)) From a2af0f9468de0815424508a656cc546ff9539cb9 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 1 Jan 2020 15:51:00 -0500 Subject: [PATCH 1018/3170] Get WHEEL metadata from wheel file directly --- src/pip/_internal/operations/install/wheel.py | 30 ++++++++++++++----- tests/unit/test_wheel.py | 8 ++--- 2 files changed, 26 insertions(+), 12 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index d3ae1bb12f3..fad5afa6ae6 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -24,7 +24,7 @@ from pip._vendor.distlib.scripts import ScriptMaker from pip._vendor.distlib.util import get_export_entry from pip._vendor.packaging.utils import canonicalize_name -from pip._vendor.six import StringIO, ensure_str +from pip._vendor.six import PY2, StringIO, ensure_str from pip._internal.exceptions import InstallationError, UnsupportedWheel from pip._internal.locations import get_major_minor_version @@ -44,6 +44,11 @@ InstalledCSVRow = Tuple[str, ...] +if PY2: + from zipfile import BadZipfile as BadZipFile +else: + from zipfile import BadZipFile + VERSION_COMPATIBLE = (1, 0) @@ -318,7 +323,7 @@ def install_unpacked_wheel( try: info_dir = wheel_dist_info_dir(wheel_zip, name) - metadata = wheel_metadata(source, info_dir) + metadata = wheel_metadata(wheel_zip, info_dir) version = wheel_version(metadata) except UnsupportedWheel as e: raise UnsupportedWheel( @@ -667,15 +672,24 @@ def wheel_dist_info_dir(source, name): def wheel_metadata(source, dist_info_dir): - # type: (str, str) -> Message + # type: (Union[str, ZipFile], str) -> Message """Return the WHEEL metadata of an extracted wheel, if possible. Otherwise, raise UnsupportedWheel. """ - try: - with open(os.path.join(source, dist_info_dir, "WHEEL"), "rb") as f: - wheel_contents = f.read() - except (IOError, OSError) as e: - raise UnsupportedWheel("could not read WHEEL file: {!r}".format(e)) + if isinstance(source, ZipFile): + try: + # Zip file path separators must be / + wheel_contents = source.read("{}/WHEEL".format(dist_info_dir)) + # BadZipFile for general corruption, KeyError for missing entry, + # and RuntimeError for password-protected files + except (BadZipFile, KeyError, RuntimeError) as e: + raise UnsupportedWheel("could not read WHEEL file: {!r}".format(e)) + else: + try: + with open(os.path.join(source, dist_info_dir, "WHEEL"), "rb") as f: + wheel_contents = f.read() + except (IOError, OSError) as e: + raise UnsupportedWheel("could not read WHEEL file: {!r}".format(e)) try: wheel_text = ensure_str(wheel_contents) diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index b6b53e3bec7..467ff98dec4 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -275,25 +275,25 @@ def test_wheel_version_ok(tmpdir, data): ) == (1, 9) -def test_wheel_metadata_fails_missing_wheel(tmpdir): +def test_wheel_metadata_fails_missing_wheel(tmpdir, zip_or_dir): dist_info_dir = tmpdir / "simple-0.1.0.dist-info" dist_info_dir.mkdir() dist_info_dir.joinpath("METADATA").touch() with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_metadata(str(tmpdir), dist_info_dir.name) + wheel.wheel_metadata(zip_or_dir(tmpdir), dist_info_dir.name) assert "could not read WHEEL file" in str(e.value) @skip_if_python2 -def test_wheel_metadata_fails_on_bad_encoding(tmpdir): +def test_wheel_metadata_fails_on_bad_encoding(tmpdir, zip_or_dir): dist_info_dir = tmpdir / "simple-0.1.0.dist-info" dist_info_dir.mkdir() dist_info_dir.joinpath("METADATA").touch() dist_info_dir.joinpath("WHEEL").write_bytes(b"\xff") with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_metadata(str(tmpdir), dist_info_dir.name) + wheel.wheel_metadata(zip_or_dir(tmpdir), dist_info_dir.name) assert "error decoding WHEEL" in str(e.value) From d5b4c1510532788bafc50372628da62d10140762 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 1 Jan 2020 17:37:02 -0500 Subject: [PATCH 1019/3170] Remove dead code from wheel_metadata Since we don't pass the source directory anymore, remove the tests and implementation. --- src/pip/_internal/operations/install/wheel.py | 25 +++++++------------ tests/unit/test_wheel.py | 23 +++-------------- 2 files changed, 13 insertions(+), 35 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index fad5afa6ae6..9d94227f256 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -37,7 +37,7 @@ from email.message import Message from typing import ( Dict, List, Optional, Sequence, Tuple, IO, Text, Any, - Iterable, Callable, Set, Union, + Iterable, Callable, Set, ) from pip._internal.models.scheme import Scheme @@ -672,24 +672,17 @@ def wheel_dist_info_dir(source, name): def wheel_metadata(source, dist_info_dir): - # type: (Union[str, ZipFile], str) -> Message + # type: (ZipFile, str) -> Message """Return the WHEEL metadata of an extracted wheel, if possible. Otherwise, raise UnsupportedWheel. """ - if isinstance(source, ZipFile): - try: - # Zip file path separators must be / - wheel_contents = source.read("{}/WHEEL".format(dist_info_dir)) - # BadZipFile for general corruption, KeyError for missing entry, - # and RuntimeError for password-protected files - except (BadZipFile, KeyError, RuntimeError) as e: - raise UnsupportedWheel("could not read WHEEL file: {!r}".format(e)) - else: - try: - with open(os.path.join(source, dist_info_dir, "WHEEL"), "rb") as f: - wheel_contents = f.read() - except (IOError, OSError) as e: - raise UnsupportedWheel("could not read WHEEL file: {!r}".format(e)) + try: + # Zip file path separators must be / + wheel_contents = source.read("{}/WHEEL".format(dist_info_dir)) + # BadZipFile for general corruption, KeyError for missing entry, + # and RuntimeError for password-protected files + except (BadZipFile, KeyError, RuntimeError) as e: + raise UnsupportedWheel("could not read WHEEL file: {!r}".format(e)) try: wheel_text = ensure_str(wheel_contents) diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 467ff98dec4..cdf273449b4 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -30,8 +30,6 @@ from tests.lib import DATA_DIR, assert_paths_equal, skip_if_python2 if MYPY_CHECK_RUNNING: - from typing import Union - from tests.lib.path import Path @@ -221,19 +219,6 @@ def make_zip(path): yield make_zip -@pytest.fixture(params=[True, False]) -def zip_or_dir(request, zip_dir): - """Test both with directory and zip file representing directory. - """ - def get_zip_or_dir(path): - # type: (Path) -> Union[str, ZipFile] - if request.param: - return zip_dir(path) - return str(path) - - return get_zip_or_dir - - def test_wheel_dist_info_dir_found(tmpdir, zip_dir): expected = "simple-0.1.dist-info" dist_info_dir = tmpdir / expected @@ -275,25 +260,25 @@ def test_wheel_version_ok(tmpdir, data): ) == (1, 9) -def test_wheel_metadata_fails_missing_wheel(tmpdir, zip_or_dir): +def test_wheel_metadata_fails_missing_wheel(tmpdir, zip_dir): dist_info_dir = tmpdir / "simple-0.1.0.dist-info" dist_info_dir.mkdir() dist_info_dir.joinpath("METADATA").touch() with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_metadata(zip_or_dir(tmpdir), dist_info_dir.name) + wheel.wheel_metadata(zip_dir(tmpdir), dist_info_dir.name) assert "could not read WHEEL file" in str(e.value) @skip_if_python2 -def test_wheel_metadata_fails_on_bad_encoding(tmpdir, zip_or_dir): +def test_wheel_metadata_fails_on_bad_encoding(tmpdir, zip_dir): dist_info_dir = tmpdir / "simple-0.1.0.dist-info" dist_info_dir.mkdir() dist_info_dir.joinpath("METADATA").touch() dist_info_dir.joinpath("WHEEL").write_bytes(b"\xff") with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_metadata(zip_or_dir(tmpdir), dist_info_dir.name) + wheel.wheel_metadata(zip_dir(tmpdir), dist_info_dir.name) assert "error decoding WHEEL" in str(e.value) From 2f92826081e89874d5207bbfe8360ec3dc22065c Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 1 Jan 2020 17:39:06 -0500 Subject: [PATCH 1020/3170] Extract basic wheel parsing into separate function This functions as a guard for the rest of our wheel-handling code, ensuring that we will only get past this point if we have a wheel that we should be able to handle version-wise. We return a tuple instead of bundling up the result in a dedicated type because it's the simplest option. The interface will be easy to update later if the need arises. --- src/pip/_internal/operations/install/wheel.py | 32 +++++++++++++------ 1 file changed, 22 insertions(+), 10 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 9d94227f256..ca78b68bb3a 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -321,16 +321,7 @@ def install_unpacked_wheel( source = wheeldir.rstrip(os.path.sep) + os.path.sep - try: - info_dir = wheel_dist_info_dir(wheel_zip, name) - metadata = wheel_metadata(wheel_zip, info_dir) - version = wheel_version(metadata) - except UnsupportedWheel as e: - raise UnsupportedWheel( - "{} has an invalid wheel, {}".format(name, str(e)) - ) - - check_compatibility(version, name) + info_dir, metadata = parse_wheel(wheel_zip, name) if wheel_root_is_purelib(metadata): lib_dir = scheme.purelib @@ -633,6 +624,27 @@ def install_wheel( ) +def parse_wheel(wheel_zip, name): + # type: (ZipFile, str) -> Tuple[str, Message] + """Extract information from the provided wheel, ensuring it meets basic + standards. + + Returns the name of the .dist-info directory and the parsed WHEEL metadata. + """ + try: + info_dir = wheel_dist_info_dir(wheel_zip, name) + metadata = wheel_metadata(wheel_zip, info_dir) + version = wheel_version(metadata) + except UnsupportedWheel as e: + raise UnsupportedWheel( + "{} has an invalid wheel, {}".format(name, str(e)) + ) + + check_compatibility(version, name) + + return info_dir, metadata + + def wheel_dist_info_dir(source, name): # type: (ZipFile, str) -> str """Returns the name of the contained .dist-info directory. From 2e24041ca516b93e5717b824a5774dd94d6253d9 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 1 Jan 2020 23:00:33 +0800 Subject: [PATCH 1021/3170] Custom optparse.Option type that calls expanduser --- src/pip/_internal/cli/cmdoptions.py | 48 ++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index b3db456e4e3..ba693381e1c 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -130,6 +130,17 @@ def check_dist_restriction(options, check_target=False): ) +def _path_option_check(option, opt, value): + # type: (Option, str, str) -> str + return os.path.expanduser(value) + + +class _PathOption(Option): + TYPES = Option.TYPES + ("path",) + TYPE_CHECKER = Option.TYPE_CHECKER.copy() + TYPE_CHECKER["path"] = _path_option_check + + ########### # options # ########### @@ -217,10 +228,11 @@ def check_dist_restriction(options, check_target=False): ) # type: Callable[..., Option] log = partial( - Option, + _PathOption, "--log", "--log-file", "--local-log", dest="log", metavar="path", + type="path", help="Path to a verbose appending log." ) # type: Callable[..., Option] @@ -291,19 +303,19 @@ def exists_action(): cert = partial( - Option, + _PathOption, '--cert', dest='cert', - type='str', + type='path', metavar='path', help="Path to alternate CA bundle.", ) # type: Callable[..., Option] client_cert = partial( - Option, + _PathOption, '--client-cert', dest='client_cert', - type='str', + type='path', default=None, metavar='path', help="Path to SSL client certificate, a single file containing the " @@ -349,12 +361,13 @@ def extra_index_url(): def find_links(): # type: () -> Option - return Option( + return _PathOption( '-f', '--find-links', dest='find_links', action='append', default=[], metavar='url', + type='path', 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.", @@ -376,12 +389,13 @@ def trusted_host(): def constraints(): # type: () -> Option - return Option( + return _PathOption( '-c', '--constraint', dest='constraints', action='append', default=[], metavar='file', + type='path', help='Constrain versions using the given constraints file. ' 'This option can be used multiple times.' ) @@ -389,12 +403,13 @@ def constraints(): def requirements(): # type: () -> Option - return Option( + return _PathOption( '-r', '--requirement', dest='requirements', action='append', default=[], metavar='file', + type='path', help='Install from the given requirements file. ' 'This option can be used multiple times.' ) @@ -402,12 +417,13 @@ def requirements(): def editable(): # type: () -> Option - return Option( + return _PathOption( '-e', '--editable', dest='editables', action='append', default=[], metavar='path/url', + type='path', help=('Install a project in editable mode (i.e. setuptools ' '"develop mode") from a local project path or a VCS url.'), ) @@ -420,10 +436,10 @@ def _handle_src(option, opt_str, value, parser): src = partial( - Option, + _PathOption, '--src', '--source', '--source-dir', '--source-directory', dest='src_dir', - type='str', + type='path', metavar='dir', default=get_src_prefix(), action='callback', @@ -626,11 +642,12 @@ def prefer_binary(): cache_dir = partial( - Option, + _PathOption, "--cache-dir", dest="cache_dir", default=USER_CACHE_DIR, metavar="dir", + type='path', help="Store the cache data in <dir>." ) # type: Callable[..., Option] @@ -690,10 +707,10 @@ def _handle_build_dir(option, opt, value, parser): build_dir = partial( - Option, + _PathOption, '-b', '--build', '--build-dir', '--build-directory', dest='build_dir', - type='str', + type='path', metavar='dir', action='callback', callback=_handle_build_dir, @@ -874,9 +891,10 @@ def _handle_merge_hash(option, opt_str, value, parser): list_path = partial( - Option, + _PathOption, '--path', dest='path', + type='path', action='append', help='Restrict to the specified installation path for listing ' 'packages (can be used multiple times).' From 5ffc6bd4685dc89c14045c3d9813ebfb02f1f126 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 2 Jan 2020 18:58:42 +0800 Subject: [PATCH 1022/3170] Add tests for global path options --- tests/unit/test_options.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/unit/test_options.py b/tests/unit/test_options.py index 78816ef1aa7..534bc92afc9 100644 --- a/tests/unit/test_options.py +++ b/tests/unit/test_options.py @@ -421,3 +421,25 @@ def test_config_file_options(self, monkeypatch, args, expect): cmd._determine_file(options, need_value=False) else: assert expect == cmd._determine_file(options, need_value=False) + + +class TestOptionsExpandUser(AddFakeCommandMixin): + def test_cache_dir(self): + options, args = main(['--cache-dir', '~/cache/dir', 'fake']) + assert options.cache_dir == os.path.expanduser('~/cache/dir') + + def test_log(self): + options, args = main(['--log', '~/path', 'fake']) + assert options.log == os.path.expanduser('~/path') + + def test_local_log(self): + options, args = main(['--local-log', '~/path', 'fake']) + assert options.log == os.path.expanduser('~/path') + + def test_cert(self): + options, args = main(['--cert', '~/path', 'fake']) + assert options.cert == os.path.expanduser('~/path') + + def test_client_cert(self): + options, args = main(['--client-cert', '~/path', 'fake']) + assert options.client_cert == os.path.expanduser('~/path') From 192e895c3fda4e5f36371f1c73f0216ba84806d7 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 2 Jan 2020 19:00:46 +0800 Subject: [PATCH 1023/3170] News --- news/980.feature | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 news/980.feature diff --git a/news/980.feature b/news/980.feature new file mode 100644 index 00000000000..60a74a90a91 --- /dev/null +++ b/news/980.feature @@ -0,0 +1,2 @@ +Expand ``~`` prefix to user directory in path options, configs, and environment +variables. From c93acfb20b953ee9b971108fc9672081e159b54f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Wed, 1 Jan 2020 14:45:30 +0100 Subject: [PATCH 1024/3170] Test legacy clean not attempt after PEP 517 build failure --- .../pep517_wrapper_buildsys/mybuildsys.py | 3 +++ tests/functional/test_install.py | 1 + tests/functional/test_install_cleanup.py | 16 ++++++++++++++++ 3 files changed, 20 insertions(+) diff --git a/tests/data/packages/pep517_wrapper_buildsys/mybuildsys.py b/tests/data/packages/pep517_wrapper_buildsys/mybuildsys.py index df391e96a2f..e2f920ba3f5 100644 --- a/tests/data/packages/pep517_wrapper_buildsys/mybuildsys.py +++ b/tests/data/packages/pep517_wrapper_buildsys/mybuildsys.py @@ -8,6 +8,9 @@ def build_wheel(*a, **kw): + if os.environ.get("PIP_TEST_FAIL_BUILD_WHEEL"): + raise RuntimeError("Failing build_wheel, as requested.") + # Create the marker file to record that the hook was called with open(os.environ['PIP_TEST_MARKER_FILE'], 'wb'): pass diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 8ae64319d1e..5548d792cd7 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1256,6 +1256,7 @@ def test_cleanup_after_failed_wheel(script, with_wheel): shebang = open(script_py, 'r').readline().strip() assert shebang != '#!python', shebang # OK, assert that we *said* we were cleaning up: + # /!\ if in need to change this, also change test_pep517_no_legacy_cleanup assert "Running setup.py clean for wheelbrokenafter" in str(res), str(res) diff --git a/tests/functional/test_install_cleanup.py b/tests/functional/test_install_cleanup.py index dc87bc3f76a..c6464e3ee0b 100644 --- a/tests/functional/test_install_cleanup.py +++ b/tests/functional/test_install_cleanup.py @@ -136,3 +136,19 @@ def test_cleanup_prevented_upon_build_dir_exception(script, data): assert result.returncode == PREVIOUS_BUILD_DIR_ERROR, str(result) assert "pip can't proceed" in result.stderr, str(result) assert exists(build_simple), str(result) + + +@pytest.mark.network +def test_pep517_no_legacy_cleanup(script, data, with_wheel): + """Test a PEP 517 failed build does not attempt a legacy cleanup""" + to_install = data.packages.joinpath('pep517_wrapper_buildsys') + script.environ["PIP_TEST_FAIL_BUILD_WHEEL"] = "1" + res = script.pip( + 'install', '-f', data.find_links, to_install, + expect_error=True + ) + # Must not have built the package + expected = "Failed building wheel for pep517-wrapper-buildsys" + assert expected in str(res) + # Must not have attempted legacy cleanup + assert "setup.py clean" not in str(res) From 8d1d20de8ce719350a6028e93a41e6ccc829634e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Fri, 13 Dec 2019 22:33:11 +0100 Subject: [PATCH 1025/3170] Do not attempt setup.py clean for failed pep517 builds Fixes #6642 --- news/6642.bugfix | 2 ++ src/pip/_internal/wheel_builder.py | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 news/6642.bugfix diff --git a/news/6642.bugfix b/news/6642.bugfix new file mode 100644 index 00000000000..470572ad63a --- /dev/null +++ b/news/6642.bugfix @@ -0,0 +1,2 @@ +Do not attempt to run ``setup.py clean`` after a ``pep517`` build error, +since a ``setup.py`` may not exist in that case. diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index b992bd676ba..35e4f9eff0c 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -231,11 +231,12 @@ def _build_one_inside_env( req.name, e, ) # Ignore return, we can't do anything else useful. - _clean_one(req, global_options) + if not req.use_pep517: + _clean_one_legacy(req, global_options) return None -def _clean_one(req, global_options): +def _clean_one_legacy(req, global_options): # type: (InstallRequirement, List[str]) -> bool clean_args = make_setuptools_clean_args( req.setup_py_path, From 9c04298d5bde5aab97bdeded1f71f4dd8f61259e Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 2 Jan 2020 20:46:14 +0800 Subject: [PATCH 1026/3170] Revert expanduser on options that accept URL --- src/pip/_internal/cli/cmdoptions.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index ba693381e1c..8502099dd31 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -361,13 +361,12 @@ def extra_index_url(): def find_links(): # type: () -> Option - return _PathOption( + return Option( '-f', '--find-links', dest='find_links', action='append', default=[], metavar='url', - type='path', 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.", @@ -389,13 +388,12 @@ def trusted_host(): def constraints(): # type: () -> Option - return _PathOption( + return Option( '-c', '--constraint', dest='constraints', action='append', default=[], metavar='file', - type='path', help='Constrain versions using the given constraints file. ' 'This option can be used multiple times.' ) @@ -403,13 +401,12 @@ def constraints(): def requirements(): # type: () -> Option - return _PathOption( + return Option( '-r', '--requirement', dest='requirements', action='append', default=[], metavar='file', - type='path', help='Install from the given requirements file. ' 'This option can be used multiple times.' ) @@ -417,13 +414,12 @@ def requirements(): def editable(): # type: () -> Option - return _PathOption( + return Option( '-e', '--editable', dest='editables', action='append', default=[], metavar='path/url', - type='path', help=('Install a project in editable mode (i.e. setuptools ' '"develop mode") from a local project path or a VCS url.'), ) From cf071dee55c748987e5b3133b1c0a12a7b67fd76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Thu, 2 Jan 2020 15:42:10 +0100 Subject: [PATCH 1027/3170] Deprecate git+git@ form of VCS url --- docs/html/reference/pip_install.rst | 1 - news/7543.removal | 4 ++++ src/pip/_internal/req/req_install.py | 13 +++++++++++++ 3 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 news/7543.removal diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index 2b627cbee64..cb32e3baeb8 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -409,7 +409,6 @@ Here are the supported forms:: [-e] git+ssh://git.example.com/MyProject#egg=MyProject [-e] git+git://git.example.com/MyProject#egg=MyProject [-e] git+file:///home/user/projects/MyProject#egg=MyProject - -e git+git@git.example.com:MyProject#egg=MyProject Passing a branch name, a commit hash, a tag name or a git ref is possible like so:: diff --git a/news/7543.removal b/news/7543.removal new file mode 100644 index 00000000000..eab7016abc0 --- /dev/null +++ b/news/7543.removal @@ -0,0 +1,4 @@ +Support for the ``git+git@`` form of VCS requirement is being deprecated and +will be removed in pip 21.0. Switch to ``git+https://`` or +``git+ssh://``. ``git+git://`` also works but its use is discouraged as it is +insecure. diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 80f40307290..e46b1ca294b 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -30,6 +30,7 @@ from pip._internal.operations.install.wheel import install_wheel from pip._internal.pyproject import load_pyproject_toml, make_pyproject_path from pip._internal.req.req_uninstall import UninstallPathSet +from pip._internal.utils.deprecation import deprecated from pip._internal.utils.hashes import Hashes from pip._internal.utils.logging import indent_log from pip._internal.utils.marker_files import ( @@ -633,6 +634,18 @@ def update_editable(self, obtain=True): vc_type, url = self.link.url.split('+', 1) vcs_backend = vcs.get_backend(vc_type) if vcs_backend: + if not self.link.is_vcs: + reason = ( + "This form of VCS requirement is being deprecated: {}." + ).format( + self.link.url + ) + replacement = None + if self.link.url.startswith("git+git@"): + replacement = ( + "git+https:// or git+ssh://" + ) + deprecated(reason, replacement, gone_in="21.0") hidden_url = hide_url(self.link.url) if obtain: vcs_backend.obtain(self.source_dir, url=hidden_url) From 7af93711764a1a6e6da37ca68a9bca205c53ec94 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 2 Jan 2020 17:21:22 -0500 Subject: [PATCH 1028/3170] Remove redundant expect_error=False in tests This is the default, so there is no need to specify it explicitly. --- tests/functional/test_install.py | 21 ++++++++++----------- tests/functional/test_install_cleanup.py | 2 +- tests/functional/test_install_compat.py | 2 +- tests/functional/test_install_index.py | 3 +-- tests/functional/test_install_reqs.py | 15 +++++++-------- tests/functional/test_install_user.py | 1 - tests/functional/test_install_wheel.py | 20 +++++--------------- tests/functional/test_uninstall.py | 6 +++--- tests/functional/test_uninstall_user.py | 2 +- 9 files changed, 29 insertions(+), 43 deletions(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 5548d792cd7..7b2d0b9f92a 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -398,7 +398,7 @@ def test_basic_install_from_local_directory(script, data): Test installing from a local directory. """ to_install = data.packages.joinpath("FSPkg") - result = script.pip('install', to_install, expect_error=False) + result = script.pip('install', to_install) fspkg_folder = script.site_packages / 'fspkg' egg_info_folder = ( script.site_packages / 'FSPkg-0.1.dev0-py%s.egg-info' % pyversion @@ -454,7 +454,7 @@ def test_install_quiet(script, data): # https://github.com/pypa/pip/issues/3418 # https://github.com/docker-library/python/issues/83 to_install = data.packages.joinpath("FSPkg") - result = script.pip('install', '-qqq', to_install, expect_error=False) + result = script.pip('install', '-qqq', to_install) assert result.stdout == "" assert result.stderr == "" @@ -476,7 +476,7 @@ def test_hashed_install_success(script, data, tmpdir): '{simple} --hash=sha256:393043e672415891885c9a2a0929b1af95fb866d6c' 'a016b42d2e6ce53619b653'.format(simple=file_url), tmpdir) as reqs_file: - script.pip_install_local('-r', reqs_file.resolve(), expect_error=False) + script.pip_install_local('-r', reqs_file.resolve()) def test_hashed_install_failure(script, tmpdir): @@ -537,7 +537,7 @@ def test_install_from_local_directory_with_symlinks_to_directories( Test installing from a local directory containing symlinks to directories. """ to_install = data.packages.joinpath("symlinks") - result = script.pip('install', to_install, expect_error=False) + result = script.pip('install', to_install) pkg_folder = script.site_packages / 'symlinks' egg_info_folder = ( script.site_packages / 'symlinks-0.1.dev0-py%s.egg-info' % pyversion @@ -563,7 +563,7 @@ def test_install_from_local_directory_with_socket_file(script, data, tmpdir): socket_file_path = os.path.join(to_install, "example") make_socket_file(socket_file_path) - result = script.pip("install", "--verbose", to_install, expect_error=False) + result = script.pip("install", "--verbose", to_install) assert package_folder in result.files_created, str(result.stdout) assert egg_info_file in result.files_created, str(result) assert str(socket_file_path) in result.stderr @@ -650,7 +650,7 @@ def test_install_curdir(script, data): egg_info = join(run_from, "FSPkg.egg-info") if os.path.isdir(egg_info): rmtree(egg_info) - result = script.pip('install', curdir, cwd=run_from, expect_error=False) + result = script.pip('install', curdir, cwd=run_from) fspkg_folder = script.site_packages / 'fspkg' egg_info_folder = ( script.site_packages / 'FSPkg-0.1.dev0-py%s.egg-info' % pyversion @@ -664,7 +664,7 @@ def test_install_pardir(script, data): Test installing parent directory ('..'). """ run_from = data.packages.joinpath("FSPkg", "fspkg") - result = script.pip('install', pardir, cwd=run_from, expect_error=False) + result = script.pip('install', pardir, cwd=run_from) fspkg_folder = script.site_packages / 'fspkg' egg_info_folder = ( script.site_packages / 'FSPkg-0.1.dev0-py%s.egg-info' % pyversion @@ -1237,7 +1237,7 @@ def test_install_log(script, data, tmpdir): def test_install_topological_sort(script, data): args = ['install', 'TopoRequires4', '--no-index', '-f', data.packages] - res = str(script.pip(*args, expect_error=False)) + res = str(script.pip(*args)) order1 = 'TopoRequires, TopoRequires2, TopoRequires3, TopoRequires4' order2 = 'TopoRequires, TopoRequires3, TopoRequires2, TopoRequires4' assert order1 in res or order2 in res, res @@ -1407,8 +1407,7 @@ def test_double_install(script): """ Test double install passing with two same version requirements """ - result = script.pip('install', 'pip', 'pip', - expect_error=False) + result = script.pip('install', 'pip', 'pip') msg = "Double requirement given: pip (already in pip, name='pip')" assert msg not in result.stderr @@ -1564,7 +1563,7 @@ def test_installed_files_recorded_in_deterministic_order(script, data): order, to make installs reproducible. """ to_install = data.packages.joinpath("FSPkg") - result = script.pip('install', to_install, expect_error=False) + result = script.pip('install', to_install) fspkg_folder = script.site_packages / 'fspkg' egg_info = 'FSPkg-0.1.dev0-py%s.egg-info' % pyversion installed_files_path = ( diff --git a/tests/functional/test_install_cleanup.py b/tests/functional/test_install_cleanup.py index c6464e3ee0b..d9669f74335 100644 --- a/tests/functional/test_install_cleanup.py +++ b/tests/functional/test_install_cleanup.py @@ -61,7 +61,7 @@ def test_cleanup_after_install_from_local_directory(script, data): Test clean up after installing from a local directory. """ to_install = data.packages.joinpath("FSPkg") - script.pip('install', to_install, expect_error=False) + script.pip('install', to_install) build = script.venv_path / 'build' src = script.venv_path / 'src' assert not exists(build), "unexpected build/ dir exists: %s" % build diff --git a/tests/functional/test_install_compat.py b/tests/functional/test_install_compat.py index 3c77b3e046f..b957934ef48 100644 --- a/tests/functional/test_install_compat.py +++ b/tests/functional/test_install_compat.py @@ -55,4 +55,4 @@ def test_setup_py_with_dos_line_endings(script, data): Refs https://github.com/pypa/pip/issues/237 """ to_install = data.packages.joinpath("LineEndings") - script.pip('install', to_install, expect_error=False) + script.pip('install', to_install) diff --git a/tests/functional/test_install_index.py b/tests/functional/test_install_index.py index 31e748138a9..5a31db8b482 100644 --- a/tests/functional/test_install_index.py +++ b/tests/functional/test_install_index.py @@ -63,8 +63,7 @@ def test_file_index_url_quoting(script, data): """ index_url = data.index_url(urllib_parse.quote("in dex")) result = script.pip( - 'install', '-vvv', '--index-url', index_url, 'simple', - expect_error=False, + 'install', '-vvv', '--index-url', index_url, 'simple' ) assert (script.site_packages / 'simple') in result.files_created, ( str(result.stdout) diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index 55c51664366..e478d594cef 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -179,9 +179,7 @@ def test_respect_order_in_requirements_file(script, data): def test_install_local_editable_with_extras(script, data): to_install = data.packages.joinpath("LocalExtras") res = script.pip_install_local( - '-e', to_install + '[bar]', - expect_error=False, - expect_stderr=True, + '-e', to_install + '[bar]', allow_stderr_warning=True ) assert script.site_packages / 'easy-install.pth' in res.files_updated, ( str(res) @@ -373,17 +371,19 @@ def test_double_install_spurious_hash_mismatch( # Install a package (and build its wheel): result = script.pip_install_local( '--find-links', data.find_links, - '-r', reqs_file.resolve(), expect_error=False) + '-r', reqs_file.resolve(), + ) assert 'Successfully installed simple-1.0' in str(result) # Uninstall it: - script.pip('uninstall', '-y', 'simple', expect_error=False) + script.pip('uninstall', '-y', 'simple') # Then install it again. We should not hit a hash mismatch, and the # package should install happily. result = script.pip_install_local( '--find-links', data.find_links, - '-r', reqs_file.resolve(), expect_error=False) + '-r', reqs_file.resolve(), + ) assert 'Successfully installed simple-1.0' in str(result) @@ -490,8 +490,7 @@ def test_install_unsupported_wheel_link_with_marker(script): ) ) result = script.pip( - 'install', '-r', script.scratch_path / 'with-marker.txt', - expect_error=False, + 'install', '-r', script.scratch_path / 'with-marker.txt' ) assert ("Ignoring asdf: markers 'sys_platform == \"xyz\"' don't match " diff --git a/tests/functional/test_install_user.py b/tests/functional/test_install_user.py index 03725fc3229..2cc91f56c75 100644 --- a/tests/functional/test_install_user.py +++ b/tests/functional/test_install_user.py @@ -67,7 +67,6 @@ def test_install_from_current_directory_into_usersite( result = script.pip( 'install', '-vvv', '--user', curdir, cwd=run_from, - expect_error=False, ) fspkg_folder = script.user_site / 'fspkg' diff --git a/tests/functional/test_install_wheel.py b/tests/functional/test_install_wheel.py index e11664aa8f1..14106a90cc2 100644 --- a/tests/functional/test_install_wheel.py +++ b/tests/functional/test_install_wheel.py @@ -23,8 +23,7 @@ def test_install_from_future_wheel_version(script, data): package = data.packages.joinpath("futurewheel-1.9-py2.py3-none-any.whl") result = script.pip( - 'install', package, '--no-index', expect_error=False, - expect_stderr=True, + 'install', package, '--no-index', expect_stderr=True ) result.assert_installed('futurewheel', without_egg_link=True, editable=False) @@ -49,7 +48,6 @@ def test_basic_install_from_wheel(script, data): result = script.pip( 'install', 'has.script==1.0', '--no-index', '--find-links=' + data.find_links, - expect_error=False, ) dist_info_folder = script.site_packages / 'has.script-1.0.dist-info' assert dist_info_folder in result.files_created, (dist_info_folder, @@ -66,7 +64,6 @@ def test_basic_install_from_wheel_with_extras(script, data): result = script.pip( 'install', 'complex-dist[simple]', '--no-index', '--find-links=' + data.find_links, - expect_error=False, ) dist_info_folder = script.site_packages / 'complex_dist-0.1.dist-info' assert dist_info_folder in result.files_created, (dist_info_folder, @@ -83,7 +80,7 @@ def test_basic_install_from_wheel_file(script, data): Test installing directly from a wheel file. """ package = data.packages.joinpath("simple.dist-0.1-py2.py3-none-any.whl") - result = script.pip('install', package, '--no-index', expect_error=False) + result = script.pip('install', package, '--no-index') dist_info_folder = script.site_packages / 'simple.dist-0.1.dist-info' assert dist_info_folder in result.files_created, (dist_info_folder, result.files_created, @@ -106,7 +103,7 @@ def test_install_from_wheel_with_headers(script, data): Test installing from a wheel file with headers """ package = data.packages.joinpath("headers.dist-0.1-py2.py3-none-any.whl") - result = script.pip('install', package, '--no-index', expect_error=False) + result = script.pip('install', package, '--no-index') dist_info_folder = script.site_packages / 'headers.dist-0.1.dist-info' assert dist_info_folder in result.files_created, (dist_info_folder, result.files_created, @@ -152,8 +149,7 @@ def test_install_wheel_with_target_and_data_files(script, data, with_wheel): ) result = script.pip('install', package, '-t', target_dir, - '--no-index', - expect_error=False) + '--no-index') assert (Path('scratch') / 'prjwithdatafile' / 'packages1' / 'README.txt' in result.files_created), str(result) @@ -257,7 +253,6 @@ def test_install_from_wheel_gen_entrypoint(script, data): result = script.pip( 'install', 'script.wheel1a==0.1', '--no-index', '--find-links=' + data.find_links, - expect_error=False, ) if os.name == 'nt': wrapper_file = script.bin / 't1.exe' @@ -276,7 +271,6 @@ def test_install_from_wheel_gen_uppercase_entrypoint(script, data): result = script.pip( 'install', 'console-scripts-uppercase==1.0', '--no-index', '--find-links=' + data.find_links, - expect_error=False, ) if os.name == 'nt': # Case probably doesn't make any difference on NT @@ -296,7 +290,6 @@ def test_install_from_wheel_with_legacy(script, data): result = script.pip( 'install', 'script.wheel2a==0.1', '--no-index', '--find-links=' + data.find_links, - expect_error=False, ) legacy_file1 = script.bin / 'testscript1.bat' @@ -314,7 +307,6 @@ def test_install_from_wheel_no_setuptools_entrypoint(script, data): result = script.pip( 'install', 'script.wheel1==0.1', '--no-index', '--find-links=' + data.find_links, - expect_error=False, ) if os.name == 'nt': wrapper_file = script.bin / 't1.exe' @@ -339,7 +331,6 @@ def test_skipping_setuptools_doesnt_skip_legacy(script, data): result = script.pip( 'install', 'script.wheel2==0.1', '--no-index', '--find-links=' + data.find_links, - expect_error=False, ) legacy_file1 = script.bin / 'testscript1.bat' @@ -358,7 +349,6 @@ def test_install_from_wheel_gui_entrypoint(script, data): result = script.pip( 'install', 'script.wheel3==0.1', '--no-index', '--find-links=' + data.find_links, - expect_error=False, ) if os.name == 'nt': wrapper_file = script.bin / 't1.exe' @@ -414,7 +404,7 @@ def test_install_from_wheel_uninstalls_old_version(script, data): package = data.packages.joinpath("simplewheel-1.0-py2.py3-none-any.whl") result = script.pip('install', package, '--no-index') package = data.packages.joinpath("simplewheel-2.0-py2.py3-none-any.whl") - result = script.pip('install', package, '--no-index', expect_error=False) + result = script.pip('install', package, '--no-index') dist_info_folder = script.site_packages / 'simplewheel-2.0.dist-info' assert dist_info_folder in result.files_created dist_info_folder = script.site_packages / 'simplewheel-1.0.dist-info' diff --git a/tests/functional/test_uninstall.py b/tests/functional/test_uninstall.py index 42f84fa1585..15a35649d40 100644 --- a/tests/functional/test_uninstall.py +++ b/tests/functional/test_uninstall.py @@ -170,11 +170,11 @@ def test_uninstall_overlapping_package(script, data): parent_pkg = data.packages.joinpath("parent-0.1.tar.gz") child_pkg = data.packages.joinpath("child-0.1.tar.gz") - result1 = script.pip('install', parent_pkg, expect_error=False) + result1 = script.pip('install', parent_pkg) assert join(script.site_packages, 'parent') in result1.files_created, ( sorted(result1.files_created.keys()) ) - result2 = script.pip('install', child_pkg, expect_error=False) + result2 = script.pip('install', child_pkg) assert join(script.site_packages, 'child') in result2.files_created, ( sorted(result2.files_created.keys()) ) @@ -184,7 +184,7 @@ def test_uninstall_overlapping_package(script, data): # The import forces the generation of __pycache__ if the version of python # supports it script.run('python', '-c', "import parent.plugins.child_plugin, child") - result3 = script.pip('uninstall', '-y', 'child', expect_error=False) + result3 = script.pip('uninstall', '-y', 'child') assert join(script.site_packages, 'child') in result3.files_deleted, ( sorted(result3.files_created.keys()) ) diff --git a/tests/functional/test_uninstall_user.py b/tests/functional/test_uninstall_user.py index 58079a293a8..ed277739a1f 100644 --- a/tests/functional/test_uninstall_user.py +++ b/tests/functional/test_uninstall_user.py @@ -57,7 +57,7 @@ def test_uninstall_editable_from_usersite(self, script, data): # install to_install = data.packages.joinpath("FSPkg") result1 = script.pip( - 'install', '--user', '-e', to_install, expect_error=False, + 'install', '--user', '-e', to_install ) egg_link = script.user_site / 'FSPkg.egg-link' assert egg_link in result1.files_created, str(result1.stdout) From 4e5d854456f1e9046a41e9c7f879c4d87929172a Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 3 Jan 2020 17:58:40 -0500 Subject: [PATCH 1029/3170] Normalize --find-links argument format in wheel install tests --- tests/functional/test_install_wheel.py | 28 +++++++++++++------------- 1 file changed, 14 insertions(+), 14 deletions(-) diff --git a/tests/functional/test_install_wheel.py b/tests/functional/test_install_wheel.py index 14106a90cc2..25437475943 100644 --- a/tests/functional/test_install_wheel.py +++ b/tests/functional/test_install_wheel.py @@ -47,7 +47,7 @@ def test_basic_install_from_wheel(script, data): """ result = script.pip( 'install', 'has.script==1.0', '--no-index', - '--find-links=' + data.find_links, + '--find-links', data.find_links, ) dist_info_folder = script.site_packages / 'has.script-1.0.dist-info' assert dist_info_folder in result.files_created, (dist_info_folder, @@ -63,7 +63,7 @@ def test_basic_install_from_wheel_with_extras(script, data): """ result = script.pip( 'install', 'complex-dist[simple]', '--no-index', - '--find-links=' + data.find_links, + '--find-links', data.find_links, ) dist_info_folder = script.site_packages / 'complex_dist-0.1.dist-info' assert dist_info_folder in result.files_created, (dist_info_folder, @@ -117,7 +117,7 @@ def test_install_wheel_with_target(script, data, with_wheel): target_dir = script.scratch_path / 'target' result = script.pip( 'install', 'simple.dist==0.1', '-t', target_dir, - '--no-index', '--find-links=' + data.find_links, + '--no-index', '--find-links', data.find_links, ) assert Path('scratch') / 'target' / 'simpledist' in result.files_created, ( str(result) @@ -166,7 +166,7 @@ def test_install_wheel_with_root(script, data): root_dir = script.scratch_path / 'root' result = script.pip( 'install', 'simple.dist==0.1', '--root', root_dir, - '--no-index', '--find-links=' + data.find_links, + '--no-index', '--find-links', data.find_links, ) assert Path('scratch') / 'root' in result.files_created @@ -178,7 +178,7 @@ def test_install_wheel_with_prefix(script, data): prefix_dir = script.scratch_path / 'prefix' result = script.pip( 'install', 'simple.dist==0.1', '--prefix', prefix_dir, - '--no-index', '--find-links=' + data.find_links, + '--no-index', '--find-links', data.find_links, ) lib = distutils.sysconfig.get_python_lib(prefix=Path('scratch') / 'prefix') assert lib in result.files_created, str(result) @@ -238,7 +238,7 @@ def test_install_user_wheel(script, data, with_wheel): """ result = script.pip( 'install', 'has.script==1.0', '--user', '--no-index', - '--find-links=' + data.find_links, + '--find-links', data.find_links, ) egg_info_folder = script.user_site / 'has.script-1.0.dist-info' assert egg_info_folder in result.files_created, str(result) @@ -252,7 +252,7 @@ def test_install_from_wheel_gen_entrypoint(script, data): """ result = script.pip( 'install', 'script.wheel1a==0.1', '--no-index', - '--find-links=' + data.find_links, + '--find-links', data.find_links, ) if os.name == 'nt': wrapper_file = script.bin / 't1.exe' @@ -270,7 +270,7 @@ def test_install_from_wheel_gen_uppercase_entrypoint(script, data): """ result = script.pip( 'install', 'console-scripts-uppercase==1.0', '--no-index', - '--find-links=' + data.find_links, + '--find-links', data.find_links, ) if os.name == 'nt': # Case probably doesn't make any difference on NT @@ -289,7 +289,7 @@ def test_install_from_wheel_with_legacy(script, data): """ result = script.pip( 'install', 'script.wheel2a==0.1', '--no-index', - '--find-links=' + data.find_links, + '--find-links', data.find_links, ) legacy_file1 = script.bin / 'testscript1.bat' @@ -306,7 +306,7 @@ def test_install_from_wheel_no_setuptools_entrypoint(script, data): """ result = script.pip( 'install', 'script.wheel1==0.1', '--no-index', - '--find-links=' + data.find_links, + '--find-links', data.find_links, ) if os.name == 'nt': wrapper_file = script.bin / 't1.exe' @@ -330,7 +330,7 @@ def test_skipping_setuptools_doesnt_skip_legacy(script, data): """ result = script.pip( 'install', 'script.wheel2==0.1', '--no-index', - '--find-links=' + data.find_links, + '--find-links', data.find_links, ) legacy_file1 = script.bin / 'testscript1.bat' @@ -348,7 +348,7 @@ def test_install_from_wheel_gui_entrypoint(script, data): """ result = script.pip( 'install', 'script.wheel3==0.1', '--no-index', - '--find-links=' + data.find_links, + '--find-links', data.find_links, ) if os.name == 'nt': wrapper_file = script.bin / 't1.exe' @@ -363,7 +363,7 @@ def test_wheel_compiles_pyc(script, data): """ script.pip( "install", "--compile", "simple.dist==0.1", "--no-index", - "--find-links=" + data.find_links + "--find-links", data.find_links ) # There are many locations for the __init__.pyc file so attempt to find # any of them @@ -384,7 +384,7 @@ def test_wheel_no_compiles_pyc(script, data): """ script.pip( "install", "--no-compile", "simple.dist==0.1", "--no-index", - "--find-links=" + data.find_links + "--find-links", data.find_links ) # There are many locations for the __init__.pyc file so attempt to find # any of them From d468da2796030376fafe6a2be5256aa1cd7dcfc8 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 3 Jan 2020 18:05:47 -0500 Subject: [PATCH 1030/3170] Explicitly copy required packages to tmpdir for wheel install tests Any test using --find-links data.packages is potentially using several packages. By copying specifically the packages we need, we can more easily see the packages that each test depends on and avoid using the `data` fixture. --- tests/functional/test_install_wheel.py | 96 +++++++++++++++++--------- 1 file changed, 64 insertions(+), 32 deletions(-) diff --git a/tests/functional/test_install_wheel.py b/tests/functional/test_install_wheel.py index 25437475943..f62e79f286a 100644 --- a/tests/functional/test_install_wheel.py +++ b/tests/functional/test_install_wheel.py @@ -41,13 +41,14 @@ def test_install_from_broken_wheel(script, data): editable=False) -def test_basic_install_from_wheel(script, data): +def test_basic_install_from_wheel(script, data, tmpdir): """ Test installing from a wheel (that has a script) """ + shutil.copy(data.packages / "has.script-1.0-py2.py3-none-any.whl", tmpdir) result = script.pip( 'install', 'has.script==1.0', '--no-index', - '--find-links', data.find_links, + '--find-links', tmpdir, ) dist_info_folder = script.site_packages / 'has.script-1.0.dist-info' assert dist_info_folder in result.files_created, (dist_info_folder, @@ -57,13 +58,17 @@ def test_basic_install_from_wheel(script, data): assert script_file in result.files_created -def test_basic_install_from_wheel_with_extras(script, data): +def test_basic_install_from_wheel_with_extras(script, data, tmpdir): """ Test installing from a wheel with extras. """ + shutil.copy( + data.packages / "complex_dist-0.1-py2.py3-none-any.whl", tmpdir + ) + shutil.copy(data.packages / "simple.dist-0.1-py2.py3-none-any.whl", tmpdir) result = script.pip( 'install', 'complex-dist[simple]', '--no-index', - '--find-links', data.find_links, + '--find-links', tmpdir, ) dist_info_folder = script.site_packages / 'complex_dist-0.1.dist-info' assert dist_info_folder in result.files_created, (dist_info_folder, @@ -110,14 +115,15 @@ def test_install_from_wheel_with_headers(script, data): result.stdout) -def test_install_wheel_with_target(script, data, with_wheel): +def test_install_wheel_with_target(script, data, with_wheel, tmpdir): """ Test installing a wheel using pip install --target """ + shutil.copy(data.packages / "simple.dist-0.1-py2.py3-none-any.whl", tmpdir) target_dir = script.scratch_path / 'target' result = script.pip( 'install', 'simple.dist==0.1', '-t', target_dir, - '--no-index', '--find-links', data.find_links, + '--no-index', '--find-links', tmpdir, ) assert Path('scratch') / 'target' / 'simpledist' in result.files_created, ( str(result) @@ -159,32 +165,34 @@ def test_install_wheel_with_target_and_data_files(script, data, with_wheel): not in result.files_created), str(result) -def test_install_wheel_with_root(script, data): +def test_install_wheel_with_root(script, data, tmpdir): """ Test installing a wheel using pip install --root """ root_dir = script.scratch_path / 'root' + shutil.copy(data.packages / "simple.dist-0.1-py2.py3-none-any.whl", tmpdir) result = script.pip( 'install', 'simple.dist==0.1', '--root', root_dir, - '--no-index', '--find-links', data.find_links, + '--no-index', '--find-links', tmpdir, ) assert Path('scratch') / 'root' in result.files_created -def test_install_wheel_with_prefix(script, data): +def test_install_wheel_with_prefix(script, data, tmpdir): """ Test installing a wheel using pip install --prefix """ prefix_dir = script.scratch_path / 'prefix' + shutil.copy(data.packages / "simple.dist-0.1-py2.py3-none-any.whl", tmpdir) result = script.pip( 'install', 'simple.dist==0.1', '--prefix', prefix_dir, - '--no-index', '--find-links', data.find_links, + '--no-index', '--find-links', tmpdir, ) lib = distutils.sysconfig.get_python_lib(prefix=Path('scratch') / 'prefix') assert lib in result.files_created, str(result) -def test_install_from_wheel_installs_deps(script, data): +def test_install_from_wheel_installs_deps(script, data, tmpdir): """ Test can install dependencies of wheels """ @@ -192,13 +200,14 @@ def test_install_from_wheel_installs_deps(script, data): package = data.packages.joinpath( "requires_source-1.0-py2.py3-none-any.whl" ) + shutil.copy(data.packages / "source-1.0.tar.gz", tmpdir) result = script.pip( - 'install', '--no-index', '--find-links', data.find_links, package, + 'install', '--no-index', '--find-links', tmpdir, package, ) result.assert_installed('source', editable=False) -def test_install_from_wheel_no_deps(script, data): +def test_install_from_wheel_no_deps(script, data, tmpdir): """ Test --no-deps works with wheel installs """ @@ -206,8 +215,9 @@ def test_install_from_wheel_no_deps(script, data): package = data.packages.joinpath( "requires_source-1.0-py2.py3-none-any.whl" ) + shutil.copy(data.packages / "source-1.0.tar.gz", tmpdir) result = script.pip( - 'install', '--no-index', '--find-links', data.find_links, '--no-deps', + 'install', '--no-index', '--find-links', tmpdir, '--no-deps', package, ) pkg_folder = script.site_packages / 'source' @@ -232,13 +242,14 @@ def test_wheel_record_lines_in_deterministic_order(script, data): @pytest.mark.incompatible_with_test_venv -def test_install_user_wheel(script, data, with_wheel): +def test_install_user_wheel(script, data, with_wheel, tmpdir): """ Test user install from wheel (that has a script) """ + shutil.copy(data.packages / "has.script-1.0-py2.py3-none-any.whl", tmpdir) result = script.pip( 'install', 'has.script==1.0', '--user', '--no-index', - '--find-links', data.find_links, + '--find-links', tmpdir, ) egg_info_folder = script.user_site / 'has.script-1.0.dist-info' assert egg_info_folder in result.files_created, str(result) @@ -246,13 +257,16 @@ def test_install_user_wheel(script, data, with_wheel): assert script_file in result.files_created, str(result) -def test_install_from_wheel_gen_entrypoint(script, data): +def test_install_from_wheel_gen_entrypoint(script, data, tmpdir): """ Test installing scripts (entry points are generated) """ + shutil.copy( + data.packages / "script.wheel1a-0.1-py2.py3-none-any.whl", tmpdir + ) result = script.pip( 'install', 'script.wheel1a==0.1', '--no-index', - '--find-links', data.find_links, + '--find-links', tmpdir, ) if os.name == 'nt': wrapper_file = script.bin / 't1.exe' @@ -264,13 +278,17 @@ def test_install_from_wheel_gen_entrypoint(script, data): assert bool(os.access(script.base_path / wrapper_file, os.X_OK)) -def test_install_from_wheel_gen_uppercase_entrypoint(script, data): +def test_install_from_wheel_gen_uppercase_entrypoint(script, data, tmpdir): """ Test installing scripts with uppercase letters in entry point names """ + shutil.copy( + data.packages / "console_scripts_uppercase-1.0-py2.py3-none-any.whl", + tmpdir, + ) result = script.pip( 'install', 'console-scripts-uppercase==1.0', '--no-index', - '--find-links', data.find_links, + '--find-links', tmpdir, ) if os.name == 'nt': # Case probably doesn't make any difference on NT @@ -283,13 +301,16 @@ def test_install_from_wheel_gen_uppercase_entrypoint(script, data): assert bool(os.access(script.base_path / wrapper_file, os.X_OK)) -def test_install_from_wheel_with_legacy(script, data): +def test_install_from_wheel_with_legacy(script, data, tmpdir): """ Test installing scripts (legacy scripts are preserved) """ + shutil.copy( + data.packages / "script.wheel2a-0.1-py2.py3-none-any.whl", tmpdir + ) result = script.pip( 'install', 'script.wheel2a==0.1', '--no-index', - '--find-links', data.find_links, + '--find-links', tmpdir, ) legacy_file1 = script.bin / 'testscript1.bat' @@ -299,14 +320,17 @@ def test_install_from_wheel_with_legacy(script, data): assert legacy_file2 in result.files_created -def test_install_from_wheel_no_setuptools_entrypoint(script, data): +def test_install_from_wheel_no_setuptools_entrypoint(script, data, tmpdir): """ Test that when we generate scripts, any existing setuptools wrappers in the wheel are skipped. """ + shutil.copy( + data.packages / "script.wheel1-0.1-py2.py3-none-any.whl", tmpdir + ) result = script.pip( 'install', 'script.wheel1==0.1', '--no-index', - '--find-links', data.find_links, + '--find-links', tmpdir, ) if os.name == 'nt': wrapper_file = script.bin / 't1.exe' @@ -323,14 +347,17 @@ def test_install_from_wheel_no_setuptools_entrypoint(script, data): assert wrapper_helper not in result.files_created -def test_skipping_setuptools_doesnt_skip_legacy(script, data): +def test_skipping_setuptools_doesnt_skip_legacy(script, data, tmpdir): """ Test installing scripts (legacy scripts are preserved even when we skip setuptools wrappers) """ + shutil.copy( + data.packages / "script.wheel2-0.1-py2.py3-none-any.whl", tmpdir + ) result = script.pip( 'install', 'script.wheel2==0.1', '--no-index', - '--find-links', data.find_links, + '--find-links', tmpdir, ) legacy_file1 = script.bin / 'testscript1.bat' @@ -342,13 +369,16 @@ def test_skipping_setuptools_doesnt_skip_legacy(script, data): assert wrapper_helper not in result.files_created -def test_install_from_wheel_gui_entrypoint(script, data): +def test_install_from_wheel_gui_entrypoint(script, data, tmpdir): """ Test installing scripts (gui entry points are generated) """ + shutil.copy( + data.packages / "script.wheel3-0.1-py2.py3-none-any.whl", tmpdir + ) result = script.pip( 'install', 'script.wheel3==0.1', '--no-index', - '--find-links', data.find_links, + '--find-links', tmpdir, ) if os.name == 'nt': wrapper_file = script.bin / 't1.exe' @@ -357,13 +387,14 @@ def test_install_from_wheel_gui_entrypoint(script, data): assert wrapper_file in result.files_created -def test_wheel_compiles_pyc(script, data): +def test_wheel_compiles_pyc(script, data, tmpdir): """ Test installing from wheel with --compile on """ + shutil.copy(data.packages / "simple.dist-0.1-py2.py3-none-any.whl", tmpdir) script.pip( "install", "--compile", "simple.dist==0.1", "--no-index", - "--find-links", data.find_links + "--find-links", tmpdir, ) # There are many locations for the __init__.pyc file so attempt to find # any of them @@ -378,13 +409,14 @@ def test_wheel_compiles_pyc(script, data): assert any(exists) -def test_wheel_no_compiles_pyc(script, data): +def test_wheel_no_compiles_pyc(script, data, tmpdir): """ Test installing from wheel with --compile on """ + shutil.copy(data.packages / "simple.dist-0.1-py2.py3-none-any.whl", tmpdir) script.pip( "install", "--no-compile", "simple.dist==0.1", "--no-index", - "--find-links", data.find_links + "--find-links", tmpdir, ) # There are many locations for the __init__.pyc file so attempt to find # any of them From f5684ed7aed459ce2e97a7e2a212679520427d3b Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 3 Jan 2020 18:20:01 -0500 Subject: [PATCH 1031/3170] Use shared_data fixture in wheel install tests shared_data avoids copying the entire data directory, so use it in cases where we know pip won't have any opportunity to edit the data files (where we're passing tmpdir for --find-links). --- tests/functional/test_install_wheel.py | 81 +++++++++++++++++--------- 1 file changed, 52 insertions(+), 29 deletions(-) diff --git a/tests/functional/test_install_wheel.py b/tests/functional/test_install_wheel.py index f62e79f286a..1aec5a9acea 100644 --- a/tests/functional/test_install_wheel.py +++ b/tests/functional/test_install_wheel.py @@ -41,11 +41,13 @@ def test_install_from_broken_wheel(script, data): editable=False) -def test_basic_install_from_wheel(script, data, tmpdir): +def test_basic_install_from_wheel(script, shared_data, tmpdir): """ Test installing from a wheel (that has a script) """ - shutil.copy(data.packages / "has.script-1.0-py2.py3-none-any.whl", tmpdir) + shutil.copy( + shared_data.packages / "has.script-1.0-py2.py3-none-any.whl", tmpdir + ) result = script.pip( 'install', 'has.script==1.0', '--no-index', '--find-links', tmpdir, @@ -58,14 +60,16 @@ def test_basic_install_from_wheel(script, data, tmpdir): assert script_file in result.files_created -def test_basic_install_from_wheel_with_extras(script, data, tmpdir): +def test_basic_install_from_wheel_with_extras(script, shared_data, tmpdir): """ Test installing from a wheel with extras. """ shutil.copy( - data.packages / "complex_dist-0.1-py2.py3-none-any.whl", tmpdir + shared_data.packages / "complex_dist-0.1-py2.py3-none-any.whl", tmpdir + ) + shutil.copy( + shared_data.packages / "simple.dist-0.1-py2.py3-none-any.whl", tmpdir ) - shutil.copy(data.packages / "simple.dist-0.1-py2.py3-none-any.whl", tmpdir) result = script.pip( 'install', 'complex-dist[simple]', '--no-index', '--find-links', tmpdir, @@ -115,11 +119,13 @@ def test_install_from_wheel_with_headers(script, data): result.stdout) -def test_install_wheel_with_target(script, data, with_wheel, tmpdir): +def test_install_wheel_with_target(script, shared_data, with_wheel, tmpdir): """ Test installing a wheel using pip install --target """ - shutil.copy(data.packages / "simple.dist-0.1-py2.py3-none-any.whl", tmpdir) + shutil.copy( + shared_data.packages / "simple.dist-0.1-py2.py3-none-any.whl", tmpdir + ) target_dir = script.scratch_path / 'target' result = script.pip( 'install', 'simple.dist==0.1', '-t', target_dir, @@ -165,12 +171,14 @@ def test_install_wheel_with_target_and_data_files(script, data, with_wheel): not in result.files_created), str(result) -def test_install_wheel_with_root(script, data, tmpdir): +def test_install_wheel_with_root(script, shared_data, tmpdir): """ Test installing a wheel using pip install --root """ root_dir = script.scratch_path / 'root' - shutil.copy(data.packages / "simple.dist-0.1-py2.py3-none-any.whl", tmpdir) + shutil.copy( + shared_data.packages / "simple.dist-0.1-py2.py3-none-any.whl", tmpdir + ) result = script.pip( 'install', 'simple.dist==0.1', '--root', root_dir, '--no-index', '--find-links', tmpdir, @@ -178,12 +186,14 @@ def test_install_wheel_with_root(script, data, tmpdir): assert Path('scratch') / 'root' in result.files_created -def test_install_wheel_with_prefix(script, data, tmpdir): +def test_install_wheel_with_prefix(script, shared_data, tmpdir): """ Test installing a wheel using pip install --prefix """ prefix_dir = script.scratch_path / 'prefix' - shutil.copy(data.packages / "simple.dist-0.1-py2.py3-none-any.whl", tmpdir) + shutil.copy( + shared_data.packages / "simple.dist-0.1-py2.py3-none-any.whl", tmpdir + ) result = script.pip( 'install', 'simple.dist==0.1', '--prefix', prefix_dir, '--no-index', '--find-links', tmpdir, @@ -242,11 +252,13 @@ def test_wheel_record_lines_in_deterministic_order(script, data): @pytest.mark.incompatible_with_test_venv -def test_install_user_wheel(script, data, with_wheel, tmpdir): +def test_install_user_wheel(script, shared_data, with_wheel, tmpdir): """ Test user install from wheel (that has a script) """ - shutil.copy(data.packages / "has.script-1.0-py2.py3-none-any.whl", tmpdir) + shutil.copy( + shared_data.packages / "has.script-1.0-py2.py3-none-any.whl", tmpdir + ) result = script.pip( 'install', 'has.script==1.0', '--user', '--no-index', '--find-links', tmpdir, @@ -257,12 +269,13 @@ def test_install_user_wheel(script, data, with_wheel, tmpdir): assert script_file in result.files_created, str(result) -def test_install_from_wheel_gen_entrypoint(script, data, tmpdir): +def test_install_from_wheel_gen_entrypoint(script, shared_data, tmpdir): """ Test installing scripts (entry points are generated) """ shutil.copy( - data.packages / "script.wheel1a-0.1-py2.py3-none-any.whl", tmpdir + shared_data.packages / "script.wheel1a-0.1-py2.py3-none-any.whl", + tmpdir, ) result = script.pip( 'install', 'script.wheel1a==0.1', '--no-index', @@ -278,12 +291,15 @@ def test_install_from_wheel_gen_entrypoint(script, data, tmpdir): assert bool(os.access(script.base_path / wrapper_file, os.X_OK)) -def test_install_from_wheel_gen_uppercase_entrypoint(script, data, tmpdir): +def test_install_from_wheel_gen_uppercase_entrypoint( + script, shared_data, tmpdir +): """ Test installing scripts with uppercase letters in entry point names """ shutil.copy( - data.packages / "console_scripts_uppercase-1.0-py2.py3-none-any.whl", + shared_data.packages / + "console_scripts_uppercase-1.0-py2.py3-none-any.whl", tmpdir, ) result = script.pip( @@ -301,12 +317,13 @@ def test_install_from_wheel_gen_uppercase_entrypoint(script, data, tmpdir): assert bool(os.access(script.base_path / wrapper_file, os.X_OK)) -def test_install_from_wheel_with_legacy(script, data, tmpdir): +def test_install_from_wheel_with_legacy(script, shared_data, tmpdir): """ Test installing scripts (legacy scripts are preserved) """ shutil.copy( - data.packages / "script.wheel2a-0.1-py2.py3-none-any.whl", tmpdir + shared_data.packages / "script.wheel2a-0.1-py2.py3-none-any.whl", + tmpdir, ) result = script.pip( 'install', 'script.wheel2a==0.1', '--no-index', @@ -320,13 +337,15 @@ def test_install_from_wheel_with_legacy(script, data, tmpdir): assert legacy_file2 in result.files_created -def test_install_from_wheel_no_setuptools_entrypoint(script, data, tmpdir): +def test_install_from_wheel_no_setuptools_entrypoint( + script, shared_data, tmpdir +): """ Test that when we generate scripts, any existing setuptools wrappers in the wheel are skipped. """ shutil.copy( - data.packages / "script.wheel1-0.1-py2.py3-none-any.whl", tmpdir + shared_data.packages / "script.wheel1-0.1-py2.py3-none-any.whl", tmpdir ) result = script.pip( 'install', 'script.wheel1==0.1', '--no-index', @@ -347,13 +366,13 @@ def test_install_from_wheel_no_setuptools_entrypoint(script, data, tmpdir): assert wrapper_helper not in result.files_created -def test_skipping_setuptools_doesnt_skip_legacy(script, data, tmpdir): +def test_skipping_setuptools_doesnt_skip_legacy(script, shared_data, tmpdir): """ Test installing scripts (legacy scripts are preserved even when we skip setuptools wrappers) """ shutil.copy( - data.packages / "script.wheel2-0.1-py2.py3-none-any.whl", tmpdir + shared_data.packages / "script.wheel2-0.1-py2.py3-none-any.whl", tmpdir ) result = script.pip( 'install', 'script.wheel2==0.1', '--no-index', @@ -369,12 +388,12 @@ def test_skipping_setuptools_doesnt_skip_legacy(script, data, tmpdir): assert wrapper_helper not in result.files_created -def test_install_from_wheel_gui_entrypoint(script, data, tmpdir): +def test_install_from_wheel_gui_entrypoint(script, shared_data, tmpdir): """ Test installing scripts (gui entry points are generated) """ shutil.copy( - data.packages / "script.wheel3-0.1-py2.py3-none-any.whl", tmpdir + shared_data.packages / "script.wheel3-0.1-py2.py3-none-any.whl", tmpdir ) result = script.pip( 'install', 'script.wheel3==0.1', '--no-index', @@ -387,11 +406,13 @@ def test_install_from_wheel_gui_entrypoint(script, data, tmpdir): assert wrapper_file in result.files_created -def test_wheel_compiles_pyc(script, data, tmpdir): +def test_wheel_compiles_pyc(script, shared_data, tmpdir): """ Test installing from wheel with --compile on """ - shutil.copy(data.packages / "simple.dist-0.1-py2.py3-none-any.whl", tmpdir) + shutil.copy( + shared_data.packages / "simple.dist-0.1-py2.py3-none-any.whl", tmpdir + ) script.pip( "install", "--compile", "simple.dist==0.1", "--no-index", "--find-links", tmpdir, @@ -409,11 +430,13 @@ def test_wheel_compiles_pyc(script, data, tmpdir): assert any(exists) -def test_wheel_no_compiles_pyc(script, data, tmpdir): +def test_wheel_no_compiles_pyc(script, shared_data, tmpdir): """ Test installing from wheel with --compile on """ - shutil.copy(data.packages / "simple.dist-0.1-py2.py3-none-any.whl", tmpdir) + shutil.copy( + shared_data.packages / "simple.dist-0.1-py2.py3-none-any.whl", tmpdir + ) script.pip( "install", "--no-compile", "simple.dist==0.1", "--no-index", "--find-links", tmpdir, From 79eaf132fca0501803c287cc588a9156dd250257 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 3 Jan 2020 19:13:08 -0500 Subject: [PATCH 1032/3170] Remove unnecessary expect_stderr from _git_commit This simplifies our interface to git, which will make it easier to trade out our subprocess-based invocations in the future. --- tests/lib/__init__.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index 394a469d17d..8a98aadb91e 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -716,8 +716,7 @@ def main(): dir_path.joinpath(filename).write_text(text) -def _git_commit(env_or_script, repo_dir, message=None, args=None, - expect_stderr=False): +def _git_commit(env_or_script, repo_dir, message=None, args=None): """ Run git-commit. @@ -737,7 +736,7 @@ def _git_commit(env_or_script, repo_dir, message=None, args=None, ] new_args.extend(args) new_args.extend(['-m', message]) - env_or_script.run(*new_args, cwd=repo_dir, expect_stderr=expect_stderr) + env_or_script.run(*new_args, cwd=repo_dir) def _vcs_add(script, version_pkg_path, vcs='git'): @@ -876,8 +875,7 @@ def _change_test_package_version(script, version_pkg_path): ) # Pass -a to stage the change to the main file. _git_commit( - script, version_pkg_path, message='messed version', args=['-a'], - expect_stderr=True, + script, version_pkg_path, message='messed version', args=['-a'] ) From f9bf1a70046afc72de66490cac1a1d4b72eca621 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 3 Jan 2020 19:19:18 -0500 Subject: [PATCH 1033/3170] Make explicit argument for git commit --allow-empty --- tests/functional/test_vcs_git.py | 2 +- tests/lib/__init__.py | 7 ++++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/functional/test_vcs_git.py b/tests/functional/test_vcs_git.py index c3b23afa022..de8b4c14b58 100644 --- a/tests/functional/test_vcs_git.py +++ b/tests/functional/test_vcs_git.py @@ -34,7 +34,7 @@ def checkout_new_branch(script, repo_dir, branch): def do_commit(script, dest): - _git_commit(script, dest, message='test commit', args=['--allow-empty']) + _git_commit(script, dest, message='test commit', allow_empty=True) return get_head_sha(script, dest) diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index 8a98aadb91e..ee1ca1019ed 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -716,7 +716,9 @@ def main(): dir_path.joinpath(filename).write_text(text) -def _git_commit(env_or_script, repo_dir, message=None, args=None): +def _git_commit( + env_or_script, repo_dir, message=None, args=None, allow_empty=False +): """ Run git-commit. @@ -731,6 +733,9 @@ def _git_commit(env_or_script, repo_dir, message=None, args=None): if args is None: args = [] + if allow_empty: + args.append("--allow-empty") + new_args = [ 'git', 'commit', '-q', '--author', 'pip <pypa-dev@googlegroups.com>', ] From 5665b94a5b684892e39c7793c67a29a4a210d13c Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 3 Jan 2020 19:21:36 -0500 Subject: [PATCH 1034/3170] Make explicit argument for git commit -a --- tests/lib/__init__.py | 12 ++++++++++-- tests/lib/git_submodule_helpers.py | 4 +++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index ee1ca1019ed..b31f4119af2 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -717,7 +717,12 @@ def main(): def _git_commit( - env_or_script, repo_dir, message=None, args=None, allow_empty=False + env_or_script, + repo_dir, + message=None, + args=None, + allow_empty=False, + stage_modified=False, ): """ Run git-commit. @@ -736,6 +741,9 @@ def _git_commit( if allow_empty: args.append("--allow-empty") + if stage_modified: + args.append("--all") + new_args = [ 'git', 'commit', '-q', '--author', 'pip <pypa-dev@googlegroups.com>', ] @@ -880,7 +888,7 @@ def _change_test_package_version(script, version_pkg_path): ) # Pass -a to stage the change to the main file. _git_commit( - script, version_pkg_path, message='messed version', args=['-a'] + script, version_pkg_path, message='messed version', stage_modified=True ) diff --git a/tests/lib/git_submodule_helpers.py b/tests/lib/git_submodule_helpers.py index c529620f76f..34295a05dc9 100644 --- a/tests/lib/git_submodule_helpers.py +++ b/tests/lib/git_submodule_helpers.py @@ -31,7 +31,9 @@ def _pull_in_submodule_changes_to_module(env, module_path, rel_path): submodule_path = module_path / rel_path env.run('git', 'pull', '-q', 'origin', 'master', cwd=submodule_path) # Pass -a to stage the submodule changes that were just pulled in. - _git_commit(env, module_path, message='submodule change', args=['-a']) + _git_commit( + env, module_path, message='submodule change', stage_modified=True + ) def _create_test_package_with_submodule(env, rel_path): From b65bd4c31adf2178b25fa14a44417fadbcf87c15 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 3 Jan 2020 19:22:31 -0500 Subject: [PATCH 1035/3170] Remove unused args parameter from _git_commit --- tests/lib/__init__.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index b31f4119af2..19232ec966a 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -720,7 +720,6 @@ def _git_commit( env_or_script, repo_dir, message=None, - args=None, allow_empty=False, stage_modified=False, ): @@ -731,12 +730,11 @@ def _git_commit( env_or_script: pytest's `script` or `env` argument. repo_dir: a path to a Git repository. message: an optional commit message. - args: optional additional options to pass to git-commit. """ if message is None: message = 'test commit' - if args is None: - args = [] + + args = [] if allow_empty: args.append("--allow-empty") From 8415011774617809ddf7d81358a145fc13ae2934 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Sat, 4 Jan 2020 12:50:06 +0100 Subject: [PATCH 1036/3170] Clarify git+git@ replacements --- src/pip/_internal/req/req_install.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index e46b1ca294b..05bd4308b87 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -643,7 +643,9 @@ def update_editable(self, obtain=True): replacement = None if self.link.url.startswith("git+git@"): replacement = ( - "git+https:// or git+ssh://" + "git+https://git@example.com/..., " + "git+ssh://git@example.com/..., " + "or the insecure git+git://git@example.com/..." ) deprecated(reason, replacement, gone_in="21.0") hidden_url = hide_url(self.link.url) From 9cbe7f90d6461f995f13e1602fc51e6e1ab96e4c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Sat, 4 Jan 2020 12:50:38 +0100 Subject: [PATCH 1037/3170] Reference the git+git@ removal issue in the deprecation message --- src/pip/_internal/req/req_install.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 05bd4308b87..58746cd2a41 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -647,7 +647,7 @@ def update_editable(self, obtain=True): "git+ssh://git@example.com/..., " "or the insecure git+git://git@example.com/..." ) - deprecated(reason, replacement, gone_in="21.0") + deprecated(reason, replacement, gone_in="21.0", issue=7554) hidden_url = hide_url(self.link.url) if obtain: vcs_backend.obtain(self.source_dir, url=hidden_url) From b58205ea014bd25cb24fdc53d432833a4e0e9420 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 1 Jan 2020 20:29:10 -0500 Subject: [PATCH 1038/3170] Use valid wheel for functional download tests Previously we were copying an existing wheel to a file with a different distribution name. When using stricter metadata parsing this would fail, so now we use a more conformant dummy wheel function. --- tests/functional/test_download.py | 9 ++++--- tests/lib/__init__.py | 42 +++++++++++++++++++++++++++++++ 2 files changed, 47 insertions(+), 4 deletions(-) diff --git a/tests/functional/test_download.py b/tests/functional/test_download.py index 4064221ecdc..05b97ab3aec 100644 --- a/tests/functional/test_download.py +++ b/tests/functional/test_download.py @@ -7,15 +7,16 @@ from pip._internal.cli.status_codes import ERROR from pip._internal.utils.urls import path_to_url +from tests.lib import create_really_basic_wheel from tests.lib.path import Path from tests.lib.server import file_response def fake_wheel(data, wheel_path): - shutil.copy( - data.packages.joinpath('simple.dist-0.1-py2.py3-none-any.whl'), - data.packages.joinpath(wheel_path), - ) + wheel_name = os.path.basename(wheel_path) + name, version, rest = wheel_name.split("-", 2) + wheel_data = create_really_basic_wheel(name, version) + data.packages.joinpath(wheel_path).write_bytes(wheel_data) @pytest.mark.network diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index 19232ec966a..86518b90ab8 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -7,8 +7,12 @@ import subprocess import sys import textwrap +from base64 import urlsafe_b64encode from contextlib import contextmanager +from hashlib import sha256 +from io import BytesIO from textwrap import dedent +from zipfile import ZipFile import pytest from pip._vendor.six import PY2 @@ -930,6 +934,44 @@ def create_test_package_with_setup(script, **setup_kwargs): return pkg_path +def urlsafe_b64encode_nopad(data): + # type: (bytes) -> str + return urlsafe_b64encode(data).rstrip(b"=").decode("ascii") + + +def create_really_basic_wheel(name, version): + # type: (str, str) -> bytes + def digest(contents): + return "sha256={}".format( + urlsafe_b64encode_nopad(sha256(contents).digest()) + ) + + def add_file(path, text): + contents = text.encode("utf-8") + z.writestr(path, contents) + records.append((path, digest(contents), str(len(contents)))) + + dist_info = "{}-{}.dist-info".format(name, version) + record_path = "{}/RECORD".format(dist_info) + records = [(record_path, "", "")] + buf = BytesIO() + with ZipFile(buf, "w") as z: + add_file("{}/WHEEL".format(dist_info), "Wheel-Version: 1.0") + add_file( + "{}/METADATA".format(dist_info), + dedent( + """\ + Metadata-Version: 2.1 + Name: {} + Version: {} + """.format(name, version) + ), + ) + z.writestr(record_path, "\n".join(",".join(r) for r in records)) + buf.seek(0) + return buf.read() + + def create_basic_wheel_for_package( script, name, version, depends=None, extras=None, extra_files=None ): From b263fcc105ad88cdb10a29e93e99a7343a192032 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 1 Jan 2020 18:01:50 -0500 Subject: [PATCH 1039/3170] Move parse_wheel and supporting functions to utils.wheel In order to parse metadata from wheel files directly we want to reuse parse_wheel. Moving it out helps avoid creating an unnecessary dependence on operations.install.wheel. --- src/pip/_internal/operations/install/wheel.py | 140 +--------------- src/pip/_internal/utils/wheel.py | 154 ++++++++++++++++++ tests/unit/test_utils_wheel.py | 143 ++++++++++++++++ tests/unit/test_wheel.py | 138 +--------------- 4 files changed, 301 insertions(+), 274 deletions(-) create mode 100644 src/pip/_internal/utils/wheel.py create mode 100644 tests/unit/test_utils_wheel.py diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index ca78b68bb3a..aac975c3ac8 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -17,21 +17,20 @@ import sys import warnings from base64 import urlsafe_b64encode -from email.parser import Parser from zipfile import ZipFile from pip._vendor import pkg_resources from pip._vendor.distlib.scripts import ScriptMaker from pip._vendor.distlib.util import get_export_entry -from pip._vendor.packaging.utils import canonicalize_name -from pip._vendor.six import PY2, StringIO, ensure_str +from pip._vendor.six import StringIO -from pip._internal.exceptions import InstallationError, UnsupportedWheel +from pip._internal.exceptions import InstallationError from pip._internal.locations import get_major_minor_version from pip._internal.utils.misc import captured_stdout, ensure_dir, hash_file from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.unpacking import unpack_file +from pip._internal.utils.wheel import parse_wheel if MYPY_CHECK_RUNNING: from email.message import Message @@ -44,14 +43,6 @@ InstalledCSVRow = Tuple[str, ...] -if PY2: - from zipfile import BadZipfile as BadZipFile -else: - from zipfile import BadZipFile - - -VERSION_COMPATIBLE = (1, 0) - logger = logging.getLogger(__name__) @@ -622,128 +613,3 @@ def install_wheel( pycompile=pycompile, warn_script_location=warn_script_location, ) - - -def parse_wheel(wheel_zip, name): - # type: (ZipFile, str) -> Tuple[str, Message] - """Extract information from the provided wheel, ensuring it meets basic - standards. - - Returns the name of the .dist-info directory and the parsed WHEEL metadata. - """ - try: - info_dir = wheel_dist_info_dir(wheel_zip, name) - metadata = wheel_metadata(wheel_zip, info_dir) - version = wheel_version(metadata) - except UnsupportedWheel as e: - raise UnsupportedWheel( - "{} has an invalid wheel, {}".format(name, str(e)) - ) - - check_compatibility(version, name) - - return info_dir, metadata - - -def wheel_dist_info_dir(source, name): - # type: (ZipFile, str) -> str - """Returns the name of the contained .dist-info directory. - - Raises AssertionError or UnsupportedWheel if not found, >1 found, or - it doesn't match the provided name. - """ - # Zip file path separators must be / - subdirs = list(set(p.split("/")[0] for p in source.namelist())) - - info_dirs = [s for s in subdirs if s.endswith('.dist-info')] - - if not info_dirs: - raise UnsupportedWheel(".dist-info directory not found") - - if len(info_dirs) > 1: - raise UnsupportedWheel( - "multiple .dist-info directories found: {}".format( - ", ".join(info_dirs) - ) - ) - - info_dir = info_dirs[0] - - info_dir_name = canonicalize_name(info_dir) - canonical_name = canonicalize_name(name) - if not info_dir_name.startswith(canonical_name): - raise UnsupportedWheel( - ".dist-info directory {!r} does not start with {!r}".format( - info_dir, canonical_name - ) - ) - - # Zip file paths can be unicode or str depending on the zip entry flags, - # so normalize it. - return ensure_str(info_dir) - - -def wheel_metadata(source, dist_info_dir): - # type: (ZipFile, str) -> Message - """Return the WHEEL metadata of an extracted wheel, if possible. - Otherwise, raise UnsupportedWheel. - """ - try: - # Zip file path separators must be / - wheel_contents = source.read("{}/WHEEL".format(dist_info_dir)) - # BadZipFile for general corruption, KeyError for missing entry, - # and RuntimeError for password-protected files - except (BadZipFile, KeyError, RuntimeError) as e: - raise UnsupportedWheel("could not read WHEEL file: {!r}".format(e)) - - try: - wheel_text = ensure_str(wheel_contents) - except UnicodeDecodeError as e: - raise UnsupportedWheel("error decoding WHEEL: {!r}".format(e)) - - # FeedParser (used by Parser) does not raise any exceptions. The returned - # message may have .defects populated, but for backwards-compatibility we - # currently ignore them. - return Parser().parsestr(wheel_text) - - -def wheel_version(wheel_data): - # type: (Message) -> Tuple[int, ...] - """Given WHEEL metadata, return the parsed Wheel-Version. - Otherwise, raise UnsupportedWheel. - """ - version_text = wheel_data["Wheel-Version"] - if version_text is None: - raise UnsupportedWheel("WHEEL is missing Wheel-Version") - - version = version_text.strip() - - try: - return tuple(map(int, version.split('.'))) - except ValueError: - raise UnsupportedWheel("invalid Wheel-Version: {!r}".format(version)) - - -def check_compatibility(version, name): - # type: (Tuple[int, ...], str) -> None - """Raises errors or warns if called with an incompatible Wheel-Version. - - Pip should refuse to install a Wheel-Version that's a major series - ahead of what it's compatible with (e.g 2.0 > 1.1); and warn when - installing a version only minor version ahead (e.g 1.2 > 1.1). - - version: a 2-tuple representing a Wheel-Version (Major, Minor) - name: name of wheel or package to raise exception about - - :raises UnsupportedWheel: when an incompatible Wheel-Version is given - """ - if version[0] > VERSION_COMPATIBLE[0]: - raise UnsupportedWheel( - "%s's Wheel-Version (%s) is not compatible with this version " - "of pip" % (name, '.'.join(map(str, version))) - ) - elif version > VERSION_COMPATIBLE: - logger.warning( - 'Installing from a newer Wheel-Version (%s)', - '.'.join(map(str, version)), - ) diff --git a/src/pip/_internal/utils/wheel.py b/src/pip/_internal/utils/wheel.py new file mode 100644 index 00000000000..8e5f8313acf --- /dev/null +++ b/src/pip/_internal/utils/wheel.py @@ -0,0 +1,154 @@ +"""Support functions for working with wheel files. +""" + +from __future__ import absolute_import + +import logging +from email.parser import Parser +from zipfile import ZipFile + +from pip._vendor.packaging.utils import canonicalize_name +from pip._vendor.six import PY2, ensure_str + +from pip._internal.exceptions import UnsupportedWheel +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from email.message import Message + from typing import Tuple + +if PY2: + from zipfile import BadZipfile as BadZipFile +else: + from zipfile import BadZipFile + + +VERSION_COMPATIBLE = (1, 0) + + +logger = logging.getLogger(__name__) + + +def parse_wheel(wheel_zip, name): + # type: (ZipFile, str) -> Tuple[str, Message] + """Extract information from the provided wheel, ensuring it meets basic + standards. + + Returns the name of the .dist-info directory and the parsed WHEEL metadata. + """ + try: + info_dir = wheel_dist_info_dir(wheel_zip, name) + metadata = wheel_metadata(wheel_zip, info_dir) + version = wheel_version(metadata) + except UnsupportedWheel as e: + raise UnsupportedWheel( + "{} has an invalid wheel, {}".format(name, str(e)) + ) + + check_compatibility(version, name) + + return info_dir, metadata + + +def wheel_dist_info_dir(source, name): + # type: (ZipFile, str) -> str + """Returns the name of the contained .dist-info directory. + + Raises AssertionError or UnsupportedWheel if not found, >1 found, or + it doesn't match the provided name. + """ + # Zip file path separators must be / + subdirs = list(set(p.split("/")[0] for p in source.namelist())) + + info_dirs = [s for s in subdirs if s.endswith('.dist-info')] + + if not info_dirs: + raise UnsupportedWheel(".dist-info directory not found") + + if len(info_dirs) > 1: + raise UnsupportedWheel( + "multiple .dist-info directories found: {}".format( + ", ".join(info_dirs) + ) + ) + + info_dir = info_dirs[0] + + info_dir_name = canonicalize_name(info_dir) + canonical_name = canonicalize_name(name) + if not info_dir_name.startswith(canonical_name): + raise UnsupportedWheel( + ".dist-info directory {!r} does not start with {!r}".format( + info_dir, canonical_name + ) + ) + + # Zip file paths can be unicode or str depending on the zip entry flags, + # so normalize it. + return ensure_str(info_dir) + + +def wheel_metadata(source, dist_info_dir): + # type: (ZipFile, str) -> Message + """Return the WHEEL metadata of an extracted wheel, if possible. + Otherwise, raise UnsupportedWheel. + """ + try: + # Zip file path separators must be / + wheel_contents = source.read("{}/WHEEL".format(dist_info_dir)) + # BadZipFile for general corruption, KeyError for missing entry, + # and RuntimeError for password-protected files + except (BadZipFile, KeyError, RuntimeError) as e: + raise UnsupportedWheel("could not read WHEEL file: {!r}".format(e)) + + try: + wheel_text = ensure_str(wheel_contents) + except UnicodeDecodeError as e: + raise UnsupportedWheel("error decoding WHEEL: {!r}".format(e)) + + # FeedParser (used by Parser) does not raise any exceptions. The returned + # message may have .defects populated, but for backwards-compatibility we + # currently ignore them. + return Parser().parsestr(wheel_text) + + +def wheel_version(wheel_data): + # type: (Message) -> Tuple[int, ...] + """Given WHEEL metadata, return the parsed Wheel-Version. + Otherwise, raise UnsupportedWheel. + """ + version_text = wheel_data["Wheel-Version"] + if version_text is None: + raise UnsupportedWheel("WHEEL is missing Wheel-Version") + + version = version_text.strip() + + try: + return tuple(map(int, version.split('.'))) + except ValueError: + raise UnsupportedWheel("invalid Wheel-Version: {!r}".format(version)) + + +def check_compatibility(version, name): + # type: (Tuple[int, ...], str) -> None + """Raises errors or warns if called with an incompatible Wheel-Version. + + Pip should refuse to install a Wheel-Version that's a major series + ahead of what it's compatible with (e.g 2.0 > 1.1); and warn when + installing a version only minor version ahead (e.g 1.2 > 1.1). + + version: a 2-tuple representing a Wheel-Version (Major, Minor) + name: name of wheel or package to raise exception about + + :raises UnsupportedWheel: when an incompatible Wheel-Version is given + """ + if version[0] > VERSION_COMPATIBLE[0]: + raise UnsupportedWheel( + "%s's Wheel-Version (%s) is not compatible with this version " + "of pip" % (name, '.'.join(map(str, version))) + ) + elif version > VERSION_COMPATIBLE: + logger.warning( + 'Installing from a newer Wheel-Version (%s)', + '.'.join(map(str, version)), + ) diff --git a/tests/unit/test_utils_wheel.py b/tests/unit/test_utils_wheel.py new file mode 100644 index 00000000000..758283f0acb --- /dev/null +++ b/tests/unit/test_utils_wheel.py @@ -0,0 +1,143 @@ +import os +from email import message_from_string +from io import BytesIO +from zipfile import ZipFile + +import pytest +from pip._vendor.contextlib2 import ExitStack + +from pip._internal.exceptions import UnsupportedWheel +from pip._internal.utils import wheel +from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from tests.lib import skip_if_python2 + +if MYPY_CHECK_RUNNING: + from tests.lib.path import Path + + +@pytest.fixture +def zip_dir(): + def make_zip(path): + # type: (Path) -> ZipFile + buf = BytesIO() + with ZipFile(buf, "w", allowZip64=True) as z: + for dirpath, dirnames, filenames in os.walk(path): + for filename in filenames: + file_path = os.path.join(path, dirpath, filename) + # Zip files must always have / as path separator + archive_path = os.path.relpath(file_path, path).replace( + os.pathsep, "/" + ) + z.write(file_path, archive_path) + + return stack.enter_context(ZipFile(buf, "r", allowZip64=True)) + + stack = ExitStack() + with stack: + yield make_zip + + +def test_wheel_dist_info_dir_found(tmpdir, zip_dir): + expected = "simple-0.1.dist-info" + dist_info_dir = tmpdir / expected + dist_info_dir.mkdir() + dist_info_dir.joinpath("WHEEL").touch() + assert wheel.wheel_dist_info_dir(zip_dir(tmpdir), "simple") == expected + + +def test_wheel_dist_info_dir_multiple(tmpdir, zip_dir): + dist_info_dir_1 = tmpdir / "simple-0.1.dist-info" + dist_info_dir_1.mkdir() + dist_info_dir_1.joinpath("WHEEL").touch() + dist_info_dir_2 = tmpdir / "unrelated-0.1.dist-info" + dist_info_dir_2.mkdir() + dist_info_dir_2.joinpath("WHEEL").touch() + with pytest.raises(UnsupportedWheel) as e: + wheel.wheel_dist_info_dir(zip_dir(tmpdir), "simple") + assert "multiple .dist-info directories found" in str(e.value) + + +def test_wheel_dist_info_dir_none(tmpdir, zip_dir): + with pytest.raises(UnsupportedWheel) as e: + wheel.wheel_dist_info_dir(zip_dir(tmpdir), "simple") + assert "directory not found" in str(e.value) + + +def test_wheel_dist_info_dir_wrong_name(tmpdir, zip_dir): + dist_info_dir = tmpdir / "unrelated-0.1.dist-info" + dist_info_dir.mkdir() + dist_info_dir.joinpath("WHEEL").touch() + with pytest.raises(UnsupportedWheel) as e: + wheel.wheel_dist_info_dir(zip_dir(tmpdir), "simple") + assert "does not start with 'simple'" in str(e.value) + + +def test_wheel_version_ok(tmpdir, data): + assert wheel.wheel_version( + message_from_string("Wheel-Version: 1.9") + ) == (1, 9) + + +def test_wheel_metadata_fails_missing_wheel(tmpdir, zip_dir): + dist_info_dir = tmpdir / "simple-0.1.0.dist-info" + dist_info_dir.mkdir() + dist_info_dir.joinpath("METADATA").touch() + + with pytest.raises(UnsupportedWheel) as e: + wheel.wheel_metadata(zip_dir(tmpdir), dist_info_dir.name) + assert "could not read WHEEL file" in str(e.value) + + +@skip_if_python2 +def test_wheel_metadata_fails_on_bad_encoding(tmpdir, zip_dir): + dist_info_dir = tmpdir / "simple-0.1.0.dist-info" + dist_info_dir.mkdir() + dist_info_dir.joinpath("METADATA").touch() + dist_info_dir.joinpath("WHEEL").write_bytes(b"\xff") + + with pytest.raises(UnsupportedWheel) as e: + wheel.wheel_metadata(zip_dir(tmpdir), dist_info_dir.name) + assert "error decoding WHEEL" in str(e.value) + + +def test_wheel_version_fails_on_no_wheel_version(): + with pytest.raises(UnsupportedWheel) as e: + wheel.wheel_version(message_from_string("")) + assert "missing Wheel-Version" in str(e.value) + + +@pytest.mark.parametrize("version", [ + ("",), + ("1.b",), + ("1.",), +]) +def test_wheel_version_fails_on_bad_wheel_version(version): + with pytest.raises(UnsupportedWheel) as e: + wheel.wheel_version( + message_from_string("Wheel-Version: {}".format(version)) + ) + assert "invalid Wheel-Version" in str(e.value) + + +def test_check_compatibility(): + name = 'test' + vc = wheel.VERSION_COMPATIBLE + + # Major version is higher - should be incompatible + higher_v = (vc[0] + 1, vc[1]) + + # test raises with correct error + with pytest.raises(UnsupportedWheel) as e: + wheel.check_compatibility(higher_v, name) + assert 'is not compatible' in str(e) + + # Should only log.warning - minor version is greater + higher_v = (vc[0], vc[1] + 1) + wheel.check_compatibility(higher_v, name) + + # These should work fine + wheel.check_compatibility(wheel.VERSION_COMPATIBLE, name) + + # E.g if wheel to install is 1.0 and we support up to 1.2 + lower_v = (vc[0], max(0, vc[1] - 1)) + wheel.check_compatibility(lower_v, name) diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index cdf273449b4..05300b96438 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -4,15 +4,11 @@ import os import textwrap from email import message_from_string -from io import BytesIO -from zipfile import ZipFile import pytest from mock import patch -from pip._vendor.contextlib2 import ExitStack from pip._vendor.packaging.requirements import Requirement -from pip._internal.exceptions import UnsupportedWheel from pip._internal.locations import get_scheme from pip._internal.models.scheme import Scheme from pip._internal.operations.build.wheel_legacy import ( @@ -25,12 +21,8 @@ ) from pip._internal.utils.compat import WINDOWS from pip._internal.utils.misc import hash_file -from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.unpacking import unpack_file -from tests.lib import DATA_DIR, assert_paths_equal, skip_if_python2 - -if MYPY_CHECK_RUNNING: - from tests.lib.path import Path +from tests.lib import DATA_DIR, assert_paths_equal def call_get_legacy_build_wheel_path(caplog, names): @@ -197,110 +189,6 @@ def test_get_csv_rows_for_installed__long_lines(tmpdir, caplog): assert messages == expected -@pytest.fixture -def zip_dir(): - def make_zip(path): - # type: (Path) -> ZipFile - buf = BytesIO() - with ZipFile(buf, "w", allowZip64=True) as z: - for dirpath, dirnames, filenames in os.walk(path): - for filename in filenames: - file_path = os.path.join(path, dirpath, filename) - # Zip files must always have / as path separator - archive_path = os.path.relpath(file_path, path).replace( - os.pathsep, "/" - ) - z.write(file_path, archive_path) - - return stack.enter_context(ZipFile(buf, "r", allowZip64=True)) - - stack = ExitStack() - with stack: - yield make_zip - - -def test_wheel_dist_info_dir_found(tmpdir, zip_dir): - expected = "simple-0.1.dist-info" - dist_info_dir = tmpdir / expected - dist_info_dir.mkdir() - dist_info_dir.joinpath("WHEEL").touch() - assert wheel.wheel_dist_info_dir(zip_dir(tmpdir), "simple") == expected - - -def test_wheel_dist_info_dir_multiple(tmpdir, zip_dir): - dist_info_dir_1 = tmpdir / "simple-0.1.dist-info" - dist_info_dir_1.mkdir() - dist_info_dir_1.joinpath("WHEEL").touch() - dist_info_dir_2 = tmpdir / "unrelated-0.1.dist-info" - dist_info_dir_2.mkdir() - dist_info_dir_2.joinpath("WHEEL").touch() - with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_dist_info_dir(zip_dir(tmpdir), "simple") - assert "multiple .dist-info directories found" in str(e.value) - - -def test_wheel_dist_info_dir_none(tmpdir, zip_dir): - with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_dist_info_dir(zip_dir(tmpdir), "simple") - assert "directory not found" in str(e.value) - - -def test_wheel_dist_info_dir_wrong_name(tmpdir, zip_dir): - dist_info_dir = tmpdir / "unrelated-0.1.dist-info" - dist_info_dir.mkdir() - dist_info_dir.joinpath("WHEEL").touch() - with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_dist_info_dir(zip_dir(tmpdir), "simple") - assert "does not start with 'simple'" in str(e.value) - - -def test_wheel_version_ok(tmpdir, data): - assert wheel.wheel_version( - message_from_string("Wheel-Version: 1.9") - ) == (1, 9) - - -def test_wheel_metadata_fails_missing_wheel(tmpdir, zip_dir): - dist_info_dir = tmpdir / "simple-0.1.0.dist-info" - dist_info_dir.mkdir() - dist_info_dir.joinpath("METADATA").touch() - - with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_metadata(zip_dir(tmpdir), dist_info_dir.name) - assert "could not read WHEEL file" in str(e.value) - - -@skip_if_python2 -def test_wheel_metadata_fails_on_bad_encoding(tmpdir, zip_dir): - dist_info_dir = tmpdir / "simple-0.1.0.dist-info" - dist_info_dir.mkdir() - dist_info_dir.joinpath("METADATA").touch() - dist_info_dir.joinpath("WHEEL").write_bytes(b"\xff") - - with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_metadata(zip_dir(tmpdir), dist_info_dir.name) - assert "error decoding WHEEL" in str(e.value) - - -def test_wheel_version_fails_on_no_wheel_version(): - with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_version(message_from_string("")) - assert "missing Wheel-Version" in str(e.value) - - -@pytest.mark.parametrize("version", [ - ("",), - ("1.b",), - ("1.",), -]) -def test_wheel_version_fails_on_bad_wheel_version(version): - with pytest.raises(UnsupportedWheel) as e: - wheel.wheel_version( - message_from_string("Wheel-Version: {}".format(version)) - ) - assert "invalid Wheel-Version" in str(e.value) - - @pytest.mark.parametrize("text,expected", [ ("Root-Is-Purelib: true", True), ("Root-Is-Purelib: false", False), @@ -313,30 +201,6 @@ def test_wheel_root_is_purelib(text, expected): assert wheel.wheel_root_is_purelib(message_from_string(text)) == expected -def test_check_compatibility(): - name = 'test' - vc = wheel.VERSION_COMPATIBLE - - # Major version is higher - should be incompatible - higher_v = (vc[0] + 1, vc[1]) - - # test raises with correct error - with pytest.raises(UnsupportedWheel) as e: - wheel.check_compatibility(higher_v, name) - assert 'is not compatible' in str(e) - - # Should only log.warning - minor version is greater - higher_v = (vc[0], vc[1] + 1) - wheel.check_compatibility(higher_v, name) - - # These should work fine - wheel.check_compatibility(wheel.VERSION_COMPATIBLE, name) - - # E.g if wheel to install is 1.0 and we support up to 1.2 - lower_v = (vc[0], max(0, vc[1] - 1)) - wheel.check_compatibility(lower_v, name) - - class TestWheelFile(object): def test_unpack_wheel_no_flatten(self, tmpdir): From 20706eb93fc1c0a3896ff027aae1dd74d27881ee Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 1 Jan 2020 18:52:58 -0500 Subject: [PATCH 1040/3170] Add DictMetadata for adapting zip data to Distribution pkg_resources.Distribution classes delegate to the IMetadataProvider implementation provided at construction. This is the one we'll use for adapting a ZipFile to pkg_resources.DistInfoDistribution. --- src/pip/_internal/utils/pkg_resources.py | 44 ++++++++++++++++++ tests/unit/test_utils_pkg_resources.py | 57 ++++++++++++++++++++++++ 2 files changed, 101 insertions(+) create mode 100644 src/pip/_internal/utils/pkg_resources.py create mode 100644 tests/unit/test_utils_pkg_resources.py diff --git a/src/pip/_internal/utils/pkg_resources.py b/src/pip/_internal/utils/pkg_resources.py new file mode 100644 index 00000000000..0bc129acc6a --- /dev/null +++ b/src/pip/_internal/utils/pkg_resources.py @@ -0,0 +1,44 @@ +from pip._vendor.pkg_resources import yield_lines +from pip._vendor.six import ensure_str + +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import Dict, Iterable, List + + +class DictMetadata(object): + """IMetadataProvider that reads metadata files from a dictionary. + """ + def __init__(self, metadata): + # type: (Dict[str, bytes]) -> None + self._metadata = metadata + + def has_metadata(self, name): + # type: (str) -> bool + return name in self._metadata + + def get_metadata(self, name): + # type: (str) -> str + try: + return ensure_str(self._metadata[name]) + except UnicodeDecodeError as e: + # Mirrors handling done in pkg_resources.NullProvider. + e.reason += " in {} file".format(name) + raise + + def get_metadata_lines(self, name): + # type: (str) -> Iterable[str] + return yield_lines(self.get_metadata(name)) + + def metadata_isdir(self, name): + # type: (str) -> bool + return False + + def metadata_listdir(self, name): + # type: (str) -> List[str] + return [] + + def run_script(self, script_name, namespace): + # type: (str, str) -> None + pass diff --git a/tests/unit/test_utils_pkg_resources.py b/tests/unit/test_utils_pkg_resources.py new file mode 100644 index 00000000000..d113d6df124 --- /dev/null +++ b/tests/unit/test_utils_pkg_resources.py @@ -0,0 +1,57 @@ +from email.message import Message + +import pytest +from pip._vendor.pkg_resources import DistInfoDistribution, Requirement +from pip._vendor.six import ensure_binary + +from pip._internal.utils.packaging import get_metadata, get_requires_python +from pip._internal.utils.pkg_resources import DictMetadata +from tests.lib import skip_if_python2 + + +def test_dict_metadata_works(): + name = "simple" + version = "0.1.0" + require_a = "a==1.0" + require_b = "b==1.1; extra == 'also_b'" + requires = [require_a, require_b, "c==1.2; extra == 'also_c'"] + extras = ["also_b", "also_c"] + requires_python = ">=3" + + metadata = Message() + metadata["Name"] = name + metadata["Version"] = version + for require in requires: + metadata["Requires-Dist"] = require + for extra in extras: + metadata["Provides-Extra"] = extra + metadata["Requires-Python"] = requires_python + + inner_metadata = DictMetadata({ + "METADATA": ensure_binary(metadata.as_string()) + }) + dist = DistInfoDistribution( + location="<in-memory>", metadata=inner_metadata, project_name=name + ) + + assert name == dist.project_name + assert version == dist.version + assert set(extras) == set(dist.extras) + assert [Requirement.parse(require_a)] == dist.requires([]) + assert [ + Requirement.parse(require_a), Requirement.parse(require_b) + ] == dist.requires(["also_b"]) + assert metadata.as_string() == get_metadata(dist).as_string() + assert requires_python == get_requires_python(dist) + + +# Metadata is not decoded on Python 2, so no chance for error. +@skip_if_python2 +def test_dict_metadata_throws_on_bad_unicode(): + metadata = DictMetadata({ + "METADATA": b"\xff" + }) + + with pytest.raises(UnicodeDecodeError) as e: + metadata.get_metadata("METADATA") + assert "METADATA" in str(e.value) From ae34781826687f63d6709dfb60b50c801efea727 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 1 Jan 2020 19:22:41 -0500 Subject: [PATCH 1041/3170] Parameterize wheel file path for metadata extraction --- src/pip/_internal/utils/wheel.py | 11 +++++++---- tests/unit/test_utils_wheel.py | 4 ++-- 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/utils/wheel.py b/src/pip/_internal/utils/wheel.py index 8e5f8313acf..b8a88a6cd4f 100644 --- a/src/pip/_internal/utils/wheel.py +++ b/src/pip/_internal/utils/wheel.py @@ -93,18 +93,21 @@ def wheel_metadata(source, dist_info_dir): """Return the WHEEL metadata of an extracted wheel, if possible. Otherwise, raise UnsupportedWheel. """ + # Zip file path separators must be / + path = "{}/WHEEL".format(dist_info_dir) try: - # Zip file path separators must be / - wheel_contents = source.read("{}/WHEEL".format(dist_info_dir)) + wheel_contents = source.read(path) # BadZipFile for general corruption, KeyError for missing entry, # and RuntimeError for password-protected files except (BadZipFile, KeyError, RuntimeError) as e: - raise UnsupportedWheel("could not read WHEEL file: {!r}".format(e)) + raise UnsupportedWheel( + "could not read {!r} file: {!r}".format(path, e) + ) try: wheel_text = ensure_str(wheel_contents) except UnicodeDecodeError as e: - raise UnsupportedWheel("error decoding WHEEL: {!r}".format(e)) + raise UnsupportedWheel("error decoding {!r}: {!r}".format(path, e)) # FeedParser (used by Parser) does not raise any exceptions. The returned # message may have .defects populated, but for backwards-compatibility we diff --git a/tests/unit/test_utils_wheel.py b/tests/unit/test_utils_wheel.py index 758283f0acb..20d7ea20d77 100644 --- a/tests/unit/test_utils_wheel.py +++ b/tests/unit/test_utils_wheel.py @@ -85,7 +85,7 @@ def test_wheel_metadata_fails_missing_wheel(tmpdir, zip_dir): with pytest.raises(UnsupportedWheel) as e: wheel.wheel_metadata(zip_dir(tmpdir), dist_info_dir.name) - assert "could not read WHEEL file" in str(e.value) + assert "could not read" in str(e.value) @skip_if_python2 @@ -97,7 +97,7 @@ def test_wheel_metadata_fails_on_bad_encoding(tmpdir, zip_dir): with pytest.raises(UnsupportedWheel) as e: wheel.wheel_metadata(zip_dir(tmpdir), dist_info_dir.name) - assert "error decoding WHEEL" in str(e.value) + assert "error decoding" in str(e.value) def test_wheel_version_fails_on_no_wheel_version(): From ed653938409283dd3a001cc6f5655a9a6316e1f7 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 1 Jan 2020 19:25:50 -0500 Subject: [PATCH 1042/3170] Extract reading wheel file from wheel_metadata into separate function --- src/pip/_internal/utils/wheel.py | 21 +++++++++++++-------- 1 file changed, 13 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/utils/wheel.py b/src/pip/_internal/utils/wheel.py index b8a88a6cd4f..29d6904adf4 100644 --- a/src/pip/_internal/utils/wheel.py +++ b/src/pip/_internal/utils/wheel.py @@ -88,15 +88,10 @@ def wheel_dist_info_dir(source, name): return ensure_str(info_dir) -def wheel_metadata(source, dist_info_dir): - # type: (ZipFile, str) -> Message - """Return the WHEEL metadata of an extracted wheel, if possible. - Otherwise, raise UnsupportedWheel. - """ - # Zip file path separators must be / - path = "{}/WHEEL".format(dist_info_dir) +def read_wheel_metadata_file(source, path): + # type: (ZipFile, str) -> bytes try: - wheel_contents = source.read(path) + return source.read(path) # BadZipFile for general corruption, KeyError for missing entry, # and RuntimeError for password-protected files except (BadZipFile, KeyError, RuntimeError) as e: @@ -104,6 +99,16 @@ def wheel_metadata(source, dist_info_dir): "could not read {!r} file: {!r}".format(path, e) ) + +def wheel_metadata(source, dist_info_dir): + # type: (ZipFile, str) -> Message + """Return the WHEEL metadata of an extracted wheel, if possible. + Otherwise, raise UnsupportedWheel. + """ + path = "{}/WHEEL".format(dist_info_dir) + # Zip file path separators must be / + wheel_contents = read_wheel_metadata_file(source, path) + try: wheel_text = ensure_str(wheel_contents) except UnicodeDecodeError as e: From 33043ba22f5608a0746ea4a2ab029be3530d0343 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 1 Jan 2020 20:29:11 -0500 Subject: [PATCH 1043/3170] Use pkg_resources.Distribution derived from wheel directly We now extract all metadata files from the wheel directly into memory and make them available to the wrapping pkg_resources.Distribution via the DictMetadata introduced earlier. --- src/pip/_internal/distributions/wheel.py | 18 +++++-- src/pip/_internal/utils/wheel.py | 65 +++++++++++++++++++++++- tests/functional/test_install_wheel.py | 37 +++++++++++++- tests/lib/__init__.py | 11 ++-- 4 files changed, 121 insertions(+), 10 deletions(-) diff --git a/src/pip/_internal/distributions/wheel.py b/src/pip/_internal/distributions/wheel.py index c609ef4e348..bf3482b151f 100644 --- a/src/pip/_internal/distributions/wheel.py +++ b/src/pip/_internal/distributions/wheel.py @@ -1,7 +1,8 @@ -from pip._vendor import pkg_resources +from zipfile import ZipFile from pip._internal.distributions.base import AbstractDistribution from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from pip._internal.utils.wheel import pkg_resources_distribution_for_wheel if MYPY_CHECK_RUNNING: from pip._vendor.pkg_resources import Distribution @@ -16,8 +17,19 @@ class WheelDistribution(AbstractDistribution): def get_pkg_resources_distribution(self): # type: () -> Distribution - return list(pkg_resources.find_distributions( - self.req.source_dir))[0] + """Loads the metadata from the wheel file into memory and returns a + Distribution that uses it, not relying on the wheel file or + requirement. + """ + # Set as part of preparation during download. + assert self.req.local_file_path + # Wheels are never unnamed. + assert self.req.name + + with ZipFile(self.req.local_file_path, allowZip64=True) as z: + return pkg_resources_distribution_for_wheel( + z, self.req.name, self.req.local_file_path + ) def prepare_distribution_metadata(self, finder, build_isolation): # type: (PackageFinder, bool) -> None diff --git a/src/pip/_internal/utils/wheel.py b/src/pip/_internal/utils/wheel.py index 29d6904adf4..837e0afd7e5 100644 --- a/src/pip/_internal/utils/wheel.py +++ b/src/pip/_internal/utils/wheel.py @@ -8,14 +8,18 @@ from zipfile import ZipFile from pip._vendor.packaging.utils import canonicalize_name +from pip._vendor.pkg_resources import DistInfoDistribution from pip._vendor.six import PY2, ensure_str from pip._internal.exceptions import UnsupportedWheel +from pip._internal.utils.pkg_resources import DictMetadata from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: from email.message import Message - from typing import Tuple + from typing import Dict, Tuple + + from pip._vendor.pkg_resources import Distribution if PY2: from zipfile import BadZipfile as BadZipFile @@ -29,6 +33,65 @@ logger = logging.getLogger(__name__) +class WheelMetadata(DictMetadata): + """Metadata provider that maps metadata decoding exceptions to our + internal exception type. + """ + def __init__(self, metadata, wheel_name): + # type: (Dict[str, bytes], str) -> None + super(WheelMetadata, self).__init__(metadata) + self._wheel_name = wheel_name + + def get_metadata(self, name): + # type: (str) -> str + try: + return super(WheelMetadata, self).get_metadata(name) + except UnicodeDecodeError as e: + # Augment the default error with the origin of the file. + raise UnsupportedWheel( + "Error decoding metadata for {}: {}".format( + self._wheel_name, e + ) + ) + + +def pkg_resources_distribution_for_wheel(wheel_zip, name, location): + # type: (ZipFile, str, str) -> Distribution + """Get a pkg_resources distribution given a wheel. + + :raises UnsupportedWheel: on any errors + """ + info_dir, _ = parse_wheel(wheel_zip, name) + + metadata_files = [ + p for p in wheel_zip.namelist() if p.startswith("{}/".format(info_dir)) + ] + + metadata_text = {} # type: Dict[str, bytes] + for path in metadata_files: + # If a flag is set, namelist entries may be unicode in Python 2. + # We coerce them to native str type to match the types used in the rest + # of the code. This cannot fail because unicode can always be encoded + # with UTF-8. + full_path = ensure_str(path) + _, metadata_name = full_path.split("/", 1) + + try: + metadata_text[metadata_name] = read_wheel_metadata_file( + wheel_zip, full_path + ) + except UnsupportedWheel as e: + raise UnsupportedWheel( + "{} has an invalid wheel, {}".format(name, str(e)) + ) + + metadata = WheelMetadata(metadata_text, location) + + return DistInfoDistribution( + location=location, metadata=metadata, project_name=name + ) + + def parse_wheel(wheel_zip, name): # type: (ZipFile, str) -> Tuple[str, Message] """Extract information from the provided wheel, ensuring it meets basic diff --git a/tests/functional/test_install_wheel.py b/tests/functional/test_install_wheel.py index 1aec5a9acea..9cd90194437 100644 --- a/tests/functional/test_install_wheel.py +++ b/tests/functional/test_install_wheel.py @@ -5,7 +5,7 @@ import pytest -from tests.lib import create_basic_wheel_for_package +from tests.lib import create_basic_wheel_for_package, skip_if_python2 from tests.lib.path import Path @@ -534,3 +534,38 @@ def test_wheel_installs_ok_with_nested_dist_info(script): script.pip( "install", "--no-cache-dir", "--no-index", package ) + + +def test_wheel_installs_ok_with_badly_encoded_irrelevant_dist_info_file( + script +): + package = create_basic_wheel_for_package( + script, + "simple", + "0.1.0", + extra_files={ + "simple-0.1.0.dist-info/AUTHORS.txt": b"\xff" + }, + ) + script.pip( + "install", "--no-cache-dir", "--no-index", package + ) + + +# Metadata is not decoded on Python 2. +@skip_if_python2 +def test_wheel_install_fails_with_badly_encoded_metadata(script): + package = create_basic_wheel_for_package( + script, + "simple", + "0.1.0", + extra_files={ + "simple-0.1.0.dist-info/METADATA": b"\xff" + }, + ) + result = script.pip( + "install", "--no-cache-dir", "--no-index", package, expect_error=True + ) + assert "Error decoding metadata for" in result.stderr + assert "simple-0.1.0-py2.py3-none-any.whl" in result.stderr + assert "METADATA" in result.stderr diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index 86518b90ab8..10c9e47d67b 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -15,7 +15,7 @@ from zipfile import ZipFile import pytest -from pip._vendor.six import PY2 +from pip._vendor.six import PY2, ensure_binary from scripttest import FoundDir, TestFileEnvironment from pip._internal.index.collector import LinkCollector @@ -1018,9 +1018,6 @@ def hello(): "{dist_info}/RECORD": "" } - if extra_files: - files.update(extra_files) - # Some useful shorthands archive_name = "{name}-{version}-py2.py3-none-any.whl".format( name=name, version=version @@ -1046,10 +1043,14 @@ def hello(): name=name, version=version, requires_dist=requires_dist ).strip() + # Add new files after formatting + if extra_files: + files.update(extra_files) + for fname in files: path = script.temp_path / fname path.parent.mkdir(exist_ok=True, parents=True) - path.write_text(files[fname]) + path.write_bytes(ensure_binary(files[fname])) retval = script.scratch_path / archive_name generated = shutil.make_archive(retval, 'zip', script.temp_path) From a94fb53dad7ab7be6e5fd1e267e3c686027f6cd5 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 1 Jan 2020 20:32:54 -0500 Subject: [PATCH 1044/3170] Don't unpack wheel files after building for install Actual installation has been using the wheel file directly for some time. The last piece that required an unpacked wheel was metadata. Now that it uses the wheel file directly, we can remove the unpacking after build. --- src/pip/_internal/wheel_builder.py | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index 35e4f9eff0c..9015795b9b9 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -13,13 +13,11 @@ from pip._internal.operations.build.wheel import build_wheel_pep517 from pip._internal.operations.build.wheel_legacy import build_wheel_legacy from pip._internal.utils.logging import indent_log -from pip._internal.utils.marker_files import has_delete_marker_file from pip._internal.utils.misc import ensure_dir, hash_file from pip._internal.utils.setuptools_build import make_setuptools_clean_args from pip._internal.utils.subprocess import call_subprocess from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from pip._internal.utils.unpacking import unpack_file from pip._internal.utils.urls import path_to_url from pip._internal.vcs import vcs @@ -313,27 +311,6 @@ def build( req.link = Link(path_to_url(wheel_file)) req.local_file_path = req.link.file_path assert req.link.is_wheel - if should_unpack: - # XXX: This is mildly duplicative with prepare_files, - # but not close enough to pull out to a single common - # method. - # The code below assumes temporary source dirs - - # prevent it doing bad things. - if ( - req.source_dir and - not has_delete_marker_file(req.source_dir) - ): - raise AssertionError( - "bad source dir - missing marker") - # Delete the source we built the wheel from - req.remove_temporary_source() - # set the build directory again - name is known from - # the work prepare_files did. - req.source_dir = req.ensure_build_location( - self.preparer.build_dir - ) - # extract the wheel into the dir - unpack_file(req.link.file_path, req.source_dir) build_successes.append(req) else: build_failures.append(req) From 8784fb419a857a797397aa2e1d025f7ce0f52060 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Sun, 29 Dec 2019 16:44:06 +0100 Subject: [PATCH 1045/3170] Make build_wheels return successes too In preparation for moving the unpacking out of WheelBuilder.build --- src/pip/_internal/commands/install.py | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 0f39b90d812..638c644f0e9 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -52,7 +52,7 @@ from pip._internal.models.format_control import FormatControl from pip._internal.req.req_install import InstallRequirement - from pip._internal.wheel_builder import BinaryAllowedPredicate + from pip._internal.wheel_builder import BinaryAllowedPredicate, BuildResult logger = logging.getLogger(__name__) @@ -79,15 +79,17 @@ def build_wheels( global_options, # type: List[str] check_binary_allowed, # type: BinaryAllowedPredicate ): - # type: (...) -> List[InstallRequirement] + # type: (...) -> BuildResult """ Build wheels for requirements, depending on whether wheel is installed. + + Return failures only for PEP 517 requirements. """ # We don't build wheels for legacy requirements if wheel is not installed. should_build_legacy = is_wheel_installed() # Always build PEP 517 requirements - _, build_failures = builder.build( + pep517_build_successes, build_failures = builder.build( pep517_requirements, should_unpack=True, wheel_cache=wheel_cache, @@ -100,7 +102,7 @@ def build_wheels( # We don't care about failures building legacy # requirements, as we'll fall through to a direct # install for those. - builder.build( + legacy_build_successes, _ = builder.build( legacy_requirements, should_unpack=True, wheel_cache=wheel_cache, @@ -109,7 +111,7 @@ def build_wheels( check_binary_allowed=check_binary_allowed, ) - return build_failures + return pep517_build_successes + legacy_build_successes, build_failures def get_check_binary_allowed(format_control): @@ -409,7 +411,7 @@ def run(self, options, args): legacy_requirements.append(req) wheel_builder = WheelBuilder(preparer) - build_failures = build_wheels( + _, build_failures = build_wheels( builder=wheel_builder, pep517_requirements=pep517_requirements, legacy_requirements=legacy_requirements, From 8601bb5d0678ad2a1a1fe91e4b0e3f6c6e028d9c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Sun, 29 Dec 2019 17:54:18 +0100 Subject: [PATCH 1046/3170] Make build_wheels return all failures Check for PEP 517 build failures by filtering failures. --- src/pip/_internal/commands/install.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 638c644f0e9..4b5836f482f 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -82,14 +82,12 @@ def build_wheels( # type: (...) -> BuildResult """ Build wheels for requirements, depending on whether wheel is installed. - - Return failures only for PEP 517 requirements. """ # We don't build wheels for legacy requirements if wheel is not installed. should_build_legacy = is_wheel_installed() # Always build PEP 517 requirements - pep517_build_successes, build_failures = builder.build( + pep517_build_successes, pep517_build_failures = builder.build( pep517_requirements, should_unpack=True, wheel_cache=wheel_cache, @@ -102,7 +100,7 @@ def build_wheels( # We don't care about failures building legacy # requirements, as we'll fall through to a direct # install for those. - legacy_build_successes, _ = builder.build( + legacy_build_successes, legacy_build_failures = builder.build( legacy_requirements, should_unpack=True, wheel_cache=wheel_cache, @@ -111,7 +109,10 @@ def build_wheels( check_binary_allowed=check_binary_allowed, ) - return pep517_build_successes + legacy_build_successes, build_failures + return ( + pep517_build_successes + legacy_build_successes, + pep517_build_failures + legacy_build_failures, + ) def get_check_binary_allowed(format_control): @@ -423,11 +424,14 @@ def run(self, options, args): # If we're using PEP 517, we cannot do a direct install # so we fail here. - if build_failures: + pep517_build_failures = [ + r for r in build_failures if r.use_pep517 + ] + if pep517_build_failures: raise InstallationError( "Could not build wheels for {} which use" " PEP 517 and cannot be installed directly".format( - ", ".join(r.name for r in build_failures))) + ", ".join(r.name for r in pep517_build_failures))) to_install = resolver.get_installation_order( requirement_set From 10ff58d7beb318aa3931f16bd23d90b345a5ad63 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Sun, 29 Dec 2019 18:07:49 +0100 Subject: [PATCH 1047/3170] Simplify install by calling build once We filter what to build beforehand so we can call build once. We then filter failures to detect PEP 517 failures. --- src/pip/_internal/commands/install.py | 28 +++++++++++++++------------ 1 file changed, 16 insertions(+), 12 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 4b5836f482f..2cbdf3a641f 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -402,20 +402,21 @@ def run(self, options, args): check_binary_allowed = get_check_binary_allowed( finder.format_control ) - # Consider legacy and PEP517-using requirements separately - legacy_requirements = [] - pep517_requirements = [] - for req in requirement_set.requirements.values(): - if req.use_pep517: - pep517_requirements.append(req) - else: - legacy_requirements.append(req) + + if is_wheel_installed(): + reqs_to_build = list(requirement_set.requirements.values()) + else: + # We don't build wheels for legacy requirements + # if wheel is not installed. + reqs_to_build = [ + r for r in requirement_set.requirements.values() + if r.use_pep517 + ] wheel_builder = WheelBuilder(preparer) - _, build_failures = build_wheels( - builder=wheel_builder, - pep517_requirements=pep517_requirements, - legacy_requirements=legacy_requirements, + _, build_failures = wheel_builder.build( + reqs_to_build, + should_unpack=True, wheel_cache=wheel_cache, build_options=[], global_options=[], @@ -424,6 +425,9 @@ def run(self, options, args): # If we're using PEP 517, we cannot do a direct install # so we fail here. + # We don't care about failures building legacy + # requirements, as we'll fall through to a direct + # install for those. pep517_build_failures = [ r for r in build_failures if r.use_pep517 ] From 4bdca1a09ad93b2dc18a860c8f64558185e2d243 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Sun, 29 Dec 2019 18:10:42 +0100 Subject: [PATCH 1048/3170] Remove now useless build_wheels function --- src/pip/_internal/commands/install.py | 47 +------------------ tests/unit/test_command_install.py | 65 +-------------------------- 2 files changed, 2 insertions(+), 110 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 2cbdf3a641f..221d3caf9a0 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -52,7 +52,7 @@ from pip._internal.models.format_control import FormatControl from pip._internal.req.req_install import InstallRequirement - from pip._internal.wheel_builder import BinaryAllowedPredicate, BuildResult + from pip._internal.wheel_builder import BinaryAllowedPredicate logger = logging.getLogger(__name__) @@ -70,51 +70,6 @@ def is_wheel_installed(): return True -def build_wheels( - builder, # type: WheelBuilder - pep517_requirements, # type: List[InstallRequirement] - legacy_requirements, # type: List[InstallRequirement] - wheel_cache, # type: WheelCache - build_options, # type: List[str] - global_options, # type: List[str] - check_binary_allowed, # type: BinaryAllowedPredicate -): - # type: (...) -> BuildResult - """ - Build wheels for requirements, depending on whether wheel is installed. - """ - # We don't build wheels for legacy requirements if wheel is not installed. - should_build_legacy = is_wheel_installed() - - # Always build PEP 517 requirements - pep517_build_successes, pep517_build_failures = builder.build( - pep517_requirements, - should_unpack=True, - wheel_cache=wheel_cache, - build_options=build_options, - global_options=global_options, - check_binary_allowed=check_binary_allowed, - ) - - if should_build_legacy: - # We don't care about failures building legacy - # requirements, as we'll fall through to a direct - # install for those. - legacy_build_successes, legacy_build_failures = builder.build( - legacy_requirements, - should_unpack=True, - wheel_cache=wheel_cache, - build_options=build_options, - global_options=global_options, - check_binary_allowed=check_binary_allowed, - ) - - return ( - pep517_build_successes + legacy_build_successes, - pep517_build_failures + legacy_build_failures, - ) - - def get_check_binary_allowed(format_control): # type: (FormatControl) -> BinaryAllowedPredicate def check_binary_allowed(req): diff --git a/tests/unit/test_command_install.py b/tests/unit/test_command_install.py index 36b3ca73fe6..aee03b77a4a 100644 --- a/tests/unit/test_command_install.py +++ b/tests/unit/test_command_install.py @@ -1,11 +1,10 @@ import errno import pytest -from mock import Mock, patch +from mock import patch from pip._vendor.packaging.requirements import Requirement from pip._internal.commands.install import ( - build_wheels, create_env_error_message, decide_user_install, warn_deprecated_install_options, @@ -14,68 +13,6 @@ from pip._internal.req.req_set import RequirementSet -class TestWheelCache: - - def check_build_wheels( - self, - pep517_requirements, - legacy_requirements, - ): - """ - Return: (mock_calls, return_value). - """ - built_reqs = [] - - def build(reqs, **kwargs): - # Fail the first requirement. - built_reqs.append(reqs) - return ([], [reqs[0]]) - - builder = Mock() - builder.build.side_effect = build - - build_failures = build_wheels( - builder=builder, - pep517_requirements=pep517_requirements, - legacy_requirements=legacy_requirements, - wheel_cache=Mock(cache_dir=None), - build_options=[], - global_options=[], - check_binary_allowed=None, - ) - - return (built_reqs, build_failures) - - @patch('pip._internal.commands.install.is_wheel_installed') - def test_build_wheels__wheel_installed(self, is_wheel_installed): - is_wheel_installed.return_value = True - - built_reqs, build_failures = self.check_build_wheels( - pep517_requirements=['a', 'b'], - legacy_requirements=['c', 'd'], - ) - - # Legacy requirements were built. - assert built_reqs == [['a', 'b'], ['c', 'd']] - - # Legacy build failures are not included in the return value. - assert build_failures == ['a'] - - @patch('pip._internal.commands.install.is_wheel_installed') - def test_build_wheels__wheel_not_installed(self, is_wheel_installed): - is_wheel_installed.return_value = False - - built_reqs, build_failures = self.check_build_wheels( - pep517_requirements=['a', 'b'], - legacy_requirements=['c', 'd'], - ) - - # Legacy requirements were not built. - assert built_reqs == [['a', 'b']] - - assert build_failures == ['a'] - - class TestDecideUserInstall: @patch('site.ENABLE_USER_SITE', True) @patch('pip._internal.commands.install.site_packages_writable') From 3de4765ec7fbd35c56a0f6bae9f761bc70ed4483 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Mon, 30 Dec 2019 10:04:30 +0100 Subject: [PATCH 1049/3170] Add should_build function for wheel and install commands --- src/pip/_internal/wheel_builder.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index 9015795b9b9..d80f982fc5b 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -82,6 +82,25 @@ def should_build( return True +def should_build_for_wheel_command( + req, # type: InstallRequirement +): + # type: (...) -> Optional[bool] + return should_build( + req, need_wheel=True, check_binary_allowed=_always_true + ) + + +def should_build_for_install_command( + req, # type: InstallRequirement + check_binary_allowed, # type: BinaryAllowedPredicate +): + # type: (...) -> Optional[bool] + return should_build( + req, need_wheel=False, check_binary_allowed=check_binary_allowed + ) + + def should_cache( req, # type: InstallRequirement check_binary_allowed, # type: BinaryAllowedPredicate From e45005f4bbf69110e49534113e331c0364f5c847 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Mon, 30 Dec 2019 10:08:26 +0100 Subject: [PATCH 1050/3170] Filter requirements to build beforehand in wheel command One more step so build() becomes only concerned with building. --- src/pip/_internal/commands/wheel.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index e376440b75e..13a4a915294 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -18,7 +18,10 @@ from pip._internal.utils.misc import ensure_dir, normalize_path from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from pip._internal.wheel_builder import WheelBuilder +from pip._internal.wheel_builder import ( + WheelBuilder, + should_build_for_wheel_command, +) if MYPY_CHECK_RUNNING: from optparse import Values @@ -160,10 +163,15 @@ def run(self, options, args): resolver.resolve(requirement_set) + reqs_to_build = [ + r for r in requirement_set.requirements.values() + if should_build_for_wheel_command(r) + ] + # build wheels wb = WheelBuilder(preparer) build_successes, build_failures = wb.build( - requirement_set.requirements.values(), + reqs_to_build, should_unpack=False, wheel_cache=wheel_cache, build_options=options.build_options or [], From 3ae13f7d28f24d63d826197f7b57c67a80ae1be8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Thu, 26 Dec 2019 18:19:06 +0100 Subject: [PATCH 1051/3170] Move is_wheel_installed to utils --- src/pip/_internal/commands/install.py | 13 +------------ src/pip/_internal/utils/misc.py | 12 ++++++++++++ 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 221d3caf9a0..52198bb2528 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -38,6 +38,7 @@ from pip._internal.utils.misc import ( ensure_dir, get_installed_version, + is_wheel_installed, protect_pip_from_modification_on_windows, write_output, ) @@ -58,18 +59,6 @@ logger = logging.getLogger(__name__) -def is_wheel_installed(): - """ - Return whether the wheel package is installed. - """ - try: - import wheel # noqa: F401 - except ImportError: - return False - - return True - - def get_check_binary_allowed(format_control): # type: (FormatControl) -> BinaryAllowedPredicate def check_binary_allowed(req): diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index a53ce0dc9f9..19a96290e25 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -878,3 +878,15 @@ def hash_file(path, blocksize=1 << 20): length += len(block) h.update(block) return h, length + + +def is_wheel_installed(): + """ + Return whether the wheel package is installed. + """ + try: + import wheel # noqa: F401 + except ImportError: + return False + + return True From 8ca8e9bf61f88ac2b7a45a025b04a5387b3a8d5d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Thu, 26 Dec 2019 18:24:46 +0100 Subject: [PATCH 1052/3170] Extend should_build scope wrt legacy requirements We don't build legacy requirements when wheel is not installed because we'll fallback to a legacy install in such case. --- src/pip/_internal/wheel_builder.py | 9 ++++++++- tests/unit/test_wheel_builder.py | 28 +++++++++++++++++++++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index d80f982fc5b..423d3b7268d 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -13,7 +13,7 @@ from pip._internal.operations.build.wheel import build_wheel_pep517 from pip._internal.operations.build.wheel_legacy import build_wheel_legacy from pip._internal.utils.logging import indent_log -from pip._internal.utils.misc import ensure_dir, hash_file +from pip._internal.utils.misc import ensure_dir, hash_file, is_wheel_installed from pip._internal.utils.setuptools_build import make_setuptools_clean_args from pip._internal.utils.subprocess import call_subprocess from pip._internal.utils.temp_dir import TempDirectory @@ -69,6 +69,13 @@ def should_build( # i.e. pip wheel, not pip install return True + # From this point, this concerns the pip install command only + # (need_wheel=False). + + if not req.use_pep517 and not is_wheel_installed(): + # we don't build legacy requirements if wheel is not installed + return False + if req.editable or not req.source_dir: return False diff --git a/tests/unit/test_wheel_builder.py b/tests/unit/test_wheel_builder.py index 9d2c803039e..87250d0b493 100644 --- a/tests/unit/test_wheel_builder.py +++ b/tests/unit/test_wheel_builder.py @@ -1,7 +1,7 @@ import logging import pytest -from mock import Mock +from mock import Mock, patch from pip._internal import wheel_builder from pip._internal.models.link import Link @@ -39,6 +39,7 @@ def __init__( link=None, constraint=False, source_dir="/tmp/pip-install-123/pendulum", + use_pep517=True, ): self.name = name self.is_wheel = is_wheel @@ -46,6 +47,7 @@ def __init__( self.link = link self.constraint = constraint self.source_dir = source_dir + self.use_pep517 = use_pep517 @pytest.mark.parametrize( @@ -83,6 +85,30 @@ def test_should_build(req, need_wheel, disallow_binaries, expected): assert should_build is expected +@patch("pip._internal.wheel_builder.is_wheel_installed") +def test_should_build_legacy_wheel_not_installed(is_wheel_installed): + is_wheel_installed.return_value = False + legacy_req = ReqMock(use_pep517=False) + should_build = wheel_builder.should_build( + legacy_req, + need_wheel=False, + check_binary_allowed=lambda req: True, + ) + assert not should_build + + +@patch("pip._internal.wheel_builder.is_wheel_installed") +def test_should_build_legacy_wheel_installed(is_wheel_installed): + is_wheel_installed.return_value = True + legacy_req = ReqMock(use_pep517=False) + should_build = wheel_builder.should_build( + legacy_req, + need_wheel=False, + check_binary_allowed=lambda req: True, + ) + assert should_build + + @pytest.mark.parametrize( "req, disallow_binaries, expected", [ From 212ee12474bc88deffb90c300074e05c215d37d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Mon, 30 Dec 2019 10:20:12 +0100 Subject: [PATCH 1053/3170] Filter requirements to build beforehand in install command One more step so build() becomes only concerned with building. --- src/pip/_internal/commands/install.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 52198bb2528..34f6fc7f935 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -38,14 +38,16 @@ from pip._internal.utils.misc import ( ensure_dir, get_installed_version, - is_wheel_installed, protect_pip_from_modification_on_windows, write_output, ) from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.virtualenv import virtualenv_no_global -from pip._internal.wheel_builder import WheelBuilder +from pip._internal.wheel_builder import ( + WheelBuilder, + should_build_for_install_command, +) if MYPY_CHECK_RUNNING: from optparse import Values @@ -347,15 +349,12 @@ def run(self, options, args): finder.format_control ) - if is_wheel_installed(): - reqs_to_build = list(requirement_set.requirements.values()) - else: - # We don't build wheels for legacy requirements - # if wheel is not installed. - reqs_to_build = [ - r for r in requirement_set.requirements.values() - if r.use_pep517 - ] + reqs_to_build = [ + r for r in requirement_set.requirements.values() + if should_build_for_install_command( + r, check_binary_allowed + ) + ] wheel_builder = WheelBuilder(preparer) _, build_failures = wheel_builder.build( From 870106b9bb45afde8c69e5cfcd8339de304560b0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Mon, 30 Dec 2019 10:24:41 +0100 Subject: [PATCH 1054/3170] Simplify _collect_buildset Since filtering of what to build has been done beforehand, there is no need to filter again here anymore. --- src/pip/_internal/wheel_builder.py | 10 +--------- tests/unit/test_wheel_builder.py | 22 +--------------------- 2 files changed, 2 insertions(+), 30 deletions(-) diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index 423d3b7268d..d336f948c7e 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -152,22 +152,15 @@ def _collect_buildset( requirements, # type: Iterable[InstallRequirement] wheel_cache, # type: WheelCache check_binary_allowed, # type: BinaryAllowedPredicate - need_wheel, # type: bool ): # type: (...) -> List[Tuple[InstallRequirement, str]] - """Return the list of InstallRequirement that need to be built, + """Return the list of InstallRequirement, with the persistent or temporary cache directory where the built wheel needs to be stored. """ buildset = [] cache_available = bool(wheel_cache.cache_dir) for req in requirements: - if not should_build( - req, - need_wheel=need_wheel, - check_binary_allowed=check_binary_allowed, - ): - continue if ( cache_available and should_cache(req, check_binary_allowed) @@ -312,7 +305,6 @@ def build( requirements, wheel_cache=wheel_cache, check_binary_allowed=check_binary_allowed, - need_wheel=not should_unpack, ) if not buildset: return [], [] diff --git a/tests/unit/test_wheel_builder.py b/tests/unit/test_wheel_builder.py index 87250d0b493..14d40111d19 100644 --- a/tests/unit/test_wheel_builder.py +++ b/tests/unit/test_wheel_builder.py @@ -1,7 +1,7 @@ import logging import pytest -from mock import Mock, patch +from mock import patch from pip._internal import wheel_builder from pip._internal.models.link import Link @@ -207,23 +207,3 @@ def test_format_command_result__empty_output(caplog, log_level): "Command arguments: arg1 arg2", 'Command output: None', ] - - -class TestWheelBuilder(object): - - def test_skip_building_wheels(self, caplog): - wb = wheel_builder.WheelBuilder(preparer=Mock()) - wb._build_one = mock_build_one = Mock() - - wheel_req = Mock(is_wheel=True, editable=False, constraint=False) - with caplog.at_level(logging.INFO): - wb.build( - [wheel_req], - should_unpack=False, - wheel_cache=Mock(cache_dir=None), - build_options=[], - global_options=[], - ) - - assert "due to already being wheel" in caplog.text - assert mock_build_one.mock_calls == [] From 6e7d0e5a05c12712f48a858e6e961febd3616ec3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Mon, 30 Dec 2019 10:29:34 +0100 Subject: [PATCH 1055/3170] _collect_buildset becomes _get_cache_dir The only purpose of _collect_buildset is now to compute the cache directory to use for a given requirements. This is better computed one by one in the build loop. --- src/pip/_internal/wheel_builder.py | 44 +++++++++++++----------------- 1 file changed, 19 insertions(+), 25 deletions(-) diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index d336f948c7e..7cb46e84abb 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -148,28 +148,24 @@ def should_cache( return False -def _collect_buildset( - requirements, # type: Iterable[InstallRequirement] +def _get_cache_dir( + req, # type: InstallRequirement wheel_cache, # type: WheelCache check_binary_allowed, # type: BinaryAllowedPredicate ): - # type: (...) -> List[Tuple[InstallRequirement, str]] - """Return the list of InstallRequirement, - with the persistent or temporary cache directory where the built - wheel needs to be stored. + # type: (...) -> str + """Return the persistent or temporary cache directory where the built + wheel need to be stored. """ - buildset = [] cache_available = bool(wheel_cache.cache_dir) - for req in requirements: - if ( - cache_available and - should_cache(req, check_binary_allowed) - ): - cache_dir = wheel_cache.get_path_for_link(req.link) - else: - cache_dir = wheel_cache.get_ephem_path_for_link(req.link) - buildset.append((req, cache_dir)) - return buildset + if ( + cache_available and + should_cache(req, check_binary_allowed) + ): + cache_dir = wheel_cache.get_path_for_link(req.link) + else: + cache_dir = wheel_cache.get_ephem_path_for_link(req.link) + return cache_dir def _always_true(_): @@ -301,12 +297,7 @@ def build( # Binaries allowed by default. check_binary_allowed = _always_true - buildset = _collect_buildset( - requirements, - wheel_cache=wheel_cache, - check_binary_allowed=check_binary_allowed, - ) - if not buildset: + if not requirements: return [], [] # TODO by @pradyunsg @@ -315,12 +306,15 @@ def build( # Build the wheels. logger.info( 'Building wheels for collected packages: %s', - ', '.join([req.name for (req, _) in buildset]), + ', '.join(req.name for req in requirements), ) with indent_log(): build_successes, build_failures = [], [] - for req, cache_dir in buildset: + for req in requirements: + cache_dir = _get_cache_dir( + req, wheel_cache, check_binary_allowed + ) wheel_file = _build_one( req, cache_dir, build_options, global_options ) From 25521f29b5945b646fad76f8741f1e967a55d871 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Mon, 30 Dec 2019 10:38:58 +0100 Subject: [PATCH 1056/3170] should_build always returns a boolean --- src/pip/_internal/wheel_builder.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index 7cb46e84abb..0c676455f5f 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -53,7 +53,7 @@ def should_build( need_wheel, # type: bool check_binary_allowed, # type: BinaryAllowedPredicate ): - # type: (...) -> Optional[bool] + # type: (...) -> bool """Return whether an InstallRequirement should be built into a wheel.""" if req.constraint: # never build requirements that are merely constraints @@ -92,7 +92,7 @@ def should_build( def should_build_for_wheel_command( req, # type: InstallRequirement ): - # type: (...) -> Optional[bool] + # type: (...) -> bool return should_build( req, need_wheel=True, check_binary_allowed=_always_true ) @@ -102,7 +102,7 @@ def should_build_for_install_command( req, # type: InstallRequirement check_binary_allowed, # type: BinaryAllowedPredicate ): - # type: (...) -> Optional[bool] + # type: (...) -> bool return should_build( req, need_wheel=False, check_binary_allowed=check_binary_allowed ) From 68179ea0ded8a8ca466518dbaf064d8ca434f19d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Mon, 30 Dec 2019 19:31:41 +0100 Subject: [PATCH 1057/3170] Add .trivial newsfile --- news/7527.trivial | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 news/7527.trivial diff --git a/news/7527.trivial b/news/7527.trivial new file mode 100644 index 00000000000..e69de29bb2d From c0aca1212376e20bb1c022a3a33aabf89d489304 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Tue, 31 Dec 2019 05:22:59 +0100 Subject: [PATCH 1058/3170] Clarify should_cache a bit --- src/pip/_internal/wheel_builder.py | 18 +++++++----------- 1 file changed, 7 insertions(+), 11 deletions(-) diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index 0c676455f5f..5899da2f0ab 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -118,17 +118,16 @@ def should_cache( wheel cache, assuming the wheel cache is available, and should_build() has determined a wheel needs to be built. """ - if not should_build( - req, need_wheel=False, check_binary_allowed=check_binary_allowed + if not should_build_for_install_command( + req, check_binary_allowed=check_binary_allowed ): - # never cache if pip install (need_wheel=False) would not have built + # never cache if pip install would not have built # (editable mode, etc) return False if req.link and req.link.is_vcs: - # VCS checkout. Build wheel just for this run - # unless it points to an immutable commit hash in which - # case it can be cached. + # VCS checkout. Do not cache + # unless it points to an immutable commit hash. assert not req.editable assert req.source_dir vcs_backend = vcs.get_backend_for_scheme(req.link.scheme) @@ -137,14 +136,11 @@ def should_cache( return True return False - link = req.link - base, ext = link.splitext() + base, ext = req.link.splitext() if _contains_egg_info(base): return True - # Otherwise, build the wheel just for this run using the ephemeral - # cache since we are either in the case of e.g. a local directory, or - # no cache directory is available to use. + # Otherwise, do not cache. return False From 9729273ca82e97b4c90719be44b094f1cddb2915 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Wed, 1 Jan 2020 12:42:22 +0100 Subject: [PATCH 1059/3170] Make should_build and should_cache "private" --- src/pip/_internal/wheel_builder.py | 12 ++++++------ tests/unit/test_wheel_builder.py | 14 +++++++------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index 5899da2f0ab..c29f17bee23 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -48,7 +48,7 @@ def _contains_egg_info( return bool(_egg_info_re.search(s)) -def should_build( +def _should_build( req, # type: InstallRequirement need_wheel, # type: bool check_binary_allowed, # type: BinaryAllowedPredicate @@ -93,7 +93,7 @@ def should_build_for_wheel_command( req, # type: InstallRequirement ): # type: (...) -> bool - return should_build( + return _should_build( req, need_wheel=True, check_binary_allowed=_always_true ) @@ -103,19 +103,19 @@ def should_build_for_install_command( check_binary_allowed, # type: BinaryAllowedPredicate ): # type: (...) -> bool - return should_build( + return _should_build( req, need_wheel=False, check_binary_allowed=check_binary_allowed ) -def should_cache( +def _should_cache( req, # type: InstallRequirement check_binary_allowed, # type: BinaryAllowedPredicate ): # type: (...) -> Optional[bool] """ Return whether a built InstallRequirement can be stored in the persistent - wheel cache, assuming the wheel cache is available, and should_build() + wheel cache, assuming the wheel cache is available, and _should_build() has determined a wheel needs to be built. """ if not should_build_for_install_command( @@ -156,7 +156,7 @@ def _get_cache_dir( cache_available = bool(wheel_cache.cache_dir) if ( cache_available and - should_cache(req, check_binary_allowed) + _should_cache(req, check_binary_allowed) ): cache_dir = wheel_cache.get_path_for_link(req.link) else: diff --git a/tests/unit/test_wheel_builder.py b/tests/unit/test_wheel_builder.py index 14d40111d19..fd788fbc08a 100644 --- a/tests/unit/test_wheel_builder.py +++ b/tests/unit/test_wheel_builder.py @@ -77,7 +77,7 @@ def __init__( ], ) def test_should_build(req, need_wheel, disallow_binaries, expected): - should_build = wheel_builder.should_build( + should_build = wheel_builder._should_build( req, need_wheel, check_binary_allowed=lambda req: not disallow_binaries, @@ -89,7 +89,7 @@ def test_should_build(req, need_wheel, disallow_binaries, expected): def test_should_build_legacy_wheel_not_installed(is_wheel_installed): is_wheel_installed.return_value = False legacy_req = ReqMock(use_pep517=False) - should_build = wheel_builder.should_build( + should_build = wheel_builder._should_build( legacy_req, need_wheel=False, check_binary_allowed=lambda req: True, @@ -101,7 +101,7 @@ def test_should_build_legacy_wheel_not_installed(is_wheel_installed): def test_should_build_legacy_wheel_installed(is_wheel_installed): is_wheel_installed.return_value = True legacy_req = ReqMock(use_pep517=False) - should_build = wheel_builder.should_build( + should_build = wheel_builder._should_build( legacy_req, need_wheel=False, check_binary_allowed=lambda req: True, @@ -130,10 +130,10 @@ def test_should_cache( def check_binary_allowed(req): return not disallow_binaries - should_cache = wheel_builder.should_cache( + should_cache = wheel_builder._should_cache( req, check_binary_allowed ) - if not wheel_builder.should_build( + if not wheel_builder._should_build( req, need_wheel=False, check_binary_allowed=check_binary_allowed ): # never cache if pip install (need_wheel=False) would not have built) @@ -150,14 +150,14 @@ def test_should_cache_git_sha(script, tmpdir): # a link referencing a sha should be cached url = "git+https://g.c/o/r@" + commit + "#egg=mypkg" req = ReqMock(link=Link(url), source_dir=repo_path) - assert wheel_builder.should_cache( + assert wheel_builder._should_cache( req, check_binary_allowed=lambda r: True, ) # a link not referencing a sha should not be cached url = "git+https://g.c/o/r@master#egg=mypkg" req = ReqMock(link=Link(url), source_dir=repo_path) - assert not wheel_builder.should_cache( + assert not wheel_builder._should_cache( req, check_binary_allowed=lambda r: True, ) From f04e6ab7b5a8a47a5d109663d01c57b2684082e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Wed, 1 Jan 2020 15:53:46 +0100 Subject: [PATCH 1060/3170] Split should_build test Separately test should_build_for_wheel_command and should_buid_for_install_command. The tests are simpler and this open the door for reasoning about both functions independently. --- tests/unit/test_wheel_builder.py | 61 +++++++++++++++++--------------- 1 file changed, 32 insertions(+), 29 deletions(-) diff --git a/tests/unit/test_wheel_builder.py b/tests/unit/test_wheel_builder.py index fd788fbc08a..7d70e8dcb49 100644 --- a/tests/unit/test_wheel_builder.py +++ b/tests/unit/test_wheel_builder.py @@ -51,47 +51,51 @@ def __init__( @pytest.mark.parametrize( - "req, need_wheel, disallow_binaries, expected", + "req, disallow_binaries, expected", [ - # pip wheel (need_wheel=True) - (ReqMock(), True, False, True), - (ReqMock(), True, True, True), - (ReqMock(constraint=True), True, False, False), - (ReqMock(is_wheel=True), True, False, False), - (ReqMock(editable=True), True, False, True), - (ReqMock(source_dir=None), True, False, True), - (ReqMock(link=Link("git+https://g.c/org/repo")), True, False, True), - (ReqMock(link=Link("git+https://g.c/org/repo")), True, True, True), - # pip install (need_wheel=False) - (ReqMock(), False, False, True), - (ReqMock(), False, True, False), - (ReqMock(constraint=True), False, False, False), - (ReqMock(is_wheel=True), False, False, False), - (ReqMock(editable=True), False, False, False), - (ReqMock(source_dir=None), False, False, False), + (ReqMock(), False, True), + (ReqMock(), True, False), + (ReqMock(constraint=True), False, False), + (ReqMock(is_wheel=True), False, False), + (ReqMock(editable=True), False, False), + (ReqMock(source_dir=None), False, False), # By default (i.e. when binaries are allowed), VCS requirements # should be built in install mode. - (ReqMock(link=Link("git+https://g.c/org/repo")), False, False, True), + (ReqMock(link=Link("git+https://g.c/org/repo")), False, True), # Disallowing binaries, however, should cause them not to be built. - (ReqMock(link=Link("git+https://g.c/org/repo")), False, True, False), + (ReqMock(link=Link("git+https://g.c/org/repo")), True, False), ], ) -def test_should_build(req, need_wheel, disallow_binaries, expected): - should_build = wheel_builder._should_build( +def test_should_build_for_install_command(req, disallow_binaries, expected): + should_build = wheel_builder.should_build_for_install_command( req, - need_wheel, check_binary_allowed=lambda req: not disallow_binaries, ) assert should_build is expected +@pytest.mark.parametrize( + "req, expected", + [ + (ReqMock(), True), + (ReqMock(constraint=True), False), + (ReqMock(is_wheel=True), False), + (ReqMock(editable=True), True), + (ReqMock(source_dir=None), True), + (ReqMock(link=Link("git+https://g.c/org/repo")), True), + ], +) +def test_should_build_for_wheel_command(req, expected): + should_build = wheel_builder.should_build_for_wheel_command(req) + assert should_build is expected + + @patch("pip._internal.wheel_builder.is_wheel_installed") def test_should_build_legacy_wheel_not_installed(is_wheel_installed): is_wheel_installed.return_value = False legacy_req = ReqMock(use_pep517=False) - should_build = wheel_builder._should_build( + should_build = wheel_builder.should_build_for_install_command( legacy_req, - need_wheel=False, check_binary_allowed=lambda req: True, ) assert not should_build @@ -101,9 +105,8 @@ def test_should_build_legacy_wheel_not_installed(is_wheel_installed): def test_should_build_legacy_wheel_installed(is_wheel_installed): is_wheel_installed.return_value = True legacy_req = ReqMock(use_pep517=False) - should_build = wheel_builder._should_build( + should_build = wheel_builder.should_build_for_install_command( legacy_req, - need_wheel=False, check_binary_allowed=lambda req: True, ) assert should_build @@ -133,10 +136,10 @@ def check_binary_allowed(req): should_cache = wheel_builder._should_cache( req, check_binary_allowed ) - if not wheel_builder._should_build( - req, need_wheel=False, check_binary_allowed=check_binary_allowed + if not wheel_builder.should_build_for_install_command( + req, check_binary_allowed=check_binary_allowed ): - # never cache if pip install (need_wheel=False) would not have built) + # never cache if pip install would not have built) assert not should_cache assert should_cache is expected From 66e010980a11cfff417a29a339cae35dde2576ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Fri, 3 Jan 2020 12:46:01 +0100 Subject: [PATCH 1061/3170] Remove WheelBuilder build is now a function --- src/pip/_internal/commands/install.py | 8 +- src/pip/_internal/commands/wheel.py | 8 +- src/pip/_internal/wheel_builder.py | 134 ++++++++++++-------------- 3 files changed, 64 insertions(+), 86 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 34f6fc7f935..7784d86e549 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -44,10 +44,7 @@ from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.virtualenv import virtualenv_no_global -from pip._internal.wheel_builder import ( - WheelBuilder, - should_build_for_install_command, -) +from pip._internal.wheel_builder import build, should_build_for_install_command if MYPY_CHECK_RUNNING: from optparse import Values @@ -356,8 +353,7 @@ def run(self, options, args): ) ] - wheel_builder = WheelBuilder(preparer) - _, build_failures = wheel_builder.build( + _, build_failures = build( reqs_to_build, should_unpack=True, wheel_cache=wheel_cache, diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index 13a4a915294..849f5842c7f 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -18,10 +18,7 @@ from pip._internal.utils.misc import ensure_dir, normalize_path from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from pip._internal.wheel_builder import ( - WheelBuilder, - should_build_for_wheel_command, -) +from pip._internal.wheel_builder import build, should_build_for_wheel_command if MYPY_CHECK_RUNNING: from optparse import Values @@ -169,8 +166,7 @@ def run(self, options, args): ] # build wheels - wb = WheelBuilder(preparer) - build_successes, build_failures = wb.build( + build_successes, build_failures = build( reqs_to_build, should_unpack=False, wheel_cache=wheel_cache, diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index c29f17bee23..e644014e04f 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -27,9 +27,6 @@ ) from pip._internal.cache import WheelCache - from pip._internal.operations.prepare import ( - RequirementPreparer - ) from pip._internal.req.req_install import InstallRequirement BinaryAllowedPredicate = Callable[[InstallRequirement], bool] @@ -261,78 +258,67 @@ def _clean_one_legacy(req, global_options): return False -class WheelBuilder(object): - """Build wheels from a RequirementSet.""" +def build( + requirements, # type: Iterable[InstallRequirement] + should_unpack, # type: bool + wheel_cache, # type: WheelCache + build_options, # type: List[str] + global_options, # type: List[str] + check_binary_allowed=None, # type: Optional[BinaryAllowedPredicate] +): + # type: (...) -> BuildResult + """Build wheels. + + :param should_unpack: If True, after building the wheel, unpack it + and replace the sdist with the unpacked version in preparation + for installation. + :return: The list of InstallRequirement that succeeded to build and + the list of InstallRequirement that failed to build. + """ + if check_binary_allowed is None: + # Binaries allowed by default. + check_binary_allowed = _always_true - def __init__( - self, - preparer, # type: RequirementPreparer - ): - # type: (...) -> None - self.preparer = preparer - - def build( - self, - requirements, # type: Iterable[InstallRequirement] - should_unpack, # type: bool - wheel_cache, # type: WheelCache - build_options, # type: List[str] - global_options, # type: List[str] - check_binary_allowed=None, # type: Optional[BinaryAllowedPredicate] - ): - # type: (...) -> BuildResult - """Build wheels. - - :param should_unpack: If True, after building the wheel, unpack it - and replace the sdist with the unpacked version in preparation - for installation. - :return: The list of InstallRequirement that succeeded to build and - the list of InstallRequirement that failed to build. - """ - if check_binary_allowed is None: - # Binaries allowed by default. - check_binary_allowed = _always_true - - if not requirements: - return [], [] - - # TODO by @pradyunsg - # Should break up this method into 2 separate methods. - - # Build the wheels. - logger.info( - 'Building wheels for collected packages: %s', - ', '.join(req.name for req in requirements), - ) + if not requirements: + return [], [] - with indent_log(): - build_successes, build_failures = [], [] - for req in requirements: - cache_dir = _get_cache_dir( - req, wheel_cache, check_binary_allowed - ) - wheel_file = _build_one( - req, cache_dir, build_options, global_options - ) - if wheel_file: - # Update the link for this. - req.link = Link(path_to_url(wheel_file)) - req.local_file_path = req.link.file_path - assert req.link.is_wheel - build_successes.append(req) - else: - build_failures.append(req) - - # notify success/failure - if build_successes: - logger.info( - 'Successfully built %s', - ' '.join([req.name for req in build_successes]), + # TODO by @pradyunsg + # Should break up this method into 2 separate methods. + + # Build the wheels. + logger.info( + 'Building wheels for collected packages: %s', + ', '.join(req.name for req in requirements), + ) + + with indent_log(): + build_successes, build_failures = [], [] + for req in requirements: + cache_dir = _get_cache_dir( + req, wheel_cache, check_binary_allowed ) - if build_failures: - logger.info( - 'Failed to build %s', - ' '.join([req.name for req in build_failures]), + wheel_file = _build_one( + req, cache_dir, build_options, global_options ) - # Return a list of requirements that failed to build - return build_successes, build_failures + if wheel_file: + # Update the link for this. + req.link = Link(path_to_url(wheel_file)) + req.local_file_path = req.link.file_path + assert req.link.is_wheel + build_successes.append(req) + else: + build_failures.append(req) + + # notify success/failure + if build_successes: + logger.info( + 'Successfully built %s', + ' '.join([req.name for req in build_successes]), + ) + if build_failures: + logger.info( + 'Failed to build %s', + ' '.join([req.name for req in build_failures]), + ) + # Return a list of requirements that failed to build + return build_successes, build_failures From 5aa3b3d2f02dc7e66f3b09a7c1e0ad7565fe466b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Fri, 3 Jan 2020 12:49:08 +0100 Subject: [PATCH 1062/3170] Remove unused build() argument --- src/pip/_internal/commands/install.py | 1 - src/pip/_internal/commands/wheel.py | 1 - src/pip/_internal/wheel_builder.py | 7 ------- 3 files changed, 9 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 7784d86e549..82f94213e7b 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -355,7 +355,6 @@ def run(self, options, args): _, build_failures = build( reqs_to_build, - should_unpack=True, wheel_cache=wheel_cache, build_options=[], global_options=[], diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index 849f5842c7f..eb44bcee459 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -168,7 +168,6 @@ def run(self, options, args): # build wheels build_successes, build_failures = build( reqs_to_build, - should_unpack=False, wheel_cache=wheel_cache, build_options=options.build_options or [], global_options=options.global_options or [], diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index e644014e04f..5940e4ad9ec 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -260,7 +260,6 @@ def _clean_one_legacy(req, global_options): def build( requirements, # type: Iterable[InstallRequirement] - should_unpack, # type: bool wheel_cache, # type: WheelCache build_options, # type: List[str] global_options, # type: List[str] @@ -269,9 +268,6 @@ def build( # type: (...) -> BuildResult """Build wheels. - :param should_unpack: If True, after building the wheel, unpack it - and replace the sdist with the unpacked version in preparation - for installation. :return: The list of InstallRequirement that succeeded to build and the list of InstallRequirement that failed to build. """ @@ -282,9 +278,6 @@ def build( if not requirements: return [], [] - # TODO by @pradyunsg - # Should break up this method into 2 separate methods. - # Build the wheels. logger.info( 'Building wheels for collected packages: %s', From d9d801f8182fcdffc7c8f77c95749113e0feb08a Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 6 Jan 2020 19:29:56 +0530 Subject: [PATCH 1063/3170] Drop no-suffix checks for pip-from-PATH --- src/pip/_internal/utils/misc.py | 10 +++++----- tests/functional/test_install.py | 3 --- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index a53ce0dc9f9..50b603adbb0 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -836,11 +836,11 @@ def protect_pip_from_modification_on_windows(modifying_pip): On Windows, any operation modifying pip should be run as: python -m pip ... """ - pip_names = set() - for ext in ('', '.exe'): - pip_names.add('pip{ext}'.format(ext=ext)) - pip_names.add('pip{}{ext}'.format(sys.version_info[0], ext=ext)) - pip_names.add('pip{}.{}{ext}'.format(*sys.version_info[:2], ext=ext)) + pip_names = [ + "pip.exe", + "pip{}.exe".format(sys.version_info[0]), + "pip{}.{}.exe".format(*sys.version_info[:2]) + ] # See https://github.com/pypa/pip/issues/1299 for more discussion should_show_use_python_msg = ( diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 7b2d0b9f92a..e9cadca20df 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1661,9 +1661,6 @@ def test_user_config_accepted(script): @pytest.mark.network @pytest.mark.skipif("sys.platform != 'win32'") @pytest.mark.parametrize('pip_name', [ - 'pip', - 'pip{}'.format(sys.version_info[0]), - 'pip{}.{}'.format(*sys.version_info[:2]), 'pip.exe', 'pip{}.exe'.format(sys.version_info[0]), 'pip{}.{}.exe'.format(*sys.version_info[:2]) From 8460394c47a32e05145c6604d8a92ad9d716b8d2 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Mon, 6 Jan 2020 13:59:13 -0500 Subject: [PATCH 1064/3170] Update packaging version and include py.typed Since the new packaging has types, it includes a py.typed. No harm in including this in our package, and it may facilitate debug tool usage on an installed pip by signaling that pip._vendor.packaging is type-annotated. --- MANIFEST.in | 1 + src/pip/_vendor/vendor.txt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index e16ea0c73c2..aa6a1d0e71f 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -21,6 +21,7 @@ exclude tox.ini exclude noxfile.py recursive-include src/pip/_vendor *.pem +recursive-include src/pip/_vendor py.typed recursive-include docs Makefile *.rst *.py *.bat exclude src/pip/_vendor/six diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index aadd35261a1..d19ef2e51a8 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -7,7 +7,7 @@ distro==1.4.0 html5lib==1.0.1 ipaddress==1.0.22 # Only needed on 2.6 and 2.7 msgpack==0.6.2 -packaging==19.2 +packaging==20.0 pep517==0.7.0 progress==1.5 pyparsing==2.4.2 From c750c1d82a5c0dba3a10fac4cb856aaccbbf6cde Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Mon, 6 Jan 2020 14:04:07 -0500 Subject: [PATCH 1065/3170] Update packaging to 20.0 --- src/pip/_vendor/packaging/LICENSE.APACHE | 2 +- src/pip/_vendor/packaging/__about__.py | 2 +- src/pip/_vendor/packaging/_compat.py | 9 +- src/pip/_vendor/packaging/_structures.py | 26 +- src/pip/_vendor/packaging/_typing.py | 39 ++ src/pip/_vendor/packaging/markers.py | 54 ++- src/pip/_vendor/packaging/py.typed | 0 src/pip/_vendor/packaging/requirements.py | 9 +- src/pip/_vendor/packaging/specifiers.py | 168 +++++-- src/pip/_vendor/packaging/tags.py | 543 +++++++++++++++++----- src/pip/_vendor/packaging/utils.py | 13 +- src/pip/_vendor/packaging/version.py | 151 +++++- 12 files changed, 833 insertions(+), 183 deletions(-) create mode 100644 src/pip/_vendor/packaging/_typing.py create mode 100644 src/pip/_vendor/packaging/py.typed diff --git a/src/pip/_vendor/packaging/LICENSE.APACHE b/src/pip/_vendor/packaging/LICENSE.APACHE index 4947287f7b5..f433b1a53f5 100644 --- a/src/pip/_vendor/packaging/LICENSE.APACHE +++ b/src/pip/_vendor/packaging/LICENSE.APACHE @@ -174,4 +174,4 @@ incurred by, or claims asserted against, such Contributor by reason of your accepting any such warranty or additional liability. - END OF TERMS AND CONDITIONS \ No newline at end of file + END OF TERMS AND CONDITIONS diff --git a/src/pip/_vendor/packaging/__about__.py b/src/pip/_vendor/packaging/__about__.py index dc95138d049..26947283752 100644 --- a/src/pip/_vendor/packaging/__about__.py +++ b/src/pip/_vendor/packaging/__about__.py @@ -18,7 +18,7 @@ __summary__ = "Core utilities for Python packages" __uri__ = "https://github.com/pypa/packaging" -__version__ = "19.2" +__version__ = "20.0" __author__ = "Donald Stufft and individual contributors" __email__ = "donald@stufft.io" diff --git a/src/pip/_vendor/packaging/_compat.py b/src/pip/_vendor/packaging/_compat.py index 25da473c196..a145f7eeb39 100644 --- a/src/pip/_vendor/packaging/_compat.py +++ b/src/pip/_vendor/packaging/_compat.py @@ -5,6 +5,11 @@ import sys +from ._typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: # pragma: no cover + from typing import Any, Dict, Tuple, Type + PY2 = sys.version_info[0] == 2 PY3 = sys.version_info[0] == 3 @@ -18,14 +23,16 @@ def with_metaclass(meta, *bases): + # type: (Type[Any], Tuple[Type[Any], ...]) -> Any """ Create a base class with a metaclass. """ # This requires a bit of explanation: the basic idea is to make a dummy # metaclass for one level of class instantiation that replaces itself with # the actual metaclass. - class metaclass(meta): + class metaclass(meta): # type: ignore def __new__(cls, name, this_bases, d): + # type: (Type[Any], str, Tuple[Any], Dict[Any, Any]) -> Any return meta(name, bases, d) return type.__new__(metaclass, "temporary_class", (), {}) diff --git a/src/pip/_vendor/packaging/_structures.py b/src/pip/_vendor/packaging/_structures.py index 68dcca634d8..800d5c5588c 100644 --- a/src/pip/_vendor/packaging/_structures.py +++ b/src/pip/_vendor/packaging/_structures.py @@ -4,65 +4,83 @@ from __future__ import absolute_import, division, print_function -class Infinity(object): +class InfinityType(object): def __repr__(self): + # type: () -> str return "Infinity" def __hash__(self): + # type: () -> int return hash(repr(self)) def __lt__(self, other): + # type: (object) -> bool return False def __le__(self, other): + # type: (object) -> bool return False def __eq__(self, other): + # type: (object) -> bool return isinstance(other, self.__class__) def __ne__(self, other): + # type: (object) -> bool return not isinstance(other, self.__class__) def __gt__(self, other): + # type: (object) -> bool return True def __ge__(self, other): + # type: (object) -> bool return True def __neg__(self): + # type: (object) -> NegativeInfinityType return NegativeInfinity -Infinity = Infinity() +Infinity = InfinityType() -class NegativeInfinity(object): +class NegativeInfinityType(object): def __repr__(self): + # type: () -> str return "-Infinity" def __hash__(self): + # type: () -> int return hash(repr(self)) def __lt__(self, other): + # type: (object) -> bool return True def __le__(self, other): + # type: (object) -> bool return True def __eq__(self, other): + # type: (object) -> bool return isinstance(other, self.__class__) def __ne__(self, other): + # type: (object) -> bool return not isinstance(other, self.__class__) def __gt__(self, other): + # type: (object) -> bool return False def __ge__(self, other): + # type: (object) -> bool return False def __neg__(self): + # type: (object) -> InfinityType return Infinity -NegativeInfinity = NegativeInfinity() +NegativeInfinity = NegativeInfinityType() diff --git a/src/pip/_vendor/packaging/_typing.py b/src/pip/_vendor/packaging/_typing.py new file mode 100644 index 00000000000..945b39c30a0 --- /dev/null +++ b/src/pip/_vendor/packaging/_typing.py @@ -0,0 +1,39 @@ +"""For neatly implementing static typing in packaging. + +`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 packaging, all static-typing related imports should be guarded as follows: + + from pip._vendor.packaging._typing import MYPY_CHECK_RUNNING + + if MYPY_CHECK_RUNNING: + from typing import ... + +Ref: https://github.com/python/mypy/issues/3216 +""" + +MYPY_CHECK_RUNNING = False + +if MYPY_CHECK_RUNNING: # pragma: no cover + import typing + + cast = typing.cast +else: + # typing's cast() is needed at runtime, but we don't want to import typing. + # Thus, we use a dummy no-op version, which we tell mypy to ignore. + def cast(type_, value): # type: ignore + return value diff --git a/src/pip/_vendor/packaging/markers.py b/src/pip/_vendor/packaging/markers.py index b942e846e15..b24f8edf934 100644 --- a/src/pip/_vendor/packaging/markers.py +++ b/src/pip/_vendor/packaging/markers.py @@ -13,8 +13,14 @@ from pip._vendor.pyparsing import Literal as L # noqa from ._compat import string_types +from ._typing import MYPY_CHECK_RUNNING from .specifiers import Specifier, InvalidSpecifier +if MYPY_CHECK_RUNNING: # pragma: no cover + from typing import Any, Callable, Dict, List, Optional, Tuple, Union + + Operator = Callable[[str, str], bool] + __all__ = [ "InvalidMarker", @@ -46,30 +52,37 @@ class UndefinedEnvironmentName(ValueError): class Node(object): def __init__(self, value): + # type: (Any) -> None self.value = value def __str__(self): + # type: () -> str return str(self.value) def __repr__(self): + # type: () -> str return "<{0}({1!r})>".format(self.__class__.__name__, str(self)) def serialize(self): + # type: () -> str raise NotImplementedError class Variable(Node): def serialize(self): + # type: () -> str return str(self) class Value(Node): def serialize(self): + # type: () -> str return '"{0}"'.format(self) class Op(Node): def serialize(self): + # type: () -> str return str(self) @@ -85,13 +98,13 @@ def serialize(self): | L("python_version") | L("sys_platform") | L("os_name") - | L("os.name") + | L("os.name") # PEP-345 | L("sys.platform") # PEP-345 | L("platform.version") # PEP-345 | L("platform.machine") # PEP-345 | L("platform.python_implementation") # PEP-345 - | L("python_implementation") # PEP-345 - | L("extra") # undocumented setuptools legacy + | L("python_implementation") # undocumented setuptools legacy + | L("extra") # PEP-508 ) ALIASES = { "os.name": "os_name", @@ -131,6 +144,7 @@ def serialize(self): def _coerce_parse_result(results): + # type: (Union[ParseResults, List[Any]]) -> List[Any] if isinstance(results, ParseResults): return [_coerce_parse_result(i) for i in results] else: @@ -138,6 +152,8 @@ def _coerce_parse_result(results): def _format_marker(marker, first=True): + # type: (Union[List[str], Tuple[Node, ...], str], Optional[bool]) -> str + assert isinstance(marker, (list, tuple, string_types)) # Sometimes we have a structure like [[...]] which is a single item list @@ -172,10 +188,11 @@ def _format_marker(marker, first=True): "!=": operator.ne, ">=": operator.ge, ">": operator.gt, -} +} # type: Dict[str, Operator] def _eval_op(lhs, op, rhs): + # type: (str, Op, str) -> bool try: spec = Specifier("".join([op.serialize(), rhs])) except InvalidSpecifier: @@ -183,7 +200,7 @@ def _eval_op(lhs, op, rhs): else: return spec.contains(lhs) - oper = _operators.get(op.serialize()) + oper = _operators.get(op.serialize()) # type: Optional[Operator] if oper is None: raise UndefinedComparison( "Undefined {0!r} on {1!r} and {2!r}.".format(op, lhs, rhs) @@ -192,13 +209,18 @@ def _eval_op(lhs, op, rhs): return oper(lhs, rhs) -_undefined = object() +class Undefined(object): + pass + + +_undefined = Undefined() def _get_env(environment, name): - value = environment.get(name, _undefined) + # type: (Dict[str, str], str) -> str + value = environment.get(name, _undefined) # type: Union[str, Undefined] - if value is _undefined: + if isinstance(value, Undefined): raise UndefinedEnvironmentName( "{0!r} does not exist in evaluation environment.".format(name) ) @@ -207,7 +229,8 @@ def _get_env(environment, name): def _evaluate_markers(markers, environment): - groups = [[]] + # type: (List[Any], Dict[str, str]) -> bool + groups = [[]] # type: List[List[bool]] for marker in markers: assert isinstance(marker, (list, tuple, string_types)) @@ -234,6 +257,7 @@ def _evaluate_markers(markers, environment): def format_full_version(info): + # type: (sys._version_info) -> str version = "{0.major}.{0.minor}.{0.micro}".format(info) kind = info.releaselevel if kind != "final": @@ -242,9 +266,13 @@ def format_full_version(info): def default_environment(): + # type: () -> Dict[str, str] if hasattr(sys, "implementation"): - iver = format_full_version(sys.implementation.version) - implementation_name = sys.implementation.name + # Ignoring the `sys.implementation` reference for type checking due to + # mypy not liking that the attribute doesn't exist in Python 2.7 when + # run with the `--py27` flag. + iver = format_full_version(sys.implementation.version) # type: ignore + implementation_name = sys.implementation.name # type: ignore else: iver = "0" implementation_name = "" @@ -266,6 +294,7 @@ def default_environment(): class Marker(object): def __init__(self, marker): + # type: (str) -> None try: self._markers = _coerce_parse_result(MARKER.parseString(marker)) except ParseException as e: @@ -275,12 +304,15 @@ def __init__(self, marker): raise InvalidMarker(err_str) def __str__(self): + # type: () -> str return _format_marker(self._markers) def __repr__(self): + # type: () -> str return "<Marker({0!r})>".format(str(self)) def evaluate(self, environment=None): + # type: (Optional[Dict[str, str]]) -> bool """Evaluate a marker. Return the boolean from evaluating the given marker against the diff --git a/src/pip/_vendor/packaging/py.typed b/src/pip/_vendor/packaging/py.typed new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_vendor/packaging/requirements.py b/src/pip/_vendor/packaging/requirements.py index dbc5f11db26..1e32a9376ec 100644 --- a/src/pip/_vendor/packaging/requirements.py +++ b/src/pip/_vendor/packaging/requirements.py @@ -11,9 +11,13 @@ from pip._vendor.pyparsing import Literal as L # noqa from pip._vendor.six.moves.urllib import parse as urlparse +from ._typing import MYPY_CHECK_RUNNING from .markers import MARKER_EXPR, Marker from .specifiers import LegacySpecifier, Specifier, SpecifierSet +if MYPY_CHECK_RUNNING: # pragma: no cover + from typing import List + class InvalidRequirement(ValueError): """ @@ -89,6 +93,7 @@ class Requirement(object): # TODO: Can we normalize the name and extra name? def __init__(self, requirement_string): + # type: (str) -> None try: req = REQUIREMENT.parseString(requirement_string) except ParseException as e: @@ -116,7 +121,8 @@ def __init__(self, requirement_string): self.marker = req.marker if req.marker else None def __str__(self): - parts = [self.name] + # type: () -> str + parts = [self.name] # type: List[str] if self.extras: parts.append("[{0}]".format(",".join(sorted(self.extras)))) @@ -135,4 +141,5 @@ def __str__(self): return "".join(parts) def __repr__(self): + # type: () -> str return "<Requirement({0!r})>".format(str(self)) diff --git a/src/pip/_vendor/packaging/specifiers.py b/src/pip/_vendor/packaging/specifiers.py index 743576a080a..94987486d4b 100644 --- a/src/pip/_vendor/packaging/specifiers.py +++ b/src/pip/_vendor/packaging/specifiers.py @@ -9,8 +9,26 @@ import re from ._compat import string_types, with_metaclass +from ._typing import MYPY_CHECK_RUNNING from .version import Version, LegacyVersion, parse +if MYPY_CHECK_RUNNING: # pragma: no cover + from typing import ( + List, + Dict, + Union, + Iterable, + Iterator, + Optional, + Callable, + Tuple, + FrozenSet, + ) + + ParsedVersion = Union[Version, LegacyVersion] + UnparsedVersion = Union[Version, LegacyVersion, str] + CallableOperator = Callable[[ParsedVersion, str], bool] + class InvalidSpecifier(ValueError): """ @@ -18,9 +36,10 @@ class InvalidSpecifier(ValueError): """ -class BaseSpecifier(with_metaclass(abc.ABCMeta, object)): +class BaseSpecifier(with_metaclass(abc.ABCMeta, object)): # type: ignore @abc.abstractmethod def __str__(self): + # type: () -> str """ Returns the str representation of this Specifier like object. This should be representative of the Specifier itself. @@ -28,12 +47,14 @@ def __str__(self): @abc.abstractmethod def __hash__(self): + # type: () -> int """ Returns a hash value for this Specifier like object. """ @abc.abstractmethod def __eq__(self, other): + # type: (object) -> bool """ Returns a boolean representing whether or not the two Specifier like objects are equal. @@ -41,6 +62,7 @@ def __eq__(self, other): @abc.abstractmethod def __ne__(self, other): + # type: (object) -> bool """ Returns a boolean representing whether or not the two Specifier like objects are not equal. @@ -48,6 +70,7 @@ def __ne__(self, other): @abc.abstractproperty def prereleases(self): + # type: () -> Optional[bool] """ Returns whether or not pre-releases as a whole are allowed by this specifier. @@ -55,6 +78,7 @@ def prereleases(self): @prereleases.setter def prereleases(self, value): + # type: (bool) -> None """ Sets whether or not pre-releases as a whole are allowed by this specifier. @@ -62,12 +86,14 @@ def prereleases(self, value): @abc.abstractmethod def contains(self, item, prereleases=None): + # type: (str, Optional[bool]) -> bool """ Determines if the given item is contained within this specifier. """ @abc.abstractmethod def filter(self, iterable, prereleases=None): + # type: (Iterable[UnparsedVersion], Optional[bool]) -> Iterable[UnparsedVersion] """ Takes an iterable of items and filters them so that only items which are contained within this specifier are allowed in it. @@ -76,19 +102,24 @@ def filter(self, iterable, prereleases=None): class _IndividualSpecifier(BaseSpecifier): - _operators = {} + _operators = {} # type: Dict[str, str] def __init__(self, spec="", prereleases=None): + # type: (str, Optional[bool]) -> None match = self._regex.search(spec) if not match: raise InvalidSpecifier("Invalid specifier: '{0}'".format(spec)) - self._spec = (match.group("operator").strip(), match.group("version").strip()) + self._spec = ( + match.group("operator").strip(), + match.group("version").strip(), + ) # type: Tuple[str, str] # Store whether or not this Specifier should accept prereleases self._prereleases = prereleases def __repr__(self): + # type: () -> str pre = ( ", prereleases={0!r}".format(self.prereleases) if self._prereleases is not None @@ -98,15 +129,18 @@ def __repr__(self): return "<{0}({1!r}{2})>".format(self.__class__.__name__, str(self), pre) def __str__(self): + # type: () -> str return "{0}{1}".format(*self._spec) def __hash__(self): + # type: () -> int return hash(self._spec) def __eq__(self, other): + # type: (object) -> bool if isinstance(other, string_types): try: - other = self.__class__(other) + other = self.__class__(str(other)) except InvalidSpecifier: return NotImplemented elif not isinstance(other, self.__class__): @@ -115,9 +149,10 @@ def __eq__(self, other): return self._spec == other._spec def __ne__(self, other): + # type: (object) -> bool if isinstance(other, string_types): try: - other = self.__class__(other) + other = self.__class__(str(other)) except InvalidSpecifier: return NotImplemented elif not isinstance(other, self.__class__): @@ -126,52 +161,67 @@ def __ne__(self, other): return self._spec != other._spec def _get_operator(self, op): - return getattr(self, "_compare_{0}".format(self._operators[op])) + # type: (str) -> CallableOperator + operator_callable = getattr( + self, "_compare_{0}".format(self._operators[op]) + ) # type: CallableOperator + return operator_callable def _coerce_version(self, version): + # type: (UnparsedVersion) -> ParsedVersion if not isinstance(version, (LegacyVersion, Version)): version = parse(version) return version @property def operator(self): + # type: () -> str return self._spec[0] @property def version(self): + # type: () -> str return self._spec[1] @property def prereleases(self): + # type: () -> Optional[bool] return self._prereleases @prereleases.setter def prereleases(self, value): + # type: (bool) -> None self._prereleases = value def __contains__(self, item): + # type: (str) -> bool return self.contains(item) def contains(self, item, prereleases=None): + # type: (UnparsedVersion, Optional[bool]) -> bool + # Determine if prereleases are to be allowed or not. if prereleases is None: prereleases = self.prereleases # Normalize item to a Version or LegacyVersion, this allows us to have # a shortcut for ``"2.0" in Specifier(">=2") - item = self._coerce_version(item) + normalized_item = self._coerce_version(item) # Determine if we should be supporting prereleases in this specifier # or not, if we do not support prereleases than we can short circuit # logic if this version is a prereleases. - if item.is_prerelease and not prereleases: + if normalized_item.is_prerelease and not prereleases: return False # Actually do the comparison to determine if this item is contained # within this Specifier or not. - return self._get_operator(self.operator)(item, self.version) + operator_callable = self._get_operator(self.operator) # type: CallableOperator + return operator_callable(normalized_item, self.version) def filter(self, iterable, prereleases=None): + # type: (Iterable[UnparsedVersion], Optional[bool]) -> Iterable[UnparsedVersion] + yielded = False found_prereleases = [] @@ -230,32 +280,43 @@ class LegacySpecifier(_IndividualSpecifier): } def _coerce_version(self, version): + # type: (Union[ParsedVersion, str]) -> LegacyVersion if not isinstance(version, LegacyVersion): version = LegacyVersion(str(version)) return version def _compare_equal(self, prospective, spec): + # type: (LegacyVersion, str) -> bool return prospective == self._coerce_version(spec) def _compare_not_equal(self, prospective, spec): + # type: (LegacyVersion, str) -> bool return prospective != self._coerce_version(spec) def _compare_less_than_equal(self, prospective, spec): + # type: (LegacyVersion, str) -> bool return prospective <= self._coerce_version(spec) def _compare_greater_than_equal(self, prospective, spec): + # type: (LegacyVersion, str) -> bool return prospective >= self._coerce_version(spec) def _compare_less_than(self, prospective, spec): + # type: (LegacyVersion, str) -> bool return prospective < self._coerce_version(spec) def _compare_greater_than(self, prospective, spec): + # type: (LegacyVersion, str) -> bool return prospective > self._coerce_version(spec) -def _require_version_compare(fn): +def _require_version_compare( + fn # type: (Callable[[Specifier, ParsedVersion, str], bool]) +): + # type: (...) -> Callable[[Specifier, ParsedVersion, str], bool] @functools.wraps(fn) def wrapped(self, prospective, spec): + # type: (Specifier, ParsedVersion, str) -> bool if not isinstance(prospective, Version): return False return fn(self, prospective, spec) @@ -373,6 +434,8 @@ class Specifier(_IndividualSpecifier): @_require_version_compare def _compare_compatible(self, prospective, spec): + # type: (ParsedVersion, str) -> bool + # Compatible releases have an equivalent combination of >= and ==. That # is that ~=2.2 is equivalent to >=2.2,==2.*. This allows us to # implement this in terms of the other specifiers instead of @@ -400,56 +463,67 @@ def _compare_compatible(self, prospective, spec): @_require_version_compare def _compare_equal(self, prospective, spec): + # type: (ParsedVersion, str) -> bool + # We need special logic to handle prefix matching if spec.endswith(".*"): # In the case of prefix matching we want to ignore local segment. prospective = Version(prospective.public) # Split the spec out by dots, and pretend that there is an implicit # dot in between a release segment and a pre-release segment. - spec = _version_split(spec[:-2]) # Remove the trailing .* + split_spec = _version_split(spec[:-2]) # Remove the trailing .* # Split the prospective version out by dots, and pretend that there # is an implicit dot in between a release segment and a pre-release # segment. - prospective = _version_split(str(prospective)) + split_prospective = _version_split(str(prospective)) # Shorten the prospective version to be the same length as the spec # so that we can determine if the specifier is a prefix of the # prospective version or not. - prospective = prospective[: len(spec)] + shortened_prospective = split_prospective[: len(split_spec)] # Pad out our two sides with zeros so that they both equal the same # length. - spec, prospective = _pad_version(spec, prospective) + padded_spec, padded_prospective = _pad_version( + split_spec, shortened_prospective + ) + + return padded_prospective == padded_spec else: # Convert our spec string into a Version - spec = Version(spec) + spec_version = Version(spec) # If the specifier does not have a local segment, then we want to # act as if the prospective version also does not have a local # segment. - if not spec.local: + if not spec_version.local: prospective = Version(prospective.public) - return prospective == spec + return prospective == spec_version @_require_version_compare def _compare_not_equal(self, prospective, spec): + # type: (ParsedVersion, str) -> bool return not self._compare_equal(prospective, spec) @_require_version_compare def _compare_less_than_equal(self, prospective, spec): + # type: (ParsedVersion, str) -> bool return prospective <= Version(spec) @_require_version_compare def _compare_greater_than_equal(self, prospective, spec): + # type: (ParsedVersion, str) -> bool return prospective >= Version(spec) @_require_version_compare - def _compare_less_than(self, prospective, spec): + def _compare_less_than(self, prospective, spec_str): + # type: (ParsedVersion, str) -> bool + # Convert our spec to a Version instance, since we'll want to work with # it as a version. - spec = Version(spec) + spec = Version(spec_str) # Check to see if the prospective version is less than the spec # version. If it's not we can short circuit and just return False now @@ -471,10 +545,12 @@ def _compare_less_than(self, prospective, spec): return True @_require_version_compare - def _compare_greater_than(self, prospective, spec): + def _compare_greater_than(self, prospective, spec_str): + # type: (ParsedVersion, str) -> bool + # Convert our spec to a Version instance, since we'll want to work with # it as a version. - spec = Version(spec) + spec = Version(spec_str) # Check to see if the prospective version is greater than the spec # version. If it's not we can short circuit and just return False now @@ -502,10 +578,13 @@ def _compare_greater_than(self, prospective, spec): return True def _compare_arbitrary(self, prospective, spec): + # type: (Version, str) -> bool return str(prospective).lower() == str(spec).lower() @property def prereleases(self): + # type: () -> bool + # If there is an explicit prereleases set for this, then we'll just # blindly use that. if self._prereleases is not None: @@ -530,6 +609,7 @@ def prereleases(self): @prereleases.setter def prereleases(self, value): + # type: (bool) -> None self._prereleases = value @@ -537,7 +617,8 @@ def prereleases(self, value): def _version_split(version): - result = [] + # type: (str) -> List[str] + result = [] # type: List[str] for item in version.split("."): match = _prefix_regex.search(item) if match: @@ -548,6 +629,7 @@ def _version_split(version): def _pad_version(left, right): + # type: (List[str], List[str]) -> Tuple[List[str], List[str]] left_split, right_split = [], [] # Get the release segment of our versions @@ -567,14 +649,16 @@ def _pad_version(left, right): class SpecifierSet(BaseSpecifier): def __init__(self, specifiers="", prereleases=None): - # Split on , to break each indidivual specifier into it's own item, and + # type: (str, Optional[bool]) -> None + + # Split on , to break each individual specifier into it's own item, and # strip each item to remove leading/trailing whitespace. - specifiers = [s.strip() for s in specifiers.split(",") if s.strip()] + split_specifiers = [s.strip() for s in specifiers.split(",") if s.strip()] # Parsed each individual specifier, attempting first to make it a # Specifier and falling back to a LegacySpecifier. parsed = set() - for specifier in specifiers: + for specifier in split_specifiers: try: parsed.add(Specifier(specifier)) except InvalidSpecifier: @@ -588,6 +672,7 @@ def __init__(self, specifiers="", prereleases=None): self._prereleases = prereleases def __repr__(self): + # type: () -> str pre = ( ", prereleases={0!r}".format(self.prereleases) if self._prereleases is not None @@ -597,12 +682,15 @@ def __repr__(self): return "<SpecifierSet({0!r}{1})>".format(str(self), pre) def __str__(self): + # type: () -> str return ",".join(sorted(str(s) for s in self._specs)) def __hash__(self): + # type: () -> int return hash(self._specs) def __and__(self, other): + # type: (Union[SpecifierSet, str]) -> SpecifierSet if isinstance(other, string_types): other = SpecifierSet(other) elif not isinstance(other, SpecifierSet): @@ -626,9 +714,8 @@ def __and__(self, other): return specifier def __eq__(self, other): - if isinstance(other, string_types): - other = SpecifierSet(other) - elif isinstance(other, _IndividualSpecifier): + # type: (object) -> bool + if isinstance(other, (string_types, _IndividualSpecifier)): other = SpecifierSet(str(other)) elif not isinstance(other, SpecifierSet): return NotImplemented @@ -636,9 +723,8 @@ def __eq__(self, other): return self._specs == other._specs def __ne__(self, other): - if isinstance(other, string_types): - other = SpecifierSet(other) - elif isinstance(other, _IndividualSpecifier): + # type: (object) -> bool + if isinstance(other, (string_types, _IndividualSpecifier)): other = SpecifierSet(str(other)) elif not isinstance(other, SpecifierSet): return NotImplemented @@ -646,13 +732,17 @@ def __ne__(self, other): return self._specs != other._specs def __len__(self): + # type: () -> int return len(self._specs) def __iter__(self): + # type: () -> Iterator[FrozenSet[_IndividualSpecifier]] return iter(self._specs) @property def prereleases(self): + # type: () -> Optional[bool] + # If we have been given an explicit prerelease modifier, then we'll # pass that through here. if self._prereleases is not None: @@ -670,12 +760,16 @@ def prereleases(self): @prereleases.setter def prereleases(self, value): + # type: (bool) -> None self._prereleases = value def __contains__(self, item): + # type: (Union[ParsedVersion, str]) -> bool return self.contains(item) def contains(self, item, prereleases=None): + # type: (Union[ParsedVersion, str], Optional[bool]) -> bool + # Ensure that our item is a Version or LegacyVersion instance. if not isinstance(item, (LegacyVersion, Version)): item = parse(item) @@ -701,7 +795,13 @@ def contains(self, item, prereleases=None): # will always return True, this is an explicit design decision. return all(s.contains(item, prereleases=prereleases) for s in self._specs) - def filter(self, iterable, prereleases=None): + def filter( + self, + iterable, # type: Iterable[Union[ParsedVersion, str]] + prereleases=None, # type: Optional[bool] + ): + # type: (...) -> Iterable[Union[ParsedVersion, str]] + # Determine if we're forcing a prerelease or not, if we're not forcing # one for this particular filter call, then we'll use whatever the # SpecifierSet thinks for whether or not we should support prereleases. @@ -719,8 +819,8 @@ def filter(self, iterable, prereleases=None): # which will filter out any pre-releases, unless there are no final # releases, and which will filter out LegacyVersion in general. else: - filtered = [] - found_prereleases = [] + filtered = [] # type: List[Union[ParsedVersion, str]] + found_prereleases = [] # type: List[Union[ParsedVersion, str]] for item in iterable: # Ensure that we some kind of Version class for this item. diff --git a/src/pip/_vendor/packaging/tags.py b/src/pip/_vendor/packaging/tags.py index ec9942f0f66..20512aaf992 100644 --- a/src/pip/_vendor/packaging/tags.py +++ b/src/pip/_vendor/packaging/tags.py @@ -13,12 +13,37 @@ EXTENSION_SUFFIXES = [x[0] for x in imp.get_suffixes()] del imp +import logging +import os import platform import re +import struct import sys import sysconfig import warnings +from ._typing import MYPY_CHECK_RUNNING, cast + +if MYPY_CHECK_RUNNING: # pragma: no cover + from typing import ( + Dict, + FrozenSet, + IO, + Iterable, + Iterator, + List, + Optional, + Sequence, + Tuple, + Union, + ) + + PythonVersion = Sequence[int] + MacVersion = Tuple[int, int] + GlibcVersion = Tuple[int, int] + + +logger = logging.getLogger(__name__) INTERPRETER_SHORT_NAMES = { "python": "py", # Generic. @@ -26,7 +51,7 @@ "pypy": "pp", "ironpython": "ip", "jython": "jy", -} +} # type: Dict[str, str] _32_BIT_INTERPRETER = sys.maxsize <= 2 ** 32 @@ -37,23 +62,31 @@ class Tag(object): __slots__ = ["_interpreter", "_abi", "_platform"] def __init__(self, interpreter, abi, platform): + # type: (str, str, str) -> None self._interpreter = interpreter.lower() self._abi = abi.lower() self._platform = platform.lower() @property def interpreter(self): + # type: () -> str return self._interpreter @property def abi(self): + # type: () -> str return self._abi @property def platform(self): + # type: () -> str return self._platform def __eq__(self, other): + # type: (object) -> bool + if not isinstance(other, Tag): + return NotImplemented + return ( (self.platform == other.platform) and (self.abi == other.abi) @@ -61,16 +94,20 @@ def __eq__(self, other): ) def __hash__(self): + # type: () -> int return hash((self._interpreter, self._abi, self._platform)) def __str__(self): + # type: () -> str return "{}-{}-{}".format(self._interpreter, self._abi, self._platform) def __repr__(self): + # type: () -> str return "<{self} @ {self_id}>".format(self=self, self_id=id(self)) def parse_tag(tag): + # type: (str) -> FrozenSet[Tag] tags = set() interpreters, abis, platforms = tag.split("-") for interpreter in interpreters.split("."): @@ -80,20 +117,54 @@ def parse_tag(tag): return frozenset(tags) +def _warn_keyword_parameter(func_name, kwargs): + # type: (str, Dict[str, bool]) -> bool + """ + Backwards-compatibility with Python 2.7 to allow treating 'warn' as keyword-only. + """ + if not kwargs: + return False + elif len(kwargs) > 1 or "warn" not in kwargs: + kwargs.pop("warn", None) + arg = next(iter(kwargs.keys())) + raise TypeError( + "{}() got an unexpected keyword argument {!r}".format(func_name, arg) + ) + return kwargs["warn"] + + +def _get_config_var(name, warn=False): + # type: (str, bool) -> Union[int, str, None] + value = sysconfig.get_config_var(name) + if value is None and warn: + logger.debug( + "Config variable '%s' is unset, Python ABI tag may be incorrect", name + ) + return value + + def _normalize_string(string): + # type: (str) -> str return string.replace(".", "_").replace("-", "_") -def _cpython_interpreter(py_version): - # TODO: Is using py_version_nodot for interpreter version critical? - return "cp{major}{minor}".format(major=py_version[0], minor=py_version[1]) +def _abi3_applies(python_version): + # type: (PythonVersion) -> bool + """ + Determine if the Python version supports abi3. + + PEP 384 was first implemented in Python 3.2. + """ + return len(python_version) > 1 and tuple(python_version) >= (3, 2) -def _cpython_abis(py_version): +def _cpython_abis(py_version, warn=False): + # type: (PythonVersion, bool) -> List[str] + py_version = tuple(py_version) # To allow for version comparison. abis = [] version = "{}{}".format(*py_version[:2]) debug = pymalloc = ucs4 = "" - with_debug = sysconfig.get_config_var("Py_DEBUG") + with_debug = _get_config_var("Py_DEBUG", warn) has_refcount = hasattr(sys, "gettotalrefcount") # Windows doesn't set Py_DEBUG, so checking for support of debug-compiled # extension modules is the best option. @@ -102,11 +173,11 @@ def _cpython_abis(py_version): if with_debug or (with_debug is None and (has_refcount or has_ext)): debug = "d" if py_version < (3, 8): - with_pymalloc = sysconfig.get_config_var("WITH_PYMALLOC") + with_pymalloc = _get_config_var("WITH_PYMALLOC", warn) if with_pymalloc or with_pymalloc is None: pymalloc = "m" if py_version < (3, 3): - unicode_size = sysconfig.get_config_var("Py_UNICODE_SIZE") + unicode_size = _get_config_var("Py_UNICODE_SIZE", warn) if unicode_size == 4 or ( unicode_size is None and sys.maxunicode == 0x10FFFF ): @@ -124,86 +195,152 @@ def _cpython_abis(py_version): return abis -def _cpython_tags(py_version, interpreter, abis, platforms): +def cpython_tags( + python_version=None, # type: Optional[PythonVersion] + abis=None, # type: Optional[Iterable[str]] + platforms=None, # type: Optional[Iterable[str]] + **kwargs # type: bool +): + # type: (...) -> Iterator[Tag] + """ + Yields the tags for a CPython interpreter. + + The tags consist of: + - cp<python_version>-<abi>-<platform> + - cp<python_version>-abi3-<platform> + - cp<python_version>-none-<platform> + - cp<less than python_version>-abi3-<platform> # Older Python versions down to 3.2. + + If python_version only specifies a major version then user-provided ABIs and + the 'none' ABItag will be used. + + If 'abi3' or 'none' are specified in 'abis' then they will be yielded at + their normal position and not at the beginning. + """ + warn = _warn_keyword_parameter("cpython_tags", kwargs) + if not python_version: + python_version = sys.version_info[:2] + + if len(python_version) < 2: + interpreter = "cp{}".format(python_version[0]) + else: + interpreter = "cp{}{}".format(*python_version[:2]) + + if abis is None: + if len(python_version) > 1: + abis = _cpython_abis(python_version, warn) + else: + abis = [] + abis = list(abis) + # 'abi3' and 'none' are explicitly handled later. + for explicit_abi in ("abi3", "none"): + try: + abis.remove(explicit_abi) + except ValueError: + pass + + platforms = list(platforms or _platform_tags()) for abi in abis: for platform_ in platforms: yield Tag(interpreter, abi, platform_) - for tag in (Tag(interpreter, "abi3", platform_) for platform_ in platforms): - yield tag + if _abi3_applies(python_version): + for tag in (Tag(interpreter, "abi3", platform_) for platform_ in platforms): + yield tag for tag in (Tag(interpreter, "none", platform_) for platform_ in platforms): yield tag - # PEP 384 was first implemented in Python 3.2. - for minor_version in range(py_version[1] - 1, 1, -1): - for platform_ in platforms: - interpreter = "cp{major}{minor}".format( - major=py_version[0], minor=minor_version - ) - yield Tag(interpreter, "abi3", platform_) - -def _pypy_interpreter(): - return "pp{py_major}{pypy_major}{pypy_minor}".format( - py_major=sys.version_info[0], - pypy_major=sys.pypy_version_info.major, - pypy_minor=sys.pypy_version_info.minor, - ) + if _abi3_applies(python_version): + for minor_version in range(python_version[1] - 1, 1, -1): + for platform_ in platforms: + interpreter = "cp{major}{minor}".format( + major=python_version[0], minor=minor_version + ) + yield Tag(interpreter, "abi3", platform_) def _generic_abi(): + # type: () -> Iterator[str] abi = sysconfig.get_config_var("SOABI") if abi: - return _normalize_string(abi) - else: - return "none" + yield _normalize_string(abi) -def _pypy_tags(py_version, interpreter, abi, platforms): - for tag in (Tag(interpreter, abi, platform) for platform in platforms): - yield tag - for tag in (Tag(interpreter, "none", platform) for platform in platforms): - yield tag +def generic_tags( + interpreter=None, # type: Optional[str] + abis=None, # type: Optional[Iterable[str]] + platforms=None, # type: Optional[Iterable[str]] + **kwargs # type: bool +): + # type: (...) -> Iterator[Tag] + """ + Yields the tags for a generic interpreter. + The tags consist of: + - <interpreter>-<abi>-<platform> -def _generic_tags(interpreter, py_version, abi, platforms): - for tag in (Tag(interpreter, abi, platform) for platform in platforms): - yield tag - if abi != "none": - tags = (Tag(interpreter, "none", platform_) for platform_ in platforms) - for tag in tags: - yield tag + The "none" ABI will be added if it was not explicitly provided. + """ + warn = _warn_keyword_parameter("generic_tags", kwargs) + if not interpreter: + interp_name = interpreter_name() + interp_version = interpreter_version(warn=warn) + interpreter = "".join([interp_name, interp_version]) + if abis is None: + abis = _generic_abi() + platforms = list(platforms or _platform_tags()) + abis = list(abis) + if "none" not in abis: + abis.append("none") + for abi in abis: + for platform_ in platforms: + yield Tag(interpreter, abi, platform_) def _py_interpreter_range(py_version): + # type: (PythonVersion) -> Iterator[str] """ - Yield Python versions in descending order. + Yields Python versions in descending order. After the latest version, the major-only version will be yielded, and then - all following versions up to 'end'. + all previous versions of that major version. """ - yield "py{major}{minor}".format(major=py_version[0], minor=py_version[1]) + if len(py_version) > 1: + yield "py{major}{minor}".format(major=py_version[0], minor=py_version[1]) yield "py{major}".format(major=py_version[0]) - for minor in range(py_version[1] - 1, -1, -1): - yield "py{major}{minor}".format(major=py_version[0], minor=minor) + if len(py_version) > 1: + for minor in range(py_version[1] - 1, -1, -1): + yield "py{major}{minor}".format(major=py_version[0], minor=minor) -def _independent_tags(interpreter, py_version, platforms): +def compatible_tags( + python_version=None, # type: Optional[PythonVersion] + interpreter=None, # type: Optional[str] + platforms=None, # type: Optional[Iterator[str]] +): + # type: (...) -> Iterator[Tag] """ - Return the sequence of tags that are consistent across implementations. + Yields the sequence of tags that are compatible with a specific version of Python. The tags consist of: - py*-none-<platform> - - <interpreter>-none-any + - <interpreter>-none-any # ... if `interpreter` is provided. - py*-none-any """ - for version in _py_interpreter_range(py_version): + if not python_version: + python_version = sys.version_info[:2] + if not platforms: + platforms = _platform_tags() + for version in _py_interpreter_range(python_version): for platform_ in platforms: yield Tag(version, "none", platform_) - yield Tag(interpreter, "none", "any") - for version in _py_interpreter_range(py_version): + if interpreter: + yield Tag(interpreter, "none", "any") + for version in _py_interpreter_range(python_version): yield Tag(version, "none", "any") def _mac_arch(arch, is_32bit=_32_BIT_INTERPRETER): + # type: (str, bool) -> str if not is_32bit: return arch @@ -214,6 +351,7 @@ def _mac_arch(arch, is_32bit=_32_BIT_INTERPRETER): def _mac_binary_formats(version, cpu_arch): + # type: (MacVersion, str) -> List[str] formats = [cpu_arch] if cpu_arch == "x86_64": if version < (10, 4): @@ -240,32 +378,42 @@ def _mac_binary_formats(version, cpu_arch): return formats -def _mac_platforms(version=None, arch=None): - version_str, _, cpu_arch = platform.mac_ver() +def mac_platforms(version=None, arch=None): + # type: (Optional[MacVersion], Optional[str]) -> Iterator[str] + """ + Yields the platform tags for a macOS system. + + The `version` parameter is a two-item tuple specifying the macOS version to + generate platform tags for. The `arch` parameter is the CPU architecture to + generate platform tags for. Both parameters default to the appropriate value + for the current system. + """ + version_str, _, cpu_arch = platform.mac_ver() # type: ignore if version is None: - version = tuple(map(int, version_str.split(".")[:2])) + version = cast("MacVersion", tuple(map(int, version_str.split(".")[:2]))) + else: + version = version if arch is None: arch = _mac_arch(cpu_arch) - platforms = [] + else: + arch = arch for minor_version in range(version[1], -1, -1): compat_version = version[0], minor_version binary_formats = _mac_binary_formats(compat_version, arch) for binary_format in binary_formats: - platforms.append( - "macosx_{major}_{minor}_{binary_format}".format( - major=compat_version[0], - minor=compat_version[1], - binary_format=binary_format, - ) + yield "macosx_{major}_{minor}_{binary_format}".format( + major=compat_version[0], + minor=compat_version[1], + binary_format=binary_format, ) - return platforms # From PEP 513. def _is_manylinux_compatible(name, glibc_version): + # type: (str, GlibcVersion) -> bool # Check for presence of _manylinux module. try: - import _manylinux + import _manylinux # noqa return bool(getattr(_manylinux, name + "_compatible")) except (ImportError, AttributeError): @@ -276,14 +424,50 @@ def _is_manylinux_compatible(name, glibc_version): def _glibc_version_string(): + # type: () -> Optional[str] # Returns glibc version string, or None if not using glibc. - import ctypes + return _glibc_version_string_confstr() or _glibc_version_string_ctypes() + + +def _glibc_version_string_confstr(): + # type: () -> Optional[str] + """ + Primary implementation of glibc_version_string using os.confstr. + """ + # os.confstr is quite a bit faster than ctypes.DLL. It's also less likely + # to be broken or missing. This strategy is used in the standard library + # platform module. + # https://github.com/python/cpython/blob/fcf1d003bf4f0100c9d0921ff3d70e1127ca1b71/Lib/platform.py#L175-L183 + try: + # os.confstr("CS_GNU_LIBC_VERSION") returns a string like "glibc 2.17". + version_string = os.confstr( # type: ignore[attr-defined] # noqa: F821 + "CS_GNU_LIBC_VERSION" + ) + assert version_string is not None + _, version = version_string.split() # type: Tuple[str, str] + except (AssertionError, AttributeError, OSError, ValueError): + # os.confstr() or CS_GNU_LIBC_VERSION not available (or a bad value)... + return None + return version + + +def _glibc_version_string_ctypes(): + # type: () -> Optional[str] + """ + Fallback implementation of glibc_version_string using ctypes. + """ + try: + import ctypes + except ImportError: + return None # ctypes.CDLL(None) internally calls dlopen(NULL), and as the dlopen # manpage says, "If filename is NULL, then the returned handle is for the # main program". This way we can let the linker do the work to figure out # which libc our process is actually using. - process_namespace = ctypes.CDLL(None) + # + # Note: typeshed is wrong here so we are ignoring this line. + process_namespace = ctypes.CDLL(None) # type: ignore try: gnu_get_libc_version = process_namespace.gnu_get_libc_version except AttributeError: @@ -293,7 +477,7 @@ def _glibc_version_string(): # Call gnu_get_libc_version, which returns a string like "2.5" gnu_get_libc_version.restype = ctypes.c_char_p - version_str = gnu_get_libc_version() + version_str = gnu_get_libc_version() # type: str # py2 / py3 compatibility: if not isinstance(version_str, str): version_str = version_str.decode("ascii") @@ -303,6 +487,7 @@ def _glibc_version_string(): # Separated out from have_compatible_glibc for easier unit testing. def _check_glibc_version(version_str, required_major, minimum_minor): + # type: (str, int, int) -> bool # Parse string and check against requested version. # # We use a regexp instead of str.split because we want to discard any @@ -324,81 +509,223 @@ def _check_glibc_version(version_str, required_major, minimum_minor): def _have_compatible_glibc(required_major, minimum_minor): + # type: (int, int) -> bool version_str = _glibc_version_string() if version_str is None: return False return _check_glibc_version(version_str, required_major, minimum_minor) +# Python does not provide platform information at sufficient granularity to +# identify the architecture of the running executable in some cases, so we +# determine it dynamically by reading the information from the running +# process. This only applies on Linux, which uses the ELF format. +class _ELFFileHeader(object): + # https://en.wikipedia.org/wiki/Executable_and_Linkable_Format#File_header + class _InvalidELFFileHeader(ValueError): + """ + An invalid ELF file header was found. + """ + + ELF_MAGIC_NUMBER = 0x7F454C46 + ELFCLASS32 = 1 + ELFCLASS64 = 2 + ELFDATA2LSB = 1 + ELFDATA2MSB = 2 + EM_386 = 3 + EM_S390 = 22 + EM_ARM = 40 + EM_X86_64 = 62 + EF_ARM_ABIMASK = 0xFF000000 + EF_ARM_ABI_VER5 = 0x05000000 + EF_ARM_ABI_FLOAT_HARD = 0x00000400 + + def __init__(self, file): + # type: (IO[bytes]) -> None + def unpack(fmt): + # type: (str) -> int + try: + result, = struct.unpack( + fmt, file.read(struct.calcsize(fmt)) + ) # type: (int, ) + except struct.error: + raise _ELFFileHeader._InvalidELFFileHeader() + return result + + self.e_ident_magic = unpack(">I") + if self.e_ident_magic != self.ELF_MAGIC_NUMBER: + raise _ELFFileHeader._InvalidELFFileHeader() + self.e_ident_class = unpack("B") + if self.e_ident_class not in {self.ELFCLASS32, self.ELFCLASS64}: + raise _ELFFileHeader._InvalidELFFileHeader() + self.e_ident_data = unpack("B") + if self.e_ident_data not in {self.ELFDATA2LSB, self.ELFDATA2MSB}: + raise _ELFFileHeader._InvalidELFFileHeader() + self.e_ident_version = unpack("B") + self.e_ident_osabi = unpack("B") + self.e_ident_abiversion = unpack("B") + self.e_ident_pad = file.read(7) + format_h = "<H" if self.e_ident_data == self.ELFDATA2LSB else ">H" + format_i = "<I" if self.e_ident_data == self.ELFDATA2LSB else ">I" + format_q = "<Q" if self.e_ident_data == self.ELFDATA2LSB else ">Q" + format_p = format_i if self.e_ident_class == self.ELFCLASS32 else format_q + self.e_type = unpack(format_h) + self.e_machine = unpack(format_h) + self.e_version = unpack(format_i) + self.e_entry = unpack(format_p) + self.e_phoff = unpack(format_p) + self.e_shoff = unpack(format_p) + self.e_flags = unpack(format_i) + self.e_ehsize = unpack(format_h) + self.e_phentsize = unpack(format_h) + self.e_phnum = unpack(format_h) + self.e_shentsize = unpack(format_h) + self.e_shnum = unpack(format_h) + self.e_shstrndx = unpack(format_h) + + +def _get_elf_header(): + # type: () -> Optional[_ELFFileHeader] + try: + with open(sys.executable, "rb") as f: + elf_header = _ELFFileHeader(f) + except (IOError, OSError, TypeError, _ELFFileHeader._InvalidELFFileHeader): + return None + return elf_header + + +def _is_linux_armhf(): + # type: () -> bool + # hard-float ABI can be detected from the ELF header of the running + # process + # https://static.docs.arm.com/ihi0044/g/aaelf32.pdf + elf_header = _get_elf_header() + if elf_header is None: + return False + result = elf_header.e_ident_class == elf_header.ELFCLASS32 + result &= elf_header.e_ident_data == elf_header.ELFDATA2LSB + result &= elf_header.e_machine == elf_header.EM_ARM + result &= ( + elf_header.e_flags & elf_header.EF_ARM_ABIMASK + ) == elf_header.EF_ARM_ABI_VER5 + result &= ( + elf_header.e_flags & elf_header.EF_ARM_ABI_FLOAT_HARD + ) == elf_header.EF_ARM_ABI_FLOAT_HARD + return result + + +def _is_linux_i686(): + # type: () -> bool + elf_header = _get_elf_header() + if elf_header is None: + return False + result = elf_header.e_ident_class == elf_header.ELFCLASS32 + result &= elf_header.e_ident_data == elf_header.ELFDATA2LSB + result &= elf_header.e_machine == elf_header.EM_386 + return result + + +def _have_compatible_manylinux_abi(arch): + # type: (str) -> bool + if arch == "armv7l": + return _is_linux_armhf() + if arch == "i686": + return _is_linux_i686() + return True + + def _linux_platforms(is_32bit=_32_BIT_INTERPRETER): + # type: (bool) -> Iterator[str] linux = _normalize_string(distutils.util.get_platform()) if linux == "linux_x86_64" and is_32bit: linux = "linux_i686" - manylinux_support = ( - ("manylinux2014", (2, 17)), # CentOS 7 w/ glibc 2.17 (PEP 599) - ("manylinux2010", (2, 12)), # CentOS 6 w/ glibc 2.12 (PEP 571) - ("manylinux1", (2, 5)), # CentOS 5 w/ glibc 2.5 (PEP 513) - ) + manylinux_support = [] + _, arch = linux.split("_", 1) + if _have_compatible_manylinux_abi(arch): + if arch in {"x86_64", "i686", "aarch64", "armv7l", "ppc64", "ppc64le", "s390x"}: + manylinux_support.append( + ("manylinux2014", (2, 17)) + ) # CentOS 7 w/ glibc 2.17 (PEP 599) + if arch in {"x86_64", "i686"}: + manylinux_support.append( + ("manylinux2010", (2, 12)) + ) # CentOS 6 w/ glibc 2.12 (PEP 571) + manylinux_support.append( + ("manylinux1", (2, 5)) + ) # CentOS 5 w/ glibc 2.5 (PEP 513) manylinux_support_iter = iter(manylinux_support) for name, glibc_version in manylinux_support_iter: if _is_manylinux_compatible(name, glibc_version): - platforms = [linux.replace("linux", name)] + yield linux.replace("linux", name) break - else: - platforms = [] # Support for a later manylinux implies support for an earlier version. - platforms += [linux.replace("linux", name) for name, _ in manylinux_support_iter] - platforms.append(linux) - return platforms + for name, _ in manylinux_support_iter: + yield linux.replace("linux", name) + yield linux def _generic_platforms(): - platform = _normalize_string(distutils.util.get_platform()) - return [platform] + # type: () -> Iterator[str] + yield _normalize_string(distutils.util.get_platform()) + + +def _platform_tags(): + # type: () -> Iterator[str] + """ + Provides the platform tags for this installation. + """ + if platform.system() == "Darwin": + return mac_platforms() + elif platform.system() == "Linux": + return _linux_platforms() + else: + return _generic_platforms() -def _interpreter_name(): - name = platform.python_implementation().lower() +def interpreter_name(): + # type: () -> str + """ + Returns the name of the running interpreter. + """ + try: + name = sys.implementation.name # type: ignore + except AttributeError: # pragma: no cover + # Python 2.7 compatibility. + name = platform.python_implementation().lower() return INTERPRETER_SHORT_NAMES.get(name) or name -def _generic_interpreter(name, py_version): - version = sysconfig.get_config_var("py_version_nodot") - if not version: - version = "".join(map(str, py_version[:2])) - return "{name}{version}".format(name=name, version=version) +def interpreter_version(**kwargs): + # type: (bool) -> str + """ + Returns the version of the running interpreter. + """ + warn = _warn_keyword_parameter("interpreter_version", kwargs) + version = _get_config_var("py_version_nodot", warn=warn) + if version: + version = str(version) + else: + version = "".join(map(str, sys.version_info[:2])) + return version -def sys_tags(): +def sys_tags(**kwargs): + # type: (bool) -> Iterator[Tag] """ Returns the sequence of tag triples for the running interpreter. The order of the sequence corresponds to priority order for the interpreter, from most to least important. """ - py_version = sys.version_info[:2] - interpreter_name = _interpreter_name() - if platform.system() == "Darwin": - platforms = _mac_platforms() - elif platform.system() == "Linux": - platforms = _linux_platforms() - else: - platforms = _generic_platforms() + warn = _warn_keyword_parameter("sys_tags", kwargs) - if interpreter_name == "cp": - interpreter = _cpython_interpreter(py_version) - abis = _cpython_abis(py_version) - for tag in _cpython_tags(py_version, interpreter, abis, platforms): - yield tag - elif interpreter_name == "pp": - interpreter = _pypy_interpreter() - abi = _generic_abi() - for tag in _pypy_tags(py_version, interpreter, abi, platforms): + interp_name = interpreter_name() + if interp_name == "cp": + for tag in cpython_tags(warn=warn): yield tag else: - interpreter = _generic_interpreter(interpreter_name, py_version) - abi = _generic_abi() - for tag in _generic_tags(interpreter, py_version, abi, platforms): + for tag in generic_tags(): yield tag - for tag in _independent_tags(interpreter, py_version, platforms): + + for tag in compatible_tags(): yield tag diff --git a/src/pip/_vendor/packaging/utils.py b/src/pip/_vendor/packaging/utils.py index 88418786933..44f1bf98732 100644 --- a/src/pip/_vendor/packaging/utils.py +++ b/src/pip/_vendor/packaging/utils.py @@ -5,28 +5,33 @@ import re +from ._typing import MYPY_CHECK_RUNNING from .version import InvalidVersion, Version +if MYPY_CHECK_RUNNING: # pragma: no cover + from typing import Union _canonicalize_regex = re.compile(r"[-_.]+") def canonicalize_name(name): + # type: (str) -> str # This is taken from PEP 503. return _canonicalize_regex.sub("-", name).lower() -def canonicalize_version(version): +def canonicalize_version(_version): + # type: (str) -> Union[Version, str] """ - This is very similar to Version.__str__, but has one subtle differences + This is very similar to Version.__str__, but has one subtle difference with the way it handles the release segment. """ try: - version = Version(version) + version = Version(_version) except InvalidVersion: # Legacy versions cannot be normalized - return version + return _version parts = [] diff --git a/src/pip/_vendor/packaging/version.py b/src/pip/_vendor/packaging/version.py index 95157a1f78c..f39a2a12a1b 100644 --- a/src/pip/_vendor/packaging/version.py +++ b/src/pip/_vendor/packaging/version.py @@ -7,8 +7,35 @@ import itertools import re -from ._structures import Infinity - +from ._structures import Infinity, NegativeInfinity +from ._typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: # pragma: no cover + from typing import Callable, Iterator, List, Optional, SupportsInt, Tuple, Union + + from ._structures import InfinityType, NegativeInfinityType + + InfiniteTypes = Union[InfinityType, NegativeInfinityType] + PrePostDevType = Union[InfiniteTypes, Tuple[str, int]] + SubLocalType = Union[InfiniteTypes, int, str] + LocalType = Union[ + NegativeInfinityType, + Tuple[ + Union[ + SubLocalType, + Tuple[SubLocalType, str], + Tuple[NegativeInfinityType, SubLocalType], + ], + ..., + ], + ] + CmpKey = Tuple[ + int, Tuple[int, ...], PrePostDevType, PrePostDevType, PrePostDevType, LocalType + ] + LegacyCmpKey = Tuple[int, Tuple[str, ...]] + VersionComparisonMethod = Callable[ + [Union[CmpKey, LegacyCmpKey], Union[CmpKey, LegacyCmpKey]], bool + ] __all__ = ["parse", "Version", "LegacyVersion", "InvalidVersion", "VERSION_PATTERN"] @@ -19,6 +46,7 @@ def parse(version): + # type: (str) -> Union[LegacyVersion, Version] """ Parse the given version string and return either a :class:`Version` object or a :class:`LegacyVersion` object depending on if the given version is @@ -37,28 +65,38 @@ class InvalidVersion(ValueError): class _BaseVersion(object): + _key = None # type: Union[CmpKey, LegacyCmpKey] + def __hash__(self): + # type: () -> int return hash(self._key) def __lt__(self, other): + # type: (_BaseVersion) -> bool return self._compare(other, lambda s, o: s < o) def __le__(self, other): + # type: (_BaseVersion) -> bool return self._compare(other, lambda s, o: s <= o) def __eq__(self, other): + # type: (object) -> bool return self._compare(other, lambda s, o: s == o) def __ge__(self, other): + # type: (_BaseVersion) -> bool return self._compare(other, lambda s, o: s >= o) def __gt__(self, other): + # type: (_BaseVersion) -> bool return self._compare(other, lambda s, o: s > o) def __ne__(self, other): + # type: (object) -> bool return self._compare(other, lambda s, o: s != o) def _compare(self, other, method): + # type: (object, VersionComparisonMethod) -> Union[bool, NotImplemented] if not isinstance(other, _BaseVersion): return NotImplemented @@ -67,57 +105,71 @@ def _compare(self, other, method): class LegacyVersion(_BaseVersion): def __init__(self, version): + # type: (str) -> None self._version = str(version) self._key = _legacy_cmpkey(self._version) def __str__(self): + # type: () -> str return self._version def __repr__(self): + # type: () -> str return "<LegacyVersion({0})>".format(repr(str(self))) @property def public(self): + # type: () -> str return self._version @property def base_version(self): + # type: () -> str return self._version @property def epoch(self): + # type: () -> int return -1 @property def release(self): + # type: () -> None return None @property def pre(self): + # type: () -> None return None @property def post(self): + # type: () -> None return None @property def dev(self): + # type: () -> None return None @property def local(self): + # type: () -> None return None @property def is_prerelease(self): + # type: () -> bool return False @property def is_postrelease(self): + # type: () -> bool return False @property def is_devrelease(self): + # type: () -> bool return False @@ -133,6 +185,7 @@ def is_devrelease(self): def _parse_version_parts(s): + # type: (str) -> Iterator[str] for part in _legacy_version_component_re.split(s): part = _legacy_version_replacement_map.get(part, part) @@ -150,6 +203,8 @@ def _parse_version_parts(s): def _legacy_cmpkey(version): + # type: (str) -> LegacyCmpKey + # We hardcode an epoch of -1 here. A PEP 440 version can only have a epoch # greater than or equal to 0. This will effectively put the LegacyVersion, # which uses the defacto standard originally implemented by setuptools, @@ -158,7 +213,7 @@ def _legacy_cmpkey(version): # This scheme is taken from pkg_resources.parse_version setuptools prior to # it's adoption of the packaging library. - parts = [] + parts = [] # type: List[str] for part in _parse_version_parts(version.lower()): if part.startswith("*"): # remove "-" before a prerelease tag @@ -171,9 +226,8 @@ def _legacy_cmpkey(version): parts.pop() parts.append(part) - parts = tuple(parts) - return epoch, parts + return epoch, tuple(parts) # Deliberately not anchored to the start and end of the string, to make it @@ -215,6 +269,8 @@ class Version(_BaseVersion): _regex = re.compile(r"^\s*" + VERSION_PATTERN + r"\s*$", re.VERBOSE | re.IGNORECASE) def __init__(self, version): + # type: (str) -> None + # Validate the version and parse it into pieces match = self._regex.search(version) if not match: @@ -243,9 +299,11 @@ def __init__(self, version): ) def __repr__(self): + # type: () -> str return "<Version({0})>".format(repr(str(self))) def __str__(self): + # type: () -> str parts = [] # Epoch @@ -275,26 +333,35 @@ def __str__(self): @property def epoch(self): - return self._version.epoch + # type: () -> int + _epoch = self._version.epoch # type: int + return _epoch @property def release(self): - return self._version.release + # type: () -> Tuple[int, ...] + _release = self._version.release # type: Tuple[int, ...] + return _release @property def pre(self): - return self._version.pre + # type: () -> Optional[Tuple[str, int]] + _pre = self._version.pre # type: Optional[Tuple[str, int]] + return _pre @property def post(self): + # type: () -> Optional[Tuple[str, int]] return self._version.post[1] if self._version.post else None @property def dev(self): + # type: () -> Optional[Tuple[str, int]] return self._version.dev[1] if self._version.dev else None @property def local(self): + # type: () -> Optional[str] if self._version.local: return ".".join(str(x) for x in self._version.local) else: @@ -302,10 +369,12 @@ def local(self): @property def public(self): + # type: () -> str return str(self).split("+", 1)[0] @property def base_version(self): + # type: () -> str parts = [] # Epoch @@ -319,18 +388,41 @@ def base_version(self): @property def is_prerelease(self): + # type: () -> bool return self.dev is not None or self.pre is not None @property def is_postrelease(self): + # type: () -> bool return self.post is not None @property def is_devrelease(self): + # type: () -> bool return self.dev is not None + @property + def major(self): + # type: () -> int + return self.release[0] if len(self.release) >= 1 else 0 + + @property + def minor(self): + # type: () -> int + return self.release[1] if len(self.release) >= 2 else 0 + + @property + def micro(self): + # type: () -> int + return self.release[2] if len(self.release) >= 3 else 0 + + +def _parse_letter_version( + letter, # type: str + number, # type: Union[str, bytes, SupportsInt] +): + # type: (...) -> Optional[Tuple[str, int]] -def _parse_letter_version(letter, number): if letter: # We consider there to be an implicit 0 in a pre-release if there is # not a numeral associated with it. @@ -360,11 +452,14 @@ def _parse_letter_version(letter, number): return letter, int(number) + return None + _local_version_separators = re.compile(r"[\._-]") def _parse_local_version(local): + # type: (str) -> Optional[LocalType] """ Takes a string like abc.1.twelve and turns it into ("abc", 1, "twelve"). """ @@ -373,15 +468,25 @@ def _parse_local_version(local): part.lower() if not part.isdigit() else int(part) for part in _local_version_separators.split(local) ) + return None + +def _cmpkey( + epoch, # type: int + release, # type: Tuple[int, ...] + pre, # type: Optional[Tuple[str, int]] + post, # type: Optional[Tuple[str, int]] + dev, # type: Optional[Tuple[str, int]] + local, # type: Optional[Tuple[SubLocalType]] +): + # type: (...) -> CmpKey -def _cmpkey(epoch, release, pre, post, dev, local): # When we compare a release version, we want to compare it with all of the # trailing zeros removed. So we'll use a reverse the list, drop all the now # leading zeros until we come to something non zero, then take the rest # re-reverse it back into the correct order and make it a tuple and use # that for our sorting key. - release = tuple( + _release = tuple( reversed(list(itertools.dropwhile(lambda x: x == 0, reversed(release)))) ) @@ -390,23 +495,31 @@ def _cmpkey(epoch, release, pre, post, dev, local): # if there is not a pre or a post segment. If we have one of those then # the normal sorting rules will handle this case correctly. if pre is None and post is None and dev is not None: - pre = -Infinity + _pre = NegativeInfinity # type: PrePostDevType # Versions without a pre-release (except as noted above) should sort after # those with one. elif pre is None: - pre = Infinity + _pre = Infinity + else: + _pre = pre # Versions without a post segment should sort before those with one. if post is None: - post = -Infinity + _post = NegativeInfinity # type: PrePostDevType + + else: + _post = post # Versions without a development segment should sort after those with one. if dev is None: - dev = Infinity + _dev = Infinity # type: PrePostDevType + + else: + _dev = dev if local is None: # Versions without a local segment should sort before those with one. - local = -Infinity + _local = NegativeInfinity # type: LocalType else: # Versions with a local segment need that segment parsed to implement # the sorting rules in PEP440. @@ -415,6 +528,8 @@ def _cmpkey(epoch, release, pre, post, dev, local): # - Numeric segments sort numerically # - Shorter versions sort before longer versions when the prefixes # match exactly - local = tuple((i, "") if isinstance(i, int) else (-Infinity, i) for i in local) + _local = tuple( + (i, "") if isinstance(i, int) else (NegativeInfinity, i) for i in local + ) - return epoch, release, pre, post, dev, local + return epoch, _release, _pre, _post, _dev, _local From 44b664fd7cbb00ad043cc5e395eea545ed527572 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Mon, 6 Jan 2020 14:05:08 -0500 Subject: [PATCH 1066/3170] Add news --- news/packaging.vendor | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/packaging.vendor diff --git a/news/packaging.vendor b/news/packaging.vendor new file mode 100644 index 00000000000..a896b7e78f8 --- /dev/null +++ b/news/packaging.vendor @@ -0,0 +1 @@ +Update packaging to 20.0. From 4b5614c9e2c32abaa0d0d121eb16c5db7a3b5eac Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 23 Nov 2019 18:02:22 -0500 Subject: [PATCH 1067/3170] Replace get_impl_ver with interpreter_version --- src/pip/_internal/cache.py | 3 ++- src/pip/_internal/pep425tags.py | 15 ++------------- tests/unit/test_pep425tags.py | 4 ++-- 3 files changed, 6 insertions(+), 16 deletions(-) diff --git a/src/pip/_internal/cache.py b/src/pip/_internal/cache.py index 53c3aed927e..a4bfba118af 100644 --- a/src/pip/_internal/cache.py +++ b/src/pip/_internal/cache.py @@ -9,12 +9,13 @@ import logging import os +from pip._vendor.packaging.tags import interpreter_version from pip._vendor.packaging.utils import canonicalize_name from pip._internal.exceptions import InvalidWheelFilename from pip._internal.models.link import Link from pip._internal.models.wheel import Wheel -from pip._internal.pep425tags import interpreter_name, interpreter_version +from pip._internal.pep425tags import interpreter_name from pip._internal.utils.compat import expanduser from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index 3a291ebaeb1..d265fd96cb3 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -9,6 +9,7 @@ import sysconfig from collections import OrderedDict +from pip._vendor.packaging.tags import interpreter_version from pip._vendor.six import PY2 import pip._internal.utils.glibc @@ -63,18 +64,6 @@ def version_info_to_nodot(version_info): return ''.join(map(str, version_info[:2])) -def get_impl_ver(): - # type: () -> str - """Return implementation version.""" - impl_ver = get_config_var("py_version_nodot") - if not impl_ver or get_abbr_impl() == 'pp': - impl_ver = ''.join(map(str, get_impl_version_info())) - return impl_ver - - -interpreter_version = get_impl_ver - - def get_impl_version_info(): # type: () -> Tuple[int, ...] """Return sys.version_info-like tuple for use in decrementing the minor @@ -126,7 +115,7 @@ def get_abi_tag(): 'Py_UNICODE_SIZE', lambda: sys.maxunicode == 0x10ffff, expected=4, warn=is_cpython): u = 'u' - abi = '%s%s%s%s%s' % (impl, get_impl_ver(), d, m, u) + abi = '%s%s%s%s%s' % (impl, interpreter_version(), d, m, u) elif soabi and soabi.startswith('cpython-'): abi = 'cp' + soabi.split('-')[1] elif soabi: diff --git a/tests/unit/test_pep425tags.py b/tests/unit/test_pep425tags.py index 6de10b9d079..540aa8b7ce8 100644 --- a/tests/unit/test_pep425tags.py +++ b/tests/unit/test_pep425tags.py @@ -2,6 +2,7 @@ import pytest from mock import patch +from pip._vendor.packaging.tags import interpreter_version from pip._internal import pep425tags @@ -53,8 +54,7 @@ def abi_tag_unicode(self, flags, config_vars): import pip._internal.pep425tags config_vars.update({'SOABI': None}) - base = pip._internal.pep425tags.get_abbr_impl() + \ - pip._internal.pep425tags.get_impl_ver() + base = pip._internal.pep425tags.get_abbr_impl() + interpreter_version() if sys.version_info >= (3, 8): # Python 3.8 removes the m flag, so don't look for it. From 893faa9e449eecdaf09b91bc736bacf0dc2af7df Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 23 Nov 2019 18:02:23 -0500 Subject: [PATCH 1068/3170] Replace get_abbr_impl with interpreter_name This reduces the amount of code we have to manage. interpreter_name is calculated differently, defaulting to the long name of the interpreter rather than "cp", but that is more conformant. --- src/pip/_internal/cache.py | 3 +-- src/pip/_internal/pep425tags.py | 25 ++++--------------------- tests/unit/test_pep425tags.py | 4 ++-- 3 files changed, 7 insertions(+), 25 deletions(-) diff --git a/src/pip/_internal/cache.py b/src/pip/_internal/cache.py index a4bfba118af..3f5a78299fa 100644 --- a/src/pip/_internal/cache.py +++ b/src/pip/_internal/cache.py @@ -9,13 +9,12 @@ import logging import os -from pip._vendor.packaging.tags import interpreter_version +from pip._vendor.packaging.tags import interpreter_name, interpreter_version from pip._vendor.packaging.utils import canonicalize_name from pip._internal.exceptions import InvalidWheelFilename from pip._internal.models.link import Link from pip._internal.models.wheel import Wheel -from pip._internal.pep425tags import interpreter_name from pip._internal.utils.compat import expanduser from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index d265fd96cb3..9ba35823975 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -9,7 +9,7 @@ import sysconfig from collections import OrderedDict -from pip._vendor.packaging.tags import interpreter_version +from pip._vendor.packaging.tags import interpreter_name, interpreter_version from pip._vendor.six import PY2 import pip._internal.utils.glibc @@ -41,23 +41,6 @@ def get_config_var(var): return sysconfig.get_config_var(var) -def get_abbr_impl(): - # type: () -> str - """Return abbreviated implementation name.""" - if hasattr(sys, 'pypy_version_info'): - pyimpl = 'pp' - elif sys.platform.startswith('java'): - pyimpl = 'jy' - elif sys.platform == 'cli': - pyimpl = 'ip' - else: - pyimpl = 'cp' - return pyimpl - - -interpreter_name = get_abbr_impl - - def version_info_to_nodot(version_info): # type: (Tuple[int, ...]) -> str # Only use up to the first two numbers. @@ -68,7 +51,7 @@ def get_impl_version_info(): # type: () -> Tuple[int, ...] """Return sys.version_info-like tuple for use in decrementing the minor version.""" - if get_abbr_impl() == 'pp': + if interpreter_name() == 'pp': # as per https://github.com/pypa/pip/issues/2882 # attrs exist only on pypy return (sys.version_info[0], @@ -96,7 +79,7 @@ def get_abi_tag(): """Return the ABI tag based on SOABI (if available) or emulate SOABI (CPython 2, PyPy).""" soabi = get_config_var('SOABI') - impl = get_abbr_impl() + impl = interpreter_name() abi = None # type: Optional[str] if not soabi and impl in {'cp', 'pp'} and hasattr(sys, 'maxunicode'): @@ -385,7 +368,7 @@ def get_supported( current_version = versions[0] other_versions = versions[1:] - impl = impl or get_abbr_impl() + impl = impl or interpreter_name() abis = [] # type: List[str] diff --git a/tests/unit/test_pep425tags.py b/tests/unit/test_pep425tags.py index 540aa8b7ce8..b41edcc7bf5 100644 --- a/tests/unit/test_pep425tags.py +++ b/tests/unit/test_pep425tags.py @@ -2,7 +2,7 @@ import pytest from mock import patch -from pip._vendor.packaging.tags import interpreter_version +from pip._vendor.packaging.tags import interpreter_name, interpreter_version from pip._internal import pep425tags @@ -54,7 +54,7 @@ def abi_tag_unicode(self, flags, config_vars): import pip._internal.pep425tags config_vars.update({'SOABI': None}) - base = pip._internal.pep425tags.get_abbr_impl() + interpreter_version() + base = interpreter_name() + interpreter_version() if sys.version_info >= (3, 8): # Python 3.8 removes the m flag, so don't look for it. From 24a9d77e4121f33e5cb102b5f4e372d1475b78e6 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Mon, 6 Jan 2020 19:10:00 -0500 Subject: [PATCH 1069/3170] Move AppVeyor tests to Azure Pipelines This will allow us to have CI running concurrently on multiple PRs, since we get 30 parallel jobs on Azure Pipelines but only 1 on AppVeyor. We have parameterized --use-venv since AppVeyor was using it, but Azure Pipelines was previously not. --- .appveyor.yml | 69 -------------------- .azure-pipelines/jobs/test-windows.yml | 11 +++- .azure-pipelines/steps/run-tests-windows.yml | 6 +- 3 files changed, 15 insertions(+), 71 deletions(-) delete mode 100644 .appveyor.yml diff --git a/.appveyor.yml b/.appveyor.yml deleted file mode 100644 index 0da6202b2cd..00000000000 --- a/.appveyor.yml +++ /dev/null @@ -1,69 +0,0 @@ -environment: - matrix: - # Unit and integration tests. - - PYTHON: "C:\\Python27-x64" - RUN_INTEGRATION_TESTS: "True" - - PYTHON: "C:\\Python36-x64" - RUN_INTEGRATION_TESTS: "True" - # Unit tests only. - # Nothing for the moment - -matrix: - fast_finish: true - -clone_depth: 50 - -install: - - "SET PATH=%PYTHON%;%PYTHON%\\Scripts;%PATH%" - - "python --version" - - "python -m pip install --upgrade --disable-pip-version-check pip setuptools wheel" - - "pip install --upgrade certifi tox tox-venv" - - "pip freeze --all" - # Fix git SSL errors. - - "python -m certifi >cacert.txt" - - "set /p GIT_SSL_CAINFO=<cacert.txt" - - "set GIT_SSL_CAINFO" - -build: off - -cache: - - '%LOCALAPPDATA%\pip\Cache' - -test_script: - - ps: | - $ErrorActionPreference = "Stop" - - function should_run_tests { - if ("$env:APPVEYOR_PULL_REQUEST_NUMBER" -eq "") { - Write-Host "Not a pull request - running tests" - return $true - } - Write-Host "Pull request $env:APPVEYOR_PULL_REQUEST_NUMBER based on branch $env:APPVEYOR_REPO_BRANCH" - git fetch -q origin +refs/heads/$env:APPVEYOR_REPO_BRANCH - $changes = (git diff --name-only HEAD (git merge-base HEAD FETCH_HEAD)) - Write-Host "Files changed:" - Write-Host $changes - $important = $changes | Where-Object { $_ -NotLike "*.rst" } | - Where-Object { $_ -NotLike "docs*" } | - Where-Object { $_ -NotLike "news*" } | - Where-Object { $_ -NotLike "*travis*" } | - Where-Object { $_ -NotLike ".github*" } - if (!$important) { - Write-Host "Only documentation changes - skipping tests" - return $false - } - - Write-Host "Pull request $env:APPVEYOR_PULL_REQUEST_NUMBER alters code - running tests" - return $true - } - - if (should_run_tests) { - # Shorten paths, workaround https://bugs.python.org/issue18199 - subst T: $env:TEMP - $env:TEMP = "T:\" - $env:TMP = "T:\" - tox -e py -- -m unit -n auto - if ($LastExitCode -eq 0 -and $env:RUN_INTEGRATION_TESTS -eq "True") { - tox -e py -- --use-venv -m integration -n auto --durations=20 - } - } diff --git a/.azure-pipelines/jobs/test-windows.yml b/.azure-pipelines/jobs/test-windows.yml index 3d25587de41..f0ab8f812df 100644 --- a/.azure-pipelines/jobs/test-windows.yml +++ b/.azure-pipelines/jobs/test-windows.yml @@ -12,18 +12,27 @@ jobs: Python27-x86: python.version: '2.7' python.architecture: x86 + Python27-x64: + python.version: '2.7' + python.architecture: x64 + useVenv: true Python35-x64: python.version: '3.5' python.architecture: x64 + Python36-x64: + python.version: '3.6' + python.architecture: x64 + useVenv: true Python37-x64: python.version: '3.7' python.architecture: x64 - maxParallel: 3 + maxParallel: 5 steps: - template: ../steps/run-tests-windows.yml parameters: runIntegrationTests: true + useVenv: '$(useVenv)' - job: Test_Secondary displayName: Test Secondary diff --git a/.azure-pipelines/steps/run-tests-windows.yml b/.azure-pipelines/steps/run-tests-windows.yml index 9926754b0aa..30b0021cb00 100644 --- a/.azure-pipelines/steps/run-tests-windows.yml +++ b/.azure-pipelines/steps/run-tests-windows.yml @@ -1,5 +1,6 @@ parameters: runIntegrationTests: + useVenv: false steps: - task: UsePythonVersion@0 @@ -43,8 +44,11 @@ steps: # https://bugs.python.org/issue18199 $env:TEMP = "R:\Temp" - tox -e py -- -m integration -n auto --duration=5 --junit-xml=junit/integration-test.xml + tox -e py -- $env:USE_VENV_ARG -m integration -n auto --duration=5 --junit-xml=junit/integration-test.xml displayName: Tox run integration tests + env: + ${{ if eq(parameters.useVenv, 'true') }}: + USE_VENV_ARG: "--use-venv" - task: PublishTestResults@2 displayName: Publish Test Results From 927ea8be293f786b31e30f8503ce764ae2405cc4 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Mon, 6 Jan 2020 19:33:49 -0500 Subject: [PATCH 1070/3170] Replace AppVeyor with Azure in CI docs --- docs/html/development/ci.rst | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/docs/html/development/ci.rst b/docs/html/development/ci.rst index 620aa528cf5..e758705b28a 100644 --- a/docs/html/development/ci.rst +++ b/docs/html/development/ci.rst @@ -59,16 +59,14 @@ interpreters. Services ======== -pip test suite and checks are distributed on four different platforms that +pip test suite and checks are distributed on three different platforms that provides free executors for open source packages: - `Travis CI`_ (Used for Linux) - - `Appveyor CI`_ (Windows only) - `Azure DevOps CI`_ (Linux, MacOS & Windows tests) - `GitHub Actions`_ (Linux, MacOS & Windows tests) .. _`Travis CI`: https://travis-ci.org/ -.. _`Appveyor CI`: https://www.appveyor.com/ .. _`Azure DevOps CI`: https://azure.microsoft.com/en-us/services/devops/ .. _`GitHub Actions`: https://github.com/features/actions @@ -107,11 +105,11 @@ Actual testing | | +-------+---------------+-----------------+ | | | PyPy3 | | | | Windows +----------+-------+---------------+-----------------+ -| | | CP2.7 | Appveyor | Appveyor | +| | | CP2.7 | Azure | Azure | | | +-------+---------------+-----------------+ | | | CP3.5 | Azure | Azure | | | +-------+---------------+-----------------+ -| | | CP3.6 | Appveyor | Appveyor | +| | | CP3.6 | Azure | Azure | | | +-------+---------------+-----------------+ | | x64 | CP3.7 | Azure | Azure | | | +-------+---------------+-----------------+ From 838d64d865fa5a4c5ce36a51cdad1323ea515eb9 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 23 Nov 2019 18:02:26 -0500 Subject: [PATCH 1071/3170] Extract platform list into separate function Simplifies get_supported without changing any behavior. --- src/pip/_internal/pep425tags.py | 39 +++++++++++++++++++-------------- 1 file changed, 22 insertions(+), 17 deletions(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index 3a291ebaeb1..b9c0ead2cc7 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -366,6 +366,27 @@ def _custom_manylinux_platforms(arch): return arches +def _get_custom_platforms(arch, platform): + # type: (str, Optional[str]) -> List[str] + arch_prefix, arch_sep, arch_suffix = arch.partition('_') + if arch.startswith('macosx'): + arches = _mac_platforms(arch) + elif arch_prefix in ['manylinux2014', 'manylinux2010']: + arches = _custom_manylinux_platforms(arch) + elif platform is None: + arches = [] + if is_manylinux2014_compatible(): + arches.append('manylinux2014' + arch_sep + arch_suffix) + if is_manylinux2010_compatible(): + arches.append('manylinux2010' + arch_sep + arch_suffix) + if is_manylinux1_compatible(): + arches.append('manylinux1' + arch_sep + arch_suffix) + arches.append(arch) + else: + arches = [arch] + return arches + + def get_supported( version=None, # type: Optional[str] platform=None, # type: Optional[str] @@ -411,23 +432,7 @@ def get_supported( abis.append('none') - arch = platform or get_platform() - arch_prefix, arch_sep, arch_suffix = arch.partition('_') - if arch.startswith('macosx'): - arches = _mac_platforms(arch) - elif arch_prefix in ['manylinux2014', 'manylinux2010']: - arches = _custom_manylinux_platforms(arch) - elif platform is None: - arches = [] - if is_manylinux2014_compatible(): - arches.append('manylinux2014' + arch_sep + arch_suffix) - if is_manylinux2010_compatible(): - arches.append('manylinux2010' + arch_sep + arch_suffix) - if is_manylinux1_compatible(): - arches.append('manylinux1' + arch_sep + arch_suffix) - arches.append(arch) - else: - arches = [arch] + arches = _get_custom_platforms(platform or get_platform(), platform) # Current version, current API (built specifically for our Python): for abi in abis: From 32ecd727d5c57ed1575933cc20332ee002560ae1 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 7 Jan 2020 10:50:51 +0530 Subject: [PATCH 1072/3170] Rename custom option class for future expansion --- src/pip/_internal/cli/cmdoptions.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index 8502099dd31..42e26951af2 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -135,7 +135,7 @@ def _path_option_check(option, opt, value): return os.path.expanduser(value) -class _PathOption(Option): +class PipOption(Option): TYPES = Option.TYPES + ("path",) TYPE_CHECKER = Option.TYPE_CHECKER.copy() TYPE_CHECKER["path"] = _path_option_check @@ -228,7 +228,7 @@ class _PathOption(Option): ) # type: Callable[..., Option] log = partial( - _PathOption, + PipOption, "--log", "--log-file", "--local-log", dest="log", metavar="path", @@ -303,7 +303,7 @@ def exists_action(): cert = partial( - _PathOption, + PipOption, '--cert', dest='cert', type='path', @@ -312,7 +312,7 @@ def exists_action(): ) # type: Callable[..., Option] client_cert = partial( - _PathOption, + PipOption, '--client-cert', dest='client_cert', type='path', @@ -432,7 +432,7 @@ def _handle_src(option, opt_str, value, parser): src = partial( - _PathOption, + PipOption, '--src', '--source', '--source-dir', '--source-directory', dest='src_dir', type='path', @@ -638,7 +638,7 @@ def prefer_binary(): cache_dir = partial( - _PathOption, + PipOption, "--cache-dir", dest="cache_dir", default=USER_CACHE_DIR, @@ -703,7 +703,7 @@ def _handle_build_dir(option, opt, value, parser): build_dir = partial( - _PathOption, + PipOption, '-b', '--build', '--build-dir', '--build-directory', dest='build_dir', type='path', @@ -887,7 +887,7 @@ def _handle_merge_hash(option, opt_str, value, parser): list_path = partial( - _PathOption, + PipOption, '--path', dest='path', type='path', From 9bf6a3b3fbb0bf4606928612ef11a15bf95c6aa1 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 7 Jan 2020 10:57:34 +0530 Subject: [PATCH 1073/3170] List URL/path options in news --- news/980.feature | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/news/980.feature b/news/980.feature index 60a74a90a91..cd055e09e6e 100644 --- a/news/980.feature +++ b/news/980.feature @@ -1,2 +1,8 @@ -Expand ``~`` prefix to user directory in path options, configs, and environment -variables. +Expand ``~`` prefix to user directory in path options, configs, and +environment variables. Values that may be either URL or path are not +currently supported, to avoid ambiguity: + +* ``--find-links`` +* ``--constraint``, ``-c`` +* ``--requirement``, ``-r`` +* ``--editable``, ``-e`` From 80b2c82d0c472deb40b09da5b045786c85b9a159 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 23 Nov 2019 18:02:25 -0500 Subject: [PATCH 1074/3170] Use packaging.tags.Tag in place of Tuple This is the standard type used by packaging.tags. Making this change throughout the code lets us start switching over to using its tag-generating functions in get_supported(). We also get rid of a test, since it was superseded by `__str__` in packaging.tags.Tag. --- src/pip/_internal/cache.py | 10 +++--- src/pip/_internal/commands/debug.py | 3 +- src/pip/_internal/index/package_finder.py | 6 ++-- src/pip/_internal/models/target_python.py | 7 +++-- src/pip/_internal/models/wheel.py | 13 ++++---- src/pip/_internal/pep425tags.py | 12 ++++--- tests/unit/test_cache.py | 8 +++-- tests/unit/test_finder.py | 7 +++-- tests/unit/test_models_wheel.py | 13 ++++---- tests/unit/test_pep425tags.py | 38 ++++++++++++++--------- 10 files changed, 68 insertions(+), 49 deletions(-) diff --git a/src/pip/_internal/cache.py b/src/pip/_internal/cache.py index 3f5a78299fa..c0e002c414e 100644 --- a/src/pip/_internal/cache.py +++ b/src/pip/_internal/cache.py @@ -22,8 +22,10 @@ if MYPY_CHECK_RUNNING: from typing import Optional, Set, List, Any, Dict + + from pip._vendor.packaging.tags import Tag + from pip._internal.models.format_control import FormatControl - from pip._internal.pep425tags import Pep425Tag logger = logging.getLogger(__name__) @@ -161,7 +163,7 @@ def get( self, link, # type: Link package_name, # type: Optional[str] - supported_tags, # type: List[Pep425Tag] + supported_tags, # type: List[Tag] ): # type: (...) -> Link """Returns a link to a cached item if it exists, otherwise returns the @@ -214,7 +216,7 @@ def get( self, link, # type: Link package_name, # type: Optional[str] - supported_tags, # type: List[Pep425Tag] + supported_tags, # type: List[Tag] ): # type: (...) -> Link candidates = [] @@ -304,7 +306,7 @@ def get( self, link, # type: Link package_name, # type: Optional[str] - supported_tags, # type: List[Pep425Tag] + supported_tags, # type: List[Tag] ): # type: (...) -> Link retval = self._wheel_cache.get( diff --git a/src/pip/_internal/commands/debug.py b/src/pip/_internal/commands/debug.py index 2a262e759eb..fe93b3a3926 100644 --- a/src/pip/_internal/commands/debug.py +++ b/src/pip/_internal/commands/debug.py @@ -14,7 +14,6 @@ from pip._internal.cli.base_command import Command from pip._internal.cli.cmdoptions import make_target_python from pip._internal.cli.status_codes import SUCCESS -from pip._internal.pep425tags import format_tag from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import get_pip_version from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -68,7 +67,7 @@ def show_tags(options): with indent_log(): for tag in tags: - logger.info(format_tag(tag)) + logger.info(str(tag)) if tags_limited: msg = ( diff --git a/src/pip/_internal/index/package_finder.py b/src/pip/_internal/index/package_finder.py index d9ee8b5a877..a74d78db5a6 100644 --- a/src/pip/_internal/index/package_finder.py +++ b/src/pip/_internal/index/package_finder.py @@ -37,11 +37,13 @@ from typing import ( FrozenSet, Iterable, List, Optional, Set, Text, Tuple, Union, ) + + from pip._vendor.packaging.tags import Tag from pip._vendor.packaging.version import _BaseVersion + from pip._internal.index.collector import LinkCollector from pip._internal.models.search_scope import SearchScope from pip._internal.req import InstallRequirement - from pip._internal.pep425tags import Pep425Tag from pip._internal.utils.hashes import Hashes BuildTag = Union[Tuple[()], Tuple[int, str]] @@ -425,7 +427,7 @@ def create( def __init__( self, project_name, # type: str - supported_tags, # type: List[Pep425Tag] + supported_tags, # type: List[Tag] specifier, # type: specifiers.BaseSpecifier prefer_binary=False, # type: bool allow_all_prereleases=False, # type: bool diff --git a/src/pip/_internal/models/target_python.py b/src/pip/_internal/models/target_python.py index cd08c917899..97ae85a0945 100644 --- a/src/pip/_internal/models/target_python.py +++ b/src/pip/_internal/models/target_python.py @@ -6,7 +6,8 @@ if MYPY_CHECK_RUNNING: from typing import List, Optional, Tuple - from pip._internal.pep425tags import Pep425Tag + + from pip._vendor.packaging.tags import Tag class TargetPython(object): @@ -55,7 +56,7 @@ def __init__( self.py_version_info = py_version_info # This is used to cache the return value of get_tags(). - self._valid_tags = None # type: Optional[List[Pep425Tag]] + self._valid_tags = None # type: Optional[List[Tag]] def format_given(self): # type: () -> str @@ -80,7 +81,7 @@ def format_given(self): ) def get_tags(self): - # type: () -> List[Pep425Tag] + # type: () -> List[Tag] """ Return the supported PEP 425 tags to check wheel candidates against. diff --git a/src/pip/_internal/models/wheel.py b/src/pip/_internal/models/wheel.py index 4c7a8af78fa..f1e3f44c598 100644 --- a/src/pip/_internal/models/wheel.py +++ b/src/pip/_internal/models/wheel.py @@ -3,15 +3,14 @@ """ import re +from pip._vendor.packaging.tags import Tag + from pip._internal.exceptions import InvalidWheelFilename -from pip._internal.pep425tags import format_tag from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: from typing import List - from pip._internal.pep425tags import Pep425Tag - class Wheel(object): """A wheel file""" @@ -45,17 +44,17 @@ def __init__(self, filename): # All the tag combinations from this file self.file_tags = { - (x, y, z) for x in self.pyversions + Tag(x, y, z) for x in self.pyversions for y in self.abis for z in self.plats } def get_formatted_file_tags(self): # type: () -> List[str] """Return the wheel's tags as a sorted list of strings.""" - return sorted(format_tag(tag) for tag in self.file_tags) + return sorted(str(tag) for tag in self.file_tags) def support_index_min(self, tags): - # type: (List[Pep425Tag]) -> int + # type: (List[Tag]) -> int """Return the lowest index that one of the wheel's file_tag combinations achieves in the given list of supported tags. @@ -71,7 +70,7 @@ def support_index_min(self, tags): return min(tags.index(tag) for tag in self.file_tags if tag in tags) def supported(self, tags): - # type: (List[Pep425Tag]) -> bool + # type: (List[Tag]) -> bool """Return whether the wheel is compatible with one of the given tags. :param tags: the PEP 425 tags to check the wheel against. diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index 841597596b2..f2e62c93896 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -9,7 +9,11 @@ import sysconfig from collections import OrderedDict -from pip._vendor.packaging.tags import interpreter_name, interpreter_version +from pip._vendor.packaging.tags import ( + Tag, + interpreter_name, + interpreter_version, +) from pip._vendor.six import PY2 import pip._internal.utils.glibc @@ -20,8 +24,6 @@ Tuple, Callable, List, Optional, Union, Dict ) - Pep425Tag = Tuple[str, str, str] - logger = logging.getLogger(__name__) _osx_arch_pat = re.compile(r'(.+)_(\d+)_(\d+)_(.+)') @@ -365,7 +367,7 @@ def get_supported( impl=None, # type: Optional[str] abi=None # type: Optional[str] ): - # type: (...) -> List[Pep425Tag] + # type: (...) -> List[Tag] """Return a list of supported tags for each version specified in `versions`. @@ -433,4 +435,4 @@ def get_supported( for version in other_versions: supported.append(('py%s' % (version,), 'none', 'any')) - return supported + return [Tag(*parts) for parts in supported] diff --git a/tests/unit/test_cache.py b/tests/unit/test_cache.py index 5d81fc01d14..1a4f98b28bd 100644 --- a/tests/unit/test_cache.py +++ b/tests/unit/test_cache.py @@ -1,5 +1,7 @@ import os +from pip._vendor.packaging.tags import Tag + from pip._internal.cache import WheelCache, _hash_dict from pip._internal.models.format_control import FormatControl from pip._internal.models.link import Link @@ -39,11 +41,11 @@ def test_wheel_name_filter(tmpdir): with open(os.path.join(cache_path, "package-1.0-py3-none-any.whl"), "w"): pass # package matches wheel name - cached_link = wc.get(link, "package", [("py3", "none", "any")]) + cached_link = wc.get(link, "package", [Tag("py3", "none", "any")]) assert cached_link is not link assert os.path.exists(cached_link.file_path) # package2 does not match wheel name - assert wc.get(link, "package2", [("py3", "none", "any")]) is link + assert wc.get(link, "package2", [Tag("py3", "none", "any")]) is link def test_cache_hash(): @@ -89,7 +91,7 @@ def test_get_with_legacy_entry_only(tmpdir): ensure_dir(legacy_path) with open(os.path.join(legacy_path, "test-1.0.0-py3-none-any.whl"), "w"): pass - cached_link = wc.get(link, "test", [("py3", "none", "any")]) + cached_link = wc.get(link, "test", [Tag("py3", "none", "any")]) assert ( os.path.normcase(os.path.dirname(cached_link.file_path)) == os.path.normcase(legacy_path) diff --git a/tests/unit/test_finder.py b/tests/unit/test_finder.py index b8e179c115e..fb2326c82d5 100644 --- a/tests/unit/test_finder.py +++ b/tests/unit/test_finder.py @@ -4,6 +4,7 @@ import pytest from mock import Mock, patch from pip._vendor.packaging.specifiers import SpecifierSet +from pip._vendor.packaging.tags import Tag from pkg_resources import parse_version import pip._internal.pep425tags @@ -238,9 +239,9 @@ def test_link_sorting(self): ), ] valid_tags = [ - ('pyT', 'none', 'TEST'), - ('pyT', 'TEST', 'any'), - ('pyT', 'none', 'any'), + Tag('pyT', 'none', 'TEST'), + Tag('pyT', 'TEST', 'any'), + Tag('pyT', 'none', 'any'), ] specifier = SpecifierSet() evaluator = CandidateEvaluator( diff --git a/tests/unit/test_models_wheel.py b/tests/unit/test_models_wheel.py index 8b351356d83..f74dd367954 100644 --- a/tests/unit/test_models_wheel.py +++ b/tests/unit/test_models_wheel.py @@ -1,4 +1,5 @@ import pytest +from pip._vendor.packaging.tags import Tag from pip._internal import pep425tags from pip._internal.exceptions import InvalidWheelFilename @@ -54,21 +55,21 @@ def test_supported_single_version(self): Test single-version wheel is known to be supported """ w = Wheel('simple-0.1-py2-none-any.whl') - assert w.supported(tags=[('py2', 'none', 'any')]) + assert w.supported(tags=[Tag('py2', 'none', 'any')]) def test_supported_multi_version(self): """ Test multi-version wheel is known to be supported """ w = Wheel('simple-0.1-py2.py3-none-any.whl') - assert w.supported(tags=[('py3', 'none', 'any')]) + assert w.supported(tags=[Tag('py3', 'none', 'any')]) def test_not_supported_version(self): """ Test unsupported wheel is known to be unsupported """ w = Wheel('simple-0.1-py2-none-any.whl') - assert not w.supported(tags=[('py1', 'none', 'any')]) + assert not w.supported(tags=[Tag('py1', 'none', 'any')]) def test_supported_osx_version(self): """ @@ -153,9 +154,9 @@ def test_support_index_min(self): Test results from `support_index_min` """ tags = [ - ('py2', 'none', 'TEST'), - ('py2', 'TEST', 'any'), - ('py2', 'none', 'any'), + Tag('py2', 'none', 'TEST'), + Tag('py2', 'TEST', 'any'), + Tag('py2', 'none', 'any'), ] w = Wheel('simple-0.1-py2-none-any.whl') assert w.support_index_min(tags=tags) == 2 diff --git a/tests/unit/test_pep425tags.py b/tests/unit/test_pep425tags.py index b41edcc7bf5..14ea5f0ac96 100644 --- a/tests/unit/test_pep425tags.py +++ b/tests/unit/test_pep425tags.py @@ -98,10 +98,10 @@ def test_no_hyphen_tag(self): mock_gcf): supported = pip._internal.pep425tags.get_supported() - for (py, abi, plat) in supported: - assert '-' not in py - assert '-' not in abi - assert '-' not in plat + for tag in supported: + assert '-' not in tag.interpreter + assert '-' not in tag.abi + assert '-' not in tag.platform def test_manual_abi_noflags(self): """ @@ -192,8 +192,10 @@ def test_manylinux1_tag_is_first(self): Test that the more specific tag manylinux1 comes first. """ groups = {} - for pyimpl, abi, arch in pep425tags.get_supported(): - groups.setdefault((pyimpl, abi), []).append(arch) + for tag in pep425tags.get_supported(): + groups.setdefault( + (tag.interpreter, tag.abi), [] + ).append(tag.platform) for arches in groups.values(): if arches == ['any']: @@ -218,8 +220,10 @@ def test_manylinux2010_tag_is_first(self): Test that the more specific tag manylinux2010 comes first. """ groups = {} - for pyimpl, abi, arch in pep425tags.get_supported(): - groups.setdefault((pyimpl, abi), []).append(arch) + for tag in pep425tags.get_supported(): + groups.setdefault( + (tag.interpreter, tag.abi), [] + ).append(tag.platform) for arches in groups.values(): if arches == ['any']: @@ -245,8 +249,10 @@ def test_manylinux2010_implies_manylinux1(self, manylinux2010, manylinux1): """ groups = {} supported = pep425tags.get_supported(platform=manylinux2010) - for pyimpl, abi, arch in supported: - groups.setdefault((pyimpl, abi), []).append(arch) + for tag in supported: + groups.setdefault( + (tag.interpreter, tag.abi), [] + ).append(tag.platform) for arches in groups.values(): if arches == ['any']: @@ -265,8 +271,10 @@ def test_manylinux2014_tag_is_first(self): Test that the more specific tag manylinux2014 comes first. """ groups = {} - for pyimpl, abi, arch in pep425tags.get_supported(): - groups.setdefault((pyimpl, abi), []).append(arch) + for tag in pep425tags.get_supported(): + groups.setdefault( + (tag.interpreter, tag.abi), [] + ).append(tag.platform) for arches in groups.values(): if arches == ['any']: @@ -295,8 +303,10 @@ def test_manylinuxA_implies_manylinuxB(self, manylinuxA, manylinuxB): """ groups = {} supported = pep425tags.get_supported(platform=manylinuxA) - for pyimpl, abi, arch in supported: - groups.setdefault((pyimpl, abi), []).append(arch) + for tag in supported: + groups.setdefault( + (tag.interpreter, tag.abi), [] + ).append(tag.platform) expected_arches = [manylinuxA] expected_arches.extend(manylinuxB) From 58f175fdf2505d119467c78222d9736cf54dc968 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 23 Nov 2019 18:02:26 -0500 Subject: [PATCH 1075/3170] Remove unused format_tag Since we delegate tag formatting to packaging.tags.Tag, we don't need this function or its tests. --- src/pip/_internal/pep425tags.py | 9 --------- tests/unit/test_pep425tags.py | 9 --------- 2 files changed, 18 deletions(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index f2e62c93896..25c0cedd681 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -29,15 +29,6 @@ _osx_arch_pat = re.compile(r'(.+)_(\d+)_(\d+)_(.+)') -def format_tag(file_tag): - # type: (Tuple[str, ...]) -> str - """Format three tags in the form "<python_tag>-<abi_tag>-<platform_tag>". - - :param file_tag: A 3-tuple of tags (python_tag, abi_tag, platform_tag). - """ - return '-'.join(file_tag) - - def get_config_var(var): # type: (str) -> Optional[str] return sysconfig.get_config_var(var) diff --git a/tests/unit/test_pep425tags.py b/tests/unit/test_pep425tags.py index 14ea5f0ac96..eaa88888fb0 100644 --- a/tests/unit/test_pep425tags.py +++ b/tests/unit/test_pep425tags.py @@ -7,15 +7,6 @@ from pip._internal import pep425tags -@pytest.mark.parametrize('file_tag, expected', [ - (('py27', 'none', 'any'), 'py27-none-any'), - (('cp33', 'cp32dmu', 'linux_x86_64'), 'cp33-cp32dmu-linux_x86_64'), -]) -def test_format_tag(file_tag, expected): - actual = pep425tags.format_tag(file_tag) - assert actual == expected - - @pytest.mark.parametrize('version_info, expected', [ ((2,), '2'), ((2, 8), '28'), From 4ee779f79e297982422018bfc7603f07fba59215 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 7 Jan 2020 12:21:13 +0530 Subject: [PATCH 1076/3170] Drop no longer valid tests --- tests/functional/test_install.py | 81 -------------------------------- 1 file changed, 81 deletions(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index e9cadca20df..713c9518b5d 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -10,7 +10,6 @@ import pytest -from pip import __version__ as pip_current_version from pip._internal.cli.status_codes import ERROR, SUCCESS from pip._internal.models.index import PyPI, TestPyPI from pip._internal.utils.misc import rmtree @@ -1658,86 +1657,6 @@ def test_user_config_accepted(script): assert join(relative_user, 'simplewheel') in result.files_created -@pytest.mark.network -@pytest.mark.skipif("sys.platform != 'win32'") -@pytest.mark.parametrize('pip_name', [ - 'pip.exe', - 'pip{}.exe'.format(sys.version_info[0]), - 'pip{}.{}.exe'.format(*sys.version_info[:2]) -]) -def test_protect_pip_from_modification_on_windows(script, pip_name): - """ - Test that pip modification command using ``pip install ...`` - raises an error on Windows. - """ - command = [pip_name, 'install', 'pip != {}'.format(pip_current_version)] - result = script.run(*command, expect_error=True) - new_command = [sys.executable, '-m', 'pip'] + command[1:] - expected_message = ( - 'To modify pip, please run the following command:\n{}' - .format(' '.join(new_command)) - ) - assert expected_message in result.stderr, str(result) - - -@pytest.mark.network -@pytest.mark.skipif("sys.platform != 'win32'") -def test_protect_pip_from_modification_via_deps_on_windows(script): - """ - Test ``pip install pkga`` raises an error on Windows - if `pkga` implicitly tries to upgrade pip. - """ - pkga_wheel_path = create_basic_wheel_for_package( - script, - 'pkga', '0.1', - depends=['pip != {}'.format(pip_current_version)], - ) - - # Make sure pip install pkga raises an error - args = ['install', pkga_wheel_path] - result = script.pip(*args, expect_error=True, use_module=False) - new_command = [sys.executable, '-m', 'pip'] + args - expected_message = ( - 'To modify pip, please run the following command:\n{}' - .format(' '.join(new_command)) - ) - assert expected_message in result.stderr, str(result) - - -@pytest.mark.network -@pytest.mark.skipif("sys.platform != 'win32'") -def test_protect_pip_from_modification_via_sub_deps_on_windows(script): - """ - Test ``pip install pkga`` raises an error on Windows - if sub-dependencies of `pkga` implicitly tries to upgrade pip. - """ - # Make a wheel for pkga which requires pkgb - pkga_wheel_path = create_basic_wheel_for_package( - script, - 'pkga', '0.1', - depends=['pkgb'], - ) - - # Make a wheel for pkgb which requires pip - pkgb_wheel_path = create_basic_wheel_for_package( - script, - 'pkgb', '0.1', - depends=['pip != {}'.format(pip_current_version)], - ) - - # Make sure pip install pkga raises an error - args = [ - 'install', pkga_wheel_path, '--find-links', pkgb_wheel_path.parent - ] - result = script.pip(*args, expect_error=True, use_module=False) - new_command = [sys.executable, '-m', 'pip'] + args - expected_message = ( - 'To modify pip, please run the following command:\n{}' - .format(' '.join(new_command)) - ) - assert expected_message in result.stderr, str(result) - - @pytest.mark.parametrize( 'install_args, expected_message', [ ([], 'Requirement already satisfied: pip in'), From 687a00b3e1ab64cf7b2a8b48daeb1375011b7b73 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 23 Nov 2019 18:02:21 -0500 Subject: [PATCH 1077/3170] Sort pep425tags typing imports Makes the next change easier to review. --- src/pip/_internal/pep425tags.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index 25c0cedd681..d2119443bc8 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -21,7 +21,7 @@ if MYPY_CHECK_RUNNING: from typing import ( - Tuple, Callable, List, Optional, Union, Dict + Callable, Dict, List, Optional, Tuple, Union ) logger = logging.getLogger(__name__) From bc20a983daf56f121f2eedbbbc866a079b207d28 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 23 Nov 2019 18:02:22 -0500 Subject: [PATCH 1078/3170] Use packaging.tags.mac_platforms This function takes care of the version iteration AND architecture determination internally. --- src/pip/_internal/pep425tags.py | 82 +++++---------------------------- 1 file changed, 12 insertions(+), 70 deletions(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index d2119443bc8..7b7a3551b71 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -7,12 +7,12 @@ import re import sys import sysconfig -from collections import OrderedDict from pip._vendor.packaging.tags import ( Tag, interpreter_name, interpreter_version, + mac_platforms, ) from pip._vendor.six import PY2 @@ -21,7 +21,7 @@ if MYPY_CHECK_RUNNING: from typing import ( - Callable, Dict, List, Optional, Tuple, Union + Callable, List, Optional, Tuple, Union ) logger = logging.getLogger(__name__) @@ -220,69 +220,6 @@ def is_manylinux2014_compatible(): return pip._internal.utils.glibc.have_compatible_glibc(2, 17) -def get_darwin_arches(major, minor, machine): - # type: (int, int, str) -> List[str] - """Return a list of supported arches (including group arches) for - the given major, minor and machine architecture of an macOS machine. - """ - arches = [] - - def _supports_arch(major, minor, arch): - # type: (int, int, str) -> bool - # Looking at the application support for macOS versions in the chart - # provided by https://en.wikipedia.org/wiki/OS_X#Versions it appears - # our timeline looks roughly like: - # - # 10.0 - Introduces ppc support. - # 10.4 - Introduces ppc64, i386, and x86_64 support, however the ppc64 - # and x86_64 support is CLI only, and cannot be used for GUI - # applications. - # 10.5 - Extends ppc64 and x86_64 support to cover GUI applications. - # 10.6 - Drops support for ppc64 - # 10.7 - Drops support for ppc - # - # Given that we do not know if we're installing a CLI or a GUI - # application, we must be conservative and assume it might be a GUI - # application and behave as if ppc64 and x86_64 support did not occur - # until 10.5. - # - # Note: The above information is taken from the "Application support" - # column in the chart not the "Processor support" since I believe - # that we care about what instruction sets an application can use - # not which processors the OS supports. - if arch == 'ppc': - return (major, minor) <= (10, 5) - if arch == 'ppc64': - return (major, minor) == (10, 5) - if arch == 'i386': - return (major, minor) >= (10, 4) - if arch == 'x86_64': - return (major, minor) >= (10, 5) - if arch in groups: - for garch in groups[arch]: - if _supports_arch(major, minor, garch): - return True - return False - - groups = OrderedDict([ - ("fat", ("i386", "ppc")), - ("intel", ("x86_64", "i386")), - ("fat64", ("x86_64", "ppc64")), - ("fat32", ("x86_64", "i386", "ppc")), - ]) # type: Dict[str, Tuple[str, ...]] - - if _supports_arch(major, minor, machine): - arches.append(machine) - - for garch in groups: - if machine in groups[garch] and _supports_arch(major, minor, garch): - arches.append(garch) - - arches.append('universal') - - return arches - - def get_all_minor_versions_as_strings(version_info): # type: (Tuple[int, ...]) -> List[str] versions = [] @@ -298,11 +235,16 @@ def _mac_platforms(arch): match = _osx_arch_pat.match(arch) if match: name, major, minor, actual_arch = match.groups() - tpl = '{}_{}_%i_%s'.format(name, major) - arches = [] - for m in reversed(range(int(minor) + 1)): - for a in get_darwin_arches(int(major), m, actual_arch): - arches.append(tpl % (m, a)) + mac_version = (int(major), int(minor)) + arches = [ + # Since we have always only checked that the platform starts + # with "macosx", for backwards-compatibility we extract the + # actual prefix provided by the user in case they provided + # something like "macosxcustom_". It may be good to remove + # this as undocumented or deprecate it in the future. + '{}_{}'.format(name, arch[len('macosx_'):]) + for arch in mac_platforms(mac_version, actual_arch) + ] else: # arch pattern didn't match (?!) arches = [arch] From 555d05ce0da968b80a8243807133f21b57b89ab9 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 7 Jan 2020 20:40:36 +0530 Subject: [PATCH 1079/3170] Use logger to log instead of logging --- src/pip/_internal/req/req_uninstall.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/req/req_uninstall.py b/src/pip/_internal/req/req_uninstall.py index 544f296592f..5971b130ec0 100644 --- a/src/pip/_internal/req/req_uninstall.py +++ b/src/pip/_internal/req/req_uninstall.py @@ -295,7 +295,7 @@ def rollback(self): # type: () -> None """Undoes the uninstall by moving stashed files back.""" for p in self._moves: - logging.info("Moving to %s\n from %s", *p) + logger.info("Moving to %s\n from %s", *p) for new_path, path in self._moves: try: From 1ea7a05df69590533e4cd5c85acf6650a91223bd Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 7 Jan 2020 20:49:55 +0530 Subject: [PATCH 1080/3170] One more found by grepping --- src/pip/_internal/operations/check.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/operations/check.py b/src/pip/_internal/operations/check.py index 6bd18841a20..b85a12306a4 100644 --- a/src/pip/_internal/operations/check.py +++ b/src/pip/_internal/operations/check.py @@ -53,7 +53,7 @@ def create_package_set_from_installed(**kwargs): package_set[name] = PackageDetails(dist.version, dist.requires()) except RequirementParseError as e: # Don't crash on broken metadata - logging.warning("Error parsing requirements for %s: %s", name, e) + logger.warning("Error parsing requirements for %s: %s", name, e) problems = True return package_set, problems From 3ada01fb51fdbc168e04a15f8eac1b77542e19d2 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 23 Nov 2019 18:02:27 -0500 Subject: [PATCH 1081/3170] Convert return values in pep425tags.get_supported Now we can incrementally use utility functions from pep425tags without switching everything at once or converting in multiple places. --- src/pip/_internal/pep425tags.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index 7b7a3551b71..dc0aaed6533 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -313,7 +313,7 @@ def get_supported( :param abi: specify the exact abi you want valid tags for, or None. If None, use the local interpreter abi. """ - supported = [] + supported = [] # type: List[Union[Tag, Tuple[str, str, str]]] # Versions must be given with respect to the preference if version is None: @@ -368,4 +368,7 @@ def get_supported( for version in other_versions: supported.append(('py%s' % (version,), 'none', 'any')) - return [Tag(*parts) for parts in supported] + return [ + parts if isinstance(parts, Tag) else Tag(*parts) + for parts in supported + ] From d386bb2fffd1af7d164da44556190cbde83e8146 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 23 Nov 2019 18:02:28 -0500 Subject: [PATCH 1082/3170] Copy get_supported into packaging.tags-like functions packaging.tags provides a simple `sys_tags` function for getting applicable tags for the running interpreter, but it does not allow customization of arguments. packaging.tags provides three functions for getting custom tags: 1. `cpython_tags` - for CPython only 2. `generic_tags` - for any non-CPython Python implementation 3. `compatible_tags` - tags that are not specific to an interpreter implementation Since pip allows users to provide explicit impl, platform, abi, and version, we have to use these functions. `cpython_tags` and `generic_tags` are mutually exclusive, and return tags that are the highest priority. These capture the most specific tags. `compatible_tags` are always applicable, and a lower priority since they may just be compatible with e.g. the Python language version, but lack any optimizations available in the interpreter-specific tags. To be able to do a meaningful comparison between our current implementation and the above functions, we need to segment the pip code into pieces that look like them. To that end, we now have copies of the current `get_supported` function, one for each of the functions provided by packaging.tags, and will walk through converting them to rely on packaging.tags. For each simplification step, if desired, we can compare the implementation in the packaging.tags function with what we're replacing in pip. Specifically, for each function in turn, we will: 1. Refactor it locally, taking into account its new, more limited, responsibilities 2. Introduce the packaging.tags function for the case where there are no custom arguments provided 3. Customize arguments one-by-one and delegate to the packaging.tags function 4. When there is no pip-specific logic left, remove the intermediate function and use the packaging.tags function directly in `get_supported` In the end all these functions will be gone again and we'll be left with an implementation that relies solely on the tag generation in packaging.tags. --- src/pip/_internal/pep425tags.py | 195 ++++++++++++++++++++++++++++++++ 1 file changed, 195 insertions(+) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index dc0aaed6533..915f5bcf19e 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -294,6 +294,201 @@ def _get_custom_platforms(arch, platform): return arches +def _cpython_tags( + version=None, # type: Optional[str] + platform=None, # type: Optional[str] + impl=None, # type: Optional[str] + abi=None, # type: Optional[str] +): + # type: (...) -> List[Tuple[str, str, str]] + supported = [] # type: List[Tuple[str, str, str]] + + # Versions must be given with respect to the preference + if version is None: + version_info = get_impl_version_info() + versions = get_all_minor_versions_as_strings(version_info) + else: + versions = [version] + current_version = versions[0] + other_versions = versions[1:] + + impl = impl or interpreter_name() + + abis = [] # type: List[str] + + abi = abi or get_abi_tag() + if abi: + abis[0:0] = [abi] + + supports_abi3 = not PY2 and impl == "cp" + + if supports_abi3: + abis.append("abi3") + + abis.append('none') + + arches = _get_custom_platforms(platform or get_platform(), platform) + + # Current version, current API (built specifically for our Python): + for abi in abis: + for arch in arches: + supported.append(('%s%s' % (impl, current_version), abi, arch)) + + # abi3 modules compatible with older version of Python + if supports_abi3: + for version in other_versions: + # abi3 was introduced in Python 3.2 + if version in {'31', '30'}: + break + for arch in arches: + supported.append(("%s%s" % (impl, version), "abi3", arch)) + + # Has binaries, does not use the Python API: + for arch in arches: + supported.append(('py%s' % (current_version[0]), 'none', arch)) + + # No abi / arch, but requires our implementation: + supported.append(('%s%s' % (impl, current_version), 'none', 'any')) + + # No abi / arch, generic Python + supported.append(('py%s' % (current_version,), 'none', 'any')) + supported.append(('py%s' % (current_version[0]), 'none', 'any')) + for version in other_versions: + supported.append(('py%s' % (version,), 'none', 'any')) + + return supported + + +def _generic_tags( + version=None, # type: Optional[str] + platform=None, # type: Optional[str] + impl=None, # type: Optional[str] + abi=None, # type: Optional[str] +): + # type: (...) -> List[Tuple[str, str, str]] + supported = [] # type: List[Tuple[str, str, str]] + + # Versions must be given with respect to the preference + if version is None: + version_info = get_impl_version_info() + versions = get_all_minor_versions_as_strings(version_info) + else: + versions = [version] + current_version = versions[0] + other_versions = versions[1:] + + impl = impl or interpreter_name() + + abis = [] # type: List[str] + + abi = abi or get_abi_tag() + if abi: + abis[0:0] = [abi] + + supports_abi3 = not PY2 and impl == "cp" + + if supports_abi3: + abis.append("abi3") + + abis.append('none') + + arches = _get_custom_platforms(platform or get_platform(), platform) + + # Current version, current API (built specifically for our Python): + for abi in abis: + for arch in arches: + supported.append(('%s%s' % (impl, current_version), abi, arch)) + + # abi3 modules compatible with older version of Python + if supports_abi3: + for version in other_versions: + # abi3 was introduced in Python 3.2 + if version in {'31', '30'}: + break + for arch in arches: + supported.append(("%s%s" % (impl, version), "abi3", arch)) + + # Has binaries, does not use the Python API: + for arch in arches: + supported.append(('py%s' % (current_version[0]), 'none', arch)) + + # No abi / arch, but requires our implementation: + supported.append(('%s%s' % (impl, current_version), 'none', 'any')) + + # No abi / arch, generic Python + supported.append(('py%s' % (current_version,), 'none', 'any')) + supported.append(('py%s' % (current_version[0]), 'none', 'any')) + for version in other_versions: + supported.append(('py%s' % (version,), 'none', 'any')) + + return supported + + +def _compatible_tags( + version=None, # type: Optional[str] + platform=None, # type: Optional[str] + impl=None, # type: Optional[str] + abi=None, # type: Optional[str] +): + # type: (...) -> List[Tuple[str, str, str]] + supported = [] # type: List[Tuple[str, str, str]] + + # Versions must be given with respect to the preference + if version is None: + version_info = get_impl_version_info() + versions = get_all_minor_versions_as_strings(version_info) + else: + versions = [version] + current_version = versions[0] + other_versions = versions[1:] + + impl = impl or interpreter_name() + + abis = [] # type: List[str] + + abi = abi or get_abi_tag() + if abi: + abis[0:0] = [abi] + + supports_abi3 = not PY2 and impl == "cp" + + if supports_abi3: + abis.append("abi3") + + abis.append('none') + + arches = _get_custom_platforms(platform or get_platform(), platform) + + # Current version, current API (built specifically for our Python): + for abi in abis: + for arch in arches: + supported.append(('%s%s' % (impl, current_version), abi, arch)) + + # abi3 modules compatible with older version of Python + if supports_abi3: + for version in other_versions: + # abi3 was introduced in Python 3.2 + if version in {'31', '30'}: + break + for arch in arches: + supported.append(("%s%s" % (impl, version), "abi3", arch)) + + # Has binaries, does not use the Python API: + for arch in arches: + supported.append(('py%s' % (current_version[0]), 'none', arch)) + + # No abi / arch, but requires our implementation: + supported.append(('%s%s' % (impl, current_version), 'none', 'any')) + + # No abi / arch, generic Python + supported.append(('py%s' % (current_version,), 'none', 'any')) + supported.append(('py%s' % (current_version[0]), 'none', 'any')) + for version in other_versions: + supported.append(('py%s' % (version,), 'none', 'any')) + + return supported + + def get_supported( version=None, # type: Optional[str] platform=None, # type: Optional[str] From 54db17c976084edf98113aa5401ecf1c9418d018 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 23 Nov 2019 18:02:29 -0500 Subject: [PATCH 1083/3170] Use _cpython_tags, _generic_tags, and _compatible_tags Since these functions are copies of the existing code, there is no behavior change except each tag will now be present 3 times. To accommodate this we remove all but the first duplicate tag from the set of all tags. We put `_compatible_tags` last because it will provide the lowest priority tags. The order of `_cpython_tags` and `_generic_tags` here is not significant - when we start customizing them we will introduce a condition so that they are mutually exclusive. --- src/pip/_internal/pep425tags.py | 69 +++++++-------------------------- 1 file changed, 15 insertions(+), 54 deletions(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index 915f5bcf19e..2a021418de4 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -21,7 +21,7 @@ if MYPY_CHECK_RUNNING: from typing import ( - Callable, List, Optional, Tuple, Union + Callable, Iterator, List, Optional, Set, Tuple, Union ) logger = logging.getLogger(__name__) @@ -489,6 +489,15 @@ def _compatible_tags( return supported +def _stable_unique_tags(tags): + # type: (List[Tag]) -> Iterator[Tag] + observed = set() # type: Set[Tag] + for tag in tags: + if tag not in observed: + observed.add(tag) + yield tag + + def get_supported( version=None, # type: Optional[str] platform=None, # type: Optional[str] @@ -510,60 +519,12 @@ def get_supported( """ supported = [] # type: List[Union[Tag, Tuple[str, str, str]]] - # Versions must be given with respect to the preference - if version is None: - version_info = get_impl_version_info() - versions = get_all_minor_versions_as_strings(version_info) - else: - versions = [version] - current_version = versions[0] - other_versions = versions[1:] - - impl = impl or interpreter_name() - - abis = [] # type: List[str] - - abi = abi or get_abi_tag() - if abi: - abis[0:0] = [abi] - - supports_abi3 = not PY2 and impl == "cp" - - if supports_abi3: - abis.append("abi3") - - abis.append('none') - - arches = _get_custom_platforms(platform or get_platform(), platform) - - # Current version, current API (built specifically for our Python): - for abi in abis: - for arch in arches: - supported.append(('%s%s' % (impl, current_version), abi, arch)) - - # abi3 modules compatible with older version of Python - if supports_abi3: - for version in other_versions: - # abi3 was introduced in Python 3.2 - if version in {'31', '30'}: - break - for arch in arches: - supported.append(("%s%s" % (impl, version), "abi3", arch)) - - # Has binaries, does not use the Python API: - for arch in arches: - supported.append(('py%s' % (current_version[0]), 'none', arch)) - - # No abi / arch, but requires our implementation: - supported.append(('%s%s' % (impl, current_version), 'none', 'any')) - - # No abi / arch, generic Python - supported.append(('py%s' % (current_version,), 'none', 'any')) - supported.append(('py%s' % (current_version[0]), 'none', 'any')) - for version in other_versions: - supported.append(('py%s' % (version,), 'none', 'any')) + supported.extend(_cpython_tags(version, platform, impl, abi)) + supported.extend(_generic_tags(version, platform, impl, abi)) + supported.extend(_compatible_tags(version, platform, impl, abi)) - return [ + tags = [ parts if isinstance(parts, Tag) else Tag(*parts) for parts in supported ] + return list(_stable_unique_tags(tags)) From 1c8c481214f89df6ea50659646f537388c80ea2d Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 23 Nov 2019 18:02:30 -0500 Subject: [PATCH 1084/3170] Only calculate py-compatible tags in one place Since `_compatible_tags` is the function that will be responsible for generating the non-interpreter-specific tags, we remove the corresponding sections from `_cpython_tags` and `_generic_tags`. The resulting tags in `get_supported` are equivalent because these were the last tags to be computed in those functions, and `_compatible_tags` is executed after them (so any non-duplicate tags it produces will be last). To reinforce the reponsibility of `_compatible_tags` we also remove the abi-related tag generation, which is already handled in `_cpython_tags` and `_generic_tags`. --- src/pip/_internal/pep425tags.py | 53 --------------------------------- 1 file changed, 53 deletions(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index 2a021418de4..99a2fc29623 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -343,19 +343,6 @@ def _cpython_tags( for arch in arches: supported.append(("%s%s" % (impl, version), "abi3", arch)) - # Has binaries, does not use the Python API: - for arch in arches: - supported.append(('py%s' % (current_version[0]), 'none', arch)) - - # No abi / arch, but requires our implementation: - supported.append(('%s%s' % (impl, current_version), 'none', 'any')) - - # No abi / arch, generic Python - supported.append(('py%s' % (current_version,), 'none', 'any')) - supported.append(('py%s' % (current_version[0]), 'none', 'any')) - for version in other_versions: - supported.append(('py%s' % (version,), 'none', 'any')) - return supported @@ -408,19 +395,6 @@ def _generic_tags( for arch in arches: supported.append(("%s%s" % (impl, version), "abi3", arch)) - # Has binaries, does not use the Python API: - for arch in arches: - supported.append(('py%s' % (current_version[0]), 'none', arch)) - - # No abi / arch, but requires our implementation: - supported.append(('%s%s' % (impl, current_version), 'none', 'any')) - - # No abi / arch, generic Python - supported.append(('py%s' % (current_version,), 'none', 'any')) - supported.append(('py%s' % (current_version[0]), 'none', 'any')) - for version in other_versions: - supported.append(('py%s' % (version,), 'none', 'any')) - return supported @@ -444,35 +418,8 @@ def _compatible_tags( impl = impl or interpreter_name() - abis = [] # type: List[str] - - abi = abi or get_abi_tag() - if abi: - abis[0:0] = [abi] - - supports_abi3 = not PY2 and impl == "cp" - - if supports_abi3: - abis.append("abi3") - - abis.append('none') - arches = _get_custom_platforms(platform or get_platform(), platform) - # Current version, current API (built specifically for our Python): - for abi in abis: - for arch in arches: - supported.append(('%s%s' % (impl, current_version), abi, arch)) - - # abi3 modules compatible with older version of Python - if supports_abi3: - for version in other_versions: - # abi3 was introduced in Python 3.2 - if version in {'31', '30'}: - break - for arch in arches: - supported.append(("%s%s" % (impl, version), "abi3", arch)) - # Has binaries, does not use the Python API: for arch in arches: supported.append(('py%s' % (current_version[0]), 'none', arch)) From 480911bc8e943f38bda692da7757b0fdea221670 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 23 Nov 2019 18:02:31 -0500 Subject: [PATCH 1085/3170] Remove unused abi arg from _compatible_tags --- src/pip/_internal/pep425tags.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index 99a2fc29623..d2c72954427 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -402,7 +402,6 @@ def _compatible_tags( version=None, # type: Optional[str] platform=None, # type: Optional[str] impl=None, # type: Optional[str] - abi=None, # type: Optional[str] ): # type: (...) -> List[Tuple[str, str, str]] supported = [] # type: List[Tuple[str, str, str]] @@ -468,7 +467,7 @@ def get_supported( supported.extend(_cpython_tags(version, platform, impl, abi)) supported.extend(_generic_tags(version, platform, impl, abi)) - supported.extend(_compatible_tags(version, platform, impl, abi)) + supported.extend(_compatible_tags(version, platform, impl)) tags = [ parts if isinstance(parts, Tag) else Tag(*parts) From 4659a78935533b90ea0e2211890a968b05c52994 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 23 Nov 2019 18:02:32 -0500 Subject: [PATCH 1086/3170] Use packaging.tags.compatible_tags We assume this function improves on our existing behavior, so use it as-is. We will customize the arguments over the next few commits. Since packaging.tags internally calculates platforms when not provided, we skip the tests which patch functions assuming that manylinux compatibility determination depends on them. --- src/pip/_internal/pep425tags.py | 6 +++++- tests/unit/test_pep425tags.py | 3 +++ 2 files changed, 8 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index d2c72954427..21c58593f15 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -10,6 +10,7 @@ from pip._vendor.packaging.tags import ( Tag, + compatible_tags, interpreter_name, interpreter_version, mac_platforms, @@ -403,7 +404,10 @@ def _compatible_tags( platform=None, # type: Optional[str] impl=None, # type: Optional[str] ): - # type: (...) -> List[Tuple[str, str, str]] + # type: (...) -> Union[Iterator[Tag], List[Tuple[str, str, str]]] + if version is None and platform is None and impl is None: + return compatible_tags() + supported = [] # type: List[Tuple[str, str, str]] # Versions must be given with respect to the preference diff --git a/tests/unit/test_pep425tags.py b/tests/unit/test_pep425tags.py index eaa88888fb0..58e97a3e85e 100644 --- a/tests/unit/test_pep425tags.py +++ b/tests/unit/test_pep425tags.py @@ -170,6 +170,7 @@ def test_manylinux_3(self, is_manylinux_compatible): class TestManylinux1Tags(object): + @pytest.mark.xfail @patch('pip._internal.pep425tags.is_manylinux2010_compatible', lambda: False) @patch('pip._internal.pep425tags.is_manylinux2014_compatible', @@ -200,6 +201,7 @@ def test_manylinux1_tag_is_first(self): class TestManylinux2010Tags(object): + @pytest.mark.xfail @patch('pip._internal.pep425tags.is_manylinux2014_compatible', lambda: False) @patch('pip._internal.pep425tags.get_platform', lambda: 'linux_x86_64') @@ -253,6 +255,7 @@ def test_manylinux2010_implies_manylinux1(self, manylinux2010, manylinux1): class TestManylinux2014Tags(object): + @pytest.mark.xfail @patch('pip._internal.pep425tags.get_platform', lambda: 'linux_x86_64') @patch('pip._internal.utils.glibc.have_compatible_glibc', lambda major, minor: True) From 750abcab041d136231036bdc5153d4976d4a83bf Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 23 Nov 2019 18:02:33 -0500 Subject: [PATCH 1087/3170] Customize python_version for packaging.tags.compatible_tags --- src/pip/_internal/pep425tags.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index 21c58593f15..59fa9d8d1d5 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -25,6 +25,8 @@ Callable, Iterator, List, Optional, Set, Tuple, Union ) + from pip._vendor.packaging.tags import PythonVersion + logger = logging.getLogger(__name__) _osx_arch_pat = re.compile(r'(.+)_(\d+)_(\d+)_(.+)') @@ -295,6 +297,14 @@ def _get_custom_platforms(arch, platform): return arches +def _get_python_version(version): + # type: (str) -> PythonVersion + if len(version) > 1: + return int(version[0]), int(version[1:]) + else: + return (int(version[0]),) + + def _cpython_tags( version=None, # type: Optional[str] platform=None, # type: Optional[str] @@ -405,8 +415,12 @@ def _compatible_tags( impl=None, # type: Optional[str] ): # type: (...) -> Union[Iterator[Tag], List[Tuple[str, str, str]]] - if version is None and platform is None and impl is None: - return compatible_tags() + python_version = None # type: Optional[PythonVersion] + if version is not None: + python_version = _get_python_version(version) + + if platform is None and impl is None: + return compatible_tags(python_version=python_version) supported = [] # type: List[Tuple[str, str, str]] From 72d00ddcc1eb927de9355215f5fbc935654dc12a Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 23 Nov 2019 18:02:34 -0500 Subject: [PATCH 1088/3170] Customize interpreter for packaging.tags.compatible_tags --- src/pip/_internal/pep425tags.py | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index 59fa9d8d1d5..044f7a01a15 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -305,6 +305,15 @@ def _get_python_version(version): return (int(version[0]),) +def _get_custom_interpreter(implementation=None, version=None): + # type: (Optional[str], Optional[str]) -> str + if implementation is None: + implementation = interpreter_name() + if version is None: + version = interpreter_version() + return "{}{}".format(implementation, version) + + def _cpython_tags( version=None, # type: Optional[str] platform=None, # type: Optional[str] @@ -419,8 +428,13 @@ def _compatible_tags( if version is not None: python_version = _get_python_version(version) - if platform is None and impl is None: - return compatible_tags(python_version=python_version) + interpreter = _get_custom_interpreter(impl, version) + + if platform is None: + return compatible_tags( + python_version=python_version, + interpreter=interpreter, + ) supported = [] # type: List[Tuple[str, str, str]] From 2de0b7c560e3fc910ebdddebb708d2f9430dce69 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 23 Nov 2019 18:02:35 -0500 Subject: [PATCH 1089/3170] Customize platforms for packaging.tags.compatible_tags --- src/pip/_internal/pep425tags.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index 044f7a01a15..0ef93d008b7 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -430,10 +430,15 @@ def _compatible_tags( interpreter = _get_custom_interpreter(impl, version) - if platform is None: + platforms = None # type: Optional[List[str]] + if platform is not None: + platforms = _get_custom_platforms(platform, platform) + + if True: return compatible_tags( python_version=python_version, interpreter=interpreter, + platforms=platforms, ) supported = [] # type: List[Tuple[str, str, str]] From c514c6b35f3f277e0f12add0f238b5e11c3e9715 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 23 Nov 2019 18:02:36 -0500 Subject: [PATCH 1090/3170] Make packaging.tags.compatible_tags unconditional --- src/pip/_internal/pep425tags.py | 43 +++++---------------------------- 1 file changed, 6 insertions(+), 37 deletions(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index 0ef93d008b7..482e37b6211 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -423,7 +423,7 @@ def _compatible_tags( platform=None, # type: Optional[str] impl=None, # type: Optional[str] ): - # type: (...) -> Union[Iterator[Tag], List[Tuple[str, str, str]]] + # type: (...) -> Iterator[Tag] python_version = None # type: Optional[PythonVersion] if version is not None: python_version = _get_python_version(version) @@ -434,42 +434,11 @@ def _compatible_tags( if platform is not None: platforms = _get_custom_platforms(platform, platform) - if True: - return compatible_tags( - python_version=python_version, - interpreter=interpreter, - platforms=platforms, - ) - - supported = [] # type: List[Tuple[str, str, str]] - - # Versions must be given with respect to the preference - if version is None: - version_info = get_impl_version_info() - versions = get_all_minor_versions_as_strings(version_info) - else: - versions = [version] - current_version = versions[0] - other_versions = versions[1:] - - impl = impl or interpreter_name() - - arches = _get_custom_platforms(platform or get_platform(), platform) - - # Has binaries, does not use the Python API: - for arch in arches: - supported.append(('py%s' % (current_version[0]), 'none', arch)) - - # No abi / arch, but requires our implementation: - supported.append(('%s%s' % (impl, current_version), 'none', 'any')) - - # No abi / arch, generic Python - supported.append(('py%s' % (current_version,), 'none', 'any')) - supported.append(('py%s' % (current_version[0]), 'none', 'any')) - for version in other_versions: - supported.append(('py%s' % (version,), 'none', 'any')) - - return supported + return compatible_tags( + python_version=python_version, + interpreter=interpreter, + platforms=platforms, + ) def _stable_unique_tags(tags): From b91286c38d0c871b7d15f000e80b2c74235e19ad Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 23 Nov 2019 18:02:37 -0500 Subject: [PATCH 1091/3170] Inline packaging.tags.compatible_tags --- src/pip/_internal/pep425tags.py | 41 ++++++++++++++------------------- 1 file changed, 17 insertions(+), 24 deletions(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index 482e37b6211..26721b92feb 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -418,29 +418,6 @@ def _generic_tags( return supported -def _compatible_tags( - version=None, # type: Optional[str] - platform=None, # type: Optional[str] - impl=None, # type: Optional[str] -): - # type: (...) -> Iterator[Tag] - python_version = None # type: Optional[PythonVersion] - if version is not None: - python_version = _get_python_version(version) - - interpreter = _get_custom_interpreter(impl, version) - - platforms = None # type: Optional[List[str]] - if platform is not None: - platforms = _get_custom_platforms(platform, platform) - - return compatible_tags( - python_version=python_version, - interpreter=interpreter, - platforms=platforms, - ) - - def _stable_unique_tags(tags): # type: (List[Tag]) -> Iterator[Tag] observed = set() # type: Set[Tag] @@ -471,9 +448,25 @@ def get_supported( """ supported = [] # type: List[Union[Tag, Tuple[str, str, str]]] + python_version = None # type: Optional[PythonVersion] + if version is not None: + python_version = _get_python_version(version) + + interpreter = _get_custom_interpreter(impl, version) + + platforms = None # type: Optional[List[str]] + if platform is not None: + platforms = _get_custom_platforms(platform, platform) + supported.extend(_cpython_tags(version, platform, impl, abi)) supported.extend(_generic_tags(version, platform, impl, abi)) - supported.extend(_compatible_tags(version, platform, impl)) + supported.extend( + compatible_tags( + python_version=python_version, + interpreter=interpreter, + platforms=platforms, + ) + ) tags = [ parts if isinstance(parts, Tag) else Tag(*parts) From 8f1c60ead06238773f8f6827767ef835c641b636 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 23 Nov 2019 18:02:38 -0500 Subject: [PATCH 1092/3170] Only use _cpython_tags for CPython Since the behavior for both of these functions is the same, there is no behavior change here. This will let us simplify `_cpython_tags` a bit. --- src/pip/_internal/pep425tags.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index 26721b92feb..b5017605100 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -458,8 +458,11 @@ def get_supported( if platform is not None: platforms = _get_custom_platforms(platform, platform) - supported.extend(_cpython_tags(version, platform, impl, abi)) - supported.extend(_generic_tags(version, platform, impl, abi)) + is_cpython = (impl or interpreter_name()) == "cp" + if is_cpython: + supported.extend(_cpython_tags(version, platform, impl, abi)) + else: + supported.extend(_generic_tags(version, platform, impl, abi)) supported.extend( compatible_tags( python_version=python_version, From e388df6c594267e8e3ad9c033325965d5bcf3603 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 23 Nov 2019 18:02:39 -0500 Subject: [PATCH 1093/3170] Remove impl from _cpython_tags We only call this function when the user or platform-provided implementation is "cp", so we can inline the literal in place of the parameter. This will make refactoring easier. --- src/pip/_internal/pep425tags.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index b5017605100..be61646a004 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -317,7 +317,6 @@ def _get_custom_interpreter(implementation=None, version=None): def _cpython_tags( version=None, # type: Optional[str] platform=None, # type: Optional[str] - impl=None, # type: Optional[str] abi=None, # type: Optional[str] ): # type: (...) -> List[Tuple[str, str, str]] @@ -332,15 +331,13 @@ def _cpython_tags( current_version = versions[0] other_versions = versions[1:] - impl = impl or interpreter_name() - abis = [] # type: List[str] abi = abi or get_abi_tag() if abi: abis[0:0] = [abi] - supports_abi3 = not PY2 and impl == "cp" + supports_abi3 = not PY2 if supports_abi3: abis.append("abi3") @@ -352,7 +349,7 @@ def _cpython_tags( # Current version, current API (built specifically for our Python): for abi in abis: for arch in arches: - supported.append(('%s%s' % (impl, current_version), abi, arch)) + supported.append(('cp%s' % current_version, abi, arch)) # abi3 modules compatible with older version of Python if supports_abi3: @@ -361,7 +358,7 @@ def _cpython_tags( if version in {'31', '30'}: break for arch in arches: - supported.append(("%s%s" % (impl, version), "abi3", arch)) + supported.append(("cp%s" % version, "abi3", arch)) return supported @@ -460,7 +457,7 @@ def get_supported( is_cpython = (impl or interpreter_name()) == "cp" if is_cpython: - supported.extend(_cpython_tags(version, platform, impl, abi)) + supported.extend(_cpython_tags(version, platform, abi)) else: supported.extend(_generic_tags(version, platform, impl, abi)) supported.extend( From 5dbef5debecc1677472dcbfa26f76f2db1896af2 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 23 Nov 2019 18:02:41 -0500 Subject: [PATCH 1094/3170] Use packaging.tags.cpython_tags We assume this function improves the status quo over the current `_cpython_tags`, so we use it when no arguments need to be customized. Since `packaging.tags` has its own tests and derives defaults differently than pep425tags, we also remove the applicable tests that were testing against the result of a plain call to `pep425tags.get_supported()`. --- src/pip/_internal/pep425tags.py | 6 ++- tests/unit/test_pep425tags.py | 93 --------------------------------- 2 files changed, 5 insertions(+), 94 deletions(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index be61646a004..7804766f4c7 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -11,6 +11,7 @@ from pip._vendor.packaging.tags import ( Tag, compatible_tags, + cpython_tags, interpreter_name, interpreter_version, mac_platforms, @@ -319,7 +320,10 @@ def _cpython_tags( platform=None, # type: Optional[str] abi=None, # type: Optional[str] ): - # type: (...) -> List[Tuple[str, str, str]] + # type: (...) -> Union[Iterator[Tag], List[Tuple[str, str, str]]] + if version is None and platform is None and abi is None: + return cpython_tags() + supported = [] # type: List[Tuple[str, str, str]] # Versions must be given with respect to the preference diff --git a/tests/unit/test_pep425tags.py b/tests/unit/test_pep425tags.py index 58e97a3e85e..a5469725e96 100644 --- a/tests/unit/test_pep425tags.py +++ b/tests/unit/test_pep425tags.py @@ -168,70 +168,8 @@ def test_manylinux_3(self, is_manylinux_compatible): assert not is_manylinux_compatible() -class TestManylinux1Tags(object): - - @pytest.mark.xfail - @patch('pip._internal.pep425tags.is_manylinux2010_compatible', - lambda: False) - @patch('pip._internal.pep425tags.is_manylinux2014_compatible', - lambda: False) - @patch('pip._internal.pep425tags.get_platform', lambda: 'linux_x86_64') - @patch('pip._internal.utils.glibc.have_compatible_glibc', - lambda major, minor: True) - @patch('sys.platform', 'linux2') - def test_manylinux1_tag_is_first(self): - """ - Test that the more specific tag manylinux1 comes first. - """ - groups = {} - for tag in pep425tags.get_supported(): - groups.setdefault( - (tag.interpreter, tag.abi), [] - ).append(tag.platform) - - for arches in groups.values(): - if arches == ['any']: - continue - # Expect the most specific arch first: - if len(arches) == 3: - assert arches == ['manylinux1_x86_64', 'linux_x86_64', 'any'] - else: - assert arches == ['manylinux1_x86_64', 'linux_x86_64'] - - class TestManylinux2010Tags(object): - @pytest.mark.xfail - @patch('pip._internal.pep425tags.is_manylinux2014_compatible', - lambda: False) - @patch('pip._internal.pep425tags.get_platform', lambda: 'linux_x86_64') - @patch('pip._internal.utils.glibc.have_compatible_glibc', - lambda major, minor: True) - @patch('sys.platform', 'linux2') - def test_manylinux2010_tag_is_first(self): - """ - Test that the more specific tag manylinux2010 comes first. - """ - groups = {} - for tag in pep425tags.get_supported(): - groups.setdefault( - (tag.interpreter, tag.abi), [] - ).append(tag.platform) - - for arches in groups.values(): - if arches == ['any']: - continue - # Expect the most specific arch first: - if len(arches) == 4: - assert arches == ['manylinux2010_x86_64', - 'manylinux1_x86_64', - 'linux_x86_64', - 'any'] - else: - assert arches == ['manylinux2010_x86_64', - 'manylinux1_x86_64', - 'linux_x86_64'] - @pytest.mark.parametrize("manylinux2010,manylinux1", [ ("manylinux2010_x86_64", "manylinux1_x86_64"), ("manylinux2010_i686", "manylinux1_i686"), @@ -255,37 +193,6 @@ def test_manylinux2010_implies_manylinux1(self, manylinux2010, manylinux1): class TestManylinux2014Tags(object): - @pytest.mark.xfail - @patch('pip._internal.pep425tags.get_platform', lambda: 'linux_x86_64') - @patch('pip._internal.utils.glibc.have_compatible_glibc', - lambda major, minor: True) - @patch('sys.platform', 'linux2') - def test_manylinux2014_tag_is_first(self): - """ - Test that the more specific tag manylinux2014 comes first. - """ - groups = {} - for tag in pep425tags.get_supported(): - groups.setdefault( - (tag.interpreter, tag.abi), [] - ).append(tag.platform) - - for arches in groups.values(): - if arches == ['any']: - continue - # Expect the most specific arch first: - if len(arches) == 5: - assert arches == ['manylinux2014_x86_64', - 'manylinux2010_x86_64', - 'manylinux1_x86_64', - 'linux_x86_64', - 'any'] - else: - assert arches == ['manylinux2014_x86_64', - 'manylinux2010_x86_64', - 'manylinux1_x86_64', - 'linux_x86_64'] - @pytest.mark.parametrize("manylinuxA,manylinuxB", [ ("manylinux2014_x86_64", ["manylinux2010_x86_64", "manylinux1_x86_64"]), From 147680a6134a6eabbae64715d93adda555d0738a Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 23 Nov 2019 18:02:42 -0500 Subject: [PATCH 1095/3170] Customize python_version for packaging.tags.cpython_tags --- src/pip/_internal/pep425tags.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index 7804766f4c7..0f5f7ceef94 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -321,8 +321,12 @@ def _cpython_tags( abi=None, # type: Optional[str] ): # type: (...) -> Union[Iterator[Tag], List[Tuple[str, str, str]]] - if version is None and platform is None and abi is None: - return cpython_tags() + python_version = None # type: Optional[PythonVersion] + if version is not None: + python_version = _get_python_version(version) + + if platform is None and abi is None: + return cpython_tags(python_version=python_version) supported = [] # type: List[Tuple[str, str, str]] From 05045e790324ebc63c9bc714ab3d2f76589a2fa0 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 23 Nov 2019 18:02:43 -0500 Subject: [PATCH 1096/3170] Customize abis for packaging.tags.cpython_tags --- src/pip/_internal/pep425tags.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index 0f5f7ceef94..2c90790567c 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -325,8 +325,12 @@ def _cpython_tags( if version is not None: python_version = _get_python_version(version) - if platform is None and abi is None: - return cpython_tags(python_version=python_version) + abis = None # type: Optional[List[str]] + if abi is not None: + abis = [abi] + + if platform is None: + return cpython_tags(python_version=python_version, abis=abis) supported = [] # type: List[Tuple[str, str, str]] @@ -339,7 +343,7 @@ def _cpython_tags( current_version = versions[0] other_versions = versions[1:] - abis = [] # type: List[str] + abis = [] # type: ignore # we will be removing this soon abi = abi or get_abi_tag() if abi: From fecfadb8100d679862600e506dfd2436a5bbb125 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 23 Nov 2019 18:02:44 -0500 Subject: [PATCH 1097/3170] Customize platforms for packaging.tags.cpython_tags --- src/pip/_internal/pep425tags.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index 2c90790567c..a841acf45cc 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -329,8 +329,16 @@ def _cpython_tags( if abi is not None: abis = [abi] - if platform is None: - return cpython_tags(python_version=python_version, abis=abis) + platforms = None # type: Optional[List[str]] + if platform is not None: + platforms = _get_custom_platforms(platform, platform) + + if True: + return cpython_tags( + python_version=python_version, + abis=abis, + platforms=platforms, + ) supported = [] # type: List[Tuple[str, str, str]] From 56840c30c5d897691b14ddc9d8979f82bc981539 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 23 Nov 2019 18:02:45 -0500 Subject: [PATCH 1098/3170] Make packaging.tags.cpython_tags unconditional --- src/pip/_internal/pep425tags.py | 55 ++++----------------------------- 1 file changed, 6 insertions(+), 49 deletions(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index a841acf45cc..309c5a3d987 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -320,7 +320,7 @@ def _cpython_tags( platform=None, # type: Optional[str] abi=None, # type: Optional[str] ): - # type: (...) -> Union[Iterator[Tag], List[Tuple[str, str, str]]] + # type: (...) -> Iterator[Tag] python_version = None # type: Optional[PythonVersion] if version is not None: python_version = _get_python_version(version) @@ -333,54 +333,11 @@ def _cpython_tags( if platform is not None: platforms = _get_custom_platforms(platform, platform) - if True: - return cpython_tags( - python_version=python_version, - abis=abis, - platforms=platforms, - ) - - supported = [] # type: List[Tuple[str, str, str]] - - # Versions must be given with respect to the preference - if version is None: - version_info = get_impl_version_info() - versions = get_all_minor_versions_as_strings(version_info) - else: - versions = [version] - current_version = versions[0] - other_versions = versions[1:] - - abis = [] # type: ignore # we will be removing this soon - - abi = abi or get_abi_tag() - if abi: - abis[0:0] = [abi] - - supports_abi3 = not PY2 - - if supports_abi3: - abis.append("abi3") - - abis.append('none') - - arches = _get_custom_platforms(platform or get_platform(), platform) - - # Current version, current API (built specifically for our Python): - for abi in abis: - for arch in arches: - supported.append(('cp%s' % current_version, abi, arch)) - - # abi3 modules compatible with older version of Python - if supports_abi3: - for version in other_versions: - # abi3 was introduced in Python 3.2 - if version in {'31', '30'}: - break - for arch in arches: - supported.append(("cp%s" % version, "abi3", arch)) - - return supported + return cpython_tags( + python_version=python_version, + abis=abis, + platforms=platforms, + ) def _generic_tags( From 1574872162f3310f98367cb19ef6e28f0ef60bf7 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 23 Nov 2019 18:02:46 -0500 Subject: [PATCH 1099/3170] Inline packaging.tags.cpython_tags --- src/pip/_internal/pep425tags.py | 37 ++++++++++----------------------- 1 file changed, 11 insertions(+), 26 deletions(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index 309c5a3d987..2f205d7390e 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -315,31 +315,6 @@ def _get_custom_interpreter(implementation=None, version=None): return "{}{}".format(implementation, version) -def _cpython_tags( - version=None, # type: Optional[str] - platform=None, # type: Optional[str] - abi=None, # type: Optional[str] -): - # type: (...) -> Iterator[Tag] - python_version = None # type: Optional[PythonVersion] - if version is not None: - python_version = _get_python_version(version) - - abis = None # type: Optional[List[str]] - if abi is not None: - abis = [abi] - - platforms = None # type: Optional[List[str]] - if platform is not None: - platforms = _get_custom_platforms(platform, platform) - - return cpython_tags( - python_version=python_version, - abis=abis, - platforms=platforms, - ) - - def _generic_tags( version=None, # type: Optional[str] platform=None, # type: Optional[str] @@ -428,13 +403,23 @@ def get_supported( interpreter = _get_custom_interpreter(impl, version) + abis = None # type: Optional[List[str]] + if abi is not None: + abis = [abi] + platforms = None # type: Optional[List[str]] if platform is not None: platforms = _get_custom_platforms(platform, platform) is_cpython = (impl or interpreter_name()) == "cp" if is_cpython: - supported.extend(_cpython_tags(version, platform, abi)) + supported.extend( + cpython_tags( + python_version=python_version, + abis=abis, + platforms=platforms, + ) + ) else: supported.extend(_generic_tags(version, platform, impl, abi)) supported.extend( From fa1ec40ce01a67c97ccee44a44b451542a0fd674 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 23 Nov 2019 18:02:47 -0500 Subject: [PATCH 1100/3170] Remove unused abi3 branch in _generic_tags Since this function is only called for non-CPython interpreters, the abi3-related branches are not relevant. --- src/pip/_internal/pep425tags.py | 16 ---------------- 1 file changed, 16 deletions(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index 2f205d7390e..25e6ddd1aeb 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -16,7 +16,6 @@ interpreter_version, mac_platforms, ) -from pip._vendor.six import PY2 import pip._internal.utils.glibc from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -331,7 +330,6 @@ def _generic_tags( else: versions = [version] current_version = versions[0] - other_versions = versions[1:] impl = impl or interpreter_name() @@ -341,11 +339,6 @@ def _generic_tags( if abi: abis[0:0] = [abi] - supports_abi3 = not PY2 and impl == "cp" - - if supports_abi3: - abis.append("abi3") - abis.append('none') arches = _get_custom_platforms(platform or get_platform(), platform) @@ -355,15 +348,6 @@ def _generic_tags( for arch in arches: supported.append(('%s%s' % (impl, current_version), abi, arch)) - # abi3 modules compatible with older version of Python - if supports_abi3: - for version in other_versions: - # abi3 was introduced in Python 3.2 - if version in {'31', '30'}: - break - for arch in arches: - supported.append(("%s%s" % (impl, version), "abi3", arch)) - return supported From 281273dac3d66273e6ffc43522122680cc13e40a Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 23 Nov 2019 18:02:48 -0500 Subject: [PATCH 1101/3170] Use packaging.tags.generic_tags As with cpython_tags and compatible_tags, we assume this function covers our use cases in the no-argument case and will customize our arguments for it in a few steps. --- src/pip/_internal/pep425tags.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index 25e6ddd1aeb..08179deca2a 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -12,6 +12,7 @@ Tag, compatible_tags, cpython_tags, + generic_tags, interpreter_name, interpreter_version, mac_platforms, @@ -320,7 +321,10 @@ def _generic_tags( impl=None, # type: Optional[str] abi=None, # type: Optional[str] ): - # type: (...) -> List[Tuple[str, str, str]] + # type: (...) -> Union[Iterator[Tag], List[Tuple[str, str, str]]] + if version is None and platform is None and impl is None and abi is None: + return generic_tags() + supported = [] # type: List[Tuple[str, str, str]] # Versions must be given with respect to the preference From 77dbd27703e1dce568af1eff7ffd7cc77189639e Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 23 Nov 2019 18:02:49 -0500 Subject: [PATCH 1102/3170] Customize interpreter for packaging.tags.generic_tags --- src/pip/_internal/pep425tags.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index 08179deca2a..db8af9c9dc2 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -322,8 +322,10 @@ def _generic_tags( abi=None, # type: Optional[str] ): # type: (...) -> Union[Iterator[Tag], List[Tuple[str, str, str]]] - if version is None and platform is None and impl is None and abi is None: - return generic_tags() + interpreter = _get_custom_interpreter(impl, version) + + if platform is None and abi is None: + return generic_tags(interpreter=interpreter) supported = [] # type: List[Tuple[str, str, str]] From 0bebeb66e6b33c5d191b40de121f36c83dee6dbd Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 23 Nov 2019 18:02:50 -0500 Subject: [PATCH 1103/3170] Customize abis for packaging.tags.generic_tags --- src/pip/_internal/pep425tags.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index db8af9c9dc2..f14ed8c35cc 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -324,8 +324,12 @@ def _generic_tags( # type: (...) -> Union[Iterator[Tag], List[Tuple[str, str, str]]] interpreter = _get_custom_interpreter(impl, version) - if platform is None and abi is None: - return generic_tags(interpreter=interpreter) + abis = None # type: Optional[List[str]] + if abi: + abis = [abi] + + if platform is None: + return generic_tags(interpreter=interpreter, abis=abis) supported = [] # type: List[Tuple[str, str, str]] @@ -339,7 +343,7 @@ def _generic_tags( impl = impl or interpreter_name() - abis = [] # type: List[str] + abis = [] # type: ignore # we will be removing this soon abi = abi or get_abi_tag() if abi: From 293b778374bf7ce9db650adb7e73a9ec96884ba4 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 23 Nov 2019 18:02:51 -0500 Subject: [PATCH 1104/3170] Customize platforms for packaging.tags.generic_tags --- src/pip/_internal/pep425tags.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index f14ed8c35cc..8194b584023 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -328,8 +328,16 @@ def _generic_tags( if abi: abis = [abi] - if platform is None: - return generic_tags(interpreter=interpreter, abis=abis) + platforms = None # type: Optional[List[str]] + if platform is not None: + platforms = _get_custom_platforms(platform, platform) + + if True: + return generic_tags( + interpreter=interpreter, + abis=abis, + platforms=platforms, + ) supported = [] # type: List[Tuple[str, str, str]] From 72dcd34eb214e7df95de3a8f49bb76750fbd5952 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 23 Nov 2019 18:02:52 -0500 Subject: [PATCH 1105/3170] Make packaging.tags.generic_tags unconditional --- src/pip/_internal/pep425tags.py | 42 +++++---------------------------- 1 file changed, 6 insertions(+), 36 deletions(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index 8194b584023..bbfff6ba87c 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -321,7 +321,7 @@ def _generic_tags( impl=None, # type: Optional[str] abi=None, # type: Optional[str] ): - # type: (...) -> Union[Iterator[Tag], List[Tuple[str, str, str]]] + # type: (...) -> Iterator[Tag] interpreter = _get_custom_interpreter(impl, version) abis = None # type: Optional[List[str]] @@ -332,41 +332,11 @@ def _generic_tags( if platform is not None: platforms = _get_custom_platforms(platform, platform) - if True: - return generic_tags( - interpreter=interpreter, - abis=abis, - platforms=platforms, - ) - - supported = [] # type: List[Tuple[str, str, str]] - - # Versions must be given with respect to the preference - if version is None: - version_info = get_impl_version_info() - versions = get_all_minor_versions_as_strings(version_info) - else: - versions = [version] - current_version = versions[0] - - impl = impl or interpreter_name() - - abis = [] # type: ignore # we will be removing this soon - - abi = abi or get_abi_tag() - if abi: - abis[0:0] = [abi] - - abis.append('none') - - arches = _get_custom_platforms(platform or get_platform(), platform) - - # Current version, current API (built specifically for our Python): - for abi in abis: - for arch in arches: - supported.append(('%s%s' % (impl, current_version), abi, arch)) - - return supported + return generic_tags( + interpreter=interpreter, + abis=abis, + platforms=platforms, + ) def _stable_unique_tags(tags): From 3e66ab0918dfd89f74b1c87b05675e6f29e97e03 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 23 Nov 2019 18:02:53 -0500 Subject: [PATCH 1106/3170] Inline packaging.tags.generic_tags --- src/pip/_internal/pep425tags.py | 32 +++++++------------------------- 1 file changed, 7 insertions(+), 25 deletions(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index bbfff6ba87c..38ac99aa388 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -315,30 +315,6 @@ def _get_custom_interpreter(implementation=None, version=None): return "{}{}".format(implementation, version) -def _generic_tags( - version=None, # type: Optional[str] - platform=None, # type: Optional[str] - impl=None, # type: Optional[str] - abi=None, # type: Optional[str] -): - # type: (...) -> Iterator[Tag] - interpreter = _get_custom_interpreter(impl, version) - - abis = None # type: Optional[List[str]] - if abi: - abis = [abi] - - platforms = None # type: Optional[List[str]] - if platform is not None: - platforms = _get_custom_platforms(platform, platform) - - return generic_tags( - interpreter=interpreter, - abis=abis, - platforms=platforms, - ) - - def _stable_unique_tags(tags): # type: (List[Tag]) -> Iterator[Tag] observed = set() # type: Set[Tag] @@ -393,7 +369,13 @@ def get_supported( ) ) else: - supported.extend(_generic_tags(version, platform, impl, abi)) + supported.extend( + generic_tags( + interpreter=interpreter, + abis=abis, + platforms=platforms, + ) + ) supported.extend( compatible_tags( python_version=python_version, From ad546b5e8da2b7758e7f795ff940071d82f0719e Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 23 Nov 2019 18:02:54 -0500 Subject: [PATCH 1107/3170] Remove unnecessary conversion in get_supported Now that we're fully using packaging.tags, everything in the supported list is already Tag. Further, since each function is responsible for its own set of non-overlapping tags, we can also remove the de-duplication. --- src/pip/_internal/pep425tags.py | 19 +++---------------- 1 file changed, 3 insertions(+), 16 deletions(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index 38ac99aa388..e577a295bdf 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -23,7 +23,7 @@ if MYPY_CHECK_RUNNING: from typing import ( - Callable, Iterator, List, Optional, Set, Tuple, Union + Callable, List, Optional, Tuple, Union ) from pip._vendor.packaging.tags import PythonVersion @@ -315,15 +315,6 @@ def _get_custom_interpreter(implementation=None, version=None): return "{}{}".format(implementation, version) -def _stable_unique_tags(tags): - # type: (List[Tag]) -> Iterator[Tag] - observed = set() # type: Set[Tag] - for tag in tags: - if tag not in observed: - observed.add(tag) - yield tag - - def get_supported( version=None, # type: Optional[str] platform=None, # type: Optional[str] @@ -343,7 +334,7 @@ def get_supported( :param abi: specify the exact abi you want valid tags for, or None. If None, use the local interpreter abi. """ - supported = [] # type: List[Union[Tag, Tuple[str, str, str]]] + supported = [] # type: List[Tag] python_version = None # type: Optional[PythonVersion] if version is not None: @@ -384,8 +375,4 @@ def get_supported( ) ) - tags = [ - parts if isinstance(parts, Tag) else Tag(*parts) - for parts in supported - ] - return list(_stable_unique_tags(tags)) + return supported From 9b3443583e73764411a0ad28f10934b4eca530c8 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 23 Nov 2019 18:02:55 -0500 Subject: [PATCH 1108/3170] Simplify _get_custom_platforms Since we only call this function when platform is not None, we can drop one of the branches. This is the only place using our manylinux auto-deduction functions, so those can be removed next. --- src/pip/_internal/pep425tags.py | 15 +++------------ 1 file changed, 3 insertions(+), 12 deletions(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index e577a295bdf..2d0664b952c 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -277,22 +277,13 @@ def _custom_manylinux_platforms(arch): return arches -def _get_custom_platforms(arch, platform): - # type: (str, Optional[str]) -> List[str] +def _get_custom_platforms(arch): + # type: (str) -> List[str] arch_prefix, arch_sep, arch_suffix = arch.partition('_') if arch.startswith('macosx'): arches = _mac_platforms(arch) elif arch_prefix in ['manylinux2014', 'manylinux2010']: arches = _custom_manylinux_platforms(arch) - elif platform is None: - arches = [] - if is_manylinux2014_compatible(): - arches.append('manylinux2014' + arch_sep + arch_suffix) - if is_manylinux2010_compatible(): - arches.append('manylinux2010' + arch_sep + arch_suffix) - if is_manylinux1_compatible(): - arches.append('manylinux1' + arch_sep + arch_suffix) - arches.append(arch) else: arches = [arch] return arches @@ -348,7 +339,7 @@ def get_supported( platforms = None # type: Optional[List[str]] if platform is not None: - platforms = _get_custom_platforms(platform, platform) + platforms = _get_custom_platforms(platform) is_cpython = (impl or interpreter_name()) == "cp" if is_cpython: From 2455977bc86f1732024dfe19e2c72614f27327c9 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 23 Nov 2019 18:02:57 -0500 Subject: [PATCH 1109/3170] Remove unused manylinux auto-deduction functions --- src/pip/_internal/pep425tags.py | 89 --------------------------------- tests/unit/test_pep425tags.py | 49 ------------------ 2 files changed, 138 deletions(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index 2d0664b952c..15b195d1688 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -18,7 +18,6 @@ mac_platforms, ) -import pip._internal.utils.glibc from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: @@ -136,94 +135,6 @@ def get_platform(): return result -def is_linux_armhf(): - # type: () -> bool - if get_platform() != "linux_armv7l": - return False - # hard-float ABI can be detected from the ELF header of the running - # process - try: - with open(sys.executable, 'rb') as f: - elf_header_raw = f.read(40) # read 40 first bytes of ELF header - except (IOError, OSError, TypeError): - return False - if elf_header_raw is None or len(elf_header_raw) < 40: - return False - if isinstance(elf_header_raw, str): - elf_header = [ord(c) for c in elf_header_raw] - else: - elf_header = [b for b in elf_header_raw] - result = elf_header[0:4] == [0x7f, 0x45, 0x4c, 0x46] # ELF magic number - result &= elf_header[4:5] == [1] # 32-bit ELF - result &= elf_header[5:6] == [1] # little-endian - result &= elf_header[18:20] == [0x28, 0] # ARM machine - result &= elf_header[39:40] == [5] # ARM EABIv5 - result &= (elf_header[37:38][0] & 4) == 4 # EF_ARM_ABI_FLOAT_HARD - return result - - -def is_manylinux1_compatible(): - # type: () -> bool - # Only Linux, and only x86-64 / i686 - if get_platform() not in {"linux_x86_64", "linux_i686"}: - return False - - # Check for presence of _manylinux module - try: - import _manylinux - return bool(_manylinux.manylinux1_compatible) - except (ImportError, AttributeError): - # Fall through to heuristic check below - pass - - # Check glibc version. CentOS 5 uses glibc 2.5. - return pip._internal.utils.glibc.have_compatible_glibc(2, 5) - - -def is_manylinux2010_compatible(): - # type: () -> bool - # Only Linux, and only x86-64 / i686 - if get_platform() not in {"linux_x86_64", "linux_i686"}: - return False - - # Check for presence of _manylinux module - try: - import _manylinux - return bool(_manylinux.manylinux2010_compatible) - except (ImportError, AttributeError): - # Fall through to heuristic check below - pass - - # Check glibc version. CentOS 6 uses glibc 2.12. - return pip._internal.utils.glibc.have_compatible_glibc(2, 12) - - -def is_manylinux2014_compatible(): - # type: () -> bool - # Only Linux, and only supported architectures - platform = get_platform() - if platform not in {"linux_x86_64", "linux_i686", "linux_aarch64", - "linux_armv7l", "linux_ppc64", "linux_ppc64le", - "linux_s390x"}: - return False - - # check for hard-float ABI in case we're running linux_armv7l not to - # install hard-float ABI wheel in a soft-float ABI environment - if platform == "linux_armv7l" and not is_linux_armhf(): - return False - - # Check for presence of _manylinux module - try: - import _manylinux - return bool(_manylinux.manylinux2014_compatible) - except (ImportError, AttributeError): - # Fall through to heuristic check below - pass - - # Check glibc version. CentOS 7 uses glibc 2.17. - return pip._internal.utils.glibc.have_compatible_glibc(2, 17) - - def get_all_minor_versions_as_strings(version_info): # type: (Tuple[int, ...]) -> List[str] versions = [] diff --git a/tests/unit/test_pep425tags.py b/tests/unit/test_pep425tags.py index a5469725e96..14774d530cf 100644 --- a/tests/unit/test_pep425tags.py +++ b/tests/unit/test_pep425tags.py @@ -119,55 +119,6 @@ def test_manual_abi_dm_flags(self): self.abi_tag_unicode('dm', {'Py_DEBUG': True, 'WITH_PYMALLOC': True}) -@pytest.mark.parametrize('is_manylinux_compatible', [ - pep425tags.is_manylinux1_compatible, - pep425tags.is_manylinux2010_compatible, - pep425tags.is_manylinux2014_compatible, -]) -class TestManylinuxTags(object): - """ - Tests common to all manylinux tags (e.g. manylinux1, manylinux2010, - ...) - """ - @patch('pip._internal.pep425tags.get_platform', lambda: 'linux_x86_64') - @patch('pip._internal.utils.glibc.have_compatible_glibc', - lambda major, minor: True) - def test_manylinux_compatible_on_linux_x86_64(self, - is_manylinux_compatible): - """ - Test that manylinuxes are enabled on linux_x86_64 - """ - assert is_manylinux_compatible() - - @patch('pip._internal.pep425tags.get_platform', lambda: 'linux_i686') - @patch('pip._internal.utils.glibc.have_compatible_glibc', - lambda major, minor: True) - def test_manylinux_compatible_on_linux_i686(self, - is_manylinux_compatible): - """ - Test that manylinuxes are enabled on linux_i686 - """ - assert is_manylinux_compatible() - - @patch('pip._internal.pep425tags.get_platform', lambda: 'linux_x86_64') - @patch('pip._internal.utils.glibc.have_compatible_glibc', - lambda major, minor: False) - def test_manylinux_2(self, is_manylinux_compatible): - """ - Test that manylinuxes are disabled with incompatible glibc - """ - assert not is_manylinux_compatible() - - @patch('pip._internal.pep425tags.get_platform', lambda: 'arm6vl') - @patch('pip._internal.utils.glibc.have_compatible_glibc', - lambda major, minor: True) - def test_manylinux_3(self, is_manylinux_compatible): - """ - Test that manylinuxes are disabled on arm6vl - """ - assert not is_manylinux_compatible() - - class TestManylinux2010Tags(object): @pytest.mark.parametrize("manylinux2010,manylinux1", [ From 7aaa705c15b16b13ac64807d1d23dcd797f5977d Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 23 Nov 2019 18:02:58 -0500 Subject: [PATCH 1110/3170] Remove unused glibc functions The remaining glibc-related functions are required to allow us to put the libc version in the pip user agent (for stats). --- src/pip/_internal/utils/glibc.py | 28 -------------------------- tests/unit/test_utils.py | 34 -------------------------------- 2 files changed, 62 deletions(-) diff --git a/src/pip/_internal/utils/glibc.py b/src/pip/_internal/utils/glibc.py index 42b1d3919a3..36104244138 100644 --- a/src/pip/_internal/utils/glibc.py +++ b/src/pip/_internal/utils/glibc.py @@ -4,9 +4,7 @@ from __future__ import absolute_import import os -import re import sys -import warnings from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -69,32 +67,6 @@ def glibc_version_string_ctypes(): return version_str -# Separated out from have_compatible_glibc for easier unit testing -def check_glibc_version(version_str, required_major, minimum_minor): - # type: (str, int, int) -> bool - # Parse string and check against requested version. - # - # We use a regexp instead of str.split because we want to discard any - # random junk that might come after the minor version -- this might happen - # in patched/forked versions of glibc (e.g. Linaro's version of glibc - # uses version strings like "2.20-2014.11"). See gh-3588. - m = re.match(r"(?P<major>[0-9]+)\.(?P<minor>[0-9]+)", version_str) - if not m: - warnings.warn("Expected glibc version with 2 components major.minor," - " got: %s" % version_str, RuntimeWarning) - return False - return (int(m.group("major")) == required_major and - int(m.group("minor")) >= minimum_minor) - - -def have_compatible_glibc(required_major, minimum_minor): - # type: (int, int) -> bool - version_str = glibc_version_string() - if version_str is None: - return False - return check_glibc_version(version_str, required_major, minimum_minor) - - # platform.libc_ver regularly returns completely nonsensical glibc # versions. E.g. on my computer, platform says: # diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 64c8aabf505..011543bdcc5 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -10,7 +10,6 @@ import stat import sys import time -import warnings from io import BytesIO import pytest @@ -24,7 +23,6 @@ from pip._internal.utils.deprecation import PipDeprecationWarning, deprecated from pip._internal.utils.encoding import BOMS, auto_decode from pip._internal.utils.glibc import ( - check_glibc_version, glibc_version_string, glibc_version_string_confstr, glibc_version_string_ctypes, @@ -538,38 +536,6 @@ def raises(error): class TestGlibc(object): - def test_manylinux_check_glibc_version(self): - """ - Test that the check_glibc_version function is robust against weird - glibc version strings. - """ - for two_twenty in ["2.20", - # used by "linaro glibc", see gh-3588 - "2.20-2014.11", - # weird possibilities that I just made up - "2.20+dev", - "2.20-custom", - "2.20.1", - ]: - assert check_glibc_version(two_twenty, 2, 15) - assert check_glibc_version(two_twenty, 2, 20) - assert not check_glibc_version(two_twenty, 2, 21) - assert not check_glibc_version(two_twenty, 3, 15) - assert not check_glibc_version(two_twenty, 1, 15) - - # For strings that we just can't parse at all, we should warn and - # return false - for bad_string in ["asdf", "", "foo.bar"]: - with warnings.catch_warnings(record=True) as ws: - warnings.filterwarnings("always") - assert not check_glibc_version(bad_string, 2, 5) - for w in ws: - if "Expected glibc version with" in str(w.message): - break - else: - # Didn't find the warning we were expecting - assert False - @pytest.mark.skipif("sys.platform == 'win32'") def test_glibc_version_string(self, monkeypatch): monkeypatch.setattr( From 896317d13d9bccf75b9cb35a37a83d93d3d2e55b Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 23 Nov 2019 18:02:59 -0500 Subject: [PATCH 1111/3170] Remove unused abi functions Previously, these were used when the user did not provide an explicit ABI. Now this is handled internally in `packaging.tags` (by `_cpython_abi` and `_generic_abi`). --- src/pip/_internal/pep425tags.py | 56 +------------------------ tests/unit/test_pep425tags.py | 73 ++------------------------------- 2 files changed, 4 insertions(+), 125 deletions(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index 15b195d1688..915c92b81d6 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -6,7 +6,6 @@ import platform import re import sys -import sysconfig from pip._vendor.packaging.tags import ( Tag, @@ -21,9 +20,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import ( - Callable, List, Optional, Tuple, Union - ) + from typing import List, Optional, Tuple from pip._vendor.packaging.tags import PythonVersion @@ -32,11 +29,6 @@ _osx_arch_pat = re.compile(r'(.+)_(\d+)_(\d+)_(.+)') -def get_config_var(var): - # type: (str) -> Optional[str] - return sysconfig.get_config_var(var) - - def version_info_to_nodot(version_info): # type: (Tuple[int, ...]) -> str # Only use up to the first two numbers. @@ -57,52 +49,6 @@ def get_impl_version_info(): return sys.version_info[0], sys.version_info[1] -def get_flag(var, fallback, expected=True, warn=True): - # type: (str, Callable[..., bool], Union[bool, int], bool) -> bool - """Use a fallback method for determining SOABI flags if the needed config - var is unset or unavailable.""" - val = get_config_var(var) - if val is None: - if warn: - logger.debug("Config variable '%s' is unset, Python ABI tag may " - "be incorrect", var) - return fallback() - return val == expected - - -def get_abi_tag(): - # type: () -> Optional[str] - """Return the ABI tag based on SOABI (if available) or emulate SOABI - (CPython 2, PyPy).""" - soabi = get_config_var('SOABI') - impl = interpreter_name() - abi = None # type: Optional[str] - - if not soabi and impl in {'cp', 'pp'} and hasattr(sys, 'maxunicode'): - d = '' - m = '' - u = '' - is_cpython = (impl == 'cp') - if get_flag( - 'Py_DEBUG', lambda: hasattr(sys, 'gettotalrefcount'), - warn=is_cpython): - d = 'd' - if sys.version_info < (3, 8) and get_flag( - 'WITH_PYMALLOC', lambda: is_cpython, warn=is_cpython): - m = 'm' - if sys.version_info < (3, 3) and get_flag( - 'Py_UNICODE_SIZE', lambda: sys.maxunicode == 0x10ffff, - expected=4, warn=is_cpython): - u = 'u' - abi = '%s%s%s%s%s' % (impl, interpreter_version(), d, m, u) - elif soabi and soabi.startswith('cpython-'): - abi = 'cp' + soabi.split('-')[1] - elif soabi: - abi = soabi.replace('.', '_').replace('-', '_') - - return abi - - def _is_running_32bit(): # type: () -> bool return sys.maxsize == 2147483647 diff --git a/tests/unit/test_pep425tags.py b/tests/unit/test_pep425tags.py index 14774d530cf..71d0aefe4f9 100644 --- a/tests/unit/test_pep425tags.py +++ b/tests/unit/test_pep425tags.py @@ -1,8 +1,7 @@ -import sys +import sysconfig import pytest from mock import patch -from pip._vendor.packaging.tags import interpreter_name, interpreter_version from pip._internal import pep425tags @@ -28,9 +27,7 @@ def mock_get_config_var(self, **kwd): """ Patch sysconfig.get_config_var for arbitrary keys. """ - import pip._internal.pep425tags - - get_config_var = pip._internal.pep425tags.sysconfig.get_config_var + get_config_var = sysconfig.get_config_var def _mock_get_config_var(var): if var in kwd: @@ -38,45 +35,6 @@ def _mock_get_config_var(var): return get_config_var(var) return _mock_get_config_var - def abi_tag_unicode(self, flags, config_vars): - """ - Used to test ABI tags, verify correct use of the `u` flag - """ - import pip._internal.pep425tags - - config_vars.update({'SOABI': None}) - base = interpreter_name() + interpreter_version() - - if sys.version_info >= (3, 8): - # Python 3.8 removes the m flag, so don't look for it. - flags = flags.replace('m', '') - - if sys.version_info < (3, 3): - config_vars.update({'Py_UNICODE_SIZE': 2}) - mock_gcf = self.mock_get_config_var(**config_vars) - with patch('pip._internal.pep425tags.sysconfig.get_config_var', - mock_gcf): - abi_tag = pip._internal.pep425tags.get_abi_tag() - assert abi_tag == base + flags - - config_vars.update({'Py_UNICODE_SIZE': 4}) - mock_gcf = self.mock_get_config_var(**config_vars) - with patch('pip._internal.pep425tags.sysconfig.get_config_var', - mock_gcf): - abi_tag = pip._internal.pep425tags.get_abi_tag() - assert abi_tag == base + flags + 'u' - - else: - # On Python >= 3.3, UCS-4 is essentially permanently enabled, and - # Py_UNICODE_SIZE is None. SOABI on these builds does not include - # the 'u' so manual SOABI detection should not do so either. - config_vars.update({'Py_UNICODE_SIZE': None}) - mock_gcf = self.mock_get_config_var(**config_vars) - with patch('pip._internal.pep425tags.sysconfig.get_config_var', - mock_gcf): - abi_tag = pip._internal.pep425tags.get_abi_tag() - assert abi_tag == base + flags - def test_no_hyphen_tag(self): """ Test that no tag contains a hyphen. @@ -85,8 +43,7 @@ def test_no_hyphen_tag(self): mock_gcf = self.mock_get_config_var(SOABI='cpython-35m-darwin') - with patch('pip._internal.pep425tags.sysconfig.get_config_var', - mock_gcf): + with patch('sysconfig.get_config_var', mock_gcf): supported = pip._internal.pep425tags.get_supported() for tag in supported: @@ -94,30 +51,6 @@ def test_no_hyphen_tag(self): assert '-' not in tag.abi assert '-' not in tag.platform - def test_manual_abi_noflags(self): - """ - Test that no flags are set on a non-PyDebug, non-Pymalloc ABI tag. - """ - self.abi_tag_unicode('', {'Py_DEBUG': False, 'WITH_PYMALLOC': False}) - - def test_manual_abi_d_flag(self): - """ - Test that the `d` flag is set on a PyDebug, non-Pymalloc ABI tag. - """ - self.abi_tag_unicode('d', {'Py_DEBUG': True, 'WITH_PYMALLOC': False}) - - def test_manual_abi_m_flag(self): - """ - Test that the `m` flag is set on a non-PyDebug, Pymalloc ABI tag. - """ - self.abi_tag_unicode('m', {'Py_DEBUG': False, 'WITH_PYMALLOC': True}) - - def test_manual_abi_dm_flags(self): - """ - Test that the `dm` flags are set on a PyDebug, Pymalloc ABI tag. - """ - self.abi_tag_unicode('dm', {'Py_DEBUG': True, 'WITH_PYMALLOC': True}) - class TestManylinux2010Tags(object): From 2b1b60f6db39ee3f26a0174ae66fbb909f94a709 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 23 Nov 2019 18:03:00 -0500 Subject: [PATCH 1112/3170] Remove unused get_platform function Now handled internally in `packaging.tags` (in `_platform_tags`). --- src/pip/_internal/pep425tags.py | 34 --------------------------------- 1 file changed, 34 deletions(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index 915c92b81d6..3e5bc234b55 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -1,9 +1,7 @@ """Generate and work with PEP 425 Compatibility Tags.""" from __future__ import absolute_import -import distutils.util import logging -import platform import re import sys @@ -49,38 +47,6 @@ def get_impl_version_info(): return sys.version_info[0], sys.version_info[1] -def _is_running_32bit(): - # type: () -> bool - return sys.maxsize == 2147483647 - - -def get_platform(): - # type: () -> str - """Return our platform name 'win32', 'linux_x86_64'""" - if sys.platform == 'darwin': - # distutils.util.get_platform() returns the release based on the value - # of MACOSX_DEPLOYMENT_TARGET on which Python was built, which may - # be significantly older than the user's current machine. - release, _, machine = platform.mac_ver() - split_ver = release.split('.') - - if machine == "x86_64" and _is_running_32bit(): - machine = "i386" - elif machine == "ppc64" and _is_running_32bit(): - machine = "ppc" - - return 'macosx_{}_{}_{}'.format(split_ver[0], split_ver[1], machine) - - # XXX remove distutils dependency - result = distutils.util.get_platform().replace('.', '_').replace('-', '_') - if result == "linux_x86_64" and _is_running_32bit(): - # 32 bit Python program (running on a 64 bit Linux): pip should only - # install and run 32 bit compiled extensions in that case. - result = "linux_i686" - - return result - - def get_all_minor_versions_as_strings(version_info): # type: (Tuple[int, ...]) -> List[str] versions = [] From ae21af701db314a6ff97c86b6f3afb96d10c8fd4 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 23 Nov 2019 18:02:59 -0500 Subject: [PATCH 1113/3170] Remove unused version functions --- src/pip/_internal/pep425tags.py | 25 ------------------------- 1 file changed, 25 deletions(-) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/pep425tags.py index 3e5bc234b55..a2386ee75b8 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/pep425tags.py @@ -3,7 +3,6 @@ import logging import re -import sys from pip._vendor.packaging.tags import ( Tag, @@ -33,30 +32,6 @@ def version_info_to_nodot(version_info): return ''.join(map(str, version_info[:2])) -def get_impl_version_info(): - # type: () -> Tuple[int, ...] - """Return sys.version_info-like tuple for use in decrementing the minor - version.""" - if interpreter_name() == 'pp': - # as per https://github.com/pypa/pip/issues/2882 - # attrs exist only on pypy - return (sys.version_info[0], - sys.pypy_version_info.major, # type: ignore - sys.pypy_version_info.minor) # type: ignore - else: - return sys.version_info[0], sys.version_info[1] - - -def get_all_minor_versions_as_strings(version_info): - # type: (Tuple[int, ...]) -> List[str] - versions = [] - major = version_info[:-1] - # Support all previous minor Python versions. - for minor in range(version_info[-1], -1, -1): - versions.append(''.join(map(str, major + (minor,)))) - return versions - - def _mac_platforms(arch): # type: (str) -> List[str] match = _osx_arch_pat.match(arch) From d7fda717ac1e269806607d912d126c047348aed0 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 23 Nov 2019 18:03:01 -0500 Subject: [PATCH 1114/3170] Add news --- news/6908.removal | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 news/6908.removal diff --git a/news/6908.removal b/news/6908.removal new file mode 100644 index 00000000000..aca9d590ab0 --- /dev/null +++ b/news/6908.removal @@ -0,0 +1,2 @@ +Remove wheel tag calculation from pip and use ``packaging.tags``. This +should provide more tags ordered better than in prior releases. From 8c28cda679c8cf927d81ccf0f6519f5cd06fc0fa Mon Sep 17 00:00:00 2001 From: Hugo <hugovk@users.noreply.github.com> Date: Sat, 9 Nov 2019 13:32:03 +0200 Subject: [PATCH 1115/3170] Test Windows on Python 3.8 --- .azure-pipelines/jobs/test-windows.yml | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/.azure-pipelines/jobs/test-windows.yml b/.azure-pipelines/jobs/test-windows.yml index f0ab8f812df..1a933a6934b 100644 --- a/.azure-pipelines/jobs/test-windows.yml +++ b/.azure-pipelines/jobs/test-windows.yml @@ -26,7 +26,10 @@ jobs: Python37-x64: python.version: '3.7' python.architecture: x64 - maxParallel: 5 + Python38-x64: + python.version: '3.8' + python.architecture: x64 + maxParallel: 6 steps: - template: ../steps/run-tests-windows.yml @@ -54,7 +57,10 @@ jobs: Python37-x86: python.version: '3.7' python.architecture: x86 - maxParallel: 5 + Python38-x86: + python.version: '3.8' + python.architecture: x86 + maxParallel: 6 steps: - template: ../steps/run-tests-windows.yml From f08dc673a7910cec3bc537369557bbf90c95c110 Mon Sep 17 00:00:00 2001 From: Hugo <hugovk@users.noreply.github.com> Date: Sun, 10 Nov 2019 12:49:33 +0200 Subject: [PATCH 1116/3170] Upgrade virtualenv --- .azure-pipelines/steps/run-tests-windows.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.azure-pipelines/steps/run-tests-windows.yml b/.azure-pipelines/steps/run-tests-windows.yml index 30b0021cb00..4908c0ec70a 100644 --- a/.azure-pipelines/steps/run-tests-windows.yml +++ b/.azure-pipelines/steps/run-tests-windows.yml @@ -25,7 +25,7 @@ steps: Set-Acl "R:\Temp" $acl displayName: Set RAMDisk Permissions -- bash: pip install --upgrade setuptools tox +- bash: pip install --upgrade setuptools tox virtualenv displayName: Install Tox - script: tox -e py -- -m unit -n auto --junit-xml=junit/unit-test.xml From 4d79037527b10b5f59f7b6ea4c3448cd40a39e1f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Thu, 2 Jan 2020 13:07:01 +0100 Subject: [PATCH 1117/3170] Eagerly normalize the cache directory Fixes #7541 --- news/7541.bugfix | 1 + src/pip/_internal/cli/base_command.py | 3 ++- src/pip/_internal/utils/filesystem.py | 2 ++ 3 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 news/7541.bugfix diff --git a/news/7541.bugfix b/news/7541.bugfix new file mode 100644 index 00000000000..4fccfe3adf0 --- /dev/null +++ b/news/7541.bugfix @@ -0,0 +1 @@ +Correctly handle relative cache directory provided via --cache-dir. diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index 74dcb0b3fc0..628faa3eee0 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -33,7 +33,7 @@ from pip._internal.utils.deprecation import deprecated from pip._internal.utils.filesystem import check_path_owner from pip._internal.utils.logging import BrokenStdoutLoggingError, setup_logging -from pip._internal.utils.misc import get_prog +from pip._internal.utils.misc import get_prog, normalize_path from pip._internal.utils.temp_dir import global_tempdir_manager from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.virtualenv import running_under_virtualenv @@ -170,6 +170,7 @@ def _main(self, args): sys.exit(VIRTUALENV_NOT_FOUND) if options.cache_dir: + options.cache_dir = normalize_path(options.cache_dir) if not check_path_owner(options.cache_dir): logger.warning( "The directory '%s' or its parent directory is not owned " diff --git a/src/pip/_internal/utils/filesystem.py b/src/pip/_internal/utils/filesystem.py index 7e1e3c8c7a5..6f1537e4032 100644 --- a/src/pip/_internal/utils/filesystem.py +++ b/src/pip/_internal/utils/filesystem.py @@ -33,6 +33,8 @@ def check_path_owner(path): if sys.platform == "win32" or not hasattr(os, "geteuid"): return True + assert os.path.isabs(path) + previous = None while path != previous: if os.path.lexists(path): From 8ce08590129e05526394a6f3f9256124094a09e0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Thu, 2 Jan 2020 13:24:09 +0100 Subject: [PATCH 1118/3170] Remove redundant expanduser in WheelCache Path normalization, which includes expanduser is now done eagerly. Assert this when initializing WheelCache. --- src/pip/_internal/cache.py | 4 ++-- tests/unit/test_cache.py | 8 +------- 2 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/cache.py b/src/pip/_internal/cache.py index c0e002c414e..abecd78f8d9 100644 --- a/src/pip/_internal/cache.py +++ b/src/pip/_internal/cache.py @@ -15,7 +15,6 @@ from pip._internal.exceptions import InvalidWheelFilename from pip._internal.models.link import Link from pip._internal.models.wheel import Wheel -from pip._internal.utils.compat import expanduser from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.urls import path_to_url @@ -51,7 +50,8 @@ class Cache(object): def __init__(self, cache_dir, format_control, allowed_formats): # type: (str, FormatControl, Set[str]) -> None super(Cache, self).__init__() - self.cache_dir = expanduser(cache_dir) if cache_dir else None + assert not cache_dir or os.path.isabs(cache_dir) + self.cache_dir = cache_dir or None self.format_control = format_control self.allowed_formats = allowed_formats diff --git a/tests/unit/test_cache.py b/tests/unit/test_cache.py index 1a4f98b28bd..31f8f729341 100644 --- a/tests/unit/test_cache.py +++ b/tests/unit/test_cache.py @@ -5,15 +5,9 @@ from pip._internal.cache import WheelCache, _hash_dict from pip._internal.models.format_control import FormatControl from pip._internal.models.link import Link -from pip._internal.utils.compat import expanduser from pip._internal.utils.misc import ensure_dir -def test_expands_path(): - wc = WheelCache("~/.foo/", None) - assert wc.cache_dir == expanduser("~/.foo/") - - def test_falsey_path_none(): wc = WheelCache(False, None) assert wc.cache_dir is None @@ -23,7 +17,7 @@ def test_subdirectory_fragment(): """ Test the subdirectory URL fragment is part of the cache key. """ - wc = WheelCache("~/.foo/", None) + wc = WheelCache("/tmp/.foo/", None) link1 = Link("git+https://g.c/o/r#subdirectory=d1") link2 = Link("git+https://g.c/o/r#subdirectory=d2") assert wc.get_path_for_link(link1) != wc.get_path_for_link(link2) From 6fa3a2ee58375264ca9838219fcd439e4c52ca8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Thu, 2 Jan 2020 13:35:18 +0100 Subject: [PATCH 1119/3170] Remove redundant cache dir normalization in _build_session Now the cache is normalized eagerly, this is not necessary here anymore. assert the path is absolute to prevent regression. --- src/pip/_internal/cli/req_command.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index cf6e101f78a..9383b3b8dca 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -28,7 +28,6 @@ make_link_collector, pip_self_version_check, ) -from pip._internal.utils.misc import normalize_path from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: @@ -81,9 +80,10 @@ def get_default_session(self, options): def _build_session(self, options, retries=None, timeout=None): # type: (Values, Optional[int], Optional[int]) -> PipSession + assert not options.cache_dir or os.path.isabs(options.cache_dir) session = PipSession( cache=( - normalize_path(os.path.join(options.cache_dir, "http")) + os.path.join(options.cache_dir, "http") if options.cache_dir else None ), retries=retries if retries is not None else options.retries, From e2c345100152fc7f2638ec8c3fd117cb67959c26 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 8 Jan 2020 18:23:34 +0530 Subject: [PATCH 1120/3170] Delete tmpdir with rmtree to handle Unicode paths pytest (rather py.path.local) does not handle non-ASCII paths properly on Windows with Python 2, but Python's builtin shutil.rmtree() does. Co-authored-by: Pradyun Gedam <pradyunsg@gmail.com> --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 0b8496856f5..16b5c6202e9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -103,7 +103,7 @@ def tmpdir(request, tmpdir): # This should prevent us from needing a multiple gigabyte temporary # directory while running the tests. if not request.config.getoption("--keep-tmpdir"): - tmpdir.remove(ignore_errors=True) + shutil.rmtree(six.text_type(tmpdir), ignore_errors=True) @pytest.fixture(autouse=True) From 71e0ea19af7fb8dd3eb4b955cea04373f74618de Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 8 Jan 2020 20:11:28 -0500 Subject: [PATCH 1121/3170] Assert req.source_dir is None during linked requirement preparation req.source_dir is only set by: 1. `InstallRequirement.__init__` 2. `InstallRequirement.ensure_has_source_dir` `InstallRequirement.__init__` is only called with source_dir for editable requirements, for which we would not call `RequirementPreparer.prepare_linked_requirement` (only `prepare_editable_requirement`). We will use this assertion for justifying later refactoring. --- src/pip/_internal/operations/prepare.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 5f5505cd71c..0b61f20524d 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -420,6 +420,8 @@ def prepare_linked_requirement( # editable in a req, a non deterministic error # occurs when the script attempts to unpack the # build directory + # Since source_dir is only set for editable requirements. + assert req.source_dir is None req.ensure_has_source_dir(self.build_dir) # If a checkout exists, it's unwise to keep going. version # inconsistencies are logged later, but do not fail the From 3fac3d74ac00505b23ffb597dda3052820951916 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 8 Jan 2020 20:30:15 -0500 Subject: [PATCH 1122/3170] Inline _make_build_dir in InstallRequirement This untested function was only used in one place. Inlining it makes it easier to see the symmetry between the writing of the delete marker file in `ensure_build_location` and checking for it in `remove_temporary_source`. --- src/pip/_internal/req/req_install.py | 5 +++-- src/pip/_internal/utils/misc.py | 6 ------ 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 58746cd2a41..22ac24b96d3 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -36,9 +36,9 @@ from pip._internal.utils.marker_files import ( PIP_DELETE_MARKER_FILENAME, has_delete_marker_file, + write_delete_marker_file, ) from pip._internal.utils.misc import ( - _make_build_dir, ask_path_exists, backup_dir, display_path, @@ -370,7 +370,8 @@ def ensure_build_location(self, build_dir): # need this) if not os.path.exists(build_dir): logger.debug('Creating directory %s', build_dir) - _make_build_dir(build_dir) + os.makedirs(build_dir) + write_delete_marker_file(build_dir) return os.path.join(build_dir, name) def _set_requirement(self): diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index a7ee5398b12..4a581601991 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -39,7 +39,6 @@ stdlib_pkgs, str_to_display, ) -from pip._internal.utils.marker_files import write_delete_marker_file from pip._internal.utils.typing import MYPY_CHECK_RUNNING, cast from pip._internal.utils.virtualenv import ( running_under_virtualenv, @@ -523,11 +522,6 @@ def write_output(msg, *args): logger.info(msg, *args) -def _make_build_dir(build_dir): - os.makedirs(build_dir) - write_delete_marker_file(build_dir) - - class FakeFile(object): """Wrap a list of lines in an object with readline() to make ConfigParser happy.""" From 37f97140afe2c4178d519645815471d94af62192 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 9 Jan 2020 13:00:49 +0530 Subject: [PATCH 1123/3170] Also use rmtree to remove tmpdir_factory Same as e2c345100152fc7f2638ec8c3fd117cb67959c26 --- tests/conftest.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 16b5c6202e9..bd3ca171f88 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -83,7 +83,10 @@ def tmpdir_factory(request, tmpdir_factory): """ yield tmpdir_factory if not request.config.getoption("--keep-tmpdir"): - tmpdir_factory.getbasetemp().remove(ignore_errors=True) + shutil.rmtree( + six.text_type(tmpdir_factory.getbasetemp()), + ignore_errors=True, + ) @pytest.fixture From 7a80acaf446c018bd4be0b970a38466dee240d4d Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 9 Jan 2020 13:01:31 +0530 Subject: [PATCH 1124/3170] Tell shutil.make_archive to use Unicode paths By default, make_archive uses str paths on Python 2, which causes it to skip files with unencodable names. By passing in a unicode base_dir explicitly, it is smart enough to use unicode all the way down instead. --- tests/lib/__init__.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index 10c9e47d67b..de30829a3ab 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -15,7 +15,7 @@ from zipfile import ZipFile import pytest -from pip._vendor.six import PY2, ensure_binary +from pip._vendor.six import PY2, ensure_binary, text_type from scripttest import FoundDir, TestFileEnvironment from pip._internal.index.collector import LinkCollector @@ -1053,7 +1053,12 @@ def hello(): path.write_bytes(ensure_binary(files[fname])) retval = script.scratch_path / archive_name - generated = shutil.make_archive(retval, 'zip', script.temp_path) + generated = shutil.make_archive( + retval, + 'zip', + root_dir=script.temp_path, + base_dir=text_type(os.curdir), + ) shutil.move(generated, retval) shutil.rmtree(script.temp_path) From facf5c8894bce519066e9f5c108100a59a7116dd Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 9 Jan 2020 13:11:30 +0530 Subject: [PATCH 1125/3170] Add comments to unicode workarounds --- tests/conftest.py | 6 ++++++ tests/lib/__init__.py | 3 +++ 2 files changed, 9 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index bd3ca171f88..fd8204713ee 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -83,6 +83,9 @@ def tmpdir_factory(request, tmpdir_factory): """ yield tmpdir_factory if not request.config.getoption("--keep-tmpdir"): + # py.path.remove() uses str paths on Python 2 and cannot + # handle non-ASCII file names. This works around the problem by + # passing a unicode object to rmtree(). shutil.rmtree( six.text_type(tmpdir_factory.getbasetemp()), ignore_errors=True, @@ -106,6 +109,9 @@ def tmpdir(request, tmpdir): # This should prevent us from needing a multiple gigabyte temporary # directory while running the tests. if not request.config.getoption("--keep-tmpdir"): + # py.path.remove() uses str paths on Python 2 and cannot + # handle non-ASCII file names. This works around the problem by + # passing a unicode object to rmtree(). shutil.rmtree(six.text_type(tmpdir), ignore_errors=True) diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index de30829a3ab..9a55c156e4a 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -1052,6 +1052,9 @@ def hello(): path.parent.mkdir(exist_ok=True, parents=True) path.write_bytes(ensure_binary(files[fname])) + # The base_dir cast is required to make `shutil.make_archive()` use + # Unicode paths on Python 2, making it able to properly archive + # files with non-ASCII names. retval = script.scratch_path / archive_name generated = shutil.make_archive( retval, From ff4ee68470d63c1f018324b8d1df8eb52cb99a23 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 9 Jan 2020 13:15:35 +0530 Subject: [PATCH 1126/3170] News --- news/7577.trivial | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 news/7577.trivial diff --git a/news/7577.trivial b/news/7577.trivial new file mode 100644 index 00000000000..e69de29bb2d From 0a57e4e9f27e9e7b2c95bf1c5e6e71a36ecbeded Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 13 Dec 2019 22:05:22 -0500 Subject: [PATCH 1127/3170] Manage temp directory deletion centrally This gives us a global toggle that we can use to control whether temporary directories get deleted from one place (ideally, in the commands taking --no-clean). --- src/pip/_internal/utils/temp_dir.py | 48 +++++++++++++++++++++++++++-- tests/unit/test_utils_temp_dir.py | 27 ++++++++++++++++ 2 files changed, 73 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/utils/temp_dir.py b/src/pip/_internal/utils/temp_dir.py index 0be0ff785cd..2d342236567 100644 --- a/src/pip/_internal/utils/temp_dir.py +++ b/src/pip/_internal/utils/temp_dir.py @@ -13,7 +13,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Any, Iterator, Optional, TypeVar + from typing import Any, Dict, Iterator, Optional, TypeVar _T = TypeVar('_T', bound='TempDirectory') @@ -36,6 +36,47 @@ def global_tempdir_manager(): _tempdir_manager = old_tempdir_manager +class TempDirectoryTypeRegistry(object): + """Manages temp directory behavior + """ + + def __init__(self): + # type: () -> None + self._should_delete = {} # type: Dict[str, bool] + + def set_delete(self, kind, value): + # type: (str, bool) -> None + """Indicate whether a TempDirectory of the given kind should be + auto-deleted. + """ + self._should_delete[kind] = value + + def get_delete(self, kind): + # type: (str) -> bool + """Get configured auto-delete flag for a given TempDirectory type, + default True. + """ + return self._should_delete.get(kind, True) + + +_tempdir_registry = None # type: Optional[TempDirectoryTypeRegistry] + + +@contextmanager +def tempdir_registry(): + # type: () -> Iterator[TempDirectoryTypeRegistry] + """Provides a scoped global tempdir registry that can be used to dictate + whether directories should be deleted. + """ + global _tempdir_registry + old_tempdir_registry = _tempdir_registry + _tempdir_registry = TempDirectoryTypeRegistry() + try: + yield _tempdir_registry + finally: + _tempdir_registry = old_tempdir_registry + + class TempDirectory(object): """Helper class that owns and cleans up a temporary directory. @@ -68,8 +109,11 @@ def __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. + # an explicit delete option, then we'll default to deleting unless + # the tempdir_registry says otherwise. delete = True + if _tempdir_registry: + delete = _tempdir_registry.get_delete(kind) if path is None: path = self._create(kind) diff --git a/tests/unit/test_utils_temp_dir.py b/tests/unit/test_utils_temp_dir.py index 9b45a75b9e2..c395a7861c0 100644 --- a/tests/unit/test_utils_temp_dir.py +++ b/tests/unit/test_utils_temp_dir.py @@ -11,6 +11,7 @@ AdjacentTempDirectory, TempDirectory, global_tempdir_manager, + tempdir_registry, ) @@ -207,3 +208,29 @@ def test_tempdirectory_asserts_global_tempdir(monkeypatch): monkeypatch.setattr(temp_dir, "_tempdir_manager", None) with pytest.raises(AssertionError): TempDirectory(globally_managed=True) + + +deleted_kind = "deleted" +not_deleted_kind = "not-deleted" + + +@pytest.mark.parametrize("delete,kind,exists", [ + (None, deleted_kind, False), + (True, deleted_kind, False), + (False, deleted_kind, True), + (None, not_deleted_kind, True), + (True, not_deleted_kind, False), + (False, not_deleted_kind, True), + (None, "unspecified", False), + (True, "unspecified", False), + (False, "unspecified", True), +]) +def test_tempdir_registry(kind, delete, exists): + with tempdir_registry() as registry: + registry.set_delete(deleted_kind, True) + registry.set_delete(not_deleted_kind, False) + + with TempDirectory(delete=delete, kind=kind) as d: + path = d.path + assert os.path.exists(path) + assert os.path.exists(path) == exists From 99f582185e3ef5a7264d319fa960aadb55df982b Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <hugovk@users.noreply.github.com> Date: Fri, 10 Jan 2020 23:33:05 +0200 Subject: [PATCH 1128/3170] Add docs and repo to project_urls metadata --- setup.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/setup.py b/setup.py index 13e919c3fc7..7dab1fd45b2 100644 --- a/setup.py +++ b/setup.py @@ -53,6 +53,10 @@ def get_version(rel_path): ], url='https://pip.pypa.io/', keywords='distutils easy_install egg setuptools wheel virtualenv', + project_urls={ + "Documentation": "https://pip.pypa.io", + "Source": "https://github.com/pypa/pip", + }, author='The pip developers', author_email='pypa-dev@groups.google.com', From 2801de5825c724c9e2e381f03c5b17693f779b0c Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 11 Jan 2020 19:32:44 -0500 Subject: [PATCH 1129/3170] Parametrize long relative install tests This lets us make better use of parallelization and will let us remove the unnecessary uninstallation from these individual tests. --- tests/functional/test_install.py | 18 ++++++++++++++---- tests/functional/test_install_reqs.py | 18 ++++++++++++++---- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 713c9518b5d..2e8b0d98210 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -406,7 +406,12 @@ def test_basic_install_from_local_directory(script, data): assert egg_info_folder in result.files_created, str(result) -def test_basic_install_relative_directory(script, data): +@pytest.mark.parametrize("test_type", [ + ("rel_path"), + ("rel_url"), + ("embedded_rel_path"), +]) +def test_basic_install_relative_directory(script, data, test_type): """ Test installing a requirement using a relative path. """ @@ -427,9 +432,14 @@ def test_basic_install_relative_directory(script, data): ) embedded_rel_path = script.scratch_path.joinpath(full_rel_path) - # For each relative path, install as either editable or not using either - # URLs with egg links or not. - for req_path in (full_rel_path, full_rel_url, embedded_rel_path): + req_path = { + "rel_path": full_rel_path, + "rel_url": full_rel_url, + "embedded_rel_path": embedded_rel_path, + }[test_type] + + # Install as either editable or not. + if True: # Regular install. result = script.pip('install', req_path, cwd=script.scratch_path) diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index e478d594cef..a340c09e88d 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -57,7 +57,12 @@ def test_schema_check_in_requirements_file(script): ) -def test_relative_requirements_file(script, data): +@pytest.mark.parametrize("test_type", [ + ("rel_path"), + ("rel_url"), + ("embedded_rel_path"), +]) +def test_relative_requirements_file(script, data, test_type): """ Test installing from a requirements file with a relative path. For path URLs, use an egg= definition. @@ -78,9 +83,14 @@ def test_relative_requirements_file(script, data): full_rel_url = 'file:' + full_rel_path + '#egg=FSPkg' embedded_rel_path = script.scratch_path.joinpath(full_rel_path) - # For each relative path, install as either editable or not using either - # URLs with egg links or not. - for req_path in (full_rel_path, full_rel_url, embedded_rel_path): + req_path = { + "rel_path": full_rel_path, + "rel_url": full_rel_url, + "embedded_rel_path": embedded_rel_path, + }[test_type] + + # Install as either editable or not. + if True: req_path = req_path.replace(os.path.sep, '/') # Regular install. with requirements_file(req_path + '\n', From f89013daa487333407a73c951e52d2aefd9c640e Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 11 Jan 2020 19:37:10 -0500 Subject: [PATCH 1130/3170] Parametrize editable for relative install tests --- tests/functional/test_install.py | 18 ++++++++++-------- tests/functional/test_install_reqs.py | 21 +++++++++++---------- 2 files changed, 21 insertions(+), 18 deletions(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 2e8b0d98210..90ceec53975 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -406,12 +406,15 @@ def test_basic_install_from_local_directory(script, data): assert egg_info_folder in result.files_created, str(result) -@pytest.mark.parametrize("test_type", [ - ("rel_path"), - ("rel_url"), - ("embedded_rel_path"), +@pytest.mark.parametrize("test_type,editable", [ + ("rel_path", False), + ("rel_path", True), + ("rel_url", False), + ("rel_url", True), + ("embedded_rel_path", False), + ("embedded_rel_path", True), ]) -def test_basic_install_relative_directory(script, data, test_type): +def test_basic_install_relative_directory(script, data, test_type, editable): """ Test installing a requirement using a relative path. """ @@ -439,14 +442,13 @@ def test_basic_install_relative_directory(script, data, test_type): }[test_type] # Install as either editable or not. - if True: - # Regular install. + if not editable: result = script.pip('install', req_path, cwd=script.scratch_path) assert egg_info_file in result.files_created, str(result) assert package_folder in result.files_created, str(result) script.pip('uninstall', '-y', 'fspkg') - + else: # Editable install. result = script.pip('install', '-e' + req_path, cwd=script.scratch_path) diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index a340c09e88d..47c92757ee9 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -57,12 +57,15 @@ def test_schema_check_in_requirements_file(script): ) -@pytest.mark.parametrize("test_type", [ - ("rel_path"), - ("rel_url"), - ("embedded_rel_path"), +@pytest.mark.parametrize("test_type,editable", [ + ("rel_path", False), + ("rel_path", True), + ("rel_url", False), + ("rel_url", True), + ("embedded_rel_path", False), + ("embedded_rel_path", True), ]) -def test_relative_requirements_file(script, data, test_type): +def test_relative_requirements_file(script, data, test_type, editable): """ Test installing from a requirements file with a relative path. For path URLs, use an egg= definition. @@ -89,10 +92,9 @@ def test_relative_requirements_file(script, data, test_type): "embedded_rel_path": embedded_rel_path, }[test_type] + req_path = req_path.replace(os.path.sep, '/') # Install as either editable or not. - if True: - req_path = req_path.replace(os.path.sep, '/') - # Regular install. + if not editable: with requirements_file(req_path + '\n', script.scratch_path) as reqs_file: result = script.pip('install', '-vvv', '-r', reqs_file.name, @@ -100,8 +102,7 @@ def test_relative_requirements_file(script, data, test_type): assert egg_info_file in result.files_created, str(result) assert package_folder in result.files_created, str(result) script.pip('uninstall', '-y', 'fspkg') - - # Editable install. + else: with requirements_file('-e ' + req_path + '\n', script.scratch_path) as reqs_file: result = script.pip('install', '-vvv', '-r', reqs_file.name, From e53d10db01cc4123752a12439dca4456a9649cc6 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 11 Jan 2020 19:39:12 -0500 Subject: [PATCH 1131/3170] Remove unnecessary uninstall Since a new temporary script path is used for each test, no need to do uninstall. --- tests/functional/test_install.py | 2 -- tests/functional/test_install_reqs.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 90ceec53975..3c31534ce96 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -447,13 +447,11 @@ def test_basic_install_relative_directory(script, data, test_type, editable): cwd=script.scratch_path) assert egg_info_file in result.files_created, str(result) assert package_folder in result.files_created, str(result) - script.pip('uninstall', '-y', 'fspkg') else: # Editable install. result = script.pip('install', '-e' + req_path, cwd=script.scratch_path) assert egg_link_file in result.files_created, str(result) - script.pip('uninstall', '-y', 'fspkg') def test_install_quiet(script, data): diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index 47c92757ee9..2584e09535a 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -101,14 +101,12 @@ def test_relative_requirements_file(script, data, test_type, editable): cwd=script.scratch_path) assert egg_info_file in result.files_created, str(result) assert package_folder in result.files_created, str(result) - script.pip('uninstall', '-y', 'fspkg') else: with requirements_file('-e ' + req_path + '\n', script.scratch_path) as reqs_file: result = script.pip('install', '-vvv', '-r', reqs_file.name, cwd=script.scratch_path) assert egg_link_file in result.files_created, str(result) - script.pip('uninstall', '-y', 'fspkg') @pytest.mark.network From 10022df906dd36ef48da7ebc7298b63271d7449f Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sun, 12 Jan 2020 17:03:28 +0800 Subject: [PATCH 1132/3170] Rename test to make its intention clearer --- tests/functional/test_uninstall.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/test_uninstall.py b/tests/functional/test_uninstall.py index 15a35649d40..c279c299220 100644 --- a/tests/functional/test_uninstall.py +++ b/tests/functional/test_uninstall.py @@ -202,7 +202,7 @@ def test_uninstall_overlapping_package(script, data): @pytest.mark.parametrize("console_scripts", ["test_ = distutils_install", "test_:test_ = distutils_install"]) -def test_uninstall_entry_point(script, console_scripts): +def test_uninstall_entry_point_colon_in_name(script, console_scripts): """ Test uninstall package with two or more entry points in the same section, whose name contain a colon. From 4def73ca3a07cc8acb842b44faacda697b3f1bbe Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sun, 12 Jan 2020 17:12:06 +0800 Subject: [PATCH 1133/3170] Refactor test to remove unneeded network dep --- tests/functional/test_uninstall.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/functional/test_uninstall.py b/tests/functional/test_uninstall.py index c279c299220..175364ad11c 100644 --- a/tests/functional/test_uninstall.py +++ b/tests/functional/test_uninstall.py @@ -255,15 +255,18 @@ def test_uninstall_gui_scripts(script): assert not script_name.exists() -@pytest.mark.network def test_uninstall_console_scripts(script): """ Test uninstalling a package with more files (console_script entry points, extra directories). """ - args = ['install'] - args.append('discover') - result = script.pip(*args) + pkg_path = create_test_package_with_setup( + script, + name='discover', + version='0.1', + entry_points={'console_scripts': ['discover = discover:main']}, + ) + result = script.pip('install', pkg_path) assert script.bin / 'discover' + script.exe in result.files_created, ( sorted(result.files_created.keys()) ) From 602b0de040a292ef221ae1bfc0829b19142c56c9 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sun, 12 Jan 2020 17:14:24 +0800 Subject: [PATCH 1134/3170] News --- news/7582.trivial | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 news/7582.trivial diff --git a/news/7582.trivial b/news/7582.trivial new file mode 100644 index 00000000000..e69de29bb2d From 8e72502118b5fc89794042bc193605b34c948c6b Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 10 Jan 2020 17:39:47 +0530 Subject: [PATCH 1135/3170] Add test for uppercase script name --- news/3801.trivial | 0 tests/functional/test_uninstall.py | 23 +++++++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 news/3801.trivial diff --git a/news/3801.trivial b/news/3801.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/functional/test_uninstall.py b/tests/functional/test_uninstall.py index 15a35649d40..59ffb65ea4c 100644 --- a/tests/functional/test_uninstall.py +++ b/tests/functional/test_uninstall.py @@ -271,6 +271,29 @@ def test_uninstall_console_scripts(script): assert_all_changes(result, result2, [script.venv / 'build', 'cache']) +def test_uninstall_console_scripts_uppercase_name(script): + """ + Test uninstalling console script with uppercase character. + """ + pkg_path = create_test_package_with_setup( + script, + name='ep_install', + version='0.1', + entry_points={ + "console_scripts": [ + "Test = distutils_install", + ], + }, + ) + script_name = script.bin_path.joinpath('Test' + script.exe) + + script.pip('install', pkg_path) + assert script_name.exists() + + script.pip('uninstall', 'ep_install', '-y') + assert not script_name.exists() + + @pytest.mark.network def test_uninstall_easy_installed_console_scripts(script): """ From 8d92e40c1fc92cbddff885f3654f0dccf3a2f7b0 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 12 Jan 2020 19:44:43 -0500 Subject: [PATCH 1136/3170] Use USERPROFILE in expanduser test In Python 3.8, expanduser on Windows no longer respects HOME, per https://bugs.python.org/issue36264. --- tests/unit/test_compat.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/test_compat.py b/tests/unit/test_compat.py index 634aacc40c2..1f31bc5ce81 100644 --- a/tests/unit/test_compat.py +++ b/tests/unit/test_compat.py @@ -136,4 +136,5 @@ def check_warning(msg, *args, **kwargs): ]) def test_expanduser(home, path, expanded, monkeypatch): monkeypatch.setenv("HOME", home) + monkeypatch.setenv("USERPROFILE", home) assert expanduser(path) == expanded From a5f5d8fa81f238fe6dbf4b65080a9c93c8f92661 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 12 Jan 2020 19:54:47 -0500 Subject: [PATCH 1137/3170] Add Python 3.8 Windows Azure tests to CI docs --- docs/html/development/ci.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/html/development/ci.rst b/docs/html/development/ci.rst index e758705b28a..e68b2b4e648 100644 --- a/docs/html/development/ci.rst +++ b/docs/html/development/ci.rst @@ -99,7 +99,7 @@ Actual testing | | +-------+---------------+-----------------+ | | x86 | CP3.7 | Azure | | | | +-------+---------------+-----------------+ -| | | CP3.8 | | | +| | | CP3.8 | Azure | | | | +-------+---------------+-----------------+ | | | PyPy | | | | | +-------+---------------+-----------------+ @@ -113,7 +113,7 @@ Actual testing | | +-------+---------------+-----------------+ | | x64 | CP3.7 | Azure | Azure | | | +-------+---------------+-----------------+ -| | | CP3.8 | | | +| | | CP3.8 | Azure | Azure | | | +-------+---------------+-----------------+ | | | PyPy | | | | | +-------+---------------+-----------------+ From 2d3f2cdba54420d9ee043d19d9b29e4c805cafa0 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Mon, 13 Jan 2020 17:57:39 +0800 Subject: [PATCH 1138/3170] Delete a file to let --force-reinstall fix it --- news/7587.trivial | 0 tests/functional/test_install_force_reinstall.py | 7 ++++++- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 news/7587.trivial diff --git a/news/7587.trivial b/news/7587.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/functional/test_install_force_reinstall.py b/tests/functional/test_install_force_reinstall.py index 56c7aee396f..c8b993c11e2 100644 --- a/tests/functional/test_install_force_reinstall.py +++ b/tests/functional/test_install_force_reinstall.py @@ -1,3 +1,4 @@ +import os from tests.lib import assert_all_changes @@ -22,8 +23,12 @@ def check_force_reinstall(script, specifier, expected): result = script.pip_install_local('simplewheel==1.0') check_installed_version(script, 'simplewheel', '1.0') + # Remove an installed file to test whether --force-reinstall fixes it. + script.site_packages_path.joinpath("simplewheel", "__init__.py").unlink() + to_fix = os.path.join(script.site_packages, "simplewheel", "__init__.py") + result2 = script.pip_install_local('--force-reinstall', specifier) - assert result2.files_updated, 'force-reinstall failed' + assert to_fix in result2.files_created, 'force-reinstall failed' check_installed_version(script, 'simplewheel', expected) result3 = script.pip('uninstall', 'simplewheel', '-y') From 2d3e8f72e61ce57459d02afa499feb429f991011 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 14 Jan 2020 00:30:27 +0800 Subject: [PATCH 1139/3170] Detect all registered VCS and choose inner-most --- src/pip/_internal/vcs/git.py | 19 ++++++++------ src/pip/_internal/vcs/mercurial.py | 15 ++++++----- src/pip/_internal/vcs/versioncontrol.py | 34 +++++++++++++++++-------- 3 files changed, 42 insertions(+), 26 deletions(-) diff --git a/src/pip/_internal/vcs/git.py b/src/pip/_internal/vcs/git.py index 7483303a94b..426a56367ff 100644 --- a/src/pip/_internal/vcs/git.py +++ b/src/pip/_internal/vcs/git.py @@ -11,7 +11,7 @@ from pip._vendor.six.moves.urllib import parse as urllib_parse from pip._vendor.six.moves.urllib import request as urllib_request -from pip._internal.exceptions import BadCommand +from pip._internal.exceptions import BadCommand, InstallationError from pip._internal.utils.misc import display_path, hide_url from pip._internal.utils.subprocess import make_command from pip._internal.utils.temp_dir import TempDirectory @@ -370,20 +370,23 @@ def update_submodules(cls, location): ) @classmethod - def controls_location(cls, location): - if super(Git, cls).controls_location(location): - return True + def get_repository_root(cls, location): + loc = super(Git, cls).get_repository_root(location) + if loc: + return loc try: - r = cls.run_command(['rev-parse'], + r = cls.run_command(['rev-parse', '--show-toplevel'], cwd=location, show_stdout=False, - on_returncode='ignore', + on_returncode='raise', log_failed_cmd=False) - return not r except BadCommand: logger.debug("could not determine if %s is under git control " "because git is not available", location) - return False + return None + except InstallationError: + return None + return r vcs.register(Git) diff --git a/src/pip/_internal/vcs/mercurial.py b/src/pip/_internal/vcs/mercurial.py index d9b58cfe9a4..a03f0580221 100644 --- a/src/pip/_internal/vcs/mercurial.py +++ b/src/pip/_internal/vcs/mercurial.py @@ -137,19 +137,20 @@ def get_subdirectory(cls, location): return find_path_to_setup_from_repo_root(location, repo_root) @classmethod - def controls_location(cls, location): - if super(Mercurial, cls).controls_location(location): - return True + def get_repository_root(cls, location): + loc = super(Mercurial, cls).get_repository_root(location) + if loc: + return loc try: - cls.run_command( - ['identify'], + r = cls.run_command( + ['root'], cwd=location, show_stdout=False, on_returncode='raise', log_failed_cmd=False) - return True except (BadCommand, InstallationError): - return False + return None + return r vcs.register(Mercurial) diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index 7cfd568829f..8a2d341ca79 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -232,12 +232,20 @@ def get_backend_for_dir(self, location): Return a VersionControl object if a repository of that type is found at the given directory. """ + candidates = {} for vcs_backend in self._registry.values(): - if vcs_backend.controls_location(location): - logger.debug('Determine that %s uses VCS: %s', - location, vcs_backend.name) - return vcs_backend - return None + root = vcs_backend.get_repository_root(location) + if not root: + continue + logger.debug('Determine that %s uses VCS: %s', + location, vcs_backend.name) + candidates[root] = vcs_backend + + # Choose the VCS in the inner-most directory (i.e. path to root + # is longest). + if not candidates: + return None + return candidates[max(candidates, key=len)] def get_backend_for_scheme(self, scheme): # type: (str) -> Optional[VersionControl] @@ -687,14 +695,18 @@ def is_repository_directory(cls, path): return os.path.exists(os.path.join(path, cls.dirname)) @classmethod - def controls_location(cls, location): - # type: (str) -> bool + def get_repository_root(cls, location): + # type: (str) -> Optional[str] """ - Check if a location is controlled by the vcs. + Return the "root" (top-level) directory controlled by the vcs, + or ``None`` if the directory is not in any. + It is meant to be overridden to implement smarter detection mechanisms for specific vcs. - This can do more than is_repository_directory() alone. For example, - the Git override checks that Git is actually available. + This can do more than is_repository_directory() alone. For + example, the Git override checks that Git is actually available. """ - return cls.is_repository_directory(location) + if cls.is_repository_directory(location): + return location + return None From cdd7821d924d05b67823aa3078d9928f3833d8b9 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 14 Jan 2020 15:55:06 +0800 Subject: [PATCH 1140/3170] Fix incorrect quoting Link.url Cherry-picked manually from atugushev:fix-issue-6446. Co-Authored-By: Albert Tugushev <albert@tugushev.ru> --- news/6446.bugfix | 1 + src/pip/_internal/index/collector.py | 78 +++++++++++++++++------- src/pip/_internal/utils/misc.py | 16 ++++- tests/unit/test_collector.py | 88 +++++++++++++++++++++++++++- 4 files changed, 160 insertions(+), 23 deletions(-) create mode 100644 news/6446.bugfix diff --git a/news/6446.bugfix b/news/6446.bugfix new file mode 100644 index 00000000000..ce8713d9acf --- /dev/null +++ b/news/6446.bugfix @@ -0,0 +1 @@ +Fix ``_clean_link()`` unquotes quoted ``/`` character. diff --git a/src/pip/_internal/index/collector.py b/src/pip/_internal/index/collector.py index 8330793171a..beb47908006 100644 --- a/src/pip/_internal/index/collector.py +++ b/src/pip/_internal/index/collector.py @@ -7,6 +7,7 @@ import logging import mimetypes import os +import re from collections import OrderedDict from pip._vendor import html5lib, requests @@ -17,7 +18,7 @@ from pip._internal.models.link import Link from pip._internal.utils.filetypes import ARCHIVE_EXTENSIONS -from pip._internal.utils.misc import redact_auth_from_url +from pip._internal.utils.misc import pairwise, redact_auth_from_url from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.urls import path_to_url, url_to_path from pip._internal.vcs import is_url, vcs @@ -186,29 +187,66 @@ def _determine_base_url(document, page_url): return page_url +def _clean_url_path_part(part): + """ + Clean a "part" of a URL path (i.e. after splitting on "@" characters). + """ + # We unquote prior to quoting to make sure nothing is double quoted. + return urllib_parse.quote(urllib_parse.unquote(part)) + + +def _clean_file_url_path(part): + """ + Clean the first part of a URL path that corresponds to a local + filesystem path (i.e. the first part after splitting on "@" characters). + """ + # We unquote prior to quoting to make sure nothing is double quoted. + # Also, on Windows the path part might contain a drive letter which + # should not be quoted. On Linux where drive letters do not + # exist, the colon should be quoted. We rely on urllib.request + # to do the right thing here. + return urllib_request.pathname2url(urllib_request.url2pathname(part)) + + +# percent-encoded: / +_reserved_chars_re = re.compile('(@|%2F)', re.IGNORECASE) + + +def _clean_url_path(path, is_local_path): + """ + Clean the path portion of a URL. + """ + if is_local_path: + clean_func = _clean_file_url_path + else: + clean_func = _clean_url_path_part + + # Split on the reserved characters prior to cleaning so that + # revision strings in VCS URLs are properly preserved. + parts = _reserved_chars_re.split(path) + + cleaned_parts = [] + for to_clean, reserved in pairwise(itertools.chain(parts, [''])): + cleaned_parts.append(clean_func(to_clean)) + # Normalize %xx escapes (e.g. %2f -> %2F) + cleaned_parts.append(reserved.upper()) + + return ''.join(cleaned_parts) + + def _clean_link(url): # type: (str) -> str - """Makes sure a link is fully encoded. That is, if a ' ' shows up in - the link, it will be rewritten to %20 (while not over-quoting - % or other characters).""" + """ + Make sure a link is fully quoted. + For example, if ' ' occurs in the URL, it will be replaced with "%20", + and without double-quoting other characters. + """ # Split the URL into parts according to the general structure - # `scheme://netloc/path;parameters?query#fragment`. Note that the - # `netloc` can be empty and the URI will then refer to a local - # filesystem path. + # `scheme://netloc/path;parameters?query#fragment`. result = urllib_parse.urlparse(url) - # In both cases below we unquote prior to quoting to make sure - # nothing is double quoted. - if result.netloc == "": - # On Windows the path part might contain a drive letter which - # should not be quoted. On Linux where drive letters do not - # exist, the colon should be quoted. We rely on urllib.request - # to do the right thing here. - path = urllib_request.pathname2url( - urllib_request.url2pathname(result.path)) - else: - # In addition to the `/` character we protect `@` so that - # revision strings in VCS URLs are properly parsed. - path = urllib_parse.quote(urllib_parse.unquote(result.path), safe="/@") + # If the netloc is empty, then the URL refers to a local filesystem path. + is_local_path = not result.netloc + path = _clean_url_path(result.path, is_local_path=is_local_path) return urllib_parse.urlunparse(result._replace(path=path)) diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 4a581601991..eef59002115 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -22,7 +22,7 @@ # why we ignore the type on this import. from pip._vendor.retrying import retry # type: ignore from pip._vendor.six import PY2, text_type -from pip._vendor.six.moves import input +from pip._vendor.six.moves import input, zip_longest from pip._vendor.six.moves.urllib import parse as urllib_parse from pip._vendor.six.moves.urllib.parse import unquote as urllib_unquote @@ -52,7 +52,7 @@ if MYPY_CHECK_RUNNING: from typing import ( - Any, AnyStr, Container, Iterable, List, Optional, Text, + Any, AnyStr, Container, Iterable, Iterator, List, Optional, Text, Tuple, Union, ) from pip._vendor.pkg_resources import Distribution @@ -884,3 +884,15 @@ def is_wheel_installed(): return False return True + + +def pairwise(iterable): + # type: (Iterable[Any]) -> Iterator[Tuple[Any, Any]] + """ + Return paired elements. + + For example: + s -> (s0, s1), (s2, s3), (s4, s5), ... + """ + iterable = iter(iterable) + return zip_longest(iterable, iterable) diff --git a/tests/unit/test_collector.py b/tests/unit/test_collector.py index e266ea16342..1872b046ac6 100644 --- a/tests/unit/test_collector.py +++ b/tests/unit/test_collector.py @@ -12,6 +12,7 @@ from pip._internal.index.collector import ( HTMLPage, _clean_link, + _clean_url_path, _determine_base_url, _get_html_page, _get_html_response, @@ -191,6 +192,69 @@ def test_determine_base_url(html, url, expected): assert _determine_base_url(document, url) == expected +@pytest.mark.parametrize( + ('path', 'expected'), + [ + # Test a character that needs quoting. + ('a b', 'a%20b'), + # Test an unquoted "@". + ('a @ b', 'a%20@%20b'), + # Test multiple unquoted "@". + ('a @ @ b', 'a%20@%20@%20b'), + # Test a quoted "@". + ('a %40 b', 'a%20%40%20b'), + # Test a quoted "@" before an unquoted "@". + ('a %40b@ c', 'a%20%40b@%20c'), + # Test a quoted "@" after an unquoted "@". + ('a @b%40 c', 'a%20@b%40%20c'), + # Test alternating quoted and unquoted "@". + ('a %40@b %40@c %40', 'a%20%40@b%20%40@c%20%40'), + # Test an unquoted "/". + ('a / b', 'a%20/%20b'), + # Test multiple unquoted "/". + ('a / / b', 'a%20/%20/%20b'), + # Test a quoted "/". + ('a %2F b', 'a%20%2F%20b'), + # Test a quoted "/" before an unquoted "/". + ('a %2Fb/ c', 'a%20%2Fb/%20c'), + # Test a quoted "/" after an unquoted "/". + ('a /b%2F c', 'a%20/b%2F%20c'), + # Test alternating quoted and unquoted "/". + ('a %2F/b %2F/c %2F', 'a%20%2F/b%20%2F/c%20%2F'), + # Test normalizing non-reserved quoted characters "[" and "]" + ('a %5b %5d b', 'a%20%5B%20%5D%20b'), + # Test normalizing a reserved quoted "/" + ('a %2f b', 'a%20%2F%20b'), + ] +) +@pytest.mark.parametrize('is_local_path', [True, False]) +def test_clean_url_path(path, expected, is_local_path): + assert _clean_url_path(path, is_local_path=is_local_path) == expected + + +@pytest.mark.parametrize( + ('path', 'expected'), + [ + # Test a VCS path with a Windows drive letter and revision. + pytest.param( + '/T:/with space/repo.git@1.0', + '///T:/with%20space/repo.git@1.0', + marks=pytest.mark.skipif("sys.platform != 'win32'"), + ), + # Test a VCS path with a Windows drive letter and revision, + # running on non-windows platform. + pytest.param( + '/T:/with space/repo.git@1.0', + '/T%3A/with%20space/repo.git@1.0', + marks=pytest.mark.skipif("sys.platform == 'win32'"), + ), + ] +) +def test_clean_url_path_with_local_path(path, expected): + actual = _clean_url_path(path, is_local_path=True) + assert actual == expected + + @pytest.mark.parametrize( ("url", "clean_url"), [ @@ -218,9 +282,18 @@ def test_determine_base_url(html, url, expected): # not. The `:` should be quoted. ("https://localhost.localdomain/T:/path/", "https://localhost.localdomain/T%3A/path/"), + # URL with a quoted "/" in the path portion. + ("https://example.com/access%2Ftoken/path/", + "https://example.com/access%2Ftoken/path/"), # VCS URL containing revision string. ("git+ssh://example.com/path to/repo.git@1.0#egg=my-package-1.0", "git+ssh://example.com/path%20to/repo.git@1.0#egg=my-package-1.0"), + # VCS URL with a quoted "#" in the revision string. + ("git+https://example.com/repo.git@hash%23symbol#egg=my-package-1.0", + "git+https://example.com/repo.git@hash%23symbol#egg=my-package-1.0"), + # VCS URL with a quoted "@" in the revision string. + ("git+https://example.com/repo.git@at%40 space#egg=my-package-1.0", + "git+https://example.com/repo.git@at%40%20space#egg=my-package-1.0"), # URL with Windows drive letter. The `:` after the drive # letter should not be quoted. The trailing `/` should be # removed. @@ -236,10 +309,23 @@ def test_determine_base_url(html, url, expected): "file:///T%3A/path/with%20spaces/", marks=pytest.mark.skipif("sys.platform == 'win32'"), ), + # Test a VCS URL with a Windows drive letter and revision. + pytest.param( + "git+file:///T:/with space/repo.git@1.0#egg=my-package-1.0", + "git+file:///T:/with%20space/repo.git@1.0#egg=my-package-1.0", + marks=pytest.mark.skipif("sys.platform != 'win32'"), + ), + # Test a VCS URL with a Windows drive letter and revision, + # running on non-windows platform. + pytest.param( + "git+file:///T:/with space/repo.git@1.0#egg=my-package-1.0", + "git+file:/T%3A/with%20space/repo.git@1.0#egg=my-package-1.0", + marks=pytest.mark.skipif("sys.platform == 'win32'"), + ), ] ) def test_clean_link(url, clean_url): - assert(_clean_link(url) == clean_url) + assert _clean_link(url) == clean_url @pytest.mark.parametrize('anchor_html, expected', [ From 5fb0957219bd4dfb4101b87a73680bf2bbe2c6c1 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 14 Jan 2020 16:24:45 +0800 Subject: [PATCH 1141/3170] Add required type hints --- src/pip/_internal/index/collector.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pip/_internal/index/collector.py b/src/pip/_internal/index/collector.py index beb47908006..42bf8309fcf 100644 --- a/src/pip/_internal/index/collector.py +++ b/src/pip/_internal/index/collector.py @@ -188,6 +188,7 @@ def _determine_base_url(document, page_url): def _clean_url_path_part(part): + # type: (str) -> str """ Clean a "part" of a URL path (i.e. after splitting on "@" characters). """ @@ -196,6 +197,7 @@ def _clean_url_path_part(part): def _clean_file_url_path(part): + # type: (str) -> str """ Clean the first part of a URL path that corresponds to a local filesystem path (i.e. the first part after splitting on "@" characters). @@ -213,6 +215,7 @@ def _clean_file_url_path(part): def _clean_url_path(path, is_local_path): + # type: (str, bool) -> str """ Clean the path portion of a URL. """ From e0781159c6597796ee74d64bc02c7e8e9471b901 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 14 Jan 2020 16:40:48 +0800 Subject: [PATCH 1142/3170] Strip the repo root output --- src/pip/_internal/vcs/git.py | 2 +- src/pip/_internal/vcs/mercurial.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/vcs/git.py b/src/pip/_internal/vcs/git.py index 426a56367ff..3f534c030bb 100644 --- a/src/pip/_internal/vcs/git.py +++ b/src/pip/_internal/vcs/git.py @@ -386,7 +386,7 @@ def get_repository_root(cls, location): return None except InstallationError: return None - return r + return r.strip() vcs.register(Git) diff --git a/src/pip/_internal/vcs/mercurial.py b/src/pip/_internal/vcs/mercurial.py index a03f0580221..e1e97bf7bec 100644 --- a/src/pip/_internal/vcs/mercurial.py +++ b/src/pip/_internal/vcs/mercurial.py @@ -150,7 +150,7 @@ def get_repository_root(cls, location): log_failed_cmd=False) except (BadCommand, InstallationError): return None - return r + return r.strip() vcs.register(Mercurial) From e5c43ed6afcd9f05ff0bc51f560b3f2c6dd34045 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 14 Jan 2020 16:45:28 +0800 Subject: [PATCH 1143/3170] Add tests for get_repository_root --- tests/functional/test_vcs_git.py | 12 ++++++++++++ tests/functional/test_vcs_mercurial.py | 14 ++++++++++++++ 2 files changed, 26 insertions(+) create mode 100644 tests/functional/test_vcs_mercurial.py diff --git a/tests/functional/test_vcs_git.py b/tests/functional/test_vcs_git.py index de8b4c14b58..de1534a130d 100644 --- a/tests/functional/test_vcs_git.py +++ b/tests/functional/test_vcs_git.py @@ -238,3 +238,15 @@ def test_is_immutable_rev_checkout(script): assert not Git().is_immutable_rev_checkout( "git+https://g.c/o/r@master", version_pkg_path ) + + +def test_get_repository_root(script): + version_pkg_path = _create_test_package(script) + tests_path = version_pkg_path.joinpath("tests") + tests_path.mkdir() + + root1 = Git.get_repository_root(version_pkg_path) + assert root1 == version_pkg_path + + root2 = Git.get_repository_root(version_pkg_path.joinpath("tests")) + assert root2 == version_pkg_path diff --git a/tests/functional/test_vcs_mercurial.py b/tests/functional/test_vcs_mercurial.py new file mode 100644 index 00000000000..07925e4fcf5 --- /dev/null +++ b/tests/functional/test_vcs_mercurial.py @@ -0,0 +1,14 @@ +from pip._internal.vcs.mercurial import Mercurial +from tests.lib import _create_test_package + + +def test_get_repository_root(script): + version_pkg_path = _create_test_package(script, vcs="hg") + tests_path = version_pkg_path.joinpath("tests") + tests_path.mkdir() + + root1 = Mercurial.get_repository_root(version_pkg_path) + assert root1 == version_pkg_path + + root2 = Mercurial.get_repository_root(version_pkg_path.joinpath("tests")) + assert root2 == version_pkg_path From 284352a42c6d4d05b36ebc6f200dd76c0065baec Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 14 Jan 2020 17:54:54 +0800 Subject: [PATCH 1144/3170] Add test to verify freeze output --- tests/functional/test_freeze.py | 34 +++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/tests/functional/test_freeze.py b/tests/functional/test_freeze.py index 2e35de3e686..349c4af0805 100644 --- a/tests/functional/test_freeze.py +++ b/tests/functional/test_freeze.py @@ -10,6 +10,7 @@ _create_test_package, _create_test_package_with_srcdir, _git_commit, + _vcs_add, need_bzr, need_mercurial, need_svn, @@ -495,6 +496,39 @@ def test_freeze_bazaar_clone(script, tmpdir): _check_output(result.stdout, expected) +@need_mercurial +@pytest.mark.git +@pytest.mark.parametrize( + "outer_vcs, inner_vcs", + [("hg", "git"), ("git", "hg")], +) +def test_freeze_nested_vcs(script, outer_vcs, inner_vcs): + """Test VCS can be correctly freezed when resides inside another VCS repo. + """ + # Create Python package. + pkg_path = _create_test_package(script, vcs=inner_vcs) + + # Create outer repo to clone into. + root_path = script.scratch_path.joinpath("test_freeze_nested_vcs") + root_path.mkdir() + root_path.joinpath(".hgignore").write_text("src") + root_path.joinpath(".gitignore").write_text("src") + _vcs_add(script, root_path, outer_vcs) + + # Clone Python package into inner directory and install it. + src_path = root_path.joinpath("src") + src_path.mkdir() + script.run(inner_vcs, "clone", pkg_path, src_path, expect_stderr=True) + script.pip("install", "-e", src_path, expect_stderr=True) + + # Check the freeze output recognizes the inner VCS. + result = script.pip("freeze", expect_stderr=True) + _check_output( + result.stdout, + "...-e {}+...#egg=version_pkg\n...".format(inner_vcs), + ) + + # used by the test_freeze_with_requirement_* tests below _freeze_req_opts = textwrap.dedent("""\ # Unchanged requirements below this line From d301cbeb4e6f248ed137a9d1a6b6f39558231cc3 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 14 Jan 2020 18:00:49 +0800 Subject: [PATCH 1145/3170] Add marker to Mercurial test --- tests/functional/test_vcs_mercurial.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/functional/test_vcs_mercurial.py b/tests/functional/test_vcs_mercurial.py index 07925e4fcf5..38c66d5b0a5 100644 --- a/tests/functional/test_vcs_mercurial.py +++ b/tests/functional/test_vcs_mercurial.py @@ -1,7 +1,8 @@ from pip._internal.vcs.mercurial import Mercurial -from tests.lib import _create_test_package +from tests.lib import _create_test_package, need_mercurial +@need_mercurial def test_get_repository_root(script): version_pkg_path = _create_test_package(script, vcs="hg") tests_path = version_pkg_path.joinpath("tests") From ef6542240ee93d045589ff08468f34c336551cca Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 14 Jan 2020 18:06:10 +0800 Subject: [PATCH 1146/3170] News --- news/3988.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/3988.bugfix diff --git a/news/3988.bugfix b/news/3988.bugfix new file mode 100644 index 00000000000..314bd31fcbb --- /dev/null +++ b/news/3988.bugfix @@ -0,0 +1 @@ +Correctly freeze a VCS editable package when it is nested inside another VCS repository. From 85654a9eb80097c23f554939f1a8e027184944b6 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 14 Jan 2020 18:08:00 +0800 Subject: [PATCH 1147/3170] Minor code formatting and logging --- src/pip/_internal/vcs/git.py | 12 +++++++----- src/pip/_internal/vcs/mercurial.py | 9 +++++++-- 2 files changed, 14 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/vcs/git.py b/src/pip/_internal/vcs/git.py index 3f534c030bb..d66db152640 100644 --- a/src/pip/_internal/vcs/git.py +++ b/src/pip/_internal/vcs/git.py @@ -375,11 +375,13 @@ def get_repository_root(cls, location): if loc: return loc try: - r = cls.run_command(['rev-parse', '--show-toplevel'], - cwd=location, - show_stdout=False, - on_returncode='raise', - log_failed_cmd=False) + r = cls.run_command( + ['rev-parse', '--show-toplevel'], + cwd=location, + show_stdout=False, + on_returncode='raise', + log_failed_cmd=False, + ) except BadCommand: logger.debug("could not determine if %s is under git control " "because git is not available", location) diff --git a/src/pip/_internal/vcs/mercurial.py b/src/pip/_internal/vcs/mercurial.py index e1e97bf7bec..6b2fd00f93a 100644 --- a/src/pip/_internal/vcs/mercurial.py +++ b/src/pip/_internal/vcs/mercurial.py @@ -147,8 +147,13 @@ def get_repository_root(cls, location): cwd=location, show_stdout=False, on_returncode='raise', - log_failed_cmd=False) - except (BadCommand, InstallationError): + log_failed_cmd=False, + ) + except BadCommand: + logger.debug("could not determine if %s is under hg control " + "because hg is not available", location) + return None + except InstallationError: return None return r.strip() From 8b1f4d80c14ce5ff79aa22ec65cb4e35810fcb05 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 14 Jan 2020 21:56:23 +0800 Subject: [PATCH 1148/3170] Modify test to unify site_packages path usages --- tests/functional/test_install_force_reinstall.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/functional/test_install_force_reinstall.py b/tests/functional/test_install_force_reinstall.py index c8b993c11e2..c312ac79fba 100644 --- a/tests/functional/test_install_force_reinstall.py +++ b/tests/functional/test_install_force_reinstall.py @@ -24,13 +24,17 @@ def check_force_reinstall(script, specifier, expected): check_installed_version(script, 'simplewheel', '1.0') # Remove an installed file to test whether --force-reinstall fixes it. - script.site_packages_path.joinpath("simplewheel", "__init__.py").unlink() - to_fix = os.path.join(script.site_packages, "simplewheel", "__init__.py") + to_fix = script.site_packages_path.joinpath("simplewheel", "__init__.py") + to_fix.unlink() result2 = script.pip_install_local('--force-reinstall', specifier) - assert to_fix in result2.files_created, 'force-reinstall failed' check_installed_version(script, 'simplewheel', expected) + # site_packages_path is absolute, but files_created mapping uses + # relative paths as key. + fixed_key = os.path.relpath(to_fix, script.base_path) + assert fixed_key in result2.files_created, 'force-reinstall failed' + result3 = script.pip('uninstall', 'simplewheel', '-y') assert_all_changes(result, result3, [script.venv / 'build', 'cache']) From 62ffc7b16541762609ff5ad3268a6095e223e016 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 14 Jan 2020 22:17:46 +0800 Subject: [PATCH 1149/3170] Clarify why the max(key=len) logic works --- src/pip/_internal/vcs/versioncontrol.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index 8a2d341ca79..1e3e0875a68 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -232,20 +232,24 @@ def get_backend_for_dir(self, location): Return a VersionControl object if a repository of that type is found at the given directory. """ - candidates = {} + vcs_backends = {} for vcs_backend in self._registry.values(): - root = vcs_backend.get_repository_root(location) - if not root: + repo_path = vcs_backend.get_repository_root(location) + if not repo_path: continue logger.debug('Determine that %s uses VCS: %s', location, vcs_backend.name) - candidates[root] = vcs_backend + vcs_backends[repo_path] = vcs_backend - # Choose the VCS in the inner-most directory (i.e. path to root - # is longest). - if not candidates: + if not vcs_backends: return None - return candidates[max(candidates, key=len)] + + # Choose the VCS in the inner-most directory. Since all repository + # roots found here would be either ``location``` or one of its + # parents, the longest path should have the most path components, + # i.e. the backend representing the inner-most repository. + inner_most_repo_path = max(vcs_backends, key=len) + return vcs_backends[inner_most_repo_path] def get_backend_for_scheme(self, scheme): # type: (str) -> Optional[VersionControl] From 9dff8c14406081bb5cc6a8e265c973a09ecb7be1 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Tue, 14 Jan 2020 15:09:14 +0000 Subject: [PATCH 1150/3170] Fix invalid assumption that version file contains just a version number --- tools/automation/release/__init__.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/tools/automation/release/__init__.py b/tools/automation/release/__init__.py index 518ac522360..5f8c8fd3182 100644 --- a/tools/automation/release/__init__.py +++ b/tools/automation/release/__init__.py @@ -86,8 +86,14 @@ def generate_news(session: Session, version: str) -> None: def update_version_file(version: str, filepath: str) -> None: + with open(filepath, "r", encoding="utf-8") as f: + content = list(f) with open(filepath, "w", encoding="utf-8") as f: - f.write('__version__ = "{}"\n'.format(version)) + for line in content: + if line.startswith("__version__ ="): + f.write('__version__ = "{}"\n'.format(version)) + else: + f.write(line) def create_git_tag(session: Session, tag_name: str, *, message: str) -> None: From 9d1e1ed8aec6072b815b1bce1124b192ec72d740 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 15 Jan 2020 16:26:58 +0800 Subject: [PATCH 1151/3170] Whitespace --- src/pip/_internal/index/collector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/index/collector.py b/src/pip/_internal/index/collector.py index 42bf8309fcf..730ba5563bd 100644 --- a/src/pip/_internal/index/collector.py +++ b/src/pip/_internal/index/collector.py @@ -203,7 +203,7 @@ def _clean_file_url_path(part): filesystem path (i.e. the first part after splitting on "@" characters). """ # We unquote prior to quoting to make sure nothing is double quoted. - # Also, on Windows the path part might contain a drive letter which + # Also, on Windows the path part might contain a drive letter which # should not be quoted. On Linux where drive letters do not # exist, the colon should be quoted. We rely on urllib.request # to do the right thing here. From 6e2e74ac29f2979b6ed33b9dcc40b474c4419f1f Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 15 Jan 2020 16:36:24 +0800 Subject: [PATCH 1152/3170] Normalize path returned by VCS on return --- src/pip/_internal/vcs/git.py | 2 +- src/pip/_internal/vcs/mercurial.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/vcs/git.py b/src/pip/_internal/vcs/git.py index d66db152640..e173ec894ca 100644 --- a/src/pip/_internal/vcs/git.py +++ b/src/pip/_internal/vcs/git.py @@ -388,7 +388,7 @@ def get_repository_root(cls, location): return None except InstallationError: return None - return r.strip() + return os.path.normpath(r.rstrip('\r\n')) vcs.register(Git) diff --git a/src/pip/_internal/vcs/mercurial.py b/src/pip/_internal/vcs/mercurial.py index 6b2fd00f93a..75e903cc8a6 100644 --- a/src/pip/_internal/vcs/mercurial.py +++ b/src/pip/_internal/vcs/mercurial.py @@ -155,7 +155,7 @@ def get_repository_root(cls, location): return None except InstallationError: return None - return r.strip() + return os.path.normpath(r.rstrip('\r\n')) vcs.register(Mercurial) From 53eba71ef4e9f20d43fc84a56654da16d24f5891 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Wed, 15 Jan 2020 09:10:27 +0000 Subject: [PATCH 1153/3170] Add an assertion to check the version got modified --- tools/automation/release/__init__.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tools/automation/release/__init__.py b/tools/automation/release/__init__.py index 5f8c8fd3182..c70d6003b75 100644 --- a/tools/automation/release/__init__.py +++ b/tools/automation/release/__init__.py @@ -88,13 +88,19 @@ def generate_news(session: Session, version: str) -> None: def update_version_file(version: str, filepath: str) -> None: with open(filepath, "r", encoding="utf-8") as f: content = list(f) + + file_modified = False with open(filepath, "w", encoding="utf-8") as f: for line in content: if line.startswith("__version__ ="): f.write('__version__ = "{}"\n'.format(version)) + file_modified = True else: f.write(line) + assert file_modified, \ + "Version file {} did not get modified".format(filepath) + def create_git_tag(session: Session, tag_name: str, *, message: str) -> None: session.run( From 21e5c0eb11d9de805875da32038b0fb2215c69e5 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Wed, 15 Jan 2020 09:40:16 +0000 Subject: [PATCH 1154/3170] Fix lint error --- tools/automation/release/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/automation/release/__init__.py b/tools/automation/release/__init__.py index c70d6003b75..e983f4cbb80 100644 --- a/tools/automation/release/__init__.py +++ b/tools/automation/release/__init__.py @@ -99,7 +99,7 @@ def update_version_file(version: str, filepath: str) -> None: f.write(line) assert file_modified, \ - "Version file {} did not get modified".format(filepath) + "Version file {} did not get modified".format(filepath) def create_git_tag(session: Session, tag_name: str, *, message: str) -> None: From 2dc061d9601ffebe840a41a5ec83364d27914594 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 15 Jan 2020 17:50:41 +0800 Subject: [PATCH 1155/3170] Normailze case in tests --- tests/functional/test_vcs_git.py | 4 ++-- tests/functional/test_vcs_mercurial.py | 6 ++++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/tests/functional/test_vcs_git.py b/tests/functional/test_vcs_git.py index de1534a130d..cee0a3cfeeb 100644 --- a/tests/functional/test_vcs_git.py +++ b/tests/functional/test_vcs_git.py @@ -246,7 +246,7 @@ def test_get_repository_root(script): tests_path.mkdir() root1 = Git.get_repository_root(version_pkg_path) - assert root1 == version_pkg_path + assert os.path.normcase(root1) == os.path.normcase(version_pkg_path) root2 = Git.get_repository_root(version_pkg_path.joinpath("tests")) - assert root2 == version_pkg_path + assert os.path.normcase(root2) == os.path.normcase(version_pkg_path) diff --git a/tests/functional/test_vcs_mercurial.py b/tests/functional/test_vcs_mercurial.py index 38c66d5b0a5..841c4d8218e 100644 --- a/tests/functional/test_vcs_mercurial.py +++ b/tests/functional/test_vcs_mercurial.py @@ -1,3 +1,5 @@ +import os + from pip._internal.vcs.mercurial import Mercurial from tests.lib import _create_test_package, need_mercurial @@ -9,7 +11,7 @@ def test_get_repository_root(script): tests_path.mkdir() root1 = Mercurial.get_repository_root(version_pkg_path) - assert root1 == version_pkg_path + assert os.path.normcase(root1) == os.path.normcase(version_pkg_path) root2 = Mercurial.get_repository_root(version_pkg_path.joinpath("tests")) - assert root2 == version_pkg_path + assert os.path.normcase(root2) == os.path.normcase(version_pkg_path) From 3288d902e63911bdc100c44de6797204f0a745df Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Wed, 15 Jan 2020 15:14:22 +0000 Subject: [PATCH 1156/3170] Touch command may not be available on Windows --- tests/functional/test_freeze.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/test_freeze.py b/tests/functional/test_freeze.py index 2e35de3e686..a62dd271dca 100644 --- a/tests/functional/test_freeze.py +++ b/tests/functional/test_freeze.py @@ -267,7 +267,7 @@ def test_freeze_git_clone(script, tmpdir): # Create a new commit to ensure that the commit has only one branch # or tag name associated to it (to avoid the non-determinism reported # in issue #1867). - script.run('touch', 'newfile', cwd=repo_dir) + (repo_dir / 'newfile').touch() script.run('git', 'add', 'newfile', cwd=repo_dir) _git_commit(script, repo_dir, message='...') result = script.pip('freeze', expect_stderr=True) From f404eaa6ef76e87922cec308c65a428e1314915e Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Wed, 15 Jan 2020 15:32:39 +0000 Subject: [PATCH 1157/3170] Add HTTP proxy variables to tox environment passthrough --- news/85a9deeb-db71-4c14-a57a-6d440995130d.trivial | 0 tox.ini | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 news/85a9deeb-db71-4c14-a57a-6d440995130d.trivial diff --git a/news/85a9deeb-db71-4c14-a57a-6d440995130d.trivial b/news/85a9deeb-db71-4c14-a57a-6d440995130d.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tox.ini b/tox.ini index 6be97232065..993d3f517b9 100644 --- a/tox.ini +++ b/tox.ini @@ -11,7 +11,7 @@ pip = python {toxinidir}/tools/tox_pip.py [testenv] # Remove USERNAME once we drop PY2. -passenv = CI GIT_SSL_CAINFO USERNAME +passenv = CI GIT_SSL_CAINFO USERNAME HTTP_PROXY HTTPS_PROXY NO_PROXY setenv = # This is required in order to get UTF-8 output inside of the subprocesses # that our tests use. From ffa6d9d3d0c7950878387739907a36f21b3b6c1b Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 17 Jan 2020 17:19:43 +0800 Subject: [PATCH 1158/3170] Delay TempDirectory.delete resolution to cleanup --- src/pip/_internal/utils/temp_dir.py | 20 ++++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/utils/temp_dir.py b/src/pip/_internal/utils/temp_dir.py index 2d342236567..65e41bc70e2 100644 --- a/src/pip/_internal/utils/temp_dir.py +++ b/src/pip/_internal/utils/temp_dir.py @@ -107,13 +107,10 @@ def __init__( ): 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 unless - # the tempdir_registry says otherwise. - delete = True - if _tempdir_registry: - delete = _tempdir_registry.get_delete(kind) + # If we were given an explicit directory, resolve delete option now. + # Otherwise we wait until cleanup and see what tempdir_registry says. + if path is not None and delete is None: + delete = False if path is None: path = self._create(kind) @@ -145,7 +142,14 @@ def __enter__(self): def __exit__(self, exc, value, tb): # type: (Any, Any, Any) -> None - if self.delete: + if self.delete is not None: + delete = self.delete + elif _tempdir_registry: + delete = _tempdir_registry.get_delete(self.kind) + else: + delete = True + + if delete: self.cleanup() def _create(self, kind): From c55eee4188638a861a8a12413e78e4ae763718fd Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sun, 19 Jan 2020 02:45:20 +0800 Subject: [PATCH 1159/3170] Explicitly set newline when rewriting for release (#7600) --- tools/automation/release/__init__.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tools/automation/release/__init__.py b/tools/automation/release/__init__.py index e983f4cbb80..0e26806c4e9 100644 --- a/tools/automation/release/__init__.py +++ b/tools/automation/release/__init__.py @@ -3,7 +3,6 @@ These are written according to the order they are called in. """ -import io import os import subprocess from typing import List, Optional, Set @@ -70,7 +69,7 @@ def generate_authors(filename: str) -> None: authors = get_author_list() # Write our authors to the AUTHORS file - with io.open(filename, "w", encoding="utf-8") as fp: + with open(filename, "w", encoding="utf-8", newline="\n") as fp: fp.write(u"\n".join(authors)) fp.write(u"\n") @@ -90,7 +89,7 @@ def update_version_file(version: str, filepath: str) -> None: content = list(f) file_modified = False - with open(filepath, "w", encoding="utf-8") as f: + with open(filepath, "w", encoding="utf-8", newline="\n") as f: for line in content: if line.startswith("__version__ ="): f.write('__version__ = "{}"\n'.format(version)) From 35377c995c1f56193031a8b8540b92e261b26771 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sun, 19 Jan 2020 18:22:14 +0700 Subject: [PATCH 1160/3170] Add test for tempdir registry lazy eval --- tests/unit/test_utils_temp_dir.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/unit/test_utils_temp_dir.py b/tests/unit/test_utils_temp_dir.py index c395a7861c0..06055084e0c 100644 --- a/tests/unit/test_utils_temp_dir.py +++ b/tests/unit/test_utils_temp_dir.py @@ -234,3 +234,17 @@ def test_tempdir_registry(kind, delete, exists): path = d.path assert os.path.exists(path) assert os.path.exists(path) == exists + + +@pytest.mark.parametrize("should_delete", [True, False]) +def test_tempdir_registry_lazy(should_delete): + """ + Test the registry entry can be updated after a temp dir is created, + to change whether a kind should be deleted or not. + """ + with tempdir_registry() as registry: + with TempDirectory(delete=None, kind="test-for-lazy") as d: + path = d.path + registry.set_delete("test-for-lazy", should_delete) + assert os.path.exists(path) + assert os.path.exists(path) == (not should_delete) From 0e1e0ef5662cae83fa5596bb5bc8386f395dd756 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul=20=28ACSONE=29?= <stephane.bidoul@acsone.eu> Date: Sun, 12 Jan 2020 14:20:58 +0100 Subject: [PATCH 1161/3170] _should_cache does not depend on check_binary_allowed _should_cache is only called by _get_cache_dir. In pip install mode, _get_cache_dir is never called when check_binary_allowed returns False because in that case should_build_for_install_command has returned False before and the build was skipped. In pip wheel mode, check_binary_allowed always returns True (because it is not passed to the build function). So _should_cache can use _always_true for check_binary_allowed. *Alternative* Alternatively, we could have passed check_binary_allowed to build in pip wheel mode. The only difference is that wheels built locally from *legacy* packages would then not be cached, when pip wheel is used with --no-binary. --- src/pip/_internal/commands/install.py | 1 - src/pip/_internal/wheel_builder.py | 18 ++---------- tests/unit/test_wheel_builder.py | 42 +++++++-------------------- 3 files changed, 13 insertions(+), 48 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 82f94213e7b..02a187c8aa2 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -358,7 +358,6 @@ def run(self, options, args): wheel_cache=wheel_cache, build_options=[], global_options=[], - check_binary_allowed=check_binary_allowed, ) # If we're using PEP 517, we cannot do a direct install diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index 5940e4ad9ec..7c7820d4f26 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -107,7 +107,6 @@ def should_build_for_install_command( def _should_cache( req, # type: InstallRequirement - check_binary_allowed, # type: BinaryAllowedPredicate ): # type: (...) -> Optional[bool] """ @@ -116,7 +115,7 @@ def _should_cache( has determined a wheel needs to be built. """ if not should_build_for_install_command( - req, check_binary_allowed=check_binary_allowed + req, check_binary_allowed=_always_true ): # never cache if pip install would not have built # (editable mode, etc) @@ -144,17 +143,13 @@ def _should_cache( def _get_cache_dir( req, # type: InstallRequirement wheel_cache, # type: WheelCache - check_binary_allowed, # type: BinaryAllowedPredicate ): # type: (...) -> str """Return the persistent or temporary cache directory where the built wheel need to be stored. """ cache_available = bool(wheel_cache.cache_dir) - if ( - cache_available and - _should_cache(req, check_binary_allowed) - ): + if cache_available and _should_cache(req): cache_dir = wheel_cache.get_path_for_link(req.link) else: cache_dir = wheel_cache.get_ephem_path_for_link(req.link) @@ -263,7 +258,6 @@ def build( wheel_cache, # type: WheelCache build_options, # type: List[str] global_options, # type: List[str] - check_binary_allowed=None, # type: Optional[BinaryAllowedPredicate] ): # type: (...) -> BuildResult """Build wheels. @@ -271,10 +265,6 @@ def build( :return: The list of InstallRequirement that succeeded to build and the list of InstallRequirement that failed to build. """ - if check_binary_allowed is None: - # Binaries allowed by default. - check_binary_allowed = _always_true - if not requirements: return [], [] @@ -287,9 +277,7 @@ def build( with indent_log(): build_successes, build_failures = [], [] for req in requirements: - cache_dir = _get_cache_dir( - req, wheel_cache, check_binary_allowed - ) + cache_dir = _get_cache_dir(req, wheel_cache) wheel_file = _build_one( req, cache_dir, build_options, global_options ) diff --git a/tests/unit/test_wheel_builder.py b/tests/unit/test_wheel_builder.py index 7d70e8dcb49..dcaa1e793ad 100644 --- a/tests/unit/test_wheel_builder.py +++ b/tests/unit/test_wheel_builder.py @@ -113,35 +113,17 @@ def test_should_build_legacy_wheel_installed(is_wheel_installed): @pytest.mark.parametrize( - "req, disallow_binaries, expected", + "req, expected", [ - (ReqMock(editable=True), False, False), - (ReqMock(source_dir=None), False, False), - (ReqMock(link=Link("git+https://g.c/org/repo")), False, False), - (ReqMock(link=Link("https://g.c/dist.tgz")), False, False), - (ReqMock(link=Link("https://g.c/dist-2.0.4.tgz")), False, True), - (ReqMock(editable=True), True, False), - (ReqMock(source_dir=None), True, False), - (ReqMock(link=Link("git+https://g.c/org/repo")), True, False), - (ReqMock(link=Link("https://g.c/dist.tgz")), True, False), - (ReqMock(link=Link("https://g.c/dist-2.0.4.tgz")), True, False), + (ReqMock(editable=True), False), + (ReqMock(source_dir=None), False), + (ReqMock(link=Link("git+https://g.c/org/repo")), False), + (ReqMock(link=Link("https://g.c/dist.tgz")), False), + (ReqMock(link=Link("https://g.c/dist-2.0.4.tgz")), True), ], ) -def test_should_cache( - req, disallow_binaries, expected -): - def check_binary_allowed(req): - return not disallow_binaries - - should_cache = wheel_builder._should_cache( - req, check_binary_allowed - ) - if not wheel_builder.should_build_for_install_command( - req, check_binary_allowed=check_binary_allowed - ): - # never cache if pip install would not have built) - assert not should_cache - assert should_cache is expected +def test_should_cache(req, expected): + assert wheel_builder._should_cache(req) is expected def test_should_cache_git_sha(script, tmpdir): @@ -153,16 +135,12 @@ def test_should_cache_git_sha(script, tmpdir): # a link referencing a sha should be cached url = "git+https://g.c/o/r@" + commit + "#egg=mypkg" req = ReqMock(link=Link(url), source_dir=repo_path) - assert wheel_builder._should_cache( - req, check_binary_allowed=lambda r: True, - ) + assert wheel_builder._should_cache(req) # a link not referencing a sha should not be cached url = "git+https://g.c/o/r@master#egg=mypkg" req = ReqMock(link=Link(url), source_dir=repo_path) - assert not wheel_builder._should_cache( - req, check_binary_allowed=lambda r: True, - ) + assert not wheel_builder._should_cache(req) def test_format_command_result__INFO(caplog): From ac42c232ce4f9c7f5eb13525c2103683b075f20e Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 21 Jan 2020 12:48:59 +0530 Subject: [PATCH 1162/3170] Update CacheControl to 0.12.6 --- src/pip/_vendor/cachecontrol/__init__.py | 2 +- src/pip/_vendor/cachecontrol/adapter.py | 2 +- src/pip/_vendor/cachecontrol/controller.py | 11 ++++++++++- src/pip/_vendor/cachecontrol/serialize.py | 4 +++- src/pip/_vendor/cachecontrol/wrapper.py | 2 +- src/pip/_vendor/vendor.txt | 2 +- 6 files changed, 17 insertions(+), 6 deletions(-) diff --git a/src/pip/_vendor/cachecontrol/__init__.py b/src/pip/_vendor/cachecontrol/__init__.py index 8fdee66ffe6..a1bbbbe3bff 100644 --- a/src/pip/_vendor/cachecontrol/__init__.py +++ b/src/pip/_vendor/cachecontrol/__init__.py @@ -4,7 +4,7 @@ """ __author__ = "Eric Larson" __email__ = "eric@ionrock.org" -__version__ = "0.12.5" +__version__ = "0.12.6" from .wrapper import CacheControl from .adapter import CacheControlAdapter diff --git a/src/pip/_vendor/cachecontrol/adapter.py b/src/pip/_vendor/cachecontrol/adapter.py index 780eb2883b9..815650e81fe 100644 --- a/src/pip/_vendor/cachecontrol/adapter.py +++ b/src/pip/_vendor/cachecontrol/adapter.py @@ -24,7 +24,7 @@ def __init__( **kw ): super(CacheControlAdapter, self).__init__(*args, **kw) - self.cache = cache or DictCache() + self.cache = DictCache() if cache is None else cache self.heuristic = heuristic self.cacheable_methods = cacheable_methods or ("GET",) diff --git a/src/pip/_vendor/cachecontrol/controller.py b/src/pip/_vendor/cachecontrol/controller.py index 1b2b943cb9b..dafe55ca70c 100644 --- a/src/pip/_vendor/cachecontrol/controller.py +++ b/src/pip/_vendor/cachecontrol/controller.py @@ -34,7 +34,7 @@ class CacheController(object): def __init__( self, cache=None, cache_etags=True, serializer=None, status_codes=None ): - self.cache = cache or DictCache() + self.cache = DictCache() if cache is None else cache self.cache_etags = cache_etags self.serializer = serializer or Serializer() self.cacheable_status_codes = status_codes or (200, 203, 300, 301) @@ -293,6 +293,15 @@ def cache_response(self, request, response, body=None, status_codes=None): if no_store: return + # https://tools.ietf.org/html/rfc7234#section-4.1: + # A Vary header field-value of "*" always fails to match. + # Storing such a response leads to a deserialization warning + # during cache lookup and is not allowed to ever be served, + # so storing it can be avoided. + if "*" in response_headers.get("vary", ""): + logger.debug('Response header has "Vary: *"') + return + # If we've been given an etag, then keep the response if self.cache_etags and "etag" in response_headers: logger.debug("Caching due to etag") diff --git a/src/pip/_vendor/cachecontrol/serialize.py b/src/pip/_vendor/cachecontrol/serialize.py index ec43ff27a87..3b6ec2de1c1 100644 --- a/src/pip/_vendor/cachecontrol/serialize.py +++ b/src/pip/_vendor/cachecontrol/serialize.py @@ -107,6 +107,8 @@ def prepare_response(self, request, cached): """ # Special case the '*' Vary value as it means we cannot actually # determine if the cached response is suitable for this request. + # This case is also handled in the controller code when creating + # a cache entry, but is left here for backwards compatibility. if "*" in cached.get("vary", {}): return @@ -179,7 +181,7 @@ def _loads_v3(self, request, data): def _loads_v4(self, request, data): try: - cached = msgpack.loads(data, encoding="utf-8") + cached = msgpack.loads(data, raw=False) except ValueError: return diff --git a/src/pip/_vendor/cachecontrol/wrapper.py b/src/pip/_vendor/cachecontrol/wrapper.py index 265bfc8bc1e..d8e6fc6a9e3 100644 --- a/src/pip/_vendor/cachecontrol/wrapper.py +++ b/src/pip/_vendor/cachecontrol/wrapper.py @@ -13,7 +13,7 @@ def CacheControl( cacheable_methods=None, ): - cache = cache or DictCache() + cache = DictCache() if cache is None else cache adapter_class = adapter_class or CacheControlAdapter adapter = adapter_class( cache, diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index d19ef2e51a8..9f19fb519ce 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -1,5 +1,5 @@ appdirs==1.4.3 -CacheControl==0.12.5 +CacheControl==0.12.6 colorama==0.4.1 contextlib2==0.6.0 distlib==0.2.9.post0 From 744d0ebbd4dbfdc8479deefaf2da730ee4dd42a0 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 21 Jan 2020 13:10:01 +0530 Subject: [PATCH 1163/3170] Upgrade colorama to 0.4.3 --- src/pip/_vendor/colorama/__init__.py | 2 +- src/pip/_vendor/vendor.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pip/_vendor/colorama/__init__.py b/src/pip/_vendor/colorama/__init__.py index 2a3bf471423..34c263cc8bb 100644 --- a/src/pip/_vendor/colorama/__init__.py +++ b/src/pip/_vendor/colorama/__init__.py @@ -3,4 +3,4 @@ from .ansi import Fore, Back, Style, Cursor from .ansitowin32 import AnsiToWin32 -__version__ = '0.4.1' +__version__ = '0.4.3' diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 9f19fb519ce..21a2f5b48b1 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -1,6 +1,6 @@ appdirs==1.4.3 CacheControl==0.12.6 -colorama==0.4.1 +colorama==0.4.3 contextlib2==0.6.0 distlib==0.2.9.post0 distro==1.4.0 From dd07badf20c1a7e351d87816e3394a62539a7018 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 21 Jan 2020 13:11:38 +0530 Subject: [PATCH 1164/3170] Upgrade distlib to 0.3.0 --- src/pip/_vendor/distlib/__init__.py | 2 +- .../_vendor/distlib/_backport/sysconfig.py | 8 +++--- src/pip/_vendor/distlib/database.py | 2 +- src/pip/_vendor/distlib/locators.py | 21 ++++++++++----- src/pip/_vendor/distlib/scripts.py | 25 +++++++++++++----- src/pip/_vendor/distlib/t32.exe | Bin 92672 -> 96768 bytes src/pip/_vendor/distlib/t64.exe | Bin 102912 -> 105984 bytes src/pip/_vendor/distlib/util.py | 5 ++-- src/pip/_vendor/distlib/w32.exe | Bin 89088 -> 90112 bytes src/pip/_vendor/distlib/w64.exe | Bin 99840 -> 99840 bytes src/pip/_vendor/distlib/wheel.py | 2 +- src/pip/_vendor/vendor.txt | 2 +- 12 files changed, 43 insertions(+), 24 deletions(-) diff --git a/src/pip/_vendor/distlib/__init__.py b/src/pip/_vendor/distlib/__init__.py index a2d70d475a3..e19aebdc4cc 100644 --- a/src/pip/_vendor/distlib/__init__.py +++ b/src/pip/_vendor/distlib/__init__.py @@ -6,7 +6,7 @@ # import logging -__version__ = '0.2.9.post0' +__version__ = '0.3.0' class DistlibException(Exception): pass diff --git a/src/pip/_vendor/distlib/_backport/sysconfig.py b/src/pip/_vendor/distlib/_backport/sysconfig.py index 1df3aba144c..b470a373c86 100644 --- a/src/pip/_vendor/distlib/_backport/sysconfig.py +++ b/src/pip/_vendor/distlib/_backport/sysconfig.py @@ -119,11 +119,9 @@ def _replacer(matchobj): #_expand_globals(_SCHEMES) - # FIXME don't rely on sys.version here, its format is an implementation detail - # of CPython, use sys.version_info or sys.hexversion -_PY_VERSION = sys.version.split()[0] -_PY_VERSION_SHORT = sys.version[:3] -_PY_VERSION_SHORT_NO_DOT = _PY_VERSION[0] + _PY_VERSION[2] +_PY_VERSION = '%s.%s.%s' % sys.version_info[:3] +_PY_VERSION_SHORT = '%s.%s' % sys.version_info[:2] +_PY_VERSION_SHORT_NO_DOT = '%s%s' % sys.version_info[:2] _PREFIX = os.path.normpath(sys.prefix) _EXEC_PREFIX = os.path.normpath(sys.exec_prefix) _CONFIG_VARS = None diff --git a/src/pip/_vendor/distlib/database.py b/src/pip/_vendor/distlib/database.py index b13cdac92b2..c16c0c8d9ed 100644 --- a/src/pip/_vendor/distlib/database.py +++ b/src/pip/_vendor/distlib/database.py @@ -567,7 +567,7 @@ def __init__(self, path, metadata=None, env=None): p = os.path.join(path, 'top_level.txt') if os.path.exists(p): with open(p, 'rb') as f: - data = f.read() + data = f.read().decode('utf-8') self.modules = data.splitlines() def __repr__(self): diff --git a/src/pip/_vendor/distlib/locators.py b/src/pip/_vendor/distlib/locators.py index a7ed9469d8e..12a1d06351e 100644 --- a/src/pip/_vendor/distlib/locators.py +++ b/src/pip/_vendor/distlib/locators.py @@ -304,18 +304,25 @@ def same_project(name1, name2): def _get_digest(self, info): """ - Get a digest from a dictionary by looking at keys of the form - 'algo_digest'. + Get a digest from a dictionary by looking at a "digests" dictionary + or keys of the form 'algo_digest'. Returns a 2-tuple (algo, digest) if found, else None. Currently looks only for SHA256, then MD5. """ result = None - for algo in ('sha256', 'md5'): - key = '%s_digest' % algo - if key in info: - result = (algo, info[key]) - break + if 'digests' in info: + digests = info['digests'] + for algo in ('sha256', 'md5'): + if algo in digests: + result = (algo, digests[algo]) + break + if not result: + for algo in ('sha256', 'md5'): + key = '%s_digest' % algo + if key in info: + result = (algo, info[key]) + break return result def _update_version_data(self, result, info): diff --git a/src/pip/_vendor/distlib/scripts.py b/src/pip/_vendor/distlib/scripts.py index 5965e241d60..51859741867 100644 --- a/src/pip/_vendor/distlib/scripts.py +++ b/src/pip/_vendor/distlib/scripts.py @@ -172,8 +172,16 @@ def _get_shebang(self, encoding, post_interp=b'', options=None): if sys.platform.startswith('java'): # pragma: no cover executable = self._fix_jython_executable(executable) - # Normalise case for Windows - executable = os.path.normcase(executable) + + # Normalise case for Windows - COMMENTED OUT + # executable = os.path.normcase(executable) + # N.B. The normalising operation above has been commented out: See + # issue #124. Although paths in Windows are generally case-insensitive, + # they aren't always. For example, a path containing a ẞ (which is a + # LATIN CAPITAL LETTER SHARP S - U+1E9E) is normcased to ß (which is a + # LATIN SMALL LETTER SHARP S' - U+00DF). The two are not considered by + # Windows as equivalent in path names. + # If the user didn't specify an executable, it may be necessary to # cater for executable paths with spaces (not uncommon on Windows) if enquote: @@ -285,9 +293,10 @@ def _make_script(self, entry, filenames, options=None): if '' in self.variants: scriptnames.add(name) if 'X' in self.variants: - scriptnames.add('%s%s' % (name, sys.version[0])) + scriptnames.add('%s%s' % (name, sys.version_info[0])) if 'X.Y' in self.variants: - scriptnames.add('%s-%s' % (name, sys.version[:3])) + scriptnames.add('%s-%s.%s' % (name, sys.version_info[0], + sys.version_info[1])) if options and options.get('gui', False): ext = 'pyw' else: @@ -367,8 +376,12 @@ def _get_launcher(self, kind): # Issue 31: don't hardcode an absolute package name, but # determine it relative to the current package distlib_package = __name__.rsplit('.', 1)[0] - result = finder(distlib_package).find(name).bytes - return result + resource = finder(distlib_package).find(name) + if not resource: + msg = ('Unable to find resource %s in package %s' % (name, + distlib_package)) + raise ValueError(msg) + return resource.bytes # Public API follows diff --git a/src/pip/_vendor/distlib/t32.exe b/src/pip/_vendor/distlib/t32.exe index 5d5bce1f4a65f0bea9636a5a825646c520903df4..8932a18e4596952373a38c60b81b7116d4ef9ee8 100644 GIT binary patch delta 28024 zcmd_Te_YhX_CNmG1!UF5MFm9Umja5v_b2SKu&fD)2DXZVl%l56MwX_#W?gadE_B^i zN*gOHEeS1eKg}PhrG{dnrDbJBX+<}04R@{ZYDz`!_jzU)tb5<@&-eR%Jih;ZvGbZU zXU@!Pew;aT=FGgdnzLawSHjk3%Q`mXOmwY#{nUyRXP!&zhTERWN;`@8$Hl$V+C`Y1 z)*?bh+Up39?9im0MtI^(W7^|_t_<=UgJG@+r{B52PGw5}@n_4Iq#{|E<Qnn&Y$T<K z<Tgnz>nrOcNuMIRRgk0*Ey74Z2OA|z>U~BgNuiRI*~O;IhWArP$)s%Jn<taD*)|Lv zsBDr+YkMQ}B}t0kFmy<aO(m18#L@lNAxX1j8-~WQ4w)h&?kvbuIK2?X{_!svrH)&+ zaK2-{B=sAE6y%h~0lr}4<Vk&!Q4z|ikg+m(VsBJHnvQ5JTP=_4_gz1rsEXjI+Woxz zR@nx%;?dAvE`=mD)DD{<>E7|=ce$$-k~6<PKhoxRD(maIuKE3Te~2X6sV+8tN4zAt zk3|I%Z1(Iv6C_@x0<Fnv<A?fEqOwZnkCr;DlDD`^cbeagzB}1G(|d8v#ZVOS-)~|= zLI=%xuC%+{-~4`?eI=DEb(ecI`hO@lq`O>iv7$`puP^AbsSbxVjhi6ZrtvlC2Gpdk z$uZ<W(>V4>=<w`KD6&4kOXs)GGlR!d-Q}t7;CG@F-AQwcYU2_6R&Y$?Nq)b-N?zu7 zcJWI?B#FW+>{4iI|6_reJrsRdB`e_{u;F29`RoWbD=c~B{6JzU+U~r<?}?&}SG*nk z)9Ze}w?o%tzNUJt$$qfU1lACi=Cb3h?qg%T#TP<~+p46I#oX`2e_igcBz`SUy!35) zO&PsMhQd(A6GNH~x1omDT1##G=imMQN^jr&T_CuRrB<31&M?oM4&MxKJ`d}{JMWk4 znyMs>VHbZsP@pJl??6fNcC!3l@q;OArMbT&ywKdd{6}AiC%=QZlM>hiz2aT}giw^1 zqyG|0?e?4rf#EOpI~Aq=Xa&X=DUJf)y=8W(FLmn`8@krUQ$;Ua@pe*4Pl_>!GGBL2 zuFvnZy&VZo>Q-O!ABsc`Zxrc_BF3^(UNISM#`u|!JJ0$;bXN<gyk3|?<#WBqD}*$a zdOQ5lQK*S~xKt&tl2u8iRF=&jT}YzWQFqm;^haAeSVefOD}Mr2xHol3bUc!EP1ZRi zlI{zgYc8Jek^f_!+SFFWy!ncfQpi+UnI|tb-XCqG5?kx@QX_3uOA)NJRrN0MN2@_b zu}b%<)X1gIFcj)TuiInMP8%QC<@c{ym%0{{PGUcY4{(9mZWKvAh<H_AMJY<sm)W>> z4z)GP<48^S8nVGwb=Ci=4T%f+6^vv{XzDDHZ4a`QmskB9NVN0M0*MZh_(UM_N+5AD ze>0G{UL-z%#Asxd**_U*m0I#rXG;(+e{^04ds;DsYG!V6=1^~0rLqGw(WljKOy#Kg z^l3H>F$U5for=Wz+SE2sZGJ~;tbHfSF5}Ps>i4^kMH0oEmzrAAXh&!Jqm#i68al5C zSd~A3$5I*8je%%7qV~`cO6sqpD5ty}^vHnnIECSvhEee56kf7QZ>1|Iu(7>UWn0+v z-eczJk)?#s$581`)-_@7me_3|P2(9TrT#Cms(s<^W3iPOoPkve8=ufWD9qhsRAS#K zUK%bcuXMM_k%QR#5&MAt?5o~`ru>3t(-d$j+{fjWre4nPRr`8TdRs8Tq0lu|dU_$X zjE~1~mt1SLpF+7q{8iGdHQ&>;!KAnGM_6v3LE%yaHS=CJr_UfoA@CA@S2$bMXNcM^ z64bv?KPvGGs>g78kEQ!PvEn^GQMr+ktfkK=RkylE5BZ}*J09(u78!-{@TJx-mHYZr z?l+mC@37%eb){7pk1^=#N_P&azF`i9XYct=zuy<pQb*zKW>(!dE{-%O{}OR;tFEc; z9O<k5qG}23^}cGw8G(OyjkWiUAEZZdq}Fuj%$JF=OC;`M@yayUD_CfR>In2(qVs!H z-)%{;r}smLY($4tVNroN%I5}}w5mHppO9<Resi;vi!4L!v%0fniYx*CqW^)vzOEHD zx0QOfuqNfeTh}3<jbFLy_v@NWBFVm=ilX883e+mqZ6#%P8V3G7#2qRW<Gezw4E1#v zF<pd~xAE%0$`hbLc@$k@V<UzI$8serjYyK)F0uO}R=e`#&>KA+cSXGa*w0u?_MSz> z{G!ACd#RFT_H-3SrL=@=pwZmN)Fpg42LDw1i;y961g*qQ`Q_&Jk%O?#DD9DvsH8O! zqVD%pU(hvq=P1lyqam2GQT*csNotW|$WY)b64=UqLtT$U-FxyCNRHr7isV>KS>H)4 zc$FrzGnv<R=aH(e*XMWG?DR&yJBjL|^ff?>Mxt(y<L#K&p8N|f`Lv+;IVuayc!7U~ z{LN6ce0~@Py4mZ;OfB^~70|c?`G~$qt%|AesQG*GEJc+nzZTCXtH!y|5;e6X3h8Ks zQ=#*VH4*iU<}bpk5Y3QyF&P*q{JxtpaMeoDPRG|CEE3#^fhtEUO8J0f>T9x5u%0MO zxojvk`2uRa5h?Ct@&8%n3-P(ZI^RKs1nNwdj$<qug85-c){@qm2eF1A#Fp~Gs0OVt zq!MlX<Pi3ERbt{DFuA-PWLI2u#+vfG90PePRy@qtKKz3r?4PRSDJzYrN`b;#xY*Or zTWI%84fXajN7zSCkQ@WN5y>O55EU+UhVu~_;Opfr2sMYLrz0`Oi%dP~3jW|Yc1vVI zL;{Krc1%+odoFU>fU#MUq}ta(0|%XD;|W=8cz;W}4fU5wo%f^H@*`EXYiO**kPPBF z2t=@qPY1Jg{nxmtv63^Eud`sR68WduFz@`x!M`(!se8^5>TSll3!Cmn-z=ml-&yB( z4zE&Hn8NlLQR|2jz8+<elKi_boEHy9qtv`W1m2dp6~3qvzJwUK7PJ;giI?b_T(`|> z-Y+FjkeW}OI@QorN-8R<0%e5nmoSTbz0Aj}!@b8FDWxhD`qoOdYTQ;ptGF|<DzwyN zpfuj{r-ZPoxLFm=x{nS}(Y~+-;`b=!?jy<WqoHelsezQQ`d8KU3SIw?4n%^b%zN}m zXP7s%t|p%i9+=_+Y1R94bxjq`e8EIXs>xYJ-Q)=4;}Q4`R2+}Q6#aw9i&mIBmJM5T zeIAOxFV6e^=o3eN33DHboW>jSBx%i5rBs93`}(+#C&P5IqZ2FU1mq7^hPyMWI>y}; zY2%JO7{K0q<(k4k2Jqx7-A&2RIh5x{7STHMk!49a;*GIu<|+Lyzkl=*v_+IO9d$3C z(;UdVxP$eNN*<W<XBw5qilW}N+)j&bu<U8mZkXmJrQS=rb7khsP%+ieE`bR>6j=Kh z89IZe<B)tdBYL`i6I$s#fz{Si+-ZK#If#=sYLP}^r1{U#o?fZ_Rp6###q$1Q8RK6^ zvoE64v|A|SF>|vczC}893d7#t#@oq?0jYP3^yQbB$xRAxai_Pn?i_U4+GwVU8Ju(l zO~KnR=lYk3db|<Xba*>_N^Xm0cg7^fjKX4ZGC0XEMna#}7kBVdwk2k|{HbtuDMlUf zIPA;doG?YPaf3$4idpHPCGy!ttYy%xy}!5_eTc<y3L6z0C+8DcL9DfR5xrQ+M-60; z#HNSaNSWCNvcs`yDd47*(*t@6T?$m!#Am|XhN_I^i&(G08L|eJGdOM3Qp&lV7|wa- zkV3Ckn13Th*9=DEjWW*lXX^$J8PJTiq1-d4>*^$>)w!gM=S^V;1}7?DW%<GmtQ^h0 z9z2xnTVL4zmC!O*t*JGRDE>Swo?!bX19cZ)@SEmbaHf>;6dDt#k4PK3Pg%zMq4Ln3 zFuJ#kweG)k=g-J5)cs1TNT64z?e{}pRGRV?&QM*`N#=?hICw0U*L|TAd<nD@sFKy^ zEAaG3FYRD2#ic~F!<GqFXk8>b7Z)GVbc3=Wl7$X=RTp-HvV*pMC;aV;AZ?|vt_rEr z<sN6JX>MqBh-YpnHb$lV+nMa2Lvm#KEG0fuK1ao7$7jW(E3o=`Lymmc>uR1lw8n8i zb_bByP98Ip?TXjQznQ_lkDsbuPowY=ED6$>3UohIOEg~$La>@DptKOB(^40Hl`Tq0 z>30$|(HBLqA<Ee;#mOq!jU3w|VCt_fj#YgDBv?u{(Y6=Q9a;?~TVC$&^gD+6i!ZDU z_vW{)jrH}f&u_Dp@OemNX;a6l5^v-@0%ghOPaQY?8z6zw+PyTZK1AH_jE7kHol)lB zR*do*Czh1j(`u{|-++}Gv%}qqO&8w*Mz?X8a}Iw7n8%3SVQd*OvTvp)lr0OUj^K7m zjrAqgHQk?}J6Y-JN7*C!G>}`QVf4x?eMMi)t$^kh>pfmhof?I?1u3E)`Z@6CIqdO- z@sS^39?PJ=7DY<srTi8H`y@f#yMvUFjqjPp`X$En+DZNYB^#A!lFKf$MTsL_<UJ6b zYrWhfXzrXEs9mYlYgi0rOv2{7uMDg2IFn%^2#!1WE%PO*k&i=Rcg~}w`2ODf^SOjL zZw_RtyXu%AmU=T)^EsrIdpmvUHvR;m=9U$4-d>gN?3FAcski){3KpA`((epa?l?B- zRpFdxu*pf&BYv%rr23_@DtRgI^&{JnbW79>q&Jt*AMXp>2&}~h);D?MWVe=T+{sf> zwNhS)`rG&nR1W48>KEqz!0#L2Er^E->uclBU&M%2#!KbyN~J_==A34jN*z>zB*v4C zy+@<$=2D*1!A1^Eo%&G6ANuAZnk~lyWf?1UGXLA??uGn`bdoj{6=jd?kD~rcTcxWe z!&daBvu`Qf6j+Q_V*59=$dwNJ5Z$Gyisy4+=auu*X+c6HFA{{Aiy$D_f&0gZ9#&Kh z;Txdf%l*-g4y)9-nO4T97GfMO&^UauAjqZQcM%t#ihe-D6;-|YtwgAjaSheaeB1#8 zrf?F`8fZ8h?RePyhS>=GhFN&>ly<Q#^5nOfoEMzEZ2W_fkjJZd_{MqjFM1W;-k##~ z@c3+<%U&H86Tg>|Kfxnt0%Dr+<>BZ-erh3W8y4llV(y8!LeJ<9dL~??r-2rEPtFBA zE7wIwsws#F6+uFqAcmb6L0YQ_#-2s6`H?UMSiJdCV;7>{{Ln@TacI6qCDH5JU@f$S z6(UWBe+`uAYV1NEwS;xx**jEfY6-gnOomA&R$otDEAlj~g)!l&I}g+!y_UMziZak` zkD-&xG4Y~nL9`#l#@HWS1Js>E3=+mr#_uH_JCz;qkHCUh1(x7?*vow}vLV*IyEEl2 zS@<y&z+bImZ>Eh_oC03T?`~&5qzxL;Dk4(@eGl2_>pmX2<~<44nQvHCxc#C?9^cMJ z51$ls_D+)QC`dES&SY<Xls~L(99uhl=y)}W-mDN>>BQy*wOc+OwZm$ryXtYanS~jr zs(+W7zpa3^4j=AXG7b#b)7Nec5FrWrouz#JzaW|A!lv_v?!0PMx*bYeDgR!O3w#^A zo!A`t7E~E|42hFBLRf#Hufv#_YTt_0q&sWCE?S2kscRB6=RmxG|Nc*ukN6;TeEd$| z5WFP7N!AzWY~u+1ZEjen&EDz@Xonk9F1Q%Nj%$p9tOix%nv3aZ@gHm{Y}m@&zxf^E zB|IBipxnKk<U*<{s;|2M1WqaL%wj3&ubXJ7t6*dY#ur;!BxCq!gfR$_u)USRf+h(y zR@^mLcltKI^&c!^WWo?|1;@G~sH6>4Er0aJ4wgSMI{`A~7&yWG%h+X?>I;8x|I+`y zepL~(Q0vgs8;7xHM#d)6-U~14wZAh3#yPC7W}eke>Plf}M~)c<i#_;fUvIKX3;hNR zlLXICq-SwzWIbSX8+;OIK1&`|A{)jWqlOMpegmh%R3)}JAs^_JM&8xNUKn-n08d+h zTq?*}{QfpJYIMp_QuvN!`zF#VA4BuFkA*_GRNk48?&<#MO$F?p(Qzo*GdgX+<*)x- z#J69wrqM}AY#W`{Z>PDZf>fV@V~SMIRZ7wUnr+^R3cjb3-7{vKJnU=s%$S8PKVC%+ z4c$rIM>@a%JM3!FkSb}qy)@ej*V5q^BuOlQGrfyb71O7|RTaheid9cJ=cq#J*$0>M zXY{1<&!9lz2U*33eG4ttlx*%0E0~H85OFKw^RUUH`7$9zx>lf6qH4~mC1v&5b+H&a zcdqmnIp3VgCH$onm;<Y3&Cn;OVOvqD^#4ffMl88M-IWTdyn9!HsbO{Wa<9bRHjW&k zd>X33-hL&{wdUf=&_4>M@N#;cN*MayYFf$b>vv$?hTfs)p*DJMIVhf;;(10quZU-> zc<vX^vv_g~MuK0*e!^3?8FAAVD#ba9`#=)bi%a4U12=9VKc=IPw{9mw_q?UQlbv^^ zk0hBdJ3dnF>!<t3vxOtcvxWRHo-N1dS$_e~O7nZF+SkEZCBH4?vilHmNuIq;2vqBy z1igMjTcx{m)_p3~z9Xu`HR%$%uuVlyS=HfM*U>`6tHO_ThTiI_+lox)qpH6pBbz_9 zZt!F)2H#yzjYA7IA@J0R*42qt*NOJlbqYGQxBdIxv=2Yx=E;sJZXV*uaC3ztZFI9E z8ZlMf2_(DgiC@uvP~a|#gtqVONszT~1HMKu)j%ZMA49bF7Gi6E2xz7Imr>3!?v~KN zSa@Aso<mfbI={+O)l*97wJk(!p9P9nRQ9f>KB|2u>+=+6JR;tnL(JP<)HbP|d`_a| z;eRffQmN$rqGZtz?O~!!!v9z%#oPY{gMymhIoVO>?zB4!+?|UZusiQl)qQ}ZK&vER z?M-;V{kPcV929anNQX=Z#;v^(seyiqY=5E$Pj2@DhcD3Gnc+;Tbg!1{ow4rYp~39v ztzh<rl%-P_;L!$nwo?!KT^K<j8tqoWv(Tlg%@o+3s@ib^S)!^<6Npn)8!wQRs@j19 znWd`jL&$cL6Z9(y^R)WCBylZuXYnK*!dF2*w|@icLiEH!)g$AvK&Wb)@N{>Us2&*t zF2`y&pWuib-RyFdLpl_rn^kpRhk@Zpa*L2NDk&h}fqt;M4^a<G^;^0S2#<(oO9xPo za7?P|A4Tykb;Q$)q~;=`*MwxMQDaCRh=G!l+YbWoPGiPhO4k<3hBd?)_7?RKMBu8( zn*0YDqj#xluR&K}!Rg!3hK-%4j)?B<G$SSNUW&j${BR`N@ldZ?EY`h%1=NS?jizZf zo=-*y9UsXG3jCtpCYsK3{HxBipF_!RjvoFQHAi=dm45V5Rqa(MQ|g{K!9X7TmQbAh zbk}b$^?Fb%)olarxM^+O354}^JMgr5>vpiAyOa9;^>&ml3~iNX?|wFQcWOA5gq-aD z-RUkWg?haG5*BIqc8W^v-_wG9qn$RsbUv!zeuh{&m#QA=L=IJL8=mgYMXGg&1^o`y z!|gbQb9dh9xYylzk0agPxkOd>9MS?+u4$<wrG+t(9P@*MD|f+eo$1Z&nR@)kd9>uJ zY8RjbgH3fPE4_6jX*gT(DFUdcSBp?bqUu+u1BKeK2HpW3CV2(n+7YRS>iPm0z2V-n zP<M->{bNL&F_rE^7k-=I_ut{16ObM%%Qwv=Bx!Xoj|kTY*=t0~>P+um5|%Z`=YX-4 z-vdLS+*5aqeYhve)#~dLD1$ih9>Yc$A9$6gZauPiCv|!1HUbF_jeqYZL{x`GfA~eI zfq}I7_mV`_i(Dlz{9|DY`#<%c@gAZ%3`g#o@6jB|kr12{I3T3acKPAFFr)^V%gaU6 zsAaK`*Z_;*NbcdNF^_fj4-NvFGchouO4@rt<N__Baq>KN5U=&6y7_Wd?VXt1B%SMM zi6@o*7BaTf5yz|%VE)r6L!qJ97lXA*J*(1FcM4oy!Rv0ChWPu4d$zRV***zO7$)RU z)fH3On1e@>9kJ9U?B@9?as<k4aqOWtrpa2_`8S59kQhBV-Bwn@C(OghA5nX@tfTT< zy}!DDF|F<uQh$&o?@M1W7xN8%(;B1NF~eU^lhiBbZD0Z(m(;!-F{3Z^%;-S+4UOjO zb91HJC^^F2O>#Kh{ykdc|FAQldfbQhvlsVex~Lf>JYKOJjXp$UIQkG#JrFU!e}~eU z>PrkL&8p;p*1U!P8;2o*HQQhA2(9zGzQS<`xV?uq(^J?B^`hf!^+No4Hhk%UURIQs zJH`jlQ0M&NUhx`VT$MUdxVQNhzVSbc|A+gKnBC61yuy;GJGXki_Yn2U4UK__f9K0+ zO1)4iWwx7n*c_NkNA^cYoF`79@p<~s?A!fgRDu$y+FhC8N<0wbqW1oGTi?F&pKWy? zItES{;QqaY@M|A~W4oZ0djlrSKFmQXj?Vwadmp2f$1AKb?}_%i&^j*1-aqJ8+WwQ@ zUvp^BZ&>A|_Wnqu2$lZ_yDFeLa0z2>1?>{v{*aua>tD25rHvM=bR3Y9Yn7G)Rsos; z?;))UNBxjZ+GK9Vq!#C^j~!{GBLnY|z`g<(F6vG?hE%obK4MG9_LkrA6Z^~9)B%Ab z6gnor9zyHR*<)m{k6kWr7{P{*OOmCsf^pfg*n@~$CNOd9IIKaNe?_Q2dTR%JYFx4W ztKqDD+-P~+ToyAvQGR+3%NU;`-!X^XHhx?Dx%a8|@Qm1KPnrG$y5mW*PC6TsN3tR6 zB)RnmmaUF8(OF6cTt0E|Z|n$EMYZO-lnQcqDIb4^c6oH*;+bUk8U*6J0M>0CtX@5) z<aVTc5>m<ZzJA@A*b+u4iMl^}Ckivp>2l=5Bki8Q^mkvj`}*(B0e@t987#hY?(1F7 z0p9!$uQiksnNpL2{fJtVAxmd>XmrtgM6Fb{r6{*PpW_IN+W(p+T8=(CuE~#jGX?D` zz5tK%T_@GpCD*@hO=n5kc;wI47UEf_y?Z!SOjS37^v4N*&lrF7p6I10`%>zqq<^tk z-94^gH}v-w*mD9MVdLSa(Gi{sdx<w^8|vf#>}tVMxvK78XzEqmW<jG>bzdS<g)>CB zh0EQGy0B#mZPb$(OjI-)MW}4dMyP9o|JdCbRvlUCEt(iv=~3pVgKYKT5SKj5IWn}g zI^v<C*!Z<;lZR7EyGY5QuC0!IXkBaqkhN>mM^M^#eladR!#7ZjS5M8So}!v3q^Gml z`r)^TaV_DoMC8+W_x$jMx)-&<A=Ab`1FhMevyL{E&H+`w^HOwKc}o}(@KRJhAk#SU zFhB@C)ZZ){$r233@*N#)v0;*k=-Bs$B)M@MQ)CP)xbI!)j5o-z;)7?Ck>h=E4<hh0 z3^K(!2ikb)4TeW<BL@E)JBizJgRXm*EzL-B4FJXa;3*`&U4zHy7901UB+{8+>=I)B zH<}RR-vNbL<9+ZfVmav;A;%EYCVDfy^RY6B=o^&84}r0~yneN<C0{}I;Tl(GDslJs zhNF&sQAhaPHW5>bX>Jz|B`1a6psyjiyCu?JtZ=rJ@?|HPCNozyirt$TJ8(UT)C+Kv z@K`wnw1Xzz=FHUSH$lS0<d4%O3z8GR!9JROk~z+`9+K7UkKT{qXejo_rGCRw98vhA z4}v0Y=nU~2@*pbYq995SLlZ@hS2Q;)20!`CV_+N<^>F&nxFaaV-$3P2dOvtRjv<pf zLcF1UCp<o-RSNq~s`N`2aGqcEGsHal1QqO5v=q$VsX$I_oOS+5p4CyF)k}{*NS5=& z;}6~pxtQ2eV5hkP`F|YRFwm|QcA+R*jcKyc?7DG?eEtPC#57bvXT*NP;?XS6G}27f zG8F!ys?g@}m6!5P5<Iv}&rmg|hWcUL^)BUMEuiYILJ6GeU^`5yu2!n?Lih*7@vW-1 z6P5B7;Ccf2O7|ZJx3^$SAogM)rVqq62V&WQ*u#OCH4v)_#PR~MWr0{>Ahs|No1H^F z;t0cAN{%yI*5Iff_3002jWRy`IIGSYKORkVM6L0!AB9i}AB8i#jE}<VQ{H{-o`^b^ z^7r2Xi8uU%oy!{P`a}2A8FFu~hwZFun%j(dcNRTjUyJc6^M7hzMPZ|rI`C8bQV|g% z@~Pb}BBE<Qg~JmZlGi|z(nylwh;io-BMznAIm9Hv$Y^q=HjuD2W>8WCNk~hWD7(Z8 zr!TRI%mE>73`&!#_6Qn4qRC!2N0k%5*FeGy(^SZ7uzG=UZVXf}AfJta>WM=6cR0{M zA20Ccka*jhW|EkQw}0k8(+GJ<%uY(WAuk|;dkD$O7}%ZxSz)uYGv#ZKvB$FY@|O46 zd)ZTC)32f&A<1Bmw#On0=|9`SG&w_M9jqj0MBGDP`u(LmWeyEPKk*EKDk|j{Z)F>E zbS_ecj=}cz2>GK05N{OJ2fiYIG?B{r^eH$;SP3VueI6dX0a{30gql%WW`Ff@P)?)F z{j-pEcs3q5Pca`~uJVSX7&+!<OQ`w!vP<rqjr5+m6^<cx!gzY{P0Bl~=*Aw+dXRNZ zIHgFoO9R01*;lZi75o?MXT*$yZ_E@{i|$v|wqisYYk)qKANd>|%)6_VSOB+*fm(}l z0(%hur`}f2oHKRj9Cp8<wt&56G4@3wkwyI9mek&>P~|n%7uH^4Nx8AT*NIr$`b*53 zJ5pY&Wq0Kcm$!e(SnkRZP1miksI2bJm}NgwJ-wmt90RGx(c2{x{xqhwjx5N<`4Eox zWUAV5P^&`tI3hp-A&xaLdyw!WD!b--7oMux3^3Wd{OIDS5a|E4Xi*t|`Xkn09q4)! zk<yaN#-n=FT-@Ax>o=%el$B&Zg)BJvT<{Ss2XwzxscW*)AD@1X#{8^WlB}pl_KH!Z zyakc^(0me5?}^dxSA~4|i~Sf182x#Pjqk?lCH(fzL-vCp9;S-mtabPpk$|$D+x>e( zp|wC9&_ml`k4%<H_NB;C#vi$r&7BY}&+cF=CS+OPp#g^Ru0S3ejH*^heUE7paY&dZ zA^aGMqK<Mzqm%Yggc?Sj#5yLVMeoGDl3)k>4HOan>rpqQ%P(}W88@Yc(*du+k<V&w z8i|wN7jMc-0Hyk_w`&npS*w!yDRiyyJxp_lfv@kxg1$&v^_;JlHv#e46Gz6ip=Rj2 z9Pc&vcS+tBT%EviafqI7f;>AhUcRn_9h#Ve(!ZHFE-IT^(dj0KL!IAYfu?TjU@3WH zh-+HjFshF~`g{j_Aa6+O`oqxMo#jw)BTQlYQTY`~=!c6aq5eUR?lptR4&)gI{cmYu z`KhCJmP4_1q6L2E7*oE|@qZ|)B!7fUc`LD#FbMw0{{uhj6GG-<2bN6dq2OU-q&;~6 z=APp-=wrH&fWwUqs45-ORE5m-RJWOP=(0`q4DU5@USf*h2_rao2BWfN@JN{E;$S<( z4?EBwwJKfk^_-w^G+$8YoaKQx!A$2V;|pTty3g3;g6!VU;s_lF!;9vzhYRKn%|Oh? zKb!~Jwbt?>P2|3%L(wR^mFk)PDN{{K7>^pM>fM-1qU8F}ohGBB!i(z<I2bC$!O&WW zTY0(eqnkW4yXbYdO`1w-Pn;!f5NXZbtJMIMf&*e1?zTBXy!q$(-#;OXM#gDlSWZY1 zDL~kXxD$N!?IsMXJNxH@P~0+SLVf7Z`+<3k;*iO&cDKa(qXVZ)4rPJ+dg`i=p~dD_ zG`mw>f)fhQcggE7@g`8elpdzsWnK$JYoL2|sLT;j=Kr{iPsIfbc=pTP#}#X?w~m4M zo)?^Ry}cl}y@}HqV)K7|BmKLI+k(5Ze1%{C4i)Q<UW`|W^JF`^u0O9oi!p}1gKLE6 zObN+aw!bh1rp)=m!E)cn>{?;Uh(OJqgUWdIKD3#?fo*Ew+7=|k*Vkk5VXeP?ADb|F zVEk~D)?C$xj|}z>4oq;QP+CtfXG<q1%IzI&{p8e$F`^9I3X=E-AF(5o<08UtP<DL8 z_~a7V8!UUu_y{MoT5y9K`w^QvHJNRi5;?%wLv8;X6_O4uIW>CpZ^y6@9fGO}-glV9 z{n+8seTQ=XGceqDu!;K)za3-0P8p(nFPNIfk9^36-aIhkWe|e*8*cfKO}M#4-qFDx zzj>6a`-JP%wECh{#po06Z&V>ihMeqsq`dvsJLzt8kQ&}i%>z{&|4hR@NZdzE!Oql^ zkMk~N`HsKhO4J9~R<=kW!thfj(|N~u=%^i5M+Dy^l7=D4_ic&p+zy!|hGi6~)2E>O z#fgza$t?n>)1nBR0P>Ga5KHJx>}Rv8*uzER<YR7Shl_Nv&_q4AfI^cUQC0Gil4<-C zJL_FMOn#`GjV(@(30~BU=J$Wl)7SAIuxYm#<tcYE?=ADG6y6^`ZFgHz`H{9>A>rk7 zMX4;*c5k2CFxTD5xR3fibJ^mGz`HDoYe4kG@<9j!t>kZFdr8fl3a_U3IMizy^ehgw zYLLMj4qf|Dks4kv8LpRI0V^_pahm<pHoxGZKx`?3pdbtc>dWvR{g8W#BXRF7#FI}q zy7(oySiG&!QXOzqC31_%p(ObzIL+2gi<3XGi0zp6y!=WBvrXTlyn>m4KI<bs35YDQ z$bVYEY$eMFV`@nb{?I_x8}F(Pd)g4KJBiC?-@ePvmlVmP7qC$?64Uj_?8(1^{QB-2 zp=+^sD|Z)P^kb)QhLVQa9Qi6+I^)ehzbRv8iG0s|=AAitFzphm!Yc4d!yx|UiJq?d z=Lz=1%(VEmq}zT28K=eW->y4{@Rt#xuHJBhjVm2AlDayM*9D1HHwuUV>ifvsj9wN! z%<d^ojkN^%QUo8(J>R|(erN$}TNE4jXyB?j_U67p{6}m+f}?h|g`F=QJnDAjX+||l zc-TTxfI_k{v(fx${*qu9lX+?jODaoMJ^)JKeqdq?yQOTfrB5yy7D`_Wgq1D{DR??b z{xPMyI#P1HWpXN)_v32HHLTpD@Luxms*0Hf4Ih|xY-`!j-e@egBJEcfvUkgD?EF*1 z<m-NZ^y#SHYcAHn5Te6e*VA!qK)HG7O)rvT3|km@oPWou#8u~Lei<8&p!7Wc4!fs3 zO%Vn)QT`?_%ROD5fMwmfIStRR%PR)_`H1icP6*j8x5mn6uHJp;t<iG%a5bx$JtQ<1 z<~VzHc4FvN>~h$V*#~7~S@PDw?1?!uWe>9-<~$f7JgRM|fQ{cZhSgLolYcduU8(q5 zp7uTaWUfYzBY4HU3G&!+?B;o+<f-G>ee;s!$>Vl!npY*0UmL|jZ@&*^uDX4@>```P zdo0UZ5G!w*#AYlQB{%%clAa&MUR;nQfBiQ0-hy%R!IRi;3&zNO1#INPaiOy@5!h`D z6XaW-VGk^v=5lWqe!icaR_Vv(Rw>Z|{~q83;A6mG&?m33{_(i_@tGKAG6`_s2g`F% zQ{n1cF|Jof(hw@K!RUni3Ok7Q2_($4Fp~jl<Jq*RW4`0s#PMv%p}13vRr0$eO!a!Y zg_rz$kF~LbBs~i`dL-R<ARy^y4zQ_Dc1!xPAo1@AgYsDf^iPuh@RPB!ME3Qf;Ssmo z;3_*nQmkqQB_PF>)I8Sj>7ne(D}&-DWAS(AE97pAzuc*E=Z-=PbG^-6bBLwAN}jsO zug)C$5c-f$!5SPm%ob8*Ml>i@U+rUWzq)+L&faKa;C$IDyt;Qa^;W%6t3K0v0xN!P zsQex8?xnBA$>r}JVC!DLi1EFDmo9X}P<W1a?phyuItfVL?v<(@Z@i7ag`G2dZ};t? z*@S5JJSu<xbN0?2AwQjag#4KI3i-KXZ!*b`XYXoRs5b%3efL%34a@dzm*?zegZIyt zf4z&{x4$6NhTGrpXD7>-?}8Ki4tbwltmr_p{H6`;o&yU<Mn6KrQa@Rm?Y2s@*I1>8 z0qp?8U#-#vz~2C?k;crGi3`rY+|vd30Y!)SUG3jta?`ShPKx8ZZg9-(;b;a&+1tcy z{(40;fA|KE259>uNTy?(;tu}mdUmjKLBDB5q%UyC*BQ^{FSD_C7;X;SZ&%fA0JHc+ zF^=zlsi!u#0S(lq7U|w%CH&K0!xs0uOwPy^QM~>JtNJDO${i0!E!1M`33Ip0+p7%z zFPk?<-@$IN$H-s*h}~fyDwm4bL-yhQEDuAneLk3eUIQP;4%;=Z5im!v@W2O2R}!)K zgpuSgqI9gk27HZx_u{}ir@Vfp%rOj~67nmsG>{<#vt>dlzWAEC6*5S>H|Jz`b7;MO zHP|KRJAP3Ff2JO(X;hlzPJzFIZa~V=EL~IiZF4KeG{e<|7xR^ZjZkq-^+NYP@V6cK zmV}&b(b6UM)SY8oeew3drX&KyE}F(Ib8o9?#_bj^4W*t|`o8PAy@HFt&b)v=aM&7X z!FFGlIeTBNp`MlDCfIekGybYI4~9yWoKFF3l?-<sCr}>TrgX%Yas%Pe7)5whU6U)Y z=#uh)#=sVJ!$b@b83lh*6XnODCvf8Iz@6mtXoAou&@kdcWTgGX05thB=sj9v8zQtm zdJy)gtahqp-MN(tGq!qS#jG9{<h#9xkB0&MPemgCY~rJLCCDRpu$@oEk6H(gfc6+q zjR~xQWIMs8+u?R3?oc@5{3{gP>t&1Xx@G8B!)SQsz{_bLzJa_mwP<>`Yq^xa+`Rjf zyYghh@8S&OA3_B|p6Ie9J~G6-i6vW!Bs)9DA)8dn*Yw9b>m9vhbhz&PS5@ewGJXUG z4($p(IV8I1r}qLSl=yU|+=dJ_ByqDy;_%HV12=fdBa>T-&Hl`IjB}X~xk>QPgj}wH zz@6}mI`CW%2^>{Na*y~gfrGr!8~I?tMd+X9ZSywv)7|Crfs@&kd%l&AD`exB43+mS zWYd>S84bxrdvAFw_&wF($Sm?~tWAhA)9xe`u9pV7Jc|9imbEO&lOKPVMJ!ECrJJ7~ zaSIfiCEBs#-c`?ieq~8Ov}Z0IE>~P))k_oPneVdAOOwZGA4H>ZPZghL3MtR;^3i9S zgp(YJB1-p5krSlDc(BNO<NK8ES5OoWL{p*t4fhVqfValCsFXfM^+(5|j$UV%&WTHT zQNVQ_0S_*Pks?lC;`pPLmstJ1=~2jf?Ng6|?r&Di#S7hOOY_;udnd|UC$WCZZi?x^ z%=g5)`}z6DzyfB|eOp=OvQshrdXg?Z+nw~-v#fCWNcjvcTfY2e!<S}s{d;g6<4b{| z+yO4gzbBEG0c(+Pi&x@BK=H-JJU;^5g3pP+#4azdqVEI#xENpWwqn0}20g}i;pv5e z+u2?jN~*YxTpYq&&huvc_e>2e=)p4JwC9_s6p>BL_^ZxW_*{@$qzF{Wx7FRr&O2fU z-iy;*Z!hl|>?@A^8s?3949=EtXA%st5zeuZ%gN(SUJUbZ&cXbvEo`APWn2#SC;>eg zSLv>9jdKq4w%{P#pTR#ODFkz~<0zbo5-3sM?%wO{EgSfgo>C(F9(A0;&1@umrDq?m zh?So^&3r4S4Z62GSyej)ofKf1t!E|ojgi0lG+T3DynNI*?AiNb<&od81NSA$??_{x z-!}}k?7D9#o{1}sc$Te9lNWWc)hiQS@el%xN6*IKg{RPvUicyg&HN?ItE?v#ihWdl zo-DK``8p~TYryPoZ-6O@-hKZDQ(%NVq5ZrViJ8CRds6!2>z;%@hxdaIAf=DeyT1c; z#^M9If#O5JfxIsg0^;=AlWg4m^W>(d*!ufZRCxJ~H}5g9qxV;Y2}7GDJ@DjU#Xx-7 zE$(qPV5lUU`yze#O!oDM54_aJ*Z@@q^Zooy?7_bbmpkugul_~X&kAbr+9xso^p_QB zL~=%YrzySDm8NO<6n(!8r8SpKd#eM+jjyjj*h8y^xgeDEae<02d%Rm~lVi262^t&D zI%w<xysRgQ|N0md^;iGhnX1F+O?R6$=qRK+%{@P0fRj9@@@j$7TBfQaFKa2Ef{khU zTl95I8l4r5E9EAlpdyZ8rF<-KEJmfl?I}-qpJl(8G^&d<Xpp@lfD85u!2ig8_B5*l z`|dxmzwih4r~iZ9dz!ufVyb+Sfn9kqL;h<s8}rimshcseagf-3Qs20Oj56VF^85Em z(93YlV?uiVMmW^B+n>JdJNT4DqunQDx|61`c&Jo-2#qs(R_}S3J@}GA{*U|Ep_dlO zVb~0N*&y%BSUCd3)+1<L!CGF<W09{sC2oXcAa|{rM2WwxnjF)o39|VgpJtWb$8N1Q zl8a<xb(P%q_M@w#<xj3)gI3?1qren^_9)}l`-LL7!ZU$Dip%R4U4Y(2!*r+lxqAbx zf}05A1-T-FFWkYNU2TEh{c814db`Y(8`JObf4}`!Co6R&%D-q~D_q0$OQ|#RJ4<+< zmEG^T*z=xOm;Mj$`C=J6<;ru#EyJA0vOPeAO5R6=kz6jqDE=$z=wA`b|4m^A_lamS z|5k*lq6z*L>HK36AH&}hp_(^~P|x2IVLab0LL+}kgxS0iq5GKC?^<Eyn*=S7uNPq< zUn{~QUL!&qUn#;Fe5nX$ak~g-^LZki$7hLfA-9Rp&I?7jm|I1-lp96p;A#=B<mn<@ z#gh@bs1k3~U~cjOM1F&;B_1x&wS;z|@BDA9BlMC$*Ax1kKsOS4PN16zJtNS~guWxt zt%SZQ&~1diCeR&(ZWHK1+<uezlLBud^kIP>Bh)QWI)UXY1$v6myMc;2pP|@Xk+7f8 z83Ju3bh1Ft6KWA?8=*RZULZ7Gpd6@+CkXr^kp~F$ETQ27?I5&k3H87gLN5ullhE%3 z+C}I&fl5#;{ER?D34KSP3PRrmD(bAH*vldzlF)`FE-E{U@W%x?mQb%i;|Z-1XfmNr zfu<5#CD3$2=L&QTp)&-kCUmku^@LgkY9v%A&}>4}1!|R{|9OJI^N2h^poN5n0~M7n zqS!C@P(^Ko{wUBHgnlc~S%kI<bT*-<1v-z=qd>83gDa<JH-Vct<=$TAFe4O~Biv=< z&w>Xh*!b)@TaawLK0tx~w+1PH2~gDLFnf@4r=YCRn+=NvrIeQu<<D<Y)zbA980hA( zykI^}prlB1SRqkd@X{p(NKxjnq99X5fD&sCvjr)?-7P8^Zw{Lgq_hVp$>y+GLCR+V zN~$?*c98N;fMPU<Ee%q32PoNC+5}~WjW+~HR&&_OAk!lON}f4vRgh90pcI<JYJ!xz z1Z72$*-$GerM#Rd!fD1nuU()V4dXnp{C}{P71oWC|Gbtt>ORw9Ma7-7vQqc=D))CP z^Yx0k&aig78n=R5LeN>xs3Yxi_di11zbKZCyu96?`k1tft$r{jG3s?J{i*elyR6cB zgc}iEpNfYK;CP+wdT@jsoaY`)r9k$#_%sQea3fqp`USvwz*)d4z%jr<z@FFHO@Awl zB2KTLN{$7j0*t%ZGk;qcv3WNoT>BjFE85LeYX`?{MIvI9uzn}-vw*H$%(6B~zIivB zyEdHy&sudFap3JX1hWA10CvDqz)C<3fbC{qtu2fSyyRsN_5h9nT6eQ7Pbo`&XreM5 z<`BP73*XPo2Dap(<bqT9%7_0BLbKhv2Xzt4aAP`_+8(cH;~2ym)%4KQ!-z+40+0nB zQ3PkuCl8H@ro_NK7dq}Pq&FFBS&Vm7LT?!U^!ZXN4)CIACrGpHFcsS{#?0n;hc9yA z^fBmq8U)mL;4qceKo4KcVS0B1J$!u*)7u;9(UN}wrO?Fyx_f}n=kRqybsG+$KLyp4 z-{uU*>Fb5<_)r6<UYHh-yvDfKG}a227QUzRM&tnriJ+*d_<Z$XZ}IuNPAa`+vpsqJ z75@D5zAyRlEo|z#aq{F>+3Iy;EZ_f?Hq7)>5NLIy6*aig35|I-qWDRdz)5|X-$2Lq zbfp64^nBe=dEIE%w|<z*|940>oo~d{PmDA-FSFn)mM-2Nq&mhQ@yl)X`u)MN5M^Ln zo+IvOd`7><5?bp2tg#klqbB5|?B4lRh2n5{rYMK58U@CZ@(X5c!^5g#XH^Vq#<?5) z2mtOH+G&*5uYo-5z#Ry2w+ZHO7rQrMkm6;KoB7`w*bnvbE@9K8-mpRO<`VQdKH)Dd zgPWbVKTX*$;yb-UbOnZkJFD$`(7E`boJ){;eBrLDrA5M<a~8v8-+`nO|7UbD{8!Rx z;`WIR7h*P1LL(L8|BT|bB3@F|L$C{itvlX03mBXLk#<|4NMvf90a^n+yu~~)80I66 zNu~(mb>xBr-(H&8^@mLvRIWLnJ4S+tQil1@c=9g-8Qef`pcGXgMd5c|6p6SKV`g_h zk`h)sA7&Wqmpn*$50<@vZW`cX`yVNg$GaX~Z&H4<x|>|Ln&q!|%crkq@2(%73<Wo* zxq&`y^+eEfPBR|_)}Z{qS<NCh#JEfgAtZBfc}VyjaV&-}d-1|@_zQ~5d}<7)tbl&4 z3R}bAPkW?NKhxEt?%%Gkmhyi-hb(ALg8O>giuX$SClu$Ks_AV?^n1Yj#Qz3@7yEs3 zdfRv=Vlm~oz4Z^i<WC7n)$FYeX+yLAkli;y^;&Txe#GdKX^z6tm#VpIIFmh^l6-D2 zt(8jhig$lgz5fDSwOkt=IN->8R6ly}DtHTw{AmQi=39W0itzS=R0tp1^^*E{y0_5y zsLohMsmT)0zLDYUzre$q0Pd@KMw|(%=;96CFnTgdY?Dj*I$Yml{n#LR(<U~8CCJBI zVg)Qwo_>i{vb#e+;7I+94G%3LM7A+GbUYzx8^@FHrF7#6d20uAZ7idpb>lqw%PB1D zvC(o@3cKU6FNs+2cq;|TPrOeVI-VF$v7wuy<kk+Bxha=oD>o%j?1@do<L6-K6Lj`3 z0_suwa{YVrP(yy_3ikP?)QAMZ0B4rO(^jxPPbQ`L%E;WWinOm4Di!HH9>R8KpgZ1o z{^4BVXpr~<!r-f>0R1m_V0?c%D`QPhMnn|fV4dlt!v)t7Py+73h5gArh}Z9+*DnM; z`1*ld-jj_+tO0fYgJW_4|JKn{fd_&5zPNFOhnaaf;0^GGW2@T>TV0vs{SrPa5-)=q zvfvQ)D#$X2ri7ovRY&r+sv+RentcOuN{4&61bYF_@(Z}@#XWchE){zee6fQ(oGK1N zpb1_o73U)vvG!MB_hE-_@jLs`$%xAP-p*V;{S`L*>0#KTxt^XmbPu|lJC=)+k>7*o zZHo{M%BNyE`}yfK#eML%|JQk2<>uS}bYxP05CsH|OcZov^6lmnx$48+iY>A7r0Orl z%zUWsC-9UzhI#AH0IA=59#6jH?=1J(WO?!5+5Be@i=()E<p1o))^GjOIoy%YSxUn_ zy^Wuvme8Y9*l!iX*ujQ<^3@xe^Eq*(vg0{%q~d!{9H|Uy6i0H#M!PI@dMl)S+qM<r zP;Mm4+rCPE@i}&M`)%?^p2Kn6WO?7-tm^r7@;4e-=krP7*KUTSZQsgV3uBo6g~E~V zO(vm)Me@W6+_pcC+xCEAEmr9szyp9Lz`v2kUVEXy)gZ@Q`~W|E(&FsTXWr2*7Ty-` zXTB-oWTa~ve!v1_OlNS|_=0g@V3?u2dfWIU<SF-k@V~k0e#chaI}@J)V4g(TPmrMI zAqTk<=bZP<abcYjUYU4)u`)1YK13Lp9NuQ{#{t>B;i8G-$AWx+@8RP`Kz$#2>DO=S zvxU<O*NnsgpjV|jT%-wge<O1oxA8<YR$T5Q5mrNl<JsjM2@#WSlyplaOZa=@us1*v zKL>Fu<p|go;!D}U2MT;^J)84)%ec}qIFHH$&ZFhi#QVjIyDOo>5u`i2pWkT^SIjhT zro`)MDG2w(_tG`p=8NI87j-w5FI>cyzL`7*R`c=_Fa4s9_Y&Ih$BPSrI%l}E)qaOs zlz8yXL|33hx3U+mwECj?r&yhWy5%dNaQF%r_6>`Od|<KsDAGt>ztF=DzCSs`;`lQ` z9(NB99}d*_NuZ?x_hKPA8{6=ECkpiEX>X`P-1*@&&<$Y556p-=ZsN9E_eHlJ6A@Q| zDilrwF7iCHo;AD`L-*Payfub=#`SqdI{CFj>gb2RZbw<ox^pNF%<~?B%=K7&|LvlK z*OBrDZy?3m6;Y_oxdmu3ev5`&nCGkdaxE1CrKKPZIUn{9Xl+FERI`N#`wv7N>aMD4 z4?uU~LkMr^AuM!j4<;r)K|H#16LEg$UX9-?9U^XD)fdnkQT<~FQ!=U2R}0V|LvRE@ zwQwJ|`rO{?R<E-S)vtJ%s_(8oFFC(E4mP;tN9(Eh#6!t(RSzMt90nYw*Zg`V$S_7^ z;x^%|L-czyj~^P|XUpyA4b&HMc>K`Oa3h4OadAF-<gj%}2mJ2=i)SM+?{ObYkyvtf z4ajHzI-DN;C|tT&{P}jI27fB)sd;R~+lk5tDCaQx^|Zu!Y}(t!v}kU9JD%3Z!*3_a zTQ;$;-!{d}p~{zgJG}Y$5s}q>VIary$q%z}P4V*VORT6Vtsfl=`clh~0)@YuxtfL# zAkvXe`L$NsH6I@xwph#dHI0rs{vc}Dfz~>^U^UTibhNBtSDP#m-U_N*3@x?eD%gZ0 zgXPO7u-Qk3OrnCY$Hmjxhi72LXcUy=&XwXvJaE6mp~FvmjOT0iAdSv3`O~{e3Ez(& zN)d=-Di5h(hmRzWud72#8tG68Ill0yO@ZHe8CVs{t3*<uXvf<gwHN2DEaK>RdENwO zIXWWUvI|*5nr%D_jC3+)zV5gcTLs!J3=5`}@rO%T{n3#lXF~uWXDWB`6+iEq-QDeF z{B<izF*!RO&=6g`(aOF)I&KiXl&ev!IDAmm8Ul@Esm)`<McTz$Hmf;tFyZZ=U&m=4 z{qe_d10PDcoXZ|*PLvm0*=x<t#35Ip0rGjuQJhM6W?m$hGJg87gnqn=`!BPbj%kx; zpmvblg$VEqyr}0OZbVrDBb1N5jcq)plPkYsCy$Lw3i8eaukKv?gETxRZbeOvVa~q% zrCV9dJ8AOJ%Pi-e`0=yAbFFnq)3w&ZC-Lj9DMvjsuh2D*k-P{w0>?9a2GD?U%htY= zD1Y+bY{xreA>yCD^SZ@Y-Xj9%W|2tKuSG=6hUxFYPhSSrS@6!AZ38v4`_9{7-ZhKe ze`1LI>sjpa6M5;&a#6)5`~U*3vsDVb=Q`W3M=%0tGp@7U{%<BfIbL4!4~8FM7>r5x z$Lnm-C1kly<G;b+JUL2!?i=>f$y5c-ptn4L0(L)tJWS?#`XiDvhoUNmZvk3<HEb)y zPwwcCpFV|MZN;AVg&AU4fob8S#z8sl-RL)uqj`C0PjkCwf{T>#pY{+obZB?0-8nl^ z^mQC>l4r4jB5tNT5vQ~YJIwKqO025v^vS`NL%5<8{N<pNCK6RTxcbeJgrac*A6+Es zgANGww)m8RGaor#FE+0DowhOWI#S2<p{*hvH8s+Ungg$cBq`|-BOJ1W)2L(b-W0N_ z!@7G~=l3$#v1pm}5#R#g-vDKdOiBc30fm5tfEvJ~fE|FtfG+?YfY3oQDGHDZ$OcRW z{2Sl0w*k%o-T>?XZ4aOc&<f}P$l=e70*s1<r_loBR=`TYI=~*lF~C_s8{kL4uYduA zkpZ9wSOL=k6@bNn2LQEzO@JMM!+<k@^MD@#U4VXZGHEzq9KZsY0+=1=l1VFotOq;~ zI0*O@@ExEF5Q7$^0`!2J0Mh{r0V@G(0owrA0rXdcmQ?}P0siyvP?A-8)@GF!?6XRn zKf}g-gEhc{FBqhzZ(;8xTiFqLLSpbI#U+?BQZZnY3a$DDzu%3~EF8ak*J*i#Yu<G$ z{a}KmKuC8*cm8gbs2n>&nCMa@LhLrAon2OG0MQW+Bs#)agj;{Z?Jk5igxE<+ZNFNj zAqbZuq#aW-LR>47+J3Q0NeE*RCL`S0X{Bo*qot;+aF>Cw72*8|YY{$x(2lSR;XH(X zgx0IOE6#n;$0*I09Dv0rWs$TD|CRufr8}j2@LY@(C$I%D*C;g$v@~fswP^Rq?=odG zQIIqf4D+Si!Mq5`4iu1#49gIsqKURt6uSWUa{Nn1k;}yk?f^al1ujN>M6ht+tnT|; zcDH_?6OuG+g4Cgc4-0RZUXoWb{c_Xu`>MtkuIZdHBjYXUO{Vu{QzweQ^(OQJ&fu&K z8-~WP^}g7UO+&2A>q`iEG(Hf{8XgGOj0l9Uj0%JY#s<PM<3%XHN6o4(#`StfW0gMF zu!k-thFs1Fq--o=?;=`h3q=1_!utG>sEWonL0CViSEbaMR;K!4aKFz=Z_vIiWfecf z$yeS+WyvSrPGzW0FR+Sg)8oknR`!T*u-vvta75iJg553={A)e&%X6N^yW)EJwxZmv zg!Y{WXXZix{@C0f28R^C5vbL>`$WFI%_4aCn5c5oM<TH_>JyQnr@kkG_3p0!=br}B zsnlrTi|M2z_&1v-PlqJ!fhy|$+YDOaCG`0J-<1{oz5m}UD;9U@Xk>+~V9Bz(=R1lQ zEnhzWjzwj=zqmBK*Y0_pQ)GRb;P|A!rh?slI+GN#)W=52uDc3<wn}*bD<B(S1n2>3 zKsq295DQQOBtYjCt8@{-0T%#mfb)P>z*)c<z$w59z%f7*;2>ZRU<Y6;U=x7&*I%)^ zq_se501m)nz&yYVfDKRtumZ*ak^%UQGpSpgViAu7C;$?m^RiXy0B}Gn-~?blU>jgP zpa!rQun;gCFauC@c@bI<BpaXyi~*zq;sH?rC7|oyC=_rWa0Wp9#}MuZ>;P;8)B=_Q zW&>sbiU3AHB%te(^@+DbWYO%5yznOaGWU;v|L@~Zj0%wr)6(%;_umfL1WDa%g0wSy zf<)g5(`TBPsp9YDkN{rO2dk2W)%+T|uPH>9EO+6ur=$i@<`=<~9L8uV>G{)r19S8L z0d!};kGP5dFNGQvOE&-Cf>Ck>|MbX^iVXi7P)ljd=>-V)J<&_{54r1hnZ&WQUVuQ* zU&lh3MCYa5@z_e(4_I2e<4yPA3%0Nu@pK2$({9AiJc)SZjrhtJ!H=@L^S8Z<INsA8 zmlwXa@7Lb4J~me?sD0EE)VHb~>X+2V)ECs()Jd9=nh6?()~>x@`<_;*i`R|RmFVW` zmgu(Y-qan}eXYBwlk1c88Tw-VQoU3Eg#KmyN&R>FpY<VzB*REUfuYQBkHKMBZFtD= zq~RIEPQzhCvtdxisEqQA=8X3<e#(f*RA+iJpU6Dy%KRy_Gt-|LWgKsuXner<jPX_D zW#e$uVw21CsHxF(%*0LovNmVEne|!L_gM?g1G8Vv-jjVi+nQ6DGbhKM<FROS=jYy$ zyCQdQ?%~|l-0yR*<f28%k~9N>q>fY%QV&;a)w${gYNy(*-lTp`eM0@4x{qd{CR0<X zaph}2GyFB<YR2ywNtwelU(Y<5Il;8oG&3t9yC8c?&bv7u<$Rm-Q%<;Lz6IhY(HXx& zU8CNv-lcw9{hs<W^_S|O&|`AVWX)8~EX|{uCp6D!wrK`yleB5t(OQi*Q)|)YYm2lc z+FP~r(W6VXE44M+b=t?YPiePmUvz1AYY%JR)qbG;T6;nJFKvgmmo5Sw{<*%dVU%IA z;bp@IhD(MngEr%)jKvw%84qP_$Y{veov}Ydkr|iyYi6jizws7hsd0hvPUG{&qsD(4 zJB=FCRMT^&Rav#@w|!at%m(uWa}oNjDZ4tS(&Dtx4`54H9J3;Dy`t&RX6e`HU(tV{ z-<C1hlwcZaN;8c#jWuaZ22-BNW_sMT+4Q{0oV7CRku1MCGJ8sPS@!(wCD|*p|C;?q z_L1!OvM*%+m@Ugm%bA_CA!l1oltp8Cz|w9}<W}c;ayRF`ntL+$!`#nvJ8~tPB+=o0 zZ*`h_oLaBWQQzcJPgAc{H=sL@s|RT2YyPhJUDKrfQCq32(QVMZtNT?a(?{qBLfF#u z<Meucj()QK7JZq1j{Z*l68$RugZg#)$MyU5r}dZhI)lluz;LHwiJ{K01p=3oF)yPk z<GG9%GcITRlA+3s%8bt(mN_zWd}c;wc4l7Y&6&1L*R0I>nRjO{%e*hMI&)3t-!j)_ zJ_@;fKJ#Ga(ad)<Kgj$n^L*xo%=XL<2x+LXk8z+;XEYh786!<GrX<sFQ=#dQ=^az6 z>01cuucpwf-dSl`<FoR!Y*|%V&aCRJ`Ye{UE$d4NZ-hC@JPv~~&n(Z5&K{9{Pxgb^ z>s{F|W$(^Dklm8~Tec!6A!lmNT{-)5{+Xk+47Ma$CR>(Zyq>jOu=p%e?xfslxvO$F z=I+Y<G*_A@N$W6vz0|Sl0`;BhCF&<3*{`bqss2&@t6HbY))Z>$G!JW@((Klp)qJT5 z)%MrMXj8R*?J(U~olW<w?mOKNx@$UDZ+)`p*fM>E{yx1|zd`>LWZ^@7f5Rel<8jPY zZ)S(_nlUUZK5JLjw^{b=#GLe;^Etkp+fli^+^M-g=ECNc3Q?Ic>I(IP>Nd^&+IU@- zZXSM{@B`hybpDK=Gd-qLrcTq~tl!NuE#;Oum`#f;cUtbY-0QMDZ`p16*z$uV3GI_= zuo*x>8g;xzkLfgDzunN5k&<~!=DnG>n%*&=G@mwqX#T|fx%o@;H)bXJ=^7-u(Xz|( zGy11b?vUK!xi{sm&V4zzJ@+!{f%VuEAm|HWt5iR%zEiVAb5#4GwzqDeZUz+EojTV8 zx;kCHq0~@eSSUp2oZ%b8?}oIDdokJ8VY2PY*q8BThLjnRIXH7DgvXFsky(lA3^6@z z>XkJ(%a}DKYfjdZtk<$W$l8{3Jjb6i+)`k%S?;i`v%F@RO_kV+9Ss7z`fjyLy+!@H zdY^iXW`kynCR#h*r8PpyZq~k`{Y?9d_9mTIm#x1^U#Op@U!Z@?5R#FYc}J#XJY$?< zx*M;%loMi!wv4ctEORaQTHKb$EH79NSl+XIY5B<_%N>}TmYYd0+$o9O&ztIt>QGHz zXu4QUye1hcYKf*^vr)52vstqhN@|DZWsPg6=2wlR4b>{NO6@XjwRV&CS?xCM4(-d@ z)7op=kvg?buQTehbyi)Tu25H`dtA3!w^g@Iw?j8k|A4+mU#DmKP5LeR=k&Yu-{`;B z|D?|_m<>+D1BNw*2MzUxM-5vIvWz|%kr|sZUO-<S&N!Ztk!i;CE6H4u`4?Aa4W?U1 z=0IbjG1aKY)S6<PZk%Pj-Do#1Gp;oL)#x=oZhYFf4eP=I;}v5+Q@ZKptle1$vQ*}1 z^HB3F^HQ_lTmkhnG^f8M%fc*DlO*M#6J+Xh>X$U<HUHM=w9~aeX@}}2>zX0+5&DT( zw!#fB8crB~G`OZiMXb-*VR|-8oxME!St!XyQjce_<3*rC=MB;f(Ijb7G$S;lHP52U z-qC!kNzzV&7^Ubkbdz+8buVC5{7Cn2UB3QqD5U40NxSd@gJGhf$gmFU-xmhIL7Ab? zsLZ%0<K>L4ncFkpFrGEuZt|E8xlE@`k7PZT)sXdK)}gGfEQNWHImP_GIW0RY`_Al# zv)ARY9F-*oqy3EKqGfNc)P`*rDt1zRMm=5oBnImh?VH-qwL5ih=-$#DK{aCZv~8j3 zxKLx)EY>X5I5ahyTFpUCljfM^l;y1DJoL#$%N0wPB{cVmrunj%#S3#6lia{Qp+n?# zx$ASKNSQPXc)Ge!ZC9^V@4%|b)e23jCLNl^u2~D|IRVYmq3P1ZLWXB)7h+l4s%_G? zX(g;*$wFl2L5pvN8feo=P%~rnC@}d6%k8q(EXfjTQCO6gNK2F@))H??wxn8&mTZgF ql4mKj6k!6*u*|Z|w%9F;ElVv9%Sy{COO2)0vevS1+5*|iUjG;5p}WZd delta 25283 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 diff --git a/src/pip/_vendor/distlib/t64.exe b/src/pip/_vendor/distlib/t64.exe index 039ce441b721ee180d373e5590289a6aa9249a51..325b8057c08cf7113d4fd889991fa5638d443793 100644 GIT binary patch delta 30704 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 W8H)=RS1fK=yuGlWv7c$80XqQBh9l$v delta 27635 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 diff --git a/src/pip/_vendor/distlib/util.py b/src/pip/_vendor/distlib/util.py index e851146c0f3..01324eae462 100644 --- a/src/pip/_vendor/distlib/util.py +++ b/src/pip/_vendor/distlib/util.py @@ -703,7 +703,7 @@ def __eq__(self, other): ENTRY_RE = re.compile(r'''(?P<name>(\w|[-.+])+) \s*=\s*(?P<callable>(\w+)([:\.]\w+)*) - \s*(\[\s*(?P<flags>\w+(=\w+)?(,\s*\w+(=\w+)?)*)\s*\])? + \s*(\[\s*(?P<flags>[\w-]+(=\w+)?(,\s*\w+(=\w+)?)*)\s*\])? ''', re.VERBOSE) def get_export_entry(specification): @@ -1438,7 +1438,8 @@ def connect(self): ca_certs=self.ca_certs) else: # pragma: no cover context = ssl.SSLContext(ssl.PROTOCOL_SSLv23) - context.options |= ssl.OP_NO_SSLv2 + if hasattr(ssl, 'OP_NO_SSLv2'): + context.options |= ssl.OP_NO_SSLv2 if self.cert_file: context.load_cert_chain(self.cert_file, self.key_file) kwargs = {} diff --git a/src/pip/_vendor/distlib/w32.exe b/src/pip/_vendor/distlib/w32.exe index 4df77001a222c84ff3fef542618b3f45f4c1eb9e..e6439e9e45897365d5ac6a85a46864c158a225fd 100644 GIT binary patch delta 17543 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 W6<hGnR<0;+E5yo!e7@*<;Qs*}Q%$@8 delta 17252 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 diff --git a/src/pip/_vendor/distlib/w64.exe b/src/pip/_vendor/distlib/w64.exe index 63ce483d1e462373fe16015c70ec52bbbd816c11..46139dbf9400b7bc0b64e6756ce17b4eb5fd7436 100644 GIT binary patch delta 25576 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 Ai~s-t delta 25342 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≥?Q>KVWeGOAcjD;Wv1MXJ z4$`nlB_@=tGGtSiD=gpLJ773Zy+FDBQ3k3VEFKd?r6(iVZuel_ckG<|xcndWl+#r4 zmm&j!B;z|Lpi$rciPja*4v$O2DH)w+GhBs4nph!-7qKAHu+c@P6;BB_OQ|l%i6v45 z-LA$OjFeu(MvfKw{5Nfyu?_9i5KJ(<;;G|uTMI{zx=(v_RCbkf`9CX!qbJ?#2m7$E zTX#3Og<*f;ydf;D4`p)eh|KUdR70&$oD!Z+x?+${g_4XfWE#G~+UT4cD(zQTlnuR6 zmu(ta*Bhmkl*E=dw80tPk48W_2=50_kxkf`q{RWO(4(u{Aml$p*^7OpD<Q1HBb@gO zW%V9C122=(?!SNxFac-}i2?_Eq-+{{f%W!`$lP&W*^v%lez^Kg;S^EnHK@G0I%zoV zOw!lwlw9bDv``^WORbcSot7qO@gW-igfh@HoGa#W^<Rdwqn-u%eP6;ni&rha*oS+9 zmcH*u>R@pslcy~)^kATM?_VS^%mPTuu(4yn#v=ZC6yjs?N3aFGpok<U0UF!{(T+OC z%Qe_c9EF4%^Y01P#B1rO&e8@MFMyKfc&W7+VOhkUOtz)-ScmECl2@-0&oFQ$*2Grf zI!<RS)FE4?{*N$3Fd8Y#BW{B%x@Ii8L^C5A;!v7w+EPQ2Mw-!RRBkNXs-}7<sf9Ic zvZ;OYfd~l2JLK{WzX}P@=1&wvqijl1a@I;4D6!Z<hB6w;hf<jx)D*<GD{=oCR&DA! zt{qLB&EN=-_Wz@tkKroIM67LKmGmq6#bQ*ZOsX?;9HO~xEA=-+)lR4hl}Jd2npZ%% zfwJkVvn<2=Wv2n1FfFI0x(@7!ciXUnXdOqk0f$or1Mefb1XE9IWz)*DkgJmR1~CVp z;5G}8i&Ppi0r4r}Nfwh-MRp-3!bLg~#8Q0P@u?APj!$r(^T9gK;w=_bVTg+K%pa@A zQL2lBaIqb%i#PnR6~w>jXbBL+7v06>ikM+Gi8)F2UD!#VnT9hkgtR3!HjpLxzTz<e z`HlOHbkv%S`)j41aje33V(`q)$hK%OTCrFtI+^P)oxwV3&Kbax{8;|#PN)xQW~ogF z9CYX&Cw&RMJs=Y`jtm9SFdITRTWBQipQfHRQcpX^>+_Ldfy$kh{=#^o9z(+p>3e^w zgx0Kdu7woD6Ad2HIY>zdTbQxs?7M1rHrYR^`+R4#Blb9~Dz0Db!@(lX&p!n5DmBep zN=A<6#eBvOt8yN+QR>o*UGeYYzbPIYSymky^n&yo5)YvtwCaEIW9EQFeybmw7|@Sj z7tcNli04E6*v){bpmi88wAKTi62$MQ{db%&pS2CHWTdwb{6g28T?<S#T=!`iuNgk9 zYtV~)Sv*@9^oCAge+SL?{0z6Ne&UHV(Gs4WDt-rj$Y;xfgLx@|Z3-UXK(iuwd$YU2 zxq+v9f_~rBQd~Qb4PB__dIMY9#>`K2WB+M0htKw5Zss=px#O&hIoj}nS4)P@UTn7c zgx^`u7S1@3)+)JvEDgq4HYmhjcaBX93FEhRWn#!+KGKtYAM%pUiFFK()m>(pp;7z? z(aaV)Dn7`grI1fsD}^-pOS55-mr#QoU>2%->3y(hgvN)*(dIR=f^CoJ>-H(unD~z! zE)l1tPy4Z^h*-B*ljvBhRED59g~hkso!u%K%@V|*RbH4_@xHOpifMsimK!U?2h9Z2 zk}E|GB(>LXLrl6Y(3HIDBzY@D?r7v4PI+O=4HAkTTKbb=^uquu6dSz@G{X`rof*Jh zYUj(h8Ng<>dr5blecdkI(+HU~9Hs|()R-z(OUv#vb7UV~FE%mqa=`4qRKjVoN#dz? z^kG7xvXr}|vZ3uweADl2TKjjyH{b@Ad}#1kh}aTWogtelKIko=#Xk00Lsub7|J500 z?$9ki4~r4H7ivwM)EXO1u3icS=KumHl|dcVg*2f-mL)%&N2+KKP4p4g;{kUPwY7-G zlIZtPyb@_!Z)HrJ4?v8Q7XC)IfA!aBQn5ZL5o0C{<@!_PsyiYnxL9KVoz?wqAcUo^ zgESVDo0EEirC74km#6~j?*N4jB{|zb<`>l_wnn0f$0lyZG*RN-dq_NiKMnq3ll5@; zZWM#<3Mn6gSuGv9$7V$Z^VXqEj0$Xf6s?d=VbnfRzY{&A^B@B!3~fu9%B4-3vxA+9 z>cY=<XV0Si@&k5Y5rhh2Vfb<sjE=-nE}Is$mg7pVKaNsSUa(T0ewC48h2bTtd<haO z_0zy~vggVx^_ifcc9fyh6Dl!<t}OcY8l5AXIv>X7ps#`40Ub1fliChd%}_Ta;9!}K zdB;`e5LQLvr;y@uoz*dWhT+9QfP{lomtruSI>c%_9_TRyW-4b1M(%!~BwuBkEp7+Z zmYhMQV2gw7la=dbs(3lo?i}uim7UtRk`AH?j6Ljh-*~=><0@3+Le&2f#~yYL$v+Ci zQ0h2X8Jl+^(RHX#xGmigYFiw%jS`J&VxE@hP=>9NYgib25~f2IN)(E{bb|HUXu*2a zQIJntoG=`NAl&e)GBKuckm+X=>{7+5G@GewTOC$Rxp1^eZ@t+jmA%^~A<>HEQIJ^W zLVeUr|4@CR;X*q!2KVIpIrUHkwbis;+YDv6R0s)I=l2n8`rc2NTXcumOl%{@gR^{% ziUm}G`x=@9TS$;2Y-KR1nzMgz{o8A)r}e12)S1nS_V*n}iMSftOzo9IPtEU+LO*-V zHb!^saLy6+SdT`d5HHA4R)g{2d|xB-*YBZ3+dxWuw4ePM9mWUy@Ad7vUgtgsvdLw3 zijG{A7L8yPF<nP>Q<$)v(Y4dk`&b4TXS%658xQ{O(J%xp2vI~(tRk}LxrjSNS~o;N z#9I+@&_qT^Y+&pQ?yn7}C1@<VicM42e_&wm#g1sR0~e=~SQ$pQIU(`KSRH4y-KA_< zk1#vC!kBBf$gm}74w+K4<^<7{3%g4Yz5am7(h4R^ni!N7#zuGRU>u<Ah*fYXA#aB; zR?^KoY2ZG}n;VRYa!~TQ{w;0!MalIDR#2)n{0Y|(4*7b-1ktsL22wWJAsY(56v}?> z786u}3!Ok0gz@1?0!}yqa~3_Z0ylMo!IgFI9u_P70SWSi6xBgCS!@#j5U*PQmZ2tt z`(sL`gy&!u(!wpV7m~}?q92%7v3sCj`@K}phmO!F+8UCuM?)E<#UHTp?s0st57^`G z-Fp@9p>-)X$)-i1s2D*uO#z9HxfjW~uGS|qcCgb@HV$Dyv{4g;*lOwM4{Sz{?miv9 zz(Vy=7b+<#G~su!*eh>&vh)IbA3?0g=*)%Hkt|+Kr4!{3*|h2l_H&PHzS9FXxTi3! z9j1~NoD-Dpg}<<>>;-GNI~7);+>0Fkl^)id?xmz_%lNAFGiJKE4Ecdph8L`pQ%Jyq zyZwMY>Di98jSJ-;Z(;-ECi6)T*seIUaT!KOu(?(V#juk7cC#PiMh`0c69*MkBuXf{ zX9=*r=BTrn1QGko{?BO$&PXnG<c|o$x~Om$<g>Y#a5)f>Ne~A$(Rt}&P0;1Gl1*=Y z&hq2ix%a0eyOU6WDhyBD=j^Na(Dnh?QaM<<J7K`th6G6|SiI^PcIO%ygHHrI8JGKe ze=uDyFSq^4RJvmL8l|}`tXBt*k9R3^gb{`*aRUcP44c@?<bL-@SmqdVcT_5yX6|Bz zy*j4%CJnN?CRlslz$H@p3zi24+|eeqmgYbnExSSbr)(D!(MEfet&IIRMLCz((BeSH z?B(+foHR3SO@ir#+n<3~tk0wYq`9IUq}VWqqQ!=>DvVQMJU~eo4XMPDBSJr8BNB!u z)x-ay*~tq*HYBClBF;NNLNuHL)UQCjV4YQ2m4F7^<0HOO<DWaQ+X-#?ah1&2JF!Qc z%IpeIT$@<+nX<`aC#?Y~)%uSv*CDpN{wYcvTy!tjIoUSYPFiuDz0vz+x1}dA;pdBS z@fG#CC)mB-VSL&N=8?FxeNZd39R2w(*u0NkMKfI?Ae)YS%E}TW%x!Sa(lFpQFB<yZ zr`=Mv7~oj|-)e}y&;Cga<(D2~A$_{=z3;POefshlkJ-C@dgZ5mf)*G?HR3N;VvkQT zwCZdNHbHl=3Ko_$2Iqsyw8V;rD2T!zjSecZr(tGz!%N<XnUzi6#IwR%S8UhdAa(m5 z-q34w_j-t0w5>*+YV919*w7c;`Kwpiq!*&{FI^>t7$neq>!lNFEJKZzsj+M|_Nf}n zQDYm_Se_bNsl>zrO>y+nQkAip7&iTmZvhEas9-Ai-x$Efj{6S0s>~(HzxDLrsiA35 zNXWF3I$mWxk~+G)-GrmjAGt#wu{lX$`8BoF(iWq+*<&#EG8V%xa9d$aGI3IVp^QoX zb}~q2{Dzrmk}ZNVrmDq}n?RM(+Ol*N4Fe#d3~Xm)04o@dWUMe(I#*0F;AsO}rentB z@IXH$a~1N-rmsInYO($Z4tK!kZQ0P|9_D$M)b+RsT-kK<BT~mYD9%JBe*Po&VRBnO zB9)y<4&`t5V)v8%h2y<Y4yx`g9F^?^TOc+~culS$D~i1yL8il5*>qZoBb~gmN7Tgq zO5%?VeC##p^NVbF-~LWJDv`~WxUG_X-?xjCRgEoCV(zgx4Zp#@gATjMrduB}zZbvY zFYIO2FDCF2yO~o;KYrBDEIp-d{*MD`+~BCl#ja%=Vkeye16zo}n&|v9M;?svNO)bQ z<z*}+`wI^pLU6ZDKI{b}0KT^=I2EvW!sjPl{uv5+l1^*fJCN3?0|_dQ%4x;jBj5{x z?IxR?w}NY15`Md=+%vZWiueO(wEEO!ElOaS{k+1m@53yj=O|Cwqj<Sour~ExIDi;; zz(Aglce5Az1@V!oY(hWpe0LHDcRWf;6)(|Vo+~IFNrsP3<7BBED4I#ahM!#KZl)dS zjMTS3uEZarhy7wt;vl6*67($0EFEY>vHzX9?cYpQ&TTdEwC4t<Elk%xsgnzi)ij*C z(aI4`K0?@qRH|%}w$PBR2v5TDrz5xg44d1(W57YQ4|8GhIY8!>v_oxfkTp@NV_){~ z8IwcViuF1>v>IB}9*<nW>D_99Kh&;)hl>HvZ=#bAU~5;LF40?9@PHUy5_@Str2lJ3 zhvAD0yKIs-qaaLjCu`y=UdPyge17q5<|RxD>T-ixL>5VoazNY{Q|+n!c+PNXGg~9r z`1nCAB()RokqYo+S*d3J2Z0r(ZsGN(So?w9`3ZMe=D@D;QT15Ted+EQ&?vj)dSh`* zzo6{qwXc^xJ4=gP&qGiseha1_67>%xc4}ZKpLUl0HZU~!5=5*Kx1J%*`s7(|M5X>a z&`>QbIAIg(JSaGLXA8r76Rd+v)+bMMy#x_guu6T&Gd6QjJda!Lhl4us|J-Kh2c`H- z-$-g<Ec}%!BzF((pS?D+j%nw4`6-jr+VdNJWgP}r@GqQTzYm_u=XGMEhJ^6_;@PW1 z!t;Nx!-GZH2@YFvCAB#<p&EPLXLw*E6mKF%qipx^%_<PQvFI`2p}yf3t)rrQh#XP& z!?V%>+#gHC7wwDnVNam?FFF?MgB8NHSno%~73lKX?Xp~q23jBMmzspEQ^x%%gIM6u z5pB{ikd3k<wZV`CT5ZGkC#elVU4CJ!hI)J6#zdwjT({6oN7{uX=_)H5+QSs&i{Z=l zIzYRg<b(~hQt#l)?9#{ZQ+{C+(l_uYer8Y8r}6>yEbFBX!o^COBry`1r1b-6ibN`a zjskCMJeeF{{7b^1N{|60z4tG^t&&}SDKvkE$`X+LO#wzjh&m^YRC(!CD5Cw<Xf&13 zOQka?$);cT699*Xzx6;SI8^oOzF>(W2}lMKw!M~dq!krx)4xpO@dFwy9&nRVbDgaY zXs=$nb%HG&=F6Wx!Age(^E*zk{lk*^wZE{(!=m{$H(AHwUHMhF*u>#M{Ndwl+3;9C z_5|BId;q`jEPFcqQ`b8`wHP*w*q#xO4ZqyPwZPV2hx2H)zz&RTZ{7utH7?Xx^cI>T zZAPrvw_%x73Ialc!1z&dFN{)!a$}`5P$|XchW7A1M4oWK4)82DNa_J@C0;XtPP<6$ zl?XZGrLSP2!e_<IRv%U}%F6qF%}k?b^LA^Pb@ZXuGu@y&gM5Xo#)n7WfDPK04b2G9 zwPmw1`gZL39qPB1<K(TixJm8RSSDh*F7OMzJXYB)6?OS%47-}qmA{k0T*riWT7j(} zcUij93bRgT2zIXDHaMUo$LgTm$&O}=PwWNpEFQ=5(#Ul7@|Z||UdG<H#&prKwqc$9 zYN28RZX3SRmg_Ae*99b-6Sgqq6Wqk;)D98Eel_yo+C2N^z76j5%r*9;v;v9DKKxz7 zG}z7B5N{pNJ`Qgm77dOv#AHWGp@<dx))x@%4H}IbJ)Zp;9-nv#782xgyGf@&(I|Jo zD7VK$xi0E3?*+SMm_Je?$}q=*WHTbn>}srm9USXy$b6o5?<zY#wkMxCk~xo?!vFM$ z<%|pE-LA8><2Jc?!05rfG)}6oWr^ecOus$Ev1wfgrG(EnO>Dw*qsnz`*7y!ipTow1 z-11tsY5dGy8G$$~RIhM=WJHqY{|#42NA#?|PLH`AEW}>GQ0t@xsdTy+8cSK<mw)D; zENAT|g!8xFWrHUi4)j^B%%!tz+W4kIO^aW8-7*PVis4-rF)`J*57ueH(hO&C_P+y< zoUy?H<j!~4n-fcTx68~oa}giZpRLYp+u>#*IVS6HSeRtfT}0r!dJmS5^|V}n3JWix zLVURn>+Pkr>}KZi@Wn++g>G>4K$a|lchPoKZ!ENdKrPvfotl&rIzI*1OR`YFy0Zq} z!T6E27VievX5M#e*!amqvIg!$RZpHJ8&j@}6%Au#(_e4U8L7_=kS1QBQF1~Nl0_jg zK3sh=Ha2V1oLqbHM#GnA>#Inr?+7LydHG9&5W}E{KhOHbd1lB;PJDQtOle#z&wx-% ztv`bDVy_jQG%m-H!6hVKfbD9ut_GbZ-cA!Ar5=5zPqQUf{?5d#4nC`p5&5%~{G>w- zqha`-Vbxio-FmbXDh*P^FnUN#$i#3~pjniTyvFBZ8){hSlt{z(HPjkKBd6D}(Np5w z-T|lClOIyUHcs&g+F(Xyr_;o5aZM=sgO=Mv%W)y$$pTktWDPq%rEh%D>lo||w8=KJ z3g*rcFS2wV!Wbqaz>!?wvv_D|vEGYpWIgACcAY6UJif=$rgq^k-(yRs`tZB&v3I8? zbbJfvm{OL2l6aIvrbxDE8V9~$TTXfL9Lnx;u_^T)`)6v3haDwYD;*OqE?M5tN(}n* zE*m~A%6(4(>4&ACko7xlCU@D&X`T7%e0F477(eeW`*~WNpKTd(;z0)O5QC{2L9v&g z<+E<nyYLbDZ0ht_$DIvi4ZNMtHc#(k=s%9?YLau^rGzuAVY+{(*nIS*iC8eEH!a0T z@v-G7$_cvhBD~QJ)i@yO>SQyS8(2*CkeFlD&>%A>Z2)STunI$0bTV&2LgL4FIBvl# zLA;SB{+@dDAIyLE&3D+`!~VKzb|yQdM~{|F(jc`Qb*g)zVVLRwsk>IX;kn@Zr&--$ zUsj$QZLE2W=@W;$)1!bY$&JamIZiKuhsd(=EN@}AFh`g~C9&6%t><LCU}H`Q>Irkf zXv1LQ8P3nJpd1?5xrN@&Vuf-$(ciwqeqY!(cq`gHQ1to<#=t(L$QAWD_^V!%rdP9} zizXW$!AQ__t?(o}vM7uX*vWogG|Ocf+73NwC%tr%jmaDDmwyeXFWm<5JnVK=OD0Pd zJDazK53FYMUk&Hq%wy|b?d~x&PnqRj<bFzmewL19H(u?*XMMta7Dw>wPP0CX1vl^0 zm^IAFVLY+udz!6XoWS4Q&(1IQ53N5%8gN=Ff+>_%9PvG(X~pd-^?JB7$q2ynFZCd_ zXOXYDdrvtID;!4Ac*seyYM%5{Y{+Y7enKA0d99yk8>}U~SORMt-a51mGtYL<2=Z6n zdK0q##;PnE4mPRW7>wqXpPBrcIlUM>Ww3A^l$MY{LWLz5!Z}3KB`#p0GDz5S>vJ*H zwRBTM`nF-TTiif>!qLC=$=09o1lctGXEtw%4?p=hTfU@oz6Tmj`gZ47i@ssGlD<t% z2>R<M=-bQWG!p+5l>H{+H?XX?+X%Ddj8xJ7f$>OMoTK%o&e9X(<d5c*5cgq99GqJ& zU{&G(7MmK}q_<njhwdQZUC4@+4L$+VYNSvLX0%914=Y$wr5DRw>d!y>kS$sI5+7B` z&LQ@*g*7Y<<4>L1>%Qz;ozF1%pI}sl;dI30g_AeRraxTB>hs(4?`JTtg1(*3z+FNz zn-o7m(DdV&HtbQ?asz1N*@wj5iFn$Mr(~z4pP02E-2c`w6a#%IN0ELIFG%1l>6E;U z9WCgjo5_AJ@C!UfLf~a@LDklXeTZV(+Tm%&uZ=AH_0HY{zfqMT1}T!CDiv{zWxt-! zA0Nf)U%v)>g`Idq?0Es39yM?)nMAh4Uuv4~thfb=S5D}^JCAhPG(Lwa3@V~6vN*Br zw`{q!*lpHP2+OBq%ke(fxTCC1VK@HOd2D3iNIqsB+gTVN`Dm`97kKINtn}6<Sxyxn zD9Tp}K0VS!0#xz9TxKZh$Xj=^grcAqXT!DCu#lDve9EPYAY6E|sSsp}SeqJqO*Ylc zc}`Cu5lF{1Ck~p+))z&EKHPyjz@v+4#X%21seWkXDP*-Z;^tL!s)Csp`$n6^#ME<` zlzVu}m~5@aM4pv;Z>9+)*GX<<k!%SkC+T%mDJBM=W6^I;kNkvgJgPv4c>Tv`Ic{Mn z^<*Tq%l01$H`tVfpjBttH*b#VwEZQjUMZ~$j6n->gs3^-?l?D_a%|FaIGttv-pcIs z`~S?b_KZ>oa`gQ#IhLGZ7v6dyf9qjvc(#dl>aKVhx5}kcg{(iZhCe}9;UH~Pl*qO5 zVY08);x3KeL56~CX(d;}p%`v`;<cnJgqM)8=-Q|{^uL-(F~f9~d0*n!E6)H%BaUYP ze{ym~!zy(X<--rLHOnphvf<3JIK_8K85tT@7g-txs`<eFaX*_<e1~s)h^4<hB24HH z$CVu&O(Bx+{$}~#zM^fw7qcV%{1vNwyF2ebgFSnDxc8xKw57OTopQeQ60+$1u6dW( zE-|3vo@tnwDrq9JO8)pG#?`O4r<P-5@<uky;vh9cq>>?cmXZN`nKz^bad4e%DgX<M z@)N1zOLkj~a82rq0+dRQTx7m0CK*rPMicv0QG5NQzF)HCE86krudy#yczKQ<Oh%b( zIVQR&?h|S5?Y*@toOPXtM4%9?<m%>xL}EbHSh$+@B^Q|bRqE3uY5w=jePthh>|U0> z(rmtTOPL;Hp$T#+%jaa&uf&iv7xUFER<g34_p9Mb@y5bGmBQ4zr?4|CJL){y<CXjP zU6a}FRUJ*yQ`A~>yUV7u$;w#${RO+fDuln&hk2Fs<X_s&2A71p_59-bTvhC5`6YAw zB<LyKe(2^S+J9Rx#nJJZXq1k=!1SvNeJ<|B@S%Us2NWCa5~2_FKkQ+fSDW0{e})5A zapX*0Ez5gPuO4pKYt7D<<FXA%0Xv}LVowu~Ld|Mnal$F7K5V%-L2)@B;L+`RekIFz z=R*Gb>(LML&ZLA3$)c1d{*fluOZ(93*lTzx!1Eg<<BhhW%g6(xYU?7}x6X_wF1>JK zEMzz|aR4nwf}$KJakMK5iz$KBWfe)=OoAeD7AD~lG)gvYCc%6Xn5PQ3w+MWs2o#V2 zr4LopYj7T7BS8TxmGsQ!^e#wG!(LFVFG2zhUMT7-R>fsgT(g`5l~b(GfDmqkqJHQR zE=GAfOWz+XJY^d|*%K-)lhYDzSa3w5?ldK{p%bNujk2ltI5xI4)o}D{l00YWCYyd8 z%XXJ~wcD<7JY~~yTzsh&bo0e4Uuc<VcooUAX~$UBP#Vg=@Fp{^>*|*C$#dO!x}6PK zC-DBCuyyMOxt)Qb(Y(UPY-jh^<?#cyvsv$jgl5o29wO?~fyIV_6!Vmf5ZCOi4Ii^z z?{(zwl(OsZ<?%J6*{Ju^0tb>OBdSh-gEWaY%bk@D)zwPQ4I(@8J{%IG*`M!6@GCNy z|N8!XN(P&}zBfPQ1lznmkl*k&JF>nTzh^Xiu)a^XJ)@!Pjc_D4z8?R>56U5z@D@t9 z4Smk&0ET!1dw`=5dq$F8XA?GcWNS8f@R@J1k2b91-(1U*HpcNgN3g{k2LwjJL{J(> z&y?WY`=ZiLYuwW1?1zmV_{+zc)24u4+z8rrEGE3kpP2%`r+86$N9+}_@u=|M4T>p3 z{W$ywL$l9Fjrpj*51(DcHf^%x&n-l4;vjcvs!I4u<5j{e4OfXsX`o6(OUVl1BE_qW zBq>@Y1gV`$q)Wjnks+B>B2#iziEPPECE}#VR;A7y>Ap2zq4Fe2VTlD;Rnm2p#toa3 zYE-&}=wm9qj_5L#-c0mPmEKPDW|fAEnv>S5bQ#ghRl1z$B`RG-bbgK+fkDDaQ&hT^ z=nR#vBRWl`C8Cp5x{>H^D&0hMq)L;?ECs4Gp4M@ayGkqmI(wBS@1E532K8LbH&f)U z8c|$;bt<j6=Ps#q9LZFvbP~~rRhqnf(r%R|@1C?xrE`d0r_y;uuTbd%q6<`dInkCk zl<^l!D3YxvtRs57N@o+DuF~6yPEqOIM8~Og8POe8x}0dUN>>r>rP4J-JF4_%qMyF5 z^uAVytE1GYM(QZx7nPQXu2tzqqN`Q9iRf~b=CEx``&8PI=<O=)PV@$q_9ePRrOiYa zs&pjLd7$$#2hkLnttP|~ovG6F=s+5w(gM+fN~aUuTctCIj#lYRqQg}>n`mE^&LP@W zrSpi!vmToJ0?_%=g90V8oDw9JE+P85O0OfjMx{3seN3gd6J4g#yNTYZ(q%+%CYs(} z!GmLR%uZ8Y2c~ylL?^rsz3~I(HJQbb!V7DI@B*%ZsjO_JkU{R?6sw$3DklS+Z&Xf- z%7I(c@TJO$DPZYa&G{*6%t=i*BfXc(d8DrLNN}Q6&MlP#N2#H$%DJd=;=l=1IY(7a z5;z_z=W~@Kfa9ogHmaO#aJ=(XmZ-9Fz;abNud19paCnt7UEwIZ4VZszR)%Ak%A{=v zOi5)Xs>~9Vc}-<T5>p#q?Rho^na$m1bc@~i{Gj?~0~5DJ@sBs`E!*}5&tE*ach<*Y zI=)9SE7|^HSl8EJN!G5D<$kb7iu-M*>sm*t5)AF+8nKQ&+}=KFV+Ou@DEg2ZfUDSg zI=O77ruT6tN;qMOgp1V@mX=ixKcyvu4g4g8e<_2_`6Slh{=A@#rR=j$7PZ;!s)Lzj zY`_O1r-ZDdcyXgVHLF~(tLkt3s?e#1z4+<hy!-pC&yK<T$@iFTM^FB63_G#o1Yf>x z@5-I$bh=e+!mbeBAhOqYdGcHedv{lo<Jq_A7%zK~{kY55@gpU&?!~>&cIkEe(#}l3 z`(6Ile7147nU~YqH@g$~VRQCA-QA8i*jBdm(|;w4-WzThABY~3<=a|%X(gMqw@p+^ z5{~qg@Hk?^iukOLa^8_wX%uod;O3}#m11|Y9eW4I)Y{ZL_HLl&Ra%2#!!3VdLgFD@ z&lb%6FJ7g6JK$CFW=Z?(`MLnse_u#kYD-ROf?AlM`j*nDad0jLO8vD;f}e}-Si%0Y zucxc`XP6oo6?idj3HxbZr}jrz$Z|-W^gckF(sm%}Mt-yvnO9HJ#Gi4M|L9W|_hnX6 z@~7lJvad^inBw4AU@Y8A3wa+?>`pvG8!w2<%Sj1rdLKx}kMSBPy4IzNu14e93fVNg z54-lIA0OR^J^V77|LPMK`PGqL&wA4ZwvMdQAv)>+K4uc|#*>*!xr+0W-m0PJry2B= zE|Utw=L)i^e{W{q-;*!e&c^JA{hq*<?4Q6NY|E}AR@#=y`y=@;KW6RABKY6pS$dg? zU-U7XUiK#c`$z2jfl%k+y=bD0g<WL!&jEkd{lNRYK}q3XiD!QuILn8`v*QP&V(xBJ zS^%>gpTto!;Cpm+Oo=)no5sW`bM0b>SGrOXp5>P{ILfBLo-FiGcmFYOHcziK7bKoa zl>IH^{em++*eizu`71ryszat;X-KNiLjska4okhAQk(rzWqgw?F3AN;Ie%ke3`C?G zFpw%a%rPfDC}KYz3gCxNW={?!$E-zy)r`|Y5g<o+{c}g<qOJeKQJJ%q9F_bAPxkg< zbB|UnxugKKAXS^3;nGz+m3poCA7B}2e)gG#@Kl=J#^Wl6{rUjsjyBz3ZeI)Lu`NOk zeWBBx6Aal~(15l=7QIKAy@f6Ox=nsSi;%P()<$f(a`#cbT=Nz1X^nZ*=;RT2$0iAC zYG*+m>QyKDVP9Xc9vk=HHx?Bsx^haoy@&RiPAP9fS#1&buyxV}v^%^hQZ<rj>xrbu zHzWt|TUM>m>gW@_w0jSmb0nw-Z!9z@C7qMrRK&Hsul_sl?B{tY>&uiiGAdS+hqvII z)K-(nyMX5;zyB)lyoZ$^>CCTb!y1ny@vd80%r_%@&S*npM{fn-xqsONYTWqn96_8N zo`=y$N*+IP0^Sf54W{|z-4v`Rbwa$M<{ftAn;HBMS*(3|9&hc+ww5RIH3jUa@>Krs zXx8ay1ixo8+jO)Cd;6#dpRJO;X9U`pJ;BX${EQAd{-e2U`OE=f8#AF%Da)ES_A+&2 zj|D^91<IGQKW19^l>=G!tT2AYz`eFvX*$<yFokG`|MMIki`|;tm$&7!_H%;mC!u^c zXwFRg1<+J>V9qiAg=H*%?s9vlCRFS)uRv#i5`G!BDkn7X#T;^)oN%P?r_z&+NB$AA z9^z~%mrY&UvoksIj;$adn;IjT+bcniSLadWbR>&?CB(kqG|I?$<wyHJ@Ff`QHGd25 znz8rC`S0<3Vmf;@H`ws{f|d@rEnwSn!@`yCAT8m(s$*CD9!fbq!7y$$lyV~cyilES ztC@T>nD@_OZN6>WZWcKv#FW2q=M2Q_WDVZZ<@qgoe+ML18g@rGi$mGUZ(ZE>;?!L* zL&(A#yPfuyu#dlu;Ac%{XTOa!A0(%QnDP$}3|BjP?Bp$lJufP5KJ%^!kMvMWl<vIJ zTuH-VG_h-fp$ThvL3<(VJhp|VRcvxasHqA<w9q%V2uqz_VeeIR<yWp^)fGX>LoM)q zS(n7Ra=Cc9B`wzd;ASHY->OiKdsG{6A?&f766<=vnf8z{3>9vb#zCU%uaNa~mrWU= zti!QLb1T#=h~HX&(w_BRAdd%*;xu=>3mM@X%H|)73f_x`!=2<Ho~Zu{3kW(<i~{j4 zE_UtztYn9d4fc&DhV?ZxCImOV=C;faVdmrhy`G^D{qUOJ6iSj!Jwxyi`9-*@UDbFO z#2fIU>{&(Vg%d~;ievGN@K+nN9Ph(_WMlh|o6TN$EJ$7u+(FAN3xq6sT_VQ@{VKMv zvFgXz*n{KK`5$uF%O~PHzdN^ii0e0E0O)yM5ilWf;|h%QQk=T@%2o5Nzq^7RJJF;6 z5i{-Zxp3z13q~ImXFe{wCMh0e;X$28OOfiH8I0$P*fTw)zvnd9CA~QZ9`ux?oj8ig zsgR|-NRKxci^`Y5=L<)}IjIC`DAtTV1fnyMDI%WYoe(e6C#m#mvut|Sh8?S%XmC=B zRa{K(&Sue7fw~$txT=l&KCGA&@t<Vl+Dajz`>q_eq$<*s@&(n1SIfxdL$885NkK!{ zfvT83-64*LgUtq;-h?<*A(aL(MLPE(jj3J$Qxdn)EWi4ybZ{#XmK;exo3%gLJ<)l$ zBD4dmlXBq=O6;=TcPi0`xn}j#iCdp5Lhouqn^d8<=CSoBWB74f*twI@e8@beJJnuy zfkmC_7qELSx!fH|Jzu2?Q1WXH^HFf=Tqd4+iTB;jemoVF6alf6c<hvxT>2_y&j+%6 zghwV=i>>g<?b%((ijE`qNJ?LW^uhFn<2k|R)k=CYmL;4H9lF(DnOir^oK!J`rmF)L zf$1n3M5-y{!)Ij9Lu7DqXR)Gw3nusZD`4~qs99Q;jb{ra_7dB9x}$D8yL#F-ZSG(g zelvxl3%L%`+XF$57p#x#7QKkw-1u7ar}0JC1Y=6A@-0{zeX01eA62eQ)x3DUC*;&e z9mP;FbR<f!KB-&yHSN8Nz>y|rv+C-AsD7v)S|yvRe4(FJ(!J@>OS^{4Qpe_m&((xW z)0xMa?*6xsuD(%#{U|Zj_&I#>_-bG3bcjtl(}6!z%xq`+^0#W)`7>Sv&()%EI!zM= z@dqIaqSuTok7AJU;+hX8>Ki)uZKL5Z!v~~zo6H4O(PLU@TUW`sr6x(9#=4yCJMNpS zbR5fA;p^1GmIX9ila5aVH%_1HBCQ8i$r)%8@L&qB5#sR!Q)){@ZE50h)I%T6O+>6= z<f|{k%PWYNA#~>ot3K=7(G~nhwL*X_Jh~ys=LA?5xe-!KOR3TTkjh|v>doBG`S7+Y zto^xQzI7o>J$J_6dn#40tgWq6WVwu}24_+KM;?o=84<i4yk_TKH_%w-Qv`8j68_6Y z9c-U1HL)JnH!(58kfy4VG++w5T@&qU0>?Hn6x+qtEEabDmGo*8X`aQ?x-^nA7RACW zBoEpLn756x$pL1UwP~@jC_v#q#CuCnMT4R!%CjR_W74QBGzNRR@^#*1_RIO+US)9G z3)a7%<+@6#SPRg#kEXG17uxZ2Ozh<gT|CNGqs_yWH-f%>1CIL=7zY;u`T3LC;S0$f ztz&4ao8Jk_Rb#k3tM3GT@&oB$E++c4l==f}e=&ydex1F1F}%|i*aQE&cY=&?wEwqv zf;L}dyDoO*zq-b*U+gVx!_80mtc+fNhTcn_FVf^Fi?*|18>LgesIHP8yrfjE7x0AA zSG^WIUxU)O6Ite^=-IWMls7gHVnC8ouz_O3yyt;2DAspIJ77);Fr}<_u|pYx6b4?} zcBR3>Lm!i8YegioLXKDtQjU0A`MVZ>@uqim(n#bq)4xv;2d7J&$FS9xJ>9Ke7oaZA z;-c<z{!Dkp%K-wJQey_|bJ;iHy9^wCbo+j}LM<U0B^+=-9_jQXH7U9|Ni7_Ma9(tj z>er(JKYq<5st4zTVdh9$Z^zu(smp=<C}Mj#&>E2vCb8GA_&K-|9iWKw(hL^&T`GSl zQ;||qHfORAu9)o8rP6y;B0Ke+v&-^jq#s_Vr1hiOjqgJE-kFTQ67APvi8_~f!ht8p z<?0g-XUXm!8+65wubrqAs+bO6Pej$h{KknW+ef#S9k?=J|893(k}kj23|j!dUo~vf zuvLi4H*45g!z2wyXgE{DH#OX%VVQ<!HN2xChtF>4=cQsk7p^h7Y1m)GQ5w$Gus}nP zc4`5<h7a)4HvPVDMosQF4WDV~jIV*{=dEF+f^1i<bAFn}%G7X)hVN^*OT%w9yslxR zhR-zg#)sqd>#X4@4X0_iOv9BLZcs6w`&47>)9{#v-)s1*hJR@2h<D2A=dEG5hS3`K z(y+gV85(A5XwmRZ1+gQn)fgXXSgzss8a8U!N*iRehS3`K*DzDVYz-G`_=bjSG~A}) zKElrENtMP3*Jf4e9{%jnW^aRrM?!V%CLa-$-jhqBF(^z`QOHm+d<k%Mh^Ha%hRTSJ z10qWsQ}h3UBD-TSLb?b3m&V>P*M>UfM?Y1C$&R64%@Z|FH*@;sXmMIg^fPO5oTkdJ z{IM#J9Z30QYH@67%FkVkdus9Bf2;C1e(9IbC20(p6w0r$Nfm&_qx=fAcz_mn*W%<` zpkMhTRUS64^2^iWZM69AhibZ6izjLE5G9V+Nzxyv0(qK1v=)C=i@R&_#ag`aPgVY@ z7MHZRti`ML{(URoxVQYTWL?<YNt0(!HCyJHv*u5oWSMHtnLjUU>VgI4+yyh{PB*uU zn7yDK8}Vp7YkcIl_vj-}JNF;W-2&t1|Ji@z+aD#K%{VfS{qi`okFFKZwTj`>TYEZk zo>N>o&u%W9=P+Z9q1+`$=#H1RQFBZX$BjhTeM4==SkQBoI9vS0zqiiWo^$TjigS)> zmD4)VnG2j^<N~|7ae?!F8@=niYCOu_m7++o?93e38pCr+D~erzV&?NEvS&|X9F(RE z<5`!de)&{2$>+cpEk^iFlh@hnIQz*+bLOeO=+;Ji&e+X?GsdXRr9Ag=3O+(`tV0C9 z)?TeRuPKh47v#KRp0}lS7pNRUi=3Y`=XcD7^Rqa!gr^3j4b=UJXpTFK5L!p2q3xh= z{44&yYeQ5F$8|?&F5AhV=M3F^ID^iMqicP0|2$)12O}ibsi<@1Iagh4PG@#y^Zx1Q z(JVKrH{SI~<hXHl?8HC8G-AL0)4yeu*7xPOs|X8G3jLgDln`&uJ?2G@dl`X7hp@Ge zGv_nKjq~Yd;CzO8Hny&NKH(&DKLx9&ANH?0MDT0<uNkH?X;e=1=LT^#{jrzSkqF1# z0_}?TXfM<$QLeS8J+#Sz^NcaJXwqzf<K9DP&O={x{Dg3!jy-%9+@ra@cT%Bj12L0Y zQUAR^?b0}IEQ0$@LN-ti4$PYfn+NUEd10byvPWRDp_DY?EJyYgNFYbw+!qH$Tbt|q zX&8L8SgtWBkA8k_969%H?wtF4*Ba+?#~g>uR_T;K4rwg<sc%YiMqxC^JwRyA*jimp z(aq_5@%)`$DHyC3tn;wvJTT}UYh2RbN!LZZ5a7%OSlqb)ESUf-nSeFQa!J?uI&;2Q zjlLsXINxrdhpEd*XKck8r$F7&GgA3?EV}3x9oc}};8}$1o9}$3>oKgk4I3A5+&P47 ztqrY7%`L8+-xR1i%Jv&(YV@pWT~6{eu`#(&B!t&B`M=?KT!r`-H%lk$bR9xlw{FF? zM!wcDv`08+HrF#0SG5HQvwkW)q0<fUQK~VZ8dQaDC{@ijm6y8my0n%yPk_7f0>Zeu z(wV$2I9Sy*XU=(ktCl_E@0A=ExC&P2&C)%*t}T?m^m`3?J6#Wo_p;NqDgA=id6gF1 z=>j^_Tveejtxb-c2^*Rzrdh+EH<!K!{u&|sX6XexUFbl{V-EO#&T|HqwT#d_lzt(O zoW}?wrqqq|nD18Zn&Xm**7UL0wd?8Yh#5qH9{OUod}F+F{GwgBHr+nr+UPcL?u~AB z&y|bgsQMavjw{Wx*G0B0>^)~W?jnNeCsIo^P8n1;3@Qe;8wPg1sqWu%cm5p5H6cW6 z@_C^B&f^?v&i{XyyN@B$fG|sy({ZIMbUNQsvtH*{dQ+$KE}f{?jdpZ_-Z^u|`K3qn zy5L^a;%Ky3)m7-|2q*LuJ&y6Jqb=Vr!h!R#bi*bS%lYV{IhS;`mpQu94y|;3dQkon z+ys;@y)Vc4;@{Xb|5lP+(ugh5{G4|dyv={C4)OlK#`XU-uJiwYHm+e_{J$`+OXoZ3 zYWI(K)@{?XM*iSe?z+tmgG-od>-~R%FvTbBAL6U~$-Xo_Nas6n1dm%N+L*3knudag zNgBp!7_FgMLw61B`O-Z>x*_c^>D2OSG_2OJO2cvucbA3)>wG5tqD9wgh}}eK4*vU> zf_WO|Xqc&Cx`u*=DH_IUXx7lZ^mSBIx;<DIsjDkJ8LVsLx1CdqF3~Vg!|YP0HoA$e zn`Cvg8#SyeeWi`gXU=vl8mURI)8flD%+oMa!*mVfG&F1IrJ<{aP0!ToYBVe(EH%&6 z1wsLamS&iBwXGZN)D{;ws>qclhv<Ct5A=v)J}XMip}HjJW~o2)`)7vYKjAuMyQ<<D zrSFC5=H(0c$GX+-csACW<DUBHxNg8)Pi(E==K&|dj}`}d3h*34B<LF81B6SU34eu; z&jMNkKEUm1Jn?}!xCre8O}H3=<V%2`Bb4Kww>uD*EE{eD)rgb;e@1Y`zwfihjVTd< z=p^7Mjm`n?)o6N>a2vrLG7>PP4UT!x^w0b*YV;*wRtOpm{s!Q;2vp`Vpe~f-?t12< zG7$X`C_w<$YxG?pACA-74>G_F2-E|@VG-(R<p9UE<G2ju9S{5%p#b!5;01&gpf3Sk za0gin+7-ALVISyy!1MX|^9u<9dqq);@PCehPY|fGr@%efEqbF7`+%;UF~n35@CZV! zw~i|Z_UeKY5Fd&620lWdscZs1j)q+e{!`%eu4*T<fj=S8XyyL`VjylB1rSOD-iqZo z<=<ZcA0sG@0QQG@R}C35*ZTDYA2hD}9B%T;$Ptc4Ao&d7A&q|+cu%9p$HPwUjguL9 zdjnY_=HJzXA;s0?b{~%G23i98y`UBp2ow<_(fbv^PZ4Oe9ss?PR2jl<8cnzufu^zq zxDO!%WtIbL5i&v70dFEq0o|CCkJ}0&*@!#<<|T97Y|w-<0u8TkUygf*PyjxM+fNDt z4ISZhgq`4L1B(!-_hrC4xcyXvZ;wk&PXuaE3UCGj$;<|Z48SzyLns`?b_6Pvuo{6f z5X!jSM57TLt~9SB5Fg$Zu2Q3`fENa$H;}mmY(St2?gGOHX)6LaUZV-uXtWtup!`Yr zvlf|4fcJ)A$Urv&V~45*bpyVw(JO#$)73&Ff%_0>gb3-woV%b2Ck%(8LIE>@jR@NV zP$qEdD0Bw=Y+(NkJd5zbD}nG4aXARo>dj*?_qgwn_67nMAW)Ajz<Puw;NJxv9gpP* z`WW!jmoZ|XcLE<wz{VAbsRVjVRCUD*xCnvDA)E0X1gdKt@UMya*gz18&g8gd2-{Im z0njiRH%QQCpmP?^YS6C0wo}x~B7wP6Q33dQz*E!IMppxWpM&1}qQ^j&d6)*!uD}=s zs<<2Q5CV1ba6X8Y^Eqw=gi3&I7pMu5KnDv(27E{0>j;#g5Lkmiy&*h|o7D!$Q~?_o za@=*$goTT=5d}`oLmK$mz>RPn(Fkn@Ml8ns)BHt(_yvI!knrQzILrf-5qJZEDysvA zEKv)EJCEzF(S)wcR6QUpLZEWUIbMd*rKL;w*6Zpfwj8+Z4J=FW3*Nx`w^-FJmattR zmKhSFfx8e^fZh#^EmG-jzylh67)WocY=aEpZwQA$Hv-qKM2A2V&Rm6&13eqK34!*H zYM^Tgbq4#t8N>+2ab|y=f;P}JPrHE`YtaMn2{Yg2xQ?I+Zz0gANWlH2DqRL_LZG~a z-@XqI3gnLg$F9dzf*ub%xB;C6eHhp)e<PF*k>0=#A7IEpcLc6OptFE*%NCWt4XEF$ zR%Qm)Ae2MC4*2mlC@JW2VD5)V1Dyvvf<O%|2e$qQN{yxH1^fhoMlOFRh}XVAtC3*| zu=O5w{=9&X5SqYm0%jb9CL=Rp&qL@L=s2K=KozV2HXu;%?-KnrYy!yGAK|!Hzk$+$ zE(D%Lp#7j4=vj{Wr-)ZM#|=WDg3^E+5vZk`fxV8ZE$t0_x0>UQfxiye?~JN+0`Lz6 z3H%4Z?6Ybk2o2}Z0r2C1N#`+@(0oFl3y=ftdm$e~ga|E(a^P#<p>?2_03RWcwc~mP zs{nzPUmb8-tx9JDx$iLrkS9Ega2<3FP)4AU;I5&;*P(Ra6V?)qP7-$g0UK6661st? z_z{JY5HS9Rx+(PrE<!M)AUMvs_YoAu1d;>v7_25ja&8h$=vapX2^A2AA`qW2QR5Si z)A)q>3$+O0T8$>$qtS$CH2MPYo<<Ye-Bc@Z2gYbLA-SulVnXtD5KTxfA)*OCQE35w zA#ABy0wMV-s0W1PhoB6EgEX3a5#(neK4GCs@BhGEH(2+vW3+CVN9~rnEz*|8Esk5= Yx8`jv*t%|KsqY)Q4*OHy&>gn>U(tnFn*aa+ diff --git a/src/pip/_vendor/distlib/wheel.py b/src/pip/_vendor/distlib/wheel.py index 0c8efad9ae0..bd179383ac9 100644 --- a/src/pip/_vendor/distlib/wheel.py +++ b/src/pip/_vendor/distlib/wheel.py @@ -684,7 +684,7 @@ def _get_dylib_cache(self): if cache is None: # Use native string to avoid issues on 2.x: see Python #20140. base = os.path.join(get_cache_base(), str('dylib-cache'), - sys.version[:3]) + '%s.%s' % sys.version_info[:2]) cache = Cache(base) return cache diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 21a2f5b48b1..a35cd841268 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -2,7 +2,7 @@ appdirs==1.4.3 CacheControl==0.12.6 colorama==0.4.3 contextlib2==0.6.0 -distlib==0.2.9.post0 +distlib==0.3.0 distro==1.4.0 html5lib==1.0.1 ipaddress==1.0.22 # Only needed on 2.6 and 2.7 From 9bce54e54a3bbf495c3f18060b6ea7427b14599d Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 21 Jan 2020 13:13:27 +0530 Subject: [PATCH 1165/3170] Upgrade ipaddress to 1.0.23 --- src/pip/_vendor/ipaddress.py | 5 +++-- src/pip/_vendor/vendor.txt | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pip/_vendor/ipaddress.py b/src/pip/_vendor/ipaddress.py index f2d0766842a..3e6f9e499c3 100644 --- a/src/pip/_vendor/ipaddress.py +++ b/src/pip/_vendor/ipaddress.py @@ -14,7 +14,7 @@ import itertools import struct -__version__ = '1.0.22' +__version__ = '1.0.23' # Compatibility functions _compat_int_types = (int,) @@ -1103,7 +1103,8 @@ def _is_subnet_of(a, b): try: # Always false if one is v4 and the other is v6. if a._version != b._version: - raise TypeError("%s and %s are not of the same version" (a, b)) + raise TypeError( + "%s and %s are not of the same version" % (a, b)) return (b.network_address <= a.network_address and b.broadcast_address >= a.broadcast_address) except AttributeError: diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index a35cd841268..b7da8b6eb1c 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -5,7 +5,7 @@ contextlib2==0.6.0 distlib==0.3.0 distro==1.4.0 html5lib==1.0.1 -ipaddress==1.0.22 # Only needed on 2.6 and 2.7 +ipaddress==1.0.23 # Only needed on 2.6 and 2.7 msgpack==0.6.2 packaging==20.0 pep517==0.7.0 From 7770dc204e49e82b6f7aa2c0ebc69dfa175376d4 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 21 Jan 2020 13:16:34 +0530 Subject: [PATCH 1166/3170] Upgrade pyparsing to 2.4.6 --- src/pip/_vendor/pyparsing.py | 280 +++++++++++++++++++++++++++++------ src/pip/_vendor/vendor.txt | 2 +- 2 files changed, 239 insertions(+), 43 deletions(-) diff --git a/src/pip/_vendor/pyparsing.py b/src/pip/_vendor/pyparsing.py index dc1594d9dac..c461d6eca82 100644 --- a/src/pip/_vendor/pyparsing.py +++ b/src/pip/_vendor/pyparsing.py @@ -95,8 +95,8 @@ namespace class """ -__version__ = "2.4.2" -__versionTime__ = "29 Jul 2019 02:58 UTC" +__version__ = "2.4.6" +__versionTime__ = "24 Dec 2019 04:27 UTC" __author__ = "Paul McGuire <ptmcg@users.sourceforge.net>" import string @@ -114,6 +114,7 @@ from operator import itemgetter import itertools from functools import wraps +from contextlib import contextmanager try: # Python 3 @@ -184,8 +185,15 @@ class SimpleNamespace: pass __diag__.warn_name_set_on_empty_Forward = False __diag__.warn_on_multiple_string_args_to_oneof = False __diag__.enable_debug_on_named_expressions = False +__diag__._all_names = [nm for nm in vars(__diag__) if nm.startswith("enable_") or nm.startswith("warn_")] + +def _enable_all_warnings(): + __diag__.warn_multiple_tokens_in_named_alternation = True + __diag__.warn_ungrouped_named_tokens_in_collection = True + __diag__.warn_name_set_on_empty_Forward = True + __diag__.warn_on_multiple_string_args_to_oneof = True +__diag__.enable_all_warnings = _enable_all_warnings -# ~ sys.stderr.write("testing pyparsing module, version %s, %s\n" % (__version__, __versionTime__)) __all__ = ['__version__', '__versionTime__', '__author__', '__compat__', '__diag__', 'And', 'CaselessKeyword', 'CaselessLiteral', 'CharsNotIn', 'Combine', 'Dict', 'Each', 'Empty', @@ -206,7 +214,7 @@ class SimpleNamespace: pass 'stringStart', 'traceParseAction', 'unicodeString', 'upcaseTokens', 'withAttribute', 'indentedBlock', 'originalTextFor', 'ungroup', 'infixNotation', 'locatedExpr', 'withClass', 'CloseMatch', 'tokenMap', 'pyparsing_common', 'pyparsing_unicode', 'unicode_set', - 'conditionAsParseAction', + 'conditionAsParseAction', 're', ] system_version = tuple(sys.version_info)[:3] @@ -2561,15 +2569,13 @@ def parseFile(self, file_or_filename, parseAll=False): raise exc def __eq__(self, other): - if isinstance(other, ParserElement): - if PY_3: - self is other or super(ParserElement, self).__eq__(other) - else: - return self is other or vars(self) == vars(other) + if self is other: + return True elif isinstance(other, basestring): return self.matches(other) - else: - return super(ParserElement, self) == other + elif isinstance(other, ParserElement): + return vars(self) == vars(other) + return False def __ne__(self, other): return not (self == other) @@ -3252,14 +3258,23 @@ class Regex(Token): If the given regex contains named groups (defined using ``(?P<name>...)``), these will be preserved as named parse results. + If instead of the Python stdlib re module you wish to use a different RE module + (such as the `regex` module), you can replace it by either building your + Regex object with a compiled RE that was compiled using regex: + Example:: realnum = Regex(r"[+-]?\d+\.\d*") date = Regex(r'(?P<year>\d{4})-(?P<month>\d\d?)-(?P<day>\d\d?)') # ref: https://stackoverflow.com/questions/267399/how-do-you-match-only-valid-roman-numerals-with-a-regular-expression roman = Regex(r"M{0,4}(CM|CD|D?{0,3})(XC|XL|L?X{0,3})(IX|IV|V?I{0,3})") + + # use regex module instead of stdlib re module to construct a Regex using + # a compiled regular expression + import regex + parser = pp.Regex(regex.compile(r'[0-9]')) + """ - compiledREtype = type(re.compile("[A-Z]")) def __init__(self, pattern, flags=0, asGroupList=False, asMatch=False): """The parameters ``pattern`` and ``flags`` are passed to the ``re.compile()`` function as-is. See the Python @@ -3284,13 +3299,13 @@ def __init__(self, pattern, flags=0, asGroupList=False, asMatch=False): SyntaxWarning, stacklevel=2) raise - elif isinstance(pattern, Regex.compiledREtype): + elif hasattr(pattern, 'pattern') and hasattr(pattern, 'match'): self.re = pattern - self.pattern = self.reString = str(pattern) + self.pattern = self.reString = pattern.pattern self.flags = flags else: - raise ValueError("Regex may only be constructed with a string or a compiled RE object") + raise TypeError("Regex may only be constructed with a string or a compiled RE object") self.re_match = self.re.match @@ -3617,24 +3632,24 @@ class White(Token): '\n': '<LF>', '\r': '<CR>', '\f': '<FF>', - 'u\00A0': '<NBSP>', - 'u\1680': '<OGHAM_SPACE_MARK>', - 'u\180E': '<MONGOLIAN_VOWEL_SEPARATOR>', - 'u\2000': '<EN_QUAD>', - 'u\2001': '<EM_QUAD>', - 'u\2002': '<EN_SPACE>', - 'u\2003': '<EM_SPACE>', - 'u\2004': '<THREE-PER-EM_SPACE>', - 'u\2005': '<FOUR-PER-EM_SPACE>', - 'u\2006': '<SIX-PER-EM_SPACE>', - 'u\2007': '<FIGURE_SPACE>', - 'u\2008': '<PUNCTUATION_SPACE>', - 'u\2009': '<THIN_SPACE>', - 'u\200A': '<HAIR_SPACE>', - 'u\200B': '<ZERO_WIDTH_SPACE>', - 'u\202F': '<NNBSP>', - 'u\205F': '<MMSP>', - 'u\3000': '<IDEOGRAPHIC_SPACE>', + u'\u00A0': '<NBSP>', + u'\u1680': '<OGHAM_SPACE_MARK>', + u'\u180E': '<MONGOLIAN_VOWEL_SEPARATOR>', + u'\u2000': '<EN_QUAD>', + u'\u2001': '<EM_QUAD>', + u'\u2002': '<EN_SPACE>', + u'\u2003': '<EM_SPACE>', + u'\u2004': '<THREE-PER-EM_SPACE>', + u'\u2005': '<FOUR-PER-EM_SPACE>', + u'\u2006': '<SIX-PER-EM_SPACE>', + u'\u2007': '<FIGURE_SPACE>', + u'\u2008': '<PUNCTUATION_SPACE>', + u'\u2009': '<THIN_SPACE>', + u'\u200A': '<HAIR_SPACE>', + u'\u200B': '<ZERO_WIDTH_SPACE>', + u'\u202F': '<NNBSP>', + u'\u205F': '<MMSP>', + u'\u3000': '<IDEOGRAPHIC_SPACE>', } def __init__(self, ws=" \t\r\n", min=1, max=0, exact=0): super(White, self).__init__() @@ -4566,6 +4581,7 @@ def __init__(self, expr, retreat=None): self.retreat = retreat self.errmsg = "not preceded by " + str(expr) self.skipWhitespace = False + self.parseAction.append(lambda s, l, t: t.__delitem__(slice(None, None))) def parseImpl(self, instring, loc=0, doActions=True): if self.exact: @@ -4576,19 +4592,18 @@ def parseImpl(self, instring, loc=0, doActions=True): else: # retreat specified a maximum lookbehind window, iterate test_expr = self.expr + StringEnd() - instring_slice = instring[:loc] + instring_slice = instring[max(0, loc - self.retreat):loc] last_expr = ParseException(instring, loc, self.errmsg) - for offset in range(1, min(loc, self.retreat + 1)): + for offset in range(1, min(loc, self.retreat + 1)+1): try: - _, ret = test_expr._parse(instring_slice, loc - offset) + # print('trying', offset, instring_slice, repr(instring_slice[loc - offset:])) + _, ret = test_expr._parse(instring_slice, len(instring_slice) - offset) except ParseBaseException as pbe: last_expr = pbe else: break else: raise last_expr - # return empty list of tokens, but preserve any defined results names - del ret[:] return loc, ret @@ -6051,7 +6066,7 @@ def parseImpl(self, instring, loc, doActions=True): matchExpr = _FB(lastExpr + lastExpr) + Group(lastExpr + OneOrMore(lastExpr)) elif arity == 3: matchExpr = (_FB(lastExpr + opExpr1 + lastExpr + opExpr2 + lastExpr) - + Group(lastExpr + opExpr1 + lastExpr + opExpr2 + lastExpr)) + + Group(lastExpr + OneOrMore(opExpr1 + lastExpr + opExpr2 + lastExpr))) else: raise ValueError("operator must be unary (1), binary (2), or ternary (3)") elif rightLeftAssoc == opAssoc.RIGHT: @@ -6305,18 +6320,18 @@ def checkUnindent(s, l, t): if curCol < indentStack[-1]: indentStack.pop() - NL = OneOrMore(LineEnd().setWhitespaceChars("\t ").suppress()) + NL = OneOrMore(LineEnd().setWhitespaceChars("\t ").suppress(), stopOn=StringEnd()) INDENT = (Empty() + Empty().setParseAction(checkSubIndent)).setName('INDENT') PEER = Empty().setParseAction(checkPeerIndent).setName('') UNDENT = Empty().setParseAction(checkUnindent).setName('UNINDENT') if indent: smExpr = Group(Optional(NL) + INDENT - + OneOrMore(PEER + Group(blockStatementExpr) + Optional(NL)) + + OneOrMore(PEER + Group(blockStatementExpr) + Optional(NL), stopOn=StringEnd()) + UNDENT) else: smExpr = Group(Optional(NL) - + OneOrMore(PEER + Group(blockStatementExpr) + Optional(NL)) + + OneOrMore(PEER + Group(blockStatementExpr) + Optional(NL), stopOn=StringEnd()) + UNDENT) smExpr.setFailAction(lambda a, b, c, d: reset_stack()) blockStatementExpr.ignore(_bslash + LineEnd()) @@ -6822,6 +6837,187 @@ class Devanagari(unicode_set): setattr(pyparsing_unicode, u"देवनागरी", pyparsing_unicode.Devanagari) +class pyparsing_test: + """ + namespace class for classes useful in writing unit tests + """ + + class reset_pyparsing_context: + """ + Context manager to be used when writing unit tests that modify pyparsing config values: + - packrat parsing + - default whitespace characters. + - default keyword characters + - literal string auto-conversion class + - __diag__ settings + + Example: + with reset_pyparsing_context(): + # test that literals used to construct a grammar are automatically suppressed + ParserElement.inlineLiteralsUsing(Suppress) + + term = Word(alphas) | Word(nums) + group = Group('(' + term[...] + ')') + + # assert that the '()' characters are not included in the parsed tokens + self.assertParseAndCheckLisst(group, "(abc 123 def)", ['abc', '123', 'def']) + + # after exiting context manager, literals are converted to Literal expressions again + """ + + def __init__(self): + self._save_context = {} + + def save(self): + self._save_context["default_whitespace"] = ParserElement.DEFAULT_WHITE_CHARS + self._save_context["default_keyword_chars"] = Keyword.DEFAULT_KEYWORD_CHARS + self._save_context[ + "literal_string_class" + ] = ParserElement._literalStringClass + self._save_context["packrat_enabled"] = ParserElement._packratEnabled + self._save_context["packrat_parse"] = ParserElement._parse + self._save_context["__diag__"] = { + name: getattr(__diag__, name) for name in __diag__._all_names + } + self._save_context["__compat__"] = { + "collect_all_And_tokens": __compat__.collect_all_And_tokens + } + return self + + def restore(self): + # reset pyparsing global state + if ( + ParserElement.DEFAULT_WHITE_CHARS + != self._save_context["default_whitespace"] + ): + ParserElement.setDefaultWhitespaceChars( + self._save_context["default_whitespace"] + ) + Keyword.DEFAULT_KEYWORD_CHARS = self._save_context["default_keyword_chars"] + ParserElement.inlineLiteralsUsing( + self._save_context["literal_string_class"] + ) + for name, value in self._save_context["__diag__"].items(): + setattr(__diag__, name, value) + ParserElement._packratEnabled = self._save_context["packrat_enabled"] + ParserElement._parse = self._save_context["packrat_parse"] + __compat__.collect_all_And_tokens = self._save_context["__compat__"] + + def __enter__(self): + return self.save() + + def __exit__(self, *args): + return self.restore() + + class TestParseResultsAsserts: + """ + A mixin class to add parse results assertion methods to normal unittest.TestCase classes. + """ + def assertParseResultsEquals( + self, result, expected_list=None, expected_dict=None, msg=None + ): + """ + Unit test assertion to compare a ParseResults object with an optional expected_list, + and compare any defined results names with an optional expected_dict. + """ + if expected_list is not None: + self.assertEqual(expected_list, result.asList(), msg=msg) + if expected_dict is not None: + self.assertEqual(expected_dict, result.asDict(), msg=msg) + + def assertParseAndCheckList( + self, expr, test_string, expected_list, msg=None, verbose=True + ): + """ + Convenience wrapper assert to test a parser element and input string, and assert that + the resulting ParseResults.asList() is equal to the expected_list. + """ + result = expr.parseString(test_string, parseAll=True) + if verbose: + print(result.dump()) + self.assertParseResultsEquals(result, expected_list=expected_list, msg=msg) + + def assertParseAndCheckDict( + self, expr, test_string, expected_dict, msg=None, verbose=True + ): + """ + Convenience wrapper assert to test a parser element and input string, and assert that + the resulting ParseResults.asDict() is equal to the expected_dict. + """ + result = expr.parseString(test_string, parseAll=True) + if verbose: + print(result.dump()) + self.assertParseResultsEquals(result, expected_dict=expected_dict, msg=msg) + + def assertRunTestResults( + self, run_tests_report, expected_parse_results=None, msg=None + ): + """ + Unit test assertion to evaluate output of ParserElement.runTests(). If a list of + list-dict tuples is given as the expected_parse_results argument, then these are zipped + with the report tuples returned by runTests and evaluated using assertParseResultsEquals. + Finally, asserts that the overall runTests() success value is True. + + :param run_tests_report: tuple(bool, [tuple(str, ParseResults or Exception)]) returned from runTests + :param expected_parse_results (optional): [tuple(str, list, dict, Exception)] + """ + run_test_success, run_test_results = run_tests_report + + if expected_parse_results is not None: + merged = [ + (rpt[0], rpt[1], expected) + for rpt, expected in zip(run_test_results, expected_parse_results) + ] + for test_string, result, expected in merged: + # expected should be a tuple containing a list and/or a dict or an exception, + # and optional failure message string + # an empty tuple will skip any result validation + fail_msg = next( + (exp for exp in expected if isinstance(exp, str)), None + ) + expected_exception = next( + ( + exp + for exp in expected + if isinstance(exp, type) and issubclass(exp, Exception) + ), + None, + ) + if expected_exception is not None: + with self.assertRaises( + expected_exception=expected_exception, msg=fail_msg or msg + ): + if isinstance(result, Exception): + raise result + else: + expected_list = next( + (exp for exp in expected if isinstance(exp, list)), None + ) + expected_dict = next( + (exp for exp in expected if isinstance(exp, dict)), None + ) + if (expected_list, expected_dict) != (None, None): + self.assertParseResultsEquals( + result, + expected_list=expected_list, + expected_dict=expected_dict, + msg=fail_msg or msg, + ) + else: + # warning here maybe? + print("no validation for {!r}".format(test_string)) + + # do this last, in case some specific test results can be reported instead + self.assertTrue( + run_test_success, msg=msg if msg is not None else "failed runTests" + ) + + @contextmanager + def assertRaisesParseException(self, exc_type=ParseException, msg=None): + with self.assertRaises(exc_type, msg=msg): + yield + + if __name__ == "__main__": selectToken = CaselessLiteral("select") diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index b7da8b6eb1c..39cb1cf3181 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -10,7 +10,7 @@ msgpack==0.6.2 packaging==20.0 pep517==0.7.0 progress==1.5 -pyparsing==2.4.2 +pyparsing==2.4.6 pytoml==0.1.21 requests==2.22.0 certifi==2019.9.11 From 022f0e265bf530dbef4a9584be88f9aabcbee850 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 21 Jan 2020 13:18:25 +0530 Subject: [PATCH 1167/3170] Upgrade certifi to 2019.11.28 --- src/pip/_vendor/certifi/__init__.py | 2 +- src/pip/_vendor/certifi/cacert.pem | 44 +++++++++++++++++++++++++++++ src/pip/_vendor/vendor.txt | 2 +- 3 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/pip/_vendor/certifi/__init__.py b/src/pip/_vendor/certifi/__init__.py index 8e358e4c8f8..0d59a05630e 100644 --- a/src/pip/_vendor/certifi/__init__.py +++ b/src/pip/_vendor/certifi/__init__.py @@ -1,3 +1,3 @@ from .core import where -__version__ = "2019.09.11" +__version__ = "2019.11.28" diff --git a/src/pip/_vendor/certifi/cacert.pem b/src/pip/_vendor/certifi/cacert.pem index 70fa91f6181..a4758ef3afb 100644 --- a/src/pip/_vendor/certifi/cacert.pem +++ b/src/pip/_vendor/certifi/cacert.pem @@ -4556,3 +4556,47 @@ L5/ndtFhKvshuzHQqp9HpLIiyhY6UFfEW0NnxWViA0kB60PZ2Pierc+xYw5F9KBa LJstxabArahH9CdMOA0uG0k7UvToiIMrVCjU8jVStDKDYmlkDJGcn5fqdBb9HxEG mpv0 -----END CERTIFICATE----- + +# Issuer: CN=Entrust Root Certification Authority - G4 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2015 Entrust, Inc. - for authorized use only +# Subject: CN=Entrust Root Certification Authority - G4 O=Entrust, Inc. OU=See www.entrust.net/legal-terms/(c) 2015 Entrust, Inc. - for authorized use only +# Label: "Entrust Root Certification Authority - G4" +# Serial: 289383649854506086828220374796556676440 +# MD5 Fingerprint: 89:53:f1:83:23:b7:7c:8e:05:f1:8c:71:38:4e:1f:88 +# SHA1 Fingerprint: 14:88:4e:86:26:37:b0:26:af:59:62:5c:40:77:ec:35:29:ba:96:01 +# SHA256 Fingerprint: db:35:17:d1:f6:73:2a:2d:5a:b9:7c:53:3e:c7:07:79:ee:32:70:a6:2f:b4:ac:42:38:37:24:60:e6:f0:1e:88 +-----BEGIN CERTIFICATE----- +MIIGSzCCBDOgAwIBAgIRANm1Q3+vqTkPAAAAAFVlrVgwDQYJKoZIhvcNAQELBQAw +gb4xCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQL +Ex9TZWUgd3d3LmVudHJ1c3QubmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykg +MjAxNSBFbnRydXN0LCBJbmMuIC0gZm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMjAw +BgNVBAMTKUVudHJ1c3QgUm9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEc0 +MB4XDTE1MDUyNzExMTExNloXDTM3MTIyNzExNDExNlowgb4xCzAJBgNVBAYTAlVT +MRYwFAYDVQQKEw1FbnRydXN0LCBJbmMuMSgwJgYDVQQLEx9TZWUgd3d3LmVudHJ1 +c3QubmV0L2xlZ2FsLXRlcm1zMTkwNwYDVQQLEzAoYykgMjAxNSBFbnRydXN0LCBJ +bmMuIC0gZm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxMjAwBgNVBAMTKUVudHJ1c3Qg +Um9vdCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eSAtIEc0MIICIjANBgkqhkiG9w0B +AQEFAAOCAg8AMIICCgKCAgEAsewsQu7i0TD/pZJH4i3DumSXbcr3DbVZwbPLqGgZ +2K+EbTBwXX7zLtJTmeH+H17ZSK9dE43b/2MzTdMAArzE+NEGCJR5WIoV3imz/f3E +T+iq4qA7ec2/a0My3dl0ELn39GjUu9CH1apLiipvKgS1sqbHoHrmSKvS0VnM1n4j +5pds8ELl3FFLFUHtSUrJ3hCX1nbB76W1NhSXNdh4IjVS70O92yfbYVaCNNzLiGAM +C1rlLAHGVK/XqsEQe9IFWrhAnoanw5CGAlZSCXqc0ieCU0plUmr1POeo8pyvi73T +DtTUXm6Hnmo9RR3RXRv06QqsYJn7ibT/mCzPfB3pAqoEmh643IhuJbNsZvc8kPNX +wbMv9W3y+8qh+CmdRouzavbmZwe+LGcKKh9asj5XxNMhIWNlUpEbsZmOeX7m640A +2Vqq6nPopIICR5b+W45UYaPrL0swsIsjdXJ8ITzI9vF01Bx7owVV7rtNOzK+mndm +nqxpkCIHH2E6lr7lmk/MBTwoWdPBDFSoWWG9yHJM6Nyfh3+9nEg2XpWjDrk4JFX8 +dWbrAuMINClKxuMrLzOg2qOGpRKX/YAr2hRC45K9PvJdXmd0LhyIRyk0X+IyqJwl +N4y6mACXi0mWHv0liqzc2thddG5msP9E36EYxr5ILzeUePiVSj9/E15dWf10hkNj +c0kCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYD +VR0OBBYEFJ84xFYjwznooHFs6FRM5Og6sb9nMA0GCSqGSIb3DQEBCwUAA4ICAQAS +5UKme4sPDORGpbZgQIeMJX6tuGguW8ZAdjwD+MlZ9POrYs4QjbRaZIxowLByQzTS +Gwv2LFPSypBLhmb8qoMi9IsabyZIrHZ3CL/FmFz0Jomee8O5ZDIBf9PD3Vht7LGr +hFV0d4QEJ1JrhkzO3bll/9bGXp+aEJlLdWr+aumXIOTkdnrG0CSqkM0gkLpHZPt/ +B7NTeLUKYvJzQ85BK4FqLoUWlFPUa19yIqtRLULVAJyZv967lDtX/Zr1hstWO1uI +AeV8KEsD+UmDfLJ/fOPtjqF/YFOOVZ1QNBIPt5d7bIdKROf1beyAN/BYGW5KaHbw +H5Lk6rWS02FREAutp9lfx1/cH6NcjKF+m7ee01ZvZl4HliDtC3T7Zk6LERXpgUl+ +b7DUUH8i119lAg2m9IUe2K4GS0qn0jFmwvjO5QimpAKWRGhXxNUzzxkvFMSUHHuk +2fCfDrGA4tGeEWSpiBE6doLlYsKA2KSD7ZPvfC+QsDJMlhVoSFLUmQjAJOgc47Ol +IQ6SwJAfzyBfyjs4x7dtOvPmRLgOMWuIjnDrnBdSqEGULoe256YSxXXfW8AKbnuk +5F6G+TaU33fD6Q3AOfF5u0aOq0NZJ7cguyPpVkAh7DE9ZapD8j3fcEThuk0mEDuY +n/PIjhs4ViFqUZPTkcpG2om3PVODLAgfi49T3f+sHw== +-----END CERTIFICATE----- diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 39cb1cf3181..8a5e7480812 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -13,7 +13,7 @@ progress==1.5 pyparsing==2.4.6 pytoml==0.1.21 requests==2.22.0 - certifi==2019.9.11 + certifi==2019.11.28 chardet==3.0.4 idna==2.8 urllib3==1.25.6 From 501e00cec4978ae332761f27df945f95f982a384 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 21 Jan 2020 13:19:56 +0530 Subject: [PATCH 1168/3170] Upgrade urllib3 to 1.25.7 --- src/pip/_vendor/urllib3/__init__.py | 2 +- src/pip/_vendor/urllib3/connection.py | 4 ++-- src/pip/_vendor/urllib3/connectionpool.py | 14 +++++------ .../urllib3/contrib/_appengine_environ.py | 24 +++++++++++-------- .../contrib/_securetransport/bindings.py | 1 + src/pip/_vendor/urllib3/contrib/appengine.py | 7 ------ src/pip/_vendor/urllib3/contrib/ntlmpool.py | 4 +--- .../urllib3/contrib/securetransport.py | 19 ++++----------- src/pip/_vendor/urllib3/exceptions.py | 2 +- .../ssl_match_hostname/_implementation.py | 6 ++--- src/pip/_vendor/urllib3/util/connection.py | 2 +- src/pip/_vendor/urllib3/util/request.py | 4 ++-- src/pip/_vendor/urllib3/util/timeout.py | 2 +- src/pip/_vendor/urllib3/util/url.py | 7 ++---- src/pip/_vendor/vendor.txt | 2 +- 15 files changed, 40 insertions(+), 60 deletions(-) diff --git a/src/pip/_vendor/urllib3/__init__.py b/src/pip/_vendor/urllib3/__init__.py index 8f5a21f3462..96474d3680c 100644 --- a/src/pip/_vendor/urllib3/__init__.py +++ b/src/pip/_vendor/urllib3/__init__.py @@ -22,7 +22,7 @@ __author__ = "Andrey Petrov (andrey.petrov@shazow.net)" __license__ = "MIT" -__version__ = "1.25.6" +__version__ = "1.25.7" __all__ = ( "HTTPConnectionPool", diff --git a/src/pip/_vendor/urllib3/connection.py b/src/pip/_vendor/urllib3/connection.py index 3eeb1af58ed..f5c946adf77 100644 --- a/src/pip/_vendor/urllib3/connection.py +++ b/src/pip/_vendor/urllib3/connection.py @@ -412,7 +412,7 @@ def connect(self): ( "Certificate for {0} has no `subjectAltName`, falling back to check for a " "`commonName` for now. This feature is being removed by major browsers and " - "deprecated by RFC 2818. (See https://github.com/shazow/urllib3/issues/497 " + "deprecated by RFC 2818. (See https://github.com/urllib3/urllib3/issues/497 " "for details.)".format(hostname) ), SubjectAltNameWarning, @@ -430,7 +430,7 @@ def _match_hostname(cert, asserted_hostname): match_hostname(cert, asserted_hostname) except CertificateError as e: log.warning( - "Certificate did not match expected hostname: %s. " "Certificate: %s", + "Certificate did not match expected hostname: %s. Certificate: %s", asserted_hostname, cert, ) diff --git a/src/pip/_vendor/urllib3/connectionpool.py b/src/pip/_vendor/urllib3/connectionpool.py index e73fa57a427..31696460f08 100644 --- a/src/pip/_vendor/urllib3/connectionpool.py +++ b/src/pip/_vendor/urllib3/connectionpool.py @@ -257,7 +257,7 @@ def _get_conn(self, timeout=None): if self.block: raise EmptyPoolError( self, - "Pool reached maximum size and no more " "connections are allowed.", + "Pool reached maximum size and no more connections are allowed.", ) pass # Oh well, we'll create a new connection then @@ -626,7 +626,7 @@ def urlopen( # # See issue #651 [1] for details. # - # [1] <https://github.com/shazow/urllib3/issues/651> + # [1] <https://github.com/urllib3/urllib3/issues/651> release_this_conn = release_conn # Merge the proxy headers. Only do this in HTTP. We have to copy the @@ -742,10 +742,7 @@ def urlopen( if not conn: # Try again log.warning( - "Retrying (%r) after connection " "broken by '%r': %s", - retries, - err, - url, + "Retrying (%r) after connection broken by '%r': %s", retries, err, url ) return self.urlopen( method, @@ -758,6 +755,7 @@ def urlopen( timeout=timeout, pool_timeout=pool_timeout, release_conn=release_conn, + chunked=chunked, body_pos=body_pos, **response_kw ) @@ -809,6 +807,7 @@ def drain_and_release_conn(response): timeout=timeout, pool_timeout=pool_timeout, release_conn=release_conn, + chunked=chunked, body_pos=body_pos, **response_kw ) @@ -842,6 +841,7 @@ def drain_and_release_conn(response): timeout=timeout, pool_timeout=pool_timeout, release_conn=release_conn, + chunked=chunked, body_pos=body_pos, **response_kw ) @@ -961,7 +961,7 @@ def _new_conn(self): if not self.ConnectionCls or self.ConnectionCls is DummyConnection: raise SSLError( - "Can't connect to HTTPS URL because the SSL " "module is not available." + "Can't connect to HTTPS URL because the SSL module is not available." ) actual_host = self.host diff --git a/src/pip/_vendor/urllib3/contrib/_appengine_environ.py b/src/pip/_vendor/urllib3/contrib/_appengine_environ.py index c909010bf27..119efaeeb67 100644 --- a/src/pip/_vendor/urllib3/contrib/_appengine_environ.py +++ b/src/pip/_vendor/urllib3/contrib/_appengine_environ.py @@ -6,27 +6,31 @@ def is_appengine(): - return is_local_appengine() or is_prod_appengine() or is_prod_appengine_mvms() + return "APPENGINE_RUNTIME" in os.environ def is_appengine_sandbox(): - return is_appengine() and not is_prod_appengine_mvms() + """Reports if the app is running in the first generation sandbox. + + The second generation runtimes are technically still in a sandbox, but it + is much less restrictive, so generally you shouldn't need to check for it. + see https://cloud.google.com/appengine/docs/standard/runtimes + """ + return is_appengine() and os.environ["APPENGINE_RUNTIME"] == "python27" def is_local_appengine(): - return ( - "APPENGINE_RUNTIME" in os.environ - and "Development/" in os.environ["SERVER_SOFTWARE"] + return is_appengine() and os.environ.get("SERVER_SOFTWARE", "").startswith( + "Development/" ) def is_prod_appengine(): - return ( - "APPENGINE_RUNTIME" in os.environ - and "Google App Engine/" in os.environ["SERVER_SOFTWARE"] - and not is_prod_appengine_mvms() + return is_appengine() and os.environ.get("SERVER_SOFTWARE", "").startswith( + "Google App Engine/" ) def is_prod_appengine_mvms(): - return os.environ.get("GAE_VM", False) == "true" + """Deprecated.""" + return False diff --git a/src/pip/_vendor/urllib3/contrib/_securetransport/bindings.py b/src/pip/_vendor/urllib3/contrib/_securetransport/bindings.py index b46e1e3b5de..d9b67333188 100644 --- a/src/pip/_vendor/urllib3/contrib/_securetransport/bindings.py +++ b/src/pip/_vendor/urllib3/contrib/_securetransport/bindings.py @@ -415,6 +415,7 @@ class SecurityConst(object): kTLSProtocol1 = 4 kTLSProtocol11 = 7 kTLSProtocol12 = 8 + # SecureTransport does not support TLS 1.3 even if there's a constant for it kTLSProtocol13 = 10 kTLSProtocolMaxSupported = 999 diff --git a/src/pip/_vendor/urllib3/contrib/appengine.py b/src/pip/_vendor/urllib3/contrib/appengine.py index 886b3a7bc0f..d09d2be645a 100644 --- a/src/pip/_vendor/urllib3/contrib/appengine.py +++ b/src/pip/_vendor/urllib3/contrib/appengine.py @@ -108,13 +108,6 @@ def __init__( "URLFetch is not available in this environment." ) - if is_prod_appengine_mvms(): - raise AppEnginePlatformError( - "Use normal urllib3.PoolManager instead of AppEngineManager" - "on Managed VMs, as using URLFetch is not necessary in " - "this environment." - ) - warnings.warn( "urllib3 is using URLFetch on Google App Engine sandbox instead " "of sockets. To use sockets directly instead of URLFetch see " diff --git a/src/pip/_vendor/urllib3/contrib/ntlmpool.py b/src/pip/_vendor/urllib3/contrib/ntlmpool.py index 9c96be29d8a..1fd242a6e0d 100644 --- a/src/pip/_vendor/urllib3/contrib/ntlmpool.py +++ b/src/pip/_vendor/urllib3/contrib/ntlmpool.py @@ -96,9 +96,7 @@ def _new_conn(self): log.debug("Response data: %s [...]", res.read()[:100]) if res.status != 200: if res.status == 401: - raise Exception( - "Server rejected request: wrong " "username or password" - ) + raise Exception("Server rejected request: wrong username or password") raise Exception("Wrong server response: %s %s" % (res.status, res.reason)) res.fp = None diff --git a/src/pip/_vendor/urllib3/contrib/securetransport.py b/src/pip/_vendor/urllib3/contrib/securetransport.py index 24e6b5c4d9e..87d844afa78 100644 --- a/src/pip/_vendor/urllib3/contrib/securetransport.py +++ b/src/pip/_vendor/urllib3/contrib/securetransport.py @@ -144,13 +144,10 @@ ] # Basically this is simple: for PROTOCOL_SSLv23 we turn it into a low of -# TLSv1 and a high of TLSv1.3. For everything else, we pin to that version. -# TLSv1 to 1.2 are supported on macOS 10.8+ and TLSv1.3 is macOS 10.13+ +# TLSv1 and a high of TLSv1.2. For everything else, we pin to that version. +# TLSv1 to 1.2 are supported on macOS 10.8+ _protocol_to_min_max = { - util.PROTOCOL_TLS: ( - SecurityConst.kTLSProtocol1, - SecurityConst.kTLSProtocolMaxSupported, - ) + util.PROTOCOL_TLS: (SecurityConst.kTLSProtocol1, SecurityConst.kTLSProtocol12) } if hasattr(ssl, "PROTOCOL_SSLv2"): @@ -488,15 +485,7 @@ def handshake( result = Security.SSLSetProtocolVersionMin(self.context, min_version) _assert_no_error(result) - # TLS 1.3 isn't necessarily enabled by the OS - # so we have to detect when we error out and try - # setting TLS 1.3 if it's allowed. kTLSProtocolMaxSupported - # was added in macOS 10.13 along with kTLSProtocol13. result = Security.SSLSetProtocolVersionMax(self.context, max_version) - if result != 0 and max_version == SecurityConst.kTLSProtocolMaxSupported: - result = Security.SSLSetProtocolVersionMax( - self.context, SecurityConst.kTLSProtocol12 - ) _assert_no_error(result) # If there's a trust DB, we need to use it. We do that by telling @@ -707,7 +696,7 @@ def version(self): ) _assert_no_error(result) if protocol.value == SecurityConst.kTLSProtocol13: - return "TLSv1.3" + raise ssl.SSLError("SecureTransport does not support TLS 1.3") elif protocol.value == SecurityConst.kTLSProtocol12: return "TLSv1.2" elif protocol.value == SecurityConst.kTLSProtocol11: diff --git a/src/pip/_vendor/urllib3/exceptions.py b/src/pip/_vendor/urllib3/exceptions.py index 93d93fba7d1..0a74c79b5ea 100644 --- a/src/pip/_vendor/urllib3/exceptions.py +++ b/src/pip/_vendor/urllib3/exceptions.py @@ -222,7 +222,7 @@ def __init__(self, partial, expected): super(IncompleteRead, self).__init__(partial, expected) def __repr__(self): - return "IncompleteRead(%i bytes read, " "%i more expected)" % ( + return "IncompleteRead(%i bytes read, %i more expected)" % ( self.partial, self.expected, ) diff --git a/src/pip/_vendor/urllib3/packages/ssl_match_hostname/_implementation.py b/src/pip/_vendor/urllib3/packages/ssl_match_hostname/_implementation.py index 89543225241..5831c2e01d6 100644 --- a/src/pip/_vendor/urllib3/packages/ssl_match_hostname/_implementation.py +++ b/src/pip/_vendor/urllib3/packages/ssl_match_hostname/_implementation.py @@ -153,10 +153,8 @@ def match_hostname(cert, hostname): "doesn't match either of %s" % (hostname, ", ".join(map(repr, dnsnames))) ) elif len(dnsnames) == 1: - raise CertificateError( - "hostname %r " "doesn't match %r" % (hostname, dnsnames[0]) - ) + raise CertificateError("hostname %r doesn't match %r" % (hostname, dnsnames[0])) else: raise CertificateError( - "no appropriate commonName or " "subjectAltName fields were found" + "no appropriate commonName or subjectAltName fields were found" ) diff --git a/src/pip/_vendor/urllib3/util/connection.py b/src/pip/_vendor/urllib3/util/connection.py index 0e1112628e5..86f0a3b00ed 100644 --- a/src/pip/_vendor/urllib3/util/connection.py +++ b/src/pip/_vendor/urllib3/util/connection.py @@ -121,7 +121,7 @@ def _has_ipv6(host): # has_ipv6 returns true if cPython was compiled with IPv6 support. # It does not tell us if the system has IPv6 support enabled. To # determine that we must bind to an IPv6 address. - # https://github.com/shazow/urllib3/pull/611 + # https://github.com/urllib3/urllib3/pull/611 # https://bugs.python.org/issue658327 try: sock = socket.socket(socket.AF_INET6) diff --git a/src/pip/_vendor/urllib3/util/request.py b/src/pip/_vendor/urllib3/util/request.py index 262a6d61854..3b7bb54dafb 100644 --- a/src/pip/_vendor/urllib3/util/request.py +++ b/src/pip/_vendor/urllib3/util/request.py @@ -122,7 +122,7 @@ def rewind_body(body, body_pos): body_seek(body_pos) except (IOError, OSError): raise UnrewindableBodyError( - "An error occurred when rewinding request " "body for redirect/retry." + "An error occurred when rewinding request body for redirect/retry." ) elif body_pos is _FAILEDTELL: raise UnrewindableBodyError( @@ -131,5 +131,5 @@ def rewind_body(body, body_pos): ) else: raise ValueError( - "body_pos must be of type integer, " "instead it was %s." % type(body_pos) + "body_pos must be of type integer, instead it was %s." % type(body_pos) ) diff --git a/src/pip/_vendor/urllib3/util/timeout.py b/src/pip/_vendor/urllib3/util/timeout.py index c1dc1e97126..9883700556e 100644 --- a/src/pip/_vendor/urllib3/util/timeout.py +++ b/src/pip/_vendor/urllib3/util/timeout.py @@ -203,7 +203,7 @@ def get_connect_duration(self): """ if self._start_connect is None: raise TimeoutStateError( - "Can't get connect duration for timer " "that has not started." + "Can't get connect duration for timer that has not started." ) return current_time() - self._start_connect diff --git a/src/pip/_vendor/urllib3/util/url.py b/src/pip/_vendor/urllib3/util/url.py index 5fe37a72dfe..12600626170 100644 --- a/src/pip/_vendor/urllib3/util/url.py +++ b/src/pip/_vendor/urllib3/util/url.py @@ -55,7 +55,7 @@ ZONE_ID_PAT = "(?:%25|%)(?:[" + UNRESERVED_PAT + "]|%[a-fA-F0-9]{2})+" IPV6_ADDRZ_PAT = r"\[" + IPV6_PAT + r"(?:" + ZONE_ID_PAT + r")?\]" REG_NAME_PAT = r"(?:[^\[\]%:/?#]|%[a-fA-F0-9]{2})*" -TARGET_RE = re.compile(r"^(/[^?]*)(?:\?([^#]+))?(?:#(.*))?$") +TARGET_RE = re.compile(r"^(/[^?#]*)(?:\?([^#]*))?(?:#.*)?$") IPV4_RE = re.compile("^" + IPV4_PAT + "$") IPV6_RE = re.compile("^" + IPV6_PAT + "$") @@ -325,14 +325,11 @@ def _encode_target(target): if not target.startswith("/"): return target - path, query, fragment = TARGET_RE.match(target).groups() + path, query = TARGET_RE.match(target).groups() target = _encode_invalid_chars(path, PATH_CHARS) query = _encode_invalid_chars(query, QUERY_CHARS) - fragment = _encode_invalid_chars(fragment, FRAGMENT_CHARS) if query is not None: target += "?" + query - if fragment is not None: - target += "#" + target return target diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 8a5e7480812..23609c2885c 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -16,7 +16,7 @@ requests==2.22.0 certifi==2019.11.28 chardet==3.0.4 idna==2.8 - urllib3==1.25.6 + urllib3==1.25.7 retrying==1.3.3 setuptools==41.4.0 six==1.12.0 From 947fceb0f9ea9cca1fd716cdb95e145e3c171d44 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 21 Jan 2020 13:26:58 +0530 Subject: [PATCH 1169/3170] Upgrade six to 1.14.0 --- src/pip/_vendor/six.LICENSE | 2 +- src/pip/_vendor/six.py | 70 ++++++++++++++++++++++++++----------- src/pip/_vendor/vendor.txt | 2 +- 3 files changed, 51 insertions(+), 23 deletions(-) diff --git a/src/pip/_vendor/six.LICENSE b/src/pip/_vendor/six.LICENSE index 365d10741bf..de6633112c1 100644 --- a/src/pip/_vendor/six.LICENSE +++ b/src/pip/_vendor/six.LICENSE @@ -1,4 +1,4 @@ -Copyright (c) 2010-2018 Benjamin Peterson +Copyright (c) 2010-2020 Benjamin Peterson 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 diff --git a/src/pip/_vendor/six.py b/src/pip/_vendor/six.py index 89b2188fd63..5fe9f8e141e 100644 --- a/src/pip/_vendor/six.py +++ b/src/pip/_vendor/six.py @@ -1,4 +1,4 @@ -# Copyright (c) 2010-2018 Benjamin Peterson +# Copyright (c) 2010-2020 Benjamin Peterson # # Permission is hereby granted, free of charge, to any person obtaining a copy # of this software and associated documentation files (the "Software"), to deal @@ -29,7 +29,7 @@ import types __author__ = "Benjamin Peterson <benjamin@python.org>" -__version__ = "1.12.0" +__version__ = "1.14.0" # Useful for very coarse version differentiation. @@ -255,9 +255,11 @@ class _MovedItems(_LazyModule): MovedAttribute("zip_longest", "itertools", "itertools", "izip_longest", "zip_longest"), MovedModule("builtins", "__builtin__"), MovedModule("configparser", "ConfigParser"), + MovedModule("collections_abc", "collections", "collections.abc" if sys.version_info >= (3, 3) else "collections"), MovedModule("copyreg", "copy_reg"), MovedModule("dbm_gnu", "gdbm", "dbm.gnu"), - MovedModule("_dummy_thread", "dummy_thread", "_dummy_thread"), + MovedModule("dbm_ndbm", "dbm", "dbm.ndbm"), + MovedModule("_dummy_thread", "dummy_thread", "_dummy_thread" if sys.version_info < (3, 9) else "_thread"), MovedModule("http_cookiejar", "cookielib", "http.cookiejar"), MovedModule("http_cookies", "Cookie", "http.cookies"), MovedModule("html_entities", "htmlentitydefs", "html.entities"), @@ -637,13 +639,16 @@ def u(s): import io StringIO = io.StringIO BytesIO = io.BytesIO + del io _assertCountEqual = "assertCountEqual" if sys.version_info[1] <= 1: _assertRaisesRegex = "assertRaisesRegexp" _assertRegex = "assertRegexpMatches" + _assertNotRegex = "assertNotRegexpMatches" else: _assertRaisesRegex = "assertRaisesRegex" _assertRegex = "assertRegex" + _assertNotRegex = "assertNotRegex" else: def b(s): return s @@ -665,6 +670,7 @@ def indexbytes(buf, i): _assertCountEqual = "assertItemsEqual" _assertRaisesRegex = "assertRaisesRegexp" _assertRegex = "assertRegexpMatches" + _assertNotRegex = "assertNotRegexpMatches" _add_doc(b, """Byte literal""") _add_doc(u, """Text literal""") @@ -681,6 +687,10 @@ def assertRegex(self, *args, **kwargs): return getattr(self, _assertRegex)(*args, **kwargs) +def assertNotRegex(self, *args, **kwargs): + return getattr(self, _assertNotRegex)(*args, **kwargs) + + if PY3: exec_ = getattr(moves.builtins, "exec") @@ -716,16 +726,7 @@ def exec_(_code_, _globs_=None, _locs_=None): """) -if sys.version_info[:2] == (3, 2): - exec_("""def raise_from(value, from_value): - try: - if from_value is None: - raise value - raise value from from_value - finally: - value = None -""") -elif sys.version_info[:2] > (3, 2): +if sys.version_info[:2] > (3,): exec_("""def raise_from(value, from_value): try: raise value from from_value @@ -805,13 +806,33 @@ def print_(*args, **kwargs): _add_doc(reraise, """Reraise an exception.""") if sys.version_info[0:2] < (3, 4): + # This does exactly the same what the :func:`py3:functools.update_wrapper` + # function does on Python versions after 3.2. It sets the ``__wrapped__`` + # attribute on ``wrapper`` object and it doesn't raise an error if any of + # the attributes mentioned in ``assigned`` and ``updated`` are missing on + # ``wrapped`` object. + def _update_wrapper(wrapper, wrapped, + assigned=functools.WRAPPER_ASSIGNMENTS, + updated=functools.WRAPPER_UPDATES): + for attr in assigned: + try: + value = getattr(wrapped, attr) + except AttributeError: + continue + else: + setattr(wrapper, attr, value) + for attr in updated: + getattr(wrapper, attr).update(getattr(wrapped, attr, {})) + wrapper.__wrapped__ = wrapped + return wrapper + _update_wrapper.__doc__ = functools.update_wrapper.__doc__ + def wraps(wrapped, assigned=functools.WRAPPER_ASSIGNMENTS, updated=functools.WRAPPER_UPDATES): - def wrapper(f): - f = functools.wraps(wrapped, assigned, updated)(f) - f.__wrapped__ = wrapped - return f - return wrapper + return functools.partial(_update_wrapper, wrapped=wrapped, + assigned=assigned, updated=updated) + wraps.__doc__ = functools.wraps.__doc__ + else: wraps = functools.wraps @@ -824,7 +845,15 @@ def with_metaclass(meta, *bases): class metaclass(type): def __new__(cls, name, this_bases, d): - return meta(name, bases, d) + if sys.version_info[:2] >= (3, 7): + # This version introduced PEP 560 that requires a bit + # of extra care (we mimic what is done by __build_class__). + resolved_bases = types.resolve_bases(bases) + if resolved_bases is not bases: + d['__orig_bases__'] = bases + else: + resolved_bases = bases + return meta(name, resolved_bases, d) @classmethod def __prepare__(cls, name, this_bases): @@ -908,10 +937,9 @@ def ensure_text(s, encoding='utf-8', errors='strict'): raise TypeError("not expecting type '%s'" % type(s)) - def python_2_unicode_compatible(klass): """ - A decorator that defines __unicode__ and __str__ methods under Python 2. + A class decorator that defines __unicode__ and __str__ methods under Python 2. Under Python 3 it does nothing. To support Python 2 and 3 with a single code base, define a __str__ method diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 23609c2885c..b7e0fbd6786 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -19,5 +19,5 @@ requests==2.22.0 urllib3==1.25.7 retrying==1.3.3 setuptools==41.4.0 -six==1.12.0 +six==1.14.0 webencodings==0.5.1 From 11765917fbd7448a8b68c3f5226f98eb3b6f5c87 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 21 Jan 2020 13:29:59 +0530 Subject: [PATCH 1170/3170] Add the required NEWS fragments --- news/CacheControl.vendor | 1 + news/certifi.vendor | 1 + news/colorama.vendor | 1 + news/distlib.vendor | 1 + news/ipaddress.vendor | 1 + news/pyparsing.vendor | 1 + news/six.vendor | 1 + news/urllib3.vendor | 1 + 8 files changed, 8 insertions(+) create mode 100644 news/CacheControl.vendor create mode 100644 news/certifi.vendor create mode 100644 news/colorama.vendor create mode 100644 news/distlib.vendor create mode 100644 news/ipaddress.vendor create mode 100644 news/pyparsing.vendor create mode 100644 news/six.vendor create mode 100644 news/urllib3.vendor diff --git a/news/CacheControl.vendor b/news/CacheControl.vendor new file mode 100644 index 00000000000..2c8e09f3cf0 --- /dev/null +++ b/news/CacheControl.vendor @@ -0,0 +1 @@ +Upgrade CacheControl to 0.12.5 diff --git a/news/certifi.vendor b/news/certifi.vendor new file mode 100644 index 00000000000..66e84cb207e --- /dev/null +++ b/news/certifi.vendor @@ -0,0 +1 @@ +Upgrade certifi to 2019.9.11 diff --git a/news/colorama.vendor b/news/colorama.vendor new file mode 100644 index 00000000000..1defb49f0d1 --- /dev/null +++ b/news/colorama.vendor @@ -0,0 +1 @@ +Upgrade colorama to 0.4.1 diff --git a/news/distlib.vendor b/news/distlib.vendor new file mode 100644 index 00000000000..8b11e09a35b --- /dev/null +++ b/news/distlib.vendor @@ -0,0 +1 @@ +Upgrade distlib to 0.2.9.post0 diff --git a/news/ipaddress.vendor b/news/ipaddress.vendor new file mode 100644 index 00000000000..902e589a918 --- /dev/null +++ b/news/ipaddress.vendor @@ -0,0 +1 @@ +Upgrade ipaddress to 1.0.22 diff --git a/news/pyparsing.vendor b/news/pyparsing.vendor new file mode 100644 index 00000000000..90374a1ef91 --- /dev/null +++ b/news/pyparsing.vendor @@ -0,0 +1 @@ +Upgrade pyparsing to 2.4.2 diff --git a/news/six.vendor b/news/six.vendor new file mode 100644 index 00000000000..3186ab0cfc1 --- /dev/null +++ b/news/six.vendor @@ -0,0 +1 @@ +Upgrade six to 1.12.0 diff --git a/news/urllib3.vendor b/news/urllib3.vendor new file mode 100644 index 00000000000..80b98b44e10 --- /dev/null +++ b/news/urllib3.vendor @@ -0,0 +1 @@ +Upgrade urllib3 to 1.25.6 From 7b2f0aa00d0b0614abfe3259cef6ff47cfeeac73 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 21 Jan 2020 13:51:30 +0530 Subject: [PATCH 1171/3170] Upgrade pkg_resources (via setuptools) to 44.0.0 --- news/pkg_resources.vendor | 1 + src/pip/_vendor/pkg_resources/__init__.py | 7 ++++--- src/pip/_vendor/vendor.txt | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) create mode 100644 news/pkg_resources.vendor diff --git a/news/pkg_resources.vendor b/news/pkg_resources.vendor new file mode 100644 index 00000000000..7f1972b3b06 --- /dev/null +++ b/news/pkg_resources.vendor @@ -0,0 +1 @@ +Upgrade pkg_resources (via setuptools) to 44.0.0 diff --git a/src/pip/_vendor/pkg_resources/__init__.py b/src/pip/_vendor/pkg_resources/__init__.py index 363a6309e55..a457ff27ef0 100644 --- a/src/pip/_vendor/pkg_resources/__init__.py +++ b/src/pip/_vendor/pkg_resources/__init__.py @@ -88,8 +88,8 @@ __metaclass__ = type -if (3, 0) < sys.version_info < (3, 4): - raise RuntimeError("Python 3.4 or later is required") +if (3, 0) < sys.version_info < (3, 5): + raise RuntimeError("Python 3.5 or later is required") if six.PY2: # Those builtin exceptions are only defined in Python 3 @@ -333,7 +333,7 @@ class UnknownExtra(ResolutionError): _provider_factories = {} -PY_MAJOR = sys.version[:3] +PY_MAJOR = '{}.{}'.format(*sys.version_info) EGG_DIST = 3 BINARY_DIST = 2 SOURCE_DIST = 1 @@ -3109,6 +3109,7 @@ def __init__(self, requirement_string): self.extras = tuple(map(safe_extra, self.extras)) self.hashCmp = ( self.key, + self.url, self.specifier, frozenset(self.extras), str(self.marker) if self.marker else None, diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index b7e0fbd6786..eac6d96cc1d 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -18,6 +18,6 @@ requests==2.22.0 idna==2.8 urllib3==1.25.7 retrying==1.3.3 -setuptools==41.4.0 +setuptools==44.0.0 six==1.14.0 webencodings==0.5.1 From 6a41ea396273e57fe5c1c90366b0c0b508d8203e Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 21 Jan 2020 16:15:21 +0530 Subject: [PATCH 1172/3170] Update AUTHORS.txt --- AUTHORS.txt | 35 +++++++++++++++++++++++++++++++++++ 1 file changed, 35 insertions(+) diff --git a/AUTHORS.txt b/AUTHORS.txt index e06e07b132a..72c87d7d38a 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -1,14 +1,19 @@ A_Rog <adam.thomas.rogerson@gmail.com> +Aakanksha Agrawal <11389424+rasponic@users.noreply.github.com> Abhinav Sagar <40603139+abhinavsagar@users.noreply.github.com> ABHYUDAY PRATAP SINGH <abhyudaypratap@outlook.com> +abs51295 <aagams68@gmail.com> AceGentile <ventogrigio83@gmail.com> Adam Chainz <adam@adamj.eu> Adam Tse <adam.tse@me.com> Adam Tse <atse@users.noreply.github.com> Adam Wentz <awentz@theonion.com> +admin <admin@admins-MacBook-Pro.local> Adrien Morison <adrien.morison@gmail.com> ahayrapetyan <ahayrapetya2@bloomberg.net> +Ahilya <ahilya16009@iiitd.ac.in> AinsworthK <yat626@yahoo.com.hk> +Akash Srivastava <akashsrivastava4927@gmail.com> Alan Yee <alyee@ucsd.edu> Albert Tugushev <albert@tugushev.ru> Albert-Guan <albert.guan94@gmail.com> @@ -26,6 +31,7 @@ Alexey Popravka <a.popravka@smartweb.com.ua> Alexey Popravka <alexey.popravka@horsedevel.com> Alli <alzeih@users.noreply.github.com> Ami Fischman <ami@fischman.org> +Ananya Maiti <ananyoevo@gmail.com> Anatoly Techtonik <techtonik@gmail.com> Anders Kaseorg <andersk@mit.edu> Andreas Lutro <anlutro@gmail.com> @@ -38,6 +44,7 @@ Andy Freeland <andy.freeland@redjack.com> Andy Freeland <andy@andyfreeland.net> Andy Kluger <AndydeCleyre@users.noreply.github.com> Ani Hayrapetyan <ahayrapetya2@bloomberg.net> +Aniruddha Basak <codewithaniruddha@gmail.com> Anish Tambe <anish.tambe@yahoo.in> Anrs Hu <anrs@douban.com> Anthony Sottile <asottile@umich.edu> @@ -76,6 +83,7 @@ Bernardo B. Marques <bernardo.fire@gmail.com> Bernhard M. Wiedemann <bwiedemann@suse.de> Bertil Hatt <bertil.hatt@farfetch.com> Bogdan Opanchuk <bogdan@opanchuk.net> +BorisZZZ <BorisZZZ@users.noreply.github.com> Brad Erickson <eosrei@gmail.com> Bradley Ayers <bradley.ayers@gmail.com> Brandon L. Reiss <brandon@damyata.co> @@ -153,6 +161,7 @@ David Tucker <david@tucker.name> David Wales <daviewales@gmail.com> Davidovich <david.genest@gmail.com> derwolfe <chriswwolfe@gmail.com> +Desetude <harry@desetude.com> Diego Caraballo <diegocaraballo84@gmail.com> DiegoCaraballo <diegocaraballo84@gmail.com> Dmitry Gladkov <dmitry.gladkov@gmail.com> @@ -165,6 +174,7 @@ Dustin Ingram <di@di.codes> Dwayne Bailey <dwayne@translate.org.za> Ed Morley <501702+edmorley@users.noreply.github.com> Ed Morley <emorley@mozilla.com> +Eitan Adler <lists@eitanadler.com> ekristina <panacejja@gmail.com> elainechan <elaine.chan@outlook.com> Eli Schwartz <eschwartz93@gmail.com> @@ -183,6 +193,7 @@ Ernest W Durbin III <ewdurbin@gmail.com> Ernest W. Durbin III <ewdurbin@gmail.com> Erwin Janssen <erwinjanssen@outlook.com> Eugene Vereshchagin <evvers@gmail.com> +everdimension <everdimension@gmail.com> Felix Yan <felixonmars@archlinux.org> fiber-space <fiber-space@users.noreply.github.com> Filip Kokosiński <filip.kokosinski@gmail.com> @@ -202,16 +213,20 @@ Georgi Valkov <georgi.t.valkov@gmail.com> Giftlin Rajaiah <giftlin.rgn@gmail.com> gizmoguy1 <gizmoguy1@gmail.com> gkdoc <40815324+gkdoc@users.noreply.github.com> +Gopinath M <31352222+mgopi1990@users.noreply.github.com> GOTO Hayato <3532528+gh640@users.noreply.github.com> gpiks <gaurav.pikale@gmail.com> Guilherme Espada <porcariadagata@gmail.com> Guy Rozendorn <guy@rzn.co.il> gzpan123 <gzpan123@gmail.com> +Hanjun Kim <hallazzang@gmail.com> Hari Charan <hcharan997@gmail.com> +Harsh Vardhan <harsh59v@gmail.com> Herbert Pfennig <herbert@albinen.com> Hsiaoming Yang <lepture@me.com> Hugo <hugovk@users.noreply.github.com> Hugo Lopes Tavares <hltbra@gmail.com> +Hugo van Kemenade <hugovk@users.noreply.github.com> hugovk <hugovk@users.noreply.github.com> Hynek Schlawack <hs@ox.cx> Ian Bicking <ianb@colorstudy.com> @@ -227,6 +242,7 @@ INADA Naoki <songofacandy@gmail.com> Ionel Cristian Mărieș <contact@ionelmc.ro> Ionel Maries Cristian <ionel.mc@gmail.com> Ivan Pozdeev <vano@mail.mipt.ru> +Jacob Kim <me@thejacobkim.com> jakirkham <jakirkham@gmail.com> Jakub Stasiak <kuba.stasiak@gmail.com> Jakub Vysoky <jakub@borka.cz> @@ -244,8 +260,10 @@ Jean-Christophe Fillion-Robin <jchris.fillionr@kitware.com> Jeff Barber <jbarber@computer.org> Jeff Dairiki <dairiki@dairiki.org> Jelmer Vernooij <jelmer@jelmer.uk> +jenix21 <devfrog@gmail.com> Jeremy Stanley <fungi@yuggoth.org> Jeremy Zafran <jzafran@users.noreply.github.com> +Jiashuo Li <jiasli@microsoft.com> Jim Garrison <jim@garrison.cc> Jivan Amara <Development@JivanAmara.net> John Paton <j.paton@catawiki.nl> @@ -269,9 +287,11 @@ Julian Gethmann <julian.gethmann@kit.edu> Julien Demoor <julien@jdemoor.com> jwg4 <jack.grahl@yahoo.co.uk> Jyrki Pulliainen <jyrki@spotify.com> +Kai Chen <kaichen120@gmail.com> Kamal Bin Mustafa <kamal@smach.net> kaustav haldar <hi@kaustav.me> keanemind <keanemind@gmail.com> +Keith Maxwell <keith.maxwell@gmail.com> Kelsey Hightower <kelsey.hightower@gmail.com> Kenneth Belitzky <kenny@belitzky.com> Kenneth Reitz <me@kennethreitz.com> @@ -331,6 +351,7 @@ Maxim Kurnikov <maxim.kurnikov@gmail.com> Maxime Rouyrre <rouyrre+git@gmail.com> mayeut <mayeut@users.noreply.github.com> mbaluna <44498973+mbaluna@users.noreply.github.com> +mdebi <17590103+mdebi@users.noreply.github.com> memoselyk <memoselyk@gmail.com> Michael <michael-k@users.noreply.github.com> Michael Aquilina <michaelaquilina@gmail.com> @@ -352,6 +373,7 @@ Monty Taylor <mordred@inaugust.com> Nate Coraor <nate@bx.psu.edu> Nathaniel J. Smith <njs@pobox.com> Nehal J Wani <nehaljw.kkd1@gmail.com> +Neil Botelho <neil.botelho321@gmail.com> Nick Coghlan <ncoghlan@gmail.com> Nick Stenning <nick@whiteink.com> Nick Timkovich <prometheus235@gmail.com> @@ -378,6 +400,7 @@ Patrick Dubroy <pdubroy@gmail.com> Patrick Jenkins <patrick@socialgrowthtechnologies.com> Patrick Lawson <pl@foursquare.com> patricktokeeffe <patricktokeeffe@users.noreply.github.com> +Patrik Kopkan <pkopkan@redhat.com> Paul Kehrer <paul.l.kehrer@gmail.com> Paul Moore <p.f.moore@gmail.com> Paul Nasrat <pnasrat@gmail.com> @@ -402,8 +425,10 @@ Pierre-Yves Rofes <github@rofes.fr> pip <pypa-dev@googlegroups.com> Prabakaran Kumaresshan <k_prabakaran+github@hotmail.com> Prabhjyotsing Surjit Singh Sodhi <psinghsodhi@bloomberg.net> +Prabhu Marappan <prabhum.794@gmail.com> Pradyun Gedam <pradyunsg@gmail.com> Pratik Mallya <mallya@us.ibm.com> +Preet Thakkar <preet.thakkar@students.iiit.ac.in> Preston Holmes <preston@ptone.com> Przemek Wrzos <hetmankp@none> Pulkit Goyal <7895pulkit@gmail.com> @@ -414,6 +439,7 @@ Rafael Caricio <rafael.jacinto@gmail.com> Ralf Schmitt <ralf@systemexit.de> Razzi Abuissa <razzi53@gmail.com> rdb <rdb@users.noreply.github.com> +Remi Rampin <r@remirampin.com> Remi Rampin <remirampin@gmail.com> Rene Dudfield <renesd@gmail.com> Riccardo Magliocchetti <riccardo.magliocchetti@gmail.com> @@ -422,6 +448,7 @@ RobberPhex <robberphex@gmail.com> Robert Collins <rbtcollins@hp.com> Robert McGibbon <rmcgibbo@gmail.com> Robert T. McGibbon <rmcgibbo@gmail.com> +robin elisha robinson <elisha.rob@gmail.com> Roey Berman <roey.berman@gmail.com> Rohan Jain <crodjer@gmail.com> Rohan Jain <crodjer@users.noreply.github.com> @@ -442,15 +469,18 @@ schlamar <marc.schlaich@gmail.com> Scott Kitterman <sklist@kitterman.com> Sean <me@sean.taipei> seanj <seanj@xyke.com> +Sebastian Jordan <sebastian.jordan.mail@googlemail.com> Sebastian Schaetz <sschaetz@butterflynetinc.com> Segev Finer <segev208@gmail.com> SeongSoo Cho <ppiyakk2@printf.kr> Sergey Vasilyev <nolar@nolar.info> Seth Woodworth <seth@sethish.com> Shlomi Fish <shlomif@shlomifish.org> +Shovan Maity <shovan.maity@mayadata.io> Simeon Visser <svisser@users.noreply.github.com> Simon Cross <hodgestar@gmail.com> Simon Pichugin <simon.pichugin@gmail.com> +sinoroc <sinoroc.code+git@gmail.com> Sorin Sbarnea <sorin.sbarnea@gmail.com> Stavros Korokithakis <stavros@korokithakis.net> Stefan Scherfke <stefan@sofa-rockers.org> @@ -468,6 +498,7 @@ Stéphane Klein <contact@stephane-klein.info> Sumana Harihareswara <sh@changeset.nyc> Sviatoslav Sydorenko <wk.cvs.github@sydorenko.org.ua> Sviatoslav Sydorenko <wk@sydorenko.org.ua> +Swat009 <swatantra.kumar8@gmail.com> Takayuki SHIMIZUKAWA <shimizukawa@gmail.com> tbeswick <tbeswick@enphaseenergy.com> Thijs Triemstra <info@collab.nl> @@ -486,15 +517,18 @@ tinruufu <tinruufu@gmail.com> Tom Forbes <tom@tomforb.es> Tom Freudenheim <tom.freudenheim@onepeloton.com> Tom V <tom@viner.tv> +Tomas Orsava <torsava@redhat.com> Tomer Chachamu <tomer.chachamu@gmail.com> Tony Beswick <tonybeswick@orcon.net.nz> Tony Zhaocheng Tan <tony@tonytan.io> TonyBeswick <TonyBeswick@users.noreply.github.com> +toonarmycaptain <toonarmycaptain@hotmail.com> Toshio Kuratomi <toshio@fedoraproject.org> Travis Swicegood <development@domain51.com> Tzu-ping Chung <uranusjr@gmail.com> Valentin Haenel <valentin.haenel@gmx.de> Victor Stinner <victor.stinner@gmail.com> +victorvpaulo <victorvpaulo@gmail.com> Viktor Szépe <viktor@szepe.net> Ville Skyttä <ville.skytta@iki.fi> Vinay Sajip <vinay_sajip@yahoo.co.uk> @@ -516,6 +550,7 @@ xoviat <xoviat@users.noreply.github.com> xtreak <tir.karthi@gmail.com> YAMAMOTO Takashi <yamamoto@midokura.com> Yen Chi Hsuan <yan12125@gmail.com> +Yeray Diaz Diaz <yeraydiazdiaz@gmail.com> Yoval P <yoval@gmx.com> Yu Jian <askingyj@gmail.com> Yuan Jing Vincent Yan <yyan82@bloomberg.net> From 8ad871a6bbdccfe152cc75cbb11e3faf0778b9b3 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 21 Jan 2020 16:15:21 +0530 Subject: [PATCH 1173/3170] Bump for release --- NEWS.rst | 120 ++++++++++++++++++ ...0cda98-2240-11ea-9951-00e04c3600d8.trivial | 0 news/1668.feature | 2 - ...4c23de-df0b-4aaa-8454-4569829768fc.trivial | 0 ...044E84-3F3C-48A8-84B2-6028E21FEBF1.trivial | 0 news/3801.trivial | 0 news/4785.process | 1 - news/5702.bugfix | 1 - news/5716.bugfix | 1 - news/5860.trivial | 1 - news/6004.trivial | 1 - news/6410.bugfix | 2 - news/6414.feature | 1 - news/6426.bugfix | 1 - news/6599.bugfix | 1 - news/6640.feature | 2 - news/6642.bugfix | 2 - news/6673.feature | 2 - news/6783.bugfix | 2 - news/6852.feature | 5 - news/6908.removal | 2 - news/6998.removal | 1 - ...054cec-e4d6-4494-a554-90a2c0bee837.trivial | 0 news/7146.feature | 1 - news/7155.bugfix | 1 - news/7178.trivial | 0 news/7182.doc | 1 - news/7191.bugfix | 1 - news/7193.bugfix | 1 - news/7197.doc | 1 - news/7199.trivial | 1 - news/7201.doc | 1 - news/7207.bugfix | 1 - news/7222.doc | 1 - news/7225.feature | 1 - news/7230.trivial | 1 - news/7268.trivial | 1 - news/7281.trivial | 1 - news/7296.bugfix | 5 - news/7296.removal | 3 - news/7297.removal | 1 - news/7309.removal | 1 - news/7327.removal | 2 - news/7333.bugfix | 1 - news/7334.trivial | 0 news/7340.bugfix | 2 - news/7347.doc | 1 - news/7355.removal | 4 - news/7359.trivial | 0 news/7376.feature | 2 - news/7385.doc | 1 - news/7393.bugfix | 1 - news/7411.trivial | 1 - news/7463.trivial | 0 news/7487.removal | 2 - news/7488.bugfix | 2 - news/7490.trivial | 1 - news/7498.feature | 2 - news/7517.feature | 4 - news/7521.trivial | 0 news/7527.trivial | 0 news/7541.bugfix | 1 - news/7543.removal | 4 - news/7577.trivial | 0 news/7582.trivial | 0 news/7587.trivial | 0 ...a2e30d-d448-43fd-9223-81dff5ae5001.trivial | 0 ...a9deeb-db71-4c14-a57a-6d440995130d.trivial | 0 news/980.feature | 8 -- news/CacheControl.vendor | 1 - news/certifi.vendor | 1 - news/colorama.vendor | 1 - news/distlib.vendor | 1 - news/ipaddress.vendor | 1 - news/packaging.vendor | 1 - news/pkg_resources.vendor | 1 - news/pre-commit-gha-cache.trivial | 0 news/pyparsing.vendor | 1 - news/six.vendor | 1 - news/urllib3.vendor | 1 - src/pip/__init__.py | 2 +- 81 files changed, 121 insertions(+), 102 deletions(-) delete mode 100644 news/050cda98-2240-11ea-9951-00e04c3600d8.trivial delete mode 100644 news/1668.feature delete mode 100644 news/284c23de-df0b-4aaa-8454-4569829768fc.trivial delete mode 100644 news/31044E84-3F3C-48A8-84B2-6028E21FEBF1.trivial delete mode 100644 news/3801.trivial delete mode 100644 news/4785.process delete mode 100644 news/5702.bugfix delete mode 100644 news/5716.bugfix delete mode 100644 news/5860.trivial delete mode 100644 news/6004.trivial delete mode 100644 news/6410.bugfix delete mode 100644 news/6414.feature delete mode 100644 news/6426.bugfix delete mode 100644 news/6599.bugfix delete mode 100644 news/6640.feature delete mode 100644 news/6642.bugfix delete mode 100644 news/6673.feature delete mode 100644 news/6783.bugfix delete mode 100644 news/6852.feature delete mode 100644 news/6908.removal delete mode 100644 news/6998.removal delete mode 100644 news/6a054cec-e4d6-4494-a554-90a2c0bee837.trivial delete mode 100644 news/7146.feature delete mode 100644 news/7155.bugfix delete mode 100644 news/7178.trivial delete mode 100644 news/7182.doc delete mode 100644 news/7191.bugfix delete mode 100644 news/7193.bugfix delete mode 100644 news/7197.doc delete mode 100644 news/7199.trivial delete mode 100644 news/7201.doc delete mode 100644 news/7207.bugfix delete mode 100644 news/7222.doc delete mode 100644 news/7225.feature delete mode 100644 news/7230.trivial delete mode 100644 news/7268.trivial delete mode 100644 news/7281.trivial delete mode 100644 news/7296.bugfix delete mode 100644 news/7296.removal delete mode 100644 news/7297.removal delete mode 100644 news/7309.removal delete mode 100644 news/7327.removal delete mode 100644 news/7333.bugfix delete mode 100644 news/7334.trivial delete mode 100644 news/7340.bugfix delete mode 100644 news/7347.doc delete mode 100644 news/7355.removal delete mode 100644 news/7359.trivial delete mode 100644 news/7376.feature delete mode 100644 news/7385.doc delete mode 100644 news/7393.bugfix delete mode 100644 news/7411.trivial delete mode 100644 news/7463.trivial delete mode 100644 news/7487.removal delete mode 100644 news/7488.bugfix delete mode 100644 news/7490.trivial delete mode 100644 news/7498.feature delete mode 100644 news/7517.feature delete mode 100644 news/7521.trivial delete mode 100644 news/7527.trivial delete mode 100644 news/7541.bugfix delete mode 100644 news/7543.removal delete mode 100644 news/7577.trivial delete mode 100644 news/7582.trivial delete mode 100644 news/7587.trivial delete mode 100644 news/77a2e30d-d448-43fd-9223-81dff5ae5001.trivial delete mode 100644 news/85a9deeb-db71-4c14-a57a-6d440995130d.trivial delete mode 100644 news/980.feature delete mode 100644 news/CacheControl.vendor delete mode 100644 news/certifi.vendor delete mode 100644 news/colorama.vendor delete mode 100644 news/distlib.vendor delete mode 100644 news/ipaddress.vendor delete mode 100644 news/packaging.vendor delete mode 100644 news/pkg_resources.vendor delete mode 100644 news/pre-commit-gha-cache.trivial delete mode 100644 news/pyparsing.vendor delete mode 100644 news/six.vendor delete mode 100644 news/urllib3.vendor diff --git a/NEWS.rst b/NEWS.rst index d440dce3181..949065dfa74 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -7,6 +7,126 @@ .. towncrier release notes start +20.0 (2020-01-21) +================= + +Process +------- + +- Switch to a dedicated CLI tool for vendoring dependencies. + +Deprecations and Removals +------------------------- + +- Remove wheel tag calculation from pip and use ``packaging.tags``. This + should provide more tags ordered better than in prior releases. (`#6908 <https://github.com/pypa/pip/issues/6908>`_) +- Deprecate setup.py-based builds that do not generate an ``.egg-info`` directory. (`#6998 <https://github.com/pypa/pip/issues/6998>`_) +- The pip>=20 wheel cache is not retro-compatible with previous versions. Until + pip 21.0, pip will continue to take advantage of existing legacy cache + entries. (`#7296 <https://github.com/pypa/pip/issues/7296>`_) +- Deprecate undocumented ``--skip-requirements-regex`` option. (`#7297 <https://github.com/pypa/pip/issues/7297>`_) +- Deprecate passing install-location-related options via ``--install-option``. (`#7309 <https://github.com/pypa/pip/issues/7309>`_) +- Use literal "abi3" for wheel tag on CPython 3.x, to align with PEP 384 + which only defines it for this platform. (`#7327 <https://github.com/pypa/pip/issues/7327>`_) +- Remove interpreter-specific major version tag e.g. ``cp3-none-any`` + from consideration. This behavior was not documented strictly, and this + tag in particular is `not useful <https://snarky.ca/the-challenges-in-designing-a-library-for-pep-425/>`_. + Anyone with a use case can create an issue with pypa/packaging. (`#7355 <https://github.com/pypa/pip/issues/7355>`_) +- Wheel processing no longer permits wheels containing more than one top-level + .dist-info directory. (`#7487 <https://github.com/pypa/pip/issues/7487>`_) +- Support for the ``git+git@`` form of VCS requirement is being deprecated and + will be removed in pip 21.0. Switch to ``git+https://`` or + ``git+ssh://``. ``git+git://`` also works but its use is discouraged as it is + insecure. (`#7543 <https://github.com/pypa/pip/issues/7543>`_) + +Features +-------- + +- Default to doing a user install (as if ``--user`` was passed) when the main + site-packages directory is not writeable and user site-packages are enabled. (`#1668 <https://github.com/pypa/pip/issues/1668>`_) +- Warn if a path in PATH starts with tilde during ``pip install``. (`#6414 <https://github.com/pypa/pip/issues/6414>`_) +- Cache wheels built from Git requirements that are considered immutable, + because they point to a commit hash. (`#6640 <https://github.com/pypa/pip/issues/6640>`_) +- Add option ``--no-python-version-warning`` to silence warnings + related to deprecation of Python versions. (`#6673 <https://github.com/pypa/pip/issues/6673>`_) +- Cache wheels that ``pip wheel`` built locally, matching what + ``pip install`` does. This particularly helps performance in workflows where + ``pip wheel`` is used for `building before installing + <https://pip.pypa.io/en/stable/user_guide/#installing-from-local-packages>`_. + Users desiring the original behavior can use ``pip wheel --no-cache-dir``. (`#6852 <https://github.com/pypa/pip/issues/6852>`_) +- Display CA information in ``pip debug``. (`#7146 <https://github.com/pypa/pip/issues/7146>`_) +- Show only the filename (instead of full URL), when downloading from PyPI. (`#7225 <https://github.com/pypa/pip/issues/7225>`_) +- Suggest a more robust command to upgrade pip itself to avoid confusion when the + current pip command is not available as ``pip``. (`#7376 <https://github.com/pypa/pip/issues/7376>`_) +- Define all old pip console script entrypoints to prevent import issues in + stale wrapper scripts. (`#7498 <https://github.com/pypa/pip/issues/7498>`_) +- The build step of ``pip wheel`` now builds all wheels to a cache first, + then copies them to the wheel directory all at once. + Before, it built them to a temporary direcory and moved + them to the wheel directory one by one. (`#7517 <https://github.com/pypa/pip/issues/7517>`_) +- Expand ``~`` prefix to user directory in path options, configs, and + environment variables. Values that may be either URL or path are not + currently supported, to avoid ambiguity: + + * ``--find-links`` + * ``--constraint``, ``-c`` + * ``--requirement``, ``-r`` + * ``--editable``, ``-e`` (`#980 <https://github.com/pypa/pip/issues/980>`_) + +Bug Fixes +--------- + +- Correctly handle system site-packages, in virtual environments created with venv (PEP 405). (`#5702 <https://github.com/pypa/pip/issues/5702>`_, `#7155 <https://github.com/pypa/pip/issues/7155>`_) +- Fix case sensitive comparison of pip freeze when used with -r option. (`#5716 <https://github.com/pypa/pip/issues/5716>`_) +- Enforce PEP 508 requirement format in ``pyproject.toml`` + ``build-system.requires``. (`#6410 <https://github.com/pypa/pip/issues/6410>`_) +- Make ``ensure_dir()`` also ignore ``ENOTEMPTY`` as seen on Windows. (`#6426 <https://github.com/pypa/pip/issues/6426>`_) +- Fix building packages which specify ``backend-path`` in pyproject.toml. (`#6599 <https://github.com/pypa/pip/issues/6599>`_) +- Do not attempt to run ``setup.py clean`` after a ``pep517`` build error, + since a ``setup.py`` may not exist in that case. (`#6642 <https://github.com/pypa/pip/issues/6642>`_) +- Fix passwords being visible in the index-url in + "Downloading <url>" message. (`#6783 <https://github.com/pypa/pip/issues/6783>`_) +- Change method from shutil.remove to shutil.rmtree in noxfile.py. (`#7191 <https://github.com/pypa/pip/issues/7191>`_) +- Skip running tests which require subversion, when svn isn't installed (`#7193 <https://github.com/pypa/pip/issues/7193>`_) +- Fix not sending client certificates when using ``--trusted-host``. (`#7207 <https://github.com/pypa/pip/issues/7207>`_) +- Make sure ``pip wheel`` never outputs pure python wheels with a + python implementation tag. Better fix/workaround for + `#3025 <https://github.com/pypa/pip/issues/3025>`_ by + using a per-implementation wheel cache instead of caching pure python + wheels with an implementation tag in their name. (`#7296 <https://github.com/pypa/pip/issues/7296>`_) +- Include ``subdirectory`` URL fragments in cache keys. (`#7333 <https://github.com/pypa/pip/issues/7333>`_) +- Fix typo in warning message when any of ``--build-option``, ``--global-option`` + and ``--install-option`` is used in requirements.txt (`#7340 <https://github.com/pypa/pip/issues/7340>`_) +- Fix the logging of cached HTTP response shown as downloading. (`#7393 <https://github.com/pypa/pip/issues/7393>`_) +- Effectively disable the wheel cache when it is not writable, as is the + case with the http cache. (`#7488 <https://github.com/pypa/pip/issues/7488>`_) +- Correctly handle relative cache directory provided via --cache-dir. (`#7541 <https://github.com/pypa/pip/issues/7541>`_) + +Vendored Libraries +------------------ + +- Upgrade CacheControl to 0.12.5 +- Upgrade certifi to 2019.9.11 +- Upgrade colorama to 0.4.1 +- Upgrade distlib to 0.2.9.post0 +- Upgrade ipaddress to 1.0.22 +- Update packaging to 20.0. +- Upgrade pkg_resources (via setuptools) to 44.0.0 +- Upgrade pyparsing to 2.4.2 +- Upgrade six to 1.12.0 +- Upgrade urllib3 to 1.25.6 + +Improved Documentation +---------------------- + +- Document that "coding: utf-8" is supported in requirements.txt (`#7182 <https://github.com/pypa/pip/issues/7182>`_) +- Explain how to get pip's source code in `Getting Started <https://pip.pypa.io/en/stable/development/getting-started/>`_ (`#7197 <https://github.com/pypa/pip/issues/7197>`_) +- Describe how basic authentication credentials in URLs work. (`#7201 <https://github.com/pypa/pip/issues/7201>`_) +- Add more clear installation instructions (`#7222 <https://github.com/pypa/pip/issues/7222>`_) +- Fix documentation links for index options (`#7347 <https://github.com/pypa/pip/issues/7347>`_) +- Better document the requirements file format (`#7385 <https://github.com/pypa/pip/issues/7385>`_) + + 19.3.1 (2019-10-17) =================== diff --git a/news/050cda98-2240-11ea-9951-00e04c3600d8.trivial b/news/050cda98-2240-11ea-9951-00e04c3600d8.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/1668.feature b/news/1668.feature deleted file mode 100644 index d200841ea7f..00000000000 --- a/news/1668.feature +++ /dev/null @@ -1,2 +0,0 @@ -Default to doing a user install (as if ``--user`` was passed) when the main -site-packages directory is not writeable and user site-packages are enabled. diff --git a/news/284c23de-df0b-4aaa-8454-4569829768fc.trivial b/news/284c23de-df0b-4aaa-8454-4569829768fc.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/31044E84-3F3C-48A8-84B2-6028E21FEBF1.trivial b/news/31044E84-3F3C-48A8-84B2-6028E21FEBF1.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/3801.trivial b/news/3801.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/4785.process b/news/4785.process deleted file mode 100644 index 022510f4b18..00000000000 --- a/news/4785.process +++ /dev/null @@ -1 +0,0 @@ -Switch to a dedicated CLI tool for vendoring dependencies. diff --git a/news/5702.bugfix b/news/5702.bugfix deleted file mode 100644 index 2541d745ed7..00000000000 --- a/news/5702.bugfix +++ /dev/null @@ -1 +0,0 @@ -Correctly handle system site-packages, in virtual environments created with venv (PEP 405). diff --git a/news/5716.bugfix b/news/5716.bugfix deleted file mode 100644 index 2659657cb61..00000000000 --- a/news/5716.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix case sensitive comparison of pip freeze when used with -r option. diff --git a/news/5860.trivial b/news/5860.trivial deleted file mode 100644 index 7f77d3fd7ab..00000000000 --- a/news/5860.trivial +++ /dev/null @@ -1 +0,0 @@ -Updated info about pip support for url_req portion of PEP508 in doc. diff --git a/news/6004.trivial b/news/6004.trivial deleted file mode 100644 index 79eb6962c19..00000000000 --- a/news/6004.trivial +++ /dev/null @@ -1 +0,0 @@ -Read version in setup.py without re diff --git a/news/6410.bugfix b/news/6410.bugfix deleted file mode 100644 index c96fba8bdb1..00000000000 --- a/news/6410.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -Enforce PEP 508 requirement format in ``pyproject.toml`` -``build-system.requires``. diff --git a/news/6414.feature b/news/6414.feature deleted file mode 100644 index 5a72befdcfc..00000000000 --- a/news/6414.feature +++ /dev/null @@ -1 +0,0 @@ -Warn if a path in PATH starts with tilde during ``pip install``. diff --git a/news/6426.bugfix b/news/6426.bugfix deleted file mode 100644 index 25512b3c808..00000000000 --- a/news/6426.bugfix +++ /dev/null @@ -1 +0,0 @@ -Make ``ensure_dir()`` also ignore ``ENOTEMPTY`` as seen on Windows. diff --git a/news/6599.bugfix b/news/6599.bugfix deleted file mode 100644 index 38bd89e2e03..00000000000 --- a/news/6599.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix building packages which specify ``backend-path`` in pyproject.toml. diff --git a/news/6640.feature b/news/6640.feature deleted file mode 100644 index cb7e939dabb..00000000000 --- a/news/6640.feature +++ /dev/null @@ -1,2 +0,0 @@ -Cache wheels built from Git requirements that are considered immutable, -because they point to a commit hash. diff --git a/news/6642.bugfix b/news/6642.bugfix deleted file mode 100644 index 470572ad63a..00000000000 --- a/news/6642.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -Do not attempt to run ``setup.py clean`` after a ``pep517`` build error, -since a ``setup.py`` may not exist in that case. diff --git a/news/6673.feature b/news/6673.feature deleted file mode 100644 index 829bb232859..00000000000 --- a/news/6673.feature +++ /dev/null @@ -1,2 +0,0 @@ -Add option ``--no-python-version-warning`` to silence warnings -related to deprecation of Python versions. diff --git a/news/6783.bugfix b/news/6783.bugfix deleted file mode 100644 index 239d962f2ba..00000000000 --- a/news/6783.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -Fix passwords being visible in the index-url in -"Downloading <url>" message. diff --git a/news/6852.feature b/news/6852.feature deleted file mode 100644 index 30097e82a8f..00000000000 --- a/news/6852.feature +++ /dev/null @@ -1,5 +0,0 @@ -Cache wheels that ``pip wheel`` built locally, matching what -``pip install`` does. This particularly helps performance in workflows where -``pip wheel`` is used for `building before installing -<https://pip.pypa.io/en/stable/user_guide/#installing-from-local-packages>`_. -Users desiring the original behavior can use ``pip wheel --no-cache-dir``. diff --git a/news/6908.removal b/news/6908.removal deleted file mode 100644 index aca9d590ab0..00000000000 --- a/news/6908.removal +++ /dev/null @@ -1,2 +0,0 @@ -Remove wheel tag calculation from pip and use ``packaging.tags``. This -should provide more tags ordered better than in prior releases. diff --git a/news/6998.removal b/news/6998.removal deleted file mode 100644 index 7c38a48fd11..00000000000 --- a/news/6998.removal +++ /dev/null @@ -1 +0,0 @@ -Deprecate setup.py-based builds that do not generate an ``.egg-info`` directory. diff --git a/news/6a054cec-e4d6-4494-a554-90a2c0bee837.trivial b/news/6a054cec-e4d6-4494-a554-90a2c0bee837.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/7146.feature b/news/7146.feature deleted file mode 100644 index db57345ede5..00000000000 --- a/news/7146.feature +++ /dev/null @@ -1 +0,0 @@ -Display CA information in ``pip debug``. diff --git a/news/7155.bugfix b/news/7155.bugfix deleted file mode 100644 index 2541d745ed7..00000000000 --- a/news/7155.bugfix +++ /dev/null @@ -1 +0,0 @@ -Correctly handle system site-packages, in virtual environments created with venv (PEP 405). diff --git a/news/7178.trivial b/news/7178.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/7182.doc b/news/7182.doc deleted file mode 100644 index f55c6ee7eef..00000000000 --- a/news/7182.doc +++ /dev/null @@ -1 +0,0 @@ -Document that "coding: utf-8" is supported in requirements.txt diff --git a/news/7191.bugfix b/news/7191.bugfix deleted file mode 100644 index 06b3b01eff4..00000000000 --- a/news/7191.bugfix +++ /dev/null @@ -1 +0,0 @@ -Change method from shutil.remove to shutil.rmtree in noxfile.py. diff --git a/news/7193.bugfix b/news/7193.bugfix deleted file mode 100644 index a871b63031d..00000000000 --- a/news/7193.bugfix +++ /dev/null @@ -1 +0,0 @@ -Skip running tests which require subversion, when svn isn't installed diff --git a/news/7197.doc b/news/7197.doc deleted file mode 100644 index 47b6d7c4add..00000000000 --- a/news/7197.doc +++ /dev/null @@ -1 +0,0 @@ -Explain how to get pip's source code in `Getting Started <https://pip.pypa.io/en/stable/development/getting-started/>`_ diff --git a/news/7199.trivial b/news/7199.trivial deleted file mode 100644 index 0f7f2fea83b..00000000000 --- a/news/7199.trivial +++ /dev/null @@ -1 +0,0 @@ -adding line in trivial file to avoid linter issues. diff --git a/news/7201.doc b/news/7201.doc deleted file mode 100644 index 95df888c48a..00000000000 --- a/news/7201.doc +++ /dev/null @@ -1 +0,0 @@ -Describe how basic authentication credentials in URLs work. diff --git a/news/7207.bugfix b/news/7207.bugfix deleted file mode 100644 index 014f979c712..00000000000 --- a/news/7207.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix not sending client certificates when using ``--trusted-host``. diff --git a/news/7222.doc b/news/7222.doc deleted file mode 100644 index 5d68f707a20..00000000000 --- a/news/7222.doc +++ /dev/null @@ -1 +0,0 @@ -Add more clear installation instructions diff --git a/news/7225.feature b/news/7225.feature deleted file mode 100644 index 1678a80f4fd..00000000000 --- a/news/7225.feature +++ /dev/null @@ -1 +0,0 @@ -Show only the filename (instead of full URL), when downloading from PyPI. diff --git a/news/7230.trivial b/news/7230.trivial deleted file mode 100644 index 2e63419b14c..00000000000 --- a/news/7230.trivial +++ /dev/null @@ -1 +0,0 @@ -Change ``pip._internal.wheel`` to respect docstring conventions. diff --git a/news/7268.trivial b/news/7268.trivial deleted file mode 100644 index 052c8279189..00000000000 --- a/news/7268.trivial +++ /dev/null @@ -1 +0,0 @@ -refactoring: remove should_use_ephemeral_cache diff --git a/news/7281.trivial b/news/7281.trivial deleted file mode 100644 index 8fce9ce5107..00000000000 --- a/news/7281.trivial +++ /dev/null @@ -1 +0,0 @@ -refactor _get_used_vcs_backend diff --git a/news/7296.bugfix b/news/7296.bugfix deleted file mode 100644 index 5d617bf75db..00000000000 --- a/news/7296.bugfix +++ /dev/null @@ -1,5 +0,0 @@ -Make sure ``pip wheel`` never outputs pure python wheels with a -python implementation tag. Better fix/workaround for -`#3025 <https://github.com/pypa/pip/issues/3025>`_ by -using a per-implementation wheel cache instead of caching pure python -wheels with an implementation tag in their name. diff --git a/news/7296.removal b/news/7296.removal deleted file mode 100644 index ef0e5f7495f..00000000000 --- a/news/7296.removal +++ /dev/null @@ -1,3 +0,0 @@ -The pip>=20 wheel cache is not retro-compatible with previous versions. Until -pip 21.0, pip will continue to take advantage of existing legacy cache -entries. diff --git a/news/7297.removal b/news/7297.removal deleted file mode 100644 index 663fd9ad609..00000000000 --- a/news/7297.removal +++ /dev/null @@ -1 +0,0 @@ -Deprecate undocumented ``--skip-requirements-regex`` option. diff --git a/news/7309.removal b/news/7309.removal deleted file mode 100644 index 54c2f062656..00000000000 --- a/news/7309.removal +++ /dev/null @@ -1 +0,0 @@ -Deprecate passing install-location-related options via ``--install-option``. diff --git a/news/7327.removal b/news/7327.removal deleted file mode 100644 index b35f58e0fd2..00000000000 --- a/news/7327.removal +++ /dev/null @@ -1,2 +0,0 @@ -Use literal "abi3" for wheel tag on CPython 3.x, to align with PEP 384 -which only defines it for this platform. diff --git a/news/7333.bugfix b/news/7333.bugfix deleted file mode 100644 index 8ddcba76a39..00000000000 --- a/news/7333.bugfix +++ /dev/null @@ -1 +0,0 @@ -Include ``subdirectory`` URL fragments in cache keys. diff --git a/news/7334.trivial b/news/7334.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/7340.bugfix b/news/7340.bugfix deleted file mode 100644 index ca6332d1712..00000000000 --- a/news/7340.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -Fix typo in warning message when any of ``--build-option``, ``--global-option`` -and ``--install-option`` is used in requirements.txt diff --git a/news/7347.doc b/news/7347.doc deleted file mode 100644 index bc62c56cdb9..00000000000 --- a/news/7347.doc +++ /dev/null @@ -1 +0,0 @@ -Fix documentation links for index options diff --git a/news/7355.removal b/news/7355.removal deleted file mode 100644 index 3f61b40112c..00000000000 --- a/news/7355.removal +++ /dev/null @@ -1,4 +0,0 @@ -Remove interpreter-specific major version tag e.g. ``cp3-none-any`` -from consideration. This behavior was not documented strictly, and this -tag in particular is `not useful <https://snarky.ca/the-challenges-in-designing-a-library-for-pep-425/>`_. -Anyone with a use case can create an issue with pypa/packaging. diff --git a/news/7359.trivial b/news/7359.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/7376.feature b/news/7376.feature deleted file mode 100644 index 43ed51345db..00000000000 --- a/news/7376.feature +++ /dev/null @@ -1,2 +0,0 @@ -Suggest a more robust command to upgrade pip itself to avoid confusion when the -current pip command is not available as ``pip``. diff --git a/news/7385.doc b/news/7385.doc deleted file mode 100644 index ec8c4a4a3cb..00000000000 --- a/news/7385.doc +++ /dev/null @@ -1 +0,0 @@ -Better document the requirements file format diff --git a/news/7393.bugfix b/news/7393.bugfix deleted file mode 100644 index a7b88d2e09a..00000000000 --- a/news/7393.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix the logging of cached HTTP response shown as downloading. diff --git a/news/7411.trivial b/news/7411.trivial deleted file mode 100644 index 9181e01abf5..00000000000 --- a/news/7411.trivial +++ /dev/null @@ -1 +0,0 @@ -Added a note about # noqa comments in the Getting Started Guide diff --git a/news/7463.trivial b/news/7463.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/7487.removal b/news/7487.removal deleted file mode 100644 index aa89bf42ad0..00000000000 --- a/news/7487.removal +++ /dev/null @@ -1,2 +0,0 @@ -Wheel processing no longer permits wheels containing more than one top-level -.dist-info directory. diff --git a/news/7488.bugfix b/news/7488.bugfix deleted file mode 100644 index 047a8c1fc8d..00000000000 --- a/news/7488.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -Effectively disable the wheel cache when it is not writable, as is the -case with the http cache. diff --git a/news/7490.trivial b/news/7490.trivial deleted file mode 100644 index 25619f36b8e..00000000000 --- a/news/7490.trivial +++ /dev/null @@ -1 +0,0 @@ -Fix unrelease bug from #7319. diff --git a/news/7498.feature b/news/7498.feature deleted file mode 100644 index 52f0c57a327..00000000000 --- a/news/7498.feature +++ /dev/null @@ -1,2 +0,0 @@ -Define all old pip console script entrypoints to prevent import issues in -stale wrapper scripts. diff --git a/news/7517.feature b/news/7517.feature deleted file mode 100644 index 089fbc38781..00000000000 --- a/news/7517.feature +++ /dev/null @@ -1,4 +0,0 @@ -The build step of ``pip wheel`` now builds all wheels to a cache first, -then copies them to the wheel directory all at once. -Before, it built them to a temporary direcory and moved -them to the wheel directory one by one. diff --git a/news/7521.trivial b/news/7521.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/7527.trivial b/news/7527.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/7541.bugfix b/news/7541.bugfix deleted file mode 100644 index 4fccfe3adf0..00000000000 --- a/news/7541.bugfix +++ /dev/null @@ -1 +0,0 @@ -Correctly handle relative cache directory provided via --cache-dir. diff --git a/news/7543.removal b/news/7543.removal deleted file mode 100644 index eab7016abc0..00000000000 --- a/news/7543.removal +++ /dev/null @@ -1,4 +0,0 @@ -Support for the ``git+git@`` form of VCS requirement is being deprecated and -will be removed in pip 21.0. Switch to ``git+https://`` or -``git+ssh://``. ``git+git://`` also works but its use is discouraged as it is -insecure. diff --git a/news/7577.trivial b/news/7577.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/7582.trivial b/news/7582.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/7587.trivial b/news/7587.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/77a2e30d-d448-43fd-9223-81dff5ae5001.trivial b/news/77a2e30d-d448-43fd-9223-81dff5ae5001.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/85a9deeb-db71-4c14-a57a-6d440995130d.trivial b/news/85a9deeb-db71-4c14-a57a-6d440995130d.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/980.feature b/news/980.feature deleted file mode 100644 index cd055e09e6e..00000000000 --- a/news/980.feature +++ /dev/null @@ -1,8 +0,0 @@ -Expand ``~`` prefix to user directory in path options, configs, and -environment variables. Values that may be either URL or path are not -currently supported, to avoid ambiguity: - -* ``--find-links`` -* ``--constraint``, ``-c`` -* ``--requirement``, ``-r`` -* ``--editable``, ``-e`` diff --git a/news/CacheControl.vendor b/news/CacheControl.vendor deleted file mode 100644 index 2c8e09f3cf0..00000000000 --- a/news/CacheControl.vendor +++ /dev/null @@ -1 +0,0 @@ -Upgrade CacheControl to 0.12.5 diff --git a/news/certifi.vendor b/news/certifi.vendor deleted file mode 100644 index 66e84cb207e..00000000000 --- a/news/certifi.vendor +++ /dev/null @@ -1 +0,0 @@ -Upgrade certifi to 2019.9.11 diff --git a/news/colorama.vendor b/news/colorama.vendor deleted file mode 100644 index 1defb49f0d1..00000000000 --- a/news/colorama.vendor +++ /dev/null @@ -1 +0,0 @@ -Upgrade colorama to 0.4.1 diff --git a/news/distlib.vendor b/news/distlib.vendor deleted file mode 100644 index 8b11e09a35b..00000000000 --- a/news/distlib.vendor +++ /dev/null @@ -1 +0,0 @@ -Upgrade distlib to 0.2.9.post0 diff --git a/news/ipaddress.vendor b/news/ipaddress.vendor deleted file mode 100644 index 902e589a918..00000000000 --- a/news/ipaddress.vendor +++ /dev/null @@ -1 +0,0 @@ -Upgrade ipaddress to 1.0.22 diff --git a/news/packaging.vendor b/news/packaging.vendor deleted file mode 100644 index a896b7e78f8..00000000000 --- a/news/packaging.vendor +++ /dev/null @@ -1 +0,0 @@ -Update packaging to 20.0. diff --git a/news/pkg_resources.vendor b/news/pkg_resources.vendor deleted file mode 100644 index 7f1972b3b06..00000000000 --- a/news/pkg_resources.vendor +++ /dev/null @@ -1 +0,0 @@ -Upgrade pkg_resources (via setuptools) to 44.0.0 diff --git a/news/pre-commit-gha-cache.trivial b/news/pre-commit-gha-cache.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/pyparsing.vendor b/news/pyparsing.vendor deleted file mode 100644 index 90374a1ef91..00000000000 --- a/news/pyparsing.vendor +++ /dev/null @@ -1 +0,0 @@ -Upgrade pyparsing to 2.4.2 diff --git a/news/six.vendor b/news/six.vendor deleted file mode 100644 index 3186ab0cfc1..00000000000 --- a/news/six.vendor +++ /dev/null @@ -1 +0,0 @@ -Upgrade six to 1.12.0 diff --git a/news/urllib3.vendor b/news/urllib3.vendor deleted file mode 100644 index 80b98b44e10..00000000000 --- a/news/urllib3.vendor +++ /dev/null @@ -1 +0,0 @@ -Upgrade urllib3 to 1.25.6 diff --git a/src/pip/__init__.py b/src/pip/__init__.py index c1e42c80fc1..8336d39e94f 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -4,7 +4,7 @@ from typing import List, Optional -__version__ = "20.0.dev0" +__version__ = "20.0" def main(args=None): From d9a315eaf6f9f902dfb702f3eafa47e06bf6bf88 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 21 Jan 2020 16:15:22 +0530 Subject: [PATCH 1174/3170] Bump for development --- src/pip/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/__init__.py b/src/pip/__init__.py index 8336d39e94f..a41767bd904 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -4,7 +4,7 @@ from typing import List, Optional -__version__ = "20.0" +__version__ = "20.1.dev0" def main(args=None): From 8f3687cfd9977039f953c9a6216fb62bbb6b4848 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 21 Jan 2020 17:48:32 +0530 Subject: [PATCH 1175/3170] Rename pip._internal.distributions.{source -> sdist} Certain environments seem to be leaving behind empty folders in this directory -- There was an older pip release that included a source/ folder here which isn't getting deleted for some reason. I have not figured out the root cause why this happens yet. This is resulting in ImportErrors since Python imports have a higher precedence for packages compared to modules. This commit changes the name we're trying to import from, which should help prevent this mode of failure. --- src/pip/_internal/distributions/__init__.py | 2 +- src/pip/_internal/distributions/{source.py => sdist.py} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/pip/_internal/distributions/{source.py => sdist.py} (100%) diff --git a/src/pip/_internal/distributions/__init__.py b/src/pip/_internal/distributions/__init__.py index 20ffe2b32b5..d5c1afc5bc1 100644 --- a/src/pip/_internal/distributions/__init__.py +++ b/src/pip/_internal/distributions/__init__.py @@ -1,4 +1,4 @@ -from pip._internal.distributions.source import SourceDistribution +from pip._internal.distributions.sdist import SourceDistribution from pip._internal.distributions.wheel import WheelDistribution from pip._internal.utils.typing import MYPY_CHECK_RUNNING diff --git a/src/pip/_internal/distributions/source.py b/src/pip/_internal/distributions/sdist.py similarity index 100% rename from src/pip/_internal/distributions/source.py rename to src/pip/_internal/distributions/sdist.py From e103345cb3223fdc97a57eb467cb216bb15a1f36 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 21 Jan 2020 18:12:47 +0530 Subject: [PATCH 1176/3170] Add a bugfix for our hot fix release --- news/7621.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/7621.bugfix diff --git a/news/7621.bugfix b/news/7621.bugfix new file mode 100644 index 00000000000..1ed04aae44b --- /dev/null +++ b/news/7621.bugfix @@ -0,0 +1 @@ +Rename an internal module, to avoid ImportErrors due to improper uninstallation. From 0b397b42c5159942c73a882ca5f33364502e1c3a Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 21 Jan 2020 18:12:53 +0530 Subject: [PATCH 1177/3170] Bump for release --- NEWS.rst | 9 +++++++++ news/7621.bugfix | 1 - src/pip/__init__.py | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) delete mode 100644 news/7621.bugfix diff --git a/NEWS.rst b/NEWS.rst index 949065dfa74..9e03d5ce63b 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -7,6 +7,15 @@ .. towncrier release notes start +20.0.1 (2020-01-21) +=================== + +Bug Fixes +--------- + +- Rename an internal module, to avoid ImportErrors due to improper uninstallation. (`#7621 <https://github.com/pypa/pip/issues/7621>`_) + + 20.0 (2020-01-21) ================= diff --git a/news/7621.bugfix b/news/7621.bugfix deleted file mode 100644 index 1ed04aae44b..00000000000 --- a/news/7621.bugfix +++ /dev/null @@ -1 +0,0 @@ -Rename an internal module, to avoid ImportErrors due to improper uninstallation. diff --git a/src/pip/__init__.py b/src/pip/__init__.py index a41767bd904..9cea7bbe530 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -4,7 +4,7 @@ from typing import List, Optional -__version__ = "20.1.dev0" +__version__ = "20.0.1" def main(args=None): From b6fb081e4b4c36d5d73134e7e6befbe4bff91cfc Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 21 Jan 2020 18:12:54 +0530 Subject: [PATCH 1178/3170] Bump for development --- src/pip/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/__init__.py b/src/pip/__init__.py index 9cea7bbe530..a41767bd904 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -4,7 +4,7 @@ from typing import List, Optional -__version__ = "20.0.1" +__version__ = "20.1.dev0" def main(args=None): From bda17b73e618f2d229508f6c2c902168817f8179 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 21 Jan 2020 18:33:33 +0530 Subject: [PATCH 1179/3170] Add dist/ prefix to expected filenames --- noxfile.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/noxfile.py b/noxfile.py index c19ff37c1a8..655aa076e8e 100644 --- a/noxfile.py +++ b/noxfile.py @@ -234,8 +234,8 @@ def upload_release(session): ) # Sanity check: Make sure the files are correctly named. expected_distribution_files = [ - f"pip-{version}-py2.py3-none-any.whl", - f"pip-{version}.tar.gz", + f"dist/pip-{version}-py2.py3-none-any.whl", + f"dist/pip-{version}.tar.gz", ] if sorted(distribution_files) != sorted(expected_distribution_files): session.error( From b9555fb9d48ea97a782530db9edb47c8e2578de0 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 21 Jan 2020 18:35:12 +0530 Subject: [PATCH 1180/3170] Empty build directory before building distributions Mainly to avoid pypa/wheel#147 --- noxfile.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/noxfile.py b/noxfile.py index 655aa076e8e..2151ce37e08 100644 --- a/noxfile.py +++ b/noxfile.py @@ -203,6 +203,10 @@ def build_release(session): session.log("# Checkout the tag") session.run("git", "checkout", version, external=True, silent=True) + session.log("# Cleanup build/ before building the wheel") + if release.have_files_in_folder("build"): + shutil.rmtree("build") + session.log("# Build distributions") session.run("python", "setup.py", "sdist", "bdist_wheel", silent=True) From 389443117c714747d149f7bdf751fe3acf4c1690 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 23 Jan 2020 13:15:36 +0530 Subject: [PATCH 1181/3170] Revert "Explicitly set newline when rewriting for release (#7600)" This reverts commit c55eee4188638a861a8a12413e78e4ae763718fd. --- tools/automation/release/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tools/automation/release/__init__.py b/tools/automation/release/__init__.py index 0e26806c4e9..e983f4cbb80 100644 --- a/tools/automation/release/__init__.py +++ b/tools/automation/release/__init__.py @@ -3,6 +3,7 @@ These are written according to the order they are called in. """ +import io import os import subprocess from typing import List, Optional, Set @@ -69,7 +70,7 @@ def generate_authors(filename: str) -> None: authors = get_author_list() # Write our authors to the AUTHORS file - with open(filename, "w", encoding="utf-8", newline="\n") as fp: + with io.open(filename, "w", encoding="utf-8") as fp: fp.write(u"\n".join(authors)) fp.write(u"\n") @@ -89,7 +90,7 @@ def update_version_file(version: str, filepath: str) -> None: content = list(f) file_modified = False - with open(filepath, "w", encoding="utf-8", newline="\n") as f: + with open(filepath, "w", encoding="utf-8") as f: for line in content: if line.startswith("__version__ ="): f.write('__version__ = "{}"\n'.format(version)) From 848472ea4e2046b6c2d65e634efb639633074823 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Thu, 23 Jan 2020 10:53:33 +0000 Subject: [PATCH 1182/3170] Modify gitattributes to match changed location of vendoring patches --- .gitattributes | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.gitattributes b/.gitattributes index 2793a6c1062..7b547a58cc2 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,4 @@ # Patches must have Unix-style line endings, even on Windows -tasks/vendoring/patches/* eol=lf +tools/automation/vendoring/patches/* eol=lf # The CA Bundle should always use Unix-style line endings, even on Windows src/pip/_vendor/certifi/*.pem eol=lf From b242c3979af2c6c67c76e8cee8c12d7e592f8abe Mon Sep 17 00:00:00 2001 From: Deepak Sharma <deepshar@redhat.com> Date: Mon, 20 Jan 2020 20:39:57 +0530 Subject: [PATCH 1183/3170] String formatting updated --- ...1aae95-1bc6-4a32-b005-65d0a7843207.trivial | 0 src/pip/_internal/req/__init__.py | 9 ++++--- src/pip/_internal/req/req_tracker.py | 2 +- tests/functional/test_install_cleanup.py | 26 +++++++++---------- tests/functional/test_install_index.py | 12 ++++----- tests/functional/test_install_reqs.py | 7 ++--- tests/functional/test_show.py | 2 +- 7 files changed, 30 insertions(+), 28 deletions(-) create mode 100644 news/c11aae95-1bc6-4a32-b005-65d0a7843207.trivial diff --git a/news/c11aae95-1bc6-4a32-b005-65d0a7843207.trivial b/news/c11aae95-1bc6-4a32-b005-65d0a7843207.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/req/__init__.py b/src/pip/_internal/req/__init__.py index d2d027adeec..05e08972852 100644 --- a/src/pip/_internal/req/__init__.py +++ b/src/pip/_internal/req/__init__.py @@ -48,17 +48,18 @@ def install_given_reqs( """ if to_install: - logger.info( - 'Installing collected packages: %s', - ', '.join([req.name for req in to_install]), + msg = 'Installing collected packages: {}'.format( + ', '.join([req.name for req in to_install]) ) + logger.info(msg) installed = [] with indent_log(): for requirement in to_install: if requirement.should_reinstall: - logger.info('Attempting uninstall: %s', requirement.name) + logger.info('Attempting uninstall: {}'.format( + requirement.name)) with indent_log(): uninstalled_pathset = requirement.uninstall( auto_confirm=True diff --git a/src/pip/_internal/req/req_tracker.py b/src/pip/_internal/req/req_tracker.py index 84e0c0419fc..d72a41d203a 100644 --- a/src/pip/_internal/req/req_tracker.py +++ b/src/pip/_internal/req/req_tracker.py @@ -60,7 +60,7 @@ def get_requirement_tracker(): TempDirectory(kind='req-tracker') ).path ctx.enter_context(update_env_context_manager(PIP_REQ_TRACKER=root)) - logger.debug("Initialized build tracking at %s", root) + logger.debug("Initialized build tracking at {}".format(root)) with RequirementTracker(root) as tracker: yield tracker diff --git a/tests/functional/test_install_cleanup.py b/tests/functional/test_install_cleanup.py index d9669f74335..1a51d528235 100644 --- a/tests/functional/test_install_cleanup.py +++ b/tests/functional/test_install_cleanup.py @@ -14,12 +14,14 @@ def test_cleanup_after_install(script, data): Test clean up after installing a package. """ script.pip( - 'install', '--no-index', '--find-links=%s' % data.find_links, 'simple' + 'install', '--no-index', + '--find-links={}'.format(data.find_links), + 'simple' ) build = script.venv_path / "build" src = script.venv_path / "src" - assert not exists(build), "build/ dir still exists: %s" % build - assert not exists(src), "unexpected src/ dir exists: %s" % src + assert not exists(build), "build/ dir still exists: {}".format(build) + assert not exists(src), "unexpected src/ dir exists: {}" .format(src) script.assert_no_temp() @@ -31,7 +33,7 @@ def test_no_clean_option_blocks_cleaning_after_install(script, data): build = script.base_path / 'pip-build' script.pip( 'install', '--no-clean', '--no-index', '--build', build, - '--find-links=%s' % data.find_links, 'simple', expect_temp=True, + '--find-links={}'.format(data.find_links), 'simple', expect_temp=True, ) assert exists(build) @@ -43,16 +45,14 @@ def test_cleanup_after_install_editable_from_hg(script, tmpdir): Test clean up after cloning from Mercurial. """ - script.pip( - 'install', - '-e', - '%s#egg=ScriptTest' % - local_checkout('hg+https://bitbucket.org/ianb/scripttest', tmpdir), + requirement = '{}#egg=ScriptTest'.format( + local_checkout('hg+https://bitbucket.org/ianb/scripttest', tmpdir) ) + script.pip('install', '-e', requirement) build = script.venv_path / 'build' src = script.venv_path / 'src' - assert not exists(build), "build/ dir still exists: %s" % build - assert exists(src), "expected src/ dir doesn't exist: %s" % src + assert not exists(build), "build/ dir still exists: {}".format(build) + assert exists(src), "expected src/ dir doesn't exist: {}".format(src) script.assert_no_temp() @@ -64,8 +64,8 @@ def test_cleanup_after_install_from_local_directory(script, data): script.pip('install', to_install) build = script.venv_path / 'build' src = script.venv_path / 'src' - assert not exists(build), "unexpected build/ dir exists: %s" % build - assert not exists(src), "unexpected src/ dir exist: %s" % src + assert not exists(build), "unexpected build/ dir exists: {}".format(build) + assert not exists(src), "unexpected src/ dir exist: {}".format(src) script.assert_no_temp() diff --git a/tests/functional/test_install_index.py b/tests/functional/test_install_index.py index 5a31db8b482..60f09c5ad7a 100644 --- a/tests/functional/test_install_index.py +++ b/tests/functional/test_install_index.py @@ -17,7 +17,7 @@ def test_find_links_relative_path(script, data): cwd=data.root, ) egg_info_folder = ( - script.site_packages / 'parent-0.1-py%s.egg-info' % pyversion + script.site_packages / 'parent-0.1-py{}.egg-info'.format(pyversion) ) initools_folder = script.site_packages / 'parent' assert egg_info_folder in result.files_created, str(result) @@ -28,9 +28,9 @@ def test_find_links_requirements_file_relative_path(script, data): """Test find-links as a relative path to a reqs file.""" script.scratch_path.joinpath("test-req.txt").write_text(textwrap.dedent(""" --no-index - --find-links=%s + --find-links={} parent==0.1 - """ % data.packages.replace(os.path.sep, '/'))) + """ .format(data.packages.replace(os.path.sep, '/')))) result = script.pip( 'install', '-r', @@ -38,7 +38,7 @@ def test_find_links_requirements_file_relative_path(script, data): cwd=data.root, ) egg_info_folder = ( - script.site_packages / 'parent-0.1-py%s.egg-info' % pyversion + script.site_packages / 'parent-0.1-py{}.egg-info'.format(pyversion) ) initools_folder = script.site_packages / 'parent' assert egg_info_folder in result.files_created, str(result) @@ -52,7 +52,7 @@ def test_install_from_file_index_hash_link(script, data): """ result = script.pip('install', '-i', data.index_url(), 'simple==1.0') egg_info_folder = ( - script.site_packages / 'simple-1.0-py%s.egg-info' % pyversion + script.site_packages / 'simple-1.0-py{}.egg-info'.format(pyversion) ) assert egg_info_folder in result.files_created, str(result) @@ -69,5 +69,5 @@ def test_file_index_url_quoting(script, data): str(result.stdout) ) assert ( - script.site_packages / 'simple-1.0-py%s.egg-info' % pyversion + script.site_packages / 'simple-1.0-py{}.egg-info'.format(pyversion) ) in result.files_created, str(result) diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index 2584e09535a..2422bdb500e 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -30,12 +30,13 @@ def test_requirements_file(script): 'install', '-r', script.scratch_path / 'initools-req.txt' ) assert ( - script.site_packages / 'INITools-0.2-py%s.egg-info' % - pyversion in result.files_created + script.site_packages / 'INITools-0.2-py{}.egg-info'.format( + pyversion in result.files_created) ) assert script.site_packages / 'initools' in result.files_created assert result.files_created[script.site_packages / other_lib_name].dir - fn = '%s-%s-py%s.egg-info' % (other_lib_name, other_lib_version, pyversion) + fn = '{}-{}-py{}.egg-info'.format( + other_lib_name, other_lib_version, pyversion) assert result.files_created[script.site_packages / fn].dir diff --git a/tests/functional/test_show.py b/tests/functional/test_show.py index 4cbccc39b54..a4000a20a03 100644 --- a/tests/functional/test_show.py +++ b/tests/functional/test_show.py @@ -15,7 +15,7 @@ def test_basic_show(script): lines = result.stdout.splitlines() assert len(lines) == 10 assert 'Name: pip' in lines - assert 'Version: %s' % __version__ in lines + assert 'Version: {}'.format(__version__) in lines assert any(line.startswith('Location: ') for line in lines) assert 'Requires: ' in lines From 78a77229b40dd109c65f4a1563a0befa74ce647f Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Fri, 24 Jan 2020 16:37:38 +0530 Subject: [PATCH 1184/3170] Upgrade packaging to 20.1 --- news/packaging.vendor | 1 + src/pip/_vendor/packaging/__about__.py | 2 +- src/pip/_vendor/packaging/tags.py | 5 ++--- src/pip/_vendor/vendor.txt | 2 +- 4 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 news/packaging.vendor diff --git a/news/packaging.vendor b/news/packaging.vendor new file mode 100644 index 00000000000..f2094519492 --- /dev/null +++ b/news/packaging.vendor @@ -0,0 +1 @@ +Upgrade packaging to 20.1 diff --git a/src/pip/_vendor/packaging/__about__.py b/src/pip/_vendor/packaging/__about__.py index 26947283752..08d2c892b83 100644 --- a/src/pip/_vendor/packaging/__about__.py +++ b/src/pip/_vendor/packaging/__about__.py @@ -18,7 +18,7 @@ __summary__ = "Core utilities for Python packages" __uri__ = "https://github.com/pypa/packaging" -__version__ = "20.0" +__version__ = "20.1" __author__ = "Donald Stufft and individual contributors" __email__ = "donald@stufft.io" diff --git a/src/pip/_vendor/packaging/tags.py b/src/pip/_vendor/packaging/tags.py index 20512aaf992..60a69d8f943 100644 --- a/src/pip/_vendor/packaging/tags.py +++ b/src/pip/_vendor/packaging/tags.py @@ -315,7 +315,7 @@ def _py_interpreter_range(py_version): def compatible_tags( python_version=None, # type: Optional[PythonVersion] interpreter=None, # type: Optional[str] - platforms=None, # type: Optional[Iterator[str]] + platforms=None, # type: Optional[Iterable[str]] ): # type: (...) -> Iterator[Tag] """ @@ -328,8 +328,7 @@ def compatible_tags( """ if not python_version: python_version = sys.version_info[:2] - if not platforms: - platforms = _platform_tags() + platforms = list(platforms or _platform_tags()) for version in _py_interpreter_range(python_version): for platform_ in platforms: yield Tag(version, "none", platform_) diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index eac6d96cc1d..cbc2830ac09 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -7,7 +7,7 @@ distro==1.4.0 html5lib==1.0.1 ipaddress==1.0.23 # Only needed on 2.6 and 2.7 msgpack==0.6.2 -packaging==20.0 +packaging==20.1 pep517==0.7.0 progress==1.5 pyparsing==2.4.6 From 1cf779c1ea88053c690686571d67826f11463232 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Fri, 24 Jan 2020 16:37:53 +0530 Subject: [PATCH 1185/3170] Add a NEWS fragment for release notes --- news/7626.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/7626.bugfix diff --git a/news/7626.bugfix b/news/7626.bugfix new file mode 100644 index 00000000000..8d2ef2336df --- /dev/null +++ b/news/7626.bugfix @@ -0,0 +1 @@ +Fix a regression in generation of compatibility tags. From 931749f8712daf73fff7a4b3869ee177a849eaa7 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Fri, 24 Jan 2020 20:17:34 +0530 Subject: [PATCH 1186/3170] Bump for release --- NEWS.rst | 14 ++++++++++++++ news/7626.bugfix | 1 - news/packaging.vendor | 1 - src/pip/__init__.py | 2 +- 4 files changed, 15 insertions(+), 3 deletions(-) delete mode 100644 news/7626.bugfix delete mode 100644 news/packaging.vendor diff --git a/NEWS.rst b/NEWS.rst index 9e03d5ce63b..1bacf36df15 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -7,6 +7,20 @@ .. towncrier release notes start +20.0.2 (2020-01-24) +=================== + +Bug Fixes +--------- + +- Fix a regression in generation of compatibility tags. (`#7626 <https://github.com/pypa/pip/issues/7626>`_) + +Vendored Libraries +------------------ + +- Upgrade packaging to 20.1 + + 20.0.1 (2020-01-21) =================== diff --git a/news/7626.bugfix b/news/7626.bugfix deleted file mode 100644 index 8d2ef2336df..00000000000 --- a/news/7626.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix a regression in generation of compatibility tags. diff --git a/news/packaging.vendor b/news/packaging.vendor deleted file mode 100644 index f2094519492..00000000000 --- a/news/packaging.vendor +++ /dev/null @@ -1 +0,0 @@ -Upgrade packaging to 20.1 diff --git a/src/pip/__init__.py b/src/pip/__init__.py index a41767bd904..827a4e20a7b 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -4,7 +4,7 @@ from typing import List, Optional -__version__ = "20.1.dev0" +__version__ = "20.0.2" def main(args=None): From a4fac3f8825dc09c834d14c88db44b507c686833 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Fri, 24 Jan 2020 20:17:38 +0530 Subject: [PATCH 1187/3170] Bump for development --- src/pip/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/__init__.py b/src/pip/__init__.py index 827a4e20a7b..a41767bd904 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -4,7 +4,7 @@ from typing import List, Optional -__version__ = "20.0.2" +__version__ = "20.1.dev0" def main(args=None): From eae86210769cdb334b45dc95f019d650b30ff87b Mon Sep 17 00:00:00 2001 From: Hugo <hugovk@users.noreply.github.com> Date: Fri, 24 Jan 2020 19:03:53 +0200 Subject: [PATCH 1188/3170] Replace soon-unsupported macOS 10.13 with 10.14 --- .azure-pipelines/macos.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.azure-pipelines/macos.yml b/.azure-pipelines/macos.yml index 7408a38840f..85c2a0246af 100644 --- a/.azure-pipelines/macos.yml +++ b/.azure-pipelines/macos.yml @@ -1,8 +1,8 @@ jobs: - template: jobs/test.yml parameters: - vmImage: xcode9-macos10.13 + vmImage: macos-10.14 - template: jobs/package.yml parameters: - vmImage: xcode9-macos10.13 + vmImage: macos-10.14 From d028af98e3f86dc2d36fc9def07708cd034be428 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 25 Jan 2020 17:31:36 -0500 Subject: [PATCH 1189/3170] Remove unnecessary write_delete_marker_file InstallRequirement only checks for a delete marker file in source_dir. Since the result of ensure_build_location is set to source_dir (in ensure_has_source_dir), and ensure_build_location returns a subdirectory of the build_directory to which we write the delete marker file, then this delete marker file is never used. --- src/pip/_internal/req/req_install.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 22ac24b96d3..6bbad88578a 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -36,7 +36,6 @@ from pip._internal.utils.marker_files import ( PIP_DELETE_MARKER_FILENAME, has_delete_marker_file, - write_delete_marker_file, ) from pip._internal.utils.misc import ( ask_path_exists, @@ -371,7 +370,6 @@ def ensure_build_location(self, build_dir): if not os.path.exists(build_dir): logger.debug('Creating directory %s', build_dir) os.makedirs(build_dir) - write_delete_marker_file(build_dir) return os.path.join(build_dir, name) def _set_requirement(self): From 964fb0d8e998ff3544366e92e1ddea1fcc3bd924 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko <wk@sydorenko.org.ua> Date: Tue, 21 Jan 2020 16:46:53 +0100 Subject: [PATCH 1190/3170] Add a config for the issue template chooser --- .github/ISSUE_TEMPLATE/config.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/config.yml diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml new file mode 100644 index 00000000000..e360c8dc2a2 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -0,0 +1,15 @@ +# Ref: https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository#configuring-the-template-chooser +blank_issues_enabled: true # default +contact_links: +- name: '💬 IRC: #pypa @ Freenode' + url: https://webchat.freenode.net/#pypa + about: Chat with devs. +- name: 🤷💻🤦 Discourse + url: https://discuss.python.org/c/packaging + about: Please ask typical Q&A here. +- name: 🔐 Security Policy + url: https://pypi.org/security/ + about: Please learn how to report security vulnerabilities here. +- name: 📝 PyPA Code of Conduct + url: https://www.pypa.io/en/latest/code-of-conduct/ + about: ❤ Be nice to other members of the community. ☮ Behave. From 00cb3684367829d11562ae869216e3689bd85c7c Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko <wk@sydorenko.org.ua> Date: Sun, 26 Jan 2020 03:50:18 +0100 Subject: [PATCH 1191/3170] Take out security policy from the issue chooser --- .github/ISSUE_TEMPLATE/config.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index e360c8dc2a2..7fd401fbf2e 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -7,9 +7,6 @@ contact_links: - name: 🤷💻🤦 Discourse url: https://discuss.python.org/c/packaging about: Please ask typical Q&A here. -- name: 🔐 Security Policy - url: https://pypi.org/security/ - about: Please learn how to report security vulnerabilities here. - name: 📝 PyPA Code of Conduct url: https://www.pypa.io/en/latest/code-of-conduct/ about: ❤ Be nice to other members of the community. ☮ Behave. From 31b837ecd3ba96656421ab58dd2b1dd7659b5e2f Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko <wk@sydorenko.org.ua> Date: Sun, 26 Jan 2020 03:51:58 +0100 Subject: [PATCH 1192/3170] Drop trailing dots for consistency --- .github/ISSUE_TEMPLATE/config.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 7fd401fbf2e..f575da2f28c 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -3,10 +3,10 @@ blank_issues_enabled: true # default contact_links: - name: '💬 IRC: #pypa @ Freenode' url: https://webchat.freenode.net/#pypa - about: Chat with devs. + about: Chat with devs - name: 🤷💻🤦 Discourse url: https://discuss.python.org/c/packaging - about: Please ask typical Q&A here. + about: Please ask typical Q&A here - name: 📝 PyPA Code of Conduct url: https://www.pypa.io/en/latest/code-of-conduct/ about: ❤ Be nice to other members of the community. ☮ Behave. From 4e821c23c0aaa61e28a982ab7f4a4af962023974 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko <wk@sydorenko.org.ua> Date: Sun, 26 Jan 2020 03:52:49 +0100 Subject: [PATCH 1193/3170] Extend the description of the discourse ref --- .github/ISSUE_TEMPLATE/config.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index f575da2f28c..dc37e964c3e 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -6,7 +6,9 @@ contact_links: about: Chat with devs - name: 🤷💻🤦 Discourse url: https://discuss.python.org/c/packaging - about: Please ask typical Q&A here + about: | + Please ask typical Q&A here: general ideas for Python packaging, + questions about structuring projects and so on - name: 📝 PyPA Code of Conduct url: https://www.pypa.io/en/latest/code-of-conduct/ about: ❤ Be nice to other members of the community. ☮ Behave. From ea17da2b58fead0c5b3c7867fd8bc72f20bba89f Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko <wk@sydorenko.org.ua> Date: Sun, 26 Jan 2020 03:53:51 +0100 Subject: [PATCH 1194/3170] Reorder IRC and discourse links --- .github/ISSUE_TEMPLATE/config.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index dc37e964c3e..3babf35bdb0 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,14 +1,14 @@ # Ref: https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository#configuring-the-template-chooser blank_issues_enabled: true # default contact_links: -- name: '💬 IRC: #pypa @ Freenode' - url: https://webchat.freenode.net/#pypa - about: Chat with devs - name: 🤷💻🤦 Discourse url: https://discuss.python.org/c/packaging about: | Please ask typical Q&A here: general ideas for Python packaging, questions about structuring projects and so on +- name: '💬 IRC: #pypa @ Freenode' + url: https://webchat.freenode.net/#pypa + about: Chat with devs - name: 📝 PyPA Code of Conduct url: https://www.pypa.io/en/latest/code-of-conduct/ about: ❤ Be nice to other members of the community. ☮ Behave. From dd8753cdeef9d909ba255679eeccb20c5e5353d7 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 29 Jan 2020 08:47:52 -0500 Subject: [PATCH 1195/3170] Mitigate Windows test failures due to PAX-format wheel release --- tests/functional/test_install.py | 4 ++++ tests/functional/test_install_cleanup.py | 3 ++- tests/functional/test_pep517.py | 3 ++- tests/lib/__init__.py | 7 +++++++ 4 files changed, 15 insertions(+), 2 deletions(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 3c31534ce96..a95b46741dd 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -27,6 +27,7 @@ requirements_file, skip_if_not_python2, skip_if_python2, + windows_workaround_7667, ) from tests.lib.filesystem import make_socket_file from tests.lib.local_repos import local_checkout @@ -726,6 +727,7 @@ def test_install_using_install_option_and_editable(script, tmpdir): @pytest.mark.network @need_mercurial +@windows_workaround_7667 def test_install_global_option_using_editable(script, tmpdir): """ Test using global distutils options, but in an editable installation @@ -1333,6 +1335,7 @@ def test_install_no_binary_disables_building_wheels(script, data, with_wheel): @pytest.mark.network +@windows_workaround_7667 def test_install_no_binary_builds_pep_517_wheel(script, data, with_wheel): to_install = data.packages.joinpath('pep517_setup_and_pyproject') res = script.pip( @@ -1347,6 +1350,7 @@ def test_install_no_binary_builds_pep_517_wheel(script, data, with_wheel): @pytest.mark.network +@windows_workaround_7667 def test_install_no_binary_uses_local_backend( script, data, with_wheel, tmpdir): to_install = data.packages.joinpath('pep517_wrapper_buildsys') diff --git a/tests/functional/test_install_cleanup.py b/tests/functional/test_install_cleanup.py index 1a51d528235..8810402f416 100644 --- a/tests/functional/test_install_cleanup.py +++ b/tests/functional/test_install_cleanup.py @@ -5,7 +5,7 @@ from pip._internal.cli.status_codes import PREVIOUS_BUILD_DIR_ERROR from pip._internal.utils.marker_files import write_delete_marker_file -from tests.lib import need_mercurial +from tests.lib import need_mercurial, windows_workaround_7667 from tests.lib.local_repos import local_checkout @@ -40,6 +40,7 @@ def test_no_clean_option_blocks_cleaning_after_install(script, data): @pytest.mark.network @need_mercurial +@windows_workaround_7667 def test_cleanup_after_install_editable_from_hg(script, tmpdir): """ Test clean up after cloning from Mercurial. diff --git a/tests/functional/test_pep517.py b/tests/functional/test_pep517.py index b16e692c5ac..d932f2ef83b 100644 --- a/tests/functional/test_pep517.py +++ b/tests/functional/test_pep517.py @@ -3,7 +3,7 @@ from pip._internal.build_env import BuildEnvironment from pip._internal.req import InstallRequirement -from tests.lib import make_test_finder, path_to_url +from tests.lib import make_test_finder, path_to_url, windows_workaround_7667 def make_project(tmpdir, requires=[], backend=None, backend_path=None): @@ -249,6 +249,7 @@ def test_explicit_setuptools_backend(script, tmpdir, data, common_wheels): @pytest.mark.network +@windows_workaround_7667 def test_pep517_and_build_options(script, tmpdir, data, common_wheels): """Backend generated requirements are installed in the build env""" project_dir, name = make_pyproject_with_setup(tmpdir) diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index 9a55c156e4a..c0baa0a1cfb 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -1116,3 +1116,10 @@ def need_mercurial(fn): skip_if_python2 = pytest.mark.skipif(PY2, reason="Non-Python 2 only") skip_if_not_python2 = pytest.mark.skipif(not PY2, reason="Python 2 only") + + +# Workaround for test failures after new wheel release. +windows_workaround_7667 = pytest.mark.skipif( + "sys.platform == 'win32' and sys.version_info < (3,)", + reason="Workaround for #7667", +) From 45991bcc1e97dcd3b21844980ecc0139c6f30c57 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 25 Jan 2020 14:01:32 -0500 Subject: [PATCH 1196/3170] Use explicit default value for TempDirectory delete flag Now we can opt-in to globally-managed + globally-configured file deletion for pre-existing directories by passing an explicit `None`. --- src/pip/_internal/utils/temp_dir.py | 24 ++++++++++++++++++------ tests/unit/test_utils_temp_dir.py | 22 ++++++++++++++++++++++ 2 files changed, 40 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/utils/temp_dir.py b/src/pip/_internal/utils/temp_dir.py index 65e41bc70e2..ae730e4917e 100644 --- a/src/pip/_internal/utils/temp_dir.py +++ b/src/pip/_internal/utils/temp_dir.py @@ -13,7 +13,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Any, Dict, Iterator, Optional, TypeVar + from typing import Any, Dict, Iterator, Optional, TypeVar, Union _T = TypeVar('_T', bound='TempDirectory') @@ -77,6 +77,13 @@ def tempdir_registry(): _tempdir_registry = old_tempdir_registry +class _Default(object): + pass + + +_default = _Default() + + class TempDirectory(object): """Helper class that owns and cleans up a temporary directory. @@ -101,16 +108,21 @@ class TempDirectory(object): def __init__( self, path=None, # type: Optional[str] - delete=None, # type: Optional[bool] + delete=_default, # type: Union[bool, None, _Default] kind="temp", # type: str globally_managed=False, # type: bool ): super(TempDirectory, self).__init__() - # If we were given an explicit directory, resolve delete option now. - # Otherwise we wait until cleanup and see what tempdir_registry says. - if path is not None and delete is None: - delete = False + if delete is _default: + if path is not None: + # If we were given an explicit directory, resolve delete option + # now. + delete = False + else: + # Otherwise, we wait until cleanup and see what + # tempdir_registry says. + delete = None if path is None: path = self._create(kind) diff --git a/tests/unit/test_utils_temp_dir.py b/tests/unit/test_utils_temp_dir.py index 06055084e0c..0d1b0a5ea20 100644 --- a/tests/unit/test_utils_temp_dir.py +++ b/tests/unit/test_utils_temp_dir.py @@ -10,6 +10,7 @@ from pip._internal.utils.temp_dir import ( AdjacentTempDirectory, TempDirectory, + _default, global_tempdir_manager, tempdir_registry, ) @@ -216,12 +217,15 @@ def test_tempdirectory_asserts_global_tempdir(monkeypatch): @pytest.mark.parametrize("delete,kind,exists", [ (None, deleted_kind, False), + (_default, deleted_kind, False), (True, deleted_kind, False), (False, deleted_kind, True), (None, not_deleted_kind, True), + (_default, not_deleted_kind, True), (True, not_deleted_kind, False), (False, not_deleted_kind, True), (None, "unspecified", False), + (_default, "unspecified", False), (True, "unspecified", False), (False, "unspecified", True), ]) @@ -236,6 +240,24 @@ def test_tempdir_registry(kind, delete, exists): assert os.path.exists(path) == exists +@pytest.mark.parametrize("delete,exists", [ + (_default, True), (None, False) +]) +def test_temp_dir_does_not_delete_explicit_paths_by_default( + tmpdir, delete, exists +): + path = tmpdir / "example" + path.mkdir() + + with tempdir_registry() as registry: + registry.set_delete(deleted_kind, True) + + with TempDirectory(path=path, delete=delete, kind=deleted_kind) as d: + assert str(d.path) == path + assert os.path.exists(path) + assert os.path.exists(path) == exists + + @pytest.mark.parametrize("should_delete", [True, False]) def test_tempdir_registry_lazy(should_delete): """ From f2af7df45381a86f4e2d6ed6da19cee4780c9045 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 25 Jan 2020 15:45:49 -0500 Subject: [PATCH 1197/3170] Use tempdir_registry to control auto-deleted files globally Next we can actually transition some files to be globally-managed that are only deleted conditionally. --- src/pip/_internal/cli/base_command.py | 17 ++++++- tests/conftest.py | 11 +++-- tests/unit/test_base_command.py | 68 ++++++++++++++++++++++++++- 3 files changed, 90 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index 628faa3eee0..8c504a1ec12 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -34,14 +34,21 @@ from pip._internal.utils.filesystem import check_path_owner from pip._internal.utils.logging import BrokenStdoutLoggingError, setup_logging from pip._internal.utils.misc import get_prog, normalize_path -from pip._internal.utils.temp_dir import global_tempdir_manager +from pip._internal.utils.temp_dir import ( + global_tempdir_manager, + tempdir_registry, +) from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.virtualenv import running_under_virtualenv if MYPY_CHECK_RUNNING: - from typing import List, Tuple, Any + from typing import List, Optional, Tuple, Any from optparse import Values + from pip._internal.utils.temp_dir import ( + TempDirectoryTypeRegistry as TempDirRegistry + ) + __all__ = ['Command'] logger = logging.getLogger(__name__) @@ -68,6 +75,8 @@ def __init__(self, name, summary, isolated=False): self.summary = summary self.parser = ConfigOptionParser(**parser_kw) + self.tempdir_registry = None # type: Optional[TempDirRegistry] + # Commands should add options to this option group optgroup_name = '%s Options' % self.name.capitalize() self.cmd_opts = optparse.OptionGroup(self.parser, optgroup_name) @@ -108,6 +117,10 @@ def main(self, args): def _main(self, args): # type: (List[str]) -> int + # We must initialize this before the tempdir manager, otherwise the + # configuration would not be accessible by the time we clean up the + # tempdir manager. + self.tempdir_registry = self.enter_context(tempdir_registry()) # Intentionally set as early as possible so globally-managed temporary # directories are available to the rest of the code. self.enter_context(global_tempdir_manager()) diff --git a/tests/conftest.py b/tests/conftest.py index fd8204713ee..3430e271d77 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,7 +10,7 @@ import pytest import six -from pip._vendor.contextlib2 import ExitStack +from pip._vendor.contextlib2 import ExitStack, nullcontext from setuptools.wheel import Wheel from pip._internal.cli.main import main as pip_entry_point @@ -188,13 +188,18 @@ def isolate(tmpdir): @pytest.fixture(autouse=True) -def scoped_global_tempdir_manager(): +def scoped_global_tempdir_manager(request): """Make unit tests with globally-managed tempdirs easier Each test function gets its own individual scope for globally-managed temporary directories in the application. """ - with global_tempdir_manager(): + if "no_auto_tempdir_manager" in request.keywords: + ctx = nullcontext + else: + ctx = global_tempdir_manager + + with ctx(): yield diff --git a/tests/unit/test_base_command.py b/tests/unit/test_base_command.py index ba34b3922f8..a58f83b3430 100644 --- a/tests/unit/test_base_command.py +++ b/tests/unit/test_base_command.py @@ -2,10 +2,14 @@ import os import time -from mock import patch +import pytest +from mock import Mock, patch from pip._internal.cli.base_command import Command +from pip._internal.cli.status_codes import SUCCESS +from pip._internal.utils import temp_dir from pip._internal.utils.logging import BrokenStdoutLoggingError +from pip._internal.utils.temp_dir import TempDirectory class FakeCommand(Command): @@ -145,3 +149,65 @@ def test_unicode_messages(self, tmpdir): cmd = FakeCommandWithUnicode() log_path = tmpdir.joinpath('log') cmd.main(['fake_unicode', '--log', log_path]) + + +@pytest.mark.no_auto_tempdir_manager +def test_base_command_provides_tempdir_helpers(): + assert temp_dir._tempdir_manager is None + assert temp_dir._tempdir_registry is None + + def assert_helpers_set(options, args): + assert temp_dir._tempdir_manager is not None + assert temp_dir._tempdir_registry is not None + + c = Command("fake", "fake") + c.run = Mock(side_effect=assert_helpers_set) + assert c.main(["fake"]) == SUCCESS + c.run.assert_called_once() + + +not_deleted = "not_deleted" + + +@pytest.mark.parametrize("kind,exists", [ + (not_deleted, True), ("deleted", False) +]) +@pytest.mark.no_auto_tempdir_manager +def test_base_command_global_tempdir_cleanup(kind, exists): + assert temp_dir._tempdir_manager is None + assert temp_dir._tempdir_registry is None + + class Holder(object): + value = None + + def create_temp_dirs(options, args): + c.tempdir_registry.set_delete(not_deleted, False) + Holder.value = TempDirectory(kind=kind, globally_managed=True).path + + c = Command("fake", "fake") + c.run = Mock(side_effect=create_temp_dirs) + assert c.main(["fake"]) == SUCCESS + c.run.assert_called_once() + assert os.path.exists(Holder.value) == exists + + +@pytest.mark.parametrize("kind,exists", [ + (not_deleted, True), ("deleted", False) +]) +@pytest.mark.no_auto_tempdir_manager +def test_base_command_local_tempdir_cleanup(kind, exists): + assert temp_dir._tempdir_manager is None + assert temp_dir._tempdir_registry is None + + def create_temp_dirs(options, args): + c.tempdir_registry.set_delete(not_deleted, False) + + with TempDirectory(kind=kind) as d: + path = d.path + assert os.path.exists(path) + assert os.path.exists(path) == exists + + c = Command("fake", "fake") + c.run = Mock(side_effect=create_temp_dirs) + assert c.main(["fake"]) == SUCCESS + c.run.assert_called_once() From d31cf696e834825a7aae606144ccfb1a493ba405 Mon Sep 17 00:00:00 2001 From: Deepak Sharma <deepshar@redhat.com> Date: Wed, 29 Jan 2020 22:54:26 +0530 Subject: [PATCH 1198/3170] string_formatting --- ...a93c6a-3d20-4662-b510-0a11603837b1.trivial | 0 src/pip/_internal/cli/base_command.py | 4 +-- src/pip/_internal/cli/cmdoptions.py | 10 +++---- src/pip/_internal/cli/main.py | 2 +- src/pip/_internal/cli/main_parser.py | 4 +-- src/pip/_internal/cli/parser.py | 13 +++++---- src/pip/_internal/commands/completion.py | 2 +- src/pip/_internal/commands/freeze.py | 2 +- src/pip/_internal/commands/hash.py | 4 +-- src/pip/_internal/commands/help.py | 4 +-- src/pip/_internal/exceptions.py | 10 +++---- src/pip/_internal/index/collector.py | 2 +- src/pip/_internal/index/package_finder.py | 23 ++++++++------- src/pip/_internal/models/link.py | 6 ++-- src/pip/_internal/models/wheel.py | 2 +- src/pip/_internal/network/auth.py | 2 +- .../operations/build/wheel_legacy.py | 2 +- src/pip/_internal/operations/freeze.py | 4 +-- src/pip/_internal/operations/install/wheel.py | 6 ++-- src/pip/_internal/req/__init__.py | 9 +++--- src/pip/_internal/req/constructors.py | 29 ++++++++++--------- src/pip/_internal/req/req_file.py | 11 +++---- src/pip/_internal/req/req_install.py | 28 +++++++++--------- src/pip/_internal/req/req_set.py | 12 ++++---- src/pip/_internal/req/req_tracker.py | 5 ++-- src/pip/_internal/req/req_uninstall.py | 8 +++-- src/pip/_internal/utils/compat.py | 3 +- src/pip/_internal/utils/hashes.py | 4 ++- src/pip/_internal/utils/misc.py | 10 +++---- src/pip/_internal/utils/subprocess.py | 8 ++--- src/pip/_internal/utils/ui.py | 4 +-- src/pip/_internal/utils/wheel.py | 4 +-- src/pip/_internal/vcs/versioncontrol.py | 3 +- 33 files changed, 128 insertions(+), 112 deletions(-) create mode 100644 news/f2a93c6a-3d20-4662-b510-0a11603837b1.trivial diff --git a/news/f2a93c6a-3d20-4662-b510-0a11603837b1.trivial b/news/f2a93c6a-3d20-4662-b510-0a11603837b1.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index 628faa3eee0..5bb8fa2bc11 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -56,7 +56,7 @@ def __init__(self, name, summary, isolated=False): super(Command, self).__init__() parser_kw = { 'usage': self.usage, - 'prog': '%s %s' % (get_prog(), name), + 'prog': '{} {}'.format(get_prog(), name), 'formatter': UpdatingDefaultsHelpFormatter(), 'add_help_option': False, 'name': name, @@ -69,7 +69,7 @@ def __init__(self, name, summary, isolated=False): self.parser = ConfigOptionParser(**parser_kw) # Commands should add options to this option group - optgroup_name = '%s Options' % self.name.capitalize() + optgroup_name = '{} Options'.format(self.name.capitalize()) self.cmd_opts = optparse.OptionGroup(self.parser, optgroup_name) # Add the general options diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index 42e26951af2..f3450199095 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -851,12 +851,12 @@ def _handle_merge_hash(option, opt_str, value, parser): try: algo, digest = value.split(':', 1) except ValueError: - parser.error('Arguments to %s must be a hash name ' - 'followed by a value, like --hash=sha256:abcde...' % - opt_str) + parser.error('Arguments to {} must be a hash name ' + 'followed by a value, like --hash=sha256:' + 'abcde...'.format(opt_str)) if algo not in STRONG_HASHES: - parser.error('Allowed hash algorithms for %s are %s.' % - (opt_str, ', '.join(STRONG_HASHES))) + parser.error('Allowed hash algorithms for {} are {}.'.format( + opt_str, ', '.join(STRONG_HASHES))) parser.values.hashes.setdefault(algo, []).append(digest) diff --git a/src/pip/_internal/cli/main.py b/src/pip/_internal/cli/main.py index 5e97a5103f6..172f30dd5bf 100644 --- a/src/pip/_internal/cli/main.py +++ b/src/pip/_internal/cli/main.py @@ -59,7 +59,7 @@ def main(args=None): try: cmd_name, cmd_args = parse_command(args) except PipError as exc: - sys.stderr.write("ERROR: %s" % exc) + sys.stderr.write("ERROR: {}".format(exc)) sys.stderr.write(os.linesep) sys.exit(1) diff --git a/src/pip/_internal/cli/main_parser.py b/src/pip/_internal/cli/main_parser.py index a89821d4489..4871956c9de 100644 --- a/src/pip/_internal/cli/main_parser.py +++ b/src/pip/_internal/cli/main_parser.py @@ -86,9 +86,9 @@ def parse_command(args): if cmd_name not in commands_dict: guess = get_similar_commands(cmd_name) - msg = ['unknown command "%s"' % cmd_name] + msg = ['unknown command "{}"'.format(cmd_name)] if guess: - msg.append('maybe you meant "%s"' % guess) + msg.append('maybe you meant "{}"'.format(guess)) raise CommandError(' - '.join(msg)) diff --git a/src/pip/_internal/cli/parser.py b/src/pip/_internal/cli/parser.py index c99456bae88..e799215bef1 100644 --- a/src/pip/_internal/cli/parser.py +++ b/src/pip/_internal/cli/parser.py @@ -33,7 +33,7 @@ def __init__(self, *args, **kwargs): def format_option_strings(self, option): return self._format_option_strings(option, ' <%s>', ', ') - def _format_option_strings(self, option, mvarfmt=' <%s>', optsep=', '): + def _format_option_strings(self, option, mvarfmt=' <{}>', optsep=', '): """ Return a comma-separated list of option strings and metavars. @@ -52,7 +52,7 @@ def _format_option_strings(self, option, mvarfmt=' <%s>', optsep=', '): if option.takes_value(): metavar = option.metavar or option.dest.lower() - opts.append(mvarfmt % metavar.lower()) + opts.append(mvarfmt.format(metavar.lower())) return ''.join(opts) @@ -66,7 +66,8 @@ def format_usage(self, usage): Ensure there is only one newline between usage and the first heading if there is no description. """ - msg = '\nUsage: %s\n' % self.indent_lines(textwrap.dedent(usage), " ") + msg = '\nUsage: {}\n'.format( + self.indent_lines(textwrap.dedent(usage), " ")) return msg def format_description(self, description): @@ -82,7 +83,7 @@ def format_description(self, description): description = description.rstrip() # dedent, then reindent description = self.indent_lines(textwrap.dedent(description), " ") - description = '%s:\n%s\n' % (label, description) + description = '{}:\n{}\n'.format(label, description) return description else: return '' @@ -150,7 +151,7 @@ def check_default(self, option, key, val): try: return option.check_value(key, val) except optparse.OptionValueError as exc: - print("An error occurred during configuration: %s" % exc) + print("An error occurred during configuration: {}".format(exc)) sys.exit(3) def _get_ordered_configuration_items(self): @@ -249,7 +250,7 @@ def get_default_values(self): def error(self, msg): self.print_usage(sys.stderr) - self.exit(UNKNOWN_ERROR, "%s\n" % msg) + self.exit(UNKNOWN_ERROR, "{}\n".format(msg)) def invalid_config_error_message(action, key, val): diff --git a/src/pip/_internal/commands/completion.py b/src/pip/_internal/commands/completion.py index c532806e386..e0b743e542f 100644 --- a/src/pip/_internal/commands/completion.py +++ b/src/pip/_internal/commands/completion.py @@ -92,5 +92,5 @@ def run(self, options, args): print(BASE_COMPLETION % {'script': script, 'shell': options.shell}) else: sys.stderr.write( - 'ERROR: You must pass %s\n' % ' or '.join(shell_options) + 'ERROR: You must pass {}\n' .format(' or '.join(shell_options)) ) diff --git a/src/pip/_internal/commands/freeze.py b/src/pip/_internal/commands/freeze.py index c59eb3960a6..41fea20ca5e 100644 --- a/src/pip/_internal/commands/freeze.py +++ b/src/pip/_internal/commands/freeze.py @@ -65,7 +65,7 @@ def __init__(self, *args, **kw): dest='freeze_all', action='store_true', help='Do not skip these packages in the output:' - ' %s' % ', '.join(DEV_PKGS)) + ' {}'.format(', '.join(DEV_PKGS))) self.cmd_opts.add_option( '--exclude-editable', dest='exclude_editable', diff --git a/src/pip/_internal/commands/hash.py b/src/pip/_internal/commands/hash.py index 1dc7fb0eac9..f26686156ee 100644 --- a/src/pip/_internal/commands/hash.py +++ b/src/pip/_internal/commands/hash.py @@ -34,8 +34,8 @@ def __init__(self, *args, **kw): choices=STRONG_HASHES, action='store', default=FAVORITE_HASH, - help='The hash algorithm to use: one of %s' % - ', '.join(STRONG_HASHES)) + help='The hash algorithm to use: one of {}'.format( + ', '.join(STRONG_HASHES))) self.parser.insert_option_group(0, self.cmd_opts) def run(self, options, args): diff --git a/src/pip/_internal/commands/help.py b/src/pip/_internal/commands/help.py index 75af999b41e..c17d7a457c4 100644 --- a/src/pip/_internal/commands/help.py +++ b/src/pip/_internal/commands/help.py @@ -29,9 +29,9 @@ def run(self, options, args): if cmd_name not in commands_dict: guess = get_similar_commands(cmd_name) - msg = ['unknown command "%s"' % cmd_name] + msg = ['unknown command "{}"'.format(cmd_name)] if guess: - msg.append('maybe you meant "%s"' % guess) + msg.append('maybe you meant "{}"'.format(guess)) raise CommandError(' - '.join(msg)) diff --git a/src/pip/_internal/exceptions.py b/src/pip/_internal/exceptions.py index dddec789ef4..882529dece7 100644 --- a/src/pip/_internal/exceptions.py +++ b/src/pip/_internal/exceptions.py @@ -255,8 +255,8 @@ def __init__(self, allowed, gots): self.gots = gots def body(self): - return ' %s:\n%s' % (self._requirement_name(), - self._hash_comparison()) + return ' {}:\n{}'.format(self._requirement_name(), + self._hash_comparison()) def _hash_comparison(self): """ @@ -277,10 +277,10 @@ def hash_then_or(hash_name): lines = [] for hash_name, expecteds in iteritems(self.allowed): prefix = hash_then_or(hash_name) - lines.extend((' Expected %s %s' % (next(prefix), e)) + lines.extend((' Expected {} {}'.format(next(prefix), e)) for e in expecteds) - lines.append(' Got %s\n' % - self.gots[hash_name].hexdigest()) + lines.append(' Got {}\n'.format( + self.gots[hash_name].hexdigest())) return '\n'.join(lines) diff --git a/src/pip/_internal/index/collector.py b/src/pip/_internal/index/collector.py index 8330793171a..69bf738e524 100644 --- a/src/pip/_internal/index/collector.py +++ b/src/pip/_internal/index/collector.py @@ -354,7 +354,7 @@ def _get_html_page(link, session=None): reason += str(exc) _handle_get_page_fail(link, reason, meth=logger.info) except requests.ConnectionError as exc: - _handle_get_page_fail(link, "connection error: %s" % exc) + _handle_get_page_fail(link, "connection error: {}".format(exc)) except requests.Timeout: _handle_get_page_fail(link, "timed out") else: diff --git a/src/pip/_internal/index/package_finder.py b/src/pip/_internal/index/package_finder.py index a74d78db5a6..e88ad9f5c69 100644 --- a/src/pip/_internal/index/package_finder.py +++ b/src/pip/_internal/index/package_finder.py @@ -177,9 +177,10 @@ def evaluate_link(self, link): if not ext: return (False, 'not a file') if ext not in SUPPORTED_EXTENSIONS: - return (False, 'unsupported archive format: %s' % ext) + return (False, 'unsupported archive format: {}'.format(ext)) if "binary" not in self._formats and ext == WHEEL_EXTENSION: - reason = 'No binaries permitted for %s' % self.project_name + reason = 'No binaries permitted for {}'.format( + self.project_name) return (False, reason) if "macosx10" in link.path and ext == '.zip': return (False, 'macosx10 one') @@ -189,7 +190,8 @@ def evaluate_link(self, link): except InvalidWheelFilename: return (False, 'invalid wheel filename') if canonicalize_name(wheel.name) != self._canonical_name: - reason = 'wrong project name (not %s)' % self.project_name + reason = 'wrong project name (not {})'.format( + self.project_name) return (False, reason) supported_tags = self._target_python.get_tags() @@ -208,16 +210,16 @@ def evaluate_link(self, link): # This should be up by the self.ok_binary check, but see issue 2700. if "source" not in self._formats and ext != WHEEL_EXTENSION: - return (False, 'No sources permitted for %s' % self.project_name) + reason = 'No sources permitted for {}'.format(self.project_name) + return (False, reason) if not version: version = _extract_version_from_fragment( egg_info, self._canonical_name, ) if not version: - return ( - False, 'Missing project version for %s' % self.project_name, - ) + reason = 'Missing project version for {}'.format(self.project_name) + return (False, reason) match = self._py_version_re.search(version) if match: @@ -524,8 +526,8 @@ def _sort_key(self, candidate): wheel = Wheel(link.filename) if not wheel.supported(valid_tags): raise UnsupportedWheel( - "%s is not a supported wheel for this platform. It " - "can't be sorted." % wheel.filename + "{} is not a supported wheel for this platform. It " + "can't be sorted.".format(wheel.filename) ) if self._prefer_binary: binary_preference = 1 @@ -924,7 +926,8 @@ def _format_versions(cand_iter): ) raise DistributionNotFound( - 'No matching distribution found for %s' % req + 'No matching distribution found for {}'.format( + req) ) best_installed = False diff --git a/src/pip/_internal/models/link.py b/src/pip/_internal/models/link.py index 34fbcbfe7e4..1b3aa591ab0 100644 --- a/src/pip/_internal/models/link.py +++ b/src/pip/_internal/models/link.py @@ -66,12 +66,12 @@ def __init__( def __str__(self): # type: () -> str if self.requires_python: - rp = ' (requires-python:%s)' % self.requires_python + rp = ' (requires-python:{})'.format(self.requires_python) else: rp = '' if self.comes_from: - return '%s (from %s)%s' % (redact_auth_from_url(self._url), - self.comes_from, rp) + return '{} (from {}){}'.format( + redact_auth_from_url(self._url), self.comes_from, rp) else: return redact_auth_from_url(str(self._url)) diff --git a/src/pip/_internal/models/wheel.py b/src/pip/_internal/models/wheel.py index f1e3f44c598..4d4068f3b73 100644 --- a/src/pip/_internal/models/wheel.py +++ b/src/pip/_internal/models/wheel.py @@ -30,7 +30,7 @@ def __init__(self, filename): wheel_info = self.wheel_file_re.match(filename) if not wheel_info: raise InvalidWheelFilename( - "%s is not a valid wheel filename." % filename + "{} is not a valid wheel filename.".format(filename) ) self.filename = filename self.name = wheel_info.group('name').replace('_', '-') diff --git a/src/pip/_internal/network/auth.py b/src/pip/_internal/network/auth.py index 1e1da54ca59..94da3d46aaa 100644 --- a/src/pip/_internal/network/auth.py +++ b/src/pip/_internal/network/auth.py @@ -215,7 +215,7 @@ def __call__(self, req): # Factored out to allow for easy patching in tests def _prompt_for_password(self, netloc): - username = ask_input("User for %s: " % netloc) + username = ask_input("User for {}: ".format(netloc)) if not username: return None, None auth = get_keyring_auth(netloc, username) diff --git a/src/pip/_internal/operations/build/wheel_legacy.py b/src/pip/_internal/operations/build/wheel_legacy.py index 3ebd9fe444b..96dd09a4542 100644 --- a/src/pip/_internal/operations/build/wheel_legacy.py +++ b/src/pip/_internal/operations/build/wheel_legacy.py @@ -89,7 +89,7 @@ def build_wheel_legacy( destination_dir=tempd, ) - spin_message = 'Building wheel for %s (setup.py)' % (name,) + spin_message = 'Building wheel for {} (setup.py)'.format(name) with open_spinner(spin_message) as spinner: logger.debug('Destination directory: %s', tempd) diff --git a/src/pip/_internal/operations/freeze.py b/src/pip/_internal/operations/freeze.py index 36a5c339a2a..5575d70d06f 100644 --- a/src/pip/_internal/operations/freeze.py +++ b/src/pip/_internal/operations/freeze.py @@ -60,7 +60,7 @@ def freeze( skip_match = re.compile(skip_regex).search for link in find_links: - yield '-f %s' % link + yield '-f {}'.format(link) installations = {} # type: Dict[str, FrozenRequirement] for dist in get_installed_distributions(local_only=local_only, skip=(), @@ -261,5 +261,5 @@ def from_dist(cls, dist): def __str__(self): req = self.req if self.editable: - req = '-e %s' % req + req = '-e {}'.format(req) return '\n'.join(list(self.comments) + [str(req)]) + '\n' diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index aac975c3ac8..b1a76a24ce2 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -504,11 +504,11 @@ def is_entrypoint_wrapper(name): if os.environ.get("ENSUREPIP_OPTIONS", "") != "altinstall": scripts_to_generate.append( - 'pip%s = %s' % (sys.version_info[0], pip_script) + 'pip{} = {}'.format(sys.version_info[0], pip_script) ) scripts_to_generate.append( - 'pip%s = %s' % (get_major_minor_version(), pip_script) + 'pip{} = {}'.format(get_major_minor_version(), pip_script) ) # Delete any other versioned pip entry points pip_ep = [k for k in console if re.match(r'pip(\d(\.\d)?)?$', k)] @@ -522,7 +522,7 @@ def is_entrypoint_wrapper(name): ) scripts_to_generate.append( - 'easy_install-%s = %s' % ( + 'easy_install-{} = {}'.format( get_major_minor_version(), easy_install_script ) ) diff --git a/src/pip/_internal/req/__init__.py b/src/pip/_internal/req/__init__.py index 05e08972852..d2d027adeec 100644 --- a/src/pip/_internal/req/__init__.py +++ b/src/pip/_internal/req/__init__.py @@ -48,18 +48,17 @@ def install_given_reqs( """ if to_install: - msg = 'Installing collected packages: {}'.format( - ', '.join([req.name for req in to_install]) + logger.info( + 'Installing collected packages: %s', + ', '.join([req.name for req in to_install]), ) - logger.info(msg) installed = [] with indent_log(): for requirement in to_install: if requirement.should_reinstall: - logger.info('Attempting uninstall: {}'.format( - requirement.name)) + logger.info('Attempting uninstall: %s', requirement.name) with indent_log(): uninstalled_pathset = requirement.uninstall( auto_confirm=True diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index 1f3cd8a104c..16f2c9c7d99 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -137,16 +137,17 @@ def parse_editable(editable_req): vc_type = url.split('+', 1)[0].lower() if not vcs.get_backend(vc_type): - error_message = 'For --editable=%s only ' % editable_req + \ - ', '.join([backend.name + '+URL' for backend in vcs.backends]) + \ - ' is currently supported' + error_message = 'For --editable={} only '.format( + editable_req + ', '.join( + [backend.name + '+URL' for backend in vcs.backends]) + + ' is currently supported') raise InstallationError(error_message) package_name = Link(url).egg_fragment if not package_name: raise InstallationError( - "Could not detect requirement name for '%s', please specify one " - "with #egg=your_package_name" % editable_req + "Could not detect requirement name for '{}', please specify one " + "with #egg=your_package_name".format(editable_req) ) return package_name, url, None @@ -166,16 +167,18 @@ def deduce_helpful_msg(req): 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" + \ + msg += ( + "The argument you provided " + "({}) appears to be a" + " requirements file. If that is the" + " case, use the '-r' flag to install" " the packages specified within it." + ).format(req) except RequirementParseError: logger.debug("Cannot parse '%s' as requirements \ file" % (req), exc_info=True) else: - msg += " File '%s' does not exist." % (req) + msg += " File '{}' does not exist.".format(req) return msg @@ -201,7 +204,7 @@ def parse_req_from_editable(editable_req): try: req = Requirement(name) except InvalidRequirement: - raise InstallationError("Invalid requirement: '%s'" % name) + raise InstallationError("Invalid requirement: '{}'".format(name)) else: req = None @@ -415,7 +418,7 @@ def install_req_from_req_string( try: req = Requirement(req_string) except InvalidRequirement: - raise InstallationError("Invalid requirement: '%s'" % req_string) + raise InstallationError("Invalid requirement: '{}'".format(req_string)) domains_not_allowed = [ PyPI.file_storage_domain, @@ -427,7 +430,7 @@ def install_req_from_req_string( raise InstallationError( "Packages installed from PyPI cannot depend on packages " "which are not also hosted on PyPI.\n" - "%s depends on %s " % (comes_from.name, req) + "{} depends on {} ".format(comes_from.name, req) ) return InstallRequirement( diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index 8c7810481ee..baf8d03ea2a 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -183,7 +183,7 @@ def handle_line( """ # preserve for the nested code path - line_comes_from = '%s %s (line %s)' % ( + line_comes_from = '{} {} (line {})'.format( '-c' if line.constraint else '-r', line.filename, line.lineno, ) @@ -329,7 +329,7 @@ def _parse_file(self, filename, constraint): args_str, opts = self._line_parser(line) except OptionParsingError as e: # add offending line - msg = 'Invalid requirement: %s\n%s' % (line, e.msg) + msg = 'Invalid requirement: {}\n{}'.format(line, e.msg) raise RequirementsFileParseError(msg) yield ParsedLine( @@ -520,8 +520,9 @@ def get_file_content(url, session, comes_from=None): elif scheme == 'file': if comes_from and comes_from.startswith('http'): raise InstallationError( - 'Requirements file %s references URL %s, which is local' - % (comes_from, url)) + 'Requirements file {} references URL {}, ' + 'which is local'.format(comes_from, url) + ) path = url.split(':', 1)[1] path = path.replace('\\', '/') @@ -538,7 +539,7 @@ def get_file_content(url, session, comes_from=None): content = auto_decode(f.read()) except IOError as exc: raise InstallationError( - 'Could not open requirements file: %s' % str(exc) + 'Could not open requirements file: {}'.format(exc) ) return url, content diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 6bbad88578a..e560cf449bf 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -195,25 +195,25 @@ def __str__(self): if self.req: s = str(self.req) if self.link: - s += ' from %s' % redact_auth_from_url(self.link.url) + s += ' from {}'.format(redact_auth_from_url(self.link.url)) elif self.link: s = redact_auth_from_url(self.link.url) else: s = '<InstallRequirement>' if self.satisfied_by is not None: - s += ' in %s' % display_path(self.satisfied_by.location) + s += ' in {}'.format(display_path(self.satisfied_by.location)) if self.comes_from: if isinstance(self.comes_from, six.string_types): comes_from = self.comes_from # type: Optional[str] else: comes_from = self.comes_from.from_path() if comes_from: - s += ' (from %s)' % comes_from + s += ' (from {})'.format(comes_from) return s def __repr__(self): # type: () -> str - return '<%s object: %s editable=%r>' % ( + return '<{} object: {} editable={!r}>'.format( self.__class__.__name__, str(self), self.editable) def format_debug(self): @@ -452,8 +452,8 @@ def check_if_exists(self, use_user_site): dist_in_site_packages(existing_dist)): raise InstallationError( "Will not install to the user site because it will " - "lack sys.path precedence to %s in %s" % - (existing_dist.project_name, existing_dist.location) + "lack sys.path precedence to {} in {}".format( + existing_dist.project_name, existing_dist.location) ) else: self.should_reinstall = True @@ -483,7 +483,7 @@ def unpacked_source_directory(self): @property def setup_py_path(self): # type: () -> str - assert self.source_dir, "No source dir for %s" % self + assert self.source_dir, "No source dir for {}".format(self) setup_py = os.path.join(self.unpacked_source_directory, 'setup.py') # Python2 __file__ should not be unicode @@ -495,7 +495,7 @@ def setup_py_path(self): @property def pyproject_toml_path(self): # type: () -> str - assert self.source_dir, "No source dir for %s" % self + assert self.source_dir, "No source dir for {}".format(self) return make_pyproject_path(self.unpacked_source_directory) def load_pyproject_toml(self): @@ -654,8 +654,8 @@ def update_editable(self, obtain=True): vcs_backend.export(self.source_dir, url=hidden_url) else: assert 0, ( - 'Unexpected version control type (in %s): %s' - % (self.link, vc_type)) + 'Unexpected version control type (in {}): {}'.format( + self.link, vc_type)) # Top-level Actions def uninstall(self, auto_confirm=False, verbose=False): @@ -710,13 +710,15 @@ def archive(self, build_dir): assert self.source_dir create_archive = True - archive_name = '%s-%s.zip' % (self.name, self.metadata["version"]) + archive_name = '{}-{}.zip'.format(self.name, self.metadata["version"]) archive_path = os.path.join(build_dir, archive_name) if os.path.exists(archive_path): response = ask_path_exists( - 'The file %s exists. (i)gnore, (w)ipe, (b)ackup, (a)bort ' % - display_path(archive_path), ('i', 'w', 'b', 'a')) + 'The file {} exists. (i)gnore, (w)ipe, ' + '(b)ackup, (a)bort '.format( + display_path(archive_path)), + ('i', 'w', 'b', 'a')) if response == 'i': create_archive = False elif response == 'w': diff --git a/src/pip/_internal/req/req_set.py b/src/pip/_internal/req/req_set.py index 087ac5925f5..1312622b87b 100644 --- a/src/pip/_internal/req/req_set.py +++ b/src/pip/_internal/req/req_set.py @@ -108,8 +108,8 @@ def add_requirement( tags = pep425tags.get_supported() if (self.check_supported_wheels and not wheel.supported(tags)): raise InstallationError( - "%s is not a supported wheel on this platform." % - wheel.filename + "{} is not a supported wheel on this platform.".format( + wheel.filename) ) # This next bit is really a sanity check. @@ -138,8 +138,8 @@ def add_requirement( ) if has_conflicting_requirement: raise InstallationError( - "Double requirement given: %s (already in %s, name=%r)" - % (install_req, existing_req, install_req.name) + "Double requirement given: {} (already in {}, name={!r})" + .format(install_req, existing_req, install_req.name) ) # When no existing requirement exists, add the requirement as a @@ -164,9 +164,9 @@ def add_requirement( if does_not_satisfy_constraint: self.reqs_to_cleanup.append(install_req) raise InstallationError( - "Could not satisfy constraints for '%s': " + "Could not satisfy constraints for '{}': " "installation from path or url cannot be " - "constrained to a version" % install_req.name, + "constrained to a version".format(install_req.name) ) # If we're now installing a constraint, mark the existing # object for real installation. diff --git a/src/pip/_internal/req/req_tracker.py b/src/pip/_internal/req/req_tracker.py index d72a41d203a..14adeab29b5 100644 --- a/src/pip/_internal/req/req_tracker.py +++ b/src/pip/_internal/req/req_tracker.py @@ -60,7 +60,7 @@ def get_requirement_tracker(): TempDirectory(kind='req-tracker') ).path ctx.enter_context(update_env_context_manager(PIP_REQ_TRACKER=root)) - logger.debug("Initialized build tracking at {}".format(root)) + logger.debug("Initialized build tracking at %s", root) with RequirementTracker(root) as tracker: yield tracker @@ -111,7 +111,8 @@ def add(self, req): if e.errno != errno.ENOENT: raise else: - message = '%s is already being built: %s' % (req.link, contents) + message = '{} is already being built: {}'.format( + req.link, contents) raise LookupError(message) # If we're here, req should really not be building already. diff --git a/src/pip/_internal/req/req_uninstall.py b/src/pip/_internal/req/req_uninstall.py index 5971b130ec0..ec9da2c3484 100644 --- a/src/pip/_internal/req/req_uninstall.py +++ b/src/pip/_internal/req/req_uninstall.py @@ -540,8 +540,9 @@ def from_dist(cls, dist): 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) + 'Egg-link {} does not match installed location of {} ' + '(at {})'.format( + 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), @@ -586,7 +587,8 @@ def __init__(self, pth_file): # type: (str) -> None if not os.path.isfile(pth_file): raise UninstallationError( - "Cannot remove entries from nonexistent file %s" % pth_file + "Cannot remove entries from nonexistent file {}".format( + pth_file) ) self.file = pth_file self.entries = set() # type: Set[str] diff --git a/src/pip/_internal/utils/compat.py b/src/pip/_internal/utils/compat.py index 6efa52ad2b8..08c292d189d 100644 --- a/src/pip/_internal/utils/compat.py +++ b/src/pip/_internal/utils/compat.py @@ -184,7 +184,8 @@ def get_path_uid(path): else: # raise OSError for parity with os.O_NOFOLLOW above raise OSError( - "%s is a symlink; Will not return uid for symlinks" % path + "{} is a symlink; Will not return uid for symlinks".format( + path) ) return file_uid diff --git a/src/pip/_internal/utils/hashes.py b/src/pip/_internal/utils/hashes.py index 4c41551a255..396cf82e753 100644 --- a/src/pip/_internal/utils/hashes.py +++ b/src/pip/_internal/utils/hashes.py @@ -73,7 +73,9 @@ def check_against_chunks(self, chunks): try: gots[hash_name] = hashlib.new(hash_name) except (ValueError, TypeError): - raise InstallationError('Unknown hash name: %s' % hash_name) + raise InstallationError( + 'Unknown hash name: {}'.format(hash_name) + ) for chunk in chunks: for hash in itervalues(gots): diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 4a581601991..667ed80a38e 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -120,7 +120,7 @@ def get_prog(): try: prog = os.path.basename(sys.argv[0]) if prog in ('__main__.py', '-c'): - return "%s -m pip" % sys.executable + return "{} -m pip".format(sys.executable) else: return prog except (AttributeError, TypeError, IndexError): @@ -228,8 +228,8 @@ def _check_no_input(message): """Raise an error if no input is allowed.""" if os.environ.get('PIP_NO_INPUT'): raise Exception( - 'No input was expected ($PIP_NO_INPUT set); question: %s' % - message + 'No input was expected ($PIP_NO_INPUT set); question: {}'.format( + message) ) @@ -242,8 +242,8 @@ def ask(message, options): response = response.strip().lower() if response not in options: print( - 'Your response (%r) was not one of the expected responses: ' - '%s' % (response, ', '.join(options)) + 'Your response ({!r}) was not one of the expected responses: ' + '{}'.format(response, ', '.join(options)) ) else: return response diff --git a/src/pip/_internal/utils/subprocess.py b/src/pip/_internal/utils/subprocess.py index ea0176d341e..61c40a49eae 100644 --- a/src/pip/_internal/utils/subprocess.py +++ b/src/pip/_internal/utils/subprocess.py @@ -242,14 +242,14 @@ def call_subprocess( raise InstallationError(exc_msg) elif on_returncode == 'warn': subprocess_logger.warning( - 'Command "%s" had error code %s in %s', - command_desc, proc.returncode, cwd, + 'Command "{}" had error code {} in {}'.format( + command_desc, proc.returncode, cwd) ) elif on_returncode == 'ignore': pass else: - raise ValueError('Invalid value: on_returncode=%s' % - repr(on_returncode)) + raise ValueError('Invalid value: on_returncode={!r}'.format( + on_returncode)) return ''.join(all_output) diff --git a/src/pip/_internal/utils/ui.py b/src/pip/_internal/utils/ui.py index 87782aa641d..f84feb3171e 100644 --- a/src/pip/_internal/utils/ui.py +++ b/src/pip/_internal/utils/ui.py @@ -153,7 +153,7 @@ def download_speed(self): @property def pretty_eta(self): if self.eta: - return "eta %s" % self.eta_td + return "eta {}".format(self.eta_td) return "" def iter(self, it): @@ -399,7 +399,7 @@ def finish(self, final_status): # type: (str) -> None if self._finished: return - self._update("finished with status '%s'" % (final_status,)) + self._update("finished with status '{}'".format(final_status)) self._finished = True diff --git a/src/pip/_internal/utils/wheel.py b/src/pip/_internal/utils/wheel.py index 837e0afd7e5..e4166a68e7b 100644 --- a/src/pip/_internal/utils/wheel.py +++ b/src/pip/_internal/utils/wheel.py @@ -215,8 +215,8 @@ def check_compatibility(version, name): """ if version[0] > VERSION_COMPATIBLE[0]: raise UnsupportedWheel( - "%s's Wheel-Version (%s) is not compatible with this version " - "of pip" % (name, '.'.join(map(str, version))) + "{}'s Wheel-Version ({}) is not compatible with this version " + "of pip".format(name, '.'.join(map(str, version))) ) elif version > VERSION_COMPATIBLE: logger.warning( diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index 7cfd568829f..1ec986ba52f 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -574,7 +574,8 @@ def obtain(self, dest, url): self.name, url, ) - response = ask_path_exists('What to do? %s' % prompt[0], prompt[1]) + response = ask_path_exists('What to do? {}'.format( + prompt[0]), prompt[1]) if response == 'a': sys.exit(-1) From 61e1721b80a7df0dc6aa9d0c823cc3881aae1dd2 Mon Sep 17 00:00:00 2001 From: Deepak Sharma <deepshar@redhat.com> Date: Thu, 30 Jan 2020 21:57:31 +0530 Subject: [PATCH 1199/3170] msg formatting updated --- src/pip/_internal/req/constructors.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index 16f2c9c7d99..a1bcb3382c3 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -137,10 +137,10 @@ def parse_editable(editable_req): vc_type = url.split('+', 1)[0].lower() if not vcs.get_backend(vc_type): - error_message = 'For --editable={} only '.format( - editable_req + ', '.join( - [backend.name + '+URL' for backend in vcs.backends]) + - ' is currently supported') + backends = ", ".join([bends.name + '+URL' for bends in vcs.backends]) + error_message = "For --editable={}, " \ + "only {} are currently supported".format( + editable_req, backends) raise InstallationError(error_message) package_name = Link(url).egg_fragment From 96ce5236f1a9b204d4ef38cf859c1779b4b338f0 Mon Sep 17 00:00:00 2001 From: Andre Aguiar <aguiarcandre@gmail.com> Date: Mon, 27 Jan 2020 21:38:22 -0300 Subject: [PATCH 1200/3170] Capitalize "URL" in user guide --- docs/html/user_guide.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index ee827f0890b..ad938917999 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -52,7 +52,7 @@ For more information and examples, see the :ref:`pip install` reference. Basic Authentication Credentials ******************************** -pip supports basic authentication credentials. Basically, in the url there is +pip supports basic authentication credentials. Basically, in the URL there is a username and password separated by ``:``. ``https://[username[:password]@]pypi.company.com/simple`` From fb2598a645e3c732e1c4ea046e156713baf4b5f5 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko <wk@sydorenko.org.ua> Date: Sun, 26 Jan 2020 03:00:40 +0100 Subject: [PATCH 1201/3170] Suggest a way of cleaning dist if non-empty --- noxfile.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index 2151ce37e08..19a6eaa00a5 100644 --- a/noxfile.py +++ b/noxfile.py @@ -195,7 +195,10 @@ def build_release(session): session.log("# Ensure no files in dist/") if release.have_files_in_folder("dist"): - session.error("There are files in dist/. Remove them and try again") + session.error( + "There are files in dist/. Remove them and try again. " + "You can use `git clean -fxdi -- dist` command to do this" + ) session.log("# Install dependencies") session.install("setuptools", "wheel", "twine") From 39186d08572784f0b22d287a26afd344dc9b58af Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Fri, 31 Jan 2020 20:25:34 +0530 Subject: [PATCH 1202/3170] Stop suggesting use of the /request-review bot --- .github/CONTRIBUTING.md | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index fbccffdfd81..2a56334d81e 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -10,16 +10,3 @@ As a reminder, all contributors are expected to follow our [Code of Conduct][coc ## Development Documentation Our [development documentation](https://pip.pypa.io/en/latest/development/) contains details on how to get started with contributing to pip, and details of our development processes. - -## Bot Commands - -We have a bot monitoring the [pypa/pip](https://github.com/pypa/pip) repository -to help manage the state of issues and pull requests to keep everything running -smoothly. Each command given to the bot should be on its own line and is -generally case sensitive. Multiple commands may be listed in a single comment -(but they must each be on their own line) and the comments may also include -other, non command content. - -Command | Who can run it | Description ---- | --- | --- -`/request-review` | anyone | Dismisses all of the current reviews on a pull request, making it appear back in the review queue. From 6d8a58f7e136df92704d2334288e4589d91a082b Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 25 Jan 2020 11:35:54 -0500 Subject: [PATCH 1203/3170] Add wheel builder test helper As we introduce stricter metadata parsing, we will need to ensure that the wheel files used in our tests are compliant (except in the specific way that we're testing against). Currently we have a large number of test cases relying on undocumented or under-documented wheel files that have various inconsistencies (incorrect name, missing fields) that are unrelated to the features actually under test. With a new wheel builder helper function, we will be able to replace all of our instances of pre-built wheel test fixtures with dynamically-generated files in code that are correct by default. --- tests/lib/test_wheel.py | 199 +++++++++++++++++ tests/lib/wheel.py | 413 +++++++++++++++++++++++++++++++++++ tools/requirements/tests.txt | 2 + 3 files changed, 614 insertions(+) create mode 100644 tests/lib/test_wheel.py create mode 100644 tests/lib/wheel.py diff --git a/tests/lib/test_wheel.py b/tests/lib/test_wheel.py new file mode 100644 index 00000000000..b5c22ae31fd --- /dev/null +++ b/tests/lib/test_wheel.py @@ -0,0 +1,199 @@ +"""Tests for wheel helper. +""" +from email import message_from_string +from functools import partial +from zipfile import ZipFile + +from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from tests.lib.wheel import ( + _default, + make_metadata_file, + make_wheel, + make_wheel_metadata_file, + message_from_dict, +) + +if MYPY_CHECK_RUNNING: + from email import Message + + +def test_message_from_dict_one_value(): + message = message_from_dict({"a": "1"}) + assert set(message.get_all("a")) == {"1"} + + +def test_message_from_dict_multiple_values(): + message = message_from_dict({"a": ["1", "2"]}) + assert set(message.get_all("a")) == {"1", "2"} + + +def message_from_bytes(contents): + # type: (bytes) -> Message + return message_from_string(contents.decode("utf-8")) + + +default_make_metadata = partial( + make_metadata_file, + name="simple", + value=_default, + version="0.1.0", + updates=_default, + body=_default, +) + + +def default_metadata_checks(f): + assert f.name == "simple-0.1.0.dist-info/METADATA" + message = message_from_bytes(f.contents) + assert message.get_all("Metadata-Version") == ["2.1"] + assert message.get_all("Name") == ["simple"] + assert message.get_all("Version") == ["0.1.0"] + return message + + +def test_make_metadata_file_defaults(): + f = default_make_metadata() + default_metadata_checks(f) + + +def test_make_metadata_file_custom_value(): + f = default_make_metadata(updates={"a": "1"}) + message = default_metadata_checks(f) + assert message.get_all("a") == ["1"] + + +def test_make_metadata_file_custom_value_list(): + f = default_make_metadata(updates={"a": ["1", "2"]}) + message = default_metadata_checks(f) + assert set(message.get_all("a")) == {"1", "2"} + + +def test_make_metadata_file_custom_value_overrides(): + f = default_make_metadata(updates={"Metadata-Version": "2.2"}) + message = message_from_bytes(f.contents) + assert message.get_all("Metadata-Version") == ["2.2"] + + +def test_make_metadata_file_custom_contents(): + value = b"hello" + f = default_make_metadata(value=value) + assert f.contents == value + + +tags = [("py2", "none", "any"), ("py3", "none", "any")] +default_make_wheel_metadata = partial( + make_wheel_metadata_file, + name="simple", + version="0.1.0", + value=_default, + tags=tags, + updates=_default, +) + + +def default_wheel_metadata_checks(f): + assert f.name == "simple-0.1.0.dist-info/WHEEL" + message = message_from_bytes(f.contents) + assert message.get_all("Wheel-Version") == ["1.0"] + assert message.get_all("Generator") == ["pip-test-suite"] + assert message.get_all("Root-Is-Purelib") == ["true"] + assert set(message.get_all("Tag")) == {"py2-none-any", "py3-none-any"} + return message + + +def test_make_wheel_metadata_file_defaults(): + f = default_make_wheel_metadata() + default_wheel_metadata_checks(f) + + +def test_make_wheel_metadata_file_custom_value(): + f = default_make_wheel_metadata(updates={"a": "1"}) + message = default_wheel_metadata_checks(f) + assert message.get_all("a") == ["1"] + + +def test_make_wheel_metadata_file_custom_value_list(): + f = default_make_wheel_metadata(updates={"a": ["1", "2"]}) + message = default_wheel_metadata_checks(f) + assert set(message.get_all("a")) == {"1", "2"} + + +def test_make_wheel_metadata_file_custom_value_override(): + f = default_make_wheel_metadata(updates={"Wheel-Version": "1.1"}) + message = message_from_bytes(f.contents) + assert message.get_all("Wheel-Version") == ["1.1"] + + +def test_make_wheel_metadata_file_custom_contents(): + value = b"hello" + f = default_make_wheel_metadata(value=value) + + assert f.name == "simple-0.1.0.dist-info/WHEEL" + assert f.contents == value + + +def test_make_wheel_metadata_file_no_contents(): + f = default_make_wheel_metadata(value=None) + assert f is None + + +def test_make_wheel_basics(tmpdir): + make_wheel(name="simple", version="0.1.0").save_to_dir(tmpdir) + + expected_wheel_path = tmpdir / "simple-0.1.0-py2.py3-none-any.whl" + assert expected_wheel_path.exists() + + with ZipFile(expected_wheel_path) as z: + names = z.namelist() + assert set(names) == { + "simple-0.1.0.dist-info/METADATA", + "simple-0.1.0.dist-info/RECORD", + "simple-0.1.0.dist-info/WHEEL", + } + + +def test_make_wheel_extra_files(): + with make_wheel( + name="simple", + version="0.1.0", + extra_files={"simple/__init__.py": "a"}, + extra_metadata_files={"LICENSE": "b"}, + extra_data_files={"info.txt": "c"}, + ).as_zipfile() as z: + names = z.namelist() + assert set(names) == { + "simple/__init__.py", + "simple-0.1.0.data/info.txt", + "simple-0.1.0.dist-info/LICENSE", + "simple-0.1.0.dist-info/METADATA", + "simple-0.1.0.dist-info/RECORD", + "simple-0.1.0.dist-info/WHEEL", + } + + assert z.read("simple/__init__.py") == b"a" + assert z.read("simple-0.1.0.dist-info/LICENSE") == b"b" + assert z.read("simple-0.1.0.data/info.txt") == b"c" + + +def test_make_wheel_no_files(): + with make_wheel( + name="simple", + version="0.1.0", + wheel_metadata=None, + metadata=None, + record=None, + ).as_zipfile() as z: + assert not z.namelist() + + +def test_make_wheel_custom_files(): + with make_wheel( + name="simple", + version="0.1.0", + wheel_metadata=b"a", + metadata=b"b", + record=b"c", + ).as_zipfile() as z: + assert z.read("simple-0.1.0.dist-info/WHEEL") == b"a" + assert z.read("simple-0.1.0.dist-info/METADATA") == b"b" + assert z.read("simple-0.1.0.dist-info/RECORD") == b"c" diff --git a/tests/lib/wheel.py b/tests/lib/wheel.py new file mode 100644 index 00000000000..8ea458658e3 --- /dev/null +++ b/tests/lib/wheel.py @@ -0,0 +1,413 @@ +"""Helper for building wheels as would be in test cases. +""" +import itertools +from base64 import urlsafe_b64encode +from collections import namedtuple +from copy import deepcopy +from email.message import Message +from enum import Enum +from functools import partial +from hashlib import sha256 +from io import BytesIO, StringIO +from zipfile import ZipFile + +import csv23 +from pip._vendor.requests.structures import CaseInsensitiveDict +from pip._vendor.six import ensure_binary, ensure_text, iteritems + +from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from tests.lib.path import Path + +if MYPY_CHECK_RUNNING: + from typing import ( + AnyStr, Callable, Dict, List, Iterable, Optional, Tuple, Sequence, + TypeVar, Union, + ) + + # path, digest, size + RecordLike = Tuple[str, str, str] + RecordCallback = Callable[ + [List["Record"]], Union[str, bytes, List[RecordLike]] + ] + # As would be used in metadata + HeaderValue = Union[str, List[str]] + + +File = namedtuple("File", ["name", "contents"]) +Record = namedtuple("Record", ["path", "digest", "size"]) + + +class Default(Enum): + token = 0 + + +_default = Default.token + + +if MYPY_CHECK_RUNNING: + T = TypeVar("T") + + class Defaulted(Union[Default, T]): + """A type which may be defaulted. + """ + pass + + +def message_from_dict(headers): + # type: (Dict[str, HeaderValue]) -> Message + """Plain key-value pairs are set in the returned message. + + List values are converted into repeated headers in the result. + """ + message = Message() + for name, value in iteritems(headers): + if isinstance(value, list): + for v in value: + message[name] = v + else: + message[name] = value + return message + + +def dist_info_path(name, version, path): + # type: (str, str, str) -> str + return "{}-{}.dist-info/{}".format(name, version, path) + + +def make_metadata_file( + name, # type: str + version, # type: str + value, # type: Defaulted[Optional[AnyStr]] + updates, # type: Defaulted[Dict[str, HeaderValue]] + body, # type: Defaulted[AnyStr] +): + # type: () -> File + if value is None: + return None + + path = dist_info_path(name, version, "METADATA") + + if value is not _default: + return File(path, ensure_binary(value)) + + metadata = CaseInsensitiveDict({ + "Metadata-Version": "2.1", + "Name": name, + "Version": version, + }) + if updates is not _default: + metadata.update(updates) + + message = message_from_dict(metadata) + if body is not _default: + message.set_payload(body) + + return File(path, ensure_binary(message_from_dict(metadata).as_string())) + + +def make_wheel_metadata_file( + name, # type: str + version, # type: str + value, # type: Defaulted[Optional[AnyStr]] + tags, # type: Sequence[Tuple[str, str, str]] + updates, # type: Defaulted[Dict[str, HeaderValue]] +): + # type: (...) -> Optional[File] + if value is None: + return None + + path = dist_info_path(name, version, "WHEEL") + + if value is not _default: + return File(path, ensure_binary(value)) + + metadata = CaseInsensitiveDict({ + "Wheel-Version": "1.0", + "Generator": "pip-test-suite", + "Root-Is-Purelib": "true", + "Tag": ["-".join(parts) for parts in tags], + }) + + if updates is not _default: + metadata.update(updates) + + return File(path, ensure_binary(message_from_dict(metadata).as_string())) + + +def make_entry_points_file( + name, # type: str + version, # type: str + entry_points, # type: Defaulted[Dict[str, List[str]]] + console_scripts, # type: Defaulted[List[str]] +): + # type: (...) -> Optional[File] + if entry_points is _default and console_scripts is _default: + return None + + if entry_points is _default: + entry_points_data = {} + else: + entry_points_data = deepcopy(entry_points) + + if console_scripts is not _default: + entry_points_data["console_scripts"] = console_scripts + + lines = [] + for section, values in iteritems(entry_points_data): + lines.append("[{}]".format(section)) + lines.extend(values) + + return File( + dist_info_path(name, version, "entry_points.txt"), + ensure_binary("\n".join(lines)), + ) + + +def make_files(files): + # type: (Dict[str, AnyStr]) -> List[File] + return [ + File(name, ensure_binary(contents)) + for name, contents in iteritems(files) + ] + + +def make_metadata_files(name, version, files): + # type: (str, str, Dict[str, AnyStr]) -> List[File] + get_path = partial(dist_info_path, name, version) + return [ + File(get_path(name), ensure_binary(contents)) + for name, contents in iteritems(files) + ] + + +def make_data_files(name, version, files): + # type: (str, str, Dict[str, AnyStr]) -> List[File] + data_dir = "{}-{}.data".format(name, version) + return [ + File("{}/{}".format(data_dir, name), ensure_binary(contents)) + for name, contents in iteritems(files) + ] + + +def urlsafe_b64encode_nopad(data): + # type: (bytes) -> str + return urlsafe_b64encode(data).rstrip(b"=").decode("ascii") + + +def digest(contents): + # type: (bytes) -> str + return "sha256={}".format( + urlsafe_b64encode_nopad(sha256(contents).digest()) + ) + + +def record_file_maker_wrapper( + name, # type: str + version, # type: str + files, # type: List[File] + record, # type: Defaulted[Optional[AnyStr]] + record_callback, # type: Defaulted[RecordCallback] +): + # type: (...) -> Iterable[File] + records = [] # type: List[Record] + for file in files: + records.append( + Record( + file.name, digest(file.contents), str(len(file.contents)) + ) + ) + yield file + + if record is None: + return + + record_path = dist_info_path(name, version, "RECORD") + + if record is not _default: + yield File(record_path, ensure_binary(record)) + return + + records.append(Record(record_path, "", "")) + + if record_callback is not _default: + records = record_callback(records) + + with StringIO(newline=u"") as buf: + writer = csv23.writer(buf) + for record in records: + writer.writerow(map(ensure_text, record)) + contents = buf.getvalue().encode("utf-8") + + yield File(record_path, contents) + + +def wheel_name(name, version, pythons, abis, platforms): + # type: (str, str, str, str, str) -> str + stem = "-".join([ + name, + version, + ".".join(pythons), + ".".join(abis), + ".".join(platforms), + ]) + return "{}.whl".format(stem) + + +class WheelBuilder(object): + """A wheel that can be saved or converted to several formats. + """ + + def __init__(self, name, files): + # type: (str, List[File]) -> None + self._name = name + self._files = files + + def save_to_dir(self, path): + # type: (Union[Path, str]) -> str + """Generate wheel file with correct name and save into the provided + directory. + + :returns the wheel file path + """ + path = Path(path) / self._name + path.write_bytes(self.as_bytes()) + return str(path) + + def save_to(self, path): + # type: (Union[Path, str]) -> str + """Generate wheel file, saving to the provided path. Any parent + directories must already exist. + + :returns the wheel file path + """ + path = Path(path) + path.write_bytes(self.as_bytes()) + return str(path) + + def as_bytes(self): + # type: () -> bytes + with BytesIO() as buf: + with ZipFile(buf, "w") as z: + for file in self._files: + z.writestr(file.name, file.contents) + return buf.getvalue() + + def as_zipfile(self): + # type: () -> ZipFile + return ZipFile(BytesIO(self.as_bytes())) + + +def make_wheel( + name, # type: str + version, # type: str + wheel_metadata=_default, # type: Defaulted[Optional[AnyStr]] + wheel_metadata_updates=_default, # type: Defaulted[Dict[str, HeaderValue]] + metadata=_default, # type: Defaulted[Optional[AnyStr]] + metadata_body=_default, # type: Defaulted[AnyStr] + metadata_updates=_default, # type: Defaulted[Dict[str, HeaderValue]] + extra_files=_default, # type: Defaulted[Dict[str, AnyStr]] + extra_metadata_files=_default, # type: Defaulted[Dict[str, AnyStr]] + extra_data_files=_default, # type: Defaulted[Dict[str, AnyStr]] + console_scripts=_default, # type: Defaulted[List[str]] + entry_points=_default, # type: Defaulted[Dict[str, List[str]]] + record=_default, # type: Defaulted[Optional[AnyStr]] + record_callback=_default, # type: Defaulted[RecordCallback] +): + # type: (...) -> WheelBuilder + """ + Helper function for generating test wheels which are compliant by default. + + Examples: + + ``` + # Basic wheel, which will have valid metadata, RECORD, etc + make_wheel(name="foo", version="0.1.0") + # Wheel with custom metadata + make_wheel( + name="foo", + version="0.1.0", + metadata_updates={ + # Overrides default + "Name": "hello", + # Expands into separate Requires-Dist entries + "Requires-Dist": ["a == 1.0", "b == 2.0; sys_platform == 'win32'"], + }, + ) + ``` + + After specifying the wheel, it can be consumed in several ways: + + ``` + # Normal case, valid wheel we want pip to pick up. + make_wheel(...).save_to_dir(tmpdir) + # For a test case, to check that pip validates contents against wheel name. + make_wheel(name="simple", ...).save_to(tmpdir / "notsimple-...") + # In-memory, for unit tests. + z = make_wheel(...).as_zipfile() + ``` + + Below, any unicode value provided for AnyStr will be encoded as utf-8. + + :param name: name of the distribution, propagated to the .dist-info + directory, METADATA, and wheel file name + :param version: version of the distribution, propagated to the .dist-info + directory, METADATA, and wheel file name + :param wheel_metadata: if provided and None, then no WHEEL metadata file + is generated; else if a string then sets the content of the WHEEL file + :param wheel_metadata_updates: override the default WHEEL metadata fields, + ignored if wheel_metadata is provided + :param metadata: if provided and None, then no METADATA file is generated; + else if a string then sets the content of the METADATA file + :param metadata_body: sets the value of the body text in METADATA, ignored + if metadata is provided + :param metadata_updates: override the default METADATA fields, + ignored if metadata is provided + :param extra_files: map from path to file contents for additional files to + be put in the wheel + :param extra_metadata_files: map from path (relative to .dist-info) to file + contents for additional files to be put in the wheel + :param extra_data_files: map from path (relative to .data) to file contents + for additional files to be put in the wheel + :param console_scripts: list of console scripts text to be put into + entry_points.txt - overrides any value set in entry_points + :param entry_points: + :param record: if provided and None, then no RECORD file is generated; + else if a string then sets the content of the RECORD file + :param record_callback: callback function that receives and can edit the + records before they are written to RECORD, ignored if record is + provided + """ + pythons = ["py2", "py3"] + abis = ["none"] + platforms = ["any"] + tags = list(itertools.product(pythons, abis, platforms)) + + possible_files = [ + make_metadata_file( + name, version, metadata, metadata_updates, metadata_body + ), + make_wheel_metadata_file( + name, version, wheel_metadata, tags, wheel_metadata_updates + ), + make_entry_points_file(name, version, entry_points, console_scripts), + ] + + if extra_files is not _default: + possible_files.extend(make_files(extra_files)) + + if extra_metadata_files is not _default: + possible_files.extend( + make_metadata_files(name, version, extra_metadata_files) + ) + + if extra_data_files is not _default: + possible_files.extend(make_data_files(name, version, extra_data_files)) + + actual_files = filter(None, possible_files) + + files_and_record_file = record_file_maker_wrapper( + name, version, actual_files, record, record_callback + ) + wheel_file_name = wheel_name(name, version, pythons, abis, platforms) + + return WheelBuilder(wheel_file_name, files_and_record_file) diff --git a/tools/requirements/tests.txt b/tools/requirements/tests.txt index 747ea321fb6..136ab9549c6 100644 --- a/tools/requirements/tests.txt +++ b/tools/requirements/tests.txt @@ -1,4 +1,6 @@ cryptography==2.8 +csv23 +enum34; python_version < '3.4' freezegun mock pretend From e83e134f5fb611fc7980477e2eaed998a231342e Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 25 Jan 2020 11:49:26 -0500 Subject: [PATCH 1204/3170] Use wheel_builder for future wheel functional test --- tests/functional/test_install_wheel.py | 22 +++++++++++++++++++--- 1 file changed, 19 insertions(+), 3 deletions(-) diff --git a/tests/functional/test_install_wheel.py b/tests/functional/test_install_wheel.py index 9cd90194437..65a07f6a2d5 100644 --- a/tests/functional/test_install_wheel.py +++ b/tests/functional/test_install_wheel.py @@ -7,21 +7,37 @@ from tests.lib import create_basic_wheel_for_package, skip_if_python2 from tests.lib.path import Path +from tests.lib.wheel import make_wheel -def test_install_from_future_wheel_version(script, data): +# assert_installed expects a package subdirectory, so give it to them +def make_wheel_with_file(name, version, **kwargs): + extra_files = kwargs.setdefault("extra_files", {}) + extra_files["{}/__init__.py".format(name)] = "# example" + return make_wheel(name=name, version=version, **kwargs) + + +def test_install_from_future_wheel_version(script, tmpdir): """ Test installing a future wheel """ from tests.lib import TestFailure + package = make_wheel_with_file( + name="futurewheel", + version="3.0", + wheel_metadata_updates={"Wheel-Version": "3.0"}, + ).save_to_dir(tmpdir) - package = data.packages.joinpath("futurewheel-3.0-py2.py3-none-any.whl") result = script.pip('install', package, '--no-index', expect_error=True) with pytest.raises(TestFailure): result.assert_installed('futurewheel', without_egg_link=True, editable=False) - package = data.packages.joinpath("futurewheel-1.9-py2.py3-none-any.whl") + package = make_wheel_with_file( + name="futurewheel", + version="1.9", + wheel_metadata_updates={"Wheel-Version": "1.9"}, + ).save_to_dir(tmpdir) result = script.pip( 'install', package, '--no-index', expect_stderr=True ) From 667dc392e50f374cf42721b49ed6a2cd6eefdf87 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 25 Jan 2020 11:51:29 -0500 Subject: [PATCH 1205/3170] Remove unused futurewheel fixture --- tests/data/packages/README.txt | 5 ----- .../futurewheel-1.9-py2.py3-none-any.whl | Bin 1778 -> 0 bytes .../futurewheel-3.0-py2.py3-none-any.whl | Bin 1770 -> 0 bytes tests/functional/test_install_wheel.py | 4 +++- 4 files changed, 3 insertions(+), 6 deletions(-) delete mode 100644 tests/data/packages/futurewheel-1.9-py2.py3-none-any.whl delete mode 100644 tests/data/packages/futurewheel-3.0-py2.py3-none-any.whl diff --git a/tests/data/packages/README.txt b/tests/data/packages/README.txt index a7fccc5bbd2..aa957b337f0 100644 --- a/tests/data/packages/README.txt +++ b/tests/data/packages/README.txt @@ -29,11 +29,6 @@ FSPkg ----- for installing from the file system -futurewheel ------------ -Wheels of a Wheel-Version that is newer in minor and major series. -Their version coincides with the apparent Wheel-Version they indicate. - gmpy-1.15.tar.gz ---------------- hash testing (although this pkg isn't needed explicitly) diff --git a/tests/data/packages/futurewheel-1.9-py2.py3-none-any.whl b/tests/data/packages/futurewheel-1.9-py2.py3-none-any.whl deleted file mode 100644 index 703243cbbcc191d5a4afc51bec5f9faaf2b9440f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1778 zcmWIWW@Zs#U|`^2(AP3_Y-nN);{@{9fmj@f(@IN9i&D!oQd4vE<Kr{)GE3s)^$IG3 z{WQ;>UHgQIApovBu+!fqPPhIN4^S5u5No39)-}|#)Jw@MF44`*OUu`HaSe73@(c*^ z^!L*%DlXyv`uW>u{_l?;Jwi7!{-u9UKhUVHK&*<_$bw3c@p@Us`FXxQzTAfl1YG`m z-JIOia%*<nRcFDVkIen5X4$$kT+F0hz5efx{rI5p-kzCqIT@3j&u=y7Q`Xrmr*o`r z7kl}^e#vXI4`;={>q}iK>e`k#t?|UASYh=;*LgqkDVLo%CS~?jSo7br)>BKfE-Fj% zsk}+red25RG~c4oo^v5fX3R1EBKCC2_FY!l3+vO1w>CUvMh&Fs)O}O=fIa~QlsaA? zmE;%1=cJaU=IE7Flz40FYU+CFp7-!Q`w%^lGKKg&<AFxy0kJ$@Bf~vhU46JghFs+J z*3~+9=KSU$gDab#6#1X^*7edkslO@6!|SAuUZ+0$%D@W-7mSTA8DDt9b?UtS#jiTo zcr|pr&Ytwy6r`c4<@=P&(`S2-#tM^5XHTE^zUq5sbxWs&vda#%H1XinxqXE|H?0L? zCA@C)bq#TJaSU<%?5peP<Ei6$1>wR=#t%MupYh!q<bf0lP2SS4B8yn3KYOMo78<45 z@~iUq&-UMil~0~8dnKl_Y`Id4jJLPZr|t_scdgvnIn$COaQ>MmSN0^S<T6@zs%HLl z-N_-SXVJ`!=3sHVPbxW#3=Gap3=9f*9USE9>>uP(d%?SSwt)!igW9xD{u~=yDl0Cs zGA3%Uoe3^7<LNxmpsCMk|LOH?y*o4YUfqqo^Yh%yVBMOTr@dpR-Q^ZASIU*B&-|5D z@ik!4?5aiFYxEYZ`4iCH*5H47YLi;-$CIJ5x?A-Z%ycR#5i0Io5x&{^&$?f`7(!nD zT=`2schQZGIkk^i_E{L$`E&H7MAoE!@0`AFTXN^A;;TVoMLdb?Z=Kk_C@Pb8sSO`f zge}(rGpF6wuN}5pWXu0!437M#x%<j*rj*s`V#?9m-=A!GuC_$n^|LLbO__2E%S26% z{<>qg=SSz<yl`Jy<-+y0;B#lK<}cg6W5+Rxy1?WwyAIoJzTkA`%h}a=4v+3%&U`TO zT)W}>Q^hws|NJW7vGJ73=}XJ2CU1x;TU@#GQSpnEORr{3(p~V{A<)^p?fr{tiy!>| zXFd15|G`H5`0csMvtMmczL<4Bx_oZkhWOS+>2_+{eeA8C{gj&icE{A*6aLwbwROLE z1H2iTM3`|GSHS23gC&h13P<6EYzVee3t|KV!;(fjR73EWVd%!9ms|*AC*n00sT@N$ z620g^7?})Aut?rTb16zef^H~!;ejx88(u@fMG3lL=(!zXSP(Ef;dUV`@1vWCo*5D5 bF*D;c50W(lyjj^mI@p0wA87wt77z~rumNd9 diff --git a/tests/data/packages/futurewheel-3.0-py2.py3-none-any.whl b/tests/data/packages/futurewheel-3.0-py2.py3-none-any.whl deleted file mode 100644 index 372b1be79a2ed856aaebbda505ffdb4e6b9aee30..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1770 zcmWIWW@Zs#U|`^2(AP3_Y-nN);{@{9fmj@f(@IN9i&D!oQd4vE<Kr{)GE3s)^$IG3 z{WQ;>UHgQIApovBx6|JxPPhIN4^S5u5No39)-~2M&`ZfIF44`*OUu`HaSe73@(c*^ z^!L*%DlXyv`uW>u{_l?;Jwi8fLu1^}KA=%sfmju<kp-0?<MpzN^YeUre7O%92)O+B zx-q$FN>F~>RcFDVkIemwW~;?~v}X4zE&YFYO~nU`{Cj5@*5yw=en2gvUr;QIp?%}h zcL!tTi`uSM?Z0#Nw)5Q=O-hF?f)zHEOh3~3q11Vg^D()bR;RDoOnj6dC%QBwoX2BY zwNP1EY|7`kD?KAaZqDoynf!g)y+tx#-OSZ>zr4?xcm5bZY8XYQ?wiU7^av=N)bV<# zB)=d&C$%g!N3W!!#9LceQ`bxPyoc}Ehv;FXeDS$?JkY2-AeP5#WVnZ`s}DEGkc+(D zx?1PXoZlQ|aLxEZk^fn5T`!%J`kR70yiV%qb?URP47^})!Pw}M@r5T`r_SqN{Hk+} zS3}q9>`9+ZK^mG`zE8P4eYOW_tT4HB_Vju0tG;Jew{%J<yX-(q5{Ck{Hx&Tgv=)e! z@Vd>{HN?@yF~sq+udb($r;g_pgbObjKltQ*#&>Iw2T~|Bc}p`dZT2oM7Vg$sE8_Hb z&)+|X|L(B)R2*E&Ep$0p(5Y{!s^m}C7d3aU+&OYare%eHT2WS<;iPR1bB;{f@@G*@ z3y1iegRE%oO^JSNlg7xv;LOCppn%uCL9WjJK`yly_7*ich_F7mH**iC(9hedu3RiU z)&c<z4${Y3j;1;~M2LL);>k7FQ*d|mouB7sPUg>f@nLF0%vsgCgXym4pSjL8KmR^$ zg=Ex<LR-rYt_t4lpeFqni7e6`w<mMWSy|$c{aE9I`l7N$IRWcmYG_$9diihu6MxHq z`L6qGfl9smU*Al7@baO-jsrh?+Pfker)b9dZLT|<6u4$#>Mrg146B%Hax4-*YA9^; zxx91ZiJ%ke0lrM<PjgK0TouHm?NPjS$FtKHmpMy6{`4f{-ZWvk-TPKn?>ku7Uw7-} z_h*^+uY6}qzLENy<J?3BX0|n#Z)Nud#a~{kuN<x&I*DcXkE+t1`7c8rPCPfuZ0!fu za2x6OhqqR$tPQ(0b5DzHV9tK&V?Q=IPTXRVH`!cqvMqyonZmoX^B*3u|5H3mtn}ce zDf!E#r|)@Ut^QFw+by^9_r>{q4&8SwW-7hy=s7KsaBH9Dv=d%4oBcQE2Y53wi7?|X zrhpLz21^=26plg**$`}{6~qVzh9!-5sD|J#yU>kAFR>8DPQ+_0Qu&2$Bzl>FFfs|4 zRFS-k=2Dar1l>^dQUhV=R=kFS%Mf(K(6c$hus~o&!tFv>rbjmqJsTp-V`Ro>9wbu+ Tc(byBbg%=VKG6Q9EFc~LEX-p~ diff --git a/tests/functional/test_install_wheel.py b/tests/functional/test_install_wheel.py index 65a07f6a2d5..6f7b77597ed 100644 --- a/tests/functional/test_install_wheel.py +++ b/tests/functional/test_install_wheel.py @@ -19,7 +19,9 @@ def make_wheel_with_file(name, version, **kwargs): def test_install_from_future_wheel_version(script, tmpdir): """ - Test installing a future wheel + Test installing a wheel with a WHEEL metadata version that is: + - a major version ahead of what we expect (not ok), and + - a minor version ahead of what we expect (ok) """ from tests.lib import TestFailure package = make_wheel_with_file( From f58ec8365bdceb5e0033b5f9b0c96622dff85ce7 Mon Sep 17 00:00:00 2001 From: Christian Clauss <cclauss@me.com> Date: Sun, 2 Feb 2020 19:14:08 +0200 Subject: [PATCH 1206/3170] NEWS.rst: Fix typos discovered by codespell (#7642) https://github.com/codespell-project/codespell --- NEWS.rst | 4 ++-- src/pip/_internal/commands/show.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/NEWS.rst b/NEWS.rst index 1bacf36df15..0715ddb9048 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -85,7 +85,7 @@ Features stale wrapper scripts. (`#7498 <https://github.com/pypa/pip/issues/7498>`_) - The build step of ``pip wheel`` now builds all wheels to a cache first, then copies them to the wheel directory all at once. - Before, it built them to a temporary direcory and moved + Before, it built them to a temporary directory and moved them to the wheel directory one by one. (`#7517 <https://github.com/pypa/pip/issues/7517>`_) - Expand ``~`` prefix to user directory in path options, configs, and environment variables. Values that may be either URL or path are not @@ -520,7 +520,7 @@ Bug Fixes - Handle ``requests.exceptions.RetryError`` raised in ``PackageFinder`` that was causing pip to fail silently when some indexes were unreachable. (`#5270 <https://github.com/pypa/pip/issues/5270>`_, `#5483 <https://github.com/pypa/pip/issues/5483>`_) - Handle a broken stdout pipe more gracefully (e.g. when running ``pip list | head``). (`#4170 <https://github.com/pypa/pip/issues/4170>`_) - Fix crash from setting ``PIP_NO_CACHE_DIR=yes``. (`#5385 <https://github.com/pypa/pip/issues/5385>`_) -- Fix crash from unparseable requirements when checking installed packages. (`#5839 <https://github.com/pypa/pip/issues/5839>`_) +- Fix crash from unparsable requirements when checking installed packages. (`#5839 <https://github.com/pypa/pip/issues/5839>`_) - Fix content type detection if a directory named like an archive is used as a package source. (`#5838 <https://github.com/pypa/pip/issues/5838>`_) - Fix listing of outdated packages that are not dependencies of installed packages in ``pip list --outdated --not-required`` (`#5737 <https://github.com/pypa/pip/issues/5737>`_) - Fix sorting ``TypeError`` in ``move_wheel_files()`` when installing some packages. (`#5868 <https://github.com/pypa/pip/issues/5868>`_) diff --git a/src/pip/_internal/commands/show.py b/src/pip/_internal/commands/show.py index a46b08eeb3d..a61294ba7bb 100644 --- a/src/pip/_internal/commands/show.py +++ b/src/pip/_internal/commands/show.py @@ -142,7 +142,7 @@ def get_requiring_packages(package_name): def print_results(distributions, list_files=False, verbose=False): """ - Print the informations from installed distributions found. + Print the information from installed distributions found. """ results_printed = False for i, dist in enumerate(distributions): From e6a29690d4e4aa3f5efdebf26de9dc8f598f7df5 Mon Sep 17 00:00:00 2001 From: "@Switch01" <sudhanshukumar5459@gmail.com> Date: Sun, 2 Feb 2020 17:03:44 +0530 Subject: [PATCH 1207/3170] Update sphinx version to 2.3.1 --- news/b397ed3a-49da-4279-b71a-7b67de91c34a.trivial | 0 tools/requirements/docs.txt | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 news/b397ed3a-49da-4279-b71a-7b67de91c34a.trivial diff --git a/news/b397ed3a-49da-4279-b71a-7b67de91c34a.trivial b/news/b397ed3a-49da-4279-b71a-7b67de91c34a.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tools/requirements/docs.txt b/tools/requirements/docs.txt index 74fc71d96da..b780ebf649a 100644 --- a/tools/requirements/docs.txt +++ b/tools/requirements/docs.txt @@ -1,4 +1,4 @@ -sphinx == 2.2.0 +sphinx == 2.3.1 git+https://github.com/python/python-docs-theme.git#egg=python-docs-theme git+https://github.com/pypa/pypa-docs-theme.git#egg=pypa-docs-theme From 7534dccd6c436df71482c2c0e9328be248a0df5f Mon Sep 17 00:00:00 2001 From: Bhavam Vidyarthi <Bhavam.vidyarthi@gmail.com> Date: Mon, 3 Feb 2020 02:47:02 +0530 Subject: [PATCH 1208/3170] Removed tests/scripts folder with its contents (#7680) --- .../html/development/architecture/anatomy.rst | 1 - news/7680.removal | 1 + tests/scripts/test_all_pip.py | 139 ------------------ 3 files changed, 1 insertion(+), 140 deletions(-) create mode 100644 news/7680.removal delete mode 100644 tests/scripts/test_all_pip.py diff --git a/docs/html/development/architecture/anatomy.rst b/docs/html/development/architecture/anatomy.rst index 9613460b0cb..7415358451b 100644 --- a/docs/html/development/architecture/anatomy.rst +++ b/docs/html/development/architecture/anatomy.rst @@ -50,7 +50,6 @@ The ``README``, license, ``pyproject.toml``, ``setup.py``, and so on are in the * ``data/`` *[test data for running tests -- pesudo package index in it! Lots of small packages that are invalid or are valid. Test fixtures. Used by functional tests]* * ``functional/`` *[functional tests of pip’s CLI -- end-to-end, invoke pip in subprocess & check results of execution against desired result. This also is what makes test suite slow]* * ``lib/`` *[helpers for tests]* - * ``scripts/`` *[will probably die in future in a refactor -- scripts for running all of the tests, but we use pytest now. Someone could make a PR to remove this! Good first issue!]* * ``unit/`` *[unit tests -- fast and small and nice!]* * ``yaml/`` *[resolver tests! They’re written in YAML. This folder just contains .yaml files -- actual code for reading/running them is in lib/yaml.py . This is fine!]* diff --git a/news/7680.removal b/news/7680.removal new file mode 100644 index 00000000000..7d582156f23 --- /dev/null +++ b/news/7680.removal @@ -0,0 +1 @@ +Remove unused ``tests/scripts/test_all_pip.py`` test script and the ``tests/scripts`` folder. diff --git a/tests/scripts/test_all_pip.py b/tests/scripts/test_all_pip.py deleted file mode 100644 index 18c97f1c819..00000000000 --- a/tests/scripts/test_all_pip.py +++ /dev/null @@ -1,139 +0,0 @@ -import os -import re -import subprocess -import sys -from os.path import abspath, dirname - -from pip._vendor.six.moves.urllib import request as urllib_request - -from pip._internal.utils.misc import rmtree - -src_folder = dirname(dirname(abspath(__file__))) - -if sys.platform == 'win32': - bin_dir = 'Scripts' -else: - bin_dir = 'bin' - - -def all_projects(): - data = urllib_request.urlopen('http://pypi.org/simple/').read() - projects = [m.group(1) for m in re.finditer(r'<a.*?>(.+)</a>', data)] - return projects - - -def main(args=None): - if args is None: - args = sys.argv[1:] - if not args: - print('Usage: test_all_pip.py <output-dir>') - sys.exit(1) - output = os.path.abspath(args[0]) - if not os.path.exists(output): - print('Creating %s' % output) - os.makedirs(output) - pending_fn = os.path.join(output, 'pending.txt') - if not os.path.exists(pending_fn): - print('Downloading pending list') - projects = all_projects() - print('Found %s projects' % len(projects)) - with open(pending_fn, 'w') as f: - for name in projects: - f.write(name + '\n') - print('Starting testing...') - while os.stat(pending_fn).st_size: - _test_packages(output, pending_fn) - print('Finished all pending!') - - -def _test_packages(output, pending_fn): - package = get_last_item(pending_fn) - print('Testing package %s' % package) - dest_dir = os.path.join(output, package) - print('Creating virtualenv in %s' % dest_dir) - create_venv(dest_dir) - print('Uninstalling actual pip') - code = subprocess.check_call([ - os.path.join(dest_dir, bin_dir, 'pip'), - 'uninstall', - '-y', - 'pip', - ]) - assert not code, 'pip uninstallation failed' - print('Installing development pip') - code = subprocess.check_call( - [ - os.path.join(dest_dir, bin_dir, 'python'), - 'setup.py', - 'install' - ], - cwd=src_folder, - ) - assert not code, 'pip installation failed' - print('Trying installation of %s' % dest_dir) - code = subprocess.check_call([ - os.path.join(dest_dir, bin_dir, 'pip'), - 'install', - package, - ]) - if code: - print('Installation of %s failed' % package) - print('Now checking easy_install...') - create_venv(dest_dir) - code = subprocess.check_call([ - os.path.join(dest_dir, bin_dir, 'easy_install'), - package, - ]) - if code: - print('easy_install also failed') - add_package(os.path.join(output, 'easy-failure.txt'), package) - else: - print('easy_install succeeded') - add_package(os.path.join(output, 'failure.txt'), package) - pop_last_item(pending_fn, package) - else: - print('Installation of %s succeeded' % package) - add_package(os.path.join(output, 'success.txt'), package) - pop_last_item(pending_fn, package) - rmtree(dest_dir) - - -def create_venv(dest_dir): - if os.path.exists(dest_dir): - rmtree(dest_dir) - print('Creating virtualenv in %s' % dest_dir) - code = subprocess.check_call([ - 'virtualenv', - '--no-site-packages', - dest_dir, - ]) - assert not code, "virtualenv failed" - - -def get_last_item(fn): - f = open(fn, 'r') - lines = f.readlines() - f.close() - return lines[-1].strip() - - -def pop_last_item(fn, line=None): - f = open(fn, 'r') - lines = f.readlines() - f.close() - if line: - assert lines[-1].strip() == line.strip() - lines.pop() - f = open(fn, 'w') - f.writelines(lines) - f.close() - - -def add_package(filename, package): - f = open(filename, 'a') - f.write(package + '\n') - f.close() - - -if __name__ == '__main__': - main() From 9f92d3d46569f9855f4ea57186dbcd836009d96b Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko <wk@sydorenko.org.ua> Date: Tue, 21 Jan 2020 16:58:04 +0100 Subject: [PATCH 1209/3170] =?UTF-8?q?=F0=9F=94=A5=20Exterminate=20files=20?= =?UTF-8?q?that=20aren't=20Git-tracked?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This is a follow-up improvement for https://github.com/pypa/pip/pull/7624/files --- noxfile.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/noxfile.py b/noxfile.py index 19a6eaa00a5..09a1d37ea8b 100644 --- a/noxfile.py +++ b/noxfile.py @@ -206,9 +206,8 @@ def build_release(session): session.log("# Checkout the tag") session.run("git", "checkout", version, external=True, silent=True) - session.log("# Cleanup build/ before building the wheel") - if release.have_files_in_folder("build"): - shutil.rmtree("build") + session.log("# Wipe Git-untracked files before building the wheel") + session.run("git", "clean", "-fxd", external=True, silent=True) session.log("# Build distributions") session.run("python", "setup.py", "sdist", "bdist_wheel", silent=True) From 231ce2e36c9a33e53163742b53a6339503d69dba Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko <wk@sydorenko.org.ua> Date: Sat, 25 Jan 2020 13:17:29 +0100 Subject: [PATCH 1210/3170] =?UTF-8?q?Revert=20"=F0=9F=94=A5=20Exterminate?= =?UTF-8?q?=20files=20that=20aren't=20Git-tracked"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 9c1a4d4c4d64d72f5eff5100bbb18c008b7408ad. --- noxfile.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/noxfile.py b/noxfile.py index 09a1d37ea8b..19a6eaa00a5 100644 --- a/noxfile.py +++ b/noxfile.py @@ -206,8 +206,9 @@ def build_release(session): session.log("# Checkout the tag") session.run("git", "checkout", version, external=True, silent=True) - session.log("# Wipe Git-untracked files before building the wheel") - session.run("git", "clean", "-fxd", external=True, silent=True) + session.log("# Cleanup build/ before building the wheel") + if release.have_files_in_folder("build"): + shutil.rmtree("build") session.log("# Build distributions") session.run("python", "setup.py", "sdist", "bdist_wheel", silent=True) From 957fb28b3008bc4cdcd5124fb8132697acd1ab2b Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko <wk@sydorenko.org.ua> Date: Sat, 25 Jan 2020 13:40:45 +0100 Subject: [PATCH 1211/3170] Abort on Git-untracked files in the workdir Allow own `.nox/` runtime env files. --- noxfile.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/noxfile.py b/noxfile.py index 19a6eaa00a5..ea50533c644 100644 --- a/noxfile.py +++ b/noxfile.py @@ -7,6 +7,7 @@ import glob import os import shutil +import subprocess import sys import nox @@ -210,6 +211,27 @@ def build_release(session): if release.have_files_in_folder("build"): shutil.rmtree("build") + session.log( + "# Check if there's any Git-untracked files before building the wheel", + ) + has_git_untracked_files = any( + bool(l) for l in + # session.run doesn't seem to return any output + subprocess.check_output( + ( + "git", "ls-files", "--ignored", "--exclude-standard", + "--others", "--", ".", + ), + text=True, + ).split('\n') + if not l.startswith('.nox/build-release/') # exclude nox env file + ) + if has_git_untracked_files: + session.error( + "There are untracked files in the Git repo workdir. " + "Remove them and try again", + ) + session.log("# Build distributions") session.run("python", "setup.py", "sdist", "bdist_wheel", silent=True) From 63ee9b702b24ff259f765e6f4deddcf1e8a61f6f Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko <wk@sydorenko.org.ua> Date: Sun, 26 Jan 2020 03:06:22 +0100 Subject: [PATCH 1212/3170] Make release task build dists @ clean tmp checkout Co-Authored-By: Pradyun Gedam <pradyunsg@gmail.com> --- noxfile.py | 59 +++++++++++++++++++++++++++++++++++++++++++++++++----- 1 file changed, 54 insertions(+), 5 deletions(-) diff --git a/noxfile.py b/noxfile.py index ea50533c644..39027fc8e47 100644 --- a/noxfile.py +++ b/noxfile.py @@ -4,11 +4,14 @@ # The following comment should be removed at some point in the future. # mypy: disallow-untyped-defs=False +import contextlib import glob import os +import pathlib import shutil import subprocess import sys +import tempfile import nox @@ -188,6 +191,48 @@ def prepare_release(session): release.commit_file(session, VERSION_FILE, message="Bump for development") +@contextlib.contextmanager +def workdir(nox_session, dir_path: pathlib.Path): + """Temporarily chdir when entering CM and chdir back on exit.""" + orig_dir = pathlib.Path.cwd() + + nox_session.log(f"# Changing dir to {dir_path}") + os.chdir(dir_path) + try: + yield dir_path + finally: + nox_session.log(f"# Changing dir back to {orig_dir}") + os.chdir(orig_dir) + + +@contextlib.contextmanager +def mk_tmp_git_checkout(nox_session, target_commitish: str): + """Make a clean checkout of a given version in tmp dir. + + This is a context manager that cleans up after itself. + """ + with tempfile.TemporaryDirectory() as tmp_dir_path: + tmp_dir = pathlib.Path(tmp_dir_path) + git_checkout_dir = tmp_dir / f'pip-build-{target_commitish}' + nox_session.log( + f"# Creating a temporary Git checkout at {git_checkout_dir!s}", + ) + nox_session.run( + 'git', 'worktree', 'add', '--force', '--checkout', + str(git_checkout_dir), str(target_commitish), + external=True, silent=True, + ) + + try: + yield git_checkout_dir + finally: + nox_session.run( + 'git', 'worktree', 'remove', '--force', + str(git_checkout_dir), + external=True, silent=True, + ) + + @nox.session(name="build-release") def build_release(session): version = release.get_version_from_arguments(session.posargs) @@ -204,9 +249,16 @@ def build_release(session): session.log("# Install dependencies") session.install("setuptools", "wheel", "twine") - session.log("# Checkout the tag") - session.run("git", "checkout", version, external=True, silent=True) + with mk_tmp_git_checkout(session, version) as build_dir_path: + with workdir(session, build_dir_path): + build_dists(session) + tmp_dist_dir = build_dir_path / 'dist' + session.log(f"# Copying dists from {tmp_dist_dir}") + shutil.copytree(tmp_dist_dir, 'dist') + + +def build_dists(session): session.log("# Cleanup build/ before building the wheel") if release.have_files_in_folder("build"): shutil.rmtree("build") @@ -238,9 +290,6 @@ def build_release(session): session.log("# Verify distributions") session.run("twine", "check", *glob.glob("dist/*"), silent=True) - session.log("# Checkout the master branch") - session.run("git", "checkout", "master", external=True, silent=True) - @nox.session(name="upload-release") def upload_release(session): From 3da19325f4402b43c0fa4ef00e937748720edda6 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko <wk@sydorenko.org.ua> Date: Tue, 28 Jan 2020 16:36:04 +0100 Subject: [PATCH 1213/3170] Use nox.sessions.Session.chdir instead of os.chdir --- noxfile.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/noxfile.py b/noxfile.py index 39027fc8e47..364e6e69604 100644 --- a/noxfile.py +++ b/noxfile.py @@ -196,13 +196,11 @@ def workdir(nox_session, dir_path: pathlib.Path): """Temporarily chdir when entering CM and chdir back on exit.""" orig_dir = pathlib.Path.cwd() - nox_session.log(f"# Changing dir to {dir_path}") - os.chdir(dir_path) + nox_session.chdir(dir_path) try: yield dir_path finally: - nox_session.log(f"# Changing dir back to {orig_dir}") - os.chdir(orig_dir) + nox_session.chdir(orig_dir) @contextlib.contextmanager From bed90abe2d2982ad722eeea6f6f2604cf687a9ee Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko <wk@sydorenko.org.ua> Date: Tue, 28 Jan 2020 16:36:58 +0100 Subject: [PATCH 1214/3170] Remove `dist/` before copying the dists back --- noxfile.py | 1 + 1 file changed, 1 insertion(+) diff --git a/noxfile.py b/noxfile.py index 364e6e69604..80a7cfcade7 100644 --- a/noxfile.py +++ b/noxfile.py @@ -253,6 +253,7 @@ def build_release(session): tmp_dist_dir = build_dir_path / 'dist' session.log(f"# Copying dists from {tmp_dist_dir}") + shutil.rmtree('dist', ignore_errors=True) # remove empty `dist/` shutil.copytree(tmp_dist_dir, 'dist') From 01c455375425880e5cf00162f0c6257a4fb7a3e8 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko <wk@sydorenko.org.ua> Date: Tue, 28 Jan 2020 16:40:39 +0100 Subject: [PATCH 1215/3170] Make mk_tmp_git_checkout docstring shorter --- noxfile.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/noxfile.py b/noxfile.py index 80a7cfcade7..833c367826a 100644 --- a/noxfile.py +++ b/noxfile.py @@ -205,10 +205,7 @@ def workdir(nox_session, dir_path: pathlib.Path): @contextlib.contextmanager def mk_tmp_git_checkout(nox_session, target_commitish: str): - """Make a clean checkout of a given version in tmp dir. - - This is a context manager that cleans up after itself. - """ + """Make a clean checkout of a given version in tmp dir.""" with tempfile.TemporaryDirectory() as tmp_dir_path: tmp_dir = pathlib.Path(tmp_dir_path) git_checkout_dir = tmp_dir / f'pip-build-{target_commitish}' From daba4b4c9e4b5e5aff3bca1aa880b515d1afc9f0 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko <wk@sydorenko.org.ua> Date: Tue, 28 Jan 2020 16:45:42 +0100 Subject: [PATCH 1216/3170] Move tmp dir logging to build_release --- noxfile.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/noxfile.py b/noxfile.py index 833c367826a..1c77701e369 100644 --- a/noxfile.py +++ b/noxfile.py @@ -209,9 +209,6 @@ def mk_tmp_git_checkout(nox_session, target_commitish: str): with tempfile.TemporaryDirectory() as tmp_dir_path: tmp_dir = pathlib.Path(tmp_dir_path) git_checkout_dir = tmp_dir / f'pip-build-{target_commitish}' - nox_session.log( - f"# Creating a temporary Git checkout at {git_checkout_dir!s}", - ) nox_session.run( 'git', 'worktree', 'add', '--force', '--checkout', str(git_checkout_dir), str(target_commitish), @@ -245,6 +242,10 @@ def build_release(session): session.install("setuptools", "wheel", "twine") with mk_tmp_git_checkout(session, version) as build_dir_path: + session.log( + "# Start the build in an isolated, " + f"temporary Git checkout at {build_dir_path!s}", + ) with workdir(session, build_dir_path): build_dists(session) From db028e516ce49e4aaef55af8973a760827a047ee Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko <wk@sydorenko.org.ua> Date: Tue, 28 Jan 2020 16:58:16 +0100 Subject: [PATCH 1217/3170] Refactor isolated_temporary_checkout CM naming Co-Authored-By: Pradyun Gedam <pradyunsg@gmail.com> --- noxfile.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/noxfile.py b/noxfile.py index 1c77701e369..5611fbe6eef 100644 --- a/noxfile.py +++ b/noxfile.py @@ -204,14 +204,17 @@ def workdir(nox_session, dir_path: pathlib.Path): @contextlib.contextmanager -def mk_tmp_git_checkout(nox_session, target_commitish: str): +def isolated_temporary_checkout( + nox_session: nox.sessions.Session, + target_ref: str, +) -> pathlib.Path: """Make a clean checkout of a given version in tmp dir.""" with tempfile.TemporaryDirectory() as tmp_dir_path: tmp_dir = pathlib.Path(tmp_dir_path) - git_checkout_dir = tmp_dir / f'pip-build-{target_commitish}' + git_checkout_dir = tmp_dir / f'pip-build-{target_ref}' nox_session.run( 'git', 'worktree', 'add', '--force', '--checkout', - str(git_checkout_dir), str(target_commitish), + str(git_checkout_dir), str(target_ref), external=True, silent=True, ) @@ -241,7 +244,7 @@ def build_release(session): session.log("# Install dependencies") session.install("setuptools", "wheel", "twine") - with mk_tmp_git_checkout(session, version) as build_dir_path: + with isolated_temporary_checkout(session, version) as build_dir_path: session.log( "# Start the build in an isolated, " f"temporary Git checkout at {build_dir_path!s}", From 3306f21ed8c29e57f469f4cf00ae4dc4ce98c87e Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko <wk@sydorenko.org.ua> Date: Tue, 28 Jan 2020 17:05:01 +0100 Subject: [PATCH 1218/3170] Drop unnecessary build dir cleanup in tmp checkout Co-Authored-By: Pradyun Gedam <pradyunsg@gmail.com> --- noxfile.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/noxfile.py b/noxfile.py index 5611fbe6eef..619d14ae058 100644 --- a/noxfile.py +++ b/noxfile.py @@ -259,10 +259,6 @@ def build_release(session): def build_dists(session): - session.log("# Cleanup build/ before building the wheel") - if release.have_files_in_folder("build"): - shutil.rmtree("build") - session.log( "# Check if there's any Git-untracked files before building the wheel", ) From 75f8c6d6ff47cdb8dbe19354a3d3220c78624a90 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko <wk@sydorenko.org.ua> Date: Tue, 28 Jan 2020 17:07:27 +0100 Subject: [PATCH 1219/3170] Reword has_git_untracked_files error log msg Co-Authored-By: Pradyun Gedam <pradyunsg@gmail.com> --- noxfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index 619d14ae058..7a7e707fa0e 100644 --- a/noxfile.py +++ b/noxfile.py @@ -276,7 +276,7 @@ def build_dists(session): ) if has_git_untracked_files: session.error( - "There are untracked files in the Git repo workdir. " + "There are untracked files in the working directory. " "Remove them and try again", ) From 6a0b1f26752236ec5ba9740bf72ad0326141a2cb Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko <wk@sydorenko.org.ua> Date: Tue, 28 Jan 2020 17:21:02 +0100 Subject: [PATCH 1220/3170] Refactor the untracked files check Co-Authored-By: Pradyun Gedam <pradyunsg@gmail.com> --- noxfile.py | 33 +++++++++++++++++++++------------ 1 file changed, 21 insertions(+), 12 deletions(-) diff --git a/noxfile.py b/noxfile.py index 7a7e707fa0e..bf603eabab9 100644 --- a/noxfile.py +++ b/noxfile.py @@ -262,19 +262,28 @@ def build_dists(session): session.log( "# Check if there's any Git-untracked files before building the wheel", ) - has_git_untracked_files = any( - bool(l) for l in - # session.run doesn't seem to return any output - subprocess.check_output( - ( - "git", "ls-files", "--ignored", "--exclude-standard", - "--others", "--", ".", - ), - text=True, - ).split('\n') - if not l.startswith('.nox/build-release/') # exclude nox env file + + def get_git_untracked_files(): + """List all local file paths that aren't tracked by Git.""" + git_ls_files_cmd = ( + "git", "ls-files", "--ignored", "--exclude-standard", + "--others", "--", ".", + ) + # session.run doesn't seem to return any output: + ls_files_out = subprocess.check_output(git_ls_files_cmd, text=True) + ls_files_out_lines = ls_files_out.splitlines() + for file_name in ls_files_out_lines: + if file_name.strip(): # it's useless if empty + continue + + yield file_name + + has_forbidden_git_untracked_files = any( + # Don't report the environment this session is running in + not untracked_file.startswith('.nox/build-release/') + for untracked_file in get_git_untracked_files() ) - if has_git_untracked_files: + if has_forbidden_git_untracked_files: session.error( "There are untracked files in the working directory. " "Remove them and try again", From 6f1a43e4b2aca1b8ce5fc3b8d203ba82763ce112 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko <wk@sydorenko.org.ua> Date: Tue, 28 Jan 2020 17:30:11 +0100 Subject: [PATCH 1221/3170] Relocate helper CMs to tools.automation.release --- noxfile.py | 48 +++------------------------- tools/automation/release/__init__.py | 45 +++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 45 deletions(-) diff --git a/noxfile.py b/noxfile.py index bf603eabab9..438d7eb9696 100644 --- a/noxfile.py +++ b/noxfile.py @@ -4,14 +4,11 @@ # The following comment should be removed at some point in the future. # mypy: disallow-untyped-defs=False -import contextlib import glob import os -import pathlib import shutil import subprocess import sys -import tempfile import nox @@ -191,43 +188,6 @@ def prepare_release(session): release.commit_file(session, VERSION_FILE, message="Bump for development") -@contextlib.contextmanager -def workdir(nox_session, dir_path: pathlib.Path): - """Temporarily chdir when entering CM and chdir back on exit.""" - orig_dir = pathlib.Path.cwd() - - nox_session.chdir(dir_path) - try: - yield dir_path - finally: - nox_session.chdir(orig_dir) - - -@contextlib.contextmanager -def isolated_temporary_checkout( - nox_session: nox.sessions.Session, - target_ref: str, -) -> pathlib.Path: - """Make a clean checkout of a given version in tmp dir.""" - with tempfile.TemporaryDirectory() as tmp_dir_path: - tmp_dir = pathlib.Path(tmp_dir_path) - git_checkout_dir = tmp_dir / f'pip-build-{target_ref}' - nox_session.run( - 'git', 'worktree', 'add', '--force', '--checkout', - str(git_checkout_dir), str(target_ref), - external=True, silent=True, - ) - - try: - yield git_checkout_dir - finally: - nox_session.run( - 'git', 'worktree', 'remove', '--force', - str(git_checkout_dir), - external=True, silent=True, - ) - - @nox.session(name="build-release") def build_release(session): version = release.get_version_from_arguments(session.posargs) @@ -244,15 +204,15 @@ def build_release(session): session.log("# Install dependencies") session.install("setuptools", "wheel", "twine") - with isolated_temporary_checkout(session, version) as build_dir_path: + with release.isolated_temporary_checkout(session, version) as build_dir: session.log( "# Start the build in an isolated, " - f"temporary Git checkout at {build_dir_path!s}", + f"temporary Git checkout at {build_dir!s}", ) - with workdir(session, build_dir_path): + with release.workdir(session, build_dir): build_dists(session) - tmp_dist_dir = build_dir_path / 'dist' + tmp_dist_dir = build_dir / 'dist' session.log(f"# Copying dists from {tmp_dist_dir}") shutil.rmtree('dist', ignore_errors=True) # remove empty `dist/` shutil.copytree(tmp_dist_dir, 'dist') diff --git a/tools/automation/release/__init__.py b/tools/automation/release/__init__.py index e983f4cbb80..50cce88cefb 100644 --- a/tools/automation/release/__init__.py +++ b/tools/automation/release/__init__.py @@ -3,10 +3,13 @@ These are written according to the order they are called in. """ +import contextlib import io import os +import pathlib import subprocess -from typing import List, Optional, Set +import tempfile +from typing import Iterator, List, Optional, Set from nox.sessions import Session @@ -126,3 +129,43 @@ def have_files_in_folder(folder_name: str) -> bool: if not os.path.exists(folder_name): return False return bool(os.listdir(folder_name)) + + +@contextlib.contextmanager +def workdir( + nox_session: Session, + dir_path: pathlib.Path, +) -> Iterator[pathlib.Path]: + """Temporarily chdir when entering CM and chdir back on exit.""" + orig_dir = pathlib.Path.cwd() + + nox_session.chdir(dir_path) + try: + yield dir_path + finally: + nox_session.chdir(orig_dir) + + +@contextlib.contextmanager +def isolated_temporary_checkout( + nox_session: Session, + target_ref: str, +) -> Iterator[pathlib.Path]: + """Make a clean checkout of a given version in tmp dir.""" + with tempfile.TemporaryDirectory() as tmp_dir_path: + tmp_dir = pathlib.Path(tmp_dir_path) + git_checkout_dir = tmp_dir / f'pip-build-{target_ref}' + nox_session.run( + 'git', 'worktree', 'add', '--force', '--checkout', + str(git_checkout_dir), str(target_ref), + external=True, silent=True, + ) + + try: + yield git_checkout_dir + finally: + nox_session.run( + 'git', 'worktree', 'remove', '--force', + str(git_checkout_dir), + external=True, silent=True, + ) From 9d592b394a2a2cb2003221d8ccf8a283eee1367b Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko <wk@sydorenko.org.ua> Date: Mon, 3 Feb 2020 11:12:47 +0100 Subject: [PATCH 1222/3170] =?UTF-8?q?=F0=9F=8E=A8=20Copy=20and=20log=20bui?= =?UTF-8?q?lt=20dists=20explicitly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- noxfile.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/noxfile.py b/noxfile.py index 438d7eb9696..fee6d2a5825 100644 --- a/noxfile.py +++ b/noxfile.py @@ -210,15 +210,18 @@ def build_release(session): f"temporary Git checkout at {build_dir!s}", ) with release.workdir(session, build_dir): - build_dists(session) + tmp_dists = build_dists(session) - tmp_dist_dir = build_dir / 'dist' - session.log(f"# Copying dists from {tmp_dist_dir}") + tmp_dist_paths = (build_dir / p for p in tmp_dists) + session.log(f"# Copying dists from {build_dir}") shutil.rmtree('dist', ignore_errors=True) # remove empty `dist/` - shutil.copytree(tmp_dist_dir, 'dist') + for dist in tmp_dist_paths: + session.log(f"# Copying {dist}") + shutil.copy(dist, 'dist') def build_dists(session): + """Return dists with valid metadata.""" session.log( "# Check if there's any Git-untracked files before building the wheel", ) @@ -251,9 +254,12 @@ def get_git_untracked_files(): session.log("# Build distributions") session.run("python", "setup.py", "sdist", "bdist_wheel", silent=True) + produced_dists = glob.glob("dist/*") - session.log("# Verify distributions") - session.run("twine", "check", *glob.glob("dist/*"), silent=True) + session.log(f"# Verify distributions: {', '.join(produced_dists)}") + session.run("twine", "check", *produced_dists, silent=True) + + return produced_dists @nox.session(name="upload-release") From d772171ad921e4431ea9a69507c4876746a3b00f Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko <wk@sydorenko.org.ua> Date: Mon, 3 Feb 2020 11:22:02 +0100 Subject: [PATCH 1223/3170] =?UTF-8?q?=E2=99=BB=20Relocate=20get=5Fgit=5Fun?= =?UTF-8?q?tracked=5Ffiles=20to=20utils?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- noxfile.py | 18 +----------------- tools/automation/release/__init__.py | 16 ++++++++++++++++ 2 files changed, 17 insertions(+), 17 deletions(-) diff --git a/noxfile.py b/noxfile.py index fee6d2a5825..a266131b9c1 100644 --- a/noxfile.py +++ b/noxfile.py @@ -7,7 +7,6 @@ import glob import os import shutil -import subprocess import sys import nox @@ -226,25 +225,10 @@ def build_dists(session): "# Check if there's any Git-untracked files before building the wheel", ) - def get_git_untracked_files(): - """List all local file paths that aren't tracked by Git.""" - git_ls_files_cmd = ( - "git", "ls-files", "--ignored", "--exclude-standard", - "--others", "--", ".", - ) - # session.run doesn't seem to return any output: - ls_files_out = subprocess.check_output(git_ls_files_cmd, text=True) - ls_files_out_lines = ls_files_out.splitlines() - for file_name in ls_files_out_lines: - if file_name.strip(): # it's useless if empty - continue - - yield file_name - has_forbidden_git_untracked_files = any( # Don't report the environment this session is running in not untracked_file.startswith('.nox/build-release/') - for untracked_file in get_git_untracked_files() + for untracked_file in release.get_git_untracked_files() ) if has_forbidden_git_untracked_files: session.error( diff --git a/tools/automation/release/__init__.py b/tools/automation/release/__init__.py index 50cce88cefb..c82133c7439 100644 --- a/tools/automation/release/__init__.py +++ b/tools/automation/release/__init__.py @@ -169,3 +169,19 @@ def isolated_temporary_checkout( str(git_checkout_dir), external=True, silent=True, ) + + +def get_git_untracked_files() -> Iterator[str]: + """List all local file paths that aren't tracked by Git.""" + git_ls_files_cmd = ( + "git", "ls-files", + "--ignored", "--exclude-standard", + "--others", "--", ".", + ) + # session.run doesn't seem to return any output: + ls_files_out = subprocess.check_output(git_ls_files_cmd, text=True) + for file_name in ls_files_out.splitlines(): + if file_name.strip(): # it's useless if empty + continue + + yield file_name From e8cc348816bcf86e680e6459d812824fc6e11b47 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko <wk@sydorenko.org.ua> Date: Sat, 18 Jan 2020 19:22:21 +0100 Subject: [PATCH 1224/3170] Add a change fragment --- news/7611-gh-actions--linters-adjustment.trivial | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/7611-gh-actions--linters-adjustment.trivial diff --git a/news/7611-gh-actions--linters-adjustment.trivial b/news/7611-gh-actions--linters-adjustment.trivial new file mode 100644 index 00000000000..43efc8aa42d --- /dev/null +++ b/news/7611-gh-actions--linters-adjustment.trivial @@ -0,0 +1 @@ +Test vendoring lint target under GitHub Actions CI/CD. From b662f76ac7df9cf1345977945e632d263d8fad6e Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko <wk@sydorenko.org.ua> Date: Sat, 18 Jan 2020 18:58:03 +0100 Subject: [PATCH 1225/3170] Adjust GitHub Actions CI/CD linters * workflow vs job name * TOX_PARALLEL_NO_SPINNER * vendoring * env state logging * Pip cache --- .github/workflows/python-linters.yml | 66 ++++++++++++++++++++++++---- 1 file changed, 57 insertions(+), 9 deletions(-) diff --git a/.github/workflows/python-linters.yml b/.github/workflows/python-linters.yml index 81a87d46346..030d8f9d4d5 100644 --- a/.github/workflows/python-linters.yml +++ b/.github/workflows/python-linters.yml @@ -1,4 +1,6 @@ -name: Code quality +name: >- + 🤖 + Code quality on: push: @@ -10,24 +12,53 @@ on: jobs: linters: - name: 🤖 + name: >- + ${{ matrix.env.TOXENV }} + / + ${{ matrix.python-version }} + / + ${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: # max-parallel: 5 matrix: + python-version: + - 3.8 os: - - ubuntu-18.04 + - ubuntu-latest - windows-latest - macos-latest env: - TOXENV: docs - TOXENV: lint + - TOXENV: vendoring + + env: + TOX_PARALLEL_NO_SPINNER: 1 + steps: - uses: actions/checkout@master - - name: Set up Python ${{ matrix.env.PYTHON_VERSION || 3.8 }} + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v1 with: - python-version: ${{ matrix.env.PYTHON_VERSION || 3.8 }} + python-version: ${{ matrix.python-version }} + - name: Log Python version + run: >- + python --version + - name: Log Python location + run: >- + which python + - name: Log Python env + run: >- + python -m sysconfig + - name: Pip cache + uses: actions/cache@v1 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('tools/requirements/tests.txt') }}-${{ hashFiles('tools/requirements/docs.txt') }}-${{ hashFiles('tox.ini') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- - name: set PY run: echo "::set-env name=PY::$(python -VV | sha256sum | cut -d' ' -f1)" - uses: actions/cache@v1 @@ -38,16 +69,33 @@ jobs: run: | git config --global user.email "pypa-dev@googlegroups.com" git config --global user.name "pip" - - name: Update setuptools and tox dependencies - run: | + - name: Update setuptools + run: >- python -m pip install --upgrade setuptools + - name: Install tox + run: >- python -m pip install --upgrade tox tox-venv + - name: Log the list of packages + run: >- python -m pip freeze --all - name: 'Initialize tox envs: ${{ matrix.env.TOXENV }}' run: >- - python -m tox --notest --skip-missing-interpreters false + python -m + tox + --parallel auto + --notest + --skip-missing-interpreters false env: ${{ matrix.env }} + - name: Pre-fetch pre-commit hooks + # This is to separate test step from deps install + if: matrix.env.TOXENV == 'lint' + run: >- + .tox/lint/${{ runner.os == 'Windows' && 'Scripts' || 'bin' }}/python -m + pre_commit + install-hooks - name: Test with tox run: >- - python -m tox + python -m + tox + --parallel auto env: ${{ matrix.env }} From 476bcb8d7c08394a84bb3b1450783980b35ea41d Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko <wk@sydorenko.org.ua> Date: Sat, 18 Jan 2020 19:35:42 +0100 Subject: [PATCH 1226/3170] =?UTF-8?q?=F0=9F=93=9D=20Document=20running=20v?= =?UTF-8?q?endoring=20check=20@=20GitHub=20CI/CD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/html/development/ci.rst | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/docs/html/development/ci.rst b/docs/html/development/ci.rst index e68b2b4e648..4604c7c6a54 100644 --- a/docs/html/development/ci.rst +++ b/docs/html/development/ci.rst @@ -77,13 +77,13 @@ Current run tests Developer tasks --------------- -======== =============== ================ =========== ============ - OS docs lint vendoring packages -======== =============== ================ =========== ============ -Linux Travis, Github Travis, Github Travis Azure -Windows Azure -MacOS Azure -======== =============== ================ =========== ============ +======== =============== ================ ================== ============ + OS docs lint vendoring packages +======== =============== ================ ================== ============ +Linux Travis, Github Travis, Github Travis, Github Azure +Windows Azure +MacOS Azure +======== =============== ================ ================== ============ Actual testing -------------- From f12c417abf83723e397979e74dc702f4041e17dc Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko <wk@sydorenko.org.ua> Date: Mon, 3 Feb 2020 14:10:31 +0100 Subject: [PATCH 1227/3170] Collapse job name --- .github/workflows/python-linters.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/workflows/python-linters.yml b/.github/workflows/python-linters.yml index 030d8f9d4d5..e0b8fbe86be 100644 --- a/.github/workflows/python-linters.yml +++ b/.github/workflows/python-linters.yml @@ -13,11 +13,7 @@ on: jobs: linters: name: >- - ${{ matrix.env.TOXENV }} - / - ${{ matrix.python-version }} - / - ${{ matrix.os }} + ${{ matrix.env.TOXENV }}/${{ matrix.python-version }}@${{ matrix.os }} runs-on: ${{ matrix.os }} strategy: # max-parallel: 5 From 9be7454fd69081d06a3f8234a08a6d791e13173f Mon Sep 17 00:00:00 2001 From: Christoph Reiter <reiter.christoph@gmail.com> Date: Sun, 2 Feb 2020 19:29:31 +0100 Subject: [PATCH 1228/3170] fix un-vendored support; add missing entry for appdirs pip has started to use the vendored appdirs directly since #7501 but didn't add an alias for the unbundled case. This adds the missing alias. --- news/7690.bugfix | 1 + src/pip/_vendor/__init__.py | 1 + 2 files changed, 2 insertions(+) create mode 100644 news/7690.bugfix diff --git a/news/7690.bugfix b/news/7690.bugfix new file mode 100644 index 00000000000..5d7c5d0545c --- /dev/null +++ b/news/7690.bugfix @@ -0,0 +1 @@ +Fix an 'appdirs' related import error in case pip is installed debundled i.e., without vendored dependencies. diff --git a/src/pip/_vendor/__init__.py b/src/pip/_vendor/__init__.py index a0fcb8e2cc4..1112e1012b0 100644 --- a/src/pip/_vendor/__init__.py +++ b/src/pip/_vendor/__init__.py @@ -58,6 +58,7 @@ def vendored(modulename): sys.path[:] = glob.glob(os.path.join(WHEEL_DIR, "*.whl")) + sys.path # Actually alias all of our vendored dependencies. + vendored("appdirs") vendored("cachecontrol") vendored("colorama") vendored("contextlib2") From 51ecf4de3519b01dfe57995160f55b2585f54a2f Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 4 Feb 2020 16:08:40 +0530 Subject: [PATCH 1229/3170] Drop NEWS entry for a debundling change --- news/7690.bugfix | 1 - 1 file changed, 1 deletion(-) delete mode 100644 news/7690.bugfix diff --git a/news/7690.bugfix b/news/7690.bugfix deleted file mode 100644 index 5d7c5d0545c..00000000000 --- a/news/7690.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix an 'appdirs' related import error in case pip is installed debundled i.e., without vendored dependencies. From 0bf5fc85d86c6ce2e9e5c78b8eef1d41fea3007e Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 4 Feb 2020 18:50:38 +0530 Subject: [PATCH 1230/3170] Add a .vendor NEWS fragment --- news/7690.vendor | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/7690.vendor diff --git a/news/7690.vendor b/news/7690.vendor new file mode 100644 index 00000000000..e6e79b1ecc2 --- /dev/null +++ b/news/7690.vendor @@ -0,0 +1 @@ +Update semi-supported debundling script to reflect that appdirs is vendored. From 0b4ec28a3900b58b258a7c2c760ad467c29f7929 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 25 Jan 2020 15:56:32 -0500 Subject: [PATCH 1231/3170] Centralize addition of no_clean argument We want to rely on --no-clean being a valid option for RequirementCommand types, so move it to one place close to the code that will depend on it. --- src/pip/_internal/cli/req_command.py | 9 ++++++++- src/pip/_internal/commands/download.py | 1 - src/pip/_internal/commands/install.py | 1 - src/pip/_internal/commands/wheel.py | 1 - 4 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 9383b3b8dca..3383e11af03 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -9,6 +9,7 @@ import os from functools import partial +from pip._internal.cli import cmdoptions from pip._internal.cli.base_command import Command from pip._internal.cli.command_context import CommandContextMixIn from pip._internal.exceptions import CommandError @@ -32,7 +33,7 @@ if MYPY_CHECK_RUNNING: from optparse import Values - from typing import List, Optional, Tuple + from typing import Any, List, Optional, Tuple from pip._internal.cache import WheelCache from pip._internal.models.target_python import TargetPython from pip._internal.req.req_set import RequirementSet @@ -151,6 +152,12 @@ def handle_pip_version_check(self, options): class RequirementCommand(IndexGroupCommand): + def __init__(self, *args, **kw): + # type: (Any, Any) -> None + super(RequirementCommand, self).__init__(*args, **kw) + + self.cmd_opts.add_option(cmdoptions.no_clean()) + @staticmethod def make_requirement_preparer( temp_build_dir, # type: TempDirectory diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index 24da3eb2a26..4a19091ae04 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -52,7 +52,6 @@ def __init__(self, *args, **kw): cmd_opts.add_option(cmdoptions.prefer_binary()) cmd_opts.add_option(cmdoptions.src()) 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()) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 02a187c8aa2..2b548642274 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -227,7 +227,6 @@ def __init__(self, *args, **kw): cmd_opts.add_option(cmdoptions.no_binary()) cmd_opts.add_option(cmdoptions.only_binary()) cmd_opts.add_option(cmdoptions.prefer_binary()) - cmd_opts.add_option(cmdoptions.no_clean()) cmd_opts.add_option(cmdoptions.require_hashes()) cmd_opts.add_option(cmdoptions.progress_bar()) diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index eb44bcee459..016be1f8bd8 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -101,7 +101,6 @@ def __init__(self, *args, **kw): "pip only finds stable versions."), ) - cmd_opts.add_option(cmdoptions.no_clean()) cmd_opts.add_option(cmdoptions.require_hashes()) index_opts = cmdoptions.make_option_group( From 7068e58b6ff1aefba437dd4e3e98e733061882b9 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 25 Jan 2020 16:23:46 -0500 Subject: [PATCH 1232/3170] Configure tempdir registry This mirrors the current logic within the individual requirement-related commands (install, wheel) for setting options.no_clean, which is used to determine whether we need to delete directories. Next, we'll add the actual directories to track and remove them from being managed by other objects. --- src/pip/_internal/cli/req_command.py | 40 ++++++++++++++++++++++++-- src/pip/_internal/commands/download.py | 3 +- src/pip/_internal/commands/install.py | 3 +- src/pip/_internal/commands/wheel.py | 3 +- 4 files changed, 44 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 3383e11af03..cac49a0303d 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -12,7 +12,7 @@ from pip._internal.cli import cmdoptions from pip._internal.cli.base_command import Command from pip._internal.cli.command_context import CommandContextMixIn -from pip._internal.exceptions import CommandError +from pip._internal.exceptions import CommandError, PreviousBuildDirError from pip._internal.index.package_finder import PackageFinder from pip._internal.legacy_resolve import Resolver from pip._internal.models.selection_prefs import SelectionPreferences @@ -34,11 +34,16 @@ if MYPY_CHECK_RUNNING: from optparse import Values from typing import Any, List, Optional, Tuple + from pip._internal.cache import WheelCache from pip._internal.models.target_python import TargetPython from pip._internal.req.req_set import RequirementSet from pip._internal.req.req_tracker import RequirementTracker - from pip._internal.utils.temp_dir import TempDirectory + from pip._internal.utils.temp_dir import ( + TempDirectory, + TempDirectoryTypeRegistry, + ) + logger = logging.getLogger(__name__) @@ -150,6 +155,37 @@ def handle_pip_version_check(self, options): pip_self_version_check(session, options) +KEEPABLE_TEMPDIR_TYPES = [] # type: List[str] + + +def with_cleanup(func): + # type: (Any) -> Any + """Decorator for common logic related to managing temporary + directories. + """ + def configure_tempdir_registry(registry): + # type: (TempDirectoryTypeRegistry) -> None + for t in KEEPABLE_TEMPDIR_TYPES: + registry.set_delete(t, False) + + def wrapper(self, options, args): + # type: (RequirementCommand, Values, List[Any]) -> Optional[int] + assert self.tempdir_registry is not None + if options.no_clean: + configure_tempdir_registry(self.tempdir_registry) + + try: + return func(self, options, args) + except PreviousBuildDirError: + # This kind of conflict can occur when the user passes an explicit + # build directory with a pre-existing folder. In that case we do + # not want to accidentally remove it. + configure_tempdir_registry(self.tempdir_registry) + raise + + return wrapper + + class RequirementCommand(IndexGroupCommand): def __init__(self, *args, **kw): diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index 4a19091ae04..d1c59990a83 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -8,7 +8,7 @@ from pip._internal.cli import cmdoptions from pip._internal.cli.cmdoptions import make_target_python -from pip._internal.cli.req_command import RequirementCommand +from pip._internal.cli.req_command import RequirementCommand, with_cleanup from pip._internal.req import RequirementSet from pip._internal.req.req_tracker import get_requirement_tracker from pip._internal.utils.misc import ensure_dir, normalize_path, write_output @@ -76,6 +76,7 @@ def __init__(self, *args, **kw): self.parser.insert_option_group(0, index_opts) self.parser.insert_option_group(0, cmd_opts) + @with_cleanup def run(self, options, args): options.ignore_installed = True # editable doesn't really make sense for `pip download`, but the bowels diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 2b548642274..cf629c81c0b 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -21,7 +21,7 @@ from pip._internal.cache import WheelCache from pip._internal.cli import cmdoptions from pip._internal.cli.cmdoptions import make_target_python -from pip._internal.cli.req_command import RequirementCommand +from pip._internal.cli.req_command import RequirementCommand, with_cleanup from pip._internal.cli.status_codes import ERROR, SUCCESS from pip._internal.exceptions import ( CommandError, @@ -238,6 +238,7 @@ def __init__(self, *args, **kw): self.parser.insert_option_group(0, index_opts) self.parser.insert_option_group(0, cmd_opts) + @with_cleanup def run(self, options, args): # type: (Values, List[Any]) -> int cmdoptions.check_install_build_global(options) diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index 016be1f8bd8..d9308b152a4 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -11,7 +11,7 @@ from pip._internal.cache import WheelCache from pip._internal.cli import cmdoptions -from pip._internal.cli.req_command import RequirementCommand +from pip._internal.cli.req_command import RequirementCommand, with_cleanup from pip._internal.exceptions import CommandError, PreviousBuildDirError from pip._internal.req import RequirementSet from pip._internal.req.req_tracker import get_requirement_tracker @@ -111,6 +111,7 @@ def __init__(self, *args, **kw): self.parser.insert_option_group(0, index_opts) self.parser.insert_option_group(0, cmd_opts) + @with_cleanup def run(self, options, args): # type: (Values, List[Any]) -> None cmdoptions.check_install_build_global(options) From 2f4bfc3efc3093d234078a9778c20a0b217ac136 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 25 Jan 2020 16:37:29 -0500 Subject: [PATCH 1233/3170] Add InstallRequirement._temp_build_dir to tempdir registry Now we can refactor this to be globally managed, and it will have the same behavior as it does currently (if there is any PreviousBuildDirError it will not be cleaned up). --- src/pip/_internal/cli/req_command.py | 3 ++- src/pip/_internal/req/req_install.py | 4 ++-- src/pip/_internal/utils/temp_dir.py | 9 ++++++++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index cac49a0303d..8386aa85d5a 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -29,6 +29,7 @@ make_link_collector, pip_self_version_check, ) +from pip._internal.utils.temp_dir import tempdir_kinds from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: @@ -155,7 +156,7 @@ def handle_pip_version_check(self, options): pip_self_version_check(session, options) -KEEPABLE_TEMPDIR_TYPES = [] # type: List[str] +KEEPABLE_TEMPDIR_TYPES = [tempdir_kinds.REQ_BUILD] def with_cleanup(func): diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index e560cf449bf..0c5301e30f2 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -49,7 +49,7 @@ rmtree, ) from pip._internal.utils.packaging import get_metadata -from pip._internal.utils.temp_dir import TempDirectory +from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.virtualenv import running_under_virtualenv from pip._internal.vcs import vcs @@ -358,7 +358,7 @@ def ensure_build_location(self, build_dir): # Some systems have /tmp as a symlink which confuses custom # builds (such as numpy). Thus, we ensure that the real path # is returned. - self._temp_build_dir = TempDirectory(kind="req-build") + self._temp_build_dir = TempDirectory(kind=tempdir_kinds.REQ_BUILD) return self._temp_build_dir.path if self.editable: diff --git a/src/pip/_internal/utils/temp_dir.py b/src/pip/_internal/utils/temp_dir.py index ae730e4917e..c2dd4bfde18 100644 --- a/src/pip/_internal/utils/temp_dir.py +++ b/src/pip/_internal/utils/temp_dir.py @@ -9,7 +9,7 @@ from pip._vendor.contextlib2 import ExitStack -from pip._internal.utils.misc import rmtree +from pip._internal.utils.misc import enum, rmtree from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: @@ -21,6 +21,13 @@ logger = logging.getLogger(__name__) +# Kinds of temporary directories. Only needed for ones that are +# globally-managed. +tempdir_kinds = enum( + REQ_BUILD="req-build" +) + + _tempdir_manager = None # type: Optional[ExitStack] From c35cb7819bf2c02624b75df68955bd013d8452d5 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 30 Jan 2020 21:58:04 -0500 Subject: [PATCH 1234/3170] Globally-manage InstallRequirement._temp_build_dir InstallRequirement.remove_temporary_source was already being called at the end of processing (as part of RequirementSet.cleanup()), so this doesn't change behavior - cleanup still happens right after the command finishes. --- src/pip/_internal/req/req_install.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 0c5301e30f2..e49adb63742 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -358,7 +358,9 @@ def ensure_build_location(self, build_dir): # Some systems have /tmp as a symlink which confuses custom # builds (such as numpy). Thus, we ensure that the real path # is returned. - self._temp_build_dir = TempDirectory(kind=tempdir_kinds.REQ_BUILD) + self._temp_build_dir = TempDirectory( + kind=tempdir_kinds.REQ_BUILD, globally_managed=True + ) return self._temp_build_dir.path if self.editable: @@ -418,9 +420,7 @@ def remove_temporary_source(self): logger.debug('Removing source in %s', self.source_dir) rmtree(self.source_dir) self.source_dir = None - if self._temp_build_dir: - self._temp_build_dir.cleanup() - self._temp_build_dir = None + self._temp_build_dir = None self.build_env.cleanup() def check_if_exists(self, use_user_site): From bdde27bfd847f1ad4fdc95326239011ac286a8d6 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 30 Jan 2020 22:04:37 -0500 Subject: [PATCH 1235/3170] Add BuildEnvironment._temp_dir to tempdir registry Similar to the InstallRequirement temp build dir, now we'll be able to refactor this to be globally managed. --- src/pip/_internal/build_env.py | 4 ++-- src/pip/_internal/cli/req_command.py | 2 +- src/pip/_internal/utils/temp_dir.py | 3 ++- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py index f55f0e6b8d9..9e914115ff0 100644 --- a/src/pip/_internal/build_env.py +++ b/src/pip/_internal/build_env.py @@ -17,7 +17,7 @@ from pip import __file__ as pip_location from pip._internal.utils.subprocess import call_subprocess -from pip._internal.utils.temp_dir import TempDirectory +from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.ui import open_spinner @@ -54,7 +54,7 @@ class BuildEnvironment(object): def __init__(self): # type: () -> None - self._temp_dir = TempDirectory(kind="build-env") + self._temp_dir = TempDirectory(kind=tempdir_kinds.BUILD_ENV) self._prefixes = OrderedDict(( (name, _Prefix(os.path.join(self._temp_dir.path, name))) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 8386aa85d5a..58fe49dc772 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -156,7 +156,7 @@ def handle_pip_version_check(self, options): pip_self_version_check(session, options) -KEEPABLE_TEMPDIR_TYPES = [tempdir_kinds.REQ_BUILD] +KEEPABLE_TEMPDIR_TYPES = [tempdir_kinds.BUILD_ENV, tempdir_kinds.REQ_BUILD] def with_cleanup(func): diff --git a/src/pip/_internal/utils/temp_dir.py b/src/pip/_internal/utils/temp_dir.py index c2dd4bfde18..428173d8c96 100644 --- a/src/pip/_internal/utils/temp_dir.py +++ b/src/pip/_internal/utils/temp_dir.py @@ -24,7 +24,8 @@ # Kinds of temporary directories. Only needed for ones that are # globally-managed. tempdir_kinds = enum( - REQ_BUILD="req-build" + BUILD_ENV="build-env", + REQ_BUILD="req-build", ) From 39d1c51fdbd23c835fe64a7e89d6362f7b992815 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 30 Jan 2020 22:08:57 -0500 Subject: [PATCH 1236/3170] Globally-manage BuildEnvironment._temp_dir --- src/pip/_internal/build_env.py | 4 +++- src/pip/_internal/req/req_install.py | 1 - tests/unit/test_build_env.py | 17 ++++++++++------- 3 files changed, 13 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py index 9e914115ff0..1c217065bfe 100644 --- a/src/pip/_internal/build_env.py +++ b/src/pip/_internal/build_env.py @@ -54,7 +54,9 @@ class BuildEnvironment(object): def __init__(self): # type: () -> None - self._temp_dir = TempDirectory(kind=tempdir_kinds.BUILD_ENV) + self._temp_dir = TempDirectory( + kind=tempdir_kinds.BUILD_ENV, globally_managed=True + ) self._prefixes = OrderedDict(( (name, _Prefix(os.path.join(self._temp_dir.path, name))) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index e49adb63742..6088bacc1af 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -421,7 +421,6 @@ def remove_temporary_source(self): rmtree(self.source_dir) self.source_dir = None self._temp_build_dir = None - self.build_env.cleanup() def check_if_exists(self, use_user_site): # type: (bool) -> None diff --git a/tests/unit/test_build_env.py b/tests/unit/test_build_env.py index ff3b2e90cef..b4469046bdd 100644 --- a/tests/unit/test_build_env.py +++ b/tests/unit/test_build_env.py @@ -29,6 +29,7 @@ def run_with_build_env(script, setup_script_contents, SelectionPreferences ) from pip._internal.network.session import PipSession + from pip._internal.utils.temp_dir import global_tempdir_manager link_collector = LinkCollector( session=PipSession(), @@ -41,19 +42,21 @@ def run_with_build_env(script, setup_script_contents, link_collector=link_collector, selection_prefs=selection_prefs, ) - build_env = BuildEnvironment() - try: + with global_tempdir_manager(): + build_env = BuildEnvironment() ''' % str(script.scratch_path)) + indent(dedent(setup_script_contents), ' ') + - dedent( - ''' + indent( + dedent( + ''' if len(sys.argv) > 1: with build_env: subprocess.check_call((sys.executable, sys.argv[1])) - finally: - build_env.cleanup() - ''') + ''' + ), + ' ' + ) ) args = ['python', build_env_script] if test_script_contents is not None: From d99462a067ba5d9a91328d52fea93de8c864e518 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 30 Jan 2020 22:09:53 -0500 Subject: [PATCH 1237/3170] Remove unused BuildEnvironment.cleanup --- src/pip/_internal/build_env.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py index 1c217065bfe..6134e0a368e 100644 --- a/src/pip/_internal/build_env.py +++ b/src/pip/_internal/build_env.py @@ -135,10 +135,6 @@ def __exit__(self, exc_type, exc_val, exc_tb): else: os.environ[varname] = old_value - def cleanup(self): - # type: () -> None - self._temp_dir.cleanup() - def check_requirements(self, reqs): # type: (Iterable[str]) -> Tuple[Set[Tuple[str, str]], Set[str]] """Return 2 sets: From e800cb16046e4992f50569a9fa1e78017c55f2ea Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 30 Jan 2020 22:10:47 -0500 Subject: [PATCH 1238/3170] Make BuildEnvironment._temp_dir a local variable --- src/pip/_internal/build_env.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py index 6134e0a368e..0cbcfdf2811 100644 --- a/src/pip/_internal/build_env.py +++ b/src/pip/_internal/build_env.py @@ -54,12 +54,12 @@ class BuildEnvironment(object): def __init__(self): # type: () -> None - self._temp_dir = TempDirectory( + temp_dir = TempDirectory( kind=tempdir_kinds.BUILD_ENV, globally_managed=True ) self._prefixes = OrderedDict(( - (name, _Prefix(os.path.join(self._temp_dir.path, name))) + (name, _Prefix(os.path.join(temp_dir.path, name))) for name in ('normal', 'overlay') )) @@ -78,7 +78,7 @@ def __init__(self): get_python_lib(plat_specific=True), ) } - self._site_dir = os.path.join(self._temp_dir.path, 'site') + self._site_dir = os.path.join(temp_dir.path, 'site') if not os.path.exists(self._site_dir): os.mkdir(self._site_dir) with open(os.path.join(self._site_dir, 'sitecustomize.py'), 'w') as fp: From b8c0d0175d3dc7c124a76bb0cd7965325c208229 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 4 Feb 2020 21:25:30 -0500 Subject: [PATCH 1239/3170] Do not test unpack_http_url or unpack_file_url The tests for unpack_{file,http}_url relies on these functions to both retrieve and unpack. We want to move unpacking out, so call unpack_url instead. --- tests/unit/test_operations_prepare.py | 37 +++++++++++++++------------ 1 file changed, 20 insertions(+), 17 deletions(-) diff --git a/tests/unit/test_operations_prepare.py b/tests/unit/test_operations_prepare.py index 6040db26ec0..0158eed5197 100644 --- a/tests/unit/test_operations_prepare.py +++ b/tests/unit/test_operations_prepare.py @@ -13,8 +13,7 @@ from pip._internal.operations.prepare import ( _copy_source_tree, _download_http_url, - unpack_file_url, - unpack_http_url, + unpack_url, ) from pip._internal.utils.hashes import Hashes from pip._internal.utils.urls import path_to_url @@ -27,7 +26,7 @@ from tests.lib.requests_mocks import MockResponse -def test_unpack_http_url_with_urllib_response_without_content_type(data): +def test_unpack_url_with_urllib_response_without_content_type(data): """ It should download and unpack files even if no Content-Type header exists """ @@ -46,7 +45,7 @@ def _fake_session_get(*args, **kwargs): link = Link(uri) temp_dir = mkdtemp() try: - unpack_http_url( + unpack_url( link, temp_dir, downloader=downloader, @@ -172,7 +171,7 @@ def test_copy_source_tree_with_unreadable_dir_fails(clean_project, tmpdir): assert expected_files == copied_files -class Test_unpack_file_url(object): +class Test_unpack_url(object): def prep(self, tmpdir, data): self.build_dir = tmpdir.joinpath('build') @@ -185,16 +184,17 @@ def prep(self, tmpdir, data): self.dist_path2 = data.packages.joinpath(self.dist_file2) self.dist_url = Link(path_to_url(self.dist_path)) self.dist_url2 = Link(path_to_url(self.dist_path2)) + self.no_downloader = Mock(side_effect=AssertionError) - def test_unpack_file_url_no_download(self, tmpdir, data): + def test_unpack_url_no_download(self, tmpdir, data): self.prep(tmpdir, data) - unpack_file_url(self.dist_url, self.build_dir) + unpack_url(self.dist_url, self.build_dir, self.no_downloader) assert os.path.isdir(os.path.join(self.build_dir, 'simple')) assert not os.path.isfile( os.path.join(self.download_dir, self.dist_file)) - def test_unpack_file_url_bad_hash(self, tmpdir, data, - monkeypatch): + def test_unpack_url_bad_hash(self, tmpdir, data, + monkeypatch): """ Test when the file url hash fragment is wrong """ @@ -202,16 +202,18 @@ def test_unpack_file_url_bad_hash(self, tmpdir, data, url = '{}#md5=bogus'.format(self.dist_url.url) dist_url = Link(url) with pytest.raises(HashMismatch): - unpack_file_url(dist_url, - self.build_dir, - hashes=Hashes({'md5': ['bogus']})) + unpack_url(dist_url, + self.build_dir, + downloader=self.no_downloader, + hashes=Hashes({'md5': ['bogus']})) - def test_unpack_file_url_thats_a_dir(self, tmpdir, data): + def test_unpack_url_thats_a_dir(self, tmpdir, data): self.prep(tmpdir, data) dist_path = data.packages.joinpath("FSPkg") dist_url = Link(path_to_url(dist_path)) - unpack_file_url(dist_url, self.build_dir, - download_dir=self.download_dir) + unpack_url(dist_url, self.build_dir, + downloader=self.no_downloader, + download_dir=self.download_dir) assert os.path.isdir(os.path.join(self.build_dir, 'fspkg')) @@ -219,7 +221,7 @@ def test_unpack_file_url_thats_a_dir(self, tmpdir, data): '.nox', '.tox' ]) -def test_unpack_file_url_excludes_expected_dirs(tmpdir, exclude_dir): +def test_unpack_url_excludes_expected_dirs(tmpdir, exclude_dir): src_dir = tmpdir / 'src' dst_dir = tmpdir / 'dst' src_included_file = src_dir.joinpath('file.txt') @@ -239,9 +241,10 @@ def test_unpack_file_url_excludes_expected_dirs(tmpdir, exclude_dir): dst_included_dir = dst_dir.joinpath('subdir', exclude_dir) src_link = Link(path_to_url(src_dir)) - unpack_file_url( + unpack_url( src_link, dst_dir, + Mock(side_effect=AssertionError), download_dir=None ) assert not os.path.isdir(dst_excluded_dir) From 062ccf9dba50a44502aab15a89acf45b9217c545 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 4 Feb 2020 21:31:59 -0500 Subject: [PATCH 1240/3170] Use early-return style in unpack_url This will make it easier to move directory handling up to unpack_url. --- src/pip/_internal/operations/prepare.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 0b61f20524d..afca0531693 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -274,7 +274,7 @@ def unpack_url( return None # file urls - elif link.is_file: + if link.is_file: return unpack_file_url(link, location, download_dir, hashes=hashes) # http urls From 395787bebc3febb9e5cb02fcd640dc6362e23f2c Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 4 Feb 2020 21:32:50 -0500 Subject: [PATCH 1241/3170] Inline link.file_path in unpack_file_url --- src/pip/_internal/operations/prepare.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index afca0531693..c002e9b47c7 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -216,12 +216,11 @@ def unpack_file_url( # type: (...) -> Optional[str] """Unpack link into location. """ - link_path = link.file_path # If it's a url to a local directory if link.is_existing_dir(): if os.path.isdir(location): rmtree(location) - _copy_source_tree(link_path, location) + _copy_source_tree(link.file_path, location) return None # If a download dir is specified, is the file already there and valid? @@ -234,7 +233,7 @@ def unpack_file_url( if already_downloaded_path: from_path = already_downloaded_path else: - from_path = link_path + from_path = link.file_path # If --require-hashes is off, `hashes` is either empty, the # link's embedded hash, or MissingHashes; it is required to From 791725aad9a8bdeff39bf1092c58bf797dbd51d1 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 4 Feb 2020 21:35:31 -0500 Subject: [PATCH 1242/3170] Move directory handling from unpack_file_url to unpack_url --- src/pip/_internal/operations/prepare.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index c002e9b47c7..544e8937514 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -213,16 +213,9 @@ def unpack_file_url( download_dir=None, # type: Optional[str] hashes=None # type: Optional[Hashes] ): - # type: (...) -> Optional[str] + # type: (...) -> str """Unpack link into location. """ - # If it's a url to a local directory - if link.is_existing_dir(): - if os.path.isdir(location): - rmtree(location) - _copy_source_tree(link.file_path, location) - return None - # If a download dir is specified, is the file already there and valid? already_downloaded_path = None if download_dir: @@ -272,6 +265,13 @@ def unpack_url( unpack_vcs_link(link, location) return None + # If it's a url to a local directory + if link.is_existing_dir(): + if os.path.isdir(location): + rmtree(location) + _copy_source_tree(link.file_path, location) + return None + # file urls if link.is_file: return unpack_file_url(link, location, download_dir, hashes=hashes) From 71eaa4658fffeb72729cf16e20a9e2d5e9ef3b71 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 4 Feb 2020 21:39:04 -0500 Subject: [PATCH 1243/3170] Return File from unpack_* functions Since we need both the file path and content type to unpack, and we want to move unpacking out of the lower-level functions, return all the information needed so it's easier to move the unpacking out. --- src/pip/_internal/operations/prepare.py | 35 ++++++++++++++++--------- 1 file changed, 23 insertions(+), 12 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 544e8937514..65c2127806b 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -134,6 +134,13 @@ def _copy_file(filename, location, link): logger.info('Saved %s', display_path(download_location)) +class File(object): + def __init__(self, path, content_type): + # type: (str, str) -> None + self.path = path + self.content_type = content_type + + def unpack_http_url( link, # type: Link location, # type: str @@ -141,7 +148,7 @@ def unpack_http_url( download_dir=None, # type: Optional[str] hashes=None, # type: Optional[Hashes] ): - # type: (...) -> str + # type: (...) -> File temp_dir = TempDirectory(kind="unpack", globally_managed=True) # If a download dir is specified, is the file already downloaded there? already_downloaded_path = None @@ -159,11 +166,13 @@ def unpack_http_url( link, downloader, temp_dir.path, hashes ) + file = File(from_path, content_type) + # 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) + unpack_file(file.path, location, file.content_type) - return from_path + return file def _copy2_ignoring_special_files(src, dest): @@ -213,7 +222,7 @@ def unpack_file_url( download_dir=None, # type: Optional[str] hashes=None # type: Optional[Hashes] ): - # type: (...) -> str + # type: (...) -> File """Unpack link into location. """ # If a download dir is specified, is the file already there and valid? @@ -238,11 +247,13 @@ def unpack_file_url( content_type = mimetypes.guess_type(from_path)[0] + file = File(from_path, content_type) + # 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) + unpack_file(file.path, location, file.content_type) - return from_path + return file def unpack_url( @@ -252,7 +263,7 @@ def unpack_url( download_dir=None, # type: Optional[str] hashes=None, # type: Optional[Hashes] ): - # type: (...) -> Optional[str] + # type: (...) -> Optional[File] """Unpack link into location, downloading if required. :param hashes: A Hashes object, one of whose embedded hashes must match, @@ -476,7 +487,7 @@ def prepare_linked_requirement( download_dir = self.wheel_download_dir try: - local_path = unpack_url( + local_file = unpack_url( link, req.source_dir, self.downloader, download_dir, hashes=hashes, ) @@ -493,8 +504,8 @@ def prepare_linked_requirement( # For use in later processing, preserve the file path on the # requirement. - if local_path: - req.local_file_path = local_path + if local_file: + req.local_file_path = local_file.path if link.is_wheel: if download_dir: @@ -518,10 +529,10 @@ def prepare_linked_requirement( if download_dir: if link.is_existing_dir(): logger.info('Link is a directory, ignoring download_dir') - elif local_path and not os.path.exists( + elif local_file and not os.path.exists( os.path.join(download_dir, link.filename) ): - _copy_file(local_path, download_dir, link) + _copy_file(local_file.path, download_dir, link) if self._download_should_save: # Make a .zip of the source_dir we already created. From 35a52c139746a6aa8485c308aa5b3c98d39dfadd Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 4 Feb 2020 21:40:41 -0500 Subject: [PATCH 1244/3170] Hold file in intermediate variable in unpack_url --- src/pip/_internal/operations/prepare.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 65c2127806b..44d84ba5ac5 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -285,11 +285,11 @@ def unpack_url( # file urls if link.is_file: - return unpack_file_url(link, location, download_dir, hashes=hashes) + file = unpack_file_url(link, location, download_dir, hashes=hashes) # http urls else: - return unpack_http_url( + file = unpack_http_url( link, location, downloader, @@ -297,6 +297,8 @@ def unpack_url( hashes=hashes, ) + return file + def _download_http_url( link, # type: Link From 135f2ac0ef8c72303b32e83ec321b4e9b4aa859d Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 4 Feb 2020 21:41:49 -0500 Subject: [PATCH 1245/3170] Move unpacking into unpack_url --- src/pip/_internal/operations/prepare.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 44d84ba5ac5..5878162fea7 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -168,10 +168,6 @@ def unpack_http_url( file = File(from_path, content_type) - # unpack the archive to the build dir location. even when only - # downloading archives, they have to be unpacked to parse dependencies - unpack_file(file.path, location, file.content_type) - return file @@ -249,10 +245,6 @@ def unpack_file_url( file = File(from_path, content_type) - # unpack the archive to the build dir location. even when only downloading - # archives, they have to be unpacked to parse dependencies - unpack_file(file.path, location, file.content_type) - return file @@ -297,6 +289,10 @@ def unpack_url( hashes=hashes, ) + # unpack the archive to the build dir location. even when only downloading + # archives, they have to be unpacked to parse dependencies + unpack_file(file.path, location, file.content_type) + return file From ada2f55f13eb706f6b5ce50df1b61dfa34d68567 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 4 Feb 2020 21:42:21 -0500 Subject: [PATCH 1246/3170] Inline file variable --- src/pip/_internal/operations/prepare.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 5878162fea7..ae5622a74ea 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -166,9 +166,7 @@ def unpack_http_url( link, downloader, temp_dir.path, hashes ) - file = File(from_path, content_type) - - return file + return File(from_path, content_type) def _copy2_ignoring_special_files(src, dest): @@ -243,9 +241,7 @@ def unpack_file_url( content_type = mimetypes.guess_type(from_path)[0] - file = File(from_path, content_type) - - return file + return File(from_path, content_type) def unpack_url( From c825c53f76173205990817161a1f85556316494f Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 4 Feb 2020 21:43:08 -0500 Subject: [PATCH 1247/3170] Rename unpack_{file,http}_url since they no longer unpack --- src/pip/_internal/operations/prepare.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index ae5622a74ea..aebbb8cbfc1 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -141,7 +141,7 @@ def __init__(self, path, content_type): self.content_type = content_type -def unpack_http_url( +def get_http_url( link, # type: Link location, # type: str downloader, # type: Downloader @@ -210,14 +210,14 @@ def ignore(d, names): shutil.copytree(source, target, **kwargs) -def unpack_file_url( +def get_file_url( link, # type: Link location, # type: str download_dir=None, # type: Optional[str] hashes=None # type: Optional[Hashes] ): # type: (...) -> File - """Unpack link into location. + """Get file and optionally check its hash. """ # If a download dir is specified, is the file already there and valid? already_downloaded_path = None @@ -273,11 +273,11 @@ def unpack_url( # file urls if link.is_file: - file = unpack_file_url(link, location, download_dir, hashes=hashes) + file = get_file_url(link, location, download_dir, hashes=hashes) # http urls else: - file = unpack_http_url( + file = get_http_url( link, location, downloader, From 4acc059cfd32f05d0c3b5d34d402d7a36356408e Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 4 Feb 2020 21:55:42 -0500 Subject: [PATCH 1248/3170] Remove unused argument --- src/pip/_internal/operations/prepare.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index aebbb8cbfc1..7d3aa69495b 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -143,7 +143,6 @@ def __init__(self, path, content_type): def get_http_url( link, # type: Link - location, # type: str downloader, # type: Downloader download_dir=None, # type: Optional[str] hashes=None, # type: Optional[Hashes] @@ -212,7 +211,6 @@ def ignore(d, names): def get_file_url( link, # type: Link - location, # type: str download_dir=None, # type: Optional[str] hashes=None # type: Optional[Hashes] ): @@ -273,13 +271,12 @@ def unpack_url( # file urls if link.is_file: - file = get_file_url(link, location, download_dir, hashes=hashes) + file = get_file_url(link, download_dir, hashes=hashes) # http urls else: file = get_http_url( link, - location, downloader, download_dir, hashes=hashes, From e69d10637bee9d59a58f82b84897434d20eabfda Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 4 Feb 2020 21:56:40 -0500 Subject: [PATCH 1249/3170] Remove confusing comment from operations.prepare prepare_linked_requirements only handles non-editable requirements, so this comment seems like its been misplaced over several years of refactoring. --- src/pip/_internal/operations/prepare.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 0b61f20524d..fce764b31d3 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -416,10 +416,6 @@ def prepare_linked_requirement( logger.info('Collecting %s', req.req or 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 # Since source_dir is only set for editable requirements. assert req.source_dir is None req.ensure_has_source_dir(self.build_dir) From 63743fcc305f9830c4676489bceb365df60981f3 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 4 Feb 2020 23:01:24 -0500 Subject: [PATCH 1250/3170] Make ephem-wheel-cache tempdir name globally accessible --- src/pip/_internal/cache.py | 6 ++++-- src/pip/_internal/utils/temp_dir.py | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/cache.py b/src/pip/_internal/cache.py index abecd78f8d9..471e26f0363 100644 --- a/src/pip/_internal/cache.py +++ b/src/pip/_internal/cache.py @@ -15,7 +15,7 @@ from pip._internal.exceptions import InvalidWheelFilename from pip._internal.models.link import Link from pip._internal.models.wheel import Wheel -from pip._internal.utils.temp_dir import TempDirectory +from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.urls import path_to_url @@ -264,7 +264,9 @@ class EphemWheelCache(SimpleWheelCache): def __init__(self, format_control): # type: (FormatControl) -> None - self._temp_dir = TempDirectory(kind="ephem-wheel-cache") + self._temp_dir = TempDirectory( + kind=tempdir_kinds.EPHEM_WHEEL_CACHE + ) super(EphemWheelCache, self).__init__( self._temp_dir.path, format_control diff --git a/src/pip/_internal/utils/temp_dir.py b/src/pip/_internal/utils/temp_dir.py index 428173d8c96..201ba6d9811 100644 --- a/src/pip/_internal/utils/temp_dir.py +++ b/src/pip/_internal/utils/temp_dir.py @@ -25,6 +25,7 @@ # globally-managed. tempdir_kinds = enum( BUILD_ENV="build-env", + EPHEM_WHEEL_CACHE="ephem-wheel-cache", REQ_BUILD="req-build", ) From 34df623016d4051bbe91dc8badd829b07fb74919 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 4 Feb 2020 23:03:25 -0500 Subject: [PATCH 1251/3170] Globally manage EphemWheelCache temp directory --- src/pip/_internal/cache.py | 5 +++-- src/pip/_internal/cli/req_command.py | 6 +++++- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/cache.py b/src/pip/_internal/cache.py index 471e26f0363..c38994e3e51 100644 --- a/src/pip/_internal/cache.py +++ b/src/pip/_internal/cache.py @@ -265,7 +265,8 @@ class EphemWheelCache(SimpleWheelCache): def __init__(self, format_control): # type: (FormatControl) -> None self._temp_dir = TempDirectory( - kind=tempdir_kinds.EPHEM_WHEEL_CACHE + kind=tempdir_kinds.EPHEM_WHEEL_CACHE, + globally_managed=True, ) super(EphemWheelCache, self).__init__( @@ -274,7 +275,7 @@ def __init__(self, format_control): def cleanup(self): # type: () -> None - self._temp_dir.cleanup() + pass class WheelCache(Cache): diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 58fe49dc772..377e05a6378 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -156,7 +156,11 @@ def handle_pip_version_check(self, options): pip_self_version_check(session, options) -KEEPABLE_TEMPDIR_TYPES = [tempdir_kinds.BUILD_ENV, tempdir_kinds.REQ_BUILD] +KEEPABLE_TEMPDIR_TYPES = [ + tempdir_kinds.BUILD_ENV, + tempdir_kinds.EPHEM_WHEEL_CACHE, + tempdir_kinds.REQ_BUILD, +] def with_cleanup(func): From eead5790495bfedf590e0d3928e30e8904a7c94d Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 4 Feb 2020 23:04:57 -0500 Subject: [PATCH 1252/3170] Remove no-op calls to WheelCache.cleanup --- src/pip/_internal/cache.py | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/pip/_internal/cache.py b/src/pip/_internal/cache.py index c38994e3e51..8dbc6b0964a 100644 --- a/src/pip/_internal/cache.py +++ b/src/pip/_internal/cache.py @@ -273,10 +273,6 @@ def __init__(self, format_control): self._temp_dir.path, format_control ) - def cleanup(self): - # type: () -> None - pass - class WheelCache(Cache): """Wraps EphemWheelCache and SimpleWheelCache into a single Cache @@ -325,8 +321,3 @@ def get( package_name=package_name, supported_tags=supported_tags, ) - - def cleanup(self): - # type: () -> None - self._wheel_cache.cleanup() - self._ephem_cache.cleanup() From 77e9b79d0e73d4f7bd147c5a8fd6b5ff1038a89f Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 4 Feb 2020 23:06:02 -0500 Subject: [PATCH 1253/3170] Remove no-op calls to WheelCache.cleanup --- src/pip/_internal/commands/freeze.py | 7 ++----- src/pip/_internal/commands/install.py | 1 - src/pip/_internal/commands/wheel.py | 1 - 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/commands/freeze.py b/src/pip/_internal/commands/freeze.py index 41fea20ca5e..4758e30343f 100644 --- a/src/pip/_internal/commands/freeze.py +++ b/src/pip/_internal/commands/freeze.py @@ -96,8 +96,5 @@ def run(self, options, args): exclude_editable=options.exclude_editable, ) - try: - for line in freeze(**freeze_kwargs): - sys.stdout.write(line + '\n') - finally: - wheel_cache.cleanup() + for line in freeze(**freeze_kwargs): + sys.stdout.write(line + '\n') diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index cf629c81c0b..ce0aa9d86fa 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -447,7 +447,6 @@ def run(self, options, args): # Clean up if not options.no_clean: requirement_set.cleanup_files() - wheel_cache.cleanup() if options.target_dir: self._handle_target_dir( diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index d9308b152a4..35ba2d9c268 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -194,4 +194,3 @@ def run(self, options, args): finally: if not options.no_clean: requirement_set.cleanup_files() - wheel_cache.cleanup() From 0410535ef251f88c232f54d95db4b5daa2808b43 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 4 Feb 2020 23:07:37 -0500 Subject: [PATCH 1254/3170] Remove unused WheelCache.cleanup --- src/pip/_internal/cache.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/pip/_internal/cache.py b/src/pip/_internal/cache.py index 8dbc6b0964a..d4398ba8f88 100644 --- a/src/pip/_internal/cache.py +++ b/src/pip/_internal/cache.py @@ -171,10 +171,6 @@ def get( """ raise NotImplementedError() - def cleanup(self): - # type: () -> None - pass - class SimpleWheelCache(Cache): """A cache of wheels for future installs. From 87217fbf2e716e5802c6d3bddcc4e4c74c3842d2 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Wed, 5 Feb 2020 09:21:41 +0000 Subject: [PATCH 1255/3170] Refactor legacy_install to not take an install_req --- .../_internal/operations/install/legacy.py | 41 +++++++++---------- src/pip/_internal/req/req_install.py | 21 +++++++++- 2 files changed, 38 insertions(+), 24 deletions(-) diff --git a/src/pip/_internal/operations/install/legacy.py b/src/pip/_internal/operations/install/legacy.py index 2d4adc4f62c..4fe0098a734 100644 --- a/src/pip/_internal/operations/install/legacy.py +++ b/src/pip/_internal/operations/install/legacy.py @@ -16,15 +16,14 @@ if MYPY_CHECK_RUNNING: from typing import List, Optional, Sequence + from pip._internal.build_env import BuildEnvironment from pip._internal.models.scheme import Scheme - from pip._internal.req.req_install import InstallRequirement logger = logging.getLogger(__name__) def install( - install_req, # type: InstallRequirement install_options, # type: List[str] global_options, # type: Sequence[str] root, # type: Optional[str] @@ -33,24 +32,21 @@ def install( use_user_site, # type: bool pycompile, # type: bool scheme, # type: Scheme + setup_py_path, # type: str + isolated, # type: bool + req_name, # type: str + build_env, # type: BuildEnvironment + unpacked_source_directory, # type: str + req_description, # type: str ): - # type: (...) -> None - # Extend the list of global and install options passed on to - # the setup.py call with the ones from the requirements file. - # Options specified in requirements file override those - # specified on the command line, since the last option given - # to setup.py is the one that is used. - global_options = list(global_options) + \ - install_req.options.get('global_options', []) - install_options = list(install_options) + \ - install_req.options.get('install_options', []) + # type: (...) -> bool header_dir = scheme.headers with TempDirectory(kind="record") as temp_dir: record_filename = os.path.join(temp_dir.path, 'install-record.txt') install_args = make_setuptools_install_args( - install_req.setup_py_path, + setup_py_path, global_options=global_options, install_options=install_options, record_filename=record_filename, @@ -59,23 +55,22 @@ def install( header_dir=header_dir, home=home, use_user_site=use_user_site, - no_user_config=install_req.isolated, + no_user_config=isolated, pycompile=pycompile, ) runner = runner_with_spinner_message( - "Running setup.py install for {}".format(install_req.name) + "Running setup.py install for {}".format(req_name) ) - with indent_log(), install_req.build_env: + with indent_log(), build_env: runner( cmd=install_args, - cwd=install_req.unpacked_source_directory, + cwd=unpacked_source_directory, ) if not os.path.exists(record_filename): logger.debug('Record file %s not found', record_filename) - return - install_req.install_succeeded = True + return False # We intentionally do not use any encoding to read the file because # setuptools writes the file using distutils.file_util.write_file, @@ -101,19 +96,19 @@ def prepend_root(path): "{} did not indicate that it installed an " ".egg-info directory. Only setup.py projects " "generating .egg-info directories are supported." - ).format(install_req), + ).format(req_description), replacement=( "for maintainers: updating the setup.py of {0}. " "For users: contact the maintainers of {0} to let " "them know to update their setup.py.".format( - install_req.name + req_name ) ), gone_in="20.2", issue=6998, ) # FIXME: put the record somewhere - return + return True new_lines = [] for line in record_lines: filename = line.strip() @@ -127,3 +122,5 @@ def prepend_root(path): 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') + + return True diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 6088bacc1af..0fb020d766e 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -816,8 +816,19 @@ def install( self.install_succeeded = True return - install_legacy( - self, + # TODO: Why don't we do this for editable installs? + + # Extend the list of global and install options passed on to + # the setup.py call with the ones from the requirements file. + # Options specified in requirements file override those + # specified on the command line, since the last option given + # to setup.py is the one that is used. + global_options = list(global_options) + \ + self.options.get('global_options', []) + install_options = list(install_options) + \ + self.options.get('install_options', []) + + self.install_succeeded = install_legacy( install_options=install_options, global_options=global_options, root=root, @@ -826,4 +837,10 @@ def install( use_user_site=use_user_site, pycompile=pycompile, scheme=scheme, + setup_py_path=self.setup_py_path, + isolated=self.isolated, + req_name=self.name, + build_env=self.build_env, + unpacked_source_directory=self.unpacked_source_directory, + req_description=str(self.req), ) From c7cb3cd81d73739fd8179526b6a2e114a6f9e208 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 4 Feb 2020 22:04:38 -0500 Subject: [PATCH 1256/3170] Calculate autodelete_unpacked earlier in prepare_linked_requirement We want to use this value to determine whether a globally-managed source_dir should delegate choosing deletion to the global tempdir manager, so it needs to be above our call to InstallRequirement.ensure_has_source_dir. --- src/pip/_internal/operations/prepare.py | 37 +++++++++++++------------ 1 file changed, 19 insertions(+), 18 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 7d3aa69495b..3038cb5503d 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -416,6 +416,25 @@ def prepare_linked_requirement( else: logger.info('Collecting %s', req.req or req) + download_dir = self.download_dir + if 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 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 + else: + # We always delete unpacked sdists after pip runs. + autodelete_unpacked = True + with indent_log(): # @@ if filesystem packages are not marked # editable in a req, a non deterministic error @@ -471,12 +490,6 @@ def prepare_linked_requirement( # showing the user what the hash should be. hashes = MissingHashes() - download_dir = self.download_dir - if 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 - try: local_file = unpack_url( link, req.source_dir, self.downloader, download_dir, @@ -498,18 +511,6 @@ def prepare_linked_requirement( if local_file: req.local_file_path = local_file.path - if 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 - else: - # We always delete unpacked sdists after pip runs. - autodelete_unpacked = True if autodelete_unpacked: write_delete_marker_file(req.source_dir) From 470c39990e71f16cb75fb72c68b675564ea873d0 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 4 Feb 2020 22:13:23 -0500 Subject: [PATCH 1257/3170] Wrap InstallRequirement.ensure_build_location in TempDirectory Since we explicitly disable deletion this is a no-op, but we'll parameterize the deletion soon. --- src/pip/_internal/req/req_install.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 6088bacc1af..4c58ac0167c 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -372,7 +372,13 @@ def ensure_build_location(self, build_dir): if not os.path.exists(build_dir): logger.debug('Creating directory %s', build_dir) os.makedirs(build_dir) - return os.path.join(build_dir, name) + actual_build_dir = os.path.join(build_dir, name) + return TempDirectory( + path=actual_build_dir, + delete=False, + kind=tempdir_kinds.REQ_BUILD, + globally_managed=True, + ).path def _set_requirement(self): # type: () -> None From 8197a4bbc553258e0b78bb8506520b047b5b1fa9 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 4 Feb 2020 22:26:11 -0500 Subject: [PATCH 1258/3170] Do not write delete marker file to track source_dir delete preference Previously we were writing a delete marker file which is checked in InstallRequirement.remove_temporary_source which is only invoked if the user did not pass --no-clean (and a PreviousBuildDirError was not raised). Since our TempDirectory machinery now respects these conditions we can just wrap our source directory in that instead of using this ad-hoc mechanism for tracking our delete preference. This will let us clean up a lot of dead code that only existed for this use case. --- src/pip/_internal/operations/prepare.py | 6 +----- src/pip/_internal/req/req_install.py | 17 +++++++++++------ tests/unit/test_req_install.py | 4 +++- 3 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 3038cb5503d..f32517e50c7 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -28,7 +28,6 @@ from pip._internal.utils.filesystem import copy2_fixed from pip._internal.utils.hashes import MissingHashes from pip._internal.utils.logging import indent_log -from pip._internal.utils.marker_files import write_delete_marker_file from pip._internal.utils.misc import ( ask_path_exists, backup_dir, @@ -442,7 +441,7 @@ def prepare_linked_requirement( # build directory # Since source_dir is only set for editable requirements. assert req.source_dir is None - req.ensure_has_source_dir(self.build_dir) + req.ensure_has_source_dir(self.build_dir, autodelete_unpacked) # If a checkout exists, it's unwise to keep going. version # inconsistencies are logged later, but do not fail the # installation. @@ -511,9 +510,6 @@ def prepare_linked_requirement( if local_file: req.local_file_path = local_file.path - if autodelete_unpacked: - write_delete_marker_file(req.source_dir) - abstract_dist = _get_prepared_distribution( req, self.req_tracker, self.finder, self.build_isolation, ) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 4c58ac0167c..b8aadb5cd0a 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -348,8 +348,8 @@ def from_path(self): s += '->' + comes_from return s - def ensure_build_location(self, build_dir): - # type: (str) -> str + def ensure_build_location(self, build_dir, autodelete): + # type: (str, bool) -> str assert build_dir is not None if self._temp_build_dir is not None: assert self._temp_build_dir.path @@ -373,9 +373,12 @@ def ensure_build_location(self, build_dir): logger.debug('Creating directory %s', build_dir) os.makedirs(build_dir) actual_build_dir = os.path.join(build_dir, name) + # `None` indicates that we respect the globally-configured deletion + # settings, which is what we actually want when auto-deleting. + delete_arg = None if autodelete else False return TempDirectory( path=actual_build_dir, - delete=False, + delete=delete_arg, kind=tempdir_kinds.REQ_BUILD, globally_managed=True, ).path @@ -605,8 +608,8 @@ def assert_source_matches_version(self): ) # For both source distributions and editables - def ensure_has_source_dir(self, parent_dir): - # type: (str) -> None + def ensure_has_source_dir(self, parent_dir, autodelete=False): + # type: (str, bool) -> None """Ensure that a source_dir is set. This will create a temporary build dir if the name of the requirement @@ -617,7 +620,9 @@ def ensure_has_source_dir(self, parent_dir): :return: self.source_dir """ if self.source_dir is None: - self.source_dir = self.ensure_build_location(parent_dir) + self.source_dir = self.ensure_build_location( + parent_dir, autodelete + ) # For editable installations def update_editable(self, obtain=True): diff --git a/tests/unit/test_req_install.py b/tests/unit/test_req_install.py index a8eae8249bb..c3482d5360a 100644 --- a/tests/unit/test_req_install.py +++ b/tests/unit/test_req_install.py @@ -20,7 +20,9 @@ def test_tmp_build_directory(self): # Make sure we're handling it correctly with real path. requirement = InstallRequirement(None, None) tmp_dir = tempfile.mkdtemp('-build', 'pip-') - tmp_build_dir = requirement.ensure_build_location(tmp_dir) + tmp_build_dir = requirement.ensure_build_location( + tmp_dir, autodelete=False + ) assert ( os.path.dirname(tmp_build_dir) == os.path.realpath(os.path.dirname(tmp_dir)) From e6cc9e9351d66a3dc27dc2f0e31167d88162d762 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 4 Feb 2020 22:30:05 -0500 Subject: [PATCH 1259/3170] Do not remove source directory in cleanup_temporary_source Since nothing in our code writes the delete marker file, this block will never execute. --- src/pip/_internal/req/req_install.py | 11 +---------- 1 file changed, 1 insertion(+), 10 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index b8aadb5cd0a..3bf879ce5f0 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -33,10 +33,7 @@ from pip._internal.utils.deprecation import deprecated from pip._internal.utils.hashes import Hashes from pip._internal.utils.logging import indent_log -from pip._internal.utils.marker_files import ( - PIP_DELETE_MARKER_FILENAME, - has_delete_marker_file, -) +from pip._internal.utils.marker_files import PIP_DELETE_MARKER_FILENAME from pip._internal.utils.misc import ( ask_path_exists, backup_dir, @@ -46,7 +43,6 @@ get_installed_version, hide_url, redact_auth_from_url, - rmtree, ) from pip._internal.utils.packaging import get_metadata from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds @@ -423,11 +419,6 @@ def warn_on_mismatching_name(self): def remove_temporary_source(self): # type: () -> None - """Remove the source files from this requirement, if they are marked - for deletion""" - if self.source_dir and has_delete_marker_file(self.source_dir): - logger.debug('Removing source in %s', self.source_dir) - rmtree(self.source_dir) self.source_dir = None self._temp_build_dir = None From 98cd1937278c49269e5bc71896f7d8a958e2a004 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 4 Feb 2020 22:30:59 -0500 Subject: [PATCH 1260/3170] Remove delete_marker_file writing in tests Nothing checks for this file, so no need to write it. --- tests/functional/test_install_cleanup.py | 2 -- tests/functional/test_wheel.py | 2 -- 2 files changed, 4 deletions(-) diff --git a/tests/functional/test_install_cleanup.py b/tests/functional/test_install_cleanup.py index 8810402f416..b07c809079b 100644 --- a/tests/functional/test_install_cleanup.py +++ b/tests/functional/test_install_cleanup.py @@ -4,7 +4,6 @@ import pytest from pip._internal.cli.status_codes import PREVIOUS_BUILD_DIR_ERROR -from pip._internal.utils.marker_files import write_delete_marker_file from tests.lib import need_mercurial, windows_workaround_7667 from tests.lib.local_repos import local_checkout @@ -126,7 +125,6 @@ def test_cleanup_prevented_upon_build_dir_exception(script, data): build = script.venv_path / 'build' build_simple = build / 'simple' os.makedirs(build_simple) - write_delete_marker_file(build_simple) build_simple.joinpath("setup.py").write_text("#") result = script.pip( 'install', '-f', data.find_links, '--no-index', 'simple', diff --git a/tests/functional/test_wheel.py b/tests/functional/test_wheel.py index 792f5fe6c10..181ca05845b 100644 --- a/tests/functional/test_wheel.py +++ b/tests/functional/test_wheel.py @@ -6,7 +6,6 @@ import pytest from pip._internal.cli.status_codes import ERROR, PREVIOUS_BUILD_DIR_ERROR -from pip._internal.utils.marker_files import write_delete_marker_file from tests.lib import pyversion @@ -225,7 +224,6 @@ def test_pip_wheel_fail_cause_of_previous_build_dir(script, data): # Given that I have a previous build dir of the `simple` package build = script.venv_path / 'build' / 'simple' os.makedirs(build) - write_delete_marker_file(script.venv_path / 'build' / 'simple') build.joinpath('setup.py').write_text('#') # When I call pip trying to install things again From efe663d476132f2f1668079dd647469d44a8032b Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 4 Feb 2020 22:32:37 -0500 Subject: [PATCH 1261/3170] Remove unused utils.marker_files --- src/pip/_internal/req/req_install.py | 3 --- src/pip/_internal/utils/marker_files.py | 25 ------------------------- 2 files changed, 28 deletions(-) delete mode 100644 src/pip/_internal/utils/marker_files.py diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 3bf879ce5f0..dc2a6cd94a2 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -33,7 +33,6 @@ from pip._internal.utils.deprecation import deprecated from pip._internal.utils.hashes import Hashes from pip._internal.utils.logging import indent_log -from pip._internal.utils.marker_files import PIP_DELETE_MARKER_FILENAME from pip._internal.utils.misc import ( ask_path_exists, backup_dir, @@ -757,8 +756,6 @@ def archive(self, build_dir): zipdir.external_attr = 0x1ED << 16 # 0o755 zip_output.writestr(zipdir, '') for filename in filenames: - if filename == PIP_DELETE_MARKER_FILENAME: - continue file_arcname = self._get_archive_name( filename, parentdir=dirpath, rootdir=dir, ) diff --git a/src/pip/_internal/utils/marker_files.py b/src/pip/_internal/utils/marker_files.py deleted file mode 100644 index 42ea8140508..00000000000 --- a/src/pip/_internal/utils/marker_files.py +++ /dev/null @@ -1,25 +0,0 @@ -import os.path - -DELETE_MARKER_MESSAGE = '''\ -This file is placed here by pip to indicate the source was put -here by pip. - -Once this package is successfully installed this source code will be -deleted (unless you remove this file). -''' -PIP_DELETE_MARKER_FILENAME = 'pip-delete-this-directory.txt' - - -def has_delete_marker_file(directory): - # type: (str) -> bool - return os.path.exists(os.path.join(directory, PIP_DELETE_MARKER_FILENAME)) - - -def write_delete_marker_file(directory): - # type: (str) -> None - """ - Write the pip delete marker file into this directory. - """ - filepath = os.path.join(directory, PIP_DELETE_MARKER_FILENAME) - with open(filepath, 'w') as marker_fp: - marker_fp.write(DELETE_MARKER_MESSAGE) From 5cca8f10b304a5a7f3a96dfd66937615324cf826 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 4 Feb 2020 22:34:29 -0500 Subject: [PATCH 1262/3170] Remove InstallRequirement.remove_temporary_source Since all directories are now globally-managed, we don't need to be concerned with resetting the member values. This will also let us remove several responsibilities from RequirementSet, which will make integrating the new resolver easier. --- src/pip/_internal/req/req_install.py | 5 ----- src/pip/_internal/req/req_set.py | 7 +------ 2 files changed, 1 insertion(+), 11 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index dc2a6cd94a2..b23b2268a54 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -416,11 +416,6 @@ def warn_on_mismatching_name(self): ) self.req = Requirement(metadata_name) - def remove_temporary_source(self): - # type: () -> None - self.source_dir = None - self._temp_build_dir = None - def check_if_exists(self, use_user_site): # type: (bool) -> None """Find an installed distribution that satisfies or conflicts diff --git a/src/pip/_internal/req/req_set.py b/src/pip/_internal/req/req_set.py index 1312622b87b..fc1f3ec4927 100644 --- a/src/pip/_internal/req/req_set.py +++ b/src/pip/_internal/req/req_set.py @@ -11,7 +11,6 @@ from pip._internal import pep425tags from pip._internal.exceptions import InstallationError from pip._internal.models.wheel import Wheel -from pip._internal.utils.logging import indent_log from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: @@ -202,8 +201,4 @@ def get_requirement(self, name): def cleanup_files(self): # type: () -> None - """Clean up files, remove builds.""" - logger.debug('Cleaning up...') - with indent_log(): - for req in self.reqs_to_cleanup: - req.remove_temporary_source() + pass From 4a93045be187f77b4de7c17d03045fd7ec7f341d Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 4 Feb 2020 22:37:14 -0500 Subject: [PATCH 1263/3170] Remove no-op RequirementSet.cleanup_files --- src/pip/_internal/commands/download.py | 4 ---- src/pip/_internal/commands/install.py | 4 ---- src/pip/_internal/commands/wheel.py | 3 --- src/pip/_internal/req/req_set.py | 4 ---- 4 files changed, 15 deletions(-) diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index d1c59990a83..9c82c419646 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -140,8 +140,4 @@ def run(self, options, args): if downloaded: write_output('Successfully downloaded %s', downloaded) - # Clean up - if not options.no_clean: - requirement_set.cleanup_files() - return requirement_set diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index ce0aa9d86fa..95c90499be0 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -443,10 +443,6 @@ def run(self, options, args): except PreviousBuildDirError: options.no_clean = True raise - finally: - # Clean up - if not options.no_clean: - requirement_set.cleanup_files() if options.target_dir: self._handle_target_dir( diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index 35ba2d9c268..8c03c6b82e4 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -191,6 +191,3 @@ def run(self, options, args): except PreviousBuildDirError: options.no_clean = True raise - finally: - if not options.no_clean: - requirement_set.cleanup_files() diff --git a/src/pip/_internal/req/req_set.py b/src/pip/_internal/req/req_set.py index fc1f3ec4927..15d10007349 100644 --- a/src/pip/_internal/req/req_set.py +++ b/src/pip/_internal/req/req_set.py @@ -198,7 +198,3 @@ def get_requirement(self, name): return self.requirements[project_name] raise KeyError("No project with the name %r" % name) - - def cleanup_files(self): - # type: () -> None - pass From 85ab574dc1e61ad6ec42ab34643a7de48b08a3b8 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 4 Feb 2020 22:38:47 -0500 Subject: [PATCH 1264/3170] Remove unused RequirementSet.reqs_to_cleanup --- src/pip/_internal/legacy_resolve.py | 3 --- src/pip/_internal/req/req_set.py | 2 -- 2 files changed, 5 deletions(-) diff --git a/src/pip/_internal/legacy_resolve.py b/src/pip/_internal/legacy_resolve.py index ca269121b60..d05a4c063a0 100644 --- a/src/pip/_internal/legacy_resolve.py +++ b/src/pip/_internal/legacy_resolve.py @@ -327,9 +327,6 @@ def _resolve_one( 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 diff --git a/src/pip/_internal/req/req_set.py b/src/pip/_internal/req/req_set.py index 15d10007349..df5fbee08dc 100644 --- a/src/pip/_internal/req/req_set.py +++ b/src/pip/_internal/req/req_set.py @@ -33,7 +33,6 @@ def __init__(self, check_supported_wheels=True): self.unnamed_requirements = [] # type: List[InstallRequirement] self.successfully_downloaded = [] # type: List[InstallRequirement] - self.reqs_to_cleanup = [] # type: List[InstallRequirement] def __str__(self): # type: () -> str @@ -161,7 +160,6 @@ def add_requirement( ) ) if does_not_satisfy_constraint: - self.reqs_to_cleanup.append(install_req) raise InstallationError( "Could not satisfy constraints for '{}': " "installation from path or url cannot be " From 441b211048f5c16b104ef4dce22f2728901d063e Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 5 Feb 2020 21:08:58 -0500 Subject: [PATCH 1265/3170] Remove old comment from Resolver.resolve This was moved to RequirementCommand.populate_requirement_set, so it's no longer applicable in this context. --- src/pip/_internal/legacy_resolve.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/pip/_internal/legacy_resolve.py b/src/pip/_internal/legacy_resolve.py index d05a4c063a0..1107f703e87 100644 --- a/src/pip/_internal/legacy_resolve.py +++ b/src/pip/_internal/legacy_resolve.py @@ -159,8 +159,6 @@ def resolve(self, requirement_set): possible to move the preparation to become a step separated from dependency resolution. """ - # 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()) From 58c06299db4c7b9fbfbfbc44cc639b0a4e578c12 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 5 Feb 2020 21:17:22 -0500 Subject: [PATCH 1266/3170] Return a different RequirementSet from Resolver.resolve This makes the resolver interface simpler by returning a brand new RequirementSet vs mutating the one that was input to the function, and will let us specialize RequirementSet for the different use cases. --- src/pip/_internal/commands/download.py | 2 +- src/pip/_internal/commands/install.py | 2 +- src/pip/_internal/commands/wheel.py | 2 +- src/pip/_internal/legacy_resolve.py | 12 ++++++++++-- tests/unit/test_req.py | 2 +- 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index 9c82c419646..13e6a24e9e1 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -132,7 +132,7 @@ def run(self, options, args): self.trace_basic_info(finder) - resolver.resolve(requirement_set) + requirement_set = resolver.resolve(requirement_set) downloaded = ' '.join([ req.name for req in requirement_set.successfully_downloaded diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 95c90499be0..5c496660882 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -328,7 +328,7 @@ def run(self, options, args): self.trace_basic_info(finder) - resolver.resolve(requirement_set) + requirement_set = resolver.resolve(requirement_set) try: pip_req = requirement_set.get_requirement("pip") diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index 8c03c6b82e4..8e64c97f1b1 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -158,7 +158,7 @@ def run(self, options, args): self.trace_basic_info(finder) - resolver.resolve(requirement_set) + requirement_set = resolver.resolve(requirement_set) reqs_to_build = [ r for r in requirement_set.requirements.values() diff --git a/src/pip/_internal/legacy_resolve.py b/src/pip/_internal/legacy_resolve.py index 1107f703e87..cfd538a01c3 100644 --- a/src/pip/_internal/legacy_resolve.py +++ b/src/pip/_internal/legacy_resolve.py @@ -28,6 +28,7 @@ HashErrors, UnsupportedPythonVersion, ) +from pip._internal.req.req_set import RequirementSet from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import dist_in_usersite, normalize_version_info from pip._internal.utils.packaging import ( @@ -44,7 +45,6 @@ from pip._internal.index.package_finder import PackageFinder from pip._internal.operations.prepare import RequirementPreparer from pip._internal.req.req_install import InstallRequirement - from pip._internal.req.req_set import RequirementSet InstallRequirementProvider = Callable[ [str, InstallRequirement], InstallRequirement @@ -148,7 +148,7 @@ def __init__( defaultdict(list) # type: DiscoveredDependencies def resolve(self, requirement_set): - # type: (RequirementSet) -> None + # type: (RequirementSet) -> RequirementSet """Resolve what operations need to be done As a side-effect of this method, the packages (and their dependencies) @@ -163,6 +163,12 @@ def resolve(self, requirement_set): requirement_set.unnamed_requirements + list(requirement_set.requirements.values()) ) + check_supported_wheels = requirement_set.check_supported_wheels + requirement_set = RequirementSet( + check_supported_wheels=check_supported_wheels + ) + for req in root_reqs: + requirement_set.add_requirement(req) # Actually prepare the files, and collect any exceptions. Most hash # exceptions cannot be checked ahead of time, because @@ -180,6 +186,8 @@ def resolve(self, requirement_set): if hash_errors: raise hash_errors + return requirement_set + def _is_upgrade_allowed(self, req): # type: (InstallRequirement) -> bool if self.upgrade_strategy == "to-satisfy-only": diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index 0d5790831c6..ee0b6a5d6b7 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -129,7 +129,7 @@ def test_environment_marker_extras(self, data): reqset.add_requirement(req) finder = make_test_finder(find_links=[data.find_links]) with self._basic_resolver(finder) as resolver: - resolver.resolve(reqset) + reqset = resolver.resolve(reqset) # This is hacky but does test both case in py2 and py3 if sys.version_info[:2] == (2, 7): assert reqset.has_requirement('simple') From 550ae907bd711c308c040efd2d50a033f7d41c31 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 5 Feb 2020 21:21:52 -0500 Subject: [PATCH 1267/3170] Parameterize check_supported_wheels to Resolver.resolve This reduces our dependence on the input RequirementSet. --- src/pip/_internal/commands/download.py | 4 +++- src/pip/_internal/commands/install.py | 4 +++- src/pip/_internal/commands/wheel.py | 4 +++- src/pip/_internal/legacy_resolve.py | 5 ++--- tests/unit/test_req.py | 10 ++++++++-- 5 files changed, 19 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index 13e6a24e9e1..37ea918d70f 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -132,7 +132,9 @@ def run(self, options, args): self.trace_basic_info(finder) - requirement_set = resolver.resolve(requirement_set) + requirement_set = resolver.resolve( + requirement_set, requirement_set.check_supported_wheels + ) downloaded = ' '.join([ req.name for req in requirement_set.successfully_downloaded diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 5c496660882..c4cad913912 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -328,7 +328,9 @@ def run(self, options, args): self.trace_basic_info(finder) - requirement_set = resolver.resolve(requirement_set) + requirement_set = resolver.resolve( + requirement_set, requirement_set.check_supported_wheels + ) try: pip_req = requirement_set.get_requirement("pip") diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index 8e64c97f1b1..0f1474bf515 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -158,7 +158,9 @@ def run(self, options, args): self.trace_basic_info(finder) - requirement_set = resolver.resolve(requirement_set) + requirement_set = resolver.resolve( + requirement_set, requirement_set.check_supported_wheels + ) reqs_to_build = [ r for r in requirement_set.requirements.values() diff --git a/src/pip/_internal/legacy_resolve.py b/src/pip/_internal/legacy_resolve.py index cfd538a01c3..3e53c3af9b9 100644 --- a/src/pip/_internal/legacy_resolve.py +++ b/src/pip/_internal/legacy_resolve.py @@ -147,8 +147,8 @@ def __init__( self._discovered_dependencies = \ defaultdict(list) # type: DiscoveredDependencies - def resolve(self, requirement_set): - # type: (RequirementSet) -> RequirementSet + def resolve(self, requirement_set, check_supported_wheels): + # type: (RequirementSet, bool) -> RequirementSet """Resolve what operations need to be done As a side-effect of this method, the packages (and their dependencies) @@ -163,7 +163,6 @@ def resolve(self, requirement_set): requirement_set.unnamed_requirements + list(requirement_set.requirements.values()) ) - check_supported_wheels = requirement_set.check_supported_wheels requirement_set = RequirementSet( check_supported_wheels=check_supported_wheels ) diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index ee0b6a5d6b7..9392096fd36 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -113,6 +113,7 @@ def test_no_reuse_existing_build_dir(self, data): (req, build_dir.replace('\\', '\\\\')), resolver.resolve, reqset, + True, ) # TODO: Update test when Python 2.7 is dropped. @@ -129,7 +130,7 @@ def test_environment_marker_extras(self, data): reqset.add_requirement(req) finder = make_test_finder(find_links=[data.find_links]) with self._basic_resolver(finder) as resolver: - reqset = resolver.resolve(reqset) + reqset = resolver.resolve(reqset, True) # This is hacky but does test both case in py2 and py3 if sys.version_info[:2] == (2, 7): assert reqset.has_requirement('simple') @@ -155,7 +156,8 @@ def test_missing_hash_with_require_hashes(self, data): r' simple==1.0 --hash=sha256:393043e672415891885c9a2a0929b1' r'af95fb866d6ca016b42d2e6ce53619b653$', resolver.resolve, - reqset + reqset, + True, ) def test_missing_hash_with_require_hashes_in_reqs_file(self, data, tmpdir): @@ -210,6 +212,7 @@ def test_unsupported_hashes(self, data): r"\(from -r file \(line 2\)\)".format(sep=sep), resolver.resolve, reqset, + True, ) def test_unpinned_hash_checking(self, data): @@ -238,6 +241,7 @@ def test_unpinned_hash_checking(self, data): r' simple2>1.0 .* \(from -r file \(line 2\)\)', resolver.resolve, reqset, + True, ) def test_hash_mismatch(self, data): @@ -259,6 +263,7 @@ def test_hash_mismatch(self, data): r'866d6ca016b42d2e6ce53619b653$', resolver.resolve, reqset, + True, ) def test_unhashed_deps_on_require_hashes(self, data): @@ -281,6 +286,7 @@ def test_unhashed_deps_on_require_hashes(self, data): r' TopoRequires from .*$', resolver.resolve, reqset, + True, ) def test_hashed_deps_on_require_hashes(self): From a814dc9f1f76294f1e8a5f059dae0b244c111650 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 5 Feb 2020 21:21:53 -0500 Subject: [PATCH 1268/3170] Centralize logic for getting 'all requirements' --- src/pip/_internal/cli/req_command.py | 5 +---- src/pip/_internal/commands/install.py | 5 +---- src/pip/_internal/legacy_resolve.py | 5 +---- src/pip/_internal/req/req_set.py | 5 +++++ 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 377e05a6378..aa708723947 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -320,10 +320,7 @@ def populate_requirement_set( requirement_set.add_requirement(req_to_add) # If any requirement has hash options, enable hash checking. - requirements = ( - requirement_set.unnamed_requirements + - list(requirement_set.requirements.values()) - ) + requirements = requirement_set.all_requirements if any(req.has_hash_options for req in requirements): options.require_hashes = True diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index c4cad913912..f25efb455fc 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -616,10 +616,7 @@ def format_options(option_names): # type: (Iterable[str]) -> List[str] return ["--{}".format(name.replace("_", "-")) for name in option_names] - requirements = ( - requirement_set.unnamed_requirements + - list(requirement_set.requirements.values()) - ) + requirements = requirement_set.all_requirements offenders = [] diff --git a/src/pip/_internal/legacy_resolve.py b/src/pip/_internal/legacy_resolve.py index 3e53c3af9b9..d386bd9bac5 100644 --- a/src/pip/_internal/legacy_resolve.py +++ b/src/pip/_internal/legacy_resolve.py @@ -159,10 +159,7 @@ def resolve(self, requirement_set, check_supported_wheels): possible to move the preparation to become a step separated from dependency resolution. """ - root_reqs = ( - requirement_set.unnamed_requirements + - list(requirement_set.requirements.values()) - ) + root_reqs = requirement_set.all_requirements requirement_set = RequirementSet( check_supported_wheels=check_supported_wheels ) diff --git a/src/pip/_internal/req/req_set.py b/src/pip/_internal/req/req_set.py index df5fbee08dc..56b731c46dd 100644 --- a/src/pip/_internal/req/req_set.py +++ b/src/pip/_internal/req/req_set.py @@ -196,3 +196,8 @@ def get_requirement(self, name): return self.requirements[project_name] raise KeyError("No project with the name %r" % name) + + @property + def all_requirements(self): + # type: () -> List[InstallRequirement] + return self.unnamed_requirements + list(self.requirements.values()) From fd815bc1fa6b9282b474976897f42152cbe8c707 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 5 Feb 2020 21:24:23 -0500 Subject: [PATCH 1269/3170] Pass in a plain list of InstallRequirement to Resolver.resolve Further simplifies the Resolver interface, and will give us the opportunity to remove any knowledge of RequirementSet from the individual commands. --- src/pip/_internal/commands/download.py | 3 ++- src/pip/_internal/commands/install.py | 3 ++- src/pip/_internal/commands/wheel.py | 3 ++- src/pip/_internal/legacy_resolve.py | 5 ++--- tests/unit/test_req.py | 14 +++++++------- 5 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index 37ea918d70f..aef684ed8bf 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -133,7 +133,8 @@ def run(self, options, args): self.trace_basic_info(finder) requirement_set = resolver.resolve( - requirement_set, requirement_set.check_supported_wheels + requirement_set.all_requirements, + requirement_set.check_supported_wheels, ) downloaded = ' '.join([ diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index f25efb455fc..1cba1adb41e 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -329,7 +329,8 @@ def run(self, options, args): self.trace_basic_info(finder) requirement_set = resolver.resolve( - requirement_set, requirement_set.check_supported_wheels + requirement_set.all_requirements, + requirement_set.check_supported_wheels, ) try: diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index 0f1474bf515..12c768e256e 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -159,7 +159,8 @@ def run(self, options, args): self.trace_basic_info(finder) requirement_set = resolver.resolve( - requirement_set, requirement_set.check_supported_wheels + requirement_set.all_requirements, + requirement_set.check_supported_wheels, ) reqs_to_build = [ diff --git a/src/pip/_internal/legacy_resolve.py b/src/pip/_internal/legacy_resolve.py index d386bd9bac5..8e603ed089c 100644 --- a/src/pip/_internal/legacy_resolve.py +++ b/src/pip/_internal/legacy_resolve.py @@ -147,8 +147,8 @@ def __init__( self._discovered_dependencies = \ defaultdict(list) # type: DiscoveredDependencies - def resolve(self, requirement_set, check_supported_wheels): - # type: (RequirementSet, bool) -> RequirementSet + def resolve(self, root_reqs, check_supported_wheels): + # type: (List[InstallRequirement], bool) -> RequirementSet """Resolve what operations need to be done As a side-effect of this method, the packages (and their dependencies) @@ -159,7 +159,6 @@ def resolve(self, requirement_set, check_supported_wheels): possible to move the preparation to become a step separated from dependency resolution. """ - root_reqs = requirement_set.all_requirements requirement_set = RequirementSet( check_supported_wheels=check_supported_wheels ) diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index 9392096fd36..9b1d480cf1c 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -112,7 +112,7 @@ def test_no_reuse_existing_build_dir(self, data): r"pip can't proceed with [\s\S]*%s[\s\S]*%s" % (req, build_dir.replace('\\', '\\\\')), resolver.resolve, - reqset, + reqset.all_requirements, True, ) @@ -130,7 +130,7 @@ def test_environment_marker_extras(self, data): reqset.add_requirement(req) finder = make_test_finder(find_links=[data.find_links]) with self._basic_resolver(finder) as resolver: - reqset = resolver.resolve(reqset, True) + reqset = resolver.resolve(reqset.all_requirements, True) # This is hacky but does test both case in py2 and py3 if sys.version_info[:2] == (2, 7): assert reqset.has_requirement('simple') @@ -156,7 +156,7 @@ def test_missing_hash_with_require_hashes(self, data): r' simple==1.0 --hash=sha256:393043e672415891885c9a2a0929b1' r'af95fb866d6ca016b42d2e6ce53619b653$', resolver.resolve, - reqset, + reqset.all_requirements, True, ) @@ -211,7 +211,7 @@ def test_unsupported_hashes(self, data): r" file://.*{sep}data{sep}packages{sep}FSPkg " r"\(from -r file \(line 2\)\)".format(sep=sep), resolver.resolve, - reqset, + reqset.all_requirements, True, ) @@ -240,7 +240,7 @@ def test_unpinned_hash_checking(self, data): r' simple .* \(from -r file \(line 1\)\)\n' r' simple2>1.0 .* \(from -r file \(line 2\)\)', resolver.resolve, - reqset, + reqset.all_requirements, True, ) @@ -262,7 +262,7 @@ def test_hash_mismatch(self, data): r' Got 393043e672415891885c9a2a0929b1af95fb' r'866d6ca016b42d2e6ce53619b653$', resolver.resolve, - reqset, + reqset.all_requirements, True, ) @@ -285,7 +285,7 @@ def test_unhashed_deps_on_require_hashes(self, data): r'versions pinned.*\n' r' TopoRequires from .*$', resolver.resolve, - reqset, + reqset.all_requirements, True, ) From 5dcc5626164e406f1005edafc91c91532e512b87 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 5 Feb 2020 21:29:18 -0500 Subject: [PATCH 1270/3170] Return a list of InstallRequirement from populate_requirement_set Next we can hide RequirementSet from the setup phase of the individual commands. --- src/pip/_internal/cli/req_command.py | 5 ++++- src/pip/_internal/commands/download.py | 5 ++--- src/pip/_internal/commands/install.py | 5 ++--- src/pip/_internal/commands/wheel.py | 5 ++--- 4 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index aa708723947..87a10bc92ae 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -38,6 +38,7 @@ from pip._internal.cache import WheelCache from pip._internal.models.target_python import TargetPython + from pip._internal.req.req_install import InstallRequirement from pip._internal.req.req_set import RequirementSet from pip._internal.req.req_tracker import RequirementTracker from pip._internal.utils.temp_dir import ( @@ -278,7 +279,7 @@ def populate_requirement_set( session, # type: PipSession wheel_cache, # type: Optional[WheelCache] ): - # type: (...) -> None + # type: (...) -> List[InstallRequirement] """ Marshal cmd line args into a requirement set. """ @@ -336,6 +337,8 @@ def populate_requirement_set( 'You must give at least one requirement to %(name)s ' '(see "pip help %(name)s")' % opts) + return requirements + @staticmethod def trace_basic_info(finder): # type: (PackageFinder) -> None diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index aef684ed8bf..29e8b2ae920 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -104,7 +104,7 @@ def run(self, options, args): ) as directory: requirement_set = RequirementSet() - self.populate_requirement_set( + reqs = self.populate_requirement_set( requirement_set, args, options, @@ -133,8 +133,7 @@ def run(self, options, args): self.trace_basic_info(finder) requirement_set = resolver.resolve( - requirement_set.all_requirements, - requirement_set.check_supported_wheels, + reqs, requirement_set.check_supported_wheels ) downloaded = ' '.join([ diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 1cba1adb41e..3a141c05313 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -296,7 +296,7 @@ def run(self, options, args): ) try: - self.populate_requirement_set( + reqs = self.populate_requirement_set( requirement_set, args, options, finder, session, wheel_cache ) @@ -329,8 +329,7 @@ def run(self, options, args): self.trace_basic_info(finder) requirement_set = resolver.resolve( - requirement_set.all_requirements, - requirement_set.check_supported_wheels, + reqs, requirement_set.check_supported_wheels ) try: diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index 12c768e256e..b7996434fde 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -132,7 +132,7 @@ def run(self, options, args): requirement_set = RequirementSet() try: - self.populate_requirement_set( + reqs = self.populate_requirement_set( requirement_set, args, options, finder, session, wheel_cache ) @@ -159,8 +159,7 @@ def run(self, options, args): self.trace_basic_info(finder) requirement_set = resolver.resolve( - requirement_set.all_requirements, - requirement_set.check_supported_wheels, + reqs, requirement_set.check_supported_wheels ) reqs_to_build = [ From 779d8df5e63767030bba34edd6c83473966fc895 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 5 Feb 2020 21:33:14 -0500 Subject: [PATCH 1271/3170] Inline check_supported_wheels Reduces our dependency on RequirementSet in individual commands. The default value for this is True, used everywhere except InstallCommand. --- src/pip/_internal/commands/download.py | 2 +- src/pip/_internal/commands/install.py | 2 +- src/pip/_internal/commands/wheel.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index 29e8b2ae920..0627bb9862d 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -133,7 +133,7 @@ def run(self, options, args): self.trace_basic_info(finder) requirement_set = resolver.resolve( - reqs, requirement_set.check_supported_wheels + reqs, check_supported_wheels=True ) downloaded = ' '.join([ diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 3a141c05313..01829e5e855 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -329,7 +329,7 @@ def run(self, options, args): self.trace_basic_info(finder) requirement_set = resolver.resolve( - reqs, requirement_set.check_supported_wheels + reqs, check_supported_wheels=not options.target_dir ) try: diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index b7996434fde..c914844be1f 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -159,7 +159,7 @@ def run(self, options, args): self.trace_basic_info(finder) requirement_set = resolver.resolve( - reqs, requirement_set.check_supported_wheels + reqs, check_supported_wheels=True ) reqs_to_build = [ From 9c58aa7eb5068b06981efdb9b993221aefbb488a Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 5 Feb 2020 21:39:10 -0500 Subject: [PATCH 1272/3170] Do not use InstallRequirement in warn_deprecated_install_options This only needed a list of requirements, so give it just that. --- src/pip/_internal/commands/install.py | 8 +++----- tests/unit/test_command_install.py | 11 ++++------- 2 files changed, 7 insertions(+), 12 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 01829e5e855..067507c6967 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -302,7 +302,7 @@ def run(self, options, args): ) warn_deprecated_install_options( - requirement_set, options.install_options + reqs, options.install_options ) preparer = self.make_requirement_preparer( @@ -607,8 +607,8 @@ def decide_user_install( return True -def warn_deprecated_install_options(requirement_set, options): - # type: (RequirementSet, Optional[List[str]]) -> None +def warn_deprecated_install_options(requirements, options): + # type: (List[InstallRequirement], Optional[List[str]]) -> None """If any location-changing --install-option arguments were passed for requirements or on the command-line, then show a deprecation warning. """ @@ -616,8 +616,6 @@ def format_options(option_names): # type: (Iterable[str]) -> List[str] return ["--{}".format(name.replace("_", "-")) for name in option_names] - requirements = requirement_set.all_requirements - offenders = [] for requirement in requirements: diff --git a/tests/unit/test_command_install.py b/tests/unit/test_command_install.py index aee03b77a4a..f3215d0b1a8 100644 --- a/tests/unit/test_command_install.py +++ b/tests/unit/test_command_install.py @@ -10,7 +10,6 @@ warn_deprecated_install_options, ) from pip._internal.req.req_install import InstallRequirement -from pip._internal.req.req_set import RequirementSet class TestDecideUserInstall: @@ -47,8 +46,7 @@ def test_most_cases( def test_deprecation_notice_for_pip_install_options(recwarn): install_options = ["--prefix=/hello"] - req_set = RequirementSet() - warn_deprecated_install_options(req_set, install_options) + warn_deprecated_install_options([], install_options) assert len(recwarn) == 1 message = recwarn[0].message.args[0] @@ -57,21 +55,20 @@ def test_deprecation_notice_for_pip_install_options(recwarn): def test_deprecation_notice_for_requirement_options(recwarn): install_options = [] - req_set = RequirementSet() bad_named_req_options = {"install_options": ["--home=/wow"]} bad_named_req = InstallRequirement( Requirement("hello"), "requirements.txt", options=bad_named_req_options ) - req_set.add_named_requirement(bad_named_req) bad_unnamed_req_options = {"install_options": ["--install-lib=/lib"]} bad_unnamed_req = InstallRequirement( None, "requirements2.txt", options=bad_unnamed_req_options ) - req_set.add_unnamed_requirement(bad_unnamed_req) - warn_deprecated_install_options(req_set, install_options) + warn_deprecated_install_options( + [bad_named_req, bad_unnamed_req], install_options + ) assert len(recwarn) == 1 message = recwarn[0].message.args[0] From e872d2bfb6b8eb680505659d51b4bfa556281fef Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 5 Feb 2020 21:40:06 -0500 Subject: [PATCH 1273/3170] Make new RequirementSet in populate_requirement_set Concentrates use of RequirementSet in individual commands so we can eventually break it up. --- src/pip/_internal/cli/req_command.py | 6 +++++- src/pip/_internal/commands/install.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 87a10bc92ae..89a2fcb7402 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -25,6 +25,7 @@ install_req_from_req_string, ) from pip._internal.req.req_file import parse_requirements +from pip._internal.req.req_set import RequirementSet from pip._internal.self_outdated_check import ( make_link_collector, pip_self_version_check, @@ -39,7 +40,6 @@ from pip._internal.cache import WheelCache from pip._internal.models.target_python import TargetPython from pip._internal.req.req_install import InstallRequirement - from pip._internal.req.req_set import RequirementSet from pip._internal.req.req_tracker import RequirementTracker from pip._internal.utils.temp_dir import ( TempDirectory, @@ -278,11 +278,15 @@ def populate_requirement_set( finder, # type: PackageFinder session, # type: PipSession wheel_cache, # type: Optional[WheelCache] + check_supported_wheels=True, # type: bool ): # type: (...) -> List[InstallRequirement] """ Marshal cmd line args into a requirement set. """ + requirement_set = RequirementSet( + check_supported_wheels=check_supported_wheels + ) for filename in options.constraints: for req_to_add in parse_requirements( filename, diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 067507c6967..4863cd4f455 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -298,7 +298,7 @@ def run(self, options, args): try: reqs = self.populate_requirement_set( requirement_set, args, options, finder, session, - wheel_cache + wheel_cache, check_supported_wheels=not options.target_dir, ) warn_deprecated_install_options( From de5ec7e884810cae11d10c8bcd5388329c440075 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 5 Feb 2020 21:41:10 -0500 Subject: [PATCH 1274/3170] Remove unused requirement_set argument --- src/pip/_internal/cli/req_command.py | 1 - src/pip/_internal/commands/download.py | 1 - src/pip/_internal/commands/install.py | 2 +- src/pip/_internal/commands/wheel.py | 2 +- tests/unit/test_req.py | 3 +-- 5 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 89a2fcb7402..bfe3136b024 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -272,7 +272,6 @@ def make_resolver( def populate_requirement_set( self, - requirement_set, # type: RequirementSet args, # type: List[str] options, # type: Values finder, # type: PackageFinder diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index 0627bb9862d..08f1b14ad20 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -105,7 +105,6 @@ def run(self, options, args): requirement_set = RequirementSet() reqs = self.populate_requirement_set( - requirement_set, args, options, finder, diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 4863cd4f455..8694b1d7bf8 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -297,7 +297,7 @@ def run(self, options, args): try: reqs = self.populate_requirement_set( - requirement_set, args, options, finder, session, + args, options, finder, session, wheel_cache, check_supported_wheels=not options.target_dir, ) diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index c914844be1f..e1a9f7dcb31 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -133,7 +133,7 @@ def run(self, options, args): try: reqs = self.populate_requirement_set( - requirement_set, args, options, finder, session, + args, options, finder, session, wheel_cache ) diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index 9b1d480cf1c..1c97bfef080 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -164,14 +164,13 @@ def test_missing_hash_with_require_hashes_in_reqs_file(self, data, tmpdir): """--require-hashes in a requirements file should make its way to the RequirementSet. """ - req_set = RequirementSet() finder = make_test_finder(find_links=[data.find_links]) session = finder._link_collector.session command = create_command('install') with requirements_file('--require-hashes', tmpdir) as reqs_file: options, args = command.parse_args(['-r', reqs_file]) command.populate_requirement_set( - req_set, args, options, finder, session, wheel_cache=None, + args, options, finder, session, wheel_cache=None, ) assert options.require_hashes From 543c84402d7298dcc703325eab9cbbeb9347dffd Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 5 Feb 2020 21:42:04 -0500 Subject: [PATCH 1275/3170] Remove unused requirement_set in commands --- src/pip/_internal/commands/download.py | 3 --- src/pip/_internal/commands/install.py | 6 +----- src/pip/_internal/commands/wheel.py | 4 ---- 3 files changed, 1 insertion(+), 12 deletions(-) diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index 08f1b14ad20..ccc31a94f87 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -9,7 +9,6 @@ from pip._internal.cli import cmdoptions from pip._internal.cli.cmdoptions import make_target_python from pip._internal.cli.req_command import RequirementCommand, with_cleanup -from pip._internal.req import RequirementSet from pip._internal.req.req_tracker import get_requirement_tracker from pip._internal.utils.misc import ensure_dir, normalize_path, write_output from pip._internal.utils.temp_dir import TempDirectory @@ -102,8 +101,6 @@ def run(self, options, args): with get_requirement_tracker() as req_tracker, TempDirectory( options.build_dir, delete=build_delete, kind="download" ) as directory: - - requirement_set = RequirementSet() reqs = self.populate_requirement_set( args, options, diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 8694b1d7bf8..c2e5517bcd1 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -30,7 +30,7 @@ ) from pip._internal.locations import distutils_scheme from pip._internal.operations.check import check_install_conflicts -from pip._internal.req import RequirementSet, install_given_reqs +from pip._internal.req import install_given_reqs from pip._internal.req.req_tracker import get_requirement_tracker from pip._internal.utils.deprecation import deprecated from pip._internal.utils.distutils_args import parse_distutils_args @@ -291,10 +291,6 @@ def run(self, options, args): with get_requirement_tracker() as req_tracker, TempDirectory( options.build_dir, delete=build_delete, kind="install" ) as directory: - requirement_set = RequirementSet( - check_supported_wheels=not options.target_dir, - ) - try: reqs = self.populate_requirement_set( args, options, finder, session, diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index e1a9f7dcb31..67c8fd7f7a8 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -13,7 +13,6 @@ from pip._internal.cli import cmdoptions from pip._internal.cli.req_command import RequirementCommand, with_cleanup from pip._internal.exceptions import CommandError, PreviousBuildDirError -from pip._internal.req import RequirementSet from pip._internal.req.req_tracker import get_requirement_tracker from pip._internal.utils.misc import ensure_dir, normalize_path from pip._internal.utils.temp_dir import TempDirectory @@ -128,9 +127,6 @@ def run(self, options, args): with get_requirement_tracker() as req_tracker, TempDirectory( options.build_dir, delete=build_delete, kind="wheel" ) as directory: - - requirement_set = RequirementSet() - try: reqs = self.populate_requirement_set( args, options, finder, session, From e7998a36179341b86177577d0a8405476bb9c0fd Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 5 Feb 2020 21:44:58 -0500 Subject: [PATCH 1276/3170] Rename populate_requirement_set to get_requirements --- src/pip/_internal/cli/req_command.py | 4 ++-- src/pip/_internal/commands/download.py | 2 +- src/pip/_internal/commands/install.py | 2 +- src/pip/_internal/commands/wheel.py | 2 +- tests/unit/test_req.py | 2 +- 5 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index bfe3136b024..29bbb8fe72d 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -270,7 +270,7 @@ def make_resolver( py_version_info=py_version_info, ) - def populate_requirement_set( + def get_requirements( self, args, # type: List[str] options, # type: Values @@ -281,7 +281,7 @@ def populate_requirement_set( ): # type: (...) -> List[InstallRequirement] """ - Marshal cmd line args into a requirement set. + Parse command-line arguments into the corresponding requirements. """ requirement_set = RequirementSet( check_supported_wheels=check_supported_wheels diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index ccc31a94f87..5ad4b4544c7 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -101,7 +101,7 @@ def run(self, options, args): with get_requirement_tracker() as req_tracker, TempDirectory( options.build_dir, delete=build_delete, kind="download" ) as directory: - reqs = self.populate_requirement_set( + reqs = self.get_requirements( args, options, finder, diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index c2e5517bcd1..811ecbc39db 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -292,7 +292,7 @@ def run(self, options, args): options.build_dir, delete=build_delete, kind="install" ) as directory: try: - reqs = self.populate_requirement_set( + reqs = self.get_requirements( args, options, finder, session, wheel_cache, check_supported_wheels=not options.target_dir, ) diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index 67c8fd7f7a8..a5bf28c9427 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -128,7 +128,7 @@ def run(self, options, args): options.build_dir, delete=build_delete, kind="wheel" ) as directory: try: - reqs = self.populate_requirement_set( + reqs = self.get_requirements( args, options, finder, session, wheel_cache ) diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index 1c97bfef080..29a23926257 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -169,7 +169,7 @@ def test_missing_hash_with_require_hashes_in_reqs_file(self, data, tmpdir): command = create_command('install') with requirements_file('--require-hashes', tmpdir) as reqs_file: options, args = command.parse_args(['-r', reqs_file]) - command.populate_requirement_set( + command.get_requirements( args, options, finder, session, wheel_cache=None, ) assert options.require_hashes From 4d4511fa763441ed4d1c3552ba203fd1e60687dd Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 6 Feb 2020 14:09:35 +0800 Subject: [PATCH 1277/3170] Better news content --- news/6446.bugfix | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/news/6446.bugfix b/news/6446.bugfix index ce8713d9acf..5f6f290a72a 100644 --- a/news/6446.bugfix +++ b/news/6446.bugfix @@ -1 +1,2 @@ -Fix ``_clean_link()`` unquotes quoted ``/`` character. +Correctly handle ``%2F`` in URL parameters to avoid accidently unescape them +into ``/``. From 0dd1c72715056d57eedd284181ca999e0e1bc5a5 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Thu, 6 Feb 2020 11:38:14 +0000 Subject: [PATCH 1278/3170] Fix handling of rollback on failure --- .../_internal/operations/install/legacy.py | 67 +++++++++++-------- src/pip/_internal/req/req_install.py | 38 ++++++----- 2 files changed, 61 insertions(+), 44 deletions(-) diff --git a/src/pip/_internal/operations/install/legacy.py b/src/pip/_internal/operations/install/legacy.py index 4fe0098a734..072bdedd458 100644 --- a/src/pip/_internal/operations/install/legacy.py +++ b/src/pip/_internal/operations/install/legacy.py @@ -23,6 +23,10 @@ logger = logging.getLogger(__name__) +class LegacyInstallFailure(Exception): + pass + + def install( install_options, # type: List[str] global_options, # type: Sequence[str] @@ -39,38 +43,46 @@ def install( unpacked_source_directory, # type: str req_description, # type: str ): - # type: (...) -> bool + # type: (...) -> None header_dir = scheme.headers with TempDirectory(kind="record") as temp_dir: - record_filename = os.path.join(temp_dir.path, 'install-record.txt') - install_args = make_setuptools_install_args( - setup_py_path, - global_options=global_options, - install_options=install_options, - record_filename=record_filename, - root=root, - prefix=prefix, - header_dir=header_dir, - home=home, - use_user_site=use_user_site, - no_user_config=isolated, - pycompile=pycompile, - ) + try: + record_filename = os.path.join(temp_dir.path, 'install-record.txt') + install_args = make_setuptools_install_args( + setup_py_path, + global_options=global_options, + install_options=install_options, + record_filename=record_filename, + root=root, + prefix=prefix, + header_dir=header_dir, + home=home, + use_user_site=use_user_site, + no_user_config=isolated, + pycompile=pycompile, + ) - runner = runner_with_spinner_message( - "Running setup.py install for {}".format(req_name) - ) - with indent_log(), build_env: - runner( - cmd=install_args, - cwd=unpacked_source_directory, + runner = runner_with_spinner_message( + "Running setup.py install for {}".format(req_name) ) + with indent_log(), build_env: + runner( + cmd=install_args, + cwd=unpacked_source_directory, + ) + + if not os.path.exists(record_filename): + logger.debug('Record file %s not found', record_filename) + # Signal to the caller that we didn't install the new package + raise LegacyInstallFailure + + except Exception: + # Signal to the caller that we didn't install the new package + raise LegacyInstallFailure - if not os.path.exists(record_filename): - logger.debug('Record file %s not found', record_filename) - return False + # At this point, we have successfully installed the requirement. # We intentionally do not use any encoding to read the file because # setuptools writes the file using distutils.file_util.write_file, @@ -108,7 +120,8 @@ def prepend_root(path): issue=6998, ) # FIXME: put the record somewhere - return True + return + new_lines = [] for line in record_lines: filename = line.strip() @@ -122,5 +135,3 @@ def prepend_root(path): 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') - - return True diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 0fb020d766e..772e223a9b2 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -26,6 +26,7 @@ generate_metadata as generate_metadata_legacy from pip._internal.operations.install.editable_legacy import \ install_editable as install_editable_legacy +from pip._internal.operations.install.legacy import LegacyInstallFailure from pip._internal.operations.install.legacy import install as install_legacy from pip._internal.operations.install.wheel import install_wheel from pip._internal.pyproject import load_pyproject_toml, make_pyproject_path @@ -828,19 +829,24 @@ def install( install_options = list(install_options) + \ self.options.get('install_options', []) - self.install_succeeded = install_legacy( - install_options=install_options, - global_options=global_options, - root=root, - home=home, - prefix=prefix, - use_user_site=use_user_site, - pycompile=pycompile, - scheme=scheme, - setup_py_path=self.setup_py_path, - isolated=self.isolated, - req_name=self.name, - build_env=self.build_env, - unpacked_source_directory=self.unpacked_source_directory, - req_description=str(self.req), - ) + try: + install_legacy( + install_options=install_options, + global_options=global_options, + root=root, + home=home, + prefix=prefix, + use_user_site=use_user_site, + pycompile=pycompile, + scheme=scheme, + setup_py_path=self.setup_py_path, + isolated=self.isolated, + req_name=self.name, + build_env=self.build_env, + unpacked_source_directory=self.unpacked_source_directory, + req_description=str(self.req), + ) + except Exception as exc: + self.install_succeeded = not isinstance(exc, LegacyInstallFailure) + raise + self.install_succeeded = True From 98da0a680d1614f535732eb8d51281995a4a30ca Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Thu, 6 Feb 2020 12:10:10 +0000 Subject: [PATCH 1279/3170] Correctly preserve exception type --- src/pip/_internal/operations/install/legacy.py | 13 +++++++++---- src/pip/_internal/req/req_install.py | 13 +++++++++---- 2 files changed, 18 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/operations/install/legacy.py b/src/pip/_internal/operations/install/legacy.py index 072bdedd458..0fac90573db 100644 --- a/src/pip/_internal/operations/install/legacy.py +++ b/src/pip/_internal/operations/install/legacy.py @@ -3,6 +3,7 @@ import logging import os +import sys from distutils.util import change_root from pip._internal.utils.deprecation import deprecated @@ -24,7 +25,9 @@ class LegacyInstallFailure(Exception): - pass + def __init__(self): + # type: () -> None + self.parent = sys.exc_info() def install( @@ -43,7 +46,7 @@ def install( unpacked_source_directory, # type: str req_description, # type: str ): - # type: (...) -> None + # type: (...) -> bool header_dir = scheme.headers @@ -76,7 +79,7 @@ def install( if not os.path.exists(record_filename): logger.debug('Record file %s not found', record_filename) # Signal to the caller that we didn't install the new package - raise LegacyInstallFailure + return False except Exception: # Signal to the caller that we didn't install the new package @@ -120,7 +123,7 @@ def prepend_root(path): issue=6998, ) # FIXME: put the record somewhere - return + return True new_lines = [] for line in record_lines: @@ -135,3 +138,5 @@ def prepend_root(path): 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') + + return True diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 772e223a9b2..ca32c3e4ebe 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -830,7 +830,7 @@ def install( self.options.get('install_options', []) try: - install_legacy( + success = install_legacy( install_options=install_options, global_options=global_options, root=root, @@ -847,6 +847,11 @@ def install( req_description=str(self.req), ) except Exception as exc: - self.install_succeeded = not isinstance(exc, LegacyInstallFailure) - raise - self.install_succeeded = True + if isinstance(exc, LegacyInstallFailure): + self.install_succeeded = False + six.reraise(*exc.parent) + else: + self.install_succeeded = True + raise + + self.install_succeeded = success From a9f1d8562ba47a4c23c5ba1a671cf5a1eb746657 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Thu, 6 Feb 2020 16:05:11 +0000 Subject: [PATCH 1280/3170] Pass individual options to InstallRequirement rather than an options object --- src/pip/_internal/commands/install.py | 2 +- src/pip/_internal/operations/install/legacy.py | 6 ++---- src/pip/_internal/req/constructors.py | 8 ++++++-- src/pip/_internal/req/req_install.py | 13 +++++++++---- tests/unit/test_command_install.py | 9 +++++---- tests/unit/test_req_file.py | 9 ++++----- 6 files changed, 27 insertions(+), 20 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 95c90499be0..7ed5d626bb1 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -622,7 +622,7 @@ def format_options(option_names): offenders = [] for requirement in requirements: - install_options = requirement.options.get("install_options", []) + install_options = requirement.install_options location_options = parse_distutils_args(install_options) if location_options: offenders.append( diff --git a/src/pip/_internal/operations/install/legacy.py b/src/pip/_internal/operations/install/legacy.py index 2d4adc4f62c..a547544d3b8 100644 --- a/src/pip/_internal/operations/install/legacy.py +++ b/src/pip/_internal/operations/install/legacy.py @@ -40,10 +40,8 @@ def install( # Options specified in requirements file override those # specified on the command line, since the last option given # to setup.py is the one that is used. - global_options = list(global_options) + \ - install_req.options.get('global_options', []) - install_options = list(install_options) + \ - install_req.options.get('install_options', []) + global_options = list(global_options) + install_req.global_options + install_options = list(install_options) + install_req.install_options header_dir = scheme.headers diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index a1bcb3382c3..c350aaa8da7 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -238,7 +238,9 @@ def install_req_from_editable( constraint=constraint, use_pep517=use_pep517, isolated=isolated, - options=options if options else {}, + install_options=options.get("install_options", []) if options else [], + global_options=options.get("global_options", []) if options else [], + hash_options=options.get("hashes", {}) if options else {}, wheel_cache=wheel_cache, extras=parts.extras, ) @@ -400,7 +402,9 @@ def install_req_from_line( return InstallRequirement( parts.requirement, comes_from, link=parts.link, markers=parts.markers, use_pep517=use_pep517, isolated=isolated, - options=options if options else {}, + install_options=options.get("install_options", []) if options else [], + global_options=options.get("global_options", []) if options else [], + hash_options=options.get("hashes", {}) if options else {}, wheel_cache=wheel_cache, constraint=constraint, extras=parts.extras, diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index b23b2268a54..8d3081f547f 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -107,7 +107,9 @@ def __init__( markers=None, # type: Optional[Marker] use_pep517=None, # type: Optional[bool] isolated=False, # type: bool - options=None, # type: Optional[Dict[str, Any]] + install_options=None, # type: Optional[List[str]] + global_options=None, # type: Optional[List[str]] + hash_options=None, # type: Optional[Dict[str, List[str]]] wheel_cache=None, # type: Optional[WheelCache] constraint=False, # type: bool extras=() # type: Iterable[str] @@ -155,7 +157,10 @@ def __init__( self._temp_build_dir = None # type: Optional[TempDirectory] # Set to True after successful installation self.install_succeeded = None # type: Optional[bool] - self.options = options if options else {} + # Supplied options + self.install_options = install_options if install_options else [] + self.global_options = global_options if global_options else [] + self.hash_options = hash_options if hash_options else {} # Set to True after successful preparation of this requirement self.prepared = False self.is_direct = False @@ -303,7 +308,7 @@ def has_hash_options(self): URL do not. """ - return bool(self.options.get('hashes', {})) + return bool(self.hash_options) def hashes(self, trust_internet=True): # type: (bool) -> Hashes @@ -321,7 +326,7 @@ def hashes(self, trust_internet=True): downloaded from the internet, as by populate_link() """ - good_hashes = self.options.get('hashes', {}).copy() + good_hashes = self.hash_options.copy() link = self.link if trust_internet else self.original_link if link and link.hash: good_hashes.setdefault(link.hash_name, []).append(link.hash) diff --git a/tests/unit/test_command_install.py b/tests/unit/test_command_install.py index aee03b77a4a..89afd97072f 100644 --- a/tests/unit/test_command_install.py +++ b/tests/unit/test_command_install.py @@ -59,15 +59,16 @@ def test_deprecation_notice_for_requirement_options(recwarn): install_options = [] req_set = RequirementSet() - bad_named_req_options = {"install_options": ["--home=/wow"]} + bad_named_req_options = ["--home=/wow"] bad_named_req = InstallRequirement( - Requirement("hello"), "requirements.txt", options=bad_named_req_options + Requirement("hello"), "requirements.txt", + install_options=bad_named_req_options ) req_set.add_named_requirement(bad_named_req) - bad_unnamed_req_options = {"install_options": ["--install-lib=/lib"]} + bad_unnamed_req_options = ["--install-lib=/lib"] bad_unnamed_req = InstallRequirement( - None, "requirements2.txt", options=bad_unnamed_req_options + None, "requirements2.txt", install_options=bad_unnamed_req_options ) req_set.add_unnamed_requirement(bad_unnamed_req) diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py index 4f4843d8f76..597fc2f82f2 100644 --- a/tests/unit/test_req_file.py +++ b/tests/unit/test_req_file.py @@ -320,9 +320,8 @@ def test_options_on_a_requirement_line(self, line_processor): '--global-option="yo3" --global-option "yo4"' filename = 'filename' req = line_processor(line, filename, 1)[0] - assert req.options == { - 'global_options': ['yo3', 'yo4'], - 'install_options': ['yo1', 'yo2']} + assert req.global_options == ['yo3', 'yo4'] + assert req.install_options == ['yo1', 'yo2'] def test_hash_options(self, line_processor): """Test the --hash option: mostly its value storage. @@ -338,13 +337,13 @@ def test_hash_options(self, line_processor): 'e5a6c65260e9cb8a7') filename = 'filename' req = line_processor(line, filename, 1)[0] - assert req.options == {'hashes': { + assert req.hash_options == { 'sha256': ['2cf24dba5fb0a30e26e83b2ac5b9e29e1b161e5c1fa7425e730433' '62938b9824', '486ea46224d1bb4fb680f34f7c9ad96a8f24ec88be73ea8e5a6c65' '260e9cb8a7'], 'sha384': ['59e1748777448c69de6b800d7a33bbfb9ff1b463e44354c3553bcd' - 'b9c666fa90125a3c79f90397bdf5f6a13de828684f']}} + 'b9c666fa90125a3c79f90397bdf5f6a13de828684f']} def test_set_isolated(self, line_processor, options): line = 'SomeProject' From c7184ec503e9faa9f6a01b20e3b9cee2e18f4641 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 7 Feb 2020 15:23:57 +0800 Subject: [PATCH 1281/3170] Fix typo in news fragment Co-Authored-By: Pradyun Gedam <pradyunsg@gmail.com> --- news/6446.bugfix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/6446.bugfix b/news/6446.bugfix index 5f6f290a72a..425e72bd830 100644 --- a/news/6446.bugfix +++ b/news/6446.bugfix @@ -1,2 +1,2 @@ -Correctly handle ``%2F`` in URL parameters to avoid accidently unescape them +Correctly handle ``%2F`` in URL parameters to avoid accidentally unescape them into ``/``. From 89bf3b02dbcb1d7d0668902e5a3b18051812e7e1 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Fri, 7 Feb 2020 16:38:40 +0000 Subject: [PATCH 1282/3170] Record on the requirement whether it has been successfully downloaded --- src/pip/_internal/commands/download.py | 3 ++- src/pip/_internal/legacy_resolve.py | 2 +- src/pip/_internal/req/req_install.py | 9 +++++++++ src/pip/_internal/req/req_set.py | 1 - 4 files changed, 12 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index 5ad4b4544c7..69c7c9a60e6 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -133,7 +133,8 @@ def run(self, options, args): ) downloaded = ' '.join([ - req.name for req in requirement_set.successfully_downloaded + req.name for req in requirement_set.requirements.values() + if req.successfully_downloaded ]) if downloaded: write_output('Successfully downloaded %s', downloaded) diff --git a/src/pip/_internal/legacy_resolve.py b/src/pip/_internal/legacy_resolve.py index 8e603ed089c..2f32631dc60 100644 --- a/src/pip/_internal/legacy_resolve.py +++ b/src/pip/_internal/legacy_resolve.py @@ -395,7 +395,7 @@ def add_req(subreq, extras_requested): # 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) + req_to_install.successfully_downloaded = True return more_reqs diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 8d3081f547f..778b0e0518d 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -165,6 +165,15 @@ def __init__( self.prepared = False self.is_direct = False + # Set by the legacy resolver when the requirement has been downloaded + # TODO: This introduces a strong coupling between the resolver and the + # requirement (the coupling was previously between the resolver + # and the requirement set). This should be refactored to allow + # the requirement to decide for itself when it has been + # successfully downloaded - but that is more tricky to get right, + # se we are making the change in stages. + self.successfully_downloaded = False + self.isolated = isolated self.build_env = NoOpBuildEnvironment() # type: BuildEnvironment diff --git a/src/pip/_internal/req/req_set.py b/src/pip/_internal/req/req_set.py index 56b731c46dd..dc76cce9b30 100644 --- a/src/pip/_internal/req/req_set.py +++ b/src/pip/_internal/req/req_set.py @@ -32,7 +32,6 @@ def __init__(self, check_supported_wheels=True): self.check_supported_wheels = check_supported_wheels self.unnamed_requirements = [] # type: List[InstallRequirement] - self.successfully_downloaded = [] # type: List[InstallRequirement] def __str__(self): # type: () -> str From 8b0146b748979a38ef3bbafff3e0f50bb86bdee7 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sat, 8 Feb 2020 17:53:58 +0800 Subject: [PATCH 1283/3170] Use double backtick in comments Co-Authored-By: Christopher Hunt <chrahunt@gmail.com> --- src/pip/_internal/vcs/versioncontrol.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index 1e3e0875a68..211c6f0ec78 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -245,7 +245,7 @@ def get_backend_for_dir(self, location): return None # Choose the VCS in the inner-most directory. Since all repository - # roots found here would be either ``location``` or one of its + # roots found here would be either `location` or one of its # parents, the longest path should have the most path components, # i.e. the backend representing the inner-most repository. inner_most_repo_path = max(vcs_backends, key=len) @@ -703,7 +703,7 @@ def get_repository_root(cls, location): # type: (str) -> Optional[str] """ Return the "root" (top-level) directory controlled by the vcs, - or ``None`` if the directory is not in any. + or `None` if the directory is not in any. It is meant to be overridden to implement smarter detection mechanisms for specific vcs. From 9f87f46ac33a7cf3773bc6433d83d5d809c239aa Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Mon, 10 Feb 2020 18:50:58 +0000 Subject: [PATCH 1284/3170] Tidy up exception block --- src/pip/_internal/req/req_install.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 0ddcaab163f..e5e136488ef 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -843,12 +843,11 @@ def install( unpacked_source_directory=self.unpacked_source_directory, req_description=str(self.req), ) - except Exception as exc: - if isinstance(exc, LegacyInstallFailure): - self.install_succeeded = False - six.reraise(*exc.parent) - else: - self.install_succeeded = True - raise + except LegacyInstallFailure as exc: + self.install_succeeded = False + six.reraise(*exc.parent) + except Exception: + self.install_succeeded = True + raise self.install_succeeded = success From 6fd36bdf16e735422678e0ed221211706fafbb41 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 11 Feb 2020 00:55:20 +0530 Subject: [PATCH 1285/3170] Workaround breaking changes in virtualenv for CI --- tools/requirements/tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/requirements/tests.txt b/tools/requirements/tests.txt index 136ab9549c6..00a306558b8 100644 --- a/tools/requirements/tests.txt +++ b/tools/requirements/tests.txt @@ -14,6 +14,6 @@ pytest-xdist<1.28.0 pyyaml setuptools>=39.2.0 # Needed for `setuptools.wheel.Wheel` support. scripttest -https://github.com/pypa/virtualenv/archive/master.zip#egg=virtualenv +https://github.com/pypa/virtualenv/archive/legacy.zip#egg=virtualenv werkzeug==0.16.0 wheel From 61040605803c8b181364549298747f7d719313f2 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 11 Feb 2020 14:38:19 +0800 Subject: [PATCH 1286/3170] Swap venv and virtualenv checks Environments created by pypa/virtualenv >=20 can pass both real_prefix and base_prefix checks, but are only able to use the pyvenv.cfg values, not the legacy `no-global-site-packages.txt`. So we need to check for venv (PEP 405) first. --- news/7718.bugfix | 2 ++ src/pip/_internal/utils/virtualenv.py | 7 ++++--- 2 files changed, 6 insertions(+), 3 deletions(-) create mode 100644 news/7718.bugfix diff --git a/news/7718.bugfix b/news/7718.bugfix new file mode 100644 index 00000000000..5abcce69a83 --- /dev/null +++ b/news/7718.bugfix @@ -0,0 +1,2 @@ +Correctly detect global site-packages availability of virtual environments +created by PyPA’s virtualenv>=20.0. diff --git a/src/pip/_internal/utils/virtualenv.py b/src/pip/_internal/utils/virtualenv.py index d81e6ac54bb..596a69a7dad 100644 --- a/src/pip/_internal/utils/virtualenv.py +++ b/src/pip/_internal/utils/virtualenv.py @@ -105,11 +105,12 @@ def virtualenv_no_global(): # type: () -> bool """Returns a boolean, whether running in venv with no system site-packages. """ + # PEP 405 compliance needs to be checked first since virtualenv >=20 would + # return True for both checks, but is only able to use the PEP 405 config. + if _running_under_venv(): + return _no_global_under_venv() if _running_under_regular_virtualenv(): return _no_global_under_regular_virtualenv() - if _running_under_venv(): - return _no_global_under_venv() - return False From 540d3aa0ec88a03bfd756bec604d386e9368e247 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 11 Feb 2020 19:10:27 +0530 Subject: [PATCH 1287/3170] Fix nox -s docs --- noxfile.py | 1 + 1 file changed, 1 insertion(+) diff --git a/noxfile.py b/noxfile.py index a266131b9c1..282e390d281 100644 --- a/noxfile.py +++ b/noxfile.py @@ -23,6 +23,7 @@ "protected-pip": "tools/tox_pip.py", } REQUIREMENTS = { + "docs": "tools/requirements/docs.txt", "tests": "tools/requirements/tests.txt", "common-wheels": "tools/requirements/tests-common_wheels.txt", } From f2e49b3946423c07a46398f691f721771b80b38a Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Thu, 13 Feb 2020 10:18:10 +0000 Subject: [PATCH 1288/3170] Use a new ParsedRequirement class to communicate between handle_line and parse_requirements --- src/pip/_internal/req/req_file.py | 76 +++++++++++++++++++++++++++---- tests/unit/test_req.py | 5 +- 2 files changed, 69 insertions(+), 12 deletions(-) diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index baf8d03ea2a..0263dc15388 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -33,7 +33,7 @@ if MYPY_CHECK_RUNNING: from optparse import Values from typing import ( - Any, Callable, Iterator, List, NoReturn, Optional, Text, Tuple, + Any, Callable, Iterator, List, NoReturn, Optional, Text, Tuple, Dict, ) from pip._internal.req import InstallRequirement @@ -84,6 +84,58 @@ SUPPORTED_OPTIONS_REQ_DEST = [str(o().dest) for o in SUPPORTED_OPTIONS_REQ] +class ParsedRequirement(object): + def __init__( + self, + is_editable, # type: bool + comes_from, # type: str + use_pep517, # type: Optional[bool] + isolated, # type: bool + wheel_cache, # type: Optional[WheelCache] + constraint, # type: bool + args=None, # type: Optional[str] + editables=None, # type: Optional[str] + options=None, # type: Optional[Dict[str, Any]] + line_source=None, # type: Optional[str] + ): + # type: (...) -> None + self.args = args + self.editables = editables + self.is_editable = is_editable + self.comes_from = comes_from + self.use_pep517 = use_pep517 + self.isolated = isolated + self.options = options + self.wheel_cache = wheel_cache + self.constraint = constraint + self.line_source = line_source + + def make_requirement(self): + # type: (...) -> InstallRequirement + if self.is_editable: + req = install_req_from_editable( + self.editables, + comes_from=self.comes_from, + use_pep517=self.use_pep517, + constraint=self.constraint, + isolated=self.isolated, + wheel_cache=self.wheel_cache + ) + + else: + req = install_req_from_line( + self.args, + comes_from=self.comes_from, + use_pep517=self.use_pep517, + isolated=self.isolated, + options=self.options, + wheel_cache=self.wheel_cache, + constraint=self.constraint, + line_source=self.line_source, + ) + return req + + class ParsedLine(object): def __init__( self, @@ -135,11 +187,11 @@ def parse_requirements( ) for parsed_line in parser.parse(filename, constraint): - req = handle_line( + parsed_req = handle_line( parsed_line, finder, options, session, wheel_cache, use_pep517 ) - if req is not None: - yield req + if parsed_req is not None: + yield parsed_req.make_requirement() def preprocess(content, skip_requirements_regex): @@ -166,7 +218,7 @@ def handle_line( wheel_cache=None, # type: Optional[WheelCache] use_pep517=None, # type: Optional[bool] ): - # type: (...) -> Optional[InstallRequirement] + # type: (...) -> Optional[ParsedRequirement] """Handle a single parsed requirements line; This can result in creating/yielding requirements, or updating the finder. @@ -198,8 +250,9 @@ def handle_line( if dest in line.opts.__dict__ and line.opts.__dict__[dest]: req_options[dest] = line.opts.__dict__[dest] line_source = 'line {} of {}'.format(line.lineno, line.filename) - return install_req_from_line( - line.args, + return ParsedRequirement( + args=line.args, + is_editable=False, comes_from=line_comes_from, use_pep517=use_pep517, isolated=isolated, @@ -212,10 +265,13 @@ def handle_line( # return an editable requirement elif line.opts.editables: isolated = options.isolated_mode if options else False - return install_req_from_editable( - line.opts.editables[0], comes_from=line_comes_from, + return ParsedRequirement( + editables=line.opts.editables[0], + is_editable=True, + comes_from=line_comes_from, use_pep517=use_pep517, - constraint=line.constraint, isolated=isolated, + constraint=line.constraint, + isolated=isolated, wheel_cache=wheel_cache ) diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index 29a23926257..3ce7cedc934 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -48,8 +48,9 @@ def get_processed_req_from_line(line, fname='file', lineno=1): opts, False, ) - req = handle_line(parsed_line) - assert req is not None + parsed_req = handle_line(parsed_line) + assert parsed_req is not None + req = parsed_req.make_requirement() req.is_direct = True return req From e2a57fd1e6a74821edb1cf8cf37ec63f59c04fb5 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Thu, 13 Feb 2020 16:16:32 +0000 Subject: [PATCH 1289/3170] Refactor to reduce coupling. * Make ParsedLine record the type of line * Split handle_line to allow passing arguments only where needed * Remove unneeded attributes from ParsedRequirement --- src/pip/_internal/req/req_file.py | 205 ++++++++++++++++++------------ tests/unit/test_req.py | 8 +- 2 files changed, 132 insertions(+), 81 deletions(-) diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index 0263dc15388..4d1a02d8f61 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -87,49 +87,46 @@ class ParsedRequirement(object): def __init__( self, + requirement, # type:str is_editable, # type: bool comes_from, # type: str - use_pep517, # type: Optional[bool] - isolated, # type: bool - wheel_cache, # type: Optional[WheelCache] constraint, # type: bool - args=None, # type: Optional[str] - editables=None, # type: Optional[str] options=None, # type: Optional[Dict[str, Any]] line_source=None, # type: Optional[str] ): # type: (...) -> None - self.args = args - self.editables = editables + self.requirement = requirement self.is_editable = is_editable self.comes_from = comes_from - self.use_pep517 = use_pep517 - self.isolated = isolated self.options = options - self.wheel_cache = wheel_cache self.constraint = constraint self.line_source = line_source - def make_requirement(self): + def make_requirement( + self, + isolated=False, # type: bool + wheel_cache=None, # type: Optional[WheelCache] + use_pep517=None # type: Optional[bool] + ): # type: (...) -> InstallRequirement if self.is_editable: req = install_req_from_editable( - self.editables, + self.requirement, comes_from=self.comes_from, - use_pep517=self.use_pep517, + use_pep517=use_pep517, constraint=self.constraint, - isolated=self.isolated, - wheel_cache=self.wheel_cache + isolated=isolated, + wheel_cache=wheel_cache ) else: req = install_req_from_line( - self.args, + self.requirement, comes_from=self.comes_from, - use_pep517=self.use_pep517, - isolated=self.isolated, + use_pep517=use_pep517, + isolated=isolated, options=self.options, - wheel_cache=self.wheel_cache, + wheel_cache=wheel_cache, constraint=self.constraint, line_source=self.line_source, ) @@ -150,10 +147,21 @@ def __init__( self.filename = filename self.lineno = lineno self.comes_from = comes_from - self.args = args self.opts = opts self.constraint = constraint + if args: + self.is_requirement = True + self.is_editable = False + self.requirement = args + elif opts.editables: + self.is_requirement = True + self.is_editable = True + # We don't support multiple -e on one line + self.requirement = opts.editables[0] + else: + self.is_requirement = False + def parse_requirements( filename, # type: str @@ -188,10 +196,18 @@ def parse_requirements( for parsed_line in parser.parse(filename, constraint): parsed_req = handle_line( - parsed_line, finder, options, session, wheel_cache, use_pep517 + parsed_line, + options=options, + finder=finder, + session=session ) if parsed_req is not None: - yield parsed_req.make_requirement() + isolated = options.isolated_mode if options else False + yield parsed_req.make_requirement( + isolated, + wheel_cache, + use_pep517 + ) def preprocess(content, skip_requirements_regex): @@ -210,91 +226,80 @@ def preprocess(content, skip_requirements_regex): return lines_enum -def handle_line( +def handle_requirement_line( line, # type: ParsedLine - finder=None, # type: Optional[PackageFinder] options=None, # type: Optional[optparse.Values] - session=None, # type: Optional[PipSession] - wheel_cache=None, # type: Optional[WheelCache] - use_pep517=None, # type: Optional[bool] ): - # type: (...) -> Optional[ParsedRequirement] - """Handle a single parsed requirements line; This can result in - creating/yielding requirements, or updating the finder. - - For lines that contain requirements, the only options that have an effect - are from SUPPORTED_OPTIONS_REQ, and they are scoped to the - requirement. Other options from SUPPORTED_OPTIONS may be present, but are - ignored. - - For lines that do not contain requirements, the only options that have an - effect are from SUPPORTED_OPTIONS. Options from SUPPORTED_OPTIONS_REQ may - be present, but are ignored. These lines may contain multiple options - (although our docs imply only one is supported), and all our parsed and - affect the finder. - """ + # type: (...) -> ParsedRequirement # preserve for the nested code path line_comes_from = '{} {} (line {})'.format( '-c' if line.constraint else '-r', line.filename, line.lineno, ) - # return a line requirement - if line.args: - isolated = options.isolated_mode if options else False + assert line.is_requirement + + if line.is_editable: + # For editable requirements, we don't support per-requirement + # options, so just return the parsed requirement. + return ParsedRequirement( + requirement=line.requirement, + is_editable=line.is_editable, + comes_from=line_comes_from, + constraint=line.constraint, + ) + else: if options: + # Disable wheels if the user has specified build options cmdoptions.check_install_build_global(options, line.opts) + # get the options that apply to requirements req_options = {} for dest in SUPPORTED_OPTIONS_REQ_DEST: if dest in line.opts.__dict__ and line.opts.__dict__[dest]: req_options[dest] = line.opts.__dict__[dest] + line_source = 'line {} of {}'.format(line.lineno, line.filename) return ParsedRequirement( - args=line.args, - is_editable=False, + requirement=line.requirement, + is_editable=line.is_editable, comes_from=line_comes_from, - use_pep517=use_pep517, - isolated=isolated, - options=req_options, - wheel_cache=wheel_cache, constraint=line.constraint, + options=req_options, line_source=line_source, ) - # return an editable requirement - elif line.opts.editables: - isolated = options.isolated_mode if options else False - return ParsedRequirement( - editables=line.opts.editables[0], - is_editable=True, - comes_from=line_comes_from, - use_pep517=use_pep517, - constraint=line.constraint, - isolated=isolated, - wheel_cache=wheel_cache - ) + +def handle_option_line( + opts, # type: Values + filename, # type: str + lineno, # type: int + finder=None, # type: Optional[PackageFinder] + options=None, # type: Optional[optparse.Values] + session=None, # type: Optional[PipSession] +): + # type: (...) -> None # percolate hash-checking option upward - elif line.opts.require_hashes: - options.require_hashes = line.opts.require_hashes + if opts.require_hashes: + options.require_hashes = opts.require_hashes # set finder options elif finder: find_links = finder.find_links index_urls = finder.index_urls - if line.opts.index_url: - index_urls = [line.opts.index_url] - if line.opts.no_index is True: + if opts.index_url: + index_urls = [opts.index_url] + if opts.no_index is True: index_urls = [] - if line.opts.extra_index_urls: - index_urls.extend(line.opts.extra_index_urls) - if line.opts.find_links: + if opts.extra_index_urls: + index_urls.extend(opts.extra_index_urls) + if opts.find_links: # FIXME: it would be nice to keep track of the source # of the find_links: support a find-links local path # relative to a requirements file. - value = line.opts.find_links[0] - req_dir = os.path.dirname(os.path.abspath(line.filename)) + value = opts.find_links[0] + req_dir = os.path.dirname(os.path.abspath(filename)) relative_to_reqs_file = os.path.join(req_dir, value) if os.path.exists(relative_to_reqs_file): value = relative_to_reqs_file @@ -306,15 +311,58 @@ def handle_line( ) finder.search_scope = search_scope - if line.opts.pre: + if opts.pre: finder.set_allow_all_prereleases() if session: - for host in line.opts.trusted_hosts or []: - source = 'line {} of {}'.format(line.lineno, line.filename) + for host in opts.trusted_hosts or []: + source = 'line {} of {}'.format(lineno, filename) session.add_trusted_host(host, source=source) - return None + +def handle_line( + line, # type: ParsedLine + options=None, # type: Optional[optparse.Values] + finder=None, # type: Optional[PackageFinder] + session=None, # type: Optional[PipSession] +): + # type: (...) -> Optional[ParsedRequirement] + """Handle a single parsed requirements line; This can result in + creating/yielding requirements, or updating the finder. + + :param line: The parsed line to be processed. + :param options: CLI options. + :param finder: The finder - updated by non-requirement lines. + :param session: The session - updated by non-requirement lines. + + Returns a ParsedRequirement object if the line is a requirement line, + otherwise returns None. + + For lines that contain requirements, the only options that have an effect + are from SUPPORTED_OPTIONS_REQ, and they are scoped to the + requirement. Other options from SUPPORTED_OPTIONS may be present, but are + ignored. + + For lines that do not contain requirements, the only options that have an + effect are from SUPPORTED_OPTIONS. Options from SUPPORTED_OPTIONS_REQ may + be present, but are ignored. These lines may contain multiple options + (although our docs imply only one is supported), and all our parsed and + affect the finder. + """ + + if parsed_line.is_requirement: + parsed_req = handle_requirement_line(parsed_line, options) + return parsed_req + else: + handle_option_line( + parsed_line.opts, + parsed_line.filename, + parsed_line.lineno, + finder, + options, + session, + ) + return None class RequirementsFileParser(object): @@ -342,8 +390,7 @@ def _parse_and_recurse(self, filename, constraint): # type: (str, bool) -> Iterator[ParsedLine] for line in self._parse_file(filename, constraint): if ( - not line.args and - not line.opts.editables and + not line.is_requirement and (line.opts.requirements or line.opts.constraints) ): # parse a nested requirements file diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index 3ce7cedc934..d633c2ce13b 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -31,7 +31,11 @@ install_req_from_req_string, parse_editable, ) -from pip._internal.req.req_file import ParsedLine, get_line_parser, handle_line +from pip._internal.req.req_file import ( + ParsedLine, + get_line_parser, + handle_requirement_line, +) from pip._internal.req.req_tracker import get_requirement_tracker from pip._internal.utils.urls import path_to_url from tests.lib import assert_raises_regexp, make_test_finder, requirements_file @@ -48,7 +52,7 @@ def get_processed_req_from_line(line, fname='file', lineno=1): opts, False, ) - parsed_req = handle_line(parsed_line) + parsed_req = handle_requirement_line(parsed_line) assert parsed_req is not None req = parsed_req.make_requirement() req.is_direct = True From 90e4eb3eedc7c550c2dd036b7a535cdec50a93d5 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Fri, 14 Feb 2020 11:52:53 +0000 Subject: [PATCH 1290/3170] Make parse_requirements return a ParsedRequirement --- src/pip/_internal/cli/req_command.py | 17 ++++-- src/pip/_internal/commands/uninstall.py | 5 +- src/pip/_internal/req/req_file.py | 23 +++------ tests/unit/test_req_file.py | 69 ++++++++++++++++--------- 4 files changed, 67 insertions(+), 47 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 29bbb8fe72d..e67a7ead96d 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -287,10 +287,14 @@ def get_requirements( check_supported_wheels=check_supported_wheels ) for filename in options.constraints: - for req_to_add in parse_requirements( + for parsed_req in parse_requirements( filename, constraint=True, finder=finder, options=options, - session=session, wheel_cache=wheel_cache): + session=session): + req_to_add = parsed_req.make_requirement( + isolated=options.isolated_mode, + wheel_cache=wheel_cache, + ) req_to_add.is_direct = True requirement_set.add_requirement(req_to_add) @@ -315,11 +319,14 @@ def get_requirements( # NOTE: options.require_hashes may be set if --require-hashes is True for filename in options.requirements: - for req_to_add in parse_requirements( + for parsed_req in parse_requirements( filename, - finder=finder, options=options, session=session, + finder=finder, options=options, session=session): + req_to_add = parsed_req.make_requirement( + isolated=options.isolated_mode, wheel_cache=wheel_cache, - use_pep517=options.use_pep517): + use_pep517=options.use_pep517 + ) req_to_add.is_direct = True requirement_set.add_requirement(req_to_add) diff --git a/src/pip/_internal/commands/uninstall.py b/src/pip/_internal/commands/uninstall.py index 1bde414a6c1..71b00483b94 100644 --- a/src/pip/_internal/commands/uninstall.py +++ b/src/pip/_internal/commands/uninstall.py @@ -58,10 +58,13 @@ def run(self, options, args): if req.name: reqs_to_uninstall[canonicalize_name(req.name)] = req for filename in options.requirements: - for req in parse_requirements( + for parsed_req in parse_requirements( filename, options=options, session=session): + req = parsed_req.make_requirement( + isolated=options.isolated_mode + ) if req.name: reqs_to_uninstall[canonicalize_name(req.name)] = req if not reqs_to_uninstall: diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index 4d1a02d8f61..26d2885425f 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -170,10 +170,8 @@ def parse_requirements( comes_from=None, # type: Optional[str] options=None, # type: Optional[optparse.Values] constraint=False, # type: bool - wheel_cache=None, # type: Optional[WheelCache] - use_pep517=None # type: Optional[bool] ): - # type: (...) -> Iterator[InstallRequirement] + # type: (...) -> Iterator[ParsedRequirement] """Parse a requirements file and yield InstallRequirement instances. :param filename: Path or url of requirements file. @@ -183,8 +181,6 @@ def parse_requirements( :param options: cli options. :param constraint: If true, parsing a constraint file rather than requirements file. - :param wheel_cache: Instance of pip.wheel.WheelCache - :param use_pep517: Value of the --use-pep517 option. """ skip_requirements_regex = ( options.skip_requirements_regex if options else None @@ -202,12 +198,7 @@ def parse_requirements( session=session ) if parsed_req is not None: - isolated = options.isolated_mode if options else False - yield parsed_req.make_requirement( - isolated, - wheel_cache, - use_pep517 - ) + yield parsed_req def preprocess(content, skip_requirements_regex): @@ -350,14 +341,14 @@ def handle_line( affect the finder. """ - if parsed_line.is_requirement: - parsed_req = handle_requirement_line(parsed_line, options) + if line.is_requirement: + parsed_req = handle_requirement_line(line, options) return parsed_req else: handle_option_line( - parsed_line.opts, - parsed_line.filename, - parsed_line.lineno, + line.opts, + line.filename, + line.lineno, finder, options, session, diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py index 597fc2f82f2..1eee1024e41 100644 --- a/tests/unit/test_req_file.py +++ b/tests/unit/test_req_file.py @@ -48,6 +48,24 @@ def options(session): format_control=FormatControl(set(), set())) +def parse_reqfile( + filename, + session, + finder=None, + options=None, + constraint=False, + isolated=False, +): + # Wrap parse_requirements/make_requirement to + # avoid having to write the same chunk of code in + # lots of tests. + for parsed_req in parse_requirements( + filename, session, finder=finder, + options=options, constraint=constraint, + ): + yield parsed_req.make_requirement(isolated=isolated) + + class TestPreprocess(object): """tests for `preprocess`""" @@ -191,12 +209,13 @@ def process_line( path.parent.mkdir(exist_ok=True) path.write_text(prefix + line) monkeypatch.chdir(str(tmpdir)) - return list(parse_requirements( + return list(parse_reqfile( filename, finder=finder, options=options, session=session, constraint=constraint, + isolated=options.isolated_mode if options else False )) return process_line @@ -309,7 +328,7 @@ def test_nested_constraints_file(self, monkeypatch, tmpdir): monkeypatch.chdir(str(tmpdir)) reqs = list( - parse_requirements('./parent/req_file.txt', session=session) + parse_reqfile('./parent/req_file.txt', session=session) ) assert len(reqs) == 1 assert reqs[0].name == req_name @@ -447,7 +466,7 @@ def get_file_content(filename, *args, **kwargs): pip._internal.req.req_file, 'get_file_content', get_file_content ) - result = list(parse_requirements(req_file, session=session)) + result = list(parse_reqfile(req_file, session=session)) assert len(result) == 1 assert result[0].name == req_name assert not result[0].constraint @@ -467,7 +486,7 @@ def test_relative_local_nested_req_files( monkeypatch.chdir(str(tmpdir)) reqs = list( - parse_requirements('./parent/req_file.txt', session=session) + parse_reqfile('./parent/req_file.txt', session=session) ) assert len(reqs) == 1 assert reqs[0].name == req_name @@ -490,7 +509,7 @@ def test_absolute_local_nested_req_files( req_file.write_text('-r {}'.format(other_req_file_str)) other_req_file.write_text(req_name) - reqs = list(parse_requirements(str(req_file), session=session)) + reqs = list(parse_reqfile(str(req_file), session=session)) assert len(reqs) == 1 assert reqs[0].name == req_name assert not reqs[0].constraint @@ -516,7 +535,7 @@ def get_file_content(filename, *args, **kwargs): pip._internal.req.req_file, 'get_file_content', get_file_content ) - result = list(parse_requirements(req_file, session=session)) + result = list(parse_reqfile(req_file, session=session)) assert len(result) == 1 assert result[0].name == req_name assert not result[0].constraint @@ -565,7 +584,7 @@ def test_variant5(self, line_processor, finder): class TestParseRequirements(object): - """tests for `parse_requirements`""" + """tests for `parse_reqfile`""" @pytest.mark.network def test_remote_reqs_parse(self): @@ -574,7 +593,7 @@ def test_remote_reqs_parse(self): """ # this requirements file just contains a comment previously this has # failed in py3: https://github.com/pypa/pip/issues/760 - for req in parse_requirements( + for req in parse_reqfile( 'https://raw.githubusercontent.com/pypa/' 'pip-test-package/master/' 'tests/req_just_comment.txt', session=PipSession()): @@ -585,8 +604,8 @@ def test_multiple_appending_options(self, tmpdir, finder, options): fp.write("--extra-index-url url1 \n") fp.write("--extra-index-url url2 ") - list(parse_requirements(tmpdir.joinpath("req1.txt"), finder=finder, - session=PipSession(), options=options)) + list(parse_reqfile(tmpdir.joinpath("req1.txt"), finder=finder, + session=PipSession(), options=options)) assert finder.index_urls == ['url1', 'url2'] @@ -596,8 +615,8 @@ def test_skip_regex(self, tmpdir, finder, options): fp.write("--extra-index-url Bad \n") fp.write("--extra-index-url Good ") - list(parse_requirements(tmpdir.joinpath("req1.txt"), finder=finder, - options=options, session=PipSession())) + list(parse_reqfile(tmpdir.joinpath("req1.txt"), finder=finder, + options=options, session=PipSession())) assert finder.index_urls == ['Good'] @@ -617,7 +636,7 @@ def test_expand_existing_env_variables(self, tmpdir, finder): with patch('pip._internal.req.req_file.os.getenv') as getenv: getenv.side_effect = lambda n: dict(env_vars)[n] - reqs = list(parse_requirements( + reqs = list(parse_reqfile( tmpdir.joinpath('req1.txt'), finder=finder, session=PipSession() @@ -642,7 +661,7 @@ def test_expand_missing_env_variables(self, tmpdir, finder): with patch('pip._internal.req.req_file.os.getenv') as getenv: getenv.return_value = '' - reqs = list(parse_requirements( + reqs = list(parse_reqfile( tmpdir.joinpath('req1.txt'), finder=finder, session=PipSession() @@ -657,13 +676,13 @@ def test_join_lines(self, tmpdir, finder): with open(tmpdir.joinpath("req1.txt"), "w") as fp: fp.write("--extra-index-url url1 \\\n--extra-index-url url2") - list(parse_requirements(tmpdir.joinpath("req1.txt"), finder=finder, - session=PipSession())) + list(parse_reqfile(tmpdir.joinpath("req1.txt"), finder=finder, + session=PipSession())) assert finder.index_urls == ['url1', 'url2'] def test_req_file_parse_no_only_binary(self, data, finder): - list(parse_requirements( + list(parse_reqfile( data.reqfiles.joinpath("supported_options2.txt"), finder=finder, session=PipSession())) @@ -677,7 +696,7 @@ def test_req_file_parse_comment_start_of_line(self, tmpdir, finder): with open(tmpdir.joinpath("req1.txt"), "w") as fp: fp.write("# Comment ") - reqs = list(parse_requirements(tmpdir.joinpath("req1.txt"), + reqs = list(parse_reqfile(tmpdir.joinpath("req1.txt"), finder=finder, session=PipSession())) @@ -690,7 +709,7 @@ def test_req_file_parse_comment_end_of_line_with_url(self, tmpdir, finder): with open(tmpdir.joinpath("req1.txt"), "w") as fp: fp.write("https://example.com/foo.tar.gz # Comment ") - reqs = list(parse_requirements(tmpdir.joinpath("req1.txt"), + reqs = list(parse_reqfile(tmpdir.joinpath("req1.txt"), finder=finder, session=PipSession())) @@ -704,7 +723,7 @@ def test_req_file_parse_egginfo_end_of_line_with_url(self, tmpdir, finder): with open(tmpdir.joinpath("req1.txt"), "w") as fp: fp.write("https://example.com/foo.tar.gz#egg=wat") - reqs = list(parse_requirements(tmpdir.joinpath("req1.txt"), + reqs = list(parse_reqfile(tmpdir.joinpath("req1.txt"), finder=finder, session=PipSession())) @@ -724,7 +743,7 @@ def test_req_file_no_finder(self, tmpdir): --no-index """) - parse_requirements(tmpdir.joinpath("req.txt"), session=PipSession()) + parse_reqfile(tmpdir.joinpath("req.txt"), session=PipSession()) def test_install_requirements_with_options(self, tmpdir, finder, session, options): @@ -738,10 +757,10 @@ def test_install_requirements_with_options(self, tmpdir, finder, session, '''.format(global_option=global_option, install_option=install_option) with requirements_file(content, tmpdir) as reqs_file: - req = next(parse_requirements(reqs_file.resolve(), - finder=finder, - options=options, - session=session)) + req = next(parse_reqfile(reqs_file.resolve(), + finder=finder, + options=options, + session=session)) req.source_dir = os.curdir with patch.object(subprocess, 'Popen') as popen: From aac5d821f97a088d044003b5ee37e7c73cc1b5bc Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Fri, 14 Feb 2020 12:22:50 +0000 Subject: [PATCH 1291/3170] Move make_requirement to pip._internal.req.constructors (and rename it) --- src/pip/_internal/cli/req_command.py | 7 +++-- src/pip/_internal/commands/uninstall.py | 8 ++++-- src/pip/_internal/req/constructors.py | 32 ++++++++++++++++++++++ src/pip/_internal/req/req_file.py | 36 ------------------------- tests/unit/test_req.py | 3 ++- tests/unit/test_req_file.py | 11 +++++--- 6 files changed, 52 insertions(+), 45 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index e67a7ead96d..0aa02ab1f9a 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -22,6 +22,7 @@ from pip._internal.req.constructors import ( install_req_from_editable, install_req_from_line, + install_req_from_parsed_requirement, install_req_from_req_string, ) from pip._internal.req.req_file import parse_requirements @@ -291,7 +292,8 @@ def get_requirements( filename, constraint=True, finder=finder, options=options, session=session): - req_to_add = parsed_req.make_requirement( + req_to_add = install_req_from_parsed_requirement( + parsed_req, isolated=options.isolated_mode, wheel_cache=wheel_cache, ) @@ -322,7 +324,8 @@ def get_requirements( for parsed_req in parse_requirements( filename, finder=finder, options=options, session=session): - req_to_add = parsed_req.make_requirement( + req_to_add = install_req_from_parsed_requirement( + parsed_req, isolated=options.isolated_mode, wheel_cache=wheel_cache, use_pep517=options.use_pep517 diff --git a/src/pip/_internal/commands/uninstall.py b/src/pip/_internal/commands/uninstall.py index 71b00483b94..e93ad74e29e 100644 --- a/src/pip/_internal/commands/uninstall.py +++ b/src/pip/_internal/commands/uninstall.py @@ -9,7 +9,10 @@ from pip._internal.cli.req_command import SessionCommandMixin from pip._internal.exceptions import InstallationError from pip._internal.req import parse_requirements -from pip._internal.req.constructors import install_req_from_line +from pip._internal.req.constructors import ( + install_req_from_line, + install_req_from_parsed_requirement, +) from pip._internal.utils.misc import protect_pip_from_modification_on_windows @@ -62,7 +65,8 @@ def run(self, options, args): filename, options=options, session=session): - req = parsed_req.make_requirement( + req = install_req_from_parsed_requirement( + parsed_req, isolated=options.isolated_mode ) if req.name: diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index c350aaa8da7..e4294b52aa3 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -37,6 +37,7 @@ Any, Dict, Optional, Set, Tuple, Union, ) from pip._internal.cache import WheelCache + from pip._internal.req.req_file import ParsedRequirement __all__ = [ @@ -441,3 +442,34 @@ def install_req_from_req_string( req, comes_from, isolated=isolated, wheel_cache=wheel_cache, use_pep517=use_pep517 ) + + +def install_req_from_parsed_requirement( + parsed_req, # type: ParsedRequirement + isolated=False, # type: bool + wheel_cache=None, # type: Optional[WheelCache] + use_pep517=None # type: Optional[bool] +): + # type: (...) -> InstallRequirement + if parsed_req.is_editable: + req = install_req_from_editable( + parsed_req.requirement, + comes_from=parsed_req.comes_from, + use_pep517=use_pep517, + constraint=parsed_req.constraint, + isolated=isolated, + wheel_cache=wheel_cache + ) + + else: + req = install_req_from_line( + parsed_req.requirement, + comes_from=parsed_req.comes_from, + use_pep517=use_pep517, + isolated=isolated, + options=parsed_req.options, + wheel_cache=wheel_cache, + constraint=parsed_req.constraint, + line_source=parsed_req.line_source, + ) + return req diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index 26d2885425f..ec47abab9f7 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -22,10 +22,6 @@ RequirementsFileParseError, ) from pip._internal.models.search_scope import SearchScope -from pip._internal.req.constructors import ( - install_req_from_editable, - install_req_from_line, -) from pip._internal.utils.encoding import auto_decode from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.urls import get_url_scheme @@ -36,8 +32,6 @@ Any, Callable, Iterator, List, NoReturn, Optional, Text, Tuple, Dict, ) - from pip._internal.req import InstallRequirement - from pip._internal.cache import WheelCache from pip._internal.index.package_finder import PackageFinder from pip._internal.network.session import PipSession @@ -102,36 +96,6 @@ def __init__( self.constraint = constraint self.line_source = line_source - def make_requirement( - self, - isolated=False, # type: bool - wheel_cache=None, # type: Optional[WheelCache] - use_pep517=None # type: Optional[bool] - ): - # type: (...) -> InstallRequirement - if self.is_editable: - req = install_req_from_editable( - self.requirement, - comes_from=self.comes_from, - use_pep517=use_pep517, - constraint=self.constraint, - isolated=isolated, - wheel_cache=wheel_cache - ) - - else: - req = install_req_from_line( - self.requirement, - comes_from=self.comes_from, - use_pep517=use_pep517, - isolated=isolated, - options=self.options, - wheel_cache=wheel_cache, - constraint=self.constraint, - line_source=self.line_source, - ) - return req - class ParsedLine(object): def __init__( diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index d633c2ce13b..f301e11054c 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -28,6 +28,7 @@ _looks_like_path, install_req_from_editable, install_req_from_line, + install_req_from_parsed_requirement, install_req_from_req_string, parse_editable, ) @@ -54,7 +55,7 @@ def get_processed_req_from_line(line, fname='file', lineno=1): ) parsed_req = handle_requirement_line(parsed_line) assert parsed_req is not None - req = parsed_req.make_requirement() + req = install_req_from_parsed_requirement(parsed_req) req.is_direct = True return req diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py index 1eee1024e41..5b8d5e7c376 100644 --- a/tests/unit/test_req_file.py +++ b/tests/unit/test_req_file.py @@ -18,6 +18,7 @@ from pip._internal.req.constructors import ( install_req_from_editable, install_req_from_line, + install_req_from_parsed_requirement, ) from pip._internal.req.req_file import ( break_args_options, @@ -56,14 +57,16 @@ def parse_reqfile( constraint=False, isolated=False, ): - # Wrap parse_requirements/make_requirement to - # avoid having to write the same chunk of code in - # lots of tests. + # Wrap parse_requirements/install_req_from_parsed_requirement to + # avoid having to write the same chunk of code in lots of tests. for parsed_req in parse_requirements( filename, session, finder=finder, options=options, constraint=constraint, ): - yield parsed_req.make_requirement(isolated=isolated) + yield install_req_from_parsed_requirement( + parsed_req, + isolated=isolated + ) class TestPreprocess(object): From 29a06ef947c768ee9c185b627ac0370231f37489 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Fri, 14 Feb 2020 12:25:01 +0000 Subject: [PATCH 1292/3170] Add a trivial news marker --- news/69a4dd1e-c03f-4780-ae6f-892f818fb367.trivial | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 news/69a4dd1e-c03f-4780-ae6f-892f818fb367.trivial diff --git a/news/69a4dd1e-c03f-4780-ae6f-892f818fb367.trivial b/news/69a4dd1e-c03f-4780-ae6f-892f818fb367.trivial new file mode 100644 index 00000000000..e69de29bb2d From 0df2a3d51dc17aeed16c32751708262e4d01979a Mon Sep 17 00:00:00 2001 From: Anthony Sottile <asottile@umich.edu> Date: Fri, 14 Feb 2020 09:44:49 -0800 Subject: [PATCH 1293/3170] Bump cache key after it was poisoned by virtualenv 20.x The symlinks issue has been fixed as of virtualenv 20.0.2, however caches built by virtualenv 20.0.0 and 20.0.1 will continue showing this error Resolves #7749 --- .github/workflows/python-linters.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-linters.yml b/.github/workflows/python-linters.yml index 81a87d46346..f03d7c2e9c8 100644 --- a/.github/workflows/python-linters.yml +++ b/.github/workflows/python-linters.yml @@ -33,7 +33,7 @@ jobs: - uses: actions/cache@v1 with: path: ~/.cache/pre-commit - key: pre-commit|${{ env.PY }}|${{ hashFiles('.pre-commit-config.yaml') }} + key: pre-commit|2020-02-14|${{ env.PY }}|${{ hashFiles('.pre-commit-config.yaml') }} - name: Pre-configure global Git settings run: | git config --global user.email "pypa-dev@googlegroups.com" From 5da3a7d3cf7735b4c7989fe9d571d72e0a60af5d Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Mon, 17 Feb 2020 13:55:57 +0000 Subject: [PATCH 1294/3170] Add documentation to internals on upgrade options --- docs/html/development/architecture/index.rst | 1 + .../architecture/upgrade-options.rst | 115 ++++++++++++++++++ 2 files changed, 116 insertions(+) create mode 100644 docs/html/development/architecture/upgrade-options.rst diff --git a/docs/html/development/architecture/index.rst b/docs/html/development/architecture/index.rst index bb6ff56dd4b..782cc02a7d7 100644 --- a/docs/html/development/architecture/index.rst +++ b/docs/html/development/architecture/index.rst @@ -19,6 +19,7 @@ Architecture of pip's internals overview anatomy package-finding + upgrade-options .. _`tracking issue`: https://github.com/pypa/pip/issues/6831 diff --git a/docs/html/development/architecture/upgrade-options.rst b/docs/html/development/architecture/upgrade-options.rst new file mode 100644 index 00000000000..e6653947688 --- /dev/null +++ b/docs/html/development/architecture/upgrade-options.rst @@ -0,0 +1,115 @@ +Options that control the installation process +--------------------------------------------- + +When installing packages, pip chooses a distribution file, and installs it in +the user's environment. There are many choices involved in deciding which file +to install, and these are controlled by a variety of options. + +Controlling what gets installed +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +These options directly affect how the resolver uses the list of available +distribution files to decide which one to install. So these modify the +resolution algorithm itself, rather than the input to that algorithm. + +``--upgrade`` + +Allow installing a newer version of an installed package. In principle, this +option actually affects "what gets considered", in the sense that it allows +the resolver to see other versions of installed packages. Without +``--upgrade``, the resolver will only see the installed version as a +candidate. + +``--upgrade-strategy`` + +This option affects which packages are allowed to be installed. It is only +relevant if ``--upgrade`` is specified. The base behaviour is to allow +packages specified on pip's command line to be upgraded. This option controls +what *other* packages can be upgraded: + + * ``eager`` - all packages will be upgraded to the latest possible version. + It should be noted here that pip's current resolution algorithm isn't even + aware of packages other than those specified on the command line, and + those identified as dependencies. This may or may not be true of the new + resolver. + * ``only-if-needed`` - packages are only upgraded if they are named in the + pip command or a requirement file (i.e, they are direct requirements), or + an upgraded parent needs a later version of the dependency than is + currently installed. + * ``to-satisfy-only`` (**undocumented**) - packages are not upgraded (not + even direct requirements) unless the currently installed version fails to + satisfy a requirement (either explicitly specified or a dependency). + +``--force-reinstall`` + +Doesn't affect resolution, but if the resolved result is the same as what is +currently installed, uninstall and reinstall it rather than leaving the +current version in place. This occurs even if ``--upgrade`` is not set. + +``--ignore-installed`` + +Act as if the currently installed version isn't there - so don't care about +``--upgrade``, and don't uninstall before (re-)installing. + +Controlling what gets considered +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +These options affect the list of distribution files that the resolver will +consider as candidates for installation. As such, they affect the data that +the resolver has to work with, rather than influencing what pip does with the +resolution result. + +Prereleases + +``--pre`` + +Source vs Binary + +``--no-binary`` + +``--only-binary`` + +``--prefer-binary`` + +Wheel tag specification + +``--platform`` + +``--implementation`` + +``--abi`` + +Index options + +``--index-url`` + +``--extra-index-url`` + +``--no-index`` + +``--find-links`` + +Controlling dependency data +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +These options control what dependency data the resolver sees for any given +package (or, in the case of ``--python-version``, the environment information +the resolver uses to *check* the dependency). + +``--no-deps`` + +``--python-version`` + +``--ignore-requires-python`` + +Special cases +~~~~~~~~~~~~~ + +These need further investigation. They affect the install process, but not +necessarily resolution or what gets installed. + +``--require-hashes`` + +``--constraint`` + +``--editable <LOCATION>`` From 9944ad114f2735db45c4e5fc089ec9b70bb28946 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 11 Feb 2020 17:07:58 +0530 Subject: [PATCH 1295/3170] Make page-title heading style consistent --- docs/html/cookbook.rst | 4 ++-- docs/html/development/ci.rst | 4 ++-- docs/html/development/configuration.rst | 1 + docs/html/development/index.rst | 1 + docs/html/index.rst | 1 + docs/html/installing.rst | 1 + docs/html/quickstart.rst | 1 + docs/html/reference/pip.rst | 3 ++- docs/html/reference/pip_check.rst | 3 ++- docs/html/reference/pip_config.rst | 3 ++- docs/html/reference/pip_debug.rst | 3 ++- docs/html/reference/pip_download.rst | 3 ++- docs/html/reference/pip_freeze.rst | 3 ++- docs/html/reference/pip_hash.rst | 3 ++- docs/html/reference/pip_install.rst | 3 ++- docs/html/reference/pip_list.rst | 3 ++- docs/html/reference/pip_search.rst | 3 ++- docs/html/reference/pip_show.rst | 3 ++- docs/html/reference/pip_uninstall.rst | 3 ++- docs/html/reference/pip_wheel.rst | 3 ++- docs/html/usage.rst | 4 ++-- src/pip/_vendor/README.rst | 1 + 22 files changed, 38 insertions(+), 19 deletions(-) diff --git a/docs/html/cookbook.rst b/docs/html/cookbook.rst index 83d42a608f4..efd76af150b 100644 --- a/docs/html/cookbook.rst +++ b/docs/html/cookbook.rst @@ -1,7 +1,7 @@ :orphan: -============ +======== Cookbook -============ +======== This content is now covered in the :doc:`User Guide <user_guide>` diff --git a/docs/html/development/ci.rst b/docs/html/development/ci.rst index e68b2b4e648..4e37f3d4860 100644 --- a/docs/html/development/ci.rst +++ b/docs/html/development/ci.rst @@ -5,9 +5,9 @@ .. _`tracking issue`: https://github.com/pypa/pip/issues/7279 -********************** +====================== Continuous Integration -********************** +====================== Supported interpreters ====================== diff --git a/docs/html/development/configuration.rst b/docs/html/development/configuration.rst index adf8207dd8f..8615065aa6b 100644 --- a/docs/html/development/configuration.rst +++ b/docs/html/development/configuration.rst @@ -1,5 +1,6 @@ :orphan: +============= Configuration ============= diff --git a/docs/html/development/index.rst b/docs/html/development/index.rst index ffaa3c07b09..5ad4bef98e8 100644 --- a/docs/html/development/index.rst +++ b/docs/html/development/index.rst @@ -1,3 +1,4 @@ +=========== Development =========== diff --git a/docs/html/index.rst b/docs/html/index.rst index a8fab2bd3a9..af817ccf636 100644 --- a/docs/html/index.rst +++ b/docs/html/index.rst @@ -1,3 +1,4 @@ +================================== pip - The Python Package Installer ================================== diff --git a/docs/html/installing.rst b/docs/html/installing.rst index 3ffeef5caae..7a2553bb366 100644 --- a/docs/html/installing.rst +++ b/docs/html/installing.rst @@ -1,5 +1,6 @@ .. _`Installation`: +============ Installation ============ diff --git a/docs/html/quickstart.rst b/docs/html/quickstart.rst index baeb693f13c..c2250399c6a 100644 --- a/docs/html/quickstart.rst +++ b/docs/html/quickstart.rst @@ -1,3 +1,4 @@ +========== Quickstart ========== diff --git a/docs/html/reference/pip.rst b/docs/html/reference/pip.rst index 62b60926752..676869a46a5 100644 --- a/docs/html/reference/pip.rst +++ b/docs/html/reference/pip.rst @@ -1,5 +1,6 @@ +=== pip ---- +=== .. contents:: diff --git a/docs/html/reference/pip_check.rst b/docs/html/reference/pip_check.rst index 2c33e90a3ec..9db7e097f91 100644 --- a/docs/html/reference/pip_check.rst +++ b/docs/html/reference/pip_check.rst @@ -1,7 +1,8 @@ .. _`pip check`: +========= pip check ---------- +========= .. contents:: diff --git a/docs/html/reference/pip_config.rst b/docs/html/reference/pip_config.rst index 0f92f5714a7..2261380aa53 100644 --- a/docs/html/reference/pip_config.rst +++ b/docs/html/reference/pip_config.rst @@ -1,8 +1,9 @@ .. _`pip config`: +========== pip config ------------- +========== .. contents:: diff --git a/docs/html/reference/pip_debug.rst b/docs/html/reference/pip_debug.rst index 8e8b21b5183..718d4d78341 100644 --- a/docs/html/reference/pip_debug.rst +++ b/docs/html/reference/pip_debug.rst @@ -1,7 +1,8 @@ .. _`pip debug`: +========= pip debug ------------ +========= .. contents:: diff --git a/docs/html/reference/pip_download.rst b/docs/html/reference/pip_download.rst index 2432d889457..b3314532409 100644 --- a/docs/html/reference/pip_download.rst +++ b/docs/html/reference/pip_download.rst @@ -1,8 +1,9 @@ .. _`pip download`: +============ pip download ------------- +============ .. contents:: diff --git a/docs/html/reference/pip_freeze.rst b/docs/html/reference/pip_freeze.rst index c13bc00f3b6..22cb7664f4e 100644 --- a/docs/html/reference/pip_freeze.rst +++ b/docs/html/reference/pip_freeze.rst @@ -1,8 +1,9 @@ .. _`pip freeze`: +========== pip freeze ------------ +========== .. contents:: diff --git a/docs/html/reference/pip_hash.rst b/docs/html/reference/pip_hash.rst index 72052bc22dc..70f3375716a 100644 --- a/docs/html/reference/pip_hash.rst +++ b/docs/html/reference/pip_hash.rst @@ -1,7 +1,8 @@ .. _`pip hash`: +======== pip hash ------------- +======== .. contents:: diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index cb32e3baeb8..ca0dbf72166 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -1,7 +1,8 @@ .. _`pip install`: +=========== pip install ------------ +=========== .. contents:: diff --git a/docs/html/reference/pip_list.rst b/docs/html/reference/pip_list.rst index f7f7dd0529f..1126af270f4 100644 --- a/docs/html/reference/pip_list.rst +++ b/docs/html/reference/pip_list.rst @@ -1,7 +1,8 @@ .. _`pip list`: +======== pip list ---------- +======== .. contents:: diff --git a/docs/html/reference/pip_search.rst b/docs/html/reference/pip_search.rst index 1332c99f781..6570a450624 100644 --- a/docs/html/reference/pip_search.rst +++ b/docs/html/reference/pip_search.rst @@ -1,7 +1,8 @@ .. _`pip search`: +========== pip search ----------- +========== .. contents:: diff --git a/docs/html/reference/pip_show.rst b/docs/html/reference/pip_show.rst index 6c9aa84aae4..786cc013c5c 100644 --- a/docs/html/reference/pip_show.rst +++ b/docs/html/reference/pip_show.rst @@ -1,7 +1,8 @@ .. _`pip show`: +======== pip show --------- +======== .. contents:: diff --git a/docs/html/reference/pip_uninstall.rst b/docs/html/reference/pip_uninstall.rst index 28f1bcd2414..e0688cedf4b 100644 --- a/docs/html/reference/pip_uninstall.rst +++ b/docs/html/reference/pip_uninstall.rst @@ -1,7 +1,8 @@ .. _`pip uninstall`: +============= pip uninstall -------------- +============= .. contents:: diff --git a/docs/html/reference/pip_wheel.rst b/docs/html/reference/pip_wheel.rst index 2b8cee946f8..dd682db6700 100644 --- a/docs/html/reference/pip_wheel.rst +++ b/docs/html/reference/pip_wheel.rst @@ -1,8 +1,9 @@ .. _`pip wheel`: +========= pip wheel ---------- +========= .. contents:: diff --git a/docs/html/usage.rst b/docs/html/usage.rst index 20c2b29ec39..ab1e9737f1c 100644 --- a/docs/html/usage.rst +++ b/docs/html/usage.rst @@ -1,7 +1,7 @@ :orphan: -========== +===== Usage -========== +===== The "Usage" section is now covered in the :doc:`Reference Guide <reference/index>` diff --git a/src/pip/_vendor/README.rst b/src/pip/_vendor/README.rst index 38c306aab8e..05804ddccd6 100644 --- a/src/pip/_vendor/README.rst +++ b/src/pip/_vendor/README.rst @@ -1,3 +1,4 @@ +================ Vendoring Policy ================ From 49b978dd8a7eaadb92434f4421a341e36b229e0e Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 11 Feb 2020 18:30:03 +0530 Subject: [PATCH 1296/3170] Make heading style consistent --- docs/html/development/getting-started.rst | 12 ++--- docs/html/development/issue-triage.rst | 6 +-- docs/html/index.rst | 2 +- docs/html/installing.rst | 12 ++--- docs/html/reference/pip_check.rst | 6 +-- docs/html/reference/pip_config.rst | 6 +-- docs/html/reference/pip_debug.rst | 6 +-- docs/html/reference/pip_download.rst | 10 ++-- docs/html/reference/pip_freeze.rst | 8 +-- docs/html/reference/pip_hash.rst | 11 ++-- docs/html/reference/pip_install.rst | 62 +++++++++++------------ docs/html/reference/pip_list.rst | 8 +-- docs/html/reference/pip_search.rst | 8 +-- docs/html/reference/pip_show.rst | 8 +-- docs/html/reference/pip_uninstall.rst | 8 +-- docs/html/reference/pip_wheel.rst | 12 ++--- docs/html/user_guide.rst | 36 ++++++------- src/pip/_vendor/README.rst | 8 +-- 18 files changed, 115 insertions(+), 114 deletions(-) diff --git a/docs/html/development/getting-started.rst b/docs/html/development/getting-started.rst index ed25f968125..4a43b52dbdb 100644 --- a/docs/html/development/getting-started.rst +++ b/docs/html/development/getting-started.rst @@ -9,7 +9,7 @@ reference to the development setup. If you face any issues during this process, please `open an issue`_ about it on the issue tracker. Get the source code -------------------- +=================== To work on pip, you first need to get the source code of pip. The source code is available on `GitHub`_. @@ -21,7 +21,7 @@ available on `GitHub`_. Development Environment ------------------------ +======================= pip is a command line application written in Python. For developing pip, you should `install Python`_ on your computer. @@ -30,7 +30,7 @@ For developing pip, you need to install :pypi:`tox`. Often, you can run ``python -m pip install tox`` to install and use it. Running pip From Source Tree ----------------------------- +============================ To run the pip executable from your source tree during development, run pip from the ``src`` directory: @@ -40,7 +40,7 @@ from the ``src`` directory: $ python src/pip --version Running Tests -------------- +============= pip's tests are written using the :pypi:`pytest` test framework, :pypi:`mock` and :pypi:`pretend`. :pypi:`tox` is used to automate the setup and execution of @@ -85,7 +85,7 @@ tools, you can tell pip to skip those tests: $ tox -e py36 -- -k "not (svn or git)" Running Linters ---------------- +=============== pip uses :pypi:`pre-commit` for managing linting of the codebase. ``pre-commit`` performs various checks on all files in pip and uses tools that @@ -105,7 +105,7 @@ To use linters locally, run: readability problems. Building Documentation ----------------------- +====================== pip's documentation is built using :pypi:`Sphinx`. The documentation is written in reStructuredText. diff --git a/docs/html/development/issue-triage.rst b/docs/html/development/issue-triage.rst index 9fc54f544eb..4b3cfb792d1 100644 --- a/docs/html/development/issue-triage.rst +++ b/docs/html/development/issue-triage.rst @@ -13,7 +13,7 @@ how to help triage reported issues. Issue Tracker -************* +============= The `pip issue tracker <https://github.com/pypa/pip/issues>`__ is hosted on GitHub alongside the project. @@ -101,7 +101,7 @@ links on the closed issue. Triage Issues -************* +============= Users can make issues for a number of reasons: @@ -297,7 +297,7 @@ An issue may be considered resolved and closed when: Common issues -************* +============= #. network-related issues - any issue involving retries, address lookup, or anything like that are typically network issues. diff --git a/docs/html/index.rst b/docs/html/index.rst index af817ccf636..1b90561965c 100644 --- a/docs/html/index.rst +++ b/docs/html/index.rst @@ -29,7 +29,7 @@ If you want to get involved head over to GitHub to get the source code and feel * `Dev IRC`_ Code of Conduct ---------------- +=============== Everyone interacting in the pip project's codebases, issue trackers, chat rooms, and mailing lists is expected to follow the `PyPA Code of Conduct`_. diff --git a/docs/html/installing.rst b/docs/html/installing.rst index 7a2553bb366..0a263ac4137 100644 --- a/docs/html/installing.rst +++ b/docs/html/installing.rst @@ -5,7 +5,7 @@ Installation ============ Do I need to install pip? -------------------------- +========================= pip is already installed if you are using Python 2 >=2.7.9 or Python 3 >=3.4 downloaded from `python.org <https://www.python.org>`_ or if you are working @@ -17,7 +17,7 @@ Just make sure to :ref:`upgrade pip <Upgrading pip>`. .. _`get-pip`: Installing with get-pip.py --------------------------- +========================== To install pip, securely [1]_ download ``get-pip.py`` by following this link: `get-pip.py @@ -52,7 +52,7 @@ speed), although neither are required to install pre-built :term:`wheels get-pip.py options -~~~~~~~~~~~~~~~~~~~ +------------------ .. option:: --no-setuptools @@ -86,7 +86,7 @@ Install behind a proxy:: Using Linux Package Managers ----------------------------- +============================ See :ref:`pypug:Installing pip/setuptools/wheel with Linux Package Managers` in the `Python Packaging User Guide @@ -95,7 +95,7 @@ the `Python Packaging User Guide .. _`Upgrading pip`: Upgrading pip -------------- +============= On Linux or macOS:: @@ -110,7 +110,7 @@ On Windows [4]_:: .. _compatibility-requirements: Python and OS Compatibility ---------------------------- +=========================== pip works with CPython versions 2.7, 3.5, 3.6, 3.7, 3.8 and also PyPy. diff --git a/docs/html/reference/pip_check.rst b/docs/html/reference/pip_check.rst index 9db7e097f91..e106f1a376c 100644 --- a/docs/html/reference/pip_check.rst +++ b/docs/html/reference/pip_check.rst @@ -7,19 +7,19 @@ pip check .. contents:: Usage -***** +===== .. pip-command-usage:: check Description -*********** +=========== .. pip-command-description:: check Examples -******** +======== #. If all dependencies are compatible: diff --git a/docs/html/reference/pip_config.rst b/docs/html/reference/pip_config.rst index 2261380aa53..506a95427aa 100644 --- a/docs/html/reference/pip_config.rst +++ b/docs/html/reference/pip_config.rst @@ -8,16 +8,16 @@ pip config .. contents:: Usage -***** +===== .. pip-command-usage:: config Description -*********** +=========== .. pip-command-description:: config Options -******* +======= .. pip-command-options:: config diff --git a/docs/html/reference/pip_debug.rst b/docs/html/reference/pip_debug.rst index 718d4d78341..05478e984df 100644 --- a/docs/html/reference/pip_debug.rst +++ b/docs/html/reference/pip_debug.rst @@ -7,7 +7,7 @@ pip debug .. contents:: Usage -***** +===== .. pip-command-usage:: debug @@ -18,12 +18,12 @@ Usage Description -*********** +=========== .. pip-command-description:: debug Options -******* +======= .. pip-command-options:: debug diff --git a/docs/html/reference/pip_download.rst b/docs/html/reference/pip_download.rst index b3314532409..ab2611e1739 100644 --- a/docs/html/reference/pip_download.rst +++ b/docs/html/reference/pip_download.rst @@ -8,19 +8,19 @@ pip download .. contents:: Usage -***** +===== .. pip-command-usage:: download Description -*********** +=========== .. pip-command-description:: download Overview -++++++++ +-------- ``pip download`` does the same resolution and downloading as ``pip install``, but instead of installing the dependencies, it collects the downloaded @@ -44,7 +44,7 @@ constrained download requirement. Options -******* +======= .. pip-command-options:: download @@ -52,7 +52,7 @@ Options Examples -******** +======== #. Download a package and all of its dependencies diff --git a/docs/html/reference/pip_freeze.rst b/docs/html/reference/pip_freeze.rst index 22cb7664f4e..8a4e954ed36 100644 --- a/docs/html/reference/pip_freeze.rst +++ b/docs/html/reference/pip_freeze.rst @@ -8,25 +8,25 @@ pip freeze .. contents:: Usage -***** +===== .. pip-command-usage:: freeze Description -*********** +=========== .. pip-command-description:: freeze Options -******* +======= .. pip-command-options:: freeze Examples -******** +======== #. Generate output suitable for a requirements file. diff --git a/docs/html/reference/pip_hash.rst b/docs/html/reference/pip_hash.rst index 70f3375716a..468ecff4283 100644 --- a/docs/html/reference/pip_hash.rst +++ b/docs/html/reference/pip_hash.rst @@ -7,19 +7,20 @@ pip hash .. contents:: Usage -***** +===== .. pip-command-usage:: hash Description -*********** +=========== .. pip-command-description:: hash Overview -++++++++ +-------- + ``pip hash`` is a convenient way to get a hash digest for use with :ref:`hash-checking mode`, especially for packages with multiple archives. The error message from ``pip install --require-hashes ...`` will give you one @@ -30,13 +31,13 @@ different set of options, like :ref:`--no-binary <install_--no-binary>`. Options -******* +======= .. pip-command-options:: hash Example -******** +======= Compute the hash of a downloaded archive:: diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index ca0dbf72166..a188d7a4bdb 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -7,18 +7,18 @@ pip install .. contents:: Usage -***** +===== .. pip-command-usage:: install Description -*********** +=========== .. pip-command-description:: install Overview -++++++++ +-------- Pip install has several stages: @@ -29,7 +29,7 @@ Pip install has several stages: 4. Install the packages (and uninstall anything being upgraded/replaced). Argument Handling -+++++++++++++++++ +----------------- When looking at the items to be installed, pip checks what type of item each is, in the following order: @@ -45,7 +45,7 @@ Each item identified is added to the set of requirements to be satisfied by the install. Working Out the Name and Version -++++++++++++++++++++++++++++++++ +-------------------------------- For each candidate item, pip needs to know the project name and version. For wheels (identified by the ``.whl`` file extension) this can be obtained from @@ -60,7 +60,7 @@ Any URL may use the ``#egg=name`` syntax (see :ref:`VCS Support`) to explicitly state the project name. Satisfying Requirements -+++++++++++++++++++++++ +----------------------- Once pip has the set of requirements to satisfy, it chooses which version of each requirement to install using the simple rule that the latest version that @@ -70,7 +70,7 @@ the chosen version is available, it is assumed that any source is acceptable (as otherwise the versions would differ). Installation Order -++++++++++++++++++ +------------------ .. note:: This section is only about installation order of runtime dependencies, and @@ -125,7 +125,7 @@ profile: .. _`Requirements File Format`: Requirements File Format -++++++++++++++++++++++++ +------------------------ Each line of the requirements file indicates something to be installed, and like arguments to :ref:`pip install`, the following forms are supported:: @@ -188,7 +188,7 @@ You can also refer to :ref:`constraints files <Constraints Files>`, like this:: .. _`Using Environment Variables`: Using Environment Variables -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +^^^^^^^^^^^^^^^^^^^^^^^^^^^ Since version 10, pip supports the use of environment variables inside the requirements file. You can now store sensitive data (tokens, keys, etc.) in @@ -210,7 +210,7 @@ runtime. .. _`Example Requirements File`: Example Requirements File -~~~~~~~~~~~~~~~~~~~~~~~~~ +^^^^^^^^^^^^^^^^^^^^^^^^^ Use ``pip install -r example-requirements.txt`` to install:: @@ -246,7 +246,7 @@ Use ``pip install -r example-requirements.txt`` to install:: .. _`Requirement Specifiers`: Requirement Specifiers -++++++++++++++++++++++ +---------------------- pip supports installing from a package index using a :term:`requirement specifier <pypug:Requirement Specifier>`. Generally speaking, a requirement @@ -291,7 +291,7 @@ Environment markers are supported in the command line and in requirements files. .. _`Per-requirement Overrides`: Per-requirement Overrides -+++++++++++++++++++++++++ +------------------------- Since version 7.0 pip supports controlling the command line options given to ``setup.py`` via requirements files. This disables the use of wheels (cached or @@ -329,7 +329,7 @@ installation error. .. _`Pre Release Versions`: Pre-release Versions -++++++++++++++++++++ +-------------------- Starting with v1.4, pip will only install stable versions as specified by `pre-releases`_ by default. If a version cannot be parsed as a compliant :pep:`440` @@ -349,7 +349,7 @@ that enables installation of pre-releases and development releases. .. _`VCS Support`: VCS Support -+++++++++++ +----------- pip supports installing from Git, Mercurial, Subversion and Bazaar, and detects the type of VCS using URL prefixes: ``git+``, ``hg+``, ``svn+``, and ``bzr+``. @@ -397,7 +397,7 @@ You'll need to use ``pip install -e "vcs+protocol://repo_url/#egg=pkg&subdirecto Git -~~~ +^^^ pip currently supports cloning over ``git``, ``git+http``, ``git+https``, ``git+ssh``, ``git+git`` and ``git+file``: @@ -423,7 +423,7 @@ hash because a full hash allows pip to operate more efficiently (e.g. by making fewer network calls). Mercurial -~~~~~~~~~ +^^^^^^^^^ The supported schemes are: ``hg+file``, ``hg+http``, ``hg+https``, ``hg+static-http``, and ``hg+ssh``. @@ -444,7 +444,7 @@ branch name like so:: [-e] hg+http://hg.example.com/MyProject@special_feature#egg=MyProject Subversion -~~~~~~~~~~ +^^^^^^^^^^ pip supports the URL schemes ``svn``, ``svn+svn``, ``svn+http``, ``svn+https``, ``svn+ssh``. @@ -464,7 +464,7 @@ out the revision from 2008-01-01. You can only check out specific revisions using ``-e svn+...``. Bazaar -~~~~~~ +^^^^^^ pip supports Bazaar using the ``bzr+http``, ``bzr+https``, ``bzr+ssh``, ``bzr+sftp``, ``bzr+ftp`` and ``bzr+lp`` schemes. @@ -483,7 +483,7 @@ Tags or revisions can be installed like so:: [-e] bzr+http://bzr.example.com/MyProject/trunk@v1.0#egg=MyProject Using Environment Variables -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +^^^^^^^^^^^^^^^^^^^^^^^^^^^ Since version 10, pip also makes it possible to use environment variables which makes it possible to reference private repositories without having to store @@ -499,7 +499,7 @@ allowing Basic Auth for authentication can be refenced like this:: ``%VARIABLE%`` won't work. Finding Packages -++++++++++++++++ +---------------- pip searches for packages on `PyPI`_ using the `HTTP simple interface <https://pypi.org/simple/>`_, @@ -522,7 +522,7 @@ See the :ref:`pip install Examples<pip install Examples>`. .. _`SSL Certificate Verification`: SSL Certificate Verification -++++++++++++++++++++++++++++ +---------------------------- Starting with v1.3, pip provides SSL certificate verification over https, to prevent man-in-the-middle attacks against PyPI downloads. @@ -531,7 +531,7 @@ prevent man-in-the-middle attacks against PyPI downloads. .. _`Caching`: Caching -+++++++ +------- Starting with v6.0, pip provides an on-by-default cache which functions similarly to that of a web browser. While the cache is on by default and is @@ -570,7 +570,7 @@ Windows .. _`Wheel cache`: Wheel Cache -~~~~~~~~~~~ +^^^^^^^^^^^ Pip will read from the subdirectory ``wheels`` within the pip cache directory and use any packages found there. This is disabled via the same @@ -595,7 +595,7 @@ automatically and insert it into the wheel cache. .. _`hash-checking mode`: Hash-Checking Mode -++++++++++++++++++ +------------------ Since version 8.0, pip can check downloaded package archives against local hashes to protect against remote tampering. To verify a package against one or @@ -700,7 +700,7 @@ Hash-checking mode also works with :ref:`pip download` and :ref:`pip wheel`. A Hashes from PyPI -~~~~~~~~~~~~~~~~ +^^^^^^^^^^^^^^^^ PyPI provides an MD5 hash in the fragment portion of each package download URL, like ``#md5=123...``, which pip checks as a protection against download @@ -712,7 +712,7 @@ local hash. Local project installs -++++++++++++++++++++++ +---------------------- pip supports installing local project in both regular mode and editable mode. You can install local projects by specifying the project path to pip:: @@ -725,7 +725,7 @@ The exception is that pip will exclude .tox and .nox directories present in the .. _`editable-installs`: "Editable" Installs -~~~~~~~~~~~~~~~~~~~ +^^^^^^^^^^^^^^^^^^^ "Editable" installs are fundamentally `"setuptools develop mode" <https://setuptools.readthedocs.io/en/latest/setuptools.html#development-mode>`_ @@ -746,7 +746,7 @@ which creates the "egg-info" directly relative the current working directory. .. _`controlling-setup-requires`: Controlling setup_requires -++++++++++++++++++++++++++ +-------------------------- Setuptools offers the ``setup_requires`` `setup() keyword <https://setuptools.readthedocs.io/en/latest/setuptools.html#new-and-changed-setup-keywords>`_ @@ -779,7 +779,7 @@ To have the dependency located from a local directory and not crawl PyPI, add th Build System Interface -++++++++++++++++++++++ +---------------------- In order for pip to install a package from source, ``setup.py`` must implement the following commands:: @@ -825,7 +825,7 @@ Installing a package from a wheel does not invoke the build system at all. .. _`pip install Options`: Options -******* +======= .. pip-command-options:: install @@ -835,7 +835,7 @@ Options .. _`pip install Examples`: Examples -******** +======== #. Install ``SomePackage`` and its dependencies from `PyPI`_ using :ref:`Requirement Specifiers` diff --git a/docs/html/reference/pip_list.rst b/docs/html/reference/pip_list.rst index 1126af270f4..3930dd2e56d 100644 --- a/docs/html/reference/pip_list.rst +++ b/docs/html/reference/pip_list.rst @@ -7,17 +7,17 @@ pip list .. contents:: Usage -***** +===== .. pip-command-usage:: list Description -*********** +=========== .. pip-command-description:: list Options -******* +======= .. pip-command-options:: list @@ -25,7 +25,7 @@ Options Examples -******** +======== #. List installed packages. diff --git a/docs/html/reference/pip_search.rst b/docs/html/reference/pip_search.rst index 6570a450624..bbc9fcf9574 100644 --- a/docs/html/reference/pip_search.rst +++ b/docs/html/reference/pip_search.rst @@ -7,24 +7,24 @@ pip search .. contents:: Usage -***** +===== .. pip-command-usage:: search Description -*********** +=========== .. pip-command-description:: search Options -******* +======= .. pip-command-options:: search Examples -******** +======== #. Search for "peppercorn" diff --git a/docs/html/reference/pip_show.rst b/docs/html/reference/pip_show.rst index 786cc013c5c..e9568b6b098 100644 --- a/docs/html/reference/pip_show.rst +++ b/docs/html/reference/pip_show.rst @@ -8,25 +8,25 @@ pip show Usage -***** +===== .. pip-command-usage:: show Description -*********** +=========== .. pip-command-description:: show Options -******* +======= .. pip-command-options:: show Examples -******** +======== #. Show information about a package: diff --git a/docs/html/reference/pip_uninstall.rst b/docs/html/reference/pip_uninstall.rst index e0688cedf4b..f25c4361faf 100644 --- a/docs/html/reference/pip_uninstall.rst +++ b/docs/html/reference/pip_uninstall.rst @@ -7,23 +7,23 @@ pip uninstall .. contents:: Usage -***** +===== .. pip-command-usage:: uninstall Description -*********** +=========== .. pip-command-description:: uninstall Options -******* +======= .. pip-command-options:: uninstall Examples -******** +======== #. Uninstall a package. diff --git a/docs/html/reference/pip_wheel.rst b/docs/html/reference/pip_wheel.rst index dd682db6700..df14dc037f7 100644 --- a/docs/html/reference/pip_wheel.rst +++ b/docs/html/reference/pip_wheel.rst @@ -8,19 +8,19 @@ pip wheel .. contents:: Usage -***** +===== .. pip-command-usage:: wheel Description -*********** +=========== .. pip-command-description:: wheel Build System Interface -++++++++++++++++++++++ +---------------------- In order for pip to build a wheel, ``setup.py`` must implement the ``bdist_wheel`` command with the following syntax:: @@ -33,7 +33,7 @@ interpreter, and save that wheel in the directory TARGET. No other build system commands are invoked by the ``pip wheel`` command. Customising the build -~~~~~~~~~~~~~~~~~~~~~ +^^^^^^^^^^^^^^^^^^^^^ It is possible using ``--global-option`` to include additional build commands with their arguments in the ``setup.py`` command. This is currently the only @@ -56,7 +56,7 @@ the current implementation than a supported interface. Options -******* +======= .. pip-command-options:: wheel @@ -64,7 +64,7 @@ Options Examples -******** +======== #. Build wheels for a requirement (and all its dependencies), and then install diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index ad938917999..ff4384f0df2 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -5,7 +5,7 @@ User Guide .. contents:: Running pip -*********** +=========== pip is a command line program. When you install pip, a ``pip`` command is added to your system, which can be run from the command prompt as follows:: @@ -28,7 +28,7 @@ more details, see :ref:`Using pip from your program`. Installing Packages -******************* +=================== pip supports installing from `PyPI`_, version control, local projects, and directly from distribution files. @@ -50,7 +50,7 @@ For more information and examples, see the :ref:`pip install` reference. Basic Authentication Credentials -******************************** +================================ pip supports basic authentication credentials. Basically, in the URL there is a username and password separated by ``:``. @@ -79,7 +79,7 @@ as the "username" and do not provide a password, for example - Using a Proxy Server -******************** +==================== When installing packages from `PyPI`_, pip requires internet access, which in many corporate environments requires an outbound HTTP proxy server. @@ -98,7 +98,7 @@ pip can be configured to connect through a proxy server in various ways: .. _`Requirements Files`: Requirements Files -****************** +================== "Requirements files" are files containing a list of items to be installed using :ref:`pip install` like so: @@ -180,7 +180,7 @@ See also: .. _`Constraints Files`: Constraints Files -***************** +================= Constraints files are requirements files that only control which version of a requirement is installed, not whether it is installed or not. Their syntax and @@ -214,7 +214,7 @@ Constraints file support was added in pip 7.1. .. _`Installing from Wheels`: Installing from Wheels -********************** +====================== "Wheel" is a built, archive format that can greatly speed installation compared to building and installing from source archives. For more information, see the @@ -258,7 +258,7 @@ wheels (and not from PyPI): Uninstalling Packages -********************* +===================== pip is able to uninstall most packages like so: @@ -273,7 +273,7 @@ For more information and examples, see the :ref:`pip uninstall` reference. Listing Packages -**************** +================ To list installed packages: @@ -311,7 +311,7 @@ reference pages. Searching for Packages -********************** +====================== pip can search `PyPI`_ for packages using the ``pip search`` command:: @@ -326,12 +326,12 @@ For more information and examples, see the :ref:`pip search` reference. .. _`Configuration`: Configuration -************* +============= .. _config-file: Config file ------------- +----------- pip allows you to set all command line option defaults in a standard ini style config file. @@ -497,7 +497,7 @@ Examples: Command Completion -****************** +================== pip comes with support for command line completion in bash, zsh and fish. @@ -524,7 +524,7 @@ startup file:: .. _`Installing from local packages`: Installing from local packages -****************************** +============================== In some cases, you may want to install from local packages only, with no traffic to PyPI. @@ -550,7 +550,7 @@ $ pip install --no-index --find-links=DIR -r requirements.txt "Only if needed" Recursive Upgrade -********************************** +================================== ``pip install --upgrade`` now has a ``--upgrade-strategy`` option which controls how pip handles upgrading of dependencies. There are 2 upgrade @@ -575,7 +575,7 @@ alternative to the behaviour of eager upgrading. User Installs -************* +============= With Python 2.6 came the `"user scheme" for installation <https://docs.python.org/3/install/index.html#alternate-installation-the-user-scheme>`_, @@ -670,7 +670,7 @@ is the latest version:: .. _`Repeatability`: Ensuring Repeatability -********************** +====================== pip can achieve various levels of repeatability: @@ -755,7 +755,7 @@ archives are built with identical packages. .. _`Using pip from your program`: Using pip from your program -*************************** +=========================== As noted previously, pip is a command line program. While it is implemented in Python, and so is available from your Python code via ``import pip``, you must diff --git a/src/pip/_vendor/README.rst b/src/pip/_vendor/README.rst index 05804ddccd6..c5ed6b0d542 100644 --- a/src/pip/_vendor/README.rst +++ b/src/pip/_vendor/README.rst @@ -25,7 +25,7 @@ Vendoring Policy ``pip/_vendor/__init__.py``. Rationale ---------- +========= Historically pip has not had any dependencies except for ``setuptools`` itself, choosing instead to implement any functionality it needed to prevent needing @@ -95,7 +95,7 @@ such as OS packages. Modifications -------------- +============= * ``setuptools`` is completely stripped to only keep ``pkg_resources`` * ``pkg_resources`` has been modified to import its dependencies from ``pip._vendor`` @@ -108,7 +108,7 @@ Modifications Automatic Vendoring -------------------- +=================== Vendoring is automated via the ``vendoring`` tool from the content of ``pip/_vendor/vendor.txt`` and the different patches in @@ -117,7 +117,7 @@ Launch it via ``vendoring sync . -v`` (requires ``vendoring>=0.2.2``). Debundling ----------- +========== As mentioned in the rationale, we, the pip team, would prefer it if pip was not debundled (other than optionally ``pip/_vendor/requests/cacert.pem``) and that From 404f89305e318f7529bda790ac233b4f70e4697c Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 11 Feb 2020 18:35:28 +0530 Subject: [PATCH 1297/3170] Enforce 2 blank lines before h2 --- docs/html/development/ci.rst | 1 + docs/html/development/contributing.rst | 1 + docs/html/development/getting-started.rst | 5 +++++ docs/html/index.rst | 1 + docs/html/reference/pip.rst | 2 ++ docs/html/reference/pip_check.rst | 1 + docs/html/reference/pip_config.rst | 3 +++ docs/html/reference/pip_debug.rst | 1 + docs/html/reference/pip_download.rst | 1 + docs/html/reference/pip_freeze.rst | 1 + docs/html/reference/pip_hash.rst | 1 + docs/html/reference/pip_install.rst | 2 ++ docs/html/reference/pip_list.rst | 3 +++ docs/html/reference/pip_search.rst | 2 ++ docs/html/reference/pip_uninstall.rst | 3 +++ docs/html/reference/pip_wheel.rst | 1 + docs/html/user_guide.rst | 8 ++++++++ 17 files changed, 37 insertions(+) diff --git a/docs/html/development/ci.rst b/docs/html/development/ci.rst index 4e37f3d4860..a953d046b57 100644 --- a/docs/html/development/ci.rst +++ b/docs/html/development/ci.rst @@ -56,6 +56,7 @@ specified it's ok to require the latest CPython interpreter. So only unit tests and integration tests would need to be run with the different interpreters. + Services ======== diff --git a/docs/html/development/contributing.rst b/docs/html/development/contributing.rst index a82420da464..582f7f742ff 100644 --- a/docs/html/development/contributing.rst +++ b/docs/html/development/contributing.rst @@ -34,6 +34,7 @@ Examples include re-flowing text in comments or documentation, or addition or removal of blank lines or whitespace within lines. Such changes can be made separately, as a "formatting cleanup" PR, if needed. + Automated Testing ================= diff --git a/docs/html/development/getting-started.rst b/docs/html/development/getting-started.rst index 4a43b52dbdb..6fa4f9edc54 100644 --- a/docs/html/development/getting-started.rst +++ b/docs/html/development/getting-started.rst @@ -8,6 +8,7 @@ This document is meant to get you setup to work on pip and to act as a guide and reference to the development setup. If you face any issues during this process, please `open an issue`_ about it on the issue tracker. + Get the source code =================== @@ -29,6 +30,7 @@ you should `install Python`_ on your computer. For developing pip, you need to install :pypi:`tox`. Often, you can run ``python -m pip install tox`` to install and use it. + Running pip From Source Tree ============================ @@ -39,6 +41,7 @@ from the ``src`` directory: $ python src/pip --version + Running Tests ============= @@ -84,6 +87,7 @@ tools, you can tell pip to skip those tests: $ tox -e py36 -- -k "not svn" $ tox -e py36 -- -k "not (svn or git)" + Running Linters =============== @@ -104,6 +108,7 @@ To use linters locally, run: reserved for rare cases where the recommended style causes severe readability problems. + Building Documentation ====================== diff --git a/docs/html/index.rst b/docs/html/index.rst index 1b90561965c..1df75855b21 100644 --- a/docs/html/index.rst +++ b/docs/html/index.rst @@ -28,6 +28,7 @@ If you want to get involved head over to GitHub to get the source code and feel * `Dev mailing list`_ * `Dev IRC`_ + Code of Conduct =============== diff --git a/docs/html/reference/pip.rst b/docs/html/reference/pip.rst index 676869a46a5..7907f973b5b 100644 --- a/docs/html/reference/pip.rst +++ b/docs/html/reference/pip.rst @@ -18,6 +18,7 @@ Description .. _`Logging`: + Logging ======= @@ -69,6 +70,7 @@ when decision is needed. .. _`build-interface`: + Build System Interface ====================== diff --git a/docs/html/reference/pip_check.rst b/docs/html/reference/pip_check.rst index e106f1a376c..a12d5b3ec78 100644 --- a/docs/html/reference/pip_check.rst +++ b/docs/html/reference/pip_check.rst @@ -6,6 +6,7 @@ pip check .. contents:: + Usage ===== diff --git a/docs/html/reference/pip_config.rst b/docs/html/reference/pip_config.rst index 506a95427aa..70d9406c562 100644 --- a/docs/html/reference/pip_config.rst +++ b/docs/html/reference/pip_config.rst @@ -7,16 +7,19 @@ pip config .. contents:: + Usage ===== .. pip-command-usage:: config + Description =========== .. pip-command-description:: config + Options ======= diff --git a/docs/html/reference/pip_debug.rst b/docs/html/reference/pip_debug.rst index 05478e984df..b89e531dd64 100644 --- a/docs/html/reference/pip_debug.rst +++ b/docs/html/reference/pip_debug.rst @@ -6,6 +6,7 @@ pip debug .. contents:: + Usage ===== diff --git a/docs/html/reference/pip_download.rst b/docs/html/reference/pip_download.rst index ab2611e1739..a1b4e51ee49 100644 --- a/docs/html/reference/pip_download.rst +++ b/docs/html/reference/pip_download.rst @@ -7,6 +7,7 @@ pip download .. contents:: + Usage ===== diff --git a/docs/html/reference/pip_freeze.rst b/docs/html/reference/pip_freeze.rst index 8a4e954ed36..31efd571b5f 100644 --- a/docs/html/reference/pip_freeze.rst +++ b/docs/html/reference/pip_freeze.rst @@ -7,6 +7,7 @@ pip freeze .. contents:: + Usage ===== diff --git a/docs/html/reference/pip_hash.rst b/docs/html/reference/pip_hash.rst index 468ecff4283..6320a9ab6c5 100644 --- a/docs/html/reference/pip_hash.rst +++ b/docs/html/reference/pip_hash.rst @@ -6,6 +6,7 @@ pip hash .. contents:: + Usage ===== diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index a188d7a4bdb..6867a98ce1f 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -6,11 +6,13 @@ pip install .. contents:: + Usage ===== .. pip-command-usage:: install + Description =========== diff --git a/docs/html/reference/pip_list.rst b/docs/html/reference/pip_list.rst index 3930dd2e56d..15d0920a7f2 100644 --- a/docs/html/reference/pip_list.rst +++ b/docs/html/reference/pip_list.rst @@ -6,16 +6,19 @@ pip list .. contents:: + Usage ===== .. pip-command-usage:: list + Description =========== .. pip-command-description:: list + Options ======= diff --git a/docs/html/reference/pip_search.rst b/docs/html/reference/pip_search.rst index bbc9fcf9574..db1bd2be806 100644 --- a/docs/html/reference/pip_search.rst +++ b/docs/html/reference/pip_search.rst @@ -6,6 +6,7 @@ pip search .. contents:: + Usage ===== @@ -17,6 +18,7 @@ Description .. pip-command-description:: search + Options ======= diff --git a/docs/html/reference/pip_uninstall.rst b/docs/html/reference/pip_uninstall.rst index f25c4361faf..67d752d6b97 100644 --- a/docs/html/reference/pip_uninstall.rst +++ b/docs/html/reference/pip_uninstall.rst @@ -6,16 +6,19 @@ pip uninstall .. contents:: + Usage ===== .. pip-command-usage:: uninstall + Description =========== .. pip-command-description:: uninstall + Options ======= diff --git a/docs/html/reference/pip_wheel.rst b/docs/html/reference/pip_wheel.rst index df14dc037f7..dc32dda463c 100644 --- a/docs/html/reference/pip_wheel.rst +++ b/docs/html/reference/pip_wheel.rst @@ -7,6 +7,7 @@ pip wheel .. contents:: + Usage ===== diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index ff4384f0df2..4ebccd5dd2f 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -4,6 +4,7 @@ User Guide .. contents:: + Running pip =========== @@ -97,6 +98,7 @@ pip can be configured to connect through a proxy server in various ways: .. _`Requirements Files`: + Requirements Files ================== @@ -179,6 +181,7 @@ See also: .. _`Constraints Files`: + Constraints Files ================= @@ -213,6 +216,7 @@ Constraints file support was added in pip 7.1. .. _`Installing from Wheels`: + Installing from Wheels ====================== @@ -325,6 +329,7 @@ For more information and examples, see the :ref:`pip search` reference. .. _`Configuration`: + Configuration ============= @@ -523,6 +528,7 @@ startup file:: .. _`Installing from local packages`: + Installing from local packages ============================== @@ -669,6 +675,7 @@ is the latest version:: .. _`Repeatability`: + Ensuring Repeatability ====================== @@ -754,6 +761,7 @@ archives are built with identical packages. .. _`Using pip from your program`: + Using pip from your program =========================== From 4089cdd0de1218cbd47168cde73d50a51f0b65a4 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 11 Feb 2020 18:35:56 +0530 Subject: [PATCH 1298/3170] Enforce 1 blank line before h3 --- docs/html/development/issue-triage.rst | 2 -- docs/html/reference/pip_download.rst | 1 - docs/html/reference/pip_hash.rst | 1 - docs/html/reference/pip_install.rst | 3 ++- 4 files changed, 2 insertions(+), 5 deletions(-) diff --git a/docs/html/development/issue-triage.rst b/docs/html/development/issue-triage.rst index 4b3cfb792d1..d98fc67b6b6 100644 --- a/docs/html/development/issue-triage.rst +++ b/docs/html/development/issue-triage.rst @@ -24,7 +24,6 @@ user support. In the pip issue tracker, we make use of labels and milestones to organize and track work. - Labels ------ @@ -78,7 +77,6 @@ In addition, there are several standalone labels: this is a special label used by BrownTruck to mark PRs that have merge conflicts - Automation ---------- diff --git a/docs/html/reference/pip_download.rst b/docs/html/reference/pip_download.rst index a1b4e51ee49..b74b1d24038 100644 --- a/docs/html/reference/pip_download.rst +++ b/docs/html/reference/pip_download.rst @@ -19,7 +19,6 @@ Description .. pip-command-description:: download - Overview -------- diff --git a/docs/html/reference/pip_hash.rst b/docs/html/reference/pip_hash.rst index 6320a9ab6c5..dbf1f3e94f8 100644 --- a/docs/html/reference/pip_hash.rst +++ b/docs/html/reference/pip_hash.rst @@ -18,7 +18,6 @@ Description .. pip-command-description:: hash - Overview -------- diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index 6867a98ce1f..92a74c953cf 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -18,7 +18,6 @@ Description .. pip-command-description:: install - Overview -------- @@ -826,6 +825,7 @@ Installing a package from a wheel does not invoke the build system at all. .. _`pip install Options`: + Options ======= @@ -836,6 +836,7 @@ Options .. _`pip install Examples`: + Examples ======== From 2d87d5f6891a4de0295616285331851e398517f8 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 11 Feb 2020 18:40:42 +0530 Subject: [PATCH 1299/3170] Enforce blank lines before directive-blocks --- docs/html/development/architecture/anatomy.rst | 1 + docs/html/development/architecture/index.rst | 2 ++ docs/html/development/architecture/overview.rst | 1 + docs/html/development/ci.rst | 1 + docs/html/development/issue-triage.rst | 1 + docs/html/reference/pip_debug.rst | 1 + docs/html/reference/pip_install.rst | 3 +++ docs/html/user_guide.rst | 1 + 8 files changed, 11 insertions(+) diff --git a/docs/html/development/architecture/anatomy.rst b/docs/html/development/architecture/anatomy.rst index 7415358451b..4fcdeca7714 100644 --- a/docs/html/development/architecture/anatomy.rst +++ b/docs/html/development/architecture/anatomy.rst @@ -1,4 +1,5 @@ .. note:: + This section of the documentation is currently being written. pip developers welcome your help to complete this documentation. If you're interested in helping out, please let us know in the `tracking issue`_. diff --git a/docs/html/development/architecture/index.rst b/docs/html/development/architecture/index.rst index 782cc02a7d7..094adeede1d 100644 --- a/docs/html/development/architecture/index.rst +++ b/docs/html/development/architecture/index.rst @@ -3,11 +3,13 @@ Architecture of pip's internals =============================== .. note:: + This section of the documentation is currently being written. pip developers welcome your help to complete this documentation. If you're interested in helping out, please let us know in the `tracking issue`_. .. note:: + Direct use of pip's internals is *not supported*, and these internals can change at any time. For more details, see :ref:`Using pip from your program`. diff --git a/docs/html/development/architecture/overview.rst b/docs/html/development/architecture/overview.rst index c83600b8b35..1847a04deb2 100644 --- a/docs/html/development/architecture/overview.rst +++ b/docs/html/development/architecture/overview.rst @@ -1,4 +1,5 @@ .. note:: + This section of the documentation is currently being written. pip developers welcome your help to complete this documentation. If you're interested in helping out, please let us know in the `tracking issue`_. diff --git a/docs/html/development/ci.rst b/docs/html/development/ci.rst index a953d046b57..a0e8bd72f4e 100644 --- a/docs/html/development/ci.rst +++ b/docs/html/development/ci.rst @@ -1,4 +1,5 @@ .. note:: + This section of the documentation is currently being written. pip developers welcome your help to complete this documentation. If you're interested in helping out, please let us know in the `tracking issue`_. diff --git a/docs/html/development/issue-triage.rst b/docs/html/development/issue-triage.rst index d98fc67b6b6..edb86e7ee41 100644 --- a/docs/html/development/issue-triage.rst +++ b/docs/html/development/issue-triage.rst @@ -1,4 +1,5 @@ .. note:: + This section of the documentation is currently being written. pip developers welcome your help to complete this documentation. If you're interested in helping out, please let us know in the diff --git a/docs/html/reference/pip_debug.rst b/docs/html/reference/pip_debug.rst index b89e531dd64..da147bcf2fa 100644 --- a/docs/html/reference/pip_debug.rst +++ b/docs/html/reference/pip_debug.rst @@ -14,6 +14,7 @@ Usage .. warning:: + This command is only meant for debugging. Its options and outputs are provisional and may change without notice. diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index 92a74c953cf..9aa2cab68f9 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -74,6 +74,7 @@ Installation Order ------------------ .. note:: + This section is only about installation order of runtime dependencies, and does not apply to build dependencies (those are specified using PEP 518). @@ -676,6 +677,7 @@ Hash-checking mode also works with :ref:`pip download` and :ref:`pip wheel`. A <Repeatability>` is available in the User Guide. .. warning:: + Beware of the ``setup_requires`` keyword arg in :file:`setup.py`. The (rare) packages that use it will cause those dependencies to be downloaded by setuptools directly, skipping pip's hash-checking. If you need to use @@ -683,6 +685,7 @@ Hash-checking mode also works with :ref:`pip download` and :ref:`pip wheel`. A setup_requires<controlling-setup-requires>`. .. warning:: + Be careful not to nullify all your security work when you install your actual project by using setuptools directly: for example, by calling ``python setup.py install``, ``python setup.py develop``, or diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 4ebccd5dd2f..603a5734561 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -753,6 +753,7 @@ Hash-checking mode can be used along with this method to ensure that future archives are built with identical packages. .. warning:: + Finally, beware of the ``setup_requires`` keyword arg in :file:`setup.py`. The (rare) packages that use it will cause those dependencies to be downloaded by setuptools directly, skipping pip's protections. If you need From 59c47ab88c6137205f40ae4720c92f0846ec858e Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 11 Feb 2020 17:27:52 +0530 Subject: [PATCH 1300/3170] Link to our architecture documentation --- docs/html/logic.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/html/logic.rst b/docs/html/logic.rst index 79092629267..189169a8c54 100644 --- a/docs/html/logic.rst +++ b/docs/html/logic.rst @@ -4,4 +4,4 @@ Internal Details ================ -This content is now covered in the :doc:`Reference Guide <reference/index>` +This content is now covered in the :doc:`Architecture section <development/architecture/index>`. From e648e00dc0226ade30ade99591b245b0c98e86c9 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 11 Feb 2020 19:21:29 +0530 Subject: [PATCH 1301/3170] pip is spelt all-lowercase --- .github/ISSUE_TEMPLATE.md | 2 +- NEWS.rst | 8 ++++---- docs/html/development/architecture/anatomy.rst | 2 +- docs/html/development/architecture/overview.rst | 6 +++--- docs/html/reference/pip.rst | 4 ++-- docs/html/reference/pip_install.rst | 6 +++--- docs/html/user_guide.rst | 2 +- src/pip/_internal/commands/install.py | 2 +- src/pip/_internal/utils/wheel.py | 2 +- tests/functional/test_show.py | 2 +- 10 files changed, 18 insertions(+), 18 deletions(-) diff --git a/.github/ISSUE_TEMPLATE.md b/.github/ISSUE_TEMPLATE.md index 2743bae9665..508153d8d25 100644 --- a/.github/ISSUE_TEMPLATE.md +++ b/.github/ISSUE_TEMPLATE.md @@ -1,3 +1,3 @@ -* Pip version: +* pip version: * Python version: * Operating system: diff --git a/NEWS.rst b/NEWS.rst index 0715ddb9048..d0b290f8642 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1538,7 +1538,7 @@ Improved Documentation convince some servers who double compress the downloaded file to stop doing so. (#1688) - Stop breaking when given pip commands in uppercase (#1559, #1725) -- Pip no longer adds duplicate logging consumers, so it won't create duplicate +- pip no longer adds duplicate logging consumers, so it won't create duplicate output when being called multiple times. (#1618, #1723) - `pip wheel` now returns an error code if any wheels fail to build. (#1769) - `pip wheel` wasn't building wheels for dependencies of editable requirements. @@ -1661,7 +1661,7 @@ Improved Documentation - pip will now install Mac OSX platform wheels from PyPI. (:pull:`1278`) - pip now generates the appropriate platform-specific console scripts when installing wheels. (#1251) -- Pip now confirms a wheel is supported when installing directly from a path or +- pip now confirms a wheel is supported when installing directly from a path or url. (#1315) - ``--ignore-installed`` now behaves again as designed, after it was unintentionally broke in v0.8.3 when fixing #14. (#1097, #1352) @@ -1870,7 +1870,7 @@ Improved Documentation Dan Callahan for report and patch. (#182) - Understand version tags without minor version ("py3") in sdist filenames. Thanks Stuart Andrews for report and Olivier Girardot for patch. (#310) -- Pip now supports optionally installing setuptools "extras" dependencies; e.g. +- pip now supports optionally installing setuptools "extras" dependencies; e.g. "pip install Paste[openid]". Thanks Matt Maker and Olivier Girardot. (#7) - freeze no longer borks on requirements files with --index-url or --find-links. Thanks Herbert Pfennig. (#391) @@ -1995,7 +1995,7 @@ Improved Documentation - Track which ``build/`` directories pip creates, never remove directories it doesn't create. From Hugo Lopes Tavares. -- Pip now accepts file:// index URLs. Thanks Dave Abrahams. +- pip now accepts file:// index URLs. Thanks Dave Abrahams. - Various cleanup to make test-running more consistent and less fragile. Thanks Dave Abrahams. - Real Windows support (with passing tests). Thanks Dave Abrahams. diff --git a/docs/html/development/architecture/anatomy.rst b/docs/html/development/architecture/anatomy.rst index 4fcdeca7714..2c731a97ae2 100644 --- a/docs/html/development/architecture/anatomy.rst +++ b/docs/html/development/architecture/anatomy.rst @@ -77,7 +77,7 @@ Within ``src/``: * ``__init__.py`` * ``__main__.py`` * ``__pycache__/`` *[not discussing contents right now]* - * ``_internal/`` *[where all the pip code lives that’s written by pip maintainers -- underscore means private. Pip is not a library -- it’s a command line tool! A very important distinction! People who want to install stuff with pip should not use the internals -- they should use the CLI. There’s a note on this in the docs.]* + * ``_internal/`` *[where all the pip code lives that’s written by pip maintainers -- underscore means private. pip is not a library -- it’s a command line tool! A very important distinction! People who want to install stuff with pip should not use the internals -- they should use the CLI. There’s a note on this in the docs.]* * ``__init__.py`` * ``build_env.py`` [not discussing now] diff --git a/docs/html/development/architecture/overview.rst b/docs/html/development/architecture/overview.rst index 1847a04deb2..637a22f2f31 100644 --- a/docs/html/development/architecture/overview.rst +++ b/docs/html/development/architecture/overview.rst @@ -10,7 +10,7 @@ Broad functionality overview **************************** -Pip is a package installer. +pip is a package installer. pip does a lot more than installation; it also has a cache, and it has configuration, and it has a CLI, which has its own quirks. But mainly: @@ -53,7 +53,7 @@ In sequence, what does pip do?: 4. Install the actual items to be installed. -Why? Pip installs from places other than PyPI! But also, we’ve never had +Why? pip installs from places other than PyPI! But also, we’ve never had guarantees of PyPI’s JSON API before now, so no one has been getting metadata from PyPI separate from downloading the package itself. @@ -118,7 +118,7 @@ When pip looks at the package index, the place where it looks has basically a link. The link’s text is the name of the file This is the `PyPI Simple API`_ (PyPI has several APIs, some are being -deprecated). Pip looks at Simple API, documented initially at :pep:`503` -- +deprecated). pip looks at Simple API, documented initially at :pep:`503` -- packaging.python.org has PyPA specifications with more details for Simple Repository API diff --git a/docs/html/reference/pip.rst b/docs/html/reference/pip.rst index 7907f973b5b..9c218f3557d 100644 --- a/docs/html/reference/pip.rst +++ b/docs/html/reference/pip.rst @@ -74,7 +74,7 @@ when decision is needed. Build System Interface ====================== -Pip builds packages by invoking the build system. By default, builds will use +pip builds packages by invoking the build system. By default, builds will use ``setuptools``, but if a project specifies a different build system using a ``pyproject.toml`` file, as per :pep:`517`, pip will use that instead. As well as package building, the build system is also invoked to install packages @@ -113,7 +113,7 @@ ASCII, pip assumes UTF-8 (to account for the behaviour of some Unix systems). Build systems should ensure that any tools they invoke (compilers, etc) produce output in the correct encoding. In practice - and in particular on Windows, where tools are inconsistent in their use of the "OEM" and "ANSI" codepages - -this may not always be possible. Pip will therefore attempt to recover cleanly +this may not always be possible. pip will therefore attempt to recover cleanly if presented with incorrectly encoded build tool output, by translating unexpected byte sequences to Python-style hexadecimal escape sequences (``"\x80\xff"``, etc). However, it is still possible for output to be displayed diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index 9aa2cab68f9..c81e43ba49e 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -21,7 +21,7 @@ Description Overview -------- -Pip install has several stages: +pip install has several stages: 1. Identify the base requirements. The user supplied arguments are processed here. @@ -574,7 +574,7 @@ Windows Wheel Cache ^^^^^^^^^^^ -Pip will read from the subdirectory ``wheels`` within the pip cache directory +pip will read from the subdirectory ``wheels`` within the pip cache directory and use any packages found there. This is disabled via the same ``--no-cache-dir`` option that disables the HTTP cache. The internal structure of that is not part of the pip API. As of 7.0, pip makes a subdirectory for @@ -583,7 +583,7 @@ each sdist that wheels are built from and places the resulting wheels inside. As of version 20.0, pip also caches wheels when building from an immutable Git reference (i.e. a commit hash). -Pip attempts to choose the best wheels from those built in preference to +pip attempts to choose the best wheels from those built in preference to building a new wheel. Note that this means when a package has both optional C extensions and builds ``py`` tagged wheels when the C extension can't be built that pip will not attempt to build a better wheel for Pythons that would have diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 603a5734561..bc843627380 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -224,7 +224,7 @@ Installing from Wheels to building and installing from source archives. For more information, see the `Wheel docs <https://wheel.readthedocs.io>`_ , :pep:`427`, and :pep:`425`. -Pip prefers Wheels where they are available. To disable this, use the +pip prefers Wheels where they are available. To disable this, use the :ref:`--no-binary <install_--no-binary>` flag for :ref:`pip install`. If no satisfactory wheels are found, pip will default to finding source diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index c1f6a9bf669..242df7d820e 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -489,7 +489,7 @@ def _handle_target_dir(self, target_dir, target_temp_dir, upgrade): if os.path.islink(target_item_dir): logger.warning( 'Target directory %s already exists and is ' - 'a link. Pip will not automatically replace ' + 'a link. pip will not automatically replace ' 'links, please remove if replacement is ' 'desired.', target_item_dir diff --git a/src/pip/_internal/utils/wheel.py b/src/pip/_internal/utils/wheel.py index e4166a68e7b..3ebb7710bc6 100644 --- a/src/pip/_internal/utils/wheel.py +++ b/src/pip/_internal/utils/wheel.py @@ -204,7 +204,7 @@ def check_compatibility(version, name): # type: (Tuple[int, ...], str) -> None """Raises errors or warns if called with an incompatible Wheel-Version. - Pip should refuse to install a Wheel-Version that's a major series + pip should refuse to install a Wheel-Version that's a major series ahead of what it's compatible with (e.g 2.0 > 1.1); and warn when installing a version only minor version ahead (e.g 1.2 > 1.1). diff --git a/tests/functional/test_show.py b/tests/functional/test_show.py index a4000a20a03..0a75d0b10a2 100644 --- a/tests/functional/test_show.py +++ b/tests/functional/test_show.py @@ -122,7 +122,7 @@ def test_more_than_one_package(): Search for more than one package. """ - result = list(search_packages_info(['Pip', 'pytest', 'Virtualenv'])) + result = list(search_packages_info(['pIp', 'pytest', 'Virtualenv'])) assert len(result) == 3 From 0f513ddbea078f2a03ef1dee9f1cae62254c92ba Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 18 Feb 2020 10:56:55 +0530 Subject: [PATCH 1302/3170] Synchronize upgrade-options.rst --- .../development/architecture/upgrade-options.rst | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/html/development/architecture/upgrade-options.rst b/docs/html/development/architecture/upgrade-options.rst index e6653947688..c87e6c97676 100644 --- a/docs/html/development/architecture/upgrade-options.rst +++ b/docs/html/development/architecture/upgrade-options.rst @@ -1,12 +1,14 @@ +============================================= Options that control the installation process ---------------------------------------------- +============================================= When installing packages, pip chooses a distribution file, and installs it in the user's environment. There are many choices involved in deciding which file to install, and these are controlled by a variety of options. + Controlling what gets installed -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +=============================== These options directly affect how the resolver uses the list of available distribution files to decide which one to install. So these modify the @@ -51,8 +53,9 @@ current version in place. This occurs even if ``--upgrade`` is not set. Act as if the currently installed version isn't there - so don't care about ``--upgrade``, and don't uninstall before (re-)installing. + Controlling what gets considered -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +================================ These options affect the list of distribution files that the resolver will consider as candidates for installation. As such, they affect the data that @@ -89,8 +92,9 @@ Index options ``--find-links`` + Controlling dependency data -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +=========================== These options control what dependency data the resolver sees for any given package (or, in the case of ``--python-version``, the environment information @@ -102,8 +106,9 @@ the resolver uses to *check* the dependency). ``--ignore-requires-python`` + Special cases -~~~~~~~~~~~~~ +============= These need further investigation. They affect the install process, but not necessarily resolution or what gets installed. From c93c5e84bf98eab0b8654de94d0ca62b46cbedea Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 18 Feb 2020 11:01:22 +0530 Subject: [PATCH 1303/3170] Add initial draft for documentation conventions --- docs/html/development/conventions.rst | 130 ++++++++++++++++++++++++++ 1 file changed, 130 insertions(+) create mode 100644 docs/html/development/conventions.rst diff --git a/docs/html/development/conventions.rst b/docs/html/development/conventions.rst new file mode 100644 index 00000000000..381a50f7d68 --- /dev/null +++ b/docs/html/development/conventions.rst @@ -0,0 +1,130 @@ +:orphan: + +========================= +Documentation Conventions +========================= + +This document describes the conventions used in pip's documentation. We +expect it to evolve over time as additional conventions are identified +and past conventions are rendered obsolete. + +.. note:: + + Currently, these conventions are not enforced automatically, and + need to be verified manually during code review. We are interested + in linters that can help us enforce these conventions automatically. + + +Files +===== + +Naming +------ + +Folder names should be a single word, all lowercase. + +File names must use the kebab-case style (all lowercase, hyphen for +separating words) and have the extension ``.rst``. + +Encoding +-------- + +All files in our documentation must be UTF-8 encoding. + + +File Layout +=========== + +Line Length +----------- + +Limit all lines to a maximum of 72 characters, where possible. This may +be exceeded when it does not make sense to abide by it (eg. long links, +code blocks). + +Indentation +----------- + +We use 4 spaces for indentation. + +:: + + .. note:: + + Directive blocks + + :: + + Code block. + +Bullet lists are the only exception to the 4 spaces rule, using 2 spaces +when wrapping lines. + +:: + + - This is a bullet list. + - This is a lot of text in a single bullet which would require wrapping + across multiple lines to fit in the line length limits. + + +Headings +======== + +Use the following symbols to create headings: + +#. ``=`` with overline +#. ``=`` +#. ``-`` +#. ``^`` +#. ``'`` +#. ``*`` + +For visual separation from the rest of the content, all other headings +must have one empty line before and after. Heading 2 (``=``) should have +two empty lines before, for indicating the end of the section prior to +it. + +:: + + ========= + Heading 1 + ========= + + Lorem ipsum dolor sit amet consectetur adipisicing elit. + + + Heading 2 + ========= + + Lorem ipsum dolor sit amet consectetur adipisicing elit. + + Heading 3 + --------- + + Lorem ipsum dolor sit amet consectetur adipisicing elit. + + Heading 4 + ^^^^^^^^^ + + Lorem ipsum dolor sit amet consectetur adipisicing elit. + + Heading 5 + ''''''''' + + Lorem ipsum dolor sit amet consectetur adipisicing elit. + + Heading 6 + ********* + + Lorem ipsum dolor sit amet consectetur adipisicing elit. + + +Writing +======= + +pip is a proper noun, and spelt all lowercase. Do not capitalize pip as +"Pip" at the start of a sentence. + +Avoid using phrases such as "easy", "just", "simply" etc, which imply +that the task is trivial. If it were trivial, the user wouldn't be +reading the documentation for it. From ce1e0f470aee6dc7702610f57295b5a4e0270261 Mon Sep 17 00:00:00 2001 From: Nitesh Sharma <nbsharma@outlook.com> Date: Fri, 21 Feb 2020 14:01:13 +0530 Subject: [PATCH 1304/3170] Move UI helpers to cli subpackage (#6727) --- ...c551d8-61a7-4f48-9d90-58909eca5537.trivial | 0 src/pip/_internal/build_env.py | 2 +- src/pip/_internal/cli/cmdoptions.py | 2 +- .../{utils/ui.py => cli/progress_bars.py} | 223 +++--------------- src/pip/_internal/cli/spinners.py | 172 ++++++++++++++ src/pip/_internal/network/download.py | 2 +- .../operations/build/wheel_legacy.py | 2 +- src/pip/_internal/utils/subprocess.py | 3 +- src/pip/_internal/vcs/versioncontrol.py | 2 +- tests/unit/test_utils_subprocess.py | 2 +- 10 files changed, 215 insertions(+), 195 deletions(-) create mode 100644 news/5fc551d8-61a7-4f48-9d90-58909eca5537.trivial rename src/pip/_internal/{utils/ui.py => cli/progress_bars.py} (54%) create mode 100644 src/pip/_internal/cli/spinners.py diff --git a/news/5fc551d8-61a7-4f48-9d90-58909eca5537.trivial b/news/5fc551d8-61a7-4f48-9d90-58909eca5537.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py index 0cbcfdf2811..b8f005f5ca9 100644 --- a/src/pip/_internal/build_env.py +++ b/src/pip/_internal/build_env.py @@ -16,10 +16,10 @@ from pip._vendor.pkg_resources import Requirement, VersionConflict, WorkingSet from pip import __file__ as pip_location +from pip._internal.cli.spinners import open_spinner from pip._internal.utils.subprocess import call_subprocess from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from pip._internal.utils.ui import open_spinner if MYPY_CHECK_RUNNING: from typing import Tuple, Set, Iterable, Optional, List diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index f3450199095..c74d2b632a6 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -21,6 +21,7 @@ from optparse import SUPPRESS_HELP, Option, OptionGroup from textwrap import dedent +from pip._internal.cli.progress_bars import BAR_TYPES from pip._internal.exceptions import CommandError from pip._internal.locations import USER_CACHE_DIR, get_src_prefix from pip._internal.models.format_control import FormatControl @@ -28,7 +29,6 @@ from pip._internal.models.target_python import TargetPython 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, Callable, Dict, Optional, Tuple diff --git a/src/pip/_internal/utils/ui.py b/src/pip/_internal/cli/progress_bars.py similarity index 54% rename from src/pip/_internal/utils/ui.py rename to src/pip/_internal/cli/progress_bars.py index f84feb3171e..7ed224790cf 100644 --- a/src/pip/_internal/utils/ui.py +++ b/src/pip/_internal/cli/progress_bars.py @@ -1,18 +1,10 @@ -# The following comment should be removed at some point in the future. -# mypy: strict-optional=False -# mypy: disallow-untyped-defs=False +from __future__ import division -from __future__ import absolute_import, division - -import contextlib import itertools -import logging import sys -import time from signal import SIGINT, default_int_handler, signal from pip._vendor import six -from pip._vendor.progress import HIDE_CURSOR, SHOW_CURSOR from pip._vendor.progress.bar import Bar, FillingCirclesBar, IncrementalBar from pip._vendor.progress.spinner import Spinner @@ -22,7 +14,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Any, Iterator, IO + from typing import Any, Dict, List try: from pip._vendor import colorama @@ -31,10 +23,9 @@ except Exception: colorama = None -logger = logging.getLogger(__name__) - def _select_progress_class(preferred, fallback): + # type: (Bar, Bar) -> Bar encoding = getattr(preferred.file, "encoding", None) # If we don't know what encoding this file is in, then we'll just assume @@ -83,10 +74,14 @@ class InterruptibleMixin(object): """ def __init__(self, *args, **kwargs): + # type: (List[Any], Dict[Any, Any]) -> None """ Save the original SIGINT handler for later. """ - super(InterruptibleMixin, self).__init__(*args, **kwargs) + super(InterruptibleMixin, self).__init__( # type: ignore + *args, + **kwargs + ) self.original_handler = signal(SIGINT, self.handle_sigint) @@ -99,16 +94,17 @@ def __init__(self, *args, **kwargs): self.original_handler = default_int_handler def finish(self): + # type: () -> None """ Restore the original SIGINT handler after finishing. This should happen regardless of whether the progress display finishes normally, or gets interrupted. """ - super(InterruptibleMixin, self).finish() + super(InterruptibleMixin, self).finish() # type: ignore signal(SIGINT, self.original_handler) - def handle_sigint(self, signum, frame): + def handle_sigint(self, signum, frame): # type: ignore """ Call self.finish() before delegating to the original SIGINT handler. @@ -122,6 +118,7 @@ def handle_sigint(self, signum, frame): class SilentBar(Bar): def update(self): + # type: () -> None pass @@ -136,27 +133,36 @@ class BlueEmojiBar(IncrementalBar): class DownloadProgressMixin(object): def __init__(self, *args, **kwargs): - super(DownloadProgressMixin, self).__init__(*args, **kwargs) - self.message = (" " * (get_indentation() + 2)) + self.message + # type: (List[Any], Dict[Any, Any]) -> None + super(DownloadProgressMixin, self).__init__( # type: ignore + *args, + **kwargs + ) + self.message = (" " * ( + get_indentation() + 2 + )) + self.message # type: str @property def downloaded(self): - return format_size(self.index) + # type: () -> str + return format_size(self.index) # type: ignore @property def download_speed(self): + # type: () -> str # Avoid zero division errors... - if self.avg == 0.0: + if self.avg == 0.0: # type: ignore return "..." - return format_size(1 / self.avg) + "/s" + return format_size(1 / self.avg) + "/s" # type: ignore @property def pretty_eta(self): - if self.eta: - return "eta {}".format(self.eta_td) + # type: () -> str + if self.eta: # type: ignore + return "eta {}".format(self.eta_td) # type: ignore return "" - def iter(self, it): + def iter(self, it): # type: ignore for x in it: yield x self.next(len(x)) @@ -166,6 +172,7 @@ def iter(self, it): class WindowsMixin(object): def __init__(self, *args, **kwargs): + # type: (List[Any], Dict[Any, Any]) -> None # The Windows terminal does not support the hide/show cursor ANSI codes # even with colorama. So we'll ensure that hide_cursor is False on # Windows. @@ -173,15 +180,15 @@ def __init__(self, *args, **kwargs): # is set in time. The base progress bar class writes the "hide cursor" # code to the terminal in its init, so if we don't set this soon # enough, we get a "hide" with no corresponding "show"... - if WINDOWS and self.hide_cursor: + if WINDOWS and self.hide_cursor: # type: ignore self.hide_cursor = False - super(WindowsMixin, self).__init__(*args, **kwargs) + super(WindowsMixin, self).__init__(*args, **kwargs) # type: ignore # Check if we are running on Windows and we have the colorama module, # if we do then wrap our file with it. if WINDOWS and colorama: - self.file = colorama.AnsiToWin32(self.file) + self.file = colorama.AnsiToWin32(self.file) # type: ignore # The progress code expects to be able to call self.file.isatty() # but the colorama.AnsiToWin32() object doesn't have that, so we'll # add it. @@ -233,12 +240,13 @@ class DownloadProgressSpinner(WindowsMixin, InterruptibleMixin, file = sys.stdout suffix = "%(downloaded)s %(download_speed)s" - def next_phase(self): + def next_phase(self): # type: ignore if not hasattr(self, "_phaser"): self._phaser = itertools.cycle(self.phases) return next(self._phaser) def update(self): + # type: () -> None message = self.message % self phase = self.next_phase() suffix = self.suffix % self @@ -262,167 +270,8 @@ def update(self): } -def DownloadProgressProvider(progress_bar, max=None): +def DownloadProgressProvider(progress_bar, max=None): # type: ignore 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 -# -# We don't even try using progress.spinner.Spinner here because it's actually -# simpler to reimplement from scratch than to coerce their code into doing -# what we need. -################################################################ - -@contextlib.contextmanager -def hidden_cursor(file): - # type: (IO[Any]) -> Iterator[None] - # The Windows terminal does not support the hide/show cursor ANSI codes, - # even via colorama. So don't even try. - if WINDOWS: - yield - # We don't want to clutter the output with control characters if we're - # writing to a file, or if the user is running with --quiet. - # See https://github.com/pypa/pip/issues/3418 - elif not file.isatty() or logger.getEffectiveLevel() > logging.INFO: - yield - else: - file.write(HIDE_CURSOR) - try: - yield - finally: - file.write(SHOW_CURSOR) - - -class RateLimiter(object): - def __init__(self, min_update_interval_seconds): - # type: (float) -> None - self._min_update_interval_seconds = min_update_interval_seconds - self._last_update = 0 # type: float - - def ready(self): - # type: () -> bool - now = time.time() - delta = now - self._last_update - return delta >= self._min_update_interval_seconds - - def reset(self): - # type: () -> None - self._last_update = time.time() - - -class SpinnerInterface(object): - def spin(self): - # type: () -> None - raise NotImplementedError() - - def finish(self, final_status): - # type: (str) -> None - raise NotImplementedError() - - -class InteractiveSpinner(SpinnerInterface): - def __init__(self, message, file=None, spin_chars="-\\|/", - # Empirically, 8 updates/second looks nice - min_update_interval_seconds=0.125): - self._message = message - if file is None: - file = sys.stdout - self._file = file - self._rate_limiter = RateLimiter(min_update_interval_seconds) - self._finished = False - - self._spin_cycle = itertools.cycle(spin_chars) - - self._file.write(" " * get_indentation() + self._message + " ... ") - self._width = 0 - - def _write(self, status): - assert not self._finished - # Erase what we wrote before by backspacing to the beginning, writing - # spaces to overwrite the old text, and then backspacing again - backup = "\b" * self._width - self._file.write(backup + " " * self._width + backup) - # Now we have a blank slate to add our status - self._file.write(status) - self._width = len(status) - self._file.flush() - self._rate_limiter.reset() - - def spin(self): - # type: () -> None - if self._finished: - return - if not self._rate_limiter.ready(): - return - self._write(next(self._spin_cycle)) - - def finish(self, final_status): - # type: (str) -> None - if self._finished: - return - self._write(final_status) - self._file.write("\n") - self._file.flush() - self._finished = True - - -# Used for dumb terminals, non-interactive installs (no tty), etc. -# We still print updates occasionally (once every 60 seconds by default) to -# act as a keep-alive for systems like Travis-CI that take lack-of-output as -# an indication that a task has frozen. -class NonInteractiveSpinner(SpinnerInterface): - def __init__(self, message, min_update_interval_seconds=60): - # type: (str, float) -> None - self._message = message - self._finished = False - self._rate_limiter = RateLimiter(min_update_interval_seconds) - self._update("started") - - def _update(self, status): - assert not self._finished - self._rate_limiter.reset() - logger.info("%s: %s", self._message, status) - - def spin(self): - # type: () -> None - if self._finished: - return - if not self._rate_limiter.ready(): - return - self._update("still running...") - - def finish(self, final_status): - # type: (str) -> None - if self._finished: - return - self._update("finished with status '{}'".format(final_status)) - self._finished = True - - -@contextlib.contextmanager -def open_spinner(message): - # type: (str) -> Iterator[SpinnerInterface] - # Interactive spinner goes directly to sys.stdout rather than being routed - # through the logging system, but it acts like it has level INFO, - # i.e. it's only displayed if we're at level INFO or better. - # Non-interactive spinner goes through the logging system, so it is always - # in sync with logging configuration. - if sys.stdout.isatty() and logger.getEffectiveLevel() <= logging.INFO: - spinner = InteractiveSpinner(message) # type: SpinnerInterface - else: - spinner = NonInteractiveSpinner(message) - try: - with hidden_cursor(sys.stdout): - yield spinner - except KeyboardInterrupt: - spinner.finish("canceled") - raise - except Exception: - spinner.finish("error") - raise - else: - spinner.finish("done") diff --git a/src/pip/_internal/cli/spinners.py b/src/pip/_internal/cli/spinners.py new file mode 100644 index 00000000000..3d152a80742 --- /dev/null +++ b/src/pip/_internal/cli/spinners.py @@ -0,0 +1,172 @@ +from __future__ import absolute_import, division + +import contextlib +import itertools +import logging +import sys +import time + +from pip._vendor.progress import HIDE_CURSOR, SHOW_CURSOR + +from pip._internal.utils.compat import WINDOWS +from pip._internal.utils.logging import get_indentation +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import Iterator, IO + +logger = logging.getLogger(__name__) + + +class SpinnerInterface(object): + def spin(self): + # type: () -> None + raise NotImplementedError() + + def finish(self, final_status): + # type: (str) -> None + raise NotImplementedError() + + +class InteractiveSpinner(SpinnerInterface): + def __init__(self, message, file=None, spin_chars="-\\|/", + # Empirically, 8 updates/second looks nice + min_update_interval_seconds=0.125): + # type: (str, IO[str], str, float) -> None + self._message = message + if file is None: + file = sys.stdout + self._file = file + self._rate_limiter = RateLimiter(min_update_interval_seconds) + self._finished = False + + self._spin_cycle = itertools.cycle(spin_chars) + + self._file.write(" " * get_indentation() + self._message + " ... ") + self._width = 0 + + def _write(self, status): + # type: (str) -> None + assert not self._finished + # Erase what we wrote before by backspacing to the beginning, writing + # spaces to overwrite the old text, and then backspacing again + backup = "\b" * self._width + self._file.write(backup + " " * self._width + backup) + # Now we have a blank slate to add our status + self._file.write(status) + self._width = len(status) + self._file.flush() + self._rate_limiter.reset() + + def spin(self): + # type: () -> None + if self._finished: + return + if not self._rate_limiter.ready(): + return + self._write(next(self._spin_cycle)) + + def finish(self, final_status): + # type: (str) -> None + if self._finished: + return + self._write(final_status) + self._file.write("\n") + self._file.flush() + self._finished = True + + +# Used for dumb terminals, non-interactive installs (no tty), etc. +# We still print updates occasionally (once every 60 seconds by default) to +# act as a keep-alive for systems like Travis-CI that take lack-of-output as +# an indication that a task has frozen. +class NonInteractiveSpinner(SpinnerInterface): + def __init__(self, message, min_update_interval_seconds=60): + # type: (str, float) -> None + self._message = message + self._finished = False + self._rate_limiter = RateLimiter(min_update_interval_seconds) + self._update("started") + + def _update(self, status): + # type: (str) -> None + assert not self._finished + self._rate_limiter.reset() + logger.info("%s: %s", self._message, status) + + def spin(self): + # type: () -> None + if self._finished: + return + if not self._rate_limiter.ready(): + return + self._update("still running...") + + def finish(self, final_status): + # type: (str) -> None + if self._finished: + return + self._update("finished with status '%s'" % (final_status,)) + self._finished = True + + +class RateLimiter(object): + def __init__(self, min_update_interval_seconds): + # type: (float) -> None + self._min_update_interval_seconds = min_update_interval_seconds + self._last_update = 0 # type: float + + def ready(self): + # type: () -> bool + now = time.time() + delta = now - self._last_update + return delta >= self._min_update_interval_seconds + + def reset(self): + # type: () -> None + self._last_update = time.time() + + +@contextlib.contextmanager +def open_spinner(message): + # type: (str) -> Iterator[SpinnerInterface] + # Interactive spinner goes directly to sys.stdout rather than being routed + # through the logging system, but it acts like it has level INFO, + # i.e. it's only displayed if we're at level INFO or better. + # Non-interactive spinner goes through the logging system, so it is always + # in sync with logging configuration. + if sys.stdout.isatty() and logger.getEffectiveLevel() <= logging.INFO: + spinner = InteractiveSpinner(message) # type: SpinnerInterface + else: + spinner = NonInteractiveSpinner(message) + try: + with hidden_cursor(sys.stdout): + yield spinner + except KeyboardInterrupt: + spinner.finish("canceled") + raise + except Exception: + spinner.finish("error") + raise + else: + spinner.finish("done") + + +@contextlib.contextmanager +def hidden_cursor(file): + # type: (IO[str]) -> Iterator[None] + # The Windows terminal does not support the hide/show cursor ANSI codes, + # even via colorama. So don't even try. + if WINDOWS: + yield + # We don't want to clutter the output with control characters if we're + # writing to a file, or if the user is running with --quiet. + # See https://github.com/pypa/pip/issues/3418 + elif not file.isatty() or logger.getEffectiveLevel() > logging.INFO: + yield + else: + file.write(HIDE_CURSOR) + try: + yield + finally: + file.write(SHOW_CURSOR) diff --git a/src/pip/_internal/network/download.py b/src/pip/_internal/network/download.py index c90c4bf42cf..2f3e08ae62e 100644 --- a/src/pip/_internal/network/download.py +++ b/src/pip/_internal/network/download.py @@ -8,6 +8,7 @@ from pip._vendor import requests from pip._vendor.requests.models import CONTENT_CHUNK_SIZE +from pip._internal.cli.progress_bars import DownloadProgressProvider from pip._internal.models.index import PyPI from pip._internal.network.cache import is_from_cache from pip._internal.network.utils import response_chunks @@ -17,7 +18,6 @@ splitext, ) from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from pip._internal.utils.ui import DownloadProgressProvider if MYPY_CHECK_RUNNING: from typing import Iterable, Optional diff --git a/src/pip/_internal/operations/build/wheel_legacy.py b/src/pip/_internal/operations/build/wheel_legacy.py index 96dd09a4542..37dc876acbd 100644 --- a/src/pip/_internal/operations/build/wheel_legacy.py +++ b/src/pip/_internal/operations/build/wheel_legacy.py @@ -1,6 +1,7 @@ import logging import os.path +from pip._internal.cli.spinners import open_spinner from pip._internal.utils.setuptools_build import ( make_setuptools_bdist_wheel_args, ) @@ -10,7 +11,6 @@ format_command_args, ) from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from pip._internal.utils.ui import open_spinner if MYPY_CHECK_RUNNING: from typing import List, Optional, Text diff --git a/src/pip/_internal/utils/subprocess.py b/src/pip/_internal/utils/subprocess.py index 61c40a49eae..55c82daea7c 100644 --- a/src/pip/_internal/utils/subprocess.py +++ b/src/pip/_internal/utils/subprocess.py @@ -9,18 +9,17 @@ from pip._vendor.six.moves import shlex_quote +from pip._internal.cli.spinners import SpinnerInterface, open_spinner from pip._internal.exceptions import InstallationError from pip._internal.utils.compat import console_to_str, str_to_display from pip._internal.utils.logging import subprocess_logger from pip._internal.utils.misc import HiddenText, path_to_display from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from pip._internal.utils.ui import open_spinner if MYPY_CHECK_RUNNING: from typing import ( Any, Callable, Iterable, List, Mapping, Optional, Text, Union, ) - from pip._internal.utils.ui import SpinnerInterface CommandArgs = List[Union[str, HiddenText]] diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index f849037110e..7b6171451ac 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -30,7 +30,7 @@ Any, Dict, Iterable, Iterator, List, Mapping, Optional, Text, Tuple, Type, Union ) - from pip._internal.utils.ui import SpinnerInterface + from pip._internal.cli.spinners import SpinnerInterface from pip._internal.utils.misc import HiddenText from pip._internal.utils.subprocess import CommandArgs diff --git a/tests/unit/test_utils_subprocess.py b/tests/unit/test_utils_subprocess.py index 44b8a7b3c45..b0de2bf578d 100644 --- a/tests/unit/test_utils_subprocess.py +++ b/tests/unit/test_utils_subprocess.py @@ -6,6 +6,7 @@ import pytest +from pip._internal.cli.spinners import SpinnerInterface from pip._internal.exceptions import InstallationError from pip._internal.utils.misc import hide_value from pip._internal.utils.subprocess import ( @@ -14,7 +15,6 @@ make_command, make_subprocess_output_error, ) -from pip._internal.utils.ui import SpinnerInterface @pytest.mark.parametrize('args, expected', [ From 082c0f01d29bb1341c8f9332c0470c4fe2d3895b Mon Sep 17 00:00:00 2001 From: Anudit Nagar <nagaranudit@gmail.com> Date: Fri, 21 Feb 2020 21:48:14 +0530 Subject: [PATCH 1305/3170] Update old-style formatting to new-style formatting (#7762) --- docs/pip_sphinxext.py | 2 +- news/37d2f118-19f0-4c2c-b002-d70a8629b350.trivial | 0 src/pip/_internal/exceptions.py | 10 +++++----- src/pip/_internal/models/link.py | 2 +- src/pip/_internal/req/constructors.py | 8 ++++---- tests/unit/test_req.py | 12 ++++++------ tests/unit/test_req_file.py | 14 +++++++------- tests/unit/test_utils.py | 4 ++-- tests/unit/test_utils_unpacking.py | 2 +- 9 files changed, 27 insertions(+), 27 deletions(-) create mode 100644 news/37d2f118-19f0-4c2c-b002-d70a8629b350.trivial diff --git a/docs/pip_sphinxext.py b/docs/pip_sphinxext.py index bfc5a6b4aef..a0e69c6e4c2 100644 --- a/docs/pip_sphinxext.py +++ b/docs/pip_sphinxext.py @@ -55,7 +55,7 @@ def _format_option(self, option, cmd_name=None): line += option._long_opts[0] if option.takes_value(): metavar = option.metavar or option.dest.lower() - line += " <%s>" % metavar.lower() + line += " <{}>".format(metavar.lower()) # fix defaults opt_help = option.help.replace('%default', str(option.default)) # fix paths with sys.prefix diff --git a/news/37d2f118-19f0-4c2c-b002-d70a8629b350.trivial b/news/37d2f118-19f0-4c2c-b002-d70a8629b350.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/exceptions.py b/src/pip/_internal/exceptions.py index 882529dece7..115f7eb0c6b 100644 --- a/src/pip/_internal/exceptions.py +++ b/src/pip/_internal/exceptions.py @@ -150,10 +150,10 @@ def body(self): populate_link() having already been called """ - return ' %s' % self._requirement_name() + return ' {}'.format(self._requirement_name()) def __str__(self): - return '%s\n%s' % (self.head, self.body()) + return '{}\n{}'.format(self.head, self.body()) def _requirement_name(self): """Return a description of the requirement that triggered me. @@ -215,9 +215,9 @@ def body(self): # In case someone feeds something downright stupid # to InstallRequirement's constructor. else getattr(self.req, 'req', None)) - return ' %s --hash=%s:%s' % (package or 'unknown package', - FAVORITE_HASH, - self.gotten_hash) + return ' {} --hash={}:{}'.format(package or 'unknown package', + FAVORITE_HASH, + self.gotten_hash) class HashUnpinned(HashError): diff --git a/src/pip/_internal/models/link.py b/src/pip/_internal/models/link.py index 1b3aa591ab0..0b30ab2511a 100644 --- a/src/pip/_internal/models/link.py +++ b/src/pip/_internal/models/link.py @@ -77,7 +77,7 @@ def __str__(self): def __repr__(self): # type: () -> str - return '<Link %s>' % self + return '<Link {}>'.format(self) @property def url(self): diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index c350aaa8da7..ddceabfd7c2 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -123,8 +123,8 @@ def parse_editable(editable_req): return package_name, url_no_extras, None for version_control in vcs: - if url.lower().startswith('%s:' % version_control): - url = '%s+%s' % (version_control, url) + if url.lower().startswith('{}:'.format(version_control)): + url = '{}+{}'.format(version_control, url) break if '+' not in url: @@ -175,8 +175,8 @@ def deduce_helpful_msg(req): " the packages specified within it." ).format(req) except RequirementParseError: - logger.debug("Cannot parse '%s' as requirements \ - file" % (req), exc_info=True) + logger.debug("Cannot parse '{}' as requirements \ + file".format(req), exc_info=True) else: msg += " File '{}' does not exist.".format(req) return msg diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index 29a23926257..8ef40833ad0 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -449,14 +449,14 @@ def test_markers_semicolon(self): def test_markers_url(self): # test "URL; markers" syntax url = 'http://foo.com/?p=bar.git;a=snapshot;h=v0.1;sf=tgz' - line = '%s; python_version >= "3"' % url + line = '{}; python_version >= "3"'.format(url) req = install_req_from_line(line) assert req.link.url == url, req.url assert str(req.markers) == 'python_version >= "3"' # without space, markers are part of the URL url = 'http://foo.com/?p=bar.git;a=snapshot;h=v0.1;sf=tgz' - line = '%s;python_version >= "3"' % url + line = '{};python_version >= "3"'.format(url) req = install_req_from_line(line) assert req.link.url == line, req.url assert req.markers is None @@ -506,7 +506,7 @@ def test_markers_match(self): def test_extras_for_line_path_requirement(self): line = 'SomeProject[ex1,ex2]' filename = 'filename' - comes_from = '-r %s (line %s)' % (filename, 1) + comes_from = '-r {} (line {})'.format(filename, 1) req = install_req_from_line(line, comes_from=comes_from) assert len(req.extras) == 2 assert req.extras == {'ex1', 'ex2'} @@ -514,7 +514,7 @@ def test_extras_for_line_path_requirement(self): def test_extras_for_line_url_requirement(self): line = 'git+https://url#egg=SomeProject[ex1,ex2]' filename = 'filename' - comes_from = '-r %s (line %s)' % (filename, 1) + comes_from = '-r {} (line {})'.format(filename, 1) req = install_req_from_line(line, comes_from=comes_from) assert len(req.extras) == 2 assert req.extras == {'ex1', 'ex2'} @@ -522,7 +522,7 @@ def test_extras_for_line_url_requirement(self): def test_extras_for_editable_path_requirement(self): url = '.[ex1,ex2]' filename = 'filename' - comes_from = '-r %s (line %s)' % (filename, 1) + comes_from = '-r {} (line {})'.format(filename, 1) req = install_req_from_editable(url, comes_from=comes_from) assert len(req.extras) == 2 assert req.extras == {'ex1', 'ex2'} @@ -530,7 +530,7 @@ def test_extras_for_editable_path_requirement(self): def test_extras_for_editable_url_requirement(self): url = 'git+https://url#egg=SomeProject[ex1,ex2]' filename = 'filename' - comes_from = '-r %s (line %s)' % (filename, 1) + comes_from = '-r {} (line {})'.format(filename, 1) req = install_req_from_editable(url, comes_from=comes_from) assert len(req.extras) == 2 assert req.extras == {'ex1', 'ex2'} diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py index 597fc2f82f2..846aeaeff09 100644 --- a/tests/unit/test_req_file.py +++ b/tests/unit/test_req_file.py @@ -249,21 +249,21 @@ def test_error_message(self, line_processor): def test_yield_line_requirement(self, line_processor): line = 'SomeProject' filename = 'filename' - comes_from = '-r %s (line %s)' % (filename, 1) + comes_from = '-r {} (line {})'.format(filename, 1) req = install_req_from_line(line, comes_from=comes_from) assert repr(line_processor(line, filename, 1)[0]) == repr(req) def test_yield_pep440_line_requirement(self, line_processor): line = 'SomeProject @ https://url/SomeProject-py2-py3-none-any.whl' filename = 'filename' - comes_from = '-r %s (line %s)' % (filename, 1) + comes_from = '-r {} (line {})'.format(filename, 1) req = install_req_from_line(line, comes_from=comes_from) assert repr(line_processor(line, filename, 1)[0]) == repr(req) def test_yield_line_constraint(self, line_processor): line = 'SomeProject' filename = 'filename' - comes_from = '-c %s (line %s)' % (filename, 1) + comes_from = '-c {} (line {})'.format(filename, 1) req = install_req_from_line( line, comes_from=comes_from, constraint=True) found_req = line_processor(line, filename, 1, constraint=True)[0] @@ -275,7 +275,7 @@ def test_yield_line_requirement_with_spaces_in_specifier( ): line = 'SomeProject >= 2' filename = 'filename' - comes_from = '-r %s (line %s)' % (filename, 1) + comes_from = '-r {} (line {})'.format(filename, 1) req = install_req_from_line(line, comes_from=comes_from) assert repr(line_processor(line, filename, 1)[0]) == repr(req) assert str(req.req.specifier) == '>=2' @@ -284,15 +284,15 @@ def test_yield_editable_requirement(self, line_processor): url = 'git+https://url#egg=SomeProject' line = '-e %s' % url filename = 'filename' - comes_from = '-r %s (line %s)' % (filename, 1) + comes_from = '-r {} (line {})'.format(filename, 1) req = install_req_from_editable(url, comes_from=comes_from) assert repr(line_processor(line, filename, 1)[0]) == repr(req) def test_yield_editable_constraint(self, line_processor): url = 'git+https://url#egg=SomeProject' - line = '-e %s' % url + line = '-e {}'.format(url) filename = 'filename' - comes_from = '-c %s (line %s)' % (filename, 1) + comes_from = '-c {} (line {})'.format(filename, 1) req = install_req_from_editable( url, comes_from=comes_from, constraint=True) found_req = line_processor(line, filename, 1, constraint=True)[0] diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 011543bdcc5..fa042519ee9 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -66,11 +66,11 @@ def setup(self): self.user_site = 'USER_SITE' self.user_site_egglink = os.path.join( self.user_site, - '%s.egg-link' % project + '{}.egg-link'.format(project) ) self.site_packages_egglink = os.path.join( self.site_packages, - '%s.egg-link' % project, + '{}.egg-link'.format(project), ) # patches diff --git a/tests/unit/test_utils_unpacking.py b/tests/unit/test_utils_unpacking.py index af9ae1c0e71..d01ffb9cd0b 100644 --- a/tests/unit/test_utils_unpacking.py +++ b/tests/unit/test_utils_unpacking.py @@ -77,7 +77,7 @@ def confirm_files(self): continue mode = self.mode(path) assert mode == expected_mode, ( - "mode: %s, expected mode: %s" % (mode, expected_mode) + "mode: {}, expected mode: {}".format(mode, expected_mode) ) def make_zip_file(self, filename, file_list): From 9d281112e356ba3c40c10d523f48e421bf97a0ce Mon Sep 17 00:00:00 2001 From: Reece Dunham <me@rdil.rocks> Date: Fri, 21 Feb 2020 22:39:08 +0000 Subject: [PATCH 1306/3170] Added copyright doc Signed-off-by: Reece Dunham <me@rdil.rocks> --- docs/html/copyright.rst | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 docs/html/copyright.rst diff --git a/docs/html/copyright.rst b/docs/html/copyright.rst new file mode 100644 index 00000000000..b860e2fb25a --- /dev/null +++ b/docs/html/copyright.rst @@ -0,0 +1,7 @@ +========= +Copyright +========= + +pip and this documentation is: + +Copyright © 2008-2020 The pip developers (see `AUTHORS.txt <https://github.com/pypa/pip/blob/master/AUTHORS.txt>`_ file). All rights reserved. From 7514fb2a1017db49151dda305348ebe5c819c01f Mon Sep 17 00:00:00 2001 From: Reece Dunham <me@rdil.rocks> Date: Fri, 21 Feb 2020 22:45:45 +0000 Subject: [PATCH 1307/3170] News fragment Signed-off-by: Reece Dunham <me@rdil.rocks> --- news/7767.doc | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/7767.doc diff --git a/news/7767.doc b/news/7767.doc new file mode 100644 index 00000000000..7bc10b06975 --- /dev/null +++ b/news/7767.doc @@ -0,0 +1 @@ +Added "copyright" page to documentation. From e093938d15599cbe7f42fc820f641ef613d14159 Mon Sep 17 00:00:00 2001 From: Reece Dunham <me@rdil.rocks> Date: Fri, 21 Feb 2020 22:50:46 +0000 Subject: [PATCH 1308/3170] Bump year Signed-off-by: Reece Dunham <me@rdil.rocks> --- docs/html/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/html/conf.py b/docs/html/conf.py index aae7ab12df1..bd44b9bff9d 100644 --- a/docs/html/conf.py +++ b/docs/html/conf.py @@ -54,7 +54,7 @@ # General information about the project. project = 'pip' -copyright = '2008-2017, PyPA' +copyright = '2008-2020, PyPA' # The version info for the project you're documenting, acts as replacement for # |version| and |release|, also used in various other places throughout the From aa4823341a5634d1966b0555297ff4c5dd149294 Mon Sep 17 00:00:00 2001 From: Reece Dunham <me@rdil.rocks> Date: Sat, 22 Feb 2020 08:49:39 -0500 Subject: [PATCH 1309/3170] Update news/7767.doc Co-Authored-By: Pradyun Gedam <pradyunsg@gmail.com> --- news/7767.doc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/7767.doc b/news/7767.doc index 7bc10b06975..0803a314974 100644 --- a/news/7767.doc +++ b/news/7767.doc @@ -1 +1 @@ -Added "copyright" page to documentation. +Add a "Copyright" page. From 3fb48183c3fc6bc84c8f4cfcaa5adc73f7596b73 Mon Sep 17 00:00:00 2001 From: Reece Dunham <me@rdil.rocks> Date: Sat, 22 Feb 2020 10:10:28 -0500 Subject: [PATCH 1310/3170] Add :orphan: --- docs/html/copyright.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/html/copyright.rst b/docs/html/copyright.rst index b860e2fb25a..fd0212f53ec 100644 --- a/docs/html/copyright.rst +++ b/docs/html/copyright.rst @@ -1,3 +1,5 @@ +:orphan: + ========= Copyright ========= From 2b6fb95ba46e712071282126e8fc693890bcd588 Mon Sep 17 00:00:00 2001 From: Christopher Hunt <chrahunt@gmail.com> Date: Sun, 23 Feb 2020 23:44:13 +0800 Subject: [PATCH 1311/3170] Reorder imports --- src/pip/_internal/req/req_file.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index ec47abab9f7..cdb61a4ca28 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -29,7 +29,7 @@ if MYPY_CHECK_RUNNING: from optparse import Values from typing import ( - Any, Callable, Iterator, List, NoReturn, Optional, Text, Tuple, Dict, + Any, Callable, Dict, Iterator, List, NoReturn, Optional, Text, Tuple, ) from pip._internal.index.package_finder import PackageFinder From ccc6d77a7334801a1ae0a138e5e787f4f4425d1d Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 23 Feb 2020 14:25:46 -0500 Subject: [PATCH 1312/3170] Do not update `no_clean` option value on PreviousBuildDirError Since a recent refactoring of our temporary directory handling, `no_clean` is only read prior to these `except` blocks. Since it's not needed, remove it! --- src/pip/_internal/commands/install.py | 9 +-------- src/pip/_internal/commands/wheel.py | 7 ++----- 2 files changed, 3 insertions(+), 13 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 242df7d820e..28b255677f0 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -23,11 +23,7 @@ from pip._internal.cli.cmdoptions import make_target_python from pip._internal.cli.req_command import RequirementCommand, with_cleanup from pip._internal.cli.status_codes import ERROR, SUCCESS -from pip._internal.exceptions import ( - CommandError, - InstallationError, - PreviousBuildDirError, -) +from pip._internal.exceptions import CommandError, InstallationError from pip._internal.locations import distutils_scheme from pip._internal.operations.check import check_install_conflicts from pip._internal.req import install_given_reqs @@ -438,9 +434,6 @@ def run(self, options, args): logger.error(message, exc_info=show_traceback) return ERROR - except PreviousBuildDirError: - options.no_clean = True - raise if options.target_dir: self._handle_target_dir( diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index a5bf28c9427..d6fc81124a4 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -12,7 +12,7 @@ from pip._internal.cache import WheelCache from pip._internal.cli import cmdoptions from pip._internal.cli.req_command import RequirementCommand, with_cleanup -from pip._internal.exceptions import CommandError, PreviousBuildDirError +from pip._internal.exceptions import CommandError from pip._internal.req.req_tracker import get_requirement_tracker from pip._internal.utils.misc import ensure_dir, normalize_path from pip._internal.utils.temp_dir import TempDirectory @@ -127,7 +127,7 @@ def run(self, options, args): with get_requirement_tracker() as req_tracker, TempDirectory( options.build_dir, delete=build_delete, kind="wheel" ) as directory: - try: + if True: # Keep block indented temporarily, for a cleaner commit reqs = self.get_requirements( args, options, finder, session, wheel_cache @@ -186,6 +186,3 @@ def run(self, options, args): raise CommandError( "Failed to build one or more wheels" ) - except PreviousBuildDirError: - options.no_clean = True - raise From 83b0295be93dacf46d0501a25dc5ad39443ec31b Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 23 Feb 2020 14:27:54 -0500 Subject: [PATCH 1313/3170] Dedent block --- src/pip/_internal/commands/wheel.py | 115 ++++++++++++++-------------- 1 file changed, 57 insertions(+), 58 deletions(-) diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index d6fc81124a4..aacc51ab575 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -127,62 +127,61 @@ def run(self, options, args): with get_requirement_tracker() as req_tracker, TempDirectory( options.build_dir, delete=build_delete, kind="wheel" ) as directory: - if True: # Keep block indented temporarily, for a cleaner commit - reqs = self.get_requirements( - args, options, finder, session, - wheel_cache - ) - - preparer = self.make_requirement_preparer( - temp_build_dir=directory, - options=options, - req_tracker=req_tracker, - session=session, - finder=finder, - wheel_download_dir=options.wheel_dir, - use_user_site=False, - ) - - resolver = self.make_resolver( - preparer=preparer, - finder=finder, - options=options, - wheel_cache=wheel_cache, - ignore_requires_python=options.ignore_requires_python, - use_pep517=options.use_pep517, - ) - - self.trace_basic_info(finder) - - requirement_set = resolver.resolve( - reqs, check_supported_wheels=True - ) - - reqs_to_build = [ - r for r in requirement_set.requirements.values() - if should_build_for_wheel_command(r) - ] - - # build wheels - build_successes, build_failures = build( - reqs_to_build, - wheel_cache=wheel_cache, - build_options=options.build_options or [], - global_options=options.global_options or [], - ) - for req in build_successes: - assert req.link and req.link.is_wheel - assert req.local_file_path - # copy from cache to target directory - try: - shutil.copy(req.local_file_path, options.wheel_dir) - except OSError as e: - logger.warning( - "Building wheel for %s failed: %s", - req.name, e, - ) - build_failures.append(req) - if len(build_failures) != 0: - raise CommandError( - "Failed to build one or more wheels" + reqs = self.get_requirements( + args, options, finder, session, + wheel_cache + ) + + preparer = self.make_requirement_preparer( + temp_build_dir=directory, + options=options, + req_tracker=req_tracker, + session=session, + finder=finder, + wheel_download_dir=options.wheel_dir, + use_user_site=False, + ) + + resolver = self.make_resolver( + preparer=preparer, + finder=finder, + options=options, + wheel_cache=wheel_cache, + ignore_requires_python=options.ignore_requires_python, + use_pep517=options.use_pep517, + ) + + self.trace_basic_info(finder) + + requirement_set = resolver.resolve( + reqs, check_supported_wheels=True + ) + + reqs_to_build = [ + r for r in requirement_set.requirements.values() + if should_build_for_wheel_command(r) + ] + + # build wheels + build_successes, build_failures = build( + reqs_to_build, + wheel_cache=wheel_cache, + build_options=options.build_options or [], + global_options=options.global_options or [], + ) + for req in build_successes: + assert req.link and req.link.is_wheel + assert req.local_file_path + # copy from cache to target directory + try: + shutil.copy(req.local_file_path, options.wheel_dir) + except OSError as e: + logger.warning( + "Building wheel for %s failed: %s", + req.name, e, ) + build_failures.append(req) + if len(build_failures) != 0: + raise CommandError( + "Failed to build one or more wheels" + ) From eb91e8ca39dd59e41f4be51f955f52413004533a Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 23 Feb 2020 14:37:01 -0500 Subject: [PATCH 1314/3170] Use Command context helper to cleanup requirement tracker The lifetime of the requirement tracker will be essentially the same, but now we have more flexibility on where in the code we create it. In a followup we can do the same thing with build_dir and remove the `with` statement (and its indentation) entirely from these commands. --- src/pip/_internal/commands/download.py | 4 +++- src/pip/_internal/commands/install.py | 4 +++- src/pip/_internal/commands/wheel.py | 4 +++- 3 files changed, 9 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index 69c7c9a60e6..edd33434688 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -98,7 +98,9 @@ def run(self, options, args): ) build_delete = (not (options.no_clean or options.build_dir)) - with get_requirement_tracker() as req_tracker, TempDirectory( + req_tracker = self.enter_context(get_requirement_tracker()) + + with TempDirectory( options.build_dir, delete=build_delete, kind="download" ) as directory: reqs = self.get_requirements( diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 242df7d820e..afa2c24d05b 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -288,7 +288,9 @@ def run(self, options, args): build_delete = (not (options.no_clean or options.build_dir)) wheel_cache = WheelCache(options.cache_dir, options.format_control) - with get_requirement_tracker() as req_tracker, TempDirectory( + req_tracker = self.enter_context(get_requirement_tracker()) + + with TempDirectory( options.build_dir, delete=build_delete, kind="install" ) as directory: try: diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index a5bf28c9427..7fbcb70f196 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -124,7 +124,9 @@ def run(self, options, args): options.wheel_dir = normalize_path(options.wheel_dir) ensure_dir(options.wheel_dir) - with get_requirement_tracker() as req_tracker, TempDirectory( + req_tracker = self.enter_context(get_requirement_tracker()) + + with TempDirectory( options.build_dir, delete=build_delete, kind="wheel" ) as directory: try: From 40bc4ea8fe743a785698e824bb470b57dd8a2f34 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 23 Feb 2020 16:22:30 -0500 Subject: [PATCH 1315/3170] Move network unit tests to align with subpackage names --- tests/unit/{test_networking_auth.py => test_network_auth.py} | 0 tests/unit/{test_networking_cache.py => test_network_cache.py} | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename tests/unit/{test_networking_auth.py => test_network_auth.py} (100%) rename tests/unit/{test_networking_cache.py => test_network_cache.py} (100%) diff --git a/tests/unit/test_networking_auth.py b/tests/unit/test_network_auth.py similarity index 100% rename from tests/unit/test_networking_auth.py rename to tests/unit/test_network_auth.py diff --git a/tests/unit/test_networking_cache.py b/tests/unit/test_network_cache.py similarity index 100% rename from tests/unit/test_networking_cache.py rename to tests/unit/test_network_cache.py From e6ed38dbcee8259ee1ecf76b30b41022a8144e9a Mon Sep 17 00:00:00 2001 From: Anudit Nagar <nagaranudit@gmail.com> Date: Mon, 24 Feb 2020 03:26:36 +0530 Subject: [PATCH 1316/3170] Add info log when wheel building is skipped (#7768) --- news/7768.feature | 1 + src/pip/_internal/wheel_builder.py | 4 ++++ 2 files changed, 5 insertions(+) create mode 100644 news/7768.feature diff --git a/news/7768.feature b/news/7768.feature new file mode 100644 index 00000000000..82e600b3dc0 --- /dev/null +++ b/news/7768.feature @@ -0,0 +1 @@ +Indicate when wheel building is skipped, due to lack of the ``wheel`` package. diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index 7c7820d4f26..9aa82b746d5 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -71,6 +71,10 @@ def _should_build( if not req.use_pep517 and not is_wheel_installed(): # we don't build legacy requirements if wheel is not installed + logger.info( + "Could not build wheels for %s," + "since package 'wheel' is not installed.", req.name, + ) return False if req.editable or not req.source_dir: From 45911713dbae9b5221046e8b5aff130b2f6a0393 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 23 Feb 2020 14:52:16 -0500 Subject: [PATCH 1317/3170] Globally manage temp build dir This will let us remove the indentation associated with the `with` statement and eventually refactor these functions more easily. --- src/pip/_internal/commands/download.py | 11 ++++++++--- src/pip/_internal/commands/install.py | 11 ++++++++--- src/pip/_internal/commands/wheel.py | 11 ++++++++--- 3 files changed, 24 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index edd33434688..ab1de191f95 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -100,9 +100,14 @@ def run(self, options, args): req_tracker = self.enter_context(get_requirement_tracker()) - with TempDirectory( - options.build_dir, delete=build_delete, kind="download" - ) as directory: + directory = TempDirectory( + options.build_dir, + delete=build_delete, + kind="download", + globally_managed=True, + ) + + if True: # Temporary, to keep commit clean reqs = self.get_requirements( args, options, diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 931d3248396..350edb2f137 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -286,9 +286,14 @@ def run(self, options, args): req_tracker = self.enter_context(get_requirement_tracker()) - with TempDirectory( - options.build_dir, delete=build_delete, kind="install" - ) as directory: + directory = TempDirectory( + options.build_dir, + delete=build_delete, + kind="install", + globally_managed=True, + ) + + if True: # Temporary, to keep commit clean try: reqs = self.get_requirements( args, options, finder, session, diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index bfe1fbea95e..1f432d14f2c 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -126,9 +126,14 @@ def run(self, options, args): req_tracker = self.enter_context(get_requirement_tracker()) - with TempDirectory( - options.build_dir, delete=build_delete, kind="wheel" - ) as directory: + directory = TempDirectory( + options.build_dir, + delete=build_delete, + kind="wheel", + globally_managed=True, + ) + + if True: # Temporary, to keep commit clean reqs = self.get_requirements( args, options, finder, session, wheel_cache From 60a2fa4dce30eb76f21935ab67eaf2975846a03a Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 23 Feb 2020 14:53:57 -0500 Subject: [PATCH 1318/3170] Inline unconditionally-executed blocks --- src/pip/_internal/commands/download.py | 75 ++++--- src/pip/_internal/commands/install.py | 265 ++++++++++++------------- src/pip/_internal/commands/wheel.py | 105 +++++----- 3 files changed, 221 insertions(+), 224 deletions(-) diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index ab1de191f95..5b3b10da64f 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -107,43 +107,42 @@ def run(self, options, args): globally_managed=True, ) - if True: # Temporary, to keep commit clean - reqs = self.get_requirements( - args, - options, - finder, - session, - None - ) - - preparer = self.make_requirement_preparer( - temp_build_dir=directory, - options=options, - req_tracker=req_tracker, - session=session, - finder=finder, - download_dir=options.download_dir, - use_user_site=False, - ) - - resolver = self.make_resolver( - preparer=preparer, - finder=finder, - options=options, - py_version_info=options.python_version, - ) - - self.trace_basic_info(finder) - - requirement_set = resolver.resolve( - reqs, check_supported_wheels=True - ) - - downloaded = ' '.join([ - req.name for req in requirement_set.requirements.values() - if req.successfully_downloaded - ]) - if downloaded: - write_output('Successfully downloaded %s', downloaded) + reqs = self.get_requirements( + args, + options, + finder, + session, + None + ) + + preparer = self.make_requirement_preparer( + temp_build_dir=directory, + options=options, + req_tracker=req_tracker, + session=session, + finder=finder, + download_dir=options.download_dir, + use_user_site=False, + ) + + resolver = self.make_resolver( + preparer=preparer, + finder=finder, + options=options, + py_version_info=options.python_version, + ) + + self.trace_basic_info(finder) + + requirement_set = resolver.resolve( + reqs, check_supported_wheels=True + ) + + downloaded = ' '.join([ + req.name for req in requirement_set.requirements.values() + if req.successfully_downloaded + ]) + if downloaded: + write_output('Successfully downloaded %s', downloaded) return requirement_set diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 350edb2f137..da652238987 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -293,154 +293,153 @@ def run(self, options, args): globally_managed=True, ) - if True: # Temporary, to keep commit clean - try: - reqs = self.get_requirements( - args, options, finder, session, - wheel_cache, check_supported_wheels=not options.target_dir, - ) + try: + reqs = self.get_requirements( + args, options, finder, session, + wheel_cache, check_supported_wheels=not options.target_dir, + ) - warn_deprecated_install_options( - reqs, options.install_options - ) + warn_deprecated_install_options( + reqs, options.install_options + ) - preparer = self.make_requirement_preparer( - temp_build_dir=directory, - options=options, - req_tracker=req_tracker, - session=session, - finder=finder, - use_user_site=options.use_user_site, - ) - resolver = self.make_resolver( - preparer=preparer, - finder=finder, - options=options, - wheel_cache=wheel_cache, - use_user_site=options.use_user_site, - ignore_installed=options.ignore_installed, - ignore_requires_python=options.ignore_requires_python, - force_reinstall=options.force_reinstall, - upgrade_strategy=upgrade_strategy, - use_pep517=options.use_pep517, - ) + preparer = self.make_requirement_preparer( + temp_build_dir=directory, + options=options, + req_tracker=req_tracker, + session=session, + finder=finder, + use_user_site=options.use_user_site, + ) + resolver = self.make_resolver( + preparer=preparer, + finder=finder, + options=options, + wheel_cache=wheel_cache, + use_user_site=options.use_user_site, + ignore_installed=options.ignore_installed, + ignore_requires_python=options.ignore_requires_python, + force_reinstall=options.force_reinstall, + upgrade_strategy=upgrade_strategy, + use_pep517=options.use_pep517, + ) - self.trace_basic_info(finder) + self.trace_basic_info(finder) - requirement_set = resolver.resolve( - reqs, check_supported_wheels=not options.target_dir - ) + requirement_set = resolver.resolve( + reqs, check_supported_wheels=not options.target_dir + ) - try: - pip_req = requirement_set.get_requirement("pip") - except KeyError: - modifying_pip = None - else: - # If we're not replacing an already installed pip, - # we're not modifying it. - modifying_pip = pip_req.satisfied_by is None - protect_pip_from_modification_on_windows( - modifying_pip=modifying_pip - ) + try: + pip_req = requirement_set.get_requirement("pip") + except KeyError: + modifying_pip = None + else: + # If we're not replacing an already installed pip, + # we're not modifying it. + modifying_pip = pip_req.satisfied_by is None + protect_pip_from_modification_on_windows( + modifying_pip=modifying_pip + ) - check_binary_allowed = get_check_binary_allowed( - finder.format_control + check_binary_allowed = get_check_binary_allowed( + finder.format_control + ) + + reqs_to_build = [ + r for r in requirement_set.requirements.values() + if should_build_for_install_command( + r, check_binary_allowed ) + ] - reqs_to_build = [ - r for r in requirement_set.requirements.values() - if should_build_for_install_command( - r, check_binary_allowed - ) - ] + _, build_failures = build( + reqs_to_build, + wheel_cache=wheel_cache, + build_options=[], + global_options=[], + ) - _, build_failures = build( - reqs_to_build, - wheel_cache=wheel_cache, - build_options=[], - global_options=[], - ) + # If we're using PEP 517, we cannot do a direct install + # so we fail here. + # We don't care about failures building legacy + # requirements, as we'll fall through to a direct + # install for those. + pep517_build_failures = [ + r for r in build_failures if r.use_pep517 + ] + if pep517_build_failures: + raise InstallationError( + "Could not build wheels for {} which use" + " PEP 517 and cannot be installed directly".format( + ", ".join(r.name for r in pep517_build_failures))) + + to_install = resolver.get_installation_order( + requirement_set + ) - # If we're using PEP 517, we cannot do a direct install - # so we fail here. - # We don't care about failures building legacy - # requirements, as we'll fall through to a direct - # install for those. - pep517_build_failures = [ - r for r in build_failures if r.use_pep517 - ] - if pep517_build_failures: - raise InstallationError( - "Could not build wheels for {} which use" - " PEP 517 and cannot be installed directly".format( - ", ".join(r.name for r in pep517_build_failures))) - - 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, + ) - # 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, - ) + 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, + ) + working_set = pkg_resources.WorkingSet(lib_locations) - 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, - ) - working_set = pkg_resources.WorkingSet(lib_locations) - - installed.sort(key=operator.attrgetter('name')) - items = [] - for result in installed: - item = result.name - try: - installed_version = get_installed_version( - result.name, working_set=working_set - ) - if installed_version: - item += '-' + installed_version - except Exception: - pass - items.append(item) - installed_desc = ' '.join(items) - if installed_desc: - write_output( - 'Successfully installed %s', installed_desc, + installed.sort(key=operator.attrgetter('name')) + items = [] + for result in installed: + item = result.name + try: + installed_version = get_installed_version( + result.name, working_set=working_set ) - except EnvironmentError as error: - show_traceback = (self.verbosity >= 1) - - message = create_env_error_message( - error, show_traceback, options.use_user_site, + if installed_version: + item += '-' + installed_version + except Exception: + pass + items.append(item) + installed_desc = ' '.join(items) + if installed_desc: + write_output( + 'Successfully installed %s', installed_desc, ) - logger.error(message, exc_info=show_traceback) + 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 + return ERROR if options.target_dir: self._handle_target_dir( diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index 1f432d14f2c..b60d13e10d6 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -133,62 +133,61 @@ def run(self, options, args): globally_managed=True, ) - if True: # Temporary, to keep commit clean - reqs = self.get_requirements( - args, options, finder, session, - wheel_cache - ) + reqs = self.get_requirements( + args, options, finder, session, + wheel_cache + ) - preparer = self.make_requirement_preparer( - temp_build_dir=directory, - options=options, - req_tracker=req_tracker, - session=session, - finder=finder, - wheel_download_dir=options.wheel_dir, - use_user_site=False, - ) + preparer = self.make_requirement_preparer( + temp_build_dir=directory, + options=options, + req_tracker=req_tracker, + session=session, + finder=finder, + wheel_download_dir=options.wheel_dir, + use_user_site=False, + ) - resolver = self.make_resolver( - preparer=preparer, - finder=finder, - options=options, - wheel_cache=wheel_cache, - ignore_requires_python=options.ignore_requires_python, - use_pep517=options.use_pep517, - ) + resolver = self.make_resolver( + preparer=preparer, + finder=finder, + options=options, + wheel_cache=wheel_cache, + ignore_requires_python=options.ignore_requires_python, + use_pep517=options.use_pep517, + ) - self.trace_basic_info(finder) + self.trace_basic_info(finder) - requirement_set = resolver.resolve( - reqs, check_supported_wheels=True - ) + requirement_set = resolver.resolve( + reqs, check_supported_wheels=True + ) - reqs_to_build = [ - r for r in requirement_set.requirements.values() - if should_build_for_wheel_command(r) - ] - - # build wheels - build_successes, build_failures = build( - reqs_to_build, - wheel_cache=wheel_cache, - build_options=options.build_options or [], - global_options=options.global_options or [], - ) - for req in build_successes: - assert req.link and req.link.is_wheel - assert req.local_file_path - # copy from cache to target directory - try: - shutil.copy(req.local_file_path, options.wheel_dir) - except OSError as e: - logger.warning( - "Building wheel for %s failed: %s", - req.name, e, - ) - build_failures.append(req) - if len(build_failures) != 0: - raise CommandError( - "Failed to build one or more wheels" + reqs_to_build = [ + r for r in requirement_set.requirements.values() + if should_build_for_wheel_command(r) + ] + + # build wheels + build_successes, build_failures = build( + reqs_to_build, + wheel_cache=wheel_cache, + build_options=options.build_options or [], + global_options=options.global_options or [], + ) + for req in build_successes: + assert req.link and req.link.is_wheel + assert req.local_file_path + # copy from cache to target directory + try: + shutil.copy(req.local_file_path, options.wheel_dir) + except OSError as e: + logger.warning( + "Building wheel for %s failed: %s", + req.name, e, ) + build_failures.append(req) + if len(build_failures) != 0: + raise CommandError( + "Failed to build one or more wheels" + ) From 42814f0124c38cf4d577b72780b8da612d015d5f Mon Sep 17 00:00:00 2001 From: Andy Kluger <andykluger@gmail.com> Date: Mon, 24 Feb 2020 15:19:57 -0500 Subject: [PATCH 1319/3170] Fix docs: comment stripping comes _after_ line continuation processing --- docs/html/reference/pip_install.rst | 2 +- news/f7e1f2af-8382-450d-aa7e-9357fcd7c645.trivial | 0 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 news/f7e1f2af-8382-450d-aa7e-9357fcd7c645.trivial diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index c81e43ba49e..538f0ef8901 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -149,7 +149,7 @@ treated as a comment. A line ending in an unescaped ``\`` is treated as a line continuation and the newline following it is effectively ignored. -Comments are stripped *before* line continuations are processed. +Comments are stripped *after* line continuations are processed. To interpret the requirements file in UTF-8 format add a comment ``# -*- coding: utf-8 -*-`` to the first or second line of the file. diff --git a/news/f7e1f2af-8382-450d-aa7e-9357fcd7c645.trivial b/news/f7e1f2af-8382-450d-aa7e-9357fcd7c645.trivial new file mode 100644 index 00000000000..e69de29bb2d From 80b952b6e252bfb4788d469b85953d20ee7cbf98 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 25 Feb 2020 04:06:25 +0530 Subject: [PATCH 1320/3170] Apply suggestions from code review Co-Authored-By: Christopher Hunt <chrahunt@gmail.com> --- docs/html/development/conventions.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/html/development/conventions.rst b/docs/html/development/conventions.rst index 381a50f7d68..4d8998fe3c6 100644 --- a/docs/html/development/conventions.rst +++ b/docs/html/development/conventions.rst @@ -29,7 +29,7 @@ separating words) and have the extension ``.rst``. Encoding -------- -All files in our documentation must be UTF-8 encoding. +All files in our documentation must use UTF-8 encoding. File Layout From fd74d0362c1bbfd2f2f6536ea76b18cc0fdf5845 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 23 Feb 2020 16:17:38 -0500 Subject: [PATCH 1321/3170] Move legacy_resolve to resolution.legacy.resolver This gives us a concrete place to put the new resolver code and resolver-specific modules (`resolution.resolver`). The reason for another level of hierarchy compared to other modules is to allow us to move other modules here as they become implementation details of the legacy resolver. Examples I have in mind are: `req.req_set`, `req.req_install`, `req.constructors`, and `operations.prepare`. --- src/pip/_internal/cli/req_command.py | 2 +- src/pip/_internal/resolution/__init__.py | 0 src/pip/_internal/resolution/legacy/__init__.py | 0 .../{legacy_resolve.py => resolution/legacy/resolver.py} | 0 tests/unit/test_legacy_resolve.py | 4 +++- tests/unit/test_req.py | 2 +- 6 files changed, 5 insertions(+), 3 deletions(-) create mode 100644 src/pip/_internal/resolution/__init__.py create mode 100644 src/pip/_internal/resolution/legacy/__init__.py rename src/pip/_internal/{legacy_resolve.py => resolution/legacy/resolver.py} (100%) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 0aa02ab1f9a..21770df62e2 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -14,7 +14,6 @@ from pip._internal.cli.command_context import CommandContextMixIn from pip._internal.exceptions import CommandError, PreviousBuildDirError from pip._internal.index.package_finder import PackageFinder -from pip._internal.legacy_resolve import Resolver from pip._internal.models.selection_prefs import SelectionPreferences from pip._internal.network.download import Downloader from pip._internal.network.session import PipSession @@ -27,6 +26,7 @@ ) from pip._internal.req.req_file import parse_requirements from pip._internal.req.req_set import RequirementSet +from pip._internal.resolution.legacy.resolver import Resolver from pip._internal.self_outdated_check import ( make_link_collector, pip_self_version_check, diff --git a/src/pip/_internal/resolution/__init__.py b/src/pip/_internal/resolution/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/resolution/legacy/__init__.py b/src/pip/_internal/resolution/legacy/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/legacy_resolve.py b/src/pip/_internal/resolution/legacy/resolver.py similarity index 100% rename from src/pip/_internal/legacy_resolve.py rename to src/pip/_internal/resolution/legacy/resolver.py diff --git a/tests/unit/test_legacy_resolve.py b/tests/unit/test_legacy_resolve.py index e0edccc837b..64968358d86 100644 --- a/tests/unit/test_legacy_resolve.py +++ b/tests/unit/test_legacy_resolve.py @@ -7,7 +7,9 @@ NoneMetadataError, UnsupportedPythonVersion, ) -from pip._internal.legacy_resolve import _check_dist_requires_python +from pip._internal.resolution.legacy.resolver import ( + _check_dist_requires_python, +) from pip._internal.utils.packaging import get_requires_python diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index f663fb85f27..7e3feaa5ce7 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -18,7 +18,6 @@ InvalidWheelFilename, PreviousBuildDirError, ) -from pip._internal.legacy_resolve import Resolver from pip._internal.network.download import Downloader from pip._internal.network.session import PipSession from pip._internal.operations.prepare import RequirementPreparer @@ -38,6 +37,7 @@ handle_requirement_line, ) from pip._internal.req.req_tracker import get_requirement_tracker +from pip._internal.resolution.legacy.resolver import Resolver from pip._internal.utils.urls import path_to_url from tests.lib import assert_raises_regexp, make_test_finder, requirements_file From 8362c20da5937065111e4ba1c5855c922fb1d9fd Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 23 Feb 2020 16:18:34 -0500 Subject: [PATCH 1322/3170] Rename legacy resolver unit tests It's a little long, but better to be consistent with our other test file naming conventions until we come up with better ones. --- ...{test_legacy_resolve.py => test_resolution_legacy_resolver.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/unit/{test_legacy_resolve.py => test_resolution_legacy_resolver.py} (100%) diff --git a/tests/unit/test_legacy_resolve.py b/tests/unit/test_resolution_legacy_resolver.py similarity index 100% rename from tests/unit/test_legacy_resolve.py rename to tests/unit/test_resolution_legacy_resolver.py From db377cec280df94c953d60672fb12b18a9e25a23 Mon Sep 17 00:00:00 2001 From: Xavier Fernandez <xavier.fernandez@polyconseil.fr> Date: Tue, 25 Feb 2020 23:49:11 +0100 Subject: [PATCH 1323/3170] vendoring: move /etc appdir patch to pip wrapper --- src/pip/_internal/utils/appdirs.py | 5 ++++- src/pip/_vendor/appdirs.py | 2 -- tools/automation/vendoring/patches/appdirs.patch | 4 +--- 3 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/utils/appdirs.py b/src/pip/_internal/utils/appdirs.py index 251c5fd59a5..93d17b5a81b 100644 --- a/src/pip/_internal/utils/appdirs.py +++ b/src/pip/_internal/utils/appdirs.py @@ -33,9 +33,12 @@ def user_data_dir(appname, roaming=False): return _appdirs.user_data_dir(appname, appauthor=False, roaming=roaming) +# for the discussion regarding site_config_dir locations +# see <https://github.com/pypa/pip/issues/1733> def site_config_dirs(appname): # type: (str) -> List[str] dirval = _appdirs.site_config_dir(appname, appauthor=False, multipath=True) if _appdirs.system not in ["win32", "darwin"]: - return dirval.split(os.pathsep) + # always look in /etc directly as well + return dirval.split(os.pathsep) + ['/etc'] return [dirval] diff --git a/src/pip/_vendor/appdirs.py b/src/pip/_vendor/appdirs.py index 3a52b75846b..cf37f9820c2 100644 --- a/src/pip/_vendor/appdirs.py +++ b/src/pip/_vendor/appdirs.py @@ -257,8 +257,6 @@ def site_config_dir(appname=None, appauthor=None, version=None, multipath=False) if version: appname = os.path.join(appname, version) pathlist = [os.path.join(x, appname) for x in pathlist] - # always look in /etc directly as well - pathlist.append('/etc') if multipath: path = os.pathsep.join(pathlist) diff --git a/tools/automation/vendoring/patches/appdirs.patch b/tools/automation/vendoring/patches/appdirs.patch index a6135c35f9e..fd3c200ac8a 100644 --- a/tools/automation/vendoring/patches/appdirs.patch +++ b/tools/automation/vendoring/patches/appdirs.patch @@ -51,7 +51,7 @@ index ae67001a..3a52b758 100644 def site_config_dir(appname=None, appauthor=None, version=None, multipath=False): r"""Return full path to the user-shared data dir for this application. -@@ -238,14 +248,17 @@ def site_config_dir(appname=None, appauthor=None, version=None, multipath=False) +@@ -238,14 +248,15 @@ def site_config_dir(appname=None, appauthor=None, version=None, multipath=False) if appname and version: path = os.path.join(path, version) else: @@ -68,8 +68,6 @@ index ae67001a..3a52b758 100644 appname = os.path.join(appname, version) - pathlist = [os.sep.join([x, appname]) for x in pathlist] + pathlist = [os.path.join(x, appname) for x in pathlist] -+ # always look in /etc directly as well -+ pathlist.append('/etc') if multipath: path = os.pathsep.join(pathlist) From 22bbc67bddbc75ec827c34335f23f642b563d815 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Wed, 26 Feb 2020 10:20:21 -0800 Subject: [PATCH 1324/3170] Move pep425tags -> utils.compatibility_tags --- .../_internal/{pep425tags.py => utils/compatibility_tags.py} | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) rename src/pip/_internal/{pep425tags.py => utils/compatibility_tags.py} (98%) diff --git a/src/pip/_internal/pep425tags.py b/src/pip/_internal/utils/compatibility_tags.py similarity index 98% rename from src/pip/_internal/pep425tags.py rename to src/pip/_internal/utils/compatibility_tags.py index a2386ee75b8..47d04f078c1 100644 --- a/src/pip/_internal/pep425tags.py +++ b/src/pip/_internal/utils/compatibility_tags.py @@ -1,4 +1,6 @@ -"""Generate and work with PEP 425 Compatibility Tags.""" +"""Generate and work with PEP 425 Compatibility Tags. +""" + from __future__ import absolute_import import logging From 3fa356a772f7ecf691877701cbfd4b37786fd023 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Wed, 26 Feb 2020 10:24:43 -0800 Subject: [PATCH 1325/3170] Update references to pep425tags --- src/pip/_internal/models/target_python.py | 9 +++++--- src/pip/_internal/req/req_install.py | 4 ++-- src/pip/_internal/req/req_set.py | 4 ++-- tests/functional/test_debug.py | 4 ++-- tests/unit/test_finder.py | 4 ++-- tests/unit/test_index.py | 2 +- tests/unit/test_models_wheel.py | 22 +++++++++---------- ...gs.py => test_utils_compatibility_tags.py} | 14 ++++++------ 8 files changed, 33 insertions(+), 30 deletions(-) rename tests/unit/{test_pep425tags.py => test_utils_compatibility_tags.py} (85%) diff --git a/src/pip/_internal/models/target_python.py b/src/pip/_internal/models/target_python.py index 97ae85a0945..84f1c209c66 100644 --- a/src/pip/_internal/models/target_python.py +++ b/src/pip/_internal/models/target_python.py @@ -1,6 +1,9 @@ import sys -from pip._internal.pep425tags import get_supported, version_info_to_nodot +from pip._internal.utils.compatibility_tags import ( + get_supported, + version_info_to_nodot, +) from pip._internal.utils.misc import normalize_version_info from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -34,10 +37,10 @@ def __init__( :param py_version_info: An optional tuple of ints representing the Python version information to use (e.g. `sys.version_info[:3]`). This can have length 1, 2, or 3 when provided. - :param abi: A string or None. This is passed to pep425tags.py's + :param abi: A string or None. This is passed to compatibility_tags.py's get_supported() function as is. :param implementation: A string or None. This is passed to - pep425tags.py's get_supported() function as is. + compatibility_tags.py's get_supported() function as is. """ # Store the given py_version_info for when we call get_supported(). self._given_py_version_info = py_version_info diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 5926a17de89..e749d9ecce3 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -16,7 +16,6 @@ from pip._vendor.packaging.version import parse as parse_version from pip._vendor.pep517.wrappers import Pep517HookCaller -from pip._internal import pep425tags from pip._internal.build_env import NoOpBuildEnvironment from pip._internal.exceptions import InstallationError from pip._internal.locations import get_scheme @@ -31,6 +30,7 @@ from pip._internal.operations.install.wheel import install_wheel from pip._internal.pyproject import load_pyproject_toml, make_pyproject_path from pip._internal.req.req_uninstall import UninstallPathSet +from pip._internal.utils import compatibility_tags from pip._internal.utils.deprecation import deprecated from pip._internal.utils.hashes import Hashes from pip._internal.utils.logging import indent_log @@ -258,7 +258,7 @@ def populate_link(self, finder, upgrade, require_hashes): self.link = finder.find_requirement(self, upgrade) if self._wheel_cache is not None and not require_hashes: old_link = self.link - supported_tags = pep425tags.get_supported() + supported_tags = compatibility_tags.get_supported() self.link = self._wheel_cache.get( link=self.link, package_name=self.name, diff --git a/src/pip/_internal/req/req_set.py b/src/pip/_internal/req/req_set.py index dc76cce9b30..462605248b4 100644 --- a/src/pip/_internal/req/req_set.py +++ b/src/pip/_internal/req/req_set.py @@ -8,9 +8,9 @@ from pip._vendor.packaging.utils import canonicalize_name -from pip._internal import pep425tags from pip._internal.exceptions import InstallationError from pip._internal.models.wheel import Wheel +from pip._internal.utils import compatibility_tags from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: @@ -102,7 +102,7 @@ def add_requirement( # single requirements file. if install_req.link and install_req.link.is_wheel: wheel = Wheel(install_req.link.filename) - tags = pep425tags.get_supported() + tags = compatibility_tags.get_supported() if (self.check_supported_wheels and not wheel.supported(tags)): raise InstallationError( "{} is not a supported wheel on this platform.".format( diff --git a/tests/functional/test_debug.py b/tests/functional/test_debug.py index 1d3509c7b72..29cc3429522 100644 --- a/tests/functional/test_debug.py +++ b/tests/functional/test_debug.py @@ -1,6 +1,6 @@ import pytest -from pip._internal import pep425tags +from pip._internal.utils import compatibility_tags @pytest.mark.parametrize('expected_text', [ @@ -42,7 +42,7 @@ def test_debug__tags(script, args): result = script.pip(*args, allow_stderr_warning=True) stdout = result.stdout - tags = pep425tags.get_supported() + tags = compatibility_tags.get_supported() expected_tag_header = 'Compatible tags: {}'.format(len(tags)) assert expected_tag_header in stdout diff --git a/tests/unit/test_finder.py b/tests/unit/test_finder.py index fb2326c82d5..f8c143bc74f 100644 --- a/tests/unit/test_finder.py +++ b/tests/unit/test_finder.py @@ -7,7 +7,7 @@ from pip._vendor.packaging.tags import Tag from pkg_resources import parse_version -import pip._internal.pep425tags +import pip._internal.utils.compatibility_tags from pip._internal.exceptions import ( BestVersionAlreadyInstalled, DistributionNotFound, @@ -171,7 +171,7 @@ def test_find_wheel_supported(self, data, monkeypatch): Test finding supported wheel. """ monkeypatch.setattr( - pip._internal.pep425tags, + pip._internal.utils.compatibility_tags, "get_supported", lambda **kw: [('py2', 'none', 'any')], ) diff --git a/tests/unit/test_index.py b/tests/unit/test_index.py index bc86398eecf..24dff7b38b0 100644 --- a/tests/unit/test_index.py +++ b/tests/unit/test_index.py @@ -21,7 +21,7 @@ from pip._internal.models.selection_prefs import SelectionPreferences from pip._internal.models.target_python import TargetPython from pip._internal.network.session import PipSession -from pip._internal.pep425tags import get_supported +from pip._internal.utils.compatibility_tags import get_supported from pip._internal.utils.hashes import Hashes from tests.lib import CURRENT_PY_VERSION_INFO diff --git a/tests/unit/test_models_wheel.py b/tests/unit/test_models_wheel.py index f74dd367954..f1fef6f09e8 100644 --- a/tests/unit/test_models_wheel.py +++ b/tests/unit/test_models_wheel.py @@ -1,9 +1,9 @@ import pytest from pip._vendor.packaging.tags import Tag -from pip._internal import pep425tags from pip._internal.exceptions import InvalidWheelFilename from pip._internal.models.wheel import Wheel +from pip._internal.utils import compatibility_tags class TestWheelFile(object): @@ -75,7 +75,7 @@ def test_supported_osx_version(self): """ Wheels built for macOS 10.6 are supported on 10.9 """ - tags = pep425tags.get_supported( + tags = compatibility_tags.get_supported( '27', platform='macosx_10_9_intel', impl='cp' ) w = Wheel('simple-0.1-cp27-none-macosx_10_6_intel.whl') @@ -87,7 +87,7 @@ def test_not_supported_osx_version(self): """ Wheels built for macOS 10.9 are not supported on 10.6 """ - tags = pep425tags.get_supported( + tags = compatibility_tags.get_supported( '27', platform='macosx_10_6_intel', impl='cp' ) w = Wheel('simple-0.1-cp27-none-macosx_10_9_intel.whl') @@ -97,22 +97,22 @@ def test_supported_multiarch_darwin(self): """ Multi-arch wheels (intel) are supported on components (i386, x86_64) """ - universal = pep425tags.get_supported( + universal = compatibility_tags.get_supported( '27', platform='macosx_10_5_universal', impl='cp' ) - intel = pep425tags.get_supported( + intel = compatibility_tags.get_supported( '27', platform='macosx_10_5_intel', impl='cp' ) - x64 = pep425tags.get_supported( + x64 = compatibility_tags.get_supported( '27', platform='macosx_10_5_x86_64', impl='cp' ) - i386 = pep425tags.get_supported( + i386 = compatibility_tags.get_supported( '27', platform='macosx_10_5_i386', impl='cp' ) - ppc = pep425tags.get_supported( + ppc = compatibility_tags.get_supported( '27', platform='macosx_10_5_ppc', impl='cp' ) - ppc64 = pep425tags.get_supported( + ppc64 = compatibility_tags.get_supported( '27', platform='macosx_10_5_ppc64', impl='cp' ) @@ -135,10 +135,10 @@ def test_not_supported_multiarch_darwin(self): """ Single-arch wheels (x86_64) are not supported on multi-arch (intel) """ - universal = pep425tags.get_supported( + universal = compatibility_tags.get_supported( '27', platform='macosx_10_5_universal', impl='cp' ) - intel = pep425tags.get_supported( + intel = compatibility_tags.get_supported( '27', platform='macosx_10_5_intel', impl='cp' ) diff --git a/tests/unit/test_pep425tags.py b/tests/unit/test_utils_compatibility_tags.py similarity index 85% rename from tests/unit/test_pep425tags.py rename to tests/unit/test_utils_compatibility_tags.py index 71d0aefe4f9..12c8da453d9 100644 --- a/tests/unit/test_pep425tags.py +++ b/tests/unit/test_utils_compatibility_tags.py @@ -3,7 +3,7 @@ import pytest from mock import patch -from pip._internal import pep425tags +from pip._internal.utils import compatibility_tags @pytest.mark.parametrize('version_info, expected', [ @@ -17,11 +17,11 @@ ((3, 10), '310'), ]) def test_version_info_to_nodot(version_info, expected): - actual = pep425tags.version_info_to_nodot(version_info) + actual = compatibility_tags.version_info_to_nodot(version_info) assert actual == expected -class TestPEP425Tags(object): +class Testcompatibility_tags(object): def mock_get_config_var(self, **kwd): """ @@ -39,12 +39,12 @@ def test_no_hyphen_tag(self): """ Test that no tag contains a hyphen. """ - import pip._internal.pep425tags + import pip._internal.utils.compatibility_tags mock_gcf = self.mock_get_config_var(SOABI='cpython-35m-darwin') with patch('sysconfig.get_config_var', mock_gcf): - supported = pip._internal.pep425tags.get_supported() + supported = pip._internal.utils.compatibility_tags.get_supported() for tag in supported: assert '-' not in tag.interpreter @@ -63,7 +63,7 @@ def test_manylinux2010_implies_manylinux1(self, manylinux2010, manylinux1): Specifying manylinux2010 implies manylinux1. """ groups = {} - supported = pep425tags.get_supported(platform=manylinux2010) + supported = compatibility_tags.get_supported(platform=manylinux2010) for tag in supported: groups.setdefault( (tag.interpreter, tag.abi), [] @@ -87,7 +87,7 @@ def test_manylinuxA_implies_manylinuxB(self, manylinuxA, manylinuxB): Specifying manylinux2014 implies manylinux2010/manylinux1. """ groups = {} - supported = pep425tags.get_supported(platform=manylinuxA) + supported = compatibility_tags.get_supported(platform=manylinuxA) for tag in supported: groups.setdefault( (tag.interpreter, tag.abi), [] From 8380b2b09affbff8f1ee5ddc466b755fc36306c6 Mon Sep 17 00:00:00 2001 From: sinscary <nbsharma@outlook.com> Date: Mon, 24 Feb 2020 14:28:53 +0530 Subject: [PATCH 1326/3170] Raise error if --user and --target arguments are used together --- news/7249.feature | 1 + src/pip/_internal/cli/main_parser.py | 19 ++++++++++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 news/7249.feature diff --git a/news/7249.feature b/news/7249.feature new file mode 100644 index 00000000000..4548aa006e2 --- /dev/null +++ b/news/7249.feature @@ -0,0 +1 @@ +Raise error if --user and --target are used together in command diff --git a/src/pip/_internal/cli/main_parser.py b/src/pip/_internal/cli/main_parser.py index 4871956c9de..9fd3af49e9b 100644 --- a/src/pip/_internal/cli/main_parser.py +++ b/src/pip/_internal/cli/main_parser.py @@ -83,6 +83,17 @@ def parse_command(args): # the subcommand name cmd_name = args_else[0] + validate_command_args(cmd_name, args_else) + + # all the args without the subcommand + cmd_args = args[:] + cmd_args.remove(cmd_name) + + return cmd_name, cmd_args + + +def validate_command_args(cmd_name, args_else): + # type: (str, List[str]) -> None if cmd_name not in commands_dict: guess = get_similar_commands(cmd_name) @@ -92,8 +103,6 @@ def parse_command(args): raise CommandError(' - '.join(msg)) - # all the args without the subcommand - cmd_args = args[:] - cmd_args.remove(cmd_name) - - return cmd_name, cmd_args + if set(['--user', '--target']).issubset(set(args_else)): + error_msg = '--user and --target cant not be used together.' + raise CommandError(error_msg) From 268a2dd818ed4a539845609a5c0370101548c4e4 Mon Sep 17 00:00:00 2001 From: sinscary <nbsharma@outlook.com> Date: Wed, 26 Feb 2020 14:14:34 +0530 Subject: [PATCH 1327/3170] Revert "Raise error if --user and --target arguments are used together" This reverts commit b6d775d98837a5eaf2cba73a719821c89a71fc8f. --- news/7249.feature | 1 - src/pip/_internal/cli/main_parser.py | 19 +++++-------------- 2 files changed, 5 insertions(+), 15 deletions(-) delete mode 100644 news/7249.feature diff --git a/news/7249.feature b/news/7249.feature deleted file mode 100644 index 4548aa006e2..00000000000 --- a/news/7249.feature +++ /dev/null @@ -1 +0,0 @@ -Raise error if --user and --target are used together in command diff --git a/src/pip/_internal/cli/main_parser.py b/src/pip/_internal/cli/main_parser.py index 9fd3af49e9b..4871956c9de 100644 --- a/src/pip/_internal/cli/main_parser.py +++ b/src/pip/_internal/cli/main_parser.py @@ -83,17 +83,6 @@ def parse_command(args): # the subcommand name cmd_name = args_else[0] - validate_command_args(cmd_name, args_else) - - # all the args without the subcommand - cmd_args = args[:] - cmd_args.remove(cmd_name) - - return cmd_name, cmd_args - - -def validate_command_args(cmd_name, args_else): - # type: (str, List[str]) -> None if cmd_name not in commands_dict: guess = get_similar_commands(cmd_name) @@ -103,6 +92,8 @@ def validate_command_args(cmd_name, args_else): raise CommandError(' - '.join(msg)) - if set(['--user', '--target']).issubset(set(args_else)): - error_msg = '--user and --target cant not be used together.' - raise CommandError(error_msg) + # all the args without the subcommand + cmd_args = args[:] + cmd_args.remove(cmd_name) + + return cmd_name, cmd_args From 716c9202eeecbf4601b39a8ab7bc7085c36e3883 Mon Sep 17 00:00:00 2001 From: sinscary <nbsharma@outlook.com> Date: Thu, 27 Feb 2020 12:22:05 +0530 Subject: [PATCH 1328/3170] Raise error if --user and --target arguments are used together --- news/7249.feature | 1 + src/pip/_internal/commands/install.py | 3 +++ tests/functional/test_install.py | 21 +++++++++++++++++++++ 3 files changed, 25 insertions(+) create mode 100644 news/7249.feature diff --git a/news/7249.feature b/news/7249.feature new file mode 100644 index 00000000000..4548aa006e2 --- /dev/null +++ b/news/7249.feature @@ -0,0 +1 @@ +Raise error if --user and --target are used together in command diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index da652238987..df9681f8c1c 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -237,6 +237,9 @@ def __init__(self, *args, **kw): @with_cleanup def run(self, options, args): # type: (Values, List[Any]) -> int + if options.use_user_site and options.target_dir is not None: + raise CommandError("Can not combine '--user' and '--target'") + cmdoptions.check_install_build_global(options) upgrade_strategy = "to-satisfy-only" if options.upgrade: diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index a95b46741dd..1ea9db26e4b 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -847,6 +847,27 @@ def test_install_package_with_target(script): assert singlemodule_py in result.files_updated, str(result) +def test_install_package_to_usersite_with_target_must_fail(script): + """ + Test that installing package to usersite with target + must raise error + """ + target_dir = script.scratch_path / 'target' + result = script.pip_install_local( + '--user', '-t', target_dir, "simple==1.0", expect_error=True + ) + assert "Can not combine '--user' and '--target'" in result.stderr, ( + str(result) + ) + + result = script.pip_install_local( + '--user', '--target', target_dir, "simple==1.0", expect_error=True + ) + assert "Can not combine '--user' and '--target'" in result.stderr, ( + str(result) + ) + + def test_install_nonlocal_compatible_wheel(script, data): target_dir = script.scratch_path / 'target' From 7e2bab41b9550faf8bc82de170a33fe7f44823d1 Mon Sep 17 00:00:00 2001 From: sinscary <nbsharma@outlook.com> Date: Thu, 27 Feb 2020 14:12:01 +0530 Subject: [PATCH 1329/3170] update newsfile --- news/7249.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/7249.feature b/news/7249.feature index 4548aa006e2..0a791c928df 100644 --- a/news/7249.feature +++ b/news/7249.feature @@ -1 +1 @@ -Raise error if --user and --target are used together in command +Raise error if --user and --target are used together in pip install From 9c57a6b8987003f10d464d273d15195a2593e0f3 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 27 Feb 2020 17:35:20 +0800 Subject: [PATCH 1330/3170] Make tests relying virtualenv semantics pass --- .azure-pipelines/steps/run-tests-windows.yml | 2 +- .azure-pipelines/steps/run-tests.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.azure-pipelines/steps/run-tests-windows.yml b/.azure-pipelines/steps/run-tests-windows.yml index 4908c0ec70a..3832e46621b 100644 --- a/.azure-pipelines/steps/run-tests-windows.yml +++ b/.azure-pipelines/steps/run-tests-windows.yml @@ -25,7 +25,7 @@ steps: Set-Acl "R:\Temp" $acl displayName: Set RAMDisk Permissions -- bash: pip install --upgrade setuptools tox virtualenv +- bash: pip install --upgrade 'virtualenv<20' setuptools tox displayName: Install Tox - script: tox -e py -- -m unit -n auto --junit-xml=junit/unit-test.xml diff --git a/.azure-pipelines/steps/run-tests.yml b/.azure-pipelines/steps/run-tests.yml index 64163a5be8b..11ea2272728 100644 --- a/.azure-pipelines/steps/run-tests.yml +++ b/.azure-pipelines/steps/run-tests.yml @@ -4,7 +4,7 @@ steps: inputs: versionSpec: '$(python.version)' -- bash: pip install --upgrade setuptools tox +- bash: pip install --upgrade 'virtualenv<20' setuptools tox displayName: Install Tox - script: tox -e py -- -m unit -n auto --junit-xml=junit/unit-test.xml From ae80d7d1794235377253a22f3ee3cb734d6d3810 Mon Sep 17 00:00:00 2001 From: Reece Dunham <me@rdil.rocks> Date: Tue, 11 Feb 2020 08:13:48 -0500 Subject: [PATCH 1331/3170] Updated sphinx to 2.4.0 --- tools/requirements/docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/requirements/docs.txt b/tools/requirements/docs.txt index b780ebf649a..c8467b13f61 100644 --- a/tools/requirements/docs.txt +++ b/tools/requirements/docs.txt @@ -1,4 +1,4 @@ -sphinx == 2.3.1 +sphinx == 2.4.0 git+https://github.com/python/python-docs-theme.git#egg=python-docs-theme git+https://github.com/pypa/pypa-docs-theme.git#egg=pypa-docs-theme From a30b024040b875c8ecf310e80c0923d2ad984618 Mon Sep 17 00:00:00 2001 From: Reece Dunham <me@rdil.rocks> Date: Tue, 11 Feb 2020 09:16:48 -0500 Subject: [PATCH 1332/3170] Update tools/requirements/docs.txt Co-Authored-By: Pradyun Gedam <pradyunsg@gmail.com> --- tools/requirements/docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/requirements/docs.txt b/tools/requirements/docs.txt index c8467b13f61..edde3887476 100644 --- a/tools/requirements/docs.txt +++ b/tools/requirements/docs.txt @@ -1,4 +1,4 @@ -sphinx == 2.4.0 +sphinx == 2.4.1 git+https://github.com/python/python-docs-theme.git#egg=python-docs-theme git+https://github.com/pypa/pypa-docs-theme.git#egg=pypa-docs-theme From 07d2966e7bdc59b5cd48e9906a1485aed5d2459a Mon Sep 17 00:00:00 2001 From: Reece Dunham <me@rdil.rocks> Date: Sat, 22 Feb 2020 10:18:35 -0500 Subject: [PATCH 1333/3170] 2.4.3 just came out --- tools/requirements/docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/requirements/docs.txt b/tools/requirements/docs.txt index edde3887476..acbd3390631 100644 --- a/tools/requirements/docs.txt +++ b/tools/requirements/docs.txt @@ -1,4 +1,4 @@ -sphinx == 2.4.1 +sphinx == 2.4.3 git+https://github.com/python/python-docs-theme.git#egg=python-docs-theme git+https://github.com/pypa/pypa-docs-theme.git#egg=pypa-docs-theme From b42a069920d11d169eb1cc84141cb9ff0fcdcab5 Mon Sep 17 00:00:00 2001 From: Xavier Fernandez <xavier.fernandez@polyconseil.fr> Date: Wed, 26 Feb 2020 13:46:55 +0100 Subject: [PATCH 1334/3170] utils.appdirs: drop unused user_data_dir function The vendored appdir's user_data_dir function is used inside user_config_dir which is itself tested. --- src/pip/_internal/utils/appdirs.py | 5 -- tests/unit/test_appdirs.py | 82 ------------------------------ 2 files changed, 87 deletions(-) diff --git a/src/pip/_internal/utils/appdirs.py b/src/pip/_internal/utils/appdirs.py index 93d17b5a81b..f80e4af45b5 100644 --- a/src/pip/_internal/utils/appdirs.py +++ b/src/pip/_internal/utils/appdirs.py @@ -28,11 +28,6 @@ def user_config_dir(appname, roaming=True): return _appdirs.user_config_dir(appname, appauthor=False, roaming=roaming) -def user_data_dir(appname, roaming=False): - # type: (str, bool) -> str - return _appdirs.user_data_dir(appname, appauthor=False, roaming=roaming) - - # for the discussion regarding site_config_dir locations # see <https://github.com/pypa/pip/issues/1733> def site_config_dirs(appname): diff --git a/tests/unit/test_appdirs.py b/tests/unit/test_appdirs.py index 6b2ca8ff196..a329c24aadb 100644 --- a/tests/unit/test_appdirs.py +++ b/tests/unit/test_appdirs.py @@ -147,88 +147,6 @@ def test_site_config_dirs_linux_empty(self, monkeypatch): assert appdirs.site_config_dirs("pip") == ['/etc/xdg/pip', '/etc'] -class TestUserDataDir: - - def test_user_data_dir_win_no_roaming(self, monkeypatch): - @pretend.call_recorder - def _get_win_folder(base): - return "C:\\Users\\test\\AppData\\Local" - - monkeypatch.setattr( - _appdirs, - "_get_win_folder", - _get_win_folder, - raising=False, - ) - monkeypatch.setattr(_appdirs, "system", "win32") - monkeypatch.setattr(os, "path", ntpath) - - assert (appdirs.user_data_dir("pip") == - "C:\\Users\\test\\AppData\\Local\\pip") - assert _get_win_folder.calls == [pretend.call("CSIDL_LOCAL_APPDATA")] - - def test_user_data_dir_win_yes_roaming(self, monkeypatch): - @pretend.call_recorder - def _get_win_folder(base): - return "C:\\Users\\test\\AppData\\Roaming" - - monkeypatch.setattr( - _appdirs, - "_get_win_folder", - _get_win_folder, - raising=False, - ) - monkeypatch.setattr(_appdirs, "system", "win32") - monkeypatch.setattr(os, "path", ntpath) - - assert ( - appdirs.user_data_dir("pip", roaming=True) == - "C:\\Users\\test\\AppData\\Roaming\\pip" - ) - assert _get_win_folder.calls == [pretend.call("CSIDL_APPDATA")] - - def test_user_data_dir_osx(self, monkeypatch): - monkeypatch.setattr(_appdirs, "system", "darwin") - monkeypatch.setattr(os, "path", posixpath) - monkeypatch.setenv("HOME", "/home/test") - monkeypatch.setattr(sys, "platform", "darwin") - - if os.path.isdir('/home/test/Library/Application Support/'): - assert (appdirs.user_data_dir("pip") == - "/home/test/Library/Application Support/pip") - else: - assert (appdirs.user_data_dir("pip") == - "/home/test/.config/pip") - - def test_user_data_dir_linux(self, monkeypatch): - monkeypatch.setattr(_appdirs, "system", "linux2") - monkeypatch.setattr(os, "path", posixpath) - monkeypatch.delenv("XDG_DATA_HOME", raising=False) - monkeypatch.setenv("HOME", "/home/test") - monkeypatch.setattr(sys, "platform", "linux2") - - assert appdirs.user_data_dir("pip") == "/home/test/.local/share/pip" - - def test_user_data_dir_linux_override(self, monkeypatch): - monkeypatch.setattr(_appdirs, "system", "linux2") - monkeypatch.setattr(os, "path", posixpath) - monkeypatch.setenv("XDG_DATA_HOME", "/home/test/.other-share") - monkeypatch.setenv("HOME", "/home/test") - monkeypatch.setattr(sys, "platform", "linux2") - - assert appdirs.user_data_dir("pip") == "/home/test/.other-share/pip" - - def test_user_data_dir_linux_home_slash(self, monkeypatch): - monkeypatch.setattr(_appdirs, "system", "linux2") - monkeypatch.setattr(os, "path", posixpath) - # Verify that we are not affected by https://bugs.python.org/issue14768 - monkeypatch.delenv("XDG_DATA_HOME", raising=False) - monkeypatch.setenv("HOME", "/") - monkeypatch.setattr(sys, "platform", "linux2") - - assert appdirs.user_data_dir("pip") == "/.local/share/pip" - - class TestUserConfigDir: def test_user_config_dir_win_no_roaming(self, monkeypatch): From 3ce1872aaf21745e5c98d1eca2cba1a585e362fd Mon Sep 17 00:00:00 2001 From: Xavier Fernandez <xavier.fernandez@polyconseil.fr> Date: Wed, 26 Feb 2020 13:54:41 +0100 Subject: [PATCH 1335/3170] Actually test user_config_dir in TestUserConfigDir --- tests/unit/test_appdirs.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_appdirs.py b/tests/unit/test_appdirs.py index a329c24aadb..e129c0c0b83 100644 --- a/tests/unit/test_appdirs.py +++ b/tests/unit/test_appdirs.py @@ -194,10 +194,10 @@ def test_user_config_dir_osx(self, monkeypatch): monkeypatch.setattr(sys, "platform", "darwin") if os.path.isdir('/home/test/Library/Application Support/'): - assert (appdirs.user_data_dir("pip") == + assert (appdirs.user_config_dir("pip") == "/home/test/Library/Application Support/pip") else: - assert (appdirs.user_data_dir("pip") == + assert (appdirs.user_config_dir("pip") == "/home/test/.config/pip") def test_user_config_dir_linux(self, monkeypatch): From 63d93b4c7e8e8fe222587cc5ee2baac38f1132b6 Mon Sep 17 00:00:00 2001 From: Albert Tugushev <albert@tugushev.ru> Date: Mon, 2 Mar 2020 22:48:46 +0700 Subject: [PATCH 1336/3170] Fix the docstring of preprocess() function --- src/pip/_internal/req/req_file.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index cdb61a4ca28..2501e7c25fd 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -170,7 +170,7 @@ def preprocess(content, skip_requirements_regex): """Split, filter, and join lines, and return a line iterator :param content: the content of the requirements file - :param options: cli options + :param skip_requirements_regex: the pattern to skip lines """ lines_enum = enumerate(content.splitlines(), start=1) # type: ReqFileLines lines_enum = join_lines(lines_enum) From 958f4ec4325588b0b90464548b09c0a141e93f15 Mon Sep 17 00:00:00 2001 From: KOLANICH <kolan_n@mail.ru> Date: Thu, 13 Feb 2020 12:54:46 +0300 Subject: [PATCH 1337/3170] Added `__repr__` for `Configuration`. --- news/7737.trivial | 1 + src/pip/_internal/configuration.py | 4 ++++ 2 files changed, 5 insertions(+) create mode 100644 news/7737.trivial diff --git a/news/7737.trivial b/news/7737.trivial new file mode 100644 index 00000000000..64b3cc2260c --- /dev/null +++ b/news/7737.trivial @@ -0,0 +1 @@ +Added ``__repr__`` for ``Configuration`` to make debugging a bit easier. diff --git a/src/pip/_internal/configuration.py b/src/pip/_internal/configuration.py index f09a1ae25c2..2648b8af327 100644 --- a/src/pip/_internal/configuration.py +++ b/src/pip/_internal/configuration.py @@ -420,3 +420,7 @@ def _mark_as_modified(self, fname, parser): file_parser_tuple = (fname, parser) if file_parser_tuple not in self._modified_parsers: self._modified_parsers.append(file_parser_tuple) + + def __repr__(self): + # type: () -> str + return "{}({!r})".format(self.__class__.__name__, self._dictionary) From 2c0d6918934a9bc01612ade569ef7f58d40a54f6 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" <jaraco@jaraco.com> Date: Fri, 6 Mar 2020 04:09:48 -0500 Subject: [PATCH 1338/3170] Skip svn tests if svnadmin is not available. Fixes #7823. --- tests/lib/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index c0baa0a1cfb..272bccdf69f 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -1105,7 +1105,9 @@ def need_bzr(fn): def need_svn(fn): return pytest.mark.svn(need_executable( 'Subversion', ('svn', '--version') - )(fn)) + )(need_executable( + 'Subversion Admin', ('svnadmin', '--version') + )(fn))) def need_mercurial(fn): From 3511d3d49350885364d31ca753dcb4aad27101d1 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" <jaraco@jaraco.com> Date: Sat, 29 Feb 2020 13:53:59 -0600 Subject: [PATCH 1339/3170] Convert the remaining '%' formatters to '.format'. Fixes #6973. --- docs/pip_sphinxext.py | 13 +- src/pip/_internal/cli/main_parser.py | 2 +- src/pip/_internal/cli/parser.py | 4 +- src/pip/_internal/cli/progress_bars.py | 31 +++-- src/pip/_internal/cli/req_command.py | 10 +- src/pip/_internal/cli/spinners.py | 2 +- src/pip/_internal/commands/completion.py | 24 ++-- src/pip/_internal/commands/search.py | 5 +- src/pip/_internal/commands/uninstall.py | 4 +- src/pip/_internal/models/link.py | 2 +- src/pip/_internal/operations/install/wheel.py | 9 +- src/pip/_internal/req/constructors.py | 6 +- src/pip/_internal/req/req_install.py | 5 +- src/pip/_internal/req/req_set.py | 2 +- src/pip/_internal/utils/compat.py | 2 +- src/pip/_internal/utils/filesystem.py | 2 +- src/pip/_internal/utils/logging.py | 2 +- src/pip/_internal/utils/misc.py | 8 +- src/pip/_internal/utils/urls.py | 6 +- src/pip/_internal/vcs/subversion.py | 2 +- src/pip/_internal/vcs/versioncontrol.py | 6 +- tests/conftest.py | 4 +- tests/data/src/chattymodule/setup.py | 2 +- tests/functional/test_completion.py | 2 +- tests/functional/test_freeze.py | 34 ++--- tests/functional/test_install.py | 129 +++++++++++------- tests/functional/test_install_cleanup.py | 6 +- tests/functional/test_install_compat.py | 7 +- tests/functional/test_install_config.py | 2 +- tests/functional/test_install_extras.py | 2 +- tests/functional/test_install_reqs.py | 86 ++++++------ tests/functional/test_install_upgrade.py | 91 +++++++----- tests/functional/test_install_user.py | 27 ++-- tests/functional/test_install_vcs_git.py | 2 +- tests/functional/test_pep517.py | 8 +- tests/functional/test_uninstall.py | 13 +- tests/functional/test_uninstall_user.py | 2 +- tests/functional/test_wheel.py | 31 +++-- tests/lib/__init__.py | 68 ++++----- tests/lib/path.py | 2 +- tests/lib/test_lib.py | 4 +- tests/unit/test_build_env.py | 23 ++-- tests/unit/test_collector.py | 4 +- tests/unit/test_req.py | 17 +-- tests/unit/test_req_file.py | 27 ++-- 45 files changed, 415 insertions(+), 325 deletions(-) diff --git a/docs/pip_sphinxext.py b/docs/pip_sphinxext.py index a0e69c6e4c2..29ca34c7289 100644 --- a/docs/pip_sphinxext.py +++ b/docs/pip_sphinxext.py @@ -42,15 +42,16 @@ def run(self): class PipOptions(rst.Directive): def _format_option(self, option, cmd_name=None): - if cmd_name: - bookmark_line = ".. _`%s_%s`:" % (cmd_name, option._long_opts[0]) - else: - bookmark_line = ".. _`%s`:" % option._long_opts[0] + bookmark_line = ( + ".. _`{cmd_name}_{option._long_opts[0]}`:" + if cmd_name else + ".. _`{option._long_opts[0]}`:" + ).format(**locals()) line = ".. option:: " if option._short_opts: line += option._short_opts[0] if option._short_opts and option._long_opts: - line += ", %s" % option._long_opts[0] + line += ", " + option._long_opts[0] elif option._long_opts: line += option._long_opts[0] if option.takes_value(): @@ -60,7 +61,7 @@ def _format_option(self, option, cmd_name=None): opt_help = option.help.replace('%default', str(option.default)) # fix paths with sys.prefix opt_help = opt_help.replace(sys.prefix, "<sys.prefix>") - return [bookmark_line, "", line, "", " %s" % opt_help, ""] + return [bookmark_line, "", line, "", " " + opt_help, ""] def _format_options(self, options, cmd_name=None): for option in options: diff --git a/src/pip/_internal/cli/main_parser.py b/src/pip/_internal/cli/main_parser.py index 4871956c9de..08c82c1f711 100644 --- a/src/pip/_internal/cli/main_parser.py +++ b/src/pip/_internal/cli/main_parser.py @@ -48,7 +48,7 @@ def create_main_parser(): # create command listing for description description = [''] + [ - '%-27s %s' % (name, command_info.summary) + '{name:27} {command_info.summary}'.format(**locals()) for name, command_info in commands_dict.items() ] parser.description = '\n'.join(description) diff --git a/src/pip/_internal/cli/parser.py b/src/pip/_internal/cli/parser.py index e799215bef1..04e00b72132 100644 --- a/src/pip/_internal/cli/parser.py +++ b/src/pip/_internal/cli/parser.py @@ -31,14 +31,14 @@ def __init__(self, *args, **kwargs): optparse.IndentedHelpFormatter.__init__(self, *args, **kwargs) def format_option_strings(self, option): - return self._format_option_strings(option, ' <%s>', ', ') + return self._format_option_strings(option) def _format_option_strings(self, option, mvarfmt=' <{}>', optsep=', '): """ Return a comma-separated list of option strings and metavars. :param option: tuple of (short opt, long opt), e.g: ('-f', '--format') - :param mvarfmt: metavar format string - evaluated as mvarfmt % metavar + :param mvarfmt: metavar format string :param optsep: separator """ opts = [] diff --git a/src/pip/_internal/cli/progress_bars.py b/src/pip/_internal/cli/progress_bars.py index 7ed224790cf..baf40f52373 100644 --- a/src/pip/_internal/cli/progress_bars.py +++ b/src/pip/_internal/cli/progress_bars.py @@ -124,7 +124,7 @@ def update(self): class BlueEmojiBar(IncrementalBar): - suffix = "%(percent)d%%" + suffix = "{percent}%" bar_prefix = " " bar_suffix = " " phases = (u"\U0001F539", u"\U0001F537", u"\U0001F535") # type: Any @@ -203,8 +203,8 @@ class BaseDownloadProgressBar(WindowsMixin, InterruptibleMixin, DownloadProgressMixin): file = sys.stdout - message = "%(percent)d%%" - suffix = "%(downloaded)s %(download_speed)s %(pretty_eta)s" + message = "{percent}%" + suffix = "{downloaded} {download_speed} {pretty_eta}" # NOTE: The "type: ignore" comments on the following classes are there to # work around https://github.com/python/typing/issues/241 @@ -234,11 +234,21 @@ class DownloadBlueEmojiProgressBar(BaseDownloadProgressBar, # type: ignore pass +class ObjectMapAdapter: + """Translate getitem to getattr.""" + + def __init__(self, obj): + self.obj = obj + + def __getitem__(self, key): + return getattr(self.obj, key) + + class DownloadProgressSpinner(WindowsMixin, InterruptibleMixin, DownloadProgressMixin, Spinner): file = sys.stdout - suffix = "%(downloaded)s %(download_speed)s" + suffix = "{downloaded} {download_speed}" def next_phase(self): # type: ignore if not hasattr(self, "_phaser"): @@ -247,17 +257,10 @@ def next_phase(self): # type: ignore def update(self): # type: () -> None - message = self.message % self + message = self.message.format_map(ObjectMapAdapter(self)) phase = self.next_phase() - suffix = self.suffix % self - line = ''.join([ - message, - " " if message else "", - phase, - " " if suffix else "", - suffix, - ]) - + suffix = self.suffix.format_map(ObjectMapAdapter(self)) + line = " ".join(filter(None, (message, phase, suffix))) self.writeln(line) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 21770df62e2..db7b3138a58 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -342,13 +342,13 @@ def get_requirements( opts = {'name': self.name} if 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))) + 'You must give at least one requirement to {name} ' + '(maybe you meant "pip {name} {links}"?)'.format( + **dict(opts, links=' '.join(options.find_links)))) else: raise CommandError( - 'You must give at least one requirement to %(name)s ' - '(see "pip help %(name)s")' % opts) + 'You must give at least one requirement to {name} ' + '(see "pip help {name}")'.format(**opts)) return requirements diff --git a/src/pip/_internal/cli/spinners.py b/src/pip/_internal/cli/spinners.py index 3d152a80742..8e8e44eeb5c 100644 --- a/src/pip/_internal/cli/spinners.py +++ b/src/pip/_internal/cli/spinners.py @@ -106,7 +106,7 @@ def finish(self, final_status): # type: (str) -> None if self._finished: return - self._update("finished with status '%s'" % (final_status,)) + self._update("finished with status '{final_status}'".format(**locals())) self._finished = True diff --git a/src/pip/_internal/commands/completion.py b/src/pip/_internal/commands/completion.py index e0b743e542f..b2a793903a3 100644 --- a/src/pip/_internal/commands/completion.py +++ b/src/pip/_internal/commands/completion.py @@ -10,29 +10,29 @@ from pip._internal.utils.misc import get_prog BASE_COMPLETION = """ -# pip %(shell)s completion start%(script)s# pip %(shell)s completion end +# pip {shell} completion start{script}# pip {shell} completion end """ COMPLETION_SCRIPTS = { 'bash': """ _pip_completion() - { - COMPREPLY=( $( COMP_WORDS="${COMP_WORDS[*]}" \\ + {{ + COMPREPLY=( $( COMP_WORDS="${{COMP_WORDS[*]}}" \\ COMP_CWORD=$COMP_CWORD \\ PIP_AUTO_COMPLETE=1 $1 2>/dev/null ) ) - } - complete -o default -F _pip_completion %(prog)s + }} + complete -o default -F _pip_completion {prog} """, 'zsh': """ - function _pip_completion { + 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] 2>/dev/null )) - } - compctl -K _pip_completion %(prog)s + }} + compctl -K _pip_completion {prog} """, 'fish': """ function __fish_complete_pip @@ -43,7 +43,7 @@ set -lx PIP_AUTO_COMPLETE 1 string split \\ -- (eval $COMP_WORDS[1]) end - complete -fa "(__fish_complete_pip)" -c %(prog)s + complete -fa "(__fish_complete_pip)" -c {prog} """, } @@ -85,11 +85,9 @@ def run(self, options, args): shell_options = ['--' + shell for shell in sorted(shells)] if options.shell in shells: script = textwrap.dedent( - COMPLETION_SCRIPTS.get(options.shell, '') % { - 'prog': get_prog(), - } + COMPLETION_SCRIPTS.get(options.shell, '').format(prog=get_prog()) ) - print(BASE_COMPLETION % {'script': script, 'shell': options.shell}) + print(BASE_COMPLETION.format(script=script, shell=options.shell)) else: sys.stderr.write( 'ERROR: You must pass {}\n' .format(' or '.join(shell_options)) diff --git a/src/pip/_internal/commands/search.py b/src/pip/_internal/commands/search.py index 2e880eec224..e5f286ea5bf 100644 --- a/src/pip/_internal/commands/search.py +++ b/src/pip/_internal/commands/search.py @@ -121,8 +121,9 @@ def print_results(hits, name_column_width=None, terminal_width=None): summary = textwrap.wrap(summary, target_width) summary = ('\n' + ' ' * (name_column_width + 3)).join(summary) - line = '%-*s - %s' % (name_column_width, - '%s (%s)' % (name, latest), summary) + line = '{name_latest:{name_column_width}} - {summary}'.format( + name_latest='{name} ({latest})'.format(**locals()), + **locals()) try: write_output(line) if name in installed_packages: diff --git a/src/pip/_internal/commands/uninstall.py b/src/pip/_internal/commands/uninstall.py index e93ad74e29e..5db4fb46721 100644 --- a/src/pip/_internal/commands/uninstall.py +++ b/src/pip/_internal/commands/uninstall.py @@ -73,8 +73,8 @@ def run(self, options, args): 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) + 'You must give at least one requirement to {self.name} (see ' + '"pip help {self.name}")'.format(**locals()) ) protect_pip_from_modification_on_windows( diff --git a/src/pip/_internal/models/link.py b/src/pip/_internal/models/link.py index 0b30ab2511a..ed057de196b 100644 --- a/src/pip/_internal/models/link.py +++ b/src/pip/_internal/models/link.py @@ -96,7 +96,7 @@ def filename(self): return netloc name = urllib_parse.unquote(name) - assert name, ('URL %r produced no filename' % self._url) + assert name, ('URL {self._url!r} produced no filename'.format(**locals())) return name @property diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index b1a76a24ce2..329d90c67e6 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -17,6 +17,7 @@ import sys import warnings from base64 import urlsafe_b64encode +from itertools import starmap from zipfile import ZipFile from pip._vendor import pkg_resources @@ -534,13 +535,9 @@ def is_entrypoint_wrapper(name): del console[k] # Generate the console and GUI entry points specified in the wheel - scripts_to_generate.extend( - '%s = %s' % kv for kv in console.items() - ) + scripts_to_generate.extend(starmap('{} = {}'.format, console.items())) - gui_scripts_to_generate = [ - '%s = %s' % kv for kv in gui.items() - ] + gui_scripts_to_generate = list(starmap('{} = {}'.format, gui.items())) generated_console_scripts = [] # type: List[str] diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index 395157c4be6..195b0d0be78 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -281,8 +281,8 @@ def _get_url_from_path(path, name): if is_installable_dir(path): return path_to_url(path) raise InstallationError( - "Directory %r is not installable. Neither 'setup.py' " - "nor 'pyproject.toml' found." % name + "Directory {name!r} is not installable. Neither 'setup.py' " + "nor 'pyproject.toml' found.".format(**locals()) ) if not is_archive_file(path): return None @@ -339,7 +339,7 @@ def parse_req_from_line(name, line_source): # wheel file if link.is_wheel: wheel = Wheel(link.filename) # can raise InvalidWheelFilename - req_as_string = "%s==%s" % (wheel.name, wheel.version) + req_as_string = "{wheel.name}=={wheel.version}".format(**locals()) else: # set the req to the egg fragment. when it's not there, this # will become an 'unnamed' requirement diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 5926a17de89..ce0309932cb 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -639,7 +639,7 @@ def update_editable(self, obtain=True): if self.link.scheme == 'file': # Static paths don't get updated return - assert '+' in self.link.url, "bad url: %r" % self.link.url + assert '+' in self.link.url, "bad url: {self.link.url!r}".format(**locals()) vc_type, url = self.link.url.split('+', 1) vcs_backend = vcs.get_backend(vc_type) if vcs_backend: @@ -701,7 +701,8 @@ def _get_archive_name(self, path, parentdir, rootdir): def _clean_zip_name(name, prefix): # type: (str, str) -> str assert name.startswith(prefix + os.path.sep), ( - "name %r doesn't start with prefix %r" % (name, prefix) + "name {name!r} doesn't start with prefix {prefix!r}" + .format(**locals()) ) name = name[len(prefix) + 1:] name = name.replace(os.path.sep, '/') diff --git a/src/pip/_internal/req/req_set.py b/src/pip/_internal/req/req_set.py index dc76cce9b30..bc8e32264a3 100644 --- a/src/pip/_internal/req/req_set.py +++ b/src/pip/_internal/req/req_set.py @@ -194,7 +194,7 @@ def get_requirement(self, name): if project_name in self.requirements: return self.requirements[project_name] - raise KeyError("No project with the name %r" % name) + raise KeyError("No project with the name {name!r}".format(**locals())) @property def all_requirements(self): diff --git a/src/pip/_internal/utils/compat.py b/src/pip/_internal/utils/compat.py index 08c292d189d..d939e21fe2a 100644 --- a/src/pip/_internal/utils/compat.py +++ b/src/pip/_internal/utils/compat.py @@ -64,7 +64,7 @@ def backslashreplace_decode_fn(err): raw_bytes = (err.object[i] for i in range(err.start, err.end)) # 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 + return u"".join(map(u"\\x{:x}".format, raw_bytes)), err.end codecs.register_error( "backslashreplace_decode", backslashreplace_decode_fn, diff --git a/src/pip/_internal/utils/filesystem.py b/src/pip/_internal/utils/filesystem.py index 6f1537e4032..a0866779719 100644 --- a/src/pip/_internal/utils/filesystem.py +++ b/src/pip/_internal/utils/filesystem.py @@ -73,7 +73,7 @@ def copy2_fixed(src, dest): pass else: if is_socket_file: - raise shutil.SpecialFileError("`%s` is a socket" % f) + raise shutil.SpecialFileError("`{f}` is a socket".format(**locals())) raise diff --git a/src/pip/_internal/utils/logging.py b/src/pip/_internal/utils/logging.py index 7767111a6ba..134f7908d9d 100644 --- a/src/pip/_internal/utils/logging.py +++ b/src/pip/_internal/utils/logging.py @@ -156,7 +156,7 @@ def format(self, record): if self.add_timestamp: # TODO: Use Formatter.default_time_format after dropping PY2. t = self.formatTime(record, "%Y-%m-%dT%H:%M:%S") - prefix = '%s,%03d ' % (t, record.msecs) + prefix = '{t},{record.msecs:03.0f} '.format(**locals()) prefix += " " * get_indentation() formatted = "".join([ prefix + line diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 5ad0544be79..a36701a7095 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -266,13 +266,13 @@ def ask_password(message): def format_size(bytes): # type: (float) -> str if bytes > 1000 * 1000: - return '%.1f MB' % (bytes / 1000.0 / 1000) + return '{:.1f} MB'.format(bytes / 1000.0 / 1000) elif bytes > 10 * 1000: - return '%i kB' % (bytes / 1000) + return '{} kB'.format(int(bytes / 1000)) elif bytes > 1000: - return '%.1f kB' % (bytes / 1000.0) + return '{:.1f} kB'.format(bytes / 1000.0) else: - return '%i bytes' % bytes + return '{} bytes'.format(int(bytes)) def is_installable_dir(path): diff --git a/src/pip/_internal/utils/urls.py b/src/pip/_internal/utils/urls.py index 9ad40feb345..a3dcc83b527 100644 --- a/src/pip/_internal/utils/urls.py +++ b/src/pip/_internal/utils/urls.py @@ -34,7 +34,7 @@ def url_to_path(url): Convert a file: URL to a path. """ assert url.startswith('file:'), ( - "You can only turn file: urls into filenames (not %r)" % url) + "You can only turn file: urls into filenames (not {url!r})".format(**locals())) _, netloc, path, _, _ = urllib_parse.urlsplit(url) @@ -46,8 +46,8 @@ def url_to_path(url): netloc = '\\\\' + netloc else: raise ValueError( - 'non-local file URIs are not supported on this platform: %r' - % url + 'non-local file URIs are not supported on this platform: {url!r}' + .format(**locals()) ) path = urllib_request.url2pathname(netloc + path) diff --git a/src/pip/_internal/vcs/subversion.py b/src/pip/_internal/vcs/subversion.py index 6c76d1ad435..064fb77e50e 100644 --- a/src/pip/_internal/vcs/subversion.py +++ b/src/pip/_internal/vcs/subversion.py @@ -151,7 +151,7 @@ def _get_svn_url_rev(cls, location): elif data.startswith('<?xml'): match = _svn_xml_url_re.search(data) if not match: - raise ValueError('Badly formatted data: %r' % data) + raise ValueError('Badly formatted data: {data!r}'.format(**locals())) url = match.group(1) # get repository URL revs = [int(m.group(1)) for m in _svn_rev_re.finditer(data)] + [0] else: diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index 7b6171451ac..da53827cd46 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -683,9 +683,9 @@ def run_command( # In other words, the VCS executable isn't available if e.errno == errno.ENOENT: raise BadCommand( - 'Cannot find command %r - do you have ' - '%r installed and in your ' - 'PATH?' % (cls.name, cls.name)) + 'Cannot find command {cls.name!r} - do you have ' + '{cls.name!r} installed and in your ' + 'PATH?'.format(**locals())) else: raise # re-raise exception if a different error occurred diff --git a/tests/conftest.py b/tests/conftest.py index 3430e271d77..f539bb32be2 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -234,11 +234,11 @@ def not_code_files_and_folders(path, names): def _common_wheel_editable_install(tmpdir_factory, common_wheels, package): - wheel_candidates = list(common_wheels.glob('%s-*.whl' % package)) + wheel_candidates = list(common_wheels.glob('{package}-*.whl'.format(**locals()))) assert len(wheel_candidates) == 1, wheel_candidates install_dir = Path(str(tmpdir_factory.mktemp(package))) / 'install' Wheel(wheel_candidates[0]).install_as_egg(install_dir) - (install_dir / 'EGG-INFO').rename(install_dir / '%s.egg-info' % package) + (install_dir / 'EGG-INFO').rename(install_dir / '{package}.egg-info'.format(**locals())) assert compileall.compile_dir(str(install_dir), quiet=1) return install_dir diff --git a/tests/data/src/chattymodule/setup.py b/tests/data/src/chattymodule/setup.py index 7211972d1a7..01d7720765f 100644 --- a/tests/data/src/chattymodule/setup.py +++ b/tests/data/src/chattymodule/setup.py @@ -5,7 +5,7 @@ from setuptools import setup -print("HELLO FROM CHATTYMODULE %s" % (sys.argv[1],)) +print("HELLO FROM CHATTYMODULE {sys.argv[1]}".format(**locals())) print(os.environ) print(sys.argv) if "--fail" in sys.argv: diff --git a/tests/functional/test_completion.py b/tests/functional/test_completion.py index 4eaf36b4c85..a3986811b6f 100644 --- a/tests/functional/test_completion.py +++ b/tests/functional/test_completion.py @@ -230,7 +230,7 @@ def test_completion_not_files_after_nonexpecting_option( (e.g. ``pip install``) """ res, env = autocomplete( - words=('pip install %s r' % cl_opts), + words=('pip install {cl_opts} r'.format(**locals())), cword='2', cwd=data.completion_paths, ) diff --git a/tests/functional/test_freeze.py b/tests/functional/test_freeze.py index 3542e7754ef..c4842b7ac51 100644 --- a/tests/functional/test_freeze.py +++ b/tests/functional/test_freeze.py @@ -40,7 +40,7 @@ def _check_output(result, expected): actual = distribute_re.sub('', actual) def banner(msg): - return '\n========== %s ==========\n' % msg + return '\n========== {msg} ==========\n'.format(**locals()) assert checker.check_output(expected, actual, ELLIPSIS), ( banner('EXPECTED') + expected + banner('ACTUAL') + actual + @@ -246,15 +246,15 @@ def test_freeze_git_clone(script, tmpdir): _check_output(result.stdout, expected) result = script.pip( - 'freeze', '-f', '%s#egg=pip_test_package' % repo_dir, + 'freeze', '-f', '{repo_dir}#egg=pip_test_package'.format(**locals()), expect_stderr=True, ) expected = textwrap.dedent( """ - -f %(repo)s#egg=pip_test_package... + -f {repo}#egg=pip_test_package... -e git+...#egg=version_pkg ... - """ % {'repo': repo_dir}, + """.format(repo=repo_dir), ).strip() _check_output(result.stdout, expected) @@ -311,15 +311,15 @@ def test_freeze_git_clone_srcdir(script, tmpdir): _check_output(result.stdout, expected) result = script.pip( - 'freeze', '-f', '%s#egg=pip_test_package' % repo_dir, + 'freeze', '-f', '{repo_dir}#egg=pip_test_package'.format(**locals()), expect_stderr=True, ) expected = textwrap.dedent( """ - -f %(repo)s#egg=pip_test_package... + -f {repo}#egg=pip_test_package... -e git+...#egg=version_pkg&subdirectory=subdir ... - """ % {'repo': repo_dir}, + """.format(repo=repo_dir), ).strip() _check_output(result.stdout, expected) @@ -352,14 +352,14 @@ def test_freeze_mercurial_clone_srcdir(script, tmpdir): _check_output(result.stdout, expected) result = script.pip( - 'freeze', '-f', '%s#egg=pip_test_package' % repo_dir + 'freeze', '-f', '{repo_dir}#egg=pip_test_package'.format(**locals()) ) expected = textwrap.dedent( """ - -f %(repo)s#egg=pip_test_package... + -f {repo}#egg=pip_test_package... -e hg+...#egg=version_pkg&subdirectory=subdir ... - """ % {'repo': repo_dir}, + """.format(repo=repo_dir), ).strip() _check_output(result.stdout, expected) @@ -446,15 +446,15 @@ def test_freeze_mercurial_clone(script, tmpdir): _check_output(result.stdout, expected) result = script.pip( - 'freeze', '-f', '%s#egg=pip_test_package' % repo_dir, + 'freeze', '-f', '{repo_dir}#egg=pip_test_package'.format(**locals()), expect_stderr=True, ) expected = textwrap.dedent( """ - -f %(repo)s#egg=pip_test_package... + -f {repo}#egg=pip_test_package... ...-e hg+...#egg=version_pkg ... - """ % {'repo': repo_dir}, + """.format(repo=repo_dir), ).strip() _check_output(result.stdout, expected) @@ -468,7 +468,7 @@ def test_freeze_bazaar_clone(script, tmpdir): try: checkout_path = _create_test_package(script, vcs='bazaar') except OSError as e: - pytest.fail('Invoking `bzr` failed: %s' % e) + pytest.fail('Invoking `bzr` failed: {e}'.format(**locals())) result = script.run( 'bzr', 'checkout', checkout_path, 'bzr-package' @@ -486,13 +486,13 @@ def test_freeze_bazaar_clone(script, tmpdir): result = script.pip( 'freeze', '-f', - '%s/#egg=django-wikiapp' % checkout_path, + '{checkout_path}/#egg=django-wikiapp'.format(**locals()), expect_stderr=True, ) expected = textwrap.dedent("""\ - -f %(repo)s/#egg=django-wikiapp + -f {repo}/#egg=django-wikiapp ...-e bzr+file://...@...#egg=version_pkg - ...""" % {'repo': checkout_path}) + ...""".format(repo=checkout_path)) _check_output(result.stdout, expected) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 1ea9db26e4b..21cd94cb951 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -80,9 +80,9 @@ def test_pep518_refuses_conflicting_requires(script, data): project_dir, expect_error=True) assert ( result.returncode != 0 and - ('Some build dependencies for %s conflict with PEP 517/518 supported ' + ('Some build dependencies for {url} conflict with PEP 517/518 supported ' 'requirements: setuptools==1.0 is incompatible with ' - 'setuptools>=40.8.0.' % path_to_url(project_dir)) in result.stderr + 'setuptools>=40.8.0.'.format(url=path_to_url(project_dir))) in result.stderr ), str(result) @@ -198,12 +198,13 @@ def test_pip_second_command_line_interface_works( if pyversion_tuple < (2, 7, 9): kwargs['expect_stderr'] = True - args = ['pip%s' % pyversion] + args = ['pip{pyversion}'.format(**globals())] args.extend(['install', 'INITools==0.2']) args.extend(['-f', data.packages]) result = script.run(*args, **kwargs) egg_info_folder = ( - script.site_packages / 'INITools-0.2-py%s.egg-info' % pyversion + script.site_packages / + 'INITools-0.2-py{pyversion}.egg-info'.format(**globals()) ) initools_folder = script.site_packages / 'initools' assert egg_info_folder in result.files_created, str(result) @@ -234,7 +235,8 @@ def test_basic_install_from_pypi(script): """ result = script.pip('install', 'INITools==0.2') egg_info_folder = ( - script.site_packages / 'INITools-0.2-py%s.egg-info' % pyversion + script.site_packages / + 'INITools-0.2-py{pyversion}.egg-info'.format(**globals()) ) initools_folder = script.site_packages / 'initools' assert egg_info_folder in result.files_created, str(result) @@ -281,7 +283,7 @@ def test_basic_install_editable_from_svn(script): def _test_install_editable_from_git(script, tmpdir): """Test cloning from Git.""" pkg_path = _create_test_package(script, name='testpackage', vcs='git') - args = ['install', '-e', 'git+%s#egg=testpackage' % path_to_url(pkg_path)] + args = ['install', '-e', 'git+{url}#egg=testpackage'.format(url=path_to_url(pkg_path))] result = script.pip(*args) result.assert_installed('testpackage', with_files=['.git']) @@ -310,10 +312,10 @@ def test_install_editable_uninstalls_existing(data, script, tmpdir): result = script.pip( 'install', '-e', - '%s#egg=pip-test-package' % - local_checkout( + '{dir}#egg=pip-test-package'.format( + dir=local_checkout( 'git+https://github.com/pypa/pip-test-package.git', tmpdir, - ), + )), ) result.assert_installed('pip-test-package', with_files=['.git']) assert 'Found existing installation: pip-test-package 0.1' in result.stdout @@ -362,7 +364,7 @@ def test_vcs_url_final_slash_normalization(script, tmpdir): Test that presence or absence of final slash in VCS URL is normalized. """ pkg_path = _create_test_package(script, name='testpackage', vcs='hg') - args = ['install', '-e', 'hg+%s/#egg=testpackage' % path_to_url(pkg_path)] + args = ['install', '-e', 'hg+{url}/#egg=testpackage'.format(url=path_to_url(pkg_path))] result = script.pip(*args) result.assert_installed('testpackage', with_files=['.hg']) @@ -371,7 +373,7 @@ def test_vcs_url_final_slash_normalization(script, tmpdir): def test_install_editable_from_bazaar(script, tmpdir): """Test checking out from Bazaar.""" pkg_path = _create_test_package(script, name='testpackage', vcs='bazaar') - args = ['install', '-e', 'bzr+%s/#egg=testpackage' % path_to_url(pkg_path)] + args = ['install', '-e', 'bzr+{url}/#egg=testpackage'.format(url=path_to_url(pkg_path))] result = script.pip(*args) result.assert_installed('testpackage', with_files=['.bzr']) @@ -384,12 +386,12 @@ def test_vcs_url_urlquote_normalization(script, tmpdir): """ script.pip( 'install', '-e', - '%s/#egg=django-wikiapp' % - local_checkout( - 'bzr+http://bazaar.launchpad.net/%7Edjango-wikiapp/django-wikiapp' - '/release-0.1', - tmpdir, - ), + '{url}/#egg=django-wikiapp'.format( + url=local_checkout( + 'bzr+http://bazaar.launchpad.net/%7Edjango-wikiapp/django-wikiapp' + '/release-0.1', + tmpdir, + )), ) @@ -401,7 +403,8 @@ def test_basic_install_from_local_directory(script, data): result = script.pip('install', to_install) fspkg_folder = script.site_packages / 'fspkg' egg_info_folder = ( - script.site_packages / 'FSPkg-0.1.dev0-py%s.egg-info' % pyversion + script.site_packages / + 'FSPkg-0.1.dev0-py{pyversion}.egg-info'.format(**globals()) ) assert fspkg_folder in result.files_created, str(result.stdout) assert egg_info_folder in result.files_created, str(result) @@ -420,7 +423,8 @@ def test_basic_install_relative_directory(script, data, test_type, editable): Test installing a requirement using a relative path. """ egg_info_file = ( - script.site_packages / 'FSPkg-0.1.dev0-py%s.egg-info' % pyversion + script.site_packages / + 'FSPkg-0.1.dev0-py{pyversion}.egg-info'.format(**globals()) ) egg_link_file = ( script.site_packages / 'FSPkg.egg-link' @@ -550,7 +554,8 @@ def test_install_from_local_directory_with_symlinks_to_directories( result = script.pip('install', to_install) pkg_folder = script.site_packages / 'symlinks' egg_info_folder = ( - script.site_packages / 'symlinks-0.1.dev0-py%s.egg-info' % pyversion + script.site_packages / + 'symlinks-0.1.dev0-py{pyversion}.egg-info'.format(**globals()) ) assert pkg_folder in result.files_created, str(result.stdout) assert egg_info_folder in result.files_created, str(result) @@ -562,7 +567,8 @@ def test_install_from_local_directory_with_socket_file(script, data, tmpdir): Test installing from a local directory containing a socket file. """ egg_info_file = ( - script.site_packages / "FSPkg-0.1.dev0-py%s.egg-info" % pyversion + script.site_packages / + "FSPkg-0.1.dev0-py{pyversion}.egg-info".format(**globals()) ) package_folder = script.site_packages / "fspkg" to_copy = data.packages.joinpath("FSPkg") @@ -663,7 +669,8 @@ def test_install_curdir(script, data): result = script.pip('install', curdir, cwd=run_from) fspkg_folder = script.site_packages / 'fspkg' egg_info_folder = ( - script.site_packages / 'FSPkg-0.1.dev0-py%s.egg-info' % pyversion + script.site_packages / + 'FSPkg-0.1.dev0-py{pyversion}.egg-info'.format(**globals()) ) assert fspkg_folder in result.files_created, str(result.stdout) assert egg_info_folder in result.files_created, str(result) @@ -677,7 +684,8 @@ def test_install_pardir(script, data): result = script.pip('install', pardir, cwd=run_from) fspkg_folder = script.site_packages / 'fspkg' egg_info_folder = ( - script.site_packages / 'FSPkg-0.1.dev0-py%s.egg-info' % pyversion + script.site_packages / + 'FSPkg-0.1.dev0-py{pyversion}.egg-info'.format(**globals()) ) assert fspkg_folder in result.files_created, str(result.stdout) assert egg_info_folder in result.files_created, str(result) @@ -714,9 +722,9 @@ def test_install_using_install_option_and_editable(script, tmpdir): script.scratch_path.joinpath(folder).mkdir() url = 'git+git://github.com/pypa/pip-test-package' result = script.pip( - 'install', '-e', '%s#egg=pip-test-package' % - local_checkout(url, tmpdir), - '--install-option=--script-dir=%s' % folder, + 'install', '-e', '{url}#egg=pip-test-package' + .format(url=local_checkout(url, tmpdir)), + '--install-option=--script-dir={folder}'.format(**locals()), expect_stderr=True) script_file = ( script.venv / 'src' / 'pip-test-package' / @@ -735,7 +743,7 @@ def test_install_global_option_using_editable(script, tmpdir): url = 'hg+http://bitbucket.org/runeh/anyjson' result = script.pip( 'install', '--global-option=--version', '-e', - '%s@0.2.5#egg=anyjson' % local_checkout(url, tmpdir), + '{url}@0.2.5#egg=anyjson'.format(url=local_checkout(url, tmpdir)), expect_stderr=True) assert 'Successfully installed anyjson' in result.stdout @@ -747,7 +755,10 @@ def test_install_package_with_same_name_in_curdir(script): """ script.scratch_path.joinpath("mock==0.6").mkdir() result = script.pip('install', 'mock==0.6') - egg_folder = script.site_packages / 'mock-0.6.0-py%s.egg-info' % pyversion + egg_folder = ( + script.site_packages / + 'mock-0.6.0-py{pyversion}.egg-info'.format(**globals()) + ) assert egg_folder in result.files_created, str(result) @@ -765,7 +776,10 @@ def test_install_folder_using_dot_slash(script): pkg_path = script.scratch_path / 'mock' pkg_path.joinpath("setup.py").write_text(mock100_setup_py) result = script.pip('install', './mock') - egg_folder = script.site_packages / 'mock-100.1-py%s.egg-info' % pyversion + egg_folder = ( + script.site_packages / + 'mock-100.1-py{pyversion}.egg-info'.format(**globals()) + ) assert egg_folder in result.files_created, str(result) @@ -777,7 +791,10 @@ def test_install_folder_using_slash_in_the_end(script): pkg_path = script.scratch_path / 'mock' pkg_path.joinpath("setup.py").write_text(mock100_setup_py) result = script.pip('install', 'mock' + os.path.sep) - egg_folder = script.site_packages / 'mock-100.1-py%s.egg-info' % pyversion + egg_folder = ( + script.site_packages / + 'mock-100.1-py{pyversion}.egg-info'.format(**globals()) + ) assert egg_folder in result.files_created, str(result) @@ -790,7 +807,10 @@ def test_install_folder_using_relative_path(script): pkg_path = script.scratch_path / 'initools' / 'mock' pkg_path.joinpath("setup.py").write_text(mock100_setup_py) result = script.pip('install', Path('initools') / 'mock') - egg_folder = script.site_packages / 'mock-100.1-py%s.egg-info' % pyversion + egg_folder = ( + script.site_packages / + 'mock-100.1-py{pyversion}.egg-info'.format(**globals()) + ) assert egg_folder in result.files_created, str(result) @@ -802,8 +822,8 @@ def test_install_package_which_contains_dev_in_name(script): result = script.pip('install', 'django-devserver==0.0.4') devserver_folder = script.site_packages / 'devserver' egg_info_folder = ( - script.site_packages / 'django_devserver-0.0.4-py%s.egg-info' % - pyversion + script.site_packages / + 'django_devserver-0.0.4-py{pyversion}.egg-info'.format(**globals()) ) assert devserver_folder in result.files_created, str(result.stdout) assert egg_info_folder in result.files_created, str(result) @@ -832,7 +852,8 @@ def test_install_package_with_target(script): str(result) ) egg_folder = ( - Path('scratch') / 'target' / 'simple-2.0-py%s.egg-info' % pyversion) + Path('scratch') / 'target' / + 'simple-2.0-py{pyversion}.egg-info'.format(**globals())) assert egg_folder in result.files_created, ( str(result) ) @@ -966,8 +987,8 @@ def test_install_package_with_root(script, data): 'simple==1.0', ) normal_install_path = ( - script.base_path / script.site_packages / 'simple-1.0-py%s.egg-info' % - pyversion + script.base_path / script.site_packages / + 'simple-1.0-py{pyversion}.egg-info'.format(**globals()) ) # use distutils to change the root exactly how the --root option does it from distutils.util import change_root @@ -1090,9 +1111,11 @@ def test_url_req_case_mismatch_no_index(script, data): ) # only Upper-1.0.tar.gz should get installed. - egg_folder = script.site_packages / 'Upper-1.0-py%s.egg-info' % pyversion + egg_folder = script.site_packages / \ + 'Upper-1.0-py{pyversion}.egg-info'.format(**globals()) assert egg_folder in result.files_created, str(result) - egg_folder = script.site_packages / 'Upper-2.0-py%s.egg-info' % pyversion + egg_folder = script.site_packages / \ + 'Upper-2.0-py{pyversion}.egg-info'.format(**globals()) assert egg_folder not in result.files_created, str(result) @@ -1117,9 +1140,11 @@ def test_url_req_case_mismatch_file_index(script, data): ) # only Upper-1.0.tar.gz should get installed. - egg_folder = script.site_packages / 'Dinner-1.0-py%s.egg-info' % pyversion + egg_folder = script.site_packages / \ + 'Dinner-1.0-py{pyversion}.egg-info'.format(**globals()) assert egg_folder in result.files_created, str(result) - egg_folder = script.site_packages / 'Dinner-2.0-py%s.egg-info' % pyversion + egg_folder = script.site_packages / \ + 'Dinner-2.0-py{pyversion}.egg-info'.format(**globals()) assert egg_folder not in result.files_created, str(result) @@ -1134,9 +1159,11 @@ def test_url_incorrect_case_no_index(script, data): ) # only Upper-2.0.tar.gz should get installed. - egg_folder = script.site_packages / 'Upper-1.0-py%s.egg-info' % pyversion + egg_folder = script.site_packages / \ + 'Upper-1.0-py{pyversion}.egg-info'.format(**globals()) assert egg_folder not in result.files_created, str(result) - egg_folder = script.site_packages / 'Upper-2.0-py%s.egg-info' % pyversion + egg_folder = script.site_packages / \ + 'Upper-2.0-py{pyversion}.egg-info'.format(**globals()) assert egg_folder in result.files_created, str(result) @@ -1152,9 +1179,11 @@ def test_url_incorrect_case_file_index(script, data): ) # only Upper-2.0.tar.gz should get installed. - egg_folder = script.site_packages / 'Dinner-1.0-py%s.egg-info' % pyversion + egg_folder = script.site_packages / \ + 'Dinner-1.0-py{pyversion}.egg-info'.format(**globals()) assert egg_folder not in result.files_created, str(result) - egg_folder = script.site_packages / 'Dinner-2.0-py%s.egg-info' % pyversion + egg_folder = script.site_packages / \ + 'Dinner-2.0-py{pyversion}.egg-info'.format(**globals()) assert egg_folder in result.files_created, str(result) # Should show index-url location in output @@ -1258,7 +1287,7 @@ def test_install_subprocess_output_handling(script, data): def test_install_log(script, data, tmpdir): # test that verbose logs go to "--log" file f = tmpdir.joinpath("log.txt") - args = ['--log=%s' % f, + args = ['--log={f}'.format(**locals()), 'install', data.src.joinpath('chattymodule')] result = script.pip(*args) assert 0 == result.stdout.count("HELLO FROM CHATTYMODULE") @@ -1411,7 +1440,7 @@ def test_install_editable_with_wrong_egg_name(script): version='0.1') """)) result = script.pip( - 'install', '--editable', 'file://%s#egg=pkgb' % pkga_path + 'install', '--editable', 'file://{pkga_path}#egg=pkgb'.format(**locals()) ) assert ("Generating metadata for package pkgb produced metadata " "for project name pkga. Fix your #egg=pkgb " @@ -1485,7 +1514,7 @@ def test_install_incompatible_python_requires_editable(script): version='0.1') """)) result = script.pip( - 'install', '--editable=%s' % pkga_path, expect_error=True) + 'install', '--editable={pkga_path}'.format(**locals()), expect_error=True) assert _get_expected_error_text() in result.stderr, str(result) @@ -1599,7 +1628,7 @@ def test_installed_files_recorded_in_deterministic_order(script, data): to_install = data.packages.joinpath("FSPkg") result = script.pip('install', to_install) fspkg_folder = script.site_packages / 'fspkg' - egg_info = 'FSPkg-0.1.dev0-py%s.egg-info' % pyversion + egg_info = 'FSPkg-0.1.dev0-py{pyversion}.egg-info'.format(**globals()) installed_files_path = ( script.site_packages / egg_info / 'installed-files.txt' ) @@ -1664,8 +1693,8 @@ def test_target_install_ignores_distutils_config_install_prefix(script): distutils_config.write_text(textwrap.dedent( ''' [install] - prefix=%s - ''' % str(prefix))) + prefix={prefix} + '''.format(**locals()))) target = script.scratch_path / 'target' result = script.pip_install_local('simplewheel', '-t', target) diff --git a/tests/functional/test_install_cleanup.py b/tests/functional/test_install_cleanup.py index b07c809079b..7f91be3a4b8 100644 --- a/tests/functional/test_install_cleanup.py +++ b/tests/functional/test_install_cleanup.py @@ -85,7 +85,7 @@ def test_cleanup_req_satisfied_no_name(script, data): script.pip('install', dist) build = script.venv_path / 'build' - assert not exists(build), "unexpected build/ dir exists: %s" % build + assert not exists(build), "unexpected build/ dir exists: {build}".format(**locals()) script.assert_no_temp() @@ -99,7 +99,7 @@ def test_cleanup_after_install_exception(script, data): expect_error=True, ) build = script.venv_path / 'build' - assert not exists(build), "build/ dir still exists: %s" % result.stdout + assert not exists(build), "build/ dir still exists: {result.stdout}".format(**locals()) script.assert_no_temp() @@ -113,7 +113,7 @@ def test_cleanup_after_egg_info_exception(script, data): expect_error=True, ) build = script.venv_path / 'build' - assert not exists(build), "build/ dir still exists: %s" % result.stdout + assert not exists(build), "build/ dir still exists: {result.stdout}".format(**locals()) script.assert_no_temp() diff --git a/tests/functional/test_install_compat.py b/tests/functional/test_install_compat.py index b957934ef48..e5401985b17 100644 --- a/tests/functional/test_install_compat.py +++ b/tests/functional/test_install_compat.py @@ -24,17 +24,18 @@ def test_debian_egg_name_workaround(script): result = script.pip('install', 'INITools==0.2') egg_info = os.path.join( - script.site_packages, "INITools-0.2-py%s.egg-info" % pyversion) + script.site_packages, + "INITools-0.2-py{pyversion}.egg-info".format(**globals())) # Debian only removes pyversion for global installs, not inside a venv # so even if this test runs on a Debian/Ubuntu system with broken # setuptools, since our test runs inside a venv we'll still have the normal # .egg-info - assert egg_info in result.files_created, "Couldn't find %s" % egg_info + assert egg_info in result.files_created, "Couldn't find {egg_info}".format(**locals()) # The Debian no-pyversion version of the .egg-info mangled = os.path.join(script.site_packages, "INITools-0.2.egg-info") - assert mangled not in result.files_created, "Found unexpected %s" % mangled + assert mangled not in result.files_created, "Found unexpected {mangled}".format(**locals()) # Simulate a Debian install by copying the .egg-info to their name for it full_egg_info = os.path.join(script.base_path, egg_info) diff --git a/tests/functional/test_install_config.py b/tests/functional/test_install_config.py index 2b76e81cd25..088016a9f1b 100644 --- a/tests/functional/test_install_config.py +++ b/tests/functional/test_install_config.py @@ -107,7 +107,7 @@ def test_command_line_appends_correctly(script, data): """ script.environ['PIP_FIND_LINKS'] = ( - 'https://test.pypi.org %s' % data.find_links + 'https://test.pypi.org {data.find_links}'.format(**locals()) ) result = script.pip( 'install', '-vvv', 'INITools', '--trusted-host', diff --git a/tests/functional/test_install_extras.py b/tests/functional/test_install_extras.py index acfd58dff17..bce95cb196d 100644 --- a/tests/functional/test_install_extras.py +++ b/tests/functional/test_install_extras.py @@ -121,7 +121,7 @@ def test_install_special_extra(script): """)) result = script.pip( - 'install', '--no-index', '%s[Hop_hOp-hoP]' % pkga_path, + 'install', '--no-index', '{pkga_path}[Hop_hOp-hoP]'.format(**locals()), expect_error=True) assert ( "Could not find a version that satisfies the requirement missing_pkg" diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index 2422bdb500e..0c00060a23d 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -24,14 +24,14 @@ def test_requirements_file(script): script.scratch_path.joinpath("initools-req.txt").write_text(textwrap.dedent("""\ INITools==0.2 # and something else to test out: - %s<=%s - """ % (other_lib_name, other_lib_version))) + {other_lib_name}<={other_lib_version} + """.format(**locals()))) result = script.pip( 'install', '-r', script.scratch_path / 'initools-req.txt' ) assert ( script.site_packages / 'INITools-0.2-py{}.egg-info'.format( - pyversion in result.files_created) + pyversion) in result.files_created ) assert script.site_packages / 'initools' in result.files_created assert result.files_created[script.site_packages / other_lib_name].dir @@ -46,7 +46,7 @@ def test_schema_check_in_requirements_file(script): """ script.scratch_path.joinpath("file-egg-req.txt").write_text( - "\n%s\n" % ( + "\n{}\n".format( "git://github.com/alex/django-fixture-generator.git" "#egg=fixture_generator" ) @@ -73,7 +73,8 @@ def test_relative_requirements_file(script, data, test_type, editable): """ egg_info_file = ( - script.site_packages / 'FSPkg-0.1.dev0-py%s.egg-info' % pyversion + script.site_packages / + 'FSPkg-0.1.dev0-py{pyversion}.egg-info'.format(**globals()) ) egg_link_file = ( script.site_packages / 'FSPkg.egg-link' @@ -120,22 +121,24 @@ def test_multiple_requirements_files(script, tmpdir): other_lib_name, other_lib_version = 'anyjson', '0.3' script.scratch_path.joinpath("initools-req.txt").write_text( textwrap.dedent(""" - -e %s@10#egg=INITools - -r %s-req.txt - """) % + -e {}@10#egg=INITools + -r {}-req.txt + """).format ( local_checkout('svn+http://svn.colorstudy.com/INITools', tmpdir), other_lib_name ), ) - script.scratch_path.joinpath("%s-req.txt" % other_lib_name).write_text( - "%s<=%s" % (other_lib_name, other_lib_version) + script.scratch_path.joinpath( + "{other_lib_name}-req.txt".format(**locals())).write_text( + "{other_lib_name}<={other_lib_version}".format(**locals()) ) result = script.pip( 'install', '-r', script.scratch_path / 'initools-req.txt' ) assert result.files_created[script.site_packages / other_lib_name].dir - fn = '%s-%s-py%s.egg-info' % (other_lib_name, other_lib_version, pyversion) + fn = '{other_lib_name}-{other_lib_version}-py{pyversion}.egg-info'.format( + pyversion=pyversion, **locals()) assert result.files_created[script.site_packages / fn].dir assert script.venv / 'src' / 'initools' in result.files_created @@ -176,13 +179,13 @@ def test_respect_order_in_requirements_file(script, data): if 'Processing' in line] assert 'parent' in downloaded[0], ( - 'First download should be "parent" but was "%s"' % downloaded[0] + 'First download should be "parent" but was "{}"'.format(downloaded[0]) ) assert 'child' in downloaded[1], ( - 'Second download should be "child" but was "%s"' % downloaded[1] + 'Second download should be "child" but was "{}"'.format(downloaded[1]) ) assert 'simple' in downloaded[2], ( - 'Third download should be "simple" but was "%s"' % downloaded[2] + 'Third download should be "simple" but was "{}"'.format(downloaded[2]) ) @@ -215,8 +218,9 @@ def test_install_local_editable_with_subdirectory(script): 'version_subdir') result = script.pip( 'install', '-e', - '%s#egg=version_subpkg&subdirectory=version_subdir' % - ('git+%s' % path_to_url(version_pkg_path),) + '{uri}#egg=version_subpkg&subdirectory=version_subdir'.format( + uri='git+' + path_to_url(version_pkg_path), + ), ) result.assert_installed('version-subpkg', sub_dir='version_subdir') @@ -228,8 +232,9 @@ def test_install_local_with_subdirectory(script): 'version_subdir') result = script.pip( 'install', - '%s#egg=version_subpkg&subdirectory=version_subdir' % - ('git+' + path_to_url(version_pkg_path),) + '{uri}#egg=version_subpkg&subdirectory=version_subdir'.format( + uri='git+' + path_to_url(version_pkg_path), + ), ) result.assert_installed('version_subpkg.py', editable=False) @@ -247,7 +252,7 @@ def test_wheel_user_with_prefix_in_pydistutils_cfg( with open(user_cfg, "w") as cfg: cfg.write(textwrap.dedent(""" [install] - prefix=%s""" % script.scratch_path)) + prefix={script.scratch_path}""".format(**locals()))) result = script.pip( 'install', '--user', '--no-index', @@ -268,13 +273,14 @@ def test_install_option_in_requirements_file(script, data, virtualenv): script.scratch_path.joinpath("reqs.txt").write_text( textwrap.dedent( - """simple --install-option='--home=%s'""" - % script.scratch_path.joinpath("home1"))) + """simple --install-option='--home={home}'""".format( + home=script.scratch_path.joinpath("home1")))) result = script.pip( 'install', '--no-index', '-f', data.find_links, '-r', script.scratch_path / 'reqs.txt', - '--install-option=--home=%s' % script.scratch_path.joinpath("home2"), + '--install-option=--home={home}'.format( + home=script.scratch_path.joinpath("home2")), expect_stderr=True) package_dir = script.scratch / 'home1' / 'lib' / 'python' / 'simple' @@ -333,7 +339,7 @@ def test_constraints_local_install_causes_error(script, data): def test_constraints_constrain_to_local_editable(script, data): to_install = data.src.joinpath("singlemodule") script.scratch_path.joinpath("constraints.txt").write_text( - "-e %s#egg=singlemodule" % path_to_url(to_install) + "-e {url}#egg=singlemodule".format(url=path_to_url(to_install)) ) result = script.pip( 'install', '--no-index', '-f', data.find_links, '-c', @@ -344,7 +350,7 @@ def test_constraints_constrain_to_local_editable(script, data): def test_constraints_constrain_to_local(script, data): to_install = data.src.joinpath("singlemodule") script.scratch_path.joinpath("constraints.txt").write_text( - "%s#egg=singlemodule" % path_to_url(to_install) + "{url}#egg=singlemodule".format(url=path_to_url(to_install)) ) result = script.pip( 'install', '--no-index', '-f', data.find_links, '-c', @@ -400,7 +406,7 @@ def test_double_install_spurious_hash_mismatch( def test_install_with_extras_from_constraints(script, data): to_install = data.packages.joinpath("LocalExtras") script.scratch_path.joinpath("constraints.txt").write_text( - "%s#egg=LocalExtras[bar]" % path_to_url(to_install) + "{url}#egg=LocalExtras[bar]".format(url=path_to_url(to_install)) ) result = script.pip_install_local( '-c', script.scratch_path / 'constraints.txt', 'LocalExtras') @@ -410,7 +416,7 @@ def test_install_with_extras_from_constraints(script, data): def test_install_with_extras_from_install(script, data): to_install = data.packages.joinpath("LocalExtras") script.scratch_path.joinpath("constraints.txt").write_text( - "%s#egg=LocalExtras" % path_to_url(to_install) + "{url}#egg=LocalExtras".format(url=path_to_url(to_install)) ) result = script.pip_install_local( '-c', script.scratch_path / 'constraints.txt', 'LocalExtras[baz]') @@ -420,7 +426,7 @@ def test_install_with_extras_from_install(script, data): def test_install_with_extras_joined(script, data): to_install = data.packages.joinpath("LocalExtras") script.scratch_path.joinpath("constraints.txt").write_text( - "%s#egg=LocalExtras[bar]" % path_to_url(to_install) + "{url}#egg=LocalExtras[bar]".format(url=path_to_url(to_install)) ) result = script.pip_install_local( '-c', script.scratch_path / 'constraints.txt', 'LocalExtras[baz]' @@ -432,7 +438,7 @@ def test_install_with_extras_joined(script, data): def test_install_with_extras_editable_joined(script, data): to_install = data.packages.joinpath("LocalExtras") script.scratch_path.joinpath("constraints.txt").write_text( - "-e %s#egg=LocalExtras[bar]" % path_to_url(to_install) + "-e {url}#egg=LocalExtras[bar]".format(url=path_to_url(to_install)) ) result = script.pip_install_local( '-c', script.scratch_path / 'constraints.txt', 'LocalExtras[baz]') @@ -454,13 +460,15 @@ def test_install_distribution_duplicate_extras(script, data): package_name = to_install + "[bar]" with pytest.raises(AssertionError): result = script.pip_install_local(package_name, package_name) - assert 'Double requirement given: %s' % package_name in result.stderr + expected = ( + 'Double requirement given: {package_name}'.format(**locals())) + assert expected in result.stderr def test_install_distribution_union_with_constraints(script, data): to_install = data.packages.joinpath("LocalExtras") script.scratch_path.joinpath("constraints.txt").write_text( - "%s[bar]" % to_install) + "{to_install}[bar]".format(**locals())) result = script.pip_install_local( '-c', script.scratch_path / 'constraints.txt', to_install + '[baz]') assert 'Running setup.py install for LocalExtras' in result.stdout @@ -492,11 +500,10 @@ def test_install_distribution_union_conflicting_extras(script, data): def test_install_unsupported_wheel_link_with_marker(script): script.scratch_path.joinpath("with-marker.txt").write_text( textwrap.dedent("""\ - %s; %s - """) % - ( - 'https://github.com/a/b/c/asdf-1.5.2-cp27-none-xyz.whl', - 'sys_platform == "xyz"' + {url}; {req} + """).format( + url='https://github.com/a/b/c/asdf-1.5.2-cp27-none-xyz.whl', + req='sys_platform == "xyz"', ) ) result = script.pip( @@ -511,9 +518,10 @@ def test_install_unsupported_wheel_link_with_marker(script): def test_install_unsupported_wheel_file(script, data): # Trying to install a local wheel with an incompatible version/type # should fail. + path = data.packages.joinpath("simple.dist-0.1-py1-none-invalid.whl") script.scratch_path.joinpath("wheel-file.txt").write_text(textwrap.dedent("""\ - %s - """ % data.packages.joinpath("simple.dist-0.1-py1-none-invalid.whl"))) + {path} + """.format(**locals()))) result = script.pip( 'install', '-r', script.scratch_path / 'wheel-file.txt', expect_error=True, @@ -538,9 +546,9 @@ def test_install_options_local_to_package(script, data): reqs_file = script.scratch_path.joinpath("reqs.txt") reqs_file.write_text( textwrap.dedent(""" - simple --install-option='--home=%s' + simple --install-option='--home={home_simple}' INITools - """ % home_simple)) + """.format(**locals()))) result = script.pip( 'install', '--no-index', '-f', data.find_links, diff --git a/tests/functional/test_install_upgrade.py b/tests/functional/test_install_upgrade.py index 0024de4d438..87ccd893afc 100644 --- a/tests/functional/test_install_upgrade.py +++ b/tests/functional/test_install_upgrade.py @@ -46,11 +46,13 @@ def test_only_if_needed_does_not_upgrade_deps_when_satisfied(script): ) assert ( - (script.site_packages / 'require_simple-1.0-py%s.egg-info' % pyversion) + (script.site_packages / 'require_simple-1.0-py{pyversion}.egg-info' + .format(**globals())) not in result.files_deleted ), "should have installed require_simple==1.0" assert ( - (script.site_packages / 'simple-2.0-py%s.egg-info' % pyversion) + (script.site_packages / 'simple-2.0-py{pyversion}.egg-info' + .format(**globals())) not in result.files_deleted ), "should not have uninstalled simple==2.0" assert ( @@ -70,16 +72,23 @@ def test_only_if_needed_does_upgrade_deps_when_no_longer_satisfied(script): ) assert ( - (script.site_packages / 'require_simple-1.0-py%s.egg-info' % pyversion) + (script.site_packages / 'require_simple-1.0-py{pyversion}.egg-info' + .format(**globals())) not in result.files_deleted ), "should have installed require_simple==1.0" + expected = ( + script.site_packages / + 'simple-3.0-py{pyversion}.egg-info'.format(**globals()) + ) assert ( - script.site_packages / 'simple-3.0-py%s.egg-info' % - pyversion in result.files_created + expected in result.files_created ), "should have installed simple==3.0" + expected = ( + script.site_packages / + 'simple-1.0-py{pyversion}.egg-info'.format(**globals()) + ) assert ( - script.site_packages / 'simple-1.0-py%s.egg-info' % - pyversion in result.files_deleted + expected in result.files_deleted ), "should have uninstalled simple==1.0" @@ -94,11 +103,13 @@ def test_eager_does_upgrade_dependecies_when_currently_satisfied(script): ) assert ( - (script.site_packages / 'require_simple-1.0-py%s.egg-info' % pyversion) + (script.site_packages / + 'require_simple-1.0-py{pyversion}.egg-info'.format(**globals())) not in result.files_deleted ), "should have installed require_simple==1.0" assert ( - (script.site_packages / 'simple-2.0-py%s.egg-info' % pyversion) + (script.site_packages / + 'simple-2.0-py{pyversion}.egg-info'.format(**globals())) in result.files_deleted ), "should have uninstalled simple==2.0" @@ -114,16 +125,19 @@ def test_eager_does_upgrade_dependecies_when_no_longer_satisfied(script): ) assert ( - (script.site_packages / 'require_simple-1.0-py%s.egg-info' % pyversion) + (script.site_packages / + 'require_simple-1.0-py{pyversion}.egg-info'.format(**globals())) not in result.files_deleted ), "should have installed require_simple==1.0" assert ( - script.site_packages / 'simple-3.0-py%s.egg-info' % - pyversion in result.files_created + script.site_packages / + 'simple-3.0-py{pyversion}.egg-info'.format(**globals()) + in result.files_created ), "should have installed simple==3.0" assert ( - script.site_packages / 'simple-1.0-py%s.egg-info' % - pyversion in result.files_deleted + script.site_packages / + 'simple-1.0-py{pyversion}.egg-info'.format(**globals()) + in result.files_deleted ), "should have uninstalled simple==1.0" @@ -139,12 +153,14 @@ def test_upgrade_to_specific_version(script): 'pip install with specific version did not upgrade' ) assert ( - script.site_packages / 'INITools-0.1-py%s.egg-info' % - pyversion in result.files_deleted + script.site_packages / 'INITools-0.1-py{pyversion}.egg-info' + .format(**globals()) + in result.files_deleted ) assert ( - script.site_packages / 'INITools-0.2-py%s.egg-info' % - pyversion in result.files_created + script.site_packages / 'INITools-0.2-py{pyversion}.egg-info' + .format(**globals()) + in result.files_created ) @@ -158,8 +174,9 @@ def test_upgrade_if_requested(script): result = script.pip('install', '--upgrade', 'INITools') assert result.files_created, 'pip install --upgrade did not upgrade' assert ( - script.site_packages / 'INITools-0.1-py%s.egg-info' % - pyversion not in result.files_created + script.site_packages / + 'INITools-0.1-py{pyversion}.egg-info'.format(**globals()) + not in result.files_created ) @@ -322,12 +339,14 @@ def test_should_not_install_always_from_cache(script): script.pip('uninstall', '-y', 'INITools') result = script.pip('install', 'INITools==0.1') assert ( - script.site_packages / 'INITools-0.2-py%s.egg-info' % - pyversion not in result.files_created + script.site_packages / + 'INITools-0.2-py{pyversion}.egg-info'.format(**globals()) + not in result.files_created ) assert ( - script.site_packages / 'INITools-0.1-py%s.egg-info' % - pyversion in result.files_created + script.site_packages / + 'INITools-0.1-py{pyversion}.egg-info'.format(**globals()) + in result.files_created ) @@ -341,18 +360,22 @@ def test_install_with_ignoreinstalled_requested(script): assert result.files_created, 'pip install -I did not install' # both the old and new metadata should be present. assert os.path.exists( - script.site_packages_path / 'INITools-0.1-py%s.egg-info' % pyversion + script.site_packages_path / + 'INITools-0.1-py{pyversion}.egg-info'.format(**globals()) ) assert os.path.exists( - script.site_packages_path / 'INITools-0.3-py%s.egg-info' % pyversion + script.site_packages_path / + 'INITools-0.3-py{pyversion}.egg-info'.format(**globals()) ) @pytest.mark.network def test_upgrade_vcs_req_with_no_dists_found(script, tmpdir): """It can upgrade a VCS requirement that has no distributions otherwise.""" - req = "%s#egg=pip-test-package" % local_checkout( - "git+https://github.com/pypa/pip-test-package.git", tmpdir, + req = "{checkout}#egg=pip-test-package".format( + checkout=local_checkout( + "git+https://github.com/pypa/pip-test-package.git", tmpdir, + ) ) script.pip("install", req) result = script.pip("install", "-U", req) @@ -365,10 +388,11 @@ def test_upgrade_vcs_req_with_dist_found(script): # TODO(pnasrat) Using local_checkout fails on windows - oddness with the # test path urls/git. req = ( - "%s#egg=pretend" % - ( - "git+git://github.com/alex/pretend@e7f26ad7dbcb4a02a4995aade4" - "743aad47656b27" + "{url}#egg=pretend".format( + url=( + "git+git://github.com/alex/pretend@e7f26ad7dbcb4a02a4995aade4" + "743aad47656b27" + ), ) ) script.pip("install", req, expect_stderr=True) @@ -401,7 +425,8 @@ class TestUpgradeDistributeToSetuptools(object): def prep_ve(self, script, version, pip_src, distribute=False): self.script = script - self.script.pip_install_local('virtualenv==%s' % version) + self.script.pip_install_local( + 'virtualenv=={version}'.format(**locals())) args = ['virtualenv', self.script.scratch_path / 'VE'] if distribute: args.insert(1, '--distribute') diff --git a/tests/functional/test_install_user.py b/tests/functional/test_install_user.py index 2cc91f56c75..8432dbc0bd6 100644 --- a/tests/functional/test_install_user.py +++ b/tests/functional/test_install_user.py @@ -52,8 +52,10 @@ def test_install_subversion_usersite_editable_with_distribute( """ result = script.pip( 'install', '--user', '-e', - '%s#egg=initools' % - local_checkout('svn+http://svn.colorstudy.com/INITools', tmpdir) + '{checkout}#egg=initools'.format( + checkout=local_checkout( + 'svn+http://svn.colorstudy.com/INITools', tmpdir) + ) ) result.assert_installed('INITools', use_user_site=True) @@ -110,7 +112,7 @@ def test_install_user_conflict_in_usersite(self, script): # usersite has 0.1 egg_info_folder = ( - script.user_site / 'INITools-0.1-py%s.egg-info' % pyversion + script.user_site / 'INITools-0.1-py{pyversion}.egg-info'.format(**globals()) ) initools_v3_file = ( # file only in 0.3 @@ -136,7 +138,7 @@ def test_install_user_conflict_in_globalsite(self, virtualenv, script): # usersite has 0.1 egg_info_folder = ( - script.user_site / 'INITools-0.1-py%s.egg-info' % pyversion + script.user_site / 'INITools-0.1-py{pyversion}.egg-info'.format(**globals()) ) initools_folder = script.user_site / 'initools' assert egg_info_folder in result2.files_created, str(result2) @@ -145,7 +147,7 @@ def test_install_user_conflict_in_globalsite(self, virtualenv, script): # site still has 0.2 (can't look in result1; have to check) egg_info_folder = ( script.base_path / script.site_packages / - 'INITools-0.2-py%s.egg-info' % pyversion + 'INITools-0.2-py{pyversion}.egg-info'.format(**globals()) ) initools_folder = script.base_path / script.site_packages / 'initools' assert isdir(egg_info_folder) @@ -166,7 +168,7 @@ def test_upgrade_user_conflict_in_globalsite(self, virtualenv, script): # usersite has 0.3.1 egg_info_folder = ( - script.user_site / 'INITools-0.3.1-py%s.egg-info' % pyversion + script.user_site / 'INITools-0.3.1-py{pyversion}.egg-info'.format(**globals()) ) initools_folder = script.user_site / 'initools' assert egg_info_folder in result2.files_created, str(result2) @@ -175,7 +177,7 @@ def test_upgrade_user_conflict_in_globalsite(self, virtualenv, script): # site still has 0.2 (can't look in result1; have to check) egg_info_folder = ( script.base_path / script.site_packages / - 'INITools-0.2-py%s.egg-info' % pyversion + 'INITools-0.2-py{pyversion}.egg-info'.format(**globals()) ) initools_folder = script.base_path / script.site_packages / 'initools' assert isdir(egg_info_folder), result2.stdout @@ -199,7 +201,7 @@ def test_install_user_conflict_in_globalsite_and_usersite( # usersite has 0.1 egg_info_folder = ( - script.user_site / 'INITools-0.1-py%s.egg-info' % pyversion + script.user_site / 'INITools-0.1-py{pyversion}.egg-info'.format(**globals()) ) initools_v3_file = ( # file only in 0.3 @@ -212,7 +214,7 @@ def test_install_user_conflict_in_globalsite_and_usersite( # site still has 0.2 (can't just look in result1; have to check) egg_info_folder = ( script.base_path / script.site_packages / - 'INITools-0.2-py%s.egg-info' % pyversion + 'INITools-0.2-py{pyversion}.egg-info'.format(**globals()) ) initools_folder = script.base_path / script.site_packages / 'initools' assert isdir(egg_info_folder) @@ -241,6 +243,9 @@ def test_install_user_in_global_virtualenv_with_conflict_fails( dist_location = resultp.stdout.strip() assert ( "Will not install to the user site because it will lack sys.path " - "precedence to %s in %s" % - ('INITools', dist_location) in result2.stderr + "precedence to {name} in {location}".format( + name='INITools', + location=dist_location, + ) + in result2.stderr ) diff --git a/tests/functional/test_install_vcs_git.py b/tests/functional/test_install_vcs_git.py index 01fec7a8bd4..3cb4d1490b2 100644 --- a/tests/functional/test_install_vcs_git.py +++ b/tests/functional/test_install_vcs_git.py @@ -171,7 +171,7 @@ def test_install_noneditable_git(script, tmpdir): ) egg_info_folder = ( script.site_packages / - 'pip_test_package-0.1.1-py%s.egg-info' % pyversion + 'pip_test_package-0.1.1-py{pyversion}.egg-info'.format(**globals()) ) result.assert_installed('piptestpackage', without_egg_link=True, diff --git a/tests/functional/test_pep517.py b/tests/functional/test_pep517.py index d932f2ef83b..0286fe1f034 100644 --- a/tests/functional/test_pep517.py +++ b/tests/functional/test_pep517.py @@ -134,11 +134,13 @@ def test_conflicting_pep517_backend_requirements(script, tmpdir, data): project_dir, expect_error=True ) + msg = ( + 'Some build dependencies for {url} conflict with the backend ' + 'dependencies: simplewheel==1.0 is incompatible with ' + 'simplewheel==2.0.'.format(url=path_to_url(project_dir))) assert ( result.returncode != 0 and - ('Some build dependencies for %s conflict with the backend ' - 'dependencies: simplewheel==1.0 is incompatible with ' - 'simplewheel==2.0.' % path_to_url(project_dir)) in result.stderr + msg in result.stderr ), str(result) diff --git a/tests/functional/test_uninstall.py b/tests/functional/test_uninstall.py index e06ff024630..04d8550f32a 100644 --- a/tests/functional/test_uninstall.py +++ b/tests/functional/test_uninstall.py @@ -328,8 +328,9 @@ def test_uninstall_editable_from_svn(script, tmpdir): """ result = script.pip( 'install', '-e', - '%s#egg=initools' % ( - local_checkout('svn+http://svn.colorstudy.com/INITools', tmpdir) + '{checkout}#egg=initools'.format( + checkout=local_checkout( + 'svn+http://svn.colorstudy.com/INITools', tmpdir) ), ) result.assert_installed('INITools') @@ -396,10 +397,10 @@ def test_uninstall_from_reqs_file(script, tmpdir): ) script.scratch_path.joinpath("test-req.txt").write_text( textwrap.dedent(""" - -e %s#egg=initools + -e {url}#egg=initools # and something else to test out: PyLogo<0.4 - """) % local_svn_url + """).format(url=local_svn_url) ) result = script.pip('install', '-r', 'test-req.txt') script.scratch_path.joinpath("test-req.txt").write_text( @@ -409,10 +410,10 @@ def test_uninstall_from_reqs_file(script, tmpdir): -i http://www.example.com --extra-index-url http://www.example.com - -e %s#egg=initools + -e {url}#egg=initools # and something else to test out: PyLogo<0.4 - """) % local_svn_url + """).format(url=local_svn_url) ) result2 = script.pip('uninstall', '-r', 'test-req.txt', '-y') assert_all_changes( diff --git a/tests/functional/test_uninstall_user.py b/tests/functional/test_uninstall_user.py index ed277739a1f..1b19294b78d 100644 --- a/tests/functional/test_uninstall_user.py +++ b/tests/functional/test_uninstall_user.py @@ -44,7 +44,7 @@ def test_uninstall_from_usersite_with_dist_in_global_site( # site still has 0.2 (can't look in result1; have to check) egg_info_folder = ( script.base_path / script.site_packages / - 'pip_test_package-0.1-py%s.egg-info' % pyversion + 'pip_test_package-0.1-py{pyversion}.egg-info'.format(**globals()) ) assert isdir(egg_info_folder) diff --git a/tests/functional/test_wheel.py b/tests/functional/test_wheel.py index 181ca05845b..3c8c528be34 100644 --- a/tests/functional/test_wheel.py +++ b/tests/functional/test_wheel.py @@ -48,12 +48,13 @@ def test_pip_wheel_success(script, data): 'wheel', '--no-index', '-f', data.find_links, 'simple==3.0', ) - wheel_file_name = 'simple-3.0-py%s-none-any.whl' % pyversion[0] + wheel_file_name = 'simple-3.0-py{pyversion[0]}-none-any.whl' \ + .format(**globals()) wheel_file_path = script.scratch / wheel_file_name assert re.search( r"Created wheel for simple: " - r"filename=%s size=\d+ sha256=[A-Fa-f0-9]{64}" - % re.escape(wheel_file_name), result.stdout) + r"filename={filename} size=\d+ sha256=[A-Fa-f0-9]{{64}}" + .format(filename=re.escape(wheel_file_name)), result.stdout) assert re.search( r"^\s+Stored in directory: ", result.stdout, re.M) assert wheel_file_path in result.files_created, result.stdout @@ -68,7 +69,7 @@ def test_pip_wheel_build_cache(script, data): 'wheel', '--no-index', '-f', data.find_links, 'simple==3.0', ) - wheel_file_name = 'simple-3.0-py%s-none-any.whl' % pyversion[0] + wheel_file_name = 'simple-3.0-py{pyversion[0]}-none-any.whl'.format(**globals()) wheel_file_path = script.scratch / wheel_file_name assert wheel_file_path in result.files_created, result.stdout assert "Successfully built simple" in result.stdout, result.stdout @@ -145,7 +146,7 @@ def test_pip_wheel_builds_editable_deps(script, data): 'wheel', '--no-index', '-f', data.find_links, '-e', editable_path ) - wheel_file_name = 'simple-1.0-py%s-none-any.whl' % pyversion[0] + wheel_file_name = 'simple-1.0-py{pyversion[0]}-none-any.whl'.format(**globals()) wheel_file_path = script.scratch / wheel_file_name assert wheel_file_path in result.files_created, result.stdout @@ -159,7 +160,7 @@ def test_pip_wheel_builds_editable(script, data): 'wheel', '--no-index', '-f', data.find_links, '-e', editable_path ) - wheel_file_name = 'simplewheel-1.0-py%s-none-any.whl' % pyversion[0] + wheel_file_name = 'simplewheel-1.0-py{pyversion[0]}-none-any.whl'.format(**globals()) wheel_file_path = script.scratch / wheel_file_name assert wheel_file_path in result.files_created, result.stdout @@ -173,7 +174,7 @@ def test_pip_wheel_fail(script, data): 'wheelbroken==0.1', expect_error=True, ) - wheel_file_name = 'wheelbroken-0.1-py%s-none-any.whl' % pyversion[0] + wheel_file_name = 'wheelbroken-0.1-py{pyversion[0]}-none-any.whl'.format(**globals()) wheel_file_path = script.scratch / wheel_file_name assert wheel_file_path not in result.files_created, ( wheel_file_path, @@ -191,12 +192,12 @@ def test_no_clean_option_blocks_cleaning_after_wheel(script, data): build = script.venv_path / 'build' result = script.pip( 'wheel', '--no-clean', '--no-index', '--build', build, - '--find-links=%s' % data.find_links, + '--find-links={data.find_links}'.format(**locals()), 'simple', expect_temp=True, ) build = build / 'simple' - assert exists(build), "build/simple should still exist %s" % str(result) + assert exists(build), "build/simple should still exist {result}".format(**locals()) def test_pip_wheel_source_deps(script, data): @@ -209,7 +210,7 @@ def test_pip_wheel_source_deps(script, data): 'wheel', '--no-index', '-f', data.find_links, 'requires_source', ) - wheel_file_name = 'source-1.0-py%s-none-any.whl' % pyversion[0] + wheel_file_name = 'source-1.0-py{pyversion[0]}-none-any.whl'.format(**globals()) wheel_file_path = script.scratch / wheel_file_name assert wheel_file_path in result.files_created, result.stdout assert "Successfully built source" in result.stdout, result.stdout @@ -228,7 +229,7 @@ def test_pip_wheel_fail_cause_of_previous_build_dir(script, data): # When I call pip trying to install things again result = script.pip( - 'wheel', '--no-index', '--find-links=%s' % data.find_links, + 'wheel', '--no-index', '--find-links={data.find_links}'.format(**locals()), '--build', script.venv_path / 'build', 'simple==3.0', expect_error=True, expect_temp=True, ) @@ -248,7 +249,7 @@ def test_wheel_package_with_latin1_setup(script, data): def test_pip_wheel_with_pep518_build_reqs(script, data, common_wheels): result = script.pip('wheel', '--no-index', '-f', data.find_links, '-f', common_wheels, 'pep518==3.0',) - wheel_file_name = 'pep518-3.0-py%s-none-any.whl' % pyversion[0] + wheel_file_name = 'pep518-3.0-py{pyversion[0]}-none-any.whl'.format(**globals()) wheel_file_path = script.scratch / wheel_file_name assert wheel_file_path in result.files_created, result.stdout assert "Successfully built pep518" in result.stdout, result.stdout @@ -261,7 +262,7 @@ def test_pip_wheel_with_pep518_build_reqs_no_isolation(script, data): 'wheel', '--no-index', '-f', data.find_links, '--no-build-isolation', 'pep518==3.0', ) - wheel_file_name = 'pep518-3.0-py%s-none-any.whl' % pyversion[0] + wheel_file_name = 'pep518-3.0-py{pyversion[0]}-none-any.whl'.format(**globals()) wheel_file_path = script.scratch / wheel_file_name assert wheel_file_path in result.files_created, result.stdout assert "Successfully built pep518" in result.stdout, result.stdout @@ -289,7 +290,7 @@ def test_pep517_wheels_are_not_confused_with_other_files(script, tmpdir, data): result = script.pip('wheel', pkg_to_wheel, '-w', script.scratch_path) assert "Installing build dependencies" in result.stdout, result.stdout - wheel_file_name = 'withpyproject-0.0.1-py%s-none-any.whl' % pyversion[0] + wheel_file_name = 'withpyproject-0.0.1-py{pyversion[0]}-none-any.whl'.format(**globals()) wheel_file_path = script.scratch / wheel_file_name assert wheel_file_path in result.files_created, result.stdout @@ -303,6 +304,6 @@ def test_legacy_wheels_are_not_confused_with_other_files(script, tmpdir, data): result = script.pip('wheel', pkg_to_wheel, '-w', script.scratch_path) assert "Installing build dependencies" not in result.stdout, result.stdout - wheel_file_name = 'simplewheel-1.0-py%s-none-any.whl' % pyversion[0] + wheel_file_name = 'simplewheel-1.0-py{pyversion[0]}-none-any.whl'.format(**globals()) wheel_file_path = script.scratch / wheel_file_name assert wheel_file_path in result.files_created, result.stdout diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index c0baa0a1cfb..1f355fa8f57 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -285,14 +285,16 @@ def assert_installed(self, pkg_name, editable=True, with_files=[], if without_egg_link: if egg_link_path in self.files_created: raise TestFailure( - 'unexpected egg link file created: %r\n%s' % - (egg_link_path, self) + 'unexpected egg link file created: ' + '{egg_link_path!r}\n{self}' + .format(**locals()) ) else: if egg_link_path not in self.files_created: raise TestFailure( - 'expected egg link file missing: %r\n%s' % - (egg_link_path, self) + 'expected egg link file missing: ' + '{egg_link_path!r}\n{self}' + .format(**locals()) ) egg_link_file = self.files_created[egg_link_path] @@ -301,15 +303,15 @@ def assert_installed(self, pkg_name, editable=True, with_files=[], # FIXME: I don't understand why there's a trailing . here if not (egg_link_contents.endswith('\n.') and egg_link_contents[:-2].endswith(pkg_dir)): - raise TestFailure(textwrap.dedent(u'''\ - Incorrect egg_link file %r - Expected ending: %r + raise TestFailure(textwrap.dedent( + u'''\ + Incorrect egg_link file {egg_link_file!r} + Expected ending: {expected_ending!r} ------- Actual contents ------- - %s - -------------------------------''' % ( - egg_link_file, - pkg_dir + '\n.', - repr(egg_link_contents)) + {egg_link_contents!r} + -------------------------------'''.format( + expected_ending=pkg_dir + '\n.', + **locals()) )) if use_user_site: @@ -318,33 +320,36 @@ def assert_installed(self, pkg_name, editable=True, with_files=[], pth_file = e.site_packages / 'easy-install.pth' if (pth_file in self.files_updated) == without_egg_link: - raise TestFailure('%r unexpectedly %supdated by install' % ( - pth_file, (not without_egg_link and 'not ' or ''))) + raise TestFailure( + '{pth_file} unexpectedly {maybe}updated by install'.format( + maybe=not without_egg_link and 'not ' or '', + **locals())) if (pkg_dir in self.files_created) == (curdir in without_files): raise TestFailure(textwrap.dedent('''\ - expected package directory %r %sto be created + expected package directory {pkg_dir!r} {maybe}to be created actually created: - %s - ''') % ( - pkg_dir, - (curdir in without_files and 'not ' or ''), - sorted(self.files_created.keys()))) + {files} + ''').format( + pkg_dir=pkg_dir, + maybe=curdir in without_files and 'not ' or '', + files=sorted(self.files_created.keys()), + )) for f in with_files: normalized_path = os.path.normpath(pkg_dir / f) if normalized_path not in self.files_created: raise TestFailure( - 'Package directory %r missing expected content %r' % - (pkg_dir, f) + 'Package directory {pkg_dir!r} missing ' + 'expected content {f!r}'.format(**locals()) ) for f in without_files: normalized_path = os.path.normpath(pkg_dir / f) if normalized_path in self.files_created: raise TestFailure( - 'Package directory %r has unexpected content %f' % - (pkg_dir, f) + 'Package directory {pkg_dir!r} has unexpected content {f}' + .format(**locals()) ) @@ -487,7 +492,7 @@ def __init__(self, base_path, *args, **kwargs): # Expand our absolute path directories into relative for name in ["base", "venv", "bin", "lib", "site_packages", "user_base", "user_site", "user_bin", "scratch"]: - real_name = "%s_path" % name + real_name = "{name}_path".format(**locals()) relative_path = Path(os.path.relpath( getattr(self, real_name), self.base_path )) @@ -537,7 +542,7 @@ def run(self, *args, **kw): compatibility. """ if self.verbose: - print('>> running %s %s' % (args, kw)) + print('>> running {args} {kw}'.format(**locals())) cwd = kw.pop('cwd', None) run_from = kw.pop('run_from', None) @@ -791,7 +796,7 @@ def _vcs_add(script, version_pkg_path, vcs='git'): '-m', 'initial version', cwd=version_pkg_path, ) else: - raise ValueError('Unknown vcs: %r' % vcs) + raise ValueError('Unknown vcs: {vcs}'.format(**locals())) return version_pkg_path @@ -900,7 +905,7 @@ def assert_raises_regexp(exception, reg, run, *args, **kwargs): try: run(*args, **kwargs) - assert False, "%s should have been thrown" % exception + assert False, "{exception} should have been thrown".format(**locals()) except exception: e = sys.exc_info()[1] p = re.compile(reg) @@ -928,9 +933,9 @@ def create_test_package_with_setup(script, **setup_kwargs): pkg_path.mkdir() pkg_path.joinpath("setup.py").write_text(textwrap.dedent(""" from setuptools import setup - kwargs = %r + kwargs = {setup_kwargs!r} setup(**kwargs) - """) % setup_kwargs) + """).format(**locals())) return pkg_path @@ -1075,7 +1080,8 @@ def wrapper(fn): try: subprocess.check_output(check_cmd) except OSError: - return pytest.mark.skip(reason='%s is not available' % name)(fn) + return pytest.mark.skip( + reason='{name} is not available'.format(name=name))(fn) return fn return wrapper diff --git a/tests/lib/path.py b/tests/lib/path.py index 35eefa6bb0a..9ca1e3ecedd 100644 --- a/tests/lib/path.py +++ b/tests/lib/path.py @@ -81,7 +81,7 @@ def __radd__(self, path): return Path(path + _base(self)) def __repr__(self): - return u"Path(%s)" % _base.__repr__(self) + return u"Path({inner})".format(inner=_base.__repr__(self)) def __hash__(self): return _base.__hash__(self) diff --git a/tests/lib/test_lib.py b/tests/lib/test_lib.py index 051196032a2..9c00e9d1f0c 100644 --- a/tests/lib/test_lib.py +++ b/tests/lib/test_lib.py @@ -65,8 +65,8 @@ def test_correct_pip_version(script): if x.endswith('.py') ] assert not mismatch_py, ( - 'mismatched source files in %r and %r: %r' % - (pip_folder, pip_folder_outputed, mismatch_py) + 'mismatched source files in {pip_folder!r} ' + 'and {pip_folder_outputed!r}: {mismatch_py!r}'.format(**locals()) ) diff --git a/tests/unit/test_build_env.py b/tests/unit/test_build_env.py index b4469046bdd..1f3b88b4d87 100644 --- a/tests/unit/test_build_env.py +++ b/tests/unit/test_build_env.py @@ -33,7 +33,7 @@ def run_with_build_env(script, setup_script_contents, link_collector = LinkCollector( session=PipSession(), - search_scope=SearchScope.create([%r], []), + search_scope=SearchScope.create([{scratch!r}], []), ) selection_prefs = SelectionPreferences( allow_yanked=True, @@ -45,7 +45,7 @@ def run_with_build_env(script, setup_script_contents, with global_tempdir_manager(): build_env = BuildEnvironment() - ''' % str(script.scratch_path)) + + '''.format(scratch=str(script.scratch_path))) + indent(dedent(setup_script_contents), ' ') + indent( dedent( @@ -78,14 +78,17 @@ def test_build_env_allow_only_one_install(script): finder = make_test_finder(find_links=[script.scratch_path]) build_env = BuildEnvironment() for prefix in ('normal', 'overlay'): - build_env.install_requirements(finder, ['foo'], prefix, - 'installing foo in %s' % prefix) + build_env.install_requirements( + finder, ['foo'], prefix, + 'installing foo in {prefix}'.format(**locals())) with pytest.raises(AssertionError): - build_env.install_requirements(finder, ['bar'], prefix, - 'installing bar in %s' % prefix) + build_env.install_requirements( + finder, ['bar'], prefix, + 'installing bar in {prefix}'.format(**locals())) with pytest.raises(AssertionError): - build_env.install_requirements(finder, [], prefix, - 'installing in %s' % prefix) + build_env.install_requirements( + finder, [], prefix, + 'installing in {prefix}'.format(**locals())) def test_build_env_requirements_check(script): @@ -201,7 +204,9 @@ def test_build_env_isolation(script): except ImportError: pass else: - print('imported `pkg` from `%s`' % pkg.__file__, file=sys.stderr) + print( + 'imported `pkg` from `{pkg.__file__}`'.format(**locals()), + file=sys.stderr) print('system sites:\n ' + '\n '.join(sorted({ get_python_lib(plat_specific=0), get_python_lib(plat_specific=1), diff --git a/tests/unit/test_collector.py b/tests/unit/test_collector.py index 1872b046ac6..a650a2a17ef 100644 --- a/tests/unit/test_collector.py +++ b/tests/unit/test_collector.py @@ -486,8 +486,8 @@ def test_group_locations__file_expand_dir(data): """ files, urls = group_locations([data.find_links], expand_dir=True) assert files and not urls, ( - "files and not urls should have been found at find-links url: %s" % - data.find_links + "files and not urls should have been found " + "at find-links url: {data.find_links}".format(**locals()) ) diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index 7e3feaa5ce7..530c3735c5e 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -115,8 +115,9 @@ def test_no_reuse_existing_build_dir(self, data): with self._basic_resolver(finder) as resolver: assert_raises_regexp( PreviousBuildDirError, - r"pip can't proceed with [\s\S]*%s[\s\S]*%s" % - (req, build_dir.replace('\\', '\\\\')), + r"pip can't proceed with [\s\S]*{req}[\s\S]*{build_dir_esc}" + .format( + build_dir_esc=build_dir.replace('\\', '\\\\'), req=req), resolver.resolve, reqset.all_requirements, True, @@ -195,7 +196,7 @@ def test_unsupported_hashes(self, data): )) dir_path = data.packages.joinpath('FSPkg') reqset.add_requirement(get_processed_req_from_line( - 'file://%s' % (dir_path,), + 'file://{dir_path}'.format(**locals()), lineno=2, )) finder = make_test_finder(find_links=[data.find_links]) @@ -255,7 +256,7 @@ def test_hash_mismatch(self, data): (data.packages / 'simple-1.0.tar.gz').resolve()) reqset = RequirementSet() reqset.add_requirement(get_processed_req_from_line( - '%s --hash=sha256:badbad' % file_url, lineno=1, + '{file_url} --hash=sha256:badbad'.format(**locals()), lineno=1, )) finder = make_test_finder(find_links=[data.find_links]) with self._basic_resolver(finder, require_hashes=True) as resolver: @@ -471,7 +472,7 @@ def test_markers_match_from_line(self): # match for markers in ( 'python_version >= "1.0"', - 'sys_platform == %r' % sys.platform, + 'sys_platform == {sys.platform!r}'.format(**globals()), ): line = 'name; ' + markers req = install_req_from_line(line) @@ -481,7 +482,7 @@ def test_markers_match_from_line(self): # don't match for markers in ( 'python_version >= "5.0"', - 'sys_platform != %r' % sys.platform, + 'sys_platform != {sys.platform!r}'.format(**globals()), ): line = 'name; ' + markers req = install_req_from_line(line) @@ -492,7 +493,7 @@ def test_markers_match(self): # match for markers in ( 'python_version >= "1.0"', - 'sys_platform == %r' % sys.platform, + 'sys_platform == {sys.platform!r}'.format(**globals()), ): line = 'name; ' + markers req = install_req_from_line(line, comes_from='') @@ -502,7 +503,7 @@ def test_markers_match(self): # don't match for markers in ( 'python_version >= "5.0"', - 'sys_platform != %r' % sys.platform, + 'sys_platform != {sys.platform!r}'.format(**globals()), ): line = 'name; ' + markers req = install_req_from_line(line, comes_from='') diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py index ea9cebba90e..5b61c25b23b 100644 --- a/tests/unit/test_req_file.py +++ b/tests/unit/test_req_file.py @@ -2,6 +2,7 @@ import os import subprocess import textwrap +import collections import pytest from mock import patch @@ -304,7 +305,7 @@ def test_yield_line_requirement_with_spaces_in_specifier( def test_yield_editable_requirement(self, line_processor): url = 'git+https://url#egg=SomeProject' - line = '-e %s' % url + line = '-e {url}'.format(**locals()) filename = 'filename' comes_from = '-r {} (line {})'.format(filename, 1) req = install_req_from_editable(url, comes_from=comes_from) @@ -625,19 +626,23 @@ def test_skip_regex(self, tmpdir, finder, options): def test_expand_existing_env_variables(self, tmpdir, finder): template = ( - 'https://%s:x-oauth-basic@github.com/user/%s/archive/master.zip' + 'https://{}:x-oauth-basic@github.com/' + 'user/{}/archive/master.zip' ) - env_vars = ( + def make_var(name): + return '${{{name}}}'.format(**locals()) + + env_vars = collections.OrderedDict([ ('GITHUB_TOKEN', 'notarealtoken'), ('DO_12_FACTOR', 'awwyeah'), - ) + ]) with open(tmpdir.joinpath('req1.txt'), 'w') as fp: - fp.write(template % tuple(['${%s}' % k for k, _ in env_vars])) + fp.write(template.format(*map(make_var, env_vars))) with patch('pip._internal.req.req_file.os.getenv') as getenv: - getenv.side_effect = lambda n: dict(env_vars)[n] + getenv.side_effect = lambda n: env_vars[n] reqs = list(parse_reqfile( tmpdir.joinpath('req1.txt'), @@ -645,12 +650,12 @@ def test_expand_existing_env_variables(self, tmpdir, finder): session=PipSession() )) - assert len(reqs) == 1, \ - 'parsing requirement file with env variable failed' + assert len(reqs) == 1, \ + 'parsing requirement file with env variable failed' - expected_url = template % tuple([v for _, v in env_vars]) - assert reqs[0].link.url == expected_url, \ - 'variable expansion in req file failed' + expected_url = template.format(*env_vars.values()) + assert reqs[0].link.url == expected_url, \ + 'variable expansion in req file failed' def test_expand_missing_env_variables(self, tmpdir, finder): req_url = ( From 816efa1805e30243f5555f530f574319c7a8de4c Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" <jaraco@jaraco.com> Date: Fri, 6 Mar 2020 11:50:07 -0500 Subject: [PATCH 1340/3170] Update changelog --- news/7826.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/7826.feature diff --git a/news/7826.feature b/news/7826.feature new file mode 100644 index 00000000000..7c6897cc802 --- /dev/null +++ b/news/7826.feature @@ -0,0 +1 @@ +Replaced remaining uses of '%' formatting with .format. Fixed two regressions introduced in earlier attempts. From 6282a307dc5f20e3ff389fb8d8e1cbc7bc6d021f Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" <jaraco@jaraco.com> Date: Fri, 6 Mar 2020 12:30:16 -0500 Subject: [PATCH 1341/3170] =?UTF-8?q?=F0=9F=91=B9=20Feed=20the=20hobgoblin?= =?UTF-8?q?s=20(delint).?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/pip/_internal/cli/progress_bars.py | 20 +++++-------- src/pip/_internal/cli/req_command.py | 2 +- src/pip/_internal/cli/spinners.py | 3 +- src/pip/_internal/commands/completion.py | 3 +- src/pip/_internal/models/link.py | 3 +- src/pip/_internal/req/req_install.py | 3 +- src/pip/_internal/utils/filesystem.py | 3 +- src/pip/_internal/utils/urls.py | 3 +- src/pip/_internal/vcs/subversion.py | 3 +- tests/conftest.py | 6 ++-- tests/functional/test_freeze.py | 2 +- tests/functional/test_install.py | 37 ++++++++++++++++-------- tests/functional/test_install_cleanup.py | 9 ++++-- tests/functional/test_install_compat.py | 9 ++++-- tests/functional/test_install_upgrade.py | 3 +- tests/functional/test_install_user.py | 15 ++++++---- tests/functional/test_install_vcs_git.py | 2 +- tests/functional/test_uninstall_user.py | 3 +- tests/functional/test_wheel.py | 35 ++++++++++++++-------- tests/unit/test_req_file.py | 2 +- 20 files changed, 104 insertions(+), 62 deletions(-) diff --git a/src/pip/_internal/cli/progress_bars.py b/src/pip/_internal/cli/progress_bars.py index baf40f52373..4647f8726bc 100644 --- a/src/pip/_internal/cli/progress_bars.py +++ b/src/pip/_internal/cli/progress_bars.py @@ -234,16 +234,6 @@ class DownloadBlueEmojiProgressBar(BaseDownloadProgressBar, # type: ignore pass -class ObjectMapAdapter: - """Translate getitem to getattr.""" - - def __init__(self, obj): - self.obj = obj - - def __getitem__(self, key): - return getattr(self.obj, key) - - class DownloadProgressSpinner(WindowsMixin, InterruptibleMixin, DownloadProgressMixin, Spinner): @@ -257,9 +247,15 @@ def next_phase(self): # type: ignore def update(self): # type: () -> None - message = self.message.format_map(ObjectMapAdapter(self)) + vals = dict( + downloaded=self.downloaded, + download_speed=self.download_speed, + pretty_eta=self.pretty_eta, + percent=self.percent, + ) + message = self.message.format(**vals) phase = self.next_phase() - suffix = self.suffix.format_map(ObjectMapAdapter(self)) + suffix = self.suffix.format(**vals) line = " ".join(filter(None, (message, phase, suffix))) self.writeln(line) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index db7b3138a58..45ddb8b47f3 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -344,7 +344,7 @@ def get_requirements( raise CommandError( 'You must give at least one requirement to {name} ' '(maybe you meant "pip {name} {links}"?)'.format( - **dict(opts, links=' '.join(options.find_links)))) + **dict(opts, links=' '.join(options.find_links)))) else: raise CommandError( 'You must give at least one requirement to {name} ' diff --git a/src/pip/_internal/cli/spinners.py b/src/pip/_internal/cli/spinners.py index 8e8e44eeb5c..c6c4c5cd1b1 100644 --- a/src/pip/_internal/cli/spinners.py +++ b/src/pip/_internal/cli/spinners.py @@ -106,7 +106,8 @@ def finish(self, final_status): # type: (str) -> None if self._finished: return - self._update("finished with status '{final_status}'".format(**locals())) + self._update( + "finished with status '{final_status}'".format(**locals())) self._finished = True diff --git a/src/pip/_internal/commands/completion.py b/src/pip/_internal/commands/completion.py index b2a793903a3..910fcbfe358 100644 --- a/src/pip/_internal/commands/completion.py +++ b/src/pip/_internal/commands/completion.py @@ -85,7 +85,8 @@ def run(self, options, args): shell_options = ['--' + shell for shell in sorted(shells)] if options.shell in shells: script = textwrap.dedent( - COMPLETION_SCRIPTS.get(options.shell, '').format(prog=get_prog()) + COMPLETION_SCRIPTS.get(options.shell, '').format( + prog=get_prog()) ) print(BASE_COMPLETION.format(script=script, shell=options.shell)) else: diff --git a/src/pip/_internal/models/link.py b/src/pip/_internal/models/link.py index ed057de196b..c3e743d3904 100644 --- a/src/pip/_internal/models/link.py +++ b/src/pip/_internal/models/link.py @@ -96,7 +96,8 @@ def filename(self): return netloc name = urllib_parse.unquote(name) - assert name, ('URL {self._url!r} produced no filename'.format(**locals())) + assert name, ( + 'URL {self._url!r} produced no filename'.format(**locals())) return name @property diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index ce0309932cb..dccb0c795e9 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -639,7 +639,8 @@ def update_editable(self, obtain=True): if self.link.scheme == 'file': # Static paths don't get updated return - assert '+' in self.link.url, "bad url: {self.link.url!r}".format(**locals()) + assert '+' in self.link.url, \ + "bad url: {self.link.url!r}".format(**locals()) vc_type, url = self.link.url.split('+', 1) vcs_backend = vcs.get_backend(vc_type) if vcs_backend: diff --git a/src/pip/_internal/utils/filesystem.py b/src/pip/_internal/utils/filesystem.py index a0866779719..161c450aafd 100644 --- a/src/pip/_internal/utils/filesystem.py +++ b/src/pip/_internal/utils/filesystem.py @@ -73,7 +73,8 @@ def copy2_fixed(src, dest): pass else: if is_socket_file: - raise shutil.SpecialFileError("`{f}` is a socket".format(**locals())) + raise shutil.SpecialFileError( + "`{f}` is a socket".format(**locals())) raise diff --git a/src/pip/_internal/utils/urls.py b/src/pip/_internal/utils/urls.py index a3dcc83b527..f37bc8f90b2 100644 --- a/src/pip/_internal/utils/urls.py +++ b/src/pip/_internal/utils/urls.py @@ -34,7 +34,8 @@ def url_to_path(url): Convert a file: URL to a path. """ assert url.startswith('file:'), ( - "You can only turn file: urls into filenames (not {url!r})".format(**locals())) + "You can only turn file: urls into filenames (not {url!r})" + .format(**locals())) _, netloc, path, _, _ = urllib_parse.urlsplit(url) diff --git a/src/pip/_internal/vcs/subversion.py b/src/pip/_internal/vcs/subversion.py index 064fb77e50e..0ec65974492 100644 --- a/src/pip/_internal/vcs/subversion.py +++ b/src/pip/_internal/vcs/subversion.py @@ -151,7 +151,8 @@ def _get_svn_url_rev(cls, location): elif data.startswith('<?xml'): match = _svn_xml_url_re.search(data) if not match: - raise ValueError('Badly formatted data: {data!r}'.format(**locals())) + raise ValueError( + 'Badly formatted data: {data!r}'.format(**locals())) url = match.group(1) # get repository URL revs = [int(m.group(1)) for m in _svn_rev_re.finditer(data)] + [0] else: diff --git a/tests/conftest.py b/tests/conftest.py index f539bb32be2..077de95280c 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -234,11 +234,13 @@ def not_code_files_and_folders(path, names): def _common_wheel_editable_install(tmpdir_factory, common_wheels, package): - wheel_candidates = list(common_wheels.glob('{package}-*.whl'.format(**locals()))) + wheel_candidates = list( + common_wheels.glob('{package}-*.whl'.format(**locals()))) assert len(wheel_candidates) == 1, wheel_candidates install_dir = Path(str(tmpdir_factory.mktemp(package))) / 'install' Wheel(wheel_candidates[0]).install_as_egg(install_dir) - (install_dir / 'EGG-INFO').rename(install_dir / '{package}.egg-info'.format(**locals())) + (install_dir / 'EGG-INFO').rename( + install_dir / '{package}.egg-info'.format(**locals())) assert compileall.compile_dir(str(install_dir), quiet=1) return install_dir diff --git a/tests/functional/test_freeze.py b/tests/functional/test_freeze.py index c4842b7ac51..dabfbde483c 100644 --- a/tests/functional/test_freeze.py +++ b/tests/functional/test_freeze.py @@ -468,7 +468,7 @@ def test_freeze_bazaar_clone(script, tmpdir): try: checkout_path = _create_test_package(script, vcs='bazaar') except OSError as e: - pytest.fail('Invoking `bzr` failed: {e}'.format(**locals())) + pytest.fail('Invoking `bzr` failed: {e}'.format(e=e)) result = script.run( 'bzr', 'checkout', checkout_path, 'bzr-package' diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 21cd94cb951..330cc34836d 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -79,10 +79,12 @@ def test_pep518_refuses_conflicting_requires(script, data): result = script.pip_install_local('-f', script.scratch_path, project_dir, expect_error=True) assert ( - result.returncode != 0 and - ('Some build dependencies for {url} conflict with PEP 517/518 supported ' - 'requirements: setuptools==1.0 is incompatible with ' - 'setuptools>=40.8.0.'.format(url=path_to_url(project_dir))) in result.stderr + result.returncode != 0 and ( + 'Some build dependencies for {url} conflict ' + 'with PEP 517/518 supported ' + 'requirements: setuptools==1.0 is incompatible with ' + 'setuptools>=40.8.0.' + .format(url=path_to_url(project_dir))) in result.stderr ), str(result) @@ -283,7 +285,10 @@ def test_basic_install_editable_from_svn(script): def _test_install_editable_from_git(script, tmpdir): """Test cloning from Git.""" pkg_path = _create_test_package(script, name='testpackage', vcs='git') - args = ['install', '-e', 'git+{url}#egg=testpackage'.format(url=path_to_url(pkg_path))] + args = [ + 'install', '-e', + 'git+{url}#egg=testpackage'.format(url=path_to_url(pkg_path)), + ] result = script.pip(*args) result.assert_installed('testpackage', with_files=['.git']) @@ -314,8 +319,8 @@ def test_install_editable_uninstalls_existing(data, script, tmpdir): 'install', '-e', '{dir}#egg=pip-test-package'.format( dir=local_checkout( - 'git+https://github.com/pypa/pip-test-package.git', tmpdir, - )), + 'git+https://github.com/pypa/pip-test-package.git', tmpdir, + )), ) result.assert_installed('pip-test-package', with_files=['.git']) assert 'Found existing installation: pip-test-package 0.1' in result.stdout @@ -364,7 +369,9 @@ def test_vcs_url_final_slash_normalization(script, tmpdir): Test that presence or absence of final slash in VCS URL is normalized. """ pkg_path = _create_test_package(script, name='testpackage', vcs='hg') - args = ['install', '-e', 'hg+{url}/#egg=testpackage'.format(url=path_to_url(pkg_path))] + args = [ + 'install', + '-e', 'hg+{url}/#egg=testpackage'.format(url=path_to_url(pkg_path))] result = script.pip(*args) result.assert_installed('testpackage', with_files=['.hg']) @@ -373,7 +380,9 @@ def test_vcs_url_final_slash_normalization(script, tmpdir): def test_install_editable_from_bazaar(script, tmpdir): """Test checking out from Bazaar.""" pkg_path = _create_test_package(script, name='testpackage', vcs='bazaar') - args = ['install', '-e', 'bzr+{url}/#egg=testpackage'.format(url=path_to_url(pkg_path))] + args = [ + 'install', + '-e', 'bzr+{url}/#egg=testpackage'.format(url=path_to_url(pkg_path))] result = script.pip(*args) result.assert_installed('testpackage', with_files=['.bzr']) @@ -388,7 +397,8 @@ def test_vcs_url_urlquote_normalization(script, tmpdir): 'install', '-e', '{url}/#egg=django-wikiapp'.format( url=local_checkout( - 'bzr+http://bazaar.launchpad.net/%7Edjango-wikiapp/django-wikiapp' + 'bzr+http://bazaar.launchpad.net/' + '%7Edjango-wikiapp/django-wikiapp' '/release-0.1', tmpdir, )), @@ -1440,7 +1450,8 @@ def test_install_editable_with_wrong_egg_name(script): version='0.1') """)) result = script.pip( - 'install', '--editable', 'file://{pkga_path}#egg=pkgb'.format(**locals()) + 'install', '--editable', + 'file://{pkga_path}#egg=pkgb'.format(**locals()), ) assert ("Generating metadata for package pkgb produced metadata " "for project name pkga. Fix your #egg=pkgb " @@ -1514,7 +1525,9 @@ def test_install_incompatible_python_requires_editable(script): version='0.1') """)) result = script.pip( - 'install', '--editable={pkga_path}'.format(**locals()), expect_error=True) + 'install', + '--editable={pkga_path}'.format(**locals()), + expect_error=True) assert _get_expected_error_text() in result.stderr, str(result) diff --git a/tests/functional/test_install_cleanup.py b/tests/functional/test_install_cleanup.py index 7f91be3a4b8..131caf681e3 100644 --- a/tests/functional/test_install_cleanup.py +++ b/tests/functional/test_install_cleanup.py @@ -85,7 +85,8 @@ def test_cleanup_req_satisfied_no_name(script, data): script.pip('install', dist) build = script.venv_path / 'build' - assert not exists(build), "unexpected build/ dir exists: {build}".format(**locals()) + assert not exists(build), \ + "unexpected build/ dir exists: {build}".format(**locals()) script.assert_no_temp() @@ -99,7 +100,8 @@ def test_cleanup_after_install_exception(script, data): expect_error=True, ) build = script.venv_path / 'build' - assert not exists(build), "build/ dir still exists: {result.stdout}".format(**locals()) + assert not exists(build), \ + "build/ dir still exists: {result.stdout}".format(**locals()) script.assert_no_temp() @@ -113,7 +115,8 @@ def test_cleanup_after_egg_info_exception(script, data): expect_error=True, ) build = script.venv_path / 'build' - assert not exists(build), "build/ dir still exists: {result.stdout}".format(**locals()) + assert not exists(build), \ + "build/ dir still exists: {result.stdout}".format(**locals()) script.assert_no_temp() diff --git a/tests/functional/test_install_compat.py b/tests/functional/test_install_compat.py index e5401985b17..60d505188be 100644 --- a/tests/functional/test_install_compat.py +++ b/tests/functional/test_install_compat.py @@ -6,7 +6,8 @@ import pytest -from tests.lib import assert_all_changes, pyversion +from tests.lib import pyversion # noqa: F401 +from tests.lib import assert_all_changes @pytest.mark.network @@ -31,11 +32,13 @@ def test_debian_egg_name_workaround(script): # so even if this test runs on a Debian/Ubuntu system with broken # setuptools, since our test runs inside a venv we'll still have the normal # .egg-info - assert egg_info in result.files_created, "Couldn't find {egg_info}".format(**locals()) + assert egg_info in result.files_created, \ + "Couldn't find {egg_info}".format(**locals()) # The Debian no-pyversion version of the .egg-info mangled = os.path.join(script.site_packages, "INITools-0.2.egg-info") - assert mangled not in result.files_created, "Found unexpected {mangled}".format(**locals()) + assert mangled not in result.files_created, \ + "Found unexpected {mangled}".format(**locals()) # Simulate a Debian install by copying the .egg-info to their name for it full_egg_info = os.path.join(script.base_path, egg_info) diff --git a/tests/functional/test_install_upgrade.py b/tests/functional/test_install_upgrade.py index 87ccd893afc..f5445a0b3e6 100644 --- a/tests/functional/test_install_upgrade.py +++ b/tests/functional/test_install_upgrade.py @@ -4,7 +4,8 @@ import pytest -from tests.lib import assert_all_changes, pyversion +from tests.lib import pyversion # noqa: F401 +from tests.lib import assert_all_changes from tests.lib.local_repos import local_checkout diff --git a/tests/functional/test_install_user.py b/tests/functional/test_install_user.py index 8432dbc0bd6..09dbdf4912a 100644 --- a/tests/functional/test_install_user.py +++ b/tests/functional/test_install_user.py @@ -6,7 +6,8 @@ import pytest -from tests.lib import need_svn, pyversion +from tests.lib import pyversion # noqa: F401 +from tests.lib import need_svn from tests.lib.local_repos import local_checkout @@ -112,7 +113,8 @@ def test_install_user_conflict_in_usersite(self, script): # usersite has 0.1 egg_info_folder = ( - script.user_site / 'INITools-0.1-py{pyversion}.egg-info'.format(**globals()) + script.user_site / + 'INITools-0.1-py{pyversion}.egg-info'.format(**globals()) ) initools_v3_file = ( # file only in 0.3 @@ -138,7 +140,8 @@ def test_install_user_conflict_in_globalsite(self, virtualenv, script): # usersite has 0.1 egg_info_folder = ( - script.user_site / 'INITools-0.1-py{pyversion}.egg-info'.format(**globals()) + script.user_site / + 'INITools-0.1-py{pyversion}.egg-info'.format(**globals()) ) initools_folder = script.user_site / 'initools' assert egg_info_folder in result2.files_created, str(result2) @@ -168,7 +171,8 @@ def test_upgrade_user_conflict_in_globalsite(self, virtualenv, script): # usersite has 0.3.1 egg_info_folder = ( - script.user_site / 'INITools-0.3.1-py{pyversion}.egg-info'.format(**globals()) + script.user_site / + 'INITools-0.3.1-py{pyversion}.egg-info'.format(**globals()) ) initools_folder = script.user_site / 'initools' assert egg_info_folder in result2.files_created, str(result2) @@ -201,7 +205,8 @@ def test_install_user_conflict_in_globalsite_and_usersite( # usersite has 0.1 egg_info_folder = ( - script.user_site / 'INITools-0.1-py{pyversion}.egg-info'.format(**globals()) + script.user_site / + 'INITools-0.1-py{pyversion}.egg-info'.format(**globals()) ) initools_v3_file = ( # file only in 0.3 diff --git a/tests/functional/test_install_vcs_git.py b/tests/functional/test_install_vcs_git.py index 3cb4d1490b2..6c6f5a0c7d7 100644 --- a/tests/functional/test_install_vcs_git.py +++ b/tests/functional/test_install_vcs_git.py @@ -1,10 +1,10 @@ import pytest +from tests.lib import pyversion # noqa: F401 from tests.lib import ( _change_test_package_version, _create_test_package, _test_path_to_file_url, - pyversion, ) from tests.lib.git_submodule_helpers import ( _change_test_package_submodule, diff --git a/tests/functional/test_uninstall_user.py b/tests/functional/test_uninstall_user.py index 1b19294b78d..df635ccf8f8 100644 --- a/tests/functional/test_uninstall_user.py +++ b/tests/functional/test_uninstall_user.py @@ -6,7 +6,8 @@ import pytest from tests.functional.test_install_user import _patch_dist_in_site_packages -from tests.lib import assert_all_changes, pyversion +from tests.lib import pyversion # noqa: F401 +from tests.lib import assert_all_changes @pytest.mark.incompatible_with_test_venv diff --git a/tests/functional/test_wheel.py b/tests/functional/test_wheel.py index 3c8c528be34..ce79dbee5ee 100644 --- a/tests/functional/test_wheel.py +++ b/tests/functional/test_wheel.py @@ -6,7 +6,7 @@ import pytest from pip._internal.cli.status_codes import ERROR, PREVIOUS_BUILD_DIR_ERROR -from tests.lib import pyversion +from tests.lib import pyversion # noqa: F401 @pytest.fixture(autouse=True) @@ -69,7 +69,8 @@ def test_pip_wheel_build_cache(script, data): 'wheel', '--no-index', '-f', data.find_links, 'simple==3.0', ) - wheel_file_name = 'simple-3.0-py{pyversion[0]}-none-any.whl'.format(**globals()) + wheel_file_name = 'simple-3.0-py{pyversion[0]}-none-any.whl' \ + .format(**globals()) wheel_file_path = script.scratch / wheel_file_name assert wheel_file_path in result.files_created, result.stdout assert "Successfully built simple" in result.stdout, result.stdout @@ -146,7 +147,8 @@ def test_pip_wheel_builds_editable_deps(script, data): 'wheel', '--no-index', '-f', data.find_links, '-e', editable_path ) - wheel_file_name = 'simple-1.0-py{pyversion[0]}-none-any.whl'.format(**globals()) + wheel_file_name = 'simple-1.0-py{pyversion[0]}-none-any.whl' \ + .format(**globals()) wheel_file_path = script.scratch / wheel_file_name assert wheel_file_path in result.files_created, result.stdout @@ -160,7 +162,8 @@ def test_pip_wheel_builds_editable(script, data): 'wheel', '--no-index', '-f', data.find_links, '-e', editable_path ) - wheel_file_name = 'simplewheel-1.0-py{pyversion[0]}-none-any.whl'.format(**globals()) + wheel_file_name = 'simplewheel-1.0-py{pyversion[0]}-none-any.whl' \ + .format(**globals()) wheel_file_path = script.scratch / wheel_file_name assert wheel_file_path in result.files_created, result.stdout @@ -174,7 +177,8 @@ def test_pip_wheel_fail(script, data): 'wheelbroken==0.1', expect_error=True, ) - wheel_file_name = 'wheelbroken-0.1-py{pyversion[0]}-none-any.whl'.format(**globals()) + wheel_file_name = 'wheelbroken-0.1-py{pyversion[0]}-none-any.whl' \ + .format(**globals()) wheel_file_path = script.scratch / wheel_file_name assert wheel_file_path not in result.files_created, ( wheel_file_path, @@ -197,7 +201,8 @@ def test_no_clean_option_blocks_cleaning_after_wheel(script, data): expect_temp=True, ) build = build / 'simple' - assert exists(build), "build/simple should still exist {result}".format(**locals()) + assert exists(build), \ + "build/simple should still exist {result}".format(**locals()) def test_pip_wheel_source_deps(script, data): @@ -210,7 +215,8 @@ def test_pip_wheel_source_deps(script, data): 'wheel', '--no-index', '-f', data.find_links, 'requires_source', ) - wheel_file_name = 'source-1.0-py{pyversion[0]}-none-any.whl'.format(**globals()) + wheel_file_name = 'source-1.0-py{pyversion[0]}-none-any.whl' \ + .format(**globals()) wheel_file_path = script.scratch / wheel_file_name assert wheel_file_path in result.files_created, result.stdout assert "Successfully built source" in result.stdout, result.stdout @@ -229,7 +235,8 @@ def test_pip_wheel_fail_cause_of_previous_build_dir(script, data): # When I call pip trying to install things again result = script.pip( - 'wheel', '--no-index', '--find-links={data.find_links}'.format(**locals()), + 'wheel', '--no-index', + '--find-links={data.find_links}'.format(**locals()), '--build', script.venv_path / 'build', 'simple==3.0', expect_error=True, expect_temp=True, ) @@ -249,7 +256,8 @@ def test_wheel_package_with_latin1_setup(script, data): def test_pip_wheel_with_pep518_build_reqs(script, data, common_wheels): result = script.pip('wheel', '--no-index', '-f', data.find_links, '-f', common_wheels, 'pep518==3.0',) - wheel_file_name = 'pep518-3.0-py{pyversion[0]}-none-any.whl'.format(**globals()) + wheel_file_name = 'pep518-3.0-py{pyversion[0]}-none-any.whl' \ + .format(**globals()) wheel_file_path = script.scratch / wheel_file_name assert wheel_file_path in result.files_created, result.stdout assert "Successfully built pep518" in result.stdout, result.stdout @@ -262,7 +270,8 @@ def test_pip_wheel_with_pep518_build_reqs_no_isolation(script, data): 'wheel', '--no-index', '-f', data.find_links, '--no-build-isolation', 'pep518==3.0', ) - wheel_file_name = 'pep518-3.0-py{pyversion[0]}-none-any.whl'.format(**globals()) + wheel_file_name = 'pep518-3.0-py{pyversion[0]}-none-any.whl' \ + .format(**globals()) wheel_file_path = script.scratch / wheel_file_name assert wheel_file_path in result.files_created, result.stdout assert "Successfully built pep518" in result.stdout, result.stdout @@ -290,7 +299,8 @@ def test_pep517_wheels_are_not_confused_with_other_files(script, tmpdir, data): result = script.pip('wheel', pkg_to_wheel, '-w', script.scratch_path) assert "Installing build dependencies" in result.stdout, result.stdout - wheel_file_name = 'withpyproject-0.0.1-py{pyversion[0]}-none-any.whl'.format(**globals()) + wheel_file_name = 'withpyproject-0.0.1-py{pyversion[0]}-none-any.whl' \ + .format(**globals()) wheel_file_path = script.scratch / wheel_file_name assert wheel_file_path in result.files_created, result.stdout @@ -304,6 +314,7 @@ def test_legacy_wheels_are_not_confused_with_other_files(script, tmpdir, data): result = script.pip('wheel', pkg_to_wheel, '-w', script.scratch_path) assert "Installing build dependencies" not in result.stdout, result.stdout - wheel_file_name = 'simplewheel-1.0-py{pyversion[0]}-none-any.whl'.format(**globals()) + wheel_file_name = 'simplewheel-1.0-py{pyversion[0]}-none-any.whl' \ + .format(**globals()) wheel_file_path = script.scratch / wheel_file_name assert wheel_file_path in result.files_created, result.stdout diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py index 5b61c25b23b..c12cdb64bf6 100644 --- a/tests/unit/test_req_file.py +++ b/tests/unit/test_req_file.py @@ -1,8 +1,8 @@ +import collections import logging import os import subprocess import textwrap -import collections import pytest from mock import patch From def75dc6917ed26606b0a6be0f1bdac1ce6a7fd8 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" <jaraco@jaraco.com> Date: Fri, 6 Mar 2020 12:53:54 -0500 Subject: [PATCH 1342/3170] Fix issue where format_map isn't available on Python 2 --- src/pip/_internal/cli/progress_bars.py | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/cli/progress_bars.py b/src/pip/_internal/cli/progress_bars.py index 4647f8726bc..b45d176df46 100644 --- a/src/pip/_internal/cli/progress_bars.py +++ b/src/pip/_internal/cli/progress_bars.py @@ -14,7 +14,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Any, Dict, List + from typing import Any, Dict, Iterator, List, Tuple try: from pip._vendor import colorama @@ -247,18 +247,22 @@ def next_phase(self): # type: ignore def update(self): # type: () -> None - vals = dict( - downloaded=self.downloaded, - download_speed=self.download_speed, - pretty_eta=self.pretty_eta, - percent=self.percent, - ) + vals = dict(self._load_vals( + 'downloaded', 'download_speed', 'pretty_eta', 'percent')) message = self.message.format(**vals) phase = self.next_phase() suffix = self.suffix.format(**vals) line = " ".join(filter(None, (message, phase, suffix))) self.writeln(line) + def _load_vals(self, *names): + # type: (*str) -> Iterator[Tuple[str, Any]] + for name in names: + try: + yield name, getattr(self, name) + except Exception: + pass + BAR_TYPES = { "off": (DownloadSilentBar, DownloadSilentBar), From 420a6d26debf963c845f507363d20b19edfaf4a8 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 8 Mar 2020 18:01:49 -0400 Subject: [PATCH 1343/3170] Pass combined download_location to _copy_file This makes the function arguments look more like `src` and `dest`, which makes more sense for a file copying function. --- src/pip/_internal/operations/prepare.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 3065005e0c3..b88e6b16e81 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -102,10 +102,9 @@ def unpack_vcs_link(link, location): vcs_backend.unpack(location, url=hide_url(link.url)) -def _copy_file(filename, location, link): - # type: (str, str, Link) -> None +def _copy_file(filename, download_location): + # type: (str, str) -> None copy = True - download_location = os.path.join(location, link.filename) if os.path.exists(download_location): response = ask_path_exists( 'The file {} exists. (i)gnore, (w)ipe, (b)ackup, (a)abort'.format( @@ -516,7 +515,10 @@ def prepare_linked_requirement( elif local_file and not os.path.exists( os.path.join(download_dir, link.filename) ): - _copy_file(local_file.path, download_dir, link) + download_location = os.path.join( + download_dir, link.filename + ) + _copy_file(local_file.path, download_location) if self._download_should_save: # Make a .zip of the source_dir we already created. From b0cd7a7a0b3ecb645b064ef98960d4655f69a72f Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 8 Mar 2020 18:04:43 -0400 Subject: [PATCH 1344/3170] Assert that download_location doesn't exist in _copy_file --- src/pip/_internal/operations/prepare.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index b88e6b16e81..689c71c1521 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -105,6 +105,7 @@ def unpack_vcs_link(link, location): def _copy_file(filename, download_location): # type: (str, str) -> None copy = True + assert not os.path.exists(download_location) if os.path.exists(download_location): response = ask_path_exists( 'The file {} exists. (i)gnore, (w)ipe, (b)ackup, (a)abort'.format( @@ -512,13 +513,12 @@ def prepare_linked_requirement( if download_dir: if link.is_existing_dir(): logger.info('Link is a directory, ignoring download_dir') - elif local_file and not os.path.exists( - os.path.join(download_dir, link.filename) - ): + elif local_file: download_location = os.path.join( download_dir, link.filename ) - _copy_file(local_file.path, download_location) + if not os.path.exists(download_location): + _copy_file(local_file.path, download_location) if self._download_should_save: # Make a .zip of the source_dir we already created. From e1d1b1c19251bb8cbfef625ff7e348013ae9753e Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 8 Mar 2020 18:05:28 -0400 Subject: [PATCH 1345/3170] Remove dead code Since download_location doesn't exist, this block could've never been executed. --- src/pip/_internal/operations/prepare.py | 26 ------------------------- 1 file changed, 26 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 689c71c1521..20d5bdaec51 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -8,7 +8,6 @@ import mimetypes import os import shutil -import sys from pip._vendor import requests from pip._vendor.six import PY2 @@ -29,8 +28,6 @@ from pip._internal.utils.hashes import MissingHashes from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import ( - ask_path_exists, - backup_dir, display_path, hide_url, path_to_display, @@ -105,29 +102,6 @@ def unpack_vcs_link(link, location): def _copy_file(filename, download_location): # type: (str, str) -> None copy = True - assert not os.path.exists(download_location) - if os.path.exists(download_location): - response = ask_path_exists( - 'The file {} exists. (i)gnore, (w)ipe, (b)ackup, (a)abort'.format( - display_path(download_location) - ), - ('i', 'w', 'b', 'a'), - ) - if response == 'i': - copy = False - elif response == 'w': - logger.warning('Deleting %s', display_path(download_location)) - os.remove(download_location) - elif response == 'b': - dest_file = backup_dir(download_location) - logger.warning( - 'Backing up %s to %s', - display_path(download_location), - display_path(dest_file), - ) - shutil.move(download_location, dest_file) - elif response == 'a': - sys.exit(-1) if copy: shutil.copy(filename, download_location) logger.info('Saved %s', display_path(download_location)) From 36d52edeb4b55e0354dd4c6a514c2c0d6cf0c700 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 8 Mar 2020 18:06:21 -0400 Subject: [PATCH 1346/3170] Inline variable --- src/pip/_internal/operations/prepare.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 20d5bdaec51..d3063d85b9e 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -101,8 +101,7 @@ def unpack_vcs_link(link, location): def _copy_file(filename, download_location): # type: (str, str) -> None - copy = True - if copy: + if True: shutil.copy(filename, download_location) logger.info('Saved %s', display_path(download_location)) From cd5d8b7865b21326310d506306445c39a1bf999c Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 8 Mar 2020 18:06:41 -0400 Subject: [PATCH 1347/3170] Inline unconditional block --- src/pip/_internal/operations/prepare.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index d3063d85b9e..b3f98308e21 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -101,9 +101,8 @@ def unpack_vcs_link(link, location): def _copy_file(filename, download_location): # type: (str, str) -> None - if True: - shutil.copy(filename, download_location) - logger.info('Saved %s', display_path(download_location)) + shutil.copy(filename, download_location) + logger.info('Saved %s', display_path(download_location)) class File(object): From fad99dce4a4c914279cbdac37b7236d7c20d7cdf Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 8 Mar 2020 18:07:11 -0400 Subject: [PATCH 1348/3170] Inline _copy_file --- src/pip/_internal/operations/prepare.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index b3f98308e21..9f87148c031 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -99,12 +99,6 @@ def unpack_vcs_link(link, location): vcs_backend.unpack(location, url=hide_url(link.url)) -def _copy_file(filename, download_location): - # type: (str, str) -> None - shutil.copy(filename, download_location) - logger.info('Saved %s', display_path(download_location)) - - class File(object): def __init__(self, path, content_type): # type: (str, str) -> None @@ -490,7 +484,10 @@ def prepare_linked_requirement( download_dir, link.filename ) if not os.path.exists(download_location): - _copy_file(local_file.path, download_location) + shutil.copy(local_file.path, download_location) + logger.info( + 'Saved %s', display_path(download_location) + ) if self._download_should_save: # Make a .zip of the source_dir we already created. From 9669c0b3123584490ab4864c3e4a6daa0f29d072 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" <jaraco@jaraco.com> Date: Sun, 8 Mar 2020 18:35:53 -0400 Subject: [PATCH 1349/3170] Update src/pip/_internal/cli/progress_bars.py Co-Authored-By: Xavier Fernandez <xav.fernandez@gmail.com> --- src/pip/_internal/cli/progress_bars.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/cli/progress_bars.py b/src/pip/_internal/cli/progress_bars.py index b45d176df46..1cb6dc876b3 100644 --- a/src/pip/_internal/cli/progress_bars.py +++ b/src/pip/_internal/cli/progress_bars.py @@ -124,7 +124,7 @@ def update(self): class BlueEmojiBar(IncrementalBar): - suffix = "{percent}%" + suffix = "{percent:.0f}%" bar_prefix = " " bar_suffix = " " phases = (u"\U0001F539", u"\U0001F537", u"\U0001F535") # type: Any From 047e249767749eac3f1f5c0256c2dc076ca8c617 Mon Sep 17 00:00:00 2001 From: "Jason R. Coombs" <jaraco@jaraco.com> Date: Sun, 8 Mar 2020 18:36:01 -0400 Subject: [PATCH 1350/3170] Update src/pip/_internal/cli/progress_bars.py Co-Authored-By: Xavier Fernandez <xav.fernandez@gmail.com> --- src/pip/_internal/cli/progress_bars.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/cli/progress_bars.py b/src/pip/_internal/cli/progress_bars.py index 1cb6dc876b3..f9a68104e34 100644 --- a/src/pip/_internal/cli/progress_bars.py +++ b/src/pip/_internal/cli/progress_bars.py @@ -203,7 +203,7 @@ class BaseDownloadProgressBar(WindowsMixin, InterruptibleMixin, DownloadProgressMixin): file = sys.stdout - message = "{percent}%" + message = "{percent:.0f}%" suffix = "{downloaded} {download_speed} {pretty_eta}" # NOTE: The "type: ignore" comments on the following classes are there to From dfd6a163bdb0e6675a47e2f092144319e847202f Mon Sep 17 00:00:00 2001 From: Xavier Fernandez <xav.fernandez@gmail.com> Date: Mon, 9 Mar 2020 15:17:17 +0100 Subject: [PATCH 1351/3170] GitHub Actions: update cache key to include interpreter path (#7835) And make it more portable by using python --- .github/workflows/python-linters.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/python-linters.yml b/.github/workflows/python-linters.yml index 0b9bb0a2e44..d5124e226ea 100644 --- a/.github/workflows/python-linters.yml +++ b/.github/workflows/python-linters.yml @@ -56,7 +56,7 @@ jobs: ${{ runner.os }}-pip- ${{ runner.os }}- - name: set PY - run: echo "::set-env name=PY::$(python -VV | sha256sum | cut -d' ' -f1)" + run: echo "::set-env name=PY::$(python -c 'import hashlib, sys;print(hashlib.sha256(sys.version.encode()+sys.executable.encode()).hexdigest())')" - uses: actions/cache@v1 with: path: ~/.cache/pre-commit From 115a83698f4c42c0b22a867b392d1858ff929fcf Mon Sep 17 00:00:00 2001 From: Prashant Sharma <31796326+gutsytechster@users.noreply.github.com> Date: Tue, 10 Mar 2020 03:59:27 +0530 Subject: [PATCH 1352/3170] fix(tests/lib/path): Remove duplicate resolve method (#7837) --- tests/lib/path.py | 6 ------ 1 file changed, 6 deletions(-) diff --git a/tests/lib/path.py b/tests/lib/path.py index 35eefa6bb0a..a74e478ecc4 100644 --- a/tests/lib/path.py +++ b/tests/lib/path.py @@ -107,12 +107,6 @@ def suffix(self): """ return Path(os.path.splitext(self)[1]) - def resolve(self): - """ - './a/bc.d' -> '/home/a/bc.d' - """ - return Path(os.path.abspath(self)) - def resolve(self): """ Resolves symbolic links. From a096d4cd762b4ffc5ae3696d5179d078ab7372f4 Mon Sep 17 00:00:00 2001 From: Xavier Fernandez <xavier.fernandez@polyconseil.fr> Date: Tue, 10 Mar 2020 13:21:53 +0100 Subject: [PATCH 1353/3170] cli: revert format() related changes Since the progress bar API is % related, it seems simpler to stick with it. --- src/pip/_internal/cli/progress_bars.py | 33 ++++++++++++-------------- 1 file changed, 15 insertions(+), 18 deletions(-) diff --git a/src/pip/_internal/cli/progress_bars.py b/src/pip/_internal/cli/progress_bars.py index f9a68104e34..7ed224790cf 100644 --- a/src/pip/_internal/cli/progress_bars.py +++ b/src/pip/_internal/cli/progress_bars.py @@ -14,7 +14,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Any, Dict, Iterator, List, Tuple + from typing import Any, Dict, List try: from pip._vendor import colorama @@ -124,7 +124,7 @@ def update(self): class BlueEmojiBar(IncrementalBar): - suffix = "{percent:.0f}%" + suffix = "%(percent)d%%" bar_prefix = " " bar_suffix = " " phases = (u"\U0001F539", u"\U0001F537", u"\U0001F535") # type: Any @@ -203,8 +203,8 @@ class BaseDownloadProgressBar(WindowsMixin, InterruptibleMixin, DownloadProgressMixin): file = sys.stdout - message = "{percent:.0f}%" - suffix = "{downloaded} {download_speed} {pretty_eta}" + 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 @@ -238,7 +238,7 @@ class DownloadProgressSpinner(WindowsMixin, InterruptibleMixin, DownloadProgressMixin, Spinner): file = sys.stdout - suffix = "{downloaded} {download_speed}" + suffix = "%(downloaded)s %(download_speed)s" def next_phase(self): # type: ignore if not hasattr(self, "_phaser"): @@ -247,21 +247,18 @@ def next_phase(self): # type: ignore def update(self): # type: () -> None - vals = dict(self._load_vals( - 'downloaded', 'download_speed', 'pretty_eta', 'percent')) - message = self.message.format(**vals) + message = self.message % self phase = self.next_phase() - suffix = self.suffix.format(**vals) - line = " ".join(filter(None, (message, phase, suffix))) - self.writeln(line) + suffix = self.suffix % self + line = ''.join([ + message, + " " if message else "", + phase, + " " if suffix else "", + suffix, + ]) - def _load_vals(self, *names): - # type: (*str) -> Iterator[Tuple[str, Any]] - for name in names: - try: - yield name, getattr(self, name) - except Exception: - pass + self.writeln(line) BAR_TYPES = { From fc810d73537544ead26cd7fcc1d2b6345f9bbc5f Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 11 Mar 2020 17:16:11 +0800 Subject: [PATCH 1354/3170] Add --unstable-feature=resolver This introduces a new general option --unstable-feature that can be used to opt into "preview" features in pip not enabled by default. Currently the only available feature is "resolver". A stub resolver interface (which would fail on invocation) is provided to respond to the flag. The --unstable-feature option is hidden from --help since the resolver does not yet work. This suppression should be removed when we release the resolver for general/public testing. --- src/pip/_internal/cli/cmdoptions.py | 14 ++++++++ src/pip/_internal/cli/req_command.py | 24 +++++++++++-- src/pip/_internal/resolution/base.py | 20 +++++++++++ .../_internal/resolution/legacy/resolver.py | 9 +++-- .../resolution/resolvelib/__init__.py | 0 .../resolution/resolvelib/resolver.py | 36 +++++++++++++++++++ 6 files changed, 95 insertions(+), 8 deletions(-) create mode 100644 src/pip/_internal/resolution/base.py create mode 100644 src/pip/_internal/resolution/resolvelib/__init__.py create mode 100644 src/pip/_internal/resolution/resolvelib/resolver.py diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index c74d2b632a6..79958550ec4 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -915,6 +915,19 @@ def check_list_path_option(options): ) # type: Callable[..., Option] +unstable_feature = partial( + Option, + '--unstable-feature', + dest='unstable_features', + metavar='feature', + action='append', + default=[], + choices=['resolver'], + help=SUPPRESS_HELP, # TODO: Enable this when the resolver actually works. + # help='Enable unstable feature(s) that may be backward incompatible.', +) # type: Callable[..., Option] + + ########## # groups # ########## @@ -943,6 +956,7 @@ def check_list_path_option(options): disable_pip_version_check, no_color, no_python_version_warning, + unstable_feature, ] } # type: Dict[str, Any] diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 45ddb8b47f3..9a98335b4fc 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -26,7 +26,6 @@ ) from pip._internal.req.req_file import parse_requirements from pip._internal.req.req_set import RequirementSet -from pip._internal.resolution.legacy.resolver import Resolver from pip._internal.self_outdated_check import ( make_link_collector, pip_self_version_check, @@ -42,6 +41,7 @@ from pip._internal.models.target_python import TargetPython from pip._internal.req.req_install import InstallRequirement from pip._internal.req.req_tracker import RequirementTracker + from pip._internal.resolution.base import BaseResolver from pip._internal.utils.temp_dir import ( TempDirectory, TempDirectoryTypeRegistry, @@ -248,7 +248,7 @@ def make_resolver( use_pep517=None, # type: Optional[bool] py_version_info=None # type: Optional[Tuple[int, ...]] ): - # type: (...) -> Resolver + # type: (...) -> BaseResolver """ Create a Resolver instance for the given parameters. """ @@ -258,7 +258,25 @@ def make_resolver( wheel_cache=wheel_cache, use_pep517=use_pep517, ) - return Resolver( + # The long import name and duplicated invocation is needed to convince + # Mypy into correctly typechecking. Otherwise it would complain the + # "Resolver" class being redefined. + if 'resolver' in options.unstable_features: + import pip._internal.resolution.resolvelib.resolver + return pip._internal.resolution.resolvelib.resolver.Resolver( + preparer=preparer, + finder=finder, + make_install_req=make_install_req, + use_user_site=use_user_site, + ignore_dependencies=options.ignore_dependencies, + ignore_installed=ignore_installed, + ignore_requires_python=ignore_requires_python, + force_reinstall=force_reinstall, + upgrade_strategy=upgrade_strategy, + py_version_info=py_version_info, + ) + import pip._internal.resolution.legacy.resolver + return pip._internal.resolution.legacy.resolver.Resolver( preparer=preparer, finder=finder, make_install_req=make_install_req, diff --git a/src/pip/_internal/resolution/base.py b/src/pip/_internal/resolution/base.py new file mode 100644 index 00000000000..2fa118bd894 --- /dev/null +++ b/src/pip/_internal/resolution/base.py @@ -0,0 +1,20 @@ +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import Callable, List + from pip._internal.req.req_install import InstallRequirement + from pip._internal.req.req_set import RequirementSet + + InstallRequirementProvider = Callable[ + [str, InstallRequirement], InstallRequirement + ] + + +class BaseResolver(object): + def resolve(self, root_reqs, check_supported_wheels): + # type: (List[InstallRequirement], bool) -> RequirementSet + raise NotImplementedError() + + def get_installation_order(self, req_set): + # type: (RequirementSet) -> List[InstallRequirement] + raise NotImplementedError() diff --git a/src/pip/_internal/resolution/legacy/resolver.py b/src/pip/_internal/resolution/legacy/resolver.py index 2f32631dc60..d6800352614 100644 --- a/src/pip/_internal/resolution/legacy/resolver.py +++ b/src/pip/_internal/resolution/legacy/resolver.py @@ -29,6 +29,7 @@ UnsupportedPythonVersion, ) from pip._internal.req.req_set import RequirementSet +from pip._internal.resolution.base import BaseResolver from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import dist_in_usersite, normalize_version_info from pip._internal.utils.packaging import ( @@ -38,17 +39,15 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Callable, DefaultDict, List, Optional, Set, Tuple + from typing import DefaultDict, List, Optional, Set, Tuple from pip._vendor import pkg_resources from pip._internal.distributions import AbstractDistribution from pip._internal.index.package_finder import PackageFinder from pip._internal.operations.prepare import RequirementPreparer from pip._internal.req.req_install import InstallRequirement + from pip._internal.resolution.base import InstallRequirementProvider - InstallRequirementProvider = Callable[ - [str, InstallRequirement], InstallRequirement - ] DiscoveredDependencies = DefaultDict[str, List[InstallRequirement]] logger = logging.getLogger(__name__) @@ -102,7 +101,7 @@ def _check_dist_requires_python( )) -class Resolver(object): +class Resolver(BaseResolver): """Resolves which packages need to be installed/uninstalled to perform \ the requested operation without breaking the requirements of any package. """ diff --git a/src/pip/_internal/resolution/resolvelib/__init__.py b/src/pip/_internal/resolution/resolvelib/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py new file mode 100644 index 00000000000..2d9b14751ce --- /dev/null +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -0,0 +1,36 @@ +from pip._internal.resolution.base import BaseResolver +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import List, Optional, Tuple + + from pip._internal.index.package_finder import PackageFinder + from pip._internal.operations.prepare import RequirementPreparer + from pip._internal.req.req_install import InstallRequirement + from pip._internal.req.req_set import RequirementSet + from pip._internal.resolution.base import InstallRequirementProvider + + +class Resolver(BaseResolver): + def __init__( + self, + preparer, # type: RequirementPreparer + finder, # type: PackageFinder + make_install_req, # type: InstallRequirementProvider + use_user_site, # type: bool + ignore_dependencies, # type: bool + ignore_installed, # type: bool + ignore_requires_python, # type: bool + force_reinstall, # type: bool + upgrade_strategy, # type: str + py_version_info=None, # type: Optional[Tuple[int, ...]] + ): + super(Resolver, self).__init__() + + def resolve(self, root_reqs, check_supported_wheels): + # type: (List[InstallRequirement], bool) -> RequirementSet + raise NotImplementedError() + + def get_installation_order(self, req_set): + # type: (RequirementSet) -> List[InstallRequirement] + raise NotImplementedError() From 9b10b9350301cb9ff0c62c745f3ca8197f393753 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Mon, 9 Mar 2020 09:06:32 +0000 Subject: [PATCH 1355/3170] Implement the resolvelib Requirement class --- ...a56cb3-00b1-4ccb-805a-ac4807c72a52.trivial | 0 .../_internal/resolution/resolvelib/base.py | 51 +++++++ .../resolution/resolvelib/requirements.py | 141 ++++++++++++++++++ tests/unit/resolution_resolvelib/conftest.py | 43 ++++++ .../resolution_resolvelib/test_requirement.py | 75 ++++++++++ 5 files changed, 310 insertions(+) create mode 100644 news/c9a56cb3-00b1-4ccb-805a-ac4807c72a52.trivial create mode 100644 src/pip/_internal/resolution/resolvelib/base.py create mode 100644 src/pip/_internal/resolution/resolvelib/requirements.py create mode 100644 tests/unit/resolution_resolvelib/conftest.py create mode 100644 tests/unit/resolution_resolvelib/test_requirement.py diff --git a/news/c9a56cb3-00b1-4ccb-805a-ac4807c72a52.trivial b/news/c9a56cb3-00b1-4ccb-805a-ac4807c72a52.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/resolution/resolvelib/base.py b/src/pip/_internal/resolution/resolvelib/base.py new file mode 100644 index 00000000000..76c3ec444ff --- /dev/null +++ b/src/pip/_internal/resolution/resolvelib/base.py @@ -0,0 +1,51 @@ +from pip._vendor.packaging.utils import canonicalize_name + +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import (Sequence, Set) + + from pip._vendor.packaging.version import _BaseVersion + from pip._internal.index.package_finder import PackageFinder + + +def format_name(project, extras): + # type: (str, Set[str]) -> str + if not extras: + return project + canonical_extras = sorted(canonicalize_name(e) for e in extras) + return "{}[{}]".format(project, ",".join(canonical_extras)) + + +class Requirement(object): + @property + def name(self): + # type: () -> str + raise NotImplementedError("Subclass should override") + + def find_matches( + self, + finder, # type: PackageFinder + ): + # type: (...) -> Sequence[Candidate] + raise NotImplementedError("Subclass should override") + + def is_satisfied_by(self, candidate): + # type: (Candidate) -> bool + return False + + +class Candidate(object): + @property + def name(self): + # type: () -> str + raise NotImplementedError("Override in subclass") + + @property + def version(self): + # type: () -> _BaseVersion + raise NotImplementedError("Override in subclass") + + def get_dependencies(self): + # type: () -> Sequence[Requirement] + raise NotImplementedError("Override in subclass") diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py new file mode 100644 index 00000000000..76fa16a96e4 --- /dev/null +++ b/src/pip/_internal/resolution/resolvelib/requirements.py @@ -0,0 +1,141 @@ +from pip._vendor.packaging.utils import canonicalize_name + +from pip._internal.req.req_install import InstallRequirement +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +from .base import Candidate, Requirement, format_name + +if MYPY_CHECK_RUNNING: + from typing import (Optional, Sequence) + + from pip._vendor.packaging.version import _BaseVersion + + from pip._internal.index.package_finder import PackageFinder + + +def make_requirement(install_req): + # type: (InstallRequirement) -> Requirement + if install_req.link: + if install_req.req and install_req.req.name: + return NamedRequirement(install_req) + else: + return UnnamedRequirement(install_req) + else: + return VersionedRequirement(install_req) + + +class UnnamedRequirement(Requirement): + def __init__(self, req): + # type: (InstallRequirement) -> None + self._ireq = req + self._candidate = None # type: Optional[Candidate] + + @property + def name(self): + # type: () -> str + assert self._ireq.req is None or self._ireq.name is None, \ + "Unnamed requirement has a name" + # TODO: Get the candidate and use its name... + return "" + + def _get_candidate(self): + # type: () -> Candidate + if self._candidate is None: + self._candidate = Candidate() + return self._candidate + + def find_matches( + self, + finder, # type: PackageFinder + ): + # type: (...) -> Sequence[Candidate] + return [self._get_candidate()] + + def is_satisfied_by(self, candidate): + # type: (Candidate) -> bool + return candidate is self._get_candidate() + + +class NamedRequirement(Requirement): + def __init__(self, req): + # type: (InstallRequirement) -> None + self._ireq = req + self._candidate = None # type: Optional[Candidate] + + @property + def name(self): + # type: () -> str + assert self._ireq.req.name is not None, "Named requirement has no name" + canonical_name = canonicalize_name(self._ireq.req.name) + return format_name(canonical_name, self._ireq.req.extras) + + def _get_candidate(self): + # type: () -> Candidate + if self._candidate is None: + self._candidate = Candidate() + return self._candidate + + def find_matches( + self, + finder, # type: PackageFinder + ): + # type: (...) -> Sequence[Candidate] + return [self._get_candidate()] + + def is_satisfied_by(self, candidate): + # type: (Candidate) -> bool + return candidate is self._get_candidate() + + +# TODO: This is temporary, to make the tests pass +class DummyCandidate(Candidate): + def __init__(self, name, version): + # type: (str, _BaseVersion) -> None + self._name = name + self._version = version + + @property + def name(self): + # type: () -> str + return self._name + + @property + def version(self): + # type: () -> _BaseVersion + return self._version + + +class VersionedRequirement(Requirement): + def __init__(self, ireq): + # type: (InstallRequirement) -> None + assert ireq.req is not None, "Un-specified requirement not allowed" + assert ireq.req.url is None, "Direct reference not allowed" + self._ireq = ireq + + @property + def name(self): + # type: () -> str + canonical_name = canonicalize_name(self._ireq.req.name) + return format_name(canonical_name, self._ireq.req.extras) + + def find_matches( + self, + finder, # type: PackageFinder + ): + # type: (...) -> Sequence[Candidate] + found = finder.find_best_candidate( + project_name=self._ireq.req.name, + specifier=self._ireq.req.specifier, + hashes=self._ireq.hashes(trust_internet=False), + ) + return [ + DummyCandidate(ican.name, ican.version) + for ican in found.iter_applicable() + ] + + def is_satisfied_by(self, candidate): + # type: (Candidate) -> bool + # TODO: Should check name matches as well. Defer this + # until we have the proper Candidate object, and + # no longer have to deal with unnmed requirements... + return candidate.version in self._ireq.req.specifier diff --git a/tests/unit/resolution_resolvelib/conftest.py b/tests/unit/resolution_resolvelib/conftest.py new file mode 100644 index 00000000000..f885d1c855b --- /dev/null +++ b/tests/unit/resolution_resolvelib/conftest.py @@ -0,0 +1,43 @@ +import pytest + +from pip._internal.cli.req_command import RequirementCommand +from pip._internal.commands.install import InstallCommand +from pip._internal.index.collector import LinkCollector +from pip._internal.index.package_finder import PackageFinder +# from pip._internal.models.index import PyPI +from pip._internal.models.search_scope import SearchScope +from pip._internal.models.selection_prefs import SelectionPreferences +from pip._internal.network.session import PipSession +from pip._internal.req.req_tracker import get_requirement_tracker +from pip._internal.utils.temp_dir import TempDirectory, global_tempdir_manager + + +@pytest.fixture +def finder(data): + session = PipSession() + scope = SearchScope([str(data.packages)], []) + collector = LinkCollector(session, scope) + prefs = SelectionPreferences(allow_yanked=False) + finder = PackageFinder.create(collector, prefs) + yield finder + + +@pytest.fixture +def preparer(finder): + session = PipSession() + rc = InstallCommand("x", "y") + o = rc.parse_args([]) + + with global_tempdir_manager(): + with TempDirectory() as tmp: + with get_requirement_tracker() as tracker: + preparer = RequirementCommand.make_requirement_preparer( + tmp, + options=o[0], + req_tracker=tracker, + session=session, + finder=finder, + use_user_site=False + ) + + yield preparer diff --git a/tests/unit/resolution_resolvelib/test_requirement.py b/tests/unit/resolution_resolvelib/test_requirement.py new file mode 100644 index 00000000000..28fdaaa20e9 --- /dev/null +++ b/tests/unit/resolution_resolvelib/test_requirement.py @@ -0,0 +1,75 @@ +import pytest + +from pip._internal.req.constructors import install_req_from_line +from pip._internal.resolution.resolvelib.base import Candidate +from pip._internal.resolution.resolvelib.requirements import make_requirement +from pip._internal.utils.urls import path_to_url + +# NOTE: All tests are prefixed `test_rlr` (for "test resolvelib resolver"). +# This helps select just these tests using pytest's `-k` option, and +# keeps test names shorter. + +# Basic tests: +# Create a requirement from a project name - "pip" +# Create a requirement from a name + version constraint - "pip >= 20.0" +# Create a requirement from a wheel filename +# Create a requirement from a sdist filename +# Create a requirement from a local directory (which has no obvious name!) +# Editables +# + + +@pytest.fixture +def test_cases(data): + def data_file(name): + return data.packages.joinpath(name) + + def data_url(name): + return path_to_url(data_file(name)) + + test_cases = [ + # requirement, name, matches + # Version specifiers + ("simple", "simple", 3), + ("simple>1.0", "simple", 2), + ("simple[extra]==1.0", "simple[extra]", 1), + # Wheels + (data_file("simplewheel-1.0-py2.py3-none-any.whl"), "simplewheel", 1), + (data_url("simplewheel-1.0-py2.py3-none-any.whl"), "simplewheel", 1), + # Direct URLs + ("foo @ " + data_url("simple-1.0.tar.gz"), "foo", 1), + # SDists + # TODO: sdists should have a name + (data_file("simple-1.0.tar.gz"), "", 1), + (data_url("simple-1.0.tar.gz"), "", 1), + # TODO: directory, editables + ] + + yield test_cases + + +def req_from_line(line): + return make_requirement(install_req_from_line(line)) + + +def test_rlr_requirement_has_name(test_cases): + """All requirements should have a name""" + for requirement, name, matches in test_cases: + req = req_from_line(requirement) + assert req.name == name + + +def test_rlr_correct_number_of_matches(test_cases, finder): + """Requirements should return the correct number of candidates""" + for requirement, name, matches in test_cases: + req = req_from_line(requirement) + assert len(req.find_matches(finder)) == matches + + +def test_rlr_candidates_match_requirement(test_cases, finder): + """Candidates returned from find_matches should satisfy the requirement""" + for requirement, name, matches in test_cases: + req = req_from_line(requirement) + for c in req.find_matches(finder): + assert isinstance(c, Candidate) + assert req.is_satisfied_by(c) From 36065cf2e0edd9ae1b035be1dcae7e77e0984478 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 12 Mar 2020 22:27:28 +0800 Subject: [PATCH 1356/3170] Vendor ResolveLib from Git We are vendoring from the Git source for now, so the bug fix turnover can be quicker if there's anything wrong in the resolution logic. HEAD up-to-date as of 2020-03-12. --- news/7850.vendor | 1 + pyproject.toml | 1 + src/pip/_vendor/__init__.py | 1 + src/pip/_vendor/resolvelib.pyi | 1 + src/pip/_vendor/resolvelib/LICENSE | 13 + src/pip/_vendor/resolvelib/__init__.py | 24 ++ src/pip/_vendor/resolvelib/providers.py | 121 ++++++++ src/pip/_vendor/resolvelib/reporters.py | 24 ++ src/pip/_vendor/resolvelib/resolvers.py | 376 ++++++++++++++++++++++++ src/pip/_vendor/resolvelib/structs.py | 68 +++++ src/pip/_vendor/vendor.txt | 2 + 11 files changed, 632 insertions(+) create mode 100644 news/7850.vendor create mode 100644 src/pip/_vendor/resolvelib.pyi create mode 100644 src/pip/_vendor/resolvelib/LICENSE create mode 100644 src/pip/_vendor/resolvelib/__init__.py create mode 100644 src/pip/_vendor/resolvelib/providers.py create mode 100644 src/pip/_vendor/resolvelib/reporters.py create mode 100644 src/pip/_vendor/resolvelib/resolvers.py create mode 100644 src/pip/_vendor/resolvelib/structs.py diff --git a/news/7850.vendor b/news/7850.vendor new file mode 100644 index 00000000000..b9b5f22e002 --- /dev/null +++ b/news/7850.vendor @@ -0,0 +1 @@ +Add ResolveLib as a vendored dependency. diff --git a/pyproject.toml b/pyproject.toml index 01fae701523..b45b527079c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -56,4 +56,5 @@ msgpack-python = "msgpack" [tool.vendoring.license.fallback-urls] pytoml = "https://github.com/avakar/pytoml/raw/master/LICENSE" +resolvelib = "https://github.com/sarugaku/resolvelib/raw/master/LICENSE" webencodings = "https://github.com/SimonSapin/python-webencodings/raw/master/LICENSE" diff --git a/src/pip/_vendor/__init__.py b/src/pip/_vendor/__init__.py index 1112e1012b0..c5b5481122c 100644 --- a/src/pip/_vendor/__init__.py +++ b/src/pip/_vendor/__init__.py @@ -107,4 +107,5 @@ def vendored(modulename): vendored("requests.packages.urllib3.util.ssl_") vendored("requests.packages.urllib3.util.timeout") vendored("requests.packages.urllib3.util.url") + vendored("resolvelib") vendored("urllib3") diff --git a/src/pip/_vendor/resolvelib.pyi b/src/pip/_vendor/resolvelib.pyi new file mode 100644 index 00000000000..b4ef4e108c4 --- /dev/null +++ b/src/pip/_vendor/resolvelib.pyi @@ -0,0 +1 @@ +from resolvelib import * \ No newline at end of file diff --git a/src/pip/_vendor/resolvelib/LICENSE b/src/pip/_vendor/resolvelib/LICENSE new file mode 100644 index 00000000000..b9077766e9b --- /dev/null +++ b/src/pip/_vendor/resolvelib/LICENSE @@ -0,0 +1,13 @@ +Copyright (c) 2018, Tzu-ping Chung <uranusjr@gmail.com> + +Permission to use, copy, modify, and distribute this software for any +purpose with or without fee is hereby granted, provided that the above +copyright notice and this permission notice appear in all copies. + +THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. diff --git a/src/pip/_vendor/resolvelib/__init__.py b/src/pip/_vendor/resolvelib/__init__.py new file mode 100644 index 00000000000..b663b0c2902 --- /dev/null +++ b/src/pip/_vendor/resolvelib/__init__.py @@ -0,0 +1,24 @@ +__all__ = [ + "__version__", + "AbstractProvider", + "AbstractResolver", + "BaseReporter", + "Resolver", + "RequirementsConflicted", + "ResolutionError", + "ResolutionImpossible", + "ResolutionTooDeep", +] + +__version__ = "0.2.3.dev0" + + +from .providers import AbstractProvider, AbstractResolver +from .reporters import BaseReporter +from .resolvers import ( + RequirementsConflicted, + Resolver, + ResolutionError, + ResolutionImpossible, + ResolutionTooDeep, +) diff --git a/src/pip/_vendor/resolvelib/providers.py b/src/pip/_vendor/resolvelib/providers.py new file mode 100644 index 00000000000..db1682195e0 --- /dev/null +++ b/src/pip/_vendor/resolvelib/providers.py @@ -0,0 +1,121 @@ +class AbstractProvider(object): + """Delegate class to provide requirement interface for the resolver. + """ + + def identify(self, dependency): + """Given a dependency, return an identifier for it. + + This is used in many places to identify the dependency, e.g. whether + two requirements should have their specifier parts merged, whether + two specifications would conflict with each other (because they the + same name but different versions). + """ + raise NotImplementedError + + def get_preference(self, resolution, candidates, information): + """Produce a sort key for given specification based on preference. + + The preference is defined as "I think this requirement should be + resolved first". The lower the return value is, the more preferred + this group of arguments is. + + :param resolution: Currently pinned candidate, or `None`. + :param candidates: A list of possible candidates. + :param information: A list of requirement information. + + Each information instance is a named tuple with two entries: + + * `requirement` specifies a requirement contributing to the current + candidate list + * `parent` specifies the candidate that provids (dependend on) the + requirement, or `None` to indicate a root requirement. + + The preference could depend on a various of issues, including (not + necessarily in this order): + + * Is this package pinned in the current resolution result? + * How relaxed is the requirement? Stricter ones should probably be + worked on first? (I don't know, actually.) + * How many possibilities are there to satisfy this requirement? Those + with few left should likely be worked on first, I guess? + * Are there any known conflicts for this requirement? We should + probably work on those with the most known conflicts. + + A sortable value should be returned (this will be used as the `key` + parameter of the built-in sorting function). The smaller the value is, + the more preferred this specification is (i.e. the sorting function + is called with `reverse=False`). + """ + raise NotImplementedError + + def find_matches(self, requirement): + """Find all possible candidates that satisfy a requirement. + + This should try to get candidates based on the requirement's type. + For VCS, local, and archive requirements, the one-and-only match is + returned, and for a "named" requirement, the index(es) should be + consulted to find concrete candidates for this requirement. + + The returned candidates should be sorted by reversed preference, e.g. + the most preferred should be LAST. This is done so list-popping can be + as efficient as possible. + """ + raise NotImplementedError + + def is_satisfied_by(self, requirement, candidate): + """Whether the given requirement can be satisfied by a candidate. + + A boolean should be returned to indicate whether `candidate` is a + viable solution to the requirement. + """ + raise NotImplementedError + + def get_dependencies(self, candidate): + """Get dependencies of a candidate. + + This should return a collection of requirements that `candidate` + specifies as its dependencies. + """ + raise NotImplementedError + + +class AbstractResolver(object): + """The thing that performs the actual resolution work. + """ + + base_exception = Exception + + def __init__(self, provider, reporter): + self.provider = provider + self.reporter = reporter + + def resolve(self, requirements, **kwargs): + """Take a collection of constraints, spit out the resolution result. + + Parameters + ---------- + requirements : Collection + A collection of constraints + kwargs : optional + Additional keyword arguments that subclasses may accept. + + Raises + ------ + self.base_exception + Any raised exception is guaranteed to be a subclass of + self.base_exception. The string representation of an exception + should be human readable and provide context for why it occurred. + + Returns + ------- + retval : object + A representation of the final resolution state. It can be any object + with a `mapping` attribute that is a Mapping. Other attributes can + be used to provide resolver-specific information. + + The `mapping` attribute MUST be key-value pair is an identifier of a + requirement (as returned by the provider's `identify` method) mapped + to the resolved candidate (chosen from the return value of the + provider's `find_matches` method). + """ + raise NotImplementedError diff --git a/src/pip/_vendor/resolvelib/reporters.py b/src/pip/_vendor/resolvelib/reporters.py new file mode 100644 index 00000000000..5bcaf4d8268 --- /dev/null +++ b/src/pip/_vendor/resolvelib/reporters.py @@ -0,0 +1,24 @@ +class BaseReporter(object): + """Delegate class to provider progress reporting for the resolver. + """ + + def starting(self): + """Called before the resolution actually starts. + """ + + def starting_round(self, index): + """Called before each round of resolution starts. + + The index is zero-based. + """ + + def ending_round(self, index, state): + """Called before each round of resolution ends. + + This is NOT called if the resolution ends at this round. Use `ending` + if you want to report finalization. The index is zero-based. + """ + + def ending(self, state): + """Called before the resolution ends successfully. + """ diff --git a/src/pip/_vendor/resolvelib/resolvers.py b/src/pip/_vendor/resolvelib/resolvers.py new file mode 100644 index 00000000000..45e7e7d3130 --- /dev/null +++ b/src/pip/_vendor/resolvelib/resolvers.py @@ -0,0 +1,376 @@ +import collections + +from .providers import AbstractResolver +from .structs import DirectedGraph + + +RequirementInformation = collections.namedtuple( + "RequirementInformation", ["requirement", "parent"] +) + + +class ResolverException(Exception): + """A base class for all exceptions raised by this module. + + Exceptions derived by this class should all be handled in this module. Any + bubbling pass the resolver should be treated as a bug. + """ + + +class RequirementsConflicted(ResolverException): + def __init__(self, criterion): + super(RequirementsConflicted, self).__init__(criterion) + self.criterion = criterion + + +class Criterion(object): + """Representation of possible resolution results of a package. + + This holds three attributes: + + * `information` is a collection of `RequirementInformation` pairs. + Each pair is a requirement contributing to this criterion, and the + candidate that provides the requirement. + * `incompatibilities` is a collection of all known not-to-work candidates + to exclude from consideration. + * `candidates` is a collection containing all possible candidates deducted + from the union of contributing requirements and known incompatibilities. + It should never be empty, except when the criterion is an attribute of a + raised `RequirementsConflicted` (in which case it is always empty). + + .. note:: + This class is intended to be externally immutable. **Do not** mutate + any of its attribute containers. + """ + + def __init__(self, candidates, information, incompatibilities): + self.candidates = candidates + self.information = information + self.incompatibilities = incompatibilities + + @classmethod + def from_requirement(cls, provider, requirement, parent): + """Build an instance from a requirement. + """ + candidates = provider.find_matches(requirement) + criterion = cls( + candidates=candidates, + information=[RequirementInformation(requirement, parent)], + incompatibilities=[], + ) + if not candidates: + raise RequirementsConflicted(criterion) + return criterion + + def iter_requirement(self): + return (i.requirement for i in self.information) + + def iter_parent(self): + return (i.parent for i in self.information) + + def merged_with(self, provider, requirement, parent): + """Build a new instance from this and a new requirement. + """ + infos = list(self.information) + infos.append(RequirementInformation(requirement, parent)) + candidates = [ + c + for c in self.candidates + if provider.is_satisfied_by(requirement, c) + ] + criterion = type(self)(candidates, infos, list(self.incompatibilities)) + if not candidates: + raise RequirementsConflicted(criterion) + return criterion + + def excluded_of(self, candidate): + """Build a new instance from this, but excluding specified candidate. + """ + incompats = list(self.incompatibilities) + incompats.append(candidate) + candidates = [c for c in self.candidates if c != candidate] + criterion = type(self)(candidates, list(self.information), incompats) + if not candidates: + raise RequirementsConflicted(criterion) + return criterion + + +class ResolutionError(ResolverException): + pass + + +class ResolutionImpossible(ResolutionError): + def __init__(self, requirements): + super(ResolutionImpossible, self).__init__(requirements) + self.requirements = requirements + + +class ResolutionTooDeep(ResolutionError): + def __init__(self, round_count): + super(ResolutionTooDeep, self).__init__(round_count) + self.round_count = round_count + + +# Resolution state in a round. +State = collections.namedtuple("State", "mapping criteria") + + +class Resolution(object): + """Stateful resolution object. + + This is designed as a one-off object that holds information to kick start + the resolution process, and holds the results afterwards. + """ + + def __init__(self, provider, reporter): + self._p = provider + self._r = reporter + self._states = [] + + @property + def state(self): + try: + return self._states[-1] + except IndexError: + raise AttributeError("state") + + def _push_new_state(self): + """Push a new state into history. + + This new state will be used to hold resolution results of the next + coming round. + """ + try: + base = self._states[-1] + except IndexError: + state = State(mapping=collections.OrderedDict(), criteria={}) + else: + state = State( + mapping=base.mapping.copy(), criteria=base.criteria.copy(), + ) + self._states.append(state) + + def _merge_into_criterion(self, requirement, parent): + name = self._p.identify(requirement) + try: + crit = self.state.criteria[name] + except KeyError: + crit = Criterion.from_requirement(self._p, requirement, parent) + else: + crit = crit.merged_with(self._p, requirement, parent) + return name, crit + + def _get_criterion_item_preference(self, item): + name, criterion = item + try: + pinned = self.state.mapping[name] + except KeyError: + pinned = None + return self._p.get_preference( + pinned, criterion.candidates, criterion.information, + ) + + def _is_current_pin_satisfying(self, name, criterion): + try: + current_pin = self.state.mapping[name] + except KeyError: + return False + return all( + self._p.is_satisfied_by(r, current_pin) + for r in criterion.iter_requirement() + ) + + def _get_criteria_to_update(self, candidate): + criteria = {} + for r in self._p.get_dependencies(candidate): + name, crit = self._merge_into_criterion(r, parent=candidate) + criteria[name] = crit + return criteria + + def _attempt_to_pin_criterion(self, name, criterion): + causes = [] + for candidate in reversed(criterion.candidates): + try: + criteria = self._get_criteria_to_update(candidate) + except RequirementsConflicted as e: + causes.append(e.criterion) + continue + # Put newly-pinned candidate at the end. This is essential because + # backtracking looks at this mapping to get the last pin. + self.state.mapping.pop(name, None) + self.state.mapping[name] = candidate + self.state.criteria.update(criteria) + return [] + + # All candidates tried, nothing works. This criterion is a dead + # end, signal for backtracking. + return causes + + def _backtrack(self): + # We need at least 3 states here: + # (a) One known not working, to drop. + # (b) One to backtrack to. + # (c) One to restore state (b) to its state prior to candidate-pinning, + # so we can pin another one instead. + while len(self._states) >= 3: + del self._states[-1] + + # Retract the last candidate pin, and create a new (b). + name, candidate = self._states.pop().mapping.popitem() + self._push_new_state() + + try: + # Mark the retracted candidate as incompatible. + criterion = self.state.criteria[name].excluded_of(candidate) + except RequirementsConflicted: + # This state still does not work. Try the still previous state. + continue + self.state.criteria[name] = criterion + + return True + + return False + + def resolve(self, requirements, max_rounds): + if self._states: + raise RuntimeError("already resolved") + + self._push_new_state() + for r in requirements: + try: + name, crit = self._merge_into_criterion(r, parent=None) + except RequirementsConflicted as e: + # If initial requirements conflict, nothing would ever work. + raise ResolutionImpossible(e.requirements + [r]) + self.state.criteria[name] = crit + + self._r.starting() + + for round_index in range(max_rounds): + self._r.starting_round(round_index) + + self._push_new_state() + curr = self.state + + unsatisfied_criterion_items = [ + item + for item in self.state.criteria.items() + if not self._is_current_pin_satisfying(*item) + ] + + # All criteria are accounted for. Nothing more to pin, we are done! + if not unsatisfied_criterion_items: + del self._states[-1] + self._r.ending(curr) + return self.state + + # Choose the most preferred unpinned criterion to try. + name, criterion = min( + unsatisfied_criterion_items, + key=self._get_criterion_item_preference, + ) + failure_causes = self._attempt_to_pin_criterion(name, criterion) + + # Backtrack if pinning fails. + if failure_causes: + result = self._backtrack() + if not result: + requirements = [ + requirement + for crit in failure_causes + for requirement in crit.iter_requirement() + ] + raise ResolutionImpossible(requirements) + + self._r.ending_round(round_index, curr) + + raise ResolutionTooDeep(max_rounds) + + +def _has_route_to_root(criteria, key, all_keys, connected): + if key in connected: + return True + if key not in criteria: + return False + for p in criteria[key].iter_parent(): + try: + pkey = all_keys[id(p)] + except KeyError: + continue + if pkey in connected: + connected.add(key) + return True + if _has_route_to_root(criteria, pkey, all_keys, connected): + connected.add(key) + return True + return False + + +Result = collections.namedtuple("Result", "mapping graph criteria") + + +def _build_result(state): + mapping = state.mapping + all_keys = {id(v): k for k, v in mapping.items()} + all_keys[id(None)] = None + + graph = DirectedGraph() + graph.add(None) # Sentinel as root dependencies' parent. + + connected = {None} + for key, criterion in state.criteria.items(): + if not _has_route_to_root(state.criteria, key, all_keys, connected): + continue + if key not in graph: + graph.add(key) + for p in criterion.iter_parent(): + try: + pkey = all_keys[id(p)] + except KeyError: + continue + if pkey not in graph: + graph.add(pkey) + graph.connect(pkey, key) + + return Result( + mapping={k: v for k, v in mapping.items() if k in connected}, + graph=graph, + criteria=state.criteria, + ) + + +class Resolver(AbstractResolver): + """The thing that performs the actual resolution work. + """ + + base_exception = ResolverException + + def resolve(self, requirements, max_rounds=100): + """Take a collection of constraints, spit out the resolution result. + + The return value is a representation to the final resolution result. It + is a tuple subclass with three public members: + + * `mapping`: A dict of resolved candidates. Each key is an identifier + of a requirement (as returned by the provider's `identify` method), + and the value is the resolved candidate. + * `graph`: A `DirectedGraph` instance representing the dependency tree. + The vertices are keys of `mapping`, and each edge represents *why* + a particular package is included. A special vertex `None` is + included to represent parents of user-supplied requirements. + * `criteria`: A dict of "criteria" that hold detailed information on + how edges in the graph are derived. Each key is an identifier of a + requirement, and the value is a `Criterion` instance. + + The following exceptions may be raised if a resolution cannot be found: + + * `ResolutionImpossible`: A resolution cannot be found for the given + combination of requirements. + * `ResolutionTooDeep`: The dependency tree is too deeply nested and + the resolver gave up. This is usually caused by a circular + dependency, but you can try to resolve this by increasing the + `max_rounds` argument. + """ + resolution = Resolution(self.provider, self.reporter) + state = resolution.resolve(requirements, max_rounds=max_rounds) + return _build_result(state) diff --git a/src/pip/_vendor/resolvelib/structs.py b/src/pip/_vendor/resolvelib/structs.py new file mode 100644 index 00000000000..1eee08b383a --- /dev/null +++ b/src/pip/_vendor/resolvelib/structs.py @@ -0,0 +1,68 @@ +class DirectedGraph(object): + """A graph structure with directed edges. + """ + + def __init__(self): + self._vertices = set() + self._forwards = {} # <key> -> Set[<key>] + self._backwards = {} # <key> -> Set[<key>] + + def __iter__(self): + return iter(self._vertices) + + def __len__(self): + return len(self._vertices) + + def __contains__(self, key): + return key in self._vertices + + def copy(self): + """Return a shallow copy of this graph. + """ + other = DirectedGraph() + other._vertices = set(self._vertices) + other._forwards = {k: set(v) for k, v in self._forwards.items()} + other._backwards = {k: set(v) for k, v in self._backwards.items()} + return other + + def add(self, key): + """Add a new vertex to the graph. + """ + if key in self._vertices: + raise ValueError("vertex exists") + self._vertices.add(key) + self._forwards[key] = set() + self._backwards[key] = set() + + def remove(self, key): + """Remove a vertex from the graph, disconnecting all edges from/to it. + """ + self._vertices.remove(key) + for f in self._forwards.pop(key): + self._backwards[f].remove(key) + for t in self._backwards.pop(key): + self._forwards[t].remove(key) + + def connected(self, f, t): + return f in self._backwards[t] and t in self._forwards[f] + + def connect(self, f, t): + """Connect two existing vertices. + + Nothing happens if the vertices are already connected. + """ + if t not in self._vertices: + raise KeyError(t) + self._forwards[f].add(t) + self._backwards[t].add(f) + + def iter_edges(self): + for f, children in self._forwards.items(): + for t in children: + yield f, t + + def iter_children(self, key): + return iter(self._forwards[key]) + + def iter_parents(self, key): + return iter(self._backwards[key]) diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index cbc2830ac09..401a4269213 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -21,3 +21,5 @@ retrying==1.3.3 setuptools==44.0.0 six==1.14.0 webencodings==0.5.1 + +git+https://github.com/sarugaku/resolvelib.git@fbc8bb28d6cff98b2#egg=resolvelib From a8058fe9e9f1aaeb60545ca1e752a767bac76002 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sat, 14 Mar 2020 00:29:04 +0530 Subject: [PATCH 1357/3170] Speed up `nox -s docs` --- noxfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index 282e390d281..8dc7abd40ad 100644 --- a/noxfile.py +++ b/noxfile.py @@ -116,7 +116,7 @@ def test(session): @nox.session def docs(session): - session.install(".") + session.install("-e", ".") session.install("-r", REQUIREMENTS["docs"]) def get_sphinx_build_command(kind): From 9351c61b824ac287f3fba0f9bddb8148de00a1a4 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sat, 14 Mar 2020 00:29:19 +0530 Subject: [PATCH 1358/3170] Fix typo in package-finding.rst --- docs/html/development/architecture/package-finding.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/html/development/architecture/package-finding.rst b/docs/html/development/architecture/package-finding.rst index 77eab584736..18545275de2 100644 --- a/docs/html/development/architecture/package-finding.rst +++ b/docs/html/development/architecture/package-finding.rst @@ -74,7 +74,7 @@ instantiate ``PackageFinder`` only once for the whole pip invocation. In fact, pip creates this ``PackageFinder`` instance when command options are first parsed. -With the excepton of :ref:`pip list`, each of the above commands is +With the exception of :ref:`pip list`, each of the above commands is implemented as a ``Command`` class inheriting from ``RequirementCommand`` (for example :ref:`pip download` is implemented by ``DownloadCommand``), and the ``PackageFinder`` instance is created by calling the From bbd439898b4e71dcea7d6c97bea32db3fe219dd1 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sat, 14 Mar 2020 00:29:41 +0530 Subject: [PATCH 1359/3170] Better indentation for `pip config` description --- src/pip/_internal/commands/configuration.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/commands/configuration.py b/src/pip/_internal/commands/configuration.py index efcf5bb3699..b801be6a03c 100644 --- a/src/pip/_internal/commands/configuration.py +++ b/src/pip/_internal/commands/configuration.py @@ -21,7 +21,7 @@ class ConfigurationCommand(Command): """Manage local and global configuration. - Subcommands: + Subcommands: list: List the active configuration (or from the file specified) edit: Edit the configuration file in an editor @@ -29,10 +29,10 @@ class ConfigurationCommand(Command): set: Set the name=value unset: Unset the value associated with name - If none of --user, --global and --site are passed, a virtual - environment configuration file is used if one is active and the file - exists. Otherwise, all modifications happen on the to the user file by - default. + If none of --user, --global and --site are passed, a virtual + environment configuration file is used if one is active and the file + exists. Otherwise, all modifications happen on the to the user file by + default. """ ignore_require_venv = True From 5c9c68c10feca3f4d78cfa44a0fce05337dcdeb2 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sat, 14 Mar 2020 00:28:29 +0530 Subject: [PATCH 1360/3170] WIP: Add initial draft for CLI deep dive --- .../architecture/command-line-interface.rst | 57 +++++++++++++++++++ docs/html/development/architecture/index.rst | 1 + 2 files changed, 58 insertions(+) create mode 100644 docs/html/development/architecture/command-line-interface.rst diff --git a/docs/html/development/architecture/command-line-interface.rst b/docs/html/development/architecture/command-line-interface.rst new file mode 100644 index 00000000000..9bfa9119258 --- /dev/null +++ b/docs/html/development/architecture/command-line-interface.rst @@ -0,0 +1,57 @@ +====================== +Command Line Interface +====================== + +The ``pip._internal.cli`` package is responsible for processing and providing +pip's command line interface. This package handles: + +* CLI option definition and parsing +* autocompletion +* dispatching to the various commands +* utilities like progress bars and spinners + +.. note:: + + This section of the documentation is currently being written. pip + developers welcome your help to complete this documentation. If you're + interested in helping out, please let us know in the + `tracking issue <https://github.com/pypa/pip/issues/6831>`_. + + +.. _cli-overview: + +Overview +======== + +A ``ConfigOptionParser`` instance is used as the "main parser", +for parsing top level args. + +``Command`` then uses another ``ConfigOptionParser`` instance, to parse command-specific args. + +* TODO: How & where options are defined + (cmdoptions, command-specific files). + +* TODO: How & where arguments are processed. + (main_parser, command-specific parser) + +* TODO: How processed arguments are accessed. + (attributes on argument to ``Command.run()``) + +* TODO: How configuration and CLI "blend". + (implemented in ``ConfigOptionParser``) + +* TODO: progress bars and spinners + +* TODO: quirks / standard practices / broad ideas. + (avoiding lists in option def'n, special cased option value types, + ) + + +Future Refactoring Ideas +======================== + +* Change option definition to be a more declarative, consistent, static + data-structure, replacing the current ``partial(Option, ...)`` form +* Move progress bar and spinner to a ``cli.ui`` subpackage +* Move all ``Command`` classes into a ``cli.commands`` subpackage + (including base classes) diff --git a/docs/html/development/architecture/index.rst b/docs/html/development/architecture/index.rst index 094adeede1d..2053a2d476f 100644 --- a/docs/html/development/architecture/index.rst +++ b/docs/html/development/architecture/index.rst @@ -21,6 +21,7 @@ Architecture of pip's internals overview anatomy package-finding + command-line-interface upgrade-options From a3f6c17d36339c8259b1102009335f8332840f82 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sat, 14 Mar 2020 16:36:09 +0800 Subject: [PATCH 1361/3170] Implement resolver methods on provider interface This is using a temporary PipProvider class, which should be replaced by the actual implementation after it is merged. --- .../resolution/resolvelib/resolver.py | 87 ++++++++++++++++++- 1 file changed, 84 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 2d9b14751ce..16cd61fe747 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -1,8 +1,13 @@ +from pip._vendor.resolvelib import BaseReporter +from pip._vendor.resolvelib import Resolver as RLResolver + from pip._internal.resolution.base import BaseResolver from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import List, Optional, Tuple + from typing import Dict, List, Optional, Tuple + + from pip._vendor.resolvelib.resolvers import Result from pip._internal.index.package_finder import PackageFinder from pip._internal.operations.prepare import RequirementPreparer @@ -10,6 +15,29 @@ from pip._internal.req.req_set import RequirementSet from pip._internal.resolution.base import InstallRequirementProvider + from .base import Candidate, Requirement + + +# FIXME: Import the actual implementation. +# This is a stub to pass typing checks. +class PipProvider(object): + def __init__( + self, + finder, # type: PackageFinder + preparer, # type: RequirementPreparer + make_install_req, # type: InstallRequirementProvider + ): + # type: (...) -> None + super(PipProvider, self).__init__() + + def make_requirement(self, r): + # type: (InstallRequirement) -> Requirement + raise NotImplementedError() + + def get_install_requirement(self, c): + # type: (Candidate) -> InstallRequirement + raise NotImplementedError() + class Resolver(BaseResolver): def __init__( @@ -26,11 +54,64 @@ def __init__( py_version_info=None, # type: Optional[Tuple[int, ...]] ): super(Resolver, self).__init__() + self.finder = finder + self.preparer = preparer + self.make_install_req = make_install_req + self._result = None # type: Optional[Result] def resolve(self, root_reqs, check_supported_wheels): # type: (List[InstallRequirement], bool) -> RequirementSet - raise NotImplementedError() + provider = PipProvider( + self.finder, + self.preparer, + self.make_install_req, + ) + reporter = BaseReporter() + resolver = RLResolver(provider, reporter) + + requirements = [provider.make_requirement(r) for r in root_reqs] + self._result = resolver.resolve(requirements) + + req_set = RequirementSet(check_supported_wheels=check_supported_wheels) + for candidate in self._result.mapping.values(): + ireq = provider.get_install_requirement(candidate) + req_set.add_named_requirement(ireq) + + return req_set def get_installation_order(self, req_set): # type: (RequirementSet) -> List[InstallRequirement] - raise NotImplementedError() + """Create a list that orders given requirements for installation. + + The returned list should contain all requirements in ``req_set``, + so the caller can loop through it and have a requirement installed + before the requiring thing. + + The current implementation walks the resolved dependency graph, and + make sure every node has a greater "weight" than all its parents. + """ + assert self._result is not None + weights = {None: 0} # type: Dict[Optional[str], int] + + graph = self._result.graph + while len(weights) < len(self._result.mapping): + progressed = False + for key in graph: + if key in weights: + continue + if not all(p in weights for p in graph.iter_parents(key)): + continue + weight = max(weights[p] for p in graph.iter_parents(key)) + 1 + weights[key] = weight + progressed = True + + # FIXME: This check will fail if there are unbreakable cycles. + # Implement something to forcifully break them up to continue. + assert progressed, "Order calculation stuck in dependency loop." + + sorted_items = sorted( + req_set.requirements.items(), + key=lambda item: weights[item[0]], + reverse=True, + ) + return [ireq for _, ireq in sorted_items] From 53775279c4f8449e7f7c97718ae8b377954fa674 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sun, 15 Mar 2020 22:25:26 +0800 Subject: [PATCH 1362/3170] Fix graph-walking terminal condition The weight mapping should have size `len(packages) + 1` because it needs to contain a "sentinel" node (None). --- src/pip/_internal/resolution/resolvelib/resolver.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 16cd61fe747..e7718bf81a3 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -94,7 +94,8 @@ def get_installation_order(self, req_set): weights = {None: 0} # type: Dict[Optional[str], int] graph = self._result.graph - while len(weights) < len(self._result.mapping): + key_count = len(self._result.mapping) + 1 # Packages plus sentinal. + while len(weights) < key_count: progressed = False for key in graph: if key in weights: From ffb3692e00634d57bdcf63f51c9e237fc4e1d73e Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sun, 15 Mar 2020 22:26:29 +0800 Subject: [PATCH 1363/3170] Add simple tests for get_installation_order() The implementation is improved a bit to make the sorting result more predictable for easier testing. --- .../resolution/resolvelib/resolver.py | 22 ++++- .../resolution_resolvelib/test_resolver.py | 81 +++++++++++++++++++ 2 files changed, 101 insertions(+), 2 deletions(-) create mode 100644 tests/unit/resolution_resolvelib/test_resolver.py diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index e7718bf81a3..abd9fe0b506 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -1,3 +1,6 @@ +import functools + +from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.resolvelib import BaseReporter from pip._vendor.resolvelib import Resolver as RLResolver @@ -90,7 +93,7 @@ def get_installation_order(self, req_set): The current implementation walks the resolved dependency graph, and make sure every node has a greater "weight" than all its parents. """ - assert self._result is not None + assert self._result is not None, "must call resolve() first" weights = {None: 0} # type: Dict[Optional[str], int] graph = self._result.graph @@ -112,7 +115,22 @@ def get_installation_order(self, req_set): sorted_items = sorted( req_set.requirements.items(), - key=lambda item: weights[item[0]], + key=functools.partial(_req_set_item_sorter, weights=weights), reverse=True, ) return [ireq for _, ireq in sorted_items] + + +def _req_set_item_sorter( + item, # type: Tuple[str, InstallRequirement] + weights, # type: Dict[Optional[str], int] +): + # type: (...) -> Tuple[int, str] + """Key function used to sort install requirements for installation. + + Based on the "weight" mapping calculated in ``get_installation_order()``. + The canonical package name is returned as the second member as a tie- + breaker to ensure the result is predictable, which is useful in tests. + """ + name = canonicalize_name(item[0]) + return weights[name], name diff --git a/tests/unit/resolution_resolvelib/test_resolver.py b/tests/unit/resolution_resolvelib/test_resolver.py new file mode 100644 index 00000000000..116b4d03ccc --- /dev/null +++ b/tests/unit/resolution_resolvelib/test_resolver.py @@ -0,0 +1,81 @@ +import mock +import pytest +from pip._vendor.packaging.utils import canonicalize_name +from pip._vendor.resolvelib.resolvers import Result +from pip._vendor.resolvelib.structs import DirectedGraph + +from pip._internal.req.constructors import install_req_from_line +from pip._internal.req.req_set import RequirementSet +from pip._internal.resolution.resolvelib.resolver import Resolver + + +@pytest.fixture() +def resolver(preparer, finder): + resolver = Resolver( + preparer=preparer, + finder=finder, + make_install_req=mock.Mock(), + use_user_site="not-used", + ignore_dependencies="not-used", + ignore_installed="not-used", + ignore_requires_python="not-used", + force_reinstall="not-used", + upgrade_strategy="not-used", + ) + return resolver + + +@pytest.mark.parametrize( + "edges, ordered_reqs", + [ + ( + [(None, "require-simple"), ("require-simple", "simple")], + ["simple==3.0", "require-simple==1.0"], + ), + ( + [(None, "meta"), ("meta", "simple"), ("meta", "simple2")], + ["simple2==3.0", "simple==3.0", "meta==1.0"], + ), + ( + [ + (None, "toporequires"), + (None, "toporequire2"), + (None, "toporequire3"), + (None, "toporequire4"), + ("toporequires2", "toporequires"), + ("toporequires3", "toporequires"), + ("toporequires4", "toporequires"), + ("toporequires4", "toporequires2"), + ("toporequires4", "toporequires3"), + ], + [ + "toporequires==0.0.1", + "toporequires3==0.0.1", + "toporequires2==0.0.1", + "toporequires4==0.0.1", + ], + ), + ], +) +def test_rlr_resolver_get_installation_order(resolver, edges, ordered_reqs): + # Build graph from edge declarations. + graph = DirectedGraph() + for parent, child in edges: + parent = canonicalize_name(parent) if parent else None + child = canonicalize_name(child) if child else None + for v in (parent, child): + if v not in graph: + graph.add(v) + graph.connect(parent, child) + + # Mapping values and criteria are not used in test, so we stub them out. + mapping = {vertex: None for vertex in graph if vertex is not None} + resolver._result = Result(mapping, graph, criteria=None) + + reqset = RequirementSet() + for r in ordered_reqs: + reqset.add_named_requirement(install_req_from_line(r)) + + ireqs = resolver.get_installation_order(reqset) + req_strs = [str(r.req) for r in ireqs] + assert req_strs == ordered_reqs From dad77a9e4dc097b9dd21734f503147116ccb38a2 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sun, 15 Mar 2020 22:33:17 +0800 Subject: [PATCH 1364/3170] Better handle root vetices in graph ResolveLib *should* always produce a graph with only one root vertice, which is None. But we don't really need to rely on that implementation detail. Vertices without any parents can be assigned 0 in all cases. --- src/pip/_internal/resolution/resolvelib/resolver.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index abd9fe0b506..f0750673c24 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -94,7 +94,7 @@ def get_installation_order(self, req_set): make sure every node has a greater "weight" than all its parents. """ assert self._result is not None, "must call resolve() first" - weights = {None: 0} # type: Dict[Optional[str], int] + weights = {} # type: Dict[Optional[str], int] graph = self._result.graph key_count = len(self._result.mapping) + 1 # Packages plus sentinal. @@ -103,9 +103,13 @@ def get_installation_order(self, req_set): for key in graph: if key in weights: continue - if not all(p in weights for p in graph.iter_parents(key)): + parents = list(graph.iter_parents(key)) + if not all(p in weights for p in parents): continue - weight = max(weights[p] for p in graph.iter_parents(key)) + 1 + if parents: + weight = max(weights[p] for p in parents) + 1 + else: + weight = 0 weights[key] = weight progressed = True From 7d2eb544b5ed1eeeee71e24bd3a99ab1eb33aa15 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Thu, 12 Mar 2020 15:18:47 +0000 Subject: [PATCH 1365/3170] Implement PipProvider --- .../_internal/resolution/resolvelib/base.py | 13 +- .../resolution/resolvelib/candidates.py | 112 ++++++++++++ .../resolution/resolvelib/provider.py | 71 ++++++++ .../resolution/resolvelib/requirements.py | 169 +++++++----------- tests/unit/resolution_resolvelib/conftest.py | 15 ++ .../resolution_resolvelib/test_requirement.py | 29 +-- 6 files changed, 287 insertions(+), 122 deletions(-) create mode 100644 src/pip/_internal/resolution/resolvelib/candidates.py create mode 100644 src/pip/_internal/resolution/resolvelib/provider.py diff --git a/src/pip/_internal/resolution/resolvelib/base.py b/src/pip/_internal/resolution/resolvelib/base.py index 76c3ec444ff..5e9f041b5b2 100644 --- a/src/pip/_internal/resolution/resolvelib/base.py +++ b/src/pip/_internal/resolution/resolvelib/base.py @@ -3,10 +3,10 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import (Sequence, Set) + from typing import Sequence, Set + from pip._internal.req.req_install import InstallRequirement from pip._vendor.packaging.version import _BaseVersion - from pip._internal.index.package_finder import PackageFinder def format_name(project, extras): @@ -23,11 +23,8 @@ def name(self): # type: () -> str raise NotImplementedError("Subclass should override") - def find_matches( - self, - finder, # type: PackageFinder - ): - # type: (...) -> Sequence[Candidate] + def find_matches(self): + # type: () -> Sequence[Candidate] raise NotImplementedError("Subclass should override") def is_satisfied_by(self, candidate): @@ -47,5 +44,5 @@ def version(self): raise NotImplementedError("Override in subclass") def get_dependencies(self): - # type: () -> Sequence[Requirement] + # type: () -> Sequence[InstallRequirement] raise NotImplementedError("Override in subclass") diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py new file mode 100644 index 00000000000..f068754a5a2 --- /dev/null +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -0,0 +1,112 @@ +from pip._internal.req.constructors import install_req_from_line +from pip._internal.req.req_install import InstallRequirement +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +from .base import Candidate + +if MYPY_CHECK_RUNNING: + from typing import Dict, Optional, Sequence + + from pip._internal.models.link import Link + from pip._internal.operations.prepare import RequirementPreparer + from pip._internal.resolution.base import InstallRequirementProvider + + from pip._vendor.packaging.version import _BaseVersion + from pip._vendor.pkg_resources import Distribution + + +# Dummy to make lint pass +_CANDIDATE_CACHE = {} # type: Dict[Link, Candidate] + + +def make_candidate( + link, # type: Link + preparer, # type: RequirementPreparer + parent, # type: InstallRequirement + make_install_req # type: InstallRequirementProvider +): + # type: (...) -> Candidate + if link not in _CANDIDATE_CACHE: + _CANDIDATE_CACHE[link] = LinkCandidate( + link, + preparer, + parent=parent, + make_install_req=make_install_req + ) + return _CANDIDATE_CACHE[link] + + +def make_install_req_from_link(link, parent): + # type: (Link, InstallRequirement) -> InstallRequirement + # TODO: Do we need to support editables? + return install_req_from_line( + link.url, + comes_from=parent.comes_from, + use_pep517=parent.use_pep517, + isolated=parent.isolated, + wheel_cache=parent._wheel_cache, + constraint=parent.constraint, + options=dict( + install_options=parent.install_options, + global_options=parent.global_options, + hashes=parent.hash_options + ), + ) + + +class LinkCandidate(Candidate): + def __init__( + self, + link, # type: Link + preparer, # type: RequirementPreparer + parent, # type: InstallRequirement + make_install_req, # type: InstallRequirementProvider + ): + # type: (...) -> None + self.link = link + self._preparer = preparer + self._ireq = make_install_req_from_link(link, parent) + self._make_install_req = make_install_req + + self._name = None # type: Optional[str] + self._version = None # type: Optional[_BaseVersion] + self._dist = None # type: Optional[Distribution] + + @property + def name(self): + # type: () -> str + if self._name is None: + self._name = self.dist.project_name + return self._name + + @property + def version(self): + # type: () -> _BaseVersion + if self._version is None: + self._version = self.dist.parsed_version + return self._version + + @property + def dist(self): + # type: () -> Distribution + if self._dist is None: + abstract_dist = self._preparer.prepare_linked_requirement( + self._ireq + ) + self._dist = abstract_dist.get_pkg_resources_distribution() + # TODO: Only InstalledDistribution can return None here :-( + assert self._dist is not None + # These should be "proper" errors, not just asserts, as they + # can result from user errors like a requirement "foo @ URL" + # when the project at URL has a name of "bar" in its metadata. + assert self._name is None or self._name == self._dist.project_name + assert (self._version is None or + self._version == self.dist.parsed_version) + return self._dist + + def get_dependencies(self): + # type: () -> Sequence[InstallRequirement] + return [ + self._make_install_req(r, self._ireq) + for r in self.dist.requires() + ] diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py new file mode 100644 index 00000000000..2313d40b814 --- /dev/null +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -0,0 +1,71 @@ +from pip._vendor.resolvelib.providers import AbstractProvider + +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +from .requirements import make_requirement + +if MYPY_CHECK_RUNNING: + from typing import Any, Optional, Sequence, Tuple, Union + + from pip._internal.index.package_finder import PackageFinder + from pip._internal.operations.prepare import RequirementPreparer + from pip._internal.req.req_install import InstallRequirement + from pip._internal.resolution.base import InstallRequirementProvider + + from .base import Requirement, Candidate + + +class PipProvider(AbstractProvider): + def __init__( + self, + finder, # type: PackageFinder + preparer, # type: RequirementPreparer + make_install_req # type: InstallRequirementProvider + ): + # type: (...) -> None + self._finder = finder + self._preparer = preparer + self._make_install_req = make_install_req + + def make_requirement(self, ireq): + # type: (InstallRequirement) -> Requirement + return make_requirement( + ireq, + self._finder, + self._preparer, + self._make_install_req + ) + + def identify(self, dependency): + # type: (Union[Requirement, Candidate]) -> str + return dependency.name + + def get_preference( + self, + resolution, # type: Optional[Candidate] + candidates, # type: Sequence[Candidate] + information # type: Sequence[Tuple[Requirement, Candidate]] + ): + # type: (...) -> Any + # Use the "usual" value for now + return len(candidates) + + def find_matches(self, requirement): + # type: (Requirement) -> Sequence[Candidate] + return requirement.find_matches() + + def is_satisfied_by(self, requirement, candidate): + # type: (Requirement, Candidate) -> bool + return requirement.is_satisfied_by(candidate) + + def get_dependencies(self, candidate): + # type: (Candidate) -> Sequence[Requirement] + return [ + make_requirement( + r, + self._finder, + self._preparer, + self._make_install_req + ) + for r in candidate.get_dependencies() + ] diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py index 76fa16a96e4..1ec793ad263 100644 --- a/src/pip/_internal/resolution/resolvelib/requirements.py +++ b/src/pip/_internal/resolution/resolvelib/requirements.py @@ -1,141 +1,108 @@ from pip._vendor.packaging.utils import canonicalize_name -from pip._internal.req.req_install import InstallRequirement from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from .base import Candidate, Requirement, format_name +from .base import Requirement +from .candidates import make_candidate if MYPY_CHECK_RUNNING: - from typing import (Optional, Sequence) - - from pip._vendor.packaging.version import _BaseVersion + from typing import Sequence from pip._internal.index.package_finder import PackageFinder - - -def make_requirement(install_req): - # type: (InstallRequirement) -> Requirement - if install_req.link: - if install_req.req and install_req.req.name: - return NamedRequirement(install_req) - else: - return UnnamedRequirement(install_req) + from pip._internal.operations.prepare import RequirementPreparer + from pip._internal.req.req_install import InstallRequirement + from pip._internal.resolution.base import InstallRequirementProvider + + from .base import Candidate + + +def make_requirement( + ireq, # type: InstallRequirement + finder, # type: PackageFinder + preparer, # type: RequirementPreparer + make_install_req # type: InstallRequirementProvider +): + # type: (...) -> Requirement + if ireq.link: + candidate = make_candidate( + ireq.link, + preparer, + ireq, + make_install_req + ) + return ExplicitRequirement(candidate) else: - return VersionedRequirement(install_req) + return SpecifierRequirement( + ireq, + finder, + preparer, + make_install_req + ) -class UnnamedRequirement(Requirement): - def __init__(self, req): - # type: (InstallRequirement) -> None - self._ireq = req - self._candidate = None # type: Optional[Candidate] +class ExplicitRequirement(Requirement): + def __init__(self, candidate): + # type: (Candidate) -> None + self.candidate = candidate @property def name(self): # type: () -> str - assert self._ireq.req is None or self._ireq.name is None, \ - "Unnamed requirement has a name" - # TODO: Get the candidate and use its name... - return "" - - def _get_candidate(self): - # type: () -> Candidate - if self._candidate is None: - self._candidate = Candidate() - return self._candidate - - def find_matches( - self, - finder, # type: PackageFinder - ): - # type: (...) -> Sequence[Candidate] - return [self._get_candidate()] + return self.candidate.name + + def find_matches(self): + # type: () -> Sequence[Candidate] + return [self.candidate] def is_satisfied_by(self, candidate): # type: (Candidate) -> bool - return candidate is self._get_candidate() - - -class NamedRequirement(Requirement): - def __init__(self, req): - # type: (InstallRequirement) -> None - self._ireq = req - self._candidate = None # type: Optional[Candidate] - - @property - def name(self): - # type: () -> str - assert self._ireq.req.name is not None, "Named requirement has no name" - canonical_name = canonicalize_name(self._ireq.req.name) - return format_name(canonical_name, self._ireq.req.extras) + # TODO: Typing - Candidate doesn't have a link attribute + # But I think the following would be better... + # return candidate.link == self.candidate.link + return candidate == self.candidate - def _get_candidate(self): - # type: () -> Candidate - if self._candidate is None: - self._candidate = Candidate() - return self._candidate - def find_matches( +class SpecifierRequirement(Requirement): + def __init__( self, - finder, # type: PackageFinder + ireq, # type: InstallRequirement + finder, # type: PackageFinder + preparer, # type:RequirementPreparer + make_install_req # type: InstallRequirementProvider ): - # type: (...) -> Sequence[Candidate] - return [self._get_candidate()] - - def is_satisfied_by(self, candidate): - # type: (Candidate) -> bool - return candidate is self._get_candidate() - - -# TODO: This is temporary, to make the tests pass -class DummyCandidate(Candidate): - def __init__(self, name, version): - # type: (str, _BaseVersion) -> None - self._name = name - self._version = version - - @property - def name(self): - # type: () -> str - return self._name - - @property - def version(self): - # type: () -> _BaseVersion - return self._version - - -class VersionedRequirement(Requirement): - def __init__(self, ireq): - # type: (InstallRequirement) -> None - assert ireq.req is not None, "Un-specified requirement not allowed" - assert ireq.req.url is None, "Direct reference not allowed" + # type: (...) -> None + assert ireq.link is None, "This is a link, not a specifier" + assert not ireq.req.extras, "Extras not yet supported" self._ireq = ireq + self._finder = finder + self._preparer = preparer + self._make_install_req = make_install_req @property def name(self): # type: () -> str canonical_name = canonicalize_name(self._ireq.req.name) - return format_name(canonical_name, self._ireq.req.extras) + return canonical_name - def find_matches( - self, - finder, # type: PackageFinder - ): - # type: (...) -> Sequence[Candidate] - found = finder.find_best_candidate( + def find_matches(self): + # type: () -> Sequence[Candidate] + found = self._finder.find_best_candidate( project_name=self._ireq.req.name, specifier=self._ireq.req.specifier, hashes=self._ireq.hashes(trust_internet=False), ) return [ - DummyCandidate(ican.name, ican.version) + make_candidate( + ican.link, + self._preparer, + self._ireq, + self._make_install_req + ) for ican in found.iter_applicable() ] def is_satisfied_by(self, candidate): # type: (Candidate) -> bool - # TODO: Should check name matches as well. Defer this - # until we have the proper Candidate object, and - # no longer have to deal with unnmed requirements... + assert candidate.name == self.name, \ + "Internal issue: Candidate is not for this requirement" return candidate.version in self._ireq.req.specifier diff --git a/tests/unit/resolution_resolvelib/conftest.py b/tests/unit/resolution_resolvelib/conftest.py index f885d1c855b..6ddc6422adf 100644 --- a/tests/unit/resolution_resolvelib/conftest.py +++ b/tests/unit/resolution_resolvelib/conftest.py @@ -1,3 +1,5 @@ +from functools import partial + import pytest from pip._internal.cli.req_command import RequirementCommand @@ -8,7 +10,9 @@ from pip._internal.models.search_scope import SearchScope from pip._internal.models.selection_prefs import SelectionPreferences from pip._internal.network.session import PipSession +from pip._internal.req.constructors import install_req_from_req_string from pip._internal.req.req_tracker import get_requirement_tracker +from pip._internal.resolution.resolvelib.provider import PipProvider from pip._internal.utils.temp_dir import TempDirectory, global_tempdir_manager @@ -41,3 +45,14 @@ def preparer(finder): ) yield preparer + + +@pytest.fixture +def provider(finder, preparer): + make_install_req = partial( + install_req_from_req_string, + isolated=False, + wheel_cache=None, + use_pep517=None, + ) + yield PipProvider(finder, preparer, make_install_req) diff --git a/tests/unit/resolution_resolvelib/test_requirement.py b/tests/unit/resolution_resolvelib/test_requirement.py index 28fdaaa20e9..41a6f7de977 100644 --- a/tests/unit/resolution_resolvelib/test_requirement.py +++ b/tests/unit/resolution_resolvelib/test_requirement.py @@ -16,7 +16,6 @@ # Create a requirement from a sdist filename # Create a requirement from a local directory (which has no obvious name!) # Editables -# @pytest.fixture @@ -32,16 +31,17 @@ def data_url(name): # Version specifiers ("simple", "simple", 3), ("simple>1.0", "simple", 2), - ("simple[extra]==1.0", "simple[extra]", 1), + # ("simple[extra]==1.0", "simple[extra]", 1), # Wheels (data_file("simplewheel-1.0-py2.py3-none-any.whl"), "simplewheel", 1), (data_url("simplewheel-1.0-py2.py3-none-any.whl"), "simplewheel", 1), # Direct URLs - ("foo @ " + data_url("simple-1.0.tar.gz"), "foo", 1), + # TODO: The following test fails + # ("foo @ " + data_url("simple-1.0.tar.gz"), "foo", 1), # SDists # TODO: sdists should have a name - (data_file("simple-1.0.tar.gz"), "", 1), - (data_url("simple-1.0.tar.gz"), "", 1), + (data_file("simple-1.0.tar.gz"), "simple", 1), + (data_url("simple-1.0.tar.gz"), "simple", 1), # TODO: directory, editables ] @@ -52,24 +52,27 @@ def req_from_line(line): return make_requirement(install_req_from_line(line)) -def test_rlr_requirement_has_name(test_cases): +def test_rlr_requirement_has_name(test_cases, provider): """All requirements should have a name""" for requirement, name, matches in test_cases: - req = req_from_line(requirement) + ireq = install_req_from_line(requirement) + req = provider.make_requirement(ireq) assert req.name == name -def test_rlr_correct_number_of_matches(test_cases, finder): +def test_rlr_correct_number_of_matches(test_cases, provider): """Requirements should return the correct number of candidates""" for requirement, name, matches in test_cases: - req = req_from_line(requirement) - assert len(req.find_matches(finder)) == matches + ireq = install_req_from_line(requirement) + req = provider.make_requirement(ireq) + assert len(req.find_matches()) == matches -def test_rlr_candidates_match_requirement(test_cases, finder): +def test_rlr_candidates_match_requirement(test_cases, provider): """Candidates returned from find_matches should satisfy the requirement""" for requirement, name, matches in test_cases: - req = req_from_line(requirement) - for c in req.find_matches(finder): + ireq = install_req_from_line(requirement) + req = provider.make_requirement(ireq) + for c in req.find_matches(): assert isinstance(c, Candidate) assert req.is_satisfied_by(c) From 098d00d8c3e9b3a92b3262368a3c509e5ba7a3e6 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Fri, 13 Mar 2020 15:06:57 +0000 Subject: [PATCH 1366/3170] Add a test that runs the full resolver --- tests/unit/resolution_resolvelib/test_requirement.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/unit/resolution_resolvelib/test_requirement.py b/tests/unit/resolution_resolvelib/test_requirement.py index 41a6f7de977..a563eea28d0 100644 --- a/tests/unit/resolution_resolvelib/test_requirement.py +++ b/tests/unit/resolution_resolvelib/test_requirement.py @@ -1,4 +1,5 @@ import pytest +from pip._vendor.resolvelib import BaseReporter, Resolver from pip._internal.req.constructors import install_req_from_line from pip._internal.resolution.resolvelib.base import Candidate @@ -76,3 +77,12 @@ def test_rlr_candidates_match_requirement(test_cases, provider): for c in req.find_matches(): assert isinstance(c, Candidate) assert req.is_satisfied_by(c) + + +def test_rlr_full_resolve(provider): + """A very basic full resolve""" + ireq = install_req_from_line("simplewheel") + req = provider.make_requirement(ireq) + r = Resolver(provider, BaseReporter()) + result = r.resolve([req]) + assert set(result.mapping.keys()) == {'simplewheel'} From 1ebe1e093583a8e694217a37b88076123534bf62 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Wed, 18 Mar 2020 14:49:49 +0000 Subject: [PATCH 1367/3170] Integrate the new provider with --unstable-feature=resolver --- .../resolution/resolvelib/provider.py | 4 +++ .../resolution/resolvelib/resolver.py | 26 ++----------------- 2 files changed, 6 insertions(+), 24 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index 2313d40b814..21c3ecbb6d8 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -36,6 +36,10 @@ def make_requirement(self, ireq): self._make_install_req ) + def get_install_requirement(self, c): + # type: (Candidate) -> InstallRequirement + return getattr(c, "_ireq", None) + def identify(self, dependency): # type: (Union[Requirement, Candidate]) -> str return dependency.name diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index f0750673c24..cb40ab70b95 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -4,7 +4,9 @@ from pip._vendor.resolvelib import BaseReporter from pip._vendor.resolvelib import Resolver as RLResolver +from pip._internal.req.req_set import RequirementSet from pip._internal.resolution.base import BaseResolver +from pip._internal.resolution.resolvelib.provider import PipProvider from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: @@ -15,32 +17,8 @@ from pip._internal.index.package_finder import PackageFinder from pip._internal.operations.prepare import RequirementPreparer from pip._internal.req.req_install import InstallRequirement - from pip._internal.req.req_set import RequirementSet from pip._internal.resolution.base import InstallRequirementProvider - from .base import Candidate, Requirement - - -# FIXME: Import the actual implementation. -# This is a stub to pass typing checks. -class PipProvider(object): - def __init__( - self, - finder, # type: PackageFinder - preparer, # type: RequirementPreparer - make_install_req, # type: InstallRequirementProvider - ): - # type: (...) -> None - super(PipProvider, self).__init__() - - def make_requirement(self, r): - # type: (InstallRequirement) -> Requirement - raise NotImplementedError() - - def get_install_requirement(self, c): - # type: (Candidate) -> InstallRequirement - raise NotImplementedError() - class Resolver(BaseResolver): def __init__( From f1b4be892a198b69560c80b70d119fe7de90f970 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Wed, 18 Mar 2020 14:53:04 +0000 Subject: [PATCH 1368/3170] Fix bug in get_dependencies() passing Requirement object rather than string --- src/pip/_internal/resolution/resolvelib/candidates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index f068754a5a2..b7b24e03a26 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -107,6 +107,6 @@ def dist(self): def get_dependencies(self): # type: () -> Sequence[InstallRequirement] return [ - self._make_install_req(r, self._ireq) + self._make_install_req(str(r), self._ireq) for r in self.dist.requires() ] From a23e936bc72603daec45bc7a8c63174b461b9f5d Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Wed, 18 Mar 2020 14:54:15 +0000 Subject: [PATCH 1369/3170] Ensure candidate name is canonicalised --- src/pip/_internal/resolution/resolvelib/candidates.py | 4 +++- src/pip/_internal/resolution/resolvelib/requirements.py | 4 +++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index b7b24e03a26..a8e1aa9033d 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -1,3 +1,5 @@ +from pip._vendor.packaging.utils import canonicalize_name + from pip._internal.req.constructors import install_req_from_line from pip._internal.req.req_install import InstallRequirement from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -76,7 +78,7 @@ def __init__( def name(self): # type: () -> str if self._name is None: - self._name = self.dist.project_name + self._name = canonicalize_name(self.dist.project_name) return self._name @property diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py index 1ec793ad263..d5965e3e13f 100644 --- a/src/pip/_internal/resolution/resolvelib/requirements.py +++ b/src/pip/_internal/resolution/resolvelib/requirements.py @@ -103,6 +103,8 @@ def find_matches(self): def is_satisfied_by(self, candidate): # type: (Candidate) -> bool + assert candidate.name == self.name, \ - "Internal issue: Candidate is not for this requirement" + "Internal issue: Candidate is not for this requirement " \ + " {} vs {}".format(candidate.name, self.name) return candidate.version in self._ireq.req.specifier From cec27c747cb63783016a05c842805851dfd30874 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Wed, 18 Mar 2020 12:13:07 +0000 Subject: [PATCH 1370/3170] Added an end-to-end install test of the new resolver --- tests/functional/test_install.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 330cc34836d..1072c81d198 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -405,12 +405,17 @@ def test_vcs_url_urlquote_normalization(script, tmpdir): ) -def test_basic_install_from_local_directory(script, data): +@pytest.mark.parametrize("resolver", ["", "--unstable-feature=resolver"]) +def test_basic_install_from_local_directory(script, data, resolver): """ Test installing from a local directory. """ + args = ["install"] + if resolver: + args.append(resolver) to_install = data.packages.joinpath("FSPkg") - result = script.pip('install', to_install) + args.append(to_install) + result = script.pip(*args) fspkg_folder = script.site_packages / 'fspkg' egg_info_folder = ( script.site_packages / From ffe553638c035292e17657cbacbfb48e189a9221 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Thu, 19 Mar 2020 10:53:15 +0000 Subject: [PATCH 1371/3170] Address review requirements --- .../resolution/resolvelib/candidates.py | 18 +++++++++++++++--- .../resolution/resolvelib/provider.py | 8 ++++++++ .../resolution/resolvelib/requirements.py | 1 + 3 files changed, 24 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index a8e1aa9033d..08eff0ddf9d 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -7,7 +7,7 @@ from .base import Candidate if MYPY_CHECK_RUNNING: - from typing import Dict, Optional, Sequence + from typing import Any, Dict, Optional, Sequence from pip._internal.models.link import Link from pip._internal.operations.prepare import RequirementPreparer @@ -17,7 +17,6 @@ from pip._vendor.pkg_resources import Distribution -# Dummy to make lint pass _CANDIDATE_CACHE = {} # type: Dict[Link, Candidate] @@ -74,9 +73,21 @@ def __init__( self._version = None # type: Optional[_BaseVersion] self._dist = None # type: Optional[Distribution] + def __eq__(self, other): + # type: (Any) -> bool + if isinstance(other, self.__class__): + return self.link == other.link + return False + + # Needed for Python 2, which does not implement this by default + def __ne__(self, other): + # type: (Any) -> bool + return not self.__eq__(other) + @property def name(self): # type: () -> str + """The normalised name of the project the candidate refers to""" if self._name is None: self._name = canonicalize_name(self.dist.project_name) return self._name @@ -101,7 +112,8 @@ def dist(self): # These should be "proper" errors, not just asserts, as they # can result from user errors like a requirement "foo @ URL" # when the project at URL has a name of "bar" in its metadata. - assert self._name is None or self._name == self._dist.project_name + assert (self._name is None or + self._name == canonicalize_name(self._dist.project_name)) assert (self._version is None or self._version == self.dist.parsed_version) return self._dist diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index 21c3ecbb6d8..981646ea96c 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -38,6 +38,14 @@ def make_requirement(self, ireq): def get_install_requirement(self, c): # type: (Candidate) -> InstallRequirement + + # The base Candidate class does not have an _ireq attribute, so we + # fetch it dynamically here, to satisfy mypy. In practice, though, we + # only ever deal with LinkedCandidate objects at the moment, which do + # have an _ireq attribute. When we have a candidate type for installed + # requirements we should probably review this. + # + # TODO: Longer term, make a proper interface for this on the candidate. return getattr(c, "_ireq", None) def identify(self, dependency): diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py index d5965e3e13f..4adc8a09a34 100644 --- a/src/pip/_internal/resolution/resolvelib/requirements.py +++ b/src/pip/_internal/resolution/resolvelib/requirements.py @@ -48,6 +48,7 @@ def __init__(self, candidate): @property def name(self): # type: () -> str + # No need to canonicalise - the candidate did this return self.candidate.name def find_matches(self): From 98aa09cf88d8851bb2be6ad39be1cbca7d181916 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= <miro@hroncok.cz> Date: Thu, 19 Mar 2020 17:57:53 +0100 Subject: [PATCH 1372/3170] Prevent infinite recursion with pip wheel with $TMPDIR in $PWD During a build of extension module within `pip wheel` the source directory is recursively copied in a temporary directory. See https://github.com/pypa/pip/issues/7555 When the temporary directory is inside the source directory (for example by setting `TMPDIR=$PWD/tmp`) this caused an infinite recursion that ended in: [Errno 36] File name too long We prevent that buy never copying the target to the target in _copy_source_tree. Fixes https://github.com/pypa/pip/issues/7872 --- news/7872.bugfix | 1 + src/pip/_internal/operations/prepare.py | 22 +++++++++++++++++----- tests/data/src/extension/extension.c | 0 tests/data/src/extension/setup.py | 4 ++++ tests/functional/test_wheel.py | 11 +++++++++++ 5 files changed, 33 insertions(+), 5 deletions(-) create mode 100644 news/7872.bugfix create mode 100644 tests/data/src/extension/extension.c create mode 100644 tests/data/src/extension/setup.py diff --git a/news/7872.bugfix b/news/7872.bugfix new file mode 100644 index 00000000000..3550d573b88 --- /dev/null +++ b/news/7872.bugfix @@ -0,0 +1 @@ +Prevent an infinite recursion with ``pip wheel`` when ``$TMPDIR`` is within the source directory. diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 9f87148c031..1fcbb775ece 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -156,13 +156,25 @@ def _copy2_ignoring_special_files(src, dest): def _copy_source_tree(source, target): # type: (str, str) -> None + target_abspath = os.path.abspath(target) + target_basename = os.path.basename(target_abspath) + target_dirname = os.path.dirname(target_abspath) + def ignore(d, names): # type: (str, List[str]) -> List[str] - # Pulling in those directories can potentially be very slow, - # exclude the following directories if they appear in the top - # level dir (and only it). - # See discussion at https://github.com/pypa/pip/pull/6770 - return ['.tox', '.nox'] if d == source else [] + skipped = [] # type: List[str] + if d == source: + # Pulling in those directories can potentially be very slow, + # exclude the following directories if they appear in the top + # level dir (and only it). + # See discussion at https://github.com/pypa/pip/pull/6770 + skipped += ['.tox', '.nox'] + if os.path.abspath(d) == target_dirname: + # Prevent an infinite recursion if the target is in source. + # This can happen when TMPDIR is set to ${PWD}/... + # and we copy PWD to TMPDIR. + skipped += [target_basename] + return skipped kwargs = dict(ignore=ignore, symlinks=True) # type: CopytreeKwargs diff --git a/tests/data/src/extension/extension.c b/tests/data/src/extension/extension.c new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/data/src/extension/setup.py b/tests/data/src/extension/setup.py new file mode 100644 index 00000000000..b26302b0536 --- /dev/null +++ b/tests/data/src/extension/setup.py @@ -0,0 +1,4 @@ +from setuptools import Extension, setup + +module = Extension('extension', sources=['extension.c']) +setup(name='extension', version='0.0.1', ext_modules = [module]) diff --git a/tests/functional/test_wheel.py b/tests/functional/test_wheel.py index ce79dbee5ee..f293233b9d9 100644 --- a/tests/functional/test_wheel.py +++ b/tests/functional/test_wheel.py @@ -289,6 +289,17 @@ def test_pip_wheel_with_user_set_in_config(script, data, common_wheels): assert "Successfully built withpyproject" in result.stdout, result.stdout +def test_pip_wheel_ext_module_with_tmpdir_inside(script, data, common_wheels): + tmpdir = data.src / 'extension/tmp' + tmpdir.mkdir() + script.environ['TMPDIR'] = str(tmpdir) + result = script.pip( + 'wheel', data.src / 'extension', + '--no-index', '-f', common_wheels + ) + assert "Successfully built extension" in result.stdout, result.stdout + + @pytest.mark.network def test_pep517_wheels_are_not_confused_with_other_files(script, tmpdir, data): """Check correct wheels are copied. (#6196) From eb070d23721c5a0bff59ed5a252291efd3f5a7c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= <miro@hroncok.cz> Date: Thu, 19 Mar 2020 23:21:56 +0100 Subject: [PATCH 1373/3170] Avoid a test dependency on a C compiler, skip the test on Windows --- tests/functional/test_wheel.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/functional/test_wheel.py b/tests/functional/test_wheel.py index f293233b9d9..545c50ac9a8 100644 --- a/tests/functional/test_wheel.py +++ b/tests/functional/test_wheel.py @@ -1,6 +1,7 @@ """'pip wheel' tests""" import os import re +import sys from os.path import exists import pytest @@ -289,10 +290,17 @@ def test_pip_wheel_with_user_set_in_config(script, data, common_wheels): assert "Successfully built withpyproject" in result.stdout, result.stdout +@pytest.mark.skipif(sys.platform.startswith('win'), + reason='The empty extension module does not work on Win') def test_pip_wheel_ext_module_with_tmpdir_inside(script, data, common_wheels): tmpdir = data.src / 'extension/tmp' tmpdir.mkdir() script.environ['TMPDIR'] = str(tmpdir) + + # To avoid a test dependency on a C compiler, we set the env vars to "noop" + # The .c source is empty anyway + script.environ['CC'] = script.environ['LDSHARED'] = str('true') + result = script.pip( 'wheel', data.src / 'extension', '--no-index', '-f', common_wheels From 89f4f16e3ec8b6018c9ef487ae4059271e1829df Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Fri, 20 Mar 2020 11:00:07 +0000 Subject: [PATCH 1374/3170] Add some functional tests for the new resolver --- tests/functional/test_new_resolver.py | 81 +++++++++++++++++++++++++++ 1 file changed, 81 insertions(+) create mode 100644 tests/functional/test_new_resolver.py diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py new file mode 100644 index 00000000000..e0118a91ca1 --- /dev/null +++ b/tests/functional/test_new_resolver.py @@ -0,0 +1,81 @@ +import json +from tests.lib import create_basic_wheel_for_package + + +def assert_installed(script, **kwargs): + ret = script.pip('list', '--format=json') + installed = set( + (val['name'], val['version']) + for val in json.loads(ret.stdout) + ) + assert set(kwargs.items()) <= installed + + +def test_new_resolver_can_install(script): + package = create_basic_wheel_for_package( + script, + "simple", + "0.1.0", + ) + script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "simple" + ) + assert_installed(script, simple="0.1.0") + + +def test_new_resolver_can_install_with_version(script): + package = create_basic_wheel_for_package( + script, + "simple", + "0.1.0", + ) + script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "simple==0.1.0" + ) + assert_installed(script, simple="0.1.0") + + +def test_new_resolver_picks_latest_version(script): + package = create_basic_wheel_for_package( + script, + "simple", + "0.1.0", + ) + package = create_basic_wheel_for_package( + script, + "simple", + "0.2.0", + ) + script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "simple" + ) + assert_installed(script, simple="0.2.0") + +def test_new_resolver_installs_dependencies(script): + package = create_basic_wheel_for_package( + script, + "base", + "0.1.0", + depends=["dep"], + ) + package = create_basic_wheel_for_package( + script, + "dep", + "0.1.0", + ) + script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "base" + ) + assert_installed(script, base="0.1.0", dep="0.1.0") From 0b1306bbf08bf20e58846678a60939718ee37b07 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Fri, 20 Mar 2020 11:12:38 +0000 Subject: [PATCH 1375/3170] I must remember to run lint locally before pushing :-( --- tests/functional/test_new_resolver.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index e0118a91ca1..cd7443dd6ad 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -1,4 +1,5 @@ import json + from tests.lib import create_basic_wheel_for_package @@ -9,10 +10,10 @@ def assert_installed(script, **kwargs): for val in json.loads(ret.stdout) ) assert set(kwargs.items()) <= installed - + def test_new_resolver_can_install(script): - package = create_basic_wheel_for_package( + create_basic_wheel_for_package( script, "simple", "0.1.0", @@ -27,7 +28,7 @@ def test_new_resolver_can_install(script): def test_new_resolver_can_install_with_version(script): - package = create_basic_wheel_for_package( + create_basic_wheel_for_package( script, "simple", "0.1.0", @@ -42,12 +43,12 @@ def test_new_resolver_can_install_with_version(script): def test_new_resolver_picks_latest_version(script): - package = create_basic_wheel_for_package( + create_basic_wheel_for_package( script, "simple", "0.1.0", ) - package = create_basic_wheel_for_package( + create_basic_wheel_for_package( script, "simple", "0.2.0", @@ -60,14 +61,15 @@ def test_new_resolver_picks_latest_version(script): ) assert_installed(script, simple="0.2.0") + def test_new_resolver_installs_dependencies(script): - package = create_basic_wheel_for_package( + create_basic_wheel_for_package( script, "base", "0.1.0", depends=["dep"], ) - package = create_basic_wheel_for_package( + create_basic_wheel_for_package( script, "dep", "0.1.0", From c83505b848d1bc953a66fcb5b058d761def92703 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Fri, 20 Mar 2020 13:29:45 +0000 Subject: [PATCH 1376/3170] Remove an obsolete comment --- src/pip/_internal/resolution/resolvelib/requirements.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py index 4adc8a09a34..1a5f257a574 100644 --- a/src/pip/_internal/resolution/resolvelib/requirements.py +++ b/src/pip/_internal/resolution/resolvelib/requirements.py @@ -57,9 +57,6 @@ def find_matches(self): def is_satisfied_by(self, candidate): # type: (Candidate) -> bool - # TODO: Typing - Candidate doesn't have a link attribute - # But I think the following would be better... - # return candidate.link == self.candidate.link return candidate == self.candidate From e42929dcf64fccbea0d93a0bfaf965e20c569e39 Mon Sep 17 00:00:00 2001 From: onlinejudge95 <onlinejudge95> Date: Sat, 21 Mar 2020 12:49:15 +0530 Subject: [PATCH 1377/3170] Rephrases documentation --- docs/html/development/getting-started.rst | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/docs/html/development/getting-started.rst b/docs/html/development/getting-started.rst index 6fa4f9edc54..025435f6541 100644 --- a/docs/html/development/getting-started.rst +++ b/docs/html/development/getting-started.rst @@ -49,18 +49,18 @@ pip's tests are written using the :pypi:`pytest` test framework, :pypi:`mock` and :pypi:`pretend`. :pypi:`tox` is used to automate the setup and execution of pip's tests. +It is preferable to run the tests in **parallel** for better user experience during development, +as the test suite is very extensive. WIthout the `-n auto` your tests would be running sequentially, +causing more time to finish. To run tests locally, run: - .. code-block:: console - $ tox -e py36 - -Generally, it can take a long time to run pip's test suite. To run tests in parallel, -which is faster, run: + $ tox -e py36 -- -n auto +For running tests sequentially remove the `-n` flag. .. code-block:: console - $ tox -e py36 -- -n auto + $ tox -e py36 The example above runs tests against Python 3.6. You can also use other versions like ``py27`` and ``pypy3``. From c387d8e7663584914112b328b50fba53a83a775a Mon Sep 17 00:00:00 2001 From: onlinejudge95 <onlinejudge95> Date: Sat, 21 Mar 2020 12:52:06 +0530 Subject: [PATCH 1378/3170] Adds news --- news/7683.doc | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/7683.doc diff --git a/news/7683.doc b/news/7683.doc new file mode 100644 index 00000000000..67680eef870 --- /dev/null +++ b/news/7683.doc @@ -0,0 +1 @@ +Rephrases test execution command \ No newline at end of file From 3cadfd2e80456656332f738c3e1abd0f95a38228 Mon Sep 17 00:00:00 2001 From: onlinejudge95 <onlinejudge95> Date: Sat, 21 Mar 2020 13:30:26 +0530 Subject: [PATCH 1379/3170] Addresses PR comments --- docs/html/development/getting-started.rst | 2 +- news/7683.doc | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/html/development/getting-started.rst b/docs/html/development/getting-started.rst index 025435f6541..59b7370ce43 100644 --- a/docs/html/development/getting-started.rst +++ b/docs/html/development/getting-started.rst @@ -50,7 +50,7 @@ and :pypi:`pretend`. :pypi:`tox` is used to automate the setup and execution of pip's tests. It is preferable to run the tests in **parallel** for better user experience during development, -as the test suite is very extensive. WIthout the `-n auto` your tests would be running sequentially, +as the test suite is very extensive. Without the `-n auto` your tests would be running sequentially, causing more time to finish. To run tests locally, run: .. code-block:: console diff --git a/news/7683.doc b/news/7683.doc index 67680eef870..921a8093316 100644 --- a/news/7683.doc +++ b/news/7683.doc @@ -1 +1 @@ -Rephrases test execution command \ No newline at end of file +Rephrases test execution command From adf3dc8572ee06d1ee5526b14440eb5fc3cfe8d1 Mon Sep 17 00:00:00 2001 From: onlinejudge95 <onlinejudge95> Date: Sat, 21 Mar 2020 13:43:54 +0530 Subject: [PATCH 1380/3170] Fixes linting checks --- docs/html/development/getting-started.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/html/development/getting-started.rst b/docs/html/development/getting-started.rst index 59b7370ce43..f8db708110c 100644 --- a/docs/html/development/getting-started.rst +++ b/docs/html/development/getting-started.rst @@ -50,14 +50,14 @@ and :pypi:`pretend`. :pypi:`tox` is used to automate the setup and execution of pip's tests. It is preferable to run the tests in **parallel** for better user experience during development, -as the test suite is very extensive. Without the `-n auto` your tests would be running sequentially, +as the test suite is very extensive. Without the ``-n auto`` your tests would be running sequentially, causing more time to finish. To run tests locally, run: .. code-block:: console $ tox -e py36 -- -n auto -For running tests sequentially remove the `-n` flag. +For running tests sequentially remove the ``-n`` flag. .. code-block:: console $ tox -e py36 From 4d6a982976135fcc09a54edb2f5ddcefe8ba24bc Mon Sep 17 00:00:00 2001 From: onlinejudge95 <onlinejudge95> Date: Sat, 21 Mar 2020 14:28:34 +0530 Subject: [PATCH 1381/3170] Addresses PR comments --- docs/html/development/getting-started.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/html/development/getting-started.rst b/docs/html/development/getting-started.rst index f8db708110c..fe081c54f1d 100644 --- a/docs/html/development/getting-started.rst +++ b/docs/html/development/getting-started.rst @@ -49,9 +49,8 @@ pip's tests are written using the :pypi:`pytest` test framework, :pypi:`mock` and :pypi:`pretend`. :pypi:`tox` is used to automate the setup and execution of pip's tests. -It is preferable to run the tests in **parallel** for better user experience during development, -as the test suite is very extensive. Without the ``-n auto`` your tests would be running sequentially, -causing more time to finish. +It is preferable to run the tests in parallel for better experience during development, +since the tests can take a long time to finish when run sequentially. To run tests locally, run: .. code-block:: console From 58adbb507a60ca1b76a253914f667d899998d184 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Mon, 23 Mar 2020 14:54:25 +0800 Subject: [PATCH 1382/3170] Add Travis job against the new resolver --- .travis.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/.travis.yml b/.travis.yml index 7259b473a45..ffd0f7698f2 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,6 +11,9 @@ stages: - primary - secondary +env: + - PIP_UNSTABLE_FEATURE= + jobs: include: # Basic Checks @@ -51,7 +54,19 @@ jobs: - env: GROUP=2 python: 3.5 + # Test experimental stuff that are not part of the standard pip usage. + # Helpful for developers working on them to see how they're doing. + - stage: experimental + env: + - GROUP=1 + - PIP_UNSTABLE_FEATURE=resolver + - env: + - GROUP=2 + - PIP_UNSTABLE_FEATURE=resolver + fast_finish: true + allow_failures: + - env: PIP_UNSTABLE_FEATURE=resolver before_install: tools/travis/setup.sh install: travis_retry tools/travis/install.sh From bfd7168046483219017fc0fed9fe359891704644 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Mon, 23 Mar 2020 15:52:00 +0800 Subject: [PATCH 1383/3170] Remove global env declaration for now This will probably break allow_failures, but let's ensure things fail first. --- .travis.yml | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index ffd0f7698f2..29bde32a2f3 100644 --- a/.travis.yml +++ b/.travis.yml @@ -11,9 +11,6 @@ stages: - primary - secondary -env: - - PIP_UNSTABLE_FEATURE= - jobs: include: # Basic Checks @@ -58,8 +55,8 @@ jobs: # Helpful for developers working on them to see how they're doing. - stage: experimental env: - - GROUP=1 - - PIP_UNSTABLE_FEATURE=resolver + - GROUP=1 + - PIP_UNSTABLE_FEATURE=resolver - env: - GROUP=2 - PIP_UNSTABLE_FEATURE=resolver From f5093a022be560a5cdfbc6b7f3670c835d0f8f1e Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Mon, 23 Mar 2020 16:32:20 +0800 Subject: [PATCH 1384/3170] Pass through PIP_UNSTABLE_FEATURE into Tox --- tox.ini | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 993d3f517b9..f6d5cb74179 100644 --- a/tox.ini +++ b/tox.ini @@ -11,7 +11,14 @@ pip = python {toxinidir}/tools/tox_pip.py [testenv] # Remove USERNAME once we drop PY2. -passenv = CI GIT_SSL_CAINFO USERNAME HTTP_PROXY HTTPS_PROXY NO_PROXY +passenv = + CI + GIT_SSL_CAINFO + USERNAME + HTTP_PROXY + HTTPS_PROXY + NO_PROXY + PIP_UNSTABLE_FEATURE setenv = # This is required in order to get UTF-8 output inside of the subprocesses # that our tests use. From 9ce1690a685b63e8988c2c7bd94ed26a745319af Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Mon, 23 Mar 2020 17:05:04 +0800 Subject: [PATCH 1385/3170] Expiremental stage --- .travis.yml | 1 + 1 file changed, 1 insertion(+) diff --git a/.travis.yml b/.travis.yml index 29bde32a2f3..39878fb2d6d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,6 +10,7 @@ addons: stages: - primary - secondary +- experimental jobs: include: From 9f5a8eb69f5ce360cfa3531cda47a13c498f909d Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Mon, 23 Mar 2020 17:05:27 +0800 Subject: [PATCH 1386/3170] Add back global env definition --- .travis.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.travis.yml b/.travis.yml index 39878fb2d6d..4900e8a405e 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,6 +7,10 @@ addons: packages: - bzr +env: + global: + - PIP_UNSTABLE_FEATURE= + stages: - primary - secondary From ff35f7f26a33f558ed7b20a8c7ac09c7ce883fc5 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Mon, 23 Mar 2020 17:15:55 +0800 Subject: [PATCH 1387/3170] Remove stale TODO comment --- src/pip/_internal/resolution/resolvelib/requirements.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py index 4adc8a09a34..74c464d59ff 100644 --- a/src/pip/_internal/resolution/resolvelib/requirements.py +++ b/src/pip/_internal/resolution/resolvelib/requirements.py @@ -57,9 +57,6 @@ def find_matches(self): def is_satisfied_by(self, candidate): # type: (Candidate) -> bool - # TODO: Typing - Candidate doesn't have a link attribute - # But I think the following would be better... - # return candidate.link == self.candidate.link return candidate == self.candidate @@ -104,7 +101,6 @@ def find_matches(self): def is_satisfied_by(self, candidate): # type: (Candidate) -> bool - assert candidate.name == self.name, \ "Internal issue: Candidate is not for this requirement " \ " {} vs {}".format(candidate.name, self.name) From a83ea610d268d5a0684ad1a0ea7ac65be1161072 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sat, 14 Mar 2020 00:27:42 +0530 Subject: [PATCH 1388/3170] =?UTF-8?q?WIP:=20Add=20initial=20draft=20of=20c?= =?UTF-8?q?onfiguration=C2=A0deep=20dive?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../architecture/configuration-files.rst | 89 +++++++++++++++++++ docs/html/development/architecture/index.rst | 1 + 2 files changed, 90 insertions(+) create mode 100644 docs/html/development/architecture/configuration-files.rst diff --git a/docs/html/development/architecture/configuration-files.rst b/docs/html/development/architecture/configuration-files.rst new file mode 100644 index 00000000000..c25b25817cc --- /dev/null +++ b/docs/html/development/architecture/configuration-files.rst @@ -0,0 +1,89 @@ +=========================== +Configuration File Handling +=========================== + +The ``pip._internal.configuration`` module is responsible for handling +configuration files (eg. loading from and saving values to) that are used by +pip. The module's functionality is largely exposed through and coordinated by +the module's ``Configuration`` class. + +.. note:: + + This section of the documentation is currently being written. pip + developers welcome your help to complete this documentation. If you're + interested in helping out, please let us know in the + `tracking issue <https://github.com/pypa/pip/issues/6831>`_. + + +.. _configuration-overview: + +Overview +======== + +TODO: Figure out how to structure the initial part of this document. + +Loading +------- + +#. Determine configuration files to be used (built on top of :pypi:`appdirs`). +#. Load from all the configuration files. + #. For each file, construct a ``RawConfigParser`` instance and read the + file with it. Store the filename and parser for accessing / manipulating + the file's contents later. +#. Load values stored in ``PIP_*`` environment variables. + +The precedence of the various "configuration sources" is determined by +``Configuration._override_order``, and the precedence-respecting values are +lazily computed when values are accessed by a callee. + +Saving +------ + +Once the configuration is loaded, it is saved by iterating through all the +"modified parser" pairs (filename and associated parser, that were modified +in-memory after the initial load), and writing the state of the parser to file. + +----- + +The remainder of this section is organized by documenting some of the +implementation details of the ``configuration`` module, in the following order: + +* the :ref:`kinds <config-kinds>` enum, +* the :ref:`Configuration <configuration-class>` class, + + +.. _config-kinds: + +kinds +===== + +- used to represent "where" a configuration value comes from + (eg. environment variables, site-specific configuration file, + global configuration file) + +.. _configuration-class: + +Configuration +============ + +- TODO: API & usage - ``Command``, when processing CLI options. + - __init__() + - load() + - items() +- TODO: API & usage - ``pip config``, when loading / manipulating config files. + - __init__() + - get_file_to_edit() + - get_value() + - set_value() + - unset_value() + - save() +- TODO: nuances of ``load_only`` and ``get_file_to_edit`` +- TODO: nuances of ``isolated`` + +Future Refactoring Ideas +======================== + +* Break up the ``Configuration`` class into 2 smaller classes, by use case + * ``Command`` use-case (read only) -- ``ConfigurationReader`` + * ``pip config`` use-case (read / write) -- ``ConfigurationModifier`` (inherit from ``ConfigurationReader``) +* Eagerly populate ``Configuration._dictionary`` on load. diff --git a/docs/html/development/architecture/index.rst b/docs/html/development/architecture/index.rst index 094adeede1d..9c1eaa7cf57 100644 --- a/docs/html/development/architecture/index.rst +++ b/docs/html/development/architecture/index.rst @@ -20,6 +20,7 @@ Architecture of pip's internals overview anatomy + configuration-files package-finding upgrade-options From 96ea512de1fe1f4f25d0487a56245aaaaee3d08c Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sat, 21 Mar 2020 13:48:48 +0530 Subject: [PATCH 1389/3170] =?UTF-8?q?WIP:=20Add=20better=20overview=20for?= =?UTF-8?q?=20configuration=C2=A0deep=20dive?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../architecture/configuration-files.rst | 40 +++++++------------ 1 file changed, 14 insertions(+), 26 deletions(-) diff --git a/docs/html/development/architecture/configuration-files.rst b/docs/html/development/architecture/configuration-files.rst index c25b25817cc..15b0d3fec82 100644 --- a/docs/html/development/architecture/configuration-files.rst +++ b/docs/html/development/architecture/configuration-files.rst @@ -20,36 +20,24 @@ the module's ``Configuration`` class. Overview ======== -TODO: Figure out how to structure the initial part of this document. +pip stores configuration files in standard OS-appropriate locations, which are +determined by ``appdirs``. These files are in the INI format and are processed +with ``RawConfigParser``. -Loading -------- +pip uses configuration files in two operations: -#. Determine configuration files to be used (built on top of :pypi:`appdirs`). -#. Load from all the configuration files. - #. For each file, construct a ``RawConfigParser`` instance and read the - file with it. Store the filename and parser for accessing / manipulating - the file's contents later. -#. Load values stored in ``PIP_*`` environment variables. +- During processing of command line options. + - Reading from *all* configuration sources +- As part of ``pip config`` command. + - Reading from *all* configuration sources + - Manipulating a single configuration file -The precedence of the various "configuration sources" is determined by -``Configuration._override_order``, and the precedence-respecting values are -lazily computed when values are accessed by a callee. +Both of these operations utilize functionality provided the ``Configuration`` +object, which encapsulates all the logic for handling configuration files and +provides APIs for the same. -Saving ------- - -Once the configuration is loaded, it is saved by iterating through all the -"modified parser" pairs (filename and associated parser, that were modified -in-memory after the initial load), and writing the state of the parser to file. - ------ - -The remainder of this section is organized by documenting some of the -implementation details of the ``configuration`` module, in the following order: - -* the :ref:`kinds <config-kinds>` enum, -* the :ref:`Configuration <configuration-class>` class, +The remainder of this section documents the ``Configuration`` class and +other implementation details of the ``configuration`` module. .. _config-kinds: From 8eb33303fc18e17adfebe1b23b76eb295d9ec87a Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 23 Mar 2020 17:45:57 +0530 Subject: [PATCH 1390/3170] Document the two lifecycles of Configuration --- .../architecture/configuration-files.rst | 83 +++++++++++++------ 1 file changed, 59 insertions(+), 24 deletions(-) diff --git a/docs/html/development/architecture/configuration-files.rst b/docs/html/development/architecture/configuration-files.rst index 15b0d3fec82..99a4ce439fd 100644 --- a/docs/html/development/architecture/configuration-files.rst +++ b/docs/html/development/architecture/configuration-files.rst @@ -36,37 +36,72 @@ Both of these operations utilize functionality provided the ``Configuration`` object, which encapsulates all the logic for handling configuration files and provides APIs for the same. -The remainder of this section documents the ``Configuration`` class and -other implementation details of the ``configuration`` module. +The remainder of this section documents the ``Configuration`` class, and +discusses potential future refactoring ideas. +.. _configuration-class: + +``Configuration`` class +======================= + +``Configuration`` loads configuration values from sources in the local +environment: a combination of config files and environment variables. + +It can be used in two "modes", for reading all the values from the local +environment and for manipulating a single config file. It differentiates +between these two modes using the ``load_only`` attribute. + +The ``isolated`` attribute manipulates which sources are used when loading the +configuration. If ``isolated`` is ``True``, user-specific config files and +environment variables are not used. + +Reading from local environment +------------------------------ + +When using a ``Configuration`` object to read from all sources in the local +environment, the ``load_only`` attribute is ``None``. The API provided for this +use case is ``Configuration.load`` and ``Configuration.items``. + +``Configuration.load`` does all the interactions with the environment to load +all the configuration into objects in memory. ``Configuration.items`` +provides key-value pairs (like ``dict.items``) from the loaded-in-memory +information, handling all of the override ordering logic. + +At the time of writing, the only part of the codebase that uses +``Configuration`` like this is the ``ConfigOptionParser`` in the command line parsing +logic. + +Manipulating a single config file +--------------------------------- + +When using a ``Configuration`` object to read from a single config file, the +``load_only`` attribute would be non-None, and would represent the +:ref:`kind <config-kinds>` of the config file. + +This use case uses the methods discussed in the previous section +(``Configuration.load`` and ``Configuration.items``) and a few more that +are more specific to this use case. + +``Configuration.get_file_to_edit`` provides the "highest priority" file, for +the :ref:`kind <config-kinds>` of config file specified by ``load_only``. +The rest of this document will refer to this file as the "``load_only`` file". + +``Configuration.set_value`` provides a way to add/change a single key-value pair +in the ``load_only`` file. ``Configuration.unset_value`` removes a single +key-value pair in the ``load_only`` file. ``Configuration.get_value`` gets the +value of the given key from the loaded configuration. ``Configuration.save`` is +used save the state ``load_only`` file, back into the local environment. + .. _config-kinds: kinds ===== -- used to represent "where" a configuration value comes from - (eg. environment variables, site-specific configuration file, - global configuration file) - -.. _configuration-class: - -Configuration -============ - -- TODO: API & usage - ``Command``, when processing CLI options. - - __init__() - - load() - - items() -- TODO: API & usage - ``pip config``, when loading / manipulating config files. - - __init__() - - get_file_to_edit() - - get_value() - - set_value() - - unset_value() - - save() -- TODO: nuances of ``load_only`` and ``get_file_to_edit`` -- TODO: nuances of ``isolated`` +This is an enumeration that provides values to represent a "source" for +configuration. This includes environment variables and various types of +configuration files (global, site-specific, user_specific, specified via +``PIP_CONFIG_FILE``). Future Refactoring Ideas ======================== From e06671775000fdf4de0725f7e8a175efc6cde2e0 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Mon, 23 Mar 2020 21:11:27 +0800 Subject: [PATCH 1391/3170] Apply Pradyun's suggestion Co-Authored-By: Pradyun Gedam <pradyunsg@gmail.com> --- .travis.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4900e8a405e..b44b07b1e99 100644 --- a/.travis.yml +++ b/.travis.yml @@ -7,10 +7,6 @@ addons: packages: - bzr -env: - global: - - PIP_UNSTABLE_FEATURE= - stages: - primary - secondary @@ -68,7 +64,7 @@ jobs: fast_finish: true allow_failures: - - env: PIP_UNSTABLE_FEATURE=resolver + - stage: experimental before_install: tools/travis/setup.sh install: travis_retry tools/travis/install.sh From 5af542ccac7a4e6cc8988c93d793c54279e70869 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 24 Mar 2020 01:57:26 +0800 Subject: [PATCH 1392/3170] Use named arguments for clarity --- src/pip/_internal/resolution/resolvelib/resolver.py | 6 +++--- tests/unit/resolution_resolvelib/conftest.py | 6 +++++- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index cb40ab70b95..1dc408c9843 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -43,9 +43,9 @@ def __init__( def resolve(self, root_reqs, check_supported_wheels): # type: (List[InstallRequirement], bool) -> RequirementSet provider = PipProvider( - self.finder, - self.preparer, - self.make_install_req, + finder=self.finder, + preparer=self.preparer, + make_install_req=self.make_install_req, ) reporter = BaseReporter() resolver = RLResolver(provider, reporter) diff --git a/tests/unit/resolution_resolvelib/conftest.py b/tests/unit/resolution_resolvelib/conftest.py index 6ddc6422adf..e0892d771b2 100644 --- a/tests/unit/resolution_resolvelib/conftest.py +++ b/tests/unit/resolution_resolvelib/conftest.py @@ -55,4 +55,8 @@ def provider(finder, preparer): wheel_cache=None, use_pep517=None, ) - yield PipProvider(finder, preparer, make_install_req) + yield PipProvider( + finder=finder, + preparer=preparer, + make_install_req=make_install_req, + ) From 3cb7a08f0d0ffa8d4c058ab4a81a253736cfe467 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 24 Mar 2020 02:02:36 +0800 Subject: [PATCH 1393/3170] Implement ignore_dependencies in new resolver If this flag is set, simply report every candidate has no dependencies. --- src/pip/_internal/resolution/resolvelib/provider.py | 4 ++++ src/pip/_internal/resolution/resolvelib/resolver.py | 2 ++ tests/unit/resolution_resolvelib/conftest.py | 1 + 3 files changed, 7 insertions(+) diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index 981646ea96c..f57fcaf7856 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -20,11 +20,13 @@ def __init__( self, finder, # type: PackageFinder preparer, # type: RequirementPreparer + ignore_dependencies, # type: bool make_install_req # type: InstallRequirementProvider ): # type: (...) -> None self._finder = finder self._preparer = preparer + self._ignore_dependencies = ignore_dependencies self._make_install_req = make_install_req def make_requirement(self, ireq): @@ -72,6 +74,8 @@ def is_satisfied_by(self, requirement, candidate): def get_dependencies(self, candidate): # type: (Candidate) -> Sequence[Requirement] + if self._ignore_dependencies: + return [] return [ make_requirement( r, diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 1dc408c9843..f14bdaecf06 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -37,6 +37,7 @@ def __init__( super(Resolver, self).__init__() self.finder = finder self.preparer = preparer + self.ignore_dependencies = ignore_dependencies self.make_install_req = make_install_req self._result = None # type: Optional[Result] @@ -45,6 +46,7 @@ def resolve(self, root_reqs, check_supported_wheels): provider = PipProvider( finder=self.finder, preparer=self.preparer, + ignore_dependencies=self.ignore_dependencies, make_install_req=self.make_install_req, ) reporter = BaseReporter() diff --git a/tests/unit/resolution_resolvelib/conftest.py b/tests/unit/resolution_resolvelib/conftest.py index e0892d771b2..f4b979d1538 100644 --- a/tests/unit/resolution_resolvelib/conftest.py +++ b/tests/unit/resolution_resolvelib/conftest.py @@ -58,5 +58,6 @@ def provider(finder, preparer): yield PipProvider( finder=finder, preparer=preparer, + ignore_dependencies=False, make_install_req=make_install_req, ) From d43699b111499cf16e6f7090b05b7c31d0b32d83 Mon Sep 17 00:00:00 2001 From: Xavier Fernandez <xavier.fernandez@polyconseil.fr> Date: Wed, 26 Feb 2020 16:16:20 +0100 Subject: [PATCH 1394/3170] Move darwin special config dir to pip wrapper --- src/pip/_internal/utils/appdirs.py | 7 ++++++- src/pip/_vendor/appdirs.py | 4 ---- .../vendoring/patches/appdirs.patch | 21 +++++-------------- 3 files changed, 11 insertions(+), 21 deletions(-) diff --git a/src/pip/_internal/utils/appdirs.py b/src/pip/_internal/utils/appdirs.py index f80e4af45b5..3989ed31c3a 100644 --- a/src/pip/_internal/utils/appdirs.py +++ b/src/pip/_internal/utils/appdirs.py @@ -25,7 +25,12 @@ def user_cache_dir(appname): def user_config_dir(appname, roaming=True): # type: (str, bool) -> str - return _appdirs.user_config_dir(appname, appauthor=False, roaming=roaming) + path = _appdirs.user_config_dir(appname, appauthor=False, roaming=roaming) + if _appdirs.system == "darwin" and not os.path.isdir(path): + path = os.path.expanduser('~/.config/') + if appname: + path = os.path.join(path, appname) + return path # for the discussion regarding site_config_dir locations diff --git a/src/pip/_vendor/appdirs.py b/src/pip/_vendor/appdirs.py index cf37f9820c2..8bd9c9ca0b8 100644 --- a/src/pip/_vendor/appdirs.py +++ b/src/pip/_vendor/appdirs.py @@ -92,10 +92,6 @@ def user_data_dir(appname=None, appauthor=None, version=None, roaming=False): path = os.path.expanduser('~/Library/Application Support/') if appname: path = os.path.join(path, appname) - if not os.path.isdir(path): - path = os.path.expanduser('~/.config/') - if appname: - path = os.path.join(path, appname) else: path = os.getenv('XDG_DATA_HOME', os.path.expanduser("~/.local/share")) if appname: diff --git a/tools/automation/vendoring/patches/appdirs.patch b/tools/automation/vendoring/patches/appdirs.patch index fd3c200ac8a..69afd3e8681 100644 --- a/tools/automation/vendoring/patches/appdirs.patch +++ b/tools/automation/vendoring/patches/appdirs.patch @@ -22,18 +22,7 @@ index ae67001a..3a52b758 100644 Unix: ~/.local/share/<AppName> # or in $XDG_DATA_HOME, if defined Win XP (not roaming): C:\Documents and Settings\<username>\Application Data\<AppAuthor>\<AppName> Win XP (roaming): C:\Documents and Settings\<username>\Local Settings\Application Data\<AppAuthor>\<AppName> -@@ -88,6 +92,10 @@ def user_data_dir(appname=None, appauthor=None, version=None, roaming=False): - path = os.path.expanduser('~/Library/Application Support/') - if appname: - path = os.path.join(path, appname) -+ if not os.path.isdir(path): -+ path = os.path.expanduser('~/.config/') -+ if appname: -+ path = os.path.join(path, appname) - else: - path = os.getenv('XDG_DATA_HOME', os.path.expanduser("~/.local/share")) - if appname: -@@ -150,7 +158,7 @@ def site_data_dir(appname=None, appauthor=None, version=None, multipath=False): +@@ -150,7 +154,7 @@ def site_data_dir(appname=None, appauthor=None, version=None, multipath=False): if appname: if version: appname = os.path.join(appname, version) @@ -42,7 +31,7 @@ index ae67001a..3a52b758 100644 if multipath: path = os.pathsep.join(pathlist) -@@ -203,6 +211,8 @@ def user_config_dir(appname=None, appauthor=None, version=None, roaming=False): +@@ -203,6 +203,8 @@ def user_config_dir(appname=None, appauthor=None, version=None, roaming=False): return path @@ -51,7 +40,7 @@ index ae67001a..3a52b758 100644 def site_config_dir(appname=None, appauthor=None, version=None, multipath=False): r"""Return full path to the user-shared data dir for this application. -@@ -238,14 +248,15 @@ def site_config_dir(appname=None, appauthor=None, version=None, multipath=False) +@@ -238,14 +244,15 @@ def site_config_dir(appname=None, appauthor=None, version=None, multipath=False) if appname and version: path = os.path.join(path, version) else: @@ -71,7 +60,7 @@ index ae67001a..3a52b758 100644 if multipath: path = os.pathsep.join(pathlist) -@@ -291,6 +304,10 @@ def user_cache_dir(appname=None, appauthor=None, version=None, opinion=True): +@@ -291,6 +300,10 @@ def user_cache_dir(appname=None, appauthor=None, version=None, opinion=True): if appauthor is None: appauthor = appname path = os.path.normpath(_get_win_folder("CSIDL_LOCAL_APPDATA")) @@ -82,7 +71,7 @@ index ae67001a..3a52b758 100644 if appname: if appauthor is not False: path = os.path.join(path, appauthor, appname) -@@ -557,18 +574,32 @@ def _get_win_folder_with_jna(csidl_name): +@@ -557,18 +570,32 @@ def _get_win_folder_with_jna(csidl_name): if system == "win32": try: From 145c189a4998b52121c09ea669f7463dad03e154 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sat, 21 Mar 2020 16:22:32 +0530 Subject: [PATCH 1395/3170] Add a What Next page in getting started --- docs/html/development/getting-started.rst | 14 ++++++++++++++ news/533EA005-0471-4D5D-A81B-B6904A844EEE.trivial | 0 2 files changed, 14 insertions(+) create mode 100644 news/533EA005-0471-4D5D-A81B-B6904A844EEE.trivial diff --git a/docs/html/development/getting-started.rst b/docs/html/development/getting-started.rst index 6fa4f9edc54..5c699cd6188 100644 --- a/docs/html/development/getting-started.rst +++ b/docs/html/development/getting-started.rst @@ -123,8 +123,22 @@ To build it locally, run: The built documentation can be found in the ``docs/build`` folder. +What Next? +========== + +The following pages may be helpful for new contributors on where to look next +in order to start contributing. + +* Some `good first issues`_ on GitHub for new contributors +* A deep dive into `pip's architecture`_ +* A guide on `triaging issues`_ for issue tracker + + .. _`open an issue`: https://github.com/pypa/pip/issues/new?title=Trouble+with+pip+development+environment .. _`install Python`: https://realpython.com/installing-python/ .. _`PEP 484 type-comments`: https://www.python.org/dev/peps/pep-0484/#suggested-syntax-for-python-2-7-and-straddling-code .. _`rich CLI`: https://docs.pytest.org/en/latest/usage.html#specifying-tests-selecting-tests .. _`GitHub`: https://github.com/pypa/pip +.. _`good first issues`: https://github.com/pypa/pip/labels/good%20first%20issue +.. _`pip's architecture`: https://pip.pypa.io/en/latest/development/architecture/ +.. _`triaging issues`: https://pip.pypa.io/en/latest/development/issue-triage/ diff --git a/news/533EA005-0471-4D5D-A81B-B6904A844EEE.trivial b/news/533EA005-0471-4D5D-A81B-B6904A844EEE.trivial new file mode 100644 index 00000000000..e69de29bb2d From 9229de985879ad32ed8429d84564fc275022fc7d Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 24 Mar 2020 15:43:34 +0530 Subject: [PATCH 1396/3170] Switch to an API-describing format --- .../architecture/configuration-files.rst | 93 +++++++++++++------ docs/html/user_guide.rst | 2 + 2 files changed, 65 insertions(+), 30 deletions(-) diff --git a/docs/html/development/architecture/configuration-files.rst b/docs/html/development/architecture/configuration-files.rst index 99a4ce439fd..aa0c1a52c5b 100644 --- a/docs/html/development/architecture/configuration-files.rst +++ b/docs/html/development/architecture/configuration-files.rst @@ -46,52 +46,85 @@ discusses potential future refactoring ideas. ======================= ``Configuration`` loads configuration values from sources in the local -environment: a combination of config files and environment variables. +environment: a combination of configuration files and environment variables. It can be used in two "modes", for reading all the values from the local -environment and for manipulating a single config file. It differentiates +environment and for manipulating a single configuration file. It differentiates between these two modes using the ``load_only`` attribute. The ``isolated`` attribute manipulates which sources are used when loading the -configuration. If ``isolated`` is ``True``, user-specific config files and -environment variables are not used. +configuration. If ``isolated`` is ``True``, user-specific configuration files +and environment variables are not used. Reading from local environment ------------------------------ -When using a ``Configuration`` object to read from all sources in the local -environment, the ``load_only`` attribute is ``None``. The API provided for this -use case is ``Configuration.load`` and ``Configuration.items``. +``Configuration`` can be used to read from all configuration sources in the +local environment and access the values, as per the precedence logic described +in the :ref:`Config Precedence <config-precedence>` section. -``Configuration.load`` does all the interactions with the environment to load -all the configuration into objects in memory. ``Configuration.items`` -provides key-value pairs (like ``dict.items``) from the loaded-in-memory -information, handling all of the override ordering logic. +For this use case, the ``Configuration.load_only`` attribute would be ``None``, +and the methods used would be: -At the time of writing, the only part of the codebase that uses -``Configuration`` like this is the ``ConfigOptionParser`` in the command line parsing -logic. +.. py:class:: Configuration -Manipulating a single config file ---------------------------------- + .. py:method:: load() -When using a ``Configuration`` object to read from a single config file, the -``load_only`` attribute would be non-None, and would represent the -:ref:`kind <config-kinds>` of the config file. + Handles all the interactions with the environment, to load all the + configuration data into objects in memory. -This use case uses the methods discussed in the previous section -(``Configuration.load`` and ``Configuration.items``) and a few more that -are more specific to this use case. + .. py:method:: items() -``Configuration.get_file_to_edit`` provides the "highest priority" file, for -the :ref:`kind <config-kinds>` of config file specified by ``load_only``. -The rest of this document will refer to this file as the "``load_only`` file". + Provides key-value pairs (like ``dict.items()``) from the loaded-in-memory + information, handling all of the override ordering logic. -``Configuration.set_value`` provides a way to add/change a single key-value pair -in the ``load_only`` file. ``Configuration.unset_value`` removes a single -key-value pair in the ``load_only`` file. ``Configuration.get_value`` gets the -value of the given key from the loaded configuration. ``Configuration.save`` is -used save the state ``load_only`` file, back into the local environment. +At the time of writing, the parts of the codebase that use ``Configuration`` +in this manner are: ``ConfigOptionParser``, to transparently include +configuration handling as part of the command line processing logic, +and ``pip config get``, for printing the entire configuration when no +:ref:`kind <config-kinds>` is specified via the CLI. + +Manipulating a single configuration file +---------------------------------------- + +``Configuration`` can be used to manipulate a single configuration file, +such as to add, change or remove certain key-value pairs. + +For this use case, the ``load_only`` attribute would be non-None, and would +represent the :ref:`kind <config-kinds>` of the configuration file to be +manipulated. In addition to the methods discussed in the previous section, +the methods used would be: + +.. py:class:: Configuration + + .. py:method:: get_value(key) + + Provides the value of the given key from the loaded configuration. + The loaded configuration may have ``load_only`` be None or non-None. + This uses the same underlying mechanism as ``Configuration.items()`` and + does follow the precedence logic described in :ref:`Config Precedence + <config-precedence>`. + + .. py:method:: get_file_to_edit() + + Provides the "highest priority" file, for the :ref:`kind <config-kinds>` of + configuration file specified by ``load_only``. This requires ``load_only`` + to be non-None. + + .. py:method:: set_value(key, value) + + Provides a way to add/change a single key-value pair, in the file specified + by ``Configuration.get_file_to_edit()``. + + .. py:method:: unset_value(key) + + Provides a way to remove a single key-value pair, in the file specified + by ``Configuration.get_file_to_edit()``. + + .. py:method:: save() + + Saves the in-memory state of to the original files, saving any modifications + made to the ``Configuration`` object back into the local environment. .. _config-kinds: diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index bc843627380..ae8b295bc7e 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -484,6 +484,8 @@ is the same as calling:: Please use ``no``, ``false`` or ``0`` instead. +.. _config-precedence: + Config Precedence ----------------- From 4264d5e0d30b969d1da61b1834019be7fc0f9cbe Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Tue, 24 Mar 2020 17:02:46 +0530 Subject: [PATCH 1397/3170] Don't fail uninstallation if easy-install.pth doesn't exist --- news/7856.bugfix | 0 src/pip/_internal/req/req_uninstall.py | 13 ++++++++----- 2 files changed, 8 insertions(+), 5 deletions(-) create mode 100644 news/7856.bugfix diff --git a/news/7856.bugfix b/news/7856.bugfix new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/req/req_uninstall.py b/src/pip/_internal/req/req_uninstall.py index ec9da2c3484..559061a6296 100644 --- a/src/pip/_internal/req/req_uninstall.py +++ b/src/pip/_internal/req/req_uninstall.py @@ -585,11 +585,6 @@ def from_dist(cls, dist): class UninstallPthEntries(object): def __init__(self, pth_file): # type: (str) -> None - if not os.path.isfile(pth_file): - raise UninstallationError( - "Cannot remove entries from nonexistent file {}".format( - pth_file) - ) self.file = pth_file self.entries = set() # type: Set[str] self._saved_lines = None # type: Optional[List[bytes]] @@ -613,6 +608,14 @@ def add(self, entry): def remove(self): # type: () -> None logger.debug('Removing pth entries from %s:', self.file) + + # If the file doesn't exist, log a warning and return + if not os.path.isfile(self.file): + logger.warning( + "Cannot remove entries from nonexistent file {}".format( + self.file) + ) + return with open(self.file, 'rb') as fh: # windows uses '\r\n' with py3k, but uses '\n' with py2.x lines = fh.readlines() From 88e4abd66ff6fe065c6aad408e636a0e22526f60 Mon Sep 17 00:00:00 2001 From: onlinejudge95 <onlinejudge95> Date: Tue, 24 Mar 2020 19:27:44 +0530 Subject: [PATCH 1398/3170] Removed news file --- news/7683.doc | 1 - 1 file changed, 1 deletion(-) delete mode 100644 news/7683.doc diff --git a/news/7683.doc b/news/7683.doc deleted file mode 100644 index 921a8093316..00000000000 --- a/news/7683.doc +++ /dev/null @@ -1 +0,0 @@ -Rephrases test execution command From 4d0a55ee6f7d7458a4f8e5688782f3a907c76057 Mon Sep 17 00:00:00 2001 From: onlinejudge95 <onlinejudge95> Date: Tue, 24 Mar 2020 19:32:37 +0530 Subject: [PATCH 1399/3170] Address minor comments --- docs/html/development/getting-started.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/html/development/getting-started.rst b/docs/html/development/getting-started.rst index fe081c54f1d..db34a0c54aa 100644 --- a/docs/html/development/getting-started.rst +++ b/docs/html/development/getting-started.rst @@ -51,12 +51,13 @@ pip's tests. It is preferable to run the tests in parallel for better experience during development, since the tests can take a long time to finish when run sequentially. -To run tests locally, run: + +To run tests: .. code-block:: console $ tox -e py36 -- -n auto -For running tests sequentially remove the ``-n`` flag. +To run tests without parallelization, run: .. code-block:: console $ tox -e py36 From 653bac26c9cc9c957dc7bbd0176f6201215c84da Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Fri, 20 Mar 2020 13:32:20 +0000 Subject: [PATCH 1400/3170] Add a test to validate if extras are respected --- tests/functional/test_new_resolver.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index cd7443dd6ad..d812a6e025b 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -81,3 +81,24 @@ def test_new_resolver_installs_dependencies(script): "base" ) assert_installed(script, base="0.1.0", dep="0.1.0") + + +def test_new_resolver_installs_extras(script): + create_basic_wheel_for_package( + script, + "base", + "0.1.0", + extras={"add": ["dep"]}, + ) + create_basic_wheel_for_package( + script, + "dep", + "0.1.0", + ) + script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "base[add]" + ) + assert_installed(script, base="0.1.0", dep="0.1.0") From 76de49bc24d538bea914346ab8ed53930cdeefe6 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Wed, 25 Mar 2020 11:41:33 +0000 Subject: [PATCH 1401/3170] Make a proper get_install_requirement method for candidates --- src/pip/_internal/resolution/resolvelib/base.py | 4 ++++ src/pip/_internal/resolution/resolvelib/candidates.py | 4 ++++ src/pip/_internal/resolution/resolvelib/provider.py | 10 +--------- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/base.py b/src/pip/_internal/resolution/resolvelib/base.py index 5e9f041b5b2..f7484658914 100644 --- a/src/pip/_internal/resolution/resolvelib/base.py +++ b/src/pip/_internal/resolution/resolvelib/base.py @@ -46,3 +46,7 @@ def version(self): def get_dependencies(self): # type: () -> Sequence[InstallRequirement] raise NotImplementedError("Override in subclass") + + def get_install_requirement(self): + # type: () -> InstallRequirement + raise NotImplementedError("Override in subclass") diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 08eff0ddf9d..667d6fa0cec 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -124,3 +124,7 @@ def get_dependencies(self): self._make_install_req(str(r), self._ireq) for r in self.dist.requires() ] + + def get_install_requirement(self): + # type: () -> InstallRequirement + return self._ireq diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index 981646ea96c..b50abdd1f87 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -38,15 +38,7 @@ def make_requirement(self, ireq): def get_install_requirement(self, c): # type: (Candidate) -> InstallRequirement - - # The base Candidate class does not have an _ireq attribute, so we - # fetch it dynamically here, to satisfy mypy. In practice, though, we - # only ever deal with LinkedCandidate objects at the moment, which do - # have an _ireq attribute. When we have a candidate type for installed - # requirements we should probably review this. - # - # TODO: Longer term, make a proper interface for this on the candidate. - return getattr(c, "_ireq", None) + return c.get_install_requirement() def identify(self, dependency): # type: (Union[Requirement, Candidate]) -> str From bd0f7fe3467a03d6f9cf8f20a1b79b06ce0defd9 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Wed, 25 Mar 2020 12:25:23 +0000 Subject: [PATCH 1402/3170] Add an ExtrasCandidate class --- .../resolution/resolvelib/candidates.py | 65 +++++++++++++++---- .../resolution/resolvelib/requirements.py | 12 ++-- 2 files changed, 59 insertions(+), 18 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 667d6fa0cec..60baf18d7d8 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -4,10 +4,10 @@ from pip._internal.req.req_install import InstallRequirement from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from .base import Candidate +from .base import Candidate, format_name if MYPY_CHECK_RUNNING: - from typing import Any, Dict, Optional, Sequence + from typing import Any, Dict, Optional, Sequence, Set from pip._internal.models.link import Link from pip._internal.operations.prepare import RequirementPreparer @@ -17,14 +17,15 @@ from pip._vendor.pkg_resources import Distribution -_CANDIDATE_CACHE = {} # type: Dict[Link, Candidate] +_CANDIDATE_CACHE = {} # type: Dict[Link, LinkCandidate] def make_candidate( - link, # type: Link - preparer, # type: RequirementPreparer - parent, # type: InstallRequirement - make_install_req # type: InstallRequirementProvider + link, # type: Link + preparer, # type: RequirementPreparer + parent, # type: InstallRequirement + make_install_req, # type: InstallRequirementProvider + extras # type: Set[str] ): # type: (...) -> Candidate if link not in _CANDIDATE_CACHE: @@ -34,7 +35,10 @@ def make_candidate( parent=parent, make_install_req=make_install_req ) - return _CANDIDATE_CACHE[link] + base = _CANDIDATE_CACHE[link] + if extras: + return ExtrasCandidate(base, extras) + return base def make_install_req_from_link(link, parent): @@ -67,7 +71,7 @@ def __init__( self.link = link self._preparer = preparer self._ireq = make_install_req_from_link(link, parent) - self._make_install_req = make_install_req + self._make_install_req = lambda spec: make_install_req(spec, self._ireq) self._name = None # type: Optional[str] self._version = None # type: Optional[_BaseVersion] @@ -120,11 +124,46 @@ def dist(self): def get_dependencies(self): # type: () -> Sequence[InstallRequirement] - return [ - self._make_install_req(str(r), self._ireq) - for r in self.dist.requires() - ] + return [self._make_install_req(str(r)) for r in self.dist.requires()] def get_install_requirement(self): # type: () -> InstallRequirement return self._ireq + + +class ExtrasCandidate(LinkCandidate): + def __init__( + self, + base, # type: LinkCandidate + extras, # type: Set[str] + ): + # type: (...) -> None + self.base = base + self.extras = extras + self.link = base.link + + @property + def name(self): + # type: () -> str + """The normalised name of the project the candidate refers to""" + return format_name(self.base.name, self.extras) + + @property + def version(self): + # type: () -> _BaseVersion + return self.base.version + + def get_dependencies(self): + # type: () -> Sequence[InstallRequirement] + deps = [ + self.base._make_install_req(str(r)) + for r in self.base.dist.requires(self.extras) + ] + # Add a dependency on the exact base + spec = "{}=={}".format(self.base.name, self.base.version) + deps.append(self.base._make_install_req(spec)) + return deps + + def get_install_requirement(self): + # type: () -> InstallRequirement + return self.base.get_install_requirement() diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py index 1a5f257a574..e9ba610080f 100644 --- a/src/pip/_internal/resolution/resolvelib/requirements.py +++ b/src/pip/_internal/resolution/resolvelib/requirements.py @@ -2,7 +2,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from .base import Requirement +from .base import Requirement, format_name from .candidates import make_candidate if MYPY_CHECK_RUNNING: @@ -28,7 +28,8 @@ def make_requirement( ireq.link, preparer, ireq, - make_install_req + make_install_req, + set() ) return ExplicitRequirement(candidate) else: @@ -70,17 +71,17 @@ def __init__( ): # type: (...) -> None assert ireq.link is None, "This is a link, not a specifier" - assert not ireq.req.extras, "Extras not yet supported" self._ireq = ireq self._finder = finder self._preparer = preparer self._make_install_req = make_install_req + self.extras = ireq.req.extras @property def name(self): # type: () -> str canonical_name = canonicalize_name(self._ireq.req.name) - return canonical_name + return format_name(canonical_name, self.extras) def find_matches(self): # type: () -> Sequence[Candidate] @@ -94,7 +95,8 @@ def find_matches(self): ican.link, self._preparer, self._ireq, - self._make_install_req + self._make_install_req, + self.extras ) for ican in found.iter_applicable() ] From d79aacc61c3c70bebde578145b40cfdbb609ac2a Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Wed, 25 Mar 2020 12:39:21 +0000 Subject: [PATCH 1403/3170] Forgot to run lint before pushing again :-( --- src/pip/_internal/resolution/resolvelib/candidates.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 60baf18d7d8..c2e28bb5d12 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -71,7 +71,10 @@ def __init__( self.link = link self._preparer = preparer self._ireq = make_install_req_from_link(link, parent) - self._make_install_req = lambda spec: make_install_req(spec, self._ireq) + self._make_install_req = lambda spec: make_install_req( + spec, + self._ireq + ) self._name = None # type: Optional[str] self._version = None # type: Optional[_BaseVersion] From 1a210d1c62877a60a9308272c1dab39e95ed7125 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 26 Mar 2020 01:47:22 +0800 Subject: [PATCH 1404/3170] Improve utility to test installed env Make assert_installed actually check the provided entries against the pip --list output. Add assert_not_installed to check for the reverse. --- tests/functional/test_new_resolver.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index cd7443dd6ad..4e8c0f04122 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -9,7 +9,15 @@ def assert_installed(script, **kwargs): (val['name'], val['version']) for val in json.loads(ret.stdout) ) - assert set(kwargs.items()) <= installed + assert all(item in installed for item in kwargs.items()), \ + "{!r} not all in {!r}".format(kwargs, installed) + + +def assert_not_installed(script, *args): + ret = script.pip("list", "--format=json") + installed = set(val["name"] for val in json.loads(ret.stdout)) + assert all(a not in installed for a in args), \ + "{!r} contained in {!r}".format(args, installed) def test_new_resolver_can_install(script): From 6d3a89c9924d324a6c262d18d4b935963fab8d79 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 26 Mar 2020 01:48:47 +0800 Subject: [PATCH 1405/3170] Add --no-deps test for the new resolver --- tests/functional/test_new_resolver.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 4e8c0f04122..ed91a60f2a6 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -89,3 +89,25 @@ def test_new_resolver_installs_dependencies(script): "base" ) assert_installed(script, base="0.1.0", dep="0.1.0") + + +def test_new_resolver_ignore_dependencies(script): + create_basic_wheel_for_package( + script, + "base", + "0.1.0", + depends=["dep"], + ) + create_basic_wheel_for_package( + script, + "dep", + "0.1.0", + ) + script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", "--no-deps", + "--find-links", script.scratch_path, + "base" + ) + assert_installed(script, base="0.1.0") + assert_not_installed(script, "dep") From 30d1870cc78efb1e9143784bd38f945cc4baaf2c Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Wed, 25 Mar 2020 22:55:57 +0530 Subject: [PATCH 1406/3170] Only use names in AUTHORS.txt --- AUTHORS.txt | 1102 +++++++++++++------------- news/5979.removal | 1 + tools/automation/release/__init__.py | 3 +- 3 files changed, 543 insertions(+), 563 deletions(-) create mode 100644 news/5979.removal diff --git a/AUTHORS.txt b/AUTHORS.txt index 72c87d7d38a..ac424168adc 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -1,562 +1,540 @@ -A_Rog <adam.thomas.rogerson@gmail.com> -Aakanksha Agrawal <11389424+rasponic@users.noreply.github.com> -Abhinav Sagar <40603139+abhinavsagar@users.noreply.github.com> -ABHYUDAY PRATAP SINGH <abhyudaypratap@outlook.com> -abs51295 <aagams68@gmail.com> -AceGentile <ventogrigio83@gmail.com> -Adam Chainz <adam@adamj.eu> -Adam Tse <adam.tse@me.com> -Adam Tse <atse@users.noreply.github.com> -Adam Wentz <awentz@theonion.com> -admin <admin@admins-MacBook-Pro.local> -Adrien Morison <adrien.morison@gmail.com> -ahayrapetyan <ahayrapetya2@bloomberg.net> -Ahilya <ahilya16009@iiitd.ac.in> -AinsworthK <yat626@yahoo.com.hk> -Akash Srivastava <akashsrivastava4927@gmail.com> -Alan Yee <alyee@ucsd.edu> -Albert Tugushev <albert@tugushev.ru> -Albert-Guan <albert.guan94@gmail.com> -albertg <albert.guan94@gmail.com> -Aleks Bunin <github@compuix.com> -Alethea Flowers <magicalgirl@google.com> -Alex Gaynor <alex.gaynor@gmail.com> -Alex Grönholm <alex.gronholm@nextday.fi> -Alex Loosley <a.loosley@reply.de> -Alex Morega <alex@grep.ro> -Alex Stachowiak <alexander@computer.org> -Alexander Shtyrov <rawzausho@gmail.com> -Alexandre Conrad <alexandre.conrad@gmail.com> -Alexey Popravka <a.popravka@smartweb.com.ua> -Alexey Popravka <alexey.popravka@horsedevel.com> -Alli <alzeih@users.noreply.github.com> -Ami Fischman <ami@fischman.org> -Ananya Maiti <ananyoevo@gmail.com> -Anatoly Techtonik <techtonik@gmail.com> -Anders Kaseorg <andersk@mit.edu> -Andreas Lutro <anlutro@gmail.com> -Andrei Geacar <andrei.geacar@gmail.com> -Andrew Gaul <andrew@gaul.org> -Andrey Bulgakov <mail@andreiko.ru> -Andrés Delfino <34587441+andresdelfino@users.noreply.github.com> -Andrés Delfino <adelfino@gmail.com> -Andy Freeland <andy.freeland@redjack.com> -Andy Freeland <andy@andyfreeland.net> -Andy Kluger <AndydeCleyre@users.noreply.github.com> -Ani Hayrapetyan <ahayrapetya2@bloomberg.net> -Aniruddha Basak <codewithaniruddha@gmail.com> -Anish Tambe <anish.tambe@yahoo.in> -Anrs Hu <anrs@douban.com> -Anthony Sottile <asottile@umich.edu> -Antoine Musso <hashar@free.fr> -Anton Ovchinnikov <revolver112@gmail.com> -Anton Patrushev <apatrushev@gmail.com> -Antonio Alvarado Hernandez <tnotstar@gmail.com> -Antony Lee <anntzer.lee@gmail.com> -Antti Kaihola <akaihol+github@ambitone.com> -Anubhav Patel <anubhavp28@gmail.com> -Anuj Godase <godaseanuj@gmail.com> -AQNOUCH Mohammed <aqnouch.mohammed@gmail.com> -AraHaan <seandhunt_7@yahoo.com> -Arindam Choudhury <arindam@live.com> -Armin Ronacher <armin.ronacher@active-4.com> -Artem <duketemon@users.noreply.github.com> -Ashley Manton <ajd.manton@googlemail.com> -Ashwin Ramaswami <aramaswamis@gmail.com> -atse <atse@users.noreply.github.com> -Atsushi Odagiri <aodagx@gmail.com> -Avner Cohen <israbirding@gmail.com> -Baptiste Mispelon <bmispelon@gmail.com> -Barney Gale <barney.gale@gmail.com> -barneygale <barney.gale@gmail.com> -Bartek Ogryczak <b.ogryczak@gmail.com> -Bastian Venthur <mail@venthur.de> -Ben Darnell <ben@bendarnell.com> -Ben Hoyt <benhoyt@gmail.com> -Ben Rosser <rosser.bjr@gmail.com> -Bence Nagy <bence@underyx.me> -Benjamin Peterson <benjamin@python.org> -Benjamin VanEvery <ben@simondata.com> -Benoit Pierre <benoit.pierre@gmail.com> -Berker Peksag <berker.peksag@gmail.com> -Bernardo B. Marques <bernardo.fire@gmail.com> -Bernhard M. Wiedemann <bwiedemann@suse.de> -Bertil Hatt <bertil.hatt@farfetch.com> -Bogdan Opanchuk <bogdan@opanchuk.net> -BorisZZZ <BorisZZZ@users.noreply.github.com> -Brad Erickson <eosrei@gmail.com> -Bradley Ayers <bradley.ayers@gmail.com> -Brandon L. Reiss <brandon@damyata.co> -Brandt Bucher <brandtbucher@gmail.com> -Brett Randall <javabrett@gmail.com> -Brian Cristante <33549821+brcrista@users.noreply.github.com> -Brian Cristante <brcrista@microsoft.com> -Brian Rosner <brosner@gmail.com> -BrownTruck <BrownTruck@users.noreply.github.com> -Bruno Oliveira <nicoddemus@gmail.com> -Bruno Renié <brutasse@gmail.com> -Bstrdsmkr <bstrdsmkr@gmail.com> -Buck Golemon <buck@yelp.com> -burrows <burrows@preveil.com> -Bussonnier Matthias <bussonniermatthias@gmail.com> -c22 <c22@users.noreply.github.com> -Caleb Martinez <accounts@calebmartinez.com> -Calvin Smith <eukaryote@users.noreply.github.com> -Carl Meyer <carl@oddbird.net> -Carlos Liam <carlos@aarzee.me> -Carol Willing <carolcode@willingconsulting.com> -Carter Thayer <carterwthayer@gmail.com> -Cass <cass.petrus@gmail.com> -Chandrasekhar Atina <chandu.atina@gmail.com> -Chih-Hsuan Yen <yan12125@gmail.com> -Chih-Hsuan Yen <yen@chyen.cc> -Chris Brinker <chris.brinker@gmail.com> -Chris Hunt <chrahunt@gmail.com> -Chris Jerdonek <chris.jerdonek@gmail.com> -Chris McDonough <chrism@plope.com> -Chris Wolfe <chriswwolfe@gmail.com> -Christian Heimes <christian@python.org> -Christian Oudard <christian.oudard@gmail.com> -Christopher Hunt <chrahunt@gmail.com> -Christopher Snyder <cnsnyder@users.noreply.github.com> -Clark Boylan <clark.boylan@gmail.com> -Clay McClure <clay@daemons.net> -Cody <Purring@users.noreply.github.com> -Cody Soyland <codysoyland@gmail.com> -Colin Watson <cjwatson@debian.org> -Connor Osborn <cdosborn@email.arizona.edu> -Cooper Lees <me@cooperlees.com> -Cooper Ry Lees <me@cooperlees.com> -Cory Benfield <lukasaoz@gmail.com> -Cory Wright <corywright@gmail.com> -Craig Kerstiens <craig.kerstiens@gmail.com> -Cristian Sorinel <cristian.sorinel@gmail.com> -Curtis Doty <Curtis@GreenKey.net> -cytolentino <ctolentino8@bloomberg.net> -Damian Quiroga <qdamian@gmail.com> -Dan Black <dyspop@gmail.com> -Dan Savilonis <djs@n-cube.org> -Dan Sully <daniel-github@electricrain.com> -daniel <mcdonaldd@unimelb.edu.au> -Daniel Collins <accounts@dac.io> -Daniel Hahler <git@thequod.de> -Daniel Holth <dholth@fastmail.fm> -Daniel Jost <torpedojost@gmail.com> -Daniel Shaulov <daniel.shaulov@gmail.com> -Daniele Esposti <expobrain@users.noreply.github.com> -Daniele Procida <daniele@vurt.org> -Danny Hermes <daniel.j.hermes@gmail.com> -Dav Clark <davclark@gmail.com> -Dave Abrahams <dave@boostpro.com> -Dave Jones <dave@waveform.org.uk> -David Aguilar <davvid@gmail.com> -David Black <db@d1b.org> -David Bordeynik <david.bordeynik@gmail.com> -David Bordeynik <david@zebra-med.com> -David Caro <david@dcaro.es> -David Evans <d@drhevans.com> -David Linke <dr.david.linke@gmail.com> -David Pursehouse <david.pursehouse@gmail.com> -David Tucker <david@tucker.name> -David Wales <daviewales@gmail.com> -Davidovich <david.genest@gmail.com> -derwolfe <chriswwolfe@gmail.com> -Desetude <harry@desetude.com> -Diego Caraballo <diegocaraballo84@gmail.com> -DiegoCaraballo <diegocaraballo84@gmail.com> -Dmitry Gladkov <dmitry.gladkov@gmail.com> -Domen Kožar <domen@dev.si> -Donald Stufft <donald@stufft.io> -Dongweiming <dongweiming@admaster.com.cn> -Douglas Thor <dougthor42@users.noreply.github.com> -DrFeathers <WilliamGeorgeBurgess@gmail.com> -Dustin Ingram <di@di.codes> -Dwayne Bailey <dwayne@translate.org.za> -Ed Morley <501702+edmorley@users.noreply.github.com> -Ed Morley <emorley@mozilla.com> -Eitan Adler <lists@eitanadler.com> -ekristina <panacejja@gmail.com> -elainechan <elaine.chan@outlook.com> -Eli Schwartz <eschwartz93@gmail.com> -Eli Schwartz <eschwartz@archlinux.org> -Emil Burzo <contact@emilburzo.com> -Emil Styrke <emil.styrke@gmail.com> -Endoh Takanao <djmchl@gmail.com> -enoch <lanxenet@gmail.com> -Erdinc Mutlu <erdinc_mutlu@yahoo.com> -Eric Gillingham <Gillingham@bikezen.net> -Eric Hanchrow <eric.hanchrow@gmail.com> -Eric Hopper <hopper@omnifarious.org> -Erik M. Bray <embray@stsci.edu> -Erik Rose <erik@mozilla.com> -Ernest W Durbin III <ewdurbin@gmail.com> -Ernest W. Durbin III <ewdurbin@gmail.com> -Erwin Janssen <erwinjanssen@outlook.com> -Eugene Vereshchagin <evvers@gmail.com> -everdimension <everdimension@gmail.com> -Felix Yan <felixonmars@archlinux.org> -fiber-space <fiber-space@users.noreply.github.com> -Filip Kokosiński <filip.kokosinski@gmail.com> -Florian Briand <ownerfrance+github@hotmail.com> -Florian Rathgeber <florian.rathgeber@gmail.com> -Francesco <f.guerrieri@gmail.com> -Francesco Montesano <franz.bergesund@gmail.com> -Frost Ming <mianghong@gmail.com> -Gabriel Curio <g.curio@gmail.com> -Gabriel de Perthuis <g2p.code@gmail.com> -Garry Polley <garrympolley@gmail.com> -gdanielson <graeme.danielson@gmail.com> -Geoffrey Lehée <geoffrey@lehee.name> -Geoffrey Sneddon <me@gsnedders.com> -George Song <george@55minutes.com> -Georgi Valkov <georgi.t.valkov@gmail.com> -Giftlin Rajaiah <giftlin.rgn@gmail.com> -gizmoguy1 <gizmoguy1@gmail.com> -gkdoc <40815324+gkdoc@users.noreply.github.com> -Gopinath M <31352222+mgopi1990@users.noreply.github.com> -GOTO Hayato <3532528+gh640@users.noreply.github.com> -gpiks <gaurav.pikale@gmail.com> -Guilherme Espada <porcariadagata@gmail.com> -Guy Rozendorn <guy@rzn.co.il> -gzpan123 <gzpan123@gmail.com> -Hanjun Kim <hallazzang@gmail.com> -Hari Charan <hcharan997@gmail.com> -Harsh Vardhan <harsh59v@gmail.com> -Herbert Pfennig <herbert@albinen.com> -Hsiaoming Yang <lepture@me.com> -Hugo <hugovk@users.noreply.github.com> -Hugo Lopes Tavares <hltbra@gmail.com> -Hugo van Kemenade <hugovk@users.noreply.github.com> -hugovk <hugovk@users.noreply.github.com> -Hynek Schlawack <hs@ox.cx> -Ian Bicking <ianb@colorstudy.com> -Ian Cordasco <graffatcolmingov@gmail.com> -Ian Lee <IanLee1521@gmail.com> -Ian Stapleton Cordasco <graffatcolmingov@gmail.com> -Ian Wienand <ian@wienand.org> -Ian Wienand <iwienand@redhat.com> -Igor Kuzmitshov <kuzmiigo@gmail.com> -Igor Sobreira <igor@igorsobreira.com> -Ilya Baryshev <baryshev@gmail.com> -INADA Naoki <songofacandy@gmail.com> -Ionel Cristian Mărieș <contact@ionelmc.ro> -Ionel Maries Cristian <ionel.mc@gmail.com> -Ivan Pozdeev <vano@mail.mipt.ru> -Jacob Kim <me@thejacobkim.com> -jakirkham <jakirkham@gmail.com> -Jakub Stasiak <kuba.stasiak@gmail.com> -Jakub Vysoky <jakub@borka.cz> -Jakub Wilk <jwilk@jwilk.net> -James Cleveland <jamescleveland@gmail.com> -James Cleveland <radiosilence@users.noreply.github.com> -James Firth <hello@james-firth.com> -James Polley <jp@jamezpolley.com> -Jan Pokorný <jpokorny@redhat.com> -Jannis Leidel <jannis@leidel.info> -jarondl <me@jarondl.net> -Jason R. Coombs <jaraco@jaraco.com> -Jay Graves <jay@skabber.com> -Jean-Christophe Fillion-Robin <jchris.fillionr@kitware.com> -Jeff Barber <jbarber@computer.org> -Jeff Dairiki <dairiki@dairiki.org> -Jelmer Vernooij <jelmer@jelmer.uk> -jenix21 <devfrog@gmail.com> -Jeremy Stanley <fungi@yuggoth.org> -Jeremy Zafran <jzafran@users.noreply.github.com> -Jiashuo Li <jiasli@microsoft.com> -Jim Garrison <jim@garrison.cc> -Jivan Amara <Development@JivanAmara.net> -John Paton <j.paton@catawiki.nl> -John-Scott Atlakson <john.scott.atlakson@gmail.com> -johnthagen <johnthagen@gmail.com> -johnthagen <johnthagen@users.noreply.github.com> -Jon Banafato <jon@jonafato.com> -Jon Dufresne <jon.dufresne@gmail.com> -Jon Parise <jon@indelible.org> -Jonas Nockert <jonasnockert@gmail.com> -Jonathan Herbert <foohyfooh@gmail.com> -Joost Molenaar <j.j.molenaar@gmail.com> -Jorge Niedbalski <niedbalski@gmail.com> -Joseph Long <jdl@fastmail.fm> -Josh Bronson <jabronson@gmail.com> -Josh Hansen <josh@skwash.net> -Josh Schneier <josh.schneier@gmail.com> -Juanjo Bazán <jjbazan@gmail.com> -Julian Berman <Julian@GrayVines.com> -Julian Gethmann <julian.gethmann@kit.edu> -Julien Demoor <julien@jdemoor.com> -jwg4 <jack.grahl@yahoo.co.uk> -Jyrki Pulliainen <jyrki@spotify.com> -Kai Chen <kaichen120@gmail.com> -Kamal Bin Mustafa <kamal@smach.net> -kaustav haldar <hi@kaustav.me> -keanemind <keanemind@gmail.com> -Keith Maxwell <keith.maxwell@gmail.com> -Kelsey Hightower <kelsey.hightower@gmail.com> -Kenneth Belitzky <kenny@belitzky.com> -Kenneth Reitz <me@kennethreitz.com> -Kenneth Reitz <me@kennethreitz.org> -Kevin Burke <kev@inburke.com> -Kevin Carter <kevin.carter@rackspace.com> -Kevin Frommelt <kevin.frommelt@webfilings.com> -Kevin R Patterson <kevin.r.patterson@intel.com> -Kexuan Sun <me@kianasun.com> -Kit Randel <kit@nocturne.net.nz> -kpinc <kop@meme.com> -Krishna Oza <krishoza15sep@gmail.com> -Kumar McMillan <kumar.mcmillan@gmail.com> -Kyle Persohn <kyle.persohn@gmail.com> -lakshmanaram <lakshmanaram.n@gmail.com> -Laszlo Kiss-Kollar <kiss.kollar.laszlo@gmail.com> -Laurent Bristiel <laurent@bristiel.com> -Laurie Opperman <laurie@sitesee.com.au> -Leon Sasson <leonsassonha@gmail.com> -Lev Givon <lev@columbia.edu> -Lincoln de Sousa <lincoln@comum.org> -Lipis <lipiridis@gmail.com> -Loren Carvalho <lcarvalho@linkedin.com> -Lucas Cimon <lucas.cimon@gmail.com> -Ludovic Gasc <gmludo@gmail.com> -Luke Macken <lmacken@redhat.com> -Luo Jiebin <luo.jiebin@qq.com> -luojiebin <luojiebin@users.noreply.github.com> -luz.paz <luzpaz@users.noreply.github.com> -László Kiss Kollár <lkisskollar@bloomberg.net> -László Kiss Kollár <lkollar@users.noreply.github.com> -Marc Abramowitz <marc@marc-abramowitz.com> -Marc Tamlyn <marc.tamlyn@gmail.com> -Marcus Smith <qwcode@gmail.com> -Mariatta <Mariatta@users.noreply.github.com> -Mark Kohler <mark.kohler@proteinsimple.com> -Mark Williams <markrwilliams@gmail.com> -Mark Williams <mrw@enotuniq.org> -Markus Hametner <fin+github@xbhd.org> -Masaki <mk5986@nyu.edu> -Masklinn <bitbucket.org@masklinn.net> -Matej Stuchlik <mstuchli@redhat.com> -Mathew Jennings <mjennings@foursquare.com> -Mathieu Bridon <bochecha@daitauha.fr> -Matt Good <matt@matt-good.net> -Matt Maker <trip@monstro.us> -Matt Robenolt <matt@ydekproductions.com> -matthew <matthew@trumbell.net> -Matthew Einhorn <moiein2000@gmail.com> -Matthew Gilliard <matthew.gilliard@gmail.com> -Matthew Iversen <teh.ivo@gmail.com> -Matthew Trumbell <matthew@thirdstonepartners.com> -Matthew Willson <matthew@swiftkey.com> -Matthias Bussonnier <bussonniermatthias@gmail.com> -mattip <matti.picus@gmail.com> -Maxim Kurnikov <maxim.kurnikov@gmail.com> -Maxime Rouyrre <rouyrre+git@gmail.com> -mayeut <mayeut@users.noreply.github.com> -mbaluna <44498973+mbaluna@users.noreply.github.com> -mdebi <17590103+mdebi@users.noreply.github.com> -memoselyk <memoselyk@gmail.com> -Michael <michael-k@users.noreply.github.com> -Michael Aquilina <michaelaquilina@gmail.com> -Michael E. Karpeles <michael.karpeles@gmail.com> -Michael Klich <michal@michalklich.com> -Michael Williamson <mike@zwobble.org> -michaelpacer <michaelpacer@gmail.com> -Mickaël Schoentgen <mschoentgen@nuxeo.com> -Miguel Araujo Perez <miguel.araujo.perez@gmail.com> -Mihir Singh <git.service@mihirsingh.com> -Mike <mikeh@blur.com> -Mike Hendricks <mikeh@blur.com> -Min RK <benjaminrk@gmail.com> -MinRK <benjaminrk@gmail.com> -Miro Hrončok <miro@hroncok.cz> -Monica Baluna <mbaluna@bloomberg.net> -montefra <franz.bergesund@gmail.com> -Monty Taylor <mordred@inaugust.com> -Nate Coraor <nate@bx.psu.edu> -Nathaniel J. Smith <njs@pobox.com> -Nehal J Wani <nehaljw.kkd1@gmail.com> -Neil Botelho <neil.botelho321@gmail.com> -Nick Coghlan <ncoghlan@gmail.com> -Nick Stenning <nick@whiteink.com> -Nick Timkovich <prometheus235@gmail.com> -Nicolas Bock <nicolasbock@gmail.com> -Nikhil Benesch <nikhil.benesch@gmail.com> -Nitesh Sharma <nbsharma@outlook.com> -Nowell Strite <nowell@strite.org> -NtaleGrey <Shadikntale@gmail.com> -nvdv <modestdev@gmail.com> -Ofekmeister <ofekmeister@gmail.com> -ofrinevo <ofrine@gmail.com> -Oliver Jeeves <oliver.jeeves@ocado.com> -Oliver Tonnhofer <olt@bogosoft.com> -Olivier Girardot <ssaboum@gmail.com> -Olivier Grisel <olivier.grisel@ensta.org> -Ollie Rutherfurd <orutherfurd@gmail.com> -OMOTO Kenji <k-omoto@m3.com> -Omry Yadan <omry@fb.com> -Oren Held <orenhe@il.ibm.com> -Oscar Benjamin <oscar.j.benjamin@gmail.com> -Oz N Tiram <oz.tiram@gmail.com> -Pachwenko <32424503+Pachwenko@users.noreply.github.com> -Patrick Dubroy <pdubroy@gmail.com> -Patrick Jenkins <patrick@socialgrowthtechnologies.com> -Patrick Lawson <pl@foursquare.com> -patricktokeeffe <patricktokeeffe@users.noreply.github.com> -Patrik Kopkan <pkopkan@redhat.com> -Paul Kehrer <paul.l.kehrer@gmail.com> -Paul Moore <p.f.moore@gmail.com> -Paul Nasrat <pnasrat@gmail.com> -Paul Oswald <pauloswald@gmail.com> -Paul van der Linden <mail@paultjuh.org> -Paulus Schoutsen <paulus@paulusschoutsen.nl> -Pavithra Eswaramoorthy <33131404+QueenCoffee@users.noreply.github.com> -Pawel Jasinski <pawel.jasinski@gmail.com> -Pekka Klärck <peke@iki.fi> -Peter Lisák <peter.lisak@showmax.com> -Peter Waller <peter.waller@gmail.com> -petr-tik <petr-tik@users.noreply.github.com> -Phaneendra Chiruvella <hi@pcx.io> -Phil Freo <phil@philfreo.com> -Phil Pennock <phil@pennock-tech.com> -Phil Whelan <phil123@gmail.com> -Philip Jägenstedt <philip@foolip.org> -Philip Molloy <pamolloy@users.noreply.github.com> -Philippe Ombredanne <pombredanne@gmail.com> -Pi Delport <pjdelport@gmail.com> -Pierre-Yves Rofes <github@rofes.fr> -pip <pypa-dev@googlegroups.com> -Prabakaran Kumaresshan <k_prabakaran+github@hotmail.com> -Prabhjyotsing Surjit Singh Sodhi <psinghsodhi@bloomberg.net> -Prabhu Marappan <prabhum.794@gmail.com> -Pradyun Gedam <pradyunsg@gmail.com> -Pratik Mallya <mallya@us.ibm.com> -Preet Thakkar <preet.thakkar@students.iiit.ac.in> -Preston Holmes <preston@ptone.com> -Przemek Wrzos <hetmankp@none> -Pulkit Goyal <7895pulkit@gmail.com> -Qiangning Hong <hongqn@gmail.com> -Quentin Pradet <quentin.pradet@gmail.com> -R. David Murray <rdmurray@bitdance.com> -Rafael Caricio <rafael.jacinto@gmail.com> -Ralf Schmitt <ralf@systemexit.de> -Razzi Abuissa <razzi53@gmail.com> -rdb <rdb@users.noreply.github.com> -Remi Rampin <r@remirampin.com> -Remi Rampin <remirampin@gmail.com> -Rene Dudfield <renesd@gmail.com> -Riccardo Magliocchetti <riccardo.magliocchetti@gmail.com> -Richard Jones <r1chardj0n3s@gmail.com> -RobberPhex <robberphex@gmail.com> -Robert Collins <rbtcollins@hp.com> -Robert McGibbon <rmcgibbo@gmail.com> -Robert T. McGibbon <rmcgibbo@gmail.com> -robin elisha robinson <elisha.rob@gmail.com> -Roey Berman <roey.berman@gmail.com> -Rohan Jain <crodjer@gmail.com> -Rohan Jain <crodjer@users.noreply.github.com> -Rohan Jain <mail@rohanjain.in> -Roman Bogorodskiy <roman.bogorodskiy@ericsson.com> -Romuald Brunet <romuald@chivil.com> -Ronny Pfannschmidt <Ronny.Pfannschmidt@gmx.de> -Rory McCann <rory@technomancy.org> -Ross Brattain <ross.b.brattain@intel.com> -Roy Wellington Ⅳ <cactus_hugged@yahoo.com> -Roy Wellington Ⅳ <roy@mybasis.com> -Ryan Wooden <rygwdn@gmail.com> -ryneeverett <ryneeverett@gmail.com> -Sachi King <nakato@nakato.io> -Salvatore Rinchiera <salvatore@rinchiera.com> -Savio Jomton <sajo240519@gmail.com> -schlamar <marc.schlaich@gmail.com> -Scott Kitterman <sklist@kitterman.com> -Sean <me@sean.taipei> -seanj <seanj@xyke.com> -Sebastian Jordan <sebastian.jordan.mail@googlemail.com> -Sebastian Schaetz <sschaetz@butterflynetinc.com> -Segev Finer <segev208@gmail.com> -SeongSoo Cho <ppiyakk2@printf.kr> -Sergey Vasilyev <nolar@nolar.info> -Seth Woodworth <seth@sethish.com> -Shlomi Fish <shlomif@shlomifish.org> -Shovan Maity <shovan.maity@mayadata.io> -Simeon Visser <svisser@users.noreply.github.com> -Simon Cross <hodgestar@gmail.com> -Simon Pichugin <simon.pichugin@gmail.com> -sinoroc <sinoroc.code+git@gmail.com> -Sorin Sbarnea <sorin.sbarnea@gmail.com> -Stavros Korokithakis <stavros@korokithakis.net> -Stefan Scherfke <stefan@sofa-rockers.org> -Stephan Erb <github@stephanerb.eu> -stepshal <nessento@openmailbox.org> -Steve (Gadget) Barnes <gadgetsteve@hotmail.com> -Steve Barnes <gadgetsteve@hotmail.com> -Steve Dower <steve.dower@microsoft.com> -Steve Kowalik <steven@wedontsleep.org> -Steven Myint <git@stevenmyint.com> -stonebig <stonebig34@gmail.com> -Stéphane Bidoul (ACSONE) <stephane.bidoul@acsone.eu> -Stéphane Bidoul <stephane.bidoul@acsone.eu> -Stéphane Klein <contact@stephane-klein.info> -Sumana Harihareswara <sh@changeset.nyc> -Sviatoslav Sydorenko <wk.cvs.github@sydorenko.org.ua> -Sviatoslav Sydorenko <wk@sydorenko.org.ua> -Swat009 <swatantra.kumar8@gmail.com> -Takayuki SHIMIZUKAWA <shimizukawa@gmail.com> -tbeswick <tbeswick@enphaseenergy.com> -Thijs Triemstra <info@collab.nl> -Thomas Fenzl <thomas.fenzl@gmail.com> -Thomas Grainger <tagrain@gmail.com> -Thomas Guettler <tguettler@tbz-pariv.de> -Thomas Johansson <devnull@localhost> -Thomas Kluyver <thomas@kluyver.me.uk> -Thomas Smith <smithtg@ncbi.nlm.nih.gov> -Tim D. Smith <github@tim-smith.us> -Tim Gates <tim.gates@iress.com> -Tim Harder <radhermit@gmail.com> -Tim Heap <tim@timheap.me> -tim smith <github@tim-smith.us> -tinruufu <tinruufu@gmail.com> -Tom Forbes <tom@tomforb.es> -Tom Freudenheim <tom.freudenheim@onepeloton.com> -Tom V <tom@viner.tv> -Tomas Orsava <torsava@redhat.com> -Tomer Chachamu <tomer.chachamu@gmail.com> -Tony Beswick <tonybeswick@orcon.net.nz> -Tony Zhaocheng Tan <tony@tonytan.io> -TonyBeswick <TonyBeswick@users.noreply.github.com> -toonarmycaptain <toonarmycaptain@hotmail.com> -Toshio Kuratomi <toshio@fedoraproject.org> -Travis Swicegood <development@domain51.com> -Tzu-ping Chung <uranusjr@gmail.com> -Valentin Haenel <valentin.haenel@gmx.de> -Victor Stinner <victor.stinner@gmail.com> -victorvpaulo <victorvpaulo@gmail.com> -Viktor Szépe <viktor@szepe.net> -Ville Skyttä <ville.skytta@iki.fi> -Vinay Sajip <vinay_sajip@yahoo.co.uk> -Vincent Philippon <sindaewoh@gmail.com> -Vinicyus Macedo <7549205+vinicyusmacedo@users.noreply.github.com> -Vitaly Babiy <vbabiy86@gmail.com> -Vladimir Rutsky <rutsky@users.noreply.github.com> -W. Trevor King <wking@drexel.edu> -Wil Tan <wil@dready.org> -Wilfred Hughes <me@wilfred.me.uk> -William ML Leslie <william.leslie.ttg@gmail.com> -William T Olson <trevor@heytrevor.com> -Wilson Mo <wilsonfv@126.com> -wim glenn <wim.glenn@gmail.com> -Wolfgang Maier <wolfgang.maier@biologie.uni-freiburg.de> -Xavier Fernandez <xav.fernandez@gmail.com> -Xavier Fernandez <xavier.fernandez@polyconseil.fr> -xoviat <xoviat@users.noreply.github.com> -xtreak <tir.karthi@gmail.com> -YAMAMOTO Takashi <yamamoto@midokura.com> -Yen Chi Hsuan <yan12125@gmail.com> -Yeray Diaz Diaz <yeraydiazdiaz@gmail.com> -Yoval P <yoval@gmx.com> -Yu Jian <askingyj@gmail.com> -Yuan Jing Vincent Yan <yyan82@bloomberg.net> -Zearin <zearin@gonk.net> -Zearin <Zearin@users.noreply.github.com> -Zhiping Deng <kofreestyler@gmail.com> -Zvezdan Petkovic <zpetkovic@acm.org> -Łukasz Langa <lukasz@langa.pl> -Семён Марьясин <simeon@maryasin.name> +A_Rog +Aakanksha Agrawal +Abhinav Sagar +ABHYUDAY PRATAP SINGH +abs51295 +AceGentile +Adam Chainz +Adam Tse +Adam Wentz +admin +Adrien Morison +ahayrapetyan +Ahilya +AinsworthK +Akash Srivastava +Alan Yee +Albert Tugushev +Albert-Guan +albertg +Aleks Bunin +Alethea Flowers +Alex Gaynor +Alex Grönholm +Alex Loosley +Alex Morega +Alex Stachowiak +Alexander Shtyrov +Alexandre Conrad +Alexey Popravka +Alli +Ami Fischman +Ananya Maiti +Anatoly Techtonik +Anders Kaseorg +Andreas Lutro +Andrei Geacar +Andrew Gaul +Andrey Bulgakov +Andrés Delfino +Andy Freeland +Andy Kluger +Ani Hayrapetyan +Aniruddha Basak +Anish Tambe +Anrs Hu +Anthony Sottile +Antoine Musso +Anton Ovchinnikov +Anton Patrushev +Antonio Alvarado Hernandez +Antony Lee +Antti Kaihola +Anubhav Patel +Anuj Godase +AQNOUCH Mohammed +AraHaan +Arindam Choudhury +Armin Ronacher +Artem +Ashley Manton +Ashwin Ramaswami +atse +Atsushi Odagiri +Avner Cohen +Baptiste Mispelon +Barney Gale +barneygale +Bartek Ogryczak +Bastian Venthur +Ben Darnell +Ben Hoyt +Ben Rosser +Bence Nagy +Benjamin Peterson +Benjamin VanEvery +Benoit Pierre +Berker Peksag +Bernardo B. Marques +Bernhard M. Wiedemann +Bertil Hatt +Bogdan Opanchuk +BorisZZZ +Brad Erickson +Bradley Ayers +Brandon L. Reiss +Brandt Bucher +Brett Randall +Brian Cristante +Brian Rosner +BrownTruck +Bruno Oliveira +Bruno Renié +Bstrdsmkr +Buck Golemon +burrows +Bussonnier Matthias +c22 +Caleb Martinez +Calvin Smith +Carl Meyer +Carlos Liam +Carol Willing +Carter Thayer +Cass +Chandrasekhar Atina +Chih-Hsuan Yen +Chris Brinker +Chris Hunt +Chris Jerdonek +Chris McDonough +Chris Wolfe +Christian Heimes +Christian Oudard +Christopher Hunt +Christopher Snyder +Clark Boylan +Clay McClure +Cody +Cody Soyland +Colin Watson +Connor Osborn +Cooper Lees +Cooper Ry Lees +Cory Benfield +Cory Wright +Craig Kerstiens +Cristian Sorinel +Curtis Doty +cytolentino +Damian Quiroga +Dan Black +Dan Savilonis +Dan Sully +daniel +Daniel Collins +Daniel Hahler +Daniel Holth +Daniel Jost +Daniel Shaulov +Daniele Esposti +Daniele Procida +Danny Hermes +Dav Clark +Dave Abrahams +Dave Jones +David Aguilar +David Black +David Bordeynik +David Caro +David Evans +David Linke +David Pursehouse +David Tucker +David Wales +Davidovich +derwolfe +Desetude +Diego Caraballo +DiegoCaraballo +Dmitry Gladkov +Domen Kožar +Donald Stufft +Dongweiming +Douglas Thor +DrFeathers +Dustin Ingram +Dwayne Bailey +Ed Morley +Eitan Adler +ekristina +elainechan +Eli Schwartz +Emil Burzo +Emil Styrke +Endoh Takanao +enoch +Erdinc Mutlu +Eric Gillingham +Eric Hanchrow +Eric Hopper +Erik M. Bray +Erik Rose +Ernest W Durbin III +Ernest W. Durbin III +Erwin Janssen +Eugene Vereshchagin +everdimension +Felix Yan +fiber-space +Filip Kokosiński +Florian Briand +Florian Rathgeber +Francesco +Francesco Montesano +Frost Ming +Gabriel Curio +Gabriel de Perthuis +Garry Polley +gdanielson +Geoffrey Lehée +Geoffrey Sneddon +George Song +Georgi Valkov +Giftlin Rajaiah +gizmoguy1 +gkdoc +Gopinath M +GOTO Hayato +gpiks +Guilherme Espada +Guy Rozendorn +gzpan123 +Hanjun Kim +Hari Charan +Harsh Vardhan +Herbert Pfennig +Hsiaoming Yang +Hugo +Hugo Lopes Tavares +Hugo van Kemenade +hugovk +Hynek Schlawack +Ian Bicking +Ian Cordasco +Ian Lee +Ian Stapleton Cordasco +Ian Wienand +Igor Kuzmitshov +Igor Sobreira +Ilya Baryshev +INADA Naoki +Ionel Cristian Mărieș +Ionel Maries Cristian +Ivan Pozdeev +Jacob Kim +jakirkham +Jakub Stasiak +Jakub Vysoky +Jakub Wilk +James Cleveland +James Firth +James Polley +Jan Pokorný +Jannis Leidel +jarondl +Jason R. Coombs +Jay Graves +Jean-Christophe Fillion-Robin +Jeff Barber +Jeff Dairiki +Jelmer Vernooij +jenix21 +Jeremy Stanley +Jeremy Zafran +Jiashuo Li +Jim Garrison +Jivan Amara +John Paton +John-Scott Atlakson +johnthagen +Jon Banafato +Jon Dufresne +Jon Parise +Jonas Nockert +Jonathan Herbert +Joost Molenaar +Jorge Niedbalski +Joseph Long +Josh Bronson +Josh Hansen +Josh Schneier +Juanjo Bazán +Julian Berman +Julian Gethmann +Julien Demoor +jwg4 +Jyrki Pulliainen +Kai Chen +Kamal Bin Mustafa +kaustav haldar +keanemind +Keith Maxwell +Kelsey Hightower +Kenneth Belitzky +Kenneth Reitz +Kevin Burke +Kevin Carter +Kevin Frommelt +Kevin R Patterson +Kexuan Sun +Kit Randel +kpinc +Krishna Oza +Kumar McMillan +Kyle Persohn +lakshmanaram +Laszlo Kiss-Kollar +Laurent Bristiel +Laurie Opperman +Leon Sasson +Lev Givon +Lincoln de Sousa +Lipis +Loren Carvalho +Lucas Cimon +Ludovic Gasc +Luke Macken +Luo Jiebin +luojiebin +luz.paz +László Kiss Kollár +Marc Abramowitz +Marc Tamlyn +Marcus Smith +Mariatta +Mark Kohler +Mark Williams +Markus Hametner +Masaki +Masklinn +Matej Stuchlik +Mathew Jennings +Mathieu Bridon +Matt Good +Matt Maker +Matt Robenolt +matthew +Matthew Einhorn +Matthew Gilliard +Matthew Iversen +Matthew Trumbell +Matthew Willson +Matthias Bussonnier +mattip +Maxim Kurnikov +Maxime Rouyrre +mayeut +mbaluna +mdebi +memoselyk +Michael +Michael Aquilina +Michael E. Karpeles +Michael Klich +Michael Williamson +michaelpacer +Mickaël Schoentgen +Miguel Araujo Perez +Mihir Singh +Mike +Mike Hendricks +Min RK +MinRK +Miro Hrončok +Monica Baluna +montefra +Monty Taylor +Nate Coraor +Nathaniel J. Smith +Nehal J Wani +Neil Botelho +Nick Coghlan +Nick Stenning +Nick Timkovich +Nicolas Bock +Nikhil Benesch +Nitesh Sharma +Nowell Strite +NtaleGrey +nvdv +Ofekmeister +ofrinevo +Oliver Jeeves +Oliver Tonnhofer +Olivier Girardot +Olivier Grisel +Ollie Rutherfurd +OMOTO Kenji +Omry Yadan +Oren Held +Oscar Benjamin +Oz N Tiram +Pachwenko +Patrick Dubroy +Patrick Jenkins +Patrick Lawson +patricktokeeffe +Patrik Kopkan +Paul Kehrer +Paul Moore +Paul Nasrat +Paul Oswald +Paul van der Linden +Paulus Schoutsen +Pavithra Eswaramoorthy +Pawel Jasinski +Pekka Klärck +Peter Lisák +Peter Waller +petr-tik +Phaneendra Chiruvella +Phil Freo +Phil Pennock +Phil Whelan +Philip Jägenstedt +Philip Molloy +Philippe Ombredanne +Pi Delport +Pierre-Yves Rofes +pip +Prabakaran Kumaresshan +Prabhjyotsing Surjit Singh Sodhi +Prabhu Marappan +Pradyun Gedam +Pratik Mallya +Preet Thakkar +Preston Holmes +Przemek Wrzos +Pulkit Goyal +Qiangning Hong +Quentin Pradet +R. David Murray +Rafael Caricio +Ralf Schmitt +Razzi Abuissa +rdb +Remi Rampin +Rene Dudfield +Riccardo Magliocchetti +Richard Jones +RobberPhex +Robert Collins +Robert McGibbon +Robert T. McGibbon +robin elisha robinson +Roey Berman +Rohan Jain +Roman Bogorodskiy +Romuald Brunet +Ronny Pfannschmidt +Rory McCann +Ross Brattain +Roy Wellington Ⅳ +Ryan Wooden +ryneeverett +Sachi King +Salvatore Rinchiera +Savio Jomton +schlamar +Scott Kitterman +Sean +seanj +Sebastian Jordan +Sebastian Schaetz +Segev Finer +SeongSoo Cho +Sergey Vasilyev +Seth Woodworth +Shlomi Fish +Shovan Maity +Simeon Visser +Simon Cross +Simon Pichugin +sinoroc +Sorin Sbarnea +Stavros Korokithakis +Stefan Scherfke +Stephan Erb +stepshal +Steve (Gadget) Barnes +Steve Barnes +Steve Dower +Steve Kowalik +Steven Myint +stonebig +Stéphane Bidoul +Stéphane Bidoul (ACSONE) +Stéphane Klein +Sumana Harihareswara +Sviatoslav Sydorenko +Swat009 +Takayuki SHIMIZUKAWA +tbeswick +Thijs Triemstra +Thomas Fenzl +Thomas Grainger +Thomas Guettler +Thomas Johansson +Thomas Kluyver +Thomas Smith +Tim D. Smith +Tim Gates +Tim Harder +Tim Heap +tim smith +tinruufu +Tom Forbes +Tom Freudenheim +Tom V +Tomas Orsava +Tomer Chachamu +Tony Beswick +Tony Zhaocheng Tan +TonyBeswick +toonarmycaptain +Toshio Kuratomi +Travis Swicegood +Tzu-ping Chung +Valentin Haenel +Victor Stinner +victorvpaulo +Viktor Szépe +Ville Skyttä +Vinay Sajip +Vincent Philippon +Vinicyus Macedo +Vitaly Babiy +Vladimir Rutsky +W. Trevor King +Wil Tan +Wilfred Hughes +William ML Leslie +William T Olson +Wilson Mo +wim glenn +Wolfgang Maier +Xavier Fernandez +xoviat +xtreak +YAMAMOTO Takashi +Yen Chi Hsuan +Yeray Diaz Diaz +Yoval P +Yu Jian +Yuan Jing Vincent Yan +Zearin +Zhiping Deng +Zvezdan Petkovic +Łukasz Langa +Семён Марьясин diff --git a/news/5979.removal b/news/5979.removal new file mode 100644 index 00000000000..9791a1e50b6 --- /dev/null +++ b/news/5979.removal @@ -0,0 +1 @@ +Remove emails from AUTHORS.txt to prevent usage for spamming, and only populate names in AUTHORS.txt at time of release diff --git a/tools/automation/release/__init__.py b/tools/automation/release/__init__.py index c82133c7439..a6138686289 100644 --- a/tools/automation/release/__init__.py +++ b/tools/automation/release/__init__.py @@ -49,8 +49,9 @@ def get_author_list() -> List[str]: """Get the list of authors from Git commits. """ # subprocess because session.run doesn't give us stdout + # only use names in list of Authors result = subprocess.run( - ["git", "log", "--use-mailmap", "--format=%aN <%aE>"], + ["git", "log", "--use-mailmap", "--format=%aN"], capture_output=True, encoding="utf-8", ) From c42b5d37162b828013e9dde6f022c85a56bd3b90 Mon Sep 17 00:00:00 2001 From: "John T. Wodder II" <git@varonathe.org> Date: Wed, 25 Mar 2020 23:53:31 -0400 Subject: [PATCH 1407/3170] Change docs link from PEP 301 to PEP 503 --- docs/html/reference/pip_install.rst | 2 +- news/2808D551-576D-4239-BBB4-F5B9DB5E36A2.trivial | 0 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 news/2808D551-576D-4239-BBB4-F5B9DB5E36A2.trivial diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index c81e43ba49e..bd61151d712 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -506,7 +506,7 @@ Finding Packages pip searches for packages on `PyPI`_ using the `HTTP simple interface <https://pypi.org/simple/>`_, which is documented `here <https://setuptools.readthedocs.io/en/latest/easy_install.html#package-index-api>`_ -and `there <https://www.python.org/dev/peps/pep-0301/>`_. +and `there <https://www.python.org/dev/peps/pep-0503/>`_. pip offers a number of package index options for modifying how packages are found. diff --git a/news/2808D551-576D-4239-BBB4-F5B9DB5E36A2.trivial b/news/2808D551-576D-4239-BBB4-F5B9DB5E36A2.trivial new file mode 100644 index 00000000000..e69de29bb2d From ffb56db5dde64f5a40c0b81e718b1f1efcb7e1df Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Thu, 26 Mar 2020 11:19:10 +0000 Subject: [PATCH 1408/3170] Allow candidates to not have an associated install requirement --- src/pip/_internal/resolution/resolvelib/base.py | 4 ++-- src/pip/_internal/resolution/resolvelib/candidates.py | 9 ++++++--- src/pip/_internal/resolution/resolvelib/provider.py | 2 +- src/pip/_internal/resolution/resolvelib/resolver.py | 3 ++- 4 files changed, 11 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/base.py b/src/pip/_internal/resolution/resolvelib/base.py index f7484658914..de3299496a5 100644 --- a/src/pip/_internal/resolution/resolvelib/base.py +++ b/src/pip/_internal/resolution/resolvelib/base.py @@ -3,7 +3,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Sequence, Set + from typing import Optional, Sequence, Set from pip._internal.req.req_install import InstallRequirement from pip._vendor.packaging.version import _BaseVersion @@ -48,5 +48,5 @@ def get_dependencies(self): raise NotImplementedError("Override in subclass") def get_install_requirement(self): - # type: () -> InstallRequirement + # type: () -> Optional[InstallRequirement] raise NotImplementedError("Override in subclass") diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index c2e28bb5d12..49c193383da 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -130,7 +130,7 @@ def get_dependencies(self): return [self._make_install_req(str(r)) for r in self.dist.requires()] def get_install_requirement(self): - # type: () -> InstallRequirement + # type: () -> Optional[InstallRequirement] return self._ireq @@ -168,5 +168,8 @@ def get_dependencies(self): return deps def get_install_requirement(self): - # type: () -> InstallRequirement - return self.base.get_install_requirement() + # type: () -> Optional[InstallRequirement] + # We don't return anything here, because we always + # depend on the base candidate, and we'll get the + # install requirement from that. + return None diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index b50abdd1f87..7ce924f65ff 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -37,7 +37,7 @@ def make_requirement(self, ireq): ) def get_install_requirement(self, c): - # type: (Candidate) -> InstallRequirement + # type: (Candidate) -> Optional[InstallRequirement] return c.get_install_requirement() def identify(self, dependency): diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index cb40ab70b95..4a38096a009 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -56,7 +56,8 @@ def resolve(self, root_reqs, check_supported_wheels): req_set = RequirementSet(check_supported_wheels=check_supported_wheels) for candidate in self._result.mapping.values(): ireq = provider.get_install_requirement(candidate) - req_set.add_named_requirement(ireq) + if ireq is not None: + req_set.add_named_requirement(ireq) return req_set From 67bf5890eaab77c00b5579efd71be0725ad6654d Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Thu, 26 Mar 2020 11:19:55 +0000 Subject: [PATCH 1409/3170] Document the ExtrasCandidate class --- .../resolution/resolvelib/candidates.py | 26 ++++++++++++++++++- 1 file changed, 25 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 49c193383da..3f63e556f06 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -135,6 +135,29 @@ def get_install_requirement(self): class ExtrasCandidate(LinkCandidate): + """A candidate that has 'extras', indicating additional dependencies. + + Requirements can be for a project with dependencies, something like + foo[extra]. The extras don't affect the project/version being installed + directly, but indicate that we need additional dependencies. We model that + by having an artificial ExtrasCandidate that wraps the "base" candidate. + + The ExtrasCandidate differs from the base in the following ways: + + 1. It has a unique name, of the form foo[extra]. This causes the resolver + to treat it as a separate node in the dependency graph. + 2. When we're getting the candidate's dependencies, + a) We specify that we want the extra dependencies as well. + b) We add a dependency on the base candidate (matching the name and + version). See below for why this is needed. + 3. We return None for the underlying InstallRequirement, as the base + candidate will provide it, and we don't want to end up with duplicates. + + The dependency on the base candidate is needed so that the resolver can't + decide that it should recommend foo[extra1] version 1.0 and foo[extra2] + version 2.0. Having those candidates depend on foo=1.0 and foo=2.0 + respectively forces the resolver to recognise that this is a conflict. + """ def __init__( self, base, # type: LinkCandidate @@ -162,7 +185,8 @@ def get_dependencies(self): self.base._make_install_req(str(r)) for r in self.base.dist.requires(self.extras) ] - # Add a dependency on the exact base + # Add a dependency on the exact base. + # (See note 2b in the class docstring) spec = "{}=={}".format(self.base.name, self.base.version) deps.append(self.base._make_install_req(spec)) return deps From 7e97cf64267fbfbaba3efde11844f6a7d891dfdf Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Thu, 26 Mar 2020 11:57:34 +0000 Subject: [PATCH 1410/3170] Ignore invalid extras --- .../resolution/resolvelib/candidates.py | 16 +++++++++++++++- tests/functional/test_new_resolver.py | 2 +- 2 files changed, 16 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 3f63e556f06..2c5108212db 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -181,9 +181,23 @@ def version(self): def get_dependencies(self): # type: () -> Sequence[InstallRequirement] + # TODO: We should probably warn if the user specifies an unsupported + # extra. We can't do this in the constructor, as we don't know what + # extras are valid until we prepare the candidate. Probably the best + # approach would be to override the base class ``dist`` property, to + # do an additional check of the extras, and if any are invalid, warn + # and remove them from extras. This will be tricky to get right, + # though, as modifying the extras changes the candidate's name and + # hence identity, which isn't acceptable. So for now, we just ignore + # unsupported extras here. + + # The user may have specified extras that the candidate doesn't + # support. We ignore any unsupported extras here. + valid_extras = self.extras.intersection(self.base.dist.extras) + deps = [ self.base._make_install_req(str(r)) - for r in self.base.dist.requires(self.extras) + for r in self.base.dist.requires(valid_extras) ] # Add a dependency on the exact base. # (See note 2b in the class docstring) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index d812a6e025b..35edef59d3b 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -99,6 +99,6 @@ def test_new_resolver_installs_extras(script): "install", "--unstable-feature=resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, - "base[add]" + "base[add,missing]" ) assert_installed(script, base="0.1.0", dep="0.1.0") From a68345e81cb0d1dfa7e6285237f41ce8979c080e Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Thu, 26 Mar 2020 14:53:24 +0000 Subject: [PATCH 1411/3170] Warn if invalid extras are given --- .../resolution/resolvelib/candidates.py | 20 ++++++++++--------- tests/functional/test_new_resolver.py | 7 +++++-- 2 files changed, 16 insertions(+), 11 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 2c5108212db..52de98f991f 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -1,3 +1,5 @@ +import logging + from pip._vendor.packaging.utils import canonicalize_name from pip._internal.req.constructors import install_req_from_line @@ -16,6 +18,8 @@ from pip._vendor.packaging.version import _BaseVersion from pip._vendor.pkg_resources import Distribution +logger = logging.getLogger(__name__) + _CANDIDATE_CACHE = {} # type: Dict[Link, LinkCandidate] @@ -181,19 +185,17 @@ def version(self): def get_dependencies(self): # type: () -> Sequence[InstallRequirement] - # TODO: We should probably warn if the user specifies an unsupported - # extra. We can't do this in the constructor, as we don't know what - # extras are valid until we prepare the candidate. Probably the best - # approach would be to override the base class ``dist`` property, to - # do an additional check of the extras, and if any are invalid, warn - # and remove them from extras. This will be tricky to get right, - # though, as modifying the extras changes the candidate's name and - # hence identity, which isn't acceptable. So for now, we just ignore - # unsupported extras here. # The user may have specified extras that the candidate doesn't # support. We ignore any unsupported extras here. valid_extras = self.extras.intersection(self.base.dist.extras) + invalid_extras = self.extras.difference(self.base.dist.extras) + if invalid_extras: + logger.warning( + "Invalid extras specified in %s: %s", + self.name, + ','.join(sorted(invalid_extras)) + ) deps = [ self.base._make_install_req(str(r)) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 35edef59d3b..3c3b290ba0d 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -95,10 +95,13 @@ def test_new_resolver_installs_extras(script): "dep", "0.1.0", ) - script.pip( + result = script.pip( "install", "--unstable-feature=resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, - "base[add,missing]" + "base[add,missing]", + expect_stderr=True, ) + assert "WARNING: Invalid extras specified" in result.stderr, str(result) + assert ": missing" in result.stderr, str(result) assert_installed(script, base="0.1.0", dep="0.1.0") From 79f255322846a6badc4c1325d7023d3a94461ae5 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 27 Mar 2020 00:39:34 +0800 Subject: [PATCH 1412/3170] Use set operations in tests for readability --- tests/functional/test_new_resolver.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index c3a132e91c2..bb258b72e3f 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -9,14 +9,16 @@ def assert_installed(script, **kwargs): (val['name'], val['version']) for val in json.loads(ret.stdout) ) - assert all(item in installed for item in kwargs.items()), \ + assert set(kwargs.items()) <= installed, \ "{!r} not all in {!r}".format(kwargs, installed) def assert_not_installed(script, *args): ret = script.pip("list", "--format=json") installed = set(val["name"] for val in json.loads(ret.stdout)) - assert all(a not in installed for a in args), \ + # None of the given names should be listed as installed, i.e. their + # intersection should be empty. + assert not (set(args) & installed), \ "{!r} contained in {!r}".format(args, installed) From 07563847b0cffaf900ad615919e74f12d6e1c846 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 27 Mar 2020 03:14:51 +0800 Subject: [PATCH 1413/3170] Decouple candidate and requirement modules This introduces a new module "factory" that contains all methods dealing with producing candidates/requirements from an input requirement/candidate. This allows both models to know nothing about each other, and simply rely on the intermediate to produce the other. I *believe* this also helps us reduce merge conflicts due to adding arguments to those producer functions, since now we only need to modify the factory, and exactly one of candidate/requirement. This is only part of a big scheme--the plan is to also move Candidate.get_dependencies() and Requirement.find_matches() into the factory class, so they can avoid holding and passing around finder, preparer, and make_install_req. This is also necessary to change the return type of Candidate.get_dependencies() to Requirement without hard-coupling. --- .../resolution/resolvelib/candidates.py | 27 +------- .../resolution/resolvelib/factory.py | 62 +++++++++++++++++++ .../resolution/resolvelib/provider.py | 30 ++------- .../resolution/resolvelib/requirements.py | 41 +++--------- .../resolution/resolvelib/resolver.py | 16 ++--- 5 files changed, 85 insertions(+), 91 deletions(-) create mode 100644 src/pip/_internal/resolution/resolvelib/factory.py diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 52de98f991f..2af6e25f18b 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -9,7 +9,7 @@ from .base import Candidate, format_name if MYPY_CHECK_RUNNING: - from typing import Any, Dict, Optional, Sequence, Set + from typing import Any, Optional, Sequence, Set from pip._internal.models.link import Link from pip._internal.operations.prepare import RequirementPreparer @@ -18,31 +18,8 @@ from pip._vendor.packaging.version import _BaseVersion from pip._vendor.pkg_resources import Distribution -logger = logging.getLogger(__name__) - - -_CANDIDATE_CACHE = {} # type: Dict[Link, LinkCandidate] - -def make_candidate( - link, # type: Link - preparer, # type: RequirementPreparer - parent, # type: InstallRequirement - make_install_req, # type: InstallRequirementProvider - extras # type: Set[str] -): - # type: (...) -> Candidate - if link not in _CANDIDATE_CACHE: - _CANDIDATE_CACHE[link] = LinkCandidate( - link, - preparer, - parent=parent, - make_install_req=make_install_req - ) - base = _CANDIDATE_CACHE[link] - if extras: - return ExtrasCandidate(base, extras) - return base +logger = logging.getLogger(__name__) def make_install_req_from_link(link, parent): diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py new file mode 100644 index 00000000000..7bb49f92531 --- /dev/null +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -0,0 +1,62 @@ +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +from .candidates import ExtrasCandidate, LinkCandidate +from .requirements import ExplicitRequirement, SpecifierRequirement + +if MYPY_CHECK_RUNNING: + from typing import Dict, Set + + from pip._internal.index.package_finder import PackageFinder + from pip._internal.models.link import Link + from pip._internal.operations.prepare import RequirementPreparer + from pip._internal.req.req_install import InstallRequirement + from pip._internal.resolution.base import InstallRequirementProvider + + from .base import Candidate, Requirement + + +class Factory(object): + def __init__( + self, + finder, # type: PackageFinder + preparer, # type: RequirementPreparer + make_install_req, # type: InstallRequirementProvider + ): + # type: (...) -> None + self._finder = finder + self._preparer = preparer + self._make_install_req = make_install_req + self._candidate_cache = {} # type: Dict[Link, LinkCandidate] + + def make_candidate( + self, + link, # type: Link + extras, # type: Set[str] + parent, # type: InstallRequirement + ): + # type: (...) -> Candidate + if link not in self._candidate_cache: + self._candidate_cache[link] = LinkCandidate( + link, + self._preparer, + parent=parent, + make_install_req=self._make_install_req + ) + base = self._candidate_cache[link] + if extras: + return ExtrasCandidate(base, extras) + return base + + def make_requirement(self, ireq): + # type: (InstallRequirement) -> Requirement + if ireq.link: + cand = self.make_candidate(ireq.link, extras=set(), parent=ireq) + return ExplicitRequirement(cand) + else: + return SpecifierRequirement( + ireq, + finder=self._finder, + preparer=self._preparer, + factory=self, + make_install_req=self._make_install_req, + ) diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index 21a9b07ac2f..f0860fe043b 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -2,41 +2,24 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from .requirements import make_requirement - if MYPY_CHECK_RUNNING: from typing import Any, Optional, Sequence, Tuple, Union - from pip._internal.index.package_finder import PackageFinder - from pip._internal.operations.prepare import RequirementPreparer from pip._internal.req.req_install import InstallRequirement - from pip._internal.resolution.base import InstallRequirementProvider from .base import Requirement, Candidate + from .factory import Factory class PipProvider(AbstractProvider): def __init__( self, - finder, # type: PackageFinder - preparer, # type: RequirementPreparer + factory, # type: Factory ignore_dependencies, # type: bool - make_install_req # type: InstallRequirementProvider ): # type: (...) -> None - self._finder = finder - self._preparer = preparer + self._factory = factory self._ignore_dependencies = ignore_dependencies - self._make_install_req = make_install_req - - def make_requirement(self, ireq): - # type: (InstallRequirement) -> Requirement - return make_requirement( - ireq, - self._finder, - self._preparer, - self._make_install_req - ) def get_install_requirement(self, c): # type: (Candidate) -> Optional[InstallRequirement] @@ -69,11 +52,6 @@ def get_dependencies(self, candidate): if self._ignore_dependencies: return [] return [ - make_requirement( - r, - self._finder, - self._preparer, - self._make_install_req - ) + self._factory.make_requirement(r) for r in candidate.get_dependencies() ] diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py index 5e468882134..44f989a5fc0 100644 --- a/src/pip/_internal/resolution/resolvelib/requirements.py +++ b/src/pip/_internal/resolution/resolvelib/requirements.py @@ -3,7 +3,6 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING from .base import Requirement, format_name -from .candidates import make_candidate if MYPY_CHECK_RUNNING: from typing import Sequence @@ -14,31 +13,7 @@ from pip._internal.resolution.base import InstallRequirementProvider from .base import Candidate - - -def make_requirement( - ireq, # type: InstallRequirement - finder, # type: PackageFinder - preparer, # type: RequirementPreparer - make_install_req # type: InstallRequirementProvider -): - # type: (...) -> Requirement - if ireq.link: - candidate = make_candidate( - ireq.link, - preparer, - ireq, - make_install_req, - set() - ) - return ExplicitRequirement(candidate) - else: - return SpecifierRequirement( - ireq, - finder, - preparer, - make_install_req - ) + from .factory import Factory class ExplicitRequirement(Requirement): @@ -66,12 +41,14 @@ def __init__( self, ireq, # type: InstallRequirement finder, # type: PackageFinder - preparer, # type:RequirementPreparer + preparer, # type: RequirementPreparer + factory, # type: Factory make_install_req # type: InstallRequirementProvider ): # type: (...) -> None assert ireq.link is None, "This is a link, not a specifier" self._ireq = ireq + self._factory = factory self._finder = finder self._preparer = preparer self._make_install_req = make_install_req @@ -91,12 +68,10 @@ def find_matches(self): hashes=self._ireq.hashes(trust_internet=False), ) return [ - make_candidate( - ican.link, - self._preparer, - self._ireq, - self._make_install_req, - self.extras + self._factory.make_candidate( + link=ican.link, + extras=self.extras, + parent=self._ireq, ) for ican in found.iter_applicable() ] diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index b0aa501ba2a..6e769f19ee5 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -9,6 +9,8 @@ from pip._internal.resolution.resolvelib.provider import PipProvider from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from .factory import Factory + if MYPY_CHECK_RUNNING: from typing import Dict, List, Optional, Tuple @@ -35,24 +37,24 @@ def __init__( py_version_info=None, # type: Optional[Tuple[int, ...]] ): super(Resolver, self).__init__() - self.finder = finder - self.preparer = preparer + self.factory = Factory( + finder=finder, + preparer=preparer, + make_install_req=make_install_req, + ) self.ignore_dependencies = ignore_dependencies - self.make_install_req = make_install_req self._result = None # type: Optional[Result] def resolve(self, root_reqs, check_supported_wheels): # type: (List[InstallRequirement], bool) -> RequirementSet provider = PipProvider( - finder=self.finder, - preparer=self.preparer, + factory=self.factory, ignore_dependencies=self.ignore_dependencies, - make_install_req=self.make_install_req, ) reporter = BaseReporter() resolver = RLResolver(provider, reporter) - requirements = [provider.make_requirement(r) for r in root_reqs] + requirements = [self.factory.make_requirement(r) for r in root_reqs] self._result = resolver.resolve(requirements) req_set = RequirementSet(check_supported_wheels=check_supported_wheels) From f1a8bd4ad6d51283b261bd188e946ae67bd9ae59 Mon Sep 17 00:00:00 2001 From: Noah Gorny <noah.bar.ilan@gmail.com> Date: Mon, 23 Mar 2020 09:27:55 +0200 Subject: [PATCH 1414/3170] session: Fix comment about add_insecure_host, rename to add_trusted_host --- src/pip/_internal/network/session.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py index f5eb15ef2f6..b311a2762b9 100644 --- a/src/pip/_internal/network/session.py +++ b/src/pip/_internal/network/session.py @@ -280,7 +280,7 @@ def __init__(self, *args, **kwargs): # well as any https:// host that we've marked as ignoring TLS errors # for. insecure_adapter = InsecureHTTPAdapter(max_retries=retries) - # Save this for later use in add_insecure_host(). + # Save this for later use in add_trusted_host(). self._insecure_adapter = insecure_adapter self.mount("https://", secure_adapter) From c936ed01655ea85ba408d064c2167fd0cb255c1e Mon Sep 17 00:00:00 2001 From: Noah Gorny <noah.bar.ilan@gmail.com> Date: Mon, 23 Mar 2020 09:30:55 +0200 Subject: [PATCH 1415/3170] session: Rename _insecure_adapter to _trusted_host_adapter Currently it is just the insecure adapter, but this can change in the future --- src/pip/_internal/network/session.py | 9 ++++++--- tests/unit/test_network_session.py | 12 ++++++------ tests/unit/test_req_file.py | 7 +++++-- 3 files changed, 17 insertions(+), 11 deletions(-) diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py index b311a2762b9..1e895a15c62 100644 --- a/src/pip/_internal/network/session.py +++ b/src/pip/_internal/network/session.py @@ -281,7 +281,7 @@ def __init__(self, *args, **kwargs): # for. insecure_adapter = InsecureHTTPAdapter(max_retries=retries) # Save this for later use in add_trusted_host(). - self._insecure_adapter = insecure_adapter + self._trusted_host_adapter = insecure_adapter self.mount("https://", secure_adapter) self.mount("http://", insecure_adapter) @@ -310,12 +310,15 @@ def add_trusted_host(self, host, source=None, suppress_logging=False): if host_port not in self.pip_trusted_origins: self.pip_trusted_origins.append(host_port) - self.mount(build_url_from_netloc(host) + '/', self._insecure_adapter) + self.mount( + build_url_from_netloc(host) + '/', + self._trusted_host_adapter + ) if not host_port[1]: # Mount wildcard ports for the same host. self.mount( build_url_from_netloc(host) + ':', - self._insecure_adapter + self._trusted_host_adapter ) def iter_secure_origins(self): diff --git a/tests/unit/test_network_session.py b/tests/unit/test_network_session.py index 159a4d4dea1..125ac2d8de2 100644 --- a/tests/unit/test_network_session.py +++ b/tests/unit/test_network_session.py @@ -88,7 +88,7 @@ def test_add_trusted_host(self): # Leave a gap to test how the ordering is affected. trusted_hosts = ['host1', 'host3'] session = PipSession(trusted_hosts=trusted_hosts) - insecure_adapter = session._insecure_adapter + trusted_host_adapter = session._trusted_host_adapter prefix2 = 'https://host2/' prefix3 = 'https://host3/' prefix3_wildcard = 'https://host3:' @@ -97,8 +97,8 @@ def test_add_trusted_host(self): assert session.pip_trusted_origins == [ ('host1', None), ('host3', None) ] - assert session.adapters[prefix3] is insecure_adapter - assert session.adapters[prefix3_wildcard] is insecure_adapter + assert session.adapters[prefix3] is trusted_host_adapter + assert session.adapters[prefix3_wildcard] is trusted_host_adapter assert prefix2 not in session.adapters @@ -108,8 +108,8 @@ def test_add_trusted_host(self): ('host1', None), ('host3', None), ('host2', None) ] # Check that prefix3 is still present. - assert session.adapters[prefix3] is insecure_adapter - assert session.adapters[prefix2] is insecure_adapter + assert session.adapters[prefix3] is trusted_host_adapter + assert session.adapters[prefix2] is trusted_host_adapter # Test that adding the same host doesn't create a duplicate. session.add_trusted_host('host3') @@ -123,7 +123,7 @@ def test_add_trusted_host(self): ('host1', None), ('host3', None), ('host2', None), ('host4', 8080) ] - assert session.adapters[prefix4] is insecure_adapter + assert session.adapters[prefix4] is trusted_host_adapter def test_add_trusted_host__logging(self, caplog): """ diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py index c12cdb64bf6..57b50017a7f 100644 --- a/tests/unit/test_req_file.py +++ b/tests/unit/test_req_file.py @@ -401,10 +401,13 @@ def test_set_finder_trusted_host( ) assert list(finder.trusted_hosts) == ['host1', 'host2:8080'] session = finder._link_collector.session - assert session.adapters['https://host1/'] is session._insecure_adapter + assert ( + session.adapters['https://host1/'] + is session._trusted_host_adapter + ) assert ( session.adapters['https://host2:8080/'] - is session._insecure_adapter + is session._trusted_host_adapter ) # Test the log message. From 65a9bec7a444e8b2690197b00e8b8fbb89beb896 Mon Sep 17 00:00:00 2001 From: Noah Gorny <noah.bar.ilan@gmail.com> Date: Mon, 23 Mar 2020 09:05:22 +0200 Subject: [PATCH 1416/3170] session: Add InsecureCacheControlAdapter --- src/pip/_internal/network/session.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py index 1e895a15c62..151f01c72fd 100644 --- a/src/pip/_internal/network/session.py +++ b/src/pip/_internal/network/session.py @@ -217,6 +217,14 @@ def cert_verify(self, conn, url, verify, cert): ) +class InsecureCacheControlAdapter(CacheControlAdapter): + + def cert_verify(self, conn, url, verify, cert): + super(InsecureCacheControlAdapter, self).cert_verify( + conn=conn, url=url, verify=False, cert=cert + ) + + class PipSession(requests.Session): timeout = None # type: Optional[int] From 231ce27829e6608bc115c37e86f4e438da66f658 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 27 Mar 2020 03:25:41 +0800 Subject: [PATCH 1417/3170] Delete unused improt and test util This util function is already broken by previous signature changes to make_requirement() anyway. --- tests/unit/resolution_resolvelib/test_requirement.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/tests/unit/resolution_resolvelib/test_requirement.py b/tests/unit/resolution_resolvelib/test_requirement.py index a563eea28d0..4e83f66d676 100644 --- a/tests/unit/resolution_resolvelib/test_requirement.py +++ b/tests/unit/resolution_resolvelib/test_requirement.py @@ -3,7 +3,6 @@ from pip._internal.req.constructors import install_req_from_line from pip._internal.resolution.resolvelib.base import Candidate -from pip._internal.resolution.resolvelib.requirements import make_requirement from pip._internal.utils.urls import path_to_url # NOTE: All tests are prefixed `test_rlr` (for "test resolvelib resolver"). @@ -49,10 +48,6 @@ def data_url(name): yield test_cases -def req_from_line(line): - return make_requirement(install_req_from_line(line)) - - def test_rlr_requirement_has_name(test_cases, provider): """All requirements should have a name""" for requirement, name, matches in test_cases: From 544c307ebf1073d33840305b59f754b3d33e8825 Mon Sep 17 00:00:00 2001 From: Noah Gorny <noah.bar.ilan@gmail.com> Date: Thu, 26 Mar 2020 21:23:32 +0200 Subject: [PATCH 1418/3170] session: Always cache responses from trusted-host source news: Add news about default behaviour change --- news/7847.feature | 1 + src/pip/_internal/network/session.py | 25 +++++++++++++++---------- 2 files changed, 16 insertions(+), 10 deletions(-) create mode 100644 news/7847.feature diff --git a/news/7847.feature b/news/7847.feature new file mode 100644 index 00000000000..8f1a69b6fd2 --- /dev/null +++ b/news/7847.feature @@ -0,0 +1 @@ +Change default behaviour to always cache responses from trusted-host source. diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py index 151f01c72fd..39a4a546edc 100644 --- a/src/pip/_internal/network/session.py +++ b/src/pip/_internal/network/session.py @@ -271,8 +271,16 @@ def __init__(self, *args, **kwargs): backoff_factor=0.25, ) - # We want to _only_ cache responses on securely fetched origins. We do - # this because we can't validate the response of an insecurely fetched + # Our Insecure HTTPAdapter disables HTTPS validation. It does not + # support caching so we'll use it for all http:// URLs. + # If caching is disabled, we will also use it for + # https:// hosts that we've marked as ignoring + # TLS errors for (trusted-hosts). + insecure_adapter = InsecureHTTPAdapter(max_retries=retries) + + # We want to _only_ cache responses on securely fetched origins or when + # the host is specified as trusted. We do this because + # we can't validate the response of an insecurely/untrusted fetched # origin, and we don't want someone to be able to poison the cache and # require manual eviction from the cache to fix it. if cache: @@ -280,16 +288,13 @@ def __init__(self, *args, **kwargs): cache=SafeFileCache(cache), max_retries=retries, ) + self._trusted_host_adapter = InsecureCacheControlAdapter( + cache=SafeFileCache(cache), + max_retries=retries, + ) else: secure_adapter = HTTPAdapter(max_retries=retries) - - # Our Insecure HTTPAdapter disables HTTPS validation. It does not - # support caching (see above) so we'll use it for all http:// URLs as - # well as any https:// host that we've marked as ignoring TLS errors - # for. - insecure_adapter = InsecureHTTPAdapter(max_retries=retries) - # Save this for later use in add_trusted_host(). - self._trusted_host_adapter = insecure_adapter + self._trusted_host_adapter = insecure_adapter self.mount("https://", secure_adapter) self.mount("http://", insecure_adapter) From 2050ecc7d7ef88770986aca06733e6aaf2fb1c00 Mon Sep 17 00:00:00 2001 From: Noah Gorny <noah.bar.ilan@gmail.com> Date: Mon, 23 Mar 2020 11:29:58 +0200 Subject: [PATCH 1419/3170] tests: session: Remake test_insecure_host into test_trusted_host --- tests/unit/test_network_session.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_network_session.py b/tests/unit/test_network_session.py index 125ac2d8de2..a0d1463b2cf 100644 --- a/tests/unit/test_network_session.py +++ b/tests/unit/test_network_session.py @@ -72,7 +72,7 @@ def test_http_cache_is_not_enabled(self, tmpdir): assert not hasattr(session.adapters["http://"], "cache") - def test_insecure_host_adapter(self, tmpdir): + def test_trusted_hosts_adapter(self, tmpdir): session = PipSession( cache=tmpdir.joinpath("test-cache"), trusted_hosts=["example.com"], @@ -81,8 +81,8 @@ def test_insecure_host_adapter(self, tmpdir): assert "https://example.com/" in session.adapters # Check that the "port wildcard" is present. assert "https://example.com:" in session.adapters - # Check that the cache isn't enabled. - assert not hasattr(session.adapters["https://example.com/"], "cache") + # Check that the cache is enabled. + assert hasattr(session.adapters["https://example.com/"], "cache") def test_add_trusted_host(self): # Leave a gap to test how the ordering is affected. From 4fd12fbf564801576817c7781b13be8c05d34e12 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 27 Mar 2020 03:34:24 +0800 Subject: [PATCH 1420/3170] Fix PipProvider signature in test fixture --- tests/unit/resolution_resolvelib/conftest.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/tests/unit/resolution_resolvelib/conftest.py b/tests/unit/resolution_resolvelib/conftest.py index f4b979d1538..1aa00e67773 100644 --- a/tests/unit/resolution_resolvelib/conftest.py +++ b/tests/unit/resolution_resolvelib/conftest.py @@ -12,6 +12,7 @@ from pip._internal.network.session import PipSession from pip._internal.req.constructors import install_req_from_req_string from pip._internal.req.req_tracker import get_requirement_tracker +from pip._internal.resolution.resolvelib.factory import Factory from pip._internal.resolution.resolvelib.provider import PipProvider from pip._internal.utils.temp_dir import TempDirectory, global_tempdir_manager @@ -55,9 +56,12 @@ def provider(finder, preparer): wheel_cache=None, use_pep517=None, ) - yield PipProvider( + factory = Factory( finder=finder, preparer=preparer, - ignore_dependencies=False, make_install_req=make_install_req, ) + yield PipProvider( + factory=factory, + ignore_dependencies=False, + ) From b1272a98f9e111a84d311052926589b025729733 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 27 Mar 2020 03:53:01 +0800 Subject: [PATCH 1421/3170] Make factory available in tests as a fixture --- tests/unit/resolution_resolvelib/conftest.py | 8 ++++++-- .../resolution_resolvelib/test_requirement.py | 16 ++++++++-------- 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/tests/unit/resolution_resolvelib/conftest.py b/tests/unit/resolution_resolvelib/conftest.py index 1aa00e67773..c92907130bb 100644 --- a/tests/unit/resolution_resolvelib/conftest.py +++ b/tests/unit/resolution_resolvelib/conftest.py @@ -49,18 +49,22 @@ def preparer(finder): @pytest.fixture -def provider(finder, preparer): +def factory(finder, preparer): make_install_req = partial( install_req_from_req_string, isolated=False, wheel_cache=None, use_pep517=None, ) - factory = Factory( + yield Factory( finder=finder, preparer=preparer, make_install_req=make_install_req, ) + + +@pytest.fixture +def provider(factory): yield PipProvider( factory=factory, ignore_dependencies=False, diff --git a/tests/unit/resolution_resolvelib/test_requirement.py b/tests/unit/resolution_resolvelib/test_requirement.py index 4e83f66d676..c8e973195a0 100644 --- a/tests/unit/resolution_resolvelib/test_requirement.py +++ b/tests/unit/resolution_resolvelib/test_requirement.py @@ -48,36 +48,36 @@ def data_url(name): yield test_cases -def test_rlr_requirement_has_name(test_cases, provider): +def test_rlr_requirement_has_name(test_cases, factory, provider): """All requirements should have a name""" for requirement, name, matches in test_cases: ireq = install_req_from_line(requirement) - req = provider.make_requirement(ireq) + req = factory.make_requirement(ireq) assert req.name == name -def test_rlr_correct_number_of_matches(test_cases, provider): +def test_rlr_correct_number_of_matches(test_cases, factory, provider): """Requirements should return the correct number of candidates""" for requirement, name, matches in test_cases: ireq = install_req_from_line(requirement) - req = provider.make_requirement(ireq) + req = factory.make_requirement(ireq) assert len(req.find_matches()) == matches -def test_rlr_candidates_match_requirement(test_cases, provider): +def test_rlr_candidates_match_requirement(test_cases, factory, provider): """Candidates returned from find_matches should satisfy the requirement""" for requirement, name, matches in test_cases: ireq = install_req_from_line(requirement) - req = provider.make_requirement(ireq) + req = factory.make_requirement(ireq) for c in req.find_matches(): assert isinstance(c, Candidate) assert req.is_satisfied_by(c) -def test_rlr_full_resolve(provider): +def test_rlr_full_resolve(factory, provider): """A very basic full resolve""" ireq = install_req_from_line("simplewheel") - req = provider.make_requirement(ireq) + req = factory.make_requirement(ireq) r = Resolver(provider, BaseReporter()) result = r.resolve([req]) assert set(result.mapping.keys()) == {'simplewheel'} From f32beda07537067dc7771ab4dbe018b4ef04a799 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 27 Mar 2020 18:57:49 +0800 Subject: [PATCH 1422/3170] Remove unused provider fixture from argumnet lists --- tests/unit/resolution_resolvelib/test_requirement.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/resolution_resolvelib/test_requirement.py b/tests/unit/resolution_resolvelib/test_requirement.py index c8e973195a0..928eab65ea7 100644 --- a/tests/unit/resolution_resolvelib/test_requirement.py +++ b/tests/unit/resolution_resolvelib/test_requirement.py @@ -48,7 +48,7 @@ def data_url(name): yield test_cases -def test_rlr_requirement_has_name(test_cases, factory, provider): +def test_rlr_requirement_has_name(test_cases, factory): """All requirements should have a name""" for requirement, name, matches in test_cases: ireq = install_req_from_line(requirement) @@ -56,7 +56,7 @@ def test_rlr_requirement_has_name(test_cases, factory, provider): assert req.name == name -def test_rlr_correct_number_of_matches(test_cases, factory, provider): +def test_rlr_correct_number_of_matches(test_cases, factory): """Requirements should return the correct number of candidates""" for requirement, name, matches in test_cases: ireq = install_req_from_line(requirement) @@ -64,7 +64,7 @@ def test_rlr_correct_number_of_matches(test_cases, factory, provider): assert len(req.find_matches()) == matches -def test_rlr_candidates_match_requirement(test_cases, factory, provider): +def test_rlr_candidates_match_requirement(test_cases, factory): """Candidates returned from find_matches should satisfy the requirement""" for requirement, name, matches in test_cases: ireq = install_req_from_line(requirement) From 4ae50f9af7b9dd5d5ee60ce7cd19b7765ab5f564 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 27 Mar 2020 19:00:25 +0800 Subject: [PATCH 1423/3170] Remove unused preparer from SpecifierRequirement --- src/pip/_internal/resolution/resolvelib/factory.py | 1 - src/pip/_internal/resolution/resolvelib/requirements.py | 3 --- 2 files changed, 4 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 7bb49f92531..5f8cf47a4ed 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -56,7 +56,6 @@ def make_requirement(self, ireq): return SpecifierRequirement( ireq, finder=self._finder, - preparer=self._preparer, factory=self, make_install_req=self._make_install_req, ) diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py index 44f989a5fc0..a38c4fe2ea0 100644 --- a/src/pip/_internal/resolution/resolvelib/requirements.py +++ b/src/pip/_internal/resolution/resolvelib/requirements.py @@ -8,7 +8,6 @@ from typing import Sequence from pip._internal.index.package_finder import PackageFinder - from pip._internal.operations.prepare import RequirementPreparer from pip._internal.req.req_install import InstallRequirement from pip._internal.resolution.base import InstallRequirementProvider @@ -41,7 +40,6 @@ def __init__( self, ireq, # type: InstallRequirement finder, # type: PackageFinder - preparer, # type: RequirementPreparer factory, # type: Factory make_install_req # type: InstallRequirementProvider ): @@ -50,7 +48,6 @@ def __init__( self._ireq = ireq self._factory = factory self._finder = finder - self._preparer = preparer self._make_install_req = make_install_req self.extras = ireq.req.extras From b0687a0aa760a826e9c39b646620f3a7aabdf42b Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 26 Mar 2020 16:15:32 +0530 Subject: [PATCH 1424/3170] Fix the bullets --- .../architecture/configuration-files.rst | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/docs/html/development/architecture/configuration-files.rst b/docs/html/development/architecture/configuration-files.rst index aa0c1a52c5b..3cb0db4e49e 100644 --- a/docs/html/development/architecture/configuration-files.rst +++ b/docs/html/development/architecture/configuration-files.rst @@ -26,11 +26,14 @@ with ``RawConfigParser``. pip uses configuration files in two operations: -- During processing of command line options. - - Reading from *all* configuration sources -- As part of ``pip config`` command. - - Reading from *all* configuration sources - - Manipulating a single configuration file +* During processing of command line options. + + * Reading from *all* configuration sources + +* As part of ``pip config`` command. + + * Reading from *all* configuration sources + * Manipulating a single configuration file Both of these operations utilize functionality provided the ``Configuration`` object, which encapsulates all the logic for handling configuration files and From cb7b3af06ff525d84a4575bb3fdce874441bc48e Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 26 Mar 2020 16:16:20 +0530 Subject: [PATCH 1425/3170] Put the bracketed text in the correct position --- docs/html/development/architecture/configuration-files.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/html/development/architecture/configuration-files.rst b/docs/html/development/architecture/configuration-files.rst index 3cb0db4e49e..560b8effcac 100644 --- a/docs/html/development/architecture/configuration-files.rst +++ b/docs/html/development/architecture/configuration-files.rst @@ -3,7 +3,7 @@ Configuration File Handling =========================== The ``pip._internal.configuration`` module is responsible for handling -configuration files (eg. loading from and saving values to) that are used by +(eg. loading from and saving values to) configuration files that are used by pip. The module's functionality is largely exposed through and coordinated by the module's ``Configuration`` class. From ba81cc9bd03b5d6f5c7d52c78a92b9aa6d1671fb Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 26 Mar 2020 16:17:09 +0530 Subject: [PATCH 1426/3170] Move Configuration.get_value to earlier section --- .../architecture/configuration-files.rst | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/docs/html/development/architecture/configuration-files.rst b/docs/html/development/architecture/configuration-files.rst index 560b8effcac..586ef7134e4 100644 --- a/docs/html/development/architecture/configuration-files.rst +++ b/docs/html/development/architecture/configuration-files.rst @@ -81,6 +81,14 @@ and the methods used would be: Provides key-value pairs (like ``dict.items()``) from the loaded-in-memory information, handling all of the override ordering logic. + .. py:method:: get_value(key) + + Provides the value of the given key from the loaded configuration. + The loaded configuration may have ``load_only`` be None or non-None. + This uses the same underlying mechanism as ``Configuration.items()`` and + does follow the precedence logic described in :ref:`Config Precedence + <config-precedence>`. + At the time of writing, the parts of the codebase that use ``Configuration`` in this manner are: ``ConfigOptionParser``, to transparently include configuration handling as part of the command line processing logic, @@ -100,14 +108,6 @@ the methods used would be: .. py:class:: Configuration - .. py:method:: get_value(key) - - Provides the value of the given key from the loaded configuration. - The loaded configuration may have ``load_only`` be None or non-None. - This uses the same underlying mechanism as ``Configuration.items()`` and - does follow the precedence logic described in :ref:`Config Precedence - <config-precedence>`. - .. py:method:: get_file_to_edit() Provides the "highest priority" file, for the :ref:`kind <config-kinds>` of From 98f1a9576a80c9b5ce760e15f42e98b1bf0f2afe Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 26 Mar 2020 16:22:38 +0530 Subject: [PATCH 1427/3170] Improve description of Configuration.{load_only,isolated} --- docs/html/development/architecture/configuration-files.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/html/development/architecture/configuration-files.rst b/docs/html/development/architecture/configuration-files.rst index 586ef7134e4..ce0ef40ee27 100644 --- a/docs/html/development/architecture/configuration-files.rst +++ b/docs/html/development/architecture/configuration-files.rst @@ -53,9 +53,11 @@ environment: a combination of configuration files and environment variables. It can be used in two "modes", for reading all the values from the local environment and for manipulating a single configuration file. It differentiates -between these two modes using the ``load_only`` attribute. +between these two modes using the ``load_only`` attribute, which can be None or +represent the :ref:`kind <config-kinds>` of the configuration file to be +manipulated. -The ``isolated`` attribute manipulates which sources are used when loading the +The ``isolated`` attribute determines which sources are used when loading the configuration. If ``isolated`` is ``True``, user-specific configuration files and environment variables are not used. From f116e0c4ff57d322703ef637f88de997184f42d9 Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara <sh@changeset.nyc> Date: Tue, 24 Mar 2020 16:30:11 -0400 Subject: [PATCH 1428/3170] Add docs link to resolver improvement blog post Signed-off-by: Sumana Harihareswara <sh@changeset.nyc> --- README.rst | 19 +++++++++++-------- docs/html/index.rst | 15 +++++++++------ ...e07991-9123-41bb-9571-7efbe141a93e.trivial | 0 3 files changed, 20 insertions(+), 14 deletions(-) create mode 100644 news/e5e07991-9123-41bb-9571-7efbe141a93e.trivial diff --git a/README.rst b/README.rst index 6d1bf585bb8..8edb0fb7e19 100644 --- a/README.rst +++ b/README.rst @@ -14,12 +14,14 @@ Please take a look at our documentation for how to install and use pip: * `Installation`_ * `Usage`_ -Updates are released regularly, with a new version every 3 months. More details can be found in our documentation: +We release updates regularly, with a new version every 3 months. Find more details in our documentation: * `Release notes`_ * `Release process`_ -If you find bugs, need help, or want to talk to the developers please use our mailing lists or chat rooms: +In 2020, we're working on improvements to the heart of pip. Please `learn more and take our survey`_ to help us do it right. + +If you find bugs, need help, or want to talk to the developers, please use our mailing lists or chat rooms: * `Issue tracking`_ * `Discourse channel`_ @@ -28,9 +30,9 @@ If you find bugs, need help, or want to talk to the developers please use our ma If you want to get involved head over to GitHub to get the source code, look at our development documentation and feel free to jump on the developer mailing lists and chat rooms: * `GitHub page`_ -* `Dev documentation`_ -* `Dev mailing list`_ -* `Dev IRC`_ +* `Development documentation`_ +* `Development mailing list`_ +* `Development IRC`_ Code of Conduct --------------- @@ -45,10 +47,11 @@ rooms, and mailing lists is expected to follow the `PyPA Code of Conduct`_. .. _Release notes: https://pip.pypa.io/en/stable/news.html .. _Release process: https://pip.pypa.io/en/latest/development/release-process/ .. _GitHub page: https://github.com/pypa/pip -.. _Dev documentation: https://pip.pypa.io/en/latest/development +.. _Development documentation: https://pip.pypa.io/en/latest/development +.. _learn more and take our survey: https://pyfound.blogspot.com/2020/03/new-pip-resolver-to-roll-out-this-year.html .. _Issue tracking: https://github.com/pypa/pip/issues .. _Discourse channel: https://discuss.python.org/c/packaging -.. _Dev mailing list: https://groups.google.com/forum/#!forum/pypa-dev +.. _Development mailing list: https://groups.google.com/forum/#!forum/pypa-dev .. _User IRC: https://webchat.freenode.net/?channels=%23pypa -.. _Dev IRC: https://webchat.freenode.net/?channels=%23pypa-dev +.. _Development IRC: https://webchat.freenode.net/?channels=%23pypa-dev .. _PyPA Code of Conduct: https://www.pypa.io/en/latest/code-of-conduct/ diff --git a/docs/html/index.rst b/docs/html/index.rst index 1df75855b21..005aebe850e 100644 --- a/docs/html/index.rst +++ b/docs/html/index.rst @@ -16,17 +16,19 @@ Please take a look at our documentation for how to install and use pip: development/index news -If you find bugs, need help, or want to talk to the developers please use our mailing lists or chat rooms: +In 2020, we're working on improvements to the heart of pip. Please `learn more and take our survey`_ to help us do it right. + +If you find bugs, need help, or want to talk to the developers, please use our mailing lists or chat rooms: * `Issue tracking`_ * `Discourse channel`_ * `User IRC`_ -If you want to get involved head over to GitHub to get the source code and feel free to jump on the developer mailing lists and chat rooms: +If you want to get involved, head over to GitHub to get the source code, and feel free to jump on the developer mailing lists and chat rooms: * `GitHub page`_ -* `Dev mailing list`_ -* `Dev IRC`_ +* `Development mailing list`_ +* `Development IRC`_ Code of Conduct @@ -37,13 +39,14 @@ rooms, and mailing lists is expected to follow the `PyPA Code of Conduct`_. .. _package installer: https://packaging.python.org/guides/tool-recommendations/ .. _Python Package Index: https://pypi.org +.. _learn more and take our survey: https://pyfound.blogspot.com/2020/03/new-pip-resolver-to-roll-out-this-year.html .. _Installation: https://pip.pypa.io/en/stable/installing.html .. _Documentation: https://pip.pypa.io/en/stable/ .. _Changelog: https://pip.pypa.io/en/stable/news.html .. _GitHub page: https://github.com/pypa/pip .. _Issue tracking: https://github.com/pypa/pip/issues .. _Discourse channel: https://discuss.python.org/c/packaging -.. _Dev mailing list: https://groups.google.com/forum/#!forum/pypa-dev +.. _Development mailing list: https://groups.google.com/forum/#!forum/pypa-dev .. _User IRC: https://webchat.freenode.net/?channels=%23pypa -.. _Dev IRC: https://webchat.freenode.net/?channels=%23pypa-dev +.. _Development IRC: https://webchat.freenode.net/?channels=%23pypa-dev .. _PyPA Code of Conduct: https://www.pypa.io/en/latest/code-of-conduct/ diff --git a/news/e5e07991-9123-41bb-9571-7efbe141a93e.trivial b/news/e5e07991-9123-41bb-9571-7efbe141a93e.trivial new file mode 100644 index 00000000000..e69de29bb2d From bcb4009688204a578d0e66e049c2a46ceba60d13 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sat, 28 Mar 2020 02:14:29 +0530 Subject: [PATCH 1429/3170] Add a test case for missing easy-install.pth fix --- tests/functional/test_uninstall.py | 36 ++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/tests/functional/test_uninstall.py b/tests/functional/test_uninstall.py index 04d8550f32a..88ec0c6872b 100644 --- a/tests/functional/test_uninstall.py +++ b/tests/functional/test_uninstall.py @@ -553,6 +553,42 @@ def test_uninstall_editable_and_pip_install(script, data): assert "FSPkg" not in {p["name"] for p in json.loads(list_result2.stdout)} +def test_uninstall_editable_and_pip_install_easy_install_remove(script, data): + """Try uninstall after pip install -e after pip install + and removing easy-install.pth""" + # SETUPTOOLS_SYS_PATH_TECHNIQUE=raw removes the assumption that `-e` + # installs are always higher priority than regular installs. + # This becomes the default behavior in setuptools 25. + script.environ['SETUPTOOLS_SYS_PATH_TECHNIQUE'] = 'raw' + + # Rename easy-install.pth to pip-test.pth + easy_install_pth = join(script.site_packages_path, 'easy-install.pth') + pip_test_pth = join(script.site_packages_path, 'pip-test.pth') + os.rename(easy_install_pth, pip_test_pth) + + # Install FSPkg + pkg_path = data.packages.joinpath("FSPkg") + script.pip('install', '-e', '.', + expect_stderr=True, cwd=pkg_path) + + # Rename easy-install.pth to pip-test-fspkg.pth + pip_test_fspkg_pth = join(script.site_packages_path, 'pip-test-fspkg.pth') + os.rename(easy_install_pth, pip_test_fspkg_pth) + + # Confirm that FSPkg is installed + list_result = script.pip('list', '--format=json') + assert {"name": "FSPkg", "version": "0.1.dev0"} \ + in json.loads(list_result.stdout) + + # Uninstall will fail with given warning + uninstall = script.pip('uninstall', 'FSPkg', '-y') + assert "Cannot remove entries from nonexistent file" in uninstall.stderr + + # Cleanup pth files + os.remove(pip_test_fspkg_pth) + os.rename(pip_test_pth, easy_install_pth) + + def test_uninstall_ignores_missing_packages(script, data): """Uninstall of a non existent package prints a warning and exits cleanly """ From 7f37d5fc6a459f44e6c1d5b7bacb4455335570f9 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sat, 28 Mar 2020 02:18:21 +0530 Subject: [PATCH 1430/3170] Add issue fix description in news file --- news/7856.bugfix | 1 + 1 file changed, 1 insertion(+) diff --git a/news/7856.bugfix b/news/7856.bugfix index e69de29bb2d..de1c264a46b 100644 --- a/news/7856.bugfix +++ b/news/7856.bugfix @@ -0,0 +1 @@ +Uninstall should complete successfully, removing the .egg-link, even if the easy-install.pth file is not found. From 0acfdcd71986c8635b73b09633d13d36b1cfad40 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sat, 28 Mar 2020 11:07:25 +0530 Subject: [PATCH 1431/3170] Check for uninstalled package after deleting pth file --- tests/functional/test_uninstall.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/tests/functional/test_uninstall.py b/tests/functional/test_uninstall.py index 88ec0c6872b..b049d803703 100644 --- a/tests/functional/test_uninstall.py +++ b/tests/functional/test_uninstall.py @@ -580,12 +580,19 @@ def test_uninstall_editable_and_pip_install_easy_install_remove(script, data): assert {"name": "FSPkg", "version": "0.1.dev0"} \ in json.loads(list_result.stdout) + # Remove pip-test-fspkg.pth + os.remove(pip_test_fspkg_pth) + # Uninstall will fail with given warning uninstall = script.pip('uninstall', 'FSPkg', '-y') assert "Cannot remove entries from nonexistent file" in uninstall.stderr - # Cleanup pth files - os.remove(pip_test_fspkg_pth) + # Confirm that FSPkg is uninstalled + list_result = script.pip('list', '--format=json') + assert {"name": "FSPkg", "version": "0.1.dev0"} \ + not in json.loads(list_result.stdout) + + # Rename pip-test.pth back to easy-install.pth os.rename(pip_test_pth, easy_install_pth) From b328a50125b14f4e5bfd7c74f08f3337013ed5da Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Sat, 28 Mar 2020 01:15:28 -0500 Subject: [PATCH 1432/3170] add simple yaml test --- tests/yaml/install/simple.yml | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 tests/yaml/install/simple.yml diff --git a/tests/yaml/install/simple.yml b/tests/yaml/install/simple.yml new file mode 100644 index 00000000000..e1062fd310b --- /dev/null +++ b/tests/yaml/install/simple.yml @@ -0,0 +1,21 @@ +base: + available: + - simple 0.1.0 + - simple 0.2.0 + - base 0.1.0; depends dep + - dep 0.1.0 + +cases: +- + request: + - install: simple + transaction: + - install: + - simple 0.2.0 +- + request: + - install: base + transaction: + - install: + - base 0.1.0 + - dep 0.1.0 From be2268ee874fabf2b96613cfaea104776f1f1d3d Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sat, 28 Mar 2020 16:45:25 +0530 Subject: [PATCH 1433/3170] Clarify use of freeze in pip in program example --- docs/html/user_guide.rst | 6 ++++-- news/7008.doc | 1 + 2 files changed, 5 insertions(+), 2 deletions(-) create mode 100644 news/7008.doc diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index ae8b295bc7e..b9ea48580b3 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -814,8 +814,8 @@ This is easily done using the standard ``subprocess`` module:: subprocess.check_call([sys.executable, '-m', 'pip', 'install', 'my_package']) -If you want to process the output further, use one of the other APIs in the -module:: +If you want to process the output further, use one of the other APIs in the module. +We are using `freeze`_ here which outputs installed packages in requirements format.:: reqs = subprocess.check_output([sys.executable, '-m', 'pip', 'freeze']) @@ -832,3 +832,5 @@ of ability. Some examples that you could consider include: * ``distlib`` - Packaging and distribution utilities (including functions for interacting with PyPI). + +.. _freeze: https://pip.pypa.io/en/latest/reference/pip_freeze/ diff --git a/news/7008.doc b/news/7008.doc new file mode 100644 index 00000000000..0ecd15d2ca6 --- /dev/null +++ b/news/7008.doc @@ -0,0 +1 @@ +Clarify the usage of freeze command in the example of Using pip in your program From 326efa5c710ecf19acc3e1315477251a4cd4bd13 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sat, 28 Mar 2020 17:31:13 +0530 Subject: [PATCH 1434/3170] Defining multiple values for supported options --- docs/html/user_guide.rst | 11 ++++++++++- news/7803.doc | 1 + 2 files changed, 11 insertions(+), 1 deletion(-) create mode 100644 news/7803.doc diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index b9ea48580b3..3195a4011ba 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -440,7 +440,9 @@ and ``--no-cache-dir``, falsy values have to be used: no-compile = no no-warn-script-location = false -Appending options like ``--find-links`` can be written on multiple lines: +It is possible to append values to a section within a configuration file such as the pip.ini file. +This is applicable to appending options like ``--find-links`` or ``--trusted-host``, +which can be written on multiple lines: .. code-block:: ini @@ -453,6 +455,13 @@ Appending options like ``--find-links`` can be written on multiple lines: http://mirror1.example.com http://mirror2.example.com + [install] + trusted-host = + http://mirror1.example.com + http://mirror2.example.com + +This enables users to add additional values in the order of entry for such command line arguments. + Environment Variables --------------------- diff --git a/news/7803.doc b/news/7803.doc new file mode 100644 index 00000000000..bf86b8976ab --- /dev/null +++ b/news/7803.doc @@ -0,0 +1 @@ +Added example of defining multiple values for options which support them From 106bd0d77fbe5f22c9a67f74ca74b3e97513aba9 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sun, 29 Mar 2020 12:00:14 +0530 Subject: [PATCH 1435/3170] Raise an exception if revision is empty in git url --- news/7402.bugfix | 1 + src/pip/_internal/vcs/versioncontrol.py | 6 ++++++ tests/unit/test_vcs.py | 15 +++++++++++++++ 3 files changed, 22 insertions(+) create mode 100644 news/7402.bugfix diff --git a/news/7402.bugfix b/news/7402.bugfix new file mode 100644 index 00000000000..8c8372914aa --- /dev/null +++ b/news/7402.bugfix @@ -0,0 +1 @@ +Raise an exception if revision part of URL is empty for URL used in VCS support diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index da53827cd46..8bfa1cd5772 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -436,6 +436,12 @@ def get_url_rev_and_auth(cls, url): rev = None if '@' in path: path, rev = path.rsplit('@', 1) + if not rev: + raise ValueError( + "The URL {!r} has an empty revision (after @) " + "which is not supported. Include a revision after @ " + "or remove @ from the URL.".format(url) + ) url = urllib_parse.urlunsplit((scheme, netloc, path, query, '')) return url, rev, user_pass diff --git a/tests/unit/test_vcs.py b/tests/unit/test_vcs.py index 42fc43d6855..92e1c0e345d 100644 --- a/tests/unit/test_vcs.py +++ b/tests/unit/test_vcs.py @@ -292,6 +292,21 @@ def test_version_control__get_url_rev_and_auth__missing_plus(url): assert 'malformed VCS url' in str(excinfo.value) +@pytest.mark.parametrize('url', [ + # Test a URL with revision part as empty. + 'git+https://github.com/MyUser/myProject.git@#egg=py_pkg', +]) +def test_version_control__get_url_rev_and_auth__no_revision(url): + """ + Test passing a URL to VersionControl.get_url_rev_and_auth() with + empty revision + """ + with pytest.raises(ValueError) as excinfo: + VersionControl.get_url_rev_and_auth(url) + + assert 'an empty revision (after @)' in str(excinfo.value) + + @pytest.mark.parametrize('url, expected', [ # Test http. ('bzr+http://bzr.myproject.org/MyProject/trunk/#egg=MyProject', From 315447d1703f1d17ad42b74e5f940fe4118d0106 Mon Sep 17 00:00:00 2001 From: gutsytechster <prashantsharma161198@gmail.com> Date: Sun, 29 Mar 2020 16:09:11 +0530 Subject: [PATCH 1436/3170] fix(tests/lib): Catch `subprocess.CalledProcessError` in need_executable This fixes https://github.com/pypa/pip/issues/7924 --- news/7924.bugfix | 1 + tests/lib/__init__.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 news/7924.bugfix diff --git a/news/7924.bugfix b/news/7924.bugfix new file mode 100644 index 00000000000..56411478deb --- /dev/null +++ b/news/7924.bugfix @@ -0,0 +1 @@ +Catch ``subprocess.CalledProcessError`` when checking for the presence of executable within ``need_executable`` using pytest. diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index 65a2399261e..1c1bac274bf 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -1079,7 +1079,7 @@ def need_executable(name, check_cmd): def wrapper(fn): try: subprocess.check_output(check_cmd) - except OSError: + except (OSError, subprocess.CalledProcessError): return pytest.mark.skip( reason='{name} is not available'.format(name=name))(fn) return fn From 03f0ff440dee39714fbfa04b2b367791d7f670dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Sun, 29 Mar 2020 17:26:04 +0700 Subject: [PATCH 1437/3170] Fix rst syntax in Getting Started guide --- docs/html/development/getting-started.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/html/development/getting-started.rst b/docs/html/development/getting-started.rst index 75097157bc9..a809c8b1683 100644 --- a/docs/html/development/getting-started.rst +++ b/docs/html/development/getting-started.rst @@ -53,11 +53,13 @@ It is preferable to run the tests in parallel for better experience during devel since the tests can take a long time to finish when run sequentially. To run tests: + .. code-block:: console $ tox -e py36 -- -n auto To run tests without parallelization, run: + .. code-block:: console $ tox -e py36 From 051c3988b0471f1f1a9d5c9d21b1d03e5291e5d7 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sun, 29 Mar 2020 23:50:54 +0530 Subject: [PATCH 1438/3170] Reworded the news entry --- news/7856.bugfix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/7856.bugfix b/news/7856.bugfix index de1c264a46b..209805d81e8 100644 --- a/news/7856.bugfix +++ b/news/7856.bugfix @@ -1 +1 @@ -Uninstall should complete successfully, removing the .egg-link, even if the easy-install.pth file is not found. +Uninstallation no longer fails on trying to remove non-existent files. From 2c8a0bff42f771686985a5d94f825c69be512aeb Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Mon, 30 Mar 2020 03:20:00 +0800 Subject: [PATCH 1439/3170] Rewrite tests.lib.create_basic_wheel_for_package This implementation uses tests.lib.make_wheel, which allows more flexible wheel configuration in a more structured way. Output-wise this should be almost identical to the previous implementation, with the following exceptions: * Metadata-Version is bumped from 2.0 (previous implementation) to 2.1 (from make_wheel). * Fields previously supplied as UNKNOWN are now omitted since they are not significant to tests. * The DESCRIPTION file is omitted (since the description field is now missing, see previous point). --- tests/lib/__init__.py | 124 +++++++++++++----------------------------- 1 file changed, 39 insertions(+), 85 deletions(-) diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index 1c1bac274bf..0c032388770 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -15,7 +15,7 @@ from zipfile import ZipFile import pytest -from pip._vendor.six import PY2, ensure_binary, text_type +from pip._vendor.six import PY2 from scripttest import FoundDir, TestFileEnvironment from pip._internal.index.collector import LinkCollector @@ -27,6 +27,7 @@ from pip._internal.utils.deprecation import DEPRECATION_MSG_PREFIX from pip._internal.utils.typing import MYPY_CHECK_RUNNING from tests.lib.path import Path, curdir +from tests.lib.wheel import make_wheel if MYPY_CHECK_RUNNING: from typing import List, Optional @@ -984,95 +985,48 @@ def create_basic_wheel_for_package( depends = [] if extras is None: extras = {} - files = { - "{name}/__init__.py": """ - __version__ = {version!r} - def hello(): - return "Hello From {name}" - """, - "{dist_info}/DESCRIPTION": """ - UNKNOWN - """, - "{dist_info}/WHEEL": """ - Wheel-Version: 1.0 - Generator: pip-test-suite - Root-Is-Purelib: true - Tag: py2-none-any - Tag: py3-none-any - - - """, - "{dist_info}/METADATA": """ - Metadata-Version: 2.0 - Name: {name} - Version: {version} - Summary: UNKNOWN - Home-page: UNKNOWN - Author: UNKNOWN - Author-email: UNKNOWN - License: UNKNOWN - Platform: UNKNOWN - {requires_dist} - - UNKNOWN - """, - "{dist_info}/top_level.txt": """ - {name} - """, - # Have an empty RECORD because we don't want to be checking hashes. - "{dist_info}/RECORD": "" - } + if extra_files is None: + extra_files = {} - # Some useful shorthands - archive_name = "{name}-{version}-py2.py3-none-any.whl".format( - name=name, version=version - ) - dist_info = "{name}-{version}.dist-info".format( - name=name, version=version - ) + archive_name = "{}-{}-py2.py3-none-any.whl".format(name, version) + archive_path = script.scratch_path / archive_name + + requires_dist = depends + [ + '{package}; extra == "{extra}"'.format(package=package, extra=extra) + for extra, packages in extras.items() + for package in packages + ] + + wheel_builder = make_wheel( + name=name, + version=version, + wheel_metadata_updates={"Tag": ["py2-none-any", "py3-none-any"]}, + metadata_updates={ + "Provides-Extra": list(extras), + "Requires-Dist": requires_dist, + }, + extra_metadata_files={"top_level.txt": name}, - requires_dist = "\n".join([ - "Requires-Dist: {}".format(pkg) for pkg in depends - ] + [ - "Provides-Extra: {}".format(pkg) for pkg in extras.keys() - ] + [ - "Requires-Dist: {}; extra == \"{}\"".format(pkg, extra) - for extra in extras for pkg in extras[extra] - ]) - - # Replace key-values with formatted values - for key, value in list(files.items()): - del files[key] - key = key.format(name=name, dist_info=dist_info) - files[key] = textwrap.dedent(value).format( - name=name, version=version, requires_dist=requires_dist - ).strip() - - # Add new files after formatting - if extra_files: - files.update(extra_files) - - for fname in files: - path = script.temp_path / fname - path.parent.mkdir(exist_ok=True, parents=True) - path.write_bytes(ensure_binary(files[fname])) - - # The base_dir cast is required to make `shutil.make_archive()` use - # Unicode paths on Python 2, making it able to properly archive - # files with non-ASCII names. - retval = script.scratch_path / archive_name - generated = shutil.make_archive( - retval, - 'zip', - root_dir=script.temp_path, - base_dir=text_type(os.curdir), + # Have an empty RECORD because we don't want to be checking hashes. + record="", ) - shutil.move(generated, retval) - shutil.rmtree(script.temp_path) - script.temp_path.mkdir() + # Create the wheel in-memory to add more files. + wheel_io = BytesIO(wheel_builder.as_bytes()) + with ZipFile(wheel_io, "a") as zf: + zf.writestr( + "{name}/__init__.py".format(name=name), + textwrap.dedent(""" + __version__ = {version!r} + def hello(): + return "Hello From {name}" + """).format(version=version, name=name), + ) + for fn, content in extra_files.items(): + zf.writestr(fn, content) - return retval + archive_path.write_bytes(wheel_io.getvalue()) + return archive_path def need_executable(name, check_cmd): From 0d2954d726aa56ad619097e35fb0f718190f3222 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Mon, 30 Mar 2020 01:31:08 +0530 Subject: [PATCH 1440/3170] Add test to ensure egg-link is removed after uninstall --- tests/functional/test_uninstall.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/tests/functional/test_uninstall.py b/tests/functional/test_uninstall.py index b049d803703..ab41917c986 100644 --- a/tests/functional/test_uninstall.py +++ b/tests/functional/test_uninstall.py @@ -587,6 +587,10 @@ def test_uninstall_editable_and_pip_install_easy_install_remove(script, data): uninstall = script.pip('uninstall', 'FSPkg', '-y') assert "Cannot remove entries from nonexistent file" in uninstall.stderr + assert join( + script.site_packages, 'FSPkg.egg-link' + ) in uninstall.files_deleted, list(uninstall.files_deleted.keys()) + # Confirm that FSPkg is uninstalled list_result = script.pip('list', '--format=json') assert {"name": "FSPkg", "version": "0.1.dev0"} \ From 6db0df928cc6811d46e64a0d772da4d8309ff8c2 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 28 Feb 2020 17:42:13 +0800 Subject: [PATCH 1441/3170] Move wheel cache out of InstallRequirment --- src/pip/_internal/cli/req_command.py | 8 +--- src/pip/_internal/commands/download.py | 8 +--- src/pip/_internal/commands/install.py | 2 +- src/pip/_internal/commands/wheel.py | 5 +-- src/pip/_internal/exceptions.py | 2 +- src/pip/_internal/operations/freeze.py | 5 +-- src/pip/_internal/req/constructors.py | 12 +----- src/pip/_internal/req/req_install.py | 31 --------------- .../_internal/resolution/legacy/resolver.py | 39 ++++++++++++++++--- .../resolution/resolvelib/candidates.py | 1 - .../resolution/resolvelib/resolver.py | 2 + tests/unit/test_req.py | 6 +-- 12 files changed, 46 insertions(+), 75 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 9a98335b4fc..104b033281f 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -255,7 +255,6 @@ def make_resolver( make_install_req = partial( install_req_from_req_string, isolated=options.isolated_mode, - wheel_cache=wheel_cache, use_pep517=use_pep517, ) # The long import name and duplicated invocation is needed to convince @@ -266,6 +265,7 @@ def make_resolver( return pip._internal.resolution.resolvelib.resolver.Resolver( preparer=preparer, finder=finder, + wheel_cache=wheel_cache, make_install_req=make_install_req, use_user_site=use_user_site, ignore_dependencies=options.ignore_dependencies, @@ -279,6 +279,7 @@ def make_resolver( return pip._internal.resolution.legacy.resolver.Resolver( preparer=preparer, finder=finder, + wheel_cache=wheel_cache, make_install_req=make_install_req, use_user_site=use_user_site, ignore_dependencies=options.ignore_dependencies, @@ -295,7 +296,6 @@ def get_requirements( options, # type: Values finder, # type: PackageFinder session, # type: PipSession - wheel_cache, # type: Optional[WheelCache] check_supported_wheels=True, # type: bool ): # type: (...) -> List[InstallRequirement] @@ -313,7 +313,6 @@ def get_requirements( req_to_add = install_req_from_parsed_requirement( parsed_req, isolated=options.isolated_mode, - wheel_cache=wheel_cache, ) req_to_add.is_direct = True requirement_set.add_requirement(req_to_add) @@ -322,7 +321,6 @@ def get_requirements( req_to_add = install_req_from_line( req, None, isolated=options.isolated_mode, use_pep517=options.use_pep517, - wheel_cache=wheel_cache ) req_to_add.is_direct = True requirement_set.add_requirement(req_to_add) @@ -332,7 +330,6 @@ def get_requirements( req, isolated=options.isolated_mode, use_pep517=options.use_pep517, - wheel_cache=wheel_cache ) req_to_add.is_direct = True requirement_set.add_requirement(req_to_add) @@ -345,7 +342,6 @@ def get_requirements( req_to_add = install_req_from_parsed_requirement( parsed_req, isolated=options.isolated_mode, - wheel_cache=wheel_cache, use_pep517=options.use_pep517 ) req_to_add.is_direct = True diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index 5b3b10da64f..c829550633e 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -107,13 +107,7 @@ def run(self, options, args): globally_managed=True, ) - reqs = self.get_requirements( - args, - options, - finder, - session, - None - ) + reqs = self.get_requirements(args, options, finder, session) preparer = self.make_requirement_preparer( temp_build_dir=directory, diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index df9681f8c1c..70bda2e2a92 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -299,7 +299,7 @@ def run(self, options, args): try: reqs = self.get_requirements( args, options, finder, session, - wheel_cache, check_supported_wheels=not options.target_dir, + check_supported_wheels=not options.target_dir, ) warn_deprecated_install_options( diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index b60d13e10d6..48f3bfa29ca 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -133,10 +133,7 @@ def run(self, options, args): globally_managed=True, ) - reqs = self.get_requirements( - args, options, finder, session, - wheel_cache - ) + reqs = self.get_requirements(args, options, finder, session) preparer = self.make_requirement_preparer( temp_build_dir=directory, diff --git a/src/pip/_internal/exceptions.py b/src/pip/_internal/exceptions.py index 115f7eb0c6b..8ac85485e17 100644 --- a/src/pip/_internal/exceptions.py +++ b/src/pip/_internal/exceptions.py @@ -147,7 +147,7 @@ def body(self): triggering requirement. :param req: The InstallRequirement that provoked this error, with - populate_link() having already been called + its link already populated by the resolver's _populate_link(). """ return ' {}'.format(self._requirement_name()) diff --git a/src/pip/_internal/operations/freeze.py b/src/pip/_internal/operations/freeze.py index 5575d70d06f..841133d01b7 100644 --- a/src/pip/_internal/operations/freeze.py +++ b/src/pip/_internal/operations/freeze.py @@ -118,15 +118,12 @@ def freeze( else: line = line[len('--editable'):].strip().lstrip('=') line_req = install_req_from_editable( - line, - isolated=isolated, - wheel_cache=wheel_cache, + line, isolated=isolated, ) else: line_req = install_req_from_line( COMMENT_RE.sub('', line).strip(), isolated=isolated, - wheel_cache=wheel_cache, ) if not line_req.name: diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index 195b0d0be78..edc6c36f999 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -36,7 +36,6 @@ from typing import ( Any, Dict, Optional, Set, Tuple, Union, ) - from pip._internal.cache import WheelCache from pip._internal.req.req_file import ParsedRequirement @@ -223,7 +222,6 @@ def install_req_from_editable( use_pep517=None, # type: Optional[bool] isolated=False, # type: bool options=None, # type: Optional[Dict[str, Any]] - wheel_cache=None, # type: Optional[WheelCache] constraint=False # type: bool ): # type: (...) -> InstallRequirement @@ -242,7 +240,6 @@ def install_req_from_editable( install_options=options.get("install_options", []) if options else [], global_options=options.get("global_options", []) if options else [], hash_options=options.get("hashes", {}) if options else {}, - wheel_cache=wheel_cache, extras=parts.extras, ) @@ -387,7 +384,6 @@ def install_req_from_line( use_pep517=None, # type: Optional[bool] isolated=False, # type: bool options=None, # type: Optional[Dict[str, Any]] - wheel_cache=None, # type: Optional[WheelCache] constraint=False, # type: bool line_source=None, # type: Optional[str] ): @@ -406,7 +402,6 @@ def install_req_from_line( install_options=options.get("install_options", []) if options else [], global_options=options.get("global_options", []) if options else [], hash_options=options.get("hashes", {}) if options else {}, - wheel_cache=wheel_cache, constraint=constraint, extras=parts.extras, ) @@ -416,7 +411,6 @@ def install_req_from_req_string( req_string, # type: str comes_from=None, # type: Optional[InstallRequirement] isolated=False, # type: bool - wheel_cache=None, # type: Optional[WheelCache] use_pep517=None # type: Optional[bool] ): # type: (...) -> InstallRequirement @@ -439,15 +433,13 @@ def install_req_from_req_string( ) return InstallRequirement( - req, comes_from, isolated=isolated, wheel_cache=wheel_cache, - use_pep517=use_pep517 + req, comes_from, isolated=isolated, use_pep517=use_pep517 ) def install_req_from_parsed_requirement( parsed_req, # type: ParsedRequirement isolated=False, # type: bool - wheel_cache=None, # type: Optional[WheelCache] use_pep517=None # type: Optional[bool] ): # type: (...) -> InstallRequirement @@ -458,7 +450,6 @@ def install_req_from_parsed_requirement( use_pep517=use_pep517, constraint=parsed_req.constraint, isolated=isolated, - wheel_cache=wheel_cache ) else: @@ -468,7 +459,6 @@ def install_req_from_parsed_requirement( use_pep517=use_pep517, isolated=isolated, options=parsed_req.options, - wheel_cache=wheel_cache, constraint=parsed_req.constraint, line_source=parsed_req.line_source, ) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index f5e0197f2b4..7da6640502f 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -30,7 +30,6 @@ from pip._internal.operations.install.wheel import install_wheel from pip._internal.pyproject import load_pyproject_toml, make_pyproject_path from pip._internal.req.req_uninstall import UninstallPathSet -from pip._internal.utils import compatibility_tags from pip._internal.utils.deprecation import deprecated from pip._internal.utils.hashes import Hashes from pip._internal.utils.logging import indent_log @@ -55,8 +54,6 @@ Any, Dict, Iterable, List, Optional, Sequence, Union, ) from pip._internal.build_env import BuildEnvironment - from pip._internal.cache import WheelCache - from pip._internal.index.package_finder import PackageFinder from pip._vendor.pkg_resources import Distribution from pip._vendor.packaging.specifiers import SpecifierSet from pip._vendor.packaging.markers import Marker @@ -111,7 +108,6 @@ def __init__( install_options=None, # type: Optional[List[str]] global_options=None, # type: Optional[List[str]] hash_options=None, # type: Optional[Dict[str, List[str]]] - wheel_cache=None, # type: Optional[WheelCache] constraint=False, # type: bool extras=() # type: Iterable[str] ): @@ -126,7 +122,6 @@ def __init__( self.source_dir = os.path.normpath(os.path.abspath(source_dir)) self.editable = editable - self._wheel_cache = wheel_cache if link is None and req and req.url: # PEP 508 URL requirement link = Link(req.url) @@ -241,32 +236,6 @@ def format_debug(self): state=", ".join(state), ) - def populate_link(self, finder, upgrade, require_hashes): - # type: (PackageFinder, bool, bool) -> None - """Ensure that if a link can be found for this, that it is found. - - Note that self.link may still be None - if Upgrade is False and the - requirement is already installed. - - If require_hashes is True, don't use the wheel cache, because cached - wheels, always built locally, have different hashes than the files - downloaded from the index server and thus throw false hash mismatches. - Furthermore, cached wheels at present have undeterministic contents due - to file modification times. - """ - if self.link is None: - self.link = finder.find_requirement(self, upgrade) - if self._wheel_cache is not None and not require_hashes: - old_link = self.link - supported_tags = compatibility_tags.get_supported() - self.link = self._wheel_cache.get( - link=self.link, - package_name=self.name, - supported_tags=supported_tags, - ) - if old_link != self.link: - logger.debug('Using cached wheel link: %s', self.link) - # Things that are valid for all kinds of requirements? @property def name(self): diff --git a/src/pip/_internal/resolution/legacy/resolver.py b/src/pip/_internal/resolution/legacy/resolver.py index d6800352614..a2f7895cee4 100644 --- a/src/pip/_internal/resolution/legacy/resolver.py +++ b/src/pip/_internal/resolution/legacy/resolver.py @@ -30,6 +30,7 @@ ) from pip._internal.req.req_set import RequirementSet from pip._internal.resolution.base import BaseResolver +from pip._internal.utils.compatibility_tags import get_supported from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import dist_in_usersite, normalize_version_info from pip._internal.utils.packaging import ( @@ -42,6 +43,7 @@ from typing import DefaultDict, List, Optional, Set, Tuple from pip._vendor import pkg_resources + from pip._internal.cache import WheelCache from pip._internal.distributions import AbstractDistribution from pip._internal.index.package_finder import PackageFinder from pip._internal.operations.prepare import RequirementPreparer @@ -112,6 +114,7 @@ def __init__( self, preparer, # type: RequirementPreparer finder, # type: PackageFinder + wheel_cache, # type: Optional[WheelCache] make_install_req, # type: InstallRequirementProvider use_user_site, # type: bool ignore_dependencies, # type: bool @@ -134,6 +137,7 @@ def __init__( self.preparer = preparer self.finder = finder + self.wheel_cache = wheel_cache self.upgrade_strategy = upgrade_strategy self.force_reinstall = force_reinstall @@ -166,7 +170,7 @@ def resolve(self, root_reqs, check_supported_wheels): # 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 + # _populate_link() needs to be called before we can make decisions # based on link type. discovered_reqs = [] # type: List[InstallRequirement] hash_errors = HashErrors() @@ -256,6 +260,34 @@ def _check_skip_installed(self, req_to_install): self._set_req_to_reinstall(req_to_install) return None + def _populate_link(self, req): + # type: (InstallRequirement) -> None + """Ensure that if a link can be found for this, that it is found. + + Note that req.link may still be None - if Upgrade is False and the + requirement is already installed. + + If preparer.require_hashes is True, don't use the wheel cache, because + cached wheels, always built locally, have different hashes than the + files downloaded from the index server and thus throw false hash + mismatches. Furthermore, cached wheels at present have undeterministic + contents due to file modification times. + """ + upgrade = self._is_upgrade_allowed(req) + if req.link is None: + req.link = self.finder.find_requirement(req, upgrade) + + if self.wheel_cache is None or self.preparer.require_hashes: + return + cached_link = self.wheel_cache.get( + link=req.link, + package_name=req.name, + supported_tags=get_supported(), + ) + if req.link != cached_link: + logger.debug('Using cached wheel link: %s', cached_link) + req.link = cached_link + def _get_abstract_dist_for(self, req): # type: (InstallRequirement) -> AbstractDistribution """Takes a InstallRequirement and returns a single AbstractDist \ @@ -274,11 +306,8 @@ def _get_abstract_dist_for(self, req): req, skip_reason ) - upgrade_allowed = self._is_upgrade_allowed(req) - # We eagerly populate the link, since that's our "legacy" behavior. - require_hashes = self.preparer.require_hashes - req.populate_link(self.finder, upgrade_allowed, require_hashes) + self._populate_link(req) abstract_dist = self.preparer.prepare_linked_requirement(req) # NOTE diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 2af6e25f18b..b589649c5f2 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -30,7 +30,6 @@ def make_install_req_from_link(link, parent): comes_from=parent.comes_from, use_pep517=parent.use_pep517, isolated=parent.isolated, - wheel_cache=parent._wheel_cache, constraint=parent.constraint, options=dict( install_options=parent.install_options, diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 6e769f19ee5..1f332d4ed79 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -16,6 +16,7 @@ from pip._vendor.resolvelib.resolvers import Result + from pip._internal.cache import WheelCache from pip._internal.index.package_finder import PackageFinder from pip._internal.operations.prepare import RequirementPreparer from pip._internal.req.req_install import InstallRequirement @@ -27,6 +28,7 @@ def __init__( self, preparer, # type: RequirementPreparer finder, # type: PackageFinder + wheel_cache, # type: Optional[WheelCache] make_install_req, # type: InstallRequirementProvider use_user_site, # type: bool ignore_dependencies, # type: bool diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index 530c3735c5e..d6129d51e1f 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -74,7 +74,6 @@ def _basic_resolver(self, finder, require_hashes=False): make_install_req = partial( install_req_from_req_string, isolated=False, - wheel_cache=None, use_pep517=None, ) @@ -95,6 +94,7 @@ def _basic_resolver(self, finder, require_hashes=False): preparer=preparer, make_install_req=make_install_req, finder=finder, + wheel_cache=None, use_user_site=False, upgrade_strategy="to-satisfy-only", ignore_dependencies=False, ignore_installed=False, ignore_requires_python=False, force_reinstall=False, @@ -176,9 +176,7 @@ def test_missing_hash_with_require_hashes_in_reqs_file(self, data, tmpdir): command = create_command('install') with requirements_file('--require-hashes', tmpdir) as reqs_file: options, args = command.parse_args(['-r', reqs_file]) - command.get_requirements( - args, options, finder, session, wheel_cache=None, - ) + command.get_requirements(args, options, finder, session) assert options.require_hashes def test_unsupported_hashes(self, data): From 1514d85a08b8cb2a4718b6e8f04942e20aa79e19 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Mon, 30 Mar 2020 17:37:28 +0800 Subject: [PATCH 1442/3170] Fix wheel_cache argument for the new resolver --- tests/unit/resolution_resolvelib/test_resolver.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/resolution_resolvelib/test_resolver.py b/tests/unit/resolution_resolvelib/test_resolver.py index 116b4d03ccc..bb8bd1c7424 100644 --- a/tests/unit/resolution_resolvelib/test_resolver.py +++ b/tests/unit/resolution_resolvelib/test_resolver.py @@ -14,6 +14,7 @@ def resolver(preparer, finder): resolver = Resolver( preparer=preparer, finder=finder, + wheel_cache=None, make_install_req=mock.Mock(), use_user_site="not-used", ignore_dependencies="not-used", From c388315bc016e3d2fcd33f08f6e57b0b7838f417 Mon Sep 17 00:00:00 2001 From: cjc7373 <niuchangcun@163.com> Date: Sun, 29 Mar 2020 19:18:32 +0800 Subject: [PATCH 1443/3170] Clarify the usage of --no-binary and --only-binary command --- docs/html/reference/pip_install.rst | 11 +++++++++++ news/3191.doc | 1 + src/pip/_internal/cli/cmdoptions.py | 24 ++++++++++++------------ 3 files changed, 24 insertions(+), 12 deletions(-) create mode 100644 news/3191.doc diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index bd61151d712..9b5303e2bf3 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -934,6 +934,17 @@ Examples $ pip install --pre SomePackage + +#. Install packages from source. + + Do not use any binary packages:: + + $ pip install SomePackage1 SomePackage2 --no-binary :all: + + Specify ``SomePackage1`` to be installed from source:: + + $ pip install SomePackage1 SomePackage2 --no-binary SomePackage1 + ---- .. [1] This is true with the exception that pip v7.0 and v7.0.1 required quotes diff --git a/news/3191.doc b/news/3191.doc new file mode 100644 index 00000000000..513cf2d0a62 --- /dev/null +++ b/news/3191.doc @@ -0,0 +1 @@ +Clarify the usage of --no-binary command. diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index 79958550ec4..4cbd5abf2c4 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -475,12 +475,12 @@ def no_binary(): "--no-binary", dest="format_control", action="callback", callback=_handle_no_binary, type="str", default=format_control, - help="Do not use binary packages. Can be supplied multiple times, and " - "each time adds to the existing value. Accepts either :all: to " - "disable all binary packages, :none: to empty the set, or one or " - "more package names with commas between them (no colons). Note " - "that some packages are tricky to compile and may fail to " - "install when this option is used on them.", + help='Do not use binary packages. Can be supplied multiple times, and ' + 'each time adds to the existing value. Accepts either ":all:" to ' + 'disable all binary packages, ":none:" to empty the set (notice ' + 'the colons), or one or more package names with commas between ' + 'them (no colons). Note that some packages are tricky to compile ' + 'and may fail to install when this option is used on them.', ) @@ -491,12 +491,12 @@ def only_binary(): "--only-binary", dest="format_control", action="callback", callback=_handle_only_binary, type="str", default=format_control, - help="Do not use source packages. Can be supplied multiple times, and " - "each time adds to the existing value. Accepts either :all: to " - "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.", + help='Do not use source packages. Can be supplied multiple times, and ' + 'each time adds to the existing value. Accepts either ":all:" to ' + '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.', ) From 0d2ca67729344adb5514ad7d1be7e1850c3d6be6 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Mon, 30 Mar 2020 22:29:40 +0530 Subject: [PATCH 1444/3170] Changed ValueError to InstallationError --- src/pip/_internal/vcs/versioncontrol.py | 4 ++-- tests/unit/test_vcs.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index 8bfa1cd5772..71b4650a252 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -11,7 +11,7 @@ from pip._vendor import pkg_resources from pip._vendor.six.moves.urllib import parse as urllib_parse -from pip._internal.exceptions import BadCommand +from pip._internal.exceptions import BadCommand, InstallationError from pip._internal.utils.compat import samefile from pip._internal.utils.misc import ( ask_path_exists, @@ -437,7 +437,7 @@ def get_url_rev_and_auth(cls, url): if '@' in path: path, rev = path.rsplit('@', 1) if not rev: - raise ValueError( + raise InstallationError( "The URL {!r} has an empty revision (after @) " "which is not supported. Include a revision after @ " "or remove @ from the URL.".format(url) diff --git a/tests/unit/test_vcs.py b/tests/unit/test_vcs.py index 92e1c0e345d..590cb5c0b75 100644 --- a/tests/unit/test_vcs.py +++ b/tests/unit/test_vcs.py @@ -5,7 +5,7 @@ from mock import patch from pip._vendor.packaging.version import parse as parse_version -from pip._internal.exceptions import BadCommand +from pip._internal.exceptions import BadCommand, InstallationError from pip._internal.utils.misc import hide_url, hide_value from pip._internal.vcs import make_vcs_requirement_url from pip._internal.vcs.bazaar import Bazaar @@ -301,7 +301,7 @@ def test_version_control__get_url_rev_and_auth__no_revision(url): Test passing a URL to VersionControl.get_url_rev_and_auth() with empty revision """ - with pytest.raises(ValueError) as excinfo: + with pytest.raises(InstallationError) as excinfo: VersionControl.get_url_rev_and_auth(url) assert 'an empty revision (after @)' in str(excinfo.value) From 799ffcbfe1eee193c5d671595ad0c9110d87cf0e Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 31 Mar 2020 01:29:15 +0800 Subject: [PATCH 1445/3170] One arg per line --- src/pip/_internal/operations/freeze.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/operations/freeze.py b/src/pip/_internal/operations/freeze.py index 841133d01b7..0ac3c4e9028 100644 --- a/src/pip/_internal/operations/freeze.py +++ b/src/pip/_internal/operations/freeze.py @@ -118,7 +118,8 @@ def freeze( else: line = line[len('--editable'):].strip().lstrip('=') line_req = install_req_from_editable( - line, isolated=isolated, + line, + isolated=isolated, ) else: line_req = install_req_from_line( From 4a453e12d4a76944aacce4eb3d371b44d0395696 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 31 Mar 2020 01:31:11 +0800 Subject: [PATCH 1446/3170] Reword docstring to match code --- src/pip/_internal/resolution/legacy/resolver.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/resolution/legacy/resolver.py b/src/pip/_internal/resolution/legacy/resolver.py index a2f7895cee4..dff1398cdee 100644 --- a/src/pip/_internal/resolution/legacy/resolver.py +++ b/src/pip/_internal/resolution/legacy/resolver.py @@ -264,8 +264,9 @@ def _populate_link(self, req): # type: (InstallRequirement) -> None """Ensure that if a link can be found for this, that it is found. - Note that req.link may still be None - if Upgrade is False and the - requirement is already installed. + Note that req.link may still be None - if the requirement is already + installed and not needed to be upgraded based on the return value of + _is_upgrade_allowed(). If preparer.require_hashes is True, don't use the wheel cache, because cached wheels, always built locally, have different hashes than the From d79632a7a9aaec8d75d312ec43ad991fcc833c50 Mon Sep 17 00:00:00 2001 From: Xavier Fernandez <xav.fernandez@gmail.com> Date: Mon, 30 Mar 2020 22:31:33 +0200 Subject: [PATCH 1447/3170] Remove test bugfix This does not impact our users & should not appear in the changelog --- news/7924.bugfix | 1 - 1 file changed, 1 deletion(-) delete mode 100644 news/7924.bugfix diff --git a/news/7924.bugfix b/news/7924.bugfix deleted file mode 100644 index 56411478deb..00000000000 --- a/news/7924.bugfix +++ /dev/null @@ -1 +0,0 @@ -Catch ``subprocess.CalledProcessError`` when checking for the presence of executable within ``need_executable`` using pytest. From 2c6a063a27bd137fde9a6f02cdb043c9e9ece950 Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Tue, 31 Mar 2020 01:25:45 -0500 Subject: [PATCH 1448/3170] remove proxy module tests/lib/scripttest.py in favour of importing from tests.lib directly --- tests/conftest.py | 5 ++--- tests/lib/scripttest.py | 3 --- 2 files changed, 2 insertions(+), 6 deletions(-) delete mode 100644 tests/lib/scripttest.py diff --git a/tests/conftest.py b/tests/conftest.py index 077de95280c..3af03722394 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,10 +16,9 @@ from pip._internal.cli.main import main as pip_entry_point from pip._internal.utils.temp_dir import global_tempdir_manager from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from tests.lib import DATA_DIR, SRC_DIR, TestData +from tests.lib import DATA_DIR, SRC_DIR, TestData, PipTestEnvironment from tests.lib.certs import make_tls_cert, serialize_cert, serialize_key from tests.lib.path import Path -from tests.lib.scripttest import PipTestEnvironment from tests.lib.server import make_mock_server, server_running from tests.lib.venv import VirtualEnvironment @@ -370,7 +369,7 @@ def script(tmpdir, virtualenv, script_factory): Return a PipTestEnvironment which is unique to each test function and will execute all commands inside of the unique virtual environment for this test function. The returned object is a - ``tests.lib.scripttest.PipTestEnvironment``. + ``tests.lib.PipTestEnvironment``. """ return script_factory(tmpdir.joinpath("workspace"), virtualenv) diff --git a/tests/lib/scripttest.py b/tests/lib/scripttest.py deleted file mode 100644 index 55e05806273..00000000000 --- a/tests/lib/scripttest.py +++ /dev/null @@ -1,3 +0,0 @@ -from __future__ import absolute_import - -from . import PipTestEnvironment # noqa From 6db7f421864ff2c42f4862ebd6e1b244f08a83a9 Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Tue, 31 Mar 2020 01:42:40 -0500 Subject: [PATCH 1449/3170] sort imports --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 3af03722394..faec4f641b6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -16,7 +16,7 @@ from pip._internal.cli.main import main as pip_entry_point from pip._internal.utils.temp_dir import global_tempdir_manager from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from tests.lib import DATA_DIR, SRC_DIR, TestData, PipTestEnvironment +from tests.lib import DATA_DIR, SRC_DIR, PipTestEnvironment, TestData from tests.lib.certs import make_tls_cert, serialize_cert, serialize_key from tests.lib.path import Path from tests.lib.server import make_mock_server, server_running From 6fbf80a9f9ba031a188737454bf90c1607cce99f Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Mon, 30 Mar 2020 15:54:02 +0530 Subject: [PATCH 1450/3170] Remove vcs urls pertaining to git protocol from docs --- docs/html/reference/pip_install.rst | 16 +++++++++------- news/1983.doc | 1 + 2 files changed, 10 insertions(+), 7 deletions(-) create mode 100644 news/1983.doc diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index bd61151d712..6600693ca94 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -402,28 +402,30 @@ Git ^^^ pip currently supports cloning over ``git``, ``git+http``, ``git+https``, -``git+ssh``, ``git+git`` and ``git+file``: +``git+ssh``, ``git+git`` and ``git+file``, but note that the ``git``, ``git+git``, +and ``git+http`` are not recommended due to their lack of security. +(The former two uses `the Git Protocol.`_) Here are the supported forms:: - [-e] git://git.example.com/MyProject#egg=MyProject [-e] git+http://git.example.com/MyProject#egg=MyProject [-e] git+https://git.example.com/MyProject#egg=MyProject [-e] git+ssh://git.example.com/MyProject#egg=MyProject - [-e] git+git://git.example.com/MyProject#egg=MyProject [-e] git+file:///home/user/projects/MyProject#egg=MyProject Passing a branch name, a commit hash, a tag name or a git ref is possible like so:: - [-e] git://git.example.com/MyProject.git@master#egg=MyProject - [-e] git://git.example.com/MyProject.git@v1.0#egg=MyProject - [-e] git://git.example.com/MyProject.git@da39a3ee5e6b4b0d3255bfef95601890afd80709#egg=MyProject - [-e] git://git.example.com/MyProject.git@refs/pull/123/head#egg=MyProject + [-e] git+https://git.example.com/MyProject.git@master#egg=MyProject + [-e] git+https://git.example.com/MyProject.git@v1.0#egg=MyProject + [-e] git+https://git.example.com/MyProject.git@da39a3ee5e6b4b0d3255bfef95601890afd80709#egg=MyProject + [-e] git+https://git.example.com/MyProject.git@refs/pull/123/head#egg=MyProject When passing a commit hash, specifying a full hash is preferable to a partial hash because a full hash allows pip to operate more efficiently (e.g. by making fewer network calls). +.. _`the Git Protocol.`: https://git-scm.com/book/en/v2/Git-on-the-Server-The-Protocols + Mercurial ^^^^^^^^^ diff --git a/news/1983.doc b/news/1983.doc new file mode 100644 index 00000000000..58b85ac1fe2 --- /dev/null +++ b/news/1983.doc @@ -0,0 +1 @@ +Remove VCS URLs pertaining to the Git protocol from docs From 64c78a5875a4cd57268a1e6deb9dac4c60cd4950 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 31 Mar 2020 16:19:41 +0800 Subject: [PATCH 1451/3170] Rename unit tests to use the new_resolver scheme --- tests/unit/resolution_resolvelib/test_requirement.py | 8 ++++---- tests/unit/resolution_resolvelib/test_resolver.py | 2 +- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/unit/resolution_resolvelib/test_requirement.py b/tests/unit/resolution_resolvelib/test_requirement.py index 928eab65ea7..25252dd4acb 100644 --- a/tests/unit/resolution_resolvelib/test_requirement.py +++ b/tests/unit/resolution_resolvelib/test_requirement.py @@ -48,7 +48,7 @@ def data_url(name): yield test_cases -def test_rlr_requirement_has_name(test_cases, factory): +def test_new_resolver_requirement_has_name(test_cases, factory): """All requirements should have a name""" for requirement, name, matches in test_cases: ireq = install_req_from_line(requirement) @@ -56,7 +56,7 @@ def test_rlr_requirement_has_name(test_cases, factory): assert req.name == name -def test_rlr_correct_number_of_matches(test_cases, factory): +def test_new_resolver_correct_number_of_matches(test_cases, factory): """Requirements should return the correct number of candidates""" for requirement, name, matches in test_cases: ireq = install_req_from_line(requirement) @@ -64,7 +64,7 @@ def test_rlr_correct_number_of_matches(test_cases, factory): assert len(req.find_matches()) == matches -def test_rlr_candidates_match_requirement(test_cases, factory): +def test_new_resolver_candidates_match_requirement(test_cases, factory): """Candidates returned from find_matches should satisfy the requirement""" for requirement, name, matches in test_cases: ireq = install_req_from_line(requirement) @@ -74,7 +74,7 @@ def test_rlr_candidates_match_requirement(test_cases, factory): assert req.is_satisfied_by(c) -def test_rlr_full_resolve(factory, provider): +def test_new_resolver_full_resolve(factory, provider): """A very basic full resolve""" ireq = install_req_from_line("simplewheel") req = factory.make_requirement(ireq) diff --git a/tests/unit/resolution_resolvelib/test_resolver.py b/tests/unit/resolution_resolvelib/test_resolver.py index 116b4d03ccc..3934dc9a086 100644 --- a/tests/unit/resolution_resolvelib/test_resolver.py +++ b/tests/unit/resolution_resolvelib/test_resolver.py @@ -57,7 +57,7 @@ def resolver(preparer, finder): ), ], ) -def test_rlr_resolver_get_installation_order(resolver, edges, ordered_reqs): +def test_new_resolver_get_installation_order(resolver, edges, ordered_reqs): # Build graph from edge declarations. graph = DirectedGraph() for parent, child in edges: From c5908bd222b6186059c43d2d170307e63636e5ab Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 31 Mar 2020 18:45:05 +0530 Subject: [PATCH 1452/3170] Use a code block for denoting directory structure --- docs/html/reference/pip_install.rst | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index bd61151d712..8348f9da387 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -384,16 +384,14 @@ where ``setup.py`` is not in the root of project, the "subdirectory" component is used. The value of the "subdirectory" component should be a path starting from the root of the project to where ``setup.py`` is located. -So if your repository layout is: - - - pkg_dir/ - - - setup.py # setup.py for package ``pkg`` - - some_module.py - - other_dir/ - - - some_file - - some_other_file +So if your repository layout is:: + + pkg_dir + ├── setup.py # setup.py for package "pkg" + └── some_module.py + other_dir + └── some_file + some_other_file You'll need to use ``pip install -e "vcs+protocol://repo_url/#egg=pkg&subdirectory=pkg_dir"``. From 13817bf450d48b32ab09e68d106212a7627bd0ac Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 31 Mar 2020 19:52:55 +0530 Subject: [PATCH 1453/3170] Update docs conventions based on recent experience --- docs/html/development/conventions.rst | 82 +++++++++++++++++---------- 1 file changed, 51 insertions(+), 31 deletions(-) diff --git a/docs/html/development/conventions.rst b/docs/html/development/conventions.rst index 4d8998fe3c6..e3b4c6dc14e 100644 --- a/docs/html/development/conventions.rst +++ b/docs/html/development/conventions.rst @@ -10,9 +10,9 @@ and past conventions are rendered obsolete. .. note:: - Currently, these conventions are not enforced automatically, and - need to be verified manually during code review. We are interested - in linters that can help us enforce these conventions automatically. + Currently, these conventions are not enforced automatically, and + need to be verified manually during code review. We are interested + in linters that can help us enforce these conventions automatically. Files @@ -45,27 +45,42 @@ code blocks). Indentation ----------- -We use 4 spaces for indentation. +We use 3 spaces for indentation. :: - .. note:: + .. note:: - Directive blocks + Directive blocks - :: + :: - Code block. + Code block. -Bullet lists are the only exception to the 4 spaces rule, using 2 spaces +Bullet lists are the only exception to the 3 spaces rule, using 2 spaces when wrapping lines. :: - - This is a bullet list. - - This is a lot of text in a single bullet which would require wrapping - across multiple lines to fit in the line length limits. + - This is a bullet list. + - This is a lot of text in a single bullet which would require wrapping + across multiple lines to fit in the line length limits. +Note that nested lists would use 3 spaces for indentation, and require +blank lines on either side (that's the ReST syntax). + +:: + + - This is a bullet list. + - There is a nested list associated with this list item. + + - This is a nested bullet list. + - With multiple bullets even. + - And some of the bullets have really long sentences that would + require wrapping across multiple lines. + + - This is a lot of text in a single bullet which would require wrapping + across multiple lines to fit in the line length limits. Headings ======== @@ -86,42 +101,47 @@ it. :: - ========= - Heading 1 - ========= + ========= + Heading 1 + ========= - Lorem ipsum dolor sit amet consectetur adipisicing elit. + Lorem ipsum dolor sit amet consectetur adipisicing elit. - Heading 2 - ========= + Heading 2 + ========= - Lorem ipsum dolor sit amet consectetur adipisicing elit. + Lorem ipsum dolor sit amet consectetur adipisicing elit. - Heading 3 - --------- + Heading 3 + --------- - Lorem ipsum dolor sit amet consectetur adipisicing elit. + Lorem ipsum dolor sit amet consectetur adipisicing elit. - Heading 4 - ^^^^^^^^^ + Heading 4 + ^^^^^^^^^ - Lorem ipsum dolor sit amet consectetur adipisicing elit. + Lorem ipsum dolor sit amet consectetur adipisicing elit. - Heading 5 - ''''''''' + Heading 5 + ''''''''' - Lorem ipsum dolor sit amet consectetur adipisicing elit. + Lorem ipsum dolor sit amet consectetur adipisicing elit. - Heading 6 - ********* + Heading 6 + ********* - Lorem ipsum dolor sit amet consectetur adipisicing elit. + Lorem ipsum dolor sit amet consectetur adipisicing elit. Writing ======= +.. note:: + + We're still discussing *how* pip should be capitalized in prose. The + current statement here is tentative. + pip is a proper noun, and spelt all lowercase. Do not capitalize pip as "Pip" at the start of a sentence. From 59df53690604ebed67f20977d76a9f96ece03918 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Tue, 31 Mar 2020 20:08:47 +0530 Subject: [PATCH 1454/3170] Update newsfile message --- news/7402.bugfix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/7402.bugfix b/news/7402.bugfix index 8c8372914aa..91eb085f5bc 100644 --- a/news/7402.bugfix +++ b/news/7402.bugfix @@ -1 +1 @@ -Raise an exception if revision part of URL is empty for URL used in VCS support +Reject VCS URLs with an empty revision. From 91444f546e0542a2e7414432de00a773a895902a Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 31 Mar 2020 20:13:15 +0530 Subject: [PATCH 1455/3170] Avoid Sphinx from justify-expanding code block --- docs/html/reference/pip_install.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index 8348f9da387..1bd8e1293c0 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -384,7 +384,7 @@ where ``setup.py`` is not in the root of project, the "subdirectory" component is used. The value of the "subdirectory" component should be a path starting from the root of the project to where ``setup.py`` is located. -So if your repository layout is:: +If your repository layout is:: pkg_dir ├── setup.py # setup.py for package "pkg" @@ -393,7 +393,9 @@ So if your repository layout is:: └── some_file some_other_file -You'll need to use ``pip install -e "vcs+protocol://repo_url/#egg=pkg&subdirectory=pkg_dir"``. +Then, to install from this repository, the syntax would be:: + + $ pip install -e "vcs+protocol://repo_url/#egg=pkg&subdirectory=pkg_dir" Git From 9af42c27e806779db32757533625651bb49a3f63 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Tue, 31 Mar 2020 20:34:21 +0530 Subject: [PATCH 1456/3170] Clarification on removed urls --- docs/html/reference/pip_install.rst | 12 ++++++++---- news/1983.doc | 3 ++- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index 6600693ca94..7b0cebbacf7 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -402,9 +402,13 @@ Git ^^^ pip currently supports cloning over ``git``, ``git+http``, ``git+https``, -``git+ssh``, ``git+git`` and ``git+file``, but note that the ``git``, ``git+git``, -and ``git+http`` are not recommended due to their lack of security. -(The former two uses `the Git Protocol.`_) +``git+ssh``, ``git+git`` and ``git+file``. + +.. warning:: + + Note that the ``git``, ``git+git``,and ``git+http`` are not recommended. + (The former two use `the Git Protocol`_, which lacks authentication, and HTTP is + insecure due to lack of TLS based encryption) Here are the supported forms:: @@ -424,7 +428,7 @@ When passing a commit hash, specifying a full hash is preferable to a partial hash because a full hash allows pip to operate more efficiently (e.g. by making fewer network calls). -.. _`the Git Protocol.`: https://git-scm.com/book/en/v2/Git-on-the-Server-The-Protocols +.. _`the Git Protocol`: https://git-scm.com/book/en/v2/Git-on-the-Server-The-Protocols Mercurial ^^^^^^^^^ diff --git a/news/1983.doc b/news/1983.doc index 58b85ac1fe2..9766ebb571d 100644 --- a/news/1983.doc +++ b/news/1983.doc @@ -1 +1,2 @@ -Remove VCS URLs pertaining to the Git protocol from docs +Emphasize that VCS URLs using git, git+git and git+http are insecure due to +lack of authentication and encryption From 2a8f5705b20102b8c594ccaa7dcba5e976b06a2c Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Tue, 31 Mar 2020 15:35:29 -0500 Subject: [PATCH 1457/3170] combine yaml test functionality into single module --- tests/functional/test_yaml.py | 56 +++++++++++++++++++++++++++++++---- tests/lib/yaml_helpers.py | 43 --------------------------- 2 files changed, 50 insertions(+), 49 deletions(-) delete mode 100644 tests/lib/yaml_helpers.py diff --git a/tests/functional/test_yaml.py b/tests/functional/test_yaml.py index eab11e2a42a..2a1b93f16db 100644 --- a/tests/functional/test_yaml.py +++ b/tests/functional/test_yaml.py @@ -1,15 +1,17 @@ -"""Tests for the resolver +""" +Tests for the resolver """ import os import re import pytest +import yaml from tests.lib import DATA_DIR, create_basic_wheel_for_package, path_to_url -from tests.lib.yaml_helpers import generate_yaml_tests, id_func -_conflict_finder_re = re.compile( + +conflict_finder_re = re.compile( # Conflicting Requirements: \ # A 1.0.0 requires B == 2.0.0, C 1.0.0 requires B == 1.0.0. r""" @@ -24,7 +26,49 @@ ) -def _convert_to_dict(string): +def generate_yaml_tests(directory): + """ + Generate yaml test cases from the yaml files in the given directory + """ + for yml_file in directory.glob("*/*.yml"): + data = yaml.safe_load(yml_file.read_text()) + assert "cases" in data, "A fixture needs cases to be used in testing" + + # Strip the parts of the directory to only get a name without + # extension and resolver directory + base_name = str(yml_file)[len(str(directory)) + 1:-4] + + base = data.get("base", {}) + cases = data["cases"] + + for i, case_template in enumerate(cases): + case = base.copy() + case.update(case_template) + + case[":name:"] = base_name + if len(cases) > 1: + case[":name:"] += "-" + str(i) + + if case.pop("skip", False): + case = pytest.param(case, marks=pytest.mark.xfail) + + yield case + + +def id_func(param): + """ + Give a nice parameter name to the generated function parameters + """ + if isinstance(param, dict) and ":name:" in param: + return param[":name:"] + + retval = str(param) + if len(retval) > 25: + retval = retval[:20] + "..." + retval[-2:] + return retval + + +def convert_to_dict(string): def stripping_split(my_str, splitwith, count=None): if count is None: @@ -89,7 +133,7 @@ def handle_install_request(script, requirement): message = result.stderr.rsplit("\n", 1)[-1] # XXX: There might be a better way than parsing the message - for match in re.finditer(message, _conflict_finder_re): + for match in re.finditer(message, conflict_finder_re): di = match.groupdict() retval["conflicting"].append( { @@ -119,7 +163,7 @@ def test_yaml_based(script, case): # XXX: This doesn't work because this isn't making an index of files. for package in available: if isinstance(package, str): - package = _convert_to_dict(package) + package = convert_to_dict(package) assert isinstance(package, dict), "Needs to be a dictionary" diff --git a/tests/lib/yaml_helpers.py b/tests/lib/yaml_helpers.py deleted file mode 100644 index 73770632099..00000000000 --- a/tests/lib/yaml_helpers.py +++ /dev/null @@ -1,43 +0,0 @@ -""" -""" - -import pytest -import yaml - - -def generate_yaml_tests(directory): - for yml_file in directory.glob("*/*.yml"): - data = yaml.safe_load(yml_file.read_text()) - assert "cases" in data, "A fixture needs cases to be used in testing" - - # Strip the parts of the directory to only get a name without - # extension and resolver directory - base_name = str(yml_file)[len(str(directory)) + 1:-4] - - base = data.get("base", {}) - cases = data["cases"] - - for i, case_template in enumerate(cases): - case = base.copy() - case.update(case_template) - - case[":name:"] = base_name - if len(cases) > 1: - case[":name:"] += "-" + str(i) - - if case.pop("skip", False): - case = pytest.param(case, marks=pytest.mark.xfail) - - yield case - - -def id_func(param): - """Give a nice parameter name to the generated function parameters - """ - if isinstance(param, dict) and ":name:" in param: - return param[":name:"] - - retval = str(param) - if len(retval) > 25: - retval = retval[:20] + "..." + retval[-2:] - return retval From 9de02c2b41a53c0eb7d0bf1ca4293e33b9c74b77 Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Tue, 31 Mar 2020 15:47:43 -0500 Subject: [PATCH 1458/3170] better naming of constant --- tests/functional/test_yaml.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/functional/test_yaml.py b/tests/functional/test_yaml.py index 2a1b93f16db..b2fc4539a63 100644 --- a/tests/functional/test_yaml.py +++ b/tests/functional/test_yaml.py @@ -11,7 +11,7 @@ from tests.lib import DATA_DIR, create_basic_wheel_for_package, path_to_url -conflict_finder_re = re.compile( +_conflict_finder_pat = re.compile( # Conflicting Requirements: \ # A 1.0.0 requires B == 2.0.0, C 1.0.0 requires B == 1.0.0. r""" @@ -133,7 +133,7 @@ def handle_install_request(script, requirement): message = result.stderr.rsplit("\n", 1)[-1] # XXX: There might be a better way than parsing the message - for match in re.finditer(message, conflict_finder_re): + for match in re.finditer(message, _conflict_finder_pat): di = match.groupdict() retval["conflicting"].append( { From 83ba23989de94820166a6a330b3efce882dc15e5 Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Tue, 31 Mar 2020 15:55:16 -0500 Subject: [PATCH 1459/3170] remove extra whitespace --- tests/functional/test_yaml.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/functional/test_yaml.py b/tests/functional/test_yaml.py index b2fc4539a63..cef76d2ca69 100644 --- a/tests/functional/test_yaml.py +++ b/tests/functional/test_yaml.py @@ -10,7 +10,6 @@ from tests.lib import DATA_DIR, create_basic_wheel_for_package, path_to_url - _conflict_finder_pat = re.compile( # Conflicting Requirements: \ # A 1.0.0 requires B == 2.0.0, C 1.0.0 requires B == 1.0.0. From e06048cb8bf6e20be503672ed94cad837fc05ca0 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 27 Mar 2020 22:40:05 +0800 Subject: [PATCH 1460/3170] Aggregate helper usages into factory object This prevents us from having to hold multiple references of finder/preparer/make_install_req in each requirement/candidate class. Instead we just pass around the factory object, and let others use the instances on it. --- .../resolution/resolvelib/candidates.py | 41 +++++++------------ .../resolution/resolvelib/factory.py | 18 +++----- .../resolution/resolvelib/requirements.py | 16 ++------ 3 files changed, 23 insertions(+), 52 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index b589649c5f2..a0069864ac7 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -12,12 +12,12 @@ from typing import Any, Optional, Sequence, Set from pip._internal.models.link import Link - from pip._internal.operations.prepare import RequirementPreparer - from pip._internal.resolution.base import InstallRequirementProvider from pip._vendor.packaging.version import _BaseVersion from pip._vendor.pkg_resources import Distribution + from .factory import Factory + logger = logging.getLogger(__name__) @@ -40,22 +40,11 @@ def make_install_req_from_link(link, parent): class LinkCandidate(Candidate): - def __init__( - self, - link, # type: Link - preparer, # type: RequirementPreparer - parent, # type: InstallRequirement - make_install_req, # type: InstallRequirementProvider - ): - # type: (...) -> None + def __init__(self, link, parent, factory): + # type: (Link, InstallRequirement, Factory) -> None self.link = link - self._preparer = preparer + self._factory = factory self._ireq = make_install_req_from_link(link, parent) - self._make_install_req = lambda spec: make_install_req( - spec, - self._ireq - ) - self._name = None # type: Optional[str] self._version = None # type: Optional[_BaseVersion] self._dist = None # type: Optional[Distribution] @@ -90,7 +79,7 @@ def version(self): def dist(self): # type: () -> Distribution if self._dist is None: - abstract_dist = self._preparer.prepare_linked_requirement( + abstract_dist = self._factory.preparer.prepare_linked_requirement( self._ireq ) self._dist = abstract_dist.get_pkg_resources_distribution() @@ -107,7 +96,10 @@ def dist(self): def get_dependencies(self): # type: () -> Sequence[InstallRequirement] - return [self._make_install_req(str(r)) for r in self.dist.requires()] + return [ + self._factory.make_install_req(str(r), self._ireq) + for r in self.dist.requires() + ] def get_install_requirement(self): # type: () -> Optional[InstallRequirement] @@ -138,12 +130,8 @@ class ExtrasCandidate(LinkCandidate): version 2.0. Having those candidates depend on foo=1.0 and foo=2.0 respectively forces the resolver to recognise that this is a conflict. """ - def __init__( - self, - base, # type: LinkCandidate - extras, # type: Set[str] - ): - # type: (...) -> None + def __init__(self, base, extras): + # type: (LinkCandidate, Set[str]) -> None self.base = base self.extras = extras self.link = base.link @@ -161,6 +149,7 @@ def version(self): def get_dependencies(self): # type: () -> Sequence[InstallRequirement] + factory = self.base._factory # The user may have specified extras that the candidate doesn't # support. We ignore any unsupported extras here. @@ -174,13 +163,13 @@ def get_dependencies(self): ) deps = [ - self.base._make_install_req(str(r)) + factory.make_install_req(str(r), self.base._ireq) for r in self.base.dist.requires(valid_extras) ] # Add a dependency on the exact base. # (See note 2b in the class docstring) spec = "{}=={}".format(self.base.name, self.base.version) - deps.append(self.base._make_install_req(spec)) + deps.append(factory.make_install_req(spec, self.base._ireq)) return deps def get_install_requirement(self): diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 5f8cf47a4ed..0b682181efa 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -23,9 +23,9 @@ def __init__( make_install_req, # type: InstallRequirementProvider ): # type: (...) -> None - self._finder = finder - self._preparer = preparer - self._make_install_req = make_install_req + self.finder = finder + self.preparer = preparer + self.make_install_req = make_install_req self._candidate_cache = {} # type: Dict[Link, LinkCandidate] def make_candidate( @@ -37,10 +37,7 @@ def make_candidate( # type: (...) -> Candidate if link not in self._candidate_cache: self._candidate_cache[link] = LinkCandidate( - link, - self._preparer, - parent=parent, - make_install_req=self._make_install_req + link, parent, factory=self, ) base = self._candidate_cache[link] if extras: @@ -53,9 +50,4 @@ def make_requirement(self, ireq): cand = self.make_candidate(ireq.link, extras=set(), parent=ireq) return ExplicitRequirement(cand) else: - return SpecifierRequirement( - ireq, - finder=self._finder, - factory=self, - make_install_req=self._make_install_req, - ) + return SpecifierRequirement(ireq, factory=self) diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py index a38c4fe2ea0..1fe328597cb 100644 --- a/src/pip/_internal/resolution/resolvelib/requirements.py +++ b/src/pip/_internal/resolution/resolvelib/requirements.py @@ -7,9 +7,7 @@ if MYPY_CHECK_RUNNING: from typing import Sequence - from pip._internal.index.package_finder import PackageFinder from pip._internal.req.req_install import InstallRequirement - from pip._internal.resolution.base import InstallRequirementProvider from .base import Candidate from .factory import Factory @@ -36,19 +34,11 @@ def is_satisfied_by(self, candidate): class SpecifierRequirement(Requirement): - def __init__( - self, - ireq, # type: InstallRequirement - finder, # type: PackageFinder - factory, # type: Factory - make_install_req # type: InstallRequirementProvider - ): - # type: (...) -> None + def __init__(self, ireq, factory): + # type: (InstallRequirement, Factory) -> None assert ireq.link is None, "This is a link, not a specifier" self._ireq = ireq self._factory = factory - self._finder = finder - self._make_install_req = make_install_req self.extras = ireq.req.extras @property @@ -59,7 +49,7 @@ def name(self): def find_matches(self): # type: () -> Sequence[Candidate] - found = self._finder.find_best_candidate( + found = self._factory.finder.find_best_candidate( project_name=self._ireq.req.name, specifier=self._ireq.req.specifier, hashes=self._ireq.hashes(trust_internet=False), From 4491f38047e397d8ed9b8e27839c69cad0c35d6e Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 27 Mar 2020 22:42:26 +0800 Subject: [PATCH 1461/3170] Return Requirement directly from get_dependencies This is possible now we have the factory construct. --- src/pip/_internal/resolution/resolvelib/base.py | 2 +- src/pip/_internal/resolution/resolvelib/candidates.py | 11 +++++++---- src/pip/_internal/resolution/resolvelib/provider.py | 5 +---- 3 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/base.py b/src/pip/_internal/resolution/resolvelib/base.py index de3299496a5..5f99618ce95 100644 --- a/src/pip/_internal/resolution/resolvelib/base.py +++ b/src/pip/_internal/resolution/resolvelib/base.py @@ -44,7 +44,7 @@ def version(self): raise NotImplementedError("Override in subclass") def get_dependencies(self): - # type: () -> Sequence[InstallRequirement] + # type: () -> Sequence[Requirement] raise NotImplementedError("Override in subclass") def get_install_requirement(self): diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index a0069864ac7..e3cee8d450b 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -16,6 +16,7 @@ from pip._vendor.packaging.version import _BaseVersion from pip._vendor.pkg_resources import Distribution + from .base import Requirement from .factory import Factory @@ -95,9 +96,11 @@ def dist(self): return self._dist def get_dependencies(self): - # type: () -> Sequence[InstallRequirement] + # type: () -> Sequence[Requirement] return [ - self._factory.make_install_req(str(r), self._ireq) + self._factory.make_requirement( + self._factory.make_install_req(str(r), self._ireq), + ) for r in self.dist.requires() ] @@ -148,7 +151,7 @@ def version(self): return self.base.version def get_dependencies(self): - # type: () -> Sequence[InstallRequirement] + # type: () -> Sequence[Requirement] factory = self.base._factory # The user may have specified extras that the candidate doesn't @@ -170,7 +173,7 @@ def get_dependencies(self): # (See note 2b in the class docstring) spec = "{}=={}".format(self.base.name, self.base.version) deps.append(factory.make_install_req(spec, self.base._ireq)) - return deps + return [factory.make_requirement(r) for r in deps] def get_install_requirement(self): # type: () -> Optional[InstallRequirement] diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index f0860fe043b..5c3d210a31a 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -51,7 +51,4 @@ def get_dependencies(self, candidate): # type: (Candidate) -> Sequence[Requirement] if self._ignore_dependencies: return [] - return [ - self._factory.make_requirement(r) - for r in candidate.get_dependencies() - ] + return candidate.get_dependencies() From 32c46403c5a14d060d2ae2dba0ce29c4fa674e85 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 1 Apr 2020 16:00:48 +0800 Subject: [PATCH 1462/3170] Fix factory fixture for wheel_cache refactoring --- tests/unit/resolution_resolvelib/conftest.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/unit/resolution_resolvelib/conftest.py b/tests/unit/resolution_resolvelib/conftest.py index c92907130bb..9642f00a797 100644 --- a/tests/unit/resolution_resolvelib/conftest.py +++ b/tests/unit/resolution_resolvelib/conftest.py @@ -53,7 +53,6 @@ def factory(finder, preparer): make_install_req = partial( install_req_from_req_string, isolated=False, - wheel_cache=None, use_pep517=None, ) yield Factory( From d29c86af98fb753b58b533e9e6725467fa78950e Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 1 Apr 2020 16:14:44 +0800 Subject: [PATCH 1463/3170] Add method to create requirement from spec --- .../resolution/resolvelib/candidates.py | 10 ++++------ .../resolution/resolvelib/factory.py | 9 +++++++-- .../resolution/resolvelib/resolver.py | 5 ++++- tests/unit/resolution_resolvelib/conftest.py | 11 ++--------- .../resolution_resolvelib/test_requirement.py | 19 +++++++------------ 5 files changed, 24 insertions(+), 30 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index e3cee8d450b..d2acfe1e144 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -98,9 +98,7 @@ def dist(self): def get_dependencies(self): # type: () -> Sequence[Requirement] return [ - self._factory.make_requirement( - self._factory.make_install_req(str(r), self._ireq), - ) + self._factory.make_requirement_from_spec(str(r), self._ireq) for r in self.dist.requires() ] @@ -166,14 +164,14 @@ def get_dependencies(self): ) deps = [ - factory.make_install_req(str(r), self.base._ireq) + factory.make_requirement_from_spec(str(r), self.base._ireq) for r in self.base.dist.requires(valid_extras) ] # Add a dependency on the exact base. # (See note 2b in the class docstring) spec = "{}=={}".format(self.base.name, self.base.version) - deps.append(factory.make_install_req(spec, self.base._ireq)) - return [factory.make_requirement(r) for r in deps] + deps.append(factory.make_requirement_from_spec(spec, self.base._ireq)) + return deps def get_install_requirement(self): # type: () -> Optional[InstallRequirement] diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 0b682181efa..b5a490e6ab0 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -25,7 +25,7 @@ def __init__( # type: (...) -> None self.finder = finder self.preparer = preparer - self.make_install_req = make_install_req + self._make_install_req_from_spec = make_install_req self._candidate_cache = {} # type: Dict[Link, LinkCandidate] def make_candidate( @@ -44,10 +44,15 @@ def make_candidate( return ExtrasCandidate(base, extras) return base - def make_requirement(self, ireq): + def make_requirement_from_install_req(self, ireq): # type: (InstallRequirement) -> Requirement if ireq.link: cand = self.make_candidate(ireq.link, extras=set(), parent=ireq) return ExplicitRequirement(cand) else: return SpecifierRequirement(ireq, factory=self) + + def make_requirement_from_spec(self, specifier, comes_from): + # type: (str, InstallRequirement) -> Requirement + ireq = self._make_install_req_from_spec(specifier, comes_from) + return self.make_requirement_from_install_req(ireq) diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 1f332d4ed79..7c8be9df58d 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -56,7 +56,10 @@ def resolve(self, root_reqs, check_supported_wheels): reporter = BaseReporter() resolver = RLResolver(provider, reporter) - requirements = [self.factory.make_requirement(r) for r in root_reqs] + requirements = [ + self.factory.make_requirement_from_install_req(r) + for r in root_reqs + ] self._result = resolver.resolve(requirements) req_set = RequirementSet(check_supported_wheels=check_supported_wheels) diff --git a/tests/unit/resolution_resolvelib/conftest.py b/tests/unit/resolution_resolvelib/conftest.py index 9642f00a797..de268e829a1 100644 --- a/tests/unit/resolution_resolvelib/conftest.py +++ b/tests/unit/resolution_resolvelib/conftest.py @@ -1,5 +1,3 @@ -from functools import partial - import pytest from pip._internal.cli.req_command import RequirementCommand @@ -10,7 +8,7 @@ from pip._internal.models.search_scope import SearchScope from pip._internal.models.selection_prefs import SelectionPreferences from pip._internal.network.session import PipSession -from pip._internal.req.constructors import install_req_from_req_string +from pip._internal.req.constructors import install_req_from_line from pip._internal.req.req_tracker import get_requirement_tracker from pip._internal.resolution.resolvelib.factory import Factory from pip._internal.resolution.resolvelib.provider import PipProvider @@ -50,15 +48,10 @@ def preparer(finder): @pytest.fixture def factory(finder, preparer): - make_install_req = partial( - install_req_from_req_string, - isolated=False, - use_pep517=None, - ) yield Factory( finder=finder, preparer=preparer, - make_install_req=make_install_req, + make_install_req=install_req_from_line, ) diff --git a/tests/unit/resolution_resolvelib/test_requirement.py b/tests/unit/resolution_resolvelib/test_requirement.py index 25252dd4acb..ad54df00b42 100644 --- a/tests/unit/resolution_resolvelib/test_requirement.py +++ b/tests/unit/resolution_resolvelib/test_requirement.py @@ -1,7 +1,6 @@ import pytest from pip._vendor.resolvelib import BaseReporter, Resolver -from pip._internal.req.constructors import install_req_from_line from pip._internal.resolution.resolvelib.base import Candidate from pip._internal.utils.urls import path_to_url @@ -50,25 +49,22 @@ def data_url(name): def test_new_resolver_requirement_has_name(test_cases, factory): """All requirements should have a name""" - for requirement, name, matches in test_cases: - ireq = install_req_from_line(requirement) - req = factory.make_requirement(ireq) + for spec, name, matches in test_cases: + req = factory.make_requirement_from_spec(spec, comes_from=None) assert req.name == name def test_new_resolver_correct_number_of_matches(test_cases, factory): """Requirements should return the correct number of candidates""" - for requirement, name, matches in test_cases: - ireq = install_req_from_line(requirement) - req = factory.make_requirement(ireq) + for spec, name, matches in test_cases: + req = factory.make_requirement_from_spec(spec, comes_from=None) assert len(req.find_matches()) == matches def test_new_resolver_candidates_match_requirement(test_cases, factory): """Candidates returned from find_matches should satisfy the requirement""" - for requirement, name, matches in test_cases: - ireq = install_req_from_line(requirement) - req = factory.make_requirement(ireq) + for spec, name, matches in test_cases: + req = factory.make_requirement_from_spec(spec, comes_from=None) for c in req.find_matches(): assert isinstance(c, Candidate) assert req.is_satisfied_by(c) @@ -76,8 +72,7 @@ def test_new_resolver_candidates_match_requirement(test_cases, factory): def test_new_resolver_full_resolve(factory, provider): """A very basic full resolve""" - ireq = install_req_from_line("simplewheel") - req = factory.make_requirement(ireq) + req = factory.make_requirement_from_spec("simplewheel", comes_from=None) r = Resolver(provider, BaseReporter()) result = r.resolve([req]) assert set(result.mapping.keys()) == {'simplewheel'} From 8f0dbec5734c5197c3b7070987814b584e3f31a6 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Wed, 1 Apr 2020 19:38:41 +0530 Subject: [PATCH 1464/3170] Updated clarification on unsafe urls --- docs/html/reference/pip_install.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index 71755730328..08149a76773 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -406,9 +406,9 @@ pip currently supports cloning over ``git``, ``git+http``, ``git+https``, .. warning:: - Note that the ``git``, ``git+git``,and ``git+http`` are not recommended. - (The former two use `the Git Protocol`_, which lacks authentication, and HTTP is - insecure due to lack of TLS based encryption) + Note that the use of ``git``, ``git+git``, and ``git+http`` is discouraged. + The former two use `the Git Protocol`_, which lacks authentication, and HTTP is + insecure due to lack of TLS based encryption. Here are the supported forms:: From 209c74f6902e32c89977555f59adc883e80a36ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Sun, 29 Mar 2020 17:19:34 +0700 Subject: [PATCH 1465/3170] Use better temporary files mechanism --- news/7699.bugfix | 2 + src/pip/_internal/operations/install/wheel.py | 54 +++++++++---------- src/pip/_internal/utils/filesystem.py | 16 ++++-- tests/unit/test_wheel.py | 2 +- 4 files changed, 40 insertions(+), 34 deletions(-) create mode 100644 news/7699.bugfix diff --git a/news/7699.bugfix b/news/7699.bugfix new file mode 100644 index 00000000000..51dbef88fda --- /dev/null +++ b/news/7699.bugfix @@ -0,0 +1,2 @@ +Use better mechanism for handling temporary files, when recording metadata +about installed files (RECORD) and the installer (INSTALLER). diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 329d90c67e6..e66d12b4bf0 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -27,6 +27,7 @@ from pip._internal.exceptions import InstallationError from pip._internal.locations import get_major_minor_version +from pip._internal.utils.filesystem import adjacent_tmp_file, replace from pip._internal.utils.misc import captured_stdout, ensure_dir, hash_file from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -36,7 +37,7 @@ if MYPY_CHECK_RUNNING: from email.message import Message from typing import ( - Dict, List, Optional, Sequence, Tuple, IO, Text, Any, + Dict, List, Optional, Sequence, Tuple, Any, Iterable, Callable, Set, ) @@ -64,15 +65,15 @@ def rehash(path, blocksize=1 << 20): return (digest, str(length)) # type: ignore -def open_for_csv(name, mode): - # type: (str, Text) -> IO[Any] - if sys.version_info[0] < 3: - nl = {} # type: Dict[str, Any] - bin = 'b' +def csv_io_kwargs(mode): + # type: (str) -> Dict[str, Any] + """Return keyword arguments to properly open a CSV file + in the given mode. + """ + if sys.version_info.major < 3: + return {'mode': '{}b'.format(mode)} else: - nl = {'newline': ''} # type: Dict[str, Any] - bin = '' - return open(name, mode + bin, **nl) + return {'mode': mode, 'newline': ''} def fix_script(path): @@ -563,28 +564,25 @@ def is_entrypoint_wrapper(name): logger.warning(msg) # Record pip as the installer - installer = os.path.join(dest_info_dir, 'INSTALLER') - temp_installer = os.path.join(dest_info_dir, 'INSTALLER.pip') - with open(temp_installer, 'wb') as installer_file: + installer_path = os.path.join(dest_info_dir, 'INSTALLER') + with adjacent_tmp_file(installer_path) as installer_file: installer_file.write(b'pip\n') - shutil.move(temp_installer, installer) - generated.append(installer) + replace(installer_file.name, installer_path) + generated.append(installer_path) # Record details of all files installed - record = os.path.join(dest_info_dir, 'RECORD') - temp_record = os.path.join(dest_info_dir, 'RECORD.pip') - with open_for_csv(record, 'r') as record_in: - with open_for_csv(temp_record, 'w+') as record_out: - reader = csv.reader(record_in) - outrows = get_csv_rows_for_installed( - reader, installed=installed, changed=changed, - generated=generated, lib_dir=lib_dir, - ) - writer = csv.writer(record_out) - # Sort to simplify testing. - for row in sorted_outrows(outrows): - writer.writerow(row) - shutil.move(temp_record, record) + record_path = os.path.join(dest_info_dir, 'RECORD') + with open(record_path, **csv_io_kwargs('r')) as record_file: + rows = get_csv_rows_for_installed( + csv.reader(record_file), + installed=installed, + changed=changed, + generated=generated, + lib_dir=lib_dir) + with adjacent_tmp_file(record_path, **csv_io_kwargs('w')) as record_file: + writer = csv.writer(record_file) + writer.writerows(sorted_outrows(rows)) # sort to simplify testing + replace(record_file.name, record_path) def install_wheel( diff --git a/src/pip/_internal/utils/filesystem.py b/src/pip/_internal/utils/filesystem.py index 161c450aafd..36578fb6244 100644 --- a/src/pip/_internal/utils/filesystem.py +++ b/src/pip/_internal/utils/filesystem.py @@ -17,7 +17,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING, cast if MYPY_CHECK_RUNNING: - from typing import BinaryIO, Iterator + from typing import Any, BinaryIO, Iterator class NamedTemporaryFileResult(BinaryIO): @property @@ -85,16 +85,22 @@ def is_socket(path): @contextmanager -def adjacent_tmp_file(path): - # type: (str) -> Iterator[NamedTemporaryFileResult] - """Given a path to a file, open a temp file next to it securely and ensure - it is written to disk after the context reaches its end. +def adjacent_tmp_file(path, **kwargs): + # type: (str, **Any) -> Iterator[NamedTemporaryFileResult] + """Return a file-like object pointing to a tmp file next to path. + + The file is created securely and is ensured to be written to disk + after the context reaches its end. + + kwargs will be passed to tempfile.NamedTemporaryFile to control + the way the temporary file will be opened. """ with NamedTemporaryFile( delete=False, dir=os.path.dirname(path), prefix=os.path.basename(path), suffix='.tmp', + **kwargs ) as f: result = cast('NamedTemporaryFileResult', f) try: diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 05300b96438..9328f6fb8bc 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -141,7 +141,7 @@ def call_get_csv_rows_for_installed(tmpdir, text): generated = [] lib_dir = '/lib/dir' - with wheel.open_for_csv(path, 'r') as f: + with open(path, **wheel.csv_io_kwargs('r')) as f: reader = csv.reader(f) outrows = wheel.get_csv_rows_for_installed( reader, installed=installed, changed=changed, From 09f4d0004b505bf36445102ee13b755c79e2521b Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 2 Apr 2020 01:25:05 +0800 Subject: [PATCH 1466/3170] Use extra_files to write package files instead --- tests/lib/__init__.py | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index 0c032388770..67c66b9ce22 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -991,6 +991,16 @@ def create_basic_wheel_for_package( archive_name = "{}-{}-py2.py3-none-any.whl".format(name, version) archive_path = script.scratch_path / archive_name + package_init_py = "{name}/__init__.py".format(name=name) + assert package_init_py not in extra_files + extra_files[package_init_py] = textwrap.dedent( + """ + __version__ = {version!r} + def hello(): + return "Hello From {name}" + """, + ).format(version=version, name=name) + requires_dist = depends + [ '{package}; extra == "{extra}"'.format(package=package, extra=extra) for extra, packages in extras.items() @@ -1006,26 +1016,13 @@ def create_basic_wheel_for_package( "Requires-Dist": requires_dist, }, extra_metadata_files={"top_level.txt": name}, + extra_files=extra_files, # Have an empty RECORD because we don't want to be checking hashes. record="", ) + wheel_builder.save_to(archive_path) - # Create the wheel in-memory to add more files. - wheel_io = BytesIO(wheel_builder.as_bytes()) - with ZipFile(wheel_io, "a") as zf: - zf.writestr( - "{name}/__init__.py".format(name=name), - textwrap.dedent(""" - __version__ = {version!r} - def hello(): - return "Hello From {name}" - """).format(version=version, name=name), - ) - for fn, content in extra_files.items(): - zf.writestr(fn, content) - - archive_path.write_bytes(wheel_io.getvalue()) return archive_path From 28389e83384cf9ea7477a145aaaf84d1b8d6f032 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 2 Apr 2020 01:16:25 +0530 Subject: [PATCH 1467/3170] Simplify our Linting GitHub Action --- .github/workflows/linting.yml | 54 ++++++++++++++++ .github/workflows/python-linters.yml | 97 ---------------------------- 2 files changed, 54 insertions(+), 97 deletions(-) create mode 100644 .github/workflows/linting.yml delete mode 100644 .github/workflows/python-linters.yml diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml new file mode 100644 index 00000000000..512969b0dff --- /dev/null +++ b/.github/workflows/linting.yml @@ -0,0 +1,54 @@ +name: Linting + +on: + push: + pull_request: + schedule: + # Run every Friday at 18:02 UTC + - cron: 2 18 * * 5 + +jobs: + lint: + name: ${{ matrix.os }} + runs-on: ${{ matrix.os }}-latest + env: + TOXENV: lint,docs,vendoring + + strategy: + matrix: + os: + - Ubuntu + - Windows + - MacOS + + steps: + - uses: actions/checkout@v2 + - name: Set up Python 3.8 + uses: actions/setup-python@v1 + with: + python-version: 3.8 + + # Setup Caching + - name: pip cache + uses: actions/cache@v1 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('tools/requirements/tests.txt') }}-${{ hashFiles('tools/requirements/docs.txt') }}-${{ hashFiles('tox.ini') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + + - name: Set PY (for pre-commit cache) + run: echo "::set-env name=PY::$(python -c 'import hashlib, sys;print(hashlib.sha256(sys.version.encode()+sys.executable.encode()).hexdigest())')" + - name: pre-commit cache + uses: actions/cache@v1 + with: + path: ~/.cache/pre-commit + key: pre-commit|2020-02-14|${{ env.PY }}|${{ hashFiles('.pre-commit-config.yaml') }} + + # Get the latest tox + - name: Install tox + run: python -m pip install tox + + # Main check + - run: python -m tox diff --git a/.github/workflows/python-linters.yml b/.github/workflows/python-linters.yml deleted file mode 100644 index d5124e226ea..00000000000 --- a/.github/workflows/python-linters.yml +++ /dev/null @@ -1,97 +0,0 @@ -name: >- - 🤖 - Code quality - -on: - push: - pull_request: - schedule: - # Run every Friday at 18:02 UTC - # https://crontab.guru/#2_18_*_*_5 - - cron: 2 18 * * 5 - -jobs: - linters: - name: >- - ${{ matrix.env.TOXENV }}/${{ matrix.python-version }}@${{ matrix.os }} - runs-on: ${{ matrix.os }} - strategy: - # max-parallel: 5 - matrix: - python-version: - - 3.8 - os: - - ubuntu-latest - - windows-latest - - macos-latest - env: - - TOXENV: docs - - TOXENV: lint - - TOXENV: vendoring - - env: - TOX_PARALLEL_NO_SPINNER: 1 - - steps: - - uses: actions/checkout@master - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v1 - with: - python-version: ${{ matrix.python-version }} - - name: Log Python version - run: >- - python --version - - name: Log Python location - run: >- - which python - - name: Log Python env - run: >- - python -m sysconfig - - name: Pip cache - uses: actions/cache@v1 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('tools/requirements/tests.txt') }}-${{ hashFiles('tools/requirements/docs.txt') }}-${{ hashFiles('tox.ini') }} - restore-keys: | - ${{ runner.os }}-pip- - ${{ runner.os }}- - - name: set PY - run: echo "::set-env name=PY::$(python -c 'import hashlib, sys;print(hashlib.sha256(sys.version.encode()+sys.executable.encode()).hexdigest())')" - - uses: actions/cache@v1 - with: - path: ~/.cache/pre-commit - key: pre-commit|2020-02-14|${{ env.PY }}|${{ hashFiles('.pre-commit-config.yaml') }} - - name: Pre-configure global Git settings - run: | - git config --global user.email "pypa-dev@googlegroups.com" - git config --global user.name "pip" - - name: Update setuptools - run: >- - python -m pip install --upgrade setuptools - - name: Install tox - run: >- - python -m pip install --upgrade tox tox-venv - - name: Log the list of packages - run: >- - python -m pip freeze --all - - name: 'Initialize tox envs: ${{ matrix.env.TOXENV }}' - run: >- - python -m - tox - --parallel auto - --notest - --skip-missing-interpreters false - env: ${{ matrix.env }} - - name: Pre-fetch pre-commit hooks - # This is to separate test step from deps install - if: matrix.env.TOXENV == 'lint' - run: >- - .tox/lint/${{ runner.os == 'Windows' && 'Scripts' || 'bin' }}/python -m - pre_commit - install-hooks - - name: Test with tox - run: >- - python -m - tox - --parallel auto - env: ${{ matrix.env }} From 04c0b0e6eb5798240cbaff49479be7892eb34453 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash <me@duckie.co> Date: Mon, 8 Apr 2019 13:00:09 -0400 Subject: [PATCH 1468/3170] Add 'pip cache' command. --- src/pip/_internal/commands/__init__.py | 4 + src/pip/_internal/commands/cache.py | 106 +++++++++++++++++++++++++ src/pip/_internal/utils/filesystem.py | 14 +++- tests/functional/test_cache.py | 68 ++++++++++++++++ 4 files changed, 191 insertions(+), 1 deletion(-) create mode 100644 src/pip/_internal/commands/cache.py create mode 100644 tests/functional/test_cache.py diff --git a/src/pip/_internal/commands/__init__.py b/src/pip/_internal/commands/__init__.py index 2a311f8fc89..8507b6ef9f0 100644 --- a/src/pip/_internal/commands/__init__.py +++ b/src/pip/_internal/commands/__init__.py @@ -64,6 +64,10 @@ 'pip._internal.commands.search', 'SearchCommand', 'Search PyPI for packages.', )), + ('cache', CommandInfo( + 'pip._internal.commands.cache', 'CacheCommand', + "Inspect and manage pip's caches.", + )), ('wheel', CommandInfo( 'pip._internal.commands.wheel', 'WheelCommand', 'Build wheels from your requirements.', diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py new file mode 100644 index 00000000000..0fe3352d22f --- /dev/null +++ b/src/pip/_internal/commands/cache.py @@ -0,0 +1,106 @@ +from __future__ import absolute_import + +import logging +import os +import textwrap + +from pip._internal.cli.base_command import Command +from pip._internal.exceptions import CommandError +from pip._internal.utils.filesystem import find_files + +logger = logging.getLogger(__name__) + + +class CacheCommand(Command): + """ + Inspect and manage pip's caches. + + Subcommands: + info: + Show information about the caches. + list [name]: + List filenames of packages stored in the cache. + remove <pattern>: + Remove one or more package from the cache. + `pattern` can be a glob expression or a package name. + purge: + Remove all items from the cache. + """ + actions = ['info', 'list', 'remove', 'purge'] + name = 'cache' + usage = """ + %prog <command>""" + summary = "View and manage which packages are available in pip's caches." + + def __init__(self, *args, **kw): + super(CacheCommand, self).__init__(*args, **kw) + + def run(self, options, args): + if not args: + raise CommandError('Please provide a subcommand.') + + if args[0] not in self.actions: + raise CommandError('Invalid subcommand: %s' % args[0]) + + self.wheel_dir = os.path.join(options.cache_dir, 'wheels') + + method = getattr(self, 'action_%s' % args[0]) + return method(options, args[1:]) + + def action_info(self, options, args): + format_args = (options.cache_dir, len(self.find_wheels('*.whl'))) + result = textwrap.dedent( + """\ + Cache info: + Location: %s + Packages: %s""" % format_args + ) + logger.info(result) + + def action_list(self, options, args): + if args and args[0]: + pattern = args[0] + else: + pattern = '*' + + files = self.find_wheels(pattern) + wheels = map(self._wheel_info, files) + wheels = sorted(set(wheels)) + + if not wheels: + logger.info('Nothing is currently cached.') + return + + result = 'Current cache contents:\n' + for wheel in wheels: + result += ' - %s\n' % wheel + logger.info(result.strip()) + + def action_remove(self, options, args): + if not args: + raise CommandError('Please provide a pattern') + + files = self.find_wheels(args[0]) + if not files: + raise CommandError('No matching packages') + + wheels = map(self._wheel_info, files) + result = 'Removing cached wheels for:\n' + for wheel in wheels: + result += '- %s\n' % wheel + + for filename in files: + os.unlink(filename) + logger.debug('Removed %s', filename) + logger.info('Removed %s files', len(files)) + + def action_purge(self, options, args): + return self.action_remove(options, '*') + + def _wheel_info(self, path): + filename = os.path.splitext(os.path.basename(path))[0] + name, version = filename.split('-')[0:2] + return '%s-%s' % (name, version) + + def find_wheels(self, pattern): + return find_files(self.wheel_dir, pattern + '-*.whl') diff --git a/src/pip/_internal/utils/filesystem.py b/src/pip/_internal/utils/filesystem.py index 36578fb6244..2772e0880e4 100644 --- a/src/pip/_internal/utils/filesystem.py +++ b/src/pip/_internal/utils/filesystem.py @@ -1,4 +1,5 @@ import errno +import fnmatch import os import os.path import random @@ -17,7 +18,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING, cast if MYPY_CHECK_RUNNING: - from typing import Any, BinaryIO, Iterator + from typing import Any, BinaryIO, Iterator, List class NamedTemporaryFileResult(BinaryIO): @property @@ -176,3 +177,14 @@ def _test_writable_dir_win(path): raise EnvironmentError( 'Unexpected condition testing for writable directory' ) + + +def find_files(path, pattern): + # type: (str, str) -> List[str] + """Returns a list of absolute paths of files beneath path, recursively, + with filenames which match the UNIX-style shell glob pattern.""" + result = [] # type: List[str] + for root, dirs, files in os.walk(path): + matches = fnmatch.filter(files, pattern) + result.extend(os.path.join(root, f) for f in matches) + return result diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py new file mode 100644 index 00000000000..34cd95b836d --- /dev/null +++ b/tests/functional/test_cache.py @@ -0,0 +1,68 @@ +import os +import shutil + +from pip._internal.utils import appdirs + + +def _cache_dir(script): + results = script.run( + 'python', '-c', + 'from pip._internal.locations import USER_CACHE_DIR;' + 'print(USER_CACHE_DIR)' + ) + return str(results.stdout).strip() + + +def test_cache_info(script, monkeypatch): + result = script.pip('cache', 'info') + cache_dir = _cache_dir(script) + + assert 'Location: %s' % cache_dir in result.stdout + assert 'Packages: ' in result.stdout + + +def test_cache_list(script, monkeypatch): + cache_dir = _cache_dir(script) + wheel_cache_dir = os.path.join(cache_dir, 'wheels') + destination = os.path.join(wheel_cache_dir, 'arbitrary', 'pathname') + os.makedirs(destination) + with open(os.path.join(destination, 'yyy-1.2.3.whl'), 'w'): + pass + with open(os.path.join(destination, 'zzz-4.5.6.whl'), 'w'): + pass + result = script.pip('cache', 'list') + assert 'yyy-1.2.3' in result.stdout + assert 'zzz-4.5.6' in result.stdout + shutil.rmtree(os.path.join(wheel_cache_dir, 'arbitrary')) + + +def test_cache_list_with_pattern(script, monkeypatch): + cache_dir = _cache_dir(script) + wheel_cache_dir = os.path.join(cache_dir, 'wheels') + destination = os.path.join(wheel_cache_dir, 'arbitrary', 'pathname') + os.makedirs(destination) + with open(os.path.join(destination, 'yyy-1.2.3.whl'), 'w'): + pass + with open(os.path.join(destination, 'zzz-4.5.6.whl'), 'w'): + pass + result = script.pip('cache', 'list', 'zzz') + assert 'yyy-1.2.3' not in result.stdout + assert 'zzz-4.5.6' in result.stdout + shutil.rmtree(os.path.join(wheel_cache_dir, 'arbitrary')) + + +def test_cache_remove(script, monkeypatch): + cache_dir = _cache_dir(script) + wheel_cache_dir = os.path.join(cache_dir, 'wheels') + destination = os.path.join(wheel_cache_dir, 'arbitrary', 'pathname') + os.makedirs(destination) + with open(os.path.join(wheel_cache_dir, 'yyy-1.2.3.whl'), 'w'): + pass + with open(os.path.join(wheel_cache_dir, 'zzz-4.5.6.whl'), 'w'): + pass + + script.pip('cache', 'remove', expect_error=True) + result = script.pip('cache', 'remove', 'zzz', '--verbose') + assert 'yyy-1.2.3' not in result.stdout + assert 'zzz-4.5.6' in result.stdout + shutil.rmtree(os.path.join(wheel_cache_dir, 'arbitrary')) From b0e7b66326b22984c62965c861190da88c1a79db Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash <me@duckie.co> Date: Mon, 7 Oct 2019 23:03:03 -0400 Subject: [PATCH 1469/3170] [commands/cache] Refactor + fix linting failures. --- src/pip/_internal/commands/cache.py | 112 ++++++++++++++++++---------- tests/functional/test_cache.py | 2 - 2 files changed, 72 insertions(+), 42 deletions(-) diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index 0fe3352d22f..7d5fb85ca64 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -5,50 +5,76 @@ import textwrap from pip._internal.cli.base_command import Command -from pip._internal.exceptions import CommandError +from pip._internal.cli.status_codes import ERROR, SUCCESS +from pip._internal.exceptions import CommandError, PipError from pip._internal.utils.filesystem import find_files +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from optparse import Values + from typing import Any, List + logger = logging.getLogger(__name__) class CacheCommand(Command): - """ - Inspect and manage pip's caches. - - Subcommands: - info: - Show information about the caches. - list [name]: - List filenames of packages stored in the cache. - remove <pattern>: - Remove one or more package from the cache. - `pattern` can be a glob expression or a package name. - purge: - Remove all items from the cache. + """Inspect and manage pip's caches. + + Subcommands: + + info: Show information about the caches. + list: List filenames of packages stored in the cache. + remove: Remove one or more package from the cache. + purge: Remove all items from the cache. + + <pattern> can be a glob expression or a package name. """ actions = ['info', 'list', 'remove', 'purge'] - name = 'cache' usage = """ - %prog <command>""" - summary = "View and manage which packages are available in pip's caches." + %prog info + %prog list [name] + %prog remove <pattern> + %prog purge + """ def __init__(self, *args, **kw): + # type: (*Any, **Any) -> None super(CacheCommand, self).__init__(*args, **kw) def run(self, options, args): - if not args: - raise CommandError('Please provide a subcommand.') - - if args[0] not in self.actions: - raise CommandError('Invalid subcommand: %s' % args[0]) - - self.wheel_dir = os.path.join(options.cache_dir, 'wheels') - - method = getattr(self, 'action_%s' % args[0]) - return method(options, args[1:]) - - def action_info(self, options, args): - format_args = (options.cache_dir, len(self.find_wheels('*.whl'))) + # type: (Values, List[Any]) -> int + handlers = { + "info": self.get_cache_info, + "list": self.list_cache_items, + "remove": self.remove_cache_items, + "purge": self.purge_cache, + } + + # 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] + + # 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 get_cache_info(self, options, args): + # type: (Values, List[Any]) -> None + format_args = ( + options.cache_dir, + len(self._find_wheels(options, '*.whl')) + ) result = textwrap.dedent( """\ Cache info: @@ -57,15 +83,16 @@ def action_info(self, options, args): ) logger.info(result) - def action_list(self, options, args): + def list_cache_items(self, options, args): + # type: (Values, List[Any]) -> None if args and args[0]: pattern = args[0] else: pattern = '*' - files = self.find_wheels(pattern) - wheels = map(self._wheel_info, files) - wheels = sorted(set(wheels)) + files = self._find_wheels(options, pattern) + wheels_ = map(self._wheel_info, files) + wheels = sorted(set(wheels_)) if not wheels: logger.info('Nothing is currently cached.') @@ -76,11 +103,12 @@ def action_list(self, options, args): result += ' - %s\n' % wheel logger.info(result.strip()) - def action_remove(self, options, args): + def remove_cache_items(self, options, args): + # type: (Values, List[Any]) -> None if not args: raise CommandError('Please provide a pattern') - files = self.find_wheels(args[0]) + files = self._find_wheels(options, args[0]) if not files: raise CommandError('No matching packages') @@ -94,13 +122,17 @@ def action_remove(self, options, args): logger.debug('Removed %s', filename) logger.info('Removed %s files', len(files)) - def action_purge(self, options, args): - return self.action_remove(options, '*') + def purge_cache(self, options, args): + # type: (Values, List[Any]) -> None + return self.remove_cache_items(options, ['*']) def _wheel_info(self, path): + # type: (str) -> str filename = os.path.splitext(os.path.basename(path))[0] name, version = filename.split('-')[0:2] return '%s-%s' % (name, version) - def find_wheels(self, pattern): - return find_files(self.wheel_dir, pattern + '-*.whl') + def _find_wheels(self, options, pattern): + # type: (Values, str) -> List[str] + wheel_dir = os.path.join(options.cache_dir, 'wheels') + return find_files(wheel_dir, pattern + '-*.whl') diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index 34cd95b836d..05fcb4505d1 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -1,8 +1,6 @@ import os import shutil -from pip._internal.utils import appdirs - def _cache_dir(script): results = script.run( From b9b29b8c10a18843dfbb0c23a33c073b8de603c2 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash <me@duckie.co> Date: Mon, 7 Oct 2019 23:31:01 -0400 Subject: [PATCH 1470/3170] [commands/cache] fix 'pip cache info'; don't hide python/abi/platform tags. --- src/pip/_internal/commands/cache.py | 11 ++--------- tests/functional/test_cache.py | 3 +++ 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index 7d5fb85ca64..ee669016728 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -73,7 +73,7 @@ def get_cache_info(self, options, args): # type: (Values, List[Any]) -> None format_args = ( options.cache_dir, - len(self._find_wheels(options, '*.whl')) + len(self._find_wheels(options, '*')) ) result = textwrap.dedent( """\ @@ -112,11 +112,6 @@ def remove_cache_items(self, options, args): if not files: raise CommandError('No matching packages') - wheels = map(self._wheel_info, files) - result = 'Removing cached wheels for:\n' - for wheel in wheels: - result += '- %s\n' % wheel - for filename in files: os.unlink(filename) logger.debug('Removed %s', filename) @@ -128,9 +123,7 @@ def purge_cache(self, options, args): def _wheel_info(self, path): # type: (str) -> str - filename = os.path.splitext(os.path.basename(path))[0] - name, version = filename.split('-')[0:2] - return '%s-%s' % (name, version) + return os.path.basename(path) def _find_wheels(self, options, pattern): # type: (Values, str) -> List[str] diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index 05fcb4505d1..5c97cfe1903 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -16,6 +16,9 @@ def test_cache_info(script, monkeypatch): cache_dir = _cache_dir(script) assert 'Location: %s' % cache_dir in result.stdout + # TODO(@duckinator): This should probably test that the number of + # packages is actually correct, but I'm not sure how to do that + # without pretty much re-implementing the entire cache info command. assert 'Packages: ' in result.stdout From c59ced69422cc4831a2c84662bc0c1d6dc7f6ae5 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash <me@duckie.co> Date: Mon, 7 Oct 2019 23:50:47 -0400 Subject: [PATCH 1471/3170] [commands/cache] More refactoring of cache command --- src/pip/_internal/commands/cache.py | 23 ++++++++--------------- 1 file changed, 8 insertions(+), 15 deletions(-) diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index ee669016728..7ab7e6c88f4 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -30,7 +30,7 @@ class CacheCommand(Command): <pattern> can be a glob expression or a package name. """ - actions = ['info', 'list', 'remove', 'purge'] + usage = """ %prog info %prog list [name] @@ -71,17 +71,15 @@ def run(self, options, args): def get_cache_info(self, options, args): # type: (Values, List[Any]) -> None - format_args = ( - options.cache_dir, - len(self._find_wheels(options, '*')) - ) - result = textwrap.dedent( - """\ + num_packages = len(self._find_wheels(options, '*')) + + results = textwrap.dedent("""\ Cache info: Location: %s - Packages: %s""" % format_args + Packages: %s""" % (options.cache_dir, num_packages) ) - logger.info(result) + + logger.info(results) def list_cache_items(self, options, args): # type: (Values, List[Any]) -> None @@ -91,8 +89,7 @@ def list_cache_items(self, options, args): pattern = '*' files = self._find_wheels(options, pattern) - wheels_ = map(self._wheel_info, files) - wheels = sorted(set(wheels_)) + wheels = sorted(set(map(lambda f: os.path.basename(f), files))) if not wheels: logger.info('Nothing is currently cached.') @@ -121,10 +118,6 @@ def purge_cache(self, options, args): # type: (Values, List[Any]) -> None return self.remove_cache_items(options, ['*']) - def _wheel_info(self, path): - # type: (str) -> str - return os.path.basename(path) - def _find_wheels(self, options, pattern): # type: (Values, str) -> List[str] wheel_dir = os.path.join(options.cache_dir, 'wheels') From 8ae71adbea407f4cca5baba31e9e6687571cc8f4 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash <me@duckie.co> Date: Mon, 7 Oct 2019 23:59:30 -0400 Subject: [PATCH 1472/3170] [commands/cache] Add docs for 'pip cache' command. --- docs/man/commands/cache.rst | 20 ++++++++++++++++++++ src/pip/_internal/commands/cache.py | 15 ++++++++++----- 2 files changed, 30 insertions(+), 5 deletions(-) create mode 100644 docs/man/commands/cache.rst diff --git a/docs/man/commands/cache.rst b/docs/man/commands/cache.rst new file mode 100644 index 00000000000..b0241c8135e --- /dev/null +++ b/docs/man/commands/cache.rst @@ -0,0 +1,20 @@ +:orphan: + +========== +pip-cache +========== + +Description +*********** + +.. pip-command-description:: cache + +Usage +***** + +.. pip-command-usage:: cache + +Options +******* + +.. pip-command-options:: cache diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index 7ab7e6c88f4..8dadc6a0e33 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -19,14 +19,19 @@ class CacheCommand(Command): - """Inspect and manage pip's caches. + """ + Inspect and manage pip's caches. Subcommands: - info: Show information about the caches. - list: List filenames of packages stored in the cache. - remove: Remove one or more package from the cache. - purge: Remove all items from the cache. + info: + Show information about the caches. + list: + List filenames of packages stored in the cache. + remove: + Remove one or more package from the cache. + purge: + Remove all items from the cache. <pattern> can be a glob expression or a package name. """ From d57dcd934e11e069bf4bd37e8dff8d777b452217 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash <me@duckie.co> Date: Tue, 8 Oct 2019 00:00:24 -0400 Subject: [PATCH 1473/3170] [commands/cache] Add news file for cache command. --- news/6391.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/6391.feature diff --git a/news/6391.feature b/news/6391.feature new file mode 100644 index 00000000000..73d6dffca8a --- /dev/null +++ b/news/6391.feature @@ -0,0 +1 @@ +Add ``pip cache`` command for inspecting/managing pip's cache. From 50604be6c4908f481e4ca1ea17cfaa9ec06ca417 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash <me@duckie.co> Date: Wed, 9 Oct 2019 19:58:23 -0400 Subject: [PATCH 1474/3170] [commands/cache] Raise errors if wrong number of args. Also add tests for purge_cache, since I apparently forgot those before. --- src/pip/_internal/commands/cache.py | 11 ++++++++- tests/functional/test_cache.py | 36 +++++++++++++++++++++++++++-- 2 files changed, 44 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index 8dadc6a0e33..8afe5be53d5 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -88,7 +88,10 @@ def get_cache_info(self, options, args): def list_cache_items(self, options, args): # type: (Values, List[Any]) -> None - if args and args[0]: + if len(args) > 1: + raise CommandError('Too many arguments') + + if args: pattern = args[0] else: pattern = '*' @@ -107,6 +110,9 @@ def list_cache_items(self, options, args): def remove_cache_items(self, options, args): # type: (Values, List[Any]) -> None + if len(args) > 1: + raise CommandError('Too many arguments') + if not args: raise CommandError('Please provide a pattern') @@ -121,6 +127,9 @@ def remove_cache_items(self, options, args): def purge_cache(self, options, args): # type: (Values, List[Any]) -> None + if args: + raise CommandError('Too many arguments') + return self.remove_cache_items(options, ['*']) def _find_wheels(self, options, pattern): diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index 5c97cfe1903..6388155df2a 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -3,12 +3,12 @@ def _cache_dir(script): - results = script.run( + result = script.run( 'python', '-c', 'from pip._internal.locations import USER_CACHE_DIR;' 'print(USER_CACHE_DIR)' ) - return str(results.stdout).strip() + return result.stdout.strip() def test_cache_info(script, monkeypatch): @@ -37,6 +37,11 @@ def test_cache_list(script, monkeypatch): shutil.rmtree(os.path.join(wheel_cache_dir, 'arbitrary')) +def test_cache_list_too_many_args(script, monkeypatch): + script.pip('cache', 'list', 'aaa', 'bbb', + expect_error=True) + + def test_cache_list_with_pattern(script, monkeypatch): cache_dir = _cache_dir(script) wheel_cache_dir = os.path.join(cache_dir, 'wheels') @@ -67,3 +72,30 @@ def test_cache_remove(script, monkeypatch): assert 'yyy-1.2.3' not in result.stdout assert 'zzz-4.5.6' in result.stdout shutil.rmtree(os.path.join(wheel_cache_dir, 'arbitrary')) + + +def test_cache_remove_too_many_args(script, monkeypatch): + result = script.pip('cache', 'remove', 'aaa', 'bbb', + expect_error=True) + + +def test_cache_purge(script, monkeypatch): + cache_dir = _cache_dir(script) + wheel_cache_dir = os.path.join(cache_dir, 'wheels') + destination = os.path.join(wheel_cache_dir, 'arbitrary', 'pathname') + os.makedirs(destination) + with open(os.path.join(wheel_cache_dir, 'yyy-1.2.3.whl'), 'w'): + pass + with open(os.path.join(wheel_cache_dir, 'zzz-4.5.6.whl'), 'w'): + pass + + result = script.pip('cache', 'purge', 'aaa', '--verbose', + expect_error=True) + assert 'yyy-1.2.3' not in result.stdout + assert 'zzz-4.5.6' not in result.stdout + + result = script.pip('cache', 'purge', '--verbose') + assert 'yyy-1.2.3' in result.stdout + assert 'zzz-4.5.6' in result.stdout + + shutil.rmtree(os.path.join(wheel_cache_dir, 'arbitrary')) From 9563dfb526db14b1c9a00c66a25e1a7ec4d64e78 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash <me@duckie.co> Date: Wed, 9 Oct 2019 20:00:34 -0400 Subject: [PATCH 1475/3170] [commands/cache] Refactor get_cache_info(). --- src/pip/_internal/commands/cache.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index 8afe5be53d5..8e6f12f4c94 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -78,13 +78,16 @@ def get_cache_info(self, options, args): # type: (Values, List[Any]) -> None num_packages = len(self._find_wheels(options, '*')) - results = textwrap.dedent("""\ + message = textwrap.dedent(""" Cache info: - Location: %s - Packages: %s""" % (options.cache_dir, num_packages) - ) - - logger.info(results) + Location: {location} + Packages: {package_count} + """).format( + location=options.cache_dir, + package_count=num_packages, + ).strip() + + logger.info(message) def list_cache_items(self, options, args): # type: (Values, List[Any]) -> None From 6fb1ee7a3d9c19995262f1961c7fb39fd459396e Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash <me@duckie.co> Date: Thu, 10 Oct 2019 04:30:08 -0400 Subject: [PATCH 1476/3170] [commands/cache] fix linting error. --- tests/functional/test_cache.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index 6388155df2a..745a6e2a772 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -75,8 +75,8 @@ def test_cache_remove(script, monkeypatch): def test_cache_remove_too_many_args(script, monkeypatch): - result = script.pip('cache', 'remove', 'aaa', 'bbb', - expect_error=True) + script.pip('cache', 'remove', 'aaa', 'bbb', + expect_error=True) def test_cache_purge(script, monkeypatch): From c838a6717859cfcf698ce49b0387dbd54d62a686 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash <me@duckie.co> Date: Thu, 10 Oct 2019 05:27:51 -0400 Subject: [PATCH 1477/3170] [commands/cache] Change pattern suffix from -*.whl to *.whl. --- src/pip/_internal/commands/cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index 8e6f12f4c94..daa9809ff89 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -138,4 +138,4 @@ def purge_cache(self, options, args): def _find_wheels(self, options, pattern): # type: (Values, str) -> List[str] wheel_dir = os.path.join(options.cache_dir, 'wheels') - return find_files(wheel_dir, pattern + '-*.whl') + return find_files(wheel_dir, pattern + '*.whl') From 61dd0bc16c6d85567e5c29af92d65b9e58ec0388 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash <me@duckie.co> Date: Thu, 10 Oct 2019 05:42:28 -0400 Subject: [PATCH 1478/3170] [commands/cache] Use location of wheel cache dir specifically. --- src/pip/_internal/commands/cache.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index daa9809ff89..09d7d455d9b 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -83,7 +83,7 @@ def get_cache_info(self, options, args): Location: {location} Packages: {package_count} """).format( - location=options.cache_dir, + location=self._wheels_cache_dir(options), package_count=num_packages, ).strip() @@ -135,7 +135,10 @@ def purge_cache(self, options, args): return self.remove_cache_items(options, ['*']) + def _wheels_cache_dir(self, options): + return os.path.join(options.cache_dir, 'wheels') + def _find_wheels(self, options, pattern): # type: (Values, str) -> List[str] - wheel_dir = os.path.join(options.cache_dir, 'wheels') + wheel_dir = self._wheels_cache_dir(options) return find_files(wheel_dir, pattern + '*.whl') From 94a6593a5995373d3195367b6441316d03465d09 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash <me@duckie.co> Date: Thu, 10 Oct 2019 05:46:57 -0400 Subject: [PATCH 1479/3170] [commands/cache] Add HTML docs for `pip cache`. --- docs/html/reference/index.rst | 1 + docs/html/reference/pip_cache.rst | 22 ++++++++++++++++++++++ 2 files changed, 23 insertions(+) create mode 100644 docs/html/reference/pip_cache.rst diff --git a/docs/html/reference/index.rst b/docs/html/reference/index.rst index f3312948193..d21b7a9801a 100644 --- a/docs/html/reference/index.rst +++ b/docs/html/reference/index.rst @@ -13,6 +13,7 @@ Reference Guide pip_list pip_show pip_search + pip_cache pip_check pip_config pip_wheel diff --git a/docs/html/reference/pip_cache.rst b/docs/html/reference/pip_cache.rst new file mode 100644 index 00000000000..d56d1b016ba --- /dev/null +++ b/docs/html/reference/pip_cache.rst @@ -0,0 +1,22 @@ + +.. _`pip cache`: + +pip cache +------------ + +.. contents:: + +Usage +***** + +.. pip-command-usage:: cache + +Description +*********** + +.. pip-command-description:: cache + +Options +******* + +.. pip-command-options:: cache From 61a0adcfe79043c17e905ae2e5d75b4ce8eaddd5 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash <me@duckie.co> Date: Thu, 10 Oct 2019 05:51:06 -0400 Subject: [PATCH 1480/3170] [commands/cache] Add missing type annotation. --- src/pip/_internal/commands/cache.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index 09d7d455d9b..72273265566 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -136,6 +136,7 @@ def purge_cache(self, options, args): return self.remove_cache_items(options, ['*']) def _wheels_cache_dir(self, options): + # type: (Values) -> str return os.path.join(options.cache_dir, 'wheels') def _find_wheels(self, options, pattern): From 554133a90eb9611b3c828b5b65a5f91fc6fe0a01 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash <me@duckie.co> Date: Mon, 6 Jan 2020 15:51:37 -0500 Subject: [PATCH 1481/3170] [commands/cache] Add file size information. --- src/pip/_internal/commands/cache.py | 25 +++++++++----- src/pip/_internal/utils/filesystem.py | 50 ++++++++++++++++++++++++++- 2 files changed, 65 insertions(+), 10 deletions(-) diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index 72273265566..80ea5c8f9a2 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -4,10 +4,10 @@ import os import textwrap +import pip._internal.utils.filesystem as filesystem from pip._internal.cli.base_command import Command from pip._internal.cli.status_codes import ERROR, SUCCESS from pip._internal.exceptions import CommandError, PipError -from pip._internal.utils.filesystem import find_files from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: @@ -78,13 +78,18 @@ def get_cache_info(self, options, args): # type: (Values, List[Any]) -> None num_packages = len(self._find_wheels(options, '*')) + cache_location = self._wheels_cache_dir(options) + cache_size = filesystem.friendly_directory_size(cache_location) + message = textwrap.dedent(""" Cache info: Location: {location} Packages: {package_count} + Size: {size} """).format( - location=self._wheels_cache_dir(options), + location=cache_location, package_count=num_packages, + size=cache_size, ).strip() logger.info(message) @@ -100,16 +105,18 @@ def list_cache_items(self, options, args): pattern = '*' files = self._find_wheels(options, pattern) - wheels = sorted(set(map(lambda f: os.path.basename(f), files))) - if not wheels: + if not files: logger.info('Nothing is currently cached.') return - result = 'Current cache contents:\n' - for wheel in wheels: - result += ' - %s\n' % wheel - logger.info(result.strip()) + results = [] + for filename in files: + wheel = os.path.basename(filename) + size = filesystem.friendly_file_size(filename) + results.append(' - {} ({})'.format(wheel, size)) + logger.info('Current cache contents:\n') + logger.info('\n'.join(sorted(results))) def remove_cache_items(self, options, args): # type: (Values, List[Any]) -> None @@ -142,4 +149,4 @@ def _wheels_cache_dir(self, options): def _find_wheels(self, options, pattern): # type: (Values, str) -> List[str] wheel_dir = self._wheels_cache_dir(options) - return find_files(wheel_dir, pattern + '*.whl') + return filesystem.find_files(wheel_dir, pattern + '*.whl') diff --git a/src/pip/_internal/utils/filesystem.py b/src/pip/_internal/utils/filesystem.py index 2772e0880e4..7e369e4a843 100644 --- a/src/pip/_internal/utils/filesystem.py +++ b/src/pip/_internal/utils/filesystem.py @@ -18,7 +18,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING, cast if MYPY_CHECK_RUNNING: - from typing import Any, BinaryIO, Iterator, List + from typing import Any, BinaryIO, Iterator, List, Union class NamedTemporaryFileResult(BinaryIO): @property @@ -188,3 +188,51 @@ def find_files(path, pattern): matches = fnmatch.filter(files, pattern) result.extend(os.path.join(root, f) for f in matches) return result + + +def _friendly_size(size): + # type: (Union[float, int]) -> str + suffix = 'B' + if size > 1000: + size /= 1000 + suffix = 'KB' + + if size > 1000: + size /= 1000 + suffix = 'MB' + + if size > 1000: + size /= 1000 + suffix = 'GB' + + size = round(size, 1) + + return '{} {}'.format(size, suffix) + + +def file_size(path): + # type: (str) -> Union[int, float] + # If it's a symlink, return 0. + if os.path.islink(path): + return 0 + return os.path.getsize(path) + + +def friendly_file_size(path): + # type: (str) -> str + return _friendly_size(file_size(path)) + + +def directory_size(path): + # type: (str) -> Union[int, float] + size = 0.0 + for root, _dirs, files in os.walk(path): + for filename in files: + file_path = os.path.join(root, filename) + size += file_size(file_path) + return size + + +def friendly_directory_size(path): + # type: (str) -> str + return _friendly_size(directory_size(path)) From 2d978309a2f439fe8b9cfe9624f6daeccb772005 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash <me@duckie.co> Date: Mon, 13 Jan 2020 17:53:16 -0500 Subject: [PATCH 1482/3170] [commands/cache] Minor clean-up. - Consistently use singular 'cache' (not plural 'caches'). - Remove unnecessary uses of the word 'currently'. - Use 'file(s)' instead of 'files', to account for case of only one file. - Use .format() when appropriate. - Minor cleanup of `pip cache`-related files in docs/. --- docs/html/reference/pip_cache.rst | 2 +- docs/man/commands/cache.rst | 4 ++-- src/pip/_internal/commands/__init__.py | 2 +- src/pip/_internal/commands/cache.py | 10 +++++----- tests/functional/test_cache.py | 2 +- 5 files changed, 10 insertions(+), 10 deletions(-) diff --git a/docs/html/reference/pip_cache.rst b/docs/html/reference/pip_cache.rst index d56d1b016ba..8ad99f65cba 100644 --- a/docs/html/reference/pip_cache.rst +++ b/docs/html/reference/pip_cache.rst @@ -2,7 +2,7 @@ .. _`pip cache`: pip cache ------------- +--------- .. contents:: diff --git a/docs/man/commands/cache.rst b/docs/man/commands/cache.rst index b0241c8135e..8f8e197f922 100644 --- a/docs/man/commands/cache.rst +++ b/docs/man/commands/cache.rst @@ -1,8 +1,8 @@ :orphan: -========== +========= pip-cache -========== +========= Description *********** diff --git a/src/pip/_internal/commands/__init__.py b/src/pip/_internal/commands/__init__.py index 8507b6ef9f0..48e288ab345 100644 --- a/src/pip/_internal/commands/__init__.py +++ b/src/pip/_internal/commands/__init__.py @@ -66,7 +66,7 @@ )), ('cache', CommandInfo( 'pip._internal.commands.cache', 'CacheCommand', - "Inspect and manage pip's caches.", + "Inspect and manage pip's cache.", )), ('wheel', CommandInfo( 'pip._internal.commands.wheel', 'WheelCommand', diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index 80ea5c8f9a2..9a473aeefb9 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -20,12 +20,12 @@ class CacheCommand(Command): """ - Inspect and manage pip's caches. + Inspect and manage pip's cache. Subcommands: info: - Show information about the caches. + Show information about the cache. list: List filenames of packages stored in the cache. remove: @@ -107,7 +107,7 @@ def list_cache_items(self, options, args): files = self._find_wheels(options, pattern) if not files: - logger.info('Nothing is currently cached.') + logger.info('Nothing cached.') return results = [] @@ -115,7 +115,7 @@ def list_cache_items(self, options, args): wheel = os.path.basename(filename) size = filesystem.friendly_file_size(filename) results.append(' - {} ({})'.format(wheel, size)) - logger.info('Current cache contents:\n') + logger.info('Cache contents:\n') logger.info('\n'.join(sorted(results))) def remove_cache_items(self, options, args): @@ -133,7 +133,7 @@ def remove_cache_items(self, options, args): for filename in files: os.unlink(filename) logger.debug('Removed %s', filename) - logger.info('Removed %s files', len(files)) + logger.info('Removed %s file(s)', len(files)) def purge_cache(self, options, args): # type: (Values, List[Any]) -> None diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index 745a6e2a772..642630b201a 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -15,7 +15,7 @@ def test_cache_info(script, monkeypatch): result = script.pip('cache', 'info') cache_dir = _cache_dir(script) - assert 'Location: %s' % cache_dir in result.stdout + assert 'Location: {}'.format(cache_dir) in result.stdout # TODO(@duckinator): This should probably test that the number of # packages is actually correct, but I'm not sure how to do that # without pretty much re-implementing the entire cache info command. From 6fa8498e18e2b054eafd93965c10c1c424889cef Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash <me@duckie.co> Date: Tue, 14 Jan 2020 14:36:58 -0500 Subject: [PATCH 1483/3170] [commands/cache] Avoid use of "(s)" suffix. As @hugovk pointed out, it can cause problems sometimes: https://github.com/pypa/pip/pull/6391#discussion_r366259867 --- src/pip/_internal/commands/cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index 9a473aeefb9..9e106ad7e31 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -133,7 +133,7 @@ def remove_cache_items(self, options, args): for filename in files: os.unlink(filename) logger.debug('Removed %s', filename) - logger.info('Removed %s file(s)', len(files)) + logger.info('Files removed: %s', len(files)) def purge_cache(self, options, args): # type: (Values, List[Any]) -> None From d74895a224594440f7208ec4f8b336377c1b711f Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash <me@duckie.co> Date: Sun, 23 Feb 2020 13:49:14 -0500 Subject: [PATCH 1484/3170] [commands/cache] Normalize path in test. --- tests/functional/test_cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index 642630b201a..b6a3c55e988 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -15,7 +15,7 @@ def test_cache_info(script, monkeypatch): result = script.pip('cache', 'info') cache_dir = _cache_dir(script) - assert 'Location: {}'.format(cache_dir) in result.stdout + assert 'Location: {}'.format(os.path.normcase(cache_dir)) in result.stdout # TODO(@duckinator): This should probably test that the number of # packages is actually correct, but I'm not sure how to do that # without pretty much re-implementing the entire cache info command. From 10d13762ebab7fa731153fe59fe94d8fae21157a Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash <me@duckie.co> Date: Sun, 23 Feb 2020 17:01:44 -0500 Subject: [PATCH 1485/3170] [commands/cache] Be explicit about `pip cache` only working on the wheel cache. --- news/6391.feature | 2 +- src/pip/_internal/commands/__init__.py | 2 +- src/pip/_internal/commands/cache.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/news/6391.feature b/news/6391.feature index 73d6dffca8a..e13df852713 100644 --- a/news/6391.feature +++ b/news/6391.feature @@ -1 +1 @@ -Add ``pip cache`` command for inspecting/managing pip's cache. +Add ``pip cache`` command for inspecting/managing pip's wheel cache. diff --git a/src/pip/_internal/commands/__init__.py b/src/pip/_internal/commands/__init__.py index 48e288ab345..b43a96c13f3 100644 --- a/src/pip/_internal/commands/__init__.py +++ b/src/pip/_internal/commands/__init__.py @@ -66,7 +66,7 @@ )), ('cache', CommandInfo( 'pip._internal.commands.cache', 'CacheCommand', - "Inspect and manage pip's cache.", + "Inspect and manage pip's wheel cache.", )), ('wheel', CommandInfo( 'pip._internal.commands.wheel', 'WheelCommand', diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index 9e106ad7e31..455064a0d24 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -20,7 +20,7 @@ class CacheCommand(Command): """ - Inspect and manage pip's cache. + Inspect and manage pip's wheel cache. Subcommands: From d9dc76e9094ba1e06224d140ffc5712488114d9f Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash <me@duckie.co> Date: Wed, 4 Mar 2020 12:03:36 -0500 Subject: [PATCH 1486/3170] [commands/cache] Correct argument name in documentation for `pip cache list`. --- src/pip/_internal/commands/cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index 455064a0d24..8ef507941d5 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -38,7 +38,7 @@ class CacheCommand(Command): usage = """ %prog info - %prog list [name] + %prog list [<pattern>] %prog remove <pattern> %prog purge """ From f22f69e9bdb20677ff88fadd94941d455fccc6eb Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash <me@duckie.co> Date: Wed, 4 Mar 2020 12:09:19 -0500 Subject: [PATCH 1487/3170] [utils/filesystem] Convert `size` to float, for consistent behavior between Py2 and Py3. --- src/pip/_internal/utils/filesystem.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pip/_internal/utils/filesystem.py b/src/pip/_internal/utils/filesystem.py index 7e369e4a843..adfcd8f106f 100644 --- a/src/pip/_internal/utils/filesystem.py +++ b/src/pip/_internal/utils/filesystem.py @@ -192,6 +192,7 @@ def find_files(path, pattern): def _friendly_size(size): # type: (Union[float, int]) -> str + size = float(size) # for consistent behavior between Python 2 and Python 3. suffix = 'B' if size > 1000: size /= 1000 From 03d5ec10f20406f4ba94e5dd2f81f8759f9cace3 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash <me@duckie.co> Date: Wed, 4 Mar 2020 12:12:39 -0500 Subject: [PATCH 1488/3170] [utils/filesystem] Reformat comment to keep lines <79 characters long. --- src/pip/_internal/utils/filesystem.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/utils/filesystem.py b/src/pip/_internal/utils/filesystem.py index adfcd8f106f..ea573dbe9ee 100644 --- a/src/pip/_internal/utils/filesystem.py +++ b/src/pip/_internal/utils/filesystem.py @@ -192,7 +192,11 @@ def find_files(path, pattern): def _friendly_size(size): # type: (Union[float, int]) -> str - size = float(size) # for consistent behavior between Python 2 and Python 3. + + # Explicitly convert `size` to a float, for consistent behavior + # between Python 2 and Python 3. + size = float(size) + suffix = 'B' if size > 1000: size /= 1000 From 735375fb6bd99d150d833684a219c4f17eadb64e Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash <the@smallest.dog> Date: Thu, 5 Mar 2020 08:25:09 -0500 Subject: [PATCH 1489/3170] [commands/cache] Reformat documentation. Co-Authored-By: Pradyun Gedam <pradyunsg@gmail.com> --- src/pip/_internal/commands/cache.py | 12 ++++-------- 1 file changed, 4 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index 8ef507941d5..fc46f9d6b12 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -24,14 +24,10 @@ class CacheCommand(Command): Subcommands: - info: - Show information about the cache. - list: - List filenames of packages stored in the cache. - remove: - Remove one or more package from the cache. - purge: - Remove all items from the cache. + info: Show information about the cache. + list: List filenames of packages stored in the cache. + remove: Remove one or more package from the cache. + purge: Remove all items from the cache. <pattern> can be a glob expression or a package name. """ From 8cd8c91491c904b2536bc79ec16ae6ac396b5818 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash <the@smallest.dog> Date: Thu, 5 Mar 2020 08:25:41 -0500 Subject: [PATCH 1490/3170] [commands/cache] Reformat (more) documentation. Co-Authored-By: Pradyun Gedam <pradyunsg@gmail.com> --- src/pip/_internal/commands/cache.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index fc46f9d6b12..4d557a0f92c 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -20,9 +20,9 @@ class CacheCommand(Command): """ - Inspect and manage pip's wheel cache. + Inspect and manage pip's wheel cache. - Subcommands: + Subcommands: info: Show information about the cache. list: List filenames of packages stored in the cache. From 63ba6cce4ad4f77696eeca8a13dcccb8af9c02c4 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash <me@duckie.co> Date: Thu, 5 Mar 2020 08:36:35 -0500 Subject: [PATCH 1491/3170] [command/cache, utils/filesystem] Use existing format_size; remove _friendly_size; rename friendly_*_size to format_*_size for consistency. --- src/pip/_internal/commands/cache.py | 4 ++-- src/pip/_internal/utils/filesystem.py | 34 ++++----------------------- 2 files changed, 7 insertions(+), 31 deletions(-) diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index 4d557a0f92c..2f1ad977519 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -75,7 +75,7 @@ def get_cache_info(self, options, args): num_packages = len(self._find_wheels(options, '*')) cache_location = self._wheels_cache_dir(options) - cache_size = filesystem.friendly_directory_size(cache_location) + cache_size = filesystem.format_directory_size(cache_location) message = textwrap.dedent(""" Cache info: @@ -109,7 +109,7 @@ def list_cache_items(self, options, args): results = [] for filename in files: wheel = os.path.basename(filename) - size = filesystem.friendly_file_size(filename) + size = filesystem.format_file_size(filename) results.append(' - {} ({})'.format(wheel, size)) logger.info('Cache contents:\n') logger.info('\n'.join(sorted(results))) diff --git a/src/pip/_internal/utils/filesystem.py b/src/pip/_internal/utils/filesystem.py index ea573dbe9ee..d97992acb48 100644 --- a/src/pip/_internal/utils/filesystem.py +++ b/src/pip/_internal/utils/filesystem.py @@ -15,6 +15,7 @@ from pip._vendor.six import PY2 from pip._internal.utils.compat import get_path_uid +from pip._internal.utils.misc import format_size from pip._internal.utils.typing import MYPY_CHECK_RUNNING, cast if MYPY_CHECK_RUNNING: @@ -190,31 +191,6 @@ def find_files(path, pattern): return result -def _friendly_size(size): - # type: (Union[float, int]) -> str - - # Explicitly convert `size` to a float, for consistent behavior - # between Python 2 and Python 3. - size = float(size) - - suffix = 'B' - if size > 1000: - size /= 1000 - suffix = 'KB' - - if size > 1000: - size /= 1000 - suffix = 'MB' - - if size > 1000: - size /= 1000 - suffix = 'GB' - - size = round(size, 1) - - return '{} {}'.format(size, suffix) - - def file_size(path): # type: (str) -> Union[int, float] # If it's a symlink, return 0. @@ -223,9 +199,9 @@ def file_size(path): return os.path.getsize(path) -def friendly_file_size(path): +def format_file_size(path): # type: (str) -> str - return _friendly_size(file_size(path)) + return format_size(file_size(path)) def directory_size(path): @@ -238,6 +214,6 @@ def directory_size(path): return size -def friendly_directory_size(path): +def format_directory_size(path): # type: (str) -> str - return _friendly_size(directory_size(path)) + return format_size(directory_size(path)) From ed9f885bd7b99ed09f37d30c64779885b22aae66 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash <the@smallest.dog> Date: Thu, 5 Mar 2020 10:43:44 -0500 Subject: [PATCH 1492/3170] [commands/cache] Reformat output of `pip cache info` Co-Authored-By: Pradyun Gedam <pradyunsg@gmail.com> --- src/pip/_internal/commands/cache.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index 2f1ad977519..e0d751d8d03 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -78,10 +78,9 @@ def get_cache_info(self, options, args): cache_size = filesystem.format_directory_size(cache_location) message = textwrap.dedent(""" - Cache info: - Location: {location} - Packages: {package_count} - Size: {size} + Location: {location} + Size: {size} + Number of wheels: {package_count} """).format( location=cache_location, package_count=num_packages, From f8b67c8bf14c1cc1afe15eefeef416af8f21bae3 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash <me@duckie.co> Date: Thu, 5 Mar 2020 11:10:17 -0500 Subject: [PATCH 1493/3170] [commands/cache] Fix test_cache_info test. --- tests/functional/test_cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index b6a3c55e988..1b2df3f049e 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -19,7 +19,7 @@ def test_cache_info(script, monkeypatch): # TODO(@duckinator): This should probably test that the number of # packages is actually correct, but I'm not sure how to do that # without pretty much re-implementing the entire cache info command. - assert 'Packages: ' in result.stdout + assert 'Number of wheels: ' in result.stdout def test_cache_list(script, monkeypatch): From 8b518b258df5a923d27fb7537baae31680474268 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash <me@duckie.co> Date: Thu, 5 Mar 2020 11:10:39 -0500 Subject: [PATCH 1494/3170] [commands/cache] Make filenames more realistic in tests. --- tests/functional/test_cache.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index 1b2df3f049e..2cd02829880 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -27,9 +27,9 @@ def test_cache_list(script, monkeypatch): wheel_cache_dir = os.path.join(cache_dir, 'wheels') destination = os.path.join(wheel_cache_dir, 'arbitrary', 'pathname') os.makedirs(destination) - with open(os.path.join(destination, 'yyy-1.2.3.whl'), 'w'): + with open(os.path.join(destination, 'yyy-1.2.3-py3-none-any.whl'), 'w'): pass - with open(os.path.join(destination, 'zzz-4.5.6.whl'), 'w'): + with open(os.path.join(destination, 'zzz-4.5.6-py27-none-any.whl'), 'w'): pass result = script.pip('cache', 'list') assert 'yyy-1.2.3' in result.stdout @@ -47,9 +47,9 @@ def test_cache_list_with_pattern(script, monkeypatch): wheel_cache_dir = os.path.join(cache_dir, 'wheels') destination = os.path.join(wheel_cache_dir, 'arbitrary', 'pathname') os.makedirs(destination) - with open(os.path.join(destination, 'yyy-1.2.3.whl'), 'w'): + with open(os.path.join(destination, 'yyy-1.2.3-py3-none-any.whl'), 'w'): pass - with open(os.path.join(destination, 'zzz-4.5.6.whl'), 'w'): + with open(os.path.join(destination, 'zzz-4.5.6-py27-none-any.whl'), 'w'): pass result = script.pip('cache', 'list', 'zzz') assert 'yyy-1.2.3' not in result.stdout @@ -62,9 +62,9 @@ def test_cache_remove(script, monkeypatch): wheel_cache_dir = os.path.join(cache_dir, 'wheels') destination = os.path.join(wheel_cache_dir, 'arbitrary', 'pathname') os.makedirs(destination) - with open(os.path.join(wheel_cache_dir, 'yyy-1.2.3.whl'), 'w'): + with open(os.path.join(wheel_cache_dir, 'yyy-1.2.3-py3-none-any.whl'), 'w'): pass - with open(os.path.join(wheel_cache_dir, 'zzz-4.5.6.whl'), 'w'): + with open(os.path.join(wheel_cache_dir, 'zzz-4.5.6-py27-none-any.whl'), 'w'): pass script.pip('cache', 'remove', expect_error=True) @@ -84,9 +84,9 @@ def test_cache_purge(script, monkeypatch): wheel_cache_dir = os.path.join(cache_dir, 'wheels') destination = os.path.join(wheel_cache_dir, 'arbitrary', 'pathname') os.makedirs(destination) - with open(os.path.join(wheel_cache_dir, 'yyy-1.2.3.whl'), 'w'): + with open(os.path.join(wheel_cache_dir, 'yyy-1.2.3-py3-none-any.whl'), 'w'): pass - with open(os.path.join(wheel_cache_dir, 'zzz-4.5.6.whl'), 'w'): + with open(os.path.join(wheel_cache_dir, 'zzz-4.5.6-py27-none-any.whl'), 'w'): pass result = script.pip('cache', 'purge', 'aaa', '--verbose', From d57407a37d788719f93757b7345acce7fda185b0 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash <me@duckie.co> Date: Thu, 5 Mar 2020 13:44:25 -0500 Subject: [PATCH 1495/3170] [commands/cache] Make _find_wheels(), and this `pip cache {list,remove}` behave more predictably. --- src/pip/_internal/commands/cache.py | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index e0d751d8d03..af97b40217c 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -144,4 +144,23 @@ def _wheels_cache_dir(self, options): def _find_wheels(self, options, pattern): # type: (Values, str) -> List[str] wheel_dir = self._wheels_cache_dir(options) - return filesystem.find_files(wheel_dir, pattern + '*.whl') + + # The wheel filename format, as specified in PEP 427, is: + # {distribution}-{version}(-{build})?-{python}-{abi}-{platform}.whl + # + # Additionally, non-alphanumeric values in the distribution are + # normalized to underscores (_), meaning hyphens can never occur + # before `-{version}`. + # + # Given that information: + # - If the pattern we're given contains a hyphen (-), the user is + # providing at least the version. Thus, we can just append `*.whl` + # to match the rest of it. + # - If the pattern we're given doesn't contain a hyphen (-), the + # user is only providing the name. Thus, we append `-*.whl` to + # match the hyphen before the version, followed by anything else. + # + # PEP 427: https://www.python.org/dev/peps/pep-0427/ + pattern = pattern + ("*.whl" if "-" in pattern else "-*.whl") + + return filesystem.find_files(wheel_dir, pattern) From e1fde1facaf44ab37882b74d14ab305d422bcd22 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash <me@duckie.co> Date: Thu, 5 Mar 2020 13:47:25 -0500 Subject: [PATCH 1496/3170] [commands/cache] Remove unnecessary re-definition of __init__. --- src/pip/_internal/commands/cache.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index af97b40217c..fc63d5eeceb 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -39,10 +39,6 @@ class CacheCommand(Command): %prog purge """ - def __init__(self, *args, **kw): - # type: (*Any, **Any) -> None - super(CacheCommand, self).__init__(*args, **kw) - def run(self, options, args): # type: (Values, List[Any]) -> int handlers = { From e804aa56aff9048c58b6099af121aa6cebb7a299 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash <me@duckie.co> Date: Thu, 5 Mar 2020 13:49:50 -0500 Subject: [PATCH 1497/3170] [commands/cache] Have `pip cache info` raise an exception if it gets any arguments. --- src/pip/_internal/commands/cache.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index fc63d5eeceb..7e3f72e080b 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -68,6 +68,9 @@ def run(self, options, args): def get_cache_info(self, options, args): # type: (Values, List[Any]) -> None + if args: + raise CommandError('Too many arguments') + num_packages = len(self._find_wheels(options, '*')) cache_location = self._wheels_cache_dir(options) From 6e425d80093b5e440eeb7a6797f13318ae8dc7b2 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash <me@duckie.co> Date: Fri, 6 Mar 2020 16:12:55 -0500 Subject: [PATCH 1498/3170] [tests/functional/cache] Refactor to be less redundant. --- tests/functional/test_cache.py | 88 +++++++++++++++------------------- 1 file changed, 38 insertions(+), 50 deletions(-) diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index 2cd02829880..00ed6a73eaa 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -11,83 +11,73 @@ def _cache_dir(script): return result.stdout.strip() -def test_cache_info(script, monkeypatch): - result = script.pip('cache', 'info') +def _wheel_cache_contents(script): cache_dir = _cache_dir(script) + wheel_cache_dir = os.path.join(cache_dir, 'wheels') + destination = os.path.join(wheel_cache_dir, 'arbitrary', 'pathname') + os.makedirs(destination) - assert 'Location: {}'.format(os.path.normcase(cache_dir)) in result.stdout - # TODO(@duckinator): This should probably test that the number of - # packages is actually correct, but I'm not sure how to do that - # without pretty much re-implementing the entire cache info command. - assert 'Number of wheels: ' in result.stdout + files = [ + ('yyy-1.2.3', os.path.join(destination, 'yyy-1.2.3-py3-none-any.whl')), + ('zzz-4.5.6', os.path.join(destination, 'zzz-4.5.6-py27-none-any.whl')), + ] + + for _name, filename in files: + with open(filename, 'w'): + pass + return files -def test_cache_list(script, monkeypatch): + +def test_cache_info(script): cache_dir = _cache_dir(script) - wheel_cache_dir = os.path.join(cache_dir, 'wheels') - destination = os.path.join(wheel_cache_dir, 'arbitrary', 'pathname') - os.makedirs(destination) - with open(os.path.join(destination, 'yyy-1.2.3-py3-none-any.whl'), 'w'): - pass - with open(os.path.join(destination, 'zzz-4.5.6-py27-none-any.whl'), 'w'): - pass + cache_files = _wheel_cache_contents(script) + + result = script.pip('cache', 'info') + + assert 'Location: {}'.format(os.path.normcase(cache_dir)) in result.stdout + assert 'Number of wheels: {}'.format(len(cache_files)) in result.stdout + + +def test_cache_list(script): + cache_files = _wheel_cache_contents(script) + packages = [name for (name, _path) in cache_files] result = script.pip('cache', 'list') - assert 'yyy-1.2.3' in result.stdout - assert 'zzz-4.5.6' in result.stdout - shutil.rmtree(os.path.join(wheel_cache_dir, 'arbitrary')) + for package in packages: + assert package in result.stdout + # assert 'yyy-1.2.3' in result.stdout + # assert 'zzz-4.5.6' in result.stdout -def test_cache_list_too_many_args(script, monkeypatch): +def test_cache_list_too_many_args(script): script.pip('cache', 'list', 'aaa', 'bbb', expect_error=True) -def test_cache_list_with_pattern(script, monkeypatch): - cache_dir = _cache_dir(script) - wheel_cache_dir = os.path.join(cache_dir, 'wheels') - destination = os.path.join(wheel_cache_dir, 'arbitrary', 'pathname') - os.makedirs(destination) - with open(os.path.join(destination, 'yyy-1.2.3-py3-none-any.whl'), 'w'): - pass - with open(os.path.join(destination, 'zzz-4.5.6-py27-none-any.whl'), 'w'): - pass +def test_cache_list_with_pattern(script): + cache_files = _wheel_cache_contents(script) + result = script.pip('cache', 'list', 'zzz') assert 'yyy-1.2.3' not in result.stdout assert 'zzz-4.5.6' in result.stdout - shutil.rmtree(os.path.join(wheel_cache_dir, 'arbitrary')) def test_cache_remove(script, monkeypatch): - cache_dir = _cache_dir(script) - wheel_cache_dir = os.path.join(cache_dir, 'wheels') - destination = os.path.join(wheel_cache_dir, 'arbitrary', 'pathname') - os.makedirs(destination) - with open(os.path.join(wheel_cache_dir, 'yyy-1.2.3-py3-none-any.whl'), 'w'): - pass - with open(os.path.join(wheel_cache_dir, 'zzz-4.5.6-py27-none-any.whl'), 'w'): - pass + cache_files = _wheel_cache_contents(script) script.pip('cache', 'remove', expect_error=True) result = script.pip('cache', 'remove', 'zzz', '--verbose') assert 'yyy-1.2.3' not in result.stdout assert 'zzz-4.5.6' in result.stdout - shutil.rmtree(os.path.join(wheel_cache_dir, 'arbitrary')) -def test_cache_remove_too_many_args(script, monkeypatch): +def test_cache_remove_too_many_args(script): script.pip('cache', 'remove', 'aaa', 'bbb', expect_error=True) -def test_cache_purge(script, monkeypatch): - cache_dir = _cache_dir(script) - wheel_cache_dir = os.path.join(cache_dir, 'wheels') - destination = os.path.join(wheel_cache_dir, 'arbitrary', 'pathname') - os.makedirs(destination) - with open(os.path.join(wheel_cache_dir, 'yyy-1.2.3-py3-none-any.whl'), 'w'): - pass - with open(os.path.join(wheel_cache_dir, 'zzz-4.5.6-py27-none-any.whl'), 'w'): - pass +def test_cache_purge(script): + cache_files = _wheel_cache_contents(script) result = script.pip('cache', 'purge', 'aaa', '--verbose', expect_error=True) @@ -97,5 +87,3 @@ def test_cache_purge(script, monkeypatch): result = script.pip('cache', 'purge', '--verbose') assert 'yyy-1.2.3' in result.stdout assert 'zzz-4.5.6' in result.stdout - - shutil.rmtree(os.path.join(wheel_cache_dir, 'arbitrary')) From 274b295bd8181444e2d87d0d722858955c841bfd Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash <me@duckie.co> Date: Fri, 6 Mar 2020 17:09:40 -0500 Subject: [PATCH 1499/3170] [tests/functional/cache] Make fixtures feel less magical. It bothered me that _whether or not a function had a certain argument_ dictated the contents of a directory. Pytest fixtures are inherently kinda magical, but that was a bit much for me. --- tests/functional/test_cache.py | 58 ++++++++++++++++++++-------------- 1 file changed, 34 insertions(+), 24 deletions(-) diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index 00ed6a73eaa..a3fd5e5a489 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -1,8 +1,11 @@ import os -import shutil +from glob import glob +import pytest -def _cache_dir(script): + +@pytest.fixture +def cache_dir(script): result = script.run( 'python', '-c', 'from pip._internal.locations import USER_CACHE_DIR;' @@ -11,9 +14,23 @@ def _cache_dir(script): return result.stdout.strip() -def _wheel_cache_contents(script): - cache_dir = _cache_dir(script) - wheel_cache_dir = os.path.join(cache_dir, 'wheels') +@pytest.fixture +def wheel_cache_dir(cache_dir): + return os.path.join(cache_dir, 'wheels') + + +@pytest.fixture +def wheel_cache_files(wheel_cache_dir): + destination = os.path.join(wheel_cache_dir, 'arbitrary', 'pathname') + filenames = glob(os.path.join(destination, '*.whl')) + files = [] + for filename in filenames: + files.append(os.path.join(destination, filename)) + return files + + +@pytest.fixture +def populate_wheel_cache(wheel_cache_dir): destination = os.path.join(wheel_cache_dir, 'arbitrary', 'pathname') os.makedirs(destination) @@ -29,24 +46,20 @@ def _wheel_cache_contents(script): return files -def test_cache_info(script): - cache_dir = _cache_dir(script) - cache_files = _wheel_cache_contents(script) - +@pytest.mark.usefixtures("populate_wheel_cache") +def test_cache_info(script, wheel_cache_dir, wheel_cache_files): result = script.pip('cache', 'info') - assert 'Location: {}'.format(os.path.normcase(cache_dir)) in result.stdout - assert 'Number of wheels: {}'.format(len(cache_files)) in result.stdout + assert 'Location: {}'.format(os.path.normcase(wheel_cache_dir)) in result.stdout + assert 'Number of wheels: {}'.format(len(wheel_cache_files)) in result.stdout +@pytest.mark.usefixtures("populate_wheel_cache") def test_cache_list(script): - cache_files = _wheel_cache_contents(script) - packages = [name for (name, _path) in cache_files] result = script.pip('cache', 'list') - for package in packages: - assert package in result.stdout - # assert 'yyy-1.2.3' in result.stdout - # assert 'zzz-4.5.6' in result.stdout + + assert 'yyy-1.2.3' in result.stdout + assert 'zzz-4.5.6' in result.stdout def test_cache_list_too_many_args(script): @@ -54,17 +67,15 @@ def test_cache_list_too_many_args(script): expect_error=True) +@pytest.mark.usefixtures("populate_wheel_cache") def test_cache_list_with_pattern(script): - cache_files = _wheel_cache_contents(script) - result = script.pip('cache', 'list', 'zzz') assert 'yyy-1.2.3' not in result.stdout assert 'zzz-4.5.6' in result.stdout -def test_cache_remove(script, monkeypatch): - cache_files = _wheel_cache_contents(script) - +@pytest.mark.usefixtures("populate_wheel_cache") +def test_cache_remove(script): script.pip('cache', 'remove', expect_error=True) result = script.pip('cache', 'remove', 'zzz', '--verbose') assert 'yyy-1.2.3' not in result.stdout @@ -76,9 +87,8 @@ def test_cache_remove_too_many_args(script): expect_error=True) +@pytest.mark.usefixtures("populate_wheel_cache") def test_cache_purge(script): - cache_files = _wheel_cache_contents(script) - result = script.pip('cache', 'purge', 'aaa', '--verbose', expect_error=True) assert 'yyy-1.2.3' not in result.stdout From c6b5a52a5ab794cfc433b93e9c4a54c6fdff50d6 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash <me@duckie.co> Date: Fri, 6 Mar 2020 21:26:04 -0500 Subject: [PATCH 1500/3170] [tests/functional/test_cache] Always call normcase on cache dir; fix line length problems. --- tests/functional/test_cache.py | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index a3fd5e5a489..be4e5589e36 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -16,7 +16,7 @@ def cache_dir(script): @pytest.fixture def wheel_cache_dir(cache_dir): - return os.path.join(cache_dir, 'wheels') + return os.path.normcase(os.path.join(cache_dir, 'wheels')) @pytest.fixture @@ -36,7 +36,7 @@ def populate_wheel_cache(wheel_cache_dir): files = [ ('yyy-1.2.3', os.path.join(destination, 'yyy-1.2.3-py3-none-any.whl')), - ('zzz-4.5.6', os.path.join(destination, 'zzz-4.5.6-py27-none-any.whl')), + ('zzz-4.5.6', os.path.join(destination, 'zzz-4.5.6-py3-none-any.whl')), ] for _name, filename in files: @@ -50,8 +50,9 @@ def populate_wheel_cache(wheel_cache_dir): def test_cache_info(script, wheel_cache_dir, wheel_cache_files): result = script.pip('cache', 'info') - assert 'Location: {}'.format(os.path.normcase(wheel_cache_dir)) in result.stdout - assert 'Number of wheels: {}'.format(len(wheel_cache_files)) in result.stdout + assert 'Location: {}'.format(wheel_cache_dir) in result.stdout + num_wheels = len(wheel_cache_files) + assert 'Number of wheels: {}'.format(num_wheels) in result.stdout @pytest.mark.usefixtures("populate_wheel_cache") From 32ce3bacbe0893bc0ceb326b8df98ac9300a6915 Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash <me@duckie.co> Date: Tue, 10 Mar 2020 12:44:33 -0400 Subject: [PATCH 1501/3170] [tests/functional/cache] Rewrite all of the pip cache {list,remove} tests. --- tests/functional/test_cache.py | 106 +++++++++++++++++++++++++++++---- 1 file changed, 95 insertions(+), 11 deletions(-) diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index be4e5589e36..9311c6ffcf8 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -1,4 +1,5 @@ import os +import re from glob import glob import pytest @@ -37,6 +38,8 @@ def populate_wheel_cache(wheel_cache_dir): files = [ ('yyy-1.2.3', os.path.join(destination, 'yyy-1.2.3-py3-none-any.whl')), ('zzz-4.5.6', os.path.join(destination, 'zzz-4.5.6-py3-none-any.whl')), + ('zzz-4.5.7', os.path.join(destination, 'zzz-4.5.7-py3-none-any.whl')), + ('zzz-7.8.9', os.path.join(destination, 'zzz-7.8.9-py3-none-any.whl')), ] for _name, filename in files: @@ -46,6 +49,38 @@ def populate_wheel_cache(wheel_cache_dir): return files +def list_matches_wheel(wheel_name, lines): + """Returns True if any line in `lines`, which should be the output of + a `pip cache list` call, matches `wheel_name`. + + E.g., If wheel_name is `foo-1.2.3` it searches for a line starting with + `- foo-1.2.3-py3-none-any.whl `.""" + expected = ' - {}-py3-none-any.whl '.format(wheel_name) + return any(map(lambda l: l.startswith(expected), lines)) + + +@pytest.fixture +def remove_matches_wheel(wheel_cache_dir): + """Returns True if any line in `lines`, which should be the output of + a `pip cache remove`/`pip cache purge` call, matches `wheel_name`. + + E.g., If wheel_name is `foo-1.2.3`, it searches for a line equal to + `Removed <wheel cache dir>/arbitrary/pathname/foo-1.2.3-py3-none-any.whl`. + """ + + def _remove_matches_wheel(wheel_name, lines): + wheel_filename = '{}-py3-none-any.whl'.format(wheel_name) + + # The "/arbitrary/pathname/" bit is an implementation detail of how + # the `populate_wheel_cache` fixture is implemented. + expected = 'Removed {}/arbitrary/pathname/{}'.format( + wheel_cache_dir, wheel_filename, + ) + return expected in lines + + return _remove_matches_wheel + + @pytest.mark.usefixtures("populate_wheel_cache") def test_cache_info(script, wheel_cache_dir, wheel_cache_files): result = script.pip('cache', 'info') @@ -57,37 +92,86 @@ def test_cache_info(script, wheel_cache_dir, wheel_cache_files): @pytest.mark.usefixtures("populate_wheel_cache") def test_cache_list(script): + """Running `pip cache list` should return exactly what the + populate_wheel_cache fixture adds.""" result = script.pip('cache', 'list') - - assert 'yyy-1.2.3' in result.stdout - assert 'zzz-4.5.6' in result.stdout + lines = result.stdout.splitlines() + assert list_matches_wheel('yyy-1.2.3', lines) + assert list_matches_wheel('zzz-4.5.6', lines) + assert list_matches_wheel('zzz-4.5.7', lines) + assert list_matches_wheel('zzz-7.8.9', lines) def test_cache_list_too_many_args(script): + """Passing `pip cache list` too many arguments should cause an error.""" script.pip('cache', 'list', 'aaa', 'bbb', expect_error=True) @pytest.mark.usefixtures("populate_wheel_cache") -def test_cache_list_with_pattern(script): - result = script.pip('cache', 'list', 'zzz') - assert 'yyy-1.2.3' not in result.stdout - assert 'zzz-4.5.6' in result.stdout +def test_cache_list_name_match(script): + """Running `pip cache list zzz` should list zzz-4.5.6, zzz-4.5.7, + zzz-7.8.9, but nothing else.""" + result = script.pip('cache', 'list', 'zzz', '--verbose') + lines = result.stdout.splitlines() + + assert not list_matches_wheel('yyy-1.2.3', lines) + assert list_matches_wheel('zzz-4.5.6', lines) + assert list_matches_wheel('zzz-4.5.7', lines) + assert list_matches_wheel('zzz-7.8.9', lines) @pytest.mark.usefixtures("populate_wheel_cache") -def test_cache_remove(script): +def test_cache_list_name_and_version_match(script): + """Running `pip cache list zzz-4.5.6` should list zzz-4.5.6, but + nothing else.""" + result = script.pip('cache', 'list', 'zzz-4.5.6', '--verbose') + lines = result.stdout.splitlines() + + assert not list_matches_wheel('yyy-1.2.3', lines) + assert list_matches_wheel('zzz-4.5.6', lines) + assert not list_matches_wheel('zzz-4.5.7', lines) + assert not list_matches_wheel('zzz-7.8.9', lines) + + +@pytest.mark.usefixture("populate_wheel_cache") +def test_cache_remove_no_arguments(script): + """Running `pip cache remove` with no arguments should cause an error.""" script.pip('cache', 'remove', expect_error=True) - result = script.pip('cache', 'remove', 'zzz', '--verbose') - assert 'yyy-1.2.3' not in result.stdout - assert 'zzz-4.5.6' in result.stdout def test_cache_remove_too_many_args(script): + """Passing `pip cache remove` too many arguments should cause an error.""" script.pip('cache', 'remove', 'aaa', 'bbb', expect_error=True) +@pytest.mark.usefixtures("populate_wheel_cache") +def test_cache_remove_name_match(script, remove_matches_wheel): + """Running `pip cache remove zzz` should remove zzz-4.5.6 and zzz-7.8.9, + but nothing else.""" + result = script.pip('cache', 'remove', 'zzz', '--verbose') + lines = result.stdout.splitlines() + + assert not remove_matches_wheel('yyy-1.2.3', lines) + assert remove_matches_wheel('zzz-4.5.6', lines) + assert remove_matches_wheel('zzz-4.5.7', lines) + assert remove_matches_wheel('zzz-7.8.9', lines) + + +@pytest.mark.usefixtures("populate_wheel_cache") +def test_cache_remove_name_and_version_match(script, remove_matches_wheel): + """Running `pip cache remove zzz-4.5.6` should remove zzz-4.5.6, but + nothing else.""" + result = script.pip('cache', 'remove', 'zzz-4.5.6', '--verbose') + lines = result.stdout.splitlines() + + assert not remove_matches_wheel('yyy-1.2.3', lines) + assert remove_matches_wheel('zzz-4.5.6', lines) + assert not remove_matches_wheel('zzz-4.5.7', lines) + assert not remove_matches_wheel('zzz-7.8.9', lines) + + @pytest.mark.usefixtures("populate_wheel_cache") def test_cache_purge(script): result = script.pip('cache', 'purge', 'aaa', '--verbose', From ba7c3ac9ec450452ec14e872bc9d6551dffc3d3b Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash <me@duckie.co> Date: Tue, 10 Mar 2020 14:53:24 -0400 Subject: [PATCH 1502/3170] [tests/functional/test_cache] Add test `pip cache list` with an empty cache. --- tests/functional/test_cache.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index 9311c6ffcf8..55080dbf4f2 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -1,5 +1,6 @@ import os import re +import shutil from glob import glob import pytest @@ -23,6 +24,10 @@ def wheel_cache_dir(cache_dir): @pytest.fixture def wheel_cache_files(wheel_cache_dir): destination = os.path.join(wheel_cache_dir, 'arbitrary', 'pathname') + + if not os.path.exists(destination): + return [] + filenames = glob(os.path.join(destination, '*.whl')) files = [] for filename in filenames: @@ -49,6 +54,12 @@ def populate_wheel_cache(wheel_cache_dir): return files +@pytest.fixture +def empty_wheel_cache(wheel_cache_dir): + if os.path.exists(wheel_cache_dir): + shutil.rmtree(wheel_cache_dir) + + def list_matches_wheel(wheel_name, lines): """Returns True if any line in `lines`, which should be the output of a `pip cache list` call, matches `wheel_name`. @@ -102,6 +113,14 @@ def test_cache_list(script): assert list_matches_wheel('zzz-7.8.9', lines) +@pytest.mark.usefixtures("empty_wheel_cache") +def test_cache_list_with_empty_cache(script): + """Running `pip cache list` with an empty cache should print + "Nothing cached." and exit.""" + result = script.pip('cache', 'list') + assert result.stdout == "Nothing cached.\n" + + def test_cache_list_too_many_args(script): """Passing `pip cache list` too many arguments should cause an error.""" script.pip('cache', 'list', 'aaa', 'bbb', From a20b28d0080cc69ff0fd901c15be5a7c11faeeee Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash <me@duckie.co> Date: Tue, 10 Mar 2020 15:04:07 -0400 Subject: [PATCH 1503/3170] [tests/functional/test_cache] Split apart tests for `pip cache purge`. --- tests/functional/test_cache.py | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index 55080dbf4f2..37a0de0737e 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -192,12 +192,23 @@ def test_cache_remove_name_and_version_match(script, remove_matches_wheel): @pytest.mark.usefixtures("populate_wheel_cache") -def test_cache_purge(script): +def test_cache_purge(script, remove_matches_wheel): + result = script.pip('cache', 'purge', '--verbose') + lines = result.stdout.splitlines() + + assert remove_matches_wheel('yyy-1.2.3', lines) + assert remove_matches_wheel('zzz-4.5.6', lines) + assert remove_matches_wheel('zzz-4.5.7', lines) + assert remove_matches_wheel('zzz-7.8.9', lines) + + +@pytest.mark.usefixtures("populate_wheel_cache") +def test_cache_purge_too_many_args(script, wheel_cache_files): result = script.pip('cache', 'purge', 'aaa', '--verbose', expect_error=True) - assert 'yyy-1.2.3' not in result.stdout - assert 'zzz-4.5.6' not in result.stdout + assert result.stdout == '' + assert result.stderr == 'ERROR: Too many arguments\n' - result = script.pip('cache', 'purge', '--verbose') - assert 'yyy-1.2.3' in result.stdout - assert 'zzz-4.5.6' in result.stdout + # Make sure nothing was deleted. + for filename in wheel_cache_files: + assert os.path.exists(filename) From 88582379038dbb91c13d5bc1b78ed5585e994cfb Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash <me@duckie.co> Date: Tue, 10 Mar 2020 15:08:18 -0400 Subject: [PATCH 1504/3170] [tests/functional/test_cache] Refactor list_matches_wheel() and remove_matches_wheel(). --- tests/functional/test_cache.py | 69 +++++++++++++++++----------------- 1 file changed, 35 insertions(+), 34 deletions(-) diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index 37a0de0737e..aa8dc13082f 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -60,26 +60,29 @@ def empty_wheel_cache(wheel_cache_dir): shutil.rmtree(wheel_cache_dir) -def list_matches_wheel(wheel_name, lines): - """Returns True if any line in `lines`, which should be the output of +def list_matches_wheel(wheel_name, result): + """Returns True if any line in `result`, which should be the output of a `pip cache list` call, matches `wheel_name`. E.g., If wheel_name is `foo-1.2.3` it searches for a line starting with `- foo-1.2.3-py3-none-any.whl `.""" + lines = result.stdout.splitlines() expected = ' - {}-py3-none-any.whl '.format(wheel_name) return any(map(lambda l: l.startswith(expected), lines)) @pytest.fixture def remove_matches_wheel(wheel_cache_dir): - """Returns True if any line in `lines`, which should be the output of + """Returns True if any line in `result`, which should be the output of a `pip cache remove`/`pip cache purge` call, matches `wheel_name`. E.g., If wheel_name is `foo-1.2.3`, it searches for a line equal to `Removed <wheel cache dir>/arbitrary/pathname/foo-1.2.3-py3-none-any.whl`. """ - def _remove_matches_wheel(wheel_name, lines): + def _remove_matches_wheel(wheel_name, result): + lines = result.stdout.splitlines() + wheel_filename = '{}-py3-none-any.whl'.format(wheel_name) # The "/arbitrary/pathname/" bit is an implementation detail of how @@ -106,11 +109,11 @@ def test_cache_list(script): """Running `pip cache list` should return exactly what the populate_wheel_cache fixture adds.""" result = script.pip('cache', 'list') - lines = result.stdout.splitlines() - assert list_matches_wheel('yyy-1.2.3', lines) - assert list_matches_wheel('zzz-4.5.6', lines) - assert list_matches_wheel('zzz-4.5.7', lines) - assert list_matches_wheel('zzz-7.8.9', lines) + + assert list_matches_wheel('yyy-1.2.3', result) + assert list_matches_wheel('zzz-4.5.6', result) + assert list_matches_wheel('zzz-4.5.7', result) + assert list_matches_wheel('zzz-7.8.9', result) @pytest.mark.usefixtures("empty_wheel_cache") @@ -132,12 +135,11 @@ def test_cache_list_name_match(script): """Running `pip cache list zzz` should list zzz-4.5.6, zzz-4.5.7, zzz-7.8.9, but nothing else.""" result = script.pip('cache', 'list', 'zzz', '--verbose') - lines = result.stdout.splitlines() - assert not list_matches_wheel('yyy-1.2.3', lines) - assert list_matches_wheel('zzz-4.5.6', lines) - assert list_matches_wheel('zzz-4.5.7', lines) - assert list_matches_wheel('zzz-7.8.9', lines) + assert not list_matches_wheel('yyy-1.2.3', result) + assert list_matches_wheel('zzz-4.5.6', result) + assert list_matches_wheel('zzz-4.5.7', result) + assert list_matches_wheel('zzz-7.8.9', result) @pytest.mark.usefixtures("populate_wheel_cache") @@ -145,12 +147,11 @@ def test_cache_list_name_and_version_match(script): """Running `pip cache list zzz-4.5.6` should list zzz-4.5.6, but nothing else.""" result = script.pip('cache', 'list', 'zzz-4.5.6', '--verbose') - lines = result.stdout.splitlines() - assert not list_matches_wheel('yyy-1.2.3', lines) - assert list_matches_wheel('zzz-4.5.6', lines) - assert not list_matches_wheel('zzz-4.5.7', lines) - assert not list_matches_wheel('zzz-7.8.9', lines) + assert not list_matches_wheel('yyy-1.2.3', result) + assert list_matches_wheel('zzz-4.5.6', result) + assert not list_matches_wheel('zzz-4.5.7', result) + assert not list_matches_wheel('zzz-7.8.9', result) @pytest.mark.usefixture("populate_wheel_cache") @@ -170,12 +171,11 @@ def test_cache_remove_name_match(script, remove_matches_wheel): """Running `pip cache remove zzz` should remove zzz-4.5.6 and zzz-7.8.9, but nothing else.""" result = script.pip('cache', 'remove', 'zzz', '--verbose') - lines = result.stdout.splitlines() - assert not remove_matches_wheel('yyy-1.2.3', lines) - assert remove_matches_wheel('zzz-4.5.6', lines) - assert remove_matches_wheel('zzz-4.5.7', lines) - assert remove_matches_wheel('zzz-7.8.9', lines) + assert not remove_matches_wheel('yyy-1.2.3', result) + assert remove_matches_wheel('zzz-4.5.6', result) + assert remove_matches_wheel('zzz-4.5.7', result) + assert remove_matches_wheel('zzz-7.8.9', result) @pytest.mark.usefixtures("populate_wheel_cache") @@ -183,27 +183,28 @@ def test_cache_remove_name_and_version_match(script, remove_matches_wheel): """Running `pip cache remove zzz-4.5.6` should remove zzz-4.5.6, but nothing else.""" result = script.pip('cache', 'remove', 'zzz-4.5.6', '--verbose') - lines = result.stdout.splitlines() - assert not remove_matches_wheel('yyy-1.2.3', lines) - assert remove_matches_wheel('zzz-4.5.6', lines) - assert not remove_matches_wheel('zzz-4.5.7', lines) - assert not remove_matches_wheel('zzz-7.8.9', lines) + assert not remove_matches_wheel('yyy-1.2.3', result) + assert remove_matches_wheel('zzz-4.5.6', result) + assert not remove_matches_wheel('zzz-4.5.7', result) + assert not remove_matches_wheel('zzz-7.8.9', result) @pytest.mark.usefixtures("populate_wheel_cache") def test_cache_purge(script, remove_matches_wheel): + """Running `pip cache purge` should remove all cached wheels.""" result = script.pip('cache', 'purge', '--verbose') - lines = result.stdout.splitlines() - assert remove_matches_wheel('yyy-1.2.3', lines) - assert remove_matches_wheel('zzz-4.5.6', lines) - assert remove_matches_wheel('zzz-4.5.7', lines) - assert remove_matches_wheel('zzz-7.8.9', lines) + assert remove_matches_wheel('yyy-1.2.3', result) + assert remove_matches_wheel('zzz-4.5.6', result) + assert remove_matches_wheel('zzz-4.5.7', result) + assert remove_matches_wheel('zzz-7.8.9', result) @pytest.mark.usefixtures("populate_wheel_cache") def test_cache_purge_too_many_args(script, wheel_cache_files): + """Running `pip cache purge aaa` should raise an error and remove no + cached wheels.""" result = script.pip('cache', 'purge', 'aaa', '--verbose', expect_error=True) assert result.stdout == '' From b7239f5deedbfd030bc9b247214bf8bf590a561a Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash <me@duckie.co> Date: Tue, 10 Mar 2020 15:40:25 -0400 Subject: [PATCH 1505/3170] [tests/functional/test_cache] Remove unused import. --- tests/functional/test_cache.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index aa8dc13082f..c0112929d9e 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -1,5 +1,4 @@ import os -import re import shutil from glob import glob From 0c4eafad6263fe7e6755635e041cecc390d1a57e Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash <me@duckie.co> Date: Tue, 10 Mar 2020 16:00:28 -0400 Subject: [PATCH 1506/3170] [tests/functional/test_cache] Fix test on Python 2.7. --- tests/functional/test_cache.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index c0112929d9e..1ab7fa6457e 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -207,7 +207,10 @@ def test_cache_purge_too_many_args(script, wheel_cache_files): result = script.pip('cache', 'purge', 'aaa', '--verbose', expect_error=True) assert result.stdout == '' - assert result.stderr == 'ERROR: Too many arguments\n' + + # This would be `result.stderr == ...`, but Pip prints deprecation + # warnings on Python 2.7, so we check if the _line_ is in stderr. + assert 'ERROR: Too many arguments' in result.stderr.splitlines() # Make sure nothing was deleted. for filename in wheel_cache_files: From b988417b4f0746c70d04f5a78cb92cfe15e338aa Mon Sep 17 00:00:00 2001 From: Ellen Marie Dash <me@duckie.co> Date: Fri, 13 Mar 2020 17:56:03 -0400 Subject: [PATCH 1507/3170] [tests/functional/test_cache] Use os.path.join() instead of hard-coding the path separator. --- tests/functional/test_cache.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index 1ab7fa6457e..a464ece7945 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -86,9 +86,10 @@ def _remove_matches_wheel(wheel_name, result): # The "/arbitrary/pathname/" bit is an implementation detail of how # the `populate_wheel_cache` fixture is implemented. - expected = 'Removed {}/arbitrary/pathname/{}'.format( - wheel_cache_dir, wheel_filename, + path = os.path.join( + wheel_cache_dir, 'arbitrary', 'pathname', wheel_filename, ) + expected = 'Removed {}'.format(path) return expected in lines return _remove_matches_wheel From 6b7f4ce81bb1d0e96608d27ad4bb79010d00607c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sat, 1 Feb 2020 13:39:45 +0100 Subject: [PATCH 1508/3170] Add DirectUrl model, implementing PEP 610 --- src/pip/_internal/models/direct_url.py | 245 +++++++++++++++++++++++++ tests/unit/test_direct_url.py | 151 +++++++++++++++ 2 files changed, 396 insertions(+) create mode 100644 src/pip/_internal/models/direct_url.py create mode 100644 tests/unit/test_direct_url.py diff --git a/src/pip/_internal/models/direct_url.py b/src/pip/_internal/models/direct_url.py new file mode 100644 index 00000000000..87bd9fe4b8f --- /dev/null +++ b/src/pip/_internal/models/direct_url.py @@ -0,0 +1,245 @@ +""" PEP 610 """ +import json +import re + +from pip._vendor import six +from pip._vendor.six.moves.urllib import parse as urllib_parse + +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import ( + Any, Dict, Iterable, Optional, Type, TypeVar, Union + ) + + T = TypeVar("T") + + +DIRECT_URL_METADATA_NAME = "direct_url.json" +ENV_VAR_RE = re.compile(r"^\$\{[A-Za-z0-9-_]+\}(:\$\{[A-Za-z0-9-_]+\})?$") + +__all__ = [ + "DirectUrl", + "DirectUrlValidationError", + "DirInfo", + "ArchiveInfo", + "VcsInfo", +] + + +class DirectUrlValidationError(Exception): + pass + + +def _get(d, expected_type, key, default=None): + # type: (Dict[str, Any], Type[T], str, Optional[T]) -> Optional[T] + """Get value from dictionary and verify expected type.""" + if key not in d: + return default + value = d[key] + if six.PY2 and expected_type is str: + expected_type = six.string_types # type: ignore + if not isinstance(value, expected_type): + raise DirectUrlValidationError( + "{!r} has unexpected type for {} (expected {})".format( + value, key, expected_type + ) + ) + return value + + +def _get_required(d, expected_type, key, default=None): + # type: (Dict[str, Any], Type[T], str, Optional[T]) -> T + value = _get(d, expected_type, key, default) + if value is None: + raise DirectUrlValidationError("{} must have a value".format(key)) + return value + + +def _exactly_one_of(infos): + # type: (Iterable[Optional[InfoType]]) -> InfoType + infos = [info for info in infos if info is not None] + if not infos: + raise DirectUrlValidationError( + "missing one of archive_info, dir_info, vcs_info" + ) + if len(infos) > 1: + raise DirectUrlValidationError( + "more than one of archive_info, dir_info, vcs_info" + ) + assert infos[0] is not None + return infos[0] + + +def _filter_none(**kwargs): + # type: (Any) -> Dict[str, Any] + """Make dict excluding None values.""" + return {k: v for k, v in kwargs.items() if v is not None} + + +class VcsInfo(object): + name = "vcs_info" + + def __init__( + self, + vcs, # type: str + commit_id, # type: str + requested_revision=None, # type: Optional[str] + resolved_revision=None, # type: Optional[str] + resolved_revision_type=None, # type: Optional[str] + ): + self.vcs = vcs + self.requested_revision = requested_revision + self.commit_id = commit_id + self.resolved_revision = resolved_revision + self.resolved_revision_type = resolved_revision_type + + @classmethod + def _from_dict(cls, d): + # type: (Optional[Dict[str, Any]]) -> Optional[VcsInfo] + if d is None: + return None + return cls( + vcs=_get_required(d, str, "vcs"), + commit_id=_get_required(d, str, "commit_id"), + requested_revision=_get(d, str, "requested_revision"), + resolved_revision=_get(d, str, "resolved_revision"), + resolved_revision_type=_get(d, str, "resolved_revision_type"), + ) + + def _to_dict(self): + # type: () -> Dict[str, Any] + return _filter_none( + vcs=self.vcs, + requested_revision=self.requested_revision, + commit_id=self.commit_id, + resolved_revision=self.resolved_revision, + resolved_revision_type=self.resolved_revision_type, + ) + + +class ArchiveInfo(object): + name = "archive_info" + + def __init__( + self, + hash=None, # type: Optional[str] + ): + self.hash = hash + + @classmethod + def _from_dict(cls, d): + # type: (Optional[Dict[str, Any]]) -> Optional[ArchiveInfo] + if d is None: + return None + return cls(hash=_get(d, str, "hash")) + + def _to_dict(self): + # type: () -> Dict[str, Any] + return _filter_none(hash=self.hash) + + +class DirInfo(object): + name = "dir_info" + + def __init__( + self, + editable=False, # type: bool + ): + self.editable = editable + + @classmethod + def _from_dict(cls, d): + # type: (Optional[Dict[str, Any]]) -> Optional[DirInfo] + if d is None: + return None + return cls( + editable=_get_required(d, bool, "editable", default=False) + ) + + def _to_dict(self): + # type: () -> Dict[str, Any] + return _filter_none(editable=self.editable or None) + + +if MYPY_CHECK_RUNNING: + InfoType = Union[ArchiveInfo, DirInfo, VcsInfo] + + +class DirectUrl(object): + + def __init__( + self, + url, # type: str + info, # type: InfoType + subdirectory=None, # type: Optional[str] + ): + self.url = url + self.info = info + self.subdirectory = subdirectory + + def _remove_auth_from_netloc(self, netloc): + # type: (str) -> str + if "@" not in netloc: + return netloc + user_pass, netloc_no_user_pass = netloc.split("@", 1) + if ( + isinstance(self.info, VcsInfo) and + self.info.vcs == "git" and + user_pass == "git" + ): + return netloc + if ENV_VAR_RE.match(user_pass): + return netloc + return netloc_no_user_pass + + @property + def redacted_url(self): + # type: () -> str + """url with user:password part removed unless it is formed with + environment variables as specified in PEP 610, or it is ``git`` + in the case of a git URL. + """ + purl = urllib_parse.urlsplit(self.url) + netloc = self._remove_auth_from_netloc(purl.netloc) + surl = urllib_parse.urlunsplit( + (purl.scheme, netloc, purl.path, purl.query, purl.fragment) + ) + return surl + + def validate(self): + # type: () -> None + self.from_dict(self.to_dict()) + + @classmethod + def from_dict(cls, d): + # type: (Dict[str, Any]) -> DirectUrl + return DirectUrl( + url=_get_required(d, str, "url"), + subdirectory=_get(d, str, "subdirectory"), + info=_exactly_one_of( + [ + ArchiveInfo._from_dict(_get(d, dict, "archive_info")), + DirInfo._from_dict(_get(d, dict, "dir_info")), + VcsInfo._from_dict(_get(d, dict, "vcs_info")), + ] + ), + ) + + def to_dict(self): + # type: () -> Dict[str, Any] + res = _filter_none( + url=self.redacted_url, + subdirectory=self.subdirectory, + ) + res[self.info.name] = self.info._to_dict() + return res + + @classmethod + def from_json(cls, s): + # type: (str) -> DirectUrl + return cls.from_dict(json.loads(s)) + + def to_json(self): + # type: () -> str + return json.dumps(self.to_dict(), sort_keys=True) diff --git a/tests/unit/test_direct_url.py b/tests/unit/test_direct_url.py new file mode 100644 index 00000000000..ee6b7fbf4ea --- /dev/null +++ b/tests/unit/test_direct_url.py @@ -0,0 +1,151 @@ +import pytest + +from pip._internal.models.direct_url import ( + ArchiveInfo, + DirectUrl, + DirectUrlValidationError, + DirInfo, + VcsInfo, +) + + +def test_from_json(): + json = '{"url": "file:///home/user/project", "dir_info": {}}' + direct_url = DirectUrl.from_json(json) + assert direct_url.url == "file:///home/user/project" + assert direct_url.info.editable is False + + +def test_to_json(): + direct_url = DirectUrl( + url="file:///home/user/archive.tgz", + info=ArchiveInfo(), + ) + direct_url.validate() + assert direct_url.to_json() == ( + '{"archive_info": {}, "url": "file:///home/user/archive.tgz"}' + ) + + +def test_archive_info(): + direct_url_dict = { + "url": "file:///home/user/archive.tgz", + "archive_info": { + "hash": "sha1=1b8c5bc61a86f377fea47b4276c8c8a5842d2220" + }, + } + direct_url = DirectUrl.from_dict(direct_url_dict) + assert isinstance(direct_url.info, ArchiveInfo) + assert direct_url.url == direct_url_dict["url"] + assert direct_url.info.hash == direct_url_dict["archive_info"]["hash"] + assert direct_url.to_dict() == direct_url_dict + + +def test_dir_info(): + direct_url_dict = { + "url": "file:///home/user/project", + "dir_info": {"editable": True}, + } + direct_url = DirectUrl.from_dict(direct_url_dict) + assert isinstance(direct_url.info, DirInfo) + assert direct_url.url == direct_url_dict["url"] + assert direct_url.info.editable is True + assert direct_url.to_dict() == direct_url_dict + # test editable default to False + direct_url_dict = {"url": "file:///home/user/project", "dir_info": {}} + direct_url = DirectUrl.from_dict(direct_url_dict) + assert direct_url.info.editable is False + + +def test_vcs_info(): + direct_url_dict = { + "url": "https:///g.c/u/p.git", + "vcs_info": { + "vcs": "git", + "requested_revision": "master", + "commit_id": "1b8c5bc61a86f377fea47b4276c8c8a5842d2220", + }, + } + direct_url = DirectUrl.from_dict(direct_url_dict) + assert isinstance(direct_url.info, VcsInfo) + assert direct_url.url == direct_url_dict["url"] + assert direct_url.info.vcs == "git" + assert direct_url.info.requested_revision == "master" + assert ( + direct_url.info.commit_id == "1b8c5bc61a86f377fea47b4276c8c8a5842d2220" + ) + assert direct_url.to_dict() == direct_url_dict + + +def test_parsing_validation(): + with pytest.raises( + DirectUrlValidationError, match="url must have a value" + ): + DirectUrl.from_dict({"dir_info": {}}) + with pytest.raises( + DirectUrlValidationError, + match="missing one of archive_info, dir_info, vcs_info", + ): + DirectUrl.from_dict({"url": "http://..."}) + with pytest.raises( + DirectUrlValidationError, match="unexpected type for editable" + ): + DirectUrl.from_dict( + {"url": "http://...", "dir_info": {"editable": "false"}} + ) + with pytest.raises( + DirectUrlValidationError, match="unexpected type for hash" + ): + DirectUrl.from_dict({"url": "http://...", "archive_info": {"hash": 1}}) + with pytest.raises( + DirectUrlValidationError, match="unexpected type for vcs" + ): + DirectUrl.from_dict({"url": "http://...", "vcs_info": {"vcs": None}}) + with pytest.raises( + DirectUrlValidationError, match="commit_id must have a value" + ): + DirectUrl.from_dict({"url": "http://...", "vcs_info": {"vcs": "git"}}) + with pytest.raises( + DirectUrlValidationError, + match="more than one of archive_info, dir_info, vcs_info", + ): + DirectUrl.from_dict( + {"url": "http://...", "dir_info": {}, "archive_info": {}} + ) + + +def test_redact_url(): + def _redact_git(url): + direct_url = DirectUrl( + url=url, + info=VcsInfo(vcs="git", commit_id="1"), + ) + return direct_url.redacted_url + + def _redact_archive(url): + direct_url = DirectUrl( + url=url, + info=ArchiveInfo(), + ) + return direct_url.redacted_url + + assert ( + _redact_git("https://user:password@g.c/u/p.git@branch#egg=pkg") == + "https://g.c/u/p.git@branch#egg=pkg" + ) + assert ( + _redact_git("https://${USER}:password@g.c/u/p.git") == + "https://g.c/u/p.git" + ) + assert ( + _redact_archive("file://${U}:${PIP_PASSWORD}@g.c/u/p.tgz") == + "file://${U}:${PIP_PASSWORD}@g.c/u/p.tgz" + ) + assert ( + _redact_git("https://${PIP_TOKEN}@g.c/u/p.git") == + "https://${PIP_TOKEN}@g.c/u/p.git" + ) + assert ( + _redact_git("ssh://git@g.c/u/p.git") == + "ssh://git@g.c/u/p.git" + ) From 6f689f61db2c5247656d8ecede022cf95477ead3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sat, 1 Feb 2020 13:40:06 +0100 Subject: [PATCH 1509/3170] Add helper to convert DirectUrl to PEP 440 direct reference --- src/pip/_internal/utils/direct_url_helpers.py | 34 ++++++++++ tests/unit/test_direct_url_helpers.py | 68 +++++++++++++++++++ 2 files changed, 102 insertions(+) create mode 100644 src/pip/_internal/utils/direct_url_helpers.py create mode 100644 tests/unit/test_direct_url_helpers.py diff --git a/src/pip/_internal/utils/direct_url_helpers.py b/src/pip/_internal/utils/direct_url_helpers.py new file mode 100644 index 00000000000..fd094f8442c --- /dev/null +++ b/src/pip/_internal/utils/direct_url_helpers.py @@ -0,0 +1,34 @@ +from pip._internal.models.direct_url import ( + ArchiveInfo, + DirectUrl, + DirInfo, + VcsInfo, +) + + +def direct_url_as_pep440_direct_reference(direct_url, name): + # type: (DirectUrl, str) -> str + """Convert a DirectUrl to a pip requirement string.""" + direct_url.validate() # if invalid, this is a pip bug + requirement = name + " @ " + fragments = [] + if isinstance(direct_url.info, VcsInfo): + requirement += "{}+{}@{}".format( + direct_url.info.vcs, direct_url.url, direct_url.info.commit_id + ) + elif isinstance(direct_url.info, ArchiveInfo): + requirement += direct_url.url + if direct_url.info.hash: + fragments.append(direct_url.info.hash) + else: + assert isinstance(direct_url.info, DirInfo) + # pip should never reach this point for editables, since + # pip freeze inspects the editable project location to produce + # the requirement string + assert not direct_url.info.editable + requirement += direct_url.url + if direct_url.subdirectory: + fragments.append("subdirectory=" + direct_url.subdirectory) + if fragments: + requirement += "#" + "&".join(fragments) + return requirement diff --git a/tests/unit/test_direct_url_helpers.py b/tests/unit/test_direct_url_helpers.py new file mode 100644 index 00000000000..6d6be1f3da7 --- /dev/null +++ b/tests/unit/test_direct_url_helpers.py @@ -0,0 +1,68 @@ +from pip._internal.models.direct_url import ( + ArchiveInfo, + DirectUrl, + DirInfo, + VcsInfo, +) +from pip._internal.utils.direct_url_helpers import ( + direct_url_as_pep440_direct_reference, +) + + +def test_as_pep440_requirement_archive(): + direct_url = DirectUrl( + url="file:///home/user/archive.tgz", + info=ArchiveInfo(), + ) + direct_url.validate() + assert ( + direct_url_as_pep440_direct_reference(direct_url, "pkg") == + "pkg @ file:///home/user/archive.tgz" + ) + direct_url.subdirectory = "subdir" + direct_url.validate() + assert ( + direct_url_as_pep440_direct_reference(direct_url, "pkg") == + "pkg @ file:///home/user/archive.tgz#subdirectory=subdir" + ) + direct_url.info.hash = "sha1=1b8c5bc61a86f377fea47b4276c8c8a5842d2220" + direct_url.validate() + assert ( + direct_url_as_pep440_direct_reference(direct_url, "pkg") == + "pkg @ file:///home/user/archive.tgz" + "#sha1=1b8c5bc61a86f377fea47b4276c8c8a5842d2220&subdirectory=subdir" + ) + + +def test_as_pep440_requirement_dir(): + direct_url = DirectUrl( + url="file:///home/user/project", + info=DirInfo(editable=False), + ) + direct_url.validate() + assert ( + direct_url_as_pep440_direct_reference(direct_url, "pkg") == + "pkg @ file:///home/user/project" + ) + + +def test_as_pep440_requirement_vcs(): + direct_url = DirectUrl( + url="https:///g.c/u/p.git", + info=VcsInfo( + vcs="git", commit_id="1b8c5bc61a86f377fea47b4276c8c8a5842d2220" + ) + ) + direct_url.validate() + assert ( + direct_url_as_pep440_direct_reference(direct_url, "pkg") == + "pkg @ git+https:///g.c/u/p.git" + "@1b8c5bc61a86f377fea47b4276c8c8a5842d2220" + ) + direct_url.subdirectory = "subdir" + direct_url.validate() + assert ( + direct_url_as_pep440_direct_reference(direct_url, "pkg") == + "pkg @ git+https:///g.c/u/p.git" + "@1b8c5bc61a86f377fea47b4276c8c8a5842d2220#subdirectory=subdir" + ) From 88582c2564d98d4a6397e7dc3cc1e047661e761f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sat, 1 Feb 2020 13:40:11 +0100 Subject: [PATCH 1510/3170] Add helper to create a DirectUrl from a Link --- src/pip/_internal/utils/direct_url_helpers.py | 55 +++++++++++ tests/unit/test_direct_url_helpers.py | 99 +++++++++++++++++++ 2 files changed, 154 insertions(+) diff --git a/src/pip/_internal/utils/direct_url_helpers.py b/src/pip/_internal/utils/direct_url_helpers.py index fd094f8442c..9611f3679b7 100644 --- a/src/pip/_internal/utils/direct_url_helpers.py +++ b/src/pip/_internal/utils/direct_url_helpers.py @@ -4,6 +4,11 @@ DirInfo, VcsInfo, ) +from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from pip._internal.vcs import vcs + +if MYPY_CHECK_RUNNING: + from pip._internal.models.link import Link def direct_url_as_pep440_direct_reference(direct_url, name): @@ -32,3 +37,53 @@ def direct_url_as_pep440_direct_reference(direct_url, name): if fragments: requirement += "#" + "&".join(fragments) return requirement + + +def direct_url_from_link(link, source_dir=None, link_is_in_wheel_cache=False): + # type: (Link, Optional[str], bool) -> DirectUrl + if link.is_vcs: + vcs_backend = vcs.get_backend_for_scheme(link.scheme) + assert vcs_backend + url, requested_revision, _ = ( + vcs_backend.get_url_rev_and_auth(link.url_without_fragment) + ) + # For VCS links, we need to find out and add commit_id. + if link_is_in_wheel_cache: + # If the requested VCS link corresponds to a cached + # wheel, it means the requested revision was an + # immutable commit hash, otherwise it would not have + # been cached. In that case we don't have a source_dir + # with the VCS checkout. + assert requested_revision + commit_id = requested_revision + else: + # If the wheel was not in cache, it means we have + # had to checkout from VCS to build and we have a source_dir + # which we can inspect to find out the commit id. + assert source_dir + commit_id = vcs_backend.get_revision(source_dir) + return DirectUrl( + url=url, + info=VcsInfo( + vcs=vcs_backend.name, + commit_id=commit_id, + requested_revision=requested_revision, + ), + subdirectory=link.subdirectory_fragment, + ) + elif link.is_existing_dir(): + return DirectUrl( + url=link.url_without_fragment, + info=DirInfo(), + subdirectory=link.subdirectory_fragment, + ) + else: + hash = None + hash_name = link.hash_name + if hash_name: + hash = "{}={}".format(hash_name, link.hash) + return DirectUrl( + url=link.url_without_fragment, + info=ArchiveInfo(hash=hash), + subdirectory=link.subdirectory_fragment, + ) diff --git a/tests/unit/test_direct_url_helpers.py b/tests/unit/test_direct_url_helpers.py index 6d6be1f3da7..87a37692983 100644 --- a/tests/unit/test_direct_url_helpers.py +++ b/tests/unit/test_direct_url_helpers.py @@ -1,12 +1,19 @@ +from functools import partial + +from mock import patch + from pip._internal.models.direct_url import ( ArchiveInfo, DirectUrl, DirInfo, VcsInfo, ) +from pip._internal.models.link import Link from pip._internal.utils.direct_url_helpers import ( direct_url_as_pep440_direct_reference, + direct_url_from_link, ) +from pip._internal.utils.urls import path_to_url def test_as_pep440_requirement_archive(): @@ -66,3 +73,95 @@ def test_as_pep440_requirement_vcs(): "pkg @ git+https:///g.c/u/p.git" "@1b8c5bc61a86f377fea47b4276c8c8a5842d2220#subdirectory=subdir" ) + + +@patch("pip._internal.vcs.git.Git.get_revision") +def test_from_link_vcs(mock_get_backend_for_scheme): + _direct_url_from_link = partial(direct_url_from_link, source_dir="...") + direct_url = _direct_url_from_link(Link("git+https://g.c/u/p.git")) + assert direct_url.url == "https://g.c/u/p.git" + assert isinstance(direct_url.info, VcsInfo) + assert direct_url.info.vcs == "git" + direct_url = _direct_url_from_link(Link("git+https://g.c/u/p.git#egg=pkg")) + assert direct_url.url == "https://g.c/u/p.git" + direct_url = _direct_url_from_link( + Link("git+https://g.c/u/p.git#egg=pkg&subdirectory=subdir") + ) + assert direct_url.url == "https://g.c/u/p.git" + assert direct_url.subdirectory == "subdir" + direct_url = _direct_url_from_link(Link("git+https://g.c/u/p.git@branch")) + assert direct_url.url == "https://g.c/u/p.git" + assert direct_url.info.requested_revision == "branch" + direct_url = _direct_url_from_link( + Link("git+https://g.c/u/p.git@branch#egg=pkg") + ) + assert direct_url.url == "https://g.c/u/p.git" + assert direct_url.info.requested_revision == "branch" + direct_url = _direct_url_from_link( + Link("git+https://token@g.c/u/p.git") + ) + assert direct_url.to_dict()["url"] == "https://g.c/u/p.git" + + +def test_from_link_vcs_with_source_dir_obtains_commit_id(script, tmpdir): + repo_path = tmpdir / 'test-repo' + repo_path.mkdir() + repo_dir = str(repo_path) + script.run('git', 'init', cwd=repo_dir) + (repo_path / "somefile").touch() + script.run('git', 'add', '.', cwd=repo_dir) + script.run('git', 'commit', '-m', 'commit msg', cwd=repo_dir) + commit_id = script.run( + 'git', 'rev-parse', 'HEAD', cwd=repo_dir + ).stdout.strip() + direct_url = direct_url_from_link( + Link("git+https://g.c/u/p.git"), source_dir=repo_dir + ) + assert direct_url.url == "https://g.c/u/p.git" + assert direct_url.info.commit_id == commit_id + + +def test_from_link_vcs_without_source_dir(script, tmpdir): + direct_url = direct_url_from_link( + Link("git+https://g.c/u/p.git@1"), link_is_in_wheel_cache=True + ) + assert direct_url.url == "https://g.c/u/p.git" + assert direct_url.info.commit_id == "1" + + +def test_from_link_archive(): + direct_url = direct_url_from_link(Link("https://g.c/archive.tgz")) + assert direct_url.url == "https://g.c/archive.tgz" + assert isinstance(direct_url.info, ArchiveInfo) + direct_url = direct_url_from_link( + Link( + "https://g.c/archive.tgz" + "#sha1=1b8c5bc61a86f377fea47b4276c8c8a5842d2220" + ) + ) + assert isinstance(direct_url.info, ArchiveInfo) + assert ( + direct_url.info.hash == "sha1=1b8c5bc61a86f377fea47b4276c8c8a5842d2220" + ) + + +def test_from_link_dir(tmpdir): + dir_url = path_to_url(tmpdir) + direct_url = direct_url_from_link(Link(dir_url)) + assert direct_url.url == dir_url + assert isinstance(direct_url.info, DirInfo) + + +def test_from_link_hide_user_password(): + # Basic test only here, other variants are covered by + # direct_url.redact_url tests. + direct_url = direct_url_from_link( + Link("git+https://user:password@g.c/u/p.git@branch#egg=pkg"), + link_is_in_wheel_cache=True, + ) + assert direct_url.to_dict()["url"] == "https://g.c/u/p.git" + direct_url = direct_url_from_link( + Link("git+ssh://git@g.c/u/p.git@branch#egg=pkg"), + link_is_in_wheel_cache=True, + ) + assert direct_url.to_dict()["url"] == "ssh://git@g.c/u/p.git" From bd4d52b09cdd62525485f68c83b573bc17345e48 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sat, 1 Feb 2020 13:40:16 +0100 Subject: [PATCH 1511/3170] Add helper to get DirectUrl metadata from Distrribution --- src/pip/_internal/utils/direct_url_helpers.py | 41 +++++++++++++++++++ tests/unit/test_direct_url_helpers.py | 31 +++++++++++++- 2 files changed, 71 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/utils/direct_url_helpers.py b/src/pip/_internal/utils/direct_url_helpers.py index 9611f3679b7..f1fe209e911 100644 --- a/src/pip/_internal/utils/direct_url_helpers.py +++ b/src/pip/_internal/utils/direct_url_helpers.py @@ -1,15 +1,31 @@ +import logging + from pip._internal.models.direct_url import ( + DIRECT_URL_METADATA_NAME, ArchiveInfo, DirectUrl, + DirectUrlValidationError, DirInfo, VcsInfo, ) from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.vcs import vcs +try: + from json import JSONDecodeError +except ImportError: + # PY2 + JSONDecodeError = ValueError # type: ignore + if MYPY_CHECK_RUNNING: + from typing import Optional + from pip._internal.models.link import Link + from pip._vendor.pkg_resources import Distribution + +logger = logging.getLogger(__name__) + def direct_url_as_pep440_direct_reference(direct_url, name): # type: (DirectUrl, str) -> str @@ -87,3 +103,28 @@ def direct_url_from_link(link, source_dir=None, link_is_in_wheel_cache=False): info=ArchiveInfo(hash=hash), subdirectory=link.subdirectory_fragment, ) + + +def dist_get_direct_url(dist): + # type: (Distribution) -> Optional[DirectUrl] + """Obtain a DirectUrl from a pkg_resource.Distribution. + + Returns None if the distribution has no `direct_url.json` metadata, + or if `direct_url.json` is invalid. + """ + if not dist.has_metadata(DIRECT_URL_METADATA_NAME): + return None + try: + return DirectUrl.from_json(dist.get_metadata(DIRECT_URL_METADATA_NAME)) + except ( + DirectUrlValidationError, + JSONDecodeError, + UnicodeDecodeError + ) as e: + logger.warning( + "Error parsing %s for %s: %s", + DIRECT_URL_METADATA_NAME, + dist.project_name, + e, + ) + return None diff --git a/tests/unit/test_direct_url_helpers.py b/tests/unit/test_direct_url_helpers.py index 87a37692983..55cd5855b93 100644 --- a/tests/unit/test_direct_url_helpers.py +++ b/tests/unit/test_direct_url_helpers.py @@ -1,8 +1,9 @@ from functools import partial -from mock import patch +from mock import MagicMock, patch from pip._internal.models.direct_url import ( + DIRECT_URL_METADATA_NAME, ArchiveInfo, DirectUrl, DirInfo, @@ -12,6 +13,7 @@ from pip._internal.utils.direct_url_helpers import ( direct_url_as_pep440_direct_reference, direct_url_from_link, + dist_get_direct_url, ) from pip._internal.utils.urls import path_to_url @@ -165,3 +167,30 @@ def test_from_link_hide_user_password(): link_is_in_wheel_cache=True, ) assert direct_url.to_dict()["url"] == "ssh://git@g.c/u/p.git" + + +def test_dist_get_direct_url_no_metadata(): + dist = MagicMock() + dist.has_metadata.return_value = False + assert dist_get_direct_url(dist) is None + dist.has_metadata.assert_called() + + +def test_dist_get_direct_url_bad_metadata(): + dist = MagicMock() + dist.has_metadata.return_value = True + dist.get_metadata.return_value = "{}" # invalid direct_url.json + assert dist_get_direct_url(dist) is None + dist.get_metadata.assert_called_with(DIRECT_URL_METADATA_NAME) + + +def test_dist_get_direct_url_valid_metadata(): + dist = MagicMock() + dist.has_metadata.return_value = True + dist.get_metadata.return_value = ( + '{"url": "https://e.c/p.tgz", "archive_info": {}}' + ) + direct_url = dist_get_direct_url(dist) + dist.get_metadata.assert_called_with(DIRECT_URL_METADATA_NAME) + assert direct_url.url == "https://e.c/p.tgz" + assert isinstance(direct_url.info, ArchiveInfo) From f77944733ded1d1b3b6312f1413e3a6c3ce43397 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sat, 1 Feb 2020 13:40:20 +0100 Subject: [PATCH 1512/3170] Add DirectUrl support to install_wheel --- src/pip/_internal/operations/install/wheel.py | 14 +++++++- tests/unit/test_wheel.py | 36 +++++++++++++++++++ 2 files changed, 49 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index e66d12b4bf0..0b3fbe2ffc2 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -27,6 +27,7 @@ from pip._internal.exceptions import InstallationError from pip._internal.locations import get_major_minor_version +from pip._internal.models.direct_url import DIRECT_URL_METADATA_NAME, DirectUrl from pip._internal.utils.filesystem import adjacent_tmp_file, replace from pip._internal.utils.misc import captured_stdout, ensure_dir, hash_file from pip._internal.utils.temp_dir import TempDirectory @@ -289,7 +290,8 @@ def install_unpacked_wheel( scheme, # type: Scheme req_description, # type: str pycompile=True, # type: bool - warn_script_location=True # type: bool + warn_script_location=True, # type: bool + direct_url=None, # type: Optional[DirectUrl] ): # type: (...) -> None """Install a wheel. @@ -570,6 +572,14 @@ def is_entrypoint_wrapper(name): replace(installer_file.name, installer_path) generated.append(installer_path) + # Record the PEP 610 direct URL reference + if direct_url is not None: + direct_url_path = os.path.join(dest_info_dir, DIRECT_URL_METADATA_NAME) + with adjacent_tmp_file(direct_url_path) as direct_url_file: + direct_url_file.write(direct_url.to_json().encode("utf-8")) + replace(direct_url_file.name, direct_url_path) + generated.append(direct_url_path) + # Record details of all files installed record_path = os.path.join(dest_info_dir, 'RECORD') with open(record_path, **csv_io_kwargs('r')) as record_file: @@ -593,6 +603,7 @@ def install_wheel( pycompile=True, # type: bool warn_script_location=True, # type: bool _temp_dir_for_testing=None, # type: Optional[str] + direct_url=None, # type: Optional[DirectUrl] ): # type: (...) -> None with TempDirectory( @@ -607,4 +618,5 @@ def install_wheel( req_description=req_description, pycompile=pycompile, warn_script_location=warn_script_location, + direct_url=direct_url, ) diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 9328f6fb8bc..fc719e8905e 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -10,6 +10,11 @@ from pip._vendor.packaging.requirements import Requirement from pip._internal.locations import get_scheme +from pip._internal.models.direct_url import ( + DIRECT_URL_METADATA_NAME, + ArchiveInfo, + DirectUrl, +) from pip._internal.models.scheme import Scheme from pip._internal.operations.build.wheel_legacy import ( get_legacy_build_wheel_path, @@ -259,6 +264,37 @@ def test_std_install(self, data, tmpdir): ) self.assert_installed() + def test_std_install_with_direct_url(self, data, tmpdir): + """Test that install_wheel creates direct_url.json metadata when + provided with a direct_url argument. Also test that the RECORDS + file contains an entry for direct_url.json in that case. + Note direct_url.url is intentionally different from wheelpath, + because wheelpath is typically the result of a local build. + """ + self.prep(data, tmpdir) + direct_url = DirectUrl( + url="file:///home/user/archive.tgz", + info=ArchiveInfo(), + ) + wheel.install_wheel( + self.name, + self.wheelpath, + scheme=self.scheme, + req_description=str(self.req), + direct_url=direct_url, + ) + direct_url_path = os.path.join( + self.dest_dist_info, DIRECT_URL_METADATA_NAME + ) + assert os.path.isfile(direct_url_path) + with open(direct_url_path, 'rb') as f: + expected_direct_url_json = direct_url.to_json() + direct_url_json = f.read().decode("utf-8") + assert direct_url_json == expected_direct_url_json + # check that the direc_url file is part of RECORDS + with open(os.path.join(self.dest_dist_info, "RECORD")) as f: + assert DIRECT_URL_METADATA_NAME in f.read() + def test_install_prefix(self, data, tmpdir): prefix = os.path.join(os.path.sep, 'some', 'path') self.prep(data, tmpdir) From 94b77130aabb5d3ec2eba9002162fb565a68bbbd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sat, 1 Feb 2020 13:40:25 +0100 Subject: [PATCH 1513/3170] Add WheelCache method to inform which cache was used Return whether the link was found in the persistent or ephemeral cache. --- src/pip/_internal/cache.py | 34 ++++++++++++++++++++++++++++++++-- tests/unit/test_cache.py | 23 +++++++++++++++++++++++ 2 files changed, 55 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/cache.py b/src/pip/_internal/cache.py index d4398ba8f88..b534f0cfec3 100644 --- a/src/pip/_internal/cache.py +++ b/src/pip/_internal/cache.py @@ -270,6 +270,16 @@ def __init__(self, format_control): ) +class CacheEntry(object): + def __init__( + self, + link, # type: Link + persistent, # type: bool + ): + self.link = link + self.persistent = persistent + + class WheelCache(Cache): """Wraps EphemWheelCache and SimpleWheelCache into a single Cache @@ -304,16 +314,36 @@ def get( supported_tags, # type: List[Tag] ): # type: (...) -> Link + cache_entry = self.get_cache_entry(link, package_name, supported_tags) + if cache_entry is None: + return link + return cache_entry.link + + def get_cache_entry( + self, + link, # type: Link + package_name, # type: Optional[str] + supported_tags, # type: List[Tag] + ): + # type: (...) -> Optional[CacheEntry] + """Returns a CacheEntry with a link to a cached item if it exists or + None. The cache entry indicates if the item was found in the persistent + or ephemeral cache. + """ retval = self._wheel_cache.get( link=link, package_name=package_name, supported_tags=supported_tags, ) if retval is not link: - return retval + return CacheEntry(retval, persistent=True) - return self._ephem_cache.get( + retval = self._ephem_cache.get( link=link, package_name=package_name, supported_tags=supported_tags, ) + if retval is not link: + return CacheEntry(retval, persistent=False) + + return None diff --git a/tests/unit/test_cache.py b/tests/unit/test_cache.py index 31f8f729341..a289fb59890 100644 --- a/tests/unit/test_cache.py +++ b/tests/unit/test_cache.py @@ -90,3 +90,26 @@ def test_get_with_legacy_entry_only(tmpdir): os.path.normcase(os.path.dirname(cached_link.file_path)) == os.path.normcase(legacy_path) ) + + +def test_get_cache_entry(tmpdir): + wc = WheelCache(tmpdir, FormatControl()) + persi_link = Link("https://g.c/o/r/persi") + persi_path = wc.get_path_for_link(persi_link) + ensure_dir(persi_path) + with open(os.path.join(persi_path, "persi-1.0.0-py3-none-any.whl"), "w"): + pass + ephem_link = Link("https://g.c/o/r/ephem") + ephem_path = wc.get_ephem_path_for_link(ephem_link) + ensure_dir(ephem_path) + with open(os.path.join(ephem_path, "ephem-1.0.0-py3-none-any.whl"), "w"): + pass + other_link = Link("https://g.c/o/r/other") + supported_tags = [Tag("py3", "none", "any")] + assert ( + wc.get_cache_entry(persi_link, "persi", supported_tags).persistent + ) + assert ( + not wc.get_cache_entry(ephem_link, "ephem", supported_tags).persistent + ) + assert wc.get_cache_entry(other_link, "other", supported_tags) is None From a0ed759fb389e5234dbbf9efb439abe3956dfc17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sat, 1 Feb 2020 13:40:30 +0100 Subject: [PATCH 1514/3170] Add direct_url support to InstallRequirement pass it to install_wheel via install --- src/pip/_internal/req/req_install.py | 10 ++++ .../_internal/resolution/legacy/resolver.py | 10 ++-- tests/functional/test_install_direct_url.py | 48 +++++++++++++++++++ 3 files changed, 64 insertions(+), 4 deletions(-) create mode 100644 tests/functional/test_install_direct_url.py diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 7da6640502f..44c29d1b581 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -31,6 +31,7 @@ from pip._internal.pyproject import load_pyproject_toml, make_pyproject_path from pip._internal.req.req_uninstall import UninstallPathSet from pip._internal.utils.deprecation import deprecated +from pip._internal.utils.direct_url_helpers import direct_url_from_link from pip._internal.utils.hashes import Hashes from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import ( @@ -126,6 +127,7 @@ def __init__( # PEP 508 URL requirement link = Link(req.url) self.link = self.original_link = link + self.original_link_is_in_wheel_cache = False # Path to any downloaded or already-existing package. self.local_file_path = None # type: Optional[str] if self.link and self.link.is_file: @@ -785,6 +787,13 @@ def install( if self.is_wheel: assert self.local_file_path + direct_url = None + if self.original_link: + direct_url = direct_url_from_link( + self.original_link, + self.source_dir, + self.original_link_is_in_wheel_cache, + ) install_wheel( self.name, self.local_file_path, @@ -792,6 +801,7 @@ def install( req_description=str(self.req), pycompile=pycompile, warn_script_location=warn_script_location, + direct_url=direct_url, ) self.install_succeeded = True return diff --git a/src/pip/_internal/resolution/legacy/resolver.py b/src/pip/_internal/resolution/legacy/resolver.py index dff1398cdee..cdb44d19dbe 100644 --- a/src/pip/_internal/resolution/legacy/resolver.py +++ b/src/pip/_internal/resolution/legacy/resolver.py @@ -280,14 +280,16 @@ def _populate_link(self, req): if self.wheel_cache is None or self.preparer.require_hashes: return - cached_link = self.wheel_cache.get( + cache_entry = self.wheel_cache.get_cache_entry( link=req.link, package_name=req.name, supported_tags=get_supported(), ) - if req.link != cached_link: - logger.debug('Using cached wheel link: %s', cached_link) - req.link = cached_link + if cache_entry is not None: + logger.debug('Using cached wheel link: %s', cache_entry.link) + if req.link is req.original_link and cache_entry.persistent: + req.original_link_is_in_wheel_cache = True + req.link = cache_entry.link def _get_abstract_dist_for(self, req): # type: (InstallRequirement) -> AbstractDistribution diff --git a/tests/functional/test_install_direct_url.py b/tests/functional/test_install_direct_url.py new file mode 100644 index 00000000000..ec1e927ebf8 --- /dev/null +++ b/tests/functional/test_install_direct_url.py @@ -0,0 +1,48 @@ +import re + +from pip._internal.models.direct_url import DIRECT_URL_METADATA_NAME, DirectUrl +from tests.lib import _create_test_package, path_to_url + + +def _get_created_direct_url(result, pkg): + direct_url_metadata_re = re.compile( + pkg + r"-[\d\.]+\.dist-info." + DIRECT_URL_METADATA_NAME + r"$" + ) + for filename in result.files_created: + if direct_url_metadata_re.search(filename): + direct_url_path = result.test_env.base_path / filename + with open(direct_url_path) as f: + return DirectUrl.from_json(f.read()) + return None + + +def test_install_find_links_no_direct_url(script, with_wheel): + result = script.pip_install_local("simple") + assert not _get_created_direct_url(result, "simple") + + +def test_install_vcs_editable_no_direct_url(script, with_wheel): + pkg_path = _create_test_package(script, name="testpkg") + args = ["install", "-e", "git+%s#egg=testpkg" % path_to_url(pkg_path)] + result = script.pip(*args) + # legacy editable installs do not generate .dist-info, + # hence no direct_url.json + assert not _get_created_direct_url(result, "testpkg") + + +def test_install_vcs_non_editable_direct_url(script, with_wheel): + pkg_path = _create_test_package(script, name="testpkg") + url = path_to_url(pkg_path) + args = ["install", "git+{}#egg=testpkg".format(url)] + result = script.pip(*args) + direct_url = _get_created_direct_url(result, "testpkg") + assert direct_url + assert direct_url.url == url + assert direct_url.info.vcs == "git" + + +def test_install_archive_direct_url(script, data, with_wheel): + req = "simple @ " + path_to_url(data.packages / "simple-2.0.tar.gz") + assert req.startswith("simple @ file://") + result = script.pip("install", req) + assert _get_created_direct_url(result, "simple") From 196706d305c1e7cb296824c90d932918036f53e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sat, 1 Feb 2020 13:40:35 +0100 Subject: [PATCH 1515/3170] Better freeze using direct_url.json --- news/609.feature | 2 ++ src/pip/_internal/operations/freeze.py | 16 ++++++++++++++++ tests/functional/test_freeze.py | 8 ++++++++ 3 files changed, 26 insertions(+) create mode 100644 news/609.feature diff --git a/news/609.feature b/news/609.feature new file mode 100644 index 00000000000..1a2a6702a58 --- /dev/null +++ b/news/609.feature @@ -0,0 +1,2 @@ +pip now implements PEP 610, so ``pip freeze`` has better fidelity +in presence of distributions installed from Direct URL requirements. diff --git a/src/pip/_internal/operations/freeze.py b/src/pip/_internal/operations/freeze.py index 0ac3c4e9028..3198c775771 100644 --- a/src/pip/_internal/operations/freeze.py +++ b/src/pip/_internal/operations/freeze.py @@ -19,6 +19,10 @@ install_req_from_line, ) from pip._internal.req.req_file import COMMENT_RE +from pip._internal.utils.direct_url_helpers import ( + direct_url_as_pep440_direct_reference, + dist_get_direct_url, +) from pip._internal.utils.misc import ( dist_is_editable, get_installed_distributions, @@ -250,8 +254,20 @@ def __init__(self, name, req, editable, comments=()): @classmethod def from_dist(cls, dist): # type: (Distribution) -> FrozenRequirement + # TODO `get_requirement_info` is taking care of editable requirements. + # TODO This should be refactored when we will add detection of + # editable that provide .dist-info metadata. req, editable, comments = get_requirement_info(dist) + if req is None and not editable: + # if PEP 610 metadata is present, attempt to use it + direct_url = dist_get_direct_url(dist) + if direct_url: + req = direct_url_as_pep440_direct_reference( + direct_url, dist.project_name + ) + comments = [] if req is None: + # name==version requirement req = dist.as_requirement() return cls(dist.project_name, req, editable, comments=comments) diff --git a/tests/functional/test_freeze.py b/tests/functional/test_freeze.py index dabfbde483c..7027879bd44 100644 --- a/tests/functional/test_freeze.py +++ b/tests/functional/test_freeze.py @@ -816,3 +816,11 @@ def test_freeze_path_multiple(tmpdir, script, data): simple2==3.0 <BLANKLINE>""") _check_output(result.stdout, expected) + + +def test_freeze_direct_url_archive(script, shared_data, with_wheel): + req = "simple @ " + path_to_url(shared_data.packages / "simple-2.0.tar.gz") + assert req.startswith("simple @ file://") + script.pip("install", req) + result = script.pip("freeze") + assert req in result.stdout From 4a0dd985343b3385d426624d21f5d83d9fa4af93 Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Thu, 2 Apr 2020 00:09:24 -0500 Subject: [PATCH 1516/3170] add ability to run pip with options for yaml tests --- tests/functional/test_yaml.py | 34 +++++++++++++++------------------- tests/yaml/install/simple.yml | 7 +++++++ 2 files changed, 22 insertions(+), 19 deletions(-) diff --git a/tests/functional/test_yaml.py b/tests/functional/test_yaml.py index cef76d2ca69..caf625fc07e 100644 --- a/tests/functional/test_yaml.py +++ b/tests/functional/test_yaml.py @@ -92,14 +92,17 @@ def stripping_split(my_str, splitwith, count=None): return retval -def handle_install_request(script, requirement): +def handle_install_request(script, requirement, options): assert isinstance(requirement, str), ( "Need install requirement to be a string only" ) - result = script.pip( - "install", - "--no-index", "--find-links", path_to_url(script.scratch_path), - requirement, "--verbose", + args = ["install", "--no-index", "--find-links", + path_to_url(script.scratch_path)] + args.append(requirement) + args.extend(options) + args.append("--verbose") + + result = script.pip(*args, allow_stderr_error=True, allow_stderr_warning=True, ) @@ -168,24 +171,17 @@ def test_yaml_based(script, case): create_basic_wheel_for_package(script, **package) - available_actions = { - "install": handle_install_request - } - # use scratch path for index for request, expected in zip(requests, transaction): - # The name of the key is what action has to be taken - assert len(request.keys()) == 1, "Expected only one action" - - # Get the only key - action = list(request.keys())[0] - - assert action in available_actions.keys(), ( - "Unsupported action {!r}".format(action) - ) # Perform the requested action - effect = available_actions[action](script, request[action]) + if 'install' in request: + effect = handle_install_request( + script, + request['install'], + request.get('options', '').split()) + else: + assert False, "Unsupported request {!r}".format(request) result = effect["_result_object"] del effect["_result_object"] diff --git a/tests/yaml/install/simple.yml b/tests/yaml/install/simple.yml index e1062fd310b..676b918bc50 100644 --- a/tests/yaml/install/simple.yml +++ b/tests/yaml/install/simple.yml @@ -19,3 +19,10 @@ cases: - install: - base 0.1.0 - dep 0.1.0 +- + request: + - install: base + options: --no-deps + transaction: + - install: + - base 0.1.0 From 3f2129e04094ca1cc8a036868878dbcc32381c9a Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Thu, 2 Apr 2020 00:34:55 -0500 Subject: [PATCH 1517/3170] make flake8 happy --- tests/functional/test_yaml.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/functional/test_yaml.py b/tests/functional/test_yaml.py index caf625fc07e..28fccbe7c19 100644 --- a/tests/functional/test_yaml.py +++ b/tests/functional/test_yaml.py @@ -103,9 +103,8 @@ def handle_install_request(script, requirement, options): args.append("--verbose") result = script.pip(*args, - allow_stderr_error=True, - allow_stderr_warning=True, - ) + allow_stderr_error=True, + allow_stderr_warning=True) retval = { "_result_object": result, From 190c424b1e711e2ddf83a7eb04d140774dcd28e4 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 1 Apr 2020 18:14:36 +0800 Subject: [PATCH 1518/3170] Implement Python as a dependency If a dist contains Requires-Python metadata, it is converted into a Requirement for the resolver based on whether the Requires-Python is compatible or not. If it is compatible, an ExplicitRequirement is returned to hold the Python information (either sys.version_info, or the user-supplied --python-version). If it is incompatible, a special NoMatchRequirement is returned, which never matches to anything, generating a ResolutionImpossible to report the Python version incompatibility. The --ignore-requires-python flag is implemented as to not return a Requirement for Requires-Python at all. --- .../resolution/resolvelib/candidates.py | 61 +++++++++++++++++-- .../resolution/resolvelib/factory.py | 24 +++++++- .../resolution/resolvelib/requirements.py | 21 +++++++ .../resolution/resolvelib/resolver.py | 2 + tests/unit/resolution_resolvelib/conftest.py | 2 + 5 files changed, 103 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index d2acfe1e144..1f3cca9c5da 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -1,21 +1,26 @@ import logging +import sys +from pip._vendor.packaging.specifiers import InvalidSpecifier, SpecifierSet from pip._vendor.packaging.utils import canonicalize_name +from pip._vendor.packaging.version import Version from pip._internal.req.constructors import install_req_from_line from pip._internal.req.req_install import InstallRequirement +from pip._internal.utils.misc import normalize_version_info +from pip._internal.utils.packaging import get_requires_python from pip._internal.utils.typing import MYPY_CHECK_RUNNING from .base import Candidate, format_name if MYPY_CHECK_RUNNING: - from typing import Any, Optional, Sequence, Set - - from pip._internal.models.link import Link + from typing import Any, Optional, Sequence, Set, Tuple from pip._vendor.packaging.version import _BaseVersion from pip._vendor.pkg_resources import Distribution + from pip._internal.models.link import Link + from .base import Requirement from .factory import Factory @@ -95,12 +100,32 @@ def dist(self): self._version == self.dist.parsed_version) return self._dist + def _get_requires_python_specifier(self): + # type: () -> Optional[SpecifierSet] + requires_python = get_requires_python(self.dist) + if requires_python is None: + return None + try: + spec = SpecifierSet(requires_python) + except InvalidSpecifier as e: + logger.warning( + "Package %r has an invalid Requires-Python: %s", self.name, e, + ) + return None + return spec + def get_dependencies(self): # type: () -> Sequence[Requirement] - return [ + deps = [ self._factory.make_requirement_from_spec(str(r), self._ireq) for r in self.dist.requires() ] + python_dep = self._factory.make_requires_python_requirement( + self._get_requires_python_specifier(), + ) + if python_dep: + deps.append(python_dep) + return deps def get_install_requirement(self): # type: () -> Optional[InstallRequirement] @@ -179,3 +204,31 @@ def get_install_requirement(self): # depend on the base candidate, and we'll get the # install requirement from that. return None + + +class RequiresPythonCandidate(Candidate): + def __init__(self, py_version_info): + # type: (Optional[Tuple[int, ...]]) -> None + if py_version_info is not None: + version_info = normalize_version_info(py_version_info) + else: + version_info = sys.version_info[:3] + self._version = Version(".".join(str(c) for c in version_info)) + + @property + def name(self): + # type: () -> str + return "<Python>" # Avoid conflicting with the PyPI package "Python". + + @property + def version(self): + # type: () -> _BaseVersion + return self._version + + def get_dependencies(self): + # type: () -> Sequence[Requirement] + return [] + + def get_install_requirement(self): + # type: () -> Optional[InstallRequirement] + return None diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index b5a490e6ab0..c005675176c 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -1,10 +1,16 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from .candidates import ExtrasCandidate, LinkCandidate -from .requirements import ExplicitRequirement, SpecifierRequirement +from .candidates import ExtrasCandidate, LinkCandidate, RequiresPythonCandidate +from .requirements import ( + ExplicitRequirement, + NoMatchRequirement, + SpecifierRequirement, +) if MYPY_CHECK_RUNNING: - from typing import Dict, Set + from typing import Dict, Optional, Set, Tuple + + from pip._vendor.packaging.specifiers import SpecifierSet from pip._internal.index.package_finder import PackageFinder from pip._internal.models.link import Link @@ -21,10 +27,14 @@ def __init__( finder, # type: PackageFinder preparer, # type: RequirementPreparer make_install_req, # type: InstallRequirementProvider + ignore_requires_python, # type: bool + py_version_info=None, # type: Optional[Tuple[int, ...]] ): # type: (...) -> None self.finder = finder self.preparer = preparer + self._python_candidate = RequiresPythonCandidate(py_version_info) + self._ignore_requires_python = ignore_requires_python self._make_install_req_from_spec = make_install_req self._candidate_cache = {} # type: Dict[Link, LinkCandidate] @@ -56,3 +66,11 @@ def make_requirement_from_spec(self, specifier, comes_from): # type: (str, InstallRequirement) -> Requirement ireq = self._make_install_req_from_spec(specifier, comes_from) return self.make_requirement_from_install_req(ireq) + + def make_requires_python_requirement(self, specifier): + # type: (Optional[SpecifierSet]) -> Optional[Requirement] + if self._ignore_requires_python or specifier is None: + return None + if self._python_candidate.version in specifier: + return ExplicitRequirement(self._python_candidate) + return NoMatchRequirement(self._python_candidate.name) diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py index 1fe328597cb..cdfa93c2c67 100644 --- a/src/pip/_internal/resolution/resolvelib/requirements.py +++ b/src/pip/_internal/resolution/resolvelib/requirements.py @@ -33,6 +33,27 @@ def is_satisfied_by(self, candidate): return candidate == self.candidate +class NoMatchRequirement(Requirement): + """A requirement that never matches anything. + """ + def __init__(self, name): + # type: (str) -> None + self._name = canonicalize_name(name) + + @property + def name(self): + # type: () -> str + return self._name + + def find_matches(self): + # type: () -> Sequence[Candidate] + return [] + + def is_satisfied_by(self, candidate): + # type: (Candidate) -> bool + return False + + class SpecifierRequirement(Requirement): def __init__(self, ireq, factory): # type: (InstallRequirement, Factory) -> None diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 7c8be9df58d..d2ec3d094a3 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -43,6 +43,8 @@ def __init__( finder=finder, preparer=preparer, make_install_req=make_install_req, + ignore_requires_python=ignore_requires_python, + py_version_info=py_version_info, ) self.ignore_dependencies = ignore_dependencies self._result = None # type: Optional[Result] diff --git a/tests/unit/resolution_resolvelib/conftest.py b/tests/unit/resolution_resolvelib/conftest.py index de268e829a1..0bea2bf8116 100644 --- a/tests/unit/resolution_resolvelib/conftest.py +++ b/tests/unit/resolution_resolvelib/conftest.py @@ -52,6 +52,8 @@ def factory(finder, preparer): finder=finder, preparer=preparer, make_install_req=install_req_from_line, + ignore_requires_python=False, + py_version_info=None, ) From 630339e577ebcb7b6116b96b28a00ec5b672a3dd Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 2 Apr 2020 04:27:29 +0800 Subject: [PATCH 1519/3170] Add new resolver test for Requires-Python --- tests/functional/test_new_resolver.py | 55 +++++++++++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index bb258b72e3f..e2b3da1b814 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -1,6 +1,9 @@ import json +import pytest + from tests.lib import create_basic_wheel_for_package +from tests.lib.wheel import make_wheel def assert_installed(script, **kwargs): @@ -137,3 +140,55 @@ def test_new_resolver_installs_extras(script): assert "WARNING: Invalid extras specified" in result.stderr, str(result) assert ": missing" in result.stderr, str(result) assert_installed(script, base="0.1.0", dep="0.1.0") + + +@pytest.mark.parametrize( + "requires_python, ignore_requires_python, dep_version", + [ + # Something impossible to satisfy. + ("<2", False, "0.1.0"), + ("<2", True, "0.2.0"), + + # Something guarentees to satisfy. + (">=2", False, "0.2.0"), + (">=2", True, "0.2.0"), + ], +) +def test_new_resolver_requires_python( + script, + requires_python, + ignore_requires_python, + dep_version, +): + create_basic_wheel_for_package( + script, + "base", + "0.1.0", + depends=["dep"], + ) + + # TODO: Use create_basic_wheel_for_package when it handles Requires-Python. + make_wheel( + "dep", + "0.1.0", + ).save_to_dir(script.scratch_path) + make_wheel( + "dep", + "0.2.0", + metadata_updates={"Requires-Python": requires_python}, + ).save_to_dir(script.scratch_path) + + args = [ + "install", + "--unstable-feature=resolver", + "--no-cache-dir", + "--no-index", + "--find-links", script.scratch_path, + ] + if ignore_requires_python: + args.append("--ignore-requires-python") + args.append("base") + + script.pip(*args) + + assert_installed(script, base="0.1.0", dep=dep_version) From 22f7c883ad8afb7e1841d878ddfe9531a9fb592f Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 2 Apr 2020 18:38:41 +0800 Subject: [PATCH 1520/3170] Requires-Python support in test helper --- tests/lib/__init__.py | 20 +++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index 67c66b9ce22..682f66c711e 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -979,7 +979,13 @@ def add_file(path, text): def create_basic_wheel_for_package( - script, name, version, depends=None, extras=None, extra_files=None + script, + name, + version, + depends=None, + extras=None, + requires_python=None, + extra_files=None, ): if depends is None: depends = [] @@ -1007,14 +1013,18 @@ def hello(): for package in packages ] + metadata_updates = { + "Provides-Extra": list(extras), + "Requires-Dist": requires_dist, + } + if requires_python is not None: + metadata_updates["Requires-Python"] = requires_python + wheel_builder = make_wheel( name=name, version=version, wheel_metadata_updates={"Tag": ["py2-none-any", "py3-none-any"]}, - metadata_updates={ - "Provides-Extra": list(extras), - "Requires-Dist": requires_dist, - }, + metadata_updates=metadata_updates, extra_metadata_files={"top_level.txt": name}, extra_files=extra_files, From 34c24f6e795f7eca44bca7d8d66383c49e3f7b0b Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 2 Apr 2020 18:40:48 +0800 Subject: [PATCH 1521/3170] Switch to create_basic_wheel_for_package --- tests/functional/test_new_resolver.py | 15 +++++++-------- 1 file changed, 7 insertions(+), 8 deletions(-) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index e2b3da1b814..6d4200f872d 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -3,7 +3,6 @@ import pytest from tests.lib import create_basic_wheel_for_package -from tests.lib.wheel import make_wheel def assert_installed(script, **kwargs): @@ -166,17 +165,17 @@ def test_new_resolver_requires_python( "0.1.0", depends=["dep"], ) - - # TODO: Use create_basic_wheel_for_package when it handles Requires-Python. - make_wheel( + create_basic_wheel_for_package( + script, "dep", "0.1.0", - ).save_to_dir(script.scratch_path) - make_wheel( + ) + create_basic_wheel_for_package( + script, "dep", "0.2.0", - metadata_updates={"Requires-Python": requires_python}, - ).save_to_dir(script.scratch_path) + requires_python=requires_python, + ) args = [ "install", From 65ffd338f4eb39ed46daf4c4eeb0a7c1fa19951c Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 2 Apr 2020 18:50:23 +0800 Subject: [PATCH 1522/3170] Explain how Requires-Python matching works --- src/pip/_internal/resolution/resolvelib/factory.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index c005675176c..4c4bf2bee75 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -71,6 +71,11 @@ def make_requires_python_requirement(self, specifier): # type: (Optional[SpecifierSet]) -> Optional[Requirement] if self._ignore_requires_python or specifier is None: return None + # The logic here is different from SpecifierRequirement, for which we + # "find" candidates matching the specifier. But for Requires-Python, + # there is always exactly one candidate (the one specified with + # py_version_info). Here we decide whether to return that based on + # whether Requires-Python matches that one candidate or not. if self._python_candidate.version in specifier: return ExplicitRequirement(self._python_candidate) return NoMatchRequirement(self._python_candidate.name) From 957c093a106995ec5b903b19bc314fd376b783f7 Mon Sep 17 00:00:00 2001 From: Nikolay Korolev <CrafterKolyan@mail.ru> Date: Thu, 2 Apr 2020 14:07:15 +0300 Subject: [PATCH 1523/3170] Speed up pip list --outdated --- src/pip/_internal/commands/list.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index cce470a6051..fc09bde9ee2 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -5,9 +5,11 @@ import json import logging +import concurrent.futures from pip._vendor import six from pip._vendor.six.moves import zip_longest +from pip._vendor.requests.adapters import DEFAULT_POOLSIZE from pip._internal.cli import cmdoptions from pip._internal.cli.req_command import IndexGroupCommand @@ -182,10 +184,10 @@ def get_not_required(self, packages, options): def iter_packages_latest_infos(self, packages, options): with self._build_session(options) as session: finder = self._build_package_finder(options, session) - - for dist in packages: + with concurrent.futures.ThreadPoolExecutor(max_workers=DEFAULT_POOLSIZE) as executor: + all_candidates_list = executor.map(finder.find_all_candidates, [dist.key for dist in packages]) + for dist, all_candidates in zip(packages, all_candidates_list): typ = 'unknown' - all_candidates = finder.find_all_candidates(dist.key) if not options.pre: # Remove prereleases all_candidates = [candidate for candidate in all_candidates From 3151566154a493fcd417dc7b0639b7efdb38bb83 Mon Sep 17 00:00:00 2001 From: Nikolay Korolev <korolevns98@gmail.com> Date: Thu, 2 Apr 2020 14:14:09 +0300 Subject: [PATCH 1524/3170] Add news --- news/7962.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/7962.bugfix diff --git a/news/7962.bugfix b/news/7962.bugfix new file mode 100644 index 00000000000..b1437031df7 --- /dev/null +++ b/news/7962.bugfix @@ -0,0 +1 @@ +`pip list --outdated` version fetching is multi-threaded From f8e2b5d5a0b234ea664a66b06c52ad303dc03dab Mon Sep 17 00:00:00 2001 From: Nikolay Korolev <korolevns98@gmail.com> Date: Thu, 2 Apr 2020 14:19:34 +0300 Subject: [PATCH 1525/3170] Make indent_log thread-safe --- src/pip/_internal/utils/logging.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pip/_internal/utils/logging.py b/src/pip/_internal/utils/logging.py index 134f7908d9d..0143434ee2f 100644 --- a/src/pip/_internal/utils/logging.py +++ b/src/pip/_internal/utils/logging.py @@ -104,6 +104,8 @@ def indent_log(num=2): A context manager which will cause the log output to be indented for any log messages emitted inside it. """ + # For thread-safety + _log_state.indentation = get_indentation() _log_state.indentation += num try: yield From 829a3b20c4620cc4a06bd937315eba96f1c86628 Mon Sep 17 00:00:00 2001 From: Nikolay Korolev <korolevns98@gmail.com> Date: Thu, 2 Apr 2020 14:25:02 +0300 Subject: [PATCH 1526/3170] Fix Python 2 compatibility --- src/pip/_internal/commands/list.py | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index fc09bde9ee2..7a9f3ff9347 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -5,11 +5,11 @@ import json import logging -import concurrent.futures +from multiprocessing.dummy import Pool from pip._vendor import six -from pip._vendor.six.moves import zip_longest from pip._vendor.requests.adapters import DEFAULT_POOLSIZE +from pip._vendor.six.moves import zip_longest from pip._internal.cli import cmdoptions from pip._internal.cli.req_command import IndexGroupCommand @@ -184,8 +184,15 @@ def get_not_required(self, packages, options): def iter_packages_latest_infos(self, packages, options): with self._build_session(options) as session: finder = self._build_package_finder(options, session) - with concurrent.futures.ThreadPoolExecutor(max_workers=DEFAULT_POOLSIZE) as executor: - all_candidates_list = executor.map(finder.find_all_candidates, [dist.key for dist in packages]) + + # Doing multithreading in Python 2 compatible way + executor = Pool(DEFAULT_POOLSIZE) + all_candidates_list = executor.map( + finder.find_all_candidates, + [dist.key for dist in packages] + ) + executor.terminate() + for dist, all_candidates in zip(packages, all_candidates_list): typ = 'unknown' if not options.pre: From 1d4fc03fe299102f7cf4db8e136093b11a63ab64 Mon Sep 17 00:00:00 2001 From: Nikolay Korolev <korolevns98@gmail.com> Date: Thu, 2 Apr 2020 16:20:09 +0300 Subject: [PATCH 1527/3170] Change Pool to ThreadPool and fix minor closing --- src/pip/_internal/commands/list.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index 7a9f3ff9347..0f9d84cc2a7 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -5,7 +5,7 @@ import json import logging -from multiprocessing.dummy import Pool +from multiprocessing.pool import ThreadPool from pip._vendor import six from pip._vendor.requests.adapters import DEFAULT_POOLSIZE @@ -186,12 +186,13 @@ def iter_packages_latest_infos(self, packages, options): finder = self._build_package_finder(options, session) # Doing multithreading in Python 2 compatible way - executor = Pool(DEFAULT_POOLSIZE) + executor = ThreadPool(DEFAULT_POOLSIZE) all_candidates_list = executor.map( finder.find_all_candidates, [dist.key for dist in packages] ) - executor.terminate() + executor.close() + executor.join() for dist, all_candidates in zip(packages, all_candidates_list): typ = 'unknown' From ccb0b5cc0814e21653cde30675f6aece992f1e72 Mon Sep 17 00:00:00 2001 From: Nikolay Korolev <korolevns98@gmail.com> Date: Thu, 2 Apr 2020 16:38:54 +0300 Subject: [PATCH 1528/3170] Lazier calculations --- src/pip/_internal/commands/list.py | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index 0f9d84cc2a7..1fd45ee77a4 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -185,17 +185,9 @@ def iter_packages_latest_infos(self, packages, options): with self._build_session(options) as session: finder = self._build_package_finder(options, session) - # Doing multithreading in Python 2 compatible way - executor = ThreadPool(DEFAULT_POOLSIZE) - all_candidates_list = executor.map( - finder.find_all_candidates, - [dist.key for dist in packages] - ) - executor.close() - executor.join() - - for dist, all_candidates in zip(packages, all_candidates_list): + def latest_infos(dist): typ = 'unknown' + all_candidates = finder.find_all_candidates(dist.key) if not options.pre: # Remove prereleases all_candidates = [candidate for candidate in all_candidates @@ -206,7 +198,7 @@ def iter_packages_latest_infos(self, packages, options): ) best_candidate = evaluator.sort_best_candidate(all_candidates) if best_candidate is None: - continue + return None remote_version = best_candidate.version if best_candidate.link.is_wheel: @@ -216,7 +208,16 @@ def iter_packages_latest_infos(self, packages, options): # This is dirty but makes the rest of the code much cleaner dist.latest_version = remote_version dist.latest_filetype = typ - yield dist + return dist + + pool = ThreadPool(DEFAULT_POOLSIZE) + + for dist in pool.imap_unordered(latest_infos, packages): + if dist is not None: + yield dist + + pool.close() + pool.join() def output_package_listing(self, packages, options): packages = sorted( From 9ca2240530070d3719a42abfbe8c5f4677633a04 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 2 Apr 2020 21:42:26 +0800 Subject: [PATCH 1529/3170] More clarification on the name --- src/pip/_internal/resolution/resolvelib/candidates.py | 3 ++- src/pip/_internal/resolution/resolvelib/requirements.py | 5 ++++- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 1f3cca9c5da..31453dbd4d4 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -218,7 +218,8 @@ def __init__(self, py_version_info): @property def name(self): # type: () -> str - return "<Python>" # Avoid conflicting with the PyPI package "Python". + # Avoid conflicting with the PyPI package "Python". + return "<Python fom Requires-Python>" @property def version(self): diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py index cdfa93c2c67..b8711534db6 100644 --- a/src/pip/_internal/resolution/resolvelib/requirements.py +++ b/src/pip/_internal/resolution/resolvelib/requirements.py @@ -35,10 +35,13 @@ def is_satisfied_by(self, candidate): class NoMatchRequirement(Requirement): """A requirement that never matches anything. + + Note: Similar to ExplicitRequirement, the caller should handle name + canonicalisation; this class does not perform it. """ def __init__(self, name): # type: (str) -> None - self._name = canonicalize_name(name) + self._name = name @property def name(self): From 557f7670ea8b870692b26610a38dfbceb1af056a Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 2 Apr 2020 21:44:54 +0800 Subject: [PATCH 1530/3170] Typo in comment --- tests/functional/test_new_resolver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 6d4200f872d..9bf34651b79 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -148,7 +148,7 @@ def test_new_resolver_installs_extras(script): ("<2", False, "0.1.0"), ("<2", True, "0.2.0"), - # Something guarentees to satisfy. + # Something guaranteed to satisfy. (">=2", False, "0.2.0"), (">=2", True, "0.2.0"), ], From 03bfdbcc58582b0dbf23606179ff54765add8885 Mon Sep 17 00:00:00 2001 From: Nikolay Korolev <korolevns98@gmail.com> Date: Thu, 2 Apr 2020 17:15:47 +0300 Subject: [PATCH 1531/3170] Refactoring. Rename inner function --- src/pip/_internal/commands/list.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index 1fd45ee77a4..974b6810116 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -185,7 +185,7 @@ def iter_packages_latest_infos(self, packages, options): with self._build_session(options) as session: finder = self._build_package_finder(options, session) - def latest_infos(dist): + def latest_info(dist): typ = 'unknown' all_candidates = finder.find_all_candidates(dist.key) if not options.pre: @@ -212,7 +212,7 @@ def latest_infos(dist): pool = ThreadPool(DEFAULT_POOLSIZE) - for dist in pool.imap_unordered(latest_infos, packages): + for dist in pool.imap_unordered(latest_info, packages): if dist is not None: yield dist From f061f3f691a68c413486beab61323991e6c1df0a Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 2 Apr 2020 19:49:45 +0800 Subject: [PATCH 1532/3170] Fetch install dist for a candidate if available The candidate creation logic is further moved into the factory. The factory would use pkg_resources.get_distribution() to find a matching distribution for a givan InstallationCandidate. If found, the Candidate would be created based on that found distribution, instead of the link. --ignore-installed is implemented as to always use the link to create candidates, even if an installed distribution is found. --- .../resolution/resolvelib/candidates.py | 70 +++++++++++++++- .../resolution/resolvelib/factory.py | 79 ++++++++++++++++--- .../resolution/resolvelib/requirements.py | 4 +- .../resolution/resolvelib/resolver.py | 1 + tests/functional/test_new_resolver.py | 62 +++++++++++++++ tests/unit/resolution_resolvelib/conftest.py | 1 + 6 files changed, 201 insertions(+), 16 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 31453dbd4d4..8eedb4034d4 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -14,7 +14,7 @@ from .base import Candidate, format_name if MYPY_CHECK_RUNNING: - from typing import Any, Optional, Sequence, Set, Tuple + from typing import Any, Optional, Sequence, Set, Tuple, Union from pip._vendor.packaging.version import _BaseVersion from pip._vendor.pkg_resources import Distribution @@ -45,6 +45,28 @@ def make_install_req_from_link(link, parent): ) +def make_install_req_from_dist(dist, parent): + # type: (Distribution, InstallRequirement) -> InstallRequirement + # TODO: Do we need to support editables? + ireq = install_req_from_line( + "{}=={}".format( + canonicalize_name(dist.project_name), + dist.parsed_version, + ), + comes_from=parent.comes_from, + use_pep517=parent.use_pep517, + isolated=parent.isolated, + constraint=parent.constraint, + options=dict( + install_options=parent.install_options, + global_options=parent.global_options, + hashes=parent.hash_options + ), + ) + ireq.satisfied_by = dist + return ireq + + class LinkCandidate(Candidate): def __init__(self, link, parent, factory): # type: (Link, InstallRequirement, Factory) -> None @@ -132,7 +154,48 @@ def get_install_requirement(self): return self._ireq -class ExtrasCandidate(LinkCandidate): +class InstalledCandidate(Candidate): + def __init__( + self, + dist, # type: Distribution + parent, # type: InstallRequirement + factory, # type: Factory + ): + # type: (...) -> None + self.dist = dist + self._ireq = make_install_req_from_dist(dist, parent) + self._factory = factory + + # This is just logging some messages, so we can do it eagerly. + # The returned dist would be exactly the same as self.dist because we + # set satisfied_by in make_install_req_from_dist. + # TODO: Supply reason based on force_reinstall and upgrade_strategy. + skip_reason = "already satisfied" + factory.preparer.prepare_installed_requirement(self._ireq, skip_reason) + + @property + def name(self): + # type: () -> str + return canonicalize_name(self.dist.project_name) + + @property + def version(self): + # type: () -> _BaseVersion + return self.dist.parsed_version + + def get_dependencies(self): + # type: () -> Sequence[Requirement] + return [ + self._factory.make_requirement_from_spec(str(r), self._ireq) + for r in self.dist.requires() + ] + + def get_install_requirement(self): + # type: () -> Optional[InstallRequirement] + return None + + +class ExtrasCandidate(Candidate): """A candidate that has 'extras', indicating additional dependencies. Requirements can be for a project with dependencies, something like @@ -157,10 +220,9 @@ class ExtrasCandidate(LinkCandidate): respectively forces the resolver to recognise that this is a conflict. """ def __init__(self, base, extras): - # type: (LinkCandidate, Set[str]) -> None + # type: (Union[InstalledCandidate, LinkCandidate], Set[str]) -> None self.base = base self.extras = extras - self.link = base.link @property def name(self): diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 4c4bf2bee75..2a9d263c1d3 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -1,6 +1,17 @@ +from pip._vendor.pkg_resources import ( + DistributionNotFound, + VersionConflict, + get_distribution, +) + from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from .candidates import ExtrasCandidate, LinkCandidate, RequiresPythonCandidate +from .candidates import ( + ExtrasCandidate, + InstalledCandidate, + LinkCandidate, + RequiresPythonCandidate, +) from .requirements import ( ExplicitRequirement, NoMatchRequirement, @@ -11,8 +22,10 @@ from typing import Dict, Optional, Set, Tuple from pip._vendor.packaging.specifiers import SpecifierSet + from pip._vendor.pkg_resources import Distribution from pip._internal.index.package_finder import PackageFinder + from pip._internal.models.candidate import InstallationCandidate from pip._internal.models.link import Link from pip._internal.operations.prepare import RequirementPreparer from pip._internal.req.req_install import InstallRequirement @@ -27,6 +40,7 @@ def __init__( finder, # type: PackageFinder preparer, # type: RequirementPreparer make_install_req, # type: InstallRequirementProvider + ignore_installed, # type: bool ignore_requires_python, # type: bool py_version_info=None, # type: Optional[Tuple[int, ...]] ): @@ -34,33 +48,78 @@ def __init__( self.finder = finder self.preparer = preparer self._python_candidate = RequiresPythonCandidate(py_version_info) - self._ignore_requires_python = ignore_requires_python self._make_install_req_from_spec = make_install_req - self._candidate_cache = {} # type: Dict[Link, LinkCandidate] + self._ignore_installed = ignore_installed + self._ignore_requires_python = ignore_requires_python + self._link_candidate_cache = {} # type: Dict[Link, LinkCandidate] - def make_candidate( + def _make_candidate_from_dist( + self, + dist, # type: Distribution + extras, # type: Set[str] + parent, # type: InstallRequirement + ): + # type: (...) -> Candidate + base = InstalledCandidate(dist, parent, factory=self) + if extras: + return ExtrasCandidate(base, extras) + return base + + def _make_candidate_from_link( self, link, # type: Link extras, # type: Set[str] parent, # type: InstallRequirement ): # type: (...) -> Candidate - if link not in self._candidate_cache: - self._candidate_cache[link] = LinkCandidate( + if link not in self._link_candidate_cache: + self._link_candidate_cache[link] = LinkCandidate( link, parent, factory=self, ) - base = self._candidate_cache[link] + base = self._link_candidate_cache[link] if extras: return ExtrasCandidate(base, extras) return base + def _get_installed_distribution(self, name, version): + # type: (str, str) -> Optional[Distribution] + if self._ignore_installed: + return None + specifier = "{}=={}".format(name, version) + try: + dist = get_distribution(specifier) + except (DistributionNotFound, VersionConflict): + return None + return dist + + def make_candidate_from_ican( + self, + ican, # type: InstallationCandidate + extras, # type: Set[str] + parent, # type: InstallRequirement + ): + # type: (...) -> Candidate + dist = self._get_installed_distribution(ican.name, ican.version) + if dist is None: + return self._make_candidate_from_link( + link=ican.link, + extras=extras, + parent=parent, + ) + return self._make_candidate_from_dist( + dist=dist, + extras=extras, + parent=parent, + ) + def make_requirement_from_install_req(self, ireq): # type: (InstallRequirement) -> Requirement if ireq.link: - cand = self.make_candidate(ireq.link, extras=set(), parent=ireq) + cand = self._make_candidate_from_link( + ireq.link, extras=set(), parent=ireq, + ) return ExplicitRequirement(cand) - else: - return SpecifierRequirement(ireq, factory=self) + return SpecifierRequirement(ireq, factory=self) def make_requirement_from_spec(self, specifier, comes_from): # type: (str, InstallRequirement) -> Requirement diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py index b8711534db6..033e02cafbd 100644 --- a/src/pip/_internal/resolution/resolvelib/requirements.py +++ b/src/pip/_internal/resolution/resolvelib/requirements.py @@ -79,8 +79,8 @@ def find_matches(self): hashes=self._ireq.hashes(trust_internet=False), ) return [ - self._factory.make_candidate( - link=ican.link, + self._factory.make_candidate_from_ican( + ican=ican, extras=self.extras, parent=self._ireq, ) diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index d2ec3d094a3..9a9eedee7c2 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -43,6 +43,7 @@ def __init__( finder=finder, preparer=preparer, make_install_req=make_install_req, + ignore_installed=ignore_installed, ignore_requires_python=ignore_requires_python, py_version_info=py_version_info, ) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 9bf34651b79..812207986ad 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -191,3 +191,65 @@ def test_new_resolver_requires_python( script.pip(*args) assert_installed(script, base="0.1.0", dep=dep_version) + + +def test_new_resolver_installed(script): + create_basic_wheel_for_package( + script, + "base", + "0.1.0", + depends=["dep"], + ) + create_basic_wheel_for_package( + script, + "dep", + "0.1.0", + ) + satisfied_output = "Requirement already satisfied: base==0.1.0 in" + + result = script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "base", + ) + assert satisfied_output not in result.stdout, str(result) + + result = script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "base", + ) + assert satisfied_output in result.stdout, str(result) + assert script.site_packages / "base" not in result.files_updated, ( + "base 0.1.0 reinstalled" + ) + + +def test_new_resolver_ignore_installed(script): + create_basic_wheel_for_package( + script, + "base", + "0.1.0", + ) + satisfied_output = "Requirement already satisfied: base==0.1.0 in" + + result = script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "base", + ) + assert satisfied_output not in result.stdout, str(result) + + result = script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", "--ignore-installed", + "--find-links", script.scratch_path, + "base", + ) + assert satisfied_output not in result.stdout, str(result) + assert script.site_packages / "base" in result.files_updated, ( + "base 0.1.0 not reinstalled" + ) diff --git a/tests/unit/resolution_resolvelib/conftest.py b/tests/unit/resolution_resolvelib/conftest.py index 0bea2bf8116..9cd233c8efb 100644 --- a/tests/unit/resolution_resolvelib/conftest.py +++ b/tests/unit/resolution_resolvelib/conftest.py @@ -52,6 +52,7 @@ def factory(finder, preparer): finder=finder, preparer=preparer, make_install_req=install_req_from_line, + ignore_installed=False, ignore_requires_python=False, py_version_info=None, ) From 7511737ee79275c429bda210a1041448d2e59db4 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 3 Apr 2020 01:27:34 +0800 Subject: [PATCH 1533/3170] AlreadyInstalledCandidate --- src/pip/_internal/resolution/resolvelib/candidates.py | 10 +++++++--- src/pip/_internal/resolution/resolvelib/factory.py | 4 ++-- 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 8eedb4034d4..c0439f896d0 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -154,7 +154,7 @@ def get_install_requirement(self): return self._ireq -class InstalledCandidate(Candidate): +class AlreadyInstalledCandidate(Candidate): def __init__( self, dist, # type: Distribution @@ -219,8 +219,12 @@ class ExtrasCandidate(Candidate): version 2.0. Having those candidates depend on foo=1.0 and foo=2.0 respectively forces the resolver to recognise that this is a conflict. """ - def __init__(self, base, extras): - # type: (Union[InstalledCandidate, LinkCandidate], Set[str]) -> None + def __init__( + self, + base, # type: Union[AlreadyInstalledCandidate, LinkCandidate] + extras, # type: Set[str] + ): + # type: (...) -> None self.base = base self.extras = extras diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 2a9d263c1d3..335d5beea6d 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -7,8 +7,8 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING from .candidates import ( + AlreadyInstalledCandidate, ExtrasCandidate, - InstalledCandidate, LinkCandidate, RequiresPythonCandidate, ) @@ -60,7 +60,7 @@ def _make_candidate_from_dist( parent, # type: InstallRequirement ): # type: (...) -> Candidate - base = InstalledCandidate(dist, parent, factory=self) + base = AlreadyInstalledCandidate(dist, parent, factory=self) if extras: return ExtrasCandidate(base, extras) return base From f695354bf8103ea18774e45626c71115213078bc Mon Sep 17 00:00:00 2001 From: Nikolay Korolev <korolevns98@gmail.com> Date: Thu, 2 Apr 2020 21:55:34 +0300 Subject: [PATCH 1534/3170] Add commentary --- src/pip/_internal/commands/list.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index 974b6810116..db5e7979391 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -210,6 +210,10 @@ def latest_info(dist): dist.latest_filetype = typ return dist + # This is done to multithread requests to pypi.org and eventually + # get performance boost so that "real time" of this function is + # almost equal to "user time". Also this gives performance + # boost up to 2x pool = ThreadPool(DEFAULT_POOLSIZE) for dist in pool.imap_unordered(latest_info, packages): From 6b8c191ee2953b23178854457cdc84c0e2ed8ed4 Mon Sep 17 00:00:00 2001 From: Nikolay Korolev <korolevns98@gmail.com> Date: Thu, 2 Apr 2020 21:57:10 +0300 Subject: [PATCH 1535/3170] Update comment --- src/pip/_internal/commands/list.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index db5e7979391..9526b49e2a7 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -211,9 +211,8 @@ def latest_info(dist): return dist # This is done to multithread requests to pypi.org and eventually - # get performance boost so that "real time" of this function is - # almost equal to "user time". Also this gives performance - # boost up to 2x + # get performance boost up to 2x so that "real time" of this + # function is almost equal to "user time" pool = ThreadPool(DEFAULT_POOLSIZE) for dist in pool.imap_unordered(latest_info, packages): From 9b9e137d1dcceeb6b56fc8623d11bf159c38fe47 Mon Sep 17 00:00:00 2001 From: Nikolay Korolev <korolevns98@gmail.com> Date: Thu, 2 Apr 2020 22:01:22 +0300 Subject: [PATCH 1536/3170] Update comment --- src/pip/_internal/commands/list.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index 9526b49e2a7..513b516615b 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -210,9 +210,9 @@ def latest_info(dist): dist.latest_filetype = typ return dist - # This is done to multithread requests to pypi.org and eventually - # get performance boost up to 2x so that "real time" of this - # function is almost equal to "user time" + # This is done for 2x speed up of requests to pypi.org + # so that "real time" of this function + # is almost equal to "user time" pool = ThreadPool(DEFAULT_POOLSIZE) for dist in pool.imap_unordered(latest_info, packages): From 5cccd4ef6b7150f653edc9f4409c90cecd2d13ed Mon Sep 17 00:00:00 2001 From: Nikolay Korolev <korolevns98@gmail.com> Date: Fri, 3 Apr 2020 09:07:09 +0300 Subject: [PATCH 1537/3170] Change ThreadPool to Pool --- src/pip/_internal/commands/list.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index 513b516615b..2d909ff11e0 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -5,7 +5,7 @@ import json import logging -from multiprocessing.pool import ThreadPool +from multiprocessing.dummy import Pool from pip._vendor import six from pip._vendor.requests.adapters import DEFAULT_POOLSIZE @@ -213,7 +213,7 @@ def latest_info(dist): # This is done for 2x speed up of requests to pypi.org # so that "real time" of this function # is almost equal to "user time" - pool = ThreadPool(DEFAULT_POOLSIZE) + pool = Pool(DEFAULT_POOLSIZE) for dist in pool.imap_unordered(latest_info, packages): if dist is not None: From 037791d1706b7f57a83c1947608bb828d06c0e4d Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Fri, 3 Apr 2020 10:51:22 +0100 Subject: [PATCH 1538/3170] Add a test demonstrating #7966 --- tests/functional/test_new_resolver.py | 43 +++++++++++++++++++++- tests/lib/__init__.py | 52 ++++++++++++++++++++++++++- 2 files changed, 93 insertions(+), 2 deletions(-) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 812207986ad..90309f471b9 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -2,7 +2,10 @@ import pytest -from tests.lib import create_basic_wheel_for_package +from tests.lib import ( + create_basic_sdist_for_package, + create_basic_wheel_for_package, +) def assert_installed(script, **kwargs): @@ -253,3 +256,41 @@ def test_new_resolver_ignore_installed(script): assert script.site_packages / "base" in result.files_updated, ( "base 0.1.0 not reinstalled" ) + + +def test_new_resolver_only_builds_sdists_when_needed(script): + create_basic_wheel_for_package( + script, + "base", + "0.1.0", + depends=["dep"], + ) + create_basic_sdist_for_package( + script, + "dep", + "0.1.0", + # Replace setup.py with something that fails + extra_files={"setup.py": "assert False"}, + ) + create_basic_sdist_for_package( + script, + "dep", + "0.2.0", + ) + # We only ever need to check dep 0.2.0 as it's the latest version + script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "base" + ) + assert_installed(script, base="0.1.0", dep="0.2.0") + + # We merge criteria here, as we have two "dep" requirements + script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "base", "dep" + ) + assert_installed(script, base="0.1.0", dep="0.2.0") diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index 682f66c711e..44f39858e14 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -15,7 +15,7 @@ from zipfile import ZipFile import pytest -from pip._vendor.six import PY2 +from pip._vendor.six import PY2, ensure_binary, text_type from scripttest import FoundDir, TestFileEnvironment from pip._internal.index.collector import LinkCollector @@ -1036,6 +1036,56 @@ def hello(): return archive_path +def create_basic_sdist_for_package( + script, name, version, extra_files=None +): + files = { + "setup.py": """ + from setuptools import find_packages, setup + setup(name={name!r}, version={version!r}) + """, + } + + # Some useful shorthands + archive_name = "{name}-{version}.tar.gz".format( + name=name, version=version + ) + + # Replace key-values with formatted values + for key, value in list(files.items()): + del files[key] + key = key.format(name=name) + files[key] = textwrap.dedent(value).format( + name=name, version=version + ).strip() + + # Add new files after formatting + if extra_files: + files.update(extra_files) + + for fname in files: + path = script.temp_path / fname + path.parent.mkdir(exist_ok=True, parents=True) + path.write_bytes(ensure_binary(files[fname])) + + # The base_dir cast is required to make `shutil.make_archive()` use + # Unicode paths on Python 2, making it able to properly archive + # files with non-ASCII names. + retval = script.scratch_path / archive_name + generated = shutil.make_archive( + retval, + 'gztar', + root_dir=script.temp_path, + base_dir=text_type(os.curdir), + ) + shutil.move(generated, retval) + + shutil.rmtree(script.temp_path) + script.temp_path.mkdir() + + return retval + + def need_executable(name, check_cmd): def wrapper(fn): try: From d53d3d6b24dd67dee7c89298ff192bbd9867bbbd Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Fri, 3 Apr 2020 11:23:35 +0100 Subject: [PATCH 1539/3170] Use the name/version from the InstallationCandidate --- .../resolution/resolvelib/candidates.py | 20 +++++++++++++++---- .../resolution/resolvelib/factory.py | 16 +++++++++++---- 2 files changed, 28 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index c0439f896d0..596b96062aa 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -68,13 +68,20 @@ def make_install_req_from_dist(dist, parent): class LinkCandidate(Candidate): - def __init__(self, link, parent, factory): - # type: (Link, InstallRequirement, Factory) -> None + def __init__( + self, + link, # type: Link + parent, # type: InstallRequirement + factory, # type: Factory + name=None, # type: Optional[str] + version=None, # type: Optional[_BaseVersion] + ): + # type: (...) -> None self.link = link self._factory = factory self._ireq = make_install_req_from_link(link, parent) - self._name = None # type: Optional[str] - self._version = None # type: Optional[_BaseVersion] + self._name = name + self._version = version self._dist = None # type: Optional[Distribution] def __eq__(self, other): @@ -113,6 +120,11 @@ def dist(self): self._dist = abstract_dist.get_pkg_resources_distribution() # TODO: Only InstalledDistribution can return None here :-( assert self._dist is not None + # TODO: Abort cleanly here, as the resolution has been + # based on the wrong name/version until now, and + # so is wrong. + # TODO: (Longer term) Rather than abort, reject this candidate + # and backtrack. This would need resolvelib support. # These should be "proper" errors, not just asserts, as they # can result from user errors like a requirement "foo @ URL" # when the project at URL has a name of "bar" in its metadata. diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 335d5beea6d..f7ecd716f6d 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -22,6 +22,7 @@ from typing import Dict, Optional, Set, Tuple from pip._vendor.packaging.specifiers import SpecifierSet + from pip._vendor.packaging.version import _BaseVersion from pip._vendor.pkg_resources import Distribution from pip._internal.index.package_finder import PackageFinder @@ -67,14 +68,16 @@ def _make_candidate_from_dist( def _make_candidate_from_link( self, - link, # type: Link - extras, # type: Set[str] - parent, # type: InstallRequirement + link, # type: Link + extras, # type: Set[str] + parent, # type: InstallRequirement + name=None, # type: Optional[str] + version=None, # type: Optional[_BaseVersion] ): # type: (...) -> Candidate if link not in self._link_candidate_cache: self._link_candidate_cache[link] = LinkCandidate( - link, parent, factory=self, + link, parent, factory=self, name=name, version=version, ) base = self._link_candidate_cache[link] if extras: @@ -105,6 +108,8 @@ def make_candidate_from_ican( link=ican.link, extras=extras, parent=parent, + name=ican.name, + version=ican.version, ) return self._make_candidate_from_dist( dist=dist, @@ -115,6 +120,9 @@ def make_candidate_from_ican( def make_requirement_from_install_req(self, ireq): # type: (InstallRequirement) -> Requirement if ireq.link: + # TODO: Get name and version from ireq, if possible? + # Specifically, this might be needed in "name @ URL" + # syntax - need to check where that syntax is handled. cand = self._make_candidate_from_link( ireq.link, extras=set(), parent=ireq, ) From 3b06cb6b53440979774158b82d8e1f9af05b4566 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 3 Apr 2020 20:05:05 +0800 Subject: [PATCH 1540/3170] Refactor ireq-related operations to base class --- .../resolution/resolvelib/candidates.py | 38 ++++++++++++++++--- .../resolution/resolvelib/factory.py | 7 +++- 2 files changed, 37 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 596b96062aa..8114adca0e6 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -19,6 +19,7 @@ from pip._vendor.packaging.version import _BaseVersion from pip._vendor.pkg_resources import Distribution + from pip._internal.distributions import AbstractDistribution from pip._internal.models.link import Link from .base import Requirement @@ -67,11 +68,11 @@ def make_install_req_from_dist(dist, parent): return ireq -class LinkCandidate(Candidate): +class _InstallRequirementBackedCandidate(Candidate): def __init__( self, link, # type: Link - parent, # type: InstallRequirement + ireq, # type: InstallRequirement factory, # type: Factory name=None, # type: Optional[str] version=None, # type: Optional[_BaseVersion] @@ -79,7 +80,7 @@ def __init__( # type: (...) -> None self.link = link self._factory = factory - self._ireq = make_install_req_from_link(link, parent) + self._ireq = ireq self._name = name self._version = version self._dist = None # type: Optional[Distribution] @@ -110,13 +111,15 @@ def version(self): self._version = self.dist.parsed_version return self._version + def _get_abstract_distribution(self): + # type: () -> AbstractDistribution + raise NotImplementedError("Override in subclass") + @property def dist(self): # type: () -> Distribution if self._dist is None: - abstract_dist = self._factory.preparer.prepare_linked_requirement( - self._ireq - ) + abstract_dist = self._get_abstract_distribution() self._dist = abstract_dist.get_pkg_resources_distribution() # TODO: Only InstalledDistribution can return None here :-( assert self._dist is not None @@ -166,6 +169,29 @@ def get_install_requirement(self): return self._ireq +class LinkCandidate(_InstallRequirementBackedCandidate): + def __init__( + self, + link, # type: Link + parent, # type: InstallRequirement + factory, # type: Factory + name=None, # type: Optional[str] + version=None, # type: Optional[_BaseVersion] + ): + # type: (...) -> None + super(LinkCandidate, self).__init__( + link=link, + ireq=make_install_req_from_link(link, parent), + factory=factory, + name=name, + version=version, + ) + + def _get_abstract_distribution(self): + # type: () -> AbstractDistribution + return self._factory.preparer.prepare_linked_requirement(self._ireq) + + class AlreadyInstalledCandidate(Candidate): def __init__( self, diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index f7ecd716f6d..d94934b4dac 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -19,7 +19,7 @@ ) if MYPY_CHECK_RUNNING: - from typing import Dict, Optional, Set, Tuple + from typing import Dict, Optional, Set, Tuple, TypeVar from pip._vendor.packaging.specifiers import SpecifierSet from pip._vendor.packaging.version import _BaseVersion @@ -34,6 +34,9 @@ from .base import Candidate, Requirement + C = TypeVar("C") + Cache = Dict[Link, C] + class Factory(object): def __init__( @@ -52,7 +55,7 @@ def __init__( self._make_install_req_from_spec = make_install_req self._ignore_installed = ignore_installed self._ignore_requires_python = ignore_requires_python - self._link_candidate_cache = {} # type: Dict[Link, LinkCandidate] + self._link_candidate_cache = {} # type: Cache[LinkCandidate] def _make_candidate_from_dist( self, From 2430aba87983e8582747a8e9057311277a6dbfc4 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sat, 4 Apr 2020 17:51:43 +0800 Subject: [PATCH 1541/3170] Implement editable candidate --- .../resolution/resolvelib/candidates.py | 47 ++++++++++++++++++- .../resolution/resolvelib/factory.py | 19 ++++++-- tests/functional/test_new_resolver.py | 35 ++++++++++++++ 3 files changed, 94 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 8114adca0e6..6abadf04d56 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -5,7 +5,10 @@ from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.packaging.version import Version -from pip._internal.req.constructors import install_req_from_line +from pip._internal.req.constructors import ( + install_req_from_editable, + install_req_from_line, +) from pip._internal.req.req_install import InstallRequirement from pip._internal.utils.misc import normalize_version_info from pip._internal.utils.packaging import get_requires_python @@ -31,7 +34,7 @@ def make_install_req_from_link(link, parent): # type: (Link, InstallRequirement) -> InstallRequirement - # TODO: Do we need to support editables? + assert not parent.editable, "parent is editable" return install_req_from_line( link.url, comes_from=parent.comes_from, @@ -46,6 +49,23 @@ def make_install_req_from_link(link, parent): ) +def make_install_req_from_editable(link, parent): + # type: (Link, InstallRequirement) -> InstallRequirement + assert parent.editable, "parent not editable" + return install_req_from_editable( + link.url, + comes_from=parent.comes_from, + use_pep517=parent.use_pep517, + isolated=parent.isolated, + constraint=parent.constraint, + options=dict( + install_options=parent.install_options, + global_options=parent.global_options, + hashes=parent.hash_options + ), + ) + + def make_install_req_from_dist(dist, parent): # type: (Distribution, InstallRequirement) -> InstallRequirement # TODO: Do we need to support editables? @@ -192,6 +212,29 @@ def _get_abstract_distribution(self): return self._factory.preparer.prepare_linked_requirement(self._ireq) +class EditableCandidate(_InstallRequirementBackedCandidate): + def __init__( + self, + link, # type: Link + parent, # type: InstallRequirement + factory, # type: Factory + name=None, # type: Optional[str] + version=None, # type: Optional[_BaseVersion] + ): + # type: (...) -> None + super(EditableCandidate, self).__init__( + link=link, + ireq=make_install_req_from_editable(link, parent), + factory=factory, + name=name, + version=version, + ) + + def _get_abstract_distribution(self): + # type: () -> AbstractDistribution + return self._factory.preparer.prepare_editable_requirement(self._ireq) + + class AlreadyInstalledCandidate(Candidate): def __init__( self, diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index d94934b4dac..705f47f13c1 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -8,6 +8,7 @@ from .candidates import ( AlreadyInstalledCandidate, + EditableCandidate, ExtrasCandidate, LinkCandidate, RequiresPythonCandidate, @@ -56,6 +57,7 @@ def __init__( self._ignore_installed = ignore_installed self._ignore_requires_python = ignore_requires_python self._link_candidate_cache = {} # type: Cache[LinkCandidate] + self._editable_candidate_cache = {} # type: Cache[EditableCandidate] def _make_candidate_from_dist( self, @@ -78,11 +80,18 @@ def _make_candidate_from_link( version=None, # type: Optional[_BaseVersion] ): # type: (...) -> Candidate - if link not in self._link_candidate_cache: - self._link_candidate_cache[link] = LinkCandidate( - link, parent, factory=self, name=name, version=version, - ) - base = self._link_candidate_cache[link] + if parent.editable: + if link not in self._editable_candidate_cache: + self._editable_candidate_cache[link] = EditableCandidate( + link, parent, factory=self, name=name, version=version, + ) + base = self._editable_candidate_cache[link] + else: + if link not in self._link_candidate_cache: + self._link_candidate_cache[link] = LinkCandidate( + link, parent, factory=self, name=name, version=version, + ) + base = self._link_candidate_cache[link] if extras: return ExtrasCandidate(base, extras) return base diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 90309f471b9..f9a5a3b287c 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -1,10 +1,13 @@ import json +import os + import pytest from tests.lib import ( create_basic_sdist_for_package, create_basic_wheel_for_package, + create_test_package_with_setup, ) @@ -27,6 +30,15 @@ def assert_not_installed(script, *args): "{!r} contained in {!r}".format(args, installed) +def assert_editable(script, *args): + # This simply checks whether all of the listed packages have a + # corresponding .egg-link file installed. + # TODO: Implement a more rigorous way to test for editable installations. + egg_links = set("{}.egg-link".format(arg) for arg in args) + assert egg_links <= set(os.listdir(script.site_packages_path)), \ + "{!r} not all found in {!r}".format(args, script.site_packages_path) + + def test_new_resolver_can_install(script): create_basic_wheel_for_package( script, @@ -144,6 +156,29 @@ def test_new_resolver_installs_extras(script): assert_installed(script, base="0.1.0", dep="0.1.0") +def test_new_resolver_installs_editable(script): + create_basic_wheel_for_package( + script, + "base", + "0.1.0", + depends=["dep"], + ) + source_dir = create_test_package_with_setup( + script, + name="dep", + version="0.1.0", + ) + script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "base", + "--editable", source_dir, + ) + assert_installed(script, base="0.1.0", dep="0.1.0") + assert_editable(script, "dep") + + @pytest.mark.parametrize( "requires_python, ignore_requires_python, dep_version", [ From 05f7dbd0c8361ca4de3320b1bc3c8936636ab54a Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sat, 4 Apr 2020 18:12:38 +0800 Subject: [PATCH 1542/3170] Type hint hacks --- .../_internal/resolution/resolvelib/candidates.py | 13 +++++++++++-- src/pip/_internal/resolution/resolvelib/factory.py | 3 ++- 2 files changed, 13 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 6abadf04d56..3f431843f12 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -28,6 +28,12 @@ from .base import Requirement from .factory import Factory + BaseCandidate = Union[ + "AlreadyInstalledCandidate", + "EditableCandidate", + "LinkCandidate", + ] + logger = logging.getLogger(__name__) @@ -54,7 +60,10 @@ def make_install_req_from_editable(link, parent): assert parent.editable, "parent not editable" return install_req_from_editable( link.url, - comes_from=parent.comes_from, + # HACK: install_req_from_editable accepts Optional[str] here, but + # parent.comes_from is Union[str, InstallRequirement, None]. How do + # we fix the type hint conflicts? + comes_from=parent.comes_from, # type: ignore use_pep517=parent.use_pep517, isolated=parent.isolated, constraint=parent.constraint, @@ -302,7 +311,7 @@ class ExtrasCandidate(Candidate): """ def __init__( self, - base, # type: Union[AlreadyInstalledCandidate, LinkCandidate] + base, # type: BaseCandidate extras, # type: Set[str] ): # type: (...) -> None diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 705f47f13c1..cf5b2b19edf 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -34,6 +34,7 @@ from pip._internal.resolution.base import InstallRequirementProvider from .base import Candidate, Requirement + from .candidates import BaseCandidate C = TypeVar("C") Cache = Dict[Link, C] @@ -85,7 +86,7 @@ def _make_candidate_from_link( self._editable_candidate_cache[link] = EditableCandidate( link, parent, factory=self, name=name, version=version, ) - base = self._editable_candidate_cache[link] + base = self._editable_candidate_cache[link] # type: BaseCandidate else: if link not in self._link_candidate_cache: self._link_candidate_cache[link] = LinkCandidate( From aaa82cddccd2d8ae778710fbbdbcabba694dd888 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sat, 4 Apr 2020 20:52:39 +0800 Subject: [PATCH 1543/3170] isort --- tests/functional/test_new_resolver.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index f9a5a3b287c..2697a4272c7 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -1,5 +1,4 @@ import json - import os import pytest From aead201fdaa8c74bad0a021a2aa2c260906394cc Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sat, 4 Apr 2020 21:53:04 +0800 Subject: [PATCH 1544/3170] No need to think about editable for installed dist --- src/pip/_internal/resolution/resolvelib/candidates.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 3f431843f12..b78201a34e5 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -77,7 +77,6 @@ def make_install_req_from_editable(link, parent): def make_install_req_from_dist(dist, parent): # type: (Distribution, InstallRequirement) -> InstallRequirement - # TODO: Do we need to support editables? ireq = install_req_from_line( "{}=={}".format( canonicalize_name(dist.project_name), From 45d5b377ab1fe4160878c06660d5e4de991a5b7b Mon Sep 17 00:00:00 2001 From: Ricky Ng-Adam <rngadam@gmail.com> Date: Sat, 4 Apr 2020 08:36:26 -0400 Subject: [PATCH 1545/3170] elaborate on type of links supported --- src/pip/_internal/cli/cmdoptions.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index 4cbd5abf2c4..ff9acfd4644 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -367,9 +367,11 @@ def find_links(): action='append', default=[], 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.", + help="If a URL or path to an html file, then parse for links to " + "archives such as sdist (.tar.gz) or wheel (.whl) files. " + "If a local path or file:// URL that's a directory, " + "then look for archives in the directory listing. " + "Links to VCS project URLs are not supported.", ) From a0fe4112d15ebcb20052c33f8792f237e1ad83e7 Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Sat, 4 Apr 2020 14:00:16 -0500 Subject: [PATCH 1546/3170] better naming in yaml-files: transaction -> response, install -> state --- tests/functional/test_yaml.py | 11 +++++------ tests/yaml/install/circular.yml | 16 ++++++++-------- tests/yaml/install/conflicting_diamond.yml | 2 +- tests/yaml/install/conflicting_triangle.yml | 4 ++-- tests/yaml/install/extras.yml | 12 ++++++------ tests/yaml/install/non_pinned.yml | 8 ++++---- tests/yaml/install/pinned.yml | 12 ++++++------ tests/yaml/install/simple.yml | 12 ++++++------ 8 files changed, 38 insertions(+), 39 deletions(-) diff --git a/tests/functional/test_yaml.py b/tests/functional/test_yaml.py index 28fccbe7c19..18318f296ae 100644 --- a/tests/functional/test_yaml.py +++ b/tests/functional/test_yaml.py @@ -153,10 +153,10 @@ def handle_install_request(script, requirement, options): def test_yaml_based(script, case): available = case.get("available", []) requests = case.get("request", []) - transaction = case.get("transaction", []) + responses = case.get("response", []) - assert len(requests) == len(transaction), ( - "Expected requests and transaction counts to be same" + assert len(requests) == len(responses), ( + "Expected requests and responses counts to be same" ) # Create a custom index of all the packages that are supposed to be @@ -171,7 +171,7 @@ def test_yaml_based(script, case): create_basic_wheel_for_package(script, **package) # use scratch path for index - for request, expected in zip(requests, transaction): + for request, response in zip(requests, responses): # Perform the requested action if 'install' in request: @@ -183,6 +183,5 @@ def test_yaml_based(script, case): assert False, "Unsupported request {!r}".format(request) result = effect["_result_object"] - del effect["_result_object"] - assert effect == expected, str(result) + assert effect['install'] == response['state'], str(result) diff --git a/tests/yaml/install/circular.yml b/tests/yaml/install/circular.yml index 802eaf6bc4e..95c535454fa 100644 --- a/tests/yaml/install/circular.yml +++ b/tests/yaml/install/circular.yml @@ -10,8 +10,8 @@ cases: - request: - install: A - transaction: - - install: + response: + - state: - A 1.0.0 - B 1.0.0 - C 1.0.0 @@ -19,8 +19,8 @@ cases: - request: - install: B - transaction: - - install: + response: + - state: - A 1.0.0 - B 1.0.0 - C 1.0.0 @@ -28,8 +28,8 @@ cases: - request: - install: C - transaction: - - install: + response: + - state: - A 1.0.0 - B 1.0.0 - C 1.0.0 @@ -37,8 +37,8 @@ cases: - request: - install: D - transaction: - - install: + response: + - state: - A 1.0.0 - B 1.0.0 - C 1.0.0 diff --git a/tests/yaml/install/conflicting_diamond.yml b/tests/yaml/install/conflicting_diamond.yml index aa7edd6a910..8f5ab12755f 100644 --- a/tests/yaml/install/conflicting_diamond.yml +++ b/tests/yaml/install/conflicting_diamond.yml @@ -8,7 +8,7 @@ cases: - D 2.0.0 request: - install: A - transaction: + response: - conflicting: - required_by: [A 1.0.0, B 1.0.0] selector: D == 1.0.0 diff --git a/tests/yaml/install/conflicting_triangle.yml b/tests/yaml/install/conflicting_triangle.yml index 7baa9399a40..6296b0acc29 100644 --- a/tests/yaml/install/conflicting_triangle.yml +++ b/tests/yaml/install/conflicting_triangle.yml @@ -8,8 +8,8 @@ cases: request: - install: A - install: B - transaction: - - install: + response: + - state: - A 1.0.0 - C 1.0.0 - conflicting: diff --git a/tests/yaml/install/extras.yml b/tests/yaml/install/extras.yml index ee23b5e4af0..7b39003a2eb 100644 --- a/tests/yaml/install/extras.yml +++ b/tests/yaml/install/extras.yml @@ -15,24 +15,24 @@ cases: - request: - install: B - transaction: - - install: + response: + - state: - B 1.0.0 - D 1.0.0 - E 1.0.0 - request: - install: C - transaction: - - install: + response: + - state: - C 1.0.0 - D 1.0.0 - F 1.0.0 - request: - install: A - transaction: - - install: + response: + - state: - A 1.0.0 - B 1.0.0 - C 1.0.0 diff --git a/tests/yaml/install/non_pinned.yml b/tests/yaml/install/non_pinned.yml index d2c4a68c784..6e9b26c4c10 100644 --- a/tests/yaml/install/non_pinned.yml +++ b/tests/yaml/install/non_pinned.yml @@ -11,14 +11,14 @@ cases: - request: - install: A >= 2.0.0 - transaction: - - install: + response: + - state: - A 2.0.0 - B 2.1.0 - request: - install: A < 2.0.0 - transaction: - - install: + response: + - state: - A 1.0.0 - B 1.0.0 diff --git a/tests/yaml/install/pinned.yml b/tests/yaml/install/pinned.yml index 0b5fa0c3b3e..c8bd3f35dbf 100644 --- a/tests/yaml/install/pinned.yml +++ b/tests/yaml/install/pinned.yml @@ -9,21 +9,21 @@ cases: - request: - install: B - transaction: - - install: + response: + - state: - A 2.0.0 - B 2.0.0 - request: - install: B == 2.0.0 - transaction: - - install: + response: + - state: - A 2.0.0 - B 2.0.0 - request: - install: B == 1.0.0 - transaction: - - install: + response: + - state: - A 1.0.0 - B 1.0.0 diff --git a/tests/yaml/install/simple.yml b/tests/yaml/install/simple.yml index 676b918bc50..a122d43280e 100644 --- a/tests/yaml/install/simple.yml +++ b/tests/yaml/install/simple.yml @@ -9,20 +9,20 @@ cases: - request: - install: simple - transaction: - - install: + response: + - state: - simple 0.2.0 - request: - install: base - transaction: - - install: + response: + - state: - base 0.1.0 - dep 0.1.0 - request: - install: base options: --no-deps - transaction: - - install: + response: + - state: - base 0.1.0 From 3bc3cee5aeb7d5bbf9db13091884ca92cc259abd Mon Sep 17 00:00:00 2001 From: Nikolay Korolev <korolevns98@gmail.com> Date: Sat, 4 Apr 2020 22:18:12 +0300 Subject: [PATCH 1547/3170] Add logging thread-safety tests --- tests/unit/test_logging.py | 43 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/unit/test_logging.py b/tests/unit/test_logging.py index a2bab3ea9c5..7a5309bb99b 100644 --- a/tests/unit/test_logging.py +++ b/tests/unit/test_logging.py @@ -2,6 +2,7 @@ import logging import os import time +from threading import Thread import pytest from mock import patch @@ -11,6 +12,7 @@ BrokenStdoutLoggingError, ColorizedStreamHandler, IndentingFormatter, + indent_log ) from pip._internal.utils.misc import captured_stderr, captured_stdout @@ -108,6 +110,47 @@ def test_format_deprecated(self, level_name, expected): f = IndentingFormatter(fmt="%(message)s") assert f.format(record) == expected + def test_thread_safety_base(self): + actual = "Initial content" + + record = self.make_record( + 'DEPRECATION: hello\nworld', level_name='WARNING', + ) + f = IndentingFormatter(fmt="%(message)s") + + def thread_function(): + nonlocal actual, f + actual = f.format(record) + + thread_function() + expected = actual + actual = "Another initial content" + thread = Thread(target=thread_function) + thread.start() + thread.join() + assert actual == expected + + def test_thread_safety_indent_log(self): + actual = "Initial content" + + record = self.make_record( + 'DEPRECATION: hello\nworld', level_name='WARNING', + ) + f = IndentingFormatter(fmt="%(message)s") + + def thread_function(): + nonlocal actual, f + with indent_log(): + actual = f.format(record) + + thread_function() + expected = actual + actual = "Another initial content" + thread = Thread(target=thread_function) + thread.start() + thread.join() + assert actual == expected + class TestColorizedStreamHandler(object): From f8bb362fef1b5e77d4f1f40cca003a219820120d Mon Sep 17 00:00:00 2001 From: Nikolay Korolev <korolevns98@gmail.com> Date: Sat, 4 Apr 2020 22:23:00 +0300 Subject: [PATCH 1548/3170] Fix Python 2 compatibility in tests --- tests/unit/test_logging.py | 20 ++++++-------------- 1 file changed, 6 insertions(+), 14 deletions(-) diff --git a/tests/unit/test_logging.py b/tests/unit/test_logging.py index 7a5309bb99b..7592fcf31ef 100644 --- a/tests/unit/test_logging.py +++ b/tests/unit/test_logging.py @@ -111,45 +111,37 @@ def test_format_deprecated(self, level_name, expected): assert f.format(record) == expected def test_thread_safety_base(self): - actual = "Initial content" - record = self.make_record( 'DEPRECATION: hello\nworld', level_name='WARNING', ) f = IndentingFormatter(fmt="%(message)s") + results = [] def thread_function(): - nonlocal actual, f - actual = f.format(record) + results.append(f.format(record)) thread_function() - expected = actual - actual = "Another initial content" thread = Thread(target=thread_function) thread.start() thread.join() - assert actual == expected + assert results[0] == results[1] def test_thread_safety_indent_log(self): - actual = "Initial content" - record = self.make_record( 'DEPRECATION: hello\nworld', level_name='WARNING', ) f = IndentingFormatter(fmt="%(message)s") + results = [] def thread_function(): - nonlocal actual, f with indent_log(): - actual = f.format(record) + results.append(f.format(record)) thread_function() - expected = actual - actual = "Another initial content" thread = Thread(target=thread_function) thread.start() thread.join() - assert actual == expected + assert results[0] == results[1] class TestColorizedStreamHandler(object): From e454181ba65f706f90c79f774d5f7680a0c9d559 Mon Sep 17 00:00:00 2001 From: Nikolay Korolev <korolevns98@gmail.com> Date: Sat, 4 Apr 2020 22:30:57 +0300 Subject: [PATCH 1549/3170] Fix coding style --- tests/unit/test_logging.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_logging.py b/tests/unit/test_logging.py index 7592fcf31ef..a62c18c770f 100644 --- a/tests/unit/test_logging.py +++ b/tests/unit/test_logging.py @@ -12,7 +12,7 @@ BrokenStdoutLoggingError, ColorizedStreamHandler, IndentingFormatter, - indent_log + indent_log, ) from pip._internal.utils.misc import captured_stderr, captured_stdout From 0c3bc448e17714fad752efc3b035b4a3c1bb0aad Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Sat, 4 Apr 2020 14:33:28 -0500 Subject: [PATCH 1550/3170] always check for files in site-packages - add simple test for installing twice --- tests/functional/test_yaml.py | 2 +- tests/yaml/install/simple.yml | 4 ++++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/functional/test_yaml.py b/tests/functional/test_yaml.py index 18318f296ae..0b03ccc2909 100644 --- a/tests/functional/test_yaml.py +++ b/tests/functional/test_yaml.py @@ -113,7 +113,7 @@ def handle_install_request(script, requirement, options): # Check which packages got installed retval["install"] = [] - for path in result.files_created: + for path in os.listdir(script.site_packages_path): if path.endswith(".dist-info"): name, version = ( os.path.basename(path)[:-len(".dist-info")] diff --git a/tests/yaml/install/simple.yml b/tests/yaml/install/simple.yml index a122d43280e..8430c02add5 100644 --- a/tests/yaml/install/simple.yml +++ b/tests/yaml/install/simple.yml @@ -9,9 +9,13 @@ cases: - request: - install: simple + - install: dep response: - state: - simple 0.2.0 + - state: + - dep 0.1.0 + - simple 0.2.0 - request: - install: base From c2fdf4a35bc7db0d81743e83db57122393ec4eb7 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sat, 4 Apr 2020 15:01:17 +0530 Subject: [PATCH 1551/3170] Add mypy annotations to pip._internal.commands.check --- news/1C96C81F-4A3E-42AD-9562-7BB7EB0A7EF9.trivial | 0 src/pip/_internal/commands/__init__.py | 4 ++++ src/pip/_internal/commands/check.py | 14 ++++++++++---- 3 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 news/1C96C81F-4A3E-42AD-9562-7BB7EB0A7EF9.trivial diff --git a/news/1C96C81F-4A3E-42AD-9562-7BB7EB0A7EF9.trivial b/news/1C96C81F-4A3E-42AD-9562-7BB7EB0A7EF9.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/commands/__init__.py b/src/pip/_internal/commands/__init__.py index 2a311f8fc89..0f0b6e02261 100644 --- a/src/pip/_internal/commands/__init__.py +++ b/src/pip/_internal/commands/__init__.py @@ -4,6 +4,10 @@ # The following comment should be removed at some point in the future. # mypy: disallow-untyped-defs=False +# There is currently a bug in python/typeshed mentioned at +# https://github.com/python/typeshed/issues/3906 which causes the +# return type of difflib.get_close_matches to be reported +# as List[Sequence[str]] whereas it should have been List[str] from __future__ import absolute_import diff --git a/src/pip/_internal/commands/check.py b/src/pip/_internal/commands/check.py index 968944611ea..b557ca64113 100644 --- a/src/pip/_internal/commands/check.py +++ b/src/pip/_internal/commands/check.py @@ -1,17 +1,20 @@ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - import logging from pip._internal.cli.base_command import Command +from pip._internal.cli.status_codes import ERROR, SUCCESS from pip._internal.operations.check import ( check_package_set, create_package_set_from_installed, ) from pip._internal.utils.misc import write_output +from pip._internal.utils.typing import MYPY_CHECK_RUNNING logger = logging.getLogger(__name__) +if MYPY_CHECK_RUNNING: + from typing import List, Any + from optparse import Values + class CheckCommand(Command): """Verify installed packages have compatible dependencies.""" @@ -20,6 +23,8 @@ class CheckCommand(Command): %prog [options]""" def run(self, options, args): + # type: (Values, List[Any]) -> int + package_set, parsing_probs = create_package_set_from_installed() missing, conflicting = check_package_set(package_set) @@ -40,6 +45,7 @@ def run(self, options, args): ) if missing or conflicting or parsing_probs: - return 1 + return ERROR else: write_output("No broken requirements found.") + return SUCCESS From 74c50420526de33fb8fcfd813060eb1e67a596ed Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sun, 5 Apr 2020 12:17:25 +0800 Subject: [PATCH 1552/3170] Accept InsatllRequirement as comes_from --- src/pip/_internal/req/constructors.py | 6 ++++-- src/pip/_internal/resolution/resolvelib/candidates.py | 5 +---- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index edc6c36f999..7e57f22f73b 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -218,7 +218,7 @@ def parse_req_from_editable(editable_req): def install_req_from_editable( editable_req, # type: str - comes_from=None, # type: Optional[str] + comes_from=None, # type: Optional[Union[InstallRequirement, str]] use_pep517=None, # type: Optional[bool] isolated=False, # type: bool options=None, # type: Optional[Dict[str, Any]] @@ -231,7 +231,9 @@ def install_req_from_editable( source_dir = parts.link.file_path if parts.link.scheme == 'file' else None return InstallRequirement( - parts.requirement, comes_from, source_dir=source_dir, + parts.requirement, + comes_from=comes_from, + source_dir=source_dir, editable=True, link=parts.link, constraint=constraint, diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index b78201a34e5..db750823f81 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -60,10 +60,7 @@ def make_install_req_from_editable(link, parent): assert parent.editable, "parent not editable" return install_req_from_editable( link.url, - # HACK: install_req_from_editable accepts Optional[str] here, but - # parent.comes_from is Union[str, InstallRequirement, None]. How do - # we fix the type hint conflicts? - comes_from=parent.comes_from, # type: ignore + comes_from=parent.comes_from, use_pep517=parent.use_pep517, isolated=parent.isolated, constraint=parent.constraint, From 2b883d5d8a4d964415f8a42e6889a7505832df43 Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Sun, 5 Apr 2020 00:05:43 -0500 Subject: [PATCH 1553/3170] add ability to test uninstall - and simple uninstall test --- tests/functional/test_yaml.py | 39 +++++++++++++++++++---------------- tests/yaml/install/simple.yml | 8 +++++++ 2 files changed, 29 insertions(+), 18 deletions(-) diff --git a/tests/functional/test_yaml.py b/tests/functional/test_yaml.py index 0b03ccc2909..199aa919ae9 100644 --- a/tests/functional/test_yaml.py +++ b/tests/functional/test_yaml.py @@ -92,12 +92,17 @@ def stripping_split(my_str, splitwith, count=None): return retval -def handle_install_request(script, requirement, options): +def handle_request(script, action, requirement, options): assert isinstance(requirement, str), ( "Need install requirement to be a string only" ) - args = ["install", "--no-index", "--find-links", - path_to_url(script.scratch_path)] + if action == 'install': + args = ['install', "--no-index", "--find-links", + path_to_url(script.scratch_path)] + elif action == 'uninstall': + args = ['uninstall', '--yes'] + else: + raise "Did not excpet action: {!r}".format(action) args.append(requirement) args.extend(options) args.append("--verbose") @@ -111,7 +116,7 @@ def handle_install_request(script, requirement, options): } if result.returncode == 0: # Check which packages got installed - retval["install"] = [] + retval["state"] = [] for path in os.listdir(script.site_packages_path): if path.endswith(".dist-info"): @@ -121,12 +126,9 @@ def handle_install_request(script, requirement, options): # TODO: information about extras. - retval["install"].append(" ".join((name, version))) + retval["state"].append(" ".join((name, version))) - retval["install"].sort() - - # TODO: Support checking uninstallations - # retval["uninstall"] = [] + retval["state"].sort() elif "conflicting" in result.stderr.lower(): retval["conflicting"] = [] @@ -173,15 +175,16 @@ def test_yaml_based(script, case): # use scratch path for index for request, response in zip(requests, responses): - # Perform the requested action - if 'install' in request: - effect = handle_install_request( - script, - request['install'], - request.get('options', '').split()) + for action in 'install', 'uninstall': + if action in request: + break else: - assert False, "Unsupported request {!r}".format(request) + raise "Unsupported request {!r}".format(request) - result = effect["_result_object"] + # Perform the requested action + effect = handle_request(script, action, + request[action], + request.get('options', '').split()) - assert effect['install'] == response['state'], str(result) + assert effect['state'] == (response['state'] or []), \ + str(effect["_result_object"]) diff --git a/tests/yaml/install/simple.yml b/tests/yaml/install/simple.yml index 8430c02add5..257ccb7dfc6 100644 --- a/tests/yaml/install/simple.yml +++ b/tests/yaml/install/simple.yml @@ -6,6 +6,14 @@ base: - dep 0.1.0 cases: +- + request: + - install: simple + - uninstall: simple + response: + - state: + - simple 0.2.0 + - state: null - request: - install: simple From bf8e6bc7853f1f014e20def4f7e994ba926d8e4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sat, 4 Apr 2020 11:32:58 +0200 Subject: [PATCH 1554/3170] Generate legacy metadata in temporary directory Before it was generated in a pip-egg-info subdirectory of the source dir. This will avoid polluting source dir when we build in place. --- .../operations/build/metadata_legacy.py | 27 +++++++++---------- src/pip/_internal/req/req_install.py | 2 -- 2 files changed, 13 insertions(+), 16 deletions(-) diff --git a/src/pip/_internal/operations/build/metadata_legacy.py b/src/pip/_internal/operations/build/metadata_legacy.py index b6813f89ba7..5e23ccf8eef 100644 --- a/src/pip/_internal/operations/build/metadata_legacy.py +++ b/src/pip/_internal/operations/build/metadata_legacy.py @@ -5,9 +5,9 @@ import os from pip._internal.exceptions import InstallationError -from pip._internal.utils.misc import ensure_dir from pip._internal.utils.setuptools_build import make_setuptools_egg_info_args from pip._internal.utils.subprocess import call_subprocess +from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.vcs import vcs @@ -19,9 +19,9 @@ logger = logging.getLogger(__name__) -def _find_egg_info(source_directory, is_editable): - # type: (str, bool) -> str - """Find an .egg-info in `source_directory`, based on `is_editable`. +def _find_egg_info(source_directory): + # type: (str) -> str + """Find an .egg-info in `source_directory`. """ def looks_like_virtual_env(path): @@ -31,7 +31,7 @@ def looks_like_virtual_env(path): os.path.exists(os.path.join(path, 'Scripts', 'Python.exe')) ) - def locate_editable_egg_info(base): + def locate_egg_info(base): # type: (str) -> List[str] candidates = [] # type: List[str] for root, dirs, files in os.walk(base): @@ -58,11 +58,7 @@ def depth_of_directory(dir_): ) base = source_directory - if is_editable: - filenames = locate_editable_egg_info(base) - else: - base = os.path.join(base, 'pip-egg-info') - filenames = os.listdir(base) + filenames = locate_egg_info(base) if not filenames: raise InstallationError( @@ -101,9 +97,9 @@ def generate_metadata( # to avoid confusion due to the source code being considered an installed # egg. if not editable: - egg_info_dir = os.path.join(source_dir, 'pip-egg-info') - # setuptools complains if the target directory does not exist. - ensure_dir(egg_info_dir) + egg_info_dir = TempDirectory( + kind="pip-egg-info", globally_managed=True + ).path args = make_setuptools_egg_info_args( setup_py_path, @@ -119,4 +115,7 @@ def generate_metadata( ) # Return the .egg-info directory. - return _find_egg_info(source_dir, editable) + if not editable: + assert egg_info_dir + return _find_egg_info(egg_info_dir) + return _find_egg_info(source_dir) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 44c29d1b581..985c31fd60e 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -729,8 +729,6 @@ def archive(self, build_dir): os.path.abspath(self.unpacked_source_directory) ) for dirpath, dirnames, filenames in os.walk(dir): - if 'pip-egg-info' in dirnames: - dirnames.remove('pip-egg-info') for dirname in dirnames: dir_arcname = self._get_archive_name( dirname, parentdir=dirpath, rootdir=dir, From feac59544639d36aa31b5fbd5d3b436f67a056ad Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Thu, 2 Apr 2020 02:01:51 +0530 Subject: [PATCH 1555/3170] Don't use cwd in python -m pip command --- news/7731.bugfix | 1 + src/pip/__main__.py | 7 ++++++ tests/functional/test_list.py | 42 +++++++++++++++++++++++++++++++++++ 3 files changed, 50 insertions(+) create mode 100644 news/7731.bugfix diff --git a/news/7731.bugfix b/news/7731.bugfix new file mode 100644 index 00000000000..a73391027fe --- /dev/null +++ b/news/7731.bugfix @@ -0,0 +1 @@ +Avoid using the current directory for check, freeze, install, list and show commands, when invoked as 'python -m pip <command>' diff --git a/src/pip/__main__.py b/src/pip/__main__.py index e83b9e056b3..7c2505fa5bd 100644 --- a/src/pip/__main__.py +++ b/src/pip/__main__.py @@ -3,6 +3,13 @@ import os import sys +# Remove '' and current working directory from the first entry +# of sys.path, if present to avoid using current directory +# in pip commands check, freeze, install, list and show, +# when invoked as python -m pip <command> +if sys.path[0] in ('', os.getcwd()): + sys.path.pop(0) + # If we are running from a wheel, add the wheel to sys.path # This allows the usage python pip-*.whl/pip install pip-*.whl if __package__ == '': diff --git a/tests/functional/test_list.py b/tests/functional/test_list.py index 53f4152c2b7..115675118a1 100644 --- a/tests/functional/test_list.py +++ b/tests/functional/test_list.py @@ -3,6 +3,7 @@ import pytest +from tests.lib import create_test_package_with_setup from tests.lib.path import Path @@ -543,3 +544,44 @@ def test_list_path_multiple(tmpdir, script, data): json_result = json.loads(result.stdout) assert {'name': 'simple', 'version': '2.0'} in json_result assert {'name': 'simple2', 'version': '3.0'} in json_result + + +def test_list_skip_work_dir_pkg(script): + """ + Test that list should not include package in working directory + """ + + # Create a test package and create .egg-info dir + pkg_path = create_test_package_with_setup(script, + name='simple', + version='1.0') + script.run('python', 'setup.py', 'egg_info', + expect_stderr=True, cwd=pkg_path) + + # List should not include package simple when run from package directory + result = script.pip('list', '--format=json', cwd=pkg_path) + json_result = json.loads(result.stdout) + assert {'name': 'simple', 'version': '1.0'} not in json_result + + +def test_list_include_work_dir_pkg(script): + """ + Test that list should include package in working directory + if working directory is added in sys.path + """ + + # Create a test package and create .egg-info dir + pkg_path = create_test_package_with_setup(script, + name='simple', + version='1.0') + + script.run('python', 'setup.py', 'egg_info', + expect_stderr=True, cwd=pkg_path) + + # Add PYTHONPATH env variable + script.environ.update({'PYTHONPATH': pkg_path}) + + # List should include package simple when run from package directory + result = script.pip('list', '--format=json', cwd=pkg_path) + json_result = json.loads(result.stdout) + assert {'name': 'simple', 'version': '1.0'} in json_result From 2324ae422e80ec13d1d8e35aff2994876fc3f36c Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sun, 5 Apr 2020 23:17:03 +0530 Subject: [PATCH 1556/3170] Add unit tests for pip commands not using cwd --- ...2A89E1-F932-4159-9E69-8AEA7DFD6432.trivial | 0 tests/functional/test_check.py | 53 +++++++++++++++++++ tests/functional/test_freeze.py | 45 +++++++++++++--- tests/functional/test_install.py | 48 +++++++++++++++++ tests/functional/test_list.py | 13 ++--- tests/functional/test_show.py | 39 ++++++++++++++ 6 files changed, 184 insertions(+), 14 deletions(-) create mode 100644 news/9E2A89E1-F932-4159-9E69-8AEA7DFD6432.trivial diff --git a/news/9E2A89E1-F932-4159-9E69-8AEA7DFD6432.trivial b/news/9E2A89E1-F932-4159-9E69-8AEA7DFD6432.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/functional/test_check.py b/tests/functional/test_check.py index e06d2e1dd84..3e59b0b5342 100644 --- a/tests/functional/test_check.py +++ b/tests/functional/test_check.py @@ -239,3 +239,56 @@ def test_basic_check_broken_metadata(script): assert 'Error parsing requirements' in result.stderr assert result.returncode == 1 + + +def test_check_skip_work_dir_pkg(script): + """ + Test that check should not include package + present in working directory + """ + + # Create a test package with dependency missing + # and create .egg-info dir + pkg_path = create_test_package_with_setup( + script, name='simple', version='1.0', + install_requires=['missing==0.1']) + + script.run('python', 'setup.py', 'egg_info', + expect_stderr=True, cwd=pkg_path) + + # Check should not complain about broken requirements + # when run from package directory + result = script.pip('check') + expected_lines = ( + "No broken requirements found.", + ) + assert matches_expected_lines(result.stdout, expected_lines) + assert result.returncode == 0 + + +def test_check_include_work_dir_pkg(script): + """ + Test that check should include package in working directory + if working directory is added in PYTHONPATH + """ + + # Create a test package with dependency missing + # and create .egg-info dir + pkg_path = create_test_package_with_setup( + script, name='simple', version='1.0', + install_requires=['missing==0.1']) + + script.run('python', 'setup.py', 'egg_info', + expect_stderr=True, cwd=pkg_path) + + # Add PYTHONPATH env variable + script.environ.update({'PYTHONPATH': pkg_path}) + + # Check should mention about missing requirement simple + # when run from package directory + result = script.pip('check', expect_error=True) + expected_lines = ( + "simple 1.0 requires missing, which is not installed.", + ) + assert matches_expected_lines(result.stdout, expected_lines) + assert result.returncode == 1 diff --git a/tests/functional/test_freeze.py b/tests/functional/test_freeze.py index 7027879bd44..f7ad9ea6847 100644 --- a/tests/functional/test_freeze.py +++ b/tests/functional/test_freeze.py @@ -11,6 +11,7 @@ _create_test_package_with_srcdir, _git_commit, _vcs_add, + create_test_package_with_setup, need_bzr, need_mercurial, need_svn, @@ -818,9 +819,41 @@ def test_freeze_path_multiple(tmpdir, script, data): _check_output(result.stdout, expected) -def test_freeze_direct_url_archive(script, shared_data, with_wheel): - req = "simple @ " + path_to_url(shared_data.packages / "simple-2.0.tar.gz") - assert req.startswith("simple @ file://") - script.pip("install", req) - result = script.pip("freeze") - assert req in result.stdout +def test_freeze_skip_work_dir_pkg(script): + """ + Test that freeze should not include package + present in working directory + """ + + # Create a test package and create .egg-info dir + pkg_path = create_test_package_with_setup( + script, name='simple', version='1.0') + script.run('python', 'setup.py', 'egg_info', + expect_stderr=True, cwd=pkg_path) + + # Freeze should not include package simple when run from package directory + result = script.pip('freeze', 'simple', cwd=pkg_path) + _check_output(result.stdout, '') + + +def test_freeze_include_work_dir_pkg(script): + """ + Test that freeze should include package in working directory + if working directory is added in PYTHONPATH + """ + + # Create a test package and create .egg-info dir + pkg_path = create_test_package_with_setup( + script, name='simple', version='1.0') + script.run('python', 'setup.py', 'egg_info', + expect_stderr=True, cwd=pkg_path) + + # Add PYTHONPATH env variable + script.environ.update({'PYTHONPATH': pkg_path}) + + # Freeze should include package simple when run from package directory + result = script.pip('freeze', 'simple', cwd=pkg_path) + expected = textwrap.dedent("""\ + simple==1.0 + <BLANKLINE>""") + _check_output(result.stdout, expected) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 1072c81d198..c1c84e487fd 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1821,3 +1821,51 @@ def test_install_sends_client_cert(install_args, script, cert_factory, data): environ, _ = call_args.args assert "SSL_CLIENT_CERT" in environ assert environ["SSL_CLIENT_CERT"] + + +def test_install_skip_work_dir_pkg(script, data): + """ + Test that install of a package in working directory + should pass on the second attempt after an install + and an uninstall + """ + + # Create a test package and install it + pkg_path = create_test_package_with_setup( + script, name='simple', version='1.0') + script.pip('install', '-e', '.', + expect_stderr=True, cwd=pkg_path) + + # Uninstalling the package and installing it again will succeed + script.pip('uninstall', 'simple', '-y') + + result = script.pip('install', '--find-links', + data.find_links, 'simple', + expect_stderr=True, cwd=pkg_path) + assert 'Successfully installed simple' in result.stdout + + +def test_install_include_work_dir_pkg(script, data): + """ + Test that install of a package in working directory + should fail on the second attempt after an install + if working directory is added in PYTHONPATH + """ + + # Create a test package and install it + pkg_path = create_test_package_with_setup( + script, name='simple', version='1.0') + script.pip('install', '-e', '.', + expect_stderr=True, cwd=pkg_path) + + # Uninstall will fail with given warning + script.pip('uninstall', 'simple', '-y') + + # Add PYTHONPATH env variable + script.environ.update({'PYTHONPATH': pkg_path}) + + # Uninstalling the package and installing it again will fail + result = script.pip('install', '--find-links', + data.find_links, 'simple', + expect_stderr=True, cwd=pkg_path) + assert 'Requirement already satisfied: simple' in result.stdout diff --git a/tests/functional/test_list.py b/tests/functional/test_list.py index 115675118a1..41c46ad7ce3 100644 --- a/tests/functional/test_list.py +++ b/tests/functional/test_list.py @@ -552,9 +552,8 @@ def test_list_skip_work_dir_pkg(script): """ # Create a test package and create .egg-info dir - pkg_path = create_test_package_with_setup(script, - name='simple', - version='1.0') + pkg_path = create_test_package_with_setup( + script, name='simple', version='1.0') script.run('python', 'setup.py', 'egg_info', expect_stderr=True, cwd=pkg_path) @@ -567,14 +566,12 @@ def test_list_skip_work_dir_pkg(script): def test_list_include_work_dir_pkg(script): """ Test that list should include package in working directory - if working directory is added in sys.path + if working directory is added in PYTHONPATH """ # Create a test package and create .egg-info dir - pkg_path = create_test_package_with_setup(script, - name='simple', - version='1.0') - + pkg_path = create_test_package_with_setup( + script, name='simple', version='1.0') script.run('python', 'setup.py', 'egg_info', expect_stderr=True, cwd=pkg_path) diff --git a/tests/functional/test_show.py b/tests/functional/test_show.py index 0a75d0b10a2..fab1be27b28 100644 --- a/tests/functional/test_show.py +++ b/tests/functional/test_show.py @@ -5,6 +5,7 @@ from pip import __version__ from pip._internal.commands.show import search_packages_info +from tests.lib import create_test_package_with_setup def test_basic_show(script): @@ -259,3 +260,41 @@ def test_show_required_by_packages_requiring_capitalized(script, data): assert 'Name: Requires-Capitalized' in lines assert 'Required-by: requires-requires-capitalized' in lines + + +def test_show_skip_work_dir_pkg(script): + """ + Test that show should not include package + present in working directory + """ + + # Create a test package and create .egg-info dir + pkg_path = create_test_package_with_setup( + script, name='simple', version='1.0') + script.run('python', 'setup.py', 'egg_info', + expect_stderr=True, cwd=pkg_path) + + # Show should not include package simple when run from package directory + result = script.pip('show', 'simple', cwd=pkg_path) + assert 'WARNING: Package(s) not found: simple' in result.stderr + + +def test_show_include_work_dir_pkg(script): + """ + Test that show should include package in working directory + if working directory is added in PYTHONPATH + """ + + # Create a test package and create .egg-info dir + pkg_path = create_test_package_with_setup( + script, name='simple', version='1.0') + script.run('python', 'setup.py', 'egg_info', + expect_stderr=True, cwd=pkg_path) + + # Add PYTHONPATH env variable + script.environ.update({'PYTHONPATH': pkg_path}) + + # Show should include package simple when run from package directory + result = script.pip('show', 'simple', cwd=pkg_path) + lines = result.stdout.splitlines() + assert 'Name: simple' in lines From 29b4ec79ed82cf968463f5967af1fa2f94489fc5 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Mon, 6 Apr 2020 00:27:30 +0530 Subject: [PATCH 1557/3170] Fixed issue in test_show for failing test --- tests/functional/test_show.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/test_show.py b/tests/functional/test_show.py index fab1be27b28..ba0e9d4077a 100644 --- a/tests/functional/test_show.py +++ b/tests/functional/test_show.py @@ -275,7 +275,7 @@ def test_show_skip_work_dir_pkg(script): expect_stderr=True, cwd=pkg_path) # Show should not include package simple when run from package directory - result = script.pip('show', 'simple', cwd=pkg_path) + result = script.pip('show', 'simple', expect_error=True, cwd=pkg_path) assert 'WARNING: Package(s) not found: simple' in result.stderr From 84baf2110f3fbc1d0f0e04f6e4306ba57219e1d0 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Mon, 6 Apr 2020 01:23:21 +0530 Subject: [PATCH 1558/3170] Fixed issue in test_freeze for failing test --- tests/functional/test_freeze.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/tests/functional/test_freeze.py b/tests/functional/test_freeze.py index f7ad9ea6847..b3fb9bce129 100644 --- a/tests/functional/test_freeze.py +++ b/tests/functional/test_freeze.py @@ -832,8 +832,8 @@ def test_freeze_skip_work_dir_pkg(script): expect_stderr=True, cwd=pkg_path) # Freeze should not include package simple when run from package directory - result = script.pip('freeze', 'simple', cwd=pkg_path) - _check_output(result.stdout, '') + result = script.pip('freeze', cwd=pkg_path) + assert 'simple==1.0' not in result.stdout def test_freeze_include_work_dir_pkg(script): @@ -852,8 +852,5 @@ def test_freeze_include_work_dir_pkg(script): script.environ.update({'PYTHONPATH': pkg_path}) # Freeze should include package simple when run from package directory - result = script.pip('freeze', 'simple', cwd=pkg_path) - expected = textwrap.dedent("""\ - simple==1.0 - <BLANKLINE>""") - _check_output(result.stdout, expected) + result = script.pip('freeze', cwd=pkg_path) + assert 'simple==1.0' in result.stdout From 4c70c6d350e62ba3935350dc41afd48360fcf8c5 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Fri, 3 Apr 2020 00:08:32 +0530 Subject: [PATCH 1559/3170] Warn if an invalid URL is passed with --index-url --- news/7430.bugfix | 1 + src/pip/_internal/models/search_scope.py | 26 ++++++++++++++++++++---- 2 files changed, 23 insertions(+), 4 deletions(-) create mode 100644 news/7430.bugfix diff --git a/news/7430.bugfix b/news/7430.bugfix new file mode 100644 index 00000000000..9d0146af652 --- /dev/null +++ b/news/7430.bugfix @@ -0,0 +1 @@ +Warn if an invalid URL is passed with --index-url diff --git a/src/pip/_internal/models/search_scope.py b/src/pip/_internal/models/search_scope.py index 138d1b6eedf..2b586e06b11 100644 --- a/src/pip/_internal/models/search_scope.py +++ b/src/pip/_internal/models/search_scope.py @@ -77,11 +77,29 @@ def __init__( def get_formatted_locations(self): # type: () -> str lines = [] + redacted_index_urls = [] if self.index_urls and self.index_urls != [PyPI.simple_url]: - lines.append( - 'Looking in indexes: {}'.format(', '.join( - redact_auth_from_url(url) for url in self.index_urls)) - ) + for url in self.index_urls: + + redacted_index_url = redact_auth_from_url(url) + + # Parse the URL + purl = urllib_parse.urlsplit(redacted_index_url) + + # URL is generally invalid if scheme and netlock is missing + # 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 purl.scheme and not purl.netloc: + logger.warning('index-url {} is invalid.'.format( + redacted_index_url)) + + redacted_index_urls.append(redacted_index_url) + + lines.append('Looking in indexes: {}'.format( + ', '.join(redacted_index_urls))) + if self.find_links: lines.append( 'Looking in links: {}'.format(', '.join( From c2aa573107998f4ceeaff03c64312a97117f76bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Mon, 6 Apr 2020 16:44:48 +0700 Subject: [PATCH 1560/3170] Fix tabulate col size in case of empty cell Previously, the size is no less than len(str(None)) == 4. This commit also add type hint and docstring to the function. --- ...174d2e-1647-4794-b1d0-58f32da01540.trivial | 0 src/pip/_internal/commands/list.py | 31 ++++++++++--------- 2 files changed, 16 insertions(+), 15 deletions(-) create mode 100644 news/7a174d2e-1647-4794-b1d0-58f32da01540.trivial diff --git a/news/7a174d2e-1647-4794-b1d0-58f32da01540.trivial b/news/7a174d2e-1647-4794-b1d0-58f32da01540.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index cce470a6051..9b70fe58a40 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -7,7 +7,7 @@ import logging from pip._vendor import six -from pip._vendor.six.moves import zip_longest +from pip._vendor.six.moves import map, zip_longest from pip._internal.cli import cmdoptions from pip._internal.cli.req_command import IndexGroupCommand @@ -21,6 +21,10 @@ write_output, ) from pip._internal.utils.packaging import get_installer +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import Any, Iterable, List, Tuple logger = logging.getLogger(__name__) @@ -241,22 +245,19 @@ def output_package_listing_columns(self, data, header): write_output(val) -def tabulate(vals): - # From pfmoore on GitHub: - # https://github.com/pypa/pip/issues/3651#issuecomment-216932564 - assert len(vals) > 0 - - sizes = [0] * max(len(x) for x in vals) - for row in vals: - sizes = [max(s, len(str(c))) for s, c in zip_longest(sizes, row)] +def tabulate(rows): + # type: (Iterable[Iterable[Any]]) -> Tuple[List[str], List[int]] + """Return a list of formatted rows and a list of column sizes. - result = [] - for row in vals: - display = " ".join([str(c).ljust(s) if c is not None else '' - for s, c in zip_longest(sizes, row)]) - result.append(display) + For example:: - return result, sizes + >>> tabulate([['foobar', 2000], [0xdeadbeef]]) + (['foobar 2000', '3735928559'], [10, 4]) + """ + rows = [tuple(map(str, row)) for row in rows] + sizes = [max(map(len, col)) for col in zip_longest(*rows, fillvalue='')] + table = [" ".join(map(str.ljust, row, sizes)) for row in rows] + return table, sizes def format_for_columns(pkgs, options): From ac65f136fdbdd4d5e18325bc5b249f453f385000 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Mon, 6 Apr 2020 17:22:57 +0530 Subject: [PATCH 1561/3170] Add tests to verify --index-url behaviour --- tests/functional/test_install.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 1072c81d198..4b6e8049920 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1769,6 +1769,30 @@ def test_ignore_yanked_file(script, data): assert 'Successfully installed simple-2.0\n' in result.stdout, str(result) +def test_invalid_index_url_argument(script): + """ + Test the behaviour of an invalid --index-url argument + """ + + result = script.pip('install', '--index-url', '--user', + TestPyPI.simple_url, 'simple', + expect_error=True) + + assert 'WARNING: index-url --user is invalid.' in \ + result.stderr, str(result) + + +def test_valid_index_url_argument(script): + """ + Test the behaviour of an valid --index-url argument + """ + + result = script.pip('install', '--no-deps', '--index-url', + TestPyPI.simple_url, 'simple') + + assert 'Successfully installed simple' in result.stdout, str(result) + + def test_install_yanked_file_and_print_warning(script, data): """ Test install a "yanked" file and print a warning. From a73ad91c8b3e123bc6e78d74815ec2249eda343c Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Tue, 7 Apr 2020 09:06:13 +0530 Subject: [PATCH 1562/3170] Running test_check for pkg_path --- tests/functional/test_check.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/functional/test_check.py b/tests/functional/test_check.py index 3e59b0b5342..ae13d07fd3f 100644 --- a/tests/functional/test_check.py +++ b/tests/functional/test_check.py @@ -258,7 +258,7 @@ def test_check_skip_work_dir_pkg(script): # Check should not complain about broken requirements # when run from package directory - result = script.pip('check') + result = script.pip('check', cwd=pkg_path) expected_lines = ( "No broken requirements found.", ) @@ -286,7 +286,7 @@ def test_check_include_work_dir_pkg(script): # Check should mention about missing requirement simple # when run from package directory - result = script.pip('check', expect_error=True) + result = script.pip('check', expect_error=True, cwd=pkg_path) expected_lines = ( "simple 1.0 requires missing, which is not installed.", ) From efd6dd28d0d95c7bd193da68b80f50f0c1d2c3e1 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Mon, 6 Apr 2020 21:24:20 +0530 Subject: [PATCH 1563/3170] Address review comments --- src/pip/_internal/models/search_scope.py | 7 ++++--- tests/functional/test_install.py | 17 +++++++++-------- 2 files changed, 13 insertions(+), 11 deletions(-) diff --git a/src/pip/_internal/models/search_scope.py b/src/pip/_internal/models/search_scope.py index 2b586e06b11..7a0008e4825 100644 --- a/src/pip/_internal/models/search_scope.py +++ b/src/pip/_internal/models/search_scope.py @@ -86,14 +86,15 @@ def get_formatted_locations(self): # Parse the URL purl = urllib_parse.urlsplit(redacted_index_url) - # URL is generally invalid if scheme and netlock is missing + # URL is generally invalid if scheme and netloc is missing # 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 purl.scheme and not purl.netloc: - logger.warning('index-url {} is invalid.'.format( - redacted_index_url)) + logger.warning( + 'The index url "{}" seems invalid, ' + 'please provide a scheme.'.format(redacted_index_url)) redacted_index_urls.append(redacted_index_url) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 4b6e8049920..a4bacfe9447 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1769,28 +1769,29 @@ def test_ignore_yanked_file(script, data): assert 'Successfully installed simple-2.0\n' in result.stdout, str(result) -def test_invalid_index_url_argument(script): +def test_invalid_index_url_argument(script, shared_data): """ Test the behaviour of an invalid --index-url argument """ result = script.pip('install', '--index-url', '--user', - TestPyPI.simple_url, 'simple', + shared_data.find_links3, "Dinner", expect_error=True) - assert 'WARNING: index-url --user is invalid.' in \ - result.stderr, str(result) + assert 'WARNING: The index url "--user" seems invalid, ' \ + 'please provide a scheme.' in result.stderr, str(result) -def test_valid_index_url_argument(script): +def test_valid_index_url_argument(script, shared_data): """ Test the behaviour of an valid --index-url argument """ - result = script.pip('install', '--no-deps', '--index-url', - TestPyPI.simple_url, 'simple') + result = script.pip('install', '--index-url', + shared_data.find_links3, + "Dinner") - assert 'Successfully installed simple' in result.stdout, str(result) + assert 'Successfully installed Dinner' in result.stdout, str(result) def test_install_yanked_file_and_print_warning(script, data): From 26b76f6f11dfe51632acf321af186e7a227090c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Tue, 7 Apr 2020 17:20:57 +0700 Subject: [PATCH 1564/3170] Move tabulate to utils.misc and test it --- src/pip/_internal/commands/list.py | 21 +-------------------- src/pip/_internal/utils/misc.py | 17 ++++++++++++++++- tests/unit/test_utils.py | 18 ++++++++++++++++++ 3 files changed, 35 insertions(+), 21 deletions(-) diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index 9b70fe58a40..109ec5c664e 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -7,7 +7,6 @@ import logging from pip._vendor import six -from pip._vendor.six.moves import map, zip_longest from pip._internal.cli import cmdoptions from pip._internal.cli.req_command import IndexGroupCommand @@ -18,13 +17,10 @@ from pip._internal.utils.misc import ( dist_is_editable, get_installed_distributions, + tabulate, write_output, ) from pip._internal.utils.packaging import get_installer -from pip._internal.utils.typing import MYPY_CHECK_RUNNING - -if MYPY_CHECK_RUNNING: - from typing import Any, Iterable, List, Tuple logger = logging.getLogger(__name__) @@ -245,21 +241,6 @@ def output_package_listing_columns(self, data, header): write_output(val) -def tabulate(rows): - # type: (Iterable[Iterable[Any]]) -> Tuple[List[str], List[int]] - """Return a list of formatted rows and a list of column sizes. - - For example:: - - >>> tabulate([['foobar', 2000], [0xdeadbeef]]) - (['foobar 2000', '3735928559'], [10, 4]) - """ - rows = [tuple(map(str, row)) for row in rows] - sizes = [max(map(len, col)) for col in zip_longest(*rows, fillvalue='')] - table = [" ".join(map(str.ljust, row, sizes)) for row in rows] - return table, sizes - - def format_for_columns(pkgs, options): """ Convert the package data into something usable diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index a36701a7095..09031825afa 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -22,7 +22,7 @@ # why we ignore the type on this import. from pip._vendor.retrying import retry # type: ignore from pip._vendor.six import PY2, text_type -from pip._vendor.six.moves import input, zip_longest +from pip._vendor.six.moves import input, map, zip_longest from pip._vendor.six.moves.urllib import parse as urllib_parse from pip._vendor.six.moves.urllib.parse import unquote as urllib_unquote @@ -275,6 +275,21 @@ def format_size(bytes): return '{} bytes'.format(int(bytes)) +def tabulate(rows): + # type: (Iterable[Iterable[Any]]) -> Tuple[List[str], List[int]] + """Return a list of formatted rows and a list of column sizes. + + For example:: + + >>> tabulate([['foobar', 2000], [0xdeadbeef]]) + (['foobar 2000', '3735928559'], [10, 4]) + """ + rows = [tuple(map(str, row)) for row in rows] + sizes = [max(map(len, col)) for col in zip_longest(*rows, fillvalue='')] + table = [" ".join(map(str.ljust, row, sizes)).rstrip() for row in rows] + return table, sizes + + def is_installable_dir(path): # type: (str) -> bool """Is path is a directory containing setup.py or pyproject.toml? diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index fa042519ee9..7d74a664982 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -50,6 +50,7 @@ rmtree_errorhandler, split_auth_from_netloc, split_auth_netloc_from_url, + tabulate, ) from pip._internal.utils.setuptools_build import make_setuptools_shim_args @@ -970,3 +971,20 @@ def test_is_console_interactive(monkeypatch, isatty, no_stdin, expected): ]) def test_format_size(size, expected): assert format_size(size) == expected + + +@pytest.mark.parametrize( + ('rows', 'table', 'sizes'), + [([], [], []), + ([('I?', 'version', 'sdist', 'wheel'), + ('', '1.18.2', 'zip', 'cp38-cp38m-win_amd64'), + ('v', 1.18, 'zip')], + ['I? version sdist wheel', + ' 1.18.2 zip cp38-cp38m-win_amd64', + 'v 1.18 zip'], + [2, 7, 5, 20]), + ([('I?', 'version', 'sdist', 'wheel'), (), ('v', '1.18.1', 'zip')], + ['I? version sdist wheel', '', 'v 1.18.1 zip'], + [2, 7, 5, 5])]) +def test_tabulate(rows, table, sizes): + assert tabulate(rows) == (table, sizes) From 2f36ac7587334beb058e1ad95fee52786555abcc Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 7 Apr 2020 19:02:17 +0800 Subject: [PATCH 1565/3170] Add assertion message for easier debugging --- .../resolution/resolvelib/candidates.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index db750823f81..65f817f391f 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -156,10 +156,19 @@ def dist(self): # These should be "proper" errors, not just asserts, as they # can result from user errors like a requirement "foo @ URL" # when the project at URL has a name of "bar" in its metadata. - assert (self._name is None or - self._name == canonicalize_name(self._dist.project_name)) - assert (self._version is None or - self._version == self.dist.parsed_version) + assert ( + self._name is None or + self._name == canonicalize_name(self._dist.project_name) + ), "Name mismatch: {!r} vs {!r}".format( + self._name, canonicalize_name(self._dist.project_name), + ) + assert ( + self._version is None or + self._version == self._dist.parsed_version + ), "Version mismatch: {!r} vs {!r}".format( + self._version, self._dist.parsed_version, + ) + return self._dist def _get_requires_python_specifier(self): From b89e1c2fb470292f246c6e42c429ac3a365a64e9 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 7 Apr 2020 19:02:39 +0800 Subject: [PATCH 1566/3170] Canonicalize InstallationCandidate name Caught by test_single_download_from_requirements_file etc. --- src/pip/_internal/resolution/resolvelib/factory.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index cf5b2b19edf..14cd2f9e344 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -1,3 +1,4 @@ +from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.pkg_resources import ( DistributionNotFound, VersionConflict, @@ -121,7 +122,7 @@ def make_candidate_from_ican( link=ican.link, extras=extras, parent=parent, - name=ican.name, + name=canonicalize_name(ican.name), version=ican.version, ) return self._make_candidate_from_dist( From 6d40804b12fbe6fd0e670517c9120c75d6a81df6 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 7 Apr 2020 20:53:25 +0800 Subject: [PATCH 1567/3170] Add __repr__ to requirement/candidate models --- .../resolution/resolvelib/candidates.py | 22 +++++++++++++++++++ .../resolution/resolvelib/requirements.py | 21 ++++++++++++++++++ 2 files changed, 43 insertions(+) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index db750823f81..c218be7da42 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -110,6 +110,13 @@ def __init__( self._version = version self._dist = None # type: Optional[Distribution] + def __repr__(self): + # type: () -> str + return "{class_name}({link!r})".format( + class_name=self.__class__.__name__, + link=str(self.link), + ) + def __eq__(self, other): # type: (Any) -> bool if isinstance(other, self.__class__): @@ -259,6 +266,13 @@ def __init__( skip_reason = "already satisfied" factory.preparer.prepare_installed_requirement(self._ireq, skip_reason) + def __repr__(self): + # type: () -> str + return "{class_name}({distribution!r})".format( + class_name=self.__class__.__name__, + distribution=self.dist, + ) + @property def name(self): # type: () -> str @@ -314,6 +328,14 @@ def __init__( self.base = base self.extras = extras + def __repr__(self): + # type: () -> str + return "{class_name}(base={base!r}, extras={extras!r})".format( + class_name=self.__class__.__name__, + base=self.base, + extras=self.extras, + ) + @property def name(self): # type: () -> str diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py index 033e02cafbd..027e36aa61e 100644 --- a/src/pip/_internal/resolution/resolvelib/requirements.py +++ b/src/pip/_internal/resolution/resolvelib/requirements.py @@ -18,6 +18,13 @@ def __init__(self, candidate): # type: (Candidate) -> None self.candidate = candidate + def __repr__(self): + # type: () -> str + return "{class_name}({candidate!r})".format( + class_name=self.__class__.__name__, + candidate=self.candidate, + ) + @property def name(self): # type: () -> str @@ -43,6 +50,13 @@ def __init__(self, name): # type: (str) -> None self._name = name + def __repr__(self): + # type: () -> str + return "{class_name}(name={name!r})".format( + class_name=self.__class__.__name__, + name=self._name, + ) + @property def name(self): # type: () -> str @@ -65,6 +79,13 @@ def __init__(self, ireq, factory): self._factory = factory self.extras = ireq.req.extras + def __repr__(self): + # type: () -> str + return "{class_name}({requirement!r})".format( + class_name=self.__class__.__name__, + requirement=str(self._ireq.req), + ) + @property def name(self): # type: () -> str From da2ab6b829969bfbac37cc9098e594b4f32f98cf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sun, 5 Apr 2020 11:21:38 +0200 Subject: [PATCH 1568/3170] Simplify legacy metadata generation Always generate legacy metadata in a temporary directory, even in the editable case. Generating it in the source directory did not add value, because setup.py develop always regenerates the .egg-info directory. --- .../operations/build/metadata_legacy.py | 75 ++++--------------- 1 file changed, 16 insertions(+), 59 deletions(-) diff --git a/src/pip/_internal/operations/build/metadata_legacy.py b/src/pip/_internal/operations/build/metadata_legacy.py index 5e23ccf8eef..6edc66b36a4 100644 --- a/src/pip/_internal/operations/build/metadata_legacy.py +++ b/src/pip/_internal/operations/build/metadata_legacy.py @@ -9,69 +9,34 @@ from pip._internal.utils.subprocess import call_subprocess from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from pip._internal.vcs import vcs if MYPY_CHECK_RUNNING: - from typing import List, Optional - from pip._internal.build_env import BuildEnvironment logger = logging.getLogger(__name__) -def _find_egg_info(source_directory): +def _find_egg_info(directory): # type: (str) -> str - """Find an .egg-info in `source_directory`. + """Find an .egg-info subdirectory in `directory`. """ - - def looks_like_virtual_env(path): - # type: (str) -> bool - return ( - os.path.lexists(os.path.join(path, 'bin', 'python')) or - os.path.exists(os.path.join(path, 'Scripts', 'Python.exe')) - ) - - def locate_egg_info(base): - # type: (str) -> List[str] - candidates = [] # type: List[str] - for root, dirs, files in os.walk(base): - for dir_ in vcs.dirnames: - if dir_ in dirs: - dirs.remove(dir_) - # Iterate over a copy of ``dirs``, since mutating - # a list while iterating over it can cause trouble. - # (See https://github.com/pypa/pip/pull/462.) - for dir_ in list(dirs): - if looks_like_virtual_env(os.path.join(root, dir_)): - dirs.remove(dir_) - # Also don't search through tests - elif dir_ == 'test' or dir_ == 'tests': - dirs.remove(dir_) - candidates.extend(os.path.join(root, dir_) for dir_ in dirs) - return [f for f in candidates if f.endswith('.egg-info')] - - def depth_of_directory(dir_): - # type: (str) -> int - return ( - dir_.count(os.path.sep) + - (os.path.altsep and dir_.count(os.path.altsep) or 0) - ) - - base = source_directory - filenames = locate_egg_info(base) + filenames = [ + f for f in os.listdir(directory) if f.endswith(".egg-info") + ] if not filenames: raise InstallationError( - "Files/directories not found in {}".format(base) + "No .egg-info directory found in {}".format(directory) ) - # If we have more than one match, we pick the toplevel one. This - # can easily be the case if there is a dist folder which contains - # an extracted tarball for testing purposes. if len(filenames) > 1: - filenames.sort(key=depth_of_directory) + raise InstallationError( + "More than one .egg-info directory found in {}".format( + directory + ) + ) - return os.path.join(base, filenames[0]) + return os.path.join(directory, filenames[0]) def generate_metadata( @@ -92,14 +57,9 @@ def generate_metadata( setup_py_path, details, ) - egg_info_dir = None # type: Optional[str] - # For non-editable installs, don't put the .egg-info files at the root, - # to avoid confusion due to the source code being considered an installed - # egg. - if not editable: - egg_info_dir = TempDirectory( - kind="pip-egg-info", globally_managed=True - ).path + egg_info_dir = TempDirectory( + kind="pip-egg-info", globally_managed=True + ).path args = make_setuptools_egg_info_args( setup_py_path, @@ -115,7 +75,4 @@ def generate_metadata( ) # Return the .egg-info directory. - if not editable: - assert egg_info_dir - return _find_egg_info(egg_info_dir) - return _find_egg_info(source_dir) + return _find_egg_info(egg_info_dir) From 34adf0a258f2e51db9b82c37fee067d86e6b96cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sun, 5 Apr 2020 11:22:24 +0200 Subject: [PATCH 1569/3170] Simplify make_setuptools_egg_info_args --- src/pip/_internal/utils/setuptools_build.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/utils/setuptools_build.py b/src/pip/_internal/utils/setuptools_build.py index 4147a650dca..2a664b00703 100644 --- a/src/pip/_internal/utils/setuptools_build.py +++ b/src/pip/_internal/utils/setuptools_build.py @@ -121,9 +121,9 @@ def make_setuptools_egg_info_args( no_user_config, # type: bool ): # type: (...) -> List[str] - args = make_setuptools_shim_args(setup_py_path) - if no_user_config: - args += ["--no-user-cfg"] + args = make_setuptools_shim_args( + setup_py_path, no_user_config=no_user_config + ) args += ["egg_info"] From 030578ef04bf6ea35a0ccd2acc0ca61a2fe5c050 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Tue, 7 Apr 2020 17:00:11 +0200 Subject: [PATCH 1570/3170] Remove unused argument from generate_metadata --- src/pip/_internal/operations/build/metadata_legacy.py | 1 - src/pip/_internal/req/req_install.py | 1 - 2 files changed, 2 deletions(-) diff --git a/src/pip/_internal/operations/build/metadata_legacy.py b/src/pip/_internal/operations/build/metadata_legacy.py index 6edc66b36a4..14762aef3c0 100644 --- a/src/pip/_internal/operations/build/metadata_legacy.py +++ b/src/pip/_internal/operations/build/metadata_legacy.py @@ -43,7 +43,6 @@ def generate_metadata( build_env, # type: BuildEnvironment setup_py_path, # type: str source_dir, # type: str - editable, # type: bool isolated, # type: bool details, # type: str ): diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 985c31fd60e..906058f7d65 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -516,7 +516,6 @@ def _generate_metadata(self): build_env=self.build_env, setup_py_path=self.setup_py_path, source_dir=self.unpacked_source_directory, - editable=self.editable, isolated=self.isolated, details=self.name or "from {}".format(self.link) ) From 6e7b16cec4c2c242f8b19e378d765878ed0f86df Mon Sep 17 00:00:00 2001 From: Danny McClanahan <1305167+cosmicexplorer@users.noreply.github.com> Date: Wed, 12 Feb 2020 19:23:46 -0800 Subject: [PATCH 1571/3170] add failing test ; apply the fix ; add template NEWS entry add failing test apply the fix add template NEWS entry according to https://pip.pypa.io/en/latest/development/contributing/#news-entries (wrong PR #) rename news entry to the current PR # respond to review comments fix test failures fix tests by adding uuid salt in urls cache html page fetching by link make CI pass (?) make the types much better finally listen to the maintainer and cache parse_links() by url :) avoid caching parse_links() when the url is an index url cleanup add testing for uncachable marking only conditionally vendor _lru_cache for py2 bugfix => feature python 2 does not cache! Do away with type: ignore with getattr() respond to review comments --- news/7729.feature | 3 + src/pip/_internal/index/collector.py | 96 +++++++++++++++++++++++++--- src/pip/_internal/models/link.py | 8 +++ tests/unit/test_collector.py | 60 ++++++++++++++++- 4 files changed, 154 insertions(+), 13 deletions(-) create mode 100644 news/7729.feature diff --git a/news/7729.feature b/news/7729.feature new file mode 100644 index 00000000000..de4f6e7c8d7 --- /dev/null +++ b/news/7729.feature @@ -0,0 +1,3 @@ +Cache the result of parse_links() to avoid re-tokenizing a find-links page multiple times over a pip run. + +This change significantly improves resolve performance when --find-links points to a very large html page. diff --git a/src/pip/_internal/index/collector.py b/src/pip/_internal/index/collector.py index 033e37ac75b..e2c800c2cde 100644 --- a/src/pip/_internal/index/collector.py +++ b/src/pip/_internal/index/collector.py @@ -3,6 +3,7 @@ """ import cgi +import functools import itertools import logging import mimetypes @@ -25,8 +26,8 @@ if MYPY_CHECK_RUNNING: from typing import ( - Callable, Iterable, List, MutableMapping, Optional, Sequence, Tuple, - Union, + Callable, Iterable, List, MutableMapping, Optional, + Protocol, Sequence, Tuple, TypeVar, Union, ) import xml.etree.ElementTree @@ -38,10 +39,31 @@ HTMLElement = xml.etree.ElementTree.Element ResponseHeaders = MutableMapping[str, str] + # Used in the @lru_cache polyfill. + F = TypeVar('F') + + class LruCache(Protocol): + def __call__(self, maxsize=None): + # type: (Optional[int]) -> Callable[[F], F] + raise NotImplementedError + logger = logging.getLogger(__name__) +# Fallback to noop_lru_cache in Python 2 +# TODO: this can be removed when python 2 support is dropped! +def noop_lru_cache(maxsize=None): + # type: (Optional[int]) -> Callable[[F], F] + def _wrapper(f): + # type: (F) -> F + return f + return _wrapper + + +_lru_cache = getattr(functools, "lru_cache", noop_lru_cache) # type: LruCache + + def _match_vcs_scheme(url): # type: (str) -> Optional[str] """Look for VCS schemes in the URL. @@ -285,6 +307,48 @@ def _create_link_from_element( return link +class CacheablePageContent(object): + def __init__(self, page): + # type: (HTMLPage) -> None + assert page.cache_link_parsing + self.page = page + + def __eq__(self, other): + # type: (object) -> bool + return (isinstance(other, type(self)) and + self.page.url == other.page.url) + + def __hash__(self): + # type: () -> int + return hash(self.page.url) + + +def with_cached_html_pages( + fn, # type: Callable[[HTMLPage], Iterable[Link]] +): + # type: (...) -> Callable[[HTMLPage], List[Link]] + """ + Given a function that parses an Iterable[Link] from an HTMLPage, cache the + function's result (keyed by CacheablePageContent), unless the HTMLPage + `page` has `page.cache_link_parsing == False`. + """ + + @_lru_cache(maxsize=None) + def wrapper(cacheable_page): + # type: (CacheablePageContent) -> List[Link] + return list(fn(cacheable_page.page)) + + @functools.wraps(fn) + def wrapper_wrapper(page): + # type: (HTMLPage) -> List[Link] + if page.cache_link_parsing: + return wrapper(CacheablePageContent(page)) + return list(fn(page)) + + return wrapper_wrapper + + +@with_cached_html_pages def parse_links(page): # type: (HTMLPage) -> Iterable[Link] """ @@ -314,18 +378,23 @@ class HTMLPage(object): def __init__( self, - content, # type: bytes - encoding, # type: Optional[str] - url, # type: str + content, # type: bytes + encoding, # type: Optional[str] + url, # type: str + cache_link_parsing=True, # type: bool ): # type: (...) -> None """ :param encoding: the encoding to decode the given content. :param url: the URL from which the HTML was downloaded. + :param cache_link_parsing: whether links parsed from this page's url + should be cached. PyPI index urls should + have this set to False, for example. """ self.content = content self.encoding = encoding self.url = url + self.cache_link_parsing = cache_link_parsing def __str__(self): # type: () -> str @@ -343,10 +412,14 @@ def _handle_get_page_fail( meth("Could not fetch URL %s: %s - skipping", link, reason) -def _make_html_page(response): - # type: (Response) -> HTMLPage +def _make_html_page(response, cache_link_parsing=True): + # type: (Response, bool) -> HTMLPage encoding = _get_encoding_from_headers(response.headers) - return HTMLPage(response.content, encoding=encoding, url=response.url) + return HTMLPage( + response.content, + encoding=encoding, + url=response.url, + cache_link_parsing=cache_link_parsing) def _get_html_page(link, session=None): @@ -399,7 +472,8 @@ def _get_html_page(link, session=None): except requests.Timeout: _handle_get_page_fail(link, "timed out") else: - return _make_html_page(resp) + return _make_html_page(resp, + cache_link_parsing=link.cache_link_parsing) return None @@ -562,7 +636,9 @@ def collect_links(self, project_name): # We want to filter out anything that does not have a secure origin. url_locations = [ link for link in itertools.chain( - (Link(url) for url in index_url_loc), + # Mark PyPI indices as "cache_link_parsing == False" -- this + # will avoid caching the result of parsing the page for links. + (Link(url, cache_link_parsing=False) for url in index_url_loc), (Link(url) for url in fl_url_loc), ) if self.session.is_secure_origin(link) diff --git a/src/pip/_internal/models/link.py b/src/pip/_internal/models/link.py index c3e743d3904..df4f8f01685 100644 --- a/src/pip/_internal/models/link.py +++ b/src/pip/_internal/models/link.py @@ -30,6 +30,7 @@ def __init__( comes_from=None, # type: Optional[Union[str, HTMLPage]] requires_python=None, # type: Optional[str] yanked_reason=None, # type: Optional[Text] + cache_link_parsing=True, # type: bool ): # type: (...) -> None """ @@ -46,6 +47,11 @@ def __init__( a simple repository HTML link. If the file has been yanked but no reason was provided, this should be the empty string. See PEP 592 for more information and the specification. + :param cache_link_parsing: A flag that is used elsewhere to determine + whether resources retrieved from this link + should be cached. PyPI index urls should + generally have this set to False, for + example. """ # url can be a UNC windows share @@ -63,6 +69,8 @@ def __init__( super(Link, self).__init__(key=url, defining_class=Link) + self.cache_link_parsing = cache_link_parsing + def __str__(self): # type: () -> str if self.requires_python: diff --git a/tests/unit/test_collector.py b/tests/unit/test_collector.py index a650a2a17ef..cfc2af1c07a 100644 --- a/tests/unit/test_collector.py +++ b/tests/unit/test_collector.py @@ -1,5 +1,7 @@ import logging import os.path +import re +import uuid from textwrap import dedent import mock @@ -26,7 +28,7 @@ from pip._internal.models.index import PyPI from pip._internal.models.link import Link from pip._internal.network.session import PipSession -from tests.lib import make_test_link_collector +from tests.lib import make_test_link_collector, skip_if_python2 @pytest.mark.parametrize( @@ -355,7 +357,9 @@ def test_parse_links__yanked_reason(anchor_html, expected): page = HTMLPage( html_bytes, encoding=None, - url='https://example.com/simple/', + # parse_links() is cached by url, so we inject a random uuid to ensure + # the page content isn't cached. + url='https://example.com/simple-{}/'.format(uuid.uuid4()), ) links = list(parse_links(page)) link, = links @@ -363,6 +367,51 @@ def test_parse_links__yanked_reason(anchor_html, expected): assert actual == expected +@skip_if_python2 +def test_parse_links_caches_same_page_by_url(): + html = ( + '<html><head><meta charset="utf-8"><head>' + '<body><a href="/pkg1-1.0.tar.gz"></a></body></html>' + ) + html_bytes = html.encode('utf-8') + + url = 'https://example.com/simple/' + + page_1 = HTMLPage( + html_bytes, + encoding=None, + url=url, + ) + # Make a second page with zero content, to ensure that it's not accessed, + # because the page was cached by url. + page_2 = HTMLPage( + b'', + encoding=None, + url=url, + ) + # Make a third page which represents an index url, which should not be + # cached, even for the same url. We modify the page content slightly to + # verify that the result is not cached. + page_3 = HTMLPage( + re.sub(b'pkg1', b'pkg2', html_bytes), + encoding=None, + url=url, + cache_link_parsing=False, + ) + + parsed_links_1 = list(parse_links(page_1)) + assert len(parsed_links_1) == 1 + assert 'pkg1' in parsed_links_1[0].url + + parsed_links_2 = list(parse_links(page_2)) + assert parsed_links_2 == parsed_links_1 + + parsed_links_3 = list(parse_links(page_3)) + assert len(parsed_links_3) == 1 + assert parsed_links_3 != parsed_links_1 + assert 'pkg2' in parsed_links_3[0].url + + def test_request_http_error(caplog): caplog.set_level(logging.DEBUG) link = Link('http://localhost') @@ -528,13 +577,14 @@ def test_fetch_page(self, mock_get_html_response): fake_response = make_fake_html_response(url) mock_get_html_response.return_value = fake_response - location = Link(url) + location = Link(url, cache_link_parsing=False) link_collector = make_test_link_collector() actual = link_collector.fetch_page(location) assert actual.content == fake_response.content assert actual.encoding is None assert actual.url == url + assert actual.cache_link_parsing == location.cache_link_parsing # Also check that the right session object was passed to # _get_html_response(). @@ -559,8 +609,12 @@ def test_collect_links(self, caplog, data): assert len(actual.find_links) == 1 check_links_include(actual.find_links, names=['packages']) + # Check that find-links URLs are marked as cacheable. + assert actual.find_links[0].cache_link_parsing assert actual.project_urls == [Link('https://pypi.org/simple/twine/')] + # Check that index URLs are marked as *un*cacheable. + assert not actual.project_urls[0].cache_link_parsing expected_message = dedent("""\ 1 location(s) to search for versions of twine: From fe6920bc2f425f6e15e6cc6b225e2995032a9488 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Wed, 8 Apr 2020 13:59:33 +0530 Subject: [PATCH 1572/3170] Addressed review comments --- tests/functional/test_check.py | 4 ++-- tests/functional/test_freeze.py | 14 +++++++++++--- tests/functional/test_install.py | 6 ++++-- tests/functional/test_show.py | 4 ++-- 4 files changed, 19 insertions(+), 9 deletions(-) diff --git a/tests/functional/test_check.py b/tests/functional/test_check.py index ae13d07fd3f..4e9a144bf02 100644 --- a/tests/functional/test_check.py +++ b/tests/functional/test_check.py @@ -281,11 +281,11 @@ def test_check_include_work_dir_pkg(script): script.run('python', 'setup.py', 'egg_info', expect_stderr=True, cwd=pkg_path) - # Add PYTHONPATH env variable script.environ.update({'PYTHONPATH': pkg_path}) # Check should mention about missing requirement simple - # when run from package directory + # when run from package directory, when package directory + # is in PYTHONPATH result = script.pip('check', expect_error=True, cwd=pkg_path) expected_lines = ( "simple 1.0 requires missing, which is not installed.", diff --git a/tests/functional/test_freeze.py b/tests/functional/test_freeze.py index b3fb9bce129..44278c8660d 100644 --- a/tests/functional/test_freeze.py +++ b/tests/functional/test_freeze.py @@ -819,6 +819,14 @@ def test_freeze_path_multiple(tmpdir, script, data): _check_output(result.stdout, expected) +def test_freeze_direct_url_archive(script, shared_data, with_wheel): + req = "simple @ " + path_to_url(shared_data.packages / "simple-2.0.tar.gz") + assert req.startswith("simple @ file://") + script.pip("install", req) + result = script.pip("freeze") + assert req in result.stdout + + def test_freeze_skip_work_dir_pkg(script): """ Test that freeze should not include package @@ -833,7 +841,7 @@ def test_freeze_skip_work_dir_pkg(script): # Freeze should not include package simple when run from package directory result = script.pip('freeze', cwd=pkg_path) - assert 'simple==1.0' not in result.stdout + assert 'simple' not in result.stdout def test_freeze_include_work_dir_pkg(script): @@ -848,9 +856,9 @@ def test_freeze_include_work_dir_pkg(script): script.run('python', 'setup.py', 'egg_info', expect_stderr=True, cwd=pkg_path) - # Add PYTHONPATH env variable script.environ.update({'PYTHONPATH': pkg_path}) - # Freeze should include package simple when run from package directory + # Freeze should include package simple when run from package directory, + # when package directory is in PYTHONPATH result = script.pip('freeze', cwd=pkg_path) assert 'simple==1.0' in result.stdout diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index c1c84e487fd..328bc6eb7b0 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1842,6 +1842,8 @@ def test_install_skip_work_dir_pkg(script, data): result = script.pip('install', '--find-links', data.find_links, 'simple', expect_stderr=True, cwd=pkg_path) + + assert 'Requirement already satisfied: simple' not in result.stdout assert 'Successfully installed simple' in result.stdout @@ -1861,10 +1863,10 @@ def test_install_include_work_dir_pkg(script, data): # Uninstall will fail with given warning script.pip('uninstall', 'simple', '-y') - # Add PYTHONPATH env variable script.environ.update({'PYTHONPATH': pkg_path}) - # Uninstalling the package and installing it again will fail + # Uninstalling the package and installing it again will fail, + # when package directory is in PYTHONPATH result = script.pip('install', '--find-links', data.find_links, 'simple', expect_stderr=True, cwd=pkg_path) diff --git a/tests/functional/test_show.py b/tests/functional/test_show.py index ba0e9d4077a..c19228b566c 100644 --- a/tests/functional/test_show.py +++ b/tests/functional/test_show.py @@ -291,10 +291,10 @@ def test_show_include_work_dir_pkg(script): script.run('python', 'setup.py', 'egg_info', expect_stderr=True, cwd=pkg_path) - # Add PYTHONPATH env variable script.environ.update({'PYTHONPATH': pkg_path}) - # Show should include package simple when run from package directory + # Show should include package simple when run from package directory, + # when package directory is in PYTHONPATH result = script.pip('show', 'simple', cwd=pkg_path) lines = result.stdout.splitlines() assert 'Name: simple' in lines From 018c051a8eeedcb779e650b7ac816a709ebb2460 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sat, 4 Apr 2020 15:17:56 +0800 Subject: [PATCH 1573/3170] Implement equality on candidate classes --- .../resolution/resolvelib/candidates.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 664c5858f02..a3397a271e1 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -282,6 +282,17 @@ def __repr__(self): distribution=self.dist, ) + def __eq__(self, other): + # type: (Any) -> bool + if isinstance(other, self.__class__): + return self.name == other.name and self.version == other.version + return False + + # Needed for Python 2, which does not implement this by default + def __ne__(self, other): + # type: (Any) -> bool + return not self.__eq__(other) + @property def name(self): # type: () -> str @@ -345,6 +356,17 @@ def __repr__(self): extras=self.extras, ) + def __eq__(self, other): + # type: (Any) -> bool + if isinstance(other, self.__class__): + return self.base == other.base and self.extras == other.extras + return False + + # Needed for Python 2, which does not implement this by default + def __ne__(self, other): + # type: (Any) -> bool + return not self.__eq__(other) + @property def name(self): # type: () -> str From 591d476fca082e3fa92061aca514a53e5cb8ce06 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sat, 4 Apr 2020 21:51:20 +0800 Subject: [PATCH 1574/3170] Note on why we don't implement equality --- src/pip/_internal/resolution/resolvelib/candidates.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index a3397a271e1..7bc4815b037 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -420,6 +420,10 @@ def __init__(self, py_version_info): version_info = sys.version_info[:3] self._version = Version(".".join(str(c) for c in version_info)) + # We don't need to implement __eq__() and __ne__() since there is always + # only one RequiresPythonCandidate in a resolution, i.e. the host Python. + # The built-in object.__eq__() and object.__ne__() do exactly what we want. + @property def name(self): # type: () -> str From e714b5cf84ceb2f851b6b556250df20502873b6f Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Mon, 6 Apr 2020 03:39:25 +0800 Subject: [PATCH 1575/3170] Add failing tests for reinstall and upgrade --- tests/functional/test_new_resolver.py | 54 +++++++++++++++++++++++++++ 1 file changed, 54 insertions(+) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 2697a4272c7..28468dc23e1 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -328,3 +328,57 @@ def test_new_resolver_only_builds_sdists_when_needed(script): "base", "dep" ) assert_installed(script, base="0.1.0", dep="0.2.0") + + +@pytest.mark.xfail(reason="upgrade install not implemented") +def test_new_resolver_install_different_version(script): + create_basic_wheel_for_package(script, "base", "0.1.0") + create_basic_wheel_for_package(script, "base", "0.2.0") + + script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "base==0.1.0", + ) + + # This should trigger an uninstallation of base. + result = script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "base==0.2.0", + ) + + assert "Uninstalling base-0.1.0" in result.stdout, str(result) + assert "Successfully uninstalled base-0.1.0" in result.stdout, str(result) + assert script.site_packages / "base" in result.files_updated, ( + "base not upgraded" + ) + assert_installed(script, base="0.2.0") + + +@pytest.mark.xfail(reason="force reinstall not implemented") +def test_new_resolver_force_reinstall(script): + create_basic_wheel_for_package(script, "base", "0.1.0") + + script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "base", + ) + + result = script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "--force-reinstall", + "base", + ) + + assert "Uninstalling base-0.1.0" not in result.stdout, str(result) + assert script.site_packages / "base" in result.files_updated, ( + "base not upgraded" + ) + assert_installed(script, base="0.2.0") From 90ce7c9edd02cadbce4da65379c48262ded272e0 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Mon, 6 Apr 2020 22:40:24 +0800 Subject: [PATCH 1576/3170] Refactor to prepare for upgrade procedures --- .../resolution/resolvelib/factory.py | 31 +++++++------------ 1 file changed, 12 insertions(+), 19 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 14cd2f9e344..c07be80c632 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -1,10 +1,6 @@ from pip._vendor.packaging.utils import canonicalize_name -from pip._vendor.pkg_resources import ( - DistributionNotFound, - VersionConflict, - get_distribution, -) +from pip._internal.utils.misc import get_installed_distributions from pip._internal.utils.typing import MYPY_CHECK_RUNNING from .candidates import ( @@ -56,11 +52,19 @@ def __init__( self.preparer = preparer self._python_candidate = RequiresPythonCandidate(py_version_info) self._make_install_req_from_spec = make_install_req - self._ignore_installed = ignore_installed self._ignore_requires_python = ignore_requires_python + self._link_candidate_cache = {} # type: Cache[LinkCandidate] self._editable_candidate_cache = {} # type: Cache[EditableCandidate] + if not ignore_installed: + self._installed_dists = { + dist.project_name: dist + for dist in get_installed_distributions() + } + else: + self._installed_dists = {} + def _make_candidate_from_dist( self, dist, # type: Distribution @@ -98,17 +102,6 @@ def _make_candidate_from_link( return ExtrasCandidate(base, extras) return base - def _get_installed_distribution(self, name, version): - # type: (str, str) -> Optional[Distribution] - if self._ignore_installed: - return None - specifier = "{}=={}".format(name, version) - try: - dist = get_distribution(specifier) - except (DistributionNotFound, VersionConflict): - return None - return dist - def make_candidate_from_ican( self, ican, # type: InstallationCandidate @@ -116,8 +109,8 @@ def make_candidate_from_ican( parent, # type: InstallRequirement ): # type: (...) -> Candidate - dist = self._get_installed_distribution(ican.name, ican.version) - if dist is None: + dist = self._installed_dists.get(ican.name) + if dist is None or dist.parsed_version != ican.version: return self._make_candidate_from_link( link=ican.link, extras=extras, From be60eaaa4f6e1b850abdde9a71ec3b100eb0f34a Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Mon, 6 Apr 2020 23:13:01 +0800 Subject: [PATCH 1577/3170] Uninstall existing dist before reinstallation --- .../_internal/resolution/resolvelib/factory.py | 14 +++++++++++++- .../_internal/resolution/resolvelib/resolver.py | 7 +++++-- tests/functional/test_new_resolver.py | 15 ++++++++------- tests/unit/resolution_resolvelib/conftest.py | 1 + 4 files changed, 27 insertions(+), 10 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index c07be80c632..30993d99553 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -43,6 +43,7 @@ def __init__( finder, # type: PackageFinder preparer, # type: RequirementPreparer make_install_req, # type: InstallRequirementProvider + force_reinstall, # type: bool ignore_installed, # type: bool ignore_requires_python, # type: bool py_version_info=None, # type: Optional[Tuple[int, ...]] @@ -52,6 +53,7 @@ def __init__( self.preparer = preparer self._python_candidate = RequiresPythonCandidate(py_version_info) self._make_install_req_from_spec = make_install_req + self._force_reinstall = force_reinstall self._ignore_requires_python = ignore_requires_python self._link_candidate_cache = {} # type: Cache[LinkCandidate] @@ -110,7 +112,12 @@ def make_candidate_from_ican( ): # type: (...) -> Candidate dist = self._installed_dists.get(ican.name) - if dist is None or dist.parsed_version != ican.version: + should_use_installed_dist = ( + not self._force_reinstall and + dist is not None and + dist.parsed_version == ican.version + ) + if not should_use_installed_dist: return self._make_candidate_from_link( link=ican.link, extras=extras, @@ -153,3 +160,8 @@ def make_requires_python_requirement(self, specifier): if self._python_candidate.version in specifier: return ExplicitRequirement(self._python_candidate) return NoMatchRequirement(self._python_candidate.name) + + def should_reinstall(self, candidate): + # type: (Candidate) -> bool + # TODO: Are there more cases this needs to return True? Editable? + return candidate.name in self._installed_dists diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 9a9eedee7c2..5e6408573c6 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -43,6 +43,7 @@ def __init__( finder=finder, preparer=preparer, make_install_req=make_install_req, + force_reinstall=force_reinstall, ignore_installed=ignore_installed, ignore_requires_python=ignore_requires_python, py_version_info=py_version_info, @@ -68,8 +69,10 @@ def resolve(self, root_reqs, check_supported_wheels): req_set = RequirementSet(check_supported_wheels=check_supported_wheels) for candidate in self._result.mapping.values(): ireq = provider.get_install_requirement(candidate) - if ireq is not None: - req_set.add_named_requirement(ireq) + if ireq is None: + continue + ireq.should_reinstall = self.factory.should_reinstall(candidate) + req_set.add_named_requirement(ireq) return req_set diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 28468dc23e1..25c726f2574 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -330,7 +330,6 @@ def test_new_resolver_only_builds_sdists_when_needed(script): assert_installed(script, base="0.1.0", dep="0.2.0") -@pytest.mark.xfail(reason="upgrade install not implemented") def test_new_resolver_install_different_version(script): create_basic_wheel_for_package(script, "base", "0.1.0") create_basic_wheel_for_package(script, "base", "0.2.0") @@ -358,7 +357,6 @@ def test_new_resolver_install_different_version(script): assert_installed(script, base="0.2.0") -@pytest.mark.xfail(reason="force reinstall not implemented") def test_new_resolver_force_reinstall(script): create_basic_wheel_for_package(script, "base", "0.1.0") @@ -366,19 +364,22 @@ def test_new_resolver_force_reinstall(script): "install", "--unstable-feature=resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, - "base", + "base==0.1.0", ) + # This should trigger an uninstallation of base due to --force-reinstall, + # even though the installed version matches. result = script.pip( "install", "--unstable-feature=resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "--force-reinstall", - "base", + "base==0.1.0", ) - assert "Uninstalling base-0.1.0" not in result.stdout, str(result) + assert "Uninstalling base-0.1.0" in result.stdout, str(result) + assert "Successfully uninstalled base-0.1.0" in result.stdout, str(result) assert script.site_packages / "base" in result.files_updated, ( - "base not upgraded" + "base not reinstalled" ) - assert_installed(script, base="0.2.0") + assert_installed(script, base="0.1.0") diff --git a/tests/unit/resolution_resolvelib/conftest.py b/tests/unit/resolution_resolvelib/conftest.py index 9cd233c8efb..f179aeade8c 100644 --- a/tests/unit/resolution_resolvelib/conftest.py +++ b/tests/unit/resolution_resolvelib/conftest.py @@ -52,6 +52,7 @@ def factory(finder, preparer): finder=finder, preparer=preparer, make_install_req=install_req_from_line, + force_reinstall=False, ignore_installed=False, ignore_requires_python=False, py_version_info=None, From a7d17f8da63ebfc1621e7731295c5d3e569f3076 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 8 Apr 2020 23:38:05 +0800 Subject: [PATCH 1578/3170] Make sure candidates are prepared after resolution --- src/pip/_internal/resolution/resolvelib/candidates.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 664c5858f02..2c35c3f6fa7 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -207,6 +207,7 @@ def get_dependencies(self): def get_install_requirement(self): # type: () -> Optional[InstallRequirement] + self.dist # HACK? Ensure the candidate is correctly prepared. return self._ireq From 56e065f956bd3af07d88d3ecf0e8eb8b6cf59e22 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 9 Apr 2020 02:27:37 +0800 Subject: [PATCH 1579/3170] Move dist preparation to its own method --- .../resolution/resolvelib/candidates.py | 65 ++++++++++--------- 1 file changed, 35 insertions(+), 30 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 2c35c3f6fa7..1a048eb10dc 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -143,39 +143,44 @@ def version(self): self._version = self.dist.parsed_version return self._version - def _get_abstract_distribution(self): + def _prepare_abstract_distribution(self): # type: () -> AbstractDistribution raise NotImplementedError("Override in subclass") + def _prepare(self): + # type: () -> None + if self._dist is not None: + return + + abstract_dist = self._prepare_abstract_distribution() + self._dist = abstract_dist.get_pkg_resources_distribution() + assert self._dist is not None, "Distribution already installed" + + # TODO: Abort cleanly here, as the resolution has been + # based on the wrong name/version until now, and + # so is wrong. + # TODO: (Longer term) Rather than abort, reject this candidate + # and backtrack. This would need resolvelib support. + # These should be "proper" errors, not just asserts, as they + # can result from user errors like a requirement "foo @ URL" + # when the project at URL has a name of "bar" in its metadata. + assert ( + self._name is None or + self._name == canonicalize_name(self._dist.project_name) + ), "Name mismatch: {!r} vs {!r}".format( + self._name, canonicalize_name(self._dist.project_name), + ) + assert ( + self._version is None or + self._version == self._dist.parsed_version + ), "Version mismatch: {!r} vs {!r}".format( + self._version, self._dist.parsed_version, + ) + @property def dist(self): # type: () -> Distribution - if self._dist is None: - abstract_dist = self._get_abstract_distribution() - self._dist = abstract_dist.get_pkg_resources_distribution() - # TODO: Only InstalledDistribution can return None here :-( - assert self._dist is not None - # TODO: Abort cleanly here, as the resolution has been - # based on the wrong name/version until now, and - # so is wrong. - # TODO: (Longer term) Rather than abort, reject this candidate - # and backtrack. This would need resolvelib support. - # These should be "proper" errors, not just asserts, as they - # can result from user errors like a requirement "foo @ URL" - # when the project at URL has a name of "bar" in its metadata. - assert ( - self._name is None or - self._name == canonicalize_name(self._dist.project_name) - ), "Name mismatch: {!r} vs {!r}".format( - self._name, canonicalize_name(self._dist.project_name), - ) - assert ( - self._version is None or - self._version == self._dist.parsed_version - ), "Version mismatch: {!r} vs {!r}".format( - self._version, self._dist.parsed_version, - ) - + self._prepare() return self._dist def _get_requires_python_specifier(self): @@ -207,7 +212,7 @@ def get_dependencies(self): def get_install_requirement(self): # type: () -> Optional[InstallRequirement] - self.dist # HACK? Ensure the candidate is correctly prepared. + self._prepare() return self._ireq @@ -229,7 +234,7 @@ def __init__( version=version, ) - def _get_abstract_distribution(self): + def _prepare_abstract_distribution(self): # type: () -> AbstractDistribution return self._factory.preparer.prepare_linked_requirement(self._ireq) @@ -252,7 +257,7 @@ def __init__( version=version, ) - def _get_abstract_distribution(self): + def _prepare_abstract_distribution(self): # type: () -> AbstractDistribution return self._factory.preparer.prepare_editable_requirement(self._ireq) From b438d47344dd72ccf3847eac4caa2c83d9b1b696 Mon Sep 17 00:00:00 2001 From: Tomas Hrnciar <thrnciar@redhat.com> Date: Wed, 1 Apr 2020 11:56:22 +0200 Subject: [PATCH 1580/3170] Remove shebang from nonexecutable script When packaging pip in Fedora, we have realised that there is a nonexecutable file with a shebang line. It seems that the primary purpose of this file is to be imported from Python code and hence the shebang appears to be unnecessary. Shebangs are hard to handle when doing downstream packaging because it makes sense for upstream to use `#!/usr/bin/env python` while in the RPM package, we need to avoid that and use a more specific interpreter. Since the shebang was unused, I propose to remove it to avoid the problems. We have found more shebangs but in vendored packages. I have also opened PRs there: https://github.com/ActiveState/appdirs/pull/144 https://github.com/psf/requests/pull/5410 https://github.com/chardet/chardet/pull/192 x --- news/7959.trivial | 17 +++++++++++++++++ src/pip/_internal/__init__.py | 1 - 2 files changed, 17 insertions(+), 1 deletion(-) create mode 100644 news/7959.trivial diff --git a/news/7959.trivial b/news/7959.trivial new file mode 100644 index 00000000000..dffd57a83ff --- /dev/null +++ b/news/7959.trivial @@ -0,0 +1,17 @@ +Removes shebang from nonexecutable script. + +When packaging pip in Fedora, we have realised +that there is a nonexecutable file with a shebang line. + +It seems that the primary purpose of this file is to be imported from Python +code and hence the shebang appears to be unnecessary. + +Shebangs are hard to handle when doing downstream packaging because it makes +sense for upstream to use ``#!/usr/bin/env python`` while in the RPM package, we +need to avoid that and use a more specific interpreter. Since the shebang was +unused, I propose to remove it to avoid the problems. + +We have found more shebangs but in vendored packages. I have also opened PRs there: +https://github.com/ActiveState/appdirs/pull/144 +https://github.com/psf/requests/pull/5410 +https://github.com/chardet/chardet/pull/192 diff --git a/src/pip/_internal/__init__.py b/src/pip/_internal/__init__.py index 3aa8a4693ff..264c2cab88d 100755 --- a/src/pip/_internal/__init__.py +++ b/src/pip/_internal/__init__.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python import pip._internal.utils.inject_securetransport # noqa from pip._internal.utils.typing import MYPY_CHECK_RUNNING From d5e45bb59acb0e908cabebedbffb3d273a3b240a Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 9 Apr 2020 20:28:28 +0800 Subject: [PATCH 1581/3170] Remove the version part from the dist-info stem --- src/pip/_internal/req/req_install.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 44c29d1b581..62099e5cb7d 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -70,17 +70,18 @@ def _get_dist(metadata_directory): """ dist_dir = metadata_directory.rstrip(os.sep) + # Build a PathMetadata object, from path to metadata. :wink: + base_dir, dist_dir_name = os.path.split(dist_dir) + metadata = pkg_resources.PathMetadata(base_dir, dist_dir) + # Determine the correct Distribution object type. if dist_dir.endswith(".egg-info"): dist_cls = pkg_resources.Distribution + dist_name = os.path.splitext(dist_dir_name)[0] else: assert dist_dir.endswith(".dist-info") dist_cls = pkg_resources.DistInfoDistribution - - # Build a PathMetadata object, from path to metadata. :wink: - base_dir, dist_dir_name = os.path.split(dist_dir) - dist_name = os.path.splitext(dist_dir_name)[0] - metadata = pkg_resources.PathMetadata(base_dir, dist_dir) + dist_name = os.path.splitext(dist_dir_name)[0].split("-")[0] return dist_cls( base_dir, From 917ad6aa0e78b79073948ba1ff583f90d179af76 Mon Sep 17 00:00:00 2001 From: Nikolay Korolev <CrafterKolyan@mail.ru> Date: Thu, 9 Apr 2020 17:19:54 +0300 Subject: [PATCH 1582/3170] Update news/7962.bugfix Co-Authored-By: Pradyun Gedam <pradyunsg@gmail.com> --- news/7962.bugfix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/7962.bugfix b/news/7962.bugfix index b1437031df7..4b8b0068096 100644 --- a/news/7962.bugfix +++ b/news/7962.bugfix @@ -1 +1 @@ -`pip list --outdated` version fetching is multi-threaded +Significantly speedup `pip list --outdated` through parallelizing index interaction. From b744854f442b0f56c802c793cf013d9773e1d620 Mon Sep 17 00:00:00 2001 From: Nikolay Korolev <CrafterKolyan@mail.ru> Date: Thu, 9 Apr 2020 17:57:10 +0300 Subject: [PATCH 1583/3170] Update news/7962.bugfix. Fix linter Co-Authored-By: Pradyun Gedam <pradyunsg@gmail.com> --- news/7962.bugfix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/7962.bugfix b/news/7962.bugfix index 4b8b0068096..76c3442d053 100644 --- a/news/7962.bugfix +++ b/news/7962.bugfix @@ -1 +1 @@ -Significantly speedup `pip list --outdated` through parallelizing index interaction. +Significantly speedup ``pip list --outdated`` through parallelizing index interaction. From cfac6aebdd10d1b992b689a4d17ee784b6ebc1ed Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 10 Apr 2020 19:53:18 +0800 Subject: [PATCH 1584/3170] Always return an install candidate last if matches This rewrites how a SpecifierRequirement generates candidates, so it * Always return an AlreadyInstalledCandidate (as long as the version satisfies the specifier), even if PackageFinder does not return a candidate for the same version. * Always put the AlreadyInstalledCandidate last, so it's preferred over LinkCandidate, preventing version changes if possible. --- .../resolution/resolvelib/factory.py | 53 +++++++++-------- .../resolution/resolvelib/requirements.py | 15 +---- tests/functional/test_new_resolver.py | 57 +++++++++++++++++++ 3 files changed, 89 insertions(+), 36 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 30993d99553..9367bed4ded 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -17,14 +17,13 @@ ) if MYPY_CHECK_RUNNING: - from typing import Dict, Optional, Set, Tuple, TypeVar + from typing import Dict, Iterator, Optional, Set, Tuple, TypeVar from pip._vendor.packaging.specifiers import SpecifierSet from pip._vendor.packaging.version import _BaseVersion from pip._vendor.pkg_resources import Distribution from pip._internal.index.package_finder import PackageFinder - from pip._internal.models.candidate import InstallationCandidate from pip._internal.models.link import Link from pip._internal.operations.prepare import RequirementPreparer from pip._internal.req.req_install import InstallRequirement @@ -88,6 +87,8 @@ def _make_candidate_from_link( version=None, # type: Optional[_BaseVersion] ): # type: (...) -> Candidate + # TODO: Check already installed candidate, and use it if the link and + # editable flag match. if parent.editable: if link not in self._editable_candidate_cache: self._editable_candidate_cache[link] = EditableCandidate( @@ -104,32 +105,38 @@ def _make_candidate_from_link( return ExtrasCandidate(base, extras) return base - def make_candidate_from_ican( - self, - ican, # type: InstallationCandidate - extras, # type: Set[str] - parent, # type: InstallRequirement - ): - # type: (...) -> Candidate - dist = self._installed_dists.get(ican.name) - should_use_installed_dist = ( - not self._force_reinstall and - dist is not None and - dist.parsed_version == ican.version + def iter_found_candidates(self, ireq, extras): + # type: (InstallRequirement, Set[str]) -> Iterator[Candidate] + name = canonicalize_name(ireq.req.name) + if not self._force_reinstall: + dist = self._installed_dists.get(name) + else: + dist = None + + found = self.finder.find_best_candidate( + project_name=ireq.req.name, + specifier=ireq.req.specifier, + hashes=ireq.hashes(trust_internet=False), ) - if not should_use_installed_dist: - return self._make_candidate_from_link( + for ican in found.iter_applicable(): + if dist is not None and dist.parsed_version == ican.version: + continue + yield self._make_candidate_from_link( link=ican.link, extras=extras, - parent=parent, - name=canonicalize_name(ican.name), + parent=ireq, + name=name, version=ican.version, ) - return self._make_candidate_from_dist( - dist=dist, - extras=extras, - parent=parent, - ) + + # Return installed distribution if it matches the specifier. This is + # done last so the resolver will prefer it over downloading links. + if dist is not None and dist.parsed_version in ireq.req.specifier: + yield self._make_candidate_from_dist( + dist=dist, + extras=extras, + parent=ireq, + ) def make_requirement_from_install_req(self, ireq): # type: (InstallRequirement) -> Requirement diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py index 027e36aa61e..cd413efac1d 100644 --- a/src/pip/_internal/resolution/resolvelib/requirements.py +++ b/src/pip/_internal/resolution/resolvelib/requirements.py @@ -94,19 +94,8 @@ def name(self): def find_matches(self): # type: () -> Sequence[Candidate] - found = self._factory.finder.find_best_candidate( - project_name=self._ireq.req.name, - specifier=self._ireq.req.specifier, - hashes=self._ireq.hashes(trust_internet=False), - ) - return [ - self._factory.make_candidate_from_ican( - ican=ican, - extras=self.extras, - parent=self._ireq, - ) - for ican in found.iter_applicable() - ] + it = self._factory.iter_found_candidates(self._ireq, self.extras) + return list(it) def is_satisfied_by(self, candidate): # type: (Candidate) -> bool diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 25c726f2574..5ee60cd14ab 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -88,6 +88,63 @@ def test_new_resolver_picks_latest_version(script): assert_installed(script, simple="0.2.0") +def test_new_resolver_picks_installed_version(script): + create_basic_wheel_for_package( + script, + "simple", + "0.1.0", + ) + create_basic_wheel_for_package( + script, + "simple", + "0.2.0", + ) + script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "simple==0.1.0" + ) + assert_installed(script, simple="0.1.0") + + result = script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "simple" + ) + assert "Collecting" not in result.stdout, "Should not fetch new version" + assert_installed(script, simple="0.1.0") + + +def test_new_resolver_picks_installed_version_if_no_match_found(script): + create_basic_wheel_for_package( + script, + "simple", + "0.1.0", + ) + create_basic_wheel_for_package( + script, + "simple", + "0.2.0", + ) + script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "simple==0.1.0" + ) + assert_installed(script, simple="0.1.0") + + result = script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "simple" + ) + assert "Collecting" not in result.stdout, "Should not fetch new version" + assert_installed(script, simple="0.1.0") + + def test_new_resolver_installs_dependencies(script): create_basic_wheel_for_package( script, From 528a193ddae326eb4ec6f63890d25897625b2a37 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Fri, 10 Apr 2020 14:43:33 +0530 Subject: [PATCH 1585/3170] Fix incorrect comment in tests --- news/22476270-1CF0-4D00-8621-E633D06AA53A.trivial | 0 tests/functional/test_install.py | 14 +++++++------- tests/functional/test_list.py | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) create mode 100644 news/22476270-1CF0-4D00-8621-E633D06AA53A.trivial diff --git a/news/22476270-1CF0-4D00-8621-E633D06AA53A.trivial b/news/22476270-1CF0-4D00-8621-E633D06AA53A.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 50fa2e81d85..2cb44269502 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1855,15 +1855,16 @@ def test_install_skip_work_dir_pkg(script, data): and an uninstall """ - # Create a test package and install it + # Create a test package, install it and then uninstall it pkg_path = create_test_package_with_setup( script, name='simple', version='1.0') script.pip('install', '-e', '.', expect_stderr=True, cwd=pkg_path) - # Uninstalling the package and installing it again will succeed script.pip('uninstall', 'simple', '-y') + # Running the install command again from the working directory + # will install the package as it was uninstalled earlier result = script.pip('install', '--find-links', data.find_links, 'simple', expect_stderr=True, cwd=pkg_path) @@ -1879,19 +1880,18 @@ def test_install_include_work_dir_pkg(script, data): if working directory is added in PYTHONPATH """ - # Create a test package and install it + # Create a test package, install it and then uninstall it pkg_path = create_test_package_with_setup( script, name='simple', version='1.0') script.pip('install', '-e', '.', expect_stderr=True, cwd=pkg_path) - - # Uninstall will fail with given warning script.pip('uninstall', 'simple', '-y') script.environ.update({'PYTHONPATH': pkg_path}) - # Uninstalling the package and installing it again will fail, - # when package directory is in PYTHONPATH + # Running the install command again from the working directory + # will be a no-op, as the package is found to be installed, + # when the package directory is in PYTHONPATH result = script.pip('install', '--find-links', data.find_links, 'simple', expect_stderr=True, cwd=pkg_path) diff --git a/tests/functional/test_list.py b/tests/functional/test_list.py index 41c46ad7ce3..37787246bd0 100644 --- a/tests/functional/test_list.py +++ b/tests/functional/test_list.py @@ -575,10 +575,10 @@ def test_list_include_work_dir_pkg(script): script.run('python', 'setup.py', 'egg_info', expect_stderr=True, cwd=pkg_path) - # Add PYTHONPATH env variable script.environ.update({'PYTHONPATH': pkg_path}) # List should include package simple when run from package directory + # when the package directory is in PYTHONPATH result = script.pip('list', '--format=json', cwd=pkg_path) json_result = json.loads(result.stdout) assert {'name': 'simple', 'version': '1.0'} in json_result From 72204f2640c5e6f039b7ed57b073686487067acf Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 10 Apr 2020 22:11:15 +0800 Subject: [PATCH 1586/3170] Upgrade ResolveLib vendoring definition --- src/pip/_vendor/vendor.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 401a4269213..edb504b54f4 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -22,4 +22,4 @@ setuptools==44.0.0 six==1.14.0 webencodings==0.5.1 -git+https://github.com/sarugaku/resolvelib.git@fbc8bb28d6cff98b2#egg=resolvelib +git+https://github.com/sarugaku/resolvelib.git@726834de469bfcb8b#egg=resolvelib From 4e74a735d268408b8e84d104fc7a72818c41e6ec Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 10 Apr 2020 22:39:29 +0800 Subject: [PATCH 1587/3170] Run vendoring --- src/pip/_vendor/resolvelib/__init__.py | 2 + src/pip/_vendor/resolvelib/reporters.py | 12 +++++ src/pip/_vendor/resolvelib/resolvers.py | 72 +++++++++++++++++++------ 3 files changed, 69 insertions(+), 17 deletions(-) diff --git a/src/pip/_vendor/resolvelib/__init__.py b/src/pip/_vendor/resolvelib/__init__.py index b663b0c2902..ee6793a403c 100644 --- a/src/pip/_vendor/resolvelib/__init__.py +++ b/src/pip/_vendor/resolvelib/__init__.py @@ -3,6 +3,7 @@ "AbstractProvider", "AbstractResolver", "BaseReporter", + "InconsistentCandidate", "Resolver", "RequirementsConflicted", "ResolutionError", @@ -16,6 +17,7 @@ from .providers import AbstractProvider, AbstractResolver from .reporters import BaseReporter from .resolvers import ( + InconsistentCandidate, RequirementsConflicted, Resolver, ResolutionError, diff --git a/src/pip/_vendor/resolvelib/reporters.py b/src/pip/_vendor/resolvelib/reporters.py index 5bcaf4d8268..c7e9e88b832 100644 --- a/src/pip/_vendor/resolvelib/reporters.py +++ b/src/pip/_vendor/resolvelib/reporters.py @@ -22,3 +22,15 @@ def ending_round(self, index, state): def ending(self, state): """Called before the resolution ends successfully. """ + + def adding_requirement(self, requirement): + """Called when the resolver adds a new requirement into the resolve criteria. + """ + + def backtracking(self, candidate): + """Called when the resolver rejects a candidate during backtracking. + """ + + def pinning(self, candidate): + """Called when adding a candidate to the potential solution. + """ diff --git a/src/pip/_vendor/resolvelib/resolvers.py b/src/pip/_vendor/resolvelib/resolvers.py index 45e7e7d3130..b51d337d231 100644 --- a/src/pip/_vendor/resolvelib/resolvers.py +++ b/src/pip/_vendor/resolvelib/resolvers.py @@ -22,6 +22,24 @@ def __init__(self, criterion): super(RequirementsConflicted, self).__init__(criterion) self.criterion = criterion + def __str__(self): + return "Requirements conflict: {}".format( + ", ".join(repr(r) for r in self.criterion.iter_requirement()), + ) + + +class InconsistentCandidate(ResolverException): + def __init__(self, candidate, criterion): + super(InconsistentCandidate, self).__init__(candidate, criterion) + self.candidate = candidate + self.criterion = criterion + + def __str__(self): + return "Provided candidate {!r} does not satisfy {}".format( + self.candidate, + ", ".join(repr(r) for r in self.criterion.iter_requirement()), + ) + class Criterion(object): """Representation of possible resolution results of a package. @@ -48,6 +66,13 @@ def __init__(self, candidates, information, incompatibilities): self.information = information self.incompatibilities = incompatibilities + def __repr__(self): + requirements = ", ".join( + "{!r} from {!r}".format(req, parent) + for req, parent in self.information + ) + return "<Criterion {}>".format(requirements) + @classmethod def from_requirement(cls, provider, requirement, parent): """Build an instance from a requirement. @@ -85,13 +110,15 @@ def merged_with(self, provider, requirement, parent): def excluded_of(self, candidate): """Build a new instance from this, but excluding specified candidate. + + Returns the new instance, or None if we still have no valid candidates. """ incompats = list(self.incompatibilities) incompats.append(candidate) candidates = [c for c in self.candidates if c != candidate] - criterion = type(self)(candidates, list(self.information), incompats) if not candidates: - raise RequirementsConflicted(criterion) + return None + criterion = type(self)(candidates, list(self.information), incompats) return criterion @@ -100,9 +127,10 @@ class ResolutionError(ResolverException): class ResolutionImpossible(ResolutionError): - def __init__(self, requirements): - super(ResolutionImpossible, self).__init__(requirements) - self.requirements = requirements + def __init__(self, causes): + super(ResolutionImpossible, self).__init__(causes) + # causes is a list of RequirementInformation objects + self.causes = causes class ResolutionTooDeep(ResolutionError): @@ -151,6 +179,7 @@ def _push_new_state(self): self._states.append(state) def _merge_into_criterion(self, requirement, parent): + self._r.adding_requirement(requirement) name = self._p.identify(requirement) try: crit = self.state.criteria[name] @@ -195,11 +224,21 @@ def _attempt_to_pin_criterion(self, name, criterion): except RequirementsConflicted as e: causes.append(e.criterion) continue + # Put newly-pinned candidate at the end. This is essential because # backtracking looks at this mapping to get the last pin. + self._r.pinning(candidate) self.state.mapping.pop(name, None) self.state.mapping[name] = candidate self.state.criteria.update(criteria) + + # Check the newly-pinned candidate actually works. This should + # always pass under normal circumstances, but in the case of a + # faulty provider, we will raise an error to notify the implementer + # to fix find_matches() and/or is_satisfied_by(). + if not self._is_current_pin_satisfying(name, criterion): + raise InconsistentCandidate(candidate, criterion) + return [] # All candidates tried, nothing works. This criterion is a dead @@ -217,12 +256,12 @@ def _backtrack(self): # Retract the last candidate pin, and create a new (b). name, candidate = self._states.pop().mapping.popitem() + self._r.backtracking(candidate) self._push_new_state() - try: - # Mark the retracted candidate as incompatible. - criterion = self.state.criteria[name].excluded_of(candidate) - except RequirementsConflicted: + # Mark the retracted candidate as incompatible. + criterion = self.state.criteria[name].excluded_of(candidate) + if criterion is None: # This state still does not work. Try the still previous state. continue self.state.criteria[name] = criterion @@ -240,8 +279,7 @@ def resolve(self, requirements, max_rounds): try: name, crit = self._merge_into_criterion(r, parent=None) except RequirementsConflicted as e: - # If initial requirements conflict, nothing would ever work. - raise ResolutionImpossible(e.requirements + [r]) + raise ResolutionImpossible(e.criterion.information) self.state.criteria[name] = crit self._r.starting() @@ -275,12 +313,10 @@ def resolve(self, requirements, max_rounds): if failure_causes: result = self._backtrack() if not result: - requirements = [ - requirement - for crit in failure_causes - for requirement in crit.iter_requirement() + causes = [ + i for crit in failure_causes for i in crit.information ] - raise ResolutionImpossible(requirements) + raise ResolutionImpossible(causes) self._r.ending_round(round_index, curr) @@ -365,7 +401,9 @@ def resolve(self, requirements, max_rounds=100): The following exceptions may be raised if a resolution cannot be found: * `ResolutionImpossible`: A resolution cannot be found for the given - combination of requirements. + combination of requirements. The `causes` attribute of the + exception is a list of (requirement, parent), giving the + requirements that could not be satisfied. * `ResolutionTooDeep`: The dependency tree is too deeply nested and the resolver gave up. This is usually caused by a circular dependency, but you can try to resolve this by increasing the From 3764736f20ef584ae03cfb2e2510ad087031652a Mon Sep 17 00:00:00 2001 From: KOLANICH <KOLANICH@users.noreply.github.com> Date: Fri, 10 Apr 2020 14:46:21 +0000 Subject: [PATCH 1588/3170] Explicitly specify build-backend in pyproject.toml (#7742) --- news/7740.trivial | 1 + pyproject.toml | 1 + 2 files changed, 2 insertions(+) create mode 100644 news/7740.trivial diff --git a/news/7740.trivial b/news/7740.trivial new file mode 100644 index 00000000000..cb11d514991 --- /dev/null +++ b/news/7740.trivial @@ -0,0 +1 @@ +Use PEP 517 layout by specifying ``build-backend``, so that ``pip`` can be built with tools (such as ``pep517``) that don't support the legacy layout. diff --git a/pyproject.toml b/pyproject.toml index b45b527079c..04f7258064e 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,5 +1,6 @@ [build-system] requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" [tool.towncrier] package = "pip" From d2028e9538fc58768db6170a56fd6cbb90201c94 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 10 Apr 2020 22:56:53 +0800 Subject: [PATCH 1589/3170] Implement RequiresPythonRequirement with context This specialized class is able to carry more context information than the previous implementation (which reuses ExplicitRequirement). Error reports can thus provide better messages by introspecting. --- .../resolution/resolvelib/factory.py | 43 +++++++++--- .../resolution/resolvelib/requirements.py | 65 ++++++++++--------- .../resolution/resolvelib/resolver.py | 12 +++- tests/functional/test_new_resolver.py | 23 +++++++ 4 files changed, 101 insertions(+), 42 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 30993d99553..8a96ee20f0c 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -1,5 +1,9 @@ from pip._vendor.packaging.utils import canonicalize_name +from pip._internal.exceptions import ( + InstallationError, + UnsupportedPythonVersion, +) from pip._internal.utils.misc import get_installed_distributions from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -12,7 +16,7 @@ ) from .requirements import ( ExplicitRequirement, - NoMatchRequirement, + RequiresPythonRequirement, SpecifierRequirement, ) @@ -22,6 +26,7 @@ from pip._vendor.packaging.specifiers import SpecifierSet from pip._vendor.packaging.version import _BaseVersion from pip._vendor.pkg_resources import Distribution + from pip._vendor.resolvelib import ResolutionImpossible from pip._internal.index.package_finder import PackageFinder from pip._internal.models.candidate import InstallationCandidate @@ -152,16 +157,36 @@ def make_requires_python_requirement(self, specifier): # type: (Optional[SpecifierSet]) -> Optional[Requirement] if self._ignore_requires_python or specifier is None: return None - # The logic here is different from SpecifierRequirement, for which we - # "find" candidates matching the specifier. But for Requires-Python, - # there is always exactly one candidate (the one specified with - # py_version_info). Here we decide whether to return that based on - # whether Requires-Python matches that one candidate or not. - if self._python_candidate.version in specifier: - return ExplicitRequirement(self._python_candidate) - return NoMatchRequirement(self._python_candidate.name) + return RequiresPythonRequirement(specifier, self._python_candidate) def should_reinstall(self, candidate): # type: (Candidate) -> bool # TODO: Are there more cases this needs to return True? Editable? return candidate.name in self._installed_dists + + def _report_requires_python_error( + self, + requirement, # type: RequiresPythonRequirement + parent, # type: Candidate + ): + # type: (...) -> UnsupportedPythonVersion + template = ( + "Package {package!r} requires a different Python: " + "{version} not in {specifier!r}" + ) + message = template.format( + package=parent.name, + version=self._python_candidate.version, + specifier=str(requirement.specifier), + ) + return UnsupportedPythonVersion(message) + + def get_installation_error(self, e): + # type: (ResolutionImpossible) -> Optional[InstallationError] + for cause in e.causes: + if isinstance(cause.requirement, RequiresPythonRequirement): + return self._report_requires_python_error( + cause.requirement, + cause.parent, + ) + return None diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py index 027e36aa61e..a7f0ec31388 100644 --- a/src/pip/_internal/resolution/resolvelib/requirements.py +++ b/src/pip/_internal/resolution/resolvelib/requirements.py @@ -7,6 +7,8 @@ if MYPY_CHECK_RUNNING: from typing import Sequence + from pip._vendor.packaging.specifiers import SpecifierSet + from pip._internal.req.req_install import InstallRequirement from .base import Candidate @@ -40,37 +42,6 @@ def is_satisfied_by(self, candidate): return candidate == self.candidate -class NoMatchRequirement(Requirement): - """A requirement that never matches anything. - - Note: Similar to ExplicitRequirement, the caller should handle name - canonicalisation; this class does not perform it. - """ - def __init__(self, name): - # type: (str) -> None - self._name = name - - def __repr__(self): - # type: () -> str - return "{class_name}(name={name!r})".format( - class_name=self.__class__.__name__, - name=self._name, - ) - - @property - def name(self): - # type: () -> str - return self._name - - def find_matches(self): - # type: () -> Sequence[Candidate] - return [] - - def is_satisfied_by(self, candidate): - # type: (Candidate) -> bool - return False - - class SpecifierRequirement(Requirement): def __init__(self, ireq, factory): # type: (InstallRequirement, Factory) -> None @@ -114,3 +85,35 @@ def is_satisfied_by(self, candidate): "Internal issue: Candidate is not for this requirement " \ " {} vs {}".format(candidate.name, self.name) return candidate.version in self._ireq.req.specifier + + +class RequiresPythonRequirement(Requirement): + """A requirement representing Requires-Python metadata. + """ + def __init__(self, specifier, match): + # type: (SpecifierSet, Candidate) -> None + self.specifier = specifier + self._candidate = match + + def __repr__(self): + # type: () -> str + return "{class_name}({specifier!r})".format( + class_name=self.__class__.__name__, + specifier=str(self.specifier), + ) + + @property + def name(self): + # type: () -> str + return self._candidate.name + + def find_matches(self): + # type: () -> Sequence[Candidate] + if self._candidate.version in self.specifier: + return [self._candidate] + return [] + + def is_satisfied_by(self, candidate): + # type: (Candidate) -> bool + assert candidate.name == self._candidate.name, "Not Python candidate" + return candidate.version in self.specifier diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 5e6408573c6..a8006ef6a14 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -1,7 +1,8 @@ import functools +from pip._vendor import six from pip._vendor.packaging.utils import canonicalize_name -from pip._vendor.resolvelib import BaseReporter +from pip._vendor.resolvelib import BaseReporter, ResolutionImpossible from pip._vendor.resolvelib import Resolver as RLResolver from pip._internal.req.req_set import RequirementSet @@ -64,7 +65,14 @@ def resolve(self, root_reqs, check_supported_wheels): self.factory.make_requirement_from_install_req(r) for r in root_reqs ] - self._result = resolver.resolve(requirements) + + try: + self._result = resolver.resolve(requirements) + except ResolutionImpossible as e: + error = self.factory.get_installation_error(e) + if not error: + raise + six.raise_from(error, e) req_set = RequirementSet(check_supported_wheels=check_supported_wheels) for candidate in self._result.mapping.values(): diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 25c726f2574..6b984704deb 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -1,5 +1,6 @@ import json import os +import sys import pytest @@ -230,6 +231,28 @@ def test_new_resolver_requires_python( assert_installed(script, base="0.1.0", dep=dep_version) +def test_new_resolver_requires_python_error(script): + create_basic_wheel_for_package( + script, + "base", + "0.1.0", + requires_python="<2", + ) + result = script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "base", + expect_error=True, + ) + + message = ( + "Package 'base' requires a different Python: " + "{}.{}.{} not in '<2'".format(*sys.version_info[:3]) + ) + assert message in result.stderr, str(result) + + def test_new_resolver_installed(script): create_basic_wheel_for_package( script, From f19f8875ca3ea0163a11ffe75fccfc91074d39db Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Fri, 10 Apr 2020 22:43:18 +0530 Subject: [PATCH 1590/3170] Create FUNDING.yml --- .github/FUNDING.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 .github/FUNDING.yml diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml new file mode 100644 index 00000000000..4fe24ff7ae2 --- /dev/null +++ b/.github/FUNDING.yml @@ -0,0 +1,10 @@ +github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2] +patreon: # Replace with a single Patreon username +open_collective: # Replace with a single Open Collective username +ko_fi: # Replace with a single Ko-fi username +tidelift: # Replace with a single Tidelift platform-name/package-name e.g., npm/babel +community_bridge: # Replace with a single Community Bridge project-name e.g., cloud-foundry +liberapay: # Replace with a single Liberapay username +issuehunt: # Replace with a single IssueHunt username +otechie: # Replace with a single Otechie username +custom: https://pypi.org/sponsor # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] From 48dcfdf7d041d46ea195ee07d388d754b5b655ff Mon Sep 17 00:00:00 2001 From: Noah Gorny <noah.bar.ilan@gmail.com> Date: Mon, 23 Mar 2020 14:36:33 +0200 Subject: [PATCH 1591/3170] setup: Save vendor.txt as package data as well --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 7dab1fd45b2..9dfe108c0a4 100644 --- a/setup.py +++ b/setup.py @@ -67,6 +67,7 @@ def get_version(rel_path): exclude=["contrib", "docs", "tests*", "tasks"], ), package_data={ + "pip._vendor": ["vendor.txt"], "pip._vendor.certifi": ["*.pem"], "pip._vendor.requests": ["*.pem"], "pip._vendor.distlib._backport": ["sysconfig.cfg"], From 972ef6f970e9eca10319fbfac424a09a1df326b5 Mon Sep 17 00:00:00 2001 From: Noah Gorny <noah.bar.ilan@gmail.com> Date: Tue, 24 Mar 2020 02:09:59 +0200 Subject: [PATCH 1592/3170] commands: debug: Print pip._vendor.DEBUNDLED info --- src/pip/_internal/commands/debug.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pip/_internal/commands/debug.py b/src/pip/_internal/commands/debug.py index fe93b3a3926..9cd29e394e0 100644 --- a/src/pip/_internal/commands/debug.py +++ b/src/pip/_internal/commands/debug.py @@ -8,6 +8,7 @@ import os import sys +import pip._vendor from pip._vendor.certifi import where from pip._internal.cli import cmdoptions @@ -136,6 +137,7 @@ def run(self, options, args): show_value("REQUESTS_CA_BUNDLE", os.environ.get('REQUESTS_CA_BUNDLE')) show_value("CURL_CA_BUNDLE", os.environ.get('CURL_CA_BUNDLE')) show_value("pip._vendor.certifi.where()", where()) + show_value("pip._vendor.DEBUNDLED", pip._vendor.DEBUNDLED) show_tags(options) From 3802b8045df74ad5ffde0e9e6d7f4545d3ea89dc Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sat, 11 Apr 2020 03:18:12 +0530 Subject: [PATCH 1593/3170] Avoid a redirect Co-Authored-By: Xavier Fernandez <xav.fernandez@gmail.com> --- .github/FUNDING.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml index 4fe24ff7ae2..1bbd55808d4 100644 --- a/.github/FUNDING.yml +++ b/.github/FUNDING.yml @@ -7,4 +7,4 @@ community_bridge: # Replace with a single Community Bridge project-name e.g., cl liberapay: # Replace with a single Liberapay username issuehunt: # Replace with a single IssueHunt username otechie: # Replace with a single Otechie username -custom: https://pypi.org/sponsor # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] +custom: "https://pypi.org/sponsor/" # Replace with up to 4 custom sponsorship URLs e.g., ['link1', 'link2'] From e131c825d46eba1d1864c0314da6f6ac0b894aaf Mon Sep 17 00:00:00 2001 From: Noah Gorny <noah.bar.ilan@gmail.com> Date: Thu, 9 Apr 2020 02:41:54 +0300 Subject: [PATCH 1594/3170] commands: debug: Add vendor library versions --- news/7794.trivial | 1 + src/pip/_internal/commands/debug.py | 94 ++++++++++++++++++++++++++++- 2 files changed, 94 insertions(+), 1 deletion(-) create mode 100644 news/7794.trivial diff --git a/news/7794.trivial b/news/7794.trivial new file mode 100644 index 00000000000..f4a5a1d4275 --- /dev/null +++ b/news/7794.trivial @@ -0,0 +1 @@ +Print vendored libraries version in pip debug. diff --git a/src/pip/_internal/commands/debug.py b/src/pip/_internal/commands/debug.py index 9cd29e394e0..05ff1c54e64 100644 --- a/src/pip/_internal/commands/debug.py +++ b/src/pip/_internal/commands/debug.py @@ -9,8 +9,10 @@ import sys import pip._vendor +from pip._vendor import pkg_resources from pip._vendor.certifi import where +from pip import __file__ as pip_location from pip._internal.cli import cmdoptions from pip._internal.cli.base_command import Command from pip._internal.cli.cmdoptions import make_target_python @@ -20,7 +22,8 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Any, List, Optional + from types import ModuleType + from typing import Any, List, Optional, Dict from optparse import Values logger = logging.getLogger(__name__) @@ -44,6 +47,93 @@ def show_sys_implementation(): show_value('name', implementation_name) +def create_vendor_txt_map(): + # type: () -> Dict[str, str] + vendor_txt_path = os.path.join( + os.path.dirname(pip_location), + '_vendor', + 'vendor.txt' + ) + + with open(vendor_txt_path) as f: + # Purge non version specifying lines. + # Also, remove any space prefix or suffixes (including comments). + lines = [line.strip().split(' ', 1)[0] + for line in f.readlines() if '==' in line] + + # Transform into "module" -> version dict. + return dict(line.split('==', 1) for line in lines) # type: ignore + + +def get_module_from_module_name(module_name): + # type: (str) -> ModuleType + + # Module name can be uppercase in vendor.txt for some reason... + module_name = module_name.lower() + # PATCH: setuptools is actually only pkg_resources. + if module_name == 'setuptools': + module_name = 'pkg_resources' + + __import__( + 'pip._vendor.{}'.format(module_name), + globals(), + locals(), + level=0 + ) + return getattr(pip._vendor, module_name) + + +def get_vendor_version_from_module(module_name): + # type: (str) -> str + + module = get_module_from_module_name(module_name) + version = getattr(module, '__version__', None) + + if not version: + # Try to find version in debundled module info + pkg_set = pkg_resources.WorkingSet( + [os.path.dirname(getattr(module, '__file__'))] + ) + package = pkg_set.find(pkg_resources.Requirement.parse(module_name)) + version = getattr(package, 'version', None) + + return version + + +def show_actual_vendor_versions(vendor_txt_versions): + # type: (Dict[str, str]) -> None + # Logs the actual version and print extra info + # if there is a conflict or if the actual version could not be imported. + + for module_name, expected_version in vendor_txt_versions.items(): + extra_message = '' + actual_version = get_vendor_version_from_module(module_name) + if not actual_version: + extra_message = ' (Unable to locate actual module version, using'\ + ' vendor.txt specified version)' + actual_version = expected_version + elif actual_version != expected_version: + extra_message = ' (CONFLICT: vendor.txt suggests version should'\ + ' be {})'.format(expected_version) + + logger.info( + '{name}=={actual}{extra}'.format( + name=module_name, + actual=actual_version, + extra=extra_message + ) + ) + + +def show_vendor_versions(): + # type: () -> None + logger.info('vendored library versions:') + + vendor_txt_versions = create_vendor_txt_map() + with indent_log(): + show_actual_vendor_versions(vendor_txt_versions) + + def show_tags(options): # type: (Values) -> None tag_limit = 10 @@ -139,6 +229,8 @@ def run(self, options, args): show_value("pip._vendor.certifi.where()", where()) show_value("pip._vendor.DEBUNDLED", pip._vendor.DEBUNDLED) + show_vendor_versions() + show_tags(options) return SUCCESS From 4efd8af14ba403407175197873a3720f22058c6a Mon Sep 17 00:00:00 2001 From: Noah Gorny <noah.bar.ilan@gmail.com> Date: Mon, 23 Mar 2020 19:54:26 +0200 Subject: [PATCH 1595/3170] tests: functional: Add new cases to the simple test_debug --- tests/functional/test_debug.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/functional/test_debug.py b/tests/functional/test_debug.py index 29cc3429522..387edc77063 100644 --- a/tests/functional/test_debug.py +++ b/tests/functional/test_debug.py @@ -14,6 +14,8 @@ 'REQUESTS_CA_BUNDLE: ', 'CURL_CA_BUNDLE: ', 'pip._vendor.certifi.where(): ', + 'pip._vendor.DEBUNDLED: ', + 'vendored library versions:', ]) def test_debug(script, expected_text): From bfd17cbd97c86f3fdc51ef50f20cbf45f3f8f719 Mon Sep 17 00:00:00 2001 From: Noah Gorny <noah.bar.ilan@gmail.com> Date: Wed, 25 Mar 2020 17:47:45 +0200 Subject: [PATCH 1596/3170] tests: functional: Add new test_debug vendored version test --- tests/functional/test_debug.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/functional/test_debug.py b/tests/functional/test_debug.py index 387edc77063..cf7f71729c1 100644 --- a/tests/functional/test_debug.py +++ b/tests/functional/test_debug.py @@ -1,5 +1,6 @@ import pytest +from pip._internal.commands.debug import create_vendor_txt_map from pip._internal.utils import compatibility_tags @@ -29,6 +30,18 @@ def test_debug(script, expected_text): assert expected_text in stdout +def test_debug__library_versions(script): + """ + Check the library versions normal output. + """ + args = ['debug'] + result = script.pip(*args, allow_stderr_warning=True) + stdout = result.stdout + vendored_versions = create_vendor_txt_map() + for name, value in vendored_versions.items(): + assert '{}=={}'.format(name, value) in stdout + + @pytest.mark.parametrize( 'args', [ From 74003e60ea74d981a65960b4e3b62e6dd03383f9 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sat, 11 Apr 2020 11:16:00 +0800 Subject: [PATCH 1597/3170] Upgrade ResolveLib vendoring to 0.3.0 --- src/pip/_vendor/vendor.txt | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index edb504b54f4..c590867ab6b 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -17,9 +17,8 @@ requests==2.22.0 chardet==3.0.4 idna==2.8 urllib3==1.25.7 +resolvelib==0.3.0 retrying==1.3.3 setuptools==44.0.0 six==1.14.0 webencodings==0.5.1 - -git+https://github.com/sarugaku/resolvelib.git@726834de469bfcb8b#egg=resolvelib From 7f8bf5e677da5131110ed6cc0eb7a157d39a1db1 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sat, 11 Apr 2020 11:17:24 +0800 Subject: [PATCH 1598/3170] Run vedoring --- src/pip/_vendor/resolvelib/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_vendor/resolvelib/__init__.py b/src/pip/_vendor/resolvelib/__init__.py index ee6793a403c..aaba5b3a120 100644 --- a/src/pip/_vendor/resolvelib/__init__.py +++ b/src/pip/_vendor/resolvelib/__init__.py @@ -11,7 +11,7 @@ "ResolutionTooDeep", ] -__version__ = "0.2.3.dev0" +__version__ = "0.3.0" from .providers import AbstractProvider, AbstractResolver From de633cdf4b263100e6ea2103bc9794ab827231c8 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Fri, 10 Apr 2020 01:04:39 +0530 Subject: [PATCH 1599/3170] Significantly improve release version validation --- noxfile.py | 8 ++--- tools/automation/release/__init__.py | 25 ++++++------- tools/automation/release/check_version.py | 43 +++++++++++++++++++++++ 3 files changed, 60 insertions(+), 16 deletions(-) create mode 100644 tools/automation/release/check_version.py diff --git a/noxfile.py b/noxfile.py index 8dc7abd40ad..2e4ad6fda44 100644 --- a/noxfile.py +++ b/noxfile.py @@ -155,9 +155,9 @@ def lint(session): # ----------------------------------------------------------------------------- @nox.session(name="prepare-release") def prepare_release(session): - version = release.get_version_from_arguments(session.posargs) + version = release.get_version_from_arguments(session) if not version: - session.error("Usage: nox -s prepare-release -- YY.N[.P]") + session.error("Usage: nox -s prepare-release -- <version>") session.log("# Ensure nothing is staged") if release.modified_files_in_git("--staged"): @@ -190,7 +190,7 @@ def prepare_release(session): @nox.session(name="build-release") def build_release(session): - version = release.get_version_from_arguments(session.posargs) + version = release.get_version_from_arguments(session) if not version: session.error("Usage: nox -s build-release -- YY.N[.P]") @@ -249,7 +249,7 @@ def build_dists(session): @nox.session(name="upload-release") def upload_release(session): - version = release.get_version_from_arguments(session.posargs) + version = release.get_version_from_arguments(session) if not version: session.error("Usage: nox -s upload-release -- YY.N[.P]") diff --git a/tools/automation/release/__init__.py b/tools/automation/release/__init__.py index a6138686289..9728b3613fe 100644 --- a/tools/automation/release/__init__.py +++ b/tools/automation/release/__init__.py @@ -14,24 +14,25 @@ from nox.sessions import Session -def get_version_from_arguments(arguments: List[str]) -> Optional[str]: +def get_version_from_arguments(session: Session) -> Optional[str]: """Checks the arguments passed to `nox -s release`. If there is only 1 argument that looks like a pip version, returns that. Otherwise, returns None. """ - if len(arguments) != 1: + if len(session.posargs) != 1: return None - - version = arguments[0] - - parts = version.split('.') - if not 2 <= len(parts) <= 3: - # Not of the form: YY.N or YY.N.P - return None - - if not all(part.isdigit() for part in parts): - # Not all segments are integers. + version = session.posargs[0] + + # We delegate to a script here, so that it can depend on packaging. + session.install("packaging") + cmd = [ + os.path.join(session.bin, "python"), + "tools/automation/release/check_version.py", + version + ] + not_ok = subprocess.run(cmd).returncode + if not_ok: return None # All is good. diff --git a/tools/automation/release/check_version.py b/tools/automation/release/check_version.py new file mode 100644 index 00000000000..db02e7aef3f --- /dev/null +++ b/tools/automation/release/check_version.py @@ -0,0 +1,43 @@ +"""Checks if the version is acceptable, as per this project's release process. +""" + +import sys +from datetime import datetime +from typing import Optional + +from packaging.version import InvalidVersion, Version + + +def is_this_a_good_version_number(string: str) -> Optional[str]: + try: + v = Version(string) + except InvalidVersion as e: + return str(e) + + if v.local: + return "Nope. PyPI refuses local release versions." + + if v.dev: + return "No development releases on PyPI. What are you even thinking?" + + if v.is_prerelease and v.pre[0] != "b": + return "Only beta releases are allowed. No alphas." + + release = v.release + expected_major = datetime.now().year % 100 + + if len(release) not in [2, 3]: + return "Not of the form: {0}.N or {0}.N.P".format(expected_major) + + return None + + +def main() -> None: + problem = is_this_a_good_version_number(sys.argv[1]) + if problem is not None: + print("ERROR:", problem) + sys.exit(1) + + +if __name__ == "__main__": + main() From 58295d0df72d65154e2b4035883bd4cbd24fdafc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sat, 11 Apr 2020 17:02:37 +0200 Subject: [PATCH 1600/3170] Remove InstallRequirement source_dir parameter source_dir is only passed to the InstallRequirement constructor in the case of editable requirements, and it is built from link, which is also passed to the same constructor. So we let InstallRequirement compute source_dir, to remove that burden from call sites. --- src/pip/_internal/req/constructors.py | 3 --- src/pip/_internal/req/req_install.py | 19 ++++++++++++++----- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index 7e57f22f73b..c9f1fe71396 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -228,12 +228,9 @@ def install_req_from_editable( parts = parse_req_from_editable(editable_req) - source_dir = parts.link.file_path if parts.link.scheme == 'file' else None - return InstallRequirement( parts.requirement, comes_from=comes_from, - source_dir=source_dir, editable=True, link=parts.link, constraint=constraint, diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 16a85708fda..3b28209b1bd 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -101,7 +101,6 @@ def __init__( self, req, # type: Optional[Requirement] comes_from, # type: Optional[Union[str, InstallRequirement]] - source_dir=None, # type: Optional[str] editable=False, # type: bool link=None, # type: Optional[Link] markers=None, # type: Optional[Marker] @@ -118,17 +117,27 @@ def __init__( self.req = req self.comes_from = comes_from self.constraint = constraint - if source_dir is None: - self.source_dir = None # type: Optional[str] - else: - self.source_dir = os.path.normpath(os.path.abspath(source_dir)) self.editable = editable + # source_dir is the local directory where the linked requirement is + # located, or unpacked. In case unpacking is needed, creating and + # populating source_dir is done by the RequirementPreparer. Note this + # is not necessarily the directory where pyproject.toml or setup.py is + # located - that one is obtained via unpacked_source_directory. + self.source_dir = None # type: Optional[str] + if self.editable: + assert link + if link.is_file: + self.source_dir = os.path.normpath( + os.path.abspath(link.file_path) + ) + if link is None and req and req.url: # PEP 508 URL requirement link = Link(req.url) self.link = self.original_link = link self.original_link_is_in_wheel_cache = False + # Path to any downloaded or already-existing package. self.local_file_path = None # type: Optional[str] if self.link and self.link.is_file: From 076d1a8ed82372b10035ed3debab5bcca1029fea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sat, 11 Apr 2020 18:04:26 +0200 Subject: [PATCH 1601/3170] Fix tests that build fake InstallRequirements with source_dir --- tests/functional/test_pep517.py | 9 ++++++--- tests/unit/test_pep517.py | 9 ++++++--- tests/unit/test_req.py | 2 +- 3 files changed, 13 insertions(+), 7 deletions(-) diff --git a/tests/functional/test_pep517.py b/tests/functional/test_pep517.py index 0286fe1f034..4e2dffb2c8b 100644 --- a/tests/functional/test_pep517.py +++ b/tests/functional/test_pep517.py @@ -22,7 +22,8 @@ def make_project(tmpdir, requires=[], backend=None, backend_path=None): def test_backend(tmpdir, data): """Check we can call a requirement's backend successfully""" project_dir = make_project(tmpdir, backend="dummy_backend") - req = InstallRequirement(None, None, source_dir=project_dir) + req = InstallRequirement(None, None) + req.source_dir = project_dir # make req believe it has been unpacked req.load_pyproject_toml() env = BuildEnvironment() finder = make_test_finder(find_links=[data.backends]) @@ -50,7 +51,8 @@ def test_backend_path(tmpdir, data): tmpdir, backend="dummy_backend", backend_path=['.'] ) (project_dir / 'dummy_backend.py').write_text(dummy_backend_code) - req = InstallRequirement(None, None, source_dir=project_dir) + req = InstallRequirement(None, None) + req.source_dir = project_dir # make req believe it has been unpacked req.load_pyproject_toml() env = BuildEnvironment() @@ -67,7 +69,8 @@ def test_backend_path_and_dep(tmpdir, data): (project_dir / 'dummy_internal_backend.py').write_text( "from dummy_backend import build_wheel" ) - req = InstallRequirement(None, None, source_dir=project_dir) + req = InstallRequirement(None, None) + req.source_dir = project_dir # make req believe it has been unpacked req.load_pyproject_toml() env = BuildEnvironment() finder = make_test_finder(find_links=[data.backends]) diff --git a/tests/unit/test_pep517.py b/tests/unit/test_pep517.py index c961a7f2f97..18cb178bba7 100644 --- a/tests/unit/test_pep517.py +++ b/tests/unit/test_pep517.py @@ -16,7 +16,8 @@ def test_use_pep517(shared_data, source, expected): Test that we choose correctly between PEP 517 and legacy code paths """ src = shared_data.src.joinpath(source) - req = InstallRequirement(None, None, source_dir=src) + req = InstallRequirement(None, None) + req.source_dir = src # make req believe it has been unpacked req.load_pyproject_toml() assert req.use_pep517 is expected @@ -30,7 +31,8 @@ def test_disabling_pep517_invalid(shared_data, source, msg): Test that we fail if we try to disable PEP 517 when it's not acceptable """ src = shared_data.src.joinpath(source) - req = InstallRequirement(None, None, source_dir=src) + req = InstallRequirement(None, None) + req.source_dir = src # make req believe it has been unpacked # Simulate --no-use-pep517 req.use_pep517 = False @@ -54,7 +56,8 @@ def test_pep517_parsing_checks_requirements(tmpdir, spec): build-backend = "foo" """.format(spec) )) - req = InstallRequirement(None, None, source_dir=tmpdir) + req = InstallRequirement(None, None) + req.source_dir = tmpdir # make req believe it has been unpacked with pytest.raises(InstallationError) as e: req.load_pyproject_toml() diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index d6129d51e1f..73733d46235 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -641,8 +641,8 @@ def test_mismatched_versions(caplog): req = InstallRequirement( req=Requirement('simplewheel==2.0'), comes_from=None, - source_dir="/tmp/somewhere", ) + req.source_dir = "/tmp/somewhere" # make req believe it has been unpacked # Monkeypatch! req._metadata = {"name": "simplewheel", "version": "1.0"} req.assert_source_matches_version() From 88e6e6bc5c877bfb15cce6f622dfbf9221c7b479 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sun, 22 Mar 2020 14:18:11 +0100 Subject: [PATCH 1602/3170] build in place --- docs/html/reference/pip_install.rst | 9 +++-- news/7555.removal | 9 +++++ src/pip/_internal/operations/prepare.py | 47 ++++++++++++------------- tests/unit/test_operations_prepare.py | 39 ++------------------ 4 files changed, 40 insertions(+), 64 deletions(-) create mode 100644 news/7555.removal diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index 08149a76773..6840e25fb1b 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -728,8 +728,13 @@ You can install local projects by specifying the project path to pip:: $ pip install path/to/SomeProject -During regular installation, pip will copy the entire project directory to a temporary location and install from there. -The exception is that pip will exclude .tox and .nox directories present in the top level of the project from being copied. +Until version 20.0, pip did copy the entire project directory to a temporary +location and installed from there. This approach was the cause of several +performance and correctness issues. As of version 20.1 pip installs from the +local project directory. Depending on the build backend used by the project, +this may generate secondary build artifacts in the project directory, such as +the ``.egg-info`` and ``build`` directories in the case of the setuptools +backend. .. _`editable-installs`: diff --git a/news/7555.removal b/news/7555.removal new file mode 100644 index 00000000000..2f5747f17d4 --- /dev/null +++ b/news/7555.removal @@ -0,0 +1,9 @@ +Building of local directories is now done in place. Previously pip did copy the +local directory tree to a temporary location before building. That approach had +a number of drawbacks, among which performance issues, as well as various +issues arising when the python project directory depends on its parent +directory (such as the presence of a VCS directory). The user visible effect of +this change is that secondary build artifacts, if any, may therefore be created +in the local directory, whereas before they were created in a temporary copy of +the directory and then deleted. This notably includes the ``build`` and +``.egg-info`` directories in the case of the setuptools build backend. diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 1fcbb775ece..3817323bdb8 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -27,12 +27,7 @@ from pip._internal.utils.filesystem import copy2_fixed from pip._internal.utils.hashes import MissingHashes from pip._internal.utils.logging import indent_log -from pip._internal.utils.misc import ( - display_path, - hide_url, - path_to_display, - rmtree, -) +from pip._internal.utils.misc import display_path, hide_url, path_to_display from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.unpacking import unpack_file @@ -239,11 +234,9 @@ def unpack_url( unpack_vcs_link(link, location) return None - # If it's a url to a local directory + # If it's a url to a local directory, we build in-place. + # There is nothing to be done here. if link.is_existing_dir(): - if os.path.isdir(location): - rmtree(location) - _copy_source_tree(link.file_path, location) return None # file urls @@ -415,21 +408,25 @@ def prepare_linked_requirement( with indent_log(): # Since source_dir is only set for editable requirements. assert req.source_dir is None - req.ensure_has_source_dir(self.build_dir, autodelete_unpacked) - # 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` - if os.path.exists(os.path.join(req.source_dir, 'setup.py')): - raise PreviousBuildDirError( - "pip can't proceed with requirements '{}' due to a" - " pre-existing build directory ({}). 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." - .format(req, req.source_dir) - ) + if link.is_existing_dir(): + # Build local directories in place. + req.source_dir = link.file_path + else: + req.ensure_has_source_dir(self.build_dir, autodelete_unpacked) + # 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` + if os.path.exists(os.path.join(req.source_dir, 'setup.py')): + raise PreviousBuildDirError( + "pip can't proceed with requirements '{}' due to a" + " pre-existing build directory ({}). 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." + .format(req, req.source_dir) + ) # Now that we have the real link, we can tell what kind of # requirements we have and raise some more informative errors diff --git a/tests/unit/test_operations_prepare.py b/tests/unit/test_operations_prepare.py index 0158eed5197..3df5429189a 100644 --- a/tests/unit/test_operations_prepare.py +++ b/tests/unit/test_operations_prepare.py @@ -214,40 +214,5 @@ def test_unpack_url_thats_a_dir(self, tmpdir, data): unpack_url(dist_url, self.build_dir, downloader=self.no_downloader, download_dir=self.download_dir) - assert os.path.isdir(os.path.join(self.build_dir, 'fspkg')) - - -@pytest.mark.parametrize('exclude_dir', [ - '.nox', - '.tox' -]) -def test_unpack_url_excludes_expected_dirs(tmpdir, exclude_dir): - src_dir = tmpdir / 'src' - dst_dir = tmpdir / 'dst' - src_included_file = src_dir.joinpath('file.txt') - src_excluded_dir = src_dir.joinpath(exclude_dir) - src_excluded_file = src_dir.joinpath(exclude_dir, 'file.txt') - src_included_dir = src_dir.joinpath('subdir', exclude_dir) - - # set up source directory - src_excluded_dir.mkdir(parents=True) - src_included_dir.mkdir(parents=True) - src_included_file.touch() - src_excluded_file.touch() - - dst_included_file = dst_dir.joinpath('file.txt') - dst_excluded_dir = dst_dir.joinpath(exclude_dir) - dst_excluded_file = dst_dir.joinpath(exclude_dir, 'file.txt') - dst_included_dir = dst_dir.joinpath('subdir', exclude_dir) - - src_link = Link(path_to_url(src_dir)) - unpack_url( - src_link, - dst_dir, - Mock(side_effect=AssertionError), - download_dir=None - ) - assert not os.path.isdir(dst_excluded_dir) - assert not os.path.isfile(dst_excluded_file) - assert os.path.isfile(dst_included_file) - assert os.path.isdir(dst_included_dir) + # test that nothing was copied to build_dir since we build in place + assert not os.path.exists(os.path.join(self.build_dir, 'fspkg')) From 873f1e6332aa827c886266c7d858067c12521e80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sun, 22 Mar 2020 14:26:58 +0100 Subject: [PATCH 1603/3170] remove _copy_source_tree and friends --- src/pip/_internal/operations/prepare.py | 56 +---------------- src/pip/_internal/utils/filesystem.py | 32 ---------- tests/functional/test_install.py | 26 -------- tests/lib/filesystem.py | 48 --------------- tests/unit/test_operations_prepare.py | 81 +------------------------ tests/unit/test_utils_filesystem.py | 61 ------------------- 6 files changed, 2 insertions(+), 302 deletions(-) delete mode 100644 tests/lib/filesystem.py delete mode 100644 tests/unit/test_utils_filesystem.py diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 3817323bdb8..30d5e3a308c 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -24,10 +24,9 @@ PreviousBuildDirError, VcsHashUnsupported, ) -from pip._internal.utils.filesystem import copy2_fixed from pip._internal.utils.hashes import MissingHashes from pip._internal.utils.logging import indent_log -from pip._internal.utils.misc import display_path, hide_url, path_to_display +from pip._internal.utils.misc import display_path, hide_url from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.unpacking import unpack_file @@ -128,59 +127,6 @@ def get_http_url( return File(from_path, content_type) -def _copy2_ignoring_special_files(src, dest): - # type: (str, str) -> None - """Copying special files is not supported, but as a convenience to users - we skip errors copying them. This supports tools that may create e.g. - socket files in the project source directory. - """ - try: - copy2_fixed(src, dest) - except shutil.SpecialFileError as e: - # SpecialFileError may be raised due to either the source or - # destination. If the destination was the cause then we would actually - # care, but since the destination directory is deleted prior to - # copy we ignore all of them assuming it is caused by the source. - logger.warning( - "Ignoring special file error '%s' encountered copying %s to %s.", - str(e), - path_to_display(src), - path_to_display(dest), - ) - - -def _copy_source_tree(source, target): - # type: (str, str) -> None - target_abspath = os.path.abspath(target) - target_basename = os.path.basename(target_abspath) - target_dirname = os.path.dirname(target_abspath) - - def ignore(d, names): - # type: (str, List[str]) -> List[str] - skipped = [] # type: List[str] - if d == source: - # Pulling in those directories can potentially be very slow, - # exclude the following directories if they appear in the top - # level dir (and only it). - # See discussion at https://github.com/pypa/pip/pull/6770 - skipped += ['.tox', '.nox'] - if os.path.abspath(d) == target_dirname: - # Prevent an infinite recursion if the target is in source. - # This can happen when TMPDIR is set to ${PWD}/... - # and we copy PWD to TMPDIR. - skipped += [target_basename] - return skipped - - kwargs = dict(ignore=ignore, symlinks=True) # type: CopytreeKwargs - - if not PY2: - # Python 2 does not support copy_function, so we only ignore - # errors on special file copy in Python 3. - kwargs['copy_function'] = _copy2_ignoring_special_files - - shutil.copytree(source, target, **kwargs) - - def get_file_url( link, # type: Link download_dir=None, # type: Optional[str] diff --git a/src/pip/_internal/utils/filesystem.py b/src/pip/_internal/utils/filesystem.py index 36578fb6244..ab20a7f042e 100644 --- a/src/pip/_internal/utils/filesystem.py +++ b/src/pip/_internal/utils/filesystem.py @@ -2,8 +2,6 @@ import os import os.path import random -import shutil -import stat import sys from contextlib import contextmanager from tempfile import NamedTemporaryFile @@ -54,36 +52,6 @@ def check_path_owner(path): return False # assume we don't own the path -def copy2_fixed(src, dest): - # type: (str, str) -> None - """Wrap shutil.copy2() but map errors copying socket files to - SpecialFileError as expected. - - See also https://bugs.python.org/issue37700. - """ - try: - shutil.copy2(src, dest) - except (OSError, IOError): - for f in [src, dest]: - try: - is_socket_file = is_socket(f) - except OSError: - # An error has already occurred. Another error here is not - # a problem and we can ignore it. - pass - else: - if is_socket_file: - raise shutil.SpecialFileError( - "`{f}` is a socket".format(**locals())) - - raise - - -def is_socket(path): - # type: (str) -> bool - return stat.S_ISSOCK(os.lstat(path).st_mode) - - @contextmanager def adjacent_tmp_file(path, **kwargs): # type: (str, **Any) -> Iterator[NamedTemporaryFileResult] diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 50fa2e81d85..1562e6b1d4e 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -2,7 +2,6 @@ import glob import os import re -import shutil import ssl import sys import textwrap @@ -29,7 +28,6 @@ skip_if_python2, windows_workaround_7667, ) -from tests.lib.filesystem import make_socket_file from tests.lib.local_repos import local_checkout from tests.lib.path import Path from tests.lib.server import ( @@ -576,30 +574,6 @@ def test_install_from_local_directory_with_symlinks_to_directories( assert egg_info_folder in result.files_created, str(result) -@pytest.mark.skipif("sys.platform == 'win32' or sys.version_info < (3,)") -def test_install_from_local_directory_with_socket_file(script, data, tmpdir): - """ - Test installing from a local directory containing a socket file. - """ - egg_info_file = ( - script.site_packages / - "FSPkg-0.1.dev0-py{pyversion}.egg-info".format(**globals()) - ) - package_folder = script.site_packages / "fspkg" - to_copy = data.packages.joinpath("FSPkg") - to_install = tmpdir.joinpath("src") - - shutil.copytree(to_copy, to_install) - # Socket file, should be ignored. - socket_file_path = os.path.join(to_install, "example") - make_socket_file(socket_file_path) - - result = script.pip("install", "--verbose", to_install) - assert package_folder in result.files_created, str(result.stdout) - assert egg_info_file in result.files_created, str(result) - assert str(socket_file_path) in result.stderr - - def test_install_from_local_directory_with_no_setup_py(script, data): """ Test installing from a local directory with no 'setup.py'. diff --git a/tests/lib/filesystem.py b/tests/lib/filesystem.py deleted file mode 100644 index dc14b323e33..00000000000 --- a/tests/lib/filesystem.py +++ /dev/null @@ -1,48 +0,0 @@ -"""Helpers for filesystem-dependent tests. -""" -import os -import socket -import subprocess -import sys -from functools import partial -from itertools import chain - -from .path import Path - - -def make_socket_file(path): - # Socket paths are limited to 108 characters (sometimes less) so we - # chdir before creating it and use a relative path name. - cwd = os.getcwd() - os.chdir(os.path.dirname(path)) - try: - sock = socket.socket(socket.AF_UNIX) - sock.bind(os.path.basename(path)) - finally: - os.chdir(cwd) - - -def make_unreadable_file(path): - Path(path).touch() - os.chmod(path, 0o000) - if sys.platform == "win32": - # Once we drop PY2 we can use `os.getlogin()` instead. - username = os.environ["USERNAME"] - # Remove "Read Data/List Directory" permission for current user, but - # leave everything else. - args = ["icacls", path, "/deny", username + ":(RD)"] - subprocess.check_call(args) - - -def get_filelist(base): - def join(dirpath, dirnames, filenames): - relative_dirpath = os.path.relpath(dirpath, base) - join_dirpath = partial(os.path.join, relative_dirpath) - return chain( - (join_dirpath(p) for p in dirnames), - (join_dirpath(p) for p in filenames), - ) - - return set(chain.from_iterable( - join(*dirinfo) for dirinfo in os.walk(base) - )) diff --git a/tests/unit/test_operations_prepare.py b/tests/unit/test_operations_prepare.py index 3df5429189a..bcfc8148669 100644 --- a/tests/unit/test_operations_prepare.py +++ b/tests/unit/test_operations_prepare.py @@ -10,18 +10,9 @@ from pip._internal.models.link import Link from pip._internal.network.download import Downloader from pip._internal.network.session import PipSession -from pip._internal.operations.prepare import ( - _copy_source_tree, - _download_http_url, - unpack_url, -) +from pip._internal.operations.prepare import _download_http_url, unpack_url from pip._internal.utils.hashes import Hashes from pip._internal.utils.urls import path_to_url -from tests.lib.filesystem import ( - get_filelist, - make_socket_file, - make_unreadable_file, -) from tests.lib.path import Path from tests.lib.requests_mocks import MockResponse @@ -101,76 +92,6 @@ def clean_project(tmpdir_factory, data): return new_project_dir -def test_copy_source_tree(clean_project, tmpdir): - target = tmpdir.joinpath("target") - expected_files = get_filelist(clean_project) - assert len(expected_files) == 3 - - _copy_source_tree(clean_project, target) - - copied_files = get_filelist(target) - assert expected_files == copied_files - - -@pytest.mark.skipif("sys.platform == 'win32' or sys.version_info < (3,)") -def test_copy_source_tree_with_socket(clean_project, tmpdir, caplog): - target = tmpdir.joinpath("target") - expected_files = get_filelist(clean_project) - socket_path = str(clean_project.joinpath("aaa")) - make_socket_file(socket_path) - - _copy_source_tree(clean_project, target) - - copied_files = get_filelist(target) - assert expected_files == copied_files - - # Warning should have been logged. - assert len(caplog.records) == 1 - record = caplog.records[0] - assert record.levelname == 'WARNING' - assert socket_path in record.message - - -@pytest.mark.skipif("sys.platform == 'win32' or sys.version_info < (3,)") -def test_copy_source_tree_with_socket_fails_with_no_socket_error( - clean_project, tmpdir -): - target = tmpdir.joinpath("target") - expected_files = get_filelist(clean_project) - make_socket_file(clean_project.joinpath("aaa")) - unreadable_file = clean_project.joinpath("bbb") - make_unreadable_file(unreadable_file) - - with pytest.raises(shutil.Error) as e: - _copy_source_tree(clean_project, target) - - errored_files = [err[0] for err in e.value.args[0]] - assert len(errored_files) == 1 - assert unreadable_file in errored_files - - copied_files = get_filelist(target) - # All files without errors should have been copied. - assert expected_files == copied_files - - -def test_copy_source_tree_with_unreadable_dir_fails(clean_project, tmpdir): - target = tmpdir.joinpath("target") - expected_files = get_filelist(clean_project) - unreadable_file = clean_project.joinpath("bbb") - make_unreadable_file(unreadable_file) - - with pytest.raises(shutil.Error) as e: - _copy_source_tree(clean_project, target) - - errored_files = [err[0] for err in e.value.args[0]] - assert len(errored_files) == 1 - assert unreadable_file in errored_files - - copied_files = get_filelist(target) - # All files without errors should have been copied. - assert expected_files == copied_files - - class Test_unpack_url(object): def prep(self, tmpdir, data): diff --git a/tests/unit/test_utils_filesystem.py b/tests/unit/test_utils_filesystem.py deleted file mode 100644 index 3ef814dce4b..00000000000 --- a/tests/unit/test_utils_filesystem.py +++ /dev/null @@ -1,61 +0,0 @@ -import os -import shutil - -import pytest - -from pip._internal.utils.filesystem import copy2_fixed, is_socket -from tests.lib.filesystem import make_socket_file, make_unreadable_file -from tests.lib.path import Path - - -def make_file(path): - Path(path).touch() - - -def make_valid_symlink(path): - target = path + "1" - make_file(target) - os.symlink(target, path) - - -def make_broken_symlink(path): - os.symlink("foo", path) - - -def make_dir(path): - os.mkdir(path) - - -skip_on_windows = pytest.mark.skipif("sys.platform == 'win32'") - - -@skip_on_windows -@pytest.mark.parametrize("create,result", [ - (make_socket_file, True), - (make_file, False), - (make_valid_symlink, False), - (make_broken_symlink, False), - (make_dir, False), -]) -def test_is_socket(create, result, tmpdir): - target = tmpdir.joinpath("target") - create(target) - assert os.path.lexists(target) - assert is_socket(target) == result - - -@pytest.mark.parametrize("create,error_type", [ - pytest.param( - make_socket_file, shutil.SpecialFileError, marks=skip_on_windows - ), - (make_unreadable_file, OSError), -]) -def test_copy2_fixed_raises_appropriate_errors(create, error_type, tmpdir): - src = tmpdir.joinpath("src") - create(src) - dest = tmpdir.joinpath("dest") - - with pytest.raises(error_type): - copy2_fixed(src, dest) - - assert not dest.exists() From ace0c1653121770a58ae3cccf9059227702edde5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sat, 28 Mar 2020 19:24:20 +0100 Subject: [PATCH 1604/3170] fix test_entrypoints_work test Since we now build in place, pip install calls setup.py in place which in turn creates fake_pkg.egg-info. Since in this test the package we are installing is in script.temp_path, we must tell script to expect temporary files to be created. --- tests/functional/test_cli.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/functional/test_cli.py b/tests/functional/test_cli.py index e416315125f..c401a7cf80f 100644 --- a/tests/functional/test_cli.py +++ b/tests/functional/test_cli.py @@ -27,7 +27,9 @@ def test_entrypoints_work(entrypoint, script): ) """.format(entrypoint))) - script.pip("install", "-vvv", str(fake_pkg)) + # expect_temp=True, because pip install calls setup.py which + # in turn creates fake_pkg.egg-info. + script.pip("install", "-vvv", str(fake_pkg), expect_temp=True) result = script.pip("-V") result2 = script.run("fake_pip", "-V", allow_stderr_warning=True) assert result.stdout == result2.stdout From 877e1ccc7776548918537c17630decb4f87f665f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sat, 28 Mar 2020 19:25:34 +0100 Subject: [PATCH 1605/3170] fix test_uninstall_console_scripts This particular test checks which files are created. Since we now build in place, expect the .egg-info directory to be created. --- tests/functional/test_uninstall.py | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/tests/functional/test_uninstall.py b/tests/functional/test_uninstall.py index ab41917c986..c030dbaf27b 100644 --- a/tests/functional/test_uninstall.py +++ b/tests/functional/test_uninstall.py @@ -271,7 +271,15 @@ def test_uninstall_console_scripts(script): sorted(result.files_created.keys()) ) result2 = script.pip('uninstall', 'discover', '-y') - assert_all_changes(result, result2, [script.venv / 'build', 'cache']) + assert_all_changes( + result, + result2, + [ + script.venv / 'build', + 'cache', + script.scratch / 'discover' / 'discover.egg-info', + ] + ) def test_uninstall_console_scripts_uppercase_name(script): From dbd2c2f7601477f6c70280591c13a9e58dc58835 Mon Sep 17 00:00:00 2001 From: ghost <ghost@localhost> Date: Sun, 12 Apr 2020 20:38:43 +0800 Subject: [PATCH 1606/3170] Fix Windows folder writable detection --- news/8013.bugfix | 1 + src/pip/_internal/utils/filesystem.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 news/8013.bugfix diff --git a/news/8013.bugfix b/news/8013.bugfix new file mode 100644 index 00000000000..a6e8fcb28e7 --- /dev/null +++ b/news/8013.bugfix @@ -0,0 +1 @@ +Fix Windows folder writable detection. diff --git a/src/pip/_internal/utils/filesystem.py b/src/pip/_internal/utils/filesystem.py index 36578fb6244..e25641d3b65 100644 --- a/src/pip/_internal/utils/filesystem.py +++ b/src/pip/_internal/utils/filesystem.py @@ -158,10 +158,13 @@ def _test_writable_dir_win(path): file = os.path.join(path, name) try: fd = os.open(file, os.O_RDWR | os.O_CREAT | os.O_EXCL) + # Python 2 doesn't support FileExistsError and PermissionError. except OSError as e: + # exception FileExistsError if e.errno == errno.EEXIST: continue - if e.errno == errno.EPERM: + # exception PermissionError + if e.errno == errno.EPERM or e.errno == errno.EACCES: # This could be because there's a directory with the same name. # But it's highly unlikely there's a directory called that, # so we'll assume it's because the parent dir is not writable. From 113884b3a21c179afcd95061eaee9b7179b3f164 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 12 Apr 2020 19:13:25 +0530 Subject: [PATCH 1607/3170] Drop entire section on Indentation --- docs/html/development/conventions.rst | 44 --------------------------- 1 file changed, 44 deletions(-) diff --git a/docs/html/development/conventions.rst b/docs/html/development/conventions.rst index e3b4c6dc14e..0abf4abfa3f 100644 --- a/docs/html/development/conventions.rst +++ b/docs/html/development/conventions.rst @@ -31,10 +31,6 @@ Encoding All files in our documentation must use UTF-8 encoding. - -File Layout -=========== - Line Length ----------- @@ -42,46 +38,6 @@ Limit all lines to a maximum of 72 characters, where possible. This may be exceeded when it does not make sense to abide by it (eg. long links, code blocks). -Indentation ------------ - -We use 3 spaces for indentation. - -:: - - .. note:: - - Directive blocks - - :: - - Code block. - -Bullet lists are the only exception to the 3 spaces rule, using 2 spaces -when wrapping lines. - -:: - - - This is a bullet list. - - This is a lot of text in a single bullet which would require wrapping - across multiple lines to fit in the line length limits. - -Note that nested lists would use 3 spaces for indentation, and require -blank lines on either side (that's the ReST syntax). - -:: - - - This is a bullet list. - - There is a nested list associated with this list item. - - - This is a nested bullet list. - - With multiple bullets even. - - And some of the bullets have really long sentences that would - require wrapping across multiple lines. - - - This is a lot of text in a single bullet which would require wrapping - across multiple lines to fit in the line length limits. - Headings ======== From 72c0cf41978afb6b158f391660922150e91ab5a7 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 12 Apr 2020 19:22:24 +0530 Subject: [PATCH 1608/3170] Azure Pipelines: Drop useVenv This wasn't actually ever used, since the value was always '$(useVenv)'. --- .azure-pipelines/jobs/test-windows.yml | 3 --- .azure-pipelines/steps/run-tests-windows.yml | 6 +----- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/.azure-pipelines/jobs/test-windows.yml b/.azure-pipelines/jobs/test-windows.yml index 1a933a6934b..728a4ea69e2 100644 --- a/.azure-pipelines/jobs/test-windows.yml +++ b/.azure-pipelines/jobs/test-windows.yml @@ -15,14 +15,12 @@ jobs: Python27-x64: python.version: '2.7' python.architecture: x64 - useVenv: true Python35-x64: python.version: '3.5' python.architecture: x64 Python36-x64: python.version: '3.6' python.architecture: x64 - useVenv: true Python37-x64: python.version: '3.7' python.architecture: x64 @@ -35,7 +33,6 @@ jobs: - template: ../steps/run-tests-windows.yml parameters: runIntegrationTests: true - useVenv: '$(useVenv)' - job: Test_Secondary displayName: Test Secondary diff --git a/.azure-pipelines/steps/run-tests-windows.yml b/.azure-pipelines/steps/run-tests-windows.yml index 3832e46621b..a65136289ab 100644 --- a/.azure-pipelines/steps/run-tests-windows.yml +++ b/.azure-pipelines/steps/run-tests-windows.yml @@ -1,6 +1,5 @@ parameters: runIntegrationTests: - useVenv: false steps: - task: UsePythonVersion@0 @@ -44,11 +43,8 @@ steps: # https://bugs.python.org/issue18199 $env:TEMP = "R:\Temp" - tox -e py -- $env:USE_VENV_ARG -m integration -n auto --duration=5 --junit-xml=junit/integration-test.xml + tox -e py -- -m integration -n auto --duration=5 --junit-xml=junit/integration-test.xml displayName: Tox run integration tests - env: - ${{ if eq(parameters.useVenv, 'true') }}: - USE_VENV_ARG: "--use-venv" - task: PublishTestResults@2 displayName: Publish Test Results From 609e8898afa4e69c9c7586124a1c990700b3e2af Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 12 Apr 2020 19:21:36 +0530 Subject: [PATCH 1609/3170] Azure Pipelines: Use better names --- .azure-pipelines/jobs/test-windows.yml | 24 ++++++++++++------------ .azure-pipelines/jobs/test.yml | 14 +++++++------- 2 files changed, 19 insertions(+), 19 deletions(-) diff --git a/.azure-pipelines/jobs/test-windows.yml b/.azure-pipelines/jobs/test-windows.yml index 728a4ea69e2..2d1166f4e37 100644 --- a/.azure-pipelines/jobs/test-windows.yml +++ b/.azure-pipelines/jobs/test-windows.yml @@ -3,28 +3,28 @@ parameters: jobs: - job: Test_Primary - displayName: Test Primary + displayName: Tests / pool: vmImage: ${{ parameters.vmImage }} strategy: matrix: - Python27-x86: + "2.7-x86": python.version: '2.7' python.architecture: x86 - Python27-x64: + "2.7": python.version: '2.7' python.architecture: x64 - Python35-x64: + "3.5": python.version: '3.5' python.architecture: x64 - Python36-x64: + "3.6": python.version: '3.6' python.architecture: x64 - Python37-x64: + "3.7": python.version: '3.7' python.architecture: x64 - Python38-x64: + "3.8": python.version: '3.8' python.architecture: x64 maxParallel: 6 @@ -35,7 +35,7 @@ jobs: runIntegrationTests: true - job: Test_Secondary - displayName: Test Secondary + displayName: Tests / # Don't run integration tests for these runs # Run after Test_Primary so we don't devour time and jobs if tests are going to fail dependsOn: Test_Primary @@ -45,16 +45,16 @@ jobs: strategy: matrix: # This is for Windows, so test x86 builds - Python35-x86: + "3.5-x86": python.version: '3.5' python.architecture: x86 - Python36-x86: + "3.6-x86": python.version: '3.6' python.architecture: x86 - Python37-x86: + "3.7-x86": python.version: '3.7' python.architecture: x86 - Python38-x86: + "3.8-x86": python.version: '3.8' python.architecture: x86 maxParallel: 6 diff --git a/.azure-pipelines/jobs/test.yml b/.azure-pipelines/jobs/test.yml index 68b6e5268e1..f7dac5143f7 100644 --- a/.azure-pipelines/jobs/test.yml +++ b/.azure-pipelines/jobs/test.yml @@ -3,16 +3,16 @@ parameters: jobs: - job: Test_Primary - displayName: Test Primary + displayName: Tests / pool: vmImage: ${{ parameters.vmImage }} strategy: matrix: - Python27: + "2.7": python.version: '2.7' python.architecture: x64 - Python36: + "3.6": python.version: '3.6' python.architecture: x64 maxParallel: 2 @@ -21,7 +21,7 @@ jobs: - template: ../steps/run-tests.yml - job: Test_Secondary - displayName: Test Secondary + displayName: Tests / # Run after Test_Primary so we don't devour time and jobs if tests are going to fail dependsOn: Test_Primary @@ -29,13 +29,13 @@ jobs: vmImage: ${{ parameters.vmImage }} strategy: matrix: - Python35: + "3.5": python.version: '3.5' python.architecture: x64 - Python37: + "3.7": python.version: '3.7' python.architecture: x64 - Python38: + "3.8": python.version: '3.8' python.architecture: x64 maxParallel: 4 From f0ae64f959f871c79cc7d6adf3cfb010d5649f96 Mon Sep 17 00:00:00 2001 From: Nikolay Korolev <CrafterKolyan@mail.ru> Date: Sun, 12 Apr 2020 19:01:51 +0300 Subject: [PATCH 1610/3170] Delete misleading line of code --- src/pip/_internal/utils/logging.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pip/_internal/utils/logging.py b/src/pip/_internal/utils/logging.py index 0143434ee2f..9a017cf7e33 100644 --- a/src/pip/_internal/utils/logging.py +++ b/src/pip/_internal/utils/logging.py @@ -52,7 +52,6 @@ _log_state = threading.local() -_log_state.indentation = 0 subprocess_logger = getLogger('pip.subprocessor') From 9e02a1a97f335d2aeca91047a3a7d987657a9347 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 13 Apr 2020 04:55:57 +0530 Subject: [PATCH 1611/3170] Reword based on suggestion Co-Authored-By: Noah <noah.bar.ilan@gmail.com> --- docs/html/reference/pip_install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index 6840e25fb1b..56b16781e6b 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -728,7 +728,7 @@ You can install local projects by specifying the project path to pip:: $ pip install path/to/SomeProject -Until version 20.0, pip did copy the entire project directory to a temporary +Until version 20.0, pip copied the entire project directory to a temporary location and installed from there. This approach was the cause of several performance and correctness issues. As of version 20.1 pip installs from the local project directory. Depending on the build backend used by the project, From 84b99c20b852d6dcdb0fdc48e38bb03b639b576d Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Mon, 13 Apr 2020 18:05:24 +0800 Subject: [PATCH 1612/3170] Canonicalize installed distribution keys --- src/pip/_internal/resolution/resolvelib/factory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 9367bed4ded..fb5d26abe23 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -60,7 +60,7 @@ def __init__( if not ignore_installed: self._installed_dists = { - dist.project_name: dist + canonicalize_name(dist.project_name): dist for dist in get_installed_distributions() } else: From 3642589903743be36449f4e6f8d9a8c091e5c594 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Mon, 13 Apr 2020 11:16:30 +0100 Subject: [PATCH 1613/3170] Make message more user friendly when unable to resolve --- .../resolution/resolvelib/requirements.py | 4 +++ .../resolution/resolvelib/resolver.py | 28 ++++++++++++++-- tests/functional/test_new_resolver.py | 33 +++++++++++++++++++ 3 files changed, 63 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py index 027e36aa61e..952973ae7cc 100644 --- a/src/pip/_internal/resolution/resolvelib/requirements.py +++ b/src/pip/_internal/resolution/resolvelib/requirements.py @@ -79,6 +79,10 @@ def __init__(self, ireq, factory): self._factory = factory self.extras = ireq.req.extras + def __str__(self): + # type: () -> str + return str(self._ireq.req) + def __repr__(self): # type: () -> str return "{class_name}({requirement!r})".format( diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 5e6408573c6..d2e571f17bf 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -1,9 +1,11 @@ import functools +import logging from pip._vendor.packaging.utils import canonicalize_name -from pip._vendor.resolvelib import BaseReporter +from pip._vendor.resolvelib import BaseReporter, ResolutionImpossible from pip._vendor.resolvelib import Resolver as RLResolver +from pip._internal.exceptions import InstallationError from pip._internal.req.req_set import RequirementSet from pip._internal.resolution.base import BaseResolver from pip._internal.resolution.resolvelib.provider import PipProvider @@ -23,6 +25,9 @@ from pip._internal.resolution.base import InstallRequirementProvider +logger = logging.getLogger(__name__) + + class Resolver(BaseResolver): def __init__( self, @@ -64,7 +69,26 @@ def resolve(self, root_reqs, check_supported_wheels): self.factory.make_requirement_from_install_req(r) for r in root_reqs ] - self._result = resolver.resolve(requirements) + + try: + self._result = resolver.resolve(requirements) + except ResolutionImpossible as exc: + # TODO: This is just an initial version. May need more work. + # Also could do with rewriting to fit better into 80-char + # lines :-( + for req, parent in exc.causes: + logger.critical( + "Could not find a version that satisfies " + + "the requirement " + + str(req) + + ("" if parent is None else " (from {})".format( + parent.name + )) + ) + raise InstallationError( + "No matching distribution found for " + + ", ".join([r.name for r, _ in exc.causes]) + ) req_set = RequirementSet(check_supported_wheels=check_supported_wheels) for candidate in self._result.mapping.values(): diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 25c726f2574..5d119865e7f 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -155,6 +155,39 @@ def test_new_resolver_installs_extras(script): assert_installed(script, base="0.1.0", dep="0.1.0") +def test_new_resolver_installed_message(script): + create_basic_wheel_for_package(script, "A", "1.0") + result = script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "A", + expect_stderr=False, + ) + assert "Successfully installed A-1.0" in result.stdout, str(result) + + +def test_new_resolver_no_dist_message(script): + create_basic_wheel_for_package(script, "A", "1.0") + result = script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "B", + expect_error=True, + expect_stderr=True, + ) + + # Full messages from old resolver: + # ERROR: Could not find a version that satisfies the + # requirement xxx (from versions: none) + # ERROR: No matching distribution found for xxx + + assert "Could not find a version that satisfies the requirement B" \ + in result.stderr, str(result) + assert "No matching distribution found for B" in result.stderr, str(result) + + def test_new_resolver_installs_editable(script): create_basic_wheel_for_package( script, From 8db9f5bdb6bd8ce7c5fa67ea6a7a67dc8a5d93c3 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Mon, 13 Apr 2020 12:32:28 +0100 Subject: [PATCH 1614/3170] Add a vendoring session to noxfile.py --- noxfile.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/noxfile.py b/noxfile.py index 2e4ad6fda44..ef1d4ee5875 100644 --- a/noxfile.py +++ b/noxfile.py @@ -150,6 +150,16 @@ def lint(session): session.run("pre-commit", "run", *args) +@nox.session +def vendoring(session): + # Required, otherwise we interpret --no-binary :all: as + # "do not build wheels", which fails for PEP 517 requirements + session.install("-U", "pip>=19.3.1") + session.install("vendoring") + + session.run("vendoring", "sync", ".", "-v") + + # ----------------------------------------------------------------------------- # Release Commands # ----------------------------------------------------------------------------- From f4f0a3405363e11f8d1bce60f701000229509a9b Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Mon, 13 Apr 2020 21:31:17 +0800 Subject: [PATCH 1615/3170] Block user from unfinished parts of the resolver --- src/pip/_internal/resolution/resolvelib/resolver.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 5e6408573c6..64b7d47988a 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -4,6 +4,7 @@ from pip._vendor.resolvelib import BaseReporter from pip._vendor.resolvelib import Resolver as RLResolver +from pip._internal.exceptions import InstallationError from pip._internal.req.req_set import RequirementSet from pip._internal.resolution.base import BaseResolver from pip._internal.resolution.resolvelib.provider import PipProvider @@ -53,6 +54,11 @@ def __init__( def resolve(self, root_reqs, check_supported_wheels): # type: (List[InstallRequirement], bool) -> RequirementSet + + # FIXME: Implement constraints. + if any(r.constraint for r in root_reqs): + raise InstallationError("Constraints are not yet supported.") + provider = PipProvider( factory=self.factory, ignore_dependencies=self.ignore_dependencies, @@ -109,7 +115,11 @@ def get_installation_order(self, req_set): # FIXME: This check will fail if there are unbreakable cycles. # Implement something to forcifully break them up to continue. - assert progressed, "Order calculation stuck in dependency loop." + if not progressed: + raise InstallationError( + "Could not determine installation order due to cicular " + "dependency." + ) sorted_items = sorted( req_set.requirements.items(), From 0e8093ec5cc574c8055474a239f2867f62b885d4 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sat, 11 Apr 2020 13:24:27 +0530 Subject: [PATCH 1616/3170] Type annotations for hash, show and wheel in commands --- ...F3EC962-957A-4DB8-A849-2E7179F875A9.trivial | 0 src/pip/_internal/commands/hash.py | 17 ++++++++++++----- src/pip/_internal/commands/show.py | 18 +++++++++++++----- src/pip/_internal/commands/wheel.py | 14 ++++++++------ 4 files changed, 33 insertions(+), 16 deletions(-) create mode 100644 news/BF3EC962-957A-4DB8-A849-2E7179F875A9.trivial diff --git a/news/BF3EC962-957A-4DB8-A849-2E7179F875A9.trivial b/news/BF3EC962-957A-4DB8-A849-2E7179F875A9.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/commands/hash.py b/src/pip/_internal/commands/hash.py index f26686156ee..9bb6e9e032a 100644 --- a/src/pip/_internal/commands/hash.py +++ b/src/pip/_internal/commands/hash.py @@ -1,6 +1,3 @@ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - from __future__ import absolute_import import hashlib @@ -8,9 +5,14 @@ import sys from pip._internal.cli.base_command import Command -from pip._internal.cli.status_codes import ERROR +from pip._internal.cli.status_codes import ERROR, SUCCESS from pip._internal.utils.hashes import FAVORITE_HASH, STRONG_HASHES from pip._internal.utils.misc import read_chunks, write_output +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from optparse import Values + from typing import Any, List, Dict logger = logging.getLogger(__name__) @@ -27,7 +29,9 @@ class HashCommand(Command): ignore_require_venv = True def __init__(self, *args, **kw): - super(HashCommand, self).__init__(*args, **kw) + # type: (List[Any], Dict[Any, Any]) -> None + # https://github.com/python/mypy/issues/4335 + super(HashCommand, self).__init__(*args, **kw) # type: ignore self.cmd_opts.add_option( '-a', '--algorithm', dest='algorithm', @@ -39,6 +43,7 @@ def __init__(self, *args, **kw): self.parser.insert_option_group(0, self.cmd_opts) def run(self, options, args): + # type: (Values, List[Any]) -> int if not args: self.parser.print_usage(sys.stderr) return ERROR @@ -47,9 +52,11 @@ def run(self, options, args): for path in args: write_output('%s:\n--hash=%s:%s', path, algorithm, _hash_of_file(path, algorithm)) + return SUCCESS def _hash_of_file(path, algorithm): + # type: (str, str) -> str """Return the hash digest of a file.""" with open(path, 'rb') as archive: hash = hashlib.new(algorithm) diff --git a/src/pip/_internal/commands/show.py b/src/pip/_internal/commands/show.py index a61294ba7bb..436d607391c 100644 --- a/src/pip/_internal/commands/show.py +++ b/src/pip/_internal/commands/show.py @@ -1,6 +1,3 @@ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - from __future__ import absolute_import import logging @@ -13,6 +10,11 @@ from pip._internal.cli.base_command import Command from pip._internal.cli.status_codes import ERROR, SUCCESS from pip._internal.utils.misc import write_output +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from optparse import Values + from typing import Any, List, Dict, Iterator logger = logging.getLogger(__name__) @@ -29,7 +31,9 @@ class ShowCommand(Command): ignore_require_venv = True def __init__(self, *args, **kw): - super(ShowCommand, self).__init__(*args, **kw) + # type: (List[Any], Dict[Any, Any]) -> None + # https://github.com/python/mypy/issues/4335 + super(ShowCommand, self).__init__(*args, **kw) # type: ignore self.cmd_opts.add_option( '-f', '--files', dest='files', @@ -40,6 +44,7 @@ def __init__(self, *args, **kw): self.parser.insert_option_group(0, self.cmd_opts) def run(self, options, args): + # type: (Values, List[Any]) -> int if not args: logger.warning('ERROR: Please provide a package name or names.') return ERROR @@ -53,6 +58,7 @@ def run(self, options, args): def search_packages_info(query): + # type: (List[Any]) -> Iterator[Dict[str, Any]] """ Gather details from installed distributions. Print distribution name, version, location, and installed files. Installed files requires a @@ -71,6 +77,7 @@ def search_packages_info(query): logger.warning('Package(s) not found: %s', ', '.join(missing)) def get_requiring_packages(package_name): + # type: (str) -> List[str] canonical_name = canonicalize_name(package_name) return [ pkg.project_name for pkg in pkg_resources.working_set @@ -88,7 +95,7 @@ def get_requiring_packages(package_name): 'required_by': get_requiring_packages(dist.project_name) } file_list = None - metadata = None + metadata = '' if isinstance(dist, pkg_resources.DistInfoDistribution): # RECORDs should be part of .dist-info metadatas if dist.has_metadata('RECORD'): @@ -141,6 +148,7 @@ def get_requiring_packages(package_name): def print_results(distributions, list_files=False, verbose=False): + # type: (Iterator[Dict[str, Any]], bool, bool) -> bool """ Print the information from installed distributions found. """ diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index 48f3bfa29ca..bf17d9ac854 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -1,8 +1,5 @@ # -*- coding: utf-8 -*- -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - from __future__ import absolute_import import logging @@ -12,6 +9,7 @@ from pip._internal.cache import WheelCache from pip._internal.cli import cmdoptions from pip._internal.cli.req_command import RequirementCommand, with_cleanup +from pip._internal.cli.status_codes import SUCCESS from pip._internal.exceptions import CommandError from pip._internal.req.req_tracker import get_requirement_tracker from pip._internal.utils.misc import ensure_dir, normalize_path @@ -21,7 +19,7 @@ if MYPY_CHECK_RUNNING: from optparse import Values - from typing import Any, List + from typing import Any, List, Dict logger = logging.getLogger(__name__) @@ -50,7 +48,9 @@ class WheelCommand(RequirementCommand): %prog [options] <archive url/path> ...""" def __init__(self, *args, **kw): - super(WheelCommand, self).__init__(*args, **kw) + # type: (List[Any], Dict[Any, Any]) -> None + # https://github.com/python/mypy/issues/4335 + super(WheelCommand, self).__init__(*args, **kw) # type: ignore cmd_opts = self.cmd_opts @@ -112,7 +112,7 @@ def __init__(self, *args, **kw): @with_cleanup def run(self, options, args): - # type: (Values, List[Any]) -> None + # type: (Values, List[Any]) -> int cmdoptions.check_install_build_global(options) session = self.get_default_session(options) @@ -188,3 +188,5 @@ def run(self, options, args): raise CommandError( "Failed to build one or more wheels" ) + + return SUCCESS From a1af9d517dc6d7d86f1c102d4ab6955718b25444 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 14 Apr 2020 12:59:54 +0530 Subject: [PATCH 1617/3170] Vendor toml 0.10.0 --- news/toml.vendor | 1 + src/pip/_vendor/toml.py | 1039 ++++++++++++++++++++++++++++++ src/pip/_vendor/toml.pyi | 1 + src/pip/_vendor/toml/LICENSE | 26 + src/pip/_vendor/toml/__init__.py | 21 + src/pip/_vendor/toml/decoder.py | 945 +++++++++++++++++++++++++++ src/pip/_vendor/toml/encoder.py | 250 +++++++ src/pip/_vendor/toml/ordered.py | 15 + src/pip/_vendor/toml/tz.py | 21 + src/pip/_vendor/vendor.txt | 1 + 10 files changed, 2320 insertions(+) create mode 100644 news/toml.vendor create mode 100644 src/pip/_vendor/toml.py create mode 100644 src/pip/_vendor/toml.pyi create mode 100644 src/pip/_vendor/toml/LICENSE create mode 100644 src/pip/_vendor/toml/__init__.py create mode 100644 src/pip/_vendor/toml/decoder.py create mode 100644 src/pip/_vendor/toml/encoder.py create mode 100644 src/pip/_vendor/toml/ordered.py create mode 100644 src/pip/_vendor/toml/tz.py diff --git a/news/toml.vendor b/news/toml.vendor new file mode 100644 index 00000000000..636b06b5c2f --- /dev/null +++ b/news/toml.vendor @@ -0,0 +1 @@ +Add ``toml`` 0.10.0 to vendored libraries. diff --git a/src/pip/_vendor/toml.py b/src/pip/_vendor/toml.py new file mode 100644 index 00000000000..dac398837b3 --- /dev/null +++ b/src/pip/_vendor/toml.py @@ -0,0 +1,1039 @@ +"""Python module which parses and emits TOML. + +Released under the MIT license. +""" +import re +import io +import datetime +from os import linesep +import sys + +__version__ = "0.9.6" +_spec_ = "0.4.0" + + +class TomlDecodeError(Exception): + """Base toml Exception / Error.""" + pass + + +class TomlTz(datetime.tzinfo): + def __init__(self, toml_offset): + if toml_offset == "Z": + self._raw_offset = "+00:00" + else: + self._raw_offset = toml_offset + self._sign = -1 if self._raw_offset[0] == '-' else 1 + self._hours = int(self._raw_offset[1:3]) + self._minutes = int(self._raw_offset[4:6]) + + def tzname(self, dt): + return "UTC" + self._raw_offset + + def utcoffset(self, dt): + return self._sign * datetime.timedelta(hours=self._hours, + minutes=self._minutes) + + def dst(self, dt): + return datetime.timedelta(0) + + +class InlineTableDict(object): + """Sentinel subclass of dict for inline tables.""" + + +def _get_empty_inline_table(_dict): + class DynamicInlineTableDict(_dict, InlineTableDict): + """Concrete sentinel subclass for inline tables. + It is a subclass of _dict which is passed in dynamically at load time + It is also a subclass of InlineTableDict + """ + + return DynamicInlineTableDict() + + +try: + _range = xrange +except NameError: + unicode = str + _range = range + basestring = str + unichr = chr + +try: + FNFError = FileNotFoundError +except NameError: + FNFError = IOError + + +def load(f, _dict=dict): + """Parses named file or files as toml and returns a dictionary + + Args: + f: Path to the file to open, array of files to read into single dict + or a file descriptor + _dict: (optional) Specifies the class of the returned toml dictionary + + Returns: + Parsed toml file represented as a dictionary + + Raises: + TypeError -- When f is invalid type + TomlDecodeError: Error while decoding toml + IOError / FileNotFoundError -- When an array with no valid (existing) + (Python 2 / Python 3) file paths is passed + """ + + if isinstance(f, basestring): + with io.open(f, encoding='utf-8') as ffile: + return loads(ffile.read(), _dict) + elif isinstance(f, list): + from os import path as op + from warnings import warn + if not [path for path in f if op.exists(path)]: + error_msg = "Load expects a list to contain filenames only." + error_msg += linesep + error_msg += ("The list needs to contain the path of at least one " + "existing file.") + raise FNFError(error_msg) + d = _dict() + for l in f: + if op.exists(l): + d.update(load(l)) + else: + warn("Non-existent filename in list with at least one valid " + "filename") + return d + else: + try: + return loads(f.read(), _dict) + except AttributeError: + raise TypeError("You can only load a file descriptor, filename or " + "list") + + +_groupname_re = re.compile(r'^[A-Za-z0-9_-]+$') + + +def loads(s, _dict=dict): + """Parses string as toml + + Args: + s: String to be parsed + _dict: (optional) Specifies the class of the returned toml dictionary + + Returns: + Parsed toml file represented as a dictionary + + Raises: + TypeError: When a non-string is passed + TomlDecodeError: Error while decoding toml + """ + + implicitgroups = [] + retval = _dict() + currentlevel = retval + if not isinstance(s, basestring): + raise TypeError("Expecting something like a string") + + if not isinstance(s, unicode): + s = s.decode('utf8') + + sl = list(s) + openarr = 0 + openstring = False + openstrchar = "" + multilinestr = False + arrayoftables = False + beginline = True + keygroup = False + keyname = 0 + for i, item in enumerate(sl): + if item == '\r' and sl[i + 1] == '\n': + sl[i] = ' ' + continue + if keyname: + if item == '\n': + raise TomlDecodeError("Key name found without value." + " Reached end of line.") + if openstring: + if item == openstrchar: + keyname = 2 + openstring = False + openstrchar = "" + continue + elif keyname == 1: + if item.isspace(): + keyname = 2 + continue + elif item.isalnum() or item == '_' or item == '-': + continue + elif keyname == 2 and item.isspace(): + continue + if item == '=': + keyname = 0 + else: + raise TomlDecodeError("Found invalid character in key name: '" + + item + "'. Try quoting the key name.") + if item == "'" and openstrchar != '"': + k = 1 + try: + while sl[i - k] == "'": + k += 1 + if k == 3: + break + except IndexError: + pass + if k == 3: + multilinestr = not multilinestr + openstring = multilinestr + else: + openstring = not openstring + if openstring: + openstrchar = "'" + else: + openstrchar = "" + if item == '"' and openstrchar != "'": + oddbackslash = False + k = 1 + tripquote = False + try: + while sl[i - k] == '"': + k += 1 + if k == 3: + tripquote = True + break + if k == 1 or (k == 3 and tripquote): + while sl[i - k] == '\\': + oddbackslash = not oddbackslash + k += 1 + except IndexError: + pass + if not oddbackslash: + if tripquote: + multilinestr = not multilinestr + openstring = multilinestr + else: + openstring = not openstring + if openstring: + openstrchar = '"' + else: + openstrchar = "" + if item == '#' and (not openstring and not keygroup and + not arrayoftables): + j = i + try: + while sl[j] != '\n': + sl[j] = ' ' + j += 1 + except IndexError: + break + if item == '[' and (not openstring and not keygroup and + not arrayoftables): + if beginline: + if len(sl) > i + 1 and sl[i + 1] == '[': + arrayoftables = True + else: + keygroup = True + else: + openarr += 1 + if item == ']' and not openstring: + if keygroup: + keygroup = False + elif arrayoftables: + if sl[i - 1] == ']': + arrayoftables = False + else: + openarr -= 1 + if item == '\n': + if openstring or multilinestr: + if not multilinestr: + raise TomlDecodeError("Unbalanced quotes") + if ((sl[i - 1] == "'" or sl[i - 1] == '"') and ( + sl[i - 2] == sl[i - 1])): + sl[i] = sl[i - 1] + if sl[i - 3] == sl[i - 1]: + sl[i - 3] = ' ' + elif openarr: + sl[i] = ' ' + else: + beginline = True + elif beginline and sl[i] != ' ' and sl[i] != '\t': + beginline = False + if not keygroup and not arrayoftables: + if sl[i] == '=': + raise TomlDecodeError("Found empty keyname. ") + keyname = 1 + s = ''.join(sl) + s = s.split('\n') + multikey = None + multilinestr = "" + multibackslash = False + for line in s: + if not multilinestr or multibackslash or '\n' not in multilinestr: + line = line.strip() + if line == "" and (not multikey or multibackslash): + continue + if multikey: + if multibackslash: + multilinestr += line + else: + multilinestr += line + multibackslash = False + if len(line) > 2 and (line[-1] == multilinestr[0] and + line[-2] == multilinestr[0] and + line[-3] == multilinestr[0]): + try: + value, vtype = _load_value(multilinestr, _dict) + except ValueError as err: + raise TomlDecodeError(str(err)) + currentlevel[multikey] = value + multikey = None + multilinestr = "" + else: + k = len(multilinestr) - 1 + while k > -1 and multilinestr[k] == '\\': + multibackslash = not multibackslash + k -= 1 + if multibackslash: + multilinestr = multilinestr[:-1] + else: + multilinestr += "\n" + continue + if line[0] == '[': + arrayoftables = False + if len(line) == 1: + raise TomlDecodeError("Opening key group bracket on line by " + "itself.") + if line[1] == '[': + arrayoftables = True + line = line[2:] + splitstr = ']]' + else: + line = line[1:] + splitstr = ']' + i = 1 + quotesplits = _get_split_on_quotes(line) + quoted = False + for quotesplit in quotesplits: + if not quoted and splitstr in quotesplit: + break + i += quotesplit.count(splitstr) + quoted = not quoted + line = line.split(splitstr, i) + if len(line) < i + 1 or line[-1].strip() != "": + raise TomlDecodeError("Key group not on a line by itself.") + groups = splitstr.join(line[:-1]).split('.') + i = 0 + while i < len(groups): + groups[i] = groups[i].strip() + if len(groups[i]) > 0 and (groups[i][0] == '"' or + groups[i][0] == "'"): + groupstr = groups[i] + j = i + 1 + while not groupstr[0] == groupstr[-1]: + j += 1 + if j > len(groups) + 2: + raise TomlDecodeError("Invalid group name '" + + groupstr + "' Something " + + "went wrong.") + groupstr = '.'.join(groups[i:j]).strip() + groups[i] = groupstr[1:-1] + groups[i + 1:j] = [] + else: + if not _groupname_re.match(groups[i]): + raise TomlDecodeError("Invalid group name '" + + groups[i] + "'. Try quoting it.") + i += 1 + currentlevel = retval + for i in _range(len(groups)): + group = groups[i] + if group == "": + raise TomlDecodeError("Can't have a keygroup with an empty " + "name") + try: + currentlevel[group] + if i == len(groups) - 1: + if group in implicitgroups: + implicitgroups.remove(group) + if arrayoftables: + raise TomlDecodeError("An implicitly defined " + "table can't be an array") + elif arrayoftables: + currentlevel[group].append(_dict()) + else: + raise TomlDecodeError("What? " + group + + " already exists?" + + str(currentlevel)) + except TypeError: + currentlevel = currentlevel[-1] + try: + currentlevel[group] + except KeyError: + currentlevel[group] = _dict() + if i == len(groups) - 1 and arrayoftables: + currentlevel[group] = [_dict()] + except KeyError: + if i != len(groups) - 1: + implicitgroups.append(group) + currentlevel[group] = _dict() + if i == len(groups) - 1 and arrayoftables: + currentlevel[group] = [_dict()] + currentlevel = currentlevel[group] + if arrayoftables: + try: + currentlevel = currentlevel[-1] + except KeyError: + pass + elif line[0] == "{": + if line[-1] != "}": + raise TomlDecodeError("Line breaks are not allowed in inline" + "objects") + try: + _load_inline_object(line, currentlevel, _dict, multikey, + multibackslash) + except ValueError as err: + raise TomlDecodeError(str(err)) + elif "=" in line: + try: + ret = _load_line(line, currentlevel, _dict, multikey, + multibackslash) + except ValueError as err: + raise TomlDecodeError(str(err)) + if ret is not None: + multikey, multilinestr, multibackslash = ret + return retval + + +def _load_inline_object(line, currentlevel, _dict, multikey=False, + multibackslash=False): + candidate_groups = line[1:-1].split(",") + groups = [] + if len(candidate_groups) == 1 and not candidate_groups[0].strip(): + candidate_groups.pop() + while len(candidate_groups) > 0: + candidate_group = candidate_groups.pop(0) + try: + _, value = candidate_group.split('=', 1) + except ValueError: + raise ValueError("Invalid inline table encountered") + value = value.strip() + if ((value[0] == value[-1] and value[0] in ('"', "'")) or ( + value[0] in '-0123456789' or + value in ('true', 'false') or + (value[0] == "[" and value[-1] == "]") or + (value[0] == '{' and value[-1] == '}'))): + groups.append(candidate_group) + elif len(candidate_groups) > 0: + candidate_groups[0] = candidate_group + "," + candidate_groups[0] + else: + raise ValueError("Invalid inline table value encountered") + for group in groups: + status = _load_line(group, currentlevel, _dict, multikey, + multibackslash) + if status is not None: + break + + +# Matches a TOML number, which allows underscores for readability +_number_with_underscores = re.compile('([0-9])(_([0-9]))*') + + +def _strictly_valid_num(n): + n = n.strip() + if not n: + return False + if n[0] == '_': + return False + if n[-1] == '_': + return False + if "_." in n or "._" in n: + return False + if len(n) == 1: + return True + if n[0] == '0' and n[1] != '.': + return False + if n[0] == '+' or n[0] == '-': + n = n[1:] + if n[0] == '0' and n[1] != '.': + return False + if '__' in n: + return False + return True + + +def _get_split_on_quotes(line): + doublequotesplits = line.split('"') + quoted = False + quotesplits = [] + if len(doublequotesplits) > 1 and "'" in doublequotesplits[0]: + singlequotesplits = doublequotesplits[0].split("'") + doublequotesplits = doublequotesplits[1:] + while len(singlequotesplits) % 2 == 0 and len(doublequotesplits): + singlequotesplits[-1] += '"' + doublequotesplits[0] + doublequotesplits = doublequotesplits[1:] + if "'" in singlequotesplits[-1]: + singlequotesplits = (singlequotesplits[:-1] + + singlequotesplits[-1].split("'")) + quotesplits += singlequotesplits + for doublequotesplit in doublequotesplits: + if quoted: + quotesplits.append(doublequotesplit) + else: + quotesplits += doublequotesplit.split("'") + quoted = not quoted + return quotesplits + + +def _load_line(line, currentlevel, _dict, multikey, multibackslash): + i = 1 + quotesplits = _get_split_on_quotes(line) + quoted = False + for quotesplit in quotesplits: + if not quoted and '=' in quotesplit: + break + i += quotesplit.count('=') + quoted = not quoted + pair = line.split('=', i) + strictly_valid = _strictly_valid_num(pair[-1]) + if _number_with_underscores.match(pair[-1]): + pair[-1] = pair[-1].replace('_', '') + while len(pair[-1]) and (pair[-1][0] != ' ' and pair[-1][0] != '\t' and + pair[-1][0] != "'" and pair[-1][0] != '"' and + pair[-1][0] != '[' and pair[-1][0] != '{' and + pair[-1] != 'true' and pair[-1] != 'false'): + try: + float(pair[-1]) + break + except ValueError: + pass + if _load_date(pair[-1]) is not None: + break + i += 1 + prev_val = pair[-1] + pair = line.split('=', i) + if prev_val == pair[-1]: + raise ValueError("Invalid date or number") + if strictly_valid: + strictly_valid = _strictly_valid_num(pair[-1]) + pair = ['='.join(pair[:-1]).strip(), pair[-1].strip()] + if (pair[0][0] == '"' or pair[0][0] == "'") and \ + (pair[0][-1] == '"' or pair[0][-1] == "'"): + pair[0] = pair[0][1:-1] + if len(pair[1]) > 2 and ((pair[1][0] == '"' or pair[1][0] == "'") and + pair[1][1] == pair[1][0] and + pair[1][2] == pair[1][0] and + not (len(pair[1]) > 5 and + pair[1][-1] == pair[1][0] and + pair[1][-2] == pair[1][0] and + pair[1][-3] == pair[1][0])): + k = len(pair[1]) - 1 + while k > -1 and pair[1][k] == '\\': + multibackslash = not multibackslash + k -= 1 + if multibackslash: + multilinestr = pair[1][:-1] + else: + multilinestr = pair[1] + "\n" + multikey = pair[0] + else: + value, vtype = _load_value(pair[1], _dict, strictly_valid) + try: + currentlevel[pair[0]] + raise ValueError("Duplicate keys!") + except KeyError: + if multikey: + return multikey, multilinestr, multibackslash + else: + currentlevel[pair[0]] = value + + +def _load_date(val): + microsecond = 0 + tz = None + try: + if len(val) > 19: + if val[19] == '.': + if val[-1].upper() == 'Z': + subsecondval = val[20:-1] + tzval = "Z" + else: + subsecondvalandtz = val[20:] + if '+' in subsecondvalandtz: + splitpoint = subsecondvalandtz.index('+') + subsecondval = subsecondvalandtz[:splitpoint] + tzval = subsecondvalandtz[splitpoint:] + elif '-' in subsecondvalandtz: + splitpoint = subsecondvalandtz.index('-') + subsecondval = subsecondvalandtz[:splitpoint] + tzval = subsecondvalandtz[splitpoint:] + tz = TomlTz(tzval) + microsecond = int(int(subsecondval) * + (10 ** (6 - len(subsecondval)))) + else: + tz = TomlTz(val[19:]) + except ValueError: + tz = None + if "-" not in val[1:]: + return None + try: + d = datetime.datetime( + int(val[:4]), int(val[5:7]), + int(val[8:10]), int(val[11:13]), + int(val[14:16]), int(val[17:19]), microsecond, tz) + except ValueError: + return None + return d + + +def _load_unicode_escapes(v, hexbytes, prefix): + skip = False + i = len(v) - 1 + while i > -1 and v[i] == '\\': + skip = not skip + i -= 1 + for hx in hexbytes: + if skip: + skip = False + i = len(hx) - 1 + while i > -1 and hx[i] == '\\': + skip = not skip + i -= 1 + v += prefix + v += hx + continue + hxb = "" + i = 0 + hxblen = 4 + if prefix == "\\U": + hxblen = 8 + hxb = ''.join(hx[i:i + hxblen]).lower() + if hxb.strip('0123456789abcdef'): + raise ValueError("Invalid escape sequence: " + hxb) + if hxb[0] == "d" and hxb[1].strip('01234567'): + raise ValueError("Invalid escape sequence: " + hxb + + ". Only scalar unicode points are allowed.") + v += unichr(int(hxb, 16)) + v += unicode(hx[len(hxb):]) + return v + + +# Unescape TOML string values. + +# content after the \ +_escapes = ['0', 'b', 'f', 'n', 'r', 't', '"'] +# What it should be replaced by +_escapedchars = ['\0', '\b', '\f', '\n', '\r', '\t', '\"'] +# Used for substitution +_escape_to_escapedchars = dict(zip(_escapes, _escapedchars)) + + +def _unescape(v): + """Unescape characters in a TOML string.""" + i = 0 + backslash = False + while i < len(v): + if backslash: + backslash = False + if v[i] in _escapes: + v = v[:i - 1] + _escape_to_escapedchars[v[i]] + v[i + 1:] + elif v[i] == '\\': + v = v[:i - 1] + v[i:] + elif v[i] == 'u' or v[i] == 'U': + i += 1 + else: + raise ValueError("Reserved escape sequence used") + continue + elif v[i] == '\\': + backslash = True + i += 1 + return v + + +def _load_value(v, _dict, strictly_valid=True): + if not v: + raise ValueError("Empty value is invalid") + if v == 'true': + return (True, "bool") + elif v == 'false': + return (False, "bool") + elif v[0] == '"': + testv = v[1:].split('"') + triplequote = False + triplequotecount = 0 + if len(testv) > 1 and testv[0] == '' and testv[1] == '': + testv = testv[2:] + triplequote = True + closed = False + for tv in testv: + if tv == '': + if triplequote: + triplequotecount += 1 + else: + closed = True + else: + oddbackslash = False + try: + i = -1 + j = tv[i] + while j == '\\': + oddbackslash = not oddbackslash + i -= 1 + j = tv[i] + except IndexError: + pass + if not oddbackslash: + if closed: + raise ValueError("Stuff after closed string. WTF?") + else: + if not triplequote or triplequotecount > 1: + closed = True + else: + triplequotecount = 0 + escapeseqs = v.split('\\')[1:] + backslash = False + for i in escapeseqs: + if i == '': + backslash = not backslash + else: + if i[0] not in _escapes and (i[0] != 'u' and i[0] != 'U' and + not backslash): + raise ValueError("Reserved escape sequence used") + if backslash: + backslash = False + for prefix in ["\\u", "\\U"]: + if prefix in v: + hexbytes = v.split(prefix) + v = _load_unicode_escapes(hexbytes[0], hexbytes[1:], prefix) + v = _unescape(v) + if len(v) > 1 and v[1] == '"' and (len(v) < 3 or v[1] == v[2]): + v = v[2:-2] + return (v[1:-1], "str") + elif v[0] == "'": + if v[1] == "'" and (len(v) < 3 or v[1] == v[2]): + v = v[2:-2] + return (v[1:-1], "str") + elif v[0] == '[': + return (_load_array(v, _dict), "array") + elif v[0] == '{': + inline_object = _get_empty_inline_table(_dict) + _load_inline_object(v, inline_object, _dict) + return (inline_object, "inline_object") + else: + parsed_date = _load_date(v) + if parsed_date is not None: + return (parsed_date, "date") + if not strictly_valid: + raise ValueError("Weirdness with leading zeroes or " + "underscores in your number.") + itype = "int" + neg = False + if v[0] == '-': + neg = True + v = v[1:] + elif v[0] == '+': + v = v[1:] + v = v.replace('_', '') + if '.' in v or 'e' in v or 'E' in v: + if '.' in v and v.split('.', 1)[1] == '': + raise ValueError("This float is missing digits after " + "the point") + if v[0] not in '0123456789': + raise ValueError("This float doesn't have a leading digit") + v = float(v) + itype = "float" + else: + v = int(v) + if neg: + return (0 - v, itype) + return (v, itype) + + +def _bounded_string(s): + if len(s) == 0: + return True + if s[-1] != s[0]: + return False + i = -2 + backslash = False + while len(s) + i > 0: + if s[i] == "\\": + backslash = not backslash + i -= 1 + else: + break + return not backslash + + +def _load_array(a, _dict): + atype = None + retval = [] + a = a.strip() + if '[' not in a[1:-1] or "" != a[1:-1].split('[')[0].strip(): + strarray = False + tmpa = a[1:-1].strip() + if tmpa != '' and (tmpa[0] == '"' or tmpa[0] == "'"): + strarray = True + if not a[1:-1].strip().startswith('{'): + a = a[1:-1].split(',') + else: + # a is an inline object, we must find the matching parenthesis + # to define groups + new_a = [] + start_group_index = 1 + end_group_index = 2 + in_str = False + while end_group_index < len(a[1:]): + if a[end_group_index] == '"' or a[end_group_index] == "'": + if in_str: + backslash_index = end_group_index - 1 + while (backslash_index > -1 and + a[backslash_index] == '\\'): + in_str = not in_str + backslash_index -= 1 + in_str = not in_str + if in_str or a[end_group_index] != '}': + end_group_index += 1 + continue + + # Increase end_group_index by 1 to get the closing bracket + end_group_index += 1 + new_a.append(a[start_group_index:end_group_index]) + + # The next start index is at least after the closing bracket, a + # closing bracket can be followed by a comma since we are in + # an array. + start_group_index = end_group_index + 1 + while (start_group_index < len(a[1:]) and + a[start_group_index] != '{'): + start_group_index += 1 + end_group_index = start_group_index + 1 + a = new_a + b = 0 + if strarray: + while b < len(a) - 1: + ab = a[b].strip() + while (not _bounded_string(ab) or + (len(ab) > 2 and + ab[0] == ab[1] == ab[2] and + ab[-2] != ab[0] and + ab[-3] != ab[0])): + a[b] = a[b] + ',' + a[b + 1] + ab = a[b].strip() + if b < len(a) - 2: + a = a[:b + 1] + a[b + 2:] + else: + a = a[:b + 1] + b += 1 + else: + al = list(a[1:-1]) + a = [] + openarr = 0 + j = 0 + for i in _range(len(al)): + if al[i] == '[': + openarr += 1 + elif al[i] == ']': + openarr -= 1 + elif al[i] == ',' and not openarr: + a.append(''.join(al[j:i])) + j = i + 1 + a.append(''.join(al[j:])) + for i in _range(len(a)): + a[i] = a[i].strip() + if a[i] != '': + nval, ntype = _load_value(a[i], _dict) + if atype: + if ntype != atype: + raise ValueError("Not a homogeneous array") + else: + atype = ntype + retval.append(nval) + return retval + + +def dump(o, f): + """Writes out dict as toml to a file + + Args: + o: Object to dump into toml + f: File descriptor where the toml should be stored + + Returns: + String containing the toml corresponding to dictionary + + Raises: + TypeError: When anything other than file descriptor is passed + """ + + if not f.write: + raise TypeError("You can only dump an object to a file descriptor") + d = dumps(o) + f.write(d) + return d + + +def dumps(o, preserve=False): + """Stringifies input dict as toml + + Args: + o: Object to dump into toml + + preserve: Boolean parameter. If true, preserve inline tables. + + Returns: + String containing the toml corresponding to dict + """ + + retval = "" + addtoretval, sections = _dump_sections(o, "") + retval += addtoretval + while sections != {}: + newsections = {} + for section in sections: + addtoretval, addtosections = _dump_sections(sections[section], + section, preserve) + if addtoretval or (not addtoretval and not addtosections): + if retval and retval[-2:] != "\n\n": + retval += "\n" + retval += "[" + section + "]\n" + if addtoretval: + retval += addtoretval + for s in addtosections: + newsections[section + "." + s] = addtosections[s] + sections = newsections + return retval + + +def _dump_sections(o, sup, preserve=False): + retstr = "" + if sup != "" and sup[-1] != ".": + sup += '.' + retdict = o.__class__() + arraystr = "" + for section in o: + section = unicode(section) + qsection = section + if not re.match(r'^[A-Za-z0-9_-]+$', section): + if '"' in section: + qsection = "'" + section + "'" + else: + qsection = '"' + section + '"' + if not isinstance(o[section], dict): + arrayoftables = False + if isinstance(o[section], list): + for a in o[section]: + if isinstance(a, dict): + arrayoftables = True + if arrayoftables: + for a in o[section]: + arraytabstr = "\n" + arraystr += "[[" + sup + qsection + "]]\n" + s, d = _dump_sections(a, sup + qsection) + if s: + if s[0] == "[": + arraytabstr += s + else: + arraystr += s + while d != {}: + newd = {} + for dsec in d: + s1, d1 = _dump_sections(d[dsec], sup + qsection + + "." + dsec) + if s1: + arraytabstr += ("[" + sup + qsection + "." + + dsec + "]\n") + arraytabstr += s1 + for s1 in d1: + newd[dsec + "." + s1] = d1[s1] + d = newd + arraystr += arraytabstr + else: + if o[section] is not None: + retstr += (qsection + " = " + + unicode(_dump_value(o[section])) + '\n') + elif preserve and isinstance(o[section], InlineTableDict): + retstr += (qsection + " = " + _dump_inline_table(o[section])) + else: + retdict[qsection] = o[section] + retstr += arraystr + return (retstr, retdict) + + +def _dump_inline_table(section): + """Preserve inline table in its compact syntax instead of expanding + into subsection. + + https://github.com/toml-lang/toml#user-content-inline-table + """ + retval = "" + if isinstance(section, dict): + val_list = [] + for k, v in section.items(): + val = _dump_inline_table(v) + val_list.append(k + " = " + val) + retval += "{ " + ", ".join(val_list) + " }\n" + return retval + else: + return unicode(_dump_value(section)) + + +def _dump_value(v): + dump_funcs = { + str: _dump_str, + unicode: _dump_str, + list: _dump_list, + int: lambda v: v, + bool: lambda v: unicode(v).lower(), + float: _dump_float, + datetime.datetime: lambda v: v.isoformat().replace('+00:00', 'Z'), + } + # Lookup function corresponding to v's type + dump_fn = dump_funcs.get(type(v)) + if dump_fn is None and hasattr(v, '__iter__'): + dump_fn = dump_funcs[list] + # Evaluate function (if it exists) else return v + return dump_fn(v) if dump_fn is not None else dump_funcs[str](v) + + +def _dump_str(v): + if sys.version_info < (3,) and hasattr(v, 'decode') and isinstance(v, str): + v = v.decode('utf-8') + v = "%r" % v + if v[0] == 'u': + v = v[1:] + singlequote = v.startswith("'") + if singlequote or v.startswith('"'): + v = v[1:-1] + if singlequote: + v = v.replace("\\'", "'") + v = v.replace('"', '\\"') + v = v.split("\\x") + while len(v) > 1: + i = -1 + if not v[0]: + v = v[1:] + v[0] = v[0].replace("\\\\", "\\") + # No, I don't know why != works and == breaks + joinx = v[0][i] != "\\" + while v[0][:i] and v[0][i] == "\\": + joinx = not joinx + i -= 1 + if joinx: + joiner = "x" + else: + joiner = "u00" + v = [v[0] + joiner + v[1]] + v[2:] + return unicode('"' + v[0] + '"') + + +def _dump_list(v): + retval = "[" + for u in v: + retval += " " + unicode(_dump_value(u)) + "," + retval += "]" + return retval + + +def _dump_float(v): + return "{0:.16}".format(v).replace("e+0", "e+").replace("e-0", "e-") diff --git a/src/pip/_vendor/toml.pyi b/src/pip/_vendor/toml.pyi new file mode 100644 index 00000000000..018a1ad1061 --- /dev/null +++ b/src/pip/_vendor/toml.pyi @@ -0,0 +1 @@ +from toml import * \ No newline at end of file diff --git a/src/pip/_vendor/toml/LICENSE b/src/pip/_vendor/toml/LICENSE new file mode 100644 index 00000000000..08e981ffacf --- /dev/null +++ b/src/pip/_vendor/toml/LICENSE @@ -0,0 +1,26 @@ +The MIT License + +Copyright 2013-2018 William Pearson +Copyright 2015-2016 Julien Enselme +Copyright 2016 Google Inc. +Copyright 2017 Samuel Vasko +Copyright 2017 Nate Prewitt +Copyright 2017 Jack Evans + +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. \ No newline at end of file diff --git a/src/pip/_vendor/toml/__init__.py b/src/pip/_vendor/toml/__init__.py new file mode 100644 index 00000000000..015d73cbe4e --- /dev/null +++ b/src/pip/_vendor/toml/__init__.py @@ -0,0 +1,21 @@ +"""Python module which parses and emits TOML. + +Released under the MIT license. +""" + +from pip._vendor.toml import encoder +from pip._vendor.toml import decoder + +__version__ = "0.10.0" +_spec_ = "0.5.0" + +load = decoder.load +loads = decoder.loads +TomlDecoder = decoder.TomlDecoder +TomlDecodeError = decoder.TomlDecodeError + +dump = encoder.dump +dumps = encoder.dumps +TomlEncoder = encoder.TomlEncoder +TomlArraySeparatorEncoder = encoder.TomlArraySeparatorEncoder +TomlPreserveInlineDictEncoder = encoder.TomlPreserveInlineDictEncoder diff --git a/src/pip/_vendor/toml/decoder.py b/src/pip/_vendor/toml/decoder.py new file mode 100644 index 00000000000..20be459122d --- /dev/null +++ b/src/pip/_vendor/toml/decoder.py @@ -0,0 +1,945 @@ +import datetime +import io +from os import linesep +import re +import sys + +from pip._vendor.toml.tz import TomlTz + +if sys.version_info < (3,): + _range = xrange # noqa: F821 +else: + unicode = str + _range = range + basestring = str + unichr = chr + + +def _detect_pathlib_path(p): + if (3, 4) <= sys.version_info: + import pathlib + if isinstance(p, pathlib.PurePath): + return True + return False + + +def _ispath(p): + if isinstance(p, basestring): + return True + return _detect_pathlib_path(p) + + +def _getpath(p): + if (3, 6) <= sys.version_info: + import os + return os.fspath(p) + if _detect_pathlib_path(p): + return str(p) + return p + + +try: + FNFError = FileNotFoundError +except NameError: + FNFError = IOError + + +TIME_RE = re.compile("([0-9]{2}):([0-9]{2}):([0-9]{2})(\.([0-9]{3,6}))?") + + +class TomlDecodeError(ValueError): + """Base toml Exception / Error.""" + + def __init__(self, msg, doc, pos): + lineno = doc.count('\n', 0, pos) + 1 + colno = pos - doc.rfind('\n', 0, pos) + emsg = '{} (line {} column {} char {})'.format(msg, lineno, colno, pos) + ValueError.__init__(self, emsg) + self.msg = msg + self.doc = doc + self.pos = pos + self.lineno = lineno + self.colno = colno + + +# Matches a TOML number, which allows underscores for readability +_number_with_underscores = re.compile('([0-9])(_([0-9]))*') + + +def _strictly_valid_num(n): + n = n.strip() + if not n: + return False + if n[0] == '_': + return False + if n[-1] == '_': + return False + if "_." in n or "._" in n: + return False + if len(n) == 1: + return True + if n[0] == '0' and n[1] not in ['.', 'o', 'b', 'x']: + return False + if n[0] == '+' or n[0] == '-': + n = n[1:] + if len(n) > 1 and n[0] == '0' and n[1] != '.': + return False + if '__' in n: + return False + return True + + +def load(f, _dict=dict, decoder=None): + """Parses named file or files as toml and returns a dictionary + + Args: + f: Path to the file to open, array of files to read into single dict + or a file descriptor + _dict: (optional) Specifies the class of the returned toml dictionary + + Returns: + Parsed toml file represented as a dictionary + + Raises: + TypeError -- When f is invalid type + TomlDecodeError: Error while decoding toml + IOError / FileNotFoundError -- When an array with no valid (existing) + (Python 2 / Python 3) file paths is passed + """ + + if _ispath(f): + with io.open(_getpath(f), encoding='utf-8') as ffile: + return loads(ffile.read(), _dict, decoder) + elif isinstance(f, list): + from os import path as op + from warnings import warn + if not [path for path in f if op.exists(path)]: + error_msg = "Load expects a list to contain filenames only." + error_msg += linesep + error_msg += ("The list needs to contain the path of at least one " + "existing file.") + raise FNFError(error_msg) + if decoder is None: + decoder = TomlDecoder() + d = decoder.get_empty_table() + for l in f: + if op.exists(l): + d.update(load(l, _dict, decoder)) + else: + warn("Non-existent filename in list with at least one valid " + "filename") + return d + else: + try: + return loads(f.read(), _dict, decoder) + except AttributeError: + raise TypeError("You can only load a file descriptor, filename or " + "list") + + +_groupname_re = re.compile(r'^[A-Za-z0-9_-]+$') + + +def loads(s, _dict=dict, decoder=None): + """Parses string as toml + + Args: + s: String to be parsed + _dict: (optional) Specifies the class of the returned toml dictionary + + Returns: + Parsed toml file represented as a dictionary + + Raises: + TypeError: When a non-string is passed + TomlDecodeError: Error while decoding toml + """ + + implicitgroups = [] + if decoder is None: + decoder = TomlDecoder(_dict) + retval = decoder.get_empty_table() + currentlevel = retval + if not isinstance(s, basestring): + raise TypeError("Expecting something like a string") + + if not isinstance(s, unicode): + s = s.decode('utf8') + + original = s + sl = list(s) + openarr = 0 + openstring = False + openstrchar = "" + multilinestr = False + arrayoftables = False + beginline = True + keygroup = False + dottedkey = False + keyname = 0 + for i, item in enumerate(sl): + if item == '\r' and sl[i + 1] == '\n': + sl[i] = ' ' + continue + if keyname: + if item == '\n': + raise TomlDecodeError("Key name found without value." + " Reached end of line.", original, i) + if openstring: + if item == openstrchar: + keyname = 2 + openstring = False + openstrchar = "" + continue + elif keyname == 1: + if item.isspace(): + keyname = 2 + continue + elif item == '.': + dottedkey = True + continue + elif item.isalnum() or item == '_' or item == '-': + continue + elif (dottedkey and sl[i - 1] == '.' and + (item == '"' or item == "'")): + openstring = True + openstrchar = item + continue + elif keyname == 2: + if item.isspace(): + if dottedkey: + nextitem = sl[i + 1] + if not nextitem.isspace() and nextitem != '.': + keyname = 1 + continue + if item == '.': + dottedkey = True + nextitem = sl[i + 1] + if not nextitem.isspace() and nextitem != '.': + keyname = 1 + continue + if item == '=': + keyname = 0 + dottedkey = False + else: + raise TomlDecodeError("Found invalid character in key name: '" + + item + "'. Try quoting the key name.", + original, i) + if item == "'" and openstrchar != '"': + k = 1 + try: + while sl[i - k] == "'": + k += 1 + if k == 3: + break + except IndexError: + pass + if k == 3: + multilinestr = not multilinestr + openstring = multilinestr + else: + openstring = not openstring + if openstring: + openstrchar = "'" + else: + openstrchar = "" + if item == '"' and openstrchar != "'": + oddbackslash = False + k = 1 + tripquote = False + try: + while sl[i - k] == '"': + k += 1 + if k == 3: + tripquote = True + break + if k == 1 or (k == 3 and tripquote): + while sl[i - k] == '\\': + oddbackslash = not oddbackslash + k += 1 + except IndexError: + pass + if not oddbackslash: + if tripquote: + multilinestr = not multilinestr + openstring = multilinestr + else: + openstring = not openstring + if openstring: + openstrchar = '"' + else: + openstrchar = "" + if item == '#' and (not openstring and not keygroup and + not arrayoftables): + j = i + try: + while sl[j] != '\n': + sl[j] = ' ' + j += 1 + except IndexError: + break + if item == '[' and (not openstring and not keygroup and + not arrayoftables): + if beginline: + if len(sl) > i + 1 and sl[i + 1] == '[': + arrayoftables = True + else: + keygroup = True + else: + openarr += 1 + if item == ']' and not openstring: + if keygroup: + keygroup = False + elif arrayoftables: + if sl[i - 1] == ']': + arrayoftables = False + else: + openarr -= 1 + if item == '\n': + if openstring or multilinestr: + if not multilinestr: + raise TomlDecodeError("Unbalanced quotes", original, i) + if ((sl[i - 1] == "'" or sl[i - 1] == '"') and ( + sl[i - 2] == sl[i - 1])): + sl[i] = sl[i - 1] + if sl[i - 3] == sl[i - 1]: + sl[i - 3] = ' ' + elif openarr: + sl[i] = ' ' + else: + beginline = True + elif beginline and sl[i] != ' ' and sl[i] != '\t': + beginline = False + if not keygroup and not arrayoftables: + if sl[i] == '=': + raise TomlDecodeError("Found empty keyname. ", original, i) + keyname = 1 + s = ''.join(sl) + s = s.split('\n') + multikey = None + multilinestr = "" + multibackslash = False + pos = 0 + for idx, line in enumerate(s): + if idx > 0: + pos += len(s[idx - 1]) + 1 + if not multilinestr or multibackslash or '\n' not in multilinestr: + line = line.strip() + if line == "" and (not multikey or multibackslash): + continue + if multikey: + if multibackslash: + multilinestr += line + else: + multilinestr += line + multibackslash = False + if len(line) > 2 and (line[-1] == multilinestr[0] and + line[-2] == multilinestr[0] and + line[-3] == multilinestr[0]): + try: + value, vtype = decoder.load_value(multilinestr) + except ValueError as err: + raise TomlDecodeError(str(err), original, pos) + currentlevel[multikey] = value + multikey = None + multilinestr = "" + else: + k = len(multilinestr) - 1 + while k > -1 and multilinestr[k] == '\\': + multibackslash = not multibackslash + k -= 1 + if multibackslash: + multilinestr = multilinestr[:-1] + else: + multilinestr += "\n" + continue + if line[0] == '[': + arrayoftables = False + if len(line) == 1: + raise TomlDecodeError("Opening key group bracket on line by " + "itself.", original, pos) + if line[1] == '[': + arrayoftables = True + line = line[2:] + splitstr = ']]' + else: + line = line[1:] + splitstr = ']' + i = 1 + quotesplits = decoder._get_split_on_quotes(line) + quoted = False + for quotesplit in quotesplits: + if not quoted and splitstr in quotesplit: + break + i += quotesplit.count(splitstr) + quoted = not quoted + line = line.split(splitstr, i) + if len(line) < i + 1 or line[-1].strip() != "": + raise TomlDecodeError("Key group not on a line by itself.", + original, pos) + groups = splitstr.join(line[:-1]).split('.') + i = 0 + while i < len(groups): + groups[i] = groups[i].strip() + if len(groups[i]) > 0 and (groups[i][0] == '"' or + groups[i][0] == "'"): + groupstr = groups[i] + j = i + 1 + while not groupstr[0] == groupstr[-1]: + j += 1 + if j > len(groups) + 2: + raise TomlDecodeError("Invalid group name '" + + groupstr + "' Something " + + "went wrong.", original, pos) + groupstr = '.'.join(groups[i:j]).strip() + groups[i] = groupstr[1:-1] + groups[i + 1:j] = [] + else: + if not _groupname_re.match(groups[i]): + raise TomlDecodeError("Invalid group name '" + + groups[i] + "'. Try quoting it.", + original, pos) + i += 1 + currentlevel = retval + for i in _range(len(groups)): + group = groups[i] + if group == "": + raise TomlDecodeError("Can't have a keygroup with an empty " + "name", original, pos) + try: + currentlevel[group] + if i == len(groups) - 1: + if group in implicitgroups: + implicitgroups.remove(group) + if arrayoftables: + raise TomlDecodeError("An implicitly defined " + "table can't be an array", + original, pos) + elif arrayoftables: + currentlevel[group].append(decoder.get_empty_table() + ) + else: + raise TomlDecodeError("What? " + group + + " already exists?" + + str(currentlevel), + original, pos) + except TypeError: + currentlevel = currentlevel[-1] + if group not in currentlevel: + currentlevel[group] = decoder.get_empty_table() + if i == len(groups) - 1 and arrayoftables: + currentlevel[group] = [decoder.get_empty_table()] + except KeyError: + if i != len(groups) - 1: + implicitgroups.append(group) + currentlevel[group] = decoder.get_empty_table() + if i == len(groups) - 1 and arrayoftables: + currentlevel[group] = [decoder.get_empty_table()] + currentlevel = currentlevel[group] + if arrayoftables: + try: + currentlevel = currentlevel[-1] + except KeyError: + pass + elif line[0] == "{": + if line[-1] != "}": + raise TomlDecodeError("Line breaks are not allowed in inline" + "objects", original, pos) + try: + decoder.load_inline_object(line, currentlevel, multikey, + multibackslash) + except ValueError as err: + raise TomlDecodeError(str(err), original, pos) + elif "=" in line: + try: + ret = decoder.load_line(line, currentlevel, multikey, + multibackslash) + except ValueError as err: + raise TomlDecodeError(str(err), original, pos) + if ret is not None: + multikey, multilinestr, multibackslash = ret + return retval + + +def _load_date(val): + microsecond = 0 + tz = None + try: + if len(val) > 19: + if val[19] == '.': + if val[-1].upper() == 'Z': + subsecondval = val[20:-1] + tzval = "Z" + else: + subsecondvalandtz = val[20:] + if '+' in subsecondvalandtz: + splitpoint = subsecondvalandtz.index('+') + subsecondval = subsecondvalandtz[:splitpoint] + tzval = subsecondvalandtz[splitpoint:] + elif '-' in subsecondvalandtz: + splitpoint = subsecondvalandtz.index('-') + subsecondval = subsecondvalandtz[:splitpoint] + tzval = subsecondvalandtz[splitpoint:] + else: + tzval = None + subsecondval = subsecondvalandtz + if tzval is not None: + tz = TomlTz(tzval) + microsecond = int(int(subsecondval) * + (10 ** (6 - len(subsecondval)))) + else: + tz = TomlTz(val[19:]) + except ValueError: + tz = None + if "-" not in val[1:]: + return None + try: + if len(val) == 10: + d = datetime.date( + int(val[:4]), int(val[5:7]), + int(val[8:10])) + else: + d = datetime.datetime( + int(val[:4]), int(val[5:7]), + int(val[8:10]), int(val[11:13]), + int(val[14:16]), int(val[17:19]), microsecond, tz) + except ValueError: + return None + return d + + +def _load_unicode_escapes(v, hexbytes, prefix): + skip = False + i = len(v) - 1 + while i > -1 and v[i] == '\\': + skip = not skip + i -= 1 + for hx in hexbytes: + if skip: + skip = False + i = len(hx) - 1 + while i > -1 and hx[i] == '\\': + skip = not skip + i -= 1 + v += prefix + v += hx + continue + hxb = "" + i = 0 + hxblen = 4 + if prefix == "\\U": + hxblen = 8 + hxb = ''.join(hx[i:i + hxblen]).lower() + if hxb.strip('0123456789abcdef'): + raise ValueError("Invalid escape sequence: " + hxb) + if hxb[0] == "d" and hxb[1].strip('01234567'): + raise ValueError("Invalid escape sequence: " + hxb + + ". Only scalar unicode points are allowed.") + v += unichr(int(hxb, 16)) + v += unicode(hx[len(hxb):]) + return v + + +# Unescape TOML string values. + +# content after the \ +_escapes = ['0', 'b', 'f', 'n', 'r', 't', '"'] +# What it should be replaced by +_escapedchars = ['\0', '\b', '\f', '\n', '\r', '\t', '\"'] +# Used for substitution +_escape_to_escapedchars = dict(zip(_escapes, _escapedchars)) + + +def _unescape(v): + """Unescape characters in a TOML string.""" + i = 0 + backslash = False + while i < len(v): + if backslash: + backslash = False + if v[i] in _escapes: + v = v[:i - 1] + _escape_to_escapedchars[v[i]] + v[i + 1:] + elif v[i] == '\\': + v = v[:i - 1] + v[i:] + elif v[i] == 'u' or v[i] == 'U': + i += 1 + else: + raise ValueError("Reserved escape sequence used") + continue + elif v[i] == '\\': + backslash = True + i += 1 + return v + + +class InlineTableDict(object): + """Sentinel subclass of dict for inline tables.""" + + +class TomlDecoder(object): + + def __init__(self, _dict=dict): + self._dict = _dict + + def get_empty_table(self): + return self._dict() + + def get_empty_inline_table(self): + class DynamicInlineTableDict(self._dict, InlineTableDict): + """Concrete sentinel subclass for inline tables. + It is a subclass of _dict which is passed in dynamically at load + time + + It is also a subclass of InlineTableDict + """ + + return DynamicInlineTableDict() + + def load_inline_object(self, line, currentlevel, multikey=False, + multibackslash=False): + candidate_groups = line[1:-1].split(",") + groups = [] + if len(candidate_groups) == 1 and not candidate_groups[0].strip(): + candidate_groups.pop() + while len(candidate_groups) > 0: + candidate_group = candidate_groups.pop(0) + try: + _, value = candidate_group.split('=', 1) + except ValueError: + raise ValueError("Invalid inline table encountered") + value = value.strip() + if ((value[0] == value[-1] and value[0] in ('"', "'")) or ( + value[0] in '-0123456789' or + value in ('true', 'false') or + (value[0] == "[" and value[-1] == "]") or + (value[0] == '{' and value[-1] == '}'))): + groups.append(candidate_group) + elif len(candidate_groups) > 0: + candidate_groups[0] = (candidate_group + "," + + candidate_groups[0]) + else: + raise ValueError("Invalid inline table value encountered") + for group in groups: + status = self.load_line(group, currentlevel, multikey, + multibackslash) + if status is not None: + break + + def _get_split_on_quotes(self, line): + doublequotesplits = line.split('"') + quoted = False + quotesplits = [] + if len(doublequotesplits) > 1 and "'" in doublequotesplits[0]: + singlequotesplits = doublequotesplits[0].split("'") + doublequotesplits = doublequotesplits[1:] + while len(singlequotesplits) % 2 == 0 and len(doublequotesplits): + singlequotesplits[-1] += '"' + doublequotesplits[0] + doublequotesplits = doublequotesplits[1:] + if "'" in singlequotesplits[-1]: + singlequotesplits = (singlequotesplits[:-1] + + singlequotesplits[-1].split("'")) + quotesplits += singlequotesplits + for doublequotesplit in doublequotesplits: + if quoted: + quotesplits.append(doublequotesplit) + else: + quotesplits += doublequotesplit.split("'") + quoted = not quoted + return quotesplits + + def load_line(self, line, currentlevel, multikey, multibackslash): + i = 1 + quotesplits = self._get_split_on_quotes(line) + quoted = False + for quotesplit in quotesplits: + if not quoted and '=' in quotesplit: + break + i += quotesplit.count('=') + quoted = not quoted + pair = line.split('=', i) + strictly_valid = _strictly_valid_num(pair[-1]) + if _number_with_underscores.match(pair[-1]): + pair[-1] = pair[-1].replace('_', '') + while len(pair[-1]) and (pair[-1][0] != ' ' and pair[-1][0] != '\t' and + pair[-1][0] != "'" and pair[-1][0] != '"' and + pair[-1][0] != '[' and pair[-1][0] != '{' and + pair[-1] != 'true' and pair[-1] != 'false'): + try: + float(pair[-1]) + break + except ValueError: + pass + if _load_date(pair[-1]) is not None: + break + i += 1 + prev_val = pair[-1] + pair = line.split('=', i) + if prev_val == pair[-1]: + raise ValueError("Invalid date or number") + if strictly_valid: + strictly_valid = _strictly_valid_num(pair[-1]) + pair = ['='.join(pair[:-1]).strip(), pair[-1].strip()] + if '.' in pair[0]: + if '"' in pair[0] or "'" in pair[0]: + quotesplits = self._get_split_on_quotes(pair[0]) + quoted = False + levels = [] + for quotesplit in quotesplits: + if quoted: + levels.append(quotesplit) + else: + levels += [level.strip() for level in + quotesplit.split('.')] + quoted = not quoted + else: + levels = pair[0].split('.') + while levels[-1] == "": + levels = levels[:-1] + for level in levels[:-1]: + if level == "": + continue + if level not in currentlevel: + currentlevel[level] = self.get_empty_table() + currentlevel = currentlevel[level] + pair[0] = levels[-1].strip() + elif (pair[0][0] == '"' or pair[0][0] == "'") and \ + (pair[0][-1] == pair[0][0]): + pair[0] = pair[0][1:-1] + if len(pair[1]) > 2 and ((pair[1][0] == '"' or pair[1][0] == "'") and + pair[1][1] == pair[1][0] and + pair[1][2] == pair[1][0] and + not (len(pair[1]) > 5 and + pair[1][-1] == pair[1][0] and + pair[1][-2] == pair[1][0] and + pair[1][-3] == pair[1][0])): + k = len(pair[1]) - 1 + while k > -1 and pair[1][k] == '\\': + multibackslash = not multibackslash + k -= 1 + if multibackslash: + multilinestr = pair[1][:-1] + else: + multilinestr = pair[1] + "\n" + multikey = pair[0] + else: + value, vtype = self.load_value(pair[1], strictly_valid) + try: + currentlevel[pair[0]] + raise ValueError("Duplicate keys!") + except TypeError: + raise ValueError("Duplicate keys!") + except KeyError: + if multikey: + return multikey, multilinestr, multibackslash + else: + currentlevel[pair[0]] = value + + def load_value(self, v, strictly_valid=True): + if not v: + raise ValueError("Empty value is invalid") + if v == 'true': + return (True, "bool") + elif v == 'false': + return (False, "bool") + elif v[0] == '"' or v[0] == "'": + quotechar = v[0] + testv = v[1:].split(quotechar) + triplequote = False + triplequotecount = 0 + if len(testv) > 1 and testv[0] == '' and testv[1] == '': + testv = testv[2:] + triplequote = True + closed = False + for tv in testv: + if tv == '': + if triplequote: + triplequotecount += 1 + else: + closed = True + else: + oddbackslash = False + try: + i = -1 + j = tv[i] + while j == '\\': + oddbackslash = not oddbackslash + i -= 1 + j = tv[i] + except IndexError: + pass + if not oddbackslash: + if closed: + raise ValueError("Stuff after closed string. WTF?") + else: + if not triplequote or triplequotecount > 1: + closed = True + else: + triplequotecount = 0 + if quotechar == '"': + escapeseqs = v.split('\\')[1:] + backslash = False + for i in escapeseqs: + if i == '': + backslash = not backslash + else: + if i[0] not in _escapes and (i[0] != 'u' and + i[0] != 'U' and + not backslash): + raise ValueError("Reserved escape sequence used") + if backslash: + backslash = False + for prefix in ["\\u", "\\U"]: + if prefix in v: + hexbytes = v.split(prefix) + v = _load_unicode_escapes(hexbytes[0], hexbytes[1:], + prefix) + v = _unescape(v) + if len(v) > 1 and v[1] == quotechar and (len(v) < 3 or + v[1] == v[2]): + v = v[2:-2] + return (v[1:-1], "str") + elif v[0] == '[': + return (self.load_array(v), "array") + elif v[0] == '{': + inline_object = self.get_empty_inline_table() + self.load_inline_object(v, inline_object) + return (inline_object, "inline_object") + elif TIME_RE.match(v): + h, m, s, _, ms = TIME_RE.match(v).groups() + time = datetime.time(int(h), int(m), int(s), int(ms) if ms else 0) + return (time, "time") + else: + parsed_date = _load_date(v) + if parsed_date is not None: + return (parsed_date, "date") + if not strictly_valid: + raise ValueError("Weirdness with leading zeroes or " + "underscores in your number.") + itype = "int" + neg = False + if v[0] == '-': + neg = True + v = v[1:] + elif v[0] == '+': + v = v[1:] + v = v.replace('_', '') + lowerv = v.lower() + if '.' in v or ('x' not in v and ('e' in v or 'E' in v)): + if '.' in v and v.split('.', 1)[1] == '': + raise ValueError("This float is missing digits after " + "the point") + if v[0] not in '0123456789': + raise ValueError("This float doesn't have a leading " + "digit") + v = float(v) + itype = "float" + elif len(lowerv) == 3 and (lowerv == 'inf' or lowerv == 'nan'): + v = float(v) + itype = "float" + if itype == "int": + v = int(v, 0) + if neg: + return (0 - v, itype) + return (v, itype) + + def bounded_string(self, s): + if len(s) == 0: + return True + if s[-1] != s[0]: + return False + i = -2 + backslash = False + while len(s) + i > 0: + if s[i] == "\\": + backslash = not backslash + i -= 1 + else: + break + return not backslash + + def load_array(self, a): + atype = None + retval = [] + a = a.strip() + if '[' not in a[1:-1] or "" != a[1:-1].split('[')[0].strip(): + strarray = False + tmpa = a[1:-1].strip() + if tmpa != '' and (tmpa[0] == '"' or tmpa[0] == "'"): + strarray = True + if not a[1:-1].strip().startswith('{'): + a = a[1:-1].split(',') + else: + # a is an inline object, we must find the matching parenthesis + # to define groups + new_a = [] + start_group_index = 1 + end_group_index = 2 + in_str = False + while end_group_index < len(a[1:]): + if a[end_group_index] == '"' or a[end_group_index] == "'": + if in_str: + backslash_index = end_group_index - 1 + while (backslash_index > -1 and + a[backslash_index] == '\\'): + in_str = not in_str + backslash_index -= 1 + in_str = not in_str + if in_str or a[end_group_index] != '}': + end_group_index += 1 + continue + + # Increase end_group_index by 1 to get the closing bracket + end_group_index += 1 + + new_a.append(a[start_group_index:end_group_index]) + + # The next start index is at least after the closing + # bracket, a closing bracket can be followed by a comma + # since we are in an array. + start_group_index = end_group_index + 1 + while (start_group_index < len(a[1:]) and + a[start_group_index] != '{'): + start_group_index += 1 + end_group_index = start_group_index + 1 + a = new_a + b = 0 + if strarray: + while b < len(a) - 1: + ab = a[b].strip() + while (not self.bounded_string(ab) or + (len(ab) > 2 and + ab[0] == ab[1] == ab[2] and + ab[-2] != ab[0] and + ab[-3] != ab[0])): + a[b] = a[b] + ',' + a[b + 1] + ab = a[b].strip() + if b < len(a) - 2: + a = a[:b + 1] + a[b + 2:] + else: + a = a[:b + 1] + b += 1 + else: + al = list(a[1:-1]) + a = [] + openarr = 0 + j = 0 + for i in _range(len(al)): + if al[i] == '[': + openarr += 1 + elif al[i] == ']': + openarr -= 1 + elif al[i] == ',' and not openarr: + a.append(''.join(al[j:i])) + j = i + 1 + a.append(''.join(al[j:])) + for i in _range(len(a)): + a[i] = a[i].strip() + if a[i] != '': + nval, ntype = self.load_value(a[i]) + if atype: + if ntype != atype: + raise ValueError("Not a homogeneous array") + else: + atype = ntype + retval.append(nval) + return retval diff --git a/src/pip/_vendor/toml/encoder.py b/src/pip/_vendor/toml/encoder.py new file mode 100644 index 00000000000..53b0bd5ace5 --- /dev/null +++ b/src/pip/_vendor/toml/encoder.py @@ -0,0 +1,250 @@ +import datetime +import re +import sys + +from pip._vendor.toml.decoder import InlineTableDict + +if sys.version_info >= (3,): + unicode = str + + +def dump(o, f): + """Writes out dict as toml to a file + + Args: + o: Object to dump into toml + f: File descriptor where the toml should be stored + + Returns: + String containing the toml corresponding to dictionary + + Raises: + TypeError: When anything other than file descriptor is passed + """ + + if not f.write: + raise TypeError("You can only dump an object to a file descriptor") + d = dumps(o) + f.write(d) + return d + + +def dumps(o, encoder=None): + """Stringifies input dict as toml + + Args: + o: Object to dump into toml + + preserve: Boolean parameter. If true, preserve inline tables. + + Returns: + String containing the toml corresponding to dict + """ + + retval = "" + if encoder is None: + encoder = TomlEncoder(o.__class__) + addtoretval, sections = encoder.dump_sections(o, "") + retval += addtoretval + while sections: + newsections = encoder.get_empty_table() + for section in sections: + addtoretval, addtosections = encoder.dump_sections( + sections[section], section) + + if addtoretval or (not addtoretval and not addtosections): + if retval and retval[-2:] != "\n\n": + retval += "\n" + retval += "[" + section + "]\n" + if addtoretval: + retval += addtoretval + for s in addtosections: + newsections[section + "." + s] = addtosections[s] + sections = newsections + return retval + + +def _dump_str(v): + if sys.version_info < (3,) and hasattr(v, 'decode') and isinstance(v, str): + v = v.decode('utf-8') + v = "%r" % v + if v[0] == 'u': + v = v[1:] + singlequote = v.startswith("'") + if singlequote or v.startswith('"'): + v = v[1:-1] + if singlequote: + v = v.replace("\\'", "'") + v = v.replace('"', '\\"') + v = v.split("\\x") + while len(v) > 1: + i = -1 + if not v[0]: + v = v[1:] + v[0] = v[0].replace("\\\\", "\\") + # No, I don't know why != works and == breaks + joinx = v[0][i] != "\\" + while v[0][:i] and v[0][i] == "\\": + joinx = not joinx + i -= 1 + if joinx: + joiner = "x" + else: + joiner = "u00" + v = [v[0] + joiner + v[1]] + v[2:] + return unicode('"' + v[0] + '"') + + +def _dump_float(v): + return "{0:.16}".format(v).replace("e+0", "e+").replace("e-0", "e-") + + +def _dump_time(v): + utcoffset = v.utcoffset() + if utcoffset is None: + return v.isoformat() + # The TOML norm specifies that it's local time thus we drop the offset + return v.isoformat()[:-6] + + +class TomlEncoder(object): + + def __init__(self, _dict=dict, preserve=False): + self._dict = _dict + self.preserve = preserve + self.dump_funcs = { + str: _dump_str, + unicode: _dump_str, + list: self.dump_list, + bool: lambda v: unicode(v).lower(), + int: lambda v: v, + float: _dump_float, + datetime.datetime: lambda v: v.isoformat().replace('+00:00', 'Z'), + datetime.time: _dump_time, + datetime.date: lambda v: v.isoformat() + } + + def get_empty_table(self): + return self._dict() + + def dump_list(self, v): + retval = "[" + for u in v: + retval += " " + unicode(self.dump_value(u)) + "," + retval += "]" + return retval + + def dump_inline_table(self, section): + """Preserve inline table in its compact syntax instead of expanding + into subsection. + + https://github.com/toml-lang/toml#user-content-inline-table + """ + retval = "" + if isinstance(section, dict): + val_list = [] + for k, v in section.items(): + val = self.dump_inline_table(v) + val_list.append(k + " = " + val) + retval += "{ " + ", ".join(val_list) + " }\n" + return retval + else: + return unicode(self.dump_value(section)) + + def dump_value(self, v): + # Lookup function corresponding to v's type + dump_fn = self.dump_funcs.get(type(v)) + if dump_fn is None and hasattr(v, '__iter__'): + dump_fn = self.dump_funcs[list] + # Evaluate function (if it exists) else return v + return dump_fn(v) if dump_fn is not None else self.dump_funcs[str](v) + + def dump_sections(self, o, sup): + retstr = "" + if sup != "" and sup[-1] != ".": + sup += '.' + retdict = self._dict() + arraystr = "" + for section in o: + section = unicode(section) + qsection = section + if not re.match(r'^[A-Za-z0-9_-]+$', section): + if '"' in section: + qsection = "'" + section + "'" + else: + qsection = '"' + section + '"' + if not isinstance(o[section], dict): + arrayoftables = False + if isinstance(o[section], list): + for a in o[section]: + if isinstance(a, dict): + arrayoftables = True + if arrayoftables: + for a in o[section]: + arraytabstr = "\n" + arraystr += "[[" + sup + qsection + "]]\n" + s, d = self.dump_sections(a, sup + qsection) + if s: + if s[0] == "[": + arraytabstr += s + else: + arraystr += s + while d: + newd = self._dict() + for dsec in d: + s1, d1 = self.dump_sections(d[dsec], sup + + qsection + "." + + dsec) + if s1: + arraytabstr += ("[" + sup + qsection + + "." + dsec + "]\n") + arraytabstr += s1 + for s1 in d1: + newd[dsec + "." + s1] = d1[s1] + d = newd + arraystr += arraytabstr + else: + if o[section] is not None: + retstr += (qsection + " = " + + unicode(self.dump_value(o[section])) + '\n') + elif self.preserve and isinstance(o[section], InlineTableDict): + retstr += (qsection + " = " + + self.dump_inline_table(o[section])) + else: + retdict[qsection] = o[section] + retstr += arraystr + return (retstr, retdict) + + +class TomlPreserveInlineDictEncoder(TomlEncoder): + + def __init__(self, _dict=dict): + super(TomlPreserveInlineDictEncoder, self).__init__(_dict, True) + + +class TomlArraySeparatorEncoder(TomlEncoder): + + def __init__(self, _dict=dict, preserve=False, separator=","): + super(TomlArraySeparatorEncoder, self).__init__(_dict, preserve) + if separator.strip() == "": + separator = "," + separator + elif separator.strip(' \t\n\r,'): + raise ValueError("Invalid separator for arrays") + self.separator = separator + + def dump_list(self, v): + t = [] + retval = "[" + for u in v: + t.append(self.dump_value(u)) + while t != []: + s = [] + for u in t: + if isinstance(u, list): + for r in u: + s.append(r) + else: + retval += " " + unicode(u) + self.separator + t = s + retval += "]" + return retval diff --git a/src/pip/_vendor/toml/ordered.py b/src/pip/_vendor/toml/ordered.py new file mode 100644 index 00000000000..6052016e8e6 --- /dev/null +++ b/src/pip/_vendor/toml/ordered.py @@ -0,0 +1,15 @@ +from collections import OrderedDict +from pip._vendor.toml import TomlEncoder +from pip._vendor.toml import TomlDecoder + + +class TomlOrderedDecoder(TomlDecoder): + + def __init__(self): + super(self.__class__, self).__init__(_dict=OrderedDict) + + +class TomlOrderedEncoder(TomlEncoder): + + def __init__(self): + super(self.__class__, self).__init__(_dict=OrderedDict) diff --git a/src/pip/_vendor/toml/tz.py b/src/pip/_vendor/toml/tz.py new file mode 100644 index 00000000000..93c3c8ad262 --- /dev/null +++ b/src/pip/_vendor/toml/tz.py @@ -0,0 +1,21 @@ +from datetime import tzinfo, timedelta + + +class TomlTz(tzinfo): + def __init__(self, toml_offset): + if toml_offset == "Z": + self._raw_offset = "+00:00" + else: + self._raw_offset = toml_offset + self._sign = -1 if self._raw_offset[0] == '-' else 1 + self._hours = int(self._raw_offset[1:3]) + self._minutes = int(self._raw_offset[4:6]) + + def tzname(self, dt): + return "UTC" + self._raw_offset + + def utcoffset(self, dt): + return self._sign * timedelta(hours=self._hours, minutes=self._minutes) + + def dst(self, dt): + return timedelta(0) diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index c590867ab6b..a966480e264 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -21,4 +21,5 @@ resolvelib==0.3.0 retrying==1.3.3 setuptools==44.0.0 six==1.14.0 +toml==0.10.0 webencodings==0.5.1 From 1f74437948fbb1b00f168dd1c7a278f233872989 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 14 Apr 2020 13:00:29 +0530 Subject: [PATCH 1618/3170] So... our vendoring of pep517 wasn't correct. :) --- src/pip/_vendor/pep517/build.py | 2 +- src/pip/_vendor/pep517/check.py | 2 +- src/pip/_vendor/pep517/envbuild.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pip/_vendor/pep517/build.py b/src/pip/_vendor/pep517/build.py index 7618c78c19b..264301447e2 100644 --- a/src/pip/_vendor/pep517/build.py +++ b/src/pip/_vendor/pep517/build.py @@ -3,7 +3,7 @@ import argparse import logging import os -import toml +from pip._vendor import toml import shutil from .envbuild import BuildEnvironment diff --git a/src/pip/_vendor/pep517/check.py b/src/pip/_vendor/pep517/check.py index 9e0c0682096..13e722a3748 100644 --- a/src/pip/_vendor/pep517/check.py +++ b/src/pip/_vendor/pep517/check.py @@ -4,7 +4,7 @@ import logging import os from os.path import isfile, join as pjoin -from toml import TomlDecodeError, load as toml_load +from pip._vendor.toml import TomlDecodeError, load as toml_load import shutil from subprocess import CalledProcessError import sys diff --git a/src/pip/_vendor/pep517/envbuild.py b/src/pip/_vendor/pep517/envbuild.py index cacd2b12c01..4088dcdb40a 100644 --- a/src/pip/_vendor/pep517/envbuild.py +++ b/src/pip/_vendor/pep517/envbuild.py @@ -3,7 +3,7 @@ import os import logging -import toml +from pip._vendor import toml import shutil from subprocess import check_call import sys From 78f16daa2789099da308fc74c4ba6ddc7955dfbe Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 14 Apr 2020 13:03:37 +0530 Subject: [PATCH 1619/3170] Switch pip's use of pytoml -> toml --- src/pip/_internal/pyproject.py | 4 ++-- tests/functional/test_pep517.py | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/pyproject.py b/src/pip/_internal/pyproject.py index cf614fd6eaf..6b4faf7a752 100644 --- a/src/pip/_internal/pyproject.py +++ b/src/pip/_internal/pyproject.py @@ -5,7 +5,7 @@ import sys from collections import namedtuple -from pip._vendor import pytoml, six +from pip._vendor import six, toml from pip._vendor.packaging.requirements import InvalidRequirement, Requirement from pip._internal.exceptions import InstallationError @@ -72,7 +72,7 @@ def load_pyproject_toml( if has_pyproject: with io.open(pyproject_toml, encoding="utf-8") as f: - pp_toml = pytoml.load(f) + pp_toml = toml.load(f) build_system = pp_toml.get("build-system") else: build_system = None diff --git a/tests/functional/test_pep517.py b/tests/functional/test_pep517.py index 4e2dffb2c8b..1647edf028c 100644 --- a/tests/functional/test_pep517.py +++ b/tests/functional/test_pep517.py @@ -1,5 +1,5 @@ import pytest -from pip._vendor import pytoml +from pip._vendor import toml from pip._internal.build_env import BuildEnvironment from pip._internal.req import InstallRequirement @@ -14,7 +14,7 @@ def make_project(tmpdir, requires=[], backend=None, backend_path=None): buildsys['build-backend'] = backend if backend_path: buildsys['backend-path'] = backend_path - data = pytoml.dumps({'build-system': buildsys}) + data = toml.dumps({'build-system': buildsys}) project_dir.joinpath('pyproject.toml').write_text(data) return project_dir @@ -190,7 +190,7 @@ def make_pyproject_with_setup(tmpdir, build_system=True, set_backend=True): if set_backend: buildsys['build-backend'] = 'setuptools.build_meta' expect_script_dir_on_path = False - project_data = pytoml.dumps({'build-system': buildsys}) + project_data = toml.dumps({'build-system': buildsys}) else: project_data = '' From 8b8fdd8429f590432b95c3137d4164aa32050215 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 14 Apr 2020 13:06:02 +0530 Subject: [PATCH 1620/3170] Drop pytoml from vendored libraries --- news/pytoml.vendor | 1 + src/pip/_vendor/pytoml.pyi | 1 - src/pip/_vendor/pytoml/LICENSE | 16 -- src/pip/_vendor/pytoml/__init__.py | 4 - src/pip/_vendor/pytoml/core.py | 13 -- src/pip/_vendor/pytoml/parser.py | 342 ----------------------------- src/pip/_vendor/pytoml/test.py | 30 --- src/pip/_vendor/pytoml/utils.py | 67 ------ src/pip/_vendor/pytoml/writer.py | 114 ---------- src/pip/_vendor/vendor.txt | 1 - 10 files changed, 1 insertion(+), 588 deletions(-) create mode 100644 news/pytoml.vendor delete mode 100644 src/pip/_vendor/pytoml.pyi delete mode 100644 src/pip/_vendor/pytoml/LICENSE delete mode 100644 src/pip/_vendor/pytoml/__init__.py delete mode 100644 src/pip/_vendor/pytoml/core.py delete mode 100644 src/pip/_vendor/pytoml/parser.py delete mode 100644 src/pip/_vendor/pytoml/test.py delete mode 100644 src/pip/_vendor/pytoml/utils.py delete mode 100644 src/pip/_vendor/pytoml/writer.py diff --git a/news/pytoml.vendor b/news/pytoml.vendor new file mode 100644 index 00000000000..a0d77417073 --- /dev/null +++ b/news/pytoml.vendor @@ -0,0 +1 @@ +Drop ``pytoml`` from vendored libaries. diff --git a/src/pip/_vendor/pytoml.pyi b/src/pip/_vendor/pytoml.pyi deleted file mode 100644 index 5566ee8972d..00000000000 --- a/src/pip/_vendor/pytoml.pyi +++ /dev/null @@ -1 +0,0 @@ -from pytoml import * \ No newline at end of file diff --git a/src/pip/_vendor/pytoml/LICENSE b/src/pip/_vendor/pytoml/LICENSE deleted file mode 100644 index da4b10cdc7e..00000000000 --- a/src/pip/_vendor/pytoml/LICENSE +++ /dev/null @@ -1,16 +0,0 @@ -No-notice MIT License - -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. - -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. diff --git a/src/pip/_vendor/pytoml/__init__.py b/src/pip/_vendor/pytoml/__init__.py deleted file mode 100644 index 8ed060ff529..00000000000 --- a/src/pip/_vendor/pytoml/__init__.py +++ /dev/null @@ -1,4 +0,0 @@ -from .core import TomlError -from .parser import load, loads -from .test import translate_to_test -from .writer import dump, dumps \ No newline at end of file diff --git a/src/pip/_vendor/pytoml/core.py b/src/pip/_vendor/pytoml/core.py deleted file mode 100644 index c182734e1ca..00000000000 --- a/src/pip/_vendor/pytoml/core.py +++ /dev/null @@ -1,13 +0,0 @@ -class TomlError(RuntimeError): - def __init__(self, message, line, col, filename): - RuntimeError.__init__(self, message, line, col, filename) - self.message = message - self.line = line - self.col = col - self.filename = filename - - def __str__(self): - return '{}({}, {}): {}'.format(self.filename, self.line, self.col, self.message) - - def __repr__(self): - return 'TomlError({!r}, {!r}, {!r}, {!r})'.format(self.message, self.line, self.col, self.filename) diff --git a/src/pip/_vendor/pytoml/parser.py b/src/pip/_vendor/pytoml/parser.py deleted file mode 100644 index f074317ff0d..00000000000 --- a/src/pip/_vendor/pytoml/parser.py +++ /dev/null @@ -1,342 +0,0 @@ -import re, sys -from .core import TomlError -from .utils import rfc3339_re, parse_rfc3339_re - -if sys.version_info[0] == 2: - _chr = unichr -else: - _chr = chr - -def load(fin, translate=lambda t, x, v: v, object_pairs_hook=dict): - return loads(fin.read(), translate=translate, object_pairs_hook=object_pairs_hook, filename=getattr(fin, 'name', repr(fin))) - -def loads(s, filename='<string>', translate=lambda t, x, v: v, object_pairs_hook=dict): - if isinstance(s, bytes): - s = s.decode('utf-8') - - s = s.replace('\r\n', '\n') - - root = object_pairs_hook() - tables = object_pairs_hook() - scope = root - - src = _Source(s, filename=filename) - ast = _p_toml(src, object_pairs_hook=object_pairs_hook) - - def error(msg): - raise TomlError(msg, pos[0], pos[1], filename) - - def process_value(v, object_pairs_hook): - kind, text, value, pos = v - if kind == 'array': - if value and any(k != value[0][0] for k, t, v, p in value[1:]): - error('array-type-mismatch') - value = [process_value(item, object_pairs_hook=object_pairs_hook) for item in value] - elif kind == 'table': - value = object_pairs_hook([(k, process_value(value[k], object_pairs_hook=object_pairs_hook)) for k in value]) - return translate(kind, text, value) - - for kind, value, pos in ast: - if kind == 'kv': - k, v = value - if k in scope: - error('duplicate_keys. Key "{0}" was used more than once.'.format(k)) - scope[k] = process_value(v, object_pairs_hook=object_pairs_hook) - else: - is_table_array = (kind == 'table_array') - cur = tables - for name in value[:-1]: - if isinstance(cur.get(name), list): - d, cur = cur[name][-1] - else: - d, cur = cur.setdefault(name, (None, object_pairs_hook())) - - scope = object_pairs_hook() - name = value[-1] - if name not in cur: - if is_table_array: - cur[name] = [(scope, object_pairs_hook())] - else: - cur[name] = (scope, object_pairs_hook()) - elif isinstance(cur[name], list): - if not is_table_array: - error('table_type_mismatch') - cur[name].append((scope, object_pairs_hook())) - else: - if is_table_array: - error('table_type_mismatch') - old_scope, next_table = cur[name] - if old_scope is not None: - error('duplicate_tables') - cur[name] = (scope, next_table) - - def merge_tables(scope, tables): - if scope is None: - scope = object_pairs_hook() - for k in tables: - if k in scope: - error('key_table_conflict') - v = tables[k] - if isinstance(v, list): - scope[k] = [merge_tables(sc, tbl) for sc, tbl in v] - else: - scope[k] = merge_tables(v[0], v[1]) - return scope - - return merge_tables(root, tables) - -class _Source: - def __init__(self, s, filename=None): - self.s = s - self._pos = (1, 1) - self._last = None - self._filename = filename - self.backtrack_stack = [] - - def last(self): - return self._last - - def pos(self): - return self._pos - - def fail(self): - return self._expect(None) - - def consume_dot(self): - if self.s: - self._last = self.s[0] - self.s = self[1:] - self._advance(self._last) - return self._last - return None - - def expect_dot(self): - return self._expect(self.consume_dot()) - - def consume_eof(self): - if not self.s: - self._last = '' - return True - return False - - def expect_eof(self): - return self._expect(self.consume_eof()) - - def consume(self, s): - if self.s.startswith(s): - self.s = self.s[len(s):] - self._last = s - self._advance(s) - return True - return False - - def expect(self, s): - return self._expect(self.consume(s)) - - def consume_re(self, re): - m = re.match(self.s) - if m: - self.s = self.s[len(m.group(0)):] - self._last = m - self._advance(m.group(0)) - return m - return None - - def expect_re(self, re): - return self._expect(self.consume_re(re)) - - def __enter__(self): - self.backtrack_stack.append((self.s, self._pos)) - - def __exit__(self, type, value, traceback): - if type is None: - self.backtrack_stack.pop() - else: - self.s, self._pos = self.backtrack_stack.pop() - return type == TomlError - - def commit(self): - self.backtrack_stack[-1] = (self.s, self._pos) - - def _expect(self, r): - if not r: - raise TomlError('msg', self._pos[0], self._pos[1], self._filename) - return r - - def _advance(self, s): - suffix_pos = s.rfind('\n') - if suffix_pos == -1: - self._pos = (self._pos[0], self._pos[1] + len(s)) - else: - self._pos = (self._pos[0] + s.count('\n'), len(s) - suffix_pos) - -_ews_re = re.compile(r'(?:[ \t]|#[^\n]*\n|#[^\n]*\Z|\n)*') -def _p_ews(s): - s.expect_re(_ews_re) - -_ws_re = re.compile(r'[ \t]*') -def _p_ws(s): - s.expect_re(_ws_re) - -_escapes = { 'b': '\b', 'n': '\n', 'r': '\r', 't': '\t', '"': '"', - '\\': '\\', 'f': '\f' } - -_basicstr_re = re.compile(r'[^"\\\000-\037]*') -_short_uni_re = re.compile(r'u([0-9a-fA-F]{4})') -_long_uni_re = re.compile(r'U([0-9a-fA-F]{8})') -_escapes_re = re.compile(r'[btnfr\"\\]') -_newline_esc_re = re.compile('\n[ \t\n]*') -def _p_basicstr_content(s, content=_basicstr_re): - res = [] - while True: - res.append(s.expect_re(content).group(0)) - if not s.consume('\\'): - break - if s.consume_re(_newline_esc_re): - pass - elif s.consume_re(_short_uni_re) or s.consume_re(_long_uni_re): - v = int(s.last().group(1), 16) - if 0xd800 <= v < 0xe000: - s.fail() - res.append(_chr(v)) - else: - s.expect_re(_escapes_re) - res.append(_escapes[s.last().group(0)]) - return ''.join(res) - -_key_re = re.compile(r'[0-9a-zA-Z-_]+') -def _p_key(s): - with s: - s.expect('"') - r = _p_basicstr_content(s, _basicstr_re) - s.expect('"') - return r - if s.consume('\''): - if s.consume('\'\''): - s.consume('\n') - r = s.expect_re(_litstr_ml_re).group(0) - s.expect('\'\'\'') - else: - r = s.expect_re(_litstr_re).group(0) - s.expect('\'') - return r - return s.expect_re(_key_re).group(0) - -_float_re = re.compile(r'[+-]?(?:0|[1-9](?:_?\d)*)(?:\.\d(?:_?\d)*)?(?:[eE][+-]?(?:\d(?:_?\d)*))?') - -_basicstr_ml_re = re.compile(r'(?:""?(?!")|[^"\\\000-\011\013-\037])*') -_litstr_re = re.compile(r"[^'\000\010\012-\037]*") -_litstr_ml_re = re.compile(r"(?:(?:|'|'')(?:[^'\000-\010\013-\037]))*") -def _p_value(s, object_pairs_hook): - pos = s.pos() - - if s.consume('true'): - return 'bool', s.last(), True, pos - if s.consume('false'): - return 'bool', s.last(), False, pos - - if s.consume('"'): - if s.consume('""'): - s.consume('\n') - r = _p_basicstr_content(s, _basicstr_ml_re) - s.expect('"""') - else: - r = _p_basicstr_content(s, _basicstr_re) - s.expect('"') - return 'str', r, r, pos - - if s.consume('\''): - if s.consume('\'\''): - s.consume('\n') - r = s.expect_re(_litstr_ml_re).group(0) - s.expect('\'\'\'') - else: - r = s.expect_re(_litstr_re).group(0) - s.expect('\'') - return 'str', r, r, pos - - if s.consume_re(rfc3339_re): - m = s.last() - return 'datetime', m.group(0), parse_rfc3339_re(m), pos - - if s.consume_re(_float_re): - m = s.last().group(0) - r = m.replace('_','') - if '.' in m or 'e' in m or 'E' in m: - return 'float', m, float(r), pos - else: - return 'int', m, int(r, 10), pos - - if s.consume('['): - items = [] - with s: - while True: - _p_ews(s) - items.append(_p_value(s, object_pairs_hook=object_pairs_hook)) - s.commit() - _p_ews(s) - s.expect(',') - s.commit() - _p_ews(s) - s.expect(']') - return 'array', None, items, pos - - if s.consume('{'): - _p_ws(s) - items = object_pairs_hook() - if not s.consume('}'): - k = _p_key(s) - _p_ws(s) - s.expect('=') - _p_ws(s) - items[k] = _p_value(s, object_pairs_hook=object_pairs_hook) - _p_ws(s) - while s.consume(','): - _p_ws(s) - k = _p_key(s) - _p_ws(s) - s.expect('=') - _p_ws(s) - items[k] = _p_value(s, object_pairs_hook=object_pairs_hook) - _p_ws(s) - s.expect('}') - return 'table', None, items, pos - - s.fail() - -def _p_stmt(s, object_pairs_hook): - pos = s.pos() - if s.consume( '['): - is_array = s.consume('[') - _p_ws(s) - keys = [_p_key(s)] - _p_ws(s) - while s.consume('.'): - _p_ws(s) - keys.append(_p_key(s)) - _p_ws(s) - s.expect(']') - if is_array: - s.expect(']') - return 'table_array' if is_array else 'table', keys, pos - - key = _p_key(s) - _p_ws(s) - s.expect('=') - _p_ws(s) - value = _p_value(s, object_pairs_hook=object_pairs_hook) - return 'kv', (key, value), pos - -_stmtsep_re = re.compile(r'(?:[ \t]*(?:#[^\n]*)?\n)+[ \t]*') -def _p_toml(s, object_pairs_hook): - stmts = [] - _p_ews(s) - with s: - stmts.append(_p_stmt(s, object_pairs_hook=object_pairs_hook)) - while True: - s.commit() - s.expect_re(_stmtsep_re) - stmts.append(_p_stmt(s, object_pairs_hook=object_pairs_hook)) - _p_ews(s) - s.expect_eof() - return stmts diff --git a/src/pip/_vendor/pytoml/test.py b/src/pip/_vendor/pytoml/test.py deleted file mode 100644 index ec8abfc6502..00000000000 --- a/src/pip/_vendor/pytoml/test.py +++ /dev/null @@ -1,30 +0,0 @@ -import datetime -from .utils import format_rfc3339 - -try: - _string_types = (str, unicode) - _int_types = (int, long) -except NameError: - _string_types = str - _int_types = int - -def translate_to_test(v): - if isinstance(v, dict): - return { k: translate_to_test(v) for k, v in v.items() } - if isinstance(v, list): - a = [translate_to_test(x) for x in v] - if v and isinstance(v[0], dict): - return a - else: - return {'type': 'array', 'value': a} - if isinstance(v, datetime.datetime): - return {'type': 'datetime', 'value': format_rfc3339(v)} - if isinstance(v, bool): - return {'type': 'bool', 'value': 'true' if v else 'false'} - if isinstance(v, _int_types): - return {'type': 'integer', 'value': str(v)} - if isinstance(v, float): - return {'type': 'float', 'value': '{:.17}'.format(v)} - if isinstance(v, _string_types): - return {'type': 'string', 'value': v} - raise RuntimeError('unexpected value: {!r}'.format(v)) diff --git a/src/pip/_vendor/pytoml/utils.py b/src/pip/_vendor/pytoml/utils.py deleted file mode 100644 index 636a680b06c..00000000000 --- a/src/pip/_vendor/pytoml/utils.py +++ /dev/null @@ -1,67 +0,0 @@ -import datetime -import re - -rfc3339_re = re.compile(r'(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2}):(\d{2})(\.\d+)?(?:Z|([+-]\d{2}):(\d{2}))') - -def parse_rfc3339(v): - m = rfc3339_re.match(v) - if not m or m.group(0) != v: - return None - return parse_rfc3339_re(m) - -def parse_rfc3339_re(m): - r = map(int, m.groups()[:6]) - if m.group(7): - micro = float(m.group(7)) - else: - micro = 0 - - if m.group(8): - g = int(m.group(8), 10) * 60 + int(m.group(9), 10) - tz = _TimeZone(datetime.timedelta(0, g * 60)) - else: - tz = _TimeZone(datetime.timedelta(0, 0)) - - y, m, d, H, M, S = r - return datetime.datetime(y, m, d, H, M, S, int(micro * 1000000), tz) - - -def format_rfc3339(v): - offs = v.utcoffset() - offs = int(offs.total_seconds()) // 60 if offs is not None else 0 - - if offs == 0: - suffix = 'Z' - else: - if offs > 0: - suffix = '+' - else: - suffix = '-' - offs = -offs - suffix = '{0}{1:02}:{2:02}'.format(suffix, offs // 60, offs % 60) - - if v.microsecond: - return v.strftime('%Y-%m-%dT%H:%M:%S.%f') + suffix - else: - return v.strftime('%Y-%m-%dT%H:%M:%S') + suffix - -class _TimeZone(datetime.tzinfo): - def __init__(self, offset): - self._offset = offset - - def utcoffset(self, dt): - return self._offset - - def dst(self, dt): - return None - - def tzname(self, dt): - m = self._offset.total_seconds() // 60 - if m < 0: - res = '-' - m = -m - else: - res = '+' - h = m // 60 - m = m - h * 60 - return '{}{:.02}{:.02}'.format(res, h, m) diff --git a/src/pip/_vendor/pytoml/writer.py b/src/pip/_vendor/pytoml/writer.py deleted file mode 100644 index d2e849f6196..00000000000 --- a/src/pip/_vendor/pytoml/writer.py +++ /dev/null @@ -1,114 +0,0 @@ -from __future__ import unicode_literals -import io, datetime, math, string, sys - -from .utils import format_rfc3339 - -try: - from pathlib import PurePath as _path_types -except ImportError: - _path_types = () - - -if sys.version_info[0] == 3: - long = int - unicode = str - - -def dumps(obj, sort_keys=False): - fout = io.StringIO() - dump(obj, fout, sort_keys=sort_keys) - return fout.getvalue() - - -_escapes = {'\n': 'n', '\r': 'r', '\\': '\\', '\t': 't', '\b': 'b', '\f': 'f', '"': '"'} - - -def _escape_string(s): - res = [] - start = 0 - - def flush(): - if start != i: - res.append(s[start:i]) - return i + 1 - - i = 0 - while i < len(s): - c = s[i] - if c in '"\\\n\r\t\b\f': - start = flush() - res.append('\\' + _escapes[c]) - elif ord(c) < 0x20: - start = flush() - res.append('\\u%04x' % ord(c)) - i += 1 - - flush() - return '"' + ''.join(res) + '"' - - -_key_chars = string.digits + string.ascii_letters + '-_' -def _escape_id(s): - if any(c not in _key_chars for c in s): - return _escape_string(s) - return s - - -def _format_value(v): - if isinstance(v, bool): - return 'true' if v else 'false' - if isinstance(v, int) or isinstance(v, long): - return unicode(v) - if isinstance(v, float): - if math.isnan(v) or math.isinf(v): - raise ValueError("{0} is not a valid TOML value".format(v)) - else: - return repr(v) - elif isinstance(v, unicode) or isinstance(v, bytes): - return _escape_string(v) - elif isinstance(v, datetime.datetime): - return format_rfc3339(v) - elif isinstance(v, list): - return '[{0}]'.format(', '.join(_format_value(obj) for obj in v)) - elif isinstance(v, dict): - return '{{{0}}}'.format(', '.join('{} = {}'.format(_escape_id(k), _format_value(obj)) for k, obj in v.items())) - elif isinstance(v, _path_types): - return _escape_string(str(v)) - else: - raise RuntimeError(v) - - -def dump(obj, fout, sort_keys=False): - tables = [((), obj, False)] - - while tables: - name, table, is_array = tables.pop() - if name: - section_name = '.'.join(_escape_id(c) for c in name) - if is_array: - fout.write('[[{0}]]\n'.format(section_name)) - else: - fout.write('[{0}]\n'.format(section_name)) - - table_keys = sorted(table.keys()) if sort_keys else table.keys() - new_tables = [] - has_kv = False - for k in table_keys: - v = table[k] - if isinstance(v, dict): - new_tables.append((name + (k,), v, False)) - elif isinstance(v, list) and v and all(isinstance(o, dict) for o in v): - new_tables.extend((name + (k,), d, True) for d in v) - elif v is None: - # based on mojombo's comment: https://github.com/toml-lang/toml/issues/146#issuecomment-25019344 - fout.write( - '#{} = null # To use: uncomment and replace null with value\n'.format(_escape_id(k))) - has_kv = True - else: - fout.write('{0} = {1}\n'.format(_escape_id(k), _format_value(v))) - has_kv = True - - tables.extend(reversed(new_tables)) - - if (name or has_kv) and tables: - fout.write('\n') diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index a966480e264..f8743e87c92 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -11,7 +11,6 @@ packaging==20.1 pep517==0.7.0 progress==1.5 pyparsing==2.4.6 -pytoml==0.1.21 requests==2.22.0 certifi==2019.11.28 chardet==3.0.4 From 8b359582de9c5bd4a4d0b49c304ac20c538d8e93 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 14 Apr 2020 15:16:04 +0530 Subject: [PATCH 1621/3170] Fix `vendored` calls --- src/pip/_vendor/__init__.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pip/_vendor/__init__.py b/src/pip/_vendor/__init__.py index c5b5481122c..fde93d2e57a 100644 --- a/src/pip/_vendor/__init__.py +++ b/src/pip/_vendor/__init__.py @@ -75,7 +75,6 @@ def vendored(modulename): vendored("pep517") vendored("pkg_resources") vendored("progress") - vendored("pytoml") vendored("retrying") vendored("requests") vendored("requests.exceptions") @@ -108,4 +107,7 @@ def vendored(modulename): vendored("requests.packages.urllib3.util.timeout") vendored("requests.packages.urllib3.util.url") vendored("resolvelib") + vendored("toml") + vendored("toml.encoder") + vendored("toml.decoder") vendored("urllib3") From e8613ffcaa1cb9fd615bae26a333205c1249baed Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Tue, 14 Apr 2020 11:03:48 +0100 Subject: [PATCH 1622/3170] Remove pip version requirement for vendoring --- noxfile.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/noxfile.py b/noxfile.py index ef1d4ee5875..8c73bcb0f9c 100644 --- a/noxfile.py +++ b/noxfile.py @@ -152,9 +152,6 @@ def lint(session): @nox.session def vendoring(session): - # Required, otherwise we interpret --no-binary :all: as - # "do not build wheels", which fails for PEP 517 requirements - session.install("-U", "pip>=19.3.1") session.install("vendoring") session.run("vendoring", "sync", ".", "-v") From 0470a82248fb3ce22e2cd96d308d7c07b2acf634 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sat, 11 Apr 2020 02:55:27 +0530 Subject: [PATCH 1623/3170] Type annotations for help, list and uninstall in commands --- ...D0A87D-0ACD-418E-8C02-4560A99FEB71.trivial | 0 src/pip/_internal/commands/help.py | 9 ++++-- src/pip/_internal/commands/list.py | 32 ++++++++++++++++--- src/pip/_internal/commands/uninstall.py | 13 ++++++-- 4 files changed, 43 insertions(+), 11 deletions(-) create mode 100644 news/9CD0A87D-0ACD-418E-8C02-4560A99FEB71.trivial diff --git a/news/9CD0A87D-0ACD-418E-8C02-4560A99FEB71.trivial b/news/9CD0A87D-0ACD-418E-8C02-4560A99FEB71.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/commands/help.py b/src/pip/_internal/commands/help.py index c17d7a457c4..ecd2b4737e3 100644 --- a/src/pip/_internal/commands/help.py +++ b/src/pip/_internal/commands/help.py @@ -1,11 +1,13 @@ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - from __future__ import absolute_import from pip._internal.cli.base_command import Command from pip._internal.cli.status_codes import SUCCESS from pip._internal.exceptions import CommandError +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import List, Any + from optparse import Values class HelpCommand(Command): @@ -16,6 +18,7 @@ class HelpCommand(Command): ignore_require_venv = True def run(self, options, args): + # type: (Values, List[Any]) -> int from pip._internal.commands import ( commands_dict, create_command, get_similar_commands, ) diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index 109ec5c664e..fdfc8ba07b8 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -1,6 +1,3 @@ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - from __future__ import absolute_import import json @@ -10,6 +7,7 @@ from pip._internal.cli import cmdoptions from pip._internal.cli.req_command import IndexGroupCommand +from pip._internal.cli.status_codes import SUCCESS from pip._internal.exceptions import CommandError from pip._internal.index.package_finder import PackageFinder from pip._internal.models.selection_prefs import SelectionPreferences @@ -21,6 +19,14 @@ write_output, ) from pip._internal.utils.packaging import get_installer +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from optparse import Values + from typing import Any, List, Set, Tuple + + from pip._internal.network.session import PipSession + from pip._vendor.pkg_resources import Distribution logger = logging.getLogger(__name__) @@ -36,6 +42,7 @@ class ListCommand(IndexGroupCommand): %prog [options]""" def __init__(self, *args, **kw): + # type: (*Any, **Any) -> None super(ListCommand, self).__init__(*args, **kw) cmd_opts = self.cmd_opts @@ -116,6 +123,7 @@ def __init__(self, *args, **kw): self.parser.insert_option_group(0, cmd_opts) def _build_package_finder(self, options, session): + # type: (Values, PipSession) -> PackageFinder """ Create a package finder appropriate to this list command. """ @@ -133,6 +141,7 @@ def _build_package_finder(self, options, session): ) def run(self, options, args): + # type: (Values, List[Any]) -> int if options.outdated and options.uptodate: raise CommandError( "Options --outdated and --uptodate cannot be combined.") @@ -160,26 +169,35 @@ def run(self, options, args): packages = self.get_uptodate(packages, options) self.output_package_listing(packages, options) + return SUCCESS def get_outdated(self, packages, options): + # type: (List[Distribution], Values) -> List[Distribution] return [ dist for dist in self.iter_packages_latest_infos(packages, options) if dist.latest_version > dist.parsed_version ] def get_uptodate(self, packages, options): + # type: (List[Distribution], Values) -> List[Distribution] return [ dist for dist in self.iter_packages_latest_infos(packages, options) if dist.latest_version == dist.parsed_version ] def get_not_required(self, packages, options): - dep_keys = set() + # type: (List[Distribution], Values) -> List[Distribution] + dep_keys = set() # type: Set[Distribution] for dist in packages: dep_keys.update(requirement.key for requirement in dist.requires()) - return {pkg for pkg in packages if pkg.key not in dep_keys} + + # Create a set to remove duplicate packages, and cast it to a list + # to keep the return type consistent with get_outdated and + # get_uptodate + return list({pkg for pkg in packages if pkg.key not in dep_keys}) def iter_packages_latest_infos(self, packages, options): + # type: (List[Distribution], Values) -> Distribution with self._build_session(options) as session: finder = self._build_package_finder(options, session) @@ -209,6 +227,7 @@ def iter_packages_latest_infos(self, packages, options): yield dist def output_package_listing(self, packages, options): + # type: (List[Distribution], Values) -> None packages = sorted( packages, key=lambda dist: dist.project_name.lower(), @@ -227,6 +246,7 @@ def output_package_listing(self, packages, options): write_output(format_for_json(packages, options)) def output_package_listing_columns(self, data, header): + # type: (List[List[Any]], List[str]) -> None # insert the header first: we need to know the size of column names if len(data) > 0: data.insert(0, header) @@ -242,6 +262,7 @@ def output_package_listing_columns(self, data, header): def format_for_columns(pkgs, options): + # type: (List[Distribution], Values) -> Tuple[List[List[str]], List[str]] """ Convert the package data into something usable by output_package_listing_columns. @@ -279,6 +300,7 @@ def format_for_columns(pkgs, options): def format_for_json(packages, options): + # type: (List[Distribution], Values) -> str data = [] for dist in packages: info = { diff --git a/src/pip/_internal/commands/uninstall.py b/src/pip/_internal/commands/uninstall.py index 5db4fb46721..d7f96eeb060 100644 --- a/src/pip/_internal/commands/uninstall.py +++ b/src/pip/_internal/commands/uninstall.py @@ -1,12 +1,10 @@ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - from __future__ import absolute_import from pip._vendor.packaging.utils import canonicalize_name from pip._internal.cli.base_command import Command from pip._internal.cli.req_command import SessionCommandMixin +from pip._internal.cli.status_codes import SUCCESS from pip._internal.exceptions import InstallationError from pip._internal.req import parse_requirements from pip._internal.req.constructors import ( @@ -14,6 +12,11 @@ install_req_from_parsed_requirement, ) from pip._internal.utils.misc import protect_pip_from_modification_on_windows +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from optparse import Values + from typing import Any, List class UninstallCommand(Command, SessionCommandMixin): @@ -32,6 +35,7 @@ class UninstallCommand(Command, SessionCommandMixin): %prog [options] -r <requirements file> ...""" def __init__(self, *args, **kw): + # type: (*Any, **Any) -> None super(UninstallCommand, self).__init__(*args, **kw) self.cmd_opts.add_option( '-r', '--requirement', @@ -51,6 +55,7 @@ def __init__(self, *args, **kw): self.parser.insert_option_group(0, self.cmd_opts) def run(self, options, args): + # type: (Values, List[Any]) -> int session = self.get_default_session(options) reqs_to_uninstall = {} @@ -87,3 +92,5 @@ def run(self, options, args): ) if uninstall_pathset: uninstall_pathset.commit() + + return SUCCESS From 8c118c8f3a6fd850e49256c51440142526a3d7c6 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Tue, 14 Apr 2020 15:18:24 +0100 Subject: [PATCH 1624/3170] Fix the test to check for canonical name --- tests/functional/test_new_resolver.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 73ceff9aaea..cb384d7a0ac 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -186,7 +186,9 @@ def test_new_resolver_no_dist_message(script): assert "Could not find a version that satisfies the requirement B" \ in result.stderr, str(result) - assert "No matching distribution found for B" in result.stderr, str(result) + # TODO: This reports the canonical name of the project. But the current + # resolver reports the originally specified name (i.e. uppercase B) + assert "No matching distribution found for b" in result.stderr, str(result) def test_new_resolver_installs_editable(script): From ae8391b4b76c641003aa0c39c1dc632a8759c063 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Wed, 15 Apr 2020 01:11:57 +0530 Subject: [PATCH 1625/3170] Type annotations for completion and debug in commands --- news/C7A26013-0E79-4DBB-B0E3-2DA5C5587CDC.trivial | 0 src/pip/_internal/commands/completion.py | 13 ++++++++++--- src/pip/_internal/commands/debug.py | 7 ++----- 3 files changed, 12 insertions(+), 8 deletions(-) create mode 100644 news/C7A26013-0E79-4DBB-B0E3-2DA5C5587CDC.trivial diff --git a/news/C7A26013-0E79-4DBB-B0E3-2DA5C5587CDC.trivial b/news/C7A26013-0E79-4DBB-B0E3-2DA5C5587CDC.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/commands/completion.py b/src/pip/_internal/commands/completion.py index 910fcbfe358..66448bbf695 100644 --- a/src/pip/_internal/commands/completion.py +++ b/src/pip/_internal/commands/completion.py @@ -1,13 +1,16 @@ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - from __future__ import absolute_import import sys import textwrap from pip._internal.cli.base_command import Command +from pip._internal.cli.status_codes import ERROR, SUCCESS from pip._internal.utils.misc import get_prog +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import Any, List + from optparse import Values BASE_COMPLETION = """ # pip {shell} completion start{script}# pip {shell} completion end @@ -54,6 +57,7 @@ class CompletionCommand(Command): ignore_require_venv = True def __init__(self, *args, **kw): + # type: (*Any, **Any) -> None super(CompletionCommand, self).__init__(*args, **kw) cmd_opts = self.cmd_opts @@ -80,6 +84,7 @@ def __init__(self, *args, **kw): self.parser.insert_option_group(0, cmd_opts) def run(self, options, args): + # type: (Values, List[Any]) -> int """Prints the completion code of the given shell""" shells = COMPLETION_SCRIPTS.keys() shell_options = ['--' + shell for shell in sorted(shells)] @@ -89,7 +94,9 @@ def run(self, options, args): prog=get_prog()) ) print(BASE_COMPLETION.format(script=script, shell=options.shell)) + return SUCCESS else: sys.stderr.write( 'ERROR: You must pass {}\n' .format(' or '.join(shell_options)) ) + return ERROR diff --git a/src/pip/_internal/commands/debug.py b/src/pip/_internal/commands/debug.py index 05ff1c54e64..1a56db2fb19 100644 --- a/src/pip/_internal/commands/debug.py +++ b/src/pip/_internal/commands/debug.py @@ -1,6 +1,3 @@ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - from __future__ import absolute_import import locale @@ -67,7 +64,6 @@ def create_vendor_txt_map(): def get_module_from_module_name(module_name): # type: (str) -> ModuleType - # Module name can be uppercase in vendor.txt for some reason... module_name = module_name.lower() # PATCH: setuptools is actually only pkg_resources. @@ -85,7 +81,6 @@ def get_module_from_module_name(module_name): def get_vendor_version_from_module(module_name): # type: (str) -> str - module = get_module_from_module_name(module_name) version = getattr(module, '__version__', None) @@ -169,6 +164,7 @@ def show_tags(options): def ca_bundle_info(config): + # type: (Dict[str, str]) -> str levels = set() for key, value in config.items(): levels.add(key.split('.')[0]) @@ -197,6 +193,7 @@ class DebugCommand(Command): ignore_require_venv = True def __init__(self, *args, **kw): + # type: (*Any, **Any) -> None super(DebugCommand, self).__init__(*args, **kw) cmd_opts = self.cmd_opts From fc6a3e203f7ac3e10fc4c35bae0a8038ce19d8af Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Wed, 15 Apr 2020 02:11:14 +0530 Subject: [PATCH 1626/3170] Added returncode check in completion tests --- tests/functional/test_completion.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/functional/test_completion.py b/tests/functional/test_completion.py index a3986811b6f..e00f0b03774 100644 --- a/tests/functional/test_completion.py +++ b/tests/functional/test_completion.py @@ -107,9 +107,10 @@ def test_completion_alone(autocomplete_script): """ Test getting completion for none shell, just pip completion """ - result = autocomplete_script.pip('completion', allow_stderr_error=True) + result = autocomplete_script.pip('completion', expect_error=True) assert 'ERROR: You must pass --bash or --fish or --zsh' in result.stderr, \ 'completion alone failed -- ' + result.stderr + assert result.returncode == 1 def test_completion_for_un_snippet(autocomplete): @@ -314,3 +315,4 @@ def test_completion_uses_same_executable_name( executable_name, 'completion', flag, expect_stderr=deprecated_python, ) assert executable_name in result.stdout + assert result.returncode == 0 From 2ece449217687103dad12f9da8a3dfb3fe0c6d22 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Wed, 15 Apr 2020 21:07:42 +0530 Subject: [PATCH 1627/3170] Upgrade contextlib2 to 0.6.0.post1 --- news/contextlib2.vendor | 1 + src/pip/_vendor/vendor.txt | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 news/contextlib2.vendor diff --git a/news/contextlib2.vendor b/news/contextlib2.vendor new file mode 100644 index 00000000000..e8fe38f8741 --- /dev/null +++ b/news/contextlib2.vendor @@ -0,0 +1 @@ +Upgrade contextlib2 to 0.6.0.post1 diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index f8743e87c92..69168f524be 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -1,7 +1,7 @@ appdirs==1.4.3 CacheControl==0.12.6 colorama==0.4.3 -contextlib2==0.6.0 +contextlib2==0.6.0.post1 distlib==0.3.0 distro==1.4.0 html5lib==1.0.1 From fc394ca52b84bcc04c48817b1e874bd392ca98c6 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Wed, 15 Apr 2020 21:09:17 +0530 Subject: [PATCH 1628/3170] Upgrade distro to 1.5.0 --- news/distro.vendor | 1 + src/pip/_vendor/distro.py | 36 +++++++++++++++++++++++++----------- src/pip/_vendor/vendor.txt | 2 +- 3 files changed, 27 insertions(+), 12 deletions(-) create mode 100644 news/distro.vendor diff --git a/news/distro.vendor b/news/distro.vendor new file mode 100644 index 00000000000..b4ca8ddf856 --- /dev/null +++ b/news/distro.vendor @@ -0,0 +1 @@ +Upgrade distro to 1.5.0. diff --git a/src/pip/_vendor/distro.py b/src/pip/_vendor/distro.py index 33061633efb..0611b62a3a8 100644 --- a/src/pip/_vendor/distro.py +++ b/src/pip/_vendor/distro.py @@ -49,7 +49,7 @@ #: #: * Value: Normalized value. NORMALIZED_OS_ID = { - 'ol': 'oracle', # Oracle Enterprise Linux + 'ol': 'oracle', # Oracle Linux } #: Translation table for normalizing the "Distributor ID" attribute returned by @@ -60,9 +60,11 @@ #: #: * Value: Normalized value. NORMALIZED_LSB_ID = { - 'enterpriseenterprise': 'oracle', # Oracle Enterprise Linux + 'enterpriseenterpriseas': 'oracle', # Oracle Enterprise Linux 4 + 'enterpriseenterpriseserver': 'oracle', # Oracle Linux 5 'redhatenterpriseworkstation': 'rhel', # RHEL 6, 7 Workstation 'redhatenterpriseserver': 'rhel', # RHEL 6, 7 Server + 'redhatenterprisecomputenode': 'rhel', # RHEL 6 ComputeNode } #: Translation table for normalizing the distro ID derived from the file name @@ -90,7 +92,8 @@ 'lsb-release', 'oem-release', _OS_RELEASE_BASENAME, - 'system-release' + 'system-release', + 'plesk-release', ) @@ -163,6 +166,7 @@ def id(): "openbsd" OpenBSD "netbsd" NetBSD "freebsd" FreeBSD + "midnightbsd" MidnightBSD ============== ========================================= If you have a need to get distros for reliable IDs added into this set, @@ -609,7 +613,7 @@ def __init__(self, distro release file can be found, the data source for the distro release file will be empty. - * ``include_name`` (bool): Controls whether uname command output is + * ``include_uname`` (bool): Controls whether uname command output is included as a data source. If the uname command is not available in the program execution path the data source for the uname command will be empty. @@ -757,7 +761,7 @@ def version(self, pretty=False, best=False): version = v break if pretty and version and self.codename(): - version = u'{0} ({1})'.format(version, self.codename()) + version = '{0} ({1})'.format(version, self.codename()) return version def version_parts(self, best=False): @@ -967,8 +971,6 @@ def _parse_os_release_content(lines): # * commands or their arguments (not allowed in os-release) if '=' in token: k, v = token.split('=', 1) - if isinstance(v, bytes): - v = v.decode('utf-8') props[k.lower()] = v else: # Ignore any tokens that are not variable assignments @@ -1012,7 +1014,7 @@ def _lsb_release_info(self): stdout = subprocess.check_output(cmd, stderr=devnull) except OSError: # Command not found return {} - content = stdout.decode(sys.getfilesystemencoding()).splitlines() + content = self._to_str(stdout).splitlines() return self._parse_lsb_release_content(content) @staticmethod @@ -1047,7 +1049,7 @@ def _uname_info(self): stdout = subprocess.check_output(cmd, stderr=devnull) except OSError: return {} - content = stdout.decode(sys.getfilesystemencoding()).splitlines() + content = self._to_str(stdout).splitlines() return self._parse_uname_content(content) @staticmethod @@ -1067,6 +1069,20 @@ def _parse_uname_content(lines): props['release'] = version return props + @staticmethod + def _to_str(text): + encoding = sys.getfilesystemencoding() + encoding = 'utf-8' if encoding == 'ascii' else encoding + + if sys.version_info[0] >= 3: + if isinstance(text, bytes): + return text.decode(encoding) + else: + if isinstance(text, unicode): # noqa + return text.encode(encoding) + + return text + @cached_property def _distro_release_info(self): """ @@ -1169,8 +1185,6 @@ def _parse_distro_release_content(line): Returns: A dictionary containing all information items. """ - if isinstance(line, bytes): - line = line.decode('utf-8') matches = _DISTRO_RELEASE_CONTENT_REVERSED_PATTERN.match( line.strip()[::-1]) distro_info = {} diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 69168f524be..ac0660e311e 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -3,7 +3,7 @@ CacheControl==0.12.6 colorama==0.4.3 contextlib2==0.6.0.post1 distlib==0.3.0 -distro==1.4.0 +distro==1.5.0 html5lib==1.0.1 ipaddress==1.0.23 # Only needed on 2.6 and 2.7 msgpack==0.6.2 From 8f2b7c63446adb5f5577f817092ffa7c98e54290 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Wed, 15 Apr 2020 21:14:09 +0530 Subject: [PATCH 1629/3170] Upgrade msgpack to 1.0.0 --- news/msgpack.vendor | 1 + src/pip/_vendor/msgpack/__init__.py | 19 +- src/pip/_vendor/msgpack/_version.py | 2 +- src/pip/_vendor/msgpack/ext.py | 191 ++++++++++ src/pip/_vendor/msgpack/fallback.py | 560 +++++++++++++++------------- src/pip/_vendor/vendor.txt | 2 +- 6 files changed, 496 insertions(+), 279 deletions(-) create mode 100644 news/msgpack.vendor create mode 100644 src/pip/_vendor/msgpack/ext.py diff --git a/news/msgpack.vendor b/news/msgpack.vendor new file mode 100644 index 00000000000..0c6c3991ac2 --- /dev/null +++ b/news/msgpack.vendor @@ -0,0 +1 @@ +Upgrade msgpack to 1.0.0. diff --git a/src/pip/_vendor/msgpack/__init__.py b/src/pip/_vendor/msgpack/__init__.py index 4ad9c1a5e13..d6705e22b79 100644 --- a/src/pip/_vendor/msgpack/__init__.py +++ b/src/pip/_vendor/msgpack/__init__.py @@ -1,24 +1,13 @@ # coding: utf-8 from ._version import version from .exceptions import * +from .ext import ExtType, Timestamp -from collections import namedtuple - - -class ExtType(namedtuple('ExtType', 'code data')): - """ExtType represents ext type in msgpack.""" - def __new__(cls, code, data): - if not isinstance(code, int): - raise TypeError("code must be int") - if not isinstance(data, bytes): - raise TypeError("data must be bytes") - if not 0 <= code <= 127: - raise ValueError("code must be 0~127") - return super(ExtType, cls).__new__(cls, code, data) +import os +import sys -import os -if os.environ.get('MSGPACK_PUREPYTHON'): +if os.environ.get("MSGPACK_PUREPYTHON") or sys.version_info[0] == 2: from .fallback import Packer, unpackb, Unpacker else: try: diff --git a/src/pip/_vendor/msgpack/_version.py b/src/pip/_vendor/msgpack/_version.py index 1e73a00f631..9f55cf50dc6 100644 --- a/src/pip/_vendor/msgpack/_version.py +++ b/src/pip/_vendor/msgpack/_version.py @@ -1 +1 @@ -version = (0, 6, 2) +version = (1, 0, 0) diff --git a/src/pip/_vendor/msgpack/ext.py b/src/pip/_vendor/msgpack/ext.py new file mode 100644 index 00000000000..8341c68b8ab --- /dev/null +++ b/src/pip/_vendor/msgpack/ext.py @@ -0,0 +1,191 @@ +# coding: utf-8 +from collections import namedtuple +import datetime +import sys +import struct + + +PY2 = sys.version_info[0] == 2 + +if PY2: + int_types = (int, long) + _utc = None +else: + int_types = int + try: + _utc = datetime.timezone.utc + except AttributeError: + _utc = datetime.timezone(datetime.timedelta(0)) + + +class ExtType(namedtuple("ExtType", "code data")): + """ExtType represents ext type in msgpack.""" + + def __new__(cls, code, data): + if not isinstance(code, int): + raise TypeError("code must be int") + if not isinstance(data, bytes): + raise TypeError("data must be bytes") + if not 0 <= code <= 127: + raise ValueError("code must be 0~127") + return super(ExtType, cls).__new__(cls, code, data) + + +class Timestamp(object): + """Timestamp represents the Timestamp extension type in msgpack. + + When built with Cython, msgpack uses C methods to pack and unpack `Timestamp`. When using pure-Python + msgpack, :func:`to_bytes` and :func:`from_bytes` are used to pack and unpack `Timestamp`. + + This class is immutable: Do not override seconds and nanoseconds. + """ + + __slots__ = ["seconds", "nanoseconds"] + + def __init__(self, seconds, nanoseconds=0): + """Initialize a Timestamp object. + + :param int seconds: + Number of seconds since the UNIX epoch (00:00:00 UTC Jan 1 1970, minus leap seconds). + May be negative. + + :param int nanoseconds: + Number of nanoseconds to add to `seconds` to get fractional time. + Maximum is 999_999_999. Default is 0. + + Note: Negative times (before the UNIX epoch) are represented as negative seconds + positive ns. + """ + if not isinstance(seconds, int_types): + raise TypeError("seconds must be an interger") + if not isinstance(nanoseconds, int_types): + raise TypeError("nanoseconds must be an integer") + if not (0 <= nanoseconds < 10 ** 9): + raise ValueError( + "nanoseconds must be a non-negative integer less than 999999999." + ) + self.seconds = seconds + self.nanoseconds = nanoseconds + + def __repr__(self): + """String representation of Timestamp.""" + return "Timestamp(seconds={0}, nanoseconds={1})".format( + self.seconds, self.nanoseconds + ) + + def __eq__(self, other): + """Check for equality with another Timestamp object""" + if type(other) is self.__class__: + return ( + self.seconds == other.seconds and self.nanoseconds == other.nanoseconds + ) + return False + + def __ne__(self, other): + """not-equals method (see :func:`__eq__()`)""" + return not self.__eq__(other) + + def __hash__(self): + return hash((self.seconds, self.nanoseconds)) + + @staticmethod + def from_bytes(b): + """Unpack bytes into a `Timestamp` object. + + Used for pure-Python msgpack unpacking. + + :param b: Payload from msgpack ext message with code -1 + :type b: bytes + + :returns: Timestamp object unpacked from msgpack ext payload + :rtype: Timestamp + """ + if len(b) == 4: + seconds = struct.unpack("!L", b)[0] + nanoseconds = 0 + elif len(b) == 8: + data64 = struct.unpack("!Q", b)[0] + seconds = data64 & 0x00000003FFFFFFFF + nanoseconds = data64 >> 34 + elif len(b) == 12: + nanoseconds, seconds = struct.unpack("!Iq", b) + else: + raise ValueError( + "Timestamp type can only be created from 32, 64, or 96-bit byte objects" + ) + return Timestamp(seconds, nanoseconds) + + def to_bytes(self): + """Pack this Timestamp object into bytes. + + Used for pure-Python msgpack packing. + + :returns data: Payload for EXT message with code -1 (timestamp type) + :rtype: bytes + """ + if (self.seconds >> 34) == 0: # seconds is non-negative and fits in 34 bits + data64 = self.nanoseconds << 34 | self.seconds + if data64 & 0xFFFFFFFF00000000 == 0: + # nanoseconds is zero and seconds < 2**32, so timestamp 32 + data = struct.pack("!L", data64) + else: + # timestamp 64 + data = struct.pack("!Q", data64) + else: + # timestamp 96 + data = struct.pack("!Iq", self.nanoseconds, self.seconds) + return data + + @staticmethod + def from_unix(unix_sec): + """Create a Timestamp from posix timestamp in seconds. + + :param unix_float: Posix timestamp in seconds. + :type unix_float: int or float. + """ + seconds = int(unix_sec // 1) + nanoseconds = int((unix_sec % 1) * 10 ** 9) + return Timestamp(seconds, nanoseconds) + + def to_unix(self): + """Get the timestamp as a floating-point value. + + :returns: posix timestamp + :rtype: float + """ + return self.seconds + self.nanoseconds / 1e9 + + @staticmethod + def from_unix_nano(unix_ns): + """Create a Timestamp from posix timestamp in nanoseconds. + + :param int unix_ns: Posix timestamp in nanoseconds. + :rtype: Timestamp + """ + return Timestamp(*divmod(unix_ns, 10 ** 9)) + + def to_unix_nano(self): + """Get the timestamp as a unixtime in nanoseconds. + + :returns: posix timestamp in nanoseconds + :rtype: int + """ + return self.seconds * 10 ** 9 + self.nanoseconds + + def to_datetime(self): + """Get the timestamp as a UTC datetime. + + Python 2 is not supported. + + :rtype: datetime. + """ + return datetime.datetime.fromtimestamp(self.to_unix(), _utc) + + @staticmethod + def from_datetime(dt): + """Create a Timestamp from datetime with tzinfo. + + Python 2 is not supported. + + :rtype: Timestamp + """ + return Timestamp.from_unix(dt.timestamp()) diff --git a/src/pip/_vendor/msgpack/fallback.py b/src/pip/_vendor/msgpack/fallback.py index 3836e830b8f..9f6665b3eb3 100644 --- a/src/pip/_vendor/msgpack/fallback.py +++ b/src/pip/_vendor/msgpack/fallback.py @@ -1,86 +1,98 @@ """Fallback pure Python implementation of msgpack""" +from datetime import datetime as _DateTime import sys import struct -import warnings -if sys.version_info[0] == 2: - PY2 = True +PY2 = sys.version_info[0] == 2 +if PY2: int_types = (int, long) + def dict_iteritems(d): return d.iteritems() + + else: - PY2 = False int_types = int unicode = str xrange = range + def dict_iteritems(d): return d.items() + if sys.version_info < (3, 5): # Ugly hack... RecursionError = RuntimeError def _is_recursionerror(e): - return len(e.args) == 1 and isinstance(e.args[0], str) and \ - e.args[0].startswith('maximum recursion depth exceeded') + return ( + len(e.args) == 1 + and isinstance(e.args[0], str) + and e.args[0].startswith("maximum recursion depth exceeded") + ) + + else: + def _is_recursionerror(e): return True -if hasattr(sys, 'pypy_version_info'): - # cStringIO is slow on PyPy, StringIO is faster. However: PyPy's own + +if hasattr(sys, "pypy_version_info"): + # StringIO is slow on PyPy, StringIO is faster. However: PyPy's own # StringBuilder is fastest. from __pypy__ import newlist_hint + try: from __pypy__.builders import BytesBuilder as StringBuilder except ImportError: from __pypy__.builders import StringBuilder USING_STRINGBUILDER = True + class StringIO(object): - def __init__(self, s=b''): + def __init__(self, s=b""): if s: self.builder = StringBuilder(len(s)) self.builder.append(s) else: self.builder = StringBuilder() + def write(self, s): if isinstance(s, memoryview): s = s.tobytes() elif isinstance(s, bytearray): s = bytes(s) self.builder.append(s) + def getvalue(self): return self.builder.build() + + else: USING_STRINGBUILDER = False from io import BytesIO as StringIO + newlist_hint = lambda size: [] -from .exceptions import ( - BufferFull, - OutOfData, - ExtraData, - FormatError, - StackError, -) +from .exceptions import BufferFull, OutOfData, ExtraData, FormatError, StackError -from . import ExtType +from .ext import ExtType, Timestamp -EX_SKIP = 0 -EX_CONSTRUCT = 1 -EX_READ_ARRAY_HEADER = 2 -EX_READ_MAP_HEADER = 3 +EX_SKIP = 0 +EX_CONSTRUCT = 1 +EX_READ_ARRAY_HEADER = 2 +EX_READ_MAP_HEADER = 3 -TYPE_IMMEDIATE = 0 -TYPE_ARRAY = 1 -TYPE_MAP = 2 -TYPE_RAW = 3 -TYPE_BIN = 4 -TYPE_EXT = 5 +TYPE_IMMEDIATE = 0 +TYPE_ARRAY = 1 +TYPE_MAP = 2 +TYPE_RAW = 3 +TYPE_BIN = 4 +TYPE_EXT = 5 DEFAULT_RECURSE_LIMIT = 511 @@ -93,31 +105,12 @@ def _check_type_strict(obj, t, type=type, tuple=tuple): def _get_data_from_buffer(obj): - try: - view = memoryview(obj) - except TypeError: - # try to use legacy buffer protocol if 2.7, otherwise re-raise - if PY2: - view = memoryview(buffer(obj)) - warnings.warn("using old buffer interface to unpack %s; " - "this leads to unpacking errors if slicing is used and " - "will be removed in a future version" % type(obj), - RuntimeWarning, stacklevel=3) - else: - raise + view = memoryview(obj) if view.itemsize != 1: raise ValueError("cannot unpack from multi-byte object") return view -def unpack(stream, **kwargs): - warnings.warn( - "Direct calling implementation's unpack() is deprecated, Use msgpack.unpack() or unpackb() instead.", - DeprecationWarning, stacklevel=2) - data = stream.read() - return unpackb(data, **kwargs) - - def unpackb(packed, **kwargs): """ Unpack an object from `packed`. @@ -146,9 +139,12 @@ def unpackb(packed, **kwargs): if sys.version_info < (2, 7, 6): + def _unpack_from(f, b, o=0): - """Explicit typcast for legacy struct.unpack_from""" + """Explicit type cast for legacy struct.unpack_from""" return struct.unpack_from(f, bytes(b), o) + + else: _unpack_from = struct.unpack_from @@ -156,7 +152,7 @@ def _unpack_from(f, b, o=0): class Unpacker(object): """Streaming unpacker. - arguments: + Arguments: :param file_like: File-like object having `.read(n)` method. @@ -170,19 +166,19 @@ class Unpacker(object): Otherwise, unpack to Python tuple. (default: True) :param bool raw: - If true, unpack msgpack raw to Python bytes (default). - Otherwise, unpack to Python str (or unicode on Python 2) by decoding - with UTF-8 encoding (recommended). - Currently, the default is true, but it will be changed to false in - near future. So you must specify it explicitly for keeping backward - compatibility. + If true, unpack msgpack raw to Python bytes. + Otherwise, unpack to Python str by decoding with UTF-8 encoding (default). - *encoding* option which is deprecated overrides this option. + :param int timestamp: + Control how timestamp type is unpacked: + + 0 - Timestamp + 1 - float (Seconds from the EPOCH) + 2 - int (Nanoseconds from the EPOCH) + 3 - datetime.datetime (UTC). Python 2 is not supported. :param bool strict_map_key: - If true, only str or bytes are accepted for map (dict) keys. - It's False by default for backward-compatibility. - But it will be True from msgpack 1.0. + If true (default), only str or bytes are accepted for map (dict) keys. :param callable object_hook: When specified, it should be callable. @@ -194,48 +190,46 @@ class Unpacker(object): Unpacker calls it with a list of key-value pairs after unpacking msgpack map. (See also simplejson) - :param str encoding: - Encoding used for decoding msgpack raw. - If it is None (default), msgpack raw is deserialized to Python bytes. - :param str unicode_errors: - (deprecated) Used for decoding msgpack raw with *encoding*. - (default: `'strict'`) + The error handler for decoding unicode. (default: 'strict') + This option should be used only when you have msgpack data which + contains invalid UTF-8 string. :param int max_buffer_size: - Limits size of data waiting unpacked. 0 means system's INT_MAX (default). + Limits size of data waiting unpacked. 0 means 2**32-1. + The default value is 100*1024*1024 (100MiB). Raises `BufferFull` exception when it is insufficient. You should set this parameter when unpacking data from untrusted source. :param int max_str_len: Deprecated, use *max_buffer_size* instead. - Limits max length of str. (default: max_buffer_size or 1024*1024) + Limits max length of str. (default: max_buffer_size) :param int max_bin_len: Deprecated, use *max_buffer_size* instead. - Limits max length of bin. (default: max_buffer_size or 1024*1024) + Limits max length of bin. (default: max_buffer_size) :param int max_array_len: Limits max length of array. - (default: max_buffer_size or 128*1024) + (default: max_buffer_size) :param int max_map_len: Limits max length of map. - (default: max_buffer_size//2 or 32*1024) + (default: max_buffer_size//2) :param int max_ext_len: Deprecated, use *max_buffer_size* instead. - Limits max size of ext type. (default: max_buffer_size or 1024*1024) + Limits max size of ext type. (default: max_buffer_size) Example of streaming deserialize from file-like object:: - unpacker = Unpacker(file_like, raw=False, max_buffer_size=10*1024*1024) + unpacker = Unpacker(file_like) for o in unpacker: process(o) Example of streaming deserialize from socket:: - unpacker = Unpacker(raw=False, max_buffer_size=10*1024*1024) + unpacker = Unpacker(max_buffer_size) while True: buf = sock.recv(1024**2) if not buf: @@ -251,22 +245,28 @@ class Unpacker(object): Other exceptions can be raised during unpacking. """ - def __init__(self, file_like=None, read_size=0, use_list=True, raw=True, strict_map_key=False, - object_hook=None, object_pairs_hook=None, list_hook=None, - encoding=None, unicode_errors=None, max_buffer_size=0, - ext_hook=ExtType, - max_str_len=-1, - max_bin_len=-1, - max_array_len=-1, - max_map_len=-1, - max_ext_len=-1): - if encoding is not None: - warnings.warn( - "encoding is deprecated, Use raw=False instead.", - DeprecationWarning, stacklevel=2) - + def __init__( + self, + file_like=None, + read_size=0, + use_list=True, + raw=False, + timestamp=0, + strict_map_key=True, + object_hook=None, + object_pairs_hook=None, + list_hook=None, + unicode_errors=None, + max_buffer_size=100 * 1024 * 1024, + ext_hook=ExtType, + max_str_len=-1, + max_bin_len=-1, + max_array_len=-1, + max_map_len=-1, + max_ext_len=-1, + ): if unicode_errors is None: - unicode_errors = 'strict' + unicode_errors = "strict" if file_like is None: self._feeding = True @@ -290,26 +290,30 @@ def __init__(self, file_like=None, read_size=0, use_list=True, raw=True, strict_ # state, which _buf_checkpoint records. self._buf_checkpoint = 0 + if not max_buffer_size: + max_buffer_size = 2 ** 31 - 1 if max_str_len == -1: - max_str_len = max_buffer_size or 1024*1024 + max_str_len = max_buffer_size if max_bin_len == -1: - max_bin_len = max_buffer_size or 1024*1024 + max_bin_len = max_buffer_size if max_array_len == -1: - max_array_len = max_buffer_size or 128*1024 + max_array_len = max_buffer_size if max_map_len == -1: - max_map_len = max_buffer_size//2 or 32*1024 + max_map_len = max_buffer_size // 2 if max_ext_len == -1: - max_ext_len = max_buffer_size or 1024*1024 + max_ext_len = max_buffer_size - self._max_buffer_size = max_buffer_size or 2**31-1 + self._max_buffer_size = max_buffer_size if read_size > self._max_buffer_size: raise ValueError("read_size must be smaller than max_buffer_size") - self._read_size = read_size or min(self._max_buffer_size, 16*1024) + self._read_size = read_size or min(self._max_buffer_size, 16 * 1024) self._raw = bool(raw) self._strict_map_key = bool(strict_map_key) - self._encoding = encoding self._unicode_errors = unicode_errors self._use_list = use_list + if not (0 <= timestamp <= 3): + raise ValueError("timestamp must be 0..3") + self._timestamp = timestamp self._list_hook = list_hook self._object_hook = object_hook self._object_pairs_hook = object_pairs_hook @@ -322,26 +326,27 @@ def __init__(self, file_like=None, read_size=0, use_list=True, raw=True, strict_ self._stream_offset = 0 if list_hook is not None and not callable(list_hook): - raise TypeError('`list_hook` is not callable') + raise TypeError("`list_hook` is not callable") if object_hook is not None and not callable(object_hook): - raise TypeError('`object_hook` is not callable') + raise TypeError("`object_hook` is not callable") if object_pairs_hook is not None and not callable(object_pairs_hook): - raise TypeError('`object_pairs_hook` is not callable') + raise TypeError("`object_pairs_hook` is not callable") if object_hook is not None and object_pairs_hook is not None: - raise TypeError("object_pairs_hook and object_hook are mutually " - "exclusive") + raise TypeError( + "object_pairs_hook and object_hook are mutually " "exclusive" + ) if not callable(ext_hook): raise TypeError("`ext_hook` is not callable") def feed(self, next_bytes): assert self._feeding view = _get_data_from_buffer(next_bytes) - if (len(self._buffer) - self._buff_i + len(view) > self._max_buffer_size): + if len(self._buffer) - self._buff_i + len(view) > self._max_buffer_size: raise BufferFull # Strip buffer before checkpoint before reading file. if self._buf_checkpoint > 0: - del self._buffer[:self._buf_checkpoint] + del self._buffer[: self._buf_checkpoint] self._buff_i -= self._buf_checkpoint self._buf_checkpoint = 0 @@ -357,17 +362,19 @@ def _got_extradata(self): return self._buff_i < len(self._buffer) def _get_extradata(self): - return self._buffer[self._buff_i:] + return self._buffer[self._buff_i :] def read_bytes(self, n): - return self._read(n) + ret = self._read(n) + self._consume() + return ret def _read(self, n): # (int) -> bytearray self._reserve(n) i = self._buff_i - self._buff_i = i+n - return self._buffer[i:i+n] + self._buff_i = i + n + return self._buffer[i : i + n] def _reserve(self, n): remain_bytes = len(self._buffer) - self._buff_i - n @@ -382,7 +389,7 @@ def _reserve(self, n): # Strip buffer before checkpoint before reading file. if self._buf_checkpoint > 0: - del self._buffer[:self._buf_checkpoint] + del self._buffer[: self._buf_checkpoint] self._buff_i -= self._buf_checkpoint self._buf_checkpoint = 0 @@ -411,7 +418,7 @@ def _read_header(self, execute=EX_CONSTRUCT): if b & 0b10000000 == 0: obj = b elif b & 0b11100000 == 0b11100000: - obj = -1 - (b ^ 0xff) + obj = -1 - (b ^ 0xFF) elif b & 0b11100000 == 0b10100000: n = b & 0b00011111 typ = TYPE_RAW @@ -428,13 +435,13 @@ def _read_header(self, execute=EX_CONSTRUCT): typ = TYPE_MAP if n > self._max_map_len: raise ValueError("%s exceeds max_map_len(%s)", n, self._max_map_len) - elif b == 0xc0: + elif b == 0xC0: obj = None - elif b == 0xc2: + elif b == 0xC2: obj = False - elif b == 0xc3: + elif b == 0xC3: obj = True - elif b == 0xc4: + elif b == 0xC4: typ = TYPE_BIN self._reserve(1) n = self._buffer[self._buff_i] @@ -442,7 +449,7 @@ def _read_header(self, execute=EX_CONSTRUCT): if n > self._max_bin_len: raise ValueError("%s exceeds max_bin_len(%s)" % (n, self._max_bin_len)) obj = self._read(n) - elif b == 0xc5: + elif b == 0xC5: typ = TYPE_BIN self._reserve(2) n = _unpack_from(">H", self._buffer, self._buff_i)[0] @@ -450,7 +457,7 @@ def _read_header(self, execute=EX_CONSTRUCT): if n > self._max_bin_len: raise ValueError("%s exceeds max_bin_len(%s)" % (n, self._max_bin_len)) obj = self._read(n) - elif b == 0xc6: + elif b == 0xC6: typ = TYPE_BIN self._reserve(4) n = _unpack_from(">I", self._buffer, self._buff_i)[0] @@ -458,106 +465,106 @@ def _read_header(self, execute=EX_CONSTRUCT): if n > self._max_bin_len: raise ValueError("%s exceeds max_bin_len(%s)" % (n, self._max_bin_len)) obj = self._read(n) - elif b == 0xc7: # ext 8 + elif b == 0xC7: # ext 8 typ = TYPE_EXT self._reserve(2) - L, n = _unpack_from('Bb', self._buffer, self._buff_i) + L, n = _unpack_from("Bb", self._buffer, self._buff_i) self._buff_i += 2 if L > self._max_ext_len: raise ValueError("%s exceeds max_ext_len(%s)" % (L, self._max_ext_len)) obj = self._read(L) - elif b == 0xc8: # ext 16 + elif b == 0xC8: # ext 16 typ = TYPE_EXT self._reserve(3) - L, n = _unpack_from('>Hb', self._buffer, self._buff_i) + L, n = _unpack_from(">Hb", self._buffer, self._buff_i) self._buff_i += 3 if L > self._max_ext_len: raise ValueError("%s exceeds max_ext_len(%s)" % (L, self._max_ext_len)) obj = self._read(L) - elif b == 0xc9: # ext 32 + elif b == 0xC9: # ext 32 typ = TYPE_EXT self._reserve(5) - L, n = _unpack_from('>Ib', self._buffer, self._buff_i) + L, n = _unpack_from(">Ib", self._buffer, self._buff_i) self._buff_i += 5 if L > self._max_ext_len: raise ValueError("%s exceeds max_ext_len(%s)" % (L, self._max_ext_len)) obj = self._read(L) - elif b == 0xca: + elif b == 0xCA: self._reserve(4) obj = _unpack_from(">f", self._buffer, self._buff_i)[0] self._buff_i += 4 - elif b == 0xcb: + elif b == 0xCB: self._reserve(8) obj = _unpack_from(">d", self._buffer, self._buff_i)[0] self._buff_i += 8 - elif b == 0xcc: + elif b == 0xCC: self._reserve(1) obj = self._buffer[self._buff_i] self._buff_i += 1 - elif b == 0xcd: + elif b == 0xCD: self._reserve(2) obj = _unpack_from(">H", self._buffer, self._buff_i)[0] self._buff_i += 2 - elif b == 0xce: + elif b == 0xCE: self._reserve(4) obj = _unpack_from(">I", self._buffer, self._buff_i)[0] self._buff_i += 4 - elif b == 0xcf: + elif b == 0xCF: self._reserve(8) obj = _unpack_from(">Q", self._buffer, self._buff_i)[0] self._buff_i += 8 - elif b == 0xd0: + elif b == 0xD0: self._reserve(1) obj = _unpack_from("b", self._buffer, self._buff_i)[0] self._buff_i += 1 - elif b == 0xd1: + elif b == 0xD1: self._reserve(2) obj = _unpack_from(">h", self._buffer, self._buff_i)[0] self._buff_i += 2 - elif b == 0xd2: + elif b == 0xD2: self._reserve(4) obj = _unpack_from(">i", self._buffer, self._buff_i)[0] self._buff_i += 4 - elif b == 0xd3: + elif b == 0xD3: self._reserve(8) obj = _unpack_from(">q", self._buffer, self._buff_i)[0] self._buff_i += 8 - elif b == 0xd4: # fixext 1 + elif b == 0xD4: # fixext 1 typ = TYPE_EXT if self._max_ext_len < 1: raise ValueError("%s exceeds max_ext_len(%s)" % (1, self._max_ext_len)) self._reserve(2) n, obj = _unpack_from("b1s", self._buffer, self._buff_i) self._buff_i += 2 - elif b == 0xd5: # fixext 2 + elif b == 0xD5: # fixext 2 typ = TYPE_EXT if self._max_ext_len < 2: raise ValueError("%s exceeds max_ext_len(%s)" % (2, self._max_ext_len)) self._reserve(3) n, obj = _unpack_from("b2s", self._buffer, self._buff_i) self._buff_i += 3 - elif b == 0xd6: # fixext 4 + elif b == 0xD6: # fixext 4 typ = TYPE_EXT if self._max_ext_len < 4: raise ValueError("%s exceeds max_ext_len(%s)" % (4, self._max_ext_len)) self._reserve(5) n, obj = _unpack_from("b4s", self._buffer, self._buff_i) self._buff_i += 5 - elif b == 0xd7: # fixext 8 + elif b == 0xD7: # fixext 8 typ = TYPE_EXT if self._max_ext_len < 8: raise ValueError("%s exceeds max_ext_len(%s)" % (8, self._max_ext_len)) self._reserve(9) n, obj = _unpack_from("b8s", self._buffer, self._buff_i) self._buff_i += 9 - elif b == 0xd8: # fixext 16 + elif b == 0xD8: # fixext 16 typ = TYPE_EXT if self._max_ext_len < 16: raise ValueError("%s exceeds max_ext_len(%s)" % (16, self._max_ext_len)) self._reserve(17) n, obj = _unpack_from("b16s", self._buffer, self._buff_i) self._buff_i += 17 - elif b == 0xd9: + elif b == 0xD9: typ = TYPE_RAW self._reserve(1) n = self._buffer[self._buff_i] @@ -565,46 +572,46 @@ def _read_header(self, execute=EX_CONSTRUCT): if n > self._max_str_len: raise ValueError("%s exceeds max_str_len(%s)", n, self._max_str_len) obj = self._read(n) - elif b == 0xda: + elif b == 0xDA: typ = TYPE_RAW self._reserve(2) - n, = _unpack_from(">H", self._buffer, self._buff_i) + (n,) = _unpack_from(">H", self._buffer, self._buff_i) self._buff_i += 2 if n > self._max_str_len: raise ValueError("%s exceeds max_str_len(%s)", n, self._max_str_len) obj = self._read(n) - elif b == 0xdb: + elif b == 0xDB: typ = TYPE_RAW self._reserve(4) - n, = _unpack_from(">I", self._buffer, self._buff_i) + (n,) = _unpack_from(">I", self._buffer, self._buff_i) self._buff_i += 4 if n > self._max_str_len: raise ValueError("%s exceeds max_str_len(%s)", n, self._max_str_len) obj = self._read(n) - elif b == 0xdc: + elif b == 0xDC: typ = TYPE_ARRAY self._reserve(2) - n, = _unpack_from(">H", self._buffer, self._buff_i) + (n,) = _unpack_from(">H", self._buffer, self._buff_i) self._buff_i += 2 if n > self._max_array_len: raise ValueError("%s exceeds max_array_len(%s)", n, self._max_array_len) - elif b == 0xdd: + elif b == 0xDD: typ = TYPE_ARRAY self._reserve(4) - n, = _unpack_from(">I", self._buffer, self._buff_i) + (n,) = _unpack_from(">I", self._buffer, self._buff_i) self._buff_i += 4 if n > self._max_array_len: raise ValueError("%s exceeds max_array_len(%s)", n, self._max_array_len) - elif b == 0xde: + elif b == 0xDE: self._reserve(2) - n, = _unpack_from(">H", self._buffer, self._buff_i) + (n,) = _unpack_from(">H", self._buffer, self._buff_i) self._buff_i += 2 if n > self._max_map_len: raise ValueError("%s exceeds max_map_len(%s)", n, self._max_map_len) typ = TYPE_MAP - elif b == 0xdf: + elif b == 0xDF: self._reserve(4) - n, = _unpack_from(">I", self._buffer, self._buff_i) + (n,) = _unpack_from(">I", self._buffer, self._buff_i) self._buff_i += 4 if n > self._max_map_len: raise ValueError("%s exceeds max_map_len(%s)", n, self._max_map_len) @@ -647,15 +654,19 @@ def _unpack(self, execute=EX_CONSTRUCT): return if self._object_pairs_hook is not None: ret = self._object_pairs_hook( - (self._unpack(EX_CONSTRUCT), - self._unpack(EX_CONSTRUCT)) - for _ in xrange(n)) + (self._unpack(EX_CONSTRUCT), self._unpack(EX_CONSTRUCT)) + for _ in xrange(n) + ) else: ret = {} for _ in xrange(n): key = self._unpack(EX_CONSTRUCT) if self._strict_map_key and type(key) not in (unicode, bytes): - raise ValueError("%s is not allowed for map key" % str(type(key))) + raise ValueError( + "%s is not allowed for map key" % str(type(key)) + ) + if not PY2 and type(key) is str: + key = sys.intern(key) ret[key] = self._unpack(EX_CONSTRUCT) if self._object_hook is not None: ret = self._object_hook(ret) @@ -663,17 +674,26 @@ def _unpack(self, execute=EX_CONSTRUCT): if execute == EX_SKIP: return if typ == TYPE_RAW: - if self._encoding is not None: - obj = obj.decode(self._encoding, self._unicode_errors) - elif self._raw: + if self._raw: obj = bytes(obj) else: - obj = obj.decode('utf_8') + obj = obj.decode("utf_8", self._unicode_errors) return obj - if typ == TYPE_EXT: - return self._ext_hook(n, bytes(obj)) if typ == TYPE_BIN: return bytes(obj) + if typ == TYPE_EXT: + if n == -1: # timestamp + ts = Timestamp.from_bytes(bytes(obj)) + if self._timestamp == 1: + return ts.to_unix() + elif self._timestamp == 2: + return ts.to_unix_nano() + elif self._timestamp == 3: + return ts.to_datetime() + else: + return ts + else: + return self._ext_hook(n, bytes(obj)) assert typ == TYPE_IMMEDIATE return obj @@ -723,7 +743,7 @@ class Packer(object): """ MessagePack Packer - usage: + Usage: packer = Packer() astream.write(packer.pack(a)) @@ -744,49 +764,58 @@ class Packer(object): :param bool use_bin_type: Use bin type introduced in msgpack spec 2.0 for bytes. - It also enables str8 type for unicode. + It also enables str8 type for unicode. (default: True) :param bool strict_types: If set to true, types will be checked to be exact. Derived classes - from serializeable types will not be serialized and will be + from serializable types will not be serialized and will be treated as unsupported type and forwarded to default. Additionally tuples will not be serialized as lists. This is useful when trying to implement accurate serialization for python types. - :param str encoding: - (deprecated) Convert unicode to bytes with this encoding. (default: 'utf-8') + :param bool datetime: + If set to true, datetime with tzinfo is packed into Timestamp type. + Note that the tzinfo is stripped in the timestamp. + You can get UTC datetime with `timestamp=3` option of the Unpacker. + (Python 2 is not supported). :param str unicode_errors: - Error handler for encoding unicode. (default: 'strict') + The error handler for encoding unicode. (default: 'strict') + DO NOT USE THIS!! This option is kept for very specific usage. """ - def __init__(self, default=None, encoding=None, unicode_errors=None, - use_single_float=False, autoreset=True, use_bin_type=False, - strict_types=False): - if encoding is None: - encoding = 'utf_8' - else: - warnings.warn( - "encoding is deprecated, Use raw=False instead.", - DeprecationWarning, stacklevel=2) - - if unicode_errors is None: - unicode_errors = 'strict' + def __init__( + self, + default=None, + use_single_float=False, + autoreset=True, + use_bin_type=True, + strict_types=False, + datetime=False, + unicode_errors=None, + ): self._strict_types = strict_types self._use_float = use_single_float self._autoreset = autoreset self._use_bin_type = use_bin_type - self._encoding = encoding - self._unicode_errors = unicode_errors self._buffer = StringIO() + if PY2 and datetime: + raise ValueError("datetime is not supported in Python 2") + self._datetime = bool(datetime) + self._unicode_errors = unicode_errors or "strict" if default is not None: if not callable(default): raise TypeError("default must be callable") self._default = default - def _pack(self, obj, nest_limit=DEFAULT_RECURSE_LIMIT, - check=isinstance, check_type_strict=_check_type_strict): + def _pack( + self, + obj, + nest_limit=DEFAULT_RECURSE_LIMIT, + check=isinstance, + check_type_strict=_check_type_strict, + ): default_used = False if self._strict_types: check = check_type_strict @@ -807,22 +836,22 @@ def _pack(self, obj, nest_limit=DEFAULT_RECURSE_LIMIT, return self._buffer.write(struct.pack("B", obj)) if -0x20 <= obj < 0: return self._buffer.write(struct.pack("b", obj)) - if 0x80 <= obj <= 0xff: - return self._buffer.write(struct.pack("BB", 0xcc, obj)) + if 0x80 <= obj <= 0xFF: + return self._buffer.write(struct.pack("BB", 0xCC, obj)) if -0x80 <= obj < 0: - return self._buffer.write(struct.pack(">Bb", 0xd0, obj)) - if 0xff < obj <= 0xffff: - return self._buffer.write(struct.pack(">BH", 0xcd, obj)) + return self._buffer.write(struct.pack(">Bb", 0xD0, obj)) + if 0xFF < obj <= 0xFFFF: + return self._buffer.write(struct.pack(">BH", 0xCD, obj)) if -0x8000 <= obj < -0x80: - return self._buffer.write(struct.pack(">Bh", 0xd1, obj)) - if 0xffff < obj <= 0xffffffff: - return self._buffer.write(struct.pack(">BI", 0xce, obj)) + return self._buffer.write(struct.pack(">Bh", 0xD1, obj)) + if 0xFFFF < obj <= 0xFFFFFFFF: + return self._buffer.write(struct.pack(">BI", 0xCE, obj)) if -0x80000000 <= obj < -0x8000: - return self._buffer.write(struct.pack(">Bi", 0xd2, obj)) - if 0xffffffff < obj <= 0xffffffffffffffff: - return self._buffer.write(struct.pack(">BQ", 0xcf, obj)) + return self._buffer.write(struct.pack(">Bi", 0xD2, obj)) + if 0xFFFFFFFF < obj <= 0xFFFFFFFFFFFFFFFF: + return self._buffer.write(struct.pack(">BQ", 0xCF, obj)) if -0x8000000000000000 <= obj < -0x80000000: - return self._buffer.write(struct.pack(">Bq", 0xd3, obj)) + return self._buffer.write(struct.pack(">Bq", 0xD3, obj)) if not default_used and self._default is not None: obj = self._default(obj) default_used = True @@ -830,53 +859,53 @@ def _pack(self, obj, nest_limit=DEFAULT_RECURSE_LIMIT, raise OverflowError("Integer value out of range") if check(obj, (bytes, bytearray)): n = len(obj) - if n >= 2**32: + if n >= 2 ** 32: raise ValueError("%s is too large" % type(obj).__name__) self._pack_bin_header(n) return self._buffer.write(obj) if check(obj, unicode): - if self._encoding is None: - raise TypeError( - "Can't encode unicode string: " - "no encoding is specified") - obj = obj.encode(self._encoding, self._unicode_errors) + obj = obj.encode("utf-8", self._unicode_errors) n = len(obj) - if n >= 2**32: + if n >= 2 ** 32: raise ValueError("String is too large") self._pack_raw_header(n) return self._buffer.write(obj) if check(obj, memoryview): n = len(obj) * obj.itemsize - if n >= 2**32: + if n >= 2 ** 32: raise ValueError("Memoryview is too large") self._pack_bin_header(n) return self._buffer.write(obj) if check(obj, float): if self._use_float: - return self._buffer.write(struct.pack(">Bf", 0xca, obj)) - return self._buffer.write(struct.pack(">Bd", 0xcb, obj)) - if check(obj, ExtType): - code = obj.code - data = obj.data + return self._buffer.write(struct.pack(">Bf", 0xCA, obj)) + return self._buffer.write(struct.pack(">Bd", 0xCB, obj)) + if check(obj, (ExtType, Timestamp)): + if check(obj, Timestamp): + code = -1 + data = obj.to_bytes() + else: + code = obj.code + data = obj.data assert isinstance(code, int) assert isinstance(data, bytes) L = len(data) if L == 1: - self._buffer.write(b'\xd4') + self._buffer.write(b"\xd4") elif L == 2: - self._buffer.write(b'\xd5') + self._buffer.write(b"\xd5") elif L == 4: - self._buffer.write(b'\xd6') + self._buffer.write(b"\xd6") elif L == 8: - self._buffer.write(b'\xd7') + self._buffer.write(b"\xd7") elif L == 16: - self._buffer.write(b'\xd8') - elif L <= 0xff: - self._buffer.write(struct.pack(">BB", 0xc7, L)) - elif L <= 0xffff: - self._buffer.write(struct.pack(">BH", 0xc8, L)) + self._buffer.write(b"\xd8") + elif L <= 0xFF: + self._buffer.write(struct.pack(">BB", 0xC7, L)) + elif L <= 0xFFFF: + self._buffer.write(struct.pack(">BH", 0xC8, L)) else: - self._buffer.write(struct.pack(">BI", 0xc9, L)) + self._buffer.write(struct.pack(">BI", 0xC9, L)) self._buffer.write(struct.pack("b", code)) self._buffer.write(data) return @@ -887,13 +916,20 @@ def _pack(self, obj, nest_limit=DEFAULT_RECURSE_LIMIT, self._pack(obj[i], nest_limit - 1) return if check(obj, dict): - return self._pack_map_pairs(len(obj), dict_iteritems(obj), - nest_limit - 1) + return self._pack_map_pairs( + len(obj), dict_iteritems(obj), nest_limit - 1 + ) + + if self._datetime and check(obj, _DateTime): + obj = Timestamp.from_datetime(obj) + default_used = 1 + continue + if not default_used and self._default is not None: obj = self._default(obj) default_used = 1 continue - raise TypeError("Cannot serialize %r" % (obj, )) + raise TypeError("Cannot serialize %r" % (obj,)) def pack(self, obj): try: @@ -914,7 +950,7 @@ def pack_map_pairs(self, pairs): return ret def pack_array_header(self, n): - if n >= 2**32: + if n >= 2 ** 32: raise ValueError self._pack_array_header(n) if self._autoreset: @@ -923,7 +959,7 @@ def pack_array_header(self, n): return ret def pack_map_header(self, n): - if n >= 2**32: + if n >= 2 ** 32: raise ValueError self._pack_map_header(n) if self._autoreset: @@ -939,43 +975,43 @@ def pack_ext_type(self, typecode, data): if not isinstance(data, bytes): raise TypeError("data must have bytes type") L = len(data) - if L > 0xffffffff: + if L > 0xFFFFFFFF: raise ValueError("Too large data") if L == 1: - self._buffer.write(b'\xd4') + self._buffer.write(b"\xd4") elif L == 2: - self._buffer.write(b'\xd5') + self._buffer.write(b"\xd5") elif L == 4: - self._buffer.write(b'\xd6') + self._buffer.write(b"\xd6") elif L == 8: - self._buffer.write(b'\xd7') + self._buffer.write(b"\xd7") elif L == 16: - self._buffer.write(b'\xd8') - elif L <= 0xff: - self._buffer.write(b'\xc7' + struct.pack('B', L)) - elif L <= 0xffff: - self._buffer.write(b'\xc8' + struct.pack('>H', L)) + self._buffer.write(b"\xd8") + elif L <= 0xFF: + self._buffer.write(b"\xc7" + struct.pack("B", L)) + elif L <= 0xFFFF: + self._buffer.write(b"\xc8" + struct.pack(">H", L)) else: - self._buffer.write(b'\xc9' + struct.pack('>I', L)) - self._buffer.write(struct.pack('B', typecode)) + self._buffer.write(b"\xc9" + struct.pack(">I", L)) + self._buffer.write(struct.pack("B", typecode)) self._buffer.write(data) def _pack_array_header(self, n): - if n <= 0x0f: - return self._buffer.write(struct.pack('B', 0x90 + n)) - if n <= 0xffff: - return self._buffer.write(struct.pack(">BH", 0xdc, n)) - if n <= 0xffffffff: - return self._buffer.write(struct.pack(">BI", 0xdd, n)) + if n <= 0x0F: + return self._buffer.write(struct.pack("B", 0x90 + n)) + if n <= 0xFFFF: + return self._buffer.write(struct.pack(">BH", 0xDC, n)) + if n <= 0xFFFFFFFF: + return self._buffer.write(struct.pack(">BI", 0xDD, n)) raise ValueError("Array is too large") def _pack_map_header(self, n): - if n <= 0x0f: - return self._buffer.write(struct.pack('B', 0x80 + n)) - if n <= 0xffff: - return self._buffer.write(struct.pack(">BH", 0xde, n)) - if n <= 0xffffffff: - return self._buffer.write(struct.pack(">BI", 0xdf, n)) + if n <= 0x0F: + return self._buffer.write(struct.pack("B", 0x80 + n)) + if n <= 0xFFFF: + return self._buffer.write(struct.pack(">BH", 0xDE, n)) + if n <= 0xFFFFFFFF: + return self._buffer.write(struct.pack(">BI", 0xDF, n)) raise ValueError("Dict is too large") def _pack_map_pairs(self, n, pairs, nest_limit=DEFAULT_RECURSE_LIMIT): @@ -985,28 +1021,28 @@ def _pack_map_pairs(self, n, pairs, nest_limit=DEFAULT_RECURSE_LIMIT): self._pack(v, nest_limit - 1) def _pack_raw_header(self, n): - if n <= 0x1f: - self._buffer.write(struct.pack('B', 0xa0 + n)) - elif self._use_bin_type and n <= 0xff: - self._buffer.write(struct.pack('>BB', 0xd9, n)) - elif n <= 0xffff: - self._buffer.write(struct.pack(">BH", 0xda, n)) - elif n <= 0xffffffff: - self._buffer.write(struct.pack(">BI", 0xdb, n)) + if n <= 0x1F: + self._buffer.write(struct.pack("B", 0xA0 + n)) + elif self._use_bin_type and n <= 0xFF: + self._buffer.write(struct.pack(">BB", 0xD9, n)) + elif n <= 0xFFFF: + self._buffer.write(struct.pack(">BH", 0xDA, n)) + elif n <= 0xFFFFFFFF: + self._buffer.write(struct.pack(">BI", 0xDB, n)) else: - raise ValueError('Raw is too large') + raise ValueError("Raw is too large") def _pack_bin_header(self, n): if not self._use_bin_type: return self._pack_raw_header(n) - elif n <= 0xff: - return self._buffer.write(struct.pack('>BB', 0xc4, n)) - elif n <= 0xffff: - return self._buffer.write(struct.pack(">BH", 0xc5, n)) - elif n <= 0xffffffff: - return self._buffer.write(struct.pack(">BI", 0xc6, n)) + elif n <= 0xFF: + return self._buffer.write(struct.pack(">BB", 0xC4, n)) + elif n <= 0xFFFF: + return self._buffer.write(struct.pack(">BH", 0xC5, n)) + elif n <= 0xFFFFFFFF: + return self._buffer.write(struct.pack(">BI", 0xC6, n)) else: - raise ValueError('Bin is too large') + raise ValueError("Bin is too large") def bytes(self): """Return internal buffer contents as bytes object""" @@ -1015,7 +1051,7 @@ def bytes(self): def reset(self): """Reset internal buffer. - This method is usaful only when autoreset=False. + This method is useful only when autoreset=False. """ self._buffer = StringIO() diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index ac0660e311e..6bf6c0b331f 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -6,7 +6,7 @@ distlib==0.3.0 distro==1.5.0 html5lib==1.0.1 ipaddress==1.0.23 # Only needed on 2.6 and 2.7 -msgpack==0.6.2 +msgpack==1.0.0 packaging==20.1 pep517==0.7.0 progress==1.5 From 2694496d264a20086db387a91f45bcc69478c13e Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Wed, 15 Apr 2020 21:18:24 +0530 Subject: [PATCH 1630/3170] Upgrade packaging to 20.3 --- news/packaging.vendor | 1 + src/pip/_vendor/packaging/__about__.py | 2 +- src/pip/_vendor/packaging/tags.py | 33 ++++++++++++++++---------- src/pip/_vendor/vendor.txt | 2 +- 4 files changed, 24 insertions(+), 14 deletions(-) create mode 100644 news/packaging.vendor diff --git a/news/packaging.vendor b/news/packaging.vendor new file mode 100644 index 00000000000..f158735cf68 --- /dev/null +++ b/news/packaging.vendor @@ -0,0 +1 @@ +Upgrade packaging to 20.3. diff --git a/src/pip/_vendor/packaging/__about__.py b/src/pip/_vendor/packaging/__about__.py index 08d2c892b83..5161d141be7 100644 --- a/src/pip/_vendor/packaging/__about__.py +++ b/src/pip/_vendor/packaging/__about__.py @@ -18,7 +18,7 @@ __summary__ = "Core utilities for Python packages" __uri__ = "https://github.com/pypa/packaging" -__version__ = "20.1" +__version__ = "20.3" __author__ = "Donald Stufft and individual contributors" __email__ = "donald@stufft.io" diff --git a/src/pip/_vendor/packaging/tags.py b/src/pip/_vendor/packaging/tags.py index 60a69d8f943..300faab8476 100644 --- a/src/pip/_vendor/packaging/tags.py +++ b/src/pip/_vendor/packaging/tags.py @@ -162,7 +162,7 @@ def _cpython_abis(py_version, warn=False): # type: (PythonVersion, bool) -> List[str] py_version = tuple(py_version) # To allow for version comparison. abis = [] - version = "{}{}".format(*py_version[:2]) + version = _version_nodot(py_version[:2]) debug = pymalloc = ucs4 = "" with_debug = _get_config_var("Py_DEBUG", warn) has_refcount = hasattr(sys, "gettotalrefcount") @@ -221,10 +221,7 @@ def cpython_tags( if not python_version: python_version = sys.version_info[:2] - if len(python_version) < 2: - interpreter = "cp{}".format(python_version[0]) - else: - interpreter = "cp{}{}".format(*python_version[:2]) + interpreter = "cp{}".format(_version_nodot(python_version[:2])) if abis is None: if len(python_version) > 1: @@ -252,8 +249,8 @@ def cpython_tags( if _abi3_applies(python_version): for minor_version in range(python_version[1] - 1, 1, -1): for platform_ in platforms: - interpreter = "cp{major}{minor}".format( - major=python_version[0], minor=minor_version + interpreter = "cp{version}".format( + version=_version_nodot((python_version[0], minor_version)) ) yield Tag(interpreter, "abi3", platform_) @@ -305,11 +302,11 @@ def _py_interpreter_range(py_version): all previous versions of that major version. """ if len(py_version) > 1: - yield "py{major}{minor}".format(major=py_version[0], minor=py_version[1]) + yield "py{version}".format(version=_version_nodot(py_version[:2])) yield "py{major}".format(major=py_version[0]) if len(py_version) > 1: for minor in range(py_version[1] - 1, -1, -1): - yield "py{major}{minor}".format(major=py_version[0], minor=minor) + yield "py{version}".format(version=_version_nodot((py_version[0], minor))) def compatible_tags( @@ -636,8 +633,11 @@ def _have_compatible_manylinux_abi(arch): def _linux_platforms(is_32bit=_32_BIT_INTERPRETER): # type: (bool) -> Iterator[str] linux = _normalize_string(distutils.util.get_platform()) - if linux == "linux_x86_64" and is_32bit: - linux = "linux_i686" + if is_32bit: + if linux == "linux_x86_64": + linux = "linux_i686" + elif linux == "linux_aarch64": + linux = "linux_armv7l" manylinux_support = [] _, arch = linux.split("_", 1) if _have_compatible_manylinux_abi(arch): @@ -704,10 +704,19 @@ def interpreter_version(**kwargs): if version: version = str(version) else: - version = "".join(map(str, sys.version_info[:2])) + version = _version_nodot(sys.version_info[:2]) return version +def _version_nodot(version): + # type: (PythonVersion) -> str + if any(v >= 10 for v in version): + sep = "_" + else: + sep = "" + return sep.join(map(str, version)) + + def sys_tags(**kwargs): # type: (bool) -> Iterator[Tag] """ diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 6bf6c0b331f..9288f2b189c 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -7,7 +7,7 @@ distro==1.5.0 html5lib==1.0.1 ipaddress==1.0.23 # Only needed on 2.6 and 2.7 msgpack==1.0.0 -packaging==20.1 +packaging==20.3 pep517==0.7.0 progress==1.5 pyparsing==2.4.6 From 8ce1872cdd40a0b4873e7ae1ef949f810182833a Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Wed, 15 Apr 2020 21:19:52 +0530 Subject: [PATCH 1631/3170] Upgrade pep517 to 0.8.2 --- news/pep517.vendor | 1 + src/pip/_vendor/pep517/__init__.py | 2 +- src/pip/_vendor/pep517/_in_process.py | 31 +++++++++++++++++++++++---- src/pip/_vendor/pep517/wrappers.py | 30 +++++++++++++++++--------- src/pip/_vendor/vendor.txt | 2 +- 5 files changed, 50 insertions(+), 16 deletions(-) create mode 100644 news/pep517.vendor diff --git a/news/pep517.vendor b/news/pep517.vendor new file mode 100644 index 00000000000..688e49ce8b9 --- /dev/null +++ b/news/pep517.vendor @@ -0,0 +1 @@ +Upgrade pep517 to 0.8.2. diff --git a/src/pip/_vendor/pep517/__init__.py b/src/pip/_vendor/pep517/__init__.py index 38d8e63ca1d..7355b68a240 100644 --- a/src/pip/_vendor/pep517/__init__.py +++ b/src/pip/_vendor/pep517/__init__.py @@ -1,4 +1,4 @@ """Wrappers to build Python packages using PEP 517 hooks """ -__version__ = '0.7.0' +__version__ = '0.8.2' diff --git a/src/pip/_vendor/pep517/_in_process.py b/src/pip/_vendor/pep517/_in_process.py index 1589a6cac58..a536b03e6bb 100644 --- a/src/pip/_vendor/pep517/_in_process.py +++ b/src/pip/_vendor/pep517/_in_process.py @@ -14,6 +14,7 @@ """ from glob import glob from importlib import import_module +import json import os import os.path from os.path import join as pjoin @@ -22,8 +23,30 @@ import sys import traceback -# This is run as a script, not a module, so it can't do a relative import -import compat +# This file is run as a script, and `import compat` is not zip-safe, so we +# include write_json() and read_json() from compat.py. +# +# Handle reading and writing JSON in UTF-8, on Python 3 and 2. + +if sys.version_info[0] >= 3: + # Python 3 + def write_json(obj, path, **kwargs): + with open(path, 'w', encoding='utf-8') as f: + json.dump(obj, f, **kwargs) + + def read_json(path): + with open(path, 'r', encoding='utf-8') as f: + return json.load(f) + +else: + # Python 2 + def write_json(obj, path, **kwargs): + with open(path, 'wb') as f: + json.dump(obj, f, encoding='utf-8', **kwargs) + + def read_json(path): + with open(path, 'rb') as f: + return json.load(f) class BackendUnavailable(Exception): @@ -233,7 +256,7 @@ def main(): sys.exit("Unknown hook: %s" % hook_name) hook = globals()[hook_name] - hook_input = compat.read_json(pjoin(control_dir, 'input.json')) + hook_input = read_json(pjoin(control_dir, 'input.json')) json_out = {'unsupported': False, 'return_val': None} try: @@ -250,7 +273,7 @@ def main(): except HookMissing: json_out['hook_missing'] = True - compat.write_json(json_out, pjoin(control_dir, 'output.json'), indent=2) + write_json(json_out, pjoin(control_dir, 'output.json'), indent=2) if __name__ == '__main__': diff --git a/src/pip/_vendor/pep517/wrappers.py b/src/pip/_vendor/pep517/wrappers.py index ad9a4f8c32f..00a3d1a789f 100644 --- a/src/pip/_vendor/pep517/wrappers.py +++ b/src/pip/_vendor/pep517/wrappers.py @@ -9,7 +9,16 @@ from . import compat -_in_proc_script = pjoin(dirname(abspath(__file__)), '_in_process.py') + +try: + import importlib.resources as resources + + def _in_proc_script_path(): + return resources.path(__package__, '_in_process.py') +except ImportError: + @contextmanager + def _in_proc_script_path(): + yield pjoin(dirname(abspath(__file__)), '_in_process.py') @contextmanager @@ -126,8 +135,6 @@ def __init__( self.backend_path = backend_path self._subprocess_runner = runner - # TODO: Is this over-engineered? Maybe frontends only need to - # set this when creating the wrapper, not on every call. @contextmanager def subprocess_runner(self, runner): """A context manager for temporarily overriding the default subprocess @@ -135,8 +142,10 @@ def subprocess_runner(self, runner): """ prev = self._subprocess_runner self._subprocess_runner = runner - yield - self._subprocess_runner = prev + try: + yield + finally: + self._subprocess_runner = prev def get_requires_for_build_wheel(self, config_settings=None): """Identify packages required for building a wheel @@ -242,11 +251,12 @@ def _call_hook(self, hook_name, kwargs): indent=2) # Run the hook in a subprocess - self._subprocess_runner( - [sys.executable, _in_proc_script, hook_name, td], - cwd=self.source_dir, - extra_environ=extra_environ - ) + with _in_proc_script_path() as script: + self._subprocess_runner( + [sys.executable, str(script), hook_name, td], + cwd=self.source_dir, + extra_environ=extra_environ + ) data = compat.read_json(pjoin(td, 'output.json')) if data.get('unsupported'): diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 9288f2b189c..c4bbb534f71 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -8,7 +8,7 @@ html5lib==1.0.1 ipaddress==1.0.23 # Only needed on 2.6 and 2.7 msgpack==1.0.0 packaging==20.3 -pep517==0.7.0 +pep517==0.8.2 progress==1.5 pyparsing==2.4.6 requests==2.22.0 From d2439f7d993d68df945c35e8e15d2c53a5c951aa Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Wed, 15 Apr 2020 21:20:54 +0530 Subject: [PATCH 1632/3170] Upgrade pyparsing to 2.4.7 --- news/pyparsing.vendor | 1 + src/pip/_vendor/pyparsing.py | 45 +++++++++++++++++++++++++----------- src/pip/_vendor/vendor.txt | 2 +- 3 files changed, 33 insertions(+), 15 deletions(-) create mode 100644 news/pyparsing.vendor diff --git a/news/pyparsing.vendor b/news/pyparsing.vendor new file mode 100644 index 00000000000..f892eb01359 --- /dev/null +++ b/news/pyparsing.vendor @@ -0,0 +1 @@ +Upgrade pyparsing to 2.4.7. diff --git a/src/pip/_vendor/pyparsing.py b/src/pip/_vendor/pyparsing.py index c461d6eca82..7ebc7eb994b 100644 --- a/src/pip/_vendor/pyparsing.py +++ b/src/pip/_vendor/pyparsing.py @@ -95,8 +95,8 @@ namespace class """ -__version__ = "2.4.6" -__versionTime__ = "24 Dec 2019 04:27 UTC" +__version__ = "2.4.7" +__versionTime__ = "30 Mar 2020 00:43 UTC" __author__ = "Paul McGuire <ptmcg@users.sourceforge.net>" import string @@ -1391,6 +1391,12 @@ def inlineLiteralsUsing(cls): """ ParserElement._literalStringClass = cls + @classmethod + def _trim_traceback(cls, tb): + while tb.tb_next: + tb = tb.tb_next + return tb + def __init__(self, savelist=False): self.parseAction = list() self.failAction = None @@ -1943,7 +1949,9 @@ def parseString(self, instring, parseAll=False): if ParserElement.verbose_stacktrace: raise else: - # catch and re-raise exception from here, clears out pyparsing internal stack trace + # catch and re-raise exception from here, clearing out pyparsing internal stack trace + if getattr(exc, '__traceback__', None) is not None: + exc.__traceback__ = self._trim_traceback(exc.__traceback__) raise exc else: return tokens @@ -2017,7 +2025,9 @@ def scanString(self, instring, maxMatches=_MAX_INT, overlap=False): if ParserElement.verbose_stacktrace: raise else: - # catch and re-raise exception from here, clears out pyparsing internal stack trace + # catch and re-raise exception from here, clearing out pyparsing internal stack trace + if getattr(exc, '__traceback__', None) is not None: + exc.__traceback__ = self._trim_traceback(exc.__traceback__) raise exc def transformString(self, instring): @@ -2063,7 +2073,9 @@ def transformString(self, instring): if ParserElement.verbose_stacktrace: raise else: - # catch and re-raise exception from here, clears out pyparsing internal stack trace + # catch and re-raise exception from here, clearing out pyparsing internal stack trace + if getattr(exc, '__traceback__', None) is not None: + exc.__traceback__ = self._trim_traceback(exc.__traceback__) raise exc def searchString(self, instring, maxMatches=_MAX_INT): @@ -2093,7 +2105,9 @@ def searchString(self, instring, maxMatches=_MAX_INT): if ParserElement.verbose_stacktrace: raise else: - # catch and re-raise exception from here, clears out pyparsing internal stack trace + # catch and re-raise exception from here, clearing out pyparsing internal stack trace + if getattr(exc, '__traceback__', None) is not None: + exc.__traceback__ = self._trim_traceback(exc.__traceback__) raise exc def split(self, instring, maxsplit=_MAX_INT, includeSeparators=False): @@ -2565,7 +2579,9 @@ def parseFile(self, file_or_filename, parseAll=False): if ParserElement.verbose_stacktrace: raise else: - # catch and re-raise exception from here, clears out pyparsing internal stack trace + # catch and re-raise exception from here, clearing out pyparsing internal stack trace + if getattr(exc, '__traceback__', None) is not None: + exc.__traceback__ = self._trim_traceback(exc.__traceback__) raise exc def __eq__(self, other): @@ -2724,7 +2740,7 @@ def runTests(self, tests, parseAll=True, comment='#', continue if not t: continue - out = ['\n'.join(comments), t] + out = ['\n' + '\n'.join(comments) if comments else '', t] comments = [] try: # convert newline marks to actual newlines, and strip leading BOM if present @@ -3312,7 +3328,7 @@ def __init__(self, pattern, flags=0, asGroupList=False, asMatch=False): self.name = _ustr(self) self.errmsg = "Expected " + self.name self.mayIndexError = False - self.mayReturnEmpty = True + self.mayReturnEmpty = self.re_match("") is not None self.asGroupList = asGroupList self.asMatch = asMatch if self.asGroupList: @@ -3993,6 +4009,7 @@ def __init__(self, *args, **kwargs): self.leaveWhitespace() def __init__(self, exprs, savelist=True): + exprs = list(exprs) if exprs and Ellipsis in exprs: tmp = [] for i, expr in enumerate(exprs): @@ -4358,7 +4375,7 @@ def parseImpl(self, instring, loc, doActions=True): if self.initExprGroups: self.opt1map = dict((id(e.expr), e) for e in self.exprs if isinstance(e, Optional)) opt1 = [e.expr for e in self.exprs if isinstance(e, Optional)] - opt2 = [e for e in self.exprs if e.mayReturnEmpty and not isinstance(e, Optional)] + opt2 = [e for e in self.exprs if e.mayReturnEmpty and not isinstance(e, (Optional, Regex))] self.optionals = opt1 + opt2 self.multioptionals = [e.expr for e in self.exprs if isinstance(e, ZeroOrMore)] self.multirequired = [e.expr for e in self.exprs if isinstance(e, OneOrMore)] @@ -5435,8 +5452,8 @@ def mustMatchTheseTokens(s, l, t): return rep def _escapeRegexRangeChars(s): - # ~ escape these chars: ^-] - for c in r"\^-]": + # ~ escape these chars: ^-[] + for c in r"\^-[]": s = s.replace(c, _bslash + c) s = s.replace("\n", r"\n") s = s.replace("\t", r"\t") @@ -6550,10 +6567,10 @@ class pyparsing_common: """mixed integer of the form 'integer - fraction', with optional leading integer, returns float""" mixed_integer.addParseAction(sum) - real = Regex(r'[+-]?(:?\d+\.\d*|\.\d+)').setName("real number").setParseAction(convertToFloat) + real = Regex(r'[+-]?(?:\d+\.\d*|\.\d+)').setName("real number").setParseAction(convertToFloat) """expression that parses a floating point number and returns a float""" - sci_real = Regex(r'[+-]?(:?\d+(:?[eE][+-]?\d+)|(:?\d+\.\d*|\.\d+)(:?[eE][+-]?\d+)?)').setName("real number with scientific notation").setParseAction(convertToFloat) + sci_real = Regex(r'[+-]?(?:\d+(?:[eE][+-]?\d+)|(?:\d+\.\d*|\.\d+)(?:[eE][+-]?\d+)?)').setName("real number with scientific notation").setParseAction(convertToFloat) """expression that parses a floating point number with optional scientific notation and returns a float""" diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index c4bbb534f71..6d285b8b2c5 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -10,7 +10,7 @@ msgpack==1.0.0 packaging==20.3 pep517==0.8.2 progress==1.5 -pyparsing==2.4.6 +pyparsing==2.4.7 requests==2.22.0 certifi==2019.11.28 chardet==3.0.4 From e94f93530fe8a1734ec093f3c66c46a4e172d0ba Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Wed, 15 Apr 2020 21:24:49 +0530 Subject: [PATCH 1633/3170] Upgrade requests to 2.23.0 --- news/requests.vendor | 1 + src/pip/_vendor/requests/LICENSE | 2 +- src/pip/_vendor/requests/__init__.py | 12 ++++++------ src/pip/_vendor/requests/__version__.py | 8 ++++---- src/pip/_vendor/requests/api.py | 7 +++++-- src/pip/_vendor/requests/auth.py | 4 ++-- src/pip/_vendor/requests/compat.py | 2 ++ src/pip/_vendor/requests/models.py | 7 ++++--- src/pip/_vendor/requests/sessions.py | 23 ++++++++++------------- src/pip/_vendor/requests/status_codes.py | 15 +++++++++------ src/pip/_vendor/requests/structures.py | 4 +++- src/pip/_vendor/requests/utils.py | 11 ++++++++--- src/pip/_vendor/vendor.txt | 2 +- 13 files changed, 56 insertions(+), 42 deletions(-) create mode 100644 news/requests.vendor diff --git a/news/requests.vendor b/news/requests.vendor new file mode 100644 index 00000000000..b39c46159e7 --- /dev/null +++ b/news/requests.vendor @@ -0,0 +1 @@ +Upgrade requests to 2.23.0. diff --git a/src/pip/_vendor/requests/LICENSE b/src/pip/_vendor/requests/LICENSE index 841c6023b9b..13d91ddc7a8 100644 --- a/src/pip/_vendor/requests/LICENSE +++ b/src/pip/_vendor/requests/LICENSE @@ -1,4 +1,4 @@ -Copyright 2018 Kenneth Reitz +Copyright 2019 Kenneth Reitz Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. diff --git a/src/pip/_vendor/requests/__init__.py b/src/pip/_vendor/requests/__init__.py index 1d30e3e0631..e47bcb20149 100644 --- a/src/pip/_vendor/requests/__init__.py +++ b/src/pip/_vendor/requests/__init__.py @@ -9,14 +9,14 @@ Requests HTTP Library ~~~~~~~~~~~~~~~~~~~~~ -Requests is an HTTP library, written in Python, for human beings. Basic GET -usage: +Requests is an HTTP library, written in Python, for human beings. +Basic GET usage: >>> import requests >>> r = requests.get('https://www.python.org') >>> r.status_code 200 - >>> 'Python is a programming language' in r.content + >>> b'Python is a programming language' in r.content True ... or POST: @@ -27,14 +27,14 @@ { ... "form": { - "key2": "value2", - "key1": "value1" + "key1": "value1", + "key2": "value2" }, ... } The other HTTP methods are supported - see `requests.api`. Full documentation -is at <http://python-requests.org>. +is at <https://requests.readthedocs.io>. :copyright: (c) 2017 by Kenneth Reitz. :license: Apache 2.0, see LICENSE for more details. diff --git a/src/pip/_vendor/requests/__version__.py b/src/pip/_vendor/requests/__version__.py index 9844f740abe..b9e7df4881a 100644 --- a/src/pip/_vendor/requests/__version__.py +++ b/src/pip/_vendor/requests/__version__.py @@ -4,11 +4,11 @@ __title__ = 'requests' __description__ = 'Python HTTP for Humans.' -__url__ = 'http://python-requests.org' -__version__ = '2.22.0' -__build__ = 0x022200 +__url__ = 'https://requests.readthedocs.io' +__version__ = '2.23.0' +__build__ = 0x022300 __author__ = 'Kenneth Reitz' __author_email__ = 'me@kennethreitz.org' __license__ = 'Apache 2.0' -__copyright__ = 'Copyright 2019 Kenneth Reitz' +__copyright__ = 'Copyright 2020 Kenneth Reitz' __cake__ = u'\u2728 \U0001f370 \u2728' diff --git a/src/pip/_vendor/requests/api.py b/src/pip/_vendor/requests/api.py index ef71d0759ea..e978e203118 100644 --- a/src/pip/_vendor/requests/api.py +++ b/src/pip/_vendor/requests/api.py @@ -16,7 +16,7 @@ def request(method, url, **kwargs): """Constructs and sends a :class:`Request <Request>`. - :param method: method for the new :class:`Request` object. + :param method: method for the new :class:`Request` object: ``GET``, ``OPTIONS``, ``HEAD``, ``POST``, ``PUT``, ``PATCH``, or ``DELETE``. :param url: URL for the new :class:`Request` object. :param params: (optional) Dictionary, list of tuples or bytes to send in the query string for the :class:`Request`. @@ -50,6 +50,7 @@ def request(method, url, **kwargs): >>> import requests >>> req = requests.request('GET', 'https://httpbin.org/get') + >>> req <Response [200]> """ @@ -92,7 +93,9 @@ def head(url, **kwargs): r"""Sends a HEAD request. :param url: URL for the new :class:`Request` object. - :param \*\*kwargs: Optional arguments that ``request`` takes. + :param \*\*kwargs: Optional arguments that ``request`` takes. If + `allow_redirects` is not provided, it will be set to `False` (as + opposed to the default :meth:`request` behavior). :return: :class:`Response <Response>` object :rtype: requests.Response """ diff --git a/src/pip/_vendor/requests/auth.py b/src/pip/_vendor/requests/auth.py index bdde51c7fd1..eeface39ae6 100644 --- a/src/pip/_vendor/requests/auth.py +++ b/src/pip/_vendor/requests/auth.py @@ -50,7 +50,7 @@ def _basic_auth_str(username, password): "Non-string passwords will no longer be supported in Requests " "3.0.0. Please convert the object you've passed in ({!r}) to " "a string or bytes object in the near future to avoid " - "problems.".format(password), + "problems.".format(type(password)), category=DeprecationWarning, ) password = str(password) @@ -239,7 +239,7 @@ def handle_401(self, r, **kwargs): """ # If response is not 4xx, do not auth - # See https://github.com/requests/requests/issues/3772 + # See https://github.com/psf/requests/issues/3772 if not 400 <= r.status_code < 500: self._thread_local.num_401_calls = 1 return r diff --git a/src/pip/_vendor/requests/compat.py b/src/pip/_vendor/requests/compat.py index 6a86893dc30..9e29371678b 100644 --- a/src/pip/_vendor/requests/compat.py +++ b/src/pip/_vendor/requests/compat.py @@ -47,6 +47,7 @@ import cookielib from Cookie import Morsel from StringIO import StringIO + # Keep OrderedDict for backwards compatibility. from collections import Callable, Mapping, MutableMapping, OrderedDict @@ -63,6 +64,7 @@ from http import cookiejar as cookielib from http.cookies import Morsel from io import StringIO + # Keep OrderedDict for backwards compatibility. from collections import OrderedDict from collections.abc import Callable, Mapping, MutableMapping diff --git a/src/pip/_vendor/requests/models.py b/src/pip/_vendor/requests/models.py index 0839957475d..8a3085d3783 100644 --- a/src/pip/_vendor/requests/models.py +++ b/src/pip/_vendor/requests/models.py @@ -12,7 +12,7 @@ # Import encoding now, to avoid implicit import later. # Implicit import within threads may cause LookupError when standard library is in a ZIP, -# such as in Embedded Python. See https://github.com/requests/requests/issues/3578. +# such as in Embedded Python. See https://github.com/psf/requests/issues/3578. import encodings.idna from pip._vendor.urllib3.fields import RequestField @@ -280,6 +280,7 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin): >>> import requests >>> req = requests.Request('GET', 'https://httpbin.org/get') >>> r = req.prepare() + >>> r <PreparedRequest [GET]> >>> s = requests.Session() @@ -358,7 +359,7 @@ def prepare_url(self, url, params): #: We're unable to blindly call unicode/str functions #: as this will include the bytestring indicator (b'') #: on python 3.x. - #: https://github.com/requests/requests/pull/2238 + #: https://github.com/psf/requests/pull/2238 if isinstance(url, bytes): url = url.decode('utf8') else: @@ -608,7 +609,7 @@ def __init__(self): #: File-like object representation of response (for advanced usage). #: Use of ``raw`` requires that ``stream=True`` be set on the request. - # This requirement does not apply for use internally to Requests. + #: This requirement does not apply for use internally to Requests. self.raw = None #: Final URL location of Response. diff --git a/src/pip/_vendor/requests/sessions.py b/src/pip/_vendor/requests/sessions.py index d73d700fa6b..2845880bf41 100644 --- a/src/pip/_vendor/requests/sessions.py +++ b/src/pip/_vendor/requests/sessions.py @@ -11,9 +11,10 @@ import sys import time from datetime import timedelta +from collections import OrderedDict from .auth import _basic_auth_str -from .compat import cookielib, is_py3, OrderedDict, urljoin, urlparse, Mapping +from .compat import cookielib, is_py3, urljoin, urlparse, Mapping from .cookies import ( cookiejar_from_dict, extract_cookies_to_jar, RequestsCookieJar, merge_cookies) from .models import Request, PreparedRequest, DEFAULT_REDIRECT_LIMIT @@ -162,7 +163,7 @@ def resolve_redirects(self, resp, req, stream=False, timeout=None, resp.raw.read(decode_content=False) if len(resp.history) >= self.max_redirects: - raise TooManyRedirects('Exceeded %s redirects.' % self.max_redirects, response=resp) + raise TooManyRedirects('Exceeded {} redirects.'.format(self.max_redirects), response=resp) # Release the connection back into the pool. resp.close() @@ -170,7 +171,7 @@ def resolve_redirects(self, resp, req, stream=False, timeout=None, # Handle redirection without scheme (see: RFC 1808 Section 4) if url.startswith('//'): parsed_rurl = urlparse(resp.url) - url = '%s:%s' % (to_native_string(parsed_rurl.scheme), url) + url = ':'.join([to_native_string(parsed_rurl.scheme), url]) # Normalize url case and attach previous fragment if needed (RFC 7231 7.1.2) parsed = urlparse(url) @@ -192,19 +193,16 @@ def resolve_redirects(self, resp, req, stream=False, timeout=None, self.rebuild_method(prepared_request, resp) - # https://github.com/requests/requests/issues/1084 + # https://github.com/psf/requests/issues/1084 if resp.status_code not in (codes.temporary_redirect, codes.permanent_redirect): - # https://github.com/requests/requests/issues/3490 + # https://github.com/psf/requests/issues/3490 purged_headers = ('Content-Length', 'Content-Type', 'Transfer-Encoding') for header in purged_headers: prepared_request.headers.pop(header, None) prepared_request.body = None headers = prepared_request.headers - try: - del headers['Cookie'] - except KeyError: - pass + headers.pop('Cookie', None) # Extract any cookies sent on the response to the cookiejar # in the new request. Because we've mutated our copied prepared @@ -271,7 +269,6 @@ def rebuild_auth(self, prepared_request, response): if new_auth is not None: prepared_request.prepare_auth(new_auth) - return def rebuild_proxies(self, prepared_request, proxies): """This method re-evaluates the proxy configuration by considering the @@ -352,13 +349,13 @@ class Session(SessionRedirectMixin): Or as a context manager:: >>> with requests.Session() as s: - >>> s.get('https://httpbin.org/get') + ... s.get('https://httpbin.org/get') <Response [200]> """ __attrs__ = [ 'headers', 'cookies', 'auth', 'proxies', 'hooks', 'params', 'verify', - 'cert', 'prefetch', 'adapters', 'stream', 'trust_env', + 'cert', 'adapters', 'stream', 'trust_env', 'max_redirects', ] @@ -728,7 +725,7 @@ def get_adapter(self, url): return adapter # Nothing matches :-/ - raise InvalidSchema("No connection adapters were found for '%s'" % url) + raise InvalidSchema("No connection adapters were found for {!r}".format(url)) def close(self): """Closes all adapters and as such the session""" diff --git a/src/pip/_vendor/requests/status_codes.py b/src/pip/_vendor/requests/status_codes.py index 813e8c4e62f..d80a7cd4dd4 100644 --- a/src/pip/_vendor/requests/status_codes.py +++ b/src/pip/_vendor/requests/status_codes.py @@ -5,12 +5,15 @@ to their numerical codes, accessible either as attributes or as dictionary items. ->>> requests.codes['temporary_redirect'] -307 ->>> requests.codes.teapot -418 ->>> requests.codes['\o/'] -200 +Example:: + + >>> import requests + >>> requests.codes['temporary_redirect'] + 307 + >>> requests.codes.teapot + 418 + >>> requests.codes['\o/'] + 200 Some codes have multiple names, and both upper- and lower-case versions of the names are allowed. For example, ``codes.ok``, ``codes.OK``, and diff --git a/src/pip/_vendor/requests/structures.py b/src/pip/_vendor/requests/structures.py index da930e28520..8ee0ba7a082 100644 --- a/src/pip/_vendor/requests/structures.py +++ b/src/pip/_vendor/requests/structures.py @@ -7,7 +7,9 @@ Data structures that power Requests. """ -from .compat import OrderedDict, Mapping, MutableMapping +from collections import OrderedDict + +from .compat import Mapping, MutableMapping class CaseInsensitiveDict(MutableMapping): diff --git a/src/pip/_vendor/requests/utils.py b/src/pip/_vendor/requests/utils.py index 8170a8d2c45..c1700d7fe85 100644 --- a/src/pip/_vendor/requests/utils.py +++ b/src/pip/_vendor/requests/utils.py @@ -19,6 +19,7 @@ import tempfile import warnings import zipfile +from collections import OrderedDict from .__version__ import __version__ from . import certs @@ -26,7 +27,7 @@ from ._internal_utils import to_native_string from .compat import parse_http_list as _parse_list_header from .compat import ( - quote, urlparse, bytes, str, OrderedDict, unquote, getproxies, + quote, urlparse, bytes, str, unquote, getproxies, proxy_bypass, urlunparse, basestring, integer_types, is_py3, proxy_bypass_environment, getproxies_environment, Mapping) from .cookies import cookiejar_from_dict @@ -179,7 +180,7 @@ def get_netrc_auth(url, raise_errors=False): except KeyError: # os.path.expanduser can fail when $HOME is undefined and # getpwuid fails. See https://bugs.python.org/issue20164 & - # https://github.com/requests/requests/issues/1846 + # https://github.com/psf/requests/issues/1846 return if os.path.exists(loc): @@ -266,6 +267,8 @@ def from_key_val_list(value): >>> from_key_val_list([('key', 'val')]) OrderedDict([('key', 'val')]) >>> from_key_val_list('string') + Traceback (most recent call last): + ... ValueError: cannot encode objects that are not 2-tuples >>> from_key_val_list({'key': 'val'}) OrderedDict([('key', 'val')]) @@ -292,7 +295,9 @@ def to_key_val_list(value): >>> to_key_val_list({'key': 'val'}) [('key', 'val')] >>> to_key_val_list('string') - ValueError: cannot encode objects that are not 2-tuples. + Traceback (most recent call last): + ... + ValueError: cannot encode objects that are not 2-tuples :rtype: list """ diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 6d285b8b2c5..548bc04d436 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -11,7 +11,7 @@ packaging==20.3 pep517==0.8.2 progress==1.5 pyparsing==2.4.7 -requests==2.22.0 +requests==2.23.0 certifi==2019.11.28 chardet==3.0.4 idna==2.8 From 05aae8f8884de086de2871a09ac745a58cebebf7 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Wed, 15 Apr 2020 21:25:33 +0530 Subject: [PATCH 1634/3170] Upgrade certifi to 2020.4.5.1 --- news/certifi.vendor | 1 + src/pip/_vendor/certifi/__init__.py | 4 +-- src/pip/_vendor/certifi/__main__.py | 14 +++++++++-- src/pip/_vendor/certifi/cacert.pem | 39 +++++++++++++++++++++++++++++ src/pip/_vendor/certifi/core.py | 19 ++++++++++++-- src/pip/_vendor/vendor.txt | 2 +- 6 files changed, 72 insertions(+), 7 deletions(-) create mode 100644 news/certifi.vendor diff --git a/news/certifi.vendor b/news/certifi.vendor new file mode 100644 index 00000000000..88746035e42 --- /dev/null +++ b/news/certifi.vendor @@ -0,0 +1 @@ +Upgrade certifi to 2020.4.5.1. diff --git a/src/pip/_vendor/certifi/__init__.py b/src/pip/_vendor/certifi/__init__.py index 0d59a05630e..1e2dfac7dbe 100644 --- a/src/pip/_vendor/certifi/__init__.py +++ b/src/pip/_vendor/certifi/__init__.py @@ -1,3 +1,3 @@ -from .core import where +from .core import contents, where -__version__ = "2019.11.28" +__version__ = "2020.04.05.1" diff --git a/src/pip/_vendor/certifi/__main__.py b/src/pip/_vendor/certifi/__main__.py index ae2aff5c804..00376349e69 100644 --- a/src/pip/_vendor/certifi/__main__.py +++ b/src/pip/_vendor/certifi/__main__.py @@ -1,2 +1,12 @@ -from pip._vendor.certifi import where -print(where()) +import argparse + +from pip._vendor.certifi import contents, where + +parser = argparse.ArgumentParser() +parser.add_argument("-c", "--contents", action="store_true") +args = parser.parse_args() + +if args.contents: + print(contents()) +else: + print(where()) diff --git a/src/pip/_vendor/certifi/cacert.pem b/src/pip/_vendor/certifi/cacert.pem index a4758ef3afb..ece147c9dc8 100644 --- a/src/pip/_vendor/certifi/cacert.pem +++ b/src/pip/_vendor/certifi/cacert.pem @@ -2140,6 +2140,45 @@ t/2jioSgrGK+KwmHNPBqAbubKVY8/gA3zyNs8U6qtnRGEmyR7jTV7JqR50S+kDFy SjnRBUkLp7Y3gaVdjKozXoEofKd9J+sAro03 -----END CERTIFICATE----- +# Issuer: CN=EC-ACC O=Agencia Catalana de Certificacio (NIF Q-0801176-I) OU=Serveis Publics de Certificacio/Vegeu https://www.catcert.net/verarrel (c)03/Jerarquia Entitats de Certificacio Catalanes +# Subject: CN=EC-ACC O=Agencia Catalana de Certificacio (NIF Q-0801176-I) OU=Serveis Publics de Certificacio/Vegeu https://www.catcert.net/verarrel (c)03/Jerarquia Entitats de Certificacio Catalanes +# Label: "EC-ACC" +# Serial: -23701579247955709139626555126524820479 +# MD5 Fingerprint: eb:f5:9d:29:0d:61:f9:42:1f:7c:c2:ba:6d:e3:15:09 +# SHA1 Fingerprint: 28:90:3a:63:5b:52:80:fa:e6:77:4c:0b:6d:a7:d6:ba:a6:4a:f2:e8 +# SHA256 Fingerprint: 88:49:7f:01:60:2f:31:54:24:6a:e2:8c:4d:5a:ef:10:f1:d8:7e:bb:76:62:6f:4a:e0:b7:f9:5b:a7:96:87:99 +-----BEGIN CERTIFICATE----- +MIIFVjCCBD6gAwIBAgIQ7is969Qh3hSoYqwE893EATANBgkqhkiG9w0BAQUFADCB +8zELMAkGA1UEBhMCRVMxOzA5BgNVBAoTMkFnZW5jaWEgQ2F0YWxhbmEgZGUgQ2Vy +dGlmaWNhY2lvIChOSUYgUS0wODAxMTc2LUkpMSgwJgYDVQQLEx9TZXJ2ZWlzIFB1 +YmxpY3MgZGUgQ2VydGlmaWNhY2lvMTUwMwYDVQQLEyxWZWdldSBodHRwczovL3d3 +dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbCAoYykwMzE1MDMGA1UECxMsSmVyYXJxdWlh +IEVudGl0YXRzIGRlIENlcnRpZmljYWNpbyBDYXRhbGFuZXMxDzANBgNVBAMTBkVD +LUFDQzAeFw0wMzAxMDcyMzAwMDBaFw0zMTAxMDcyMjU5NTlaMIHzMQswCQYDVQQG +EwJFUzE7MDkGA1UEChMyQWdlbmNpYSBDYXRhbGFuYSBkZSBDZXJ0aWZpY2FjaW8g +KE5JRiBRLTA4MDExNzYtSSkxKDAmBgNVBAsTH1NlcnZlaXMgUHVibGljcyBkZSBD +ZXJ0aWZpY2FjaW8xNTAzBgNVBAsTLFZlZ2V1IGh0dHBzOi8vd3d3LmNhdGNlcnQu +bmV0L3ZlcmFycmVsIChjKTAzMTUwMwYDVQQLEyxKZXJhcnF1aWEgRW50aXRhdHMg +ZGUgQ2VydGlmaWNhY2lvIENhdGFsYW5lczEPMA0GA1UEAxMGRUMtQUNDMIIBIjAN +BgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsyLHT+KXQpWIR4NA9h0X84NzJB5R +85iKw5K4/0CQBXCHYMkAqbWUZRkiFRfCQ2xmRJoNBD45b6VLeqpjt4pEndljkYRm +4CgPukLjbo73FCeTae6RDqNfDrHrZqJyTxIThmV6PttPB/SnCWDaOkKZx7J/sxaV +HMf5NLWUhdWZXqBIoH7nF2W4onW4HvPlQn2v7fOKSGRdghST2MDk/7NQcvJ29rNd +QlB50JQ+awwAvthrDk4q7D7SzIKiGGUzE3eeml0aE9jD2z3Il3rucO2n5nzbcc8t +lGLfbdb1OL4/pYUKGbio2Al1QnDE6u/LDsg0qBIimAy4E5S2S+zw0JDnJwIDAQAB +o4HjMIHgMB0GA1UdEQQWMBSBEmVjX2FjY0BjYXRjZXJ0Lm5ldDAPBgNVHRMBAf8E +BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUoMOLRKo3pUW/l4Ba0fF4 +opvpXY0wfwYDVR0gBHgwdjB0BgsrBgEEAfV4AQMBCjBlMCwGCCsGAQUFBwIBFiBo +dHRwczovL3d3dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbDA1BggrBgEFBQcCAjApGidW +ZWdldSBodHRwczovL3d3dy5jYXRjZXJ0Lm5ldC92ZXJhcnJlbCAwDQYJKoZIhvcN +AQEFBQADggEBAKBIW4IB9k1IuDlVNZyAelOZ1Vr/sXE7zDkJlF7W2u++AVtd0x7Y +/X1PzaBB4DSTv8vihpw3kpBWHNzrKQXlxJ7HNd+KDM3FIUPpqojlNcAZQmNaAl6k +SBg6hW/cnbw/nZzBh7h6YQjpdwt/cKt63dmXLGQehb+8dJahw3oS7AwaboMMPOhy +Rp/7SNVel+axofjk70YllJyJ22k4vuxcDlbHZVHlUIiIv0LVKz3l+bqeLrPK9HOS +Agu+TGbrIP65y7WZf+a2E/rKS03Z7lNGBjvGTq2TWoF+bCpLagVFjPIhpDGQh2xl +nJ2lYJU6Un/10asIbvPuW/mIPX64b24D5EI= +-----END CERTIFICATE----- + # Issuer: CN=Hellenic Academic and Research Institutions RootCA 2011 O=Hellenic Academic and Research Institutions Cert. Authority # Subject: CN=Hellenic Academic and Research Institutions RootCA 2011 O=Hellenic Academic and Research Institutions Cert. Authority # Label: "Hellenic Academic and Research Institutions RootCA 2011" diff --git a/src/pip/_vendor/certifi/core.py b/src/pip/_vendor/certifi/core.py index 7271acf40ee..56b52a3c8f4 100644 --- a/src/pip/_vendor/certifi/core.py +++ b/src/pip/_vendor/certifi/core.py @@ -4,12 +4,27 @@ certifi.py ~~~~~~~~~~ -This module returns the installation location of cacert.pem. +This module returns the installation location of cacert.pem or its contents. """ import os +try: + from importlib.resources import read_text +except ImportError: + # This fallback will work for Python versions prior to 3.7 that lack the + # importlib.resources module but relies on the existing `where` function + # so won't address issues with environments like PyOxidizer that don't set + # __file__ on modules. + def read_text(_module, _path, encoding="ascii"): + with open(where(), "r", encoding=encoding) as data: + return data.read() + def where(): f = os.path.dirname(__file__) - return os.path.join(f, 'cacert.pem') + return os.path.join(f, "cacert.pem") + + +def contents(): + return read_text("certifi", "cacert.pem", encoding="ascii") diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 548bc04d436..55099f4c33d 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -12,7 +12,7 @@ pep517==0.8.2 progress==1.5 pyparsing==2.4.7 requests==2.23.0 - certifi==2019.11.28 + certifi==2020.4.5.1 chardet==3.0.4 idna==2.8 urllib3==1.25.7 From 812f2a9f5bf5e6956829aaac39241bd34b492641 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Wed, 15 Apr 2020 21:26:59 +0530 Subject: [PATCH 1635/3170] Upgrade idna to 2.9 --- news/idna.vendor | 1 + src/pip/_vendor/idna/LICENSE.rst | 52 +- src/pip/_vendor/idna/core.py | 4 +- src/pip/_vendor/idna/idnadata.py | 56 ++- src/pip/_vendor/idna/package_data.py | 2 +- src/pip/_vendor/idna/uts46data.py | 724 ++++++++++++++++----------- src/pip/_vendor/vendor.txt | 2 +- 7 files changed, 461 insertions(+), 380 deletions(-) create mode 100644 news/idna.vendor diff --git a/news/idna.vendor b/news/idna.vendor new file mode 100644 index 00000000000..2029253f5fd --- /dev/null +++ b/news/idna.vendor @@ -0,0 +1 @@ +Upgrade idna to 2.9. diff --git a/src/pip/_vendor/idna/LICENSE.rst b/src/pip/_vendor/idna/LICENSE.rst index 3ee64fba29e..63664b82e7a 100644 --- a/src/pip/_vendor/idna/LICENSE.rst +++ b/src/pip/_vendor/idna/LICENSE.rst @@ -1,7 +1,9 @@ License ------- -Copyright (c) 2013-2018, Kim Davies. All rights reserved. +License: bsd-3-clause + +Copyright (c) 2013-2020, Kim Davies. All rights reserved. Redistribution and use in source and binary forms, with or without modification, are permitted provided that the following conditions are met: @@ -30,51 +32,3 @@ modification, are permitted provided that the following conditions are met: (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -Portions of the codec implementation and unit tests are derived from the -Python standard library, which carries the `Python Software Foundation -License <https://docs.python.org/2/license.html>`_: - - Copyright (c) 2001-2014 Python Software Foundation; All Rights Reserved - -Portions of the unit tests are derived from the Unicode standard, which -is subject to the Unicode, Inc. License Agreement: - - Copyright (c) 1991-2014 Unicode, Inc. All rights reserved. - Distributed under the Terms of Use in - <http://www.unicode.org/copyright.html>. - - Permission is hereby granted, free of charge, to any person obtaining - a copy of the Unicode data files and any associated documentation - (the "Data Files") or Unicode software and any associated documentation - (the "Software") to deal in the Data Files or Software - without restriction, including without limitation the rights to use, - copy, modify, merge, publish, distribute, and/or sell copies of - the Data Files or Software, and to permit persons to whom the Data Files - or Software are furnished to do so, provided that - - (a) this copyright and permission notice appear with all copies - of the Data Files or Software, - - (b) this copyright and permission notice appear in associated - documentation, and - - (c) there is clear notice in each modified Data File or in the Software - as well as in the documentation associated with the Data File(s) or - Software that the data or software has been modified. - - THE DATA FILES AND SOFTWARE ARE 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 OF THIRD PARTY RIGHTS. - IN NO EVENT SHALL THE COPYRIGHT HOLDER OR HOLDERS INCLUDED IN THIS - NOTICE BE LIABLE FOR ANY CLAIM, OR ANY SPECIAL INDIRECT OR CONSEQUENTIAL - DAMAGES, OR ANY DAMAGES WHATSOEVER RESULTING FROM LOSS OF USE, - DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR OTHER - TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR - PERFORMANCE OF THE DATA FILES OR SOFTWARE. - - Except as contained in this notice, the name of a copyright holder - shall not be used in advertising or otherwise to promote the sale, - use or other dealings in these Data Files or Software without prior - written authorization of the copyright holder. diff --git a/src/pip/_vendor/idna/core.py b/src/pip/_vendor/idna/core.py index 104624ad2dd..9c3bba2ad7d 100644 --- a/src/pip/_vendor/idna/core.py +++ b/src/pip/_vendor/idna/core.py @@ -9,7 +9,7 @@ _alabel_prefix = b'xn--' _unicode_dots_re = re.compile(u'[\u002e\u3002\uff0e\uff61]') -if sys.version_info[0] == 3: +if sys.version_info[0] >= 3: unicode = str unichr = chr @@ -300,6 +300,8 @@ def ulabel(label): label = label.lower() if label.startswith(_alabel_prefix): label = label[len(_alabel_prefix):] + if label.decode('ascii')[-1] == '-': + raise IDNAError('A-label must not end with a hyphen') else: check_label(label) return label.decode('ascii') diff --git a/src/pip/_vendor/idna/idnadata.py b/src/pip/_vendor/idna/idnadata.py index a80c959d2a7..2b81c522cf5 100644 --- a/src/pip/_vendor/idna/idnadata.py +++ b/src/pip/_vendor/idna/idnadata.py @@ -1,6 +1,6 @@ # This file is automatically generated by tools/idna-data -__version__ = "11.0.0" +__version__ = "12.1.0" scripts = { 'Greek': ( 0x37000000374, @@ -74,6 +74,7 @@ 0x304100003097, 0x309d000030a0, 0x1b0010001b11f, + 0x1b1500001b153, 0x1f2000001f201, ), 'Katakana': ( @@ -85,6 +86,7 @@ 0xff660000ff70, 0xff710000ff9e, 0x1b0000001b001, + 0x1b1640001b168, ), } joining_types = { @@ -824,6 +826,7 @@ 0x1e941: 68, 0x1e942: 68, 0x1e943: 68, + 0x1e94b: 84, } codepoint_classes = { 'PVALID': ( @@ -1258,18 +1261,11 @@ 0xe5000000e5a, 0xe8100000e83, 0xe8400000e85, - 0xe8700000e89, - 0xe8a00000e8b, - 0xe8d00000e8e, - 0xe9400000e98, - 0xe9900000ea0, - 0xea100000ea4, + 0xe8600000e8b, + 0xe8c00000ea4, 0xea500000ea6, - 0xea700000ea8, - 0xeaa00000eac, - 0xead00000eb3, - 0xeb400000eba, - 0xebb00000ebe, + 0xea700000eb3, + 0xeb400000ebe, 0xec000000ec5, 0xec600000ec7, 0xec800000ece, @@ -1370,7 +1366,7 @@ 0x1c4000001c4a, 0x1c4d00001c7e, 0x1cd000001cd3, - 0x1cd400001cfa, + 0x1cd400001cfb, 0x1d0000001d2c, 0x1d2f00001d30, 0x1d3b00001d3c, @@ -1727,6 +1723,10 @@ 0xa7b50000a7b6, 0xa7b70000a7b8, 0xa7b90000a7ba, + 0xa7bb0000a7bc, + 0xa7bd0000a7be, + 0xa7bf0000a7c0, + 0xa7c30000a7c4, 0xa7f70000a7f8, 0xa7fa0000a828, 0xa8400000a874, @@ -1753,7 +1753,7 @@ 0xab200000ab27, 0xab280000ab2f, 0xab300000ab5b, - 0xab600000ab66, + 0xab600000ab68, 0xabc00000abeb, 0xabec0000abee, 0xabf00000abfa, @@ -1830,6 +1830,7 @@ 0x10f0000010f1d, 0x10f2700010f28, 0x10f3000010f51, + 0x10fe000010ff7, 0x1100000011047, 0x1106600011070, 0x1107f000110bb, @@ -1871,7 +1872,7 @@ 0x1137000011375, 0x114000001144b, 0x114500001145a, - 0x1145e0001145f, + 0x1145e00011460, 0x11480000114c6, 0x114c7000114c8, 0x114d0000114da, @@ -1881,7 +1882,7 @@ 0x1160000011641, 0x1164400011645, 0x116500001165a, - 0x11680000116b8, + 0x11680000116b9, 0x116c0000116ca, 0x117000001171b, 0x1171d0001172c, @@ -1889,10 +1890,13 @@ 0x118000001183b, 0x118c0000118ea, 0x118ff00011900, + 0x119a0000119a8, + 0x119aa000119d8, + 0x119da000119e2, + 0x119e3000119e5, 0x11a0000011a3f, 0x11a4700011a48, - 0x11a5000011a84, - 0x11a8600011a9a, + 0x11a5000011a9a, 0x11a9d00011a9e, 0x11ac000011af9, 0x11c0000011c09, @@ -1931,13 +1935,16 @@ 0x16b6300016b78, 0x16b7d00016b90, 0x16e6000016e80, - 0x16f0000016f45, - 0x16f5000016f7f, + 0x16f0000016f4b, + 0x16f4f00016f88, 0x16f8f00016fa0, 0x16fe000016fe2, - 0x17000000187f2, + 0x16fe300016fe4, + 0x17000000187f8, 0x1880000018af3, 0x1b0000001b11f, + 0x1b1500001b153, + 0x1b1640001b168, 0x1b1700001b2fc, 0x1bc000001bc6b, 0x1bc700001bc7d, @@ -1955,9 +1962,14 @@ 0x1e01b0001e022, 0x1e0230001e025, 0x1e0260001e02b, + 0x1e1000001e12d, + 0x1e1300001e13e, + 0x1e1400001e14a, + 0x1e14e0001e14f, + 0x1e2c00001e2fa, 0x1e8000001e8c5, 0x1e8d00001e8d7, - 0x1e9220001e94b, + 0x1e9220001e94c, 0x1e9500001e95a, 0x200000002a6d7, 0x2a7000002b735, diff --git a/src/pip/_vendor/idna/package_data.py b/src/pip/_vendor/idna/package_data.py index 257e8989393..b5d8216558a 100644 --- a/src/pip/_vendor/idna/package_data.py +++ b/src/pip/_vendor/idna/package_data.py @@ -1,2 +1,2 @@ -__version__ = '2.8' +__version__ = '2.9' diff --git a/src/pip/_vendor/idna/uts46data.py b/src/pip/_vendor/idna/uts46data.py index a68ed4c0ec3..2711136d7d2 100644 --- a/src/pip/_vendor/idna/uts46data.py +++ b/src/pip/_vendor/idna/uts46data.py @@ -4,7 +4,7 @@ """IDNA Mapping Table from UTS46.""" -__version__ = "11.0.0" +__version__ = "12.1.0" def _seg_0(): return [ (0x0, '3'), @@ -1272,7 +1272,7 @@ def _seg_12(): (0xC64, 'X'), (0xC66, 'V'), (0xC70, 'X'), - (0xC78, 'V'), + (0xC77, 'V'), (0xC8D, 'X'), (0xC8E, 'V'), (0xC91, 'X'), @@ -1348,33 +1348,19 @@ def _seg_12(): (0xE83, 'X'), (0xE84, 'V'), (0xE85, 'X'), - (0xE87, 'V'), - (0xE89, 'X'), - (0xE8A, 'V'), + (0xE86, 'V'), (0xE8B, 'X'), - (0xE8D, 'V'), - (0xE8E, 'X'), - (0xE94, 'V'), - ] - -def _seg_13(): - return [ - (0xE98, 'X'), - (0xE99, 'V'), - (0xEA0, 'X'), - (0xEA1, 'V'), + (0xE8C, 'V'), (0xEA4, 'X'), (0xEA5, 'V'), (0xEA6, 'X'), (0xEA7, 'V'), - (0xEA8, 'X'), - (0xEAA, 'V'), - (0xEAC, 'X'), - (0xEAD, 'V'), + ] + +def _seg_13(): + return [ (0xEB3, 'M', u'ໍາ'), (0xEB4, 'V'), - (0xEBA, 'X'), - (0xEBB, 'V'), (0xEBE, 'X'), (0xEC0, 'V'), (0xEC5, 'X'), @@ -1459,10 +1445,6 @@ def _seg_13(): (0x1260, 'V'), (0x1289, 'X'), (0x128A, 'V'), - ] - -def _seg_14(): - return [ (0x128E, 'X'), (0x1290, 'V'), (0x12B1, 'X'), @@ -1477,6 +1459,10 @@ def _seg_14(): (0x12C8, 'V'), (0x12D7, 'X'), (0x12D8, 'V'), + ] + +def _seg_14(): + return [ (0x1311, 'X'), (0x1312, 'V'), (0x1316, 'X'), @@ -1563,10 +1549,6 @@ def _seg_14(): (0x1A7F, 'V'), (0x1A8A, 'X'), (0x1A90, 'V'), - ] - -def _seg_15(): - return [ (0x1A9A, 'X'), (0x1AA0, 'V'), (0x1AAE, 'X'), @@ -1581,6 +1563,10 @@ def _seg_15(): (0x1BFC, 'V'), (0x1C38, 'X'), (0x1C3B, 'V'), + ] + +def _seg_15(): + return [ (0x1C4A, 'X'), (0x1C4D, 'V'), (0x1C80, 'M', u'в'), @@ -1592,10 +1578,57 @@ def _seg_15(): (0x1C87, 'M', u'ѣ'), (0x1C88, 'M', u'ꙋ'), (0x1C89, 'X'), + (0x1C90, 'M', u'ა'), + (0x1C91, 'M', u'ბ'), + (0x1C92, 'M', u'გ'), + (0x1C93, 'M', u'დ'), + (0x1C94, 'M', u'ე'), + (0x1C95, 'M', u'ვ'), + (0x1C96, 'M', u'ზ'), + (0x1C97, 'M', u'თ'), + (0x1C98, 'M', u'ი'), + (0x1C99, 'M', u'კ'), + (0x1C9A, 'M', u'ლ'), + (0x1C9B, 'M', u'მ'), + (0x1C9C, 'M', u'ნ'), + (0x1C9D, 'M', u'ო'), + (0x1C9E, 'M', u'პ'), + (0x1C9F, 'M', u'ჟ'), + (0x1CA0, 'M', u'რ'), + (0x1CA1, 'M', u'ს'), + (0x1CA2, 'M', u'ტ'), + (0x1CA3, 'M', u'უ'), + (0x1CA4, 'M', u'ფ'), + (0x1CA5, 'M', u'ქ'), + (0x1CA6, 'M', u'ღ'), + (0x1CA7, 'M', u'ყ'), + (0x1CA8, 'M', u'შ'), + (0x1CA9, 'M', u'ჩ'), + (0x1CAA, 'M', u'ც'), + (0x1CAB, 'M', u'ძ'), + (0x1CAC, 'M', u'წ'), + (0x1CAD, 'M', u'ჭ'), + (0x1CAE, 'M', u'ხ'), + (0x1CAF, 'M', u'ჯ'), + (0x1CB0, 'M', u'ჰ'), + (0x1CB1, 'M', u'ჱ'), + (0x1CB2, 'M', u'ჲ'), + (0x1CB3, 'M', u'ჳ'), + (0x1CB4, 'M', u'ჴ'), + (0x1CB5, 'M', u'ჵ'), + (0x1CB6, 'M', u'ჶ'), + (0x1CB7, 'M', u'ჷ'), + (0x1CB8, 'M', u'ჸ'), + (0x1CB9, 'M', u'ჹ'), + (0x1CBA, 'M', u'ჺ'), + (0x1CBB, 'X'), + (0x1CBD, 'M', u'ჽ'), + (0x1CBE, 'M', u'ჾ'), + (0x1CBF, 'M', u'ჿ'), (0x1CC0, 'V'), (0x1CC8, 'X'), (0x1CD0, 'V'), - (0x1CFA, 'X'), + (0x1CFB, 'X'), (0x1D00, 'V'), (0x1D2C, 'M', u'a'), (0x1D2D, 'M', u'æ'), @@ -1634,6 +1667,10 @@ def _seg_15(): (0x1D4E, 'V'), (0x1D4F, 'M', u'k'), (0x1D50, 'M', u'm'), + ] + +def _seg_16(): + return [ (0x1D51, 'M', u'ŋ'), (0x1D52, 'M', u'o'), (0x1D53, 'M', u'ɔ'), @@ -1667,10 +1704,6 @@ def _seg_15(): (0x1D9C, 'M', u'c'), (0x1D9D, 'M', u'ɕ'), (0x1D9E, 'M', u'ð'), - ] - -def _seg_16(): - return [ (0x1D9F, 'M', u'ɜ'), (0x1DA0, 'M', u'f'), (0x1DA1, 'M', u'ɟ'), @@ -1738,6 +1771,10 @@ def _seg_16(): (0x1E1C, 'M', u'ḝ'), (0x1E1D, 'V'), (0x1E1E, 'M', u'ḟ'), + ] + +def _seg_17(): + return [ (0x1E1F, 'V'), (0x1E20, 'M', u'ḡ'), (0x1E21, 'V'), @@ -1771,10 +1808,6 @@ def _seg_16(): (0x1E3D, 'V'), (0x1E3E, 'M', u'ḿ'), (0x1E3F, 'V'), - ] - -def _seg_17(): - return [ (0x1E40, 'M', u'ṁ'), (0x1E41, 'V'), (0x1E42, 'M', u'ṃ'), @@ -1842,6 +1875,10 @@ def _seg_17(): (0x1E80, 'M', u'ẁ'), (0x1E81, 'V'), (0x1E82, 'M', u'ẃ'), + ] + +def _seg_18(): + return [ (0x1E83, 'V'), (0x1E84, 'M', u'ẅ'), (0x1E85, 'V'), @@ -1875,10 +1912,6 @@ def _seg_17(): (0x1EA6, 'M', u'ầ'), (0x1EA7, 'V'), (0x1EA8, 'M', u'ẩ'), - ] - -def _seg_18(): - return [ (0x1EA9, 'V'), (0x1EAA, 'M', u'ẫ'), (0x1EAB, 'V'), @@ -1946,6 +1979,10 @@ def _seg_18(): (0x1EE9, 'V'), (0x1EEA, 'M', u'ừ'), (0x1EEB, 'V'), + ] + +def _seg_19(): + return [ (0x1EEC, 'M', u'ử'), (0x1EED, 'V'), (0x1EEE, 'M', u'ữ'), @@ -1979,10 +2016,6 @@ def _seg_18(): (0x1F18, 'M', u'ἐ'), (0x1F19, 'M', u'ἑ'), (0x1F1A, 'M', u'ἒ'), - ] - -def _seg_19(): - return [ (0x1F1B, 'M', u'ἓ'), (0x1F1C, 'M', u'ἔ'), (0x1F1D, 'M', u'ἕ'), @@ -2050,6 +2083,10 @@ def _seg_19(): (0x1F80, 'M', u'ἀι'), (0x1F81, 'M', u'ἁι'), (0x1F82, 'M', u'ἂι'), + ] + +def _seg_20(): + return [ (0x1F83, 'M', u'ἃι'), (0x1F84, 'M', u'ἄι'), (0x1F85, 'M', u'ἅι'), @@ -2083,10 +2120,6 @@ def _seg_19(): (0x1FA1, 'M', u'ὡι'), (0x1FA2, 'M', u'ὢι'), (0x1FA3, 'M', u'ὣι'), - ] - -def _seg_20(): - return [ (0x1FA4, 'M', u'ὤι'), (0x1FA5, 'M', u'ὥι'), (0x1FA6, 'M', u'ὦι'), @@ -2154,6 +2187,10 @@ def _seg_20(): (0x1FEE, '3', u' ̈́'), (0x1FEF, '3', u'`'), (0x1FF0, 'X'), + ] + +def _seg_21(): + return [ (0x1FF2, 'M', u'ὼι'), (0x1FF3, 'M', u'ωι'), (0x1FF4, 'M', u'ώι'), @@ -2187,10 +2224,6 @@ def _seg_20(): (0x2035, 'V'), (0x2036, 'M', u'‵‵'), (0x2037, 'M', u'‵‵‵'), - ] - -def _seg_21(): - return [ (0x2038, 'V'), (0x203C, '3', u'!!'), (0x203D, 'V'), @@ -2258,6 +2291,10 @@ def _seg_21(): (0x20C0, 'X'), (0x20D0, 'V'), (0x20F1, 'X'), + ] + +def _seg_22(): + return [ (0x2100, '3', u'a/c'), (0x2101, '3', u'a/s'), (0x2102, 'M', u'c'), @@ -2291,10 +2328,6 @@ def _seg_21(): (0x2127, 'V'), (0x2128, 'M', u'z'), (0x2129, 'V'), - ] - -def _seg_22(): - return [ (0x212A, 'M', u'k'), (0x212B, 'M', u'å'), (0x212C, 'M', u'b'), @@ -2362,6 +2395,10 @@ def _seg_22(): (0x2175, 'M', u'vi'), (0x2176, 'M', u'vii'), (0x2177, 'M', u'viii'), + ] + +def _seg_23(): + return [ (0x2178, 'M', u'ix'), (0x2179, 'M', u'x'), (0x217A, 'M', u'xi'), @@ -2395,10 +2432,6 @@ def _seg_22(): (0x244B, 'X'), (0x2460, 'M', u'1'), (0x2461, 'M', u'2'), - ] - -def _seg_23(): - return [ (0x2462, 'M', u'3'), (0x2463, 'M', u'4'), (0x2464, 'M', u'5'), @@ -2466,6 +2499,10 @@ def _seg_23(): (0x24B5, '3', u'(z)'), (0x24B6, 'M', u'a'), (0x24B7, 'M', u'b'), + ] + +def _seg_24(): + return [ (0x24B8, 'M', u'c'), (0x24B9, 'M', u'd'), (0x24BA, 'M', u'e'), @@ -2499,10 +2536,6 @@ def _seg_23(): (0x24D6, 'M', u'g'), (0x24D7, 'M', u'h'), (0x24D8, 'M', u'i'), - ] - -def _seg_24(): - return [ (0x24D9, 'M', u'j'), (0x24DA, 'M', u'k'), (0x24DB, 'M', u'l'), @@ -2534,9 +2567,6 @@ def _seg_24(): (0x2B76, 'V'), (0x2B96, 'X'), (0x2B98, 'V'), - (0x2BC9, 'X'), - (0x2BCA, 'V'), - (0x2BFF, 'X'), (0x2C00, 'M', u'ⰰ'), (0x2C01, 'M', u'ⰱ'), (0x2C02, 'M', u'ⰲ'), @@ -2573,6 +2603,10 @@ def _seg_24(): (0x2C21, 'M', u'ⱑ'), (0x2C22, 'M', u'ⱒ'), (0x2C23, 'M', u'ⱓ'), + ] + +def _seg_25(): + return [ (0x2C24, 'M', u'ⱔ'), (0x2C25, 'M', u'ⱕ'), (0x2C26, 'M', u'ⱖ'), @@ -2603,10 +2637,6 @@ def _seg_24(): (0x2C6E, 'M', u'ɱ'), (0x2C6F, 'M', u'ɐ'), (0x2C70, 'M', u'ɒ'), - ] - -def _seg_25(): - return [ (0x2C71, 'V'), (0x2C72, 'M', u'ⱳ'), (0x2C73, 'V'), @@ -2677,6 +2707,10 @@ def _seg_25(): (0x2CBA, 'M', u'ⲻ'), (0x2CBB, 'V'), (0x2CBC, 'M', u'ⲽ'), + ] + +def _seg_26(): + return [ (0x2CBD, 'V'), (0x2CBE, 'M', u'ⲿ'), (0x2CBF, 'V'), @@ -2707,10 +2741,6 @@ def _seg_25(): (0x2CD8, 'M', u'ⳙ'), (0x2CD9, 'V'), (0x2CDA, 'M', u'ⳛ'), - ] - -def _seg_26(): - return [ (0x2CDB, 'V'), (0x2CDC, 'M', u'ⳝ'), (0x2CDD, 'V'), @@ -2757,7 +2787,7 @@ def _seg_26(): (0x2DD8, 'V'), (0x2DDF, 'X'), (0x2DE0, 'V'), - (0x2E4F, 'X'), + (0x2E50, 'X'), (0x2E80, 'V'), (0x2E9A, 'X'), (0x2E9B, 'V'), @@ -2781,6 +2811,10 @@ def _seg_26(): (0x2F0D, 'M', u'冖'), (0x2F0E, 'M', u'冫'), (0x2F0F, 'M', u'几'), + ] + +def _seg_27(): + return [ (0x2F10, 'M', u'凵'), (0x2F11, 'M', u'刀'), (0x2F12, 'M', u'力'), @@ -2811,10 +2845,6 @@ def _seg_26(): (0x2F2B, 'M', u'尸'), (0x2F2C, 'M', u'屮'), (0x2F2D, 'M', u'山'), - ] - -def _seg_27(): - return [ (0x2F2E, 'M', u'巛'), (0x2F2F, 'M', u'工'), (0x2F30, 'M', u'己'), @@ -2885,6 +2915,10 @@ def _seg_27(): (0x2F71, 'M', u'禸'), (0x2F72, 'M', u'禾'), (0x2F73, 'M', u'穴'), + ] + +def _seg_28(): + return [ (0x2F74, 'M', u'立'), (0x2F75, 'M', u'竹'), (0x2F76, 'M', u'米'), @@ -2915,10 +2949,6 @@ def _seg_27(): (0x2F8F, 'M', u'行'), (0x2F90, 'M', u'衣'), (0x2F91, 'M', u'襾'), - ] - -def _seg_28(): - return [ (0x2F92, 'M', u'見'), (0x2F93, 'M', u'角'), (0x2F94, 'M', u'言'), @@ -2989,6 +3019,10 @@ def _seg_28(): (0x2FD5, 'M', u'龠'), (0x2FD6, 'X'), (0x3000, '3', u' '), + ] + +def _seg_29(): + return [ (0x3001, 'V'), (0x3002, 'M', u'.'), (0x3003, 'V'), @@ -3019,10 +3053,6 @@ def _seg_28(): (0x3136, 'M', u'ᆭ'), (0x3137, 'M', u'ᄃ'), (0x3138, 'M', u'ᄄ'), - ] - -def _seg_29(): - return [ (0x3139, 'M', u'ᄅ'), (0x313A, 'M', u'ᆰ'), (0x313B, 'M', u'ᆱ'), @@ -3093,6 +3123,10 @@ def _seg_29(): (0x317C, 'M', u'ᄯ'), (0x317D, 'M', u'ᄲ'), (0x317E, 'M', u'ᄶ'), + ] + +def _seg_30(): + return [ (0x317F, 'M', u'ᅀ'), (0x3180, 'M', u'ᅇ'), (0x3181, 'M', u'ᅌ'), @@ -3123,10 +3157,6 @@ def _seg_29(): (0x319B, 'M', u'丙'), (0x319C, 'M', u'丁'), (0x319D, 'M', u'天'), - ] - -def _seg_30(): - return [ (0x319E, 'M', u'地'), (0x319F, 'M', u'人'), (0x31A0, 'V'), @@ -3197,6 +3227,10 @@ def _seg_30(): (0x323C, '3', u'(監)'), (0x323D, '3', u'(企)'), (0x323E, '3', u'(資)'), + ] + +def _seg_31(): + return [ (0x323F, '3', u'(協)'), (0x3240, '3', u'(祭)'), (0x3241, '3', u'(休)'), @@ -3227,10 +3261,6 @@ def _seg_30(): (0x3261, 'M', u'ᄂ'), (0x3262, 'M', u'ᄃ'), (0x3263, 'M', u'ᄅ'), - ] - -def _seg_31(): - return [ (0x3264, 'M', u'ᄆ'), (0x3265, 'M', u'ᄇ'), (0x3266, 'M', u'ᄉ'), @@ -3301,6 +3331,10 @@ def _seg_31(): (0x32A7, 'M', u'左'), (0x32A8, 'M', u'右'), (0x32A9, 'M', u'医'), + ] + +def _seg_32(): + return [ (0x32AA, 'M', u'宗'), (0x32AB, 'M', u'学'), (0x32AC, 'M', u'監'), @@ -3331,10 +3365,6 @@ def _seg_31(): (0x32C5, 'M', u'6月'), (0x32C6, 'M', u'7月'), (0x32C7, 'M', u'8月'), - ] - -def _seg_32(): - return [ (0x32C8, 'M', u'9月'), (0x32C9, 'M', u'10月'), (0x32CA, 'M', u'11月'), @@ -3390,7 +3420,7 @@ def _seg_32(): (0x32FC, 'M', u'ヰ'), (0x32FD, 'M', u'ヱ'), (0x32FE, 'M', u'ヲ'), - (0x32FF, 'X'), + (0x32FF, 'M', u'令和'), (0x3300, 'M', u'アパート'), (0x3301, 'M', u'アルファ'), (0x3302, 'M', u'アンペア'), @@ -3405,6 +3435,10 @@ def _seg_32(): (0x330B, 'M', u'カイリ'), (0x330C, 'M', u'カラット'), (0x330D, 'M', u'カロリー'), + ] + +def _seg_33(): + return [ (0x330E, 'M', u'ガロン'), (0x330F, 'M', u'ガンマ'), (0x3310, 'M', u'ギガ'), @@ -3435,10 +3469,6 @@ def _seg_32(): (0x3329, 'M', u'ノット'), (0x332A, 'M', u'ハイツ'), (0x332B, 'M', u'パーセント'), - ] - -def _seg_33(): - return [ (0x332C, 'M', u'パーツ'), (0x332D, 'M', u'バーレル'), (0x332E, 'M', u'ピアストル'), @@ -3509,6 +3539,10 @@ def _seg_33(): (0x336F, 'M', u'23点'), (0x3370, 'M', u'24点'), (0x3371, 'M', u'hpa'), + ] + +def _seg_34(): + return [ (0x3372, 'M', u'da'), (0x3373, 'M', u'au'), (0x3374, 'M', u'bar'), @@ -3539,10 +3573,6 @@ def _seg_33(): (0x338D, 'M', u'μg'), (0x338E, 'M', u'mg'), (0x338F, 'M', u'kg'), - ] - -def _seg_34(): - return [ (0x3390, 'M', u'hz'), (0x3391, 'M', u'khz'), (0x3392, 'M', u'mhz'), @@ -3613,6 +3643,10 @@ def _seg_34(): (0x33D3, 'M', u'lx'), (0x33D4, 'M', u'mb'), (0x33D5, 'M', u'mil'), + ] + +def _seg_35(): + return [ (0x33D6, 'M', u'mol'), (0x33D7, 'M', u'ph'), (0x33D8, 'X'), @@ -3643,10 +3677,6 @@ def _seg_34(): (0x33F1, 'M', u'18日'), (0x33F2, 'M', u'19日'), (0x33F3, 'M', u'20日'), - ] - -def _seg_35(): - return [ (0x33F4, 'M', u'21日'), (0x33F5, 'M', u'22日'), (0x33F6, 'M', u'23日'), @@ -3717,6 +3747,10 @@ def _seg_35(): (0xA66D, 'V'), (0xA680, 'M', u'ꚁ'), (0xA681, 'V'), + ] + +def _seg_36(): + return [ (0xA682, 'M', u'ꚃ'), (0xA683, 'V'), (0xA684, 'M', u'ꚅ'), @@ -3747,10 +3781,6 @@ def _seg_35(): (0xA69D, 'M', u'ь'), (0xA69E, 'V'), (0xA6F8, 'X'), - ] - -def _seg_36(): - return [ (0xA700, 'V'), (0xA722, 'M', u'ꜣ'), (0xA723, 'V'), @@ -3821,6 +3851,10 @@ def _seg_36(): (0xA766, 'M', u'ꝧ'), (0xA767, 'V'), (0xA768, 'M', u'ꝩ'), + ] + +def _seg_37(): + return [ (0xA769, 'V'), (0xA76A, 'M', u'ꝫ'), (0xA76B, 'V'), @@ -3851,10 +3885,6 @@ def _seg_36(): (0xA78E, 'V'), (0xA790, 'M', u'ꞑ'), (0xA791, 'V'), - ] - -def _seg_37(): - return [ (0xA792, 'M', u'ꞓ'), (0xA793, 'V'), (0xA796, 'M', u'ꞗ'), @@ -3891,9 +3921,21 @@ def _seg_37(): (0xA7B5, 'V'), (0xA7B6, 'M', u'ꞷ'), (0xA7B7, 'V'), - (0xA7B8, 'X'), + (0xA7B8, 'M', u'ꞹ'), (0xA7B9, 'V'), - (0xA7BA, 'X'), + (0xA7BA, 'M', u'ꞻ'), + (0xA7BB, 'V'), + (0xA7BC, 'M', u'ꞽ'), + (0xA7BD, 'V'), + (0xA7BE, 'M', u'ꞿ'), + (0xA7BF, 'V'), + (0xA7C0, 'X'), + (0xA7C2, 'M', u'ꟃ'), + (0xA7C3, 'V'), + (0xA7C4, 'M', u'ꞔ'), + (0xA7C5, 'M', u'ʂ'), + (0xA7C6, 'M', u'ᶎ'), + (0xA7C7, 'X'), (0xA7F7, 'V'), (0xA7F8, 'M', u'ħ'), (0xA7F9, 'M', u'œ'), @@ -3913,6 +3955,10 @@ def _seg_37(): (0xA97D, 'X'), (0xA980, 'V'), (0xA9CE, 'X'), + ] + +def _seg_38(): + return [ (0xA9CF, 'V'), (0xA9DA, 'X'), (0xA9DE, 'V'), @@ -3943,7 +3989,7 @@ def _seg_37(): (0xAB5E, 'M', u'ɫ'), (0xAB5F, 'M', u'ꭒ'), (0xAB60, 'V'), - (0xAB66, 'X'), + (0xAB68, 'X'), (0xAB70, 'M', u'Ꭰ'), (0xAB71, 'M', u'Ꭱ'), (0xAB72, 'M', u'Ꭲ'), @@ -3955,10 +4001,6 @@ def _seg_37(): (0xAB78, 'M', u'Ꭸ'), (0xAB79, 'M', u'Ꭹ'), (0xAB7A, 'M', u'Ꭺ'), - ] - -def _seg_38(): - return [ (0xAB7B, 'M', u'Ꭻ'), (0xAB7C, 'M', u'Ꭼ'), (0xAB7D, 'M', u'Ꭽ'), @@ -4017,6 +4059,10 @@ def _seg_38(): (0xABB2, 'M', u'Ꮲ'), (0xABB3, 'M', u'Ꮳ'), (0xABB4, 'M', u'Ꮴ'), + ] + +def _seg_39(): + return [ (0xABB5, 'M', u'Ꮵ'), (0xABB6, 'M', u'Ꮶ'), (0xABB7, 'M', u'Ꮷ'), @@ -4059,10 +4105,6 @@ def _seg_38(): (0xF913, 'M', u'邏'), (0xF914, 'M', u'樂'), (0xF915, 'M', u'洛'), - ] - -def _seg_39(): - return [ (0xF916, 'M', u'烙'), (0xF917, 'M', u'珞'), (0xF918, 'M', u'落'), @@ -4121,6 +4163,10 @@ def _seg_39(): (0xF94D, 'M', u'淚'), (0xF94E, 'M', u'漏'), (0xF94F, 'M', u'累'), + ] + +def _seg_40(): + return [ (0xF950, 'M', u'縷'), (0xF951, 'M', u'陋'), (0xF952, 'M', u'勒'), @@ -4163,10 +4209,6 @@ def _seg_39(): (0xF977, 'M', u'亮'), (0xF978, 'M', u'兩'), (0xF979, 'M', u'凉'), - ] - -def _seg_40(): - return [ (0xF97A, 'M', u'梁'), (0xF97B, 'M', u'糧'), (0xF97C, 'M', u'良'), @@ -4225,6 +4267,10 @@ def _seg_40(): (0xF9B1, 'M', u'鈴'), (0xF9B2, 'M', u'零'), (0xF9B3, 'M', u'靈'), + ] + +def _seg_41(): + return [ (0xF9B4, 'M', u'領'), (0xF9B5, 'M', u'例'), (0xF9B6, 'M', u'禮'), @@ -4267,10 +4313,6 @@ def _seg_40(): (0xF9DB, 'M', u'率'), (0xF9DC, 'M', u'隆'), (0xF9DD, 'M', u'利'), - ] - -def _seg_41(): - return [ (0xF9DE, 'M', u'吏'), (0xF9DF, 'M', u'履'), (0xF9E0, 'M', u'易'), @@ -4329,6 +4371,10 @@ def _seg_41(): (0xFA17, 'M', u'益'), (0xFA18, 'M', u'礼'), (0xFA19, 'M', u'神'), + ] + +def _seg_42(): + return [ (0xFA1A, 'M', u'祥'), (0xFA1B, 'M', u'福'), (0xFA1C, 'M', u'靖'), @@ -4371,10 +4417,6 @@ def _seg_41(): (0xFA44, 'M', u'梅'), (0xFA45, 'M', u'海'), (0xFA46, 'M', u'渚'), - ] - -def _seg_42(): - return [ (0xFA47, 'M', u'漢'), (0xFA48, 'M', u'煮'), (0xFA49, 'M', u'爫'), @@ -4433,6 +4475,10 @@ def _seg_42(): (0xFA80, 'M', u'婢'), (0xFA81, 'M', u'嬨'), (0xFA82, 'M', u'廒'), + ] + +def _seg_43(): + return [ (0xFA83, 'M', u'廙'), (0xFA84, 'M', u'彩'), (0xFA85, 'M', u'徭'), @@ -4475,10 +4521,6 @@ def _seg_42(): (0xFAAA, 'M', u'着'), (0xFAAB, 'M', u'磌'), (0xFAAC, 'M', u'窱'), - ] - -def _seg_43(): - return [ (0xFAAD, 'M', u'節'), (0xFAAE, 'M', u'类'), (0xFAAF, 'M', u'絛'), @@ -4537,6 +4579,10 @@ def _seg_43(): (0xFB15, 'M', u'մի'), (0xFB16, 'M', u'վն'), (0xFB17, 'M', u'մխ'), + ] + +def _seg_44(): + return [ (0xFB18, 'X'), (0xFB1D, 'M', u'יִ'), (0xFB1E, 'V'), @@ -4579,10 +4625,6 @@ def _seg_43(): (0xFB43, 'M', u'ףּ'), (0xFB44, 'M', u'פּ'), (0xFB45, 'X'), - ] - -def _seg_44(): - return [ (0xFB46, 'M', u'צּ'), (0xFB47, 'M', u'קּ'), (0xFB48, 'M', u'רּ'), @@ -4641,6 +4683,10 @@ def _seg_44(): (0xFBF0, 'M', u'ئۇ'), (0xFBF2, 'M', u'ئۆ'), (0xFBF4, 'M', u'ئۈ'), + ] + +def _seg_45(): + return [ (0xFBF6, 'M', u'ئې'), (0xFBF9, 'M', u'ئى'), (0xFBFC, 'M', u'ی'), @@ -4683,10 +4729,6 @@ def _seg_44(): (0xFC24, 'M', u'ضخ'), (0xFC25, 'M', u'ضم'), (0xFC26, 'M', u'طح'), - ] - -def _seg_45(): - return [ (0xFC27, 'M', u'طم'), (0xFC28, 'M', u'ظم'), (0xFC29, 'M', u'عج'), @@ -4745,6 +4787,10 @@ def _seg_45(): (0xFC5E, '3', u' ٌّ'), (0xFC5F, '3', u' ٍّ'), (0xFC60, '3', u' َّ'), + ] + +def _seg_46(): + return [ (0xFC61, '3', u' ُّ'), (0xFC62, '3', u' ِّ'), (0xFC63, '3', u' ّٰ'), @@ -4787,10 +4833,6 @@ def _seg_45(): (0xFC88, 'M', u'ما'), (0xFC89, 'M', u'مم'), (0xFC8A, 'M', u'نر'), - ] - -def _seg_46(): - return [ (0xFC8B, 'M', u'نز'), (0xFC8C, 'M', u'نم'), (0xFC8D, 'M', u'نن'), @@ -4849,6 +4891,10 @@ def _seg_46(): (0xFCC2, 'M', u'قح'), (0xFCC3, 'M', u'قم'), (0xFCC4, 'M', u'كج'), + ] + +def _seg_47(): + return [ (0xFCC5, 'M', u'كح'), (0xFCC6, 'M', u'كخ'), (0xFCC7, 'M', u'كل'), @@ -4891,10 +4937,6 @@ def _seg_46(): (0xFCEC, 'M', u'كم'), (0xFCED, 'M', u'لم'), (0xFCEE, 'M', u'نم'), - ] - -def _seg_47(): - return [ (0xFCEF, 'M', u'نه'), (0xFCF0, 'M', u'يم'), (0xFCF1, 'M', u'يه'), @@ -4953,6 +4995,10 @@ def _seg_47(): (0xFD26, 'M', u'شح'), (0xFD27, 'M', u'شخ'), (0xFD28, 'M', u'شم'), + ] + +def _seg_48(): + return [ (0xFD29, 'M', u'شر'), (0xFD2A, 'M', u'سر'), (0xFD2B, 'M', u'صر'), @@ -4995,10 +5041,6 @@ def _seg_47(): (0xFD66, 'M', u'صمم'), (0xFD67, 'M', u'شحم'), (0xFD69, 'M', u'شجي'), - ] - -def _seg_48(): - return [ (0xFD6A, 'M', u'شمخ'), (0xFD6C, 'M', u'شمم'), (0xFD6E, 'M', u'ضحى'), @@ -5057,6 +5099,10 @@ def _seg_48(): (0xFDAD, 'M', u'لمي'), (0xFDAE, 'M', u'يحي'), (0xFDAF, 'M', u'يجي'), + ] + +def _seg_49(): + return [ (0xFDB0, 'M', u'يمي'), (0xFDB1, 'M', u'ممي'), (0xFDB2, 'M', u'قمي'), @@ -5099,10 +5145,6 @@ def _seg_48(): (0xFDFE, 'X'), (0xFE00, 'I'), (0xFE10, '3', u','), - ] - -def _seg_49(): - return [ (0xFE11, 'M', u'、'), (0xFE12, 'X'), (0xFE13, '3', u':'), @@ -5161,6 +5203,10 @@ def _seg_49(): (0xFE65, '3', u'>'), (0xFE66, '3', u'='), (0xFE67, 'X'), + ] + +def _seg_50(): + return [ (0xFE68, '3', u'\\'), (0xFE69, '3', u'$'), (0xFE6A, '3', u'%'), @@ -5203,10 +5249,6 @@ def _seg_49(): (0xFEB1, 'M', u'س'), (0xFEB5, 'M', u'ش'), (0xFEB9, 'M', u'ص'), - ] - -def _seg_50(): - return [ (0xFEBD, 'M', u'ض'), (0xFEC1, 'M', u'ط'), (0xFEC5, 'M', u'ظ'), @@ -5265,6 +5307,10 @@ def _seg_50(): (0xFF22, 'M', u'b'), (0xFF23, 'M', u'c'), (0xFF24, 'M', u'd'), + ] + +def _seg_51(): + return [ (0xFF25, 'M', u'e'), (0xFF26, 'M', u'f'), (0xFF27, 'M', u'g'), @@ -5307,10 +5353,6 @@ def _seg_50(): (0xFF4C, 'M', u'l'), (0xFF4D, 'M', u'm'), (0xFF4E, 'M', u'n'), - ] - -def _seg_51(): - return [ (0xFF4F, 'M', u'o'), (0xFF50, 'M', u'p'), (0xFF51, 'M', u'q'), @@ -5369,6 +5411,10 @@ def _seg_51(): (0xFF86, 'M', u'ニ'), (0xFF87, 'M', u'ヌ'), (0xFF88, 'M', u'ネ'), + ] + +def _seg_52(): + return [ (0xFF89, 'M', u'ノ'), (0xFF8A, 'M', u'ハ'), (0xFF8B, 'M', u'ヒ'), @@ -5411,10 +5457,6 @@ def _seg_51(): (0xFFB0, 'M', u'ᄚ'), (0xFFB1, 'M', u'ᄆ'), (0xFFB2, 'M', u'ᄇ'), - ] - -def _seg_52(): - return [ (0xFFB3, 'M', u'ᄈ'), (0xFFB4, 'M', u'ᄡ'), (0xFFB5, 'M', u'ᄉ'), @@ -5473,6 +5515,10 @@ def _seg_52(): (0x1000C, 'X'), (0x1000D, 'V'), (0x10027, 'X'), + ] + +def _seg_53(): + return [ (0x10028, 'V'), (0x1003B, 'X'), (0x1003C, 'V'), @@ -5515,10 +5561,6 @@ def _seg_52(): (0x103D6, 'X'), (0x10400, 'M', u'𐐨'), (0x10401, 'M', u'𐐩'), - ] - -def _seg_53(): - return [ (0x10402, 'M', u'𐐪'), (0x10403, 'M', u'𐐫'), (0x10404, 'M', u'𐐬'), @@ -5577,6 +5619,10 @@ def _seg_53(): (0x104BD, 'M', u'𐓥'), (0x104BE, 'M', u'𐓦'), (0x104BF, 'M', u'𐓧'), + ] + +def _seg_54(): + return [ (0x104C0, 'M', u'𐓨'), (0x104C1, 'M', u'𐓩'), (0x104C2, 'M', u'𐓪'), @@ -5619,10 +5665,6 @@ def _seg_53(): (0x1080A, 'V'), (0x10836, 'X'), (0x10837, 'V'), - ] - -def _seg_54(): - return [ (0x10839, 'X'), (0x1083C, 'V'), (0x1083D, 'X'), @@ -5681,6 +5723,10 @@ def _seg_54(): (0x10BA9, 'V'), (0x10BB0, 'X'), (0x10C00, 'V'), + ] + +def _seg_55(): + return [ (0x10C49, 'X'), (0x10C80, 'M', u'𐳀'), (0x10C81, 'M', u'𐳁'), @@ -5723,10 +5769,6 @@ def _seg_54(): (0x10CA6, 'M', u'𐳦'), (0x10CA7, 'M', u'𐳧'), (0x10CA8, 'M', u'𐳨'), - ] - -def _seg_55(): - return [ (0x10CA9, 'M', u'𐳩'), (0x10CAA, 'M', u'𐳪'), (0x10CAB, 'M', u'𐳫'), @@ -5750,6 +5792,8 @@ def _seg_55(): (0x10F28, 'X'), (0x10F30, 'V'), (0x10F5A, 'X'), + (0x10FE0, 'V'), + (0x10FF7, 'X'), (0x11000, 'V'), (0x1104E, 'X'), (0x11052, 'V'), @@ -5783,6 +5827,10 @@ def _seg_55(): (0x11288, 'V'), (0x11289, 'X'), (0x1128A, 'V'), + ] + +def _seg_56(): + return [ (0x1128E, 'X'), (0x1128F, 'V'), (0x1129E, 'X'), @@ -5827,11 +5875,7 @@ def _seg_55(): (0x1145B, 'V'), (0x1145C, 'X'), (0x1145D, 'V'), - ] - -def _seg_56(): - return [ - (0x1145F, 'X'), + (0x11460, 'X'), (0x11480, 'V'), (0x114C8, 'X'), (0x114D0, 'V'), @@ -5847,7 +5891,7 @@ def _seg_56(): (0x11660, 'V'), (0x1166D, 'X'), (0x11680, 'V'), - (0x116B8, 'X'), + (0x116B9, 'X'), (0x116C0, 'V'), (0x116CA, 'X'), (0x11700, 'V'), @@ -5887,6 +5931,10 @@ def _seg_56(): (0x118BA, 'M', u'𑣚'), (0x118BB, 'M', u'𑣛'), (0x118BC, 'M', u'𑣜'), + ] + +def _seg_57(): + return [ (0x118BD, 'M', u'𑣝'), (0x118BE, 'M', u'𑣞'), (0x118BF, 'M', u'𑣟'), @@ -5894,11 +5942,15 @@ def _seg_56(): (0x118F3, 'X'), (0x118FF, 'V'), (0x11900, 'X'), + (0x119A0, 'V'), + (0x119A8, 'X'), + (0x119AA, 'V'), + (0x119D8, 'X'), + (0x119DA, 'V'), + (0x119E5, 'X'), (0x11A00, 'V'), (0x11A48, 'X'), (0x11A50, 'V'), - (0x11A84, 'X'), - (0x11A86, 'V'), (0x11AA3, 'X'), (0x11AC0, 'V'), (0x11AF9, 'X'), @@ -5931,10 +5983,6 @@ def _seg_56(): (0x11D50, 'V'), (0x11D5A, 'X'), (0x11D60, 'V'), - ] - -def _seg_57(): - return [ (0x11D66, 'X'), (0x11D67, 'V'), (0x11D69, 'X'), @@ -5948,7 +5996,9 @@ def _seg_57(): (0x11DAA, 'X'), (0x11EE0, 'V'), (0x11EF9, 'X'), - (0x12000, 'V'), + (0x11FC0, 'V'), + (0x11FF2, 'X'), + (0x11FFF, 'V'), (0x1239A, 'X'), (0x12400, 'V'), (0x1246F, 'X'), @@ -5982,22 +6032,62 @@ def _seg_57(): (0x16B78, 'X'), (0x16B7D, 'V'), (0x16B90, 'X'), + (0x16E40, 'M', u'𖹠'), + (0x16E41, 'M', u'𖹡'), + (0x16E42, 'M', u'𖹢'), + ] + +def _seg_58(): + return [ + (0x16E43, 'M', u'𖹣'), + (0x16E44, 'M', u'𖹤'), + (0x16E45, 'M', u'𖹥'), + (0x16E46, 'M', u'𖹦'), + (0x16E47, 'M', u'𖹧'), + (0x16E48, 'M', u'𖹨'), + (0x16E49, 'M', u'𖹩'), + (0x16E4A, 'M', u'𖹪'), + (0x16E4B, 'M', u'𖹫'), + (0x16E4C, 'M', u'𖹬'), + (0x16E4D, 'M', u'𖹭'), + (0x16E4E, 'M', u'𖹮'), + (0x16E4F, 'M', u'𖹯'), + (0x16E50, 'M', u'𖹰'), + (0x16E51, 'M', u'𖹱'), + (0x16E52, 'M', u'𖹲'), + (0x16E53, 'M', u'𖹳'), + (0x16E54, 'M', u'𖹴'), + (0x16E55, 'M', u'𖹵'), + (0x16E56, 'M', u'𖹶'), + (0x16E57, 'M', u'𖹷'), + (0x16E58, 'M', u'𖹸'), + (0x16E59, 'M', u'𖹹'), + (0x16E5A, 'M', u'𖹺'), + (0x16E5B, 'M', u'𖹻'), + (0x16E5C, 'M', u'𖹼'), + (0x16E5D, 'M', u'𖹽'), + (0x16E5E, 'M', u'𖹾'), + (0x16E5F, 'M', u'𖹿'), (0x16E60, 'V'), (0x16E9B, 'X'), (0x16F00, 'V'), - (0x16F45, 'X'), - (0x16F50, 'V'), - (0x16F7F, 'X'), + (0x16F4B, 'X'), + (0x16F4F, 'V'), + (0x16F88, 'X'), (0x16F8F, 'V'), (0x16FA0, 'X'), (0x16FE0, 'V'), - (0x16FE2, 'X'), + (0x16FE4, 'X'), (0x17000, 'V'), - (0x187F2, 'X'), + (0x187F8, 'X'), (0x18800, 'V'), (0x18AF3, 'X'), (0x1B000, 'V'), (0x1B11F, 'X'), + (0x1B150, 'V'), + (0x1B153, 'X'), + (0x1B164, 'V'), + (0x1B168, 'X'), (0x1B170, 'V'), (0x1B2FC, 'X'), (0x1BC00, 'V'), @@ -6035,10 +6125,6 @@ def _seg_57(): (0x1D1C1, 'V'), (0x1D1E9, 'X'), (0x1D200, 'V'), - ] - -def _seg_58(): - return [ (0x1D246, 'X'), (0x1D2E0, 'V'), (0x1D2F4, 'X'), @@ -6053,6 +6139,10 @@ def _seg_58(): (0x1D404, 'M', u'e'), (0x1D405, 'M', u'f'), (0x1D406, 'M', u'g'), + ] + +def _seg_59(): + return [ (0x1D407, 'M', u'h'), (0x1D408, 'M', u'i'), (0x1D409, 'M', u'j'), @@ -6139,10 +6229,6 @@ def _seg_58(): (0x1D45A, 'M', u'm'), (0x1D45B, 'M', u'n'), (0x1D45C, 'M', u'o'), - ] - -def _seg_59(): - return [ (0x1D45D, 'M', u'p'), (0x1D45E, 'M', u'q'), (0x1D45F, 'M', u'r'), @@ -6157,6 +6243,10 @@ def _seg_59(): (0x1D468, 'M', u'a'), (0x1D469, 'M', u'b'), (0x1D46A, 'M', u'c'), + ] + +def _seg_60(): + return [ (0x1D46B, 'M', u'd'), (0x1D46C, 'M', u'e'), (0x1D46D, 'M', u'f'), @@ -6243,10 +6333,6 @@ def _seg_59(): (0x1D4C1, 'M', u'l'), (0x1D4C2, 'M', u'm'), (0x1D4C3, 'M', u'n'), - ] - -def _seg_60(): - return [ (0x1D4C4, 'X'), (0x1D4C5, 'M', u'p'), (0x1D4C6, 'M', u'q'), @@ -6261,6 +6347,10 @@ def _seg_60(): (0x1D4CF, 'M', u'z'), (0x1D4D0, 'M', u'a'), (0x1D4D1, 'M', u'b'), + ] + +def _seg_61(): + return [ (0x1D4D2, 'M', u'c'), (0x1D4D3, 'M', u'd'), (0x1D4D4, 'M', u'e'), @@ -6347,10 +6437,6 @@ def _seg_60(): (0x1D526, 'M', u'i'), (0x1D527, 'M', u'j'), (0x1D528, 'M', u'k'), - ] - -def _seg_61(): - return [ (0x1D529, 'M', u'l'), (0x1D52A, 'M', u'm'), (0x1D52B, 'M', u'n'), @@ -6365,6 +6451,10 @@ def _seg_61(): (0x1D534, 'M', u'w'), (0x1D535, 'M', u'x'), (0x1D536, 'M', u'y'), + ] + +def _seg_62(): + return [ (0x1D537, 'M', u'z'), (0x1D538, 'M', u'a'), (0x1D539, 'M', u'b'), @@ -6451,10 +6541,6 @@ def _seg_61(): (0x1D58C, 'M', u'g'), (0x1D58D, 'M', u'h'), (0x1D58E, 'M', u'i'), - ] - -def _seg_62(): - return [ (0x1D58F, 'M', u'j'), (0x1D590, 'M', u'k'), (0x1D591, 'M', u'l'), @@ -6469,6 +6555,10 @@ def _seg_62(): (0x1D59A, 'M', u'u'), (0x1D59B, 'M', u'v'), (0x1D59C, 'M', u'w'), + ] + +def _seg_63(): + return [ (0x1D59D, 'M', u'x'), (0x1D59E, 'M', u'y'), (0x1D59F, 'M', u'z'), @@ -6555,10 +6645,6 @@ def _seg_62(): (0x1D5F0, 'M', u'c'), (0x1D5F1, 'M', u'd'), (0x1D5F2, 'M', u'e'), - ] - -def _seg_63(): - return [ (0x1D5F3, 'M', u'f'), (0x1D5F4, 'M', u'g'), (0x1D5F5, 'M', u'h'), @@ -6573,6 +6659,10 @@ def _seg_63(): (0x1D5FE, 'M', u'q'), (0x1D5FF, 'M', u'r'), (0x1D600, 'M', u's'), + ] + +def _seg_64(): + return [ (0x1D601, 'M', u't'), (0x1D602, 'M', u'u'), (0x1D603, 'M', u'v'), @@ -6659,10 +6749,6 @@ def _seg_63(): (0x1D654, 'M', u'y'), (0x1D655, 'M', u'z'), (0x1D656, 'M', u'a'), - ] - -def _seg_64(): - return [ (0x1D657, 'M', u'b'), (0x1D658, 'M', u'c'), (0x1D659, 'M', u'd'), @@ -6677,6 +6763,10 @@ def _seg_64(): (0x1D662, 'M', u'm'), (0x1D663, 'M', u'n'), (0x1D664, 'M', u'o'), + ] + +def _seg_65(): + return [ (0x1D665, 'M', u'p'), (0x1D666, 'M', u'q'), (0x1D667, 'M', u'r'), @@ -6763,10 +6853,6 @@ def _seg_64(): (0x1D6B9, 'M', u'θ'), (0x1D6BA, 'M', u'σ'), (0x1D6BB, 'M', u'τ'), - ] - -def _seg_65(): - return [ (0x1D6BC, 'M', u'υ'), (0x1D6BD, 'M', u'φ'), (0x1D6BE, 'M', u'χ'), @@ -6781,6 +6867,10 @@ def _seg_65(): (0x1D6C7, 'M', u'ζ'), (0x1D6C8, 'M', u'η'), (0x1D6C9, 'M', u'θ'), + ] + +def _seg_66(): + return [ (0x1D6CA, 'M', u'ι'), (0x1D6CB, 'M', u'κ'), (0x1D6CC, 'M', u'λ'), @@ -6867,10 +6957,6 @@ def _seg_65(): (0x1D71F, 'M', u'δ'), (0x1D720, 'M', u'ε'), (0x1D721, 'M', u'ζ'), - ] - -def _seg_66(): - return [ (0x1D722, 'M', u'η'), (0x1D723, 'M', u'θ'), (0x1D724, 'M', u'ι'), @@ -6885,6 +6971,10 @@ def _seg_66(): (0x1D72D, 'M', u'θ'), (0x1D72E, 'M', u'σ'), (0x1D72F, 'M', u'τ'), + ] + +def _seg_67(): + return [ (0x1D730, 'M', u'υ'), (0x1D731, 'M', u'φ'), (0x1D732, 'M', u'χ'), @@ -6971,10 +7061,6 @@ def _seg_66(): (0x1D785, 'M', u'φ'), (0x1D786, 'M', u'χ'), (0x1D787, 'M', u'ψ'), - ] - -def _seg_67(): - return [ (0x1D788, 'M', u'ω'), (0x1D789, 'M', u'∂'), (0x1D78A, 'M', u'ε'), @@ -6989,6 +7075,10 @@ def _seg_67(): (0x1D793, 'M', u'δ'), (0x1D794, 'M', u'ε'), (0x1D795, 'M', u'ζ'), + ] + +def _seg_68(): + return [ (0x1D796, 'M', u'η'), (0x1D797, 'M', u'θ'), (0x1D798, 'M', u'ι'), @@ -7075,10 +7165,6 @@ def _seg_67(): (0x1D7EC, 'M', u'0'), (0x1D7ED, 'M', u'1'), (0x1D7EE, 'M', u'2'), - ] - -def _seg_68(): - return [ (0x1D7EF, 'M', u'3'), (0x1D7F0, 'M', u'4'), (0x1D7F1, 'M', u'5'), @@ -7093,6 +7179,10 @@ def _seg_68(): (0x1D7FA, 'M', u'4'), (0x1D7FB, 'M', u'5'), (0x1D7FC, 'M', u'6'), + ] + +def _seg_69(): + return [ (0x1D7FD, 'M', u'7'), (0x1D7FE, 'M', u'8'), (0x1D7FF, 'M', u'9'), @@ -7112,6 +7202,18 @@ def _seg_68(): (0x1E025, 'X'), (0x1E026, 'V'), (0x1E02B, 'X'), + (0x1E100, 'V'), + (0x1E12D, 'X'), + (0x1E130, 'V'), + (0x1E13E, 'X'), + (0x1E140, 'V'), + (0x1E14A, 'X'), + (0x1E14E, 'V'), + (0x1E150, 'X'), + (0x1E2C0, 'V'), + (0x1E2FA, 'X'), + (0x1E2FF, 'V'), + (0x1E300, 'X'), (0x1E800, 'V'), (0x1E8C5, 'X'), (0x1E8C7, 'V'), @@ -7151,13 +7253,15 @@ def _seg_68(): (0x1E920, 'M', u'𞥂'), (0x1E921, 'M', u'𞥃'), (0x1E922, 'V'), - (0x1E94B, 'X'), + (0x1E94C, 'X'), (0x1E950, 'V'), (0x1E95A, 'X'), (0x1E95E, 'V'), (0x1E960, 'X'), (0x1EC71, 'V'), (0x1ECB5, 'X'), + (0x1ED01, 'V'), + (0x1ED3E, 'X'), (0x1EE00, 'M', u'ا'), (0x1EE01, 'M', u'ب'), (0x1EE02, 'M', u'ج'), @@ -7181,7 +7285,7 @@ def _seg_68(): (0x1EE14, 'M', u'ش'), ] -def _seg_69(): +def _seg_70(): return [ (0x1EE15, 'M', u'ت'), (0x1EE16, 'M', u'ث'), @@ -7285,7 +7389,7 @@ def _seg_69(): (0x1EE83, 'M', u'د'), ] -def _seg_70(): +def _seg_71(): return [ (0x1EE84, 'M', u'ه'), (0x1EE85, 'M', u'و'), @@ -7389,7 +7493,7 @@ def _seg_70(): (0x1F124, '3', u'(u)'), ] -def _seg_71(): +def _seg_72(): return [ (0x1F125, '3', u'(v)'), (0x1F126, '3', u'(w)'), @@ -7437,7 +7541,8 @@ def _seg_71(): (0x1F150, 'V'), (0x1F16A, 'M', u'mc'), (0x1F16B, 'M', u'md'), - (0x1F16C, 'X'), + (0x1F16C, 'M', u'mr'), + (0x1F16D, 'X'), (0x1F170, 'V'), (0x1F190, 'M', u'dj'), (0x1F191, 'V'), @@ -7490,11 +7595,11 @@ def _seg_71(): (0x1F238, 'M', u'申'), (0x1F239, 'M', u'割'), (0x1F23A, 'M', u'営'), - (0x1F23B, 'M', u'配'), ] -def _seg_72(): +def _seg_73(): return [ + (0x1F23B, 'M', u'配'), (0x1F23C, 'X'), (0x1F240, 'M', u'〔本〕'), (0x1F241, 'M', u'〔三〕'), @@ -7512,15 +7617,17 @@ def _seg_72(): (0x1F260, 'V'), (0x1F266, 'X'), (0x1F300, 'V'), - (0x1F6D5, 'X'), + (0x1F6D6, 'X'), (0x1F6E0, 'V'), (0x1F6ED, 'X'), (0x1F6F0, 'V'), - (0x1F6FA, 'X'), + (0x1F6FB, 'X'), (0x1F700, 'V'), (0x1F774, 'X'), (0x1F780, 'V'), (0x1F7D9, 'X'), + (0x1F7E0, 'V'), + (0x1F7EC, 'X'), (0x1F800, 'V'), (0x1F80C, 'X'), (0x1F810, 'V'), @@ -7533,24 +7640,28 @@ def _seg_72(): (0x1F8AE, 'X'), (0x1F900, 'V'), (0x1F90C, 'X'), - (0x1F910, 'V'), - (0x1F93F, 'X'), - (0x1F940, 'V'), - (0x1F971, 'X'), + (0x1F90D, 'V'), + (0x1F972, 'X'), (0x1F973, 'V'), (0x1F977, 'X'), (0x1F97A, 'V'), - (0x1F97B, 'X'), - (0x1F97C, 'V'), (0x1F9A3, 'X'), - (0x1F9B0, 'V'), - (0x1F9BA, 'X'), - (0x1F9C0, 'V'), - (0x1F9C3, 'X'), - (0x1F9D0, 'V'), - (0x1FA00, 'X'), + (0x1F9A5, 'V'), + (0x1F9AB, 'X'), + (0x1F9AE, 'V'), + (0x1F9CB, 'X'), + (0x1F9CD, 'V'), + (0x1FA54, 'X'), (0x1FA60, 'V'), (0x1FA6E, 'X'), + (0x1FA70, 'V'), + (0x1FA74, 'X'), + (0x1FA78, 'V'), + (0x1FA7B, 'X'), + (0x1FA80, 'V'), + (0x1FA83, 'X'), + (0x1FA90, 'V'), + (0x1FA96, 'X'), (0x20000, 'V'), (0x2A6D7, 'X'), (0x2A700, 'V'), @@ -7588,6 +7699,10 @@ def _seg_72(): (0x2F818, 'M', u'冤'), (0x2F819, 'M', u'仌'), (0x2F81A, 'M', u'冬'), + ] + +def _seg_74(): + return [ (0x2F81B, 'M', u'况'), (0x2F81C, 'M', u'𩇟'), (0x2F81D, 'M', u'凵'), @@ -7595,10 +7710,6 @@ def _seg_72(): (0x2F81F, 'M', u'㓟'), (0x2F820, 'M', u'刻'), (0x2F821, 'M', u'剆'), - ] - -def _seg_73(): - return [ (0x2F822, 'M', u'割'), (0x2F823, 'M', u'剷'), (0x2F824, 'M', u'㔕'), @@ -7692,6 +7803,10 @@ def _seg_73(): (0x2F880, 'M', u'嵼'), (0x2F881, 'M', u'巡'), (0x2F882, 'M', u'巢'), + ] + +def _seg_75(): + return [ (0x2F883, 'M', u'㠯'), (0x2F884, 'M', u'巽'), (0x2F885, 'M', u'帨'), @@ -7699,10 +7814,6 @@ def _seg_73(): (0x2F887, 'M', u'幩'), (0x2F888, 'M', u'㡢'), (0x2F889, 'M', u'𢆃'), - ] - -def _seg_74(): - return [ (0x2F88A, 'M', u'㡼'), (0x2F88B, 'M', u'庰'), (0x2F88C, 'M', u'庳'), @@ -7796,6 +7907,10 @@ def _seg_74(): (0x2F8E6, 'M', u'椔'), (0x2F8E7, 'M', u'㮝'), (0x2F8E8, 'M', u'楂'), + ] + +def _seg_76(): + return [ (0x2F8E9, 'M', u'榣'), (0x2F8EA, 'M', u'槪'), (0x2F8EB, 'M', u'檨'), @@ -7803,10 +7918,6 @@ def _seg_74(): (0x2F8ED, 'M', u'櫛'), (0x2F8EE, 'M', u'㰘'), (0x2F8EF, 'M', u'次'), - ] - -def _seg_75(): - return [ (0x2F8F0, 'M', u'𣢧'), (0x2F8F1, 'M', u'歔'), (0x2F8F2, 'M', u'㱎'), @@ -7900,6 +8011,10 @@ def _seg_75(): (0x2F94C, 'M', u'䂖'), (0x2F94D, 'M', u'𥐝'), (0x2F94E, 'M', u'硎'), + ] + +def _seg_77(): + return [ (0x2F94F, 'M', u'碌'), (0x2F950, 'M', u'磌'), (0x2F951, 'M', u'䃣'), @@ -7907,10 +8022,6 @@ def _seg_75(): (0x2F953, 'M', u'祖'), (0x2F954, 'M', u'𥚚'), (0x2F955, 'M', u'𥛅'), - ] - -def _seg_76(): - return [ (0x2F956, 'M', u'福'), (0x2F957, 'M', u'秫'), (0x2F958, 'M', u'䄯'), @@ -8004,6 +8115,10 @@ def _seg_76(): (0x2F9B1, 'M', u'𧃒'), (0x2F9B2, 'M', u'䕫'), (0x2F9B3, 'M', u'虐'), + ] + +def _seg_78(): + return [ (0x2F9B4, 'M', u'虜'), (0x2F9B5, 'M', u'虧'), (0x2F9B6, 'M', u'虩'), @@ -8011,10 +8126,6 @@ def _seg_76(): (0x2F9B8, 'M', u'蚈'), (0x2F9B9, 'M', u'蜎'), (0x2F9BA, 'M', u'蛢'), - ] - -def _seg_77(): - return [ (0x2F9BB, 'M', u'蝹'), (0x2F9BC, 'M', u'蜨'), (0x2F9BD, 'M', u'蝫'), @@ -8108,6 +8219,10 @@ def _seg_77(): (0x2FA16, 'M', u'䵖'), (0x2FA17, 'M', u'黹'), (0x2FA18, 'M', u'黾'), + ] + +def _seg_79(): + return [ (0x2FA19, 'M', u'鼅'), (0x2FA1A, 'M', u'鼏'), (0x2FA1B, 'M', u'鼖'), @@ -8115,10 +8230,6 @@ def _seg_77(): (0x2FA1D, 'M', u'𪘀'), (0x2FA1E, 'X'), (0xE0100, 'I'), - ] - -def _seg_78(): - return [ (0xE01F0, 'X'), ] @@ -8202,4 +8313,5 @@ def _seg_78(): + _seg_76() + _seg_77() + _seg_78() + + _seg_79() ) diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 55099f4c33d..a089cc87173 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -14,7 +14,7 @@ pyparsing==2.4.7 requests==2.23.0 certifi==2020.4.5.1 chardet==3.0.4 - idna==2.8 + idna==2.9 urllib3==1.25.7 resolvelib==0.3.0 retrying==1.3.3 From 101332f70f7684e3ea5a1e767a32bc50bb22efd0 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Wed, 15 Apr 2020 21:28:18 +0530 Subject: [PATCH 1636/3170] Upgrade urllib3 to 1.25.8 --- news/urllib3.vendor | 1 + src/pip/_vendor/urllib3/__init__.py | 2 +- src/pip/_vendor/urllib3/connection.py | 34 ------------------- src/pip/_vendor/urllib3/connectionpool.py | 4 +-- .../urllib3/contrib/_appengine_environ.py | 14 ++++---- src/pip/_vendor/urllib3/response.py | 2 +- src/pip/_vendor/urllib3/util/ssl_.py | 2 +- src/pip/_vendor/urllib3/util/url.py | 18 ++++------ src/pip/_vendor/vendor.txt | 2 +- 9 files changed, 20 insertions(+), 59 deletions(-) create mode 100644 news/urllib3.vendor diff --git a/news/urllib3.vendor b/news/urllib3.vendor new file mode 100644 index 00000000000..2e4c40342d7 --- /dev/null +++ b/news/urllib3.vendor @@ -0,0 +1 @@ +Upgrade urllib3 to 1.25.8. diff --git a/src/pip/_vendor/urllib3/__init__.py b/src/pip/_vendor/urllib3/__init__.py index 96474d3680c..9bd8323f91e 100644 --- a/src/pip/_vendor/urllib3/__init__.py +++ b/src/pip/_vendor/urllib3/__init__.py @@ -22,7 +22,7 @@ __author__ = "Andrey Petrov (andrey.petrov@shazow.net)" __license__ = "MIT" -__version__ = "1.25.7" +__version__ = "1.25.8" __all__ = ( "HTTPConnectionPool", diff --git a/src/pip/_vendor/urllib3/connection.py b/src/pip/_vendor/urllib3/connection.py index f5c946adf77..71e6790b1b9 100644 --- a/src/pip/_vendor/urllib3/connection.py +++ b/src/pip/_vendor/urllib3/connection.py @@ -251,40 +251,6 @@ def __init__( # HTTPS requests to go out as HTTP. (See Issue #356) self._protocol = "https" - def connect(self): - conn = self._new_conn() - self._prepare_conn(conn) - - # Wrap socket using verification with the root certs in - # trusted_root_certs - default_ssl_context = False - if self.ssl_context is None: - default_ssl_context = True - self.ssl_context = create_urllib3_context( - ssl_version=resolve_ssl_version(self.ssl_version), - cert_reqs=resolve_cert_reqs(self.cert_reqs), - ) - - # Try to load OS default certs if none are given. - # Works well on Windows (requires Python3.4+) - context = self.ssl_context - if ( - not self.ca_certs - and not self.ca_cert_dir - and default_ssl_context - and hasattr(context, "load_default_certs") - ): - context.load_default_certs() - - self.sock = ssl_wrap_socket( - sock=conn, - keyfile=self.key_file, - certfile=self.cert_file, - key_password=self.key_password, - ssl_context=self.ssl_context, - server_hostname=self.server_hostname, - ) - class VerifiedHTTPSConnection(HTTPSConnection): """ diff --git a/src/pip/_vendor/urllib3/connectionpool.py b/src/pip/_vendor/urllib3/connectionpool.py index 31696460f08..d42eb7be673 100644 --- a/src/pip/_vendor/urllib3/connectionpool.py +++ b/src/pip/_vendor/urllib3/connectionpool.py @@ -996,10 +996,10 @@ def _validate_conn(self, conn): if not conn.is_verified: warnings.warn( ( - "Unverified HTTPS request is being made. " + "Unverified HTTPS request is being made to host '%s'. " "Adding certificate verification is strongly advised. See: " "https://urllib3.readthedocs.io/en/latest/advanced-usage.html" - "#ssl-warnings" + "#ssl-warnings" % conn.host ), InsecureRequestWarning, ) diff --git a/src/pip/_vendor/urllib3/contrib/_appengine_environ.py b/src/pip/_vendor/urllib3/contrib/_appengine_environ.py index 119efaeeb67..8765b907d70 100644 --- a/src/pip/_vendor/urllib3/contrib/_appengine_environ.py +++ b/src/pip/_vendor/urllib3/contrib/_appengine_environ.py @@ -6,7 +6,7 @@ def is_appengine(): - return "APPENGINE_RUNTIME" in os.environ + return is_local_appengine() or is_prod_appengine() def is_appengine_sandbox(): @@ -20,15 +20,15 @@ def is_appengine_sandbox(): def is_local_appengine(): - return is_appengine() and os.environ.get("SERVER_SOFTWARE", "").startswith( - "Development/" - ) + return "APPENGINE_RUNTIME" in os.environ and os.environ.get( + "SERVER_SOFTWARE", "" + ).startswith("Development/") def is_prod_appengine(): - return is_appengine() and os.environ.get("SERVER_SOFTWARE", "").startswith( - "Google App Engine/" - ) + return "APPENGINE_RUNTIME" in os.environ and os.environ.get( + "SERVER_SOFTWARE", "" + ).startswith("Google App Engine/") def is_prod_appengine_mvms(): diff --git a/src/pip/_vendor/urllib3/response.py b/src/pip/_vendor/urllib3/response.py index adc321e713b..6090a7350f9 100644 --- a/src/pip/_vendor/urllib3/response.py +++ b/src/pip/_vendor/urllib3/response.py @@ -792,7 +792,7 @@ def geturl(self): return self._request_url def __iter__(self): - buffer = [b""] + buffer = [] for chunk in self.stream(decode_content=True): if b"\n" in chunk: chunk = chunk.split(b"\n") diff --git a/src/pip/_vendor/urllib3/util/ssl_.py b/src/pip/_vendor/urllib3/util/ssl_.py index e5739fb6757..3f78296f656 100644 --- a/src/pip/_vendor/urllib3/util/ssl_.py +++ b/src/pip/_vendor/urllib3/util/ssl_.py @@ -182,7 +182,7 @@ def resolve_cert_reqs(candidate): """ Resolves the argument to a numeric constant, which can be passed to the wrap_socket function/method from the ssl module. - Defaults to :data:`ssl.CERT_NONE`. + Defaults to :data:`ssl.CERT_REQUIRED`. If given a string it is assumed to be the name of the constant in the :mod:`ssl` module or its abbreviation. (So you can specify `REQUIRED` instead of `CERT_REQUIRED`. diff --git a/src/pip/_vendor/urllib3/util/url.py b/src/pip/_vendor/urllib3/util/url.py index 12600626170..5f8aee629a7 100644 --- a/src/pip/_vendor/urllib3/util/url.py +++ b/src/pip/_vendor/urllib3/util/url.py @@ -216,18 +216,15 @@ def _encode_invalid_chars(component, allowed_chars, encoding="utf-8"): component = six.ensure_text(component) + # Normalize existing percent-encoded bytes. # Try to see if the component we're encoding is already percent-encoded # so we can skip all '%' characters but still encode all others. - percent_encodings = PERCENT_RE.findall(component) - - # Normalize existing percent-encoded bytes. - for enc in percent_encodings: - if not enc.isupper(): - component = component.replace(enc, enc.upper()) + component, percent_encodings = PERCENT_RE.subn( + lambda match: match.group(0).upper(), component + ) uri_bytes = component.encode("utf-8", "surrogatepass") - is_percent_encoded = len(percent_encodings) == uri_bytes.count(b"%") - + is_percent_encoded = percent_encodings == uri_bytes.count(b"%") encoded_component = bytearray() for i in range(0, len(uri_bytes)): @@ -237,7 +234,7 @@ def _encode_invalid_chars(component, allowed_chars, encoding="utf-8"): if (is_percent_encoded and byte == b"%") or ( byte_ord < 128 and byte.decode() in allowed_chars ): - encoded_component.extend(byte) + encoded_component += byte continue encoded_component.extend(b"%" + (hex(byte_ord)[2:].encode().zfill(2).upper())) @@ -322,9 +319,6 @@ def _idna_encode(name): def _encode_target(target): """Percent-encodes a request target so that there are no invalid characters""" - if not target.startswith("/"): - return target - path, query = TARGET_RE.match(target).groups() target = _encode_invalid_chars(path, PATH_CHARS) query = _encode_invalid_chars(query, QUERY_CHARS) diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index a089cc87173..d301f467d0f 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -15,7 +15,7 @@ requests==2.23.0 certifi==2020.4.5.1 chardet==3.0.4 idna==2.9 - urllib3==1.25.7 + urllib3==1.25.8 resolvelib==0.3.0 retrying==1.3.3 setuptools==44.0.0 From df9bbb403f0bc7769fcc154e099347a5ea1d7bcd Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Wed, 15 Apr 2020 23:26:12 +0530 Subject: [PATCH 1637/3170] Fix bug in pip debug --- src/pip/_internal/commands/debug.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/commands/debug.py b/src/pip/_internal/commands/debug.py index 05ff1c54e64..b7c92fa6598 100644 --- a/src/pip/_internal/commands/debug.py +++ b/src/pip/_internal/commands/debug.py @@ -183,7 +183,8 @@ def ca_bundle_info(config): if not global_overriding_level: return 'global' - levels.remove('global') + if 'global' in levels: + levels.remove('global') return ", ".join(levels) From 5bea53089912768a01dcf89b97adc9663211254b Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Wed, 15 Apr 2020 23:27:30 +0530 Subject: [PATCH 1638/3170] Change certifi version to 2020.04.05.1 --- news/certifi.vendor | 2 +- src/pip/_vendor/vendor.txt | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/news/certifi.vendor b/news/certifi.vendor index 88746035e42..b53a40e989e 100644 --- a/news/certifi.vendor +++ b/news/certifi.vendor @@ -1 +1 @@ -Upgrade certifi to 2020.4.5.1. +Upgrade certifi to 2020.04.05.1 diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index d301f467d0f..74ecca4252e 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -12,7 +12,7 @@ pep517==0.8.2 progress==1.5 pyparsing==2.4.7 requests==2.23.0 - certifi==2020.4.5.1 + certifi==2020.04.05.1 chardet==3.0.4 idna==2.9 urllib3==1.25.8 From bdc886be40521d98243bebc45abe6ac9baac9958 Mon Sep 17 00:00:00 2001 From: Phil Elson <philip.elson@cern.ch> Date: Thu, 16 Apr 2020 05:17:01 +0200 Subject: [PATCH 1639/3170] Add xfail tests for case where specifications added as an extra are not honoured. --- tests/functional/test_install_extras.py | 29 +++++++++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/functional/test_install_extras.py b/tests/functional/test_install_extras.py index bce95cb196d..bf67a3c9bfd 100644 --- a/tests/functional/test_install_extras.py +++ b/tests/functional/test_install_extras.py @@ -126,3 +126,32 @@ def test_install_special_extra(script): assert ( "Could not find a version that satisfies the requirement missing_pkg" ) in result.stderr, str(result) + + +@pytest.mark.parametrize( + "extra_to_install, simple_version", [ + ['', '3.0'], + pytest.param('[extra1]', '2.0', marks=pytest.mark.xfail), + pytest.param('[extra2]', '1.0', marks=pytest.mark.xfail), + pytest.param('[extra1,extra2]', '1.0', marks=pytest.mark.xfail), + ]) +def test_install_extra_merging(script, data, extra_to_install, simple_version): + # Check that extra specifications in the extras section are honoured. + pkga_path = script.scratch_path / 'pkga' + pkga_path.mkdir() + pkga_path.joinpath("setup.py").write_text(textwrap.dedent(""" + from setuptools import setup + setup(name='pkga', + version='0.1', + install_requires=['simple'], + extras_require={'extra1': ['simple<3'], + 'extra2': ['simple==1.*']}, + ) + """)) + + result = script.pip_install_local( + '{pkga_path}{extra_to_install}'.format(**locals()), + ) + + assert ('Successfully installed pkga-0.1 simple-{}'.format(simple_version) + ) in result.stdout From 9c97b285b9a3df360034417ddc4f8d2d7f29a4b6 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 16 Apr 2020 13:44:13 +0800 Subject: [PATCH 1640/3170] Rename variable for clarity --- src/pip/_internal/resolution/resolvelib/factory.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index fb5d26abe23..d6236d77984 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -109,9 +109,9 @@ def iter_found_candidates(self, ireq, extras): # type: (InstallRequirement, Set[str]) -> Iterator[Candidate] name = canonicalize_name(ireq.req.name) if not self._force_reinstall: - dist = self._installed_dists.get(name) + installed_dist = self._installed_dists.get(name) else: - dist = None + installed_dist = None found = self.finder.find_best_candidate( project_name=ireq.req.name, @@ -119,7 +119,8 @@ def iter_found_candidates(self, ireq, extras): hashes=ireq.hashes(trust_internet=False), ) for ican in found.iter_applicable(): - if dist is not None and dist.parsed_version == ican.version: + if (installed_dist is not None and + installed_dist.parsed_version == ican.version): continue yield self._make_candidate_from_link( link=ican.link, @@ -131,9 +132,10 @@ def iter_found_candidates(self, ireq, extras): # Return installed distribution if it matches the specifier. This is # done last so the resolver will prefer it over downloading links. - if dist is not None and dist.parsed_version in ireq.req.specifier: + if (installed_dist is not None and + installed_dist.parsed_version in ireq.req.specifier): yield self._make_candidate_from_dist( - dist=dist, + dist=installed_dist, extras=extras, parent=ireq, ) From 7bcccbd3dcf19fdd353b05648e40edd0074e6dd3 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Fri, 17 Apr 2020 01:04:50 +0530 Subject: [PATCH 1641/3170] Canonicalise package name in tests.lib.create_basic_wheel_for_package --- news/FCD7E4ED-BA3E-4018-B43E-D445DA8E542B.trivial | 0 tests/lib/__init__.py | 3 +++ 2 files changed, 3 insertions(+) create mode 100644 news/FCD7E4ED-BA3E-4018-B43E-D445DA8E542B.trivial diff --git a/news/FCD7E4ED-BA3E-4018-B43E-D445DA8E542B.trivial b/news/FCD7E4ED-BA3E-4018-B43E-D445DA8E542B.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index 44f39858e14..892da51e1a3 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -994,6 +994,9 @@ def create_basic_wheel_for_package( if extra_files is None: extra_files = {} + # Fix wheel distribution name by replacing runs of non-alphanumeric + # characters with an underscore _ as per PEP 491 + name = re.sub(r"[^\w\d.]+", "_", name, re.UNICODE) archive_name = "{}-{}-py2.py3-none-any.whl".format(name, version) archive_path = script.scratch_path / archive_name From 510aa46580b2b51d8200c3e0ef8fff68efd7b057 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 17 Apr 2020 22:39:35 +0800 Subject: [PATCH 1642/3170] Space after comma --- src/pip/_internal/wheel_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index 9aa82b746d5..6d1022d5661 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -72,7 +72,7 @@ def _should_build( if not req.use_pep517 and not is_wheel_installed(): # we don't build legacy requirements if wheel is not installed logger.info( - "Could not build wheels for %s," + "Could not build wheels for %s, " "since package 'wheel' is not installed.", req.name, ) return False From 18fd161c448660499c110d5955a1d140902ec225 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sat, 18 Apr 2020 01:03:19 +0530 Subject: [PATCH 1643/3170] Fix pip config docstring to render correctly in docs --- news/8072.doc | 1 + src/pip/_internal/commands/configuration.py | 13 +++++++------ 2 files changed, 8 insertions(+), 6 deletions(-) create mode 100644 news/8072.doc diff --git a/news/8072.doc b/news/8072.doc new file mode 100644 index 00000000000..71eb46f292d --- /dev/null +++ b/news/8072.doc @@ -0,0 +1 @@ +Fix pip config docstring so that the subcommands render correctly in the docs diff --git a/src/pip/_internal/commands/configuration.py b/src/pip/_internal/commands/configuration.py index b801be6a03c..354a852b2a1 100644 --- a/src/pip/_internal/commands/configuration.py +++ b/src/pip/_internal/commands/configuration.py @@ -19,15 +19,16 @@ class ConfigurationCommand(Command): - """Manage local and global configuration. + """ + 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 + - 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 --site are passed, a virtual environment configuration file is used if one is active and the file From 22878a827ba640c81eb14911985c61df2d717c6f Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Fri, 17 Apr 2020 19:49:40 +0530 Subject: [PATCH 1644/3170] Added unit tests to verify correct normalization of package name --- tests/functional/test_install_wheel.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/tests/functional/test_install_wheel.py b/tests/functional/test_install_wheel.py index 6f7b77597ed..236eaf4d43e 100644 --- a/tests/functional/test_install_wheel.py +++ b/tests/functional/test_install_wheel.py @@ -587,3 +587,16 @@ def test_wheel_install_fails_with_badly_encoded_metadata(script): assert "Error decoding metadata for" in result.stderr assert "simple-0.1.0-py2.py3-none-any.whl" in result.stderr assert "METADATA" in result.stderr + + +@pytest.mark.parametrize( + 'package_name', + ['simple-package', 'simple_package'], +) +def test_correct_package_name_while_creating_wheel_bug(script, package_name): + """Check that the package name is correctly named while creating + a .whl file with a given format + """ + package = create_basic_wheel_for_package(script, package_name, '1.0') + wheel_name = os.path.basename(package) + assert wheel_name == 'simple_package-1.0-py2.py3-none-any.whl' From 4fb7687fa7c3b590adb4e68ea0f0a2e0c2cc4f5f Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sat, 18 Apr 2020 22:22:55 +0800 Subject: [PATCH 1645/3170] Add failing tests --- tests/functional/test_new_resolver.py | 31 +++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 3e28843d1f3..f20041c24bc 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -498,3 +498,34 @@ def test_new_resolver_force_reinstall(script): "base not reinstalled" ) assert_installed(script, base="0.1.0") + + +@pytest.mark.parametrize( + "available_versions, pip_args, expected_version", + [ + # Choose the latest non-prerelease by default. + (["1.0", "2.0a1"], ["pkg"], "1.0"), + # Choose the prerelease if the specifier spells out a prerelease. + (["1.0", "2.0a1"], ["pkg==2.0a1"], "2.0a1"), + # Choose the prerelease if explicitly allowed by the user. + (["1.0", "2.0a1"], ["pkg", "--pre"], "2.0a1"), + # Choose the prerelease if no stable releases are available. + (["2.0a1"], ["pkg"], "2.0a1"), + ], + ids=["default", "exact-pre", "explicit-pre", "no-stable"], +) +def test_new_resolver_handles_prerelease( + script, + available_versions, + pip_args, + expected_version, +): + for version in available_versions: + create_basic_wheel_for_package(script, "pkg", version) + script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + *pip_args + ) + assert_installed(script, pkg=expected_version) From 572d03b25bcad0044ddea013299cfad35297f900 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sat, 18 Apr 2020 22:50:42 +0800 Subject: [PATCH 1646/3170] Always allow prereleases in is_satisfied_by() --- .../_internal/resolution/resolvelib/requirements.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py index 97a41feee25..d2e4479b084 100644 --- a/src/pip/_internal/resolution/resolvelib/requirements.py +++ b/src/pip/_internal/resolution/resolvelib/requirements.py @@ -77,7 +77,11 @@ def is_satisfied_by(self, candidate): assert candidate.name == self.name, \ "Internal issue: Candidate is not for this requirement " \ " {} vs {}".format(candidate.name, self.name) - return candidate.version in self._ireq.req.specifier + # We can safely always allow prereleases here since PackageFinder + # already implements the prerelease logic, and would have filtered out + # prerelease candidates if the user does not expect them. + spec = self._ireq.req.specifier + return spec.contains(candidate.version, prereleases=True) class RequiresPythonRequirement(Requirement): @@ -109,4 +113,7 @@ def find_matches(self): def is_satisfied_by(self, candidate): # type: (Candidate) -> bool assert candidate.name == self._candidate.name, "Not Python candidate" - return candidate.version in self.specifier + # We can safely always allow prereleases here since PackageFinder + # already implements the prerelease logic, and would have filtered out + # prerelease candidates if the user does not expect them. + return self.specifier.contains(candidate.version, prereleases=True) From b69f70e725eec16588137b9e8ed83386998c7f93 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sat, 18 Apr 2020 23:10:55 +0800 Subject: [PATCH 1647/3170] News --- news/8075.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/8075.bugfix diff --git a/news/8075.bugfix b/news/8075.bugfix new file mode 100644 index 00000000000..e4b891dbdf8 --- /dev/null +++ b/news/8075.bugfix @@ -0,0 +1 @@ +Correctly allow prereleases when the user requires them in the new resolver. From a75f2fa31d2e0cef2102155d57090e1d46d7232d Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 19 Apr 2020 23:42:39 +0530 Subject: [PATCH 1648/3170] Document pip 21.0 as end of Python 2 support --- docs/html/development/release-process.rst | 5 ++--- news/python2.process | 1 + src/pip/_internal/cli/base_command.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) create mode 100644 news/python2.process diff --git a/docs/html/development/release-process.rst b/docs/html/development/release-process.rst index f41f3cb803b..11799e3c014 100644 --- a/docs/html/development/release-process.rst +++ b/docs/html/development/release-process.rst @@ -62,9 +62,8 @@ Python 2 support pip will continue to ensure that it runs on Python 2.7 after the CPython 2.7 EOL date. Support for Python 2.7 will be dropped, if bugs in Python 2.7 itself -make this necessary (which is unlikely) or Python 2 usage reduces to a level -where pip maintainers feel it is OK to drop support. The same approach is used -to determine when to drop support for other Python versions. +make this necessary (which is unlikely) or in pip 21.0 (Jan 2021), whichever is +earlier. However, bugs reported with pip which only occur on Python 2.7 would likely not be addressed directly by pip's maintainers. Pull Requests to fix Python 2.7 diff --git a/news/python2.process b/news/python2.process new file mode 100644 index 00000000000..4becb99c9b6 --- /dev/null +++ b/news/python2.process @@ -0,0 +1 @@ +Document that pip 21.0 will drop support for Python 2.7. diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index c3087c8b178..03c3774a656 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -141,7 +141,7 @@ def _main(self, args): not options.no_python_version_warning ): message = ( - "A future version of pip will drop support for Python 2.7. " + "pip 21.0 will drop support for Python 2.7. " "More details about Python 2 support in pip, can be found at " "https://pip.pypa.io/en/latest/development/release-process/#python-2-support" # noqa ) From 5483e6da9e3c87c9353e0b6ab0101017128384ea Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 20 Apr 2020 00:20:31 +0530 Subject: [PATCH 1649/3170] Add NEWS entry for the alpha resolver --- news/988.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/988.feature diff --git a/news/988.feature b/news/988.feature new file mode 100644 index 00000000000..80f162d5681 --- /dev/null +++ b/news/988.feature @@ -0,0 +1 @@ +An alpha version of a new resolver is available via ``--unstable-feature=resolver``. From ba46eb92cfbd4ca13afc49b71a844cd6ad0403c0 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 20 Apr 2020 00:29:38 +0530 Subject: [PATCH 1650/3170] "Release Notes" -> "Changelog" --- docs/html/development/architecture/anatomy.rst | 4 ++-- docs/html/news.rst | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/html/development/architecture/anatomy.rst b/docs/html/development/architecture/anatomy.rst index 2c731a97ae2..769d2b4796f 100644 --- a/docs/html/development/architecture/anatomy.rst +++ b/docs/html/development/architecture/anatomy.rst @@ -38,9 +38,9 @@ The ``README``, license, ``pyproject.toml``, ``setup.py``, and so on are in the * ``man/`` has man pages the distros can use by running ``man pip`` * ``pip_sphinxext.py`` *[an extension -- pip-specific plugins to Sphinx that do not apply to other packages]* -* ``news/`` *[pip stores news fragments… Every time pip makes a user-facing change, a file is added to this directory (usually a short note referring to a GitHub issue) with the right extension & name so it gets included in release notes…. So every release the maintainers will be deleting old files in this directory? Yes - we use the towncrier automation to generate a NEWS file, and auto-delete old stuff. There’s more about this in the contributor documentation!]* +* ``news/`` *[pip stores news fragments… Every time pip makes a user-facing change, a file is added to this directory (usually a short note referring to a GitHub issue) with the right extension & name so it gets included in changelog…. So every release the maintainers will be deleting old files in this directory? Yes - we use the towncrier automation to generate a NEWS file, and auto-delete old stuff. There’s more about this in the contributor documentation!]* - * ``template.rst`` *[template for release notes -- this is a file towncrier uses…. Is this jinja? I don’t know, check towncrier docs]* + * ``template.rst`` *[template for changelog -- this is a file towncrier uses…. Is this jinja? I don’t know, check towncrier docs]* * ``src/`` *[source; see below]* * ``tasks/`` *[invoke is a PyPI library which uses files in this directory to define automation commands that are used in pip’s development processes -- not discussing further right now. For instance, automating the release.]* diff --git a/docs/html/news.rst b/docs/html/news.rst index 1f6bfbf8ec3..c2b835a0ff9 100644 --- a/docs/html/news.rst +++ b/docs/html/news.rst @@ -1,5 +1,5 @@ -============= -Release Notes -============= +========= +Changelog +========= .. include:: ../../NEWS.rst From f1f63f052ef1b7342f18ea1aa0f545ddb4a0c264 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 20 Apr 2020 00:47:00 +0530 Subject: [PATCH 1651/3170] Make get_next_development_version understand betas --- tools/automation/release/__init__.py | 29 ++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/tools/automation/release/__init__.py b/tools/automation/release/__init__.py index 9728b3613fe..042723100a8 100644 --- a/tools/automation/release/__init__.py +++ b/tools/automation/release/__init__.py @@ -114,17 +114,30 @@ def create_git_tag(session: Session, tag_name: str, *, message: str) -> None: def get_next_development_version(version: str) -> str: - major, minor, *_ = map(int, version.split(".")) + is_beta = "b" in version.lower() - # We have at most 4 releases, starting with 0. Once we reach 3, we'd want - # to roll-over to the next year's release numbers. - if minor == 3: - major += 1 - minor = 0 + parts = version.split(".") + s_major, s_minor, *_ = parts + + # We only permit betas. + if is_beta: + s_minor, _, s_dev_number = s_minor.partition("b") else: - minor += 1 + s_dev_number = "0" + + major, minor = map(int, [s_major, s_minor]) + + # Increase minor version number if we're not releasing a beta. + if not is_beta: + # We have at most 4 releases, starting with 0. Once we reach 3, we'd + # want to roll-over to the next year's release numbers. + if minor == 3: + major += 1 + minor = 0 + else: + minor += 1 - return f"{major}.{minor}.dev0" + return f"{major}.{minor}.dev" + s_dev_number def have_files_in_folder(folder_name: str) -> bool: From 9c45fd43cad34f1a8c07283132af7713878bd1e6 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 20 Apr 2020 02:02:20 +0530 Subject: [PATCH 1652/3170] Remove 7826.feature --- news/7826.feature | 1 - 1 file changed, 1 deletion(-) delete mode 100644 news/7826.feature diff --git a/news/7826.feature b/news/7826.feature deleted file mode 100644 index 7c6897cc802..00000000000 --- a/news/7826.feature +++ /dev/null @@ -1 +0,0 @@ -Replaced remaining uses of '%' formatting with .format. Fixed two regressions introduced in earlier attempts. From a5d23523b6964507bf5129bff08364a7044e6c1a Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 20 Apr 2020 02:02:27 +0530 Subject: [PATCH 1653/3170] Remove 8075.feature --- news/8075.bugfix | 1 - 1 file changed, 1 deletion(-) delete mode 100644 news/8075.bugfix diff --git a/news/8075.bugfix b/news/8075.bugfix deleted file mode 100644 index e4b891dbdf8..00000000000 --- a/news/8075.bugfix +++ /dev/null @@ -1 +0,0 @@ -Correctly allow prereleases when the user requires them in the new resolver. From 45094a6c3204e24720e7989b352afb9eb0f28af5 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 20 Apr 2020 02:02:36 +0530 Subject: [PATCH 1654/3170] Reword 7856.bugfix --- news/7856.bugfix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/7856.bugfix b/news/7856.bugfix index 209805d81e8..50bf6810dd7 100644 --- a/news/7856.bugfix +++ b/news/7856.bugfix @@ -1 +1 @@ -Uninstallation no longer fails on trying to remove non-existent files. +Stop failing uninstallation, when trying to remove non-existent files. From 01d12700f1eedf1b35a242d5cbe47dfcde238406 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 20 Apr 2020 02:04:28 +0530 Subject: [PATCH 1655/3170] Reword 8013.bugfix --- news/8013.bugfix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/8013.bugfix b/news/8013.bugfix index a6e8fcb28e7..767b749e0c8 100644 --- a/news/8013.bugfix +++ b/news/8013.bugfix @@ -1 +1 @@ -Fix Windows folder writable detection. +Improve Windows compatibility when detecting writability in folder. From e8eb80802f89033f39619f73290f491d8ed60fdb Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 20 Apr 2020 02:04:54 +0530 Subject: [PATCH 1656/3170] Fix wording in 7962.bugfix --- news/7962.bugfix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/7962.bugfix b/news/7962.bugfix index 76c3442d053..6ee2cc55c5b 100644 --- a/news/7962.bugfix +++ b/news/7962.bugfix @@ -1 +1 @@ -Significantly speedup ``pip list --outdated`` through parallelizing index interaction. +Significantly speedup ``pip list --outdated`` by parallelizing index interaction. From 7cb733545b554454fcb183d41efcbd239a5bbd0e Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 20 Apr 2020 02:05:49 +0530 Subject: [PATCH 1657/3170] Reword pytoml.vendor --- news/pytoml.vendor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/pytoml.vendor b/news/pytoml.vendor index a0d77417073..4f7e9b335a7 100644 --- a/news/pytoml.vendor +++ b/news/pytoml.vendor @@ -1 +1 @@ -Drop ``pytoml`` from vendored libaries. +Remove pytoml as a vendored dependency. From 95edc13a48a62ef8a04057b3282f50f38011ce4f Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 20 Apr 2020 02:06:24 +0530 Subject: [PATCH 1658/3170] Reword toml.vendor --- news/toml.vendor | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/toml.vendor b/news/toml.vendor index 636b06b5c2f..ead9e5c9a29 100644 --- a/news/toml.vendor +++ b/news/toml.vendor @@ -1 +1 @@ -Add ``toml`` 0.10.0 to vendored libraries. +Add toml as a vendored dependency. From 406473db2aad04f29279f346cc3f57ee3d267ed3 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 20 Apr 2020 02:06:54 +0530 Subject: [PATCH 1659/3170] Add backticks to 7249.feature --- news/7249.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/7249.feature b/news/7249.feature index 0a791c928df..6074b3c841b 100644 --- a/news/7249.feature +++ b/news/7249.feature @@ -1 +1 @@ -Raise error if --user and --target are used together in pip install +Raise error if ``--user`` and ``--target`` are used together in ``pip install`` From 7470b7a3340f5e9879bbc908e5043112fa272fb1 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 20 Apr 2020 02:08:12 +0530 Subject: [PATCH 1660/3170] Reword 7729.feature --- news/7729.feature | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/news/7729.feature b/news/7729.feature index de4f6e7c8d7..6d050fe1414 100644 --- a/news/7729.feature +++ b/news/7729.feature @@ -1,3 +1 @@ -Cache the result of parse_links() to avoid re-tokenizing a find-links page multiple times over a pip run. - -This change significantly improves resolve performance when --find-links points to a very large html page. +Significantly improve performance when ``--find-links`` points to a very large HTML page. From 510be6a29d9a07be4a3be8769e0f5d494b0cd636 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 20 Apr 2020 02:08:50 +0530 Subject: [PATCH 1661/3170] Reword 7430.bugfix --- news/7430.bugfix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/7430.bugfix b/news/7430.bugfix index 9d0146af652..25980e0de49 100644 --- a/news/7430.bugfix +++ b/news/7430.bugfix @@ -1 +1 @@ -Warn if an invalid URL is passed with --index-url +Warn when an invalid URL is passed with ``--index-url`` From ab7be3211b3e93fd2cc174a41ed4d2b9ed111386 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 20 Apr 2020 02:10:07 +0530 Subject: [PATCH 1662/3170] Reword 7731.bugfix --- news/7731.bugfix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/7731.bugfix b/news/7731.bugfix index a73391027fe..7e9e17554e6 100644 --- a/news/7731.bugfix +++ b/news/7731.bugfix @@ -1 +1 @@ -Avoid using the current directory for check, freeze, install, list and show commands, when invoked as 'python -m pip <command>' +Remove current directory from ``sys.path`` when invoked as ``python -m pip <command>`` From 756b02f712c4798b388f0f206d6b9a9c402893d3 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 20 Apr 2020 02:02:02 +0530 Subject: [PATCH 1663/3170] Improve documentation for in-place builds --- docs/html/reference/pip_install.rst | 21 +++++++++++++-------- news/7555.removal | 11 ++--------- 2 files changed, 15 insertions(+), 17 deletions(-) diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index 56b16781e6b..6601ad865fa 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -723,19 +723,24 @@ local hash. Local project installs ---------------------- + pip supports installing local project in both regular mode and editable mode. You can install local projects by specifying the project path to pip:: $ pip install path/to/SomeProject -Until version 20.0, pip copied the entire project directory to a temporary -location and installed from there. This approach was the cause of several -performance and correctness issues. As of version 20.1 pip installs from the -local project directory. Depending on the build backend used by the project, -this may generate secondary build artifacts in the project directory, such as -the ``.egg-info`` and ``build`` directories in the case of the setuptools -backend. - +pip treats this directory like an unpacked source archive, and directly +attempts installation. + +Prior to pip 20.1, pip copied the entire project directory to a temporary +location and attempted installation from that directory. This approach was the +cause of several performance issues, as well as various issues arising when the +project directory depends on its parent directory (such as the presence of a +VCS directory). The main user visible effect of this change is that secondary +build artifacts, if any, would be created in the local directory, whereas +earlier they were created in a temporary copy of the directory and then +deleted. This notably includes the ``build`` and ``.egg-info`` directories in +the case of the setuptools backend. .. _`editable-installs`: diff --git a/news/7555.removal b/news/7555.removal index 2f5747f17d4..34a009ec8f2 100644 --- a/news/7555.removal +++ b/news/7555.removal @@ -1,9 +1,2 @@ -Building of local directories is now done in place. Previously pip did copy the -local directory tree to a temporary location before building. That approach had -a number of drawbacks, among which performance issues, as well as various -issues arising when the python project directory depends on its parent -directory (such as the presence of a VCS directory). The user visible effect of -this change is that secondary build artifacts, if any, may therefore be created -in the local directory, whereas before they were created in a temporary copy of -the directory and then deleted. This notably includes the ``build`` and -``.egg-info`` directories in the case of the setuptools build backend. +Building of local directories is now done in place, instead of a temporary +location containing a copy of the directory tree. From 35ea5a6d5f0d6b79d132e4a3ec3c37cc5c767aeb Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Mon, 20 Apr 2020 03:19:43 +0530 Subject: [PATCH 1664/3170] Remove Any type from run function --- src/pip/_internal/commands/completion.py | 2 +- src/pip/_internal/commands/debug.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/commands/completion.py b/src/pip/_internal/commands/completion.py index 66448bbf695..c62ce7b3ba3 100644 --- a/src/pip/_internal/commands/completion.py +++ b/src/pip/_internal/commands/completion.py @@ -84,7 +84,7 @@ def __init__(self, *args, **kw): self.parser.insert_option_group(0, cmd_opts) def run(self, options, args): - # type: (Values, List[Any]) -> int + # type: (Values, List[str]) -> int """Prints the completion code of the given shell""" shells = COMPLETION_SCRIPTS.keys() shell_options = ['--' + shell for shell in sorted(shells)] diff --git a/src/pip/_internal/commands/debug.py b/src/pip/_internal/commands/debug.py index 1a56db2fb19..341583ea7cf 100644 --- a/src/pip/_internal/commands/debug.py +++ b/src/pip/_internal/commands/debug.py @@ -202,7 +202,7 @@ def __init__(self, *args, **kw): self.parser.config.load() def run(self, options, args): - # type: (Values, List[Any]) -> int + # type: (Values, List[str]) -> int logger.warning( "This command is only meant for debugging. " "Do not use this with automation for parsing and getting these " From 3b35b416e95cffc6695e16801a17f2b8a61f7417 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Tue, 14 Apr 2020 12:06:21 +0530 Subject: [PATCH 1665/3170] Changed type of variadic args to Any and removed unneeded type ignore --- src/pip/_internal/commands/hash.py | 9 ++++----- src/pip/_internal/commands/show.py | 15 +++++++++------ src/pip/_internal/commands/wheel.py | 9 ++++----- 3 files changed, 17 insertions(+), 16 deletions(-) diff --git a/src/pip/_internal/commands/hash.py b/src/pip/_internal/commands/hash.py index 9bb6e9e032a..aab4a3dc2fe 100644 --- a/src/pip/_internal/commands/hash.py +++ b/src/pip/_internal/commands/hash.py @@ -12,7 +12,7 @@ if MYPY_CHECK_RUNNING: from optparse import Values - from typing import Any, List, Dict + from typing import Any, List logger = logging.getLogger(__name__) @@ -29,9 +29,8 @@ class HashCommand(Command): ignore_require_venv = True def __init__(self, *args, **kw): - # type: (List[Any], Dict[Any, Any]) -> None - # https://github.com/python/mypy/issues/4335 - super(HashCommand, self).__init__(*args, **kw) # type: ignore + # type: (*Any, **Any) -> None + super(HashCommand, self).__init__(*args, **kw) self.cmd_opts.add_option( '-a', '--algorithm', dest='algorithm', @@ -43,7 +42,7 @@ def __init__(self, *args, **kw): self.parser.insert_option_group(0, self.cmd_opts) def run(self, options, args): - # type: (Values, List[Any]) -> int + # type: (Values, List[str]) -> int if not args: self.parser.print_usage(sys.stderr) return ERROR diff --git a/src/pip/_internal/commands/show.py b/src/pip/_internal/commands/show.py index 436d607391c..f7522df9088 100644 --- a/src/pip/_internal/commands/show.py +++ b/src/pip/_internal/commands/show.py @@ -31,9 +31,8 @@ class ShowCommand(Command): ignore_require_venv = True def __init__(self, *args, **kw): - # type: (List[Any], Dict[Any, Any]) -> None - # https://github.com/python/mypy/issues/4335 - super(ShowCommand, self).__init__(*args, **kw) # type: ignore + # type: (*Any, **Any) -> None + super(ShowCommand, self).__init__(*args, **kw) self.cmd_opts.add_option( '-f', '--files', dest='files', @@ -44,7 +43,7 @@ def __init__(self, *args, **kw): self.parser.insert_option_group(0, self.cmd_opts) def run(self, options, args): - # type: (Values, List[Any]) -> int + # type: (Values, List[str]) -> int if not args: logger.warning('ERROR: Please provide a package name or names.') return ERROR @@ -58,7 +57,7 @@ def run(self, options, args): def search_packages_info(query): - # type: (List[Any]) -> Iterator[Dict[str, Any]] + # type: (List[str]) -> Iterator[Dict[str, str]] """ Gather details from installed distributions. Print distribution name, version, location, and installed files. Installed files requires a @@ -95,6 +94,10 @@ def get_requiring_packages(package_name): 'required_by': get_requiring_packages(dist.project_name) } file_list = None + # Set metadata to empty string to avoid metadata being typed as + # Optional[Any] in function calls using metadata below + # and since dist.get_metadata returns us a str, the default + # value of empty string should be valid metadata = '' if isinstance(dist, pkg_resources.DistInfoDistribution): # RECORDs should be part of .dist-info metadatas @@ -148,7 +151,7 @@ def get_requiring_packages(package_name): def print_results(distributions, list_files=False, verbose=False): - # type: (Iterator[Dict[str, Any]], bool, bool) -> bool + # type: (Iterator[Dict[str, str]], bool, bool) -> bool """ Print the information from installed distributions found. """ diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index bf17d9ac854..f028d681f7b 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -19,7 +19,7 @@ if MYPY_CHECK_RUNNING: from optparse import Values - from typing import Any, List, Dict + from typing import Any, List logger = logging.getLogger(__name__) @@ -48,9 +48,8 @@ class WheelCommand(RequirementCommand): %prog [options] <archive url/path> ...""" def __init__(self, *args, **kw): - # type: (List[Any], Dict[Any, Any]) -> None - # https://github.com/python/mypy/issues/4335 - super(WheelCommand, self).__init__(*args, **kw) # type: ignore + # type: (*Any, **Any) -> None + super(WheelCommand, self).__init__(*args, **kw) cmd_opts = self.cmd_opts @@ -112,7 +111,7 @@ def __init__(self, *args, **kw): @with_cleanup def run(self, options, args): - # type: (Values, List[Any]) -> int + # type: (Values, List[str]) -> int cmdoptions.check_install_build_global(options) session = self.get_default_session(options) From 52f30a1c46dcd9bb80c3b9e57eb93b97ee044c1a Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Tue, 14 Apr 2020 16:44:41 +0530 Subject: [PATCH 1666/3170] Add type for latest_info and fix type for iter_packages_latest_infos --- src/pip/_internal/commands/list.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index 7a0de088da3..4d68af1f240 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -25,7 +25,7 @@ if MYPY_CHECK_RUNNING: from optparse import Values - from typing import Any, List, Set, Tuple + from typing import Any, List, Set, Tuple, Iterator from pip._internal.network.session import PipSession from pip._vendor.pkg_resources import Distribution @@ -199,11 +199,12 @@ def get_not_required(self, packages, options): return list({pkg for pkg in packages if pkg.key not in dep_keys}) def iter_packages_latest_infos(self, packages, options): - # type: (List[Distribution], Values) -> Distribution + # type: (List[Distribution], Values) -> Iterator[Distribution] with self._build_session(options) as session: finder = self._build_package_finder(options, session) def latest_info(dist): + # type: (Distribution) -> Distribution typ = 'unknown' all_candidates = finder.find_all_candidates(dist.key) if not options.pre: From 2b6113797d8ce278c4329e5677de8f586cd40d0c Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Mon, 20 Apr 2020 03:29:47 +0530 Subject: [PATCH 1667/3170] Remove Any type from run function --- src/pip/_internal/commands/help.py | 4 ++-- src/pip/_internal/commands/list.py | 4 ++-- src/pip/_internal/commands/uninstall.py | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/commands/help.py b/src/pip/_internal/commands/help.py index ecd2b4737e3..a2edc29897f 100644 --- a/src/pip/_internal/commands/help.py +++ b/src/pip/_internal/commands/help.py @@ -6,7 +6,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import List, Any + from typing import List from optparse import Values @@ -18,7 +18,7 @@ class HelpCommand(Command): ignore_require_venv = True def run(self, options, args): - # type: (Values, List[Any]) -> int + # type: (Values, List[str]) -> int from pip._internal.commands import ( commands_dict, create_command, get_similar_commands, ) diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index 4d68af1f240..81612403d5e 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -143,7 +143,7 @@ def _build_package_finder(self, options, session): ) def run(self, options, args): - # type: (Values, List[Any]) -> int + # type: (Values, List[str]) -> int if options.outdated and options.uptodate: raise CommandError( "Options --outdated and --uptodate cannot be combined.") @@ -261,7 +261,7 @@ def output_package_listing(self, packages, options): write_output(format_for_json(packages, options)) def output_package_listing_columns(self, data, header): - # type: (List[List[Any]], List[str]) -> None + # type: (List[List[str]], List[str]) -> None # insert the header first: we need to know the size of column names if len(data) > 0: data.insert(0, header) diff --git a/src/pip/_internal/commands/uninstall.py b/src/pip/_internal/commands/uninstall.py index d7f96eeb060..0542e78c79c 100644 --- a/src/pip/_internal/commands/uninstall.py +++ b/src/pip/_internal/commands/uninstall.py @@ -55,7 +55,7 @@ def __init__(self, *args, **kw): self.parser.insert_option_group(0, self.cmd_opts) def run(self, options, args): - # type: (Values, List[Any]) -> int + # type: (Values, List[str]) -> int session = self.get_default_session(options) reqs_to_uninstall = {} From cebac6aa6c3ee09ead0f1e641de6a0e209dfb599 Mon Sep 17 00:00:00 2001 From: Hugo <hugovk@users.noreply.github.com> Date: Tue, 14 Apr 2020 13:36:33 +0300 Subject: [PATCH 1668/3170] Add 'pip cache dir' command to show the path to pip's cache directory --- news/7350.feature | 1 + src/pip/_internal/commands/cache.py | 10 ++++++++++ tests/functional/test_cache.py | 6 ++++++ 3 files changed, 17 insertions(+) create mode 100644 news/7350.feature diff --git a/news/7350.feature b/news/7350.feature new file mode 100644 index 00000000000..1b57d7b1907 --- /dev/null +++ b/news/7350.feature @@ -0,0 +1 @@ +Add ``pip cache dir`` to show the cache directory. diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index 7e3f72e080b..0625adb0fe7 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -24,6 +24,7 @@ class CacheCommand(Command): Subcommands: + dir: Show the cache directory. info: Show information about the cache. list: List filenames of packages stored in the cache. remove: Remove one or more package from the cache. @@ -33,6 +34,7 @@ class CacheCommand(Command): """ usage = """ + %prog dir %prog info %prog list [<pattern>] %prog remove <pattern> @@ -42,6 +44,7 @@ class CacheCommand(Command): def run(self, options, args): # type: (Values, List[Any]) -> int handlers = { + "dir": self.get_cache_dir, "info": self.get_cache_info, "list": self.list_cache_items, "remove": self.remove_cache_items, @@ -66,6 +69,13 @@ def run(self, options, args): return SUCCESS + def get_cache_dir(self, options, args): + # type: (Values, List[Any]) -> None + if args: + raise CommandError('Too many arguments') + + logger.info(options.cache_dir) + def get_cache_info(self, options, args): # type: (Values, List[Any]) -> None if args: diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index a464ece7945..62337876023 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -95,6 +95,12 @@ def _remove_matches_wheel(wheel_name, result): return _remove_matches_wheel +def test_cache_dir(script, cache_dir): + result = script.pip('cache', 'dir') + + assert os.path.normcase(cache_dir) == result.stdout.strip() + + @pytest.mark.usefixtures("populate_wheel_cache") def test_cache_info(script, wheel_cache_dir, wheel_cache_files): result = script.pip('cache', 'info') From 7e61569b1a33470a46efbc6c14a60e101d816080 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 20 Apr 2020 19:36:38 +0530 Subject: [PATCH 1669/3170] Drop --skip-requirements-regex --- src/pip/_internal/cli/base_command.py | 11 ----- src/pip/_internal/cli/cmdoptions.py | 11 ----- src/pip/_internal/commands/freeze.py | 1 - src/pip/_internal/operations/freeze.py | 6 --- src/pip/_internal/req/req_file.py | 30 ++----------- tests/unit/test_options.py | 6 --- tests/unit/test_req_file.py | 58 ++------------------------ 7 files changed, 8 insertions(+), 115 deletions(-) diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index c3087c8b178..722522110aa 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -153,17 +153,6 @@ def _main(self, args): ) + message deprecated(message, replacement=None, gone_in=None) - if options.skip_requirements_regex: - deprecated( - "--skip-requirements-regex is unsupported and will be removed", - replacement=( - "manage requirements/constraints files explicitly, " - "possibly generating them from metadata" - ), - gone_in="20.1", - issue=7297, - ) - # TODO: Try to get these passing down from the command? # without resorting to os.environ to hold these. # This also affects isolated builds and it should. diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index ff9acfd4644..5b5647d64f4 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -275,16 +275,6 @@ class PipOption(Option): help='Set the socket timeout (default %default seconds).', ) # type: Callable[..., Option] -skip_requirements_regex = partial( - Option, - # A regex to be used to skip requirements - '--skip-requirements-regex', - dest='skip_requirements_regex', - type='str', - default='', - help=SUPPRESS_HELP, -) # type: Callable[..., Option] - def exists_action(): # type: () -> Option @@ -948,7 +938,6 @@ def check_list_path_option(options): proxy, retries, timeout, - skip_requirements_regex, exists_action, trusted_host, cert, diff --git a/src/pip/_internal/commands/freeze.py b/src/pip/_internal/commands/freeze.py index 4758e30343f..13171772e5c 100644 --- a/src/pip/_internal/commands/freeze.py +++ b/src/pip/_internal/commands/freeze.py @@ -89,7 +89,6 @@ def run(self, options, args): local_only=options.local, user_only=options.user, paths=options.path, - skip_regex=options.skip_requirements_regex, isolated=options.isolated_mode, wheel_cache=wheel_cache, skip=skip, diff --git a/src/pip/_internal/operations/freeze.py b/src/pip/_internal/operations/freeze.py index 3198c775771..5b7d79b9ce5 100644 --- a/src/pip/_internal/operations/freeze.py +++ b/src/pip/_internal/operations/freeze.py @@ -50,7 +50,6 @@ def freeze( local_only=None, # type: Optional[bool] user_only=None, # type: Optional[bool] paths=None, # type: Optional[List[str]] - skip_regex=None, # type: Optional[str] isolated=False, # type: bool wheel_cache=None, # type: Optional[WheelCache] exclude_editable=False, # type: bool @@ -58,10 +57,6 @@ def freeze( ): # type: (...) -> Iterator[str] find_links = find_links or [] - skip_match = None - - if skip_regex: - skip_match = re.compile(skip_regex).search for link in find_links: yield '-f {}'.format(link) @@ -100,7 +95,6 @@ def freeze( 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', diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index 2501e7c25fd..30a4a760f8d 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -146,13 +146,8 @@ def parse_requirements( :param constraint: If true, parsing a constraint file rather than requirements file. """ - skip_requirements_regex = ( - options.skip_requirements_regex if options else None - ) line_parser = get_line_parser(finder) - parser = RequirementsFileParser( - session, line_parser, comes_from, skip_requirements_regex - ) + parser = RequirementsFileParser(session, line_parser, comes_from) for parsed_line in parser.parse(filename, constraint): parsed_req = handle_line( @@ -165,18 +160,15 @@ def parse_requirements( yield parsed_req -def preprocess(content, skip_requirements_regex): - # type: (Text, Optional[str]) -> ReqFileLines +def preprocess(content): + # type: (Text) -> ReqFileLines """Split, filter, and join lines, and return a line iterator :param content: the content of the requirements file - :param skip_requirements_regex: the pattern to skip lines """ lines_enum = enumerate(content.splitlines(), start=1) # type: ReqFileLines lines_enum = join_lines(lines_enum) lines_enum = ignore_comments(lines_enum) - if skip_requirements_regex: - lines_enum = skip_regex(lines_enum, skip_requirements_regex) lines_enum = expand_env_variables(lines_enum) return lines_enum @@ -326,13 +318,11 @@ def __init__( session, # type: PipSession line_parser, # type: LineParser comes_from, # type: str - skip_requirements_regex, # type: Optional[str] ): # type: (...) -> None self._session = session self._line_parser = line_parser self._comes_from = comes_from - self._skip_requirements_regex = skip_requirements_regex def parse(self, filename, constraint): # type: (str, bool) -> Iterator[ParsedLine] @@ -380,7 +370,7 @@ def _parse_file(self, filename, constraint): filename, self._session, comes_from=self._comes_from ) - lines_enum = preprocess(content, self._skip_requirements_regex) + lines_enum = preprocess(content) for line_number, line in lines_enum: try: @@ -517,18 +507,6 @@ def ignore_comments(lines_enum): yield line_number, line -def skip_regex(lines_enum, pattern): - # type: (ReqFileLines, str) -> ReqFileLines - """ - Skip lines that match the provided pattern - - Note: the regex pattern is only built once - """ - matcher = re.compile(pattern) - lines_enum = filterfalse(lambda e: matcher.search(e[1]), lines_enum) - return lines_enum - - def expand_env_variables(lines_enum): # type: (ReqFileLines) -> ReqFileLines """Replace all environment variables that can be retrieved via `os.getenv`. diff --git a/tests/unit/test_options.py b/tests/unit/test_options.py index 534bc92afc9..950075ba435 100644 --- a/tests/unit/test_options.py +++ b/tests/unit/test_options.py @@ -359,12 +359,6 @@ def test_timeout(self): options2, args2 = main(['fake', '--timeout', '-1']) assert options1.timeout == options2.timeout == -1 - def test_skip_requirements_regex(self): - options1, args1 = main(['--skip-requirements-regex', 'path', 'fake']) - options2, args2 = main(['fake', '--skip-requirements-regex', 'path']) - assert options1.skip_requirements_regex == 'path' - assert options2.skip_requirements_regex == 'path' - def test_exists_action(self): options1, args1 = main(['--exists-action', 'w', 'fake']) options2, args2 = main(['fake', '--exists-action', 'w']) diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py index 57b50017a7f..f2809cbc6fc 100644 --- a/tests/unit/test_req_file.py +++ b/tests/unit/test_req_file.py @@ -27,7 +27,6 @@ join_lines, parse_requirements, preprocess, - skip_regex, ) from tests.lib import make_test_finder, requirements_file @@ -45,9 +44,10 @@ def finder(session): @pytest.fixture def options(session): return stub( - isolated_mode=False, index_url='default_url', - skip_requirements_regex=False, - format_control=FormatControl(set(), set())) + isolated_mode=False, + index_url='default_url', + format_control=FormatControl(set(), set()), + ) def parse_reqfile( @@ -99,26 +99,6 @@ def test_comments_and_joins_case3(self): result = preprocess(content, None) assert list(result) == [(1, 'req1'), (3, 'req2')] - def test_skip_regex_after_joining_case1(self, options): - content = textwrap.dedent("""\ - patt\\ - ern - line2 - """) - skip_requirements_regex = 'pattern' - result = preprocess(content, skip_requirements_regex) - assert list(result) == [(3, 'line2')] - - def test_skip_regex_after_joining_case2(self, options): - content = textwrap.dedent("""\ - pattern \\ - line2 - line3 - """) - skip_requirements_regex = 'pattern' - result = preprocess(content, skip_requirements_regex) - assert list(result) == [(3, 'line3')] - class TestIgnoreComments(object): """tests for `ignore_comment`""" @@ -172,25 +152,6 @@ def test_last_line_with_escape(self): assert expect == list(join_lines(lines)) -class TestSkipRegex(object): - """tests for `skip_reqex``""" - - def test_skip_regex_pattern_match(self): - pattern = '.*Bad.*' - line = '--extra-index-url Bad' - assert [] == list(skip_regex(enumerate([line]), pattern)) - - def test_skip_regex_pattern_not_match(self): - pattern = '.*Bad.*' - line = '--extra-index-url Good' - assert [(0, line)] == list(skip_regex(enumerate([line]), pattern)) - - def test_skip_regex_no_options(self): - pattern = None - line = '--extra-index-url Good' - assert [(1, line)] == list(preprocess(line, pattern)) - - @pytest.fixture def line_processor( monkeypatch, @@ -616,17 +577,6 @@ def test_multiple_appending_options(self, tmpdir, finder, options): assert finder.index_urls == ['url1', 'url2'] - def test_skip_regex(self, tmpdir, finder, options): - options.skip_requirements_regex = '.*Bad.*' - with open(tmpdir.joinpath("req1.txt"), "w") as fp: - fp.write("--extra-index-url Bad \n") - fp.write("--extra-index-url Good ") - - list(parse_reqfile(tmpdir.joinpath("req1.txt"), finder=finder, - options=options, session=PipSession())) - - assert finder.index_urls == ['Good'] - def test_expand_existing_env_variables(self, tmpdir, finder): template = ( 'https://{}:x-oauth-basic@github.com/' From 9994a4d9f5db87570fe64f4376f68e16e870048b Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 20 Apr 2020 19:37:42 +0530 Subject: [PATCH 1670/3170] :newspaper: --- news/7297.removal | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/7297.removal diff --git a/news/7297.removal b/news/7297.removal new file mode 100644 index 00000000000..332451f2541 --- /dev/null +++ b/news/7297.removal @@ -0,0 +1 @@ +Remove deprecated ``--skip-requirements-regex`` option. From b5e2cc626435f3a058b1f2eaad213b0e545737a4 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 20 Apr 2020 22:45:09 +0530 Subject: [PATCH 1671/3170] Remove unused imports --- src/pip/_internal/operations/freeze.py | 1 - src/pip/_internal/req/req_file.py | 1 - 2 files changed, 2 deletions(-) diff --git a/src/pip/_internal/operations/freeze.py b/src/pip/_internal/operations/freeze.py index 5b7d79b9ce5..aa6b052b6aa 100644 --- a/src/pip/_internal/operations/freeze.py +++ b/src/pip/_internal/operations/freeze.py @@ -7,7 +7,6 @@ import collections import logging import os -import re from pip._vendor import six from pip._vendor.packaging.utils import canonicalize_name diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index 30a4a760f8d..63cab76f6f2 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -13,7 +13,6 @@ import shlex import sys -from pip._vendor.six.moves import filterfalse from pip._vendor.six.moves.urllib import parse as urllib_parse from pip._internal.cli import cmdoptions From 5e4c3565e4a98890c612a0c91c6766e3399c09dd Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 20 Apr 2020 22:58:10 +0530 Subject: [PATCH 1672/3170] Update tests, didn't directly reference "regex" --- tests/unit/test_req_file.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py index f2809cbc6fc..4ef4f7217e0 100644 --- a/tests/unit/test_req_file.py +++ b/tests/unit/test_req_file.py @@ -79,7 +79,7 @@ def test_comments_and_joins_case1(self): # comment \\ req2 """) - result = preprocess(content, None) + result = preprocess(content) assert list(result) == [(1, 'req1'), (3, 'req2')] def test_comments_and_joins_case2(self): @@ -87,7 +87,7 @@ def test_comments_and_joins_case2(self): req1\\ # comment """) - result = preprocess(content, None) + result = preprocess(content) assert list(result) == [(1, 'req1')] def test_comments_and_joins_case3(self): @@ -96,7 +96,7 @@ def test_comments_and_joins_case3(self): # comment req2 """) - result = preprocess(content, None) + result = preprocess(content) assert list(result) == [(1, 'req1'), (3, 'req2')] From 012f69ecbd5b8a9b3faf9cfb557b038a6745497a Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 21 Apr 2020 00:31:40 +0530 Subject: [PATCH 1673/3170] Update AUTHORS.txt --- AUTHORS.txt | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/AUTHORS.txt b/AUTHORS.txt index ac424168adc..134f83a78a4 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -1,3 +1,4 @@ +@Switch01 A_Rog Aakanksha Agrawal Abhinav Sagar @@ -32,6 +33,7 @@ Ami Fischman Ananya Maiti Anatoly Techtonik Anders Kaseorg +Andre Aguiar Andreas Lutro Andrei Geacar Andrew Gaul @@ -51,6 +53,7 @@ Antonio Alvarado Hernandez Antony Lee Antti Kaihola Anubhav Patel +Anudit Nagar Anuj Godase AQNOUCH Mohammed AraHaan @@ -78,6 +81,7 @@ Berker Peksag Bernardo B. Marques Bernhard M. Wiedemann Bertil Hatt +Bhavam Vidyarthi Bogdan Opanchuk BorisZZZ Brad Erickson @@ -109,10 +113,13 @@ Chris Hunt Chris Jerdonek Chris McDonough Chris Wolfe +Christian Clauss Christian Heimes Christian Oudard +Christoph Reiter Christopher Hunt Christopher Snyder +cjc7373 Clark Boylan Clay McClure Cody @@ -140,6 +147,7 @@ Daniel Shaulov Daniele Esposti Daniele Procida Danny Hermes +Danny McClanahan Dav Clark Dave Abrahams Dave Jones @@ -153,8 +161,10 @@ David Pursehouse David Tucker David Wales Davidovich +Deepak Sharma derwolfe Desetude +Devesh Kumar Singh Diego Caraballo DiegoCaraballo Dmitry Gladkov @@ -170,6 +180,7 @@ Eitan Adler ekristina elainechan Eli Schwartz +Ellen Marie Dash Emil Burzo Emil Styrke Endoh Takanao @@ -201,6 +212,7 @@ Geoffrey Lehée Geoffrey Sneddon George Song Georgi Valkov +ghost Giftlin Rajaiah gizmoguy1 gkdoc @@ -208,6 +220,7 @@ Gopinath M GOTO Hayato gpiks Guilherme Espada +gutsytechster Guy Rozendorn gzpan123 Hanjun Kim @@ -227,6 +240,7 @@ Ian Stapleton Cordasco Ian Wienand Igor Kuzmitshov Igor Sobreira +Ilan Schnell Ilya Baryshev INADA Naoki Ionel Cristian Mărieș @@ -256,6 +270,7 @@ Jiashuo Li Jim Garrison Jivan Amara John Paton +John T. Wodder II John-Scott Atlakson johnthagen Jon Banafato @@ -289,6 +304,7 @@ Kevin Frommelt Kevin R Patterson Kexuan Sun Kit Randel +KOLANICH kpinc Krishna Oza Kumar McMillan @@ -359,12 +375,15 @@ Nate Coraor Nathaniel J. Smith Nehal J Wani Neil Botelho +Nguyễn Gia Phong Nick Coghlan Nick Stenning Nick Timkovich Nicolas Bock Nikhil Benesch +Nikolay Korolev Nitesh Sharma +Noah Gorny Nowell Strite NtaleGrey nvdv @@ -377,6 +396,7 @@ Olivier Grisel Ollie Rutherfurd OMOTO Kenji Omry Yadan +onlinejudge95 Oren Held Oscar Benjamin Oz N Tiram @@ -412,6 +432,7 @@ Prabakaran Kumaresshan Prabhjyotsing Surjit Singh Sodhi Prabhu Marappan Pradyun Gedam +Prashant Sharma Pratik Mallya Preet Thakkar Preston Holmes @@ -424,10 +445,12 @@ Rafael Caricio Ralf Schmitt Razzi Abuissa rdb +Reece Dunham Remi Rampin Rene Dudfield Riccardo Magliocchetti Richard Jones +Ricky Ng-Adam RobberPhex Robert Collins Robert McGibbon @@ -462,6 +485,7 @@ Simeon Visser Simon Cross Simon Pichugin sinoroc +sinscary Sorin Sbarnea Stavros Korokithakis Stefan Scherfke @@ -497,6 +521,7 @@ tinruufu Tom Forbes Tom Freudenheim Tom V +Tomas Hrnciar Tomas Orsava Tomer Chachamu Tony Beswick From 1626bb917b4bbf5f88b4800b6002350e6a909164 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 21 Apr 2020 00:31:45 +0530 Subject: [PATCH 1674/3170] Bump for release --- NEWS.rst | 71 +++++++++++++++++++ news/1983.doc | 2 - ...96C81F-4A3E-42AD-9562-7BB7EB0A7EF9.trivial | 0 ...476270-1CF0-4D00-8621-E633D06AA53A.trivial | 0 ...08D551-576D-4239-BBB4-F5B9DB5E36A2.trivial | 0 news/3191.doc | 1 - ...d2f118-19f0-4c2c-b002-d70a8629b350.trivial | 0 news/3988.bugfix | 1 - ...3EA005-0471-4D5D-A81B-B6904A844EEE.trivial | 0 news/5979.removal | 1 - ...c551d8-61a7-4f48-9d90-58909eca5537.trivial | 0 news/609.feature | 2 - news/6391.feature | 1 - news/6446.bugfix | 2 - ...a4dd1e-c03f-4780-ae6f-892f818fb367.trivial | 0 news/7008.doc | 1 - news/7249.feature | 1 - news/7297.removal | 1 - news/7402.bugfix | 1 - news/7430.bugfix | 1 - news/7555.removal | 2 - ...611-gh-actions--linters-adjustment.trivial | 1 - news/7680.removal | 1 - news/7690.vendor | 1 - news/7699.bugfix | 2 - news/7718.bugfix | 2 - news/7729.feature | 1 - news/7731.bugfix | 1 - news/7737.trivial | 1 - news/7740.trivial | 1 - news/7767.doc | 1 - news/7768.feature | 1 - news/7794.trivial | 1 - news/7803.doc | 1 - news/7847.feature | 1 - news/7850.vendor | 1 - news/7856.bugfix | 1 - news/7872.bugfix | 1 - news/7959.trivial | 17 ----- news/7962.bugfix | 1 - ...174d2e-1647-4794-b1d0-58f32da01540.trivial | 0 news/8013.bugfix | 1 - news/988.feature | 1 - ...2A89E1-F932-4159-9E69-8AEA7DFD6432.trivial | 0 ...97ed3a-49da-4279-b71a-7b67de91c34a.trivial | 0 ...1aae95-1bc6-4a32-b005-65d0a7843207.trivial | 0 ...a56cb3-00b1-4ccb-805a-ac4807c72a52.trivial | 0 news/certifi.vendor | 1 - news/contextlib2.vendor | 1 - news/distro.vendor | 1 - ...e07991-9123-41bb-9571-7efbe141a93e.trivial | 0 ...a93c6a-3d20-4662-b510-0a11603837b1.trivial | 0 news/idna.vendor | 1 - news/msgpack.vendor | 1 - news/packaging.vendor | 1 - news/pep517.vendor | 1 - news/pyparsing.vendor | 1 - news/pytoml.vendor | 1 - news/requests.vendor | 1 - news/toml.vendor | 1 - news/urllib3.vendor | 1 - src/pip/__init__.py | 2 +- 62 files changed, 72 insertions(+), 69 deletions(-) delete mode 100644 news/1983.doc delete mode 100644 news/1C96C81F-4A3E-42AD-9562-7BB7EB0A7EF9.trivial delete mode 100644 news/22476270-1CF0-4D00-8621-E633D06AA53A.trivial delete mode 100644 news/2808D551-576D-4239-BBB4-F5B9DB5E36A2.trivial delete mode 100644 news/3191.doc delete mode 100644 news/37d2f118-19f0-4c2c-b002-d70a8629b350.trivial delete mode 100644 news/3988.bugfix delete mode 100644 news/533EA005-0471-4D5D-A81B-B6904A844EEE.trivial delete mode 100644 news/5979.removal delete mode 100644 news/5fc551d8-61a7-4f48-9d90-58909eca5537.trivial delete mode 100644 news/609.feature delete mode 100644 news/6391.feature delete mode 100644 news/6446.bugfix delete mode 100644 news/69a4dd1e-c03f-4780-ae6f-892f818fb367.trivial delete mode 100644 news/7008.doc delete mode 100644 news/7249.feature delete mode 100644 news/7297.removal delete mode 100644 news/7402.bugfix delete mode 100644 news/7430.bugfix delete mode 100644 news/7555.removal delete mode 100644 news/7611-gh-actions--linters-adjustment.trivial delete mode 100644 news/7680.removal delete mode 100644 news/7690.vendor delete mode 100644 news/7699.bugfix delete mode 100644 news/7718.bugfix delete mode 100644 news/7729.feature delete mode 100644 news/7731.bugfix delete mode 100644 news/7737.trivial delete mode 100644 news/7740.trivial delete mode 100644 news/7767.doc delete mode 100644 news/7768.feature delete mode 100644 news/7794.trivial delete mode 100644 news/7803.doc delete mode 100644 news/7847.feature delete mode 100644 news/7850.vendor delete mode 100644 news/7856.bugfix delete mode 100644 news/7872.bugfix delete mode 100644 news/7959.trivial delete mode 100644 news/7962.bugfix delete mode 100644 news/7a174d2e-1647-4794-b1d0-58f32da01540.trivial delete mode 100644 news/8013.bugfix delete mode 100644 news/988.feature delete mode 100644 news/9E2A89E1-F932-4159-9E69-8AEA7DFD6432.trivial delete mode 100644 news/b397ed3a-49da-4279-b71a-7b67de91c34a.trivial delete mode 100644 news/c11aae95-1bc6-4a32-b005-65d0a7843207.trivial delete mode 100644 news/c9a56cb3-00b1-4ccb-805a-ac4807c72a52.trivial delete mode 100644 news/certifi.vendor delete mode 100644 news/contextlib2.vendor delete mode 100644 news/distro.vendor delete mode 100644 news/e5e07991-9123-41bb-9571-7efbe141a93e.trivial delete mode 100644 news/f2a93c6a-3d20-4662-b510-0a11603837b1.trivial delete mode 100644 news/idna.vendor delete mode 100644 news/msgpack.vendor delete mode 100644 news/packaging.vendor delete mode 100644 news/pep517.vendor delete mode 100644 news/pyparsing.vendor delete mode 100644 news/pytoml.vendor delete mode 100644 news/requests.vendor delete mode 100644 news/toml.vendor delete mode 100644 news/urllib3.vendor diff --git a/NEWS.rst b/NEWS.rst index d0b290f8642..493dc206e2b 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -7,6 +7,77 @@ .. towncrier release notes start +20.1b1 (2020-04-21) +=================== + +Deprecations and Removals +------------------------- + +- Remove emails from AUTHORS.txt to prevent usage for spamming, and only populate names in AUTHORS.txt at time of release (`#5979 <https://github.com/pypa/pip/issues/5979>`_) +- Remove deprecated ``--skip-requirements-regex`` option. (`#7297 <https://github.com/pypa/pip/issues/7297>`_) +- Building of local directories is now done in place, instead of a temporary + location containing a copy of the directory tree. (`#7555 <https://github.com/pypa/pip/issues/7555>`_) +- Remove unused ``tests/scripts/test_all_pip.py`` test script and the ``tests/scripts`` folder. (`#7680 <https://github.com/pypa/pip/issues/7680>`_) + +Features +-------- + +- pip now implements PEP 610, so ``pip freeze`` has better fidelity + in presence of distributions installed from Direct URL requirements. (`#609 <https://github.com/pypa/pip/issues/609>`_) +- Add ``pip cache`` command for inspecting/managing pip's wheel cache. (`#6391 <https://github.com/pypa/pip/issues/6391>`_) +- Raise error if ``--user`` and ``--target`` are used together in ``pip install`` (`#7249 <https://github.com/pypa/pip/issues/7249>`_) +- Significantly improve performance when ``--find-links`` points to a very large HTML page. (`#7729 <https://github.com/pypa/pip/issues/7729>`_) +- Indicate when wheel building is skipped, due to lack of the ``wheel`` package. (`#7768 <https://github.com/pypa/pip/issues/7768>`_) +- Change default behaviour to always cache responses from trusted-host source. (`#7847 <https://github.com/pypa/pip/issues/7847>`_) +- An alpha version of a new resolver is available via ``--unstable-feature=resolver``. (`#988 <https://github.com/pypa/pip/issues/988>`_) + +Bug Fixes +--------- + +- Correctly freeze a VCS editable package when it is nested inside another VCS repository. (`#3988 <https://github.com/pypa/pip/issues/3988>`_) +- Correctly handle ``%2F`` in URL parameters to avoid accidentally unescape them + into ``/``. (`#6446 <https://github.com/pypa/pip/issues/6446>`_) +- Reject VCS URLs with an empty revision. (`#7402 <https://github.com/pypa/pip/issues/7402>`_) +- Warn when an invalid URL is passed with ``--index-url`` (`#7430 <https://github.com/pypa/pip/issues/7430>`_) +- Use better mechanism for handling temporary files, when recording metadata + about installed files (RECORD) and the installer (INSTALLER). (`#7699 <https://github.com/pypa/pip/issues/7699>`_) +- Correctly detect global site-packages availability of virtual environments + created by PyPA’s virtualenv>=20.0. (`#7718 <https://github.com/pypa/pip/issues/7718>`_) +- Remove current directory from ``sys.path`` when invoked as ``python -m pip <command>`` (`#7731 <https://github.com/pypa/pip/issues/7731>`_) +- Stop failing uninstallation, when trying to remove non-existent files. (`#7856 <https://github.com/pypa/pip/issues/7856>`_) +- Prevent an infinite recursion with ``pip wheel`` when ``$TMPDIR`` is within the source directory. (`#7872 <https://github.com/pypa/pip/issues/7872>`_) +- Significantly speedup ``pip list --outdated`` by parallelizing index interaction. (`#7962 <https://github.com/pypa/pip/issues/7962>`_) +- Improve Windows compatibility when detecting writability in folder. (`#8013 <https://github.com/pypa/pip/issues/8013>`_) + +Vendored Libraries +------------------ + +- Update semi-supported debundling script to reflect that appdirs is vendored. +- Add ResolveLib as a vendored dependency. +- Upgrade certifi to 2020.04.05.1 +- Upgrade contextlib2 to 0.6.0.post1 +- Upgrade distro to 1.5.0. +- Upgrade idna to 2.9. +- Upgrade msgpack to 1.0.0. +- Upgrade packaging to 20.3. +- Upgrade pep517 to 0.8.2. +- Upgrade pyparsing to 2.4.7. +- Remove pytoml as a vendored dependency. +- Upgrade requests to 2.23.0. +- Add toml as a vendored dependency. +- Upgrade urllib3 to 1.25.8. + +Improved Documentation +---------------------- + +- Emphasize that VCS URLs using git, git+git and git+http are insecure due to + lack of authentication and encryption (`#1983 <https://github.com/pypa/pip/issues/1983>`_) +- Clarify the usage of --no-binary command. (`#3191 <https://github.com/pypa/pip/issues/3191>`_) +- Clarify the usage of freeze command in the example of Using pip in your program (`#7008 <https://github.com/pypa/pip/issues/7008>`_) +- Add a "Copyright" page. (`#7767 <https://github.com/pypa/pip/issues/7767>`_) +- Added example of defining multiple values for options which support them (`#7803 <https://github.com/pypa/pip/issues/7803>`_) + + 20.0.2 (2020-01-24) =================== diff --git a/news/1983.doc b/news/1983.doc deleted file mode 100644 index 9766ebb571d..00000000000 --- a/news/1983.doc +++ /dev/null @@ -1,2 +0,0 @@ -Emphasize that VCS URLs using git, git+git and git+http are insecure due to -lack of authentication and encryption diff --git a/news/1C96C81F-4A3E-42AD-9562-7BB7EB0A7EF9.trivial b/news/1C96C81F-4A3E-42AD-9562-7BB7EB0A7EF9.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/22476270-1CF0-4D00-8621-E633D06AA53A.trivial b/news/22476270-1CF0-4D00-8621-E633D06AA53A.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/2808D551-576D-4239-BBB4-F5B9DB5E36A2.trivial b/news/2808D551-576D-4239-BBB4-F5B9DB5E36A2.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/3191.doc b/news/3191.doc deleted file mode 100644 index 513cf2d0a62..00000000000 --- a/news/3191.doc +++ /dev/null @@ -1 +0,0 @@ -Clarify the usage of --no-binary command. diff --git a/news/37d2f118-19f0-4c2c-b002-d70a8629b350.trivial b/news/37d2f118-19f0-4c2c-b002-d70a8629b350.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/3988.bugfix b/news/3988.bugfix deleted file mode 100644 index 314bd31fcbb..00000000000 --- a/news/3988.bugfix +++ /dev/null @@ -1 +0,0 @@ -Correctly freeze a VCS editable package when it is nested inside another VCS repository. diff --git a/news/533EA005-0471-4D5D-A81B-B6904A844EEE.trivial b/news/533EA005-0471-4D5D-A81B-B6904A844EEE.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/5979.removal b/news/5979.removal deleted file mode 100644 index 9791a1e50b6..00000000000 --- a/news/5979.removal +++ /dev/null @@ -1 +0,0 @@ -Remove emails from AUTHORS.txt to prevent usage for spamming, and only populate names in AUTHORS.txt at time of release diff --git a/news/5fc551d8-61a7-4f48-9d90-58909eca5537.trivial b/news/5fc551d8-61a7-4f48-9d90-58909eca5537.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/609.feature b/news/609.feature deleted file mode 100644 index 1a2a6702a58..00000000000 --- a/news/609.feature +++ /dev/null @@ -1,2 +0,0 @@ -pip now implements PEP 610, so ``pip freeze`` has better fidelity -in presence of distributions installed from Direct URL requirements. diff --git a/news/6391.feature b/news/6391.feature deleted file mode 100644 index e13df852713..00000000000 --- a/news/6391.feature +++ /dev/null @@ -1 +0,0 @@ -Add ``pip cache`` command for inspecting/managing pip's wheel cache. diff --git a/news/6446.bugfix b/news/6446.bugfix deleted file mode 100644 index 425e72bd830..00000000000 --- a/news/6446.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -Correctly handle ``%2F`` in URL parameters to avoid accidentally unescape them -into ``/``. diff --git a/news/69a4dd1e-c03f-4780-ae6f-892f818fb367.trivial b/news/69a4dd1e-c03f-4780-ae6f-892f818fb367.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/7008.doc b/news/7008.doc deleted file mode 100644 index 0ecd15d2ca6..00000000000 --- a/news/7008.doc +++ /dev/null @@ -1 +0,0 @@ -Clarify the usage of freeze command in the example of Using pip in your program diff --git a/news/7249.feature b/news/7249.feature deleted file mode 100644 index 6074b3c841b..00000000000 --- a/news/7249.feature +++ /dev/null @@ -1 +0,0 @@ -Raise error if ``--user`` and ``--target`` are used together in ``pip install`` diff --git a/news/7297.removal b/news/7297.removal deleted file mode 100644 index 332451f2541..00000000000 --- a/news/7297.removal +++ /dev/null @@ -1 +0,0 @@ -Remove deprecated ``--skip-requirements-regex`` option. diff --git a/news/7402.bugfix b/news/7402.bugfix deleted file mode 100644 index 91eb085f5bc..00000000000 --- a/news/7402.bugfix +++ /dev/null @@ -1 +0,0 @@ -Reject VCS URLs with an empty revision. diff --git a/news/7430.bugfix b/news/7430.bugfix deleted file mode 100644 index 25980e0de49..00000000000 --- a/news/7430.bugfix +++ /dev/null @@ -1 +0,0 @@ -Warn when an invalid URL is passed with ``--index-url`` diff --git a/news/7555.removal b/news/7555.removal deleted file mode 100644 index 34a009ec8f2..00000000000 --- a/news/7555.removal +++ /dev/null @@ -1,2 +0,0 @@ -Building of local directories is now done in place, instead of a temporary -location containing a copy of the directory tree. diff --git a/news/7611-gh-actions--linters-adjustment.trivial b/news/7611-gh-actions--linters-adjustment.trivial deleted file mode 100644 index 43efc8aa42d..00000000000 --- a/news/7611-gh-actions--linters-adjustment.trivial +++ /dev/null @@ -1 +0,0 @@ -Test vendoring lint target under GitHub Actions CI/CD. diff --git a/news/7680.removal b/news/7680.removal deleted file mode 100644 index 7d582156f23..00000000000 --- a/news/7680.removal +++ /dev/null @@ -1 +0,0 @@ -Remove unused ``tests/scripts/test_all_pip.py`` test script and the ``tests/scripts`` folder. diff --git a/news/7690.vendor b/news/7690.vendor deleted file mode 100644 index e6e79b1ecc2..00000000000 --- a/news/7690.vendor +++ /dev/null @@ -1 +0,0 @@ -Update semi-supported debundling script to reflect that appdirs is vendored. diff --git a/news/7699.bugfix b/news/7699.bugfix deleted file mode 100644 index 51dbef88fda..00000000000 --- a/news/7699.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -Use better mechanism for handling temporary files, when recording metadata -about installed files (RECORD) and the installer (INSTALLER). diff --git a/news/7718.bugfix b/news/7718.bugfix deleted file mode 100644 index 5abcce69a83..00000000000 --- a/news/7718.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -Correctly detect global site-packages availability of virtual environments -created by PyPA’s virtualenv>=20.0. diff --git a/news/7729.feature b/news/7729.feature deleted file mode 100644 index 6d050fe1414..00000000000 --- a/news/7729.feature +++ /dev/null @@ -1 +0,0 @@ -Significantly improve performance when ``--find-links`` points to a very large HTML page. diff --git a/news/7731.bugfix b/news/7731.bugfix deleted file mode 100644 index 7e9e17554e6..00000000000 --- a/news/7731.bugfix +++ /dev/null @@ -1 +0,0 @@ -Remove current directory from ``sys.path`` when invoked as ``python -m pip <command>`` diff --git a/news/7737.trivial b/news/7737.trivial deleted file mode 100644 index 64b3cc2260c..00000000000 --- a/news/7737.trivial +++ /dev/null @@ -1 +0,0 @@ -Added ``__repr__`` for ``Configuration`` to make debugging a bit easier. diff --git a/news/7740.trivial b/news/7740.trivial deleted file mode 100644 index cb11d514991..00000000000 --- a/news/7740.trivial +++ /dev/null @@ -1 +0,0 @@ -Use PEP 517 layout by specifying ``build-backend``, so that ``pip`` can be built with tools (such as ``pep517``) that don't support the legacy layout. diff --git a/news/7767.doc b/news/7767.doc deleted file mode 100644 index 0803a314974..00000000000 --- a/news/7767.doc +++ /dev/null @@ -1 +0,0 @@ -Add a "Copyright" page. diff --git a/news/7768.feature b/news/7768.feature deleted file mode 100644 index 82e600b3dc0..00000000000 --- a/news/7768.feature +++ /dev/null @@ -1 +0,0 @@ -Indicate when wheel building is skipped, due to lack of the ``wheel`` package. diff --git a/news/7794.trivial b/news/7794.trivial deleted file mode 100644 index f4a5a1d4275..00000000000 --- a/news/7794.trivial +++ /dev/null @@ -1 +0,0 @@ -Print vendored libraries version in pip debug. diff --git a/news/7803.doc b/news/7803.doc deleted file mode 100644 index bf86b8976ab..00000000000 --- a/news/7803.doc +++ /dev/null @@ -1 +0,0 @@ -Added example of defining multiple values for options which support them diff --git a/news/7847.feature b/news/7847.feature deleted file mode 100644 index 8f1a69b6fd2..00000000000 --- a/news/7847.feature +++ /dev/null @@ -1 +0,0 @@ -Change default behaviour to always cache responses from trusted-host source. diff --git a/news/7850.vendor b/news/7850.vendor deleted file mode 100644 index b9b5f22e002..00000000000 --- a/news/7850.vendor +++ /dev/null @@ -1 +0,0 @@ -Add ResolveLib as a vendored dependency. diff --git a/news/7856.bugfix b/news/7856.bugfix deleted file mode 100644 index 50bf6810dd7..00000000000 --- a/news/7856.bugfix +++ /dev/null @@ -1 +0,0 @@ -Stop failing uninstallation, when trying to remove non-existent files. diff --git a/news/7872.bugfix b/news/7872.bugfix deleted file mode 100644 index 3550d573b88..00000000000 --- a/news/7872.bugfix +++ /dev/null @@ -1 +0,0 @@ -Prevent an infinite recursion with ``pip wheel`` when ``$TMPDIR`` is within the source directory. diff --git a/news/7959.trivial b/news/7959.trivial deleted file mode 100644 index dffd57a83ff..00000000000 --- a/news/7959.trivial +++ /dev/null @@ -1,17 +0,0 @@ -Removes shebang from nonexecutable script. - -When packaging pip in Fedora, we have realised -that there is a nonexecutable file with a shebang line. - -It seems that the primary purpose of this file is to be imported from Python -code and hence the shebang appears to be unnecessary. - -Shebangs are hard to handle when doing downstream packaging because it makes -sense for upstream to use ``#!/usr/bin/env python`` while in the RPM package, we -need to avoid that and use a more specific interpreter. Since the shebang was -unused, I propose to remove it to avoid the problems. - -We have found more shebangs but in vendored packages. I have also opened PRs there: -https://github.com/ActiveState/appdirs/pull/144 -https://github.com/psf/requests/pull/5410 -https://github.com/chardet/chardet/pull/192 diff --git a/news/7962.bugfix b/news/7962.bugfix deleted file mode 100644 index 6ee2cc55c5b..00000000000 --- a/news/7962.bugfix +++ /dev/null @@ -1 +0,0 @@ -Significantly speedup ``pip list --outdated`` by parallelizing index interaction. diff --git a/news/7a174d2e-1647-4794-b1d0-58f32da01540.trivial b/news/7a174d2e-1647-4794-b1d0-58f32da01540.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/8013.bugfix b/news/8013.bugfix deleted file mode 100644 index 767b749e0c8..00000000000 --- a/news/8013.bugfix +++ /dev/null @@ -1 +0,0 @@ -Improve Windows compatibility when detecting writability in folder. diff --git a/news/988.feature b/news/988.feature deleted file mode 100644 index 80f162d5681..00000000000 --- a/news/988.feature +++ /dev/null @@ -1 +0,0 @@ -An alpha version of a new resolver is available via ``--unstable-feature=resolver``. diff --git a/news/9E2A89E1-F932-4159-9E69-8AEA7DFD6432.trivial b/news/9E2A89E1-F932-4159-9E69-8AEA7DFD6432.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/b397ed3a-49da-4279-b71a-7b67de91c34a.trivial b/news/b397ed3a-49da-4279-b71a-7b67de91c34a.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/c11aae95-1bc6-4a32-b005-65d0a7843207.trivial b/news/c11aae95-1bc6-4a32-b005-65d0a7843207.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/c9a56cb3-00b1-4ccb-805a-ac4807c72a52.trivial b/news/c9a56cb3-00b1-4ccb-805a-ac4807c72a52.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/certifi.vendor b/news/certifi.vendor deleted file mode 100644 index b53a40e989e..00000000000 --- a/news/certifi.vendor +++ /dev/null @@ -1 +0,0 @@ -Upgrade certifi to 2020.04.05.1 diff --git a/news/contextlib2.vendor b/news/contextlib2.vendor deleted file mode 100644 index e8fe38f8741..00000000000 --- a/news/contextlib2.vendor +++ /dev/null @@ -1 +0,0 @@ -Upgrade contextlib2 to 0.6.0.post1 diff --git a/news/distro.vendor b/news/distro.vendor deleted file mode 100644 index b4ca8ddf856..00000000000 --- a/news/distro.vendor +++ /dev/null @@ -1 +0,0 @@ -Upgrade distro to 1.5.0. diff --git a/news/e5e07991-9123-41bb-9571-7efbe141a93e.trivial b/news/e5e07991-9123-41bb-9571-7efbe141a93e.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/f2a93c6a-3d20-4662-b510-0a11603837b1.trivial b/news/f2a93c6a-3d20-4662-b510-0a11603837b1.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/idna.vendor b/news/idna.vendor deleted file mode 100644 index 2029253f5fd..00000000000 --- a/news/idna.vendor +++ /dev/null @@ -1 +0,0 @@ -Upgrade idna to 2.9. diff --git a/news/msgpack.vendor b/news/msgpack.vendor deleted file mode 100644 index 0c6c3991ac2..00000000000 --- a/news/msgpack.vendor +++ /dev/null @@ -1 +0,0 @@ -Upgrade msgpack to 1.0.0. diff --git a/news/packaging.vendor b/news/packaging.vendor deleted file mode 100644 index f158735cf68..00000000000 --- a/news/packaging.vendor +++ /dev/null @@ -1 +0,0 @@ -Upgrade packaging to 20.3. diff --git a/news/pep517.vendor b/news/pep517.vendor deleted file mode 100644 index 688e49ce8b9..00000000000 --- a/news/pep517.vendor +++ /dev/null @@ -1 +0,0 @@ -Upgrade pep517 to 0.8.2. diff --git a/news/pyparsing.vendor b/news/pyparsing.vendor deleted file mode 100644 index f892eb01359..00000000000 --- a/news/pyparsing.vendor +++ /dev/null @@ -1 +0,0 @@ -Upgrade pyparsing to 2.4.7. diff --git a/news/pytoml.vendor b/news/pytoml.vendor deleted file mode 100644 index 4f7e9b335a7..00000000000 --- a/news/pytoml.vendor +++ /dev/null @@ -1 +0,0 @@ -Remove pytoml as a vendored dependency. diff --git a/news/requests.vendor b/news/requests.vendor deleted file mode 100644 index b39c46159e7..00000000000 --- a/news/requests.vendor +++ /dev/null @@ -1 +0,0 @@ -Upgrade requests to 2.23.0. diff --git a/news/toml.vendor b/news/toml.vendor deleted file mode 100644 index ead9e5c9a29..00000000000 --- a/news/toml.vendor +++ /dev/null @@ -1 +0,0 @@ -Add toml as a vendored dependency. diff --git a/news/urllib3.vendor b/news/urllib3.vendor deleted file mode 100644 index 2e4c40342d7..00000000000 --- a/news/urllib3.vendor +++ /dev/null @@ -1 +0,0 @@ -Upgrade urllib3 to 1.25.8. diff --git a/src/pip/__init__.py b/src/pip/__init__.py index a41767bd904..037fe7422d5 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -4,7 +4,7 @@ from typing import List, Optional -__version__ = "20.1.dev0" +__version__ = "20.1b1" def main(args=None): From 4502aba7992926fda66f57d24ed33026f1fd648b Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 21 Apr 2020 00:31:45 +0530 Subject: [PATCH 1675/3170] Bump for development --- src/pip/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/__init__.py b/src/pip/__init__.py index 037fe7422d5..5e02decc479 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -4,7 +4,7 @@ from typing import List, Optional -__version__ = "20.1b1" +__version__ = "20.1.dev1" def main(args=None): From 4d42b897136fea5dd9ff475d19a8d15379347683 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 21 Apr 2020 07:49:09 +0530 Subject: [PATCH 1676/3170] Fix copying distribution files from checkout --- noxfile.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/noxfile.py b/noxfile.py index 8c73bcb0f9c..f2a959ca601 100644 --- a/noxfile.py +++ b/noxfile.py @@ -221,10 +221,10 @@ def build_release(session): tmp_dist_paths = (build_dir / p for p in tmp_dists) session.log(f"# Copying dists from {build_dir}") - shutil.rmtree('dist', ignore_errors=True) # remove empty `dist/` - for dist in tmp_dist_paths: - session.log(f"# Copying {dist}") - shutil.copy(dist, 'dist') + os.makedirs('dist', exist_ok=True) + for dist, final in zip(tmp_dist_paths, tmp_dists): + session.log(f"# Copying {dist} to {final}") + shutil.copy(dist, final) def build_dists(session): From c46e16794de06053164fbd4335da4972c4fb80e5 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 21 Apr 2020 08:22:47 +0530 Subject: [PATCH 1677/3170] Include month and year for pip 21.0 Co-Authored-By: Hugo van Kemenade <hugovk@users.noreply.github.com> --- src/pip/_internal/cli/base_command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index 03c3774a656..50aeb29c985 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -141,7 +141,7 @@ def _main(self, args): not options.no_python_version_warning ): message = ( - "pip 21.0 will drop support for Python 2.7. " + "pip 21.0 will drop support for Python 2.7 in January 2021. " "More details about Python 2 support in pip, can be found at " "https://pip.pypa.io/en/latest/development/release-process/#python-2-support" # noqa ) From b56de93bc9a648edb4011dd473784be18e7416f4 Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara <sh@changeset.nyc> Date: Tue, 21 Apr 2020 11:06:03 -0400 Subject: [PATCH 1678/3170] Add resolver testing doc link Signed-off-by: Sumana Harihareswara <sh@changeset.nyc> --- docs/html/development/contributing.rst | 4 ++++ news/8106-fdsadh-34-sdfsa-35dd.trivial | 0 2 files changed, 4 insertions(+) create mode 100644 news/8106-fdsadh-34-sdfsa-35dd.trivial diff --git a/docs/html/development/contributing.rst b/docs/html/development/contributing.rst index 582f7f742ff..f031ba072ca 100644 --- a/docs/html/development/contributing.rst +++ b/docs/html/development/contributing.rst @@ -50,6 +50,9 @@ To trigger CI to run again for a pull request, you can close and open the pull request or submit another change to the pull request. If needed, project maintainers can manually trigger a restart of a job/build. +To understand the broader software architecture around dependency +resolution in pip, and how we automatically test this functionality, +see `Testing the next-gen pip dependency resolver`_. NEWS Entries ============ @@ -255,3 +258,4 @@ and they will initiate a vote among the existing maintainers. .. _`.travis.yml`: https://github.com/pypa/pip/blob/master/.travis.yml .. _`.appveyor.yml`: https://github.com/pypa/pip/blob/master/.appveyor.yml .. _`towncrier`: https://pypi.org/project/towncrier/ +.. _`Testing the next-gen pip dependency resolver`: https://pradyunsg.me/blog/2020/03/27/pip-resolver-testing/ diff --git a/news/8106-fdsadh-34-sdfsa-35dd.trivial b/news/8106-fdsadh-34-sdfsa-35dd.trivial new file mode 100644 index 00000000000..e69de29bb2d From 2cdde769540e85d59d15e054d73d4817e163db87 Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara <sh@changeset.nyc> Date: Tue, 21 Apr 2020 11:18:59 -0400 Subject: [PATCH 1679/3170] Slightly polish contributor documentation Signed-off-by: Sumana Harihareswara <sh@changeset.nyc> --- docs/html/development/architecture/index.rst | 8 +++-- docs/html/development/ci.rst | 6 ++-- docs/html/development/contributing.rst | 38 +++++++++++--------- docs/html/development/issue-triage.rst | 7 ++-- news/fhasjhf-343-gdg--11sfdd.trivial | 0 5 files changed, 36 insertions(+), 23 deletions(-) create mode 100644 news/fhasjhf-343-gdg--11sfdd.trivial diff --git a/docs/html/development/architecture/index.rst b/docs/html/development/architecture/index.rst index 9c1eaa7cf57..417edf4e9e9 100644 --- a/docs/html/development/architecture/index.rst +++ b/docs/html/development/architecture/index.rst @@ -1,3 +1,5 @@ +.. _architecture-pip-internals: + =============================== Architecture of pip's internals =============================== @@ -5,8 +7,10 @@ Architecture of pip's internals .. note:: This section of the documentation is currently being written. pip - developers welcome your help to complete this documentation. If you're - interested in helping out, please let us know in the `tracking issue`_. + developers welcome your help to complete this documentation. If + you're interested in helping out, please let us know in the + `tracking issue`_, or just go ahead and submit a pull request and + mention it in that tracking issue. .. note:: diff --git a/docs/html/development/ci.rst b/docs/html/development/ci.rst index dbd80eb6cef..8b6412307a9 100644 --- a/docs/html/development/ci.rst +++ b/docs/html/development/ci.rst @@ -1,8 +1,10 @@ .. note:: This section of the documentation is currently being written. pip - developers welcome your help to complete this documentation. If you're - interested in helping out, please let us know in the `tracking issue`_. + developers welcome your help to complete this documentation. If + you're interested in helping out, please let us know in the + `tracking issue`_, or just submit a pull request and mention it in + that tracking issue. .. _`tracking issue`: https://github.com/pypa/pip/issues/7279 diff --git a/docs/html/development/contributing.rst b/docs/html/development/contributing.rst index 582f7f742ff..a0587e8363e 100644 --- a/docs/html/development/contributing.rst +++ b/docs/html/development/contributing.rst @@ -2,10 +2,11 @@ Contributing ============ -.. todo - Create a "guide" to pip's internals and link to it from here saying - "you might want to take a look at the guide" +Pip's internals +=============== +We have an in-progress guide to the +:ref:`architecture-pip-internals`. It might be helpful as you dive in. Submitting Pull Requests ======================== @@ -20,7 +21,7 @@ Provide tests that cover your changes and run the tests locally first. pip operating systems. Any pull request must consider and work on all these platforms. -Pull Requests should be small to facilitate easier review. Keep them +Pull requests should be small to facilitate easier review. Keep them self-contained, and limited in scope. `Studies have shown`_ that review quality falls off as patch size grows. Sometimes this will result in many small PRs to land a single large feature. In particular, pull requests must not be treated @@ -30,8 +31,8 @@ can be reviewed and merged individually. Additionally, avoid including "cosmetic" changes to code that is unrelated to your change, as these make reviewing the PR more difficult. -Examples include re-flowing text in comments or documentation, or addition or -removal of blank lines or whitespace within lines. Such changes can be made +Examples include re-flowing text in comments or documentation, or adding or +removing blank lines or whitespace within lines. Such changes can be made separately, as a "formatting cleanup" PR, if needed. @@ -41,7 +42,7 @@ Automated Testing All pull requests and merges to 'master' branch are tested using `Travis CI`_ and `Appveyor CI`_ based on our `.travis.yml`_ and `.appveyor.yml`_ files. -You can find the status and results to the CI runs for your PR on GitHub's Web +You can find the status and results to the CI runs for your PR on GitHub's web UI for the pull request. You can also find links to the CI services' pages for the specific builds in the form of "Details" links, in case the CI run fails and you wish to view the output. @@ -76,10 +77,11 @@ deduplicate them. Contents of a NEWS entry ------------------------ -The contents of this file are reStructuredText formatted text that will be used -as the content of the news file entry. You do not need to reference the issue -or PR numbers here as towncrier will automatically add a reference to all of -the affected issues when rendering the news file. +The contents of this file are reStructuredText formatted text that +will be used as the content of the news file entry. You do not need to +reference the issue or PR numbers in the entry, since ``towncrier`` +will automatically add a reference to all of the affected issues when +rendering the NEWS file. In order to maintain a consistent style in the ``NEWS.rst`` file, it is preferred to keep the news entry to the point, in sentence case, shorter than @@ -223,19 +225,23 @@ Try force-pushing your branch with ``push -f``. The ``master`` branch in the main pip repository gets updated frequently, so you might have to update your branch at least once while you are working on it. +Thank you for your contribution! + Becoming a maintainer ===================== If you want to become an official maintainer, start by helping out. -As a first step, we welcome you to triage issues on pip's issue tracker. pip -maintainers provide triage abilities to contributors once they have been around -for some time and contributed positively to the project. This is optional and highly +As a first step, we welcome you to triage issues on pip's issue +tracker. pip maintainers provide triage abilities to contributors once +they have been around for some time (probably at least 2-3 months) and +contributed positively to the project. This is optional and highly recommended for becoming a pip maintainer. -Later, when you think you're ready, get in touch with one of the maintainers -and they will initiate a vote among the existing maintainers. +Later, when you think you're ready (probably at least 5 months after +starting to triage), get in touch with one of the maintainers and they +will initiate a vote among the existing maintainers. .. note:: diff --git a/docs/html/development/issue-triage.rst b/docs/html/development/issue-triage.rst index edb86e7ee41..a887bda62a8 100644 --- a/docs/html/development/issue-triage.rst +++ b/docs/html/development/issue-triage.rst @@ -1,9 +1,10 @@ .. note:: This section of the documentation is currently being written. pip - developers welcome your help to complete this documentation. If you're - interested in helping out, please let us know in the - `tracking issue <https://github.com/pypa/pip/issues/6583>`__. + developers welcome your help to complete this documentation. If + you're interested in helping out, please let us know in the + `tracking issue <https://github.com/pypa/pip/issues/6583>`__, or + just submit a pull request and mention it in that tracking issue. ============ Issue Triage diff --git a/news/fhasjhf-343-gdg--11sfdd.trivial b/news/fhasjhf-343-gdg--11sfdd.trivial new file mode 100644 index 00000000000..e69de29bb2d From 7fb9a9dbc546142c768d8c0a036efa94014dd68f Mon Sep 17 00:00:00 2001 From: Stefano Rivera <stefano@rivera.za.net> Date: Tue, 21 Apr 2020 09:05:43 -0700 Subject: [PATCH 1680/3170] Mark certifi as bundled, it's used in the debug command --- news/7fc6ae31-6e5d-4f67-9b6b-3e7d4934d775.trivial | 0 src/pip/_vendor/__init__.py | 1 + 2 files changed, 1 insertion(+) create mode 100644 news/7fc6ae31-6e5d-4f67-9b6b-3e7d4934d775.trivial diff --git a/news/7fc6ae31-6e5d-4f67-9b6b-3e7d4934d775.trivial b/news/7fc6ae31-6e5d-4f67-9b6b-3e7d4934d775.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_vendor/__init__.py b/src/pip/_vendor/__init__.py index fde93d2e57a..c3db83ff6aa 100644 --- a/src/pip/_vendor/__init__.py +++ b/src/pip/_vendor/__init__.py @@ -60,6 +60,7 @@ def vendored(modulename): # Actually alias all of our vendored dependencies. vendored("appdirs") vendored("cachecontrol") + vendored("certifi") vendored("colorama") vendored("contextlib2") vendored("distlib") From e84699fe0e0cfe157cc953c474e98ded142e05b5 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Thu, 23 Apr 2020 00:33:28 +0530 Subject: [PATCH 1681/3170] Remove appveyor CI references from docs --- docs/html/development/architecture/anatomy.rst | 1 - docs/html/development/contributing.rst | 4 +--- news/579B649E-EE91-4EA2-9860-4D13F792959F.trivial | 0 3 files changed, 1 insertion(+), 4 deletions(-) create mode 100644 news/579B649E-EE91-4EA2-9860-4D13F792959F.trivial diff --git a/docs/html/development/architecture/anatomy.rst b/docs/html/development/architecture/anatomy.rst index 769d2b4796f..4b117bafe42 100644 --- a/docs/html/development/architecture/anatomy.rst +++ b/docs/html/development/architecture/anatomy.rst @@ -25,7 +25,6 @@ The ``README``, license, ``pyproject.toml``, ``setup.py``, and so on are in the * ``setup.cfg`` * ``setup.py`` * ``tox.ini`` -- ``pip`` uses Tox, an automation tool, configured by this `tox.ini`_ file. ``tox.ini`` describes a few environments ``pip`` uses during development for simplifying how tests are run (complicated situation there). Example: ``tox -e -py36``. We can run tests for different versions of Python by changing “36” to “27” or similar. -* ``.appveyor.yml`` * ``.coveragerc`` * ``.gitattributes`` * ``.gitignore`` diff --git a/docs/html/development/contributing.rst b/docs/html/development/contributing.rst index a0587e8363e..8fc2d68e11e 100644 --- a/docs/html/development/contributing.rst +++ b/docs/html/development/contributing.rst @@ -40,7 +40,7 @@ Automated Testing ================= All pull requests and merges to 'master' branch are tested using `Travis CI`_ -and `Appveyor CI`_ based on our `.travis.yml`_ and `.appveyor.yml`_ files. +based on our `.travis.yml`_ file. You can find the status and results to the CI runs for your PR on GitHub's web UI for the pull request. You can also find links to the CI services' pages for @@ -257,7 +257,5 @@ will initiate a vote among the existing maintainers. .. _`Studies have shown`: https://www.kessler.de/prd/smartbear/BestPracticesForPeerCodeReview.pdf .. _`resolve merge conflicts`: https://help.github.com/articles/resolving-a-merge-conflict-using-the-command-line .. _`Travis CI`: https://travis-ci.org/ -.. _`Appveyor CI`: https://www.appveyor.com/ .. _`.travis.yml`: https://github.com/pypa/pip/blob/master/.travis.yml -.. _`.appveyor.yml`: https://github.com/pypa/pip/blob/master/.appveyor.yml .. _`towncrier`: https://pypi.org/project/towncrier/ diff --git a/news/579B649E-EE91-4EA2-9860-4D13F792959F.trivial b/news/579B649E-EE91-4EA2-9860-4D13F792959F.trivial new file mode 100644 index 00000000000..e69de29bb2d From 2daedca9c4066e7ceef84ef1691fc94856f8f6d0 Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Wed, 22 Apr 2020 15:20:59 -0500 Subject: [PATCH 1682/3170] add new_resolver option to handle_request() --- tests/functional/test_yaml.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/functional/test_yaml.py b/tests/functional/test_yaml.py index 199aa919ae9..2579179c692 100644 --- a/tests/functional/test_yaml.py +++ b/tests/functional/test_yaml.py @@ -92,13 +92,16 @@ def stripping_split(my_str, splitwith, count=None): return retval -def handle_request(script, action, requirement, options): +def handle_request(script, action, requirement, options, new_resolver=False): assert isinstance(requirement, str), ( "Need install requirement to be a string only" ) if action == 'install': - args = ['install', "--no-index", "--find-links", - path_to_url(script.scratch_path)] + args = ['install'] + if new_resolver: + args.append("--unstable-feature=resolver") + args.extend(["--no-index", "--find-links", + path_to_url(script.scratch_path)]) elif action == 'uninstall': args = ['uninstall', '--yes'] else: From 07845f8c973c8238db22983d612b05d2fd17c529 Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Wed, 22 Apr 2020 15:23:35 -0500 Subject: [PATCH 1683/3170] move yaml test files up one level --- tests/functional/test_yaml.py | 2 +- tests/yaml/{install => }/circular.yml | 0 tests/yaml/{install => }/conflicting_diamond.yml | 0 tests/yaml/{install => }/conflicting_triangle.yml | 0 tests/yaml/{install => }/extras.yml | 0 tests/yaml/{install => }/non_pinned.yml | 0 tests/yaml/{install => }/pinned.yml | 0 tests/yaml/{install => }/simple.yml | 0 8 files changed, 1 insertion(+), 1 deletion(-) rename tests/yaml/{install => }/circular.yml (100%) rename tests/yaml/{install => }/conflicting_diamond.yml (100%) rename tests/yaml/{install => }/conflicting_triangle.yml (100%) rename tests/yaml/{install => }/extras.yml (100%) rename tests/yaml/{install => }/non_pinned.yml (100%) rename tests/yaml/{install => }/pinned.yml (100%) rename tests/yaml/{install => }/simple.yml (100%) diff --git a/tests/functional/test_yaml.py b/tests/functional/test_yaml.py index 2579179c692..10fe49e627d 100644 --- a/tests/functional/test_yaml.py +++ b/tests/functional/test_yaml.py @@ -29,7 +29,7 @@ def generate_yaml_tests(directory): """ Generate yaml test cases from the yaml files in the given directory """ - for yml_file in directory.glob("*/*.yml"): + for yml_file in directory.glob("*.yml"): data = yaml.safe_load(yml_file.read_text()) assert "cases" in data, "A fixture needs cases to be used in testing" diff --git a/tests/yaml/install/circular.yml b/tests/yaml/circular.yml similarity index 100% rename from tests/yaml/install/circular.yml rename to tests/yaml/circular.yml diff --git a/tests/yaml/install/conflicting_diamond.yml b/tests/yaml/conflicting_diamond.yml similarity index 100% rename from tests/yaml/install/conflicting_diamond.yml rename to tests/yaml/conflicting_diamond.yml diff --git a/tests/yaml/install/conflicting_triangle.yml b/tests/yaml/conflicting_triangle.yml similarity index 100% rename from tests/yaml/install/conflicting_triangle.yml rename to tests/yaml/conflicting_triangle.yml diff --git a/tests/yaml/install/extras.yml b/tests/yaml/extras.yml similarity index 100% rename from tests/yaml/install/extras.yml rename to tests/yaml/extras.yml diff --git a/tests/yaml/install/non_pinned.yml b/tests/yaml/non_pinned.yml similarity index 100% rename from tests/yaml/install/non_pinned.yml rename to tests/yaml/non_pinned.yml diff --git a/tests/yaml/install/pinned.yml b/tests/yaml/pinned.yml similarity index 100% rename from tests/yaml/install/pinned.yml rename to tests/yaml/pinned.yml diff --git a/tests/yaml/install/simple.yml b/tests/yaml/simple.yml similarity index 100% rename from tests/yaml/install/simple.yml rename to tests/yaml/simple.yml From 9ef15bc01266725a82e78e3d899774824cd182cb Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Wed, 22 Apr 2020 17:33:12 -0500 Subject: [PATCH 1684/3170] run tests with old and new resolver, and add ability to skip for either --- tests/functional/test_yaml.py | 27 +++++++++++++++++---------- tests/yaml/circular.yml | 4 ++++ 2 files changed, 21 insertions(+), 10 deletions(-) diff --git a/tests/functional/test_yaml.py b/tests/functional/test_yaml.py index 10fe49e627d..e794fcf27a3 100644 --- a/tests/functional/test_yaml.py +++ b/tests/functional/test_yaml.py @@ -40,18 +40,24 @@ def generate_yaml_tests(directory): base = data.get("base", {}) cases = data["cases"] - for i, case_template in enumerate(cases): - case = base.copy() - case.update(case_template) + for resolver in 'old', 'new': + for i, case_template in enumerate(cases): + case = base.copy() + case.update(case_template) - case[":name:"] = base_name - if len(cases) > 1: - case[":name:"] += "-" + str(i) + case[":name:"] = base_name + '-' + resolver + if len(cases) > 1: + case[":name:"] += "-" + str(i) + case[":resolver:"] = resolver - if case.pop("skip", False): - case = pytest.param(case, marks=pytest.mark.xfail) + skip = case.pop("skip", None) + assert skip in [None, True, 'old', 'new'], ( + "invalid value for skip: %r" % skip + ) + if skip == True or skip == resolver: + case = pytest.param(case, marks=pytest.mark.xfail) - yield case + yield case def id_func(param): @@ -187,7 +193,8 @@ def test_yaml_based(script, case): # Perform the requested action effect = handle_request(script, action, request[action], - request.get('options', '').split()) + request.get('options', '').split(), + case[':resolver:'] == 'new') assert effect['state'] == (response['state'] or []), \ str(effect["_result_object"]) diff --git a/tests/yaml/circular.yml b/tests/yaml/circular.yml index 95c535454fa..ba938992370 100644 --- a/tests/yaml/circular.yml +++ b/tests/yaml/circular.yml @@ -16,6 +16,7 @@ cases: - B 1.0.0 - C 1.0.0 - D 1.0.0 + skip: new - request: - install: B @@ -25,6 +26,7 @@ cases: - B 1.0.0 - C 1.0.0 - D 1.0.0 + skip: new - request: - install: C @@ -34,6 +36,7 @@ cases: - B 1.0.0 - C 1.0.0 - D 1.0.0 + skip: new - request: - install: D @@ -43,3 +46,4 @@ cases: - B 1.0.0 - C 1.0.0 - D 1.0.0 + skip: new From 5af919c4753d5ec4d91d5e27dfc285c452c1861e Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Wed, 22 Apr 2020 17:45:08 -0500 Subject: [PATCH 1685/3170] yaml test extras-2 passws with new resolver --- tests/yaml/extras.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/yaml/extras.yml b/tests/yaml/extras.yml index 7b39003a2eb..6e2a1b17e7b 100644 --- a/tests/yaml/extras.yml +++ b/tests/yaml/extras.yml @@ -39,4 +39,4 @@ cases: - D 1.0.0 - E 1.0.0 - F 1.0.0 - skip: true + skip: old From 05babbcb68ebb334b60740fc715f71d621ccc623 Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Wed, 22 Apr 2020 17:50:01 -0500 Subject: [PATCH 1686/3170] improve test name --- tests/functional/test_yaml.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/functional/test_yaml.py b/tests/functional/test_yaml.py index e794fcf27a3..232ed7c0911 100644 --- a/tests/functional/test_yaml.py +++ b/tests/functional/test_yaml.py @@ -45,9 +45,10 @@ def generate_yaml_tests(directory): case = base.copy() case.update(case_template) - case[":name:"] = base_name + '-' + resolver + case[":name:"] = base_name if len(cases) > 1: case[":name:"] += "-" + str(i) + case[":name:"] += "*" + resolver case[":resolver:"] = resolver skip = case.pop("skip", None) From 1d1b7a674e67f88d2c798e761ccec26eb530ed82 Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Wed, 22 Apr 2020 17:57:20 -0500 Subject: [PATCH 1687/3170] simplify assertion for valid skip values --- tests/functional/test_yaml.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/functional/test_yaml.py b/tests/functional/test_yaml.py index 232ed7c0911..5018a90ae4e 100644 --- a/tests/functional/test_yaml.py +++ b/tests/functional/test_yaml.py @@ -51,10 +51,8 @@ def generate_yaml_tests(directory): case[":name:"] += "*" + resolver case[":resolver:"] = resolver - skip = case.pop("skip", None) - assert skip in [None, True, 'old', 'new'], ( - "invalid value for skip: %r" % skip - ) + skip = case.pop("skip", False) + assert skip in [False, True, 'old', 'new'] if skip == True or skip == resolver: case = pytest.param(case, marks=pytest.mark.xfail) From 8780a80c4de74f273eb58a5c3ae7080c5b67a58d Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Wed, 22 Apr 2020 18:23:24 -0500 Subject: [PATCH 1688/3170] make flake8 happy --- tests/functional/test_yaml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/test_yaml.py b/tests/functional/test_yaml.py index 5018a90ae4e..5f5e1a8c61f 100644 --- a/tests/functional/test_yaml.py +++ b/tests/functional/test_yaml.py @@ -53,7 +53,7 @@ def generate_yaml_tests(directory): skip = case.pop("skip", False) assert skip in [False, True, 'old', 'new'] - if skip == True or skip == resolver: + if skip is True or skip == resolver: case = pytest.param(case, marks=pytest.mark.xfail) yield case From 3fe1eab593d2f2ef5125570c297e14b96ad7d709 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 23 Apr 2020 07:02:53 +0530 Subject: [PATCH 1689/3170] Reword get_installation_order docstring --- src/pip/_internal/resolution/resolvelib/resolver.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index cba5a496508..199b1d756c3 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -114,11 +114,11 @@ def resolve(self, root_reqs, check_supported_wheels): def get_installation_order(self, req_set): # type: (RequirementSet) -> List[InstallRequirement] - """Create a list that orders given requirements for installation. + """Get order for installation of requirements in RequirementSet. - The returned list should contain all requirements in ``req_set``, - so the caller can loop through it and have a requirement installed - before the requiring thing. + The returned list contains a requirement before another that depends on + it. This helps ensure that the environment is kept consistent as they + get installed one-by-one. The current implementation walks the resolved dependency graph, and make sure every node has a greater "weight" than all its parents. From f927f9ae39160bbb4252bbdb73124704aeb43cf0 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 23 Apr 2020 07:04:28 +0530 Subject: [PATCH 1690/3170] Drop code for nicer diffs later --- src/pip/_internal/resolution/resolvelib/resolver.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 199b1d756c3..e3640e142b7 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -151,13 +151,6 @@ def get_installation_order(self, req_set): "dependency." ) - sorted_items = sorted( - req_set.requirements.items(), - key=functools.partial(_req_set_item_sorter, weights=weights), - reverse=True, - ) - return [ireq for _, ireq in sorted_items] - def _req_set_item_sorter( item, # type: Tuple[str, InstallRequirement] From 3ba506fd537a1e2abf37baa2d91e7a898134002e Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Wed, 22 Apr 2020 23:05:51 -0500 Subject: [PATCH 1691/3170] update work on yaml readme --- tests/yaml/README.md | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/yaml/README.md b/tests/yaml/README.md index 559c7d8e24c..7ee9d5bcf7b 100644 --- a/tests/yaml/README.md +++ b/tests/yaml/README.md @@ -1,5 +1,10 @@ # Fixtures -This directory contains fixtures for testing pip's resolver. The fixtures are written as yml files, with a convenient format that allows for specifying a custom index for temporary use. +This directory contains fixtures for testing pip's resolver. +The fixtures are written as `.yml` files, with a convenient format +that allows for specifying a custom index for temporary use. + +The `.yml` files are organized in the following way. A `base` section +which ... <!-- TODO: Add a good description of the format and how it can be used. --> From 08aecfb41c41d867a1650c793e04efada9a1746e Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Thu, 23 Apr 2020 22:40:53 -0500 Subject: [PATCH 1692/3170] add test for https://github.com/python-poetry/poetry/issues/2298 --- tests/yaml/poetry2298.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 tests/yaml/poetry2298.yml diff --git a/tests/yaml/poetry2298.yml b/tests/yaml/poetry2298.yml new file mode 100644 index 00000000000..992f3a02ef0 --- /dev/null +++ b/tests/yaml/poetry2298.yml @@ -0,0 +1,24 @@ +# see: https://github.com/python-poetry/poetry/issues/2298 +base: + available: + - poetry 1.0.5; depends zappa == 0.51.0, sphinx == 3.0.1 + - zappa 0.51.0; depends boto3 + - sphinx 3.0.1; depends docutils + - boto3 1.4.5; depends botocore ~=1.5.0 + - botocore 1.5.92; depends docutils <0.16 + - docutils 0.16.0 + - docutils 0.15.0 + +cases: +- + request: + - install: poetry + response: + - state: + - boto3 1.4.5 + - botocore 1.5.92 + - docutils 0.15.0 + - poetry 1.0.5 + - sphinx 3.0.1 + - zappa 0.51.0 + skip: old From e212f6657502376fcc8927b5d82a8efd12252691 Mon Sep 17 00:00:00 2001 From: Noah Gorny <noah.bar.ilan@gmail.com> Date: Fri, 24 Apr 2020 12:05:19 +0300 Subject: [PATCH 1693/3170] commands: cache: Abort early if cache is disabled --- news/8124.bugfix | 1 + src/pip/_internal/commands/cache.py | 5 +++++ 2 files changed, 6 insertions(+) create mode 100644 news/8124.bugfix diff --git a/news/8124.bugfix b/news/8124.bugfix new file mode 100644 index 00000000000..a859381e0d8 --- /dev/null +++ b/news/8124.bugfix @@ -0,0 +1 @@ +Abort pip cache commands early when cache is disabled. diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index 7e3f72e080b..3c345dfa0d1 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -48,6 +48,11 @@ def run(self, options, args): "purge": self.purge_cache, } + if not options.cache_dir: + logger.error("pip cache commands can not " + "function since cache is disabled.") + return ERROR + # Determine action if not args or args[0] not in handlers: logger.error("Need an action ({}) to perform.".format( From d605530b449b55664e21e24cdb9130570e17fa4e Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Fri, 24 Apr 2020 11:08:06 +0100 Subject: [PATCH 1694/3170] Implement upgrade strategies for the new resolver --- ...0153d5-ae85-4b80-80f7-1c46e7b566dc.trivial | 0 .../resolution/resolvelib/factory.py | 28 +++++- .../resolution/resolvelib/resolver.py | 3 +- tests/functional/test_new_resolver.py | 85 +++++++++++++++++++ 4 files changed, 113 insertions(+), 3 deletions(-) create mode 100644 news/e20153d5-ae85-4b80-80f7-1c46e7b566dc.trivial diff --git a/news/e20153d5-ae85-4b80-80f7-1c46e7b566dc.trivial b/news/e20153d5-ae85-4b80-80f7-1c46e7b566dc.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 23686f76ac2..e518af433e7 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -50,6 +50,7 @@ def __init__( force_reinstall, # type: bool ignore_installed, # type: bool ignore_requires_python, # type: bool + upgrade_strategy, # type: str py_version_info=None, # type: Optional[Tuple[int, ...]] ): # type: (...) -> None @@ -59,6 +60,9 @@ def __init__( self._make_install_req_from_spec = make_install_req self._force_reinstall = force_reinstall self._ignore_requires_python = ignore_requires_python + self._upgrade_strategy = upgrade_strategy + + self.root_reqs = set() # type: Set[str] self._link_candidate_cache = {} # type: Cache[LinkCandidate] self._editable_candidate_cache = {} # type: Cache[EditableCandidate] @@ -110,13 +114,23 @@ def _make_candidate_from_link( return ExtrasCandidate(base, extras) return base + def _eligible_for_upgrade(self, dist_name): + # type: (str) -> bool + if self._upgrade_strategy == "eager": + return True + elif self._upgrade_strategy == "only-if-needed": + return (dist_name in self.root_reqs) + return False + def iter_found_candidates(self, ireq, extras): # type: (InstallRequirement, Set[str]) -> Iterator[Candidate] name = canonicalize_name(ireq.req.name) if not self._force_reinstall: installed_dist = self._installed_dists.get(name) + can_upgrade = self._eligible_for_upgrade(name) else: installed_dist = None + can_upgrade = False found = self.finder.find_best_candidate( project_name=ireq.req.name, @@ -126,6 +140,12 @@ def iter_found_candidates(self, ireq, extras): for ican in found.iter_applicable(): if (installed_dist is not None and installed_dist.parsed_version == ican.version): + if can_upgrade: + yield self._make_candidate_from_dist( + dist=installed_dist, + extras=extras, + parent=ireq, + ) continue yield self._make_candidate_from_link( link=ican.link, @@ -138,6 +158,7 @@ def iter_found_candidates(self, ireq, extras): # Return installed distribution if it matches the specifier. This is # done last so the resolver will prefer it over downloading links. if (installed_dist is not None and + not can_upgrade and installed_dist.parsed_version in ireq.req.specifier): yield self._make_candidate_from_dist( dist=installed_dist, @@ -145,8 +166,11 @@ def iter_found_candidates(self, ireq, extras): parent=ireq, ) - def make_requirement_from_install_req(self, ireq): - # type: (InstallRequirement) -> Requirement + def make_requirement_from_install_req(self, ireq, root=False): + # type: (InstallRequirement, bool) -> Requirement + if root and ireq.name: + self.root_reqs.add(ireq.name) + if ireq.link: # TODO: Get name and version from ireq, if possible? # Specifically, this might be needed in "name @ URL" diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index cba5a496508..94cd75c9834 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -52,6 +52,7 @@ def __init__( force_reinstall=force_reinstall, ignore_installed=ignore_installed, ignore_requires_python=ignore_requires_python, + upgrade_strategy=upgrade_strategy, py_version_info=py_version_info, ) self.ignore_dependencies = ignore_dependencies @@ -72,7 +73,7 @@ def resolve(self, root_reqs, check_supported_wheels): resolver = RLResolver(provider, reporter) requirements = [ - self.factory.make_requirement_from_install_req(r) + self.factory.make_requirement_from_install_req(r, root=True) for r in root_reqs ] diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index f20041c24bc..d0730d20937 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -529,3 +529,88 @@ def test_new_resolver_handles_prerelease( *pip_args ) assert_installed(script, pkg=expected_version) + + +def test_new_resolver_upgrade_needs_option(script): + # Install pkg 1.0.0 + create_basic_wheel_for_package(script, "pkg", "1.0.0") + script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "pkg", + ) + + # Now release a new version + create_basic_wheel_for_package(script, "pkg", "2.0.0") + + # This should not upgrade because we don't specify --upgrade + result = script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "pkg", + ) + + assert "Requirement already satisfied" in result.stdout, str(result) + assert_installed(script, pkg="1.0.0") + + # This should upgrade + result = script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "--upgrade", + "pkg", + ) + + assert "Uninstalling pkg-1.0.0" in result.stdout, str(result) + assert "Successfully uninstalled pkg-1.0.0" in result.stdout, str(result) + assert script.site_packages / "pkg" in result.files_updated, ( + "pkg not upgraded" + ) + assert_installed(script, pkg="2.0.0") + + +def test_new_resolver_upgrade_strategy(script): + create_basic_wheel_for_package(script, "base", "1.0.0", depends=["dep"]) + create_basic_wheel_for_package(script, "dep", "1.0.0") + script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "base", + ) + + assert_installed(script, base="1.0.0") + assert_installed(script, dep="1.0.0") + + # Now release new versions + create_basic_wheel_for_package(script, "base", "2.0.0", depends=["dep"]) + create_basic_wheel_for_package(script, "dep", "2.0.0") + + script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "--upgrade", + "base", + ) + + # With upgrade strategy "only-if-needed" (the default), dep should not + # be upgraded. + assert_installed(script, base="2.0.0") + assert_installed(script, dep="1.0.0") + + create_basic_wheel_for_package(script, "base", "3.0.0", depends=["dep"]) + script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "--upgrade", "--upgrade-strategy=eager", + "base", + ) + + # With upgrade strategy "eager", dep should be upgraded. + assert_installed(script, base="3.0.0") + assert_installed(script, dep="2.0.0") From 8c28b8173a29da65a2820f9e45f5c5eb955f4614 Mon Sep 17 00:00:00 2001 From: Noah Gorny <noah.bar.ilan@gmail.com> Date: Fri, 24 Apr 2020 12:32:24 +0300 Subject: [PATCH 1695/3170] tests: commands: cache: Add no-cache-dir test case --- tests/functional/test_cache.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index a464ece7945..7fec4dd56c4 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -216,3 +216,15 @@ def test_cache_purge_too_many_args(script, wheel_cache_files): # Make sure nothing was deleted. for filename in wheel_cache_files: assert os.path.exists(filename) + + +@pytest.mark.parametrize("command", ["info", "list", "remove", "purge"]) +def test_cache_abort_when_no_cache_dir(script, command): + """Running any pip cache command when cache is disabled should + abort and log an informative error""" + result = script.pip('cache', command, '--no-cache-dir', + expect_error=True) + assert result.stdout == '' + + assert ('ERROR: pip cache commands can not function' + ' since cache is disabled.' in result.stderr.splitlines()) From df3b440f7b9a2cf5c739ed9ce98ce2f115ffb6db Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Fri, 24 Apr 2020 12:03:29 +0100 Subject: [PATCH 1696/3170] Fix the factory fixture to supply the new upgrade_strategy argument --- tests/unit/resolution_resolvelib/conftest.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/resolution_resolvelib/conftest.py b/tests/unit/resolution_resolvelib/conftest.py index f179aeade8c..11278fa8594 100644 --- a/tests/unit/resolution_resolvelib/conftest.py +++ b/tests/unit/resolution_resolvelib/conftest.py @@ -55,6 +55,7 @@ def factory(finder, preparer): force_reinstall=False, ignore_installed=False, ignore_requires_python=False, + upgrade_strategy="to-satisfy-only", py_version_info=None, ) From e03614fe6d90c890916c81561a594abf6c967e59 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 9 Apr 2020 17:33:54 +0800 Subject: [PATCH 1697/3170] Use req to populate candidate line instead of link This ensures a candidate built from a "found" link (from e.g. index) is categorized as specifier-based, not URL-based. This is important to avoid a specifier-based candidate to be 'pip freeze'-ed in the direct URL form due to PEP 610 (direct-url.json). --- .../_internal/resolution/resolvelib/candidates.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index f8461ade266..166b18a7fde 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -41,8 +41,12 @@ def make_install_req_from_link(link, parent): # type: (Link, InstallRequirement) -> InstallRequirement assert not parent.editable, "parent is editable" - return install_req_from_line( - link.url, + if parent.req: + line = str(parent.req) + else: + line = link.url + ireq = install_req_from_line( + line, comes_from=parent.comes_from, use_pep517=parent.use_pep517, isolated=parent.isolated, @@ -53,6 +57,10 @@ def make_install_req_from_link(link, parent): hashes=parent.hash_options ), ) + if ireq.link is None: + ireq.link = link + # TODO: Handle wheel cache resolution. + return ireq def make_install_req_from_editable(link, parent): From 746f47cc3ae8fc7e24b24e112fbc2ccb252aa182 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 23 Apr 2020 07:05:06 +0530 Subject: [PATCH 1698/3170] Factor out logic for getting weights --- .../resolution/resolvelib/resolver.py | 60 +++++++++++-------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index e3640e142b7..9925cbfac8f 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -18,6 +18,7 @@ from typing import Dict, List, Optional, Tuple from pip._vendor.resolvelib.resolvers import Result + from pip._vendor.resolvelib.structs import Graph from pip._internal.cache import WheelCache from pip._internal.index.package_finder import PackageFinder @@ -124,32 +125,43 @@ def get_installation_order(self, req_set): make sure every node has a greater "weight" than all its parents. """ assert self._result is not None, "must call resolve() first" - weights = {} # type: Dict[Optional[str], int] graph = self._result.graph - key_count = len(self._result.mapping) + 1 # Packages plus sentinal. - while len(weights) < key_count: - progressed = False - for key in graph: - if key in weights: - continue - parents = list(graph.iter_parents(key)) - if not all(p in weights for p in parents): - continue - if parents: - weight = max(weights[p] for p in parents) + 1 - else: - weight = 0 - weights[key] = weight - progressed = True - - # FIXME: This check will fail if there are unbreakable cycles. - # Implement something to forcifully break them up to continue. - if not progressed: - raise InstallationError( - "Could not determine installation order due to cicular " - "dependency." - ) + weights = get_topological_weights(graph) + + +def get_topological_weights(graph): + # type: (Graph) -> Dict[str, int] + """Assign weights to each node based on how "deep" they are. + """ + visited = set() # type: Set[str] + weights = {} + + key_count = len(graph) + while len(weights) < key_count: + progressed = False + for key in graph: + if key in weights: + continue + parents = list(graph.iter_parents(key)) + if not all(p in weights for p in parents): + continue + if parents: + weight = max(weights[p] for p in parents) + 1 + else: + weight = 0 + weights[key] = weight + progressed = True + + # FIXME: This check will fail if there are unbreakable cycles. + # Implement something to forcifully break them up to continue. + if not progressed: + raise InstallationError( + "Could not determine installation order due to cicular " + "dependency." + ) + + return weights def _req_set_item_sorter( From 04bf5715210cdfed3aed6cc8a5928e9176e7d95a Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Fri, 24 Apr 2020 14:27:24 +0100 Subject: [PATCH 1699/3170] Ensure root requirement name is canonicalised --- src/pip/_internal/resolution/resolvelib/factory.py | 2 +- tests/functional/test_new_resolver.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index e518af433e7..8dedaf28803 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -169,7 +169,7 @@ def iter_found_candidates(self, ireq, extras): def make_requirement_from_install_req(self, ireq, root=False): # type: (InstallRequirement, bool) -> Requirement if root and ireq.name: - self.root_reqs.add(ireq.name) + self.root_reqs.add(canonicalize_name(ireq.name)) if ireq.link: # TODO: Get name and version from ireq, if possible? diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index d0730d20937..029702f01b4 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -561,7 +561,7 @@ def test_new_resolver_upgrade_needs_option(script): "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "--upgrade", - "pkg", + "PKG", # Deliberately uppercase to check canonicalization ) assert "Uninstalling pkg-1.0.0" in result.stdout, str(result) From 7a54b2b3a8cf42b7f9803467d18cff705e0970e9 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Fri, 24 Apr 2020 14:29:34 +0100 Subject: [PATCH 1700/3170] Use is_direct rather than an explicit root parameter --- src/pip/_internal/resolution/resolvelib/factory.py | 6 +++--- src/pip/_internal/resolution/resolvelib/resolver.py | 2 +- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 8dedaf28803..22e7f26ed7c 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -166,9 +166,9 @@ def iter_found_candidates(self, ireq, extras): parent=ireq, ) - def make_requirement_from_install_req(self, ireq, root=False): - # type: (InstallRequirement, bool) -> Requirement - if root and ireq.name: + def make_requirement_from_install_req(self, ireq): + # type: (InstallRequirement) -> Requirement + if ireq.is_direct and ireq.name: self.root_reqs.add(canonicalize_name(ireq.name)) if ireq.link: diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 94cd75c9834..d9133e8f567 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -73,7 +73,7 @@ def resolve(self, root_reqs, check_supported_wheels): resolver = RLResolver(provider, reporter) requirements = [ - self.factory.make_requirement_from_install_req(r, root=True) + self.factory.make_requirement_from_install_req(r) for r in root_reqs ] From e6853336fe788404aef91ff61aba9804726876cd Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Fri, 24 Apr 2020 19:57:58 +0530 Subject: [PATCH 1701/3170] Revert "Drop code for nicer diffs later" This reverts commit f927f9ae39160bbb4252bbdb73124704aeb43cf0. --- src/pip/_internal/resolution/resolvelib/resolver.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 9925cbfac8f..df0ea0941fa 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -129,6 +129,13 @@ def get_installation_order(self, req_set): graph = self._result.graph weights = get_topological_weights(graph) + sorted_items = sorted( + req_set.requirements.items(), + key=functools.partial(_req_set_item_sorter, weights=weights), + reverse=True, + ) + return [ireq for _, ireq in sorted_items] + def get_topological_weights(graph): # type: (Graph) -> Dict[str, int] From 2a66ad1e2df5974240e606b5aa7f76641849d900 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Fri, 24 Apr 2020 16:44:02 +0100 Subject: [PATCH 1702/3170] Add an assertion to ensure we don't reuse the factory --- src/pip/_internal/resolution/resolvelib/resolver.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index d9133e8f567..7360e966f6b 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -77,6 +77,13 @@ def resolve(self, root_reqs, check_supported_wheels): for r in root_reqs ] + # The factory should not have retained state from any previous usage. + # In theory this could only happen if self was reused to do a second + # resolve, which isn't something we do at the moment. We assert here + # in order to catch the issue if that ever changes. + # The persistent state that we care about is `root_reqs`. + assert len(self.factory.root_reqs) == 0, "Factory is being re-used" + try: self._result = resolver.resolve(requirements) From bdf56a03033af603c09c57597f514f7bf7ee927a Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Fri, 24 Apr 2020 17:52:51 +0100 Subject: [PATCH 1703/3170] Fix really, really dumb mistake --- .../_internal/resolution/resolvelib/resolver.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 7360e966f6b..297feb7a365 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -61,6 +61,13 @@ def __init__( def resolve(self, root_reqs, check_supported_wheels): # type: (List[InstallRequirement], bool) -> RequirementSet + # The factory should not have retained state from any previous usage. + # In theory this could only happen if self was reused to do a second + # resolve, which isn't something we do at the moment. We assert here + # in order to catch the issue if that ever changes. + # The persistent state that we care about is `root_reqs`. + assert len(self.factory.root_reqs) == 0, "Factory is being re-used" + # FIXME: Implement constraints. if any(r.constraint for r in root_reqs): raise InstallationError("Constraints are not yet supported.") @@ -77,13 +84,6 @@ def resolve(self, root_reqs, check_supported_wheels): for r in root_reqs ] - # The factory should not have retained state from any previous usage. - # In theory this could only happen if self was reused to do a second - # resolve, which isn't something we do at the moment. We assert here - # in order to catch the issue if that ever changes. - # The persistent state that we care about is `root_reqs`. - assert len(self.factory.root_reqs) == 0, "Factory is being re-used" - try: self._result = resolver.resolve(requirements) From f35f37ef72cf00ad84d72b81e778d42456e769f1 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 24 Apr 2020 18:51:29 +0800 Subject: [PATCH 1704/3170] Add find_requirement() wrapper in legacy resolver --- src/pip/_internal/index/package_finder.py | 6 +- .../_internal/resolution/legacy/resolver.py | 12 +++- tests/unit/test_finder.py | 67 ++++++++++--------- 3 files changed, 47 insertions(+), 38 deletions(-) diff --git a/src/pip/_internal/index/package_finder.py b/src/pip/_internal/index/package_finder.py index e88ad9f5c69..98a6acaab5f 100644 --- a/src/pip/_internal/index/package_finder.py +++ b/src/pip/_internal/index/package_finder.py @@ -889,11 +889,11 @@ def find_best_candidate( return candidate_evaluator.compute_best_candidate(candidates) def find_requirement(self, req, upgrade): - # type: (InstallRequirement, bool) -> Optional[Link] + # type: (InstallRequirement, bool) -> Optional[InstallationCandidate] """Try to find a Link matching req Expects req, an InstallRequirement and upgrade, a boolean - Returns a Link if found, + Returns a InstallationCandidate if found, Raises DistributionNotFound or BestVersionAlreadyInstalled otherwise """ hashes = req.hashes(trust_internet=False) @@ -967,7 +967,7 @@ def _format_versions(cand_iter): best_candidate.version, _format_versions(best_candidate_result.iter_applicable()), ) - return best_candidate.link + return best_candidate def _find_name_version_sep(fragment, canonical_name): diff --git a/src/pip/_internal/resolution/legacy/resolver.py b/src/pip/_internal/resolution/legacy/resolver.py index cdb44d19dbe..3cf68bbd28a 100644 --- a/src/pip/_internal/resolution/legacy/resolver.py +++ b/src/pip/_internal/resolution/legacy/resolver.py @@ -46,6 +46,7 @@ from pip._internal.cache import WheelCache from pip._internal.distributions import AbstractDistribution from pip._internal.index.package_finder import PackageFinder + from pip._internal.models.link import Link from pip._internal.operations.prepare import RequirementPreparer from pip._internal.req.req_install import InstallRequirement from pip._internal.resolution.base import InstallRequirementProvider @@ -260,6 +261,14 @@ def _check_skip_installed(self, req_to_install): self._set_req_to_reinstall(req_to_install) return None + def _find_requirement_link(self, req): + # type: (InstallRequirement) -> Optional[Link] + upgrade = self._is_upgrade_allowed(req) + best_candidate = self.finder.find_requirement(req, upgrade) + if not best_candidate: + return None + return best_candidate.link + def _populate_link(self, req): # type: (InstallRequirement) -> None """Ensure that if a link can be found for this, that it is found. @@ -274,9 +283,8 @@ def _populate_link(self, req): mismatches. Furthermore, cached wheels at present have undeterministic contents due to file modification times. """ - upgrade = self._is_upgrade_allowed(req) if req.link is None: - req.link = self.finder.find_requirement(req, upgrade) + req.link = self._find_requirement_link(req) if self.wheel_cache is None or self.preparer.require_hashes: return diff --git a/tests/unit/test_finder.py b/tests/unit/test_finder.py index f8c143bc74f..853af723b5a 100644 --- a/tests/unit/test_finder.py +++ b/tests/unit/test_finder.py @@ -48,7 +48,7 @@ def test_no_mpkg(data): req = install_req_from_line("pkgwithmpkg") found = finder.find_requirement(req, False) - assert found.url.endswith("pkgwithmpkg-1.0.tar.gz"), found + assert found.link.url.endswith("pkgwithmpkg-1.0.tar.gz"), found def test_no_partial_name_match(data): @@ -57,7 +57,7 @@ def test_no_partial_name_match(data): req = install_req_from_line("gmpy") found = finder.find_requirement(req, False) - assert found.url.endswith("gmpy-1.15.tar.gz"), found + assert found.link.url.endswith("gmpy-1.15.tar.gz"), found def test_tilde(): @@ -79,23 +79,23 @@ def test_duplicates_sort_ok(data): req = install_req_from_line("duplicate") found = finder.find_requirement(req, False) - assert found.url.endswith("duplicate-1.0.tar.gz"), found + assert found.link.url.endswith("duplicate-1.0.tar.gz"), found def test_finder_detects_latest_find_links(data): """Test PackageFinder detects latest using find-links""" req = install_req_from_line('simple', None) finder = make_test_finder(find_links=[data.find_links]) - link = finder.find_requirement(req, False) - assert link.url.endswith("simple-3.0.tar.gz") + found = finder.find_requirement(req, False) + assert found.link.url.endswith("simple-3.0.tar.gz") def test_incorrect_case_file_index(data): """Test PackageFinder detects latest using wrong case""" req = install_req_from_line('dinner', None) finder = make_test_finder(index_urls=[data.find_links3]) - link = finder.find_requirement(req, False) - assert link.url.endswith("Dinner-2.0.tar.gz") + found = finder.find_requirement(req, False) + assert found.link.url.endswith("Dinner-2.0.tar.gz") @pytest.mark.network @@ -180,7 +180,7 @@ def test_find_wheel_supported(self, data, monkeypatch): finder = make_test_finder(find_links=[data.find_links]) found = finder.find_requirement(req, True) assert ( - found.url.endswith("simple.dist-0.1-py2.py3-none-any.whl") + found.link.url.endswith("simple.dist-0.1-py2.py3-none-any.whl") ), found def test_wheel_over_sdist_priority(self, data): @@ -191,7 +191,8 @@ def test_wheel_over_sdist_priority(self, data): req = install_req_from_line("priority") finder = make_test_finder(find_links=[data.find_links]) found = finder.find_requirement(req, True) - assert found.url.endswith("priority-1.0-py2.py3-none-any.whl"), found + assert found.link.url.endswith("priority-1.0-py2.py3-none-any.whl"), \ + found def test_existing_over_wheel_priority(self, data): """ @@ -292,8 +293,8 @@ def test_finder_priority_file_over_page(data): assert all(version.link.scheme == 'https' for version in all_versions[1:]), all_versions - link = finder.find_requirement(req, False) - assert link.url.startswith("file://") + found = finder.find_requirement(req, False) + assert found.link.url.startswith("file://") def test_finder_priority_nonegg_over_eggfragments(): @@ -306,9 +307,9 @@ def test_finder_priority_nonegg_over_eggfragments(): assert all_versions[0].link.url.endswith('tar.gz') assert all_versions[1].link.url.endswith('#egg=bar-1.0') - link = finder.find_requirement(req, False) + found = finder.find_requirement(req, False) - assert link.url.endswith('tar.gz') + assert found.link.url.endswith('tar.gz') links.reverse() @@ -316,9 +317,9 @@ def test_finder_priority_nonegg_over_eggfragments(): all_versions = finder.find_all_candidates(req.name) assert all_versions[0].link.url.endswith('tar.gz') assert all_versions[1].link.url.endswith('#egg=bar-1.0') - link = finder.find_requirement(req, False) + found = finder.find_requirement(req, False) - assert link.url.endswith('tar.gz') + assert found.link.url.endswith('tar.gz') def test_finder_only_installs_stable_releases(data): @@ -330,21 +331,21 @@ def test_finder_only_installs_stable_releases(data): # using a local index (that has pre & dev releases) finder = make_test_finder(index_urls=[data.index_url("pre")]) - link = finder.find_requirement(req, False) - assert link.url.endswith("bar-1.0.tar.gz"), link.url + found = finder.find_requirement(req, False) + assert found.link.url.endswith("bar-1.0.tar.gz"), found.link.url # using find-links links = ["https://foo/bar-1.0.tar.gz", "https://foo/bar-2.0b1.tar.gz"] finder = make_no_network_finder(links) - link = finder.find_requirement(req, False) - assert link.url == "https://foo/bar-1.0.tar.gz" + found = finder.find_requirement(req, False) + assert found.link.url == "https://foo/bar-1.0.tar.gz" links.reverse() finder = make_no_network_finder(links) - link = finder.find_requirement(req, False) - assert link.url == "https://foo/bar-1.0.tar.gz" + found = finder.find_requirement(req, False) + assert found.link.url == "https://foo/bar-1.0.tar.gz" def test_finder_only_installs_data_require(data): @@ -383,21 +384,21 @@ def test_finder_installs_pre_releases(data): index_urls=[data.index_url("pre")], allow_all_prereleases=True, ) - link = finder.find_requirement(req, False) - assert link.url.endswith("bar-2.0b1.tar.gz"), link.url + found = finder.find_requirement(req, False) + assert found.link.url.endswith("bar-2.0b1.tar.gz"), found.link.url # using find-links links = ["https://foo/bar-1.0.tar.gz", "https://foo/bar-2.0b1.tar.gz"] finder = make_no_network_finder(links, allow_all_prereleases=True) - link = finder.find_requirement(req, False) - assert link.url == "https://foo/bar-2.0b1.tar.gz" + found = finder.find_requirement(req, False) + assert found.link.url == "https://foo/bar-2.0b1.tar.gz" links.reverse() finder = make_no_network_finder(links, allow_all_prereleases=True) - link = finder.find_requirement(req, False) - assert link.url == "https://foo/bar-2.0b1.tar.gz" + found = finder.find_requirement(req, False) + assert found.link.url == "https://foo/bar-2.0b1.tar.gz" def test_finder_installs_dev_releases(data): @@ -412,8 +413,8 @@ def test_finder_installs_dev_releases(data): index_urls=[data.index_url("dev")], allow_all_prereleases=True, ) - link = finder.find_requirement(req, False) - assert link.url.endswith("bar-2.0.dev1.tar.gz"), link.url + found = finder.find_requirement(req, False) + assert found.link.url.endswith("bar-2.0.dev1.tar.gz"), found.link.url def test_finder_installs_pre_releases_with_version_spec(): @@ -424,14 +425,14 @@ def test_finder_installs_pre_releases_with_version_spec(): links = ["https://foo/bar-1.0.tar.gz", "https://foo/bar-2.0b1.tar.gz"] finder = make_no_network_finder(links) - link = finder.find_requirement(req, False) - assert link.url == "https://foo/bar-2.0b1.tar.gz" + found = finder.find_requirement(req, False) + assert found.link.url == "https://foo/bar-2.0b1.tar.gz" links.reverse() finder = make_no_network_finder(links) - link = finder.find_requirement(req, False) - assert link.url == "https://foo/bar-2.0b1.tar.gz" + found = finder.find_requirement(req, False) + assert found.link.url == "https://foo/bar-2.0b1.tar.gz" class TestLinkEvaluator(object): From 806067f09f6a6e0fd605728e0914a986dd4f936f Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 24 Apr 2020 18:53:56 +0800 Subject: [PATCH 1705/3170] Move yanked link warning into the legacy resolver --- src/pip/_internal/index/package_finder.py | 16 ---------------- src/pip/_internal/resolution/legacy/resolver.py | 17 ++++++++++++++++- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/src/pip/_internal/index/package_finder.py b/src/pip/_internal/index/package_finder.py index 98a6acaab5f..441992b92b3 100644 --- a/src/pip/_internal/index/package_finder.py +++ b/src/pip/_internal/index/package_finder.py @@ -556,23 +556,7 @@ def sort_best_candidate( """ if not candidates: return None - best_candidate = max(candidates, key=self._sort_key) - - # Log a warning per PEP 592 if necessary before returning. - link = best_candidate.link - if link.is_yanked: - reason = link.yanked_reason or '<none given>' - msg = ( - # Mark this as a unicode string to prevent - # "UnicodeEncodeError: 'ascii' codec can't encode character" - # in Python 2 when the reason contains non-ascii characters. - u'The candidate selected for download or install is a ' - 'yanked version: {candidate}\n' - 'Reason for being yanked: {reason}' - ).format(candidate=best_candidate, reason=reason) - logger.warning(msg) - return best_candidate def compute_best_candidate( diff --git a/src/pip/_internal/resolution/legacy/resolver.py b/src/pip/_internal/resolution/legacy/resolver.py index 3cf68bbd28a..2854e21033d 100644 --- a/src/pip/_internal/resolution/legacy/resolver.py +++ b/src/pip/_internal/resolution/legacy/resolver.py @@ -267,7 +267,22 @@ def _find_requirement_link(self, req): best_candidate = self.finder.find_requirement(req, upgrade) if not best_candidate: return None - return best_candidate.link + + # Log a warning per PEP 592 if necessary before returning. + link = best_candidate.link + if link.is_yanked: + reason = link.yanked_reason or '<none given>' + msg = ( + # Mark this as a unicode string to prevent + # "UnicodeEncodeError: 'ascii' codec can't encode character" + # in Python 2 when the reason contains non-ascii characters. + u'The candidate selected for download or install is a ' + 'yanked version: {candidate}\n' + 'Reason for being yanked: {reason}' + ).format(candidate=best_candidate, reason=reason) + logger.warning(msg) + + return link def _populate_link(self, req): # type: (InstallRequirement) -> None From 367e6617bd1b613f708cb28081f8f331d33be57f Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sat, 25 Apr 2020 03:14:09 +0800 Subject: [PATCH 1706/3170] Move yanked message unit tests to use resolver --- tests/lib/index.py | 14 +++ tests/unit/test_index.py | 71 +----------- tests/unit/test_resolution_legacy_resolver.py | 107 ++++++++++++++++++ 3 files changed, 122 insertions(+), 70 deletions(-) create mode 100644 tests/lib/index.py diff --git a/tests/lib/index.py b/tests/lib/index.py new file mode 100644 index 00000000000..0f507a0e7ff --- /dev/null +++ b/tests/lib/index.py @@ -0,0 +1,14 @@ +from pip._internal.models.candidate import InstallationCandidate +from pip._internal.models.link import Link + + +def make_mock_candidate(version, yanked_reason=None, hex_digest=None): + url = 'https://example.com/pkg-{}.tar.gz'.format(version) + if hex_digest is not None: + assert len(hex_digest) == 64 + url += '#sha256={}'.format(hex_digest) + + link = Link(url, yanked_reason=yanked_reason) + candidate = InstallationCandidate('mypackage', version, link) + + return candidate diff --git a/tests/unit/test_index.py b/tests/unit/test_index.py index 24dff7b38b0..2ae847ae0b7 100644 --- a/tests/unit/test_index.py +++ b/tests/unit/test_index.py @@ -15,7 +15,6 @@ _find_name_version_sep, filter_unallowed_hashes, ) -from pip._internal.models.candidate import InstallationCandidate from pip._internal.models.link import Link from pip._internal.models.search_scope import SearchScope from pip._internal.models.selection_prefs import SelectionPreferences @@ -24,18 +23,7 @@ from pip._internal.utils.compatibility_tags import get_supported from pip._internal.utils.hashes import Hashes from tests.lib import CURRENT_PY_VERSION_INFO - - -def make_mock_candidate(version, yanked_reason=None, hex_digest=None): - url = 'https://example.com/pkg-{}.tar.gz'.format(version) - if hex_digest is not None: - assert len(hex_digest) == 64 - url += '#sha256={}'.format(hex_digest) - - link = Link(url, yanked_reason=yanked_reason) - candidate = InstallationCandidate('mypackage', version, link) - - return candidate +from tests.lib.index import make_mock_candidate @pytest.mark.parametrize('requires_python, expected', [ @@ -470,63 +458,6 @@ def test_sort_best_candidate__no_candidates(self): actual = evaluator.sort_best_candidate([]) assert actual is None - def test_sort_best_candidate__all_yanked(self, caplog): - """ - Test all candidates yanked. - """ - candidates = [ - make_mock_candidate('1.0', yanked_reason='bad metadata #1'), - # Put the best candidate in the middle, to test sorting. - make_mock_candidate('3.0', yanked_reason='bad metadata #3'), - make_mock_candidate('2.0', yanked_reason='bad metadata #2'), - ] - expected_best = candidates[1] - evaluator = CandidateEvaluator.create('my-project') - actual = evaluator.sort_best_candidate(candidates) - assert actual is expected_best - assert str(actual.version) == '3.0' - - # Check the log messages. - assert len(caplog.records) == 1 - record = caplog.records[0] - assert record.levelname == 'WARNING' - assert record.message == ( - 'The candidate selected for download or install is a yanked ' - "version: 'mypackage' candidate " - '(version 3.0 at https://example.com/pkg-3.0.tar.gz)\n' - 'Reason for being yanked: bad metadata #3' - ) - - @pytest.mark.parametrize('yanked_reason, expected_reason', [ - # Test no reason given. - ('', '<none given>'), - # Test a unicode string with a non-ascii character. - (u'curly quote: \u2018', u'curly quote: \u2018'), - ]) - def test_sort_best_candidate__yanked_reason( - self, caplog, yanked_reason, expected_reason, - ): - """ - Test the log message with various reason strings. - """ - candidates = [ - make_mock_candidate('1.0', yanked_reason=yanked_reason), - ] - evaluator = CandidateEvaluator.create('my-project') - actual = evaluator.sort_best_candidate(candidates) - assert str(actual.version) == '1.0' - - assert len(caplog.records) == 1 - record = caplog.records[0] - assert record.levelname == 'WARNING' - expected_message = ( - 'The candidate selected for download or install is a yanked ' - "version: 'mypackage' candidate " - '(version 1.0 at https://example.com/pkg-1.0.tar.gz)\n' - 'Reason for being yanked: ' - ) + expected_reason - assert record.message == expected_message - def test_sort_best_candidate__best_yanked_but_not_all( self, caplog, ): diff --git a/tests/unit/test_resolution_legacy_resolver.py b/tests/unit/test_resolution_legacy_resolver.py index 64968358d86..561313c002b 100644 --- a/tests/unit/test_resolution_legacy_resolver.py +++ b/tests/unit/test_resolution_legacy_resolver.py @@ -1,5 +1,6 @@ import logging +import mock import pytest from pip._vendor import pkg_resources @@ -7,10 +8,14 @@ NoneMetadataError, UnsupportedPythonVersion, ) +from pip._internal.req.constructors import install_req_from_line from pip._internal.resolution.legacy.resolver import ( + Resolver, _check_dist_requires_python, ) from pip._internal.utils.packaging import get_requires_python +from tests.lib import make_test_finder +from tests.lib.index import make_mock_candidate # We need to inherit from DistInfoDistribution for the `isinstance()` @@ -169,3 +174,105 @@ def test_empty_metadata_error(self, caplog, metadata_name): "None {} metadata found for distribution: " "<distribution 'my-project'>".format(metadata_name) ) + + +class TestYankedWarning(object): + """ + Test _populate_link() emits warning if one or more candidates are yanked. + """ + def _make_test_resolver(self, monkeypatch, mock_candidates): + def _find_candidates(project_name): + return mock_candidates + + finder = make_test_finder() + monkeypatch.setattr(finder, "find_all_candidates", _find_candidates) + + return Resolver( + finder=finder, + preparer=mock.Mock(), # Not used. + make_install_req=install_req_from_line, + wheel_cache=None, + use_user_site=False, + force_reinstall=False, + ignore_dependencies=False, + ignore_installed=False, + ignore_requires_python=False, + upgrade_strategy="to-satisfy-only", + ) + + def test_sort_best_candidate__has_non_yanked(self, caplog, monkeypatch): + """ + Test unyanked candidate preferred over yanked. + """ + candidates = [ + make_mock_candidate('1.0'), + make_mock_candidate('2.0', yanked_reason='bad metadata #2'), + ] + ireq = install_req_from_line("pkg") + + resolver = self._make_test_resolver(monkeypatch, candidates) + resolver._populate_link(ireq) + + assert ireq.link == candidates[0].link + assert len(caplog.records) == 0 + + def test_sort_best_candidate__all_yanked(self, caplog, monkeypatch): + """ + Test all candidates yanked. + """ + candidates = [ + make_mock_candidate('1.0', yanked_reason='bad metadata #1'), + # Put the best candidate in the middle, to test sorting. + make_mock_candidate('3.0', yanked_reason='bad metadata #3'), + make_mock_candidate('2.0', yanked_reason='bad metadata #2'), + ] + ireq = install_req_from_line("pkg") + + resolver = self._make_test_resolver(monkeypatch, candidates) + resolver._populate_link(ireq) + + assert ireq.link == candidates[1].link + + # Check the log messages. + assert len(caplog.records) == 1 + record = caplog.records[0] + assert record.levelname == 'WARNING' + assert record.message == ( + 'The candidate selected for download or install is a yanked ' + "version: 'mypackage' candidate " + '(version 3.0 at https://example.com/pkg-3.0.tar.gz)\n' + 'Reason for being yanked: bad metadata #3' + ) + + @pytest.mark.parametrize('yanked_reason, expected_reason', [ + # Test no reason given. + ('', '<none given>'), + # Test a unicode string with a non-ascii character. + (u'curly quote: \u2018', u'curly quote: \u2018'), + ]) + def test_sort_best_candidate__yanked_reason( + self, caplog, monkeypatch, yanked_reason, expected_reason, + ): + """ + Test the log message with various reason strings. + """ + candidates = [ + make_mock_candidate('1.0', yanked_reason=yanked_reason), + ] + ireq = install_req_from_line("pkg") + + resolver = self._make_test_resolver(monkeypatch, candidates) + resolver._populate_link(ireq) + + assert ireq.link == candidates[0].link + + assert len(caplog.records) == 1 + record = caplog.records[0] + assert record.levelname == 'WARNING' + expected_message = ( + 'The candidate selected for download or install is a yanked ' + "version: 'mypackage' candidate " + '(version 1.0 at https://example.com/pkg-1.0.tar.gz)\n' + 'Reason for being yanked: ' + ) + expected_reason + assert record.message == expected_message From d23600413866393ccc43108f2de0ad14cf8d7491 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Fri, 24 Apr 2020 19:58:52 +0530 Subject: [PATCH 1707/3170] Rewrite `get_topological_weights` to allow cycles MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit We take the length for the longest path to any node from root, ignoring any paths that contain any node twice (i.e. cycles). --- .../resolution/resolvelib/resolver.py | 73 +++++++++++-------- 1 file changed, 44 insertions(+), 29 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index df0ea0941fa..d610afd8ea3 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -121,8 +121,10 @@ def get_installation_order(self, req_set): it. This helps ensure that the environment is kept consistent as they get installed one-by-one. - The current implementation walks the resolved dependency graph, and - make sure every node has a greater "weight" than all its parents. + The current implementation creates a topological ordering of the + dependency graph, while breaking any cycles in the graph at arbitrary + points. We make no guarantees about where the cycle would be broken, + other than they would be broken. """ assert self._result is not None, "must call resolve() first" @@ -138,35 +140,48 @@ def get_installation_order(self, req_set): def get_topological_weights(graph): - # type: (Graph) -> Dict[str, int] + # type: (Graph) -> Dict[Optional[str], int] """Assign weights to each node based on how "deep" they are. + + This implementation may change at any point in the future without prior + notice. + + We take the length for the longest path to any node from root, ignoring any + paths that contain a single node twice (i.e. cycles). This is done through + a depth-first search through the graph, while keeping track of the path to + the node. + + Cycles in the graph result would result in node being revisited while also + being it's own path. In this case, take no action. This helps ensure we + don't get stuck in a cycle. + + When assigning weight, the longer path (i.e. larger length) is preferred. """ - visited = set() # type: Set[str] - weights = {} - - key_count = len(graph) - while len(weights) < key_count: - progressed = False - for key in graph: - if key in weights: - continue - parents = list(graph.iter_parents(key)) - if not all(p in weights for p in parents): - continue - if parents: - weight = max(weights[p] for p in parents) + 1 - else: - weight = 0 - weights[key] = weight - progressed = True - - # FIXME: This check will fail if there are unbreakable cycles. - # Implement something to forcifully break them up to continue. - if not progressed: - raise InstallationError( - "Could not determine installation order due to cicular " - "dependency." - ) + path = [] # type: List[Optional[str]] + weights = {} # type: Dict[Optional[str], int] + + def visit(node): + # type: (Optional[str]) -> None + if node in path: + # We hit a cycle, so we'll break it here. + return + + # Time to visit the children! + path.append(node) + for child in graph.iter_children(node): + visit(child) + popped = path.pop() + assert popped == node, "Sanity check failed. Please file a bug report." + + last_known_parent_count = weights.get(node, 0) + weights[node] = max(last_known_parent_count, len(path)) + + # `None` is guaranteed to be the root node by resolvelib. + visit(None) + + # Sanity checks + assert weights[None] == 0 + assert len(weights) == len(graph) return weights From ebc0e3337879f779992377b5a771fc8b3bcea182 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Fri, 24 Apr 2020 18:06:30 +0530 Subject: [PATCH 1708/3170] Amend tests, by fixing typos --- tests/unit/resolution_resolvelib/test_resolver.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/unit/resolution_resolvelib/test_resolver.py b/tests/unit/resolution_resolvelib/test_resolver.py index aef80f55e39..88b2d546503 100644 --- a/tests/unit/resolution_resolvelib/test_resolver.py +++ b/tests/unit/resolution_resolvelib/test_resolver.py @@ -40,9 +40,9 @@ def resolver(preparer, finder): ( [ (None, "toporequires"), - (None, "toporequire2"), - (None, "toporequire3"), - (None, "toporequire4"), + (None, "toporequires2"), + (None, "toporequires3"), + (None, "toporequires4"), ("toporequires2", "toporequires"), ("toporequires3", "toporequires"), ("toporequires4", "toporequires"), From 9ae99b6d2bcb691413073ae8b6c3552fe24fbfd3 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sat, 25 Apr 2020 03:29:51 +0530 Subject: [PATCH 1709/3170] Factor out test logic for graph generation --- .../resolution_resolvelib/test_resolver.py | 25 ++++++++++++------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/tests/unit/resolution_resolvelib/test_resolver.py b/tests/unit/resolution_resolvelib/test_resolver.py index 88b2d546503..3c6f64891b3 100644 --- a/tests/unit/resolution_resolvelib/test_resolver.py +++ b/tests/unit/resolution_resolvelib/test_resolver.py @@ -26,6 +26,21 @@ def resolver(preparer, finder): return resolver +def _make_graph(edges): + """Build graph from edge declarations. + """ + + graph = DirectedGraph() + for parent, child in edges: + parent = canonicalize_name(parent) if parent else None + child = canonicalize_name(child) if child else None + for v in (parent, child): + if v not in graph: + graph.add(v) + graph.connect(parent, child) + return graph + + @pytest.mark.parametrize( "edges, ordered_reqs", [ @@ -59,15 +74,7 @@ def resolver(preparer, finder): ], ) def test_new_resolver_get_installation_order(resolver, edges, ordered_reqs): - # Build graph from edge declarations. - graph = DirectedGraph() - for parent, child in edges: - parent = canonicalize_name(parent) if parent else None - child = canonicalize_name(child) if child else None - for v in (parent, child): - if v not in graph: - graph.add(v) - graph.connect(parent, child) + graph = _make_graph(edges) # Mapping values and criteria are not used in test, so we stub them out. mapping = {vertex: None for vertex in graph if vertex is not None} From ea52559cf8543fdfaf90b0f8f0f065e31826145c Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sat, 25 Apr 2020 03:49:10 +0530 Subject: [PATCH 1710/3170] Add tests for get_topological_weights --- .../resolution_resolvelib/test_resolver.py | 149 +++++++++++++++++- 1 file changed, 148 insertions(+), 1 deletion(-) diff --git a/tests/unit/resolution_resolvelib/test_resolver.py b/tests/unit/resolution_resolvelib/test_resolver.py index 3c6f64891b3..6ae6bd7936f 100644 --- a/tests/unit/resolution_resolvelib/test_resolver.py +++ b/tests/unit/resolution_resolvelib/test_resolver.py @@ -6,7 +6,10 @@ from pip._internal.req.constructors import install_req_from_line from pip._internal.req.req_set import RequirementSet -from pip._internal.resolution.resolvelib.resolver import Resolver +from pip._internal.resolution.resolvelib.resolver import ( + Resolver, + get_topological_weights, +) @pytest.fixture() @@ -87,3 +90,147 @@ def test_new_resolver_get_installation_order(resolver, edges, ordered_reqs): ireqs = resolver.get_installation_order(reqset) req_strs = [str(r.req) for r in ireqs] assert req_strs == ordered_reqs + + +@pytest.mark.parametrize( + "name, edges, expected_weights", + [ + ( + # From https://github.com/pypa/pip/pull/8127#discussion_r414564664 + "deep second edge", + [ + (None, "one"), + (None, "two"), + ("one", "five"), + ("two", "three"), + ("three", "four"), + ("four", "five"), + ], + {None: 0, "one": 1, "two": 1, "three": 2, "four": 3, "five": 4}, + ), + ( + "linear", + [ + (None, "one"), + ("one", "two"), + ("two", "three"), + ("three", "four"), + ("four", "five"), + ], + {None: 0, "one": 1, "two": 2, "three": 3, "four": 4, "five": 5}, + ), + ( + "linear AND root -> two", + [ + (None, "one"), + ("one", "two"), + ("two", "three"), + ("three", "four"), + ("four", "five"), + (None, "two"), + ], + {None: 0, "one": 1, "two": 2, "three": 3, "four": 4, "five": 5}, + ), + ( + "linear AND root -> three", + [ + (None, "one"), + ("one", "two"), + ("two", "three"), + ("three", "four"), + ("four", "five"), + (None, "three"), + ], + {None: 0, "one": 1, "two": 2, "three": 3, "four": 4, "five": 5}, + ), + ( + "linear AND root -> four", + [ + (None, "one"), + ("one", "two"), + ("two", "three"), + ("three", "four"), + ("four", "five"), + (None, "four"), + ], + {None: 0, "one": 1, "two": 2, "three": 3, "four": 4, "five": 5}, + ), + ( + "linear AND root -> five", + [ + (None, "one"), + ("one", "two"), + ("two", "three"), + ("three", "four"), + ("four", "five"), + (None, "five"), + ], + {None: 0, "one": 1, "two": 2, "three": 3, "four": 4, "five": 5}, + ), + ( + "linear AND one -> four", + [ + (None, "one"), + ("one", "two"), + ("two", "three"), + ("three", "four"), + ("four", "five"), + ("one", "four"), + ], + {None: 0, "one": 1, "two": 2, "three": 3, "four": 4, "five": 5}, + ), + ( + "linear AND two -> four", + [ + (None, "one"), + ("one", "two"), + ("two", "three"), + ("three", "four"), + ("four", "five"), + ("two", "four"), + ], + {None: 0, "one": 1, "two": 2, "three": 3, "four": 4, "five": 5}, + ), + ( + "linear AND four -> one (cycle)", + [ + (None, "one"), + ("one", "two"), + ("two", "three"), + ("three", "four"), + ("four", "five"), + ("four", "one"), + ], + {None: 0, "one": 1, "two": 2, "three": 3, "four": 4, "five": 5}, + ), + ( + "linear AND four -> two (cycle)", + [ + (None, "one"), + ("one", "two"), + ("two", "three"), + ("three", "four"), + ("four", "five"), + ("four", "two"), + ], + {None: 0, "one": 1, "two": 2, "three": 3, "four": 4, "five": 5}, + ), + ( + "linear AND four -> three (cycle)", + [ + (None, "one"), + ("one", "two"), + ("two", "three"), + ("three", "four"), + ("four", "five"), + ("four", "three"), + ], + {None: 0, "one": 1, "two": 2, "three": 3, "four": 4, "five": 5}, + ), + ], +) +def test_new_resolver_topological_weights(name, edges, expected_weights): + graph = _make_graph(edges) + + weights = get_topological_weights(graph) + assert weights == expected_weights From 58e2057f1ae2ff2ef318a54e2cc7f83e43e0a82e Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sat, 25 Apr 2020 04:20:20 +0530 Subject: [PATCH 1711/3170] Optimize "in path" lookups O(n) list lookups to O(1) set lookups --- src/pip/_internal/resolution/resolvelib/resolver.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index d610afd8ea3..5ec4a97b6a7 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -15,7 +15,7 @@ from .factory import Factory if MYPY_CHECK_RUNNING: - from typing import Dict, List, Optional, Tuple + from typing import Dict, List, Optional, Set, Tuple from pip._vendor.resolvelib.resolvers import Result from pip._vendor.resolvelib.structs import Graph @@ -157,7 +157,7 @@ def get_topological_weights(graph): When assigning weight, the longer path (i.e. larger length) is preferred. """ - path = [] # type: List[Optional[str]] + path = set() # type: Set[Optional[str]] weights = {} # type: Dict[Optional[str], int] def visit(node): @@ -167,11 +167,10 @@ def visit(node): return # Time to visit the children! - path.append(node) + path.add(node) for child in graph.iter_children(node): visit(child) - popped = path.pop() - assert popped == node, "Sanity check failed. Please file a bug report." + path.remove(node) last_known_parent_count = weights.get(node, 0) weights[node] = max(last_known_parent_count, len(path)) From da99c147900481b50c7d53737112eed7f3fc7ebc Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Fri, 24 Apr 2020 23:09:42 -0500 Subject: [PATCH 1712/3170] add test contructed from blog post, see notes within file --- tests/yaml/overlap1.yml | 43 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) create mode 100644 tests/yaml/overlap1.yml diff --git a/tests/yaml/overlap1.yml b/tests/yaml/overlap1.yml new file mode 100644 index 00000000000..ce526012fb6 --- /dev/null +++ b/tests/yaml/overlap1.yml @@ -0,0 +1,43 @@ +# see: https://medium.com/knerd/the-nine-circles-of-python-dependency-hell-481d53e3e025 +base: + available: + - myapp 0.2.4; depends fussy, capridous + - name: fussy + version: 3.8.0 + depends: ['requests >=1.2.0,<3'] + - name: capridous + version: 1.1.0 + depends: ['requests >=1.0.3,<2'] + - requests 1.0.1 + - requests 1.0.3 + - requests 1.1.0 + - requests 1.2.0 + - requests 1.3.0 + - requests 2.1.0 + - requests 3.2.0 + +cases: +- + request: + - install: myapp + response: + - state: + - capridous 1.1.0 + - fussy 3.8.0 + - myapp 0.2.4 + - requests 1.3.0 + skip: old +- + request: + - install: fussy + response: + - state: + - fussy 3.8.0 + - requests 2.1.0 +- + request: + - install: capridous + response: + - state: + - capridous 1.1.0 + - requests 1.3.0 From f8fa3f8c4fbcbf13f25e07f6fa62034282e9813c Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Fri, 24 Apr 2020 23:11:35 -0500 Subject: [PATCH 1713/3170] add comment --- tests/yaml/overlap1.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/yaml/overlap1.yml b/tests/yaml/overlap1.yml index ce526012fb6..9cad2650b03 100644 --- a/tests/yaml/overlap1.yml +++ b/tests/yaml/overlap1.yml @@ -1,4 +1,5 @@ -# see: https://medium.com/knerd/the-nine-circles-of-python-dependency-hell-481d53e3e025 +# https://medium.com/knerd/the-nine-circles-of-python-dependency-hell-481d53e3e025 +# Circle 4: Overlapping transitive dependencies base: available: - myapp 0.2.4; depends fussy, capridous From 17103e8d1a35bf6662587104addd65688b9394c6 Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Sat, 25 Apr 2020 00:07:57 -0500 Subject: [PATCH 1714/3170] add new test example mentioned in issue #988 --- tests/yaml/pip988.yml | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) create mode 100644 tests/yaml/pip988.yml diff --git a/tests/yaml/pip988.yml b/tests/yaml/pip988.yml new file mode 100644 index 00000000000..0e97aac2488 --- /dev/null +++ b/tests/yaml/pip988.yml @@ -0,0 +1,38 @@ +# https://github.com/pypa/pip/issues/988 +# see comment from benoit-pierre +base: + available: + - A 1.0.0; depends B >= 1.0.0, C >= 1.0.0 + - A 2.0.0; depends B >= 2.0.0, C >= 1.0.0 + - B 1.0.0; depends C >= 1.0.0 + - B 2.0.0; depends C >= 2.0.0 + - C 1.0.0 + - C 2.0.0 + +cases: +- + request: + - install: C==1.0.0 + - install: B==1.0.0 + - install: A==1.0.0 + - install: A==2.0.0 + response: + - state: + - C 1.0.0 + - state: + - B 1.0.0 + - C 1.0.0 + - state: + - A 1.0.0 + - B 1.0.0 + - C 1.0.0 + - state: + - A 2.0.0 + - B 2.0.0 + - C 2.0.0 + # for the last install (A==2.0.0) the old resolver does gives + # - A 2.0.0 + # - B 2.0.0 + # - C 1.0.0 + # but because B 2.0.0 depends on C >=2.0.0 this is wrong + skip: old From 051c39494478aa444669c28638be9254a2b625a7 Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Sat, 25 Apr 2020 00:27:28 -0500 Subject: [PATCH 1715/3170] typo --- tests/yaml/pip988.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/yaml/pip988.yml b/tests/yaml/pip988.yml index 0e97aac2488..a5a3eab9fc4 100644 --- a/tests/yaml/pip988.yml +++ b/tests/yaml/pip988.yml @@ -30,7 +30,7 @@ cases: - A 2.0.0 - B 2.0.0 - C 2.0.0 - # for the last install (A==2.0.0) the old resolver does gives + # for the last install (A==2.0.0) the old resolver gives # - A 2.0.0 # - B 2.0.0 # - C 1.0.0 From bb9a2bdc4bdae1ae183199cceb7170259bcbe79b Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Sat, 25 Apr 2020 14:48:29 -0500 Subject: [PATCH 1716/3170] add yaml linter and update readme --- tests/yaml/README.md | 9 +++++ tests/yaml/linter.py | 89 ++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 98 insertions(+) create mode 100644 tests/yaml/linter.py diff --git a/tests/yaml/README.md b/tests/yaml/README.md index 7ee9d5bcf7b..3c725d2105d 100644 --- a/tests/yaml/README.md +++ b/tests/yaml/README.md @@ -7,4 +7,13 @@ that allows for specifying a custom index for temporary use. The `.yml` files are organized in the following way. A `base` section which ... +The linter is very useful for initally checking `.yml` files, e.g.: + + $ python linter.py -v simple.yml + +To run only the yaml tests, use (from the root of the source tree): + + $ tox -e py38 -- -m yaml -vv + + <!-- TODO: Add a good description of the format and how it can be used. --> diff --git a/tests/yaml/linter.py b/tests/yaml/linter.py new file mode 100644 index 00000000000..f85e0c44591 --- /dev/null +++ b/tests/yaml/linter.py @@ -0,0 +1,89 @@ +import sys +from pprint import pprint + +import yaml + +sys.path.insert(0, '../../src') +sys.path.insert(0, '../..') + +from tests.functional.test_yaml import convert_to_dict + + +def check_dict(d, required=None, optional=None): + assert isinstance(d, dict) + if required is None: + required = [] + if optional is None: + optional = [] + for key in required: + if key not in d: + sys.exit("key %r is required" % key) + allowed_keys = set(required) + allowed_keys.update(optional) + for key in d.keys(): + if key not in allowed_keys: + sys.exit("key %r is not allowed. Allowed keys are: %r" % + (key, allowed_keys)) + + +def lint_case(case, verbose=False): + if verbose: + print("--- linting case ---") + pprint(case) + + check_dict(case, optional=['available', 'request', 'response', 'skip']) + available = case.get("available", []) + requests = case.get("request", []) + responses = case.get("response", []) + assert isinstance(available, list) + assert isinstance(requests, list) + assert isinstance(responses, list) + assert len(requests) == len(responses) + + for package in available: + if isinstance(package, str): + package = convert_to_dict(package) + assert isinstance(package, dict) + + for request in requests: + check_dict(request, optional=['install', 'uninstall', 'options']) + + for response in responses: + check_dict(response, optional=['state', 'conflicting']) + assert len(response) == 1 + + +def lint_yml(yml_file, verbose=False): + if verbose: + print("=== linting: %s ===" % yml_file) + assert yml_file.endswith(".yml") + with open(yml_file) as fi: + data = yaml.safe_load(fi) + if verbose: + pprint(data) + + check_dict(data, required=['cases'], optional=['base']) + base = data.get("base", {}) + cases = data["cases"] + for i, case_template in enumerate(cases): + case = base.copy() + case.update(case_template) + lint_case(case, verbose) + + +if __name__ == '__main__': + from optparse import OptionParser + + p = OptionParser(usage="usage: %prog [options] FILE ...", + description='linter for .yml FILE(s)') + + p.add_option('-v', '--verbose', + action="store_true") + + opts, args = p.parse_args() + + if len(args) < 1: + p.error('at least one argument required, try -h') + + for yml_file in args: + lint_yml(yml_file, opts.verbose) From 1b22ef6666ad748fa4bbe8efa7787b271991b095 Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Sat, 25 Apr 2020 18:17:18 -0500 Subject: [PATCH 1717/3170] simplify code, ensure state is a list --- tests/yaml/linter.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/yaml/linter.py b/tests/yaml/linter.py index f85e0c44591..57570c6bba1 100644 --- a/tests/yaml/linter.py +++ b/tests/yaml/linter.py @@ -45,12 +45,11 @@ def lint_case(case, verbose=False): package = convert_to_dict(package) assert isinstance(package, dict) - for request in requests: + for request, response in zip(requests, responses): check_dict(request, optional=['install', 'uninstall', 'options']) - - for response in responses: check_dict(response, optional=['state', 'conflicting']) assert len(response) == 1 + assert isinstance(response.get('state') or [], list) def lint_yml(yml_file, verbose=False): From 9c9ae3dc8b71b173e2e5e0b5b2d684167b0ef598 Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Sat, 25 Apr 2020 18:26:40 -0500 Subject: [PATCH 1718/3170] update description - make flake8 happy --- tests/yaml/linter.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/yaml/linter.py b/tests/yaml/linter.py index 57570c6bba1..e079a9d1c55 100644 --- a/tests/yaml/linter.py +++ b/tests/yaml/linter.py @@ -6,7 +6,7 @@ sys.path.insert(0, '../../src') sys.path.insert(0, '../..') -from tests.functional.test_yaml import convert_to_dict +from tests.functional.test_yaml import convert_to_dict # noqa def check_dict(d, required=None, optional=None): @@ -74,7 +74,7 @@ def lint_yml(yml_file, verbose=False): from optparse import OptionParser p = OptionParser(usage="usage: %prog [options] FILE ...", - description='linter for .yml FILE(s)') + description="linter for pip's yaml test FILE(s)") p.add_option('-v', '--verbose', action="store_true") From b837b37074db56116f5f1bfec6d67a945b776391 Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Sat, 25 Apr 2020 20:00:44 -0500 Subject: [PATCH 1719/3170] update link to refer directly to benoit-pierre's comment --- tests/yaml/pip988.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/yaml/pip988.yml b/tests/yaml/pip988.yml index a5a3eab9fc4..29441f3a0a7 100644 --- a/tests/yaml/pip988.yml +++ b/tests/yaml/pip988.yml @@ -1,5 +1,4 @@ -# https://github.com/pypa/pip/issues/988 -# see comment from benoit-pierre +# https://github.com/pypa/pip/issues/988#issuecomment-606967707 base: available: - A 1.0.0; depends B >= 1.0.0, C >= 1.0.0 From a9930dbd19ba28061eecc0e898a732610a766f3a Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Sat, 25 Apr 2020 23:10:43 -0500 Subject: [PATCH 1720/3170] add checking for requested package dict --- tests/yaml/linter.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/yaml/linter.py b/tests/yaml/linter.py index e079a9d1c55..14065fc2bd6 100644 --- a/tests/yaml/linter.py +++ b/tests/yaml/linter.py @@ -3,9 +3,8 @@ import yaml -sys.path.insert(0, '../../src') -sys.path.insert(0, '../..') - +sys.path.insert(0, '../../src') # noqa +sys.path.insert(0, '../..') # noqa from tests.functional.test_yaml import convert_to_dict # noqa @@ -43,7 +42,11 @@ def lint_case(case, verbose=False): for package in available: if isinstance(package, str): package = convert_to_dict(package) - assert isinstance(package, dict) + if verbose: + pprint(package) + check_dict(package, + required=['name', 'version'], + optional=['depends', 'extras']) for request, response in zip(requests, responses): check_dict(request, optional=['install', 'uninstall', 'options']) From 65297804616c0cc2ff816404d0917acf9da1e5ad Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Sun, 26 Apr 2020 00:51:12 -0500 Subject: [PATCH 1721/3170] move import so isort does not try to "fix" import order --- tests/yaml/linter.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tests/yaml/linter.py b/tests/yaml/linter.py index 14065fc2bd6..8420db1c294 100644 --- a/tests/yaml/linter.py +++ b/tests/yaml/linter.py @@ -3,9 +3,8 @@ import yaml -sys.path.insert(0, '../../src') # noqa -sys.path.insert(0, '../..') # noqa -from tests.functional.test_yaml import convert_to_dict # noqa +sys.path.insert(0, '../../src') +sys.path.insert(0, '../..') def check_dict(d, required=None, optional=None): @@ -26,6 +25,8 @@ def check_dict(d, required=None, optional=None): def lint_case(case, verbose=False): + from tests.functional.test_yaml import convert_to_dict + if verbose: print("--- linting case ---") pprint(case) From 0104adb037ca4156a1442daa0bee5ace6705ba3e Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sun, 26 Apr 2020 16:39:31 +0800 Subject: [PATCH 1722/3170] Add failing test for explicit requirement extras --- tests/functional/test_new_resolver.py | 71 +++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index f20041c24bc..ad7100a3eb9 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -529,3 +529,74 @@ def test_new_resolver_handles_prerelease( *pip_args ) assert_installed(script, pkg=expected_version) + + +class TestExtraMerge(object): + """ + Test installing a package that depends the same package with different + extras, one listed as required and the other as in extra. + """ + + def _local_with_setup(script, name, version, requires, extras): + """Create the package as a local source directory to install from path. + """ + return create_test_package_with_setup( + script, + name=name, + version=version, + install_requires=requires, + extras_require=extras, + ) + + def _direct_wheel(script, name, version, requires, extras): + """Create the package as a wheel to install from path directly. + """ + return create_basic_wheel_for_package( + script, + name=name, + version=version, + depends=requires, + extras=extras, + ) + + def _wheel_from_index(script, name, version, requires, extras): + """Create the package as a wheel to install from index. + """ + create_basic_wheel_for_package( + script, + name=name, + version=version, + depends=requires, + extras=extras, + ) + return name + + @pytest.mark.parametrize( + "pkg_builder", + [_local_with_setup, _direct_wheel, _wheel_from_index], + ) + def test_new_resolver_extra_merge_in_package( + self, monkeypatch, script, pkg_builder, + ): + create_basic_wheel_for_package(script, "depdev", "1.0.0") + create_basic_wheel_for_package( + script, + "dep", + "1.0.0", + extras={"dev": ["depdev"]}, + ) + requirement = pkg_builder( + script, + name="pkg", + version="1.0.0", + requires=["dep"], + extras={"dev": ["dep[dev]"]}, + ) + + script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + requirement + "[dev]", + ) + assert_installed(script, pkg="1.0.0", dep="1.0.0", depdev="1.0.0") From 88da3441ced8b163eec60b6a1a5f5b2586396eca Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sun, 26 Apr 2020 11:31:02 +0200 Subject: [PATCH 1723/3170] Simplify _should_cache The condition "never cache if pip install would not have built" can be simplified to "do not cache editable requirements". This is easier to read, and avoid a double warning if the 'wheel' package is not installed. --- src/pip/_internal/wheel_builder.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index 6d1022d5661..74f47417974 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -118,11 +118,8 @@ def _should_cache( wheel cache, assuming the wheel cache is available, and _should_build() has determined a wheel needs to be built. """ - if not should_build_for_install_command( - req, check_binary_allowed=_always_true - ): - # never cache if pip install would not have built - # (editable mode, etc) + if req.editable or not req.source_dir: + # never cache editable requirements return False if req.link and req.link.is_vcs: From 452d39eb2f88feb0932a55f856129b67c3580539 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sun, 26 Apr 2020 17:01:34 +0800 Subject: [PATCH 1724/3170] Wrap wheel file generation in function --- src/pip/_internal/operations/install/wheel.py | 20 ++++++++++++------- 1 file changed, 13 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 0b3fbe2ffc2..5a5aa5bc0d6 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -8,6 +8,7 @@ import collections import compileall +import contextlib import csv import logging import os.path @@ -39,10 +40,11 @@ from email.message import Message from typing import ( Dict, List, Optional, Sequence, Tuple, Any, - Iterable, Callable, Set, + Iterable, Iterator, Callable, Set, ) from pip._internal.models.scheme import Scheme + from pip._internal.utils.filesystem import NamedTemporaryFileResult InstalledCSVRow = Tuple[str, ...] @@ -565,19 +567,24 @@ def is_entrypoint_wrapper(name): if msg is not None: logger.warning(msg) + @contextlib.contextmanager + def _generate_file(path, **kwargs): + # type: (str, **Any) -> Iterator[NamedTemporaryFileResult] + with adjacent_tmp_file(path, **kwargs) as f: + yield f + replace(f.name, path) + # Record pip as the installer installer_path = os.path.join(dest_info_dir, 'INSTALLER') - with adjacent_tmp_file(installer_path) as installer_file: + with _generate_file(installer_path) as installer_file: installer_file.write(b'pip\n') - replace(installer_file.name, installer_path) generated.append(installer_path) # Record the PEP 610 direct URL reference if direct_url is not None: direct_url_path = os.path.join(dest_info_dir, DIRECT_URL_METADATA_NAME) - with adjacent_tmp_file(direct_url_path) as direct_url_file: + with _generate_file(direct_url_path) as direct_url_file: direct_url_file.write(direct_url.to_json().encode("utf-8")) - replace(direct_url_file.name, direct_url_path) generated.append(direct_url_path) # Record details of all files installed @@ -589,10 +596,9 @@ def is_entrypoint_wrapper(name): changed=changed, generated=generated, lib_dir=lib_dir) - with adjacent_tmp_file(record_path, **csv_io_kwargs('w')) as record_file: + with _generate_file(record_path, **csv_io_kwargs('w')) as record_file: writer = csv.writer(record_file) writer.writerows(sorted_outrows(rows)) # sort to simplify testing - replace(record_file.name, record_path) def install_wheel( From 388ca923ef672ae7b4a491980c9de8c81e33703f Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sun, 26 Apr 2020 17:27:22 +0800 Subject: [PATCH 1725/3170] Add persumably failing permission check --- tests/unit/test_wheel.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index fc719e8905e..a7ac364510b 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -239,13 +239,19 @@ def prep(self, data, tmpdir): self.dest_dist_info = os.path.join( self.scheme.purelib, 'sample-1.2.0.dist-info') + def assert_permission(self, path, mode): + target_mode = os.stat(path).st_mode & 0o777 + assert (target_mode & mode) == mode, target_mode + def assert_installed(self): # lib assert os.path.isdir( os.path.join(self.scheme.purelib, 'sample')) # dist-info metadata = os.path.join(self.dest_dist_info, 'METADATA') - assert os.path.isfile(metadata) + self.assert_permission(metadata, 0o644) + record = os.path.join(self.dest_dist_info, 'RECORD') + self.assert_permission(record, 0o644) # data files data_file = os.path.join(self.scheme.data, 'my_data', 'data_file') assert os.path.isfile(data_file) @@ -286,7 +292,7 @@ def test_std_install_with_direct_url(self, data, tmpdir): direct_url_path = os.path.join( self.dest_dist_info, DIRECT_URL_METADATA_NAME ) - assert os.path.isfile(direct_url_path) + assert self.assert_permission(direct_url_path, 0o644) with open(direct_url_path, 'rb') as f: expected_direct_url_json = direct_url.to_json() direct_url_json = f.read().decode("utf-8") From 9af0b3daeb71d17cb60db42f62955467959b4e49 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sun, 26 Apr 2020 18:17:33 +0800 Subject: [PATCH 1726/3170] Use oct for better error message --- tests/unit/test_wheel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index a7ac364510b..3669d4d332b 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -241,7 +241,7 @@ def prep(self, data, tmpdir): def assert_permission(self, path, mode): target_mode = os.stat(path).st_mode & 0o777 - assert (target_mode & mode) == mode, target_mode + assert (target_mode & mode) == mode, oct(target_mode) def assert_installed(self): # lib From f1977cfa5fac9edbff0dc7352602c4133b8f6fff Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sun, 26 Apr 2020 17:44:33 +0800 Subject: [PATCH 1727/3170] Set permission on manually created metadata files --- src/pip/_internal/operations/install/wheel.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 5a5aa5bc0d6..9a292e5513b 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -572,6 +572,7 @@ def _generate_file(path, **kwargs): # type: (str, **Any) -> Iterator[NamedTemporaryFileResult] with adjacent_tmp_file(path, **kwargs) as f: yield f + os.chmod(f.name, 0o644) replace(f.name, path) # Record pip as the installer From 8e65f531b2f50ebfec503f0d4c57e8b3be521314 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sun, 26 Apr 2020 17:47:12 +0800 Subject: [PATCH 1728/3170] News --- news/8139.bugfix | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 news/8139.bugfix diff --git a/news/8139.bugfix b/news/8139.bugfix new file mode 100644 index 00000000000..63f8c9737d7 --- /dev/null +++ b/news/8139.bugfix @@ -0,0 +1,2 @@ +Set permission on distribution metadata generated by a wheel installation so +a non-priviledged user can read them from a system-site package. From adef52610efaa0ed88b33a2dd574c4e8ba427062 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sun, 26 Apr 2020 18:22:21 +0800 Subject: [PATCH 1729/3170] Fix assertion --- tests/unit/test_wheel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 3669d4d332b..2350927541d 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -292,7 +292,7 @@ def test_std_install_with_direct_url(self, data, tmpdir): direct_url_path = os.path.join( self.dest_dist_info, DIRECT_URL_METADATA_NAME ) - assert self.assert_permission(direct_url_path, 0o644) + self.assert_permission(direct_url_path, 0o644) with open(direct_url_path, 'rb') as f: expected_direct_url_json = direct_url.to_json() direct_url_json = f.read().decode("utf-8") From ff869e0c1588bf3a922534b5b2c6cfffc9a62bdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Sun, 26 Apr 2020 16:14:43 +0700 Subject: [PATCH 1730/3170] Make mypy happy with beta release automation Co-Authored-By: Pradyun Gedam <pradyunsg@gmail.com> --- news/ac1c4196-d21d-4e39-9d39-118e39c837ab.trivial | 0 tools/automation/release/check_version.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 news/ac1c4196-d21d-4e39-9d39-118e39c837ab.trivial diff --git a/news/ac1c4196-d21d-4e39-9d39-118e39c837ab.trivial b/news/ac1c4196-d21d-4e39-9d39-118e39c837ab.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tools/automation/release/check_version.py b/tools/automation/release/check_version.py index db02e7aef3f..e89d1b5bad9 100644 --- a/tools/automation/release/check_version.py +++ b/tools/automation/release/check_version.py @@ -20,7 +20,7 @@ def is_this_a_good_version_number(string: str) -> Optional[str]: if v.dev: return "No development releases on PyPI. What are you even thinking?" - if v.is_prerelease and v.pre[0] != "b": + if v.pre and v.pre[0] != "b": return "Only beta releases are allowed. No alphas." release = v.release From 850b8c5f53ce8c35d3abbc68351d4a29e70c25ec Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Sun, 26 Apr 2020 10:43:52 -0500 Subject: [PATCH 1731/3170] add information about only single tests to readme --- tests/yaml/README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/yaml/README.md b/tests/yaml/README.md index 3c725d2105d..f9adee78795 100644 --- a/tests/yaml/README.md +++ b/tests/yaml/README.md @@ -15,5 +15,17 @@ To run only the yaml tests, use (from the root of the source tree): $ tox -e py38 -- -m yaml -vv +Or, in order to avoid collecting all the test cases: + + $ tox -e py38 -- tests/functional/test_yaml.py + +Or, only a specific test: + + $ tox -e py38 -- tests/functional/test_yaml.py -k simple + +Or, just a specific test case: + + $ tox -e py38 -- tests/functional/test_yaml.py -k simple-0 + <!-- TODO: Add a good description of the format and how it can be used. --> From 87a6bbec39c2bbaa53bfa23b0bd238c1187574a5 Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Sun, 26 Apr 2020 14:09:27 -0500 Subject: [PATCH 1732/3170] add backtracking resolver yaml test --- tests/yaml/backtrack.yml | 40 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 40 insertions(+) create mode 100644 tests/yaml/backtrack.yml diff --git a/tests/yaml/backtrack.yml b/tests/yaml/backtrack.yml new file mode 100644 index 00000000000..0fe843875d6 --- /dev/null +++ b/tests/yaml/backtrack.yml @@ -0,0 +1,40 @@ +# Pradyun's backtracking example +base: + available: + - A 1.0.0; depends B == 1.0.0 + - A 2.0.0; depends B == 2.0.0, C == 1.0.0 + - A 3.0.0; depends B == 3.0.0, C == 2.0.0 + - A 4.0.0; depends B == 4.0.0, C == 3.0.0 + - A 5.0.0; depends B == 5.0.0, C == 4.0.0 + - A 6.0.0; depends B == 6.0.0, C == 5.0.0 + - A 7.0.0; depends B == 7.0.0, C == 6.0.0 + - A 8.0.0; depends B == 8.0.0, C == 7.0.0 + + - B 1.0.0; depends C == 1.0.0 + - B 2.0.0; depends C == 2.0.0 + - B 3.0.0; depends C == 3.0.0 + - B 4.0.0; depends C == 4.0.0 + - B 5.0.0; depends C == 5.0.0 + - B 6.0.0; depends C == 6.0.0 + - B 7.0.0; depends C == 7.0.0 + - B 8.0.0; depends C == 8.0.0 + + - C 1.0.0 + - C 2.0.0 + - C 3.0.0 + - C 4.0.0 + - C 5.0.0 + - C 6.0.0 + - C 7.0.0 + - C 8.0.0 + +cases: +- + request: + - install: A + response: + - state: + - A 1.0.0 + - B 1.0.0 + - C 1.0.0 + skip: old From d8aede42d16be66ebc4cd167b898d9e6e6afb39f Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sun, 26 Apr 2020 16:53:34 +0800 Subject: [PATCH 1733/3170] Correctly pass extras to explicit requirement Local source directory is still not working due to some other reasons. Need to investigate. --- src/pip/_internal/resolution/resolvelib/factory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 23686f76ac2..d0ab51d7b5f 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -152,7 +152,7 @@ def make_requirement_from_install_req(self, ireq): # Specifically, this might be needed in "name @ URL" # syntax - need to check where that syntax is handled. cand = self._make_candidate_from_link( - ireq.link, extras=set(), parent=ireq, + ireq.link, extras=set(ireq.extras), parent=ireq, ) return ExplicitRequirement(cand) return SpecifierRequirement(ireq, factory=self) From 06d9ea095276fb86bd65ccb31d31f80bad3c20b0 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Mon, 27 Apr 2020 14:36:21 +0800 Subject: [PATCH 1734/3170] Mark local install as xfail --- src/pip/_internal/resolution/resolvelib/candidates.py | 2 ++ tests/functional/test_new_resolver.py | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index f8461ade266..0de4febe1bb 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -405,6 +405,8 @@ def get_dependencies(self): ] # Add a dependency on the exact base. # (See note 2b in the class docstring) + # FIXME: This does not work if the base candidate is specified by + # link, e.g. "pip install .[dev]" will fail. spec = "{}=={}".format(self.base.name, self.base.version) deps.append(factory.make_requirement_from_spec(spec, self.base._ireq)) return deps diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index ad7100a3eb9..64c1f11af24 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -573,7 +573,13 @@ def _wheel_from_index(script, name, version, requires, extras): @pytest.mark.parametrize( "pkg_builder", - [_local_with_setup, _direct_wheel, _wheel_from_index], + [ + pytest.param( + _local_with_setup, marks=pytest.mark.xfail(strict=True), + ), + _direct_wheel, + _wheel_from_index, + ], ) def test_new_resolver_extra_merge_in_package( self, monkeypatch, script, pkg_builder, From a24fbd915e97ce167c9e318f9fb3b01824cb254f Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Mon, 27 Apr 2020 15:00:11 +0800 Subject: [PATCH 1735/3170] Wording on news fragment Co-Authored-By: Pradyun Gedam <pradyunsg@gmail.com> --- news/8139.bugfix | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/news/8139.bugfix b/news/8139.bugfix index 63f8c9737d7..5cb857c71ee 100644 --- a/news/8139.bugfix +++ b/news/8139.bugfix @@ -1,2 +1,2 @@ -Set permission on distribution metadata generated by a wheel installation so -a non-priviledged user can read them from a system-site package. +Correctly set permissions on metadata files during wheel installation, +to permit non-privileged users to read from system site-packages. From 6ec16f540febdc037eadbe2c4370d9d49207c792 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Thu, 23 Apr 2020 09:32:25 +0530 Subject: [PATCH 1736/3170] Add azure pipelines and github actions to contributing doc --- docs/html/development/contributing.rst | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/docs/html/development/contributing.rst b/docs/html/development/contributing.rst index 8fc2d68e11e..976b3529afd 100644 --- a/docs/html/development/contributing.rst +++ b/docs/html/development/contributing.rst @@ -39,8 +39,11 @@ separately, as a "formatting cleanup" PR, if needed. Automated Testing ================= -All pull requests and merges to 'master' branch are tested using `Travis CI`_ -based on our `.travis.yml`_ file. +All pull requests and merges to 'master' branch are tested using `Travis CI`_, +`Azure Pipelines`_ and `GitHub Actions`_ based on our `.travis.yml`_, +`.azure-pipelines`_ and `.github/workflows`_ files. More details about pip's +Continuous Integration can be found in the `CI Documentation`_ + You can find the status and results to the CI runs for your PR on GitHub's web UI for the pull request. You can also find links to the CI services' pages for @@ -257,5 +260,10 @@ will initiate a vote among the existing maintainers. .. _`Studies have shown`: https://www.kessler.de/prd/smartbear/BestPracticesForPeerCodeReview.pdf .. _`resolve merge conflicts`: https://help.github.com/articles/resolving-a-merge-conflict-using-the-command-line .. _`Travis CI`: https://travis-ci.org/ +.. _`Azure Pipelines`: https://azure.microsoft.com/en-in/services/devops/pipelines/ +.. _`GitHub Actions`: https://github.com/features/actions .. _`.travis.yml`: https://github.com/pypa/pip/blob/master/.travis.yml +.. _`.azure-pipelines`: https://github.com/pypa/pip/blob/master/.azure-pipelines +.. _`.github/workflows`: https://github.com/pypa/pip/blob/master/.github/workflows +.. _`CI Documentation`: https://pip.pypa.io/en/latest/development/ci/ .. _`towncrier`: https://pypi.org/project/towncrier/ From 6b640d1254ef9f39aaf443c242d6bbe80d8e0f35 Mon Sep 17 00:00:00 2001 From: Hugo <hugovk@users.noreply.github.com> Date: Mon, 27 Apr 2020 11:02:18 +0300 Subject: [PATCH 1737/3170] Add test for 'pip cache dir 123' with too many arguments --- tests/functional/test_cache.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index 62337876023..d359137d904 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -101,6 +101,16 @@ def test_cache_dir(script, cache_dir): assert os.path.normcase(cache_dir) == result.stdout.strip() +def test_cache_dir_too_many_args(script, cache_dir): + result = script.pip('cache', 'dir', 'aaa', expect_error=True) + + assert result.stdout == '' + + # This would be `result.stderr == ...`, but pip prints deprecation + # warnings on Python 2.7, so we check if the _line_ is in stderr. + assert 'ERROR: Too many arguments' in result.stderr.splitlines() + + @pytest.mark.usefixtures("populate_wheel_cache") def test_cache_info(script, wheel_cache_dir, wheel_cache_files): result = script.pip('cache', 'info') @@ -215,7 +225,7 @@ def test_cache_purge_too_many_args(script, wheel_cache_files): expect_error=True) assert result.stdout == '' - # This would be `result.stderr == ...`, but Pip prints deprecation + # This would be `result.stderr == ...`, but pip prints deprecation # warnings on Python 2.7, so we check if the _line_ is in stderr. assert 'ERROR: Too many arguments' in result.stderr.splitlines() From 2245fceb3a957d3cff2f3f7108241ac2186d64c0 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Mon, 27 Apr 2020 16:48:22 +0800 Subject: [PATCH 1738/3170] Make the permission logic more generic --- src/pip/_internal/operations/install/wheel.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 9a292e5513b..e7315ee4b52 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -33,7 +33,7 @@ from pip._internal.utils.misc import captured_stdout, ensure_dir, hash_file from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from pip._internal.utils.unpacking import unpack_file +from pip._internal.utils.unpacking import current_umask, unpack_file from pip._internal.utils.wheel import parse_wheel if MYPY_CHECK_RUNNING: @@ -567,12 +567,14 @@ def is_entrypoint_wrapper(name): if msg is not None: logger.warning(msg) + generated_file_mode = 0o666 - current_umask() + @contextlib.contextmanager def _generate_file(path, **kwargs): # type: (str, **Any) -> Iterator[NamedTemporaryFileResult] with adjacent_tmp_file(path, **kwargs) as f: yield f - os.chmod(f.name, 0o644) + os.chmod(f.name, generated_file_mode) replace(f.name, path) # Record pip as the installer From 1eb7011da4d065df8ee308ae9c7a3eae9416f282 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Tue, 28 Apr 2020 10:28:02 +0100 Subject: [PATCH 1739/3170] Add an assertion that upgrade_strategy is valid --- src/pip/_internal/resolution/resolvelib/factory.py | 4 ++++ tests/unit/resolution_resolvelib/test_resolver.py | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 22e7f26ed7c..86fc6de6281 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -42,6 +42,8 @@ class Factory(object): + _allowed_strategies = {"eager", "only-if-needed", "to-satisfy-only"} + def __init__( self, finder, # type: PackageFinder @@ -54,6 +56,8 @@ def __init__( py_version_info=None, # type: Optional[Tuple[int, ...]] ): # type: (...) -> None + assert upgrade_strategy in self._allowed_strategies + self.finder = finder self.preparer = preparer self._python_candidate = RequiresPythonCandidate(py_version_info) diff --git a/tests/unit/resolution_resolvelib/test_resolver.py b/tests/unit/resolution_resolvelib/test_resolver.py index aef80f55e39..be14a3b1a96 100644 --- a/tests/unit/resolution_resolvelib/test_resolver.py +++ b/tests/unit/resolution_resolvelib/test_resolver.py @@ -21,7 +21,7 @@ def resolver(preparer, finder): ignore_installed="not-used", ignore_requires_python="not-used", force_reinstall="not-used", - upgrade_strategy="not-used", + upgrade_strategy="to-satisfy-only", ) return resolver From a12fc7cc2ca7022a5326a58a03ba3fe6ba2617ae Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 28 Apr 2020 17:05:19 +0530 Subject: [PATCH 1740/3170] Ignore require-virtualenv in `pip cache` --- src/pip/_internal/commands/cache.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index 3c345dfa0d1..b404628c92d 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -32,6 +32,7 @@ class CacheCommand(Command): <pattern> can be a glob expression or a package name. """ + ignore_require_venv = True usage = """ %prog info %prog list [<pattern>] From 7090f803a25e4c423ba7c86ced33c165d730e31e Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 28 Apr 2020 21:02:02 +0530 Subject: [PATCH 1741/3170] Update AUTHORS.txt --- AUTHORS.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.txt b/AUTHORS.txt index 134f83a78a4..04c42fc2156 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -489,6 +489,7 @@ sinscary Sorin Sbarnea Stavros Korokithakis Stefan Scherfke +Stefano Rivera Stephan Erb stepshal Steve (Gadget) Barnes From 4b4b2d4b1f673234a2337d03f66a13a1238a49e3 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 28 Apr 2020 21:02:03 +0530 Subject: [PATCH 1742/3170] Bump for development --- src/pip/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/__init__.py b/src/pip/__init__.py index 3d249cecf1a..dc41d31b63e 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -4,7 +4,7 @@ from typing import List, Optional -__version__ = "20.1" +__version__ = "20.2.dev0" def main(args=None): From 5ce941ca5b6ac19f4003e8e7b5b07133c2df5f46 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 28 Apr 2020 21:02:03 +0530 Subject: [PATCH 1743/3170] Bump for release --- NEWS.rst | 21 +++++++++++++++++++ news/7350.feature | 1 - ...c6ae31-6e5d-4f67-9b6b-3e7d4934d775.trivial | 0 news/8106-fdsadh-34-sdfsa-35dd.trivial | 0 news/8124.bugfix | 1 - news/8139.bugfix | 2 -- ...e1f2af-8382-450d-aa7e-9357fcd7c645.trivial | 0 news/fhasjhf-343-gdg--11sfdd.trivial | 0 news/python2.process | 1 - src/pip/__init__.py | 2 +- 10 files changed, 22 insertions(+), 6 deletions(-) delete mode 100644 news/7350.feature delete mode 100644 news/7fc6ae31-6e5d-4f67-9b6b-3e7d4934d775.trivial delete mode 100644 news/8106-fdsadh-34-sdfsa-35dd.trivial delete mode 100644 news/8124.bugfix delete mode 100644 news/8139.bugfix delete mode 100644 news/f7e1f2af-8382-450d-aa7e-9357fcd7c645.trivial delete mode 100644 news/fhasjhf-343-gdg--11sfdd.trivial delete mode 100644 news/python2.process diff --git a/NEWS.rst b/NEWS.rst index 493dc206e2b..78d8d52252b 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -7,6 +7,27 @@ .. towncrier release notes start +20.1 (2020-04-28) +================= + +Process +------- + +- Document that pip 21.0 will drop support for Python 2.7. + +Features +-------- + +- Add ``pip cache dir`` to show the cache directory. (`#7350 <https://github.com/pypa/pip/issues/7350>`_) + +Bug Fixes +--------- + +- Abort pip cache commands early when cache is disabled. (`#8124 <https://github.com/pypa/pip/issues/8124>`_) +- Correctly set permissions on metadata files during wheel installation, + to permit non-privileged users to read from system site-packages. (`#8139 <https://github.com/pypa/pip/issues/8139>`_) + + 20.1b1 (2020-04-21) =================== diff --git a/news/7350.feature b/news/7350.feature deleted file mode 100644 index 1b57d7b1907..00000000000 --- a/news/7350.feature +++ /dev/null @@ -1 +0,0 @@ -Add ``pip cache dir`` to show the cache directory. diff --git a/news/7fc6ae31-6e5d-4f67-9b6b-3e7d4934d775.trivial b/news/7fc6ae31-6e5d-4f67-9b6b-3e7d4934d775.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/8106-fdsadh-34-sdfsa-35dd.trivial b/news/8106-fdsadh-34-sdfsa-35dd.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/8124.bugfix b/news/8124.bugfix deleted file mode 100644 index a859381e0d8..00000000000 --- a/news/8124.bugfix +++ /dev/null @@ -1 +0,0 @@ -Abort pip cache commands early when cache is disabled. diff --git a/news/8139.bugfix b/news/8139.bugfix deleted file mode 100644 index 5cb857c71ee..00000000000 --- a/news/8139.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -Correctly set permissions on metadata files during wheel installation, -to permit non-privileged users to read from system site-packages. diff --git a/news/f7e1f2af-8382-450d-aa7e-9357fcd7c645.trivial b/news/f7e1f2af-8382-450d-aa7e-9357fcd7c645.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/fhasjhf-343-gdg--11sfdd.trivial b/news/fhasjhf-343-gdg--11sfdd.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/python2.process b/news/python2.process deleted file mode 100644 index 4becb99c9b6..00000000000 --- a/news/python2.process +++ /dev/null @@ -1 +0,0 @@ -Document that pip 21.0 will drop support for Python 2.7. diff --git a/src/pip/__init__.py b/src/pip/__init__.py index 5e02decc479..3d249cecf1a 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -4,7 +4,7 @@ from typing import List, Optional -__version__ = "20.1.dev1" +__version__ = "20.1" def main(args=None): From 28b0fd353bcd790376597dc6707525304e79aa26 Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Tue, 28 Apr 2020 22:49:18 -0500 Subject: [PATCH 1744/3170] add allow_error parameter to .run() method --- tests/lib/__init__.py | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index 44f39858e14..f51cce1e23a 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -533,6 +533,10 @@ def run(self, *args, **kw): `allow_stderr_warning` since warnings are weaker than errors. :param allow_stderr_warning: whether a logged warning (or deprecation message) is allowed in stderr. + :param allow_error: if True (default is False) does not raise + exception when the command exit value is non-zero. Implies + expect_error, but in contrast to expect_error will not assert + that the exit value is zero. :param expect_error: if False (the default), asserts that the command exits with 0. Otherwise, asserts that the command exits with a non-zero exit code. Passing True also implies allow_stderr_error @@ -553,10 +557,14 @@ def run(self, *args, **kw): # Partial fix for ScriptTest.run using `shell=True` on Windows. args = [str(a).replace('^', '^^').replace('&', '^&') for a in args] - # Remove `allow_stderr_error` and `allow_stderr_warning` before - # calling run() because PipTestEnvironment doesn't support them. + # Remove `allow_stderr_error`, `allow_stderr_warning` and + # `allow_error` before calling run() because PipTestEnvironment + # doesn't support them. allow_stderr_error = kw.pop('allow_stderr_error', None) allow_stderr_warning = kw.pop('allow_stderr_warning', None) + allow_error = kw.pop('allow_error', None) + if allow_error: + kw['expect_error'] = True # Propagate default values. expect_error = kw.get('expect_error') @@ -596,7 +604,7 @@ def run(self, *args, **kw): kw['expect_stderr'] = True result = super(PipTestEnvironment, self).run(cwd=cwd, *args, **kw) - if expect_error: + if expect_error and not allow_error: if result.returncode == 0: __tracebackhide__ = True raise AssertionError("Script passed unexpectedly.") From c14c92d9cd2e4f807e91914fcf7ccb6d58f1228c Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Tue, 28 Apr 2020 23:15:57 -0500 Subject: [PATCH 1745/3170] simplify handle_request() - improve error checking --- tests/functional/test_yaml.py | 69 ++++++++++++++++------------------- 1 file changed, 32 insertions(+), 37 deletions(-) diff --git a/tests/functional/test_yaml.py b/tests/functional/test_yaml.py index 5f5e1a8c61f..6525878c286 100644 --- a/tests/functional/test_yaml.py +++ b/tests/functional/test_yaml.py @@ -117,43 +117,20 @@ def handle_request(script, action, requirement, options, new_resolver=False): result = script.pip(*args, allow_stderr_error=True, - allow_stderr_warning=True) + allow_stderr_warning=True, + allow_error=True) - retval = { - "_result_object": result, - } - if result.returncode == 0: - # Check which packages got installed - retval["state"] = [] + # Check which packages got installed + state = [] + for path in os.listdir(script.site_packages_path): + if path.endswith(".dist-info"): + name, version = ( + os.path.basename(path)[:-len(".dist-info")] + ).rsplit("-", 1) + # TODO: information about extras. + state.append(" ".join((name, version))) - for path in os.listdir(script.site_packages_path): - if path.endswith(".dist-info"): - name, version = ( - os.path.basename(path)[:-len(".dist-info")] - ).rsplit("-", 1) - - # TODO: information about extras. - - retval["state"].append(" ".join((name, version))) - - retval["state"].sort() - - elif "conflicting" in result.stderr.lower(): - retval["conflicting"] = [] - - message = result.stderr.rsplit("\n", 1)[-1] - - # XXX: There might be a better way than parsing the message - for match in re.finditer(message, _conflict_finder_pat): - di = match.groupdict() - retval["conflicting"].append( - { - "required_by": "{} {}".format(di["name"], di["version"]), - "selector": di["selector"] - } - ) - - return retval + return {"result": result, "state": sorted(state)} @pytest.mark.yaml @@ -195,5 +172,23 @@ def test_yaml_based(script, case): request.get('options', '').split(), case[':resolver:'] == 'new') - assert effect['state'] == (response['state'] or []), \ - str(effect["_result_object"]) + if 0: # for analyzing output easier + with open(DATA_DIR.parent / "yaml" / + case[':name:'].replace('*', '-'), 'w') as fo: + result = effect['result'] + fo.write("=== RETURNCODE = %d\n" % result.returncode) + fo.write("=== STDERR ===:\n%s\n" % result.stderr) + + if 'state' in response: + assert effect['state'] == (response['state'] or []), \ + str(effect["result"]) + + error = False + if 'conflicting' in response: + error = True + + if error: + if case[":resolver:"] == 'old': + assert effect["result"].returncode == 0, str(effect["result"]) + elif case[":resolver:"] == 'new': + assert effect["result"].returncode == 1, str(effect["result"]) From 8b4a4c469af717b7bb966b94dd7f7ef026879e50 Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Tue, 28 Apr 2020 23:32:55 -0500 Subject: [PATCH 1746/3170] add ability to install list of requirements - with example in simple.yml --- tests/functional/test_yaml.py | 12 ++++++++---- tests/yaml/simple.yml | 7 +++++++ 2 files changed, 15 insertions(+), 4 deletions(-) diff --git a/tests/functional/test_yaml.py b/tests/functional/test_yaml.py index 6525878c286..c63b9f5eb80 100644 --- a/tests/functional/test_yaml.py +++ b/tests/functional/test_yaml.py @@ -98,9 +98,6 @@ def stripping_split(my_str, splitwith, count=None): def handle_request(script, action, requirement, options, new_resolver=False): - assert isinstance(requirement, str), ( - "Need install requirement to be a string only" - ) if action == 'install': args = ['install'] if new_resolver: @@ -111,7 +108,14 @@ def handle_request(script, action, requirement, options, new_resolver=False): args = ['uninstall', '--yes'] else: raise "Did not excpet action: {!r}".format(action) - args.append(requirement) + + if isinstance(requirement, str): + args.append(requirement) + elif isinstance(requirement, list): + args.extend(requirement) + else: + raise "requirement neither str nor list {!r}".format(requirement) + args.extend(options) args.append("--verbose") diff --git a/tests/yaml/simple.yml b/tests/yaml/simple.yml index 257ccb7dfc6..8e90e605d54 100644 --- a/tests/yaml/simple.yml +++ b/tests/yaml/simple.yml @@ -38,3 +38,10 @@ cases: response: - state: - base 0.1.0 +- + request: + - install: ['dep', 'simple==0.1.0'] + response: + - state: + - dep 0.1.0 + - simple 0.1.0 From 2423d58ea50464787cde1ac61baf5c1d5d80ef39 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Wed, 29 Apr 2020 10:05:29 +0530 Subject: [PATCH 1747/3170] Fix generated file mode to use bitwise AND --- news/8164.bugfix | 1 + src/pip/_internal/operations/install/wheel.py | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 news/8164.bugfix diff --git a/news/8164.bugfix b/news/8164.bugfix new file mode 100644 index 00000000000..f8d41b3bee1 --- /dev/null +++ b/news/8164.bugfix @@ -0,0 +1 @@ +Get generated file mode by performing bitwise AND with the negation of current umask diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index e7315ee4b52..2fb86b866db 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -567,7 +567,7 @@ def is_entrypoint_wrapper(name): if msg is not None: logger.warning(msg) - generated_file_mode = 0o666 - current_umask() + generated_file_mode = 0o666 & ~current_umask() @contextlib.contextmanager def _generate_file(path, **kwargs): From 1f6ff0cc6e264579034f2bb653ed3b37e86219fc Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Tue, 28 Apr 2020 23:43:00 -0500 Subject: [PATCH 1748/3170] make flake8 happy --- tests/functional/test_yaml.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/functional/test_yaml.py b/tests/functional/test_yaml.py index c63b9f5eb80..4a03381a31c 100644 --- a/tests/functional/test_yaml.py +++ b/tests/functional/test_yaml.py @@ -177,8 +177,8 @@ def test_yaml_based(script, case): case[':resolver:'] == 'new') if 0: # for analyzing output easier - with open(DATA_DIR.parent / "yaml" / - case[':name:'].replace('*', '-'), 'w') as fo: + with open(DATA_DIR.parent / "yaml" / + case[':name:'].replace('*', '-'), 'w') as fo: result = effect['result'] fo.write("=== RETURNCODE = %d\n" % result.returncode) fo.write("=== STDERR ===:\n%s\n" % result.stderr) From e8920daeab32e596ec26eeec96f9a3ace16678bf Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Wed, 29 Apr 2020 00:12:02 -0500 Subject: [PATCH 1749/3170] add trivial example to demonstrate installing and uninstalling multiple packages --- tests/yaml/trivial.yml | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) create mode 100644 tests/yaml/trivial.yml diff --git a/tests/yaml/trivial.yml b/tests/yaml/trivial.yml new file mode 100644 index 00000000000..418422044e4 --- /dev/null +++ b/tests/yaml/trivial.yml @@ -0,0 +1,24 @@ +base: + available: + - a 0.1.0 + - b 0.2.0 + - c 0.3.0 + +cases: +- + request: + - install: ['a', 'b'] + - install: c + - uninstall: ['b', 'c'] + - uninstall: a + response: + - state: + - a 0.1.0 + - b 0.2.0 + - state: + - a 0.1.0 + - b 0.2.0 + - c 0.3.0 + - state: + - a 0.1.0 + - state: null From 9319703ad9658a2415f62b87b3bfe60f1f6a9b7f Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Wed, 29 Apr 2020 11:02:06 +0530 Subject: [PATCH 1750/3170] Add unit test for checking permissions with custom umask --- tests/unit/test_wheel.py | 32 +++++++++++++++++++++++++++----- 1 file changed, 27 insertions(+), 5 deletions(-) diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 2350927541d..a53dc728411 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -243,15 +243,15 @@ def assert_permission(self, path, mode): target_mode = os.stat(path).st_mode & 0o777 assert (target_mode & mode) == mode, oct(target_mode) - def assert_installed(self): + def assert_installed(self, expected_permission): # lib assert os.path.isdir( os.path.join(self.scheme.purelib, 'sample')) # dist-info metadata = os.path.join(self.dest_dist_info, 'METADATA') - self.assert_permission(metadata, 0o644) + self.assert_permission(metadata, expected_permission) record = os.path.join(self.dest_dist_info, 'RECORD') - self.assert_permission(record, 0o644) + self.assert_permission(record, expected_permission) # data files data_file = os.path.join(self.scheme.data, 'my_data', 'data_file') assert os.path.isfile(data_file) @@ -268,7 +268,29 @@ def test_std_install(self, data, tmpdir): scheme=self.scheme, req_description=str(self.req), ) - self.assert_installed() + self.assert_installed(0o644) + + @pytest.mark.parametrize("user_mask, expected_permission", [ + (0o27, 0o640) + ]) + def test_std_install_with_custom_umask(self, data, tmpdir, + user_mask, expected_permission): + """Test that the files created after install honor the permissions + set when the user sets a custom umask""" + + prev_umask = os.umask(0) + try: + os.umask(user_mask) + self.prep(data, tmpdir) + wheel.install_wheel( + self.name, + self.wheelpath, + scheme=self.scheme, + req_description=str(self.req), + ) + self.assert_installed(expected_permission) + finally: + os.umask(prev_umask) def test_std_install_with_direct_url(self, data, tmpdir): """Test that install_wheel creates direct_url.json metadata when @@ -340,7 +362,7 @@ def test_dist_info_contains_empty_dir(self, data, tmpdir): req_description=str(self.req), _temp_dir_for_testing=self.src, ) - self.assert_installed() + self.assert_installed(0o644) assert not os.path.isdir( os.path.join(self.dest_dist_info, 'empty_dir')) From 8fc6f356205aa6f73c490dde631bbe200dd18029 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Wed, 29 Apr 2020 11:48:17 +0530 Subject: [PATCH 1751/3170] Fix NEWS wording --- news/8164.bugfix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/8164.bugfix b/news/8164.bugfix index f8d41b3bee1..1707d28401a 100644 --- a/news/8164.bugfix +++ b/news/8164.bugfix @@ -1 +1 @@ -Get generated file mode by performing bitwise AND with the negation of current umask +Fix metadata permission issues when umask has the executable bit set. From 8dccece9c000593aa220de7e60dac69dc10b9b6d Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Wed, 29 Apr 2020 11:57:11 +0530 Subject: [PATCH 1752/3170] Combine setting new umask and getting old one into one --- tests/unit/test_wheel.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index a53dc728411..b64d4cef312 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -278,9 +278,8 @@ def test_std_install_with_custom_umask(self, data, tmpdir, """Test that the files created after install honor the permissions set when the user sets a custom umask""" - prev_umask = os.umask(0) + prev_umask = os.umask(user_mask) try: - os.umask(user_mask) self.prep(data, tmpdir) wheel.install_wheel( self.name, From 5186f05bd772f545d62a78888a1592fff485b1ed Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Sat, 25 Apr 2020 11:48:16 +0100 Subject: [PATCH 1753/3170] Add tests for constraints --- tests/functional/test_new_resolver.py | 51 +++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index f20041c24bc..85e400b1063 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -529,3 +529,54 @@ def test_new_resolver_handles_prerelease( *pip_args ) assert_installed(script, pkg=expected_version) + + +def test_new_resolver_constraints(script): + create_basic_wheel_for_package(script, "pkg", "1.0") + create_basic_wheel_for_package(script, "pkg", "2.0") + create_basic_wheel_for_package(script, "pkg", "3.0") + constraints_file = script.scratch_path / "constraints.txt" + constraints_file.write_text("pkg<2.0\nconstraint_only<1.0") + script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "-c", constraints_file, + "pkg" + ) + assert_installed(script, pkg="1.0") + assert_not_installed(script, "constraint_only") + + +def test_new_resolver_constraint_on_dependency(script): + create_basic_wheel_for_package(script, "base", "1.0", depends=["dep"]) + create_basic_wheel_for_package(script, "dep", "1.0") + constraints_file = script.scratch_path / "constraints.txt" + constraints_file.write_text("dep==1.0") + script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "-c", constraints_file, + "base" + ) + assert_installed(script, base="1.0") + assert_installed(script, dep="1.0") + + +def test_new_resolver_constraint_on_path(script): + setup_py = script.scratch_path / "setup.py" + text = "from setuptools import setup\nsetup(name='foo', version='2.0')" + setup_py.write_text(text) + constraints_txt = script.scratch_path / "constraints.txt" + constraints_txt.write_text("foo==1.0") + result = script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "-c", constraints_txt, + str(script.scratch_path), + expect_error=True, + ) + + msg = "installation from path or url cannot be constrained to a version" + assert msg in result.stderr, str(result) From c92f55f9fcc22be3b65d968b53d94d86e0dc1fe1 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Wed, 29 Apr 2020 12:24:15 +0100 Subject: [PATCH 1754/3170] Implement constraints --- .../resolution/resolvelib/factory.py | 6 +++-- .../resolution/resolvelib/requirements.py | 18 ++++++++++--- .../resolution/resolvelib/resolver.py | 25 +++++++++++++------ 3 files changed, 36 insertions(+), 13 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 23686f76ac2..f6085a9e14c 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -21,7 +21,7 @@ ) if MYPY_CHECK_RUNNING: - from typing import Dict, Iterator, Optional, Set, Tuple, TypeVar + from typing import Dict, Iterator, List, Optional, Set, Tuple, TypeVar from pip._vendor.packaging.specifiers import SpecifierSet from pip._vendor.packaging.version import _BaseVersion @@ -71,6 +71,8 @@ def __init__( else: self._installed_dists = {} + self._constraints = {} # type: Dict[str,List[SpecifierSet]] + def _make_candidate_from_dist( self, dist, # type: Distribution @@ -154,7 +156,7 @@ def make_requirement_from_install_req(self, ireq): cand = self._make_candidate_from_link( ireq.link, extras=set(), parent=ireq, ) - return ExplicitRequirement(cand) + return ExplicitRequirement(cand, factory=self) return SpecifierRequirement(ireq, factory=self) def make_requirement_from_spec(self, specifier, comes_from): diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py index d2e4479b084..1c4c54398b1 100644 --- a/src/pip/_internal/resolution/resolvelib/requirements.py +++ b/src/pip/_internal/resolution/resolvelib/requirements.py @@ -1,5 +1,6 @@ from pip._vendor.packaging.utils import canonicalize_name +from pip._internal.exceptions import InstallationError from pip._internal.utils.typing import MYPY_CHECK_RUNNING from .base import Requirement, format_name @@ -16,9 +17,10 @@ class ExplicitRequirement(Requirement): - def __init__(self, candidate): - # type: (Candidate) -> None + def __init__(self, candidate, factory): + # type: (Candidate, Factory) -> None self.candidate = candidate + self._factory = factory def __repr__(self): # type: () -> str @@ -35,6 +37,12 @@ def name(self): def find_matches(self): # type: () -> Sequence[Candidate] + if self.name in self._factory._constraints: + raise InstallationError( + "Could not satisfy constraints for '{}': " + "installation from path or url cannot be " + "constrained to a version".format(self.name) + ) return [self.candidate] def is_satisfied_by(self, candidate): @@ -70,7 +78,11 @@ def name(self): def find_matches(self): # type: () -> Sequence[Candidate] it = self._factory.iter_found_candidates(self._ireq, self.extras) - return list(it) + constraints = self._factory._constraints.get(self.name, []) + lit = [c for c in it if all( + s.contains(c.version, prereleases=True) for s in constraints + )] + return lit def is_satisfied_by(self, candidate): # type: (Candidate) -> bool diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index cba5a496508..739774f9ba8 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -1,5 +1,6 @@ import functools import logging +from collections import defaultdict from pip._vendor import six from pip._vendor.packaging.utils import canonicalize_name @@ -17,6 +18,7 @@ if MYPY_CHECK_RUNNING: from typing import Dict, List, Optional, Tuple + from pip._vendor.packaging.specifiers import SpecifierSet from pip._vendor.resolvelib.resolvers import Result from pip._internal.cache import WheelCache @@ -60,9 +62,21 @@ def __init__( def resolve(self, root_reqs, check_supported_wheels): # type: (List[InstallRequirement], bool) -> RequirementSet - # FIXME: Implement constraints. - if any(r.constraint for r in root_reqs): - raise InstallationError("Constraints are not yet supported.") + constraints = defaultdict(list) # type: Dict[str,List[SpecifierSet]] + requirements = [] + for req in root_reqs: + if req.constraint: + assert req.name + assert req.specifier + name = canonicalize_name(req.name) + constraints[name].append(req.specifier) + else: + requirements.append( + self.factory.make_requirement_from_install_req(req) + ) + + # TODO: Refactor this, it's just for proof of concept + self.factory._constraints = constraints provider = PipProvider( factory=self.factory, @@ -71,11 +85,6 @@ def resolve(self, root_reqs, check_supported_wheels): reporter = BaseReporter() resolver = RLResolver(provider, reporter) - requirements = [ - self.factory.make_requirement_from_install_req(r) - for r in root_reqs - ] - try: self._result = resolver.resolve(requirements) From 0058bb52125afeba1f0ba625e4f563fcbe953c73 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Wed, 29 Apr 2020 14:55:15 +0100 Subject: [PATCH 1755/3170] Improve test --- tests/functional/test_new_resolver.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 85e400b1063..04dcdb1b8b8 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -551,8 +551,10 @@ def test_new_resolver_constraints(script): def test_new_resolver_constraint_on_dependency(script): create_basic_wheel_for_package(script, "base", "1.0", depends=["dep"]) create_basic_wheel_for_package(script, "dep", "1.0") + create_basic_wheel_for_package(script, "dep", "2.0") + create_basic_wheel_for_package(script, "dep", "3.0") constraints_file = script.scratch_path / "constraints.txt" - constraints_file.write_text("dep==1.0") + constraints_file.write_text("dep==2.0") script.pip( "install", "--unstable-feature=resolver", "--no-cache-dir", "--no-index", @@ -561,7 +563,7 @@ def test_new_resolver_constraint_on_dependency(script): "base" ) assert_installed(script, base="1.0") - assert_installed(script, dep="1.0") + assert_installed(script, dep="2.0") def test_new_resolver_constraint_on_path(script): From 64b9eddfa4634fb29bb9e31aaa8f0901538f3a0f Mon Sep 17 00:00:00 2001 From: Hugo <hugovk@users.noreply.github.com> Date: Wed, 29 Apr 2020 18:22:35 +0300 Subject: [PATCH 1756/3170] Reference 'pip cache' in the 'pip install' caching section --- docs/html/reference/pip_install.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index 9d9b891a9ed..ec58899743a 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -565,7 +565,7 @@ While this cache attempts to minimize network activity, it does not prevent network access altogether. If you want a local install solution that circumvents accessing PyPI, see :ref:`Installing from local packages`. -The default location for the cache directory depends on the Operating System: +The default location for the cache directory depends on the operating system: Unix :file:`~/.cache/pip` and it respects the ``XDG_CACHE_HOME`` directory. @@ -574,6 +574,9 @@ macOS Windows :file:`<CSIDL_LOCAL_APPDATA>\\pip\\Cache` +Run ``pip cache dir`` to show the cache directory and see :ref:`pip cache` to +inspect and manage pip’s cache. + .. _`Wheel cache`: From c20f778899e635d81d85c9d2126913c955f9c0b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Fri, 1 May 2020 09:50:27 +0200 Subject: [PATCH 1757/3170] Do not unncessarily warn about wheel being absent Do not warn about the wheel package being absent if wheel building would have been skipped for another reason. --- src/pip/_internal/wheel_builder.py | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index 6d1022d5661..12ee3ab9031 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -69,14 +69,6 @@ def _should_build( # From this point, this concerns the pip install command only # (need_wheel=False). - if not req.use_pep517 and not is_wheel_installed(): - # we don't build legacy requirements if wheel is not installed - logger.info( - "Could not build wheels for %s, " - "since package 'wheel' is not installed.", req.name, - ) - return False - if req.editable or not req.source_dir: return False @@ -87,6 +79,14 @@ def _should_build( ) return False + if not req.use_pep517 and not is_wheel_installed(): + # we don't build legacy requirements if wheel is not installed + logger.info( + "Could not build wheels for %s, " + "since package 'wheel' is not installed.", req.name, + ) + return False + return True From d8c14d4006987190030afac33efba2d5c6a0219f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Fri, 1 May 2020 09:54:20 +0200 Subject: [PATCH 1758/3170] Clarify message when wheel is not installed --- src/pip/_internal/wheel_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index 12ee3ab9031..fcaeeb6c3f4 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -82,7 +82,7 @@ def _should_build( if not req.use_pep517 and not is_wheel_installed(): # we don't build legacy requirements if wheel is not installed logger.info( - "Could not build wheels for %s, " + "Using legacy setup.py install for %s, " "since package 'wheel' is not installed.", req.name, ) return False From 430ca847f39906a7080628f30165860e92adfc74 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Fri, 1 May 2020 09:56:28 +0200 Subject: [PATCH 1759/3170] Add news --- news/8178.bugfix | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 news/8178.bugfix diff --git a/news/8178.bugfix b/news/8178.bugfix new file mode 100644 index 00000000000..b68f477a232 --- /dev/null +++ b/news/8178.bugfix @@ -0,0 +1,2 @@ +Avoid unncessary message about the wheel package not being installed +when a wheel would not have been built. Additionally, clarify the message. From 55d602298998f933b65a1b16096ecf4c9a9e657a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@acsone.eu> Date: Fri, 1 May 2020 21:36:02 +0200 Subject: [PATCH 1760/3170] Update news/8178.bugfix Co-authored-by: Xavier Fernandez <xav.fernandez@gmail.com> --- news/8178.bugfix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/8178.bugfix b/news/8178.bugfix index b68f477a232..6960053eda2 100644 --- a/news/8178.bugfix +++ b/news/8178.bugfix @@ -1,2 +1,2 @@ -Avoid unncessary message about the wheel package not being installed +Avoid unnecessary message about the wheel package not being installed when a wheel would not have been built. Additionally, clarify the message. From b30dd1e04e1f37901733f1be0a5a1e02c466ad0c Mon Sep 17 00:00:00 2001 From: gutsytechster <prashantsharma161198@gmail.com> Date: Wed, 15 Apr 2020 19:54:48 +0530 Subject: [PATCH 1761/3170] fix(tests/unit): Update tests to be endian safe This updates `test_path_to_display` and `test_str_to_display__encoding` to use the endian safe expected result instead of the hardcoded one. This fixes https://github.com/pypa/pip/issues/7921 --- tests/unit/test_compat.py | 8 +++++++- tests/unit/test_utils.py | 16 +++++++++++++--- 2 files changed, 20 insertions(+), 4 deletions(-) diff --git a/tests/unit/test_compat.py b/tests/unit/test_compat.py index 1f31bc5ce81..b13087a1dd7 100644 --- a/tests/unit/test_compat.py +++ b/tests/unit/test_compat.py @@ -2,6 +2,7 @@ import locale import os +import sys import pytest @@ -91,8 +92,13 @@ def test_str_to_display__decode_error(monkeypatch, caplog): # Encode with an incompatible encoding. data = u'ab'.encode('utf-16') actual = str_to_display(data) + # Keep the expected value endian safe + if sys.byteorder == "little": + expected = "\\xff\\xfea\x00b\x00" + elif sys.byteorder == "big": + expected = "\\xfe\\xff\x00a\x00b" - assert actual == u'\\xff\\xfea\x00b\x00', ( + assert actual == expected, ( # Show the encoding for easier troubleshooting. 'encoding: {!r}'.format(locale.getpreferredencoding()) ) diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 7d74a664982..ebabd29e260 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -375,6 +375,18 @@ def test_rmtree_retries_for_3sec(tmpdir, monkeypatch): rmtree('foo') +if sys.byteorder == "little": + expected_byte_string = ( + u"b'\\xff\\xfe/\\x00p\\x00a\\x00t\\x00h\\x00/" + "\\x00d\\x00\\xe9\\x00f\\x00'" + ) +elif sys.byteorder == "big": + expected_byte_string = ( + u"b'\\xfe\\xff\\x00/\\x00p\\x00a\\x00t\\x00h\\" + "x00/\\x00d\\x00\\xe9\\x00f'" + ) + + @pytest.mark.parametrize('path, fs_encoding, expected', [ (None, None, None), # Test passing a text (unicode) string. @@ -383,9 +395,7 @@ def test_rmtree_retries_for_3sec(tmpdir, monkeypatch): (u'/path/déf'.encode('utf-8'), 'utf-8', u'/path/déf'), # Test a bytes object with a character that can't be decoded. (u'/path/déf'.encode('utf-8'), 'ascii', u"b'/path/d\\xc3\\xa9f'"), - (u'/path/déf'.encode('utf-16'), 'utf-8', - u"b'\\xff\\xfe/\\x00p\\x00a\\x00t\\x00h\\x00/" - "\\x00d\\x00\\xe9\\x00f\\x00'"), + (u'/path/déf'.encode('utf-16'), 'utf-8', expected_byte_string), ]) def test_path_to_display(monkeypatch, path, fs_encoding, expected): monkeypatch.setattr(sys, 'getfilesystemencoding', lambda: fs_encoding) From 8facc4cee107fa7727d819fa6f3ec980b85e0c6b Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 16 Apr 2020 15:05:12 +0800 Subject: [PATCH 1762/3170] Avoid RequirementSet before reaching the resolver The RequirementSet implementation conflates requirements incorrectly in a lot of places. This means the (new) resolver would get incomplete requirements. The removes all RequirementSet.add_requirement() calls outside of the legacy resolver, so the new resolver can get the unmodified list of requirements specified by the user, allowing for more sophisticated requirement merging. --- src/pip/_internal/cli/req_command.py | 15 +++++---------- src/pip/_internal/commands/install.py | 5 +---- src/pip/_internal/resolution/legacy/resolver.py | 2 +- 3 files changed, 7 insertions(+), 15 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 104b033281f..1bc59c175c7 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -25,7 +25,6 @@ install_req_from_req_string, ) from pip._internal.req.req_file import parse_requirements -from pip._internal.req.req_set import RequirementSet from pip._internal.self_outdated_check import ( make_link_collector, pip_self_version_check, @@ -296,15 +295,12 @@ def get_requirements( options, # type: Values finder, # type: PackageFinder session, # type: PipSession - check_supported_wheels=True, # type: bool ): # type: (...) -> List[InstallRequirement] """ Parse command-line arguments into the corresponding requirements. """ - requirement_set = RequirementSet( - check_supported_wheels=check_supported_wheels - ) + requirements = [] # type: List[InstallRequirement] for filename in options.constraints: for parsed_req in parse_requirements( filename, @@ -315,7 +311,7 @@ def get_requirements( isolated=options.isolated_mode, ) req_to_add.is_direct = True - requirement_set.add_requirement(req_to_add) + requirements.append(req_to_add) for req in args: req_to_add = install_req_from_line( @@ -323,7 +319,7 @@ def get_requirements( use_pep517=options.use_pep517, ) req_to_add.is_direct = True - requirement_set.add_requirement(req_to_add) + requirements.append(req_to_add) for req in options.editables: req_to_add = install_req_from_editable( @@ -332,7 +328,7 @@ def get_requirements( use_pep517=options.use_pep517, ) req_to_add.is_direct = True - requirement_set.add_requirement(req_to_add) + requirements.append(req_to_add) # NOTE: options.require_hashes may be set if --require-hashes is True for filename in options.requirements: @@ -345,10 +341,9 @@ def get_requirements( use_pep517=options.use_pep517 ) req_to_add.is_direct = True - requirement_set.add_requirement(req_to_add) + requirements.append(req_to_add) # If any requirement has hash options, enable hash checking. - requirements = requirement_set.all_requirements if any(req.has_hash_options for req in requirements): options.require_hashes = True diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 70bda2e2a92..c9b9ea4a8c5 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -297,10 +297,7 @@ def run(self, options, args): ) try: - reqs = self.get_requirements( - args, options, finder, session, - check_supported_wheels=not options.target_dir, - ) + reqs = self.get_requirements(args, options, finder, session) warn_deprecated_install_options( reqs, options.install_options diff --git a/src/pip/_internal/resolution/legacy/resolver.py b/src/pip/_internal/resolution/legacy/resolver.py index cdb44d19dbe..c83cd7a0b01 100644 --- a/src/pip/_internal/resolution/legacy/resolver.py +++ b/src/pip/_internal/resolution/legacy/resolver.py @@ -174,7 +174,7 @@ def resolve(self, root_reqs, check_supported_wheels): # based on link type. discovered_reqs = [] # type: List[InstallRequirement] hash_errors = HashErrors() - for req in chain(root_reqs, discovered_reqs): + for req in chain(requirement_set.all_requirements, discovered_reqs): try: discovered_reqs.extend(self._resolve_one(requirement_set, req)) except HashError as exc: From 647dc6e128c84c0bd8f31a3e9071800d594bda3a Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Mon, 4 May 2020 02:08:27 +0800 Subject: [PATCH 1763/3170] Include prereleases in specifier check --- src/pip/_internal/resolution/resolvelib/factory.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 5d66f2a23a5..b3cbb22f17b 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -161,9 +161,10 @@ def iter_found_candidates(self, ireq, extras): # Return installed distribution if it matches the specifier. This is # done last so the resolver will prefer it over downloading links. - if (installed_dist is not None and - not can_upgrade and - installed_dist.parsed_version in ireq.req.specifier): + if can_upgrade or installed_dist is None: + return + installed_version = installed_dist.parsed_version + if ireq.req.specifier.contains(installed_version, prereleases=True): yield self._make_candidate_from_dist( dist=installed_dist, extras=extras, From 6b34b39b7e6d277a05cc82a59e46b9de61af4f84 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Mon, 4 May 2020 02:19:12 +0800 Subject: [PATCH 1764/3170] Build ireq line from parent for installed dist --- .../_internal/resolution/resolvelib/candidates.py | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index c4772c33ff8..8322f6e29de 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -82,11 +82,15 @@ def make_install_req_from_editable(link, parent): def make_install_req_from_dist(dist, parent): # type: (Distribution, InstallRequirement) -> InstallRequirement + project_name = canonicalize_name(dist.project_name) + if parent.req: + line = str(parent.req) + elif parent.link: + line = "{} @ {}".format(project_name, parent.link.url) + else: + line = "{}=={}".format(project_name, dist.parsed_version) ireq = install_req_from_line( - "{}=={}".format( - canonicalize_name(dist.project_name), - dist.parsed_version, - ), + line, comes_from=parent.comes_from, use_pep517=parent.use_pep517, isolated=parent.isolated, From 1f32d8dfc6ed120cfa57ce112467141775e6df08 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 5 May 2020 18:40:10 +0800 Subject: [PATCH 1765/3170] Refine output check to accomodate specifier change --- tests/functional/test_new_resolver.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 56400b3c64f..a0d1a186e3c 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -357,7 +357,6 @@ def test_new_resolver_installed(script): "dep", "0.1.0", ) - satisfied_output = "Requirement already satisfied: base==0.1.0 in" result = script.pip( "install", "--unstable-feature=resolver", @@ -365,15 +364,16 @@ def test_new_resolver_installed(script): "--find-links", script.scratch_path, "base", ) - assert satisfied_output not in result.stdout, str(result) + assert "Requirement already satisfied" not in result.stdout, str(result) result = script.pip( "install", "--unstable-feature=resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, - "base", + "base>=0.1.0", ) - assert satisfied_output in result.stdout, str(result) + assert "Requirement already satisfied: base>=0.1.0" in result.stdout, \ + str(result) assert script.site_packages / "base" not in result.files_updated, ( "base 0.1.0 reinstalled" ) @@ -385,7 +385,7 @@ def test_new_resolver_ignore_installed(script): "base", "0.1.0", ) - satisfied_output = "Requirement already satisfied: base==0.1.0 in" + satisfied_output = "Requirement already satisfied" result = script.pip( "install", "--unstable-feature=resolver", From 9eb47650b0e378ff23292b44b4bb24c738fa12bf Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 5 May 2020 20:15:27 +0800 Subject: [PATCH 1766/3170] Please don't use shell=True :( --- tests/functional/test_new_resolver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index a0d1a186e3c..a43b84fff53 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -370,9 +370,9 @@ def test_new_resolver_installed(script): "install", "--unstable-feature=resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, - "base>=0.1.0", + "base~=0.1.0", ) - assert "Requirement already satisfied: base>=0.1.0" in result.stdout, \ + assert "Requirement already satisfied: base~=0.1.0" in result.stdout, \ str(result) assert script.site_packages / "base" not in result.files_updated, ( "base 0.1.0 reinstalled" From d8062791ddd31b842114699db178fb89cdee6d6c Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 8 Apr 2020 17:13:13 +0800 Subject: [PATCH 1767/3170] Implement use-user-site --- .../resolution/resolvelib/factory.py | 36 +++++++++++++++++-- .../resolution/resolvelib/resolver.py | 1 + tests/unit/resolution_resolvelib/conftest.py | 1 + 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 5d66f2a23a5..122585fbd4a 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -4,8 +4,13 @@ InstallationError, UnsupportedPythonVersion, ) -from pip._internal.utils.misc import get_installed_distributions +from pip._internal.utils.misc import ( + dist_in_site_packages, + dist_in_usersite, + get_installed_distributions, +) from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from pip._internal.utils.virtualenv import running_under_virtualenv from .candidates import ( AlreadyInstalledCandidate, @@ -49,6 +54,7 @@ def __init__( finder, # type: PackageFinder preparer, # type: RequirementPreparer make_install_req, # type: InstallRequirementProvider + use_user_site, # type: bool force_reinstall, # type: bool ignore_installed, # type: bool ignore_requires_python, # type: bool @@ -62,6 +68,7 @@ def __init__( self.preparer = preparer self._python_candidate = RequiresPythonCandidate(py_version_info) self._make_install_req_from_spec = make_install_req + self._use_user_site = use_user_site self._force_reinstall = force_reinstall self._ignore_requires_python = ignore_requires_python self._upgrade_strategy = upgrade_strategy @@ -199,7 +206,32 @@ def make_requires_python_requirement(self, specifier): def should_reinstall(self, candidate): # type: (Candidate) -> bool # TODO: Are there more cases this needs to return True? Editable? - return candidate.name in self._installed_dists + dist = self._installed_dists.get(candidate.name) + if dist is None: # Not installed, no uninstallation required. + return False + + # We're installing into global site. The current installation must + # be uninstalled, no matter it's in global or user site, because the + # user site installation has precedence over global. + if not self._use_user_site: + return True + + # We're installing into user site. Remove the user site installation. + if dist_in_usersite(dist): + return True + + # We're installing into user site, but the installed incompatible + # package is in global site. We can't uninstall that, and would let + # the new user installation to "shadow" it. But shadowing won't work + # in virtual environments, so we error out. + if running_under_virtualenv() and dist_in_site_packages(dist): + raise InstallationError( + "Will not install to the user site because it will " + "lack sys.path precedence to {} in {}".format( + dist.project_name, dist.location, + ) + ) + return False def _report_requires_python_error( self, diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index bb2e1ec4450..d2d682da61c 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -50,6 +50,7 @@ def __init__( finder=finder, preparer=preparer, make_install_req=make_install_req, + use_user_site=use_user_site, force_reinstall=force_reinstall, ignore_installed=ignore_installed, ignore_requires_python=ignore_requires_python, diff --git a/tests/unit/resolution_resolvelib/conftest.py b/tests/unit/resolution_resolvelib/conftest.py index 11278fa8594..6ac3b51ca3d 100644 --- a/tests/unit/resolution_resolvelib/conftest.py +++ b/tests/unit/resolution_resolvelib/conftest.py @@ -52,6 +52,7 @@ def factory(finder, preparer): finder=finder, preparer=preparer, make_install_req=install_req_from_line, + use_user_site=False, force_reinstall=False, ignore_installed=False, ignore_requires_python=False, From 6ab42a86b80c320b925d236fe52b643b6c7d1695 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 8 Apr 2020 22:53:15 +0800 Subject: [PATCH 1768/3170] Add tests for --user installs Tests are based on equivalents from test_install_user.py with modifications to appropriately monkey-patch things in the new resolver module. --- tests/functional/test_new_resolver_user.py | 223 +++++++++++++++++++++ 1 file changed, 223 insertions(+) create mode 100644 tests/functional/test_new_resolver_user.py diff --git a/tests/functional/test_new_resolver_user.py b/tests/functional/test_new_resolver_user.py new file mode 100644 index 00000000000..f5ff8df2436 --- /dev/null +++ b/tests/functional/test_new_resolver_user.py @@ -0,0 +1,223 @@ +import os +import textwrap + +import pytest + +from tests.lib import create_basic_wheel_for_package + + +@pytest.mark.incompatible_with_test_venv +def test_new_resolver_install_user(script): + create_basic_wheel_for_package(script, "base", "0.1.0") + result = script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "--user", + "base", + ) + assert script.user_site / "base" in result.files_created, str(result) + + +@pytest.mark.incompatible_with_test_venv +def test_new_resolver_install_user_satisfied_by_global_site(script): + """ + An install a matching version to user site should re-use a global site + installation if it satisfies. + """ + create_basic_wheel_for_package(script, "base", "1.0.0") + + script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "base==1.0.0", + ) + result = script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "--user", + "base==1.0.0", + ) + + assert script.user_site / "base" not in result.files_created, str(result) + + +@pytest.mark.incompatible_with_test_venv +def test_new_resolver_install_user_conflict_in_user_site(script): + """ + Installing a different version in user site should uninstall an existing + different version in user site. + """ + create_basic_wheel_for_package(script, "base", "1.0.0") + create_basic_wheel_for_package(script, "base", "2.0.0") + + script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "--user", + "base==2.0.0", + ) + + result = script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "--user", + "base==1.0.0", + ) + + base_1_dist_info = script.user_site / "base-1.0.0.dist-info" + base_2_dist_info = script.user_site / "base-2.0.0.dist-info" + + assert base_1_dist_info in result.files_created, str(result) + assert base_2_dist_info not in result.files_created, str(result) + + +@pytest.mark.incompatible_with_test_venv +def test_new_resolver_install_user_in_virtualenv_with_conflict_fails(script): + create_basic_wheel_for_package(script, "base", "1.0.0") + create_basic_wheel_for_package(script, "base", "2.0.0") + + script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "base==2.0.0", + ) + result = script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "--user", + "base==1.0.0", + expect_error=True, + ) + + error_message = ( + "Will not install to the user site because it will lack sys.path " + "precedence to base in {}" + ).format(os.path.normcase(script.site_packages_path)) + assert error_message in result.stderr + + +@pytest.fixture() +def patch_dist_in_site_packages(virtualenv): + # Since the tests are run from a virtualenv, and to avoid the "Will not + # install to the usersite because it will lack sys.path precedence..." + # error: Monkey patch `dist_in_site_packages` in the resolver module so + # it's possible to install a conflicting distribution in the user site. + virtualenv.sitecustomize = textwrap.dedent(""" + def dist_in_site_packages(dist): + return False + + from pip._internal.resolution.resolvelib import factory + factory.dist_in_site_packages = dist_in_site_packages + """) + + +@pytest.mark.incompatible_with_test_venv +@pytest.mark.usefixtures("patch_dist_in_site_packages") +def test_new_resolver_install_user_reinstall_global_site(script): + """ + Specifying --force-reinstall makes a different version in user site, + ignoring the matching installation in global site. + """ + create_basic_wheel_for_package(script, "base", "1.0.0") + + script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "base==1.0.0", + ) + result = script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "--user", + "--force-reinstall", + "base==1.0.0", + ) + + assert script.user_site / "base" in result.files_created, str(result) + + site_packages_content = set(os.listdir(script.site_packages_path)) + assert "base" in site_packages_content + + +@pytest.mark.incompatible_with_test_venv +@pytest.mark.usefixtures("patch_dist_in_site_packages") +def test_new_resolver_install_user_conflict_in_global_site(script): + """ + Installing a different version in user site should ignore an existing + different version in global site, and simply add to the user site. + """ + create_basic_wheel_for_package(script, "base", "1.0.0") + create_basic_wheel_for_package(script, "base", "2.0.0") + + script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "base==1.0.0", + ) + + result = script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "--user", + "base==2.0.0", + ) + + base_2_dist_info = script.user_site / "base-2.0.0.dist-info" + assert base_2_dist_info in result.files_created, str(result) + + site_packages_content = set(os.listdir(script.site_packages_path)) + assert "base-1.0.0.dist-info" in site_packages_content + + +@pytest.mark.incompatible_with_test_venv +@pytest.mark.usefixtures("patch_dist_in_site_packages") +def test_new_resolver_install_user_conflict_in_global_and_user_sites(script): + """ + Installing a different version in user site should ignore an existing + different version in global site, but still upgrade the user site. + """ + create_basic_wheel_for_package(script, "base", "1.0.0") + create_basic_wheel_for_package(script, "base", "2.0.0") + + script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "base==2.0.0", + ) + script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "--user", + "--force-reinstall", + "base==2.0.0", + ) + + result = script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "--user", + "base==1.0.0", + ) + + base_1_dist_info = script.user_site / "base-1.0.0.dist-info" + base_2_dist_info = script.user_site / "base-2.0.0.dist-info" + + assert base_1_dist_info in result.files_created, str(result) + assert base_2_dist_info in result.files_deleted, str(result) + + site_packages_content = set(os.listdir(script.site_packages_path)) + assert "base-2.0.0.dist-info" in site_packages_content From 4fa167e433fd18e69e36c44562247c2cdaec1758 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ville=20Skytt=C3=A4?= <ville.skytta@iki.fi> Date: Wed, 6 May 2020 08:25:35 +0300 Subject: [PATCH 1769/3170] Add Changelog project URL Background info at https://github.com/pypa/warehouse/pull/7882#issue-412444446 --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 9dfe108c0a4..2fdb4c50434 100644 --- a/setup.py +++ b/setup.py @@ -56,6 +56,7 @@ def get_version(rel_path): project_urls={ "Documentation": "https://pip.pypa.io", "Source": "https://github.com/pypa/pip", + "Changelog": "https://pip.pypa.io/en/stable/news/", }, author='The pip developers', From ee4830b5ecb5e45879a51710518528c85cb70ac1 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Tue, 5 May 2020 17:07:12 +0100 Subject: [PATCH 1770/3170] Move constraints from factory to provider --- .../_internal/resolution/resolvelib/base.py | 5 +++-- .../_internal/resolution/resolvelib/factory.py | 4 +--- .../resolution/resolvelib/provider.py | 9 +++++++-- .../resolution/resolvelib/requirements.py | 18 ++++++++---------- .../resolution/resolvelib/resolver.py | 4 +--- tests/unit/resolution_resolvelib/conftest.py | 1 + .../resolution_resolvelib/test_requirement.py | 4 ++-- 7 files changed, 23 insertions(+), 22 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/base.py b/src/pip/_internal/resolution/resolvelib/base.py index 5f99618ce95..c87b9da0625 100644 --- a/src/pip/_internal/resolution/resolvelib/base.py +++ b/src/pip/_internal/resolution/resolvelib/base.py @@ -6,6 +6,7 @@ from typing import Optional, Sequence, Set from pip._internal.req.req_install import InstallRequirement + from pip._vendor.packaging.specifiers import SpecifierSet from pip._vendor.packaging.version import _BaseVersion @@ -23,8 +24,8 @@ def name(self): # type: () -> str raise NotImplementedError("Subclass should override") - def find_matches(self): - # type: () -> Sequence[Candidate] + def find_matches(self, constraints): + # type: (Sequence[SpecifierSet]) -> Sequence[Candidate] raise NotImplementedError("Subclass should override") def is_satisfied_by(self, candidate): diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 0f69a14df45..12322228175 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -21,7 +21,7 @@ ) if MYPY_CHECK_RUNNING: - from typing import Dict, Iterator, List, Optional, Set, Tuple, TypeVar + from typing import Dict, Iterator, Optional, Set, Tuple, TypeVar from pip._vendor.packaging.specifiers import SpecifierSet from pip._vendor.packaging.version import _BaseVersion @@ -79,8 +79,6 @@ def __init__( else: self._installed_dists = {} - self._constraints = {} # type: Dict[str,List[SpecifierSet]] - def _make_candidate_from_dist( self, dist, # type: Distribution diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index 5c3d210a31a..0fe9b977134 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -3,7 +3,9 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Any, Optional, Sequence, Tuple, Union + from typing import Any, Dict, List, Optional, Sequence, Tuple, Union + + from pip._vendor.packaging.specifiers import SpecifierSet from pip._internal.req.req_install import InstallRequirement @@ -15,10 +17,12 @@ class PipProvider(AbstractProvider): def __init__( self, factory, # type: Factory + constraints, # type: Dict[str,List[SpecifierSet]] ignore_dependencies, # type: bool ): # type: (...) -> None self._factory = factory + self._constraints = constraints self._ignore_dependencies = ignore_dependencies def get_install_requirement(self, c): @@ -41,7 +45,8 @@ def get_preference( def find_matches(self, requirement): # type: (Requirement) -> Sequence[Candidate] - return requirement.find_matches() + constraints = self._constraints.get(requirement.name, []) + return requirement.find_matches(constraints) def is_satisfied_by(self, requirement, candidate): # type: (Requirement, Candidate) -> bool diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py index 1c4c54398b1..92ddd048007 100644 --- a/src/pip/_internal/resolution/resolvelib/requirements.py +++ b/src/pip/_internal/resolution/resolvelib/requirements.py @@ -35,9 +35,9 @@ def name(self): # No need to canonicalise - the candidate did this return self.candidate.name - def find_matches(self): - # type: () -> Sequence[Candidate] - if self.name in self._factory._constraints: + def find_matches(self, constraints): + # type: (Sequence[SpecifierSet]) -> Sequence[Candidate] + if constraints: raise InstallationError( "Could not satisfy constraints for '{}': " "installation from path or url cannot be " @@ -75,14 +75,12 @@ def name(self): canonical_name = canonicalize_name(self._ireq.req.name) return format_name(canonical_name, self.extras) - def find_matches(self): - # type: () -> Sequence[Candidate] + def find_matches(self, constraints): + # type: (Sequence[SpecifierSet]) -> Sequence[Candidate] it = self._factory.iter_found_candidates(self._ireq, self.extras) - constraints = self._factory._constraints.get(self.name, []) - lit = [c for c in it if all( + return [c for c in it if all( s.contains(c.version, prereleases=True) for s in constraints )] - return lit def is_satisfied_by(self, candidate): # type: (Candidate) -> bool @@ -116,8 +114,8 @@ def name(self): # type: () -> str return self._candidate.name - def find_matches(self): - # type: () -> Sequence[Candidate] + def find_matches(self, constraints): + # type: (Sequence[SpecifierSet]) -> Sequence[Candidate] if self._candidate.version in self.specifier: return [self._candidate] return [] diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 94d6046b689..86e2ab9531f 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -84,11 +84,9 @@ def resolve(self, root_reqs, check_supported_wheels): self.factory.make_requirement_from_install_req(req) ) - # TODO: Refactor this, it's just for proof of concept - self.factory._constraints = constraints - provider = PipProvider( factory=self.factory, + constraints=constraints, ignore_dependencies=self.ignore_dependencies, ) reporter = BaseReporter() diff --git a/tests/unit/resolution_resolvelib/conftest.py b/tests/unit/resolution_resolvelib/conftest.py index 11278fa8594..41c479bf350 100644 --- a/tests/unit/resolution_resolvelib/conftest.py +++ b/tests/unit/resolution_resolvelib/conftest.py @@ -64,5 +64,6 @@ def factory(finder, preparer): def provider(factory): yield PipProvider( factory=factory, + constraints={}, ignore_dependencies=False, ) diff --git a/tests/unit/resolution_resolvelib/test_requirement.py b/tests/unit/resolution_resolvelib/test_requirement.py index ad54df00b42..e25f44c4a1e 100644 --- a/tests/unit/resolution_resolvelib/test_requirement.py +++ b/tests/unit/resolution_resolvelib/test_requirement.py @@ -58,14 +58,14 @@ def test_new_resolver_correct_number_of_matches(test_cases, factory): """Requirements should return the correct number of candidates""" for spec, name, matches in test_cases: req = factory.make_requirement_from_spec(spec, comes_from=None) - assert len(req.find_matches()) == matches + assert len(req.find_matches([])) == matches def test_new_resolver_candidates_match_requirement(test_cases, factory): """Candidates returned from find_matches should satisfy the requirement""" for spec, name, matches in test_cases: req = factory.make_requirement_from_spec(spec, comes_from=None) - for c in req.find_matches(): + for c in req.find_matches([]): assert isinstance(c, Candidate) assert req.is_satisfied_by(c) From 64b3d1b278562e462d3ec66f0a8201c540722b28 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Wed, 6 May 2020 11:30:25 +0100 Subject: [PATCH 1771/3170] Merge constraints into a single SpecifierSet --- .../_internal/resolution/resolvelib/base.py | 4 +-- .../resolution/resolvelib/factory.py | 2 +- .../resolution/resolvelib/provider.py | 11 ++++---- .../resolution/resolvelib/requirements.py | 28 +++++++++++-------- .../resolution/resolvelib/resolver.py | 8 ++++-- .../resolution_resolvelib/test_requirement.py | 5 ++-- 6 files changed, 32 insertions(+), 26 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/base.py b/src/pip/_internal/resolution/resolvelib/base.py index c87b9da0625..3ccc48e1fea 100644 --- a/src/pip/_internal/resolution/resolvelib/base.py +++ b/src/pip/_internal/resolution/resolvelib/base.py @@ -24,8 +24,8 @@ def name(self): # type: () -> str raise NotImplementedError("Subclass should override") - def find_matches(self, constraints): - # type: (Sequence[SpecifierSet]) -> Sequence[Candidate] + def find_matches(self, constraint): + # type: (SpecifierSet) -> Sequence[Candidate] raise NotImplementedError("Subclass should override") def is_satisfied_by(self, candidate): diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 12322228175..5d66f2a23a5 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -182,7 +182,7 @@ def make_requirement_from_install_req(self, ireq): cand = self._make_candidate_from_link( ireq.link, extras=set(ireq.extras), parent=ireq, ) - return ExplicitRequirement(cand, factory=self) + return ExplicitRequirement(cand) return SpecifierRequirement(ireq, factory=self) def make_requirement_from_spec(self, specifier, comes_from): diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index 0fe9b977134..226dc3687d3 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -1,11 +1,10 @@ +from pip._vendor.packaging.specifiers import SpecifierSet from pip._vendor.resolvelib.providers import AbstractProvider from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Any, Dict, List, Optional, Sequence, Tuple, Union - - from pip._vendor.packaging.specifiers import SpecifierSet + from typing import Any, Dict, Optional, Sequence, Tuple, Union from pip._internal.req.req_install import InstallRequirement @@ -17,7 +16,7 @@ class PipProvider(AbstractProvider): def __init__( self, factory, # type: Factory - constraints, # type: Dict[str,List[SpecifierSet]] + constraints, # type: Dict[str, SpecifierSet] ignore_dependencies, # type: bool ): # type: (...) -> None @@ -45,8 +44,8 @@ def get_preference( def find_matches(self, requirement): # type: (Requirement) -> Sequence[Candidate] - constraints = self._constraints.get(requirement.name, []) - return requirement.find_matches(constraints) + constraint = self._constraints.get(requirement.name, SpecifierSet()) + return requirement.find_matches(constraint) def is_satisfied_by(self, requirement, candidate): # type: (Requirement, Candidate) -> bool diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py index 92ddd048007..3d089f708c4 100644 --- a/src/pip/_internal/resolution/resolvelib/requirements.py +++ b/src/pip/_internal/resolution/resolvelib/requirements.py @@ -17,10 +17,9 @@ class ExplicitRequirement(Requirement): - def __init__(self, candidate, factory): - # type: (Candidate, Factory) -> None + def __init__(self, candidate): + # type: (Candidate) -> None self.candidate = candidate - self._factory = factory def __repr__(self): # type: () -> str @@ -35,9 +34,9 @@ def name(self): # No need to canonicalise - the candidate did this return self.candidate.name - def find_matches(self, constraints): + def find_matches(self, constraint): # type: (Sequence[SpecifierSet]) -> Sequence[Candidate] - if constraints: + if len(constraint) > 0: raise InstallationError( "Could not satisfy constraints for '{}': " "installation from path or url cannot be " @@ -75,12 +74,15 @@ def name(self): canonical_name = canonicalize_name(self._ireq.req.name) return format_name(canonical_name, self.extras) - def find_matches(self, constraints): - # type: (Sequence[SpecifierSet]) -> Sequence[Candidate] - it = self._factory.iter_found_candidates(self._ireq, self.extras) - return [c for c in it if all( - s.contains(c.version, prereleases=True) for s in constraints - )] + def find_matches(self, constraint): + # type: (SpecifierSet) -> Sequence[Candidate] + return [ + c + for c in self._factory.iter_found_candidates( + self._ireq, self.extras + ) + if constraint.contains(c.version, prereleases=True) + ] def is_satisfied_by(self, candidate): # type: (Candidate) -> bool @@ -114,8 +116,10 @@ def name(self): # type: () -> str return self._candidate.name - def find_matches(self, constraints): + def find_matches(self, constraint): # type: (Sequence[SpecifierSet]) -> Sequence[Candidate] + assert len(constraint) == 0, \ + "RequiresPythonRequirement cannot have constraints" if self._candidate.version in self.specifier: return [self._candidate] return [] diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 86e2ab9531f..5c94d3dc057 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -1,6 +1,5 @@ import functools import logging -from collections import defaultdict from pip._vendor import six from pip._vendor.packaging.utils import canonicalize_name @@ -71,14 +70,17 @@ def resolve(self, root_reqs, check_supported_wheels): # The persistent state that we care about is `root_reqs`. assert len(self.factory.root_reqs) == 0, "Factory is being re-used" - constraints = defaultdict(list) # type: Dict[str,List[SpecifierSet]] + constraints = {} # type: Dict[str, SpecifierSet] requirements = [] for req in root_reqs: if req.constraint: assert req.name assert req.specifier name = canonicalize_name(req.name) - constraints[name].append(req.specifier) + if name in constraints: + constraints[name] = constraints[name] & req.specifier + else: + constraints[name] = req.specifier else: requirements.append( self.factory.make_requirement_from_install_req(req) diff --git a/tests/unit/resolution_resolvelib/test_requirement.py b/tests/unit/resolution_resolvelib/test_requirement.py index e25f44c4a1e..0b7dec02de2 100644 --- a/tests/unit/resolution_resolvelib/test_requirement.py +++ b/tests/unit/resolution_resolvelib/test_requirement.py @@ -1,4 +1,5 @@ import pytest +from pip._vendor.packaging.specifiers import SpecifierSet from pip._vendor.resolvelib import BaseReporter, Resolver from pip._internal.resolution.resolvelib.base import Candidate @@ -58,14 +59,14 @@ def test_new_resolver_correct_number_of_matches(test_cases, factory): """Requirements should return the correct number of candidates""" for spec, name, matches in test_cases: req = factory.make_requirement_from_spec(spec, comes_from=None) - assert len(req.find_matches([])) == matches + assert len(req.find_matches(SpecifierSet())) == matches def test_new_resolver_candidates_match_requirement(test_cases, factory): """Candidates returned from find_matches should satisfy the requirement""" for spec, name, matches in test_cases: req = factory.make_requirement_from_spec(spec, comes_from=None) - for c in req.find_matches([]): + for c in req.find_matches(SpecifierSet()): assert isinstance(c, Candidate) assert req.is_satisfied_by(c) From c7c319c4ed6b2dba3d07b49fdd367a09c906028b Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Thu, 7 May 2020 11:55:56 +0100 Subject: [PATCH 1772/3170] Fix type declarations. Mypy let me down :-( --- src/pip/_internal/resolution/resolvelib/requirements.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py index 3d089f708c4..c32187c6c5f 100644 --- a/src/pip/_internal/resolution/resolvelib/requirements.py +++ b/src/pip/_internal/resolution/resolvelib/requirements.py @@ -35,7 +35,7 @@ def name(self): return self.candidate.name def find_matches(self, constraint): - # type: (Sequence[SpecifierSet]) -> Sequence[Candidate] + # type: (SpecifierSet) -> Sequence[Candidate] if len(constraint) > 0: raise InstallationError( "Could not satisfy constraints for '{}': " @@ -117,7 +117,7 @@ def name(self): return self._candidate.name def find_matches(self, constraint): - # type: (Sequence[SpecifierSet]) -> Sequence[Candidate] + # type: (SpecifierSet) -> Sequence[Candidate] assert len(constraint) == 0, \ "RequiresPythonRequirement cannot have constraints" if self._candidate.version in self.specifier: From 1ea42caf1f4298125280c0fa1e0a5bcc7588abfd Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 5 May 2020 20:01:59 +0800 Subject: [PATCH 1773/3170] Allow prereleases in Requires-Python check --- src/pip/_internal/resolution/resolvelib/requirements.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py index c32187c6c5f..1ce447a33d4 100644 --- a/src/pip/_internal/resolution/resolvelib/requirements.py +++ b/src/pip/_internal/resolution/resolvelib/requirements.py @@ -120,7 +120,7 @@ def find_matches(self, constraint): # type: (SpecifierSet) -> Sequence[Candidate] assert len(constraint) == 0, \ "RequiresPythonRequirement cannot have constraints" - if self._candidate.version in self.specifier: + if self.specifier.contains(self._candidate.version, prereleases=True): return [self._candidate] return [] From d18ebcfe41c037bc21dcc3b92fc614034c17f3c1 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Wed, 29 Apr 2020 15:02:58 +0530 Subject: [PATCH 1774/3170] Drop parallelization in `pip list` --- src/pip/_internal/commands/list.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index cf3be7eb459..2aa3075b1b7 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -5,10 +5,8 @@ import json import logging -from multiprocessing.dummy import Pool from pip._vendor import six -from pip._vendor.requests.adapters import DEFAULT_POOLSIZE from pip._internal.cli import cmdoptions from pip._internal.cli.req_command import IndexGroupCommand @@ -210,18 +208,10 @@ def latest_info(dist): dist.latest_filetype = typ return dist - # This is done for 2x speed up of requests to pypi.org - # so that "real time" of this function - # is almost equal to "user time" - pool = Pool(DEFAULT_POOLSIZE) - - for dist in pool.imap_unordered(latest_info, packages): + for dist in map(latest_info, packages): if dist is not None: yield dist - pool.close() - pool.join() - def output_package_listing(self, packages, options): packages = sorted( packages, From d812fc232778444256552343003ec0030232ee40 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 7 May 2020 21:58:05 +0530 Subject: [PATCH 1775/3170] :newspaper: --- news/8167.removal | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/8167.removal diff --git a/news/8167.removal b/news/8167.removal new file mode 100644 index 00000000000..d719377eb26 --- /dev/null +++ b/news/8167.removal @@ -0,0 +1 @@ +Drop parallelization from ``pip list --outdated``. From 05d171f66cf4d5ad91a9a26db66551f151b5d696 Mon Sep 17 00:00:00 2001 From: Nicole Harris <n.harris@kabucreative.com> Date: Fri, 8 May 2020 13:22:53 +0100 Subject: [PATCH 1776/3170] Template for users to report resolver failures --- .github/ISSUE_TEMPLATE/resolver-failure.md | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 .github/ISSUE_TEMPLATE/resolver-failure.md diff --git a/.github/ISSUE_TEMPLATE/resolver-failure.md b/.github/ISSUE_TEMPLATE/resolver-failure.md new file mode 100644 index 00000000000..e7b159cf120 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/resolver-failure.md @@ -0,0 +1,20 @@ +--- +name: Pip resolver failure +about: Use this issue type to report when the pip dependency resolver fails +labels: ["K: UX", "K: crash"] + +--- + +<!-- + Please provide as much information as you can about your failure, so that we can understand the root cause. + + For example, if you are installing packages from pypi.org, we'd like to see: + + - Your terminal output + - Any inputs to pip, for example: + - any package requirements: any CLI arguments and/or your requirements.txt file + - any already installed packages, outputted via `pip freeze` + + For users installing packages from a private repository or local directory, please try your best to describe your setup. + We'd like to understand how to reproduce the error locally, so would need (at a minimum) a description of the packages you are trying to install, and a list of dependencies for each package. +--> From f478156063ce93263df41a358edb70646b0132d6 Mon Sep 17 00:00:00 2001 From: Nicole Harris <n.harris@kabucreative.com> Date: Sun, 10 May 2020 13:30:27 +0100 Subject: [PATCH 1777/3170] Update .github/ISSUE_TEMPLATE/resolver-failure.md Co-authored-by: Pradyun Gedam <pradyunsg@gmail.com> --- .github/ISSUE_TEMPLATE/resolver-failure.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/resolver-failure.md b/.github/ISSUE_TEMPLATE/resolver-failure.md index e7b159cf120..58b1dff8dd3 100644 --- a/.github/ISSUE_TEMPLATE/resolver-failure.md +++ b/.github/ISSUE_TEMPLATE/resolver-failure.md @@ -1,7 +1,7 @@ --- name: Pip resolver failure about: Use this issue type to report when the pip dependency resolver fails -labels: ["K: UX", "K: crash"] +labels: ["K: UX", "K: crash", "C: new resolver", "C: dependency resolution"] --- From c0c78e169da10f39650c3f0602d345b1503edcd8 Mon Sep 17 00:00:00 2001 From: Nicole Harris <n.harris@kabucreative.com> Date: Sun, 10 May 2020 13:31:05 +0100 Subject: [PATCH 1778/3170] Update .github/ISSUE_TEMPLATE/resolver-failure.md Co-authored-by: Pradyun Gedam <pradyunsg@gmail.com> --- .github/ISSUE_TEMPLATE/resolver-failure.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/resolver-failure.md b/.github/ISSUE_TEMPLATE/resolver-failure.md index 58b1dff8dd3..6fc45aa7023 100644 --- a/.github/ISSUE_TEMPLATE/resolver-failure.md +++ b/.github/ISSUE_TEMPLATE/resolver-failure.md @@ -1,5 +1,5 @@ --- -name: Pip resolver failure +name: Dependency resolver failures / errors about: Use this issue type to report when the pip dependency resolver fails labels: ["K: UX", "K: crash", "C: new resolver", "C: dependency resolution"] From 0c8e97f9a94713e072bdcd3643b4e1be45ea88ea Mon Sep 17 00:00:00 2001 From: Nicole Harris <n.harris@kabucreative.com> Date: Sun, 10 May 2020 13:34:44 +0100 Subject: [PATCH 1779/3170] Update resolver failure issue template Update "about" to be more consistent with other templates. Add link to pipdevtree. --- .github/ISSUE_TEMPLATE/resolver-failure.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/resolver-failure.md b/.github/ISSUE_TEMPLATE/resolver-failure.md index 6fc45aa7023..5fd3fe2f185 100644 --- a/.github/ISSUE_TEMPLATE/resolver-failure.md +++ b/.github/ISSUE_TEMPLATE/resolver-failure.md @@ -1,6 +1,6 @@ --- name: Dependency resolver failures / errors -about: Use this issue type to report when the pip dependency resolver fails +about: Report when the pip dependency resolver fails labels: ["K: UX", "K: crash", "C: new resolver", "C: dependency resolution"] --- @@ -14,7 +14,8 @@ labels: ["K: UX", "K: crash", "C: new resolver", "C: dependency resolution"] - Any inputs to pip, for example: - any package requirements: any CLI arguments and/or your requirements.txt file - any already installed packages, outputted via `pip freeze` + + It would be great if you could also include your dependency tree. For this you can use pipdeptree: https://pypi.org/project/pipdeptree/ - For users installing packages from a private repository or local directory, please try your best to describe your setup. - We'd like to understand how to reproduce the error locally, so would need (at a minimum) a description of the packages you are trying to install, and a list of dependencies for each package. + For users installing packages from a private repository or local directory, please try your best to describe your setup. We'd like to understand how to reproduce the error locally, so would need (at a minimum) a description of the packages you are trying to install, and a list of dependencies for each package. --> From ab509f4fe036a22adea83f0cb1b001b363e1529a Mon Sep 17 00:00:00 2001 From: Nicole Harris <n.harris@kabucreative.com> Date: Mon, 11 May 2020 09:55:50 +0100 Subject: [PATCH 1780/3170] Add news --- news/8207.doc | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/8207.doc diff --git a/news/8207.doc b/news/8207.doc new file mode 100644 index 00000000000..a9cf944c62a --- /dev/null +++ b/news/8207.doc @@ -0,0 +1 @@ +Add GitHub issue template for reporting when the dependency resolver fails From 1e4bfe79488153252747d834934730af1bd5fa54 Mon Sep 17 00:00:00 2001 From: Nicole Harris <n.harris@kabucreative.com> Date: Mon, 11 May 2020 09:59:31 +0100 Subject: [PATCH 1781/3170] Fix linting? --- .github/ISSUE_TEMPLATE/resolver-failure.md | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/resolver-failure.md b/.github/ISSUE_TEMPLATE/resolver-failure.md index 5fd3fe2f185..2e719b51b5e 100644 --- a/.github/ISSUE_TEMPLATE/resolver-failure.md +++ b/.github/ISSUE_TEMPLATE/resolver-failure.md @@ -2,20 +2,19 @@ name: Dependency resolver failures / errors about: Report when the pip dependency resolver fails labels: ["K: UX", "K: crash", "C: new resolver", "C: dependency resolution"] - --- -<!-- +<!-- Please provide as much information as you can about your failure, so that we can understand the root cause. - + For example, if you are installing packages from pypi.org, we'd like to see: - Your terminal output - Any inputs to pip, for example: - any package requirements: any CLI arguments and/or your requirements.txt file - any already installed packages, outputted via `pip freeze` - + It would be great if you could also include your dependency tree. For this you can use pipdeptree: https://pypi.org/project/pipdeptree/ - - For users installing packages from a private repository or local directory, please try your best to describe your setup. We'd like to understand how to reproduce the error locally, so would need (at a minimum) a description of the packages you are trying to install, and a list of dependencies for each package. + + For users installing packages from a private repository or local directory, please try your best to describe your setup. We'd like to understand how to reproduce the error locally, so would need (at a minimum) a description of the packages you are trying to install, and a list of dependencies for each package. --> From 608530755f3b63ce24a1110ed13e67d6cb673434 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 12 May 2020 18:42:33 +0530 Subject: [PATCH 1782/3170] Make mypy pretty --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index c3c944ff8f3..1ff550bbed8 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,11 +29,11 @@ repos: hooks: - id: mypy exclude: docs|tests - args: [] + args: ["--pretty"] - id: mypy - name: mypy, for Py2 + name: mypy, for Python 2 exclude: noxfile.py|tools/automation/release|docs|tests - args: ["-2"] + args: ["--pretty", "-2"] - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.4.1 From e078531da2a894c4edf829320a6bfeff303200f7 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 12 May 2020 18:40:12 +0530 Subject: [PATCH 1783/3170] Switch to upstream flake8 --- .pre-commit-config.yaml | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 1ff550bbed8..e32c87ebec5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -12,12 +12,16 @@ repos: - id: debug-statements - id: end-of-file-fixer exclude: WHEEL - - id: flake8 - exclude: tests/data - id: forbid-new-submodules - id: trailing-whitespace exclude: .patch +- repo: https://gitlab.com/pycqa/flake8 + rev: 3.7.9 + hooks: + - id: flake8 + exclude: tests/data + - repo: https://github.com/timothycrosley/isort rev: 4.3.21 hooks: From cb3f7babed2486692099de00114a50c18c5aa5d7 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 12 May 2020 18:43:13 +0530 Subject: [PATCH 1784/3170] Bump linters --- .pre-commit-config.yaml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index e32c87ebec5..89430962cab 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ exclude: 'src/pip/_vendor/' repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.3.0 + rev: v2.5.0 hooks: - id: check-builtin-literals - id: check-added-large-files @@ -29,7 +29,7 @@ repos: files: \.py$ - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.750 + rev: v0.760 hooks: - id: mypy exclude: docs|tests @@ -40,7 +40,7 @@ repos: args: ["--pretty", "-2"] - repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.4.1 + rev: v1.5.1 hooks: - id: python-no-log-warn - id: python-no-eval @@ -52,6 +52,6 @@ repos: exclude: NEWS.rst - repo: https://github.com/mgedmin/check-manifest - rev: '0.39' + rev: '0.42' hooks: - id: check-manifest From 4297b8d9b7436cd949682f7cda66cff1d9a4cef9 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 12 May 2020 05:25:09 +0530 Subject: [PATCH 1785/3170] Revert "fix test_uninstall_console_scripts" This reverts commit 877e1ccc7776548918537c17630decb4f87f665f. --- tests/functional/test_uninstall.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/tests/functional/test_uninstall.py b/tests/functional/test_uninstall.py index c030dbaf27b..ab41917c986 100644 --- a/tests/functional/test_uninstall.py +++ b/tests/functional/test_uninstall.py @@ -271,15 +271,7 @@ def test_uninstall_console_scripts(script): sorted(result.files_created.keys()) ) result2 = script.pip('uninstall', 'discover', '-y') - assert_all_changes( - result, - result2, - [ - script.venv / 'build', - 'cache', - script.scratch / 'discover' / 'discover.egg-info', - ] - ) + assert_all_changes(result, result2, [script.venv / 'build', 'cache']) def test_uninstall_console_scripts_uppercase_name(script): From c0641464dfc8f82091d980cd0b022febe735bebb Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 12 May 2020 05:25:18 +0530 Subject: [PATCH 1786/3170] Revert "fix test_entrypoints_work test" This reverts commit ace0c1653121770a58ae3cccf9059227702edde5. --- tests/functional/test_cli.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/functional/test_cli.py b/tests/functional/test_cli.py index c401a7cf80f..e416315125f 100644 --- a/tests/functional/test_cli.py +++ b/tests/functional/test_cli.py @@ -27,9 +27,7 @@ def test_entrypoints_work(entrypoint, script): ) """.format(entrypoint))) - # expect_temp=True, because pip install calls setup.py which - # in turn creates fake_pkg.egg-info. - script.pip("install", "-vvv", str(fake_pkg), expect_temp=True) + script.pip("install", "-vvv", str(fake_pkg)) result = script.pip("-V") result2 = script.run("fake_pip", "-V", allow_stderr_warning=True) assert result.stdout == result2.stdout From 1b90e1bccf4c5556e3b78ff980cb67f306bbca48 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 12 May 2020 05:23:08 +0530 Subject: [PATCH 1787/3170] Revert "remove _copy_source_tree and friends" This reverts commit 873f1e6332aa827c886266c7d858067c12521e80. --- src/pip/_internal/operations/prepare.py | 56 ++++++++++++++++- src/pip/_internal/utils/filesystem.py | 32 ++++++++++ tests/functional/test_install.py | 26 ++++++++ tests/lib/filesystem.py | 48 +++++++++++++++ tests/unit/test_operations_prepare.py | 81 ++++++++++++++++++++++++- tests/unit/test_utils_filesystem.py | 61 +++++++++++++++++++ 6 files changed, 302 insertions(+), 2 deletions(-) create mode 100644 tests/lib/filesystem.py create mode 100644 tests/unit/test_utils_filesystem.py diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 30d5e3a308c..3817323bdb8 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -24,9 +24,10 @@ PreviousBuildDirError, VcsHashUnsupported, ) +from pip._internal.utils.filesystem import copy2_fixed from pip._internal.utils.hashes import MissingHashes from pip._internal.utils.logging import indent_log -from pip._internal.utils.misc import display_path, hide_url +from pip._internal.utils.misc import display_path, hide_url, path_to_display from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.unpacking import unpack_file @@ -127,6 +128,59 @@ def get_http_url( return File(from_path, content_type) +def _copy2_ignoring_special_files(src, dest): + # type: (str, str) -> None + """Copying special files is not supported, but as a convenience to users + we skip errors copying them. This supports tools that may create e.g. + socket files in the project source directory. + """ + try: + copy2_fixed(src, dest) + except shutil.SpecialFileError as e: + # SpecialFileError may be raised due to either the source or + # destination. If the destination was the cause then we would actually + # care, but since the destination directory is deleted prior to + # copy we ignore all of them assuming it is caused by the source. + logger.warning( + "Ignoring special file error '%s' encountered copying %s to %s.", + str(e), + path_to_display(src), + path_to_display(dest), + ) + + +def _copy_source_tree(source, target): + # type: (str, str) -> None + target_abspath = os.path.abspath(target) + target_basename = os.path.basename(target_abspath) + target_dirname = os.path.dirname(target_abspath) + + def ignore(d, names): + # type: (str, List[str]) -> List[str] + skipped = [] # type: List[str] + if d == source: + # Pulling in those directories can potentially be very slow, + # exclude the following directories if they appear in the top + # level dir (and only it). + # See discussion at https://github.com/pypa/pip/pull/6770 + skipped += ['.tox', '.nox'] + if os.path.abspath(d) == target_dirname: + # Prevent an infinite recursion if the target is in source. + # This can happen when TMPDIR is set to ${PWD}/... + # and we copy PWD to TMPDIR. + skipped += [target_basename] + return skipped + + kwargs = dict(ignore=ignore, symlinks=True) # type: CopytreeKwargs + + if not PY2: + # Python 2 does not support copy_function, so we only ignore + # errors on special file copy in Python 3. + kwargs['copy_function'] = _copy2_ignoring_special_files + + shutil.copytree(source, target, **kwargs) + + def get_file_url( link, # type: Link download_dir=None, # type: Optional[str] diff --git a/src/pip/_internal/utils/filesystem.py b/src/pip/_internal/utils/filesystem.py index 85010ac25a3..437a7fd1482 100644 --- a/src/pip/_internal/utils/filesystem.py +++ b/src/pip/_internal/utils/filesystem.py @@ -3,6 +3,8 @@ import os import os.path import random +import shutil +import stat import sys from contextlib import contextmanager from tempfile import NamedTemporaryFile @@ -54,6 +56,36 @@ def check_path_owner(path): return False # assume we don't own the path +def copy2_fixed(src, dest): + # type: (str, str) -> None + """Wrap shutil.copy2() but map errors copying socket files to + SpecialFileError as expected. + + See also https://bugs.python.org/issue37700. + """ + try: + shutil.copy2(src, dest) + except (OSError, IOError): + for f in [src, dest]: + try: + is_socket_file = is_socket(f) + except OSError: + # An error has already occurred. Another error here is not + # a problem and we can ignore it. + pass + else: + if is_socket_file: + raise shutil.SpecialFileError( + "`{f}` is a socket".format(**locals())) + + raise + + +def is_socket(path): + # type: (str) -> bool + return stat.S_ISSOCK(os.lstat(path).st_mode) + + @contextmanager def adjacent_tmp_file(path, **kwargs): # type: (str, **Any) -> Iterator[NamedTemporaryFileResult] diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 571049aa447..2cb44269502 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -2,6 +2,7 @@ import glob import os import re +import shutil import ssl import sys import textwrap @@ -28,6 +29,7 @@ skip_if_python2, windows_workaround_7667, ) +from tests.lib.filesystem import make_socket_file from tests.lib.local_repos import local_checkout from tests.lib.path import Path from tests.lib.server import ( @@ -574,6 +576,30 @@ def test_install_from_local_directory_with_symlinks_to_directories( assert egg_info_folder in result.files_created, str(result) +@pytest.mark.skipif("sys.platform == 'win32' or sys.version_info < (3,)") +def test_install_from_local_directory_with_socket_file(script, data, tmpdir): + """ + Test installing from a local directory containing a socket file. + """ + egg_info_file = ( + script.site_packages / + "FSPkg-0.1.dev0-py{pyversion}.egg-info".format(**globals()) + ) + package_folder = script.site_packages / "fspkg" + to_copy = data.packages.joinpath("FSPkg") + to_install = tmpdir.joinpath("src") + + shutil.copytree(to_copy, to_install) + # Socket file, should be ignored. + socket_file_path = os.path.join(to_install, "example") + make_socket_file(socket_file_path) + + result = script.pip("install", "--verbose", to_install) + assert package_folder in result.files_created, str(result.stdout) + assert egg_info_file in result.files_created, str(result) + assert str(socket_file_path) in result.stderr + + def test_install_from_local_directory_with_no_setup_py(script, data): """ Test installing from a local directory with no 'setup.py'. diff --git a/tests/lib/filesystem.py b/tests/lib/filesystem.py new file mode 100644 index 00000000000..dc14b323e33 --- /dev/null +++ b/tests/lib/filesystem.py @@ -0,0 +1,48 @@ +"""Helpers for filesystem-dependent tests. +""" +import os +import socket +import subprocess +import sys +from functools import partial +from itertools import chain + +from .path import Path + + +def make_socket_file(path): + # Socket paths are limited to 108 characters (sometimes less) so we + # chdir before creating it and use a relative path name. + cwd = os.getcwd() + os.chdir(os.path.dirname(path)) + try: + sock = socket.socket(socket.AF_UNIX) + sock.bind(os.path.basename(path)) + finally: + os.chdir(cwd) + + +def make_unreadable_file(path): + Path(path).touch() + os.chmod(path, 0o000) + if sys.platform == "win32": + # Once we drop PY2 we can use `os.getlogin()` instead. + username = os.environ["USERNAME"] + # Remove "Read Data/List Directory" permission for current user, but + # leave everything else. + args = ["icacls", path, "/deny", username + ":(RD)"] + subprocess.check_call(args) + + +def get_filelist(base): + def join(dirpath, dirnames, filenames): + relative_dirpath = os.path.relpath(dirpath, base) + join_dirpath = partial(os.path.join, relative_dirpath) + return chain( + (join_dirpath(p) for p in dirnames), + (join_dirpath(p) for p in filenames), + ) + + return set(chain.from_iterable( + join(*dirinfo) for dirinfo in os.walk(base) + )) diff --git a/tests/unit/test_operations_prepare.py b/tests/unit/test_operations_prepare.py index bcfc8148669..3df5429189a 100644 --- a/tests/unit/test_operations_prepare.py +++ b/tests/unit/test_operations_prepare.py @@ -10,9 +10,18 @@ from pip._internal.models.link import Link from pip._internal.network.download import Downloader from pip._internal.network.session import PipSession -from pip._internal.operations.prepare import _download_http_url, unpack_url +from pip._internal.operations.prepare import ( + _copy_source_tree, + _download_http_url, + unpack_url, +) from pip._internal.utils.hashes import Hashes from pip._internal.utils.urls import path_to_url +from tests.lib.filesystem import ( + get_filelist, + make_socket_file, + make_unreadable_file, +) from tests.lib.path import Path from tests.lib.requests_mocks import MockResponse @@ -92,6 +101,76 @@ def clean_project(tmpdir_factory, data): return new_project_dir +def test_copy_source_tree(clean_project, tmpdir): + target = tmpdir.joinpath("target") + expected_files = get_filelist(clean_project) + assert len(expected_files) == 3 + + _copy_source_tree(clean_project, target) + + copied_files = get_filelist(target) + assert expected_files == copied_files + + +@pytest.mark.skipif("sys.platform == 'win32' or sys.version_info < (3,)") +def test_copy_source_tree_with_socket(clean_project, tmpdir, caplog): + target = tmpdir.joinpath("target") + expected_files = get_filelist(clean_project) + socket_path = str(clean_project.joinpath("aaa")) + make_socket_file(socket_path) + + _copy_source_tree(clean_project, target) + + copied_files = get_filelist(target) + assert expected_files == copied_files + + # Warning should have been logged. + assert len(caplog.records) == 1 + record = caplog.records[0] + assert record.levelname == 'WARNING' + assert socket_path in record.message + + +@pytest.mark.skipif("sys.platform == 'win32' or sys.version_info < (3,)") +def test_copy_source_tree_with_socket_fails_with_no_socket_error( + clean_project, tmpdir +): + target = tmpdir.joinpath("target") + expected_files = get_filelist(clean_project) + make_socket_file(clean_project.joinpath("aaa")) + unreadable_file = clean_project.joinpath("bbb") + make_unreadable_file(unreadable_file) + + with pytest.raises(shutil.Error) as e: + _copy_source_tree(clean_project, target) + + errored_files = [err[0] for err in e.value.args[0]] + assert len(errored_files) == 1 + assert unreadable_file in errored_files + + copied_files = get_filelist(target) + # All files without errors should have been copied. + assert expected_files == copied_files + + +def test_copy_source_tree_with_unreadable_dir_fails(clean_project, tmpdir): + target = tmpdir.joinpath("target") + expected_files = get_filelist(clean_project) + unreadable_file = clean_project.joinpath("bbb") + make_unreadable_file(unreadable_file) + + with pytest.raises(shutil.Error) as e: + _copy_source_tree(clean_project, target) + + errored_files = [err[0] for err in e.value.args[0]] + assert len(errored_files) == 1 + assert unreadable_file in errored_files + + copied_files = get_filelist(target) + # All files without errors should have been copied. + assert expected_files == copied_files + + class Test_unpack_url(object): def prep(self, tmpdir, data): diff --git a/tests/unit/test_utils_filesystem.py b/tests/unit/test_utils_filesystem.py new file mode 100644 index 00000000000..3ef814dce4b --- /dev/null +++ b/tests/unit/test_utils_filesystem.py @@ -0,0 +1,61 @@ +import os +import shutil + +import pytest + +from pip._internal.utils.filesystem import copy2_fixed, is_socket +from tests.lib.filesystem import make_socket_file, make_unreadable_file +from tests.lib.path import Path + + +def make_file(path): + Path(path).touch() + + +def make_valid_symlink(path): + target = path + "1" + make_file(target) + os.symlink(target, path) + + +def make_broken_symlink(path): + os.symlink("foo", path) + + +def make_dir(path): + os.mkdir(path) + + +skip_on_windows = pytest.mark.skipif("sys.platform == 'win32'") + + +@skip_on_windows +@pytest.mark.parametrize("create,result", [ + (make_socket_file, True), + (make_file, False), + (make_valid_symlink, False), + (make_broken_symlink, False), + (make_dir, False), +]) +def test_is_socket(create, result, tmpdir): + target = tmpdir.joinpath("target") + create(target) + assert os.path.lexists(target) + assert is_socket(target) == result + + +@pytest.mark.parametrize("create,error_type", [ + pytest.param( + make_socket_file, shutil.SpecialFileError, marks=skip_on_windows + ), + (make_unreadable_file, OSError), +]) +def test_copy2_fixed_raises_appropriate_errors(create, error_type, tmpdir): + src = tmpdir.joinpath("src") + create(src) + dest = tmpdir.joinpath("dest") + + with pytest.raises(error_type): + copy2_fixed(src, dest) + + assert not dest.exists() From 0bd624275dd86b971ba32a16f070c04febbe2c7d Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 12 May 2020 05:24:28 +0530 Subject: [PATCH 1788/3170] Revert "build in place" This reverts commit 88e6e6bc5c877bfb15cce6f622dfbf9221c7b479. --- docs/html/reference/pip_install.rst | 17 +++------ src/pip/_internal/operations/prepare.py | 47 +++++++++++++------------ tests/unit/test_operations_prepare.py | 39 ++++++++++++++++++-- 3 files changed, 67 insertions(+), 36 deletions(-) diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index ec58899743a..294e969f3e1 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -732,18 +732,11 @@ You can install local projects by specifying the project path to pip:: $ pip install path/to/SomeProject -pip treats this directory like an unpacked source archive, and directly -attempts installation. - -Prior to pip 20.1, pip copied the entire project directory to a temporary -location and attempted installation from that directory. This approach was the -cause of several performance issues, as well as various issues arising when the -project directory depends on its parent directory (such as the presence of a -VCS directory). The main user visible effect of this change is that secondary -build artifacts, if any, would be created in the local directory, whereas -earlier they were created in a temporary copy of the directory and then -deleted. This notably includes the ``build`` and ``.egg-info`` directories in -the case of the setuptools backend. +During regular installation, pip will copy the entire project directory to a +temporary location and install from there. The exception is that pip will +exclude .tox and .nox directories present in the top level of the project from +being copied. + .. _`editable-installs`: diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 3817323bdb8..1fcbb775ece 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -27,7 +27,12 @@ from pip._internal.utils.filesystem import copy2_fixed from pip._internal.utils.hashes import MissingHashes from pip._internal.utils.logging import indent_log -from pip._internal.utils.misc import display_path, hide_url, path_to_display +from pip._internal.utils.misc import ( + display_path, + hide_url, + path_to_display, + rmtree, +) from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.unpacking import unpack_file @@ -234,9 +239,11 @@ def unpack_url( unpack_vcs_link(link, location) return None - # If it's a url to a local directory, we build in-place. - # There is nothing to be done here. + # If it's a url to a local directory if link.is_existing_dir(): + if os.path.isdir(location): + rmtree(location) + _copy_source_tree(link.file_path, location) return None # file urls @@ -408,25 +415,21 @@ def prepare_linked_requirement( with indent_log(): # Since source_dir is only set for editable requirements. assert req.source_dir is None - if link.is_existing_dir(): - # Build local directories in place. - req.source_dir = link.file_path - else: - req.ensure_has_source_dir(self.build_dir, autodelete_unpacked) - # 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` - if os.path.exists(os.path.join(req.source_dir, 'setup.py')): - raise PreviousBuildDirError( - "pip can't proceed with requirements '{}' due to a" - " pre-existing build directory ({}). 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." - .format(req, req.source_dir) - ) + req.ensure_has_source_dir(self.build_dir, autodelete_unpacked) + # 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` + if os.path.exists(os.path.join(req.source_dir, 'setup.py')): + raise PreviousBuildDirError( + "pip can't proceed with requirements '{}' due to a" + " pre-existing build directory ({}). 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." + .format(req, req.source_dir) + ) # Now that we have the real link, we can tell what kind of # requirements we have and raise some more informative errors diff --git a/tests/unit/test_operations_prepare.py b/tests/unit/test_operations_prepare.py index 3df5429189a..0158eed5197 100644 --- a/tests/unit/test_operations_prepare.py +++ b/tests/unit/test_operations_prepare.py @@ -214,5 +214,40 @@ def test_unpack_url_thats_a_dir(self, tmpdir, data): unpack_url(dist_url, self.build_dir, downloader=self.no_downloader, download_dir=self.download_dir) - # test that nothing was copied to build_dir since we build in place - assert not os.path.exists(os.path.join(self.build_dir, 'fspkg')) + assert os.path.isdir(os.path.join(self.build_dir, 'fspkg')) + + +@pytest.mark.parametrize('exclude_dir', [ + '.nox', + '.tox' +]) +def test_unpack_url_excludes_expected_dirs(tmpdir, exclude_dir): + src_dir = tmpdir / 'src' + dst_dir = tmpdir / 'dst' + src_included_file = src_dir.joinpath('file.txt') + src_excluded_dir = src_dir.joinpath(exclude_dir) + src_excluded_file = src_dir.joinpath(exclude_dir, 'file.txt') + src_included_dir = src_dir.joinpath('subdir', exclude_dir) + + # set up source directory + src_excluded_dir.mkdir(parents=True) + src_included_dir.mkdir(parents=True) + src_included_file.touch() + src_excluded_file.touch() + + dst_included_file = dst_dir.joinpath('file.txt') + dst_excluded_dir = dst_dir.joinpath(exclude_dir) + dst_excluded_file = dst_dir.joinpath(exclude_dir, 'file.txt') + dst_included_dir = dst_dir.joinpath('subdir', exclude_dir) + + src_link = Link(path_to_url(src_dir)) + unpack_url( + src_link, + dst_dir, + Mock(side_effect=AssertionError), + download_dir=None + ) + assert not os.path.isdir(dst_excluded_dir) + assert not os.path.isfile(dst_excluded_file) + assert os.path.isfile(dst_included_file) + assert os.path.isdir(dst_included_dir) From 6ed32347afbad240279115c442fcdce35b8514d9 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 12 May 2020 05:28:23 +0530 Subject: [PATCH 1789/3170] :newspaper: --- news/7555.bugfix | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 news/7555.bugfix diff --git a/news/7555.bugfix b/news/7555.bugfix new file mode 100644 index 00000000000..f762236e235 --- /dev/null +++ b/news/7555.bugfix @@ -0,0 +1,2 @@ +Revert building of local directories in place, restoring the pre-20.1 +behaviour of copying to a temporary directory. From 713645a5548f9a045885fa3431dd2286caa1c729 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 13 May 2020 07:13:19 +0800 Subject: [PATCH 1790/3170] Add one more constraint test --- tests/functional/test_new_resolver.py | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index bae225ec22c..487f2693625 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -531,12 +531,21 @@ def test_new_resolver_handles_prerelease( assert_installed(script, pkg=expected_version) -def test_new_resolver_constraints(script): +@pytest.mark.parametrize( + "constraints", + [ + ["pkg<2.0", "constraint_only<1.0"], + # This also tests the pkg constraint don't get merged with the + # requirement prematurely. (pypa/pip#8134) + ["pkg<2.0"], + ] +) +def test_new_resolver_constraints(script, constraints): create_basic_wheel_for_package(script, "pkg", "1.0") create_basic_wheel_for_package(script, "pkg", "2.0") create_basic_wheel_for_package(script, "pkg", "3.0") constraints_file = script.scratch_path / "constraints.txt" - constraints_file.write_text("pkg<2.0\nconstraint_only<1.0") + constraints_file.write_text("\n".join(constraints)) script.pip( "install", "--unstable-feature=resolver", "--no-cache-dir", "--no-index", From 89f248c7bfdae65c911e4efc5398823dc5d54c28 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sun, 29 Mar 2020 21:35:22 +0530 Subject: [PATCH 1791/3170] Removing type ignore comments from cli.progress_bars --- ...770AC380-E84F-44C7-A20C-CD31A829EDA5.trivial | 1 + src/pip/_internal/cli/progress_bars.py | 17 +++++++++-------- 2 files changed, 10 insertions(+), 8 deletions(-) create mode 100644 news/770AC380-E84F-44C7-A20C-CD31A829EDA5.trivial diff --git a/news/770AC380-E84F-44C7-A20C-CD31A829EDA5.trivial b/news/770AC380-E84F-44C7-A20C-CD31A829EDA5.trivial new file mode 100644 index 00000000000..5e5d3c4f01d --- /dev/null +++ b/news/770AC380-E84F-44C7-A20C-CD31A829EDA5.trivial @@ -0,0 +1 @@ +Remove "type: ignore" comments from cli subpackage diff --git a/src/pip/_internal/cli/progress_bars.py b/src/pip/_internal/cli/progress_bars.py index 7ed224790cf..9a4ae592e7c 100644 --- a/src/pip/_internal/cli/progress_bars.py +++ b/src/pip/_internal/cli/progress_bars.py @@ -78,6 +78,7 @@ def __init__(self, *args, **kwargs): """ Save the original SIGINT handler for later. """ + # https://github.com/python/mypy/issues/5887 super(InterruptibleMixin, self).__init__( # type: ignore *args, **kwargs @@ -134,6 +135,7 @@ class DownloadProgressMixin(object): def __init__(self, *args, **kwargs): # type: (List[Any], Dict[Any, Any]) -> None + # https://github.com/python/mypy/issues/5887 super(DownloadProgressMixin, self).__init__( # type: ignore *args, **kwargs @@ -183,6 +185,7 @@ def __init__(self, *args, **kwargs): if WINDOWS and self.hide_cursor: # type: ignore self.hide_cursor = False + # https://github.com/python/mypy/issues/5887 super(WindowsMixin, self).__init__(*args, **kwargs) # type: ignore # Check if we are running on Windows and we have the colorama module, @@ -206,30 +209,27 @@ 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): pass -class DownloadSilentBar(BaseDownloadProgressBar, SilentBar): # type: ignore +class DownloadSilentBar(BaseDownloadProgressBar, SilentBar): pass -class DownloadBar(BaseDownloadProgressBar, # type: ignore +class DownloadBar(BaseDownloadProgressBar, Bar): pass -class DownloadFillingCirclesBar(BaseDownloadProgressBar, # type: ignore +class DownloadFillingCirclesBar(BaseDownloadProgressBar, FillingCirclesBar): pass -class DownloadBlueEmojiProgressBar(BaseDownloadProgressBar, # type: ignore +class DownloadBlueEmojiProgressBar(BaseDownloadProgressBar, BlueEmojiBar): pass @@ -240,7 +240,8 @@ class DownloadProgressSpinner(WindowsMixin, InterruptibleMixin, file = sys.stdout suffix = "%(downloaded)s %(download_speed)s" - def next_phase(self): # type: ignore + def next_phase(self): + # type: () -> str if not hasattr(self, "_phaser"): self._phaser = itertools.cycle(self.phases) return next(self._phaser) From a7bc07591b2d995176d3eb728ccca61792196081 Mon Sep 17 00:00:00 2001 From: gutsytechster <prashantsharma161198@gmail.com> Date: Wed, 15 Apr 2020 16:07:34 +0530 Subject: [PATCH 1792/3170] fix(_internal/utils/filesystem): Update comment for catched exception This fixes https://github.com/pypa/pip/issues/8047 --- src/pip/_internal/utils/filesystem.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pip/_internal/utils/filesystem.py b/src/pip/_internal/utils/filesystem.py index 85010ac25a3..2c3492d397b 100644 --- a/src/pip/_internal/utils/filesystem.py +++ b/src/pip/_internal/utils/filesystem.py @@ -138,6 +138,8 @@ def _test_writable_dir_win(path): # This could be because there's a directory with the same name. # But it's highly unlikely there's a directory called that, # so we'll assume it's because the parent dir is not writable. + # This could as well be because the parent dir is not readable, + # due to non-privileged user access. return False raise else: From ddbc8fd7c15dc642854cccf099f5632119fc2055 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Wed, 13 May 2020 17:52:09 +0100 Subject: [PATCH 1793/3170] Split find_matches into generation and sorting --- .../resolution/resolvelib/candidates.py | 8 ++ .../resolution/resolvelib/factory.py | 79 +++++++----------- .../resolution/resolvelib/provider.py | 81 ++++++++++++++++++- .../resolution/resolvelib/resolver.py | 19 +++-- tests/unit/resolution_resolvelib/conftest.py | 3 +- 5 files changed, 127 insertions(+), 63 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index c4772c33ff8..794d14026ce 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -101,6 +101,14 @@ def make_install_req_from_dist(dist, parent): return ireq +def is_already_installed(cand): + # type: (Candidate) -> bool + # For an ExtrasCandidate, we check the base + if isinstance(cand, ExtrasCandidate): + cand = cand.base + return isinstance(cand, AlreadyInstalledCandidate) + + class _InstallRequirementBackedCandidate(Candidate): def __init__( self, diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index b3cbb22f17b..79343da32bd 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -42,8 +42,6 @@ class Factory(object): - _allowed_strategies = {"eager", "only-if-needed", "to-satisfy-only"} - def __init__( self, finder, # type: PackageFinder @@ -52,11 +50,9 @@ def __init__( force_reinstall, # type: bool ignore_installed, # type: bool ignore_requires_python, # type: bool - upgrade_strategy, # type: str py_version_info=None, # type: Optional[Tuple[int, ...]] ): # type: (...) -> None - assert upgrade_strategy in self._allowed_strategies self.finder = finder self.preparer = preparer @@ -64,9 +60,6 @@ def __init__( self._make_install_req_from_spec = make_install_req self._force_reinstall = force_reinstall self._ignore_requires_python = ignore_requires_python - self._upgrade_strategy = upgrade_strategy - - self.root_reqs = set() # type: Set[str] self._link_candidate_cache = {} # type: Cache[LinkCandidate] self._editable_candidate_cache = {} # type: Cache[EditableCandidate] @@ -118,23 +111,27 @@ def _make_candidate_from_link( return ExtrasCandidate(base, extras) return base - def _eligible_for_upgrade(self, dist_name): - # type: (str) -> bool - if self._upgrade_strategy == "eager": - return True - elif self._upgrade_strategy == "only-if-needed": - return (dist_name in self.root_reqs) - return False - def iter_found_candidates(self, ireq, extras): # type: (InstallRequirement, Set[str]) -> Iterator[Candidate] name = canonicalize_name(ireq.req.name) - if not self._force_reinstall: - installed_dist = self._installed_dists.get(name) - can_upgrade = self._eligible_for_upgrade(name) - else: - installed_dist = None - can_upgrade = False + seen_versions = set() + + # Yield the installed version, if it matches, unless the user + # specified `--force-reinstall`, when we want the version from + # the index instead. + if not self._force_reinstall and name in self._installed_dists: + installed_dist = self._installed_dists[name] + installed_version = installed_dist.parsed_version + if ireq.req.specifier.contains( + installed_version, + prereleases=True + ): + seen_versions.add(installed_version) + yield self._make_candidate_from_dist( + dist=installed_dist, + extras=extras, + parent=ireq, + ) found = self.finder.find_best_candidate( project_name=ireq.req.name, @@ -142,40 +139,18 @@ def iter_found_candidates(self, ireq, extras): hashes=ireq.hashes(trust_internet=False), ) for ican in found.iter_applicable(): - if (installed_dist is not None and - installed_dist.parsed_version == ican.version): - if can_upgrade: - yield self._make_candidate_from_dist( - dist=installed_dist, - extras=extras, - parent=ireq, - ) - continue - yield self._make_candidate_from_link( - link=ican.link, - extras=extras, - parent=ireq, - name=name, - version=ican.version, - ) - - # Return installed distribution if it matches the specifier. This is - # done last so the resolver will prefer it over downloading links. - if can_upgrade or installed_dist is None: - return - installed_version = installed_dist.parsed_version - if ireq.req.specifier.contains(installed_version, prereleases=True): - yield self._make_candidate_from_dist( - dist=installed_dist, - extras=extras, - parent=ireq, - ) + if ican.version not in seen_versions: + seen_versions.add(ican.version) + yield self._make_candidate_from_link( + link=ican.link, + extras=extras, + parent=ireq, + name=name, + version=ican.version, + ) def make_requirement_from_install_req(self, ireq): # type: (InstallRequirement) -> Requirement - if ireq.is_direct and ireq.name: - self.root_reqs.add(canonicalize_name(ireq.name)) - if ireq.link: # TODO: Get name and version from ireq, if possible? # Specifically, this might be needed in "name @ URL" diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index 226dc3687d3..2dbcc873573 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -3,14 +3,35 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from .candidates import is_already_installed + if MYPY_CHECK_RUNNING: - from typing import Any, Dict, Optional, Sequence, Tuple, Union + from typing import Any, Dict, Optional, Sequence, Set, Tuple, Union from pip._internal.req.req_install import InstallRequirement + from pip._vendor.packaging.version import _BaseVersion from .base import Requirement, Candidate from .factory import Factory +# Notes on the relationship between the provider, the factory, and the +# candidate and requirement classes. +# +# The provider is a direct implementation of the resolvelib class. Its role +# is to deliver the API that resolvelib expects. +# +# Rather than work with completely abstract "requirement" and "candidate" +# concepts as resolvelib does, pip has concrete classes implementing these two +# ideas. The API of Requirement and Candidate objects are defined in the base +# classes, but essentially map fairly directly to the equivalent provider +# methods. In particular, `find_matches` and `is_satisfied_by` are +# requirement methods, and `get_dependencies` is a candidate method. +# +# The factory is the interface to pip's internal mechanisms. It is stateless, +# and is created by the resolver and held as a property of the provider. It is +# responsible for creating Requirement and Candidate objects, and provides +# services to those objects (access to pip's finder and preparer). + class PipProvider(AbstractProvider): def __init__( @@ -18,11 +39,66 @@ def __init__( factory, # type: Factory constraints, # type: Dict[str, SpecifierSet] ignore_dependencies, # type: bool + upgrade_strategy, # type: str + roots, # type: Set[str] ): # type: (...) -> None self._factory = factory self._constraints = constraints self._ignore_dependencies = ignore_dependencies + self._upgrade_strategy = upgrade_strategy + self.roots = roots + + def sort_matches(self, matches): + # type: (Sequence[Candidate]) -> Sequence[Candidate] + + # The requirement is responsible for returning a sequence of potential + # candidates, one per version. The provider handles the logic of + # deciding the order in which these candidates should be passed to + # the resolver. + + # The `matches` argument is a sequence of candidates, one per version, + # which are potential options to be installed. The requirement will + # have already sorted out whether to give us an already-installed + # candidate or a version from PyPI (i.e., it will deal with options + # like --force-reinstall and --ignore-installed). + + # We now work out the correct order. + # + # 1. If no other considerations apply, later versions take priority. + # 2. An already installed distribution is preferred over any other, + # unless the user has requested an upgrade. + # Upgrades are allowed when: + # * The --upgrade flag is set, and + # - The project was specified on the command line, or + # - The project is a dependency and the "eager" upgrade strategy + # was requested. + + def _eligible_for_upgrade(name): + # type: (str) -> bool + if self._upgrade_strategy == "eager": + return True + elif self._upgrade_strategy == "only-if-needed": + print(name, self.roots) + return (name in self.roots) + return False + + def keep_installed(c): + # type: (Candidate) -> int + """Give priority to an installed version?""" + if not is_already_installed(c): + return 0 + + if _eligible_for_upgrade(c.name): + return 0 + + return 1 + + def key(c): + # type: (Candidate) -> Tuple[int, _BaseVersion] + return (keep_installed(c), c.version) + + return sorted(matches, key=key) def get_install_requirement(self, c): # type: (Candidate) -> Optional[InstallRequirement] @@ -45,7 +121,8 @@ def get_preference( def find_matches(self, requirement): # type: (Requirement) -> Sequence[Candidate] constraint = self._constraints.get(requirement.name, SpecifierSet()) - return requirement.find_matches(constraint) + matches = requirement.find_matches(constraint) + return self.sort_matches(matches) def is_satisfied_by(self, requirement, candidate): # type: (Requirement, Candidate) -> bool diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 5c94d3dc057..b363f303232 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -32,6 +32,8 @@ class Resolver(BaseResolver): + _allowed_strategies = {"eager", "only-if-needed", "to-satisfy-only"} + def __init__( self, preparer, # type: RequirementPreparer @@ -47,6 +49,9 @@ def __init__( py_version_info=None, # type: Optional[Tuple[int, ...]] ): super(Resolver, self).__init__() + + assert upgrade_strategy in self._allowed_strategies + self.factory = Factory( finder=finder, preparer=preparer, @@ -54,23 +59,17 @@ def __init__( force_reinstall=force_reinstall, ignore_installed=ignore_installed, ignore_requires_python=ignore_requires_python, - upgrade_strategy=upgrade_strategy, py_version_info=py_version_info, ) self.ignore_dependencies = ignore_dependencies + self.upgrade_strategy = upgrade_strategy self._result = None # type: Optional[Result] def resolve(self, root_reqs, check_supported_wheels): # type: (List[InstallRequirement], bool) -> RequirementSet - # The factory should not have retained state from any previous usage. - # In theory this could only happen if self was reused to do a second - # resolve, which isn't something we do at the moment. We assert here - # in order to catch the issue if that ever changes. - # The persistent state that we care about is `root_reqs`. - assert len(self.factory.root_reqs) == 0, "Factory is being re-used" - constraints = {} # type: Dict[str, SpecifierSet] + roots = set() requirements = [] for req in root_reqs: if req.constraint: @@ -82,6 +81,8 @@ def resolve(self, root_reqs, check_supported_wheels): else: constraints[name] = req.specifier else: + if req.is_direct and req.name: + roots.add(canonicalize_name(req.name)) requirements.append( self.factory.make_requirement_from_install_req(req) ) @@ -90,6 +91,8 @@ def resolve(self, root_reqs, check_supported_wheels): factory=self.factory, constraints=constraints, ignore_dependencies=self.ignore_dependencies, + upgrade_strategy=self.upgrade_strategy, + roots=roots, ) reporter = BaseReporter() resolver = RLResolver(provider, reporter) diff --git a/tests/unit/resolution_resolvelib/conftest.py b/tests/unit/resolution_resolvelib/conftest.py index 41c479bf350..3327d9cc009 100644 --- a/tests/unit/resolution_resolvelib/conftest.py +++ b/tests/unit/resolution_resolvelib/conftest.py @@ -55,7 +55,6 @@ def factory(finder, preparer): force_reinstall=False, ignore_installed=False, ignore_requires_python=False, - upgrade_strategy="to-satisfy-only", py_version_info=None, ) @@ -66,4 +65,6 @@ def provider(factory): factory=factory, constraints={}, ignore_dependencies=False, + upgrade_strategy="to-satisfy-only", + roots=set(), ) From 0db022fc425112cc9d6f334262e218df0801bd2d Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Wed, 13 May 2020 18:04:20 +0100 Subject: [PATCH 1794/3170] Remove left-over print from debugging --- src/pip/_internal/resolution/resolvelib/provider.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index 2dbcc873573..b8e7413f96b 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -79,7 +79,6 @@ def _eligible_for_upgrade(name): if self._upgrade_strategy == "eager": return True elif self._upgrade_strategy == "only-if-needed": - print(name, self.roots) return (name in self.roots) return False From feaa76a1d629ce653452b7670b2fa5a0bee7112b Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Thu, 14 May 2020 02:14:07 +0530 Subject: [PATCH 1795/3170] Update mypy to v0.770 --- .pre-commit-config.yaml | 2 +- news/CDB04414-2228-431F-9F5D-AFF4C5C08D05.trivial | 0 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 news/CDB04414-2228-431F-9F5D-AFF4C5C08D05.trivial diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 89430962cab..3e2c0a2ad57 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -29,7 +29,7 @@ repos: files: \.py$ - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.760 + rev: v0.770 hooks: - id: mypy exclude: docs|tests diff --git a/news/CDB04414-2228-431F-9F5D-AFF4C5C08D05.trivial b/news/CDB04414-2228-431F-9F5D-AFF4C5C08D05.trivial new file mode 100644 index 00000000000..e69de29bb2d From 56d53cd60bece912fba37bc6e1c54dc0fd5cdffe Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 14 May 2020 15:11:10 +0800 Subject: [PATCH 1796/3170] Add a reason why we're xfail-ing this test This test uses xfail to test the negation of what's described in the test, i.e. the assertion should "fail" if the command does what we expect. I think this is a terrible way to write a test, but don't bother to rewrite. This commit message is left here to tell future maintainers what the test is all about by git blame. --- tests/functional/test_freeze.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/tests/functional/test_freeze.py b/tests/functional/test_freeze.py index 44278c8660d..2199bb48214 100644 --- a/tests/functional/test_freeze.py +++ b/tests/functional/test_freeze.py @@ -191,7 +191,12 @@ def test_freeze_svn(script, tmpdir): @pytest.mark.git -@pytest.mark.xfail +@pytest.mark.xfail( + condition=True, + reason="xfail means editable is not in output", + run=True, + strict=True, +) def test_freeze_exclude_editable(script, tmpdir): """ Test excluding editable from freezing list. From a82a5e7abe960708fece0a79f0ea1ca702ee2d31 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 14 May 2020 17:09:33 +0800 Subject: [PATCH 1797/3170] Add pytest option to globally switch resolver --- .travis.yml | 4 ++-- tests/conftest.py | 32 ++++++++++++++++++++++++++++---- tools/travis/run.sh | 12 ++++++++++-- tox.ini | 1 - 4 files changed, 40 insertions(+), 9 deletions(-) diff --git a/.travis.yml b/.travis.yml index b44b07b1e99..02c71a243fb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -57,10 +57,10 @@ jobs: - stage: experimental env: - GROUP=1 - - PIP_UNSTABLE_FEATURE=resolver + - NEW_RESOLVER=1 - env: - GROUP=2 - - PIP_UNSTABLE_FEATURE=resolver + - NEW_RESOLVER=1 fast_finish: true allow_failures: diff --git a/tests/conftest.py b/tests/conftest.py index faec4f641b6..bf8cd7975c7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -30,11 +30,23 @@ def pytest_addoption(parser): parser.addoption( - "--keep-tmpdir", action="store_true", - default=False, help="keep temporary test directories" + "--keep-tmpdir", + action="store_true", + default=False, + help="keep temporary test directories", + ) + parser.addoption( + "--new-resolver", + action="store_true", + default=False, + help="use new resolver in tests", + ) + parser.addoption( + "--use-venv", + action="store_true", + default=False, + help="use venv for virtual environment creation", ) - parser.addoption("--use-venv", action="store_true", - help="use venv for virtual environment creation") def pytest_collection_modifyitems(config, items): @@ -75,6 +87,18 @@ def pytest_collection_modifyitems(config, items): ) +@pytest.fixture(scope="session", autouse=True) +def use_new_resolver(request): + """Set environment variable to make pip default to the new resolver. + """ + new_resolver = request.config.getoption("--new-resolver") + if new_resolver: + os.environ["PIP_UNSTABLE_FEATURE"] = "resolver" + else: + os.environ.pop("PIP_UNSTABLE_FEATURE", None) + yield new_resolver + + @pytest.fixture(scope='session') def tmpdir_factory(request, tmpdir_factory): """ Modified `tmpdir_factory` session fixture diff --git a/tools/travis/run.sh b/tools/travis/run.sh index 86f975f4f16..90e7d570860 100755 --- a/tools/travis/run.sh +++ b/tools/travis/run.sh @@ -37,16 +37,24 @@ if [[ -z "$TOXENV" ]]; then fi echo "TOXENV=${TOXENV}" +if [[ -z "$NEW_RESOLVER" ]]; then + RESOLVER_SWITCH='' +else + RESOLVER_SWITCH='--new-resolver' +fi + # Print the commands run for this test. set -x if [[ "$GROUP" == "1" ]]; then # Unit tests tox -- --use-venv -m unit -n auto # Integration tests (not the ones for 'pip install') - tox -- --use-venv -m integration -n auto --duration=5 -k "not test_install" + tox -- -m integration -n auto --duration=5 -k "not test_install" \ + --use-venv $RESOLVER_SWITCH elif [[ "$GROUP" == "2" ]]; then # Separate Job for running integration tests for 'pip install' - tox -- --use-venv -m integration -n auto --duration=5 -k "test_install" + tox -- -m integration -n auto --duration=5 -k "test_install" \ + --use-venv $RESOLVER_SWITCH else # Non-Testing Jobs should run once tox diff --git a/tox.ini b/tox.ini index f6d5cb74179..f05473d9764 100644 --- a/tox.ini +++ b/tox.ini @@ -18,7 +18,6 @@ passenv = HTTP_PROXY HTTPS_PROXY NO_PROXY - PIP_UNSTABLE_FEATURE setenv = # This is required in order to get UTF-8 output inside of the subprocesses # that our tests use. From 9cf1bed78d7d4fa556b8a93274516eb07100f172 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Thu, 14 May 2020 11:33:30 +0100 Subject: [PATCH 1798/3170] Address review comments --- .../_internal/resolution/resolvelib/base.py | 5 ++ .../resolution/resolvelib/candidates.py | 20 ++++---- .../resolution/resolvelib/factory.py | 6 +++ .../resolution/resolvelib/provider.py | 46 +++++++++++-------- .../resolution/resolvelib/requirements.py | 4 ++ .../resolution/resolvelib/resolver.py | 6 +-- tests/unit/resolution_resolvelib/conftest.py | 2 +- 7 files changed, 57 insertions(+), 32 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/base.py b/src/pip/_internal/resolution/resolvelib/base.py index 3ccc48e1fea..eacdf8ecc8e 100644 --- a/src/pip/_internal/resolution/resolvelib/base.py +++ b/src/pip/_internal/resolution/resolvelib/base.py @@ -44,6 +44,11 @@ def version(self): # type: () -> _BaseVersion raise NotImplementedError("Override in subclass") + @property + def is_installed(self): + # type: () -> bool + raise NotImplementedError("Override in subclass") + def get_dependencies(self): # type: () -> Sequence[Requirement] raise NotImplementedError("Override in subclass") diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 794d14026ce..ed6e71bab57 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -101,15 +101,10 @@ def make_install_req_from_dist(dist, parent): return ireq -def is_already_installed(cand): - # type: (Candidate) -> bool - # For an ExtrasCandidate, we check the base - if isinstance(cand, ExtrasCandidate): - cand = cand.base - return isinstance(cand, AlreadyInstalledCandidate) - - class _InstallRequirementBackedCandidate(Candidate): + # These are not installed + is_installed = False + def __init__( self, link, # type: Link @@ -279,6 +274,8 @@ def _prepare_abstract_distribution(self): class AlreadyInstalledCandidate(Candidate): + is_installed = True + def __init__( self, dist, # type: Distribution @@ -400,6 +397,11 @@ def version(self): # type: () -> _BaseVersion return self.base.version + @property + def is_installed(self): + # type: () -> _BaseVersion + return self.base.is_installed + def get_dependencies(self): # type: () -> Sequence[Requirement] factory = self.base._factory @@ -436,6 +438,8 @@ def get_install_requirement(self): class RequiresPythonCandidate(Candidate): + is_installed = False + def __init__(self, py_version_info): # type: (Optional[Tuple[int, ...]]) -> None if py_version_info is not None: diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 79343da32bd..2dc3a6ac9ec 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -114,6 +114,12 @@ def _make_candidate_from_link( def iter_found_candidates(self, ireq, extras): # type: (InstallRequirement, Set[str]) -> Iterator[Candidate] name = canonicalize_name(ireq.req.name) + + # We use this to ensure that we only yield a single candidate for + # each version (the finder's preferred one for that version). The + # requirement needs to return only one candidate per version, so we + # implement that logic here so that requirements using this helper + # don't all have to do the same thing later. seen_versions = set() # Yield the installed version, if it matches, unless the user diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index b8e7413f96b..f74fcae2f29 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -3,8 +3,6 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from .candidates import is_already_installed - if MYPY_CHECK_RUNNING: from typing import Any, Dict, Optional, Sequence, Set, Tuple, Union @@ -40,16 +38,16 @@ def __init__( constraints, # type: Dict[str, SpecifierSet] ignore_dependencies, # type: bool upgrade_strategy, # type: str - roots, # type: Set[str] + user_requested, # type: Set[str] ): # type: (...) -> None self._factory = factory self._constraints = constraints self._ignore_dependencies = ignore_dependencies self._upgrade_strategy = upgrade_strategy - self.roots = roots + self.user_requested = user_requested - def sort_matches(self, matches): + def _sort_matches(self, matches): # type: (Sequence[Candidate]) -> Sequence[Candidate] # The requirement is responsible for returning a sequence of potential @@ -76,28 +74,36 @@ def sort_matches(self, matches): def _eligible_for_upgrade(name): # type: (str) -> bool + """Are upgrades allowed for this project? + + This checks the upgrade strategy, and whether the project was one + that the user specified in the command line, in order to decide + whether we should upgrade if there's a newer version available. + + (Note that we don't need access to the `--upgrade` flag, because + an upgrade strategy of "to-satisfy-only" means that `--upgrade` + was not specified). + """ if self._upgrade_strategy == "eager": return True elif self._upgrade_strategy == "only-if-needed": - return (name in self.roots) + return (name in self.user_requested) return False - def keep_installed(c): - # type: (Candidate) -> int - """Give priority to an installed version?""" - if not is_already_installed(c): - return 0 - - if _eligible_for_upgrade(c.name): - return 0 + def sort_key(c): + # type: (Candidate) -> Tuple[int, _BaseVersion] + """Return a sort key for the matches. - return 1 + The highest priority should be given to installed candidates that + are not eligible for upgrade. We use the integer value in the first + part of the key to sort these before other candidates. + """ + if c.is_installed and not _eligible_for_upgrade(c.name): + return (1, c.version) - def key(c): - # type: (Candidate) -> Tuple[int, _BaseVersion] - return (keep_installed(c), c.version) + return (0, c.version) - return sorted(matches, key=key) + return sorted(matches, key=sort_key) def get_install_requirement(self, c): # type: (Candidate) -> Optional[InstallRequirement] @@ -121,7 +127,7 @@ def find_matches(self, requirement): # type: (Requirement) -> Sequence[Candidate] constraint = self._constraints.get(requirement.name, SpecifierSet()) matches = requirement.find_matches(constraint) - return self.sort_matches(matches) + return self._sort_matches(matches) def is_satisfied_by(self, requirement, candidate): # type: (Requirement, Candidate) -> bool diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py index c32187c6c5f..5c7d00c2a4a 100644 --- a/src/pip/_internal/resolution/resolvelib/requirements.py +++ b/src/pip/_internal/resolution/resolvelib/requirements.py @@ -76,6 +76,10 @@ def name(self): def find_matches(self, constraint): # type: (SpecifierSet) -> Sequence[Candidate] + + # We should only return one candidate per version, but + # iter_found_candidates does that for us, so we don't need + # to do anything special here. return [ c for c in self._factory.iter_found_candidates( diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index b363f303232..34c74ef95e6 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -69,7 +69,7 @@ def resolve(self, root_reqs, check_supported_wheels): # type: (List[InstallRequirement], bool) -> RequirementSet constraints = {} # type: Dict[str, SpecifierSet] - roots = set() + user_requested = set() requirements = [] for req in root_reqs: if req.constraint: @@ -82,7 +82,7 @@ def resolve(self, root_reqs, check_supported_wheels): constraints[name] = req.specifier else: if req.is_direct and req.name: - roots.add(canonicalize_name(req.name)) + user_requested.add(canonicalize_name(req.name)) requirements.append( self.factory.make_requirement_from_install_req(req) ) @@ -92,7 +92,7 @@ def resolve(self, root_reqs, check_supported_wheels): constraints=constraints, ignore_dependencies=self.ignore_dependencies, upgrade_strategy=self.upgrade_strategy, - roots=roots, + user_requested=user_requested, ) reporter = BaseReporter() resolver = RLResolver(provider, reporter) diff --git a/tests/unit/resolution_resolvelib/conftest.py b/tests/unit/resolution_resolvelib/conftest.py index 3327d9cc009..45deca10947 100644 --- a/tests/unit/resolution_resolvelib/conftest.py +++ b/tests/unit/resolution_resolvelib/conftest.py @@ -66,5 +66,5 @@ def provider(factory): constraints={}, ignore_dependencies=False, upgrade_strategy="to-satisfy-only", - roots=set(), + user_requested=set(), ) From bd07bfb8db730003ca374d5588a3354b416a9f92 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Thu, 14 May 2020 11:56:06 +0530 Subject: [PATCH 1799/3170] Cast record_file to io.StringIO for Python 3 to appease mypy --- src/pip/_internal/operations/install/wheel.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 2fb86b866db..2fe13823841 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -10,6 +10,7 @@ import compileall import contextlib import csv +import io import logging import os.path import re @@ -24,7 +25,7 @@ from pip._vendor import pkg_resources from pip._vendor.distlib.scripts import ScriptMaker from pip._vendor.distlib.util import get_export_entry -from pip._vendor.six import StringIO +from pip._vendor.six import PY3, StringIO from pip._internal.exceptions import InstallationError from pip._internal.locations import get_major_minor_version @@ -32,7 +33,7 @@ from pip._internal.utils.filesystem import adjacent_tmp_file, replace from pip._internal.utils.misc import captured_stdout, ensure_dir, hash_file from pip._internal.utils.temp_dir import TempDirectory -from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from pip._internal.utils.typing import MYPY_CHECK_RUNNING, cast from pip._internal.utils.unpacking import current_umask, unpack_file from pip._internal.utils.wheel import parse_wheel @@ -600,6 +601,12 @@ def _generate_file(path, **kwargs): generated=generated, lib_dir=lib_dir) with _generate_file(record_path, **csv_io_kwargs('w')) as record_file: + + # For Python 3, we create the file in text mode, hence we + # cast record_file to io.StringIO + if PY3: + record_file = cast(io.StringIO, record_file) + writer = csv.writer(record_file) writer.writerows(sorted_outrows(rows)) # sort to simplify testing From 6fcaf49cb0f478adba1e8eb842eb53987551271b Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Thu, 14 May 2020 16:56:55 +0100 Subject: [PATCH 1800/3170] Tidy up handling of unexpected forms of constraint --- .../resolution/resolvelib/resolver.py | 26 ++++++++-- tests/functional/test_new_resolver.py | 48 +++++++++++++++++++ 2 files changed, 69 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 5c94d3dc057..f7b90c8da66 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -2,6 +2,7 @@ import logging from pip._vendor import six +from pip._vendor.packaging.specifiers import SpecifierSet from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.resolvelib import BaseReporter, ResolutionImpossible from pip._vendor.resolvelib import Resolver as RLResolver @@ -17,7 +18,6 @@ if MYPY_CHECK_RUNNING: from typing import Dict, List, Optional, Set, Tuple - from pip._vendor.packaging.specifiers import SpecifierSet from pip._vendor.resolvelib.resolvers import Result from pip._vendor.resolvelib.structs import Graph @@ -74,13 +74,29 @@ def resolve(self, root_reqs, check_supported_wheels): requirements = [] for req in root_reqs: if req.constraint: - assert req.name - assert req.specifier + # TODO: Add warnings to accompany these errors, explaining + # that these were undocumented behaviour of the old resolver + # and will be removed in the new resolver. We need to consider + # how we remember to remove these warnings when the new + # resolver becomes the default... + if not req.name: + raise InstallationError( + "Unnamed requirements are not allowed as constraints" + ) + if req.link: + raise InstallationError( + "Links are not allowed as constraints" + ) + if req.extras: + raise InstallationError( + "Constraints cannot have extras" + ) + specifier = req.specifier or SpecifierSet() name = canonicalize_name(req.name) if name in constraints: - constraints[name] = constraints[name] & req.specifier + constraints[name] = constraints[name] & specifier else: - constraints[name] = req.specifier + constraints[name] = specifier else: requirements.append( self.factory.make_requirement_from_install_req(req) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index bae225ec22c..824a12d3a73 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -548,6 +548,54 @@ def test_new_resolver_constraints(script): assert_not_installed(script, "constraint_only") +def test_new_resolver_constraint_no_specifier(script): + "It's allowed (but useless...) for a constraint to have no specifier" + create_basic_wheel_for_package(script, "pkg", "1.0") + constraints_file = script.scratch_path / "constraints.txt" + constraints_file.write_text("pkg") + script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "-c", constraints_file, + "pkg" + ) + assert_installed(script, pkg="1.0") + + +@pytest.mark.parametrize( + "constraint, error", + [ + ( + "dist.zip", + "Unnamed requirements are not allowed as constraints", + ), + ( + "req @ https://example.com/dist.zip", + "Links are not allowed as constraints", + ), + ( + "pkg[extra]", + "Constraints cannot have extras", + ), + ], +) +def test_new_resolver_constraint_reject_invalid(script, constraint, error): + create_basic_wheel_for_package(script, "pkg", "1.0") + constraints_file = script.scratch_path / "constraints.txt" + constraints_file.write_text(constraint) + result = script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "-c", constraints_file, + "pkg", + expect_error=True, + expect_stderr=True, + ) + assert error in result.stderr, str(result) + + def test_new_resolver_constraint_on_dependency(script): create_basic_wheel_for_package(script, "base", "1.0", depends=["dep"]) create_basic_wheel_for_package(script, "dep", "1.0") From 0bcae1e9644300527ad83bc9888bf02ceb5b868c Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Fri, 15 May 2020 09:54:40 +0100 Subject: [PATCH 1801/3170] Add a deprecation warning for undocumented constraint forms --- .../resolution/resolvelib/resolver.py | 52 +++++++++++++------ 1 file changed, 35 insertions(+), 17 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index f7b90c8da66..5487b88a472 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -11,6 +11,7 @@ from pip._internal.req.req_set import RequirementSet from pip._internal.resolution.base import BaseResolver from pip._internal.resolution.resolvelib.provider import PipProvider +from pip._internal.utils.deprecation import deprecated from pip._internal.utils.typing import MYPY_CHECK_RUNNING from .factory import Factory @@ -31,6 +32,37 @@ logger = logging.getLogger(__name__) +def reject_invalid_constraint_types(req): + # type: (InstallRequirement) -> None + + # Check for unsupported forms + problem = "" + if not req.name: + problem = "Unnamed requirements are not allowed as constraints" + elif req.link: + problem = "Links are not allowed as constraints" + elif req.extras: + problem = "Constraints cannot have extras" + + if problem: + deprecated( + reason=( + "Constraints are only allowed to take the form of a package " + "name and a version specifier. Other forms were originally " + "permitted as an accident of the implementation, but were " + "undocumented. The new implementation of the resolver no " + "longer supports these forms." + ), + replacement=( + "replacing the constraint with a requirement." + ), + # No plan yet for when the new resolver becomes default + gone_in=None, + issue=8210 + ) + raise InstallationError(problem) + + class Resolver(BaseResolver): def __init__( self, @@ -74,23 +106,9 @@ def resolve(self, root_reqs, check_supported_wheels): requirements = [] for req in root_reqs: if req.constraint: - # TODO: Add warnings to accompany these errors, explaining - # that these were undocumented behaviour of the old resolver - # and will be removed in the new resolver. We need to consider - # how we remember to remove these warnings when the new - # resolver becomes the default... - if not req.name: - raise InstallationError( - "Unnamed requirements are not allowed as constraints" - ) - if req.link: - raise InstallationError( - "Links are not allowed as constraints" - ) - if req.extras: - raise InstallationError( - "Constraints cannot have extras" - ) + # Ensure we only accept valid constraints + reject_invalid_constraint_types(req) + specifier = req.specifier or SpecifierSet() name = canonicalize_name(req.name) if name in constraints: From 96b3377cd79e5d946f4d2b07e1cb8f8f3676850e Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Fri, 15 May 2020 10:57:07 +0100 Subject: [PATCH 1802/3170] Type annotations --- src/pip/_internal/resolution/resolvelib/factory.py | 2 +- src/pip/_internal/resolution/resolvelib/resolver.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 2dc3a6ac9ec..99e20c35511 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -120,7 +120,7 @@ def iter_found_candidates(self, ireq, extras): # requirement needs to return only one candidate per version, so we # implement that logic here so that requirements using this helper # don't all have to do the same thing later. - seen_versions = set() + seen_versions = set() # type: Set[_BaseVersion] # Yield the installed version, if it matches, unless the user # specified `--force-reinstall`, when we want the version from diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 34c74ef95e6..7cfded74ca6 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -69,7 +69,7 @@ def resolve(self, root_reqs, check_supported_wheels): # type: (List[InstallRequirement], bool) -> RequirementSet constraints = {} # type: Dict[str, SpecifierSet] - user_requested = set() + user_requested = set() # type: Set[str] requirements = [] for req in root_reqs: if req.constraint: From 78c0a7192aae3cc61060ca91e4276626bdc6cd86 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Fri, 15 May 2020 16:55:58 +0530 Subject: [PATCH 1803/3170] Change return type annotation for commands.debug.get_vendor_version_from_module --- src/pip/_internal/commands/debug.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/commands/debug.py b/src/pip/_internal/commands/debug.py index 341583ea7cf..f8d5817c032 100644 --- a/src/pip/_internal/commands/debug.py +++ b/src/pip/_internal/commands/debug.py @@ -80,7 +80,7 @@ def get_module_from_module_name(module_name): def get_vendor_version_from_module(module_name): - # type: (str) -> str + # type: (str) -> Optional[str] module = get_module_from_module_name(module_name) version = getattr(module, '__version__', None) From c83c2804caef4460094bad20b8ccf287dff58d36 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Fri, 15 May 2020 16:57:31 +0530 Subject: [PATCH 1804/3170] Always return SUCCESS from commands.completion.run --- src/pip/_internal/commands/completion.py | 4 ++-- tests/functional/test_completion.py | 4 +--- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/commands/completion.py b/src/pip/_internal/commands/completion.py index c62ce7b3ba3..70d33243fcd 100644 --- a/src/pip/_internal/commands/completion.py +++ b/src/pip/_internal/commands/completion.py @@ -4,7 +4,7 @@ import textwrap from pip._internal.cli.base_command import Command -from pip._internal.cli.status_codes import ERROR, SUCCESS +from pip._internal.cli.status_codes import SUCCESS from pip._internal.utils.misc import get_prog from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -99,4 +99,4 @@ def run(self, options, args): sys.stderr.write( 'ERROR: You must pass {}\n' .format(' or '.join(shell_options)) ) - return ERROR + return SUCCESS diff --git a/tests/functional/test_completion.py b/tests/functional/test_completion.py index e00f0b03774..a3986811b6f 100644 --- a/tests/functional/test_completion.py +++ b/tests/functional/test_completion.py @@ -107,10 +107,9 @@ def test_completion_alone(autocomplete_script): """ Test getting completion for none shell, just pip completion """ - result = autocomplete_script.pip('completion', expect_error=True) + result = autocomplete_script.pip('completion', allow_stderr_error=True) assert 'ERROR: You must pass --bash or --fish or --zsh' in result.stderr, \ 'completion alone failed -- ' + result.stderr - assert result.returncode == 1 def test_completion_for_un_snippet(autocomplete): @@ -315,4 +314,3 @@ def test_completion_uses_same_executable_name( executable_name, 'completion', flag, expect_stderr=deprecated_python, ) assert executable_name in result.stdout - assert result.returncode == 0 From a56376b20f87a6c81960199c692cd71c8a360c80 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Fri, 15 May 2020 12:24:23 +0100 Subject: [PATCH 1805/3170] req.specifier is always a SpecifierSet --- src/pip/_internal/resolution/resolvelib/resolver.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 5487b88a472..88dcdf396bf 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -2,7 +2,6 @@ import logging from pip._vendor import six -from pip._vendor.packaging.specifiers import SpecifierSet from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.resolvelib import BaseReporter, ResolutionImpossible from pip._vendor.resolvelib import Resolver as RLResolver @@ -19,6 +18,7 @@ if MYPY_CHECK_RUNNING: from typing import Dict, List, Optional, Set, Tuple + from pip._vendor.packaging.specifiers import SpecifierSet from pip._vendor.resolvelib.resolvers import Result from pip._vendor.resolvelib.structs import Graph @@ -109,12 +109,11 @@ def resolve(self, root_reqs, check_supported_wheels): # Ensure we only accept valid constraints reject_invalid_constraint_types(req) - specifier = req.specifier or SpecifierSet() name = canonicalize_name(req.name) if name in constraints: - constraints[name] = constraints[name] & specifier + constraints[name] = constraints[name] & req.specifier else: - constraints[name] = specifier + constraints[name] = req.specifier else: requirements.append( self.factory.make_requirement_from_install_req(req) From c258a1f5173bc847f67226a39841494bc9c85bba Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Fri, 15 May 2020 12:22:30 +0100 Subject: [PATCH 1806/3170] Fix some test failures related to message differences --- src/pip/_internal/resolution/resolvelib/candidates.py | 9 +++++---- tests/functional/test_install.py | 2 +- tests/functional/test_install_extras.py | 9 +++++---- 3 files changed, 11 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index ed6e71bab57..6032b27ec1f 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -410,11 +410,12 @@ def get_dependencies(self): # support. We ignore any unsupported extras here. valid_extras = self.extras.intersection(self.base.dist.extras) invalid_extras = self.extras.difference(self.base.dist.extras) - if invalid_extras: + for extra in sorted(invalid_extras): logger.warning( - "Invalid extras specified in %s: %s", - self.name, - ','.join(sorted(invalid_extras)) + "%s %s does not provide the extra '%s'", + self.base.name, + self.version, + extra ) deps = [ diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 2cb44269502..6e19ef50699 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1741,7 +1741,7 @@ def test_user_config_accepted(script): @pytest.mark.parametrize( 'install_args, expected_message', [ - ([], 'Requirement already satisfied: pip in'), + ([], 'Requirement already satisfied: pip'), (['--upgrade'], 'Requirement already up-to-date: pip in'), ] ) diff --git a/tests/functional/test_install_extras.py b/tests/functional/test_install_extras.py index bf67a3c9bfd..3c0359a73f1 100644 --- a/tests/functional/test_install_extras.py +++ b/tests/functional/test_install_extras.py @@ -1,3 +1,4 @@ +import re import textwrap from os.path import join @@ -100,11 +101,11 @@ def test_nonexistent_options_listed_in_order(script, data): '--find-links=' + data.find_links, 'simplewheel[nonexistent, nope]', expect_stderr=True, ) - msg = ( - " WARNING: simplewheel 2.0 does not provide the extra 'nonexistent'\n" - " WARNING: simplewheel 2.0 does not provide the extra 'nope'" + matches = re.findall( + "WARNING: simplewheel 2.0 does not provide the extra '([a-z]*)'", + result.stderr ) - assert msg in result.stderr + assert matches == ['nonexistent', 'nope'] def test_install_special_extra(script): From 041f83f76adbaedc92b275ed0de11b716fddc54d Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Fri, 15 May 2020 14:45:44 +0100 Subject: [PATCH 1807/3170] Doh, I forgot to update our own tests... --- tests/functional/test_new_resolver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index bae225ec22c..5888d339ca7 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -208,8 +208,8 @@ def test_new_resolver_installs_extras(script): "base[add,missing]", expect_stderr=True, ) - assert "WARNING: Invalid extras specified" in result.stderr, str(result) - assert ": missing" in result.stderr, str(result) + assert "does not provide the extra" in result.stderr, str(result) + assert "missing" in result.stderr, str(result) assert_installed(script, base="0.1.0", dep="0.1.0") From 2276f9528c97c1158a6837e2ae1bed5fb9253ec4 Mon Sep 17 00:00:00 2001 From: gutsytechster <prashantsharma161198@gmail.com> Date: Sun, 12 Apr 2020 16:02:42 +0530 Subject: [PATCH 1808/3170] fix(network/auth): Asks for password when it is None When `get_keyring_auth` provides the password as None, the user should be prompt to ask password. --- news/7998.bugfix | 1 + src/pip/_internal/network/auth.py | 2 +- tests/unit/test_network_auth.py | 20 ++++++++++++++++++++ 3 files changed, 22 insertions(+), 1 deletion(-) create mode 100644 news/7998.bugfix diff --git a/news/7998.bugfix b/news/7998.bugfix new file mode 100644 index 00000000000..aec751f86a0 --- /dev/null +++ b/news/7998.bugfix @@ -0,0 +1 @@ +Prompt the user for password if the keyring backend doesn't return one diff --git a/src/pip/_internal/network/auth.py b/src/pip/_internal/network/auth.py index 94da3d46aaa..ab8ac07012e 100644 --- a/src/pip/_internal/network/auth.py +++ b/src/pip/_internal/network/auth.py @@ -219,7 +219,7 @@ def _prompt_for_password(self, netloc): if not username: return None, None auth = get_keyring_auth(netloc, username) - if auth: + if auth and auth[0] is not None and auth[1] is not None: return auth[0], auth[1], False password = ask_password("Password: ") return username, password, True diff --git a/tests/unit/test_network_auth.py b/tests/unit/test_network_auth.py index 1fadd1db348..08320cfa143 100644 --- a/tests/unit/test_network_auth.py +++ b/tests/unit/test_network_auth.py @@ -123,6 +123,26 @@ def ask_input(prompt): assert actual == ("user", "user!netloc", False) +def test_keyring_get_password_after_prompt_when_none(monkeypatch): + keyring = KeyringModuleV1() + monkeypatch.setattr('pip._internal.network.auth.keyring', keyring) + auth = MultiDomainBasicAuth() + + def ask_input(prompt): + assert prompt == "User for unknown.com: " + return "user" + + def ask_password(prompt): + assert prompt == "Password: " + return "fake_password" + + monkeypatch.setattr('pip._internal.network.auth.ask_input', ask_input) + monkeypatch.setattr( + 'pip._internal.network.auth.ask_password', ask_password) + actual = auth._prompt_for_password("unknown.com") + assert actual == ("user", "fake_password", True) + + def test_keyring_get_password_username_in_index(monkeypatch): keyring = KeyringModuleV1() monkeypatch.setattr('pip._internal.network.auth.keyring', keyring) From 29940371ccce820706677e414e39d6e671006983 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Sat, 16 May 2020 11:59:19 +0700 Subject: [PATCH 1809/3170] Fix typo and simplify ireq call --- news/86222709-663e-40a1-af2e-f20afab42122.trivial | 0 src/pip/_internal/resolution/resolvelib/candidates.py | 2 +- src/pip/_internal/resolution/resolvelib/provider.py | 5 ----- src/pip/_internal/resolution/resolvelib/resolver.py | 2 +- 4 files changed, 2 insertions(+), 7 deletions(-) create mode 100644 news/86222709-663e-40a1-af2e-f20afab42122.trivial diff --git a/news/86222709-663e-40a1-af2e-f20afab42122.trivial b/news/86222709-663e-40a1-af2e-f20afab42122.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index e71b27d13f5..418da5d4d0d 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -461,7 +461,7 @@ def __init__(self, py_version_info): def name(self): # type: () -> str # Avoid conflicting with the PyPI package "Python". - return "<Python fom Requires-Python>" + return "<Python from Requires-Python>" @property def version(self): diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index f74fcae2f29..4e8c5ae309b 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -6,7 +6,6 @@ if MYPY_CHECK_RUNNING: from typing import Any, Dict, Optional, Sequence, Set, Tuple, Union - from pip._internal.req.req_install import InstallRequirement from pip._vendor.packaging.version import _BaseVersion from .base import Requirement, Candidate @@ -105,10 +104,6 @@ def sort_key(c): return sorted(matches, key=sort_key) - def get_install_requirement(self, c): - # type: (Candidate) -> Optional[InstallRequirement] - return c.get_install_requirement() - def identify(self, dependency): # type: (Union[Requirement, Candidate]) -> str return dependency.name diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index d5170237379..d05a277b000 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -159,7 +159,7 @@ def resolve(self, root_reqs, check_supported_wheels): req_set = RequirementSet(check_supported_wheels=check_supported_wheels) for candidate in self._result.mapping.values(): - ireq = provider.get_install_requirement(candidate) + ireq = candidate.get_install_requirement() if ireq is None: continue ireq.should_reinstall = self.factory.should_reinstall(candidate) From 8ce6b88077dafc49ba030379aec609fb60e8b0ec Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sun, 17 May 2020 15:08:19 +0530 Subject: [PATCH 1810/3170] Remove comment addressing metadata initial value --- src/pip/_internal/commands/show.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/pip/_internal/commands/show.py b/src/pip/_internal/commands/show.py index f7522df9088..8d9fa13fd91 100644 --- a/src/pip/_internal/commands/show.py +++ b/src/pip/_internal/commands/show.py @@ -94,10 +94,6 @@ def get_requiring_packages(package_name): 'required_by': get_requiring_packages(dist.project_name) } file_list = None - # Set metadata to empty string to avoid metadata being typed as - # Optional[Any] in function calls using metadata below - # and since dist.get_metadata returns us a str, the default - # value of empty string should be valid metadata = '' if isinstance(dist, pkg_resources.DistInfoDistribution): # RECORDs should be part of .dist-info metadatas From 4d208b02b4d3e9fda028fa37b77e983263f6271b Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Fri, 15 May 2020 23:40:51 +0530 Subject: [PATCH 1811/3170] Cast record file to typing.IO[str] to appease mypy for python 2 --- src/pip/_internal/operations/install/wheel.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 2fe13823841..a0885c48cde 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -10,7 +10,6 @@ import compileall import contextlib import csv -import io import logging import os.path import re @@ -25,7 +24,7 @@ from pip._vendor import pkg_resources from pip._vendor.distlib.scripts import ScriptMaker from pip._vendor.distlib.util import get_export_entry -from pip._vendor.six import PY3, StringIO +from pip._vendor.six import StringIO from pip._internal.exceptions import InstallationError from pip._internal.locations import get_major_minor_version @@ -39,6 +38,7 @@ if MYPY_CHECK_RUNNING: from email.message import Message + import typing # noqa F401 from typing import ( Dict, List, Optional, Sequence, Tuple, Any, Iterable, Iterator, Callable, Set, @@ -602,12 +602,14 @@ def _generate_file(path, **kwargs): lib_dir=lib_dir) with _generate_file(record_path, **csv_io_kwargs('w')) as record_file: - # For Python 3, we create the file in text mode, hence we - # cast record_file to io.StringIO - if PY3: - record_file = cast(io.StringIO, record_file) + # The type mypy infers for record_file using reveal_type + # is different for Python 3 (typing.IO[Any]) and + # Python 2 (typing.BinaryIO), leading us to explicitly + # cast to typing.IO[str] as a workaround + # for bad Python 2 behaviour + record_file_obj = cast('typing.IO[str]', record_file) - writer = csv.writer(record_file) + writer = csv.writer(record_file_obj) writer.writerows(sorted_outrows(rows)) # sort to simplify testing From ec86cb1970ff670fb374c31337587c0b41ffa435 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Mon, 18 May 2020 16:35:09 +0800 Subject: [PATCH 1812/3170] Use OrderedDict to prefer links found *later* --- .../resolution/resolvelib/factory.py | 32 ++++++++++++------- 1 file changed, 20 insertions(+), 12 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index ce93707b701..46e6f1d082b 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -1,3 +1,6 @@ +import collections + +from pip._vendor import six from pip._vendor.packaging.utils import canonicalize_name from pip._internal.exceptions import ( @@ -127,11 +130,13 @@ def iter_found_candidates(self, ireq, extras): # requirement needs to return only one candidate per version, so we # implement that logic here so that requirements using this helper # don't all have to do the same thing later. - seen_versions = set() # type: Set[_BaseVersion] + version_candidates = collections.OrderedDict( + ) # type: Dict[_BaseVersion, Candidate] # Yield the installed version, if it matches, unless the user # specified `--force-reinstall`, when we want the version from # the index instead. + installed_version = None if not self._force_reinstall and name in self._installed_dists: installed_dist = self._installed_dists[name] installed_version = installed_dist.parsed_version @@ -139,12 +144,12 @@ def iter_found_candidates(self, ireq, extras): installed_version, prereleases=True ): - seen_versions.add(installed_version) - yield self._make_candidate_from_dist( + candidate = self._make_candidate_from_dist( dist=installed_dist, extras=extras, parent=ireq, ) + version_candidates[installed_version] = candidate found = self.finder.find_best_candidate( project_name=ireq.req.name, @@ -152,15 +157,18 @@ def iter_found_candidates(self, ireq, extras): hashes=ireq.hashes(trust_internet=False), ) for ican in found.iter_applicable(): - if ican.version not in seen_versions: - seen_versions.add(ican.version) - yield self._make_candidate_from_link( - link=ican.link, - extras=extras, - parent=ireq, - name=name, - version=ican.version, - ) + if ican.version == installed_version: + continue + candidate = self._make_candidate_from_link( + link=ican.link, + extras=extras, + parent=ireq, + name=name, + version=ican.version, + ) + version_candidates[ican.version] = candidate + + return six.itervalues(version_candidates) def make_requirement_from_install_req(self, ireq): # type: (InstallRequirement) -> Requirement From f39134699b3f57c409e2b9c1bc986ceeb70cc4d8 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Mon, 18 May 2020 17:15:04 +0800 Subject: [PATCH 1813/3170] Avoid the horrendous line break for type hints --- src/pip/_internal/resolution/resolvelib/factory.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 46e6f1d082b..3246caffee6 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -47,6 +47,7 @@ C = TypeVar("C") Cache = Dict[Link, C] + VersionCandidates = Dict[_BaseVersion, Candidate] class Factory(object): @@ -130,8 +131,7 @@ def iter_found_candidates(self, ireq, extras): # requirement needs to return only one candidate per version, so we # implement that logic here so that requirements using this helper # don't all have to do the same thing later. - version_candidates = collections.OrderedDict( - ) # type: Dict[_BaseVersion, Candidate] + candidates = collections.OrderedDict() # type: VersionCandidates # Yield the installed version, if it matches, unless the user # specified `--force-reinstall`, when we want the version from @@ -149,7 +149,7 @@ def iter_found_candidates(self, ireq, extras): extras=extras, parent=ireq, ) - version_candidates[installed_version] = candidate + candidates[installed_version] = candidate found = self.finder.find_best_candidate( project_name=ireq.req.name, @@ -166,9 +166,9 @@ def iter_found_candidates(self, ireq, extras): name=name, version=ican.version, ) - version_candidates[ican.version] = candidate + candidates[ican.version] = candidate - return six.itervalues(version_candidates) + return six.itervalues(candidates) def make_requirement_from_install_req(self, ireq): # type: (InstallRequirement) -> Requirement From f167d99b600a707f2578ba19b5e821757e6ccbcb Mon Sep 17 00:00:00 2001 From: cjc7373 <niuchangcun@gmail.com> Date: Mon, 18 May 2020 18:23:35 +0800 Subject: [PATCH 1814/3170] update pytest to 4.6 also update pytest plugins --- tools/requirements/tests.txt | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tools/requirements/tests.txt b/tools/requirements/tests.txt index 00a306558b8..0c84f20aa47 100644 --- a/tools/requirements/tests.txt +++ b/tools/requirements/tests.txt @@ -4,13 +4,13 @@ enum34; python_version < '3.4' freezegun mock pretend -pytest==3.8.2 +# pytest 5.x only supports python 3.5+ +pytest<5.0.0 pytest-cov -# Prevent installing 7.0 which has install_requires "pytest >= 3.10". -pytest-rerunfailures<7.0 +# Prevent installing 9.0 which has install_requires "pytest >= 5.0". +pytest-rerunfailures<9.0 pytest-timeout -# Prevent installing 1.28.0 which has install_requires "pytest >= 4.4.0". -pytest-xdist<1.28.0 +pytest-xdist pyyaml setuptools>=39.2.0 # Needed for `setuptools.wheel.Wheel` support. scripttest From 67317292bb0d50d22df1c28c3baa1c00fb77ff5a Mon Sep 17 00:00:00 2001 From: cjc7373 <niuchangcun@gmail.com> Date: Mon, 18 May 2020 18:30:39 +0800 Subject: [PATCH 1815/3170] tests: register custom marks otherwise pytest will emit warnings --- tests/conftest.py | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index bf8cd7975c7..75e43c3b72e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -49,6 +49,20 @@ def pytest_addoption(parser): ) +def pytest_configure(config): + config.addinivalue_line("markers", "network: tests that needs network") + config.addinivalue_line("markers", "incompatible_with_test_venv") + config.addinivalue_line("markers", "incompatible_with_venv") + config.addinivalue_line("markers", "no_auto_tempdir_manager") + config.addinivalue_line("markers", "unit: unit tests") + config.addinivalue_line("markers", "integration: integration tests") + config.addinivalue_line("markers", "bzr: VCS: Bazaar") + config.addinivalue_line("markers", "svn: VCS: Subversion") + config.addinivalue_line("markers", "mercurial: VCS: Mercurial") + config.addinivalue_line("markers", "git: VCS: git") + config.addinivalue_line("markers", "yaml: yaml based tests") + + def pytest_collection_modifyitems(config, items): for item in items: if not hasattr(item, 'module'): # e.g.: DoctestTextfile From 1150f0d2bf92b8c330134cd7bfa7ce2d15e10bc6 Mon Sep 17 00:00:00 2001 From: cjc7373 <niuchangcun@gmail.com> Date: Mon, 18 May 2020 18:34:57 +0800 Subject: [PATCH 1816/3170] add news file --- news/93898036-99ac-4e02-88c7-429280fe3e27.trivial | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 news/93898036-99ac-4e02-88c7-429280fe3e27.trivial diff --git a/news/93898036-99ac-4e02-88c7-429280fe3e27.trivial b/news/93898036-99ac-4e02-88c7-429280fe3e27.trivial new file mode 100644 index 00000000000..e69de29bb2d From c513c725748aa729eab3a7d4a0a1a987c9bc7e50 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 30 Apr 2020 06:47:39 +0530 Subject: [PATCH 1817/3170] Merge pull request #8173 from hugovk/document-pip-cache-dir --- docs/html/reference/pip_install.rst | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index 9d9b891a9ed..ec58899743a 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -565,7 +565,7 @@ While this cache attempts to minimize network activity, it does not prevent network access altogether. If you want a local install solution that circumvents accessing PyPI, see :ref:`Installing from local packages`. -The default location for the cache directory depends on the Operating System: +The default location for the cache directory depends on the operating system: Unix :file:`~/.cache/pip` and it respects the ``XDG_CACHE_HOME`` directory. @@ -574,6 +574,9 @@ macOS Windows :file:`<CSIDL_LOCAL_APPDATA>\\pip\\Cache` +Run ``pip cache dir`` to show the cache directory and see :ref:`pip cache` to +inspect and manage pip’s cache. + .. _`Wheel cache`: From c5150d420a6a1081a9f4f53c452a7f764ef90400 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 30 Apr 2020 06:48:03 +0530 Subject: [PATCH 1818/3170] Merge pull request #8166 from deveshks/fix-generated-file-mode --- news/8164.bugfix | 1 + src/pip/_internal/operations/install/wheel.py | 2 +- tests/unit/test_wheel.py | 31 ++++++++++++++++--- 3 files changed, 28 insertions(+), 6 deletions(-) create mode 100644 news/8164.bugfix diff --git a/news/8164.bugfix b/news/8164.bugfix new file mode 100644 index 00000000000..1707d28401a --- /dev/null +++ b/news/8164.bugfix @@ -0,0 +1 @@ +Fix metadata permission issues when umask has the executable bit set. diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index e7315ee4b52..2fb86b866db 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -567,7 +567,7 @@ def is_entrypoint_wrapper(name): if msg is not None: logger.warning(msg) - generated_file_mode = 0o666 - current_umask() + generated_file_mode = 0o666 & ~current_umask() @contextlib.contextmanager def _generate_file(path, **kwargs): diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 2350927541d..b64d4cef312 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -243,15 +243,15 @@ def assert_permission(self, path, mode): target_mode = os.stat(path).st_mode & 0o777 assert (target_mode & mode) == mode, oct(target_mode) - def assert_installed(self): + def assert_installed(self, expected_permission): # lib assert os.path.isdir( os.path.join(self.scheme.purelib, 'sample')) # dist-info metadata = os.path.join(self.dest_dist_info, 'METADATA') - self.assert_permission(metadata, 0o644) + self.assert_permission(metadata, expected_permission) record = os.path.join(self.dest_dist_info, 'RECORD') - self.assert_permission(record, 0o644) + self.assert_permission(record, expected_permission) # data files data_file = os.path.join(self.scheme.data, 'my_data', 'data_file') assert os.path.isfile(data_file) @@ -268,7 +268,28 @@ def test_std_install(self, data, tmpdir): scheme=self.scheme, req_description=str(self.req), ) - self.assert_installed() + self.assert_installed(0o644) + + @pytest.mark.parametrize("user_mask, expected_permission", [ + (0o27, 0o640) + ]) + def test_std_install_with_custom_umask(self, data, tmpdir, + user_mask, expected_permission): + """Test that the files created after install honor the permissions + set when the user sets a custom umask""" + + prev_umask = os.umask(user_mask) + try: + self.prep(data, tmpdir) + wheel.install_wheel( + self.name, + self.wheelpath, + scheme=self.scheme, + req_description=str(self.req), + ) + self.assert_installed(expected_permission) + finally: + os.umask(prev_umask) def test_std_install_with_direct_url(self, data, tmpdir): """Test that install_wheel creates direct_url.json metadata when @@ -340,7 +361,7 @@ def test_dist_info_contains_empty_dir(self, data, tmpdir): req_description=str(self.req), _temp_dir_for_testing=self.src, ) - self.assert_installed() + self.assert_installed(0o644) assert not os.path.isdir( os.path.join(self.dest_dist_info, 'empty_dir')) From a526f939db426cc20e59edde71fa4b9d96a88af6 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 5 May 2020 16:08:54 +0530 Subject: [PATCH 1819/3170] Merge pull request #8180 from sbidoul/wheel-absent-warning-sbi --- news/8178.bugfix | 2 ++ src/pip/_internal/wheel_builder.py | 16 ++++++++-------- 2 files changed, 10 insertions(+), 8 deletions(-) create mode 100644 news/8178.bugfix diff --git a/news/8178.bugfix b/news/8178.bugfix new file mode 100644 index 00000000000..6960053eda2 --- /dev/null +++ b/news/8178.bugfix @@ -0,0 +1,2 @@ +Avoid unnecessary message about the wheel package not being installed +when a wheel would not have been built. Additionally, clarify the message. diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index 6d1022d5661..fcaeeb6c3f4 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -69,14 +69,6 @@ def _should_build( # From this point, this concerns the pip install command only # (need_wheel=False). - if not req.use_pep517 and not is_wheel_installed(): - # we don't build legacy requirements if wheel is not installed - logger.info( - "Could not build wheels for %s, " - "since package 'wheel' is not installed.", req.name, - ) - return False - if req.editable or not req.source_dir: return False @@ -87,6 +79,14 @@ def _should_build( ) return False + if not req.use_pep517 and not is_wheel_installed(): + # we don't build legacy requirements if wheel is not installed + logger.info( + "Using legacy setup.py install for %s, " + "since package 'wheel' is not installed.", req.name, + ) + return False + return True From 4a063b750f107fccce9549764775d5bfb407c501 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 7 May 2020 23:57:17 +0530 Subject: [PATCH 1820/3170] Merge pull request #8167 from pradyunsg/drop-list-parallelization --- news/8167.removal | 1 + src/pip/_internal/commands/list.py | 12 +----------- 2 files changed, 2 insertions(+), 11 deletions(-) create mode 100644 news/8167.removal diff --git a/news/8167.removal b/news/8167.removal new file mode 100644 index 00000000000..d719377eb26 --- /dev/null +++ b/news/8167.removal @@ -0,0 +1 @@ +Drop parallelization from ``pip list --outdated``. diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index cf3be7eb459..2aa3075b1b7 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -5,10 +5,8 @@ import json import logging -from multiprocessing.dummy import Pool from pip._vendor import six -from pip._vendor.requests.adapters import DEFAULT_POOLSIZE from pip._internal.cli import cmdoptions from pip._internal.cli.req_command import IndexGroupCommand @@ -210,18 +208,10 @@ def latest_info(dist): dist.latest_filetype = typ return dist - # This is done for 2x speed up of requests to pypi.org - # so that "real time" of this function - # is almost equal to "user time" - pool = Pool(DEFAULT_POOLSIZE) - - for dist in pool.imap_unordered(latest_info, packages): + for dist in map(latest_info, packages): if dist is not None: yield dist - pool.close() - pool.join() - def output_package_listing(self, packages, options): packages = sorted( packages, From 60f00b837a7a21cfe2eee161dcbc12966571d8f0 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Mon, 18 May 2020 20:35:38 +0530 Subject: [PATCH 1821/3170] Upgrade flake8 to 3.8.1 Co-Authored-By: Anthony Sottile <asottile@umich.edu> --- .pre-commit-config.yaml | 2 +- news/0FD33C4F-0B92-427D-AE3B-93EE33E15621.trivial | 0 src/pip/_internal/commands/show.py | 2 +- src/pip/_internal/operations/install/wheel.py | 13 +++++++++---- src/pip/_internal/utils/misc.py | 2 +- src/pip/_internal/wheel_builder.py | 3 ++- tests/functional/test_install_reqs.py | 6 +++--- 7 files changed, 17 insertions(+), 11 deletions(-) create mode 100644 news/0FD33C4F-0B92-427D-AE3B-93EE33E15621.trivial diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 3e2c0a2ad57..0a7847c132c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -17,7 +17,7 @@ repos: exclude: .patch - repo: https://gitlab.com/pycqa/flake8 - rev: 3.7.9 + rev: 3.8.1 hooks: - id: flake8 exclude: tests/data diff --git a/news/0FD33C4F-0B92-427D-AE3B-93EE33E15621.trivial b/news/0FD33C4F-0B92-427D-AE3B-93EE33E15621.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/commands/show.py b/src/pip/_internal/commands/show.py index 8d9fa13fd91..97735f2d232 100644 --- a/src/pip/_internal/commands/show.py +++ b/src/pip/_internal/commands/show.py @@ -99,7 +99,7 @@ def get_requiring_packages(package_name): # RECORDs should be part of .dist-info metadatas if dist.has_metadata('RECORD'): lines = dist.get_metadata_lines('RECORD') - paths = [l.split(',')[0] for l in lines] + paths = [line.split(',')[0] for line in lines] paths = [os.path.join(dist.location, p) for p in paths] file_list = [os.path.relpath(p, dist.location) for p in paths] diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index a0885c48cde..8eb938f07ba 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -32,16 +32,21 @@ from pip._internal.utils.filesystem import adjacent_tmp_file, replace from pip._internal.utils.misc import captured_stdout, ensure_dir, hash_file from pip._internal.utils.temp_dir import TempDirectory -from pip._internal.utils.typing import MYPY_CHECK_RUNNING, cast +from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.unpacking import current_umask, unpack_file from pip._internal.utils.wheel import parse_wheel -if MYPY_CHECK_RUNNING: +# Use the custom cast function at runtime to make cast work, +# and import typing.cast when performing pre-commit and type +# checks +if not MYPY_CHECK_RUNNING: + from pip._internal.utils.typing import cast +else: from email.message import Message import typing # noqa F401 from typing import ( Dict, List, Optional, Sequence, Tuple, Any, - Iterable, Iterator, Callable, Set, + Iterable, Iterator, Callable, Set, IO, cast ) from pip._internal.models.scheme import Scheme @@ -607,7 +612,7 @@ def _generate_file(path, **kwargs): # Python 2 (typing.BinaryIO), leading us to explicitly # cast to typing.IO[str] as a workaround # for bad Python 2 behaviour - record_file_obj = cast('typing.IO[str]', record_file) + record_file_obj = cast('IO[str]', record_file) writer = csv.writer(record_file_obj) writer.writerows(sorted_outrows(rows)) # sort to simplify testing diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 09031825afa..65cd3888932 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -541,7 +541,7 @@ class FakeFile(object): """Wrap a list of lines in an object with readline() to make ConfigParser happy.""" def __init__(self, lines): - self._gen = (l for l in lines) + self._gen = iter(lines) def readline(self): try: diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index fcaeeb6c3f4..b6f75aa0bf3 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -36,7 +36,8 @@ def _contains_egg_info( - s, _egg_info_re=re.compile(r'([a-z0-9_.]+)-([a-z0-9_.!+-]+)', re.I)): + s, _egg_info_re=re.compile(r'([a-z0-9_.]+)-([a-z0-9_.!+-]+)', + re.IGNORECASE)): # type: (str, Pattern[str]) -> bool """Determine whether the string looks like an egg_info. diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index 0c00060a23d..40b0c3c75ec 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -420,7 +420,7 @@ def test_install_with_extras_from_install(script, data): ) result = script.pip_install_local( '-c', script.scratch_path / 'constraints.txt', 'LocalExtras[baz]') - assert script.site_packages / 'singlemodule.py'in result.files_created + assert script.site_packages / 'singlemodule.py' in result.files_created def test_install_with_extras_joined(script, data): @@ -432,7 +432,7 @@ def test_install_with_extras_joined(script, data): '-c', script.scratch_path / 'constraints.txt', 'LocalExtras[baz]' ) assert script.site_packages / 'simple' in result.files_created - assert script.site_packages / 'singlemodule.py'in result.files_created + assert script.site_packages / 'singlemodule.py' in result.files_created def test_install_with_extras_editable_joined(script, data): @@ -443,7 +443,7 @@ def test_install_with_extras_editable_joined(script, data): result = script.pip_install_local( '-c', script.scratch_path / 'constraints.txt', 'LocalExtras[baz]') assert script.site_packages / 'simple' in result.files_created - assert script.site_packages / 'singlemodule.py'in result.files_created + assert script.site_packages / 'singlemodule.py' in result.files_created def test_install_distribution_full_union(script, data): From ab6ac9d31ba7d6b7572c3f5da52968c577f34727 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 14 May 2020 16:23:03 +0530 Subject: [PATCH 1822/3170] Merge pull request #8221 from pradyunsg/revert-in-place-builds --- docs/html/reference/pip_install.rst | 17 +--- news/7555.bugfix | 2 + src/pip/_internal/operations/prepare.py | 101 +++++++++++++++----- src/pip/_internal/utils/filesystem.py | 32 +++++++ tests/functional/test_cli.py | 4 +- tests/functional/test_install.py | 26 +++++ tests/functional/test_uninstall.py | 10 +- tests/lib/filesystem.py | 48 ++++++++++ tests/unit/test_operations_prepare.py | 120 +++++++++++++++++++++++- tests/unit/test_utils_filesystem.py | 61 ++++++++++++ 10 files changed, 372 insertions(+), 49 deletions(-) create mode 100644 news/7555.bugfix create mode 100644 tests/lib/filesystem.py create mode 100644 tests/unit/test_utils_filesystem.py diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index ec58899743a..294e969f3e1 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -732,18 +732,11 @@ You can install local projects by specifying the project path to pip:: $ pip install path/to/SomeProject -pip treats this directory like an unpacked source archive, and directly -attempts installation. - -Prior to pip 20.1, pip copied the entire project directory to a temporary -location and attempted installation from that directory. This approach was the -cause of several performance issues, as well as various issues arising when the -project directory depends on its parent directory (such as the presence of a -VCS directory). The main user visible effect of this change is that secondary -build artifacts, if any, would be created in the local directory, whereas -earlier they were created in a temporary copy of the directory and then -deleted. This notably includes the ``build`` and ``.egg-info`` directories in -the case of the setuptools backend. +During regular installation, pip will copy the entire project directory to a +temporary location and install from there. The exception is that pip will +exclude .tox and .nox directories present in the top level of the project from +being copied. + .. _`editable-installs`: diff --git a/news/7555.bugfix b/news/7555.bugfix new file mode 100644 index 00000000000..f762236e235 --- /dev/null +++ b/news/7555.bugfix @@ -0,0 +1,2 @@ +Revert building of local directories in place, restoring the pre-20.1 +behaviour of copying to a temporary directory. diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 30d5e3a308c..1fcbb775ece 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -24,9 +24,15 @@ PreviousBuildDirError, VcsHashUnsupported, ) +from pip._internal.utils.filesystem import copy2_fixed from pip._internal.utils.hashes import MissingHashes from pip._internal.utils.logging import indent_log -from pip._internal.utils.misc import display_path, hide_url +from pip._internal.utils.misc import ( + display_path, + hide_url, + path_to_display, + rmtree, +) from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.unpacking import unpack_file @@ -127,6 +133,59 @@ def get_http_url( return File(from_path, content_type) +def _copy2_ignoring_special_files(src, dest): + # type: (str, str) -> None + """Copying special files is not supported, but as a convenience to users + we skip errors copying them. This supports tools that may create e.g. + socket files in the project source directory. + """ + try: + copy2_fixed(src, dest) + except shutil.SpecialFileError as e: + # SpecialFileError may be raised due to either the source or + # destination. If the destination was the cause then we would actually + # care, but since the destination directory is deleted prior to + # copy we ignore all of them assuming it is caused by the source. + logger.warning( + "Ignoring special file error '%s' encountered copying %s to %s.", + str(e), + path_to_display(src), + path_to_display(dest), + ) + + +def _copy_source_tree(source, target): + # type: (str, str) -> None + target_abspath = os.path.abspath(target) + target_basename = os.path.basename(target_abspath) + target_dirname = os.path.dirname(target_abspath) + + def ignore(d, names): + # type: (str, List[str]) -> List[str] + skipped = [] # type: List[str] + if d == source: + # Pulling in those directories can potentially be very slow, + # exclude the following directories if they appear in the top + # level dir (and only it). + # See discussion at https://github.com/pypa/pip/pull/6770 + skipped += ['.tox', '.nox'] + if os.path.abspath(d) == target_dirname: + # Prevent an infinite recursion if the target is in source. + # This can happen when TMPDIR is set to ${PWD}/... + # and we copy PWD to TMPDIR. + skipped += [target_basename] + return skipped + + kwargs = dict(ignore=ignore, symlinks=True) # type: CopytreeKwargs + + if not PY2: + # Python 2 does not support copy_function, so we only ignore + # errors on special file copy in Python 3. + kwargs['copy_function'] = _copy2_ignoring_special_files + + shutil.copytree(source, target, **kwargs) + + def get_file_url( link, # type: Link download_dir=None, # type: Optional[str] @@ -180,9 +239,11 @@ def unpack_url( unpack_vcs_link(link, location) return None - # If it's a url to a local directory, we build in-place. - # There is nothing to be done here. + # If it's a url to a local directory if link.is_existing_dir(): + if os.path.isdir(location): + rmtree(location) + _copy_source_tree(link.file_path, location) return None # file urls @@ -354,25 +415,21 @@ def prepare_linked_requirement( with indent_log(): # Since source_dir is only set for editable requirements. assert req.source_dir is None - if link.is_existing_dir(): - # Build local directories in place. - req.source_dir = link.file_path - else: - req.ensure_has_source_dir(self.build_dir, autodelete_unpacked) - # 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` - if os.path.exists(os.path.join(req.source_dir, 'setup.py')): - raise PreviousBuildDirError( - "pip can't proceed with requirements '{}' due to a" - " pre-existing build directory ({}). 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." - .format(req, req.source_dir) - ) + req.ensure_has_source_dir(self.build_dir, autodelete_unpacked) + # 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` + if os.path.exists(os.path.join(req.source_dir, 'setup.py')): + raise PreviousBuildDirError( + "pip can't proceed with requirements '{}' due to a" + " pre-existing build directory ({}). 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." + .format(req, req.source_dir) + ) # Now that we have the real link, we can tell what kind of # requirements we have and raise some more informative errors diff --git a/src/pip/_internal/utils/filesystem.py b/src/pip/_internal/utils/filesystem.py index 85010ac25a3..437a7fd1482 100644 --- a/src/pip/_internal/utils/filesystem.py +++ b/src/pip/_internal/utils/filesystem.py @@ -3,6 +3,8 @@ import os import os.path import random +import shutil +import stat import sys from contextlib import contextmanager from tempfile import NamedTemporaryFile @@ -54,6 +56,36 @@ def check_path_owner(path): return False # assume we don't own the path +def copy2_fixed(src, dest): + # type: (str, str) -> None + """Wrap shutil.copy2() but map errors copying socket files to + SpecialFileError as expected. + + See also https://bugs.python.org/issue37700. + """ + try: + shutil.copy2(src, dest) + except (OSError, IOError): + for f in [src, dest]: + try: + is_socket_file = is_socket(f) + except OSError: + # An error has already occurred. Another error here is not + # a problem and we can ignore it. + pass + else: + if is_socket_file: + raise shutil.SpecialFileError( + "`{f}` is a socket".format(**locals())) + + raise + + +def is_socket(path): + # type: (str) -> bool + return stat.S_ISSOCK(os.lstat(path).st_mode) + + @contextmanager def adjacent_tmp_file(path, **kwargs): # type: (str, **Any) -> Iterator[NamedTemporaryFileResult] diff --git a/tests/functional/test_cli.py b/tests/functional/test_cli.py index c401a7cf80f..e416315125f 100644 --- a/tests/functional/test_cli.py +++ b/tests/functional/test_cli.py @@ -27,9 +27,7 @@ def test_entrypoints_work(entrypoint, script): ) """.format(entrypoint))) - # expect_temp=True, because pip install calls setup.py which - # in turn creates fake_pkg.egg-info. - script.pip("install", "-vvv", str(fake_pkg), expect_temp=True) + script.pip("install", "-vvv", str(fake_pkg)) result = script.pip("-V") result2 = script.run("fake_pip", "-V", allow_stderr_warning=True) assert result.stdout == result2.stdout diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 571049aa447..2cb44269502 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -2,6 +2,7 @@ import glob import os import re +import shutil import ssl import sys import textwrap @@ -28,6 +29,7 @@ skip_if_python2, windows_workaround_7667, ) +from tests.lib.filesystem import make_socket_file from tests.lib.local_repos import local_checkout from tests.lib.path import Path from tests.lib.server import ( @@ -574,6 +576,30 @@ def test_install_from_local_directory_with_symlinks_to_directories( assert egg_info_folder in result.files_created, str(result) +@pytest.mark.skipif("sys.platform == 'win32' or sys.version_info < (3,)") +def test_install_from_local_directory_with_socket_file(script, data, tmpdir): + """ + Test installing from a local directory containing a socket file. + """ + egg_info_file = ( + script.site_packages / + "FSPkg-0.1.dev0-py{pyversion}.egg-info".format(**globals()) + ) + package_folder = script.site_packages / "fspkg" + to_copy = data.packages.joinpath("FSPkg") + to_install = tmpdir.joinpath("src") + + shutil.copytree(to_copy, to_install) + # Socket file, should be ignored. + socket_file_path = os.path.join(to_install, "example") + make_socket_file(socket_file_path) + + result = script.pip("install", "--verbose", to_install) + assert package_folder in result.files_created, str(result.stdout) + assert egg_info_file in result.files_created, str(result) + assert str(socket_file_path) in result.stderr + + def test_install_from_local_directory_with_no_setup_py(script, data): """ Test installing from a local directory with no 'setup.py'. diff --git a/tests/functional/test_uninstall.py b/tests/functional/test_uninstall.py index c030dbaf27b..ab41917c986 100644 --- a/tests/functional/test_uninstall.py +++ b/tests/functional/test_uninstall.py @@ -271,15 +271,7 @@ def test_uninstall_console_scripts(script): sorted(result.files_created.keys()) ) result2 = script.pip('uninstall', 'discover', '-y') - assert_all_changes( - result, - result2, - [ - script.venv / 'build', - 'cache', - script.scratch / 'discover' / 'discover.egg-info', - ] - ) + assert_all_changes(result, result2, [script.venv / 'build', 'cache']) def test_uninstall_console_scripts_uppercase_name(script): diff --git a/tests/lib/filesystem.py b/tests/lib/filesystem.py new file mode 100644 index 00000000000..dc14b323e33 --- /dev/null +++ b/tests/lib/filesystem.py @@ -0,0 +1,48 @@ +"""Helpers for filesystem-dependent tests. +""" +import os +import socket +import subprocess +import sys +from functools import partial +from itertools import chain + +from .path import Path + + +def make_socket_file(path): + # Socket paths are limited to 108 characters (sometimes less) so we + # chdir before creating it and use a relative path name. + cwd = os.getcwd() + os.chdir(os.path.dirname(path)) + try: + sock = socket.socket(socket.AF_UNIX) + sock.bind(os.path.basename(path)) + finally: + os.chdir(cwd) + + +def make_unreadable_file(path): + Path(path).touch() + os.chmod(path, 0o000) + if sys.platform == "win32": + # Once we drop PY2 we can use `os.getlogin()` instead. + username = os.environ["USERNAME"] + # Remove "Read Data/List Directory" permission for current user, but + # leave everything else. + args = ["icacls", path, "/deny", username + ":(RD)"] + subprocess.check_call(args) + + +def get_filelist(base): + def join(dirpath, dirnames, filenames): + relative_dirpath = os.path.relpath(dirpath, base) + join_dirpath = partial(os.path.join, relative_dirpath) + return chain( + (join_dirpath(p) for p in dirnames), + (join_dirpath(p) for p in filenames), + ) + + return set(chain.from_iterable( + join(*dirinfo) for dirinfo in os.walk(base) + )) diff --git a/tests/unit/test_operations_prepare.py b/tests/unit/test_operations_prepare.py index bcfc8148669..0158eed5197 100644 --- a/tests/unit/test_operations_prepare.py +++ b/tests/unit/test_operations_prepare.py @@ -10,9 +10,18 @@ from pip._internal.models.link import Link from pip._internal.network.download import Downloader from pip._internal.network.session import PipSession -from pip._internal.operations.prepare import _download_http_url, unpack_url +from pip._internal.operations.prepare import ( + _copy_source_tree, + _download_http_url, + unpack_url, +) from pip._internal.utils.hashes import Hashes from pip._internal.utils.urls import path_to_url +from tests.lib.filesystem import ( + get_filelist, + make_socket_file, + make_unreadable_file, +) from tests.lib.path import Path from tests.lib.requests_mocks import MockResponse @@ -92,6 +101,76 @@ def clean_project(tmpdir_factory, data): return new_project_dir +def test_copy_source_tree(clean_project, tmpdir): + target = tmpdir.joinpath("target") + expected_files = get_filelist(clean_project) + assert len(expected_files) == 3 + + _copy_source_tree(clean_project, target) + + copied_files = get_filelist(target) + assert expected_files == copied_files + + +@pytest.mark.skipif("sys.platform == 'win32' or sys.version_info < (3,)") +def test_copy_source_tree_with_socket(clean_project, tmpdir, caplog): + target = tmpdir.joinpath("target") + expected_files = get_filelist(clean_project) + socket_path = str(clean_project.joinpath("aaa")) + make_socket_file(socket_path) + + _copy_source_tree(clean_project, target) + + copied_files = get_filelist(target) + assert expected_files == copied_files + + # Warning should have been logged. + assert len(caplog.records) == 1 + record = caplog.records[0] + assert record.levelname == 'WARNING' + assert socket_path in record.message + + +@pytest.mark.skipif("sys.platform == 'win32' or sys.version_info < (3,)") +def test_copy_source_tree_with_socket_fails_with_no_socket_error( + clean_project, tmpdir +): + target = tmpdir.joinpath("target") + expected_files = get_filelist(clean_project) + make_socket_file(clean_project.joinpath("aaa")) + unreadable_file = clean_project.joinpath("bbb") + make_unreadable_file(unreadable_file) + + with pytest.raises(shutil.Error) as e: + _copy_source_tree(clean_project, target) + + errored_files = [err[0] for err in e.value.args[0]] + assert len(errored_files) == 1 + assert unreadable_file in errored_files + + copied_files = get_filelist(target) + # All files without errors should have been copied. + assert expected_files == copied_files + + +def test_copy_source_tree_with_unreadable_dir_fails(clean_project, tmpdir): + target = tmpdir.joinpath("target") + expected_files = get_filelist(clean_project) + unreadable_file = clean_project.joinpath("bbb") + make_unreadable_file(unreadable_file) + + with pytest.raises(shutil.Error) as e: + _copy_source_tree(clean_project, target) + + errored_files = [err[0] for err in e.value.args[0]] + assert len(errored_files) == 1 + assert unreadable_file in errored_files + + copied_files = get_filelist(target) + # All files without errors should have been copied. + assert expected_files == copied_files + + class Test_unpack_url(object): def prep(self, tmpdir, data): @@ -135,5 +214,40 @@ def test_unpack_url_thats_a_dir(self, tmpdir, data): unpack_url(dist_url, self.build_dir, downloader=self.no_downloader, download_dir=self.download_dir) - # test that nothing was copied to build_dir since we build in place - assert not os.path.exists(os.path.join(self.build_dir, 'fspkg')) + assert os.path.isdir(os.path.join(self.build_dir, 'fspkg')) + + +@pytest.mark.parametrize('exclude_dir', [ + '.nox', + '.tox' +]) +def test_unpack_url_excludes_expected_dirs(tmpdir, exclude_dir): + src_dir = tmpdir / 'src' + dst_dir = tmpdir / 'dst' + src_included_file = src_dir.joinpath('file.txt') + src_excluded_dir = src_dir.joinpath(exclude_dir) + src_excluded_file = src_dir.joinpath(exclude_dir, 'file.txt') + src_included_dir = src_dir.joinpath('subdir', exclude_dir) + + # set up source directory + src_excluded_dir.mkdir(parents=True) + src_included_dir.mkdir(parents=True) + src_included_file.touch() + src_excluded_file.touch() + + dst_included_file = dst_dir.joinpath('file.txt') + dst_excluded_dir = dst_dir.joinpath(exclude_dir) + dst_excluded_file = dst_dir.joinpath(exclude_dir, 'file.txt') + dst_included_dir = dst_dir.joinpath('subdir', exclude_dir) + + src_link = Link(path_to_url(src_dir)) + unpack_url( + src_link, + dst_dir, + Mock(side_effect=AssertionError), + download_dir=None + ) + assert not os.path.isdir(dst_excluded_dir) + assert not os.path.isfile(dst_excluded_file) + assert os.path.isfile(dst_included_file) + assert os.path.isdir(dst_included_dir) diff --git a/tests/unit/test_utils_filesystem.py b/tests/unit/test_utils_filesystem.py new file mode 100644 index 00000000000..3ef814dce4b --- /dev/null +++ b/tests/unit/test_utils_filesystem.py @@ -0,0 +1,61 @@ +import os +import shutil + +import pytest + +from pip._internal.utils.filesystem import copy2_fixed, is_socket +from tests.lib.filesystem import make_socket_file, make_unreadable_file +from tests.lib.path import Path + + +def make_file(path): + Path(path).touch() + + +def make_valid_symlink(path): + target = path + "1" + make_file(target) + os.symlink(target, path) + + +def make_broken_symlink(path): + os.symlink("foo", path) + + +def make_dir(path): + os.mkdir(path) + + +skip_on_windows = pytest.mark.skipif("sys.platform == 'win32'") + + +@skip_on_windows +@pytest.mark.parametrize("create,result", [ + (make_socket_file, True), + (make_file, False), + (make_valid_symlink, False), + (make_broken_symlink, False), + (make_dir, False), +]) +def test_is_socket(create, result, tmpdir): + target = tmpdir.joinpath("target") + create(target) + assert os.path.lexists(target) + assert is_socket(target) == result + + +@pytest.mark.parametrize("create,error_type", [ + pytest.param( + make_socket_file, shutil.SpecialFileError, marks=skip_on_windows + ), + (make_unreadable_file, OSError), +]) +def test_copy2_fixed_raises_appropriate_errors(create, error_type, tmpdir): + src = tmpdir.joinpath("src") + create(src) + dest = tmpdir.joinpath("dest") + + with pytest.raises(error_type): + copy2_fixed(src, dest) + + assert not dest.exists() From f7cd93a5ded0086017e7062f0fd198cc77ad6a4b Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Mon, 18 May 2020 01:58:29 +0530 Subject: [PATCH 1823/3170] Type annotations for pip._internal.commands.freeze --- src/pip/_internal/commands/freeze.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/commands/freeze.py b/src/pip/_internal/commands/freeze.py index 13171772e5c..5b9a78ff5ec 100644 --- a/src/pip/_internal/commands/freeze.py +++ b/src/pip/_internal/commands/freeze.py @@ -1,6 +1,3 @@ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - from __future__ import absolute_import import sys @@ -8,12 +5,18 @@ from pip._internal.cache import WheelCache from pip._internal.cli import cmdoptions from pip._internal.cli.base_command import Command +from pip._internal.cli.status_codes import SUCCESS from pip._internal.models.format_control import FormatControl from pip._internal.operations.freeze import freeze from pip._internal.utils.compat import stdlib_pkgs +from pip._internal.utils.typing import MYPY_CHECK_RUNNING DEV_PKGS = {'pip', 'setuptools', 'distribute', 'wheel'} +if MYPY_CHECK_RUNNING: + from optparse import Values + from typing import Any, List + class FreezeCommand(Command): """ @@ -27,6 +30,7 @@ class FreezeCommand(Command): log_streams = ("ext://sys.stderr", "ext://sys.stderr") def __init__(self, *args, **kw): + # type: (*Any, **Any) -> None super(FreezeCommand, self).__init__(*args, **kw) self.cmd_opts.add_option( @@ -75,6 +79,7 @@ def __init__(self, *args, **kw): self.parser.insert_option_group(0, self.cmd_opts) def run(self, options, args): + # type: (Values, List[str]) -> int format_control = FormatControl(set(), set()) wheel_cache = WheelCache(options.cache_dir, format_control) skip = set(stdlib_pkgs) @@ -97,3 +102,4 @@ def run(self, options, args): for line in freeze(**freeze_kwargs): sys.stdout.write(line + '\n') + return SUCCESS From 2da7a2b077169db62bf38d7b554a045f633c9d87 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Mon, 18 May 2020 02:00:22 +0530 Subject: [PATCH 1824/3170] Type annotations for pip._internal.commands.download --- src/pip/_internal/commands/download.py | 14 ++++++++++---- 1 file changed, 10 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index c829550633e..570d7289c37 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -1,6 +1,3 @@ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - from __future__ import absolute_import import logging @@ -12,6 +9,12 @@ from pip._internal.req.req_tracker import get_requirement_tracker from pip._internal.utils.misc import ensure_dir, normalize_path, write_output from pip._internal.utils.temp_dir import TempDirectory +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from optparse import Values + from typing import Any, List + from pip._internal.req.req_set import RequirementSet logger = logging.getLogger(__name__) @@ -37,6 +40,7 @@ class DownloadCommand(RequirementCommand): %prog [options] <archive url/path> ...""" def __init__(self, *args, **kw): + # type: (*Any, **Any) -> None super(DownloadCommand, self).__init__(*args, **kw) cmd_opts = self.cmd_opts @@ -77,6 +81,8 @@ def __init__(self, *args, **kw): @with_cleanup def run(self, options, args): + # type: (Values, List[str]) -> RequirementSet + options.ignore_installed = True # editable doesn't really make sense for `pip download`, but the bowels # of the RequirementSet code require that property. @@ -134,7 +140,7 @@ def run(self, options, args): downloaded = ' '.join([ req.name for req in requirement_set.requirements.values() - if req.successfully_downloaded + if req.successfully_downloaded and req.name ]) if downloaded: write_output('Successfully downloaded %s', downloaded) From 4f98df6e3e61d50ea0c584d1297cd1f8bbb1c2e2 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Mon, 18 May 2020 02:00:41 +0530 Subject: [PATCH 1825/3170] Type annotations for pip._internal.commands.search --- src/pip/_internal/commands/search.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/commands/search.py b/src/pip/_internal/commands/search.py index e5f286ea5bf..8d581b9bdfc 100644 --- a/src/pip/_internal/commands/search.py +++ b/src/pip/_internal/commands/search.py @@ -1,6 +1,3 @@ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - from __future__ import absolute_import import logging @@ -23,6 +20,12 @@ from pip._internal.utils.compat import get_terminal_size from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import write_output +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from optparse import Values + from typing import Any, List, Dict, Optional + logger = logging.getLogger(__name__) @@ -35,6 +38,7 @@ class SearchCommand(Command, SessionCommandMixin): ignore_require_venv = True def __init__(self, *args, **kw): + # type: (*Any, **Any) -> None super(SearchCommand, self).__init__(*args, **kw) self.cmd_opts.add_option( '-i', '--index', @@ -46,6 +50,7 @@ def __init__(self, *args, **kw): self.parser.insert_option_group(0, self.cmd_opts) def run(self, options, args): + # type: (Values, List[str]) -> int if not args: raise CommandError('Missing required argument (search query).') query = args @@ -62,6 +67,7 @@ def run(self, options, args): return NO_MATCHES_FOUND def search(self, query, options): + # type: (List[str], Values) -> List[Dict[str, str]] index_url = options.index session = self.get_default_session(options) @@ -73,12 +79,13 @@ def search(self, query, options): def transform_hits(hits): + # type: (List[Dict[str, str]]) -> List[Dict[str, Any]] """ The list from pypi is really a list of versions. We want a list of packages with the list of versions stored inline. This converts the list from pypi into one we can use. """ - packages = OrderedDict() + packages = OrderedDict() # type: OrderedDict[str, Any] for hit in hits: name = hit['name'] summary = hit['summary'] @@ -101,6 +108,7 @@ def transform_hits(hits): def print_results(hits, name_column_width=None, terminal_width=None): + # type: (List[Dict[str, Any]], Optional[int], Optional[int]) -> None if not hits: return if name_column_width is None: @@ -143,4 +151,5 @@ def print_results(hits, name_column_width=None, terminal_width=None): def highest_version(versions): + # type: (List[str]) -> str return max(versions, key=parse_version) From bfe633603510fb1fb037b8533c6f10b061d07d16 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 19 May 2020 13:11:56 +0530 Subject: [PATCH 1826/3170] NEWS: in-tree builds revert is a removal, not bugfix --- news/{7555.bugfix => 7555.removal} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename news/{7555.bugfix => 7555.removal} (100%) diff --git a/news/7555.bugfix b/news/7555.removal similarity index 100% rename from news/7555.bugfix rename to news/7555.removal From 8356bc5161e0a9b3054f0e04e12d18feae4c2b46 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 19 May 2020 13:12:13 +0530 Subject: [PATCH 1827/3170] Bump for release --- NEWS.rst | 18 ++++++++++++++++++ news/7555.removal | 2 -- news/8164.bugfix | 1 - news/8167.removal | 1 - news/8178.bugfix | 2 -- src/pip/__init__.py | 2 +- 6 files changed, 19 insertions(+), 7 deletions(-) delete mode 100644 news/7555.removal delete mode 100644 news/8164.bugfix delete mode 100644 news/8167.removal delete mode 100644 news/8178.bugfix diff --git a/NEWS.rst b/NEWS.rst index 78d8d52252b..917580f17dd 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -7,6 +7,24 @@ .. towncrier release notes start +20.1.1 (2020-05-19) +=================== + +Deprecations and Removals +------------------------- + +- Revert building of local directories in place, restoring the pre-20.1 + behaviour of copying to a temporary directory. (`#7555 <https://github.com/pypa/pip/issues/7555>`_) +- Drop parallelization from ``pip list --outdated``. (`#8167 <https://github.com/pypa/pip/issues/8167>`_) + +Bug Fixes +--------- + +- Fix metadata permission issues when umask has the executable bit set. (`#8164 <https://github.com/pypa/pip/issues/8164>`_) +- Avoid unnecessary message about the wheel package not being installed + when a wheel would not have been built. Additionally, clarify the message. (`#8178 <https://github.com/pypa/pip/issues/8178>`_) + + 20.1 (2020-04-28) ================= diff --git a/news/7555.removal b/news/7555.removal deleted file mode 100644 index f762236e235..00000000000 --- a/news/7555.removal +++ /dev/null @@ -1,2 +0,0 @@ -Revert building of local directories in place, restoring the pre-20.1 -behaviour of copying to a temporary directory. diff --git a/news/8164.bugfix b/news/8164.bugfix deleted file mode 100644 index 1707d28401a..00000000000 --- a/news/8164.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix metadata permission issues when umask has the executable bit set. diff --git a/news/8167.removal b/news/8167.removal deleted file mode 100644 index d719377eb26..00000000000 --- a/news/8167.removal +++ /dev/null @@ -1 +0,0 @@ -Drop parallelization from ``pip list --outdated``. diff --git a/news/8178.bugfix b/news/8178.bugfix deleted file mode 100644 index 6960053eda2..00000000000 --- a/news/8178.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -Avoid unnecessary message about the wheel package not being installed -when a wheel would not have been built. Additionally, clarify the message. diff --git a/src/pip/__init__.py b/src/pip/__init__.py index 3d249cecf1a..3dcf3a9deea 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -4,7 +4,7 @@ from typing import List, Optional -__version__ = "20.1" +__version__ = "20.1.1" def main(args=None): From 656917ac5e7ca30d7c87e1300b300cf63f50fd7e Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Tue, 19 May 2020 13:12:13 +0530 Subject: [PATCH 1828/3170] Bump for development --- src/pip/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/__init__.py b/src/pip/__init__.py index 3dcf3a9deea..dc41d31b63e 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -4,7 +4,7 @@ from typing import List, Optional -__version__ = "20.1.1" +__version__ = "20.2.dev0" def main(args=None): From 2339a7a859ebb0ba8c498b48c6931ede93a9279d Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 12 May 2020 18:34:30 +0800 Subject: [PATCH 1829/3170] Fix installation of wheel with non-ASCII entries This mainly deals with correctly recording the wheel content in the RECORD metadata. This metadata file must be written in UTF-8, but the actual files need to be installed to the filesystem, the encoding of which is not (always) UTF-8. So we need to carefully handle file name encoding/decoding when comparing RECORD entries to the actual file. The fix here makes sure we always use the correct encoding by adding strict type hints. The entries in RECORD is decoded/encoded with UTF-8 on the read/write boundaries to make sure we always deal with text types. A type-hint-only type RecordPath is introduced to make sure this is enforced (because Python 2 "helpfully" coerces str to unicode with the wrong encoding). --- src/pip/_internal/operations/install/wheel.py | 126 +++++++++++------- tests/functional/test_install_wheel.py | 29 ++++ 2 files changed, 109 insertions(+), 46 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 8eb938f07ba..496aa772abc 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -24,7 +24,14 @@ from pip._vendor import pkg_resources from pip._vendor.distlib.scripts import ScriptMaker from pip._vendor.distlib.util import get_export_entry -from pip._vendor.six import StringIO +from pip._vendor.six import ( + PY2, + StringIO, + ensure_str, + ensure_text, + itervalues, + text_type, +) from pip._internal.exceptions import InstallationError from pip._internal.locations import get_major_minor_version @@ -43,26 +50,33 @@ from pip._internal.utils.typing import cast else: from email.message import Message - import typing # noqa F401 from typing import ( - Dict, List, Optional, Sequence, Tuple, Any, - Iterable, Iterator, Callable, Set, IO, cast + Any, + Callable, + Dict, + IO, + Iterable, + Iterator, + List, + NewType, + Optional, + Sequence, + Set, + Tuple, + Union, + cast, ) from pip._internal.models.scheme import Scheme from pip._internal.utils.filesystem import NamedTemporaryFileResult - InstalledCSVRow = Tuple[str, ...] + RecordPath = NewType('RecordPath', text_type) + InstalledCSVRow = Tuple[RecordPath, str, Union[int, str]] logger = logging.getLogger(__name__) -def normpath(src, p): - # type: (str, str) -> str - return os.path.relpath(src, p).replace(os.path.sep, '/') - - def rehash(path, blocksize=1 << 20): # type: (str, int) -> Tuple[str, str] """Return (encoded_digest, length) for path using hashlib.sha256()""" @@ -79,10 +93,10 @@ def csv_io_kwargs(mode): """Return keyword arguments to properly open a CSV file in the given mode. """ - if sys.version_info.major < 3: + if PY2: return {'mode': '{}b'.format(mode)} else: - return {'mode': mode, 'newline': ''} + return {'mode': mode, 'newline': '', 'encoding': 'utf-8'} def fix_script(path): @@ -217,9 +231,12 @@ def message_about_scripts_not_on_PATH(scripts): return "\n".join(msg_lines) -def sorted_outrows(outrows): - # type: (Iterable[InstalledCSVRow]) -> List[InstalledCSVRow] - """Return the given rows of a RECORD file in sorted order. +def _normalized_outrows(outrows): + # type: (Iterable[InstalledCSVRow]) -> List[Tuple[str, str, str]] + """Normalize the given rows of a RECORD file. + + Items in each row are converted into str. Rows are then sorted to make + the value more predictable for tests. Each row is a 3-tuple (path, hash, size) and corresponds to a record of a RECORD file (see PEP 376 and PEP 427 for details). For the rows @@ -234,13 +251,36 @@ def sorted_outrows(outrows): # coerce each element to a string to avoid a TypeError in this case. # For additional background, see-- # https://github.com/pypa/pip/issues/5868 - return sorted(outrows, key=lambda row: tuple(str(x) for x in row)) + return sorted( + (ensure_str(row[0], encoding='utf-8'), row[1], str(row[2])) + for row in outrows + ) + + +def _record_to_fs_path(record_path): + # type: (RecordPath) -> str + return ensure_str(record_path, encoding=sys.getfilesystemencoding()) + + +def _fs_to_record_path(path, relative_to=None): + # type: (str, Optional[str]) -> RecordPath + if relative_to is not None: + path = os.path.relpath(path, relative_to) + path = path.replace(os.path.sep, '/') + p = ensure_text(path, encoding=sys.getfilesystemencoding()) + return cast('RecordPath', p) + + +def _parse_record_path(record_column): + # type: (str) -> RecordPath + p = ensure_text(record_column, encoding='utf-8') + return cast('RecordPath', p) def get_csv_rows_for_installed( old_csv_rows, # type: Iterable[List[str]] - installed, # type: Dict[str, str] - changed, # type: Set[str] + installed, # type: Dict[RecordPath, RecordPath] + changed, # type: Set[RecordPath] generated, # type: List[str] lib_dir, # type: str ): @@ -255,21 +295,20 @@ def get_csv_rows_for_installed( logger.warning( 'RECORD line has more than three elements: {}'.format(row) ) - # Make a copy because we are mutating the row. - row = list(row) - old_path = row[0] - new_path = installed.pop(old_path, old_path) - row[0] = new_path - if new_path in changed: - digest, length = rehash(new_path) - row[1] = digest - row[2] = length - installed_rows.append(tuple(row)) + old_record_path = _parse_record_path(row[0]) + new_record_path = installed.pop(old_record_path, old_record_path) + if new_record_path in changed: + digest, length = rehash(_record_to_fs_path(new_record_path)) + else: + digest = row[1] if len(row) > 1 else '' + length = row[2] if len(row) > 2 else '' + installed_rows.append((new_record_path, digest, length)) for f in generated: + path = _fs_to_record_path(f, lib_dir) digest, length = rehash(f) - installed_rows.append((normpath(f, lib_dir), digest, str(length))) - for f in installed: - installed_rows.append((installed[f], '', '')) + installed_rows.append((path, digest, length)) + for installed_record_path in itervalues(installed): + installed_rows.append((installed_record_path, '', '')) return installed_rows @@ -338,8 +377,8 @@ def install_unpacked_wheel( # installed = files copied from the wheel to the destination # changed = files changed while installing (scripts #! line typically) # generated = files newly generated during the install (script wrappers) - installed = {} # type: Dict[str, str] - changed = set() + installed = {} # type: Dict[RecordPath, RecordPath] + changed = set() # type: Set[RecordPath] generated = [] # type: List[str] # Compile all of the pyc files that we're going to be installing @@ -353,11 +392,11 @@ def install_unpacked_wheel( def record_installed(srcfile, destfile, modified=False): # type: (str, str, bool) -> None """Map archive RECORD paths to installation RECORD paths.""" - oldpath = normpath(srcfile, wheeldir) - newpath = normpath(destfile, lib_dir) + oldpath = _fs_to_record_path(srcfile, wheeldir) + newpath = _fs_to_record_path(destfile, lib_dir) installed[oldpath] = newpath if modified: - changed.add(destfile) + changed.add(_fs_to_record_path(destfile)) def clobber( source, # type: str @@ -606,16 +645,11 @@ def _generate_file(path, **kwargs): generated=generated, lib_dir=lib_dir) with _generate_file(record_path, **csv_io_kwargs('w')) as record_file: - - # The type mypy infers for record_file using reveal_type - # is different for Python 3 (typing.IO[Any]) and - # Python 2 (typing.BinaryIO), leading us to explicitly - # cast to typing.IO[str] as a workaround - # for bad Python 2 behaviour - record_file_obj = cast('IO[str]', record_file) - - writer = csv.writer(record_file_obj) - writer.writerows(sorted_outrows(rows)) # sort to simplify testing + # The type mypy infers for record_file is different for Python 3 + # (typing.IO[Any]) and Python 2 (typing.BinaryIO). We explicitly + # cast to typing.IO[str] as a workaround. + writer = csv.writer(cast('IO[str]', record_file)) + writer.writerows(_normalized_outrows(rows)) def install_wheel( diff --git a/tests/functional/test_install_wheel.py b/tests/functional/test_install_wheel.py index 6f7b77597ed..22cdec8f042 100644 --- a/tests/functional/test_install_wheel.py +++ b/tests/functional/test_install_wheel.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- + import distutils import glob import os @@ -125,6 +127,33 @@ def test_basic_install_from_wheel_file(script, data): result.stdout) +def test_basic_install_from_unicode_wheel(script, data): + """ + Test installing from a wheel (that has a script) + """ + make_wheel( + 'unicode_package', + '1.0', + extra_files={ + u'வணக்கம்/__init__.py': b'', + u'வணக்கம்/નમસ્તે.py': b'', + }, + ).save_to_dir(script.scratch_path) + + result = script.pip( + 'install', 'unicode_package==1.0', '--no-index', + '--find-links', script.scratch_path, + ) + dist_info_folder = script.site_packages / 'unicode_package-1.0.dist-info' + assert dist_info_folder in result.files_created, str(result) + + file1 = script.site_packages.joinpath(u'வணக்கம்', '__init__.py') + assert file1 in result.files_created, str(result) + + file2 = script.site_packages.joinpath(u'வணக்கம்', u'નમસ્તે.py') + assert file2 in result.files_created, str(result) + + def test_install_from_wheel_with_headers(script, data): """ Test installing from a wheel file with headers From 43b237051ecd035803be08c2376eb15cf52185cb Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 12 May 2020 18:49:39 +0800 Subject: [PATCH 1830/3170] News --- news/5712.bugfix | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 news/5712.bugfix diff --git a/news/5712.bugfix b/news/5712.bugfix new file mode 100644 index 00000000000..c6cf0d5b5da --- /dev/null +++ b/news/5712.bugfix @@ -0,0 +1,2 @@ +Correctly treat wheels contenting non-ASCII file contents so they can be +installed on Windows. From 7a54132b1640769141df84bc7f94154fdb5e624b Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 12 May 2020 20:21:59 +0800 Subject: [PATCH 1831/3170] Can drop the strict-optional marker now --- src/pip/_internal/operations/install/wheel.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 496aa772abc..08d096b95ab 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -1,9 +1,6 @@ """Support for installing and building the "wheel" binary package format. """ -# The following comment should be removed at some point in the future. -# mypy: strict-optional=False - from __future__ import absolute_import import collections From 776a55a4199601337f99bf102677789594336df1 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 12 May 2020 21:18:27 +0800 Subject: [PATCH 1832/3170] Fix unit tests on module-internal functions --- tests/unit/test_wheel.py | 29 ++++++++++++++++++++--------- 1 file changed, 20 insertions(+), 9 deletions(-) diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index b64d4cef312..15217e2c79e 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -1,3 +1,5 @@ +# -*- coding: utf-8 -*- + """Tests for wheel binary packages and .dist-info.""" import csv import logging @@ -114,8 +116,8 @@ def test_raise_for_invalid_entrypoint_fail(entrypoint): @pytest.mark.parametrize("outrows, expected", [ ([ - ('', '', 'a'), - ('', '', ''), + (u'', '', 'a'), + (u'', '', ''), ], [ ('', '', ''), ('', '', 'a'), @@ -123,15 +125,24 @@ def test_raise_for_invalid_entrypoint_fail(entrypoint): ([ # Include an int to check avoiding the following error: # > TypeError: '<' not supported between instances of 'str' and 'int' - ('', '', 1), + (u'', '', 1), + (u'', '', ''), + ], [ ('', '', ''), + ('', '', '1'), + ]), + ([ + # Include an int to check avoiding the following error: + # > TypeError: '<' not supported between instances of 'str' and 'int' + (u'😉', '', 1), + (u'', '', ''), ], [ ('', '', ''), - ('', '', 1), + ('😉', '', '1'), ]), ]) -def test_sorted_outrows(outrows, expected): - actual = wheel.sorted_outrows(outrows) +def test_normalized_outrows(outrows, expected): + actual = wheel._normalized_outrows(outrows) assert actual == expected @@ -141,7 +152,7 @@ def call_get_csv_rows_for_installed(tmpdir, text): # Test that an installed file appearing in RECORD has its filename # updated in the new RECORD file. - installed = {'a': 'z'} + installed = {u'a': 'z'} changed = set() generated = [] lib_dir = '/lib/dir' @@ -180,9 +191,9 @@ def test_get_csv_rows_for_installed__long_lines(tmpdir, caplog): outrows = call_get_csv_rows_for_installed(tmpdir, text) expected = [ - ('z', 'b', 'c', 'd'), + ('z', 'b', 'c'), ('e', 'f', 'g'), - ('h', 'i', 'j', 'k'), + ('h', 'i', 'j'), ] assert outrows == expected From dc4171c213aba7a4c6dc943713f27b55b8d64d61 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 12 May 2020 21:33:05 +0800 Subject: [PATCH 1833/3170] Python 2 works but not tested --- src/pip/_internal/operations/install/wheel.py | 27 ++++++++++++------- src/pip/_internal/utils/misc.py | 2 +- src/pip/_internal/utils/temp_dir.py | 5 +++- tests/functional/test_install_wheel.py | 11 +++++--- 4 files changed, 30 insertions(+), 15 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 08d096b95ab..7f6f702f6f4 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -256,16 +256,15 @@ def _normalized_outrows(outrows): def _record_to_fs_path(record_path): # type: (RecordPath) -> str - return ensure_str(record_path, encoding=sys.getfilesystemencoding()) + return record_path def _fs_to_record_path(path, relative_to=None): - # type: (str, Optional[str]) -> RecordPath + # type: (text_type, Optional[text_type]) -> RecordPath if relative_to is not None: path = os.path.relpath(path, relative_to) path = path.replace(os.path.sep, '/') - p = ensure_text(path, encoding=sys.getfilesystemencoding()) - return cast('RecordPath', p) + return cast('RecordPath', path) def _parse_record_path(record_column): @@ -387,7 +386,7 @@ def install_unpacked_wheel( logger.debug(stdout.getvalue()) def record_installed(srcfile, destfile, modified=False): - # type: (str, str, bool) -> None + # type: (text_type, text_type, bool) -> None """Map archive RECORD paths to installation RECORD paths.""" oldpath = _fs_to_record_path(srcfile, wheeldir) newpath = _fs_to_record_path(destfile, lib_dir) @@ -396,8 +395,8 @@ def record_installed(srcfile, destfile, modified=False): changed.add(_fs_to_record_path(destfile)) def clobber( - source, # type: str - dest, # type: str + source, # type: text_type + dest, # type: text_type is_base, # type: bool fixer=None, # type: Optional[Callable[[str], Any]] filter=None # type: Optional[Callable[[str], bool]] @@ -459,7 +458,11 @@ def clobber( changed = fixer(destfile) record_installed(srcfile, destfile, changed) - clobber(source, lib_dir, True) + clobber( + ensure_text(source, encoding=sys.getfilesystemencoding()), + ensure_text(lib_dir, encoding=sys.getfilesystemencoding()), + True, + ) dest_info_dir = os.path.join(lib_dir, info_dir) @@ -492,7 +495,13 @@ def is_entrypoint_wrapper(name): filter = is_entrypoint_wrapper source = os.path.join(wheeldir, datadir, subdir) dest = getattr(scheme, subdir) - clobber(source, dest, False, fixer=fixer, filter=filter) + clobber( + ensure_text(source, encoding=sys.getfilesystemencoding()), + ensure_text(dest, encoding=sys.getfilesystemencoding()), + False, + fixer=fixer, + filter=filter, + ) maker = PipScriptMaker(None, scheme.scripts) diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 65cd3888932..719a8c335c2 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -131,7 +131,7 @@ def get_prog(): # Retry every half second for up to 3 seconds @retry(stop_max_delay=3000, wait_fixed=500) def rmtree(dir, ignore_errors=False): - # type: (str, bool) -> None + # type: (Text, bool) -> None shutil.rmtree(dir, ignore_errors=ignore_errors, onerror=rmtree_errorhandler) diff --git a/src/pip/_internal/utils/temp_dir.py b/src/pip/_internal/utils/temp_dir.py index 201ba6d9811..9d2dbecf279 100644 --- a/src/pip/_internal/utils/temp_dir.py +++ b/src/pip/_internal/utils/temp_dir.py @@ -8,6 +8,7 @@ from contextlib import contextmanager from pip._vendor.contextlib2 import ExitStack +from pip._vendor.six import ensure_text from pip._internal.utils.misc import enum, rmtree from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -193,7 +194,9 @@ def cleanup(self): """ self._deleted = True if os.path.exists(self._path): - rmtree(self._path) + # Make sure to pass unicode on Python 2 so the contents with weird + # names are also in unicode and can be deleted correctly. + rmtree(ensure_text(self._path)) class AdjacentTempDirectory(TempDirectory): diff --git a/tests/functional/test_install_wheel.py b/tests/functional/test_install_wheel.py index 22cdec8f042..a45ce165b22 100644 --- a/tests/functional/test_install_wheel.py +++ b/tests/functional/test_install_wheel.py @@ -127,6 +127,9 @@ def test_basic_install_from_wheel_file(script, data): result.stdout) +# Installation works, but Path fails to check those weird names on Python 2. +# I really don't care now since we're desupporting it soon anyway. +@skip_if_python2 def test_basic_install_from_unicode_wheel(script, data): """ Test installing from a wheel (that has a script) @@ -135,8 +138,8 @@ def test_basic_install_from_unicode_wheel(script, data): 'unicode_package', '1.0', extra_files={ - u'வணக்கம்/__init__.py': b'', - u'வணக்கம்/નમસ્તે.py': b'', + 'வணக்கம்/__init__.py': b'', + 'வணக்கம்/નમસ્તે.py': b'', }, ).save_to_dir(script.scratch_path) @@ -147,10 +150,10 @@ def test_basic_install_from_unicode_wheel(script, data): dist_info_folder = script.site_packages / 'unicode_package-1.0.dist-info' assert dist_info_folder in result.files_created, str(result) - file1 = script.site_packages.joinpath(u'வணக்கம்', '__init__.py') + file1 = script.site_packages.joinpath('வணக்கம்', '__init__.py') assert file1 in result.files_created, str(result) - file2 = script.site_packages.joinpath(u'வணக்கம்', u'નમસ્તે.py') + file2 = script.site_packages.joinpath('வணக்கம்', 'નમસ્તે.py') assert file2 in result.files_created, str(result) From aef15104c34cd4c1942873c0f647f0783bae319e Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 13 May 2020 06:33:05 +0800 Subject: [PATCH 1834/3170] Fix test comment --- tests/unit/test_wheel.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 15217e2c79e..2834b18f087 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -132,8 +132,7 @@ def test_raise_for_invalid_entrypoint_fail(entrypoint): ('', '', '1'), ]), ([ - # Include an int to check avoiding the following error: - # > TypeError: '<' not supported between instances of 'str' and 'int' + # Test the normalization correctly encode everything for csv.writer(). (u'😉', '', 1), (u'', '', ''), ], [ From 10566816868c85d618ea40ae5db741c600d040f3 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 13 May 2020 06:56:20 +0800 Subject: [PATCH 1835/3170] Non-ASCII names are not weird --- src/pip/_internal/utils/temp_dir.py | 4 ++-- tests/functional/test_install_wheel.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/utils/temp_dir.py b/src/pip/_internal/utils/temp_dir.py index 9d2dbecf279..54c3140110c 100644 --- a/src/pip/_internal/utils/temp_dir.py +++ b/src/pip/_internal/utils/temp_dir.py @@ -194,8 +194,8 @@ def cleanup(self): """ self._deleted = True if os.path.exists(self._path): - # Make sure to pass unicode on Python 2 so the contents with weird - # names are also in unicode and can be deleted correctly. + # Make sure to pass unicode on Python 2 to make the contents also + # use unicode, ensuring non-ASCII names and can be represented. rmtree(ensure_text(self._path)) diff --git a/tests/functional/test_install_wheel.py b/tests/functional/test_install_wheel.py index a45ce165b22..6bb20cf087d 100644 --- a/tests/functional/test_install_wheel.py +++ b/tests/functional/test_install_wheel.py @@ -127,7 +127,7 @@ def test_basic_install_from_wheel_file(script, data): result.stdout) -# Installation works, but Path fails to check those weird names on Python 2. +# Installation seems to work, but scripttest fails to check. # I really don't care now since we're desupporting it soon anyway. @skip_if_python2 def test_basic_install_from_unicode_wheel(script, data): From dd50a03f10a9c009b2cc7f0e31f5b46a4b12c41e Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 14 May 2020 03:57:56 +0800 Subject: [PATCH 1836/3170] Use unicode for filesystem stuff on Python 2 --- src/pip/_internal/operations/install/wheel.py | 12 ++++++------ src/pip/_internal/utils/misc.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 7f6f702f6f4..1e7d4910309 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -75,7 +75,7 @@ def rehash(path, blocksize=1 << 20): - # type: (str, int) -> Tuple[str, str] + # type: (text_type, int) -> Tuple[str, str] """Return (encoded_digest, length) for path using hashlib.sha256()""" h, length = hash_file(path, blocksize) digest = 'sha256=' + urlsafe_b64encode( @@ -97,7 +97,7 @@ def csv_io_kwargs(mode): def fix_script(path): - # type: (str) -> Optional[bool] + # type: (text_type) -> Optional[bool] """Replace #!python with #!/path/to/python Return True if file was changed. """ @@ -255,7 +255,7 @@ def _normalized_outrows(outrows): def _record_to_fs_path(record_path): - # type: (RecordPath) -> str + # type: (RecordPath) -> text_type return record_path @@ -398,8 +398,8 @@ def clobber( source, # type: text_type dest, # type: text_type is_base, # type: bool - fixer=None, # type: Optional[Callable[[str], Any]] - filter=None # type: Optional[Callable[[str], bool]] + fixer=None, # type: Optional[Callable[[text_type], Any]] + filter=None # type: Optional[Callable[[text_type], bool]] ): # type: (...) -> None ensure_dir(dest) # common for the 'include' path @@ -471,7 +471,7 @@ def clobber( console, gui = get_entrypoints(ep_file) def is_entrypoint_wrapper(name): - # type: (str) -> bool + # type: (text_type) -> bool # EP, EP.exe and EP-script.py are scripts generated for # entry point EP by setuptools if name.lower().endswith('.exe'): diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 719a8c335c2..658a30b86ac 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -876,7 +876,7 @@ def is_console_interactive(): def hash_file(path, blocksize=1 << 20): - # type: (str, int) -> Tuple[Any, int] + # type: (Text, int) -> Tuple[Any, int] """Return (hash, length) for path using hashlib.sha256() """ From 0a31845007a5508f3236c61afdccba87885c48df Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 14 May 2020 04:01:41 +0800 Subject: [PATCH 1837/3170] Explode the record row for readability --- src/pip/_internal/operations/install/wheel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 1e7d4910309..3f96c487a89 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -249,8 +249,8 @@ def _normalized_outrows(outrows): # For additional background, see-- # https://github.com/pypa/pip/issues/5868 return sorted( - (ensure_str(row[0], encoding='utf-8'), row[1], str(row[2])) - for row in outrows + (ensure_str(record_path, encoding='utf-8'), hash_, str(size)) + for record_path, hash_, size in outrows ) From 1bd89038159b20c21793c6f9367ded2023a922e9 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Mon, 18 May 2020 03:11:19 +0530 Subject: [PATCH 1838/3170] Type annotations for pip._internal.models.format_control --- src/pip/_internal/models/format_control.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/pip/_internal/models/format_control.py b/src/pip/_internal/models/format_control.py index 2e13727ca00..c39b84a84b5 100644 --- a/src/pip/_internal/models/format_control.py +++ b/src/pip/_internal/models/format_control.py @@ -1,6 +1,3 @@ -# The following comment should be removed at some point in the future. -# mypy: strict-optional=False - from pip._vendor.packaging.utils import canonicalize_name from pip._internal.exceptions import CommandError @@ -42,7 +39,7 @@ def __repr__(self): @staticmethod def handle_mutual_excludes(value, target, other): - # type: (str, Optional[Set[str]], Optional[Set[str]]) -> None + # type: (str, Set[str], Set[str]) -> None if value.startswith('-'): raise CommandError( "--no-binary / --only-binary option requires 1 argument." From e572d9e0c9bfe160957e17cd3fe150c19d2e73c6 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Mon, 18 May 2020 03:11:35 +0530 Subject: [PATCH 1839/3170] Type annotations for pip._internal.network.cache --- src/pip/_internal/network/cache.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/network/cache.py b/src/pip/_internal/network/cache.py index c9386e17360..a0d55b5e992 100644 --- a/src/pip/_internal/network/cache.py +++ b/src/pip/_internal/network/cache.py @@ -1,9 +1,6 @@ """HTTP cache implementation. """ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - import os from contextlib import contextmanager @@ -16,7 +13,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Optional + from typing import Optional, Iterator def is_from_cache(response): @@ -26,6 +23,7 @@ def is_from_cache(response): @contextmanager def suppressed_cache_errors(): + # type: () -> Iterator[None] """If we can't access the cache then we can just skip caching and process requests as if caching wasn't enabled. """ From fdc062475ed34b41f71b53fa9eb28eba26c92fef Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Mon, 18 May 2020 03:11:44 +0530 Subject: [PATCH 1840/3170] Type annotations for pip._internal.network.xmlrpc --- src/pip/_internal/network/xmlrpc.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/network/xmlrpc.py b/src/pip/_internal/network/xmlrpc.py index 121edd93056..8e360cc0f74 100644 --- a/src/pip/_internal/network/xmlrpc.py +++ b/src/pip/_internal/network/xmlrpc.py @@ -1,9 +1,6 @@ """xmlrpclib.Transport implementation """ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - import logging from pip._vendor import requests @@ -12,6 +9,12 @@ from pip._vendor.six.moves import xmlrpc_client # type: ignore from pip._vendor.six.moves.urllib import parse as urllib_parse +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import Optional, Dict + from pip._internal.network.session import PipSession + logger = logging.getLogger(__name__) @@ -21,12 +24,14 @@ class PipXmlrpcTransport(xmlrpc_client.Transport): """ def __init__(self, index_url, session, use_datetime=False): + # type: (str, PipSession, Optional[bool]) -> None xmlrpc_client.Transport.__init__(self, use_datetime) index_parts = urllib_parse.urlparse(index_url) self._scheme = index_parts.scheme self._session = session def request(self, host, handler, request_body, verbose=False): + # type: (str, str, Dict[str, str], Optional[bool]) -> None parts = (self._scheme, host, handler, None, None, None) url = urllib_parse.urlunparse(parts) try: From 2fe5d84d68ac656f1f3d35f3d2f4aedb75c4ade5 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Tue, 19 May 2020 22:16:10 +0530 Subject: [PATCH 1841/3170] Add news file --- news/99C26CF6-C60A-4ECD-8ED9-426918DCA4ED.trivial | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 news/99C26CF6-C60A-4ECD-8ED9-426918DCA4ED.trivial diff --git a/news/99C26CF6-C60A-4ECD-8ED9-426918DCA4ED.trivial b/news/99C26CF6-C60A-4ECD-8ED9-426918DCA4ED.trivial new file mode 100644 index 00000000000..e69de29bb2d From 65a6152f024de93c91a152847faee925f73c050a Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Wed, 20 May 2020 12:40:05 +0530 Subject: [PATCH 1842/3170] Remove Optional type from xmlrpc bool parameters --- src/pip/_internal/network/xmlrpc.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/network/xmlrpc.py b/src/pip/_internal/network/xmlrpc.py index 8e360cc0f74..beab4fcfa7a 100644 --- a/src/pip/_internal/network/xmlrpc.py +++ b/src/pip/_internal/network/xmlrpc.py @@ -12,7 +12,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Optional, Dict + from typing import Dict from pip._internal.network.session import PipSession logger = logging.getLogger(__name__) @@ -24,14 +24,14 @@ class PipXmlrpcTransport(xmlrpc_client.Transport): """ def __init__(self, index_url, session, use_datetime=False): - # type: (str, PipSession, Optional[bool]) -> None + # type: (str, PipSession, bool) -> None xmlrpc_client.Transport.__init__(self, use_datetime) index_parts = urllib_parse.urlparse(index_url) self._scheme = index_parts.scheme self._session = session def request(self, host, handler, request_body, verbose=False): - # type: (str, str, Dict[str, str], Optional[bool]) -> None + # type: (str, str, Dict[str, str], bool) -> None parts = (self._scheme, host, handler, None, None, None) url = urllib_parse.urlunparse(parts) try: From 418d9051b87ff1069990163dc9f7c8eeb7863e9a Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 20 May 2020 20:50:33 +0800 Subject: [PATCH 1843/3170] Set max_rounds to an incredibly large number We really don't want to raise ResolutionTooDeep yet since the metric is not very useful. Try to avoid this until we come up with a better method to count. --- src/pip/_internal/resolution/resolvelib/resolver.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index d05a277b000..2f0e67cc683 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -132,7 +132,10 @@ def resolve(self, root_reqs, check_supported_wheels): resolver = RLResolver(provider, reporter) try: - self._result = resolver.resolve(requirements) + try_to_avoid_resolution_too_deep = 2000000 + self._result = resolver.resolve( + requirements, max_rounds=try_to_avoid_resolution_too_deep, + ) except ResolutionImpossible as e: error = self.factory.get_installation_error(e) From c469e11d475ecf8c5131148dce28f9e08ab635c6 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Wed, 20 May 2020 23:12:23 +0530 Subject: [PATCH 1844/3170] Type annotations for pip._internal.network.auth --- src/pip/_internal/network/auth.py | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/network/auth.py b/src/pip/_internal/network/auth.py index ab8ac07012e..19c75aa9dfa 100644 --- a/src/pip/_internal/network/auth.py +++ b/src/pip/_internal/network/auth.py @@ -4,9 +4,6 @@ providing credentials in the context of network requests. """ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - import logging from pip._vendor.requests.auth import AuthBase, HTTPBasicAuth @@ -23,11 +20,10 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from optparse import Values - from typing import Dict, Optional, Tuple + from typing import Dict, Optional, Tuple, List, Any from pip._internal.vcs.versioncontrol import AuthInfo - + from pip._vendor.requests.models import Response, Request Credentials = Tuple[str, str, str] logger = logging.getLogger(__name__) @@ -44,6 +40,7 @@ def get_keyring_auth(url, username): + # type: (str, str) -> Optional[AuthInfo] """Return the tuple auth for a given url from keyring.""" if not url or not keyring: return None @@ -70,12 +67,13 @@ def get_keyring_auth(url, username): logger.warning( "Keyring is skipped due to an exception: %s", str(exc), ) + return None class MultiDomainBasicAuth(AuthBase): def __init__(self, prompting=True, index_urls=None): - # type: (bool, Optional[Values]) -> None + # type: (bool, Optional[List[str]]) -> None self.prompting = prompting self.index_urls = index_urls self.passwords = {} # type: Dict[str, AuthInfo] @@ -87,6 +85,7 @@ def __init__(self, prompting=True, index_urls=None): self._credentials_to_save = None # type: Optional[Credentials] def _get_index_url(self, url): + # type: (str) -> Optional[str] """Return the original index URL matching the requested URL. Cached or dynamically generated credentials may work against @@ -106,9 +105,11 @@ def _get_index_url(self, url): prefix = remove_auth_from_url(u).rstrip("/") + "/" if url.startswith(prefix): return u + return None def _get_new_credentials(self, original_url, allow_netrc=True, allow_keyring=True): + # type: (str, bool, bool) -> AuthInfo """Find and return credentials for the specified URL.""" # Split the credentials and netloc from the url. url, netloc, url_user_password = split_auth_netloc_from_url( @@ -158,6 +159,7 @@ def _get_new_credentials(self, original_url, allow_netrc=True, return username, password def _get_url_and_credentials(self, original_url): + # type: (str) -> Tuple[str, Optional[str], Optional[str]] """Return the credentials to use for the provided URL. If allowed, netrc and keyring may be used to obtain the @@ -178,7 +180,7 @@ def _get_url_and_credentials(self, original_url): username, password = self._get_new_credentials(original_url) if username is not None or password is not None: - # Convert the username and password if they're None, so that + # Convert the username and password if they 're None, so that # this netloc will show up as "cached" in the conditional above. # Further, HTTPBasicAuth doesn't accept None, so it makes sense to # cache the value that is going to be used. @@ -198,6 +200,7 @@ def _get_url_and_credentials(self, original_url): return url, username, password def __call__(self, req): + # type: (Request) -> Request # Get credentials for this request url, username, password = self._get_url_and_credentials(req.url) @@ -215,9 +218,10 @@ def __call__(self, req): # Factored out to allow for easy patching in tests def _prompt_for_password(self, netloc): + # type: (str) -> Tuple[Optional[str], Optional[str], bool] username = ask_input("User for {}: ".format(netloc)) if not username: - return None, None + return None, None, False auth = get_keyring_auth(netloc, username) if auth and auth[0] is not None and auth[1] is not None: return auth[0], auth[1], False @@ -226,11 +230,13 @@ def _prompt_for_password(self, netloc): # Factored out to allow for easy patching in tests def _should_save_password_to_keyring(self): + # type: () -> bool if not keyring: return False return ask("Save credentials to keyring [y/N]: ", ["y", "n"]) == "y" def handle_401(self, resp, **kwargs): + # type: (Response, **Any) -> None # We only care about 401 responses, anything else we want to just # pass through the actual response if resp.status_code != 401: @@ -276,6 +282,7 @@ def handle_401(self, resp, **kwargs): return new_resp def warn_on_401(self, resp, **kwargs): + # type: (Response, **Any) -> None """Response callback to warn about incorrect credentials.""" if resp.status_code == 401: logger.warning( @@ -283,6 +290,7 @@ def warn_on_401(self, resp, **kwargs): ) def save_credentials(self, resp, **kwargs): + # type: (Response, **Any) -> None """Response callback to save credentials on success.""" assert keyring is not None, "should never reach here without keyring" if not keyring: From e9901982bfe36da0e8956c0ab79ca597256c03c7 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Wed, 20 May 2020 23:12:49 +0530 Subject: [PATCH 1845/3170] Type annotations for pip._internal.utils.encoding --- src/pip/_internal/utils/encoding.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/utils/encoding.py b/src/pip/_internal/utils/encoding.py index ab4d4b98e3e..823e7fc0455 100644 --- a/src/pip/_internal/utils/encoding.py +++ b/src/pip/_internal/utils/encoding.py @@ -1,6 +1,3 @@ -# The following comment should be removed at some point in the future. -# mypy: strict-optional=False - import codecs import locale import re @@ -35,8 +32,10 @@ def auto_decode(data): # Lets check the first two lines as in PEP263 for line in data.split(b'\n')[:2]: if line[0:1] == b'#' and ENCODING_RE.search(line): - encoding = ENCODING_RE.search(line).groups()[0].decode('ascii') - return data.decode(encoding) + result = ENCODING_RE.search(line) + if result: + encoding = result.groups()[0].decode('ascii') + return data.decode(encoding) return data.decode( locale.getpreferredencoding(False) or sys.getdefaultencoding(), ) From d84c14013ab761d89b01724e8f50f7d701965d38 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Wed, 20 May 2020 23:13:00 +0530 Subject: [PATCH 1846/3170] Type annotations for pip._internal.utils.unpacking --- src/pip/_internal/utils/unpacking.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/utils/unpacking.py b/src/pip/_internal/utils/unpacking.py index 7252dc217bf..93a8b15ca21 100644 --- a/src/pip/_internal/utils/unpacking.py +++ b/src/pip/_internal/utils/unpacking.py @@ -1,10 +1,6 @@ """Utilities related archives. """ -# The following comment should be removed at some point in the future. -# mypy: strict-optional=False -# mypy: disallow-untyped-defs=False - from __future__ import absolute_import import logging @@ -48,6 +44,7 @@ def current_umask(): + # type: () -> int """Get the current umask which involves having to set it temporarily.""" mask = os.umask(0) os.umask(mask) @@ -208,6 +205,7 @@ def untar_file(filename, location): ) continue else: + fp = None try: fp = tar.extractfile(member) except (KeyError, AttributeError) as exc: @@ -220,8 +218,10 @@ def untar_file(filename, location): continue ensure_dir(os.path.dirname(path)) with open(path, 'wb') as destfp: - shutil.copyfileobj(fp, destfp) - fp.close() + if fp: + shutil.copyfileobj(fp, destfp) + if fp: + fp.close() # Update the timestamp (useful for cython compiled files) # https://github.com/python/typeshed/issues/2673 tar.utime(member, path) # type: ignore From ea7a7d26fc430fc663e05807c7aa799e70ade847 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Wed, 20 May 2020 23:13:43 +0530 Subject: [PATCH 1847/3170] Add NEWS file --- news/EBB1CF12-70ED-405F-90C0-BEA7CF25DCE4.trivial | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 news/EBB1CF12-70ED-405F-90C0-BEA7CF25DCE4.trivial diff --git a/news/EBB1CF12-70ED-405F-90C0-BEA7CF25DCE4.trivial b/news/EBB1CF12-70ED-405F-90C0-BEA7CF25DCE4.trivial new file mode 100644 index 00000000000..e69de29bb2d From bf31e536b362a4c79e3d7046de9fcd0a83483038 Mon Sep 17 00:00:00 2001 From: Christian Heimes <christian@python.org> Date: Thu, 21 May 2020 00:17:55 +0200 Subject: [PATCH 1848/3170] Don't use cElementTree on Python 3 It's been deprecated and will be removed in 3.9 or 3.10. 3.9.0b1 doesn't have cElementTree. I'd like to bring it back with a deprecation warning to drop in 3.10. See: https://github.com/python/cpython/pull/19921 Signed-off-by: Christian Heimes <christian@python.org> --- src/pip/_vendor/html5lib/_utils.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/pip/_vendor/html5lib/_utils.py b/src/pip/_vendor/html5lib/_utils.py index 0703afb38b1..96eb17b2c17 100644 --- a/src/pip/_vendor/html5lib/_utils.py +++ b/src/pip/_vendor/html5lib/_utils.py @@ -2,12 +2,15 @@ from types import ModuleType -from pip._vendor.six import text_type +from pip._vendor.six import text_type, PY3 -try: - import xml.etree.cElementTree as default_etree -except ImportError: +if PY3: import xml.etree.ElementTree as default_etree +else: + try: + import xml.etree.cElementTree as default_etree + except ImportError: + import xml.etree.ElementTree as default_etree __all__ = ["default_etree", "MethodDispatcher", "isSurrogatePair", From 4333333d1371ef944d34addb9dfb96140f998a62 Mon Sep 17 00:00:00 2001 From: Christian Heimes <christian@python.org> Date: Thu, 21 May 2020 00:26:30 +0200 Subject: [PATCH 1849/3170] news entry --- news/8278.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/8278.bugfix diff --git a/news/8278.bugfix b/news/8278.bugfix new file mode 100644 index 00000000000..3a95794f19b --- /dev/null +++ b/news/8278.bugfix @@ -0,0 +1 @@ +htmlib5 no longer imports deprecated xml.etree.cElementTree on Python 3. From ac5702c9b3dea28444dee607c8a0c417c249114d Mon Sep 17 00:00:00 2001 From: cjc7373 <niuchangcun@gmail.com> Date: Thu, 21 May 2020 11:57:46 +0800 Subject: [PATCH 1850/3170] move marker registering to setup.cfg --- setup.cfg | 12 ++++++++++++ tests/conftest.py | 14 -------------- 2 files changed, 12 insertions(+), 14 deletions(-) diff --git a/setup.cfg b/setup.cfg index 617e8b5673f..03abe8cea2c 100644 --- a/setup.cfg +++ b/setup.cfg @@ -38,6 +38,18 @@ ignore_errors = True [tool:pytest] addopts = --ignore src/pip/_vendor --ignore tests/tests_cache -r aR +markers = + network: tests that needs network + incompatible_with_test_venv + incompatible_with_venv + no_auto_tempdir_manager + unit: unit tests + integration: integration tests + bzr: VCS: Bazaar + svn: VCS: Subversion + mercurial: VCS: Mercurial + git: VCS: git + yaml: yaml based tests [bdist_wheel] universal = 1 diff --git a/tests/conftest.py b/tests/conftest.py index 75e43c3b72e..bf8cd7975c7 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -49,20 +49,6 @@ def pytest_addoption(parser): ) -def pytest_configure(config): - config.addinivalue_line("markers", "network: tests that needs network") - config.addinivalue_line("markers", "incompatible_with_test_venv") - config.addinivalue_line("markers", "incompatible_with_venv") - config.addinivalue_line("markers", "no_auto_tempdir_manager") - config.addinivalue_line("markers", "unit: unit tests") - config.addinivalue_line("markers", "integration: integration tests") - config.addinivalue_line("markers", "bzr: VCS: Bazaar") - config.addinivalue_line("markers", "svn: VCS: Subversion") - config.addinivalue_line("markers", "mercurial: VCS: Mercurial") - config.addinivalue_line("markers", "git: VCS: git") - config.addinivalue_line("markers", "yaml: yaml based tests") - - def pytest_collection_modifyitems(config, items): for item in items: if not hasattr(item, 'module'): # e.g.: DoctestTextfile From c3ac76f66c5368376bc01333912133013da7d83b Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 21 May 2020 15:39:16 +0800 Subject: [PATCH 1851/3170] Turn Candidate.get_dependencies() into iterable This makes it easier to exclude dependencies when markers don't match. --- .../_internal/resolution/resolvelib/base.py | 6 +-- .../resolution/resolvelib/candidates.py | 45 ++++++++----------- .../resolution/resolvelib/provider.py | 2 +- 3 files changed, 23 insertions(+), 30 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/base.py b/src/pip/_internal/resolution/resolvelib/base.py index eacdf8ecc8e..17513d336e7 100644 --- a/src/pip/_internal/resolution/resolvelib/base.py +++ b/src/pip/_internal/resolution/resolvelib/base.py @@ -3,7 +3,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Optional, Sequence, Set + from typing import Iterable, Optional, Sequence, Set from pip._internal.req.req_install import InstallRequirement from pip._vendor.packaging.specifiers import SpecifierSet @@ -49,8 +49,8 @@ def is_installed(self): # type: () -> bool raise NotImplementedError("Override in subclass") - def get_dependencies(self): - # type: () -> Sequence[Requirement] + def iter_dependencies(self): + # type: () -> Iterable[Requirement] raise NotImplementedError("Override in subclass") def get_install_requirement(self): diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 418da5d4d0d..fcdc20dd6e5 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -17,7 +17,7 @@ from .base import Candidate, format_name if MYPY_CHECK_RUNNING: - from typing import Any, Optional, Sequence, Set, Tuple, Union + from typing import Any, Iterable, Optional, Set, Tuple, Union from pip._vendor.packaging.version import _BaseVersion from pip._vendor.pkg_resources import Distribution @@ -212,18 +212,15 @@ def _get_requires_python_specifier(self): return None return spec - def get_dependencies(self): - # type: () -> Sequence[Requirement] - deps = [ - self._factory.make_requirement_from_spec(str(r), self._ireq) - for r in self.dist.requires() - ] + def iter_dependencies(self): + # type: () -> Iterable[Requirement] + for r in self.dist.requires(): + yield self._factory.make_requirement_from_spec(str(r), self._ireq) python_dep = self._factory.make_requires_python_requirement( self._get_requires_python_specifier(), ) if python_dep: - deps.append(python_dep) - return deps + yield python_dep def get_install_requirement(self): # type: () -> Optional[InstallRequirement] @@ -326,12 +323,10 @@ def version(self): # type: () -> _BaseVersion return self.dist.parsed_version - def get_dependencies(self): - # type: () -> Sequence[Requirement] - return [ - self._factory.make_requirement_from_spec(str(r), self._ireq) - for r in self.dist.requires() - ] + def iter_dependencies(self): + # type: () -> Iterable[Requirement] + for r in self.dist.requires(): + yield self._factory.make_requirement_from_spec(str(r), self._ireq) def get_install_requirement(self): # type: () -> Optional[InstallRequirement] @@ -406,8 +401,8 @@ def is_installed(self): # type: () -> _BaseVersion return self.base.is_installed - def get_dependencies(self): - # type: () -> Sequence[Requirement] + def iter_dependencies(self): + # type: () -> Iterable[Requirement] factory = self.base._factory # The user may have specified extras that the candidate doesn't @@ -422,17 +417,15 @@ def get_dependencies(self): extra ) - deps = [ - factory.make_requirement_from_spec(str(r), self.base._ireq) - for r in self.base.dist.requires(valid_extras) - ] + for r in self.base.dist.requires(valid_extras): + yield factory.make_requirement_from_spec(str(r), self.base._ireq) + # Add a dependency on the exact base. # (See note 2b in the class docstring) # FIXME: This does not work if the base candidate is specified by # link, e.g. "pip install .[dev]" will fail. spec = "{}=={}".format(self.base.name, self.base.version) - deps.append(factory.make_requirement_from_spec(spec, self.base._ireq)) - return deps + yield factory.make_requirement_from_spec(spec, self.base._ireq) def get_install_requirement(self): # type: () -> Optional[InstallRequirement] @@ -468,9 +461,9 @@ def version(self): # type: () -> _BaseVersion return self._version - def get_dependencies(self): - # type: () -> Sequence[Requirement] - return [] + def iter_dependencies(self): + # type: () -> Iterable[Requirement] + return () def get_install_requirement(self): # type: () -> Optional[InstallRequirement] diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index 4e8c5ae309b..e4c516948c4 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -132,4 +132,4 @@ def get_dependencies(self, candidate): # type: (Candidate) -> Sequence[Requirement] if self._ignore_dependencies: return [] - return candidate.get_dependencies() + return list(candidate.iter_dependencies()) From 0a4629febb851f925750c1d3a4ba0dcadeb22107 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 21 May 2020 15:42:58 +0800 Subject: [PATCH 1852/3170] Add test for marker that should fail for now --- tests/functional/test_new_resolver.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index dace95968bd..3f812758994 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -531,6 +531,29 @@ def test_new_resolver_handles_prerelease( assert_installed(script, pkg=expected_version) +@pytest.mark.parametrize( + "pkg_deps, root_deps", + [ + # This tests the marker is picked up from a transitive dependency. + (["dep; os_name == 'nonexist_os'"], ["pkg"]), + # This tests the marker is picked up from a root dependency. + ([], ["pkg", "dep; os_name == 'nonexist_os'"]), + ] +) +def test_new_reolver_skips_marker(script, pkg_deps, root_deps): + create_basic_wheel_for_package(script, "pkg", "1.0", depends=pkg_deps) + create_basic_wheel_for_package(script, "dep", "1.0") + + script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + *root_deps + ) + assert_installed(script, pkg="1.0") + assert_not_installed(script, "dep") + + @pytest.mark.parametrize( "constraints", [ From 5ebf22248cd41001b25bab0ac499c821bb4cf204 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 21 May 2020 15:53:01 +0800 Subject: [PATCH 1853/3170] Ignore root dependencies with non-matching markers --- src/pip/_internal/resolution/resolvelib/resolver.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 2f0e67cc683..cecc70661d1 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -105,6 +105,8 @@ def resolve(self, root_reqs, check_supported_wheels): user_requested = set() # type: Set[str] requirements = [] for req in root_reqs: + if not req.match_markers(): + continue if req.constraint: # Ensure we only accept valid constraints reject_invalid_constraint_types(req) From e6352bc468e237ca8545230222b389b492a8dcc4 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 21 May 2020 16:14:26 +0800 Subject: [PATCH 1854/3170] Return package dependencies only if markers match --- .../_internal/resolution/resolvelib/candidates.py | 6 +++++- src/pip/_internal/resolution/resolvelib/factory.py | 14 +++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index fcdc20dd6e5..2937c29424f 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -418,7 +418,11 @@ def iter_dependencies(self): ) for r in self.base.dist.requires(valid_extras): - yield factory.make_requirement_from_spec(str(r), self.base._ireq) + requirement = factory.make_requirement_from_spec_matching_extras( + str(r), self.base._ireq, valid_extras, + ) + if requirement: + yield requirement # Add a dependency on the exact base. # (See note 2b in the class docstring) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 3246caffee6..3090dbcfd56 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -29,7 +29,7 @@ ) if MYPY_CHECK_RUNNING: - from typing import Dict, Iterator, Optional, Set, Tuple, TypeVar + from typing import Dict, Iterable, Iterator, Optional, Set, Tuple, TypeVar from pip._vendor.packaging.specifiers import SpecifierSet from pip._vendor.packaging.version import _BaseVersion @@ -187,6 +187,18 @@ def make_requirement_from_spec(self, specifier, comes_from): ireq = self._make_install_req_from_spec(specifier, comes_from) return self.make_requirement_from_install_req(ireq) + def make_requirement_from_spec_matching_extras( + self, + specifier, # type: str + comes_from, # type: InstallRequirement + requested_extras=(), # type: Iterable[str] + ): + # type: (...) -> Optional[Requirement] + ireq = self._make_install_req_from_spec(specifier, comes_from) + if not ireq.match_markers(requested_extras): + return None + return self.make_requirement_from_install_req(ireq) + def make_requires_python_requirement(self, specifier): # type: (Optional[SpecifierSet]) -> Optional[Requirement] if self._ignore_requires_python or specifier is None: From 086dc58686952c65f6ae9129b2595fc42e080482 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Thu, 21 May 2020 09:33:41 +0100 Subject: [PATCH 1855/3170] Update AUTHORS.txt --- AUTHORS.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AUTHORS.txt b/AUTHORS.txt index 04c42fc2156..dff91f93092 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -380,6 +380,7 @@ Nick Coghlan Nick Stenning Nick Timkovich Nicolas Bock +Nicole Harris Nikhil Benesch Nikolay Korolev Nitesh Sharma @@ -419,6 +420,7 @@ Peter Lisák Peter Waller petr-tik Phaneendra Chiruvella +Phil Elson Phil Freo Phil Pennock Phil Whelan From 96225c91248326abff3df568c31df4da6ed6d51c Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Thu, 21 May 2020 09:33:43 +0100 Subject: [PATCH 1856/3170] Bump for release --- NEWS.rst | 26 +++++++++++++++++++ ...D33C4F-0B92-427D-AE3B-93EE33E15621.trivial | 0 news/5712.bugfix | 2 -- news/7555.bugfix | 2 -- ...0AC380-E84F-44C7-A20C-CD31A829EDA5.trivial | 1 - news/7998.bugfix | 1 - news/8164.bugfix | 1 - news/8167.removal | 1 - news/8178.bugfix | 2 -- news/8207.doc | 1 - ...222709-663e-40a1-af2e-f20afab42122.trivial | 0 ...C26CF6-C60A-4ECD-8ED9-426918DCA4ED.trivial | 0 ...D0A87D-0ACD-418E-8C02-4560A99FEB71.trivial | 0 ...3EC962-957A-4DB8-A849-2E7179F875A9.trivial | 0 ...A26013-0E79-4DBB-B0E3-2DA5C5587CDC.trivial | 0 ...B04414-2228-431F-9F5D-AFF4C5C08D05.trivial | 0 ...1c4196-d21d-4e39-9d39-118e39c837ab.trivial | 0 ...0153d5-ae85-4b80-80f7-1c46e7b566dc.trivial | 0 src/pip/__init__.py | 2 +- 19 files changed, 27 insertions(+), 12 deletions(-) delete mode 100644 news/0FD33C4F-0B92-427D-AE3B-93EE33E15621.trivial delete mode 100644 news/5712.bugfix delete mode 100644 news/7555.bugfix delete mode 100644 news/770AC380-E84F-44C7-A20C-CD31A829EDA5.trivial delete mode 100644 news/7998.bugfix delete mode 100644 news/8164.bugfix delete mode 100644 news/8167.removal delete mode 100644 news/8178.bugfix delete mode 100644 news/8207.doc delete mode 100644 news/86222709-663e-40a1-af2e-f20afab42122.trivial delete mode 100644 news/99C26CF6-C60A-4ECD-8ED9-426918DCA4ED.trivial delete mode 100644 news/9CD0A87D-0ACD-418E-8C02-4560A99FEB71.trivial delete mode 100644 news/BF3EC962-957A-4DB8-A849-2E7179F875A9.trivial delete mode 100644 news/C7A26013-0E79-4DBB-B0E3-2DA5C5587CDC.trivial delete mode 100644 news/CDB04414-2228-431F-9F5D-AFF4C5C08D05.trivial delete mode 100644 news/ac1c4196-d21d-4e39-9d39-118e39c837ab.trivial delete mode 100644 news/e20153d5-ae85-4b80-80f7-1c46e7b566dc.trivial diff --git a/NEWS.rst b/NEWS.rst index 917580f17dd..46d322a4216 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,29 @@ +20.2b1 (2020-05-21) +=================== + +Deprecations and Removals +------------------------- + +- Drop parallelization from ``pip list --outdated``. (`#8167 <https://github.com/pypa/pip/issues/8167>`_) + +Bug Fixes +--------- + +- Correctly treat wheels contenting non-ASCII file contents so they can be + installed on Windows. (`#5712 <https://github.com/pypa/pip/issues/5712>`_) +- Revert building of local directories in place, restoring the pre-20.1 + behaviour of copying to a temporary directory. (`#7555 <https://github.com/pypa/pip/issues/7555>`_) +- Prompt the user for password if the keyring backend doesn't return one (`#7998 <https://github.com/pypa/pip/issues/7998>`_) +- Fix metadata permission issues when umask has the executable bit set. (`#8164 <https://github.com/pypa/pip/issues/8164>`_) +- Avoid unnecessary message about the wheel package not being installed + when a wheel would not have been built. Additionally, clarify the message. (`#8178 <https://github.com/pypa/pip/issues/8178>`_) + +Improved Documentation +---------------------- + +- Add GitHub issue template for reporting when the dependency resolver fails (`#8207 <https://github.com/pypa/pip/issues/8207>`_) + + .. NOTE: You should *NOT* be adding new change log entries to this file, this file is managed by towncrier. You *may* edit previous change logs to fix problems like typo corrections or such. diff --git a/news/0FD33C4F-0B92-427D-AE3B-93EE33E15621.trivial b/news/0FD33C4F-0B92-427D-AE3B-93EE33E15621.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/5712.bugfix b/news/5712.bugfix deleted file mode 100644 index c6cf0d5b5da..00000000000 --- a/news/5712.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -Correctly treat wheels contenting non-ASCII file contents so they can be -installed on Windows. diff --git a/news/7555.bugfix b/news/7555.bugfix deleted file mode 100644 index f762236e235..00000000000 --- a/news/7555.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -Revert building of local directories in place, restoring the pre-20.1 -behaviour of copying to a temporary directory. diff --git a/news/770AC380-E84F-44C7-A20C-CD31A829EDA5.trivial b/news/770AC380-E84F-44C7-A20C-CD31A829EDA5.trivial deleted file mode 100644 index 5e5d3c4f01d..00000000000 --- a/news/770AC380-E84F-44C7-A20C-CD31A829EDA5.trivial +++ /dev/null @@ -1 +0,0 @@ -Remove "type: ignore" comments from cli subpackage diff --git a/news/7998.bugfix b/news/7998.bugfix deleted file mode 100644 index aec751f86a0..00000000000 --- a/news/7998.bugfix +++ /dev/null @@ -1 +0,0 @@ -Prompt the user for password if the keyring backend doesn't return one diff --git a/news/8164.bugfix b/news/8164.bugfix deleted file mode 100644 index 1707d28401a..00000000000 --- a/news/8164.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix metadata permission issues when umask has the executable bit set. diff --git a/news/8167.removal b/news/8167.removal deleted file mode 100644 index d719377eb26..00000000000 --- a/news/8167.removal +++ /dev/null @@ -1 +0,0 @@ -Drop parallelization from ``pip list --outdated``. diff --git a/news/8178.bugfix b/news/8178.bugfix deleted file mode 100644 index 6960053eda2..00000000000 --- a/news/8178.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -Avoid unnecessary message about the wheel package not being installed -when a wheel would not have been built. Additionally, clarify the message. diff --git a/news/8207.doc b/news/8207.doc deleted file mode 100644 index a9cf944c62a..00000000000 --- a/news/8207.doc +++ /dev/null @@ -1 +0,0 @@ -Add GitHub issue template for reporting when the dependency resolver fails diff --git a/news/86222709-663e-40a1-af2e-f20afab42122.trivial b/news/86222709-663e-40a1-af2e-f20afab42122.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/99C26CF6-C60A-4ECD-8ED9-426918DCA4ED.trivial b/news/99C26CF6-C60A-4ECD-8ED9-426918DCA4ED.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/9CD0A87D-0ACD-418E-8C02-4560A99FEB71.trivial b/news/9CD0A87D-0ACD-418E-8C02-4560A99FEB71.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/BF3EC962-957A-4DB8-A849-2E7179F875A9.trivial b/news/BF3EC962-957A-4DB8-A849-2E7179F875A9.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/C7A26013-0E79-4DBB-B0E3-2DA5C5587CDC.trivial b/news/C7A26013-0E79-4DBB-B0E3-2DA5C5587CDC.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/CDB04414-2228-431F-9F5D-AFF4C5C08D05.trivial b/news/CDB04414-2228-431F-9F5D-AFF4C5C08D05.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/ac1c4196-d21d-4e39-9d39-118e39c837ab.trivial b/news/ac1c4196-d21d-4e39-9d39-118e39c837ab.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/e20153d5-ae85-4b80-80f7-1c46e7b566dc.trivial b/news/e20153d5-ae85-4b80-80f7-1c46e7b566dc.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/pip/__init__.py b/src/pip/__init__.py index dc41d31b63e..d98afba9e06 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -4,7 +4,7 @@ from typing import List, Optional -__version__ = "20.2.dev0" +__version__ = "20.2b1" def main(args=None): From 374a12bc5a1e5b2908b043ce13fb004b0ad556ef Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Thu, 21 May 2020 09:33:44 +0100 Subject: [PATCH 1857/3170] Bump for development --- src/pip/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/__init__.py b/src/pip/__init__.py index d98afba9e06..90ce10888ef 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -4,7 +4,7 @@ from typing import List, Optional -__version__ = "20.2b1" +__version__ = "20.2.dev1" def main(args=None): From a7b643004cd06c7f2b1cbe5fc6171b9eaf068eb9 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 21 May 2020 16:54:59 +0800 Subject: [PATCH 1858/3170] Add reprod for pre-existing build dir failure --- tests/functional/test_new_resolver.py | 58 ++++++++++++++++++++++++--- 1 file changed, 52 insertions(+), 6 deletions(-) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index dace95968bd..686c5f6953e 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -3,6 +3,7 @@ import sys import pytest +from pip._vendor.packaging.utils import canonicalize_name from tests.lib import ( create_basic_sdist_for_package, @@ -14,20 +15,25 @@ def assert_installed(script, **kwargs): ret = script.pip('list', '--format=json') installed = set( - (val['name'], val['version']) + (canonicalize_name(val['name']), val['version']) for val in json.loads(ret.stdout) ) - assert set(kwargs.items()) <= installed, \ - "{!r} not all in {!r}".format(kwargs, installed) + expected = set((canonicalize_name(k), v) for k, v in kwargs.items()) + assert expected <= installed, \ + "{!r} not all in {!r}".format(expected, installed) def assert_not_installed(script, *args): ret = script.pip("list", "--format=json") - installed = set(val["name"] for val in json.loads(ret.stdout)) + installed = set( + canonicalize_name(val["name"]) + for val in json.loads(ret.stdout) + ) # None of the given names should be listed as installed, i.e. their # intersection should be empty. - assert not (set(args) & installed), \ - "{!r} contained in {!r}".format(args, installed) + expected = set(canonicalize_name(k) for k in args) + assert not (expected & installed), \ + "{!r} contained in {!r}".format(expected, installed) def assert_editable(script, *args): @@ -801,3 +807,43 @@ def test_new_resolver_extra_merge_in_package( requirement + "[dev]", ) assert_installed(script, pkg="1.0.0", dep="1.0.0", depdev="1.0.0") + + +@pytest.mark.xfail(reason="pre-existing build directory") +def test_new_resolver_build_directory_error_zazo_19(script): + """https://github.com/pradyunsg/zazo/issues/19#issuecomment-631615674 + + This will first resolve like this: + + 1. Pin pkg-b==2.0.0 (since pkg-b has fewer choices) + 2. Pin pkg-a==3.0.0 -> Conflict due to dependency pkg-b<2 + 3. Pin pkg-b==1.0.0 + + Since pkg-b is only available as sdist, both the first and third steps + would trigger building from source. This ensures the preparer can build + different versions of a package for the resolver. + + The preparer would fail with the following message if the different + versions end up using the same build directory:: + + ERROR: pip can't proceed with requirements 'pkg-b ...' due to a + pre-existing build directory (...). 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. + """ + create_basic_wheel_for_package( + script, "pkg_a", "3.0.0", depends=["pkg-b<2"], + ) + create_basic_wheel_for_package(script, "pkg_a", "2.0.0") + create_basic_wheel_for_package(script, "pkg_a", "1.0.0") + + create_basic_sdist_for_package(script, "pkg_b", "2.0.0") + create_basic_sdist_for_package(script, "pkg_b", "1.0.0") + + script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "pkg-a", "pkg-b", + ) + assert_installed(script, pkg_a="3.0.0", pkg_b="1.0.0") From de63eae0c38869a5f83ee5756943f6d8097bc875 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 21 May 2020 18:09:35 +0800 Subject: [PATCH 1859/3170] Match logging --- src/pip/_internal/resolution/resolvelib/factory.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 3090dbcfd56..c59944d7ea5 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -1,4 +1,5 @@ import collections +import logging from pip._vendor import six from pip._vendor.packaging.utils import canonicalize_name @@ -50,6 +51,9 @@ VersionCandidates = Dict[_BaseVersion, Candidate] +logger = logging.getLogger(__name__) + + class Factory(object): def __init__( self, @@ -196,6 +200,10 @@ def make_requirement_from_spec_matching_extras( # type: (...) -> Optional[Requirement] ireq = self._make_install_req_from_spec(specifier, comes_from) if not ireq.match_markers(requested_extras): + logger.info( + "Ignoring %s: markers '%s' don't match your environment", + ireq.name, ireq.markers, + ) return None return self.make_requirement_from_install_req(ireq) From 8d79644170c1b0d8f5805584a675b2cb6f5134f5 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Thu, 21 May 2020 11:32:47 +0100 Subject: [PATCH 1860/3170] Make nox upload-release work on Windows --- noxfile.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/noxfile.py b/noxfile.py index f2a959ca601..7c515bef997 100644 --- a/noxfile.py +++ b/noxfile.py @@ -274,11 +274,12 @@ def upload_release(session): f"Remove dist/ and run 'nox -s build-release -- {version}'" ) # Sanity check: Make sure the files are correctly named. + distfile_names = [os.path.basename(f) for f in distribution_files] expected_distribution_files = [ - f"dist/pip-{version}-py2.py3-none-any.whl", - f"dist/pip-{version}.tar.gz", + f"pip-{version}-py2.py3-none-any.whl", + f"pip-{version}.tar.gz", ] - if sorted(distribution_files) != sorted(expected_distribution_files): + if sorted(distfile_names) != sorted(expected_distribution_files): session.error( f"Distribution files do not seem to be for {version} release." ) From d848ee934d8f7b194c54bdbe1ec9841b4a9af233 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 21 May 2020 20:32:05 +0800 Subject: [PATCH 1861/3170] Always read InstallRequirement.extras --- src/pip/_internal/resolution/resolvelib/requirements.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py index 208f7a300e1..f21e37a4a63 100644 --- a/src/pip/_internal/resolution/resolvelib/requirements.py +++ b/src/pip/_internal/resolution/resolvelib/requirements.py @@ -55,7 +55,7 @@ def __init__(self, ireq, factory): assert ireq.link is None, "This is a link, not a specifier" self._ireq = ireq self._factory = factory - self.extras = ireq.req.extras + self.extras = set(ireq.extras) def __str__(self): # type: () -> str From 4d17d932c515c45b9cc5847f3924df5210069a43 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 21 May 2020 21:00:25 +0800 Subject: [PATCH 1862/3170] Add test for picking up non-PEP-508 extrax --- tests/functional/test_new_resolver.py | 37 ++++++++++++++++++++++++++- 1 file changed, 36 insertions(+), 1 deletion(-) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index dace95968bd..a67c79b5bad 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -189,7 +189,42 @@ def test_new_resolver_ignore_dependencies(script): assert_not_installed(script, "dep") -def test_new_resolver_installs_extras(script): +@pytest.mark.parametrize( + "root_dep", + [ + "base[add]", + "base[add] >= 0.1.0", + pytest.param( # Non-standard syntax. To deprecate, see pypa/pip#8288. + "base >= 0.1.0[add]", + marks=pytest.mark.skipif( + "sys.platform == 'win32'", + reason="script.pip() does not handle >= on Windows", + ), + ), + ], +) +def test_new_resolver_installs_extras(script, root_dep): + create_basic_wheel_for_package( + script, + "base", + "0.1.0", + extras={"add": ["dep"]}, + ) + create_basic_wheel_for_package( + script, + "dep", + "0.1.0", + ) + script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + root_dep, + ) + assert_installed(script, base="0.1.0", dep="0.1.0") + + +def test_new_resolver_installs_extras_warn_missing(script): create_basic_wheel_for_package( script, "base", From 5cc4f2e390df76423ceb1ea3b65b8b13160f5cee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Thu, 21 May 2020 14:15:10 +0200 Subject: [PATCH 1863/3170] Pass explicit arguments to install_given_req. --- src/pip/_internal/commands/install.py | 2 +- src/pip/_internal/req/__init__.py | 20 ++++++++++++++------ 2 files changed, 15 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index c9b9ea4a8c5..bd15cddeff2 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -399,9 +399,9 @@ def run(self, options, args): 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, + pycompile=options.compile, ) lib_locations = get_lib_location_guesses( diff --git a/src/pip/_internal/req/__init__.py b/src/pip/_internal/req/__init__.py index d2d027adeec..75b532b0ee7 100644 --- a/src/pip/_internal/req/__init__.py +++ b/src/pip/_internal/req/__init__.py @@ -13,7 +13,7 @@ from .req_set import RequirementSet if MYPY_CHECK_RUNNING: - from typing import Any, List, Sequence + from typing import List, Optional, Sequence __all__ = [ "RequirementSet", "InstallRequirement", @@ -36,9 +36,13 @@ def __repr__(self): def install_given_reqs( to_install, # type: List[InstallRequirement] install_options, # type: List[str] - global_options=(), # type: Sequence[str] - *args, # type: Any - **kwargs # type: Any + global_options, # type: Sequence[str] + root, # type: Optional[str] + home, # type: Optional[str] + prefix, # type: Optional[str] + warn_script_location, # type: bool + use_user_site, # type: bool + pycompile, # type: bool ): # type: (...) -> List[InstallationResult] """ @@ -67,8 +71,12 @@ def install_given_reqs( requirement.install( install_options, global_options, - *args, - **kwargs + root=root, + home=home, + prefix=prefix, + warn_script_location=warn_script_location, + use_user_site=use_user_site, + pycompile=pycompile, ) except Exception: should_rollback = ( From 19db59747a6a6ede972ec019dce95997084cef50 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 21 May 2020 21:37:16 +0800 Subject: [PATCH 1864/3170] Use a temp requirements file to avoid shell syntax --- tests/functional/test_new_resolver.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index a67c79b5bad..5192c2010f1 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -194,16 +194,14 @@ def test_new_resolver_ignore_dependencies(script): [ "base[add]", "base[add] >= 0.1.0", - pytest.param( # Non-standard syntax. To deprecate, see pypa/pip#8288. - "base >= 0.1.0[add]", - marks=pytest.mark.skipif( - "sys.platform == 'win32'", - reason="script.pip() does not handle >= on Windows", - ), - ), + # Non-standard syntax. To deprecate, see pypa/pip#8288. + "base >= 0.1.0[add]", ], ) -def test_new_resolver_installs_extras(script, root_dep): +def test_new_resolver_installs_extras(tmpdir, script, root_dep): + req_file = tmpdir.joinpath("requirements.txt") + req_file.write_text(root_dep) + create_basic_wheel_for_package( script, "base", @@ -219,7 +217,7 @@ def test_new_resolver_installs_extras(script, root_dep): "install", "--unstable-feature=resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, - root_dep, + "-r", req_file, ) assert_installed(script, base="0.1.0", dep="0.1.0") From f5ff110df727733057e8b3b2d8b982d186780abf Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 21 May 2020 19:29:18 +0530 Subject: [PATCH 1865/3170] Apply suggestion from review comments MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Nguyễn Gia Phong <mcsinyx@disroot.org> --- noxfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index 7c515bef997..dbb9c30295c 100644 --- a/noxfile.py +++ b/noxfile.py @@ -274,7 +274,7 @@ def upload_release(session): f"Remove dist/ and run 'nox -s build-release -- {version}'" ) # Sanity check: Make sure the files are correctly named. - distfile_names = [os.path.basename(f) for f in distribution_files] + distfile_names = sorted(map(os.path.basename, distribution_files)) expected_distribution_files = [ f"pip-{version}-py2.py3-none-any.whl", f"pip-{version}.tar.gz", From cfaa08efeddf59dda7e46a79f11779bebdaf8cd2 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 21 May 2020 19:32:28 +0530 Subject: [PATCH 1866/3170] Remove a sorted call Otherwise, we'd try sorting this list twice. --- noxfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index dbb9c30295c..1746bb69915 100644 --- a/noxfile.py +++ b/noxfile.py @@ -274,7 +274,7 @@ def upload_release(session): f"Remove dist/ and run 'nox -s build-release -- {version}'" ) # Sanity check: Make sure the files are correctly named. - distfile_names = sorted(map(os.path.basename, distribution_files)) + distfile_names = map(os.path.basename, distribution_files) expected_distribution_files = [ f"pip-{version}-py2.py3-none-any.whl", f"pip-{version}.tar.gz", From 1bc7f535caf93f654404b9d2555c2f03ba45f4db Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sun, 19 Apr 2020 15:03:53 +0530 Subject: [PATCH 1867/3170] Warn if package index gets unexpected Content-Type --- news/6754.feature | 1 + src/pip/_internal/index/collector.py | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 news/6754.feature diff --git a/news/6754.feature b/news/6754.feature new file mode 100644 index 00000000000..561643dbd28 --- /dev/null +++ b/news/6754.feature @@ -0,0 +1 @@ +Warn if index pages have unexpected content-type diff --git a/src/pip/_internal/index/collector.py b/src/pip/_internal/index/collector.py index e2c800c2cde..321861e2c76 100644 --- a/src/pip/_internal/index/collector.py +++ b/src/pip/_internal/index/collector.py @@ -455,8 +455,9 @@ def _get_html_page(link, session=None): 'be checked by HEAD.', link, ) except _NotHTML as exc: - logger.debug( - 'Skipping page %s because the %s request got Content-Type: %s', + logger.warning( + 'Skipping page %s because the %s request got Content-Type: %s.' + 'The supported Content-Type is text/html', link, exc.request_desc, exc.content_type, ) except HTTPError as exc: From fa67244d4580ae2cb2bbff8216ff5e4f5237975e Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sun, 19 Apr 2020 15:38:37 +0530 Subject: [PATCH 1868/3170] Add unit test to warn on invalid content-type --- tests/unit/test_collector.py | 30 ++++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/unit/test_collector.py b/tests/unit/test_collector.py index cfc2af1c07a..9a7966688b3 100644 --- a/tests/unit/test_collector.py +++ b/tests/unit/test_collector.py @@ -476,6 +476,36 @@ def test_get_html_page_invalid_scheme(caplog, url, vcs_scheme): ] +@pytest.mark.parametrize( + "content_type", + [ + "application/xhtml+xml", + "application/json", + ], +) +def test_get_html_page_invalid_content_type(caplog, content_type): + """`_get_html_page()` should warn if an invalid content-type is given. + Only text/html is allowed. + """ + caplog.set_level(logging.DEBUG) + url = 'https://pypi.org/simple/pip' + link = Link(url) + + session = mock.Mock(PipSession) + session.get.return_value = mock.Mock(**{ + "request.method": "GET", + "headers": {"Content-Type": content_type}, + }) + + assert _get_html_page(link, session=session) is None + assert ('pip._internal.index.collector', + logging.WARNING, + 'Skipping page {} because the GET request got Content-Type: {}.' + 'The supported Content-Type is text/html'.format( + url, content_type)) \ + in caplog.record_tuples + + def make_fake_html_response(url): """ Create a fake requests.Response object. From 64c78b19b93313d7ae849f3f90c18e6f800d6e9a Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Fri, 24 Apr 2020 19:22:29 +0530 Subject: [PATCH 1869/3170] Reword warning for invalid content-type --- src/pip/_internal/index/collector.py | 2 +- tests/unit/test_collector.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/index/collector.py b/src/pip/_internal/index/collector.py index 321861e2c76..7908ab996e6 100644 --- a/src/pip/_internal/index/collector.py +++ b/src/pip/_internal/index/collector.py @@ -457,7 +457,7 @@ def _get_html_page(link, session=None): except _NotHTML as exc: logger.warning( 'Skipping page %s because the %s request got Content-Type: %s.' - 'The supported Content-Type is text/html', + 'The only supported Content-Type is text/html', link, exc.request_desc, exc.content_type, ) except HTTPError as exc: diff --git a/tests/unit/test_collector.py b/tests/unit/test_collector.py index 9a7966688b3..0387813ad0a 100644 --- a/tests/unit/test_collector.py +++ b/tests/unit/test_collector.py @@ -501,7 +501,7 @@ def test_get_html_page_invalid_content_type(caplog, content_type): assert ('pip._internal.index.collector', logging.WARNING, 'Skipping page {} because the GET request got Content-Type: {}.' - 'The supported Content-Type is text/html'.format( + 'The only supported Content-Type is text/html'.format( url, content_type)) \ in caplog.record_tuples From 0d48186d1b832d19657d152d91c61e35cc0127b1 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Wed, 8 Apr 2020 09:19:33 +0530 Subject: [PATCH 1870/3170] Allow --prefer-binary option in requirements file --- news/7693.feature | 1 + src/pip/_internal/build_env.py | 2 ++ src/pip/_internal/index/package_finder.py | 9 +++++++++ src/pip/_internal/req/req_file.py | 4 ++++ 4 files changed, 16 insertions(+) create mode 100644 news/7693.feature diff --git a/news/7693.feature b/news/7693.feature new file mode 100644 index 00000000000..f84c5ac2cc9 --- /dev/null +++ b/news/7693.feature @@ -0,0 +1 @@ +Allow specifying --prefer-binary option in a requirements file diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py index b8f005f5ca9..089a523b725 100644 --- a/src/pip/_internal/build_env.py +++ b/src/pip/_internal/build_env.py @@ -193,6 +193,8 @@ def install_requirements( args.extend(['--trusted-host', host]) if finder.allow_all_prereleases: args.append('--pre') + if finder.prefer_binary: + args.append('--prefer-binary') args.append('--') args.extend(requirements) with open_spinner(message) as spinner: diff --git a/src/pip/_internal/index/package_finder.py b/src/pip/_internal/index/package_finder.py index 441992b92b3..731e4981d72 100644 --- a/src/pip/_internal/index/package_finder.py +++ b/src/pip/_internal/index/package_finder.py @@ -693,6 +693,15 @@ def set_allow_all_prereleases(self): # type: () -> None self._candidate_prefs.allow_all_prereleases = True + @property + def prefer_binary(self): + # type: () -> bool + return self._candidate_prefs.prefer_binary + + def set_prefer_binary(self): + # type: () -> None + self._candidate_prefs.prefer_binary = True + def make_link_evaluator(self, project_name): # type: (str) -> LinkEvaluator canonical_name = canonicalize_name(project_name) diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index 63cab76f6f2..cde0b08d6dd 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -60,6 +60,7 @@ cmdoptions.find_links, cmdoptions.no_binary, cmdoptions.only_binary, + cmdoptions.prefer_binary, cmdoptions.require_hashes, cmdoptions.pre, cmdoptions.trusted_host, @@ -260,6 +261,9 @@ def handle_option_line( if opts.pre: finder.set_allow_all_prereleases() + if opts.prefer_binary: + finder.set_prefer_binary() + if session: for host in opts.trusted_hosts or []: source = 'line {} of {}'.format(lineno, filename) From 714a6c5469d26260f975c2924dd1e803ab6fce0c Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Wed, 8 Apr 2020 21:04:16 +0530 Subject: [PATCH 1871/3170] Added unit tests --- tests/functional/test_download.py | 66 +++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/tests/functional/test_download.py b/tests/functional/test_download.py index 05b97ab3aec..e5bb89b7be5 100644 --- a/tests/functional/test_download.py +++ b/tests/functional/test_download.py @@ -688,6 +688,30 @@ def test_download_prefer_binary_when_tarball_higher_than_wheel(script, data): ) +def test_prefer_binary_tarball_higher_than_wheel_req_file(script, data): + fake_wheel(data, 'source-0.8-py2.py3-none-any.whl') + script.scratch_path.joinpath("test-req.txt").write_text(textwrap.dedent(""" + --prefer-binary + source + """)) + result = script.pip( + 'download', + '-r', script.scratch_path / 'test-req.txt', + '--no-index', + '-f', data.packages, + '-d', '.' + ) + + assert ( + Path('scratch') / 'source-0.8-py2.py3-none-any.whl' + in result.files_created + ) + assert ( + Path('scratch') / 'source-1.0.tar.gz' + not in result.files_created + ) + + def test_download_prefer_binary_when_wheel_doesnt_satisfy_req(script, data): fake_wheel(data, 'source-0.8-py2.py3-none-any.whl') script.scratch_path.joinpath("test-req.txt").write_text(textwrap.dedent(""" @@ -712,6 +736,30 @@ def test_download_prefer_binary_when_wheel_doesnt_satisfy_req(script, data): ) +def test_prefer_binary_when_wheel_doesnt_satisfy_req_req_file(script, data): + fake_wheel(data, 'source-0.8-py2.py3-none-any.whl') + script.scratch_path.joinpath("test-req.txt").write_text(textwrap.dedent(""" + --prefer-binary + source>0.9 + """)) + + result = script.pip( + 'download', + '--no-index', + '-f', data.packages, + '-d', '.', + '-r', script.scratch_path / 'test-req.txt' + ) + assert ( + Path('scratch') / 'source-1.0.tar.gz' + in result.files_created + ) + assert ( + Path('scratch') / 'source-0.8-py2.py3-none-any.whl' + not in result.files_created + ) + + def test_download_prefer_binary_when_only_tarball_exists(script, data): result = script.pip( 'download', @@ -726,6 +774,24 @@ def test_download_prefer_binary_when_only_tarball_exists(script, data): ) +def test_prefer_binary_when_only_tarball_exists_req_file(script, data): + script.scratch_path.joinpath("test-req.txt").write_text(textwrap.dedent(""" + --prefer-binary + source + """)) + result = script.pip( + 'download', + '--no-index', + '-f', data.packages, + '-d', '.', + '-r', script.scratch_path / 'test-req.txt' + ) + assert ( + Path('scratch') / 'source-1.0.tar.gz' + in result.files_created + ) + + @pytest.fixture(scope="session") def shared_script(tmpdir_factory, script_factory): tmpdir = Path(str(tmpdir_factory.mktemp("download_shared_script"))) From ebf27ee61ae300383b5a23b9c41bc42c5381c00a Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sat, 25 Apr 2020 10:00:30 +0530 Subject: [PATCH 1872/3170] Add quotes around option in News --- news/7693.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/7693.feature b/news/7693.feature index f84c5ac2cc9..4e458559110 100644 --- a/news/7693.feature +++ b/news/7693.feature @@ -1 +1 @@ -Allow specifying --prefer-binary option in a requirements file +Allow specifying ``--prefer-binary`` option in a requirements file From b4d8523b1f0fb320c75985de7b30b604df0d5a4a Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sun, 26 Apr 2020 00:46:02 +0530 Subject: [PATCH 1873/3170] Remove tests_install_cleanup tests using static build dir --- ...E77CF6-D22C-45A1-840F-AA913FF90F93.trivial | 0 tests/functional/test_install_cleanup.py | 101 ------------------ 2 files changed, 101 deletions(-) create mode 100644 news/FDE77CF6-D22C-45A1-840F-AA913FF90F93.trivial diff --git a/news/FDE77CF6-D22C-45A1-840F-AA913FF90F93.trivial b/news/FDE77CF6-D22C-45A1-840F-AA913FF90F93.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/functional/test_install_cleanup.py b/tests/functional/test_install_cleanup.py index 131caf681e3..ece2161cfb2 100644 --- a/tests/functional/test_install_cleanup.py +++ b/tests/functional/test_install_cleanup.py @@ -4,24 +4,6 @@ import pytest from pip._internal.cli.status_codes import PREVIOUS_BUILD_DIR_ERROR -from tests.lib import need_mercurial, windows_workaround_7667 -from tests.lib.local_repos import local_checkout - - -def test_cleanup_after_install(script, data): - """ - Test clean up after installing a package. - """ - script.pip( - 'install', '--no-index', - '--find-links={}'.format(data.find_links), - 'simple' - ) - build = script.venv_path / "build" - src = script.venv_path / "src" - assert not exists(build), "build/ dir still exists: {}".format(build) - assert not exists(src), "unexpected src/ dir exists: {}" .format(src) - script.assert_no_temp() @pytest.mark.network @@ -37,89 +19,6 @@ def test_no_clean_option_blocks_cleaning_after_install(script, data): assert exists(build) -@pytest.mark.network -@need_mercurial -@windows_workaround_7667 -def test_cleanup_after_install_editable_from_hg(script, tmpdir): - """ - Test clean up after cloning from Mercurial. - - """ - requirement = '{}#egg=ScriptTest'.format( - local_checkout('hg+https://bitbucket.org/ianb/scripttest', tmpdir) - ) - script.pip('install', '-e', requirement) - build = script.venv_path / 'build' - src = script.venv_path / 'src' - assert not exists(build), "build/ dir still exists: {}".format(build) - assert exists(src), "expected src/ dir doesn't exist: {}".format(src) - script.assert_no_temp() - - -def test_cleanup_after_install_from_local_directory(script, data): - """ - Test clean up after installing from a local directory. - """ - to_install = data.packages.joinpath("FSPkg") - script.pip('install', to_install) - build = script.venv_path / 'build' - src = script.venv_path / 'src' - assert not exists(build), "unexpected build/ dir exists: {}".format(build) - assert not exists(src), "unexpected src/ dir exist: {}".format(src) - script.assert_no_temp() - - -def test_cleanup_req_satisfied_no_name(script, data): - """ - Test cleanup when req is already satisfied, and req has no 'name' - """ - # this test confirms Issue #420 is fixed - # reqs with no 'name' that were already satisfied were leaving behind tmp - # build dirs - # 2 examples of reqs that would do this - # 1) https://bitbucket.org/ianb/initools/get/tip.zip - # 2) parent-0.1.tar.gz - dist = data.packages.joinpath("parent-0.1.tar.gz") - - script.pip('install', dist) - script.pip('install', dist) - - build = script.venv_path / 'build' - assert not exists(build), \ - "unexpected build/ dir exists: {build}".format(**locals()) - script.assert_no_temp() - - -def test_cleanup_after_install_exception(script, data): - """ - Test clean up after a 'setup.py install' exception. - """ - # broken==0.2broken fails during install; see packages readme file - result = script.pip( - 'install', '-f', data.find_links, '--no-index', 'broken==0.2broken', - expect_error=True, - ) - build = script.venv_path / 'build' - assert not exists(build), \ - "build/ dir still exists: {result.stdout}".format(**locals()) - script.assert_no_temp() - - -def test_cleanup_after_egg_info_exception(script, data): - """ - Test clean up after a 'setup.py egg_info' exception. - """ - # brokenegginfo fails during egg_info; see packages readme file - result = script.pip( - 'install', '-f', data.find_links, '--no-index', 'brokenegginfo==0.1', - expect_error=True, - ) - build = script.venv_path / 'build' - assert not exists(build), \ - "build/ dir still exists: {result.stdout}".format(**locals()) - script.assert_no_temp() - - @pytest.mark.network def test_cleanup_prevented_upon_build_dir_exception(script, data): """ From c2fa0dd9976c573e57afd0facec0886362863208 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Wed, 20 May 2020 15:37:28 +0100 Subject: [PATCH 1874/3170] Add a mark for tests that fail on the new resolver --- tests/conftest.py | 12 ++++++++++++ tests/functional/test_download.py | 1 + tests/functional/test_install.py | 8 ++++++++ tests/functional/test_install_config.py | 3 +++ tests/functional/test_install_direct_url.py | 3 +++ tests/functional/test_install_extras.py | 1 + tests/functional/test_install_reqs.py | 12 ++++++++++++ tests/functional/test_install_upgrade.py | 3 +++ tests/functional/test_install_user.py | 3 +++ tests/functional/test_install_vcs_git.py | 1 + tests/functional/test_uninstall_user.py | 1 + tests/functional/test_wheel.py | 1 + 12 files changed, 49 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index bf8cd7975c7..c5f369cb8d4 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -41,6 +41,12 @@ def pytest_addoption(parser): default=False, help="use new resolver in tests", ) + parser.addoption( + "--new-resolver-runtests", + action="store_true", + default=False, + help="run the skipped tests for the new resolver", + ) parser.addoption( "--use-venv", action="store_true", @@ -59,6 +65,12 @@ def pytest_collection_modifyitems(config, items): "CI" in os.environ): item.add_marker(pytest.mark.flaky(reruns=3)) + if (item.get_closest_marker('fails_on_new_resolver') and + config.getoption("--new-resolver") and + not config.getoption("--new-resolver-runtests")): + item.add_marker(pytest.mark.skip( + 'This test does not work with the new resolver')) + if six.PY3: if (item.get_closest_marker('incompatible_with_test_venv') and config.getoption("--use-venv")): diff --git a/tests/functional/test_download.py b/tests/functional/test_download.py index 05b97ab3aec..4c3e0ba3c43 100644 --- a/tests/functional/test_download.py +++ b/tests/functional/test_download.py @@ -669,6 +669,7 @@ def test_download_exit_status_code_when_blank_requirements_file(script): script.pip('download', '-r', 'blank.txt') +@pytest.mark.fails_on_new_resolver def test_download_prefer_binary_when_tarball_higher_than_wheel(script, data): fake_wheel(data, 'source-0.8-py2.py3-none-any.whl') result = script.pip( diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 6e19ef50699..0543f9e4cef 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -148,6 +148,7 @@ def test_pep518_with_user_pip(script, pip_src, data, common_wheels): ) +@pytest.mark.fails_on_new_resolver def test_pep518_with_extra_and_markers(script, data, common_wheels): script.pip( 'wheel', '--no-index', @@ -532,6 +533,7 @@ def assert_re_match(pattern, text): @pytest.mark.network +@pytest.mark.fails_on_new_resolver def test_hashed_install_failure_later_flag(script, tmpdir): with requirements_file( "blessings==1.0\n" @@ -937,6 +939,7 @@ def test_install_nonlocal_compatible_wheel(script, data): assert result.returncode == ERROR +@pytest.mark.fails_on_new_resolver def test_install_nonlocal_compatible_wheel_path(script, data): target_dir = script.scratch_path / 'target' @@ -1491,6 +1494,7 @@ def test_double_install(script): assert msg not in result.stderr +@pytest.mark.fails_on_new_resolver def test_double_install_fail(script): """ Test double install failing with two different version requirements @@ -1746,6 +1750,7 @@ def test_user_config_accepted(script): ] ) @pytest.mark.parametrize("use_module", [True, False]) +@pytest.mark.fails_on_new_resolver def test_install_pip_does_not_modify_pip_when_satisfied( script, install_args, expected_message, use_module): """ @@ -1757,6 +1762,7 @@ def test_install_pip_does_not_modify_pip_when_satisfied( assert expected_message in result.stdout, str(result) +@pytest.mark.fails_on_new_resolver def test_ignore_yanked_file(script, data): """ Test ignore a "yanked" file. @@ -1794,6 +1800,7 @@ def test_valid_index_url_argument(script, shared_data): assert 'Successfully installed Dinner' in result.stdout, str(result) +@pytest.mark.fails_on_new_resolver def test_install_yanked_file_and_print_warning(script, data): """ Test install a "yanked" file and print a warning. @@ -1873,6 +1880,7 @@ def test_install_skip_work_dir_pkg(script, data): assert 'Successfully installed simple' in result.stdout +@pytest.mark.fails_on_new_resolver def test_install_include_work_dir_pkg(script, data): """ Test that install of a package in working directory diff --git a/tests/functional/test_install_config.py b/tests/functional/test_install_config.py index 088016a9f1b..712fc4ae5c7 100644 --- a/tests/functional/test_install_config.py +++ b/tests/functional/test_install_config.py @@ -7,6 +7,7 @@ from tests.lib.server import file_response, package_page +@pytest.mark.fails_on_new_resolver def test_options_from_env_vars(script): """ Test if ConfigOptionParser reads env vars (e.g. not using PyPI here) @@ -43,6 +44,7 @@ def test_command_line_options_override_env_vars(script, virtualenv): @pytest.mark.network +@pytest.mark.fails_on_new_resolver def test_env_vars_override_config_file(script, virtualenv): """ Test that environmental variables override settings in config files. @@ -174,6 +176,7 @@ def test_config_file_override_stack( assert requests[3]["PATH_INFO"] == "/files/INITools-0.2.tar.gz" +@pytest.mark.fails_on_new_resolver def test_options_from_venv_config(script, virtualenv): """ Test if ConfigOptionParser reads a virtualenv-local config file diff --git a/tests/functional/test_install_direct_url.py b/tests/functional/test_install_direct_url.py index ec1e927ebf8..4afd3925e6f 100644 --- a/tests/functional/test_install_direct_url.py +++ b/tests/functional/test_install_direct_url.py @@ -1,5 +1,7 @@ import re +import pytest + from pip._internal.models.direct_url import DIRECT_URL_METADATA_NAME, DirectUrl from tests.lib import _create_test_package, path_to_url @@ -30,6 +32,7 @@ def test_install_vcs_editable_no_direct_url(script, with_wheel): assert not _get_created_direct_url(result, "testpkg") +@pytest.mark.fails_on_new_resolver def test_install_vcs_non_editable_direct_url(script, with_wheel): pkg_path = _create_test_package(script, name="testpkg") url = path_to_url(pkg_path) diff --git a/tests/functional/test_install_extras.py b/tests/functional/test_install_extras.py index 3c0359a73f1..dfde7d1b676 100644 --- a/tests/functional/test_install_extras.py +++ b/tests/functional/test_install_extras.py @@ -136,6 +136,7 @@ def test_install_special_extra(script): pytest.param('[extra2]', '1.0', marks=pytest.mark.xfail), pytest.param('[extra1,extra2]', '1.0', marks=pytest.mark.xfail), ]) +@pytest.mark.fails_on_new_resolver def test_install_extra_merging(script, data, extra_to_install, simple_version): # Check that extra specifications in the extras section are honoured. pkga_path = script.scratch_path / 'pkga' diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index 40b0c3c75ec..d39263b98ea 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -189,6 +189,7 @@ def test_respect_order_in_requirements_file(script, data): ) +@pytest.mark.fails_on_new_resolver def test_install_local_editable_with_extras(script, data): to_install = data.packages.joinpath("LocalExtras") res = script.pip_install_local( @@ -336,6 +337,7 @@ def test_constraints_local_install_causes_error(script, data): assert 'Could not satisfy constraints for' in result.stderr +@pytest.mark.fails_on_new_resolver def test_constraints_constrain_to_local_editable(script, data): to_install = data.src.joinpath("singlemodule") script.scratch_path.joinpath("constraints.txt").write_text( @@ -347,6 +349,7 @@ def test_constraints_constrain_to_local_editable(script, data): assert 'Running setup.py develop for singlemodule' in result.stdout +@pytest.mark.fails_on_new_resolver def test_constraints_constrain_to_local(script, data): to_install = data.src.joinpath("singlemodule") script.scratch_path.joinpath("constraints.txt").write_text( @@ -358,6 +361,7 @@ def test_constraints_constrain_to_local(script, data): assert 'Running setup.py install for singlemodule' in result.stdout +@pytest.mark.fails_on_new_resolver def test_constrained_to_url_install_same_url(script, data): to_install = data.src.joinpath("singlemodule") constraints = path_to_url(to_install) + "#egg=singlemodule" @@ -403,6 +407,7 @@ def test_double_install_spurious_hash_mismatch( assert 'Successfully installed simple-1.0' in str(result) +@pytest.mark.fails_on_new_resolver def test_install_with_extras_from_constraints(script, data): to_install = data.packages.joinpath("LocalExtras") script.scratch_path.joinpath("constraints.txt").write_text( @@ -413,6 +418,7 @@ def test_install_with_extras_from_constraints(script, data): assert script.site_packages / 'simple' in result.files_created +@pytest.mark.fails_on_new_resolver def test_install_with_extras_from_install(script, data): to_install = data.packages.joinpath("LocalExtras") script.scratch_path.joinpath("constraints.txt").write_text( @@ -423,6 +429,7 @@ def test_install_with_extras_from_install(script, data): assert script.site_packages / 'singlemodule.py' in result.files_created +@pytest.mark.fails_on_new_resolver def test_install_with_extras_joined(script, data): to_install = data.packages.joinpath("LocalExtras") script.scratch_path.joinpath("constraints.txt").write_text( @@ -435,6 +442,7 @@ def test_install_with_extras_joined(script, data): assert script.site_packages / 'singlemodule.py' in result.files_created +@pytest.mark.fails_on_new_resolver def test_install_with_extras_editable_joined(script, data): to_install = data.packages.joinpath("LocalExtras") script.scratch_path.joinpath("constraints.txt").write_text( @@ -465,6 +473,7 @@ def test_install_distribution_duplicate_extras(script, data): assert expected in result.stderr +@pytest.mark.fails_on_new_resolver def test_install_distribution_union_with_constraints(script, data): to_install = data.packages.joinpath("LocalExtras") script.scratch_path.joinpath("constraints.txt").write_text( @@ -475,6 +484,7 @@ def test_install_distribution_union_with_constraints(script, data): assert script.site_packages / 'singlemodule.py' in result.files_created +@pytest.mark.fails_on_new_resolver def test_install_distribution_union_with_versions(script, data): to_install_001 = data.packages.joinpath("LocalExtras") to_install_002 = data.packages.joinpath("LocalExtras-0.0.2") @@ -497,6 +507,7 @@ def test_install_distribution_union_conflicting_extras(script, data): assert "Conflict" in result.stderr +@pytest.mark.fails_on_new_resolver def test_install_unsupported_wheel_link_with_marker(script): script.scratch_path.joinpath("with-marker.txt").write_text( textwrap.dedent("""\ @@ -515,6 +526,7 @@ def test_install_unsupported_wheel_link_with_marker(script): assert len(result.files_created) == 0 +@pytest.mark.fails_on_new_resolver def test_install_unsupported_wheel_file(script, data): # Trying to install a local wheel with an incompatible version/type # should fail. diff --git a/tests/functional/test_install_upgrade.py b/tests/functional/test_install_upgrade.py index f5445a0b3e6..604d2afa812 100644 --- a/tests/functional/test_install_upgrade.py +++ b/tests/functional/test_install_upgrade.py @@ -36,6 +36,7 @@ def test_invalid_upgrade_strategy_causes_error(script): assert "invalid choice" in result.stderr +@pytest.mark.fails_on_new_resolver def test_only_if_needed_does_not_upgrade_deps_when_satisfied(script): """ It doesn't upgrade a dependency if it already satisfies the requirements. @@ -181,6 +182,7 @@ def test_upgrade_if_requested(script): ) +@pytest.mark.fails_on_new_resolver def test_upgrade_with_newest_already_installed(script, data): """ If the newest version of a package is already installed, the package should @@ -249,6 +251,7 @@ def test_uninstall_before_upgrade_from_url(script): @pytest.mark.network +@pytest.mark.fails_on_new_resolver def test_upgrade_to_same_version_from_url(script): """ When installing from a URL the same version that is already installed, no diff --git a/tests/functional/test_install_user.py b/tests/functional/test_install_user.py index 09dbdf4912a..c885bf4b6ef 100644 --- a/tests/functional/test_install_user.py +++ b/tests/functional/test_install_user.py @@ -126,6 +126,7 @@ def test_install_user_conflict_in_usersite(self, script): @pytest.mark.network @pytest.mark.incompatible_with_test_venv + @pytest.mark.fails_on_new_resolver def test_install_user_conflict_in_globalsite(self, virtualenv, script): """ Test user install with conflict in global site ignores site and @@ -158,6 +159,7 @@ def test_install_user_conflict_in_globalsite(self, virtualenv, script): @pytest.mark.network @pytest.mark.incompatible_with_test_venv + @pytest.mark.fails_on_new_resolver def test_upgrade_user_conflict_in_globalsite(self, virtualenv, script): """ Test user install/upgrade with conflict in global site ignores site and @@ -189,6 +191,7 @@ def test_upgrade_user_conflict_in_globalsite(self, virtualenv, script): @pytest.mark.network @pytest.mark.incompatible_with_test_venv + @pytest.mark.fails_on_new_resolver def test_install_user_conflict_in_globalsite_and_usersite( self, virtualenv, script): """ diff --git a/tests/functional/test_install_vcs_git.py b/tests/functional/test_install_vcs_git.py index 6c6f5a0c7d7..97b792c0135 100644 --- a/tests/functional/test_install_vcs_git.py +++ b/tests/functional/test_install_vcs_git.py @@ -495,6 +495,7 @@ def test_install_git_branch_not_cached(script, with_wheel): ), result.stdout +@pytest.mark.fails_on_new_resolver def test_install_git_sha_cached(script, with_wheel): """ Installing git urls with a sha revision does cause wheel caching. diff --git a/tests/functional/test_uninstall_user.py b/tests/functional/test_uninstall_user.py index df635ccf8f8..a367796fdfd 100644 --- a/tests/functional/test_uninstall_user.py +++ b/tests/functional/test_uninstall_user.py @@ -22,6 +22,7 @@ def test_uninstall_from_usersite(self, script): result2 = script.pip('uninstall', '-y', 'INITools') assert_all_changes(result1, result2, [script.venv / 'build', 'cache']) + @pytest.mark.fails_on_new_resolver def test_uninstall_from_usersite_with_dist_in_global_site( self, virtualenv, script): """ diff --git a/tests/functional/test_wheel.py b/tests/functional/test_wheel.py index 545c50ac9a8..6bf0486819a 100644 --- a/tests/functional/test_wheel.py +++ b/tests/functional/test_wheel.py @@ -62,6 +62,7 @@ def test_pip_wheel_success(script, data): assert "Successfully built simple" in result.stdout, result.stdout +@pytest.mark.fails_on_new_resolver def test_pip_wheel_build_cache(script, data): """ Test 'pip wheel' builds and caches. From 8346c44f1cb031f470f8f82e68fac07f0e9f255a Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Thu, 21 May 2020 15:58:43 +0100 Subject: [PATCH 1875/3170] Register the new marker --- setup.cfg | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index 03abe8cea2c..f0bd7a8d9bd 100644 --- a/setup.cfg +++ b/setup.cfg @@ -39,7 +39,7 @@ ignore_errors = True [tool:pytest] addopts = --ignore src/pip/_vendor --ignore tests/tests_cache -r aR markers = - network: tests that needs network + network: tests that need network incompatible_with_test_venv incompatible_with_venv no_auto_tempdir_manager @@ -50,6 +50,7 @@ markers = mercurial: VCS: Mercurial git: VCS: git yaml: yaml based tests + fails_on_new_resolver: Does not yet work on the new resolver [bdist_wheel] universal = 1 From 66f323a53ead9a381af8fc093f21b88a4422c734 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Thu, 21 May 2020 15:59:25 +0100 Subject: [PATCH 1876/3170] Fix a marker typo that the new registration of markers picked up :-) --- tests/functional/test_cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index 4e2390aa79d..e30b2c07987 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -170,7 +170,7 @@ def test_cache_list_name_and_version_match(script): assert not list_matches_wheel('zzz-7.8.9', result) -@pytest.mark.usefixture("populate_wheel_cache") +@pytest.mark.usefixtures("populate_wheel_cache") def test_cache_remove_no_arguments(script): """Running `pip cache remove` with no arguments should cause an error.""" script.pip('cache', 'remove', expect_error=True) From 775f9ff6ca75173d8b5c0cb2260c54ed1c1f60e6 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 21 May 2020 20:36:51 +0530 Subject: [PATCH 1877/3170] Add a helper for making ExplicitRequirement objects --- src/pip/_internal/resolution/resolvelib/factory.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index c59944d7ea5..7e0748c42e6 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -180,12 +180,15 @@ def make_requirement_from_install_req(self, ireq): # TODO: Get name and version from ireq, if possible? # Specifically, this might be needed in "name @ URL" # syntax - need to check where that syntax is handled. - cand = self._make_candidate_from_link( + candidate = self._make_candidate_from_link( ireq.link, extras=set(ireq.extras), parent=ireq, ) - return ExplicitRequirement(cand) + return self.make_requirement_from_candidate(candidate) return SpecifierRequirement(ireq, factory=self) + def make_requirement_from_candidate(self, candidate): + return ExplicitRequirement(candidate) + def make_requirement_from_spec(self, specifier, comes_from): # type: (str, InstallRequirement) -> Requirement ireq = self._make_install_req_from_spec(specifier, comes_from) From 95347df102a692ee31346bc97ca4cb71bf48cc7b Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 21 May 2020 20:37:59 +0530 Subject: [PATCH 1878/3170] Directly require BaseCandidate in ExtrasCandidate --- src/pip/_internal/resolution/resolvelib/candidates.py | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 2937c29424f..b13415d5ebf 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -425,11 +425,7 @@ def iter_dependencies(self): yield requirement # Add a dependency on the exact base. - # (See note 2b in the class docstring) - # FIXME: This does not work if the base candidate is specified by - # link, e.g. "pip install .[dev]" will fail. - spec = "{}=={}".format(self.base.name, self.base.version) - yield factory.make_requirement_from_spec(spec, self.base._ireq) + yield factory.make_requirement_from_candidate(self.base) def get_install_requirement(self): # type: () -> Optional[InstallRequirement] From 9506a281147a5f07c8eba4d6767e1f8bae05b828 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 21 May 2020 21:08:01 +0530 Subject: [PATCH 1879/3170] ExtrasCandidate depends on exact base, before optional dependencies --- src/pip/_internal/resolution/resolvelib/candidates.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index b13415d5ebf..8d32c590a9a 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -417,6 +417,9 @@ def iter_dependencies(self): extra ) + # Add a dependency on the exact base. + yield factory.make_requirement_from_candidate(self.base) + for r in self.base.dist.requires(valid_extras): requirement = factory.make_requirement_from_spec_matching_extras( str(r), self.base._ireq, valid_extras, @@ -424,9 +427,6 @@ def iter_dependencies(self): if requirement: yield requirement - # Add a dependency on the exact base. - yield factory.make_requirement_from_candidate(self.base) - def get_install_requirement(self): # type: () -> Optional[InstallRequirement] # We don't return anything here, because we always From 901898c84f83bf767102b49683bd51d32650d8eb Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 21 May 2020 21:18:44 +0530 Subject: [PATCH 1880/3170] Make mypy happy --- src/pip/_internal/resolution/resolvelib/factory.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 7e0748c42e6..046119cfe45 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -187,6 +187,7 @@ def make_requirement_from_install_req(self, ireq): return SpecifierRequirement(ireq, factory=self) def make_requirement_from_candidate(self, candidate): + # type: (Candidate) -> ExplicitRequirement return ExplicitRequirement(candidate) def make_requirement_from_spec(self, specifier, comes_from): From 24e4cf71186750d152ce0d8438653e525079f4b2 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 21 May 2020 21:25:19 +0530 Subject: [PATCH 1881/3170] Update comments and documentation --- src/pip/_internal/resolution/resolvelib/candidates.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 8d32c590a9a..da11c4fe785 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -347,8 +347,8 @@ class ExtrasCandidate(Candidate): to treat it as a separate node in the dependency graph. 2. When we're getting the candidate's dependencies, a) We specify that we want the extra dependencies as well. - b) We add a dependency on the base candidate (matching the name and - version). See below for why this is needed. + b) We add a dependency on the base candidate. + See below for why this is needed. 3. We return None for the underlying InstallRequirement, as the base candidate will provide it, and we don't want to end up with duplicates. @@ -417,7 +417,8 @@ def iter_dependencies(self): extra ) - # Add a dependency on the exact base. + # Add a dependency on the exact base + # (See note 2b in the class docstring) yield factory.make_requirement_from_candidate(self.base) for r in self.base.dist.requires(valid_extras): From 50c9ea2fe0ea69990a4a01ab6d9d409555a16077 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 21 May 2020 21:28:59 +0530 Subject: [PATCH 1882/3170] This works now! --- tests/functional/test_new_resolver.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index b0da43279e6..693ede21a78 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -825,9 +825,7 @@ def _wheel_from_index(script, name, version, requires, extras): @pytest.mark.parametrize( "pkg_builder", [ - pytest.param( - _local_with_setup, marks=pytest.mark.xfail(strict=True), - ), + _local_with_setup, _direct_wheel, _wheel_from_index, ], From 3ed33bd3a1223ca3fb3887ab5641364e2b7cd402 Mon Sep 17 00:00:00 2001 From: Christian Heimes <christian@python.org> Date: Thu, 21 May 2020 18:49:22 +0200 Subject: [PATCH 1883/3170] update html5lib vendoring patch --- .../vendoring/patches/html5lib.patch | 24 +++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/tools/automation/vendoring/patches/html5lib.patch b/tools/automation/vendoring/patches/html5lib.patch index 92a34e4b2e7..ae9cafe2d8e 100644 --- a/tools/automation/vendoring/patches/html5lib.patch +++ b/tools/automation/vendoring/patches/html5lib.patch @@ -28,4 +28,28 @@ index dcfac220..d8b53004 100644 + from collections import MutableMapping from xml.dom import minidom, Node import weakref + +diff --git a/src/pip/_vendor/html5lib/_utils.py b/src/pip/_vendor/html5lib/_utils.py +index 0703afb3..96eb17b2 100644 +--- a/src/pip/_vendor/html5lib/_utils.py ++++ b/src/pip/_vendor/html5lib/_utils.py +@@ -2,12 +2,15 @@ from __future__ import absolute_import, division, unicode_literals + from types import ModuleType + +-from pip._vendor.six import text_type ++from pip._vendor.six import text_type, PY3 + +-try: +- import xml.etree.cElementTree as default_etree +-except ImportError: ++if PY3: + import xml.etree.ElementTree as default_etree ++else: ++ try: ++ import xml.etree.cElementTree as default_etree ++ except ImportError: ++ import xml.etree.ElementTree as default_etree + + + __all__ = ["default_etree", "MethodDispatcher", "isSurrogatePair", From 50cbd6a03245d6315da74c470cf1c481ef554cba Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Thu, 21 May 2020 22:46:56 +0530 Subject: [PATCH 1884/3170] Assert not None instead of if check --- src/pip/_internal/utils/encoding.py | 6 +++--- src/pip/_internal/utils/unpacking.py | 8 +++----- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/utils/encoding.py b/src/pip/_internal/utils/encoding.py index 823e7fc0455..5b83d61bb13 100644 --- a/src/pip/_internal/utils/encoding.py +++ b/src/pip/_internal/utils/encoding.py @@ -33,9 +33,9 @@ def auto_decode(data): for line in data.split(b'\n')[:2]: if line[0:1] == b'#' and ENCODING_RE.search(line): result = ENCODING_RE.search(line) - if result: - encoding = result.groups()[0].decode('ascii') - return data.decode(encoding) + assert result is not None + encoding = result.groups()[0].decode('ascii') + return data.decode(encoding) return data.decode( locale.getpreferredencoding(False) or sys.getdefaultencoding(), ) diff --git a/src/pip/_internal/utils/unpacking.py b/src/pip/_internal/utils/unpacking.py index 93a8b15ca21..fe71d26e355 100644 --- a/src/pip/_internal/utils/unpacking.py +++ b/src/pip/_internal/utils/unpacking.py @@ -205,7 +205,6 @@ def untar_file(filename, location): ) continue else: - fp = None try: fp = tar.extractfile(member) except (KeyError, AttributeError) as exc: @@ -217,11 +216,10 @@ def untar_file(filename, location): ) continue ensure_dir(os.path.dirname(path)) + assert fp is not None with open(path, 'wb') as destfp: - if fp: - shutil.copyfileobj(fp, destfp) - if fp: - fp.close() + shutil.copyfileobj(fp, destfp) + fp.close() # Update the timestamp (useful for cython compiled files) # https://github.com/python/typeshed/issues/2673 tar.utime(member, path) # type: ignore From 197bbceed172bebba95b5f3a8ff190820a6107df Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Tue, 19 May 2020 12:49:46 +0530 Subject: [PATCH 1885/3170] Add news file and convert TransformedHit type to TypedDict --- ...3C29002F-4AB2-4093-B321-994F7882F944.trivial | 0 src/pip/_internal/commands/search.py | 17 +++++++++++------ 2 files changed, 11 insertions(+), 6 deletions(-) create mode 100644 news/3C29002F-4AB2-4093-B321-994F7882F944.trivial diff --git a/news/3C29002F-4AB2-4093-B321-994F7882F944.trivial b/news/3C29002F-4AB2-4093-B321-994F7882F944.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/commands/search.py b/src/pip/_internal/commands/search.py index 8d581b9bdfc..d8c5fba8f0e 100644 --- a/src/pip/_internal/commands/search.py +++ b/src/pip/_internal/commands/search.py @@ -25,7 +25,11 @@ if MYPY_CHECK_RUNNING: from optparse import Values from typing import Any, List, Dict, Optional - + from typing_extensions import TypedDict + TransformedHit = TypedDict( + 'TransformedHit', + {'name': str, 'summary': str, 'versions': List[str]}, + ) logger = logging.getLogger(__name__) @@ -79,13 +83,13 @@ def search(self, query, options): def transform_hits(hits): - # type: (List[Dict[str, str]]) -> List[Dict[str, Any]] + # type: (List[Dict[str, str]]) -> List[TransformedHit] """ The list from pypi is really a list of versions. We want a list of packages with the list of versions stored inline. This converts the list from pypi into one we can use. """ - packages = OrderedDict() # type: OrderedDict[str, Any] + packages = OrderedDict() # type: OrderedDict[str, TransformedHit] for hit in hits: name = hit['name'] summary = hit['summary'] @@ -108,7 +112,7 @@ def transform_hits(hits): def print_results(hits, name_column_width=None, terminal_width=None): - # type: (List[Dict[str, Any]], Optional[int], Optional[int]) -> None + # type: (List[TransformedHit], Optional[int], Optional[int]) -> None if not hits: return if name_column_width is None: @@ -126,8 +130,9 @@ def print_results(hits, name_column_width=None, terminal_width=None): target_width = terminal_width - name_column_width - 5 if target_width > 10: # wrap and indent summary to fit terminal - summary = textwrap.wrap(summary, target_width) - summary = ('\n' + ' ' * (name_column_width + 3)).join(summary) + summary_lines = textwrap.wrap(summary, target_width) + summary = ('\n' + ' ' * (name_column_width + 3)).join( + summary_lines) line = '{name_latest:{name_column_width}} - {summary}'.format( name_latest='{name} ({latest})'.format(**locals()), From 716a067335a39766a9d05544093cc36573500dfb Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Fri, 22 May 2020 00:32:37 +0530 Subject: [PATCH 1886/3170] Return int status code from download.run --- src/pip/_internal/commands/download.py | 7 ++++--- tests/functional/test_download.py | 4 +++- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index 570d7289c37..62bca709cc2 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -6,6 +6,7 @@ from pip._internal.cli import cmdoptions from pip._internal.cli.cmdoptions import make_target_python from pip._internal.cli.req_command import RequirementCommand, with_cleanup +from pip._internal.cli.status_codes import ERROR, SUCCESS from pip._internal.req.req_tracker import get_requirement_tracker from pip._internal.utils.misc import ensure_dir, normalize_path, write_output from pip._internal.utils.temp_dir import TempDirectory @@ -14,7 +15,6 @@ if MYPY_CHECK_RUNNING: from optparse import Values from typing import Any, List - from pip._internal.req.req_set import RequirementSet logger = logging.getLogger(__name__) @@ -81,7 +81,7 @@ def __init__(self, *args, **kw): @with_cleanup def run(self, options, args): - # type: (Values, List[str]) -> RequirementSet + # type: (Values, List[str]) -> int options.ignore_installed = True # editable doesn't really make sense for `pip download`, but the bowels @@ -144,5 +144,6 @@ def run(self, options, args): ]) if downloaded: write_output('Successfully downloaded %s', downloaded) + return SUCCESS - return requirement_set + return ERROR diff --git a/tests/functional/test_download.py b/tests/functional/test_download.py index 05b97ab3aec..2b6ff8e9dca 100644 --- a/tests/functional/test_download.py +++ b/tests/functional/test_download.py @@ -666,7 +666,9 @@ def test_download_exit_status_code_when_blank_requirements_file(script): Test download exit status code when blank requirements file specified """ script.scratch_path.joinpath("blank.txt").write_text("\n") - script.pip('download', '-r', 'blank.txt') + result = script.pip('download', '-r', 'blank.txt', expect_error=True) + print(result) + assert result.returncode == ERROR def test_download_prefer_binary_when_tarball_higher_than_wheel(script, data): From b7f4f4a992dd69c875f6c3bac56ca3c82b66e2ae Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Fri, 22 May 2020 00:41:00 +0530 Subject: [PATCH 1887/3170] Update and rename 8278.bugfix to 8278.vendor --- news/8278.bugfix | 1 - news/8278.vendor | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 news/8278.bugfix create mode 100644 news/8278.vendor diff --git a/news/8278.bugfix b/news/8278.bugfix deleted file mode 100644 index 3a95794f19b..00000000000 --- a/news/8278.bugfix +++ /dev/null @@ -1 +0,0 @@ -htmlib5 no longer imports deprecated xml.etree.cElementTree on Python 3. diff --git a/news/8278.vendor b/news/8278.vendor new file mode 100644 index 00000000000..ad3ce7e4506 --- /dev/null +++ b/news/8278.vendor @@ -0,0 +1 @@ +Vendored htmlib5 no longer imports deprecated xml.etree.cElementTree on Python 3. From 5e33373a0738a03ca8515e3618046f0ad4b10582 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Fri, 22 May 2020 00:41:50 +0530 Subject: [PATCH 1888/3170] Remove req.name check --- src/pip/_internal/commands/download.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index 62bca709cc2..01ebf6a5570 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -138,10 +138,9 @@ def run(self, options, args): reqs, check_supported_wheels=True ) - downloaded = ' '.join([ - req.name for req in requirement_set.requirements.values() - if req.successfully_downloaded and req.name - ]) + downloaded = ' '.join([req.name # type: ignore + for req in requirement_set.requirements.values() + if req.successfully_downloaded]) if downloaded: write_output('Successfully downloaded %s', downloaded) return SUCCESS From a731989b0f4467f725f6bda521ed6a09568f6993 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Fri, 22 May 2020 00:43:08 +0530 Subject: [PATCH 1889/3170] Document the html5lib modification in Vendoring docs --- src/pip/_vendor/README.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pip/_vendor/README.rst b/src/pip/_vendor/README.rst index c5ed6b0d542..f9208057672 100644 --- a/src/pip/_vendor/README.rst +++ b/src/pip/_vendor/README.rst @@ -100,8 +100,9 @@ Modifications * ``setuptools`` is completely stripped to only keep ``pkg_resources`` * ``pkg_resources`` has been modified to import its dependencies from ``pip._vendor`` * ``packaging`` has been modified to import its dependencies from ``pip._vendor`` -* ``html5lib`` has been modified to import six from ``pip._vendor`` and - to prefer importing from ``collections.abc`` instead of ``collections``. +* ``html5lib`` has been modified to import six from ``pip._vendor``, to prefer + importing from ``collections.abc`` instead of ``collections`` and does not import + ``xml.etree.cElementTree`` on Python 3. * ``CacheControl`` has been modified to import its dependencies from ``pip._vendor`` * ``requests`` has been modified to import its other dependencies from ``pip._vendor`` and to *not* load ``simplejson`` (all platforms) and ``pyopenssl`` (Windows). From ea3aa04987163b76c4292be0446b5597e478c748 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sun, 17 May 2020 04:37:53 +0530 Subject: [PATCH 1890/3170] Add mypy annotations to pip._internal.req.constructors --- src/pip/_internal/req/constructors.py | 7 ++----- src/pip/_internal/req/req_install.py | 2 +- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index c9f1fe71396..fd54c4347f1 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -8,9 +8,6 @@ InstallRequirement. """ -# The following comment should be removed at some point in the future. -# mypy: strict-optional=False - import logging import os import re @@ -188,7 +185,7 @@ def __init__( requirement, # type: Optional[Requirement] link, # type: Optional[Link] markers, # type: Optional[Marker] - extras, # type: Set[str] + extras, # type: Optional[Set[str]] ): self.requirement = requirement self.link = link @@ -264,7 +261,7 @@ def _looks_like_path(name): def _get_url_from_path(path, name): - # type: (str, str) -> str + # type: (str, str) -> Optional[str] """ First, it checks whether a provided path is an installable directory (e.g. it has a setup.py). If it is, returns the path. diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 3b28209b1bd..b570d026ac0 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -110,7 +110,7 @@ def __init__( global_options=None, # type: Optional[List[str]] hash_options=None, # type: Optional[Dict[str, List[str]]] constraint=False, # type: bool - extras=() # type: Iterable[str] + extras=() # type: Optional[Iterable[str]] ): # type: (...) -> None assert req is None or isinstance(req, Requirement), req From 311eb48ca585a22d07801b1ca6edf7b6403a0f6a Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Fri, 22 May 2020 01:51:20 +0530 Subject: [PATCH 1891/3170] Add mypy annotations to pip._internal.req.req_set --- src/pip/_internal/req/req_set.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/req/req_set.py b/src/pip/_internal/req/req_set.py index f168ce17abd..d64bb78a327 100644 --- a/src/pip/_internal/req/req_set.py +++ b/src/pip/_internal/req/req_set.py @@ -1,6 +1,3 @@ -# The following comment should be removed at some point in the future. -# mypy: strict-optional=False - from __future__ import absolute_import import logging @@ -122,7 +119,8 @@ def add_requirement( return [install_req], None try: - existing_req = self.get_requirement(install_req.name) + existing_req = self.get_requirement( + install_req.name) # type: Optional[InstallRequirement] except KeyError: existing_req = None From 4812f77503330eb0ea4c43d390a6af12769715bb Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Fri, 22 May 2020 01:51:50 +0530 Subject: [PATCH 1892/3170] Add mypy annotations to pip._internal.req.req_tracker --- src/pip/_internal/req/req_tracker.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/req/req_tracker.py b/src/pip/_internal/req/req_tracker.py index 14adeab29b5..6beaa8d1fd3 100644 --- a/src/pip/_internal/req/req_tracker.py +++ b/src/pip/_internal/req/req_tracker.py @@ -1,6 +1,3 @@ -# The following comment should be removed at some point in the future. -# mypy: strict-optional=False - from __future__ import absolute_import import contextlib @@ -99,6 +96,7 @@ def add(self, req): """ # Get the file to write information about this requirement. + assert req.link entry_path = self._entry_path(req.link) # Try reading from the file. If it exists and can be read from, a build @@ -130,6 +128,7 @@ def remove(self, req): """Remove an InstallRequirement from build tracking. """ + assert req.link # Delete the created file and the corresponding entries. os.unlink(self._entry_path(req.link)) self._entries.remove(req) From 8b5e1583f8464ae5ca8dbae1091505f85f3b7e05 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Fri, 22 May 2020 01:52:49 +0530 Subject: [PATCH 1893/3170] Add news file --- news/749E6F3D-CAEB-4AEA-A53F-E623365ACB82.trivial | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 news/749E6F3D-CAEB-4AEA-A53F-E623365ACB82.trivial diff --git a/news/749E6F3D-CAEB-4AEA-A53F-E623365ACB82.trivial b/news/749E6F3D-CAEB-4AEA-A53F-E623365ACB82.trivial new file mode 100644 index 00000000000..e69de29bb2d From 2d22608809ab024777987f66d22cff3c171f3684 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Fri, 22 May 2020 03:00:55 +0530 Subject: [PATCH 1894/3170] Type annotations for pip._internal.build_env --- src/pip/_internal/build_env.py | 37 ++++++++++++++++++++++++++-------- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py index b8f005f5ca9..3c780f6dabf 100644 --- a/src/pip/_internal/build_env.py +++ b/src/pip/_internal/build_env.py @@ -1,10 +1,6 @@ """Build Environment used for isolation during sdist building """ -# The following comment should be removed at some point in the future. -# mypy: strict-optional=False -# mypy: disallow-untyped-defs=False - import logging import os import sys @@ -22,7 +18,8 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Tuple, Set, Iterable, Optional, List + from types import TracebackType + from typing import Tuple, Set, Iterable, Optional, List, Type from pip._internal.index.package_finder import PackageFinder logger = logging.getLogger(__name__) @@ -110,6 +107,7 @@ def __init__(self): ).format(system_sites=system_sites, lib_dirs=self._lib_dirs)) def __enter__(self): + # type: () -> None self._save_env = { name: os.environ.get(name, None) for name in ('PATH', 'PYTHONNOUSERSITE', 'PYTHONPATH') @@ -128,7 +126,13 @@ def __enter__(self): 'PYTHONPATH': os.pathsep.join(pythonpath), }) - def __exit__(self, exc_type, exc_val, exc_tb): + def __exit__( + self, + exc_type, # type: Optional[Type[BaseException]] + exc_val, # type: Optional[BaseException] + exc_tb # type: Optional[TracebackType] + ): + # type: (...) -> None for varname, old_value in self._save_env.items(): if old_value is None: os.environ.pop(varname, None) @@ -195,6 +199,7 @@ def install_requirements( args.append('--pre') args.append('--') args.extend(requirements) + assert message with open_spinner(message) as spinner: call_subprocess(args, spinner=spinner) @@ -204,16 +209,32 @@ class NoOpBuildEnvironment(BuildEnvironment): """ def __init__(self): + # type: () -> None pass def __enter__(self): + # type: () -> None pass - def __exit__(self, exc_type, exc_val, exc_tb): + def __exit__( + self, + exc_type, # type: Optional[Type[BaseException]] + exc_val, # type: Optional[BaseException] + exc_tb # type: Optional[TracebackType] + ): + # type: (...) -> None pass def cleanup(self): + # type: () -> None pass - def install_requirements(self, finder, requirements, prefix, message): + def install_requirements( + self, + finder, # type: PackageFinder + requirements, # type: Iterable[str] + prefix_as_string, # type: str + message # type: Optional[str] + ): + # type: (...) -> None raise NotImplementedError() From cd1e8cd604cdf54bbf2ed145c4dda37ffafc8441 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Fri, 22 May 2020 03:05:22 +0530 Subject: [PATCH 1895/3170] Type annotations for pip._internal.self_outdated_check --- src/pip/_internal/self_outdated_check.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pip/_internal/self_outdated_check.py b/src/pip/_internal/self_outdated_check.py index 8fc3c594acf..c5dcdb51b0a 100644 --- a/src/pip/_internal/self_outdated_check.py +++ b/src/pip/_internal/self_outdated_check.py @@ -1,6 +1,3 @@ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - from __future__ import absolute_import import datetime @@ -104,6 +101,7 @@ def __init__(self, cache_dir): @property def key(self): + # type: () -> str return sys.prefix def save(self, pypi_version, current_time): From 2c5cab492c29169102d0cd4e2ebb4ab93313fb80 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Fri, 22 May 2020 02:27:09 +0530 Subject: [PATCH 1896/3170] Pass empty set if parts.extras is None --- src/pip/_internal/req/constructors.py | 4 ++-- src/pip/_internal/req/req_install.py | 2 +- src/pip/_internal/req/req_tracker.py | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index fd54c4347f1..f0d365c027b 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -236,7 +236,7 @@ def install_req_from_editable( install_options=options.get("install_options", []) if options else [], global_options=options.get("global_options", []) if options else [], hash_options=options.get("hashes", {}) if options else {}, - extras=parts.extras, + extras=parts.extras if parts.extras else set(), ) @@ -399,7 +399,7 @@ def install_req_from_line( global_options=options.get("global_options", []) if options else [], hash_options=options.get("hashes", {}) if options else {}, constraint=constraint, - extras=parts.extras, + extras=parts.extras if parts.extras else set(), ) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index b570d026ac0..3b28209b1bd 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -110,7 +110,7 @@ def __init__( global_options=None, # type: Optional[List[str]] hash_options=None, # type: Optional[Dict[str, List[str]]] constraint=False, # type: bool - extras=() # type: Optional[Iterable[str]] + extras=() # type: Iterable[str] ): # type: (...) -> None assert req is None or isinstance(req, Requirement), req diff --git a/src/pip/_internal/req/req_tracker.py b/src/pip/_internal/req/req_tracker.py index 6beaa8d1fd3..13fb24563fe 100644 --- a/src/pip/_internal/req/req_tracker.py +++ b/src/pip/_internal/req/req_tracker.py @@ -95,8 +95,8 @@ def add(self, req): """Add an InstallRequirement to build tracking. """ - # Get the file to write information about this requirement. assert req.link + # Get the file to write information about this requirement. entry_path = self._entry_path(req.link) # Try reading from the file. If it exists and can be read from, a build From ac39efa53702cf2856b82d577642a8a70274c986 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Fri, 22 May 2020 03:58:47 +0530 Subject: [PATCH 1897/3170] Update parse_editable to return Set[str] --- src/pip/_internal/req/constructors.py | 12 ++++++------ tests/unit/test_req.py | 8 ++++---- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index f0d365c027b..7ca3370110f 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -75,7 +75,7 @@ def convert_extras(extras): def parse_editable(editable_req): - # type: (str) -> Tuple[Optional[str], str, Optional[Set[str]]] + # type: (str) -> Tuple[Optional[str], str, Set[str]] """Parses an editable requirement into: - a requirement name - an URL @@ -117,7 +117,7 @@ def parse_editable(editable_req): Requirement("placeholder" + extras.lower()).extras, ) else: - return package_name, url_no_extras, None + return package_name, url_no_extras, set() for version_control in vcs: if url.lower().startswith('{}:'.format(version_control)): @@ -146,7 +146,7 @@ def parse_editable(editable_req): "Could not detect requirement name for '{}', please specify one " "with #egg=your_package_name".format(editable_req) ) - return package_name, url, None + return package_name, url, set() def deduce_helpful_msg(req): @@ -185,7 +185,7 @@ def __init__( requirement, # type: Optional[Requirement] link, # type: Optional[Link] markers, # type: Optional[Marker] - extras, # type: Optional[Set[str]] + extras, # type: Set[str] ): self.requirement = requirement self.link = link @@ -236,7 +236,7 @@ def install_req_from_editable( install_options=options.get("install_options", []) if options else [], global_options=options.get("global_options", []) if options else [], hash_options=options.get("hashes", {}) if options else {}, - extras=parts.extras if parts.extras else set(), + extras=parts.extras, ) @@ -399,7 +399,7 @@ def install_req_from_line( global_options=options.get("global_options", []) if options else [], hash_options=options.get("hashes", {}) if options else {}, constraint=constraint, - extras=parts.extras if parts.extras else set(), + extras=parts.extras, ) diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index 73733d46235..2da0b62dbfc 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -583,10 +583,10 @@ def test_parse_editable_local( exists_mock.return_value = isdir_mock.return_value = True # mocks needed to support path operations on windows tests abspath_mock.return_value = "/some/path" - assert parse_editable('.') == (None, 'file:///some/path', None) + assert parse_editable('.') == (None, 'file:///some/path', set()) abspath_mock.return_value = "/some/path/foo" assert parse_editable('foo') == ( - None, 'file:///some/path/foo', None, + None, 'file:///some/path/foo', set(), ) @@ -594,7 +594,7 @@ def test_parse_editable_explicit_vcs(): assert parse_editable('svn+https://foo#egg=foo') == ( 'foo', 'svn+https://foo#egg=foo', - None, + set(), ) @@ -602,7 +602,7 @@ def test_parse_editable_vcs_extras(): assert parse_editable('svn+https://foo#egg=foo[extras]') == ( 'foo[extras]', 'svn+https://foo#egg=foo[extras]', - None, + set(), ) From f6bf9a0065e847a657648ab88d5af4b57081867e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Mu=C3=B1oz?= <hi@xmunoz.com> Date: Wed, 20 Nov 2019 16:54:17 -0800 Subject: [PATCH 1898/3170] Restrict dynamic attribute creation with slots Fixes #7313. --- src/pip/_internal/models/candidate.py | 2 ++ src/pip/_internal/models/format_control.py | 13 +++++++++++-- src/pip/_internal/models/index.py | 5 ++++- src/pip/_internal/models/link.py | 8 ++++++++ src/pip/_internal/models/scheme.py | 6 +++++- src/pip/_internal/models/search_scope.py | 5 ++++- src/pip/_internal/models/selection_prefs.py | 6 ++++-- src/pip/_internal/models/target_python.py | 13 ++++++++++++- src/pip/_internal/utils/models.py | 6 +++++- 9 files changed, 55 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/models/candidate.py b/src/pip/_internal/models/candidate.py index 1dc1a576eea..9149e0fc69c 100644 --- a/src/pip/_internal/models/candidate.py +++ b/src/pip/_internal/models/candidate.py @@ -12,6 +12,8 @@ class InstallationCandidate(KeyBasedCompareMixin): """Represents a potential "candidate" for installation. """ + __slots__ = ["name", "version", "link"] + def __init__(self, name, version, link): # type: (str, str, Link) -> None self.name = name diff --git a/src/pip/_internal/models/format_control.py b/src/pip/_internal/models/format_control.py index c39b84a84b5..ae7d02c1f1b 100644 --- a/src/pip/_internal/models/format_control.py +++ b/src/pip/_internal/models/format_control.py @@ -1,16 +1,20 @@ +import operator from pip._vendor.packaging.utils import canonicalize_name from pip._internal.exceptions import CommandError from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from pip._internal.utils.models import Base if MYPY_CHECK_RUNNING: from typing import Optional, Set, FrozenSet -class FormatControl(object): +class FormatControl(Base): """Helper for managing formats from which a package can be installed. """ + __slots__ = ["no_binary", "only_binary"] + def __init__(self, no_binary=None, only_binary=None): # type: (Optional[Set[str]], Optional[Set[str]]) -> None if no_binary is None: @@ -23,7 +27,12 @@ def __init__(self, no_binary=None, only_binary=None): def __eq__(self, other): # type: (object) -> bool - return self.__dict__ == other.__dict__ + if isinstance(other, self.__class__): + if self.__slots__ == other.__slots__: + attr_getters = [operator.attrgetter(attr) for attr in self.__slots__] + return all(getter(self) == getter(other) for getter in attr_getters) + + return False def __ne__(self, other): # type: (object) -> bool diff --git a/src/pip/_internal/models/index.py b/src/pip/_internal/models/index.py index ead1efbda76..f7772ea39cb 100644 --- a/src/pip/_internal/models/index.py +++ b/src/pip/_internal/models/index.py @@ -1,10 +1,13 @@ from pip._vendor.six.moves.urllib import parse as urllib_parse +from pip._internal.utils.models import Base -class PackageIndex(object): +class PackageIndex(Base): """Represents a Package Index and provides easier access to endpoints """ + __slots__ = ["url", "netloc", "simple_url", "pypi_url", "file_storage_domain"] + def __init__(self, url, file_storage_domain): # type: (str, str) -> None super(PackageIndex, self).__init__() diff --git a/src/pip/_internal/models/link.py b/src/pip/_internal/models/link.py index df4f8f01685..a54f37d2fcf 100644 --- a/src/pip/_internal/models/link.py +++ b/src/pip/_internal/models/link.py @@ -24,6 +24,14 @@ class Link(KeyBasedCompareMixin): """Represents a parsed link from a Package Index's simple URL """ + __slots__ = [ + "_parsed_url", + "_url", + "comes_from", + "requires_python", + "yanked_reason", + ] + def __init__( self, url, # type: str diff --git a/src/pip/_internal/models/scheme.py b/src/pip/_internal/models/scheme.py index af07b4078f9..bda56648f8d 100644 --- a/src/pip/_internal/models/scheme.py +++ b/src/pip/_internal/models/scheme.py @@ -5,11 +5,15 @@ https://docs.python.org/3/install/index.html#alternate-installation. """ +from pip._internal.utils.models import Base -class Scheme(object): +class Scheme(Base): """A Scheme holds paths which are used as the base directories for artifacts associated with a Python package. """ + + __slots__ = ['platlib', 'purelib', 'headers', 'scripts', 'data'] + def __init__( self, platlib, # type: str diff --git a/src/pip/_internal/models/search_scope.py b/src/pip/_internal/models/search_scope.py index 7a0008e4825..b0b93c09b44 100644 --- a/src/pip/_internal/models/search_scope.py +++ b/src/pip/_internal/models/search_scope.py @@ -9,6 +9,7 @@ from pip._internal.models.index import PyPI from pip._internal.utils.compat import has_tls from pip._internal.utils.misc import normalize_path, redact_auth_from_url +from pip._internal.utils.models import Base from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: @@ -18,12 +19,14 @@ logger = logging.getLogger(__name__) -class SearchScope(object): +class SearchScope(Base): """ Encapsulates the locations that pip is configured to search. """ + __slots__ = ["find_links", "index_urls"] + @classmethod def create( cls, diff --git a/src/pip/_internal/models/selection_prefs.py b/src/pip/_internal/models/selection_prefs.py index f58fdce9cdf..b45af52a3cd 100644 --- a/src/pip/_internal/models/selection_prefs.py +++ b/src/pip/_internal/models/selection_prefs.py @@ -1,3 +1,4 @@ +from pip._internal.utils.models import Base from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: @@ -5,13 +6,14 @@ from pip._internal.models.format_control import FormatControl -class SelectionPreferences(object): - +class SelectionPreferences(Base): """ Encapsulates the candidate selection preferences for downloading and installing files. """ + __slots__ = ['allow_yanked', 'allow_all_prereleases', 'format_control', 'prefer_binary', 'ignore_requires_python'] + # Don't include an allow_yanked default value to make sure each call # site considers whether yanked releases are allowed. This also causes # that decision to be made explicit in the calling code, which helps diff --git a/src/pip/_internal/models/target_python.py b/src/pip/_internal/models/target_python.py index 84f1c209c66..901504d39a5 100644 --- a/src/pip/_internal/models/target_python.py +++ b/src/pip/_internal/models/target_python.py @@ -5,6 +5,7 @@ version_info_to_nodot, ) from pip._internal.utils.misc import normalize_version_info +from pip._internal.utils.models import Base from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: @@ -13,13 +14,23 @@ from pip._vendor.packaging.tags import Tag -class TargetPython(object): +class TargetPython(Base): """ Encapsulates the properties of a Python interpreter one is targeting for a package install, download, etc. """ + __slots__ = [ + "_given_py_version_info", + "abi", + "implementation", + "platform", + "py_version", + "py_version_info", + "_valid_tags", + ] + def __init__( self, platform=None, # type: Optional[str] diff --git a/src/pip/_internal/utils/models.py b/src/pip/_internal/utils/models.py index 29e1441153b..ac6f24797b2 100644 --- a/src/pip/_internal/utils/models.py +++ b/src/pip/_internal/utils/models.py @@ -6,9 +6,13 @@ import operator -class KeyBasedCompareMixin(object): +class Base(object): + __slots__ = [] + +class KeyBasedCompareMixin(Base): """Provides comparison capabilities that is based on a key """ + __slots__ = ['_compare_key', '_defining_class'] def __init__(self, key, defining_class): self._compare_key = key From 0527f8053147971623b2f55626d51c49c3a85a64 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Mu=C3=B1oz?= <hi@xmunoz.com> Date: Wed, 20 Nov 2019 17:37:21 -0800 Subject: [PATCH 1899/3170] Clean up lint errors --- src/pip/_internal/models/format_control.py | 8 +++++--- src/pip/_internal/models/index.py | 4 +++- src/pip/_internal/models/scheme.py | 1 + src/pip/_internal/models/selection_prefs.py | 3 ++- src/pip/_internal/utils/models.py | 4 +++- 5 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/models/format_control.py b/src/pip/_internal/models/format_control.py index ae7d02c1f1b..d66b5e4703a 100644 --- a/src/pip/_internal/models/format_control.py +++ b/src/pip/_internal/models/format_control.py @@ -2,8 +2,8 @@ from pip._vendor.packaging.utils import canonicalize_name from pip._internal.exceptions import CommandError -from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.models import Base +from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: from typing import Optional, Set, FrozenSet @@ -29,8 +29,10 @@ def __eq__(self, other): # type: (object) -> bool if isinstance(other, self.__class__): if self.__slots__ == other.__slots__: - attr_getters = [operator.attrgetter(attr) for attr in self.__slots__] - return all(getter(self) == getter(other) for getter in attr_getters) + attr_getters = [operator.attrgetter(attr) + for attr in self.__slots__] + return all(getter(self) == getter(other) + for getter in attr_getters) return False diff --git a/src/pip/_internal/models/index.py b/src/pip/_internal/models/index.py index f7772ea39cb..28f5bf22503 100644 --- a/src/pip/_internal/models/index.py +++ b/src/pip/_internal/models/index.py @@ -1,4 +1,5 @@ from pip._vendor.six.moves.urllib import parse as urllib_parse + from pip._internal.utils.models import Base @@ -6,7 +7,8 @@ class PackageIndex(Base): """Represents a Package Index and provides easier access to endpoints """ - __slots__ = ["url", "netloc", "simple_url", "pypi_url", "file_storage_domain"] + __slots__ = ['url', 'netloc', 'simple_url', 'pypi_url', + 'file_storage_domain'] def __init__(self, url, file_storage_domain): # type: (str, str) -> None diff --git a/src/pip/_internal/models/scheme.py b/src/pip/_internal/models/scheme.py index bda56648f8d..5e658481a1c 100644 --- a/src/pip/_internal/models/scheme.py +++ b/src/pip/_internal/models/scheme.py @@ -7,6 +7,7 @@ from pip._internal.utils.models import Base + class Scheme(Base): """A Scheme holds paths which are used as the base directories for artifacts associated with a Python package. diff --git a/src/pip/_internal/models/selection_prefs.py b/src/pip/_internal/models/selection_prefs.py index b45af52a3cd..4b334652d9a 100644 --- a/src/pip/_internal/models/selection_prefs.py +++ b/src/pip/_internal/models/selection_prefs.py @@ -12,7 +12,8 @@ class SelectionPreferences(Base): and installing files. """ - __slots__ = ['allow_yanked', 'allow_all_prereleases', 'format_control', 'prefer_binary', 'ignore_requires_python'] + __slots__ = ['allow_yanked', 'allow_all_prereleases', 'format_control', + 'prefer_binary', 'ignore_requires_python'] # Don't include an allow_yanked default value to make sure each call # site considers whether yanked releases are allowed. This also causes diff --git a/src/pip/_internal/utils/models.py b/src/pip/_internal/utils/models.py index ac6f24797b2..2cddf669c3c 100644 --- a/src/pip/_internal/utils/models.py +++ b/src/pip/_internal/utils/models.py @@ -4,10 +4,12 @@ # mypy: disallow-untyped-defs=False import operator +from typing import List class Base(object): - __slots__ = [] + __slots__ = [] # type: List[str] + class KeyBasedCompareMixin(Base): """Provides comparison capabilities that is based on a key From bcc5ac404ce3de314591e9cd93d3ff7d984dfedc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Mu=C3=B1oz?= <hi@xmunoz.com> Date: Wed, 20 Nov 2019 17:47:11 -0800 Subject: [PATCH 1900/3170] Conditionally import List for Python 2 compatibility --- src/pip/_internal/utils/models.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/utils/models.py b/src/pip/_internal/utils/models.py index 2cddf669c3c..4dd50e7959f 100644 --- a/src/pip/_internal/utils/models.py +++ b/src/pip/_internal/utils/models.py @@ -4,7 +4,11 @@ # mypy: disallow-untyped-defs=False import operator -from typing import List + +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import List class Base(object): From 3c1ad453702e3ac22e17fd597deaf8ad2c02a41c Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Fri, 22 May 2020 03:44:57 +0530 Subject: [PATCH 1901/3170] Type annotations for pip._internal.exceptions --- src/pip/_internal/exceptions.py | 41 +++++++++++++++++++++++++-------- 1 file changed, 31 insertions(+), 10 deletions(-) diff --git a/src/pip/_internal/exceptions.py b/src/pip/_internal/exceptions.py index 8ac85485e17..c92b8cecd59 100644 --- a/src/pip/_internal/exceptions.py +++ b/src/pip/_internal/exceptions.py @@ -1,8 +1,5 @@ """Exceptions used throughout package""" -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - from __future__ import absolute_import from itertools import chain, groupby, repeat @@ -12,9 +9,14 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Optional + from typing import Optional, List, Dict from pip._vendor.pkg_resources import Distribution from pip._internal.req.req_install import InstallRequirement + from pip._vendor.six import PY3 + if PY3: + from hashlib import _Hash + else: + from hashlib import _hash as _Hash class PipError(Exception): @@ -100,12 +102,15 @@ class HashErrors(InstallationError): """Multiple HashError instances rolled into one for reporting""" def __init__(self): - self.errors = [] + # type: () -> None + self.errors = [] # type: List[HashError] def append(self, error): + # type: (HashError) -> None self.errors.append(error) def __str__(self): + # type: () -> str lines = [] self.errors.sort(key=lambda e: e.order) for cls, errors_of_cls in groupby(self.errors, lambda e: e.__class__): @@ -113,11 +118,14 @@ def __str__(self): lines.extend(e.body() for e in errors_of_cls) if lines: return '\n'.join(lines) + return '' def __nonzero__(self): + # type: () -> bool return bool(self.errors) def __bool__(self): + # type: () -> bool return self.__nonzero__() @@ -139,8 +147,10 @@ class HashError(InstallationError): """ req = None # type: Optional[InstallRequirement] head = '' + order = None # type: Optional[int] def body(self): + # type: () -> str """Return a summary of me for display under the heading. This default implementation simply prints a description of the @@ -153,9 +163,11 @@ def body(self): return ' {}'.format(self._requirement_name()) def __str__(self): + # type: () -> str return '{}\n{}'.format(self.head, self.body()) def _requirement_name(self): + # type: () -> str """Return a description of the requirement that triggered me. This default implementation returns long description of the req, with @@ -196,13 +208,15 @@ class HashMissing(HashError): 'has a hash.)') def __init__(self, gotten_hash): + # type: (str) -> None """ :param gotten_hash: The hash of the (possibly malicious) archive we just downloaded """ - self.gotten_hash = gotten_hash + self.gotten_hash = gotten_hash # type: str def body(self): + # type: () -> str # Dodge circular import. from pip._internal.utils.hashes import FAVORITE_HASH @@ -245,20 +259,23 @@ class HashMismatch(HashError): 'someone may have tampered with them.') def __init__(self, allowed, gots): + # type: (Dict[str, List[str]], Dict[str, _Hash]) -> None """ :param allowed: A dict of algorithm names pointing to lists of allowed hex digests :param gots: A dict of algorithm names pointing to hashes we actually got from the files under suspicion """ - self.allowed = allowed - self.gots = gots + self.allowed = allowed # type: Dict[str, List[str]] + self.gots = gots # type: Dict[str, _Hash] def body(self): + # type: () -> str return ' {}:\n{}'.format(self._requirement_name(), self._hash_comparison()) def _hash_comparison(self): + # type: () -> str """ Return a comparison of actual and expected hash values. @@ -270,11 +287,12 @@ def _hash_comparison(self): """ def hash_then_or(hash_name): + # type: (str) -> chain[str] # For now, all the decent hashes have 6-char names, so we can get # away with hard-coding space literals. return chain([hash_name], repeat(' or')) - lines = [] + lines = [] # type: List[str] for hash_name, expecteds in iteritems(self.allowed): prefix = hash_then_or(hash_name) lines.extend((' Expected {} {}'.format(next(prefix), e)) @@ -294,15 +312,18 @@ class ConfigurationFileCouldNotBeLoaded(ConfigurationError): """ def __init__(self, reason="could not be loaded", fname=None, error=None): + # type: (str, Optional[str], Optional[Exception]) -> None super(ConfigurationFileCouldNotBeLoaded, self).__init__(error) self.reason = reason self.fname = fname self.error = error def __str__(self): + # type: () -> str if self.fname is not None: message_part = " in {}.".format(self.fname) else: assert self.error is not None - message_part = ".\n{}\n".format(self.error.message) + # message attribute for Exception is only available for Python 2 + message_part = ".\n{}\n".format(self.error.message) # type: ignore return "Configuration file {}{}".format(self.reason, message_part) From 19a613efc5616492894ab771ab0a950fa685f7d8 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Fri, 22 May 2020 09:00:19 +0100 Subject: [PATCH 1902/3170] Add a travis job to test the new resolver known failures --- .travis.yml | 3 +++ tools/travis/run.sh | 4 ++++ 2 files changed, 7 insertions(+) diff --git a/.travis.yml b/.travis.yml index 02c71a243fb..4bb82f4e652 100644 --- a/.travis.yml +++ b/.travis.yml @@ -61,6 +61,9 @@ jobs: - env: - GROUP=2 - NEW_RESOLVER=1 + - env: + - GROUP=3 + - NEW_RESOLVER=1 fast_finish: true allow_failures: diff --git a/tools/travis/run.sh b/tools/travis/run.sh index 90e7d570860..a531cbb56fd 100755 --- a/tools/travis/run.sh +++ b/tools/travis/run.sh @@ -55,6 +55,10 @@ elif [[ "$GROUP" == "2" ]]; then # Separate Job for running integration tests for 'pip install' tox -- -m integration -n auto --duration=5 -k "test_install" \ --use-venv $RESOLVER_SWITCH +elif [[ "$GROUP" == "3" ]]; then + # Separate Job for tests that fail with the new resolver + tox -- -m fails_on_new_resolver -n auto --duration=5 \ + --use-venv $RESOLVER_SWITCH --new-resolver-runtests else # Non-Testing Jobs should run once tox From e85a0995113b53a9f173e21cb4573d4a3fcab9c3 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 22 May 2020 17:16:43 +0800 Subject: [PATCH 1903/3170] Remove test_install_include_work_dir_pkg --- tests/functional/test_install.py | 26 -------------------------- 1 file changed, 26 deletions(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 0543f9e4cef..5ae1495cf7d 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1878,29 +1878,3 @@ def test_install_skip_work_dir_pkg(script, data): assert 'Requirement already satisfied: simple' not in result.stdout assert 'Successfully installed simple' in result.stdout - - -@pytest.mark.fails_on_new_resolver -def test_install_include_work_dir_pkg(script, data): - """ - Test that install of a package in working directory - should fail on the second attempt after an install - if working directory is added in PYTHONPATH - """ - - # Create a test package, install it and then uninstall it - pkg_path = create_test_package_with_setup( - script, name='simple', version='1.0') - script.pip('install', '-e', '.', - expect_stderr=True, cwd=pkg_path) - script.pip('uninstall', 'simple', '-y') - - script.environ.update({'PYTHONPATH': pkg_path}) - - # Running the install command again from the working directory - # will be a no-op, as the package is found to be installed, - # when the package directory is in PYTHONPATH - result = script.pip('install', '--find-links', - data.find_links, 'simple', - expect_stderr=True, cwd=pkg_path) - assert 'Requirement already satisfied: simple' in result.stdout From a9c8dc5468a6dd39e20fa12ee21718b673189c38 Mon Sep 17 00:00:00 2001 From: cjc7373 <niuchangcun@163.com> Date: Thu, 16 Apr 2020 20:59:03 +0800 Subject: [PATCH 1904/3170] fix normalize path for Windows --- news/7625.bugfix | 1 + src/pip/_internal/operations/install/wheel.py | 6 +++++- 2 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 news/7625.bugfix diff --git a/news/7625.bugfix b/news/7625.bugfix new file mode 100644 index 00000000000..3a675f8d2b0 --- /dev/null +++ b/news/7625.bugfix @@ -0,0 +1 @@ +Fix normalizing path on Windows when installing package on another logical disk. diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 3f96c487a89..36877ca5e76 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -262,7 +262,11 @@ def _record_to_fs_path(record_path): def _fs_to_record_path(path, relative_to=None): # type: (text_type, Optional[text_type]) -> RecordPath if relative_to is not None: - path = os.path.relpath(path, relative_to) + # On Windows, do not handle relative paths if they belong to different + # logical disks + if os.path.splitdrive(path)[0].lower() == \ + os.path.splitdrive(relative_to)[0].lower(): + path = os.path.relpath(path, relative_to) path = path.replace(os.path.sep, '/') return cast('RecordPath', path) From 7e4280ad9536d3b556e4973855434eebd21d29fc Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Fri, 22 May 2020 10:16:12 +0530 Subject: [PATCH 1905/3170] Add news file --- news/80A30837-433E-45F5-9177-FAB3447802EE.trivial | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 news/80A30837-433E-45F5-9177-FAB3447802EE.trivial diff --git a/news/80A30837-433E-45F5-9177-FAB3447802EE.trivial b/news/80A30837-433E-45F5-9177-FAB3447802EE.trivial new file mode 100644 index 00000000000..e69de29bb2d From 52c48a49abed6408c74bef096d58db3300f97fa9 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 22 May 2020 17:10:50 +0800 Subject: [PATCH 1906/3170] Account for package casing in new resolver --- tests/functional/test_install_config.py | 18 ++++++------------ 1 file changed, 6 insertions(+), 12 deletions(-) diff --git a/tests/functional/test_install_config.py b/tests/functional/test_install_config.py index 712fc4ae5c7..62c88be1f6c 100644 --- a/tests/functional/test_install_config.py +++ b/tests/functional/test_install_config.py @@ -7,7 +7,6 @@ from tests.lib.server import file_response, package_page -@pytest.mark.fails_on_new_resolver def test_options_from_env_vars(script): """ Test if ConfigOptionParser reads env vars (e.g. not using PyPI here) @@ -16,10 +15,9 @@ def test_options_from_env_vars(script): script.environ['PIP_NO_INDEX'] = '1' result = script.pip('install', '-vvv', 'INITools', expect_error=True) assert "Ignoring indexes:" in result.stdout, str(result) - assert ( - "DistributionNotFound: No matching distribution found for INITools" - in result.stdout - ) + + err = "distributionnotfound: no matching distribution found for initools" + assert err in result.stdout.lower() def test_command_line_options_override_env_vars(script, virtualenv): @@ -44,7 +42,6 @@ def test_command_line_options_override_env_vars(script, virtualenv): @pytest.mark.network -@pytest.mark.fails_on_new_resolver def test_env_vars_override_config_file(script, virtualenv): """ Test that environmental variables override settings in config files. @@ -68,7 +65,7 @@ def test_env_vars_override_config_file(script, virtualenv): script.environ['PIP_NO_INDEX'] = '0' virtualenv.clear() result = script.pip('install', '-vvv', 'INITools') - assert "Successfully installed INITools" in result.stdout + assert "successfully installed initools" in result.stdout.lower() @pytest.mark.network @@ -176,7 +173,6 @@ def test_config_file_override_stack( assert requests[3]["PATH_INFO"] == "/files/INITools-0.2.tar.gz" -@pytest.mark.fails_on_new_resolver def test_options_from_venv_config(script, virtualenv): """ Test if ConfigOptionParser reads a virtualenv-local config file @@ -189,10 +185,8 @@ def test_options_from_venv_config(script, virtualenv): f.write(conf) result = script.pip('install', '-vvv', 'INITools', expect_error=True) assert "Ignoring indexes:" in result.stdout, str(result) - assert ( - "DistributionNotFound: No matching distribution found for INITools" - in result.stdout - ) + err = "distributionnotfound: no matching distribution found for initools" + assert err in result.stdout.lower() def test_install_no_binary_via_config_disables_cached_wheels( From 25521518589b3dabda24c0a3da2031b22bbf2f41 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Fri, 22 May 2020 17:27:44 +0530 Subject: [PATCH 1907/3170] Add helpers to TestPipResult --- tests/lib/__init__.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index 9416534086d..d08e1f3613f 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -353,6 +353,27 @@ def assert_installed(self, pkg_name, editable=True, with_files=[], .format(**locals()) ) + def did_create(self, path, message=None): + assert str(path) in self.files_created, _one_or_both(message, self) + + def did_not_create(self, path, message=None): + assert str(path) not in self.files_created, _one_or_both(message, self) + + def did_update(self, path, message=None): + assert str(path) in self.files_updated, _one_or_both(message, self) + + def did_not_update(self, path, message=None): + assert str(path) not in self.files_updated, _one_or_both(message, self) + + +def _one_or_both(a, b): + """Returns f"{a}\n{b}" if a is truthy, else returns str(b). + """ + if not a: + return str(b) + + return "{a}\n{b}".format(a=a, b=b) + def make_check_stderr_message(stderr, line, reason): """ From 16d45b607e40067b9fce10a44a16ea9eb407bbb3 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Fri, 22 May 2020 14:10:41 +0100 Subject: [PATCH 1908/3170] Revert "Account for package casing in new resolver" This reverts commit 52c48a49abed6408c74bef096d58db3300f97fa9. --- tests/functional/test_install_config.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/tests/functional/test_install_config.py b/tests/functional/test_install_config.py index 62c88be1f6c..712fc4ae5c7 100644 --- a/tests/functional/test_install_config.py +++ b/tests/functional/test_install_config.py @@ -7,6 +7,7 @@ from tests.lib.server import file_response, package_page +@pytest.mark.fails_on_new_resolver def test_options_from_env_vars(script): """ Test if ConfigOptionParser reads env vars (e.g. not using PyPI here) @@ -15,9 +16,10 @@ def test_options_from_env_vars(script): script.environ['PIP_NO_INDEX'] = '1' result = script.pip('install', '-vvv', 'INITools', expect_error=True) assert "Ignoring indexes:" in result.stdout, str(result) - - err = "distributionnotfound: no matching distribution found for initools" - assert err in result.stdout.lower() + assert ( + "DistributionNotFound: No matching distribution found for INITools" + in result.stdout + ) def test_command_line_options_override_env_vars(script, virtualenv): @@ -42,6 +44,7 @@ def test_command_line_options_override_env_vars(script, virtualenv): @pytest.mark.network +@pytest.mark.fails_on_new_resolver def test_env_vars_override_config_file(script, virtualenv): """ Test that environmental variables override settings in config files. @@ -65,7 +68,7 @@ def test_env_vars_override_config_file(script, virtualenv): script.environ['PIP_NO_INDEX'] = '0' virtualenv.clear() result = script.pip('install', '-vvv', 'INITools') - assert "successfully installed initools" in result.stdout.lower() + assert "Successfully installed INITools" in result.stdout @pytest.mark.network @@ -173,6 +176,7 @@ def test_config_file_override_stack( assert requests[3]["PATH_INFO"] == "/files/INITools-0.2.tar.gz" +@pytest.mark.fails_on_new_resolver def test_options_from_venv_config(script, virtualenv): """ Test if ConfigOptionParser reads a virtualenv-local config file @@ -185,8 +189,10 @@ def test_options_from_venv_config(script, virtualenv): f.write(conf) result = script.pip('install', '-vvv', 'INITools', expect_error=True) assert "Ignoring indexes:" in result.stdout, str(result) - err = "distributionnotfound: no matching distribution found for initools" - assert err in result.stdout.lower() + assert ( + "DistributionNotFound: No matching distribution found for INITools" + in result.stdout + ) def test_install_no_binary_via_config_disables_cached_wheels( From e8aa5e73c556b1b2e56d0f5485edeb4a535748e0 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Thu, 21 May 2020 22:49:58 +0530 Subject: [PATCH 1909/3170] Fix return type annotation for handle_401 --- src/pip/_internal/network/auth.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/network/auth.py b/src/pip/_internal/network/auth.py index 19c75aa9dfa..ca729fcdf5e 100644 --- a/src/pip/_internal/network/auth.py +++ b/src/pip/_internal/network/auth.py @@ -23,7 +23,9 @@ from typing import Dict, Optional, Tuple, List, Any from pip._internal.vcs.versioncontrol import AuthInfo + from pip._vendor.requests.models import Response, Request + Credentials = Tuple[str, str, str] logger = logging.getLogger(__name__) @@ -180,7 +182,7 @@ def _get_url_and_credentials(self, original_url): username, password = self._get_new_credentials(original_url) if username is not None or password is not None: - # Convert the username and password if they 're None, so that + # Convert the username and password if they're None, so that # this netloc will show up as "cached" in the conditional above. # Further, HTTPBasicAuth doesn't accept None, so it makes sense to # cache the value that is going to be used. @@ -236,7 +238,7 @@ def _should_save_password_to_keyring(self): return ask("Save credentials to keyring [y/N]: ", ["y", "n"]) == "y" def handle_401(self, resp, **kwargs): - # type: (Response, **Any) -> None + # type: (Response, **Any) -> Response # We only care about 401 responses, anything else we want to just # pass through the actual response if resp.status_code != 401: From e8b842389c94111f510d372ca834a6d4a123ae0b Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Fri, 22 May 2020 19:44:29 +0530 Subject: [PATCH 1910/3170] Always return SUCCESS from download.run --- src/pip/_internal/commands/download.py | 5 ++--- tests/functional/test_download.py | 4 +--- 2 files changed, 3 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index 01ebf6a5570..e48a7838c9f 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -6,7 +6,7 @@ from pip._internal.cli import cmdoptions from pip._internal.cli.cmdoptions import make_target_python from pip._internal.cli.req_command import RequirementCommand, with_cleanup -from pip._internal.cli.status_codes import ERROR, SUCCESS +from pip._internal.cli.status_codes import SUCCESS from pip._internal.req.req_tracker import get_requirement_tracker from pip._internal.utils.misc import ensure_dir, normalize_path, write_output from pip._internal.utils.temp_dir import TempDirectory @@ -143,6 +143,5 @@ def run(self, options, args): if req.successfully_downloaded]) if downloaded: write_output('Successfully downloaded %s', downloaded) - return SUCCESS - return ERROR + return SUCCESS diff --git a/tests/functional/test_download.py b/tests/functional/test_download.py index 2b6ff8e9dca..05b97ab3aec 100644 --- a/tests/functional/test_download.py +++ b/tests/functional/test_download.py @@ -666,9 +666,7 @@ def test_download_exit_status_code_when_blank_requirements_file(script): Test download exit status code when blank requirements file specified """ script.scratch_path.joinpath("blank.txt").write_text("\n") - result = script.pip('download', '-r', 'blank.txt', expect_error=True) - print(result) - assert result.returncode == ERROR + script.pip('download', '-r', 'blank.txt') def test_download_prefer_binary_when_tarball_higher_than_wheel(script, data): From 48334aeb80f574c374362120d9c084cbc8d2c7bf Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Fri, 22 May 2020 15:36:10 +0100 Subject: [PATCH 1911/3170] New resolver not raising DistributionNotFound --- .../resolution/resolvelib/resolver.py | 5 ++- tests/functional/test_install_config.py | 31 ++++++++++--------- 2 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index cecc70661d1..9eab87b3a72 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -6,7 +6,7 @@ from pip._vendor.resolvelib import BaseReporter, ResolutionImpossible from pip._vendor.resolvelib import Resolver as RLResolver -from pip._internal.exceptions import InstallationError +from pip._internal.exceptions import DistributionNotFound, InstallationError from pip._internal.req.req_set import RequirementSet from pip._internal.resolution.base import BaseResolver from pip._internal.resolution.resolvelib.provider import PipProvider @@ -155,11 +155,10 @@ def resolve(self, root_reqs, check_supported_wheels): parent.name )) ) - raise InstallationError( + raise DistributionNotFound( "No matching distribution found for " + ", ".join([r.name for r, _ in e.causes]) ) - raise six.raise_from(error, e) req_set = RequirementSet(check_supported_wheels=check_supported_wheels) diff --git a/tests/functional/test_install_config.py b/tests/functional/test_install_config.py index 712fc4ae5c7..1a8a36116df 100644 --- a/tests/functional/test_install_config.py +++ b/tests/functional/test_install_config.py @@ -1,4 +1,5 @@ import os +import re import tempfile import textwrap @@ -7,7 +8,6 @@ from tests.lib.server import file_response, package_page -@pytest.mark.fails_on_new_resolver def test_options_from_env_vars(script): """ Test if ConfigOptionParser reads env vars (e.g. not using PyPI here) @@ -16,10 +16,11 @@ def test_options_from_env_vars(script): script.environ['PIP_NO_INDEX'] = '1' result = script.pip('install', '-vvv', 'INITools', expect_error=True) assert "Ignoring indexes:" in result.stdout, str(result) - assert ( - "DistributionNotFound: No matching distribution found for INITools" - in result.stdout - ) + assert re.search( + "DistributionNotFound: No matching distribution found for " + "(?i:INITools)", + result.stdout + ), str(result) def test_command_line_options_override_env_vars(script, virtualenv): @@ -44,7 +45,6 @@ def test_command_line_options_override_env_vars(script, virtualenv): @pytest.mark.network -@pytest.mark.fails_on_new_resolver def test_env_vars_override_config_file(script, virtualenv): """ Test that environmental variables override settings in config files. @@ -61,10 +61,11 @@ def test_env_vars_override_config_file(script, virtualenv): no-index = 1 """)) result = script.pip('install', '-vvv', 'INITools', expect_error=True) - assert ( - "DistributionNotFound: No matching distribution found for INITools" - in result.stdout - ) + assert re.search( + "DistributionNotFound: No matching distribution found for " + "(?i:INITools)", + result.stdout + ), str(result) script.environ['PIP_NO_INDEX'] = '0' virtualenv.clear() result = script.pip('install', '-vvv', 'INITools') @@ -176,7 +177,6 @@ def test_config_file_override_stack( assert requests[3]["PATH_INFO"] == "/files/INITools-0.2.tar.gz" -@pytest.mark.fails_on_new_resolver def test_options_from_venv_config(script, virtualenv): """ Test if ConfigOptionParser reads a virtualenv-local config file @@ -189,10 +189,11 @@ def test_options_from_venv_config(script, virtualenv): f.write(conf) result = script.pip('install', '-vvv', 'INITools', expect_error=True) assert "Ignoring indexes:" in result.stdout, str(result) - assert ( - "DistributionNotFound: No matching distribution found for INITools" - in result.stdout - ) + assert re.search( + "DistributionNotFound: No matching distribution found for " + "(?i:INITools)", + result.stdout + ), str(result) def test_install_no_binary_via_config_disables_cached_wheels( From 7fa571e07663dd4f849561426188327ffd8901c4 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Fri, 22 May 2020 16:07:29 +0100 Subject: [PATCH 1912/3170] Fix test_double_install_fail for new resolver --- tests/functional/test_install.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 0543f9e4cef..2889882f0d6 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1494,15 +1494,21 @@ def test_double_install(script): assert msg not in result.stderr -@pytest.mark.fails_on_new_resolver -def test_double_install_fail(script): +def test_double_install_fail(script, use_new_resolver): """ Test double install failing with two different version requirements """ - result = script.pip('install', 'pip==*', 'pip==7.1.2', expect_error=True) - msg = ("Double requirement given: pip==7.1.2 (already in pip==*, " - "name='pip')") - assert msg in result.stderr + result = script.pip( + 'install', + 'pip==7.*', + 'pip==7.1.2', + # The new resolver is perfectly capable of handling this + expect_error=(not use_new_resolver) + ) + if not use_new_resolver: + msg = ("Double requirement given: pip==7.1.2 (already in pip==7.*, " + "name='pip')") + assert msg in result.stderr def _get_expected_error_text(): From c5e2cfc3f7a1543ad7e1947d2b8190c3d2754141 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Fri, 22 May 2020 16:15:24 +0100 Subject: [PATCH 1913/3170] Give up on trying to be clever --- tests/functional/test_install_config.py | 25 +++++++++---------------- 1 file changed, 9 insertions(+), 16 deletions(-) diff --git a/tests/functional/test_install_config.py b/tests/functional/test_install_config.py index 1a8a36116df..6cd283f077f 100644 --- a/tests/functional/test_install_config.py +++ b/tests/functional/test_install_config.py @@ -1,5 +1,4 @@ import os -import re import tempfile import textwrap @@ -16,11 +15,9 @@ def test_options_from_env_vars(script): script.environ['PIP_NO_INDEX'] = '1' result = script.pip('install', '-vvv', 'INITools', expect_error=True) assert "Ignoring indexes:" in result.stdout, str(result) - assert re.search( - "DistributionNotFound: No matching distribution found for " - "(?i:INITools)", - result.stdout - ), str(result) + msg = "DistributionNotFound: No matching distribution found for INITools" + # Case insensitive as the new resolver canonicalises the project name + assert msg.lower() in result.stdout.lower(), str(result) def test_command_line_options_override_env_vars(script, virtualenv): @@ -61,11 +58,9 @@ def test_env_vars_override_config_file(script, virtualenv): no-index = 1 """)) result = script.pip('install', '-vvv', 'INITools', expect_error=True) - assert re.search( - "DistributionNotFound: No matching distribution found for " - "(?i:INITools)", - result.stdout - ), str(result) + msg = "DistributionNotFound: No matching distribution found for INITools" + # Case insensitive as the new resolver canonicalises the project name + assert msg.lower() in result.stdout.lower(), str(result) script.environ['PIP_NO_INDEX'] = '0' virtualenv.clear() result = script.pip('install', '-vvv', 'INITools') @@ -189,11 +184,9 @@ def test_options_from_venv_config(script, virtualenv): f.write(conf) result = script.pip('install', '-vvv', 'INITools', expect_error=True) assert "Ignoring indexes:" in result.stdout, str(result) - assert re.search( - "DistributionNotFound: No matching distribution found for " - "(?i:INITools)", - result.stdout - ), str(result) + msg = "DistributionNotFound: No matching distribution found for INITools" + # Case insensitive as the new resolver canonicalises the project name + assert msg.lower() in result.stdout.lower(), str(result) def test_install_no_binary_via_config_disables_cached_wheels( From 15045f819606001181e39efb172c678e27daeb9e Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sat, 23 May 2020 05:39:23 +0530 Subject: [PATCH 1914/3170] Remove duplicate jobs from Travis CI --- .travis.yml | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/.travis.yml b/.travis.yml index 4bb82f4e652..8d84f958161 100644 --- a/.travis.yml +++ b/.travis.yml @@ -19,13 +19,6 @@ jobs: env: TOXENV=docs - env: TOXENV=lint - env: TOXENV=vendoring - # Latest CPython - - env: GROUP=1 - python: 2.7 - - env: GROUP=2 - python: 2.7 - - env: GROUP=1 - - env: GROUP=2 # Complete checking for ensuring compatibility # PyPy @@ -38,19 +31,6 @@ jobs: python: pypy2.7-7.1.1 - env: GROUP=2 python: pypy2.7-7.1.1 - # Other Supported CPython - - env: GROUP=1 - python: 3.7 - - env: GROUP=2 - python: 3.7 - - env: GROUP=1 - python: 3.6 - - env: GROUP=2 - python: 3.6 - - env: GROUP=1 - python: 3.5 - - env: GROUP=2 - python: 3.5 # Test experimental stuff that are not part of the standard pip usage. # Helpful for developers working on them to see how they're doing. From d1c68bae7cf2c9be0efeb6a58e3c1e0a72c5d50e Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sat, 23 May 2020 05:53:11 +0530 Subject: [PATCH 1915/3170] Skip integration tests for Py3.{5,6,7} on Windows CI --- .azure-pipelines/jobs/test-windows.yml | 18 +++++++++--------- docs/html/development/ci.rst | 6 +++--- 2 files changed, 12 insertions(+), 12 deletions(-) diff --git a/.azure-pipelines/jobs/test-windows.yml b/.azure-pipelines/jobs/test-windows.yml index 2d1166f4e37..08131b683c4 100644 --- a/.azure-pipelines/jobs/test-windows.yml +++ b/.azure-pipelines/jobs/test-windows.yml @@ -15,15 +15,6 @@ jobs: "2.7": python.version: '2.7' python.architecture: x64 - "3.5": - python.version: '3.5' - python.architecture: x64 - "3.6": - python.version: '3.6' - python.architecture: x64 - "3.7": - python.version: '3.7' - python.architecture: x64 "3.8": python.version: '3.8' python.architecture: x64 @@ -44,6 +35,15 @@ jobs: vmImage: ${{ parameters.vmImage }} strategy: matrix: + "3.5": + python.version: '3.5' + python.architecture: x64 + "3.6": + python.version: '3.6' + python.architecture: x64 + "3.7": + python.version: '3.7' + python.architecture: x64 # This is for Windows, so test x86 builds "3.5-x86": python.version: '3.5' diff --git a/docs/html/development/ci.rst b/docs/html/development/ci.rst index 8b6412307a9..d2654f1f67c 100644 --- a/docs/html/development/ci.rst +++ b/docs/html/development/ci.rst @@ -111,11 +111,11 @@ Actual testing | Windows +----------+-------+---------------+-----------------+ | | | CP2.7 | Azure | Azure | | | +-------+---------------+-----------------+ -| | | CP3.5 | Azure | Azure | +| | | CP3.5 | Azure | | | | +-------+---------------+-----------------+ -| | | CP3.6 | Azure | Azure | +| | | CP3.6 | Azure | | | | +-------+---------------+-----------------+ -| | x64 | CP3.7 | Azure | Azure | +| | x64 | CP3.7 | Azure | | | | +-------+---------------+-----------------+ | | | CP3.8 | Azure | Azure | | | +-------+---------------+-----------------+ From 9113ef0a0a1dfac8e5573e9f78c11371ee4c5f7e Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sat, 23 May 2020 05:59:16 +0530 Subject: [PATCH 1916/3170] Use Python 3.8 for "primary" run on Azure Ubuntu/MacOS --- .azure-pipelines/jobs/test.yml | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.azure-pipelines/jobs/test.yml b/.azure-pipelines/jobs/test.yml index f7dac5143f7..274e075a69b 100644 --- a/.azure-pipelines/jobs/test.yml +++ b/.azure-pipelines/jobs/test.yml @@ -12,8 +12,8 @@ jobs: "2.7": python.version: '2.7' python.architecture: x64 - "3.6": - python.version: '3.6' + "3.8": + python.version: '3.8' python.architecture: x64 maxParallel: 2 @@ -32,12 +32,12 @@ jobs: "3.5": python.version: '3.5' python.architecture: x64 + "3.6": + python.version: '3.6' + python.architecture: x64 "3.7": python.version: '3.7' python.architecture: x64 - "3.8": - python.version: '3.8' - python.architecture: x64 maxParallel: 4 steps: From 3ffd0305d9136f5cb1dd1ee3efbde5139dcab84b Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sat, 23 May 2020 05:42:48 +0530 Subject: [PATCH 1917/3170] Update CI documentation to reflect current realities --- docs/html/development/ci.rst | 32 ++++++++++++++++---------------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/docs/html/development/ci.rst b/docs/html/development/ci.rst index 8b6412307a9..f3b64c69cfd 100644 --- a/docs/html/development/ci.rst +++ b/docs/html/development/ci.rst @@ -66,9 +66,9 @@ Services pip test suite and checks are distributed on three different platforms that provides free executors for open source packages: - - `Travis CI`_ (Used for Linux) - - `Azure DevOps CI`_ (Linux, MacOS & Windows tests) - - `GitHub Actions`_ (Linux, MacOS & Windows tests) + - `GitHub Actions`_ (Used for code quality and development tasks) + - `Azure DevOps CI`_ (Used for tests) + - `Travis CI`_ (Used for PyPy tests) .. _`Travis CI`: https://travis-ci.org/ .. _`Azure DevOps CI`: https://azure.microsoft.com/en-us/services/devops/ @@ -81,13 +81,13 @@ Current run tests Developer tasks --------------- -======== =============== ================ ================== ============ - OS docs lint vendoring packages -======== =============== ================ ================== ============ -Linux Travis, Github Travis, Github Travis, Github Azure -Windows Azure -MacOS Azure -======== =============== ================ ================== ============ +======== =============== ================ ================== ============= + OS docs lint vendoring packaging +======== =============== ================ ================== ============= +Linux Travis, Github Travis, Github Travis, Github Azure +Windows Github Github Github Azure +MacOS Github Github Github Azure +======== =============== ================ ================== ============= Actual testing -------------- @@ -137,15 +137,15 @@ Actual testing | | +-------+---------------+-----------------+ | | | PyPy3 | | | | Linux +----------+-------+---------------+-----------------+ -| | | CP2.7 | Travis,Azure | Travis,Azure | +| | | CP2.7 | Azure | Azure | | | +-------+---------------+-----------------+ -| | | CP3.5 | Travis,Azure | Travis,Azure | +| | | CP3.5 | Azure | Azure | | | +-------+---------------+-----------------+ -| | | CP3.6 | Travis,Azure | Travis,Azure | +| | | CP3.6 | Azure | Azure | | | +-------+---------------+-----------------+ -| | x64 | CP3.7 | Travis,Azure | Travis,Azure | +| | x64 | CP3.7 | Azure | Azure | | | +-------+---------------+-----------------+ -| | | CP3.8 | Travis | Travis | +| | | CP3.8 | Azure | Azure | | | +-------+---------------+-----------------+ | | | PyPy | Travis | Travis | | | +-------+---------------+-----------------+ @@ -173,7 +173,7 @@ Actual testing | | +-------+---------------+-----------------+ | | x64 | CP3.7 | Azure | Azure | | | +-------+---------------+-----------------+ -| | | CP3.8 | | | +| | | CP3.8 | Azure | Azure | | | +-------+---------------+-----------------+ | | | PyPy | | | | | +-------+---------------+-----------------+ From 71c0fb0581e5c512a415ca79ec27c11c45e2919f Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sat, 23 May 2020 09:15:25 +0530 Subject: [PATCH 1918/3170] Update build_env.install_requirements message type to str --- src/pip/_internal/build_env.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py index 3c780f6dabf..f329d2bcc96 100644 --- a/src/pip/_internal/build_env.py +++ b/src/pip/_internal/build_env.py @@ -163,7 +163,7 @@ def install_requirements( finder, # type: PackageFinder requirements, # type: Iterable[str] prefix_as_string, # type: str - message # type: Optional[str] + message # type: str ): # type: (...) -> None prefix = self._prefixes[prefix_as_string] @@ -199,7 +199,6 @@ def install_requirements( args.append('--pre') args.append('--') args.extend(requirements) - assert message with open_spinner(message) as spinner: call_subprocess(args, spinner=spinner) @@ -234,7 +233,7 @@ def install_requirements( finder, # type: PackageFinder requirements, # type: Iterable[str] prefix_as_string, # type: str - message # type: Optional[str] + message # type: str ): # type: (...) -> None raise NotImplementedError() From 3ac13ee7549e2c4bfe2798ca3eb6c457cb84ba32 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sat, 23 May 2020 10:38:07 +0530 Subject: [PATCH 1919/3170] Add back Python 3.5 --- .azure-pipelines/jobs/test-windows.yml | 10 +++++----- docs/html/development/ci.rst | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.azure-pipelines/jobs/test-windows.yml b/.azure-pipelines/jobs/test-windows.yml index 08131b683c4..6053b0eb005 100644 --- a/.azure-pipelines/jobs/test-windows.yml +++ b/.azure-pipelines/jobs/test-windows.yml @@ -12,10 +12,13 @@ jobs: "2.7-x86": python.version: '2.7' python.architecture: x86 - "2.7": + "2.7": # because Python 2! python.version: '2.7' python.architecture: x64 - "3.8": + "3.5": # lowest Py3 version + python.version: '3.5' + python.architecture: x64 + "3.8": # current python.version: '3.8' python.architecture: x64 maxParallel: 6 @@ -35,9 +38,6 @@ jobs: vmImage: ${{ parameters.vmImage }} strategy: matrix: - "3.5": - python.version: '3.5' - python.architecture: x64 "3.6": python.version: '3.6' python.architecture: x64 diff --git a/docs/html/development/ci.rst b/docs/html/development/ci.rst index d2654f1f67c..baa8cbb20bd 100644 --- a/docs/html/development/ci.rst +++ b/docs/html/development/ci.rst @@ -111,7 +111,7 @@ Actual testing | Windows +----------+-------+---------------+-----------------+ | | | CP2.7 | Azure | Azure | | | +-------+---------------+-----------------+ -| | | CP3.5 | Azure | | +| | | CP3.5 | Azure | Azure | | | +-------+---------------+-----------------+ | | | CP3.6 | Azure | | | | +-------+---------------+-----------------+ From 4fbe61af3a77fef5594329d41355571cf1adf94f Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sat, 23 May 2020 10:04:20 +0530 Subject: [PATCH 1920/3170] Update message type to configparser.Error --- src/pip/_internal/exceptions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/exceptions.py b/src/pip/_internal/exceptions.py index c92b8cecd59..f478a2b4934 100644 --- a/src/pip/_internal/exceptions.py +++ b/src/pip/_internal/exceptions.py @@ -11,6 +11,7 @@ if MYPY_CHECK_RUNNING: from typing import Optional, List, Dict from pip._vendor.pkg_resources import Distribution + from pip._vendor.six.moves import configparser from pip._internal.req.req_install import InstallRequirement from pip._vendor.six import PY3 if PY3: @@ -312,7 +313,7 @@ class ConfigurationFileCouldNotBeLoaded(ConfigurationError): """ def __init__(self, reason="could not be loaded", fname=None, error=None): - # type: (str, Optional[str], Optional[Exception]) -> None + # type: (str, Optional[str], Optional[configparser.Error]) -> None super(ConfigurationFileCouldNotBeLoaded, self).__init__(error) self.reason = reason self.fname = fname @@ -324,6 +325,5 @@ def __str__(self): message_part = " in {}.".format(self.fname) else: assert self.error is not None - # message attribute for Exception is only available for Python 2 - message_part = ".\n{}\n".format(self.error.message) # type: ignore + message_part = ".\n{}\n".format(self.error) return "Configuration file {}{}".format(self.reason, message_part) From 72a42197a4552e9b9f561731613c6f23f731a644 Mon Sep 17 00:00:00 2001 From: gutsytechster <prashantsharma161198@gmail.com> Date: Thu, 21 May 2020 14:00:44 +0530 Subject: [PATCH 1921/3170] refactor(commands): Add method add_options and remove __init__ This removes the __init__ method of child classes and defines explicit method for adding command options. --- src/pip/_internal/cli/base_command.py | 7 +++++++ src/pip/_internal/commands/completion.py | 12 ++++-------- src/pip/_internal/commands/configuration.py | 17 ++++++++++------- src/pip/_internal/commands/debug.py | 11 ++++------- src/pip/_internal/commands/download.py | 7 +------ src/pip/_internal/commands/freeze.py | 21 +++++++++------------ src/pip/_internal/commands/hash.py | 13 ++++++------- src/pip/_internal/commands/install.py | 9 +++------ src/pip/_internal/commands/list.py | 12 ++++-------- src/pip/_internal/commands/search.py | 8 +++----- src/pip/_internal/commands/show.py | 13 ++++++------- src/pip/_internal/commands/uninstall.py | 15 +++++++-------- src/pip/_internal/commands/wheel.py | 10 ++++------ tests/unit/test_format_control.py | 6 ++++-- 14 files changed, 72 insertions(+), 89 deletions(-) diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index 1fa5ba0bd4a..71d3e4c8206 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -87,6 +87,13 @@ def __init__(self, name, summary, isolated=False): self.parser, ) self.parser.add_option_group(gen_opts) + try: + # mypy raises error due to + # https://github.com/python/mypy/issues/5868 + self.add_options(self.cmd_opts) # type: ignore + except AttributeError: + # it means that the base class has not defined the method + logger.debug("No add_options method defined in the class") def handle_pip_version_check(self, options): # type: (Values) -> None diff --git a/src/pip/_internal/commands/completion.py b/src/pip/_internal/commands/completion.py index 70d33243fcd..1dddf869d96 100644 --- a/src/pip/_internal/commands/completion.py +++ b/src/pip/_internal/commands/completion.py @@ -9,8 +9,8 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Any, List - from optparse import Values + from typing import List + from optparse import Values, OptionGroup BASE_COMPLETION = """ # pip {shell} completion start{script}# pip {shell} completion end @@ -56,12 +56,8 @@ class CompletionCommand(Command): ignore_require_venv = True - def __init__(self, *args, **kw): - # type: (*Any, **Any) -> None - super(CompletionCommand, self).__init__(*args, **kw) - - cmd_opts = self.cmd_opts - + def add_options(self, cmd_opts): + # type: (OptionGroup) -> None cmd_opts.add_option( '--bash', '-b', action='store_const', diff --git a/src/pip/_internal/commands/configuration.py b/src/pip/_internal/commands/configuration.py index b801be6a03c..26cfd7a4a8b 100644 --- a/src/pip/_internal/commands/configuration.py +++ b/src/pip/_internal/commands/configuration.py @@ -45,12 +45,15 @@ class ConfigurationCommand(Command): %prog [<file-option>] unset name """ - def __init__(self, *args, **kwargs): - super(ConfigurationCommand, self).__init__(*args, **kwargs) + def __init__(self, name, summary, isolated=False): + super(ConfigurationCommand, self).__init__( + name, summary, isolated=isolated + ) self.configuration = None - self.cmd_opts.add_option( + def add_options(self, cmd_opts): + cmd_opts.add_option( '--editor', dest='editor', action='store', @@ -61,7 +64,7 @@ def __init__(self, *args, **kwargs): ) ) - self.cmd_opts.add_option( + cmd_opts.add_option( '--global', dest='global_file', action='store_true', @@ -69,7 +72,7 @@ def __init__(self, *args, **kwargs): help='Use the system-wide configuration file only' ) - self.cmd_opts.add_option( + cmd_opts.add_option( '--user', dest='user_file', action='store_true', @@ -77,7 +80,7 @@ def __init__(self, *args, **kwargs): help='Use the user configuration file only' ) - self.cmd_opts.add_option( + cmd_opts.add_option( '--site', dest='site_file', action='store_true', @@ -85,7 +88,7 @@ def __init__(self, *args, **kwargs): help='Use the current environment configuration file only' ) - self.parser.insert_option_group(0, self.cmd_opts) + self.parser.insert_option_group(0, cmd_opts) def run(self, options, args): handlers = { diff --git a/src/pip/_internal/commands/debug.py b/src/pip/_internal/commands/debug.py index 8e243011f97..3a32f929922 100644 --- a/src/pip/_internal/commands/debug.py +++ b/src/pip/_internal/commands/debug.py @@ -20,8 +20,8 @@ if MYPY_CHECK_RUNNING: from types import ModuleType - from typing import Any, List, Optional, Dict - from optparse import Values + from typing import List, Optional, Dict + from optparse import Values, OptionGroup logger = logging.getLogger(__name__) @@ -193,11 +193,8 @@ class DebugCommand(Command): %prog <options>""" ignore_require_venv = True - def __init__(self, *args, **kw): - # type: (*Any, **Any) -> None - super(DebugCommand, self).__init__(*args, **kw) - - cmd_opts = self.cmd_opts + def add_options(self, cmd_opts): + # type: (OptionGroup) -> None cmdoptions.add_target_python_options(cmd_opts) self.parser.insert_option_group(0, cmd_opts) self.parser.config.load() diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index e48a7838c9f..c8da0e5ee94 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -39,12 +39,7 @@ class DownloadCommand(RequirementCommand): %prog [options] <local project path> ... %prog [options] <archive url/path> ...""" - def __init__(self, *args, **kw): - # type: (*Any, **Any) -> None - super(DownloadCommand, self).__init__(*args, **kw) - - cmd_opts = self.cmd_opts - + def add_options(self, cmd_opts): cmd_opts.add_option(cmdoptions.constraints()) cmd_opts.add_option(cmdoptions.requirements()) cmd_opts.add_option(cmdoptions.build_dir()) diff --git a/src/pip/_internal/commands/freeze.py b/src/pip/_internal/commands/freeze.py index 5b9a78ff5ec..a140f94bd32 100644 --- a/src/pip/_internal/commands/freeze.py +++ b/src/pip/_internal/commands/freeze.py @@ -29,11 +29,8 @@ class FreezeCommand(Command): %prog [options]""" log_streams = ("ext://sys.stderr", "ext://sys.stderr") - def __init__(self, *args, **kw): - # type: (*Any, **Any) -> None - super(FreezeCommand, self).__init__(*args, **kw) - - self.cmd_opts.add_option( + def add_options(self, cmd_opts): + cmd_opts.add_option( '-r', '--requirement', dest='requirements', action='append', @@ -42,7 +39,7 @@ def __init__(self, *args, **kw): help="Use the order in the given requirements file and its " "comments when generating output. This option can be " "used multiple times.") - self.cmd_opts.add_option( + cmd_opts.add_option( '-f', '--find-links', dest='find_links', action='append', @@ -50,33 +47,33 @@ def __init__(self, *args, **kw): metavar='URL', help='URL for finding packages, which will be added to the ' 'output.') - self.cmd_opts.add_option( + cmd_opts.add_option( '-l', '--local', dest='local', action='store_true', default=False, help='If in a virtualenv that has global access, do not output ' 'globally-installed packages.') - self.cmd_opts.add_option( + cmd_opts.add_option( '--user', dest='user', action='store_true', default=False, help='Only output packages installed in user-site.') - self.cmd_opts.add_option(cmdoptions.list_path()) - self.cmd_opts.add_option( + cmd_opts.add_option(cmdoptions.list_path()) + cmd_opts.add_option( '--all', dest='freeze_all', action='store_true', help='Do not skip these packages in the output:' ' {}'.format(', '.join(DEV_PKGS))) - self.cmd_opts.add_option( + 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) + self.parser.insert_option_group(0, cmd_opts) def run(self, options, args): # type: (Values, List[str]) -> int diff --git a/src/pip/_internal/commands/hash.py b/src/pip/_internal/commands/hash.py index aab4a3dc2fe..1a09c4f7c34 100644 --- a/src/pip/_internal/commands/hash.py +++ b/src/pip/_internal/commands/hash.py @@ -11,8 +11,8 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from optparse import Values - from typing import Any, List + from optparse import Values, OptionGroup + from typing import List logger = logging.getLogger(__name__) @@ -28,10 +28,9 @@ class HashCommand(Command): usage = '%prog [options] <file> ...' ignore_require_venv = True - def __init__(self, *args, **kw): - # type: (*Any, **Any) -> None - super(HashCommand, self).__init__(*args, **kw) - self.cmd_opts.add_option( + def add_options(self, cmd_opts): + # type: (OptionGroup) -> None + cmd_opts.add_option( '-a', '--algorithm', dest='algorithm', choices=STRONG_HASHES, @@ -39,7 +38,7 @@ def __init__(self, *args, **kw): default=FAVORITE_HASH, help='The hash algorithm to use: one of {}'.format( ', '.join(STRONG_HASHES))) - self.parser.insert_option_group(0, self.cmd_opts) + self.parser.insert_option_group(0, cmd_opts) def run(self, options, args): # type: (Values, List[str]) -> int diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index c9b9ea4a8c5..ee933b67e25 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -43,7 +43,7 @@ from pip._internal.wheel_builder import build, should_build_for_install_command if MYPY_CHECK_RUNNING: - from optparse import Values + from optparse import Values, OptionGroup from typing import Any, Iterable, List, Optional from pip._internal.models.format_control import FormatControl @@ -87,11 +87,8 @@ class InstallCommand(RequirementCommand): %prog [options] [-e] <local project path> ... %prog [options] <archive url/path> ...""" - def __init__(self, *args, **kw): - super(InstallCommand, self).__init__(*args, **kw) - - cmd_opts = self.cmd_opts - + def add_options(self, cmd_opts): + # type: (OptionGroup) -> None cmd_opts.add_option(cmdoptions.requirements()) cmd_opts.add_option(cmdoptions.constraints()) cmd_opts.add_option(cmdoptions.no_deps()) diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index 052f63890ec..7d6acb818d8 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -22,8 +22,8 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from optparse import Values - from typing import Any, List, Set, Tuple, Iterator + from optparse import Values, OptionGroup + from typing import List, Set, Tuple, Iterator from pip._internal.network.session import PipSession from pip._vendor.pkg_resources import Distribution @@ -41,12 +41,8 @@ class ListCommand(IndexGroupCommand): usage = """ %prog [options]""" - def __init__(self, *args, **kw): - # type: (*Any, **Any) -> None - super(ListCommand, self).__init__(*args, **kw) - - cmd_opts = self.cmd_opts - + def add_options(self, cmd_opts): + # type: (OptionGroup) -> None cmd_opts.add_option( '-o', '--outdated', action='store_true', diff --git a/src/pip/_internal/commands/search.py b/src/pip/_internal/commands/search.py index d8c5fba8f0e..dc97dae1e37 100644 --- a/src/pip/_internal/commands/search.py +++ b/src/pip/_internal/commands/search.py @@ -41,17 +41,15 @@ class SearchCommand(Command, SessionCommandMixin): %prog [options] <query>""" ignore_require_venv = True - def __init__(self, *args, **kw): - # type: (*Any, **Any) -> None - super(SearchCommand, self).__init__(*args, **kw) - self.cmd_opts.add_option( + def add_options(self, cmd_opts): + cmd_opts.add_option( '-i', '--index', dest='index', metavar='URL', default=PyPI.pypi_url, help='Base URL of Python Package Index (default %default)') - self.parser.insert_option_group(0, self.cmd_opts) + self.parser.insert_option_group(0, cmd_opts) def run(self, options, args): # type: (Values, List[str]) -> int diff --git a/src/pip/_internal/commands/show.py b/src/pip/_internal/commands/show.py index 97735f2d232..0b9723498ce 100644 --- a/src/pip/_internal/commands/show.py +++ b/src/pip/_internal/commands/show.py @@ -13,8 +13,8 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from optparse import Values - from typing import Any, List, Dict, Iterator + from optparse import Values, OptionGroup + from typing import List, Dict, Iterator logger = logging.getLogger(__name__) @@ -30,17 +30,16 @@ class ShowCommand(Command): %prog [options] <package> ...""" ignore_require_venv = True - def __init__(self, *args, **kw): - # type: (*Any, **Any) -> None - super(ShowCommand, self).__init__(*args, **kw) - self.cmd_opts.add_option( + def add_options(self, cmd_opts): + # type: (OptionGroup) -> None + cmd_opts.add_option( '-f', '--files', dest='files', action='store_true', default=False, help='Show the full list of installed files for each package.') - self.parser.insert_option_group(0, self.cmd_opts) + self.parser.insert_option_group(0, cmd_opts) def run(self, options, args): # type: (Values, List[str]) -> int diff --git a/src/pip/_internal/commands/uninstall.py b/src/pip/_internal/commands/uninstall.py index 0542e78c79c..e5b6b946052 100644 --- a/src/pip/_internal/commands/uninstall.py +++ b/src/pip/_internal/commands/uninstall.py @@ -15,8 +15,8 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from optparse import Values - from typing import Any, List + from optparse import Values, OptionGroup + from typing import List class UninstallCommand(Command, SessionCommandMixin): @@ -34,10 +34,9 @@ class UninstallCommand(Command, SessionCommandMixin): %prog [options] <package> ... %prog [options] -r <requirements file> ...""" - def __init__(self, *args, **kw): - # type: (*Any, **Any) -> None - super(UninstallCommand, self).__init__(*args, **kw) - self.cmd_opts.add_option( + def add_options(self, cmd_opts): + # type: (OptionGroup) -> None + cmd_opts.add_option( '-r', '--requirement', dest='requirements', action='append', @@ -46,13 +45,13 @@ def __init__(self, *args, **kw): help='Uninstall all the packages listed in the given requirements ' 'file. This option can be used multiple times.', ) - self.cmd_opts.add_option( + cmd_opts.add_option( '-y', '--yes', dest='yes', action='store_true', help="Don't ask for confirmation of uninstall deletions.") - self.parser.insert_option_group(0, self.cmd_opts) + self.parser.insert_option_group(0, cmd_opts) def run(self, options, args): # type: (Values, List[str]) -> int diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index f028d681f7b..e3d249f2517 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -18,8 +18,8 @@ from pip._internal.wheel_builder import build, should_build_for_wheel_command if MYPY_CHECK_RUNNING: - from optparse import Values - from typing import Any, List + from optparse import Values, OptionGroup + from typing import List logger = logging.getLogger(__name__) @@ -47,10 +47,8 @@ class WheelCommand(RequirementCommand): %prog [options] [-e] <local project path> ... %prog [options] <archive url/path> ...""" - def __init__(self, *args, **kw): - # type: (*Any, **Any) -> None - super(WheelCommand, self).__init__(*args, **kw) - + def add_options(self, cmd_opts): + # type: (OptionGroup) -> None cmd_opts = self.cmd_opts cmd_opts.add_option( diff --git a/tests/unit/test_format_control.py b/tests/unit/test_format_control.py index 0b0e2bde221..9901c0303d3 100644 --- a/tests/unit/test_format_control.py +++ b/tests/unit/test_format_control.py @@ -9,8 +9,10 @@ class SimpleCommand(Command): def __init__(self): super(SimpleCommand, self).__init__('fake', 'fake summary') - self.cmd_opts.add_option(cmdoptions.no_binary()) - self.cmd_opts.add_option(cmdoptions.only_binary()) + + def add_options(self, cmd_opts): + cmd_opts.add_option(cmdoptions.no_binary()) + cmd_opts.add_option(cmdoptions.only_binary()) def run(self, options, args): self.options = options From 58fb2ec0fcefeb0b91a766ce9bdb5464fb56d862 Mon Sep 17 00:00:00 2001 From: gutsytechster <prashantsharma161198@gmail.com> Date: Thu, 21 May 2020 14:08:49 +0530 Subject: [PATCH 1922/3170] docs(news): Add news file entry --- news/d9f9c55b-f959-456f-a849-ee976ef227de.trivial | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 news/d9f9c55b-f959-456f-a849-ee976ef227de.trivial diff --git a/news/d9f9c55b-f959-456f-a849-ee976ef227de.trivial b/news/d9f9c55b-f959-456f-a849-ee976ef227de.trivial new file mode 100644 index 00000000000..ece7751fc27 --- /dev/null +++ b/news/d9f9c55b-f959-456f-a849-ee976ef227de.trivial @@ -0,0 +1,2 @@ +Refactor the commands by removing the ``__init__`` method and defining and explicit +``add_options`` method for adding command options. From b82516c9cab2f8f804a5eedf3d6bfbcfb3ab05f4 Mon Sep 17 00:00:00 2001 From: gutsytechster <prashantsharma161198@gmail.com> Date: Fri, 22 May 2020 15:01:58 +0530 Subject: [PATCH 1923/3170] fix(_internal/commands): Define a default add_option to child commands --- src/pip/_internal/cli/base_command.py | 13 ++-- src/pip/_internal/commands/completion.py | 14 ++-- src/pip/_internal/commands/configuration.py | 12 ++-- src/pip/_internal/commands/debug.py | 10 +-- src/pip/_internal/commands/download.py | 43 ++++++------ src/pip/_internal/commands/freeze.py | 21 +++--- src/pip/_internal/commands/hash.py | 10 +-- src/pip/_internal/commands/install.py | 72 ++++++++++----------- src/pip/_internal/commands/list.py | 28 ++++---- src/pip/_internal/commands/search.py | 9 +-- src/pip/_internal/commands/show.py | 10 +-- src/pip/_internal/commands/uninstall.py | 12 ++-- src/pip/_internal/commands/wheel.py | 49 +++++++------- tests/unit/test_format_control.py | 6 +- 14 files changed, 155 insertions(+), 154 deletions(-) diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index 71d3e4c8206..535a491625f 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -87,13 +87,12 @@ def __init__(self, name, summary, isolated=False): self.parser, ) self.parser.add_option_group(gen_opts) - try: - # mypy raises error due to - # https://github.com/python/mypy/issues/5868 - self.add_options(self.cmd_opts) # type: ignore - except AttributeError: - # it means that the base class has not defined the method - logger.debug("No add_options method defined in the class") + + self.add_options() + + def add_options(self): + # type: () -> None + pass def handle_pip_version_check(self, options): # type: (Values) -> None diff --git a/src/pip/_internal/commands/completion.py b/src/pip/_internal/commands/completion.py index 1dddf869d96..9b99f51f006 100644 --- a/src/pip/_internal/commands/completion.py +++ b/src/pip/_internal/commands/completion.py @@ -10,7 +10,7 @@ if MYPY_CHECK_RUNNING: from typing import List - from optparse import Values, OptionGroup + from optparse import Values BASE_COMPLETION = """ # pip {shell} completion start{script}# pip {shell} completion end @@ -56,28 +56,28 @@ class CompletionCommand(Command): ignore_require_venv = True - def add_options(self, cmd_opts): - # type: (OptionGroup) -> None - cmd_opts.add_option( + def add_options(self): + # type: () -> None + self.cmd_opts.add_option( '--bash', '-b', action='store_const', const='bash', dest='shell', help='Emit completion code for bash') - cmd_opts.add_option( + self.cmd_opts.add_option( '--zsh', '-z', action='store_const', const='zsh', dest='shell', help='Emit completion code for zsh') - cmd_opts.add_option( + self.cmd_opts.add_option( '--fish', '-f', action='store_const', const='fish', dest='shell', help='Emit completion code for fish') - self.parser.insert_option_group(0, cmd_opts) + self.parser.insert_option_group(0, self.cmd_opts) def run(self, options, args): # type: (Values, List[str]) -> int diff --git a/src/pip/_internal/commands/configuration.py b/src/pip/_internal/commands/configuration.py index 26cfd7a4a8b..4e5ebd2c5e1 100644 --- a/src/pip/_internal/commands/configuration.py +++ b/src/pip/_internal/commands/configuration.py @@ -52,8 +52,8 @@ def __init__(self, name, summary, isolated=False): self.configuration = None - def add_options(self, cmd_opts): - cmd_opts.add_option( + def add_options(self): + self.cmd_opts.add_option( '--editor', dest='editor', action='store', @@ -64,7 +64,7 @@ def add_options(self, cmd_opts): ) ) - cmd_opts.add_option( + self.cmd_opts.add_option( '--global', dest='global_file', action='store_true', @@ -72,7 +72,7 @@ def add_options(self, cmd_opts): help='Use the system-wide configuration file only' ) - cmd_opts.add_option( + self.cmd_opts.add_option( '--user', dest='user_file', action='store_true', @@ -80,7 +80,7 @@ def add_options(self, cmd_opts): help='Use the user configuration file only' ) - cmd_opts.add_option( + self.cmd_opts.add_option( '--site', dest='site_file', action='store_true', @@ -88,7 +88,7 @@ def add_options(self, cmd_opts): help='Use the current environment configuration file only' ) - self.parser.insert_option_group(0, cmd_opts) + self.parser.insert_option_group(0, self.cmd_opts) def run(self, options, args): handlers = { diff --git a/src/pip/_internal/commands/debug.py b/src/pip/_internal/commands/debug.py index 3a32f929922..d8e2484c1b4 100644 --- a/src/pip/_internal/commands/debug.py +++ b/src/pip/_internal/commands/debug.py @@ -21,7 +21,7 @@ if MYPY_CHECK_RUNNING: from types import ModuleType from typing import List, Optional, Dict - from optparse import Values, OptionGroup + from optparse import Values logger = logging.getLogger(__name__) @@ -193,10 +193,10 @@ class DebugCommand(Command): %prog <options>""" ignore_require_venv = True - def add_options(self, cmd_opts): - # type: (OptionGroup) -> None - cmdoptions.add_target_python_options(cmd_opts) - self.parser.insert_option_group(0, cmd_opts) + def add_options(self): + # type: () -> None + cmdoptions.add_target_python_options(self.cmd_opts) + self.parser.insert_option_group(0, self.cmd_opts) self.parser.config.load() def run(self, options, args): diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index c8da0e5ee94..46e8371261e 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -14,7 +14,7 @@ if MYPY_CHECK_RUNNING: from optparse import Values - from typing import Any, List + from typing import List logger = logging.getLogger(__name__) @@ -39,24 +39,25 @@ class DownloadCommand(RequirementCommand): %prog [options] <local project path> ... %prog [options] <archive url/path> ...""" - def add_options(self, cmd_opts): - cmd_opts.add_option(cmdoptions.constraints()) - cmd_opts.add_option(cmdoptions.requirements()) - cmd_opts.add_option(cmdoptions.build_dir()) - cmd_opts.add_option(cmdoptions.no_deps()) - cmd_opts.add_option(cmdoptions.global_options()) - cmd_opts.add_option(cmdoptions.no_binary()) - cmd_opts.add_option(cmdoptions.only_binary()) - cmd_opts.add_option(cmdoptions.prefer_binary()) - cmd_opts.add_option(cmdoptions.src()) - cmd_opts.add_option(cmdoptions.pre()) - 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(cmdoptions.use_pep517()) - cmd_opts.add_option(cmdoptions.no_use_pep517()) - - cmd_opts.add_option( + def add_options(self): + # type: () -> None + self.cmd_opts.add_option(cmdoptions.constraints()) + self.cmd_opts.add_option(cmdoptions.requirements()) + self.cmd_opts.add_option(cmdoptions.build_dir()) + self.cmd_opts.add_option(cmdoptions.no_deps()) + self.cmd_opts.add_option(cmdoptions.global_options()) + self.cmd_opts.add_option(cmdoptions.no_binary()) + self.cmd_opts.add_option(cmdoptions.only_binary()) + self.cmd_opts.add_option(cmdoptions.prefer_binary()) + self.cmd_opts.add_option(cmdoptions.src()) + self.cmd_opts.add_option(cmdoptions.pre()) + self.cmd_opts.add_option(cmdoptions.require_hashes()) + self.cmd_opts.add_option(cmdoptions.progress_bar()) + self.cmd_opts.add_option(cmdoptions.no_build_isolation()) + self.cmd_opts.add_option(cmdoptions.use_pep517()) + self.cmd_opts.add_option(cmdoptions.no_use_pep517()) + + self.cmd_opts.add_option( '-d', '--dest', '--destination-dir', '--destination-directory', dest='download_dir', metavar='dir', @@ -64,7 +65,7 @@ def add_options(self, cmd_opts): help=("Download packages into <dir>."), ) - cmdoptions.add_target_python_options(cmd_opts) + cmdoptions.add_target_python_options(self.cmd_opts) index_opts = cmdoptions.make_option_group( cmdoptions.index_group, @@ -72,7 +73,7 @@ def add_options(self, cmd_opts): ) self.parser.insert_option_group(0, index_opts) - self.parser.insert_option_group(0, cmd_opts) + self.parser.insert_option_group(0, self.cmd_opts) @with_cleanup def run(self, options, args): diff --git a/src/pip/_internal/commands/freeze.py b/src/pip/_internal/commands/freeze.py index a140f94bd32..2071fbabd61 100644 --- a/src/pip/_internal/commands/freeze.py +++ b/src/pip/_internal/commands/freeze.py @@ -15,7 +15,7 @@ if MYPY_CHECK_RUNNING: from optparse import Values - from typing import Any, List + from typing import List class FreezeCommand(Command): @@ -29,8 +29,9 @@ class FreezeCommand(Command): %prog [options]""" log_streams = ("ext://sys.stderr", "ext://sys.stderr") - def add_options(self, cmd_opts): - cmd_opts.add_option( + def add_options(self): + # type: () -> None + self.cmd_opts.add_option( '-r', '--requirement', dest='requirements', action='append', @@ -39,7 +40,7 @@ def add_options(self, cmd_opts): help="Use the order in the given requirements file and its " "comments when generating output. This option can be " "used multiple times.") - cmd_opts.add_option( + self.cmd_opts.add_option( '-f', '--find-links', dest='find_links', action='append', @@ -47,33 +48,33 @@ def add_options(self, cmd_opts): metavar='URL', help='URL for finding packages, which will be added to the ' 'output.') - cmd_opts.add_option( + self.cmd_opts.add_option( '-l', '--local', dest='local', action='store_true', default=False, help='If in a virtualenv that has global access, do not output ' 'globally-installed packages.') - cmd_opts.add_option( + self.cmd_opts.add_option( '--user', dest='user', action='store_true', default=False, help='Only output packages installed in user-site.') - cmd_opts.add_option(cmdoptions.list_path()) - cmd_opts.add_option( + self.cmd_opts.add_option(cmdoptions.list_path()) + self.cmd_opts.add_option( '--all', dest='freeze_all', action='store_true', help='Do not skip these packages in the output:' ' {}'.format(', '.join(DEV_PKGS))) - cmd_opts.add_option( + 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, cmd_opts) + self.parser.insert_option_group(0, self.cmd_opts) def run(self, options, args): # type: (Values, List[str]) -> int diff --git a/src/pip/_internal/commands/hash.py b/src/pip/_internal/commands/hash.py index 1a09c4f7c34..37831c39522 100644 --- a/src/pip/_internal/commands/hash.py +++ b/src/pip/_internal/commands/hash.py @@ -11,7 +11,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from optparse import Values, OptionGroup + from optparse import Values from typing import List logger = logging.getLogger(__name__) @@ -28,9 +28,9 @@ class HashCommand(Command): usage = '%prog [options] <file> ...' ignore_require_venv = True - def add_options(self, cmd_opts): - # type: (OptionGroup) -> None - cmd_opts.add_option( + def add_options(self): + # type: () -> None + self.cmd_opts.add_option( '-a', '--algorithm', dest='algorithm', choices=STRONG_HASHES, @@ -38,7 +38,7 @@ def add_options(self, cmd_opts): default=FAVORITE_HASH, help='The hash algorithm to use: one of {}'.format( ', '.join(STRONG_HASHES))) - self.parser.insert_option_group(0, cmd_opts) + self.parser.insert_option_group(0, self.cmd_opts) def run(self, options, args): # type: (Values, List[str]) -> int diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index ee933b67e25..56dd707bdd9 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -43,7 +43,7 @@ from pip._internal.wheel_builder import build, should_build_for_install_command if MYPY_CHECK_RUNNING: - from optparse import Values, OptionGroup + from optparse import Values from typing import Any, Iterable, List, Optional from pip._internal.models.format_control import FormatControl @@ -87,15 +87,15 @@ class InstallCommand(RequirementCommand): %prog [options] [-e] <local project path> ... %prog [options] <archive url/path> ...""" - def add_options(self, cmd_opts): - # type: (OptionGroup) -> None - 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()) + def add_options(self): + # type: () -> None + self.cmd_opts.add_option(cmdoptions.requirements()) + self.cmd_opts.add_option(cmdoptions.constraints()) + self.cmd_opts.add_option(cmdoptions.no_deps()) + self.cmd_opts.add_option(cmdoptions.pre()) - cmd_opts.add_option(cmdoptions.editable()) - cmd_opts.add_option( + self.cmd_opts.add_option(cmdoptions.editable()) + self.cmd_opts.add_option( '-t', '--target', dest='target_dir', metavar='dir', @@ -105,9 +105,9 @@ def add_options(self, cmd_opts): '<dir>. Use --upgrade to replace existing packages in <dir> ' 'with new versions.' ) - cmdoptions.add_target_python_options(cmd_opts) + cmdoptions.add_target_python_options(self.cmd_opts) - cmd_opts.add_option( + self.cmd_opts.add_option( '--user', dest='use_user_site', action='store_true', @@ -115,19 +115,19 @@ def add_options(self, cmd_opts): "platform. Typically ~/.local/, or %APPDATA%\\Python on " "Windows. (See the Python documentation for site.USER_BASE " "for full details.)") - cmd_opts.add_option( + self.cmd_opts.add_option( '--no-user', dest='use_user_site', action='store_false', help=SUPPRESS_HELP) - cmd_opts.add_option( + self.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( + self.cmd_opts.add_option( '--prefix', dest='prefix_path', metavar='dir', @@ -135,11 +135,11 @@ def add_options(self, cmd_opts): help="Installation prefix where lib, bin and other top-level " "folders are placed") - cmd_opts.add_option(cmdoptions.build_dir()) + self.cmd_opts.add_option(cmdoptions.build_dir()) - cmd_opts.add_option(cmdoptions.src()) + self.cmd_opts.add_option(cmdoptions.src()) - cmd_opts.add_option( + self.cmd_opts.add_option( '-U', '--upgrade', dest='upgrade', action='store_true', @@ -148,7 +148,7 @@ def add_options(self, cmd_opts): 'upgrade-strategy used.' ) - cmd_opts.add_option( + self.cmd_opts.add_option( '--upgrade-strategy', dest='upgrade_strategy', default='only-if-needed', @@ -162,14 +162,14 @@ def add_options(self, cmd_opts): 'satisfy the requirements of the upgraded package(s).' ) - cmd_opts.add_option( + self.cmd_opts.add_option( '--force-reinstall', dest='force_reinstall', action='store_true', help='Reinstall all packages even if they are already ' 'up-to-date.') - cmd_opts.add_option( + self.cmd_opts.add_option( '-I', '--ignore-installed', dest='ignore_installed', action='store_true', @@ -179,15 +179,15 @@ def add_options(self, cmd_opts): 'with a different package manager!' ) - cmd_opts.add_option(cmdoptions.ignore_requires_python()) - cmd_opts.add_option(cmdoptions.no_build_isolation()) - cmd_opts.add_option(cmdoptions.use_pep517()) - cmd_opts.add_option(cmdoptions.no_use_pep517()) + self.cmd_opts.add_option(cmdoptions.ignore_requires_python()) + self.cmd_opts.add_option(cmdoptions.no_build_isolation()) + self.cmd_opts.add_option(cmdoptions.use_pep517()) + self.cmd_opts.add_option(cmdoptions.no_use_pep517()) - cmd_opts.add_option(cmdoptions.install_options()) - cmd_opts.add_option(cmdoptions.global_options()) + self.cmd_opts.add_option(cmdoptions.install_options()) + self.cmd_opts.add_option(cmdoptions.global_options()) - cmd_opts.add_option( + self.cmd_opts.add_option( "--compile", action="store_true", dest="compile", @@ -195,21 +195,21 @@ def add_options(self, cmd_opts): help="Compile Python source files to bytecode", ) - cmd_opts.add_option( + self.cmd_opts.add_option( "--no-compile", action="store_false", dest="compile", help="Do not compile Python source files to bytecode", ) - cmd_opts.add_option( + self.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( + self.cmd_opts.add_option( "--no-warn-conflicts", action="store_false", dest="warn_about_conflicts", @@ -217,11 +217,11 @@ def add_options(self, cmd_opts): 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.prefer_binary()) - cmd_opts.add_option(cmdoptions.require_hashes()) - cmd_opts.add_option(cmdoptions.progress_bar()) + self.cmd_opts.add_option(cmdoptions.no_binary()) + self.cmd_opts.add_option(cmdoptions.only_binary()) + self.cmd_opts.add_option(cmdoptions.prefer_binary()) + self.cmd_opts.add_option(cmdoptions.require_hashes()) + self.cmd_opts.add_option(cmdoptions.progress_bar()) index_opts = cmdoptions.make_option_group( cmdoptions.index_group, @@ -229,7 +229,7 @@ def add_options(self, cmd_opts): ) self.parser.insert_option_group(0, index_opts) - self.parser.insert_option_group(0, cmd_opts) + self.parser.insert_option_group(0, self.cmd_opts) @with_cleanup def run(self, options, args): diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index 7d6acb818d8..df9e1b38eda 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -22,7 +22,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from optparse import Values, OptionGroup + from optparse import Values from typing import List, Set, Tuple, Iterator from pip._internal.network.session import PipSession @@ -41,24 +41,24 @@ class ListCommand(IndexGroupCommand): usage = """ %prog [options]""" - def add_options(self, cmd_opts): - # type: (OptionGroup) -> None - cmd_opts.add_option( + def add_options(self): + # type: () -> None + self.cmd_opts.add_option( '-o', '--outdated', action='store_true', default=False, help='List outdated packages') - cmd_opts.add_option( + self.cmd_opts.add_option( '-u', '--uptodate', action='store_true', default=False, help='List uptodate packages') - cmd_opts.add_option( + self.cmd_opts.add_option( '-e', '--editable', action='store_true', default=False, help='List editable projects.') - cmd_opts.add_option( + self.cmd_opts.add_option( '-l', '--local', action='store_true', default=False, @@ -71,8 +71,8 @@ def add_options(self, cmd_opts): action='store_true', default=False, help='Only output packages installed in user-site.') - cmd_opts.add_option(cmdoptions.list_path()) - cmd_opts.add_option( + self.cmd_opts.add_option(cmdoptions.list_path()) + self.cmd_opts.add_option( '--pre', action='store_true', default=False, @@ -80,7 +80,7 @@ def add_options(self, cmd_opts): "pip only finds stable versions."), ) - cmd_opts.add_option( + self.cmd_opts.add_option( '--format', action='store', dest='list_format', @@ -90,7 +90,7 @@ def add_options(self, cmd_opts): "or json", ) - cmd_opts.add_option( + self.cmd_opts.add_option( '--not-required', action='store_true', dest='not_required', @@ -98,13 +98,13 @@ def add_options(self, cmd_opts): "installed packages.", ) - cmd_opts.add_option( + self.cmd_opts.add_option( '--exclude-editable', action='store_false', dest='include_editable', help='Exclude editable package from output.', ) - cmd_opts.add_option( + self.cmd_opts.add_option( '--include-editable', action='store_true', dest='include_editable', @@ -116,7 +116,7 @@ def add_options(self, cmd_opts): ) self.parser.insert_option_group(0, index_opts) - self.parser.insert_option_group(0, cmd_opts) + self.parser.insert_option_group(0, self.cmd_opts) def _build_package_finder(self, options, session): # type: (Values, PipSession) -> PackageFinder diff --git a/src/pip/_internal/commands/search.py b/src/pip/_internal/commands/search.py index dc97dae1e37..3e75254812a 100644 --- a/src/pip/_internal/commands/search.py +++ b/src/pip/_internal/commands/search.py @@ -24,7 +24,7 @@ if MYPY_CHECK_RUNNING: from optparse import Values - from typing import Any, List, Dict, Optional + from typing import List, Dict, Optional from typing_extensions import TypedDict TransformedHit = TypedDict( 'TransformedHit', @@ -41,15 +41,16 @@ class SearchCommand(Command, SessionCommandMixin): %prog [options] <query>""" ignore_require_venv = True - def add_options(self, cmd_opts): - cmd_opts.add_option( + def add_options(self): + # type: () -> None + self.cmd_opts.add_option( '-i', '--index', dest='index', metavar='URL', default=PyPI.pypi_url, help='Base URL of Python Package Index (default %default)') - self.parser.insert_option_group(0, cmd_opts) + self.parser.insert_option_group(0, self.cmd_opts) def run(self, options, args): # type: (Values, List[str]) -> int diff --git a/src/pip/_internal/commands/show.py b/src/pip/_internal/commands/show.py index 0b9723498ce..3892c5959ee 100644 --- a/src/pip/_internal/commands/show.py +++ b/src/pip/_internal/commands/show.py @@ -13,7 +13,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from optparse import Values, OptionGroup + from optparse import Values from typing import List, Dict, Iterator logger = logging.getLogger(__name__) @@ -30,16 +30,16 @@ class ShowCommand(Command): %prog [options] <package> ...""" ignore_require_venv = True - def add_options(self, cmd_opts): - # type: (OptionGroup) -> None - cmd_opts.add_option( + def add_options(self): + # type: () -> None + self.cmd_opts.add_option( '-f', '--files', dest='files', action='store_true', default=False, help='Show the full list of installed files for each package.') - self.parser.insert_option_group(0, cmd_opts) + self.parser.insert_option_group(0, self.cmd_opts) def run(self, options, args): # type: (Values, List[str]) -> int diff --git a/src/pip/_internal/commands/uninstall.py b/src/pip/_internal/commands/uninstall.py index e5b6b946052..3371fe47ff1 100644 --- a/src/pip/_internal/commands/uninstall.py +++ b/src/pip/_internal/commands/uninstall.py @@ -15,7 +15,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from optparse import Values, OptionGroup + from optparse import Values from typing import List @@ -34,9 +34,9 @@ class UninstallCommand(Command, SessionCommandMixin): %prog [options] <package> ... %prog [options] -r <requirements file> ...""" - def add_options(self, cmd_opts): - # type: (OptionGroup) -> None - cmd_opts.add_option( + def add_options(self): + # type: () -> None + self.cmd_opts.add_option( '-r', '--requirement', dest='requirements', action='append', @@ -45,13 +45,13 @@ def add_options(self, cmd_opts): help='Uninstall all the packages listed in the given requirements ' 'file. This option can be used multiple times.', ) - cmd_opts.add_option( + self.cmd_opts.add_option( '-y', '--yes', dest='yes', action='store_true', help="Don't ask for confirmation of uninstall deletions.") - self.parser.insert_option_group(0, cmd_opts) + self.parser.insert_option_group(0, self.cmd_opts) def run(self, options, args): # type: (Values, List[str]) -> int diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index e3d249f2517..0f718566bd0 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -18,7 +18,7 @@ from pip._internal.wheel_builder import build, should_build_for_wheel_command if MYPY_CHECK_RUNNING: - from optparse import Values, OptionGroup + from optparse import Values from typing import List @@ -47,11 +47,10 @@ class WheelCommand(RequirementCommand): %prog [options] [-e] <local project path> ... %prog [options] <archive url/path> ...""" - def add_options(self, cmd_opts): - # type: (OptionGroup) -> None - cmd_opts = self.cmd_opts + def add_options(self): + # type: () -> None - cmd_opts.add_option( + self.cmd_opts.add_option( '-w', '--wheel-dir', dest='wheel_dir', metavar='dir', @@ -59,29 +58,29 @@ def add_options(self, cmd_opts): help=("Build wheels into <dir>, where the default is the " "current working directory."), ) - cmd_opts.add_option(cmdoptions.no_binary()) - cmd_opts.add_option(cmdoptions.only_binary()) - cmd_opts.add_option(cmdoptions.prefer_binary()) - cmd_opts.add_option( + self.cmd_opts.add_option(cmdoptions.no_binary()) + self.cmd_opts.add_option(cmdoptions.only_binary()) + self.cmd_opts.add_option(cmdoptions.prefer_binary()) + self.cmd_opts.add_option( '--build-option', dest='build_options', metavar='options', action='append', help="Extra arguments to be supplied to 'setup.py bdist_wheel'.", ) - cmd_opts.add_option(cmdoptions.no_build_isolation()) - cmd_opts.add_option(cmdoptions.use_pep517()) - cmd_opts.add_option(cmdoptions.no_use_pep517()) - cmd_opts.add_option(cmdoptions.constraints()) - cmd_opts.add_option(cmdoptions.editable()) - cmd_opts.add_option(cmdoptions.requirements()) - cmd_opts.add_option(cmdoptions.src()) - 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( + self.cmd_opts.add_option(cmdoptions.no_build_isolation()) + self.cmd_opts.add_option(cmdoptions.use_pep517()) + self.cmd_opts.add_option(cmdoptions.no_use_pep517()) + self.cmd_opts.add_option(cmdoptions.constraints()) + self.cmd_opts.add_option(cmdoptions.editable()) + self.cmd_opts.add_option(cmdoptions.requirements()) + self.cmd_opts.add_option(cmdoptions.src()) + self.cmd_opts.add_option(cmdoptions.ignore_requires_python()) + self.cmd_opts.add_option(cmdoptions.no_deps()) + self.cmd_opts.add_option(cmdoptions.build_dir()) + self.cmd_opts.add_option(cmdoptions.progress_bar()) + + self.cmd_opts.add_option( '--global-option', dest='global_options', action='append', @@ -89,7 +88,7 @@ def add_options(self, cmd_opts): help="Extra global options to be supplied to the setup.py " "call before the 'bdist_wheel' command.") - cmd_opts.add_option( + self.cmd_opts.add_option( '--pre', action='store_true', default=False, @@ -97,7 +96,7 @@ def add_options(self, cmd_opts): "pip only finds stable versions."), ) - cmd_opts.add_option(cmdoptions.require_hashes()) + self.cmd_opts.add_option(cmdoptions.require_hashes()) index_opts = cmdoptions.make_option_group( cmdoptions.index_group, @@ -105,7 +104,7 @@ def add_options(self, cmd_opts): ) self.parser.insert_option_group(0, index_opts) - self.parser.insert_option_group(0, cmd_opts) + self.parser.insert_option_group(0, self.cmd_opts) @with_cleanup def run(self, options, args): diff --git a/tests/unit/test_format_control.py b/tests/unit/test_format_control.py index 9901c0303d3..0e152798184 100644 --- a/tests/unit/test_format_control.py +++ b/tests/unit/test_format_control.py @@ -10,9 +10,9 @@ class SimpleCommand(Command): def __init__(self): super(SimpleCommand, self).__init__('fake', 'fake summary') - def add_options(self, cmd_opts): - cmd_opts.add_option(cmdoptions.no_binary()) - cmd_opts.add_option(cmdoptions.only_binary()) + def add_options(self): + self.cmd_opts.add_option(cmdoptions.no_binary()) + self.cmd_opts.add_option(cmdoptions.only_binary()) def run(self, options, args): self.options = options From 0a1df7b11cc7a91a9947b000c5679331d5faa7e5 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sat, 23 May 2020 18:43:31 +0530 Subject: [PATCH 1924/3170] Apply suggestions from code review --- src/pip/_internal/models/link.py | 1 + src/pip/_internal/utils/models.py | 1 + 2 files changed, 2 insertions(+) diff --git a/src/pip/_internal/models/link.py b/src/pip/_internal/models/link.py index a54f37d2fcf..c0d278adee9 100644 --- a/src/pip/_internal/models/link.py +++ b/src/pip/_internal/models/link.py @@ -30,6 +30,7 @@ class Link(KeyBasedCompareMixin): "comes_from", "requires_python", "yanked_reason", + "cache_link_parsing", ] def __init__( diff --git a/src/pip/_internal/utils/models.py b/src/pip/_internal/utils/models.py index 4dd50e7959f..18390021930 100644 --- a/src/pip/_internal/utils/models.py +++ b/src/pip/_internal/utils/models.py @@ -18,6 +18,7 @@ class Base(object): class KeyBasedCompareMixin(Base): """Provides comparison capabilities that is based on a key """ + __slots__ = ['_compare_key', '_defining_class'] def __init__(self, key, defining_class): From 1471897b84b43c467c753b5edebe636f835afc6a Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Fri, 3 Apr 2020 13:08:41 +0530 Subject: [PATCH 1925/3170] Improve check for svn version string --- news/7968.bugfix | 1 + src/pip/_internal/vcs/subversion.py | 15 +++++++++++++-- tests/unit/test_vcs.py | 12 ++++++++++++ 3 files changed, 26 insertions(+), 2 deletions(-) create mode 100644 news/7968.bugfix diff --git a/news/7968.bugfix b/news/7968.bugfix new file mode 100644 index 00000000000..d6959730ab3 --- /dev/null +++ b/news/7968.bugfix @@ -0,0 +1 @@ +Look for version string in the entire output of svn --version, not just the first line diff --git a/src/pip/_internal/vcs/subversion.py b/src/pip/_internal/vcs/subversion.py index 0ec65974492..d0fe3cd9df9 100644 --- a/src/pip/_internal/vcs/subversion.py +++ b/src/pip/_internal/vcs/subversion.py @@ -25,7 +25,7 @@ if MYPY_CHECK_RUNNING: - from typing import Optional, Tuple + from typing import Optional, Tuple, Text from pip._internal.utils.subprocess import CommandArgs from pip._internal.utils.misc import HiddenText from pip._internal.vcs.versioncontrol import AuthInfo, RevOptions @@ -215,7 +215,18 @@ def call_vcs_version(self): # svn, version 1.7.14 (r1542130) # compiled Mar 28 2018, 08:49:13 on x86_64-pc-linux-gnu version_prefix = 'svn, version ' - version = self.run_command(['--version'], show_stdout=False) + cmd_output = self.run_command(['--version'], show_stdout=False) + + # Split the output by newline, and find the first line where + # version_prefix is present + output_lines = cmd_output.split('\n') + version = '' # type: Text + + for line in output_lines: + if version_prefix in line: + version = line + break + if not version.startswith(version_prefix): return () diff --git a/tests/unit/test_vcs.py b/tests/unit/test_vcs.py index 590cb5c0b75..166585db385 100644 --- a/tests/unit/test_vcs.py +++ b/tests/unit/test_vcs.py @@ -443,6 +443,18 @@ def test_subversion__call_vcs_version(): ('svn, version 1.10.3 (r1842928)\n' ' compiled Feb 25 2019, 14:20:39 on x86_64-apple-darwin17.0.0', (1, 10, 3)), + ('Warning: Failed to set locale category LC_NUMERIC to en_IN.\n' + 'Warning: Failed to set locale category LC_TIME to en_IN.\n' + 'svn, version 1.10.3 (r1842928)\n' + ' compiled Feb 25 2019, 14:20:39 on x86_64-apple-darwin17.0.0', + (1, 10, 3)), + ('Warning: Failed to set locale category LC_NUMERIC to en_IN.\n' + 'Warning: Failed to set locale category LC_TIME to en_IN.\n' + 'svn, version 1.10.3 (r1842928)\n' + ' compiled Feb 25 2019, 14:20:39 on x86_64-apple-darwin17.0.0' + 'svn, version 1.11.3 (r1842928)\n' + ' compiled Feb 25 2019, 14:20:39 on x86_64-apple-darwin17.0.0', + (1, 10, 3)), ('svn, version 1.9.7 (r1800392)', (1, 9, 7)), ('svn, version 1.9.7a1 (r1800392)', ()), ('svn, version 1.9 (r1800392)', (1, 9)), From 8adbc216a647b6b349f1b7f1eaa9e71cd3108955 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sat, 18 Apr 2020 12:47:35 +0530 Subject: [PATCH 1926/3170] Create call_subprocess just for vcs commands --- src/pip/_internal/exceptions.py | 5 + src/pip/_internal/vcs/subversion.py | 14 +-- src/pip/_internal/vcs/versioncontrol.py | 144 ++++++++++++++++++++++-- tests/unit/test_vcs.py | 12 -- 4 files changed, 139 insertions(+), 36 deletions(-) diff --git a/src/pip/_internal/exceptions.py b/src/pip/_internal/exceptions.py index 8ac85485e17..e0d7f095d48 100644 --- a/src/pip/_internal/exceptions.py +++ b/src/pip/_internal/exceptions.py @@ -84,6 +84,11 @@ class CommandError(PipError): """Raised when there is an error in command-line arguments""" +class SubProcessError(PipError): + """Raised when there is an error raised while executing a + command in subprocess""" + + class PreviousBuildDirError(PipError): """Raised when there's a previous conflicting build directory""" diff --git a/src/pip/_internal/vcs/subversion.py b/src/pip/_internal/vcs/subversion.py index d0fe3cd9df9..3f9d0833fa9 100644 --- a/src/pip/_internal/vcs/subversion.py +++ b/src/pip/_internal/vcs/subversion.py @@ -25,7 +25,7 @@ if MYPY_CHECK_RUNNING: - from typing import Optional, Tuple, Text + from typing import Optional, Tuple from pip._internal.utils.subprocess import CommandArgs from pip._internal.utils.misc import HiddenText from pip._internal.vcs.versioncontrol import AuthInfo, RevOptions @@ -215,17 +215,7 @@ def call_vcs_version(self): # svn, version 1.7.14 (r1542130) # compiled Mar 28 2018, 08:49:13 on x86_64-pc-linux-gnu version_prefix = 'svn, version ' - cmd_output = self.run_command(['--version'], show_stdout=False) - - # Split the output by newline, and find the first line where - # version_prefix is present - output_lines = cmd_output.split('\n') - version = '' # type: Text - - for line in output_lines: - if version_prefix in line: - version = line - break + version = self.run_command(['--version'], show_stdout=True) if not version.startswith(version_prefix): return () diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index 71b4650a252..1956559b397 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -6,13 +6,19 @@ import logging import os import shutil +import subprocess import sys from pip._vendor import pkg_resources from pip._vendor.six.moves.urllib import parse as urllib_parse -from pip._internal.exceptions import BadCommand, InstallationError -from pip._internal.utils.compat import samefile +from pip._internal.exceptions import ( + BadCommand, + InstallationError, + SubProcessError, +) +from pip._internal.utils.compat import console_to_str, samefile +from pip._internal.utils.logging import subprocess_logger from pip._internal.utils.misc import ( ask_path_exists, backup_dir, @@ -21,16 +27,20 @@ hide_value, rmtree, ) -from pip._internal.utils.subprocess import call_subprocess, make_command +from pip._internal.utils.subprocess import ( + format_command_args, + make_command, + make_subprocess_output_error, + reveal_command_args, +) from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.urls import get_url_scheme if MYPY_CHECK_RUNNING: from typing import ( - Any, Dict, Iterable, Iterator, List, Mapping, Optional, Text, Tuple, + Dict, Iterable, Iterator, List, Optional, Text, Tuple, Type, Union ) - from pip._internal.cli.spinners import SpinnerInterface from pip._internal.utils.misc import HiddenText from pip._internal.utils.subprocess import CommandArgs @@ -71,6 +81,123 @@ def make_vcs_requirement_url(repo_url, rev, project_name, subdir=None): return req +def call_subprocess( + cmd, # type: Union[List[str], CommandArgs] + show_stdout=False, # type: bool + cwd=None, # type: Optional[str] + on_returncode='raise', # type: str + extra_ok_returncodes=None, # type: Optional[Iterable[int]] + log_failed_cmd=True # type: Optional[bool] +): + # type: (...) -> Text + """ + Args: + show_stdout: if true, use INFO to log the subprocess's stderr and + stdout streams. Otherwise, use DEBUG. Defaults to False. + extra_ok_returncodes: an iterable of integer return codes that are + acceptable, in addition to 0. Defaults to None, which means []. + log_failed_cmd: if false, failed commands are not logged, + only raised. + """ + if extra_ok_returncodes is None: + extra_ok_returncodes = [] + # Most places in pip use show_stdout=False. + # What this means is-- + # + # - We log this output of stdout and stderr at DEBUG level + # as it is received. + # - If DEBUG logging isn't enabled (e.g. if --verbose logging wasn't + # requested), then we show a spinner so the user can still see the + # subprocess is in progress. + # - If the subprocess exits with an error, we log the output to stderr + # at ERROR level if it hasn't already been displayed to the console + # (e.g. if --verbose logging wasn't enabled). This way we don't log + # the output to the console twice. + # + # If show_stdout=True, then the above is still done, but with DEBUG + # replaced by INFO. + if show_stdout: + # Then log the subprocess output at INFO level. + log_subprocess = subprocess_logger.info + used_level = logging.INFO + else: + # Then log the subprocess output using DEBUG. This also ensures + # it will be logged to the log file (aka user_log), if enabled. + log_subprocess = subprocess_logger.debug + used_level = logging.DEBUG + + # Whether the subprocess will be visible in the console. + showing_subprocess = subprocess_logger.getEffectiveLevel() <= used_level + + command_desc = format_command_args(cmd) + try: + proc = subprocess.Popen( + # Convert HiddenText objects to the underlying str. + reveal_command_args(cmd), + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + cwd=cwd + ) + if proc.stdin: + proc.stdin.close() + except Exception as exc: + if log_failed_cmd: + subprocess_logger.critical( + "Error %s while executing command %s", exc, command_desc, + ) + raise + all_output = [] + while True: + # The "line" value is a unicode string in Python 2. + line = None + if proc.stdout: + line = console_to_str(proc.stdout.readline()) + if not line: + break + line = line.rstrip() + all_output.append(line + '\n') + + # Show the line immediately. + log_subprocess(line) + try: + proc.wait() + finally: + if proc.stdout: + proc.stdout.close() + + proc_had_error = ( + proc.returncode and proc.returncode not in extra_ok_returncodes + ) + if proc_had_error: + if on_returncode == 'raise': + if not showing_subprocess and log_failed_cmd: + # Then the subprocess streams haven't been logged to the + # console yet. + msg = make_subprocess_output_error( + cmd_args=cmd, + cwd=cwd, + lines=all_output, + exit_status=proc.returncode, + ) + subprocess_logger.error(msg) + exc_msg = ( + 'Command errored out with exit status {}: {} ' + 'Check the logs for full command output.' + ).format(proc.returncode, command_desc) + raise SubProcessError(exc_msg) + elif on_returncode == 'warn': + subprocess_logger.warning( + 'Command "{}" had error code {} in {}'.format( + command_desc, proc.returncode, cwd) + ) + elif on_returncode == 'ignore': + pass + else: + raise ValueError('Invalid value: on_returncode={!r}'.format( + on_returncode)) + return ''.join(all_output) + + def find_path_to_setup_from_repo_root(location, repo_root): # type: (str, str) -> Optional[str] """ @@ -663,9 +790,6 @@ def run_command( cwd=None, # type: Optional[str] on_returncode='raise', # type: str extra_ok_returncodes=None, # type: Optional[Iterable[int]] - command_desc=None, # type: Optional[str] - extra_environ=None, # type: Optional[Mapping[str, Any]] - spinner=None, # type: Optional[SpinnerInterface] log_failed_cmd=True # type: bool ): # type: (...) -> Text @@ -679,10 +803,6 @@ def run_command( return call_subprocess(cmd, show_stdout, cwd, on_returncode=on_returncode, extra_ok_returncodes=extra_ok_returncodes, - command_desc=command_desc, - extra_environ=extra_environ, - unset_environ=cls.unset_environ, - spinner=spinner, log_failed_cmd=log_failed_cmd) except OSError as e: # errno.ENOENT = no such file or directory diff --git a/tests/unit/test_vcs.py b/tests/unit/test_vcs.py index 166585db385..590cb5c0b75 100644 --- a/tests/unit/test_vcs.py +++ b/tests/unit/test_vcs.py @@ -443,18 +443,6 @@ def test_subversion__call_vcs_version(): ('svn, version 1.10.3 (r1842928)\n' ' compiled Feb 25 2019, 14:20:39 on x86_64-apple-darwin17.0.0', (1, 10, 3)), - ('Warning: Failed to set locale category LC_NUMERIC to en_IN.\n' - 'Warning: Failed to set locale category LC_TIME to en_IN.\n' - 'svn, version 1.10.3 (r1842928)\n' - ' compiled Feb 25 2019, 14:20:39 on x86_64-apple-darwin17.0.0', - (1, 10, 3)), - ('Warning: Failed to set locale category LC_NUMERIC to en_IN.\n' - 'Warning: Failed to set locale category LC_TIME to en_IN.\n' - 'svn, version 1.10.3 (r1842928)\n' - ' compiled Feb 25 2019, 14:20:39 on x86_64-apple-darwin17.0.0' - 'svn, version 1.11.3 (r1842928)\n' - ' compiled Feb 25 2019, 14:20:39 on x86_64-apple-darwin17.0.0', - (1, 10, 3)), ('svn, version 1.9.7 (r1800392)', (1, 9, 7)), ('svn, version 1.9.7a1 (r1800392)', ()), ('svn, version 1.9 (r1800392)', (1, 9)), From 94882fd1ed9171ea5a2f4b8904dbd8763f05ba68 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sat, 18 Apr 2020 14:10:45 +0530 Subject: [PATCH 1927/3170] Remove show_stdout from run_command args --- src/pip/_internal/vcs/bazaar.py | 7 ++-- src/pip/_internal/vcs/git.py | 19 +++++------ src/pip/_internal/vcs/mercurial.py | 16 ++++----- src/pip/_internal/vcs/subversion.py | 9 +++-- src/pip/_internal/vcs/versioncontrol.py | 44 ++++++++----------------- 5 files changed, 36 insertions(+), 59 deletions(-) diff --git a/src/pip/_internal/vcs/bazaar.py b/src/pip/_internal/vcs/bazaar.py index 347c06f9dc7..94408c52fa9 100644 --- a/src/pip/_internal/vcs/bazaar.py +++ b/src/pip/_internal/vcs/bazaar.py @@ -54,8 +54,7 @@ def export(self, location, url): url, rev_options = self.get_url_rev_options(url) self.run_command( - make_command('export', location, url, rev_options.to_args()), - show_stdout=False, + make_command('export', location, url, rev_options.to_args()) ) def fetch_new(self, dest, url, rev_options): @@ -92,7 +91,7 @@ def get_url_rev_and_auth(cls, url): @classmethod def get_remote_url(cls, location): - urls = cls.run_command(['info'], show_stdout=False, cwd=location) + urls = cls.run_command(['info'], cwd=location) for line in urls.splitlines(): line = line.strip() for x in ('checkout of branch: ', @@ -107,7 +106,7 @@ def get_remote_url(cls, location): @classmethod def get_revision(cls, location): revision = cls.run_command( - ['revno'], show_stdout=False, cwd=location, + ['revno'], cwd=location, ) return revision.splitlines()[-1] diff --git a/src/pip/_internal/vcs/git.py b/src/pip/_internal/vcs/git.py index e173ec894ca..61d0ff647b7 100644 --- a/src/pip/_internal/vcs/git.py +++ b/src/pip/_internal/vcs/git.py @@ -11,7 +11,7 @@ from pip._vendor.six.moves.urllib import parse as urllib_parse from pip._vendor.six.moves.urllib import request as urllib_request -from pip._internal.exceptions import BadCommand, InstallationError +from pip._internal.exceptions import BadCommand, SubProcessError from pip._internal.utils.misc import display_path, hide_url from pip._internal.utils.subprocess import make_command from pip._internal.utils.temp_dir import TempDirectory @@ -78,7 +78,7 @@ def is_immutable_rev_checkout(self, url, dest): def get_git_version(self): VERSION_PFX = 'git version ' - version = self.run_command(['version'], show_stdout=False) + version = self.run_command(['version']) if version.startswith(VERSION_PFX): version = version[len(VERSION_PFX):].split()[0] else: @@ -101,7 +101,7 @@ def get_current_branch(cls, location): # and to suppress the message to stderr. args = ['symbolic-ref', '-q', 'HEAD'] output = cls.run_command( - args, extra_ok_returncodes=(1, ), show_stdout=False, cwd=location, + args, extra_ok_returncodes=(1, ), cwd=location, ) ref = output.strip() @@ -120,7 +120,7 @@ def export(self, location, url): self.unpack(temp_dir.path, url=url) self.run_command( ['checkout-index', '-a', '-f', '--prefix', location], - show_stdout=False, cwd=temp_dir.path + cwd=temp_dir.path ) @classmethod @@ -135,7 +135,7 @@ def get_revision_sha(cls, dest, rev): """ # Pass rev to pre-filter the list. output = cls.run_command(['show-ref', rev], cwd=dest, - show_stdout=False, on_returncode='ignore') + on_returncode='ignore') refs = {} for line in output.strip().splitlines(): try: @@ -286,7 +286,7 @@ def get_remote_url(cls, location): # exits with return code 1 if there are no matching lines. stdout = cls.run_command( ['config', '--get-regexp', r'remote\..*\.url'], - extra_ok_returncodes=(1, ), show_stdout=False, cwd=location, + extra_ok_returncodes=(1, ), cwd=location, ) remotes = stdout.splitlines() try: @@ -306,7 +306,7 @@ def get_revision(cls, location, rev=None): if rev is None: rev = 'HEAD' current_rev = cls.run_command( - ['rev-parse', rev], show_stdout=False, cwd=location, + ['rev-parse', rev], cwd=location, ) return current_rev.strip() @@ -319,7 +319,7 @@ def get_subdirectory(cls, location): # find the repo root git_dir = cls.run_command( ['rev-parse', '--git-dir'], - show_stdout=False, cwd=location).strip() + cwd=location).strip() if not os.path.isabs(git_dir): git_dir = os.path.join(location, git_dir) repo_root = os.path.abspath(os.path.join(git_dir, '..')) @@ -378,7 +378,6 @@ def get_repository_root(cls, location): r = cls.run_command( ['rev-parse', '--show-toplevel'], cwd=location, - show_stdout=False, on_returncode='raise', log_failed_cmd=False, ) @@ -386,7 +385,7 @@ def get_repository_root(cls, location): logger.debug("could not determine if %s is under git control " "because git is not available", location) return None - except InstallationError: + except SubProcessError: return None return os.path.normpath(r.rstrip('\r\n')) diff --git a/src/pip/_internal/vcs/mercurial.py b/src/pip/_internal/vcs/mercurial.py index 75e903cc8a6..b7f8073fd38 100644 --- a/src/pip/_internal/vcs/mercurial.py +++ b/src/pip/_internal/vcs/mercurial.py @@ -8,7 +8,7 @@ from pip._vendor.six.moves import configparser -from pip._internal.exceptions import BadCommand, InstallationError +from pip._internal.exceptions import BadCommand, SubProcessError from pip._internal.utils.misc import display_path from pip._internal.utils.subprocess import make_command from pip._internal.utils.temp_dir import TempDirectory @@ -47,7 +47,7 @@ def export(self, location, url): self.unpack(temp_dir.path, url=url) self.run_command( - ['archive', location], show_stdout=False, cwd=temp_dir.path + ['archive', location], cwd=temp_dir.path ) def fetch_new(self, dest, url, rev_options): @@ -92,7 +92,7 @@ def update(self, dest, url, rev_options): def get_remote_url(cls, location): url = cls.run_command( ['showconfig', 'paths.default'], - show_stdout=False, cwd=location).strip() + cwd=location).strip() if cls._is_local_repository(url): url = path_to_url(url) return url.strip() @@ -103,8 +103,7 @@ def get_revision(cls, location): Return the repository-local changeset revision number, as an integer. """ current_revision = cls.run_command( - ['parents', '--template={rev}'], - show_stdout=False, cwd=location).strip() + ['parents', '--template={rev}'], cwd=location).strip() return current_revision @classmethod @@ -115,7 +114,7 @@ def get_requirement_revision(cls, location): """ current_rev_hash = cls.run_command( ['parents', '--template={node}'], - show_stdout=False, cwd=location).strip() + cwd=location).strip() return current_rev_hash @classmethod @@ -131,7 +130,7 @@ def get_subdirectory(cls, location): """ # find the repo root repo_root = cls.run_command( - ['root'], show_stdout=False, cwd=location).strip() + ['root'], cwd=location).strip() if not os.path.isabs(repo_root): repo_root = os.path.abspath(os.path.join(location, repo_root)) return find_path_to_setup_from_repo_root(location, repo_root) @@ -145,7 +144,6 @@ def get_repository_root(cls, location): r = cls.run_command( ['root'], cwd=location, - show_stdout=False, on_returncode='raise', log_failed_cmd=False, ) @@ -153,7 +151,7 @@ def get_repository_root(cls, location): logger.debug("could not determine if %s is under hg control " "because hg is not available", location) return None - except InstallationError: + except SubProcessError: return None return os.path.normpath(r.rstrip('\r\n')) diff --git a/src/pip/_internal/vcs/subversion.py b/src/pip/_internal/vcs/subversion.py index 3f9d0833fa9..4324a5d9f82 100644 --- a/src/pip/_internal/vcs/subversion.py +++ b/src/pip/_internal/vcs/subversion.py @@ -132,7 +132,7 @@ def get_remote_url(cls, location): @classmethod def _get_svn_url_rev(cls, location): - from pip._internal.exceptions import InstallationError + from pip._internal.exceptions import SubProcessError entries_path = os.path.join(location, cls.dirname, 'entries') if os.path.exists(entries_path): @@ -165,13 +165,12 @@ def _get_svn_url_rev(cls, location): # are only potentially needed for remote server requests. xml = cls.run_command( ['info', '--xml', location], - show_stdout=False, ) url = _svn_info_xml_url_re.search(xml).group(1) revs = [ int(m.group(1)) for m in _svn_info_xml_rev_re.finditer(xml) ] - except InstallationError: + except SubProcessError: url, revs = None, [] if revs: @@ -215,7 +214,7 @@ def call_vcs_version(self): # svn, version 1.7.14 (r1542130) # compiled Mar 28 2018, 08:49:13 on x86_64-pc-linux-gnu version_prefix = 'svn, version ' - version = self.run_command(['--version'], show_stdout=True) + version = self.run_command(['--version']) if not version.startswith(version_prefix): return () @@ -298,7 +297,7 @@ def export(self, location, url): 'export', self.get_remote_call_options(), rev_options.to_args(), url, location, ) - self.run_command(cmd_args, show_stdout=False) + self.run_command(cmd_args) def fetch_new(self, dest, url, rev_options): # type: (str, HiddenText, RevOptions) -> None diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index 1956559b397..02bdda37c96 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -39,7 +39,7 @@ if MYPY_CHECK_RUNNING: from typing import ( Dict, Iterable, Iterator, List, Optional, Text, Tuple, - Type, Union + Type, Union, Mapping, Any ) from pip._internal.utils.misc import HiddenText from pip._internal.utils.subprocess import CommandArgs @@ -83,17 +83,15 @@ def make_vcs_requirement_url(repo_url, rev, project_name, subdir=None): def call_subprocess( cmd, # type: Union[List[str], CommandArgs] - show_stdout=False, # type: bool cwd=None, # type: Optional[str] on_returncode='raise', # type: str + extra_environ=None, # type: Optional[Mapping[str, Any]] extra_ok_returncodes=None, # type: Optional[Iterable[int]] log_failed_cmd=True # type: Optional[bool] ): # type: (...) -> Text """ Args: - show_stdout: if true, use INFO to log the subprocess's stderr and - stdout streams. Otherwise, use DEBUG. Defaults to False. extra_ok_returncodes: an iterable of integer return codes that are acceptable, in addition to 0. Defaults to None, which means []. log_failed_cmd: if false, failed commands are not logged, @@ -101,33 +99,16 @@ def call_subprocess( """ if extra_ok_returncodes is None: extra_ok_returncodes = [] - # Most places in pip use show_stdout=False. - # What this means is-- - # - # - We log this output of stdout and stderr at DEBUG level - # as it is received. - # - If DEBUG logging isn't enabled (e.g. if --verbose logging wasn't - # requested), then we show a spinner so the user can still see the - # subprocess is in progress. - # - If the subprocess exits with an error, we log the output to stderr - # at ERROR level if it hasn't already been displayed to the console - # (e.g. if --verbose logging wasn't enabled). This way we don't log - # the output to the console twice. - # - # If show_stdout=True, then the above is still done, but with DEBUG - # replaced by INFO. - if show_stdout: - # Then log the subprocess output at INFO level. - log_subprocess = subprocess_logger.info - used_level = logging.INFO - else: - # Then log the subprocess output using DEBUG. This also ensures - # it will be logged to the log file (aka user_log), if enabled. - log_subprocess = subprocess_logger.debug - used_level = logging.DEBUG + + # log the subprocess output at DEBUG level. + log_subprocess = subprocess_logger.debug + + env = os.environ.copy() + if extra_environ: + env.update(extra_environ) # Whether the subprocess will be visible in the console. - showing_subprocess = subprocess_logger.getEffectiveLevel() <= used_level + showing_subprocess = True command_desc = format_command_args(cmd) try: @@ -786,9 +767,9 @@ def get_revision(cls, location): def run_command( cls, cmd, # type: Union[List[str], CommandArgs] - show_stdout=True, # type: bool cwd=None, # type: Optional[str] on_returncode='raise', # type: str + extra_environ=None, # type: Optional[Mapping[str, Any]] extra_ok_returncodes=None, # type: Optional[Iterable[int]] log_failed_cmd=True # type: bool ): @@ -800,8 +781,9 @@ def run_command( """ cmd = make_command(cls.name, *cmd) try: - return call_subprocess(cmd, show_stdout, cwd, + return call_subprocess(cmd, cwd, on_returncode=on_returncode, + extra_environ=extra_environ, extra_ok_returncodes=extra_ok_returncodes, log_failed_cmd=log_failed_cmd) except OSError as e: From ab3ee7191ca47294f8827916180969e23f5e0381 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Thu, 14 May 2020 20:06:36 +0530 Subject: [PATCH 1928/3170] Remove on_returncode parameter from call_subprocess --- src/pip/_internal/vcs/git.py | 10 ++++-- src/pip/_internal/vcs/mercurial.py | 1 - src/pip/_internal/vcs/versioncontrol.py | 42 +++++++++---------------- 3 files changed, 21 insertions(+), 32 deletions(-) diff --git a/src/pip/_internal/vcs/git.py b/src/pip/_internal/vcs/git.py index 61d0ff647b7..a9c7fb66e33 100644 --- a/src/pip/_internal/vcs/git.py +++ b/src/pip/_internal/vcs/git.py @@ -134,8 +134,13 @@ def get_revision_sha(cls, dest, rev): rev: the revision name. """ # Pass rev to pre-filter the list. - output = cls.run_command(['show-ref', rev], cwd=dest, - on_returncode='ignore') + + output = '' + try: + output = cls.run_command(['show-ref', rev], cwd=dest) + except SubProcessError: + pass + refs = {} for line in output.strip().splitlines(): try: @@ -378,7 +383,6 @@ def get_repository_root(cls, location): r = cls.run_command( ['rev-parse', '--show-toplevel'], cwd=location, - on_returncode='raise', log_failed_cmd=False, ) except BadCommand: diff --git a/src/pip/_internal/vcs/mercurial.py b/src/pip/_internal/vcs/mercurial.py index b7f8073fd38..69763feaea4 100644 --- a/src/pip/_internal/vcs/mercurial.py +++ b/src/pip/_internal/vcs/mercurial.py @@ -144,7 +144,6 @@ def get_repository_root(cls, location): r = cls.run_command( ['root'], cwd=location, - on_returncode='raise', log_failed_cmd=False, ) except BadCommand: diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index 02bdda37c96..96f830f9918 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -84,7 +84,6 @@ def make_vcs_requirement_url(repo_url, rev, project_name, subdir=None): def call_subprocess( cmd, # type: Union[List[str], CommandArgs] cwd=None, # type: Optional[str] - on_returncode='raise', # type: str extra_environ=None, # type: Optional[Mapping[str, Any]] extra_ok_returncodes=None, # type: Optional[Iterable[int]] log_failed_cmd=True # type: Optional[bool] @@ -150,32 +149,21 @@ def call_subprocess( proc.returncode and proc.returncode not in extra_ok_returncodes ) if proc_had_error: - if on_returncode == 'raise': - if not showing_subprocess and log_failed_cmd: - # Then the subprocess streams haven't been logged to the - # console yet. - msg = make_subprocess_output_error( - cmd_args=cmd, - cwd=cwd, - lines=all_output, - exit_status=proc.returncode, - ) - subprocess_logger.error(msg) - exc_msg = ( - 'Command errored out with exit status {}: {} ' - 'Check the logs for full command output.' - ).format(proc.returncode, command_desc) - raise SubProcessError(exc_msg) - elif on_returncode == 'warn': - subprocess_logger.warning( - 'Command "{}" had error code {} in {}'.format( - command_desc, proc.returncode, cwd) + if not showing_subprocess and log_failed_cmd: + # Then the subprocess streams haven't been logged to the + # console yet. + msg = make_subprocess_output_error( + cmd_args=cmd, + cwd=cwd, + lines=all_output, + exit_status=proc.returncode, ) - elif on_returncode == 'ignore': - pass - else: - raise ValueError('Invalid value: on_returncode={!r}'.format( - on_returncode)) + subprocess_logger.error(msg) + exc_msg = ( + 'Command errored out with exit status {}: {} ' + 'Check the logs for full command output.' + ).format(proc.returncode, command_desc) + raise SubProcessError(exc_msg) return ''.join(all_output) @@ -768,7 +756,6 @@ def run_command( cls, cmd, # type: Union[List[str], CommandArgs] cwd=None, # type: Optional[str] - on_returncode='raise', # type: str extra_environ=None, # type: Optional[Mapping[str, Any]] extra_ok_returncodes=None, # type: Optional[Iterable[int]] log_failed_cmd=True # type: bool @@ -782,7 +769,6 @@ def run_command( cmd = make_command(cls.name, *cmd) try: return call_subprocess(cmd, cwd, - on_returncode=on_returncode, extra_environ=extra_environ, extra_ok_returncodes=extra_ok_returncodes, log_failed_cmd=log_failed_cmd) From e9f738a3daec91b131ae985e16809d47b1cfdaff Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Fri, 15 May 2020 09:38:53 +0530 Subject: [PATCH 1929/3170] Bubble up SubProcessError to basecommand._main --- src/pip/_internal/cli/base_command.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index 535a491625f..c52ffa2f267 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -28,6 +28,7 @@ CommandError, InstallationError, PreviousBuildDirError, + SubProcessError, UninstallationError, ) from pip._internal.utils.deprecation import deprecated @@ -201,7 +202,8 @@ def _main(self, args): logger.debug('Exception information:', exc_info=True) return PREVIOUS_BUILD_DIR_ERROR - except (InstallationError, UninstallationError, BadCommand) as exc: + except (InstallationError, UninstallationError, BadCommand, + SubProcessError) as exc: logger.critical(str(exc)) logger.debug('Exception information:', exc_info=True) From 5c615aa775e343dfe7b97bdfc5cb826fa5691061 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Fri, 15 May 2020 12:07:03 +0530 Subject: [PATCH 1930/3170] Update news entry --- news/7968.bugfix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/7968.bugfix b/news/7968.bugfix index d6959730ab3..36b282fc821 100644 --- a/news/7968.bugfix +++ b/news/7968.bugfix @@ -1 +1 @@ -Look for version string in the entire output of svn --version, not just the first line +The VCS commands run by pip as subprocesses don't merge stdout and stderr anymore, improving the output parsing by subsequent commands. From 99660b8d86029c5719b5e6a10c0f3b41b7a8dc76 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sat, 23 May 2020 19:27:32 +0530 Subject: [PATCH 1931/3170] Fix pip cache docstring to render correctly in docs --- news/598F8551-DB46-4A12-987E-094EF18DAF7C.trivial | 0 src/pip/_internal/commands/cache.py | 12 ++++++------ 2 files changed, 6 insertions(+), 6 deletions(-) create mode 100644 news/598F8551-DB46-4A12-987E-094EF18DAF7C.trivial diff --git a/news/598F8551-DB46-4A12-987E-094EF18DAF7C.trivial b/news/598F8551-DB46-4A12-987E-094EF18DAF7C.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index ca6d4379be3..5de19a16ef3 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -24,13 +24,13 @@ class CacheCommand(Command): Subcommands: - dir: Show the cache directory. - info: Show information about the cache. - list: List filenames of packages stored in the cache. - remove: Remove one or more package from the cache. - purge: Remove all items from the cache. + - dir: Show the cache directory. + - info: Show information about the cache. + - list: List filenames of packages stored in the cache. + - remove: Remove one or more package from the cache. + - purge: Remove all items from the cache. - <pattern> can be a glob expression or a package name. + <pattern> can be a glob expression or a package name. """ ignore_require_venv = True From f9b1a54009455268ef18007e8d55d887c8a7b596 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sat, 23 May 2020 19:32:27 +0530 Subject: [PATCH 1932/3170] Apply suggestion from review comments --- src/pip/_internal/commands/cache.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index 5de19a16ef3..209614ff6d4 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -30,7 +30,7 @@ class CacheCommand(Command): - remove: Remove one or more package from the cache. - purge: Remove all items from the cache. - <pattern> can be a glob expression or a package name. + ``<pattern>`` can be a glob expression or a package name. """ ignore_require_venv = True From ea3b0ab9a925a31788e28f3f8d888d4113d62771 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Mu=C3=B1oz?= <hi@xmunoz.com> Date: Sun, 24 May 2020 17:57:28 -0700 Subject: [PATCH 1933/3170] Add missing class attribute --- src/pip/_internal/models/link.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pip/_internal/models/link.py b/src/pip/_internal/models/link.py index a54f37d2fcf..c0d278adee9 100644 --- a/src/pip/_internal/models/link.py +++ b/src/pip/_internal/models/link.py @@ -30,6 +30,7 @@ class Link(KeyBasedCompareMixin): "comes_from", "requires_python", "yanked_reason", + "cache_link_parsing", ] def __init__( From 9238fc3f8464f80859f9f5e6fb8dfa6697b71d36 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 22 May 2020 18:25:19 +0800 Subject: [PATCH 1934/3170] Disallow failures for tests we expect to pass --- .travis.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 8d84f958161..5037174f408 100644 --- a/.travis.yml +++ b/.travis.yml @@ -47,7 +47,7 @@ jobs: fast_finish: true allow_failures: - - stage: experimental + - env: GROUP=3 NEW_RESOLVER=1 before_install: tools/travis/setup.sh install: travis_retry tools/travis/install.sh From 5f2bc2b47cf6bcb5c557b7d19e8aeb0fbfe99600 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Mon, 25 May 2020 14:09:26 +0800 Subject: [PATCH 1935/3170] DO NOT sort the returned matches by version A higher version is not always preferred over the lower; the user may be explicitly preferring lower versions by specifying --prefer-binary or similar flags. PackageFinder already takes these into account for these and orders the matches. Don't break it. --- .../_internal/resolution/resolvelib/provider.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index e4c516948c4..e9a41f04fc9 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -6,8 +6,6 @@ if MYPY_CHECK_RUNNING: from typing import Any, Dict, Optional, Sequence, Set, Tuple, Union - from pip._vendor.packaging.version import _BaseVersion - from .base import Requirement, Candidate from .factory import Factory @@ -90,17 +88,22 @@ def _eligible_for_upgrade(name): return False def sort_key(c): - # type: (Candidate) -> Tuple[int, _BaseVersion] + # type: (Candidate) -> int """Return a sort key for the matches. The highest priority should be given to installed candidates that are not eligible for upgrade. We use the integer value in the first part of the key to sort these before other candidates. + + We only pull the installed candidate to the bottom (i.e. most + preferred), but otherwise keep the ordering returned by the + requirement. The requirement is responsible for returning a list + otherwise sorted for the resolver, taking account for versions + and binary preferences as specified by the user. """ if c.is_installed and not _eligible_for_upgrade(c.name): - return (1, c.version) - - return (0, c.version) + return 1 + return 0 return sorted(matches, key=sort_key) From 0e4dd69759a90c7b66e603ec001f638738bf2d26 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Mon, 25 May 2020 20:39:43 +0800 Subject: [PATCH 1936/3170] Also fixed this test --- tests/functional/test_download.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/functional/test_download.py b/tests/functional/test_download.py index 83e2aae265f..e5bb89b7be5 100644 --- a/tests/functional/test_download.py +++ b/tests/functional/test_download.py @@ -669,7 +669,6 @@ def test_download_exit_status_code_when_blank_requirements_file(script): script.pip('download', '-r', 'blank.txt') -@pytest.mark.fails_on_new_resolver def test_download_prefer_binary_when_tarball_higher_than_wheel(script, data): fake_wheel(data, 'source-0.8-py2.py3-none-any.whl') result = script.pip( From d4dd94aa4fc3e55e265ef1fcbbe554fcc45dc219 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Mu=C3=B1oz?= <hi@xmunoz.com> Date: Mon, 25 May 2020 16:18:38 -0700 Subject: [PATCH 1937/3170] fix lint --- src/pip/_internal/models/format_control.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pip/_internal/models/format_control.py b/src/pip/_internal/models/format_control.py index d66b5e4703a..3257a894f62 100644 --- a/src/pip/_internal/models/format_control.py +++ b/src/pip/_internal/models/format_control.py @@ -1,4 +1,5 @@ import operator + from pip._vendor.packaging.utils import canonicalize_name from pip._internal.exceptions import CommandError From fa06ccf05410e4f778a062085e973160b584f50f Mon Sep 17 00:00:00 2001 From: gutsytechster <prashantsharma161198@gmail.com> Date: Tue, 26 May 2020 20:13:28 +0530 Subject: [PATCH 1938/3170] tests(test_install_{reqs, upgrade}): add methods for path lookups --- tests/functional/test_install_reqs.py | 51 +++++++++++------------- tests/functional/test_install_upgrade.py | 41 ++++++++++--------- 2 files changed, 43 insertions(+), 49 deletions(-) diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index d39263b98ea..708f3b7aac2 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -29,11 +29,10 @@ def test_requirements_file(script): result = script.pip( 'install', '-r', script.scratch_path / 'initools-req.txt' ) - assert ( - script.site_packages / 'INITools-0.2-py{}.egg-info'.format( - pyversion) in result.files_created + result.did_create( + script.site_packages / 'INITools-0.2-py{}.egg-info'.format(pyversion) ) - assert script.site_packages / 'initools' in result.files_created + result.did_create(script.site_packages / 'initools') assert result.files_created[script.site_packages / other_lib_name].dir fn = '{}-{}-py{}.egg-info'.format( other_lib_name, other_lib_version, pyversion) @@ -101,14 +100,14 @@ def test_relative_requirements_file(script, data, test_type, editable): script.scratch_path) as reqs_file: result = script.pip('install', '-vvv', '-r', reqs_file.name, cwd=script.scratch_path) - assert egg_info_file in result.files_created, str(result) - assert package_folder in result.files_created, str(result) + result.did_create(egg_info_file) + result.did_create(package_folder) else: with requirements_file('-e ' + req_path + '\n', script.scratch_path) as reqs_file: result = script.pip('install', '-vvv', '-r', reqs_file.name, cwd=script.scratch_path) - assert egg_link_file in result.files_created, str(result) + result.did_create(egg_link_file) @pytest.mark.network @@ -140,7 +139,7 @@ def test_multiple_requirements_files(script, tmpdir): fn = '{other_lib_name}-{other_lib_version}-py{pyversion}.egg-info'.format( pyversion=pyversion, **locals()) assert result.files_created[script.site_packages / fn].dir - assert script.venv / 'src' / 'initools' in result.files_created + result.did_create(script.venv / 'src' / 'initools') def test_package_in_constraints_and_dependencies(script, data): @@ -195,13 +194,9 @@ def test_install_local_editable_with_extras(script, data): res = script.pip_install_local( '-e', to_install + '[bar]', allow_stderr_warning=True ) - assert script.site_packages / 'easy-install.pth' in res.files_updated, ( - str(res) - ) - assert ( - script.site_packages / 'LocalExtras.egg-link' in res.files_created - ), str(res) - assert script.site_packages / 'simple' in res.files_created, str(res) + res.did_update(script.site_packages / 'easy-install.pth') + res.did_create(script.site_packages / 'LocalExtras.egg-link') + res.did_create(script.site_packages / 'simple') def test_install_collected_dependencies_first(script): @@ -285,7 +280,7 @@ def test_install_option_in_requirements_file(script, data, virtualenv): expect_stderr=True) package_dir = script.scratch / 'home1' / 'lib' / 'python' / 'simple' - assert package_dir in result.files_created + result.did_create(package_dir) def test_constraints_not_installed_by_default(script, data): @@ -415,7 +410,7 @@ def test_install_with_extras_from_constraints(script, data): ) result = script.pip_install_local( '-c', script.scratch_path / 'constraints.txt', 'LocalExtras') - assert script.site_packages / 'simple' in result.files_created + result.did_create(script.site_packages / 'simple') @pytest.mark.fails_on_new_resolver @@ -426,7 +421,7 @@ def test_install_with_extras_from_install(script, data): ) result = script.pip_install_local( '-c', script.scratch_path / 'constraints.txt', 'LocalExtras[baz]') - assert script.site_packages / 'singlemodule.py' in result.files_created + result.did_create(script.site_packages / 'singlemodule.py') @pytest.mark.fails_on_new_resolver @@ -438,8 +433,8 @@ def test_install_with_extras_joined(script, data): result = script.pip_install_local( '-c', script.scratch_path / 'constraints.txt', 'LocalExtras[baz]' ) - assert script.site_packages / 'simple' in result.files_created - assert script.site_packages / 'singlemodule.py' in result.files_created + result.did_create(script.site_packages / 'simple') + result.did_create(script.site_packages / 'singlemodule.py') @pytest.mark.fails_on_new_resolver @@ -450,8 +445,8 @@ def test_install_with_extras_editable_joined(script, data): ) result = script.pip_install_local( '-c', script.scratch_path / 'constraints.txt', 'LocalExtras[baz]') - assert script.site_packages / 'simple' in result.files_created - assert script.site_packages / 'singlemodule.py' in result.files_created + result.did_create(script.site_packages / 'simple') + result.did_create(script.site_packages / 'singlemodule.py') def test_install_distribution_full_union(script, data): @@ -459,8 +454,8 @@ def test_install_distribution_full_union(script, data): result = script.pip_install_local( to_install, to_install + "[bar]", to_install + "[baz]") assert 'Running setup.py install for LocalExtras' in result.stdout - assert script.site_packages / 'simple' in result.files_created - assert script.site_packages / 'singlemodule.py' in result.files_created + result.did_create(script.site_packages / 'simple') + result.did_create(script.site_packages / 'singlemodule.py') def test_install_distribution_duplicate_extras(script, data): @@ -481,7 +476,7 @@ def test_install_distribution_union_with_constraints(script, data): result = script.pip_install_local( '-c', script.scratch_path / 'constraints.txt', to_install + '[baz]') assert 'Running setup.py install for LocalExtras' in result.stdout - assert script.site_packages / 'singlemodule.py' in result.files_created + result.did_create(script.site_packages / 'singlemodule.py') @pytest.mark.fails_on_new_resolver @@ -571,8 +566,8 @@ def test_install_options_local_to_package(script, data): simple = test_simple / 'lib' / 'python' / 'simple' bad = test_simple / 'lib' / 'python' / 'initools' good = script.site_packages / 'initools' - assert simple in result.files_created + result.did_create(simple) assert result.files_created[simple].dir - assert bad not in result.files_created - assert good in result.files_created + result.did_not_create(bad) + result.did_create(good) assert result.files_created[good].dir diff --git a/tests/functional/test_install_upgrade.py b/tests/functional/test_install_upgrade.py index 604d2afa812..ce5b15dbd52 100644 --- a/tests/functional/test_install_upgrade.py +++ b/tests/functional/test_install_upgrade.py @@ -82,9 +82,7 @@ def test_only_if_needed_does_upgrade_deps_when_no_longer_satisfied(script): script.site_packages / 'simple-3.0-py{pyversion}.egg-info'.format(**globals()) ) - assert ( - expected in result.files_created - ), "should have installed simple==3.0" + result.did_create(expected, "should have installed simple==3.0") expected = ( script.site_packages / 'simple-1.0-py{pyversion}.egg-info'.format(**globals()) @@ -131,11 +129,11 @@ def test_eager_does_upgrade_dependecies_when_no_longer_satisfied(script): 'require_simple-1.0-py{pyversion}.egg-info'.format(**globals())) not in result.files_deleted ), "should have installed require_simple==1.0" - assert ( + result.did_create( script.site_packages / - 'simple-3.0-py{pyversion}.egg-info'.format(**globals()) - in result.files_created - ), "should have installed simple==3.0" + 'simple-3.0-py{pyversion}.egg-info'.format(**globals()), + "should have installed simple==3.0" + ) assert ( script.site_packages / 'simple-1.0-py{pyversion}.egg-info'.format(**globals()) @@ -159,10 +157,9 @@ def test_upgrade_to_specific_version(script): .format(**globals()) in result.files_deleted ) - assert ( + result.did_create( script.site_packages / 'INITools-0.2-py{pyversion}.egg-info' .format(**globals()) - in result.files_created ) @@ -175,10 +172,9 @@ def test_upgrade_if_requested(script): script.pip('install', 'INITools==0.1') result = script.pip('install', '--upgrade', 'INITools') assert result.files_created, 'pip install --upgrade did not upgrade' - assert ( + result.did_not_create( script.site_packages / 'INITools-0.1-py{pyversion}.egg-info'.format(**globals()) - not in result.files_created ) @@ -203,7 +199,8 @@ def test_upgrade_force_reinstall_newest(script): version if --force-reinstall is supplied. """ result = script.pip('install', 'INITools') - assert script.site_packages / 'initools' in result.files_created, ( + result.did_create( + script.site_packages / 'initools', sorted(result.files_created.keys()) ) result2 = script.pip( @@ -221,7 +218,8 @@ def test_uninstall_before_upgrade(script): """ result = script.pip('install', 'INITools==0.2') - assert script.site_packages / 'initools' in result.files_created, ( + result.did_create( + script.site_packages / 'initools', sorted(result.files_created.keys()) ) result2 = script.pip('install', 'INITools==0.3') @@ -237,7 +235,8 @@ def test_uninstall_before_upgrade_from_url(script): """ result = script.pip('install', 'INITools==0.2') - assert script.site_packages / 'initools' in result.files_created, ( + result.did_create( + script.site_packages / 'initools', sorted(result.files_created.keys()) ) result2 = script.pip( @@ -259,7 +258,8 @@ def test_upgrade_to_same_version_from_url(script): """ result = script.pip('install', 'INITools==0.3') - assert script.site_packages / 'initools' in result.files_created, ( + result.did_create( + script.site_packages / 'initools', sorted(result.files_created.keys()) ) result2 = script.pip( @@ -315,8 +315,9 @@ def test_uninstall_rollback(script, data): result = script.pip( 'install', '-f', data.find_links, '--no-index', 'broken==0.1' ) - assert script.site_packages / 'broken.py' in result.files_created, list( - result.files_created.keys() + result.did_create( + script.site_packages / 'broken.py', + list(result.files_created.keys()) ) result2 = script.pip( 'install', '-f', data.find_links, '--no-index', 'broken===0.2broken', @@ -342,15 +343,13 @@ def test_should_not_install_always_from_cache(script): script.pip('install', 'INITools==0.2') script.pip('uninstall', '-y', 'INITools') result = script.pip('install', 'INITools==0.1') - assert ( + result.did_not_create( script.site_packages / 'INITools-0.2-py{pyversion}.egg-info'.format(**globals()) - not in result.files_created ) - assert ( + result.did_create( script.site_packages / 'INITools-0.1-py{pyversion}.egg-info'.format(**globals()) - in result.files_created ) From 1ef34692d6de6afd55fe3e4872f79aba5df20906 Mon Sep 17 00:00:00 2001 From: gutsytechster <prashantsharma161198@gmail.com> Date: Tue, 26 May 2020 20:21:28 +0530 Subject: [PATCH 1939/3170] news(*): Add trivial news file entry --- news/dc9b761d-5d4e-4ea4-9629-0afcc2636cb6.trivial | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/dc9b761d-5d4e-4ea4-9629-0afcc2636cb6.trivial diff --git a/news/dc9b761d-5d4e-4ea4-9629-0afcc2636cb6.trivial b/news/dc9b761d-5d4e-4ea4-9629-0afcc2636cb6.trivial new file mode 100644 index 00000000000..f23264777a4 --- /dev/null +++ b/news/dc9b761d-5d4e-4ea4-9629-0afcc2636cb6.trivial @@ -0,0 +1 @@ +Add methods for path lookups in ``test_install_reqs.py`` and ``test_install_upgrade.py``. From 732bf44f91d4d9bbcc1c2a21bf632437cfeaf659 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Mu=C3=B1oz?= <hi@xmunoz.com> Date: Tue, 26 May 2020 10:19:48 -0700 Subject: [PATCH 1940/3170] Remove Base class --- src/pip/_internal/models/format_control.py | 3 +-- src/pip/_internal/models/index.py | 4 +--- src/pip/_internal/models/scheme.py | 4 +--- src/pip/_internal/models/search_scope.py | 3 +-- src/pip/_internal/models/selection_prefs.py | 3 +-- src/pip/_internal/models/target_python.py | 3 +-- src/pip/_internal/utils/models.py | 9 +-------- 7 files changed, 7 insertions(+), 22 deletions(-) diff --git a/src/pip/_internal/models/format_control.py b/src/pip/_internal/models/format_control.py index 3257a894f62..b7c3ceb8ed2 100644 --- a/src/pip/_internal/models/format_control.py +++ b/src/pip/_internal/models/format_control.py @@ -3,14 +3,13 @@ from pip._vendor.packaging.utils import canonicalize_name from pip._internal.exceptions import CommandError -from pip._internal.utils.models import Base from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: from typing import Optional, Set, FrozenSet -class FormatControl(Base): +class FormatControl(object): """Helper for managing formats from which a package can be installed. """ diff --git a/src/pip/_internal/models/index.py b/src/pip/_internal/models/index.py index 28f5bf22503..5b4a1fe2274 100644 --- a/src/pip/_internal/models/index.py +++ b/src/pip/_internal/models/index.py @@ -1,9 +1,7 @@ from pip._vendor.six.moves.urllib import parse as urllib_parse -from pip._internal.utils.models import Base - -class PackageIndex(Base): +class PackageIndex(object): """Represents a Package Index and provides easier access to endpoints """ diff --git a/src/pip/_internal/models/scheme.py b/src/pip/_internal/models/scheme.py index 5e658481a1c..b9d0ea68a41 100644 --- a/src/pip/_internal/models/scheme.py +++ b/src/pip/_internal/models/scheme.py @@ -5,10 +5,8 @@ https://docs.python.org/3/install/index.html#alternate-installation. """ -from pip._internal.utils.models import Base - -class Scheme(Base): +class Scheme(object): """A Scheme holds paths which are used as the base directories for artifacts associated with a Python package. """ diff --git a/src/pip/_internal/models/search_scope.py b/src/pip/_internal/models/search_scope.py index b0b93c09b44..965fcf9c3ae 100644 --- a/src/pip/_internal/models/search_scope.py +++ b/src/pip/_internal/models/search_scope.py @@ -9,7 +9,6 @@ from pip._internal.models.index import PyPI from pip._internal.utils.compat import has_tls from pip._internal.utils.misc import normalize_path, redact_auth_from_url -from pip._internal.utils.models import Base from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: @@ -19,7 +18,7 @@ logger = logging.getLogger(__name__) -class SearchScope(Base): +class SearchScope(object): """ Encapsulates the locations that pip is configured to search. diff --git a/src/pip/_internal/models/selection_prefs.py b/src/pip/_internal/models/selection_prefs.py index 4b334652d9a..5db3ca91ca6 100644 --- a/src/pip/_internal/models/selection_prefs.py +++ b/src/pip/_internal/models/selection_prefs.py @@ -1,4 +1,3 @@ -from pip._internal.utils.models import Base from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: @@ -6,7 +5,7 @@ from pip._internal.models.format_control import FormatControl -class SelectionPreferences(Base): +class SelectionPreferences(object): """ Encapsulates the candidate selection preferences for downloading and installing files. diff --git a/src/pip/_internal/models/target_python.py b/src/pip/_internal/models/target_python.py index 901504d39a5..6d1ca79645f 100644 --- a/src/pip/_internal/models/target_python.py +++ b/src/pip/_internal/models/target_python.py @@ -5,7 +5,6 @@ version_info_to_nodot, ) from pip._internal.utils.misc import normalize_version_info -from pip._internal.utils.models import Base from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: @@ -14,7 +13,7 @@ from pip._vendor.packaging.tags import Tag -class TargetPython(Base): +class TargetPython(object): """ Encapsulates the properties of a Python interpreter one is targeting diff --git a/src/pip/_internal/utils/models.py b/src/pip/_internal/utils/models.py index 18390021930..71d5115e221 100644 --- a/src/pip/_internal/utils/models.py +++ b/src/pip/_internal/utils/models.py @@ -7,15 +7,8 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: - from typing import List - -class Base(object): - __slots__ = [] # type: List[str] - - -class KeyBasedCompareMixin(Base): +class KeyBasedCompareMixin(object): """Provides comparison capabilities that is based on a key """ From 330323bf8b1f4b450771dd912ed94340adc3f349 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Mu=C3=B1oz?= <hi@xmunoz.com> Date: Tue, 26 May 2020 11:12:57 -0700 Subject: [PATCH 1941/3170] Remove unneeded MYPY_CHECK_RUNNING import --- src/pip/_internal/utils/models.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/pip/_internal/utils/models.py b/src/pip/_internal/utils/models.py index 71d5115e221..d1c2f226796 100644 --- a/src/pip/_internal/utils/models.py +++ b/src/pip/_internal/utils/models.py @@ -5,8 +5,6 @@ import operator -from pip._internal.utils.typing import MYPY_CHECK_RUNNING - class KeyBasedCompareMixin(object): """Provides comparison capabilities that is based on a key From 57a0815dd8f48402d5d68b256e69aee2508843af Mon Sep 17 00:00:00 2001 From: Surbhi Sharma <ssurbhi560@gmail.com> Date: Wed, 27 May 2020 00:47:00 +0530 Subject: [PATCH 1942/3170] use methods for path lookups in files: test_install_{extras, index} --- ...7c7a0ae-e29b-4066-ab22-6784dbfc45af.trivial | 0 tests/functional/test_install_extras.py | 14 +++++--------- tests/functional/test_install_index.py | 18 ++++++++---------- 3 files changed, 13 insertions(+), 19 deletions(-) create mode 100644 news/47c7a0ae-e29b-4066-ab22-6784dbfc45af.trivial diff --git a/news/47c7a0ae-e29b-4066-ab22-6784dbfc45af.trivial b/news/47c7a0ae-e29b-4066-ab22-6784dbfc45af.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/functional/test_install_extras.py b/tests/functional/test_install_extras.py index dfde7d1b676..c39964ce779 100644 --- a/tests/functional/test_install_extras.py +++ b/tests/functional/test_install_extras.py @@ -14,7 +14,7 @@ def test_simple_extras_install_from_pypi(script): 'install', 'Paste[openid]==1.7.5.1', expect_stderr=True, ) initools_folder = script.site_packages / 'openid' - assert initools_folder in result.files_created, result.files_created + result.did_create(initools_folder) def test_extras_after_wheel(script, data): @@ -27,13 +27,13 @@ def test_extras_after_wheel(script, data): 'install', '--no-index', '-f', data.find_links, 'requires_simple_extra', expect_stderr=True, ) - assert simple not in no_extra.files_created, no_extra.files_created + no_extra.did_not_create(simple) extra = script.pip( 'install', '--no-index', '-f', data.find_links, 'requires_simple_extra[extra]', expect_stderr=True, ) - assert simple in extra.files_created, extra.files_created + extra.did_create(simple) @pytest.mark.network @@ -44,12 +44,8 @@ def test_no_extras_uninstall(script): result = script.pip( 'install', 'Paste[openid]==1.7.5.1', expect_stderr=True, ) - assert join(script.site_packages, 'paste') in result.files_created, ( - sorted(result.files_created.keys()) - ) - assert join(script.site_packages, 'openid') in result.files_created, ( - sorted(result.files_created.keys()) - ) + result.did_create(join(script.site_packages, 'paste')) + result.did_create(join(script.site_packages, 'openid')) result2 = script.pip('uninstall', 'Paste', '-y') # openid should not be uninstalled initools_folder = script.site_packages / 'openid' diff --git a/tests/functional/test_install_index.py b/tests/functional/test_install_index.py index 60f09c5ad7a..1c778587fa4 100644 --- a/tests/functional/test_install_index.py +++ b/tests/functional/test_install_index.py @@ -20,8 +20,8 @@ def test_find_links_relative_path(script, data): script.site_packages / 'parent-0.1-py{}.egg-info'.format(pyversion) ) initools_folder = script.site_packages / 'parent' - assert egg_info_folder in result.files_created, str(result) - assert initools_folder in result.files_created, str(result) + result.did_create(egg_info_folder) + result.did_create(initools_folder) def test_find_links_requirements_file_relative_path(script, data): @@ -41,8 +41,8 @@ def test_find_links_requirements_file_relative_path(script, data): script.site_packages / 'parent-0.1-py{}.egg-info'.format(pyversion) ) initools_folder = script.site_packages / 'parent' - assert egg_info_folder in result.files_created, str(result) - assert initools_folder in result.files_created, str(result) + result.did_create(egg_info_folder) + result.did_create(initools_folder) def test_install_from_file_index_hash_link(script, data): @@ -54,7 +54,7 @@ def test_install_from_file_index_hash_link(script, data): egg_info_folder = ( script.site_packages / 'simple-1.0-py{}.egg-info'.format(pyversion) ) - assert egg_info_folder in result.files_created, str(result) + result.did_create(egg_info_folder) def test_file_index_url_quoting(script, data): @@ -65,9 +65,7 @@ def test_file_index_url_quoting(script, data): result = script.pip( 'install', '-vvv', '--index-url', index_url, 'simple' ) - assert (script.site_packages / 'simple') in result.files_created, ( - str(result.stdout) - ) - assert ( + result.did_create(script.site_packages / 'simple') + result.did_create( script.site_packages / 'simple-1.0-py{}.egg-info'.format(pyversion) - ) in result.files_created, str(result) + ) From dab7b94ade94892611dc7825526749d4efa417c8 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 7 May 2020 17:12:15 +0800 Subject: [PATCH 1943/3170] Derive Requirement name from ireq is possible This is useful when resolving the wheel cache. --- .../resolution/resolvelib/factory.py | 29 ++++++++++--------- 1 file changed, 15 insertions(+), 14 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 046119cfe45..0c976204e00 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -101,11 +101,11 @@ def _make_candidate_from_dist( def _make_candidate_from_link( self, - link, # type: Link - extras, # type: Set[str] - parent, # type: InstallRequirement - name=None, # type: Optional[str] - version=None, # type: Optional[_BaseVersion] + link, # type: Link + extras, # type: Set[str] + parent, # type: InstallRequirement + name, # type: Optional[str] + version, # type: Optional[_BaseVersion] ): # type: (...) -> Candidate # TODO: Check already installed candidate, and use it if the link and @@ -176,15 +176,16 @@ def iter_found_candidates(self, ireq, extras): def make_requirement_from_install_req(self, ireq): # type: (InstallRequirement) -> Requirement - if ireq.link: - # TODO: Get name and version from ireq, if possible? - # Specifically, this might be needed in "name @ URL" - # syntax - need to check where that syntax is handled. - candidate = self._make_candidate_from_link( - ireq.link, extras=set(ireq.extras), parent=ireq, - ) - return self.make_requirement_from_candidate(candidate) - return SpecifierRequirement(ireq, factory=self) + if not ireq.link: + return SpecifierRequirement(ireq, factory=self) + cand = self._make_candidate_from_link( + ireq.link, + extras=set(ireq.extras), + parent=ireq, + name=canonicalize_name(ireq.name) if ireq.name else None, + version=None, + ) + return self.make_requirement_from_candidate(cand) def make_requirement_from_candidate(self, candidate): # type: (Candidate) -> ExplicitRequirement From bc9b288b1e35888f13acc65e1f8923bea8921231 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 7 May 2020 18:06:16 +0800 Subject: [PATCH 1944/3170] Implement wheel cache lookup in the new resolver --- .../resolution/resolvelib/candidates.py | 14 +++++++++++- .../resolution/resolvelib/factory.py | 22 +++++++++++++++++++ .../resolution/resolvelib/resolver.py | 1 + tests/unit/resolution_resolvelib/conftest.py | 1 + 4 files changed, 37 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index da11c4fe785..04cd904b3eb 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -238,9 +238,21 @@ def __init__( version=None, # type: Optional[_BaseVersion] ): # type: (...) -> None + cache_entry = factory.get_wheel_cache_entry(link, name) + if cache_entry is not None: + logger.debug("Using cached wheel link: %s", cache_entry.link) + link = cache_entry.link + ireq = make_install_req_from_link(link, parent) + + # TODO: Is this logic setting original_link_is_in_wheel_cache correct? + if (cache_entry is not None and + cache_entry.persistent and + parent.link is parent.original_link): + ireq.original_link_is_in_wheel_cache = True + super(LinkCandidate, self).__init__( link=link, - ireq=make_install_req_from_link(link, parent), + ireq=ireq, factory=factory, name=name, version=version, diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 0c976204e00..75aa4dd1cc8 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -8,6 +8,7 @@ InstallationError, UnsupportedPythonVersion, ) +from pip._internal.utils.compatibility_tags import get_supported from pip._internal.utils.misc import ( dist_in_site_packages, dist_in_usersite, @@ -37,6 +38,7 @@ from pip._vendor.pkg_resources import Distribution from pip._vendor.resolvelib import ResolutionImpossible + from pip._internal.cache import CacheEntry, WheelCache from pip._internal.index.package_finder import PackageFinder from pip._internal.models.link import Link from pip._internal.operations.prepare import RequirementPreparer @@ -60,6 +62,7 @@ def __init__( finder, # type: PackageFinder preparer, # type: RequirementPreparer make_install_req, # type: InstallRequirementProvider + wheel_cache, # type: Optional[WheelCache] use_user_site, # type: bool force_reinstall, # type: bool ignore_installed, # type: bool @@ -70,6 +73,7 @@ def __init__( self.finder = finder self.preparer = preparer + self._wheel_cache = wheel_cache self._python_candidate = RequiresPythonCandidate(py_version_info) self._make_install_req_from_spec = make_install_req self._use_user_site = use_user_site @@ -218,6 +222,24 @@ def make_requires_python_requirement(self, specifier): return None return RequiresPythonRequirement(specifier, self._python_candidate) + def get_wheel_cache_entry(self, link, name): + # type: (Link, Optional[str]) -> Optional[CacheEntry] + """Look up the link in the wheel cache. + + If ``preparer.require_hashes`` is True, don't use the wheel cache, + because cached wheels, always built locally, have different hashes + than the files downloaded from the index server and thus throw false + hash mismatches. Furthermore, cached wheels at present have + undeterministic contents due to file modification times. + """ + if self._wheel_cache is None or self.preparer.require_hashes: + return None + return self._wheel_cache.get_cache_entry( + link=link, + package_name=name, + supported_tags=get_supported(), + ) + def should_reinstall(self, candidate): # type: (Candidate) -> bool # TODO: Are there more cases this needs to return True? Editable? diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 9eab87b3a72..0e55357b824 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -88,6 +88,7 @@ def __init__( finder=finder, preparer=preparer, make_install_req=make_install_req, + wheel_cache=wheel_cache, use_user_site=use_user_site, force_reinstall=force_reinstall, ignore_installed=ignore_installed, diff --git a/tests/unit/resolution_resolvelib/conftest.py b/tests/unit/resolution_resolvelib/conftest.py index ba30903629e..87f5d129cbd 100644 --- a/tests/unit/resolution_resolvelib/conftest.py +++ b/tests/unit/resolution_resolvelib/conftest.py @@ -52,6 +52,7 @@ def factory(finder, preparer): finder=finder, preparer=preparer, make_install_req=install_req_from_line, + wheel_cache=None, use_user_site=False, force_reinstall=False, ignore_installed=False, From 48c3d0c8ec5f117ffde87e9e7b3323a8ca4752b0 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 22 May 2020 15:17:53 +0800 Subject: [PATCH 1945/3170] Typo in docstring Co-authored-by: Paul Moore <p.f.moore@gmail.com> --- src/pip/_internal/resolution/resolvelib/factory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 75aa4dd1cc8..51b7a6f7922 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -230,7 +230,7 @@ def get_wheel_cache_entry(self, link, name): because cached wheels, always built locally, have different hashes than the files downloaded from the index server and thus throw false hash mismatches. Furthermore, cached wheels at present have - undeterministic contents due to file modification times. + nondeterministic contents due to file modification times. """ if self._wheel_cache is None or self.preparer.require_hashes: return None From 80f3b3e6e06a4a3fbd2dd1fbf8f16443e527b764 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 22 May 2020 19:45:21 +0800 Subject: [PATCH 1946/3170] This is correct! --- src/pip/_internal/resolution/resolvelib/candidates.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 04cd904b3eb..ed5173fe047 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -244,7 +244,6 @@ def __init__( link = cache_entry.link ireq = make_install_req_from_link(link, parent) - # TODO: Is this logic setting original_link_is_in_wheel_cache correct? if (cache_entry is not None and cache_entry.persistent and parent.link is parent.original_link): From 46f433615e943f9c54754c50d55d2679010a0ce5 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 19 May 2020 17:08:12 +0800 Subject: [PATCH 1947/3170] Update vendored ResolveLib to 0.4.0 --- src/pip/_vendor/resolvelib/__init__.py | 2 +- src/pip/_vendor/resolvelib/compat/__init__.py | 0 .../resolvelib/compat/collections_abc.py | 6 ++ src/pip/_vendor/resolvelib/providers.py | 52 ++++++--------- src/pip/_vendor/resolvelib/reporters.py | 12 +++- src/pip/_vendor/resolvelib/resolvers.py | 64 +++++++++++-------- src/pip/_vendor/vendor.txt | 2 +- 7 files changed, 76 insertions(+), 62 deletions(-) create mode 100644 src/pip/_vendor/resolvelib/compat/__init__.py create mode 100644 src/pip/_vendor/resolvelib/compat/collections_abc.py diff --git a/src/pip/_vendor/resolvelib/__init__.py b/src/pip/_vendor/resolvelib/__init__.py index aaba5b3a120..3b444545de0 100644 --- a/src/pip/_vendor/resolvelib/__init__.py +++ b/src/pip/_vendor/resolvelib/__init__.py @@ -11,7 +11,7 @@ "ResolutionTooDeep", ] -__version__ = "0.3.0" +__version__ = "0.4.0" from .providers import AbstractProvider, AbstractResolver diff --git a/src/pip/_vendor/resolvelib/compat/__init__.py b/src/pip/_vendor/resolvelib/compat/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_vendor/resolvelib/compat/collections_abc.py b/src/pip/_vendor/resolvelib/compat/collections_abc.py new file mode 100644 index 00000000000..366cc5e2e12 --- /dev/null +++ b/src/pip/_vendor/resolvelib/compat/collections_abc.py @@ -0,0 +1,6 @@ +__all__ = ["Sequence"] + +try: + from collections.abc import Sequence +except ImportError: + from collections import Sequence diff --git a/src/pip/_vendor/resolvelib/providers.py b/src/pip/_vendor/resolvelib/providers.py index db1682195e0..68b7290dfa0 100644 --- a/src/pip/_vendor/resolvelib/providers.py +++ b/src/pip/_vendor/resolvelib/providers.py @@ -27,7 +27,7 @@ def get_preference(self, resolution, candidates, information): * `requirement` specifies a requirement contributing to the current candidate list - * `parent` specifies the candidate that provids (dependend on) the + * `parent` specifies the candidate that provides (dependend on) the requirement, or `None` to indicate a root requirement. The preference could depend on a various of issues, including (not @@ -48,23 +48,28 @@ def get_preference(self, resolution, candidates, information): """ raise NotImplementedError - def find_matches(self, requirement): - """Find all possible candidates that satisfy a requirement. + def find_matches(self, requirements): + """Find all possible candidates that satisfy the given requirements. - This should try to get candidates based on the requirement's type. + This should try to get candidates based on the requirements' types. For VCS, local, and archive requirements, the one-and-only match is returned, and for a "named" requirement, the index(es) should be consulted to find concrete candidates for this requirement. - The returned candidates should be sorted by reversed preference, e.g. - the most preferred should be LAST. This is done so list-popping can be - as efficient as possible. + :param requirements: A collection of requirements which all of the the + returned candidates must match. All requirements are guaranteed to + have the same identifier. The collection is never empty. + :returns: An iterable that orders candidates by preference, e.g. the + most preferred candidate should come first. """ raise NotImplementedError def is_satisfied_by(self, requirement, candidate): """Whether the given requirement can be satisfied by a candidate. + The candidate is guarenteed to have been generated from the + requirement. + A boolean should be returned to indicate whether `candidate` is a viable solution to the requirement. """ @@ -92,30 +97,13 @@ def __init__(self, provider, reporter): def resolve(self, requirements, **kwargs): """Take a collection of constraints, spit out the resolution result. - Parameters - ---------- - requirements : Collection - A collection of constraints - kwargs : optional - Additional keyword arguments that subclasses may accept. - - Raises - ------ - self.base_exception - Any raised exception is guaranteed to be a subclass of - self.base_exception. The string representation of an exception - should be human readable and provide context for why it occurred. - - Returns - ------- - retval : object - A representation of the final resolution state. It can be any object - with a `mapping` attribute that is a Mapping. Other attributes can - be used to provide resolver-specific information. - - The `mapping` attribute MUST be key-value pair is an identifier of a - requirement (as returned by the provider's `identify` method) mapped - to the resolved candidate (chosen from the return value of the - provider's `find_matches` method). + This returns a representation of the final resolution state, with one + guarenteed attribute ``mapping`` that contains resolved candidates as + values. The keys are their respective identifiers. + + :param requirements: A collection of constraints. + :param kwargs: Additional keyword arguments that subclasses may accept. + + :raises: ``self.base_exception`` or its subclass. """ raise NotImplementedError diff --git a/src/pip/_vendor/resolvelib/reporters.py b/src/pip/_vendor/resolvelib/reporters.py index c7e9e88b832..a0a2a458844 100644 --- a/src/pip/_vendor/resolvelib/reporters.py +++ b/src/pip/_vendor/resolvelib/reporters.py @@ -23,12 +23,18 @@ def ending(self, state): """Called before the resolution ends successfully. """ - def adding_requirement(self, requirement): - """Called when the resolver adds a new requirement into the resolve criteria. + def adding_requirement(self, requirement, parent): + """Called when adding a new requirement into the resolve criteria. + + :param requirement: The additional requirement to be applied to filter + the available candidaites. + :param parent: The candidate that requires ``requirement`` as a + dependency, or None if ``requirement`` is one of the root + requirements passed in from ``Resolver.resolve()``. """ def backtracking(self, candidate): - """Called when the resolver rejects a candidate during backtracking. + """Called when rejecting a candidate during backtracking. """ def pinning(self, candidate): diff --git a/src/pip/_vendor/resolvelib/resolvers.py b/src/pip/_vendor/resolvelib/resolvers.py index b51d337d231..4497f976a86 100644 --- a/src/pip/_vendor/resolvelib/resolvers.py +++ b/src/pip/_vendor/resolvelib/resolvers.py @@ -1,5 +1,6 @@ import collections +from .compat import collections_abc from .providers import AbstractResolver from .structs import DirectedGraph @@ -68,16 +69,18 @@ def __init__(self, candidates, information, incompatibilities): def __repr__(self): requirements = ", ".join( - "{!r} from {!r}".format(req, parent) + "({!r}, via={!r})".format(req, parent) for req, parent in self.information ) - return "<Criterion {}>".format(requirements) + return "Criterion({})".format(requirements) @classmethod def from_requirement(cls, provider, requirement, parent): """Build an instance from a requirement. """ - candidates = provider.find_matches(requirement) + candidates = provider.find_matches([requirement]) + if not isinstance(candidates, collections_abc.Sequence): + candidates = list(candidates) criterion = cls( candidates=candidates, information=[RequirementInformation(requirement, parent)], @@ -98,11 +101,9 @@ def merged_with(self, provider, requirement, parent): """ infos = list(self.information) infos.append(RequirementInformation(requirement, parent)) - candidates = [ - c - for c in self.candidates - if provider.is_satisfied_by(requirement, c) - ] + candidates = provider.find_matches([r for r, _ in infos]) + if not isinstance(candidates, collections_abc.Sequence): + candidates = list(candidates) criterion = type(self)(candidates, infos, list(self.incompatibilities)) if not candidates: raise RequirementsConflicted(criterion) @@ -179,7 +180,7 @@ def _push_new_state(self): self._states.append(state) def _merge_into_criterion(self, requirement, parent): - self._r.adding_requirement(requirement) + self._r.adding_requirement(requirement, parent) name = self._p.identify(requirement) try: crit = self.state.criteria[name] @@ -218,13 +219,24 @@ def _get_criteria_to_update(self, candidate): def _attempt_to_pin_criterion(self, name, criterion): causes = [] - for candidate in reversed(criterion.candidates): + for candidate in criterion.candidates: try: criteria = self._get_criteria_to_update(candidate) except RequirementsConflicted as e: causes.append(e.criterion) continue + # Check the newly-pinned candidate actually works. This should + # always pass under normal circumstances, but in the case of a + # faulty provider, we will raise an error to notify the implementer + # to fix find_matches() and/or is_satisfied_by(). + satisfied = all( + self._p.is_satisfied_by(r, candidate) + for r in criterion.iter_requirement() + ) + if not satisfied: + raise InconsistentCandidate(candidate, criterion) + # Put newly-pinned candidate at the end. This is essential because # backtracking looks at this mapping to get the last pin. self._r.pinning(candidate) @@ -232,13 +244,6 @@ def _attempt_to_pin_criterion(self, name, criterion): self.state.mapping[name] = candidate self.state.criteria.update(criteria) - # Check the newly-pinned candidate actually works. This should - # always pass under normal circumstances, but in the case of a - # faulty provider, we will raise an error to notify the implementer - # to fix find_matches() and/or is_satisfied_by(). - if not self._is_current_pin_satisfying(name, criterion): - raise InconsistentCandidate(candidate, criterion) - return [] # All candidates tried, nothing works. This criterion is a dead @@ -246,23 +251,32 @@ def _attempt_to_pin_criterion(self, name, criterion): return causes def _backtrack(self): - # We need at least 3 states here: - # (a) One known not working, to drop. - # (b) One to backtrack to. - # (c) One to restore state (b) to its state prior to candidate-pinning, + # Drop the current state, it's known not to work. + del self._states[-1] + + # We need at least 2 states here: + # (a) One to backtrack to. + # (b) One to restore state (a) to its state prior to candidate-pinning, # so we can pin another one instead. - while len(self._states) >= 3: - del self._states[-1] - # Retract the last candidate pin, and create a new (b). - name, candidate = self._states.pop().mapping.popitem() + while len(self._states) >= 2: + # Retract the last candidate pin. + prev_state = self._states.pop() + try: + name, candidate = prev_state.mapping.popitem() + except KeyError: + continue self._r.backtracking(candidate) + + # Create a new state to work on, with the newly known not-working + # candidate excluded. self._push_new_state() # Mark the retracted candidate as incompatible. criterion = self.state.criteria[name].excluded_of(candidate) if criterion is None: # This state still does not work. Try the still previous state. + del self._states[-1] continue self.state.criteria[name] = criterion diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 74ecca4252e..e032f5f732a 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -16,7 +16,7 @@ requests==2.23.0 chardet==3.0.4 idna==2.9 urllib3==1.25.8 -resolvelib==0.3.0 +resolvelib==0.4.0 retrying==1.3.3 setuptools==44.0.0 six==1.14.0 From 6c6b6a7765e18138678bd0e38858df45fe9a9271 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 19 May 2020 17:04:15 +0800 Subject: [PATCH 1948/3170] Implement new Provider.find_matches() --- .../_internal/resolution/resolvelib/base.py | 21 +++-- .../resolution/resolvelib/candidates.py | 16 +++- .../resolution/resolvelib/factory.py | 89 +++++++++++++++---- .../resolution/resolvelib/provider.py | 28 ++++-- .../resolution/resolvelib/requirements.py | 55 ++++-------- src/pip/_internal/utils/hashes.py | 12 +++ .../resolution_resolvelib/test_requirement.py | 10 ++- 7 files changed, 153 insertions(+), 78 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/base.py b/src/pip/_internal/resolution/resolvelib/base.py index 17513d336e7..57013b7b214 100644 --- a/src/pip/_internal/resolution/resolvelib/base.py +++ b/src/pip/_internal/resolution/resolvelib/base.py @@ -3,15 +3,20 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Iterable, Optional, Sequence, Set + from typing import FrozenSet, Iterable, Optional, Tuple - from pip._internal.req.req_install import InstallRequirement - from pip._vendor.packaging.specifiers import SpecifierSet from pip._vendor.packaging.version import _BaseVersion + from pip._internal.req.req_install import InstallRequirement + + CandidateLookup = Tuple[ + Optional["Candidate"], + Optional[InstallRequirement], + ] + def format_name(project, extras): - # type: (str, Set[str]) -> str + # type: (str, FrozenSet[str]) -> str if not extras: return project canonical_extras = sorted(canonicalize_name(e) for e in extras) @@ -24,14 +29,14 @@ def name(self): # type: () -> str raise NotImplementedError("Subclass should override") - def find_matches(self, constraint): - # type: (SpecifierSet) -> Sequence[Candidate] - raise NotImplementedError("Subclass should override") - def is_satisfied_by(self, candidate): # type: (Candidate) -> bool return False + def get_candidate_lookup(self): + # type: () -> CandidateLookup + raise NotImplementedError("Subclass should override") + class Candidate(object): @property diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index ed5173fe047..1f729198fb5 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -17,7 +17,7 @@ from .base import Candidate, format_name if MYPY_CHECK_RUNNING: - from typing import Any, Iterable, Optional, Set, Tuple, Union + from typing import Any, FrozenSet, Iterable, Optional, Tuple, Union from pip._vendor.packaging.version import _BaseVersion from pip._vendor.pkg_resources import Distribution @@ -132,6 +132,10 @@ def __repr__(self): link=str(self.link), ) + def __hash__(self): + # type: () -> int + return hash((self.__class__, self.link)) + def __eq__(self, other): # type: (Any) -> bool if isinstance(other, self.__class__): @@ -313,6 +317,10 @@ def __repr__(self): distribution=self.dist, ) + def __hash__(self): + # type: () -> int + return hash((self.__class__, self.name, self.version)) + def __eq__(self, other): # type: (Any) -> bool if isinstance(other, self.__class__): @@ -371,7 +379,7 @@ class ExtrasCandidate(Candidate): def __init__( self, base, # type: BaseCandidate - extras, # type: Set[str] + extras, # type: FrozenSet[str] ): # type: (...) -> None self.base = base @@ -385,6 +393,10 @@ def __repr__(self): extras=self.extras, ) + def __hash__(self): + # type: () -> int + return hash((self.base, self.extras)) + def __eq__(self, other): # type: (Any) -> bool if isinstance(other, self.__class__): diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 51b7a6f7922..bc044e168ba 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -9,6 +9,7 @@ UnsupportedPythonVersion, ) from pip._internal.utils.compatibility_tags import get_supported +from pip._internal.utils.hashes import Hashes from pip._internal.utils.misc import ( dist_in_site_packages, dist_in_usersite, @@ -31,7 +32,17 @@ ) if MYPY_CHECK_RUNNING: - from typing import Dict, Iterable, Iterator, Optional, Set, Tuple, TypeVar + from typing import ( + FrozenSet, + Dict, + Iterable, + List, + Optional, + Sequence, + Set, + Tuple, + TypeVar, + ) from pip._vendor.packaging.specifiers import SpecifierSet from pip._vendor.packaging.version import _BaseVersion @@ -71,7 +82,7 @@ def __init__( ): # type: (...) -> None - self.finder = finder + self._finder = finder self.preparer = preparer self._wheel_cache = wheel_cache self._python_candidate = RequiresPythonCandidate(py_version_info) @@ -94,7 +105,7 @@ def __init__( def _make_candidate_from_dist( self, dist, # type: Distribution - extras, # type: Set[str] + extras, # type: FrozenSet[str] parent, # type: InstallRequirement ): # type: (...) -> Candidate @@ -130,9 +141,28 @@ def _make_candidate_from_link( return ExtrasCandidate(base, extras) return base - def iter_found_candidates(self, ireq, extras): - # type: (InstallRequirement, Set[str]) -> Iterator[Candidate] - name = canonicalize_name(ireq.req.name) + def _iter_found_candidates( + self, + ireqs, # type: Sequence[InstallRequirement] + specifier, # type: SpecifierSet + ): + # type: (...) -> Iterable[Candidate] + if not ireqs: + return () + + # The InstallRequirement implementation requires us to give it a + # "parent", which doesn't really fit with graph-based resolution. + # Here we just choose the first requirement to represent all of them. + # Hopefully the Project model can correct this mismatch in the future. + parent = ireqs[0] + name = canonicalize_name(parent.req.name) + + hashes = Hashes() + extras = frozenset() # type: FrozenSet[str] + for ireq in ireqs: + specifier &= ireq.req.specifier + hashes |= ireq.hashes(trust_internet=False) + extras |= ireq.req.extras # We use this to ensure that we only yield a single candidate for # each version (the finder's preferred one for that version). The @@ -148,21 +178,18 @@ def iter_found_candidates(self, ireq, extras): if not self._force_reinstall and name in self._installed_dists: installed_dist = self._installed_dists[name] installed_version = installed_dist.parsed_version - if ireq.req.specifier.contains( - installed_version, - prereleases=True - ): + if specifier.contains(installed_version, prereleases=True): candidate = self._make_candidate_from_dist( dist=installed_dist, extras=extras, - parent=ireq, + parent=parent, ) candidates[installed_version] = candidate - found = self.finder.find_best_candidate( - project_name=ireq.req.name, - specifier=ireq.req.specifier, - hashes=ireq.hashes(trust_internet=False), + found = self._finder.find_best_candidate( + project_name=name, + specifier=specifier, + hashes=hashes, ) for ican in found.iter_applicable(): if ican.version == installed_version: @@ -170,7 +197,7 @@ def iter_found_candidates(self, ireq, extras): candidate = self._make_candidate_from_link( link=ican.link, extras=extras, - parent=ireq, + parent=parent, name=name, version=ican.version, ) @@ -178,10 +205,38 @@ def iter_found_candidates(self, ireq, extras): return six.itervalues(candidates) + def find_candidates(self, requirements, constraint): + # type: (Sequence[Requirement], SpecifierSet) -> Iterable[Candidate] + explicit_candidates = set() # type: Set[Candidate] + ireqs = [] # type: List[InstallRequirement] + for req in requirements: + cand, ireq = req.get_candidate_lookup() + if cand is not None: + explicit_candidates.add(cand) + if ireq is not None: + ireqs.append(ireq) + + # If none of the requirements want an explicit candidate, we can ask + # the finder for candidates. + if not explicit_candidates: + return self._iter_found_candidates(ireqs, constraint) + + if constraint: + name = explicit_candidates.pop().name + raise InstallationError( + "Could not satisfy constraints for {!r}: installation from " + "path or url cannot be constrained to a version".format(name) + ) + + return ( + c for c in explicit_candidates + if all(req.is_satisfied_by(c) for req in requirements) + ) + def make_requirement_from_install_req(self, ireq): # type: (InstallRequirement) -> Requirement if not ireq.link: - return SpecifierRequirement(ireq, factory=self) + return SpecifierRequirement(ireq) cand = self._make_candidate_from_link( ireq.link, extras=set(ireq.extras), diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index e9a41f04fc9..98b9f94207b 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -4,7 +4,16 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Any, Dict, Optional, Sequence, Set, Tuple, Union + from typing import ( + Any, + Dict, + Iterable, + Optional, + Sequence, + Set, + Tuple, + Union, + ) from .base import Requirement, Candidate from .factory import Factory @@ -45,7 +54,7 @@ def __init__( self.user_requested = user_requested def _sort_matches(self, matches): - # type: (Sequence[Candidate]) -> Sequence[Candidate] + # type: (Iterable[Candidate]) -> Sequence[Candidate] # The requirement is responsible for returning a sequence of potential # candidates, one per version. The provider handles the logic of @@ -68,7 +77,6 @@ def _sort_matches(self, matches): # - The project was specified on the command line, or # - The project is a dependency and the "eager" upgrade strategy # was requested. - def _eligible_for_upgrade(name): # type: (str) -> bool """Are upgrades allowed for this project? @@ -121,11 +129,15 @@ def get_preference( # Use the "usual" value for now return len(candidates) - def find_matches(self, requirement): - # type: (Requirement) -> Sequence[Candidate] - constraint = self._constraints.get(requirement.name, SpecifierSet()) - matches = requirement.find_matches(constraint) - return self._sort_matches(matches) + def find_matches(self, requirements): + # type: (Sequence[Requirement]) -> Iterable[Candidate] + if not requirements: + return [] + constraint = self._constraints.get( + requirements[0].name, SpecifierSet(), + ) + candidates = self._factory.find_candidates(requirements, constraint) + return reversed(self._sort_matches(candidates)) def is_satisfied_by(self, requirement, candidate): # type: (Requirement, Candidate) -> bool diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py index f21e37a4a63..a10df94940c 100644 --- a/src/pip/_internal/resolution/resolvelib/requirements.py +++ b/src/pip/_internal/resolution/resolvelib/requirements.py @@ -1,19 +1,15 @@ from pip._vendor.packaging.utils import canonicalize_name -from pip._internal.exceptions import InstallationError from pip._internal.utils.typing import MYPY_CHECK_RUNNING from .base import Requirement, format_name if MYPY_CHECK_RUNNING: - from typing import Sequence - from pip._vendor.packaging.specifiers import SpecifierSet from pip._internal.req.req_install import InstallRequirement - from .base import Candidate - from .factory import Factory + from .base import Candidate, CandidateLookup class ExplicitRequirement(Requirement): @@ -34,15 +30,9 @@ def name(self): # No need to canonicalise - the candidate did this return self.candidate.name - def find_matches(self, constraint): - # type: (SpecifierSet) -> Sequence[Candidate] - if len(constraint) > 0: - raise InstallationError( - "Could not satisfy constraints for '{}': " - "installation from path or url cannot be " - "constrained to a version".format(self.name) - ) - return [self.candidate] + def get_candidate_lookup(self): + # type: () -> CandidateLookup + return self.candidate, None def is_satisfied_by(self, candidate): # type: (Candidate) -> bool @@ -50,12 +40,11 @@ def is_satisfied_by(self, candidate): class SpecifierRequirement(Requirement): - def __init__(self, ireq, factory): - # type: (InstallRequirement, Factory) -> None + def __init__(self, ireq): + # type: (InstallRequirement) -> None assert ireq.link is None, "This is a link, not a specifier" self._ireq = ireq - self._factory = factory - self.extras = set(ireq.extras) + self._extras = frozenset(ireq.extras) def __str__(self): # type: () -> str @@ -72,21 +61,11 @@ def __repr__(self): def name(self): # type: () -> str canonical_name = canonicalize_name(self._ireq.req.name) - return format_name(canonical_name, self.extras) - - def find_matches(self, constraint): - # type: (SpecifierSet) -> Sequence[Candidate] - - # We should only return one candidate per version, but - # iter_found_candidates does that for us, so we don't need - # to do anything special here. - return [ - c - for c in self._factory.iter_found_candidates( - self._ireq, self.extras - ) - if constraint.contains(c.version, prereleases=True) - ] + return format_name(canonical_name, self._extras) + + def get_candidate_lookup(self): + # type: () -> CandidateLookup + return None, self._ireq def is_satisfied_by(self, candidate): # type: (Candidate) -> bool @@ -120,13 +99,11 @@ def name(self): # type: () -> str return self._candidate.name - def find_matches(self, constraint): - # type: (SpecifierSet) -> Sequence[Candidate] - assert len(constraint) == 0, \ - "RequiresPythonRequirement cannot have constraints" + def get_candidate_lookup(self): + # type: () -> CandidateLookup if self.specifier.contains(self._candidate.version, prereleases=True): - return [self._candidate] - return [] + return self._candidate, None + return None, None def is_satisfied_by(self, candidate): # type: (Candidate) -> bool diff --git a/src/pip/_internal/utils/hashes.py b/src/pip/_internal/utils/hashes.py index 396cf82e753..d1b062fedf6 100644 --- a/src/pip/_internal/utils/hashes.py +++ b/src/pip/_internal/utils/hashes.py @@ -46,6 +46,18 @@ def __init__(self, hashes=None): """ self._allowed = {} if hashes is None else hashes + def __or__(self, other): + # type: (Hashes) -> Hashes + if not isinstance(other, Hashes): + return NotImplemented + new = self._allowed.copy() + for alg, values in iteritems(other._allowed): + try: + new[alg] += values + except KeyError: + new[alg] = values + return Hashes(new) + @property def digest_count(self): # type: () -> int diff --git a/tests/unit/resolution_resolvelib/test_requirement.py b/tests/unit/resolution_resolvelib/test_requirement.py index 0b7dec02de2..07cd0c0f061 100644 --- a/tests/unit/resolution_resolvelib/test_requirement.py +++ b/tests/unit/resolution_resolvelib/test_requirement.py @@ -57,16 +57,18 @@ def test_new_resolver_requirement_has_name(test_cases, factory): def test_new_resolver_correct_number_of_matches(test_cases, factory): """Requirements should return the correct number of candidates""" - for spec, name, matches in test_cases: + for spec, name, match_count in test_cases: req = factory.make_requirement_from_spec(spec, comes_from=None) - assert len(req.find_matches(SpecifierSet())) == matches + matches = factory.find_candidates([req], SpecifierSet()) + assert len(list(matches)) == match_count def test_new_resolver_candidates_match_requirement(test_cases, factory): - """Candidates returned from find_matches should satisfy the requirement""" + """Candidates returned from find_candidates should satisfy the requirement + """ for spec, name, matches in test_cases: req = factory.make_requirement_from_spec(spec, comes_from=None) - for c in req.find_matches(SpecifierSet()): + for c in factory.find_candidates([req], SpecifierSet()): assert isinstance(c, Candidate) assert req.is_satisfied_by(c) From b8404fde991be121b9d840c1e907e6658aa29ee4 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 22 May 2020 16:52:59 +0800 Subject: [PATCH 1949/3170] Always read extras from InstallRequirement.extras --- src/pip/_internal/resolution/resolvelib/factory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index bc044e168ba..20f5d72bdc4 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -162,7 +162,7 @@ def _iter_found_candidates( for ireq in ireqs: specifier &= ireq.req.specifier hashes |= ireq.hashes(trust_internet=False) - extras |= ireq.req.extras + extras |= frozenset(ireq.extras) # We use this to ensure that we only yield a single candidate for # each version (the finder's preferred one for that version). The From 9ee19a1190bc11a261651e9e776bc69a0bfa452c Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 27 May 2020 20:49:28 +0800 Subject: [PATCH 1950/3170] Always use frozenset --- src/pip/_internal/resolution/resolvelib/factory.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 20f5d72bdc4..502b9fa4d7a 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -117,7 +117,7 @@ def _make_candidate_from_dist( def _make_candidate_from_link( self, link, # type: Link - extras, # type: Set[str] + extras, # type: FrozenSet[str] parent, # type: InstallRequirement name, # type: Optional[str] version, # type: Optional[_BaseVersion] @@ -239,7 +239,7 @@ def make_requirement_from_install_req(self, ireq): return SpecifierRequirement(ireq) cand = self._make_candidate_from_link( ireq.link, - extras=set(ireq.extras), + extras=frozenset(ireq.extras), parent=ireq, name=canonicalize_name(ireq.name) if ireq.name else None, version=None, From 8586098b183dddbb01debf748c402d9a37f931d9 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Mon, 25 May 2020 21:18:24 +0800 Subject: [PATCH 1951/3170] These tests should pass now --- tests/functional/test_install_extras.py | 1 - tests/functional/test_install_reqs.py | 1 - 2 files changed, 2 deletions(-) diff --git a/tests/functional/test_install_extras.py b/tests/functional/test_install_extras.py index dfde7d1b676..3c0359a73f1 100644 --- a/tests/functional/test_install_extras.py +++ b/tests/functional/test_install_extras.py @@ -136,7 +136,6 @@ def test_install_special_extra(script): pytest.param('[extra2]', '1.0', marks=pytest.mark.xfail), pytest.param('[extra1,extra2]', '1.0', marks=pytest.mark.xfail), ]) -@pytest.mark.fails_on_new_resolver def test_install_extra_merging(script, data, extra_to_install, simple_version): # Check that extra specifications in the extras section are honoured. pkga_path = script.scratch_path / 'pkga' diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index d39263b98ea..ae9be4a2ffc 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -189,7 +189,6 @@ def test_respect_order_in_requirements_file(script, data): ) -@pytest.mark.fails_on_new_resolver def test_install_local_editable_with_extras(script, data): to_install = data.packages.joinpath("LocalExtras") res = script.pip_install_local( From 71511afb97ad1b57e99dc6b3ad20f75a43f4b9df Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Mon, 25 May 2020 22:13:48 +0800 Subject: [PATCH 1952/3170] This should pass as well --- tests/functional/test_install.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 4decbd2d81f..e6aa1043cb1 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -148,7 +148,6 @@ def test_pep518_with_user_pip(script, pip_src, data, common_wheels): ) -@pytest.mark.fails_on_new_resolver def test_pep518_with_extra_and_markers(script, data, common_wheels): script.pip( 'wheel', '--no-index', From f39a090441d7912ebbc6ec0b0f67320b360d4db3 Mon Sep 17 00:00:00 2001 From: gutsytechster <prashantsharma161198@gmail.com> Date: Thu, 28 May 2020 15:08:38 +0530 Subject: [PATCH 1953/3170] fix(tests): Only pass readable message as keyword arg to helpers --- tests/functional/test_install_upgrade.py | 29 ++++++------------------ 1 file changed, 7 insertions(+), 22 deletions(-) diff --git a/tests/functional/test_install_upgrade.py b/tests/functional/test_install_upgrade.py index ce5b15dbd52..1e1ab552265 100644 --- a/tests/functional/test_install_upgrade.py +++ b/tests/functional/test_install_upgrade.py @@ -82,7 +82,7 @@ def test_only_if_needed_does_upgrade_deps_when_no_longer_satisfied(script): script.site_packages / 'simple-3.0-py{pyversion}.egg-info'.format(**globals()) ) - result.did_create(expected, "should have installed simple==3.0") + result.did_create(expected, message="should have installed simple==3.0") expected = ( script.site_packages / 'simple-1.0-py{pyversion}.egg-info'.format(**globals()) @@ -132,7 +132,7 @@ def test_eager_does_upgrade_dependecies_when_no_longer_satisfied(script): result.did_create( script.site_packages / 'simple-3.0-py{pyversion}.egg-info'.format(**globals()), - "should have installed simple==3.0" + message="should have installed simple==3.0" ) assert ( script.site_packages / @@ -199,10 +199,7 @@ def test_upgrade_force_reinstall_newest(script): version if --force-reinstall is supplied. """ result = script.pip('install', 'INITools') - result.did_create( - script.site_packages / 'initools', - sorted(result.files_created.keys()) - ) + result.did_create(script.site_packages / 'initools') result2 = script.pip( 'install', '--upgrade', '--force-reinstall', 'INITools' ) @@ -218,10 +215,7 @@ def test_uninstall_before_upgrade(script): """ result = script.pip('install', 'INITools==0.2') - result.did_create( - script.site_packages / 'initools', - sorted(result.files_created.keys()) - ) + result.did_create(script.site_packages / 'initools') result2 = script.pip('install', 'INITools==0.3') assert result2.files_created, 'upgrade to INITools 0.3 failed' result3 = script.pip('uninstall', 'initools', '-y') @@ -235,10 +229,7 @@ def test_uninstall_before_upgrade_from_url(script): """ result = script.pip('install', 'INITools==0.2') - result.did_create( - script.site_packages / 'initools', - sorted(result.files_created.keys()) - ) + result.did_create(script.site_packages / 'initools') result2 = script.pip( 'install', 'https://files.pythonhosted.org/packages/source/I/INITools/INITools-' @@ -258,10 +249,7 @@ def test_upgrade_to_same_version_from_url(script): """ result = script.pip('install', 'INITools==0.3') - result.did_create( - script.site_packages / 'initools', - sorted(result.files_created.keys()) - ) + result.did_create(script.site_packages / 'initools') result2 = script.pip( 'install', 'https://files.pythonhosted.org/packages/source/I/INITools/INITools-' @@ -315,10 +303,7 @@ def test_uninstall_rollback(script, data): result = script.pip( 'install', '-f', data.find_links, '--no-index', 'broken==0.1' ) - result.did_create( - script.site_packages / 'broken.py', - list(result.files_created.keys()) - ) + result.did_create(script.site_packages / 'broken.py') result2 = script.pip( 'install', '-f', data.find_links, '--no-index', 'broken===0.2broken', expect_error=True, From fa280b9370e8b6147bff0d12913d86bfea4d6a44 Mon Sep 17 00:00:00 2001 From: gutsytechster <prashantsharma161198@gmail.com> Date: Thu, 28 May 2020 15:48:16 +0530 Subject: [PATCH 1954/3170] tests(test_install_{vcs_git, wheel}.py): Refactor path lookups --- ...799f39-2041-42cd-9d65-5f38ff5820d8.trivial | 0 tests/functional/test_install_vcs_git.py | 10 +-- tests/functional/test_install_wheel.py | 87 ++++++++----------- 3 files changed, 38 insertions(+), 59 deletions(-) create mode 100644 news/fe799f39-2041-42cd-9d65-5f38ff5820d8.trivial diff --git a/news/fe799f39-2041-42cd-9d65-5f38ff5820d8.trivial b/news/fe799f39-2041-42cd-9d65-5f38ff5820d8.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/functional/test_install_vcs_git.py b/tests/functional/test_install_vcs_git.py index 97b792c0135..b13137dec49 100644 --- a/tests/functional/test_install_vcs_git.py +++ b/tests/functional/test_install_vcs_git.py @@ -176,7 +176,7 @@ def test_install_noneditable_git(script, tmpdir): result.assert_installed('piptestpackage', without_egg_link=True, editable=False) - assert egg_info_folder in result.files_created, str(result) + result.did_create(egg_info_folder) def test_git_with_sha1_revisions(script): @@ -341,7 +341,7 @@ def test_git_with_non_editable_where_egg_contains_dev_string(script, tmpdir): ) result = script.pip('install', local_url) devserver_folder = script.site_packages / 'devserver' - assert devserver_folder in result.files_created, str(result) + result.did_create(devserver_folder) def test_git_with_ambiguous_revs(script): @@ -456,9 +456,8 @@ def test_check_submodule_addition(script): install_result = script.pip( 'install', '-e', 'git+' + module_path + '#egg=version_pkg' ) - assert ( + install_result.did_create( script.venv / 'src/version-pkg/testpkg/static/testfile' - in install_result.files_created ) _change_test_package_submodule(script, submodule_path) @@ -472,9 +471,8 @@ def test_check_submodule_addition(script): '--upgrade', ) - assert ( + update_result.did_create( script.venv / 'src/version-pkg/testpkg/static/testfile2' - in update_result.files_created ) diff --git a/tests/functional/test_install_wheel.py b/tests/functional/test_install_wheel.py index a9293438c11..d3bf948fc56 100644 --- a/tests/functional/test_install_wheel.py +++ b/tests/functional/test_install_wheel.py @@ -73,11 +73,9 @@ def test_basic_install_from_wheel(script, shared_data, tmpdir): '--find-links', tmpdir, ) dist_info_folder = script.site_packages / 'has.script-1.0.dist-info' - assert dist_info_folder in result.files_created, (dist_info_folder, - result.files_created, - result.stdout) + result.did_create(dist_info_folder) script_file = script.bin / 'script.py' - assert script_file in result.files_created + result.did_create(script_file) def test_basic_install_from_wheel_with_extras(script, shared_data, tmpdir): @@ -95,13 +93,9 @@ def test_basic_install_from_wheel_with_extras(script, shared_data, tmpdir): '--find-links', tmpdir, ) dist_info_folder = script.site_packages / 'complex_dist-0.1.dist-info' - assert dist_info_folder in result.files_created, (dist_info_folder, - result.files_created, - result.stdout) + result.did_create(dist_info_folder) dist_info_folder = script.site_packages / 'simple.dist-0.1.dist-info' - assert dist_info_folder in result.files_created, (dist_info_folder, - result.files_created, - result.stdout) + result.did_create(dist_info_folder) def test_basic_install_from_wheel_file(script, data): @@ -111,20 +105,14 @@ def test_basic_install_from_wheel_file(script, data): package = data.packages.joinpath("simple.dist-0.1-py2.py3-none-any.whl") result = script.pip('install', package, '--no-index') dist_info_folder = script.site_packages / 'simple.dist-0.1.dist-info' - assert dist_info_folder in result.files_created, (dist_info_folder, - result.files_created, - result.stdout) + result.did_create(dist_info_folder) installer = dist_info_folder / 'INSTALLER' - assert installer in result.files_created, (dist_info_folder, - result.files_created, - result.stdout) + result.did_create(installer) with open(script.base_path / installer, 'rb') as installer_file: installer_details = installer_file.read() assert installer_details == b'pip\n' installer_temp = dist_info_folder / 'INSTALLER.pip' - assert installer_temp not in result.files_created, (dist_info_folder, - result.files_created, - result.stdout) + result.did_not_create(installer_temp) # Installation seems to work, but scripttest fails to check. @@ -148,13 +136,13 @@ def test_basic_install_from_unicode_wheel(script, data): '--find-links', script.scratch_path, ) dist_info_folder = script.site_packages / 'unicode_package-1.0.dist-info' - assert dist_info_folder in result.files_created, str(result) + result.did_create(dist_info_folder) file1 = script.site_packages.joinpath('வணக்கம்', '__init__.py') - assert file1 in result.files_created, str(result) + result.did_create(file1) file2 = script.site_packages.joinpath('வணக்கம்', 'નમસ્તે.py') - assert file2 in result.files_created, str(result) + result.did_create(file2) def test_install_from_wheel_with_headers(script, data): @@ -164,9 +152,7 @@ def test_install_from_wheel_with_headers(script, data): package = data.packages.joinpath("headers.dist-0.1-py2.py3-none-any.whl") result = script.pip('install', package, '--no-index') dist_info_folder = script.site_packages / 'headers.dist-0.1.dist-info' - assert dist_info_folder in result.files_created, (dist_info_folder, - result.files_created, - result.stdout) + result.did_create(dist_info_folder) def test_install_wheel_with_target(script, shared_data, with_wheel, tmpdir): @@ -181,9 +167,7 @@ def test_install_wheel_with_target(script, shared_data, with_wheel, tmpdir): 'install', 'simple.dist==0.1', '-t', target_dir, '--no-index', '--find-links', tmpdir, ) - assert Path('scratch') / 'target' / 'simpledist' in result.files_created, ( - str(result) - ) + result.did_create(Path('scratch') / 'target' / 'simpledist') def test_install_wheel_with_target_and_data_files(script, data, with_wheel): @@ -213,12 +197,9 @@ def test_install_wheel_with_target_and_data_files(script, data, with_wheel): '-t', target_dir, '--no-index') - assert (Path('scratch') / 'prjwithdatafile' / 'packages1' / 'README.txt' - in result.files_created), str(result) - assert (Path('scratch') / 'prjwithdatafile' / 'packages2' / 'README.txt' - in result.files_created), str(result) - assert (Path('scratch') / 'prjwithdatafile' / 'lib' / 'python' - not in result.files_created), str(result) + result.did_create(Path('scratch') / 'prjwithdatafile' / 'packages1' / 'README.txt') + result.did_create(Path('scratch') / 'prjwithdatafile' / 'packages2' / 'README.txt') + result.did_not_create(Path('scratch') / 'prjwithdatafile' / 'lib' / 'python') def test_install_wheel_with_root(script, shared_data, tmpdir): @@ -233,7 +214,7 @@ def test_install_wheel_with_root(script, shared_data, tmpdir): 'install', 'simple.dist==0.1', '--root', root_dir, '--no-index', '--find-links', tmpdir, ) - assert Path('scratch') / 'root' in result.files_created + result.did_create(Path('scratch') / 'root') def test_install_wheel_with_prefix(script, shared_data, tmpdir): @@ -249,7 +230,7 @@ def test_install_wheel_with_prefix(script, shared_data, tmpdir): '--no-index', '--find-links', tmpdir, ) lib = distutils.sysconfig.get_python_lib(prefix=Path('scratch') / 'prefix') - assert lib in result.files_created, str(result) + result.did_create(lib) def test_install_from_wheel_installs_deps(script, data, tmpdir): @@ -281,7 +262,7 @@ def test_install_from_wheel_no_deps(script, data, tmpdir): package, ) pkg_folder = script.site_packages / 'source' - assert pkg_folder not in result.files_created + result.did_not_create(pkg_folder) def test_wheel_record_lines_in_deterministic_order(script, data): @@ -291,8 +272,8 @@ def test_wheel_record_lines_in_deterministic_order(script, data): dist_info_folder = script.site_packages / 'simplewheel-1.0.dist-info' record_path = dist_info_folder / 'RECORD' - assert dist_info_folder in result.files_created, str(result) - assert record_path in result.files_created, str(result) + result.did_create(dist_info_folder) + result.did_create(record_path) record_path = result.files_created[record_path].full record_lines = [ @@ -314,9 +295,9 @@ def test_install_user_wheel(script, shared_data, with_wheel, tmpdir): '--find-links', tmpdir, ) egg_info_folder = script.user_site / 'has.script-1.0.dist-info' - assert egg_info_folder in result.files_created, str(result) + result.did_create(egg_info_folder) script_file = script.user_bin / 'script.py' - assert script_file in result.files_created, str(result) + result.did_create(script_file) def test_install_from_wheel_gen_entrypoint(script, shared_data, tmpdir): @@ -335,7 +316,7 @@ def test_install_from_wheel_gen_entrypoint(script, shared_data, tmpdir): wrapper_file = script.bin / 't1.exe' else: wrapper_file = script.bin / 't1' - assert wrapper_file in result.files_created + result.did_create(wrapper_file) if os.name != "nt": assert bool(os.access(script.base_path / wrapper_file, os.X_OK)) @@ -361,7 +342,7 @@ def test_install_from_wheel_gen_uppercase_entrypoint( wrapper_file = script.bin / 'cmdName.exe' else: wrapper_file = script.bin / 'cmdName' - assert wrapper_file in result.files_created + result.did_create(wrapper_file) if os.name != "nt": assert bool(os.access(script.base_path / wrapper_file, os.X_OK)) @@ -383,8 +364,8 @@ def test_install_from_wheel_with_legacy(script, shared_data, tmpdir): legacy_file1 = script.bin / 'testscript1.bat' legacy_file2 = script.bin / 'testscript2' - assert legacy_file1 in result.files_created - assert legacy_file2 in result.files_created + result.did_create(legacy_file1) + result.did_create(legacy_file2) def test_install_from_wheel_no_setuptools_entrypoint( @@ -412,8 +393,8 @@ def test_install_from_wheel_no_setuptools_entrypoint( # is present and that the -script.py helper has been skipped. We can't # easily test that the wrapper from the wheel has been skipped / # overwritten without getting very platform-dependent, so omit that. - assert wrapper_file in result.files_created - assert wrapper_helper not in result.files_created + result.did_create(wrapper_file) + result.did_not_create(wrapper_helper) def test_skipping_setuptools_doesnt_skip_legacy(script, shared_data, tmpdir): @@ -433,9 +414,9 @@ def test_skipping_setuptools_doesnt_skip_legacy(script, shared_data, tmpdir): legacy_file2 = script.bin / 'testscript2' wrapper_helper = script.bin / 't1-script.py' - assert legacy_file1 in result.files_created - assert legacy_file2 in result.files_created - assert wrapper_helper not in result.files_created + result.did_create(legacy_file1) + result.did_create(legacy_file2) + result.did_not_create(wrapper_helper) def test_install_from_wheel_gui_entrypoint(script, shared_data, tmpdir): @@ -453,7 +434,7 @@ def test_install_from_wheel_gui_entrypoint(script, shared_data, tmpdir): wrapper_file = script.bin / 't1.exe' else: wrapper_file = script.bin / 't1' - assert wrapper_file in result.files_created + result.did_create(wrapper_file) def test_wheel_compiles_pyc(script, shared_data, tmpdir): @@ -511,9 +492,9 @@ def test_install_from_wheel_uninstalls_old_version(script, data): package = data.packages.joinpath("simplewheel-2.0-py2.py3-none-any.whl") result = script.pip('install', package, '--no-index') dist_info_folder = script.site_packages / 'simplewheel-2.0.dist-info' - assert dist_info_folder in result.files_created + result.did_create(dist_info_folder) dist_info_folder = script.site_packages / 'simplewheel-1.0.dist-info' - assert dist_info_folder not in result.files_created + result.did_not_create(dist_info_folder) def test_wheel_compile_syntax_error(script, data): From 41d33cc83ce22fefe28cb1d91180e121ca104f90 Mon Sep 17 00:00:00 2001 From: gutsytechster <prashantsharma161198@gmail.com> Date: Thu, 28 May 2020 16:05:17 +0530 Subject: [PATCH 1955/3170] fix(test_install_wheels): Fix flake8 errors --- tests/functional/test_install_wheel.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/functional/test_install_wheel.py b/tests/functional/test_install_wheel.py index d3bf948fc56..fa9fd0c81a6 100644 --- a/tests/functional/test_install_wheel.py +++ b/tests/functional/test_install_wheel.py @@ -197,9 +197,12 @@ def test_install_wheel_with_target_and_data_files(script, data, with_wheel): '-t', target_dir, '--no-index') - result.did_create(Path('scratch') / 'prjwithdatafile' / 'packages1' / 'README.txt') - result.did_create(Path('scratch') / 'prjwithdatafile' / 'packages2' / 'README.txt') - result.did_not_create(Path('scratch') / 'prjwithdatafile' / 'lib' / 'python') + result.did_create( + Path('scratch') / 'prjwithdatafile' / 'packages1' / 'README.txt') + result.did_create( + Path('scratch') / 'prjwithdatafile' / 'packages2' / 'README.txt') + result.did_not_create( + Path('scratch') / 'prjwithdatafile' / 'lib' / 'python') def test_install_wheel_with_root(script, shared_data, tmpdir): From ccee4c784c2ee318ef30119bf72d2c303741f733 Mon Sep 17 00:00:00 2001 From: gutsytechster <prashantsharma161198@gmail.com> Date: Thu, 28 May 2020 16:32:07 +0530 Subject: [PATCH 1956/3170] Refactor test --- tests/functional/test_install_wheel.py | 11 ++++------- 1 file changed, 4 insertions(+), 7 deletions(-) diff --git a/tests/functional/test_install_wheel.py b/tests/functional/test_install_wheel.py index fa9fd0c81a6..660ee667045 100644 --- a/tests/functional/test_install_wheel.py +++ b/tests/functional/test_install_wheel.py @@ -196,13 +196,10 @@ def test_install_wheel_with_target_and_data_files(script, data, with_wheel): result = script.pip('install', package, '-t', target_dir, '--no-index') - - result.did_create( - Path('scratch') / 'prjwithdatafile' / 'packages1' / 'README.txt') - result.did_create( - Path('scratch') / 'prjwithdatafile' / 'packages2' / 'README.txt') - result.did_not_create( - Path('scratch') / 'prjwithdatafile' / 'lib' / 'python') + project_path = Path('scratch') / 'prjwithdatafile' + result.did_create(project_path / 'packages1' / 'README.txt') + result.did_create(project_path / 'packages2' / 'README.txt') + result.did_not_create(project_path / 'lib' / 'python') def test_install_wheel_with_root(script, shared_data, tmpdir): From b5a5bcf13b25aaa946e4e430021efe8d4b2d93aa Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Wed, 27 May 2020 21:24:00 +0530 Subject: [PATCH 1957/3170] Rename parent to template --- .../resolution/resolvelib/candidates.py | 78 +++++++++---------- .../resolution/resolvelib/factory.py | 34 ++++---- 2 files changed, 56 insertions(+), 56 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 1f729198fb5..695b42a96cc 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -38,23 +38,23 @@ logger = logging.getLogger(__name__) -def make_install_req_from_link(link, parent): +def make_install_req_from_link(link, template): # type: (Link, InstallRequirement) -> InstallRequirement - assert not parent.editable, "parent is editable" - if parent.req: - line = str(parent.req) + assert not template.editable, "template is editable" + if template.req: + line = str(template.req) else: line = link.url ireq = install_req_from_line( line, - comes_from=parent.comes_from, - use_pep517=parent.use_pep517, - isolated=parent.isolated, - constraint=parent.constraint, + comes_from=template.comes_from, + use_pep517=template.use_pep517, + isolated=template.isolated, + constraint=template.constraint, options=dict( - install_options=parent.install_options, - global_options=parent.global_options, - hashes=parent.hash_options + install_options=template.install_options, + global_options=template.global_options, + hashes=template.hash_options ), ) if ireq.link is None: @@ -63,42 +63,42 @@ def make_install_req_from_link(link, parent): return ireq -def make_install_req_from_editable(link, parent): +def make_install_req_from_editable(link, template): # type: (Link, InstallRequirement) -> InstallRequirement - assert parent.editable, "parent not editable" + assert template.editable, "template not editable" return install_req_from_editable( link.url, - comes_from=parent.comes_from, - use_pep517=parent.use_pep517, - isolated=parent.isolated, - constraint=parent.constraint, + comes_from=template.comes_from, + use_pep517=template.use_pep517, + isolated=template.isolated, + constraint=template.constraint, options=dict( - install_options=parent.install_options, - global_options=parent.global_options, - hashes=parent.hash_options + install_options=template.install_options, + global_options=template.global_options, + hashes=template.hash_options ), ) -def make_install_req_from_dist(dist, parent): +def make_install_req_from_dist(dist, template): # type: (Distribution, InstallRequirement) -> InstallRequirement project_name = canonicalize_name(dist.project_name) - if parent.req: - line = str(parent.req) - elif parent.link: - line = "{} @ {}".format(project_name, parent.link.url) + if template.req: + line = str(template.req) + elif template.link: + line = "{} @ {}".format(project_name, template.link.url) else: line = "{}=={}".format(project_name, dist.parsed_version) ireq = install_req_from_line( line, - comes_from=parent.comes_from, - use_pep517=parent.use_pep517, - isolated=parent.isolated, - constraint=parent.constraint, + comes_from=template.comes_from, + use_pep517=template.use_pep517, + isolated=template.isolated, + constraint=template.constraint, options=dict( - install_options=parent.install_options, - global_options=parent.global_options, - hashes=parent.hash_options + install_options=template.install_options, + global_options=template.global_options, + hashes=template.hash_options ), ) ireq.satisfied_by = dist @@ -236,7 +236,7 @@ class LinkCandidate(_InstallRequirementBackedCandidate): def __init__( self, link, # type: Link - parent, # type: InstallRequirement + template, # type: InstallRequirement factory, # type: Factory name=None, # type: Optional[str] version=None, # type: Optional[_BaseVersion] @@ -246,11 +246,11 @@ def __init__( if cache_entry is not None: logger.debug("Using cached wheel link: %s", cache_entry.link) link = cache_entry.link - ireq = make_install_req_from_link(link, parent) + ireq = make_install_req_from_link(link, template) if (cache_entry is not None and cache_entry.persistent and - parent.link is parent.original_link): + template.link is template.original_link): ireq.original_link_is_in_wheel_cache = True super(LinkCandidate, self).__init__( @@ -270,7 +270,7 @@ class EditableCandidate(_InstallRequirementBackedCandidate): def __init__( self, link, # type: Link - parent, # type: InstallRequirement + template, # type: InstallRequirement factory, # type: Factory name=None, # type: Optional[str] version=None, # type: Optional[_BaseVersion] @@ -278,7 +278,7 @@ def __init__( # type: (...) -> None super(EditableCandidate, self).__init__( link=link, - ireq=make_install_req_from_editable(link, parent), + ireq=make_install_req_from_editable(link, template), factory=factory, name=name, version=version, @@ -295,12 +295,12 @@ class AlreadyInstalledCandidate(Candidate): def __init__( self, dist, # type: Distribution - parent, # type: InstallRequirement + template, # type: InstallRequirement factory, # type: Factory ): # type: (...) -> None self.dist = dist - self._ireq = make_install_req_from_dist(dist, parent) + self._ireq = make_install_req_from_dist(dist, template) self._factory = factory # This is just logging some messages, so we can do it eagerly. diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 502b9fa4d7a..56f13632bf9 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -106,10 +106,10 @@ def _make_candidate_from_dist( self, dist, # type: Distribution extras, # type: FrozenSet[str] - parent, # type: InstallRequirement + template, # type: InstallRequirement ): # type: (...) -> Candidate - base = AlreadyInstalledCandidate(dist, parent, factory=self) + base = AlreadyInstalledCandidate(dist, template, factory=self) if extras: return ExtrasCandidate(base, extras) return base @@ -118,23 +118,23 @@ def _make_candidate_from_link( self, link, # type: Link extras, # type: FrozenSet[str] - parent, # type: InstallRequirement + template, # type: InstallRequirement name, # type: Optional[str] version, # type: Optional[_BaseVersion] ): # type: (...) -> Candidate # TODO: Check already installed candidate, and use it if the link and # editable flag match. - if parent.editable: + if template.editable: if link not in self._editable_candidate_cache: self._editable_candidate_cache[link] = EditableCandidate( - link, parent, factory=self, name=name, version=version, + link, template, factory=self, name=name, version=version, ) base = self._editable_candidate_cache[link] # type: BaseCandidate else: if link not in self._link_candidate_cache: self._link_candidate_cache[link] = LinkCandidate( - link, parent, factory=self, name=name, version=version, + link, template, factory=self, name=name, version=version, ) base = self._link_candidate_cache[link] if extras: @@ -151,11 +151,11 @@ def _iter_found_candidates( return () # The InstallRequirement implementation requires us to give it a - # "parent", which doesn't really fit with graph-based resolution. - # Here we just choose the first requirement to represent all of them. + # "template". Here we just choose the first requirement to represent + # all of them. # Hopefully the Project model can correct this mismatch in the future. - parent = ireqs[0] - name = canonicalize_name(parent.req.name) + template = ireqs[0] + name = canonicalize_name(template.req.name) hashes = Hashes() extras = frozenset() # type: FrozenSet[str] @@ -182,7 +182,7 @@ def _iter_found_candidates( candidate = self._make_candidate_from_dist( dist=installed_dist, extras=extras, - parent=parent, + template=template, ) candidates[installed_version] = candidate @@ -197,7 +197,7 @@ def _iter_found_candidates( candidate = self._make_candidate_from_link( link=ican.link, extras=extras, - parent=parent, + template=template, name=name, version=ican.version, ) @@ -240,7 +240,7 @@ def make_requirement_from_install_req(self, ireq): cand = self._make_candidate_from_link( ireq.link, extras=frozenset(ireq.extras), - parent=ireq, + template=ireq, name=canonicalize_name(ireq.name) if ireq.name else None, version=None, ) @@ -328,15 +328,15 @@ def should_reinstall(self, candidate): def _report_requires_python_error( self, requirement, # type: RequiresPythonRequirement - parent, # type: Candidate + template, # type: Candidate ): # type: (...) -> UnsupportedPythonVersion - template = ( + message_format = ( "Package {package!r} requires a different Python: " "{version} not in {specifier!r}" ) - message = template.format( - package=parent.name, + message = message_format.format( + package=template.name, version=self._python_candidate.version, specifier=str(requirement.specifier), ) From d49e957905a8d577552ad408f7d785e69e75c290 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 28 May 2020 19:59:44 +0530 Subject: [PATCH 1958/3170] Skip the correct test --- tests/functional/test_install.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 4decbd2d81f..01fdc8e0d0e 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1449,6 +1449,7 @@ def test_install_no_binary_disables_cached_wheels(script, data, with_wheel): assert "Running setup.py install for upper" in str(res), str(res) +@pytest.mark.fails_on_new_resolver def test_install_editable_with_wrong_egg_name(script): script.scratch_path.joinpath("pkga").mkdir() pkga_path = script.scratch_path / 'pkga' From d480e408388462815df59d796d33203d7821dd40 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 28 May 2020 20:43:15 +0530 Subject: [PATCH 1959/3170] Make Travis CI correctly ignore failures --- .travis.yml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.travis.yml b/.travis.yml index 5037174f408..7c41f5fbccb 100644 --- a/.travis.yml +++ b/.travis.yml @@ -47,7 +47,9 @@ jobs: fast_finish: true allow_failures: - - env: GROUP=3 NEW_RESOLVER=1 + - env: + - GROUP=3 + - NEW_RESOLVER=1 before_install: tools/travis/setup.sh install: travis_retry tools/travis/install.sh From 2bcbc52b2c9c431c3f9048b63bf37250bacc2f2e Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Mon, 25 May 2020 19:20:16 +0530 Subject: [PATCH 1960/3170] Remove isinstance check for status --- src/pip/_internal/cli/base_command.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index c52ffa2f267..adccf5d898b 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -193,10 +193,7 @@ def _main(self, args): try: status = self.run(options, args) - # FIXME: all commands should return an exit status - # and when it is done, isinstance is not needed anymore - if isinstance(status, int): - return status + return status except PreviousBuildDirError as exc: logger.critical(str(exc)) logger.debug('Exception information:', exc_info=True) From b0e3c57210acc29e0529ac9d72c1f6e08f8adfb1 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Mon, 25 May 2020 19:22:44 +0530 Subject: [PATCH 1961/3170] Add news entry --- news/F4F62E2A-7A1F-475C-95DE-004ED3B87DFB.trivial | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 news/F4F62E2A-7A1F-475C-95DE-004ED3B87DFB.trivial diff --git a/news/F4F62E2A-7A1F-475C-95DE-004ED3B87DFB.trivial b/news/F4F62E2A-7A1F-475C-95DE-004ED3B87DFB.trivial new file mode 100644 index 00000000000..e69de29bb2d From 4b5f723480335efaa79227863e96f6f356929016 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Mon, 25 May 2020 21:35:10 +0530 Subject: [PATCH 1962/3170] Don't return SUCCESS at end of run method --- src/pip/_internal/cli/base_command.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index adccf5d898b..6c949abd87e 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -19,7 +19,6 @@ from pip._internal.cli.status_codes import ( ERROR, PREVIOUS_BUILD_DIR_ERROR, - SUCCESS, UNKNOWN_ERROR, VIRTUALENV_NOT_FOUND, ) @@ -229,5 +228,3 @@ def _main(self, args): return UNKNOWN_ERROR finally: self.handle_pip_version_check(options) - - return SUCCESS From f2fb3610f30d83a445fcc59ac69fbcec481f29d6 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Mon, 25 May 2020 21:35:44 +0530 Subject: [PATCH 1963/3170] Return SUCCESS from side effect function --- tests/unit/test_base_command.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/unit/test_base_command.py b/tests/unit/test_base_command.py index a58f83b3430..f819a0fb189 100644 --- a/tests/unit/test_base_command.py +++ b/tests/unit/test_base_command.py @@ -159,6 +159,7 @@ def test_base_command_provides_tempdir_helpers(): def assert_helpers_set(options, args): assert temp_dir._tempdir_manager is not None assert temp_dir._tempdir_registry is not None + return SUCCESS c = Command("fake", "fake") c.run = Mock(side_effect=assert_helpers_set) @@ -183,6 +184,7 @@ class Holder(object): def create_temp_dirs(options, args): c.tempdir_registry.set_delete(not_deleted, False) Holder.value = TempDirectory(kind=kind, globally_managed=True).path + return SUCCESS c = Command("fake", "fake") c.run = Mock(side_effect=create_temp_dirs) @@ -206,6 +208,7 @@ def create_temp_dirs(options, args): path = d.path assert os.path.exists(path) assert os.path.exists(path) == exists + return SUCCESS c = Command("fake", "fake") c.run = Mock(side_effect=create_temp_dirs) From cbfac1a5d1d01d270e7eea99328328b5fcdd6db7 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Mon, 25 May 2020 21:36:03 +0530 Subject: [PATCH 1964/3170] import status codes from cli.status_codes --- tests/functional/test_help.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/test_help.py b/tests/functional/test_help.py index 1d1e439cf63..00a395006b7 100644 --- a/tests/functional/test_help.py +++ b/tests/functional/test_help.py @@ -1,7 +1,7 @@ import pytest from mock import Mock -from pip._internal.cli.base_command import ERROR, SUCCESS +from pip._internal.cli.status_codes import ERROR, SUCCESS from pip._internal.commands import commands_dict, create_command from pip._internal.exceptions import CommandError From cb25f93a2074c2aed5c43b48719c656dba73b3d2 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Thu, 28 May 2020 14:21:47 +0530 Subject: [PATCH 1965/3170] Assert that status is int --- src/pip/_internal/cli/base_command.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index 6c949abd87e..5431eb98168 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -192,6 +192,7 @@ def _main(self, args): try: status = self.run(options, args) + assert isinstance(status, int) return status except PreviousBuildDirError as exc: logger.critical(str(exc)) From fe76a1229ca0e6ed9082ba0f75f2c50d8792b69f Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Thu, 28 May 2020 14:43:16 +0530 Subject: [PATCH 1966/3170] Return SUCCESS from FakeCommand.run if run_func isn't provided --- tests/unit/test_base_command.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/unit/test_base_command.py b/tests/unit/test_base_command.py index f819a0fb189..27147e76927 100644 --- a/tests/unit/test_base_command.py +++ b/tests/unit/test_base_command.py @@ -30,8 +30,11 @@ def main(self, args): def run(self, options, args): logging.getLogger("pip.tests").info("fake") + # Return SUCCESS from run if run_func is not provided if self.run_func: return self.run_func() + else: + return SUCCESS class FakeCommandWithUnicode(FakeCommand): From 3f76479c60b66e5a2a4b7e2ab69887c7c27996c3 Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara <sh@changeset.nyc> Date: Thu, 28 May 2020 16:15:32 -0400 Subject: [PATCH 1967/3170] Update mailing list link in documentation Per https://groups.google.com/d/msg/pypa-dev/rUNsfIbruHM/LCEx-CB5AgAJ the pypa-dev Google Group is now decommissioned. Pointing to distutils-sig instead. Signed-off-by: Sumana Harihareswara <sh@changeset.nyc> --- docs/html/development/index.rst | 4 ++-- news/2e99f59c-a122-11ea-94b5-1fa2aa7b8ec0.trivial | 0 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 news/2e99f59c-a122-11ea-94b5-1fa2aa7b8ec0.trivial diff --git a/docs/html/development/index.rst b/docs/html/development/index.rst index 5ad4bef98e8..47907584919 100644 --- a/docs/html/development/index.rst +++ b/docs/html/development/index.rst @@ -8,7 +8,7 @@ testing, and documentation. You can also join ``#pypa`` (general packaging discussion and user support) and ``#pypa-dev`` (discussion about development of packaging tools) `on Freenode`_, -or the `pypa-dev mailing list`_, to ask questions or get involved. +or the `distutils-sig mailing list`_, to ask questions or get involved. .. toctree:: :maxdepth: 2 @@ -27,4 +27,4 @@ or the `pypa-dev mailing list`_, to ask questions or get involved. references might be broken. .. _`on Freenode`: https://webchat.freenode.net/?channels=%23pypa-dev,pypa -.. _`pypa-dev mailing list`: https://groups.google.com/forum/#!forum/pypa-dev +.. _`distutils-sig mailing list`: https://mail.python.org/mailman3/lists/distutils-sig.python.org/ diff --git a/news/2e99f59c-a122-11ea-94b5-1fa2aa7b8ec0.trivial b/news/2e99f59c-a122-11ea-94b5-1fa2aa7b8ec0.trivial new file mode 100644 index 00000000000..e69de29bb2d From cef69fef7fa2895abde0763f4abe54bf3c255e7e Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara <sh@changeset.nyc> Date: Thu, 28 May 2020 16:46:32 -0400 Subject: [PATCH 1968/3170] Updated author email in configuration and tests Per https://groups.google.com/d/msg/pypa-dev/rUNsfIbruHM/LCEx-CB5AgAJ the pypa-dev Google Group is now decommissioned. Using distutils-sig instead as author/maintainer email. Signed-off-by: Sumana Harihareswara <sh@changeset.nyc> --- .azure-pipelines/jobs/package.yml | 2 +- news/8454.bugfix | 1 + setup.py | 2 +- tests/conftest.py | 4 ++-- tests/data/src/sample/setup.py | 2 +- tests/functional/test_vcs_bazaar.py | 2 +- tests/lib/__init__.py | 8 ++++---- tools/travis/setup.sh | 2 +- 8 files changed, 12 insertions(+), 11 deletions(-) create mode 100644 news/8454.bugfix diff --git a/.azure-pipelines/jobs/package.yml b/.azure-pipelines/jobs/package.yml index 8663720de9c..bdb0254a1ba 100644 --- a/.azure-pipelines/jobs/package.yml +++ b/.azure-pipelines/jobs/package.yml @@ -16,7 +16,7 @@ jobs: versionSpec: '3' - bash: | - git config --global user.email "pypa-dev@googlegroups.com" + git config --global user.email "distutils-sig@python.org" git config --global user.name "pip" displayName: Setup Git credentials diff --git a/news/8454.bugfix b/news/8454.bugfix new file mode 100644 index 00000000000..fe799f9346c --- /dev/null +++ b/news/8454.bugfix @@ -0,0 +1 @@ +Update author email in config and tests to reflect decommissioning of pypa-dev list. diff --git a/setup.py b/setup.py index 2fdb4c50434..f731b61e890 100644 --- a/setup.py +++ b/setup.py @@ -60,7 +60,7 @@ def get_version(rel_path): }, author='The pip developers', - author_email='pypa-dev@groups.google.com', + author_email='distutils-sig@python.org', package_dir={"": "src"}, packages=find_packages( diff --git a/tests/conftest.py b/tests/conftest.py index c5f369cb8d4..d57c66896e6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -206,7 +206,7 @@ def isolate(tmpdir): # and cause test failures. os.environ["GIT_CONFIG_NOSYSTEM"] = "1" os.environ["GIT_AUTHOR_NAME"] = "pip" - os.environ["GIT_AUTHOR_EMAIL"] = "pypa-dev@googlegroups.com" + os.environ["GIT_AUTHOR_EMAIL"] = "distutils-sig@python.org" # We want to disable the version check from running in the tests os.environ["PIP_DISABLE_PIP_VERSION_CHECK"] = "true" @@ -218,7 +218,7 @@ def isolate(tmpdir): os.makedirs(os.path.join(home_dir, ".config", "git")) with open(os.path.join(home_dir, ".config", "git", "config"), "wb") as fp: fp.write( - b"[user]\n\tname = pip\n\temail = pypa-dev@googlegroups.com\n" + b"[user]\n\tname = pip\n\temail = distutils-sig@python.org\n" ) diff --git a/tests/data/src/sample/setup.py b/tests/data/src/sample/setup.py index d5a6e1b53c0..875860cb7ae 100644 --- a/tests/data/src/sample/setup.py +++ b/tests/data/src/sample/setup.py @@ -39,7 +39,7 @@ def find_version(*file_paths): # Author details author='The Python Packaging Authority', - author_email='pypa-dev@googlegroups.com', + author_email='distutils-sig@python.org', # Choose your license license='MIT', diff --git a/tests/functional/test_vcs_bazaar.py b/tests/functional/test_vcs_bazaar.py index af52daa63ca..d928da8b364 100644 --- a/tests/functional/test_vcs_bazaar.py +++ b/tests/functional/test_vcs_bazaar.py @@ -55,7 +55,7 @@ def test_export_rev(script, tmpdir): create_file(source_dir / 'test_file', 'something new') script.run( 'bzr', 'commit', '-q', - '--author', 'pip <pypa-dev@googlegroups.com>', + '--author', 'pip <distutils-sig@python.org>', '-m', 'change test file', cwd=source_dir, ) diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index d08e1f3613f..6f7a43dadcc 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -782,7 +782,7 @@ def _git_commit( args.append("--all") new_args = [ - 'git', 'commit', '-q', '--author', 'pip <pypa-dev@googlegroups.com>', + 'git', 'commit', '-q', '--author', 'pip <distutils-sig@python.org>', ] new_args.extend(args) new_args.extend(['-m', message]) @@ -799,7 +799,7 @@ def _vcs_add(script, version_pkg_path, vcs='git'): script.run('hg', 'add', '.', cwd=version_pkg_path) script.run( 'hg', 'commit', '-q', - '--user', 'pip <pypa-dev@googlegroups.com>', + '--user', 'pip <distutils-sig@python.org>', '-m', 'initial version', cwd=version_pkg_path, ) elif vcs == 'svn': @@ -818,11 +818,11 @@ def _vcs_add(script, version_pkg_path, vcs='git'): script.run('bzr', 'init', cwd=version_pkg_path) script.run('bzr', 'add', '.', cwd=version_pkg_path) script.run( - 'bzr', 'whoami', 'pip <pypa-dev@googlegroups.com>', + 'bzr', 'whoami', 'pip <distutils-sig@python.org>', cwd=version_pkg_path) script.run( 'bzr', 'commit', '-q', - '--author', 'pip <pypa-dev@googlegroups.com>', + '--author', 'pip <distutils-sig@python.org>', '-m', 'initial version', cwd=version_pkg_path, ) else: diff --git a/tools/travis/setup.sh b/tools/travis/setup.sh index d4676e2fbc4..c52ce5f167e 100755 --- a/tools/travis/setup.sh +++ b/tools/travis/setup.sh @@ -2,5 +2,5 @@ set -e echo "Setting Git Credentials..." -git config --global user.email "pypa-dev@googlegroups.com" +git config --global user.email "distutils-sig@python.org" git config --global user.name "pip" From c38f962cfc89f9cabb07621ce74293db2ac5a9f9 Mon Sep 17 00:00:00 2001 From: Noah Gorny <noah.bar.ilan@gmail.com> Date: Sun, 26 Apr 2020 21:05:54 +0300 Subject: [PATCH 1969/3170] docs: getting-started: Improve "running pip from source tree" section Also add news entry --- docs/html/development/getting-started.rst | 10 +++++++--- news/8148.trivial | 2 ++ 2 files changed, 9 insertions(+), 3 deletions(-) create mode 100644 news/8148.trivial diff --git a/docs/html/development/getting-started.rst b/docs/html/development/getting-started.rst index a809c8b1683..e387597b177 100644 --- a/docs/html/development/getting-started.rst +++ b/docs/html/development/getting-started.rst @@ -34,12 +34,16 @@ For developing pip, you need to install :pypi:`tox`. Often, you can run Running pip From Source Tree ============================ -To run the pip executable from your source tree during development, run pip -from the ``src`` directory: +To run the pip executable from your source tree during development, install pip +locally using editable installation (inside a virtualenv). +You can then invoke your local source tree pip normally. .. code-block:: console - $ python src/pip --version + $ virtualenv venv # You can also use "python -m venv venv" from python3.3+ + $ source venv/bin/activate + $ python -m pip install -e . + $ python -m pip --version Running Tests diff --git a/news/8148.trivial b/news/8148.trivial new file mode 100644 index 00000000000..3d3066aa2c5 --- /dev/null +++ b/news/8148.trivial @@ -0,0 +1,2 @@ +Improve "Running pip from source tree" section in getting-started +to use editable installation instead of running pip directly from source. From 381a826760cbdd31fb30104703f2d25fa35f6a6e Mon Sep 17 00:00:00 2001 From: Surbhi Sharma <ssurbhi560@gmail.com> Date: Fri, 29 May 2020 13:42:13 +0530 Subject: [PATCH 1970/3170] use methods for path lookups in files:{test_install, test_install_compat} --- ...de5945-af83-49a7-aa42-d8f2860fcaeb.trivial | 0 tests/functional/test_install.py | 108 +++++++++--------- tests/functional/test_install_compat.py | 12 +- 3 files changed, 62 insertions(+), 58 deletions(-) create mode 100644 news/f9de5945-af83-49a7-aa42-d8f2860fcaeb.trivial diff --git a/news/f9de5945-af83-49a7-aa42-d8f2860fcaeb.trivial b/news/f9de5945-af83-49a7-aa42-d8f2860fcaeb.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 01fdc8e0d0e..4184ccd55cf 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -210,8 +210,8 @@ def test_pip_second_command_line_interface_works( 'INITools-0.2-py{pyversion}.egg-info'.format(**globals()) ) initools_folder = script.site_packages / 'initools' - assert egg_info_folder in result.files_created, str(result) - assert initools_folder in result.files_created, str(result) + result.did_create(egg_info_folder) + result.did_create(initools_folder) def test_install_exit_status_code_when_no_requirements(script): @@ -242,8 +242,8 @@ def test_basic_install_from_pypi(script): 'INITools-0.2-py{pyversion}.egg-info'.format(**globals()) ) initools_folder = script.site_packages / 'initools' - assert egg_info_folder in result.files_created, str(result) - assert initools_folder in result.files_created, str(result) + result.did_create(egg_info_folder) + result.did_create(initools_folder) # Should not display where it's looking for files assert "Looking in indexes: " not in result.stdout @@ -339,14 +339,14 @@ def test_install_editable_uninstalls_existing_from_path(script, data): assert 'Successfully installed simplewheel' in result.stdout simple_folder = script.site_packages / 'simplewheel' result.assert_installed('simplewheel', editable=False) - assert simple_folder in result.files_created, str(result.stdout) + result.did_create(simple_folder) result = script.pip( 'install', '-e', to_install, ) install_path = script.site_packages / 'simplewheel.egg-link' - assert install_path in result.files_created, str(result) + result.did_create(install_path) assert 'Found existing installation: simplewheel 1.0' in result.stdout assert 'Uninstalling simplewheel-' in result.stdout assert 'Successfully uninstalled simplewheel' in result.stdout @@ -422,8 +422,8 @@ def test_basic_install_from_local_directory(script, data, resolver): script.site_packages / 'FSPkg-0.1.dev0-py{pyversion}.egg-info'.format(**globals()) ) - assert fspkg_folder in result.files_created, str(result.stdout) - assert egg_info_folder in result.files_created, str(result) + result.did_create(fspkg_folder) + result.did_create(egg_info_folder) @pytest.mark.parametrize("test_type,editable", [ @@ -466,13 +466,13 @@ def test_basic_install_relative_directory(script, data, test_type, editable): if not editable: result = script.pip('install', req_path, cwd=script.scratch_path) - assert egg_info_file in result.files_created, str(result) - assert package_folder in result.files_created, str(result) + result.did_create(egg_info_file) + result.did_create(package_folder) else: # Editable install. result = script.pip('install', '-e' + req_path, cwd=script.scratch_path) - assert egg_link_file in result.files_created, str(result) + result.did_create(egg_link_file) def test_install_quiet(script, data): @@ -574,8 +574,8 @@ def test_install_from_local_directory_with_symlinks_to_directories( script.site_packages / 'symlinks-0.1.dev0-py{pyversion}.egg-info'.format(**globals()) ) - assert pkg_folder in result.files_created, str(result.stdout) - assert egg_info_folder in result.files_created, str(result) + result.did_create(pkg_folder) + result.did_create(egg_info_folder) @pytest.mark.skipif("sys.platform == 'win32' or sys.version_info < (3,)") @@ -597,8 +597,8 @@ def test_install_from_local_directory_with_socket_file(script, data, tmpdir): make_socket_file(socket_file_path) result = script.pip("install", "--verbose", to_install) - assert package_folder in result.files_created, str(result.stdout) - assert egg_info_file in result.files_created, str(result) + result.did_create(package_folder) + result.did_create(egg_info_file) assert str(socket_file_path) in result.stderr @@ -689,8 +689,8 @@ def test_install_curdir(script, data): script.site_packages / 'FSPkg-0.1.dev0-py{pyversion}.egg-info'.format(**globals()) ) - assert fspkg_folder in result.files_created, str(result.stdout) - assert egg_info_folder in result.files_created, str(result) + result.did_create(fspkg_folder) + result.did_create(egg_info_folder) def test_install_pardir(script, data): @@ -704,8 +704,8 @@ def test_install_pardir(script, data): script.site_packages / 'FSPkg-0.1.dev0-py{pyversion}.egg-info'.format(**globals()) ) - assert fspkg_folder in result.files_created, str(result.stdout) - assert egg_info_folder in result.files_created, str(result) + result.did_create(fspkg_folder) + result.did_create(egg_info_folder) @pytest.mark.network @@ -747,7 +747,7 @@ def test_install_using_install_option_and_editable(script, tmpdir): script.venv / 'src' / 'pip-test-package' / folder / 'pip-test-package' + script.exe ) - assert script_file in result.files_created + result.did_create(script_file) @pytest.mark.network @@ -776,7 +776,7 @@ def test_install_package_with_same_name_in_curdir(script): script.site_packages / 'mock-0.6.0-py{pyversion}.egg-info'.format(**globals()) ) - assert egg_folder in result.files_created, str(result) + result.did_create(egg_folder) mock100_setup_py = textwrap.dedent('''\ @@ -797,7 +797,7 @@ def test_install_folder_using_dot_slash(script): script.site_packages / 'mock-100.1-py{pyversion}.egg-info'.format(**globals()) ) - assert egg_folder in result.files_created, str(result) + result.did_create(egg_folder) def test_install_folder_using_slash_in_the_end(script): @@ -812,7 +812,7 @@ def test_install_folder_using_slash_in_the_end(script): script.site_packages / 'mock-100.1-py{pyversion}.egg-info'.format(**globals()) ) - assert egg_folder in result.files_created, str(result) + result.did_create(egg_folder) def test_install_folder_using_relative_path(script): @@ -828,7 +828,7 @@ def test_install_folder_using_relative_path(script): script.site_packages / 'mock-100.1-py{pyversion}.egg-info'.format(**globals()) ) - assert egg_folder in result.files_created, str(result) + result.did_create(egg_folder) @pytest.mark.network @@ -842,8 +842,8 @@ def test_install_package_which_contains_dev_in_name(script): script.site_packages / 'django_devserver-0.0.4-py{pyversion}.egg-info'.format(**globals()) ) - assert devserver_folder in result.files_created, str(result.stdout) - assert egg_info_folder in result.files_created, str(result) + result.did_create(devserver_folder) + result.did_create(egg_info_folder) def test_install_package_with_target(script): @@ -852,37 +852,37 @@ def test_install_package_with_target(script): """ target_dir = script.scratch_path / 'target' result = script.pip_install_local('-t', target_dir, "simple==1.0") - assert Path('scratch') / 'target' / 'simple' in result.files_created, ( - str(result) + result.did_create( + Path('scratch') / 'target' / 'simple' ) # Test repeated call without --upgrade, no files should have changed result = script.pip_install_local( '-t', target_dir, "simple==1.0", expect_stderr=True, ) - assert not Path('scratch') / 'target' / 'simple' in result.files_updated + result.did_not_update( + Path('scratch') / 'target' / 'simple' + ) # Test upgrade call, check that new version is installed result = script.pip_install_local('--upgrade', '-t', target_dir, "simple==2.0") - assert Path('scratch') / 'target' / 'simple' in result.files_updated, ( - str(result) + result.did_update( + Path('scratch') / 'target' / 'simple' ) egg_folder = ( Path('scratch') / 'target' / 'simple-2.0-py{pyversion}.egg-info'.format(**globals())) - assert egg_folder in result.files_created, ( - str(result) - ) + result.did_create(egg_folder) # Test install and upgrade of single-module package result = script.pip_install_local('-t', target_dir, 'singlemodule==0.0.0') singlemodule_py = Path('scratch') / 'target' / 'singlemodule.py' - assert singlemodule_py in result.files_created, str(result) + result.did_create(singlemodule_py) result = script.pip_install_local('-t', target_dir, 'singlemodule==0.0.1', '--upgrade') - assert singlemodule_py in result.files_updated, str(result) + result.did_update(singlemodule_py) def test_install_package_to_usersite_with_target_must_fail(script): @@ -923,7 +923,7 @@ def test_install_nonlocal_compatible_wheel(script, data): assert result.returncode == SUCCESS distinfo = Path('scratch') / 'target' / 'simplewheel-2.0-1.dist-info' - assert distinfo in result.files_created + result.did_create(distinfo) # Test install without --target result = script.pip( @@ -954,7 +954,7 @@ def test_install_nonlocal_compatible_wheel_path(script, data): assert result.returncode == SUCCESS distinfo = Path('scratch') / 'target' / 'simplewheel-2.0.dist-info' - assert distinfo in result.files_created + result.did_create(distinfo) # Test a full path requirement (without --target) result = script.pip( @@ -1014,7 +1014,7 @@ def test_install_package_with_root(script, data): os.path.join(script.scratch, 'root'), normal_install_path ) - assert root_path in result.files_created, str(result) + result.did_create(root_path) # Should show find-links location in output assert "Looking in indexes: " not in result.stdout @@ -1036,7 +1036,7 @@ def test_install_package_with_prefix(script, data): distutils.sysconfig.get_python_lib(prefix=rel_prefix_path) / 'simple-1.0-py{}.egg-info'.format(pyversion) ) - assert install_path in result.files_created, str(result) + result.did_create(install_path) def test_install_editable_with_prefix(script): @@ -1067,7 +1067,7 @@ def test_install_editable_with_prefix(script): # assert pkga is installed at correct location install_path = script.scratch / site_packages / 'pkga.egg-link' - assert install_path in result.files_created, str(result) + result.did_create(install_path) def test_install_package_conflict_prefix_and_user(script, data): @@ -1131,10 +1131,10 @@ def test_url_req_case_mismatch_no_index(script, data): # only Upper-1.0.tar.gz should get installed. egg_folder = script.site_packages / \ 'Upper-1.0-py{pyversion}.egg-info'.format(**globals()) - assert egg_folder in result.files_created, str(result) + result.did_create(egg_folder) egg_folder = script.site_packages / \ 'Upper-2.0-py{pyversion}.egg-info'.format(**globals()) - assert egg_folder not in result.files_created, str(result) + result.did_not_create(egg_folder) def test_url_req_case_mismatch_file_index(script, data): @@ -1160,10 +1160,10 @@ def test_url_req_case_mismatch_file_index(script, data): # only Upper-1.0.tar.gz should get installed. egg_folder = script.site_packages / \ 'Dinner-1.0-py{pyversion}.egg-info'.format(**globals()) - assert egg_folder in result.files_created, str(result) + result.did_create(egg_folder) egg_folder = script.site_packages / \ 'Dinner-2.0-py{pyversion}.egg-info'.format(**globals()) - assert egg_folder not in result.files_created, str(result) + result.did_not_create(egg_folder) def test_url_incorrect_case_no_index(script, data): @@ -1179,10 +1179,10 @@ def test_url_incorrect_case_no_index(script, data): # only Upper-2.0.tar.gz should get installed. egg_folder = script.site_packages / \ 'Upper-1.0-py{pyversion}.egg-info'.format(**globals()) - assert egg_folder not in result.files_created, str(result) + result.did_not_create(egg_folder) egg_folder = script.site_packages / \ 'Upper-2.0-py{pyversion}.egg-info'.format(**globals()) - assert egg_folder in result.files_created, str(result) + result.did_create(egg_folder) def test_url_incorrect_case_file_index(script, data): @@ -1199,10 +1199,10 @@ def test_url_incorrect_case_file_index(script, data): # only Upper-2.0.tar.gz should get installed. egg_folder = script.site_packages / \ 'Dinner-1.0-py{pyversion}.egg-info'.format(**globals()) - assert egg_folder not in result.files_created, str(result) + result.did_not_create(egg_folder) egg_folder = script.site_packages / \ 'Dinner-2.0-py{pyversion}.egg-info'.format(**globals()) - assert egg_folder in result.files_created, str(result) + result.did_create(egg_folder) # Should show index-url location in output assert "Looking in indexes: " in result.stdout @@ -1661,8 +1661,8 @@ def test_installed_files_recorded_in_deterministic_order(script, data): installed_files_path = ( script.site_packages / egg_info / 'installed-files.txt' ) - assert fspkg_folder in result.files_created, str(result.stdout) - assert installed_files_path in result.files_created, str(result) + result.did_create(fspkg_folder) + result.did_create(installed_files_path) installed_files_path = result.files_created[installed_files_path].full installed_files_lines = [ @@ -1731,8 +1731,8 @@ def test_target_install_ignores_distutils_config_install_prefix(script): relative_target = os.path.relpath(target, script.base_path) relative_script_base = os.path.relpath(prefix, script.base_path) - assert relative_target in result.files_created - assert relative_script_base not in result.files_created + result.did_create(relative_target) + result.did_not_create(relative_script_base) @pytest.mark.incompatible_with_test_venv @@ -1747,7 +1747,7 @@ def test_user_config_accepted(script): assert "Successfully installed simplewheel" in result.stdout relative_user = os.path.relpath(script.user_site_path, script.base_path) - assert join(relative_user, 'simplewheel') in result.files_created + result.did_create(join(relative_user, 'simplewheel')) @pytest.mark.parametrize( diff --git a/tests/functional/test_install_compat.py b/tests/functional/test_install_compat.py index 60d505188be..a5a0df65218 100644 --- a/tests/functional/test_install_compat.py +++ b/tests/functional/test_install_compat.py @@ -32,13 +32,17 @@ def test_debian_egg_name_workaround(script): # so even if this test runs on a Debian/Ubuntu system with broken # setuptools, since our test runs inside a venv we'll still have the normal # .egg-info - assert egg_info in result.files_created, \ - "Couldn't find {egg_info}".format(**locals()) + result.did_create( + egg_info, + message="Couldn't find {egg_info}".format(**locals()) + ) # The Debian no-pyversion version of the .egg-info mangled = os.path.join(script.site_packages, "INITools-0.2.egg-info") - assert mangled not in result.files_created, \ - "Found unexpected {mangled}".format(**locals()) + result.did_not_create( + mangled, + message="Found unexpected {mangled}".format(**locals()) + ) # Simulate a Debian install by copying the .egg-info to their name for it full_egg_info = os.path.join(script.base_path, egg_info) From b01eb958d08a2179a55446da7177c0c03af58fc2 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Thu, 28 May 2020 15:15:32 +0530 Subject: [PATCH 1971/3170] Fix run type annotation in base_command and configuration --- src/pip/_internal/cli/base_command.py | 2 +- src/pip/_internal/commands/configuration.py | 6 ++++++ 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index 5431eb98168..f51682b60e9 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -105,7 +105,7 @@ def handle_pip_version_check(self, options): assert not hasattr(options, 'no_index') def run(self, options, args): - # type: (Values, List[Any]) -> Any + # type: (Values, List[Any]) -> int raise NotImplementedError def parse_args(self, args): diff --git a/src/pip/_internal/commands/configuration.py b/src/pip/_internal/commands/configuration.py index a8d3aaddb74..e4db35cab37 100644 --- a/src/pip/_internal/commands/configuration.py +++ b/src/pip/_internal/commands/configuration.py @@ -14,6 +14,11 @@ ) from pip._internal.exceptions import PipError from pip._internal.utils.misc import get_prog, write_output +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import List + from optparse import Values logger = logging.getLogger(__name__) @@ -92,6 +97,7 @@ def add_options(self): self.parser.insert_option_group(0, self.cmd_opts) def run(self, options, args): + # type: (Values, List[str]) -> int handlers = { "list": self.list_values, "edit": self.open_in_editor, From 5c24a1eaeeccd82232252090a9c14979ddb5b470 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sat, 30 May 2020 15:53:19 +0530 Subject: [PATCH 1972/3170] Remove redundant variable annotations --- src/pip/_internal/exceptions.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/exceptions.py b/src/pip/_internal/exceptions.py index f478a2b4934..a6b1c17a54a 100644 --- a/src/pip/_internal/exceptions.py +++ b/src/pip/_internal/exceptions.py @@ -214,7 +214,7 @@ def __init__(self, gotten_hash): :param gotten_hash: The hash of the (possibly malicious) archive we just downloaded """ - self.gotten_hash = gotten_hash # type: str + self.gotten_hash = gotten_hash def body(self): # type: () -> str @@ -267,8 +267,8 @@ def __init__(self, allowed, gots): :param gots: A dict of algorithm names pointing to hashes we actually got from the files under suspicion """ - self.allowed = allowed # type: Dict[str, List[str]] - self.gots = gots # type: Dict[str, _Hash] + self.allowed = allowed + self.gots = gots def body(self): # type: () -> str From 869149d20498a63b73319523e46c87e38aaee10c Mon Sep 17 00:00:00 2001 From: Ruairidh MacLeod <5160559+rkm@users.noreply.github.com> Date: Sat, 30 May 2020 11:39:47 +0000 Subject: [PATCH 1973/3170] replace links to pypa-dev mailing list --- .github/ISSUE_TEMPLATE/~good-first-issue.md | 2 +- README.rst | 2 +- docs/html/index.rst | 2 +- news/8353.doc | 1 + 4 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 news/8353.doc diff --git a/.github/ISSUE_TEMPLATE/~good-first-issue.md b/.github/ISSUE_TEMPLATE/~good-first-issue.md index b5ef71ae6f6..885198b63a7 100644 --- a/.github/ISSUE_TEMPLATE/~good-first-issue.md +++ b/.github/ISSUE_TEMPLATE/~good-first-issue.md @@ -12,4 +12,4 @@ labels: ["good first issue"] --- -**Good First Issue**: This issue is a good starting point for first time contributors -- the process of fixing this should be a good introduction to pip's development workflow. If you've already contributed to pip, work on [another issue without this label](https://github.com/pypa/pip/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+-label%3A%22good+first+issue%22) instead. If there is not a corresponding pull request for this issue, it is up for grabs. For directions for getting set up, see our [Getting Started Guide](https://pip.pypa.io/en/latest/development/getting-started/). If you are working on this issue and have questions, feel free to ask them here, [`#pypa-dev` on Freenode](https://webchat.freenode.net/?channels=%23pypa-dev), or the [pypa-dev mailing list](https://groups.google.com/forum/#!forum/pypa-dev). +**Good First Issue**: This issue is a good starting point for first time contributors -- the process of fixing this should be a good introduction to pip's development workflow. If you've already contributed to pip, work on [another issue without this label](https://github.com/pypa/pip/issues?utf8=%E2%9C%93&q=is%3Aissue+is%3Aopen+-label%3A%22good+first+issue%22) instead. If there is not a corresponding pull request for this issue, it is up for grabs. For directions for getting set up, see our [Getting Started Guide](https://pip.pypa.io/en/latest/development/getting-started/). If you are working on this issue and have questions, feel free to ask them here, [`#pypa-dev` on Freenode](https://webchat.freenode.net/?channels=%23pypa-dev), or the [distutils-sig mailing list](https://mail.python.org/mailman3/lists/distutils-sig.python.org/). diff --git a/README.rst b/README.rst index 8edb0fb7e19..4f0f210f0e6 100644 --- a/README.rst +++ b/README.rst @@ -51,7 +51,7 @@ rooms, and mailing lists is expected to follow the `PyPA Code of Conduct`_. .. _learn more and take our survey: https://pyfound.blogspot.com/2020/03/new-pip-resolver-to-roll-out-this-year.html .. _Issue tracking: https://github.com/pypa/pip/issues .. _Discourse channel: https://discuss.python.org/c/packaging -.. _Development mailing list: https://groups.google.com/forum/#!forum/pypa-dev +.. _Development mailing list: https://mail.python.org/mailman3/lists/distutils-sig.python.org/ .. _User IRC: https://webchat.freenode.net/?channels=%23pypa .. _Development IRC: https://webchat.freenode.net/?channels=%23pypa-dev .. _PyPA Code of Conduct: https://www.pypa.io/en/latest/code-of-conduct/ diff --git a/docs/html/index.rst b/docs/html/index.rst index 005aebe850e..ff41fc42b52 100644 --- a/docs/html/index.rst +++ b/docs/html/index.rst @@ -46,7 +46,7 @@ rooms, and mailing lists is expected to follow the `PyPA Code of Conduct`_. .. _GitHub page: https://github.com/pypa/pip .. _Issue tracking: https://github.com/pypa/pip/issues .. _Discourse channel: https://discuss.python.org/c/packaging -.. _Development mailing list: https://groups.google.com/forum/#!forum/pypa-dev +.. _Development mailing list: https://mail.python.org/mailman3/lists/distutils-sig.python.org/ .. _User IRC: https://webchat.freenode.net/?channels=%23pypa .. _Development IRC: https://webchat.freenode.net/?channels=%23pypa-dev .. _PyPA Code of Conduct: https://www.pypa.io/en/latest/code-of-conduct/ diff --git a/news/8353.doc b/news/8353.doc new file mode 100644 index 00000000000..a0ed44ff5d0 --- /dev/null +++ b/news/8353.doc @@ -0,0 +1 @@ +replace links to the old pypa-dev mailing list with https://mail.python.org/mailman3/lists/distutils-sig.python.org/ From c4551ac236d9ffc6426482ef7d21ae52046ece03 Mon Sep 17 00:00:00 2001 From: gutsytechster <prashantsharma161198@gmail.com> Date: Sat, 30 May 2020 23:43:48 +0530 Subject: [PATCH 1974/3170] refactor(test_uninstall.py): Add helper methods for path lookups --- tests/functional/test_uninstall.py | 34 ++++++++++-------------------- 1 file changed, 11 insertions(+), 23 deletions(-) diff --git a/tests/functional/test_uninstall.py b/tests/functional/test_uninstall.py index ab41917c986..4c018a68e31 100644 --- a/tests/functional/test_uninstall.py +++ b/tests/functional/test_uninstall.py @@ -28,9 +28,7 @@ def test_basic_uninstall(script): """ result = script.pip('install', 'INITools==0.2') - assert join(script.site_packages, 'initools') in result.files_created, ( - sorted(result.files_created.keys()) - ) + result.did_create(join(script.site_packages, 'initools')) # the import forces the generation of __pycache__ if the version of python # supports it script.run('python', '-c', "import initools") @@ -147,9 +145,7 @@ def test_basic_uninstall_namespace_package(script): """ result = script.pip('install', 'pd.requires==0.0.3') - assert join(script.site_packages, 'pd') in result.files_created, ( - sorted(result.files_created.keys()) - ) + result.did_create(join(script.site_packages, 'pd')) result2 = script.pip('uninstall', 'pd.find', '-y') assert join(script.site_packages, 'pd') not in result2.files_deleted, ( sorted(result2.files_deleted.keys()) @@ -171,16 +167,12 @@ def test_uninstall_overlapping_package(script, data): child_pkg = data.packages.joinpath("child-0.1.tar.gz") result1 = script.pip('install', parent_pkg) - assert join(script.site_packages, 'parent') in result1.files_created, ( - sorted(result1.files_created.keys()) - ) + result1.did_create(join(script.site_packages, 'parent')) result2 = script.pip('install', child_pkg) - assert join(script.site_packages, 'child') in result2.files_created, ( - sorted(result2.files_created.keys()) - ) - assert normpath( + result2.did_create(join(script.site_packages, 'child')) + result2.did_create(normpath( join(script.site_packages, 'parent/plugins/child_plugin.py') - ) in result2.files_created, sorted(result2.files_created.keys()) + )) # The import forces the generation of __pycache__ if the version of python # supports it script.run('python', '-c', "import parent.plugins.child_plugin, child") @@ -267,9 +259,7 @@ def test_uninstall_console_scripts(script): entry_points={'console_scripts': ['discover = discover:main']}, ) result = script.pip('install', pkg_path) - assert script.bin / 'discover' + script.exe in result.files_created, ( - sorted(result.files_created.keys()) - ) + result.did_create(script.bin / 'discover' + script.exe) result2 = script.pip('uninstall', 'discover', '-y') assert_all_changes(result, result2, [script.venv / 'build', 'cache']) @@ -305,9 +295,7 @@ def test_uninstall_easy_installed_console_scripts(script): # setuptools >= 42.0.0 deprecates easy_install and prints a warning when # used result = script.easy_install('discover', allow_stderr_warning=True) - assert script.bin / 'discover' + script.exe in result.files_created, ( - sorted(result.files_created.keys()) - ) + result.did_create(script.bin / 'discover' + script.exe) result2 = script.pip('uninstall', 'discover', '-y') assert_all_changes( result, @@ -374,9 +362,9 @@ def _test_uninstall_editable_with_source_outside_venv( expect_stderr=True, ) result2 = script.pip('install', '-e', temp_pkg_dir) - assert join( + result2.did_create(join( script.site_packages, 'pip-test-package.egg-link' - ) in result2.files_created, list(result2.files_created.keys()) + )) result3 = script.pip('uninstall', '-y', 'pip-test-package') assert_all_changes( result, @@ -476,7 +464,7 @@ def test_uninstall_wheel(script, data): package = data.packages.joinpath("simple.dist-0.1-py2.py3-none-any.whl") result = script.pip('install', package, '--no-index') dist_info_folder = script.site_packages / 'simple.dist-0.1.dist-info' - assert dist_info_folder in result.files_created + result.did_create(dist_info_folder) result2 = script.pip('uninstall', 'simple.dist', '-y') assert_all_changes(result, result2, []) From 51897fcb63137f39f746cec821fa2e60e4e1f258 Mon Sep 17 00:00:00 2001 From: gutsytechster <prashantsharma161198@gmail.com> Date: Sat, 30 May 2020 23:44:43 +0530 Subject: [PATCH 1975/3170] refactor(test_wheel.py): Add helper methods for path lookups --- tests/functional/test_wheel.py | 27 ++++++++++++--------------- 1 file changed, 12 insertions(+), 15 deletions(-) diff --git a/tests/functional/test_wheel.py b/tests/functional/test_wheel.py index 6bf0486819a..d8657f9082d 100644 --- a/tests/functional/test_wheel.py +++ b/tests/functional/test_wheel.py @@ -58,7 +58,7 @@ def test_pip_wheel_success(script, data): .format(filename=re.escape(wheel_file_name)), result.stdout) assert re.search( r"^\s+Stored in directory: ", result.stdout, re.M) - assert wheel_file_path in result.files_created, result.stdout + result.did_create(wheel_file_path) assert "Successfully built simple" in result.stdout, result.stdout @@ -74,7 +74,7 @@ def test_pip_wheel_build_cache(script, data): wheel_file_name = 'simple-3.0-py{pyversion[0]}-none-any.whl' \ .format(**globals()) wheel_file_path = script.scratch / wheel_file_name - assert wheel_file_path in result.files_created, result.stdout + result.did_create(wheel_file_path) assert "Successfully built simple" in result.stdout, result.stdout # remove target file (script.scratch_path / wheel_file_name).unlink() @@ -84,7 +84,7 @@ def test_pip_wheel_build_cache(script, data): 'wheel', '--no-index', '-f', data.find_links, 'simple==3.0', ) - assert wheel_file_path in result.files_created, result.stdout + result.did_create(wheel_file_path) assert "Successfully built simple" not in result.stdout, result.stdout @@ -97,7 +97,7 @@ def test_basic_pip_wheel_downloads_wheels(script, data): ) wheel_file_name = 'simple.dist-0.1-py2.py3-none-any.whl' wheel_file_path = script.scratch / wheel_file_name - assert wheel_file_path in result.files_created, result.stdout + result.did_create(wheel_file_path) assert "Saved" in result.stdout, result.stdout @@ -152,7 +152,7 @@ def test_pip_wheel_builds_editable_deps(script, data): wheel_file_name = 'simple-1.0-py{pyversion[0]}-none-any.whl' \ .format(**globals()) wheel_file_path = script.scratch / wheel_file_name - assert wheel_file_path in result.files_created, result.stdout + result.did_create(wheel_file_path) def test_pip_wheel_builds_editable(script, data): @@ -167,7 +167,7 @@ def test_pip_wheel_builds_editable(script, data): wheel_file_name = 'simplewheel-1.0-py{pyversion[0]}-none-any.whl' \ .format(**globals()) wheel_file_path = script.scratch / wheel_file_name - assert wheel_file_path in result.files_created, result.stdout + result.did_create(wheel_file_path) def test_pip_wheel_fail(script, data): @@ -182,10 +182,7 @@ def test_pip_wheel_fail(script, data): wheel_file_name = 'wheelbroken-0.1-py{pyversion[0]}-none-any.whl' \ .format(**globals()) wheel_file_path = script.scratch / wheel_file_name - assert wheel_file_path not in result.files_created, ( - wheel_file_path, - result.files_created, - ) + result.did_not_create(wheel_file_path) assert "FakeError" in result.stderr, result.stderr assert "Failed to build wheelbroken" in result.stdout, result.stdout assert result.returncode != 0 @@ -220,7 +217,7 @@ def test_pip_wheel_source_deps(script, data): wheel_file_name = 'source-1.0-py{pyversion[0]}-none-any.whl' \ .format(**globals()) wheel_file_path = script.scratch / wheel_file_name - assert wheel_file_path in result.files_created, result.stdout + result.did_create(wheel_file_path) assert "Successfully built source" in result.stdout, result.stdout @@ -261,7 +258,7 @@ def test_pip_wheel_with_pep518_build_reqs(script, data, common_wheels): wheel_file_name = 'pep518-3.0-py{pyversion[0]}-none-any.whl' \ .format(**globals()) wheel_file_path = script.scratch / wheel_file_name - assert wheel_file_path in result.files_created, result.stdout + result.did_create(wheel_file_path) assert "Successfully built pep518" in result.stdout, result.stdout assert "Installing build dependencies" in result.stdout, result.stdout @@ -275,7 +272,7 @@ def test_pip_wheel_with_pep518_build_reqs_no_isolation(script, data): wheel_file_name = 'pep518-3.0-py{pyversion[0]}-none-any.whl' \ .format(**globals()) wheel_file_path = script.scratch / wheel_file_name - assert wheel_file_path in result.files_created, result.stdout + result.did_create(wheel_file_path) assert "Successfully built pep518" in result.stdout, result.stdout assert "Installing build dependencies" not in result.stdout, result.stdout @@ -322,7 +319,7 @@ def test_pep517_wheels_are_not_confused_with_other_files(script, tmpdir, data): wheel_file_name = 'withpyproject-0.0.1-py{pyversion[0]}-none-any.whl' \ .format(**globals()) wheel_file_path = script.scratch / wheel_file_name - assert wheel_file_path in result.files_created, result.stdout + result.did_create(wheel_file_path) def test_legacy_wheels_are_not_confused_with_other_files(script, tmpdir, data): @@ -337,4 +334,4 @@ def test_legacy_wheels_are_not_confused_with_other_files(script, tmpdir, data): wheel_file_name = 'simplewheel-1.0-py{pyversion[0]}-none-any.whl' \ .format(**globals()) wheel_file_path = script.scratch / wheel_file_name - assert wheel_file_path in result.files_created, result.stdout + result.did_create(wheel_file_path) From c51c5a74cfa12da5b699df8d1ea55aa8ab3cd533 Mon Sep 17 00:00:00 2001 From: gutsytechster <prashantsharma161198@gmail.com> Date: Sat, 30 May 2020 23:45:28 +0530 Subject: [PATCH 1976/3170] docs(news): Add news file entry --- news/b8f2cc6f-00f0-4509-baeb-2e9fd5e35bcf.trivial | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 news/b8f2cc6f-00f0-4509-baeb-2e9fd5e35bcf.trivial diff --git a/news/b8f2cc6f-00f0-4509-baeb-2e9fd5e35bcf.trivial b/news/b8f2cc6f-00f0-4509-baeb-2e9fd5e35bcf.trivial new file mode 100644 index 00000000000..e69de29bb2d From fe761c5f4fb43f31a105adc8bea324f159b7a9cd Mon Sep 17 00:00:00 2001 From: Surbhi Sharma <ssurbhi560@gmail.com> Date: Sun, 31 May 2020 18:55:43 +0530 Subject: [PATCH 1977/3170] Use helper methods in test_download and test_new_resolver --- ...ea2537-672d-44b2-b631-9a3455e5fc05.trivial | 0 tests/functional/test_download.py | 128 ++++++++---------- tests/functional/test_new_resolver.py | 25 ++-- 3 files changed, 75 insertions(+), 78 deletions(-) create mode 100644 news/9dea2537-672d-44b2-b631-9a3455e5fc05.trivial diff --git a/news/9dea2537-672d-44b2-b631-9a3455e5fc05.trivial b/news/9dea2537-672d-44b2-b631-9a3455e5fc05.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/functional/test_download.py b/tests/functional/test_download.py index e5bb89b7be5..15ce4c1f355 100644 --- a/tests/functional/test_download.py +++ b/tests/functional/test_download.py @@ -27,9 +27,10 @@ def test_download_if_requested(script): result = script.pip( 'download', '-d', 'pip_downloads', 'INITools==0.1' ) - assert Path('scratch') / 'pip_downloads' / 'INITools-0.1.tar.gz' \ - in result.files_created - assert script.site_packages / 'initools' not in result.files_created + result.did_create( + Path('scratch') / 'pip_downloads' / 'INITools-0.1.tar.gz' + ) + result.did_not_create(script.site_packages / 'initools') @pytest.mark.network @@ -54,11 +55,10 @@ def test_download_wheel(script, data): '-f', data.packages, '-d', '.', 'meta' ) - assert ( + result.did_create( Path('scratch') / 'meta-1.0-py2.py3-none-any.whl' - in result.files_created ) - assert script.site_packages / 'piptestpackage' not in result.files_created + result.did_not_create(script.site_packages / 'piptestpackage') @pytest.mark.network @@ -73,8 +73,10 @@ def test_single_download_from_requirements_file(script): result = script.pip( 'download', '-r', script.scratch_path / 'test-req.txt', '-d', '.', ) - assert Path('scratch') / 'INITools-0.1.tar.gz' in result.files_created - assert script.site_packages / 'initools' not in result.files_created + result.did_create( + Path('scratch') / 'INITools-0.1.tar.gz' + ) + result.did_not_create(script.site_packages / 'initools') @pytest.mark.network @@ -85,12 +87,14 @@ def test_basic_download_should_download_dependencies(script): result = script.pip( 'download', 'Paste[openid]==1.7.5.1', '-d', '.' ) - assert Path('scratch') / 'Paste-1.7.5.1.tar.gz' in result.files_created + result.did_create( + Path('scratch') / 'Paste-1.7.5.1.tar.gz' + ) openid_tarball_prefix = str(Path('scratch') / 'python-openid-') assert any( path.startswith(openid_tarball_prefix) for path in result.files_created ) - assert script.site_packages / 'openid' not in result.files_created + result.did_not_create(script.site_packages / 'openid') def test_download_wheel_archive(script, data): @@ -103,7 +107,9 @@ def test_download_wheel_archive(script, data): 'download', wheel_path, '-d', '.', '--no-deps' ) - assert Path('scratch') / wheel_filename in result.files_created + result.did_create( + Path('scratch') / wheel_filename + ) def test_download_should_download_wheel_deps(script, data): @@ -117,8 +123,12 @@ def test_download_should_download_wheel_deps(script, data): 'download', wheel_path, '-d', '.', '--find-links', data.find_links, '--no-index' ) - assert Path('scratch') / wheel_filename in result.files_created - assert Path('scratch') / dep_filename in result.files_created + result.did_create( + Path('scratch') / wheel_filename + ) + result.did_create( + Path('scratch') / dep_filename + ) @pytest.mark.network @@ -133,8 +143,10 @@ def test_download_should_skip_existing_files(script): result = script.pip( 'download', '-r', script.scratch_path / 'test-req.txt', '-d', '.', ) - assert Path('scratch') / 'INITools-0.1.tar.gz' in result.files_created - assert script.site_packages / 'initools' not in result.files_created + result.did_create( + Path('scratch') / 'INITools-0.1.tar.gz' + ) + result.did_not_create(script.site_packages / 'initools') # adding second package to test-req.txt script.scratch_path.joinpath("test-req.txt").write_text(textwrap.dedent(""" @@ -150,9 +162,11 @@ def test_download_should_skip_existing_files(script): assert any( path.startswith(openid_tarball_prefix) for path in result.files_created ) - assert Path('scratch') / 'INITools-0.1.tar.gz' not in result.files_created - assert script.site_packages / 'initools' not in result.files_created - assert script.site_packages / 'openid' not in result.files_created + result.did_not_create( + Path('scratch') / 'INITools-0.1.tar.gz' + ) + result.did_not_create(script.site_packages / 'initools') + result.did_not_create(script.site_packages / 'openid') @pytest.mark.network @@ -163,11 +177,10 @@ def test_download_vcs_link(script): result = script.pip( 'download', '-d', '.', 'git+git://github.com/pypa/pip-test-package.git' ) - assert ( + result.did_create( Path('scratch') / 'pip-test-package-0.1.1.zip' - in result.files_created ) - assert script.site_packages / 'piptestpackage' not in result.files_created + result.did_not_create(script.site_packages / 'piptestpackage') def test_only_binary_set_then_download_specific_platform(script, data): @@ -184,9 +197,8 @@ def test_only_binary_set_then_download_specific_platform(script, data): '--platform', 'linux_x86_64', 'fake' ) - assert ( + result.did_create( Path('scratch') / 'fake-1.0-py2.py3-none-any.whl' - in result.files_created ) @@ -204,9 +216,8 @@ def test_no_deps_set_then_download_specific_platform(script, data): '--platform', 'linux_x86_64', 'fake' ) - assert ( + result.did_create( Path('scratch') / 'fake-1.0-py2.py3-none-any.whl' - in result.files_created ) @@ -262,9 +273,8 @@ def test_download_specify_platform(script, data): '--platform', 'linux_x86_64', 'fake' ) - assert ( + result.did_create( Path('scratch') / 'fake-1.0-py2.py3-none-any.whl' - in result.files_created ) result = script.pip( @@ -286,10 +296,9 @@ def test_download_specify_platform(script, data): '--platform', 'macosx_10_10_x86_64', 'fake' ) - assert ( + result.did_create( Path('scratch') / 'fake-1.0-py2.py3-none-macosx_10_9_x86_64.whl' - in result.files_created ) # OSX platform wheels are not backward-compatible. @@ -319,9 +328,8 @@ def test_download_specify_platform(script, data): '--platform', 'linux_x86_64', 'fake==2' ) - assert ( + result.did_create( Path('scratch') / 'fake-2.0-py2.py3-none-linux_x86_64.whl' - in result.files_created ) @@ -349,9 +357,8 @@ def test_download_universal(self, platform, script, data): '--platform', platform, 'fake', ) - assert ( + result.did_create( Path('scratch') / 'fake-1.0-py2.py3-none-any.whl' - in result.files_created ) @pytest.mark.parametrize("wheel_abi,platform", [ @@ -377,7 +384,9 @@ def test_download_compatible_manylinuxes( '--platform', platform, 'fake', ) - assert Path('scratch') / wheel in result.files_created + result.did_create( + Path('scratch') / wheel + ) def test_explicit_platform_only(self, data, script): """ @@ -408,9 +417,8 @@ def test_download__python_version(script, data): '--python-version', '2', 'fake' ) - assert ( + result.did_create( Path('scratch') / 'fake-1.0-py2.py3-none-any.whl' - in result.files_created ) result = script.pip( @@ -458,9 +466,8 @@ def test_download__python_version(script, data): '--python-version', '2', 'fake' ) - assert ( + result.did_create( Path('scratch') / 'fake-1.0-py2-none-any.whl' - in result.files_created ) result = script.pip( @@ -478,9 +485,8 @@ def test_download__python_version(script, data): '--python-version', '3', 'fake' ) - assert ( + result.did_create( Path('scratch') / 'fake-2.0-py3-none-any.whl' - in result.files_created ) @@ -555,9 +561,8 @@ def test_download_specify_abi(script, data): '--abi', 'fake_abi', 'fake' ) - assert ( + result.did_create( Path('scratch') / 'fake-1.0-py2.py3-none-any.whl' - in result.files_created ) result = script.pip( @@ -589,9 +594,8 @@ def test_download_specify_abi(script, data): '--abi', 'fakeabi', 'fake' ) - assert ( + result.did_create( Path('scratch') / 'fake-1.0-fk2-fakeabi-fake_platform.whl' - in result.files_created ) result = script.pip( @@ -619,9 +623,8 @@ def test_download_specify_implementation(script, data): '--implementation', 'fk', 'fake' ) - assert ( + result.did_create( Path('scratch') / 'fake-1.0-py2.py3-none-any.whl' - in result.files_created ) data.reset() @@ -634,9 +637,8 @@ def test_download_specify_implementation(script, data): '--python-version', '3', 'fake' ) - assert ( + result.did_create( Path('scratch') / 'fake-1.0-fk3-none-any.whl' - in result.files_created ) result = script.pip( @@ -678,13 +680,11 @@ def test_download_prefer_binary_when_tarball_higher_than_wheel(script, data): '-f', data.packages, '-d', '.', 'source' ) - assert ( + result.did_create( Path('scratch') / 'source-0.8-py2.py3-none-any.whl' - in result.files_created ) - assert ( + result.did_not_create( Path('scratch') / 'source-1.0.tar.gz' - not in result.files_created ) @@ -702,13 +702,11 @@ def test_prefer_binary_tarball_higher_than_wheel_req_file(script, data): '-d', '.' ) - assert ( + result.did_create( Path('scratch') / 'source-0.8-py2.py3-none-any.whl' - in result.files_created ) - assert ( + result.did_not_create( Path('scratch') / 'source-1.0.tar.gz' - not in result.files_created ) @@ -726,13 +724,11 @@ def test_download_prefer_binary_when_wheel_doesnt_satisfy_req(script, data): '-d', '.', '-r', script.scratch_path / 'test-req.txt' ) - assert ( + result.did_create( Path('scratch') / 'source-1.0.tar.gz' - in result.files_created ) - assert ( + result.did_not_create( Path('scratch') / 'source-0.8-py2.py3-none-any.whl' - not in result.files_created ) @@ -750,13 +746,11 @@ def test_prefer_binary_when_wheel_doesnt_satisfy_req_req_file(script, data): '-d', '.', '-r', script.scratch_path / 'test-req.txt' ) - assert ( + result.did_create( Path('scratch') / 'source-1.0.tar.gz' - in result.files_created ) - assert ( + result.did_not_create( Path('scratch') / 'source-0.8-py2.py3-none-any.whl' - not in result.files_created ) @@ -768,9 +762,8 @@ def test_download_prefer_binary_when_only_tarball_exists(script, data): '-f', data.packages, '-d', '.', 'source' ) - assert ( + result.did_create( Path('scratch') / 'source-1.0.tar.gz' - in result.files_created ) @@ -786,9 +779,8 @@ def test_prefer_binary_when_only_tarball_exists_req_file(script, data): '-d', '.', '-r', script.scratch_path / 'test-req.txt' ) - assert ( + result.did_create( Path('scratch') / 'source-1.0.tar.gz' - in result.files_created ) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index f235feeb8e1..270b4cb354d 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -413,8 +413,9 @@ def test_new_resolver_installed(script): ) assert "Requirement already satisfied: base~=0.1.0" in result.stdout, \ str(result) - assert script.site_packages / "base" not in result.files_updated, ( - "base 0.1.0 reinstalled" + result.did_not_update( + script.site_packages / "base", + message="base 0.1.0 reinstalled" ) @@ -441,8 +442,9 @@ def test_new_resolver_ignore_installed(script): "base", ) assert satisfied_output not in result.stdout, str(result) - assert script.site_packages / "base" in result.files_updated, ( - "base 0.1.0 not reinstalled" + result.did_update( + script.site_packages / "base", + message="base 0.1.0 not reinstalled" ) @@ -505,8 +507,9 @@ def test_new_resolver_install_different_version(script): assert "Uninstalling base-0.1.0" in result.stdout, str(result) assert "Successfully uninstalled base-0.1.0" in result.stdout, str(result) - assert script.site_packages / "base" in result.files_updated, ( - "base not upgraded" + result.did_update( + script.site_packages / "base", + message="base not upgraded" ) assert_installed(script, base="0.2.0") @@ -533,8 +536,9 @@ def test_new_resolver_force_reinstall(script): assert "Uninstalling base-0.1.0" in result.stdout, str(result) assert "Successfully uninstalled base-0.1.0" in result.stdout, str(result) - assert script.site_packages / "base" in result.files_updated, ( - "base not reinstalled" + result.did_update( + script.site_packages / "base", + message="base not reinstalled" ) assert_installed(script, base="0.1.0") @@ -738,8 +742,9 @@ def test_new_resolver_upgrade_needs_option(script): assert "Uninstalling pkg-1.0.0" in result.stdout, str(result) assert "Successfully uninstalled pkg-1.0.0" in result.stdout, str(result) - assert script.site_packages / "pkg" in result.files_updated, ( - "pkg not upgraded" + result.did_update( + script.site_packages / "pkg", + message="pkg not upgraded" ) assert_installed(script, pkg="2.0.0") From 21df86f1974cfd73c3c8a83dfceb7c02694d13d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sat, 11 Apr 2020 18:40:55 +0200 Subject: [PATCH 1978/3170] Add REQUESTED support to install_wheel --- src/pip/_internal/operations/install/wheel.py | 10 ++++++++++ tests/unit/test_wheel.py | 13 +++++++++++++ 2 files changed, 23 insertions(+) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 36877ca5e76..d2ff3544f7c 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -339,6 +339,7 @@ def install_unpacked_wheel( pycompile=True, # type: bool warn_script_location=True, # type: bool direct_url=None, # type: Optional[DirectUrl] + requested=False, # type: bool ): # type: (...) -> None """Install a wheel. @@ -645,6 +646,13 @@ def _generate_file(path, **kwargs): direct_url_file.write(direct_url.to_json().encode("utf-8")) generated.append(direct_url_path) + # Record the REQUESTED file + if requested: + requested_path = os.path.join(dest_info_dir, 'REQUESTED') + with open(requested_path, "w"): + pass + generated.append(requested_path) + # Record details of all files installed record_path = os.path.join(dest_info_dir, 'RECORD') with open(record_path, **csv_io_kwargs('r')) as record_file: @@ -671,6 +679,7 @@ def install_wheel( warn_script_location=True, # type: bool _temp_dir_for_testing=None, # type: Optional[str] direct_url=None, # type: Optional[DirectUrl] + requested=False, # type: bool ): # type: (...) -> None with TempDirectory( @@ -686,4 +695,5 @@ def install_wheel( pycompile=pycompile, warn_script_location=warn_script_location, direct_url=direct_url, + requested=requested, ) diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 2834b18f087..388e8f01b7a 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -301,6 +301,19 @@ def test_std_install_with_custom_umask(self, data, tmpdir, finally: os.umask(prev_umask) + def test_std_install_requested(self, data, tmpdir): + self.prep(data, tmpdir) + wheel.install_wheel( + self.name, + self.wheelpath, + scheme=self.scheme, + req_description=str(self.req), + requested=True, + ) + self.assert_installed() + requested_path = os.path.join(self.dest_dist_info, 'REQUESTED') + assert os.path.isfile(requested_path) + def test_std_install_with_direct_url(self, data, tmpdir): """Test that install_wheel creates direct_url.json metadata when provided with a direct_url argument. Also test that the RECORDS From c9a445762c403926196f80ec814b9e4c4a4021ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sun, 12 Apr 2020 12:40:38 +0200 Subject: [PATCH 1979/3170] Mark top level requirements as REQUESTED --- news/7811.feature | 2 + src/pip/_internal/req/req_install.py | 1 + tests/functional/test_install_requested.py | 94 ++++++++++++++++++++++ tests/unit/test_wheel.py | 2 +- 4 files changed, 98 insertions(+), 1 deletion(-) create mode 100644 news/7811.feature create mode 100644 tests/functional/test_install_requested.py diff --git a/news/7811.feature b/news/7811.feature new file mode 100644 index 00000000000..b06aaa5bf27 --- /dev/null +++ b/news/7811.feature @@ -0,0 +1,2 @@ +Generate PEP 376 REQUESTED metadata for top level requirements installed +by pip. diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 3b28209b1bd..48ec9442114 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -809,6 +809,7 @@ def install( pycompile=pycompile, warn_script_location=warn_script_location, direct_url=direct_url, + requested=self.is_direct, ) self.install_succeeded = True return diff --git a/tests/functional/test_install_requested.py b/tests/functional/test_install_requested.py new file mode 100644 index 00000000000..6caec988eb5 --- /dev/null +++ b/tests/functional/test_install_requested.py @@ -0,0 +1,94 @@ +def _assert_requested_present(script, result, name, version): + dist_info = script.site_packages / name + "-" + version + ".dist-info" + requested = dist_info / "REQUESTED" + assert dist_info in result.files_created + assert requested in result.files_created + + +def _assert_requested_absent(script, result, name, version): + dist_info = script.site_packages / name + "-" + version + ".dist-info" + requested = dist_info / "REQUESTED" + assert dist_info in result.files_created + assert requested not in result.files_created + + +def test_install_requested_basic(script, data, with_wheel): + result = script.pip( + "install", "--no-index", "-f", data.find_links, "require_simple" + ) + _assert_requested_present(script, result, "require_simple", "1.0") + # dependency is not REQUESTED + _assert_requested_absent(script, result, "simple", "3.0") + + +def test_install_requested_requirements(script, data, with_wheel): + script.scratch_path.joinpath("requirements.txt").write_text( + "require_simple\n" + ) + result = script.pip( + "install", + "--no-index", + "-f", + data.find_links, + "-r", + script.scratch_path / "requirements.txt", + ) + _assert_requested_present(script, result, "require_simple", "1.0") + _assert_requested_absent(script, result, "simple", "3.0") + + +def test_install_requested_dep_in_requirements(script, data, with_wheel): + script.scratch_path.joinpath("requirements.txt").write_text( + "require_simple\nsimple<3\n" + ) + result = script.pip( + "install", + "--no-index", + "-f", + data.find_links, + "-r", + script.scratch_path / "requirements.txt", + ) + _assert_requested_present(script, result, "require_simple", "1.0") + # simple must have REQUESTED because it is in requirements.txt + _assert_requested_present(script, result, "simple", "2.0") + + +def test_install_requested_reqs_and_constraints(script, data, with_wheel): + script.scratch_path.joinpath("requirements.txt").write_text( + "require_simple\n" + ) + script.scratch_path.joinpath("constraints.txt").write_text("simple<3\n") + result = script.pip( + "install", + "--no-index", + "-f", + data.find_links, + "-r", + script.scratch_path / "requirements.txt", + "-c", + script.scratch_path / "constraints.txt", + ) + _assert_requested_present(script, result, "require_simple", "1.0") + # simple must not have REQUESTED because it is merely a constraint + _assert_requested_absent(script, result, "simple", "2.0") + + +def test_install_requested_in_reqs_and_constraints(script, data, with_wheel): + script.scratch_path.joinpath("requirements.txt").write_text( + "require_simple\nsimple\n" + ) + script.scratch_path.joinpath("constraints.txt").write_text("simple<3\n") + result = script.pip( + "install", + "--no-index", + "-f", + data.find_links, + "-r", + script.scratch_path / "requirements.txt", + "-c", + script.scratch_path / "constraints.txt", + ) + _assert_requested_present(script, result, "require_simple", "1.0") + # simple must have REQUESTED because it is in requirements.txt + _assert_requested_present(script, result, "simple", "2.0") diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 388e8f01b7a..12eafaa0388 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -310,7 +310,7 @@ def test_std_install_requested(self, data, tmpdir): req_description=str(self.req), requested=True, ) - self.assert_installed() + self.assert_installed(0o644) requested_path = os.path.join(self.dest_dist_info, 'REQUESTED') assert os.path.isfile(requested_path) From a226f65cf5eb25af75513e34e5a55723bf228bec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sun, 12 Apr 2020 12:43:50 +0200 Subject: [PATCH 1980/3170] A constraint is not a top level requirement --- src/pip/_internal/cli/req_command.py | 2 +- src/pip/_internal/req/req_set.py | 9 ++++++--- src/pip/_internal/resolution/legacy/resolver.py | 2 +- 3 files changed, 8 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 1bc59c175c7..5b9aa2c3c76 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -310,7 +310,7 @@ def get_requirements( parsed_req, isolated=options.isolated_mode, ) - req_to_add.is_direct = True + req_to_add.is_direct = False requirements.append(req_to_add) for req in args: diff --git a/src/pip/_internal/req/req_set.py b/src/pip/_internal/req/req_set.py index d64bb78a327..6a779b06fbf 100644 --- a/src/pip/_internal/req/req_set.py +++ b/src/pip/_internal/req/req_set.py @@ -107,9 +107,8 @@ def add_requirement( ) # 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" + assert not install_req.is_direct or parent_req_name is None, ( + "a direct req shouldn't have a parent" ) # Unnamed requirements are scanned again and the requirement won't be @@ -165,6 +164,10 @@ def add_requirement( # If we're now installing a constraint, mark the existing # object for real installation. existing_req.constraint = False + # If we're now installing a top level requirement, mark the existing + # object as top level. + if install_req.is_direct: + existing_req.is_direct = True existing_req.extras = tuple(sorted( set(existing_req.extras) | set(install_req.extras) )) diff --git a/src/pip/_internal/resolution/legacy/resolver.py b/src/pip/_internal/resolution/legacy/resolver.py index 6eafc56eaa8..afda2380203 100644 --- a/src/pip/_internal/resolution/legacy/resolver.py +++ b/src/pip/_internal/resolution/legacy/resolver.py @@ -195,7 +195,7 @@ def _is_upgrade_allowed(self, req): return True else: assert self.upgrade_strategy == "only-if-needed" - return req.is_direct + return req.is_direct or req.constraint def _set_req_to_reinstall(self, req): # type: (InstallRequirement) -> None From b9a19f6be072ba0398aaa2cce23052296a002b76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Mon, 13 Apr 2020 11:09:05 +0200 Subject: [PATCH 1981/3170] Rename is_direct to user_supplied --- src/pip/_internal/cli/req_command.py | 8 ++++---- src/pip/_internal/req/req_install.py | 7 +++++-- src/pip/_internal/req/req_set.py | 6 +++--- src/pip/_internal/resolution/legacy/resolver.py | 4 ++-- src/pip/_internal/resolution/resolvelib/resolver.py | 2 +- tests/unit/test_req.py | 10 +++++----- 6 files changed, 20 insertions(+), 17 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 5b9aa2c3c76..204ea4545c3 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -310,7 +310,7 @@ def get_requirements( parsed_req, isolated=options.isolated_mode, ) - req_to_add.is_direct = False + req_to_add.user_supplied = False requirements.append(req_to_add) for req in args: @@ -318,7 +318,7 @@ def get_requirements( req, None, isolated=options.isolated_mode, use_pep517=options.use_pep517, ) - req_to_add.is_direct = True + req_to_add.user_supplied = True requirements.append(req_to_add) for req in options.editables: @@ -327,7 +327,7 @@ def get_requirements( isolated=options.isolated_mode, use_pep517=options.use_pep517, ) - req_to_add.is_direct = True + req_to_add.user_supplied = True requirements.append(req_to_add) # NOTE: options.require_hashes may be set if --require-hashes is True @@ -340,7 +340,7 @@ def get_requirements( isolated=options.isolated_mode, use_pep517=options.use_pep517 ) - req_to_add.is_direct = True + req_to_add.user_supplied = True requirements.append(req_to_add) # If any requirement has hash options, enable hash checking. diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 48ec9442114..eb8735ba555 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -171,7 +171,10 @@ def __init__( self.hash_options = hash_options if hash_options else {} # Set to True after successful preparation of this requirement self.prepared = False - self.is_direct = False + # User supplied requirement are explicitly requested for installation + # by the user via CLI arguments or requirements files, as opposed to, + # e.g. dependencies, extras or constraints. + self.user_supplied = False # Set by the legacy resolver when the requirement has been downloaded # TODO: This introduces a strong coupling between the resolver and the @@ -809,7 +812,7 @@ def install( pycompile=pycompile, warn_script_location=warn_script_location, direct_url=direct_url, - requested=self.is_direct, + requested=self.user_supplied, ) self.install_succeeded = True return diff --git a/src/pip/_internal/req/req_set.py b/src/pip/_internal/req/req_set.py index 6a779b06fbf..78b065cf4cf 100644 --- a/src/pip/_internal/req/req_set.py +++ b/src/pip/_internal/req/req_set.py @@ -107,7 +107,7 @@ def add_requirement( ) # This next bit is really a sanity check. - assert not install_req.is_direct or parent_req_name is None, ( + assert not install_req.user_supplied or parent_req_name is None, ( "a direct req shouldn't have a parent" ) @@ -166,8 +166,8 @@ def add_requirement( existing_req.constraint = False # If we're now installing a top level requirement, mark the existing # object as top level. - if install_req.is_direct: - existing_req.is_direct = True + if install_req.user_supplied: + existing_req.user_supplied = True existing_req.extras = tuple(sorted( set(existing_req.extras) | set(install_req.extras) )) diff --git a/src/pip/_internal/resolution/legacy/resolver.py b/src/pip/_internal/resolution/legacy/resolver.py index afda2380203..7fdff1bc4f3 100644 --- a/src/pip/_internal/resolution/legacy/resolver.py +++ b/src/pip/_internal/resolution/legacy/resolver.py @@ -195,7 +195,7 @@ def _is_upgrade_allowed(self, req): return True else: assert self.upgrade_strategy == "only-if-needed" - return req.is_direct or req.constraint + return req.user_supplied or req.constraint def _set_req_to_reinstall(self, req): # type: (InstallRequirement) -> None @@ -419,7 +419,7 @@ def add_req(subreq, extras_requested): # 'unnamed' requirements will get added here # 'unnamed' requirements can only come from being directly # provided by the user. - assert req_to_install.is_direct + assert req_to_install.user_supplied requirement_set.add_requirement( req_to_install, parent_req_name=None, ) diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 0e55357b824..bd474b7021d 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -118,7 +118,7 @@ def resolve(self, root_reqs, check_supported_wheels): else: constraints[name] = req.specifier else: - if req.is_direct and req.name: + if req.user_supplied and req.name: user_requested.add(canonicalize_name(req.name)) requirements.append( self.factory.make_requirement_from_install_req(req) diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index 2da0b62dbfc..1aee7fcdf0a 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -56,7 +56,7 @@ def get_processed_req_from_line(line, fname='file', lineno=1): parsed_req = handle_requirement_line(parsed_line) assert parsed_req is not None req = install_req_from_parsed_requirement(parsed_req) - req.is_direct = True + req.user_supplied = True return req @@ -109,7 +109,7 @@ def test_no_reuse_existing_build_dir(self, data): pass reqset = RequirementSet() req = install_req_from_line('simple') - req.is_direct = True + req.user_supplied = True reqset.add_requirement(req) finder = make_test_finder(find_links=[data.find_links]) with self._basic_resolver(finder) as resolver: @@ -133,7 +133,7 @@ def test_environment_marker_extras(self, data): req = install_req_from_editable( data.packages.joinpath("LocalEnvironMarker") ) - req.is_direct = True + req.user_supplied = True reqset.add_requirement(req) finder = make_test_finder(find_links=[data.find_links]) with self._basic_resolver(finder) as resolver: @@ -626,10 +626,10 @@ def test_exclusive_environment_markers(): """Make sure RequirementSet accepts several excluding env markers""" eq36 = install_req_from_line( "Django>=1.6.10,<1.7 ; python_version == '3.6'") - eq36.is_direct = True + eq36.user_supplied = True ne36 = install_req_from_line( "Django>=1.6.10,<1.8 ; python_version != '3.6'") - ne36.is_direct = True + ne36.user_supplied = True req_set = RequirementSet() req_set.add_requirement(eq36) From aa0c1674987651fac32a810275a0866e5e3a891c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sat, 16 May 2020 11:38:57 +0200 Subject: [PATCH 1982/3170] Make sure user_supplied is propagated where needed --- src/pip/_internal/cli/req_command.py | 10 +++++----- src/pip/_internal/req/constructors.py | 20 +++++++++++++++---- src/pip/_internal/req/req_install.py | 5 +++-- .../resolution/resolvelib/candidates.py | 3 +++ 4 files changed, 27 insertions(+), 11 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 204ea4545c3..a215d459314 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -309,25 +309,25 @@ def get_requirements( req_to_add = install_req_from_parsed_requirement( parsed_req, isolated=options.isolated_mode, + user_supplied=False, ) - req_to_add.user_supplied = False requirements.append(req_to_add) for req in args: req_to_add = install_req_from_line( req, None, isolated=options.isolated_mode, use_pep517=options.use_pep517, + user_supplied=True, ) - req_to_add.user_supplied = True requirements.append(req_to_add) for req in options.editables: req_to_add = install_req_from_editable( req, + user_supplied=True, isolated=options.isolated_mode, use_pep517=options.use_pep517, ) - req_to_add.user_supplied = True requirements.append(req_to_add) # NOTE: options.require_hashes may be set if --require-hashes is True @@ -338,9 +338,9 @@ def get_requirements( req_to_add = install_req_from_parsed_requirement( parsed_req, isolated=options.isolated_mode, - use_pep517=options.use_pep517 + use_pep517=options.use_pep517, + user_supplied=True, ) - req_to_add.user_supplied = True requirements.append(req_to_add) # If any requirement has hash options, enable hash checking. diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index 7ca3370110f..46b1daa902e 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -219,7 +219,8 @@ def install_req_from_editable( use_pep517=None, # type: Optional[bool] isolated=False, # type: bool options=None, # type: Optional[Dict[str, Any]] - constraint=False # type: bool + constraint=False, # type: bool + user_supplied=False, # type: bool ): # type: (...) -> InstallRequirement @@ -228,6 +229,7 @@ def install_req_from_editable( return InstallRequirement( parts.requirement, comes_from=comes_from, + user_supplied=user_supplied, editable=True, link=parts.link, constraint=constraint, @@ -382,6 +384,7 @@ def install_req_from_line( options=None, # type: Optional[Dict[str, Any]] constraint=False, # type: bool line_source=None, # type: Optional[str] + user_supplied=False, # type: bool ): # type: (...) -> InstallRequirement """Creates an InstallRequirement from a name, which might be a @@ -400,6 +403,7 @@ def install_req_from_line( hash_options=options.get("hashes", {}) if options else {}, constraint=constraint, extras=parts.extras, + user_supplied=user_supplied, ) @@ -407,7 +411,8 @@ def install_req_from_req_string( req_string, # type: str comes_from=None, # type: Optional[InstallRequirement] isolated=False, # type: bool - use_pep517=None # type: Optional[bool] + use_pep517=None, # type: Optional[bool] + user_supplied=False, # type: bool ): # type: (...) -> InstallRequirement try: @@ -429,14 +434,19 @@ def install_req_from_req_string( ) return InstallRequirement( - req, comes_from, isolated=isolated, use_pep517=use_pep517 + req, + comes_from, + isolated=isolated, + use_pep517=use_pep517, + user_supplied=user_supplied, ) def install_req_from_parsed_requirement( parsed_req, # type: ParsedRequirement isolated=False, # type: bool - use_pep517=None # type: Optional[bool] + use_pep517=None, # type: Optional[bool] + user_supplied=False, # type: bool ): # type: (...) -> InstallRequirement if parsed_req.is_editable: @@ -446,6 +456,7 @@ def install_req_from_parsed_requirement( use_pep517=use_pep517, constraint=parsed_req.constraint, isolated=isolated, + user_supplied=user_supplied, ) else: @@ -457,5 +468,6 @@ def install_req_from_parsed_requirement( options=parsed_req.options, constraint=parsed_req.constraint, line_source=parsed_req.line_source, + user_supplied=user_supplied, ) return req diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index eb8735ba555..83de2cd135d 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -110,7 +110,8 @@ def __init__( global_options=None, # type: Optional[List[str]] hash_options=None, # type: Optional[Dict[str, List[str]]] constraint=False, # type: bool - extras=() # type: Iterable[str] + extras=(), # type: Iterable[str] + user_supplied=False, # type: bool ): # type: (...) -> None assert req is None or isinstance(req, Requirement), req @@ -174,7 +175,7 @@ def __init__( # User supplied requirement are explicitly requested for installation # by the user via CLI arguments or requirements files, as opposed to, # e.g. dependencies, extras or constraints. - self.user_supplied = False + self.user_supplied = user_supplied # Set by the legacy resolver when the requirement has been downloaded # TODO: This introduces a strong coupling between the resolver and the diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 695b42a96cc..9229b1aa61d 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -47,6 +47,7 @@ def make_install_req_from_link(link, template): line = link.url ireq = install_req_from_line( line, + user_supplied=template.user_supplied, comes_from=template.comes_from, use_pep517=template.use_pep517, isolated=template.isolated, @@ -68,6 +69,7 @@ def make_install_req_from_editable(link, template): assert template.editable, "template not editable" return install_req_from_editable( link.url, + user_supplied=template.user_supplied, comes_from=template.comes_from, use_pep517=template.use_pep517, isolated=template.isolated, @@ -91,6 +93,7 @@ def make_install_req_from_dist(dist, template): line = "{}=={}".format(project_name, dist.parsed_version) ireq = install_req_from_line( line, + user_supplied=template.user_supplied, comes_from=template.comes_from, use_pep517=template.use_pep517, isolated=template.isolated, From 21bf4f51f8c78b2a6850364e66c3faf91a165e3d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sat, 16 May 2020 12:45:43 +0200 Subject: [PATCH 1983/3170] Uniform use of user supplied vocabulary --- news/7811.feature | 2 +- src/pip/_internal/req/req_set.py | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/news/7811.feature b/news/7811.feature index b06aaa5bf27..0b471405a9c 100644 --- a/news/7811.feature +++ b/news/7811.feature @@ -1,2 +1,2 @@ -Generate PEP 376 REQUESTED metadata for top level requirements installed +Generate PEP 376 REQUESTED metadata for user supplied requirements installed by pip. diff --git a/src/pip/_internal/req/req_set.py b/src/pip/_internal/req/req_set.py index 78b065cf4cf..ab4b6f849b4 100644 --- a/src/pip/_internal/req/req_set.py +++ b/src/pip/_internal/req/req_set.py @@ -108,7 +108,7 @@ def add_requirement( # This next bit is really a sanity check. assert not install_req.user_supplied or parent_req_name is None, ( - "a direct req shouldn't have a parent" + "a user supplied req shouldn't have a parent" ) # Unnamed requirements are scanned again and the requirement won't be @@ -164,8 +164,8 @@ def add_requirement( # If we're now installing a constraint, mark the existing # object for real installation. existing_req.constraint = False - # If we're now installing a top level requirement, mark the existing - # object as top level. + # If we're now installing a user supplied requirement, + # mark the existing object as such. if install_req.user_supplied: existing_req.user_supplied = True existing_req.extras = tuple(sorted( From e68b71b8c2b5294cb19601b54b5da60eb05635ee Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 31 May 2020 23:04:51 +0530 Subject: [PATCH 1984/3170] Apply suggestions from code review --- tests/functional/test_install.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 4184ccd55cf..6b55746ea5f 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -852,24 +852,18 @@ def test_install_package_with_target(script): """ target_dir = script.scratch_path / 'target' result = script.pip_install_local('-t', target_dir, "simple==1.0") - result.did_create( - Path('scratch') / 'target' / 'simple' - ) + result.did_create(Path('scratch') / 'target' / 'simple') # Test repeated call without --upgrade, no files should have changed result = script.pip_install_local( '-t', target_dir, "simple==1.0", expect_stderr=True, ) - result.did_not_update( - Path('scratch') / 'target' / 'simple' - ) + result.did_not_update(Path('scratch') / 'target' / 'simple') # Test upgrade call, check that new version is installed result = script.pip_install_local('--upgrade', '-t', target_dir, "simple==2.0") - result.did_update( - Path('scratch') / 'target' / 'simple' - ) + result.did_update(Path('scratch') / 'target' / 'simple') egg_folder = ( Path('scratch') / 'target' / 'simple-2.0-py{pyversion}.egg-info'.format(**globals())) From a851753ff0915ae65102a5aaeee4658f9e1b31fc Mon Sep 17 00:00:00 2001 From: Surbhi Sharma <ssurbhi560@gmail.com> Date: Mon, 1 Jun 2020 18:18:33 +0530 Subject: [PATCH 1985/3170] fix method call --- tests/functional/test_download.py | 120 ++++++++---------------------- 1 file changed, 30 insertions(+), 90 deletions(-) diff --git a/tests/functional/test_download.py b/tests/functional/test_download.py index 15ce4c1f355..3e80fa5c538 100644 --- a/tests/functional/test_download.py +++ b/tests/functional/test_download.py @@ -55,9 +55,7 @@ def test_download_wheel(script, data): '-f', data.packages, '-d', '.', 'meta' ) - result.did_create( - Path('scratch') / 'meta-1.0-py2.py3-none-any.whl' - ) + result.did_create(Path('scratch') / 'meta-1.0-py2.py3-none-any.whl') result.did_not_create(script.site_packages / 'piptestpackage') @@ -73,9 +71,7 @@ def test_single_download_from_requirements_file(script): result = script.pip( 'download', '-r', script.scratch_path / 'test-req.txt', '-d', '.', ) - result.did_create( - Path('scratch') / 'INITools-0.1.tar.gz' - ) + result.did_create(Path('scratch') / 'INITools-0.1.tar.gz') result.did_not_create(script.site_packages / 'initools') @@ -87,9 +83,7 @@ def test_basic_download_should_download_dependencies(script): result = script.pip( 'download', 'Paste[openid]==1.7.5.1', '-d', '.' ) - result.did_create( - Path('scratch') / 'Paste-1.7.5.1.tar.gz' - ) + result.did_create(Path('scratch') / 'Paste-1.7.5.1.tar.gz') openid_tarball_prefix = str(Path('scratch') / 'python-openid-') assert any( path.startswith(openid_tarball_prefix) for path in result.files_created @@ -107,9 +101,7 @@ def test_download_wheel_archive(script, data): 'download', wheel_path, '-d', '.', '--no-deps' ) - result.did_create( - Path('scratch') / wheel_filename - ) + result.did_create(Path('scratch') / wheel_filename) def test_download_should_download_wheel_deps(script, data): @@ -123,12 +115,8 @@ def test_download_should_download_wheel_deps(script, data): 'download', wheel_path, '-d', '.', '--find-links', data.find_links, '--no-index' ) - result.did_create( - Path('scratch') / wheel_filename - ) - result.did_create( - Path('scratch') / dep_filename - ) + result.did_create(Path('scratch') / wheel_filename) + result.did_create(Path('scratch') / dep_filename) @pytest.mark.network @@ -143,9 +131,7 @@ def test_download_should_skip_existing_files(script): result = script.pip( 'download', '-r', script.scratch_path / 'test-req.txt', '-d', '.', ) - result.did_create( - Path('scratch') / 'INITools-0.1.tar.gz' - ) + result.did_create(Path('scratch') / 'INITools-0.1.tar.gz') result.did_not_create(script.site_packages / 'initools') # adding second package to test-req.txt @@ -162,9 +148,7 @@ def test_download_should_skip_existing_files(script): assert any( path.startswith(openid_tarball_prefix) for path in result.files_created ) - result.did_not_create( - Path('scratch') / 'INITools-0.1.tar.gz' - ) + result.did_not_create(Path('scratch') / 'INITools-0.1.tar.gz') result.did_not_create(script.site_packages / 'initools') result.did_not_create(script.site_packages / 'openid') @@ -177,9 +161,7 @@ def test_download_vcs_link(script): result = script.pip( 'download', '-d', '.', 'git+git://github.com/pypa/pip-test-package.git' ) - result.did_create( - Path('scratch') / 'pip-test-package-0.1.1.zip' - ) + result.did_create(Path('scratch') / 'pip-test-package-0.1.1.zip') result.did_not_create(script.site_packages / 'piptestpackage') @@ -197,9 +179,7 @@ def test_only_binary_set_then_download_specific_platform(script, data): '--platform', 'linux_x86_64', 'fake' ) - result.did_create( - Path('scratch') / 'fake-1.0-py2.py3-none-any.whl' - ) + result.did_create(Path('scratch') / 'fake-1.0-py2.py3-none-any.whl') def test_no_deps_set_then_download_specific_platform(script, data): @@ -216,9 +196,7 @@ def test_no_deps_set_then_download_specific_platform(script, data): '--platform', 'linux_x86_64', 'fake' ) - result.did_create( - Path('scratch') / 'fake-1.0-py2.py3-none-any.whl' - ) + result.did_create(Path('scratch') / 'fake-1.0-py2.py3-none-any.whl') def test_download_specific_platform_fails(script, data): @@ -273,9 +251,7 @@ def test_download_specify_platform(script, data): '--platform', 'linux_x86_64', 'fake' ) - result.did_create( - Path('scratch') / 'fake-1.0-py2.py3-none-any.whl' - ) + result.did_create(Path('scratch') / 'fake-1.0-py2.py3-none-any.whl') result = script.pip( 'download', '--no-index', '--find-links', data.find_links, @@ -357,9 +333,7 @@ def test_download_universal(self, platform, script, data): '--platform', platform, 'fake', ) - result.did_create( - Path('scratch') / 'fake-1.0-py2.py3-none-any.whl' - ) + result.did_create(Path('scratch') / 'fake-1.0-py2.py3-none-any.whl') @pytest.mark.parametrize("wheel_abi,platform", [ ("manylinux1_x86_64", "manylinux1_x86_64"), @@ -384,9 +358,7 @@ def test_download_compatible_manylinuxes( '--platform', platform, 'fake', ) - result.did_create( - Path('scratch') / wheel - ) + result.did_create(Path('scratch') / wheel) def test_explicit_platform_only(self, data, script): """ @@ -417,9 +389,7 @@ def test_download__python_version(script, data): '--python-version', '2', 'fake' ) - result.did_create( - Path('scratch') / 'fake-1.0-py2.py3-none-any.whl' - ) + result.did_create(Path('scratch') / 'fake-1.0-py2.py3-none-any.whl') result = script.pip( 'download', '--no-index', '--find-links', data.find_links, @@ -466,9 +436,7 @@ def test_download__python_version(script, data): '--python-version', '2', 'fake' ) - result.did_create( - Path('scratch') / 'fake-1.0-py2-none-any.whl' - ) + result.did_create(Path('scratch') / 'fake-1.0-py2-none-any.whl') result = script.pip( 'download', '--no-index', '--find-links', data.find_links, @@ -485,9 +453,7 @@ def test_download__python_version(script, data): '--python-version', '3', 'fake' ) - result.did_create( - Path('scratch') / 'fake-2.0-py3-none-any.whl' - ) + result.did_create(Path('scratch') / 'fake-2.0-py3-none-any.whl') def make_wheel_with_python_requires(script, package_name, python_requires): @@ -561,9 +527,7 @@ def test_download_specify_abi(script, data): '--abi', 'fake_abi', 'fake' ) - result.did_create( - Path('scratch') / 'fake-1.0-py2.py3-none-any.whl' - ) + result.did_create(Path('scratch') / 'fake-1.0-py2.py3-none-any.whl') result = script.pip( 'download', '--no-index', '--find-links', data.find_links, @@ -623,9 +587,7 @@ def test_download_specify_implementation(script, data): '--implementation', 'fk', 'fake' ) - result.did_create( - Path('scratch') / 'fake-1.0-py2.py3-none-any.whl' - ) + result.did_create(Path('scratch') / 'fake-1.0-py2.py3-none-any.whl') data.reset() fake_wheel(data, 'fake-1.0-fk3-none-any.whl') @@ -637,9 +599,7 @@ def test_download_specify_implementation(script, data): '--python-version', '3', 'fake' ) - result.did_create( - Path('scratch') / 'fake-1.0-fk3-none-any.whl' - ) + result.did_create(Path('scratch') / 'fake-1.0-fk3-none-any.whl') result = script.pip( 'download', '--no-index', '--find-links', data.find_links, @@ -680,12 +640,8 @@ def test_download_prefer_binary_when_tarball_higher_than_wheel(script, data): '-f', data.packages, '-d', '.', 'source' ) - result.did_create( - Path('scratch') / 'source-0.8-py2.py3-none-any.whl' - ) - result.did_not_create( - Path('scratch') / 'source-1.0.tar.gz' - ) + result.did_create(Path('scratch') / 'source-0.8-py2.py3-none-any.whl') + result.did_not_create(Path('scratch') / 'source-1.0.tar.gz') def test_prefer_binary_tarball_higher_than_wheel_req_file(script, data): @@ -702,12 +658,8 @@ def test_prefer_binary_tarball_higher_than_wheel_req_file(script, data): '-d', '.' ) - result.did_create( - Path('scratch') / 'source-0.8-py2.py3-none-any.whl' - ) - result.did_not_create( - Path('scratch') / 'source-1.0.tar.gz' - ) + result.did_create(Path('scratch') / 'source-0.8-py2.py3-none-any.whl') + result.did_not_create(Path('scratch') / 'source-1.0.tar.gz') def test_download_prefer_binary_when_wheel_doesnt_satisfy_req(script, data): @@ -724,12 +676,8 @@ def test_download_prefer_binary_when_wheel_doesnt_satisfy_req(script, data): '-d', '.', '-r', script.scratch_path / 'test-req.txt' ) - result.did_create( - Path('scratch') / 'source-1.0.tar.gz' - ) - result.did_not_create( - Path('scratch') / 'source-0.8-py2.py3-none-any.whl' - ) + result.did_create(Path('scratch') / 'source-1.0.tar.gz') + result.did_not_create(Path('scratch') / 'source-0.8-py2.py3-none-any.whl') def test_prefer_binary_when_wheel_doesnt_satisfy_req_req_file(script, data): @@ -746,12 +694,8 @@ def test_prefer_binary_when_wheel_doesnt_satisfy_req_req_file(script, data): '-d', '.', '-r', script.scratch_path / 'test-req.txt' ) - result.did_create( - Path('scratch') / 'source-1.0.tar.gz' - ) - result.did_not_create( - Path('scratch') / 'source-0.8-py2.py3-none-any.whl' - ) + result.did_create(Path('scratch') / 'source-1.0.tar.gz') + result.did_not_create(Path('scratch') / 'source-0.8-py2.py3-none-any.whl') def test_download_prefer_binary_when_only_tarball_exists(script, data): @@ -762,9 +706,7 @@ def test_download_prefer_binary_when_only_tarball_exists(script, data): '-f', data.packages, '-d', '.', 'source' ) - result.did_create( - Path('scratch') / 'source-1.0.tar.gz' - ) + result.did_create(Path('scratch') / 'source-1.0.tar.gz') def test_prefer_binary_when_only_tarball_exists_req_file(script, data): @@ -779,9 +721,7 @@ def test_prefer_binary_when_only_tarball_exists_req_file(script, data): '-d', '.', '-r', script.scratch_path / 'test-req.txt' ) - result.did_create( - Path('scratch') / 'source-1.0.tar.gz' - ) + result.did_create(Path('scratch') / 'source-1.0.tar.gz') @pytest.fixture(scope="session") From afb4e6f5ba84a4baf6484a622deaa8955da23bfb Mon Sep 17 00:00:00 2001 From: Noah Gorny <noah.bar.ilan@gmail.com> Date: Thu, 26 Mar 2020 21:01:41 +0200 Subject: [PATCH 1986/3170] cli: cmdoptions: Mark always_unzip as depreceated --- src/pip/_internal/cli/cmdoptions.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index 5b5647d64f4..adfc81767c4 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -832,6 +832,10 @@ def _handle_no_use_pep517(option, opt, value, parser): action='store_true', help=SUPPRESS_HELP, ) # type: Callable[..., Option] +# TODO: Move into a class that inherits from partial, currently does not +# work as mypy complains functools.partial is a generic class. +# This way we know we can ignore this option in docs auto generation +setattr(always_unzip, 'deprecated', True) def _handle_merge_hash(option, opt_str, value, parser): From a80d5426cac21a73e830095f54ffc16c6ce6b57d Mon Sep 17 00:00:00 2001 From: Noah Gorny <noah.bar.ilan@gmail.com> Date: Thu, 26 Mar 2020 21:08:16 +0200 Subject: [PATCH 1987/3170] docs: Reqfile options are now generated automatically --- docs/html/reference/pip_install.rst | 13 +---------- docs/pip_sphinxext.py | 34 +++++++++++++++++++++++++++++ news/7908.doc | 2 ++ 3 files changed, 37 insertions(+), 12 deletions(-) create mode 100644 news/7908.doc diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index 294e969f3e1..f33e279373d 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -156,18 +156,7 @@ To interpret the requirements file in UTF-8 format add a comment The following options are supported: - * :ref:`-i, --index-url <install_--index-url>` - * :ref:`--extra-index-url <install_--extra-index-url>` - * :ref:`--no-index <install_--no-index>` - * :ref:`-c, --constraint <install_--constraint>` - * :ref:`-r, --requirement <install_--requirement>` - * :ref:`-e, --editable <install_--editable>` - * :ref:`-f, --find-links <install_--find-links>` - * :ref:`--no-binary <install_--no-binary>` - * :ref:`--only-binary <install_--only-binary>` - * :ref:`--require-hashes <install_--require-hashes>` - * :ref:`--pre <install_--pre>` - * :ref:`--trusted-host <--trusted-host>` +.. pip-requirements-file-options-ref-list:: For example, to specify :ref:`--no-index <install_--no-index>` and two :ref:`--find-links <install_--find-links>` locations: diff --git a/docs/pip_sphinxext.py b/docs/pip_sphinxext.py index 29ca34c7289..4db8db5d7b6 100644 --- a/docs/pip_sphinxext.py +++ b/docs/pip_sphinxext.py @@ -10,6 +10,7 @@ from pip._internal.cli import cmdoptions from pip._internal.commands import create_command +from pip._internal.req.req_file import SUPPORTED_OPTIONS class PipCommandUsage(rst.Directive): @@ -108,9 +109,42 @@ def process_options(self): ) +class PipReqFileOptionsReference(PipOptions): + + def process_options(self): + for option in SUPPORTED_OPTIONS: + if getattr(option, 'deprecated', False): + continue + + opt = option() + opt_name = opt._long_opts[0] + if opt._short_opts: + short_opt_name = '{}, '.format(opt._short_opts[0]) + else: + short_opt_name = '' + + from_install = ( + 'install_' + if option not in cmdoptions.general_group['options'] else + '' + ) + self.view_list.append( + ' * :ref:`{short}{long}<{prefix}{opt_name}>`'.format( + short=short_opt_name, + long=opt_name, + prefix=from_install, + opt_name=opt_name + ), + "\n" + ) + + def setup(app): app.add_directive('pip-command-usage', PipCommandUsage) app.add_directive('pip-command-description', PipCommandDescription) app.add_directive('pip-command-options', PipCommandOptions) app.add_directive('pip-general-options', PipGeneralOptions) app.add_directive('pip-index-options', PipIndexOptions) + app.add_directive( + 'pip-requirements-file-options-ref-list', PipReqFileOptionsReference + ) diff --git a/news/7908.doc b/news/7908.doc new file mode 100644 index 00000000000..8a5d182196e --- /dev/null +++ b/news/7908.doc @@ -0,0 +1,2 @@ +Requirements file options are now extracted from the code instead +of being maintained manually. From 7494d703569008540cb6c62029dae76a327d54dc Mon Sep 17 00:00:00 2001 From: Noah Gorny <noah.bar.ilan@gmail.com> Date: Mon, 27 Apr 2020 14:39:25 +0300 Subject: [PATCH 1988/3170] docs: Improve prefix calculation of reqfile option autogeneration --- docs/pip_sphinxext.py | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/docs/pip_sphinxext.py b/docs/pip_sphinxext.py index 4db8db5d7b6..6cc7a2c82ec 100644 --- a/docs/pip_sphinxext.py +++ b/docs/pip_sphinxext.py @@ -9,7 +9,7 @@ from docutils.statemachine import ViewList from pip._internal.cli import cmdoptions -from pip._internal.commands import create_command +from pip._internal.commands import commands_dict, create_command from pip._internal.req.req_file import SUPPORTED_OPTIONS @@ -111,6 +111,14 @@ def process_options(self): class PipReqFileOptionsReference(PipOptions): + def determine_opt_prefix(self, opt_name): + for command in commands_dict: + cmd = create_command(command) + if cmd.cmd_opts.has_option(opt_name): + return command + + raise KeyError('Could not identify prefix of opt {}'.format(opt_name)) + def process_options(self): for option in SUPPORTED_OPTIONS: if getattr(option, 'deprecated', False): @@ -123,16 +131,16 @@ def process_options(self): else: short_opt_name = '' - from_install = ( - 'install_' - if option not in cmdoptions.general_group['options'] else - '' - ) + if option in cmdoptions.general_group['options']: + prefix = '' + else: + prefix = '{}_'.format(self.determine_opt_prefix(opt_name)) + self.view_list.append( ' * :ref:`{short}{long}<{prefix}{opt_name}>`'.format( short=short_opt_name, long=opt_name, - prefix=from_install, + prefix=prefix, opt_name=opt_name ), "\n" From 4c8b175649a46676273cc55cf33f326d41cf378a Mon Sep 17 00:00:00 2001 From: Noah <noah.bar.ilan@gmail.com> Date: Sun, 31 May 2020 21:07:54 +0300 Subject: [PATCH 1989/3170] news: Update news/7908.doc Co-authored-by: Pradyun Gedam <pradyunsg@gmail.com> --- news/7908.doc | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/news/7908.doc b/news/7908.doc index 8a5d182196e..ec5ee72ac2e 100644 --- a/news/7908.doc +++ b/news/7908.doc @@ -1,2 +1,2 @@ -Requirements file options are now extracted from the code instead -of being maintained manually. +List of options supported in requirements file are extracted from source of truth, +instead of being maintained manually. From 92a687ce66a5db0d0443471a56ad1f5dd8a3f743 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 2 Jun 2020 11:51:39 +0800 Subject: [PATCH 1990/3170] Make failing test fail --- tests/functional/test_install_reqs.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index d39263b98ea..d389c418e76 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -507,7 +507,6 @@ def test_install_distribution_union_conflicting_extras(script, data): assert "Conflict" in result.stderr -@pytest.mark.fails_on_new_resolver def test_install_unsupported_wheel_link_with_marker(script): script.scratch_path.joinpath("with-marker.txt").write_text( textwrap.dedent("""\ From 258bd7945ea6e7131a825b525db68afb60bd42de Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 2 Jun 2020 11:53:08 +0800 Subject: [PATCH 1991/3170] Allow Candidate.iter_dependencies() to yield None --- src/pip/_internal/resolution/resolvelib/base.py | 2 +- src/pip/_internal/resolution/resolvelib/candidates.py | 8 ++++---- src/pip/_internal/resolution/resolvelib/provider.py | 2 +- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/base.py b/src/pip/_internal/resolution/resolvelib/base.py index 57013b7b214..03d297ba2e7 100644 --- a/src/pip/_internal/resolution/resolvelib/base.py +++ b/src/pip/_internal/resolution/resolvelib/base.py @@ -55,7 +55,7 @@ def is_installed(self): raise NotImplementedError("Override in subclass") def iter_dependencies(self): - # type: () -> Iterable[Requirement] + # type: () -> Iterable[Optional[Requirement]] raise NotImplementedError("Override in subclass") def get_install_requirement(self): diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 695b42a96cc..58684e832c5 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -217,7 +217,7 @@ def _get_requires_python_specifier(self): return spec def iter_dependencies(self): - # type: () -> Iterable[Requirement] + # type: () -> Iterable[Optional[Requirement]] for r in self.dist.requires(): yield self._factory.make_requirement_from_spec(str(r), self._ireq) python_dep = self._factory.make_requires_python_requirement( @@ -343,7 +343,7 @@ def version(self): return self.dist.parsed_version def iter_dependencies(self): - # type: () -> Iterable[Requirement] + # type: () -> Iterable[Optional[Requirement]] for r in self.dist.requires(): yield self._factory.make_requirement_from_spec(str(r), self._ireq) @@ -425,7 +425,7 @@ def is_installed(self): return self.base.is_installed def iter_dependencies(self): - # type: () -> Iterable[Requirement] + # type: () -> Iterable[Optional[Requirement]] factory = self.base._factory # The user may have specified extras that the candidate doesn't @@ -486,7 +486,7 @@ def version(self): return self._version def iter_dependencies(self): - # type: () -> Iterable[Requirement] + # type: () -> Iterable[Optional[Requirement]] return () def get_install_requirement(self): diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index 98b9f94207b..72f16205981 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -147,4 +147,4 @@ def get_dependencies(self, candidate): # type: (Candidate) -> Sequence[Requirement] if self._ignore_dependencies: return [] - return list(candidate.iter_dependencies()) + return [r for r in candidate.iter_dependencies() if r is not None] From d5204dd0bad41e24224569c50f8f1cf1fb868c67 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 2 Jun 2020 11:55:12 +0800 Subject: [PATCH 1992/3170] Move markers check into the ireq->req constructor This makes the check apply to ALL requirement constructions, no exceptions. --- .../resolution/resolvelib/factory.py | 20 +++++++++---------- .../resolution/resolvelib/resolver.py | 8 ++++---- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 56f13632bf9..a864b25298e 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -233,8 +233,14 @@ def find_candidates(self, requirements, constraint): if all(req.is_satisfied_by(c) for req in requirements) ) - def make_requirement_from_install_req(self, ireq): - # type: (InstallRequirement) -> Requirement + def make_requirement_from_install_req(self, ireq, requested_extras): + # type: (InstallRequirement, Iterable[str]) -> Optional[Requirement] + if not ireq.match_markers(requested_extras): + logger.info( + "Ignoring %s: markers '%s' don't match your environment", + ireq.name, ireq.markers, + ) + return None if not ireq.link: return SpecifierRequirement(ireq) cand = self._make_candidate_from_link( @@ -253,7 +259,7 @@ def make_requirement_from_candidate(self, candidate): def make_requirement_from_spec(self, specifier, comes_from): # type: (str, InstallRequirement) -> Requirement ireq = self._make_install_req_from_spec(specifier, comes_from) - return self.make_requirement_from_install_req(ireq) + return self.make_requirement_from_install_req(ireq, ()) def make_requirement_from_spec_matching_extras( self, @@ -263,13 +269,7 @@ def make_requirement_from_spec_matching_extras( ): # type: (...) -> Optional[Requirement] ireq = self._make_install_req_from_spec(specifier, comes_from) - if not ireq.match_markers(requested_extras): - logger.info( - "Ignoring %s: markers '%s' don't match your environment", - ireq.name, ireq.markers, - ) - return None - return self.make_requirement_from_install_req(ireq) + return self.make_requirement_from_install_req(ireq, requested_extras) def make_requires_python_requirement(self, specifier): # type: (Optional[SpecifierSet]) -> Optional[Requirement] diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 0e55357b824..0417852817c 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -106,8 +106,6 @@ def resolve(self, root_reqs, check_supported_wheels): user_requested = set() # type: Set[str] requirements = [] for req in root_reqs: - if not req.match_markers(): - continue if req.constraint: # Ensure we only accept valid constraints reject_invalid_constraint_types(req) @@ -120,9 +118,11 @@ def resolve(self, root_reqs, check_supported_wheels): else: if req.is_direct and req.name: user_requested.add(canonicalize_name(req.name)) - requirements.append( - self.factory.make_requirement_from_install_req(req) + r = self.factory.make_requirement_from_install_req( + req, requested_extras=(), ) + if r is not None: + requirements.append(r) provider = PipProvider( factory=self.factory, From 1719fc3dcc9d2e1662ec5aee63738c5fe9b45e87 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 2 Jun 2020 11:59:03 +0800 Subject: [PATCH 1993/3170] Merge spec->req constructors Since both functions now return Optional[Requirement], we can just use the same implementation. --- src/pip/_internal/resolution/resolvelib/candidates.py | 2 +- src/pip/_internal/resolution/resolvelib/factory.py | 7 +------ 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 58684e832c5..44b6123f8bc 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -445,7 +445,7 @@ def iter_dependencies(self): yield factory.make_requirement_from_candidate(self.base) for r in self.base.dist.requires(valid_extras): - requirement = factory.make_requirement_from_spec_matching_extras( + requirement = factory.make_requirement_from_spec( str(r), self.base._ireq, valid_extras, ) if requirement: diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index a864b25298e..b860e4415e2 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -256,12 +256,7 @@ def make_requirement_from_candidate(self, candidate): # type: (Candidate) -> ExplicitRequirement return ExplicitRequirement(candidate) - def make_requirement_from_spec(self, specifier, comes_from): - # type: (str, InstallRequirement) -> Requirement - ireq = self._make_install_req_from_spec(specifier, comes_from) - return self.make_requirement_from_install_req(ireq, ()) - - def make_requirement_from_spec_matching_extras( + def make_requirement_from_spec( self, specifier, # type: str comes_from, # type: InstallRequirement From cc86b7f1796a02c738670bd236cfa2ac566e2760 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Tue, 2 Jun 2020 13:54:01 +0530 Subject: [PATCH 1994/3170] Use bitwise AND to set file permissions --- src/pip/_internal/utils/unpacking.py | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/utils/unpacking.py b/src/pip/_internal/utils/unpacking.py index fe71d26e355..2c54131981f 100644 --- a/src/pip/_internal/utils/unpacking.py +++ b/src/pip/_internal/utils/unpacking.py @@ -95,6 +95,15 @@ def is_within_directory(directory, target): return prefix == abs_directory +def set_file_modes(path): + # type: (Union[str, Text]) -> None + """ + Make file present at path have execute for user/group/world + (chmod +x) is no-op on windows per python docs + """ + os.chmod(path, (0o777 & ~current_umask() | 0o111)) + + def unzip_file(filename, location, flatten=True): # type: (str, str, bool) -> None """ @@ -140,9 +149,7 @@ def unzip_file(filename, location, flatten=True): # if mode and regular file and any execute permissions for # user/group/world? if mode and stat.S_ISREG(mode) and mode & 0o111: - # make dest file have execute for user/group/world - # (chmod +x) no-op on windows per python docs - os.chmod(fn, (0o777 - current_umask() | 0o111)) + set_file_modes(fn) finally: zipfp.close() @@ -225,9 +232,7 @@ def untar_file(filename, location): tar.utime(member, path) # type: ignore # member have any execute permissions for user/group/world? if member.mode & 0o111: - # make dest file have execute for user/group/world - # no-op on windows per python docs - os.chmod(path, (0o777 - current_umask() | 0o111)) + set_file_modes(path) finally: tar.close() From 885e5f95529cc949b658482f8f89a76e2427f042 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Tue, 2 Jun 2020 14:17:43 +0530 Subject: [PATCH 1995/3170] Fixed example for defining multiple values for supported options --- docs/html/user_guide.rst | 1 - 1 file changed, 1 deletion(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 3195a4011ba..3e25604ae00 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -455,7 +455,6 @@ which can be written on multiple lines: http://mirror1.example.com http://mirror2.example.com - [install] trusted-host = http://mirror1.example.com http://mirror2.example.com From ef483f54998a75b909dd817d6fa0e468949e4f82 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Tue, 2 Jun 2020 14:19:18 +0530 Subject: [PATCH 1996/3170] Add news entry --- news/8373.doc | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/8373.doc diff --git a/news/8373.doc b/news/8373.doc new file mode 100644 index 00000000000..dc804c6e506 --- /dev/null +++ b/news/8373.doc @@ -0,0 +1 @@ +Fix example for defining multiple values for options which support them From 20a933d8f752e819bd19232bfb50393123ca94c8 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Tue, 2 Jun 2020 13:55:22 +0530 Subject: [PATCH 1997/3170] Add news entry --- news/5F187A50-7217-4F88-8902-548C9F534E55.trivial | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 news/5F187A50-7217-4F88-8902-548C9F534E55.trivial diff --git a/news/5F187A50-7217-4F88-8902-548C9F534E55.trivial b/news/5F187A50-7217-4F88-8902-548C9F534E55.trivial new file mode 100644 index 00000000000..e69de29bb2d From a8e20e36b2f75fcae8ec3f176ddc53e333923f29 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Tue, 2 Jun 2020 15:56:38 +0530 Subject: [PATCH 1998/3170] Rename function --- src/pip/_internal/utils/unpacking.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/utils/unpacking.py b/src/pip/_internal/utils/unpacking.py index 2c54131981f..8966052d903 100644 --- a/src/pip/_internal/utils/unpacking.py +++ b/src/pip/_internal/utils/unpacking.py @@ -95,7 +95,7 @@ def is_within_directory(directory, target): return prefix == abs_directory -def set_file_modes(path): +def set_extracted_file_to_default_mode_plus_executable(path): # type: (Union[str, Text]) -> None """ Make file present at path have execute for user/group/world @@ -149,7 +149,7 @@ def unzip_file(filename, location, flatten=True): # if mode and regular file and any execute permissions for # user/group/world? if mode and stat.S_ISREG(mode) and mode & 0o111: - set_file_modes(fn) + set_extracted_file_to_default_mode_plus_executable(fn) finally: zipfp.close() @@ -232,7 +232,7 @@ def untar_file(filename, location): tar.utime(member, path) # type: ignore # member have any execute permissions for user/group/world? if member.mode & 0o111: - set_file_modes(path) + set_extracted_file_to_default_mode_plus_executable(path) finally: tar.close() From a90fe9bd9462d6c38f96057d6bbd4b0ffbdb4f73 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 2 Jun 2020 19:07:07 +0800 Subject: [PATCH 1999/3170] Remove outdated TODO comment --- src/pip/_internal/resolution/resolvelib/candidates.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 695b42a96cc..ff200a5a582 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -59,7 +59,6 @@ def make_install_req_from_link(link, template): ) if ireq.link is None: ireq.link = link - # TODO: Handle wheel cache resolution. return ireq From a0d12df489eff0cf3a4a23955b6bd9ebce2aac54 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 2 Jun 2020 19:12:42 +0800 Subject: [PATCH 2000/3170] Copy link values from template to candidate's ireq We need to set the original link so the value is accessible in later code, when we return the ireq backing the candidate. This is needed for some parts of the post processing like PEP 610 support, which needs to record the original, remote URL, not the potentially hit cache link. --- src/pip/_internal/resolution/resolvelib/candidates.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index ff200a5a582..9f92e969c5e 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -57,8 +57,8 @@ def make_install_req_from_link(link, template): hashes=template.hash_options ), ) - if ireq.link is None: - ireq.link = link + ireq.original_link = template.original_link + ireq.link = link return ireq From fe529c140ba6b28e0002578ccb9ea1cd4cf16d89 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 2 Jun 2020 19:45:54 +0800 Subject: [PATCH 2001/3170] This should not fail anymore --- tests/functional/test_install_direct_url.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/functional/test_install_direct_url.py b/tests/functional/test_install_direct_url.py index 4afd3925e6f..ec1e927ebf8 100644 --- a/tests/functional/test_install_direct_url.py +++ b/tests/functional/test_install_direct_url.py @@ -1,7 +1,5 @@ import re -import pytest - from pip._internal.models.direct_url import DIRECT_URL_METADATA_NAME, DirectUrl from tests.lib import _create_test_package, path_to_url @@ -32,7 +30,6 @@ def test_install_vcs_editable_no_direct_url(script, with_wheel): assert not _get_created_direct_url(result, "testpkg") -@pytest.mark.fails_on_new_resolver def test_install_vcs_non_editable_direct_url(script, with_wheel): pkg_path = _create_test_package(script, name="testpkg") url = path_to_url(pkg_path) From e03f8e88689225356a30e7abfd3fef06705a6cb7 Mon Sep 17 00:00:00 2001 From: gutsytechster <prashantsharma161198@gmail.com> Date: Tue, 2 Jun 2020 17:38:55 +0530 Subject: [PATCH 2002/3170] refactor(tests): Add helper methods for path lookups This adds helper methods to test_install_force_reinstall.py and test_uninstall_user.py --- tests/functional/test_install_force_reinstall.py | 2 +- tests/functional/test_uninstall_user.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/functional/test_install_force_reinstall.py b/tests/functional/test_install_force_reinstall.py index c312ac79fba..0fbdeb276c4 100644 --- a/tests/functional/test_install_force_reinstall.py +++ b/tests/functional/test_install_force_reinstall.py @@ -33,7 +33,7 @@ def check_force_reinstall(script, specifier, expected): # site_packages_path is absolute, but files_created mapping uses # relative paths as key. fixed_key = os.path.relpath(to_fix, script.base_path) - assert fixed_key in result2.files_created, 'force-reinstall failed' + result2.did_create(fixed_key, message='force-reinstall failed') result3 = script.pip('uninstall', 'simplewheel', '-y') assert_all_changes(result, result3, [script.venv / 'build', 'cache']) diff --git a/tests/functional/test_uninstall_user.py b/tests/functional/test_uninstall_user.py index a367796fdfd..d29e088f617 100644 --- a/tests/functional/test_uninstall_user.py +++ b/tests/functional/test_uninstall_user.py @@ -62,7 +62,7 @@ def test_uninstall_editable_from_usersite(self, script, data): 'install', '--user', '-e', to_install ) egg_link = script.user_site / 'FSPkg.egg-link' - assert egg_link in result1.files_created, str(result1.stdout) + result1.did_create(egg_link) # uninstall result2 = script.pip('uninstall', '-y', 'FSPkg') From c2a27555dad7b3b06b97a3b4361bb3463659156b Mon Sep 17 00:00:00 2001 From: gutsytechster <prashantsharma161198@gmail.com> Date: Tue, 2 Jun 2020 17:40:20 +0530 Subject: [PATCH 2003/3170] docs(news): Add a news file --- news/aac65537-8fe0-43d5-8877-af191a39a663.trivial | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 news/aac65537-8fe0-43d5-8877-af191a39a663.trivial diff --git a/news/aac65537-8fe0-43d5-8877-af191a39a663.trivial b/news/aac65537-8fe0-43d5-8877-af191a39a663.trivial new file mode 100644 index 00000000000..e69de29bb2d From 5f0f2f0218e9c5cc49d7eb6273aca353c5a11681 Mon Sep 17 00:00:00 2001 From: Cristina <hi@xmunoz.com> Date: Tue, 2 Jun 2020 09:50:16 -0700 Subject: [PATCH 2004/3170] Update src/pip/_internal/models/format_control.py Co-authored-by: Pradyun Gedam <pradyunsg@gmail.com> --- src/pip/_internal/models/format_control.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/models/format_control.py b/src/pip/_internal/models/format_control.py index b7c3ceb8ed2..dfa091ef496 100644 --- a/src/pip/_internal/models/format_control.py +++ b/src/pip/_internal/models/format_control.py @@ -27,14 +27,16 @@ def __init__(self, no_binary=None, only_binary=None): def __eq__(self, other): # type: (object) -> bool - if isinstance(other, self.__class__): - if self.__slots__ == other.__slots__: - attr_getters = [operator.attrgetter(attr) - for attr in self.__slots__] - return all(getter(self) == getter(other) - for getter in attr_getters) + if not isinstance(other, self.__class__): + return NotImplemented + + if self.__slots__ != other.__slots__: + return NotImplemented - return False + return all( + getattr(self, k) == getattr(other, k) + for k in self.__slots__ + ) def __ne__(self, other): # type: (object) -> bool From 8f4c4c9eb4c0c9f100d675927abd70ce00fcd5f7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Mu=C3=B1oz?= <hi@xmunoz.com> Date: Tue, 2 Jun 2020 09:58:39 -0700 Subject: [PATCH 2005/3170] Fix whitespace --- src/pip/_internal/models/format_control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/models/format_control.py b/src/pip/_internal/models/format_control.py index dfa091ef496..f8138e81f73 100644 --- a/src/pip/_internal/models/format_control.py +++ b/src/pip/_internal/models/format_control.py @@ -29,7 +29,7 @@ def __eq__(self, other): # type: (object) -> bool if not isinstance(other, self.__class__): return NotImplemented - + if self.__slots__ != other.__slots__: return NotImplemented From 9583642d871d557872c92fcad2616cf6af702817 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Mu=C3=B1oz?= <hi@xmunoz.com> Date: Tue, 2 Jun 2020 10:05:07 -0700 Subject: [PATCH 2006/3170] Remove superfluous operator import --- src/pip/_internal/models/format_control.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/pip/_internal/models/format_control.py b/src/pip/_internal/models/format_control.py index f8138e81f73..4ba466c83f4 100644 --- a/src/pip/_internal/models/format_control.py +++ b/src/pip/_internal/models/format_control.py @@ -1,5 +1,3 @@ -import operator - from pip._vendor.packaging.utils import canonicalize_name from pip._internal.exceptions import CommandError From 92ca536279a3b23733b04cd3abac99403507e5ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Mu=C3=B1oz?= <hi@xmunoz.com> Date: Tue, 2 Jun 2020 10:45:16 -0700 Subject: [PATCH 2007/3170] Don't raise exception for false --- src/pip/_internal/models/format_control.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/models/format_control.py b/src/pip/_internal/models/format_control.py index 4ba466c83f4..fa882df7472 100644 --- a/src/pip/_internal/models/format_control.py +++ b/src/pip/_internal/models/format_control.py @@ -26,10 +26,10 @@ def __init__(self, no_binary=None, only_binary=None): def __eq__(self, other): # type: (object) -> bool if not isinstance(other, self.__class__): - return NotImplemented + return False if self.__slots__ != other.__slots__: - return NotImplemented + return False return all( getattr(self, k) == getattr(other, k) From e4f870ba25b3bc9398f402a3f3ac4b0fe5f91c91 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 3 Jun 2020 03:52:07 +0800 Subject: [PATCH 2008/3170] Raise proper exceptions on metadata mismatch --- src/pip/_internal/exceptions.py | 27 +++++++++++++++-- .../resolution/resolvelib/candidates.py | 29 +++++++------------ 2 files changed, 36 insertions(+), 20 deletions(-) diff --git a/src/pip/_internal/exceptions.py b/src/pip/_internal/exceptions.py index ea02e868a8e..8407cec7e96 100644 --- a/src/pip/_internal/exceptions.py +++ b/src/pip/_internal/exceptions.py @@ -9,11 +9,14 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Optional, List, Dict + from typing import Any, Optional, List, Dict + from pip._vendor.pkg_resources import Distribution + from pip._vendor.six import PY3 from pip._vendor.six.moves import configparser + from pip._internal.req.req_install import InstallRequirement - from pip._vendor.six import PY3 + if PY3: from hashlib import _Hash else: @@ -104,6 +107,26 @@ class UnsupportedWheel(InstallationError): """Unsupported wheel.""" +class MetadataInconsistent(InstallationError): + """Built metadata contains inconsistent information. + + This is raised when the metadata contains values (e.g. name and version) + that do not match the information previously obtained from sdist filename + or user-supplied ``#egg=`` value. + """ + def __init__(self, ireq, field, built): + # type: (InstallRequirement, str, Any) -> None + self.ireq = ireq + self.field = field + self.built = built + + def __str__(self): + # type: () -> str + return "Requested {} has different {} in metadata: {!r}".format( + self.ireq, self.field, self.built, + ) + + class HashErrors(InstallationError): """Multiple HashError instances rolled into one for reporting""" diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 695b42a96cc..dd4e162a705 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -5,6 +5,7 @@ from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.packaging.version import Version +from pip._internal.exceptions import MetadataInconsistent from pip._internal.req.constructors import ( install_req_from_editable, install_req_from_line, @@ -175,26 +176,18 @@ def _prepare(self): self._dist = abstract_dist.get_pkg_resources_distribution() assert self._dist is not None, "Distribution already installed" - # TODO: Abort cleanly here, as the resolution has been - # based on the wrong name/version until now, and - # so is wrong. # TODO: (Longer term) Rather than abort, reject this candidate # and backtrack. This would need resolvelib support. - # These should be "proper" errors, not just asserts, as they - # can result from user errors like a requirement "foo @ URL" - # when the project at URL has a name of "bar" in its metadata. - assert ( - self._name is None or - self._name == canonicalize_name(self._dist.project_name) - ), "Name mismatch: {!r} vs {!r}".format( - self._name, canonicalize_name(self._dist.project_name), - ) - assert ( - self._version is None or - self._version == self._dist.parsed_version - ), "Version mismatch: {!r} vs {!r}".format( - self._version, self._dist.parsed_version, - ) + name = canonicalize_name(self._dist.project_name) + if self._name is not None and self._name != name: + raise MetadataInconsistent( + self._ireq, "name", self._dist.project_name, + ) + version = self._dist.parsed_version + if self._version is not None and self._version != version: + raise MetadataInconsistent( + self._ireq, "version", self._dist.version, + ) @property def dist(self): From 8ef2191a097b015139fba6bba9c0992da503d62e Mon Sep 17 00:00:00 2001 From: Vikram - Google <vikram-google@Chief.local> Date: Tue, 2 Jun 2020 16:45:48 -0700 Subject: [PATCH 2009/3170] Fixed Error Swallowing outlined in issue 5354 --- src/pip/_vendor/__init__.py | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/pip/_vendor/__init__.py b/src/pip/_vendor/__init__.py index c3db83ff6aa..581db54c8d8 100644 --- a/src/pip/_vendor/__init__.py +++ b/src/pip/_vendor/__init__.py @@ -32,15 +32,11 @@ def vendored(modulename): try: __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 pip._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* - # tries to import one of these modules to use it, which actually - # gives us a better error message than we would have otherwise - # gotten. - pass + # This error used to be silenced in earlier variants of this file, to instead + # raise the error when pip actually tries to use the missing module. + # Based on inputs in #5354, this was changed to explicitly raise the error. + # Re-raising the exception without modifying it is an intentional choice. + raise else: sys.modules[vendored_name] = sys.modules[modulename] base, head = vendored_name.rsplit(".", 1) From 2244868237a893ee656f8955a9d1a2cf9ccf22d6 Mon Sep 17 00:00:00 2001 From: Vikram - Google <vikram-google@Chief.local> Date: Tue, 2 Jun 2020 16:49:28 -0700 Subject: [PATCH 2010/3170] Added .trivial file in news dir --- news/error-swallow.trivial | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 news/error-swallow.trivial diff --git a/news/error-swallow.trivial b/news/error-swallow.trivial new file mode 100644 index 00000000000..e69de29bb2d From 85593ab326819e65d99b53c0b39e8f160fe22aa8 Mon Sep 17 00:00:00 2001 From: Surbhi Sharma <ssurbhi560@gmail.com> Date: Wed, 3 Jun 2020 12:43:49 +0530 Subject: [PATCH 2011/3170] use helper methods in test_install_user and test_new_resolver_user --- tests/functional/test_install_user.py | 16 ++++++++-------- tests/functional/test_new_resolver_user.py | 14 +++++++------- 2 files changed, 15 insertions(+), 15 deletions(-) diff --git a/tests/functional/test_install_user.py b/tests/functional/test_install_user.py index c885bf4b6ef..7ac43301afe 100644 --- a/tests/functional/test_install_user.py +++ b/tests/functional/test_install_user.py @@ -73,12 +73,12 @@ def test_install_from_current_directory_into_usersite( ) fspkg_folder = script.user_site / 'fspkg' - assert fspkg_folder in result.files_created, result.stdout + result.did_create(fspkg_folder) dist_info_folder = ( script.user_site / 'FSPkg-0.1.dev0.dist-info' ) - assert dist_info_folder in result.files_created + result.did_create(dist_info_folder) def test_install_user_venv_nositepkgs_fails(self, virtualenv, script, data): @@ -121,7 +121,7 @@ def test_install_user_conflict_in_usersite(self, script): script.base_path / script.user_site / 'initools' / 'configparser.py' ) - assert egg_info_folder in result2.files_created, str(result2) + result2.did_create(egg_info_folder) assert not isfile(initools_v3_file), initools_v3_file @pytest.mark.network @@ -145,8 +145,8 @@ def test_install_user_conflict_in_globalsite(self, virtualenv, script): 'INITools-0.1-py{pyversion}.egg-info'.format(**globals()) ) initools_folder = script.user_site / 'initools' - assert egg_info_folder in result2.files_created, str(result2) - assert initools_folder in result2.files_created, str(result2) + result2.did_create(egg_info_folder) + result2.did_create(initools_folder) # site still has 0.2 (can't look in result1; have to check) egg_info_folder = ( @@ -177,8 +177,8 @@ def test_upgrade_user_conflict_in_globalsite(self, virtualenv, script): 'INITools-0.3.1-py{pyversion}.egg-info'.format(**globals()) ) initools_folder = script.user_site / 'initools' - assert egg_info_folder in result2.files_created, str(result2) - assert initools_folder in result2.files_created, str(result2) + result2.did_create(egg_info_folder) + result2.did_create(initools_folder) # site still has 0.2 (can't look in result1; have to check) egg_info_folder = ( @@ -216,7 +216,7 @@ def test_install_user_conflict_in_globalsite_and_usersite( script.base_path / script.user_site / 'initools' / 'configparser.py' ) - assert egg_info_folder in result3.files_created, str(result3) + result3.did_create(egg_info_folder) assert not isfile(initools_v3_file), initools_v3_file # site still has 0.2 (can't just look in result1; have to check) diff --git a/tests/functional/test_new_resolver_user.py b/tests/functional/test_new_resolver_user.py index f5ff8df2436..3576b8a67dd 100644 --- a/tests/functional/test_new_resolver_user.py +++ b/tests/functional/test_new_resolver_user.py @@ -16,7 +16,7 @@ def test_new_resolver_install_user(script): "--user", "base", ) - assert script.user_site / "base" in result.files_created, str(result) + result.did_create(script.user_site / "base") @pytest.mark.incompatible_with_test_venv @@ -41,7 +41,7 @@ def test_new_resolver_install_user_satisfied_by_global_site(script): "base==1.0.0", ) - assert script.user_site / "base" not in result.files_created, str(result) + result.did_not_create(script.user_site / "base") @pytest.mark.incompatible_with_test_venv @@ -72,8 +72,8 @@ def test_new_resolver_install_user_conflict_in_user_site(script): base_1_dist_info = script.user_site / "base-1.0.0.dist-info" base_2_dist_info = script.user_site / "base-2.0.0.dist-info" - assert base_1_dist_info in result.files_created, str(result) - assert base_2_dist_info not in result.files_created, str(result) + result.did_create(base_1_dist_info) + result.did_not_create(base_2_dist_info) @pytest.mark.incompatible_with_test_venv @@ -142,7 +142,7 @@ def test_new_resolver_install_user_reinstall_global_site(script): "base==1.0.0", ) - assert script.user_site / "base" in result.files_created, str(result) + result.did_create(script.user_site / "base") site_packages_content = set(os.listdir(script.site_packages_path)) assert "base" in site_packages_content @@ -174,7 +174,7 @@ def test_new_resolver_install_user_conflict_in_global_site(script): ) base_2_dist_info = script.user_site / "base-2.0.0.dist-info" - assert base_2_dist_info in result.files_created, str(result) + result.did_create(base_2_dist_info) site_packages_content = set(os.listdir(script.site_packages_path)) assert "base-1.0.0.dist-info" in site_packages_content @@ -216,7 +216,7 @@ def test_new_resolver_install_user_conflict_in_global_and_user_sites(script): base_1_dist_info = script.user_site / "base-1.0.0.dist-info" base_2_dist_info = script.user_site / "base-2.0.0.dist-info" - assert base_1_dist_info in result.files_created, str(result) + result.did_create(base_1_dist_info) assert base_2_dist_info in result.files_deleted, str(result) site_packages_content = set(os.listdir(script.site_packages_path)) From 937bea102703495eebc674d854963daac9463271 Mon Sep 17 00:00:00 2001 From: Surbhi Sharma <ssurbhi560@gmail.com> Date: Wed, 3 Jun 2020 12:46:29 +0530 Subject: [PATCH 2012/3170] Add a news file --- news/1f046105-b105-495c-a882-cc7263871c23.trivial | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 news/1f046105-b105-495c-a882-cc7263871c23.trivial diff --git a/news/1f046105-b105-495c-a882-cc7263871c23.trivial b/news/1f046105-b105-495c-a882-cc7263871c23.trivial new file mode 100644 index 00000000000..e69de29bb2d From 50528a3c0e5602b382f2ab738fda9ac86f192d33 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Wed, 3 Jun 2020 14:09:42 +0100 Subject: [PATCH 2013/3170] Test new resolver different behaviour for test_install_editable_with_wrong_egg_name --- tests/functional/test_install.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 01fdc8e0d0e..809e50434de 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1449,8 +1449,7 @@ def test_install_no_binary_disables_cached_wheels(script, data, with_wheel): assert "Running setup.py install for upper" in str(res), str(res) -@pytest.mark.fails_on_new_resolver -def test_install_editable_with_wrong_egg_name(script): +def test_install_editable_with_wrong_egg_name(script, use_new_resolver): script.scratch_path.joinpath("pkga").mkdir() pkga_path = script.scratch_path / 'pkga' pkga_path.joinpath("setup.py").write_text(textwrap.dedent(""" @@ -1461,11 +1460,15 @@ def test_install_editable_with_wrong_egg_name(script): result = script.pip( 'install', '--editable', 'file://{pkga_path}#egg=pkgb'.format(**locals()), + expect_error=use_new_resolver, ) assert ("Generating metadata for package pkgb produced metadata " "for project name pkga. Fix your #egg=pkgb " "fragments.") in result.stderr - assert "Successfully installed pkga" in str(result), str(result) + if use_new_resolver: + assert "has different name in metadata" in result.stderr, str(result) + else: + assert "Successfully installed pkga" in str(result), str(result) def test_install_tar_xz(script, data): From 4ca684f3b83cad2d63b12c879684dd8bce7f4a10 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 21 May 2020 17:18:21 +0800 Subject: [PATCH 2014/3170] Fix for source directory reuse --- src/pip/_internal/req/req_install.py | 11 ++++++----- tests/functional/test_new_resolver.py | 1 - 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 3b28209b1bd..7bd7a05521c 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -7,6 +7,7 @@ import os import shutil import sys +import uuid import zipfile from pip._vendor import pkg_resources, six @@ -354,16 +355,16 @@ def ensure_build_location(self, build_dir, autodelete): ) return self._temp_build_dir.path - if self.editable: - name = self.name.lower() - else: - name = self.name + dir_name = "{}_{}".format( + canonicalize_name(self.name), + uuid.uuid4().hex, + ) # FIXME: Is there a better place to create the build_dir? (hg and bzr # need this) if not os.path.exists(build_dir): logger.debug('Creating directory %s', build_dir) os.makedirs(build_dir) - actual_build_dir = os.path.join(build_dir, name) + actual_build_dir = os.path.join(build_dir, dir_name) # `None` indicates that we respect the globally-configured deletion # settings, which is what we actually want when auto-deleting. delete_arg = None if autodelete else False diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index f235feeb8e1..15ed6224393 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -863,7 +863,6 @@ def test_new_resolver_extra_merge_in_package( assert_installed(script, pkg="1.0.0", dep="1.0.0", depdev="1.0.0") -@pytest.mark.xfail(reason="pre-existing build directory") def test_new_resolver_build_directory_error_zazo_19(script): """https://github.com/pradyunsg/zazo/issues/19#issuecomment-631615674 From 030e2b8c0ef234f337d69100b58af776b0126113 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Cristina=20Mu=C3=B1oz?= <hi@xmunoz.com> Date: Wed, 3 Jun 2020 09:52:56 -0700 Subject: [PATCH 2015/3170] Return not implmented if class differs --- src/pip/_internal/models/format_control.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/models/format_control.py b/src/pip/_internal/models/format_control.py index fa882df7472..c6275e721b3 100644 --- a/src/pip/_internal/models/format_control.py +++ b/src/pip/_internal/models/format_control.py @@ -26,7 +26,7 @@ def __init__(self, no_binary=None, only_binary=None): def __eq__(self, other): # type: (object) -> bool if not isinstance(other, self.__class__): - return False + return NotImplemented if self.__slots__ != other.__slots__: return False From dd3672fe3412baf95e45b579ce06267412796552 Mon Sep 17 00:00:00 2001 From: Greg Ward <greg@gerg.ca> Date: Tue, 26 May 2020 10:32:05 -0400 Subject: [PATCH 2016/3170] Fix grammar in Python 2 warning message --- news/17b9b9f6-12ba-424f-b197-10338408d36d.trivial | 0 src/pip/_internal/cli/base_command.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 news/17b9b9f6-12ba-424f-b197-10338408d36d.trivial diff --git a/news/17b9b9f6-12ba-424f-b197-10338408d36d.trivial b/news/17b9b9f6-12ba-424f-b197-10338408d36d.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index f51682b60e9..c523c4e56b5 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -148,7 +148,7 @@ def _main(self, args): ): message = ( "pip 21.0 will drop support for Python 2.7 in January 2021. " - "More details about Python 2 support in pip, can be found at " + "More details about Python 2 support in pip can be found at " "https://pip.pypa.io/en/latest/development/release-process/#python-2-support" # noqa ) if platform.python_implementation() == "CPython": From 09a7f271c7ed5bafe5acff5c65fb1531f43f8ae3 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 4 Jun 2020 00:48:14 +0800 Subject: [PATCH 2017/3170] Only attach UUID to build dir for spec candidates These are the only cases where backtracking can happen. This approach also accounts for VCS requirements relying on the same ensure function to do cloning :/ --- src/pip/_internal/operations/prepare.py | 7 ++++- src/pip/_internal/req/req_install.py | 28 +++++++++++++------ .../resolution/resolvelib/candidates.py | 4 ++- tests/unit/test_req_install.py | 2 +- 4 files changed, 29 insertions(+), 12 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 1fcbb775ece..d8a4bde5ca0 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -379,6 +379,7 @@ def _download_should_save(self): def prepare_linked_requirement( self, req, # type: InstallRequirement + parallel_builds=False, # type: bool ): # type: (...) -> AbstractDistribution """Prepare a requirement that would be obtained from req.link @@ -415,7 +416,11 @@ def prepare_linked_requirement( with indent_log(): # Since source_dir is only set for editable requirements. assert req.source_dir is None - req.ensure_has_source_dir(self.build_dir, autodelete_unpacked) + req.ensure_has_source_dir( + self.build_dir, + autodelete=autodelete_unpacked, + parallel_builds=parallel_builds, + ) # If a checkout exists, it's unwise to keep going. version # inconsistencies are logged later, but do not fail the # installation. diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 7bd7a05521c..fc6f4b3d122 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -340,8 +340,8 @@ def from_path(self): s += '->' + comes_from return s - def ensure_build_location(self, build_dir, autodelete): - # type: (str, bool) -> str + def ensure_build_location(self, build_dir, autodelete, parallel_builds): + # type: (str, bool, bool) -> str assert build_dir is not None if self._temp_build_dir is not None: assert self._temp_build_dir.path @@ -355,10 +355,13 @@ def ensure_build_location(self, build_dir, autodelete): ) return self._temp_build_dir.path - dir_name = "{}_{}".format( - canonicalize_name(self.name), - uuid.uuid4().hex, - ) + + # When parallel builds are enabled, add a UUID to the build directory + # name so multiple builds do not interfere with each other. + dir_name = canonicalize_name(self.name) + if parallel_builds: + dir_name = "{}_{}".format(dir_name, uuid.uuid4().hex) + # FIXME: Is there a better place to create the build_dir? (hg and bzr # need this) if not os.path.exists(build_dir): @@ -589,8 +592,13 @@ def assert_source_matches_version(self): ) # For both source distributions and editables - def ensure_has_source_dir(self, parent_dir, autodelete=False): - # type: (str, bool) -> None + def ensure_has_source_dir( + self, + parent_dir, + autodelete=False, + parallel_builds=False, + ): + # type: (str, bool, bool) -> None """Ensure that a source_dir is set. This will create a temporary build dir if the name of the requirement @@ -602,7 +610,9 @@ def ensure_has_source_dir(self, parent_dir, autodelete=False): """ if self.source_dir is None: self.source_dir = self.ensure_build_location( - parent_dir, autodelete + parent_dir, + autodelete=autodelete, + parallel_builds=parallel_builds, ) # For editable installations diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 695b42a96cc..bf407b715a4 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -263,7 +263,9 @@ def __init__( def _prepare_abstract_distribution(self): # type: () -> AbstractDistribution - return self._factory.preparer.prepare_linked_requirement(self._ireq) + return self._factory.preparer.prepare_linked_requirement( + self._ireq, parallel_builds=True, + ) class EditableCandidate(_InstallRequirementBackedCandidate): diff --git a/tests/unit/test_req_install.py b/tests/unit/test_req_install.py index c3482d5360a..d0d80035299 100644 --- a/tests/unit/test_req_install.py +++ b/tests/unit/test_req_install.py @@ -21,7 +21,7 @@ def test_tmp_build_directory(self): requirement = InstallRequirement(None, None) tmp_dir = tempfile.mkdtemp('-build', 'pip-') tmp_build_dir = requirement.ensure_build_location( - tmp_dir, autodelete=False + tmp_dir, autodelete=False, parallel_builds=False, ) assert ( os.path.dirname(tmp_build_dir) == From daf454bb3ba6752b2a102ae9c33ca8b60e7f5746 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 4 Jun 2020 23:05:54 +0800 Subject: [PATCH 2018/3170] Mark build dir tests as passing for new resolver The new resolver uses UUID to allow parallel build directories, so this error will no longer occur. --- tests/functional/test_install_cleanup.py | 16 +++++++++----- tests/functional/test_wheel.py | 27 ++++++++++++++++++------ 2 files changed, 31 insertions(+), 12 deletions(-) diff --git a/tests/functional/test_install_cleanup.py b/tests/functional/test_install_cleanup.py index ece2161cfb2..e5d8c4602e5 100644 --- a/tests/functional/test_install_cleanup.py +++ b/tests/functional/test_install_cleanup.py @@ -20,7 +20,11 @@ def test_no_clean_option_blocks_cleaning_after_install(script, data): @pytest.mark.network -def test_cleanup_prevented_upon_build_dir_exception(script, data): +def test_cleanup_prevented_upon_build_dir_exception( + script, + data, + use_new_resolver, +): """ Test no cleanup occurs after a PreviousBuildDirError """ @@ -31,12 +35,14 @@ def test_cleanup_prevented_upon_build_dir_exception(script, data): result = script.pip( 'install', '-f', data.find_links, '--no-index', 'simple', '--build', build, - expect_error=True, expect_temp=True, + expect_error=(not use_new_resolver), + expect_temp=(not use_new_resolver), ) - assert result.returncode == PREVIOUS_BUILD_DIR_ERROR, str(result) - assert "pip can't proceed" in result.stderr, str(result) - assert exists(build_simple), str(result) + if not use_new_resolver: + assert result.returncode == PREVIOUS_BUILD_DIR_ERROR, str(result) + assert "pip can't proceed" in result.stderr, str(result) + assert exists(build_simple), str(result) @pytest.mark.network diff --git a/tests/functional/test_wheel.py b/tests/functional/test_wheel.py index 6bf0486819a..63447a05628 100644 --- a/tests/functional/test_wheel.py +++ b/tests/functional/test_wheel.py @@ -191,7 +191,11 @@ def test_pip_wheel_fail(script, data): assert result.returncode != 0 -def test_no_clean_option_blocks_cleaning_after_wheel(script, data): +def test_no_clean_option_blocks_cleaning_after_wheel( + script, + data, + use_new_resolver, +): """ Test --no-clean option blocks cleaning after wheel build """ @@ -202,9 +206,11 @@ def test_no_clean_option_blocks_cleaning_after_wheel(script, data): 'simple', expect_temp=True, ) - build = build / 'simple' - assert exists(build), \ - "build/simple should still exist {result}".format(**locals()) + + if not use_new_resolver: + build = build / 'simple' + message = "build/simple should still exist {}".format(result) + assert exists(build), message def test_pip_wheel_source_deps(script, data): @@ -224,7 +230,11 @@ def test_pip_wheel_source_deps(script, data): assert "Successfully built source" in result.stdout, result.stdout -def test_pip_wheel_fail_cause_of_previous_build_dir(script, data): +def test_pip_wheel_fail_cause_of_previous_build_dir( + script, + data, + use_new_resolver, +): """ Test when 'pip wheel' tries to install a package that has a previous build directory @@ -240,11 +250,14 @@ def test_pip_wheel_fail_cause_of_previous_build_dir(script, data): 'wheel', '--no-index', '--find-links={data.find_links}'.format(**locals()), '--build', script.venv_path / 'build', - 'simple==3.0', expect_error=True, expect_temp=True, + 'simple==3.0', + expect_error=(not use_new_resolver), + expect_temp=(not use_new_resolver), ) # Then I see that the error code is the right one - assert result.returncode == PREVIOUS_BUILD_DIR_ERROR, result + if not use_new_resolver: + assert result.returncode == PREVIOUS_BUILD_DIR_ERROR, result def test_wheel_package_with_latin1_setup(script, data): From 9a52dcd62b3a4dd22d9108b178b35aee20bfcd56 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Fri, 5 Jun 2020 02:40:26 +0530 Subject: [PATCH 2019/3170] Add flake8-bugbear to pre-commit --- .pre-commit-config.yaml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0a7847c132c..d3a6244f39d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -20,6 +20,9 @@ repos: rev: 3.8.1 hooks: - id: flake8 + additional_dependencies: [ + 'flake8-bugbear==20.1.4' + ] exclude: tests/data - repo: https://github.com/timothycrosley/isort From 6716a2bec56d661b944e7cc68d3db32a71d7249a Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Fri, 5 Jun 2020 02:40:49 +0530 Subject: [PATCH 2020/3170] Add flake8-bugbear rules to setup.cfg --- setup.cfg | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index f0bd7a8d9bd..d1928ed56e5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,9 +23,17 @@ exclude = .scratch, _vendor, data -select = E,W,F -ignore = W504 - + # TODO: Remove when fixing flake8 warnings + src/pip/* +# B = bugbear +# E = pycodestyle errors +# F = flake8 pyflakes +# W = pycodestyle warnings +# B9 = bugbear opinions +select = B, E, W, F, B9 +per-file-ignores = + # B011: Do not call assert False since python -O removes these calls + tests/*: B011 [mypy] follow_imports = silent ignore_missing_imports = True From db11f83e2ad99a4647eab52c63fa2f232e1dc741 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Fri, 5 Jun 2020 02:41:08 +0530 Subject: [PATCH 2021/3170] Fix tests module with flake8-bugbear --- tests/functional/test_install.py | 2 +- tests/functional/test_pep517.py | 3 ++- tests/functional/test_vcs_git.py | 2 +- tests/lib/__init__.py | 6 ++++-- tests/unit/resolution_resolvelib/test_requirement.py | 6 +++--- tests/unit/test_req_file.py | 2 +- tests/unit/test_utils_wheel.py | 2 +- tests/yaml/linter.py | 2 +- 8 files changed, 14 insertions(+), 11 deletions(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 809e50434de..0ffcce002b0 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1358,7 +1358,7 @@ def test_install_builds_wheels(script, data, with_wheel): # Must have installed it all assert expected in str(res), str(res) wheels = [] - for top, dirs, files in os.walk(wheels_cache): + for _, _, files in os.walk(wheels_cache): wheels.extend(files) # and built wheels for upper and wheelbroken assert "Building wheel for upper" in str(res), str(res) diff --git a/tests/functional/test_pep517.py b/tests/functional/test_pep517.py index 1647edf028c..bcad4793672 100644 --- a/tests/functional/test_pep517.py +++ b/tests/functional/test_pep517.py @@ -6,7 +6,8 @@ from tests.lib import make_test_finder, path_to_url, windows_workaround_7667 -def make_project(tmpdir, requires=[], backend=None, backend_path=None): +def make_project(tmpdir, requires=None, backend=None, backend_path=None): + requires = requires or [] project_dir = tmpdir / 'project' project_dir.mkdir() buildsys = {'requires': requires} diff --git a/tests/functional/test_vcs_git.py b/tests/functional/test_vcs_git.py index cee0a3cfeeb..37c35c4b52a 100644 --- a/tests/functional/test_vcs_git.py +++ b/tests/functional/test_vcs_git.py @@ -41,7 +41,7 @@ def do_commit(script, dest): def add_commits(script, dest, count): """Return a list of the commit hashes from oldest to newest.""" shas = [] - for index in range(count): + for _ in range(count): sha = do_commit(script, dest) shas.append(sha) diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index 6f7a43dadcc..a8ea8676171 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -264,9 +264,11 @@ def __str__(self): def __str__(self): return str(self._impl) - def assert_installed(self, pkg_name, editable=True, with_files=[], - without_files=[], without_egg_link=False, + def assert_installed(self, pkg_name, editable=True, with_files=None, + without_files=None, without_egg_link=False, use_user_site=False, sub_dir=False): + with_files = with_files or [] + without_files = without_files or [] e = self.test_env if editable: diff --git a/tests/unit/resolution_resolvelib/test_requirement.py b/tests/unit/resolution_resolvelib/test_requirement.py index 07cd0c0f061..21de3df4a4f 100644 --- a/tests/unit/resolution_resolvelib/test_requirement.py +++ b/tests/unit/resolution_resolvelib/test_requirement.py @@ -50,14 +50,14 @@ def data_url(name): def test_new_resolver_requirement_has_name(test_cases, factory): """All requirements should have a name""" - for spec, name, matches in test_cases: + for spec, name, _ in test_cases: req = factory.make_requirement_from_spec(spec, comes_from=None) assert req.name == name def test_new_resolver_correct_number_of_matches(test_cases, factory): """Requirements should return the correct number of candidates""" - for spec, name, match_count in test_cases: + for spec, _, match_count in test_cases: req = factory.make_requirement_from_spec(spec, comes_from=None) matches = factory.find_candidates([req], SpecifierSet()) assert len(list(matches)) == match_count @@ -66,7 +66,7 @@ def test_new_resolver_correct_number_of_matches(test_cases, factory): def test_new_resolver_candidates_match_requirement(test_cases, factory): """Candidates returned from find_candidates should satisfy the requirement """ - for spec, name, matches in test_cases: + for spec, _, _ in test_cases: req = factory.make_requirement_from_spec(spec, comes_from=None) for c in factory.find_candidates([req], SpecifierSet()): assert isinstance(c, Candidate) diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py index 4ef4f7217e0..10df385dfde 100644 --- a/tests/unit/test_req_file.py +++ b/tests/unit/test_req_file.py @@ -561,7 +561,7 @@ def test_remote_reqs_parse(self): """ # this requirements file just contains a comment previously this has # failed in py3: https://github.com/pypa/pip/issues/760 - for req in parse_reqfile( + for _ in parse_reqfile( 'https://raw.githubusercontent.com/pypa/' 'pip-test-package/master/' 'tests/req_just_comment.txt', session=PipSession()): diff --git a/tests/unit/test_utils_wheel.py b/tests/unit/test_utils_wheel.py index 20d7ea20d77..cf8bd6dc3ed 100644 --- a/tests/unit/test_utils_wheel.py +++ b/tests/unit/test_utils_wheel.py @@ -21,7 +21,7 @@ def make_zip(path): # type: (Path) -> ZipFile buf = BytesIO() with ZipFile(buf, "w", allowZip64=True) as z: - for dirpath, dirnames, filenames in os.walk(path): + for dirpath, _, filenames in os.walk(path): for filename in filenames: file_path = os.path.join(path, dirpath, filename) # Zip files must always have / as path separator diff --git a/tests/yaml/linter.py b/tests/yaml/linter.py index 8420db1c294..dd8b60a3d81 100644 --- a/tests/yaml/linter.py +++ b/tests/yaml/linter.py @@ -68,7 +68,7 @@ def lint_yml(yml_file, verbose=False): check_dict(data, required=['cases'], optional=['base']) base = data.get("base", {}) cases = data["cases"] - for i, case_template in enumerate(cases): + for _, case_template in enumerate(cases): case = base.copy() case.update(case_template) lint_case(case, verbose) From b0f67fd8a49af34b53681489993c3dfb4cb64ccb Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Fri, 5 Jun 2020 02:41:45 +0530 Subject: [PATCH 2022/3170] Add news entry --- news/ED9F64FF-DD77-4276-B7CE-2B2EFF935563.trivial | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 news/ED9F64FF-DD77-4276-B7CE-2B2EFF935563.trivial diff --git a/news/ED9F64FF-DD77-4276-B7CE-2B2EFF935563.trivial b/news/ED9F64FF-DD77-4276-B7CE-2B2EFF935563.trivial new file mode 100644 index 00000000000..e69de29bb2d From 53ce9b66ac8ad629b0cfbe77300136f093f4faae Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 5 Jun 2020 00:09:14 +0800 Subject: [PATCH 2023/3170] Add failing test for bug found --- tests/functional/test_new_resolver.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index f235feeb8e1..6d0b7dc6ba9 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -901,3 +901,25 @@ def test_new_resolver_build_directory_error_zazo_19(script): "pkg-a", "pkg-b", ) assert_installed(script, pkg_a="3.0.0", pkg_b="1.0.0") + + +def test_new_resolver_upgrade_same_version(script): + create_basic_wheel_for_package(script, "pkg", "2") + create_basic_wheel_for_package(script, "pkg", "1") + + script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "pkg", + ) + assert_installed(script, pkg="2") + + script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "--upgrade", + "pkg", + ) + assert_installed(script, pkg="2") From e647b125404782e75c608ddbcf314cd5957a5dc9 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 5 Jun 2020 00:46:39 +0800 Subject: [PATCH 2024/3170] Yield installed candidate at the correct position --- .../resolution/resolvelib/factory.py | 29 +++++++++++-------- 1 file changed, 17 insertions(+), 12 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index b860e4415e2..9c5fa4f2380 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -171,20 +171,20 @@ def _iter_found_candidates( # don't all have to do the same thing later. candidates = collections.OrderedDict() # type: VersionCandidates - # Yield the installed version, if it matches, unless the user + # Get the installed version, if it matches, unless the user # specified `--force-reinstall`, when we want the version from # the index instead. installed_version = None + installed_candidate = None if not self._force_reinstall and name in self._installed_dists: installed_dist = self._installed_dists[name] installed_version = installed_dist.parsed_version if specifier.contains(installed_version, prereleases=True): - candidate = self._make_candidate_from_dist( + installed_candidate = self._make_candidate_from_dist( dist=installed_dist, extras=extras, template=template, ) - candidates[installed_version] = candidate found = self._finder.find_best_candidate( project_name=name, @@ -192,17 +192,22 @@ def _iter_found_candidates( hashes=hashes, ) for ican in found.iter_applicable(): - if ican.version == installed_version: - continue - candidate = self._make_candidate_from_link( - link=ican.link, - extras=extras, - template=template, - name=name, - version=ican.version, - ) + if ican.version == installed_version and installed_candidate: + candidate = installed_candidate + else: + candidate = self._make_candidate_from_link( + link=ican.link, + extras=extras, + template=template, + name=name, + version=ican.version, + ) candidates[ican.version] = candidate + # Yield the installed version even if it is not found on the index. + if installed_version and installed_candidate: + candidates[installed_version] = installed_candidate + return six.itervalues(candidates) def find_candidates(self, requirements, constraint): From 208282af980ea650ddfdc087ffd53ba81a4c585b Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sat, 16 May 2020 13:27:37 +0530 Subject: [PATCH 2025/3170] Parametrize test_install_package_to_usersite_with_target_must_fail --- tests/functional/test_install.py | 13 ++++--------- 1 file changed, 4 insertions(+), 9 deletions(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 809e50434de..e6ea5d32411 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -885,21 +885,16 @@ def test_install_package_with_target(script): assert singlemodule_py in result.files_updated, str(result) -def test_install_package_to_usersite_with_target_must_fail(script): +@pytest.mark.parametrize("target_option", ['--target', '-t']) +def test_install_package_to_usersite_with_target_must_fail(script, + target_option): """ Test that installing package to usersite with target must raise error """ target_dir = script.scratch_path / 'target' result = script.pip_install_local( - '--user', '-t', target_dir, "simple==1.0", expect_error=True - ) - assert "Can not combine '--user' and '--target'" in result.stderr, ( - str(result) - ) - - result = script.pip_install_local( - '--user', '--target', target_dir, "simple==1.0", expect_error=True + '--user', target_option, target_dir, "simple==1.0", expect_error=True ) assert "Can not combine '--user' and '--target'" in result.stderr, ( str(result) From d2730ba2db58c6dbf9fc19fb5a68f1dfaffd848d Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Fri, 5 Jun 2020 15:08:40 +0530 Subject: [PATCH 2026/3170] Add news entry --- news/9B4F6DCE-FE1E-4428-BB4A-40D7C613AA97.trivial | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 news/9B4F6DCE-FE1E-4428-BB4A-40D7C613AA97.trivial diff --git a/news/9B4F6DCE-FE1E-4428-BB4A-40D7C613AA97.trivial b/news/9B4F6DCE-FE1E-4428-BB4A-40D7C613AA97.trivial new file mode 100644 index 00000000000..e69de29bb2d From ae2f9c645ad48cf4e54a7512b24de94bbb7d7af3 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Fri, 5 Jun 2020 22:11:15 +0530 Subject: [PATCH 2027/3170] Remove unused cached_property --- src/pip/_internal/utils/misc.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 658a30b86ac..c7bb972ffb7 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -603,26 +603,6 @@ def captured_stderr(): return captured_output('stderr') -class cached_property(object): - """A property that is only computed once per instance and then replaces - itself with an ordinary attribute. Deleting the attribute resets the - property. - - Source: https://github.com/bottlepy/bottle/blob/0.11.5/bottle.py#L175 - """ - - def __init__(self, func): - self.__doc__ = getattr(func, '__doc__') - self.func = func - - def __get__(self, obj, cls): - if obj is None: - # We're being accessed from the class itself, not from an object - return self - value = obj.__dict__[self.func.__name__] = self.func(obj) - return value - - def get_installed_version(dist_name, working_set=None): """Get the installed version of dist_name avoiding pkg_resources cache""" # Create a requirement that we'll look for inside of setuptools. From cc2dc7ad6f02188064546af841442a14976f5578 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Fri, 5 Jun 2020 22:12:02 +0530 Subject: [PATCH 2028/3170] Add news entry --- news/D5060278-216E-4884-BB1A-A6645EC0B4D2.trivial | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 news/D5060278-216E-4884-BB1A-A6645EC0B4D2.trivial diff --git a/news/D5060278-216E-4884-BB1A-A6645EC0B4D2.trivial b/news/D5060278-216E-4884-BB1A-A6645EC0B4D2.trivial new file mode 100644 index 00000000000..e69de29bb2d From 06654ef3292f07c57d51f37b83f0ea7aa125be94 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Fri, 5 Jun 2020 03:22:53 +0530 Subject: [PATCH 2029/3170] Remove explicit select and ignore src/pip for flake8-bugbear for now --- setup.cfg | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/setup.cfg b/setup.cfg index d1928ed56e5..8c94472ab6d 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,17 +23,11 @@ exclude = .scratch, _vendor, data - # TODO: Remove when fixing flake8 warnings - src/pip/* -# B = bugbear -# E = pycodestyle errors -# F = flake8 pyflakes -# W = pycodestyle warnings -# B9 = bugbear opinions -select = B, E, W, F, B9 per-file-ignores = # B011: Do not call assert False since python -O removes these calls tests/*: B011 + # TODO: Remove this when fixing flake8-bugbear warnings in source + src/pip/*: B007,B008,B009,B014,B305 [mypy] follow_imports = silent ignore_missing_imports = True From 0dbd3938af339858436fd2b44736a2d315f4665b Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sat, 6 Jun 2020 02:18:50 +0530 Subject: [PATCH 2030/3170] Add B014 ignore --- setup.cfg | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 8c94472ab6d..b8ee43f5a46 100644 --- a/setup.cfg +++ b/setup.cfg @@ -26,8 +26,13 @@ exclude = per-file-ignores = # B011: Do not call assert False since python -O removes these calls tests/*: B011 - # TODO: Remove this when fixing flake8-bugbear warnings in source - src/pip/*: B007,B008,B009,B014,B305 + # TODO: Remove IOError from except (OSError, IOError) blocks in + # these files when Python 2 is removed. + # In Python 3, IOError have been merged into OSError + # https://github.com/PyCQA/flake8-bugbear/issues/110 + src/pip/_internal/utils/filesystem.py: B014 + src/pip/_internal/network/cache.py: B014 + src/pip/_internal/utils/misc.py: B014 [mypy] follow_imports = silent ignore_missing_imports = True From 76a130105cea16d16a80a5ec50006a8fc90cc3bd Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sat, 6 Jun 2020 02:21:03 +0530 Subject: [PATCH 2031/3170] Fix src/pip with flake8-bugbear --- src/pip/_internal/cli/progress_bars.py | 4 +++- src/pip/_internal/commands/debug.py | 4 ++-- src/pip/_internal/utils/filesystem.py | 4 ++-- src/pip/_internal/utils/misc.py | 7 +++++-- src/pip/_internal/vcs/subversion.py | 2 +- src/pip/_internal/wheel_builder.py | 10 +++++----- 6 files changed, 18 insertions(+), 13 deletions(-) diff --git a/src/pip/_internal/cli/progress_bars.py b/src/pip/_internal/cli/progress_bars.py index 9a4ae592e7c..69338552f13 100644 --- a/src/pip/_internal/cli/progress_bars.py +++ b/src/pip/_internal/cli/progress_bars.py @@ -167,7 +167,9 @@ def pretty_eta(self): def iter(self, it): # type: ignore for x in it: yield x - self.next(len(x)) + # B305 is incorrectly raised here + # https://github.com/PyCQA/flake8-bugbear/issues/59 + self.next(len(x)) # noqa: B305 self.finish() diff --git a/src/pip/_internal/commands/debug.py b/src/pip/_internal/commands/debug.py index d8e2484c1b4..ffe341dde7f 100644 --- a/src/pip/_internal/commands/debug.py +++ b/src/pip/_internal/commands/debug.py @@ -87,7 +87,7 @@ def get_vendor_version_from_module(module_name): if not version: # Try to find version in debundled module info pkg_set = pkg_resources.WorkingSet( - [os.path.dirname(getattr(module, '__file__'))] + [os.path.dirname(getattr(module, '__file__', None))] ) package = pkg_set.find(pkg_resources.Requirement.parse(module_name)) version = getattr(package, 'version', None) @@ -166,7 +166,7 @@ def show_tags(options): def ca_bundle_info(config): # type: (Dict[str, str]) -> str levels = set() - for key, value in config.items(): + for key in config: levels.add(key.split('.')[0]) if not levels: diff --git a/src/pip/_internal/utils/filesystem.py b/src/pip/_internal/utils/filesystem.py index c706a038c2a..303243fd22f 100644 --- a/src/pip/_internal/utils/filesystem.py +++ b/src/pip/_internal/utils/filesystem.py @@ -155,7 +155,7 @@ def _test_writable_dir_win(path): # and we can't use tempfile: http://bugs.python.org/issue22107 basename = 'accesstest_deleteme_fishfingers_custard_' alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789' - for i in range(10): + for _ in range(10): name = basename + ''.join(random.choice(alphabet) for _ in range(6)) file = os.path.join(path, name) try: @@ -190,7 +190,7 @@ def find_files(path, pattern): """Returns a list of absolute paths of files beneath path, recursively, with filenames which match the UNIX-style shell glob pattern.""" result = [] # type: List[str] - for root, dirs, files in os.walk(path): + for root, _, files in os.walk(path): matches = fnmatch.filter(files, pattern) result.extend(os.path.join(root, f) for f in matches) return result diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index c7bb972ffb7..467ae5452e1 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -17,7 +17,7 @@ import sys from collections import deque -from pip._vendor import pkg_resources +from pip._vendor import pkg_resources, six # 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 @@ -548,7 +548,10 @@ def readline(self): try: return next(self._gen) except NameError: - return self._gen.next() + # flake8-bugbear B305 suggests using six.next for + # Python 2 compatibility. This along with the try/except + # block can be removed once we drop Python 2 support + return six.next(self._gen) except StopIteration: return '' diff --git a/src/pip/_internal/vcs/subversion.py b/src/pip/_internal/vcs/subversion.py index 4324a5d9f82..14825f791a4 100644 --- a/src/pip/_internal/vcs/subversion.py +++ b/src/pip/_internal/vcs/subversion.py @@ -56,7 +56,7 @@ def get_revision(cls, location): # Note: taken from setuptools.command.egg_info revision = 0 - for base, dirs, files in os.walk(location): + for base, dirs, _ in os.walk(location): if cls.dirname not in dirs: dirs[:] = [] continue # no sense walking uncontrolled subdirs diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index b5e8bf33924..8b6ddad4739 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -23,7 +23,7 @@ if MYPY_CHECK_RUNNING: from typing import ( - Any, Callable, Iterable, List, Optional, Pattern, Tuple, + Any, Callable, Iterable, List, Optional, Tuple, ) from pip._internal.cache import WheelCache @@ -34,11 +34,11 @@ logger = logging.getLogger(__name__) +_egg_info_re = re.compile(r'([a-z0-9_.]+)-([a-z0-9_.!+-]+)', re.IGNORECASE) -def _contains_egg_info( - s, _egg_info_re=re.compile(r'([a-z0-9_.]+)-([a-z0-9_.!+-]+)', - re.IGNORECASE)): - # type: (str, Pattern[str]) -> bool + +def _contains_egg_info(s): + # type: (str) -> bool """Determine whether the string looks like an egg_info. :param s: The string to parse. E.g. foo-2.1 From 931558277460f951cce52f9ec5b34c64e4b283bf Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sat, 6 Jun 2020 02:21:43 +0530 Subject: [PATCH 2032/3170] Add news entry --- news/AFD0EAD4-42B3-4EC1-A2AE-70D7513C8555.trivial | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 news/AFD0EAD4-42B3-4EC1-A2AE-70D7513C8555.trivial diff --git a/news/AFD0EAD4-42B3-4EC1-A2AE-70D7513C8555.trivial b/news/AFD0EAD4-42B3-4EC1-A2AE-70D7513C8555.trivial new file mode 100644 index 00000000000..e69de29bb2d From d3f012cb3b5f3050cf12754fbd6f19eed30ca856 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sat, 6 Jun 2020 02:46:26 +0530 Subject: [PATCH 2033/3170] Fix argument type for ca_bundle_info --- src/pip/_internal/commands/debug.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/commands/debug.py b/src/pip/_internal/commands/debug.py index ffe341dde7f..acd273e625c 100644 --- a/src/pip/_internal/commands/debug.py +++ b/src/pip/_internal/commands/debug.py @@ -22,6 +22,7 @@ from types import ModuleType from typing import List, Optional, Dict from optparse import Values + from pip._internal.configuration import Configuration logger = logging.getLogger(__name__) @@ -164,9 +165,9 @@ def show_tags(options): def ca_bundle_info(config): - # type: (Dict[str, str]) -> str + # type: (Configuration) -> str levels = set() - for key in config: + for key, _ in config.items(): levels.add(key.split('.')[0]) if not levels: From 021eddcb7048df0693339ac2bfb14813f3c16622 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sat, 6 Jun 2020 12:21:06 +0530 Subject: [PATCH 2034/3170] Remove try/catch and use next --- src/pip/_internal/utils/misc.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 467ae5452e1..50a86982e4e 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -17,7 +17,7 @@ import sys from collections import deque -from pip._vendor import pkg_resources, six +from pip._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 pip._vendor.retrying import retry # type: ignore @@ -545,13 +545,7 @@ def __init__(self, lines): def readline(self): try: - try: - return next(self._gen) - except NameError: - # flake8-bugbear B305 suggests using six.next for - # Python 2 compatibility. This along with the try/except - # block can be removed once we drop Python 2 support - return six.next(self._gen) + return next(self._gen) except StopIteration: return '' From 76257e4b6595e04c9f92cc284b8866d096d6725a Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sat, 6 Jun 2020 13:46:06 +0530 Subject: [PATCH 2035/3170] Use module.__file__ --- src/pip/_internal/commands/debug.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/commands/debug.py b/src/pip/_internal/commands/debug.py index acd273e625c..119569b1886 100644 --- a/src/pip/_internal/commands/debug.py +++ b/src/pip/_internal/commands/debug.py @@ -87,8 +87,12 @@ def get_vendor_version_from_module(module_name): if not version: # Try to find version in debundled module info + # The type for module.__file__ is Optional[str] in + # Python 2, and str in Python 3. The type: ignore is + # added to account for Python 2, instead of a cast + # and should be removed once we drop Python 2 support pkg_set = pkg_resources.WorkingSet( - [os.path.dirname(getattr(module, '__file__', None))] + [os.path.dirname(module.__file__)] # type: ignore ) package = pkg_set.find(pkg_resources.Requirement.parse(module_name)) version = getattr(package, 'version', None) From 8f1d808deb6e6ce06942160c9eef36821da9d59c Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Wed, 10 Jun 2020 00:39:50 +0530 Subject: [PATCH 2036/3170] Add noqa B010 for setattr --- src/pip/_internal/cli/cmdoptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index adfc81767c4..120d51eeebe 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -835,7 +835,7 @@ def _handle_no_use_pep517(option, opt, value, parser): # TODO: Move into a class that inherits from partial, currently does not # work as mypy complains functools.partial is a generic class. # This way we know we can ignore this option in docs auto generation -setattr(always_unzip, 'deprecated', True) +setattr(always_unzip, 'deprecated', True) # noqa: B010 def _handle_merge_hash(option, opt_str, value, parser): From b372132cac9e3c7471b5a6883f7e974b034bf337 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 2 Jun 2020 23:03:33 +0800 Subject: [PATCH 2037/3170] Convert test_install_with_extras_from_install --- tests/functional/test_install_reqs.py | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index 6684ad19a6f..5cf1d31a62b 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -5,6 +5,7 @@ from tests.lib import ( _create_test_package_with_subdirectory, + create_basic_wheel_for_package, need_svn, path_to_url, pyversion, @@ -412,14 +413,19 @@ def test_install_with_extras_from_constraints(script, data): result.did_create(script.site_packages / 'simple') -@pytest.mark.fails_on_new_resolver -def test_install_with_extras_from_install(script, data): - to_install = data.packages.joinpath("LocalExtras") - script.scratch_path.joinpath("constraints.txt").write_text( - "{url}#egg=LocalExtras".format(url=path_to_url(to_install)) +def test_install_with_extras_from_install(script): + create_basic_wheel_for_package( + script, + name="LocalExtras", + version="0.0.1", + extras={"bar": "simple", "baz": ["singlemodule"]}, ) + script.scratch_path.joinpath("constraints.txt").write_text("LocalExtras") result = script.pip_install_local( - '-c', script.scratch_path / 'constraints.txt', 'LocalExtras[baz]') + '--find-links', script.scratch_path, + '-c', script.scratch_path / 'constraints.txt', + 'LocalExtras[baz]', + ) result.did_create(script.site_packages / 'singlemodule.py') From e97b3655292467d5d5d68f8a03c0111993a6bf81 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Wed, 10 Jun 2020 13:08:37 +0100 Subject: [PATCH 2038/3170] Mark a few more tests as working under the new resolver --- tests/functional/test_install.py | 1 - tests/functional/test_install_vcs_git.py | 1 - tests/functional/test_wheel.py | 1 - 3 files changed, 3 deletions(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 6a953027851..b19020108bd 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1760,7 +1760,6 @@ def test_install_pip_does_not_modify_pip_when_satisfied( assert expected_message in result.stdout, str(result) -@pytest.mark.fails_on_new_resolver def test_ignore_yanked_file(script, data): """ Test ignore a "yanked" file. diff --git a/tests/functional/test_install_vcs_git.py b/tests/functional/test_install_vcs_git.py index b13137dec49..b9f9c7abb80 100644 --- a/tests/functional/test_install_vcs_git.py +++ b/tests/functional/test_install_vcs_git.py @@ -493,7 +493,6 @@ def test_install_git_branch_not_cached(script, with_wheel): ), result.stdout -@pytest.mark.fails_on_new_resolver def test_install_git_sha_cached(script, with_wheel): """ Installing git urls with a sha revision does cause wheel caching. diff --git a/tests/functional/test_wheel.py b/tests/functional/test_wheel.py index 22c72a55432..4f60e192eda 100644 --- a/tests/functional/test_wheel.py +++ b/tests/functional/test_wheel.py @@ -62,7 +62,6 @@ def test_pip_wheel_success(script, data): assert "Successfully built simple" in result.stdout, result.stdout -@pytest.mark.fails_on_new_resolver def test_pip_wheel_build_cache(script, data): """ Test 'pip wheel' builds and caches. From 03c59c50db5302b232de894a6c8b970639f1d873 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 29 May 2020 22:36:41 +0800 Subject: [PATCH 2039/3170] Make failing tests fail --- tests/functional/test_install.py | 1 - tests/functional/test_install_reqs.py | 1 - 2 files changed, 2 deletions(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 6a953027851..f8034532eb0 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -927,7 +927,6 @@ def test_install_nonlocal_compatible_wheel(script, data): assert result.returncode == ERROR -@pytest.mark.fails_on_new_resolver def test_install_nonlocal_compatible_wheel_path(script, data): target_dir = script.scratch_path / 'target' diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index 6684ad19a6f..d9601443382 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -519,7 +519,6 @@ def test_install_unsupported_wheel_link_with_marker(script): assert len(result.files_created) == 0 -@pytest.mark.fails_on_new_resolver def test_install_unsupported_wheel_file(script, data): # Trying to install a local wheel with an incompatible version/type # should fail. From ebb90c6411a20acd9b1c1fd8ebb0fbe7dc526f92 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 2 Jun 2020 11:02:08 +0800 Subject: [PATCH 2040/3170] Reject incompatibile direct wheel link This mirrors the behavior in the legacy resolver. In the future we may want to backtrack in this situation instead, but I haven't found a clean way to do this. We may need to introduce an "empty" requirement class. The `PackageFinder.target_python` interface is also not the most clean. Maybe we should expose the target Python object instead. Not sure yet. --- src/pip/_internal/index/package_finder.py | 5 +++++ src/pip/_internal/resolution/resolvelib/factory.py | 9 +++++++++ 2 files changed, 14 insertions(+) diff --git a/src/pip/_internal/index/package_finder.py b/src/pip/_internal/index/package_finder.py index 731e4981d72..84115783ab8 100644 --- a/src/pip/_internal/index/package_finder.py +++ b/src/pip/_internal/index/package_finder.py @@ -658,6 +658,11 @@ def create( ignore_requires_python=selection_prefs.ignore_requires_python, ) + @property + def target_python(self): + # type: () -> TargetPython + return self._target_python + @property def search_scope(self): # type: () -> SearchScope diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 9c5fa4f2380..4cff52da3c5 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -7,7 +7,9 @@ from pip._internal.exceptions import ( InstallationError, UnsupportedPythonVersion, + UnsupportedWheel, ) +from pip._internal.models.wheel import Wheel from pip._internal.utils.compatibility_tags import get_supported from pip._internal.utils.hashes import Hashes from pip._internal.utils.misc import ( @@ -248,6 +250,13 @@ def make_requirement_from_install_req(self, ireq, requested_extras): return None if not ireq.link: return SpecifierRequirement(ireq) + if ireq.link.is_wheel: + wheel = Wheel(ireq.link.filename) + if not wheel.supported(self._finder.target_python.get_tags()): + msg = "{} is not a supported wheel on this platform.".format( + wheel.filename, + ) + raise UnsupportedWheel(msg) cand = self._make_candidate_from_link( ireq.link, extras=frozenset(ireq.extras), From e7635b723349eb3f6bb1d2f055ccf49f8dddd510 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sun, 7 Jun 2020 22:29:55 +0800 Subject: [PATCH 2041/3170] Is this a bug or design decision? --- tests/functional/test_install.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index f8034532eb0..6a953027851 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -927,6 +927,7 @@ def test_install_nonlocal_compatible_wheel(script, data): assert result.returncode == ERROR +@pytest.mark.fails_on_new_resolver def test_install_nonlocal_compatible_wheel_path(script, data): target_dir = script.scratch_path / 'target' From f81fd19a7bbfe742f1b6a05efd8694a2bd40be69 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 28 May 2020 16:08:17 +0800 Subject: [PATCH 2042/3170] Ensure entry points are read as UTF-8 Like the wheel metadata, this is, strictly speaking, unspecified. But UTF-8 is the de-facto standard, and we should support that. --- src/pip/_internal/operations/install/wheel.py | 16 +++++--------- tests/functional/test_install_wheel.py | 22 +++++++++++++++++++ tests/unit/test_wheel.py | 16 +++++++++----- 3 files changed, 38 insertions(+), 16 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 36877ca5e76..b2b074bea21 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -7,6 +7,7 @@ import compileall import contextlib import csv +import io import logging import os.path import re @@ -21,14 +22,7 @@ from pip._vendor import pkg_resources from pip._vendor.distlib.scripts import ScriptMaker from pip._vendor.distlib.util import get_export_entry -from pip._vendor.six import ( - PY2, - StringIO, - ensure_str, - ensure_text, - itervalues, - text_type, -) +from pip._vendor.six import PY2, ensure_str, ensure_text, itervalues, text_type from pip._internal.exceptions import InstallationError from pip._internal.locations import get_major_minor_version @@ -131,11 +125,11 @@ def get_entrypoints(filename): # means that they may or may not be valid INI files. The attempt here is to # strip leading and trailing whitespace in order to make them valid INI # files. - with open(filename) as fp: - data = StringIO() + with io.open(filename, encoding="utf-8") as fp: + data = io.StringIO() for line in fp: data.write(line.strip()) - data.write("\n") + data.write(u"\n") data.seek(0) # get the entry points and then the script names diff --git a/tests/functional/test_install_wheel.py b/tests/functional/test_install_wheel.py index 660ee667045..6d94622458a 100644 --- a/tests/functional/test_install_wheel.py +++ b/tests/functional/test_install_wheel.py @@ -348,6 +348,28 @@ def test_install_from_wheel_gen_uppercase_entrypoint( assert bool(os.access(script.base_path / wrapper_file, os.X_OK)) +# pkg_resources.EntryPoint() does not parse unicode correctly on Python 2. +@skip_if_python2 +def test_install_from_wheel_gen_unicode_entrypoint(script): + make_wheel( + "script_wheel_unicode", + "1.0", + console_scripts=["進入點 = 模組:函式"], + ).save_to_dir(script.scratch_path) + + result = script.pip( + "install", + "--no-index", + "--find-links", + script.scratch_path, + "script_wheel_unicode", + ) + if os.name == "nt": + result.did_create(script.bin.joinpath("進入點.exe")) + else: + result.did_create(script.bin.joinpath("進入點")) + + def test_install_from_wheel_with_legacy(script, shared_data, tmpdir): """ Test installing scripts (legacy scripts are preserved) diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 2834b18f087..7335e1ff8aa 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -2,6 +2,7 @@ """Tests for wheel binary packages and .dist-info.""" import csv +import io import logging import os import textwrap @@ -29,7 +30,7 @@ from pip._internal.utils.compat import WINDOWS from pip._internal.utils.misc import hash_file from pip._internal.utils.unpacking import unpack_file -from tests.lib import DATA_DIR, assert_paths_equal +from tests.lib import DATA_DIR, assert_paths_equal, skip_if_python2 def call_get_legacy_build_wheel_path(caplog, names): @@ -81,12 +82,17 @@ def test_get_legacy_build_wheel_path__multiple_names(caplog): ] -@pytest.mark.parametrize("console_scripts", - ["pip = pip._internal.main:pip", - "pip:pip = pip._internal.main:pip"]) +@pytest.mark.parametrize( + "console_scripts", + [ + "pip = pip._internal.main:pip", + "pip:pip = pip._internal.main:pip", + pytest.param("進入點 = 套件.模組:函式", marks=skip_if_python2), + ], +) def test_get_entrypoints(tmpdir, console_scripts): entry_points = tmpdir.joinpath("entry_points.txt") - with open(str(entry_points), "w") as fp: + with io.open(str(entry_points), "w", encoding="utf-8") as fp: fp.write(""" [console_scripts] {} From 216328ce102d43ba5ac9cfbc12396645db14e15c Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 28 May 2020 16:12:48 +0800 Subject: [PATCH 2043/3170] News --- news/8342.bugfix | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 news/8342.bugfix diff --git a/news/8342.bugfix b/news/8342.bugfix new file mode 100644 index 00000000000..fd6b9b8257b --- /dev/null +++ b/news/8342.bugfix @@ -0,0 +1,2 @@ +Correctly treat non-ASCII entry point declarations in wheels so they can be +installed on Windows. From dc82ac2e0c382dbd17b3b6898faf12a2d4174948 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 28 May 2020 16:13:02 +0800 Subject: [PATCH 2044/3170] Fix typo in NEWS.rst --- NEWS.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.rst b/NEWS.rst index 46d322a4216..ac3c8615375 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -9,7 +9,7 @@ Deprecations and Removals Bug Fixes --------- -- Correctly treat wheels contenting non-ASCII file contents so they can be +- Correctly treat wheels containing non-ASCII file contents so they can be installed on Windows. (`#5712 <https://github.com/pypa/pip/issues/5712>`_) - Revert building of local directories in place, restoring the pre-20.1 behaviour of copying to a temporary directory. (`#7555 <https://github.com/pypa/pip/issues/7555>`_) From b0c1308d64878c26948bcde6cfc047b1fcd021c2 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 28 May 2020 16:15:40 +0800 Subject: [PATCH 2045/3170] Always Unicode --- tests/unit/test_wheel.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 7335e1ff8aa..f734e0ae647 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -85,15 +85,15 @@ def test_get_legacy_build_wheel_path__multiple_names(caplog): @pytest.mark.parametrize( "console_scripts", [ - "pip = pip._internal.main:pip", - "pip:pip = pip._internal.main:pip", - pytest.param("進入點 = 套件.模組:函式", marks=skip_if_python2), + u"pip = pip._internal.main:pip", + u"pip:pip = pip._internal.main:pip", + pytest.param(u"進入點 = 套件.模組:函式", marks=skip_if_python2), ], ) def test_get_entrypoints(tmpdir, console_scripts): entry_points = tmpdir.joinpath("entry_points.txt") with io.open(str(entry_points), "w", encoding="utf-8") as fp: - fp.write(""" + fp.write(u""" [console_scripts] {} [section] From 64432abc37c41c8d8ee0037326fb9f18b6e5a321 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 27 May 2020 08:08:01 +0800 Subject: [PATCH 2046/3170] Update issue template to nudge users --- .github/ISSUE_TEMPLATE/bug-report.md | 5 +++- .github/ISSUE_TEMPLATE/resolver-failure.md | 30 ++++++++++++++++------ 2 files changed, 26 insertions(+), 9 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index 8852c7ce79a..fdefbe1a431 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -1,9 +1,12 @@ --- name: Bug report about: Create a report to help us improve - --- +<!-- +If you're reporting an issue for `--unstable-feature=resolver`, use the "Dependency resolver failures / errors" template instead. +--> + **Environment** * pip version: diff --git a/.github/ISSUE_TEMPLATE/resolver-failure.md b/.github/ISSUE_TEMPLATE/resolver-failure.md index 2e719b51b5e..dbe545e61ab 100644 --- a/.github/ISSUE_TEMPLATE/resolver-failure.md +++ b/.github/ISSUE_TEMPLATE/resolver-failure.md @@ -5,16 +5,30 @@ labels: ["K: UX", "K: crash", "C: new resolver", "C: dependency resolution"] --- <!-- - Please provide as much information as you can about your failure, so that we can understand the root cause. +Please provide as much information as you can about your failure, so that we can understand the root cause. - For example, if you are installing packages from pypi.org, we'd like to see: +Try if your issue has been fixed in the in-development version of pip. Use the following command to install pip from master: - - Your terminal output - - Any inputs to pip, for example: - - any package requirements: any CLI arguments and/or your requirements.txt file - - any already installed packages, outputted via `pip freeze` + python -m pip install -U "pip @ git+https://github.com/pypa/pip.git" +--> + +**What did you want to do?** +<!-- Include any inputs you gave to pip, for example: + +* Package requirements: any CLI arguments and/or your requirements.txt file +* Already installed packages, outputted via `pip freeze` +--> - It would be great if you could also include your dependency tree. For this you can use pipdeptree: https://pypi.org/project/pipdeptree/ +**Output** + +``` +Paste what pip outputted in a code block. https://github.github.com/gfm/#fenced-code-blocks +``` + +**Additional information** + +<!-- +It would be great if you could also include your dependency tree. For this you can use pipdeptree: https://pypi.org/project/pipdeptree/ - For users installing packages from a private repository or local directory, please try your best to describe your setup. We'd like to understand how to reproduce the error locally, so would need (at a minimum) a description of the packages you are trying to install, and a list of dependencies for each package. +For users installing packages from a private repository or local directory, please try your best to describe your setup. We'd like to understand how to reproduce the error locally, so would need (at a minimum) a description of the packages you are trying to install, and a list of dependencies for each package. --> From e2a2a2b9ecf607d282cfe038516ba045e1ae514d Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 10 Jun 2020 06:19:54 +0800 Subject: [PATCH 2047/3170] Use GitHub snapshot to avoid Git dependency Co-authored-by: Xavier Fernandez <xav.fernandez@gmail.com> --- .github/ISSUE_TEMPLATE/resolver-failure.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/resolver-failure.md b/.github/ISSUE_TEMPLATE/resolver-failure.md index dbe545e61ab..b5215cef94d 100644 --- a/.github/ISSUE_TEMPLATE/resolver-failure.md +++ b/.github/ISSUE_TEMPLATE/resolver-failure.md @@ -9,7 +9,7 @@ Please provide as much information as you can about your failure, so that we can Try if your issue has been fixed in the in-development version of pip. Use the following command to install pip from master: - python -m pip install -U "pip @ git+https://github.com/pypa/pip.git" + python -m pip install -U "pip @ https://github.com/pypa/pip/archive/master.zip" --> **What did you want to do?** From 91270545d0bf590f9ef230d00460781b6617c294 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Thu, 11 Jun 2020 14:59:46 +0530 Subject: [PATCH 2048/3170] Clarify global and per-requirement options in pip install docs --- docs/html/reference/pip_install.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index f33e279373d..11e643ee7d5 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -158,11 +158,16 @@ The following options are supported: .. pip-requirements-file-options-ref-list:: +Please note that the above options are global options, and should be specified on their individual lines. +The options which can be applied to individual requirements are +:ref:`--install-option <install_--install-option>`, :ref:`--global-option <install_--global-option>` and ``--hash``. + For example, to specify :ref:`--no-index <install_--no-index>` and two :ref:`--find-links <install_--find-links>` locations: :: +--pre --no-index --find-links /my/local/archives --find-links http://some.archives.com/archives From 606b49a881570905ef940ac90fc54a2fc53b0749 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Thu, 11 Jun 2020 15:00:31 +0530 Subject: [PATCH 2049/3170] Add news entry --- news/C505A166-6B1D-4509-8ECA-84EB35A6A391.trivial | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 news/C505A166-6B1D-4509-8ECA-84EB35A6A391.trivial diff --git a/news/C505A166-6B1D-4509-8ECA-84EB35A6A391.trivial b/news/C505A166-6B1D-4509-8ECA-84EB35A6A391.trivial new file mode 100644 index 00000000000..e69de29bb2d From 6446de93bff93d3ead36bf8531bc29c3707ca92b Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Thu, 11 Jun 2020 15:11:00 +0530 Subject: [PATCH 2050/3170] Update example text to include --pre --- docs/html/reference/pip_install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index 11e643ee7d5..973de8701f5 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -162,7 +162,7 @@ Please note that the above options are global options, and should be specified o The options which can be applied to individual requirements are :ref:`--install-option <install_--install-option>`, :ref:`--global-option <install_--global-option>` and ``--hash``. -For example, to specify :ref:`--no-index <install_--no-index>` and two +For example, to specify :ref:`--pre <install_--pre>`, :ref:`--no-index <install_--no-index>` and two :ref:`--find-links <install_--find-links>` locations: :: From bf2b63dd686d6228ee725d4a292076c323eb4cb8 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Thu, 11 Jun 2020 11:41:07 +0100 Subject: [PATCH 2051/3170] Modify tests to check unsupported forms of constraint in the new resolver --- tests/functional/test_install_reqs.py | 100 +++++++++++++++++--------- 1 file changed, 68 insertions(+), 32 deletions(-) diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index 5cf1d31a62b..e72007b3b3c 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -332,40 +332,56 @@ def test_constraints_local_install_causes_error(script, data): assert 'Could not satisfy constraints for' in result.stderr -@pytest.mark.fails_on_new_resolver -def test_constraints_constrain_to_local_editable(script, data): +def test_constraints_constrain_to_local_editable( + script, + data, + use_new_resolver +): to_install = data.src.joinpath("singlemodule") script.scratch_path.joinpath("constraints.txt").write_text( "-e {url}#egg=singlemodule".format(url=path_to_url(to_install)) ) result = script.pip( 'install', '--no-index', '-f', data.find_links, '-c', - script.scratch_path / 'constraints.txt', 'singlemodule') - assert 'Running setup.py develop for singlemodule' in result.stdout + script.scratch_path / 'constraints.txt', 'singlemodule', + expect_error=use_new_resolver + ) + if use_new_resolver: + assert 'Links are not allowed as constraints' in result.stderr + else: + assert 'Running setup.py develop for singlemodule' in result.stdout -@pytest.mark.fails_on_new_resolver -def test_constraints_constrain_to_local(script, data): +def test_constraints_constrain_to_local(script, data, use_new_resolver): to_install = data.src.joinpath("singlemodule") script.scratch_path.joinpath("constraints.txt").write_text( "{url}#egg=singlemodule".format(url=path_to_url(to_install)) ) result = script.pip( 'install', '--no-index', '-f', data.find_links, '-c', - script.scratch_path / 'constraints.txt', 'singlemodule') - assert 'Running setup.py install for singlemodule' in result.stdout + script.scratch_path / 'constraints.txt', 'singlemodule', + expect_error=use_new_resolver + ) + if use_new_resolver: + assert 'Links are not allowed as constraints' in result.stderr + else: + assert 'Running setup.py install for singlemodule' in result.stdout -@pytest.mark.fails_on_new_resolver -def test_constrained_to_url_install_same_url(script, data): +def test_constrained_to_url_install_same_url(script, data, use_new_resolver): to_install = data.src.joinpath("singlemodule") constraints = path_to_url(to_install) + "#egg=singlemodule" script.scratch_path.joinpath("constraints.txt").write_text(constraints) result = script.pip( 'install', '--no-index', '-f', data.find_links, '-c', - script.scratch_path / 'constraints.txt', to_install) - assert ('Running setup.py install for singlemodule' - in result.stdout), str(result) + script.scratch_path / 'constraints.txt', to_install, + expect_error=use_new_resolver + ) + if use_new_resolver: + assert 'Links are not allowed as constraints' in result.stderr + else: + assert ('Running setup.py install for singlemodule' + in result.stdout), str(result) def test_double_install_spurious_hash_mismatch( @@ -402,15 +418,19 @@ def test_double_install_spurious_hash_mismatch( assert 'Successfully installed simple-1.0' in str(result) -@pytest.mark.fails_on_new_resolver -def test_install_with_extras_from_constraints(script, data): +def test_install_with_extras_from_constraints(script, data, use_new_resolver): to_install = data.packages.joinpath("LocalExtras") script.scratch_path.joinpath("constraints.txt").write_text( "{url}#egg=LocalExtras[bar]".format(url=path_to_url(to_install)) ) result = script.pip_install_local( - '-c', script.scratch_path / 'constraints.txt', 'LocalExtras') - result.did_create(script.site_packages / 'simple') + '-c', script.scratch_path / 'constraints.txt', 'LocalExtras', + expect_error=use_new_resolver + ) + if use_new_resolver: + assert 'Links are not allowed as constraints' in result.stderr + else: + result.did_create(script.site_packages / 'simple') def test_install_with_extras_from_install(script): @@ -429,29 +449,36 @@ def test_install_with_extras_from_install(script): result.did_create(script.site_packages / 'singlemodule.py') -@pytest.mark.fails_on_new_resolver -def test_install_with_extras_joined(script, data): +def test_install_with_extras_joined(script, data, use_new_resolver): to_install = data.packages.joinpath("LocalExtras") script.scratch_path.joinpath("constraints.txt").write_text( "{url}#egg=LocalExtras[bar]".format(url=path_to_url(to_install)) ) result = script.pip_install_local( - '-c', script.scratch_path / 'constraints.txt', 'LocalExtras[baz]' + '-c', script.scratch_path / 'constraints.txt', 'LocalExtras[baz]', + expect_error=use_new_resolver ) - result.did_create(script.site_packages / 'simple') - result.did_create(script.site_packages / 'singlemodule.py') + if use_new_resolver: + assert 'Links are not allowed as constraints' in result.stderr + else: + result.did_create(script.site_packages / 'simple') + result.did_create(script.site_packages / 'singlemodule.py') -@pytest.mark.fails_on_new_resolver -def test_install_with_extras_editable_joined(script, data): +def test_install_with_extras_editable_joined(script, data, use_new_resolver): to_install = data.packages.joinpath("LocalExtras") script.scratch_path.joinpath("constraints.txt").write_text( "-e {url}#egg=LocalExtras[bar]".format(url=path_to_url(to_install)) ) result = script.pip_install_local( - '-c', script.scratch_path / 'constraints.txt', 'LocalExtras[baz]') - result.did_create(script.site_packages / 'simple') - result.did_create(script.site_packages / 'singlemodule.py') + '-c', script.scratch_path / 'constraints.txt', 'LocalExtras[baz]', + expect_error=use_new_resolver + ) + if use_new_resolver: + assert 'Links are not allowed as constraints' in result.stderr + else: + result.did_create(script.site_packages / 'simple') + result.did_create(script.site_packages / 'singlemodule.py') def test_install_distribution_full_union(script, data): @@ -473,15 +500,24 @@ def test_install_distribution_duplicate_extras(script, data): assert expected in result.stderr -@pytest.mark.fails_on_new_resolver -def test_install_distribution_union_with_constraints(script, data): +def test_install_distribution_union_with_constraints( + script, + data, + use_new_resolver +): to_install = data.packages.joinpath("LocalExtras") script.scratch_path.joinpath("constraints.txt").write_text( "{to_install}[bar]".format(**locals())) result = script.pip_install_local( - '-c', script.scratch_path / 'constraints.txt', to_install + '[baz]') - assert 'Running setup.py install for LocalExtras' in result.stdout - result.did_create(script.site_packages / 'singlemodule.py') + '-c', script.scratch_path / 'constraints.txt', to_install + '[baz]', + expect_error=use_new_resolver + ) + if use_new_resolver: + msg = 'Unnamed requirements are not allowed as constraints' + assert msg in result.stderr + else: + assert 'Running setup.py install for LocalExtras' in result.stdout + result.did_create(script.site_packages / 'singlemodule.py') @pytest.mark.fails_on_new_resolver From f9ecbf0b9d847b3b171a7ceae5efedebed3250c6 Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Tue, 5 May 2020 11:44:48 -0500 Subject: [PATCH 2052/3170] add conflicting yaml test --- tests/yaml/conflict_1.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 tests/yaml/conflict_1.yml diff --git a/tests/yaml/conflict_1.yml b/tests/yaml/conflict_1.yml new file mode 100644 index 00000000000..057ca7faddd --- /dev/null +++ b/tests/yaml/conflict_1.yml @@ -0,0 +1,19 @@ +base: + available: + - A 1.0.0; depends B == 1.0.0, B == 2.0.0 + - B 1.0.0 + - B 2.0.0 + +cases: +- + request: + - install: A + response: + - state: null + skip: true +- + request: + - install: ['B==1.0.0', 'B==2.0.0'] + response: + - state: null + skip: true From 483b5ded21691fe12b709fd1c1e4aae269b81ad5 Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Tue, 5 May 2020 11:59:16 -0500 Subject: [PATCH 2053/3170] add comments --- tests/yaml/conflict_1.yml | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/yaml/conflict_1.yml b/tests/yaml/conflict_1.yml index 057ca7faddd..cdf35803237 100644 --- a/tests/yaml/conflict_1.yml +++ b/tests/yaml/conflict_1.yml @@ -11,9 +11,20 @@ cases: response: - state: null skip: true + # -- currently the error message is: + # a 1.0.0 has requirement B==1.0.0, but you'll have b 2.0.0 which is incompatible. + # -- better would be: + # A 1.0.0 has incompatible requirements B==1.0.0, B==2.0.0 - request: - install: ['B==1.0.0', 'B==2.0.0'] response: - state: null skip: true + # -- currently the error message is: + # Could not find a version that satisfies the requirement B==1.0.0 + # Could not find a version that satisfies the requirement B==2.0.0 + # No matching distribution found for b, b + # -- better would be: + # cannot install different version (1.0.0, 2.0.0) of package B at the + # same time. From 083c2418797c90a03c292eaba68f17cd5f7299b8 Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Tue, 5 May 2020 13:20:27 -0500 Subject: [PATCH 2054/3170] add comment --- tests/yaml/conflict_1.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/yaml/conflict_1.yml b/tests/yaml/conflict_1.yml index cdf35803237..57bb875d31c 100644 --- a/tests/yaml/conflict_1.yml +++ b/tests/yaml/conflict_1.yml @@ -28,3 +28,5 @@ cases: # -- better would be: # cannot install different version (1.0.0, 2.0.0) of package B at the # same time. + # -- the old error message was actually better here: + # Double requirement given: B==2.0.0 (already in B==1.0.0, name='B') From ab5cb8ebe1d7afbd0708a6fb587dba509f7eddd9 Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Tue, 5 May 2020 21:27:08 -0500 Subject: [PATCH 2055/3170] add comment --- tests/yaml/conflicting_triangle.yml | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/yaml/conflicting_triangle.yml b/tests/yaml/conflicting_triangle.yml index 6296b0acc29..b9dbe5287bd 100644 --- a/tests/yaml/conflicting_triangle.yml +++ b/tests/yaml/conflicting_triangle.yml @@ -18,3 +18,8 @@ cases: - required_by: [B 1.0.0] selector: C == 2.0.0 skip: true + # -- currently the error message is: + # a 1.0.0 has requirement C==1.0.0, but you'll have c 2.0.0 which is incompatible. + # -- better would be something like: + # A 1.0.0 -> C 1.0.0 + # A 1.0.0 -> B 1.0.0 -> C 2.0.0 From 6c4b06295d5b21bd7626e2b502afc8c1593b6ff2 Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Tue, 5 May 2020 21:44:07 -0500 Subject: [PATCH 2056/3170] add yaml test --- tests/yaml/conflict_2.yml | 28 ++++++++++++++++++++++++++++ 1 file changed, 28 insertions(+) create mode 100644 tests/yaml/conflict_2.yml diff --git a/tests/yaml/conflict_2.yml b/tests/yaml/conflict_2.yml new file mode 100644 index 00000000000..57427f5b07a --- /dev/null +++ b/tests/yaml/conflict_2.yml @@ -0,0 +1,28 @@ +# Tzu-ping mentioned this example +base: + available: + - name: virtualenv + version: 20.0.2 + depends: ['six>=1.12.0,<2'] + - six 1.11 + - six 1.12 + - six 1.13 + +cases: +- + request: + - install: virtualenv + response: + - state: + - six 1.13 + - virtualenv 20.0.2 +- + request: + - install: ['six<1.12', 'virtualenv==20.0.2'] + response: + - state: null + skip: true + # -- currently the error message is: + # Could not find a version that satisfies the requirement six<1.12 + # Could not find a version that satisfies the requirement six<2,>=1.12.0 (from virtualenv) + # No matching distribution found for six, six From a941724aa8652cf65790e2f729fa1de16bc641e3 Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Tue, 5 May 2020 22:00:38 -0500 Subject: [PATCH 2057/3170] add test case --- tests/yaml/conflict_1.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/yaml/conflict_1.yml b/tests/yaml/conflict_1.yml index 57bb875d31c..589ee962dc1 100644 --- a/tests/yaml/conflict_1.yml +++ b/tests/yaml/conflict_1.yml @@ -30,3 +30,12 @@ cases: # same time. # -- the old error message was actually better here: # Double requirement given: B==2.0.0 (already in B==1.0.0, name='B') +- + request: + - install: A==2.0 + response: + - state: null + skip: true + # -- currently the error message is: + # Could not find a version that satisfies the requirement A==2.0 + # No matching distribution found for a From 2f961cdfa70c441b87399c9b2b89ffed62fb0bbf Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Tue, 5 May 2020 22:25:59 -0500 Subject: [PATCH 2058/3170] add explaination of yaml format --- tests/yaml/README.md | 47 ++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 45 insertions(+), 2 deletions(-) diff --git a/tests/yaml/README.md b/tests/yaml/README.md index f9adee78795..b1c9bb56644 100644 --- a/tests/yaml/README.md +++ b/tests/yaml/README.md @@ -4,8 +4,51 @@ This directory contains fixtures for testing pip's resolver. The fixtures are written as `.yml` files, with a convenient format that allows for specifying a custom index for temporary use. -The `.yml` files are organized in the following way. A `base` section -which ... +The `.yml` files are typically organized in the following way. Here, we are +going to take a closer look at the `simple.yml` file and step through the +test cases. A `base` section defines which packages are available upstream: + + base: + available: + - simple 0.1.0 + - simple 0.2.0 + - base 0.1.0; depends dep + - dep 0.1.0 + +Each package has a name and version number. Here, there are two +packages `simple` (with versoin `0.1.0` and `0.2.0`). The package +`base 0.1.0` depends on the requirement `dep` (which simply means it +depends on any version of `dep`. More generally, a package can also +depend on a specific version of another package, or a range of versions. + +Next, in our yaml file, we have the `cases:` section which is a list of +test cases. Each test case has a request and a response. The request +is what the user would want to do: + + cases: + - + request: + - install: simple + - uninstall: simple + response: + - state: + - simple 0.2.0 + - state: null + +Here the first request is to install the package simple, this would +basically be equivalent to typing `pip install simple`, and the corresponding +first response is that the state of installed packages is `simple 0.2.0`. +Note that by default the highest version of an available package will be +installed. + +The second request is to uninstall simple again, which will result in the +state `null` (basically an empty list of installed packages). + +When the yaml tests are run, each response is verified by checking which +packages got actually installed. Note that this is check is done in +alphabetical order. + + The linter is very useful for initally checking `.yml` files, e.g.: From 279bbc7d9562226af20b8a81935d995ff7c12f9b Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Tue, 5 May 2020 23:30:05 -0500 Subject: [PATCH 2059/3170] update comments --- tests/yaml/conflicting_diamond.yml | 8 ++++++++ tests/yaml/conflicting_triangle.yml | 3 --- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/tests/yaml/conflicting_diamond.yml b/tests/yaml/conflicting_diamond.yml index 8f5ab12755f..eef90e21f60 100644 --- a/tests/yaml/conflicting_diamond.yml +++ b/tests/yaml/conflicting_diamond.yml @@ -15,3 +15,11 @@ cases: - required_by: [A 1.0.0, C 1.0.0] selector: D == 2.0.0 skip: true + # -- currently the error message is: + # Could not find a version that satisfies the requirement D==2.0.0 (from c) + # Could not find a version that satisfies the requirement D==1.0.0 (from b) + # No matching distribution found for d, d + # -- This is a bit confusing, as both versions of D are available. + # -- better would be something like: + # A 1.0.0 -> B 1.0.0 -> D 1.0.0 + # A 1.0.0 -> C 1.0.0 -> D 2.0.0 diff --git a/tests/yaml/conflicting_triangle.yml b/tests/yaml/conflicting_triangle.yml index b9dbe5287bd..f2c5002ca42 100644 --- a/tests/yaml/conflicting_triangle.yml +++ b/tests/yaml/conflicting_triangle.yml @@ -20,6 +20,3 @@ cases: skip: true # -- currently the error message is: # a 1.0.0 has requirement C==1.0.0, but you'll have c 2.0.0 which is incompatible. - # -- better would be something like: - # A 1.0.0 -> C 1.0.0 - # A 1.0.0 -> B 1.0.0 -> C 2.0.0 From 057cb039900914ba3fcf553c5f68936009ad53b0 Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Wed, 6 May 2020 00:07:56 -0500 Subject: [PATCH 2060/3170] add fall back test --- tests/yaml/fallback.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) create mode 100644 tests/yaml/fallback.yml diff --git a/tests/yaml/fallback.yml b/tests/yaml/fallback.yml new file mode 100644 index 00000000000..6902ad57991 --- /dev/null +++ b/tests/yaml/fallback.yml @@ -0,0 +1,20 @@ +base: + available: + - A 1.0.0; depends B == 1.0.0, C == 1.0.0 + - A 0.8.0 + - B 1.0.0; depends D == 1.0.0 + - C 1.0.0; depends D == 2.0.0 + - D 1.0.0 + - D 2.0.0 + +cases: +- + request: + - install: A + response: + - state: + - A 0.8.0 + # the old resolver tries to install A 1.0.0 (which fails), but the new + # resolver realises that A 1.0.0 cannot be installed and falls back to + # installing the older version A 0.8.0 instead. + skip: old From ed3a6bcd627a686dd757be280d65126a8252261f Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Wed, 6 May 2020 00:20:15 -0500 Subject: [PATCH 2061/3170] add triangle conflict test --- tests/yaml/conflict_3.yml | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 tests/yaml/conflict_3.yml diff --git a/tests/yaml/conflict_3.yml b/tests/yaml/conflict_3.yml new file mode 100644 index 00000000000..d261b3158d2 --- /dev/null +++ b/tests/yaml/conflict_3.yml @@ -0,0 +1,22 @@ +base: + available: + - A 1.0.0; depends B == 1.0.0, C == 2.0.0 + - B 1.0.0; depends C == 1.0.0 + - C 1.0.0 + - C 2.0.0 + +cases: +- + request: + - install: A + response: + - state: null + skip: old + # -- currently the error message is: + # Could not find a version that satisfies the requirement C==2.0.0 (from a) + # Could not find a version that satisfies the requirement C==1.0.0 (from b) + # No matching distribution found for c, c + # -- This is a bit confusing, as both versions of C are available. + # -- better would be something like: + # A 1.0.0 -> B 1.0.0 -> C 1.0.0 + # A 1.0.0 -> C 2.0.0 From ecfaf9fd85a5c1e0eae7e07e956d42f2c397a9e0 Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Wed, 6 May 2020 13:47:38 -0500 Subject: [PATCH 2062/3170] add linter check to ensure version is string --- tests/functional/test_yaml.py | 2 +- tests/yaml/linter.py | 2 ++ 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/functional/test_yaml.py b/tests/functional/test_yaml.py index 4a03381a31c..31a36ac8349 100644 --- a/tests/functional/test_yaml.py +++ b/tests/functional/test_yaml.py @@ -176,7 +176,7 @@ def test_yaml_based(script, case): request.get('options', '').split(), case[':resolver:'] == 'new') - if 0: # for analyzing output easier + if 1: # for analyzing output easier with open(DATA_DIR.parent / "yaml" / case[':name:'].replace('*', '-'), 'w') as fo: result = effect['result'] diff --git a/tests/yaml/linter.py b/tests/yaml/linter.py index dd8b60a3d81..109ce17c155 100644 --- a/tests/yaml/linter.py +++ b/tests/yaml/linter.py @@ -48,6 +48,8 @@ def lint_case(case, verbose=False): check_dict(package, required=['name', 'version'], optional=['depends', 'extras']) + version = package['version'] + assert isinstance(version, str), repr(version) for request, response in zip(requests, responses): check_dict(request, optional=['install', 'uninstall', 'options']) From 5e707eec4b6cc8f413edcb4d905ec60e73d70b05 Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Wed, 6 May 2020 13:59:47 -0500 Subject: [PATCH 2063/3170] add large yaml test --- tests/yaml/large.yml | 247 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 247 insertions(+) create mode 100644 tests/yaml/large.yml diff --git a/tests/yaml/large.yml b/tests/yaml/large.yml new file mode 100644 index 00000000000..a7e6d421c3d --- /dev/null +++ b/tests/yaml/large.yml @@ -0,0 +1,247 @@ +base: + available: + - affine 2.2.0 + - affine 2.2.1 + - asn1crypto 0.22.0 + - asn1crypto 0.23.0 + - asn1crypto 0.24.0 + - backports 1.0 + - name: backports.functools_lru_cache + version: '1.4' + depends: ['backports', 'setuptools'] + - name: backports.functools_lru_cache + version: '1.5' + depends: ['backports', 'setuptools'] + - beautifulsoup4 4.6.0 + - beautifulsoup4 4.6.1 + - beautifulsoup4 4.6.3 + - name: cachecontrol + version: 0.12.3 + depends: ['msgpack-python', 'requests'] + - name: cachecontrol + version: 0.12.4 + depends: ['msgpack-python', 'requests'] + - name: cachecontrol + version: 0.12.5 + depends: ['msgpack-python', 'requests'] + - certifi 2017.11.5 + - certifi 2017.7.27.1 + - certifi 2018.1.18 + - certifi 2018.4.16 + - certifi 2018.8.13 + - name: cffi + version: 1.10.0 + depends: ['pycparser'] + - name: cffi + version: 1.11.2 + depends: ['pycparser'] + - name: cffi + version: 1.11.4 + depends: ['pycparser'] + - name: cffi + version: 1.11.5 + depends: ['pycparser'] + - chardet 3.0.4 + - click 6.7 + - colorama 0.3.9 + - colour 0.1.4 + - colour 0.1.5 + - contextlib2 0.5.5 + - name: cryptography + version: 2.0.3 + depends: ['asn1crypto >=0.21.0', 'cffi >=1.7', 'idna >=2.1', 'six >=1.4.1'] + - name: cryptography + version: 2.1.3 + depends: ['asn1crypto >=0.21.0', 'cffi >=1.7', 'idna >=2.1', 'six >=1.4.1'] + - name: cryptography + version: 2.1.4 + depends: ['asn1crypto >=0.21.0', 'cffi >=1.7', 'idna >=2.1', 'six >=1.4.1'] + - name: cryptography + version: 2.2.1 + depends: ['asn1crypto >=0.21.0', 'cffi >=1.7', 'idna >=2.1', 'six >=1.4.1'] + - name: cryptography + version: '2.3' + depends: ['asn1crypto >=0.21.0', 'cffi >=1.7', 'cryptography-vectors ~=2.3', 'idna >=2.1', 'six >=1.4.1'] + - cryptography-vectors 2.0.3 + - cryptography-vectors 2.1.3 + - cryptography-vectors 2.1.4 + - cryptography-vectors 2.2.1 + - cryptography-vectors 2.2.2 + - cryptography-vectors 2.3 + - name: cytoolz + version: 0.8.2 + depends: ['toolz >=0.8.0'] + - name: cytoolz + version: 0.9.0 + depends: ['toolz >=0.8.0'] + - name: cytoolz + version: 0.9.0.1 + depends: ['toolz >=0.8.0'] + - distlib 0.2.5 + - distlib 0.2.6 + - distlib 0.2.7 + - enum34 1.1.6 + - filelock 2.0.12 + - filelock 2.0.13 + - filelock 3.0.4 + - future 0.16.0 + - futures 3.1.1 + - futures 3.2.0 + - glob2 0.5 + - glob2 0.6 + - name: html5lib + version: '0.999999999' + depends: ['six >=1.9', 'webencodings'] + - name: html5lib + version: 1.0.1 + depends: ['six >=1.9', 'webencodings'] + - idna 2.6 + - idna 2.7 + - ipaddress 1.0.18 + - ipaddress 1.0.19 + - ipaddress 1.0.22 + - name: jinja2 + version: '2.10' + depends: ['markupsafe >=0.23', 'setuptools'] + - name: jinja2 + version: 2.9.6 + depends: ['markupsafe >=0.23', 'setuptools'] + - lockfile 0.12.2 + - markupsafe 1.0 + - msgpack-python 0.4.8 + - msgpack-python 0.5.1 + - msgpack-python 0.5.5 + - msgpack-python 0.5.6 + - name: packaging + version: '16.8' + depends: ['pyparsing', 'six'] + - name: packaging + version: '17.1' + depends: ['pyparsing', 'six'] + - name: pip + version: 10.0.1 + depends: ['setuptools', 'wheel'] + - name: pip + version: 9.0.1 + depends: ['cachecontrol', 'colorama', 'distlib', 'html5lib', 'lockfile', 'packaging', 'progress', 'requests', 'setuptools', 'webencodings', 'wheel'] + - name: pip + version: 9.0.3 + depends: ['setuptools', 'wheel'] + - pkginfo 1.4.1 + - pkginfo 1.4.2 + - progress 1.3 + - progress 1.4 + - psutil 5.2.2 + - psutil 5.3.1 + - psutil 5.4.0 + - psutil 5.4.1 + - psutil 5.4.3 + - psutil 5.4.5 + - psutil 5.4.6 + - pycosat 0.6.2 + - pycosat 0.6.3 + - pycparser 2.18 + - name: pyopenssl + version: 17.2.0 + depends: ['cryptography >=1.9', 'six >=1.5.2'] + - name: pyopenssl + version: 17.4.0 + depends: ['cryptography >=1.9', 'six >=1.5.2'] + - name: pyopenssl + version: 17.5.0 + depends: ['cryptography >=2.1.4', 'six >=1.5.2'] + - name: pyopenssl + version: 18.0.0 + depends: ['cryptography >=2.2.1', 'six >=1.5.2'] + - pyparsing 2.2.0 + - name: pysocks + version: 1.6.7 + depends: ['win_inet_pton'] + - name: pysocks + version: 1.6.8 + depends: ['win_inet_pton'] + - pywin32 221 + - pywin32 222 + - pywin32 223 + - pyyaml 3.12 + - pyyaml 3.13 + - name: requests + version: 2.18.4 + depends: ['certifi >=2017.4.17', 'chardet >=3.0.2,<3.1.0', 'idna >=2.5,<2.7', 'urllib3 >=1.21.1,<1.23'] + - name: requests + version: 2.19.1 + depends: ['certifi >=2017.4.17', 'chardet >=3.0.2,<3.1.0', 'idna >=2.5,<2.8', 'urllib3 >=1.21.1,<1.24'] + - scandir 1.5 + - scandir 1.6 + - scandir 1.7 + - scandir 1.8 + - scandir 1.9.0 + - name: setuptools + version: 36.2.2 + depends: ['certifi', 'wincertstore'] + - name: setuptools + version: 36.5.0 + depends: ['certifi', 'wincertstore'] + - name: setuptools + version: 38.4.0 + depends: ['certifi >=2016.09', 'wincertstore >=0.2'] + - name: setuptools + version: 38.5.1 + depends: ['certifi >=2016.09', 'wincertstore >=0.2'] + - name: setuptools + version: 39.0.1 + depends: ['certifi >=2016.09', 'wincertstore >=0.2'] + - name: setuptools + version: 39.1.0 + depends: ['certifi >=2016.09', 'wincertstore >=0.2'] + - name: setuptools + version: 39.2.0 + depends: ['certifi >=2016.09', 'wincertstore >=0.2'] + - name: setuptools + version: 40.0.0 + depends: ['certifi >=2016.09', 'wincertstore >=0.2'] + - six 1.10.0 + - six 1.11.0 + - toolz 0.8.2 + - toolz 0.9.0 + - name: urllib3 + version: '1.22' + depends: ['certifi', 'cryptography >=1.3.4', 'idna >=2.0.0', 'pyopenssl >=0.14', 'pysocks >=1.5.6,<2.0,!=1.5.7'] + - name: urllib3 + version: '1.23' + depends: ['certifi', 'cryptography >=1.3.4', 'idna >=2.0.0', 'pyopenssl >=0.14', 'pysocks >=1.5.6,<2.0,!=1.5.7'] + - webencodings 0.5.1 + - name: wheel + version: 0.29.0 + depends: ['setuptools'] + - name: wheel + version: 0.30.0 + depends: ['setuptools'] + - name: wheel + version: 0.31.0 + depends: ['setuptools'] + - name: wheel + version: 0.31.1 + depends: ['setuptools'] + - win_inet_pton 1.0.1 + - wincertstore 0.2 + +cases: +- + request: + - install: affine + response: + - state: + - affine 2.2.1 +- + request: + - install: cryptography + response: + - state: + - asn1crypto 0.24.0 + - cffi 1.11.5 + - cryptography 2.2.1 + - idna 2.7 + - pycparser 2.18 + - six 1.11.0 + skip: old From 7b4bfcb03bd24eaf4f98b09a0fc18cf550fa4780 Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Wed, 6 May 2020 14:46:24 -0500 Subject: [PATCH 2064/3170] work around bug in wheel generation for names which have "-" i them --- tests/yaml/large.yml | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/tests/yaml/large.yml b/tests/yaml/large.yml index a7e6d421c3d..89190b2fe1d 100644 --- a/tests/yaml/large.yml +++ b/tests/yaml/large.yml @@ -61,13 +61,13 @@ base: depends: ['asn1crypto >=0.21.0', 'cffi >=1.7', 'idna >=2.1', 'six >=1.4.1'] - name: cryptography version: '2.3' - depends: ['asn1crypto >=0.21.0', 'cffi >=1.7', 'cryptography-vectors ~=2.3', 'idna >=2.1', 'six >=1.4.1'] - - cryptography-vectors 2.0.3 - - cryptography-vectors 2.1.3 - - cryptography-vectors 2.1.4 - - cryptography-vectors 2.2.1 - - cryptography-vectors 2.2.2 - - cryptography-vectors 2.3 + depends: ['asn1crypto >=0.21.0', 'cffi >=1.7', 'cryptography_vectors ~=2.3', 'idna >=2.1', 'six >=1.4.1'] + - cryptography_vectors 2.0.3 + - cryptography_vectors 2.1.3 + - cryptography_vectors 2.1.4 + - cryptography_vectors 2.2.1 + - cryptography_vectors 2.2.2 + - cryptography_vectors 2.3.0 - name: cytoolz version: 0.8.2 depends: ['toolz >=0.8.0'] @@ -240,7 +240,8 @@ cases: - state: - asn1crypto 0.24.0 - cffi 1.11.5 - - cryptography 2.2.1 + - cryptography 2.3 + - cryptography_vectors 2.3.0 - idna 2.7 - pycparser 2.18 - six 1.11.0 From e8bc39274de2ce99b8e3266aa24631ade5fd46e9 Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Wed, 6 May 2020 15:12:29 -0500 Subject: [PATCH 2065/3170] add test case to install cachecontrol --- tests/yaml/large.yml | 35 ++++++++++++++++++++++++++++------- 1 file changed, 28 insertions(+), 7 deletions(-) diff --git a/tests/yaml/large.yml b/tests/yaml/large.yml index 89190b2fe1d..38b84f1e711 100644 --- a/tests/yaml/large.yml +++ b/tests/yaml/large.yml @@ -17,13 +17,13 @@ base: - beautifulsoup4 4.6.3 - name: cachecontrol version: 0.12.3 - depends: ['msgpack-python', 'requests'] + depends: ['msgpack_python', 'requests'] - name: cachecontrol version: 0.12.4 - depends: ['msgpack-python', 'requests'] + depends: ['msgpack_python', 'requests'] - name: cachecontrol version: 0.12.5 - depends: ['msgpack-python', 'requests'] + depends: ['msgpack_python', 'requests'] - certifi 2017.11.5 - certifi 2017.7.27.1 - certifi 2018.1.18 @@ -108,10 +108,10 @@ base: depends: ['markupsafe >=0.23', 'setuptools'] - lockfile 0.12.2 - markupsafe 1.0 - - msgpack-python 0.4.8 - - msgpack-python 0.5.1 - - msgpack-python 0.5.5 - - msgpack-python 0.5.6 + - msgpack_python 0.4.8 + - msgpack_python 0.5.1 + - msgpack_python 0.5.5 + - msgpack_python 0.5.6 - name: packaging version: '16.8' depends: ['pyparsing', 'six'] @@ -246,3 +246,24 @@ cases: - pycparser 2.18 - six 1.11.0 skip: old +- + request: + - install: cachecontrol + response: + - state: + - asn1crypto 0.24.0 + - cachecontrol 0.12.5 + - certifi 2018.8.13 + - cffi 1.11.5 + - chardet 3.0.4 + - cryptography 2.3 + - cryptography_vectors 2.3.0 + - idna 2.7 + - msgpack_python 0.5.6 + - pycparser 2.18 + - pyopenssl 18.0.0 + - pysocks 1.6.8 + - requests 2.19.1 + - six 1.11.0 + - urllib3 1.23 + - win_inet_pton 1.0.1 From ec288fec0e1afcb18fd47e38d378cc67f93e8c19 Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Wed, 6 May 2020 15:29:41 -0500 Subject: [PATCH 2066/3170] add comment to large.yml --- tests/yaml/large.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tests/yaml/large.yml b/tests/yaml/large.yml index 38b84f1e711..faff9bfca31 100644 --- a/tests/yaml/large.yml +++ b/tests/yaml/large.yml @@ -1,3 +1,5 @@ +# The 129 available packages have been obtained by transforming a +# conda repodata.json, and doing some manual fixes. base: available: - affine 2.2.0 From 2f884c9379bf3d0a30067ef7c32ec55e4c922731 Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Wed, 6 May 2020 15:34:30 -0500 Subject: [PATCH 2067/3170] disable debug output files --- tests/functional/test_yaml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/test_yaml.py b/tests/functional/test_yaml.py index 31a36ac8349..4a03381a31c 100644 --- a/tests/functional/test_yaml.py +++ b/tests/functional/test_yaml.py @@ -176,7 +176,7 @@ def test_yaml_based(script, case): request.get('options', '').split(), case[':resolver:'] == 'new') - if 1: # for analyzing output easier + if 0: # for analyzing output easier with open(DATA_DIR.parent / "yaml" / case[':name:'].replace('*', '-'), 'w') as fo: result = effect['result'] From b2895a5876039150d1afb52c27d3d004d7618ae5 Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Wed, 6 May 2020 22:41:38 -0500 Subject: [PATCH 2068/3170] add test cases to large.yml --- tests/yaml/large.yml | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/yaml/large.yml b/tests/yaml/large.yml index faff9bfca31..edabee3ca89 100644 --- a/tests/yaml/large.yml +++ b/tests/yaml/large.yml @@ -202,6 +202,7 @@ base: - name: setuptools version: 40.0.0 depends: ['certifi >=2016.09', 'wincertstore >=0.2'] + - six 1.8.2 - six 1.10.0 - six 1.11.0 - toolz 0.8.2 @@ -269,3 +270,21 @@ cases: - six 1.11.0 - urllib3 1.23 - win_inet_pton 1.0.1 +- + request: + - install: cytoolz + response: + - state: + - cytoolz 0.9.0.1 + - toolz 0.9.0 +- + request: + - install: ['html5lib', 'six ==1.8.2'] + response: + - state: null + skip: true + # -- the new resolver tells: + # Could not find a version that satisfies the requirement six==1.8.2 + # Could not find a version that satisfies the requirement six>=1.9 (from html5lib) + # -- the old error message (which I think was better): + # html5lib 1.0.1 has requirement six>=1.9, but you'll have six 1.8.2 which is incompatible. From 708b675ba7031b9a95fbd0d80d1b2bb629b73cb9 Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Wed, 6 May 2020 23:15:25 -0500 Subject: [PATCH 2069/3170] add test for unavailable version --- tests/yaml/README.md | 2 +- tests/yaml/conflict_1.yml | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/tests/yaml/README.md b/tests/yaml/README.md index b1c9bb56644..1a379fdcbb0 100644 --- a/tests/yaml/README.md +++ b/tests/yaml/README.md @@ -1,4 +1,4 @@ -# Fixtures +# YAML tests for pip's resolver This directory contains fixtures for testing pip's resolver. The fixtures are written as `.yml` files, with a convenient format diff --git a/tests/yaml/conflict_1.yml b/tests/yaml/conflict_1.yml index 589ee962dc1..8f8d6bdf0b0 100644 --- a/tests/yaml/conflict_1.yml +++ b/tests/yaml/conflict_1.yml @@ -21,7 +21,7 @@ cases: response: - state: null skip: true - # -- currently the error message is: + # -- currently the (new resolver) error message is: # Could not find a version that satisfies the requirement B==1.0.0 # Could not find a version that satisfies the requirement B==2.0.0 # No matching distribution found for b, b @@ -30,6 +30,18 @@ cases: # same time. # -- the old error message was actually better here: # Double requirement given: B==2.0.0 (already in B==1.0.0, name='B') +- + request: + - install: B==1.5.0 + response: + - state: null + skip: true + # -- currently (new resolver) error message is: + # Could not find a version that satisfies the requirement B==1.5.0 + # No matching distribution found for b + # -- the old error message was actually better here: + # Could not find a version that satisfies the requirement B==1.5.0 (from versions: 1.0.0, 2.0.0) + # No matching distribution found for B==1.5.0 - request: - install: A==2.0 From 4d17ec6442e315f6b762ef55bfbf64488985091e Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Thu, 7 May 2020 00:02:58 -0500 Subject: [PATCH 2070/3170] add errors draft --- tests/yaml/ERRORS.md | 60 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 60 insertions(+) create mode 100644 tests/yaml/ERRORS.md diff --git a/tests/yaml/ERRORS.md b/tests/yaml/ERRORS.md new file mode 100644 index 00000000000..700e3d4ea5d --- /dev/null +++ b/tests/yaml/ERRORS.md @@ -0,0 +1,60 @@ +# New resolver error messages + + +## Incompatible requirements + +Most resolver error messages are due to incompatible requirements. +That is, the dependency tree contains conflicting versions of the same +package. Take the example: + + base: + available: + - A 1.0.0; depends B == 1.0.0, C == 2.0.0 + - B 1.0.0; depends C == 1.0.0 + - C 1.0.0 + - C 2.0.0 + +Here, `A` cannot be installed because it depends on `B` (which depends on +a different version of `C` than `A` itself. In real world examples, the +conflicting version are not so easy to spot. I'm suggesting an error +message which looks something like this: + + A 1.0.0 -> B 1.0.0 -> C 1.0.0 + A 1.0.0 -> C 2.0.0 + +That is, for the conflicting package, we show the user where exactly the +requirement came from. + + +## Double requirement + +I've noticed that in many cases the old resolver messages are more +informative. For example, in the simple example: + + base: + available: + - B 1.0.0 + - B 2.0.0 + +Now if we want to install both version of `B` at the same time, +i.e. the requirement `B==1.0.0 B==2.0.0`, we get: + + ERROR: Could not find a version that satisfies the requirement B==1.0.0 + ERROR: Could not find a version that satisfies the requirement B==2.0.0 + No matching distribution found for b, b + +Even though both version are actually available and satisfy each requirement, +just not at once. When trying to install a version of `B` which does not +exist, say requirement `B==1.5.0`, you get the same type of error message: + + Could not find a version that satisfies the requirement B==1.5.0 + No matching distribution found for b + +For this case, the old error message was: + + Could not find a version that satisfies the requirement B==1.5.0 (from versions: 1.0.0, 2.0.0) + No matching distribution found for B==1.5.0 + +And the old error message for the requirement `B==1.0.0 B==2.0.0`: + + Double requirement given: B==2.0.0 (already in B==1.0.0, name='B') From 7dc345386a7b881900ff9ea0fb6b460b6b1c3a9b Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Thu, 14 May 2020 00:57:49 -0500 Subject: [PATCH 2071/3170] add huge yaml test (579 packages) going to add more test cases --- tests/yaml/huge.yml | 1237 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 1237 insertions(+) create mode 100644 tests/yaml/huge.yml diff --git a/tests/yaml/huge.yml b/tests/yaml/huge.yml new file mode 100644 index 00000000000..787ef522511 --- /dev/null +++ b/tests/yaml/huge.yml @@ -0,0 +1,1237 @@ +base: + available: + - alabaster 0.7.10 + - alabaster 0.7.11 + - appdirs 1.4.3 + - asn1crypto 0.22.0 + - asn1crypto 0.23.0 + - asn1crypto 0.24.0 + - name: astroid + version: 1.5.3 + depends: ['lazy-object-proxy', 'setuptools', 'six', 'wrapt'] + - name: astroid + version: 1.6.0 + depends: ['lazy-object-proxy', 'setuptools', 'six', 'wrapt'] + - name: astroid + version: 1.6.1 + depends: ['lazy-object-proxy', 'setuptools', 'six', 'wrapt'] + - name: astroid + version: 1.6.2 + depends: ['lazy-object-proxy', 'setuptools', 'six', 'wrapt'] + - name: astroid + version: 1.6.3 + depends: ['lazy-object-proxy', 'setuptools', 'six', 'wrapt'] + - name: astroid + version: 1.6.4 + depends: ['lazy-object-proxy', 'setuptools', 'six', 'wrapt'] + - name: astroid + version: 1.6.5 + depends: ['lazy-object-proxy', 'setuptools', 'six', 'wrapt'] + - name: astroid + version: 2.0.2 + depends: ['lazy-object-proxy', 'six', 'wrapt'] + - name: astroid + version: 2.0.4 + depends: ['lazy-object-proxy', 'six', 'wrapt'] + - name: attrs + version: 17.2.0 + depends: ['hypothesis', 'pympler', 'zope', 'zope.interface'] + - name: attrs + version: 17.3.0 + depends: ['hypothesis', 'pympler', 'zope', 'zope.interface'] + - attrs 17.4.0 + - attrs 18.1.0 + - name: automat + version: 0.6.0 + depends: ['attrs', 'six'] + - name: automat + version: 0.7.0 + depends: ['attrs', 'six'] + - name: babel + version: 2.5.0 + depends: ['pytz'] + - name: babel + version: 2.5.1 + depends: ['pytz'] + - name: babel + version: 2.5.3 + depends: ['pytz'] + - name: babel + version: 2.6.0 + depends: ['pytz'] + - backcall 0.1.0 + - backports 1.0 + - name: backports.functools_lru_cache + version: '1.4' + depends: ['backports', 'setuptools'] + - name: backports.functools_lru_cache + version: '1.5' + depends: ['backports', 'setuptools'] + - name: backports.shutil_get_terminal_size + version: 1.0.0 + depends: ['backports'] + - backports_abc 0.5 + - beautifulsoup4 4.6.0 + - beautifulsoup4 4.6.1 + - beautifulsoup4 4.6.3 + - bitarray 0.8.1 + - bitarray 0.8.2 + - bitarray 0.8.3 + - name: bkcharts + version: '0.2' + depends: ['numpy >=1.7.1', 'pandas', 'six >=1.5.2'] + - name: bleach + version: 2.0.0 + depends: ['html5lib >=0.99999999', 'six'] + - name: bleach + version: 2.1.1 + depends: ['html5lib >=0.99999999', 'setuptools', 'six'] + - name: bleach + version: 2.1.2 + depends: ['html5lib >=0.99999999', 'setuptools', 'six'] + - name: bleach + version: 2.1.3 + depends: ['html5lib >=0.99999999', 'setuptools', 'six'] + - name: bokeh + version: 0.12.10 + depends: ['jinja2 >=2.7', 'numpy >=1.7.1', 'python-dateutil >=2.1', 'pyyaml >=3.10', 'six >=1.5.2', 'tornado >=4.3'] + - name: bokeh + version: 0.12.11 + depends: ['jinja2 >=2.7', 'numpy >=1.7.1', 'python-dateutil >=2.1', 'pyyaml >=3.10', 'six >=1.5.2', 'tornado >=4.3'] + - name: bokeh + version: 0.12.13 + depends: ['jinja2 >=2.7', 'numpy >=1.7.1', 'python-dateutil >=2.1', 'pyyaml >=3.10', 'six >=1.5.2', 'tornado >=4.3'] + - name: bokeh + version: 0.12.14 + depends: ['jinja2 >=2.7', 'numpy >=1.7.1', 'packaging >=16.8', 'python-dateutil >=2.1', 'pyyaml >=3.10', 'six >=1.5.2', 'tornado >=4.3'] + - name: bokeh + version: 0.12.15 + depends: ['jinja2 >=2.7', 'numpy >=1.7.1', 'packaging >=16.8', 'python-dateutil >=2.1', 'pyyaml >=3.10', 'six >=1.5.2', 'tornado >=4.3'] + - name: bokeh + version: 0.12.16 + depends: ['jinja2 >=2.7', 'numpy >=1.7.1', 'packaging >=16.8', 'python-dateutil >=2.1', 'pyyaml >=3.10', 'six >=1.5.2', 'tornado >=4.3'] + - name: bokeh + version: 0.12.7 + depends: ['bkcharts >=0.2', 'jinja2 >=2.7', 'matplotlib', 'numpy >=1.7.1', 'pandas', 'python-dateutil >=2.1', 'pyyaml >=3.10', 'requests >=1.2.3', 'six >=1.5.2', 'tornado >=4.3'] + - name: bokeh + version: 0.12.9 + depends: ['jinja2 >=2.7', 'numpy >=1.7.1', 'python-dateutil >=2.1', 'pyyaml >=3.10', 'six >=1.5.2', 'tornado >=4.3'] + - name: bokeh + version: 0.13.0 + depends: ['jinja2 >=2.7', 'numpy >=1.7.1', 'packaging >=16.8', 'python-dateutil >=2.1', 'pyyaml >=3.10', 'six >=1.5.2', 'tornado >=4.3'] + - name: boto3 + version: 1.4.7 + depends: ['botocore >=1.7.0,<1.8.0', 'jmespath >=0.7.1,<1.0.0', 's3transfer >=0.1.10,<0.2.0'] + - name: boto3 + version: 1.4.8 + depends: ['botocore >=1.8.0,<1.9.0', 'jmespath >=0.7.1,<1.0.0', 's3transfer >=0.1.10,<0.2.0'] + - name: boto3 + version: 1.5.32 + depends: ['botocore >=1.8.46,<1.9.0', 'jmespath >=0.7.1,<1.0.0', 's3transfer >=0.1.10,<0.2.0'] + - name: boto3 + version: 1.6.18 + depends: ['botocore >=1.9.18,<1.10.0', 'jmespath >=0.7.1,<1.0.0', 's3transfer >=0.1.10,<0.2.0'] + - name: boto3 + version: 1.7.24 + depends: ['botocore >=1.10.24,<1.11.0', 'jmespath >=0.7.1,<1.0.0', 's3transfer >=0.1.10,<0.2.0'] + - name: boto3 + version: 1.7.32 + depends: ['botocore >=1.10.32,<1.11.0', 'jmespath >=0.7.1,<1.0.0', 's3transfer >=0.1.10,<0.2.0'] + - name: boto3 + version: 1.7.4 + depends: ['botocore >=1.10.4,<1.11.0', 'jmespath >=0.7.1,<1.0.0', 's3transfer >=0.1.10,<0.2.0'] + - name: boto3 + version: 1.7.45 + depends: ['botocore >=1.10.45,<1.11.0', 'jmespath >=0.7.1,<1.0.0', 's3transfer >=0.1.10,<0.2.0'] + - name: boto3 + version: 1.7.62 + depends: ['botocore >=1.10.62,<1.11.0', 'jmespath >=0.7.1,<1.0.0', 's3transfer >=0.1.10,<0.2.0'] + - name: botocore + version: 1.10.12 + depends: ['docutils >=0.10', 'jmespath >=0.7.1,<1.0.0', 'python-dateutil >=2.1,<3.0.0'] + - name: botocore + version: 1.10.24 + depends: ['docutils >=0.10', 'jmespath >=0.7.1,<1.0.0', 'python-dateutil >=2.1,<3.0.0'] + - name: botocore + version: 1.10.32 + depends: ['docutils >=0.10', 'jmespath >=0.7.1,<1.0.0', 'python-dateutil >=2.1,<3.0.0'] + - name: botocore + version: 1.10.4 + depends: ['docutils >=0.10', 'jmespath >=0.7.1,<1.0.0', 'python-dateutil >=2.1,<2.7.0'] + - name: botocore + version: 1.10.45 + depends: ['docutils >=0.10', 'jmespath >=0.7.1,<1.0.0', 'python-dateutil >=2.1,<3.0.0'] + - name: botocore + version: 1.10.62 + depends: ['docutils >=0.10', 'jmespath >=0.7.1,<1.0.0', 'python-dateutil >=2.1,<3.0.0'] + - name: botocore + version: 1.5.78 + depends: ['docutils >=0.10', 'jmespath >=0.7.1,<1.0.0', 'python-dateutil >=2.1,<3.0.0'] + - name: botocore + version: 1.7.14 + depends: ['docutils >=0.10', 'jmespath >=0.7.1,<1.0.0', 'python-dateutil >=2.1,<3.0.0'] + - name: botocore + version: 1.7.20 + depends: ['docutils >=0.10', 'jmespath >=0.7.1,<1.0.0', 'python-dateutil >=2.1,<3.0.0'] + - name: botocore + version: 1.7.40 + depends: ['docutils >=0.10', 'jmespath >=0.7.1,<1.0.0', 'python-dateutil >=2.1,<3.0.0'] + - name: botocore + version: 1.7.5 + depends: ['docutils >=0.10', 'jmespath >=0.7.1,<1.0.0', 'python-dateutil >=2.1,<3.0.0'] + - name: botocore + version: 1.8.21 + depends: ['docutils >=0.10', 'jmespath >=0.7.1,<1.0.0', 'python-dateutil >=2.1,<3.0.0'] + - name: botocore + version: 1.8.46 + depends: ['docutils >=0.10', 'jmespath >=0.7.1,<1.0.0', 'python-dateutil >=2.1,<3.0.0'] + - name: botocore + version: 1.8.5 + depends: ['docutils >=0.10', 'jmespath >=0.7.1,<1.0.0', 'python-dateutil >=2.1,<3.0.0'] + - name: botocore + version: 1.9.18 + depends: ['docutils >=0.10', 'jmespath >=0.7.1,<1.0.0', 'python-dateutil >=2.1,<2.7.0'] + - certifi 2017.11.5 + - certifi 2017.7.27.1 + - certifi 2018.1.18 + - certifi 2018.4.16 + - certifi 2018.8.13 + - name: cffi + version: 1.10.0 + depends: ['pycparser'] + - name: cffi + version: 1.11.2 + depends: ['pycparser'] + - name: cffi + version: 1.11.4 + depends: ['pycparser'] + - name: cffi + version: 1.11.5 + depends: ['pycparser'] + - chardet 3.0.4 + - click 6.7 + - cloudpickle 0.4.0 + - cloudpickle 0.4.2 + - cloudpickle 0.5.2 + - cloudpickle 0.5.3 + - colorama 0.3.9 + - configparser 3.5.0 + - constantly 15.1.0 + - contextlib2 0.5.5 + - coverage 4.4.2 + - coverage 4.5.1 + - name: cryptography + version: 2.0.3 + depends: ['asn1crypto >=0.21.0', 'cffi >=1.7', 'idna >=2.1', 'openssl 1.0.*', 'six >=1.4.1'] + - name: cryptography + version: 2.1.3 + depends: ['asn1crypto >=0.21.0', 'cffi >=1.7', 'idna >=2.1', 'openssl 1.0.*', 'openssl >=1.0.2m,<1.0.3a', 'six >=1.4.1'] + - name: cryptography + version: 2.1.4 + depends: ['asn1crypto >=0.21.0', 'cffi >=1.7', 'idna >=2.1', 'openssl 1.0.*', 'openssl >=1.0.2m,<1.0.3a', 'six >=1.4.1'] + - name: cryptography + version: 2.2.1 + depends: ['asn1crypto >=0.21.0', 'cffi >=1.7', 'idna >=2.1', 'openssl 1.0.*', 'openssl >=1.0.2n,<1.0.3a', 'six >=1.4.1'] + - name: cryptography + version: 2.2.2 + depends: ['asn1crypto >=0.21.0', 'cffi >=1.7', 'idna >=2.1', 'openssl 1.0.*', 'openssl >=1.0.2o,<1.0.3a', 'six >=1.4.1'] + - name: cryptography + version: '2.3' + depends: ['asn1crypto >=0.21.0', 'cffi >=1.7', 'cryptography-vectors 2.3.*', 'idna >=2.1', 'openssl >=1.0.2o,<1.0.3a', 'six >=1.4.1'] + - cryptography-vectors 2.0.3 + - cryptography-vectors 2.1.3 + - cryptography-vectors 2.1.4 + - cryptography-vectors 2.2.1 + - cryptography-vectors 2.2.2 + - cryptography-vectors 2.3 + - name: cycler + version: 0.10.0 + depends: ['six'] + - name: cytoolz + version: 0.8.2 + depends: ['toolz >=0.8.0'] + - name: cytoolz + version: 0.9.0 + depends: ['toolz >=0.8.0'] + - name: cytoolz + version: 0.9.0.1 + depends: ['toolz >=0.8.0'] + - name: dask + version: 0.15.2 + depends: ['bokeh', 'cloudpickle >=0.2.1', 'dask-core 0.15.2.*', 'distributed >=1.16.0', 'numpy >=1.10', 'pandas >=0.19.0', 'partd >=0.3.8', 'toolz >=0.7.3'] + - name: dask + version: 0.15.3 + depends: ['bokeh', 'cloudpickle >=0.2.1', 'dask-core 0.15.3.*', 'distributed >=1.19.0', 'numpy >=1.10', 'pandas >=0.19.0', 'partd >=0.3.8', 'toolz >=0.7.3'] + - name: dask + version: 0.15.4 + depends: ['bokeh', 'cloudpickle >=0.2.1', 'dask-core 0.15.4.*', 'distributed >=1.19.0', 'numpy >=1.10', 'pandas >=0.19.0', 'partd >=0.3.8', 'toolz >=0.7.3'] + - name: dask + version: 0.16.0 + depends: ['bokeh', 'cloudpickle >=0.2.1', 'dask-core 0.16.0.*', 'distributed >=1.20.0', 'numpy >=1.10', 'pandas >=0.19.0', 'partd >=0.3.8', 'toolz >=0.7.3'] + - name: dask + version: 0.16.1 + depends: ['bokeh', 'cloudpickle >=0.2.1', 'dask-core 0.16.1.*', 'distributed >=1.20.0', 'numpy >=1.10', 'pandas >=0.19.0', 'partd >=0.3.8', 'toolz >=0.7.3'] + - name: dask + version: 0.17.0 + depends: ['bokeh', 'cloudpickle >=0.2.1', 'dask-core 0.17.0.*', 'distributed >=1.21.0', 'numpy >=1.10', 'pandas >=0.19.0', 'partd >=0.3.8', 'toolz >=0.7.3'] + - name: dask + version: 0.17.1 + depends: ['bokeh', 'cloudpickle >=0.2.1', 'dask-core 0.17.1.*', 'distributed >=1.21.1', 'numpy >=1.10', 'pandas >=0.19.0', 'partd >=0.3.8', 'toolz >=0.7.3'] + - name: dask + version: 0.17.2 + depends: ['bokeh', 'cloudpickle >=0.2.1', 'cytoolz >=0.7.3', 'dask-core 0.17.2.*', 'distributed >=1.21.0', 'numpy >=1.10.4', 'pandas >=0.19.0', 'partd >=0.3.8', 'toolz >=0.7.3'] + - name: dask + version: 0.17.3 + depends: ['bokeh', 'cloudpickle >=0.2.1', 'cytoolz >=0.7.3', 'dask-core 0.17.3.*', 'distributed >=1.21.0', 'numpy >=1.11.0', 'pandas >=0.19.0', 'partd >=0.3.8', 'toolz >=0.7.3'] + - name: dask + version: 0.17.4 + depends: ['bokeh', 'cloudpickle >=0.2.1', 'cytoolz >=0.7.3', 'dask-core 0.17.4.*', 'distributed >=1.21.0', 'numpy >=1.11.0', 'pandas >=0.19.0', 'partd >=0.3.8', 'toolz >=0.7.3'] + - name: dask + version: 0.17.5 + depends: ['bokeh', 'cloudpickle >=0.2.1', 'cytoolz >=0.7.3', 'dask-core 0.17.5.*', 'distributed >=1.21.0', 'numpy >=1.11.0', 'pandas >=0.19.0', 'partd >=0.3.8', 'toolz >=0.7.3'] + - name: dask + version: 0.18.0 + depends: ['bokeh', 'cloudpickle >=0.2.1', 'cytoolz >=0.7.3', 'dask-core 0.18.0.*', 'distributed >=1.22.0', 'numpy >=1.11.0', 'pandas >=0.19.0', 'partd >=0.3.8', 'toolz >=0.7.3'] + - name: dask + version: 0.18.1 + depends: ['bokeh', 'cloudpickle >=0.2.1', 'cytoolz >=0.7.3', 'dask-core 0.18.1.*', 'distributed >=1.22.0', 'numpy >=1.11.0', 'pandas >=0.19.0', 'partd >=0.3.8', 'toolz >=0.7.3'] + - name: dask + version: 0.18.2 + depends: ['bokeh', 'cloudpickle >=0.2.1', 'cytoolz >=0.7.3', 'dask-core 0.18.2.*', 'distributed >=1.22.0', 'numpy >=1.11.0', 'pandas >=0.19.0', 'partd >=0.3.8', 'toolz >=0.7.3'] + - dask-core 0.15.2 + - dask-core 0.15.3 + - dask-core 0.15.4 + - dask-core 0.16.0 + - dask-core 0.16.1 + - dask-core 0.17.0 + - dask-core 0.17.1 + - dask-core 0.17.2 + - dask-core 0.17.3 + - dask-core 0.17.4 + - dask-core 0.17.5 + - dask-core 0.18.0 + - dask-core 0.18.1 + - dask-core 0.18.2 + - decorator 4.1.2 + - decorator 4.2.1 + - decorator 4.3.0 + - dill 0.2.7.1 + - dill 0.2.8.2 + - name: distributed + version: 1.18.3 + depends: ['click >=6.6', 'cloudpickle >=0.2.2', 'dask-core >=0.15.2', 'msgpack-python', 'psutil', 'six', 'sortedcontainers', 'tblib', 'toolz >=0.7.4', 'tornado >=4.5.1', 'zict >=0.1.2'] + - name: distributed + version: 1.19.1 + depends: ['click >=6.6', 'cloudpickle >=0.2.2', 'dask-core >=0.15.2', 'msgpack-python', 'psutil', 'six', 'sortedcontainers', 'tblib', 'toolz >=0.7.4', 'tornado >=4.5.1', 'zict >=0.1.3'] + - name: distributed + version: 1.20.0 + depends: ['click >=6.6', 'cloudpickle >=0.2.2', 'dask-core >=0.16.0', 'msgpack-python', 'psutil', 'pyyaml', 'six', 'sortedcontainers', 'tblib', 'toolz >=0.7.4', 'tornado >=4.5.1', 'zict >=0.1.3'] + - name: distributed + version: 1.20.1 + depends: ['click >=6.6', 'cloudpickle >=0.2.2', 'dask-core >=0.16.0', 'msgpack-python', 'psutil', 'pyyaml', 'six', 'sortedcontainers', 'tblib', 'toolz >=0.7.4', 'tornado >=4.5.1', 'zict >=0.1.3'] + - name: distributed + version: 1.20.2 + depends: ['click >=6.6', 'cloudpickle >=0.2.2', 'dask-core >=0.16.0', 'msgpack-python', 'psutil', 'pyyaml', 'six', 'sortedcontainers', 'tblib', 'toolz >=0.7.4', 'tornado >=4.5.1', 'zict >=0.1.3'] + - name: distributed + version: 1.21.0 + depends: ['click >=6.6', 'cloudpickle >=0.2.2', 'dask-core >=0.17.0', 'msgpack-python', 'psutil', 'pyyaml', 'six', 'sortedcontainers', 'tblib', 'toolz >=0.7.4', 'tornado >=4.5.1', 'zict >=0.1.3'] + - name: distributed + version: 1.21.1 + depends: ['click >=6.6', 'cloudpickle >=0.2.2', 'dask-core >=0.17.0', 'msgpack-python', 'psutil', 'pyyaml', 'six', 'sortedcontainers', 'tblib', 'toolz >=0.7.4', 'tornado >=4.5.1', 'zict >=0.1.3'] + - name: distributed + version: 1.21.2 + depends: ['click >=6.6', 'cloudpickle >=0.2.2', 'dask-core >=0.17.0', 'msgpack-python', 'psutil', 'pyyaml', 'six', 'sortedcontainers', 'tblib', 'toolz >=0.7.4', 'tornado >=4.5.1', 'zict >=0.1.3'] + - name: distributed + version: 1.21.3 + depends: ['click >=6.6', 'cloudpickle >=0.2.2', 'cytoolz >=0.7.4', 'dask-core >=0.17.0', 'msgpack-python', 'psutil', 'pyyaml', 'six', 'sortedcontainers', 'tblib', 'toolz >=0.7.4', 'tornado >=4.5.1', 'zict >=0.1.3'] + - name: distributed + version: 1.21.4 + depends: ['click >=6.6', 'cloudpickle >=0.2.2', 'cytoolz >=0.7.4', 'dask-core >=0.17.0', 'msgpack-python', 'psutil', 'pyyaml', 'six', 'sortedcontainers', 'tblib', 'toolz >=0.7.4', 'tornado >=4.5.1', 'zict >=0.1.3'] + - name: distributed + version: 1.21.5 + depends: ['click >=6.6', 'cloudpickle >=0.2.2', 'cytoolz >=0.7.4', 'dask-core >=0.17.0', 'msgpack-python', 'psutil', 'pyyaml', 'six', 'sortedcontainers', 'tblib', 'toolz >=0.7.4', 'tornado >=4.5.1', 'zict >=0.1.3'] + - name: distributed + version: 1.21.6 + depends: ['click >=6.6', 'cloudpickle >=0.2.2', 'cytoolz >=0.7.4', 'dask-core >=0.17.0', 'msgpack-python', 'psutil', 'pyyaml', 'six', 'sortedcontainers', 'tblib', 'toolz >=0.7.4', 'tornado >=4.5.1', 'zict >=0.1.3'] + - name: distributed + version: 1.21.8 + depends: ['click >=6.6', 'cloudpickle >=0.2.2', 'cytoolz >=0.7.4', 'dask-core >=0.17.0', 'msgpack-python', 'psutil', 'pyyaml', 'six', 'sortedcontainers', 'tblib', 'toolz >=0.7.4', 'tornado >=4.5.1', 'zict >=0.1.3'] + - name: distributed + version: 1.22.0 + depends: ['click >=6.6', 'cloudpickle >=0.2.2', 'cytoolz >=0.7.4', 'dask-core >=0.18.0', 'msgpack-python', 'psutil', 'pyyaml', 'six', 'sortedcontainers', 'tblib', 'toolz >=0.7.4', 'tornado >=4.5.1', 'zict >=0.1.3'] + - name: distributed + version: 1.22.1 + depends: ['click >=6.6', 'cloudpickle >=0.2.2', 'cytoolz >=0.7.4', 'dask-core >=0.18.0', 'msgpack-python', 'psutil', 'pyyaml', 'six', 'sortedcontainers', 'tblib', 'toolz >=0.7.4', 'tornado >=4.5.1', 'zict >=0.1.3'] + - docutils 0.14 + - entrypoints 0.2.3 + - enum34 1.1.6 + - expat 2.2.4 + - expat 2.2.5 + - filelock 2.0.12 + - filelock 2.0.13 + - filelock 3.0.4 + - name: flask + version: 0.12.2 + depends: ['click >=2.0', 'itsdangerous >=0.21', 'jinja2 >=2.4', 'werkzeug >=0.7'] + - name: flask + version: 1.0.2 + depends: ['click >=5.1', 'itsdangerous >=0.24', 'jinja2 >=2.10', 'werkzeug >=0.14'] + - fribidi 1.0.2 + - fribidi 1.0.4 + - funcsigs 1.0.2 + - functools32 3.2.3.2 + - future 0.16.0 + - futures 3.1.1 + - futures 3.2.0 + - name: gevent + version: 1.2.2 + depends: ['cffi >=1.3.0', 'greenlet >=0.4.10'] + - name: gevent + version: 1.3.0 + depends: ['cffi >=1.11.5', 'greenlet >=0.4.10'] + - name: gevent + version: 1.3.2.post0 + depends: ['cffi >=1.11.5', 'greenlet >=0.4.13'] + - name: gevent + version: 1.3.3 + depends: ['cffi >=1.11.5', 'greenlet >=0.4.13'] + - name: gevent + version: 1.3.4 + depends: ['cffi >=1.11.5', 'greenlet >=0.4.13'] + - name: gevent + version: 1.3.5 + depends: ['cffi >=1.11.5', 'greenlet >=0.4.13'] + - glob2 0.5 + - glob2 0.6 + - gmp 6.1.2 + - graphite2 1.3.10 + - graphite2 1.3.11 + - greenlet 0.4.12 + - greenlet 0.4.13 + - greenlet 0.4.14 + - name: html5lib + version: '0.999999999' + depends: ['six >=1.9', 'webencodings'] + - name: html5lib + version: 1.0.1 + depends: ['six >=1.9', 'webencodings'] + - name: hyperlink + version: 18.0.0 + depends: ['idna >=2.5'] + - hypothesis 3.23.0 + - name: hypothesis + version: 3.37.0 + depends: ['attrs', 'coverage'] + - name: hypothesis + version: 3.38.5 + depends: ['attrs', 'coverage'] + - name: hypothesis + version: 3.46.0 + depends: ['attrs', 'coverage'] + - name: hypothesis + version: 3.52.0 + depends: ['attrs >=16.0.0', 'coverage'] + - name: hypothesis + version: 3.53.0 + depends: ['attrs >=16.0.0', 'coverage'] + - name: hypothesis + version: 3.56.0 + depends: ['attrs >=16.0.0', 'coverage'] + - name: hypothesis + version: 3.57.0 + depends: ['attrs >=16.0.0', 'coverage'] + - name: hypothesis + version: 3.59.1 + depends: ['attrs >=16.0.0', 'coverage'] + - name: ibis-framework + version: 0.12.0 + depends: ['impyla >=0.14.0', 'multipledispatch', 'numpy >=1.10.0', 'pandas >=0.18.1', 'psycopg2', 'python-graphviz', 'setuptools', 'six', 'sqlalchemy >=1.0.0', 'thrift', 'thriftpy <=0.3.9', 'toolz'] + - name: ibis-framework + version: 0.13.0 + depends: ['impyla >=0.14.0', 'multipledispatch', 'numpy >=1.10.0', 'pandas >=0.18.1', 'psycopg2', 'python-graphviz', 'setuptools', 'six', 'sqlalchemy >=1.0.0', 'thrift', 'thriftpy <=0.3.9', 'toolz'] + - icu 58.2 + - idna 2.6 + - idna 2.7 + - imagesize 0.7.1 + - imagesize 1.0.0 + - name: impyla + version: 0.14.0 + depends: ['bitarray', 'setuptools', 'six', 'thriftpy >=0.3.5'] + - name: impyla + version: 0.14.1 + depends: ['bitarray', 'setuptools', 'six', 'thriftpy >=0.3.5'] + - incremental 17.5.0 + - ipaddress 1.0.18 + - ipaddress 1.0.19 + - ipaddress 1.0.22 + - name: ipykernel + version: 4.6.1 + depends: ['ipython', 'jupyter_client', 'tornado >=4.0', 'traitlets >=4.1'] + - name: ipykernel + version: 4.7.0 + depends: ['ipython', 'jupyter_client', 'tornado >=4.0', 'traitlets >=4.1'] + - name: ipykernel + version: 4.8.0 + depends: ['ipython >=4.0.0', 'jupyter_client', 'tornado >=4.0', 'traitlets >=4.1'] + - name: ipykernel + version: 4.8.2 + depends: ['ipython >=4.0.0', 'jupyter_client', 'tornado >=4.0', 'traitlets >=4.1'] + - name: ipython + version: 5.4.1 + depends: ['decorator', 'pexpect', 'pickleshare', 'prompt_toolkit >=1.0.4,<2.0.0', 'pygments', 'simplegeneric >0.8', 'traitlets'] + - name: ipython + version: 5.5.0 + depends: ['decorator', 'pexpect', 'pickleshare', 'prompt_toolkit >=1.0.4,<2.0.0', 'pygments', 'simplegeneric >0.8', 'traitlets'] + - name: ipython + version: 5.6.0 + depends: ['decorator', 'pexpect', 'pickleshare', 'prompt_toolkit >=1.0.4,<2.0.0', 'pygments', 'simplegeneric >0.8', 'traitlets'] + - name: ipython + version: 5.7.0 + depends: ['backports.shutil_get_terminal_size', 'decorator', 'pathlib2', 'pexpect', 'pickleshare', 'prompt_toolkit >=1.0.4,<2.0.0', 'pygments', 'simplegeneric >0.8', 'traitlets'] + - name: ipython + version: 5.8.0 + depends: ['decorator', 'pexpect', 'pickleshare', 'prompt_toolkit >=1.0.4,<2.0.0', 'pygments', 'simplegeneric >0.8', 'traitlets'] + - name: ipython + version: 6.1.0 + depends: ['decorator', 'jedi >=0.10', 'pexpect', 'pickleshare', 'prompt_toolkit >=1.0.4,<2.0.0', 'pygments', 'simplegeneric >0.8', 'traitlets'] + - name: ipython + version: 6.2.1 + depends: ['decorator', 'jedi >=0.10', 'pexpect', 'pickleshare', 'prompt_toolkit >=1.0.4,<2.0.0', 'pygments', 'simplegeneric >0.8', 'traitlets'] + - name: ipython + version: 6.3.0 + depends: ['backcall', 'decorator', 'jedi >=0.10', 'pexpect', 'pickleshare', 'prompt_toolkit >=1.0.4,<2.0.0', 'pygments', 'simplegeneric >0.8', 'traitlets >=4.2'] + - name: ipython + version: 6.3.1 + depends: ['backcall', 'decorator', 'jedi >=0.10', 'pexpect', 'pickleshare', 'prompt_toolkit >=1.0.4,<2.0.0', 'pygments', 'simplegeneric >0.8', 'traitlets >=4.2'] + - name: ipython + version: 6.4.0 + depends: ['backcall', 'decorator', 'jedi >=0.10', 'pexpect', 'pickleshare', 'prompt_toolkit >=1.0.4,<2.0.0', 'pygments', 'simplegeneric >0.8', 'traitlets >=4.2'] + - name: ipython + version: 6.5.0 + depends: ['backcall', 'decorator', 'jedi >=0.10', 'pexpect', 'pickleshare', 'prompt_toolkit >=1.0.4,<2.0.0', 'pygments', 'simplegeneric >0.8', 'traitlets >=4.2'] + - name: ipython-notebook + version: 0.13.2 + depends: ['ipython 0.13.2', 'pyzmq 2.2.0.1', 'tornado'] + - name: ipython-notebook + version: 1.0.0 + depends: ['ipython 1.0.0', 'pyzmq 2.2.0.1', 'tornado'] + - name: ipython-notebook + version: 1.1.0 + depends: ['ipython 1.1.0', 'jinja2', 'pyzmq 2.2.0.1', 'tornado'] + - name: ipython-notebook + version: 2.0.0 + depends: ['ipython 2.0.0', 'jinja2', 'pyzmq 14.*', 'tornado'] + - name: ipython-notebook + version: 2.1.0 + depends: ['ipython 2.1.0', 'jinja2', 'pyzmq 14.*', 'tornado'] + - name: ipython-notebook + version: 2.2.0 + depends: ['ipython 2.2.0', 'jinja2', 'pyzmq 14.*', 'tornado'] + - name: ipython-notebook + version: 2.3.0 + depends: ['ipython 2.3.0', 'jinja2', 'pyzmq 14.*', 'tornado'] + - name: ipython-notebook + version: 2.3.1 + depends: ['ipython 2.3.1', 'jinja2', 'pyzmq 14.*', 'tornado'] + - name: ipython-notebook + version: 2.4.1 + depends: ['ipython 2.4.1', 'jinja2', 'pyzmq 14.*', 'tornado'] + - name: ipython-notebook + version: 3.0.0 + depends: ['ipython 3.0.0', 'jinja2', 'jsonschema 2.4.0', 'mistune', 'pygments', 'pyzmq 14.*', 'terminado 0.5', 'tornado'] + - name: ipython-notebook + version: 3.1.0 + depends: ['ipython 3.1.0', 'jinja2', 'jsonschema 2.4.0', 'mistune', 'pygments', 'pyzmq 14.*', 'terminado 0.5', 'tornado'] + - name: ipython-notebook + version: 3.2.0 + depends: ['ipython 3.2.0', 'jinja2', 'jsonschema 2.4.0', 'mistune', 'pygments', 'pyzmq 14.*', 'terminado 0.5', 'tornado'] + - name: ipython-notebook + version: 3.2.1 + depends: ['ipython 3.2.1', 'jinja2', 'jsonschema 2.4.0', 'mistune', 'pygments', 'pyzmq 14.*', 'terminado 0.5', 'tornado'] + - name: ipython-notebook + version: 4.0.4 + depends: ['notebook'] + - ipython_genutils 0.2.0 + - name: ipywidgets + version: 7.0.0 + depends: ['ipykernel >=4.5.1', 'ipython', 'nbformat >=4.2.0', 'traitlets >=4.3.1', 'widgetsnbextension >=3.0.0'] + - name: ipywidgets + version: 7.0.5 + depends: ['ipykernel >=4.5.1', 'ipython', 'nbformat >=4.2.0', 'traitlets >=4.3.1', 'widgetsnbextension >=3.0.0'] + - name: ipywidgets + version: 7.1.0 + depends: ['ipykernel >=4.5.1', 'ipython', 'nbformat >=4.2.0', 'traitlets >=4.3.1', 'widgetsnbextension >=3.0.0'] + - name: ipywidgets + version: 7.1.1 + depends: ['ipykernel >=4.5.1', 'ipython >=4.0.0', 'nbformat >=4.2.0', 'traitlets >=4.3.1,<5.0.0', 'widgetsnbextension >=3.1.0,<4.0'] + - name: ipywidgets + version: 7.1.2 + depends: ['ipykernel >=4.5.1', 'ipython >=4.0.0', 'nbformat >=4.2.0', 'traitlets >=4.3.1,<5.0.0', 'widgetsnbextension >=3.1.0,<4.0'] + - name: ipywidgets + version: 7.2.0 + depends: ['ipykernel >=4.5.1', 'ipython >=4.0.0', 'nbformat >=4.2.0', 'traitlets >=4.3.1,<5.0.0', 'widgetsnbextension >=3.2.0,<4.0.0'] + - name: ipywidgets + version: 7.2.1 + depends: ['ipykernel >=4.5.1', 'ipython >=4.0.0', 'nbformat >=4.2.0', 'traitlets >=4.3.1,<5.0.0', 'widgetsnbextension >=3.2.0,<4.0.0'] + - name: ipywidgets + version: 7.3.0 + depends: ['ipykernel >=4.5.1', 'ipython >=4.0.0', 'nbformat >=4.2.0', 'traitlets >=4.3.1,<5.0.0', 'widgetsnbextension >=3.3.0,<3.4.0'] + - name: ipywidgets + version: 7.3.1 + depends: ['ipykernel >=4.5.1', 'ipython >=4.0.0', 'nbformat >=4.2.0', 'traitlets >=4.3.1,<5.0.0', 'widgetsnbextension >=3.3.0,<3.4.0'] + - name: ipywidgets + version: 7.4.0 + depends: ['ipykernel >=4.5.1', 'ipython >=4.0.0', 'nbformat >=4.2.0', 'traitlets >=4.3.1,<5.0.0', 'widgetsnbextension >=3.4.0,<3.5.0'] + - itsdangerous 0.24 + - jedi 0.10.2 + - name: jedi + version: 0.11.0 + depends: ['parso ==0.1.0'] + - name: jedi + version: 0.11.1 + depends: ['numpydoc', 'parso >=0.1.0,<0.2'] + - name: jedi + version: 0.12.0 + depends: ['parso >=0.2.0'] + - name: jedi + version: 0.12.1 + depends: ['parso >=0.3.0'] + - name: jinja2 + version: '2.10' + depends: ['markupsafe >=0.23', 'setuptools'] + - name: jinja2 + version: 2.9.6 + depends: ['markupsafe >=0.23', 'setuptools'] + - jmespath 0.9.3 + - jpeg 9b + - name: jsonschema + version: 2.6.0 + depends: ['setuptools'] + - name: jupyter + version: 1.0.0 + depends: ['ipykernel', 'ipywidgets', 'jupyter_console', 'nbconvert', 'notebook', 'qtconsole'] + - name: jupyter_client + version: 5.1.0 + depends: ['jupyter_core', 'python-dateutil >=2.1', 'pyzmq >=13', 'traitlets'] + - name: jupyter_client + version: 5.2.1 + depends: ['jupyter_core', 'python-dateutil >=2.1', 'pyzmq >=13', 'traitlets'] + - name: jupyter_client + version: 5.2.2 + depends: ['jupyter_core', 'python-dateutil >=2.1', 'pyzmq >=13', 'tornado', 'traitlets'] + - name: jupyter_client + version: 5.2.3 + depends: ['jupyter_core', 'python-dateutil >=2.1', 'pyzmq >=13', 'tornado', 'traitlets'] + - name: jupyter_console + version: 5.2.0 + depends: ['ipykernel', 'ipython', 'jupyter_client', 'pexpect', 'prompt_toolkit', 'pygments'] + - name: jupyter_core + version: 4.3.0 + depends: ['traitlets'] + - name: jupyter_core + version: 4.4.0 + depends: ['traitlets'] + - kiwisolver 1.0.0 + - kiwisolver 1.0.1 + - lazy-object-proxy 1.3.1 + - llvmlite 0.20.0 + - llvmlite 0.21.0 + - llvmlite 0.22.0 + - locket 0.2.0 + - name: logilab-common + version: 1.4.1 + depends: ['setuptools', 'six >=1.4.0'] + - make 4.2.1 + - markupsafe 1.0 + - name: matplotlib + version: 2.0.2 + depends: ['cycler >=0.10', 'numpy', 'pyparsing', 'pyqt 5.6.*', 'python-dateutil', 'pytz', 'setuptools', 'tornado'] + - name: matplotlib + version: 2.1.0 + depends: ['cycler >=0.10', 'numpy', 'pyparsing', 'pyqt 5.6.*', 'python-dateutil', 'pytz', 'setuptools', 'tornado'] + - name: matplotlib + version: 2.1.1 + depends: ['cycler >=0.10', 'numpy', 'pyparsing', 'pyqt 5.6.*', 'python-dateutil', 'pytz', 'setuptools', 'tornado'] + - name: matplotlib + version: 2.1.2 + depends: ['cycler >=0.10', 'numpy', 'pyparsing', 'pyqt 5.6.*', 'python-dateutil', 'pytz', 'setuptools', 'tornado'] + - name: matplotlib + version: 2.2.0 + depends: ['cycler >=0.10', 'numpy', 'pyparsing', 'pyqt 5.6.*', 'python-dateutil', 'pytz', 'setuptools', 'tornado'] + - name: matplotlib + version: 2.2.2 + depends: ['cycler >=0.10', 'numpy', 'pyparsing', 'pyqt >=5.6,<6.0a0', 'python-dateutil', 'pytz', 'setuptools', 'tornado'] + - name: matplotlib + version: 2.2.3 + depends: ['cycler >=0.10', 'numpy', 'pyparsing', 'pyqt 5.9.*', 'python-dateutil', 'pytz', 'setuptools', 'tornado'] + - mistune 0.7.4 + - mistune 0.8.1 + - mistune 0.8.3 + - msgpack-python 0.4.8 + - msgpack-python 0.5.1 + - msgpack-python 0.5.5 + - msgpack-python 0.5.6 + - multipledispatch 0.4.9 + - multipledispatch 0.5.0 + - name: multipledispatch + version: 0.6.0 + depends: ['six'] + - name: nbconvert + version: 5.3.1 + depends: ['bleach', 'entrypoints >=0.2.2', 'jinja2', 'jupyter_client >=4.2', 'jupyter_core', 'mistune >0.6', 'nbformat', 'pandoc', 'pandocfilters >=1.4.1', 'pygments', 'testpath', 'traitlets'] + - name: nbformat + version: 4.4.0 + depends: ['ipython_genutils', 'jsonschema >=2.4,!=2.5.0', 'jupyter_core', 'traitlets >=4.1'] + - ncurses 6.0 + - ncurses 6.1 + - name: nose + version: 1.3.7 + depends: ['setuptools'] + - name: notebook + version: 5.0.0 + depends: ['ipykernel', 'ipython_genutils', 'jinja2', 'jupyter_client', 'jupyter_core', 'nbconvert', 'nbformat', 'terminado >=0.3.3', 'tornado >=4', 'traitlets >=4.3'] + - name: notebook + version: 5.1.0 + depends: ['ipykernel', 'ipython_genutils', 'jinja2', 'jupyter_client', 'jupyter_core', 'nbconvert', 'nbformat', 'terminado >=0.3.3', 'tornado >=4', 'traitlets >=4.3'] + - name: notebook + version: 5.2.0 + depends: ['ipykernel', 'ipython_genutils', 'jinja2', 'jupyter_client', 'jupyter_core', 'nbconvert', 'nbformat', 'terminado >=0.3.3', 'tornado >=4', 'traitlets >=4.3'] + - name: notebook + version: 5.2.1 + depends: ['ipykernel', 'ipython_genutils', 'jinja2', 'jupyter_client', 'jupyter_core', 'nbconvert', 'nbformat', 'terminado >=0.3.3', 'tornado >=4', 'traitlets >=4.3'] + - name: notebook + version: 5.2.2 + depends: ['ipykernel', 'ipython_genutils', 'jinja2', 'jupyter_client', 'jupyter_core', 'nbconvert', 'nbformat', 'terminado >=0.3.3', 'tornado >=4', 'traitlets >=4.3'] + - name: notebook + version: 5.3.1 + depends: ['ipykernel', 'ipython_genutils', 'jinja2', 'jupyter_client >=5.2.0', 'jupyter_core >=4.4.0', 'nbconvert', 'nbformat', 'send2trash', 'terminado >=0.8.1', 'tornado >=4', 'traitlets >=4.2.1'] + - name: notebook + version: 5.4.0 + depends: ['ipykernel', 'ipython_genutils', 'jinja2', 'jupyter_client >=5.2.0', 'jupyter_core >=4.4.0', 'nbconvert', 'nbformat', 'send2trash', 'terminado >=0.8.1', 'tornado >=4', 'traitlets >=4.2.1'] + - name: notebook + version: 5.4.1 + depends: ['ipykernel', 'ipython_genutils', 'jinja2', 'jupyter_client >=5.2.0', 'jupyter_core >=4.4.0', 'nbconvert', 'nbformat', 'send2trash', 'terminado >=0.8.1', 'tornado >=4', 'traitlets >=4.2.1'] + - name: notebook + version: 5.5.0 + depends: ['ipykernel', 'ipython_genutils', 'jinja2', 'jupyter_client >=5.2.0', 'jupyter_core >=4.4.0', 'nbconvert', 'nbformat', 'pyzmq >=17', 'send2trash', 'terminado >=0.8.1', 'tornado >=4', 'traitlets >=4.2.1'] + - name: notebook + version: 5.6.0 + depends: ['ipykernel', 'ipython_genutils', 'jinja2', 'jupyter_client >=5.2.0', 'jupyter_core >=4.4.0', 'nbconvert', 'nbformat', 'prometheus_client', 'pyzmq >=17', 'send2trash', 'terminado >=0.8.1', 'tornado >=4', 'traitlets >=4.2.1'] + - numpy 1.11.3 + - numpy 1.12.1 + - numpy 1.13.1 + - numpy 1.13.3 + - numpy 1.14.0 + - numpy 1.14.1 + - numpy 1.14.2 + - numpy 1.14.3 + - numpy 1.14.4 + - numpy 1.14.5 + - numpy 1.15.0 + - numpy 1.9.3 + - name: numpydoc + version: 0.7.0 + depends: ['sphinx'] + - name: numpydoc + version: 0.8.0 + depends: ['sphinx'] + - name: openssl + version: 1.0.2l + depends: ['ca-certificates'] + - name: openssl + version: 1.0.2m + depends: ['ca-certificates'] + - name: openssl + version: 1.0.2n + depends: ['ca-certificates'] + - name: openssl + version: 1.0.2o + depends: ['ca-certificates'] + - name: openssl + version: 1.0.2p + depends: ['ca-certificates'] + - name: packaging + version: '16.8' + depends: ['pyparsing', 'six'] + - name: packaging + version: '17.1' + depends: ['pyparsing', 'six'] + - name: pandas + version: 0.20.3 + depends: ['numpy >=1.9', 'python-dateutil', 'pytz'] + - name: pandas + version: 0.21.0 + depends: ['numpy >=1.9.3,<2.0a0', 'python-dateutil', 'pytz'] + - name: pandas + version: 0.21.1 + depends: ['numpy >=1.9.3,<2.0a0', 'python-dateutil', 'pytz'] + - name: pandas + version: 0.22.0 + depends: ['numpy >=1.9.3,<2.0a0', 'python-dateutil', 'pytz'] + - name: pandas + version: 0.23.0 + depends: ['numpy >=1.9.3,<2.0a0', 'python-dateutil', 'pytz'] + - name: pandas + version: 0.23.1 + depends: ['numpy >=1.9.3,<2.0a0', 'python-dateutil >=2.5.*', 'pytz'] + - name: pandas + version: 0.23.2 + depends: ['numpy >=1.11.3,<2.0a0', 'python-dateutil >=2.5.*', 'pytz'] + - name: pandas + version: 0.23.3 + depends: ['numpy >=1.11.3,<2.0a0', 'python-dateutil >=2.5.*', 'pytz'] + - name: pandas + version: 0.23.4 + depends: ['numpy >=1.11.3,<2.0a0', 'python-dateutil >=2.5.*', 'pytz'] + - pandocfilters 1.4.2 + - parso 0.1.0 + - parso 0.1.1 + - parso 0.2.0 + - parso 0.2.1 + - parso 0.3.0 + - parso 0.3.1 + - name: partd + version: 0.3.8 + depends: ['locket', 'toolz'] + - patchelf 0.9 + - path.py 10.3.1 + - path.py 10.5 + - path.py 11.0 + - path.py 11.0.1 + - name: pathlib2 + version: 2.3.0 + depends: ['six'] + - name: pathlib2 + version: 2.3.2 + depends: ['six'] + - pcre 8.41 + - pcre 8.42 + - perl 5.26.2 + - name: perl-app-cpanminus + version: '1.7039' + depends: ['perl 5.22.0*'] + - name: perl-encode-locale + version: '1.05' + depends: ['perl >=5.26.2,<5.27.0a0'] + - name: pexpect + version: 4.2.1 + depends: ['ptyprocess >=0.5'] + - name: pexpect + version: 4.3.0 + depends: ['ptyprocess >=0.5'] + - name: pexpect + version: 4.3.1 + depends: ['ptyprocess >=0.5'] + - name: pexpect + version: 4.4.0 + depends: ['ptyprocess >=0.5'] + - name: pexpect + version: 4.5.0 + depends: ['ptyprocess >=0.5'] + - name: pexpect + version: 4.6.0 + depends: ['ptyprocess >=0.5'] + - pickleshare 0.7.4 + - name: pip + version: 10.0.1 + depends: ['setuptools', 'wheel'] + - name: pip + version: 9.0.1 + depends: ['setuptools', 'wheel'] + - name: pip + version: 9.0.3 + depends: ['setuptools', 'wheel'] + - pixman 0.34.0 + - pkginfo 1.4.1 + - pkginfo 1.4.2 + - ply 3.10 + - ply 3.11 + - name: prometheus_client + version: 0.2.0 + depends: ['twisted'] + - name: prometheus_client + version: 0.3.0 + depends: ['twisted'] + - name: prometheus_client + version: 0.3.1 + depends: ['twisted'] + - name: prompt_toolkit + version: 1.0.15 + depends: ['pygments', 'six >=1.9.0', 'wcwidth'] + - name: prompt_toolkit + version: 2.0.2 + depends: ['pygments', 'six >=1.9.0', 'wcwidth'] + - name: prompt_toolkit + version: 2.0.3 + depends: ['pygments', 'six >=1.9.0', 'wcwidth'] + - name: prompt_toolkit + version: 2.0.4 + depends: ['pygments', 'six >=1.9.0', 'wcwidth'] + - psutil 5.2.2 + - psutil 5.3.1 + - psutil 5.4.0 + - psutil 5.4.1 + - psutil 5.4.3 + - psutil 5.4.5 + - psutil 5.4.6 + - psycopg2 2.7.3.1 + - psycopg2 2.7.3.2 + - psycopg2 2.7.4 + - psycopg2 2.7.5 + - ptyprocess 0.5.2 + - ptyprocess 0.6.0 + - pyasn1 0.3.7 + - pyasn1 0.4.2 + - pyasn1 0.4.3 + - pyasn1 0.4.4 + - name: pyasn1-modules + version: 0.2.1 + depends: ['pyasn1 >=0.4.1,<0.5.0'] + - name: pyasn1-modules + version: 0.2.2 + depends: ['pyasn1 >=0.4.1,<0.5.0'] + - pycosat 0.6.2 + - pycosat 0.6.3 + - pycparser 2.18 + - name: pygments + version: 2.2.0 + depends: ['setuptools'] + - pympler 0.5 + - name: pyopenssl + version: 17.2.0 + depends: ['cryptography >=1.9', 'six >=1.5.2'] + - name: pyopenssl + version: 17.4.0 + depends: ['cryptography >=1.9', 'six >=1.5.2'] + - name: pyopenssl + version: 17.5.0 + depends: ['cryptography >=2.1.4', 'six >=1.5.2'] + - name: pyopenssl + version: 18.0.0 + depends: ['cryptography >=2.2.1', 'six >=1.5.2'] + - pyparsing 2.2.0 + - name: pyqt + version: 5.6.0 + depends: ['qt 5.6.*', 'sip 4.18.*'] + - name: pyqt + version: 5.9.2 + depends: ['dbus >=1.13.2,<2.0a0', 'qt 5.9.*', 'qt >=5.9.6,<5.10.0a0', 'sip >=4.19.4'] + - pysocks 1.6.7 + - pysocks 1.6.8 + - name: python-dateutil + version: 2.6.1 + depends: ['six'] + - name: python-dateutil + version: 2.7.0 + depends: ['six >=1.5'] + - name: python-dateutil + version: 2.7.2 + depends: ['six >=1.5'] + - name: python-dateutil + version: 2.7.3 + depends: ['six >=1.5'] + - name: python-digest + version: 1.1.1 + depends: ['cryptography <2.2'] + - python-graphviz 0.8.2 + - python-graphviz 0.8.3 + - python-graphviz 0.8.4 + - pytz 2017.2 + - pytz 2017.3 + - pytz 2018.3 + - pytz 2018.4 + - pytz 2018.5 + - pyyaml 3.12 + - pyyaml 3.13 + - pyzmq 16.0.2 + - pyzmq 16.0.3 + - pyzmq 17.0.0 + - pyzmq 17.1.0 + - pyzmq 17.1.2 + - name: qtconsole + version: 4.3.1 + depends: ['ipykernel >=4.1', 'jupyter_client >=4.1', 'jupyter_core', 'pygments', 'pyqt', 'traitlets'] + - name: qtconsole + version: 4.4.0 + depends: ['ipykernel >=4.1', 'jupyter_client >=4.1', 'jupyter_core', 'pygments', 'pyqt >=5.9.2,<5.10.0a0', 'traitlets'] + - redis 4.0.10 + - redis 4.0.2 + - redis 4.0.8 + - redis 4.0.9 + - redis-py 2.10.6 + - name: requests + version: 2.18.4 + depends: ['certifi >=2017.4.17', 'chardet >=3.0.2,<3.1.0', 'idna >=2.5,<2.7', 'urllib3 >=1.21.1,<1.23'] + - name: requests + version: 2.19.1 + depends: ['certifi >=2017.4.17', 'chardet >=3.0.2,<3.1.0', 'idna >=2.5,<2.8', 'urllib3 >=1.21.1,<1.24'] + - name: ruamel_yaml + version: 0.11.14 + depends: ['yaml'] + - name: ruamel_yaml + version: 0.15.35 + depends: ['yaml', 'yaml >=0.1.7,<0.2.0a0'] + - name: ruamel_yaml + version: 0.15.37 + depends: ['yaml >=0.1.7,<0.2.0a0'] + - name: ruamel_yaml + version: 0.15.40 + depends: ['yaml >=0.1.7,<0.2.0a0'] + - name: ruamel_yaml + version: 0.15.42 + depends: ['yaml >=0.1.7,<0.2.0a0'] + - name: ruamel_yaml + version: 0.15.46 + depends: ['yaml >=0.1.7,<0.2.0a0'] + - name: s3fs + version: 0.1.3 + depends: ['boto3'] + - name: s3fs + version: 0.1.4 + depends: ['boto3'] + - name: s3fs + version: 0.1.5 + depends: ['boto3'] + - name: s3transfer + version: 0.1.10 + depends: ['botocore >=1.3.0,<2.0.0'] + - name: s3transfer + version: 0.1.11 + depends: ['botocore >=1.3.0,<2.0.0'] + - name: s3transfer + version: 0.1.13 + depends: ['botocore >=1.3.0,<2.0.0'] + - scandir 1.5 + - scandir 1.6 + - scandir 1.7 + - scandir 1.8 + - scandir 1.9.0 + - name: scipy + version: 0.19.1 + depends: ['numpy >=1.9.3,<2.0a0'] + - name: scipy + version: 1.0.0 + depends: ['numpy >=1.9.3,<2.0a0'] + - name: scipy + version: 1.0.1 + depends: ['numpy >=1.9.3,<2.0a0'] + - name: scipy + version: 1.1.0 + depends: ['numpy >=1.11.3,<2.0a0'] + - send2trash 1.4.2 + - send2trash 1.5.0 + - name: service_identity + version: 17.0.0 + depends: ['attrs >=16.0.0', 'pyasn1', 'pyasn1-modules', 'pyopenssl >=0.12'] + - name: setuptools + version: 36.5.0 + depends: ['certifi'] + - name: setuptools + version: 38.4.0 + depends: ['certifi >=2016.09'] + - name: setuptools + version: 38.5.1 + depends: ['certifi >=2016.09'] + - name: setuptools + version: 39.0.1 + depends: ['certifi >=2016.09'] + - name: setuptools + version: 39.1.0 + depends: ['certifi >=2016.09'] + - name: setuptools + version: 39.2.0 + depends: ['certifi >=2016.09'] + - name: setuptools + version: 40.0.0 + depends: ['certifi >=2016.09'] + - simplegeneric 0.8.1 + - name: singledispatch + version: 3.4.0.3 + depends: ['six'] + - sip 4.18.1 + - sip 4.19.8 + - six 1.10.0 + - six 1.11.0 + - snowballstemmer 1.2.1 + - name: sortedcollections + version: 0.5.3 + depends: ['sortedcontainers'] + - name: sortedcollections + version: 0.6.1 + depends: ['sortedcontainers'] + - name: sortedcollections + version: 1.0.1 + depends: ['sortedcontainers >=2.0'] + - sortedcontainers 1.5.10 + - sortedcontainers 1.5.7 + - sortedcontainers 1.5.9 + - sortedcontainers 2.0.2 + - sortedcontainers 2.0.3 + - sortedcontainers 2.0.4 + - name: sphinx + version: 1.6.3 + depends: ['alabaster', 'babel', 'docutils', 'imagesize', 'jinja2', 'pygments', 'requests', 'six', 'snowballstemmer', 'sphinxcontrib-websupport', 'typing'] + - name: sphinx + version: 1.6.6 + depends: ['alabaster', 'babel', 'docutils', 'imagesize', 'jinja2', 'pygments', 'requests', 'six', 'snowballstemmer', 'sphinxcontrib-websupport', 'typing'] + - name: sphinx + version: 1.7.0 + depends: ['alabaster', 'babel', 'docutils', 'imagesize', 'jinja2', 'packaging', 'pygments', 'requests', 'six', 'snowballstemmer', 'sphinxcontrib-websupport', 'typing'] + - name: sphinx + version: 1.7.1 + depends: ['alabaster', 'babel', 'docutils', 'imagesize', 'jinja2', 'packaging', 'pygments', 'requests', 'six', 'snowballstemmer', 'sphinxcontrib-websupport', 'typing'] + - name: sphinx + version: 1.7.2 + depends: ['alabaster', 'babel', 'docutils', 'imagesize', 'jinja2', 'packaging', 'pygments', 'requests', 'six', 'snowballstemmer', 'sphinxcontrib-websupport', 'typing'] + - name: sphinx + version: 1.7.3 + depends: ['alabaster', 'babel', 'docutils', 'imagesize', 'jinja2', 'packaging', 'pygments', 'requests', 'six', 'snowballstemmer', 'sphinxcontrib-websupport', 'typing'] + - name: sphinx + version: 1.7.4 + depends: ['alabaster', 'babel', 'docutils', 'imagesize', 'jinja2', 'packaging', 'pygments', 'requests', 'six', 'snowballstemmer', 'sphinxcontrib-websupport', 'typing'] + - name: sphinx + version: 1.7.5 + depends: ['alabaster >=0.7,<0.8', 'babel >=1.3,!=2.0', 'docutils >=0.11', 'imagesize', 'jinja2 >=2.3', 'packaging', 'pygments >2.0', 'requests >2.0.0', 'six >=1.5', 'snowballstemmer >=1.1', 'sphinxcontrib-websupport'] + - name: sphinx + version: 1.7.6 + depends: ['alabaster >=0.7,<0.8', 'babel >=1.3,!=2.0', 'docutils >=0.11', 'imagesize', 'jinja2 >=2.3', 'packaging', 'pygments >2.0', 'requests >2.0.0', 'six >=1.5', 'snowballstemmer >=1.1', 'sphinxcontrib-websupport'] + - sphinxcontrib 1.0 + - name: sphinxcontrib-websupport + version: 1.0.1 + depends: ['sphinxcontrib'] + - name: sphinxcontrib-websupport + version: 1.1.0 + depends: ['sphinxcontrib'] + - sqlalchemy 1.1.13 + - sqlalchemy 1.2.0 + - sqlalchemy 1.2.1 + - sqlalchemy 1.2.10 + - sqlalchemy 1.2.3 + - sqlalchemy 1.2.4 + - sqlalchemy 1.2.5 + - sqlalchemy 1.2.6 + - sqlalchemy 1.2.7 + - sqlalchemy 1.2.8 + - name: ssl_match_hostname + version: 3.5.0.1 + depends: ['backports'] + - subprocess32 3.2.7 + - subprocess32 3.5.0 + - subprocess32 3.5.1 + - subprocess32 3.5.2 + - tblib 1.3.2 + - name: terminado + version: '0.6' + depends: ['ptyprocess', 'tornado >=4'] + - name: terminado + version: 0.8.1 + depends: ['ptyprocess', 'tornado >=4'] + - testpath 0.3.1 + - name: thrift + version: 0.11.0 + depends: ['six >=1.7.2'] + - thrift 0.9.3 + - name: thriftpy + version: 0.3.9 + depends: ['ply >=3.4,<4.0'] + - toolz 0.8.2 + - toolz 0.9.0 + - tornado 4.5.2 + - tornado 4.5.3 + - tornado 5.0 + - tornado 5.0.1 + - tornado 5.0.2 + - tornado 5.1 + - name: traitlets + version: 4.3.2 + depends: ['decorator', 'ipython_genutils', 'six'] + - name: twisted + version: 17.9.0 + depends: ['appdirs >=1.4.0', 'automat >=0.3.0', 'constantly >=15.1', 'cryptography >=1.5', 'hyperlink >=17.1.1', 'idna >=0.6,!=2.3', 'incremental >=16.10.1', 'pyasn1', 'pyopenssl >=16.0.0', 'service_identity', 'zope.interface >=4.0.2'] + - name: twisted + version: 18.4.0 + depends: ['appdirs >=1.4.0', 'automat >=0.3.0', 'constantly >=15.1', 'cryptography >=1.5', 'hyperlink >=17.1.1', 'idna >=0.6,!=2.3', 'incremental >=16.10.1', 'pyasn1', 'pyopenssl >=16.0.0', 'service_identity', 'zope.interface >=4.0.2'] + - name: twisted + version: 18.7.0 + depends: ['appdirs >=1.4.0', 'automat >=0.3.0', 'constantly >=15.1', 'cryptography >=1.5', 'hyperlink >=17.1.1', 'idna >=0.6,!=2.3', 'incremental >=16.10.1', 'pyasn1', 'pyopenssl >=16.0.0', 'service_identity', 'zope.interface >=4.0.2'] + - typed-ast 1.1.0 + - typing 3.6.2 + - typing 3.6.4 + - ujson 1.35 + - name: urllib3 + version: '1.22' + depends: ['certifi', 'cryptography >=1.3.4', 'idna >=2.0.0', 'pyopenssl >=0.14', 'pysocks >=1.5.6,<2.0,!=1.5.7'] + - name: urllib3 + version: '1.23' + depends: ['certifi', 'cryptography >=1.3.4', 'idna >=2.0.0', 'pyopenssl >=0.14', 'pysocks >=1.5.6,<2.0,!=1.5.7'] + - wcwidth 0.1.7 + - webencodings 0.5.1 + - werkzeug 0.12.2 + - werkzeug 0.14.1 + - name: wheel + version: 0.29.0 + depends: ['setuptools'] + - name: wheel + version: 0.30.0 + depends: ['setuptools'] + - name: wheel + version: 0.31.0 + depends: ['setuptools'] + - name: wheel + version: 0.31.1 + depends: ['setuptools'] + - name: widgetsnbextension + version: 3.0.2 + depends: ['notebook >=4.4.1'] + - name: widgetsnbextension + version: 3.0.8 + depends: ['notebook >=4.4.1'] + - name: widgetsnbextension + version: 3.1.0 + depends: ['notebook >=4.4.1'] + - name: widgetsnbextension + version: 3.1.4 + depends: ['notebook >=4.4.1'] + - name: widgetsnbextension + version: 3.2.0 + depends: ['notebook >=4.4.1'] + - name: widgetsnbextension + version: 3.2.1 + depends: ['notebook >=4.4.1'] + - name: widgetsnbextension + version: 3.3.0 + depends: ['notebook >=4.4.1'] + - name: widgetsnbextension + version: 3.3.1 + depends: ['notebook >=4.4.1'] + - name: widgetsnbextension + version: 3.4.0 + depends: ['notebook >=4.4.1'] + - wrapt 1.10.11 + - xz 5.2.3 + - xz 5.2.4 + - yaml 0.1.7 + - zeromq 4.2.2 + - zeromq 4.2.3 + - zeromq 4.2.5 + - name: zict + version: 0.1.2 + depends: ['heapdict'] + - name: zict + version: 0.1.3 + depends: ['heapdict'] + - zope 1.0 + - name: zope.interface + version: 4.4.3 + depends: ['zope'] + - name: zope.interface + version: 4.5.0 + depends: ['zope'] + +cases: +- + request: + - install: alabaster + response: + - state: + - alabaster 0.7.11 From 2e086eca40fc852dc682087de2fb98a9829c42ca Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Tue, 19 May 2020 23:22:35 -0500 Subject: [PATCH 2072/3170] add test case --- tests/yaml/huge.yml | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/tests/yaml/huge.yml b/tests/yaml/huge.yml index 787ef522511..e1ceed56870 100644 --- a/tests/yaml/huge.yml +++ b/tests/yaml/huge.yml @@ -1235,3 +1235,23 @@ cases: response: - state: - alabaster 0.7.11 +- + request: + - install: ipython==6.3.1 + response: + - state: + - backcall 0.1.0 + - decorator 4.3.0 + - ipython 6.3.1 + - ipython_genutils 0.2.0 + - jedi 0.12.1 + - parso 0.3.1 + - pexpect 4.6.0 + - pickleshare 0.7.4 + - prompt_toolkit 1.0.15 + - ptyprocess 0.6.0 + - pygments 2.2.0 + - simplegeneric 0.8.1 + - six 1.11.0 + - traitlets 4.3.2 + - wcwidth 0.1.7 From a45f657f972f23bbeee463cbf9500d49ef473c8b Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Tue, 19 May 2020 23:45:14 -0500 Subject: [PATCH 2073/3170] add test for double install --- tests/yaml/conflict_1.yml | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/tests/yaml/conflict_1.yml b/tests/yaml/conflict_1.yml index 8f8d6bdf0b0..1600b0ea8c0 100644 --- a/tests/yaml/conflict_1.yml +++ b/tests/yaml/conflict_1.yml @@ -15,6 +15,15 @@ cases: # a 1.0.0 has requirement B==1.0.0, but you'll have b 2.0.0 which is incompatible. # -- better would be: # A 1.0.0 has incompatible requirements B==1.0.0, B==2.0.0 +- + request: + - install: ['B==1.0.0', 'B'] + response: + - state: + - B 1.0.0 + skip: old + # -- old error: + # Double requirement given: B (already in B==1.0.0, name='B') - request: - install: ['B==1.0.0', 'B==2.0.0'] From 982fddf9c0492b26bc14668a3b3ea8b7a936a0c5 Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Wed, 20 May 2020 13:50:46 -0500 Subject: [PATCH 2074/3170] add checking exit code and stderr message --- tests/functional/test_yaml.py | 24 +++++++++++------------- 1 file changed, 11 insertions(+), 13 deletions(-) diff --git a/tests/functional/test_yaml.py b/tests/functional/test_yaml.py index 4a03381a31c..5e33f6be4d9 100644 --- a/tests/functional/test_yaml.py +++ b/tests/functional/test_yaml.py @@ -175,24 +175,22 @@ def test_yaml_based(script, case): request[action], request.get('options', '').split(), case[':resolver:'] == 'new') + result = effect['result'] if 0: # for analyzing output easier with open(DATA_DIR.parent / "yaml" / case[':name:'].replace('*', '-'), 'w') as fo: - result = effect['result'] fo.write("=== RETURNCODE = %d\n" % result.returncode) fo.write("=== STDERR ===:\n%s\n" % result.stderr) if 'state' in response: - assert effect['state'] == (response['state'] or []), \ - str(effect["result"]) - - error = False - if 'conflicting' in response: - error = True - - if error: - if case[":resolver:"] == 'old': - assert effect["result"].returncode == 0, str(effect["result"]) - elif case[":resolver:"] == 'new': - assert effect["result"].returncode == 1, str(effect["result"]) + assert effect['state'] == (response['state'] or []), str(result) + + error = response.get('error') + if error and case[":resolver:"] == 'new': + return_code = error.get('code') + if return_code: + assert result.returncode == return_code + stderr = error.get('stderr') + if stderr: + assert stderr in result.stderr From 54f350d5245648380d0205ec67d017f062666b4a Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Wed, 20 May 2020 13:52:07 -0500 Subject: [PATCH 2075/3170] add error checking to yaml test files --- tests/yaml/conflict_1.yml | 28 ++++++++++++++++++++++------ tests/yaml/conflicting_diamond.yml | 10 ++++------ tests/yaml/conflicting_triangle.yml | 10 ++++------ tests/yaml/large.yml | 5 ++++- 4 files changed, 34 insertions(+), 19 deletions(-) diff --git a/tests/yaml/conflict_1.yml b/tests/yaml/conflict_1.yml index 1600b0ea8c0..58d721e1280 100644 --- a/tests/yaml/conflict_1.yml +++ b/tests/yaml/conflict_1.yml @@ -9,12 +9,16 @@ cases: request: - install: A response: - - state: null - skip: true + - error: + code: 0 + stderr: incompatible + skip: old # -- currently the error message is: - # a 1.0.0 has requirement B==1.0.0, but you'll have b 2.0.0 which is incompatible. + # a 1.0.0 has requirement B==1.0.0, but you'll have b 2.0.0 which is + # incompatible. # -- better would be: # A 1.0.0 has incompatible requirements B==1.0.0, B==2.0.0 + - request: - install: ['B==1.0.0', 'B'] @@ -24,12 +28,16 @@ cases: skip: old # -- old error: # Double requirement given: B (already in B==1.0.0, name='B') + - request: - install: ['B==1.0.0', 'B==2.0.0'] response: - state: null - skip: true + error: + code: 1 + stderr: 'No matching distribution' + skip: old # -- currently the (new resolver) error message is: # Could not find a version that satisfies the requirement B==1.0.0 # Could not find a version that satisfies the requirement B==2.0.0 @@ -39,24 +47,32 @@ cases: # same time. # -- the old error message was actually better here: # Double requirement given: B==2.0.0 (already in B==1.0.0, name='B') + - request: - install: B==1.5.0 response: - state: null - skip: true + error: + code: 1 + stderr: 'No matching distribution' + skip: old # -- currently (new resolver) error message is: # Could not find a version that satisfies the requirement B==1.5.0 # No matching distribution found for b # -- the old error message was actually better here: # Could not find a version that satisfies the requirement B==1.5.0 (from versions: 1.0.0, 2.0.0) # No matching distribution found for B==1.5.0 + - request: - install: A==2.0 response: - state: null - skip: true + error: + code: 1 + stderr: 'No matching distribution' + skip: old # -- currently the error message is: # Could not find a version that satisfies the requirement A==2.0 # No matching distribution found for a diff --git a/tests/yaml/conflicting_diamond.yml b/tests/yaml/conflicting_diamond.yml index eef90e21f60..f36f92a380c 100644 --- a/tests/yaml/conflicting_diamond.yml +++ b/tests/yaml/conflicting_diamond.yml @@ -9,12 +9,10 @@ cases: request: - install: A response: - - conflicting: - - required_by: [A 1.0.0, B 1.0.0] - selector: D == 1.0.0 - - required_by: [A 1.0.0, C 1.0.0] - selector: D == 2.0.0 - skip: true + - error: + code: 1 + stderr: No matching distribution found + skip: old # -- currently the error message is: # Could not find a version that satisfies the requirement D==2.0.0 (from c) # Could not find a version that satisfies the requirement D==1.0.0 (from b) diff --git a/tests/yaml/conflicting_triangle.yml b/tests/yaml/conflicting_triangle.yml index f2c5002ca42..3d7754af5de 100644 --- a/tests/yaml/conflicting_triangle.yml +++ b/tests/yaml/conflicting_triangle.yml @@ -12,11 +12,9 @@ cases: - state: - A 1.0.0 - C 1.0.0 - - conflicting: - - required_by: [A 1.0.0] - selector: C == 1.0.0 - - required_by: [B 1.0.0] - selector: C == 2.0.0 - skip: true + - error: + code: 0 + stderr: incompatible + skip: old # -- currently the error message is: # a 1.0.0 has requirement C==1.0.0, but you'll have c 2.0.0 which is incompatible. diff --git a/tests/yaml/large.yml b/tests/yaml/large.yml index edabee3ca89..3f5fbbd49a7 100644 --- a/tests/yaml/large.yml +++ b/tests/yaml/large.yml @@ -282,7 +282,10 @@ cases: - install: ['html5lib', 'six ==1.8.2'] response: - state: null - skip: true + error: + code: 1 + stderr: satisfies the requirement + skip: old # -- the new resolver tells: # Could not find a version that satisfies the requirement six==1.8.2 # Could not find a version that satisfies the requirement six>=1.9 (from html5lib) From fec4a0dabd80aac3d731de495761aa1998cde45c Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Wed, 20 May 2020 13:52:51 -0500 Subject: [PATCH 2076/3170] update yaml linter --- tests/yaml/linter.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/tests/yaml/linter.py b/tests/yaml/linter.py index 109ce17c155..7daaf39ed96 100644 --- a/tests/yaml/linter.py +++ b/tests/yaml/linter.py @@ -53,9 +53,12 @@ def lint_case(case, verbose=False): for request, response in zip(requests, responses): check_dict(request, optional=['install', 'uninstall', 'options']) - check_dict(response, optional=['state', 'conflicting']) - assert len(response) == 1 + check_dict(response, optional=['state', 'error']) + assert len(response) >= 1 assert isinstance(response.get('state') or [], list) + error = response.get('error') + if error: + check_dict(error, optional=['code', 'stderr']) def lint_yml(yml_file, verbose=False): From 13ce5209b4893a95ec41290e9f71a045a403efec Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Wed, 20 May 2020 14:06:25 -0500 Subject: [PATCH 2077/3170] add error checks - run circular tests with new resolver --- tests/yaml/circular.yml | 4 ---- tests/yaml/conflict_2.yml | 4 +++- 2 files changed, 3 insertions(+), 5 deletions(-) diff --git a/tests/yaml/circular.yml b/tests/yaml/circular.yml index ba938992370..95c535454fa 100644 --- a/tests/yaml/circular.yml +++ b/tests/yaml/circular.yml @@ -16,7 +16,6 @@ cases: - B 1.0.0 - C 1.0.0 - D 1.0.0 - skip: new - request: - install: B @@ -26,7 +25,6 @@ cases: - B 1.0.0 - C 1.0.0 - D 1.0.0 - skip: new - request: - install: C @@ -36,7 +34,6 @@ cases: - B 1.0.0 - C 1.0.0 - D 1.0.0 - skip: new - request: - install: D @@ -46,4 +43,3 @@ cases: - B 1.0.0 - C 1.0.0 - D 1.0.0 - skip: new diff --git a/tests/yaml/conflict_2.yml b/tests/yaml/conflict_2.yml index 57427f5b07a..5302e0e40f6 100644 --- a/tests/yaml/conflict_2.yml +++ b/tests/yaml/conflict_2.yml @@ -21,7 +21,9 @@ cases: - install: ['six<1.12', 'virtualenv==20.0.2'] response: - state: null - skip: true + error: + stderr: 'No matching distribution found' + skip: old # -- currently the error message is: # Could not find a version that satisfies the requirement six<1.12 # Could not find a version that satisfies the requirement six<2,>=1.12.0 (from virtualenv) From f11c6d6cbd3812e38945a876488c82b51648bc4c Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Wed, 20 May 2020 15:39:11 -0500 Subject: [PATCH 2078/3170] on Windows, the errors behave differently --- tests/functional/test_yaml.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/functional/test_yaml.py b/tests/functional/test_yaml.py index 5e33f6be4d9..adee1113007 100644 --- a/tests/functional/test_yaml.py +++ b/tests/functional/test_yaml.py @@ -4,6 +4,7 @@ import os import re +import sys import pytest import yaml @@ -187,7 +188,7 @@ def test_yaml_based(script, case): assert effect['state'] == (response['state'] or []), str(result) error = response.get('error') - if error and case[":resolver:"] == 'new': + if error and case[":resolver:"] == 'new' and sys.platform != 'win32': return_code = error.get('code') if return_code: assert result.returncode == return_code From e1f007b314017a68b206d593c5295347e0d94f38 Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Thu, 28 May 2020 12:20:13 -0500 Subject: [PATCH 2079/3170] check for stderr regex in linter --- tests/yaml/linter.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/yaml/linter.py b/tests/yaml/linter.py index 7daaf39ed96..ac17bbc41be 100644 --- a/tests/yaml/linter.py +++ b/tests/yaml/linter.py @@ -1,3 +1,4 @@ +import re import sys from pprint import pprint @@ -59,6 +60,16 @@ def lint_case(case, verbose=False): error = response.get('error') if error: check_dict(error, optional=['code', 'stderr']) + stderr = error.get('stderr') + if stderr: + if isinstance(stderr, str): + patters = [stderr] + elif isinstance(stderr, list): + patters = stderr + else: + raise "string or list expected, found %r" % stderr + for patter in patters: + re.compile(patter, re.I) def lint_yml(yml_file, verbose=False): From bf3b1859fc2dbba4f9e74244690c65327538918c Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Thu, 28 May 2020 12:52:52 -0500 Subject: [PATCH 2080/3170] add ability to match error messages against regular expressions --- tests/functional/test_yaml.py | 44 ++++++++++++++++------------- tests/yaml/conflict_1.yml | 8 +++--- tests/yaml/conflict_2.yml | 2 +- tests/yaml/conflicting_diamond.yml | 2 +- tests/yaml/conflicting_triangle.yml | 2 +- tests/yaml/large.yml | 2 +- 6 files changed, 32 insertions(+), 28 deletions(-) diff --git a/tests/functional/test_yaml.py b/tests/functional/test_yaml.py index adee1113007..f39c83efb64 100644 --- a/tests/functional/test_yaml.py +++ b/tests/functional/test_yaml.py @@ -11,20 +11,6 @@ from tests.lib import DATA_DIR, create_basic_wheel_for_package, path_to_url -_conflict_finder_pat = re.compile( - # Conflicting Requirements: \ - # A 1.0.0 requires B == 2.0.0, C 1.0.0 requires B == 1.0.0. - r""" - (?P<package>[\w\-_]+?) - [ ] - (?P<version>\S+?) - [ ]requires[ ] - (?P<selector>.+?) - (?=,|\.$) - """, - re.X -) - def generate_yaml_tests(directory): """ @@ -138,6 +124,29 @@ def handle_request(script, action, requirement, options, new_resolver=False): return {"result": result, "state": sorted(state)} +def check_error(error, result): + return_code = error.get('code') + if return_code: + assert result.returncode == return_code + + stderr = error.get('stderr') + if not stderr: + return + + if isinstance(stderr, str): + patters = [stderr] + elif isinstance(stderr, list): + patters = stderr + else: + raise "string or list expected, found %r" % stderr + + for patter in patters: + pat = re.compile(patter, re.I) + match = pat.search(result.stderr) + assert match, 'regex %r not found in stderr: %r' % ( + stderr, result.stderr) + + @pytest.mark.yaml @pytest.mark.parametrize( "case", generate_yaml_tests(DATA_DIR.parent / "yaml"), ids=id_func @@ -189,9 +198,4 @@ def test_yaml_based(script, case): error = response.get('error') if error and case[":resolver:"] == 'new' and sys.platform != 'win32': - return_code = error.get('code') - if return_code: - assert result.returncode == return_code - stderr = error.get('stderr') - if stderr: - assert stderr in result.stderr + check_error(error, result) diff --git a/tests/yaml/conflict_1.yml b/tests/yaml/conflict_1.yml index 58d721e1280..b99e033dd58 100644 --- a/tests/yaml/conflict_1.yml +++ b/tests/yaml/conflict_1.yml @@ -11,7 +11,7 @@ cases: response: - error: code: 0 - stderr: incompatible + stderr: ['requirement', 'is\s+incompatible'] skip: old # -- currently the error message is: # a 1.0.0 has requirement B==1.0.0, but you'll have b 2.0.0 which is @@ -36,7 +36,7 @@ cases: - state: null error: code: 1 - stderr: 'No matching distribution' + stderr: 'no\s+matching\s+distribution' skip: old # -- currently the (new resolver) error message is: # Could not find a version that satisfies the requirement B==1.0.0 @@ -55,7 +55,7 @@ cases: - state: null error: code: 1 - stderr: 'No matching distribution' + stderr: 'no\s+matching\s+distribution' skip: old # -- currently (new resolver) error message is: # Could not find a version that satisfies the requirement B==1.5.0 @@ -71,7 +71,7 @@ cases: - state: null error: code: 1 - stderr: 'No matching distribution' + stderr: 'no\s+matching\s+distribution' skip: old # -- currently the error message is: # Could not find a version that satisfies the requirement A==2.0 diff --git a/tests/yaml/conflict_2.yml b/tests/yaml/conflict_2.yml index 5302e0e40f6..80bb8a31e72 100644 --- a/tests/yaml/conflict_2.yml +++ b/tests/yaml/conflict_2.yml @@ -22,7 +22,7 @@ cases: response: - state: null error: - stderr: 'No matching distribution found' + stderr: 'no matching distribution found for\s+six' skip: old # -- currently the error message is: # Could not find a version that satisfies the requirement six<1.12 diff --git a/tests/yaml/conflicting_diamond.yml b/tests/yaml/conflicting_diamond.yml index f36f92a380c..c237454e75c 100644 --- a/tests/yaml/conflicting_diamond.yml +++ b/tests/yaml/conflicting_diamond.yml @@ -11,7 +11,7 @@ cases: response: - error: code: 1 - stderr: No matching distribution found + stderr: 'no\s+matching\s+distribution\s+found' skip: old # -- currently the error message is: # Could not find a version that satisfies the requirement D==2.0.0 (from c) diff --git a/tests/yaml/conflicting_triangle.yml b/tests/yaml/conflicting_triangle.yml index 3d7754af5de..1fb4cac7b07 100644 --- a/tests/yaml/conflicting_triangle.yml +++ b/tests/yaml/conflicting_triangle.yml @@ -14,7 +14,7 @@ cases: - C 1.0.0 - error: code: 0 - stderr: incompatible + stderr: ['requirement\s+c==1\.0\.0', 'is\s+incompatible'] skip: old # -- currently the error message is: # a 1.0.0 has requirement C==1.0.0, but you'll have c 2.0.0 which is incompatible. diff --git a/tests/yaml/large.yml b/tests/yaml/large.yml index 3f5fbbd49a7..6076ce2aad5 100644 --- a/tests/yaml/large.yml +++ b/tests/yaml/large.yml @@ -284,7 +284,7 @@ cases: - state: null error: code: 1 - stderr: satisfies the requirement + stderr: 'version\s+that\s+satisfies\s+the\s+requirement' skip: old # -- the new resolver tells: # Could not find a version that satisfies the requirement six==1.8.2 From 6e07c0fd82f3cee33aebadd3be4615d7ee9e42df Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Mon, 1 Jun 2020 21:57:56 -0500 Subject: [PATCH 2081/3170] Update tests/functional/test_yaml.py Co-authored-by: Pradyun Gedam <pradyunsg@gmail.com> --- tests/functional/test_yaml.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/functional/test_yaml.py b/tests/functional/test_yaml.py index f39c83efb64..d147a316cfe 100644 --- a/tests/functional/test_yaml.py +++ b/tests/functional/test_yaml.py @@ -141,8 +141,7 @@ def check_error(error, result): raise "string or list expected, found %r" % stderr for patter in patters: - pat = re.compile(patter, re.I) - match = pat.search(result.stderr) + match = re.search(patter, result.stderr) assert match, 'regex %r not found in stderr: %r' % ( stderr, result.stderr) From f6691c0aca5485e6b0af242364eceb7c806ec51d Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Mon, 1 Jun 2020 22:06:04 -0500 Subject: [PATCH 2082/3170] Update tests/yaml/conflict_2.yml Co-authored-by: Pradyun Gedam <pradyunsg@gmail.com> --- tests/yaml/conflict_2.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/yaml/conflict_2.yml b/tests/yaml/conflict_2.yml index 80bb8a31e72..f1f84f95ecd 100644 --- a/tests/yaml/conflict_2.yml +++ b/tests/yaml/conflict_2.yml @@ -22,7 +22,7 @@ cases: response: - state: null error: - stderr: 'no matching distribution found for\s+six' + stderr: 'no matching distribution found for six' skip: old # -- currently the error message is: # Could not find a version that satisfies the requirement six<1.12 From 599a49494528a532f92eff00d00aa5df8ef5de43 Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Mon, 1 Jun 2020 22:06:15 -0500 Subject: [PATCH 2083/3170] Update tests/yaml/conflicting_diamond.yml Co-authored-by: Pradyun Gedam <pradyunsg@gmail.com> --- tests/yaml/conflicting_diamond.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/yaml/conflicting_diamond.yml b/tests/yaml/conflicting_diamond.yml index c237454e75c..4338cb4a07b 100644 --- a/tests/yaml/conflicting_diamond.yml +++ b/tests/yaml/conflicting_diamond.yml @@ -11,7 +11,7 @@ cases: response: - error: code: 1 - stderr: 'no\s+matching\s+distribution\s+found' + stderr: 'no matching distribution found' skip: old # -- currently the error message is: # Could not find a version that satisfies the requirement D==2.0.0 (from c) From 2198e2b2abc7893eeccd4282054a12cfa2ef2701 Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Mon, 1 Jun 2020 22:07:23 -0500 Subject: [PATCH 2084/3170] Update tests/yaml/conflicting_triangle.yml Co-authored-by: Pradyun Gedam <pradyunsg@gmail.com> --- tests/yaml/conflicting_triangle.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/yaml/conflicting_triangle.yml b/tests/yaml/conflicting_triangle.yml index 1fb4cac7b07..666c37363db 100644 --- a/tests/yaml/conflicting_triangle.yml +++ b/tests/yaml/conflicting_triangle.yml @@ -14,7 +14,7 @@ cases: - C 1.0.0 - error: code: 0 - stderr: ['requirement\s+c==1\.0\.0', 'is\s+incompatible'] + stderr: ['requirement c==1\.0\.0', 'is incompatible'] skip: old # -- currently the error message is: # a 1.0.0 has requirement C==1.0.0, but you'll have c 2.0.0 which is incompatible. From 4b6ce4fbe413f3c78b906e6282dc835b01e230eb Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Mon, 1 Jun 2020 23:27:37 -0500 Subject: [PATCH 2085/3170] add restore re.I flag --- tests/functional/test_yaml.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/test_yaml.py b/tests/functional/test_yaml.py index d147a316cfe..0450c9713ba 100644 --- a/tests/functional/test_yaml.py +++ b/tests/functional/test_yaml.py @@ -141,7 +141,7 @@ def check_error(error, result): raise "string or list expected, found %r" % stderr for patter in patters: - match = re.search(patter, result.stderr) + match = re.search(patter, result.stderr, re.I) assert match, 'regex %r not found in stderr: %r' % ( stderr, result.stderr) From 0462774be1f5dffc426c9240a73d46e50a098196 Mon Sep 17 00:00:00 2001 From: Ilan Schnell <ilanschnell@gmail.com> Date: Mon, 1 Jun 2020 23:33:47 -0500 Subject: [PATCH 2086/3170] add comment about skiping error checking test on Windows --- tests/functional/test_yaml.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/functional/test_yaml.py b/tests/functional/test_yaml.py index 0450c9713ba..ba6ff47de24 100644 --- a/tests/functional/test_yaml.py +++ b/tests/functional/test_yaml.py @@ -197,4 +197,7 @@ def test_yaml_based(script, case): error = response.get('error') if error and case[":resolver:"] == 'new' and sys.platform != 'win32': + # Note: we currently skip running these tests on Windows, as they + # were failing due to different error codes. There should not + # be a reason for not running these this check on Windows. check_error(error, result) From 9a2527ad09697c612db5a8cf073df1009a049989 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Mon, 15 Jun 2020 15:21:51 +0800 Subject: [PATCH 2087/3170] Rename CFFI for PyPy --- tests/yaml/huge.yml | 35 +++++++++++++++++++---------------- tests/yaml/large.yml | 25 ++++++++++++++----------- 2 files changed, 33 insertions(+), 27 deletions(-) diff --git a/tests/yaml/huge.yml b/tests/yaml/huge.yml index e1ceed56870..01bfdf26f3c 100644 --- a/tests/yaml/huge.yml +++ b/tests/yaml/huge.yml @@ -196,16 +196,19 @@ base: - certifi 2018.1.18 - certifi 2018.4.16 - certifi 2018.8.13 - - name: cffi + # cffi is a bundled module in PyPy and causes resolution errors if pip + # tries to installed it. Give it a different name since we are simply + # checking the graph anyway and the identifier doesn't really matter. + - name: cffi_not_really version: 1.10.0 depends: ['pycparser'] - - name: cffi + - name: cffi_not_really version: 1.11.2 depends: ['pycparser'] - - name: cffi + - name: cffi_not_really version: 1.11.4 depends: ['pycparser'] - - name: cffi + - name: cffi_not_really version: 1.11.5 depends: ['pycparser'] - chardet 3.0.4 @@ -222,22 +225,22 @@ base: - coverage 4.5.1 - name: cryptography version: 2.0.3 - depends: ['asn1crypto >=0.21.0', 'cffi >=1.7', 'idna >=2.1', 'openssl 1.0.*', 'six >=1.4.1'] + depends: ['asn1crypto >=0.21.0', 'cffi_not_really >=1.7', 'idna >=2.1', 'openssl 1.0.*', 'six >=1.4.1'] - name: cryptography version: 2.1.3 - depends: ['asn1crypto >=0.21.0', 'cffi >=1.7', 'idna >=2.1', 'openssl 1.0.*', 'openssl >=1.0.2m,<1.0.3a', 'six >=1.4.1'] + depends: ['asn1crypto >=0.21.0', 'cffi_not_really >=1.7', 'idna >=2.1', 'openssl 1.0.*', 'openssl >=1.0.2m,<1.0.3a', 'six >=1.4.1'] - name: cryptography version: 2.1.4 - depends: ['asn1crypto >=0.21.0', 'cffi >=1.7', 'idna >=2.1', 'openssl 1.0.*', 'openssl >=1.0.2m,<1.0.3a', 'six >=1.4.1'] + depends: ['asn1crypto >=0.21.0', 'cffi_not_really >=1.7', 'idna >=2.1', 'openssl 1.0.*', 'openssl >=1.0.2m,<1.0.3a', 'six >=1.4.1'] - name: cryptography version: 2.2.1 - depends: ['asn1crypto >=0.21.0', 'cffi >=1.7', 'idna >=2.1', 'openssl 1.0.*', 'openssl >=1.0.2n,<1.0.3a', 'six >=1.4.1'] + depends: ['asn1crypto >=0.21.0', 'cffi_not_really >=1.7', 'idna >=2.1', 'openssl 1.0.*', 'openssl >=1.0.2n,<1.0.3a', 'six >=1.4.1'] - name: cryptography version: 2.2.2 - depends: ['asn1crypto >=0.21.0', 'cffi >=1.7', 'idna >=2.1', 'openssl 1.0.*', 'openssl >=1.0.2o,<1.0.3a', 'six >=1.4.1'] + depends: ['asn1crypto >=0.21.0', 'cffi_not_really >=1.7', 'idna >=2.1', 'openssl 1.0.*', 'openssl >=1.0.2o,<1.0.3a', 'six >=1.4.1'] - name: cryptography version: '2.3' - depends: ['asn1crypto >=0.21.0', 'cffi >=1.7', 'cryptography-vectors 2.3.*', 'idna >=2.1', 'openssl >=1.0.2o,<1.0.3a', 'six >=1.4.1'] + depends: ['asn1crypto >=0.21.0', 'cffi_not_really >=1.7', 'cryptography-vectors 2.3.*', 'idna >=2.1', 'openssl >=1.0.2o,<1.0.3a', 'six >=1.4.1'] - cryptography-vectors 2.0.3 - cryptography-vectors 2.1.3 - cryptography-vectors 2.1.4 @@ -385,22 +388,22 @@ base: - futures 3.2.0 - name: gevent version: 1.2.2 - depends: ['cffi >=1.3.0', 'greenlet >=0.4.10'] + depends: ['cffi_not_really >=1.3.0', 'greenlet >=0.4.10'] - name: gevent version: 1.3.0 - depends: ['cffi >=1.11.5', 'greenlet >=0.4.10'] + depends: ['cffi_not_really >=1.11.5', 'greenlet >=0.4.10'] - name: gevent version: 1.3.2.post0 - depends: ['cffi >=1.11.5', 'greenlet >=0.4.13'] + depends: ['cffi_not_really >=1.11.5', 'greenlet >=0.4.13'] - name: gevent version: 1.3.3 - depends: ['cffi >=1.11.5', 'greenlet >=0.4.13'] + depends: ['cffi_not_really >=1.11.5', 'greenlet >=0.4.13'] - name: gevent version: 1.3.4 - depends: ['cffi >=1.11.5', 'greenlet >=0.4.13'] + depends: ['cffi_not_really >=1.11.5', 'greenlet >=0.4.13'] - name: gevent version: 1.3.5 - depends: ['cffi >=1.11.5', 'greenlet >=0.4.13'] + depends: ['cffi_not_really >=1.11.5', 'greenlet >=0.4.13'] - glob2 0.5 - glob2 0.6 - gmp 6.1.2 diff --git a/tests/yaml/large.yml b/tests/yaml/large.yml index 6076ce2aad5..8487870919b 100644 --- a/tests/yaml/large.yml +++ b/tests/yaml/large.yml @@ -31,16 +31,19 @@ base: - certifi 2018.1.18 - certifi 2018.4.16 - certifi 2018.8.13 - - name: cffi + # cffi is a bundled module in PyPy and causes resolution errors if pip + # tries to installed it. Give it a different name since we are simply + # checking the graph anyway and the identifier doesn't really matter. + - name: cffi_not_really version: 1.10.0 depends: ['pycparser'] - - name: cffi + - name: cffi_not_really version: 1.11.2 depends: ['pycparser'] - - name: cffi + - name: cffi_not_really version: 1.11.4 depends: ['pycparser'] - - name: cffi + - name: cffi_not_really version: 1.11.5 depends: ['pycparser'] - chardet 3.0.4 @@ -51,19 +54,19 @@ base: - contextlib2 0.5.5 - name: cryptography version: 2.0.3 - depends: ['asn1crypto >=0.21.0', 'cffi >=1.7', 'idna >=2.1', 'six >=1.4.1'] + depends: ['asn1crypto >=0.21.0', 'cffi_not_really >=1.7', 'idna >=2.1', 'six >=1.4.1'] - name: cryptography version: 2.1.3 - depends: ['asn1crypto >=0.21.0', 'cffi >=1.7', 'idna >=2.1', 'six >=1.4.1'] + depends: ['asn1crypto >=0.21.0', 'cffi_not_really >=1.7', 'idna >=2.1', 'six >=1.4.1'] - name: cryptography version: 2.1.4 - depends: ['asn1crypto >=0.21.0', 'cffi >=1.7', 'idna >=2.1', 'six >=1.4.1'] + depends: ['asn1crypto >=0.21.0', 'cffi_not_really >=1.7', 'idna >=2.1', 'six >=1.4.1'] - name: cryptography version: 2.2.1 - depends: ['asn1crypto >=0.21.0', 'cffi >=1.7', 'idna >=2.1', 'six >=1.4.1'] + depends: ['asn1crypto >=0.21.0', 'cffi_not_really >=1.7', 'idna >=2.1', 'six >=1.4.1'] - name: cryptography version: '2.3' - depends: ['asn1crypto >=0.21.0', 'cffi >=1.7', 'cryptography_vectors ~=2.3', 'idna >=2.1', 'six >=1.4.1'] + depends: ['asn1crypto >=0.21.0', 'cffi_not_really >=1.7', 'cryptography_vectors ~=2.3', 'idna >=2.1', 'six >=1.4.1'] - cryptography_vectors 2.0.3 - cryptography_vectors 2.1.3 - cryptography_vectors 2.1.4 @@ -242,7 +245,7 @@ cases: response: - state: - asn1crypto 0.24.0 - - cffi 1.11.5 + - cffi_not_really 1.11.5 - cryptography 2.3 - cryptography_vectors 2.3.0 - idna 2.7 @@ -257,7 +260,7 @@ cases: - asn1crypto 0.24.0 - cachecontrol 0.12.5 - certifi 2018.8.13 - - cffi 1.11.5 + - cffi_not_really 1.11.5 - chardet 3.0.4 - cryptography 2.3 - cryptography_vectors 2.3.0 From 5ee2eeda21363df80b623ce15aec2760d8dc749c Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 28 May 2020 11:18:41 +0800 Subject: [PATCH 2088/3170] Make the failing tests fail --- tests/functional/test_install.py | 1 - 1 file changed, 1 deletion(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index b19020108bd..de48684054d 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1797,7 +1797,6 @@ def test_valid_index_url_argument(script, shared_data): assert 'Successfully installed Dinner' in result.stdout, str(result) -@pytest.mark.fails_on_new_resolver def test_install_yanked_file_and_print_warning(script, data): """ Test install a "yanked" file and print a warning. From 439da2f902016b33691eced0be377d8fca465f77 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 28 May 2020 11:18:47 +0800 Subject: [PATCH 2089/3170] Implement yanked link warning in the new resolver --- .../_internal/resolution/resolvelib/base.py | 6 +++ .../resolution/resolvelib/candidates.py | 40 ++++++++++++++++--- .../resolution/resolvelib/resolver.py | 15 +++++++ 3 files changed, 56 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/base.py b/src/pip/_internal/resolution/resolvelib/base.py index 03d297ba2e7..0a1235a72f7 100644 --- a/src/pip/_internal/resolution/resolvelib/base.py +++ b/src/pip/_internal/resolution/resolvelib/base.py @@ -7,6 +7,7 @@ from pip._vendor.packaging.version import _BaseVersion + from pip._internal.models.link import Link from pip._internal.req.req_install import InstallRequirement CandidateLookup = Tuple[ @@ -54,6 +55,11 @@ def is_installed(self): # type: () -> bool raise NotImplementedError("Override in subclass") + @property + def source_link(self): + # type: () -> Optional[Link] + raise NotImplementedError("Override in subclass") + def iter_dependencies(self): # type: () -> Iterable[Optional[Requirement]] raise NotImplementedError("Override in subclass") diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 46c01a064a8..e4ed2512cc9 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -106,19 +106,34 @@ def make_install_req_from_dist(dist, template): class _InstallRequirementBackedCandidate(Candidate): - # These are not installed + """A candidate backed by an ``InstallRequirement``. + + This represents a package request with the target not being already + in the environment, and needs to be fetched and installed. The backing + ``InstallRequirement`` is responsible for most of the leg work; this + class exposes appropriate information to the resolver. + + :param link: The link passed to the ``InstallRequirement``. The backing + ``InstallRequirement`` will use this link to fetch the distribution. + :param source_link: The link this candidate "originates" from. This is + different from ``link`` when the link is found in the wheel cache. + ``link`` would point to the wheel cache, while this points to the + found remote link (e.g. from pypi.org). + """ is_installed = False def __init__( self, link, # type: Link + source_link, # type: Link ireq, # type: InstallRequirement factory, # type: Factory name=None, # type: Optional[str] version=None, # type: Optional[_BaseVersion] ): # type: (...) -> None - self.link = link + self._link = link + self._source_link = source_link self._factory = factory self._ireq = ireq self._name = name @@ -129,17 +144,17 @@ def __repr__(self): # type: () -> str return "{class_name}({link!r})".format( class_name=self.__class__.__name__, - link=str(self.link), + link=str(self._link), ) def __hash__(self): # type: () -> int - return hash((self.__class__, self.link)) + return hash((self.__class__, self._link)) def __eq__(self, other): # type: (Any) -> bool if isinstance(other, self.__class__): - return self.link == other.link + return self._link == other._link return False # Needed for Python 2, which does not implement this by default @@ -147,6 +162,11 @@ def __ne__(self, other): # type: (Any) -> bool return not self.__eq__(other) + @property + def source_link(self): + # type: () -> Optional[Link] + return self._source_link + @property def name(self): # type: () -> str @@ -234,6 +254,7 @@ def __init__( version=None, # type: Optional[_BaseVersion] ): # type: (...) -> None + source_link = link cache_entry = factory.get_wheel_cache_entry(link, name) if cache_entry is not None: logger.debug("Using cached wheel link: %s", cache_entry.link) @@ -247,6 +268,7 @@ def __init__( super(LinkCandidate, self).__init__( link=link, + source_link=source_link, ireq=ireq, factory=factory, name=name, @@ -272,6 +294,7 @@ def __init__( # type: (...) -> None super(EditableCandidate, self).__init__( link=link, + source_link=link, ireq=make_install_req_from_editable(link, template), factory=factory, name=name, @@ -285,6 +308,7 @@ def _prepare_abstract_distribution(self): class AlreadyInstalledCandidate(Candidate): is_installed = True + source_link = None def __init__( self, @@ -418,6 +442,11 @@ def is_installed(self): # type: () -> _BaseVersion return self.base.is_installed + @property + def source_link(self): + # type: () -> Optional[Link] + return self.base.source_link + def iter_dependencies(self): # type: () -> Iterable[Optional[Requirement]] factory = self.base._factory @@ -455,6 +484,7 @@ def get_install_requirement(self): class RequiresPythonCandidate(Candidate): is_installed = False + source_link = None def __init__(self, py_version_info): # type: (Optional[Tuple[int, ...]]) -> None diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 0417852817c..6e6e3caef87 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -167,6 +167,21 @@ def resolve(self, root_reqs, check_supported_wheels): ireq = candidate.get_install_requirement() if ireq is None: continue + link = candidate.source_link + if link and link.is_yanked: + # The reason can contain non-ASCII characters, Unicode + # is required for Python 2. + msg = ( + u'The candidate selected for download or install is a ' + u'yanked version: {name!r} candidate (version {version} ' + u'at {link})\nReason for being yanked: {reason}' + ).format( + name=candidate.name, + version=candidate.version, + link=link, + reason=link.yanked_reason or u'<none given>', + ) + logger.warning(msg) ireq.should_reinstall = self.factory.should_reinstall(candidate) req_set.add_named_requirement(ireq) From 78db0ba40b83a89e51d27f7993319cc9da4859cb Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 16 Jun 2020 14:53:07 +0800 Subject: [PATCH 2090/3170] Set the requirement context on hash error This improves the message shown by the hash error to include the requirement that caused it. --- src/pip/_internal/resolution/resolvelib/candidates.py | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 46c01a064a8..6d343458eba 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -5,7 +5,7 @@ from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.packaging.version import Version -from pip._internal.exceptions import MetadataInconsistent +from pip._internal.exceptions import HashError, MetadataInconsistent from pip._internal.req.constructors import ( install_req_from_editable, install_req_from_line, @@ -171,7 +171,12 @@ def _prepare(self): if self._dist is not None: return - abstract_dist = self._prepare_abstract_distribution() + try: + abstract_dist = self._prepare_abstract_distribution() + except HashError as e: + e.req = self._ireq + raise + self._dist = abstract_dist.get_pkg_resources_distribution() assert self._dist is not None, "Distribution already installed" From dd42e7ec258f2a8b8006068a8663bc3f51cb0a87 Mon Sep 17 00:00:00 2001 From: Jussi Kukkonen <jkukkonen@vmware.com> Date: Tue, 16 Jun 2020 13:43:21 +0300 Subject: [PATCH 2091/3170] test server: Don't mask invalid signals on py38 This removes warnings from test output in python 3.8: /usr/lib/python3.8/signal.py:60: RuntimeWarning: invalid signal number 32, please use valid_signals() --- news/a44cd47c-e34b-4679-b80e-9c543d4c63a6.trivial | 0 tests/lib/server.py | 11 ++++++++--- 2 files changed, 8 insertions(+), 3 deletions(-) create mode 100644 news/a44cd47c-e34b-4679-b80e-9c543d4c63a6.trivial diff --git a/news/a44cd47c-e34b-4679-b80e-9c543d4c63a6.trivial b/news/a44cd47c-e34b-4679-b80e-9c543d4c63a6.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/lib/server.py b/tests/lib/server.py index bb423a2d867..17cf758308e 100644 --- a/tests/lib/server.py +++ b/tests/lib/server.py @@ -42,9 +42,14 @@ class MockServer(BaseWSGIServer): def blocked_signals(): """Block all signals for e.g. starting a worker thread. """ - old_mask = signal.pthread_sigmask( - signal.SIG_SETMASK, range(1, signal.NSIG) - ) + # valid_signals() was added in Python 3.8 (and not using it results + # in a warning on pthread_sigmask() call) + try: + mask = signal.valid_signals() + except AttributeError: + mask = range(1, signal.NSIG) + + old_mask = signal.pthread_sigmask(signal.SIG_SETMASK, mask) try: yield finally: From 27521ac1f2ec75194089e6b28450d0d93a9ca2f6 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Wed, 3 Jun 2020 15:29:36 +0100 Subject: [PATCH 2092/3170] Remove duplicates from the list of conflicts in ResolutionImpossible reporting --- src/pip/_internal/resolution/resolvelib/resolver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 0417852817c..a73966db091 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -158,7 +158,7 @@ def resolve(self, root_reqs, check_supported_wheels): ) raise DistributionNotFound( "No matching distribution found for " + - ", ".join([r.name for r, _ in e.causes]) + ", ".join(sorted(set(r.name for r, _ in e.causes))) ) six.raise_from(error, e) From c827f2964ce6b3b999d3e8a491f9178392489b41 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Wed, 3 Jun 2020 16:21:23 +0100 Subject: [PATCH 2093/3170] Revised ResolutionImpossible message --- .../resolution/resolvelib/resolver.py | 51 +++++++++++++++---- 1 file changed, 40 insertions(+), 11 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index a73966db091..dd4439c2405 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -143,19 +143,48 @@ def resolve(self, root_reqs, check_supported_wheels): except ResolutionImpossible as e: error = self.factory.get_installation_error(e) if not error: - # TODO: This needs fixing, we need to look at the - # factory.get_installation_error infrastructure, as that - # doesn't really allow for the logger.critical calls I'm - # using here. + # > pip install ward==0.44.1b0 py2neo==4.3.0 --unstable-feature=resolver + # + # ERROR: Cannot install ward v0.44.1b0 and py2neo v4.3.0 because these package versions have conflicting dependencies. + # The conflict is caused by: + # ward 0.44.1b0 depends on pygments <3.0.0,>=2.4.2 + # py2neo 4.3.0 depends on pygments ~=2.3.1 + # + # There are a number of possible solutions. For instructions on how to do these steps visit: https://pypa.io/SomeLink + + def text_join(parts): + # type: (List[str]) -> str + return ", ".join(parts[:-1]) + " and " + parts[-1] + def readable_form(req): + # type: (InstallRequirement) -> str + return "{} {}".format(req.name, req.version) + + # TODO: I haven't considered what happens if `parent` is None. + # I'm not even sure how that case arises... + + logger.critical( + "Cannot install " + + text_join([ + readable_form(parent) + for req, parent in e.causes + if parent + ]) + + " because these package versions" + + " have conflicting dependencies." + ) + logger.info("The conflict is caused by: ") for req, parent in e.causes: - logger.critical( - "Could not find a version that satisfies " + - "the requirement " + - str(req) + - ("" if parent is None else " (from {})".format( - parent.name - )) + logger.info( + " " + + readable_form(parent) + + " depends on " + + str(req) ) + logger.info( + "There are a number of possible solutions. " + + "For instructions on how to do these steps visit: " + + "https://pypa.io/SomeLink" + ) raise DistributionNotFound( "No matching distribution found for " + ", ".join(sorted(set(r.name for r, _ in e.causes))) From 09d311594eb5a3a12b483b430b56163b86d8c2f6 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Thu, 4 Jun 2020 13:15:30 +0100 Subject: [PATCH 2094/3170] Move error handling to factory.get_installation_error() --- .../resolution/resolvelib/factory.py | 73 ++++++++++++++++++- .../resolution/resolvelib/resolver.py | 49 +------------ tests/functional/test_new_resolver.py | 4 +- 3 files changed, 73 insertions(+), 53 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 9c5fa4f2380..5197518a74b 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -5,6 +5,7 @@ from pip._vendor.packaging.utils import canonicalize_name from pip._internal.exceptions import ( + DistributionNotFound, InstallationError, UnsupportedPythonVersion, ) @@ -343,11 +344,79 @@ def _report_requires_python_error( return UnsupportedPythonVersion(message) def get_installation_error(self, e): - # type: (ResolutionImpossible) -> Optional[InstallationError] + # type: (ResolutionImpossible) -> InstallationError + + assert e.causes, "Installation error reported with no cause" + + # If one of the things we can't solve is "we need Python X.Y", + # that is what we report. for cause in e.causes: if isinstance(cause.requirement, RequiresPythonRequirement): return self._report_requires_python_error( cause.requirement, cause.parent, ) - return None + + # Otherwise, we have a set of causes which can't all be satisfied + # at once. + + # The simplest case is when we have *one* cause that can't be + # satisfied. We just report that case. + if len(e.causes) == 1: + req, parent = e.causes[0] + logger.critical( + "Could not find a version that satisfies " + + "the requirement " + + str(req) + + ("" if parent is None else " (from {})".format( + parent.name + )) + ) + return DistributionNotFound( + 'No matching distribution found for {}'.format(req) + ) + + # OK, we now have a list of requirements that can't all be + # satisfied at once. + + # A couple of formatting helpers + def text_join(parts): + # type: (List[str]) -> str + if len(parts) == 1: + return parts[0] + + return ", ".join(parts[:-1]) + " and " + parts[-1] + + def readable_form(cand): + # type: (Candidate) -> str + return "{} {}".format(cand.name, cand.version) + + msg = "Cannot install {} because these package versions " \ + "have conflicting dependencies.".format( + text_join([ + readable_form(parent) + for req, parent in e.causes + if parent + ]) + ) + + msg = msg + "\nThe conflict is caused by:" + for req, parent in e.causes: + msg = msg + "\n " + if parent: + msg = msg + readable_form(parent) + " depends on " + else: + msg = msg + "The user requested " + msg = msg + str(req) + + msg = msg + "\n\n" + \ + "There are a number of possible solutions. " + \ + "For instructions on how to do these steps visit: " + \ + "https://pypa.io/SomeLink" + + logger.critical(msg) + + return DistributionNotFound( + "No matching distribution found for " + + ", ".join(sorted(set(r.name for r, _ in e.causes))) + ) diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index dd4439c2405..38bfaeb35ea 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -6,7 +6,7 @@ from pip._vendor.resolvelib import BaseReporter, ResolutionImpossible from pip._vendor.resolvelib import Resolver as RLResolver -from pip._internal.exceptions import DistributionNotFound, InstallationError +from pip._internal.exceptions import InstallationError from pip._internal.req.req_set import RequirementSet from pip._internal.resolution.base import BaseResolver from pip._internal.resolution.resolvelib.provider import PipProvider @@ -142,53 +142,6 @@ def resolve(self, root_reqs, check_supported_wheels): except ResolutionImpossible as e: error = self.factory.get_installation_error(e) - if not error: - # > pip install ward==0.44.1b0 py2neo==4.3.0 --unstable-feature=resolver - # - # ERROR: Cannot install ward v0.44.1b0 and py2neo v4.3.0 because these package versions have conflicting dependencies. - # The conflict is caused by: - # ward 0.44.1b0 depends on pygments <3.0.0,>=2.4.2 - # py2neo 4.3.0 depends on pygments ~=2.3.1 - # - # There are a number of possible solutions. For instructions on how to do these steps visit: https://pypa.io/SomeLink - - def text_join(parts): - # type: (List[str]) -> str - return ", ".join(parts[:-1]) + " and " + parts[-1] - def readable_form(req): - # type: (InstallRequirement) -> str - return "{} {}".format(req.name, req.version) - - # TODO: I haven't considered what happens if `parent` is None. - # I'm not even sure how that case arises... - - logger.critical( - "Cannot install " + - text_join([ - readable_form(parent) - for req, parent in e.causes - if parent - ]) + - " because these package versions" + - " have conflicting dependencies." - ) - logger.info("The conflict is caused by: ") - for req, parent in e.causes: - logger.info( - " " + - readable_form(parent) + - " depends on " + - str(req) - ) - logger.info( - "There are a number of possible solutions. " + - "For instructions on how to do these steps visit: " + - "https://pypa.io/SomeLink" - ) - raise DistributionNotFound( - "No matching distribution found for " + - ", ".join(sorted(set(r.name for r, _ in e.causes))) - ) six.raise_from(error, e) req_set = RequirementSet(check_supported_wheels=check_supported_wheels) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 4874874e788..513a2fff4d2 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -282,9 +282,7 @@ def test_new_resolver_no_dist_message(script): assert "Could not find a version that satisfies the requirement B" \ in result.stderr, str(result) - # TODO: This reports the canonical name of the project. But the current - # resolver reports the originally specified name (i.e. uppercase B) - assert "No matching distribution found for b" in result.stderr, str(result) + assert "No matching distribution found for B" in result.stderr, str(result) def test_new_resolver_installs_editable(script): From 2795742b316ca10217289f4041eb338ad44eaa8b Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Fri, 5 Jun 2020 12:56:43 +0100 Subject: [PATCH 2095/3170] Add handling of inconsistent root requirements --- .../_internal/resolution/resolvelib/base.py | 8 ++++++ .../resolution/resolvelib/candidates.py | 23 +++++++++++++++++ .../resolution/resolvelib/factory.py | 25 +++++++++++-------- .../resolution/resolvelib/requirements.py | 12 +++++++++ tests/functional/test_new_resolver.py | 14 +++++++++++ 5 files changed, 71 insertions(+), 11 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/base.py b/src/pip/_internal/resolution/resolvelib/base.py index 03d297ba2e7..fd13eec9b4f 100644 --- a/src/pip/_internal/resolution/resolvelib/base.py +++ b/src/pip/_internal/resolution/resolvelib/base.py @@ -37,6 +37,10 @@ def get_candidate_lookup(self): # type: () -> CandidateLookup raise NotImplementedError("Subclass should override") + def format_for_error(self): + # type: () -> str + raise NotImplementedError("Subclass should override") + class Candidate(object): @property @@ -61,3 +65,7 @@ def iter_dependencies(self): def get_install_requirement(self): # type: () -> Optional[InstallRequirement] raise NotImplementedError("Override in subclass") + + def format_for_error(self): + # type: () -> str + raise NotImplementedError("Subclass should override") diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 46c01a064a8..18bd34ee6f0 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -162,6 +162,14 @@ def version(self): self._version = self.dist.parsed_version return self._version + def format_for_error(self): + # type: () -> str + return "{} {} (from {})".format( + self.name, + self.version, + self.link.file_path + ) + def _prepare_abstract_distribution(self): # type: () -> AbstractDistribution raise NotImplementedError("Override in subclass") @@ -336,6 +344,10 @@ def version(self): # type: () -> _BaseVersion return self.dist.parsed_version + def format_for_error(self): + # type: () -> str + return "{} {} (Installed)".format(self.name, self.version) + def iter_dependencies(self): # type: () -> Iterable[Optional[Requirement]] for r in self.dist.requires(): @@ -413,6 +425,13 @@ def version(self): # type: () -> _BaseVersion return self.base.version + def format_for_error(self): + # type: () -> str + return "{} [{}]".format( + self.base.format_for_error(), + ", ".join(sorted(self.extras)) + ) + @property def is_installed(self): # type: () -> _BaseVersion @@ -479,6 +498,10 @@ def version(self): # type: () -> _BaseVersion return self._version + def format_for_error(self): + # type: () -> str + return "Python {}".format(self.version) + def iter_dependencies(self): # type: () -> Iterable[Optional[Requirement]] return () diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 5197518a74b..4ad950c9eeb 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -391,23 +391,26 @@ def readable_form(cand): # type: (Candidate) -> str return "{} {}".format(cand.name, cand.version) - msg = "Cannot install {} because these package versions " \ - "have conflicting dependencies.".format( - text_join([ - readable_form(parent) - for req, parent in e.causes - if parent - ]) - ) - - msg = msg + "\nThe conflict is caused by:" + if any(parent for _, parent in e.causes): + msg = "Cannot install {} because these package versions " \ + "have conflicting dependencies.".format( + text_join([ + readable_form(parent) + for req, parent in e.causes + if parent + ]) + ) + msg = msg + "\nThe conflict is caused by:" + else: + msg = "The following requirements are inconsistent:" + for req, parent in e.causes: msg = msg + "\n " if parent: msg = msg + readable_form(parent) + " depends on " else: msg = msg + "The user requested " - msg = msg + str(req) + msg = msg + req.format_for_error() msg = msg + "\n\n" + \ "There are a number of possible solutions. " + \ diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py index a10df94940c..83fb158417b 100644 --- a/src/pip/_internal/resolution/resolvelib/requirements.py +++ b/src/pip/_internal/resolution/resolvelib/requirements.py @@ -30,6 +30,10 @@ def name(self): # No need to canonicalise - the candidate did this return self.candidate.name + def format_for_error(self): + # type: () -> str + return self.candidate.format_for_error() + def get_candidate_lookup(self): # type: () -> CandidateLookup return self.candidate, None @@ -63,6 +67,10 @@ def name(self): canonical_name = canonicalize_name(self._ireq.req.name) return format_name(canonical_name, self._extras) + def format_for_error(self): + # type: () -> str + return str(self) + def get_candidate_lookup(self): # type: () -> CandidateLookup return None, self._ireq @@ -99,6 +107,10 @@ def name(self): # type: () -> str return self._candidate.name + def format_for_error(self): + # type: () -> str + return "Python " + str(self.specifier) + def get_candidate_lookup(self): # type: () -> CandidateLookup if self.specifier.contains(self._candidate.version, prereleases=True): diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 513a2fff4d2..001ff033884 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -925,3 +925,17 @@ def test_new_resolver_upgrade_same_version(script): "pkg", ) assert_installed(script, pkg="2") + + +def test_new_resolver_local_and_req(script): + source_dir = create_test_package_with_setup( + script, + name="pkg", + version="0.1.0", + ) + script.pip( + "install", "--unstable-feature=resolver", + "--no-cache-dir", "--no-index", + source_dir, "pkg!=0.1.0", + expect_error=True, + ) From a42ed23adc17f7d2b3265a08d07de34840ac1100 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Fri, 5 Jun 2020 13:56:32 +0100 Subject: [PATCH 2096/3170] Fix format_for_error for remote URL candidates --- src/pip/_internal/resolution/resolvelib/candidates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 18bd34ee6f0..76f83d63214 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -167,7 +167,7 @@ def format_for_error(self): return "{} {} (from {})".format( self.name, self.version, - self.link.file_path + self.link.file_path if self.link.is_file else self.link ) def _prepare_abstract_distribution(self): From 6bb0d289f8e6a4109b8907f1e3b7b30e389b1b29 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Sun, 7 Jun 2020 16:15:05 +0700 Subject: [PATCH 2097/3170] Move link log from prepare_linked_requirement --- ...feb941-3e4e-4b33-bd43-8c47f67ea229.trivial | 0 src/pip/_internal/operations/prepare.py | 31 +++++++++---------- 2 files changed, 14 insertions(+), 17 deletions(-) create mode 100644 news/0cfeb941-3e4e-4b33-bd43-8c47f67ea229.trivial diff --git a/news/0cfeb941-3e4e-4b33-bd43-8c47f67ea229.trivial b/news/0cfeb941-3e4e-4b33-bd43-8c47f67ea229.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index d8a4bde5ca0..3dc583cc253 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -376,29 +376,26 @@ def _download_should_save(self): "Could not find or access download directory '{}'" .format(self.download_dir)) - def prepare_linked_requirement( - self, - req, # type: InstallRequirement - parallel_builds=False, # type: bool - ): - # type: (...) -> AbstractDistribution - """Prepare a requirement that would be obtained from req.link - """ - assert req.link - link = req.link - - # TODO: Breakup into smaller functions - if link.scheme == 'file': - path = link.file_path + def _log_preparing_link(self, req): + # type: (InstallRequirement) -> None + """Log the way the link prepared.""" + if req.link.is_file: + path = req.link.file_path logger.info('Processing %s', display_path(path)) else: logger.info('Collecting %s', req.req or req) - download_dir = self.download_dir + def prepare_linked_requirement(self, req, parallel_builds=False): + # type: (InstallRequirement, bool) -> AbstractDistribution + """Prepare a requirement to be obtained from req.link.""" + assert req.link + link = req.link + self._log_preparing_link(req) if link.is_wheel and self.wheel_download_dir: - # when doing 'pip wheel` we download wheels to a - # dedicated dir. + # Download wheels to a dedicated dir when doing `pip wheel`. download_dir = self.wheel_download_dir + else: + download_dir = self.download_dir if link.is_wheel: if download_dir: From eb2deab2f64664553db95ff6fbd03103445dfe78 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Fri, 12 Jun 2020 11:49:09 +0700 Subject: [PATCH 2098/3170] Move req.source_dir ensure out --- src/pip/_internal/operations/prepare.py | 72 +++++++++++++------------ 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 3dc583cc253..b499db7bbed 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -385,6 +385,42 @@ def _log_preparing_link(self, req): else: logger.info('Collecting %s', req.req or req) + def _ensure_link_req_src_dir(self, req, download_dir, parallel_builds): + # type: (InstallRequirement, Optional[str], bool) -> None + """Ensure source_dir of a linked InstallRequirement.""" + # Since source_dir is only set for editable requirements. + 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 + else: + # We always delete unpacked sdists after pip runs. + autodelete_unpacked = True + assert req.source_dir is None + req.ensure_has_source_dir( + self.build_dir, + autodelete=autodelete_unpacked, + parallel_builds=parallel_builds, + ) + + # 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` + if os.path.exists(os.path.join(req.source_dir, 'setup.py')): + raise PreviousBuildDirError( + "pip can't proceed with requirements '{}' due to a" + "pre-existing build directory ({}). 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.".format(req, req.source_dir) + ) + def prepare_linked_requirement(self, req, parallel_builds=False): # type: (InstallRequirement, bool) -> AbstractDistribution """Prepare a requirement to be obtained from req.link.""" @@ -397,42 +433,8 @@ def prepare_linked_requirement(self, req, parallel_builds=False): else: download_dir = self.download_dir - if 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 - else: - # We always delete unpacked sdists after pip runs. - autodelete_unpacked = True - with indent_log(): - # Since source_dir is only set for editable requirements. - assert req.source_dir is None - req.ensure_has_source_dir( - self.build_dir, - autodelete=autodelete_unpacked, - parallel_builds=parallel_builds, - ) - # 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` - if os.path.exists(os.path.join(req.source_dir, 'setup.py')): - raise PreviousBuildDirError( - "pip can't proceed with requirements '{}' due to a" - " pre-existing build directory ({}). 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." - .format(req, req.source_dir) - ) - + self._ensure_link_req_src_dir(req, download_dir, parallel_builds) # 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 From 868ba81a74ac8ee58366f6580295824850ae5241 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Thu, 11 Jun 2020 14:22:12 +0100 Subject: [PATCH 2099/3170] Next iteration of message --- .../resolution/resolvelib/factory.py | 55 +++++++++++++------ .../resolution/resolvelib/requirements.py | 13 ++++- 2 files changed, 49 insertions(+), 19 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 4ad950c9eeb..0ab61f726fb 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -391,35 +391,54 @@ def readable_form(cand): # type: (Candidate) -> str return "{} {}".format(cand.name, cand.version) - if any(parent for _, parent in e.causes): - msg = "Cannot install {} because these package versions " \ - "have conflicting dependencies.".format( - text_join([ - readable_form(parent) - for req, parent in e.causes - if parent - ]) - ) - msg = msg + "\nThe conflict is caused by:" + triggers = [] + for req, parent in e.causes: + if parent is None: + # This is a root requirement, so we can report it directly + trigger = req.format_for_error() + else: + ireq = parent.get_install_requirement() + if ireq and ireq.comes_from: + trigger = "{}".format( + ireq.comes_from.name + ) + else: + trigger = "{} {}".format( + parent.name, + parent.version + ) + triggers.append(trigger) + + if triggers: + info = text_join(triggers) else: - msg = "The following requirements are inconsistent:" + info = "the requested packages" + msg = "Cannot install {} because these package versions " \ + "have conflicting dependencies.".format(info) + logger.critical(msg) + msg = "\nThe conflict is caused by:" for req, parent in e.causes: msg = msg + "\n " if parent: - msg = msg + readable_form(parent) + " depends on " + msg = msg + "{} {} depends on ".format( + parent.name, + parent.version + ) else: msg = msg + "The user requested " msg = msg + req.format_for_error() msg = msg + "\n\n" + \ - "There are a number of possible solutions. " + \ - "For instructions on how to do these steps visit: " + \ - "https://pypa.io/SomeLink" + "To fix this you could try to:\n" + \ + "1. loosen the range of package versions you've specified\n" + \ + "2. remove package versions to allow pip attempt to solve " + \ + "the dependency conflict\n" - logger.critical(msg) + logger.info(msg) return DistributionNotFound( - "No matching distribution found for " + - ", ".join(sorted(set(r.name for r, _ in e.causes))) + "ResolutionImpossible For help visit: " + "https://pip.pypa.io/en/stable/user_guide/" + "#dependency-conflicts-resolution-impossible" ) diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py index 83fb158417b..bc1061f4303 100644 --- a/src/pip/_internal/resolution/resolvelib/requirements.py +++ b/src/pip/_internal/resolution/resolvelib/requirements.py @@ -69,7 +69,18 @@ def name(self): def format_for_error(self): # type: () -> str - return str(self) + + # Convert comma-separated specifiers into "A, B, ..., F and G" + # This makes the specifier a bit more "human readable", without + # risking a change in meaning. (Hopefully! Not all edge cases have + # been checked) + parts = [s.strip() for s in str(self).split(",")] + if len(parts) == 0: + return "" + elif len(parts) == 1: + return parts[0] + + return ", ".join(parts[:-1]) + " and " + parts[-1] def get_candidate_lookup(self): # type: () -> CandidateLookup From 8b5ff72a13b29d86ca4e8a5d475f46acd1637d35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Fri, 12 Jun 2020 11:52:17 +0700 Subject: [PATCH 2100/3170] Make linked req hashes an independant method --- src/pip/_internal/operations/prepare.py | 81 +++++++++++-------------- 1 file changed, 37 insertions(+), 44 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index b499db7bbed..07e8de9c729 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -421,6 +421,39 @@ def _ensure_link_req_src_dir(self, req, download_dir, parallel_builds): "Please delete it and try again.".format(req, req.source_dir) ) + def _get_linked_req_hashes(self, req): + # type: (InstallRequirement) -> Hashes + # By the time this is called, the requirement's link should have + # been checked so we can tell what kind of requirements req is + # and raise some more informative errors than otherwise. + # (For example, we can raise VcsHashUnsupported for a VCS URL + # rather than HashMissing.) + if not self.require_hashes: + return req.hashes(trust_internet=True) + + # 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 req.link.is_vcs: + raise VcsHashUnsupported() + if req.link.is_existing_dir(): + raise DirectoryUrlHashUnsupported() + + # 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. + if req.original_link is None and not req.is_pinned: + raise HashUnpinned() + + # If known-good hashes are missing for this requirement, + # 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. + return req.hashes(trust_internet=False) or MissingHashes() + def prepare_linked_requirement(self, req, parallel_builds=False): # type: (InstallRequirement, bool) -> AbstractDistribution """Prepare a requirement to be obtained from req.link.""" @@ -435,49 +468,11 @@ def prepare_linked_requirement(self, req, parallel_builds=False): with indent_log(): self._ensure_link_req_src_dir(req, download_dir, parallel_builds) - # 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 self.require_hashes: - # We could check these first 2 conditions inside - # unpack_url and save repetition of conditions, but then - # we would report less-useful error messages for - # unhashable requirements, complaining that there's no - # hash provided. - if link.is_vcs: - raise VcsHashUnsupported() - elif link.is_existing_dir(): - 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 self.require_hashes) - if self.require_hashes and not hashes: - # Known-good hashes are missing for this requirement, so - # shim it with a facade object that will provoke hash - # computation and then raise a HashMissing exception - # showing the user what the hash should be. - hashes = MissingHashes() - try: local_file = unpack_url( link, req.source_dir, self.downloader, download_dir, - hashes=hashes, - ) + hashes=self._get_linked_req_hashes(req)) except requests.HTTPError as exc: - logger.critical( - 'Could not install requirement %s because of error %s', - req, - exc, - ) raise InstallationError( 'Could not install requirement {} because of HTTP ' 'error {} for URL {}'.format(req, exc, link) @@ -497,13 +492,11 @@ def prepare_linked_requirement(self, req, parallel_builds=False): logger.info('Link is a directory, ignoring download_dir') elif local_file: download_location = os.path.join( - download_dir, link.filename - ) + download_dir, link.filename) if not os.path.exists(download_location): shutil.copy(local_file.path, download_location) - logger.info( - 'Saved %s', display_path(download_location) - ) + download_path = display_path(download_location) + logger.info('Saved %s', download_path) if self._download_should_save: # Make a .zip of the source_dir we already created. From 343f863785bd1477c5bf2b5686577a8da7f455e0 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Wed, 17 Jun 2020 23:20:55 +0530 Subject: [PATCH 2101/3170] Apply suggestions from code review --- src/pip/_internal/operations/prepare.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 07e8de9c729..6f35897d16e 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -471,7 +471,8 @@ def prepare_linked_requirement(self, req, parallel_builds=False): try: local_file = unpack_url( link, req.source_dir, self.downloader, download_dir, - hashes=self._get_linked_req_hashes(req)) + hashes=self._get_linked_req_hashes(req) + ) except requests.HTTPError as exc: raise InstallationError( 'Could not install requirement {} because of HTTP ' @@ -492,7 +493,8 @@ def prepare_linked_requirement(self, req, parallel_builds=False): logger.info('Link is a directory, ignoring download_dir') elif local_file: download_location = os.path.join( - download_dir, link.filename) + download_dir, link.filename + ) if not os.path.exists(download_location): shutil.copy(local_file.path, download_location) download_path = display_path(download_location) From fd3a7850187911132bf4b06497055b1e54511cca Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 18 Jun 2020 17:15:58 +0800 Subject: [PATCH 2102/3170] Reduce regex use when not necessary Co-authored-by: Pradyun Gedam <pradyunsg@gmail.com> --- tests/yaml/large.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/yaml/large.yml b/tests/yaml/large.yml index 8487870919b..62bb0a41804 100644 --- a/tests/yaml/large.yml +++ b/tests/yaml/large.yml @@ -287,7 +287,7 @@ cases: - state: null error: code: 1 - stderr: 'version\s+that\s+satisfies\s+the\s+requirement' + stderr: 'version that satisfies the requirement' skip: old # -- the new resolver tells: # Could not find a version that satisfies the requirement six==1.8.2 From 809eb3cad324fa0cd93e23e3f6025691ba901296 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Thu, 18 Jun 2020 14:01:49 +0100 Subject: [PATCH 2103/3170] Fix a merge issue that didn't get caught by Brown Truck --- src/pip/_internal/resolution/resolvelib/candidates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 3f632d068ae..c7a30e183d3 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -187,7 +187,7 @@ def format_for_error(self): return "{} {} (from {})".format( self.name, self.version, - self.link.file_path if self.link.is_file else self.link + self._link.file_path if self._link.is_file else self._link ) def _prepare_abstract_distribution(self): From 9626dacec504f45376a48e66c2b87e59477fdc10 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Thu, 18 Jun 2020 15:49:18 +0100 Subject: [PATCH 2104/3170] Some tests need to monkeypatch the new resolver internals --- tests/functional/test_install_user.py | 6 +++--- tests/functional/test_uninstall_user.py | 1 - 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/tests/functional/test_install_user.py b/tests/functional/test_install_user.py index 7ac43301afe..8593744db4f 100644 --- a/tests/functional/test_install_user.py +++ b/tests/functional/test_install_user.py @@ -15,13 +15,16 @@ def _patch_dist_in_site_packages(virtualenv): # Since the tests are run from a virtualenv, and to avoid the "Will not # install to the usersite because it will lack sys.path precedence..." # error: Monkey patch `pip._internal.req.req_install.dist_in_site_packages` + # and `pip._internal.resolution.resolvelib.factory.dist_in_site_packages` # so it's possible to install a conflicting distribution in the user site. virtualenv.sitecustomize = textwrap.dedent(""" def dist_in_site_packages(dist): return False from pip._internal.req import req_install + from pip._internal.resolution.resolvelib import factory req_install.dist_in_site_packages = dist_in_site_packages + factory.dist_in_site_packages = dist_in_site_packages """) @@ -126,7 +129,6 @@ def test_install_user_conflict_in_usersite(self, script): @pytest.mark.network @pytest.mark.incompatible_with_test_venv - @pytest.mark.fails_on_new_resolver def test_install_user_conflict_in_globalsite(self, virtualenv, script): """ Test user install with conflict in global site ignores site and @@ -159,7 +161,6 @@ def test_install_user_conflict_in_globalsite(self, virtualenv, script): @pytest.mark.network @pytest.mark.incompatible_with_test_venv - @pytest.mark.fails_on_new_resolver def test_upgrade_user_conflict_in_globalsite(self, virtualenv, script): """ Test user install/upgrade with conflict in global site ignores site and @@ -191,7 +192,6 @@ def test_upgrade_user_conflict_in_globalsite(self, virtualenv, script): @pytest.mark.network @pytest.mark.incompatible_with_test_venv - @pytest.mark.fails_on_new_resolver def test_install_user_conflict_in_globalsite_and_usersite( self, virtualenv, script): """ diff --git a/tests/functional/test_uninstall_user.py b/tests/functional/test_uninstall_user.py index d29e088f617..d73f2cfaf3e 100644 --- a/tests/functional/test_uninstall_user.py +++ b/tests/functional/test_uninstall_user.py @@ -22,7 +22,6 @@ def test_uninstall_from_usersite(self, script): result2 = script.pip('uninstall', '-y', 'INITools') assert_all_changes(result1, result2, [script.venv / 'build', 'cache']) - @pytest.mark.fails_on_new_resolver def test_uninstall_from_usersite_with_dist_in_global_site( self, virtualenv, script): """ From a7e3f8d0886426f90801c471a0ce85ab8ea8a470 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Thu, 18 Jun 2020 18:05:51 +0100 Subject: [PATCH 2105/3170] Test message check fixes --- tests/functional/test_install.py | 7 ++++--- tests/functional/test_install_upgrade.py | 22 +++++++++++++++------- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index de48684054d..4e8b3ca9f9c 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1744,16 +1744,17 @@ def test_user_config_accepted(script): @pytest.mark.parametrize( 'install_args, expected_message', [ ([], 'Requirement already satisfied: pip'), - (['--upgrade'], 'Requirement already up-to-date: pip in'), + (['--upgrade'], 'Requirement already {}: pip in'), ] ) @pytest.mark.parametrize("use_module", [True, False]) -@pytest.mark.fails_on_new_resolver def test_install_pip_does_not_modify_pip_when_satisfied( - script, install_args, expected_message, use_module): + script, install_args, expected_message, use_module, use_new_resolver): """ Test it doesn't upgrade the pip if it already satisfies the requirement. """ + variation = "satisfied" if use_new_resolver else "up-to-date" + expected_message = expected_message.format(variation) result = script.pip_install_local( 'pip', *install_args, use_module=use_module ) diff --git a/tests/functional/test_install_upgrade.py b/tests/functional/test_install_upgrade.py index 1e1ab552265..f4a41bb7c7f 100644 --- a/tests/functional/test_install_upgrade.py +++ b/tests/functional/test_install_upgrade.py @@ -36,8 +36,10 @@ def test_invalid_upgrade_strategy_causes_error(script): assert "invalid choice" in result.stderr -@pytest.mark.fails_on_new_resolver -def test_only_if_needed_does_not_upgrade_deps_when_satisfied(script): +def test_only_if_needed_does_not_upgrade_deps_when_satisfied( + script, + use_new_resolver +): """ It doesn't upgrade a dependency if it already satisfies the requirements. @@ -57,9 +59,12 @@ def test_only_if_needed_does_not_upgrade_deps_when_satisfied(script): .format(**globals())) not in result.files_deleted ), "should not have uninstalled simple==2.0" + + msg = "Requirement already satisfied" + if not use_new_resolver: + msg = msg + ", skipping upgrade: simple" assert ( - "Requirement already satisfied, skipping upgrade: simple" - in result.stdout + msg in result.stdout ), "did not print correct message for not-upgraded requirement" @@ -178,8 +183,7 @@ def test_upgrade_if_requested(script): ) -@pytest.mark.fails_on_new_resolver -def test_upgrade_with_newest_already_installed(script, data): +def test_upgrade_with_newest_already_installed(script, data, use_new_resolver): """ If the newest version of a package is already installed, the package should not be reinstalled and the user should be informed. @@ -189,7 +193,11 @@ def test_upgrade_with_newest_already_installed(script, data): 'install', '--upgrade', '-f', data.find_links, '--no-index', 'simple' ) assert not result.files_created, 'simple upgraded when it should not have' - assert 'already up-to-date' in result.stdout, result.stdout + if use_new_resolver: + msg = "Requirement already satisfied" + else: + msg = "already up-to-date" + assert msg in result.stdout, result.stdout @pytest.mark.network From acab2ee54ed25edd44edbdf793a4b1d173bfbcb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Mon, 1 Jun 2020 12:28:18 +0200 Subject: [PATCH 2106/3170] Deprecate --build-dir --- news/8372.removal | 4 ++++ src/pip/_internal/cli/base_command.py | 14 ++++++++++++++ src/pip/_internal/cli/cmdoptions.py | 3 ++- tests/functional/test_install_cleanup.py | 6 +++++- tests/functional/test_wheel.py | 3 +++ 5 files changed, 28 insertions(+), 2 deletions(-) create mode 100644 news/8372.removal diff --git a/news/8372.removal b/news/8372.removal new file mode 100644 index 00000000000..af0cb6e70c7 --- /dev/null +++ b/news/8372.removal @@ -0,0 +1,4 @@ +Deprecate -b/--build/--build-dir/--build-directory. Its current behaviour is confusing +and breaks in case different versions of the same distribution need to be built during +the resolution process. Using the TMPDIR/TEMP/TMP environment variable, possibly +combined with --no-clean covers known use cases. diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index c523c4e56b5..52027e6e804 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -190,6 +190,20 @@ def _main(self, args): ) options.cache_dir = None + if getattr(options, "build_dir", None): + deprecated( + reason=( + "The -b/--build/--build-dir/--build-directory " + "option is deprecated." + ), + replacement=( + "use the TMPDIR/TEMP/TMP environment variable, " + "possibly combined with --no-clean" + ), + gone_in="20.3", + issue=8333, + ) + try: status = self.run(options, args) assert isinstance(status, int) diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index 120d51eeebe..4c557efa80f 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -702,7 +702,8 @@ def _handle_build_dir(option, opt, value, parser): metavar='dir', action='callback', callback=_handle_build_dir, - help='Directory to unpack packages into and build in. Note that ' + help='(DEPRECATED) ' + '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. ' diff --git a/tests/functional/test_install_cleanup.py b/tests/functional/test_install_cleanup.py index e5d8c4602e5..719c3cc42f0 100644 --- a/tests/functional/test_install_cleanup.py +++ b/tests/functional/test_install_cleanup.py @@ -14,7 +14,11 @@ def test_no_clean_option_blocks_cleaning_after_install(script, data): build = script.base_path / 'pip-build' script.pip( 'install', '--no-clean', '--no-index', '--build', build, - '--find-links={}'.format(data.find_links), 'simple', expect_temp=True, + '--find-links={}'.format(data.find_links), 'simple', + expect_temp=True, + # TODO: allow_stderr_warning is used for the --build deprecation, + # remove it when removing support for --build + allow_stderr_warning=True, ) assert exists(build) diff --git a/tests/functional/test_wheel.py b/tests/functional/test_wheel.py index 4f60e192eda..9fcb180324e 100644 --- a/tests/functional/test_wheel.py +++ b/tests/functional/test_wheel.py @@ -201,6 +201,9 @@ def test_no_clean_option_blocks_cleaning_after_wheel( '--find-links={data.find_links}'.format(**locals()), 'simple', expect_temp=True, + # TODO: allow_stderr_warning is used for the --build deprecation, + # remove it when removing support for --build + allow_stderr_warning=True, ) if not use_new_resolver: From da23209fbebf46949ca685eb631a572bd19a007c Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 23 Jun 2020 20:14:29 +0800 Subject: [PATCH 2107/3170] Expect deprecation warning for build_dir in tests --- tests/functional/test_install_cleanup.py | 6 ++++++ tests/functional/test_wheel.py | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/tests/functional/test_install_cleanup.py b/tests/functional/test_install_cleanup.py index 719c3cc42f0..c01c47c3e3e 100644 --- a/tests/functional/test_install_cleanup.py +++ b/tests/functional/test_install_cleanup.py @@ -41,8 +41,14 @@ def test_cleanup_prevented_upon_build_dir_exception( '--build', build, expect_error=(not use_new_resolver), expect_temp=(not use_new_resolver), + expect_stderr=True, ) + assert ( + "The -b/--build/--build-dir/--build-directory " + "option is deprecated." + ) in result.stderr + if not use_new_resolver: assert result.returncode == PREVIOUS_BUILD_DIR_ERROR, str(result) assert "pip can't proceed" in result.stderr, str(result) diff --git a/tests/functional/test_wheel.py b/tests/functional/test_wheel.py index 9fcb180324e..0b58c923785 100644 --- a/tests/functional/test_wheel.py +++ b/tests/functional/test_wheel.py @@ -252,8 +252,14 @@ def test_pip_wheel_fail_cause_of_previous_build_dir( 'simple==3.0', expect_error=(not use_new_resolver), expect_temp=(not use_new_resolver), + expect_stderr=True, ) + assert ( + "The -b/--build/--build-dir/--build-directory " + "option is deprecated." + ) in result.stderr + # Then I see that the error code is the right one if not use_new_resolver: assert result.returncode == PREVIOUS_BUILD_DIR_ERROR, result From 95477931b2f1c2d9bd0d99d4fad48525ebc902e3 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 23 Jun 2020 20:45:41 +0800 Subject: [PATCH 2108/3170] Mark new resolver test failure on extra union --- tests/functional/test_install_reqs.py | 30 ++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index 12aaa0a4b99..577feb0eabf 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -520,14 +520,34 @@ def test_install_distribution_union_with_constraints( result.did_create(script.site_packages / 'singlemodule.py') -@pytest.mark.fails_on_new_resolver -def test_install_distribution_union_with_versions(script, data): +def test_install_distribution_union_with_versions( + script, + data, + use_new_resolver, +): to_install_001 = data.packages.joinpath("LocalExtras") to_install_002 = data.packages.joinpath("LocalExtras-0.0.2") result = script.pip_install_local( - to_install_001 + "[bar]", to_install_002 + "[baz]") - assert ("Successfully installed LocalExtras-0.0.1 simple-3.0 " + - "singlemodule-0.0.1" in result.stdout) + to_install_001 + "[bar]", + to_install_002 + "[baz]", + expect_error=use_new_resolver, + ) + if use_new_resolver: + assert ( + "Cannot install localextras[bar] 0.0.1 and localextras[baz] 0.0.2 " + "because these package versions have conflicting dependencies." + ) in result.stderr + assert ( + "localextras[bar] 0.0.1 depends on localextras 0.0.1" + ) in result.stdout + assert ( + "localextras[baz] 0.0.2 depends on localextras 0.0.2" + ) in result.stdout + else: + assert ( + "Successfully installed LocalExtras-0.0.1 simple-3.0 " + "singlemodule-0.0.1" + ) in result.stdout @pytest.mark.xfail From ec1f3cc189564ba75db1573256bd47f569c9f282 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 25 Jun 2020 15:00:21 +0800 Subject: [PATCH 2109/3170] Fix YAML tests to reflect the new resolver error --- tests/yaml/conflict_1.yml | 4 +++- tests/yaml/conflict_2.yml | 8 +++----- tests/yaml/conflicting_diamond.yml | 14 +++++--------- tests/yaml/large.yml | 11 +++++------ 4 files changed, 16 insertions(+), 21 deletions(-) diff --git a/tests/yaml/conflict_1.yml b/tests/yaml/conflict_1.yml index b99e033dd58..847bb2b6af5 100644 --- a/tests/yaml/conflict_1.yml +++ b/tests/yaml/conflict_1.yml @@ -36,7 +36,9 @@ cases: - state: null error: code: 1 - stderr: 'no\s+matching\s+distribution' + stderr: >- + Cannot install B==1.0.0 and B==2.0.0 because these + package versions have conflicting dependencies. skip: old # -- currently the (new resolver) error message is: # Could not find a version that satisfies the requirement B==1.0.0 diff --git a/tests/yaml/conflict_2.yml b/tests/yaml/conflict_2.yml index f1f84f95ecd..8a51ad57f95 100644 --- a/tests/yaml/conflict_2.yml +++ b/tests/yaml/conflict_2.yml @@ -22,9 +22,7 @@ cases: response: - state: null error: - stderr: 'no matching distribution found for six' + stderr: >- + Cannot install six<1.12 and virtualenv 20.0.2 because these + package versions have conflicting dependencies. skip: old - # -- currently the error message is: - # Could not find a version that satisfies the requirement six<1.12 - # Could not find a version that satisfies the requirement six<2,>=1.12.0 (from virtualenv) - # No matching distribution found for six, six diff --git a/tests/yaml/conflicting_diamond.yml b/tests/yaml/conflicting_diamond.yml index 4338cb4a07b..0ea5f9ca8d3 100644 --- a/tests/yaml/conflicting_diamond.yml +++ b/tests/yaml/conflicting_diamond.yml @@ -11,13 +11,9 @@ cases: response: - error: code: 1 - stderr: 'no matching distribution found' + stderr: >- + Cannot install A and A because these package + versions have conflicting dependencies. + # TODO: Tweak this error message to make sense. + # https://github.com/pypa/pip/issues/8495 skip: old - # -- currently the error message is: - # Could not find a version that satisfies the requirement D==2.0.0 (from c) - # Could not find a version that satisfies the requirement D==1.0.0 (from b) - # No matching distribution found for d, d - # -- This is a bit confusing, as both versions of D are available. - # -- better would be something like: - # A 1.0.0 -> B 1.0.0 -> D 1.0.0 - # A 1.0.0 -> C 1.0.0 -> D 2.0.0 diff --git a/tests/yaml/large.yml b/tests/yaml/large.yml index 62bb0a41804..0d5f6f3ef86 100644 --- a/tests/yaml/large.yml +++ b/tests/yaml/large.yml @@ -287,10 +287,9 @@ cases: - state: null error: code: 1 - stderr: 'version that satisfies the requirement' + stderr: >- + Cannot install six==1.8.2, html5lib 1.0.1, six==1.8.2 and + html5lib 0.999999999 because these package versions have + conflicting dependencies. + skip: old - # -- the new resolver tells: - # Could not find a version that satisfies the requirement six==1.8.2 - # Could not find a version that satisfies the requirement six>=1.9 (from html5lib) - # -- the old error message (which I think was better): - # html5lib 1.0.1 has requirement six>=1.9, but you'll have six 1.8.2 which is incompatible. From bd70025c989440e6d99c60521abafb74fc0e6387 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Wed, 24 Jun 2020 21:36:15 +0530 Subject: [PATCH 2110/3170] Remove --always-unzip based tests --- tests/functional/test_freeze.py | 2 -- tests/functional/test_uninstall.py | 3 +-- tests/unit/test_req_file.py | 4 ---- 3 files changed, 1 insertion(+), 8 deletions(-) diff --git a/tests/functional/test_freeze.py b/tests/functional/test_freeze.py index 2199bb48214..3792beeca97 100644 --- a/tests/functional/test_freeze.py +++ b/tests/functional/test_freeze.py @@ -540,8 +540,6 @@ def test_freeze_nested_vcs(script, outer_vcs, inner_vcs): # Unchanged requirements below this line -r ignore.txt --requirement ignore.txt - -Z ignore - --always-unzip ignore -f http://ignore -i http://ignore --pre diff --git a/tests/functional/test_uninstall.py b/tests/functional/test_uninstall.py index 4c018a68e31..1f2fe69125b 100644 --- a/tests/functional/test_uninstall.py +++ b/tests/functional/test_uninstall.py @@ -87,8 +87,7 @@ def test_uninstall_easy_install_after_import(script): Uninstall an easy_installed package after it's been imported """ - result = script.easy_install('--always-unzip', 'INITools==0.2', - expect_stderr=True) + result = script.easy_install('INITools==0.2', expect_stderr=True) # the import forces the generation of __pycache__ if the version of python # supports it script.run('python', '-c', "import initools") diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py index 10df385dfde..b22ce20138e 100644 --- a/tests/unit/test_req_file.py +++ b/tests/unit/test_req_file.py @@ -378,10 +378,6 @@ def test_set_finder_trusted_host( ) assert expected in actual - def test_noop_always_unzip(self, line_processor, finder): - # noop, but confirm it can be set - line_processor("--always-unzip", "file", 1, finder=finder) - def test_set_finder_allow_all_prereleases(self, line_processor, finder): line_processor("--pre", "file", 1, finder=finder) assert finder.allow_all_prereleases From 17b1c76ff547d59d6d578bad6c1b9e9cd0ae65ee Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Wed, 24 Jun 2020 21:36:56 +0530 Subject: [PATCH 2111/3170] Remove --always-unzip usage in source code --- src/pip/_internal/cli/cmdoptions.py | 14 -------------- src/pip/_internal/operations/freeze.py | 1 - 2 files changed, 15 deletions(-) diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index 4c557efa80f..516f5b89d0a 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -825,20 +825,6 @@ def _handle_no_use_pep517(option, opt, value, parser): ) # type: Callable[..., Option] -# Deprecated, Remove later -always_unzip = partial( - Option, - '-Z', '--always-unzip', - dest='always_unzip', - action='store_true', - help=SUPPRESS_HELP, -) # type: Callable[..., Option] -# TODO: Move into a class that inherits from partial, currently does not -# work as mypy complains functools.partial is a generic class. -# This way we know we can ignore this option in docs auto generation -setattr(always_unzip, 'deprecated', True) # noqa: B010 - - def _handle_merge_hash(option, opt_str, value, parser): # type: (Option, str, str, OptionParser) -> None """Given a value spelled "algo:digest", append the digest to a list diff --git a/src/pip/_internal/operations/freeze.py b/src/pip/_internal/operations/freeze.py index aa6b052b6aa..9e0ea9696d2 100644 --- a/src/pip/_internal/operations/freeze.py +++ b/src/pip/_internal/operations/freeze.py @@ -96,7 +96,6 @@ def freeze( line.strip().startswith('#') or line.startswith(( '-r', '--requirement', - '-Z', '--always-unzip', '-f', '--find-links', '-i', '--index-url', '--pre', From 3e0b8f18cc233eacee84c3dd9dd4cf3a8316896f Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Wed, 24 Jun 2020 21:37:38 +0530 Subject: [PATCH 2112/3170] Remove --always-unzip option from command-line --- src/pip/_internal/req/req_file.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index cde0b08d6dd..6cce44008ab 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -64,7 +64,6 @@ cmdoptions.require_hashes, cmdoptions.pre, cmdoptions.trusted_host, - cmdoptions.always_unzip, # Deprecated ] # type: List[Callable[..., optparse.Option]] # options to be passed to requirements From cdc4e40dadeacf13771df35d0a3ae5bedc58cea3 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Thu, 18 Jun 2020 12:10:24 +0100 Subject: [PATCH 2113/3170] Test is checking the old resolver's broken behaviour --- tests/functional/test_install.py | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 4e8b3ca9f9c..d8f5b24d6dc 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -927,8 +927,7 @@ def test_install_nonlocal_compatible_wheel(script, data): assert result.returncode == ERROR -@pytest.mark.fails_on_new_resolver -def test_install_nonlocal_compatible_wheel_path(script, data): +def test_install_nonlocal_compatible_wheel_path(script, data, use_new_resolver): target_dir = script.scratch_path / 'target' # Test a full path requirement @@ -937,12 +936,16 @@ def test_install_nonlocal_compatible_wheel_path(script, data): '-t', target_dir, '--no-index', '--only-binary=:all:', - Path(data.packages) / 'simplewheel-2.0-py3-fakeabi-fakeplat.whl' + Path(data.packages) / 'simplewheel-2.0-py3-fakeabi-fakeplat.whl', + expect_error=use_new_resolver ) - assert result.returncode == SUCCESS + if use_new_resolver: + assert result.returncode == ERROR + else: + assert result.returncode == SUCCESS - distinfo = Path('scratch') / 'target' / 'simplewheel-2.0.dist-info' - result.did_create(distinfo) + distinfo = Path('scratch') / 'target' / 'simplewheel-2.0.dist-info' + result.did_create(distinfo) # Test a full path requirement (without --target) result = script.pip( From 3dad80a31400c59ff0c804985f691980f8436ae7 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Thu, 18 Jun 2020 12:12:49 +0100 Subject: [PATCH 2114/3170] Lint fix --- tests/functional/test_install.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index d8f5b24d6dc..5f190769f69 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -927,7 +927,11 @@ def test_install_nonlocal_compatible_wheel(script, data): assert result.returncode == ERROR -def test_install_nonlocal_compatible_wheel_path(script, data, use_new_resolver): +def test_install_nonlocal_compatible_wheel_path( + script, + data, + use_new_resolver +): target_dir = script.scratch_path / 'target' # Test a full path requirement From f1622363606654ca18e27bbcfd13f56f6f40014b Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Thu, 25 Jun 2020 09:14:59 +0100 Subject: [PATCH 2115/3170] Fix deletion of temp dir when exception occurs --- src/pip/_internal/commands/install.py | 96 +++++++++++++-------------- 1 file changed, 48 insertions(+), 48 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index df21e7ceca2..86cbd09c616 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -269,6 +269,7 @@ def run(self, options, args): # Create a target directory for using with the target option target_temp_dir = TempDirectory(kind="target") target_temp_dir_path = target_temp_dir.path + self.enter_context(target_temp_dir) global_options = options.global_options or [] @@ -452,54 +453,53 @@ def _handle_target_dir(self, target_dir, target_temp_dir, upgrade): # 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 - ) + # 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): try: From 134ae32a16786d0944dc6d3c30afd8b49e0e4497 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Fri, 22 May 2020 17:18:27 +0700 Subject: [PATCH 2116/3170] Add utilities for paralleliztion --- ...1d42b8-8277-4918-94eb-031bc7be1c3f.trivial | 0 src/pip/_internal/utils/parallel.py | 65 +++++++++++++++++++ 2 files changed, 65 insertions(+) create mode 100644 news/f91d42b8-8277-4918-94eb-031bc7be1c3f.trivial create mode 100644 src/pip/_internal/utils/parallel.py diff --git a/news/f91d42b8-8277-4918-94eb-031bc7be1c3f.trivial b/news/f91d42b8-8277-4918-94eb-031bc7be1c3f.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/utils/parallel.py b/src/pip/_internal/utils/parallel.py new file mode 100644 index 00000000000..5d236505767 --- /dev/null +++ b/src/pip/_internal/utils/parallel.py @@ -0,0 +1,65 @@ +"""Convenient parallelization of higher order functions.""" + +__all__ = ['map_multiprocess', 'map_multithread'] + +from multiprocessing import Pool as ProcessPool +from multiprocessing.dummy import Pool as ThreadPool + +from pip._vendor.requests.adapters import DEFAULT_POOLSIZE +from pip._vendor.six.moves import map + +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import Callable, Iterable, List, Optional, TypeVar + + S = TypeVar('S') + T = TypeVar('T') + + +def map_multiprocess(func, iterable, chunksize=None, timeout=2000000): + # type: (Callable[[S], T], Iterable[S], Optional[int], int) -> List[T] + """Chop iterable into chunks and submit them to a process pool. + + The (approximate) size of these chunks can be specified + by setting chunksize to a positive integer. + + Block either until the results are ready and return them in a list + or till timeout is reached. By default timeout is an incredibly + large number to work around bpo-8296 on Python 2. + + Note that it may cause high memory usage for long iterables. + """ + try: + pool = ProcessPool() + except ImportError: + return list(map(func, iterable)) + else: + try: + return pool.map_async(func, iterable, chunksize).get(timeout) + finally: + pool.terminate() + + +def map_multithread(func, iterable, chunksize=None, timeout=2000000): + # type: (Callable[[S], T], Iterable[S], Optional[int], int) -> List[T] + """Chop iterable into chunks and submit them to a thread pool. + + The (approximate) size of these chunks can be specified + by setting chunksize to a positive integer. + + Block either until the results are ready and return them in a list + or till timeout is reached. By default timeout is an incredibly + large number to work around bpo-8296 on Python 2. + + Note that it may cause high memory usage for long iterables. + """ + try: + pool = ThreadPool(DEFAULT_POOLSIZE) + except ImportError: + return list(map(func, iterable)) + else: + try: + return pool.map_async(func, iterable, chunksize).get(timeout) + finally: + pool.terminate() From e7f637e5ca7940dc352234e1a224c9b5d987b0e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Mon, 25 May 2020 22:00:04 +0700 Subject: [PATCH 2117/3170] Add tests for utils.parallel --- tests/unit/test_utils_parallel.py | 36 +++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) create mode 100644 tests/unit/test_utils_parallel.py diff --git a/tests/unit/test_utils_parallel.py b/tests/unit/test_utils_parallel.py new file mode 100644 index 00000000000..96c70cc86e7 --- /dev/null +++ b/tests/unit/test_utils_parallel.py @@ -0,0 +1,36 @@ +"""Test multiprocessing/multithreading higher-order functions.""" + +from math import factorial + +from mock import patch +from pip._vendor.six import PY2 +from pip._vendor.six.moves import map +from pytest import mark + +from pip._internal.utils.parallel import map_multiprocess, map_multithread + +DUNDER_IMPORT = '__builtin__.__import__' if PY2 else 'builtins.__import__' +FUNC, ITERABLE = factorial, range(42) + + +def import_sem_open(name, *args, **kwargs): + """Raise ImportError on import of multiprocessing.synchronize.""" + if name.endswith('.synchronize'): + raise ImportError + + +@mark.parametrize('map_async', (map_multiprocess, map_multithread)) +def test_missing_sem_open(map_async, monkeypatch): + """Test fallback when sem_open is not available. + + If so, multiprocessing[.dummy].Pool will fail to be created and + map_async should fallback to map and still return correct result. + """ + with patch(DUNDER_IMPORT, side_effect=import_sem_open): + assert map_async(FUNC, ITERABLE) == list(map(FUNC, ITERABLE)) + + +@mark.parametrize('map_async', (map_multiprocess, map_multithread)) +def test_map_order(map_async): + """Test result ordering of asynchronous maps.""" + assert map_async(FUNC, ITERABLE) == list(map(FUNC, ITERABLE)) From 13539d00f831e0b6d31989c22c7bc56c5d972a05 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Wed, 3 Jun 2020 23:15:34 +0700 Subject: [PATCH 2118/3170] Wrap lazy map as well --- src/pip/_internal/utils/parallel.py | 216 +++++++++++++++++++++++----- tests/unit/test_utils_parallel.py | 62 ++++++-- 2 files changed, 232 insertions(+), 46 deletions(-) diff --git a/src/pip/_internal/utils/parallel.py b/src/pip/_internal/utils/parallel.py index 5d236505767..7e60d300b88 100644 --- a/src/pip/_internal/utils/parallel.py +++ b/src/pip/_internal/utils/parallel.py @@ -1,65 +1,215 @@ -"""Convenient parallelization of higher order functions.""" +"""Convenient parallelization of higher order functions. -__all__ = ['map_multiprocess', 'map_multithread'] +This module provides proper fallback functions for multiprocess +and multithread map, both the non-lazy, ordered variant +and the lazy, unordered variant. +""" +__all__ = ['map_multiprocess', 'imap_multiprocess', + 'map_multithread', 'imap_multithread'] + +from contextlib import contextmanager from multiprocessing import Pool as ProcessPool from multiprocessing.dummy import Pool as ThreadPool from pip._vendor.requests.adapters import DEFAULT_POOLSIZE +from pip._vendor.six import PY2 from pip._vendor.six.moves import map from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Callable, Iterable, List, Optional, TypeVar + from typing import ( + Callable, Iterable, Iterator, List, Optional, Union, TypeVar) + from multiprocessing import pool + Pool = Union[pool.Pool, pool.ThreadPool] S = TypeVar('S') T = TypeVar('T') +# On platforms without sem_open, multiprocessing[.dummy] Pool +# cannot be created. +try: + import multiprocessing.synchronize # noqa +except ImportError: + LACK_SEM_OPEN = True +else: + LACK_SEM_OPEN = False + +# Incredibly large timeout to work around bpo-8296 on Python 2. +TIMEOUT = 2000000 + + +@contextmanager +def closing(pool): + # type: (Pool) -> Iterator[Pool] + """Return a context manager that closes and joins pool. + + This is needed for Pool.imap* to make the result iterator iterate. + """ + try: + yield pool + finally: + pool.close() + pool.join() + + +def _map_fallback(func, iterable, chunksize=None): + # type: (Callable[[S], T], Iterable[S], Optional[int]) -> List[T] + """Return a list of func applied to each element in iterable. + + This function is the sequential fallback when sem_open is unavailable. + """ + return list(map(func, iterable)) + + +def _imap_fallback(func, iterable, chunksize=1): + # type: (Callable[[S], T], Iterable[S], int) -> Iterator[T] + """Make an iterator applying func to each element in iterable. + + This function is the sequential fallback when sem_open is unavailable. + """ + return map(func, iterable) + + +def _map_multiprocess_py2(func, iterable, chunksize=None): + # type: (Callable[[S], T], Iterable[S], Optional[int]) -> List[T] + """Chop iterable into chunks and submit them to a process pool. + + The (approximate) size of these chunks can be specified + by setting chunksize to a positive integer. + + Note that this function may cause high memory usage + for long iterables. + + Return a list of results in order. + """ + pool = ProcessPool() + try: + return pool.map_async(func, iterable, chunksize).get(TIMEOUT) + finally: + pool.terminate() + + +def _map_multiprocess_py3(func, iterable, chunksize=None): + # type: (Callable[[S], T], Iterable[S], Optional[int]) -> List[T] + """Chop iterable into chunks and submit them to a process pool. + + The (approximate) size of these chunks can be specified + by setting chunksize to a positive integer. + + Note that this function may cause high memory usage + for long iterables. + + Return a list of results in order. + """ + with ProcessPool() as pool: + return pool.map(func, iterable, chunksize) + + +def _imap_multiprocess_py2(func, iterable, chunksize=1): + # type: (Callable[[S], T], Iterable[S], int) -> Iterator[T] + """Chop iterable into chunks and submit them to a process pool. + + For very long iterables using a large value for chunksize can make + the job complete much faster than using the default value of 1. + + Return an unordered iterator of the results. + """ + pool = ProcessPool() + try: + return iter(pool.map_async(func, iterable, chunksize).get(TIMEOUT)) + finally: + pool.terminate() -def map_multiprocess(func, iterable, chunksize=None, timeout=2000000): - # type: (Callable[[S], T], Iterable[S], Optional[int], int) -> List[T] + +def _imap_multiprocess_py3(func, iterable, chunksize=1): + # type: (Callable[[S], T], Iterable[S], int) -> Iterator[T] """Chop iterable into chunks and submit them to a process pool. + For very long iterables using a large value for chunksize can make + the job complete much faster than using the default value of 1. + + Return an unordered iterator of the results. + """ + with ProcessPool() as pool, closing(pool): + return pool.imap_unordered(func, iterable, chunksize) + + +def _map_multithread_py2(func, iterable, chunksize=None): + # type: (Callable[[S], T], Iterable[S], Optional[int]) -> List[T] + """Chop iterable into chunks and submit them to a thread pool. + The (approximate) size of these chunks can be specified by setting chunksize to a positive integer. - Block either until the results are ready and return them in a list - or till timeout is reached. By default timeout is an incredibly - large number to work around bpo-8296 on Python 2. + Note that this function may cause high memory usage + for long iterables. - Note that it may cause high memory usage for long iterables. + Return a list of results in order. """ + pool = ThreadPool(DEFAULT_POOLSIZE) try: - pool = ProcessPool() - except ImportError: - return list(map(func, iterable)) - else: - try: - return pool.map_async(func, iterable, chunksize).get(timeout) - finally: - pool.terminate() - - -def map_multithread(func, iterable, chunksize=None, timeout=2000000): - # type: (Callable[[S], T], Iterable[S], Optional[int], int) -> List[T] + return pool.map_async(func, iterable, chunksize).get(TIMEOUT) + finally: + pool.terminate() + + +def _map_multithread_py3(func, iterable, chunksize=None): + # type: (Callable[[S], T], Iterable[S], Optional[int]) -> List[T] """Chop iterable into chunks and submit them to a thread pool. The (approximate) size of these chunks can be specified by setting chunksize to a positive integer. - Block either until the results are ready and return them in a list - or till timeout is reached. By default timeout is an incredibly - large number to work around bpo-8296 on Python 2. + Note that this function may cause high memory usage + for long iterables. - Note that it may cause high memory usage for long iterables. + Return a list of results in order. """ + with ThreadPool(DEFAULT_POOLSIZE) as pool: + return pool.map(func, iterable, chunksize) + + +def _imap_multithread_py2(func, iterable, chunksize=1): + # type: (Callable[[S], T], Iterable[S], int) -> Iterator[T] + """Chop iterable into chunks and submit them to a thread pool. + + For very long iterables using a large value for chunksize can make + the job complete much faster than using the default value of 1. + + Return an unordered iterator of the results. + """ + pool = ThreadPool(DEFAULT_POOLSIZE) try: - pool = ThreadPool(DEFAULT_POOLSIZE) - except ImportError: - return list(map(func, iterable)) - else: - try: - return pool.map_async(func, iterable, chunksize).get(timeout) - finally: - pool.terminate() + return pool.map_async(func, iterable, chunksize).get(TIMEOUT) + finally: + pool.terminate() + + +def _imap_multithread_py3(func, iterable, chunksize=1): + # type: (Callable[[S], T], Iterable[S], int) -> Iterator[T] + """Chop iterable into chunks and submit them to a thread pool. + + For very long iterables using a large value for chunksize can make + the job complete much faster than using the default value of 1. + + Return an unordered iterator of the results. + """ + with ThreadPool(DEFAULT_POOLSIZE) as pool, closing(pool): + return pool.imap_unordered(func, iterable, chunksize) + + +if LACK_SEM_OPEN: + map_multiprocess = map_multithread = _map_fallback + imap_multiprocess = imap_multithread = _imap_fallback +elif PY2: + map_multiprocess = _map_multiprocess_py2 + imap_multiprocess = _imap_multiprocess_py2 + map_multithread = _map_multithread_py2 + imap_multithread = _imap_multithread_py2 +else: + map_multiprocess = _map_multiprocess_py3 + imap_multiprocess = _imap_multiprocess_py3 + map_multithread = _map_multithread_py3 + imap_multithread = _imap_multithread_py3 diff --git a/tests/unit/test_utils_parallel.py b/tests/unit/test_utils_parallel.py index 96c70cc86e7..0afc824bcae 100644 --- a/tests/unit/test_utils_parallel.py +++ b/tests/unit/test_utils_parallel.py @@ -1,36 +1,72 @@ """Test multiprocessing/multithreading higher-order functions.""" +from importlib import import_module from math import factorial +from sys import modules -from mock import patch from pip._vendor.six import PY2 from pip._vendor.six.moves import map from pytest import mark -from pip._internal.utils.parallel import map_multiprocess, map_multithread - DUNDER_IMPORT = '__builtin__.__import__' if PY2 else 'builtins.__import__' FUNC, ITERABLE = factorial, range(42) +MAPS = ('map_multiprocess', 'imap_multiprocess', + 'map_multithread', 'imap_multithread') +_import = __import__ + + +def reload_parallel(): + try: + del modules['pip._internal.utils.parallel'] + finally: + return import_module('pip._internal.utils.parallel') -def import_sem_open(name, *args, **kwargs): +def lack_sem_open(name, *args, **kwargs): """Raise ImportError on import of multiprocessing.synchronize.""" - if name.endswith('.synchronize'): + if name.endswith('synchronize'): raise ImportError + return _import(name, *args, **kwargs) + + +def have_sem_open(name, *args, **kwargs): + """Make sure multiprocessing.synchronize import is successful.""" + if name.endswith('synchronize'): + return + return _import(name, *args, **kwargs) -@mark.parametrize('map_async', (map_multiprocess, map_multithread)) -def test_missing_sem_open(map_async, monkeypatch): +@mark.parametrize('name', MAPS) +def test_lack_sem_open(name, monkeypatch): """Test fallback when sem_open is not available. If so, multiprocessing[.dummy].Pool will fail to be created and - map_async should fallback to map and still return correct result. + map_async should fallback to map. """ - with patch(DUNDER_IMPORT, side_effect=import_sem_open): - assert map_async(FUNC, ITERABLE) == list(map(FUNC, ITERABLE)) + monkeypatch.setattr(DUNDER_IMPORT, lack_sem_open) + parallel = reload_parallel() + fallback = '_{}_fallback'.format(name.split('_')[0]) + assert getattr(parallel, name) is getattr(parallel, fallback) + + +@mark.parametrize('name', MAPS) +def test_have_sem_open(name, monkeypatch): + """Test fallback when sem_open is available.""" + monkeypatch.setattr(DUNDER_IMPORT, have_sem_open) + parallel = reload_parallel() + impl = ('_{}_py2' if PY2 else '_{}_py3').format(name) + assert getattr(parallel, name) is getattr(parallel, impl) + + +@mark.parametrize('name', MAPS) +def test_map(name): + """Test correctness of result of asynchronous maps.""" + map_async = getattr(reload_parallel(), name) + assert set(map_async(FUNC, ITERABLE)) == set(map(FUNC, ITERABLE)) -@mark.parametrize('map_async', (map_multiprocess, map_multithread)) -def test_map_order(map_async): +@mark.parametrize('name', ('map_multiprocess', 'map_multithread')) +def test_map_order(name): """Test result ordering of asynchronous maps.""" - assert map_async(FUNC, ITERABLE) == list(map(FUNC, ITERABLE)) + map_async = getattr(reload_parallel(), name) + assert tuple(map_async(FUNC, ITERABLE)) == tuple(map(FUNC, ITERABLE)) From 0a3b20f9b8514b261eed225c4b635d59bb4a5167 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Sun, 21 Jun 2020 23:25:46 +0700 Subject: [PATCH 2119/3170] Drop parallel map for Python 2 and the non-lazy variant Co-authored-by: Pradyun Gedam <pradyunsg@gmail.com> --- src/pip/_internal/utils/parallel.py | 170 +++++----------------------- tests/unit/test_utils_parallel.py | 22 ++-- 2 files changed, 39 insertions(+), 153 deletions(-) diff --git a/src/pip/_internal/utils/parallel.py b/src/pip/_internal/utils/parallel.py index 7e60d300b88..9fe1fe8b9e4 100644 --- a/src/pip/_internal/utils/parallel.py +++ b/src/pip/_internal/utils/parallel.py @@ -1,12 +1,22 @@ """Convenient parallelization of higher order functions. -This module provides proper fallback functions for multiprocess -and multithread map, both the non-lazy, ordered variant -and the lazy, unordered variant. +This module provides two helper functions, with appropriate fallbacks on +Python 2 and on systems lacking support for synchronization mechanisms: + +- map_multiprocess +- map_multithread + +These helpers work like Python 3's map, with two differences: + +- They don't guarantee the order of processing of + the elements of the iterable. +- The underlying process/thread pools chop the iterable into + a number of chunks, so that for very long iterables using + a large value for chunksize can make the job complete much faster + than using the default value of 1. """ -__all__ = ['map_multiprocess', 'imap_multiprocess', - 'map_multithread', 'imap_multithread'] +__all__ = ['map_multiprocess', 'map_multithread'] from contextlib import contextmanager from multiprocessing import Pool as ProcessPool @@ -19,8 +29,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import ( - Callable, Iterable, Iterator, List, Optional, Union, TypeVar) + from typing import Callable, Iterable, Iterator, Union, TypeVar from multiprocessing import pool Pool = Union[pool.Pool, pool.ThreadPool] @@ -43,87 +52,29 @@ @contextmanager def closing(pool): # type: (Pool) -> Iterator[Pool] - """Return a context manager that closes and joins pool. - - This is needed for Pool.imap* to make the result iterator iterate. - """ + """Return a context manager making sure the pool closes properly.""" try: yield pool finally: + # For Pool.imap*, close and join are needed + # for the returned iterator to begin yielding. pool.close() pool.join() + pool.terminate() -def _map_fallback(func, iterable, chunksize=None): - # type: (Callable[[S], T], Iterable[S], Optional[int]) -> List[T] - """Return a list of func applied to each element in iterable. - - This function is the sequential fallback when sem_open is unavailable. - """ - return list(map(func, iterable)) - - -def _imap_fallback(func, iterable, chunksize=1): +def _map_fallback(func, iterable, chunksize=1): # type: (Callable[[S], T], Iterable[S], int) -> Iterator[T] """Make an iterator applying func to each element in iterable. - This function is the sequential fallback when sem_open is unavailable. + This function is the sequential fallback either on Python 2 + where Pool.imap* doesn't react to KeyboardInterrupt + or when sem_open is unavailable. """ return map(func, iterable) -def _map_multiprocess_py2(func, iterable, chunksize=None): - # type: (Callable[[S], T], Iterable[S], Optional[int]) -> List[T] - """Chop iterable into chunks and submit them to a process pool. - - The (approximate) size of these chunks can be specified - by setting chunksize to a positive integer. - - Note that this function may cause high memory usage - for long iterables. - - Return a list of results in order. - """ - pool = ProcessPool() - try: - return pool.map_async(func, iterable, chunksize).get(TIMEOUT) - finally: - pool.terminate() - - -def _map_multiprocess_py3(func, iterable, chunksize=None): - # type: (Callable[[S], T], Iterable[S], Optional[int]) -> List[T] - """Chop iterable into chunks and submit them to a process pool. - - The (approximate) size of these chunks can be specified - by setting chunksize to a positive integer. - - Note that this function may cause high memory usage - for long iterables. - - Return a list of results in order. - """ - with ProcessPool() as pool: - return pool.map(func, iterable, chunksize) - - -def _imap_multiprocess_py2(func, iterable, chunksize=1): - # type: (Callable[[S], T], Iterable[S], int) -> Iterator[T] - """Chop iterable into chunks and submit them to a process pool. - - For very long iterables using a large value for chunksize can make - the job complete much faster than using the default value of 1. - - Return an unordered iterator of the results. - """ - pool = ProcessPool() - try: - return iter(pool.map_async(func, iterable, chunksize).get(TIMEOUT)) - finally: - pool.terminate() - - -def _imap_multiprocess_py3(func, iterable, chunksize=1): +def _map_multiprocess(func, iterable, chunksize=1): # type: (Callable[[S], T], Iterable[S], int) -> Iterator[T] """Chop iterable into chunks and submit them to a process pool. @@ -132,62 +83,11 @@ def _imap_multiprocess_py3(func, iterable, chunksize=1): Return an unordered iterator of the results. """ - with ProcessPool() as pool, closing(pool): + with closing(ProcessPool()) as pool: return pool.imap_unordered(func, iterable, chunksize) -def _map_multithread_py2(func, iterable, chunksize=None): - # type: (Callable[[S], T], Iterable[S], Optional[int]) -> List[T] - """Chop iterable into chunks and submit them to a thread pool. - - The (approximate) size of these chunks can be specified - by setting chunksize to a positive integer. - - Note that this function may cause high memory usage - for long iterables. - - Return a list of results in order. - """ - pool = ThreadPool(DEFAULT_POOLSIZE) - try: - return pool.map_async(func, iterable, chunksize).get(TIMEOUT) - finally: - pool.terminate() - - -def _map_multithread_py3(func, iterable, chunksize=None): - # type: (Callable[[S], T], Iterable[S], Optional[int]) -> List[T] - """Chop iterable into chunks and submit them to a thread pool. - - The (approximate) size of these chunks can be specified - by setting chunksize to a positive integer. - - Note that this function may cause high memory usage - for long iterables. - - Return a list of results in order. - """ - with ThreadPool(DEFAULT_POOLSIZE) as pool: - return pool.map(func, iterable, chunksize) - - -def _imap_multithread_py2(func, iterable, chunksize=1): - # type: (Callable[[S], T], Iterable[S], int) -> Iterator[T] - """Chop iterable into chunks and submit them to a thread pool. - - For very long iterables using a large value for chunksize can make - the job complete much faster than using the default value of 1. - - Return an unordered iterator of the results. - """ - pool = ThreadPool(DEFAULT_POOLSIZE) - try: - return pool.map_async(func, iterable, chunksize).get(TIMEOUT) - finally: - pool.terminate() - - -def _imap_multithread_py3(func, iterable, chunksize=1): +def _map_multithread(func, iterable, chunksize=1): # type: (Callable[[S], T], Iterable[S], int) -> Iterator[T] """Chop iterable into chunks and submit them to a thread pool. @@ -196,20 +96,12 @@ def _imap_multithread_py3(func, iterable, chunksize=1): Return an unordered iterator of the results. """ - with ThreadPool(DEFAULT_POOLSIZE) as pool, closing(pool): + with closing(ThreadPool(DEFAULT_POOLSIZE)) as pool: return pool.imap_unordered(func, iterable, chunksize) -if LACK_SEM_OPEN: +if LACK_SEM_OPEN or PY2: map_multiprocess = map_multithread = _map_fallback - imap_multiprocess = imap_multithread = _imap_fallback -elif PY2: - map_multiprocess = _map_multiprocess_py2 - imap_multiprocess = _imap_multiprocess_py2 - map_multithread = _map_multithread_py2 - imap_multithread = _imap_multithread_py2 else: - map_multiprocess = _map_multiprocess_py3 - imap_multiprocess = _imap_multiprocess_py3 - map_multithread = _map_multithread_py3 - imap_multithread = _imap_multithread_py3 + map_multiprocess = _map_multiprocess + map_multithread = _map_multithread diff --git a/tests/unit/test_utils_parallel.py b/tests/unit/test_utils_parallel.py index 0afc824bcae..bf42f6bd9e4 100644 --- a/tests/unit/test_utils_parallel.py +++ b/tests/unit/test_utils_parallel.py @@ -10,16 +10,16 @@ DUNDER_IMPORT = '__builtin__.__import__' if PY2 else 'builtins.__import__' FUNC, ITERABLE = factorial, range(42) -MAPS = ('map_multiprocess', 'imap_multiprocess', - 'map_multithread', 'imap_multithread') +MAPS = 'map_multiprocess', 'map_multithread' _import = __import__ def reload_parallel(): try: del modules['pip._internal.utils.parallel'] - finally: - return import_module('pip._internal.utils.parallel') + except KeyError: + pass + return import_module('pip._internal.utils.parallel') def lack_sem_open(name, *args, **kwargs): @@ -31,6 +31,8 @@ def lack_sem_open(name, *args, **kwargs): def have_sem_open(name, *args, **kwargs): """Make sure multiprocessing.synchronize import is successful.""" + # We don't care about the return value + # since we don't use the pool with this import. if name.endswith('synchronize'): return return _import(name, *args, **kwargs) @@ -45,8 +47,7 @@ def test_lack_sem_open(name, monkeypatch): """ monkeypatch.setattr(DUNDER_IMPORT, lack_sem_open) parallel = reload_parallel() - fallback = '_{}_fallback'.format(name.split('_')[0]) - assert getattr(parallel, name) is getattr(parallel, fallback) + assert getattr(parallel, name) is parallel._map_fallback @mark.parametrize('name', MAPS) @@ -54,7 +55,7 @@ def test_have_sem_open(name, monkeypatch): """Test fallback when sem_open is available.""" monkeypatch.setattr(DUNDER_IMPORT, have_sem_open) parallel = reload_parallel() - impl = ('_{}_py2' if PY2 else '_{}_py3').format(name) + impl = '_map_fallback' if PY2 else '_{}'.format(name) assert getattr(parallel, name) is getattr(parallel, impl) @@ -63,10 +64,3 @@ def test_map(name): """Test correctness of result of asynchronous maps.""" map_async = getattr(reload_parallel(), name) assert set(map_async(FUNC, ITERABLE)) == set(map(FUNC, ITERABLE)) - - -@mark.parametrize('name', ('map_multiprocess', 'map_multithread')) -def test_map_order(name): - """Test result ordering of asynchronous maps.""" - map_async = getattr(reload_parallel(), name) - assert tuple(map_async(FUNC, ITERABLE)) == tuple(map(FUNC, ITERABLE)) From cef064dc4b2bee2b5e872410d20500969132080e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Wed, 27 May 2020 16:24:32 +0700 Subject: [PATCH 2120/3170] Require vendored libraries to be MIT-compatibly licensed --- news/06a405e7-85cf-4027-b815-08aef54a600b.trivial | 0 src/pip/_vendor/README.rst | 3 +++ 2 files changed, 3 insertions(+) create mode 100644 news/06a405e7-85cf-4027-b815-08aef54a600b.trivial diff --git a/news/06a405e7-85cf-4027-b815-08aef54a600b.trivial b/news/06a405e7-85cf-4027-b815-08aef54a600b.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_vendor/README.rst b/src/pip/_vendor/README.rst index f9208057672..0194dbcdd8d 100644 --- a/src/pip/_vendor/README.rst +++ b/src/pip/_vendor/README.rst @@ -8,6 +8,9 @@ Vendoring Policy * Vendored libraries **MUST** be released copies of libraries available on PyPI. +* Libraries to be vendored **MUST** be available under a license that allows + them to be integrated into ``pip``, which is released under the MIT license. + * Vendored libraries **MUST** be accompanied with LICENSE files. * The versions of libraries vendored in pip **MUST** be reflected in From be75ed8566b32d942db8d1dd6f99cc8d32d44974 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Wed, 27 May 2020 03:24:27 +0530 Subject: [PATCH 2121/3170] Type annotations for pip._internal.wheel_builder --- src/pip/_internal/wheel_builder.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index 8b6ddad4739..06cedd40d65 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -1,9 +1,6 @@ """Orchestrator for building wheels from InstallRequirements. """ -# The following comment should be removed at some point in the future. -# mypy: strict-optional=False - import logging import os.path import re @@ -134,6 +131,7 @@ def _should_cache( return True return False + assert req.link base, ext = req.link.splitext() if _contains_egg_info(base): return True @@ -151,6 +149,7 @@ def _get_cache_dir( wheel need to be stored. """ cache_available = bool(wheel_cache.cache_dir) + assert req.link if cache_available and _should_cache(req): cache_dir = wheel_cache.get_path_for_link(req.link) else: @@ -198,7 +197,9 @@ def _build_one_inside_env( ): # type: (...) -> Optional[str] with TempDirectory(kind="wheel") as temp_dir: + assert req.name if req.use_pep517: + assert req.metadata_directory wheel_path = build_wheel_pep517( name=req.name, backend=req.pep517_backend, @@ -273,7 +274,7 @@ def build( # Build the wheels. logger.info( 'Building wheels for collected packages: %s', - ', '.join(req.name for req in requirements), + ', '.join(req.name for req in requirements if req.name), ) with indent_log(): @@ -296,12 +297,12 @@ def build( if build_successes: logger.info( 'Successfully built %s', - ' '.join([req.name for req in build_successes]), + ' '.join([req.name for req in build_successes if req.name]), ) if build_failures: logger.info( 'Failed to build %s', - ' '.join([req.name for req in build_failures]), + ' '.join([req.name for req in build_failures if req.name]), ) # Return a list of requirements that failed to build return build_successes, build_failures From eeea4669e4b1664e954845fd0679e8a6234b5df6 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Thu, 28 May 2020 02:49:12 +0530 Subject: [PATCH 2122/3170] Add news entry --- news/0EF5EAF5-F3CC-4B27-A128-872E6A4DC6B4.trivial | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 news/0EF5EAF5-F3CC-4B27-A128-872E6A4DC6B4.trivial diff --git a/news/0EF5EAF5-F3CC-4B27-A128-872E6A4DC6B4.trivial b/news/0EF5EAF5-F3CC-4B27-A128-872E6A4DC6B4.trivial new file mode 100644 index 00000000000..e69de29bb2d From 9a8bcf3f343fe692c540808b451113b147b4082a Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Thu, 25 Jun 2020 23:27:50 +0530 Subject: [PATCH 2123/3170] Add type: ignore to requirements loop --- src/pip/_internal/wheel_builder.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index 06cedd40d65..b9929815810 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -274,7 +274,7 @@ def build( # Build the wheels. logger.info( 'Building wheels for collected packages: %s', - ', '.join(req.name for req in requirements if req.name), + ', '.join(req.name for req in requirements), # type: ignore ) with indent_log(): @@ -297,12 +297,12 @@ def build( if build_successes: logger.info( 'Successfully built %s', - ' '.join([req.name for req in build_successes if req.name]), + ' '.join([req.name for req in build_successes]), # type: ignore ) if build_failures: logger.info( 'Failed to build %s', - ' '.join([req.name for req in build_failures if req.name]), + ' '.join([req.name for req in build_failures]), # type: ignore ) # Return a list of requirements that failed to build return build_successes, build_failures From 6119a837ea103276025faaec1feb1f49429271a3 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Thu, 30 Apr 2020 21:59:20 +0530 Subject: [PATCH 2124/3170] Add unit tests for --no-input flag --- news/7688.doc | 1 + tests/functional/test_install_config.py | 66 ++++++++++++++++++++++++- tests/lib/server.py | 16 ++++++ 3 files changed, 82 insertions(+), 1 deletion(-) create mode 100644 news/7688.doc diff --git a/news/7688.doc b/news/7688.doc new file mode 100644 index 00000000000..e891c7e8c29 --- /dev/null +++ b/news/7688.doc @@ -0,0 +1 @@ +Add ``--no-input`` option to pip docs diff --git a/tests/functional/test_install_config.py b/tests/functional/test_install_config.py index 6cd283f077f..8e9b04a37d9 100644 --- a/tests/functional/test_install_config.py +++ b/tests/functional/test_install_config.py @@ -1,10 +1,17 @@ import os +import ssl import tempfile import textwrap import pytest -from tests.lib.server import file_response, package_page +from tests.lib.server import ( + authorization_response, + file_response, + make_mock_server, + package_page, + server_running, +) def test_options_from_env_vars(script): @@ -209,3 +216,60 @@ def test_install_no_binary_via_config_disables_cached_wheels( assert "Building wheel for upper" not in str(res), str(res) # Must have used source, not a cached wheel to install upper. assert "Running setup.py install for upper" in str(res), str(res) + + +def test_prompt_for_authentication(script, data, cert_factory): + """Test behaviour while installing from a index url + requiring authentication + """ + cert_path = cert_factory() + ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + ctx.load_cert_chain(cert_path, cert_path) + ctx.load_verify_locations(cafile=cert_path) + ctx.verify_mode = ssl.CERT_REQUIRED + + server = make_mock_server(ssl_context=ctx) + server.mock.side_effect = [ + package_page({ + "simple-3.0.tar.gz": "/files/simple-3.0.tar.gz", + }), + authorization_response(str(data.packages / "simple-3.0.tar.gz")), + ] + + url = "https://{}:{}/simple".format(server.host, server.port) + + with server_running(server): + result = script.pip('install', "--index-url", url, + "--cert", cert_path, "--client-cert", cert_path, + 'simple', expect_error=True) + print(result) + assert 'User for {}:{}'.format(server.host, server.port) in result.stdout + + +def test_do_not_prompt_for_authentication(script, data, cert_factory): + """Test behaviour if --no-input option is given while installing + from a index url requiring authentication + """ + cert_path = cert_factory() + ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + ctx.load_cert_chain(cert_path, cert_path) + ctx.load_verify_locations(cafile=cert_path) + ctx.verify_mode = ssl.CERT_REQUIRED + + server = make_mock_server(ssl_context=ctx) + + server.mock.side_effect = [ + package_page({ + "simple-3.0.tar.gz": "/files/simple-3.0.tar.gz", + }), + authorization_response(str(data.packages / "simple-3.0.tar.gz")), + ] + + url = "https://{}:{}/simple".format(server.host, server.port) + + with server_running(server): + result = script.pip('install', "--index-url", url, + "--cert", cert_path, "--client-cert", cert_path, + '--no-input', 'simple', expect_error=True) + + assert "ERROR: HTTP error 401" in result.stderr diff --git a/tests/lib/server.py b/tests/lib/server.py index bb423a2d867..6cf891d0d5d 100644 --- a/tests/lib/server.py +++ b/tests/lib/server.py @@ -210,3 +210,19 @@ def responder(environ, start_response): return [f.read()] return responder + + +def authorization_response(path): + def responder(environ, start_response): + # type: (Environ, StartResponse) -> Body + + start_response( + "401 Unauthorized", [ + ("WWW-Authenticate", "Basic"), + ], + ) + + with open(path, 'rb') as f: + return [f.read()] + + return responder From a833914387386734ad9706982890d3031d18dd12 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sat, 2 May 2020 01:26:27 +0530 Subject: [PATCH 2125/3170] Add help text to --no-input option --- src/pip/_internal/cli/cmdoptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index 4c557efa80f..643b11280e3 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -243,7 +243,7 @@ class PipOption(Option): dest='no_input', action='store_true', default=False, - help=SUPPRESS_HELP + help="Disable prompting for input." ) # type: Callable[..., Option] proxy = partial( From 4ecd7ecbc705db7ad9953b1cab2e3b8a15c9d748 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sun, 17 May 2020 01:10:31 +0530 Subject: [PATCH 2126/3170] Assert result string in test_prompt_for_authentication --- tests/functional/test_install_config.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/functional/test_install_config.py b/tests/functional/test_install_config.py index 8e9b04a37d9..dcc9c66d5a4 100644 --- a/tests/functional/test_install_config.py +++ b/tests/functional/test_install_config.py @@ -242,8 +242,9 @@ def test_prompt_for_authentication(script, data, cert_factory): result = script.pip('install', "--index-url", url, "--cert", cert_path, "--client-cert", cert_path, 'simple', expect_error=True) - print(result) - assert 'User for {}:{}'.format(server.host, server.port) in result.stdout + + assert 'User for {}:{}'.format(server.host, server.port) in \ + result.stdout, str(result) def test_do_not_prompt_for_authentication(script, data, cert_factory): From 3eb85a0619371ca9a20a131ae9394d40eb949b5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Thu, 18 Jun 2020 23:10:00 +0700 Subject: [PATCH 2127/3170] Draft lazy zip over HTTP --- ...727978-e22a-427d-aa03-11ce55d8f6f9.trivial | 0 src/pip/_internal/network/download.py | 27 +-- src/pip/_internal/network/lazy_wheel.py | 204 ++++++++++++++++++ src/pip/_internal/network/utils.py | 23 +- 4 files changed, 228 insertions(+), 26 deletions(-) create mode 100644 news/70727978-e22a-427d-aa03-11ce55d8f6f9.trivial create mode 100644 src/pip/_internal/network/lazy_wheel.py diff --git a/news/70727978-e22a-427d-aa03-11ce55d8f6f9.trivial b/news/70727978-e22a-427d-aa03-11ce55d8f6f9.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/network/download.py b/src/pip/_internal/network/download.py index 2f3e08ae62e..7110c8ebdbf 100644 --- a/src/pip/_internal/network/download.py +++ b/src/pip/_internal/network/download.py @@ -11,7 +11,7 @@ from pip._internal.cli.progress_bars import DownloadProgressProvider from pip._internal.models.index import PyPI from pip._internal.network.cache import is_from_cache -from pip._internal.network.utils import response_chunks +from pip._internal.network.utils import HEADERS, response_chunks from pip._internal.utils.misc import ( format_size, redact_auth_from_url, @@ -132,30 +132,7 @@ def _get_http_response_filename(resp, link): def _http_get_download(session, link): # type: (PipSession, Link) -> Response target_url = link.url.split('#', 1)[0] - resp = session.get( - target_url, - # We use Accept-Encoding: identity here because requests - # defaults to accepting compressed responses. This breaks in - # a variety of ways depending on how the server is configured. - # - Some servers will notice that the file isn't a compressible - # file and will leave the file alone and with an empty - # Content-Encoding - # - Some servers will notice that the file is already - # compressed and will leave the file alone and will add a - # Content-Encoding: gzip header - # - Some servers won't notice anything at all and will take - # a file that's already been compressed and compress it again - # and set the Content-Encoding: gzip header - # By setting this to request only the identity encoding We're - # hoping to eliminate the third case. Hopefully there does not - # exist a server which when given a file will notice it is - # already compressed and that you're not asking for a - # compressed file and will then decompress it before sending - # because if that's the case I don't think it'll ever be - # possible to make this work. - headers={"Accept-Encoding": "identity"}, - stream=True, - ) + resp = session.get(target_url, headers=HEADERS, stream=True) resp.raise_for_status() return resp diff --git a/src/pip/_internal/network/lazy_wheel.py b/src/pip/_internal/network/lazy_wheel.py new file mode 100644 index 00000000000..68ad5afcc60 --- /dev/null +++ b/src/pip/_internal/network/lazy_wheel.py @@ -0,0 +1,204 @@ +"""Lazy ZIP over HTTP""" + +__all__ = ['LazyZip'] + +from bisect import bisect_left, bisect_right +from contextlib import contextmanager +from tempfile import NamedTemporaryFile +from zipfile import BadZipfile, ZipFile + +from pip._vendor.requests.models import CONTENT_CHUNK_SIZE +from pip._vendor.six.moves import range + +from pip._internal.network.utils import HEADERS, response_chunks +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import Any, Dict, Iterator, List, Optional, Tuple + + from pip._vendor.requests.models import Response + + from pip._internal.network.session import PipSession + + +class LazyZip: + """File-like object mapped to a ZIP file over HTTP. + + This uses HTTP range requests to lazily fetch the file's content, + which is supposed to be fed to ZipFile. + """ + + def __init__(self, session, url, chunk_size=CONTENT_CHUNK_SIZE): + # type: (PipSession, str, int) -> None + head = session.head(url, headers=HEADERS) + head.raise_for_status() + assert head.status_code == 200 + self._session, self._url, self._chunk_size = session, url, chunk_size + self._length = int(head.headers['Content-Length']) + self._file = NamedTemporaryFile() + self.truncate(self._length) + self._left = [] # type: List[int] + self._right = [] # type: List[int] + self._check_zip('bytes' in head.headers.get('Accept-Ranges', 'none')) + + @property + def mode(self): + # type: () -> str + """Opening mode, which is always rb.""" + return 'rb' + + @property + def name(self): + # type: () -> str + """File name.""" + return self._file.name + + def seekable(self): + # type: () -> bool + """Return whether random access is supported, which is True.""" + return True + + def close(self): + # type: () -> None + """Close the file.""" + self._file.close() + + @property + def closed(self): + # type: () -> bool + """Whether the file is closed.""" + return self._file.closed + + def read(self, size=-1): + # type: (int) -> bytes + """Read up to size bytes from the object and return them. + + As a convenience, if size is unspecified or -1, + all bytes until EOF are returned. Fewer than + size bytes may be returned if EOF is reached. + """ + start, length = self.tell(), self._length + stop = start + size if 0 <= size <= length-start else length + self._download(start, stop-1) + return self._file.read(size) + + def readable(self): + # type: () -> bool + """Return whether the file is readable, which is True.""" + return True + + def seek(self, offset, whence=0): + # type: (int, int) -> int + """Change stream position and return the new absolute position. + + Seek to offset relative position indicated by whence: + * 0: Start of stream (the default). pos should be >= 0; + * 1: Current position - pos may be negative; + * 2: End of stream - pos usually negative. + """ + return self._file.seek(offset, whence) + + def tell(self): + # type: () -> int + """Return the current possition.""" + return self._file.tell() + + def truncate(self, size=None): + # type: (Optional[int]) -> int + """Resize the stream to the given size in bytes. + + If size is unspecified resize to the current position. + The current stream position isn't changed. + + Return the new file size. + """ + return self._file.truncate(size) + + def writable(self): + # type: () -> bool + """Return False.""" + return False + + def __enter__(self): + # type: () -> LazyZip + self._file.__enter__() + return self + + def __exit__(self, *exc): + # type: (*Any) -> Optional[bool] + return self._file.__exit__(*exc) + + @contextmanager + def _stay(self): + # type: ()-> Iterator[None] + """Return a context manager keeping the position. + + At the end of the block, seek back to original position. + """ + pos = self.tell() + try: + yield + finally: + self.seek(pos) + + def _check_zip(self, range_request): + # type: (bool) -> None + """Check and download until the file is a valid ZIP.""" + end = self._length - 1 + if not range_request: + self._download(0, end) + return + for start in reversed(range(0, end, self._chunk_size)): + self._download(start, end) + with self._stay(): + try: + # For read-only ZIP files, ZipFile only needs + # methods read, seek, seekable and tell. + # The best way to type-hint in this case is to use + # Python 3.8+ typing.Protocol. + ZipFile(self) # type: ignore + except BadZipfile: + pass + else: + break + + def _stream_response(self, start, end, base_headers=HEADERS): + # type: (int, int, Dict[str, str]) -> Response + """Return HTTP response to a range request from start to end.""" + headers = {'Range': 'bytes={}-{}'.format(start, end)} + headers.update(base_headers) + return self._session.get(self._url, headers=headers, stream=True) + + def _merge(self, start, end, left, right): + # type: (int, int, int, int) -> Iterator[Tuple[int, int]] + """Return an iterator of intervals to be fetched. + + Args: + start (int): Start of needed interval + end (int): End of needed interval + left (int): Index of first overlapping downloaded data + right (int): Index after last overlapping downloaded data + """ + lslice, rslice = self._left[left:right], self._right[left:right] + i = start = min([start]+lslice[:1]) + end = max([end]+rslice[-1:]) + for j, k in zip(lslice, rslice): + if j > i: + yield i, j-1 + i = k + 1 + if i <= end: + yield i, end + self._left[left:right], self._right[left:right] = [start], [end] + + def _download(self, start, end): + # type: (int, int) -> None + """Download bytes from start to end inclusively.""" + with self._stay(): + left = bisect_left(self._right, start) + right = bisect_right(self._left, end) + for start, end in self._merge(start, end, left, right): + response = self._stream_response(start, end) + response.raise_for_status() + self.seek(start) + for chunk in response_chunks(response, self._chunk_size): + self._file.write(chunk) diff --git a/src/pip/_internal/network/utils.py b/src/pip/_internal/network/utils.py index a19050b0f70..412a3ca1632 100644 --- a/src/pip/_internal/network/utils.py +++ b/src/pip/_internal/network/utils.py @@ -3,7 +3,28 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Iterator + from typing import Dict, Iterator + +# The following comments and HTTP headers were originally added by +# Donald Stufft in git commit 22c562429a61bb77172039e480873fb239dd8c03. +# +# We use Accept-Encoding: identity here because requests defaults to +# accepting compressed responses. This breaks in a variety of ways +# depending on how the server is configured. +# - Some servers will notice that the file isn't a compressible file +# and will leave the file alone and with an empty Content-Encoding +# - Some servers will notice that the file is already compressed and +# will leave the file alone, adding a Content-Encoding: gzip header +# - Some servers won't notice anything at all and will take a file +# that's already been compressed and compress it again, and set +# the Content-Encoding: gzip header +# By setting this to request only the identity encoding we're hoping +# to eliminate the third case. Hopefully there does not exist a server +# which when given a file will notice it is already compressed and that +# you're not asking for a compressed file and will then decompress it +# before sending because if that's the case I don't think it'll ever be +# possible to make this work. +HEADERS = {'Accept-Encoding': 'identity'} # type: Dict[str, str] def response_chunks(response, chunk_size=CONTENT_CHUNK_SIZE): From e1438d06b522c0f6ce9bb403ddd701bc82382df7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Wed, 24 Jun 2020 16:30:48 +0700 Subject: [PATCH 2128/3170] Rename and wrap LazyZipOverHTTP --- src/pip/_internal/network/lazy_wheel.py | 47 +++++++++++++++++-------- 1 file changed, 32 insertions(+), 15 deletions(-) diff --git a/src/pip/_internal/network/lazy_wheel.py b/src/pip/_internal/network/lazy_wheel.py index 68ad5afcc60..d7b8bcc21ac 100644 --- a/src/pip/_internal/network/lazy_wheel.py +++ b/src/pip/_internal/network/lazy_wheel.py @@ -1,6 +1,6 @@ """Lazy ZIP over HTTP""" -__all__ = ['LazyZip'] +__all__ = ['dist_from_wheel_url'] from bisect import bisect_left, bisect_right from contextlib import contextmanager @@ -12,24 +12,44 @@ from pip._internal.network.utils import HEADERS, response_chunks from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from pip._internal.utils.wheel import pkg_resources_distribution_for_wheel if MYPY_CHECK_RUNNING: from typing import Any, Dict, Iterator, List, Optional, Tuple + from pip._vendor.pkg_resources import Distribution from pip._vendor.requests.models import Response from pip._internal.network.session import PipSession -class LazyZip: +def dist_from_wheel_url(name, url, session): + # type: (str, str, PipSession) -> Distribution + """Return a pkg_resources.Distribution from the given wheel URL. + + This uses HTTP range requests to only fetch the potion of the wheel + containing metadata, just enough for the object to be constructed. + If such requests are not supported, RuntimeError is raised. + """ + with LazyZipOverHTTP(url, session) as wheel: + # For read-only ZIP files, ZipFile only needs methods read, + # seek, seekable and tell, not the whole IO protocol. + zip_file = ZipFile(wheel) # type: ignore + # After context manager exit, wheel.name + # is an invalid file by intention. + return pkg_resources_distribution_for_wheel(zip_file, name, wheel.name) + + +class LazyZipOverHTTP(object): """File-like object mapped to a ZIP file over HTTP. This uses HTTP range requests to lazily fetch the file's content, - which is supposed to be fed to ZipFile. + which is supposed to be fed to ZipFile. If such requests are not + supported by the server, raise RuntimeError during initialization. """ - def __init__(self, session, url, chunk_size=CONTENT_CHUNK_SIZE): - # type: (PipSession, str, int) -> None + def __init__(self, url, session, chunk_size=CONTENT_CHUNK_SIZE): + # type: (str, PipSession, int) -> None head = session.head(url, headers=HEADERS) head.raise_for_status() assert head.status_code == 200 @@ -39,7 +59,9 @@ def __init__(self, session, url, chunk_size=CONTENT_CHUNK_SIZE): self.truncate(self._length) self._left = [] # type: List[int] self._right = [] # type: List[int] - self._check_zip('bytes' in head.headers.get('Accept-Ranges', 'none')) + if 'bytes' not in head.headers.get('Accept-Ranges', 'none'): + raise RuntimeError('range request is not supported') + self._check_zip() @property def mode(self): @@ -50,7 +72,7 @@ def mode(self): @property def name(self): # type: () -> str - """File name.""" + """Path to the underlying file.""" return self._file.name def seekable(self): @@ -120,7 +142,7 @@ def writable(self): return False def __enter__(self): - # type: () -> LazyZip + # type: () -> LazyZipOverHTTP self._file.__enter__() return self @@ -141,21 +163,16 @@ def _stay(self): finally: self.seek(pos) - def _check_zip(self, range_request): - # type: (bool) -> None + def _check_zip(self): + # type: () -> None """Check and download until the file is a valid ZIP.""" end = self._length - 1 - if not range_request: - self._download(0, end) - return for start in reversed(range(0, end, self._chunk_size)): self._download(start, end) with self._stay(): try: # For read-only ZIP files, ZipFile only needs # methods read, seek, seekable and tell. - # The best way to type-hint in this case is to use - # Python 3.8+ typing.Protocol. ZipFile(self) # type: ignore except BadZipfile: pass From 25a25a0975841ce319790d8047999876ecb1188b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Fri, 26 Jun 2020 15:15:18 +0700 Subject: [PATCH 2129/3170] Test network.lazy_wheel.dist_from_wheel_url --- tests/lib/requests_mocks.py | 2 +- tests/unit/test_network_lazy_wheel.py | 50 +++++++++++++++++++++++++++ 2 files changed, 51 insertions(+), 1 deletion(-) create mode 100644 tests/unit/test_network_lazy_wheel.py diff --git a/tests/lib/requests_mocks.py b/tests/lib/requests_mocks.py index baaf77ecc25..41c30eafd9c 100644 --- a/tests/lib/requests_mocks.py +++ b/tests/lib/requests_mocks.py @@ -28,7 +28,7 @@ def __init__(self, contents): self.status_code = 200 self.connection = None self.url = None - self.headers = {} + self.headers = {'Content-Length': len(contents)} self.history = [] def raise_for_status(self): diff --git a/tests/unit/test_network_lazy_wheel.py b/tests/unit/test_network_lazy_wheel.py new file mode 100644 index 00000000000..694d126859f --- /dev/null +++ b/tests/unit/test_network_lazy_wheel.py @@ -0,0 +1,50 @@ +from zipfile import BadZipfile + +from pip._vendor.pkg_resources import Requirement +from pytest import fixture, mark, raises + +from pip._internal.network.lazy_wheel import dist_from_wheel_url +from pip._internal.network.session import PipSession +from tests.lib.requests_mocks import MockResponse + +MYPY_0_782_WHL = ( + 'https://files.pythonhosted.org/packages/9d/65/' + 'b96e844150ce18b9892b155b780248955ded13a2581d31872e7daa90a503/' + 'mypy-0.782-py3-none-any.whl' +) +MYPY_0_782_REQS = { + Requirement('typed-ast (<1.5.0,>=1.4.0)'), + Requirement('typing-extensions (>=3.7.4)'), + Requirement('mypy-extensions (<0.5.0,>=0.4.3)'), + Requirement('psutil (>=4.0); extra == "dmypy"'), +} + + +@fixture +def session(): + return PipSession() + + +@mark.network +def test_dist_from_wheel_url(session): + """Test if the acquired distribution contain correct information.""" + dist = dist_from_wheel_url('mypy', MYPY_0_782_WHL, session) + assert dist.key == 'mypy' + assert dist.version == '0.782' + assert dist.extras == ['dmypy'] + assert set(dist.requires(dist.extras)) == MYPY_0_782_REQS + + +@mark.network +def test_dist_from_wheel_url_no_range(session, monkeypatch): + """Test handling when HTTP range requests are not supported.""" + monkeypatch.setattr(session, 'head', lambda *a, **kw: MockResponse(b'')) + with raises(RuntimeError): + dist_from_wheel_url('mypy', MYPY_0_782_WHL, session) + + +@mark.network +def test_dist_from_wheel_url_not_zip(session): + """Test handling with the given URL does not point to a ZIP.""" + with raises(BadZipfile): + dist_from_wheel_url('python', 'https://www.python.org/', session) From ec7324fba019d2679148ba1c9fea5458ea077668 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 26 Jun 2020 17:20:13 +0800 Subject: [PATCH 2130/3170] Tidy up link collector constructor imports make_link_collector() was in self_outdated_check, a module responsible for checking whether the currently-running pip is outdated, but is imported by things that has nothing to do with this outdated check. Move the function to be a class method in LinkCollector so the module hierarchy makes more sense. --- src/pip/_internal/cli/req_command.py | 8 +-- src/pip/_internal/commands/list.py | 4 +- src/pip/_internal/index/collector.py | 30 +++++++++- src/pip/_internal/self_outdated_check.py | 41 +------------ tests/unit/test_collector.py | 74 ++++++++++++++++++++++++ tests/unit/test_self_check_outdated.py | 74 ------------------------ 6 files changed, 110 insertions(+), 121 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index a215d459314..2544296d00a 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -13,6 +13,7 @@ from pip._internal.cli.base_command import Command from pip._internal.cli.command_context import CommandContextMixIn from pip._internal.exceptions import CommandError, PreviousBuildDirError +from pip._internal.index.collector import LinkCollector from pip._internal.index.package_finder import PackageFinder from pip._internal.models.selection_prefs import SelectionPreferences from pip._internal.network.download import Downloader @@ -25,10 +26,7 @@ install_req_from_req_string, ) from pip._internal.req.req_file import parse_requirements -from pip._internal.self_outdated_check import ( - make_link_collector, - pip_self_version_check, -) +from pip._internal.self_outdated_check import pip_self_version_check from pip._internal.utils.temp_dir import tempdir_kinds from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -387,7 +385,7 @@ def _build_package_finder( :param ignore_requires_python: Whether to ignore incompatible "Requires-Python" values in links. Defaults to False. """ - link_collector = make_link_collector(session, options=options) + link_collector = LinkCollector.create(session, options=options) selection_prefs = SelectionPreferences( allow_yanked=True, format_control=options.format_control, diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index df9e1b38eda..4b7ba9d9526 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -9,9 +9,9 @@ from pip._internal.cli.req_command import IndexGroupCommand from pip._internal.cli.status_codes import SUCCESS from pip._internal.exceptions import CommandError +from pip._internal.index.collector import LinkCollector from pip._internal.index.package_finder import PackageFinder from pip._internal.models.selection_prefs import SelectionPreferences -from pip._internal.self_outdated_check import make_link_collector from pip._internal.utils.misc import ( dist_is_editable, get_installed_distributions, @@ -123,7 +123,7 @@ def _build_package_finder(self, options, session): """ Create a package finder appropriate to this list command. """ - link_collector = make_link_collector(session, options=options) + link_collector = LinkCollector.create(session, options=options) # Pass allow_yanked=False to ignore yanked versions. selection_prefs = SelectionPreferences( diff --git a/src/pip/_internal/index/collector.py b/src/pip/_internal/index/collector.py index 7908ab996e6..d2dd6d2ca3d 100644 --- a/src/pip/_internal/index/collector.py +++ b/src/pip/_internal/index/collector.py @@ -18,6 +18,7 @@ from pip._vendor.six.moves.urllib import request as urllib_request from pip._internal.models.link import Link +from pip._internal.models.search_scope import SearchScope from pip._internal.utils.filetypes import ARCHIVE_EXTENSIONS from pip._internal.utils.misc import pairwise, redact_auth_from_url from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -25,6 +26,7 @@ from pip._internal.vcs import is_url, vcs if MYPY_CHECK_RUNNING: + from optparse import Values from typing import ( Callable, Iterable, List, MutableMapping, Optional, Protocol, Sequence, Tuple, TypeVar, Union, @@ -33,7 +35,6 @@ from pip._vendor.requests import Response - from pip._internal.models.search_scope import SearchScope from pip._internal.network.session import PipSession HTMLElement = xml.etree.ElementTree.Element @@ -600,6 +601,33 @@ def __init__( self.search_scope = search_scope self.session = session + @classmethod + def create(cls, session, options, suppress_no_index=False): + # type: (PipSession, Values, bool) -> LinkCollector + """ + :param session: The Session to use to make requests. + :param suppress_no_index: Whether to ignore the --no-index option + when constructing the SearchScope object. + """ + index_urls = [options.index_url] + options.extra_index_urls + if options.no_index and not suppress_no_index: + logger.debug( + 'Ignoring indexes: %s', + ','.join(redact_auth_from_url(url) for url in index_urls), + ) + index_urls = [] + + # Make sure find_links is a list before passing to create(). + find_links = options.find_links or [] + + search_scope = SearchScope.create( + find_links=find_links, index_urls=index_urls, + ) + link_collector = LinkCollector( + session=session, search_scope=search_scope, + ) + return link_collector + @property def find_links(self): # type: () -> List[str] diff --git a/src/pip/_internal/self_outdated_check.py b/src/pip/_internal/self_outdated_check.py index c5dcdb51b0a..ec5df3af105 100644 --- a/src/pip/_internal/self_outdated_check.py +++ b/src/pip/_internal/self_outdated_check.py @@ -13,24 +13,18 @@ from pip._internal.index.collector import LinkCollector from pip._internal.index.package_finder import PackageFinder -from pip._internal.models.search_scope import SearchScope from pip._internal.models.selection_prefs import SelectionPreferences from pip._internal.utils.filesystem import ( adjacent_tmp_file, check_path_owner, replace, ) -from pip._internal.utils.misc import ( - ensure_dir, - get_installed_version, - redact_auth_from_url, -) +from pip._internal.utils.misc import ensure_dir, get_installed_version from pip._internal.utils.packaging import get_installer from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: import optparse - from optparse import Values from typing import Any, Dict, Text, Union from pip._internal.network.session import PipSession @@ -42,37 +36,6 @@ logger = logging.getLogger(__name__) -def make_link_collector( - session, # type: PipSession - options, # type: Values - suppress_no_index=False, # type: bool -): - # type: (...) -> LinkCollector - """ - :param session: The Session to use to make requests. - :param suppress_no_index: Whether to ignore the --no-index option - when constructing the SearchScope object. - """ - index_urls = [options.index_url] + options.extra_index_urls - if options.no_index and not suppress_no_index: - logger.debug( - 'Ignoring indexes: %s', - ','.join(redact_auth_from_url(url) for url in index_urls), - ) - index_urls = [] - - # Make sure find_links is a list before passing to create(). - find_links = options.find_links or [] - - search_scope = SearchScope.create( - find_links=find_links, index_urls=index_urls, - ) - - link_collector = LinkCollector(session=session, search_scope=search_scope) - - return link_collector - - def _get_statefile_name(key): # type: (Union[str, Text]) -> str key_bytes = ensure_binary(key) @@ -185,7 +148,7 @@ def pip_self_version_check(session, options): # Refresh the version if we need to or just see if we need to warn if pypi_version is None: # Lets use PackageFinder to see what the latest pip version is - link_collector = make_link_collector( + link_collector = LinkCollector.create( session, options=options, suppress_no_index=True, diff --git a/tests/unit/test_collector.py b/tests/unit/test_collector.py index 0387813ad0a..b7a1d65aee0 100644 --- a/tests/unit/test_collector.py +++ b/tests/unit/test_collector.py @@ -13,6 +13,7 @@ from pip._internal.index.collector import ( HTMLPage, + LinkCollector, _clean_link, _clean_url_path, _determine_base_url, @@ -652,3 +653,76 @@ def test_collect_links(self, caplog, data): assert caplog.record_tuples == [ ('pip._internal.index.collector', logging.DEBUG, expected_message), ] + + +@pytest.mark.parametrize( + 'find_links, no_index, suppress_no_index, expected', [ + (['link1'], False, False, + (['link1'], ['default_url', 'url1', 'url2'])), + (['link1'], False, True, (['link1'], ['default_url', 'url1', 'url2'])), + (['link1'], True, False, (['link1'], [])), + # Passing suppress_no_index=True suppresses no_index=True. + (['link1'], True, True, (['link1'], ['default_url', 'url1', 'url2'])), + # Test options.find_links=False. + (False, False, False, ([], ['default_url', 'url1', 'url2'])), + ], +) +def test_link_collector_create( + find_links, no_index, suppress_no_index, expected, +): + """ + :param expected: the expected (find_links, index_urls) values. + """ + expected_find_links, expected_index_urls = expected + session = PipSession() + options = pretend.stub( + find_links=find_links, + index_url='default_url', + extra_index_urls=['url1', 'url2'], + no_index=no_index, + ) + link_collector = LinkCollector.create( + session, options=options, suppress_no_index=suppress_no_index, + ) + + assert link_collector.session is session + + search_scope = link_collector.search_scope + assert search_scope.find_links == expected_find_links + assert search_scope.index_urls == expected_index_urls + + +@patch('pip._internal.utils.misc.expanduser') +def test_link_collector_create_find_links_expansion( + mock_expanduser, tmpdir, +): + """ + Test "~" expansion in --find-links paths. + """ + # This is a mock version of expanduser() that expands "~" to the tmpdir. + def expand_path(path): + if path.startswith('~/'): + path = os.path.join(tmpdir, path[2:]) + return path + + mock_expanduser.side_effect = expand_path + + session = PipSession() + options = pretend.stub( + find_links=['~/temp1', '~/temp2'], + index_url='default_url', + extra_index_urls=[], + no_index=False, + ) + # Only create temp2 and not temp1 to test that "~" expansion only occurs + # when the directory exists. + temp2_dir = os.path.join(tmpdir, 'temp2') + os.mkdir(temp2_dir) + + link_collector = LinkCollector.create(session, options=options) + + search_scope = link_collector.search_scope + # Only ~/temp2 gets expanded. Also, the path is normalized when expanded. + expected_temp2_dir = os.path.normcase(temp2_dir) + assert search_scope.find_links == ['~/temp1', expected_temp2_dir] + assert search_scope.index_urls == ['default_url'] diff --git a/tests/unit/test_self_check_outdated.py b/tests/unit/test_self_check_outdated.py index 72fa6929357..f33e319cf78 100644 --- a/tests/unit/test_self_check_outdated.py +++ b/tests/unit/test_self_check_outdated.py @@ -6,92 +6,18 @@ import freezegun import pretend import pytest -from mock import patch from pip._vendor import pkg_resources from pip._internal import self_outdated_check from pip._internal.models.candidate import InstallationCandidate -from pip._internal.network.session import PipSession from pip._internal.self_outdated_check import ( SelfCheckState, logger, - make_link_collector, pip_self_version_check, ) from tests.lib.path import Path -@pytest.mark.parametrize( - 'find_links, no_index, suppress_no_index, expected', [ - (['link1'], False, False, - (['link1'], ['default_url', 'url1', 'url2'])), - (['link1'], False, True, (['link1'], ['default_url', 'url1', 'url2'])), - (['link1'], True, False, (['link1'], [])), - # Passing suppress_no_index=True suppresses no_index=True. - (['link1'], True, True, (['link1'], ['default_url', 'url1', 'url2'])), - # Test options.find_links=False. - (False, False, False, ([], ['default_url', 'url1', 'url2'])), - ], -) -def test_make_link_collector( - find_links, no_index, suppress_no_index, expected, -): - """ - :param expected: the expected (find_links, index_urls) values. - """ - expected_find_links, expected_index_urls = expected - session = PipSession() - options = pretend.stub( - find_links=find_links, - index_url='default_url', - extra_index_urls=['url1', 'url2'], - no_index=no_index, - ) - link_collector = make_link_collector( - session, options=options, suppress_no_index=suppress_no_index, - ) - - assert link_collector.session is session - - search_scope = link_collector.search_scope - assert search_scope.find_links == expected_find_links - assert search_scope.index_urls == expected_index_urls - - -@patch('pip._internal.utils.misc.expanduser') -def test_make_link_collector__find_links_expansion(mock_expanduser, tmpdir): - """ - Test "~" expansion in --find-links paths. - """ - # This is a mock version of expanduser() that expands "~" to the tmpdir. - def expand_path(path): - if path.startswith('~/'): - path = os.path.join(tmpdir, path[2:]) - return path - - mock_expanduser.side_effect = expand_path - - session = PipSession() - options = pretend.stub( - find_links=['~/temp1', '~/temp2'], - index_url='default_url', - extra_index_urls=[], - no_index=False, - ) - # Only create temp2 and not temp1 to test that "~" expansion only occurs - # when the directory exists. - temp2_dir = os.path.join(tmpdir, 'temp2') - os.mkdir(temp2_dir) - - link_collector = make_link_collector(session, options=options) - - search_scope = link_collector.search_scope - # Only ~/temp2 gets expanded. Also, the path is normalized when expanded. - expected_temp2_dir = os.path.normcase(temp2_dir) - assert search_scope.find_links == ['~/temp1', expected_temp2_dir] - assert search_scope.index_urls == ['default_url'] - - class MockBestCandidateResult(object): def __init__(self, best): self.best_candidate = best From 504759bfe5b0e0196dad1e164c54e3b351b77e25 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Wed, 24 Jun 2020 21:38:05 +0530 Subject: [PATCH 2131/3170] Add removal news entry --- news/8408.removal | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/8408.removal diff --git a/news/8408.removal b/news/8408.removal new file mode 100644 index 00000000000..008e21b75d0 --- /dev/null +++ b/news/8408.removal @@ -0,0 +1 @@ +Remove undocumented and deprecated option ``--always-unzip`` From 7a8f374c399b4bb1b8effb59e278d7dd9fee955a Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Mon, 20 Apr 2020 15:05:54 +0530 Subject: [PATCH 2132/3170] Add option to list config files with pip config --- news/6741.feature | 1 + src/pip/_internal/commands/configuration.py | 16 +++++++++++++++- src/pip/_internal/configuration.py | 4 ++-- tests/unit/test_options.py | 2 +- 4 files changed, 19 insertions(+), 4 deletions(-) create mode 100644 news/6741.feature diff --git a/news/6741.feature b/news/6741.feature new file mode 100644 index 00000000000..e7a4415afca --- /dev/null +++ b/news/6741.feature @@ -0,0 +1 @@ +Add a subcommand ``list-files`` to ``pip config`` to list available configuration files and their location diff --git a/src/pip/_internal/commands/configuration.py b/src/pip/_internal/commands/configuration.py index e4db35cab37..abb4e42acd6 100644 --- a/src/pip/_internal/commands/configuration.py +++ b/src/pip/_internal/commands/configuration.py @@ -13,6 +13,7 @@ kinds, ) from pip._internal.exceptions import PipError +from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import get_prog, write_output from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -34,6 +35,7 @@ class ConfigurationCommand(Command): - get: Get the value associated with name - set: Set the name=value - unset: Unset the value associated with name + - list-files: List the configuration files If none of --user, --global and --site are passed, a virtual environment configuration file is used if one is active and the file @@ -49,6 +51,7 @@ class ConfigurationCommand(Command): %prog [<file-option>] get name %prog [<file-option>] set name value %prog [<file-option>] unset name + %prog [<file-option>] list-files """ def __init__(self, name, summary, isolated=False): @@ -103,7 +106,8 @@ def run(self, options, args): "edit": self.open_in_editor, "get": self.get_name, "set": self.set_name_value, - "unset": self.unset_name + "unset": self.unset_name, + "list-files": self.list_config_files, } # Determine action @@ -190,6 +194,16 @@ def unset_name(self, options, args): self._save_configuration() + def list_config_files(self, options, args): + self._get_n_args(args, "list-files", n=0) + + for variant, files in sorted(self.configuration.iter_config_files()): + write_output("%s:", variant) + for fname in files: + with indent_log(): + write_output("%s, exists: %r ", + fname, os.path.exists(fname)) + def open_in_editor(self, options, args): editor = self._determine_editor(options) diff --git a/src/pip/_internal/configuration.py b/src/pip/_internal/configuration.py index 2648b8af327..894cdeaf6f8 100644 --- a/src/pip/_internal/configuration.py +++ b/src/pip/_internal/configuration.py @@ -280,7 +280,7 @@ def _load_config_files(self): # type: () -> None """Loads configuration from configuration files """ - config_files = dict(self._iter_config_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 " @@ -370,7 +370,7 @@ def _get_environ_vars(self): yield key[4:].lower(), val # XXX: This is patched in the tests. - def _iter_config_files(self): + def iter_config_files(self): # type: () -> Iterable[Tuple[Kind, List[str]]] """Yields variant and configuration files associated with it. diff --git a/tests/unit/test_options.py b/tests/unit/test_options.py index 950075ba435..ce4fc9c25d1 100644 --- a/tests/unit/test_options.py +++ b/tests/unit/test_options.py @@ -387,7 +387,7 @@ def test_venv_config_file_found(self, monkeypatch): cp = pip._internal.configuration.Configuration(isolated=False) files = [] - for _, val in cp._iter_config_files(): + for _, val in cp.iter_config_files(): files.extend(val) assert len(files) == 4 From b9d19a64d7ee168abe8253578c1af6f50c0e07da Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sun, 3 May 2020 01:44:35 +0530 Subject: [PATCH 2133/3170] List values per configuration file --- src/pip/_internal/commands/configuration.py | 17 ++++++++++++----- src/pip/_internal/configuration.py | 5 +++++ 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/commands/configuration.py b/src/pip/_internal/commands/configuration.py index abb4e42acd6..4183c3417e0 100644 --- a/src/pip/_internal/commands/configuration.py +++ b/src/pip/_internal/commands/configuration.py @@ -51,7 +51,7 @@ class ConfigurationCommand(Command): %prog [<file-option>] get name %prog [<file-option>] set name value %prog [<file-option>] unset name - %prog [<file-option>] list-files + %prog [<file-option>] debug """ def __init__(self, name, summary, isolated=False): @@ -107,7 +107,7 @@ def run(self, options, args): "get": self.get_name, "set": self.set_name_value, "unset": self.unset_name, - "list-files": self.list_config_files, + "debug": self.list_config_files, } # Determine action @@ -172,7 +172,6 @@ def _determine_file(self, options, need_value): def list_values(self, options, args): self._get_n_args(args, "list", n=0) - for key, value in sorted(self.configuration.items()): write_output("%s=%r", key, value) @@ -196,13 +195,21 @@ def unset_name(self, options, args): def list_config_files(self, options, args): self._get_n_args(args, "list-files", n=0) - for variant, files in sorted(self.configuration.iter_config_files()): write_output("%s:", variant) for fname in files: with indent_log(): + file_exists = os.path.exists(fname) write_output("%s, exists: %r ", - fname, os.path.exists(fname)) + fname, file_exists) + if file_exists: + self.print_config_file_values(variant) + + def print_config_file_values(self, variant): + for name, value in self.configuration. \ + get_values_in_config(variant).items(): + with indent_log(): + write_output("%s: %s ", name, value) def open_in_editor(self, options, args): editor = self._determine_editor(options) diff --git a/src/pip/_internal/configuration.py b/src/pip/_internal/configuration.py index 894cdeaf6f8..bad3357140a 100644 --- a/src/pip/_internal/configuration.py +++ b/src/pip/_internal/configuration.py @@ -401,6 +401,11 @@ def iter_config_files(self): # finally virtualenv configuration first trumping others yield kinds.SITE, config_files[kinds.SITE] + def get_values_in_config(self, variant): + # type: (Kind) -> Dict[str, Any] + """Get values present in a config file""" + return self._config[variant] + def _get_parser_to_modify(self): # type: () -> Tuple[str, RawConfigParser] # Determine which parser to modify From 2fcad8ffa7d78b7cc06221112d385f765137e37c Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Mon, 1 Jun 2020 00:08:03 +0530 Subject: [PATCH 2134/3170] Rename subcommand from list-files to debug --- news/6741.feature | 2 +- src/pip/_internal/commands/configuration.py | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/news/6741.feature b/news/6741.feature index e7a4415afca..382e095a630 100644 --- a/news/6741.feature +++ b/news/6741.feature @@ -1 +1 @@ -Add a subcommand ``list-files`` to ``pip config`` to list available configuration files and their location +Add a subcommand ``debug`` to ``pip config`` to list available configuration sources and the key-value pairs defined in them. diff --git a/src/pip/_internal/commands/configuration.py b/src/pip/_internal/commands/configuration.py index 4183c3417e0..e324eaeab82 100644 --- a/src/pip/_internal/commands/configuration.py +++ b/src/pip/_internal/commands/configuration.py @@ -35,7 +35,7 @@ class ConfigurationCommand(Command): - get: Get the value associated with name - set: Set the name=value - unset: Unset the value associated with name - - list-files: List the configuration files + - debug: List the configuration files and values defined under them If none of --user, --global and --site are passed, a virtual environment configuration file is used if one is active and the file @@ -107,7 +107,7 @@ def run(self, options, args): "get": self.get_name, "set": self.set_name_value, "unset": self.unset_name, - "debug": self.list_config_files, + "debug": self.list_config_values, } # Determine action @@ -193,8 +193,8 @@ def unset_name(self, options, args): self._save_configuration() - def list_config_files(self, options, args): - self._get_n_args(args, "list-files", n=0) + def list_config_values(self, options, args): + self._get_n_args(args, "debug", n=0) for variant, files in sorted(self.configuration.iter_config_files()): write_output("%s:", variant) for fname in files: From f0f692e8e9808e2b61aa5369e80f4a7c4a72547e Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Mon, 1 Jun 2020 01:51:32 +0530 Subject: [PATCH 2135/3170] Add env and env var values --- src/pip/_internal/commands/configuration.py | 19 +++++++++++++++++-- src/pip/_internal/configuration.py | 4 ++-- 2 files changed, 19 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/commands/configuration.py b/src/pip/_internal/commands/configuration.py index e324eaeab82..4e8e2ece3d8 100644 --- a/src/pip/_internal/commands/configuration.py +++ b/src/pip/_internal/commands/configuration.py @@ -172,6 +172,7 @@ def _determine_file(self, options, need_value): def list_values(self, options, args): self._get_n_args(args, "list", n=0) + for key, value in sorted(self.configuration.items()): write_output("%s=%r", key, value) @@ -194,22 +195,36 @@ def unset_name(self, options, args): self._save_configuration() def list_config_values(self, options, args): + """List config key-value pairs across different config files""" self._get_n_args(args, "debug", n=0) + + self.print_env_var_values() + # Iterate over config files and print if they exist, and the + # key-value pairs present in them if they do for variant, files in sorted(self.configuration.iter_config_files()): write_output("%s:", variant) for fname in files: with indent_log(): file_exists = os.path.exists(fname) - write_output("%s, exists: %r ", + write_output("%s, exists: %r", fname, file_exists) if file_exists: self.print_config_file_values(variant) def print_config_file_values(self, variant): + """Get key-value pairs from the file of a variant""" for name, value in self.configuration. \ get_values_in_config(variant).items(): with indent_log(): - write_output("%s: %s ", name, value) + write_output("%s: %s", name, value) + + def print_env_var_values(self): + """Get key-values pairs present as environment variables""" + write_output("%s:", 'env_var') + with indent_log(): + for key, value in self.configuration.get_environ_vars(): + env_var = 'PIP_{}'.format(key.upper()) + write_output("%s=%r", env_var, value) def open_in_editor(self, options, args): editor = self._determine_editor(options) diff --git a/src/pip/_internal/configuration.py b/src/pip/_internal/configuration.py index bad3357140a..0b01ee38738 100644 --- a/src/pip/_internal/configuration.py +++ b/src/pip/_internal/configuration.py @@ -342,7 +342,7 @@ def _load_environment_vars(self): """Loads configuration from environment variables """ self._config[kinds.ENV_VAR].update( - self._normalized_keys(":env:", self._get_environ_vars()) + self._normalized_keys(":env:", self.get_environ_vars()) ) def _normalized_keys(self, section, items): @@ -358,7 +358,7 @@ def _normalized_keys(self, section, items): normalized[key] = val return normalized - def _get_environ_vars(self): + 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(): From 6e62481568c3a833a7726cf0b300bac3486d40f8 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Mon, 1 Jun 2020 05:12:14 +0530 Subject: [PATCH 2136/3170] Add unit tests for pip config debug --- src/pip/_internal/commands/configuration.py | 4 +- tests/functional/test_configuration.py | 79 +++++++++++++++++++++ 2 files changed, 81 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/commands/configuration.py b/src/pip/_internal/commands/configuration.py index 4e8e2ece3d8..75289baad8b 100644 --- a/src/pip/_internal/commands/configuration.py +++ b/src/pip/_internal/commands/configuration.py @@ -213,7 +213,7 @@ def list_config_values(self, options, args): def print_config_file_values(self, variant): """Get key-value pairs from the file of a variant""" - for name, value in self.configuration. \ + for name, value in self.configuration.\ get_values_in_config(variant).items(): with indent_log(): write_output("%s: %s", name, value) @@ -222,7 +222,7 @@ def print_env_var_values(self): """Get key-values pairs present as environment variables""" write_output("%s:", 'env_var') with indent_log(): - for key, value in self.configuration.get_environ_vars(): + for key, value in sorted(self.configuration.get_environ_vars()): env_var = 'PIP_{}'.format(key.upper()) write_output("%s=%r", env_var, value) diff --git a/tests/functional/test_configuration.py b/tests/functional/test_configuration.py index c15c4780741..4c263516e46 100644 --- a/tests/functional/test_configuration.py +++ b/tests/functional/test_configuration.py @@ -1,11 +1,16 @@ """Tests for the config command """ +import re import textwrap import pytest from pip._internal.cli.status_codes import ERROR +from pip._internal.configuration import ( + CONFIG_BASENAME, + get_configuration_files, +) from tests.lib.configuration_helpers import ConfigurationMixin, kinds @@ -62,3 +67,77 @@ def test_forget_section(self, script): result = script.pip("config", "set", "isolated", "true", expect_error=True) assert "global.isolated" in result.stderr + + def test_env_var_values(self, script): + """Test that pip configuration set with environment variables + is correctly displayed under "env_var". + """ + + env_vars = { + "PIP_DEFAULT_TIMEOUT": "60", + "PIP_FIND_LINKS": "http://mirror.example.com" + } + script.environ.update(env_vars) + + result = script.pip("config", "debug") + assert "PIP_DEFAULT_TIMEOUT='60'" in result.stdout + assert "PIP_FIND_LINKS='http://mirror.example.com'" in result.stdout + assert re.search(r"env_var:\n( .+\n)+", result.stdout) + + def test_env_values(self, script): + """Test that custom pip configuration using the environment variable + PIP_CONFIG_FILE is correctly displayed under "env". This configuration + takes place of per-user configuration file displayed under "user". + """ + + config_file = script.scratch_path / "test-pip.cfg" + script.environ['PIP_CONFIG_FILE'] = str(config_file) + config_file.write_text(textwrap.dedent("""\ + [global] + timeout = 60 + + [freeze] + timeout = 10 + """)) + + result = script.pip("config", "debug") + assert "{}, exists: True".format(config_file) in result.stdout + assert "global.timeout: 60" in result.stdout + assert "freeze.timeout: 10" in result.stdout + assert re.search(r"env:\n( .+\n)+", result.stdout) + + def test_user_values(self, script,): + """Test that the user pip configuration set using --user + is correctly displayed under "user". This configuration takes place + of custom path location using the environment variable PIP_CONFIG_FILE + displayed under "env". + """ + + # Use new config file + new_config_file = get_configuration_files()[kinds.USER][1] + + script.pip("config", "--user", "set", "global.timeout", "60") + script.pip("config", "--user", "set", "freeze.timeout", "10") + + result = script.pip("config", "debug") + assert "{}, exists: True".format(new_config_file) in result.stdout + assert "global.timeout: 60" in result.stdout + assert "freeze.timeout: 10" in result.stdout + assert re.search(r"user:\n( .+\n)+", result.stdout) + + def test_site_values(self, script, virtualenv): + """Test that the current environment configuration set using --site + is correctly displayed under "site". + """ + + # Site config file will be inside the virtualenv + site_config_file = virtualenv.location / CONFIG_BASENAME + + script.pip("config", "--site", "set", "global.timeout", "60") + script.pip("config", "--site", "set", "freeze.timeout", "10") + + result = script.pip("config", "debug") + assert "{}, exists: True".format(site_config_file) in result.stdout + assert "global.timeout: 60" in result.stdout + assert "freeze.timeout: 10" in result.stdout + assert re.search(r"site:\n( .+\n)+", result.stdout) From 056f119ca91f5c57f210eee4386173fbd3dfebb1 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sun, 7 Jun 2020 16:09:17 +0530 Subject: [PATCH 2137/3170] Add test to verify global config file path --- tests/functional/test_configuration.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/functional/test_configuration.py b/tests/functional/test_configuration.py index 4c263516e46..63b243f788e 100644 --- a/tests/functional/test_configuration.py +++ b/tests/functional/test_configuration.py @@ -141,3 +141,15 @@ def test_site_values(self, script, virtualenv): assert "global.timeout: 60" in result.stdout assert "freeze.timeout: 10" in result.stdout assert re.search(r"site:\n( .+\n)+", result.stdout) + + def test_global_config_file(self, script): + """Test that the system-wide configuration can be identified""" + + # We cannot write to system-wide files which might have permissions + # defined in a way that the tox virtualenvcannot write to those + # locations. Additionally we cannot patch those paths since pip config + # commands runs inside a subprocess. + # So we just check if the file can be identified + global_config_file = get_configuration_files()[kinds.GLOBAL][0] + result = script.pip("config", "debug") + assert "{}, exists:".format(global_config_file) in result.stdout From 51f4c0322101406115b0afa91eddd66d1b9f1c0f Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sun, 24 May 2020 00:16:37 +0530 Subject: [PATCH 2138/3170] Warn if package url is a vcs or an archive url with invalid scheme --- src/pip/_internal/index/collector.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/index/collector.py b/src/pip/_internal/index/collector.py index 7908ab996e6..ef28a2e05f8 100644 --- a/src/pip/_internal/index/collector.py +++ b/src/pip/_internal/index/collector.py @@ -434,7 +434,7 @@ def _get_html_page(link, session=None): # Check for VCS schemes that do not support lookup as web pages. vcs_scheme = _match_vcs_scheme(url) if vcs_scheme: - logger.debug('Cannot look at %s URL %s', vcs_scheme, link) + logger.warning('Cannot look at %s URL %s', vcs_scheme, link) return None # Tack index.html onto file:// URLs that point to directories @@ -450,7 +450,7 @@ def _get_html_page(link, session=None): try: resp = _get_html_response(url, session=session) except _NotHTTP: - logger.debug( + logger.warning( 'Skipping page %s because it looks like an archive, and cannot ' 'be checked by HEAD.', link, ) From 499e1122757ba25cd7a028ad48aaa788a910f5b8 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sun, 24 May 2020 00:17:04 +0530 Subject: [PATCH 2139/3170] Add unit tests to check vcs and archive url with invalid scheme --- tests/unit/test_collector.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_collector.py b/tests/unit/test_collector.py index 0387813ad0a..56aea823a0c 100644 --- a/tests/unit/test_collector.py +++ b/tests/unit/test_collector.py @@ -74,6 +74,31 @@ def test_get_html_response_archive_to_http_scheme(url, content_type): assert ctx.value.args == (content_type, "HEAD") +@pytest.mark.parametrize( + "url", + [ + ("ftp://python.org/python-3.7.1.zip"), + ("file:///opt/data/pip-18.0.tar.gz"), + ], +) +def test_get_html_page_invalid_content_type_archive(caplog, url): + """`_get_html_page()` should warn if an archive URL is not HTML + and therefore cannot be used for a HEAD request. + """ + caplog.set_level(logging.WARNING) + link = Link(url) + + session = mock.Mock(PipSession) + + assert _get_html_page(link, session=session) is None + assert ('pip._internal.index.collector', + logging.WARNING, + 'Skipping page {} because it looks like an archive, and cannot ' + 'be checked by HEAD.'.format( + url)) \ + in caplog.record_tuples + + @pytest.mark.parametrize( "url", [ @@ -463,14 +488,14 @@ def test_get_html_page_invalid_scheme(caplog, url, vcs_scheme): Only file:, http:, https:, and ftp: are allowed. """ - with caplog.at_level(logging.DEBUG): + with caplog.at_level(logging.WARNING): page = _get_html_page(Link(url), session=mock.Mock(PipSession)) assert page is None assert caplog.record_tuples == [ ( "pip._internal.index.collector", - logging.DEBUG, + logging.WARNING, "Cannot look at {} URL {}".format(vcs_scheme, url), ), ] From 8d7af2da8d3be8f011929f4f438d9d180bdbe144 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Thu, 18 Jun 2020 19:48:04 +0530 Subject: [PATCH 2140/3170] Add news entry --- news/8128.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/8128.feature diff --git a/news/8128.feature b/news/8128.feature new file mode 100644 index 00000000000..9e180b50959 --- /dev/null +++ b/news/8128.feature @@ -0,0 +1 @@ +Warn if package url is a vcs or an archive url with invalid scheme From 46790a4f3c2b15cdc182608251f778f34d4da611 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Fri, 19 Jun 2020 10:11:05 +0530 Subject: [PATCH 2141/3170] Improve warning message for invalid vcs schemes --- src/pip/_internal/index/collector.py | 3 ++- tests/unit/test_collector.py | 3 ++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/index/collector.py b/src/pip/_internal/index/collector.py index ef28a2e05f8..7c9879fa3a2 100644 --- a/src/pip/_internal/index/collector.py +++ b/src/pip/_internal/index/collector.py @@ -434,7 +434,8 @@ def _get_html_page(link, session=None): # Check for VCS schemes that do not support lookup as web pages. vcs_scheme = _match_vcs_scheme(url) if vcs_scheme: - logger.warning('Cannot look at %s URL %s', vcs_scheme, link) + logger.warning('Cannot look at %s URL %s because it does not support ' + 'lookup as web pages.', vcs_scheme, link) return None # Tack index.html onto file:// URLs that point to directories diff --git a/tests/unit/test_collector.py b/tests/unit/test_collector.py index 56aea823a0c..beabbee8019 100644 --- a/tests/unit/test_collector.py +++ b/tests/unit/test_collector.py @@ -496,7 +496,8 @@ def test_get_html_page_invalid_scheme(caplog, url, vcs_scheme): ( "pip._internal.index.collector", logging.WARNING, - "Cannot look at {} URL {}".format(vcs_scheme, url), + "Cannot look at {} URL {} because it does not support " + "lookup as web pages.".format(vcs_scheme, url), ), ] From dd3a3b5eb7c2319d77a0efa9a0b245883564f5b3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Sat, 27 Jun 2020 15:50:29 +0700 Subject: [PATCH 2142/3170] Parallelize network operations in pip list --- news/8504.feature | 1 + src/pip/_internal/commands/list.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 news/8504.feature diff --git a/news/8504.feature b/news/8504.feature new file mode 100644 index 00000000000..06ab27112b6 --- /dev/null +++ b/news/8504.feature @@ -0,0 +1 @@ +Parallelize network operations in ``pip list``. diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index df9e1b38eda..35eff1c3593 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -19,6 +19,7 @@ write_output, ) from pip._internal.utils.packaging import get_installer +from pip._internal.utils.parallel import map_multithread from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: @@ -223,7 +224,7 @@ def latest_info(dist): dist.latest_filetype = typ return dist - for dist in map(latest_info, packages): + for dist in map_multithread(latest_info, packages): if dist is not None: yield dist From ecda6650ff37f4b15198917098c63f62659bad3b Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Mon, 29 Jun 2020 16:31:52 +0000 Subject: [PATCH 2143/3170] Apply suggestions from code review Co-authored-by: Xavier Fernandez <xav.fernandez@gmail.com> --- src/pip/_vendor/README.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_vendor/README.rst b/src/pip/_vendor/README.rst index 0194dbcdd8d..3b71e7436a8 100644 --- a/src/pip/_vendor/README.rst +++ b/src/pip/_vendor/README.rst @@ -8,7 +8,7 @@ Vendoring Policy * Vendored libraries **MUST** be released copies of libraries available on PyPI. -* Libraries to be vendored **MUST** be available under a license that allows +* Vendored libraries **MUST** be available under a license that allows them to be integrated into ``pip``, which is released under the MIT license. * Vendored libraries **MUST** be accompanied with LICENSE files. From 6c899ed0c88dba4e1576bfb8515737fc07418951 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Mon, 29 Jun 2020 22:20:11 +0530 Subject: [PATCH 2144/3170] Update warning message to specify HTTP request --- src/pip/_internal/index/collector.py | 2 +- tests/unit/test_collector.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/index/collector.py b/src/pip/_internal/index/collector.py index 7c9879fa3a2..8d60742247c 100644 --- a/src/pip/_internal/index/collector.py +++ b/src/pip/_internal/index/collector.py @@ -453,7 +453,7 @@ def _get_html_page(link, session=None): except _NotHTTP: logger.warning( 'Skipping page %s because it looks like an archive, and cannot ' - 'be checked by HEAD.', link, + 'be checked by a HTTP HEAD request.', link, ) except _NotHTML as exc: logger.warning( diff --git a/tests/unit/test_collector.py b/tests/unit/test_collector.py index beabbee8019..bf4ffbdc033 100644 --- a/tests/unit/test_collector.py +++ b/tests/unit/test_collector.py @@ -94,7 +94,7 @@ def test_get_html_page_invalid_content_type_archive(caplog, url): assert ('pip._internal.index.collector', logging.WARNING, 'Skipping page {} because it looks like an archive, and cannot ' - 'be checked by HEAD.'.format( + 'be checked by a HTTP HEAD request.'.format( url)) \ in caplog.record_tuples From da9c7c7f6948c8fd9f100c601299be82900e70c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Wed, 27 May 2020 16:27:25 +0700 Subject: [PATCH 2145/3170] Lint src/pip/_vendor/README.rst --- ...8f5c72-1c55-4c74-9d23-295563e7a7e7.trivial | 0 src/pip/_vendor/README.rst | 111 +++++++++--------- 2 files changed, 54 insertions(+), 57 deletions(-) create mode 100644 news/348f5c72-1c55-4c74-9d23-295563e7a7e7.trivial diff --git a/news/348f5c72-1c55-4c74-9d23-295563e7a7e7.trivial b/news/348f5c72-1c55-4c74-9d23-295563e7a7e7.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_vendor/README.rst b/src/pip/_vendor/README.rst index 3b71e7436a8..1fd0d439440 100644 --- a/src/pip/_vendor/README.rst +++ b/src/pip/_vendor/README.rst @@ -4,26 +4,19 @@ Vendoring Policy * Vendored libraries **MUST** not be modified except as required to successfully vendor them. - * Vendored libraries **MUST** be released copies of libraries available on PyPI. - * Vendored libraries **MUST** be available under a license that allows them to be integrated into ``pip``, which is released under the MIT license. - * Vendored libraries **MUST** be accompanied with LICENSE files. - * The versions of libraries vendored in pip **MUST** be reflected in ``pip/_vendor/vendor.txt``. - -* Vendored libraries **MUST** function without any build steps such as ``2to3`` or - compilation of C code, practically this limits to single source 2.x/3.x and +* Vendored libraries **MUST** function without any build steps such as ``2to3`` + or compilation of C code, practically this limits to single source 2.x/3.x and pure Python. - * Any modifications made to libraries **MUST** be noted in ``pip/_vendor/README.rst`` and their corresponding patches **MUST** be included ``tools/automation/vendoring/patches``. - * Vendored libraries should have corresponding ``vendored()`` entries in ``pip/_vendor/__init__.py``. @@ -41,41 +34,46 @@ higher quality and more battle tested code, centralization of bug fixes However, there are several issues with having dependencies in the traditional way (via ``install_requires``) for pip. These issues are: -* **Fragility.** When pip depends on another library to function then if for - whatever reason that library either isn't installed or an incompatible - version is installed then pip ceases to function. This is of course true for - all Python applications, however for every application *except* for pip the - way you fix it is by re-running pip. Obviously, when pip can't run, you can't - use pip to fix pip, so you're left having to manually resolve dependencies and - installing them by hand. - -* **Making other libraries uninstallable.** One of pip's current dependencies is - the ``requests`` library, for which pip requires a fairly recent version to run. - If pip depended on ``requests`` in the traditional manner, then we'd either - have to maintain compatibility with every ``requests`` version that has ever - existed (and ever will), OR allow pip to render certain versions of ``requests`` - uninstallable. (The second issue, although technically true for any Python - application, is magnified by pip's ubiquity; pip is installed by default in - Python, in ``pyvenv``, and in ``virtualenv``.) - -* **Security.** This might seem puzzling at first glance, since vendoring - has a tendency to complicate updating dependencies for security updates, - and that holds true for pip. However, given the *other* reasons for avoiding - dependencies, the alternative is for pip to reinvent the wheel itself. - This is what pip did historically. It forced pip to re-implement its own - HTTPS verification routines as a workaround for the Python standard library's - lack of SSL validation, which resulted in similar bugs in the validation routine - in ``requests`` and ``urllib3``, except that they had to be discovered and - fixed independently. Even though we're vendoring, reusing libraries keeps pip - more secure by relying on the great work of our dependencies, *and* allowing for - faster, easier security fixes by simply pulling in newer versions of dependencies. - -* **Bootstrapping.** Currently most popular methods of installing pip rely - on pip's self-contained nature to install pip itself. These tools work by bundling - a copy of pip, adding it to ``sys.path``, and then executing that copy of pip. - This is done instead of implementing a "mini installer" (to reduce duplication); - pip already knows how to install a Python package, and is far more battle-tested - than any "mini installer" could ever possibly be. +**Fragility** + When pip depends on another library to function then if for whatever reason + that library either isn't installed or an incompatible version is installed + then pip ceases to function. This is of course true for all Python + applications, however for every application *except* for pip the way you fix + it is by re-running pip. Obviously, when pip can't run, you can't use pip to + fix pip, so you're left having to manually resolve dependencies and + installing them by hand. + +**Making other libraries uninstallable** + One of pip's current dependencies is the ``requests`` library, for which pip + requires a fairly recent version to run. If pip depended on ``requests`` in + the traditional manner, then we'd either have to maintain compatibility with + every ``requests`` version that has ever existed (and ever will), OR allow + pip to render certain versions of ``requests`` uninstallable. (The second + issue, although technically true for any Python application, is magnified by + pip's ubiquity; pip is installed by default in Python, in ``pyvenv``, and in + ``virtualenv``.) + +**Security** + This might seem puzzling at first glance, since vendoring has a tendency to + complicate updating dependencies for security updates, and that holds true + for pip. However, given the *other* reasons for avoiding dependencies, the + alternative is for pip to reinvent the wheel itself. This is what pip did + historically. It forced pip to re-implement its own HTTPS verification + routines as a workaround for the Python standard library's lack of SSL + validation, which resulted in similar bugs in the validation routine in + ``requests`` and ``urllib3``, except that they had to be discovered and + fixed independently. Even though we're vendoring, reusing libraries keeps + pip more secure by relying on the great work of our dependencies, *and* + allowing for faster, easier security fixes by simply pulling in newer + versions of dependencies. + +**Bootstrapping** + Currently most popular methods of installing pip rely on pip's + self-contained nature to install pip itself. These tools work by bundling a + copy of pip, adding it to ``sys.path``, and then executing that copy of pip. + This is done instead of implementing a "mini installer" (to reduce + duplication); pip already knows how to install a Python package, and is far + more battle-tested than any "mini installer" could ever possibly be. Many downstream redistributors have policies against this kind of bundling, and instead opt to patch the software they distribute to debundle it and make it @@ -100,15 +98,19 @@ such as OS packages. Modifications ============= -* ``setuptools`` is completely stripped to only keep ``pkg_resources`` -* ``pkg_resources`` has been modified to import its dependencies from ``pip._vendor`` -* ``packaging`` has been modified to import its dependencies from ``pip._vendor`` +* ``setuptools`` is completely stripped to only keep ``pkg_resources``. +* ``pkg_resources`` has been modified to import its dependencies from + ``pip._vendor``. +* ``packaging`` has been modified to import its dependencies from + ``pip._vendor``. * ``html5lib`` has been modified to import six from ``pip._vendor``, to prefer - importing from ``collections.abc`` instead of ``collections`` and does not import - ``xml.etree.cElementTree`` on Python 3. -* ``CacheControl`` has been modified to import its dependencies from ``pip._vendor`` -* ``requests`` has been modified to import its other dependencies from ``pip._vendor`` - and to *not* load ``simplejson`` (all platforms) and ``pyopenssl`` (Windows). + importing from ``collections.abc`` instead of ``collections`` and does not + import ``xml.etree.cElementTree`` on Python 3. +* ``CacheControl`` has been modified to import its dependencies from + ``pip._vendor``. +* ``requests`` has been modified to import its other dependencies from + ``pip._vendor`` and to *not* load ``simplejson`` (all platforms) and + ``pyopenssl`` (Windows). Automatic Vendoring @@ -131,23 +133,18 @@ extra work on your end in order to solve the problems described above. 1. Delete everything in ``pip/_vendor/`` **except** for ``pip/_vendor/__init__.py``. - 2. Generate wheels for each of pip's dependencies (and any of their dependencies) using your patched copies of these libraries. These must be placed somewhere on the filesystem that pip can access (``pip/_vendor`` is the default assumption). - 3. Modify ``pip/_vendor/__init__.py`` so that the ``DEBUNDLED`` variable is ``True``. - 4. Upon installation, the ``INSTALLER`` file in pip's own ``dist-info`` directory should be set to something other than ``pip``, so that pip can detect that it wasn't installed using itself. - 5. *(optional)* If you've placed the wheels in a location other than ``pip/_vendor/``, then modify ``pip/_vendor/__init__.py`` so that the ``WHEEL_DIR`` variable points to the location you've placed them. - 6. *(optional)* Update the ``pip_self_version_check`` logic to use the appropriate logic for determining the latest available version of pip and prompt the user with the correct upgrade message. From 23ec070cf1ffa2449da276a78d6361de932c7792 Mon Sep 17 00:00:00 2001 From: Jussi Kukkonen <jku@goto.fi> Date: Thu, 2 Jul 2020 18:00:29 +0300 Subject: [PATCH 2146/3170] test server: use set of signals This is what signal.pthread_sigmask() really wants and matches signal.valid_signals() output Co-authored-by: Christopher Hunt <chrahunt@gmail.com> --- tests/lib/server.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lib/server.py b/tests/lib/server.py index 17cf758308e..6cdd1634fd8 100644 --- a/tests/lib/server.py +++ b/tests/lib/server.py @@ -47,7 +47,7 @@ def blocked_signals(): try: mask = signal.valid_signals() except AttributeError: - mask = range(1, signal.NSIG) + mask = set(range(1, signal.NSIG)) old_mask = signal.pthread_sigmask(signal.SIG_SETMASK, mask) try: From 09d75b8a58ed02e7506a72ef31025fd1767290bd Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 2 Jul 2020 20:40:02 -0400 Subject: [PATCH 2147/3170] Remove encouraging comments These comments are relevant to this function, since it is long overdue for refactoring. This code isn't special in that regard, and we should feel free to consider any piece of code eligible to be broken up or put into a class. So we remove these comments in fairness to the rest of the code, and to remove a distraction during upcoming code reviews. --- src/pip/_internal/operations/install/wheel.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 0ef865b007b..6996868b273 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -352,10 +352,6 @@ def install_unpacked_wheel( Wheel-Version * when the .dist-info dir does not match the wheel """ - # TODO: Investigate and break this up. - # TODO: Look into moving this into a dedicated class for representing an - # installation. - source = wheeldir.rstrip(os.path.sep) + os.path.sep info_dir, metadata = parse_wheel(wheel_zip, name) From 7c2fa59cd3de770e7b36c618e980e91f9f702729 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 2 Jul 2020 20:54:45 -0400 Subject: [PATCH 2148/3170] Add early return in fix_script No behavior change, just making this function easier to refactor. --- src/pip/_internal/operations/install/wheel.py | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 0ef865b007b..33e633a4146 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -96,19 +96,20 @@ def fix_script(path): Return True if file was changed. """ # XXX RECORD hashes will need to be updated - if os.path.isfile(path): - with open(path, 'rb') as script: - firstline = script.readline() - if not firstline.startswith(b'#!python'): - return False - exename = sys.executable.encode(sys.getfilesystemencoding()) - firstline = b'#!' + exename + os.linesep.encode("ascii") - rest = script.read() - with open(path, 'wb') as script: - script.write(firstline) - script.write(rest) - return True - return None + if not os.path.isfile(path): + return None + + with open(path, 'rb') as script: + firstline = script.readline() + if not firstline.startswith(b'#!python'): + return False + exename = sys.executable.encode(sys.getfilesystemencoding()) + firstline = b'#!' + exename + os.linesep.encode("ascii") + rest = script.read() + with open(path, 'wb') as script: + script.write(firstline) + script.write(rest) + return True def wheel_root_is_purelib(metadata): From 541ce8748a551832a537aa769413858d5fe494d6 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 2 Jul 2020 21:50:11 -0400 Subject: [PATCH 2149/3170] Remove redundant entrypoint text normalization Currently we do processing in `get_entrypoints` so incoming text is more compatible with `pkg_resources`. It turns out that `pkg_resources` is already doing the same normalization, so we can omit it. This simplifies `get_entrypoints`, opening the way for us to pass it a plain string instead of a file path. --- src/pip/_internal/operations/install/wheel.py | 10 +--------- 1 file changed, 1 insertion(+), 9 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 0ef865b007b..1dab0de7f3d 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -121,16 +121,8 @@ def get_entrypoints(filename): if not os.path.exists(filename): return {}, {} - # This is done because you can pass a string to entry_points wrappers which - # means that they may or may not be valid INI files. The attempt here is to - # strip leading and trailing whitespace in order to make them valid INI - # files. with io.open(filename, encoding="utf-8") as fp: - data = io.StringIO() - for line in fp: - data.write(line.strip()) - data.write(u"\n") - data.seek(0) + data = fp.read() # get the entry points and then the script names entry_points = pkg_resources.EntryPoint.parse_map(data) From 65e55e25409c82af97a9f4206a4716d7e38f75b7 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Mon, 25 May 2020 23:05:02 +0530 Subject: [PATCH 2150/3170] Type annotations for pip._internal.operations.check --- src/pip/_internal/operations/check.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/operations/check.py b/src/pip/_internal/operations/check.py index b85a12306a4..184676550c7 100644 --- a/src/pip/_internal/operations/check.py +++ b/src/pip/_internal/operations/check.py @@ -1,10 +1,6 @@ """Validation of dependencies of packages """ -# The following comment should be removed at some point in the future. -# mypy: strict-optional=False -# mypy: disallow-untyped-defs=False - import logging from collections import namedtuple @@ -65,8 +61,11 @@ def check_package_set(package_set, should_ignore=None): If should_ignore is passed, it should be a callable that takes a package name and returns a boolean. """ - if should_ignore is None: - def should_ignore(name): + if should_ignore: + should_ignore_package = should_ignore + else: + def should_ignore_package(name): + # type: (str) -> bool return False missing = {} @@ -77,7 +76,7 @@ def should_ignore(name): missing_deps = set() # type: Set[Missing] conflicting_deps = set() # type: Set[Conflicting] - if should_ignore(package_name): + if should_ignore_package(package_name): continue for req in package_set[package_name].requires: @@ -139,6 +138,7 @@ def _simulate_installation_of(to_install, package_set): abstract_dist = make_distribution_for_install_requirement(inst_req) dist = abstract_dist.get_pkg_resources_distribution() + assert dist is not None name = canonicalize_name(dist.key) package_set[name] = PackageDetails(dist.version, dist.requires()) From 940107d5ed9dc013e26d652e02a15616124518ff Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Mon, 25 May 2020 23:05:11 +0530 Subject: [PATCH 2151/3170] Type annotations for pip._internal.operations.freeze --- src/pip/_internal/operations/freeze.py | 18 ++++++++++-------- 1 file changed, 10 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/operations/freeze.py b/src/pip/_internal/operations/freeze.py index 9e0ea9696d2..a386d00ba0c 100644 --- a/src/pip/_internal/operations/freeze.py +++ b/src/pip/_internal/operations/freeze.py @@ -1,7 +1,3 @@ -# The following comment should be removed at some point in the future. -# mypy: strict-optional=False -# mypy: disallow-untyped-defs=False - from __future__ import absolute_import import collections @@ -60,10 +56,15 @@ def freeze( for link in find_links: yield '-f {}'.format(link) installations = {} # type: Dict[str, FrozenRequirement] - for dist in get_installed_distributions(local_only=local_only, - skip=(), - user_only=user_only, - paths=paths): + + # None is a Falsy value, so pass False if local_only + # and user_only is None + for dist in get_installed_distributions( + local_only=local_only if local_only else False, + skip=(), + user_only=user_only if user_only else False, + paths=paths + ): try: req = FrozenRequirement.from_dist(dist) except RequirementParseError as exc: @@ -265,6 +266,7 @@ def from_dist(cls, dist): return cls(dist.project_name, req, editable, comments=comments) def __str__(self): + # type: () -> str req = self.req if self.editable: req = '-e {}'.format(req) From 7e05571117cb1df2be04fbaecb0ac254be3e2c13 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Tue, 26 May 2020 02:02:08 +0530 Subject: [PATCH 2152/3170] Type annotations in pip._internal.utils.subprocess --- src/pip/_internal/utils/subprocess.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/utils/subprocess.py b/src/pip/_internal/utils/subprocess.py index 55c82daea7c..bdebaef7827 100644 --- a/src/pip/_internal/utils/subprocess.py +++ b/src/pip/_internal/utils/subprocess.py @@ -1,6 +1,3 @@ -# The following comment should be removed at some point in the future. -# mypy: strict-optional=False - from __future__ import absolute_import import logging @@ -188,7 +185,8 @@ def call_subprocess( stderr=subprocess.STDOUT, stdin=subprocess.PIPE, stdout=subprocess.PIPE, cwd=cwd, env=env, ) - proc.stdin.close() + if proc.stdin: + proc.stdin.close() except Exception as exc: if log_failed_cmd: subprocess_logger.critical( @@ -198,7 +196,9 @@ def call_subprocess( all_output = [] while True: # The "line" value is a unicode string in Python 2. - line = console_to_str(proc.stdout.readline()) + line = None + if proc.stdout: + line = console_to_str(proc.stdout.readline()) if not line: break line = line.rstrip() @@ -207,7 +207,7 @@ def call_subprocess( # Show the line immediately. log_subprocess(line) # Update the spinner. - if use_spinner: + if use_spinner and spinner: spinner.spin() try: proc.wait() @@ -217,7 +217,7 @@ def call_subprocess( proc_had_error = ( proc.returncode and proc.returncode not in extra_ok_returncodes ) - if use_spinner: + if use_spinner and spinner: if proc_had_error: spinner.finish("error") else: From 126d1de990d9042cef6bfdea0bcf611dce8312fc Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Tue, 26 May 2020 02:04:27 +0530 Subject: [PATCH 2153/3170] Add news entry --- news/FD7C8A6F-FA2D-4CDD-9C4B-D51412EC6619.trivial | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 news/FD7C8A6F-FA2D-4CDD-9C4B-D51412EC6619.trivial diff --git a/news/FD7C8A6F-FA2D-4CDD-9C4B-D51412EC6619.trivial b/news/FD7C8A6F-FA2D-4CDD-9C4B-D51412EC6619.trivial new file mode 100644 index 00000000000..e69de29bb2d From 59e19ea74396567b319086a09a39688405c50294 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Tue, 26 May 2020 09:39:13 +0530 Subject: [PATCH 2154/3170] Update type of local_only and user_only to bool --- src/pip/_internal/operations/freeze.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/operations/freeze.py b/src/pip/_internal/operations/freeze.py index a386d00ba0c..e15f3e3da0b 100644 --- a/src/pip/_internal/operations/freeze.py +++ b/src/pip/_internal/operations/freeze.py @@ -42,8 +42,8 @@ def freeze( requirement=None, # type: Optional[List[str]] find_links=None, # type: Optional[List[str]] - local_only=None, # type: Optional[bool] - user_only=None, # type: Optional[bool] + local_only=False, # type: bool + user_only=False, # type: bool paths=None, # type: Optional[List[str]] isolated=False, # type: bool wheel_cache=None, # type: Optional[WheelCache] @@ -57,12 +57,10 @@ def freeze( yield '-f {}'.format(link) installations = {} # type: Dict[str, FrozenRequirement] - # None is a Falsy value, so pass False if local_only - # and user_only is None for dist in get_installed_distributions( - local_only=local_only if local_only else False, + local_only=local_only, skip=(), - user_only=user_only if user_only else False, + user_only=user_only, paths=paths ): try: From da2448ca13b2415aa0e692582859a235bc128131 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Tue, 26 May 2020 13:39:24 +0530 Subject: [PATCH 2155/3170] Assert proc.stdin and proc.stdout --- src/pip/_internal/utils/subprocess.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/utils/subprocess.py b/src/pip/_internal/utils/subprocess.py index bdebaef7827..d93801b2ac4 100644 --- a/src/pip/_internal/utils/subprocess.py +++ b/src/pip/_internal/utils/subprocess.py @@ -185,8 +185,9 @@ def call_subprocess( stderr=subprocess.STDOUT, stdin=subprocess.PIPE, stdout=subprocess.PIPE, cwd=cwd, env=env, ) - if proc.stdin: - proc.stdin.close() + assert proc.stdin + assert proc.stdout + proc.stdin.close() except Exception as exc: if log_failed_cmd: subprocess_logger.critical( @@ -196,9 +197,7 @@ def call_subprocess( all_output = [] while True: # The "line" value is a unicode string in Python 2. - line = None - if proc.stdout: - line = console_to_str(proc.stdout.readline()) + line = console_to_str(proc.stdout.readline()) if not line: break line = line.rstrip() From d3295265e138b7b83d48ec5e7e0cbf75b246cd56 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Fri, 26 Jun 2020 14:27:25 +0530 Subject: [PATCH 2156/3170] Assert spinner instead of if condition check --- src/pip/_internal/utils/subprocess.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/utils/subprocess.py b/src/pip/_internal/utils/subprocess.py index d93801b2ac4..1cffec085c5 100644 --- a/src/pip/_internal/utils/subprocess.py +++ b/src/pip/_internal/utils/subprocess.py @@ -206,7 +206,8 @@ def call_subprocess( # Show the line immediately. log_subprocess(line) # Update the spinner. - if use_spinner and spinner: + if use_spinner: + assert spinner spinner.spin() try: proc.wait() @@ -216,7 +217,8 @@ def call_subprocess( proc_had_error = ( proc.returncode and proc.returncode not in extra_ok_returncodes ) - if use_spinner and spinner: + if use_spinner: + assert spinner if proc_had_error: spinner.finish("error") else: From 245bcd0456d4d1a9ef722283dce28dce1cbe1b99 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sat, 27 Jun 2020 14:51:33 +0530 Subject: [PATCH 2157/3170] Check should_ignore in if condition and remove function creation --- src/pip/_internal/operations/check.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/pip/_internal/operations/check.py b/src/pip/_internal/operations/check.py index 184676550c7..87c265dada5 100644 --- a/src/pip/_internal/operations/check.py +++ b/src/pip/_internal/operations/check.py @@ -61,12 +61,6 @@ def check_package_set(package_set, should_ignore=None): If should_ignore is passed, it should be a callable that takes a package name and returns a boolean. """ - if should_ignore: - should_ignore_package = should_ignore - else: - def should_ignore_package(name): - # type: (str) -> bool - return False missing = {} conflicting = {} @@ -76,7 +70,7 @@ def should_ignore_package(name): missing_deps = set() # type: Set[Missing] conflicting_deps = set() # type: Set[Conflicting] - if should_ignore_package(package_name): + if should_ignore and should_ignore(package_name): continue for req in package_set[package_name].requires: From d93b8b3e5340fa63d50efe4892d567b848f30774 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 2 Jul 2020 21:10:41 -0400 Subject: [PATCH 2158/3170] Do not shadow outer variable in loop `source` is provided as an argument to this function. Shadowing it can lead to type errors if the intermediate types change. --- src/pip/_internal/operations/install/wheel.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 33e633a4146..71253c944b5 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -493,10 +493,12 @@ def is_entrypoint_wrapper(name): if subdir == 'scripts': fixer = fix_script filter = is_entrypoint_wrapper - source = os.path.join(wheeldir, datadir, subdir) + full_datadir_path = os.path.join(wheeldir, datadir, subdir) dest = getattr(scheme, subdir) clobber( - ensure_text(source, encoding=sys.getfilesystemencoding()), + ensure_text( + full_datadir_path, encoding=sys.getfilesystemencoding() + ), ensure_text(dest, encoding=sys.getfilesystemencoding()), False, fixer=fixer, From 01e0d8befbdd74e0be5dad2f90547947d7a97fd9 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 2 Jul 2020 21:10:42 -0400 Subject: [PATCH 2159/3170] Get data directories directly from zip This reduces our dependence on the files being extracted to the filesystem. Compare the name extraction to the similar code in `utils.wheel.wheel_dist_info_dir`. We don't need to give `.data` directories the same strict treatment (yet) because it isn't inconvenient if there happen to be multiple of them in a single Wheel file. --- src/pip/_internal/operations/install/wheel.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 71253c944b5..d35cac1d03b 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -366,7 +366,8 @@ def install_unpacked_wheel( else: lib_dir = scheme.platlib - subdirs = os.listdir(source) + # Zip file path separators must be / + subdirs = set(p.split("/", 1)[0] for p in wheel_zip.namelist()) data_dirs = [s for s in subdirs if s.endswith('.data')] # Record details of the files moved From abed1d6d39bcfdf8ee9b1ffdc94607bb47826222 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 2 Jul 2020 21:15:01 -0400 Subject: [PATCH 2160/3170] Set `data_dirs` closer to first use Reducing the scope of variables reduces possible dependencies between parts of this function, and will make it easier to extract this section into its own function. --- src/pip/_internal/operations/install/wheel.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index d35cac1d03b..b9cecc78ae0 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -366,10 +366,6 @@ def install_unpacked_wheel( else: lib_dir = scheme.platlib - # Zip file path separators must be / - subdirs = set(p.split("/", 1)[0] for p in wheel_zip.namelist()) - data_dirs = [s for s in subdirs if s.endswith('.data')] - # Record details of the files moved # installed = files copied from the wheel to the destination # changed = files changed while installing (scripts #! line typically) @@ -486,6 +482,10 @@ def is_entrypoint_wrapper(name): # Ignore setuptools-generated scripts return (matchname in console or matchname in gui) + # Zip file path separators must be / + subdirs = set(p.split("/", 1)[0] for p in wheel_zip.namelist()) + data_dirs = [s for s in subdirs if s.endswith('.data')] + for datadir in data_dirs: fixer = None filter = None From 8259528ed9573120e0110258f341f47f14df440a Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 3 Jul 2020 09:02:30 -0400 Subject: [PATCH 2161/3170] Simplify and optimize getting zip subdirs Since we only care about the first path part, we can stop at 1 split. We do not need a list, so the unnecessary conversion has been dropped. --- src/pip/_internal/utils/wheel.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/utils/wheel.py b/src/pip/_internal/utils/wheel.py index 3ebb7710bc6..9ce371c76eb 100644 --- a/src/pip/_internal/utils/wheel.py +++ b/src/pip/_internal/utils/wheel.py @@ -121,7 +121,7 @@ def wheel_dist_info_dir(source, name): it doesn't match the provided name. """ # Zip file path separators must be / - subdirs = list(set(p.split("/")[0] for p in source.namelist())) + subdirs = set(p.split("/", 1)[0] for p in source.namelist()) info_dirs = [s for s in subdirs if s.endswith('.dist-info')] From cd95531951895d0b5926cc223c40190d0fe8a996 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Fri, 3 Jul 2020 18:44:02 +0530 Subject: [PATCH 2162/3170] Add --use-feature and --deprecated-feature flags --- src/pip/_internal/cli/cmdoptions.py | 29 +++++++++++++++++++++++++++-- 1 file changed, 27 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index d1525bafe8a..dd6907bd13b 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -906,8 +906,31 @@ def check_list_path_option(options): action='append', default=[], choices=['resolver'], - help=SUPPRESS_HELP, # TODO: Enable this when the resolver actually works. - # help='Enable unstable feature(s) that may be backward incompatible.', + help=SUPPRESS_HELP, # TODO: drop this in pip 20.3 +) # type: Callable[..., Option] + +use_new_feature = partial( + Option, + '--use-feature', + dest='features_enabled', + metavar='feature', + action='append', + default=[], + choices=['2020-resolver'], + help='Enable new functionality, that may be backward incompatible.', +) # type: Callable[..., Option] + +use_deprecated_feature = partial( + Option, + '--deprecated-feature', + dest='deprecated_features_enabled', + metavar='feature', + action='append', + default=[], + choices=[], + help=( + 'Enable deprecated functionality, that will be removed in the future.' + ), ) # type: Callable[..., Option] @@ -939,6 +962,8 @@ def check_list_path_option(options): no_color, no_python_version_warning, unstable_feature, + use_new_feature, + use_deprecated_feature, ] } # type: Dict[str, Any] From 79de2c8911eee4b4c7a8ec984647c8771078a3b0 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Fri, 3 Jul 2020 18:44:30 +0530 Subject: [PATCH 2163/3170] Switch to --use-feature for determining which resolver to use Also changes all invocations in the tests, to the new flag. --- src/pip/_internal/cli/req_command.py | 2 +- tests/functional/test_install.py | 2 +- tests/functional/test_new_resolver.py | 88 +++++++++++----------- tests/functional/test_new_resolver_user.py | 28 +++---- tests/functional/test_yaml.py | 2 +- 5 files changed, 61 insertions(+), 61 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index a215d459314..0cb897e184c 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -259,7 +259,7 @@ def make_resolver( # The long import name and duplicated invocation is needed to convince # Mypy into correctly typechecking. Otherwise it would complain the # "Resolver" class being redefined. - if 'resolver' in options.unstable_features: + if '2020-resolver' in options.features_enabled: import pip._internal.resolution.resolvelib.resolver return pip._internal.resolution.resolvelib.resolver.Resolver( preparer=preparer, diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 5f190769f69..cb291a61ef6 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -405,7 +405,7 @@ def test_vcs_url_urlquote_normalization(script, tmpdir): ) -@pytest.mark.parametrize("resolver", ["", "--unstable-feature=resolver"]) +@pytest.mark.parametrize("resolver", ["", "--use-feature=2020-resolver"]) def test_basic_install_from_local_directory(script, data, resolver): """ Test installing from a local directory. diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 001ff033884..202a4b2b404 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -52,7 +52,7 @@ def test_new_resolver_can_install(script): "0.1.0", ) script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "simple" @@ -67,7 +67,7 @@ def test_new_resolver_can_install_with_version(script): "0.1.0", ) script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "simple==0.1.0" @@ -87,7 +87,7 @@ def test_new_resolver_picks_latest_version(script): "0.2.0", ) script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "simple" @@ -107,7 +107,7 @@ def test_new_resolver_picks_installed_version(script): "0.2.0", ) script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "simple==0.1.0" @@ -115,7 +115,7 @@ def test_new_resolver_picks_installed_version(script): assert_installed(script, simple="0.1.0") result = script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "simple" @@ -136,7 +136,7 @@ def test_new_resolver_picks_installed_version_if_no_match_found(script): "0.2.0", ) script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "simple==0.1.0" @@ -144,7 +144,7 @@ def test_new_resolver_picks_installed_version_if_no_match_found(script): assert_installed(script, simple="0.1.0") result = script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "simple" ) @@ -165,7 +165,7 @@ def test_new_resolver_installs_dependencies(script): "0.1.0", ) script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "base" @@ -186,7 +186,7 @@ def test_new_resolver_ignore_dependencies(script): "0.1.0", ) script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--no-deps", "--find-links", script.scratch_path, "base" @@ -220,7 +220,7 @@ def test_new_resolver_installs_extras(tmpdir, script, root_dep): "0.1.0", ) script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "-r", req_file, @@ -241,7 +241,7 @@ def test_new_resolver_installs_extras_warn_missing(script): "0.1.0", ) result = script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "base[add,missing]", @@ -255,7 +255,7 @@ def test_new_resolver_installs_extras_warn_missing(script): def test_new_resolver_installed_message(script): create_basic_wheel_for_package(script, "A", "1.0") result = script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "A", @@ -267,7 +267,7 @@ def test_new_resolver_installed_message(script): def test_new_resolver_no_dist_message(script): create_basic_wheel_for_package(script, "A", "1.0") result = script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "B", @@ -298,7 +298,7 @@ def test_new_resolver_installs_editable(script): version="0.1.0", ) script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "base", @@ -346,7 +346,7 @@ def test_new_resolver_requires_python( args = [ "install", - "--unstable-feature=resolver", + "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, @@ -368,7 +368,7 @@ def test_new_resolver_requires_python_error(script): requires_python="<2", ) result = script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "base", @@ -396,7 +396,7 @@ def test_new_resolver_installed(script): ) result = script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "base", @@ -404,7 +404,7 @@ def test_new_resolver_installed(script): assert "Requirement already satisfied" not in result.stdout, str(result) result = script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "base~=0.1.0", @@ -426,7 +426,7 @@ def test_new_resolver_ignore_installed(script): satisfied_output = "Requirement already satisfied" result = script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "base", @@ -434,7 +434,7 @@ def test_new_resolver_ignore_installed(script): assert satisfied_output not in result.stdout, str(result) result = script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--ignore-installed", "--find-links", script.scratch_path, "base", @@ -467,7 +467,7 @@ def test_new_resolver_only_builds_sdists_when_needed(script): ) # We only ever need to check dep 0.2.0 as it's the latest version script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "base" @@ -476,7 +476,7 @@ def test_new_resolver_only_builds_sdists_when_needed(script): # We merge criteria here, as we have two "dep" requirements script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "base", "dep" @@ -489,7 +489,7 @@ def test_new_resolver_install_different_version(script): create_basic_wheel_for_package(script, "base", "0.2.0") script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "base==0.1.0", @@ -497,7 +497,7 @@ def test_new_resolver_install_different_version(script): # This should trigger an uninstallation of base. result = script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "base==0.2.0", @@ -516,7 +516,7 @@ def test_new_resolver_force_reinstall(script): create_basic_wheel_for_package(script, "base", "0.1.0") script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "base==0.1.0", @@ -525,7 +525,7 @@ def test_new_resolver_force_reinstall(script): # This should trigger an uninstallation of base due to --force-reinstall, # even though the installed version matches. result = script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "--force-reinstall", @@ -564,7 +564,7 @@ def test_new_resolver_handles_prerelease( for version in available_versions: create_basic_wheel_for_package(script, "pkg", version) script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, *pip_args @@ -586,7 +586,7 @@ def test_new_reolver_skips_marker(script, pkg_deps, root_deps): create_basic_wheel_for_package(script, "dep", "1.0") script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, *root_deps @@ -611,7 +611,7 @@ def test_new_resolver_constraints(script, constraints): constraints_file = script.scratch_path / "constraints.txt" constraints_file.write_text("\n".join(constraints)) script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "-c", constraints_file, @@ -627,7 +627,7 @@ def test_new_resolver_constraint_no_specifier(script): constraints_file = script.scratch_path / "constraints.txt" constraints_file.write_text("pkg") script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "-c", constraints_file, @@ -658,7 +658,7 @@ def test_new_resolver_constraint_reject_invalid(script, constraint, error): constraints_file = script.scratch_path / "constraints.txt" constraints_file.write_text(constraint) result = script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "-c", constraints_file, @@ -677,7 +677,7 @@ def test_new_resolver_constraint_on_dependency(script): constraints_file = script.scratch_path / "constraints.txt" constraints_file.write_text("dep==2.0") script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "-c", constraints_file, @@ -694,7 +694,7 @@ def test_new_resolver_constraint_on_path(script): constraints_txt = script.scratch_path / "constraints.txt" constraints_txt.write_text("foo==1.0") result = script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "-c", constraints_txt, str(script.scratch_path), @@ -709,7 +709,7 @@ def test_new_resolver_upgrade_needs_option(script): # Install pkg 1.0.0 create_basic_wheel_for_package(script, "pkg", "1.0.0") script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "pkg", @@ -720,7 +720,7 @@ def test_new_resolver_upgrade_needs_option(script): # This should not upgrade because we don't specify --upgrade result = script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "pkg", @@ -731,7 +731,7 @@ def test_new_resolver_upgrade_needs_option(script): # This should upgrade result = script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "--upgrade", @@ -751,7 +751,7 @@ def test_new_resolver_upgrade_strategy(script): create_basic_wheel_for_package(script, "base", "1.0.0", depends=["dep"]) create_basic_wheel_for_package(script, "dep", "1.0.0") script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "base", @@ -765,7 +765,7 @@ def test_new_resolver_upgrade_strategy(script): create_basic_wheel_for_package(script, "dep", "2.0.0") script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "--upgrade", @@ -779,7 +779,7 @@ def test_new_resolver_upgrade_strategy(script): create_basic_wheel_for_package(script, "base", "3.0.0", depends=["dep"]) script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "--upgrade", "--upgrade-strategy=eager", @@ -858,7 +858,7 @@ def test_new_resolver_extra_merge_in_package( ) script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, requirement + "[dev]", @@ -897,7 +897,7 @@ def test_new_resolver_build_directory_error_zazo_19(script): create_basic_sdist_for_package(script, "pkg_b", "1.0.0") script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "pkg-a", "pkg-b", @@ -910,7 +910,7 @@ def test_new_resolver_upgrade_same_version(script): create_basic_wheel_for_package(script, "pkg", "1") script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "pkg", @@ -918,7 +918,7 @@ def test_new_resolver_upgrade_same_version(script): assert_installed(script, pkg="2") script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "--upgrade", @@ -934,7 +934,7 @@ def test_new_resolver_local_and_req(script): version="0.1.0", ) script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", source_dir, "pkg!=0.1.0", expect_error=True, diff --git a/tests/functional/test_new_resolver_user.py b/tests/functional/test_new_resolver_user.py index 3576b8a67dd..2aae3eb16cb 100644 --- a/tests/functional/test_new_resolver_user.py +++ b/tests/functional/test_new_resolver_user.py @@ -10,7 +10,7 @@ def test_new_resolver_install_user(script): create_basic_wheel_for_package(script, "base", "0.1.0") result = script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "--user", @@ -28,13 +28,13 @@ def test_new_resolver_install_user_satisfied_by_global_site(script): create_basic_wheel_for_package(script, "base", "1.0.0") script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "base==1.0.0", ) result = script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "--user", @@ -54,7 +54,7 @@ def test_new_resolver_install_user_conflict_in_user_site(script): create_basic_wheel_for_package(script, "base", "2.0.0") script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "--user", @@ -62,7 +62,7 @@ def test_new_resolver_install_user_conflict_in_user_site(script): ) result = script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "--user", @@ -82,13 +82,13 @@ def test_new_resolver_install_user_in_virtualenv_with_conflict_fails(script): create_basic_wheel_for_package(script, "base", "2.0.0") script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "base==2.0.0", ) result = script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "--user", @@ -128,13 +128,13 @@ def test_new_resolver_install_user_reinstall_global_site(script): create_basic_wheel_for_package(script, "base", "1.0.0") script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "base==1.0.0", ) result = script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "--user", @@ -159,14 +159,14 @@ def test_new_resolver_install_user_conflict_in_global_site(script): create_basic_wheel_for_package(script, "base", "2.0.0") script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "base==1.0.0", ) result = script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "--user", @@ -191,13 +191,13 @@ def test_new_resolver_install_user_conflict_in_global_and_user_sites(script): create_basic_wheel_for_package(script, "base", "2.0.0") script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "base==2.0.0", ) script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "--user", @@ -206,7 +206,7 @@ def test_new_resolver_install_user_conflict_in_global_and_user_sites(script): ) result = script.pip( - "install", "--unstable-feature=resolver", + "install", "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "--user", diff --git a/tests/functional/test_yaml.py b/tests/functional/test_yaml.py index ba6ff47de24..e22a01ace91 100644 --- a/tests/functional/test_yaml.py +++ b/tests/functional/test_yaml.py @@ -88,7 +88,7 @@ def handle_request(script, action, requirement, options, new_resolver=False): if action == 'install': args = ['install'] if new_resolver: - args.append("--unstable-feature=resolver") + args.append("--use-feature=2020-resolver") args.extend(["--no-index", "--find-links", path_to_url(script.scratch_path)]) elif action == 'uninstall': From 26e29aa70e603db8162115df14f4ef9258265a8e Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Fri, 3 Jul 2020 18:43:27 +0530 Subject: [PATCH 2164/3170] Disallow --unstable-feature, pointing to --use-feature instead --- src/pip/_internal/cli/base_command.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index 52027e6e804..4dd6baa2386 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -204,6 +204,13 @@ def _main(self, args): issue=8333, ) + if 'resolver' in options.unstable_features: + logger.critical( + "--unstable-feature=resolver is no longer supported, and " + "has been replaced with --use-feature=2020-resolver instead." + ) + sys.exit(ERROR) + try: status = self.run(options, args) assert isinstance(status, int) From a8eaf11d7fba59d7909bad9fe962fbdf8d4adfbe Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Fri, 3 Jul 2020 19:20:58 +0530 Subject: [PATCH 2165/3170] Use the correct flag name MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Stéphane Bidoul <stephane.bidoul@acsone.eu> --- src/pip/_internal/cli/cmdoptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index dd6907bd13b..2a4c230f6b5 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -922,7 +922,7 @@ def check_list_path_option(options): use_deprecated_feature = partial( Option, - '--deprecated-feature', + '--use-deprecated', dest='deprecated_features_enabled', metavar='feature', action='append', From 49b793cd037d4c1d708883cb4ccde85be35951f0 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Fri, 3 Jul 2020 20:11:36 +0530 Subject: [PATCH 2166/3170] Set correct envvar for new-resolver tests --- tests/conftest.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index d57c66896e6..0db6d96725e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -105,9 +105,9 @@ def use_new_resolver(request): """ new_resolver = request.config.getoption("--new-resolver") if new_resolver: - os.environ["PIP_UNSTABLE_FEATURE"] = "resolver" + os.environ["PIP_USE_FEATURE"] = "2020-resolver" else: - os.environ.pop("PIP_UNSTABLE_FEATURE", None) + os.environ.pop("PIP_USE_FEATURE", None) yield new_resolver From dcd5cadcfddca7e0554cad57018bda7641465f60 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 3 Jul 2020 12:25:12 -0400 Subject: [PATCH 2167/3170] Expect a plain list in get_csv_rows_for_installed This makes get_csv_rows_for_installed simpler, because it is not modifying its arguments. We can also more easily refactor RECORD file reading since it is now decoupled from getting the installed RECORD file rows. --- src/pip/_internal/operations/install/wheel.py | 17 ++++++++++------- tests/unit/test_wheel.py | 10 +++++----- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 2fa2041dc18..c9edd0d857a 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -265,7 +265,7 @@ def _parse_record_path(record_column): def get_csv_rows_for_installed( - old_csv_rows, # type: Iterable[List[str]] + old_csv_rows, # type: List[List[str]] installed, # type: Dict[RecordPath, RecordPath] changed, # type: Set[RecordPath] generated, # type: List[str] @@ -642,12 +642,15 @@ def _generate_file(path, **kwargs): # Record details of all files installed record_path = os.path.join(dest_info_dir, 'RECORD') with open(record_path, **csv_io_kwargs('r')) as record_file: - rows = get_csv_rows_for_installed( - csv.reader(record_file), - installed=installed, - changed=changed, - generated=generated, - lib_dir=lib_dir) + record_rows = list(csv.reader(record_file)) + + rows = get_csv_rows_for_installed( + record_rows, + installed=installed, + changed=changed, + generated=generated, + lib_dir=lib_dir) + with _generate_file(record_path, **csv_io_kwargs('w')) as record_file: # The type mypy infers for record_file is different for Python 3 # (typing.IO[Any]) and Python 2 (typing.BinaryIO). We explicitly diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 0de1b8952ed..8e1b3194219 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -163,11 +163,11 @@ def call_get_csv_rows_for_installed(tmpdir, text): lib_dir = '/lib/dir' with open(path, **wheel.csv_io_kwargs('r')) as f: - reader = csv.reader(f) - outrows = wheel.get_csv_rows_for_installed( - reader, installed=installed, changed=changed, - generated=generated, lib_dir=lib_dir, - ) + record_rows = list(csv.reader(f)) + outrows = wheel.get_csv_rows_for_installed( + record_rows, installed=installed, changed=changed, + generated=generated, lib_dir=lib_dir, + ) return outrows From 94421cfb1add27c0ccaac13fe43246dcd7136315 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 3 Jul 2020 17:43:33 -0400 Subject: [PATCH 2168/3170] Extract console script spec calculation from install_wheel This big chunk of code was independent of the rest of our wheel installation process. Moving it out enforces that there are no dependencies between it and the original function, and makes it easier to read the original function. --- src/pip/_internal/operations/install/wheel.py | 159 ++++++++++-------- 1 file changed, 87 insertions(+), 72 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index c9edd0d857a..a99c5da3749 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -299,6 +299,92 @@ def get_csv_rows_for_installed( return installed_rows +def get_console_script_specs(console): + # type: (Dict[str, str]) -> List[str] + """ + Given the mapping from entrypoint name to callable, return the relevant + console script specs. + """ + # Don't mutate caller's version + console = console.copy() + + scripts_to_generate = [] + + # Special case pip and setuptools to generate versioned wrappers + # + # The issue is that some projects (specifically, pip and setuptools) use + # code in setup.py to create "versioned" entry points - pip2.7 on Python + # 2.7, pip3.3 on Python 3.3, etc. But these entry points are baked into + # the wheel metadata at build time, and so if the wheel is installed with + # a *different* version of Python the entry points will be wrong. The + # correct fix for this is to enhance the metadata to be able to describe + # such versioned entry points, but that won't happen till Metadata 2.0 is + # available. + # In the meantime, projects using versioned entry points will either have + # incorrect versioned entry points, or they will not be able to distribute + # "universal" wheels (i.e., they will need a wheel per Python version). + # + # Because setuptools and pip are bundled with _ensurepip and virtualenv, + # we need to use universal wheels. So, as a stopgap until Metadata 2.0, we + # override the versioned entry points in the wheel and generate the + # correct ones. This code is purely a short-term measure until Metadata 2.0 + # is available. + # + # To add the level of hack in this section of code, in order to support + # ensurepip this code will look for an ``ENSUREPIP_OPTIONS`` environment + # variable which will control which version scripts get installed. + # + # ENSUREPIP_OPTIONS=altinstall + # - Only pipX.Y and easy_install-X.Y will be generated and installed + # ENSUREPIP_OPTIONS=install + # - pipX.Y, pipX, easy_install-X.Y will be generated and installed. Note + # that this option is technically if ENSUREPIP_OPTIONS is set and is + # not altinstall + # DEFAULT + # - The default behavior is to install pip, pipX, pipX.Y, easy_install + # and easy_install-X.Y. + pip_script = console.pop('pip', None) + if pip_script: + if "ENSUREPIP_OPTIONS" not in os.environ: + scripts_to_generate.append('pip = ' + pip_script) + + if os.environ.get("ENSUREPIP_OPTIONS", "") != "altinstall": + scripts_to_generate.append( + 'pip{} = {}'.format(sys.version_info[0], pip_script) + ) + + scripts_to_generate.append( + 'pip{} = {}'.format(get_major_minor_version(), pip_script) + ) + # Delete any other versioned pip entry points + pip_ep = [k for k in console if re.match(r'pip(\d(\.\d)?)?$', k)] + for k in pip_ep: + del console[k] + easy_install_script = console.pop('easy_install', None) + if easy_install_script: + if "ENSUREPIP_OPTIONS" not in os.environ: + scripts_to_generate.append( + 'easy_install = ' + easy_install_script + ) + + scripts_to_generate.append( + 'easy_install-{} = {}'.format( + get_major_minor_version(), easy_install_script + ) + ) + # Delete any other versioned easy_install entry points + easy_install_ep = [ + k for k in console if re.match(r'easy_install(-\d\.\d)?$', k) + ] + for k in easy_install_ep: + del console[k] + + # Generate the console entry points specified in the wheel + scripts_to_generate.extend(starmap('{} = {}'.format, console.items())) + + return scripts_to_generate + + class MissingCallableSuffix(Exception): pass @@ -510,79 +596,8 @@ def is_entrypoint_wrapper(name): # See https://bitbucket.org/pypa/distlib/issue/32/ maker.set_mode = True - scripts_to_generate = [] - - # Special case pip and setuptools to generate versioned wrappers - # - # The issue is that some projects (specifically, pip and setuptools) use - # code in setup.py to create "versioned" entry points - pip2.7 on Python - # 2.7, pip3.3 on Python 3.3, etc. But these entry points are baked into - # the wheel metadata at build time, and so if the wheel is installed with - # a *different* version of Python the entry points will be wrong. The - # correct fix for this is to enhance the metadata to be able to describe - # such versioned entry points, but that won't happen till Metadata 2.0 is - # available. - # In the meantime, projects using versioned entry points will either have - # incorrect versioned entry points, or they will not be able to distribute - # "universal" wheels (i.e., they will need a wheel per Python version). - # - # Because setuptools and pip are bundled with _ensurepip and virtualenv, - # we need to use universal wheels. So, as a stopgap until Metadata 2.0, we - # override the versioned entry points in the wheel and generate the - # correct ones. This code is purely a short-term measure until Metadata 2.0 - # is available. - # - # To add the level of hack in this section of code, in order to support - # ensurepip this code will look for an ``ENSUREPIP_OPTIONS`` environment - # variable which will control which version scripts get installed. - # - # ENSUREPIP_OPTIONS=altinstall - # - Only pipX.Y and easy_install-X.Y will be generated and installed - # ENSUREPIP_OPTIONS=install - # - pipX.Y, pipX, easy_install-X.Y will be generated and installed. Note - # that this option is technically if ENSUREPIP_OPTIONS is set and is - # not altinstall - # DEFAULT - # - The default behavior is to install pip, pipX, pipX.Y, easy_install - # and easy_install-X.Y. - pip_script = console.pop('pip', None) - if pip_script: - if "ENSUREPIP_OPTIONS" not in os.environ: - scripts_to_generate.append('pip = ' + pip_script) - - if os.environ.get("ENSUREPIP_OPTIONS", "") != "altinstall": - scripts_to_generate.append( - 'pip{} = {}'.format(sys.version_info[0], pip_script) - ) - - scripts_to_generate.append( - 'pip{} = {}'.format(get_major_minor_version(), pip_script) - ) - # Delete any other versioned pip entry points - pip_ep = [k for k in console if re.match(r'pip(\d(\.\d)?)?$', k)] - for k in pip_ep: - del console[k] - easy_install_script = console.pop('easy_install', None) - if easy_install_script: - if "ENSUREPIP_OPTIONS" not in os.environ: - scripts_to_generate.append( - 'easy_install = ' + easy_install_script - ) - - scripts_to_generate.append( - 'easy_install-{} = {}'.format( - get_major_minor_version(), easy_install_script - ) - ) - # Delete any other versioned easy_install entry points - easy_install_ep = [ - k for k in console if re.match(r'easy_install(-\d\.\d)?$', k) - ] - for k in easy_install_ep: - del console[k] - # Generate the console and GUI entry points specified in the wheel - scripts_to_generate.extend(starmap('{} = {}'.format, console.items())) + scripts_to_generate = get_console_script_specs(console) gui_scripts_to_generate = list(starmap('{} = {}'.format, gui.items())) From 3fad029b7740c735a8de4b9321706458d0fad72f Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 3 Jul 2020 09:43:12 -0400 Subject: [PATCH 2169/3170] Test `get_entrypoints` when none are expected --- tests/unit/test_wheel.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 8e1b3194219..9319c960df1 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -107,6 +107,12 @@ def test_get_entrypoints(tmpdir, console_scripts): ) +def test_get_entrypoints_no_entrypoints(tmpdir): + console, gui = wheel.get_entrypoints(str(tmpdir / 'entry_points.txt')) + assert console == {} + assert gui == {} + + def test_raise_for_invalid_entrypoint_ok(): _raise_for_invalid_entrypoint("hello = hello:main") From a953787152862c8a072f761b0b2d21eb4afd98e5 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 3 Jul 2020 09:43:11 -0400 Subject: [PATCH 2170/3170] Extract entrypoint test text construction into variable We need this to construct the new argument to `get_entrypoints`. --- tests/unit/test_wheel.py | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 9319c960df1..98266561949 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -91,15 +91,17 @@ def test_get_legacy_build_wheel_path__multiple_names(caplog): ], ) def test_get_entrypoints(tmpdir, console_scripts): + entry_points_text = u""" + [console_scripts] + {} + [section] + common:one = module:func + common:two = module:other_func + """.format(console_scripts) + entry_points = tmpdir.joinpath("entry_points.txt") with io.open(str(entry_points), "w", encoding="utf-8") as fp: - fp.write(u""" - [console_scripts] - {} - [section] - common:one = module:func - common:two = module:other_func - """.format(console_scripts)) + fp.write(entry_points_text) assert wheel.get_entrypoints(str(entry_points)) == ( dict([console_scripts.split(' = ')]), From d49d97f19f50d7520284dc606f75516c7a63d63e Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 3 Jul 2020 10:35:00 -0400 Subject: [PATCH 2171/3170] Pass Wheel distribution to install.wheel.get_entrypoints Right now we're just wiring up the arguments. Next we will actually use them. --- src/pip/_internal/operations/install/wheel.py | 18 ++++++++++---- tests/unit/test_wheel.py | 24 +++++++++++++++++-- 2 files changed, 36 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index c9edd0d857a..06fb069a041 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -32,7 +32,10 @@ from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.unpacking import current_umask, unpack_file -from pip._internal.utils.wheel import parse_wheel +from pip._internal.utils.wheel import ( + parse_wheel, + pkg_resources_distribution_for_wheel, +) # Use the custom cast function at runtime to make cast work, # and import typing.cast when performing pre-commit and type @@ -58,6 +61,8 @@ cast, ) + from pip._vendor.pkg_resources import Distribution + from pip._internal.models.scheme import Scheme from pip._internal.utils.filesystem import NamedTemporaryFileResult @@ -117,8 +122,8 @@ def wheel_root_is_purelib(metadata): return metadata.get("Root-Is-Purelib", "").lower() == "true" -def get_entrypoints(filename): - # type: (str) -> Tuple[Dict[str, str], Dict[str, str]] +def get_entrypoints(filename, distribution): + # type: (str, Distribution) -> Tuple[Dict[str, str], Dict[str, str]] if not os.path.exists(filename): return {}, {} @@ -321,6 +326,7 @@ def install_unpacked_wheel( name, # type: str wheeldir, # type: str wheel_zip, # type: ZipFile + wheel_path, # type: str scheme, # type: Scheme req_description, # type: str pycompile=True, # type: bool @@ -452,8 +458,11 @@ def clobber( dest_info_dir = os.path.join(lib_dir, info_dir) # Get the defined entry points + distribution = pkg_resources_distribution_for_wheel( + wheel_zip, name, wheel_path + ) ep_file = os.path.join(dest_info_dir, 'entry_points.txt') - console, gui = get_entrypoints(ep_file) + console, gui = get_entrypoints(ep_file, distribution) def is_entrypoint_wrapper(name): # type: (text_type) -> bool @@ -679,6 +688,7 @@ def install_wheel( name=name, wheeldir=unpacked_dir.path, wheel_zip=z, + wheel_path=wheel_path, scheme=scheme, req_description=req_description, pycompile=pycompile, diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 98266561949..1d3c442df4d 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -30,7 +30,9 @@ from pip._internal.utils.compat import WINDOWS from pip._internal.utils.misc import hash_file from pip._internal.utils.unpacking import unpack_file +from pip._internal.utils.wheel import pkg_resources_distribution_for_wheel from tests.lib import DATA_DIR, assert_paths_equal, skip_if_python2 +from tests.lib.wheel import make_wheel def call_get_legacy_build_wheel_path(caplog, names): @@ -99,18 +101,36 @@ def test_get_entrypoints(tmpdir, console_scripts): common:two = module:other_func """.format(console_scripts) + wheel_zip = make_wheel( + "simple", + "0.1.0", + extra_metadata_files={ + "entry_points.txt": entry_points_text, + }, + ).as_zipfile() + distribution = pkg_resources_distribution_for_wheel( + wheel_zip, "simple", "<in memory>" + ) + entry_points = tmpdir.joinpath("entry_points.txt") with io.open(str(entry_points), "w", encoding="utf-8") as fp: fp.write(entry_points_text) - assert wheel.get_entrypoints(str(entry_points)) == ( + assert wheel.get_entrypoints(str(entry_points), distribution) == ( dict([console_scripts.split(' = ')]), {}, ) def test_get_entrypoints_no_entrypoints(tmpdir): - console, gui = wheel.get_entrypoints(str(tmpdir / 'entry_points.txt')) + wheel_zip = make_wheel("simple", "0.1.0").as_zipfile() + distribution = pkg_resources_distribution_for_wheel( + wheel_zip, "simple", "<in memory>" + ) + + console, gui = wheel.get_entrypoints( + str(tmpdir / 'entry_points.txt'), distribution + ) assert console == {} assert gui == {} From 479154b4ae03f5091a926736c669456099ab25c7 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 3 Jul 2020 10:47:26 -0400 Subject: [PATCH 2172/3170] Get Wheel entrypoints from Distribution instead of file Since the Distribution pulls its data directly from the Wheel file, without extracting intermediate files to disk, this brings us closer to installing from Wheels without extracting everything. --- src/pip/_internal/operations/install/wheel.py | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 06fb069a041..44d252fce02 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -7,7 +7,6 @@ import compileall import contextlib import csv -import io import logging import os.path import re @@ -124,16 +123,14 @@ def wheel_root_is_purelib(metadata): def get_entrypoints(filename, distribution): # type: (str, Distribution) -> Tuple[Dict[str, str], Dict[str, str]] - if not os.path.exists(filename): - return {}, {} - - with io.open(filename, encoding="utf-8") as fp: - data = fp.read() - # 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', {}) + try: + console = distribution.get_entry_map('console_scripts') + gui = distribution.get_entry_map('gui_scripts') + except KeyError: + # Our dict-based Distribution raises KeyError if entry_points.txt + # doesn't exist. + return {}, {} def _split_ep(s): # type: (pkg_resources.EntryPoint) -> Tuple[str, str] From 3930e4b0630f941e17b3b10e57de6d886be903d5 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 3 Jul 2020 10:49:44 -0400 Subject: [PATCH 2173/3170] Drop unused argument --- src/pip/_internal/operations/install/wheel.py | 7 +++---- tests/unit/test_wheel.py | 15 ++++----------- 2 files changed, 7 insertions(+), 15 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 44d252fce02..a630dfacfc0 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -121,8 +121,8 @@ def wheel_root_is_purelib(metadata): return metadata.get("Root-Is-Purelib", "").lower() == "true" -def get_entrypoints(filename, distribution): - # type: (str, Distribution) -> Tuple[Dict[str, str], Dict[str, str]] +def get_entrypoints(distribution): + # type: (Distribution) -> Tuple[Dict[str, str], Dict[str, str]] # get the entry points and then the script names try: console = distribution.get_entry_map('console_scripts') @@ -458,8 +458,7 @@ def clobber( distribution = pkg_resources_distribution_for_wheel( wheel_zip, name, wheel_path ) - ep_file = os.path.join(dest_info_dir, 'entry_points.txt') - console, gui = get_entrypoints(ep_file, distribution) + console, gui = get_entrypoints(distribution) def is_entrypoint_wrapper(name): # type: (text_type) -> bool diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 1d3c442df4d..18460bf99dd 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -2,7 +2,6 @@ """Tests for wheel binary packages and .dist-info.""" import csv -import io import logging import os import textwrap @@ -92,7 +91,7 @@ def test_get_legacy_build_wheel_path__multiple_names(caplog): pytest.param(u"進入點 = 套件.模組:函式", marks=skip_if_python2), ], ) -def test_get_entrypoints(tmpdir, console_scripts): +def test_get_entrypoints(console_scripts): entry_points_text = u""" [console_scripts] {} @@ -112,25 +111,19 @@ def test_get_entrypoints(tmpdir, console_scripts): wheel_zip, "simple", "<in memory>" ) - entry_points = tmpdir.joinpath("entry_points.txt") - with io.open(str(entry_points), "w", encoding="utf-8") as fp: - fp.write(entry_points_text) - - assert wheel.get_entrypoints(str(entry_points), distribution) == ( + assert wheel.get_entrypoints(distribution) == ( dict([console_scripts.split(' = ')]), {}, ) -def test_get_entrypoints_no_entrypoints(tmpdir): +def test_get_entrypoints_no_entrypoints(): wheel_zip = make_wheel("simple", "0.1.0").as_zipfile() distribution = pkg_resources_distribution_for_wheel( wheel_zip, "simple", "<in memory>" ) - console, gui = wheel.get_entrypoints( - str(tmpdir / 'entry_points.txt'), distribution - ) + console, gui = wheel.get_entrypoints(distribution) assert console == {} assert gui == {} From 043320013542642f69ae89facf562ca3ed659e14 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 3 Jul 2020 10:50:31 -0400 Subject: [PATCH 2174/3170] Move dest_info_dir construction closer to first use Reducing the scope of variables makes it easier to refactor, since we can extract whole contiguous chunks of code later. --- src/pip/_internal/operations/install/wheel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index a630dfacfc0..0adeb3229b0 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -452,8 +452,6 @@ def clobber( True, ) - dest_info_dir = os.path.join(lib_dir, info_dir) - # Get the defined entry points distribution = pkg_resources_distribution_for_wheel( wheel_zip, name, wheel_path @@ -624,6 +622,8 @@ def _generate_file(path, **kwargs): os.chmod(f.name, generated_file_mode) replace(f.name, path) + dest_info_dir = os.path.join(lib_dir, info_dir) + # Record pip as the installer installer_path = os.path.join(dest_info_dir, 'INSTALLER') with _generate_file(installer_path) as installer_file: From d441f9518b4c70ee75384dc5811679ace70b2805 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 3 Jul 2020 20:49:16 -0400 Subject: [PATCH 2175/3170] Get wheel RECORD directly from wheel file This reduces our dependence on disk files, and removes some complexity around Python 2/3 compatibility with the csv module. --- src/pip/_internal/operations/install/wheel.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 0adeb3229b0..9fa0c28d399 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -644,10 +644,11 @@ def _generate_file(path, **kwargs): pass generated.append(requested_path) + record_text = distribution.get_metadata('RECORD') + record_rows = list(csv.reader(record_text.splitlines())) + # Record details of all files installed record_path = os.path.join(dest_info_dir, 'RECORD') - with open(record_path, **csv_io_kwargs('r')) as record_file: - record_rows = list(csv.reader(record_file)) rows = get_csv_rows_for_installed( record_rows, From d7b5a776b3df79cbe63673ec248a40f9871a68f4 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 3 Jul 2020 20:53:56 -0400 Subject: [PATCH 2176/3170] Move record_path closer to first use --- src/pip/_internal/operations/install/wheel.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 9fa0c28d399..99edfa08187 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -647,9 +647,6 @@ def _generate_file(path, **kwargs): record_text = distribution.get_metadata('RECORD') record_rows = list(csv.reader(record_text.splitlines())) - # Record details of all files installed - record_path = os.path.join(dest_info_dir, 'RECORD') - rows = get_csv_rows_for_installed( record_rows, installed=installed, @@ -657,6 +654,9 @@ def _generate_file(path, **kwargs): generated=generated, lib_dir=lib_dir) + # Record details of all files installed + record_path = os.path.join(dest_info_dir, 'RECORD') + with _generate_file(record_path, **csv_io_kwargs('w')) as record_file: # The type mypy infers for record_file is different for Python 3 # (typing.IO[Any]) and Python 2 (typing.BinaryIO). We explicitly From 512221e1c158b885980391a86db403a592742fb6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Sat, 4 Jul 2020 15:07:38 +0700 Subject: [PATCH 2177/3170] Make utils.parallel tests tear down properly --- ...b541f0-714b-4e9b-8f6e-2f5d6c85d98f.trivial | 0 tests/unit/test_utils_parallel.py | 23 +++++++++++++------ 2 files changed, 16 insertions(+), 7 deletions(-) create mode 100644 news/e1b541f0-714b-4e9b-8f6e-2f5d6c85d98f.trivial diff --git a/news/e1b541f0-714b-4e9b-8f6e-2f5d6c85d98f.trivial b/news/e1b541f0-714b-4e9b-8f6e-2f5d6c85d98f.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/unit/test_utils_parallel.py b/tests/unit/test_utils_parallel.py index bf42f6bd9e4..6086dcaa08b 100644 --- a/tests/unit/test_utils_parallel.py +++ b/tests/unit/test_utils_parallel.py @@ -1,5 +1,6 @@ """Test multiprocessing/multithreading higher-order functions.""" +from contextlib import contextmanager from importlib import import_module from math import factorial from sys import modules @@ -14,12 +15,20 @@ _import = __import__ -def reload_parallel(): +def unload_parallel(): try: del modules['pip._internal.utils.parallel'] except KeyError: pass - return import_module('pip._internal.utils.parallel') + + +@contextmanager +def tmp_import_parallel(): + unload_parallel() + try: + yield import_module('pip._internal.utils.parallel') + finally: + unload_parallel() def lack_sem_open(name, *args, **kwargs): @@ -46,21 +55,21 @@ def test_lack_sem_open(name, monkeypatch): map_async should fallback to map. """ monkeypatch.setattr(DUNDER_IMPORT, lack_sem_open) - parallel = reload_parallel() - assert getattr(parallel, name) is parallel._map_fallback + with tmp_import_parallel() as parallel: + assert getattr(parallel, name) is parallel._map_fallback @mark.parametrize('name', MAPS) def test_have_sem_open(name, monkeypatch): """Test fallback when sem_open is available.""" monkeypatch.setattr(DUNDER_IMPORT, have_sem_open) - parallel = reload_parallel() impl = '_map_fallback' if PY2 else '_{}'.format(name) - assert getattr(parallel, name) is getattr(parallel, impl) + with tmp_import_parallel() as parallel: + assert getattr(parallel, name) is getattr(parallel, impl) @mark.parametrize('name', MAPS) def test_map(name): """Test correctness of result of asynchronous maps.""" - map_async = getattr(reload_parallel(), name) + map_async = getattr(import_module('pip._internal.utils.parallel'), name) assert set(map_async(FUNC, ITERABLE)) == set(map(FUNC, ITERABLE)) From ace54858360a21cf3543a50dfbcfc52c89e143e1 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sat, 4 Jul 2020 17:48:05 +0530 Subject: [PATCH 2178/3170] Change reject_invalid_constraint_types to be reusable --- src/pip/_internal/resolution/resolvelib/resolver.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 35941d19e98..68f06aa9e10 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -32,8 +32,8 @@ logger = logging.getLogger(__name__) -def reject_invalid_constraint_types(req): - # type: (InstallRequirement) -> None +def check_invalid_constraint_type(req): + # type: (InstallRequirement) -> str # Check for unsupported forms problem = "" @@ -60,7 +60,8 @@ def reject_invalid_constraint_types(req): gone_in=None, issue=8210 ) - raise InstallationError(problem) + + return problem class Resolver(BaseResolver): @@ -108,7 +109,9 @@ def resolve(self, root_reqs, check_supported_wheels): for req in root_reqs: if req.constraint: # Ensure we only accept valid constraints - reject_invalid_constraint_types(req) + problem = check_invalid_constraint_type(req) + if problem: + raise InstallationError(problem) name = canonicalize_name(req.name) if name in constraints: From 20431888cbc39d8d719d5007e2097938876706e2 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sat, 4 Jul 2020 17:55:49 +0530 Subject: [PATCH 2179/3170] Move check_invalid_constraint_type to req_install.py --- src/pip/_internal/req/req_install.py | 32 +++++++++++++++++ .../resolution/resolvelib/resolver.py | 34 +------------------ 2 files changed, 33 insertions(+), 33 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 1d3ee5edc68..7538c9546ca 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -864,3 +864,35 @@ def install( raise self.install_succeeded = success + + +def check_invalid_constraint_type(req): + # type: (InstallRequirement) -> str + + # Check for unsupported forms + problem = "" + if not req.name: + problem = "Unnamed requirements are not allowed as constraints" + elif req.link: + problem = "Links are not allowed as constraints" + elif req.extras: + problem = "Constraints cannot have extras" + + if problem: + deprecated( + reason=( + "Constraints are only allowed to take the form of a package " + "name and a version specifier. Other forms were originally " + "permitted as an accident of the implementation, but were " + "undocumented. The new implementation of the resolver no " + "longer supports these forms." + ), + replacement=( + "replacing the constraint with a requirement." + ), + # No plan yet for when the new resolver becomes default + gone_in=None, + issue=8210 + ) + + return problem diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 68f06aa9e10..841057135a7 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -7,10 +7,10 @@ from pip._vendor.resolvelib import Resolver as RLResolver from pip._internal.exceptions import InstallationError +from pip._internal.req.req_install import check_invalid_constraint_type from pip._internal.req.req_set import RequirementSet from pip._internal.resolution.base import BaseResolver from pip._internal.resolution.resolvelib.provider import PipProvider -from pip._internal.utils.deprecation import deprecated from pip._internal.utils.typing import MYPY_CHECK_RUNNING from .factory import Factory @@ -32,38 +32,6 @@ logger = logging.getLogger(__name__) -def check_invalid_constraint_type(req): - # type: (InstallRequirement) -> str - - # Check for unsupported forms - problem = "" - if not req.name: - problem = "Unnamed requirements are not allowed as constraints" - elif req.link: - problem = "Links are not allowed as constraints" - elif req.extras: - problem = "Constraints cannot have extras" - - if problem: - deprecated( - reason=( - "Constraints are only allowed to take the form of a package " - "name and a version specifier. Other forms were originally " - "permitted as an accident of the implementation, but were " - "undocumented. The new implementation of the resolver no " - "longer supports these forms." - ), - replacement=( - "replacing the constraint with a requirement." - ), - # No plan yet for when the new resolver becomes default - gone_in=None, - issue=8210 - ) - - return problem - - class Resolver(BaseResolver): _allowed_strategies = {"eager", "only-if-needed", "to-satisfy-only"} From 6437bec2698a26939aa3d226f454983606be4537 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sat, 4 Jul 2020 17:56:45 +0530 Subject: [PATCH 2180/3170] Warn on to-be-removed forms of constraints --- src/pip/_internal/resolution/legacy/resolver.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pip/_internal/resolution/legacy/resolver.py b/src/pip/_internal/resolution/legacy/resolver.py index 7fdff1bc4f3..51a1d0b5dcb 100644 --- a/src/pip/_internal/resolution/legacy/resolver.py +++ b/src/pip/_internal/resolution/legacy/resolver.py @@ -28,6 +28,7 @@ HashErrors, UnsupportedPythonVersion, ) +from pip._internal.req.req_install import check_invalid_constraint_type from pip._internal.req.req_set import RequirementSet from pip._internal.resolution.base import BaseResolver from pip._internal.utils.compatibility_tags import get_supported @@ -167,6 +168,8 @@ def resolve(self, root_reqs, check_supported_wheels): check_supported_wheels=check_supported_wheels ) for req in root_reqs: + if req.constraint: + check_invalid_constraint_type(req) requirement_set.add_requirement(req) # Actually prepare the files, and collect any exceptions. Most hash From 00191b2db18ff5ef7104c756bcac35231912e1f3 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 3 Jul 2020 13:42:15 -0400 Subject: [PATCH 2181/3170] Explicitly test that header file was created as-expected Since this is the special part of this test. This gives us more confidence that we're doing the right thing when removing the standalone wheel file next. --- tests/functional/test_install_wheel.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/functional/test_install_wheel.py b/tests/functional/test_install_wheel.py index 6d94622458a..915ca87c55f 100644 --- a/tests/functional/test_install_wheel.py +++ b/tests/functional/test_install_wheel.py @@ -145,6 +145,16 @@ def test_basic_install_from_unicode_wheel(script, data): result.did_create(file2) +def get_header_scheme_path_for_script(script, dist_name): + command = ( + "from pip._internal.locations import get_scheme;" + "scheme = get_scheme({!r});" + "print(scheme.headers);" + ).format(dist_name) + result = script.run('python', '-c', command).stdout + return Path(result.strip()) + + def test_install_from_wheel_with_headers(script, data): """ Test installing from a wheel file with headers @@ -154,6 +164,12 @@ def test_install_from_wheel_with_headers(script, data): dist_info_folder = script.site_packages / 'headers.dist-0.1.dist-info' result.did_create(dist_info_folder) + header_scheme_path = get_header_scheme_path_for_script( + script, 'headers.dist' + ) + header_path = header_scheme_path / 'header.h' + assert header_path.read_text() == '' + def test_install_wheel_with_target(script, shared_data, with_wheel, tmpdir): """ From 0b0d53e8faa76895d63a46cbc380cdae694b93b4 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Fri, 3 Jul 2020 13:47:05 -0400 Subject: [PATCH 2182/3170] Use wheel helper function instead of pre-created wheel file --- .../headers.dist-0.1-py2.py3-none-any.whl | Bin 1214 -> 0 bytes tests/functional/test_install_wheel.py | 10 ++++++++-- 2 files changed, 8 insertions(+), 2 deletions(-) delete mode 100644 tests/data/packages/headers.dist-0.1-py2.py3-none-any.whl diff --git a/tests/data/packages/headers.dist-0.1-py2.py3-none-any.whl b/tests/data/packages/headers.dist-0.1-py2.py3-none-any.whl deleted file mode 100644 index 271352b0b5abd13e00074af1e91a4926abb7dfbd..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 1214 zcmWIWW@Zs#U|`^2VA`?W3CLhz0ueyK2gDhvi7BZ?#VMJ^C3*#w%nSi=)j8WyRjVOX z>w%Q(8t577r6iUl>O;+d(s~)lrl<q$bQj23)(X`91c)`TngTK|GcPS)KR2}`5oD%b zR&jn_?759xOoluS55qO*E}hetckYm7XL6_6EvL(JxvM&Uyo+=B@Zy(x%}MX$1qVX{ z^QYSN{r*@W_0{Vo%NJj{gNZATwVt}fDJf%c<9Cd+-=@um%{>b?o?lvAqqar(PSt)h zB@O?T)~Xep&yN|ekZaugYwq98Vw_HICLHB&j`W_X^ubg-N^hS%B0xZ1xltco5eD=^ zDiAB+_e!{jtE&(9c@N*S7kRyPwa%S6zd6X@it&RY|Fhn@UOFfBHwAfkoz&6m)MsD$ zkn7ZW{fl39uJLN<dYwJ#vnfbJQ_J@$m#5G6AdM9!m(HF(?|s$x%<7g-31ycZNRb3` zlX9<>YCX^`hk#fazngqrLmXWkLmXp!Kp`vO^80VsABRJkGv8S`_BO5fniaOWV5i3> z$3NA{59i)EyXW|sX)Lx}uY{i-%G{EbX;mO`a+SjiY14V#2cB4OZgB1u{Cs*<Ty4rT z@2P56C#%+^Z}6J<h57y4moKj6m6u%z=hXaP?kiY1<7L}=lWFsB);a84DwEC>++dCx zra&jdlcXYk_XoK;`v<vz(ibC>2s7?f4Kfl0wlsoB)HIH)7h4(!=>UN(je0;5s+T~D zM>Z2%iU%7AHnRk)nb^}mx~b@i4`Hf3BQ}@9O$8@Kbkoq24Z^g`_)UW)B6I^m2Ed9I jP&~rGmd43IVa$MpI}Z}80p6@^AT4Y_cm?Q!D`2w#QtNH+ diff --git a/tests/functional/test_install_wheel.py b/tests/functional/test_install_wheel.py index 915ca87c55f..891b6c517d6 100644 --- a/tests/functional/test_install_wheel.py +++ b/tests/functional/test_install_wheel.py @@ -155,11 +155,17 @@ def get_header_scheme_path_for_script(script, dist_name): return Path(result.strip()) -def test_install_from_wheel_with_headers(script, data): +def test_install_from_wheel_with_headers(script): """ Test installing from a wheel file with headers """ - package = data.packages.joinpath("headers.dist-0.1-py2.py3-none-any.whl") + package = make_wheel( + 'headers.dist', + '0.1', + extra_data_files={ + 'headers/header.h': '', + }, + ).save_to_dir(script.scratch_path) result = script.pip('install', package, '--no-index') dist_info_folder = script.site_packages / 'headers.dist-0.1.dist-info' result.did_create(dist_info_folder) From b5f02f9dd8c4a170d817aa7508670e4844ea76c8 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 4 Jul 2020 14:23:10 -0400 Subject: [PATCH 2183/3170] Check that expected text is written in include files This is a little more specific than checking that we happened to create an already-empty file, and is a better mirror for reality. --- tests/functional/test_install_wheel.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/functional/test_install_wheel.py b/tests/functional/test_install_wheel.py index 891b6c517d6..16b4844462b 100644 --- a/tests/functional/test_install_wheel.py +++ b/tests/functional/test_install_wheel.py @@ -159,11 +159,12 @@ def test_install_from_wheel_with_headers(script): """ Test installing from a wheel file with headers """ + header_text = '/* hello world */\n' package = make_wheel( 'headers.dist', '0.1', extra_data_files={ - 'headers/header.h': '', + 'headers/header.h': header_text }, ).save_to_dir(script.scratch_path) result = script.pip('install', package, '--no-index') @@ -174,7 +175,7 @@ def test_install_from_wheel_with_headers(script): script, 'headers.dist' ) header_path = header_scheme_path / 'header.h' - assert header_path.read_text() == '' + assert header_path.read_text() == header_text def test_install_wheel_with_target(script, shared_data, with_wheel, tmpdir): From 28592d4c31d3ddf0e2dfed23aea83a0dc002222e Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 5 Jul 2020 01:01:02 +0530 Subject: [PATCH 2184/3170] Allow for the deprecation warning in tests --- tests/functional/test_install_reqs.py | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index 577feb0eabf..ae90a041b54 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -344,6 +344,7 @@ def test_constraints_constrain_to_local_editable( result = script.pip( 'install', '--no-index', '-f', data.find_links, '-c', script.scratch_path / 'constraints.txt', 'singlemodule', + allow_stderr_warning=True, expect_error=use_new_resolver ) if use_new_resolver: @@ -360,6 +361,7 @@ def test_constraints_constrain_to_local(script, data, use_new_resolver): result = script.pip( 'install', '--no-index', '-f', data.find_links, '-c', script.scratch_path / 'constraints.txt', 'singlemodule', + allow_stderr_warning=True, expect_error=use_new_resolver ) if use_new_resolver: @@ -375,6 +377,7 @@ def test_constrained_to_url_install_same_url(script, data, use_new_resolver): result = script.pip( 'install', '--no-index', '-f', data.find_links, '-c', script.scratch_path / 'constraints.txt', to_install, + allow_stderr_warning=True, expect_error=use_new_resolver ) if use_new_resolver: @@ -425,6 +428,7 @@ def test_install_with_extras_from_constraints(script, data, use_new_resolver): ) result = script.pip_install_local( '-c', script.scratch_path / 'constraints.txt', 'LocalExtras', + allow_stderr_warning=True, expect_error=use_new_resolver ) if use_new_resolver: @@ -456,6 +460,7 @@ def test_install_with_extras_joined(script, data, use_new_resolver): ) result = script.pip_install_local( '-c', script.scratch_path / 'constraints.txt', 'LocalExtras[baz]', + allow_stderr_warning=True, expect_error=use_new_resolver ) if use_new_resolver: @@ -472,6 +477,7 @@ def test_install_with_extras_editable_joined(script, data, use_new_resolver): ) result = script.pip_install_local( '-c', script.scratch_path / 'constraints.txt', 'LocalExtras[baz]', + allow_stderr_warning=True, expect_error=use_new_resolver ) if use_new_resolver: @@ -510,6 +516,7 @@ def test_install_distribution_union_with_constraints( "{to_install}[bar]".format(**locals())) result = script.pip_install_local( '-c', script.scratch_path / 'constraints.txt', to_install + '[baz]', + allow_stderr_warning=True, expect_error=use_new_resolver ) if use_new_resolver: From 0a3a558e386c8a8ba674bfcf332fbfb9726875b0 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 4 Jul 2020 11:44:02 -0400 Subject: [PATCH 2185/3170] Use compileall.compile_file instead of compileall.compile_dir We want to move towards having more control over the generation of pyc files, which will allow us to provide deterministic installs and generate pyc files without relying on an already-extracted wheel. To that end, here we are stripping away one layer of abstraction, `compileall.compile_dir`. `compileall.compile_dir` essentially recurses through the provided directories and passes the files and args verbatim to `compileall.compile_file`, so removing that layer means that we directly call `compileall.compile_file`. We make the assumption that we can successfully walk over the source file tree, since we just wrote it, and omit the per-directory traversal error handling done by `compileall.compile_dir`. --- src/pip/_internal/operations/install/wheel.py | 22 ++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index eb3a07c684b..af21933b14a 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -451,12 +451,32 @@ def install_unpacked_wheel( changed = set() # type: Set[RecordPath] generated = [] # type: List[str] + def pyc_source_file_paths(): + # type: () -> Iterator[text_type] + decoded_source = ensure_text( + source, encoding=sys.getfilesystemencoding() + ) + for dir_path, subdir_paths, files in os.walk(decoded_source): + subdir_paths[:] = [ + p for p in subdir_paths if p != '__pycache__' + ] + for path in files: + yield os.path.join(dir_path, path) + # Compile all of the pyc files that we're going to be installing if pycompile: with captured_stdout() as stdout: with warnings.catch_warnings(): warnings.filterwarnings('ignore') - compileall.compile_dir(source, force=True, quiet=True) + for path in pyc_source_file_paths(): + # Python 2's `compileall.compile_file` requires a str in + # error cases, so we must convert to the native type. + path_arg = ensure_str( + path, encoding=sys.getfilesystemencoding() + ) + compileall.compile_file( + path_arg, force=True, quiet=True + ) logger.debug(stdout.getvalue()) def record_installed(srcfile, destfile, modified=False): From 4cb6f729ab0c86dcd66c08d5368ab686a43ad80f Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 4 Jul 2020 14:44:49 -0400 Subject: [PATCH 2186/3170] Filter to files actually processed by compileall.compile_file `compileall.compile_file` returns a success parameter, but can return "successful" without actually generating a pyc file if the input file was filtered out and compilation was not attempted. In our file processing we mirror that logic, to ensure that a truthy success returned by `compileall.compile_file` actually indicates a file was written. --- src/pip/_internal/operations/install/wheel.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index af21933b14a..63a98f937fe 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -461,6 +461,10 @@ def pyc_source_file_paths(): p for p in subdir_paths if p != '__pycache__' ] for path in files: + if not os.path.isfile(path): + continue + if not path.endswith('.py'): + continue yield os.path.join(dir_path, path) # Compile all of the pyc files that we're going to be installing From 2ece73cc862b8637b9d0da193f5a591fa6d1a2a2 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 4 Jul 2020 14:47:31 -0400 Subject: [PATCH 2187/3170] Confirm that pyc files are written during installation In order to add generated pyc files to the RECORD file for our package, we need to know their path! To raise confidence that we're doing this correctly, we assert the existence of the expected 'pyc' files while still using the old installation logic. Some valid reasons why pyc files may not be generated: 1. Syntax error in the installed Python files 2. There is already a pyc file in-place that isn't writable by the current user We don't fail installation in those cases today, and we wouldn't want to change our behavior here, so we only assert that the pyc file was created if `compileall.compile_file` indicates success. --- src/pip/_internal/operations/install/wheel.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 63a98f937fe..4a447570df3 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -7,6 +7,7 @@ import compileall import contextlib import csv +import importlib import logging import os.path import re @@ -467,6 +468,18 @@ def pyc_source_file_paths(): continue yield os.path.join(dir_path, path) + def pyc_output_path(path): + # type: (text_type) -> text_type + """Return the path the pyc file would have been written to. + """ + if PY2: + if sys.flags.optimize: + return path + 'o' + else: + return path + 'c' + else: + return importlib.util.cache_from_source(path) + # Compile all of the pyc files that we're going to be installing if pycompile: with captured_stdout() as stdout: @@ -478,9 +491,11 @@ def pyc_source_file_paths(): path_arg = ensure_str( path, encoding=sys.getfilesystemencoding() ) - compileall.compile_file( + success = compileall.compile_file( path_arg, force=True, quiet=True ) + if success: + assert os.path.exists(pyc_output_path(path)) logger.debug(stdout.getvalue()) def record_installed(srcfile, destfile, modified=False): From 42c01ae97e8c17c43c01ae9bc39bea3f56d49863 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 4 Jul 2020 14:47:32 -0400 Subject: [PATCH 2188/3170] Normalize Path to str in wheel tests In our next commit we will use the scheme path to locate files to byte-compile. If the scheme path is a `Path`, then that causes `compileall.compile_file` (via `py_compile.compile`) to fail with: ``` .tox/py38/lib/python3.8/site-packages/pip/_internal/operations/install/wheel.py:615: in install_unpacked_wheel success = compileall.compile_file( ../../../.pyenv/versions/3.8.0/lib/python3.8/compileall.py:157: in compile_file ok = py_compile.compile(fullname, cfile, dfile, True, ../../../.pyenv/versions/3.8.0/lib/python3.8/py_compile.py:162: in compile bytecode = importlib._bootstrap_external._code_to_timestamp_pyc( _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ code = <code object <module> at 0x7fa7e274f500, file "/tmp/user/1000/pytest-of-chris/pytest-37/test_std_install_with_direct_u0/dest/lib/sample/__init__.py", line 1>, mtime = 1593910285.2200587, source_size = 134 > ??? E ValueError: unmarshallable object ``` Debugging in gdb shows that the error is set due to the `Path` object being present in the code object, which `marshal.dumps` can't handle (frame 1): ``` 0 w_complex_object (v=<optimized out>, flag=<optimized out>, p=0x7fffffff7160) at Python/marshal.c:564 1 w_object (v=<Path at remote 0x7fffee51f120>, p=0x7fffffff7160) at Python/marshal.c:370 2 w_complex_object (v=<code at remote 0x7fffee591710>, flag=<optimized out>, p=0x7fffffff7160) at Python/marshal.c:544 3 w_object (v=<code at remote 0x7fffee591710>, p=0x7fffffff7160) at Python/marshal.c:370 4 w_complex_object (v=('1.2.0', <code at remote 0x7fffee591710>, 'main', None), flag=<optimized out>, p=0x7fffffff7160) at Python/marshal.c:475 5 w_object (v=('1.2.0', <code at remote 0x7fffee591710>, 'main', None), p=0x7fffffff7160) at Python/marshal.c:370 6 w_complex_object (v=<code at remote 0x7fffee591ea0>, flag=<optimized out>, p=0x7fffffff7160) at Python/marshal.c:539 7 w_object (p=0x7fffffff7160, v=<code at remote 0x7fffee591ea0>) at Python/marshal.c:370 8 PyMarshal_WriteObjectToString (version=<optimized out>, x=<code at remote 0x7fffee591ea0>) at Python/marshal.c:1598 9 marshal_dumps_impl (module=<optimized out>, version=<optimized out>, value=<code at remote 0x7fffee591ea0>) at Python/marshal.c:1739 10 marshal_dumps (module=<optimized out>, args=<optimized out>, nargs=<optimized out>) at Python/clinic/marshal.c.h:124 ``` In the interest of easy git bisects, we commit this fix before the code that would expose the bug. --- tests/unit/test_wheel.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 18460bf99dd..b9fb4e3cec3 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -258,9 +258,15 @@ class TestInstallUnpackedWheel(object): """ def prep(self, data, tmpdir): + # Since Path implements __add__, os.path.join returns a Path object. + # Passing Path objects to interfaces expecting str (like + # `compileall.compile_file`) can cause failures, so we normalize it + # to a string here. + tmpdir = str(tmpdir) self.name = 'sample' - self.wheelpath = data.packages.joinpath( - 'sample-1.2.0-py2.py3-none-any.whl') + self.wheelpath = os.path.join( + str(data.packages), 'sample-1.2.0-py2.py3-none-any.whl' + ) self.req = Requirement('sample') self.src = os.path.join(tmpdir, 'src') self.dest = os.path.join(tmpdir, 'dest') From 452e683edaa607721d9e900e5eff1c615a5d6eec Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 4 Jul 2020 14:55:45 -0400 Subject: [PATCH 2189/3170] Move byte-compilation after installing wheel files There are a few changes here: 1. The byte-compilation now occurs after we copy the root-scheme files and files from any wheel data dirs 1. Instead of iterating over the files in the unpacked wheel directory, we iterate over the installed files as they exist in the installation path 2. In addition to asserting that pyc files were created, we also add them to the list of installed files, so they will be included in RECORD By compiling after installation, we no longer depend on a separate temporary directory - this brings us closer to installing directly from wheel files. By compiling with source files as they exist in the installation output directory, we no longer generate pyc files with an embedded randomized temp directory - this means that wheel installs can be deterministic. --- src/pip/_internal/operations/install/wheel.py | 92 +++++++++---------- 1 file changed, 46 insertions(+), 46 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 4a447570df3..87c94918c24 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -452,52 +452,6 @@ def install_unpacked_wheel( changed = set() # type: Set[RecordPath] generated = [] # type: List[str] - def pyc_source_file_paths(): - # type: () -> Iterator[text_type] - decoded_source = ensure_text( - source, encoding=sys.getfilesystemencoding() - ) - for dir_path, subdir_paths, files in os.walk(decoded_source): - subdir_paths[:] = [ - p for p in subdir_paths if p != '__pycache__' - ] - for path in files: - if not os.path.isfile(path): - continue - if not path.endswith('.py'): - continue - yield os.path.join(dir_path, path) - - def pyc_output_path(path): - # type: (text_type) -> text_type - """Return the path the pyc file would have been written to. - """ - if PY2: - if sys.flags.optimize: - return path + 'o' - else: - return path + 'c' - else: - return importlib.util.cache_from_source(path) - - # Compile all of the pyc files that we're going to be installing - if pycompile: - with captured_stdout() as stdout: - with warnings.catch_warnings(): - warnings.filterwarnings('ignore') - for path in pyc_source_file_paths(): - # Python 2's `compileall.compile_file` requires a str in - # error cases, so we must convert to the native type. - path_arg = ensure_str( - path, encoding=sys.getfilesystemencoding() - ) - success = compileall.compile_file( - path_arg, force=True, quiet=True - ) - if success: - assert os.path.exists(pyc_output_path(path)) - logger.debug(stdout.getvalue()) - def record_installed(srcfile, destfile, modified=False): # type: (text_type, text_type, bool) -> None """Map archive RECORD paths to installation RECORD paths.""" @@ -622,6 +576,52 @@ def is_entrypoint_wrapper(name): filter=filter, ) + def pyc_source_file_paths(): + # type: () -> Iterator[text_type] + # We de-duplicate installation paths, since there can be overlap (e.g. + # file in .data maps to same location as file in wheel root). + # Sorting installation paths makes it easier to reproduce and debug + # issues related to permissions on existing files. + for installed_path in sorted(set(installed.values())): + full_installed_path = os.path.join(lib_dir, installed_path) + if not os.path.isfile(full_installed_path): + continue + if not full_installed_path.endswith('.py'): + continue + yield full_installed_path + + def pyc_output_path(path): + # type: (text_type) -> text_type + """Return the path the pyc file would have been written to. + """ + if PY2: + if sys.flags.optimize: + return path + 'o' + else: + return path + 'c' + else: + return importlib.util.cache_from_source(path) + + # Compile all of the pyc files for the installed files + if pycompile: + with captured_stdout() as stdout: + with warnings.catch_warnings(): + warnings.filterwarnings('ignore') + for path in pyc_source_file_paths(): + # Python 2's `compileall.compile_file` requires a str in + # error cases, so we must convert to the native type. + path_arg = ensure_str( + path, encoding=sys.getfilesystemencoding() + ) + success = compileall.compile_file( + path_arg, force=True, quiet=True + ) + if success: + pyc_path = pyc_output_path(path) + assert os.path.exists(pyc_path) + record_installed(pyc_path, pyc_path) + logger.debug(stdout.getvalue()) + maker = PipScriptMaker(None, scheme.scripts) # Ensure old scripts are overwritten. From caad983734de960b71825edea29f6cf54599b21c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sun, 5 Jul 2020 16:45:52 +0200 Subject: [PATCH 2190/3170] Fix docstring typo --- src/pip/_internal/req/req_file.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index 6cce44008ab..1109f5c4c42 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -135,7 +135,7 @@ def parse_requirements( constraint=False, # type: bool ): # type: (...) -> Iterator[ParsedRequirement] - """Parse a requirements file and yield InstallRequirement instances. + """Parse a requirements file and yield ParsedRequirement instances. :param filename: Path or url of requirements file. :param session: PipSession instance. From df79ae4233e38d93aa3aebd64e60c26574d115ad Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sun, 5 Jul 2020 16:47:03 +0200 Subject: [PATCH 2191/3170] Enable strict typing in req_file.py --- src/pip/_internal/req/req_file.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index 1109f5c4c42..3443050f69d 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -2,9 +2,6 @@ Requirements file parsing """ -# The following comment should be removed at some point in the future. -# mypy: strict-optional=False - from __future__ import absolute_import import optparse @@ -101,7 +98,7 @@ def __init__( self, filename, # type: str lineno, # type: int - comes_from, # type: str + comes_from, # type: Optional[str] args, # type: str opts, # type: Values constraint, # type: bool @@ -227,7 +224,7 @@ def handle_option_line( # type: (...) -> None # percolate hash-checking option upward - if opts.require_hashes: + if options and opts.require_hashes: options.require_hashes = opts.require_hashes # set finder options @@ -319,7 +316,7 @@ def __init__( self, session, # type: PipSession line_parser, # type: LineParser - comes_from, # type: str + comes_from, # type: Optional[str] ): # type: (...) -> None self._session = session @@ -481,6 +478,7 @@ def join_lines(lines_enum): line = ' ' + line if new_line: new_line.append(line) + assert primary_line_number is not None yield primary_line_number, ''.join(new_line) new_line = [] else: @@ -492,6 +490,7 @@ def join_lines(lines_enum): # last line contains \ if new_line: + assert primary_line_number is not None yield primary_line_number, ''.join(new_line) # TODO: handle space after '\'. From a9c7f229b0d4251c634e7f256e79cbc7166603eb Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 5 Jul 2020 20:10:55 -0400 Subject: [PATCH 2192/3170] Create sample project wheel inline --- .../sample-1.2.0-py2.py3-none-any.whl | Bin 3581 -> 0 bytes tests/data/src/sample/MANIFEST.in | 8 -- tests/data/src/sample/data/data_file | 1 - tests/data/src/sample/sample/__init__.py | 5 - tests/data/src/sample/sample/package_data.dat | 1 - tests/data/src/sample/setup.cfg | 5 - tests/data/src/sample/setup.py | 103 ------------------ tests/unit/test_wheel.py | 47 +++++++- 8 files changed, 44 insertions(+), 126 deletions(-) delete mode 100644 tests/data/packages/sample-1.2.0-py2.py3-none-any.whl delete mode 100644 tests/data/src/sample/MANIFEST.in delete mode 100644 tests/data/src/sample/data/data_file delete mode 100644 tests/data/src/sample/sample/__init__.py delete mode 100644 tests/data/src/sample/sample/package_data.dat delete mode 100644 tests/data/src/sample/setup.cfg delete mode 100644 tests/data/src/sample/setup.py diff --git a/tests/data/packages/sample-1.2.0-py2.py3-none-any.whl b/tests/data/packages/sample-1.2.0-py2.py3-none-any.whl deleted file mode 100644 index 5a64d6b411d566b486a0a9586b6ef26183d02c4e..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 3581 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 P0N~#K(f4_rdwcYMG8ld< diff --git a/tests/data/src/sample/MANIFEST.in b/tests/data/src/sample/MANIFEST.in deleted file mode 100644 index df5350843d5..00000000000 --- a/tests/data/src/sample/MANIFEST.in +++ /dev/null @@ -1,8 +0,0 @@ -include DESCRIPTION.rst - -# Include the test suite (FIXME: does not work yet) -# recursive-include tests * - -# If using Python 2.6 or less, then have to include package data, even though -# it's already declared in setup.py -include sample/*.dat diff --git a/tests/data/src/sample/data/data_file b/tests/data/src/sample/data/data_file deleted file mode 100644 index 426863280ee..00000000000 --- a/tests/data/src/sample/data/data_file +++ /dev/null @@ -1 +0,0 @@ -some data diff --git a/tests/data/src/sample/sample/__init__.py b/tests/data/src/sample/sample/__init__.py deleted file mode 100644 index c1699a74701..00000000000 --- a/tests/data/src/sample/sample/__init__.py +++ /dev/null @@ -1,5 +0,0 @@ -__version__ = '1.2.0' - -def main(): - """Entry point for the application script""" - print("Call your main application code here") diff --git a/tests/data/src/sample/sample/package_data.dat b/tests/data/src/sample/sample/package_data.dat deleted file mode 100644 index 426863280ee..00000000000 --- a/tests/data/src/sample/sample/package_data.dat +++ /dev/null @@ -1 +0,0 @@ -some data diff --git a/tests/data/src/sample/setup.cfg b/tests/data/src/sample/setup.cfg deleted file mode 100644 index 79bc67848ff..00000000000 --- a/tests/data/src/sample/setup.cfg +++ /dev/null @@ -1,5 +0,0 @@ -[bdist_wheel] -# This flag says that the code is written to work on both Python 2 and Python -# 3. If at all possible, it is good practice to do this. If you cannot, you -# will need to generate wheels for each Python version that you support. -universal=1 diff --git a/tests/data/src/sample/setup.py b/tests/data/src/sample/setup.py deleted file mode 100644 index 875860cb7ae..00000000000 --- a/tests/data/src/sample/setup.py +++ /dev/null @@ -1,103 +0,0 @@ -import codecs -import os -import re - -from setuptools import find_packages, setup - -here = os.path.abspath(os.path.dirname(__file__)) - -# Read the version number from a source file. -# Why read it, and not import? -# see https://groups.google.com/d/topic/pypa-dev/0PkjVpcxTzQ/discussion -def find_version(*file_paths): - # Open in Latin-1 so that we avoid encoding errors. - # Use codecs.open for Python 2 compatibility - with codecs.open(os.path.join(here, *file_paths), 'r', 'latin1') as f: - version_file = f.read() - - # The version line must have the form - # __version__ = 'ver' - version_match = re.search(r"^__version__ = ['\"]([^'\"]*)['\"]", - version_file, re.M) - if version_match: - return version_match.group(1) - raise RuntimeError("Unable to find version string.") - - -# Get the long description from the relevant file -with codecs.open('DESCRIPTION.rst', encoding='utf-8') as f: - long_description = f.read() - -setup( - name="sample", - version=find_version('sample', '__init__.py'), - description="A sample Python project", - long_description=long_description, - - # The project URL. - url='https://github.com/pypa/sampleproject', - - # Author details - author='The Python Packaging Authority', - author_email='distutils-sig@python.org', - - # Choose your license - license='MIT', - - classifiers=[ - # How mature is this project? Common values are - # 3 - Alpha - # 4 - Beta - # 5 - Production/Stable - 'Development Status :: 3 - Alpha', - - # Indicate who your project is intended for - 'Intended Audience :: Developers', - 'Topic :: Software Development :: Build Tools', - - # Pick your license as you wish (should match "license" above) - 'License :: OSI Approved :: MIT License', - - # Specify the Python versions you support here. In particular, ensure - # that you indicate whether you support Python 2, Python 3 or both. - 'Programming Language :: Python :: 2', - 'Programming Language :: Python :: 2.7', - 'Programming Language :: Python :: 3', - 'Programming Language :: Python :: 3.1', - 'Programming Language :: Python :: 3.2', - 'Programming Language :: Python :: 3.3', - ], - - # What does your project relate to? - keywords='sample setuptools development', - - # You can just specify the packages manually here if your project is - # simple. Or you can use find_packages. - packages=find_packages(exclude=["contrib", "docs", "tests*"]), - - # List run-time dependencies here. These will be installed by pip when your - # project is installed. - install_requires=['peppercorn'], - - # If there are data files included in your packages that need to be - # installed, specify them here. If using Python 2.6 or less, then these - # have to be included in MANIFEST.in as well. - package_data={ - 'sample': ['package_data.dat'], - }, - - # Although 'package_data' is the preferred approach, in some case you may - # need to place data files outside of your packages. - # see https://docs.python.org/3.4/distutils/setupscript.html#installing-additional-files - # In this case, 'data_file' will be installed into '<sys.prefix>/my_data' - data_files=[('my_data', ['data/data_file'])], - - # To provide executable scripts, use entry points in preference to the - # "scripts" keyword. Entry points provide cross-platform support and allow - # pip to create the appropriate form of executable for the target platform. - entry_points={ - 'console_scripts': [ - 'sample=sample:main', - ], - }, -) diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index b9fb4e3cec3..5e0e7d03b48 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -264,9 +264,50 @@ def prep(self, data, tmpdir): # to a string here. tmpdir = str(tmpdir) self.name = 'sample' - self.wheelpath = os.path.join( - str(data.packages), 'sample-1.2.0-py2.py3-none-any.whl' - ) + self.wheelpath = make_wheel( + "sample", + "1.2.0", + metadata_body=textwrap.dedent( + """ + A sample Python project + ======================= + + ... + """ + ), + metadata_updates={ + "Requires-Dist": ["peppercorn"], + }, + extra_files={ + "sample/__init__.py": textwrap.dedent( + ''' + __version__ = '1.2.0' + + def main(): + """Entry point for the application script""" + print("Call your main application code here") + ''' + ), + "sample/package_data.dat": "some data", + }, + extra_metadata_files={ + "DESCRIPTION.rst": textwrap.dedent( + """ + A sample Python project + ======================= + + ... + """ + ), + "top_level.txt": "sample\n" + }, + extra_data_files={ + "data/my_data/data_file": "some data", + }, + console_scripts=[ + "sample = sample:main", + ], + ).save_to_dir(tmpdir) self.req = Requirement('sample') self.src = os.path.join(tmpdir, 'src') self.dest = os.path.join(tmpdir, 'dest') From ba96ba3b082d3788830a455527c6134645dec0cd Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 5 Jul 2020 20:14:15 -0400 Subject: [PATCH 2193/3170] Reduce empty directory test coupling to implementation Our previous test required that the implementation use a temporary directory and unpack the wheel in-place. Now we just provide a conventional empty directory in the wheel file itself. --- tests/unit/test_wheel.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 5e0e7d03b48..e167cb6e1e0 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -299,7 +299,8 @@ def main(): ... """ ), - "top_level.txt": "sample\n" + "top_level.txt": "sample\n", + "empty_dir/empty_dir/": "", }, extra_data_files={ "data/my_data/data_file": "some data", @@ -447,16 +448,11 @@ def test_dist_info_contains_empty_dir(self, data, tmpdir): """ # e.g. https://github.com/pypa/pip/issues/1632#issuecomment-38027275 self.prep(data, tmpdir) - src_empty_dir = os.path.join( - self.src_dist_info, 'empty_dir', 'empty_dir') - os.makedirs(src_empty_dir) - assert os.path.isdir(src_empty_dir) wheel.install_wheel( self.name, self.wheelpath, scheme=self.scheme, req_description=str(self.req), - _temp_dir_for_testing=self.src, ) self.assert_installed(0o644) assert not os.path.isdir( From 5e4c1a96a231240dd4f0d79def451ae82361289d Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 5 Jul 2020 20:16:07 -0400 Subject: [PATCH 2194/3170] Remove unused argument --- src/pip/_internal/operations/install/wheel.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 87c94918c24..ba9748e366c 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -726,13 +726,12 @@ def install_wheel( req_description, # type: str pycompile=True, # type: bool warn_script_location=True, # type: bool - _temp_dir_for_testing=None, # type: Optional[str] direct_url=None, # type: Optional[DirectUrl] requested=False, # type: bool ): # type: (...) -> None with TempDirectory( - path=_temp_dir_for_testing, kind="unpacked-wheel" + kind="unpacked-wheel" ) as unpacked_dir, ZipFile(wheel_path, allowZip64=True) as z: unpack_file(wheel_path, unpacked_dir.path) install_unpacked_wheel( From 01e0700e687aacbf001510094df6998050f932bf Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 23 Jun 2020 21:39:12 +0800 Subject: [PATCH 2195/3170] Skip installing if the version matches locally MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This check only applies to explicit requirements since we avoid downloading the dist from finder altogether when there is a matching installation (although the check wouldn’t change the behaviour in that case anyway). We can do this when we build the `ExplicitRequirement` instead, like how we did for `SpecifierRequirement`, but that would require us to resolve the direct requirement’s version eagerly, which I don’t want to. The implemented approach checks the version only after resolution, at which point the distribution is already built anyway and the operation is cheap. --- .../_internal/resolution/resolvelib/base.py | 5 ++++ .../resolution/resolvelib/candidates.py | 18 ++++++++++++-- .../resolution/resolvelib/factory.py | 18 ++++++++------ .../resolution/resolvelib/resolver.py | 24 ++++++++++++++++++- tests/functional/test_install_upgrade.py | 1 - 5 files changed, 55 insertions(+), 11 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/base.py b/src/pip/_internal/resolution/resolvelib/base.py index b987256188b..a155a1101ad 100644 --- a/src/pip/_internal/resolution/resolvelib/base.py +++ b/src/pip/_internal/resolution/resolvelib/base.py @@ -59,6 +59,11 @@ def is_installed(self): # type: () -> bool raise NotImplementedError("Override in subclass") + @property + def is_editable(self): + # type: () -> bool + raise NotImplementedError("Override in subclass") + @property def source_link(self): # type: () -> Optional[Link] diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index a16cc6b5f56..1ee46430292 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -11,7 +11,7 @@ install_req_from_line, ) from pip._internal.req.req_install import InstallRequirement -from pip._internal.utils.misc import normalize_version_info +from pip._internal.utils.misc import dist_is_editable, normalize_version_info from pip._internal.utils.packaging import get_requires_python from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -261,6 +261,8 @@ def get_install_requirement(self): class LinkCandidate(_InstallRequirementBackedCandidate): + is_editable = False + def __init__( self, link, # type: Link @@ -299,6 +301,8 @@ def _prepare_abstract_distribution(self): class EditableCandidate(_InstallRequirementBackedCandidate): + is_editable = True + def __init__( self, link, # type: Link @@ -376,6 +380,11 @@ def version(self): # type: () -> _BaseVersion return self.dist.parsed_version + @property + def is_editable(self): + # type: () -> bool + return dist_is_editable(self.dist) + def format_for_error(self): # type: () -> str return "{} {} (Installed)".format(self.name, self.version) @@ -466,9 +475,14 @@ def format_for_error(self): @property def is_installed(self): - # type: () -> _BaseVersion + # type: () -> bool return self.base.is_installed + @property + def is_editable(self): + # type: () -> bool + return self.base.is_editable + @property def source_link(self): # type: () -> Optional[Link] diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 09926de2c0b..81f887c6b1e 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -84,7 +84,6 @@ def __init__( py_version_info=None, # type: Optional[Tuple[int, ...]] ): # type: (...) -> None - self._finder = finder self.preparer = preparer self._wheel_cache = wheel_cache @@ -105,6 +104,11 @@ def __init__( else: self._installed_dists = {} + @property + def force_reinstall(self): + # type: () -> bool + return self._force_reinstall + def _make_candidate_from_dist( self, dist, # type: Distribution @@ -305,22 +309,22 @@ def get_wheel_cache_entry(self, link, name): supported_tags=get_supported(), ) - def should_reinstall(self, candidate): - # type: (Candidate) -> bool + def get_dist_to_uninstall(self, candidate): + # type: (Candidate) -> Optional[Distribution] # TODO: Are there more cases this needs to return True? Editable? dist = self._installed_dists.get(candidate.name) if dist is None: # Not installed, no uninstallation required. - return False + return None # We're installing into global site. The current installation must # be uninstalled, no matter it's in global or user site, because the # user site installation has precedence over global. if not self._use_user_site: - return True + return dist # We're installing into user site. Remove the user site installation. if dist_in_usersite(dist): - return True + return dist # We're installing into user site, but the installed incompatible # package is in global site. We can't uninstall that, and would let @@ -333,7 +337,7 @@ def should_reinstall(self, candidate): dist.project_name, dist.location, ) ) - return False + return None def _report_requires_python_error( self, diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 841057135a7..d2ac9d0418a 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -11,6 +11,7 @@ from pip._internal.req.req_set import RequirementSet from pip._internal.resolution.base import BaseResolver from pip._internal.resolution.resolvelib.provider import PipProvider +from pip._internal.utils.misc import dist_is_editable from pip._internal.utils.typing import MYPY_CHECK_RUNNING from .factory import Factory @@ -120,6 +121,27 @@ def resolve(self, root_reqs, check_supported_wheels): ireq = candidate.get_install_requirement() if ireq is None: continue + + # Check if there is already an installation under the same name, + # and set a flag for later stages to uninstall it, if needed. + # * There isn't, good -- no uninstalltion needed. + # * The --force-reinstall flag is set. Always reinstall. + # * The installation is different in version or editable-ness, so + # we need to uninstall it to install the new distribution. + # * The installed version is the same as the pending distribution. + # Skip this distrubiton altogether to save work. + installed_dist = self.factory.get_dist_to_uninstall(candidate) + if installed_dist is None: + ireq.should_reinstall = False + elif self.factory.force_reinstall: + ireq.should_reinstall = True + elif installed_dist.parsed_version != candidate.version: + ireq.should_reinstall = True + elif dist_is_editable(installed_dist) != candidate.is_editable: + ireq.should_reinstall = True + else: + continue + link = candidate.source_link if link and link.is_yanked: # The reason can contain non-ASCII characters, Unicode @@ -135,7 +157,7 @@ def resolve(self, root_reqs, check_supported_wheels): reason=link.yanked_reason or u'<none given>', ) logger.warning(msg) - ireq.should_reinstall = self.factory.should_reinstall(candidate) + req_set.add_named_requirement(ireq) return req_set diff --git a/tests/functional/test_install_upgrade.py b/tests/functional/test_install_upgrade.py index f4a41bb7c7f..b4bad29964c 100644 --- a/tests/functional/test_install_upgrade.py +++ b/tests/functional/test_install_upgrade.py @@ -249,7 +249,6 @@ def test_uninstall_before_upgrade_from_url(script): @pytest.mark.network -@pytest.mark.fails_on_new_resolver def test_upgrade_to_same_version_from_url(script): """ When installing from a URL the same version that is already installed, no From 782913725fde4804004d65013f9b7fca7282767e Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Wed, 15 Apr 2020 19:06:31 +0530 Subject: [PATCH 2196/3170] Canonicalize req name while doing pre-install package search --- news/5021.bugfix | 1 + src/pip/_internal/req/req_install.py | 8 ++++++-- 2 files changed, 7 insertions(+), 2 deletions(-) create mode 100644 news/5021.bugfix diff --git a/news/5021.bugfix b/news/5021.bugfix new file mode 100644 index 00000000000..4de60b18e76 --- /dev/null +++ b/news/5021.bugfix @@ -0,0 +1 @@ +Package name should be normalized before we use it to search if it's already installed or not diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 7538c9546ca..f58a24d74c1 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -428,12 +428,16 @@ def check_if_exists(self, use_user_site): """ if self.req is None: return + + # Canonicalize requirement name to use normalized + # names while searching for already installed packages + no_marker = Requirement(str(self.req)) + no_marker.marker = None + no_marker.name = canonicalize_name(no_marker.name) # get_distribution() will resolve the entire list of requirements # anyway, and we've already determined that we need the requirement # in question, so strip the marker so that we don't try to # evaluate it. - no_marker = Requirement(str(self.req)) - no_marker.marker = None try: self.satisfied_by = pkg_resources.get_distribution(str(no_marker)) except pkg_resources.DistributionNotFound: From 40261a475ff8bdfba6f1557f1ea4d9ab7cf75b89 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Wed, 15 Apr 2020 23:00:37 +0530 Subject: [PATCH 2197/3170] Add unit tests to verify pkg name normalization --- tests/functional/test_install.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index cb291a61ef6..cf63d0dc3d4 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1882,3 +1882,20 @@ def test_install_skip_work_dir_pkg(script, data): assert 'Requirement already satisfied: simple' not in result.stdout assert 'Successfully installed simple' in result.stdout + + +def test_install_verify_package_name_normalization(script): + """ + Test that install of a package again using a name which + normalizes to the original package name, is a no-op + since the package is already installed + """ + pkg_path = create_test_package_with_setup( + script, name='simple-package', version='1.0') + result = script.pip('install', '-e', '.', + expect_stderr=True, cwd=pkg_path) + assert 'Successfully installed simple-package' in result.stdout + + result = script.pip('install', 'simple.package') + assert 'Requirement already satisfied: simple.package' in result.stdout + From 2bdec6c9fd6cf18ceddbfc5b3911455c3e4c34b7 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Thu, 16 Apr 2020 00:01:53 +0530 Subject: [PATCH 2198/3170] Parametrize unit test --- tests/functional/test_install.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index cf63d0dc3d4..2cff6ca2229 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1884,7 +1884,10 @@ def test_install_skip_work_dir_pkg(script, data): assert 'Successfully installed simple' in result.stdout -def test_install_verify_package_name_normalization(script): +@pytest.mark.parametrize('package_name', ('simple-package', 'simple_package', + 'simple.package')) +def test_install_verify_package_name_normalization(script, package_name): + """ Test that install of a package again using a name which normalizes to the original package name, is a no-op @@ -1896,6 +1899,6 @@ def test_install_verify_package_name_normalization(script): expect_stderr=True, cwd=pkg_path) assert 'Successfully installed simple-package' in result.stdout - result = script.pip('install', 'simple.package') - assert 'Requirement already satisfied: simple.package' in result.stdout - + result = script.pip('install', package_name) + assert 'Requirement already satisfied: {}'.format( + package_name) in result.stdout From 04fedfe53ccf3a9dde0f7646d204b4acda23fc25 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Fri, 15 May 2020 00:14:15 +0530 Subject: [PATCH 2199/3170] Create custom get_distribution function --- src/pip/_internal/commands/search.py | 6 ++-- src/pip/_internal/req/req_install.py | 23 ++++++++-------- src/pip/_internal/self_outdated_check.py | 14 ++++++---- src/pip/_internal/utils/misc.py | 35 ++++++++++++++++++++++++ tests/functional/test_search.py | 3 +- tests/unit/test_self_check_outdated.py | 3 +- 6 files changed, 62 insertions(+), 22 deletions(-) diff --git a/src/pip/_internal/commands/search.py b/src/pip/_internal/commands/search.py index 3e75254812a..c01d96a2473 100644 --- a/src/pip/_internal/commands/search.py +++ b/src/pip/_internal/commands/search.py @@ -19,7 +19,8 @@ from pip._internal.network.xmlrpc import PipXmlrpcTransport from pip._internal.utils.compat import get_terminal_size from pip._internal.utils.logging import indent_log -from pip._internal.utils.misc import write_output + +from pip._internal.utils.misc import get_distribution, write_output from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: @@ -31,6 +32,7 @@ {'name': str, 'summary': str, 'versions': List[str]}, ) + logger = logging.getLogger(__name__) @@ -139,7 +141,7 @@ def print_results(hits, name_column_width=None, terminal_width=None): try: write_output(line) if name in installed_packages: - dist = pkg_resources.get_distribution(name) + dist = get_distribution(name) with indent_log(): if dist.version == latest: write_output('INSTALLED: %s (latest)', dist.version) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index f58a24d74c1..2aa853642c5 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -41,6 +41,7 @@ display_path, dist_in_site_packages, dist_in_usersite, + get_distribution, get_installed_version, hide_url, redact_auth_from_url, @@ -429,21 +430,23 @@ def check_if_exists(self, use_user_site): if self.req is None: return - # Canonicalize requirement name to use normalized - # names while searching for already installed packages - no_marker = Requirement(str(self.req)) - no_marker.marker = None - no_marker.name = canonicalize_name(no_marker.name) # get_distribution() will resolve the entire list of requirements # anyway, and we've already determined that we need the requirement # in question, so strip the marker so that we don't try to # evaluate it. + no_marker = Requirement(str(self.req)) + no_marker.marker = None + + # pkg_resources uses the canonical name to look up packages, but + # the name passed passed to get_distribution is not canonicalized + # so we have to explicitly convert it to a canonical name + no_marker.name = canonicalize_name(no_marker.name) try: self.satisfied_by = pkg_resources.get_distribution(str(no_marker)) except pkg_resources.DistributionNotFound: return except pkg_resources.VersionConflict: - existing_dist = pkg_resources.get_distribution( + existing_dist = get_distribution( self.req.name ) if use_user_site: @@ -683,13 +686,11 @@ def uninstall(self, auto_confirm=False, verbose=False): """ assert self.req - try: - dist = pkg_resources.get_distribution(self.req.name) - except pkg_resources.DistributionNotFound: + dist = get_distribution(self.req.name) + if not dist: logger.warning("Skipping %s as it is not installed.", self.name) return None - else: - logger.info('Found existing installation: %s', dist) + logger.info('Found existing installation: %s', dist) uninstalled_pathset = UninstallPathSet.from_dist(dist) uninstalled_pathset.remove(auto_confirm, verbose) diff --git a/src/pip/_internal/self_outdated_check.py b/src/pip/_internal/self_outdated_check.py index ec5df3af105..fbd9dfd48b7 100644 --- a/src/pip/_internal/self_outdated_check.py +++ b/src/pip/_internal/self_outdated_check.py @@ -7,7 +7,6 @@ import os.path import sys -from pip._vendor import pkg_resources from pip._vendor.packaging import version as packaging_version from pip._vendor.six import ensure_binary @@ -19,7 +18,11 @@ check_path_owner, replace, ) -from pip._internal.utils.misc import ensure_dir, get_installed_version +from pip._internal.utils.misc import ( + ensure_dir, + get_distribution, + get_installed_version, +) from pip._internal.utils.packaging import get_installer from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -110,11 +113,10 @@ def was_installed_by_pip(pkg): This is used not to display the upgrade message when pip is in fact installed by system package manager, such as dnf on Fedora. """ - try: - dist = pkg_resources.get_distribution(pkg) - return "pip" == get_installer(dist) - except pkg_resources.DistributionNotFound: + dist = get_distribution(pkg) + if not dist: return False + return "pip" == get_installer(dist) def pip_self_version_check(session, options): diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 50a86982e4e..b0a7320a072 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -20,6 +20,7 @@ from pip._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 pip._vendor.packaging.utils import canonicalize_name from pip._vendor.retrying import retry # type: ignore from pip._vendor.six import PY2, text_type from pip._vendor.six.moves import input, map, zip_longest @@ -480,6 +481,40 @@ def user_test(d): ] +def search_distribution(req_name): + + # Canonicalize the name before searching in the list of + # installed distributions and also while creating the package + # dictionary to get the Distribution object + req_name = canonicalize_name(req_name) + packages = get_installed_distributions(skip=()) + pkg_dict = {canonicalize_name(p.key): p for p in packages} + return pkg_dict.get(req_name) + + +def get_distribution(req_name): + """Given a requirement name, return the installed Distribution object""" + + # Search the distribution by looking through the working set + dist = search_distribution(req_name) + + # If distribution could not be found, call working_set.require + # to update the working set, and try to find the distribution + # again. + # This might happen for e.g. when you install a package + # twice, once using setup.py develop and again using setup.py install. + # Now when run pip uninstall twice, the package gets removed + # from the working set in the first uninstall, so we have to populate + # the working set again so that pip knows about it and the packages + # gets picked up and is successfully uninstalled the second time too. + if not dist: + try: + pkg_resources.working_set.require(req_name) + except pkg_resources.DistributionNotFound: + return None + return search_distribution(req_name) + + def egg_link_path(dist): # type: (Distribution) -> Optional[str] """ diff --git a/tests/functional/test_search.py b/tests/functional/test_search.py index fce6c5f819c..5918b4f64f9 100644 --- a/tests/functional/test_search.py +++ b/tests/functional/test_search.py @@ -168,7 +168,8 @@ def test_latest_prerelease_install_message(caplog, monkeypatch): dist = pretend.stub(version="1.0.0") get_dist = pretend.call_recorder(lambda x: dist) - monkeypatch.setattr("pip._vendor.pkg_resources.get_distribution", get_dist) + monkeypatch.setattr("pip._internal.commands.search.get_distribution", + get_dist) with caplog.at_level(logging.INFO): print_results(hits) diff --git a/tests/unit/test_self_check_outdated.py b/tests/unit/test_self_check_outdated.py index f33e319cf78..c5e60d92fc4 100644 --- a/tests/unit/test_self_check_outdated.py +++ b/tests/unit/test_self_check_outdated.py @@ -6,7 +6,6 @@ import freezegun import pretend import pytest -from pip._vendor import pkg_resources from pip._internal import self_outdated_check from pip._internal.models.candidate import InstallationCandidate @@ -98,7 +97,7 @@ def test_pip_self_version_check(monkeypatch, stored_time, installed_ver, pretend.call_recorder(lambda *a, **kw: None)) monkeypatch.setattr(logger, 'debug', pretend.call_recorder(lambda s, exc_info=None: None)) - monkeypatch.setattr(pkg_resources, 'get_distribution', + monkeypatch.setattr(self_outdated_check, 'get_distribution', lambda name: MockDistribution(installer)) fake_state = pretend.stub( From ac624f1e4f34037bf199c112780f889e0d0a072e Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sat, 23 May 2020 18:59:38 +0530 Subject: [PATCH 2200/3170] Reword news entry --- news/5021.bugfix | 2 +- src/pip/_internal/commands/search.py | 2 -- src/pip/_internal/req/req_install.py | 1 - 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/news/5021.bugfix b/news/5021.bugfix index 4de60b18e76..36606fd20f5 100644 --- a/news/5021.bugfix +++ b/news/5021.bugfix @@ -1 +1 @@ -Package name should be normalized before we use it to search if it's already installed or not +Use canonical package names while looking up already installed packages. diff --git a/src/pip/_internal/commands/search.py b/src/pip/_internal/commands/search.py index c01d96a2473..e906ce7667f 100644 --- a/src/pip/_internal/commands/search.py +++ b/src/pip/_internal/commands/search.py @@ -19,7 +19,6 @@ from pip._internal.network.xmlrpc import PipXmlrpcTransport from pip._internal.utils.compat import get_terminal_size from pip._internal.utils.logging import indent_log - from pip._internal.utils.misc import get_distribution, write_output from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -32,7 +31,6 @@ {'name': str, 'summary': str, 'versions': List[str]}, ) - logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 2aa853642c5..644930a1528 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -429,7 +429,6 @@ def check_if_exists(self, use_user_site): """ if self.req is None: return - # get_distribution() will resolve the entire list of requirements # anyway, and we've already determined that we need the requirement # in question, so strip the marker so that we don't try to From 6c1030ca95f22e6111d1307e6cf9a032d7a063e7 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Mon, 6 Jul 2020 19:17:26 +0530 Subject: [PATCH 2201/3170] Fix comment order for retrying module --- src/pip/_internal/utils/misc.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index b0a7320a072..7cf9944fdd3 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -18,9 +18,9 @@ from collections import deque from pip._vendor import pkg_resources +from pip._vendor.packaging.utils import canonicalize_name # 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.packaging.utils import canonicalize_name from pip._vendor.retrying import retry # type: ignore from pip._vendor.six import PY2, text_type from pip._vendor.six.moves import input, map, zip_longest From d3955e7837292151a46ad3848f87d4fcfba7e9b8 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Mon, 6 Jul 2020 21:15:35 -0400 Subject: [PATCH 2202/3170] Don't unpack wheels during preparation For some time we have not needed to pre-emptively unpack wheels as part of metadata processing, but kept the existing logic because the behavior would start to diverge more for different package types. In this case, though, removing the special cases for wheels makes this logic a bit simpler, so it is worth doing. --- src/pip/_internal/operations/prepare.py | 20 ++++++++------------ 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 6f35897d16e..b316943c458 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -260,8 +260,9 @@ def unpack_url( ) # unpack the archive to the build dir location. even when only downloading - # archives, they have to be unpacked to parse dependencies - unpack_file(file.path, location, file.content_type) + # archives, they have to be unpacked to parse dependencies, except wheels + if not link.is_wheel: + unpack_file(file.path, location, file.content_type) return file @@ -390,16 +391,11 @@ def _ensure_link_req_src_dir(self, req, download_dir, parallel_builds): """Ensure source_dir of a linked InstallRequirement.""" # Since source_dir is only set for editable requirements. 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 - else: - # We always delete unpacked sdists after pip runs. - autodelete_unpacked = True + # We don't need to unpack wheels, so no need for a source + # directory. + return + # We always delete unpacked sdists after pip runs. + autodelete_unpacked = True assert req.source_dir is None req.ensure_has_source_dir( self.build_dir, From c9f87a645ba0520e4816c3f1b87074488d442288 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Mon, 6 Jul 2020 21:19:43 -0400 Subject: [PATCH 2203/3170] Inline constant variable --- src/pip/_internal/operations/prepare.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index b316943c458..fbc5fea92ed 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -394,12 +394,11 @@ def _ensure_link_req_src_dir(self, req, download_dir, parallel_builds): # We don't need to unpack wheels, so no need for a source # directory. return - # We always delete unpacked sdists after pip runs. - autodelete_unpacked = True assert req.source_dir is None + # We always delete unpacked sdists after pip runs. req.ensure_has_source_dir( self.build_dir, - autodelete=autodelete_unpacked, + autodelete=True, parallel_builds=parallel_builds, ) From e0d625ba93f291709b8f729c898827b43005c46f Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sat, 30 May 2020 23:05:00 +0530 Subject: [PATCH 2204/3170] Add type annotations for pip._internal.commands.configuration --- src/pip/_internal/commands/configuration.py | 33 ++++++++++++--------- 1 file changed, 19 insertions(+), 14 deletions(-) diff --git a/src/pip/_internal/commands/configuration.py b/src/pip/_internal/commands/configuration.py index 75289baad8b..a677658a84b 100644 --- a/src/pip/_internal/commands/configuration.py +++ b/src/pip/_internal/commands/configuration.py @@ -1,6 +1,3 @@ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - import logging import os import subprocess @@ -18,9 +15,11 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import List + from typing import List, Any, Optional from optparse import Values + from pip._internal.configuration import Kind + logger = logging.getLogger(__name__) @@ -54,14 +53,8 @@ class ConfigurationCommand(Command): %prog [<file-option>] debug """ - def __init__(self, name, summary, isolated=False): - super(ConfigurationCommand, self).__init__( - name, summary, isolated=isolated - ) - - self.configuration = None - def add_options(self): + # type: () -> None self.cmd_opts.add_option( '--editor', dest='editor', @@ -145,6 +138,7 @@ def run(self, options, args): return SUCCESS def _determine_file(self, options, need_value): + # type: (Values, bool) -> Optional[Kind] file_options = [key for key, value in ( (kinds.USER, options.user_file), (kinds.GLOBAL, options.global_file), @@ -171,30 +165,35 @@ def _determine_file(self, options, need_value): ) def list_values(self, options, args): + # type: (Values, List[str]) -> None self._get_n_args(args, "list", n=0) for key, value in sorted(self.configuration.items()): write_output("%s=%r", key, value) def get_name(self, options, args): + # type: (Values, List[str]) -> None key = self._get_n_args(args, "get [name]", n=1) value = self.configuration.get_value(key) write_output("%s", value) def set_name_value(self, options, args): + # type: (Values, List[str]) -> None 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): + # type: (Values, List[str]) -> None key = self._get_n_args(args, "unset [name]", n=1) self.configuration.unset_value(key) self._save_configuration() def list_config_values(self, options, args): + # type: (Values, List[str]) -> None """List config key-value pairs across different config files""" self._get_n_args(args, "debug", n=0) @@ -206,12 +205,13 @@ def list_config_values(self, options, args): for fname in files: with indent_log(): file_exists = os.path.exists(fname) - write_output("%s, exists: %r", - fname, file_exists) + write_output("%s, exists: %s", + fname, str(file_exists)) if file_exists: self.print_config_file_values(variant) def print_config_file_values(self, variant): + # type: (Kind) -> None """Get key-value pairs from the file of a variant""" for name, value in self.configuration.\ get_values_in_config(variant).items(): @@ -219,6 +219,7 @@ def print_config_file_values(self, variant): write_output("%s: %s", name, value) def print_env_var_values(self): + # type: () -> None """Get key-values pairs present as environment variables""" write_output("%s:", 'env_var') with indent_log(): @@ -227,6 +228,7 @@ def print_env_var_values(self): write_output("%s=%r", env_var, value) def open_in_editor(self, options, args): + # type: (Values, List[str]) -> None editor = self._determine_editor(options) fname = self.configuration.get_file_to_edit() @@ -242,6 +244,7 @@ def open_in_editor(self, options, args): ) def _get_n_args(self, args, example, n): + # type: (List[str], str, int) -> Any """Helper to make sure the command got the right number of arguments """ if len(args) != n: @@ -257,6 +260,7 @@ def _get_n_args(self, args, example, n): return args def _save_configuration(self): + # type: () -> None # We successfully ran a modifying command. Need to save the # configuration. try: @@ -264,11 +268,12 @@ def _save_configuration(self): except Exception: logger.error( "Unable to save configuration. Please report this as a bug.", - exc_info=1 + exc_info=True ) raise PipError("Internal Error.") def _determine_editor(self, options): + # type: (Values) -> str if options.editor is not None: return options.editor elif "VISUAL" in os.environ: From 61f344b0407fdce2bfce02864f062539c289c415 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sat, 30 May 2020 23:08:08 +0530 Subject: [PATCH 2205/3170] Add type annotations for pip._internal.commands.install --- src/pip/_internal/commands/install.py | 38 ++++++++++++++++----------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 86cbd09c616..575c958678d 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -1,10 +1,3 @@ -# The following comment should be removed at some point in the future. -# It's included for now because without it InstallCommand.run() has a -# couple errors where we have to know req.name is str rather than -# Optional[str] for the InstallRequirement req. -# mypy: strict-optional=False -# mypy: disallow-untyped-defs=False - from __future__ import absolute_import import errno @@ -44,7 +37,7 @@ if MYPY_CHECK_RUNNING: from optparse import Values - from typing import Any, Iterable, List, Optional + from typing import Iterable, List, Optional from pip._internal.models.format_control import FormatControl from pip._internal.req.req_install import InstallRequirement @@ -233,7 +226,7 @@ def add_options(self): @with_cleanup def run(self, options, args): - # type: (Values, List[Any]) -> int + # type: (Values, List[str]) -> int if options.use_user_site and options.target_dir is not None: raise CommandError("Can not combine '--user' and '--target'") @@ -331,7 +324,7 @@ def run(self, options, args): try: pip_req = requirement_set.get_requirement("pip") except KeyError: - modifying_pip = None + modifying_pip = False else: # If we're not replacing an already installed pip, # we're not modifying it. @@ -370,7 +363,8 @@ def run(self, options, args): raise InstallationError( "Could not build wheels for {} which use" " PEP 517 and cannot be installed directly".format( - ", ".join(r.name for r in pep517_build_failures))) + ", ".join(r.name # type: ignore + for r in pep517_build_failures))) to_install = resolver.get_installation_order( requirement_set @@ -431,7 +425,6 @@ def run(self, options, args): ) except EnvironmentError as error: show_traceback = (self.verbosity >= 1) - message = create_env_error_message( error, show_traceback, options.use_user_site, ) @@ -447,6 +440,7 @@ def run(self, options, args): return SUCCESS def _handle_target_dir(self, target_dir, target_temp_dir, upgrade): + # type: (str, Optional[TempDirectory], bool) -> None ensure_dir(target_dir) # Checking both purelib and platlib directories for installed @@ -502,6 +496,7 @@ def _handle_target_dir(self, target_dir, target_temp_dir, upgrade): ) def _warn_about_conflicts(self, to_install): + # type: (List[InstallRequirement]) -> None try: package_set, _dep_info = check_install_conflicts(to_install) except Exception: @@ -528,14 +523,24 @@ def _warn_about_conflicts(self, to_install): ) -def get_lib_location_guesses(*args, **kwargs): - scheme = distutils_scheme('', *args, **kwargs) +def get_lib_location_guesses( + user=False, # type: bool + home=None, # type: Optional[str] + root=None, # type: Optional[str] + isolated=False, # type: bool + prefix=None # type: Optional[str] +): + # type:(...) -> List[str] + scheme = distutils_scheme('', user=user, home=home, root=root, + isolated=isolated, prefix=prefix) return [scheme['purelib'], scheme['platlib']] -def site_packages_writable(**kwargs): +def site_packages_writable(root, isolated): + # type: (Optional[str], bool) -> bool return all( - test_writable_dir(d) for d in set(get_lib_location_guesses(**kwargs)) + test_writable_dir(d) for d in set( + get_lib_location_guesses(root=root, isolated=isolated)) ) @@ -650,6 +655,7 @@ def format_options(option_names): def create_env_error_message(error, show_traceback, using_user_site): + # type: (EnvironmentError, bool, bool) -> str """Format an error message for an EnvironmentError It may occur anytime during the execution of the install command. From 005cc1636cca943aef58baa44969afb180377923 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sat, 30 May 2020 23:42:24 +0530 Subject: [PATCH 2206/3170] Add news entry --- news/B7D1519B-CB5F-409D-835C-CF7A14DD9A92.trivial | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 news/B7D1519B-CB5F-409D-835C-CF7A14DD9A92.trivial diff --git a/news/B7D1519B-CB5F-409D-835C-CF7A14DD9A92.trivial b/news/B7D1519B-CB5F-409D-835C-CF7A14DD9A92.trivial new file mode 100644 index 00000000000..e69de29bb2d From 21ca1620d10b8696259a3781a958b02d897faccb Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sun, 31 May 2020 03:50:14 +0530 Subject: [PATCH 2207/3170] Change target_temp_dir type to TempDirectory --- src/pip/_internal/commands/install.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 575c958678d..4c454276af2 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -425,6 +425,7 @@ def run(self, options, args): ) except EnvironmentError as error: show_traceback = (self.verbosity >= 1) + message = create_env_error_message( error, show_traceback, options.use_user_site, ) @@ -432,7 +433,7 @@ def run(self, options, args): return ERROR - if options.target_dir: + if options.target_dir and target_temp_dir: self._handle_target_dir( options.target_dir, target_temp_dir, options.upgrade ) @@ -440,7 +441,7 @@ def run(self, options, args): return SUCCESS def _handle_target_dir(self, target_dir, target_temp_dir, upgrade): - # type: (str, Optional[TempDirectory], bool) -> None + # type: (str, TempDirectory, bool) -> None ensure_dir(target_dir) # Checking both purelib and platlib directories for installed From 87249b9850bb390750f5e8e349c7ab6ccc75e359 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Tue, 7 Jul 2020 15:06:27 +0530 Subject: [PATCH 2208/3170] Assert target_temp_dir instead of if check --- src/pip/_internal/commands/install.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 4c454276af2..54e998370bb 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -433,7 +433,8 @@ def run(self, options, args): return ERROR - if options.target_dir and target_temp_dir: + if options.target_dir: + assert target_temp_dir self._handle_target_dir( options.target_dir, target_temp_dir, options.upgrade ) From fb057308981225f5fbca9fcbc9e3d6c8eb88c99b Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Tue, 7 Jul 2020 15:19:27 +0530 Subject: [PATCH 2209/3170] Align write_output types with logger.info --- src/pip/_internal/commands/configuration.py | 4 ++-- src/pip/_internal/utils/misc.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/commands/configuration.py b/src/pip/_internal/commands/configuration.py index a677658a84b..c5d118a5ea7 100644 --- a/src/pip/_internal/commands/configuration.py +++ b/src/pip/_internal/commands/configuration.py @@ -205,8 +205,8 @@ def list_config_values(self, options, args): for fname in files: with indent_log(): file_exists = os.path.exists(fname) - write_output("%s, exists: %s", - fname, str(file_exists)) + write_output("%s, exists: %r", + fname, file_exists) if file_exists: self.print_config_file_values(variant) diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 7cf9944fdd3..dc482135e7e 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -568,7 +568,7 @@ def dist_location(dist): def write_output(msg, *args): - # type: (str, str) -> None + # type: (Any, Any) -> None logger.info(msg, *args) From 3c38110700c171858db59290a1b690874abe3c9e Mon Sep 17 00:00:00 2001 From: Nicole Harris <n.harris@kabucreative.com> Date: Wed, 24 Jun 2020 05:49:20 +0100 Subject: [PATCH 2210/3170] Add resolver docs --- docs/html/user_guide.rst | 180 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 180 insertions(+) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 3e25604ae00..10856e3354c 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -772,6 +772,186 @@ archives are built with identical packages. .. _`Using pip from your program`: +Fixing conflicting dependencies +=============================== + +The purpose of this section of documentation is to provide practical suggestions to +pip users who encounter an error where pip cannot install their +specified packages due to conflicting dependencies (a +``ResolutionImpossible`` error). + +This documentation is specific to the new resolver, which you can use +with the flag ``--unstable-feature=resolver``. + +Understanding your error message +-------------------------------- + +When you get a ``ResolutionImpossible`` error, you might see something +like this: + +:: + + pip install package_coffee==0.44.1 package_tea==4.3.0 + +:: + + Due to conflicting dependencies pip cannot install package_coffee and + package_tea: + - package_coffee depends on package_water<3.0.0,>=2.4.2 + - package_tea depends on package_water==2.3.1 + +In this example, pip cannot install the packages you have requested, +because they each depend on different versions of the same package +(``package_water``): + +- ``package_coffee`` version ``0.44.1`` depends on a version of + ``package_water`` that is less than ``3.0.0`` but greater than or equal to + ``2.4.2`` +- ``package_tea`` version ``4.3.0`` depends on version ``2.3.1`` of + ``package_water`` + +Sometimes these messages are straightforward to read, because they use +commonly understood comparison operators to specify the required version +(e.g. ``<`` or ``>``). + +However, Python packaging also supports some more complex ways for +specifying package versions (e.g. ``~=`` or ``*``): + +.. csv-table:: + :header: "Operator", "Description", "Example" + + ``>``, "Any version greater than the specified version", "``>3.1``: any + version greater than 3.1" + ``<``, "Any version less than the specified version", "``<3.1``: any version + less than ``3.1``" + ``<=``, "Any version less than or equal to the specified version", "``<=3.1``: + any version less than or equal to ``3.1``" + ``>=``, "Any version greater than or equal to the specified + version", "``>=3.1``: version ``3.1`` and greater" + ``==``, "Exactly the specified version", ``==3.1``: only version ``3.1`` + ``!=``, "Any version not equal to the specified version", "``!=3.1``: any + version other than ``3.1``" + ``~=``, "Any compatible release. Compatible releases are releases that are + within the same major or minor version, assuming the package author is using + semantic versioning.", "``~=3.1``: version ``3.1`` or later, but not version + ``4.0`` or later. ``~=3.1.2``: version ``3.1.2`` or later, but not + version ``3.2.0`` or later." + ``*``,Can be used at the end of a version number to represent "all", "``== 3. + 1.*``: any version that starts with ``3.1``. Equivalent to ``~=3.1.0``." + +The detailed specification of supported comparison operators can be +found in :pep:`440`. + +Possible solutions +------------------ + +The solution to your error will depend on your individual use case. Here +are some things to try: + +1. Audit your top level requirements +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +As a first step it is useful to audit your project and remove any +unnecessary or out of date requirements (e.g. from your ``setup.py`` or +``requirements.txt`` files). Removing these can significantly reduce the +complexity of your dependency tree, thereby reducing opportunities for +conflicts to occur. + +2. Loosen your top level requirements +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Sometimes the packages that you have asked pip to install are +incompatible because you have been too strict when you specified the +package version. + +In our first example both ``package_coffee`` and ``package_tea`` have been +*pinned* to use specific versions +(``package_coffee==0.44.1b0 package_tea==4.3.0``). + +To find a version of both ``package_coffee`` and ``package_tea`` that depend on +the same version of ``package_water``, you might consider: + +- Loosening the range of packages that you are prepared to install + (e.g. ``pip install "package_coffee>0.44.*" "package_tea>4.0.0"``) +- Asking pip to install *any* version of ``package_coffee`` and ``package_tea`` + by removing the version specifiers altogether (e.g. + ``pip install package_coffee package_tea``) + +In the second case, pip will automatically find a version of both +``package_coffee`` and ``package_tea`` that depend on the same version of +``package_water``, installing: + +- ``package_coffee 0.46.0b0``, which depends on ``package_water 2.6.1`` +- ``package_tea 4.3.0`` which *also* depends on ``package_water 2.6.1`` + +If you want to prioritize one package over another, you can add version +specifiers to *only* the more important package:: + + pip install package_coffee==0.44.1b0 package_tea + +This will result in: + +- ``package_coffee 0.44.1b0``, which depends on ``package_water 2.6.1`` +- ``package_tea 4.1.3`` which also depends on ``package_water 2.6.1`` + +Now that you have resolved the issue, you can repin the compatible +package versions as required. + +3. Loosen the requirements of your dependencies +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Assuming that you cannot resolve the conflict by loosening the version +of the package you require (as above), you can try to fix the issue on +your *dependency* by: + +- Requesting that the package maintainers loosen *their* dependencies +- Forking the package and loosening the dependencies yourself + +.. warning:: + + If you choose to fork the package yourself, you are *opting out* of + any support provided by the package maintainers. Proceed at your own risk! + +4. All requirements are loose, but a solution does not exist +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +Sometimes it's simply impossible to find a combination of package +versions that do not conflict. Welcome to `dependency hell`_. + +In this situation, you could consider: + +- Using an alternative package, if that is acceptable for your project. + See `Awesome Python`_ for similar packages. +- Refactoring your project to reduce the number of dependencies (for + example, by breaking up a monolithic code base into smaller pieces) + +Getting help +------------ + +If none of the suggestions above work for you, we recommend that you ask +for help on: + +- `Python user Discourse`_ +- `Python user forums`_ +- `Python developers Slack channel`_ +- `Python IRC`_ +- `Stack Overflow`_ + +See `"How do I ask a good question?"`_ for tips on asking for help. + +Unfortunately, **the pip team cannot provide support for individual +dependency conflict errors**. Please *only* open a ticket on the `pip +issue tracker`_ if you believe that your problem has exposed a bug in pip. + +.. _dependency hell: https://en.wikipedia.org/wiki/Dependency_hell> +.. _Awesome Python: https://python.libhunt.com/ +.. _Python user Discourse: https://discuss.python.org/c/users/7 +.. _Python user forums: https://www.python.org/community/forums/ +.. _Python developers Slack channel: https://pythondev.slack.com/ +.. _Python IRC: https://www.python.org/community/irc/ +.. _Stack Overflow: https://stackoverflow.com/questions/tagged/python +.. _"How do I ask a good question?": https://stackoverflow.com/help/how-to-ask +.. _pip issue tracker: https://github.com/pypa/pip/issues Using pip from your program =========================== From 909f50aa8f58a268dc310631f1330ca41db09616 Mon Sep 17 00:00:00 2001 From: Nicole Harris <n.harris@kabucreative.com> Date: Tue, 7 Jul 2020 11:19:30 +0100 Subject: [PATCH 2211/3170] Add news --- news/8459.doc | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/8459.doc diff --git a/news/8459.doc b/news/8459.doc new file mode 100644 index 00000000000..1438edb891d --- /dev/null +++ b/news/8459.doc @@ -0,0 +1 @@ +Add documentation that helps the user fix dependency conflicts From 600d5272e6b7556e019002e8dbd08aeca296a532 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 7 Jul 2020 17:34:23 +0800 Subject: [PATCH 2212/3170] Add failing test for --prefix location --- tests/unit/test_locations.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/unit/test_locations.py b/tests/unit/test_locations.py index 3d24f717387..c9bbe794326 100644 --- a/tests/unit/test_locations.py +++ b/tests/unit/test_locations.py @@ -130,3 +130,19 @@ def test_install_lib_takes_precedence(self, tmpdir, monkeypatch): scheme = distutils_scheme('example') assert scheme['platlib'] == install_lib + os.path.sep assert scheme['purelib'] == install_lib + os.path.sep + + def test_prefix_modifies_appropriately(self): + prefix = os.path.abspath(os.path.join('somewhere', 'else')) + + normal_scheme = distutils_scheme("example") + prefix_scheme = distutils_scheme("example", prefix=prefix) + + def _calculate_expected(value): + path = os.path.join(prefix, os.path.relpath(value, sys.prefix)) + return os.path.normpath(path) + + expected = { + k: _calculate_expected(v) + for k, v in normal_scheme.items() + } + assert prefix_scheme == expected From b85d5026e3c8458193e4c24d5d121eeb03df56ee Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 7 Jul 2020 18:53:07 +0800 Subject: [PATCH 2213/3170] Fix header calculation when prefix is given --- news/8521.bugfix | 2 ++ src/pip/_internal/locations.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 news/8521.bugfix diff --git a/news/8521.bugfix b/news/8521.bugfix new file mode 100644 index 00000000000..fa29750f0b0 --- /dev/null +++ b/news/8521.bugfix @@ -0,0 +1,2 @@ +Headers provided by wheels in .data directories are now correctly installed +into the user-provided locations, such as ``--prefix``. diff --git a/src/pip/_internal/locations.py b/src/pip/_internal/locations.py index 0c115531911..0c1235488d6 100644 --- a/src/pip/_internal/locations.py +++ b/src/pip/_internal/locations.py @@ -138,7 +138,7 @@ def distutils_scheme( if running_under_virtualenv(): scheme['headers'] = os.path.join( - sys.prefix, + i.prefix, 'include', 'site', 'python{}'.format(get_major_minor_version()), From 9cbefbb3fa90e84f4f24b644fb4e2a4fa63749c1 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 7 Jul 2020 21:21:23 +0800 Subject: [PATCH 2214/3170] Mention the breakage only happens in virtualenvs --- news/8521.bugfix | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/news/8521.bugfix b/news/8521.bugfix index fa29750f0b0..d5b1da3a829 100644 --- a/news/8521.bugfix +++ b/news/8521.bugfix @@ -1,2 +1,3 @@ Headers provided by wheels in .data directories are now correctly installed -into the user-provided locations, such as ``--prefix``. +into the user-provided locations, such as ``--prefix``, instead of the virtual +environment pip is running in. From a365764e579104fc04a201a14894a3cd02ec31de Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 5 Jul 2020 16:14:13 -0400 Subject: [PATCH 2215/3170] Add test for RECORD population by make_wheel --- tests/lib/test_wheel.py | 44 +++++++++++++++++++++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/tests/lib/test_wheel.py b/tests/lib/test_wheel.py index b5c22ae31fd..a6f46cd899c 100644 --- a/tests/lib/test_wheel.py +++ b/tests/lib/test_wheel.py @@ -1,9 +1,12 @@ """Tests for wheel helper. """ +import csv from email import message_from_string from functools import partial from zipfile import ZipFile +from pip._vendor.six import ensure_text, iteritems + from pip._internal.utils.typing import MYPY_CHECK_RUNNING from tests.lib.wheel import ( _default, @@ -152,6 +155,47 @@ def test_make_wheel_basics(tmpdir): } +def test_make_wheel_default_record(): + with make_wheel( + name="simple", + version="0.1.0", + extra_files={"simple/__init__.py": "a"}, + extra_metadata_files={"LICENSE": "b"}, + extra_data_files={"purelib/info.txt": "c"}, + ).as_zipfile() as z: + record_bytes = z.read("simple-0.1.0.dist-info/RECORD") + record_text = ensure_text(record_bytes) + record_rows = list(csv.reader(record_text.splitlines())) + records = { + row[0]: row[1:] for row in record_rows + } + + expected = { + "simple/__init__.py": [ + "sha256=ypeBEsobvcr6wjGzmiPcTaeG7_gUfE5yuYB3ha_uSLs", "1" + ], + "simple-0.1.0.data/purelib/info.txt": [ + "sha256=Ln0sA6lQeuJl7PW1NWiFpTOTogKdJBOUmXJloaJa78Y", "1" + ], + "simple-0.1.0.dist-info/LICENSE": [ + "sha256=PiPoFgA5WUoziU9lZOGxNIu9egCI1CxKy3PurtWcAJ0", "1" + ], + "simple-0.1.0.dist-info/RECORD": ["", ""], + } + for name, values in iteritems(expected): + assert records[name] == values, name + + # WHEEL and METADATA aren't constructed in a stable way, so just spot + # check. + expected_variable = { + "simple-0.1.0.dist-info/METADATA": "51", + "simple-0.1.0.dist-info/WHEEL": "104", + } + for name, length in iteritems(expected_variable): + assert records[name][0].startswith("sha256="), name + assert records[name][1] == length, name + + def test_make_wheel_extra_files(): with make_wheel( name="simple", From ffddab6986ce724c919f6b05482a90cafb91c292 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 5 Jul 2020 16:20:07 -0400 Subject: [PATCH 2216/3170] Test that RECORD is populated correctly for data files --- tests/functional/test_install_wheel.py | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/tests/functional/test_install_wheel.py b/tests/functional/test_install_wheel.py index 6d94622458a..3762088b705 100644 --- a/tests/functional/test_install_wheel.py +++ b/tests/functional/test_install_wheel.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- +import csv import distutils import glob import os @@ -282,6 +283,28 @@ def test_wheel_record_lines_in_deterministic_order(script, data): assert record_lines == sorted(record_lines) +def test_wheel_record_lines_have_hash_for_data_files(script): + package = make_wheel( + "simple", + "0.1.0", + extra_data_files={ + "purelib/info.txt": "c", + }, + ).save_to_dir(script.scratch_path) + script.pip("install", package) + record_file = ( + script.site_packages_path / "simple-0.1.0.dist-info" / "RECORD" + ) + record_text = record_file.read_text() + record_rows = list(csv.reader(record_text.splitlines())) + records = { + r[0]: r[1:] for r in record_rows + } + assert records["info.txt"] == [ + "sha256=Ln0sA6lQeuJl7PW1NWiFpTOTogKdJBOUmXJloaJa78Y", "1" + ] + + @pytest.mark.incompatible_with_test_venv def test_install_user_wheel(script, shared_data, with_wheel, tmpdir): """ From 50d7b930d3ca1f4604a313ca527d9f29449cc949 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 7 Jul 2020 18:19:15 -0400 Subject: [PATCH 2217/3170] Create more robust test for isolated --install-option The current test depends on passing `--home` to `--install-option`. Since we would like that to fail, we need to use another argument. None of the other possible arguments have a visible side-effect, so we just write the provided arguments to a file and check that in the test. --- tests/functional/test_install_reqs.py | 84 ++++++++++++++++++++++++++- 1 file changed, 83 insertions(+), 1 deletion(-) diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index ae90a041b54..5f5877abd96 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -1,3 +1,4 @@ +import json import os import textwrap @@ -5,6 +6,7 @@ from tests.lib import ( _create_test_package_with_subdirectory, + create_basic_sdist_for_package, create_basic_wheel_for_package, need_svn, path_to_url, @@ -15,6 +17,51 @@ from tests.lib.path import Path +class ArgRecordingSdist(object): + def __init__(self, sdist_path, args_path): + self.sdist_path = sdist_path + self._args_path = args_path + + def args(self): + return json.loads(self._args_path.read_text()) + + +@pytest.fixture() +def arg_recording_sdist_maker(script): + arg_writing_setup_py = textwrap.dedent( + """ + import io + import json + import os + import sys + + from setuptools import setup + + args_path = os.path.join(os.environ["OUTPUT_DIR"], "{name}.json") + with open(args_path, 'w') as f: + json.dump(sys.argv, f) + + setup(name={name!r}, version="0.1.0") + """ + ) + output_dir = script.scratch_path.joinpath( + "args_recording_sdist_maker_output" + ) + output_dir.mkdir(parents=True) + script.environ["OUTPUT_DIR"] = str(output_dir) + + def _arg_recording_sdist_maker(name): + # type: (str) -> ArgRecordingSdist + extra_files = {"setup.py": arg_writing_setup_py.format(name=name)} + sdist_path = create_basic_sdist_for_package( + script, name, "0.1.0", extra_files + ) + args_path = output_dir / "{}.json".format(name) + return ArgRecordingSdist(sdist_path, args_path) + + return _arg_recording_sdist_maker + + @pytest.mark.network def test_requirements_file(script): """ @@ -605,7 +652,42 @@ def test_install_unsupported_wheel_file(script, data): assert len(result.files_created) == 0 -def test_install_options_local_to_package(script, data): +def test_install_options_local_to_package(script, arg_recording_sdist_maker): + """Make sure --install-options does not leak across packages. + + A requirements.txt file can have per-package --install-options; these + should be isolated to just the package instead of leaking to subsequent + packages. This needs to be a functional test because the bug was around + cross-contamination at install time. + """ + + simple1_sdist = arg_recording_sdist_maker("simple1") + simple2_sdist = arg_recording_sdist_maker("simple2") + + reqs_file = script.scratch_path.joinpath("reqs.txt") + reqs_file.write_text( + textwrap.dedent( + """ + simple1 --install-option='-O0' + simple2 + """ + ) + ) + script.pip( + 'install', + '--no-index', '-f', str(simple1_sdist.sdist_path.parent), + '-r', reqs_file, + ) + + simple1_args = simple1_sdist.args() + assert 'install' in simple1_args + assert '-O0' in simple1_args + simple2_args = simple2_sdist.args() + assert 'install' in simple2_args + assert '-O0' not in simple2_args + + +def test_install_options_local_to_package_old(script, data): """Make sure --install-options does not leak across packages. A requirements.txt file can have per-package --install-options; these From b46d8ab01ad3a764ab0a4cef970754baa9c7e25b Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 7 Jul 2020 18:19:16 -0400 Subject: [PATCH 2218/3170] Refactor overriding test to not use disallowed option Similar to our previous test refactoring, this removes the usage of `--home` from the test command. "Overriding" in the original test meant "placed after" in the command-line arguments, which makes sense because setuptools will use the last argument passed. --- tests/functional/test_install_reqs.py | 33 +++++++++++---------------- 1 file changed, 13 insertions(+), 20 deletions(-) diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index 5f5877abd96..d4274aebd7a 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -306,28 +306,21 @@ def test_wheel_user_with_prefix_in_pydistutils_cfg( assert 'installed requiresupper' in result.stdout -def test_install_option_in_requirements_file(script, data, virtualenv): - """ - Test --install-option in requirements file overrides same option in cli - """ - - script.scratch_path.joinpath("home1").mkdir() - script.scratch_path.joinpath("home2").mkdir() - - script.scratch_path.joinpath("reqs.txt").write_text( - textwrap.dedent( - """simple --install-option='--home={home}'""".format( - home=script.scratch_path.joinpath("home1")))) +def test_install_option_in_requirements_file_overrides_cli( + script, arg_recording_sdist_maker +): + simple_sdist = arg_recording_sdist_maker("simple") - result = script.pip( - 'install', '--no-index', '-f', data.find_links, '-r', - script.scratch_path / 'reqs.txt', - '--install-option=--home={home}'.format( - home=script.scratch_path.joinpath("home2")), - expect_stderr=True) + reqs_file = script.scratch_path.joinpath("reqs.txt") + reqs_file.write_text("simple --install-option='-O0'") - package_dir = script.scratch / 'home1' / 'lib' / 'python' / 'simple' - result.did_create(package_dir) + script.pip( + 'install', '--no-index', '-f', str(simple_sdist.sdist_path.parent), + '-r', str(reqs_file), '--install-option=-O1', + ) + simple_args = simple_sdist.args() + assert 'install' in simple_args + assert simple_args.index('-O1') < simple_args.index('-O0') def test_constraints_not_installed_by_default(script, data): From 89572a7d40e9a84baa692fda1bb16b3d76ad8d3b Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 7 Jul 2020 18:29:07 -0400 Subject: [PATCH 2219/3170] Throw CommandError on any location-related install options --- src/pip/_internal/commands/install.py | 21 +++++------------ tests/functional/test_install_reqs.py | 33 +++++---------------------- tests/unit/test_command_install.py | 26 ++++++++++----------- 3 files changed, 24 insertions(+), 56 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 54e998370bb..ecf0b523648 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -21,7 +21,6 @@ from pip._internal.operations.check import check_install_conflicts from pip._internal.req import install_given_reqs from pip._internal.req.req_tracker import get_requirement_tracker -from pip._internal.utils.deprecation import deprecated from pip._internal.utils.distutils_args import parse_distutils_args from pip._internal.utils.filesystem import test_writable_dir from pip._internal.utils.misc import ( @@ -639,20 +638,12 @@ def format_options(option_names): if not offenders: return - deprecated( - reason=( - "Location-changing options found in --install-option: {}. " - "This configuration may cause unexpected behavior and is " - "unsupported.".format( - "; ".join(offenders) - ) - ), - replacement=( - "using pip-level options like --user, --prefix, --root, and " - "--target" - ), - gone_in="20.2", - issue=7309, + raise CommandError( + "Location-changing options found in --install-option: {}." + " This is unsupported, use pip-level options like --user," + " --prefix, --root, and --target instead.".format( + "; ".join(offenders) + ) ) diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index d4274aebd7a..7ec863493b4 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -680,35 +680,14 @@ def test_install_options_local_to_package(script, arg_recording_sdist_maker): assert '-O0' not in simple2_args -def test_install_options_local_to_package_old(script, data): - """Make sure --install-options does not leak across packages. - - A requirements.txt file can have per-package --install-options; these - should be isolated to just the package instead of leaking to subsequent - packages. This needs to be a functional test because the bug was around - cross-contamination at install time. - """ - home_simple = script.scratch_path.joinpath("for-simple") - test_simple = script.scratch.joinpath("for-simple") - home_simple.mkdir() +def test_location_related_install_option_fails(script): + simple_sdist = create_basic_sdist_for_package(script, "simple", "0.1.0") reqs_file = script.scratch_path.joinpath("reqs.txt") - reqs_file.write_text( - textwrap.dedent(""" - simple --install-option='--home={home_simple}' - INITools - """.format(**locals()))) + reqs_file.write_text("simple --install-option='--home=/tmp'") result = script.pip( 'install', - '--no-index', '-f', data.find_links, + '--no-index', '-f', str(simple_sdist.parent), '-r', reqs_file, - expect_stderr=True, + expect_error=True ) - - simple = test_simple / 'lib' / 'python' / 'simple' - bad = test_simple / 'lib' / 'python' / 'initools' - good = script.site_packages / 'initools' - result.did_create(simple) - assert result.files_created[simple].dir - result.did_not_create(bad) - result.did_create(good) - assert result.files_created[good].dir + assert "['--home'] from simple" in result.stderr diff --git a/tests/unit/test_command_install.py b/tests/unit/test_command_install.py index 80aaf4ae8b3..aae41192e5b 100644 --- a/tests/unit/test_command_install.py +++ b/tests/unit/test_command_install.py @@ -9,6 +9,7 @@ decide_user_install, warn_deprecated_install_options, ) +from pip._internal.exceptions import CommandError from pip._internal.req.req_install import InstallRequirement @@ -44,16 +45,15 @@ def test_most_cases( assert decide_user_install(use_user_site=None) is result -def test_deprecation_notice_for_pip_install_options(recwarn): +def test_deprecation_notice_for_pip_install_options(): install_options = ["--prefix=/hello"] - warn_deprecated_install_options([], install_options) + with pytest.raises(CommandError) as e: + warn_deprecated_install_options([], install_options) - assert len(recwarn) == 1 - message = recwarn[0].message.args[0] - assert "['--prefix'] from command line" in message + assert "['--prefix'] from command line" in str(e.value) -def test_deprecation_notice_for_requirement_options(recwarn): +def test_deprecation_notice_for_requirement_options(): install_options = [] bad_named_req_options = ["--home=/wow"] @@ -67,18 +67,16 @@ def test_deprecation_notice_for_requirement_options(recwarn): None, "requirements2.txt", install_options=bad_unnamed_req_options ) - warn_deprecated_install_options( - [bad_named_req, bad_unnamed_req], install_options - ) - - assert len(recwarn) == 1 - message = recwarn[0].message.args[0] + with pytest.raises(CommandError) as e: + warn_deprecated_install_options( + [bad_named_req, bad_unnamed_req], install_options + ) assert ( "['--install-lib'] from <InstallRequirement> (from requirements2.txt)" - in message + in str(e.value) ) - assert "['--home'] from hello (from requirements.txt)" in message + assert "['--home'] from hello (from requirements.txt)" in str(e.value) @pytest.mark.parametrize('error, show_traceback, using_user_site, expected', [ From f878fcdeec90ca6b42cb66f704da25c958253d70 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 7 Jul 2020 18:31:54 -0400 Subject: [PATCH 2220/3170] Rename function that reject location install options Since it no longer shows a deprecation warning, the previous name doesn't fit. --- src/pip/_internal/commands/install.py | 4 ++-- tests/unit/test_command_install.py | 10 +++++----- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index ecf0b523648..8537d731f19 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -289,7 +289,7 @@ def run(self, options, args): try: reqs = self.get_requirements(args, options, finder, session) - warn_deprecated_install_options( + reject_location_related_install_options( reqs, options.install_options ) @@ -605,7 +605,7 @@ def decide_user_install( return True -def warn_deprecated_install_options(requirements, options): +def reject_location_related_install_options(requirements, options): # type: (List[InstallRequirement], Optional[List[str]]) -> None """If any location-changing --install-option arguments were passed for requirements or on the command-line, then show a deprecation warning. diff --git a/tests/unit/test_command_install.py b/tests/unit/test_command_install.py index aae41192e5b..7b6b38de0fa 100644 --- a/tests/unit/test_command_install.py +++ b/tests/unit/test_command_install.py @@ -7,7 +7,7 @@ from pip._internal.commands.install import ( create_env_error_message, decide_user_install, - warn_deprecated_install_options, + reject_location_related_install_options, ) from pip._internal.exceptions import CommandError from pip._internal.req.req_install import InstallRequirement @@ -45,15 +45,15 @@ def test_most_cases( assert decide_user_install(use_user_site=None) is result -def test_deprecation_notice_for_pip_install_options(): +def test_rejection_for_pip_install_options(): install_options = ["--prefix=/hello"] with pytest.raises(CommandError) as e: - warn_deprecated_install_options([], install_options) + reject_location_related_install_options([], install_options) assert "['--prefix'] from command line" in str(e.value) -def test_deprecation_notice_for_requirement_options(): +def test_rejection_for_location_requirement_options(): install_options = [] bad_named_req_options = ["--home=/wow"] @@ -68,7 +68,7 @@ def test_deprecation_notice_for_requirement_options(): ) with pytest.raises(CommandError) as e: - warn_deprecated_install_options( + reject_location_related_install_options( [bad_named_req, bad_unnamed_req], install_options ) From 8c7b9429cd69deaf76d1e7f526fe7e8d927fb6ac Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Tue, 7 Jul 2020 18:34:35 -0400 Subject: [PATCH 2221/3170] Add news --- news/7309.removal | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/7309.removal diff --git a/news/7309.removal b/news/7309.removal new file mode 100644 index 00000000000..979b8616055 --- /dev/null +++ b/news/7309.removal @@ -0,0 +1 @@ +Disallow passing install-location-related arguments in ``--install-options``. From cb540f30e330c7846deb30c0a8e74746316dc9b1 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 30 Nov 2019 19:13:06 -0500 Subject: [PATCH 2222/3170] Add basic test coverage configuration This handles: * Sub-processes within unit tests (thanks pytest-cov) * Our pytest-fixture-based virtual environments and subprocesses therein * Running with xdist (e.g. `-n auto`) * Combining results from all of the above using paths rooted with `src/pip/*` This doesn't handle: * Platform-specific branches * Python 2 * CI integration --- .coveragerc | 4 --- setup.cfg | 38 ++++++++++++++++++++++ tests/conftest.py | 16 ++++++++- tools/requirements/tests-common_wheels.txt | 2 ++ tox.ini | 16 +++++++-- 5 files changed, 69 insertions(+), 7 deletions(-) delete mode 100644 .coveragerc diff --git a/.coveragerc b/.coveragerc deleted file mode 100644 index 5f833c94a52..00000000000 --- a/.coveragerc +++ /dev/null @@ -1,4 +0,0 @@ -[run] -branch = True -omit = - src/pip/_vendor/* diff --git a/setup.cfg b/setup.cfg index b8ee43f5a46..2415d2d2b89 100644 --- a/setup.cfg +++ b/setup.cfg @@ -59,6 +59,44 @@ markers = yaml: yaml based tests fails_on_new_resolver: Does not yet work on the new resolver +[coverage:run] +branch = True +# Do not gather coverage for vendored libraries. +omit = */_vendor/* +# Centralized absolute file prefix for coverage files. +data_file = ${COVERAGE_OUTPUT_DIR}/.coverage +# By default, each covered process will try to truncate and then write to +# `data_file`, but with `parallel`, they will write to separate files suffixed +# with hostname, pid, and a timestamp. +parallel = True +# If not set, then at the termination of each worker (when using pytest-xdist), +# the following is traced: "Coverage.py warning: Module pip was previously +# imported, but not measured (module-not-measured)" +disable_warnings = module-not-measured + +[coverage:paths] +# We intentionally use "source0" here because pytest-cov unconditionally sets +# "source" after loading the config. +source0 = + # The primary source code path which other paths will be combined into. + src/pip/ + # Unit test source directory e.g. + # `.tox/coverage-py3/lib/pythonX.Y/site-packages/pip/...` + */site-packages/pip/ + # Functional test virtual environment directories, which look like + # `tmpdir/pip0/pip/src/pip/...` + */pip/src/pip/ + +[coverage:report] +exclude_lines = + # We must re-state the default because the `exclude_lines` option overrides + # it. + pragma: no cover + # This excludes typing-specific code, which will be validated by mypy anyway. + if MYPY_CHECK_RUNNING + # Can be set to exclude e.g. `if PY2:` on Python 3 + ${PIP_CI_COVERAGE_EXCLUDES} + [bdist_wheel] universal = 1 diff --git a/tests/conftest.py b/tests/conftest.py index 0db6d96725e..2aab50207be 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -294,6 +294,13 @@ def wheel_install(tmpdir_factory, common_wheels): 'wheel') +@pytest.fixture(scope='session') +def coverage_install(tmpdir_factory, common_wheels): + return _common_wheel_editable_install(tmpdir_factory, + common_wheels, + 'coverage') + + def install_egg_link(venv, project_name, egg_info_dir): with open(venv.site / 'easy-install.pth', 'a') as fp: fp.write(str(egg_info_dir.resolve()) + '\n') @@ -303,7 +310,7 @@ def install_egg_link(venv, project_name, egg_info_dir): @pytest.fixture(scope='session') def virtualenv_template(request, tmpdir_factory, pip_src, - setuptools_install, common_wheels): + setuptools_install, coverage_install): if six.PY3 and request.config.getoption('--use-venv'): venv_type = 'venv' @@ -327,6 +334,13 @@ def virtualenv_template(request, tmpdir_factory, pip_src, subprocess.check_call([venv.bin / 'python', 'setup.py', '-q', 'develop'], cwd=pip_editable) + # Install coverage and pth file for executing it in any spawned processes + # in this virtual environment. + install_egg_link(venv, 'coverage', coverage_install) + # zz prefix ensures the file is after easy-install.pth. + with open(venv.site / 'zz-coverage-helper.pth', 'a') as f: + f.write('import coverage; coverage.process_startup()') + # Drop (non-relocatable) launchers. for exe in os.listdir(venv.bin): if not ( diff --git a/tools/requirements/tests-common_wheels.txt b/tools/requirements/tests-common_wheels.txt index 6703d606cee..f0edf0b028b 100644 --- a/tools/requirements/tests-common_wheels.txt +++ b/tools/requirements/tests-common_wheels.txt @@ -7,3 +7,5 @@ setuptools >= 40.8.0 wheel +# As required by pytest-cov. +coverage >= 4.4 diff --git a/tox.ini b/tox.ini index f05473d9764..d3f4993bd53 100644 --- a/tox.ini +++ b/tox.ini @@ -8,6 +8,7 @@ envlist = # Wrapper for calls to pip that make sure the version being used is the # original virtualenv (stable) version, and not the code being tested. pip = python {toxinidir}/tools/tox_pip.py +mkdirp = python -c 'import os, sys; os.path.exists(sys.argv[1]) or os.mkdir(sys.argv[1])' [testenv] # Remove USERNAME once we drop PY2. @@ -30,9 +31,20 @@ commands = pytest --timeout 300 [] install_command = {[helpers]pip} install {opts} {packages} list_dependencies_command = {[helpers]pip} freeze --all -[testenv:coverage-py3] +[testenv:coverage] basepython = python3 -commands = pytest --timeout 300 --cov=pip --cov-report=term-missing --cov-report=xml --cov-report=html tests/unit {posargs} +commands = + {[helpers]mkdirp} {toxinidir}/.coverage-output + pytest --timeout 300 --cov=pip --cov-config={toxinidir}/setup.cfg [] + +setenv = + # Used in coverage configuration in setup.cfg. + COVERAGE_OUTPUT_DIR = {toxinidir}/.coverage-output + # Ensure coverage is enabled in child processes in virtual environments + # since they won't already have been enabled by pytest-cov. + COVERAGE_PROCESS_START = {toxinidir}/setup.cfg + # Used in coverage configuration in setup.cfg. + PIP_CI_COVERAGE_EXCLUDES = if PY2 [testenv:docs] # Don't skip install here since pip_sphinxext uses pip's internals. From a17e5e0a726fa9799c4cd1d3c4604283e052a66b Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 8 Jul 2020 20:24:29 +0800 Subject: [PATCH 2223/3170] Refactor to apply Mypy strict-optional=True --- src/pip/_internal/req/__init__.py | 41 +++++++++++++++++-------------- 1 file changed, 22 insertions(+), 19 deletions(-) diff --git a/src/pip/_internal/req/__init__.py b/src/pip/_internal/req/__init__.py index 75b532b0ee7..8568d3f8b6e 100644 --- a/src/pip/_internal/req/__init__.py +++ b/src/pip/_internal/req/__init__.py @@ -1,8 +1,6 @@ -# The following comment should be removed at some point in the future. -# mypy: strict-optional=False - from __future__ import absolute_import +import collections import logging from pip._internal.utils.logging import indent_log @@ -13,7 +11,7 @@ from .req_set import RequirementSet if MYPY_CHECK_RUNNING: - from typing import List, Optional, Sequence + from typing import Iterator, List, Optional, Sequence, Tuple __all__ = [ "RequirementSet", "InstallRequirement", @@ -33,8 +31,17 @@ def __repr__(self): return "InstallationResult(name={!r})".format(self.name) +def _validate_requirements( + requirements, # type: List[InstallRequirement] +): + # type: (...) -> Iterator[Tuple[str, InstallRequirement]] + for req in requirements: + assert req.name, "invalid to-be-installed requirement: {}".format(req) + yield req.name, req + + def install_given_reqs( - to_install, # type: List[InstallRequirement] + requirements, # type: List[InstallRequirement] install_options, # type: List[str] global_options, # type: Sequence[str] root, # type: Optional[str] @@ -50,23 +57,27 @@ def install_given_reqs( (to be called after having downloaded and unpacked the packages) """ + to_install = collections.OrderedDict(_validate_requirements(requirements)) if to_install: logger.info( 'Installing collected packages: %s', - ', '.join([req.name for req in to_install]), + ', '.join(to_install.keys()), ) installed = [] with indent_log(): - for requirement in to_install: + for req_name, requirement in to_install.items(): if requirement.should_reinstall: - logger.info('Attempting uninstall: %s', requirement.name) + logger.info('Attempting uninstall: %s', req_name) with indent_log(): uninstalled_pathset = requirement.uninstall( auto_confirm=True ) + else: + uninstalled_pathset = None + try: requirement.install( install_options, @@ -79,22 +90,14 @@ def install_given_reqs( pycompile=pycompile, ) except Exception: - should_rollback = ( - requirement.should_reinstall and - not requirement.install_succeeded - ) # if install did not succeed, rollback previous uninstall - if should_rollback: + if uninstalled_pathset and not requirement.install_succeeded: uninstalled_pathset.rollback() raise else: - should_commit = ( - requirement.should_reinstall and - requirement.install_succeeded - ) - if should_commit: + if uninstalled_pathset and requirement.install_succeeded: uninstalled_pathset.commit() - installed.append(InstallationResult(requirement.name)) + installed.append(InstallationResult(req_name)) return installed From 8c267e6e39560f80e37a7e1ba78140a0fc0c8229 Mon Sep 17 00:00:00 2001 From: gutsytechster <prashantsharma161198@gmail.com> Date: Sun, 3 May 2020 22:18:24 +0530 Subject: [PATCH 2224/3170] feat(pip/_internal/*): Use custom raise_for_status method --- news/5380.feature | 1 + src/pip/_internal/cli/base_command.py | 3 +- src/pip/_internal/exceptions.py | 17 +++++++++++ src/pip/_internal/index/collector.py | 10 ++++--- src/pip/_internal/network/download.py | 8 ++--- src/pip/_internal/network/utils.py | 28 +++++++++++++++++ src/pip/_internal/network/xmlrpc.py | 8 +++-- src/pip/_internal/operations/prepare.py | 4 +-- src/pip/_internal/req/req_file.py | 3 +- tests/lib/requests_mocks.py | 3 -- tests/unit/test_collector.py | 40 +++++++++++++++++-------- tests/unit/test_network_utils.py | 34 +++++++++++++++++++++ tests/unit/test_operations_prepare.py | 7 +++-- 13 files changed, 134 insertions(+), 32 deletions(-) create mode 100644 news/5380.feature create mode 100644 tests/unit/test_network_utils.py diff --git a/news/5380.feature b/news/5380.feature new file mode 100644 index 00000000000..df2ef032c04 --- /dev/null +++ b/news/5380.feature @@ -0,0 +1 @@ +Refine error messages to avoid showing Python tracebacks when an HTTP error occurs. diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index 4dd6baa2386..c3b6a856be4 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -26,6 +26,7 @@ BadCommand, CommandError, InstallationError, + NetworkConnectionError, PreviousBuildDirError, SubProcessError, UninstallationError, @@ -221,7 +222,7 @@ def _main(self, args): return PREVIOUS_BUILD_DIR_ERROR except (InstallationError, UninstallationError, BadCommand, - SubProcessError) as exc: + SubProcessError, NetworkConnectionError) as exc: logger.critical(str(exc)) logger.debug('Exception information:', exc_info=True) diff --git a/src/pip/_internal/exceptions.py b/src/pip/_internal/exceptions.py index 8407cec7e96..0ac5534f1fd 100644 --- a/src/pip/_internal/exceptions.py +++ b/src/pip/_internal/exceptions.py @@ -99,6 +99,23 @@ class PreviousBuildDirError(PipError): """Raised when there's a previous conflicting build directory""" +class NetworkConnectionError(PipError): + """HTTP connection error""" + + def __init__(self, *args, **kwargs): + """ + Initialize NetworkConnectionError with `request` and `response` + objects. + """ + response = kwargs.pop('response', None) + self.response = response + self.request = kwargs.pop('request', None) + if (response is not None and not self.request and + hasattr(response, 'request')): + self.request = self.response.request + super(NetworkConnectionError, self).__init__(*args, **kwargs) + + class InvalidWheelFilename(InstallationError): """Invalid wheel filename.""" diff --git a/src/pip/_internal/index/collector.py b/src/pip/_internal/index/collector.py index de2a7cb6f6d..068bad5ce50 100644 --- a/src/pip/_internal/index/collector.py +++ b/src/pip/_internal/index/collector.py @@ -13,12 +13,14 @@ from pip._vendor import html5lib, requests from pip._vendor.distlib.compat import unescape -from pip._vendor.requests.exceptions import HTTPError, RetryError, SSLError +from pip._vendor.requests.exceptions import RetryError, SSLError from pip._vendor.six.moves.urllib import parse as urllib_parse from pip._vendor.six.moves.urllib import request as urllib_request +from pip._internal.exceptions import NetworkConnectionError from pip._internal.models.link import Link from pip._internal.models.search_scope import SearchScope +from pip._internal.network.utils import raise_for_status from pip._internal.utils.filetypes import ARCHIVE_EXTENSIONS from pip._internal.utils.misc import pairwise, redact_auth_from_url from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -123,7 +125,7 @@ def _ensure_html_response(url, session): raise _NotHTTP() resp = session.head(url, allow_redirects=True) - resp.raise_for_status() + raise_for_status(resp) _ensure_html_header(resp) @@ -167,7 +169,7 @@ def _get_html_response(url, session): "Cache-Control": "max-age=0", }, ) - resp.raise_for_status() + raise_for_status(resp) # The check for archives above only works if the url ends with # something that looks like an archive. However that is not a @@ -462,7 +464,7 @@ def _get_html_page(link, session=None): 'The only supported Content-Type is text/html', link, exc.request_desc, exc.content_type, ) - except HTTPError as exc: + except NetworkConnectionError as exc: _handle_get_page_fail(link, exc) except RetryError as exc: _handle_get_page_fail(link, exc) diff --git a/src/pip/_internal/network/download.py b/src/pip/_internal/network/download.py index 7110c8ebdbf..24ab0c7b89c 100644 --- a/src/pip/_internal/network/download.py +++ b/src/pip/_internal/network/download.py @@ -5,13 +5,13 @@ import mimetypes import os -from pip._vendor import requests from pip._vendor.requests.models import CONTENT_CHUNK_SIZE from pip._internal.cli.progress_bars import DownloadProgressProvider +from pip._internal.exceptions import NetworkConnectionError from pip._internal.models.index import PyPI from pip._internal.network.cache import is_from_cache -from pip._internal.network.utils import HEADERS, response_chunks +from pip._internal.network.utils import HEADERS, raise_for_status, response_chunks from pip._internal.utils.misc import ( format_size, redact_auth_from_url, @@ -133,7 +133,7 @@ def _http_get_download(session, link): # type: (PipSession, Link) -> Response target_url = link.url.split('#', 1)[0] resp = session.get(target_url, headers=HEADERS, stream=True) - resp.raise_for_status() + raise_for_status(resp) return resp @@ -164,7 +164,7 @@ def __call__(self, link): # type: (Link) -> Download try: resp = _http_get_download(self._session, link) - except requests.HTTPError as e: + except NetworkConnectionError as e: logger.critical( "HTTP error %s while getting %s", e.response.status_code, link ) diff --git a/src/pip/_internal/network/utils.py b/src/pip/_internal/network/utils.py index 412a3ca1632..907b3fed49a 100644 --- a/src/pip/_internal/network/utils.py +++ b/src/pip/_internal/network/utils.py @@ -1,5 +1,6 @@ from pip._vendor.requests.models import CONTENT_CHUNK_SIZE, Response +from pip._internal.exceptions import NetworkConnectionError from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: @@ -27,6 +28,33 @@ HEADERS = {'Accept-Encoding': 'identity'} # type: Dict[str, str] +def raise_for_status(resp): + # type: (Response) -> None + http_error_msg = u'' + if isinstance(resp.reason, bytes): + # We attempt to decode utf-8 first because some servers + # choose to localize their reason strings. If the string + # isn't utf-8, we fall back to iso-8859-1 for all other + # encodings. + try: + reason = resp.reason.decode('utf-8') + except UnicodeDecodeError: + reason = resp.reason.decode('iso-8859-1') + else: + reason = resp.reason + + if 400 <= resp.status_code < 500: + http_error_msg = u'%s Client Error: %s for url: %s' % ( + resp.status_code, reason, resp.url) + + elif 500 <= resp.status_code < 600: + http_error_msg = u'%s Server Error: %s for url: %s' % ( + resp.status_code, reason, resp.url) + + if http_error_msg: + raise NetworkConnectionError(http_error_msg, response=resp) + + def response_chunks(response, chunk_size=CONTENT_CHUNK_SIZE): # type: (Response, int) -> Iterator[bytes] """Given a requests Response, provide the data chunks. diff --git a/src/pip/_internal/network/xmlrpc.py b/src/pip/_internal/network/xmlrpc.py index beab4fcfa7a..f3387fb5059 100644 --- a/src/pip/_internal/network/xmlrpc.py +++ b/src/pip/_internal/network/xmlrpc.py @@ -3,18 +3,20 @@ import logging -from pip._vendor import requests # 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._internal.exceptions import NetworkConnectionError +from pip._internal.network.utils import raise_for_status from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: from typing import Dict from pip._internal.network.session import PipSession + logger = logging.getLogger(__name__) @@ -38,10 +40,10 @@ def request(self, host, handler, request_body, verbose=False): headers = {'Content-Type': 'text/xml'} response = self._session.post(url, data=request_body, headers=headers, stream=True) - response.raise_for_status() + raise_for_status(response) self.verbose = verbose return self.parse_response(response.raw) - except requests.HTTPError as exc: + except NetworkConnectionError as exc: logger.critical( "HTTP error %s while getting %s", exc.response.status_code, url, diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index fbc5fea92ed..a5455fcc8e7 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -9,7 +9,6 @@ import os import shutil -from pip._vendor import requests from pip._vendor.six import PY2 from pip._internal.distributions import ( @@ -21,6 +20,7 @@ HashMismatch, HashUnpinned, InstallationError, + NetworkConnectionError, PreviousBuildDirError, VcsHashUnsupported, ) @@ -468,7 +468,7 @@ def prepare_linked_requirement(self, req, parallel_builds=False): link, req.source_dir, self.downloader, download_dir, hashes=self._get_linked_req_hashes(req) ) - except requests.HTTPError as exc: + except NetworkConnectionError as exc: raise InstallationError( 'Could not install requirement {} because of HTTP ' 'error {} for URL {}'.format(req, exc, link) diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index 3443050f69d..e120ad91b0f 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -18,6 +18,7 @@ RequirementsFileParseError, ) from pip._internal.models.search_scope import SearchScope +from pip._internal.network.utils import raise_for_status from pip._internal.utils.encoding import auto_decode from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.urls import get_url_scheme @@ -551,7 +552,7 @@ def get_file_content(url, session, comes_from=None): if scheme in ['http', 'https']: # FIXME: catch some errors resp = session.get(url) - resp.raise_for_status() + raise_for_status(resp) return resp.url, resp.text elif scheme == 'file': diff --git a/tests/lib/requests_mocks.py b/tests/lib/requests_mocks.py index 41c30eafd9c..c92d775c61d 100644 --- a/tests/lib/requests_mocks.py +++ b/tests/lib/requests_mocks.py @@ -31,9 +31,6 @@ def __init__(self, contents): self.headers = {'Content-Length': len(contents)} self.history = [] - def raise_for_status(self): - pass - class MockConnection(object): diff --git a/tests/unit/test_collector.py b/tests/unit/test_collector.py index c76f7b6409d..fa1057b640e 100644 --- a/tests/unit/test_collector.py +++ b/tests/unit/test_collector.py @@ -11,6 +11,7 @@ from pip._vendor import html5lib, requests from pip._vendor.six.moves.urllib import request as urllib_request +from pip._internal.exceptions import NetworkConnectionError from pip._internal.index.collector import ( HTMLPage, LinkCollector, @@ -55,7 +56,9 @@ def test_get_html_response_archive_to_naive_scheme(url): ("https://pypi.org/pip-18.0.tar.gz", "application/gzip"), ], ) -def test_get_html_response_archive_to_http_scheme(url, content_type): +@mock.patch("pip._internal.index.collector.raise_for_status") +def test_get_html_response_archive_to_http_scheme(mock_raise_for_status, url, + content_type): """ `_get_html_response()` should send a HEAD request on an archive-like URL if the scheme supports it, and raise `_NotHTML` if the response isn't HTML. @@ -72,6 +75,7 @@ def test_get_html_response_archive_to_http_scheme(url, content_type): session.assert_has_calls([ mock.call.head(url, allow_redirects=True), ]) + mock_raise_for_status.assert_called_once_with(session.head.return_value) assert ctx.value.args == (content_type, "HEAD") @@ -107,7 +111,10 @@ def test_get_html_page_invalid_content_type_archive(caplog, url): "https://pypi.org/pip-18.0.tar.gz", ], ) -def test_get_html_response_archive_to_http_scheme_is_html(url): +@mock.patch("pip._internal.index.collector.raise_for_status") +def test_get_html_response_archive_to_http_scheme_is_html( + mock_raise_for_status, url +): """ `_get_html_response()` should work with archive-like URLs if the HEAD request is responded with text/html. @@ -124,11 +131,13 @@ def test_get_html_response_archive_to_http_scheme_is_html(url): assert resp is not None assert session.mock_calls == [ mock.call.head(url, allow_redirects=True), - mock.call.head().raise_for_status(), mock.call.get(url, headers={ "Accept": "text/html", "Cache-Control": "max-age=0", }), - mock.call.get().raise_for_status(), + ] + assert mock_raise_for_status.mock_calls == [ + mock.call(session.head.return_value), + mock.call(resp) ] @@ -140,7 +149,8 @@ def test_get_html_response_archive_to_http_scheme_is_html(url): "https://python.org/sitemap.xml", ], ) -def test_get_html_response_no_head(url): +@mock.patch("pip._internal.index.collector.raise_for_status") +def test_get_html_response_no_head(mock_raise_for_status, url): """ `_get_html_response()` shouldn't send a HEAD request if the URL does not look like an archive, only the GET request that retrieves data. @@ -160,12 +170,14 @@ def test_get_html_response_no_head(url): mock.call(url, headers={ "Accept": "text/html", "Cache-Control": "max-age=0", }), - mock.call().raise_for_status(), mock.call().headers.get("Content-Type", ""), ] + mock_raise_for_status.assert_called_once_with(resp) -def test_get_html_response_dont_log_clear_text_password(caplog): +@mock.patch("pip._internal.index.collector.raise_for_status") +def test_get_html_response_dont_log_clear_text_password(mock_raise_for_status, + caplog): """ `_get_html_response()` should redact the password from the index URL in its DEBUG log message. @@ -184,6 +196,7 @@ def test_get_html_response_dont_log_clear_text_password(caplog): ) assert resp is not None + mock_raise_for_status.assert_called_once_with(resp) assert len(caplog.records) == 1 record = caplog.records[0] @@ -438,12 +451,13 @@ def test_parse_links_caches_same_page_by_url(): assert 'pkg2' in parsed_links_3[0].url -def test_request_http_error(caplog): +@mock.patch("pip._internal.index.collector.raise_for_status") +def test_request_http_error(mock_raise_for_status, caplog): caplog.set_level(logging.DEBUG) link = Link('http://localhost') session = Mock(PipSession) - session.get.return_value = resp = Mock() - resp.raise_for_status.side_effect = requests.HTTPError('Http error') + session.get.return_value = Mock() + mock_raise_for_status.side_effect = NetworkConnectionError('Http error') assert _get_html_page(link, session=session) is None assert ( 'Could not fetch URL http://localhost: Http error - skipping' @@ -510,7 +524,9 @@ def test_get_html_page_invalid_scheme(caplog, url, vcs_scheme): "application/json", ], ) -def test_get_html_page_invalid_content_type(caplog, content_type): +@mock.patch("pip._internal.index.collector.raise_for_status") +def test_get_html_page_invalid_content_type(mock_raise_for_status, + caplog, content_type): """`_get_html_page()` should warn if an invalid content-type is given. Only text/html is allowed. """ @@ -523,8 +539,8 @@ def test_get_html_page_invalid_content_type(caplog, content_type): "request.method": "GET", "headers": {"Content-Type": content_type}, }) - assert _get_html_page(link, session=session) is None + mock_raise_for_status.assert_called_once_with(session.get.return_value) assert ('pip._internal.index.collector', logging.WARNING, 'Skipping page {} because the GET request got Content-Type: {}.' diff --git a/tests/unit/test_network_utils.py b/tests/unit/test_network_utils.py new file mode 100644 index 00000000000..09f0684c5ee --- /dev/null +++ b/tests/unit/test_network_utils.py @@ -0,0 +1,34 @@ +import pytest + +from pip._internal.exceptions import NetworkConnectionError +from pip._internal.network.utils import raise_for_status +from tests.lib.requests_mocks import MockResponse + + +@pytest.mark.parametrize(("status_code", "error_type"), [ + (401, "Client Error"), + (501, "Server Error"), +]) +def test_raise_for_status_raises_exception(status_code, error_type): + contents = b'downloaded' + resp = MockResponse(contents) + resp.status_code = status_code + resp.url = "http://www.example.com/whatever.tgz" + resp.reason = "Network Error" + with pytest.raises(NetworkConnectionError) as exc: + raise_for_status(resp) + assert str(exc.info) == ( + "{} {}: Network Error for url:" + " http://www.example.com/whatever.tgz".format( + status_code, error_type) + ) + + +def test_raise_for_status_does_not_raises_exception(): + contents = b'downloaded' + resp = MockResponse(contents) + resp.status_code = 201 + resp.url = "http://www.example.com/whatever.tgz" + resp.reason = "No error" + return_value = raise_for_status(resp) + assert return_value is None diff --git a/tests/unit/test_operations_prepare.py b/tests/unit/test_operations_prepare.py index 0158eed5197..41d8be260d3 100644 --- a/tests/unit/test_operations_prepare.py +++ b/tests/unit/test_operations_prepare.py @@ -4,7 +4,7 @@ from tempfile import mkdtemp import pytest -from mock import Mock +from mock import Mock, patch from pip._internal.exceptions import HashMismatch from pip._internal.models.link import Link @@ -58,7 +58,9 @@ def _fake_session_get(*args, **kwargs): rmtree(temp_dir) -def test_download_http_url__no_directory_traversal(tmpdir): +@patch("pip._internal.network.download.raise_for_status") +def test_download_http_url__no_directory_traversal(mock_raise_for_status, + tmpdir): """ Test that directory traversal doesn't happen on download when the Content-Disposition header contains a filename with a ".." path part. @@ -90,6 +92,7 @@ def test_download_http_url__no_directory_traversal(tmpdir): # The file should be downloaded to download_dir. actual = os.listdir(download_dir) assert actual == ['out_dir_file'] + mock_raise_for_status.assert_called_once_with(resp) @pytest.fixture From ba44cc2e7272c8cc58fb7b6801ed40ae3d1330dc Mon Sep 17 00:00:00 2001 From: Nicole Harris <n.harris@kabucreative.com> Date: Wed, 8 Jul 2020 21:26:25 +0100 Subject: [PATCH 2225/3170] Add feature flag documentation --- docs/html/development/release-process.rst | 27 +++++++++++++++++++++++ news/8512.doc | 1 + 2 files changed, 28 insertions(+) create mode 100644 news/8512.doc diff --git a/docs/html/development/release-process.rst b/docs/html/development/release-process.rst index 11799e3c014..cc370a1d655 100644 --- a/docs/html/development/release-process.rst +++ b/docs/html/development/release-process.rst @@ -71,6 +71,33 @@ only bugs will be considered, and merged (subject to normal review processes). Note that there may be delays due to the lack of developer resources for reviewing such pull requests. +Feature Flags +============= + +``--use-deprecated`` +-------------------- + +Example: ``--use-deprecated=legacy-resolver`` + +Use for features that will be deprecated. Deprecated features should remain +available behind this flag for at least six months, as per the deprecation +policy. + +Features moved behind this flag should always include a warning that indicates +when the feature is scheduled to be removed. + +Once the feature is removed, user's who use the flag should be shown an error. + +``--use-feature`` +----------------- + +Example: ``--use-feature=2020-resolver`` + +Use for new features that users can test before they become pip's default +behaviour (e.g. alpha or beta releases). + +Once the feature becomes the default behaviour, this flag can remain in place, +but should issue a warning telling the user that it is no longer necessary. Release Process =============== diff --git a/news/8512.doc b/news/8512.doc new file mode 100644 index 00000000000..34630afc071 --- /dev/null +++ b/news/8512.doc @@ -0,0 +1 @@ +Add feature flags to docs From 64dd286d88edd5565505d49a8360a62f9766d633 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 5 Jul 2020 11:25:00 -0400 Subject: [PATCH 2226/3170] Don't unconditionally create destination directory Dropping the top-level directory creation allows us to make the processing completely dependent on files to be installed, and not on the top-level directory they happen to be installed in. We already create the parent directory in the loop below, so this call should be redundant for files that get installed. --- src/pip/_internal/operations/install/wheel.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index ba9748e366c..bf042093661 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -469,8 +469,6 @@ def clobber( filter=None # type: Optional[Callable[[text_type], bool]] ): # type: (...) -> None - ensure_dir(dest) # common for the 'include' path - for dir, subdirs, files in os.walk(source): basedir = dir[len(source):].lstrip(os.path.sep) destdir = os.path.join(dest, basedir) From 6b26ac911a44b8148a13c7e44f25849f4c648d89 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 5 Jul 2020 11:26:00 -0400 Subject: [PATCH 2227/3170] Derive parent directory from destination path By removing this dependency of the "file installation" part of `clobber` on the "file finding" part of `clobber`, we can more easily factor out the "file installation" part. --- src/pip/_internal/operations/install/wheel.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index bf042093661..66a81056d93 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -471,7 +471,6 @@ def clobber( # type: (...) -> None for dir, subdirs, files in os.walk(source): basedir = dir[len(source):].lstrip(os.path.sep) - destdir = os.path.join(dest, basedir) if is_base and basedir == '': subdirs[:] = [s for s in subdirs if not s.endswith('.data')] for f in files: @@ -483,7 +482,8 @@ def clobber( # directory creation is lazy and after the file filtering above # to ensure we don't install empty dirs; empty dirs can't be # uninstalled. - ensure_dir(destdir) + parent_dir = os.path.dirname(destfile) + ensure_dir(parent_dir) # copyfile (called below) truncates the destination if it # exists and then writes the new contents. This is fine in most From aa8dd9ceccc0b690e626ad747db213f0d0c1271d Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 5 Jul 2020 12:09:25 -0400 Subject: [PATCH 2228/3170] Add File class to represent a file to install Hiding the file-specific implementation we currently use will let us trade out the implementation for a zip-backed one later. We can also use this interface to represent the other kinds of files that we have to generate as part of wheel installation. We use a Protocol instead of a base class because there's no need for shared behavior right now, and using Protocol is less verbose. --- src/pip/_internal/operations/install/wheel.py | 101 +++++++++++------- 1 file changed, 61 insertions(+), 40 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 66a81056d93..97a0641d996 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -54,6 +54,7 @@ List, NewType, Optional, + Protocol, Sequence, Set, Tuple, @@ -69,6 +70,14 @@ RecordPath = NewType('RecordPath', text_type) InstalledCSVRow = Tuple[RecordPath, str, Union[int, str]] + class File(Protocol): + src_path = None # type: text_type + dest_path = None # type: text_type + + def save(self): + # type: () -> None + pass + logger = logging.getLogger(__name__) @@ -388,6 +397,54 @@ def get_console_script_specs(console): return scripts_to_generate +class DiskFile(object): + def __init__(self, src_path, dest_path): + # type: (text_type, text_type) -> None + self.src_path = src_path + self.dest_path = dest_path + + def save(self): + # type: () -> None + # directory creation is lazy and after file filtering + # to ensure we don't install empty dirs; empty dirs can't be + # uninstalled. + parent_dir = os.path.dirname(self.dest_path) + ensure_dir(parent_dir) + + # copyfile (called below) truncates the destination if it + # exists and then writes the new contents. This is fine in most + # cases, but can cause a segfault if pip has loaded a shared + # object (e.g. from pyopenssl through its vendored urllib3) + # Since the shared object is mmap'd an attempt to call a + # symbol in it will then cause a segfault. Unlinking the file + # allows writing of new contents while allowing the process to + # continue to use the old copy. + if os.path.exists(self.dest_path): + os.unlink(self.dest_path) + + # We use copyfile (not move, copy, or copy2) to be extra sure + # that we are not moving directories over (copyfile fails for + # directories) as well as to ensure that we are not copying + # over any metadata because we want more control over what + # metadata we actually copy over. + shutil.copyfile(self.src_path, self.dest_path) + + # Copy over the metadata for the file, currently this only + # includes the atime and mtime. + st = os.stat(self.src_path) + if hasattr(os, "utime"): + os.utime(self.dest_path, (st.st_atime, st.st_mtime)) + + # If our file is executable, then make our destination file + # executable. + if os.access(self.src_path, os.X_OK): + st = os.stat(self.src_path) + permissions = ( + st.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH + ) + os.chmod(self.dest_path, permissions) + + class MissingCallableSuffix(Exception): pass @@ -479,49 +536,13 @@ def clobber( continue srcfile = os.path.join(dir, f) destfile = os.path.join(dest, basedir, f) - # directory creation is lazy and after the file filtering above - # to ensure we don't install empty dirs; empty dirs can't be - # uninstalled. - parent_dir = os.path.dirname(destfile) - ensure_dir(parent_dir) - - # copyfile (called below) truncates the destination if it - # exists and then writes the new contents. This is fine in most - # cases, but can cause a segfault if pip has loaded a shared - # object (e.g. from pyopenssl through its vendored urllib3) - # Since the shared object is mmap'd an attempt to call a - # symbol in it will then cause a segfault. Unlinking the file - # allows writing of new contents while allowing the process to - # continue to use the old copy. - if os.path.exists(destfile): - os.unlink(destfile) - - # We use copyfile (not move, copy, or copy2) to be extra sure - # that we are not moving directories over (copyfile fails for - # directories) as well as to ensure that we are not copying - # over any metadata because we want more control over what - # metadata we actually copy over. - shutil.copyfile(srcfile, destfile) - - # Copy over the metadata for the file, currently this only - # includes the atime and mtime. - st = os.stat(srcfile) - if hasattr(os, "utime"): - os.utime(destfile, (st.st_atime, st.st_mtime)) - - # If our file is executable, then make our destination file - # executable. - if os.access(srcfile, os.X_OK): - st = os.stat(srcfile) - permissions = ( - st.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH - ) - os.chmod(destfile, permissions) + file = DiskFile(srcfile, destfile) + file.save() changed = False if fixer: - changed = fixer(destfile) - record_installed(srcfile, destfile, changed) + changed = fixer(file.dest_path) + record_installed(file.src_path, file.dest_path, changed) clobber( ensure_text(source, encoding=sys.getfilesystemencoding()), From bf45bd77be08c39189500e541b4e32dfa1758cff Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 5 Jul 2020 12:14:14 -0400 Subject: [PATCH 2229/3170] Extract "getting files" outside `clobber` "getting files" is one of the places that requires files to be on disk. By extracting this out of `clobber` we can make it simpler and then trade it out for a zip-based implementation. --- src/pip/_internal/operations/install/wheel.py | 40 +++++++++++-------- 1 file changed, 23 insertions(+), 17 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 97a0641d996..49dc009b3af 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -518,14 +518,13 @@ def record_installed(srcfile, destfile, modified=False): if modified: changed.add(_fs_to_record_path(destfile)) - def clobber( - source, # type: text_type - dest, # type: text_type - is_base, # type: bool - fixer=None, # type: Optional[Callable[[text_type], Any]] - filter=None # type: Optional[Callable[[text_type], bool]] + def files_to_process( + source, # type: text_type + dest, # type: text_type + is_base, # type: bool + filter=None, # type: Optional[Callable[[text_type], bool]] ): - # type: (...) -> None + # type: (...) -> Iterable[File] for dir, subdirs, files in os.walk(source): basedir = dir[len(source):].lstrip(os.path.sep) if is_base and basedir == '': @@ -536,19 +535,26 @@ def clobber( continue srcfile = os.path.join(dir, f) destfile = os.path.join(dest, basedir, f) - file = DiskFile(srcfile, destfile) - - file.save() - changed = False - if fixer: - changed = fixer(file.dest_path) - record_installed(file.src_path, file.dest_path, changed) + yield DiskFile(srcfile, destfile) - clobber( + def clobber( + files, # type: Iterable[File] + fixer=None, # type: Optional[Callable[[text_type], Any]] + ): + # type: (...) -> None + for file in files: + file.save() + changed = False + if fixer: + changed = fixer(file.dest_path) + record_installed(file.src_path, file.dest_path, changed) + + root_scheme_files = files_to_process( ensure_text(source, encoding=sys.getfilesystemencoding()), ensure_text(lib_dir, encoding=sys.getfilesystemencoding()), True, ) + clobber(root_scheme_files) # Get the defined entry points distribution = pkg_resources_distribution_for_wheel( @@ -585,15 +591,15 @@ def is_entrypoint_wrapper(name): filter = is_entrypoint_wrapper full_datadir_path = os.path.join(wheeldir, datadir, subdir) dest = getattr(scheme, subdir) - clobber( + data_scheme_files = files_to_process( ensure_text( full_datadir_path, encoding=sys.getfilesystemencoding() ), ensure_text(dest, encoding=sys.getfilesystemencoding()), False, - fixer=fixer, filter=filter, ) + clobber(data_scheme_files, fixer=fixer) def pyc_source_file_paths(): # type: () -> Iterator[text_type] From 07e8712677fc19ee63ac003ee9d25606c6117511 Mon Sep 17 00:00:00 2001 From: Prashant Sharma <prashantsharma161198@gmail.com> Date: Thu, 9 Jul 2020 12:44:21 +0530 Subject: [PATCH 2230/3170] reformat(pip/_internal): Resolve lint errors --- src/pip/_internal/exceptions.py | 19 +++++++++++++------ src/pip/_internal/network/download.py | 7 ++++++- src/pip/_internal/network/lazy_wheel.py | 8 ++++++-- src/pip/_internal/network/xmlrpc.py | 1 + tests/lib/requests_mocks.py | 1 + 5 files changed, 27 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/exceptions.py b/src/pip/_internal/exceptions.py index 0ac5534f1fd..3f26215d657 100644 --- a/src/pip/_internal/exceptions.py +++ b/src/pip/_internal/exceptions.py @@ -9,9 +9,10 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Any, Optional, List, Dict + from typing import Any, Optional, List, Dict, Text from pip._vendor.pkg_resources import Distribution + from pip._vendor.requests.models import Response, Request from pip._vendor.six import PY3 from pip._vendor.six.moves import configparser @@ -102,18 +103,24 @@ class PreviousBuildDirError(PipError): class NetworkConnectionError(PipError): """HTTP connection error""" - def __init__(self, *args, **kwargs): + def __init__(self, error_msg, response=None, request=None): + # type: (Text, Response, Request) -> None """ Initialize NetworkConnectionError with `request` and `response` objects. """ - response = kwargs.pop('response', None) self.response = response - self.request = kwargs.pop('request', None) - if (response is not None and not self.request and + self.request = request + self.error_msg = error_msg + if (self.response is not None and not self.request and hasattr(response, 'request')): self.request = self.response.request - super(NetworkConnectionError, self).__init__(*args, **kwargs) + super(NetworkConnectionError, self).__init__( + error_msg, response, request) + + def __str__(self): + # type: () -> str + return str(self.error_msg) class InvalidWheelFilename(InstallationError): diff --git a/src/pip/_internal/network/download.py b/src/pip/_internal/network/download.py index 24ab0c7b89c..44f9985a32b 100644 --- a/src/pip/_internal/network/download.py +++ b/src/pip/_internal/network/download.py @@ -11,7 +11,11 @@ from pip._internal.exceptions import NetworkConnectionError from pip._internal.models.index import PyPI from pip._internal.network.cache import is_from_cache -from pip._internal.network.utils import HEADERS, raise_for_status, response_chunks +from pip._internal.network.utils import ( + HEADERS, + raise_for_status, + response_chunks, +) from pip._internal.utils.misc import ( format_size, redact_auth_from_url, @@ -165,6 +169,7 @@ def __call__(self, link): try: resp = _http_get_download(self._session, link) except NetworkConnectionError as e: + assert e.response is not None logger.critical( "HTTP error %s while getting %s", e.response.status_code, link ) diff --git a/src/pip/_internal/network/lazy_wheel.py b/src/pip/_internal/network/lazy_wheel.py index d7b8bcc21ac..a8177034e2c 100644 --- a/src/pip/_internal/network/lazy_wheel.py +++ b/src/pip/_internal/network/lazy_wheel.py @@ -10,7 +10,11 @@ from pip._vendor.requests.models import CONTENT_CHUNK_SIZE from pip._vendor.six.moves import range -from pip._internal.network.utils import HEADERS, response_chunks +from pip._internal.network.utils import ( + HEADERS, + raise_for_status, + response_chunks, +) from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.wheel import pkg_resources_distribution_for_wheel @@ -51,7 +55,7 @@ class LazyZipOverHTTP(object): def __init__(self, url, session, chunk_size=CONTENT_CHUNK_SIZE): # type: (str, PipSession, int) -> None head = session.head(url, headers=HEADERS) - head.raise_for_status() + raise_for_status(head) assert head.status_code == 200 self._session, self._url, self._chunk_size = session, url, chunk_size self._length = int(head.headers['Content-Length']) diff --git a/src/pip/_internal/network/xmlrpc.py b/src/pip/_internal/network/xmlrpc.py index f3387fb5059..e61126241e8 100644 --- a/src/pip/_internal/network/xmlrpc.py +++ b/src/pip/_internal/network/xmlrpc.py @@ -44,6 +44,7 @@ def request(self, host, handler, request_body, verbose=False): self.verbose = verbose return self.parse_response(response.raw) except NetworkConnectionError as exc: + assert exc.response logger.critical( "HTTP error %s while getting %s", exc.response.status_code, url, diff --git a/tests/lib/requests_mocks.py b/tests/lib/requests_mocks.py index c92d775c61d..e8e3e9c886e 100644 --- a/tests/lib/requests_mocks.py +++ b/tests/lib/requests_mocks.py @@ -25,6 +25,7 @@ def __init__(self, contents): self.raw = FakeStream(contents) self.content = contents self.request = None + self.reason = None self.status_code = 200 self.connection = None self.url = None From 0ccbad8367df8f82dfaf27d9187f6df61868ec0a Mon Sep 17 00:00:00 2001 From: gutsytechster <prashantsharma161198@gmail.com> Date: Mon, 18 May 2020 20:01:06 +0530 Subject: [PATCH 2231/3170] feat(): Add logs for pip environment when installing --- news/3166.feature | 1 + src/pip/_internal/commands/install.py | 2 ++ tests/functional/test_install.py | 6 ++++++ 3 files changed, 9 insertions(+) create mode 100644 news/3166.feature diff --git a/news/3166.feature b/news/3166.feature new file mode 100644 index 00000000000..8abc2560cb6 --- /dev/null +++ b/news/3166.feature @@ -0,0 +1 @@ +Logs information about pip environment when installing a package. diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 54e998370bb..a155b944eca 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -27,6 +27,7 @@ from pip._internal.utils.misc import ( ensure_dir, get_installed_version, + get_pip_version, protect_pip_from_modification_on_windows, write_output, ) @@ -239,6 +240,7 @@ def run(self, options, args): install_options = options.install_options or [] + logger.debug("Using {}".format(get_pip_version())) options.use_user_site = decide_user_install( options.use_user_site, prefix_path=options.prefix_path, diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 2cff6ca2229..c4ceb03d352 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1902,3 +1902,9 @@ def test_install_verify_package_name_normalization(script, package_name): result = script.pip('install', package_name) assert 'Requirement already satisfied: {}'.format( package_name) in result.stdout + + +def test_install_logs_pip_version_in_debug(script): + result = script.pip('install', '-v', 'INITools==0.2') + pattern = "Using pip .* from .*" + assert_re_match(pattern, result.stdout) From 277b1e6c30fac469860b459248f7ce0d121aa68a Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 31 May 2020 23:00:32 +0530 Subject: [PATCH 2232/3170] Update news/3166.feature --- news/3166.feature | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/3166.feature b/news/3166.feature index 8abc2560cb6..1d8e049ffe8 100644 --- a/news/3166.feature +++ b/news/3166.feature @@ -1 +1 @@ -Logs information about pip environment when installing a package. +Log debugging information about pip, in ``pip install --verbose``. From 152642ddcf5840ae0b65ac3ccd231f654d8e14eb Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 26 Jun 2020 21:35:42 +0800 Subject: [PATCH 2233/3170] Ensure binary compat is checked in --target --- tests/functional/test_new_resolver_target.py | 71 ++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 tests/functional/test_new_resolver_target.py diff --git a/tests/functional/test_new_resolver_target.py b/tests/functional/test_new_resolver_target.py new file mode 100644 index 00000000000..6189e1cb5bc --- /dev/null +++ b/tests/functional/test_new_resolver_target.py @@ -0,0 +1,71 @@ +import pytest + +from pip._internal.cli.status_codes import ERROR, SUCCESS +from tests.lib.path import Path +from tests.lib.wheel import make_wheel + + +@pytest.fixture() +def make_fake_wheel(script): + + def _make_fake_wheel(wheel_tag): + wheel_house = script.scratch_path.joinpath("wheelhouse") + wheel_house.mkdir() + wheel_builder = make_wheel( + name="fake", + version="1.0", + wheel_metadata_updates={"Tag": []}, + ) + wheel_path = wheel_house.joinpath("fake-1.0-{}.whl".format(wheel_tag)) + wheel_builder.save_to(wheel_path) + return wheel_path + + return _make_fake_wheel + + +@pytest.mark.parametrize("implementation", [None, "fakepy"]) +@pytest.mark.parametrize("python_version", [None, "1"]) +@pytest.mark.parametrize("abi", [None, "fakeabi"]) +@pytest.mark.parametrize("platform", [None, "fakeplat"]) +def test_new_resolver_target_checks_compatibility_failure( + script, + make_fake_wheel, + implementation, + python_version, + abi, + platform, +): + fake_wheel_tag = "fakepy1-fakeabi-fakeplat" + args = [ + "install", "--use-feature=2020-resolver", + "--only-binary=:all:", + "--no-cache-dir", "--no-index", + "--target", str(script.scratch_path.joinpath("target")), + make_fake_wheel(fake_wheel_tag), + ] + if implementation: + args += ["--implementation", implementation] + if python_version: + args += ["--python-version", python_version] + if abi: + args += ["--abi", abi] + if platform: + args += ["--platform", platform] + + args_tag = "{}{}-{}-{}".format( + implementation, + python_version, + abi, + platform, + ) + wheel_tag_matches = (args_tag == fake_wheel_tag) + + result = script.pip(*args, expect_error=(not wheel_tag_matches)) + + dist_info = Path("scratch", "target", "fake-1.0.dist-info") + if wheel_tag_matches: + assert result.returncode == SUCCESS + result.did_create(dist_info) + else: + assert result.returncode == ERROR + result.did_not_create(dist_info) From dadac2ce036421998e70817c57f37b4d890536a6 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Thu, 9 Jul 2020 21:42:27 +0530 Subject: [PATCH 2234/3170] Add type annotations to pip._internal.cache --- news/558A4C36-D46C-428A-A746-62AE555D1FDE.trivial | 0 src/pip/_internal/cache.py | 8 +++----- 2 files changed, 3 insertions(+), 5 deletions(-) create mode 100644 news/558A4C36-D46C-428A-A746-62AE555D1FDE.trivial diff --git a/news/558A4C36-D46C-428A-A746-62AE555D1FDE.trivial b/news/558A4C36-D46C-428A-A746-62AE555D1FDE.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/cache.py b/src/pip/_internal/cache.py index b534f0cfec3..4a793b1f3e4 100644 --- a/src/pip/_internal/cache.py +++ b/src/pip/_internal/cache.py @@ -1,9 +1,6 @@ """Cache Management """ -# The following comment should be removed at some point in the future. -# mypy: strict-optional=False - import hashlib import json import logging @@ -122,7 +119,7 @@ def _get_cache_path_parts(self, link): return parts def _get_candidates(self, link, canonical_package_name): - # type: (Link, Optional[str]) -> List[Any] + # type: (Link, str) -> List[Any] can_not_cache = ( not self.cache_dir or not canonical_package_name or @@ -185,6 +182,7 @@ def __init__(self, cache_dir, format_control): def get_path_for_link_legacy(self, link): # type: (Link) -> str parts = self._get_cache_path_parts_legacy(link) + assert self.cache_dir return os.path.join(self.cache_dir, "wheels", *parts) def get_path_for_link(self, link): @@ -204,7 +202,7 @@ def get_path_for_link(self, link): :param link: The link of the sdist for which this will cache wheels. """ parts = self._get_cache_path_parts(link) - + assert self.cache_dir # Store wheels within the root cache_dir return os.path.join(self.cache_dir, "wheels", *parts) From 061d9fcdec07817a3dc1d8ad4c39ac1d605347f4 Mon Sep 17 00:00:00 2001 From: Nicole Harris <n.harris@kabucreative.com> Date: Thu, 9 Jul 2020 21:53:30 +0100 Subject: [PATCH 2235/3170] Update docs/html/development/release-process.rst Co-authored-by: Tzu-ping Chung <uranusjr@gmail.com> --- docs/html/development/release-process.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/html/development/release-process.rst b/docs/html/development/release-process.rst index cc370a1d655..8ccc5340336 100644 --- a/docs/html/development/release-process.rst +++ b/docs/html/development/release-process.rst @@ -86,7 +86,7 @@ policy. Features moved behind this flag should always include a warning that indicates when the feature is scheduled to be removed. -Once the feature is removed, user's who use the flag should be shown an error. +Once the feature is removed, users who use the flag should be shown an error. ``--use-feature`` ----------------- From 50f6f8d69b0a63bed9335cba43cfb73bd2fbd416 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 5 Jul 2020 12:27:54 -0400 Subject: [PATCH 2236/3170] Simplify return type for fix_script We always pass a file path to this function, so assert as much. We want the return type to be consistent so we can assign the result to non-Optional types. --- src/pip/_internal/operations/install/wheel.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 49dc009b3af..f3610404143 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -105,13 +105,12 @@ def csv_io_kwargs(mode): def fix_script(path): - # type: (text_type) -> Optional[bool] + # type: (text_type) -> bool """Replace #!python with #!/path/to/python Return True if file was changed. """ # XXX RECORD hashes will need to be updated - if not os.path.isfile(path): - return None + assert os.path.isfile(path) with open(path, 'rb') as script: firstline = script.readline() From f1e906d166ff1bf0ec6c6a1393491984c6233e8f Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 5 Jul 2020 12:35:13 -0400 Subject: [PATCH 2237/3170] Move script fixing into separate class This makes `clobber` much simpler, and aligns the interface of root_scheme files and data_scheme files, so we can process them in the same way. --- src/pip/_internal/operations/install/wheel.py | 30 +++++++++++++------ 1 file changed, 21 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index f3610404143..d2c1551c32e 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -23,6 +23,7 @@ from pip._vendor.distlib.scripts import ScriptMaker from pip._vendor.distlib.util import get_export_entry from pip._vendor.six import PY2, ensure_str, ensure_text, itervalues, text_type +from pip._vendor.six.moves import map from pip._internal.exceptions import InstallationError from pip._internal.locations import get_major_minor_version @@ -73,6 +74,7 @@ class File(Protocol): src_path = None # type: text_type dest_path = None # type: text_type + changed = None # type: bool def save(self): # type: () -> None @@ -401,6 +403,7 @@ def __init__(self, src_path, dest_path): # type: (text_type, text_type) -> None self.src_path = src_path self.dest_path = dest_path + self.changed = False def save(self): # type: () -> None @@ -444,6 +447,20 @@ def save(self): os.chmod(self.dest_path, permissions) +class ScriptFile(object): + def __init__(self, file): + # type: (File) -> None + self._file = file + self.src_path = self._file.src_path + self.dest_path = self._file.dest_path + self.changed = False + + def save(self): + # type: () -> None + self._file.save() + self.changed = fix_script(self.dest_path) + + class MissingCallableSuffix(Exception): pass @@ -538,15 +555,11 @@ def files_to_process( def clobber( files, # type: Iterable[File] - fixer=None, # type: Optional[Callable[[text_type], Any]] ): # type: (...) -> None for file in files: file.save() - changed = False - if fixer: - changed = fixer(file.dest_path) - record_installed(file.src_path, file.dest_path, changed) + record_installed(file.src_path, file.dest_path, file.changed) root_scheme_files = files_to_process( ensure_text(source, encoding=sys.getfilesystemencoding()), @@ -581,12 +594,9 @@ def is_entrypoint_wrapper(name): data_dirs = [s for s in subdirs if s.endswith('.data')] for datadir in data_dirs: - fixer = None filter = None for subdir in os.listdir(os.path.join(wheeldir, datadir)): - fixer = None if subdir == 'scripts': - fixer = fix_script filter = is_entrypoint_wrapper full_datadir_path = os.path.join(wheeldir, datadir, subdir) dest = getattr(scheme, subdir) @@ -598,7 +608,9 @@ def is_entrypoint_wrapper(name): False, filter=filter, ) - clobber(data_scheme_files, fixer=fixer) + if subdir == 'scripts': + data_scheme_files = map(ScriptFile, data_scheme_files) + clobber(data_scheme_files) def pyc_source_file_paths(): # type: () -> Iterator[text_type] From e8382871addaea7c8153f2425df7f4e9b169511c Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 5 Jul 2020 12:39:19 -0400 Subject: [PATCH 2238/3170] Filter files outside of file-finding function Simplifying the file-finding function will make it easier to drive our whole wheel installation from a single list of files later. --- src/pip/_internal/operations/install/wheel.py | 20 ++++++++----------- 1 file changed, 8 insertions(+), 12 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index d2c1551c32e..5741db56b91 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -23,7 +23,7 @@ from pip._vendor.distlib.scripts import ScriptMaker from pip._vendor.distlib.util import get_export_entry from pip._vendor.six import PY2, ensure_str, ensure_text, itervalues, text_type -from pip._vendor.six.moves import map +from pip._vendor.six.moves import filterfalse, map from pip._internal.exceptions import InstallationError from pip._internal.locations import get_major_minor_version @@ -47,7 +47,6 @@ from email.message import Message from typing import ( Any, - Callable, Dict, IO, Iterable, @@ -538,7 +537,6 @@ def files_to_process( source, # type: text_type dest, # type: text_type is_base, # type: bool - filter=None, # type: Optional[Callable[[text_type], bool]] ): # type: (...) -> Iterable[File] for dir, subdirs, files in os.walk(source): @@ -546,9 +544,6 @@ def files_to_process( if is_base and basedir == '': subdirs[:] = [s for s in subdirs if not s.endswith('.data')] for f in files: - # Skip unwanted files - if filter and filter(f): - continue srcfile = os.path.join(dir, f) destfile = os.path.join(dest, basedir, f) yield DiskFile(srcfile, destfile) @@ -574,10 +569,12 @@ def clobber( ) console, gui = get_entrypoints(distribution) - def is_entrypoint_wrapper(name): - # type: (text_type) -> bool + def is_entrypoint_wrapper(file): + # type: (File) -> bool # EP, EP.exe and EP-script.py are scripts generated for # entry point EP by setuptools + path = file.dest_path + name = os.path.basename(path) if name.lower().endswith('.exe'): matchname = name[:-4] elif name.lower().endswith('-script.py'): @@ -594,10 +591,7 @@ def is_entrypoint_wrapper(name): data_dirs = [s for s in subdirs if s.endswith('.data')] for datadir in data_dirs: - filter = None for subdir in os.listdir(os.path.join(wheeldir, datadir)): - if subdir == 'scripts': - filter = is_entrypoint_wrapper full_datadir_path = os.path.join(wheeldir, datadir, subdir) dest = getattr(scheme, subdir) data_scheme_files = files_to_process( @@ -606,9 +600,11 @@ def is_entrypoint_wrapper(name): ), ensure_text(dest, encoding=sys.getfilesystemencoding()), False, - filter=filter, ) if subdir == 'scripts': + data_scheme_files = filterfalse( + is_entrypoint_wrapper, data_scheme_files + ) data_scheme_files = map(ScriptFile, data_scheme_files) clobber(data_scheme_files) From f2239b548866fdfb185077ba9cc017f902df73f6 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 5 Jul 2020 12:49:22 -0400 Subject: [PATCH 2239/3170] Combine processing of root- and data-scheme files With this approach, we can add the rest of our generated files into the same iterable and they can undergo the same processing. --- src/pip/_internal/operations/install/wheel.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 5741db56b91..58134ea0aa4 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -16,7 +16,7 @@ import sys import warnings from base64 import urlsafe_b64encode -from itertools import starmap +from itertools import chain, starmap from zipfile import ZipFile from pip._vendor import pkg_resources @@ -556,12 +556,11 @@ def clobber( file.save() record_installed(file.src_path, file.dest_path, file.changed) - root_scheme_files = files_to_process( + files = files_to_process( ensure_text(source, encoding=sys.getfilesystemencoding()), ensure_text(lib_dir, encoding=sys.getfilesystemencoding()), True, ) - clobber(root_scheme_files) # Get the defined entry points distribution = pkg_resources_distribution_for_wheel( @@ -606,7 +605,10 @@ def is_entrypoint_wrapper(file): is_entrypoint_wrapper, data_scheme_files ) data_scheme_files = map(ScriptFile, data_scheme_files) - clobber(data_scheme_files) + + files = chain(files, data_scheme_files) + + clobber(files) def pyc_source_file_paths(): # type: () -> Iterator[text_type] From 239accb1b6adca8d4233cca4f2856eb845964922 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 5 Jul 2020 12:54:47 -0400 Subject: [PATCH 2240/3170] Inline clobber --- src/pip/_internal/operations/install/wheel.py | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 58134ea0aa4..eb9144041f1 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -548,14 +548,6 @@ def files_to_process( destfile = os.path.join(dest, basedir, f) yield DiskFile(srcfile, destfile) - def clobber( - files, # type: Iterable[File] - ): - # type: (...) -> None - for file in files: - file.save() - record_installed(file.src_path, file.dest_path, file.changed) - files = files_to_process( ensure_text(source, encoding=sys.getfilesystemencoding()), ensure_text(lib_dir, encoding=sys.getfilesystemencoding()), @@ -608,7 +600,9 @@ def is_entrypoint_wrapper(file): files = chain(files, data_scheme_files) - clobber(files) + for file in files: + file.save() + record_installed(file.src_path, file.dest_path, file.changed) def pyc_source_file_paths(): # type: () -> Iterator[text_type] From a3b977330a220c8ac83d649b84d6ddb8a9f7cab8 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 5 Jul 2020 14:42:04 -0400 Subject: [PATCH 2241/3170] Separate RECORD path from source file path When we start processing files directly from the wheel, all we will have are the files with their zip path (which should match a `RECORD` entry). Separating this from the source file path (used for copying) and annotating it with our `RecordPath` type makes it clear what the format of this public property is, and that it should match what is in `RECORD`. --- src/pip/_internal/operations/install/wheel.py | 38 +++++++++++-------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index eb9144041f1..2b99ed6c26a 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -71,7 +71,7 @@ InstalledCSVRow = Tuple[RecordPath, str, Union[int, str]] class File(Protocol): - src_path = None # type: text_type + src_record_path = None # type: RecordPath dest_path = None # type: text_type changed = None # type: bool @@ -398,10 +398,11 @@ def get_console_script_specs(console): class DiskFile(object): - def __init__(self, src_path, dest_path): - # type: (text_type, text_type) -> None - self.src_path = src_path + def __init__(self, src_record_path, dest_path, src_disk_path): + # type: (RecordPath, text_type, text_type) -> None + self.src_record_path = src_record_path self.dest_path = dest_path + self._src_disk_path = src_disk_path self.changed = False def save(self): @@ -428,18 +429,18 @@ def save(self): # directories) as well as to ensure that we are not copying # over any metadata because we want more control over what # metadata we actually copy over. - shutil.copyfile(self.src_path, self.dest_path) + shutil.copyfile(self._src_disk_path, self.dest_path) # Copy over the metadata for the file, currently this only # includes the atime and mtime. - st = os.stat(self.src_path) + st = os.stat(self._src_disk_path) if hasattr(os, "utime"): os.utime(self.dest_path, (st.st_atime, st.st_mtime)) # If our file is executable, then make our destination file # executable. - if os.access(self.src_path, os.X_OK): - st = os.stat(self.src_path) + if os.access(self._src_disk_path, os.X_OK): + st = os.stat(self._src_disk_path) permissions = ( st.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH ) @@ -450,7 +451,7 @@ class ScriptFile(object): def __init__(self, file): # type: (File) -> None self._file = file - self.src_path = self._file.src_path + self.src_record_path = self._file.src_record_path self.dest_path = self._file.dest_path self.changed = False @@ -525,11 +526,10 @@ def install_unpacked_wheel( generated = [] # type: List[str] def record_installed(srcfile, destfile, modified=False): - # type: (text_type, text_type, bool) -> None + # type: (RecordPath, text_type, bool) -> None """Map archive RECORD paths to installation RECORD paths.""" - oldpath = _fs_to_record_path(srcfile, wheeldir) newpath = _fs_to_record_path(destfile, lib_dir) - installed[oldpath] = newpath + installed[srcfile] = newpath if modified: changed.add(_fs_to_record_path(destfile)) @@ -544,9 +544,12 @@ def files_to_process( if is_base and basedir == '': subdirs[:] = [s for s in subdirs if not s.endswith('.data')] for f in files: - srcfile = os.path.join(dir, f) + srcfile = os.path.join(basedir, f).replace(os.path.sep, "/") destfile = os.path.join(dest, basedir, f) - yield DiskFile(srcfile, destfile) + src_disk_path = os.path.join(dir, f) + yield DiskFile( + cast('RecordPath', srcfile), destfile, src_disk_path + ) files = files_to_process( ensure_text(source, encoding=sys.getfilesystemencoding()), @@ -602,7 +605,7 @@ def is_entrypoint_wrapper(file): for file in files: file.save() - record_installed(file.src_path, file.dest_path, file.changed) + record_installed(file.src_record_path, file.dest_path, file.changed) def pyc_source_file_paths(): # type: () -> Iterator[text_type] @@ -647,7 +650,10 @@ def pyc_output_path(path): if success: pyc_path = pyc_output_path(path) assert os.path.exists(pyc_path) - record_installed(pyc_path, pyc_path) + record_installed( + _fs_to_record_path(pyc_path, wheeldir), + pyc_path, + ) logger.debug(stdout.getvalue()) maker = PipScriptMaker(None, scheme.scripts) From d241f0d30aa99e74442172d333cbf0f7375e8710 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 5 Jul 2020 19:19:04 -0400 Subject: [PATCH 2242/3170] Add partition from itertools recipes From https://docs.python.org/3/library/itertools.html, adapted for Python 2 and with types added. This will be used in the next commit. --- src/pip/_internal/utils/misc.py | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index dc482135e7e..24a7455628d 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -16,6 +16,7 @@ import stat import sys from collections import deque +from itertools import tee from pip._vendor import pkg_resources from pip._vendor.packaging.utils import canonicalize_name @@ -23,7 +24,7 @@ # why we ignore the type on this import. from pip._vendor.retrying import retry # type: ignore from pip._vendor.six import PY2, text_type -from pip._vendor.six.moves import input, map, zip_longest +from pip._vendor.six.moves import filter, filterfalse, input, map, zip_longest from pip._vendor.six.moves.urllib import parse as urllib_parse from pip._vendor.six.moves.urllib.parse import unquote as urllib_unquote @@ -53,12 +54,13 @@ if MYPY_CHECK_RUNNING: from typing import ( - Any, AnyStr, Container, Iterable, Iterator, List, Optional, Text, - Tuple, Union, + Any, AnyStr, Callable, Container, Iterable, Iterator, List, Optional, + Text, Tuple, TypeVar, Union, ) from pip._vendor.pkg_resources import Distribution VersionInfo = Tuple[int, int, int] + T = TypeVar("T") __all__ = ['rmtree', 'display_path', 'backup_dir', @@ -923,3 +925,18 @@ def pairwise(iterable): """ iterable = iter(iterable) return zip_longest(iterable, iterable) + + +def partition( + pred, # type: Callable[[T], bool] + iterable, # type: Iterable[T] +): + # type: (...) -> Tuple[Iterable[T], Iterable[T]] + """ + Use a predicate to partition entries into false entries and true entries, + like + + partition(is_odd, range(10)) --> 0 2 4 6 8 and 1 3 5 7 9 + """ + t1, t2 = tee(iterable) + return filterfalse(pred, t1), filter(pred, t2) From 8221aac105945ce075630819d792b8f177210e17 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 8 Jul 2020 20:17:20 -0400 Subject: [PATCH 2243/3170] Get paths and make files separately At the beginning of our wheel processing we are going to have the list of contained files. By splitting this into its own function, and deriving it from disk in the same way it will appear in the zip, we can incrementally refactor our approach using the same interface that will be available at that time. We start with the root-scheme paths (that end up in lib_dir) first. --- src/pip/_internal/operations/install/wheel.py | 42 +++++++++++++++++-- 1 file changed, 39 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 2b99ed6c26a..3a140be0b98 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -29,7 +29,12 @@ from pip._internal.locations import get_major_minor_version from pip._internal.models.direct_url import DIRECT_URL_METADATA_NAME, DirectUrl from pip._internal.utils.filesystem import adjacent_tmp_file, replace -from pip._internal.utils.misc import captured_stdout, ensure_dir, hash_file +from pip._internal.utils.misc import ( + captured_stdout, + ensure_dir, + hash_file, + partition, +) from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.unpacking import current_umask, unpack_file @@ -47,6 +52,7 @@ from email.message import Message from typing import ( Any, + Callable, Dict, IO, Iterable, @@ -551,11 +557,41 @@ def files_to_process( cast('RecordPath', srcfile), destfile, src_disk_path ) - files = files_to_process( + def all_paths(): + # type: () -> Iterable[RecordPath] + for dir, _subdirs, files in os.walk( + ensure_text(source, encoding="utf-8") + ): + basedir = dir[len(source):].lstrip(os.path.sep) + for f in files: + path = os.path.join(basedir, f).replace(os.path.sep, "/") + yield cast("RecordPath", path) + + def root_scheme_file_maker(source, dest): + # type: (text_type, text_type) -> Callable[[RecordPath], File] + def make_root_scheme_file(record_path): + # type: (RecordPath) -> File + normed_path = os.path.normpath(record_path) + source_disk_path = os.path.join(source, normed_path) + dest_path = os.path.join(dest, normed_path) + return DiskFile(record_path, dest_path, source_disk_path) + + return make_root_scheme_file + + def is_data_scheme_path(path): + # type: (RecordPath) -> bool + return path.split("/", 1)[0].endswith(".data") + + paths = all_paths() + root_scheme_paths, _data_scheme_paths = partition( + is_data_scheme_path, paths + ) + + make_root_scheme_file = root_scheme_file_maker( ensure_text(source, encoding=sys.getfilesystemencoding()), ensure_text(lib_dir, encoding=sys.getfilesystemencoding()), - True, ) + files = map(make_root_scheme_file, root_scheme_paths) # Get the defined entry points distribution = pkg_resources_distribution_for_wheel( From e0f95f12b6b2a6eb45a4e59e76bde0c03095b01d Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 8 Jul 2020 20:24:19 -0400 Subject: [PATCH 2244/3170] Get non-root scheme files from paths, not disk Now we rely solely on the list of RECORD-like paths derived from the filesystem, and can easily trade out the implementation for one that comes from the wheel file directly. --- src/pip/_internal/models/scheme.py | 5 +- src/pip/_internal/operations/install/wheel.py | 88 ++++++++++--------- 2 files changed, 51 insertions(+), 42 deletions(-) diff --git a/src/pip/_internal/models/scheme.py b/src/pip/_internal/models/scheme.py index b9d0ea68a41..5040551eb0e 100644 --- a/src/pip/_internal/models/scheme.py +++ b/src/pip/_internal/models/scheme.py @@ -6,12 +6,15 @@ """ +SCHEME_KEYS = ['platlib', 'purelib', 'headers', 'scripts', 'data'] + + class Scheme(object): """A Scheme holds paths which are used as the base directories for artifacts associated with a Python package. """ - __slots__ = ['platlib', 'purelib', 'headers', 'scripts', 'data'] + __slots__ = SCHEME_KEYS def __init__( self, diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 3a140be0b98..6dafbc1dc37 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -28,6 +28,7 @@ from pip._internal.exceptions import InstallationError from pip._internal.locations import get_major_minor_version from pip._internal.models.direct_url import DIRECT_URL_METADATA_NAME, DirectUrl +from pip._internal.models.scheme import SCHEME_KEYS from pip._internal.utils.filesystem import adjacent_tmp_file, replace from pip._internal.utils.misc import ( captured_stdout, @@ -539,24 +540,6 @@ def record_installed(srcfile, destfile, modified=False): if modified: changed.add(_fs_to_record_path(destfile)) - def files_to_process( - source, # type: text_type - dest, # type: text_type - is_base, # type: bool - ): - # type: (...) -> Iterable[File] - for dir, subdirs, files in os.walk(source): - basedir = dir[len(source):].lstrip(os.path.sep) - if is_base and basedir == '': - subdirs[:] = [s for s in subdirs if not s.endswith('.data')] - for f in files: - srcfile = os.path.join(basedir, f).replace(os.path.sep, "/") - destfile = os.path.join(dest, basedir, f) - src_disk_path = os.path.join(dir, f) - yield DiskFile( - cast('RecordPath', srcfile), destfile, src_disk_path - ) - def all_paths(): # type: () -> Iterable[RecordPath] for dir, _subdirs, files in os.walk( @@ -578,12 +561,32 @@ def make_root_scheme_file(record_path): return make_root_scheme_file + def data_scheme_file_maker(source, scheme): + # type: (text_type, Scheme) -> Callable[[RecordPath], File] + scheme_paths = {} + for key in SCHEME_KEYS: + encoded_key = ensure_text(key) + scheme_paths[encoded_key] = ensure_text( + getattr(scheme, key), encoding=sys.getfilesystemencoding() + ) + + def make_data_scheme_file(record_path): + # type: (RecordPath) -> File + normed_path = os.path.normpath(record_path) + source_disk_path = os.path.join(source, normed_path) + _, scheme_key, dest_subpath = normed_path.split(os.path.sep, 2) + scheme_path = scheme_paths[scheme_key] + dest_path = os.path.join(scheme_path, dest_subpath) + return DiskFile(record_path, dest_path, source_disk_path) + + return make_data_scheme_file + def is_data_scheme_path(path): # type: (RecordPath) -> bool return path.split("/", 1)[0].endswith(".data") paths = all_paths() - root_scheme_paths, _data_scheme_paths = partition( + root_scheme_paths, data_scheme_paths = partition( is_data_scheme_path, paths ) @@ -593,6 +596,25 @@ def is_data_scheme_path(path): ) files = map(make_root_scheme_file, root_scheme_paths) + def is_script_scheme_path(path): + # type: (RecordPath) -> bool + parts = path.split("/", 2) + return ( + len(parts) > 2 and + parts[0].endswith(".data") and + parts[1] == "scripts" + ) + + other_scheme_paths, script_scheme_paths = partition( + is_script_scheme_path, data_scheme_paths + ) + + make_data_scheme_file = data_scheme_file_maker( + ensure_text(source, encoding=sys.getfilesystemencoding()), scheme + ) + other_scheme_files = map(make_data_scheme_file, other_scheme_paths) + files = chain(files, other_scheme_files) + # Get the defined entry points distribution = pkg_resources_distribution_for_wheel( wheel_zip, name, wheel_path @@ -616,28 +638,12 @@ def is_entrypoint_wrapper(file): # Ignore setuptools-generated scripts return (matchname in console or matchname in gui) - # Zip file path separators must be / - subdirs = set(p.split("/", 1)[0] for p in wheel_zip.namelist()) - data_dirs = [s for s in subdirs if s.endswith('.data')] - - for datadir in data_dirs: - for subdir in os.listdir(os.path.join(wheeldir, datadir)): - full_datadir_path = os.path.join(wheeldir, datadir, subdir) - dest = getattr(scheme, subdir) - data_scheme_files = files_to_process( - ensure_text( - full_datadir_path, encoding=sys.getfilesystemencoding() - ), - ensure_text(dest, encoding=sys.getfilesystemencoding()), - False, - ) - if subdir == 'scripts': - data_scheme_files = filterfalse( - is_entrypoint_wrapper, data_scheme_files - ) - data_scheme_files = map(ScriptFile, data_scheme_files) - - files = chain(files, data_scheme_files) + script_scheme_files = map(make_data_scheme_file, script_scheme_paths) + script_scheme_files = filterfalse( + is_entrypoint_wrapper, script_scheme_files + ) + script_scheme_files = map(ScriptFile, script_scheme_files) + files = chain(files, script_scheme_files) for file in files: file.save() From f9432790deea6790093dd0af3aa6e724e200fe74 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 8 Jul 2020 20:25:00 -0400 Subject: [PATCH 2245/3170] Get list of files directly from wheel One less dependency on the wheel being extracted. --- src/pip/_internal/operations/install/wheel.py | 20 +++++++++++-------- 1 file changed, 12 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 6dafbc1dc37..94a6fda417e 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -542,13 +542,16 @@ def record_installed(srcfile, destfile, modified=False): def all_paths(): # type: () -> Iterable[RecordPath] - for dir, _subdirs, files in os.walk( - ensure_text(source, encoding="utf-8") - ): - basedir = dir[len(source):].lstrip(os.path.sep) - for f in files: - path = os.path.join(basedir, f).replace(os.path.sep, "/") - yield cast("RecordPath", path) + names = wheel_zip.namelist() + # If a flag is set, names may be unicode in Python 2. We convert to + # text explicitly so these are valid for lookup in RECORD. + decoded_names = map(ensure_text, names) + for name in decoded_names: + yield cast("RecordPath", name) + + def is_dir_path(path): + # type: (RecordPath) -> bool + return path.endswith("/") def root_scheme_file_maker(source, dest): # type: (text_type, text_type) -> Callable[[RecordPath], File] @@ -586,8 +589,9 @@ def is_data_scheme_path(path): return path.split("/", 1)[0].endswith(".data") paths = all_paths() + file_paths = filterfalse(is_dir_path, paths) root_scheme_paths, data_scheme_paths = partition( - is_data_scheme_path, paths + is_data_scheme_path, file_paths ) make_root_scheme_file = root_scheme_file_maker( From 4bdb8bcd7e57fecf68d07bc99140c8a94a788374 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 8 Jul 2020 20:26:00 -0400 Subject: [PATCH 2246/3170] Extract files directly from wheel --- src/pip/_internal/operations/install/wheel.py | 70 +++++++------------ src/pip/_internal/utils/unpacking.py | 14 ++-- 2 files changed, 37 insertions(+), 47 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 94a6fda417e..4549e0ebb12 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -12,7 +12,6 @@ import os.path import re import shutil -import stat import sys import warnings from base64 import urlsafe_b64encode @@ -38,7 +37,12 @@ ) from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from pip._internal.utils.unpacking import current_umask, unpack_file +from pip._internal.utils.unpacking import ( + current_umask, + set_extracted_file_to_default_mode_plus_executable, + unpack_file, + zip_item_is_executable, +) from pip._internal.utils.wheel import ( parse_wheel, pkg_resources_distribution_for_wheel, @@ -404,12 +408,12 @@ def get_console_script_specs(console): return scripts_to_generate -class DiskFile(object): - def __init__(self, src_record_path, dest_path, src_disk_path): - # type: (RecordPath, text_type, text_type) -> None +class ZipBackedFile(object): + def __init__(self, src_record_path, dest_path, zip_file): + # type: (RecordPath, text_type, ZipFile) -> None self.src_record_path = src_record_path self.dest_path = dest_path - self._src_disk_path = src_disk_path + self._zip_file = zip_file self.changed = False def save(self): @@ -420,8 +424,8 @@ def save(self): parent_dir = os.path.dirname(self.dest_path) ensure_dir(parent_dir) - # copyfile (called below) truncates the destination if it - # exists and then writes the new contents. This is fine in most + # When we open the output file below, any existing file is truncated + # before we start writing the new contents. This is fine in most # cases, but can cause a segfault if pip has loaded a shared # object (e.g. from pyopenssl through its vendored urllib3) # Since the shared object is mmap'd an attempt to call a @@ -431,27 +435,13 @@ def save(self): if os.path.exists(self.dest_path): os.unlink(self.dest_path) - # We use copyfile (not move, copy, or copy2) to be extra sure - # that we are not moving directories over (copyfile fails for - # directories) as well as to ensure that we are not copying - # over any metadata because we want more control over what - # metadata we actually copy over. - shutil.copyfile(self._src_disk_path, self.dest_path) - - # Copy over the metadata for the file, currently this only - # includes the atime and mtime. - st = os.stat(self._src_disk_path) - if hasattr(os, "utime"): - os.utime(self.dest_path, (st.st_atime, st.st_mtime)) - - # If our file is executable, then make our destination file - # executable. - if os.access(self._src_disk_path, os.X_OK): - st = os.stat(self._src_disk_path) - permissions = ( - st.st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH - ) - os.chmod(self.dest_path, permissions) + with self._zip_file.open(self.src_record_path) as f: + with open(self.dest_path, "wb") as dest: + shutil.copyfileobj(f, dest) + + zipinfo = self._zip_file.getinfo(self.src_record_path) + if zip_item_is_executable(zipinfo): + set_extracted_file_to_default_mode_plus_executable(self.dest_path) class ScriptFile(object): @@ -515,8 +505,6 @@ def install_unpacked_wheel( Wheel-Version * when the .dist-info dir does not match the wheel """ - source = wheeldir.rstrip(os.path.sep) + os.path.sep - info_dir, metadata = parse_wheel(wheel_zip, name) if wheel_root_is_purelib(metadata): @@ -553,19 +541,18 @@ def is_dir_path(path): # type: (RecordPath) -> bool return path.endswith("/") - def root_scheme_file_maker(source, dest): - # type: (text_type, text_type) -> Callable[[RecordPath], File] + def root_scheme_file_maker(zip_file, dest): + # type: (ZipFile, text_type) -> Callable[[RecordPath], File] def make_root_scheme_file(record_path): # type: (RecordPath) -> File normed_path = os.path.normpath(record_path) - source_disk_path = os.path.join(source, normed_path) dest_path = os.path.join(dest, normed_path) - return DiskFile(record_path, dest_path, source_disk_path) + return ZipBackedFile(record_path, dest_path, zip_file) return make_root_scheme_file - def data_scheme_file_maker(source, scheme): - # type: (text_type, Scheme) -> Callable[[RecordPath], File] + def data_scheme_file_maker(zip_file, scheme): + # type: (ZipFile, Scheme) -> Callable[[RecordPath], File] scheme_paths = {} for key in SCHEME_KEYS: encoded_key = ensure_text(key) @@ -576,11 +563,10 @@ def data_scheme_file_maker(source, scheme): def make_data_scheme_file(record_path): # type: (RecordPath) -> File normed_path = os.path.normpath(record_path) - source_disk_path = os.path.join(source, normed_path) _, scheme_key, dest_subpath = normed_path.split(os.path.sep, 2) scheme_path = scheme_paths[scheme_key] dest_path = os.path.join(scheme_path, dest_subpath) - return DiskFile(record_path, dest_path, source_disk_path) + return ZipBackedFile(record_path, dest_path, zip_file) return make_data_scheme_file @@ -595,7 +581,7 @@ def is_data_scheme_path(path): ) make_root_scheme_file = root_scheme_file_maker( - ensure_text(source, encoding=sys.getfilesystemencoding()), + wheel_zip, ensure_text(lib_dir, encoding=sys.getfilesystemencoding()), ) files = map(make_root_scheme_file, root_scheme_paths) @@ -613,9 +599,7 @@ def is_script_scheme_path(path): is_script_scheme_path, data_scheme_paths ) - make_data_scheme_file = data_scheme_file_maker( - ensure_text(source, encoding=sys.getfilesystemencoding()), scheme - ) + make_data_scheme_file = data_scheme_file_maker(wheel_zip, scheme) other_scheme_files = map(make_data_scheme_file, other_scheme_paths) files = chain(files, other_scheme_files) diff --git a/src/pip/_internal/utils/unpacking.py b/src/pip/_internal/utils/unpacking.py index 8966052d903..620f31ebb74 100644 --- a/src/pip/_internal/utils/unpacking.py +++ b/src/pip/_internal/utils/unpacking.py @@ -22,6 +22,7 @@ if MYPY_CHECK_RUNNING: from typing import Iterable, List, Optional, Text, Union + from zipfile import ZipInfo logger = logging.getLogger(__name__) @@ -104,6 +105,14 @@ def set_extracted_file_to_default_mode_plus_executable(path): os.chmod(path, (0o777 & ~current_umask() | 0o111)) +def zip_item_is_executable(info): + # type: (ZipInfo) -> bool + mode = info.external_attr >> 16 + # if mode and regular file and any execute permissions for + # user/group/world? + return bool(mode and stat.S_ISREG(mode) and mode & 0o111) + + def unzip_file(filename, location, flatten=True): # type: (str, str, bool) -> None """ @@ -145,10 +154,7 @@ def unzip_file(filename, location, flatten=True): shutil.copyfileobj(fp, destfp) finally: fp.close() - mode = info.external_attr >> 16 - # if mode and regular file and any execute permissions for - # user/group/world? - if mode and stat.S_ISREG(mode) and mode & 0o111: + if zip_item_is_executable(info): set_extracted_file_to_default_mode_plus_executable(fn) finally: zipfp.close() From df92f250792477ed69a391f2012b0d8d40d3eec3 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 8 Jul 2020 20:28:00 -0400 Subject: [PATCH 2247/3170] Don't use wheeldir for pyc record paths --- src/pip/_internal/operations/install/wheel.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 4549e0ebb12..1e2a7359519 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -680,10 +680,10 @@ def pyc_output_path(path): if success: pyc_path = pyc_output_path(path) assert os.path.exists(pyc_path) - record_installed( - _fs_to_record_path(pyc_path, wheeldir), - pyc_path, + pyc_record_path = cast( + "RecordPath", pyc_path.replace(os.path.sep, "/") ) + record_installed(pyc_record_path, pyc_path) logger.debug(stdout.getvalue()) maker = PipScriptMaker(None, scheme.scripts) From 483213a318a02cfc7f33ce420e9e97b416730cb7 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 8 Jul 2020 20:29:00 -0400 Subject: [PATCH 2248/3170] Drop unused wheeldir parameter --- src/pip/_internal/operations/install/wheel.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 1e2a7359519..acdf1c8e845 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -478,7 +478,6 @@ def make(self, specification, options=None): def install_unpacked_wheel( name, # type: str - wheeldir, # type: str wheel_zip, # type: ZipFile wheel_path, # type: str scheme, # type: Scheme @@ -492,7 +491,6 @@ def install_unpacked_wheel( """Install a wheel. :param name: Name of the project to install - :param wheeldir: Base directory of the unpacked wheel :param wheel_zip: open ZipFile for wheel being installed :param scheme: Distutils scheme dictating the install directories :param req_description: String used in place of the requirement, for @@ -800,7 +798,6 @@ def install_wheel( unpack_file(wheel_path, unpacked_dir.path) install_unpacked_wheel( name=name, - wheeldir=unpacked_dir.path, wheel_zip=z, wheel_path=wheel_path, scheme=scheme, From 4605b32c49409c190b6e85b8c248d9f4097d5f50 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 8 Jul 2020 20:30:00 -0400 Subject: [PATCH 2249/3170] Don't unpack wheel before installing --- src/pip/_internal/operations/install/wheel.py | 11 +++-------- 1 file changed, 3 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index acdf1c8e845..2a475e18059 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -35,12 +35,10 @@ hash_file, partition, ) -from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.unpacking import ( current_umask, set_extracted_file_to_default_mode_plus_executable, - unpack_file, zip_item_is_executable, ) from pip._internal.utils.wheel import ( @@ -476,7 +474,7 @@ def make(self, specification, options=None): return super(PipScriptMaker, self).make(specification, options) -def install_unpacked_wheel( +def _install_wheel( name, # type: str wheel_zip, # type: ZipFile wheel_path, # type: str @@ -792,11 +790,8 @@ def install_wheel( requested=False, # type: bool ): # type: (...) -> None - with TempDirectory( - kind="unpacked-wheel" - ) as unpacked_dir, ZipFile(wheel_path, allowZip64=True) as z: - unpack_file(wheel_path, unpacked_dir.path) - install_unpacked_wheel( + with ZipFile(wheel_path, allowZip64=True) as z: + _install_wheel( name=name, wheel_zip=z, wheel_path=wheel_path, From 80a2a949557d643accc42f5884b1ff80d18b89b9 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 8 Jul 2020 21:08:41 -0400 Subject: [PATCH 2250/3170] Add news --- news/6030.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/6030.feature diff --git a/news/6030.feature b/news/6030.feature new file mode 100644 index 00000000000..176eb903b37 --- /dev/null +++ b/news/6030.feature @@ -0,0 +1 @@ +Install wheel files directly instead of extracting them to a temp directory. From d13ec253613eee2a7695fff6f82ac544d6bfe031 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Thu, 9 Jul 2020 21:13:44 -0400 Subject: [PATCH 2251/3170] Prevent path traversal when installing wheels directly --- src/pip/_internal/operations/install/wheel.py | 14 ++++++++++++ tests/unit/test_wheel.py | 22 +++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 2a475e18059..3d841d1a0fd 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -38,6 +38,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.unpacking import ( current_umask, + is_within_directory, set_extracted_file_to_default_mode_plus_executable, zip_item_is_executable, ) @@ -537,12 +538,24 @@ def is_dir_path(path): # type: (RecordPath) -> bool return path.endswith("/") + def assert_no_path_traversal(dest_dir_path, target_path): + # type: (text_type, text_type) -> None + if not is_within_directory(dest_dir_path, target_path): + message = ( + "The wheel {!r} has a file {!r} trying to install" + " outside the target directory {!r}" + ) + raise InstallationError( + message.format(wheel_path, target_path, dest_dir_path) + ) + def root_scheme_file_maker(zip_file, dest): # type: (ZipFile, text_type) -> Callable[[RecordPath], File] def make_root_scheme_file(record_path): # type: (RecordPath) -> File normed_path = os.path.normpath(record_path) dest_path = os.path.join(dest, normed_path) + assert_no_path_traversal(dest, dest_path) return ZipBackedFile(record_path, dest_path, zip_file) return make_root_scheme_file @@ -562,6 +575,7 @@ def make_data_scheme_file(record_path): _, scheme_key, dest_subpath = normed_path.split(os.path.sep, 2) scheme_path = scheme_paths[scheme_key] dest_path = os.path.join(scheme_path, dest_subpath) + assert_no_path_traversal(scheme_path, dest_path) return ZipBackedFile(record_path, dest_path, zip_file) return make_data_scheme_file diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index e167cb6e1e0..421f5b95899 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -11,6 +11,7 @@ from mock import patch from pip._vendor.packaging.requirements import Requirement +from pip._internal.exceptions import InstallationError from pip._internal.locations import get_scheme from pip._internal.models.direct_url import ( DIRECT_URL_METADATA_NAME, @@ -458,6 +459,27 @@ def test_dist_info_contains_empty_dir(self, data, tmpdir): assert not os.path.isdir( os.path.join(self.dest_dist_info, 'empty_dir')) + @pytest.mark.parametrize( + "path", + ["/tmp/example", "../example", "./../example"] + ) + def test_wheel_install_rejects_bad_paths(self, data, tmpdir, path): + self.prep(data, tmpdir) + wheel_path = make_wheel( + "simple", "0.1.0", extra_files={path: "example contents\n"} + ).save_to_dir(tmpdir) + with pytest.raises(InstallationError) as e: + wheel.install_wheel( + "simple", + str(wheel_path), + scheme=self.scheme, + req_description="simple", + ) + + exc_text = str(e.value) + assert os.path.basename(wheel_path) in exc_text + assert "example" in exc_text + class TestMessageAboutScriptsNotOnPATH(object): From f7abe1f874406c205c6f5a026a9b6ad28756d040 Mon Sep 17 00:00:00 2001 From: Prashant Sharma <prashantsharma161198@gmail.com> Date: Fri, 10 Jul 2020 16:20:30 +0530 Subject: [PATCH 2252/3170] test(functional/test_install): Use shared_data for installing package --- tests/functional/test_install.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index c4ceb03d352..51c8dab4a6e 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1904,7 +1904,8 @@ def test_install_verify_package_name_normalization(script, package_name): package_name) in result.stdout -def test_install_logs_pip_version_in_debug(script): - result = script.pip('install', '-v', 'INITools==0.2') +def test_install_logs_pip_version_in_debug(script, shared_data): + fake_package = shared_data.packages / 'simple-2.0.tar.gz' + result = script.pip('install', '-v', fake_package) pattern = "Using pip .* from .*" assert_re_match(pattern, result.stdout) From 4cc731c62b2e69254b96b7edb4844224f246e9e4 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Thu, 9 Jul 2020 21:51:42 +0530 Subject: [PATCH 2253/3170] Add type annotations to pip._internal.configuration --- news/333A6FF6-6699-4D50-B54F-716A8F0984A3.trivial | 0 src/pip/_internal/configuration.py | 8 ++++---- 2 files changed, 4 insertions(+), 4 deletions(-) create mode 100644 news/333A6FF6-6699-4D50-B54F-716A8F0984A3.trivial diff --git a/news/333A6FF6-6699-4D50-B54F-716A8F0984A3.trivial b/news/333A6FF6-6699-4D50-B54F-716A8F0984A3.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/configuration.py b/src/pip/_internal/configuration.py index 0b01ee38738..70cc80c68d8 100644 --- a/src/pip/_internal/configuration.py +++ b/src/pip/_internal/configuration.py @@ -11,9 +11,6 @@ A single word describing where the configuration key-value pair came from """ -# The following comment should be removed at some point in the future. -# mypy: strict-optional=False - import locale import logging import os @@ -185,6 +182,7 @@ def set_value(self, key, value): """ self._ensure_have_load_only() + assert self.load_only fname, parser = self._get_parser_to_modify() if parser is not None: @@ -204,6 +202,7 @@ def unset_value(self, key): """ self._ensure_have_load_only() + assert self.load_only if key not in self._config[self.load_only]: raise ConfigurationError("No such key - {}".format(key)) @@ -222,7 +221,7 @@ def unset_value(self, key): # name removed from parser, section may now be empty section_iter = iter(parser.items(section)) try: - val = next(section_iter) + val = next(section_iter) # type: Optional[Tuple[str, str]] except StopIteration: val = None @@ -409,6 +408,7 @@ def get_values_in_config(self, variant): def _get_parser_to_modify(self): # type: () -> Tuple[str, RawConfigParser] # Determine which parser to modify + assert self.load_only parsers = self._parsers[self.load_only] if not parsers: # This should not happen if everything works correctly. From 135e9c13698b51716d7fb8a641ee301237ffab63 Mon Sep 17 00:00:00 2001 From: Avinash Karhana <avinashkarhana1@gmail.com> Date: Sat, 11 Jul 2020 02:15:29 +0530 Subject: [PATCH 2254/3170] Fixed useless `else` cluase after loop inside `get_version` function in setup.py --- news/440785C8-AC99-4ED9-8ECE-7C92B4358026.trivial | 0 setup.py | 3 +-- 2 files changed, 1 insertion(+), 2 deletions(-) create mode 100644 news/440785C8-AC99-4ED9-8ECE-7C92B4358026.trivial diff --git a/news/440785C8-AC99-4ED9-8ECE-7C92B4358026.trivial b/news/440785C8-AC99-4ED9-8ECE-7C92B4358026.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/setup.py b/setup.py index f731b61e890..0557690deca 100644 --- a/setup.py +++ b/setup.py @@ -22,8 +22,7 @@ def get_version(rel_path): # __version__ = "0.9" delim = '"' if '"' in line else "'" return line.split(delim)[1] - else: - raise RuntimeError("Unable to find version string.") + raise RuntimeError("Unable to find version string.") long_description = read('README.rst') From c5e19c01c46e4402ce3f7b24f0b17e2bcf2f20bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Sat, 11 Jul 2020 15:56:34 +0700 Subject: [PATCH 2255/3170] Clean up Configuration.unset_value and nit __init__ --- ...65c738-859f-40aa-be74-97a55177ee40.trivial | 0 src/pip/_internal/configuration.py | 37 ++++++------------- 2 files changed, 12 insertions(+), 25 deletions(-) create mode 100644 news/3565c738-859f-40aa-be74-97a55177ee40.trivial diff --git a/news/3565c738-859f-40aa-be74-97a55177ee40.trivial b/news/3565c738-859f-40aa-be74-97a55177ee40.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/configuration.py b/src/pip/_internal/configuration.py index 70cc80c68d8..e49a5f4f5bf 100644 --- a/src/pip/_internal/configuration.py +++ b/src/pip/_internal/configuration.py @@ -111,7 +111,7 @@ class Configuration(object): """ def __init__(self, isolated, load_only=None): - # type: (bool, Kind) -> None + # type: (bool, Optional[Kind]) -> None super(Configuration, self).__init__() _valid_load_only = [kinds.USER, kinds.GLOBAL, kinds.SITE, None] @@ -121,8 +121,8 @@ def __init__(self, isolated, load_only=None): ", ".join(map(repr, _valid_load_only[:-1])) ) ) - self.isolated = isolated # type: bool - self.load_only = load_only # type: Optional[Kind] + self.isolated = isolated + self.load_only = load_only # The order here determines the override order. self._override_order = [ @@ -198,8 +198,7 @@ def set_value(self, key, value): def unset_value(self, key): # type: (str) -> None - """Unset a value in the configuration. - """ + """Unset a value in the configuration.""" self._ensure_have_load_only() assert self.load_only @@ -210,30 +209,18 @@ def unset_value(self, key): 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 = next(section_iter) # type: Optional[Tuple[str, str]] - except StopIteration: - val = None - - if val is None: - parser.remove_section(section) - - self._mark_as_modified(fname, parser) - else: + if not (parser.has_section(section) + and parser.remove_option(section, name)): + # The option was not removed. raise ConfigurationError( "Fatal Internal error [id=1]. Please report as a bug." ) + # The section may be empty after the option was removed. + if not parser.items(section): + parser.remove_section(section) + self._mark_as_modified(fname, parser) + del self._config[self.load_only][key] def save(self): From f48c44e203432812ad196022b1bdbcff4572637b Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 11 Jul 2020 14:42:45 -0400 Subject: [PATCH 2256/3170] Add req description to error in install_wheel Moving this value up from `_install_wheel` means that we do not need to pass `req_description` anymore. This will also let us move our entrypoint error handling around without worrying about losing the context from the previous message. --- src/pip/_internal/operations/install/wheel.py | 48 +++++++++++++------ 1 file changed, 33 insertions(+), 15 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 3d841d1a0fd..905851e1fb4 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -21,7 +21,14 @@ from pip._vendor import pkg_resources from pip._vendor.distlib.scripts import ScriptMaker from pip._vendor.distlib.util import get_export_entry -from pip._vendor.six import PY2, ensure_str, ensure_text, itervalues, text_type +from pip._vendor.six import ( + PY2, + ensure_str, + ensure_text, + itervalues, + reraise, + text_type, +) from pip._vendor.six.moves import filterfalse, map from pip._internal.exceptions import InstallationError @@ -480,7 +487,6 @@ def _install_wheel( wheel_zip, # type: ZipFile wheel_path, # type: str scheme, # type: Scheme - req_description, # type: str pycompile=True, # type: bool warn_script_location=True, # type: bool direct_url=None, # type: Optional[DirectUrl] @@ -729,10 +735,10 @@ def pyc_output_path(path): except MissingCallableSuffix as e: entry = e.args[0] raise InstallationError( - "Invalid script entry point: {} for req: {} - A callable " + "Invalid script entry point: {} - A callable " "suffix is required. Cf https://packaging.python.org/" "specifications/entry-points/#use-for-scripts for more " - "information.".format(entry, req_description) + "information.".format(entry) ) if warn_script_location: @@ -793,6 +799,18 @@ def _generate_file(path, **kwargs): writer.writerows(_normalized_outrows(rows)) +@contextlib.contextmanager +def req_error_context(req_description): + # type: (str) -> Iterator[None] + try: + yield + except InstallationError as e: + message = "For req: {}. {}".format(req_description, e.args[0]) + reraise( + InstallationError, InstallationError(message), sys.exc_info()[2] + ) + + def install_wheel( name, # type: str wheel_path, # type: str @@ -805,14 +823,14 @@ def install_wheel( ): # type: (...) -> None with ZipFile(wheel_path, allowZip64=True) as z: - _install_wheel( - name=name, - wheel_zip=z, - wheel_path=wheel_path, - scheme=scheme, - req_description=req_description, - pycompile=pycompile, - warn_script_location=warn_script_location, - direct_url=direct_url, - requested=requested, - ) + with req_error_context(req_description): + _install_wheel( + name=name, + wheel_zip=z, + wheel_path=wheel_path, + scheme=scheme, + pycompile=pycompile, + warn_script_location=warn_script_location, + direct_url=direct_url, + requested=requested, + ) From 145b7add7e218a56352759fa923c4dbfe07362ff Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 11 Jul 2020 14:45:59 -0400 Subject: [PATCH 2257/3170] Move error message construction into MissingCallableSuffix Now, we are free to throw MissingCallableSuffix from anywhere and don't have to worry about catching it in the middle of our processing. --- src/pip/_internal/operations/install/wheel.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 905851e1fb4..fe1a6e92c7f 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -464,8 +464,15 @@ def save(self): self.changed = fix_script(self.dest_path) -class MissingCallableSuffix(Exception): - pass +class MissingCallableSuffix(InstallationError): + def __init__(self, entry_point): + # type: (str) -> None + super(MissingCallableSuffix, self).__init__( + "Invalid script entry point: {} - A callable " + "suffix is required. Cf https://packaging.python.org/" + "specifications/entry-points/#use-for-scripts for more " + "information.".format(entry_point) + ) def _raise_for_invalid_entrypoint(specification): @@ -732,14 +739,8 @@ def pyc_output_path(path): generated.extend( maker.make_multiple(gui_scripts_to_generate, {'gui': True}) ) - except MissingCallableSuffix as e: - entry = e.args[0] - raise InstallationError( - "Invalid script entry point: {} - A callable " - "suffix is required. Cf https://packaging.python.org/" - "specifications/entry-points/#use-for-scripts for more " - "information.".format(entry) - ) + except MissingCallableSuffix: + raise if warn_script_location: msg = message_about_scripts_not_on_PATH(generated_console_scripts) From 677b4e7f1147d6d060a3459b30203375dc476efd Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 11 Jul 2020 14:47:46 -0400 Subject: [PATCH 2258/3170] Remove redundant try ... except --- src/pip/_internal/operations/install/wheel.py | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index fe1a6e92c7f..1bc031f376c 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -730,17 +730,12 @@ def pyc_output_path(path): gui_scripts_to_generate = list(starmap('{} = {}'.format, gui.items())) - generated_console_scripts = [] # type: List[str] + generated_console_scripts = maker.make_multiple(scripts_to_generate) + generated.extend(generated_console_scripts) - try: - generated_console_scripts = maker.make_multiple(scripts_to_generate) - generated.extend(generated_console_scripts) - - generated.extend( - maker.make_multiple(gui_scripts_to_generate, {'gui': True}) - ) - except MissingCallableSuffix: - raise + generated.extend( + maker.make_multiple(gui_scripts_to_generate, {'gui': True}) + ) if warn_script_location: msg = message_about_scripts_not_on_PATH(generated_console_scripts) From 05f2d9ebf6b93b133103e027716de4aceed0372e Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sat, 11 Jul 2020 11:17:20 -0400 Subject: [PATCH 2259/3170] Make entrypoint error test higher-level The current tests didn't catch the bug that the new tests do, so they have been removed. Using higher-level tests can give us more confidence that things work end-to-end and are less likely to get in the way of refactoring. The new test has been marked xfail since the bug is still present. --- tests/unit/test_wheel.py | 50 ++++++++++++++++++++++++---------------- 1 file changed, 30 insertions(+), 20 deletions(-) diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 421f5b95899..15dff94ca16 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -23,10 +23,6 @@ get_legacy_build_wheel_path, ) from pip._internal.operations.install import wheel -from pip._internal.operations.install.wheel import ( - MissingCallableSuffix, - _raise_for_invalid_entrypoint, -) from pip._internal.utils.compat import WINDOWS from pip._internal.utils.misc import hash_file from pip._internal.utils.unpacking import unpack_file @@ -129,19 +125,6 @@ def test_get_entrypoints_no_entrypoints(): assert gui == {} -def test_raise_for_invalid_entrypoint_ok(): - _raise_for_invalid_entrypoint("hello = hello:main") - - -@pytest.mark.parametrize("entrypoint", [ - "hello = hello", - "hello = hello:", -]) -def test_raise_for_invalid_entrypoint_fail(entrypoint): - with pytest.raises(MissingCallableSuffix): - _raise_for_invalid_entrypoint(entrypoint) - - @pytest.mark.parametrize("outrows, expected", [ ([ (u'', '', 'a'), @@ -306,9 +289,10 @@ def main(): extra_data_files={ "data/my_data/data_file": "some data", }, - console_scripts=[ - "sample = sample:main", - ], + entry_points={ + "console_scripts": ["sample = sample:main"], + "gui_scripts": ["sample2 = sample:main"], + }, ).save_to_dir(tmpdir) self.req = Requirement('sample') self.src = os.path.join(tmpdir, 'src') @@ -480,6 +464,32 @@ def test_wheel_install_rejects_bad_paths(self, data, tmpdir, path): assert os.path.basename(wheel_path) in exc_text assert "example" in exc_text + @pytest.mark.xfail(strict=True) + @pytest.mark.parametrize( + "entrypoint", ["hello = hello", "hello = hello:"] + ) + @pytest.mark.parametrize( + "entrypoint_type", ["console_scripts", "gui_scripts"] + ) + def test_invalid_entrypoints_fail( + self, data, tmpdir, entrypoint, entrypoint_type + ): + self.prep(data, tmpdir) + wheel_path = make_wheel( + "simple", "0.1.0", entry_points={entrypoint_type: [entrypoint]} + ).save_to_dir(tmpdir) + with pytest.raises(InstallationError) as e: + wheel.install_wheel( + "simple", + str(wheel_path), + scheme=self.scheme, + req_description="simple", + ) + + exc_text = str(e.value) + assert os.path.basename(wheel_path) in exc_text + assert entrypoint in exc_text + class TestMessageAboutScriptsNotOnPATH(object): From 76b20d738e190801edcf5b63ac2c4ab575a19e24 Mon Sep 17 00:00:00 2001 From: Jussi Kukkonen <jkukkonen@vmware.com> Date: Wed, 10 Jun 2020 12:17:28 +0300 Subject: [PATCH 2260/3170] Deprecate requirements format "base>=1.0[extra]" This requirements format does not conform to PEP-508. Currently the extras specified like this work by accident (because _strip_extras() also parses them). The version checks end up being done with a misparsed version '1.0[extra]' -- this is not changed in this commit. Add deprecation warning and fix the corresponding resolver test. Add a command line test. Note that we really only check that the Requirement has SpecifierSet with a specifier that ends in a ']'. A valid version number cannot contain ']' and no wheels currently on pypi have versions ending in ']'. --- news/8288.removal | 1 + src/pip/_internal/req/constructors.py | 12 +++++++++++ tests/functional/test_install_extras.py | 21 +++++++++++++++++++ tests/functional/test_new_resolver.py | 28 +++++++++++++++++++++++-- 4 files changed, 60 insertions(+), 2 deletions(-) create mode 100644 news/8288.removal diff --git a/news/8288.removal b/news/8288.removal new file mode 100644 index 00000000000..830d91aab95 --- /dev/null +++ b/news/8288.removal @@ -0,0 +1 @@ +Add deprecation warning for invalid requirements format "base>=1.0[extra]" diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index 46b1daa902e..857f7fff5aa 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -23,6 +23,7 @@ from pip._internal.models.wheel import Wheel from pip._internal.pyproject import make_pyproject_path from pip._internal.req.req_install import InstallRequirement +from pip._internal.utils.deprecation import deprecated from pip._internal.utils.filetypes import ARCHIVE_EXTENSIONS from pip._internal.utils.misc import is_installable_dir, splitext from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -370,6 +371,17 @@ def with_source(text): if add_msg: msg += '\nHint: {}'.format(add_msg) raise InstallationError(msg) + else: + # Deprecate extras after specifiers: "name>=1.0[extras]" + # This currently works by accident because _strip_extras() parses + # any extras in the end of the string and those are saved in + # RequirementParts + for spec in req.specifier: + spec_str = str(spec) + if spec_str.endswith(']'): + msg = "Extras after version '{}'.".format(spec_str) + replace = "moving the extras before version specifiers" + deprecated(msg, replacement=replace, gone_in="21.0") else: req = None diff --git a/tests/functional/test_install_extras.py b/tests/functional/test_install_extras.py index ba03f1e4aab..d70067b6bca 100644 --- a/tests/functional/test_install_extras.py +++ b/tests/functional/test_install_extras.py @@ -104,6 +104,27 @@ def test_nonexistent_options_listed_in_order(script, data): assert matches == ['nonexistent', 'nope'] +def test_install_deprecated_extra(script, data): + """ + Warn about deprecated order of specifiers and extras. + + Test uses a requirements file to avoid a testing issue where + the specifier gets interpreted as shell redirect. + """ + script.scratch_path.joinpath("requirements.txt").write_text( + "requires_simple_extra>=0.1[extra]" + ) + simple = script.site_packages / 'simple' + + result = script.pip( + 'install', '--no-index', '--find-links=' + data.find_links, + '-r', script.scratch_path / 'requirements.txt', expect_stderr=True, + ) + + result.did_create(simple) + assert ("DEPRECATION: Extras after version" in result.stderr) + + def test_install_special_extra(script): # Check that uppercase letters and '-' are dealt with # make a dummy project diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 202a4b2b404..9329180334c 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -200,8 +200,6 @@ def test_new_resolver_ignore_dependencies(script): [ "base[add]", "base[add] >= 0.1.0", - # Non-standard syntax. To deprecate, see pypa/pip#8288. - "base >= 0.1.0[add]", ], ) def test_new_resolver_installs_extras(tmpdir, script, root_dep): @@ -228,6 +226,32 @@ def test_new_resolver_installs_extras(tmpdir, script, root_dep): assert_installed(script, base="0.1.0", dep="0.1.0") +def test_new_resolver_installs_extras_deprecated(tmpdir, script): + req_file = tmpdir.joinpath("requirements.txt") + req_file.write_text("base >= 0.1.0[add]") + + create_basic_wheel_for_package( + script, + "base", + "0.1.0", + extras={"add": ["dep"]}, + ) + create_basic_wheel_for_package( + script, + "dep", + "0.1.0", + ) + result = script.pip( + "install", "--use-feature=2020-resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "-r", req_file, + expect_stderr=True + ) + assert "DEPRECATION: Extras after version" in result.stderr + assert_installed(script, base="0.1.0", dep="0.1.0") + + def test_new_resolver_installs_extras_warn_missing(script): create_basic_wheel_for_package( script, From e0f311b1f42e0a41fe5c1b4d263bbdabf74cdc6d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Fri, 10 Jul 2020 00:37:38 +0700 Subject: [PATCH 2261/3170] Declare constants in configuration.py as such --- ...1e2773-eae6-43a4-b075-55f49e713fb1.trivial | 0 src/pip/_internal/configuration.py | 55 ++++++++----------- 2 files changed, 23 insertions(+), 32 deletions(-) create mode 100644 news/2a1e2773-eae6-43a4-b075-55f49e713fb1.trivial diff --git a/news/2a1e2773-eae6-43a4-b075-55f49e713fb1.trivial b/news/2a1e2773-eae6-43a4-b075-55f49e713fb1.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/configuration.py b/src/pip/_internal/configuration.py index e49a5f4f5bf..13cab923070 100644 --- a/src/pip/_internal/configuration.py +++ b/src/pip/_internal/configuration.py @@ -35,6 +35,20 @@ RawConfigParser = configparser.RawConfigParser # Shorthand Kind = NewType("Kind", str) +CONFIG_BASENAME = 'pip.ini' if WINDOWS else 'pip.conf' +ENV_NAMES_IGNORED = "version", "help" + +# The kinds of configurations there are. +kinds = enum( + USER="user", # User Specific + GLOBAL="global", # System Wide + SITE="site", # [Virtual] Environment Specific + ENV="env", # from PIP_CONFIG_FILE + ENV_VAR="env-var", # from Environment Variables +) +OVERRIDE_ORDER = kinds.GLOBAL, kinds.USER, kinds.SITE, kinds.ENV, kinds.ENV_VAR +VALID_LOAD_ONLY = kinds.USER, kinds.GLOBAL, kinds.SITE + logger = logging.getLogger(__name__) @@ -60,19 +74,6 @@ def _disassemble_key(name): return name.split(".", 1) -# The kinds of configurations there are. -kinds = enum( - USER="user", # User Specific - GLOBAL="global", # System Wide - SITE="site", # [Virtual] Environment Specific - ENV="env", # from PIP_CONFIG_FILE - ENV_VAR="env-var", # from Environment Variables -) - - -CONFIG_BASENAME = 'pip.ini' if WINDOWS else 'pip.conf' - - def get_configuration_files(): # type: () -> Dict[Kind, List[str]] global_config_files = [ @@ -114,29 +115,21 @@ def __init__(self, isolated, load_only=None): # type: (bool, Optional[Kind]) -> None super(Configuration, self).__init__() - _valid_load_only = [kinds.USER, kinds.GLOBAL, kinds.SITE, None] - if load_only not in _valid_load_only: + if load_only is not None and 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])) + ", ".join(map(repr, VALID_LOAD_ONLY)) ) ) self.isolated = isolated self.load_only = load_only - # The order here determines the override order. - self._override_order = [ - kinds.GLOBAL, kinds.USER, kinds.SITE, 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 + variant: [] for variant in OVERRIDE_ORDER } # type: Dict[Kind, List[Tuple[str, RawConfigParser]]] self._config = { - variant: {} for variant in self._override_order + variant: {} for variant in OVERRIDE_ORDER } # type: Dict[Kind, Dict[str, Any]] self._modified_parsers = [] # type: List[Tuple[str, RawConfigParser]] @@ -257,7 +250,7 @@ def _dictionary(self): # are not needed here. retval = {} - for variant in self._override_order: + for variant in OVERRIDE_ORDER: retval.update(self._config[variant]) return retval @@ -348,12 +341,10 @@ 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 + if key.startswith("PIP_"): + name = key[4:].lower() + if name not in ENV_NAMES_IGNORED: + yield name, val # XXX: This is patched in the tests. def iter_config_files(self): From db217992bd837c6a8a526dbd4c68a897e84a4148 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Wed, 15 Jul 2020 16:28:53 +0700 Subject: [PATCH 2262/3170] Use more descriptive exception when range requests are unsupported --- src/pip/_internal/network/lazy_wheel.py | 14 ++++++++++---- tests/unit/test_network_lazy_wheel.py | 7 +++++-- 2 files changed, 15 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/network/lazy_wheel.py b/src/pip/_internal/network/lazy_wheel.py index d7b8bcc21ac..7fe08d0e3cf 100644 --- a/src/pip/_internal/network/lazy_wheel.py +++ b/src/pip/_internal/network/lazy_wheel.py @@ -1,6 +1,6 @@ """Lazy ZIP over HTTP""" -__all__ = ['dist_from_wheel_url'] +__all__ = ['HTTPRangeRequestUnsupported', 'dist_from_wheel_url'] from bisect import bisect_left, bisect_right from contextlib import contextmanager @@ -23,13 +23,18 @@ from pip._internal.network.session import PipSession +class HTTPRangeRequestUnsupported(Exception): + pass + + def dist_from_wheel_url(name, url, session): # type: (str, str, PipSession) -> Distribution """Return a pkg_resources.Distribution from the given wheel URL. This uses HTTP range requests to only fetch the potion of the wheel containing metadata, just enough for the object to be constructed. - If such requests are not supported, RuntimeError is raised. + If such requests are not supported, HTTPRangeRequestUnsupported + is raised. """ with LazyZipOverHTTP(url, session) as wheel: # For read-only ZIP files, ZipFile only needs methods read, @@ -45,7 +50,8 @@ class LazyZipOverHTTP(object): This uses HTTP range requests to lazily fetch the file's content, which is supposed to be fed to ZipFile. If such requests are not - supported by the server, raise RuntimeError during initialization. + supported by the server, raise HTTPRangeRequestUnsupported + during initialization. """ def __init__(self, url, session, chunk_size=CONTENT_CHUNK_SIZE): @@ -60,7 +66,7 @@ def __init__(self, url, session, chunk_size=CONTENT_CHUNK_SIZE): self._left = [] # type: List[int] self._right = [] # type: List[int] if 'bytes' not in head.headers.get('Accept-Ranges', 'none'): - raise RuntimeError('range request is not supported') + raise HTTPRangeRequestUnsupported('range request is not supported') self._check_zip() @property diff --git a/tests/unit/test_network_lazy_wheel.py b/tests/unit/test_network_lazy_wheel.py index 694d126859f..331b87e7c88 100644 --- a/tests/unit/test_network_lazy_wheel.py +++ b/tests/unit/test_network_lazy_wheel.py @@ -3,7 +3,10 @@ from pip._vendor.pkg_resources import Requirement from pytest import fixture, mark, raises -from pip._internal.network.lazy_wheel import dist_from_wheel_url +from pip._internal.network.lazy_wheel import ( + HTTPRangeRequestUnsupported, + dist_from_wheel_url, +) from pip._internal.network.session import PipSession from tests.lib.requests_mocks import MockResponse @@ -39,7 +42,7 @@ def test_dist_from_wheel_url(session): def test_dist_from_wheel_url_no_range(session, monkeypatch): """Test handling when HTTP range requests are not supported.""" monkeypatch.setattr(session, 'head', lambda *a, **kw: MockResponse(b'')) - with raises(RuntimeError): + with raises(HTTPRangeRequestUnsupported): dist_from_wheel_url('mypy', MYPY_0_782_WHL, session) From c56f93539c0946619148f9b48440c71de21f4c46 Mon Sep 17 00:00:00 2001 From: Srinivas Nyayapati <shireenrao@gmail.com> Date: Wed, 15 Jul 2020 14:45:01 -0400 Subject: [PATCH 2263/3170] Add references to get started with Git --- docs/html/development/getting-started.rst | 8 ++++++++ news/78a83f1d-52f9-4fda-ad83-c19a3e513380.trivial | 0 2 files changed, 8 insertions(+) create mode 100644 news/78a83f1d-52f9-4fda-ad83-c19a3e513380.trivial diff --git a/docs/html/development/getting-started.rst b/docs/html/development/getting-started.rst index e387597b177..f7bc603d1ce 100644 --- a/docs/html/development/getting-started.rst +++ b/docs/html/development/getting-started.rst @@ -138,6 +138,11 @@ in order to start contributing. * Some `good first issues`_ on GitHub for new contributors * A deep dive into `pip's architecture`_ * A guide on `triaging issues`_ for issue tracker +* Getting started with Git + + - `Hello World for Git`_ + - `Understanding the GitHub flow`_ + - `Start using Git on the command line`_ .. _`open an issue`: https://github.com/pypa/pip/issues/new?title=Trouble+with+pip+development+environment @@ -148,3 +153,6 @@ in order to start contributing. .. _`good first issues`: https://github.com/pypa/pip/labels/good%20first%20issue .. _`pip's architecture`: https://pip.pypa.io/en/latest/development/architecture/ .. _`triaging issues`: https://pip.pypa.io/en/latest/development/issue-triage/ +.. _`Hello World`: https://guides.github.com/activities/hello-world/ +.. _`Understanding the GitHub flow`: https://guides.github.com/introduction/flow/ +.. _`Start using Git on the command line`: https://docs.gitlab.com/ee/gitlab-basics/start-using-git.html diff --git a/news/78a83f1d-52f9-4fda-ad83-c19a3e513380.trivial b/news/78a83f1d-52f9-4fda-ad83-c19a3e513380.trivial new file mode 100644 index 00000000000..e69de29bb2d From e3e916be8dcf3443c3bd081c9b1bf38534ab27e3 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 16 Jul 2020 01:38:36 +0530 Subject: [PATCH 2264/3170] Add a dedicated type for check_install_conflicts --- src/pip/_internal/operations/check.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/operations/check.py b/src/pip/_internal/operations/check.py index 87c265dada5..5714915bcb2 100644 --- a/src/pip/_internal/operations/check.py +++ b/src/pip/_internal/operations/check.py @@ -29,6 +29,7 @@ MissingDict = Dict[str, List[Missing]] ConflictingDict = Dict[str, List[Conflicting]] CheckResult = Tuple[MissingDict, ConflictingDict] + ConflictDetails = Tuple[PackageSet, CheckResult] PackageDetails = namedtuple('PackageDetails', ['version', 'requires']) @@ -99,7 +100,7 @@ def check_package_set(package_set, should_ignore=None): def check_install_conflicts(to_install): - # type: (List[InstallRequirement]) -> Tuple[PackageSet, CheckResult] + # type: (List[InstallRequirement]) -> ConflictDetails """For checking if the dependency graph would be consistent after \ installing given requirements """ From d6ac9a07a2b620e60f3f8b5269bf446de85da403 Mon Sep 17 00:00:00 2001 From: Srinivas Nyayapati <shireenrao@gmail.com> Date: Wed, 15 Jul 2020 16:31:59 -0400 Subject: [PATCH 2265/3170] fix hyper link in patch --- docs/html/development/getting-started.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/html/development/getting-started.rst b/docs/html/development/getting-started.rst index f7bc603d1ce..8b7900fb554 100644 --- a/docs/html/development/getting-started.rst +++ b/docs/html/development/getting-started.rst @@ -153,6 +153,6 @@ in order to start contributing. .. _`good first issues`: https://github.com/pypa/pip/labels/good%20first%20issue .. _`pip's architecture`: https://pip.pypa.io/en/latest/development/architecture/ .. _`triaging issues`: https://pip.pypa.io/en/latest/development/issue-triage/ -.. _`Hello World`: https://guides.github.com/activities/hello-world/ +.. _`Hello World for Git`: https://guides.github.com/activities/hello-world/ .. _`Understanding the GitHub flow`: https://guides.github.com/introduction/flow/ .. _`Start using Git on the command line`: https://docs.gitlab.com/ee/gitlab-basics/start-using-git.html From 67cbd0ca18ccc68a1b87e322154d9718a11c73a6 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 16 Jul 2020 13:03:26 +0530 Subject: [PATCH 2266/3170] Break up pip install's "conflict check" function Making this into two functions allows for separating the "check" and "print warnings" step in a follow up commit. --- src/pip/_internal/commands/install.py | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index b2459dfe14b..1dcd702745c 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -40,6 +40,7 @@ from typing import Iterable, List, Optional from pip._internal.models.format_control import FormatControl + from pip._internal.operations.check import ConflictDetails from pip._internal.req.req_install import InstallRequirement from pip._internal.wheel_builder import BinaryAllowedPredicate @@ -371,13 +372,14 @@ def run(self, options, args): requirement_set ) - # Consistency Checking of the package set we're installing. + # Check for conflicts in 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) + conflicts = self._determine_conflicts(to_install) + self._warn_about_conflicts(conflicts) # Don't warn about script install locations if # --target has been specified @@ -498,14 +500,22 @@ def _handle_target_dir(self, target_dir, target_temp_dir, upgrade): target_item_dir ) - def _warn_about_conflicts(self, to_install): - # type: (List[InstallRequirement]) -> None + def _determine_conflicts(self, to_install): + # type: (List[InstallRequirement]) -> Optional[ConflictDetails] try: - package_set, _dep_info = check_install_conflicts(to_install) + conflict_details = check_install_conflicts(to_install) except Exception: - logger.error("Error checking for conflicts.", exc_info=True) - return - missing, conflicting = _dep_info + logger.error( + "Error while checking for conflicts. Please file an issue on " + "pip's issue tracker: https://github.com/pypa/pip/issues/new", + exc_info=True + ) + return None + return conflict_details + + def _warn_about_conflicts(self, conflict_details): + # type: (ConflictDetails) -> None + package_set, (missing, conflicting) = conflict_details # NOTE: There is some duplication here from pip check for project_name in missing: From de741fa0dda3683fcb868b0583cb8e0c54ba454b Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 16 Jul 2020 13:05:51 +0530 Subject: [PATCH 2267/3170] Clearly note where code duplication exists The duplication of this code isn't really that bad, but saying "pip check" makes it ambigous which file is relevant. Changing to reference the exact filename makes this clearer. --- src/pip/_internal/commands/install.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 1dcd702745c..416e8be4220 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -517,7 +517,7 @@ def _warn_about_conflicts(self, conflict_details): # type: (ConflictDetails) -> None package_set, (missing, conflicting) = conflict_details - # NOTE: There is some duplication here from pip check + # NOTE: There is some duplication here, with commands/check.py for project_name in missing: version = package_set[project_name][0] for dependency in missing[project_name]: From eafbec5aa6c169c41975a4d3327a5286b26fa128 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Thu, 16 Jul 2020 13:08:32 +0530 Subject: [PATCH 2268/3170] Move conflict warning to just-before success message This is a much better location for these errors, since they're in a much more visible spot. We've had reports in the past of users missing these messages, and changing where we present these warnings should help resolve that issue. We do lose the ability for an advanced user to potentially see the warning and abort installation before the conflicts are introduced, but given that we don't even pause for input, I don't think that's a strong argument and neither do I view this as necessary. --- src/pip/_internal/commands/install.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 416e8be4220..c89f4836e20 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -373,13 +373,13 @@ def run(self, options, args): ) # Check for conflicts in the package set we're installing. + conflicts = None # type: Optional[ConflictDetails] should_warn_about_conflicts = ( not options.ignore_dependencies and options.warn_about_conflicts ) if should_warn_about_conflicts: conflicts = self._determine_conflicts(to_install) - self._warn_about_conflicts(conflicts) # Don't warn about script install locations if # --target has been specified @@ -421,6 +421,10 @@ def run(self, options, args): except Exception: pass items.append(item) + + if conflicts is not None: + self._warn_about_conflicts(conflicts) + installed_desc = ' '.join(items) if installed_desc: write_output( From 892018eaf20f7bff96de65deaf267dd584787b43 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Wed, 15 Jul 2020 22:34:33 +0700 Subject: [PATCH 2269/3170] Use local server for an unit test for lazy wheel --- .../data/packages/mypy-0.782-py3-none-any.whl | Bin 0 -> 2352680 bytes tests/unit/test_network_lazy_wheel.py | 18 +++++++++++++----- 2 files changed, 13 insertions(+), 5 deletions(-) create mode 100644 tests/data/packages/mypy-0.782-py3-none-any.whl diff --git a/tests/data/packages/mypy-0.782-py3-none-any.whl b/tests/data/packages/mypy-0.782-py3-none-any.whl new file mode 100644 index 0000000000000000000000000000000000000000..fa968ebc8138ffc7eff99f549e5dd68f4d0cc046 GIT binary patch literal 2352680 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 KE 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⋘saIQY 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%0DwFIiie;A^}-|$c(ZF^gMgm3O3 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ΠhHOt{+OV>j!od@~l`t|C~Izd+84Do#X^H8nE z_`!90Ly<87DeI1GYHpcJwBdLs{092wc5z2x$Gpa>Y^N7ve6leK)%M$gjnW2Z?-Ll` z>v8HT{neaDvcjb{IKE;GUX>=dJG7u-%rEFy8jZ?>+C{D^2h6wHp;BAD7;OT{mN$le zfLY)xvjowsY{1T&q(uBftZYvmI=@YmDKRXvaBrDVYK<nuh>Th?$laz5?7c9loGCj& z%tZ+ut(0q)`>bNgN(&w}tn1ssrK}h%@p<CJLW{2bx*XbJSX>luEi}G$!nGdHS1;6A zaDpk!^UIl%!~Cb*5R_E`(*GI|yCFcvNgn;wd)EcUxPA>Gy|4I))rfuZ+k;_V>lK1p zrZihTSuPw*Eq`kX0JTP7=i4CjPyM5FB0h5`D`w0H)j2F;1hfKZZTIU?@+G!iGAgJC zrD5j+BY}uduFh1!M6r62Z7)2MZ%%=d!22^{%2)G5>N+6>!@zV4z0H|#MMnFPwO<~? z8n_m{hH=Zd3uRc-i*Bv@<WRJ1B=P0PFmM{)UQ1HAPAoaF6EGkoHr#h!QsNgFY<TbF zZ#_yph5rRdyjkV?s22l!F5@FW2V)h%*y4MN=1vVc{I4g&3l1qwiS&J|YFEv2Bg<<& z1)G%#(5K&jldC>oLxI8Y006iw001ceN3Po0np&9Y*&8@I{ikoX2g}NCQ}m(BZ$wpq zpE1QlCQt}a$0ZEkp%yH2tv(0UEwaCMIAu7KP=&TaEa>YsD`T6)O+wOP>)>f1VSL@= zy4x+On$1IjQJ0#nJN|%Lq3_hAReY&vkXx(tJ=)<fss!cT=X&AzD)rwCUq3BWiPr9J z#=8#Y-AK$j?s2A;T5fBAjcT71oX{&4IVE1Hk@y~oljHdBWbwDpSUUvA@mf}sL^E!j z#xBbsc{f8nAP#695IJE|Kxxb=J0$f&i(2@sAP?Lq(7FXhR=dG$h$Si%9uDJGG`9J! zDmFad&hXmWTiUvv-Q1j<0Z-A7h}J^>$RF2XV-1k2ue`_siA2E9yMJwWMt2%b4CaK| zkB+?$(6@(Fzed>JF#ES{17pAcW?5JWT8(;Yx^>N(LAe1WiVGenJ(NjHn#PBl`aftQ z3245s5qSe9t;ZIKP16P1#~6!W47ZDBaqotGe;yTwtrO7tC&E2G@Ag20<iB6q0uBtP z{U)Obw8%7c0S@s)`{2?<bZx#)1?Zh|yCE!REHJR*)}umwyrs>1F`910;Og|qFeKb+ ziar1MeoP@sR3<KixxoZKtfo+}OZQ*iVr7JPo+uQvrqOw|e-*UG+<ox5@#*&XVSIZ# zI6;5n_H=pkdGLN$3A<zXe%vI(<mLLo@%6k-`Io6QYDW~3%-09KGvt|-71yMYjucHR zNL4xCpd0B5B&!4TT2oZl2nFbAHXaC0ixW&#j$<D#^+LE#H?eQrIUXP8jbiNUjeP3U zx8swm)`C>wH#eyCqEgF3*++D<(|eCK0VA!mQar$v7;5Pfqo)vqjU<v`SkKc^s7Z*5 z|5+{_Z<BzTO|UmN&soPlWh_pMl<BANdHvHU6dhD7N!THg#3)!mZY+%MMiMYC${wA7 z^u|yl(tY?^&jI0o--3`OC>{^&XP*khUYTwGJq?lTZ_hhnT#XO}Jo1S_y=WL%MyEzh zOA^SOfzr(mQh@k~MlVMkb@d$s5N;7LDzg-7P3B)gEJNTtwMdB?T(!rg2@Jn%M=*ez zpkb^+)JK3bf%@AlyRt#bMWO;_0){xaVq8g|&m9WW11-_j%k~sFS`TTs#ZZFa(sADc zc@U-yY%CsAKUh4B@H>}DAkJ9VztU#tt)@&^e;D1g6+>DJLU7!)=7!2#uHAN}0at;P zmZZUPayA$+z++%1AaaSlZ1sS91Ae3u05x{YDeo^<jB-^%u8@VvIXp!P7*mZJWt<B! z<#Q%mf9a6xL85g}lRvqS)`|Wxn^-U(9KBtG50QFi?9YE?5Jw^?Ij;QofS+4@L$DAA zl2d8|L~#pm&k^92cNSz6<}eRbzkp>wqtb{l#9)dy0ggKbUFP6L)Rs`qV0lO;FHNDg zD%QMBQZnEL{Ek9z*6@zuPRK?)K$j&9KediXC${>rT{1-MW&;qx5flr67vh#CY0+Li zVa_gWYq(*0gFeOQ(AA%}#Y8T&hyodfG2JrSn0s7GPkWxePwnrQZfZCAxdWY9pGy9Z zd7td(4sXD8u=~&ijy01oh_UtP=_{A+FuK4hK;{(U-d_GPjBz8QA`4n0+vKCN(g6jg zaWVgvng(2)6kzh{t2c!vG@C_>G~;<?t*TlP4Ht~GRUwyeyaJ@`r`ZY}JNs!7f_XwA z;q0UsLAnz}w-~GxR^OVCH(}moh~kQ#)icu#^~TjW@{%fGbq);d=<2@ylJYD98R4~5 zR5IOuO2s2t(s_Kgn`d>RKr)72l!DHeQY{x4?IgM+6tubvgwmZiv6Ep2d1TQf`RqX& z=ZJ36T6|3M9wmkVPjtZa`1HfNUA&eHP*J0JqR>@jad5iqeau;-hROm~H4s|&QH8E^ zNBQv>VFED#fO!6yJhG$<f9qSk%=^4K2*|u%zjZz+^m1L3s5@wY9wx+_i|K@C)}Ny$ z1#n+#;C}K;tTThT^cc$sdbmb<m@*y{6KJUj==`_MCuGT?Qei)z=b!82zM{4?KAkNv z&c7ACDE4bM*6u@Yb?0n0di&rq7WxUIWsMvTm>|eK`(TgBb^~Cxw4=_+#wq1^sUrg+ zbI~Y)V{o~o1GlG!hpvYN>xeJ==1sJnKv|6gO7;z9j{UieSmy})1$INXMlD@sP>>)| z@eP_$!6N|-6}u}t?HcF(Gkbr1^ymbh#cj-zU$_^TR4mCGnt)pf0^u-mS-fqK+xU7r zJKT}h|NWJ!+&ZxeR4RolJ0X`h02KO1R#QBm&8Cd)p<A7hNfuft8KD{`T<!OS4U@?o zBWv1kQALGnE?UdAM0h}QU{-mIC!%kZ6d+hc_6l-aPqex2;b5MGR`>j#0lx~hip;Y# zAT(ZTe$ZrrWp`6U_mMj4#nD(t)xW`@@a}pVbKr_VYTa*)a}CeG1F#lv2piTNB&E*u z<o7h`t<V`lWvjq1*pvBuVWe0UP<s~Per-tBi8$0+N(9L>c1%<$XY^7eqcENw=S~1j zy@+>K(STM8(S6a#G3qtiWuR3Y<Jv?Q9|VFS&a8kARA_HQQfo@iMb)XmtT&tGzp$@A zQu8bg+F`OUd=oY24}TLj#VG*!GAmvQ-H7d3lXb(TxW>(PE%9-=H!ulDn=d5w#g*D3 ztm<$R687)Zu6ip&yBDjI&5FNk)8i2UWjID`^T@y+bT7GxWwGo<e;D?lD|H}aEw!&M z!;iz<a)8)3oHx1wm1q_=yc9ea96p_j!cra|(%tGMowMlz{5?7&hgI{O<qqCa66zXb zAmtUH_6k}wM`efbbdJg!EhGqr$~H@nSS^%X9D2V+X>N`J@zk^#Z7*98Ml1h>v$i)p zV9T$WtCq~R+6NQNNi`5g*hl0JsT(pYP^UF{qh=4etbnv3)0fTkY|E$5^VG$M+Qz*^ z)smHuQ+Kw~v-gByo2&@U45;&mO)flV^%UoA{hdP=I{^?}t{vNeUlb}|0h~N94Zji8 z3IlOpDv+;=HK>&3!?avf-3%ClOQL@a*$i<$F=5CTBrh>!;`_7CUB_52@TZDE2Dsdi zr-?MkR;b{_33;eGx1K0Cw8hDSk6Th*4fyqdf3%8am-yG33hK&m$53gzmX%POb#%nF zCEsBkIA7I6m$ihZC$~S5)Yp}Uedi){&f_EaoA6?Nr3V2kH3BF7GE?Z8FOHr4W*^>M zG_jM#nx;%O_XNduxTKe!LS}3vgL7$~3(~+M2W>E>Q(JOrF;a?hA=-;y|LXYf<&lLl zrf8e*U9C`(WNIg=^*A8ru{sV=J%3xHj&Y<fd87KOTPZ#VqFjr|d~jl`A0y|lujVNz z-Qbk(PLdfjgICWs_m;M{-u9Njj?eFfPM6(TLEH&XgS`RBBI=c9@;3)A-o27pBs@mD zLGP6LLWPc!N4)Kw^dI2h*cIXvCr16lvc-RH)4`kut6LvuYb{p&5c5?#^NT3b97lP& zy#25JRUc-l_X{<Z!sp@`TtyAIW^!p4SS+{iV{^~o6j^`%HD4$CKVW9h@J7&y;=NoG z>1bpNL6w+nCj?U(cYY{A{`bsrzk0{}4puy>+!h>d>hq7t>@x?*age4!!*7w8MZ12< z{AZl~maBfDv8%aHrAfIU3zQ*GkFLbVnj1IJIc3M!F4_}z=+s|p7jrUjR*v^o)eY@4 z!BD{&N>GCg_Gxde5iSJd!Tf<{yqG>h_!_416{O^M93UFp{5c1k@cqtEUQr|TOMuH) zRw@xo3@o-eZN_#^ItTBMjbq(?{%x#ztK5r4B#d{|vokH>#&a}Sjjeb0D?xch4(&}m zt)wR<b~d2b0&%bv0SEU@pF0DFVhk(c^Y{5kM5A<2Yj!&<5-GAbP2OX!RJ9BEosGDl z`k*V*;bj`<^#v`DOsaq3(VKV`p>Z1ZBIE9SH<zWm>hAjjB`>FH^tjQW9>LkG#;5S{ zO`Co~o%6AIHOc%5wJ<@PVSeDr-5AK0;g5Y9F_^zk{!%E$A=(>n$<^W#UdA4)!%EPR zjm1mHnXnLFMLwi}XhkK&hfUmluvWVuZ1m~D&8b<dS&4yQ{^V^X_GvQ>D;Hq`TZL$Y zY+ke`)v}wTn-yPfRV(n=Hlp-e?`~DuEyH9%v&!K4Mone}%VJM%vVdc9Iu4`gbiVgY z+!Ll@sI_)Fb}|H;WZL#j=b$Dg)Py9v=i_9?Y6UE)pnh@GL4C|0t!4^&(k&?%!RGa^ z-kxpRii~MpfrEh^NV&DslKoAVwpnAPBetVWboW$*k_Oq<Vqo=B#j`QOYZk)#be+oO zircVGOobR=<AGq78NwfYPBzw|R0qNrvr$>HPQrxU7y!>stxk$Q>|J<a9SW)3TS_dV z@jr$2M1-Yh4{I#Alf&u2K^Zc{lggTy5QAUC*(8JTE)Q9p;YX8UzGb9za>|Y)tCRPE zzI>RdkTXMF<vQfNP+|juA6?F&?<}q2Fnowl@ve(qO`=u^yS^y6RGUek&^{h%NOFv- z9yVC1zmIc8;MWN9L=L%Q=3juuF}oji5hr4I!#j6(tlnX#Tk5G;fvK<pzmT2+kb9Xa ziSv<JlFlC6iRXbYo!-(~EIVxUM0boGiXX=e)xRS}?0G~N+8#Pu9oAv05ZOYu|1;Hd zY92d?jitn-J+_d!$V@I;5^K29W5F0VdWCDM`wY>7rsb6wS+vM(BU&L5%F2JjcJX@c zO0MRllsyyFuw<m#+06?4g|$ZTClBT^*p`A(LEqeB8>hw4gKDcygy`<WJ-WJ=)dZPx zGVF;?6aKtQ^$N)qau6+kJHcw8z?;3iE9)qV2{6SaFzni$6=XWDI4av)Rbqcfs-9kV z#KBYk75qHhb__lodjrwawd$>#_TgK=E`w97aI(Y&%Yr~V$}wfkywcxP7h+C@>CJZ0 z@9a7*RRO*wWkvE=QQvw7FA()Q0!{7mIY2V4o8@UaLPYQCY5L@f=C$>`ex~Uni}Qp} zQ=Ktg6u+*qJvV99uHxjp8TzKBl+&|6_IQq$iyS8$m4-4dYfmWYuw$j1Bc9hI@x~*D z^fPu&i<3p{d|QfIZ>3r@@S8z|wof*RG~IWKo6~EXD?ss|)7tV<8G?ml+*s1|Rnado z<L&??RbUUHQRNfCvaIo8Pc_*&VS_mZ{?J?*ubeD-PZkHM5zuZ#!Y4>F)p-`^_Rk#6 zKKnYU!-$fQ0?u5X=(QBVk_@_QuVPsZI^G4=Ua+7s`*FnbPGPdUD`lX7{$~uu(6bEk zrttPdIfHgi&ky*2kL|&XNrXU{003u}|Gy;0&i3EBsey&9v(vwA(`z0JyF<~`-ES(( zYj{FH7P9S66E$?Qv!{kTd<(NpS<(Olh-CFd_EPZ>v96Y&_uJ1{PLK1mopt>N!T8ki zO0_!Oo-W-KIy$=BS1yVLZxX2tx)OC%50x~B%vBX<4-XHA2Fj?K^4BhAy(s4{stL`l zrN(HqjO<9fh05H>O`RN6k8GKdoUa}MdfU2=FYU?hK-3hlJKm5Ls3+IHO|dz}%(j6L z|Jf||NqdB0JIbqHKZ&Yl+KGxc4n1U<+`FvR_uM{ja>uesI)}e}w)(0J_^c#ruhTQ2 z+1)Z}EWckI;N0(**JK)|BH81{lshR7B+K%ABPKbc7{BgROd3^eAeaD0S?&oJVa)zv z|5PJ-zfB3pzLX1=xno)xQ%W&}Jv}-D$?^T-ms5kk71XYZGL|%#@3FqKR!oDJ)G<_F zj(<2oc1F80Q8OiA_@><-JJZ!*0S3U(IssiPGeK#he-YxZc}K`bY8^$B#TnhCJ^90Z zBc<9*bctQ>cej2XJ`SfZ;a`>BKK6eOt)3Sx@Y1aM_1xTbY8_f{d2bK66HKxyF4|eq z^}D#d?HwFmA75YlzMpJ;KXtXE?1J0>VWCVXQ{a0)V&)HvU2(1RlRKhlXp_{nl=q|I z)6uDr!ly0d$F<2RQwHLDlnT|t(g@fDQMBdnGBl|Uw5W>X%{$|YTTy(o?KS~?rn?Vu zd$?~RLV%2=<OBxAVaEDzj&O8<7Rg{yZ3g_7s#oq8Cr%{TmHqX!favt{x+`GS@abc+ zN0L(kEeoE!@RS)frC>Q3Q~)EkV3Od@;6q1l1@8AW1E!b<-~z%k!B1*^fHEa(EuI$E z?@>{Bw9$icqroHfLa1FFU!lH9e6yq{7M+bkI$p&MJl{J_b~d+)D>4~Te|LWg&iW*p zDJN{S!FRq&D_@s#_#wDM)srG~xh_25@K2;lNPwA;c@yeMdw)RWi9MI6&d$JVTJh@t z9ul#6N<M;M78sJj<ze;*JA`f!>aNISSBRD#6D1`2YFfB6AJ_-IeMVnmVu8R%*PIgZ zr-Q}?nX_m`fJLm{5g{?5Vnl~19&b<+K?y^H|FpQfH)C&#CS?Yx0<u8RXmOH2c<2i= z<WT-w%s&=9@z;RFBr#Va%kBtXR0^Q2Y@22IvaeNxl&6P&>A%J8dFhY2ru`?eoM(Vh zIC9BkljPSTs+@N}NhpS#&2ba3LJyN*He8$uAM;L^QAkUAufkQ-=RYNYPv8y3eInW0 z_!HcJK1wX4K0{oZ+ue%K*)0?+iAcTdNLApX2@_!`KY2|H%$iOiCL$HyvbimnnCcjT zD4uy|zJH&dv-69-Ithyj8WOz6!DpxF-W~Hg3^-Rn;$5Q5tH+|UsKaWK;+wdxJA8@0 z4Qyk9uB_;vF%z}X58bZ}`2;R#DJt*TFLvM?)K^7XweD>(@JyyWKtyAv#h_3+(@yHA zcmv`3#Xay2=sj<S02$8ML+%(}N&rWhIxAadM&B-?q~$H5<6Nc$JD_BB*ICJr1i%XM z0~aj(@edQ@MV?)9QZd%JkG^3}DpoB06^s~Z6ljltki-ars~#S%Pcx;~N8Llc;RTc_ zVy6ZuJ71kUX0RT;8LI1?u7iKY$LZt!dO`QJ>EC^esV_U}J3oPV<jofBt9YY<;fV*H zz8%fn^_a%MxN{bo7X`!Be7g%Gi^x8*bRlkbXd+ISoQNYw^#lH6g#o&h^UiC|F&jEc zu<<%G8Q;ua(|b=?#6b~=n7T0kB5B3VeKCNqtB~8;6q{RyyC4iJCLpYDJU`eI*ikl4 zg);N@3f3$}QkkPINDU+x00-R>jpd%TLjC+jq*uU3+TxDpVa)$BO)Mw{AUPL5l!1;p z|6c#FxW&;@NrR1NrFIoqFT;U8rqWjiH%{qUyh_3O!A>%L<D3!rKCgMQK~3g0^YV_r zs~~ht=A1<_aN1BSOl4WeA7UcGTfq<<u13|55p2*PMj~-99OO{5NAHE?Cuo|`QI1bP zv7kbkBtGz6g`9PuA$UfWTf9|J%sxO@pSq>Gmm3(mHOx}M#MnzxK2HtYmeLLnHZ1~d z-KQ#_hSlK>bXpkA>cCRQPan_{P)-fr0ILkZL`+3W>Q+2OP>$s%nEgq7>Z<9K>839d zbw5@?!CZSgDLTJ{r`M#qduEyvi=MR!7gsIr+%y9nZ?NRqEtxH_PR^ysa>)YH)E>^L zSH_`H@urkOx@KB+j*YY&@bZ?q7KC5gzT__|BZ1DE%}~#;BNWt!0xe(B&kF@%FCTcX z6A=BFt;z^AG+0RnPNhx^OTPuBtxn+z>JLhz5+S05Bt?#XMv2cB2szoG<1Y>_=7@~E zp{Ji7V=akL<{T0If_+=mkRs2KI~M`V?o6rbbfh%5N$P~C5jdTp?}Mqi=zH7+;g3yo z*n6!m9kNw=hQh%b$|BtAMogZD?_Cdo#gEKguyJ-BqP<VJ=%h_{Km+G<aSBwSGH{y_ zNUSS*kZ4WN;`R1UVt-gQCN&JdP0tLJcL0-vh%VjY{TCAW>*K<)l3+j#={I+Up@w0Y z*rvc)2ms$8_OpU7(-Pz2JVGrPdF_Z+QKLaNt$7b;ss5kooE@CV7fPM-Cc%sGbh~0X zcOLvYDZtn9bo9qU))HEiJ`O11h{>ozn9{!m7*V<Z!2&lJhy%vE8u()DFVRc7$0THI zsswhFu{Z0G?HL5-O8gYY-eToc+`b?d3oS9D)Rfaa^nLDu2}@6)+QNPOVYj}$@nt-t z*TT+`>rnZULcj*A$~Is>RIX=GC-?&$U`6&|RLoWA7C#S@$xM8@GGgTAd}K~fG&t5x zqWu)4i*pZ;%nf+goJyEI&q^NulLt%?oVc5$tYAG$1%uT*SIL_6Uls&1>G4OlQmCB) z4-V?^$#yCLPSd98i6C^o;}IuW!}9mYj%f55VNo^KB_dUiRN@};?C~1<G4ZXVFE|bF zN=EKXqnZ{6L+<GuobGtYwg+-JSN@XciYboPSYvVsSRmNR5*7#Qxr~7v01^fX%@jwR zG|1^)&#kRXO)lL}4;WE+Z<XzwN3`5o1kS@>m3MDl2;##QYJt;UcLGZUM|GyVx1`*y zOpVT!2SAtpcr^;|IQDS>K3gf;VNp}A$as$(^O=9+R>=bjIvgg7_8z4r6E?a@oC(i~ z?-?oc;<7doV)VcFy!vxNnAz&MI9#x|$gNpF&WcJM@yTU#C#bPPi3WKDY1l}3RC<&c zS6mi6GLOpNgSa2&m{r85Y6T>%&~ZgD#1{@hGq|GPdDL@AS4>hDo#GcqSbGw=o%+4= zlDS;1tBSdJ)GO_h$1{DD=ZCzFo>ei{R6^MVOVS{=WpJ0xB6VEx8d21i*a9pjO{$|R zs6bXAF9uz5@b&&m;!o-0IV7u0Y%h+>#?~JEdD0c(sBB6m$MtG)HS40c7#-tIvgG<o z!;G2F?h#6NmFmRtNJ>(kk+M99kjz6SeW#yu_r2A|oS;3Rh41x?Ydq2{x}Cb1H|0+# z6n9a^(gjWQBmo1xxe^+J5c#R10T~h(^aV#KhFPGp`@V5t;GeF&8uUsJJ0;{r<_oVq zJDZL8oi|^du|H|X5RiIUIB}sTn*`3i5HUkD_i7@nmiA(XvRYrb2bA!o)O5qlmohPw z03KYr$mtv8fPZ5k-IKfKFv*ICN0yccjz-#}IRf|{576a`3b-(r;uXGmsmzvDhd}@K z?nGm)fZ+2MGnyGHAg{lP4r)p4e>Od=(IXcN1AEeIl&bD-Di#_0n7mYwnQyNkJW{Q~ z{Qzf$o_}q{XL_RNO@&(#>mng6h%zQ=k>%97fh=X)$hHZ-zcMRYnaSOkic<st1)V8F zA$0G!kp@P~E*aK>(}8aRJ>)olVVLbD?LR&#U0h&w-NpXs4`R9cr?beMcp~mSJ*piL zJfo0S@0NM?Vq=QayX(P7LW3c$7v~ei!*RPOANj7&gXw04*)eODV|ns>Q|Yz}<Bp@` zhmUEUJ2+z9C{t_Q0LoZJFg<McX7hXu{u(7MpN;s(X{yys>y0Edhi1%WDJ8r?J08&L z@G5XFGG4GcwoF`aZf3mw;V`3dqV62fhOk8?cZIh`9gnPC8=)u%pHs<K2JivL_hWxJ zIhSUTR+oQ(vnN_E<?gIWe$Jz(nxCi6AyI&vubG)C7!rH$UO<jJ0(E2B0&L!<>>NmT zB*qvAio|PC!dJNz{{o*<W9CY%4n_epMWQ#lrR0##Z%|umI?C(vx>9Pm<t26r@w6&! z`x$;9c^EM3x~PXhHaY&py@5)l3#hGg4>Em+>be<n#g=(X6kEqM@%tgqgO4f1)!$+e zC->)a>Q=)Z`ra;#4TQ+a&P=yDGxGIB-~SL$qSXixl?{pZy-sr$!4znmV~0w~m$m?@ z6`dcw;0LL)lyNS>PfJ%{6y`INOsX2kP{w{b;0OGXaaoCdaCCy_4TO(zU9980knTI1 zudM{l1Q}&%mRiD?sE}>63g~)Bp*cIC-L2Tx=c3AiER#taMFrpRZ3Y))#GD(2WF#QH z8UUc-$kQcXq-zxcGLQdm=JClYHp=;)3FU}}>ae67EHGJ0vD#jJ%b7sJv+s?IoabuM z@p;xv_k|oBZw6+;+*qiRWAEQ00A5r_iJab8PNGS4E3Ve^S&cl(mT>tloue+cBt*wd zc|YTsu$>R7XzD85HusbZ1s}q}CnY}^rpdQ|1#HO}h|TvLc<j&7v`>tWOlY4cjn*E9 z=}AMRnRffx$=6#pqY?y{x^u?AK#h9=$@)>$jc6mqo$KAQ56EZHR_-#jKc@SUBAcl< zN6&2B4bde*3s=^8Ay_<bh>hsZsoeV`x3*8*18clr1Zp0{;w5()Fcr}F{dVDb>_h<n zH&H0QRy>CxcBA<kXV4ZQ51MD6@xu2`Hs-nGj)}JDChT@eJrC>`%2XHCBuv0fn_!18 zQ;FACpLya{Fc@rwh+>Y801f7`ri#?4j{((^T2<%_LQ?=lhqPf0SLIk)d*_H=rkM~i zp|@EJ@I9)N#_1Zv;<b<oi|z+6CniqD{@sa|@3vib05CAsPCQEwu4Il@q#qPP)*?t2 zu9RoM-5sN|32%j&bbmg#l~>kYf2Lh5sO8`>m2(&epo)YGO9XsE`F^oa=-;H&_W~lc zM#&&}c6N0=QAnFw>!Z^Do!YtXP1Obs836Z|1fRsAf;W4bG^Sx@QahYOAp_J09YhXO zxj<5)gUT>$h-qF{cNiYmQN~)N*cK#lYXsG%*>qd6?Z^jQ6Lwj&1uv_Z7M6a5W;;m^ z1(xcbS|e?F7Ow0t3mzV{!UHDZ@WQ!)M-m#GpG0|P0}bt9`2n{d3XD7g?`%eUTX)d1 z&akV1uB&ermu&FLhNCId2Q&?SWdla#`2>+e<)mvfA?eg9blTf!SA;4Z2Ta82_$`M4 zZ)Q3p9&(6(2=!G)xN8?x8pysk{zn_>OGTa)s<UK$>9E!)67R)fZ(AN{dn}sdFGlt! zSCE#-%(_G!Uxr(f_0~}0zOUYi@-qik)1s;xZNUcXWQ_U<3aXmGUVnu$LOIw8*wL`E z+S1CEz$Pf(_RAceo^O4m9iS8J)6zRbI|&Br<0}OKr$PLkLeNi<S*+k|zw1cTsLFg9 zDk3gcw5x3+zDgX=2Z9fuP~>51|Gy$coi8D;cMn3oBCC=%7;++-N0%Nz%(C_Y|5y?D zIU7xzh7Vk2%?0`P^kGM>qvSr?)W%Bbg{kj&i9%pLZcuJr1GAXEIbFb1Kp7RbAtbmI zA)GIce5*Rnlm+(E)`27Hz|-(hlxYvb`BK2HJ2>z`Tz}6bx+YQ!fl=6C5x|@Beae}Q zBbb^S@K9Ya=J2QZ1HIaBBEKP3<7Rr(B~Q5dE+hMo7(u?7U#hngN7xr$^P=sMuCe(2 zu*DCb34P<w`MH54uOc{an){H8{@k$Vez+=Nzo%bdC;_>D&X)QR*xxz?Srq!TQQqg@ zbSMM#T{;zrayT$x;RmK!Wz?%P`5MnxW~hdO!^Sgp;I^mpHGf`_xsb|PR`~)*2m9>( zjEbI(xWltNE8VGQu&lLh<43@Y!NBx(jnq6tYFo6~^g_^nIUDs)lM~gR_kLSTmH6(( z<MH-)cZrb4wA^<6f|<<9v#gx*NJ2b+h~W(u65Ls*vt>~;$uRobpXXiO?@t6%yvU6e zYek-3ol_3m%ek3uN_fZQLdYMmJxddbV~b{7$_|;&n26%;cC{kzY+(Ix387PLgf5$a zSc1gL%KLK2>qK+y4pZfUi@cZBDROLC3YZB&vRqvJZliDZ$}^@yvodn=crUnQ;EdC2 zZ8>?#u~X+plv5Lmr?M2xCVQ`|Pl<-l@~*>Anz6Gs;DmXPPFQFDdC8THS%z%OG$Ncs z<&D1w$mMLeLf)RtX&2#YL$`+u0Szo^nYDhxwAZ1<I?Q1wZp>b>d9DwTP_D<<U8_uo zt_f)&6d?Wdi3#CJAUJ@kI}V_jypz3MX9E2MwHsB%K6JAw%*Ro*l>&N!;=5&wCgu4~ zeKAF*WGveV$mfg!668>Q!3ADCvU0C%-ncrcW-qI3hS}@iM428F3(}t5HAAsVRuK~B z@VT%9vySsw;?FDbl{M|cUT=sXC(vGigM?Q`7-?n_Ik-o`z(}_>c^DKq+_I@XOc^W( zOsf(?K9Mit;zAteeaXWE+uQEkst)%niWF73ffY$9X}a)TLd$JdE}YsX(9Q;3>#PrK zgI~c-9j%2p&aSbAm5nVCqt1aNph!GvkX=XrYIBM7g}LTuD=<C+I(u(B=uI#hTKwN8 z(7!WHnG~9wXg@2i{%)2Y3}r_t-#5Ws#hl_qu*pE~T2iBfq-9#JYn?h%D$U5k6w4Zu zGpD00!ODpiG1VCBEE3?1&>9>qta^`?LP=(7DL&ioxG)_QajAU8S!t;->51)a;DC-@ z%<8gp{zC;gKG97-9qkt*k+8J^W5v=BP=pC7YT0p=!^FCPs{9QC?&^UbMYH~X0A*7d zV;r9c!zEP?N^#C-t&l(J2fnF0S(#L&)}nKjLd<2XfFmN9NB9`-ZcX3rZ148{{DisL z5>$TPbM2u3NAG#3j9gAoCyep=&~B%+TiuhS<Yj)%B!9&lik%MiY|W*wW0Z~Rdlt2r z)!D^g)b-x6U5c{Z`T(C^t+us(c|TB#xxHn$-`e?(Fln4y+TFAcBE8u>*b7lEk3HCG z>GfGU<|tP=L)A?ihCxAgYAI-2Eb!=!p8`MOXn&w5cx}lf9TG$m+5_T>_>WN3{85IC zP3JF?SX)mmJS?4oCEsf+?SksfM_L>Uc$Jk@Rq>NQNl5~c2uRWhoU=V!W7Cln2%$6X z`Fj%#?4iD=n`GmnO;Kx|bAi$t2gX#3H}@xPMnfoNcJ9(lO%DwNXnG8(_V55G9WMi% zo7|Pz%HC=TRMT_e<nKN0a5*N|V9OJ+9XX{ViHSO4EQIG1C09`^jB@X4TPcNc`9!i^ z4nCS3PLJ4}yLAVJ!zAgh9(*9T3j;G0xb$#Ev&#gOXU`_?1xjfYd}Y=Y1`Y<~T2nPR z5l|Q@TzB|Tj=`-DNK!y&0M(-)&f45Dm;UdCOal33UOa~LLJsTuB9NJ)K>k9@#}bK; zvaq%hHzeE?XIz1w6jrvf?$Pw@w}U1!y{>Ms$s4`C8WOGpek`=#D3os}U>fj7hx&eS zzqNl**&uIvhZ+DL>57KzXo|bi#M*0u{bi6e#`Z@77kY(GtgKCkTdahQ;of}!StlI+ zK$ZVfL20=3H`wT~K_s`+m|;I0{R8U);_L|pF*}RF#Wu^OX72If1ssf1M!%B+@BE2B zr%!lQZ0m={u3kwU^q@?zy7hKo1{L>Zm2Fo1q)hJ54to=6b}guse`bt^2B$MF@{G-l zk6RYb(SmQuf+H`-_S~fhXGX$3=Q_jheR(l7?((B;e1A7I?~~lNTk`e%NZA`h#WCZ| zD&ra8?jb6U*;zT)&b~)xpktZ0=Plho<rdsTf`pojg!-l!yJXF};5D&C+A{!>XNtF8 z-XAe2P`M+zyIi~5-TC)$G*JPbzde!6&j!0}lDEYOx>piJ^D0gf(5WRvX4z*zUCj@- z(?d$5RjDN+tnl_4<gi!C#&b-^vG+1fClY7ZFox*!9f)fL69xhHqnu)dGdDkDe<H$X zBvl07L>dL$8ewn~72LYAeZmL)-%fE2%yZ_$?`7u=*4=8cc?@#XUR71)<=blmXnjGf zIzz|5Z-3jD*fO;aIrkhm#;RU;!h~(yoxUoniddqy+sg{%!Utm`J2%j5)<@3UGhB7H zEN|-g<m6%lYh`hB9NC=NZB~X8gQnj2QiHg#**Iskxf&dFx7^8Hc_GnSRN;5Ib2IHY zpkJGcZhfa_i}%V?*g+#2?!~M@bAD7in4VWD!a0;q<vhR8Z-;TlHruFREN+}zF0LcB zZ{^Mv4K3RuuNe$FGl%F%&$G#|zA9h|m)CYP>2dmAyF9|S4v`5f^yXX_1V;27lP;qO z*c07;kwqGq!an1mi@V?4slQzC*0RhOD0gTVT!h?oJm*d1{H<!WE8c;4qC!$pJ+E(a z+E}OSo_1sGsTRAVRyNeS7Dv*qEHbrX0&K-;BU880)lgazUNJf@s+1&+UdXJ+vFYLS z^qLB3X~Hir3m!Gi`imoGGiKQj7wIxvI(*-FN=wc_5<8nyl)$_3sa$pu-Zq^145mE7 z#!0CoGYfy)wTtnONp{T30fsCG4A;2<uj^?Kn33F_`r7(Vx8(;L_SoXkeqsMRGFtj# zdz%3S0B{5W06_SEq^Gfosez02fAXRbWi7ibdSu^IHS7v_V#@f69M*~j+`>?1aj9}~ zPMb=~qeNhLV61ggSh~;aCZUZ+1Xex+pEReD=IVC&{t}atz_k3#$VTeX9zjwHGeq;r zbR|)!$<E&fjo_-Pw=BGxAbU_fedpu;Q-=PK`Ge_%wTIfSZYyx!?>IENAhnw@+*eo6 zuDe4G1dGt?6q)5=ND0SguVH%@_?rWdyXDm-u}yiNiSlb~m$1pj3hSP9(I4uY@>TE- zVHGvqL{rUOB%bQ;a=Le(d&C*VOd08RiBFqBtNbLH*_cvKWZXs^lxz=-%7kwp*Vl_a zh_qgw-Uj|WU2Pi*h4OjuF?D6(;~hEK^|S5UhSTR_rVCPbT&)!enr&U(!46cN7bgyy z$xeS6S(4-yWqu9CB`pv|Wc)d5G$=?dX^Np5(fzr6)O$@FpRs>H(lv5e;@2P>uzs;H zq_LQ}KrVDJTBW0I)#1lQ&do;PWgz2dqu`e0wVE0}${*7?)uY|8dC|1yOxn0dYUiuq zRq6Gaf=@rZtoqRrACe=Fc9mQCc$irxN$L*sEgy~SF{GbrGc9OFX$&yqf-UWDB-PJ= ziBOjyL?s&%`ht`1vtJWGfd4*wlOBrzR3HF=_WwG28xMOAJ-h$S3R9GJ<1+aXd{67r z-QfiV)h<imSsG##ZX{=tsy0}sN8(Y$NNyU8HExJyeqV1I@i#KlXMh9V@298O@b0B1 zLV~NusjZ0KpvMVgl4yoU`J|!HM3Wgh4b3&~Ma>eE6*~40K*4~``$qoS)NQx%Yp9@5 zAWTWhMOgGkOi=4xG~dVRv%2n2+H>Z*mpyCrN`l7GD+yV=E>fd=JbJu6XR&=^$bO0W z?Z*iDd7@3>hC%bC59Z(e3E?>WyK;lO<JP4!wsfmNJZ|eHlZk0Eja~t4q8e}bV7yT| z%qc9pZC~_0@RN{udTI&TqWP}A@<}Z<<(0fckV@1$3O$Ebh{lP|t+s1u;i*2($ZeEu zX7)9*8O%7@xY7<daiG?`=)S$$0@wWsLN${?Vb`MR3+5Om|HpQLkE#!_+tJ>J?XHyy zzfeL8NCv87@k4_Ee;P_~oPjP$`NZ=#k#Um)%vBi&SY<ER^7CSP+vr>5Qa6H1Fcb2J zC)g&`(Q2R?DgbM<dXG5-$b>f0lI1~CBQ~>WCoC>A?R_v$dT`%--;eFX_NG)Ove1u} zcy^rER2pwG#C8!B{!b}lU<Vlp+9}I2h?}y&FVBkLk$Oe`sEsiFF(j_YUH)JhBmOUu z|A`yPJY12_xKqZ-f~`d6wTsbN`OKXZ&%b^!V4QVA%xFoTd49dY4*ppTEYStyn_WI~ zS)5Dw|Gq<;wUB?kNdN%k5&!^@|L^<ppF8xA5_kO%D89{Om9*KOuzRXzKbC@Yylg%i zXN_2roNN^5=F{@h&b{-XB2%iK3?Z>qARZEzfRgw7`3wvIh>&c|71yPd!h;CO93Wb> zYo42%s~h#8n><iR9`tP61f8f}9mTDV7J;6Z-K|%m5?{-fp&r#;8>qg$n>?betWvL2 z@~C1@He3poDM_74U4GGyDgZ^FZ_>e45ygLWPmMG{I6bo77`-nC1badv5Yv>8jHsJQ z7i9QnO3k4cdHL<=>FMbRBv&ul1tD!xMq5?zJ9i}IZNK6p1@&UHP9>r0R-~lYX5n3@ z^iy?OnpZ(@{k+bla|QO2F*izsyN^pFd4w|^&4Y$_MS~OCI7pG$g5tzQc{zf7GQp+5 z`~vAl{g#!TZT)<`TCo4QrCe;F$(s!-cT6Lc9TSvpfkXR?qh{~7O6Gqf^O!)Z`!>-d zr}p38b$zBYOTk!uWx~3AmlE6u#*kc0ulDvsjU>|u7tm1c<P;YR*vp~(b24g8L=_Pw zpjKpabq;kS50(k@>bi$dcB!O)&+`zQzViQYb&g%4gA1~5+qP}nwr$(CZS1yf+ctOG zwr$*fKHQmgCV$~wNhPVOr$kH+&5{u;&+a4fGcbOF4!v+s@0Ae1&`ZkI6qjw-?n;pE zzRbW-)=dhT^2y%(mzFG7=tgylUk5Fc?M^OEs@mo<)@CMDA3b`R!u~{F>Z(RRJ9pc# zZjG|)P<6@j-^C*8dLgbh{EY>VrLgf{Fu{~1CMbU{6;z38&#o(!)v2Qj`9oe;K^+UM zj&0vJ(gpSuG%j2Rr5wGUZ<SgfTP?Qwu`tCklZg~~DV_F?lsW_eFid*nr-o50f=k<E zgl3Mo&8P)zmk6Y~Ns)wJIBsE5yE?^@cdLhX%vEbWyw`2<klC+BPF_c^FK)l9+vD^6 zXvz>;s!1BO2e{Cc&fs`BryjlMyWUTeWY&{N1KzO+ghm{J&U(Nre~Du8c*{78=r#=G z{^^lC!dl<s%8&yVY@5mN$&;Q9a#zsMiX&uv^WEfr>+kvR7M`j?wn~c#H3>i86{SEo zF8mi+w7r6#t*#)#*#z<X)5|vQ8(=T<bkZmJ_<_c7S2`YAC#*(8DzXhMP=z<NcO@SX z=vC+qB=0Nt$zC4#fa~X%H#%TxBoVYoFq>?EJj5$%w+43R7R%05j1K!TBy}{6ldz7t z7kGX;;R71N57jF45KmB}C^IZb*E+6Q1%!z${ik_B0MODD7RWe-EOLBo0Q?O9)Kjsj zA5u@J*Z<|KYzke1YL33H4mg3(8K_6L2}a9^7!rr>ShB%ZFxX-PB`bfuU)k{Ji3H;v z<0Wf8R?W4ig6#Bks#%Y<iE;YNr>RO^)Q>OCpItUJhu`;UGaB4dl@-Vl*y;LeVljFw z-tTh=-)}x&ViK(c0)TXLIZgrbG?+_c<dtZAk+T*O27n~*<`IfSUkz0$Ks7|Q1&QG& z10pl~mUO^o%zIkL`dgQFouysy0Udv&yF&8>W_AVmt?#(<?eZ#S;u$6qnhs+a+TGI- zybX&dPOzVVh_$@}=K;gr47C!LsrJD`xh9GWCk~|{o&^y;{KjMK-i-KxB;;NK!kj?# z;QYL7t}OUIC|!KKv)|)oNEsn1zcb{<BJ5CPKDOXKghd~PLB`lP{AI?m(Klm6I(uql zOnls~{Cesv?Q-feua?ip<N0J{Ih>$3IufmIwaVx-(82~_lVldlFqj#OKmzEE@Z)?r zS~{p9?$$$@DMu*pvsub8R4a(q9XT4b*~aRt6@Vuc$(m2h(8nVl?H$ORJ+wnN#(`mO zSC`M*AG4?93xT)K%h~t&@z{@{*)S7!);8)Fw&tXyJc|jQ?n(6Ck&dnOfmN(UoW%Zp zmGpv0PzTiLAeeac3pDNiNF;zG-GU9rk`HIbe7|tw6uCxeduuB$0(B>@H%y(6T3SWS zv2F&?D4>6ckhwOsreGzCn%#a9>81CeY?RNByg^VV-|ED}8-xN-ZiGA9j^>ZT><E$? zM7vO@1*ai(^38&b-5b@vPWG4f4<HlMO#dF^V;7Lg0}*xW@R&fE$@SjX`EBmk-P-}q z_#+MFtedFwD9;BCpYc*|$vb9Kmm5dFsKx{?ePgK>W|sI4&~>A;butk&71dU!`-z|S z9<BE=K8U?Ba!#@VHgGS3VvMUsD5s6@1Zyzq-4Mq>D8<NH5C{#EAe!2J6C#Wb<Ub;l zY-@mmMTqP#u-mth;kL*&p{^X)rjatxc1YQrrOM4gcHG@m{h}n&3+7g{xWLfx@9$ID z-fPb3>T=*Iwuq}#^2?2MyBz}5>yn2Y6u~WK!Uq)PXBvCm5A+6mG^iyoE0nYjpEY#M zX9tP_&eA9p0^hKC4}w4>HB;#%(UgQb!g1ymYMFLZTvgXyp~n}uH(X@<oZ!WS71AMg z_E3v+H$)>8ou25eo{tB+3n1Cdi6e!#dup%q@87`Bc<$FzO-0D%{@zXTcPCx$C3`xU z^nJ(+|L)fEw)s&EcP@Z8h|Q4MB|H%Ri<C?d(`4kXq_7Nblo>May-nVDluCEbS!i@Y zR}fIbFw=Cv5s3Wh<0r+m4|GZBAP<PAsp$Gm1w-FN7lfM~H@u<lPSw7I!N!~^<9~gF znt;nfzEo(Ja2n7L^b>aqSSYhAiV7SVOPwL(-*|11TN1%^zWhUxtv>ZZNj<YEf3`=| zAKV#LBj<5BNa05wPC-Y>oqA61J^^qq=l8Ggsw(!sS>|<rp>XTnuGR|3$;sOvoQ@DJ zMWyaxBu6q6gu~SgK`wa4^^cV@K$`wNwPi^%?m9(^@>Ppl^v$Ys3-YZ+wqRx#25j$q z1?GgwCwDWOQ4nK5BRVpZzIl?c^<47`&t(WalKWTGwY(_j&tj9BS&CsdVFm7Nh~zW? zdFVmERn69wjtWIS;|x{GaQFJy43fn0SJ(gMCj-gggT%=ZHjmEDLX2Yqyc;`gY=e~j ztBBNP*A0Kc3H;S3xU58TT!@1js7%Dvn<yFs8<hdx1Rfx<6wQ@e?1-#Yd9Mc_PzB}& z&_%vFAo)GOg%W55b_cQ@;Q0$XJ~{+v@iuY>f;uk;RK;eLOQ+;i%EX;!$MrNGt6NMD zRmlBf#=umd%~5`{0K4IR1T{?0xuLRd&<+j=b15^R#jYBgN-+dwkS1;6wBwXEnb_}U zc#8JXv@=R61^-}m@YBDowZjGL06!1bNWYuQV@|?5mF!;sJ8)3RLIXZX;Xt=F$Z&#* z1Zn><5jlL3RrQ*NEJUs(C9@L}r32x*K7EO{i7m|zYFM@k3RoDc(6Bbo3uA^g>lRjy z0XJ3y5=r!=A*!emX;ZM?&&TK#g0fC667ju<E=|_0<i~7y24#@7LhW`P0fiwmNEF;Y zyLL3q@#Z)+xU^BV*>7Ef-~V8dAjyXu9aM7P83-#aFLQ}#7b>!3q*pbw@2gRa19Pnm zn-{z-Y6Pk^Y5!siqang%fLOE6Pph~R1rR%r7ye>PP3Xw3X6*71w+N+9&7x+v_BSI7 z$K{Z_Y9^9d;Jh!lv-Wb{qC(RvTaCzwSf@7X<kyTl+4GsD0l`_syZ@VQp#h~WWuO=f zqX0Df<kZThPa(g2GfqgAl0kqns2&xI<`E-B0bGJ-C`J50SRgP;8j--8^l0rYj+M)u zs7CppYfh*a4n@5QK5FN&j*cxn5nd<=$(O4Ywgv7^j#$u9)BjQ~I5Jq%Jmwmi_402z z$KaFyJGelt3eg<&u2zDNw6LswkraugJUK?~`*L{e^5{=VEZ-+8J42(G6T_3EvRiE9 z85;m%MCs1nq-3(<Bi!c2>34w<G>RZYFsz}qXAF$6nMpE@WCO(Vz@)-Gal#k21#uzk z9#&7{aAjn)(OYebF)gEw8x>ZN)lu2p-?F(4Z%d~-M*0LRA7@8ysF!Vjmrc4Hl%_ly zD4cSRF@pnuq*ZY_XJ|ebEAgGyuPvhB(41U<@u&nBQdg8EQk7BncOO+W&%Y|`bP&!g zbpc37OIY&^cyZry)*#(u@RNg<8)EN5;@<e0IJ>RLl7k6=mq{$;!kYsKIZ4sAj4_F3 z4|d^sU<N(FwFcI~t+BZ!gijzjgy2SF5E^)OptJz%o3pWVMF#r~@nf4zHKH7E)_eqQ z6V&sUH}fLUKaU?5u<%@Bu>Z#Nr07~vVjIX>2W=y>TDiS0^d+~)dL*l$Ltrsd1eX)C z+{PmDw-Dx^7Eje^TCw*vd%K=rfO`28pm`KNDEOB6aUMC9da^;v?4e|dqYEb@b0mzo zzpp|UO&3neNI7sOV)63)!JN=oUZg6bH^Ad*4Ay=renWDn0E?>&#~?sb+sv85$(M4L zJd|yOco~0#D&@_Kt}{Tca~dazAv;Ha`CNaUO)U15D9OX|r3&;DkO<fW{tTmf1&A*k znec<^!Tk9ITMFvtgJ{kzpelbtnaI;lq4XI~o#zqaWyQJ4OAf>`_SLU<4izI$y8X$_ z3;OHl#r4x7@mQY=`?<OvLg`GIuJ3vD7g0F5J0c^-%v3d^Kg<|Y9Gm6&;5FEoENZf( zZCMZLW3<EP6NhA9S6BI|!ax5=j=+51jtnLVN)7gI$82$I?`Z|NLVtP0tR5X*&MtHe zv(mg)<{>)xXGR^m<T$92#eQ8{6(CCek5ecjvI|50n0;;}hr7VuxTAAmEN!gd`8@DF z6Wky?Rou6=^0J-!!UQ_^MJno)iyW0_w(X&=X^@Gd^u9MZh$r94k;%S?pRgU`Iy)Si zZ0wFUS(xB5rZ@Le+@fy5Vb{+wBMhTBXMKU^xelbyuw~Q!9XJb1*qmS1dyc@ZVq^v& zf0^gw-yoL=iA0WVxWpK{O`NHmzD{5-GTP{OZ7G6QD*C{cPIx;_QqX)Exe5YIuO>?D zwKhWkhQ%P$z5<T=8i&iX<p--VUtDVcweDpI3PHQji&Li1S#IgJZe;GIk<&{OECBe_ zoG05Y9MDOb`Lli#3R3^5aZ40Y;;XqpQ|buHF2DyHRSbS9LHm*cdd_n75u_Y}NW7Ol z_T@d6x7d$usgT-Yb_oqCg*-45<^p1QFP?MjWO%;ML1}f*QElYueeZ$*MJ95_h=4FG z31O&YV=e)tL`hT1n!?T<PnVL`$i|IA+sq~EvWj8&B88;!q3~*6iQP=4c!uU7GyRBx z>ZKTS#ju60wmI1{tXd{}T}XBFCs7I}xDjc%1pIjIQQ0>}V{RG6=gKjp{B<=+k$fJ6 zU~BdD0MXaj9ie|TjgmA8HoVJGL}@jUmMCi39RbQtMhsjDdj_kz*HBVhY<MxunAJAM zhFBN61I&aKqFO6bZLEukZ<|fnQOoh0+Qzj^^IXf@mia%hs;)66{f*K%`KaF09=l%p z!rC1h#@xFHz+wo7)ZAY^sDcB+FLrz+@Zy0L%h{3U0Dud);OZ2~ZTTvI#=>;bcxNP{ z55oyqWZ*V`_XBO}GF1QI>0mRUs0)xwk(OsLLKww3CR{lP9+mE=fN+`%NKPmt2k(2v zOIn9{A7fOMuWg%SFV^dmHc0jbnfy9N1~R3bPY*QCMlnV2m#<UQcT9i`Ws1}6I!6JI zppXrelZ~hYXk^f(q11}~nc9QXa<9)PYC!8GzQhM%ij?#&LbylyJPKJuI%^xIWcwot zA0;=iZ$_5NnbZPvkHI8SYuarH8g6`@V!1HyT)dpT(U}G}Q#SIJ88fb~9tV~${QfO# z@yMxpL->2Y)|#^dz<0#Yn_3^E>1)`&_<eRqUTYcghviB>(Al`?kAP4AXuVMBcWjBU zpOx3?r}ofOs%dww(n*O-Gu9Kkek8^`pDH-v=xSfL@8`|K$HvT4duaft^5Tk#L}u|! z0`zgNlfVcW&ot}9sA}&cs9I-LRSWLB!MyuCX{$t<tmJz5MMd!3jdSdhqfh1mvN&(h zf#T%q#%IXRo7^j*?Os{iqQ=kl#qS^J`iV?aEAdhl@mDo)Ac{<;!$D{<{7VdT(d=@v zpV&fVqNSntr!fi!-~9t(if5?VCF4N1x_qP!_Tk%7UXD9w;N76f+}xHwZi=U8kfe#J zrtuH3iz4+*C%gpb$VA-O1+WoLFtxllJS8o`AI>VM4jBs|TcM2s6WG<cqMC^-xZetl zV&S5nCMgx+CgPPo<sGC`g6cqUwvyDJ^}W$TH~Ajl<WOthKq@74GQ8o@LX`Qkb}WrR z`9uDUl0KJ%5>qj0qqkg4X#=<Fmvpgxgw%y$jz!LiFQkc}fY^keAGoqm#UNt_vRWvD zR17;~G(m+2kJ_F&*VBja(40Tcu3ptpsrj)d#2}qRS6Ih78-EV?XtCt{--|414R_c+ zA}wo+A+*;q(^JV9h*MmQQR7((-jnAppfflG6iK<$GMrN4k{NaD0+^gy{$;~y=atAS zQ%^Gw1TsIQ*4d3fuH6a+%O}dKnVpN1c(Mu4Z8D{n2-z`TY}T;lu8Ja>_$VNdQju`S zs}uA~!i50)Sj7n=XS{}W^=ue*J|tgm{iA4i4oM2uzGlKYZR(>7b3woU91`5M0uNpe z5j*_V0~3{vGgf$eCn7UV98Y7S3P(F?yUpROAkpAiZCUqSMS3g31Ny~Ek<gusGWEFN zhvH$(I3#&v!Q|KUsg+aB#tCx<%d%T*`b$9$J2yDl%%V=N8%&BwzbU&fZ)C~Isr)UG z6h~;prPtmLL|mYT*5~q#c-$s46{U~QpFJ+Osa(VyYXe(wJxqyI;5AIaL6OVSfFZKU zyAsImDdi)D3``5ILmdWHsH`WMfN^TT9W|atMTlYa5|JZz3hAkAeMIQK3@ZZ5kDdlS z!*l-3k}P)@gI90J<a{a$w!zHY4u;{-0bLE?vv!2(1!AH^5n3t>$A|+ycV>bgmU@Tz z!bAC}TvGK0I-fR)8LXsYm6tN-q7i+*3#{La8ejHHH}Hw?&odm%T^U^U>)SPHHIr2} z9BX<DAulw0pL)Bh)pLQzY2g`wyR?%~T?y6}MmSy8>zi-}*D<FJ;zB9YWtV8qLH<UO zrsAFm-dV-JS-bhFPG3tl0PZxPXk`!zu5`igo$`}gY!&k``E>49-IH|<oniF)Xlt_& z2dlwZ+*m4jrf%r<tQVc+skeQ-T&3djhd%!gnR7SJQy&nQD)t>g>06PHM{2H{iGH4W zov|AhE9+?fusuM5Z<$t!0ALWN!wq<)Y5L`IWrtC<hAg|CbVlQq*<DEWMB>m`;~8T< zE?k^Jz8Jp>W5+FDbOx7%?4L&R9;xzyG*3y@b;M-@{YF=LHP`WkFml}$H;K3A4~Atn zKx_p>YKTY)Bn|qx;N7g0oz^(n=J=oM^-eU+s+=s(Cfg|FDkduJ_vuNs(O@@^QUg@w zQU{(o>}lWv<X|ucB}O;wm0HT{U|L-~2O`^t9ER#`a6s{u8gr=}HC1VgqAQm45TW?i zKuPNzl^>Uz4@Y?ZJ;3e}<6Mt$)*94OSyc&-3mz@h-ODfgg^`Otyl~cBJ*NT0FjTo^ zg$x?B0969>Br4a0P58Ews;g%JwlNb8t3?kE3TT9}_u+FXQn4x@0JurAkEK~!q_%Kj zq9+a0t7jNknI{_@3}jd>h6eQDHh`a;#g%TzYOc+r&lz-$L}J(Fabqrq5w)r1KppHQ z(QmdQ(BpmgoAYbvZ;5<*raq)$AAvVAo#*RH-K`pI;1UL7lCmg20p|Vu3O#}x<Ur7p z5k9@f>?pyX=G9FLp^%{j#TB`Ub^B@b#epQMn8bhPB*nlG&>NnUd%9v;J06m7>`SRa z#(hBDpR*ZpW(quI#AtBii%zpk$U5X>)!kIM!rsx$e`novdF!$o9%93SV%N&N5xf;# z@x~;HT8?HZYz2}2;w951+@z8~5^VB}vc-4W#>f`;^A428^U9_r`tY=QM_uQ>2>{(J zk7ft;;#8K5Y0#_??`xBB{sWVP54h_7X>5;GNjv5PA*&-XiOGOa&5RJBK{L9+@pI?M z9$~Pd9iX5wg&!*4BweWt`%{T8{%)(IPnGmqJ=+VGo{2(x>hrd;1i*o6jKyOVMqiP* zV7uW&NtY5d39#W;Oo#18L8PU4YG~YiD08*Q-||x7%Hzaig&N<NuBI6exr=?wuzGuW zonHs*fRQ)U5Eo1^zOt}Q1N~G(QL&I-BvHVGW??IHIEDVrkP~T30xU0lfOYxiD*$?w z0G-rZzTf;=I75w9aAdg>1Zqn72tZBvUXh4qklNrGCdtL;>Q(qk`O#!Rq14q(75c6g z8(wWJq+~NRWG-|hm88MDY;=8Fp%64;^j4m+8eTeVUk7=RJgR?y!)e2INidDK>i7y# zT<@ys%9VN^SXEv@p?xlY=YbF+^(u~xDhwq2Mdo<3Ul(rU{pFJZRp#{`5|mkl3ouRb z4r@m3-`0HXQg%>s@0&=5b6%vw9`{WcJ#@e(Lx_{hXN80WILqQ<#S<`MrR-$#RE5lf znbm^Fn0-VExDKS2!_|RZ-f<YXK#W5zE~+<u%t`k2VCz+Y=bU%%zLbtid=`_+ns11% z6W@9KH^wI_wF>nerKaw01e_;`3U?0vOgTS^2RGCeVE7>(<O_2y&l5jr4}8l~XUk*{ zyUdPbr`}Z4OtC|xw@d=hI~&;cq09wnA$OAHSZx7b*BmiO+~Kci1|6~HC>odIUOMdr zcSF@6oFI--Yf=IW@p>I#7xzB?Oz6*u&A33y9u6Q@RdxFm7mY6C11MkCAMsfrQoyCk zKj~nDEtP-KD58NxWHtW$X${~H6U^SXJjT`AA8}p<rpC4<M(Zg(TdZGxLZMx&mN?Ym z!~#9()UdAE#LjiUVJ&5vmn;`Iuw_drtd^Q`NToaT?X`^e&HkYvi)We>VQ~%vkq&W2 zv;{vmR5h+pi>aV0hTAN&kA8^TVpU$gH(MaHy#q8~9*s0$q?CEK<$Bw2EOBP$xg&WL zvH;Ny4_QSfzyHywX_oO(!$~uoYPnKO?v2!%X2cO?Vp~^mhp%Hi8yh@RR|s0yrw<`f zCWV4M*}nk(fWsw7{znpRHS-y70_KqX*dQOeBOW>JE!aIHB_`jf5uVZMN5>Y~*bXBx zzI$j!vo<+-DMBnz!!VJLe7u3`iZ^Mi_9tqegOOrbn5TWZCWPR6Fo0Q2sPi_yF(Wq? z)g3FM{5}Vn;@UC1FJDO!58INNW;A5zjaSyh)IY%}%At}DA2!=hMoZIz{)+=&1ZW&? z=~6|s2a>rJU~DIm!B}D+wqo`uxlE@97=6RsiIFpKbi;M1gfpnJmY`;j2N-*>8@*XT zBwsA?%Gt)(dW?v!R^`uO?jU|_L;uC09fNzY{qA!AVyhiPX2`#Cdw;&`vU4Is`U5dF zz)aMGjmE#j^L5BhC;PT{;+nHr3ly5sQog#d?nFAD0;5W<U=V-EAyGU~%f)@Tr2Aw{ z3N+W3(PA7OQAb#_`+-T%*RN8_9VuZuRflli`_n$b+Z~^U7%BIT$=-QM%Xlt_Z#k2$ z`x@%a!sSvwMxX>7k%L^}{Y)lslM>iggcd~kwGfYVh`8WQk%ARWG9FzA9g@U4M#NMr zGuC5!g7dQenlbjh?=8Oj@GtnAUrP3{$+z&!wmNM8>st#6FMSmcv@SGfbps7H0R`R& zD*I3o*yNiVeJEQbhZ9JX5=;oZ^KFLqH(s|^MjJ=J>W~xDxMBhM@tJoi-9@gJOOn_z zN)s<}{P@?U)b!ItPm2zH%FjqRdREe+%_sRVOI?jJ=fFLdf%~MMR9`;meNU}P@pBYv zgsW>`ZR@JVuT0K#{(Y9qx}Ck809{QIh3`TKJ+Qi6N()VPp=;jeO%hW@ffg8rK!*^Z z%S%m<Ukc_1)Em_VS1Q#AbQWt##fKF5B6q*DLK1SM!1pz0wOtuxXvf2?C7nWGv2CsV z{Lax%plgc;W4E-twISZ5BoIM*vp)!mrhi@QcDB&%eD>bc`kw6Fob)VK-NOT^x-oY( zLINPwqm@Nvh|AvAT}xrnNgU>KlHdEEfhhLR(qi-fY2dTC6UKfn?>Wd3gTP_1;dXJQ z<8>y@FKBp;luB%oERyH9@NS+Ad`NR!#vsQp#=VsQcACVGoE8)5iyH>gsbIHFvrB~6 zVOM~GC@?9x2+3Mn{tL5Z5Z^PE4_=rG?De!Nx*$!TXqYL@1A6AyMP7G4KL(IQ2Bvmg zdfn+7vvAkyY+<jV<H?Srxv<$QKH74Fpd9xWm8Kx6kCktB6uJ;{3^db3rZ|QXqWeBr zigc~i<@(pT$(|SJcJhX_N?SbnOyhj)2oI+O--Z8^1x){Pg_(g-|7?XWv5I|dfv?1w zY=iyZI0o|bNt3;yHo~}G^Zt(l-%NQ5`8QJo@wX17>BbumS&bz|^1`lUbQ><cS;NW7 z7jw2<YTpcJf@$!leTuUs7qEz}b`xf9#ZglK6BDKo&8k~lqm}s=A0vag!dolCOtmJQ zb<gm>ZmUbg&dbO;s>O5b4|S10KOG-tP{Z-6T+`BebxiKuw!vl=M)Mu4+MmGR8yj!^ z-U^3{-av-tU3rTx)EZ{l(z@n}lFU8Lu^SjKr(aJA8*Hc5dGhGRP|d((Z)ZR`rHZ)3 zR~Cb@S-gork4kVW&*HGd2d*G<JYdIJL|g;eNj0Wj+`qdYRSRWs_)KMU==e16{+T-` zaS4a#y{=FXFW-1Qrw`kx5J%c{UF4o#)-T>bxE^#Na%5E97f44QiVJ%Eqy+ctqdRrS z1Hxs*@|&xm(1{g#;IkVS!7*!uGsLVTZW(<G5_-k>e;Ex8Z8pTQ<s`BB!jUExvu?7U z-z3cKJN&)mLt9d5YrMM`6RBF0tfePBwkyPfcdMwmUsV(qh+P$Fr&f)2>zM(9CHl}_ z-~K+iO+(HI3#Zt_LTx5K4~(Te(PI1IfjoegVJH!OA~%gNBGMS##O*|}Iy}1s<4FMp z+r##6`0@bIxT~?f|13FvKY#9t#f{Ke=8NrgvD)F#C|LX8k86!O$*?W|E$NzNiI09X z=#9t4RX8VLUm&r+q-&o*X$BG_9#ZJJzG@4`{7nTKHIopuMmlI9#1h>3s~z_8aMo}s zNS&2+dp0oc-2Q-ygL8KMC|RVQ+9#d-k7CV>e$GYekf-tsMOm|80#6M%dF<0pO6&8w z&6%tg%9)^jG~H3B6y#~d_HgXar3ocGmf7Us6_Mck$mztzTcNbe@O2hFy4$8O_LlQ1 z+x05($B?Q~Os+2R;r;C&8e@FxL61BpP5ljv>nd38CgW=O{?B%1*mRZ=HSW?+Mn$o% zFH@qvPaQxrr^lFSY=z$lSB<q;w9T>0-Dt%|<c3%TPUysnYGniRn@t{pIK%lJZw}2y zESFTg(P`)8!%ErsRj3<$A})^}xw&{zqCNW%4xCFbdUWdA63w!9t=7S~(F4kq6@R|J z_S8c3-W*$POv)pgWkn~3RWzz06?|L}9L?~!*!c%-N<;F{{W*5s;RD^?paa@9p&pbE zJ5EU>24uLS?5l#{fH!W!%|UuF=EPqwn}yklez#pWInYixY1ajzM~2F%O`5=tt0q-d z!jqj<b+A7^@q)=#c_r_6``fA1{XvYLGO&7hn7EMICyb~PqCgSP5u>|GR3ndMxL+Pt zE|Ms?+C^2s4=fOt^tMtOm7JiaK$W)>C2ds&iYwAb(mdOYut!&hsc9}1zx^K)#%}T@ zpw)Zj=oQRDbcj=PcvI(MoxbIFzA=D%z{i03Qgj!Zy+>_fH+2VPD`LyTTnZdHJD+RH ze1Kq)b*U9k0F`|wWwT#e2*ztRsH6Hr$`&V_(@_Y0;0bgIyvHbV^0NBFp##T0_MUfW z=?!j7c8|uF+P^4=4Ss7f@pN<mENZ#6Vm?1ih)rgYJpO*>r*FoEA}Np!kOlE=AWx9> z6U~|6vN>j!IZD&-N3I{}0?&ux@c8;~`ju8G0=Ldw*VJ{jyL={yJzUG0`xYB^{gU%t zu_@>mfpGB)Q*#W?mgF4nqRaNg1hd?j<=wGdU{ZTUu3Gz+JXG$+<}5Hr0C$2%bU1&j zGT6_<^!YMOQBv9qR_%7c++gDYHCifBsW^4YGMU~7Af~M8rpD~6mvKEUufFqlI@nU) zFs7O%ncef!0J6r}H>W7M>QWfY|C#bKzN4ys?Y|hcpr-=h{`W8K`tIKStYzR)m%5)n zOjMn~t;fk@z-jc&39I+wyAEtbz0s{0FRl3dyoEF&eg$|0zJA~fjXD9E(znY+>~e&8 z9KS=!yT3OR$&Z}@|3X@0c?F2xY@0dvQ_HhURmn5p5>X+7O|50SnQ1E@xB)6}xZ5cB z0;trjao&YC^SJ$@Y8q+5GehXYzUSNYt8;$cF}3vme(SE~?HaNNNC=mn<dZ~VpE$hT z;&)%I_ald89CbI)axXNf@&-C=4`O!;fHrB<G>NVYqd?xwL%lpJ;;M<U7agU~VO?JB z^z)U*n19IR@-bc?uAoLcUoeR(mtYAWp@|H05){~P2(6nuf{x7!J7OGQa7~2eW9{Xw z4CGllT{#SG=~j}Qs_5{$5T6*OIKi2(rjDo4F~zDfo^4G-;zRLpzlSH^ti9T}d&h<$ zQZy5s*5cLbkz6`IQ4r!a85|L;NFLl=iE9`ThRPdY<<9ezoF1f8;hMm!G4hjbjAaXq z8D7Gsf^WG4=i{_UxL=Ed+u_xV7a_MlJp*~^?ak-P%W}qUEM+xix+}KyZiH!IHia*3 z&jal3VdVcWAVeb-pQPwt+EW@B008N~(^{@BmNx&%{3+>54>6$l&egSxm{3~endURJ z0E=d?z4I73n-l9QC3RW;_Pkp-@=>IRm(KK@W_!yGan@;{D+n5+4pJ|G7Xr3me-vDS zCx{)#$BMfPpwCK;uGGBjX?NYK1PI(FSOm!RwE*JfL7LM94&n_}u^Lo}f+tc*NBRiV zu;hQ1Qed8DGxl885PQ6?Ftob5fwMeqbPs4~r0>6Z1m}f}T3dfDH7tp_5wopBTq$4Q zoS$fUnOwDM(dpr>WO?;y^etr_ZwrH_F;F+%m-7L0L=ti2VPp)_a@Z)~E3m9!*ib;s zk*@&F)*bjbgBgOd&vl^RTrLw+NFpsLAoUK^vlnxht4$$Thww^YBdWO`XS?RN5fV(V zDQXIi^ND#U<fvtEyIVn(h#*ZN{jn?0Hk16cChFu_pvRRWjZ%R{K}^qV{I>&mFA&7P zk+*mkL(1+WES8dPw60mvEs<R^_;xyCBflA~nHc!CZXmBvLR<Qtb$7jsd54eJygHi9 z0vlECliEn}GnNAk;&=fKSGG41>4A%6$S8+T_p_5>lI{!m-vPW>3H$OiFaUt@f1Fmt z|9_;WPKM5=E}jmi|4y_;L)(6n4aM)Zj-WAIE11f_mn8x4Mhj*nszhSZF2E3?S!67m zQi-6Vc&BcclY~_94>EiI1$Y(`{M<7)x>ud5@Kwd}L+)Lv>PAx5iW6zY_=LTb)+Fg- zeX-DdjzRPYo;*w0i%LJE*@yBJXy6=f_vH4gx?Q`f!$b5|h1`mtIjy|keOVF>QR#<p zPBQ$0236I4Ns&X0_xrxgYSfv>cbxhPzh7d0m;Me911(Aorh{Kyg`r@$yo$Em$oMBN z^7KpBn{QbKOAy#|2hTii9uiDd7H|5W<oNnJVEG5hs#MX=U1sgp8(qurVC#X3ff1r; z9V%7cP8Z)#@Ar2*-z@kl_UMs9Gw;E9oG}|ned;j=;yV9Wk-F@&sw!%8u7ZaUL$jkm ztGGCpVabvk+XjavYeSU3)u_!z!-esY@%O}S;70H<DpJ;FTBOz>G9@M@1FpGL44%KS zYz_+^paGN@EpYJm?Ed!m@7G{kt+4dEEbN!r-LBqp4r>=fz3bKffb889l8Pvs*;`zb zhUj`+Hn2E?c?1)*;k0+*pP-==u2>jPrYEff<#N)+R2HfYD|9*N#5(j%Fh+`yRxlaY zqL(umiMFi|c!Lmpdv5FRTXzev$e-D3s@aU@!BKg6<J&V`S<48t=!8^|XF~v7F7O^P zn2Ax7>ZuT7@qBk-tSVZ11B6s^VZv`-I|CmHRa1Z`pm;PHD^lhaqycq*0~{VCkG)?@ zWk^s$htYj=JQ(ZjrZ+RZ$uYl;X6fFQN{s_O$xQO#E0I|fCyAcM11-@!bDYK(H{pU7 z>R-=}j;E(;=yfBn%JaV^_FL$aBk#&Rde@S9mOc};cLC`dF3wY1??_#l#j7keLuWUr zw@tJkm;>~Ic$Z(vfDJ<fX4#mLI#Lvf#ZwB^T>crpT*Zx*3cX12qct}f9zP=zi<r_{ z6yq}d4C;BW4sPOAMRm^vDdqE^g5pIY;WarLqIfBMar@y{u2KTB-6q0DPSsaVYc4#` zJ%^UH?7W{o2+cF*&Ej;{`Miryvk1}~1jf~He*ab@1h~mhy%2cUZ?;`*qy0EZ_exEK z?$9obA_F>l7j*}LGn@BN+a^|MCao}pM-k$SCPuO`QLeZVnKFjZyv>4&3?({git4C$ z$)U+>vB+vw^mrn_<=r6p)k%{IEiXI3Fi(L?g$&TlunsQ?QRAOX#^VsdX#bo?1S-Nd zf#bM<@G}jTkoJ#gq(kkaXVNC?3V*@eeYhG}!!L>PScTfQJp?yr4gRyDLEozx%vrrn z^Qn+0G-5N(VaT^;Kkhrw%8t4_-rrv($3k8}do5s&KFKmk?{!afLa7-r29M5KhtAKR z>NX5DY|j!?MoC9<f&I&HyaF=4@-m%LJut1^dKu;bUwJhy&!NcGgfNSOexcvbYO(+r z$k)(n^Df~~$X}T&VUWKUKc>5`Qm|)nrVx`MM(t@+6z~mT?*X97h>Z{tEW5d+^i7+f z0iWoaV6cuuR`V+!h^MUD*KNb1x6RspZQB6As*5%GS>f3uHW`eC=t|rnhMWD-!RC|e zLI4W<>p2bOfpX&zRvB#h9ayTAgzBM|DanH0Enw=>y7gjLh09juC>cMJZ^^-gmfg^* z7u5Cy093g^!Ri$2(nm}_ZUYfj9EfxEUN(-?v`kmP$*0dqRgksh4vh;sLex;V4`#ja zolNGaFR<Q{0R`~AbAR+!yPiFhL2TVvNzd%`(B~h#WYg9~M$TrLO&NMv%!6<6@5t5| z0J4;|uX`%b2IZJl>>#`#7YR%>Fr6x2AE-Kk;xuM(5%$R2(L_alvVs6G(Rld*Jo~bd zM6v_Oy#D=9<dF3OI2Qq~=5lpIE2<zu=kABT_*=~l=UN6H(?|GHGr5hE>@rtCDn?T; zD>zf=9p*cA+%+dGLU?yIANJm}fR<t<Z(3xf{*`9eyD=|`=_VWT0T=SQ$(V+PQ682h z3~A7OtXL@y$%u%H?L^JUB^wa$a0Actf+`P+A^yhK^@}oXpQALoj%$YeC2e%M9gYm> z(v6(zYEY9e)E{a|ksc@Mp9lF{EW>ozbtA<*z0V8Zsd0dZpb<{%a;b?DSD$><Fl-}1 zoEmqSHpGsbqg+$Fje{RMj|>V0VAc_Q1BSXq&CHiq7E&=MBe5Ng5`HrdKS2>N$dd6f zu_%mEiC$%KihK+LXM&i<5I-o1!O{hl2t)~W7|><&^xHfm^n(B2Du%#{WR>zy#VG$} z$p7cX#MH^j-pSbB#PmO9gjfy5e=>&f9~s*MCl|N^+|+JBRj?&xMVJb#ZibB#$|t2d zAxg%RB)yONdEL%2nRqs#^o$f$dfiTR-}PN`;K)AN(#n_!;jTbk#N4#R&^Qf(G;VIN z>Ds;497L54MUKpZ0sAYaAt-D;07b2oYB|J|!EIRVW#UGT#G<L04ih%CZ;*i5V`|qo zgzD6_ING3Xxqa_JqnD$AhSLyEx~g>V!+_Ckte4v~3~>Vx57Q)4HppZLLN{wlq}yx0 zVhK!LY-C4FI|XROwC9Pp$nE$(tADlWKDt9#?ICaH^XTXW)wBypJD_X$JpTz8+^1qf z6IYT%7e`+`-bDSd$19LYo!%0e$h|dU1=6a*W7SkPCe8NK4^%^?{T5C}W&yZ0;NK^Q zvmX3jRF?cojnJ(4jf3L}J)Y8Z1i-`gCe}Vk!S>M<m;C+qZpaciwB_Wk*yRq1eITlc zG52-pxi<$;>30}Z9oz0~1{0wa>EbdHwiqjsy9dy2%t5*u8tmg0>f1U*&DM!1g^?Ri zm&Hsm$9ae52Oe{v?Fa@|hLP>gx*5szY-evw-)$)<L7+Uk#lyCu+Mo-~Aqx^<qD<+X zMAf1}Szo&bbxdQ1-Mfy9LooVxC}+3T|HLsy8hb6+w((93Aj<lE9o{}oZ}Uab;X^ZB z=kXU8dC8A;D7PaKC!Y{H3074D)tvmp{ML<+8-0&?&eRKuX#W7AYfs8j9L6OwT%yXm ze{c;t$n;3GCIg5thPueKGUOmn2uNAMX50>zBw6zujA3J(uuL)qP}=}4Wo5QAFbj9q z+!pcB4l5&fv<ua)NZBoY!~(fzJCOKnDcS|Q^<=zuT%hNz0Dn7N5ZX&~lYC9H00}Dn zl-|dl8Ytj#1eRXpG%QVm5$C~IySb!-ah+4J#v-hAW3c5x(NTlQtLRy+>Rq<}F1WlV zga&Jng?Gj#6XDzw9*3meA!pxME3W`X=Y`r7;fgpj2u~tonE}+TDkt!#Vg~?1^B)4{ zp}i*Q;;Ga?y8Xwh6aHk1KrGHakQ$|U8pE<Xd_A!kM4nBCtAW(d<jIcwi6{MCUt1y2 zc*Gi5BEB5w3GT`O$vye+!z_G{#X6e2rC^?zGe+%uxCm@4(6)Qf_N=-hPyClCG}c8t zqn!kBxNx(0vvIc%T3tFSh3^`Bc9&7|#F01ol?@*+bk^k2zi$Ur7C3z2#Ye=acyd6e zP^WO5xAm-|COUctN3SC(-^`<dDKP={;AEFl0KK50Sch+z7572E2es3U>9EZvSumsp zh&|VWB(3hkV%AN)0S5go+99?DO3@2Pf5L7j3<~qAMWVXDOu7L+0o6e4f>EfKc#O?4 z!uZ1qSqKZp7V<>gk;#>}B9M%*_ihI|yWorMN3YA8L1R0NwOC@*GezzFS^WiL*(~VO zZ4cnt?Z7#^b1(Snif6Lg9e%K!L&zI%+{C$x<dWmW2(*p8V+!D)P$W_O-G39*{GDk* zW%vBgmsq5_n@x0Rcm2%engqHHtomd(1ePB6$a?j{pbbpk)`4ILVx%+tf}oK#B#v=b zmh6$-Hm_XtBypuRjHjA@{GfA(@$rfLQFnR297m&{@miCn3&{03T)cW$LX9ca;|I@x zHftClx6fn7&p*Jlg>DnGYjwzSHZPNYUGYZDRpFZ<*zJnv64~W08;Kvk2qYp<5)Q>3 zW9Uck%O7?2ptSUS<mvLtF+&GSvxHPM+`7m2XCaTh@exEv&7<2DL=q2EmB0eq?JFTM zfhCF&Yka3214g+8+1b!;3Z$otL&?8|mSrMUI2BS6uSw1`HsGhue)O!bPh#04R@Z$Q z9)lpVby`w56>t#9P*~qA<e729H5vzKnpg+eYpOpBn{~E4_kBPM!)v8?bT@4wRSOf* z{=u$d-fmX)wA@o4+|=smny^5o&&F}5sIYxcPe9=v+)Rfo`+%+6*RJ-T;AFvGOi`lx zKod6Sl|K!Sk#VI9m|{;O@kJ~;aC-oMZG2VNTtjgkrU0kYk`5xBu+8s+Ty<Oogc4|v zgB0~sx4;W-W%)hv?Ke5v6~MCa)OICD@Mi!<1gug&u&G|pR6d~mTGZqE@OncE(pNHo zmm6nYA3*Nqz^cLt9yUO#OVp(;1@s%Q8fyGX17(xec#*q9;tOyht76uiB)MTt)%<P~ zfARPt)+Y=Y^<}lQ*QK)ZsiEKCbwG2p2$`+h5c^HiXSeab{||oCD1jMV1s4Fo&<Owl z?teeU{)yOs1gwQu*L{;M_4XaL{F{I>F{6}OKC`ncV>si`d83rv(H*~A4_7M0W)!6Y z$;8H1{`YeS-Us<l3R}^(B?(gdf3Dqtsq0!x?!V<WY3}ZGQa=?hlRnoQKD6rZA3y7* zwyj3nDk(bdrpnX>+THfh>yrnz9XBV130d8Boj`s(hj`n5>ML!f_o{tWM><~`R2Duh zyA!{Yn;mZy`{=+PeZqU5p!Ubc?(cqdJ4dQ1byVlU+JoGh)cCRPq)!XRcCTH&r7JUX znQi#@;jfMBq0W1GdckvzqIabOP8IkxX+-Kv!#wv~6RoxzuF4Y7Z$Nq7f#kI*p2w_U z5l<t)%kHM8TD?6UuOF-Ub*cEfzaRTEBM;wQ?)dbzCxt4~`C%&VjT&}R1oKVQj&>1T zIXbdta{(?hz}<8KSI1np2G!1vyzg^h4t1H`>C+=e`m$~P{`7e6^^J`hGIk*xqC!)l z1s6@ODbyoP9o6hJ2N?l#6B85Q9o8jDu4%-8qD_b$3iSdhNAHm}DpY+bn9aUoIdUV~ z6)UzKk6h!sUYd2JKL?c5f47fwERMxm3$6FkP%6*0Ergj(1!}4dq(IpzP~+1hqkVkI zC*|w{g#dh`vU$-}HKi3_H&blP3x(OHAz<L!b=>0xGV`!4OvvMPykv2_Yq$&+8*l9F zrfF&cNJC<ZyvrF6;5!~1!#q79kN~W_>Bx}rcS`Fjzt`=3^-{8s$kfobmVq|dMB%^L z1lBO}{ZQuyuE?_m1H5zWO9JC$4c!j*Ye5z`$0hnJHIWk^<HibFBi_a_So-T)a)xzm z9(yz-2d!ZOW%cVa@Dq$3V&g=|o^;r*m0rKF#yz)^%$V;L2IDhV5*Z8VP7kyP6lV1z zorpg^dhPJ6@L3|}{j~XEw}5N9814|~(dR+;WFh)NAUP|t<mAp;lIr)T?D+TE9g>-d z@8c6oDav4!MgZNKg65dEkHvIacp-kl4NkjC>!2Ld)N?eRNquJ6aqXVD{n584wOVCz zBG%RW!Csq}ZXcx9Q@r)nz4Ixk>8={5HfwcWRqam8MS_#ez;oPfoi|Uk!`1{=NqeFU zdiC7<X$9sUTB~9Ez)XbiWr0WutxkkiL<zicSPR+5x4mRf3)KPPt#B~I(@zRQj2KPp z!QW};3{vmlBa*$gCuyNBrCdC{sRb1P=Oju|CuvG^XgtcClZi>7EPUq^Om*F+j#VbC zn9Ifn#wOMg2QtrstLqWgOy1&-bwlI|aS6U%iV9FXJK^c(f++ZW#<1P4YSv?ml#P3_ zykoKHI~I{_pT#sIR2NkuUsJEgv=J6P4@=gr#hfLheuhOGa86E`9bbTbHP`&cc{THs zx8v&*SMpcz>glV=j_CKLYkK$cAhe|7VerFpn#afd2hr6G9@Fkh$YW+!Er8U3F6|i| zkh<UG-v%W`ziEB0qd7i#t&B)w5Z!Cii2^lC@BPgY1^-P(&>K+aCc#D#!HB{|xN5j2 zOy%!u8Phfbs6ty%`&4kQf@En=PQ${Uu<Wlh8@V3{vbaT}=WSFNnc&Cq%iDL~pGMAj zAZyJ`W=`8<A@nn>rcSrJHf^VjYN=sPEmHp(+gd*ps5cSR-IDH5K1$xEUJ^C5w*tC| zTz1}VO<kBrKs#taUeuyeDc_jRH3g$CV(^=E59&tHaSM$;X?DneRw1Kv6xsa+W>yU* z{ML0df`wJfL<dj{w#}i^JAT8N=3A=xn$7pGMD!5&2Fdh-^)(qdL&D3qW1De;J{yIC zVl;dg@fzi#WzSVB?no@58-RMJsX1}J(B1#2kJWr$nNSMe@Arg_xn!ptq!$tJczi3l zFrYlO5Im{}R0)s1xDLnEU?6P-WxULLQt=a{53ROc^UZotq=&Qmq~!<#tz*AZd@x_a ziIpa8@zvc~k)=-SJXPEoq2Hip!a2LrxP0P(nE#6v!Gdx(zfud(6xJFVaY)MLo(nHA zEW|3*i1AaBYknj**NurH4aPW2^AyX1rHE{PWbsBEZ|{S&XJuP{IGGp<fg~3{>@1q9 zkH^jSpipl%U0{>5KWX7*(S_-z!jOu2A_3OIh(l6t>Qjby1qV-iShC$F9}Ccz2spn* z{`ywyi9nv#;`}JbChOeD5h*t4CcU#Ire|rq1ske1SzrisD^-AwDM`zY#IBoJs?Vd+ zs{~N6hzEV06MUHQB5kxjH00_zX2&Px6O)%X&3+TUsY$%@jV24CHlDFgfnTx|$haP4 zH}j!u2)2IEJEbnWdoD+nCAuJkty*%zVb{t-btV9kZ?I*-4K?Q*$jo?zQkqwAp^@e= z5r1hM1{nBY0*;W3yYVc~Vr{BN!&!WYH~_*`w;tz6&*SORKacmI5iyqDkhA4F6doxT z4Ex8!%b;4Q(t{)>5RQ8o+x(5;YPbpj_C4Cylj<i+2C+fWT7fu8cA=Z$FDp~E;K>RL z&<&SrS{JTexC4Oww*ktn`q3x=e43}~PhR#cj=i^h!}3}qObpK4Y^E5ShfRLKOI#Mn z=+N^F;_MtN5h`oU^EzO5(#M=MeE(4w#+XDbX=$R0SfpETx8yr@!I4vR@QtLqgYH5! z)X=qV8)ViO@@h*eJOdhkZk$b5u{mS!Zm!LZZKbfSXEKh}KI2<*FxaNk$=oi-)ru#) z9@rPA<JI#48OR@_RYbA4&(SIVqsaV@ec~~d7uJLtduyBHwR~%VPp178WAOSbG)MMj z`?8ekvU#-`3CKhCW_+YB{?Ban9+kz6F~|P0seTS~-3nVDrfOswm|VO}CK3r>`@I4X zVrNou9iYh_41^WbdC4IdhqZ$=%09<vKPo+R0-a+z@ZW}pNE}V{BjY7ma=WRyMz;mj zT&(gl?7~H({3!c*M-7m#GDT0p_I?gZ#{(Vop94h%j%q4)ZzEM%UTjB_je$OB{5C{3 z_{0Thn-f4<NIoIA;>7%fAv-U3m<X(wN3gvTA`lpGhjKHH_Xyj1O`hF81j7<rMuwA7 zVV_KJ?qq?X@=#X#a56hm;Fm`x`!LIAA)`^4WXhvoAWy4#S!wi|_JiSUd$@>Gqaa`z z3T2G}>8GLF<6L%8{j|PkB-}0_g#o}j*X;16e2~`y7<x=pWq4XQxYpxqU2&UuLPQ|G zPjESYk#oVMxe-T57dDo>gX(#o9o2WNMGE(E#!6wLa=-9FB$-qMQn#D=Bu7G6Tn$SK zT@a4TGih5(Jwq`Y0IH~g`9XY`q0uBZ)Yz7T6q3sTN#W0Zh*Vg9!CTRV0e+-WB$0i` z6&HpR)`KW+LXQh+6nLM2iFv%qhr;>6eofqN18K4rWsZS3ShgMT^_G$^8rAv<7nn3? znj40Kl1x`gMt&_!+|=PYQd@Zkbi=WwnS4_7T8gr12G8YSTpLhIbPI|NL_*4YW0Vrx zOwmPGrN8!A7Y~3HZ|}*)TtI~l@@pQ>2+0lY9*77Ya%x409)<=C1nKcI7O0^s{Rjme z7;|kUq>tc|2cpy$wKm4n9PByCqS9uP0T=VD5sapZf84IdlL%+jFC7%W^F9N9A2 zu$?RN;B!INn-zcgByJ7cla(KW-Dt*@JR1ZTDP-BXSjucuWyS*3iW1ndL(e%af}cTy zU-L!b1F(+?O)#?X%ys2U@xm>=MG0x;fj|eG<hQ!*>{ThyFfjS;;}i|A@K~5(kkzzF zvb1U$A)HI0N)ZG_ejHU1j4FFHCzE7aOSI&F1RG&!hci`Z)eOq#y_$GbGQA=SE6BTh zCOg5M0ApXFV3Bl@45jdiKn>GrbngxMrcF$2SIvh3fuj4g7%YLXqpjx$CE)MF4|$>; zkkd5B$9%3=HqkT`OvNE)PUnUsFdczC9P0oQjHikUB<5zZRK*GF&P^PpKnJo&JIMDh z!1`l=(D@_m(eFo@`ej9)zFV)}G6pcsM<t05SHh^RgU%&EYRGX2IbC>oct-cSFd}n{ zc2w)1gnMkAcg^d*=!Q)u;EEgppTIPEN?xd4I0JvZYd0g%*|hNh9B|U`w(awk-~IfD zEY4?Nt~=nz0PmEZg`#0ozFpq$W7)yp-pE)&G$$?dg<9zH8rG|PyT(%k)7rzpWi*@q zh?A|u3oI;H-y97Fj>;rGR_LyG$d`1K>|6oBu=UOcdt!gmN4*CIcDpzxQ~r!KrKJ76 zT+7H0)o6E>C}(4EGp!mSo%BSmh8(N|&VAJmGt3E@z-O!f5yq*PWu1E1hNHlm<L10B zJH1f%_0WmKZIZydRDkPg+<+z7;D7HLBm%g8vI@kAmPv(m9Sp{bno)LKmlYu2I{ozU z=hK%|=zQQT$RYLWWhR1!5O&m|0i^%e?wl2DAM=q$y`b)xjiqj~3%Fg;1tQP}o}FS# zw@5zaG<e;@dgGv<zKvk3vo{6WSV9Ro((Rz4ra`natp%9S42w&%Iyq3T3jnTi0{8O& zoB@cCPrO}Mle(!H7|g7Dtk0Yvd_9q?z2*F#ZK~FM#kT!_T)hL6W?j@}o3?G+wry0U zZQHhOTTj}yZR1Vbwz~SpjqdpR{DyV*jJ;#bu|nJP9kk-r@4x^-?^3B+Fa)cyRgVcU zhoo5bVH2A|QY&n{jWl5a^_bMXrNU7Wys;;*BHdY~(1vJk6*l#O7_Juu4{n+9AFred zo_TeN&mCO7BP{OVCq{}Gq@oW^pRp8{S-vyoWL-ufLdqWHa}tI1ik{07WDOiS#$dp~ z*kpLs16ZJdH3`pb;V|f2O36kste4XB&qFa95;YLIW0FX26Y!+WS?<`8Q=PhFshG$T z+D=<xx=enoRroS*nxBD@@sq;pnC4@KHK-GcD-RlmR^9}B6~*RM&uf`n2)=Y2<k&9< zp(3hx!HSm`dUcA<cP1xCtf8vfvVJ-8M>T+)fpW7G=YW8MakAekqfX02#$%+RV^e9u zjGL3L-K47qePLlH%a0G1MVRN`ly|IcUD;f_zlDvXh*4B54~qe{jtSYsOSp}fTv;b* zq9I4UZW<x2LjTRyoR6Ea=Qrq%R)i<Un(>yMKp#13@K7+ZhfVdqkY(pfMgL{p?;P_y zVOHj^*DNAc!80h08SZ!EB|>nFjQtt-a%5sD@YXN`*yOaKp$dVDeQs2W)$yR|=Kw8u zZ?4i)#01st8^x&P4iml%Y1-!XWNq!GbM2R+qwY#%VBzi`S<K_TEegL00}e}lw(aRf z<4de`%P}G^MWkv;R&OFy+SSZG2)TPc-@rR`4W_ul@h%XtZrJO%Wgfi<G0!W1rHDd^ z%Anx}16#oThv(Vq)4WOyoILD00Z0Ka8MgvFTedhCl|16Bwp%!qLqpnv+#H>R4Arwr z=j|MUjo7SD8r)*V<fFC9l{(^;2kX%ArD2?1B`|ko*%%91&Fc&@_2-<1zR}0k<S}`2 zV1z@qdMpd#Zpb25E1=mdWHIUJ<Fe$S&afdPe_X|vx)uf4<RnrA+`>n^ho0{+Z;<f! zIsdC;_<0C?7@meIE0KCOmCVDLt*2V4oqX*_$rLR4_M2oCZ81BZh!JOX**)kDsKem6 zMw7cP3YepnL;s{cPfckl%b_14Liy-l16m*!ZH6rqkT-Bq>gO@a@2Z3JW@@5O9F;K1 z`3zU8KDl$FP9{z1bgs+tN5;I0H}04ANZycFk?B<VZ+&$xacjd;J(t_QdJa&&Pc4{l z?niDmbRN?XRpAP3)q2&{^@r9xgo=R*LUk3YbsfltmiC={tlCC9$PTUwbz(9jNxj?h z>yiIMDp9SiiLL6yBv)b#!X}o%uNt`uwl4?=Mz=joE!g)+Z4P#9M5$b9Bv<%UK~Ha3 zg&Yq%zgcxPaazU4Y{mZwJa0+UY}=x_oSieA&A)%C?M-&9?fx}l<-b}(L?VX1--d$U ze3R*q0nJMFgU~Q`qrxr2cpM652^c%PZ$cqhk~?5iL#%<{CS%9CVUbW#K)7losNEZM z+7Kg#*#%}&uSVKfC)1=ixp8Po%Y96|Q#c3JXe=Q43@(P>Ki4>P{j~A?S{|fMkkd3V z;NseOYK7oRtX~#YDdKYXx83o`*l+gDBzsqYlORXtG~K1p6NHnd<B@E%Y{q_JkfP|= z$B&>`d8@mJ6EYf-Gy^n>!*ho0HKewUSps{RT~c|^wfAI*=U`>#FMj%iBP8hn2sjdp zA6$~U{1P)mddY_$oM747m>DfIm;)_Af()YV-(92neFE8TttiP=^CS=&I0rr!b5?ze z7w`2VNG$$I8WjQFDgsoG?bK`q><k{voI0=>oso%tfKt2OU3PYWQ*rPwoVVG{O0e*0 zm<#zC>uM~|ImA|3LT*X1|FCnJkw8tSB6IZ=WhYhu|6&O$m}eJF7l-|;CC!#CGcxKu zpD!~^#0nZ{=>{1H7k+R#IJ-~w*R6??M~XLxf<dLYa5W>lOcs3VMu=MkGfeKv_bGjH zNvZ*e{uo6q8!|*V%vUuQxY<zBolut8`>Cjrg8BZz<kLI~l;M~ysod8`WlC&TjLA%} z@K=A3y<51dp}*lu-yn{@MIHPGfQl2>GFlL=;|8AaL@IZZJ-tCg<Ibw?xcIR)CP=<% zMQvMRL0RlWD1yB!{WEYW$7nWSDXftIa`tSne6X}=8wHsWE(4g?)Ad_VDJyMhxIj|v z?;=gnuKB_Ak$(02SsV8iekyyP|EC|ZpU6n@hnjvM$-5dRKA4;8WJ>?As#OPmv?7bt zTP&mos=}$<#wr0fXZt9b{@(d!&w_l9gNsZW0WKK!rM1x!6zK{DT*Ta0)L>yc%MG{H z7_l4sHiauDTu7DoO3;qDF>?%h3VUYU55AfUc1)k~xdcrwNm<F?>1NMEZ^ThXeut^R zmRml37r2>>`Vdvcwzw6#zWM&c#~fk9To~Z{ouTN)ZtVmX-GeI7%@3R!tsRi%xqt-j zg82Ni7aS9jp$rO+qwVI>^RtGuiz<fWBrjP5?P6Pk)b_GY1cU*|?_|pGJf1EqZ%K6R zV#&R0J1l1v#Zc<R<uQUF7;PTBLuiYXwN+N=TC0avh!y;@xm0-a0|Xa~qXNU~1;+or zPx!>|iOXr!b2&J_e=Ht)ao89d$X&3y_nP32f$i72G|&0^pE~7uk+W@2bsU^PFb^6L zs1bakm}jI>fF^OP3xRybzQdQPG}G>j_<_Bb9a#eOfsQ<J`rAow-StX%Nk5g7mUDob zMxdsxsW8gQEx55bf+37oyScMO%+n+=;}9kg6lm|0$`oiz_v)LOPCinSg)<C`=up9d zPPiNowY}3V93HGV4ydA<oDEi(SZ-Cx!T`f2C-?(*=iF3@fRaXcwX@v@W8~aIF;;EP zdm0*&*8VpwvL$4HvM4PnF|q&(TGC(zpT509v$sgKj6;ok7)8C}r4G=*M@=;k`{Te; z{%+|wTm&%){bA>aPeMvq+zxIuoHbMe^Df8kUX~UFqjO)1fo;4oMYcTkAYKIf-o9QD zeHQVVIxgW(zH`~N=ZN!JM223qrr;Tl#<e`Nkh)IOQOrYY`r&oEC`*h`OqAd$IrTSF z3Uwk@bN;F6%{iWSoB=u9<X%l?o4jWHzkEzrmGF{zWkbURM<dW)<{d#`(dlH~i`Iqa z`@-5PvU};Lw%NDW_n4<W23%Nm3|iMimlXfh`*LQ^BG7;T`p*2DDw$gECrCW9Y+Kd$ zRZpm}cH*xNjX-M=N+m*>N=M?h^<yhq*nk7Xv$YGF%{x7O{wZT!n4)-Cmi<7==p zU07c_LRJ;!t9|Y2fH;-ku11D_ej5AR{#~n{;#Oec|Af(Wwa~Ia`QDm`MOKi4_dM!< z_g~kRk14pE-0gP3=i)(!%i<A(;POI3L||Fj?w^Yzgb3d=66hzIo;{8@A2*7haaJ`U zYkdHV$6F#^$0-Oxpng_J-)_n~)Av^|o~icg;tT|sMjz!P!^Ie=iOWkx;$vYC!bns* znL~0X>o+E+&*ud}9K$`I9j-3v=72WZFz{j&QrGI;uMr)D_Ke5S1sS6~WmS&|d@=}n zV>>In@(<rbJ9`MX-ToLJ=TaM_zk~6%^(CxM6Uc)5Zn#`Ye4&aA#8upTOEb@HoWPfZ zw+%%&+xxeinX^Mmwb8XAidFr@y~3H*qVIG&=;3C}{7yKEwt8nzG#5`as_6;>!PgIM z<x{((V*_&)9<gf^-qeZ|;CiwJx5@rq+uw*@LQ9rRKONF11jA$*-q>`cow~uYw_{U# z=fQ0`WdQ?yo?FytM@!H*zZ30T2m;7W(+KX58_YTheM2}T+b!+{x)hiG9!nIOD1udL zC$D2nRWGu@I+bv{tLWNj#aAxns5xjY?u8JlGMMq!(hE(QJ$Neu2bE<(b%%|d2uJ&| zfK7#t=GT}LkHfYil2uzh_X3k?%gqh}ZE1jp!NA0u$7pmY61AC^StYQ~nZV>^!N{LY zu~)ICj}Y#8_6$vLn9rsbk?-(SeI6~vP`#6fkvtnRjMsiYmfe3<nzn&O(-ZsxeJBkp z3z=|h)?Pk*#x1rAa0RS&m|vvOic~+J2AX)X=*)tlNeyikJFX7VTi}IEir^knw<nD8 z`mFJlMDWIMmG1r0K|JWauG!QIx{@w7L^b1wx1q@<<DkYd9a@X{g7ywFEpi&vF@_J> zJ+CkM^>2k%q7b^p<lhR)4M15n0AlV9tfY<Y#Cn}b1MQVGD2dGBfcna6jwVXf`(r(< zBc3M1)uRigezjELvSIjm<uuQkRASl``{RPpR_79_thX(SbK3h*W4#wG56uke`p;Hd z%1gbRLXeFykawtYNhx#^0MljM$-x<_wMznbifvC9`D0yATUNGlZKh?TlLX_Z3rUTQ zz^Mh*T?Ws}?+n5FlX;b1wqKO~S$~BY3eU#cEul;OpGI~r5SHP)RxD9)fqm2brV&Qe zkv?dG$eWJ$SL8+lF?^J<>AY5>aBmohA&TL%ElI?z5!bG=&oI4b@@;LpdEk$OUGT0B z?wcutvG10>`=Hq~8pe{%-7RHeza7Wk$R4|V{yhlTe2NEW^v&BGK`LrF;=<aW-&{h_ zkfq2E5rpqGrp{H`PNF8g?M@Rlm4Bj-35b#$5pM}Clr^4{(rhdpL_6OH2FPA=eziR` z_OaaQ3KDmk=ZzF+QTSqMA=d0qcg!P>3;Ad)PTV`@0b%Z{tWnDZD!+m$QAo(gq_yb| zzPSOdDMKv!GeWmK@{%BHyWgKlMkxwMr}xK|k??%|Un+)wO0D00GC2qqYD1{3Gm((k zk&R4zjc79ed^x`gGE1X+{Wmv^&T*S;+G^fl(@#vOF25Ea^rP3#%g$V_ZXfAan6~Y* z@yV0YYW*Vg(#~{`c0fgX=^uWjui7m&0N59u;6EyA<Es8<56(M3ksLJyIhcGcx|kg9 zRPM?z`h8+rB0m>|wWx1|bpv8sJ4+7YtmnV$uCB5&=x!6fVx<$HXpaVqe%Wu#JgAVK zDREks_!MVkz!624KWQxv?|1rS(4y|IV#bWbMD6>nld%Hh;jSFbrrDS)A`uWPJ>K&S zZIu1#wVGzP$-J9TYa3AHFJDd=M<ybrumhJU0zDoSmiHPJUoiy5lUvjQN(nsSdzP-h zJ8KX>{I$t*4u*<gsg@y90(!<`1cu(Dygx`G>Z-(DS&3o|*2anH`@0;3+<=QP=>hdg zY!*1!W5fx|OqT0i#@+_D2!E3jvTa^6ARS^_5>ARCt!pTN>n@?1%gNk@7RgY7JSZyc zc01|k0JwVY;HnF^?%I7~bepE78kxWL>Z!~nP>6QT<r(oqD5EEzzB@|3lPN%9adW_B zf)HY(EZ_-tn4)6YV1Sz?Anna2KvD%aJ`YOE&SKz@oNtO89EM-lti|8S(NlF{Sg7U9 zzGL8k?DYyfi&5t+xCDqfPW%oiJ1X~l(!E=UWfzxJSU)b-3^6^<f2>V$r$|$5whF`1 z<DPKD`qAxH{fl|Y|Gy!Dt%I|rTeG>J{|6!(!yD4J`UeqJ{6k^?FGS?wU}$IZe^}U= zxI+%O-c#z3+@VdO3rMI0jObZ7rv6s#t6g#|EEzh=3!X;ei4C*pUT^8iG?S4`1%zBt ziAL(EueZn&oy(W;tBz!=xuXW_7SmTJ^Yp6;n3=X%jmFo-{;$UlRQ(RU#$Au_McLNF zQ3ecl8m_!9XJD-m>{Jt22>*5{6>8&-&4DxZ9k%jie#+EEsjZ+*MysutN+<iB3RZZj ziQAeeOowX<LIn{{`Z_Ui#t#NHtwLD45Ag{#a9zYKTxOdG$ThWxQTdyH?vghMj0(_Q znO`7(N(w$zg<a!1Hw-rq#=g+>*CL5!q<-N~z4gW=TS^zp?Hx8~xBW9+(vAV)UJW`L zW!8K3G1Y$q{H6)-0OP6^tGo{8eRl0U>-Ui<LA&^QGLx<Ny{cj9O^c;&9@An#SxY_| zXsHZKPsD-5HPY_gl}=f>{=zpcjch~XS`{a<VYDpwd%J#$_)4f&%~_t`B0w`e-gD5p zc7T}%?vL%TZ<8S!e2=5#6o(VgFfI+isF&M60pGnAGjD<nA|a83fH-CqO5Jj%hU^_F zh@Df*i)AEX7mlnir0f}Rb~=zpM2vjjdnm8JON>!-qF?`4NTBKOFlsf`9*8)Hs^{<D z3d0?u0sdu|WI+F=Y;Jr}Lesg0*RI<**$`$5-3jesCh3N=tJ&E7EeMR5jA9uDNByqs zT(O}=2dW3-dVxAY+%VmNTUHjrLXVYMAEpCHoD76SV&ei@6INI?z>+F(C!n>$N~Llo zHP|$AdwhMmTpTsW%)LxBYC^f8R9A<(7PGUii2%?*`-xD`Tw@ekxlS;K8?fZf|1~Jv zk{o6RD6{6R+KpLq#1erRu4tW<z*tTirqcyF_CqasytLyTIWrn5?r}+YJ>v26KHQw_ z<ue@xbMLfO&^K#BL7L2qMBbX#nb5nt&`dg-x=k{sXMsEvDyd2|sQ|_7<Upi}v6G>@ z8EWy0m6g&^EJ6vM&paBfxt%_&s~J?5v)t}&Vy6Le@b1gDA2R<!oC^to=R$i>c}6ZA zXW;gK5T)4a70{;u4$%Nr{#c@D@q-b6laJ>Zy-#NbjgKDwGbM>1#Z@2tLdGEw%{ofM z_Ro{^O6isL#%g52$JQdGq_rUiF|J!tm~6knPF~R<aFF<S8Xs9glS^ziPRCeWP*r?M zYD&<GWH{O)<DQQh7Nddy0Rm8xjq|q_YY)PxsZEu+Y${>30t$>Qn(Q=c`$LE08u+_} zWeGp3U;z}lYw$d@YyIROMXfgwx<Q@8VyYJS=4{&yis7yHG?U%)*A1X4pCmTJ<jZ&Q zJ&g}k1kDher{`rq&7b&nE;K2m<u1Gk;vEwvtLW3qBb2)+=(WJ&)axY{C4(vu($(kj z7lY-@`e4OHU)<3aYXQ2_ecAFIdi~Z+VuSnjo=mP<M@oO&dB{p(OAT;SGWq(93B%dI zxHDS{*Bq&X79k)oc_`I5edY=nv#KHq7<2@QBt#)`gIdeD2VRXZT>E(}IrUh^>YmNF zC7<22z(|b8r^pz(3VF!!a$UgwtujrabC_%}_YGCF%jvr+Us5cx){Yr!2$Y<82k#M) z#r27Z`mYpH#l4&o`W){TkiGtB`O2)uz>85sJ-%+1FrGz5`Jw>EIQ7uZ);rtdH$$fO z*R43$Sd^8aooi7!7XY4aj>$LzE&L_^{(MDr+%)a5HG2|M8C1)(&D)!~+xiRp<YLfq zkbj)M%63DLw~n$sYu?n+<>4FJ+A*q~RdS0H+keUET8#9EYeOqK*H^v;m#0oT*Eyfu z%-vs7opbkYFBdhF&zaiwhOxM%j2~v&$D<{LpxWMoyKp4V$-mwM7<}ah8mg73LS8C( z36aP4RPY?{ol_ZSI1uFUYmmO9;sPyHbN`AbT2DEe=e37InB^nNSu_;T=_Wn5_5i%8 zJd8P8Cd-J1T>v(Zgd^$SVp<dD!qr_=1a@bAHSiGBpxA9Sd({;Hq1Drp@rdjcSQ8ZK zVxT=RJBX1S1JyCL&z0W;fg}M58x?Eyj9mTAoKyckId`AD45zUFcZE(viKPG>M@niv zf#fOHu8cm1Bva1~Kqf);b5%IW+nPY3v~>s_Gw#(>q)Ts>9K-I(-yU(8`uo!X{mqMx zlO++muEX3(C*t-MlJcK-M_(I7D2rnP;}7`%#9Qb128c-hB@->y|4b$)7yJJ|nQAn) z|2N+1|Ee#{h1d!qsnO5|gguXbu4G^x&OSr{B3eq$V@8?`B0ci_^RbOj9ob4Y_I@Wy zlIT9xwd;H5vUL8m>R>8+19lj-{Z$@QuVyupW0tdhLpwm=sTu9UldC{sUEDT(O<Qr3 z`JdV**Gs9B(Ck!w$F};X%DKpTn1O)d-6a|0x_K}9Td#qC)U^!O@0T?CLAZZ+P1PT^ zs#!rFI(EePUn;woC#fRAGx`L`RlyQ?6@6W#udN;EEr&@wmq$iUQ)*h;p;bt|wA^X$ z{YHA~bZCv~RaZ?KCGR9Lv)T?<bWSz4vPD^lw}@#TP$Kc+cCQfVqG+BPml~MgApeCB z1>U|Nz3Xq#$_e#S5Br<T8l~ALA5GA}wJ-bL%la2Se{5ao;JJbylce<jw2Z8M5K}lu zEm-^B6Ja0Box}(G&#zHfb+nZ0w<V}z&=*Fcn&m9R14&><>A?`RE=A4$M6iP*<?eS( zc_{IUGM!Q#(7?oo=>BX%EaMBcKUs~l;}NI2>Kx})!500q#7JeLnKoWpb9`2(W_YT7 zVeeZNW7RHZQH6trc}RQtkYy+Bfw`k;noHG_6bWmDZzqDiQGw;oT8#i_eESy_5MA!K z8DNJH^?R=ru3s~3up0{f+rYly^-+K#wi7C|#}RbA*)LP78n~f|koU$ECcj=Gy9O47 zu3eimO|14>t;}tjk2fX!Bs0uB*KAoV0FTTE0R;h4i9!ttaxX^e08VFbRL30kqn{AB z%g3?HBv1S&hpF5?mYJToQ(O<1mT*8xX{%@@bs@e{Xeu=r$jsU{h-+RK>q*cM7#mm% zJLH;+{~qjnujPJUnn4J={A9(jSrGq9cOA^qcCQV$f0tsgrs7a6LZnfh;zX*;ph=Zp z7IF+gTQjv0l=>&qng=`|14u&CK2`sIPMS9VPci=&X*ieQs|$(W^Sz^<_?Y-Uhvf4t zCU4Hwb_+UpNe4+kwQ(C{17!msE0=P`4oW|Eyuymr&e5A&5~8l%mc*f&t5mAVfWL|K zJ&%Niom&N~JdrFkdx+pB$8`3tuq0b7yU2|6JbFwtn~d~9g{Pqf(#1E1bhazb)l+C` za~TO<80r(4ubQ6<l@{HJK0bDc!~=wK6J=}inl=2ztOO+|R#Onod;IRjaG6c>XECmI zsAuKbK9Tid6jUQ4l&d@dOdoO-{1cy%w)pZBej6>+4dJ3oQ6g#Z0sddjR5%R<z6>J> zM>;SH@wt5#zd(T-{-|^nvAR~Pa$6!T=<X8S#zaFvqZ?69%2O3ub=tY|P<q#0;x4>6 z;S!%kBPq`nZZHP`wSc|vtF{ANhD{8eWPVQfU0a%kdk>`!peje&BFl{7WqbiCm2t>& zS@i}FF7LX=l^w$0suhMDDda9p3F?`bncM2+9%&B#3Y-O;r9fH{3GCT2+FF(3icHIO zSS0<WYJY`~cwmQExxs8Ui|#b=rc9SV4Dos$AUF!Zb&FzL67bz4rc;2qN$A_QBqT3p zGgsdvRgY=@x#O4)n@LlV6o>Xldm7>@P#<*^QiQ-Vau)!H^sVo~#}mlQt9_aSH>Kju zFD+oCh&Y_{m1bP*On$R#$KWTW(^K{vlz00tX-@bV{IN$xyRaL(u$ZR_7O%Z>FmdK} z+ULp4{l4d>+(D^0V0*-g8OOEGT3fQu!P`07^K}I0^{Ad3SJX=9{AH{L9B~ghvl$55 zm)ZN+Q6WjyyKRBy^Ed_mn(6ddty*b#YfD(437{g#O~R%A?y$L46(2ioc|9~wzxeIq zN>roJ@{<zLqd4&6>NyU=F$CacYzl}~VaNhLkLu-TG!JgMN=W~9nPJ#{j*fnXO33Fd z#vY@CWBlB?@&85e#<lSt<-Ds$x!z;@b2wP1MHN7Tv)vMmEbCeUAlGK9+EA#!ZE;Pk zhk=Yz8v5Fc^+??*z0zk6UfqPs@-&i!q%U7cP2@5`VO!YeY8WU?HY|AdU7m~;2@M}6 zs#!=;A3~E)L=5<lx&DPX<>T7WIhP!8TeKD8`?o6dt>CehQHf8=qvhhBsEg40p2%}2 zZ(*F*8|7W_e<n&7*57bYejuQIFyQ}N`!h3ic5yIta{k}7ziZ!b_brK}z1tsJrp1{= z7mJwd>75yiA`^6f6Bk^6KG{sUydrTTloTcqU<4qolcPre&lPpf4_G2<-mcFb_}wOP zknNTK?^9h_d3RD>u~jigboyDf!FhgWsF~{Swj(Hqyfy>xD?Ve=Qfc{$n&Ygp!P(wE zYRa}yON}PUZ;^LNZ{yS;u`cfq7T2X$^$q0kLmD{s>7gn3aW~aVU4okWA+G@}G&NH# z()u{3ef#F7vcA2Tt%wvhXGl;Uo8j<>hm|eR+3x+?JAd_g<*$gR&JxN^za(9WW~aid z*s3fsI)gmg?wGOrEueSoI5lk!&&X?;KdNeRX2v=;O`3UXkwv?5i-sgskDqG(DVwU! zl(6*e(K9o(=F6q=fwI$_X&McCgURcgZfL68KW5`8P1gs+{?z{Dp@n{Ypb#k(rw0r_ z+Ax!I*_YsM=<u(`@00<B!0^*9mKD0sHEF5>Y{8FyI?K)4mLjBn%~BVZlFKIQWUr<) zyHX|@1*$Kqg|!x&#;H+zrZ!#aZ+A*Plh2JbZ38^>(7z5Yd1US1FPkcD9-ZjfcR-T9 zuCt3eWnlYT_BwZWflA+2rYcVu0kA*bdiHNq{~p$KorjGhc!Z{8Zt#D_dOg+X3;%LX zlYB+!gGfB)XvPsRUG%$teb6N{))~gUZ&_CCv@G_oWph}+(bm7gg|&e`qd-WogI_VQ zuL=46e9&m?RonuO`96ur4Z@_o?bYy`Zzr2~r0N!rSNK$OH9!zg<y9YU?S?xm&24FB zz7otw@VI5Y)KdGgM0_uJ{uJ-5lNn=W%1V7{QkCeViT7GPj9N(CCB6?TDcmgTNIB)v ztMlRWl!*?O*_x&Jzsf(n%xt9ec6)ly>d($9?%cPnQP%14arSPS=CLGf$a8eb8yrmH zoeW6ZH!V3SZzQLaWs&wv{ZM<3^a%;vr{7LiS?SKcLpBupg#K!pQ4g*g*i`Oyal)c_ z0Zf<5$l!NN-SG|EU$#0yPKZ6NiE8F^&7)m(n`3_=c(zN{H2;{d1^k7dF>%#hVNFu8 zL|woPhIvv;-Ut6?(NiY_JbPnonE_`!HDy21Fg~;n5!cu!)1rw^bf$oazw7}QIE#sK z#i(5tMQmo{5vALrmo9*ecsvoT<4#H3*+3Ud*iIK!(S3aK%xzMI5(l(gQlK*3$uEe= zTDPn!C$<Ta)hX!rd8U6Ik90LHan=<Z>!2Galt^8WLDsf=I7t@-EK7E0@gbefC1e)) zAa9%2bEGcdq#@cgSe}aefiH*Dsy(t7WDhBso%2;<fSKp@cV!d^*|rUwO&wZ%y|aA` zg;WTSWH>>wj5t~euGRfPX<s<wU_2$Yh};<^noscF<kH*&3t734)D;zgJeAs=I}{v_ zFK*V@E?IbMn;{D_w^bEaW&TC&lM{r@1wMn7JraJ>M%`}L9B7ZD#EfTTzZ*LHnt9}u zT)2ErGASMx$H;}LvSaA4-$Md+o-=5jL-w~>29*>+J{FZ_Vb-{g*JU>nDoJDaUpYOx z^0DIY@30W`q^1>^;kcZ!);ghgu+=_Qzg+eF&oxn!P8spah$=C#*^e|!gF}TLil;qy z-i2uG1(X2#5_xIG4p(B<OK55)JN5C-#&?B3lW$(%u$Nh@L@_qoN_WoSBw^Kkfk;$` z7$^O2#*%oL(Hm9K7?b)@)l|hr)A*lx?nbfoTc}XbX$^N2#Ue@l11v&K2dhPO54VhR zR}Utbo|-90N(YvzG%DGw!$&Xp=LC62mTlE%TTvE+Ouf@4`h=VtJl;O|EE3*%5}2M+ zgfqy_hyx5&k?%4P%>@(5m!o;7@dWz(SG36cZ9zcN`(FNE%k+w7$mm13Ndw?q_2cr` zM#O^L{!WDKIXKLaJvktH>fxU^u1}^ZwHljzDeHX#3-n-%d0xbyQtf-;U<f>hKL*p1 zOaz{pGtz`qRegX0#IK)w)I39e_ycoQP{%RSv;2ikn%wOmlCUXT&6X14{^^O6`}pH= z?B)f;l`J$kYP4f|`%Zr?+s*9CFFvp47`1nqK7bm5Fs<MSX*#GwNSq@)Bt3Xam$Z-o zRPVY~=D<PLpur$hzgF}`G93OnOCbTgt3glJkW=g?t{VNG_Y!akgQx1}$J<%gcUNRz zVAJM3ssZ2f^DqQ6K@Y)zj>z*j)+_N{+ImCUK5(jIu+*H8rfnh;^;LruqAn1P`n)|7 zA^l}lz&5)S<03!jf|I-rT(hO&B{P6w?LJr?U~Uem>yy^Y^ZC49qn!JJS5nSSxDO19 zM8j3KJ%e+Q2<g1WahnxiHkm?0TL_M<%A|OFcIuF3i!knq#_F<FD27GCi_i05BFOo@ zdl7%QnyFG})XS$d7wi@ge7Al$*zV%I1^TaiBgOV3&>g<q&2UC4Z731);^|pmp_kt` z4{3bKUZ}BKhzM}}**t!_5Xe1-@G*e3lH5jlddj%7f3~6t$F9b-c<AMFe;b~Ti0E%a z@JIY@N@#zI%a03tAVEOLu~R+?$EF!b_yk&6?JT}I5QbK}#bd|>ZI6G6d=FOJ##%Rd zCRthSMX{1eTHlnJ$4JxD!Qn0qPStX_he$1d@k$sXQ{BQ{b_$+VI(v@9Bo&@6jak%S z2!2M0!?VRTZDiq`<WDv~hICb@SN@zKcdQlLt=KZ%b69_WjBbf1@FHK_4p99EgCuF% zCKz<}#fAMw^~l6ub27aSX+PX5f(3mma$!>3dkh6=Bw4HU<^8af^N>_N!L*)pkD^_V zJ0<nMdrzY)p#VL+s+npbw3zH_`Gh^=qqvHC(Dd=_63aF9C!9*-KrY%-__%2gv^tJ= zivvcHfN0GHJ&W99l!D4!->;YON5MYN?+&eU!}QI>;iQwdME;^Ddb7mJg25a3ww!!> z%MQQ;s`KS5pS=`aWiEE4D>MDn$=6s5gCdB2Pn<aw3F@;mUxRl+G;pF&$b}K)zu%7M zKeIK}9&W!K_8)%CY_4G?8K(m(Mz)gTD^U*I>*CJ6FX5iUE4*eT5BRs(P_3~CcaOis za1J&sgAVW~qsBk1IxZ-EbGRCh1w*K?rb^oZKhxzeumhkwnOK=#v)DBtY{yFOFDJ&+ zO};rJ;1+WAK?iWFyPCaw-K2V<;ubLWRU;r~n10$EYeWePLMbcVLl@`7T2*lqCg_hm z?2eG)(sRrVZg24+lWZUFnAk!>)5vI+-0D36H|8X2Uj%>XKbSEziZG3rPMnhG(-N`S zfu9ZFesj=6$a?&OPDH$XJWZy)WMsr3F<BSu4!L^yB7pQ#7y_<~K(%f8>&1Fq_KD|# zyWmL!(QggDFv>r~jBC*fy+(3#%5lIe4-4)cp$idlVUbm=ME|r{gg7stmI}?v_R)Vp zXQxdTv{Cm!wA9S1A4<ExZ4QomLisOsqIY<4v4ou<Wsy<E6Nu}X^IRsxEP+SYl2@U~ zrU({D1&rPuj=UZ0G79Hse0fB|3=>-*>3<_8Km&OdA?bZsj~SMr<C^28SRL*M0LkqX zI<EBSLnSINXpOG9w&(@k(3rI?xUh9Ry~}CgWDAD92653_g05>Dsi9w*a_f{S-k;Y1 zMK5|~2KrQ>W|&*MW#R?m=%F`iysDL+i*UR|_m`ai7mk^m`CqndNqbR3(u|x{MdQL- zlU?fqEWt!%jaUVBu-g`O{9sO$q76h_nvaJ)qtJ=c0LWA5`{xuggLJ=Y5#Jlg`Xadv z`mCAa7n1pYT<nL7jb>9w9Rh+OyKj_)?npQWm<2H~FuW_G-w!#?_t(@J5rjbKIqS_f zm)x}*f52!|;4~L{Sa#)T&WeH5&EZ&v_L5zgvUZ-8m=Rw+^sGR$D~$;5#=>r3#4r;z zS3}rffI*w56z&`ZX9%Z-XHWwTp{3p5Dkuab7@(||z6PLz2wn>Rw+tO|v%jeHl%2LI zEvp&{DMErb`jsLMofBrFmugUoTsH$HT@Dfk<{hbwgLnpxLEH@x?Pa9dBYH*V!M&w! z+d^#>)>>ByS&_Y5nA&0h!JDFud&mhVx#o>RCJDi1tm=LfJ`AfHC044M_+Z7<QE1=z zy2|&_^(1C%*yl3c-)~bma0yTT*#N5tU7PGDF>J1+*|UwMa688)wFZ1OLM@X{@L@dx zoEE)iPD|M+dDYW3iyA5pWCXTa;(l-;Yb_m_;jVFWPh3ZPEd6Gla>(*yD1ry6)vWY8 zlFtMNYOgZQIyLmWb6tqWL*Fu6hXqB(s*8iYN-28;2LGtyTi|i|7KRh6WJU$$FgX>p zEzY&TSY822+%y>X#1}=(1w~<}?~uP(E^aJmi3Y8eLlB55jJ47(Bo-KXP(Z%X^}WOc zKnOj0?%zyuu%5L2LN3k4VWF-1?=tFUPYX9ZqFYw6HNti3^~6&XUXUR@Gb~XmAiz9b zsuJ>hQy3;RmAZJ>CFPPS7$^q?=ErbodKvXhp@vP)_{OPi&7yGv@fwop`HH#uVDS*y zF9RZrC3`}p5Nw~&w&DT3^c-uPVCOb!E6!BPHV`gxCOC#{N%W_wl@@X=Ek}y*9Vi=v zBDMm$&a0Ft1g)lGu!~6%=3tsku6K^rWdwnPvmxsFd5vR<3%~T}a6^@h;UYXxL0F3d z?J1ahh6vJ9NB4K-i4AN+4Glqt8R`vY#ko!LR>JF+s0K5CQ`P&~J}Ro7ld-!Jv_5}) z41tc?A+s<!sj0k7MIoFsB&l&C?Z-h>j@mSz4#UUddu-<=Q3KWx4T>94cFLskJEFWv zJtVIh2kXfGQ(h;m_|6%y&Ww9ziJ-_h=)!XIXlHKO#EC?~1}WXnN-idNp{T!2ywV%8 zJ5-S=?01YO<<Q#Gc2RjIrc_$5{n#6T8v><oLg$Q1kxS~H-Xg>1)NFv7b{ccTUsM@( z8@9IE>)B*_5J-d=-d2dui}gT^$ruzkBmY%_jbVtC8daeJ7IE3n64J@4!jynQtf)mn z@B)TM(h!X&*SLbU5mD`mn3;lfE1h+r+r`VMRh{vxf#3KV3A8;s)_`!+!=a5?=_1M< zou7m!5s(c8clbzBOZ#HiGOGuf!PWTSsJGFz^-%o_KEEI);f)LIrM@x|Qvl9nQB&R; zZD))C7v}A>UQX4WFd+0em(fA8z87hSskRDIZKy=o&0ipIoD8gedVS&%QWG2W2^x#U z!z%J$Z9@~`E4KWHJb7d2v#VT}H4$bB8BIvLFiR$FT2H1Nfwz4hs`9de)(-+|D7q&> zSrVFwwFWm|LGw0ti8&ai!P-eEp$YCDC$soG%WehGpbJ*&>AitZZbKrhm9|W9C7M{E zC!ucwAFjlLdG|CW^i(`p?><N}nOYA5ss)B=^?_~>=l$5g;Iqh`*jQe-!y39L9oRf* zzz+qUm^GZ<5ax0BGA)ihtFXUz>%%iRHJCp{BvdD3{K{n<&ayQaJXfkwR+~P{*1Yvw zR=w{ILsDQfI#v`zzd7WEqb9!GNb<Rb^IvCb3d(>H^s<){OJEwWjRHvizzLyF?I1Tx zVtL1sIYh3;a4&_%hJRCOD!KvQu~IOMxVo6?AWQ;kYv_!#XMTUp<s7RbFPF85hv{!0 z4_7D3Pk7Oiml)C#SPd9XI3A~GPfaZgpBx!Bn6fsUYHY-6$|uKWMv6)+$MS~x$*{^{ z%XTU|7(D|59cw;+gnL?|V!DUt_i`_A6}tHMCJuup_6cC6ogRFkVG>){HI9IAm(plu z4y!L-<A^VW5la{eap|<XbG`a1tx(8)`XE;+N3Xn(?zWqkAx6@;KRtodkii_lapy$8 zDg!4q_aO(#a%)`I+*snFk#~4X^W=V!Q7B?;@0<vN!Wy+^IoC&8ljp$1qRY;LePrk1 zVmx#aAPm=Td5>OIDB#I0-CvCU5(T(dib~9^Xa0e(Cz?ftwBus&lT!?$xPkRDE%)Xp zsXm01wIfEM>db)e>8Yddt5>RnVojq5?I9pG!zOH1L>V4Uq94UcUtQBKlQ9XuLg3MZ zcTXG{qX^hm#r5xEM7Zk@l0Al}{ZZ)_)5i1W-PFHA<n*npETeE>1RBk0@|<wYfa8Vi ze)k;k?Ii1xgCu7YQ;PL$)FPikv;1l@>>C>~7Z8k+$j2rQeR_jyYb=($IaI#{@vwTD z_s(m5xX|BLpL=}0dvn)|h1vS9`Fyi=4ZrJw=;iO4z=ZmZX8ce6cDahN&qd!ix6R%F zmgS=_5YxZrZl(`%CnqIh^Vem!o364lZRaaa9<Gn8!5h=czI--5DNW=@$eA;uuw9xQ zfGqxRqo7fp@|<O@h&Zc8w2Ld{8^uZD3T}vFOO^&Nm?}MA@&wci0w{gD4JdAWFRlI5 zMQsck1+pD3P%^@oiNPxDp>!4E@LiV<xi8)ud~7b~9^3lO5~hDRU~KH>(0&sWYRLPC zp8t2y`=`?ideOd{6Ht&J^DycUr+NlI`}fv1vTjm{_9=`sPcY9oB+9TglU|*PrRyPv znWzusaQbOsT5(*|hcDn6{Ji@tx%mXQvn1kcHmgXTZ_yxT<Yv;<@&K4!pSS%3Jte<N z!`=;B*Iv=Z^jRBUrVA4o>YX&0cfxTBhvu$0YD;3wr&XsV8?sMr?)BnX$l7g<*Vo=d z5lH^3oMCp=HP(DvwQ3-?(GjGluPn<C4ZoFK%rH7BN;`(hL(~_llDfT*i6g&SIWE@t z!KqrHzCmTBL1EgL8^PV_z?#91yHxOV(v?dI*h|Nxbj=bb#d{HTE#aTwuMOuWvzzB= z5Q_8R7{S&f6WWJsRAS&PofFTC4a5a*0Yx*Z>{Fo>9rwZz4ylMyg^UaP#i}Kp8D54( zF(b)it#H?M{1yAcH+;Q(Onaa;+1N9)exJ?KtezkV$eGVcVnz6NcBB{}nJyJHn~u8a z)QC+oaY{n>$c_E%`@FZe*Uv?iIJ?Pi20yVd7C)eQA@rIfmMjoFumJMdj!RE#A$o`$ zn{;HAi%G9UgpEA_qPU>TlcYXF#c|Kf9kA_}CTRd+jeaJQ1U%9*h~=U(E8_h&l4!AC zYRmRb2~jZnm$UME?%&Punu>);6My(HTRhL`3hqhOl|Al9XS*=pOa^Q&(V+dIBrQ08 zUy^L~9;{#kiU>1S+f|Deya92ym}I*Evh)xbPHF(#H=LZ(F+nKnhXMA)d5kitn}mm! z5y)<PnF0YZWF68St-hXePU~tj$H=#;Lv@y-IVAHX?CQ7yzXg+L@Qk4t#CcAalWoPe z8+#bR8jDup%nlaf-m<`jr{-@;<+}uS$;(P-LY{_yB|HF4>Oz*<S0_N`6^BcX(9xe- zz={cOn9SUNAyS6>{%nDCHGD6ckEMF=&<BB)Qvay`9gnOqNzWiJ)Y@`yeiWpDKPH3A zqRAg?!_2|&BNeGUQzm6hJx$(*B%tdgu9*4DV|?b|SKsgf;Z0VMK%&iERnz8;p3~bq zy?jWXm=AmO8b44e!s$Mem0dn6!u??U6>AU8G`ncFpadOZ*H|Q&y)NPsY0j}?Y}i%p zKXWvJXC;^6yZR>TgWeyD#yvDpoeQgNBOq+~CIz=Fadwh0)3w+4cBmH(?->bDg+zQW zFxcz-CJ>HOWB_RJwZ3<{Y3ZCwGhbK*ZuZo3!A_A2wUP!8waLhp5M({!;i99Q;p0%* zTJwJ~kRpXAKJ_7uW$pd)+ugfbu{PBv%oEytKmnGiIQ?mqEhzgKC}XRP#TBY6RGtBy zY+vF`)y4z*LMmP9;6H)N$3XFJ@(l7%D01P&hWm)9SZa;Ye|e}>bxN@hwsW=X=6G$w zhqQ%Ogv*6S+nH_AsF#CZYD&dnA7UUmDW5d4R$PMd5&m^<#|<Ex;jaGh4>R6>VO}9W zMMthk&nB}Uahx7<{%hjnzRg%CC_KuKLzXEta@Atmkh^_z7;cZkq^y*PGmAg>JQi{2 z@YNT?E?MG#Tch2{cvFH^hn1<dqV@3rjHf$~q}TM2n<I&O=-Rm~BPzSK@z98U+byvQ zeyUjEgCZ_H*d3^iHERdS+yC0@cgOnuhVM!~c1uSWby^EONdJ)*LYJUF9_K;pKB1=H zwXYV)jXO8vi_Z6dec4(vr#xlUxaI@xc|7{+a%Y@{D>QBI@PZSf$S#{k!Y>;-(4;zC z*|chI6d=PP`}_~<x47Byz7s8AgD<Azd0)Po5_yJW@bIN#Z2BlA0%6~7G#IQ=`F3CM z>uWQo;1%~#EUm7=H)YJhb7#ub3h%xHu(m4r+SH>RsE7w6dV$Q^HH{;{eU%IGE0wqE zvkw<DxBc>Q9|<voMeB+%Ns-9p*$)Bp33$(;Bl1yzQYy$qMY-<=b)B~GArq-tA-U~4 zQ=7XZTQCJCU{E+5Tk6NM5C>u7H+Kn-E2)h4zFF22b?5bzvGu)J0)*sXAb)h9+db~8 zvrt`3dxea$ytqqXLK>eDJw34Sp@cw!OT^EvuB~Y<{jsX;;?Z?Ir}g|P$jd02Ku<Pf z*ZUxSZPIqHK0j{)>d>)Nko!nw`C3P{MoK7b@RXK?NMA23wO)&yNmi}nEzHO%jBhbZ zJZn7l#m=~F%*?phF+}_UuC9eAXR!xOv^k?9qbV;Q^ve1>f~9EFk2dq0xMCJ9^8v=! z)ja7|1meIdb=t9sH}HU~OlI1(f$!PEx_#5#P4&=h#feH1LIEMa25JYTVIq+u9KN4M zJMA~DW?;UG2e;=0RCAFWEyg&VC&#Ui;FbuvnQoMDCi@-GS{&?)ovUrTIN=~S+&R2s z0lP>QGdcgI{!f$wzY+C_nGbX-?)G|Fi1$ZYo`Um{HU6_XhCjnegRtQUY01G`$mn&! zWv3}FvDXU_PmRKS9Q*#bQ-WPKO|Z^wd8u<Anl@y7oQPQ^?yrSxYXt8T;rhLv_@^6` zr0=uIjE3Wqx3qFTb}KfU#^=CmiY$__v+e{P3Q$HOI$NxoI7^kqqkqJ@A+~M0pgU=v zv-Hp7V87#9EcM;I{#E?9iO$F1p@st*boRs;fEf182DeRT^zZr$wT*Vg-h4*F+)+ri z-y3M6sL8Ew1o5A2a_#-1c0>2_roKmJOV|CImN{;&go}ZBo=(wq_#agggkzlKC&?-r z$-lj14M!H9v66T|K3XhF^XJ!{q-Rf32#rbFf8w2YJ_3gEJ~x=5E#@39t0q5?6#VPu z^>`Lg4DY=nxPRpX76W0HV3#*Y&+^jliJ+^z5L|*-JlX|-Vj8;+l=piNp>m|Lodw`t zC&@+J%}770!(}k~U#auX2H^^_rY1-(Fhq9hno{r*bk#(f0x*YHc*DVnXWtEHiK_}g z=QMfXY2*f<mGeBMT+f&L`Zjz_iz6(6B{o@$s1NXAFJ68KTme$Bkfd3J1BDNpw@77L zoAA?pA83pkQ#%0gV~G$33xZqMcQ_kvgcXuvc#2qoz;NynE>haE_GK9E-p(<k7ncFl z&>?{mRnwcqE~)I-izE=&kog^s`qLR?aFf!p^D0hcGlsgG42`$(N_6@^YLSgbXA+{w z)E~%`<J=5xNs6H>_6qt=sS6MuBbq-;e@Mv@QX?25m89mBuZY}ozm_9Y<7QGf&&=Ix zU~!dV)OmXCLzpDZ;;+dB73^Tf1_Gus0>eHoz-U|*ng>{!H4dcm{S5IxNu9bjda<Tl zd2;+?bNDV^P=<<>&D<wZtiVPr(~~YK7RNm3Zi|8n{`2!y5KMS{3(g7R07AzP=*6Wg zycJu(s!2a1VV|UqhG0kVy&_mIgm~i?6U^1YHsN}6#nEeHpVASA*TLh);!-0Pm&8Yi z_zP#xFRkKy=t3hs`*h6J8oZLm!UhJTWp%7~do8OdS$4&*76yA0zm!`%Rl$#v{P|>W zr}O4KH_OkqZ9>PEl~Jtx;ku$*f4P@&*sqRq9cK+WGWfx<6@bF#AM)i_CmcD0h3k2T zY`m`x>wC}iN_hU6k?_DnpQG_n)0Nd-`HFuNjz)eI%f<iw2AqyW=G~ot!@CDIBDB?! zVa10RUf>XlP2s*^nJs5Dq$-(KD$;ha&TkwG<mU!ErQ(=b2yvFYJ<p^?522pcKREbK zQld<0IlLMHv^#dI8N++Y1pVDUMry5j)lu?3zAgU3MU-mn>>U8?cXJvhww&@2vHOWO zE`S67(l5CP)HGwYV_1eNcPZi4u#PRy{Rx{~W^5!T*b6T>xEYl@+8!&*?O?H}xd=Ex zHrqdgcCJhQ0yhs%PaMrc@r|_b(8#{rzc1nm|M(-pD5ta|d$x-XA#-Woh{dO(-^nR4 zUjR>JLU1++!j9p_Rp2GZuPT_cV;5`TZ8qFP<)i$FySvk1z$^hqpe#^D5YB9R)^#Yv z4iAHsMT~vAV{SvbyuE2e_MaJ)3bH+@#Pt4?JE*rA&IWM5tdeKs=vX~D2#j<ar3;;7 z$!;8kP&v<7XQOzz9qrEBlD&bqJzeAX3sI--o#R&@q2P`kN@qT`h5HbJ>*|E#k~Q`N z4bFsZH<XlqdFbXQNYk1Jdc7?~(h*YsJ{kWj6CsfbdDR=)bjDDeXC9A}n9Ta05W*z5 zbGyhERz%>z?WzW1BpF45jKa~)vcV0bPNJ(*&BT@*iCe?vAe-^8_J0X|IEe_Y-ch+~ z#Ab#LJ}-#AQhWc^+UvT?A7r3F0d8^t!sLY>9^)m#C~ObM9`@gff@O4b&_9Y}Bfi%s zy;@!$J)Xf5R&?R4GNKAM<Nh3MJyvEcyqaEX4Z&TPzI=uLVaf>Xf_A0xF^ej0DRgtj zDq8K2LM~B!EsK%KAs>bcV3FsNb&TFDOrj^pUd72bcR<;)nwVPFSj9vsV-KPyS9s8R z2ZzVx7(N8zG$zzbfDey9D4q-(RTn=CAOtvI*xvK3FY1hzAhaMf)c-D(l+i`t+d)Yd z<E{S)t21F!5Ogh@0`ghvK=b0K(*+%SazCxJh2i*F6Hf|Bv5q8{a3~yC_OLtH6^3F! z6WG!N6B|2ng2jCFjOWkr@WnSW$|Wn*f|i6-&#!GAYP#qk<AvIG@-unn*O8cg)1Hu? zY}dLz*+N*b|Bd3M566(#m+92jCNmgas&E~R=nHNX=RCM_$~Y^XZ7ft&k=DZK$lq>1 z3fkN;jpihhGo?q}D{QIGFHkeQ`bsMzY{FIoQsEb0x@L!Gx$Yr3TqxGJEoXn5_iXS- z_y7>POwwco=f$D9`3~_q4!?4GIHhh74>ghb;_bz;t2aYt<t<PxwG}|r?Q#XDum=a- z9KP;sS@xZp@aX;*ZU?A+KT3h81i<QZ!w{CBia3G0kBGCpj3((`VN5P-_r(Xj7Vo}# zU}APs7|hZW76EB-FyOaM;RPMk6Z07{gfRBy)ho}}5RV&oK$7gUwOGVVZeWPx<3&6+ z4N&>pKM0J7oqG4bzg0XW!cGIQ*AY|<*xy^i+uxTDKcyjFNhLYBs%QKz!%NwEhptSG z&L;$>ms=gvYJEG#O!2c31&oss{b03ymT|p%0!}FYdY_+|y_&2)da<_7%^!B8;LiGg zGbJ&`*1_-BfuH1E+R<L{$ST#;4x;u3)b6Zr`zV^(S@+w-sOC((g&j_pEx?<Op5*^1 zQ~AzchjGie3Ue-i%bz8qK;YY3NXUsGbw*u8)?Tt6$1)wq8ci~ZxwnT39L8f9-F_Zw zmv8mhKPUKm;m_7oyr2Haj6+|a%8Q4b?lpEGXw1}?o3jUpPs)Mn2PfBiqc3h=Sw_&` zR!J+Xr7)X*cw%9nj=;A>Msro7@|!$I_Togmh9ca<CmaEq6!*8FkDxHk)Ig5cWy#?) zdfHvyi^|=I=-6yaC@}Z()u3IvIm~*c1n|QU{fa9WzD)_@@hi3el5eA}3=E;GXCxHy zu4Bbg>($^l>`B4G{DBW(y(K*CW{+QlkiablI@7CZ<aICKz0|I=(YO#PanJ(^r_;lx zMKhv%S1ymsv_f??Izgd449N796rO0Sx43yco$A!WL0GQ~nO_<|(r!%iLr35EtcH4I z3WUCA?q%}8wvWC~k`Kxd8W-Nmec^L>{a*lZK##xmp?$@|=qu8pi4RB49T#-L1<YQ_ zRkkp`yDQAdwz_tNKPd`Kn>1Bu8Wz>APXm_F4w5EPaC#}!Cfo2Fw44c&gJmwpX)OFE zv(-4fBXqs|gq-$GQC+2l!DT(-P|DeC#1Dyu`0Mj*X2j6q0od-j=96LUu1)NKw6JpR zf}bvFW2hdhpa}Y5V=4Dz>Nh*$IXCenb*bM1tetKhVlUM7UY+INQPXy?(aU0KgBUx7 zc3KeM!gP0##zzj)_=qo@gfG5%T0QmMwF4^s*RUGgI!)6vr)_%1S#j~3-@1OhSM$$c zbCs8}2{9s1<gpuhB9A%pM1J_Fm~ZyN1~aD-8)4=&m%}lopv_78k>mBkM>a<GyRf<P zV=ek$S5@(5HO3ISYNdT551Es_2`i<YL8gb|53zf3$T1sQIycdn7n`T_|K}5cpdO9Y zGtUjh>y=;>eR(W5T;d0_R`Yy9=ZU$<8A0??NTvNM=T0m~g-){(H16iF>;v(}%^S-Q zs@en)ybjt);=Pt6-b_L}Z+AJFIDynHic`7<7KU-L0F??J{3bZqd{HwF=YS7eDB7_D z-M>RMd4w_HIqASqQssZPuJutYpzl3&89=lRtUn1P_RPpXM%<0KVlheqL|>j*>117U z_EmMd&yX-+yxcY~EZO>XUIL@jv@Wtm&~FIJ>WycQ;(E>m!Dm78R5~0W;0d%2Bvz~4 zQ^U)N^)59mA>eRV%z5EE3fuIw#0pZt6*b)ZeMtIUnbsR1&Kmz3!iGs(&RtTf@-{K$ zspQ^+?*WRs3a@z=R9wlm+3_iP$b&QaKl&ws0pN8Nu8p@A>{9=`lX+MaeYRWmv-MT5 z`lHdOV;v#)4$Pwm!{;A0!258^x9Rd~o{m#H!6h?H$e1SOZD!$ze5Q!L+RP$}UyaBz zF&){tVPnI~V*Iwtbde8TIU^dvB2|Zrv3v0o9qw)_a_nCh*!pGAT$K4CVb1YEB>tOw zol#U*$#$+U=%Q<MGP02N`o6H5>;8ggN`afpfg*b_GuGkEm-{&nBge^0jIc*}>CHY& zju(<h<lAkwX@-?AD(r+9!XpkagfBih)YCC7_;`B9(FW{$_$Uy1D1O-J^q@vD*^3Bg znW8}!NV$t|?M|O%+-LafcUc||MN3jEdx;{b_pDk9D?{}u-MUg|H(7~_(8K6DD|zMi z(ARYx!6c9MamQySK)XxcYlE$mZ&b@TXXx@p-sBsPN%h%tK<?XJRn;3Y-C6nG(^vQH zuiaIgAzfdA{mq!LJ^HQT`14cu9Dkr_sy96ZkeE`()iP6iJ-)A+O-damM)uRz^tGtU z6soed>Z0LLa8)}ra{$(&*$%4n6JAh~;hA<FZmp|iR)9*jD2IXCF$qjyd%Rmk6^Xo4 zVIuvGMTOKmmR6+OgQu-|a*sJ|)pu-%#w)fU8V~A!a<awA7Hu5AbTO=n?7)0mS#B3b zyAOMv+@*OtU^nIWj{gv!vAqkw+062Oi@}s!i>liA{XV$<kY~kw=#U8)u#u%_ge-mY zbqxLZ9;Zuu^vH?PPtLAP088x*(!YAJRj)o4<7af+TxJ?Z=rX*Au*=lyD_=<$lr5^h zYoVp5qTj3g?A(ogrq7jKbS4w~o<4~msCZ%mN|$zhPTbxr7cSV;afvs#$6hZKL2V4j z8nLNvV=s!PMZx66BUFlnyaDviW=^D%{03w!S7WMfun4mfHKi$!znJ3i#iq4n$SCdH z7si3h?#QJ3woCAStMlyN;&7-6b{=~bThcoumhier)qxMoCX!C0Gmw}y=FB6PUh1Uq zj6So$+zJ>Uxrv3H%Je_JP^a<LmA&J1f<_f-w#zy2_RZOF+Z_`6fWMJ#CXm3uJe+UQ z_|oA@8AW@BBTx0MG@AYc?}riyk_tWw+*B}{2J-7;J{1f9{lpH9bvv%E{+7*JVQ;ia zq#$kh-PE`e>ht&yu_t*1L^lEC39n0~k*S|ECW(BssB9-YgG#2M4XXt$ZB$bQXmP$D z)3j}K^LxA%3@;G#VVt$BOO$c5rT*2X^%9&Sgs<^A!JraJTO~K^Wm-}WGE4{oU!;L; zEnw0GI&t+1Z?xolMdYsc+;mHL1VA$)fY{am-lyqz;8dJp2w=GWqNvgkgqQeyM;N#U z6xoLWlxO^=2aa%pN>=MvS8u5Pkeef9q*aW|$)DA=W*Of@Vi{_Q%==8V*XyO4W2MY5 z$xWCx@cr%P4ZU<H7glq8`0J%{J&ugpv?Ko1q@a<W=rB6J$)Yk^pw_}!U*R`}>B?XL z>#bY$1E`EP$%nwcMX`BkfJr5bHD1(~OZseLi7BVoxXBw1L2Zmt2L>*xI|B-xKF`Zr zNgj5iv52l<v6|iR)pK;>aFx$)vw0kySXnd=xuvTO{H?1yJdd72aowxf6FOxlUw9-; z5wrg!ansgq8;bv?lWLcF0i60PObHOWV~#|Rd~;W8IQGxv(nqE<{6DMa8539px5ejw zPBC{)+BEBBhR0PU+&wCAXO|X@b;DuPuCLGqg7*g3%d{}yK(z+cz&MMk^J{K*5=YSq z0aX>{9zM$Qgxsf=N&^%XsWD?{4y-M+V_mN5Y>|J+<{pN4*<!{wSVEaLGub|E6e<x8 zKP)de6KT}})Xs5I+yDh|0`UeK{o2)SYTS)?2+#=$jv~N?9<*KPE#ToILE3to!wN<{ zG++o8-9+nMNw@RD|Eb{797lb~P~VlZWukuW#1NTW@gfDvSNdpkLjs0642=Aet|I_C z{{`Ru33IysjTYak#0-Ze{Idj4wj{<{H#lmeQT#VRz@P)(KAp&zb|e{@x`|O_tK&%5 zLXJeu0-F>2^){QlavTI<7=vNpW9AHbUg0W0A*buI%x0Jesoq3{zzj*iL9cv|C#5|@ zJr1b~+=Hb|8H*~b)jXZH=(!9x5MJX|rMLv+bTz(_>otHpuQFU;IT+38-ZUCvxlyp% zHi`gatqXSTFpHTR1Ov{FiL@`wKJA)={6h9aJC~;|!<O0Tb<8=PY)p><n~GD=o~8E7 zfvI}pG{{pG1rc<F6_2!&7*m|Ud5ca|PEBJ#!sJ;kFy>tuN8e|L_noG2i{^}Z{yv|t zK}EoeNz}b0j-l+3cpbzm<Ha|$Q5GoFcxIFlfS3XE+F%LE+O@(st2u~@s=PMkM<rPl zQ2NgBXR}^m&}y`J#0)1n0~bznjniIZDLFWJTP%K3YIli)ZQ&%q=!X%9`<mUAjWu3r zXFmTTdMqKpzd;pYc7y9d_r3$q9Sn%dpQ)Z|{Ih6u^z$#jjQol~nNYWl*Ak>`Jn<uy zLlW9TZFho6wU7|9AmW{K^xJRJl|)v(9Yv7^YF9ZS+%a{jyRTr<xlQ+yP@JFfqvRBA z!g&MktCalG2_U20e5YILU3^Hhy!SSLhutA;;X6AD40DmSFrYyhYnL^HUoE?fvI^Mv zTegDx*s0b2+CfS^w&~klHb}zW*qYl|^=#UAaGA6)DPY%Xu1R=Nyw1{xiHO;9o5;b@ zXx4u4JS!TEMqfWNxTBlPz#i*W<}!#1Qg`iJOR7gaYfA(}W{C$CO~cS~M+4ZYLLCDF zg@l?~m3fa3l%8rKFbQ56X_t<1VaqZnowir0E7=`kX^;Y=kTFe>W^AI($2b6xw{Z@W zbzqB{*%aAl%L#k7S)H$1i(i@|pJn|2c3$Z~IH4HoIiDda=e*sv8C^dPxr?0GKElUT z+ymV**}x)^71$^}V>AgWtk_oZaZhr-KT3A>K4>pKv}X>Q-km9OM~5~b{}D!+2#z+W zvZCw-zmud#cm*KrpzHJw;m6Ur+?@h1)IKza{prSOLPmz5;=L@mXHk@b5q+PPmI?ch zpclpt2-OqN%$h2U+M<f_UJwh+)Gk9|eYku&(6czlk;fZD<Z^1%cZ8NUDpFKsx#DzJ z!4f)Al*)PKPK2zpw2(K&V*0IoE_DnmjCg`D)(E}_Ld4|h$NEB17MZD5^Z9mksgAW8 z?y?HPK61VfX1L=vLh81oMHT!AHDggxoA_y$k<ipR)K=aJ+K_W_8T(@q7x$oTx3#ja zaki#1Agshf3v<+)x)<<za|uoVgAbD6&k6lpW+`5v_aU9N1>0O3To=%!d+o{UF4dy$ zj<*Pe$gpJz(B_X0>K;Q$&FnRnT*Dqrdd8Ur6xuec?UZJ6@4!hs8O5uXJ{on%QdMFy zV0W3->M%NNF1sI1o2|ULPBoqNj28-=?D);@nP8~j*fX9Q-<k?Yl_u<VE=}kgCk<P{ z9shqA&GV(F_jhHSBZqC*#lYZ67k3&zYPLJBjM90)NW$+mHuO_YWX>}WfzWpT!2`+< zYv&_8(#<3ZUWbw-eRq;@&O-C57<v%et?9c+fR!IT2^4T{DIP4#CVAY*CV7*H`B-0H zg2_(;mHHBRb0={1qc8T$P>M);;h+m;oF{g1JetOdcSE@hPR`CR?ef7M(m{y8^X_OE zn?_neL0H2eDA6D{)9)V~9Gt9H#YRf$UeJvVcu`w6{}bP*d57*Cl_!`A$K9EUXIz^- zk?hUDEA7zr(~!;uEB>riY8mubTzpyNP)oOuR=BGS&oa=pK9~p^z~Uu4<mrpe9+>M= zaCE+i9tZ8hPU8V5mN9qS0oGK{OPEeM-@B)#BQYfO6Ti0RRZAFuAz=!VG3TyZVb86r zSD|OL6R+1ZJYB;+Rx}G_4XQNg?!s#W4Z{ujK-vEULyFdyhSEDQf{r`$C9eA)Qgp>l zk#cu<Y9v$b+~E8pcn$ha4?d4Ri++DFrCaLgZYF&shDuqAhF}8_K<)N>Xz&fXlIbqQ zHlIawfg$YPqaxjawsw~n1$Gzhnb3)YQ==CLJX={0&W+ZA)z&2ir;=A!F33f@Y3EnF z&QX!aaiPOkCE0~nb@d(wcWxke2GsD7A2qNun2DhgRi`CxO6FOc<^_5#qMPcDvvMHo zhS^lO%7blhz5FEeBj^qq5l_mElM;cl#%hUWzmsd<)on_AN_N!Q97j`-vd+4R#5u<Q zy2r9C{VPs`qDtrXVr22Xed6?OjZt^R+EE<GyMxhl2IZri&Xgw|%u564M}aX%>XqD+ z$<Q#$0#nx`%Yy)~I6VtXb94kkD~%(C=*V;UOdpTEbpX;?B2>l+e9|d%ZN0G}Y18nC z)d940QzFSE;jHfod_!wVl34YqbcGfq#S%;{vGKBKvW)->U1{(N5$T(hb@0be_&N|% z$Xc@020&Ss3Wo?T={T*u<_3;1p$^(3kGx<FIiD`n%M%S?dIem{mm8FHKPP$V>k7S> zFb|zEC$FT94&JEzbo=<51n4SXJbHR*-%{e;P}1G-=B@JKTlz#}^GV1rVivb!ws$Z{ zYHzBqWhU(^Lbc-<|7+s2A6}oI{MDd2U1I-Vh-s67Xvy2nF&^{HM#IR4^l-+#ID!S> z?`v35GUM)}?)E!JYRDM&?X8PTM?@ucdWU+PwAK)vcn@JI0hsGO%9>e<YjWDoZg^29 zzh9%-^)4-L(ZEpxpENB+i97|h$$&D?Q3C_5;HIi>d1*FdOyYl0AzZQxKv<++%<#$* z1CYAquFSGIu7Ln(R%hr7B~1gWF5v|__kTKT>z^b<s#^$?sU$%sEbUB=O^gdG%-}ta z4*3gS-zT`2=mRVH*mtiGRL#A;ICdR)!p~<<vGUaD#N`wtDm*iy!d*Uji}a*H#bD%_ z(i{yN;(}~^7;&s08O6}|Noq~L*wDH$Uf3B9C~~Va`%te%z)q%yP0GEKqV2ny%4pnQ z6{Oz5+4}lAYfx+-(aGkYAOCUzEB>V!v;@z_@N9CR{O$(iNl5(qYfU!ol%9#(>t00p zm;;LD15o9F&(TY7Q=bwmf#RHLhi~Rz4MiR1+-W_xHRHZP7nQrLRC3fUw<B~a1kF3E zvTg|Lc8r26{Zs|DXRxiq6BIxsI&a_%Fd75Vk@MIeLnTe-wqKpDsdLLpDw(cIC^wqj z6J6F(G89Hn0$gQe`~#7X8eOZe!eg%YunTJ~#?YTvRRnt&^QuNU6F|RAZa5f{D2wja z<Gz>xJ`IuS(;YTm%5iw>I1cfHH)qj-4<O6|@uc4;WOpYSsb9$M{OI84Uk)NVOSlBC z%3(JtL3fO<oP*J{;HGteOfNOe89d!e8THv=c%4WV{1C@<#GwslkT}M58|Ezf<TN4- zek~gHOv(UV@bd|$<?t*D_Hy)HnHh3b$b;1En_!x1w|p635Nvn<3zAD;_u`RERtsEm zF(yM_H^?`ikR#e?dW(#=)lJQmyQ!Qgo^f&Y?aK0E+^=~~9rJ*JPtP2&E!=u1Z!Jca z%5HjtDCA`l-1U^P*6<9Yn^)hCK|xEQ8D-L4$-U#wn&+TipY0fFn~``-uYtdfAU>-V z$^50a*2>$?8<EEz?+`kULf7MCv=ut3Z`>?SUPgB?)#itvI0j%Vw9L~hcMJfs(Q;jX z;sxfzMxpWWtz5d&=xUvVI4qkOtB-s>?RcceFl$oB<dI$~m7!}n-;``E#ZzgVX*Ijc zYF7&%KAvV%DSSR{=mgp!D}K2$^v2t|%Tu7AW;>l(Q@aM}{9Ou5P@OHZ8s(4OJcfmo zF&;G}@{HCaxt0@eQpNpD{|>0v++%G2ct)aA&g2OJFtR3?Iet3^1>z@&+VL*?Hv9~l zt+w_9p8gm2VcU7P!)OG%rx6xl6T8~`)8W$~Cn`Y1ihHnNKu#P+XS`b>Qv=OIG&22; zF2xHr?u!(Hy|Xw8;58=GlpH@(=byp?jm>npCCX<>t6MLJ`yDv$Ng)CyCE*wh0<{iW z4U$1Yv}l2#Z3Ya=!frTTz!I{d2n>#D&Ku=Hirv*IV6*kI31YIjT@whJxdUDM#`l>H zZ<j#!8M(Q><%Rs>(eE!=cvl_nu~Fahcd1IBK_z&ue*VY*x>O3Tv}O8mjLRY2#Cbhq zPl}>9PJv)JBvK$<pa(4$wI%89be#C2$txH=L|1yB2!rCg4=kB@P44Hxqh{2BNRE6x zI}eT{9Un!(-u!=fhSU=c;XzacRvUW$>sy7O=A~7U)*;yicQU}bVbfpDKm$S#f5h1o zs)hFBkBOE|ClTd<V=L<frr%g*<Zkt26+O0xHANqY!xJsI%&-<iLcgr;@Nmtj^ZL4X z5Wsk^R&}+ib2K-z2N-SP5$>pJ^vGR13x-QaZ`T^8m!bSC0S`)Lr}MkBWs4`*!eSb3 z|Nj2mL1%6krgoEoC@(Q24R|!m#NEfS8g$@}LM=EBfS*Mxb`E?YAV52w%27o`d!A5e zEkfQ<4T%?t*5$0r@(6(CjHp22vr3$)lJ97VjN;0xNwDZy6Prih<*d7Rm9TZbT^(%? zZm9g_yNs>FAA27sb_LvPiukz8tPlyL{sPExg<fM$s-m4RDm+F#HejL4Kf}O?c(oa! zBidT}B%TKOhoy9a-kF3WK=OaULp=2)=YR~Tr#Q-^I^&Rwe_&UYBWTJ(P~8^JQ5fBM zH(xzBz*2{7#g`(TQ7x%c%rBbvHvE*_bQoU(0>Bikfu>odQN0(AA)y{b!OYS993xU> z>gdDE2!)p-74%K#|F8kv5svThV5%$Z6{AXxbk?qEOwjun)$1~zpvJ;{P9kq^y5rK4 zj+D^J5U7hDWe!kDqo0(N&#z%1v!67Yc`KneS0*y+k|r*lmeW~P@I%|R&dHB*0w0Zx zkXhso_)%7WPc2W6Z14!e9=p5$?48~HcSOAgh5wY@-2QlrXxisoT6ZMPmQTCxIzk8U zKkbe~$=iFJr6J3O&Ll*-9%r^0gh}6b0{KS*Nos|=0_=2b$FrGv=VHD*mV+^rq30cG zw%Bm`FF516meq&QR%Rt8PB6=nv}YdXQdI{}?%-#jlEkNn&UC@Ujal=9-43dDAIPF0 z5G6`Iux;tAwdX&)eUUtS^Xk=$*XIcbZ6q(=y?gV{;;JDP$oFIj4-dQERpF45Y%m;d zCDXdVLnUiUXEx6lWI1ao9D_Xq>zb_@jbe?)9VN%3uFphqDEEJB0ueeMPl~~_BO@h0 zXpwmmKDl%pkzJ=}LlopXn+~++7|cj#Gnz|t3e&Pm^f<3WxLX%%`aGg>Go3N2v59d@ zqS}uPov{yY6%7mUL|Hq;frYO5twwS#%5DzEw%1gy-C`J8wQXVZh;{5e*<tk^wmR%| zdM2du)9Z6(s~9>vGmVn@db!*rN>v}|xVkh?LgGHsE%Tpu2H7SNFPq7{YJ1qRhabCo zJxtjZ*o`v}e*Oja-8zEDsLtFf2b%BUBpNbOe#9~KAgc}9#ih+pdic-ALcH}ohx5Lz z(w<X_eST?~77rdgKx~}wFvG?d@C*mNx)YCJ=+k__!{^oL_|`bH^WKkB3yDrB*Biwm zO*<Udythxy8xCAf1B*Mkc?H|BSm-gRKVKb*(I4pp2r~+NrUQjR!>@VAO5PTN@<>7H zPI^!pm>4F@3VPk*I|3!!fc5Mq5(8?vF6b#)V>o7X9-DUWqi!~T`Q?|<mD&T0FP=Pp zI*pz@d7}P%JdGY3DKxP3)*Zu@U7oNV8w>EFs(vhg{P6g3pcnx#&n}LzG!)13BSkuM zT%^g1r$>*TTt4`JP)h>@6aWAK2mn1r*HBAaXEb^%008N%000sI003=yaCt9gVRLkF zVRCb2GA?j=-F<6!<2JJ3cmE2GeRf6l$ecW8&%MXn?yS>~jL%KG`?TG6Cdcb#C=s%` zmPidr*{#WDfBV)0Z-BDoBwyC&Bux=Op-`wQ6bgmn;o;%I)m_yk^J*ay__r2CyVxXE zDeAtO6^o>AlAq^A*RP7U6WMC>^CS^pW@6PR{aw)~Ri8XI?R}R#R{dR4Bp)~ZT~jCD z9GqQVB~{(^q9~JQZc4sQpd>pu_}Gevs#$l7%_O;N9+O9r+!b}X5T%BZ%$iapk42YE zH-KQ<^+_}Bi>j{bTj*VwirCphRUGu2l_+z-b^>kd2hsLe6>zL-7>A1k0$q0mO0~+k zQCBtf1Usk?+I3w^5dB@-tZ(m<<z}_X3`GtO{(gtF>ye5`3Q<n5dRdi8QI<*9EJe}) z)H|RA(5_pFSye2M#&xnN+FQ}O1D+P0=rW#_WmB#JHdJpOJHQraqr0zGG<t(<0%TrI zlCF3_T%j6u+E+_N?+U86qUfO7<K2ew)U)062^3X;%~Aj=B~PO}#H0kIt6os2gmF?V zn|0kEz`VAiT-Rk$&o&5eDrUvH6XE2*%-Sz`0%{m7wplbd2~E;JHZZ8=Y9V^D*c_DA zd@fqx!(_H7x-Pk^L|e2o_@|q|P^uXqS^$F(vFGz@wqEoI=CMU&i=?|N06XA|uIif> z`sp8qs1c0=><P$U(A0Ou9Jy?h9UOu*RLfP<_DQ$t)Z=5(A`f&2>IF1HIwwcT9N_Y= z><S<$v|wiDi)z+Osi01%!oj?4ma4skcD059Pm<3yaUItB&!U|quVK=QDVAJ{f38J6 zgQpLx9x4@!Npe<$*t~%*fYFKFf2_LRhMtQry{M5nI@Qs;ikkii)M<;w0oSIfW=ip7 z%gX6N0)LWO^ZB}-Vb{4>fM&?+Vu^DAtgv3f{8(=%2V)B^Pp|{=J-mNc;jk{CaFU!; z;rGDv>RJ7JwpgggcK~scyaOcOh&hk}*nZK#)R*cNfqso#LKWVKS<@CU>67I9MKimf zBt+5=D?xA1x~{sd5%bG_*>mH!UskQ?kY4mI@#-X>M89rj*{7oFgsuik#(;Sv7F_T> zh}=CtzHUI3)@#|~BOu97KP;MQu>f2b+?MnR7rkp*emI}=zgO*sAN~#uWx6BYLDT`7 zsCjh!6Zzpa$Wklrm<{+97Xn6Q#jHdx7<J$#F1{4<;R0rqpU!Jl*2@lnUr}3`_U~Xy z6nx-aS$z#O=g0SAIaSZ+bt%5c8XzdLlMgEc?Q>nU8#$C>IV}r?`4ZT)0%gX(5osJp zyKx6gFkl=W30{+Skv+d%EUHykb^Pn6N-Ro#Jd=Fwa|VvS=mixe-xss{f3BNe;{LI% z6sx@bB4%ca&OZH+fBbMcp@$D2ulV8e>g*FgU7Wpt`^LQd#<2It0)=6ACua9Kh?<l- zrgd>I&g*&OiW}(}SKL@DDP6+9KzH!N=XwfiyyPE>8vG0Y|6xvA<{TFB0@V)z{Ma@y zqom8Mu2rXiU{~CVycM^=Y%R1FZQHan(gU!L(}RUEm1>mV(l4vnEJ4AbUi0!Ufb>)& z$HC{YdhqN_H@)cczRB;_%c2H+iV_I`4=c9?2zk@Ob8#yr>Zgxy^Yb4rK74wce>}Uo zdi&`DNO19=XFs05$*n?%@D1r@Jq}jW_CTQj*<CTM7FBO(RkZi{6xCC{u4RRVI5+^Z zoe(NYo3m}An!)2Y{}w6FsK6x@@!#duME^iWFuyKHVe5*0-&WIgZ@nV*g7ZeOb6%;a zbWHz0lKL6Tzh!*%l&Q7w4w}nl1eYp7Yo;;-vVS)tiO)Dfp3whudVe5|PwtpFhN-2= zwv}|>{x`%LOHLA=`s)J5eO!upRg3ZlOY7#J#Yyt+uU~yl@P<0XzZFV-Izki6^tJ-c z079VbD>Q<E?4r#6XFbv_`ET@7vj(cvsuG#gXfph~A_{$*950g<*n6s+dO3z(e$GY{ z4;UJCr(m~aU0;G=@f3+gCjxx2*IIyWRDgxqqIoz&GfP>MVAl1q5p5g5dQ}!^A~w=W z1o1NfX-GlSp)nqR-hY_*TYRbd^y~4#!9{Ya?5K2fF#>IXYVH&b4k$fJV7gj4J!tnK z(lZdiUfIbghrq~-#T3R3%-^%o4A|WQ`yb&5!Cqd&0H*?I(}RjGX~=DM@c#Vr^8Dh5 z{N34)KYoAq`ab{_rL5CK1rsSYqSi1N-0?K)J^_QeS|ptSeN`+D$A(YIZac`!^>Vq% zHHcHKm{Y&cNpgKNNshk`U>uMFd;Q`4`?nWY`Q=X+S7-mo-+ub^;S+!xb(^{`zOcqd z>lj-mQlpN1P!1%}F$PRkKNMgOZf|~Y020S~IPIEsJA)FkfUjZMkm~!!p7}YfekTbQ zS}%EfbfWEIZ0xk+^~I{dOUw=6dC}Cut}1o7RadHURe78oztZL_ixlh^K9aJ6%?1sE zm~%q>j#FATXy+I8hNl%a)Co@R29{aam>~o<D@kTp?e(n?8JK;ES{hl2tlH)QY;4#- zwrG4N)GZB$z<`X+i)yiMq3s6%);?CKfz?(_!fK}fTg+&S1j`^QMDCJHGc_XmmwL(3 z;3sZCCI&@9MU{;k62_<(`TkU{<mp(K1n{ofwFak`PamEtI8{rkgeWoYM4uzUQ)G^b z<OZ%$b$QpXr&J8hu|g*5vcs84N7-sqjmF`!QIIvSy3JTW<x-1Xe|3|*gq<sHWl67F zU1$hLV3BsDKT5DPU^m>8{*yb%q?jUGynL1E2BaEQc{W33lB=KT_{7nDx*Bu|%0^c^ z<dy6Y_kz8+ZQ9N08`JyWx!TwPg<AcTm-Wn+!b!rk90@RDN2U$3B{Tp**M?Ur$#g3J z_4Ty-$5VF7|7@>1&S=Y?0(N76YM?-AJw$1u1=2+o#sp;Ld3OustU*~Ye{!L}BsEAJ zjHXY(-Wdx|DzXKb46uZtK<H-{S7c*adO-_=4V*ReIWQ|eExTJ#Z)o+2_7sI+h!$un zpOU?tv7!zpLk&GeZIAzvj82kYejOhu(emUDc~_1ChA*U)h9jg_^dpt9TB)d@YpJ{e zK;1~QoAxNB+I6P_wnQhAyYQJ*=d@`SCkiiX9);d&T8PN*uq=bM<P_Ewr?ZhXZepOc zA#1@4>t(P?twqqW-#OJ%D6nf~#AL)$v>k_#lTcW39vrX_%N8K+6iJ!qRbBOYo_1m} zk1&L{HpS#et#qt;iQuj-JxWHeM`U~xCLrSP_^0yi=$eQ`MwN0HsPX*(@yPDDzA>vg z`V_9EN9h_mAh0(#E-W^`yI51>{<%4D8yVd%pWI@-)bPWBHO}+8tWXhlumcge7`i^s zslY{<l%8~VuGy85FL)dv^JD^+nFaI$cKRolNn9*&Pq|6z0%n=qxjmS9T?mUk&$Agi zOc9~l0*Xi=GF^l2@3KLo@UD0uGy1V<+YL2fo8ezn5jY#y5kwm;G#nSrl!dAVANM6q z4Y;iq1sErfWcNZ(Y!?ts#G*u^P(M&ILjzoI!bY9D*S0{T2%urp3N(C6*!ja+S#}xp zSty4sj2aYb=lk7brvl$tprOtQZ8`yXw_fz@PAB5XjpQ+HB+;_@*{Ao<db0-zK2oFm zx_Pv~i!a5jUu+C>G|~<tr#VwNp)&_$ArKJR#Sn>sdL^)O#ZrRA1e%BkF!#VB>10ne zA8sIMDJ|?3PL7Y&i|h5_ylD<^EZ}lA!GU(gl6Pc?nrboS784q*ddcr#{a7z>T3~M` zDk%lFodTl2gqKaLHf+#3!Lx@Ft;tza_u@;xWz3p&U}2-(0zetj!krtZ?MU(kY_@w~ zrsf{0T>{H&$kD%+$1@geb%KDr^)qebbI?E_V<OMh)J_-0ot|g44}&F@7_ne__03A| zW9>0(>zvJETJWtMZ`sUv{xcpUQV(Q9j6iH|*F_6+io0gqFM@OmWFreY62g+IshBq` zZlWgX7cGs7CK05!w}h5dTA(_V)ou0hx|pF%&-qj%|A&&h<X&t5v#A8HD&S<2(eL&o zk3{krK|2MD!Kh{Qs9yJu+wl5Y?}<P*Z*Itic;hQ<%bnK6U{<v}rl4J#Y>lfQ?p%1~ zOTaqfO<scb)JZ~ZX3b)~tmO}BI-QU?A@>oH)ehEsSra)Ng~(o1GU@e`i>9-hTC0n) z^bi+?2H|KdIQj1+X0GW0vN_8m#3K=FkBRgo^ns!$#6&3PMW^=Pyx5?fY7Y&8gu5mD zlST&?7;_Qe02ctU@F}gy4WbIw>DXz6GqPLJr?U0f?q4;EqQD%9o&e_2QSxU(ILmW3 zDsp&2Uv{25?F=T+EgDveCkWm+oFoU=Yl$cB)@6ZjOv=}i0D5+C7l||MOwa{9D364( zV<*z4?a0s1&u>YI{Ed#TTb|1cLl7D`LI<>Jxk+#fnXRZ89&_F)_^4w|gK-avsKaG9 zN2CDxPI1@bM4uKK)*_iRrx-W3l^r$8th&ad3Ns_<$l9H4z`)_1RQMZBf8PczCf1?K z*yG<WD^@Gms$;#ePX{PAWVn?RS2xboZJRVE2q==Fok2~9k}0<^2a5drowS7E)p#uZ z05F4k-$OGImB&cE>#uHney}UcqR5=%IIKC}x|PPdoy8LtCAK$Io;9guX&o}mq>sUF z?ES@Aa_G9&50(2*t%bP8avVpeAg~q4-Px9pTt~3Q<6Q%myTr(zw^W5kz)a9c`N?05 zf`MYKWmTs^&2jRQo7=2~fMK%6<KcAz43<*DD5+g{Z5N~2ag!BgnZ|o{MSzwRpgEmf zbM1Nc#cabGc|6!jNHNAhoW~yA_<2)GZ+synKD?$PYndNV7ls=gZGlLj!eAbBI-vE- z(HdCkDaFCEse2n#9PR=a(;S9o_Nk%LA}6+%-zp^uFRluJ=ZGeG(QnMb>5aq0Hh|sN zboW>X|D*#dvdmEZFED1wa1-nkDu(B#sdI?>)X-yp-1W1X+Uwu509YdTtoFch>m`a& zn^#>ft?|>SY3}kKIsh&D+D^ce0FglaubF0OxY$r)EC9`ziJMivEN~8gu^gnxvu7pj zwMKuo4$b)e<$4P5kAwH`VSfz;FM@(M)dRf$OV|K6bEE$fy#LrdBAgJ=kC%7V9Q%0r zGAR3`%3i$+%D%7q4`Akx{!gsr9}O1$uUOGpjR^kNpy=JAX<9j|zp#hySMOBnTJ3#Q zJQvwelf>Vzz^*iJWGs<X8qG>+4OKwW4=wG}3^IE?H+sf%scr4FS&bcITQ!U8209J; zglC6KJ`YMqpKJ7Zz@}DWlAr|+m>BS5OOIGRpd<5tjGpd*B0kDSFq?Pl6-ULvQvJT@ zk)w3iqwyAvC7D|G&h$mEKt9ObKM=2kEJo}Vyng#nczt>ry#Jwpi(tZnAF1GFSa8ww z1nf^?(K(n`qpFL%helL{L<);*EURNZQ4t3Y{B|~#^c>*T*>VN!uq&flbMpsq>r6*> z?Z~*huoKdu=-BF!qn9QzSuVCYESD~FGq@`^8DiF=J(UN-_OK&H&h!tk;ij41tunfS zoilX?-h$y@p;7uQ3J~_e=_p&;z>>=IRyXyrSg!ib!5=mBy@tJ|H+SsDZr#4Kfi8A5 zduQtw<CS%<v1WF3-*8JMfOL?k0XXL6uEsWQl;YqmJ0)r_;XcW-zuD`NTEVq4{MKVq zyE(SW)Z!AwVjP-Cem!N_hns{@3&;^KL>&xzj5vH9R+n?Edhwg#-Y;lF>t?)@S3C3( zL@<YN#s{%+1=FEtBSOa*Es-Tj+;^(2*icVngb=YA80vYyUI!V|q9pT#t%`S@`dz1d zpQ+w=imi6MN$rgutLmIxpLsWf6_FH}caUy1-Fj$yef-VI4U)hd(Z)8z>`uw6QYy=; zBxihRARHbZT7gT&tY72Bf(nl~Ks%{hD^*FVNykRvffBsT%xt|wr{b4lNf#hA9?2-r zr|W9b0|oLCYBhb8q;HauEb2y?jgRBcqtklF;Xi}53d|vSH^TQNS#(rq{O<J2^M4yJ zxAjr-x3ky(agtay9K=D@7K;jR_TU)Q5VGWN&7*k0^8+Mgg@nKrrEt#U8Gb_Q8U&-X zJITNjPkR;5Sb#dmlfFf<!GjMow|K^dl1eAQ<19LGO;?|_s?s~k$lnwFlz$#~gnDy` z5m;fM9R9@*iJVoeuaIZ}(5dHAS{~e=#)G)izRKVqEAlZG<?FAH66T)!cIe7%-5JCV zixXQ!c;pp{DhEDLKq-LJSU^p=Q695>%_*cOwpS5y6m}FUA019abq5_Nc0|W;Ee=K6 z4#Xzra8;??kCNl#V>WZ(LB6sa@+sd~oL9j3bP-A4PU`SUc5vik0nlC~Od32Xc!4in zFFhB0Cu!78WPHGPj@wQJ!<!yYlN5fu;GXgS-z5)43;)uO7s>r2hGlRm_k91D19uj% zd5)sWFzz1-1h-Jt$DVN7MDaYD_5lV8sWxFfDIHoz)Y=9Mh+&^$$Ky<G9c4VW2_3Xu zbp+s4F+K8gi?_-6T<mmTui&$scsvm}^T5{~%TA}njYJBgCGZkXm)05ARvQr;NPb_{ zCHctVM}|Lm1#EBWwjs2;ajMge9#tI}q(YB8o6CMXe-~lz1O|Z8-T|~*<eFYemN!Qz z>h5??P#YiyF79x*Tm=N8L`?YrnVaMV8B)5yqaH`Z#P&>TwB$ZFXgR)fWI+3cjXWUe z%C(ALO1DHYf`Nx*DUl|Ad;c`ZLEjr|7du^{+P;lTwuXNopyNpqWd!M{9<V{ba#3+2 zn$;i98jn&({>lqAuFpRiz0_4-p{b~UjlxY(ctqoL!`F7S(!se4VelPVAupdH9b?;` z6vyoVM=2zNvuTgS8PpC#2x=H$?@MIWEjWLEqw`!azZEr1QE0NoQ2}xETRpiCcm7Cb z<rThEU(*BSCIN~vz8iP`*e(xrlHEsq{71>hw;z+g{`+4s=1pEppEmN2b7IECN3r%5 zknk8*(F3G#D@|N%2POvpjA`ZHL(JZM{`lkh>$9u3Y;9eFxpj8+`P17=rN}4F`O|<y z!<l@-Jq-BXJ~cR&r>BPot5?q~7Vj}24B=e5>c#-&b5|FPv~nV@ha<{mfE$!*R&g*D z&2CIs*+TSouVbygG!^l*U8~3^#0}t~u1UX|L<p>xCRn6BUsM>=AF;^6q+4OAm86la z5CJxr3+ds};UxKHj4>NjJB@gR+BRQyx6uCZmtT|7FTaisGeoH9)4^L;6SwaWsNrY{ zephyNST&>=oehxvH6C85LhjiLXp3upyVl&Mjka?yUCT1eDs85T?hj%93ua53;gazp zksRn?CcGMHYiK^Dh8YIv1CLez60O~%5}(MC0O>Wu>wudzn7~ErNyeY#nkH=&bLqvs zsJ(-#H-EIlRt8>-5SkdoafjRyOxld*ih|geD-2;}FN3lxWhC?A7F-UYS7kWzs({%Q zFmxVoEj`WB+6QNm=#_p!R`=GijINwM=~8mvG|3W2qql9NI4aw=9V!l#1EikB`Q`6` z^ISasaVW>ueW1~>0@!MI*y3U5AV>#SPu-WT6PS8WM65X&D+2G=#7eNR%b%^A@T!&l zb7)+<L-+3cT1v<E1%+pPwe<D)2W5bvx54#}E1^{;LHj%5Kbull^`Y0{R2>7!UEtsl zjT<KNx7F#S`N$cLZ`K1cnH#uHD$;k<${W_1ieaE@A60$KcVw1LS<S1NG-4kyF;Goc zW{ZcWa<>?|Q<MR`p<_%42Op5j)h1`aqD-AJ7zr74KI$qNxi2Yii`B(IT^Vz1hmp!I z{jIFq+e3lg8oW8L$3R<qzY^Xb<Qq`--C(<dKC<;YG?A~Kp{aWW*s=+Edi7o5prUsy z42+8UHQcPnTN6`j5c;(W{!G1g02GkX2X4B-B=KCY#!3$`nILHbu>Lr<Erp<F9*PAE zsd1BNPu><Q?1(bUpmVL$LGO6~h?5cEr5kMtVbM+$-wD%zRku4CSB-67o1?J@%p1TI z<EVYtUSnUUcTi3aBRrbY)|;ISz{I)7uaG!LNOhB|{g3W6c7&u~nVX25Dv#}+JGz%O z1;x^$H<i3gMXiyN4m(r_25Gu-gxHyr8-)Q~WY&7n8mGzL#9J?`NFT_L5awt=r?M)w zas+vrl+=uchI;f)7LDh0Yq+}hQESs?u&TKlG*p9Tld{!ZlmAd9bh?@Np&M41hrR9V z0&ikx$(o9$A8^9%>-PS`+}fc-ds`5TZdTKD5e4wU@scA99)=tx)=jGk@4NV-l-KPz zGCmz-Pmx{K#3la3kT4Qjf|en7h@vBEpbIV(DWh$9K-2+=<H(OFX!{t2#;<(Vz9U`| z#`cx-jAGdCte3uUs5AD-Oz}FV1M#Rx988F2Ra1MtL>IL+8ogalj_ElZ!chYBml@uy zV=0}NL^g1D&XdIUzwvJT`o!!0##I7pDC%TLp`SX?CY)_W<wCP#Tee^tN7IE23u<Za z@~=H~Pi`jWpzL|5mpXNui}wrLS50;mAMp)aa)Ujhxuu%N#3lCUijZcBEuyp|U_UX= zmOquW&XZD-1AQAqjmx3k3p&bl$w#k-sn>7xO|x*=VELtA`LkfQUtjMqc$aQZRyTCr zXO3x%JV~}tQ0BkwROFU65&!O_3_wqYytQp5`njzhpo=NUJmuGgdb34?u?|Ix%R1lv zMmrGX##WqkDVKD+xsnUNPE01GaDpuNd#4wJugsvlOopk<BI+EM@v-1UcR#joAMx!t zN#J0eF?NTmkJ&CH<h_g}S~U;d2Bbx8I6(*<+g4UMiJy;zoslMY-T)ZA&WMHb&t(Et z{Szw<)EV2I4@5*?TftuTsx5=OFcqh~T<sBQ@|3CNI~1aEwZQoM()R&RD2cYuB`rc2 z<#~@q#B#s<I#TX<8Zju0uf6RGrFua}uC$xehgb!72Nc{{lSyOISLS4pO|PxnZe$`% zB)}mM+dA?Hue5||yfsihjKV*}^9rSUi-Z%nsns~s2N}#7<$6hr?Kq4fLM8WL-L2M& zdcgLthM3Dy#)Bqzq@h;JC$8=7hV;|6(B(?$n{_(&n94*NIJK^;AfPOAd>{H%c|r5e zH1f81L^D5iVvFoQ=c?IhEMaN3#57{V5WM?p@uRoCz_Tn&E}c>uSi8kIyM^UrmD(8w z<FV;Zw|qf%ICQVLjh|=W^vJf+YbEyI6XA7++Ppj+e;w~~_yk;c;=}^Fa4{fIVjRG* zuJU=W=m5z$I4IDpnr}E@M@Mp<U0#I&x!xuAZHrdDO{>=N>{(Xx!(V>Qe)&~v1ojxY zsMee`YmB}j47=QVGRPq$GIQA>yP`C_<j}qVRF8Empt_kKME%Kf86roi^!2-Yv3YFT z@`ZXZB~~Dy3&8W<>B(VH)mV68cIrLNllrl5h~4XvW&6z7(XKmu!qfiL*?@}F+hP@O z-vw;hPZQ0z0A30hCXVBjPjBrZ8gJ>7oAz*CG!QS{#*kCkZL~cN2UUS-$%j&TJj0`j zFcm*@G=0jkrj}J;cum&uIE~*DpP2;p7N=pj?5S0hT1JQx(*QBY3oNgh)LQi32>d)l z%JR|U*C4_WR9VMEmfsSxGEKgtz<81r#PDtf*mAhssf_o2ytB<73dq<8U>{2O2V@q( zBzh^d*rvR^+74|c3Y0ivKLWGe=n|M?eiNCpa5x6pOo4>rO##2<ghP&b>bOBe*|U4X zMZ)!sj(vvnOy3=5>Z$#9gm0f}f*78{|Mn&S`qr8OIRO)MC$(AGlM#?zxArNo3Zx+# z`w~SUxbbT8Ql)bXz0R7|t`43g!l=({`z!_WP9_M|q@^DfrSi8U^7$ETC^8Lp;rVy^ zinED$mlyHn2Q_|QHkUwSf<H`8r*gH`FZY{QaRorNQ^b6_<^FO*UoVjpD=_$XXfFL( z<A&b6(My^+{Z*7eI&_IQ0ixT=&cEF@dYZ0#((Ziz9KxLkhUYn(ocz(4fL{FCaYRUJ z4TjcS?@Fve2){R}@cTC@r9bV(@&&4mW#^OZX-z>;e3pZE%yIG)JHM~8P*6SkOP4Jz zio;Y?Gof0kv)23pcGBCL{dQ*GR})w!EBcuNixpq8!K%KZ<XC}B#rQrya$mwX$p`{& zXNNrnUgjz8aK~%qP?<iyu+<GA95eMg>}%pJ0yT<Y?2h@658_!pc<*Q+*ln;|Q1g-s zKBCxDLa*?<O&E2Zy7%kw6@TFm>{|KIZP#!XD3^<L0iX2q$FTy1Ck1ws%ib)uH~kxx zgela+ZNXkF513Gjmc=H5u`G$BtT#Z2p}EuIIA^F#-Bod@5Z#vq!@t%LWWsnJ@f`8; z^ug1+o<ZX72zW=j+C2;hkYFc~j3&{#YU-W(ZWhmYL*|oRyU~N+wSk{~C18<<?RVqU zvBiy>83b2IJU`OI_Vp!J$cA(FaL>vu<>~|c^IeD<T+|8aLUSiypSx%fSyhfFgrI+| z<B6-)hnJW=eUkhv?|;0+<@MDhd0EsO%$$BpN1_G!3}5}M-1IV&l_(Fl?S=?D75zp( zwq3*+BAvbtVJYQ1R^BviclMl_8C7p`-85X>@1!8!DVd7_^yN@;TRH&Ki76hW&cpDz zIgNL)R1a&%t&CFH`)z<MeWZ;~Pl3~C{CwR_FG(i#SN?*vMwe{!cF=ygBgc5X@Ydr} zIDx8rEj#$dK_6*+M>wm`5e6u$0(bjF>(2l|or<^g`N~1pjZ+tsvoE^m4)2p#1si*O z$4RQ}?=S{qbV`2*?<arn)84JI&vx0}Mm%s>HT1y2Tjhb1-wqAXHbt?$txH?%df)vf zrEf?nBgu<IUd48?msR`?CcWiNCOul2u2s~Wzd&&uCPk3Ar?>7pMab|~YXaqkPRTq( zu56ivHPkv1sa|xlC0F2|{t757sfZy;?2Iq|q?+NcaJf06^sRR)H8HorTRKzv%&@|; zOj0uB@B;{Ryv|<^iGVBUn=^dFk~3@^7pN_k#XWuH7{3#^^vj)392i(lTiv3%OQKnu zJhB9JoY<pO=F^}ycD6tCDKMK@EQEiZl_Nw!sh;4`Dr8Xnf`oO#_njp4B(WKn@(z6m z#`A+%#F?1DL>U=NEM~^f&;;r3KzSkP=p}7!wDb6l)h^%QO$Qxo{lZHbw4@~YAipwO z(%gFXuMdsfk0%<+pDh-c-({lEv<~FWn6?=6T7tF-m%?lOOeARnG&qebY!lK#nu>)K z&9S-oC24r$T@pD;E`_;NuD(1nYry_&*E96cBLw=aWmWSjHO8~@m)FeaIWk`fSd^g9 zA@$Lbk-mmmn7Q<0u15jdLs$DEANg>uxBO<&;jc<tVl$xNWj#+W_$|qV`8;*7JeeZR z`}-|Gv>FeeqUgoiv1jCh?|!M<+8)X9t;ixDT0S<2)T^}92=FuBzU>IF0@D%RvE?W~ z`yCpNvLHMl-i`o$VBEui-uWodOE#6%H_1aYNtNu2h5EL+hlqL|L`KN>92sGK7d^np zF5DL>%oIh;g_)uhxteD2VU=R;Jp3}yN@Y=!Cb>^YuI*Ej-pT_!*W5hf&tPlq*(E8j z1mDpAza8T@UF54&y|WS!@mj-+i9{)Rb!vSU%Zt;=a0Jq#tV>VXH0Z4!K_7k=yEuRk ze(p$a-?Vn~9fAl!D{?Adrjwh!w;uaZ|2--w`f?h~;d~BO4ZQf1=~4%KQ>fW4yQ=0X zPqlD&7X~t3%Ug8e=TA6UvdV$zPr6+Gu_>+n-$Wy(xK)bu#+u~6qld3=wz=81w6@5? zfKCWe6CU+_RRi~u2rR@rdU_4!;azYn!tH#n$CuIZH~Lg0av1I0Ifa@#Cxn*S2246g zLcrsujI%*I++vZ+-FyNG{7n-Ne|L5{N<P;`y8-H~@n1`YFm3D7r=_azQB%GqdeU=* zq`r+gAh+ifau}7}|GS&yUo`RNn6Z2(0H4pwxbpe)j_ohk(`ftS(eKAUlH@yZke-l` ziPS9b!!kb>%V}Apg`0?wxrW><9?Qu39>?%ozOX=#F2_74cU>Lczpit6_umdIQ|=(I zmI|!H-HY|xuVvh#re-9zZhJa|YX%lQn)$s_8aO;)Xe`slxD*g!2$^>W`uP{F)puu2 zSOB2m`<kqLm4k5s*enPQfS1bbD&>Vcmy-mWU3TGqkrj#O_1or3jMbOJ*+<0iQwTCt z?p|!V?N%LC_Jb1P9fu^sLr4$xHYHmFZy9beL=c1BBH6YYGLd@~TPScT6ahpe87P)| zBfb<oVE8v2uY9>}l+?{$Y7pAJ2H|5)77O~M;}UGFJ5i&R^&+_yHJc6r{<^KN6y<w; zkL$G!(H&DzY;kq|gX$V(hHg8^jj;6|2ea>-Ja@pVX*g+mxbk^mw0`LyK3EhHGg^=V zH&}J&k!v^+3DslAAE=$$%>W5xK&f}KHdA4F`~htC(->5&M5n!I!^RY*_hi#~$EihC z>zMceta<k&D5ZHKBXu=0P<zM7&4^cIw}vVLd8f)mz8$Mda_v_8nr?^?-SU8L`*5ox z0?&|7r@ikyv&z_oX1!?tKjcwod@g6=-PCPUy8~N2$xOSj(C@}LKcUxn+YJq_;IRa~ zBwnDWaKY8DpK&-TTu&T+KS|iTe)_!A3)nnqyNExs6lHAHKJeJlYiT*EPc-?PR-rHL zTpNFY`|T+ihS2aN08Te<drq7^$duQ-kH5i>31`7D6g6E8Q;pa}_pujAi^YQJiT8OF z4S81(TqYaKM)Efx;~ZTTDsg+9d8jK*di=!{hlgOhH=HNiT-|n4g6p*8euK(RG-6T> zZVNNAzA`4k$5X|~yshF(PdAgD?4lYcRV)*Oc$t1f%vAkIT3|27S=Un<h;H}_uE9~~ zSWu0|K}E(^<#bSKFPv%yNhjb(*HMf@)KWi&DR$2~fJS+bqjVW<kc<~+@87=J)+!+8 zJ<eUMg-cONK(QY^xyg>y{ANFLs+^Lc?nGhT8kG{$FD6OnK+C6cN@{%f(J&Y5Wh#?( z;KiV03`turr<h!|gXfQ%4^)_W+5($-dKzANr(^cv#MPZhYCibnJfiSiePfVlL6>CP zw$0bJZQHhOytZxIwr$(Ct=Go(%tp-aH#ef<{;#O0b22MWW@qYnyBhR;uu{+%KK<}n zsrx+C(@bw&ABww^mQNF=GB~c$6uOLmIWa}Lc<{L4Y8ulD`)5MAI&&;qQcr)zzwW6B z*V20N5_*P4u5a}jqx$m%6_!qPlawruarD@>(sQtHn@lm7Q>wfUykw3|6|(>rRqVeb zAZ%0H$Eco)T!=-CMerZ_e4TiSwr5;j9&s@-Vu#4@Cvps(wH;g-gwa^zM4<<GbX3eS zfC;}g<1(jxz&?8I=>zP{bYys`NsC8aCx5x<Xicj^aKXOnk(j5Aa9+8n8~XS_B^L<b zu2|4V`mY@)K!60X#C>Tb?$lVhUuwMegx&vWirq8z=zU7lcEcvlx)(@uC|23<J<YVr zCD`ljf|GBgcKhbpL|%z34u0<IOUmBI*d&Zv*2m2!m>>-y5n!(9q-4#~@Yhx43$tX^ z@nFiZb@z4k#}f4)b|6d`_5aabN)u}SyhWpy{QL}aD$rEpSz){KIfCtnc<bt)1toB( z<9aytZIj$^$>tvDiF6dy&wn>oeYT^|ytFV|1mRSP0Av!E4+iP2*&(%vUeEHH>Uas` zIS@ATq0a*s6agVQvZL_>DVgTyQ^l>!uJM1y_6Vn#XRhx#E=CAM6&UOCF0R}z5kzFc zMlef5mTzZA6o`oLNPPn@XTQz-wC$$0lx&H31cawjme^^KtjWz`GlUuo>ILj18hW%d z88Iuw;a%m=w_1l%;z63R!+mr}dkcRI6-k{{Vje;`EL9UrKHown{l^)Gps;&Gke|S( zeW~$mfnGw^qGZxjN2GK|?9Mp|Le{819akx>)%Y!i@KWAy{wXaRZQ$Xn)?7!I+s_<0 z*M)8lqA${3SKy~ke$oe=RGqePggSogIozzn3j({&3Qy!O2o*FioMsG*$}BFE9&f<! z(Jt<W;|f86i{Frw4h6k~6BacP%oC8W1h~~WNhOa$s_spFLBd0F3ld5(3`}ZJ@8>_{ ziZU2{z9*0kh|y;kPi?W~Dnu-xog(F8LxHl%9$GG@KOd@!2D3r7eqd3$9=d^SqZ3nz zpXy*`##BZfl<Lf4Zfu#M&RDQKrbn%iwOm!gx`9In?w6x^Yy@NhxG|ayyJ5ScTDgN5 zWvc+lH~*811_eBz2ciNvYG7ztGbd4?H+s6^ApxP0lW3zksWL%SIHjL>gd{LW@(!6) z9z<=fZi}#|XS{=e8M1~J%TBxco@lS4i!?KhTb-X62k~|$m%xi9%-Wc^tFL~n<Yd2p zgjg`iku)afSS9877Zdyu^xA8~2v?jZ8jU3k=XMMMQoG=eyjJ0Tr7*c$%I<(Ww+x01 z^~yoe*t;|Y^_;0q9(V-Vrq1AoDwn(_-#ZX9_j`C==haD?FBRxH{ndX_qjzsxB2-Wg zJq~D|`Gw=3dq6R#Xec>}E=zsh4MX~GIcglI**Z_ce0NqniQ7C#)gVT0ga5@C;z;UL zaB(E+9S;3CsMQ_)^}5wl@ah+;{!oP}J?}&|LcbT41;|PWtwS^Vzi;CdK$Xn7&34v( zJCV(2B#+OZ$fBy0{_mls`$1bTrjfC$@=K%6qT_rYnJB)BE*gP8?VC`By-5M8i4#aV zpw}P3|5WOpIi(<8f&&2P{-))j0002kc-VW;nOfKy>p9uEI2xHa{c3fS)fN6LFVE+t zmLLIKqO8)Oc7#!1DzSPn4oxPcnjT0XzC10G+L9!Bh@xY+r;}JDz9GvQY-4Td-ra3? z+f6JxE6Y2f8<A$UDkWmMU_0zNC!NMj^SL6xrI7}9^SYDA3rS|Mf~FQvR`$16*IyPf zN?&wQO}o~YC2uh_-pc!XJ$KHT{s#T>@z_zt?Czb)?6TZ_37G|1PnwSJgii!@LVsJk zSMHbNq3A%xIg91dyvf?WhZJ^e!wT}f6s&6}u1h`NKTg-;*F;42+S1n4?E1EKz_G4& z{OONX@EELp*|^*0pal;|uzpfiPyA<ifA5r+mdy}ITMRB{8kJoj=gq=iSC*k@_1yE9 z8hhV*n4lPa7M1*Z-LiiV81Y#OMxk6X(4R)5^hjsWtxlYtr7m-PW6*^TkApjY1rW%x zexIchg$|akO5MM60qb*x3Vv1-+ojWZ=5%@QI^in)t&O6FJK<7-z>bhnt>IXVB}s-j z7pmuSs%H(N+0adqweGyZnt66@6Q9z51+b=-mopVYk;5T3UTW8$Ru{7zWL2t#3ZH<w z&^S-#6k74693^8mZ-C%go3fmL4!0A68(b>XPM!<-ba5(T1BS;)^^){Udb?!`&rZ^1 zHX1DdU-foSaTtJBkoT+b^hH)H_7}+%Yjgs=mWTka$*IXC$~(&;+8O0KLJ`lVc+KWe zoz3qn5}*W=mVOnD8+WYo;Y7lAat5&}2Ji!At@7Iv%HLE@!7;6J*H_Z&M(TyV&*{k; zV6z2)xF%iBuOznbBz{0UdMH+(_PINRr_q_q?-~rFi>bp|opoFJ^-Hh;$ZF<zyfrDS z#Uvi_YhngDIolR(0r!19Dvh1%)q|CTNd8!T61^TW@=>Uhy+omkD@iBjp`oHo<~x$8 zqLcbBKMoLU2zW63et=^P#8K$>nxwNR+P=gAvT!mLkoBKF%xBHC?DL3B-R42EQB8Pn zy%(G00$eIE5Roc=Tw@wdbFwjw9ONB`!R45a^pcgLy63u#kyj#EfcTCHaQU?mt9;2l z<)W1sszx)Q&3t3Ny&r(&yIn)PJ_vuvzdDhp1oj<PLH=Wj%jvb|^~QB!=8rAA4xrB7 zO(BFkfg4O)sE0>_$xvE^3ZDzDyPH@O-mQ@3qkjhG(1Oi_DI=60!S?Q~dG{Hh*aw(1 z{IDN@;htjU*5#&ng=#w+JSlEz;t{ajm<i-#aTWU86c!t*fIkC4Luc~ky&D$QVNQMD zI9oJuNb6$TDF=Oih1ELlqrhZ!H9{i_whFRn*#Y$j>1g~O30+H}+KgF~&xj9XL@H0{ zJQ*z%(UERtl=ur_NFXyrw*+rA^kcA;X(_=H3|MEF<vG)^s}k08`UL#fWUydJ7{g`z zv4%5UjrO(iIs(C1cRcAFbv(fimL}KH&_J9f8U3-1`v}h=q#lg7*Fk$aFT9e}wrVwf zLi5FWtX}!W@|JKhu+^XnTzAnVPDd#yY3Eb*i?2AyIewu;<m(n{Jw6Dyl>}-N0zjM* z@c^98p%(B&-~2%05e1ux8Y1y^`!L=BQ9<^$3FfzV4hijGiY%_?eezutm!px)Z)_Ss zMD`6IREgA+a9p~iTKY@8Cq949;*ow*3Wnw3mYDCEBf4xZ+H6kfuZ)dK7JBk>9F)=k z;uKN-OjHT=hMRQv3=W?0m*sSASuY3;c85ug=B;-WtAE$+ADnJxjtw~*ie=)aj3;`) zo8CjZ=;sS^BFX9R3s!}&c*a=G@Tx~>u23;xcP{#eRrIabIhxfq=BF=RM<TF`0$dkn z83~g!J_GXAjpz`h(R;syt$m|+7$on*n3<`%#a$X005|d&>^FaoU!Paqo*{`XF91u! zdL1y=i*|!tBG#Pyd;qjH>xVaAvQ_q&5JbX;HmM@r)X>;P&j^u!y0M#ZV182kGdySn z8Y<xzq5Pb>Q+~Y^p;o7R4u{~KymbIVhF))BU`sD}P#aal{tmzx-3VHtEjdSUX&x+s z7y<4`|IQr>3kAqUhj1oMKEMLVU#0HHy&QIa6LLb6<DnN?qsyzdi!p#E6s0I@DGgk_ zTC%5T0RwF<B*m5fs-~o{p#G6=Foj|$2xn#2HxtF#V&E1{RvKiMF_<@uGnRE@XU8EM zq3kqwfoG%hOcim3zP9Vw#;d4=?N953IiNL{F$DgO)a)bmDdEEH{Om7=lPiKQ;`&#} z!-5l=_?vUtgo&ID`$_eWr66${gn>FrUPm5V>@IW~D%4GnHnAxj|6Mb}A%~Le=in?a zY381W*lS}!8zmXVl?fvUfmd0|e^VNR0iVY+*2zsdpp9z2<xJ67b%Joy>6L8K0-k`y z`k|r@9yG(Q#gD@FP?D*GM6wNJk1b{AzT%1y*)IsT8#f>m9gX`l#?JfECDN#<^H0y5 z&E>hbhze%hIkxh{MwQ5#^ll!=+REaWIYt62d&b&4fnu21i~AyV2)CkQh8<Tw&0Hji z*AL&6hMQ7l3q40vI*9L=mftSFdzZMNX^IMIJ>NmIk|C>$qLsty6=wISL-TR1QGqK} zWx0Xtx@_Vd-#R}d!!f(X$J+Zoml?Ck)Da(sLv(|FF(9kOoVhe*xpmAshqb#M$H?RY z3U^J5kLqA52Eg#q_Hku<upRjFmpft~NyarAL9=dm@Oh}P?O-bc$o~Ht<N3pAix&h9 z06@(E008^{nge$i``@l$S<}YuunqD1<~K!GeQ3uM^r>>*I(~B-2S}SObn^;T1Ph`` zn0(`GIGKX{X8+sEEX2-}(B`y(QuD7ki4!{xTrTDe3`e#sT?6@w?OMvJ#xkq)j@hx! z;HzdtMsWe6%gC=qk+!J{rcCMLxUr5JXksVqTz%D=a<lR2v28!EX}ES>C9L8@mlRqg zIpqYHkDYXEB#Z@$LjC4dK|?^l^riK{-W&F(PyDfAf6rBfO0q8)(xcC`n#WI|X8G<v zTVmQtmu7u*n{;m!t2j%li>`xm@oPDJ5yfR~)qeqB6IQFpyu3U%d#msqc34DTgtZBw zpv^g_n09E19%7DZhw=~b+_MT~SM~a-Cd3};VKo?i)!P_ApP2>kXN^?hmOq*`6)n=! z;AivyK3J?6Glt|at1xCis8P00D=%MD<xcT`=F<Z${ezvVYF6&nv)5JcS)GJnFZVfx zJyduAe3+x(BUWv4ZV!B$0kU1l7RHSo2YAK!sMibiEr!{FK~-WM;#pt^%(Hm_BS*ay z4ye9?a&dN~?r&;N?&v_lG}4y}5Q>jDq}(0pFfJ0N{X~cW+6vIB_PAP*sLLx$Jk%mt zsHG{X032ya7$_^6rbU=?iwvtrqL<~EG@$FR{A87AA=lWVL2xriw2Jq47BlA)sw@8F zHoDykSQV=yaR_pU5cb#2h^M2!@-7}yzvG+)(poYcgaF`638PEkBzN`AJjGa!-f~cW zzF!<JCs=&MFxEj78=5WC&jgsnhpEq#y-442&gz#*1kBxrt!cbrXU$$~Pg7W2VyzI9 z?U*sEjL6Pi2qcamZ((?KWy_N*d&B7Hv))%BzP<ZZ#LiLkj!`Z|@uPjM*!>RoSIxG| zML5+(1~bWLoW(F|G91_koIG2M*W5OSpbGA&QBGz&pm-0;j&W?-(<fR+yI*UfaQ(b7 zHoPlD$}r~HHfP+qSZrs?fqPR;8O9ZDgElBP?9h0<;>HWU=)!w9uzaEUyB|A(_->-y zlOSSCAkB8eK-3w*kpzJs)x;|DgCFtU{935C)d~!*fe3q@-}RT=FmtNk#bVm^VXG>F zEE#6x4l5lz^Fu(`yMW9dMtaFWnYN_5#hz(=Dgn+EqFKPqogRq==wl-BTH$P3?Q*{h z&uT3e)EI8@3szY*C17{&u4l)9HIQ9$g95qkI|w?_0Zj*%4d<XWt5$3REJ=4hkEB>e z->dorZEK+;J)7F~&&>ag^nOw8Y%ybR&T*jX1>@^KZNT3^6tIr^JFc&G=iY_<oWF5p zu8mM;uKy_K4(<9~X6Rp#tPRB@c(erAzykrH*~)Dv(e1GUz=(qBkTHGw*G56we*%<7 zp>^z=Q8<nmv^qIiS+KCllda_*a)=tCOSo_u2Zml-_D9{s4*7|TZ5ZPas!`1R0ejLs ziYA^751AN3V%pLeqQXQU%oRosz-7v*3eaD-$#@$SA_fvq5Wr6*)qKsa-}Eni17E!b zb8s&RAy;V~%}er)=)EgK9zT{B_j({x+g_a_H0zgqvxlT~pP~{t+jB{J1Do~9N<W>m zd;dL$fiL<M9uDZx8Ce=p3|25nqYWj64OOr>rtm=~8kuT7`FV)87itOSFmdZf>NXwI zyVF@UV_o`Tr@cdxZvlA<KuLtYzUN1pm~`mMDP@rBL#@xC{eBem;ntl|^J0VB$zO!F z7*-so;C}&f_{^nxQuj@WkVg{IDA?_VSApH-seTzs!qJaukK=unwS?6%`$}HUpV}Eu zn};p)ykePh_9J67oER%Wazw%|_<PN4SQt}P4ICf#2?&v~7f^ILRxV4zDq*Zw9?Efn zTKKm1=4Htn7w_J8<a`C@`r=QI@nd&dy_5Xm<&-Vv%f(V^z8>`x3+9uTg-a=-AX5T% z0m9}3LtAV)t-!xY$=z|LVy-K|u(39-d9<jFoJIaL>6S@iTI^?NV#<9n0v-K+!Ujay zHo~<zulycKMg>PUOSY^9G3B%_Riv2vhW1Qg5RRQ!s@sB%Obt@KccYNBKXFS0bt$OA zpbUC2tJezh+jw_In`RsgmIvxDi%$DeYEVy8VPXlZZh|3imss(YQO*(Z-B|NK*6n6_ zMA*b>no`86EzXzI+{y@d`F~V3D5)t^=Gu+pHW=@Rkkc!#iu~K=MWBi#QCAQ7<KJ$G z`WB3%&AzM%H#?N;*C<5|J!sq`hUF9(se?PR3fKnd6YHXTFnHspuyj9=H4F2kIF-#Q zfP&_LrZ5|P==pVB1xt}#bJ$SPj2U<YgBAQu`+J(BNp?d-(sPvTk8yZ_^<==G#E)@| zq+3vUpM3@!<aR5axKJ=g^bzp3c*=k=BBJmoF6yJ2iJ>u@Q@w9x_hB-1qt$etp}+86 zUGMHD?x2m}ox_pd<4(Pooea#-FT;|tC=ye-&^Puq#?)=Fojui2%PIT~oLlBdfD)*= z&BH{w+Ib-_YGi>X7kLODZYjc}R`{C^#s~A@_OYJ(ntPN?g*5Z#!7JDmnBV(^kK2hl zBFnsRh^370Fh5l#%~Yt%0dib2_4$!utEFMv=*$En^78$%0LtBea#jhUX1yy*qro%E z_UnZp!1*~>*6>O9fo=ASs4z^UQp6)AgYGe^C98F1ZiefnG_v^Y(<KQ;B#RiabUv{x ze0Kc`z;@GJ8St3%kX0m$f^Bcgr3rHw@9tH?xr+OYgw;9R<u)^@qX-l(B-4+T9$~hA z=xit(1-&b*#A!<O!ths!37rY)m_a4D$02aR@JMf*uDgSFY6seA8Xt0M8&;8UsM;`R zV-LQ7wmX%efn~NTrxe<DktNTilvMDf(*Ct5oz@Nd%bHG%WQO3zhQ99AT+PT$Nh@{J zY!-g^IWG!5vVd%ez()U@n^J1^zk(Eb4^m*mf%tGgW7(Pr&1{Q3cUab+t+|Dlof2}H z(DX*$+g$k+Sc}jV+Gw)skK%uRCFp}YT*#8@!V0|SK`3b=I4el-is#NNcA&2ZDpXxl zHSlk6*4`c+t=(pFt+<&Iu1CFfM!IT+4{Gl~SP0gKgSgs4zTczcOK2ZIw!xxH-R7bK zG`1^tQ!BPn6X4?apLK;V%CBxby<s@w-QS^XxmcO*BX+$x0AW?E`C~N;%d6G3x`<nq zEakR7T08gbS9;f~bozz;-ZHUp+dYV2JR%`U!52Q_G6t!$0clqLaS8Q|aUa(TiAoUl zV&Ce)ojz%8e{?yn_%4RjKM0X+xb9TogZmTo1wzb5J&V^dO8-11C?<!KxB}xwo8qXT zOuC(}`4nrgv`wk@sW1w}Gmb8Odlxq%L}J0_A*`XX40>1UltcM(3V1EMj+=I3*GuR7 zk^P_}j(vZ$2EiM>GA26%LFufFG|X_p81u82zkinhUdc06xY*LCvGs_rP3r2&iBD^E zi!hm^2Z~VLuzL%5`d7W=q0h=+Lj|oDI3s-ihcrv#;Rh^74|NV4vAPdy-~n0zXP~xQ z`>Y0R_YXU6SG+w+$@aCsz(wJS?WFo&jLVf9NZ-DJkA^PmQBFm%=aeSZB(xP@Be#+y zBNq`$Js7|Uyc3wi%Kd+%5}(JHzk_}6j(<J=VWR23qZ^It<6TuxJnvRS0=v1ZB=I?B zp19q<H@GG`QtrrYn<)O(%St+TD%OSdbiFR=?nH*Y00p@Uv#NSRNw=&jwNbdbLxB^Y zzTcc;n0py8V{3f`>}DEnobmgEw_wI6TA^WPWBM3;SZ{yf#i#OFx!lSwU+$L2tJADO z+m9^54;MarE|`5wg|>n2%H3kH0Vz<GRgo97x<xyBM1QR%R$e=3;(Wb>u<hzla+CBA zP2LM`*2mdCy{uA~oX5|EVX<|?>{-!k$y!)&oy!@V#tm)WPJ^TG?rJ+1ox1#?g<{`@ z_YM9(xx5`6L^D1>000pn004ylY1bW1Oq_q$Z^y0^KWiIE4>NMh9XxD8;KI!hpV%Vg zc0pU|S}ka}WKJkrZ!x?%3weppf&_Rl%jfHQ(eXVLHm((Rgg}zcL^c9qHmggq?4qM0 zRDd{QqS@iK6^l&jKlpFYY?;rTDXz97mjC(JX&&~<yRV|cntm!Hwr!D|Wwyvw%bzxM z@ZxyQOn49@1UBjRTGD3!A8ZfBK}H<<Y$DR;4MY9awTz-qXU^8bx}WIs1tmK6(zJrJ z)k90ShR>hE!112`AOJ@}_a9z*b1~y?L}&YZM<8J0c8*_1kF3FQCSl=G0uY`6B^;6_ zCqv(nI%~hbO%PpCJd~?X0%GQr5%={3NN{QLETH*5f1eI08%h$e2u0iEz2l>1tJ%-Y z{3CNRDAvp6K$G$@j&~*8uFJce4zugg$SrQrms(!(&`vhDz5)Jovlr4^YQBH5Q6vQb z0Q~=Kwv&;8k@<h^^{}Rv9d;+8&uZ<S1RhE0^E=6yl3$a!6Nmk5IPwrB+y((cna+e^ zx8@&`)d<hG+b`2L^qYu=tEYSc+89@M9G}PO=t{9zJRN1-_2Q{ybu2359I8`i7n)C~ z+%rYURwv!+Vbzc1xMnu1a~E32^jd-x8M0i$GHB9Z*8yywJojdm8rp7TQ%d7zYzik` zPo%hDWD%Qub0Sb1Xhb`cW8<6Va>jcP&}u5kX)WRvMK#0xKg~-!-D%P}L8A{V=4bU4 zWtD=eIjrL*8XZdh!CIs(B^jRe{S47MjTW*s$*=Y-hy!~@5?&njA<dpbXlKoi8l}E8 zc1g$^(4^U_DR7XJ+2ujnX&aGS${se=12eZZ!rheiEp=}BsZt{FA=n-kwaIn@rdq5E zwK#ReLe(1>mmkBhny|8hV}Qbm<Hh$NZBMVzCm9u6kgtW#{boUo*Tb5!A|_pHn%9Ak zyqXP@R>v4Rv3wN>k+@pV7l$H&4Fy7;YXdu1WrQ@}Nm}hrh88j)G-=9)Y?Jba*Pg)h zKrO(X{IHxvu9}VfMUTp5S}Ks;)_D8;yOt&_#>WZFtfK}v=71mPi5nE-VG|W%n$}g6 zmIxhe)b<J9$gE8>z;#@Ge~EMhB@H!JY+1MnKm3Wih((H9>Z{V~VJ?p`?8#;90Yo`N zP@wJ0maL)!%?<|Sa2YJcI87Alcy-l<wK-6!8bOIzGe4)72CAiX&~TI28z`y-`QMUR z{{5?PA-tUZ3u|x%N-JmV5J?}uihE}xZIbwV24u)Qr$Lx&zQW6xAn|r=AdXKS**ITH z57@5nUXm?^S30L@E4yj6D1&Y-b<eDhKaCm3rOvb5!7FRm&Q_g|(D`-!nxq(VFC{<T zyib(dxe?8LQBv>Z++}<5n6TLxdkH_P0W<w+;xqQa-KF1mpf#GF0)n$2hU`C??4G71 z;JZ5Ii`So_$AYNQ5b1_fYl|T0%h0{==w>!+2T@_w$hOm;U5V<I5PeZ7rEoY+#6Nd^ zD(hYVulBYo`E~c23(lg>s44}^{*BzGsld7lhx#K91$l_y(+&52FB%Cb6FkZZWD=kX zURxP<H>|7?Yw~J$la|jvAQ7OxuWi6h09X2Y`(9IQ)h(41U!spB6%M0hkE&kFb1F{W z7(Hy>wr|AIi9xY{-{@u#Fd&JDZMZUoMNyou5FY`5d6f)y<92>sfKr+g!AZ<3A$jD0 zfI@wQ>2<?=HB79!%AG)0e|fexK(xn8mg6p$OxxOOBV>}M2J6Un4DZ~8WZ(=NISpV! zzBEMqU1$iufJ1>-yEGZ3neaN(Pr@aGR%O>tXd(h#&e|5w5OXcQmZI7sFo4G+02iWx z!txh82HlRT>)4Z><w&%WXvSS9RfEO}cA8vpG^%?Io%oxS6xh?d6cf>~xQf~;0_KvZ z#hV*BKp|E=zwqaVW>pB(tqnE@vjf97m&DyK7T%;oHvTbN9NIC-6aI}36)p)E%;Rk! z_$2x*60d|N5EH;Oo1cr;(>KGpvR!QWb_4MQti_#wOR{qg6KxAf30aKCxu{!7C>-{B z#I^VlHC4GdB5!UPyXMe0<wx0stPOzf@=sFqT)eYp+ncuhHH}nNNqhn0a{7DBl2p#I zPQ#A7@=>*Dx5eG3wUr29emS)0%Or9zPG0{PTz2_lzia+@=?sNfOA+mAy1ngZi_38K z(P*|_{~iH&T_v`mVku}aU>#<smBIn<Xy{BeAaba0C~|$N{@ThKc0H3zsgVqGiT!>K zQi7GyFPGO|Zu2=ilR<8#J;)`N^uQ^2mQ2iB#@Z@XF%3sv6kpMr4v(Gr2aI!m(?1x{ z@*){+YmO_*hV`}0WQ2ER9dG`<zsTy{2DX(o#{4-UQ>HvzAu@ydvPn6DOd2vX9A}Ao z;Tt<g$lM-MxSU1vJZ7K^zLnbRW&xN}hmfbLYqt&k^fY)}r$v8;)=~%U^sE5)-_og7 z_{d@o-O^hKM||~X2lsW7b$u+9tZrFy*K%EAOtrS{KOpyDKkDV?DYwTyb}*VqV$K># zkT+fURBHQM`W9Gr4MHm*p|&g^D3_1~;S+#c_DZ8l*@x0pL##JP&)DmQ;Ofbc#*r+c z<!d}HichoJcLvV6T%BpUnjLNL435~Z6Dy@M;ccl47B+h4pzP(=KS$dPmY)>-qe}fQ zA_niBHBgN%2Kjk`t{tM11U)7AqG!mtgZzrPkFJa5#@tez8S17aN$j?pi(THB&OiB& z_8sbcXtkn?3J_v5XRQvn;|Z6@;v>Ci`XAFi==G;7{+tsRhZ7dI?-Q6Vl<?=ch{pId zCeOBOR<z|H=TamM24v8yoE++C4Q3Wo(MIHqdugC70Eb7Dzh104B6{>5h@n_&Y@rAC zs3VUGh(}(LyrS?;kmRt8G5eCG385#z%0$5f+fuQPg8ZJ;viED2uwu}bnfgG(w1kK? ze|)euQ=k&NyBr8=<1SttSDlSzRKrJJ^pwR>=4oPrGVGcw97RZdrP9>OIw%yl5Xp~5 z4WBEL%0!vl=UDoeqTQEuiG{J;Cd?EzdnpZ?YuSngLn1~9rr!=Dn^w)9`pesrpQTmh zMQ!Z4)&2$oVF{YexiEH!si%m-l^omY2`YYa`~K`tZ_fFKki6Y2Zt4y74$q#vJ)Vw( zqNn1Ee_K54AKRg)_Tu??yWG3&T%A7kFSo{Hhf+L@<1L6Mjv`h~o5xV(Y50rkzSlc1 zVdmvlN65!=fMwcqvDTV4P&}8Xns04(E47z)N}eswd`0xTqv398M^P194H^)zWY_|0 z)~fyKg-lHi(qgx$XmREuC-WF^kAp~iQwLB|L|rvGJJ}0b*GFV}t=;e7>bQeXy)kFb zlv3ScT*l)6q|k=X-9e^`g}b}XgnG7=3I&IMi0wziibM~=g6G4DKfeZ1oY1T;*BPx! zNz;eV_#PCp4v?E$#$k)-@6`Cm#~AS}d5~?o%&x3u^&yZ+w|~Q+hJR^x`*6pTu}+)J zTus&BxYZE@d!jX1zg(KMoAUmFs|K7qz?vrcI+MbKs7O#aBeYxA`zC)M=UzgE;#11Q z(!}lY7~P57m=N}^d%|4E`uA31tWE}bq5Oat8CZ~l%g2nLHP5dg&(o(p(4zIhTVl{w zZP(;x9;mu__T#2Nsg9XtUB~6tJ3u4;t5+xia~zdmXZ;Do8QZ92CkQhk?#e={kW*U2 z?yOpI)PTGsjMwYLyW9SB=)kEBdTLQ=gvlIpT&D26uDROdaAlK~)I?yMxhx9|0ze9Q z{G8t+Z4{lSP+jSYS?Ssrjl*$nf%U5dwiN-P)@E`2SuEj9Mp6ex7mGy>yA?0X031{Q z7Vq=U<~rxBtg2f6l2Ys9;niO1h-h5-i<{M)>{hCr4Fi1_Q=`dO>AlTkT|0%DWZ?@= zCzBo-L&nx;T_d{^0jNWgqJxU{rL@7G8w}zOJTlw}u_JtP+X%gr!CZmRT?FuV^p^nF zwgU@S+4`6k+XF@XhQm4R%2lLo{e=A~$~Q0g8vNc5$KBHrzBZ&WK1YEo-u_z1K7x<W zb<6;l7Xc*y*EK>{n+vYF<Y!frt)t}p+6HU{pW)S3>pYs_WX4UI8_%3&m(LmP!9Knz zR<{S&hh@39hb!yKGo84$WQ6XY;<Sxo-8^>iRk3BwxdkKKuDQ8g@7Gx*_I4`kt6(!@ zJQ}Hugr2Q{=ED;%Ruw!br#Ecj#{gI_yB)3z<8PkD?bluQu+R<S&3qeP*N3?iEFbsQ zIvj4GhS)&Eu=D$z<RF_8*flLa`d>dCeZK*WdTG5Z7A{brshbInag8D;i449uoLQ)A zx|3}RmuO0vbkr8u;CHpDZ270^SWKF=%S#O@^yGT?KkWCnKIW?PCRTQWMAh_RLOH_* zT&#Sa`M({<;>d%=Vl!PX?u6CNIl(P$uIMV^Y)EX*wZ<YSn2Lu_>h}}MB!Vn*gDEpe zk2z(ZDNF*#-}Ez+kNK<ciK1>-^6Nqvs6Rp^5PUAVhvHS4_{~4(IY<aH))hB=i6ewx z<g=di2P(5*lQ}X4MncvPdjAO(=t19Ts6hb$D1Sj2;{SsRZU)Z3Si$iZEOe{=3M|-R zd}eFs7D8G5F7Xlc-Og69Yx)d8TL8T%fQ0IgMayU~Q?i&huX}gJCStPJ1U%XIJ`skH zB;xV;6ra3#e`x=9ASPFJBeSCW6bjcslWy&Js6=e75N(S;R%vtR&YCrLwG>mlEc4Fj z{80+KY-ngk&&~UnoXor6i173wnRzOEWP9yRqx7TlCcbrv(hIM<?5I;F{#lH>!#AmD zvYQd1ibqvj9WO$n?yqLnh>7b=*Nt)uzQDG+{oq29V_TCgCs`{M+}c&QKv$67_F0#+ z$R{q7$wp;Uw8;<LPooO$o`$y24oDDHUy*8Uvtw9e3FZdj3f&Y}3~FAZSU%$tar7D; zX;$@D4Mpq^qYUcIf~;~y2+tlNmRMTu5i!~>s@7PNTDjfq#9A8iB~MrCx2ZtJ9stZC zfdymy2GUWUNrWOt3t)lr3e_RY(DP`E;e8x5W>!#yx>he@5M}0JQC;*%604B+pGeOY zLOA|ad6b=q<^W}@uE^ac<Ajk&Gpa~_%`?!sd+>;2Z+{xPBxfvkBN3Z@8=;+$!EJ_V zk4zOUclSnOANF@9BHakPFO77S8Ev5!^DR?l4hw!<{VN+_4e^F_MXcvSl|_Bm)E8XH z-;{DnWH)QbQoV4y*q2zT{;hmx<BpKRmX(PwACh}=`w;Ar3@zEQDj6@@1Yzv!#G{dN zA~A!uV@fHsgJ(7n%cX}t?*x(R!VO2at~7dv`hAo%A-KFPK*WlFkj-_R@nmKR)F?Nz zdfn_SKnb_d-{qCZ%N1_CRudJi{e*R`-JEOz<g{+IdNpIq74XHXJ>}7_Zw)9~v^}X1 zzD*o1^ecUXge2LEJ-{hB3UAAufoXDG;cfD%0e|zC{b+|B;8u19kk4!UV7i}5$Ec-T zw`dytp7%(MYcF$PUWN!!h{F}51dhw{3AuNQ@8Bq;$3R{G=HVH%qs~$0U#R<*&Qs?k zgZC+CQ0OlGT}4^>fYfSd)mV_|gRNLr(*~f$gUkhv@J>5A-1U$9Y&grWQn6^g@a1=! zDrLw>|5E~-8Fl8$Sx{7-S0+2oCc-+aG7*g9R7ie4Y>=zzm0<Cdm%pd~PxxC~ma}J< z9p8<OUd3Qbw{rfj$bdwMS6gyIKVol~cPd8SM$FUdz&hSVAy)>Rr)MXY$pcc;bIGcr zf}U_6>CG5B@k?Q~1y2h&bSzU~5_59SX?SD^&IRnx{=~#cFcUwCD1Q3gr6OmnWE3#v zR*+ElB06qK-_lL*#|ERX=9Q#3Vc1}m!0OYCu)&`^h18pKS2J)Bt0&JLWnLH>TVpxY zusZU3_m`Xlo~{7xHtUciy*P;KW<!H}9Qs(WL4JelN&7W2d9KT<nMP$X`_JT)D2_;} zrX@ZSo#HYc8$}q#2JuWFscQ@|5Bb_;Mp?UMXV$oqAd%e#_-~Vdvv!d4{ZH4(Z#iO& zfp7LSfl}gvQ|{4?^*q!wt;I4XoihQ9wH^?qe7BG2r#^45G_uVk1Xu<U&j}fUL#W!O zR^)$l-&>sbHB?K;sT*E~bMnBSqqzTYDE1AtAHXrdom3gs|3)5}#N9wKy8=tvwtvPL zYKECnV;7j`D_zThIe5Cmv?_B1<#OHhL0`?Cfe`c;?YVroASoE}s1w#d2SV-Av{})6 zqhsL%$E@BT7hF6;ORNOO#=CVB!;^X!ea#oO?0JrG`o4nD5xw(`T?~mMEe)u03tzoH zXD`7<lD|ByHSSG@eIQ?5ko<4?+Fb<^BGxb6r4RQ1QC?<7)^=uQ|9!J2E6c<#(j#=A zszn8;TC6u|)+A4ukUkHL0y<PwjJc3&1X_V_`hyPnnq3viWNAjOgjS@p*`0XsQp;Ae z!eQlvf9E2TPc>|uA-CyO%frI-fv>#JBJnoMnhzVNm}oOv?fdtx%KVO^0%mj1-ZneF z9_<aG5OcA^Dze?#!>=%y{mOvP@Xgbb2zN8sH3;zQLn;IdGAlIIll~#8+U2vCQoQ4Q zyW4NyMF?W4z0ubN$7&MxQPJVDp%-^*TAn<sE%`gQ_H+iYTQuTziZ&&U;o>Elmx5*C zl8EBL>V4#akMypyEF&#DFifBLo8ZuOXi_BoXhi9P>6j_o<6W|aqa!8F@iIys*Ucmm zC|9$cn%;mVZiX^`TttJSxyS`wqeT$>QlY+OFXDmm=0SCYs*rDHb~p!TGyXN>KVG2` zMtGo7Ele(ENlXSEf5T~ux65Q&>Ec&rhpy3fNcQ9QS&?8XgCc(PHZP^9+NSW1@3Xg1 z(Ut1u6c7Bn=7=7HA{GoAGHx*lm7C}$2v(z!dL={#L*Hp^rx-)A)q`doF>M|b?FSiC z?=sRyZ29qf6mN#+zPwRLoO;knI5%?D4VpB-<er*HSgp3;omJdJM_x!ZL%-`Wl;#R} zdjne%WBT}f&02IAq($n<-8so9N9lFq&FV;WMswu0XID^6R&F6_1Z&DdWycQP!=-3c zAJHwp$^HR0{4b@0yX-!$*6$gLLk0l&CH4NV4ARWP`L{SvtK0mS`teui;Gh(!E^0($ z8)?gs0VkBv47$RizVfScz-V%gX&);PRdi@7`1$KrwDXxzB;#-Jji80%@v^n;L??{^ za$6-`oNTUN!mOR2(x_;PZcj)vHgyo3(h?OwaP|+ck1G%|Y>6Vxq8T8tWvsAk3s_S^ zd8IWD-9qw|-?%ut5htF6MAw8Un8PxNJ~<U4l9A5;X`Qk3*CR-d*0Gp|Lg{D1BxX90 zvZP&=A;YzpxqBxQo*aJ<!G}<IEs<&O)B*nNaL;33KHJ?!0lLz;c2&wAY(aW#D11aa zIDE;7UKL*d6{ke9SQi5GYr`*_nBx$NDbEHYF(_f%c&x;rim_59BS9@?VP@TeufFoN ziR|<&iaNs4;1p3;%1B0?@4MJ|#Pfw9>`a*+L%Hqb018v14e~_XxK4OZMAg5+ZxsVw zRa(jVNmJ(?`O^Xyeb_0Aj=ZAI!(gz{FFE01-#*3yFAF|{81;tF+20nn)@Z7eFf0Z{ z%ljZVrdidq%V#6B$m#R?+y=Y*9NnC}$TUBJNTd-fIS+0ZvpNerTF&Ri5TxixSDwkx z?1I}!g;)9>O37S=)364dsRH-Kj{-<ar3g|Qkr9*lKG8#`+a&cj*hWWw$mz_8TkY=T zI?}BdZ%~#D4T1~DaE-x<ApjTX!2ZdKW-(xHTBJBK(X{-Dv<zlvl3GCuxjmX!QTUe2 zXieV{UUh8%<kSm+E>Wn#QGU?(M32XJq%9{<C?hZ2O8fhtf$y*7eF^&qrGv<ypMGPT zQK1dQSrOOz`;t61=p}TBB&<f|k(H`zfDMHsp~{f(x*Q^Hcg1wV(9un_^sES^+I0Il z#}mLT-HEOE-N-`Z@Thb&FM_l2gpEHwK+?2^0|F9*>QI5-1X3|r@v^u1ADr0HvzacZ zxV<<1L5lzZ{p)4hijpKMT?~@+s*oYdNkl~V(nzb4Mh^a%)k?5XnNQCwr;?6u>Xlr@ zB8zwLq?+I_Ps_7|h>scgy0|}|0}fjZSyHL-R46qG7qTM=0S2^94p9c>Iz7Tc;wJ{t zZ3e4kH{H+2sq2KCZ^W=XN5(*?a*|9nUeUOEGha(E*_}ExhrQ7OHfSD5xMJw@7y<K( zf8POk5&85PdE6cSJ{AMTJ{IQ}*B9gYvtr;(?+=ge3-QfjU&Lbddn34-QJI~jRVTee zTk{Jg7EK}iY3$A!#H=x>PsM!}u<J=jF(|^sB7>V!ypNG*w+7WzEcE42(;t>|HI1Um z<JLuo)tRQe5PG<RC})9GY8jEcS~R7#VS1KDkx=v!f@Rlut|FCC(%GoykK^8pyu@#e zHWbgS6DZk;n`Bnlj(3$qb{e!7!%)C(EtpK+Fk&tuZUAbg<q?>RfxVe5K-ECex2*Zg z)<HwHWus)|F9pz-(RjC`!j_dGSAU+`eVH<eDh8kpG;Z{3ft`UR#3rSN(<jfjofmD^ zxX1x`|LUr^T)M_13Wg?!ibONp3zkFa4E78vu{l0keKn6v{Z!YQuZ3)H7|keP(poLC zz5mHce3GbfAgfjT65hQ`MAPbnQpd$S0#?X01#R}()NBLykksku?cB6`NI!Y<gjiwr zTxeyAS)>aKhz4W`Lx$MP?}ZokMV~0x{fSF)bE_;8%phWmT^Wz!_G~F6J#Nl+F3F7Z za$laaEgiR5?8f0(DJe?9puUT-&z7%ev`xeJ1%y`>n<YA3hYS%?-7%3k^#2!!l$J@q zeMMCJ`$nQP{#vW^7|nZL2BA00p13D?Paq^T8Gsc>Fd@f)#;yD?IU%Wd*)XHe`GFP4 z;l}7z`*HRTRC-@Jiw`VIC3M<=(QUh`3g#B^O!&{uW3@knj5(hml2RvB2#DG?jLhUB z_oiy{yw2e!&&E`%bS{VMuOMcIgJhA(c4ul&N5(DgButQX>Zqg>7b;Q*j+Sp=&B?oh z&Yu)-v3g~hSA2N=Gos@7t&%b`+5)WVj~bW)f&6a~nnL`4RuZ%tovA*(R&~0p9Uq&M z?wyw<<NBExJ?qY7JJyF0o&NdL{BEJ#LCd}vuV2}etty?vLn;QiG&ROM3MiRuUd=+_ zRHy0oIIZ?eHAa70^)Of+oVxYeVg9nLb#QNEn=@tZR&+2q+1z&^H}Qh+(%4<z2;BZJ zn3zxfjVl83`<nC%6MxIl|E)p4N_-1P6C-B}JKNt{RIDl!d&mm&+s;5VrecW0K4IxY z!*uY#)NN9>aCL+D=f@w1w?rICI<^RG{^%w=v>qFhO;e(kkCH!x8y;V!2U}H7K`WhZ zboB93Ai7==ijU?tZ`H?6489oQCb4MWuGJ#(=E8W}u%g=X>0MgEVJx^I=<OY{9jnyY zF49J4wV6GcHFZKtx@G>YR-u?_v2LP<;R`%8Rhozx|NR?UZV(}TZNH$v^R^j{^nCc2 zEw1_u?zy?Sy=a7tU=r9TP#@3_7_=B>sYI`+J&O%CS0qt~ldU_gNg`Aj{uM6aW@4?& zt&V2^_}&o!>fWm<!bo=HrMF+Jw<9N&Z9BCBmNLfyLd22pQyIFQx4i5QV3dZN-XTw5 zHYU1s<KnczI=cJ%7LQT<K>Tu@+=q}r&+w8P_NFg(RMhv`?64JM;q@b-m>#u`H?MI? zH7BP!FG95Uv-^{k<R7)V1xkuFjhmhMAdR4YS0E22t6|-}go^5%VW~X>iXPYWa#ia~ z{{qoK4v8;r#IZm!EgG=ctt+X-58D<ik!+9&OVMTl&@RUI>4XEJI+0|f$P>S{uiATS zH*4;zY%FHnD@%f|>Da2H9FG$TU4mnE-KbD-U^$ytR}&_DmLsbeU(hlF%RoREig<)r z3n0wIYfo|PYP?Yo$VjXa-Tsu<0_lRha6WR^ZDb;W0uv+{dJcOnlGT7Ac59U^!#4ck z;agt;vIMmp+<+4O7)J)R=A(1KW2_^CMqiIe(E7_HG2D|^K~69`8fZiLrIO}QWa7Ky z#2NQSEk(pPY<d{H#URg~C+pn=!*ZYtt|%yJWIO`wpf!S~b&NWm%IG{0q|#Qm6|Ux) zBRS8fnQiu=4k|id?6kW0M6^(}6nQGWke%L`csm*5*w#3`dO7`iM^m`NndXU+Vj4Ym zAT}~gWW6$txzJ#Ce~m!Qp?pHU${BYcwkB!T)PEz`un{eKx%ta-2o;BhceX+u4!8gh z82iqyWOk&~5LDApJbjso?&s_LqUg=%)+Liu|J3Y~;&Yn6qdDGel|!TLNzKfpFc{Lh zMV$wg?}D(%J4150_->8Y^q%3!Y;*uf)<J#c)h}iNgO%DN<DuR&j9J#_r~HV)%QJ$m z11#^i9mjAo!D$$nLrsB(rO^)ONpI`xCVUM-o8nPiy!d}jCw13>Ci~9QNK&7Mo;%zn z`dn!wjL)o{)*&z=?jU7#Bk;8jeb3xP_&`$S=@AGQrFHwj@%g=9RTmj0B|P8p@{ibg zdO5+-jw)#AOgQBFjO;un{{^qOJ#SijyhA;_O%2~pe$Ovak-i3`v_8WV)xpRC)2<G4 z!iFiO(ASle=b?_SXP!05T+3Jh?hk6^C0g@}Zfdnxjab@q>UhmnwO4~Y66k;`8u)~_ z{5%3xt6ZZ!oI^09)r?@8Zm)tDPSeey8UDnq0p48q{wFyVm=(I#@r$5xe}{nnN8@W@ zYx>{tDO%-LZj&D2E2sBBxran4Y=qa<5Ghk%Faykx-a?SVG102t{%Xoq|LfN6JAQ8t z7E8s(=rm`y+sjRZ8jURl+Li-LoeI`<lbF66lQRjeO}kKZUB6kKy1}|y<uqr{5xDCz zx#cbzTWdhXu026I=?>Tzo|b@j%jUyonZlyVR9llN<QQ}Ac>mJ4b4#ZpTxFY{a7~h6 zf7iR`+r>juFRT40wN&k%mMQ^Q2We4Z)miENHdi{gmDD?2-<A@RQIh)r<H$}COCzPZ zE#_$@&{}(-nX3$MnX$bv{7fyi(MU4)<`TXCRlG4vxC_^%+l9c*?PGx3ejO>Qa+)T7 zmnz2iCatWxLc<80%<wg+9vz~57`rnlSrI5^UlU%vZ@(1aA5nVoD6w8PmrVc8;g&bp z5*CKFJdyuEnMd+l8&y+aO5a%_%VvjJuqRPgGL|uPKj6+B@eT3f*79KAKH|}oD0s^& z_zpTLj*oq$!+sdr94l(plzhAYtz?1(PQ4fz*b_lCmN2xUhPr`$*a+(bH)f-T=$k3t z{Sk#h6PJf6S4~<6BZ-)?PTL^xAdO&5RXaGU%J-Q%XUk97W}*@%kCB<f&cgW8!gCIi zv?t#ld=l*T^HdrUAu11<N@NLV`@3c@bOgvU`NsOs)^h#MEaI-80d`eD9S`c{av&Sx z$TY!=7eGIwgqLbP!ffElYqUawTmuOzWo~%ttVRmgQzSLg+kWT>93me&1-KZof9=S> zj8-yu%M1OZZ2v?#4ALnw<Fk50);Qg%{HL_F(Zx@%>R1}rZvB)f%#aiHN;Xjpwq{Jv zrT)eGm)SIWJC+~7Yo5JJq2{1veF2Gj(n)njcH-8_Xb2r#gSC($23AaT-J8GC@v#!I z=Qm8J`p-8G*(sI);^pLa6+F<_6)cr5@n4|-dB}lka|wZ<003kt|37$VVQ=(XQm-{_ zVmHK*zGrIB;^5Q5UK6hVXc8XZV9+Dyn!*5?CU%oFV0B`8j;^J2@*vY@{`9C^tJx3I zrwh9+RHK^o>HOFCZ%Jy=wl!3O1SA~zkD5XQp);kiAkTQAJc7HBJB7vrw=7XCU^co9 ztHl^k1W5G?35gov&la)cG^hk9vN64DEzSoIV!Yd65q<m#F8tS-?9w<+hrxH!C=A8M zwQJ=tn?sU#Ls3BYcl#?^_(7t2X#)6hs|^RiI+^ahU$bT<$p%7uw1>OQL%&3n<3gTT z2s6YP(wL<(?FY36#gghHq)|;7`exfmT=F2aJgk=DEgA71Rnic3^2-VA`PThisam#A zWZz>Fwf!J=2dR|kLC~(^xTkTt<iLI|0ihA^V{*>*d7Lnd%KV%d)Eu}u7etG&I4R%5 zbg_d04c6m3m0b9yVy(9R8`>I3R9C;z)Xq*FiE+PN?VgUPH4r)<#FQbwLUh|*0lje! zH9Qm<3Q37AhHK>KdvOJycNxj+k`N>><<21i>Lmltx<-d)z3>ty+lcJJ1W^CG3ybl{ z>XP8|&Qz!t4PBo862HOU=d=dY!q25?_5g>J+KIpe!J<44X6>SIf2r(EOqwG=s87KL z!mOS{0e!TGn0f@9Kt$ih=vMx54T|M^=#;I7p{W@e4I~O3MYt0z;ngkR(mcpCxoim| z&)BdfE#yDq17+~4wQc*aIs>HMY|sLYE)Py3`#T8aP*&ht<&4Dt)+5?z;oF^cgzEH{ zi3!t$@TE~xlD!8Dl%J)@KkJ}~{_wesKlW0Mq=($~x0A^RlittX@XtVuJjg9yv=w`s z^#1@;1#$pvPolq;1*d3@3pXQKJApYhtO|3m!!F0KJZFahu<Ydr0JMzRcuc)g|NTQr z?GIy~FjqZKGkOykc+jpD=ntGyWc|X!J8u(PN8*p_c+0Br8?(_Y*NDWl=?HIHN0R3- z^~pBYu&ED8!Hg;Mo>v7s2Q=nRE!51i79(!-gdsr7zQ+yQ5Dx}lr<+1x?0+u-g#&}q zgy5hUF{j<lf!tF1*&;!eXrn~jZknw4r&_&n-S}h!+0BxJc#~swR!Lgw(Vrz6H`23& zy0{Xq<{R(6NPFCMq{WD?DiE4PdMV}-_t+7K)akCuPbQ^z(7?Y&+7;SXgHy!mvN@Zm zmz*U8*lS;Q=Gz1o{U&m7Q#nB%F5tEgvoAIlXOZ9Z#P-KV@P>bc_wAwCrXX9<0zjaZ z5?M(b0!V#ao({lzZm<y+=4&QNKN_)>-X+av#@IxJiL^GK?NlbI&U%I=Q3{oexiSDR zUY@ST(NIamcbA{>`g7|5?(CDY-3Vka^%cNG?r9Q$dz-l#3#Z{^a(v2BEO2;lE&{?W zNLL7C4oTm6&tOCOX`hr_$%<8d4S5tdOU4x1sz^`MOdXUqo&s!7zE2_*fOLv&Gnuz? zPMdplj*f4Wr-%vpWE(5Yb;AjY*yF0IIn;g#2#IWwWQr&#|Exo$uRm@4AuLenD@qo5 zk-)r5=zx1^tPIoK6=<@&Unnwux-5TXB@{O4qqgo|ZLN&tt;Ya37s(+92V_o&Dwa~+ zCOy00a7bNu%b{H~MxlWisOybRnD%&)HUa|RK}LbP^_FOa0xvYoRaNN0FIW^zIz#3Z zwJ5k^ph+(<QfvwCLKVYJepj8gVs+bypf`38$Q})fJ$eaNlHy6=dv46C`E9c@_`_Cp zP@gVL7|aT5jR#c}dbhgxi{;dzhL$$Nl1em`f?pNly6{rSIpMlciw-BW!jtI%siA@& zUB!gF4PL*C6G5iONhf6_O*@^G$+%FUFwtn0m|?j+>yNt9Pc)&{`*OxMKF%eARtr^B z!wI)vO*@+bgbR`13G-93)c@h?9hfv>fF<3w?P=SZwr$(CZJTe~wrzXbwr$(G-`R+b z*mM6wRYX-*=2JN*vK|Ji)*(_3L;>;zPATXQOww`G;OBl-+$2k>cIyB)=gS4%wX~HZ zD@QN4HlUn0FnJ(Ff<O*yJ(ZPG!R@f|+iHhj$!l+umLwt*M^C;Y-Fz<DYlnf8EvM=X zUKjHf1Cbw#Za?<mfSS|00}f*_z#&?^C3o%fPA&uCHHREmD+8^&&tDFA3WGh&;PWMH z-^mC5vE0&^C8ycilRn=5Or*X_=iy#6>ejfH0-<WosxPkK<BsCSjCXw5pqIR$jt#nv z$zv0fj<gjYJ`R2Zl5l*m<?vY#0m}7T8;l0#+u>bQhm0xUxj7I+5*t$jRob%^P=ukZ zAfAJAg9R!QEjomwV|0{jnI9d|s?y4SH{FiwB)LCL^}xS|6n&ph6-n;2h>Yzef^=us zGrWAt^wy~EH$bs6JWHobA>SPrODluDnT~F(_}W0bFMV_x$IDIT4EpLl8m@}>=?;0Y zKM`ou_G{!haRk$G=|%ou56|yub>x0mX!A7%X|zXXt2}0zU_rI%T|VE;usvhp9}4}_ zcl#E`-8XKsc@=Hq&eJ-v2(&S7`t&(}EGj;4vnRJOt6VZh29`turwlm2TpY#K7hx}b zgP#y(J+L;L-ie2dH!~O@X6iNVE>7@ZX~Z3Gq1;9L-fX>?=*S(hGcoi0&kHh6FRMOW z&2;53Qn|b9>CFGy%Xn3g@2QH;)6SV=u!dXReELNhYg#0IgrZ!)2ob$e40CBQGK4Nz z!7_xT-D!wTN8L7?W9GA#z+qiKrum}vyZPbitdYE4_x~BVA4;n_8NX_c&5!8G7pjC4 zR7STx@E%0~*6EO6Sr^sTYck$no5#F>6vF<fyL<#|MJChkz8|-DpPr71%X>W%^uisv z!0}0)XnFVkCoDWvt$rMl`4-lU7*{A^Ku=V_1S5!m46Y+^ZP#s+t)qB)SIxF2dO%K~ zTi?q7W<Fo?=n&v9h3nA8*O21jG))Y(^KR>c3q6qLv0`0rx7724hX~RPq)N@Gp>%CO z{o--rYEw?>l%>iKAF*)hIx=45!7Q~w3}ctEOFl{00en)42bN*t*5E0n-W)sO{klBe zd-m%f{`9o<P4!3AQ2azM6HmaC3U?2D5TiYiHE1F*(u_qN`th~`c237r0#4SEF`ENn zk1ZsvXJH_t{4?gJ;!kfcHpklScLDF_?MmIWtiV0@T-|pd{4^wMgWJn5-tFB>XP)@r zAeApjedED`L?G$2f^gVMsFMss<3Um(j#}ONf1s#Jx&=!etZ#iamblIqFR8g`72}F9 z;dq{ELQ%}i14tSxBq_2C_tG@DL)iPtT|#tzs|x=2p1q&jNbFKSFT^6N__}z@nw~UW zq1O#9wal`ff=#ljThE+9w#7FBZ}<ds49`F@=geJw-fmRgrGhu|%PMB<(tBg>OYW(- zv3$Qdb8?&v#!~4ur}3XZ6dnV#Z7JEu__|Xk)Sd-9zU}R-x2*1d-b-j5a+LumnUY^s z#b>QQVE=nm3>Y>Z2|xz|a!>;Tg8RQJjFr9R|Hv=bI<|3#98NzweL<`6#mE1Qx3+vQ zg$WoldU|9WxO>(zrpYW=nww?YT1cd0CEVi}#{A`b?qt!(y3AjKz%-2KjHOKyXw`VD z+0P#y9%`!9!?iLiZAH3}&8kFdirEh8cdO)RH-(!pPTJMA(D>U_i_D`ZCnuw$Wmj+h zRn}Oy4EAcNbop-naa^rYEnC@Dp&7Exw_oPz-1M(#&WP^feMx8`YvR6%gT)_WqoD`$ z;TtUJ@L0rspZsi9{DS}e(JrA`baTG1ofl)HY5y`7UX0loi}mlPw(h9z;9lgrx{Sb< zP51w)NYc_I{_n+?{LjaAK|#t-$p={9O|FW1n^Vn-vqq9)PBrNDt*JS+x1djvl_{~; zv>H8)Kk#puy*rM^l@1TJ@=X`Un|`K1GQGoQap1|0{e_g(m($<qx6UMQn?nv*{>cfa zY8KlHL;-Ld5#1-ljP)3)w3iXwzN%jy75PTBm<;i@|H3ww+-r9WJmYeUbll3T>&U^A zrQp!ihRIsj0Z5YjIyRMNx;IO96`ea$nQnL|PbcC0X}7yRuVwZp`_`VX-=nZ?aumwf zdXU()?A5#F(b3T@8&Js?N{ggXEn3TsMV?z8R6XLkd{d#l4JejxlQP{O#?5r>&5R1M z1j(E(G4Vl>W>4o0UP|!i*ofsC!xByLV<~SQuhZKx8n(+9N#)pkP!WK$rHeVsr6t)- z=*UfYxb>i3x^it6Z^tYc$?n!atg9Jfnf4h|bj)3QHJcGa3~G&3X@r8?z*q<Vb|y7} z41FEwq37&>zsaX)^=Obx96`nUv0KgsFaya{NmiE4KqsvM9;xE-rNL_dT6JM%2~>l< zC<M?Gs5X-Rg${rA^;0F=jv9BdcDl+n15)8Ej9W_Q@RJHnUAoV`iiLqv3<*%5C6mFs zGSi-cT$jveXc8l!F2ZuWaW}^0qn1lHRIGII>-8PuP<^qC(}Zs_;t9WHtq(rQI|;(X zyy`jIGm#6SQ)B1lvSi_a9o!T=Oo`NKTG+we-qZsiy;lN`rnYp9bgNiu1Ok*GNNFQB zc<eZiXj}FNu2af8hA9-nDL7=M{#6`=Vu>_4U8t7NdhaD@%|>L5QG!vq0~riqB*Vt4 zeB5>jnOeX4S{P+yVa&og?dht%a-Wj9f+e7x9p>A1ef^n2TvA`hl@{^5Vb1QSh43~S zmDbE?0|c07g3Z!;;PinLDddrX{APdbMXX(QT1~OC_MBMill*SKhT=^B?I?5ZXNmrn zuiTofl>UPVIf7AM17xx%AI$Xl29*?2H?(MclYsh?8!OBs2Z!QOd!a?ED)OHsu&8*Z z#zPaNe-4(YuC`==hz#tW8yw2fUzH1Wxa7|p@b>6`10;pCf)9PT#>K%b2m1P(w74hr zC2{ccZx2y{vQ+>LuCvrukCRLwWic9SfI>bl8aBYaM%0Vp!QuuKf_I7srswj{_+f2k zHNn6S+h)#i5j%ugCl6N1wi5DdK8+zydEG{OhQSvkb$vgO=t}ukM5724)ud3BmvERv zI>`XJo#sC&ixvUAxj-;SWB_<(iK;6x#2L=JttL`zy7h7I-Ohs0?J`UQIH3hQPAudU zHsGYh%cS}UP_+DX-&)3VBlH^)m6>hI{~rY(B08XnZfb|>2bwP<8EULr3JdwkAsg|C zk`4=ba)nFeTa>)gVpS2$o_Dz;Bu-P0iT|<K*zpgUtCYeAXbH|MdX~EqcDtjP5?)f+ z!-jb4dCqK-zB3vz5#BI)FQB?mnXCXCk5ykJ`2b435f-gTW2Sil!K7prH4dz|p?u{4 z1OD5Jw?+K)nruo3-^zFtx7^-dt{E4z!E0a3r|m~2(@i~431lnL@v_shvF<wg;3a=5 z9DORcGk!P73_q5D7~ifuTxAiyVvrbS7fF3|h_mR9GhIsT{o1U@Wvu=VRhWDekVkV* z)JzDAId$M$)(>;FM*7b=4er_;(4DW@og!}ma(LerV$52A88gNPZI8(bukCuYqji(~ zb`#FcF`p&g1P-bmSn$3TrP?(VT9Opv`Q+XOW=#tAXa?(e>eJrw_6{T_gt>*q4qjX@ z;f`fnYB6!ONT2ezlE!A#bIAZV(Dtx;<LxFP>Nu1Sp=$;La#jI1d{J{8`}Je)>i9xA zvM=>**wtajQs~bnyrrmh1dpUm)@qBPHbYi)OwRZfDWXn1hkd|NM4tE}WRJh%AbtIh zG)j!nmA=>u&7HTYGvqiGW*M3u)X{e>##Dc0xz+mn{u3pj!4v!7Hp#;r6Na&|8k?0< zYLF0~$1!|jeY^IbKJZjBQT9FXpAQnY79SA?m?8uLJ=g%10L%bIxYTeR6b^RiY1PeY z1EPSgBOb>o5#s;>W1rvxO)K2y3n-?*C>@Zgv6Ou%%;%{fT}3P~q)ZA0dNa1%L;8M& zVK{A$AZDOpS|+rab}WTxghrn6tC@WSS`_k~sJLyxl1l+hA~%=UMF<bO7c|g~!+}Jz zG#GBwb>4^2EgQ?jRa@tngv60OKJ9sK7RVT;!(l&q(rogs9%KX%@EM))XCoOO<L8%t zPUsGv)1-cs2aoa0egu!&DJ$uqEh8mVUps?=z*u9Qymcv!>dvskW5xNXKa5)P&i9i< z2K9eXJ+b+}<nVkrZ(?$N*2j$(=YG3;q~=%|X#}jBsf_!rKy&Znh#m7E94>rD=z!yD zz@5W~aBy>we}lrXAzod&qJy*86skOFsyg}dxj<%YHqCL6Y`f%<ZFEF)`f9kaAaTpB zV=jSR$k43Su-G9D_Gyil1iMk}aWcb#6*1Grw}&KZJa{qtvZ9BL{$9Rzc~kEU_O!i_ zi{1cJ@aAy@fP!?P=O1&^6|RiKUCq6|5}e-*>G*gEc+Kj9YpoLkI<?2X5Z{M`8<R3g zgol+btjMsKWJ)ABTWxzVzzyinqP5xJ@Dey!aw^G2GzVNn{l8OqO{tv$LrwQzlI<|F zF@L^1d4=U~t}oBc_PJRR`DZU*KG%;Z`<#>Bd0Cx_{1NS&r`OR9Nf8i*|3m-k2EeOP zGA;!KX|l$tja5Ma2ut8~G!?{8a|6o7NhQB@Ei#XTl0cx8NQnhZ5XF(~Pyy~y%dd>| zkwT#rKi+dlAK<@3QMZ8szme9X;CHisJ2YD$k>KUX+7%|EiZUIbt~4>ZTR_iO?tZbi zsDXN6s&G>wh1&!V1qKeFdYyp;GdnLFt@nN&g}QBVbctM&=s%kQ?pyIBOaMm=8p;<r z=~6BDBk|ir(+M4p)_vAJnK(kfc)bnARdxn;6@tChQ9_AhplkAe*l<QgJ>vLQx9xA? zyUzydQ@&QI;N7}#y9dG-zWt(x5b;Euu9-7h@r<+>vZ1sOa|%rgG9#RhJy@X*V&iZF z;cJ|W`!GK;&cjrwDY}A$nLq)XsJu<)t6`y=*Gf%HRC2F8ixr=uS;C=@<E92(rNaQ6 z<0<~sF~sV*(cA+VdazAucCnd#h!;)aB!{Et;>T(DTmMf+i{!XtG2LLt{V;CR;$g8V zPQA~5jZZD^YyW8x91U}bocDkC@CZNcjxXv@=Qb&e&NOVx<}H4v!rCIrqcy*Z-J2o} zjA&?4;f~>)dCpgxp$jbWGwN777n&M8oMGptn=6P1c~@j#$F+3mXV`NRIAzKQ_XC~Q z<BD67caSHs;YvJ#6~wCs?{iU3k9&g9Q-_`=<^d4u7#BqXJMt$K^cz@A$ulE)SQQoX zpIvdj_{N>c!09ppE-!AX9{|x9rZkz6BpRG4Ft{+pfP53+ajiB#>IHp%PI#{Vk7`%n zaUaeUGALp^PxYxCqmc}d*Qz@AoqsqYyPulA3nZ&+o_ddxJP3JIO5~gf{Ra>vJbET` z_K4C{x!nc>OTjt-0Q5XN9#0%&7@*9NB1@bjndi7c`Oo;LGgO(_OB(OfZ51SekSivW zc;T{AUlVvsPBf9<p*a)iS!Xr^#mKU#TC?1>E=)UCs;+S(EAX~n`^zzBeS<>X=0v$o zS33b?P`Wx>!1zpR{yBU{tTyk0hut;%jpWE5`-9!eZwa{GvrV<be*%_|z?}T(t=Qbf z5R<h06-DHKZ`<mbFW4{TuM2*iA&sJzXH>>ciCy&ICa{|e^Z99)mf)u5Lo7mw-C(NZ z0<Co>N^N7iUh;$ILK5u4gUy2)Vg~PkatB%ls1u5o8FX;UxgsW`4v7cpwD;&P|L0_| zc@9di-ysx-=cywNuHP(X@cM%9U9MAw0_Tg>xP3iv40_21pCFn|VaG9Z?_z#HuwWZP z(_uSVVJ?r1OZen<e7A2Ra-&1ZsCjr#!-UFeE{akf;Y`K=8g)+#Ye!TG+s4!7XAGhO z`4xFJhxzvctOzyr`h6Y{Vmc3sqbyc~Vymo8vWznLB)oZT^H$hWkZK+5+P#+nNmieW zSR&qv1sn;*4&#FYBNn3N_qEparA-T-9X;uOCThHs`aqtx-Wa)GaUTjf;!!!heQcBt zp{}p)uRK)L=`J&SQV4PTf+6Ssc58jGoh7V`Y7oq$$AAcAJ(ppp`@3z;88P~_CmOc+ z^PZ*XMu8(mWRnUZB98&;vUXQdD1OKBbJ@p$Nvqe3vV{KkzxtiImqGs<X92ss?^Sp| zaSF9U0VNcz6LaIpkCQ;(?h3<V%gbKUec*R^AS5yhJE*4tms;XST(FIrC25Vg;%HUA zDEYqQFn10J087Y?nD-?+j#G?W_6(`eYboaC2HlfS^$8pNDStU9_o7$GqwMH}ixaHW zo8}v#$CbrN0Z2V@ipq=<`$o^gL<LjXQf_$~lVAzN#1K09_@*02A}bQB2A~okueU+a zGHOuaaupzLpAr^veNF|Vk1tYYz*N)RuVfkeJD`K`AoGju@&r$sfB32&d>`c5-!%^j z@Tb4g4@cY7WVU<&+=$;U7F3tI3}A7RJVwaD2HK0E;O^{w`^DjgwWwvD++mQQ*mPe( zw%-cM8^G)&Sd7Q#`)ON81n__I90l$hA9)WM|0rgd8i(F?bJ4D`6D^<y`GV`JAzyyV zOSU8N1{<=4W{-}aQQ>G~wiE*bXc))&3OFCoW>g&ixOZUH!n5H$mSXu1dStlFb9qUb z&6uLUS%m2+2-_+PN~33rH7or75Sqw}#Z~00_+}URqT^!Iy1Osq`?<l+z$qWF6{@{w zdOnyHHhw#++s;nHZ4_U5Lci!l@~g{Fs9`C=!KI2JYAI{E`0hOsMRAu%-*YC}g4b7d z01coJB?Sl>Wp$?D3YFPAD}iKvXgw8J2a3NU2#_Nu5O?s>6VZq9Sm28&$|II6_GX-p zCq_m3#bq1=8LbZ-ydI<Ezc&6_WbF%)6oWqqL)m}_IIAQN)@==;rw+>Dhk99EQp_ny zEd;MU1Usu>EI7<;D-oetXNiY=-}<#{L)|wt;@>t}^AGIp`4Ef=j+9EdxrX9%Mi9$` zjpEjg$Kb<uIP8guLx!>i^+LWF`T(tXasSM*KO~LZLLveMG6BmLp0KY<p-gxD0u|F? zTvHmuwxK4sR9)GKZEgIEMF4?ZDD;*yV{$W-gZdbWOe+UWXXqMG_vRkKs+*Kx)M>Rt zkph}lT}||i_qfDB+0b9;vMnYz$fj*DY*9RIME=a%#c;YxH7gMT(lm1+qv$8M#)92E ztdH)y;i_a)$6I^tNLf=~=?*v8{D5E|gSu*S3=N}1BHw(PXK6l$t6Obblz6S7yfyz6 z%=j16aOq^=@BEO9(_I;oGjG;`me}H<g@s+i%O@R3;Hnb6rW(U6V^8zu(`SPAehYL- zN!Z6u=VW2177_t-jE_4w7X6E+OvfznLY##-5p#*L#&;*wu{qkOy7n(g;HORx`Fl^# z4&P@!ZjIg>VtfyNqVvQDTLSRU0QWfs=U}_9v&3N?7cahw6dBTY!7#*Pycwj5+_}x* zPGzKvHIDi3BlIcw)8$0(lpq?`&R)znSY|aZU3Eo_byRsZ<P?Q!LZW^bOq8|Afi+B| zLrFX<m2GukNQ`~3n&v?+<8V9D`n+7@EEpFs>wkwBhk7!dRZ_mktjISRw0neaW^z;9 zKeug<^4hf<-wj(W)?UO_cUcaXeX^zN_2><sE7$RTgKjq_w=+>gwzJqy>WY+Gua;bP zrfqysT+7(z?$P1Ji!G502@J|@eSPH5SZ=-K34HNp96+RYXZoy1>hO_#Mc62^-?-C+ zp05}jseF8INw`;zcQZ(3;AU?J4-To4$UZ(V?A521uFS84P+mmAV)IRPQFE&W<YtR2 zuAhLLU<d-e!IQ!Uc@aIsueNx0ouZCzWR;Y+TXvmPOM<ypI@i);e^R;%s`vR`%G(UI zp^2sMlJ4x_B$sjdOMZ<qZmF>PtEB0512`x&w{^Y*_pT@&Rsu_7ZYXQ!s^yi8ZkZ7M z5)*Ym4CG?msonZVZwWFff22{XDkdT5q&U?fEW`>zRqpE4Rlf^t5Szvw>7^B(R2*fj zf~b7%+xU7z-|bQ*IBd2)3HqPdf$$O(JD*kIT@%o**$sVSaOK{*TumS7G=}W*qBwOx z53+S?uob%k*nHUH-qQz88CgKzPp{jn>kl7@?l}r`>)(y@rA!ALtwT7E5sk8pz7(FF zi_|MQ1bh?fUbeGA34{HJTv-=7S{@Tns~c&Q0Y`qJt9IMBK(V}&ScUDL%TPECFFa{9 zxmdlo=OXxEyQ3)UZ11|r1lC@^5E8hz5lBCT2|fVK4Zs^1d}6rE<!9%-!~YpOcJ(tH zpQbYzI=Jl|YYLo<!3Qg>fv+A=6lC7Pd3bRhTdf1VF?P#Xb!erpE@~z0z0wn0LZUhq z$8^jIgJ?#eiCFR0q3lYQrWkPvm|E1-EVG#@IwQdA>6Yb>^ZRj->^;8HS)yvBKlVj< zKP0^OK9H5>5THck%OGf+@7wt*w3*|<Xu_N}j90>zL_^qB7w$j1n7Fn|gbC*(le0Ca zxZpEzsjX@$9$Q|k4nJNVD&WbPZ<ib--X&VOGX$ym?e>WT32JBdki=MJNC+Bf^~V6V zX%5E`|BL&YjiDtuvXri^rB<4>od8sztQ71RnkhXSyXPDzuAPLf^xskzMd|!bNeH!E zO>dnh+=xEHwl4rBIJ*#Fdt+o3Jm&upVKTsN96Y?v!j9|L=L=<C)$jnCroYPr2H}B{ z@IATuWnEuL*`~Eghvl!u%Ng^*P|+PJTsahem~_|Y?+?;vlT^s}x=I$KmAZ-u3J=(s z(hPBP_OV3i15eH-T6Al}+P$cWotpUnd;deS1Af7wfPg~q{~zzajirmJlcCN3U~5Y> zckQ=05d8A`gx2@~zR%d3m)ZzDqMcbg=&Q2PvKzw4V5rmSN%teII?_pz`ZxCciH$bz zqol4}0!Z4ad`bjViHWT#6QLE8&F$!xdX*CqaV3(fL`@Pt^mjxkWukbIlfD^(q&qm} zM_j?ftvKyLkf5O>y&Hzz(#Qse+{dn(qoGpy-`KQZKQs6fFwJJjeHGows+FpOHOLnR zzrVom*wmk29r3ER1PlNh>|IP~p0!cLTZY*mp6~h(FE``{pjC&TzbL0n;eKtCb$K%u z-3`;XOk`Q&O61Pvn579%t!(-42?GgP@G}(yGOo<2mR4dyi!a`tj$=%dnQ@w(z^iL; zW?Q=|3B@ZwFOQn3B_(n-7Wb^7w4UM3UUe_`^6il9Cr0{g)qhV+bq6gq3neVmZxEUE zDH<1Z%yb53uoGAbn&;3Y$|`UC4c`0(1LX{pNu=y<daqL~OT`EKIGh{EHL0`K<5f}O z8k>yr+HhMcDewYM9r7_gAD}HQhO_ZaE^L52J!4x3#9Zn&iNi<6qURpRK2^%fn;yq! zaQfB|qlaM~U1Q(IkT`uyLupYG*3*{dF1A_S-gi;^hGDOsAYstFV`%!X1PBG_{j37~ zP{bl(S{3GlX{~^3$c=jBnMs!Le_Nr1gWISlmw?r`p-FyaeI`t_I9j>~xCTeKk=X0( zMB*_Z?0`Gg6SKsHvS5gafz43E;gK}euDht@48h;7(R<SS!lAMXZbgU%A4Q;uLLhuZ z<=``x_WfvU+-3HWvpvQ=*k#?HQj&=QWZ+To%{>G<1UMMvUW5wSU3$1Uu@XHW#~W_k z$Aql|=WZC|$(@UGC#vyc{yjeEdL#}u;b^@vQ*zh|h0e|01=f3;F}GCg2^_^{;us<U zD`ADzXpP27bB;wW{^{U9h)Wc2ahm8js#wh&F?!NDb|S<v#gahWl$!-A<(phpp+bS^ zF{=&j3dF2NB=a=^*?*C%Pw|#O@FW`lTw|NV{l|WqX1eIze**}ww6v$K<BJ3Yt40RZ zBjcs%B(2~^&Bfc+aO1k2n~5vmg_l9ok1j10WZfSNzKaQc+z9r!@iMJb1fz_lVir(4 z*u4RiY8s*FU1I+2q0ObfErWQpH$cto=0+^WtSdvZKtbs&MU85gw6ZEPVXu1(UgD(s zK63pWpAfLnG#OuRQKxVqy;nVcNRxsw<*x;c*oM2Ax=C8IRi21@2s|sgUXr$FzF6zS z4nGbbr|c-XJ^L~Sys@3souTwwl^%V;6-6+A-o!LqCf`|jVno_~RNPyMO4DIL+vEg8 zIvj*fDtvv(i?CDR*3DC5@xj`X(oX_~W$-rloT&${tA~y)5<9e0|0_|17X5*0P^Q(n z!x|G?YnAfAMLmFPB?oXF8<RSi4<6PJpsI><q^<>`+Kv`%It>R)@ERGjDO2ur-%n=L z-WE$Vdak(z$_yO^)Z%hg=2j(+pgvWV4mXzspDro+;js=>i3`yz>q`LpjHXAvGcb}y zvA1e<9el9G9$t=2J8l~)EDhj=>RyfE-@xqB*tGNAJJta?v%J%Ni_abhY*0`PHCs3z zxcXDECSlc3s0YWD2-33SP|EfT2m07Hj2EvMJ=@zsiKR@bB3^eI0-E=*EkdK+MeT4L zjxfq)jV0AW6KCTO4Az%Kq_6Oug)Dq}g@;j$12)qA5vX|;##^bao0Nwyo+YL-bu0<X zUF!%k`**xffHq%O2?S4tYy!4xvj~KC*2viVUoZ|3GgV`+sq>g<T{Xo(>)INeNDKN2 zS>oqLN)kda4|qnZfAniDIP%bS(S_EtGaW?5ve95oZ3zBF^8i?FNMm-bsjGAl2Vyi_ zEP6)$A-A69*r%4#U!Ud3b%s0{fyDzMGal74s+?@w-z?o_u#?Usau{R~?#Mkw#An7W z1SHZ5@QJqFxP-VTha_W<Dx3(b#v4bEyJe}wAK+9t6IbN>YFpBRKYOjKLZM%uCnBIt z<ImaI#vo*`0}ABBO)#lr<<GE+cE3w5cNecUkeM>w$`+Vl98rbS53z!#0K^i?iabK+ zr=wVS^f>UlBZ5-s>Tx+zI^tIc6wRnSsg7sj;O?m{%S+`fhPC*#>T49M`}Hq(R|{FA z!W{VAwzPSF^g8G^gX>N!0}8+`pb^#_yLs4dAreOZ(e<^Lr?Gf&K4@=n!xy&;N8cK# ze^9^%n#68zdL=skiF|fX+%d-+kfc<PoLcSgS}|M;zW6RB<+p7l``eIh%Ch3qZ+<y6 z%Y+k$zbQL3XEXibk&1-NjWK}!UEV9zt>UxAncKtwl?vcP`0SR&YSG$L^vG(AvXNhe zSUe|ay3e-wifEux*r-7?_H_o~K$KXFyqHVBNp`K3D@xx-urvp84eu!OS>Zm(frOn- zaDY4|K$#t?JO}d}W_8ma+L3&8W~A6V5mxo7<n6zR?KJO%!iz@K{fCz_8(bLQmbjhK ziCOj$e*I{`%$H7ZQuWg8M~+p}tM|M<8`x9yl_4{zc(i{!z_WLQ^nmq~P!-{6WVf9% z9B{FtI>Iy7()EI6C-!|AZ_cs)ZP9nUUCuLVdH!Jv($JHd5iSX?$&^y}>CVjzs?#uA z#uV(&&*FbEE|Rj+rJu>^%)wX?Z0XLf^0=o~(ldj-=b5JSct@%m0gu|!MVcjve{>wh zR)%jsjkl>R5PgykGpXp#cbo!v$$b0$-^X1#ERzQXP#_@c|HcM{|9jlEvA4JWKSbLc zRaxu9|H6Ww>Tu8nl8*4(#`)gsD3n*N;qczjM3gTa7zt!`q-D>2yMNw`<Th>^=|VLK z9?edAC@h&WL@tH6^8JBJ#;ro*abPJ7ODgavjbmNGJ!_O1V%#Pz-oW&s5NDOB#!6-= zcMPeT)2J+xh^}zrcLFiU$mc^)L_1i?l4X}ky8{zbQyTF7=BA{m0sH34H!)h#J%i#K z$U~hKcY>#-@XxYj&jpBGX%%t%6$eHkQH501c{h3gEic4Xp-S-2&O-f|=RD!MtspxJ zuuC4BZYZLPVV-TaX{#aipRxi>Y3b7)F=QO}hjxA1VK8ilmG*}J!ISUv&=>Y+&UC4A ziV{=n*~JF%szaRG1G4IbxwBxYTPJ~JSZcXyhq#vL84rk-V1;||u8Ayk387pW)jkSC zgcMF^!?XsXyv9mgi;08U;M=7n3C14KhL8+cbjZ}F(4%qhJZg>!PwZvS=V2J+CD0jt z2Lve>Kv;S`B%-wl1dp1dVa%I3$-_h3!Fgg-q#-I$W(u_7RFhbGH1=SWtKe!|?pN!@ zFS3X`3n5O)(M)h(vMh1bgKd|U;$Yn4$AM^VlAfl*c0okNq8x4wZ7Mjh?OP35LN~$G z4zvH^5C{8%ed)wA7}#3tAne3@<_Bi(OUQNbJi^!X-A~{c|J8PZ!zu@jG<=Y?5QA?9 z#6C{R8bA#EyFY*%FRZHx6EAX1w{Pf<zmml#W#cfG>lphvAOqak=_jO@h^U}n*vvTA z(Wrn0x*U;0J~>0^04nzAidk2uO|#uQ+bCmeWXnBPjmY-9l#is#psF@=qL^!fg=#R8 z0Zk4qb(*DjIXXh>IXnl1)^Hi~>bbrvdMHP%j!cO*RxWa7vGnmKTmN>SFPabjVG>X4 zz(Hce`B`au$k$3&>^QF=;}lYkDP~ytjWE}_row*t?l=RMigccmusd^Dtq_?vwPwTg zC`l23e_Qh&_rEO=3pN#w+_pxv!Nmn^uj&_oTbjn#7truo(g5sF26OTp48`6XyLcc< z*4bSR=|!SBMt49a<^Mgt?-X+gvv1QMTmUX8{#pphh@IR|={OTvD0~dsJN>vC*Y?rN z#W))@`*j9>60b3(%WmQS-@OM{JRsr100h)q|Nj^(Yz_a9v0~cS+Ih1*@yz`frrOzA z(v+0^{wjC-p?XFy?r_S)9i_|0{3QFK6HBYmMvhWRSjnxq;_LMqQy>75H+!n0D$|h^ z#exMp#;*YQ>h`uzmrkuxi_VC(BbtKXP)PRSooAwyTDDO`)iW33^Yhc&d)y}hGbJi& zQ&c7Sey4iu^n$In@=dwqsN`$O^ij#5CXGFsIPah7w~1-5+BnaSu}6SJ;<lB8__|fO z9$AuA`A#;%GJ_=at{kRnqPZfM3g$h*BuCaiVb>0w;y1+R81P+lg+U0c!DtSeeai~N zJ~^n7t}mY2R8jKl*#vB*1G9|c#g{`%p6FCd)z;l)Rh8lE(Mf8_%d!EUJo-hCR1@X= z7PNQdtj@7Ca5o6Xtlko#QDne)d}U~KdFK$l`0&vUT(9;uBor-eMd$f$aRe}qCw8ew zC;Pf@GWi0M(qfpRTzyL>kEKrob(M#lga-Iw;TY`Vlp~+Mz&xst1^R?#ybKk7lhYVp z513ukgxrHo>Q>l4)iB~Gn(jDQ+~$I><V0qRxk`Qdg<_Z{3;eWb3nMRwz_{J>vXwUo z5LHY#S7yWZwlvH+AcSIU&`*dbS>6I@r!k(U*Owz(4>!M$k7))9g)`UWRL3HyA0KAr zv|$u6`_62JC7#^VVW|OJ!lT!hfanN)A&)1^<HIxj!C<3&G5=-wa8VtQGQ1nAH<jV2 zzP&QiCzeVTZvlow`6Q1()VOd-(UKqr*J05FlaxOBmRu)e`AOermG>!q1rJ8Xl4#1n zNI8dsB<q4VQ*;pX(ZjNLg?;zzrNc7i4@L6d;9bbM_`m@I%hqa;3j1DT-*3>%O^bOz zsJqapXR?VOCsm>-IulU~pdBPi5Al+1(khdoN|=r^8&jg}1s2J@1RRk99=InA*dxun zK{fd-U^ZQWkYyLdh<b(^_Z?RPVnIcfan!~U=SuOM#HI{izl)h;(^VGn&sx&!gnsJ1 z%CteYB1ryTwwP*h{P2We{priTho%}Ms7Pm=MCVY}F)k7<XeeCPG2vj+Cl)zw#3|6u zj(!hMw;G~kCbk88!=M@Hw&Vw<sg3|I3})2@!b##3Ro>r|fhs+qWd{S$ufxY7iVG%n zOF!ww0fKVL<kEoJTpwfWOa;$>)qpDET-u6ImJEs%|NLBDWrp7jL2~AcCMhXv9u_jB z!evhKzEr=%(mhmC(RPFs3>#yz587l3zCnDL@R;|!g1hDP-#={WeXk^Ifa6`@L#V`R zfSs|;=S612OtdE$c3Xfr3rJm&O`}&$-UmGf#Cv}CB!LhL5}{IAKg|}<Oo?VE?d_ZH zx%s_txi+s#*ekGs4(U5e?skCZ+@ALzpzRe9Gi%dG`mZ$yVAn->1rybD6C$6SB3xIb z^3y&)lz6qgm*h<3OM_8lyn%GE47fz9^9Z$qs?0QlFcXBz@S6Lk56*FALW%%g+A23e zAsoVuoDyupbsvwv7R%}2h@wjK($6^$KirR7Oick{6Lon}SHBHo{3Gj{|55X=4uY}? z{0HBOjcd<|qX4<v#hg;)uZDucp+j40;_^%W!}mg><(DTklw8rUEK|vdEf6wt+Q5ay zoso-Qh7mB-kOOKw7P3Qk3M&mSpn)wiWt6UFqSeE4F-U(bfxn-#l3d16Huld8a&^2a zFV^?Z<Topc@U&i+i4iW`vyK&gMu4G46`lanUc#2jvqpL0<zs`UVTr0!*-8b-wY%=! zgC|dyq@do2WS+^bOHcMd*pz<dSP+Cv15$hdIL0^S^kEYa7FOr!wUIL`==$%14Mac4 z<&#_ef@}g_y1MzycG*W9Y_|`P8Z1jBOd_0qEjtK;L)G)P_g$X=30X?|zYxLXdJ&gv z_-fH}eGJS<OnE05IhAR6R*_Kc3s!J29(jEsd3_$h@&>~A>f5?K?X|-;hs%?f*i4ev znR^9-V~{*z`6MwOc-WHcHbs4c8B68bZ^eqCYm-Zmg&L(cvIvu^k*7ut*iE>a)JNmE zR@N)~Z6nh9*uuP^B0dG$c9SYpjsf=5q$G~tjhtcCol;0_33Opx0|yk$N&F;y^ew$$ z>mXP~fr|e@-G~`ouUl14E$M?c7L1-^tlw)|!>MDK>8iA<ySB@u&*8MJ23a@QNp$Eb zEau<zqj%lbpy^vNn!-FV0=MHbCsvp!?r;E#>8g`aOTg5LUnlNJ7A4p*DoUOugP9g; zSymVU?qz8d;TLThvX4_$8h<J4*$dV=jJq@kOh(-VfiNmH%&K$y*+3@EIV%iEA%BnL z4!g?JQ7ZWw>(3I<K7#n^vh=F-=*e1dXT?W<UCa3jKSV+yZr*v<C@ijmucMP33h}Yj z&*)u{(1-wPGB67)GWB!AuXR9B5&wA!DK2cE6r+aEh3+W>q9DrJgWlnI>6UL5ft6p_ z*0PF`zj(BE3SlD4_=RIOqLz^J_ISKJ(3>uSjgQc<aj{{Uz+e$XRL;^Ixo`?c`f7N! zS7m6^@)5rT4Jr-x|Na2aWBS4%KRP6&LASE=y|D|otlIy)U_hXJmp>q=V3+rG1I6V_ zsSBhux-~vz3<|)u>)oO8)`xd-pb`qmFoq1b1A&2(YYI71xU?63@??M@CXbo1U<htY z8>F)QGe~<v?Nj0p?=Mh(+RX1LX!eUBj095XN7mq=P-szfHLvAW6eo*l*Wm5<N9K;( z>!1tNRW}lK;{Us6roMF3KyDX9^@IGxVs=?93JW?79RwpG{j4AY3#hmBP!#3w=d%nz zu?#L=(nt!erwyjRf?wpc!Ej0JStc|2RmU=lUM7|jh&ei26KU%Q+&!UAF{2SH;HsLy zNs&xgU>jt_Plo6q>(C9(yarso2eYf}uu#2sjfw3FyTj~mku757>9LcDv2A%a1C{6X zu}tU~rawllg%84dOJrG;kKI`A4<9Z8d7~RD9F#sT!}ms!>Ir?<3Q-3eG86PIJ1^B! zg0IY(2Ck|@hOgpUKK=))eJM#;E2g^}7Kjn_-8`XS;}_`=v*Sr^(9GLq|Abt_cl#T{ zj_v!y)CgB^hWUQU+GgJ5x{bDXYTB`ixC>;C<$d!>Yu_5DBIsuP?kV=NGhXJ=SY4RK z;+GUjAmSI|FVqVz(HIDglrtKj83~LPKh;DjnFcOf3ue8*Gyg*2AZ!B^j3%5_U2MSU zL=S#uX)}CMJqZ)}_30H5L*idPisYKqiV!J<DEaaoXnv{Jzt5ho{|<`9SpK)82}5ZP zTmb*wvXYMxn*aoGy%oqPp@>eVfSyuueMUxGT`&llA~p%f!<?M&K7J%8TL>rm2x0-t zNFp4~(GFi9hV4u6H3^Br76k`NBlRr87`;yyk1Es60!|pjx+Hv$i;0y3)KEw-&=FLX zwaMFCRVnKE_Vvz^XQ9nE7QK18%TEz=_Z2vfyrug11l>n)P7er3B%zuEXzO`KqesDJ zW!yln;yJ2Sfe6_rp8_LplEgxENA`8djS(@V=tSC8)^7h{sLns!?+<%l-XCCX6!|Y? zjwQVkBNjL;u4>@uk^>UgBa&XmH_m~X`Oj?|>9z77U%?)uwK$5QiT22?sjDTH%nTl2 zmByZBK(QjN%nu22<9egNYiF2z1-QQx5i_k$ZdV^6$Py4nNNFL!uK9L-$A;REApM4s zZftbM{38ltCebkI?)i+}ok_)ut#KIA@Fhz;ah@Iy0(}BEMgqq$1ZFesSr0;;C}3vi z_rvt0LkTk#-({hAa2J$7k6IG;S7cpm5dEs#qd9_{U>^Pxap;mxZ(a*d!QQ#gZ3=O^ zaB*Xv8uOm?KMa$Y0J^bjx@(b}!f#B%w5`)!B1v~4WhZ%5$Qy$oQ01R7ND=Ajw013x z%zy{k5FrG;HLQ+K0h=wiE*@_uz+{)<JH^Gr)AD)2gYg|jW0zCD-}qx3wa_9>P86Nm zgcg{1N<9%SR0-r@rax|$ct--P2cgU(ucqr9G+<e>;?<NEuU;*+BCiRIYkM#B4@gJg zN`v&0fKEcB4<;-R!dHAB#$KoB5gdrZA7NvN9|ehYmFKsyF(@AYj0FDV;{sSntrO36 zBW5Oxi$ItS@O+9Gfp}H}t&P^ImK9pk`m^Q};te>6L2cXtg}8j{TNhW;B$V@5i@%m$ z+!Z6&tp+zgK2A>;N4R<*Nql5+_6n6%^V_Un9i#kxgL#}E?I7=vSYhdh!>e-gwPMcC zFHg&#`f~LfLOFp9zE7y%>Hq1`i+M9~J7H-Q{#_vySDTQ64)D`#*N;6t>J*rxr}i`S zyV<?QQ!kVZ#DZOLinN8CT}Kus8kg=?DGac@M2fkfvv~z{P+!=Am{dfeK_@s6&H3H; zCsvGmUPjI)17m|LzHoMWsv=i~gtYyK_&k7$z!Grg_*@(=lim;hY(K^_Lfk_9ySm2o z5FK~>Th@`Igt78N#i_)E3`GdY32?^rD40`(IsE?EF>Dayh=U+e+`vBcB*WyR*~2T( zsK#n9`WE2|+0umN6_#3o5`~Cka!AN(4YQ1wn#kz&<q_zp71s((XtzPZfH0LPq_xIE zi87>;B!eG!EV^c)c@~j#m@R{Gd|aTPNHgv-pMVnRyZQa%-YZVxI4<;Za$dKqeDrv8 zmM)Hu=kxQDTvLj5U!|izBW_E+Dx@Yg|3w`M-miCOfBf8B-kBwDEVyJn8nhkr^|s(b zqvxB`bN(GQYNrex79Hlv531YGNSc67V)U1Xu}$eTU!ce(1rdVhEXIGAA6fuoAQ^d* zUM`v^<0y$2Zk>Yedjok^Zn{YaL?bf0Xg%PL1Qx5BL<#Vv)wL@?ttldnudJ2J31kdo zk!Ol%ywbnpk<v_~)PBfZW?vwP+p35$Ytm>2UL?+K`e+(GA)E#D7<a*(P-uuV&QBf( z{in*mTO@$>54TX?M%VBjrua%FdA(d62ESXb`)Vk8!EE)L28s6G@%o*?(`mtJEyq*W zSl!jvnqg>`YcUtT0@D5y&&Y$=Z$8e_EwbswOuUmE_6jyN;=1*<0yT_q^(8`@{!Q$9 zIA569yD;cJ28k;p9)|A#Y%@E-V4qPWEEVPkT*a*Vu{2?CbU;aRe)|k%I9iZz$_+6p zNr67n=`En9m(zY~U0o188kW6#VrT9?u@wIEkPV5IG8gAN@Sy^&#xoLu7*82L4;Drw zMIR*wC?m2b6<8M^?G|jiTjO60LySyf^Xa8n#c3f*5&E@<>0(flBHcTOc@z|g(AEYz z$>4`*nHcqcz(esR(7L5#&C>;A)2=;R(EhPiN??T01t36DMHQeAil5WOGSh)AC-1#F z3Qck}HwMFUAHgT>5(;+o-!yWIXDXG5+Dg)VTov>j<6UUA1NJ4$_A7io4?s?B00nY6 z&%Cy%1P=;$m1SY>(mDdl@+lGvshwXb*AFCmyLBzcB89P91v&w(l2p3+>Z=EYl6!V6 z7k9>zG-BnQD1Z)%z4!UVNIPa12V^Eh(}D}*txftEs^L}X8V=y<5;oUfosmnYJH&VF zMU&DbITXc5^6933!%>zh$HUa+-h?q|_qbD5dvg)CEPM5gBK&M{f2IIFde?DIsa6J` zaeU%5Wu8-F0?isgruB>fTTOVji+i_#Al|(AGHi#S85g?~=8L$<n}RpnzL8;RukdIf zuMqm3yqugo9UYg{GPG0O%KXN;<5x#PkuF(1O9*1^8GED5Cl<t4?*22dX}#?Rv-`Qg z6D5bM9~T_43&6en<6mS$+TTtF=Io$D%p2k20jyt^sdBFHe`Yw38(8KK;6oH6#7&-I z9gT73>C81)YQ{z_*6GRTwf*XtX`<&SsK8V`xvQ0M#zFI*)9-4!U+3}u6e$*E91~lx zf+}Gr9q1f2sM!6H|8+emg^prUWrd>lJJJSFqgVxa@V_<$Nw<=a4bP;UgfWasX9Oql zpa$*8hZOfpm41W3VALRI1k&Uy*>UkpOLF=Ej{@ilDewuHt6HLUR}8b5kd0@`sJXes zkfiXg=$*i_(61RX&dMd-M2+ZGUJKglZH!efeo0STkZu<JEe+Egis?sW<Jf5#!zJcM zl@yb1N;2T~`mxJ1+?qtkMApEAcq(Q1$4ud_M1DeJs*@By>cbT5>OmB{ZucI_ia%v? zF*VZp*HaC0Y(OL5e(jQr0R{h2zC-@Q1R`#<x|X`Oj;}?{*=qCZkx~&(6vW%o7;d3% zL-aKBbOiIo_57|5h2jA<V?mGe;JBMF+rYw;{W(uzb*}w(Y?^DUQ#q}?d@_h%L#0s- zNSeLuk4dntS(tl1N;Q(parNLzL=Y@+^3|o`x?(D$Bt?sWuy73<{Fvd<{?$U^>qMXX zs2m{`vSCN3s~Tl7lqrX&QDaSLLmvBZ);^jH4OO$PXo}RsKh;DJtY==U1z)}XcL~Up z^VjXeb*IROvk?ffWykU>sR`um!Z7WWq^vb<?o=p0b2q4>-gOv0WeE1|Iip0RW5)y% zs#@Y&$JC-gdeEL~h#2E}k-8HOsB-+rBcjC-t*m=&3y$rH6S!#H>E_2(y+~z#A|Ma) z)!9)cl#=5u{e6B(n{+UHW*ZHOi{C_%2#S=#VfzGj0ma^OA}q}aarR=m2tmUht)--> zepeyB6s2Hp5TtVJsk?9$UysQeBRgoVluW6d|6CBf21Cx`apaYte1$un2A}LHpjW#V zX4OhNR)?XS8nlHg*b#m6{6Ui}0rL!u{G0E5QJSs%D!4)=-kkp<SoVMvh07a0;rwzA zo|zw0HPQWnJCsx&wViV3<zMC^uz04cH?ZX!0WAnXP_06vttJ}*SXgIgc}PsM6d_Oh zyC74(iVK_r^0d#!AIO1F?>OrW7NVve!;g#~zEt^$8W#&kph8Y~!}tSY0kgJnC}<9D zCCSXuZ@SmDYLY1<#`4l9UIt{E8^Gr?;-Et~+lo@i&#=Ac=cD|xe>2fPG0$?9IbzE5 zhnz+jc1za1?J)ig%EmUWHOo{y-(`{4jNT?$EeoB{(T7O7CiV8A!t`OI%}rJ*o<&A4 zLrKImKzC5d`<mNB-jHt5JHn45Fmn@GyxX}$aMr5h(@}4*J^XyGpp>)_t{={qf6G~R z)(!bEnww9efs=<B&wD3?SZM8*nrfw+HkWRLO6p+Y6;|mMfQy+7R3xN~2!w5yD%_UG zwJ((mBH))Bn;$2%(%*k%jX`ZxyVSS~vEJ{zKI8i*E4Qi!WRGOnbc#6meB3-|<q2&G zIsc6pX{Cq7U>xDqBiZR9T}xvT<e2%#mR&<P@*V5Bt-7^cQi${PV&b1Rs@?G8D;O*G zvT7~w9P-q571cc+9>LctE^v!c*xmks?DHu$BP%fh4e_Y%JLPSYy5N{8>HW{4liDoM zEcrM71o2+wQ5v(uR_<@*5NTS=*iQCYBd#_LUK0FqUQTv}KcB~67Qro`h!`s`gM^gv zvpJhNa$|ny-~=EiM;3Vr@VMesGCl5V5kdSlUg7Uq0XQ;2Fesi3d#x$ENYte5K~twR z3IkF=T{m2rcvMqS@yPk+C>U#kcoml0LUE~&ZuI@npB9vKAP<L7K`nWf;ZVq~f-NaF z8>P7+(R%96xTpsAhqk1{*EDhdAEyOkOYX(GmPh>c1O1-|l#(lASq1FO>cdixiVWzE zj_OqQ7l29VdAec^3$;B3HH$J0dxIh*gxsKJG*6wAoZ_Jfa&c6O<91=Rm~mS{v0O2h z^-c{leHfhJIkQZo>yR1AMBv}?c>fSMp2hKJIXRg;((dZVNegw8kWGg(o$Lv$&vsT@ zN%JuofxCS{RNf6tn1fa|n+#@ef#cuyg(e5earNf;vLhEZt0u_I#qRMHYC*XJd@ju^ zIx1?ZEjs2h$jsJ!ok@@oprF%`%BaHIL7E#Vo9x#X$%uq9$3vMv%=i4@bWKZ}PEeG- z@LYu3E=13>CNXPrJzNId?hRawX3tz})1JVG`f3<%Pu@0H=}mq5KmQdD;n(xg*I;DB z1DKb6;^;DPWt_%4%b8+D=8+oYKp%n|9wSwo9ADnIlgs<Z)0eHaleTdQG*f|%6*ecD zxU1C5dFX&;&$_vN<a!Ic;s;XA1hRBtC+KbLPOb)?$?q<cXl-p4FL!0qy~{za9zys$ zC?cjTaWhH^f1KLCq<fHA-%LIf)5zhkbB@OfNnHtNERC17&^==wp^e_e3#(2LSo(}H zDg)nx6*O_S0VHq@sW;Wtm)ORX(e=*Se=Q=F@m(<<{Nt|c$jRVs0nY|Jwq0PGXYAYH z8=X;`M#pb($@0n1y;IHW)vPzGg}DA&XmNtA`1m>>LX=n*ObL`u^AT$jFLnQr+1Cwd z&lOc_ke9pIY|s)Gx@m~0(8QjJr{ooHZZMBUo1$9dQ1L0Fq9|!(C!SYd5X;lZlZrn& zZ!9TG<t2Q3ZneS-yt1_*@nF_Slj_{9t+zNT;LM8qF>Xp7(@$39;#9Zc2X`-R2Zr7b zaM^!|3a=_##0|RiE_GO*7V<AvKOajEF-*q02|Gk|NdiN(*Be?w07%q0_HNf*TvDq2 zGGJ?q;t`v#I?4_Ss&@6?7HxBGJ3mf%P)zWge{45!Y7ZFiWEbmA3EGR{(f$K5K~CD$ zK++Ny#2AMRKo$7bi>Qx|fkV?u!Wiij%EXf9-)m#`)N{(z<3qFd{qZN_SwJX7{4jwR zVxC}Zi5(K{A$-+rYbV@zX1^~U^6F9qFTbaYG%8(ORRjku^ce(tPa(AWArh&``h=W6 zCRh?|yzaY?*Omj_@M^~48Q}G9vK{%6KeqUb-h2HfD?{E&0&0qD{a%H$+Y+t9HQDrr z3<IXFF1eM(%9!|KE%Q=zev38}Ud!rA0`jPnV#uZwI3NcZ_ash=L`;QHtK$}Yil^Pf z*t$Oa{CB+6G<{a6(fkbw7~nV$73W0s&kGOuIEizUr*wqf0GrD`A|<zJ0?wzfsDV}0 z<0_!l>xzv@ZkjY0V(|&52fon;{pnKkd$+XXMW9)0DYp^KD1F5B9N`xZ&yZgIvBRdj z)>%PIcRBWpq@2a;|KsW$+cOK-EgL5l+kRsw6|-WaV%xTpif!ArZQJG>+eRf_`$Ko1 zeg4F}o^`J==V+6zz@80m;Cs*Y<@$@_>jc&KVhvaE+n;rQ@dYI^MSAK(1<4!PpjPq~ zc4@3=zYJmhr41Z!mPLU>dP*T}KrL$SI4K9@P=v^`p+BZTltT#%TzI)v$3-}-=?`(E zx5h@BUUJt%7W+p(-7e=(OI*9|7^>$h4qfpigxDOp^#fI2O(8LZ+iFj+hbV6F?}j65 z3iRyI^rQTzw9eLS2te;`{<Y$ue{5s<{M8m%6p({}Fy!4vTgb{BzCpGdf>5+qP>QI2 zSuiHbIc|&vyocEiv#p8|&eqDam&C4q*@xR8cNv8sOgS{>JUk?SB)X`Mspv{ylmAi# zilzcBjw*Mgh`xX;y&hdoO@g)$UY=i(87e>i&i01MEPR$E=mkuA(w_fj^VgKQFiKi^ zUt8AQUlAJZg?xy4OLMGJNZ9p|p*u?M2hx3T^Y&nPw8^rzoue?J6>gonusdy+hBu4N zj-+>bG3RCI=rd?&F5Zs1>lI_6%)v0IwnrI4=k2avhxn)&spjHL^{g8m@>||IFDMnK zHi~lltkDf^f{;ReiFcQKR;yy8;1-3lJFlu!(dkfQLrg%R*!F0-Zf22jJ2e#RW5HjB zFY!c$O=m<3|L{=nz#IaHSB9U0j$VR&wvqIcFtHmua<bPeT$>UMhH*F^zuBI~5lPdR z6GO09kU8Em<urwr_eXBLrW}(R%?eoi*(&X}JnCT%1X4hH-=R8AtHy2;G9i@9{O~8B z6h6gJc(TQ6Y_yQ6@P?Ub$YpLN-<Tw0?)X#C7sw}U@GJkqVV1R-zJZ5P@mIveVCO#} z1At85n`EBpDHmupS~$H4m&-$ji5@!#ozB&#=dd-wwome&Qzs;UvK?k#f=?l)5PUfj z7&DAQC~=9sqkbI!5CCd;At>NoI#sb1-~#SuJ`x)<h)B|XGW~z8)pgjb4xz>U(p*UZ z*_c&`y;)tQ13Ctn_f6ZU1FHSkw$3DT7*6y*6rWEslA2~1>=|eR;~<tAWIv*n%qfGO zsAN~k=c@bv{o#B6Lt5dzH&g>-=sW16cz@QR!2<e5zB#LvMrey}22(X<rk{2+oD?%g z@b5WkC9*b1-v~k1RDo{`A+BM_2D<1y;8Fch)+fBsi*xi#X(4_aaNQm+`OLKw`!MvV zg7s%54g7d2%aNi>aGQ(z%b`sxsWfmu`PWB_6Kw~z0fUmRmdi`odNIk|os=V|dIxQE zd6i50kkNwA#YT~HE6m=+yhe$x<*r027TBJ6IhvZ68t54|8?`j?<-zn?C^~Z2F4_Ko zL^WZUh4ybx+-&nQKg>q*>W9H!6TF90n()sR0A5OX`N|7WvCVX8w&;DX89iPiU!D1{ zQ)ZnXqxziAGSs`DzfM%x#p_p@BsuG+DsI{CmajI+7rkW*ZNrwS*U@y7(I(wxuJS5W zJnDY>h(Xw~%0{@HS=8=iwJO!u$3<eW6dguONZ}HVht4L9ENSHCANzdmFKeNlBm8)$ zYkA5<nGjHvDLmpkid9I7H-vFM!Y0(67=1rI1?}Ia^3pWA9n0@eJQZt&d?#4ELJbg< zDwdu?Y_bGHVd|uxp`aDF9W%dsJ#!XH(1^{l2TVn;o+Wg3v=glCD=BgM&G!#h)94pu zCL6Moc0oJ>P>eDb5WJ(dTJVta8ulEwJOkkH(;n1RMU+rZj*GM`4R)yrYVOj;X67IH zCn)OM>C6B4@0of`o(C}u3P*}K$FdS)CvdG*Nor^BJaRc_%wIpm<|UPKtQ~0tIb|we z*U-nOD`3o%jWmX-8>{(5bBKqyex7ycg~McLv#~&%Ju!!}`p8k*q%Taj{J4#QxdldZ zmOBkCET;^4Q&`*dDBda91}qQbp(kWJdH=f-2~CQ+k;_LDY|D~b8dYa7od7bApPDIP zBkbyXOvrtKt_eCvT%v1<o?Z%oZNX9qQaNGlKJ;35=>sqANa0nFLh!}J)<Nh$FDA7T zRi;iNRm?0r@|EVW(GToG8W?<KqmwTTa=W=ixpPngS<YAjMBUtfqyHe{a|!3eKw!ew znJzdRE&Un%<U@|a77M3_GGYr}RTt^np60e`vp~Ad-)LI-rCJu8we!|a@)^)d9&NvB z$-}y4b9lvztK1XDeEkM9?@Ks2yr9wLIve$ZmI@@}tpHed@|$I`c@n=>{nUIRX&O=j zdBI1kvr;<$JiL7Tyg%d}qI-?>cUlV8!Ppil`&vKfoc%|-m%kKyOiIXtq~aJL=cE99 zZ^}#A_3OT|s9|h&;UsswwDd-o0k$iDxJl#WC+pCD;;3?`0j*LEjd|(R|296wnt)M2 z<S<m5U2>}#)5bX$5+bClYV_{;=SSo~-)ir6)2?_LDi5dv9sU5Ra%EB=v*pw`I)JaK zg^`iKtMN@D`I+t%y82On+gH~-)}S8*1K}jJQWbiR5I;waDqiCxfHGT!*Q2?Z#8%q! z=9Rv;%CzDrw$dh8p3<9vH#A>Cqm(C32ZNRJ&pVGyL&GCXtSi7OkLNW1iW^qo1^TTj zr3_mCAc)x+>e?AJ{{nLg`49{^r~5eX@%Dpf;-B07quyv;ZvoG@)c2y8X%S?hHFRyF zN%37$^j*?RS3N71Y-8`hpsQHczx{v3^E&BtUdG(LtwL)?Cf6toSM7%EW1-I&(gP@q z=^Z`SNQiJR0Kabt%}A(qOXR?)mV#HOzhw~=7y4SNDc&p4w;xsK<7BCbPf>LAI$J)p znuE{4m7E>K_VEmH`;>(MSzR$P1Z%c!fRL81^Uv@TPB|cw!!Ii;U{#~m@jH9}#+68< zsNN?@y<<%G!}|8zz#rYLp2UOSkfbupoHS_sdMWnF2fsT{54pBAE=;QmVHB}>y|^f$ z|0G0RK7S^*B|U^cg?sp<S=8wTV>R`*H6mN#Iv2T>!E3sWjuvy781t>eUyd58jXc1_ zJ`srOPgeH@6UA@Khi;AJl5*?8D6y{;%p)AuQ|)z#a|g6yREX|C6=g&7qQ~b<KIb{2 zj4z{BSyy=Hb9M)?JEwI~f?i)deL4@9UtfAxW4Qc%*t{5ey4&s<UQA98&L@}(&l-IP z`;lqm0)=ohY_{;=iCO%Bz&t44H!&h@jfdoxKR(u4V`K|82Hh2sl!(F9AT&C93kBf! zA1qqT$B3qViQnpe7GaOY(C<px=Mn1y20P^b>Mnm}kB)10#&LA3a}(})Rjzz`cbtJ2 zt3kd^!T`J&%p)xB<IK^}GgRiIBRrzaf-q?Y$P-v7o<jK5jOEKY>}wf5y>Vo%S}6CJ zv$p3YkHcK{5kj@2uT@j;)CZBK)+FoBQa|1Dr-<4Xe1|iqa!#D~P!yfWdSh)<wzW;C zT90&%{RJdHJ_)KR$J@#4F@}}{jph@b(8ZkLX&mgyLX-BAZ=FXY%W(g)#&1Gg5DbD% zWn~F^ICBRta)*;EoL6_2yW8|yFIi81em*`rZraRF&r&!_xU*?e-7f|b6R(1mv!s4v zO)Mo18}<mn)Mo6x{`^|X@(c*2+<S7MmYwI;$Yo$?0D+&D!gBpBK>B+umN{Z}l^o`@ zwLvCiz<8!TiVmGtFrayber7FT0cRSL5h`fQ-H@Gd7f3JQZOb!l50r6-7z9%5T@y}n z_J|<N)xUENWA}L?vME<PZT4QwR2cA<AI=z2@hw8DkIyIpyzP^i6oiLw$FF<tzW(iY z`7ghCfvsaT93)Renz!9(md{j%5Vjt!b*P<%^WyBGIXSO7FERvkh1STfwXlt9I%r2i zU9GP&_l@w`xEHh?MPjl3%PM~cH$y=|M@L=8)#S|=a}sE$Iy#L_^7aDj7yjkZ?MJtX zEI6HA;^)8~5f$mdD=)S^-SKjV3on!H<`(`KgS_gqVC`1ZnS~K!Nq#P0twRJuGP>+( zhVA}jge^sJUn)}@i}H=#Bc(yBA6bhk=vhV}$FJTd1Qa8Z3!E&i-c7B2eVBDg8pciX z_rjoW%!7&Zrtf(X0WnuWi22>11L}^WPto2G$(w|3S+_Slm~=!jfzAa+7opnoTygRY zoB3G&Pcn5UK1QBs{O{1te&;e*n@`o?Dlf&kw_(G{Af%JD(f(mXSw>$YL<;|qzn(JH zcU;^<8<rpQLCZC^85>DxLS#QniH;YBSn@u`PHSQoS|il&bbZzYwu2h~$VdacUvl__ zb`}>l)#R^CowJl5ht-KX&b2u1f%!M8j?`UNxz$lY`~Sq2yU>u8ow3O_JNOXC_9~}v z_@V4^P!}6~>Ew`@v`#N`5{K$OM6tzD!j*$A($lsGHXZQ8gM>6EZFiLCq&2K~7qlQ2 z;$ZASn)4jcFODHWbf^e08tn8CCZG~wR`0DmerB?8(P6cBz>XO7t$vb$iF<(pzR0F% zgGC>v)VXD~oq2*oN1wtq1qUBmqUFS0109Q~sUK_oc)LzF>%!|*NdB44mFGOcR;F_- z{UJLQE5<M%=R%G)1<q9K(D7S{c0n1|b9J$oK0$Nszw%u6GScbVrb+J49}*b-Yp$8q z(H-_Xb5A9Rd^m%g2HWZ&Mrdym$M^(QMhB?=GBUb<X4x)09UNanL(Fwuu-66uIzU~{ zi8QYE+U>kRkvJK-M8G2L6gf0N7V!LAJy-VUfKUU*`*;+b)_nJmboFxL7o6p)4u!Ot zJs#pPzWCOLraYCF-x*-T{a_#ccOk8Exu$ZO^pPw?nb%le#z<Im?+UJ*z|pk|09*6M zo?=tqnK5R(1NeX{NJgn-BtXo-A&IFGk+p)q4W})WXpC~k!*Dn9*dEIP{+b9Xm`fa9 zgWu(hdFY04ortpTI$l>fDKt_GDyZDyDre=^$*IbS+ltGB`(7<)Oe_>GDzF5_qNkv~ zV6qrstxsP)WYO5(Tajv|6{&jJd>$BPFYV3GljNFPYu7Ri_bir%G-KdM6!y*ER@&@S z_i`es(}7ygvdZq{0J{pm^nK+i8W+WnLgn)uyA^Sv(nW*3yxD=BF%ZJp(bsF{y|KCI z2y*<JL(Bcx*hC`;3m*!6lr>KwdE3A$g#8&9YWmV?<J7W>qBo3P1+i;Bb{87!JlGf` z)Iz3Ih|$hpuJpx*WkZkR|DmpRlUb4X#!sAl;l&*YD&VLlh}2iJ2?rv5%kp`R3;uL- zu1`=)Bu*g&eqhyFEIib(q`jeJDV+Ju`V}GOaHBrLuTbd|>BQ+F*cO6#J#57@hbp!Q zgRlJ_;|uTSaa?>u;iI1olC_Hpi0vtL0zeI2LR3U9-WrugM*Wys-aFN+-NYf(X<pL? zf1Qz@j(5qwpRk0gbtu=v8K#DFwLNWQOTCmXqhlt1$y~RS)?=u}oybFAixcdWMJs~i zri==o;u7Kb8G9eSuBkIh?;4ccVt62ACSoCv58;-W%a*xDz#8`2AzuT!ak<CXr;j>@ zWRVTEnrPh3*0czcq_=z2?}Z%6b?m#y{NWttZbfC?=JVXO7$iX!`J#c~y67E9GG`kv zQfF%1A%y@S2Zg?%yMdVvKwJmnf@8iWR<ZAz)tcTjJwXfGZ_#wYPLvWLsN-+#-pCjo zQzt!~eR~FkVTiO!Hy3qmu?|q8O%ft$*Z*_kK5kM4eqk6>uehRP;2-!LDk1tUF5C8O zXXdlO?^<Z!EpTC>RfsOyof6j*dH|o&CK$&o#preR(2`i57`=BL0FFsTUtj4#VAL5w zc#sMPg~ki1MSewHh4j!W`Yp(G?+cR|FWU>sKdjy#j$FuAgR_Ou0w1vgO}~+2MK{A- zyzFJ5pniB0tPc6Ejk)fV>j%Cn1-D+Lxo*v;wTcNN3tU1_s6DA;LBBiare-{3r(&v2 z{P0tBIwk9iGg&vGwd0$-l(1SAF*h3f)BQcu<gq;esc+v+{#8uwL2$A-Y!r~WeGrG0 zC)Cvc+2Uh2O_cr{+zR)*!bZw?uRH?j(wSM)!ecBl7RWRYDo9010o(YI`*{Qmh7|jF zn@pI>N1c_)5B!N2nz|{D5y(vSgBHF}&ZTsF<`@@^zrFGqZ!O(-um&FqY{y0X%W1HO zgpl&&EiW+kW2CR!q)7>`w%y$+Te#)psQ%c90Xlik6yVw3+xtzs5UA)h?lt0tZ+{XT z{U^_^P6SYyb^5`o!&6ayob6)b^p~X`GP`--tbLCz8*=)%VL6C>RYHvdyXhpG4+Ies zS#&Eq1lrr9O(W1J`60qfgWl{X7kPExV>r6qBZK)_%4gB;uM~rl2?B}4<og3h6ooMD z3-1=oe$BxRVGPI5*nmaj*F`5SkBRkuk74qBeq7J{DDti+ux1)+Oq~IG2p?+<FtroH zld6^d-290-kZ;m>-isKe!U$^ZG$+kR&&-4;f+y6$m*Uu3=!;99>P?uQvgXXS%RhX| z@^DcDX!zh_VKnNlbIbHGYRmJ)q?7j?_KI038@R*mbb%-7P<_a31^k2GG0ZI{z#rAl zYpdN01N=P8Z=>cIYcQS8!}5i#qFD+4qF1-$qMi%|_(dQP+qgVR+`Uzx{CP`O&%Shs zxJ}8gDZ(Kh9U)ZQ<V9W(^sH3-W)G&FK4%>#IbgD|*m$Kg*y1r7J%w0c!CwxhP;T=H zdWIxRRBjG@N1#grvUH7y;F4MXGhAimjr5072rW(S5l`DV*5BqkGTYA9t|_lAxV!fo z_9pR0c=QJLNe{;Ee{^^3J?6rfq`(7D(e+N9DP+}Y!E@dTPwpapvlTmIP_5~?GAP?Y zv#gY8h!&~Vy5=i_2inHcAxjM27(;0ADsHaeF3LF>Gbc&0O}9@?mTeDf69ysFZ$7NN zN~8^A$+dWRPh4+w=^ZREC9PkS{2c7%UdC;s_qQL{V0|DWlY{u)bt!i=sToWaIcO%# zg!H=G*!LfB`nsfQRt+qutc(JX0(Yf(t^TNa!}H=5r?N{yx&JXn6M#~HGZ_SBpLo5E zr&`4~$@kVTwDzkNUk7P;#%E2xiWmh2$ogWbS)B>5Tz4hmWlbMFd7JqNqux$}piLR= zdT9n6fPo_>A2k(Ybht_$5y3xyVf{Cq-$B>?hWyV_xcSdz{{M95HU{?29`^synK!HH z*j=#0`^?r7UlZcm^^D|zJOA1=5cV_ZY`X{n6^xr`S%9fCkxyCl82tJmr=Cn$EIEUv zLy|2-O5o<@O?oaV??<d&h|N21VaDuH3zprZTwWhGlwW+BLrnjy#ON4GUZ`~f@Zd(Q zoRuVH>p8|cS#{eLcxdux%D>&*3{*{`uXHpDVR5<_hBAFj68<T<9cQg(Wvx@ElWGsu zY~o?FU0*j{ed3fSO!0NbkYNBWBi+!`%)3-lUqkTwa^R@f`y&zY>tmbfNgy-51)3ru z?VNzG1=i(pBYSR4XIiM-KEy46VtclbOd07(+wh!fv6AF-!W3I5F>tS&z_rxpY!Y*H z)MH24S{9395Ex@(0KS0=zYcIkRI=YO#2h6WApR*40(_I-lpb0)2B!$M^+x+-k^evn zgv|)iUK(J4184xET)!A9#ROar$@yj2ahMjK#pGu&cX$Zctby3B;s!GvG4b3m0TbCc zFI4er3BrAVgVmoW{4SIFWWm$y4*gM)Gm3SYoM~bxl$my|sbibnf!+QQl$XWaJMOev z&}c$UUq+B_-OOpvmVT5!w)4*_QE{UMkoLo(R#^5qe2rMDSiSn8Es@9PG5<6<XW{%7 z+=nw7Vj^9BN)EOfidpF-K5@9~@G;`FIfcQl(xHy#hONZyE(`~Or{qVtBPaQm)1OPv zz<EKmhg&!EoMOUA1&_K?Zy4xNomB)V&{r=V-LnoCvozE>l+Y8AN}95NqKFJh1k7a~ zy$z)XWol(NtRg&=RdM5Bjm<{$<((T-m9>?PaAW6Pk{=<VQAS(HK)E46*}ex96<=O> z`t<<Hg?_^%lRnltaRVW}xXxU}3VMyK#x3kKJ#5;oZM&N3y%Ca^3>cO?5?=4Q29dRK zXn@NV9?gcYu|zwzlvm%pCmhsOwvXZ(t*-it^`{-Yjn<u<%-KOB17@jth^64z)1=?( z2lh3bZJJbVbWY(#7cN}SAeUf0n5!;Yl8SavduT@Xi32u53tj75w(Exo4Nraw`6IhM z^8?wfe&75Y0We9aWM`Kr?ARTd&Hp^T<*KpJ7tWG|zk}7Y>YhlkXveYZ@kt6he^jZb zb9Y%OTh<zT2u8^%_N1|fFP>KL9mwHZQWK5K56Uc9K;2E45hU<U6XQi@ywhVog{0aE zmrqr$WS{Q9yN}f~mE%Gx0Fn#s36p`5!o<4IGHnc0*1ozLdeL}229poRoR)Kwf3MrI zdkU|jbKRVKy^MifS4WhxNS)nfIYUeVVZv!=8ql6jUU1sn!ej-~uKtXQM1>$QLMfv7 zswMNv{b&J{e<4~$Tc1pF5~|;DQK^MfRqX;amD=ok4es7Wcbyv6j625vzO+1Ps7@_i z=V`htN*L{XwiVuP*Cj(@@es5r1`A<~13BnI9p?&EMU8I`?<cwU*t@EGpES)MsjKL| zW1(J3L_!q}dKNcr(7H=ccjvl%mBq^s#3f;`UQ<ZO2p$)m$Q(~>d2?>}uCtWn1H6G( zJEtlqO^~<9QnV9mYb~|`X1)Z`IOadxX6T;en6Gss%984J3|p}$uVx983n8{wxnPNG z(<xCm&uWbX!eW0lgp01fg<=cTjiHTr-IKw{=OsFY4?Gtb!qxL6B`BfIOHcF{!uJGD zgrfY|y%lTuOr(vL$|`ph4aru#Rha1g{qsxp>hn31q>KBO0%0PEr{i#U_<x1x286l~ zIWY(bbMXJHHkp|GM<2J%r;~Wl5`XhS8<!3($$-FdQOUcNG+HxFQNx++uGHe>o0gG4 zje^T89S<uQqdI=ntM><(NnD)%q{EOd)rAVl>US;nCx3pcWm?xdqaq=*#@dp6;%~kM z&s`6_F>T~-Qs(tC=B9J0u!cZJ?zcjPE%gpjoxCpnFw)=r1nE15+6r%-akVY8KipXj zy{slS9n*S0UTK%5l0O<IHkZZR6r|IJe05e>p3nl+`ymc13LHM(<LvkqBH4U7n`Bq} zNOkGE9O}Dap}#;0>4!Jphlp-_zf~$4tTu{Yow{w`JAisUHkN12<62+$veg}3JMM7B zC-8X8z<1>1*9*H%ZApaJSvKM*fiG&iPL|r&Njqb5p6#$c!9mZ|TIq*4zYh6-16r`e zeTmkU6pY68=z1?~@wZ4v(B_ZOr|Xud5V!I+PH~<Du?@mm{cZ#2K>vNg9aWQ>fAovm zaxM507~7HF$31neF+!8zz9Zbbby*wOJ>h+gw%Vt5Y)PgkklYY17w7d+AESBW1iBZ& zTPHb#v$6)X4utnSKrb^e+cor2(}oR>9&`wLzgt+vR$8OVvj|<bIXSaO<tR~`;J(2H z8G#JD1w5t~Q=sd6+AFVUqv=R9!d8u@-Y%jiVZBnV7PcKjM~feHT+NHRv%O>abL}oP zxb1DBaDo2!)%p23&-#-My&pid5hLoo{nLHSiugk)N$WQ$iU<QMOTEV86&VxTUnja` z^ox=UcdSWxzkB{I&!r6T`A*2#h(Gj5cn%}l^ku<S12ezeU3#3%xM|Nez=#L*OF;NC zZI{jS_ch&-3vHP7*2G8@Y9rSt=Tgf3*38)dFd%?r?$;+XX)K3Va80Pj%@Gga<59>3 z?ZkN*(t$4$XIlFW?+LIujvWB}f@4j(>8;eu=8)s?h)kk8ajATW|2eYhAJ*%uXzEi4 zwMC9Y6BVL4rRKq3x9W&UeofKFp+WRX$TyD}f`!0GDwSeNZACMSrM~{~<O)i&i2!C9 z<KM?fxy=uFt@TK^wt~5Va?*=zJNpLC*a3g-OBoQ1E!BRKG<Y}0huX$Wf5TACPk^u3 zx>wF@ws6(Jwu53oDF&_B_28Huh#m9XZDF^j*9>X46jEgXK6m;O%tJ#rGa%=y*Deo_ zg#)6)PPryr<H+W;Sk|u;guya$(}r9z2Sbi2#qFV=W~zBgvMrK`^WUgtskQ~EJ5OFD z4ZWRT^&s{QKauX9P>?N7(wp;Ya_>8|Kn-6kd)#k}NE_EG1eddj=FArcM(BrS^ga3^ zK)O3oA$BUDxWwXe`ZyQn{u!seh={&J8<uKT0Vz7Gv!ZmN4Y9KnDd|&W^$CeY$;>5^ z4ZWF_%**PBWTADkP)2EbECWj0O<rpie-ekiQ3Y{orkOE)bc$Bbj=9{Pt&O|yDQ82p z$xcs~PU9M95eU-8(A-&m!Yd!YK(`C>iD{xKZ?@EiT}mZjf$K&<)<UFP4H?01B)}a& zff*?LqLshv54yTFW-$fnu6XDc`N6LBVgd;$gYTTS-vMudh;$^Zz#f#Zs)^??^4dg5 zZ;j29#T?(p3L8K9O=_yYqRJXAM%^@_Myx8eMsA51$2%RAv%}}>Rrek)lTz)<Ap-3U zyJF^ps@8O2H&@qY7#!?;lt0fedJ-)|u3-J9q?=$wT%#^gUKy1P_1jIW-~faWv4X=K z;T!sDIUw|RcF<><-R-XJs+2<fkt^*TmJOa;XBR-rGF6~n(Agx#m0aNgw5oDv`Y|Q6 z+aSI85$ozD-(>j;f|G;KQUSA4_ve8hH5960NEA!7N2_PLovZ1*8u3o?KznN04Ia>} zg#>4dL#ySQPGoKmRYp>*Rq3yE)7^kSyX^K+-mCxa_rjpt5NyA7-vFH;dEGSeaql$Y z&n!_&e^m(_(k*5+D2P+rA;IuGhi|_^*q<a23@nY-_S}{Q@EhWsN@*^%L|*!0krSKa z8mQ~ei#UZ?QQAfZX4Ic46PI7YTG##~Oo0b-E7otwb5KD6L$Ws0P6fTCS?QJG<dx#T z<XSh4IBa+w#Oe79aso~i{jB{|l!xLJsn9XnrG5=>+nJS+aD@CZ1bX)GTE4~V^aXGJ zIE9a{2B@#t&Kdcl{=yf5&BKxP<skYQt|lf*S~q2Fo^yE?*YNL}U-X1H8wivz4^nOz z!Nj@3Yev?K(4!#FuyWdYZW|pLLZvkf4=sww_mEX58gpf3V}V+*jL>hJlgjhzvElZB zd*(njKCTwp#Qe!(hdvD5rK=xQs01CgyhTO7%Znd3DHpI3OUoxS>(e9Qu}IiJ*pwuX zTbR6Mnd>$H1Npn2XSc!|f>xAt^%5^3y0uS77INy*1<@(<l*?i>L_q>;54<vX=ZH#7 zo#o;$v1uEf!X!ppx77_Zy>ZCGclp*B9`>&zJq;Wlyej<=FFYHogsRU*dx5|tYC)&< z=_ov@Sxpnn^TYE2ln6!8y-Wbknsvi#0Mt<}f_{Q(#^4C0M<4Fl2=@ENc(vXB$wx_C zr2QbECPn68Z{#1f@$w-w31VEj%;kH(PC<(IHEr6syMmX77*3gFhF*7{GTDF?#3Tcy z(7D<Whm1_+_wTAn=og*uTvlj)t|8t*MACFmELiIuS8B9~No-8ym9!!yfqmmlc7sKM zZkKzA3eNu=hsN^imwF8)KY+N1xN}&WL9@Cq9{12$QtS8J#zX?z@3SpQKtQUJi@e_j z!-(A7N59Voe1fFMCvhq^-aM0y8Oi;GDM09wBnwXmQAdKlmThdqok?TAu-{l%6OyjK z?&m)@mBQA-c>b$O$VNumT;XM$uCw_V%k%qD<g*8A6{%|vjF`7V#v38BG81ssMqr&J zb<c^bP$jKXiKBk|Qw->WZ1^TeSm9mRUsn#JXmM-=RfMRI>YpF7TSyFAX{LqQ7m`7t zLQeD$(GKsr&&wHcE05Hr6fjK11#S<3K-h%$QHYZcQD}0Vv!o0P`quY0qKCc@w?C0t zm_O4nqvERny}FsZ5Nm$W>?7+6AeL&$8AJ0Q(}E?i;0(vNnBcT+!9|_HA&*LnAGiN@ zrS|=&j(-I?1vPd?uoiiCS_2xSXcWTSIZ&J?3<yt%F$X}V^?gk_fFLFS?E>vb42`j) z=j$D!w&W2-1mvO~!IlNJa>#jb1`y&P)7H;rkwJ-g;;8FdU6MYI=USni!&`tf72cJi zYt1f^tgN5O&#ZL#t~wL1l$45<;o8D)GLyx#Q|Nw`orOuw4*daFa(G$4F7$cA=_y66 z&7)ZD(KQPtf(u+;CLCtG?GcXwwjSX$KMD<-iVfp%(=ZXS{IYhuccpdh=B?v)o1cD4 zhoG81;*Imx`Sovt=3;O2p4R5u;IVokOF`yPcnKP7ThZ-R(Q6M{28KblEI1%)U%nwu zrS~w9u4v@qzg&J)(AqiwLwrxif(9WBwOT4B>lhsZ*j0Gjg*=Na3HukIH76G$#DKWB zh9Dy1VwSd59dQS~p;f~%_0FzDWsDaG{>B%-yYr!z5MB(UpV2`^bTdnAin&=`Km{_> z_aJO-?WN4=ByXip-TM#hU^~KSOa4YrZJs+9V-*B)kR7xA`Xi68sc_&|tsA-xy9UIM z+w|jy5Ep1jsND{sFk0a{Wth`9r#Nn_zvV!0gCc?3di}`+;!0khto|UG;SgRU$>2?e z)DL@hdRR^k=fw?15y?+fUsbA8nzYS!v$GaBI4h2@eU0#yl@$A#NU2c0eU>%H!GK_Z zOnk+Ulx;_=Si)DKpj4E49BE6hkZ{FYpAe&(@N{5I5CRdf9rfFvgA?ov^;+xQf@+-< z;T%A0Bl}9<C0U?QF}DSGjK%=A*|bKHrLiVaYcm7@qhkoUGOFi}VS{JK=VBP)jj5_| zt<!Q7V^6dB)tM(c`#a$%vYvONZKP@2@yl<9WbQYAFh7+?)|f7Qf0|0&eG;67^6jTq z2=vDYjeP^y*_*_PXxoLL^YwhwLc!wd*_$d4sA;h2LCq1xP4#e2XYP2iy0i}=5>;O~ zU3k>JQA|t7VyC1P{f^<3kuCJsK109$^NIfi{25G1^o>iMb7T@63NFGW8>u~|OP!V$ zHR3#jxdpPvLAG0)xO+jNA+uKzD>!$72z1b;;YA(}4-B&bSJ(mCef>1t!O5C>g-e$D zUXng^8aST9U+(6SpY91-?w{CWo5MBpAd7jZ=A#(2yoo<t_u;0gBcQqCD!X@9llt~1 z#RZsNcdWsUL_N(E+G+$?km0Alni=P(;z6>yloqN7w<7l-P+8s+0LBj|2FDE4jSG8o z=;d1z&Amc3;UpfYQ(TsoW&t`#aO3qe5iBihD~nkLM8iY3IDxMHv%_e@MQKKhBtiq9 zEVCj6gVJD0{gM#0O}dO&44x-?#Cj>X>|(kRBMp~OLu_d-)^Gzkp=|m?l;{_B4sd>3 z$^M7LID3-NyrubRvGY}CMMGSm@hJ!g^-@0iGh1S%N4i01nbjFjdSvI&cC$K|z^;yt zd-<~Un*%BVe^x;lBiKxLy24m+Y5(R3F+0Q*J|cqFJW`-!54wk7{ts9Ceiy;%Fic{7 zr`iTT5=OU`ZJ8b8rhyMPq<zWtsMQa;NhYucbaXy)as*wNb1UUtm6{}7GyAeH>w<Sm zk}a`CDUJK7#gLO=5xb}S_TIzUK=SY3uI(BWzQ=TveBS#eq&mAk8#HKd2o?&S=YT7O zbTv*cKy*EHDbpHtt+hD=?z9U72A8_|x(sgZfjR|eNIn$tKgJ&)b_306&4KH61F<%8 ztO!EPUci$wcs?ktqG?B`M)eeC#;jkx(Vm?;zGj{mUf6d+e|2A=q5q2+UaS*pNk;PR znV6e4Lcu5uO-HS-&os1Q<!5O@dr-^@1YFF!1Yp2P2LAoppdtPSFGk+FZE#k?zaM__ z?TC>%78w;(0#6JpaDGI%P}Sq3ZJ>!nl*=;}EKjK~d1c%nWj6?iC7}2u!_;Vu-z;6U z@osTMZ0wvP92Hu>#=FGJOsuGOPmWA41Pxk(C&N7x5!{HbBBcQamFp5JLWw>F#r$4) z?+;{0LlaSB-Y}KsEb-JQZ^m99o=4V{MHOG_gawt7NDro-(Ma4gWOo_|Ik4WhNJE`o zmWmx*p36@)SEq)^fxUVlEv&dcC&BGk>|9^}4Tqn4(_oW5Rb>ND{7y^a1JDwXe(#3g zNq488E*C#H1U7SD!$+r07BANX$FwwG9>^6$9ju!x4|jJ!B!1)@^V&1&%`h@M_PTdU zl-DqRjvwZ%9B`=+MT${8z@wG_LjoVSLPkD1gLU%JVeJ%>h$vgbd<Nya6umifY%I_8 zXH@Jti4W8H;WZ*kLF^;d85(2`LhyZ|`l-WxysRS#WgcmX$hZEsU+sw8u9%{xz`q$6 zZ%#g$+A!k4Hl<QNbq#uV*Cr1sA4?xH$_D1jG?LsYCZB*C6p?xWM#RL0d0MRwr|o#E zdXjPa{b6wOP%M4UKj-Y9iFrw%ydUXiH?AfsyvFt1zXVihh9+{dQIMsb_X|Gi(@nAr zx&5j-**TER+fihEjGd+guixU8cx1UBm;@Vd;k|JYb{{ar(aZXvOheL~G*&#tCe7&( z?Heo~Ui2W=OaE<;=eD$y&<VXkNYP5n^dIWiU_>5AZTYL}>edj`c(b*Sy<8<EFrA*q zk9)1@7nB=;PIx#`ULeU>-cS>?2#CbRQmkF9PfPk2wR27pFt5iZ$(54MDZL{1V){+D z)OI>D3WF_%gLUxN*)%f^#GclESM#>X+Cr!px*hDkY78JhLyE&CGM^FoD|w+i6$#`< z=`wQuj8OT&ZRCp~bBJ?QnI+LjAdx8-9R}j5n0}-Menp^4(4`Ngp}?^h{HxQAbzyWK zhyB#ph@Oi4Rk6TrFMNP$pb`Bx{v6iWpN8R~kmM#Ax<p^1bFu;Am+hYpYPjkG?eg*d z1dwUgGh<p_4OMEjI$ureed|z$g`ZmOeP2HL9#>!V;8VSsEB9N37OdzNbEN@Kv~9v% z40n>gTldJ_lQ}+OGP{RP&7(6#Ys{c`p=otf4H7wKf4|%usxEOn-a66|TU+Rysf>RP zFv*cFm;S3`A1kGFvE@Ht<K4dr$b5NRJbMX`<Q|3JJ&VL?j8X1rnIi@4q?WWXH1Z$m zaCl3v;!}xe>sJ)Bm{mgbZ`uE93oK$@_M5I>gpOgQ=@;>U^@Gov`lUzm*k>@L>YSzG zRj=(b(!Jm#Zg;*e5!V6zisYN7F3>)f{jKr(OqV`e6qeH{TFV@c;$%GS@|!Wf79>ll z#6|`kO|aXK1~K)UMWh|Xj7lQCp2HY>-zTe!O3Ek28Y$U=BhN(YVxm<m>KiY*gq|0C zfrrt$l$Wlfp(G0fU0ZV96E*|tMV8OJ3%Xd0i(zMCP2IkM(Z}=|jQolE{>lu+hH9Mw z*}7~0(j@$@1e)~-9Oou)AvS5>ZKMMVzv)@HWNl+y$i`VFi;tn7c2MDc_uHW0g^ST5 z#lTQ>$D=e>*gWuI^a03gHR@*cCWWveG821b1~^060q<esN)UTsB(psL9s8;7NE7b@ z2}A7<(g+CwMdysCY-{nicIc%9YPOp^ue0opO{eB`9P;yr|KA~T^(Az^8oJ3}af*i7 z{L&EpU_)M)r&D{B8GG`Af|27OJ4_9zI~gYIj#~;wL(B)DCy|`~>t<3BmF|)TV$SJM z4_+6>+ux|4W3wVQh{RpT?iEMYj$)2j+HNj}MR>hPCxY-b?+e+SJV$;Qeh5&q!dv}? z4`l%Uy+v{SK}I3}-KP_;Q2>(X&3yYatP_D+WqZQ1EA^6J>VU9-b@E;?#L>Tx0((D| zSuAwtJtQzF?{gh3iIv|t4al)^GKCA#jAf=k6k(!G6q+(2#gbQI-{1gR{=bn2&V<A{ z>e!MpQ#?`+a+?U?>VUg`r^Hxu5X?~F?rvq~fM<Iea7!>%=%ywUr5w@m`b{WzQ38## zuYNL;T~Cfl=z4S-U>qx+EF6ol$5Oqi>sFLZp5gU&;&&T*>9F$LiX7+V5Flo$Ak2)v ztuLLyM^G5y)T%iW5FsswUpfwncRWQHC7Nu3EKwQxSoTs%%|^2O3gf}<d$^<1<D;jr zUL<`93vSfL5uo$ObP<6V%R7=b;C%P${X}BcjQBN|gb3RG5lj^^T37O4ZBbat3?y(y zkgPkSm62FxE)h|ssjI+Jf|x8qUI?anas8(gAg$mC!l~e>&E~VJIf{(19Q7*DegGE+ zFQ83PWnt8~i>HEQv@%SNa~gj?66*Lyv+H0ZzT-5k{ZP8q=@l46y2UrbP>xTYeIDEA zI1DeF#Qh}<cwo^v&hyRu`;J)}I7$M9p6d^oIy?V~eRdDPa0ec`%9R5$)%|;8W(w#{ zSRmjL`Pz&#vy}@@sh4yP9p#&a`fRr)X18O%sTi3@jRv;bD38;?B-kCr_5l4<s-(!v zVT(Xa3xsl<7d$ISG(EZ>D|O<UV0*hq1@yQc+RWPQb<i|4!ED=ZQS+3u_8kyn1qsn+ z98fzeg&!7gxV*}|QR~v*>io>sh8*&Mqi<@qk)#AYN)<0%>(tw2Nt$5n7;z0Ew?Y-O zliw)-_(&VE<WjTWhMoDpJ>I6Gh3=dT3(41~#yPdvho#u>D5sSU@L7rzX6WJ0*lh{j z*UlNlRcJE568%elmh$`5ls-m>MS=<p64sq1<S(O_81hdtNtiewT5-ls@E!}5I#C7} z>YkbxpQ1aq<qp6f@QfS`Z@-$>9SSRS%}sPhMMc>pKOj7O`JI4BjkVZl^1IA*2W$-T z!e&D5olMZevr_x$BB)B$JO<Mwt$~p11=>$@Wapuiou?1G4@q2k??g+1XBYG6>BE5| z+f*0XXU9%p&ZbHS$|xdI$B9aG!PWjkOs9+Kv0!3%`Ob>`*P)$R!sk&=TkBWD(z**i z+4niaGj8VuJBQOUGc99aL~+yQ$$35sP(ZT@^CV;DD@FZrM3Hi~s;_nCmDJuKDqc24 zn6hZW-Sr?ID&@Caushd{D*2|^#@67znnpiZ@Xspdw<MvRcZ)qAdtiu=+D=<em}u=? zh!+HJY0$zFeTsFK%LIQRPNQp$QslmeV+uM3uzn}~VMjADJ`nC!o3fr}&w&-&({G*4 zm%WNNb9=u3JAf`Gj|rn9q5WLD8ocB35Oj)5?5~%7o3l1Ip3Hn@S9wUzVP1xlJ}AN< zK?zfaYwg=4)c}F&v5vYT{eD_K>KU^O#fKQiLSfHrSof><{<_tS>xbL^YyB|dT28G* zh-$Q(=VMn}^u@{EXpSO^Q@R}r8Yz|z;lF2oDrqgzJO(?ml7~MG`=0g1Vd%X9HSc;9 z5@tR5VCRQ;Hj)&StUb|y>hHfa<Z9c@nfx75wneUyW=uL&4d(RTWaBQ(Df^LhlRe&Z zNo{K03X*9jlwJS4voO-vvE8?DFZgR>CkM0MJ;!MHg{TL3?3=8ii<Z0S=o4mLTW_V1 z#mzL+<z!eO$5vU#VZ`+I1*rT=?03!XwDxR}i$Nx=+c)&GPY2p{=f;r_T`-;@|Mqn; zoW=PzJpJV#KI_iX1cG3){-KAV=T`2eztDT6D3@nk6yr}!?Qc*vvA)gGOO)eIMBWgd z#ra&<47sbER?J04^02naAb;G1)tK<|!3V?E??*Tkl{u&Sj!U2Ph`0|PMxrcvAkXL_ zH2>siury`}zXiG{oouAza>MQBqG?Q=ag`XgatFWQ=uoIFN7qc8g5AhUIl<9bTb}dG zMYA8^1~c+yEdEhPxgF-1LnlE8L{}3e+nnl<S9aN+FZP@zqpjH%b?yP{Ew1j^F##~` zI-hQiXk^V{z4g%)KRoG&rK8i}1}ee1UL+}LF`xrU+p)HMyi=C&wlY}Hu>9^CFWebI zyp_)X5UCt<TZL8RU7nVy{Y$2*bQfjms_jJ*By7e<SUhA%Uz^|jH$Tqv_=RX+LeM_p zB2#91`Yj97$|UzPHSSsM%hq+vmQdfT3dsuBir}W&ovvL%%S8ZYeYRo0)iOGa?z(xH z1ov`_^bn%?`^&MJ+kmC(a9m!8C5}_6Q47js&uDGw?g6^Fi9q;|iKgGrVHL-Uf=1Jg zR^{uMIwanqg8+s1o9{Lji_~lmEMGw4_`!bP#FBJXm*_hRfoE|d<aw=kxQGBalZNLu zxqmm+v_+QKteklozg~uO!y+Ssv`Eb08W)NYvdjz7ZP-~WKr?!F1C17DuGJ!x#gVN1 z(bx3jeWc9C93F=wZ#VewIIy#Y^^x{^A{@Dk--Z{zLa|pTBHE-ULDr73u8x;7N&fn- znxO8-<8*y`c(3B#GtPKql;@dy&knnD%MII%tPTrYR_*;6Y2w%jSC5GQd$O|A%2SI; zLv~D5&>EvRiRlLyotNnvP^f3tp{E_Q1<!$*Uhw*|bllq%Ew>vkfa;h!VQqN66qwe& ztx;%D!M1;j*g6Yz@uGeB0xS2Xh}rp5f`QNRFqoK=H-szRXQNERC`DYDpmGTg2-fO& z@iX<yZE}q^j=hFkl%79N^q7XHuxuLCq!m6>$?&7ze%H(AYZw+rK5tJ|?um4rXWVu@ zf&t<4arywNj!*MB_Je;c@X>bjJg|>1x(9bWBv;hb)b#xJR{iI*oK(efG?&3eFpI!T z%uXD7x;QQyDcny*Z@3lLl2L2>nDA18%N8<+%N?;0T%vt@pG$cv;RU)VIfbUqos{;S zu71sZs3j5UP@EHb4zE^AZXeS3dKdTO9lbVm8u{+OOsAouXcq|*ythDzkEvj@2y`!G z#b7icLv)*r_N;>L&JDPn#x9^&$@f>wDTnj6X&D~1+YlfLbL`np(c*z<aqTGnO-g0c zN3t;&Sut$5CS4wu*M<GDp`$rE&-^IW8=$;Oso}Y*nWI5C7kH?XEE%?34@a@1@P1U5 z-#@YXEV<MfAxF^t$hnxsrxkV6_0wd3Mwj;ogU7n&?-%&6!@~sj8{vIXs*-`|UY%)7 z!mYNcY5u33ZHP#Ej*coK8{Um1bvm8iwWcfDq>-h)pLZ*GqdWUic77BgE8tF#=Wypi zi=PWab@o3bepFY#k#g&j<Lg90lRZRAZ)J4e=vpkjjdr45&}3cff3}TavjaUGwn5)v zrfs}!@Z;tIf+R4|Nd9T-KsGl_y?Gx&bX=0JwX^NZxsrnyiQizU{2)ZdBHJR)FsWQp zpwV7@^1}X{x2EPOfj$&C=8tIKi)=c&+h<nZMy&qdMQ2Q~XQ%}-XwO_q00p~xDzDi5 zpuhP%caf0s9X6852h`eGo%hi=gv%Ek1gApSFq0U4b&)fhc?+Ud`mp7QW%uUlzSX!T zt%l_qUH9QPZ>YULhtXjgbh-&H;FK>x;IT<?ZC<4sZtzl2#LFvR#Fww_z`2r+C-RO7 z1f$j1rfc4_nJkS<u7f3^0(6M`>aC{NB<62CB`=Q0Ac$#U9t8^RnxL06Ev18t%T9|y zcL%o!6tAyu9m#v!4|f%BVs}`YxmVfAb+}C^$_#FeKQZ^;a73i>(nIWw>d0%kE@F0+ zSV~vn707Wu{gw^0-s<e|9NG6$nI75dC?r<SQAVv~Eu2JW{k-}a7Id}kO__j5IhZGE zUJRerH911TCiOc~)4?%O|2k&l#P#E9BkV1<^!qg6fN?^)DXe^GtB>9Axk$01JX+%z z5f^3f`3A?2K|(z!Fh7DW=nqoV+L3E=7D^f#O)LL!L+&SAy3oCTpWHI#?7}`}?hJO} zXge*DQF8W6rce5?R#6kzE(_c&c@@Sm!u|GKq$ucB3=Bf=o&CZKq!GJl(=<<%y;*iO zV105i*~kRXpB$VR0X;vJED@_N;J?~5<OiC1*E`XFvsm;6#!7+zSS+D`AQtNXT`9IP zv9WiwGqte(4~{jgwrP97j_k9l2CoQ_f-IVeP7jMM>kraNjk)a_xfbce1n-|hmHDp) zO@^^a|9+EALcZRREuD)C5iT-gKZ8Fbi-m=yS49=PEtt=GD60b39u%P_b(6<zJ>IWw zDvJuSn9StYn-Z>QkrSw*aCJ_=ls>3g5$zubnqwkO2&z>!GGI#HfKh`3XL$!bTLoI; zk;jhugvC>q-a=|6RkG*H0$J{8YY1eQ4*2I?C8lbsAXYL7uMm=K(p#3&8uRGSSU4ta z6zpu8>Y!}=sXKq2#c_2{DS|TSr5wIjc$7geML5`0ex^4PWiuTwA1=IL!C%MrO$CQB zXxHXV91QiWsN~JHW)-(;Yc+0@kvx@#E<fE=R=q$01Lj=#!0?n+8gs=IaFNT~gaev` zQ)z^}FmnL1bNh2CJvJK^^?l5yu()^rIpn?a*nOC6WrC^Bn2>#-rmv=LkC?7@stwZq zD$4I+Rgb<Z*jbPy)3Tqf46M44I-NxP)lh9}vdcF>B_$b~2$KyZ9S%b^g)v)vPw4xO z4BQPH;Ya)qln!=#H(0RJbfXyF-Oz`uVtChs5T>@0^=|*}w>yCh9C{yt+hf~_9y+>S zRue!fl0jl55bqtzcNG>C4^~9}K#a{K1~fE=WHQ~<G#TERg}gcr;RcI*;LL{Hf<|L_ z@ez4E0>`wP!XI1ALMlAIr0^7biZN?dpKWqwC~sB{D=qAh7^@R}jB!gjT_002UmaWM z>NO)g%bGIVkX3xFP`V1{?*t5kAfX{<Nv9kVPtjq74zB2Y+FZh{5wdjnk*WD}HK7|v zo6SuvbQ*7c!yYzv>3!*KOFgZ@>=^$n9R`Yi&u90@p;MLZi!wKS&WJw^R~rTNY)X1v zY}U`$)+Cy(>vV0U=tN%b_)o^DE|fp{Z@D8T8($5f$&=v*!3D}5D%Zs_PkT7?y3n>T z*^02T_`^=DH^J@}uNui%sEb^jg`)d?-M>{}EyzPMVQPRaSkJc43tvy__*!`MICofT z!HwtTOFia1XbzQ| UKVI8;fW*U)k;q5W!^k{>8<?xY`j=h?rxot1Tc-C@lM%nWO z4YF3L+xuoV^v*qVz@b4ybF`dOA#V0nBgRv`RncDpSFpoi*9XxxMn2#4t?y5gZ12}c zWbzJ4+Lq-Jh|)_UB2OP_f9AiRUk}(6vR85H8LCM^wdxNJtY_Yq1UIn*{PXO<8gYEZ zFtc2{S-B-<ycmW*Od!yI;Dg;cJ^KR1)H5jD)%LgxMn6i)yXOHa?Y~($Pu{Nt0JWzZ zWMIN+96SAA;R%A=98|{$ECUa9(bTJ>gD{ENH`h|C^o~%Q%-VKltq*~;^c`v0<8Q>N zK=Icv(=<3f46b_M7a51Byx3lRmlt#^$FA>c4@k{~xt;}t_BO$x@Xc1TbO?9}RMp=I zR9`utEE#*8ae@$WE9X2`mxthEI<0wh390UO-JQ}|xSxY-(MdmhSp_DDvcw6^GYmW! z%$-HOwjrvxFmYl`mZsTk_483Omf1@0%z}ySp$eW{$v!45VDCwy**Fg<4-gJvLrJC9 zT2P%^s5C65DlxccTr!~-w3rdtsYA>(i%cvM`iV_AOWOzaK?@AA-`+P=R$H!-h2__< ztjk8KrvZc=c@E;pzhK3AJ+TF5G~_`h5LzTS1u^*|R<3_n=SH7=PwKO?l9NnruMYRG zj{gpIuFkkI?7Is@KWzj!cTXi^wUW(j3ojL)TVy^J9sDlM-muH}KlR8e{=jVb+Jvrs zg`cvCt^H8XK&ayjwcug$;w?#6)NtZ1jqisnw1Yp3qexa|L}V!XW!7vI+b9SP9|e^0 zkbA*rRrE0Ed8k34ATLFM{095)$aWODj%bAi0crV1fnopu$aZovFf-9}G%>Sqa(49i z7u?wzI(CH;m_9GHRP#uZMNP$Lqsgj2hej+|+1)`X39f1s)|1d&ncP7s>kB?#_*x^t ztm_i5<Hdf~@A>XJ&33cH8&O0A2*9>XEEF7CXw<o+G>XSZWTiU1kBQ_Q1V(#S>CZEi z%1{2qyx|RB-hH*=#FjZgWh9$+8dhqNCT&5dL%sMv0C_-$zc>{U<3&qJoU-yMF4!iE zc8Op`hCjugy=3V{3GF4zcKCPAiha5o#A~)IAOSCJvRvR%p0X?zY@6_vxMn<!lmuQX zF)Mrz&4L$kvd3#>o)ufMDn&G8!ILONV<*qWV3(BfhL-}*l7+hsbS?MwGs7xIH@i5A zL_S>eIJvwUvI)h7>`AlqYnjK-aT9<#f?0Y!;1RsXH|0|nu`*+a@}&7Io+K(FF+wDG zG#nU)F&M1#Y(v3Dltb}ainM@4Jha(ndC4Ym%9Fw13$xQ_p2U$%OEIvWJ9BD}!{A|V zg`RmITFzn?;<JUt{>hB?!~bAqzDIN^r&SLB_4Yn|d;gC%<adLyFV(ZPzMxJ&>_4cG zCdUKE4LvJZc#cvXT0iF2D6qz`;ljySF3Me=GT5?1s$NxoE{>JWY~Qz?TP?{C%)pvi zPC!CtaUl(_BtdN{HX<!MLUcgv*?ek`?4XiHGVIMZiC1x{g`mdPyL3f~V=qtf>WM`e z$|TM76FtI5?rd%b_RZ8fbeQ5Vb)>6ovqdp1QR)Y7lbnvglR@)76|pAlJ@3{r`XxtI z6s;d4YaZCHjGy(o5?d)`vnxu~?`Q>dkSyIBUWodUk0sV2Ys5;V`32ck3~VPf{Rd|h z-dc|3nStw33tVUyU*htq3Mc}?32t=jqNZaN5A<gCBBD+sX!9#ovfU=GccALu1BQPt zfI2$!(jh<_Zz-xX(ZHs0tG_iav=oL%3HR`J3=l1LTan9_KN;C$)4H)ZV_-KReRi7k zOk?bkjkWEcFt#>Jn{A`lbxHhWAa%t!WJ{bjAta*!w7J4Rw&ghoG6Tkt|M?yeow9Eo z3m<H2I_~MnhgcvAA2j3vK#(_n2NX`l<@iNa&c1!?XbkL%QH5N@kLd|4Jb<gQHL)FA zck>Za$<ZC3Xx(1h?oIoQSq6XMacO+oQL5E@JNO=&@YZTo>nZWFT&brGNn?yK<cNXD z!|<1s{*cc6g3i~t{8^O~*^TG5tR)y6kK=x+vSiOl+*9d()<jkR6VIJ}BvC^1XT|z~ zhyNh6b9ZW1HF!4KMJd^5OgY_V3GXe<*}@OP@8+EqI<8~2A&`vk0TAQu-8cY@lL!?o z=CI#(#@3J4bg$^tZkrI)MB=r7uD4%y7eVMwK+c?7WmW6tiu&d}eLQW64<b_uqxXG~ z82!HXgr_o4kH9w6Wv^c&({bDYVkVXsPR-fP*sT2;@sx&NoUS22hbrNlWyDX(F-IfI zr_3r4s3h>!3fzzygTMrR!J;?^R3`f#?m`nAB+r3!j&)xfgO^kBLhnlNsswVjN&t0| zs`3;8xO{}ZxA&`)VQb6mqh-?1s*RPxj;I@R9FANV=`BVytF*WUHis?Ia`2N#tTGHF z{GX)5`EEtSskbV0=p?F#fg8@O;JZ6+$4`PeEe?8#_*lqM65Aqrt)TvNeA^EIw4r$e z=gbi*^v?38t~qYn(tQ?rlJN+hRr|~n_PS*T7R8rvl&?y;&(KO#fAGxnm^Ol*PMroe zHHB8>qg(%4*q)lMK6)Lg$ts<!8AsPB11xP@nYjP$P)^;Lcoq`nI@5=6?zwZz3m&8z z&mLQ(24CKLAQ+8r9=4sYS&A)in`e@&&>~6UBMb64B4v2wCC8#vKhx0*AfK6mcEk^7 z=6sF~h8)7ih&@xATu?swmUg>S>{ar%D!Dfnl^qguXL`2jG?U4wl8hahIwHfm%d3#9 zdX|4skt0>DI`8n=$-)^)7CtvU6J-cYXzTt+?@67!VOc#Kx-}TEpSd0Zqgqp2?O83n z+4{86GFB~eW=u2}q&q{kDys5l(r`IFI`U-2jHYO%5QziI>cM2lr5E`E)N(CXmP^z3 z_3b2HDQFT!M?pKaADi38r88EW-tB@YVMlD0s!zp2nlHI~b=ctf<BmvV*NH!^hkMr> zhqd0i+-auc+auckr`W$_;M`iG3Y;38atAYj6$blT?3GcN_39*&F(ZmsJ)F^;PD45A zQJcQ?7wMIQv>H@kVM!8(^)P&Uzm~h)+xzL^4vWWQ8@LCr{Kj^xBKBn6ffL0wdm9di zkc@zlm6!PIifkt<qA@;c<A0YB)CwtJwKIW72ahSR$up-tW;DWOxH59DB1VZi;dFu4 zoDwWqV;T{FLW-1$>dbXb@Z^Q>u^8n$am|2hPkc*Qu>tZ?^TJs=n-#xzB##%WM-;0I zY5tg~Z?)spRYyZ2+NsV_bdJ$L;;8MOy$1k2^6WVcSKkUk0-zDnZ2}^wQQqE%VOR(- z3B90GsSwz7%Csfv_LQW@Pf41&;{_0EYTa2=l4S-+)6GK8j|(Y;m&n0#SBgfOs&93b z)ThowJd19E24C2%!i|C4g6&bmgMhmlkR-oqO2Smk3MHUHr)EMLe+%{;*E9)v-YnzC zUAF56U8_}^Nax4el-NeqSp_$yXN8rP_Vw;?4KEHbRDRdjvigvxtu~#9@Md9yPPBIj zecRLG3^UyuL$<)clAK>2j?q=|dS5@&Skjjy>b*OjclQ|zn7*)H+{uFgQ0Q<JN}c%h z?OhX#Wv_SD&|UbAXwxT9M_c&^TT*Z=e@H4C;qq)w6F8;w1bVILTbSUNPuH12;DUZq zC<y@Ny~tn8ErqLD2C|aw8}L&!S)O&e=uDC(;M)Cy*5c)pa^!1j9j7xef(?nXkMRwW z1;rGr*VQQDtWd<U9ySz>dPsM4ESy@Fl>XbH!EIh&OZ@Gu)yERQeL~{5Ps9P0AOqU* zA0-Jv#i65mu%44RcYS&MF{dh3ZThW)Dn~Ufaa$y7n-!9Q^Q6h(3E9eXwO>|HhLu-F zxCJV`{;RWV+Q1o|`Rdr8(1)Gc2e~IDYKQ8?seOvPyrM<BAXYDLh}Ak17avrM(q%cL zsVje&I1|Ambv5m8Q>?yt5`z%wD#;{1>XT+~+lplUl|HL~CvoYbQk@8_>Ew`x8T5P; zNKFJO@LaBJ^z#5Pu!_YceNIVeelL>G7Lan)nUG*BzSYvIqfeXH@dXzog^)>K6-b~$ zM}2W`Zrl2htY!^8bowSzt40*8A6S(rC5`uXpCgjIiC64QO<*(yt*^;vm^ZR;bW8b; zi4@3VSvavBN#UAf$`gi6seZp$(`)Da8e4PA856sfa_C~Frt^(EerR!fDJR9$qPs%^ zwkCfnRf3mURIjm-*a}QM{i8}fw>v%^o^!PFs)vdBG}%17@hF$-uB|CiHTtws+48@j zvV8?TqVH+IRwn3;{eTU1wAcH2hyJijOugqAd;}O)M+b&1QGl)3Un#IuAJjh<Xep;A zbZuQtnogT`$d!K5uG+7UO+ygpc~)kt46EkC3GA6g2av!Wxzp}qc;OPb|B>&~GTw+# z{-i7I+g36P{bRbM5{JgfAFidT^1bdHs9S<^7ud8VXj2ZFLjGVJ^RiKUt}Xr#P)h>@ z6aWAK2mn1r*HAsA%ld~^007c_0RRvH003=yaCt9nWpi_3XJvCPaCz;0{de0&lHl+D zD=_rtflO#7_U!JxQ=;rDww&lDmb?<3ojpa1fFvkk3<3-QO6GcffBV%(f1?3{w3FQK zJHDBiA{y1z)z#J2)%7tL3|_|bG)wC=se)xu2DeF`lyRNrw?R^t@WU#ps`wV3E;c3p zT5s1$FuzOYZ{e@Wqem}_JgkFwu?XUP8{|d)q%PC@G|uFk!Mw<;y4=j`w8(?3xJ~Cl zT?F-ALa-hYtfU+VaTTO>FpqP1W3?`eRa(KzSzM;|7Jx5;Rs1&5_+;?q5yDQZI?3xv zZ~{0~oB5sDM_L7|VzJ4RAO$3ncrginOlugSLa!Rbqj|Pj(BSThDtYvf)^|vZRSW|w zZ=pqXS8TFHFiV0(vP|=2fymOh;=Bg5^I)}IZzlp|R3^88RJjH6`~Y8yGM&d+whbO& z$Yly|18(tskph@Z(+S={95Tj_pt?i~W}6zJ#3c}-xX0%4L%a=^KrJ3EAaxJa08&pL z4F-cpkLc07$g+ec0Kl%+MOg<I<svDQ#fx-aKN3HciTt)mm&+`j@ym7cu6`)vwS04t zET9Jg8L&ev#G`p!)nlMA{ud#SgOhwa4z7}4H%UHE#=#kmZI;2e>1K`pTqV%xVvVT9 z**JKU!xs~B1f3@^#|GvvQ)nu>C$a;`Dj~iUE$D}c2xqnec2+=6=`Z6kwpLGvj{^8i z;iqx%9Oej7=D*Hp&T%OC_fK)jKhtQ$SNJcHmH!m%Mt=}g8X`YCFLLn`kcq!s+$UvL z#6aV5@N$#Woc)--g(okPEUA-)?!*F_-^H`kdVH}Y+P}$X#U>ZOzmCf~70AF+uOnDy z>!c)7!BhG}gLN8Ik46r?>M{m&E8t5(H$qGP_)}V?!28BKd9m=(_$*%*K<6SyP8$cW zpy|sLnWI=os|42MqB6fnbrDtD)vU-W#uMrEB3X`sfvM+cwn;M(VNsl=zzCw%$>sOa z>x-)~eYto&<uB(auTEbWU-gr#>B*)3_KbmCCiSMwuj&=xc^<El(|2o-T)+l^|Hb+A z^5mzJ%d?a7DgLRxUJ<hAFn9)n9K|p4lPR*7McisL^YCL@MdHCY04cNpf~;mJIdP)+ zd$i2rTQT6XtLW-<Iz9RB$I~(X@%+b=tE-=IQ1XxH;t~fpJ$pX3yN8ik{bSGYo4ceo zb(+a0n|u}Lw^?Jw&CNNdRrefz(XHeoS9XpA<8_j@@a55?>A$@`y^3A};&0B+UcJ7! zoWh*_aCSbu3XX&KH13ck#{}3v99zH0rR@BSi`Dr_$|>K^z|+ppDDa%0q)2mq265;7 zLXyM%jYW&|D+vzgHx#(=(}y5<OdG|Kl#Y*(ae*bTrk8J?Pk~&>-I_*XF&qVhlk#?; z7YJ}a{056D-ek4?h?WODM7Cde9@+1DTH!K+1~9PZ@6X`DUmo~(&tM)0uVDWXyIc}1 zH?VEy&_hrxgE#<1p~6jw_U4q-45?rwRSiq!9W2@;7=jo~N`QsGK3T+d{N)Cke<_|< zUyk4%*z!T--KWTP2tmV~1XJ8#;eVS7ws6o70F<FF&5|0p!_}G|?vgwxH+hath~KgI zDk$;{)Pt)x-+zC4g)BUUf1;OXf1SR$G^QyOlB}9!$YMu9i2qL3+f?#_YOp{ih6ebr zw*hRC_#FQ6KCX!mp#gpIHklPghAr@4Z-di!^Mv>wTP5-jZ`1EUFz?YEwi2=NVq5)J zD>-=Is<+r&e)l#61+%JnXTi3{pQ5>k9jQ_9is$${fCPMXL~udn`XR}nw}9KAN?j%> z;B4L5!zie9uHzC#FKl9<I@0!dhZ-+!d&ujo;t}LLVLcLbQo*nbhJjo==e?AE05viu zSWDPbzoy4R72+)e)asltz$Xs?r<AV?9L`>xo*V^Nn>ESlI0#{roq?W=nspNLnqd_h z&k(pY)10+uT!}HLA9cX`L!dr@lrw%*@Cnok^w|P%DIZY(hp{a-bL^|c#aPEMhy}lE zfRj)rauOKx$FN@ug^&iksaVO!Wh@wI0xD_nHZLA3i7AqAQ<k9O62%ZP0Zb~nk~;XS zfP754oaPjvUQ!c2fHd3SwsQ?zoz{46B--a2ni-t&6vifdxdY7#CLAQnBCT+HS}+pg zuV@aMUJd<xCE@%K&)*`G0CT{qhEqtLxO69>y2(jss!5Un@s|h%ZKa`tL=2(`g<=#9 zt0Y^Bow|~v9?KDXhd2tJd`pBlQuBvECW?^!#eS$sD8AW`o8$LB_KoJTq!g{%O8lk< zIxp5+8YH0|+DksvL&%c{fN4J-8snM}pwZYG&d7YdF^J~Kyv?mawG3CiSd#jR?;AtR zk~ok04pdE3b8t=@ei)%Fh*q$BADc%`qqV@vp<N<a$)=Y=I$kUyZX#Gv^7ByxcMrpX zfIwq`JQ3!4HAthuj9;FhC{Z$N>EpI<fR&C6Q52BO2hbMaiI*mm`#9Sq)o^43kU+4R zkd#%kg|l)diX;Sni2pGNCXqIX*0vJu@qKX|Jk?;5{4gj_EGTfCa3P>hapdxZIXwN2 zF)H3#nFU+|t*YB2w5c>62>E@a%nsM$=y}s<TH%2*t+#$apL;+r5(BO}?%W9B-*^Fc z7~=)#rL}2MrnhPC!MgQ@<r0*=Bj6yCiT-sUHpp1+$MC<rs7YBy1Do1tM($EzbWk0J z_6}V9Byc{>QrPc~MbnA=MazjST`E<j4<NbodLUKpZz0IN7qQ8Dg3Dy@c-ltL!%<-1 zk0=CBo;GH|{g_bZMYdVxhrt2%9xMJlmS4uiQO8R*pAZ8a^FKQQ@+{HFj>U}GBNa1F z2znXL{m3~umS1`hXOSG^e~dN01eMRX!dupN@73+D-aXfD@0Gi2-S%6T(n#FBE{SUe zyAp%UrBN*;FyUB}*k%<%Qp~$Se3S);5MLVeej}Q>&235dkrUlLA$`PS$28#EVLR#l zE5u&N-c^Edqtyn^w6tJdrHFL!#_a5~{q&xi9kcPN=ApuK>z2k%_CXxBx-;pwT2RM= z>9O>*3XVxN!E&{-Pd%wUCRQF!uGYysU8eH@?HF{dwhk(f$xN^;vaEQ(1FmjG7e&ss zUL}Fq2SGG%C0hXx1b<K!tHeIZBe>hm3QoqLnDco(*|MtK6jZagO60-0pjlZ>G`rr# zRg@P|T-T*vdoy?~<cA>zfJp1JAo2v!TPU#L;Jm+>&7E7$WW-q8d5M;9tX*Z*ef9 z<(DMVZb>><{v@8yfp#gpFAx^=YES}oC9-Z7>n9m({23dTfXWYd>HJQcc!FY1rVL>w z5R{Uy$Mbp<i+9<GQz?{q5fKu|7kCj1Gd$CJ6y6XfXe5Et|KxUpm#8ozvLlQgY#-7r zLo*e^WJ3e-&UUSUnJpB1rggHC>si<*5XnW6u%<}H5RIn-hCG$dHg#f<FZ4`Zh~r(y zRmFBRI`%EajT@>v^6r(^4-@u5j~^FSU2ch11+AA@+S49jp-2j3Tl}3mCqkMxK6v?C zJm|yuhK`?EQM?T{YobxLxxFQ8u^N-G0w(fS8zyU>@H#|{fG)-imC3wD1nxnDq7GqT z*3@VMoh>Ey^8y&AS{M0((1BTJaZ<nrvZ)ebUX*RLxH0T;WSO@mF^CtwI%ATa<lBk4 zBwG0V*u8=rTF*xoS2r8Oe@1$UFr`CnIJP0_d)<+;_n9B7F<pI5P=b|7yr5GA({9NB zjC>mhX|drert#qYhk-os00XACbgB*~ENdkgwZj;e6+AJCi8?+G!YINfF$^k-!q#5j z<P{rlfzx3icQsELFTzBa8iL{b4<m(w&mIB4`zB#z?1X9qt0ot(PcKiV7ngqI)7-ew zZ83v94o*lS5*r3H=&{@?W|M%1D1Cw`UclbY7c_I&ct}b(z*uJF3-Se7B=78<a$>bc zk`(K)Fugn2qG0mcY-o7&0?durbK1=|8$BygmMrT?Ov(^DcDS>_2<B%7Z1}c=Me^jf zFLyNfVnalK^7}FeqrEPAGj$8vU;Ghhr#YO&>w_`>KYs+yAUKh!8%RF}u?Q;#@Prl< z=h5`y#RXo8y;F`y!V?R&99S?0qSlKkn*Z9Qu<2_<lGu^FX~kCUV)i%R0yKy`mAm)* z4^q;S4h)I~g2lf{3Rr^IyS(~P++LVXYm|fBPblwbC$};Dgcw;oaCZLU^sikEQ0-)Z z%4L8ShOXg=0Ll>WDf}w$aCmI^)V#fOT<CV!7*r<*T6#e6vzPuuk2-+&o}~9+$Lexf z>XV^|Ssw?3P(7#Vp9}6M-5|yhFZssOI$2FTkWSyNvp5wRGOAsm1_7WItNP+MJ|rxK z2ZdC0ne*@lG;cySCi!`sOC0sv0Bw`gy97P#D}s2ocB~Ru7baB{-}GXW4ol+>;pk$; zr)xZ*(a{7>OsHWTS?9L}D<{U$!y4(kx{N_T4MUd??VY0r#nok2>-}IzQbPD9jY5Z> zF2J}i#JJk#^ES9C8JF6w7Ckptd_{j8yo`aCJ|(H|>hmnKmOk+lXMleW+T6Xk3ax$X zlmJZLCR@<pjKhIDFU{j(`vpZS690J|yoa7h7~;QZXKDf#$b9f4^BCVEQ%se@ah-J4 zv2pATQf2hA`i9}Rz(W^eJ~loiVJQx3#@Tm_4#w>8jGo=%3<})QN-U+)n;7(?(WFdP z#eE`=JboI$YM<ZHq+YMsXosCWzj*cP;yikFb~Qcu%jp$fRt%*><n;~RSgc&6Zo1%4 z$kli;oWXD-@tcBZLK$5j{rSePi?=O7;lD1%0Rz$N{idB##=vjrJVZX@Aa*i1c#<uT zCkK6|`(U$3?^|%G%T2;hJJ69j-w%9>k|v-z+g;)H6#cZ(5$*wfR9<i4ER+Y&sJ}n6 z2M;+LWZPaNZXFW0X~$FVA)WV4xWT|F9#*v$+;lYm8hwyvsF?lJ`w!i!nE7enmv-KJ ziD}y1Ze1pIy#;;k*P$T8I2aV=php)Z^ShhvfYM`cwRYE&>GU#sar*M?{Pcx?x!_0b z7gOLt-*$mgmm77Qrn{%Rb*lT$Z^zuqxnlx%kZ>pYdXbK{`Su-DMHhHAPSj+i{~?(^ zE8w2l-;#Mfz$Kie$p&@00d8^w619q49%uv!eRK-kGFEZEA%iD{prF%u9n9kS+oo0t zm_|%cHgvE$Ap&lWO<IG*0_m2c856B}j$Ne5Q|ZzGPC`!(FYM6g@e}9NLo&p3&<J0Z z!5Ms(5yoieIEt1a525WrqMpSR4I3>Db@vRr06qo!AIhy=gl+I?H$NJ@_~!f!_F3e} zhSQb^TN$c&lL@*LvZIc1Icb`@gvEm+N;>X->`$@oxV^DIWJ}`<BMgA)@dnvM_j{$^ zg^~Sfx(j&$FQoe@-i4CWi*-`zx)j~CSeDoi*|DKgh?oL0rDlA|Lw^LkaPTTaM5a}K zlReQJTG$61TS%!n86zxb#TWooyN>gi(cF~E>QzID<cZyZo1T3iawZ7fvI9Ny1rtC0 zvVAAf>s*Dm)G&lL%OTG%NR45ZzD>yfc3)%^9!UFvwz(ARuA^@6Zs7m;W2ZF-&De6T z#+od;e=Ym$>vW%cL#wfz9UY{2*mV(XecUzdSy~@4KZs@1yo<!6)7<7G=cOI|&Pg%3 z60gYim=V)rqUHwlvq&0g#C+&!@Kf2o6W`P3LpXa(ulw#v2y79_V=oOsppRb@5W{xp zXOEGv1yG?+tTJS1BNU|JP?*aM8;fOQ%QC#}61ZtQ8=8pO#I}%VDAHgb-MhIledia^ zn{$kzdl5Z9`SHg?i3ciKv0B3(f`gP!A-y*9ocw}zwOaLR<(;M-?*Otpy-DoWZ_|9y z51l=Af?|UP054a0oT56t137dB{xl~>dFv44n0x)5^Nj4xf-qzKDBnm6Q!d?o6bz=l zdOPI5!wGs4kc$96FB3SiR_HNA{t<XNsz8uodT)0F^}4zV_PQJB(MrDq<rIS}fjgIh zd~80G!yb|w-8xI0Fe4F)S|-7|U_YIaUn7#F^tdF*M}`g{ZcHo<g{c>Hi4${=GMl%W zDGgwvt!w7xM-z${$5qWiS&d(>zq;u*hNA12qy6}R^tlcYpy<dl7qCd+*_J{w3KDmr zE@0o+8{y*ZSN90uJ}%QZuh|`tVTDmthQSx&@9SqrE!MHe!OOG19tDens8XT|hW<)` zHhNewA!?I(9m?a7CZLa&b%h~&!;m~gilq|r0yo#S!~p|C$1`u5hk-GkKOGDF+u!9e zTypq48r~NieTL-!QZ9_<LEii{d>ZV79x5JM#30;0r=iwQ|7#}m<L9wGM?tvjvHbp{ z5L>m~{E@qun=BPY^VK?vh_Bi%&)bJSppB(Wt0EtK^i=LQaBIMH=i6(v%B!Js#BfxK zvvd*BZM7|=r6M?yKUCLw@FaM8BblI=LDU|1+uij?j<wn~R(ubI!_y8M>hDU5e&lE& ziQA-oVT+>^9*mNAsEg1Q1zOjH@5NYdz{YQ*rlAf$a_oGf6&tDiq1|x#NkoPFX_*c` z1+8<P_A+#m6`h?wzj*cfWP0}9kEapm**H6oPOh%bzCV9;dOqC)|4vHnysPj@2aQG# zCee-U#iLa%P&I)i>@$>ZE11U6$4pb)tFx=;`ou@^2l9fBPZpRCgkmY$bTr(gzroZ6 z--f$%wCkrwPoCOWVL3-rTJ$DYZiD2&>&YC2_#eTZ4c>HhJ-sMXVf4nuXPsTdO8i=l zB<M1JlSl*!m?aovosWAE#Ky>);2+PfrqS8-^p#<f@y#Hn+W1s6_%_)JK}*iC^8A4L zU4}+-DRq8eRo2#^<q$P2^;0KM-+T%9RtJwk!9|w<n5Z&A#hepCV0tzYXM<@Q_y{7h zQ-DTX748P6^B4_1sLG!_o0xp$F|Vq(ntOu_*z<O?^B!jt6P^eE?p5bqY+cmoAfldJ z?~Hp0sULcXVZ7N+%uWbCBzh3khUp#40Yu$JokuOA2~Irawo5i0x$@~~KN)57k2Cmw zY}`tt4(ayd?0GNghWpbK%EXx5aRxqD*7e$TR`Gg>_ebmK*A4EK2W~UJr*7pHh~6Cx zclyAhQ46<_O7Q-}jlnPSr*@9nXA|?!Ash?-{}gsl;Z@Ra2BDoZf#R3na{f@mKtpi3 z{RGqa$LW8*vk8-*ZhVY`8TpaPG54Aml)>KPTBs&Uv<pYAGZp?N*@pUz%sCF&O+ReX z#ie$;tnNF{B0RS!|BFp*i(>!198Po2Na5d4x!-RYxrzmB;y(Xp1A<c5je)I*V;%Jx z6V!1)1YOG<p+@{uT#}`nAyoVK<em-eqw^lq-K3kc-B?i2VpAr^=xx_fD&>6_a-~Yy z*a}#BR(y^oa`Q(=*J}a%tOw*KXy-)o5|~U~lOl0NCOy!S9*S~dMyvy^)>*u@FMDVW z0<Xfs1@DA%>ynGR;A^l+W0tabmSpH@UYEn%qn~RRJ@cY(tkS=-lY5rrb_3!K@-Y|% z-yR3KpGxAK+4824xIQbOdU>ZmyE#U>kWfoh!WcDdlSA)B`@usagGicN@ldJ~DEqBF z<iU(tdysm@l47#8zT&nN%iJVIpLcP5>fJvX<Mf=gjJay*z4=O2q5Gt7qB<AM$4Ltl z8Xx|zB?isBSoE=_inmFNq>+8?;x4nj*ji5H!UetI{fCb<8z~&55qKPshO6k8=C?zW z#2(XS^M&U2)NvX^-a%Wd-;yIX4fht=dfp{@9Kqk^9Beg3izc4`YyiUFwZ}=m0qolP z#pZdsfX9J?^5>=;w-NC9+S&uwQp64=GMYG#{jyJ*b>sCKlUL!*2wtYnL21J$L;A0I z4Q9_o57#yWe*)IbA;>9xI6sZ2iRy=PG|9%@of%g09nL&aTjnSKNBKpQkmJcaY9y53 z@iFcR_J8*PhuroIM1EI5Z~6#hz~f+Olm}?32r%+zm@q&aWN_Koc*NmsX@$$BF6Kqn zo~}qWi9}dv)q=j&^A4sRwqe|e;qDr%9}hKXXG|1)dTi#tAL=0Av6l&3e%-|#Kv(Hs z<kyrDSjT!~Z^Cve?{IhZ+{ChHqW(epS6Tv6{)l(7xby6Gns4R=`+V#=q5D<?;bAKs zo%)7G+(FqVG{Z9<KX;>|<k`d3*xTj7-n+pYC`xkJ$a;gyjY+7BB3dR7N+ec$d53mU zT?<)qB{p(Mi9#vYR!X*;HhE;jF%?CX<N(?5Ek2=NDH4@}BCR()+L<_+IPLI2^3++B za6H0X9VsKseq(Sb+N}2NSJ9a~ynPC@r>i5;XoclI;BQ;Czz{{$KHfGl5WN5y--_G| z4sti{l1F+Vz)_1Ih0su6iKuXmr~>Q;Z2MHF-B7BqAr$-i3W|}m_zTjk6Up!ka?Zkf zj5J)Zi4LgXJ?m7Y&5r}n=Koftcs2tiQr(A-`OPM~;h?|3aCFX$#3%WHF6qr<K{Yl} z@8K~&Es6k^utB=u3mk^>`fgCDhYLJ9PDh}(8=^vF0wA;&jze@`c|oQK64;*A?r;V5 zqm3BW@rg+D+YA#WB4%AYP$agG3+IW_IsVoY)f_=Y4(_;<EFv^L_t$yl=MXn^>tp+* z>)KI!A+ej*kArEs<!S(RQm!yLGMd6<9Tjqrro0eMhfgDefo&sbWLCjeLY%tdyH(bm zTD`Iipt1y?e~yb@aW=r-$=LGaCdG0E1hJ2YpBY|DVjn&}?O;J4x7nN4?xoXn<_}n4 z-u>jTwPpp=nAg*2c<m<ymB<@Ai=ZC``j7vOLa<>OK0tat4z6TYj2u%&XsBb(->6I) z6r|7BNlO5N@z4P!z})g9y~OBG+(Srn7F(&21OcP0cD12M<#F)#K}8^7zHZQ(@FbI% zhEkt3MQp8I7R$7u6uB?C>O{kAnp!5)7JH6zZP5l6J*XCwO_Mkiw%!aK96eqwt?r|K zs_z7>(^y!^M6l7=gQ#l_@~61y1nNiB0_+FMn%CfKP|X&>w%7!#B*uWVe;UXo(abr4 z`A<aNi32xe)jg*%6=!-bwgm&=pND4g4=F<IA<;AE%9PFLg7J)+U~P8v<vyx2C8J=m z1!5q^^fGBop-bu(ite9cogp`9#M+p>QqZ3f>lZe+DKf?3MY$|Kie7d)G*Sitx&v;L zp_Hat<fL84^_|LA-BX8BrG&JxkerV?B|V+Sdxg;jz2v$bGkTlURiMJQIGw0eT71IZ zgMy%Dd+4UD#u<)1auG*&<8^FqZkCvv6wRv<s}mmLiH%Sh0g!|@z4YnU*g%Y>!(w?k z?A03zX6C3sGj2_Aq+O-TvP7t+2A-b#3FE1?T-!{BW}#Epy3oEhoAA5uHEotrd8CC+ zJZ830%W594(;61{e~5sy-MQtARrB)A`Sa-Vbo%D<{3`nC<i|IsU58ZzF-eJ5^!$R= z*PrG$E*u+S!eW!xY7El0s>*&GgHbi&Tz1F$59W-|1Mb$XAOj61Rk11OiL?_rWPy1% z-tf>38=$XdsP-xi(`#3ps*K&3x>=!0mpi~+hP9y@`Se_al-VCcO77MOZ+*_S>ay*a z)3{+_NzVH&icjh+<(!C<!)!HKW!*(Z1#~K?(gmLmvjP^IXmk|J;Lo=$c|iV{89mX- zMPXceBGn)))OSp8UPK~jQfgQ3ajoCMY@Nj$3J{L?TDcN2A?B9Ya;e_<6rv#Xky#qL z->XW|7v1mE?LqfxJ#*Xy2i5*CuFV4x#Vyp6i=R#}f4n$(;cjI1rXdO0*fE+Lj&wib zTue@skZjoGUh`G0#;IA5kHj+!p>&}zh4ddgeV%wiS^+^po8PnhQ27C0c)$Y0V* ztePxSj3wiQD_AsT^6MroLDtmJMGWU#I>t%swMmE0K(4N<LvjZ*v3%N!<$2LS5^$8q zAf?jDTc8$XT&&0TjI`VWwa@Zl2v(mQLt7e(Lz+ZKoemqKXsl2+z=}FF&+u^iE+Wg` z@v}XavU_#*P!?znC&xL~Zz&9aldt3XTd`-#s<nJ5P&adZ&j_OLJ}G=uecW$!FYdWc zwejEWn3Im7O_a=%`~{z~V#aA(aCnp*N%o^t@u*+AQxhMOPTau=x)&#)T{w9$k(AI= z(prf^5$)R+!kVg=v_N*^Hb(3v*bYQ>d)3%)m&6{>q99V3;gxiZWmgiJI%==8=HQJ4 z2nsoaL`_En9eB;aXpX@HpUkkVwZ^0~O;YQT_h(R%ehx*7HkCbI)~q*Fr^HJZ%i~A- zIYc^7yOUL`|2@Qhqqs~eII^p}<do#dbQRBy)bVDftbWIAB?_s&3RZZJtZ}26CaN}6 z85}m?2H#rNX%fw8&h5o->Rk_&yjoiy@~9AHq7#BCfxCt9mbN|<83PZmf_x=MPl-MG zdetB@8LQe{D7ZWByREzg=w6p+FHVj9i2Pq*3xTbtCJWbvt7!zQ4d@oLL>aV1_MVEa zqA2-DT<NMt@RZCp_VBdZo9s&|_lh9Ba7JeNBY5P$KQ~aNW@#hbldxo5+lWM9CYf29 z(%+VCYTQ6pQAJOHc9^!Gu@L^fXREe*)idVBGFm$BRp?zk^fvV$l?~TvHT#v90sJ9V zp@J_KZe_<(NHc}KSoOzp@ZPM(7Du&AO|<M1_dBxPX?C>8L6XCIGo;E<_i2G*ghvG@ z=C{Q?OwO#T(|SV|k~M7jb2KrBUfytjJOGcYBnj^7dR-lT`Q>W6-cHa?1?Vh_c{NFk zFO&SsimC>D36r)e@+hq;(55DL^(uR8&g2uxe$~OaHY}^48V{l<`4zLw$h4DD^8~G` zkVbs@N@La8gG64gD0vry*rrCqc?LT@fjU+|M-8Dcaf+N;9rYGS-IxfaP~oE;a?qVp zp(L!bu&blG6o#SBEPX}_S()Gj@!c>U>lNWZ?|mrBx8jzloTYV1Ie9tT?wG5`GeS05 z5qAb6)iVrQIudVsim02@hw7o>b&GF;s;!6lq2*FljPtyx&k!SA=1J3`SG4*jD{j*{ zj*w{#=aGlP>LrjURNpEfKs;r0*KDA_j*PhzHzq<#e3FgY*c&<(I^oJIw+gwueC`Ur z>sF!Or6DMf<4$K>`HpVy-7w6Gjpm-w*f2`FVVbuXJCqJ@;mo5oG1pqgClLMCfxx74 z9|f-U=wtIrQ$XlcLweJ+98Ek^Ddem|0Uafp;#ETyNdjFHrJ)Q8GP+`me{MMsvh%>2 z?|9Msh3TXmd~lnd7n{7^%TgnPcn^EIpP8qLVYd-<O6rOps31#4HNNvFvRs@lc2)Ae z;E4#g&~%I>?#K;ei9wFuNK%?IX-i?tqHWySVPtIf?Kkgqm_1u@LRZ^Qj*_u)Z?Xm3 zQSzm@YHo3-6Bp=?v)Nue*N5qLyIwc%sJncxbKs43eiaqiXz)VUwl#WF4%nX3`_8b{ zm0AE+pP}Ed#BnQd(8rnSnGL+vKt$<P(3NgO*?6S~A8?zxtJF@6UHC|+#8$l0Dp~<Y zr}^y>X;J-bHSJZ0jS06y51WCBs^hlF_zNSittP^j8TOmi&2pK(^H!g0#mr-ER#3>* z%w)Cpbn^B%8Zh;NG+x%gXP<wrMi>WzDCUhkq2N&od}d!^SW;Ud32rg%W;OUd8IpEf z1)WN>Dd-*Ln-yZqw5)3FHib^61R%nJN3+BX_2PYFkA)-ZxqFgM(}Omq<fpB(|2TLd zlfGq}g{~r*sz}FKGLO-{D6Oe}2syycW$nsr8(>Wem{EMufy_r2+Z&6dy0|IjjaWFc za(7_|s~fLcSF6tOeI}8od`P3o7#M8Gqa`%I4sLD6m)&`1S_Y5tT8(rH=;n^K7YD*g zEJ*TVb9<*nAK@-)4RL>ln^3o!d^Np1JO4g<d2#vbWIBbfKJWG4VhkAHqX#hOl2Ndv z78|-$5qB%P%TF>QGrRFRi|5H*ku8!^UP$U+4iXM#c@8lf^Sc=HmV<EnIsB~pIShl( z%x*r#n8f6uBYL?;`xVx*;Nx(OyIoGZ-$C<qB`&y3&;$Yt&cikdCqL|?5(NcGV57%n z^+_g6_##v|v<L(x(z0b3J9IiB3@Bumc6V8vbf>VN4-^_m208)&8cGC&N0c?n=s={7 z`Yh}`yY{64=K5oWS854@0`TA<9tB-Oup)ahV?KULc+b$0KLPY_#+R$~9sY>JLdFE? z9#QqvPsW|l0Tw^SLcPlrmK|H!V2t<$ed|6so6bp9j#NyQxrH0Hh5fFXh5l^>YKdJJ zrY;=7B(crJ$4)<-Tv0-iT__??VO(?*)ftsH{?u3_mr*qbQkK*}gslsow7KNb>dGSQ zsEI84svHn^XzBo}#GX3n3_hy{%sMoJ&noSX8_y{V7{1>U*2^C?GE)<w+Jd`p<Po?V zKV?TssSB(0_JGJSICIb>h^i3%4?LXMDF}HA;|AeE5xrwT7xK{4);AY2?%yIW(!Vb7 zzm0imp}gx@Hzv%brO|u}1ne;(b1Xe0;`da&C}<_-2EOG-<i!3WS?*vq*dYvPn&s@P z@w@mQR9?JWNYVY)e;FsQa#4;U3e6SIN^_l3tj4V38Y<>d$hqg(C*oAWQ&U?Rna}ru z)aPgeeN<Yl7kdk3D7qd<Q4)(Phv+xbMDE6S02nAkha!}E*AP@<8<7tNp1}=NPr#?h zroe`px~N=kN)%#44{AM%_evV{)+$cEgH7w?`P5(v%7Vc-=^&xXx<~PE22WYsoaM`6 zzg&{|^0Px<-cO3Z&~aY+p2FO4yw}~{g+-HX#*H=L?5Y{8_q3-I{r%a*wZ)n1wHE$Y z=qJgd^AN45QPko2$@%%kR8(<@rWetx)9DWvFZyaeX4!={Y>6)SN;yAN>5)c99i_ko zSEfFa{`YCDZ2_(#WLa7`?+%_F<h{>BnbxtF%jfmG{g9L}0}0cq)GLDk2@Uk3Zj|e* z-?<c3c+yM$r5Jq@q!}w4WA9Q3F!=L6EjCrQ1w>&&N;Z2r3u0sq&p_2bQq4OpLq*g? zE2W-vk9IH^{A-MHQd|iS-9qouS<2>OGejhIM{mT^yvFhTVsuWqpMIgx7IbvcfcUE{ zS!cj9@Nv~9y+sTt5vT5km81a@V9;x2@DSdAu%!_zIh%YH=eL;=wq{L$$_2(|%8J*> z%;vh@tTlKozzM6W&^*w1M(_r1_q;B}Y0M6N?A_-}8a>^hNJ|~bYKqod$fZZH8~vj{ zO@uL0!a`bao7|^e!glUj`$@RhqDvMLh80<EPKezT#IDl=592mlx01$PF5Y<<bndeU zADok--lP03+iMZK{q)a|+YcK>j($41<cLX6-=?KkB0BD<?z+?=!p(xU9g17`9*kH5 zie|~(1+4I-1hE`xZzjRVnAJ)i52@&~{DbS5_Sreiirxu3{m{uy=JgGQ4KOJ~alA9D z5$lb_D!;AXp(Fbac=ToypqNbr2N;u!TPEx|APeVriHIiP1&p=5j6aNPXjv<KtV?>g zJIi2W(a@XhfN`>Au&#UUc2((O*vJ+%7dz&k820k)>f)Ru-A-Qnyxei!=M|t$!JzEG zk)WnOFUY-LomIu(hp1>4p4zQj8AnVERQ1-uKcjF{0!<-c-xGcjgz5=pNXd-Du|Dcf z{N9Wnf+U9He-#N<F|#N>6|Tkf$20NAe)3%V_WN)RRF(8VbkAFpSJSeNC5AthzE)(U zCB9zS?;AVpwOJPBSlW|=!gkSMuS43N;@HnXUh)O+8L1Be4j`Zb;7-qhZ6L{PGKTJp zPe1$|RsqOH5Yn}9m>5<g3MepQ4T4#T7?kZtI)-*E$nSsr>{!mqLEI_#7h$)an^W}3 z7&Da)oZ=pb6}1Zfs+L=m?nYfa!eA79;p5l^2fZ0`XP(kENpM#@aPS6+0JPM}Go0T` z!%q}+2Kqe-;+l9-0?9(p##N<v^%hHufDZloj~N#JAHf8m3NJQuOYVHCaX!GIEW)co zSxy!wHsPdu8%#&O@8u6C!}$&`A4% UhfT%wy{x1`I^rDknBWyRZvxH2#l2v+3n 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˿J*b6H*jG!?_lgOZT&%FMntfw?Ej$7pV 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�f3kCq7oaBFD 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ê%COOYn+ zT_ymU_WrBRCfuAH9GEfpzlc?ugXzNK7?k7pP^r78wBvIzjg(_A(&;u%IXh@BA=}F! zR=u?in?a?3DvTI&%&XE7((^L2BQird4~5$-LE%e{+*U-dX)7G$hKG*nszh|e&Pz-0 zZ_2cq(4gXIrcb`J^)7mVJRU!X=kFyGM+Xnrw{IekyZfaFi138SPg!>42Tm3y-5#Gx zc9gx9la~sDYA$t33)5R5f00X8Iw-T`&PeA!^Bcw6&6AbV*TTer`a@`iGMaPMj;o4| z|C(ISu0`OP2#}cQBt(nEE~_=1sZfab6_J@-=eEjk2v}nPGNWKHXOy&gvfN005-v@F z8wml1b{32H&m#yYg*5|Sm0`#BC}`LrHZ|J#nTbARQJ$hIHSW+-aWL+}ckZ5;2p7o6 zi9CV^&cgfk$d2n@yQVfsAd4xE5hWiCp#nF8t?y~Plu`s?hxyi)0QjP>P;j^he~8%8 zUdEB+NnA*shS)wPEg+;+K1`XYS{evvv^)#-qa*AN#tPRz(w?|7aXD+r#rgz8|2+tj zxDX$|`6SXBNHpE;j1=g)QVYQNi+53JVp8Nz#gn-L%rpjHz=9r(gUET4lS8=RFz~pr zlAHxi3PJrFfr1bskmPHYp}h`X(<Rf)MR)A7L;L}nsz;$ovM)gAZFK{4xF~D7GuoG+ zSWEZ0o_G!*&C{l?Vi}hmR5P;BZ)nuo0(kMrU~tH5Bi|E$7Bd*hetLNUM!?SJUm#O; zwR~NH&%ZnOqAyLT7wd-Uns){=euDFhv@}R>0%F|0Gi4QZ?PD?LVgT;nSt|wlq6S|w z)*l2MudMyTe?^c9k`^Qg!cgcRkVoG;np~Ng+0nzw<~VIK$VM*&lw@qb1lZ~@#zKSc zD;AfBFd-WREPe>2D{dQ9GyCzsKnnHu_Iuu&?m>>K-F;U^WCs%jtmHtp&S{fk%^=uU zAdWpr#;Yos*-d~ORVUb}G_}uE&>L)^jkJWVL|P<*#a_Sg(|C{2s7#(|FoO?$4uf~` zCne*>D%{Rll4VOliFF0qW+Sr5l2s44!zL(Rb|V1)2#`H?oYta0nkKnWZAkzJJ^C|- zc+Ww1V*De|aEDF8iJ_J1DkCHc(n7;w9SNo4Ljqg5uE0moBbwUYm2$egq>|89xSB6c z(|20;777u==!*h{W3J#GM?RR}N1o_fkRuON&67z?upkb=!{#d>m^*B}+QdFk_)heK z($Az(1EPQqQj5svVt@JzgWe7J)&V&r2SurmdL|IRDAn%r`*_}Zh>D7D5WCFiD*|dv zfF0Y8!*v%*`cRoE#{Z>K7kw~niv1%_Lfef>6^<0Z1Us>mg$sT>bn7BBe?{R?C>=;T z#9ciAyb4&vpN-S>^0r0)27(s=fMO?Jf?$vehY*()-*G!~_%(9i>sE(_RvD_ra*%lj z3_|r`zUQC<&4t*u{${6nJV^`w3BP+kuz8Ga=k|KZsv+fU(2RlY6db+K;du@a8>+Sz zOlnz2ck=*k5CoiKtvWsypB<pYO*G<8o_R>FB6#SPeC2a_?BEPqx>*^_yAn0+)=W(o z6C}yk`r{FAa{7l#V;34u4j9)?7E|m6a{*PAUw`>_&YB<Wn%7qWC65RsM2;?AyWF+` z;N_d0PBP}nFbLWu&VlSf1d?$HVzXYDj%gTHayPtq7e927NCdNOJeja0AK$~v=jZtB zX!3y#e;I=A(K-l<Bu6ORTjk*~l}^|jwyc^;*joggqo;K>lqTen$ms<FrpRvtz&)Nb zI~-E@mWr*5nrec+v}-7tI6_1xs0tE9VG+A@$XHPoc{X2#7{59E0ZD(wlavRn0xj85 zTS$We)B|5v*Or%X9_AFHysB{+*<KY+^JBaV;RTDK!9!QsJ1>E2upGJhOEiZagLW<l zV0=6N$2Vx;p9UqC;2)(`0O8u#Uj&gef-JdQ=#18eHCfHyoic#173Vc;|Nhe*e@-bX zW*&qK2*wecK}YR#oH5!)foyl}(`IgnvnU+u+#Dzj<ERYFdd5)bdOTSatRbj*0M19> zqd#eEW_5A(?`ALqf0w|VXCsTTKqvd*txKKi(u+dik9I|I$c7?!<j`>ch6qQ(dvZ|< z21~qEYGoLs?XI{BoUvio_)j^sNv8XJk-mXS>C(o|2e8j#)!pm{67X^Gu+*XoDv^Cq zxLcNoyuwX|I>btW(4fgIr3^a1|77p2Zq+y@AA(df?c4-LD|>!m$PV-|FiN3Rx1PQC zK3oQB7xkghI%9nw9u93A!p@McqOtE7Ukl{*osHO+>Fj%Gv7vs)Z3wJ6D-Pd<`v)wH z4e{tLh5Xx;Tg{rl1@8iHy%0nH_k~I~TZDMLrlqwCgdV@-QTjN=sw>}bkI{!@5UJK` zD)y)`I3gw<&cQz;RV}kAQ^D*E>fN<n$<S9VH8qy+J9+25gq)ne%8yOC1k&M*Fb|QO z!doYeKV~5jzJw4BLq%B#xNti53<}2tkGo!+q`wTzB7rhr5!6?a=Sg%5ASBJ$EC+hm za>DZtlb`!}=477@{1^LRvufcY=S-FJ(<5#bUff3?LSAZfMI`sQMw0oilXB+KnK5;@ zy;P-N!p!XnE!`EeQoliYm`3-c@2FF96na};i*S({2t?pCCLSBpsFLrMXfe8MtL9K& zB_e-!FkILov`NLjEdPD>-)})3V%D1F;Zh1utzDm?`0e<Ri?Dm`6J;xu<+QV&K&0v_ z@UsKZYz?-mV_pUj?pg?rcCce-pIG!+N@$9eHh#GgJ8C|U{jD=Jbj^70k?IjlSAivJ z;04DbgniLcpy<CA0p7y<BUcs1zHs1wK9yBAr`yF#mLY%+328(i(?-y#vK#~?17<3M zY-jrH%5Bl>DnRJEG6Z&Ol|k(OvNi$3_ps|8>dKG<IF>D9EpOVCy<^ZDdq@xqVdU5I zPsa6+&h(4XI8h&L{;73#D_!F~23eJZp4~1Y!d>BI#4;-Le3krwXrTtM_2UbwJYadS zUCS-AzUP_1fPmu5dj=vaImv0pleUEK1bAtPUXhDR9O~JsQ0d`V7s))i1U0p-&9Zz? zh}%Q-mMK#A<T@g;9(<4W&Wvx^_HI;^$_vMaVlt8(`DkbPdE5)UU>G%MPD{!Gd~>52 z#X^Ze`KTszoW3;UP;ot90C#J&lm!5Hur^q{^3k4mZ=qYxlFhSUj)!O9(sZnir08+t z@=Lm`_?P0_u_b^gR)~io)$-dY0XQ5u$$a71!KZQJ81LF_Bx{A~^XOc0q2<U1l=+(E zjptaqXUSX2Du9^}pw$lxNw)Hu7H%q67$ba=+-?-jBCtY}J3)z(VF^{mp9XR^ST{;0 zs4LuRGB6%i60hOi`+RoNVG)a>x_X79A?Lz|#gekFA~7OZUCX!|It-qKU|<dGKN`ZJ z`H~RGSFl_ti*B<GSXdRw2oi5_0f-RUC{fh6rlQtbej;Sr>2t_Fhz*6BvraA>+99#1 z&fK6~qM2m;N@+Dch00w!cqcG~VZ*fM6BHHz!mpIPTSqVOV}rFrBSc|meT}utSVtVy zvKFwDy`*(k_78JeE50qF$5ns|rtbLi2jGaL5Nju<_3Jl@9U|E$ek>1g?6nwo0P4(@ zBZZe3NrU97<L6Qnapgvt3(Lx<jfghFUsTv&J>-8Wde|a<n{yCc?6LUnzQm_a4^7BL zgT(o7_5qy5{D;_<fSf@*b$v)tZi-sN$Ecs&RbUEUA~$a4D9siz=#i8qiPKLeG!(_6 zq_;?`@tajv5>bvg4OcoAPD_wyM%Lan>~=ct0b8#yJ>#3h!q+m<(Jq^L_*tZlXRG|} z2bcSkF)UH(wiGD4O;9@9Hz1TzIC}#dsag~Y?*&EztCjwU9S(ErImbwbmYKtYEU`+H zVEJ;2LFXupP8)KEX0g5M;f&^uoI%k;f#THnKqbYc28vURRxwBHvOmUF#nL-vm<L`{ z=E+&#@fx3+rpc7aIeDE|As(15=Es6o1m<zZ$LElWNu#^SWTkIlh+29tX9d_F=BO)m z4Kn){97SM9h)`+)iDCRZt|2VC!W_6Aq;~9Z(I363K9yUe>y0&1NrjwxrGh@<bcTNb z4uAfb6~8+e2lVZmXHWwU)Fpp)_l^n5tB0(g8xQvftL*e0T(3M|&*>@TRCny9d}L8= z6o-92o{iXhha+GMX965`sFX|t-SQ=1Qw{nBx;rdIt-1M@ec(k*X(hz-;`hv9!3FUL zYD=>J8$eo%&RWT00s3&eE%HW2Objz+$4kVQ{?oCIA?n#^37}^*3#`jX(O)e;ZI%2! zU)v47DdL;$osnAQ!-L!kPub5)F=~S4UXLE`(BFeaLTp*WkmCM~BNf(hTmb59zi$h$ zR?@bU?~>pFw!LU15feau$<XeVzR<MBxZ|F*Q^H=Rd>cWx+uA;fXop*flT~=i)qU*< zcP{M`h`G(-`*{Hv!6B;}iwb$pzhSaI%5c`kI4}cg{h^G?-jy*5QwIBw5%`A3AJbMY z+Xow%=@6an=k9A#t&W=M9OttiWbCaUCi3LZf}?&ddUviKx7htPiF1VOZkE0v+b6vQ z!ZVu4CNGi{QpX!WX7;$&%5x?eErN2LN-_Kz-!@%+H#LO374|3Aw+3wXPUV4drY~0l zOLUygzd10ZPqO-mkX`mV6!sNn%lv1S91?ftiX#$7BUxoi5E1wRv27?E1M!8*aef}d zNq|z);>-G7NjS~+IStTEVT8GhE6JlBGlrU`#fzW>0`hJr?k?J`zku3vf%>|RL~d<; zU-J`-9}F4tb-R6{T4$XIm2bjN8;2%xh}1^$O>!;e?)v#;Mt=apXf%C{oXJ$)R95-* zVnV@IS_0F_m%pP5%Sr42awAo?Y$<R>U$<gGu$|@oq0+Mgv!eU59wtwepleRx>s!iq zLn{E+>1D9mmZ}eT&sbcwZrIgCI@pHlMS0GT#<<U)78Bs=Y)q8QP2ncQqjsnIS&9+y zL(k-@R6j$@wwvVJ_;hPd;mAS3urH;#k}qU`!by=BY1DxM1$QDwKn(*u=+!JYB>n3( zwN<T3k-G^d|Agn7D{I>gHQKeUkIUg7<fk9gYAMZ&3hG@w2{o)pzz*9f>_ipXu1`X= z#+R28KMm<#gTfN?_pi<2?AQ#E6@1sWN6w;)*BgnIkvuKw&gP&%wse~}u%(^ja%}k& z=5nwN2su`WIEuC(hihaMyUh``k|w>wvZU<b1hbd4vDHy@d#Kvh(txEb;jLzhaJ1NY zDuVjypDtUrl3hk2D=05fs%&eTwPTdDE~@2hEi3*c8_yg22-Qo0zWc~J1)dSVYA~nJ zwTXcebu%gm)q!&hO~?!$h#yEpoJ=VQuZlR~GQg+%357YY)H=O&ug1Hz<6?I=!FW@T ze}M5VblmuiYqv;@E;eEHwdIgM6edmfb;f*wTL#wj0B693{Nw7^p(By?yjW!fYiSc# znI;$vLxV&QmS70nTrj*9(be=eT+tV+qi+eev!2125A=zJfi{A4Z6C(UcIL!r)d%^2 zl@ulqF-O%v`8T)uqbdZyaASN|Qb_9_6+unx%D;UTv{d#aLa3T-d_zhXiXhfq5F(?} z`+0t;(4JQa(+Ut+sT(KMjNruA8@J9j(k!6Q5YV~^S?zs2@y#zV;$aDG+bPJ*QkO_o zqrCM=G4HCSl~3A*>i-#wj@zwHN97z!os6;C1{BH(sj7}It$?|f2x_#nEILa6*j54D zG@3-$10wYGI{pW@!5<-)JNAchR4T>;E)o<QsR5Zg-z%XKKICwt+(5Qt&dvQ+4p1<3 zDJBp>mnTCuSKF%yKmjmYab~UUvTEThRv%OR(Szmd{t?UAJNoSQuHcKKld0;;2eZs- zj+4>-22giO9@;2)ez?D(_YBQC!bg~uxojBsmdM$wSi*_>F7={CWF54s6Mp<$zu530 zwsfWDCgt;>vZ*rSUfyEa{pDRZu%-IUKazP^3-)yrFu>21^DBE$wj0%|=Wzj%0m6+8 z_(==-ZhA#;A0Ek=XjLPK`0<z($JD=w0i0`QFN2A(6z*9VG=tH81-&C|y?T}pzB^{{ zS=wq10yLYL3Gxt<i^s0xP1}E$##C!*)MHtW+zZla!m?_qCNUr7f-cYlt%2^D&yx}5 z-rHG1=gpsobwP*A$hJQlj|vkiw!Gy+jy{r|^cF>6=W(41=<my}U|$!oAFNp;zo8l% z1(3>Ymh`Rd4);Eb0a9iB`}|@Yk2F1Sgv>T9xX@e)C)_*_gQVBx^6k^yf?6Jjh|fhf z5NkBjc+s=_+nQP11x#zDG?LBCOD^tH00&AF%!6Y5ykJ8`kK!Qi_Rq=i8+NGSmfM3S zQz`g|hEYx)dk?X?i8_|jc1_&ni4Kufef1g58~PQP+?rlX=fhQ9aj@hk4H~y@$7dC{ z>n9>Qa!OUKX%0P0jN+eT!Chb!sLN8RGzvt063K#hcx;9nd(EZtF^ECM>flJ!;hyw{ zz#524`avCk$RJom8J1^jyXyDJy2Zziif(?0BIKN-LNY^R8UN}Vt*E*Y&3KA}{9dad z`v@tA1s$mZzss<BR!NfOX4PAZI~rExQ)(^%ri_PVUwHAK)962x2{sC#jNX%COROqY z5Y>9166|uLva+IduBkbb#WrlsmSR;$iGw=fIr6_nl7Y)WbqdLD{jX2GJKtO$4&T>j z4~J)ORS#~bj5)DF&H}Qyf4}7p5kB?#iYHHQbbGy>o+;n<j+!QKsKuxEzWh4*Mv%>^ zoQkm3tHUap%`l=8s~7%So%~cWiAogflHWD{gdzVpmtV%gfwFH^!Z{|ZNLAYfF7n_? zZN1WvX$6}9i^$whK?m~D{?y}ya+`piem-L^3Db+w&gJ6*XAx^GPa;}W?f(vu%^SJX z?N$+13xed_BCA%dUAm&j{9v_vC!|=cI=AMK3FUr3fh)~b3GXebD9=NJIJ7GCpJ4B< z6pyDPEKL-M+3or|U$C5RQ1)C{K^$!sve1NiL~MsdKk0XEJR-2k{l*;gKSKoIWfe%e z(X?G+3`$DVeCwcwort8K;zk!iJ13*;VjwnG1Nm9kSGr<k%`J=pv>5;vr?F4!I!@oQ zllx5Lj<-~9=K_#G-;Z9pAdGG^|Ij*X0R2}s{VXI@cExg8w6@HSR3+4jryX5F`cGBG zN$fD}aN1Khh`e+K^b2t|;Y-)Xe)eU};|Ah@x^j26bGGuh@PK%QbhbO-aqAip_A_?( zfVf_l#V$=BA2F^B2u|Mll!i*CkN0SQV11(b(&`_{M(CUxJ>T<ieojj_Yn`hNw22fm z<d@MwIBG2+ktFW=J6;5vFq{vEV7jJlHeS0xLqE<&z3nBowakj4CWJ0p7JN5DL(%NZ zOY8ZQ;?R*T0QMjSNxvJV%plmAZ+)x^Ark#D?7NCCi7>ED=TOp_3;X+jp+SD*-2XdP z|37Kz|3vBStzFD4Y@O)-f1>q-(3qA#cmM#=cK<hg&;MFy;OzV#uE?+Ruhaj@RyZz} zM0`}V&z7pZR<G)QUEUUJ^i8hWQkE<U<Ts=s1OvnZ&O9dj*r~&R7>Q4GrTf%R>XV+4 zc9ZKhbN2LnZ(ZA7u2FaCmfx5zy$I{*MjgySIii1*WPU27`{*9K4mDu%dv|WzIC><z zdEYqu9pmGbH_L`=EHT<}E-RD@b%?R<3u=vNOHrO2r=o+tL9^2zu&i`h8GU3-cS1G0 zJ0=l7y0&aF*%3G3YiUuU?_1Ds9`QBzV(+D3^yK94E1tb;C)m1a?8s?36nri&q^T~w zo2({vUKQB|icW7H3-Guw1hCTHiZD72Hc|jRKN_l_J)sr-y=gvcm8C;(y_X-gF9bE` zGaE60^UhmgSEa9iX|g<3fFW3#bx6C9Ul(eLxe#yPI;_Ce52U$o?Aol>2HNr_@O*nd zTHPO>6MMUS9&*d>eLBx~6BM6pqGER7FAPNOA;RbkFrziUpF6ZK%=FS^26wi{QT^Ub zH)!?7P~GZO8muKtBIVd$OSJ8D2ekv#9oL?YX8Q-#9J}LrUWWZj1RL#NUOt`*t!NWG zH*c1r(srhX^psZ^+jqPZeHKzus;BCk>sMy<y1n<7CHS5^^;`TQ@UCr?XpeMO0HO$A zlPbcLO1mf0wmi352pyxeI8aJNsK2zGb&&b0rYH~EOjJ;<I5u5W=*jLyg-4|f(@w6w z=kKSpP(O_qi49+eFKSb2A*<u<3wjrzflh8&koj8u&8~ySgm^y-z5#me&$D_q#&Z9< zFb~@rk+;+GjS2R{PiJZcW3qtHZ#-9ZSU^3@TD9Sxk2J5fUNm;wc!Q}mhDDqR>FsP! z@M3tYu`M-T8W`P8CMETL*SJ5thxMY{{rJ9ZWvvY3N8;=C{@h({u-(w=^&p_{KMvNO zuA9Pl{aLrAN0L$nYJK0}K`24>1Bd|0!UIu<uC5hsBeGCkszMbnLR&6CEZz0s2sfdd z6o4{xvEGIjvO++hID4e+!vtoe9pa0?LiSFn8lU<Qaddbr*0lzYi1QP$2!_bE3J{sC za-@ih*4M)a=`$+!T8SER7L8NPm)q|B+{@a(FFUz0hX&e|xchWuv~(3r63PHo2D9KH zU(HAF3F?OI0-mqXOUg$%D%^l8L}8O!c-<c%%3_9etvQrL5!qT6QZSZe)x-kDFTUJ7 zkE3FFT%i}(6pI)k7)_5hRaSBCIMLoc4z1luj!9H<_(lcH&>~>>jV%(u{`{?j2VrdH zyI(H6YTtv8Vm02BGigd0`)fkQjM?;uVx)9DE?>e3G@gODc_zmbgZQXcnP6Y_s1*|f za<!~==ZTkz?+WQSb`~;UN>$sbs2?v*KAqYS?i*UqeIev!yY8<akNgj(r@`S!h=#`N zdM!NG>P->ZUlLpY9UM#;Xe&nQuo)+YI~0X)mU*n;4$;ZrTf^keLD<>n9Gq4k=s-x2 z>=fKW<srz9APi0^kA3h3d{B&V)hNi1!1Y8Mr@dQm6A1^I!tWs#JrVT+)d&A=vwb@^ zq~pWc5JrvhfZ!Tz1U%Q<p1LZiOuFYsPn8$T`iqENOOLDF#>VL&Q{H~KRdvi+LOM2= zp6f~*;w9=93i+llRaxNGdhbP=N%_K%q%IgU_3L4vF%YEy8K{l6I)%=Y{R^)4Pyfj? zQu&%mv3Q`7g@>4xB+?U`GxJo0f;%l4s6xT$1>iAe?m(?&NKYzLg>3E7az`o(E%`yH z%i03#G=E<<1IC|d`pnq7v*-{~B5N7dk_HreB$2jPaCUTVzMbL~o74}|{11B9=>bFa zA_*)k>Q)!vSOgZRb|Ka{jmNPOnQC9=NdySHe_o}#9SGBSOrn2ZA}VwL@~g7{qfQvk z2!R#Vq6(qDQh!suW7bA%851y^MQ72?=Q9%$e%w%Wa<s4Y^~=e>PWZ#=Nd_@a+ZXb; z!!-*Ww{B>pAHHMriD1RNQxth<lu`;N03V^-L`X42m`YYG0YP{LE1<ED`IyEs_DInd zETD58V(ZSdU#?RJ9gTxR(<Ez88|U}j_-Y!1Z7n2?!c)$6Y)u5^2Wohe<|5K1U-1T! zs_yR(u=h1<dV*0jKMaANpRaws1AJfS0L4J#G$+`g()bP0zj7eg^*w{#$>E`_*~kbz z0t&$eqYPp<T}t8zpuj|+h;v%3F(~3}K|ru5q8i!wO;wY6n1za4jNBMKbyKY+-+|H3 zn&T>2?-XiHdzzgh$^XGyV&0PV93+=Nr`hfp<JgNwbTiRnZ{y0+jZ+A`^ft}`YpQCr zZCt3EMB7k9LTjrvfF0pBx~e)b@_Z_5>dVJbn$ru0!!u@ejVG=bG@KfLBB@BJN3ySy zCTrMpjTkx7Ox*>zAz7VQ(H{=zht#qd_-1KjtOL|6BmoQ}ZpHftBGMQB@4R-yfEccT zy#P)9Jx;60E(Ofg<W%hxEtGc3fU?d~#2WZ;)EBh_HUhRRAyAYp-w^iPT8FBBCqC<C zOI2Bi3ll&kuecvP%X#eYtsGG9k-rXXBS5qePk{C(9RUW)RANV`D?97z7%Cwjp#8!U zO^B&f>xH#9CHf6l0F)FEsoP%}WwGFV(_oQeanH;WSnPj8*R{Iad)%(g?AkB28?315 z7kO)yYL;26&gR89j?@&Zm~OMaP)`d1*$Ei|_*Q*Y8(k^<-0P+&QWX66M%s)<OjMil zh+dX)DQBVz#1gzHj_btfNC$n)+^63Sb;dZy0QqyG`$W;!>$*K!RM@0COm=ou9w_JC zxBLnnZ@CI%0uj2k{q6vrfPcZ-EjLsNP6A3u=6a9n(<MOFX`cj*zzk<-s~pUl^86`N zf$v2FkUPg^qZZDl2v>h{!--F40^DVTLo1wy0%|O0|ME#?0(h*EZQ{7E-N<mcHm?yP z)Z19Lrs;Q5u3n&8aP>i4GOo7wRMnt=&FH%#Yu_1P8{+I8lH<8ZQ{5;mV%!;N?!BD0 z9J4J5{zPMi0IfF1aE}Y=#Hs@tiHl*yf%oZXGUt=?u)k7|nt^B1b((hC9>9Gry1a#W z?9HHPF9>)J;-olVHdrrGh?3FIbGE?<Z8A07wM%eH6YGG)P20=|nr4&Ctioy3E8=`b za-4diMU=1yj@tORcF3|2ph@;HY$A6zH*`m5iF`WoAm2S0Njvi33Wyb&h#x^ik1-6g zj9@0@&PmC5&fJF5_zB+0SvjEI8_gMXFnH-d!X9L@9!viG1h;x}h}-nkqPt8QSkVXO zQm<r@Gu;XM2Ky;ca2NFF9`1wAz9YXRRH2gb^ED7PF=$(6fC~XKBWx>Vj_=Y;vF&-9 z*26AmLDuQ{dK;^~DZCU%a5pkNWtbyIJ)=%-Dy=OuOzLZHKV;xdc`-kAHOzYY3XCja zMMq-;_yMCW*pu7puY-Jr5^QBVT7xAcUCQy~F=hS(Snfv3(xo*aovW^FJrxcgzQ&Q^ z$loipJr}rmzr#Ad0}FWp<4yOA(avCzfEtHQ_8D*nn~Eh1({$7+Dk!nW=E5Q-!wEMJ zSKx|Li>j?JAk6r5M;a*d4#@)K6?6}ihsjyiV#zlgH0=+6B?!@pfweSy-Uwnp5svel zVKv^{bwm@Dcaj_c!$oYBx}M#J-C5JQggh#5i^(?XB4Ez-s4@Q90p!HiFA)L%d@<V~ znS97DQt*53c-!EB!qgD%K<OO)AfeS@*A5;(%_rX<hUCWRf5PVA+1{;9nS3N4AOP(0 zZn43-pgkWen@FG|J6jDlh27uP1J3;W^6T<?U*%6+gcHUSYw@0C;`lzJCyP4?@&UFQ zMwTk)C~IXZTxS15A{w2f5;0nkgU_;ig-%`sJq1BKaTEql(LLzO>^E^WdnWCejKLj` z3wrFQt5OKsP=$g9p>inrch|^F>D+U}pGT%J2l>SF#32EZV;fst!lUY{TompX2Gl+f z9vKjVKp)k0VRbKCNz<{Svo+?sAdiPHbPV&sL6*g5dvOHru(A7`9+HI*)6%H7Eip`) zhtwEVyPlM8pUaSfuzAXT8H1!TS|M-&w;miv1olMlAyAQl2@XXW0VtBqOaXt8*EtRV z7oXcWzr+R{21ic|MjP?eXK%^bp^lPYxtk#BK|92D>$>E|H~AcR)CFb;N?ee&*Tzp6 zy8++AeCSSBB97|GlX9hmz)(L@M*r|onRMQT_s>n|hp-oW85X>$Tf0}6C<#5QLfq7` z6#WGj;{b(imNHi9@2I7HvyI?!O<vzYO4h$m=nc_hxpnhJ&6%?8!P31K|FdFNB)Pt$ ze0+c<^;4$qw`@=}k-x0UPm2CeF(qnhy}f99kw4ksnS)t#pY@wuu&KVyeaE~WCvpD* z7FE)KY1h3KC?$0L0*+iL;beNQ!Ro=pv5|Ka@mg_G-RKiQZRFQ^Wn5}QeT+&3eXUi7 z0)pKQg;E7-HbV$x&1_R&X#QYcK#KCHjyTiZS8E>*5FVoI1dI*jaSWBE)~wWrGLYxC zbSz%~F;SY(9bh*pfmKd)o&_1d2z91fXy6fqrKed#e;Z?ENa%79R|Qv<kXlifeir~D zM3ZUz1mJF24e|$TFm&v0f6ZEKP>p^mfCKc@m)E47v@wRyzU#D>TrVmRv4D*9UorUs zG$!i+$d@u^oxU+q&aq;lBFu_opN&VO%8kU5QgpZ}IjbXgiLW=?_o=`YjT@;nYB5{Y zri2oHLQ;*fS!r*mtob`7aG7@b$7yS=rRHm3W{8R>D~d|BgB5_0hu?#afZhNQD(4Nl zcMcH^Mx}_}N2Kz^!z83!lxzt}Lmu=O{Xsn4;rbU7xN%DyOYEyUI2QY%xobK3#KCX} z-~6`*rFOiC7<7GfbxVT!eD{_R8}2q_?>JZM7C@k`!TRuX45V3<l|I%e_8;{QmgwJx zCu0555?by*;B7MksYXL!sC1^;z^dn+Rb~$lW-9^vv)&;!I(n0?%{)jmG+eqFEjlyE zo&7Y_^{*@$qCht*>1_e~2a>JIP5H|);<6H6*kw+5$4<E_*0ddhJd!JhZ@3sSSx8kf zOVZ#8qkuyC>2TJ^-q!B*cczNY$nwE$jUpAuV=zicbRm;b6p}xOJ>7f2#F#<HMc|<q zdQ|_+Gpv`u+re!#8n%m-ov*BCXA>`6ZTcmj2kKtI5f3QCHxH;N<(&U6RohTuK@|8h z1lffVlU>;=;+er;^q*Kr-ycEz(Knls8G&<{_C6&0xNopOrKDgxBOC5a-z!mZq}&*5 zaSCSMY`Z-0@7Y+Asz`DXEci#_eCbb1y7Fb=Lh#L>u1?5PZ|=n%2o`u%PXVi?P#FKf zdx}B-p<h3wV3xhf_4^;TyGrp7GK|cyN-0!|{(O@}tcfez$Ga4atuwjlH$sb%v>!79 zCeWAqF^}g*ioJ3;@r;9hP>cmAbO4XuYNe=c9492447LX9W!vd+ujf`TWMMUJkCkQd z{firL0tw+a+dm?g<aFEE;1Z_F$7_SmP}QVhxkskn*kQ+WSRNB?4Fg}Hw%X)*aZAXl zAQi}$C_m;E4sInJovsyBkFa(KxBK@b3eK1K5fvj#*E_Ymr%T4ptpQc6v{=;dNBMDP z?vVaPq<c~f?`_s?LPS1t+j%MFyx<Pr>-i#@_IIwmqCr25RWsx^o8jNIx58~dm~=aQ z#;D9{Bx+;AsM^X8?G=938=VviNV8?ElYDsgVfrB_QX4PjvZClWpkoU1Nh)QjGbA<t z@*C)2%9Udtsa%}#HnWVmJ(dbE0oj<lMBPM_4)?<w=dhujIFqCO$VN96N{w5IAigQ# zpYy>+A3_6b>OTV8(tSmYs3OQimAuG1j`=zKDyKW{a1M&j?{C6a!Ex%R{70B*XsbDA zWiq%U?8xc0{1580D08^xm^0uNIVrh*nC|_&R(^o<QXW%wq$Y=eS%%K71_<p0QC^#N zoXk$!fsT_Q_#Ob~3g6+{3iP$A%EprnY_OX<)tm%om-duML=#0*8JH_9&zH^$-p!*0 z{ku6%x5ePYE2agx@igp6=BTE*MaY*1^LeSs1xlg#R3Gz5yqKr#$bKdVx7jg5VNlPT zY36h6;1bs{5kgUcCVj=CPbYKK)|iATI0mfC;Uem{G{Zhu4zw{#n7dh){oPLRW~J&A z5M)+~@+>J?AzYh#Xm+`Ksc3o*G}WVlO0@NJfscpu%U_u|D6hORal!GT*vF49E4f<y za2L#(v@yoIxNH-0qq#HX-}L%Demccz2!~)>0Lyk<mJ~lDI3WaFUU+#fKIM7n-XpSk z9G2ORpW7lO*d4}W?8<#RBT<FOv>5S?PR)NrG1HAHK(hBjeQ=n(m!ri`dIHNn9<wxF z4bfTz)*6r;ywzI&Zbm|bDK(`>?VbjF!vb6A?lNod!W5=kvUf&cp^YUy$idh80$`{1 z>iZ)X=O^N|l~>PcNCzV>Z|sRK(wFY@xf@x^FBm`0Rk?t!Rr9C2C+j0E7uV+%Z2F<4 zwfV}47!=RsH1^5`A`)F+5+P#qtz=Q_ewYui+D92eUuJd&+|LUxSx=>+><@W>xq=Vl z7Fv#^jB$Uoth_e=IcvU#k883o%Py>(&qe0jWHG<FZ{i%n$W@lXxIVPgb5|6GMy%UN z*nOE2`)iQx4pFl+pE}c%Duep&{YuztcNmJ~ba5N?y)>ay@O&lj+uC6CgWv=9AXS|y zhD);^Git@#DoNwBfOoRV*C6Mto|e@vE@Cvov3*lcF%Z=e|DtcGN9*AJz_FsZ7aAmP z{+y<YdS+R!>djii=g_XQvSXhi_nQlX<5U;$0N}l5*?iiHoiaodQQpD{E&3}b04~k* z0uJwkuG^>rP2N`9XM4{kySmW(pg*NE_YB!NkBh9UA=>CxD1~=Pn2&oa-?dO=+e$f* znnm`41=PvOyzk=tTYN{g#Y8*B+yp!`G^FV1*rzs%0k#pII?kC^TZ>-GpGLxhnRCyi z{!_Sac%}kG@<6UePh9LN)AHGo4!H)|R@$D>cS0J#0>X0|39{&?PZ}PZrywc10$U>? zIk+5jqLrgZy_LHFIl5SZX4k(q$&#MxPaM)Np)L^Ckn<yf%i7`nus@B{f~0m5q-x1v z4N*1VlASfnXNc<Ggeu!k!u9~7{EgvQ#Ra^9Gtt@6ksC4TusikGtStLnqD9wbsR<&Y zW`qMaR)#>Ua5j9Ro@2s!ZqwH5z#{~gqZt4Y7q4XFbrbGMf#GPI*q?MZHYGE0zGHnH zT6S||0|#YreeKfT#!(gfb~=!1l$X3WUA7AQ%iL*yoHlVxjA+m>;5=9jxA7tMZ|4J1 zDM1Hk2mYcTO-#W~$bcfKY({}FuLPttvhG)<Us*CfCKyd7ERepqnLReX;%<zS9<8`J zT}?6eeqD#pn}(656}dxcp(@K2jeN%gK4D{;t#gdPn{<sk;+k}S-Y@v*#5v^B+Vfb| zg65bVQs$Y&Pfxu6WWm2TB*u^MCpY<Gz8-rnnopnIe-L^U_`t(x88Kopuip^Hwfswt zv}*J-#BL><es;=q6(AH=jOWNc0T3Qz&<i(ToiKo%+sir})E0IgLDva~uyp-|%7xNw z&C~Afc@us;l!JO*yp7&?rYXw?1|Ii!L<aqKeUiaBrNys1q+w#3{^nLayCS`5G!2Ju zwuTZd`p-W}<MSAQjFp^>4pOH_74R(U6oYm(14h<agK*$oaZbbH{tQZP7QgpLw4p86 z05uJsBgHx=C_bJezdyXCx!-raxrmm1?p71alVus1O@2K3Iz@?0)0kjjCRBK8KF{HW z<sWY$Q+UsDG85E(U&r^IH+p_nLBk^EP5X{b^<(R_E9&J77OP$=v;meMUY-}KNflL7 z*fDmADZHyfT<2Cuyx7lGx1T{zp<`V0-FEj%Li9dZ#dqL%F$w$;Ob0gcD(F0WAg@4a zxrtG^^@T$Hk)`BR8@pg!x|^rP&3{-5&UyJ)%e%Jr;k2Zwz1E(WP4MtL@!lyQx^1RI z`Rrx%O#3{`iuo33ASy0CVuuXKciOxocY1xpROyi;7(~lh;_^m!N^3D7WjV!YOOVgf zBb(Wo;^4J^48*srboS>`D=WqKVmv|lpIRh`Pg#)`5)<>OZVO+NU4Cu25i4dQ_NXZ% z%v{;cBSE*6vn`<UB~f=&5A;%;{2Q#1r*%x5KRDS<nRx;y#qhe!T;RLSY<?bVm+IgF z<qR!Fy{3C5`@pe{B>dnT)J`X?a2;D_G%#25G^=shCh?7NM;XK8zCBGH*;~dGWvA{} zG)A{kO_yDr@ess%M$%S2%pxzPkYfpufucFVhu5P;wYpF4E&5f;^R`I(G~(ZEXfqwV z%OzQenLzO2Cb{=T@_yj7We<p&=1lnidWnsOG!nm{W@tsiiMro(W{uf-*=KuWY-w}I z423d#aj;Pa8YX=nm|r!p#NHTlHU<U;`^~NK^3s9%|KzFNUf9lizWRp^<lrgc%A(>F zUw3B&#)s_-_Ya5U2l}`*YOhTi%H9NoV19!$_hWnS7f*h~XfK-#k^|W6?FD!k8-@6U zx?B)=y*r-ibci0~pQ_$dT-CTMhwGCdba2!z5_u5jvT$9auDN=Qf4^v}a&S(XDR!C4 z=6ZeLZKgENoou<N!XECnx|E3^u}$Otv(5WXKj%1*r@I$9+FFZLcM-!(-N1_i$n55- zKq~2=;r?(wH2J&5m;n+w-JOp-rAdy94(0A|oEy(z@Ta}scEMN!C42z7?6XpEVmq|X zNmxuVd<46%*C-lpcXrw^!S!jO7LDJDMADs}Qix4fPvNaO0$S(cXrZUpB>~HcMd}YL zZkHASZcOFlKJ&cmk&>1AyAbIur}AD2UW!WpvCcjxD;Epd@LuS4c8ub)jDwAcX@pm6 znE@``yoa!176erJkUUF{pukaTuv@ViLuODexHK44+#`IpdpK?#RotHR8i*oNSR7B} zts45ri}^0B)0JHcyOXU@#W!NYhqMr{08mYr6IGJK{DYR3P2GX$b$T!FmgRvw%4LJJ z*j~!m8ZqF=_I5GF6vI*BSVKd5W`F1QJs?l}+mLXcE+*)pqeB6@U_|ms8pY;|bBa5w zhpz<}q*I8!5iLR}t`DRj`j+sgC21;t-9}26&}5G)Ma-YJqm)*Kb2J?eC9{>&Ak)FJ z2^kRa#E=!KFxem`h#rI_Z=fI1ngz1&x2B9f>XfkQ3e3%2)fs<9pj&nH>4Ed8J*BmU z3gHzf%-0<Tg`cEs`$APtkm#1PInWQx-z-_}aJJ>qBuHJLR3@3QC5(~zqx9BFM$f(A z4nB)Zgfnh9sDulP!Erwhop;jlMvaea)J)g8#=1{?CqLi8k12C+#s-fmDu%8jQ=lEX zFk|;a(Sh#EjgM9qYn<F`v`Alx>dc)$_vtD#Sden}_Waz#t+49#iZG~4*0wMZ>D}ps zXZN4!J11=JQHT_-j8aY~ubqyn^Ci6B?~aQ%SkU*l+N728jZv}_c6(Q22htUw&Zjf3 zPlkR7;)h6LeX(VDp+0AOso(yFmv;zEBR&9tql%y_hpYh(Zo_-7lpk4{p&Q}U<lk~C z23I8HMMhY7=rB134f7GBO81T>oQ+SK4Jx#txTS%s0B-qurkG9@r{HB0!>;t`7FHc{ zW3nt{A`{al+~<Z`LNp}14Gjx*U)Bg1MNc?YYXjq}mOTH6TwY}o?!Uep7fy0-t$c)9 zxqOZSXY@A>^{pCbLHwt~T9@94Qv=6xSTl4aX;?uyZI1bk{JqJ;cq)aO=q6T|ZxcE` zZ=ML5oC_+P(0^QcWZMlTbnjUjPK^|F%twRbd-}_phXz$`R&x35UhwEyn?31$r5-hA z?@pS)s^%B}fWic}4It7?rx3~loIUMLcX`miWU!Zf3bs*BxcW_Cc|;N>9_A#Y?MD6t zLOP`M+uHj_!*bNmP2kPsDS8V=JUBV2owswb#62BDjqwAm>TVaB;xXr7&9^)ya^Hc1 zp)I>>)fY<LZEQGoIReXXS`Rk)z8rGq6^=1T{Nww2$@?BF;cC&e<=DllU9aZopko@c zx_6Z8($^rWU=Pp@bQDt*ElAEr=B1^3HkT)cHa8^M7*ZFw*4(f7Vj|4VcTUM@T(f7u z2F=)P;VOYeIA51e+*bFq4SBxJu#LQOh&qF|c%?Xz?a(rtYGE|(qd=5mJ3PZ?j$nD+ z=B6i|RjN1@mSf2elX%8H6KPI$7RO;Cd+X;jx$kRwi|C~R;?ejDjjIuWqzmFRMlsj9 zp;d?z1^<SKnZ=;&>g^QVEL+Ok{#<cq6d`ARlM^iAUwa$&v)jAfX(8+-v;6lj<t(`` zp{KQC(J601v-c70KBcrT?Qe1X(qx1P)i(To#`(bXzd=66ZW$?m~~sEQFXIik!} zj9clNF?_p;dp}!^a#>Xjc;TzEclY9OZ0+@IsQ%TkbbFltUG>w`@Vo6<%FkfuWjx$x z@aADBD{to^O6Jq7rE8l&$;p1a857Uje`VSgU@X~lz<*EruiMT4?r-~F8FM2$8=L?1 ze=n(T*dG2x^tq`c;D)C$@Cv4mF9YKYHyhEFuD@Uy*#?0wBCIE!Ad`;JOlaIfKHO1K zL7_{eW8%-9_usHQuUD_0tRTt~Xq&?GTH9h=d-klqRI|7Y^*O6q#8tz7_5z2mQVAM+ zUd4}+T^Vq9ZZ*8+_<EP}cssHj^!;#l?J+1DyjqQf8}gR#=-8=`)2?VL^Ytemj(H== zgdN-&z(D9RQ1knI-kYpDdb>f#=gZd3Nm=pJ&egFIe|%e23GNM!P-_J(&=WZb*1_Ef zQeFAX9|EOlG59&8|78_*$~87-spOh(Ro$t&>e|$eh;3=yfiTtyPjXRJDXl)-Wns`l zeq(OK!PW^AjS(bL(TziBsn+u-7hbw&3HH}ZN%>^wuiQdae-eO|c+i}~IuXCZcIM*- zWegLvC+-MaoL$jSFT;1B2VtjQA&B9<BA{6pE0*9(+ZUw>c%iVn-p?o`2bGHub4B?2 zSrB)?Ctlf88<d8W|B1nsB<+{h#vIvhko_5NyZo&4d&Bh8arD0keL4^rkId2al`Uqy zx47(<-h6HMMjEti0TRKWm0vEL2X6DEfY+=_lyf+ej>wzfwaMdP>KfLu#x%zW<7dWC z*<wV2cs9EE5slLl{59<HMs>yEG`#_dVd^EbdgLz>C^Hmss=X;TaO<i<eSOcr!u{~_ zHx^MigxMxF3ek)Bo(z>B<}&h7i-2iWf(gW)g3M!jW2N9@-x_0f3hEnzl`=b6X7g?1 zkU*FSH*5TrgGaN(Jgsy{d28$iPH`RYLg3@jpV~Fhn^j=7B7&yDxLvR+OtW<RsW+n( z_mpaj6|g{rFBaFQ&DBl8dd>!}wv~Drf#AUR<83-bo+d155upcNG#-*v0xuK9Y;gK9 ze(vDWkZUADrQO8vP^pv}x$m#Mq`$+i%&E8_op}5Tt4AEgsT9kg>4itE#H2aU_Zd?m zQs{%Xc2BJbMKk?f5pG4;<90pFo5El_y@z6mx?!6P-Q~u>!n<jdT~^x-R@0WS6!a@a z`bCJ$#79yjNH{`I!N2(Vo=S^T9GN1)tqkFTtXLjL7dZ^wHAMFfyQn`kHx1VwNA7Wp zP{CFri!&0LhdSoauCHCTfBLRp<$LR#V;JpuPl<BV`+s{HI+z5R0(n5L%L6G`jpU5M zsR4ygf(;MHfD?p6EJiea>A;j;>4!=bBLSYE%>v{h^OQiqvWv*UOE>`%qz=aEmmOhF zDUv5T^|-XCxu;j_qbaYVOt|vENw276OB54Nv%2m%X(P3ttz9$q>8RLKCe?KxJf6Fa zega<sxsu}g&UX_bwWb!FO}mevKo`50jo+p}@?H!w<#&{*V$MPg?dsYjjfetz_P+Tk zJDmmh!X^h3EPYswcu{T;=_bkMz}Ax>sLRdr+0qq|0)xfpR0@Of8CZnDo$_%aFS9On zq=Rq<GGQNjWpdNOG1usptb&>&JqtNejEkG~;zda|u1uX;>E=upty`7jlP3s>J^=}t z*bw^fJ7~2GZk)gPnCZ+DX;@C#a?hW$Lj1J(|JpKk{W5wNqWZSOSLKos63{Y5%sVqd zXIqL9WkU#5S!VXjTx{~W-S!jOr)7_@UQUrNlKczh0}cufL}KGy|J2YzrW*ufP`x&K z_5$QglwbHzn&@5$k7aX34yzHGZ{zxQ!%0|oN#*M0AyD^1%hd&S-?@Dg`l0bSTrg02 zwtsEEk=8<rMhd+6VcXb&Vnv;#i_~qgV0p-A0O6}C0rI8+-~JG2QULa0i-94&6FiwO zB3delF{{W;^4k#=43_DFjR&yLL<oT5H9M<(!u<mh7my$IKh-+e0Ay;9xEq&im?~oD z&JQ#lVuIAlFoO0RWn+8ArGlFR8*(0?DVi7xXh0wwQMoyj?ylz1N2kyE1J}We_7BfM z@wysS8~T07EdOK8fJx}L!`4Fo52u1`QvHd@m6h7YiirnCNnld(pUHus6%Ptsu;bv{ z!CGRigAOZme<Q(m`nxuSx7;sbrEVA?0SyUC7+%2#wWQZC(|71$Xx$<@3SdLtU{5g{ z@o_E+j?H<{k$2_cqC!YqZzf6zBH@)`P8sSlBzZNu4o)utV_u=8<v&=})D0mNhFj0- z>#C8%fH%Az8xEG@ZcVf#TIvzTQ!uHCTbmUHRuJPj2g`KmkEC8+yS}J`AfaXXQ>DIu z7OOh=CT!p5<dZKRFja;VW)nyB7=ca>G)bT)+20Y_NLz=N-BOLWALOmH%eT1m*|(Ce zfRQt~;<P>5c{UA8ev3tw8Hq}M96CJ1L&dcl`yOQ}`N-zZ7&NlXX!GWY%a8)qu5`k3 z%_O=0k$3foZ_|GdLir0D1GL$}wuI@Q;1Q>|o}s6}c~ekvx=(q>4w3|=oB3Eo?6z@a zc%!LEX}m}n$Sr@6nrhJG;kC72O|Cl-a~ch#-Z%E{IN}odM``!k{s?GO{H4s=ZUb~n zb>=&ApV(rwpKQD4eGI*3{*)n(AB{sXP#dik8iY$Ukek%%fk_+Z$=_8W^~?&nXn?_N zUV190&`6n&Goji`-`cu9E!3ZguK)!_tom>3dd%mYHbE!=fVe;ZUxM!JVQ=z3-dfX| zJ9e9_NWMG0fven?l;qH9&(r?UAU&lJv#g@~UiX+lYWvqn%aNRkjorWf;yY9EiRF6F zBMx<2VMA0&gL`Z{-b}?^+O&0%p5#)ds8!7!Y3=&lla|vp`p@c+gRPQGMR0*C+FFB) zscYT(86QUiM=pekM~?04)-9a7I2zaK+{%(#&rQs%rg@Z9D&tYZ`>-GK=)(zLZO0*- zk_k*2SXAz@rfJ5z_SLZxN7?tPi6k0@{|8^^6eDWXF6p*=w{6?DZQHhO+wR@AZQHhO z+n7CnCMR>wBy;!OtgA0sc&nbOpfBC=EE7dDmwuN=EqgDe@E{4)9|3)X`AlokG)lA* zuPki$5H1T}SUn}@h_npxYDpO#$Qa-OiUIVVpy^KzfVqH8XZndHEbs(85}6A;&>&Bu z6=<;B*W-K4oRO<UG|;f@c00UoC5BIxlpl@P+fo|of2k%?eHl54<(R!C3qb4)oeNMc z2PId*GE^ToU9n)a<rB{STM<irXlS*`*0iWm+SVG8uB>8IFc}(;k~xhk0Dg|`XzST? zu|{}OR8Og9hA8!ocpEs`Nws}>vjZy~#I6eBjK$6LDU?pe$V0iQh7+)Wv6c#&t}abp z?#^^gfU}998l5{*YXSHFs=9rbUKtKeW}=t-2X7NcfIsxO*z}ns#^LIYv&lnJGd77} z`)4hcCI^7j5H*d5k5W&_2|Fby8FM^)*Wl2szkRS76wnc-K;={!I@@Ce#jD+$e({8B z4T@`ln&Ps;kk}IhG`k6lASNdPo_n4IwkH_fh*33WXyn+o!jmtzH&jmrEVE(UE+3}C z2Ij|hWYRoaZOm()5rF-37-AQo+SF<MfF-1R<4e<if#eXBw5t8b_v#6nc+wG@*7(t( z@wC=teEi?u{qrUD`?k!HH*9w4YbpDi0R_C*plLQjR@#>cfmQYMwr3Nxy783i%%~Kj zjAvm9I&|AB;)9{93m#Fc4ItKryY>Xl4{)cLt}?Wd3T|DTw<nAYDH9}gi^^8#ZG?No zt`WD;)zeW5wBfd;0kn|nqo7}CO)GIDQIF>7@5h*+O<);rK?{i@<2Lv`O4B;tPi>G- zU`HDA8EdO2lp!_rNsz&VqfL>LvOD~;&G)iWQ}`gEt&3>Zct#6AGabs<tA}xmwwcQQ z`5^KTFzD7pC(H<#BJkaB{T=)4kW;g}&`7HH7=&inQ-9vZ9DYYQ?ov(qK%;aONILUt z8SdtGT7VmPRxMCs9pntjUYI+m5~sWLV)d$OnEaLYmiDaTmg+d30brc4x&UDFNA1?c zjJXE-byQ;3gX{~#{OxJf;qfoD;#IfQ55jJ9wQ*cV$_2@)M^YyQgFxW2b%khi@^2&y z(Bsp01vW<YVR|}3z%RQ&({?^Wz7U(Tw8AsGE26LqPg*b2c<+dKz5N?{%>XRh3uR|i z!fayn0mqt)Z^4VPHe=Gd)+k_+Ir@-#{Z%G9WuLKHY=&BBvl8`<EarXZN&#YkyWIzU zgMDy(?j5Bs&qR2j$MVQqtgBgH6_-ocMJ3V}449FSyBPI0X^#a4iY$iAY&uD)>;d^) z+7;sJ_J)&Ef)VL1WP(96+j(ooCLa}^@qJ|~{)y?>N+f6HGr>L!yykn-n=t<K#csCi z$X@!-x~5)%nP_ld<{#m9?ylegS#@!$1aE!lx^=~^7l=OW?u?W>N#+cgu`Z@HQ`!^} zhA)j>NR{R=l}^)eoK*VCc*bX=jX*BDZ#Y;irfbnE=y8gm_kHoPZXBJ9?Tjm~^!h6< zbo1w;SnXP3xB_|{q@AxWv%<)#sCc0Q68W6-EDYV20Rf{KX0Y>re_)Qnzyt9*UH?>q z;97S5_yK(FaB}u$N6B1`Etly`I%*p%QTB_kQlQvBIQvMh0+U>4maRj9UG@o4;%F$G z%07D!OmnF`Z&2CJ9OVfGBA^up!hN)uf0W95SL{8Z%D<1Z???|*UpS+DFd?e!%7hzs zodh53J6q0VflBfWJb9PdEY;233M@hsv%H8W!LCQ201C%11D_8PUe1&8K|HvNI(u$8 zk$IFlD0Mq1d4JMuHA%W$@8yAF!)9Atk6dfdiR#nrl`&&Xq4EmJZ;cDkY^8hnjz;|n zG+i}j@+X2!=~LZ@OtPAeD<=mjSb%2|e<6#bmD%__xAlp;@?yTi8l$v5LTC-yVNoHM z%b89%N6yHrgi{_|q99FF(xLie%1}jL2yL00%Y^XElw&@NQv<ImbX~k)0j3ya;*`BU zDxmh0Ps<eiu9)I}kZV3l_kFZ;t-Y=@htW>O@!XXBc-@?}$pHOj4j7htF>je86Fj%Q zdo99lXQBzjfgqPI&bjhlw%F4B9>f1_EyK(VGNV1^2v;Woz^jYK>M~ipy$>@$a^?Jc z1dXAY!-EECJ~iiC%K_c+*SFNI2mM26$IsJQUAHi2&2zE$3%hWuX~oMIE{_?}87|DL z+iK=bkj_gn$|~)dz|`ssQuCABG=>l1q|1L73g-2LO{4kdfZw=aSGy7L$aHE)gs9r~ zWDo5853|Sbd+F1~B3g$Mzj-J0go_caEqso74R^_S|5YMQ+OQ`x5ML#OXf^up5BPor z_s4hKIaP`Z(rZ6tDTz*QzmqAVn@Fc^{hu1^&1uo^(E-r}U^b`sb-U5BblISf<B#uC z2Hh73W*h#$<^X9oC*1E4F#UflAbIk{eqFa9Jq5C(Gk9j61{Qp~p$BBEd_BJ?!gogi z8Sgo{`(~xu@JF>Fp!=e|pDlsN@m>t&mV@+yw=2Jnm+|2(6;tJl6Pup#ZLInmGrbcb zz)KGuxG1lrMBGp!Z?;rD1F?1nnhiTWT|JVgEY@?iP$CVjdhY=o*5g;Cawe-5-Q&UU z#i?YRIfcYFRtAi4oA|P`*S(;!=3+5(GQ3VFAp`EN?)R~*wjontv2RG>4g_+zy}pKn zuyXvmiM~}YlckwTO*hcmFG;lC2ChSB*8CYx<RsfyFT60xH)UWFA37H`zJkl`h+K@o zR*FSGmm%=bU)+0W&w)SnM83`oZaZV{s@UAp-YOdoy0Bu@w4A+w9+t320__j7ojhit zGY-GSyZ|9iZ@@chvN_y7-NC;`c^lwwp1F>wx_Ls2Lsr!9*ebvnH}3_!2phTnm{f3< zfTt|xJ9G@1Te#k1!_Mw*H~0bc{PbVHNgAkHX*%%~2W<I1_`i~&IcxsVRi2QUd#=eI zXgp{s<DhFsmG=4CRj2;_*Q<EQ1It|fH9_unn?{~HC-I?`@%Qp28^!lbSr1k(fggFq zrPojHK+jjUO!0QZ{VNvv7C=#VVVnHRFFHe^c?RUi+o00?0G`=RL@4lXsX5xqiOqp! z?a|>)j8sO(OGge@*0uQaFq%VdDwxu5*&>5Y*;I;f7g0hOapu;5_J!PO)WlO{lM9*# zXG*FoO6wzLp#KlY`<BD|(BWN@+tLq?5Iyab>()8@DvRHn6eG^ccjxK5C+R%dVfgF# zZhF=%#aQ|JdnN~<foDqe+b2o?@Bc_C>V8J@$3g%AIOhNW!1|xShOvROfswU=lhc2$ zp3PWR|H6tMI{k(TMG4AP(y9Io1B$9FnJ0E)XM!UVg#{VFBeyrGD`}?`6_AVkeoh<Z zZWUi|?L!c!wly|#H9qdlCy~kRu!L=PpbWO45A`VJUP!(a@Lj!DU=4}76i|j1SV#7x z`MeyBCp9+8l)4(SUthkcE^!5V>81@Terdkm{enaO>fpS@Xh*hw=BwJwzd1Sn<0@?6 zZoQbkO=9R_nz*jzG%D@UJe_?>_u9RGU%#$9D@lCZUtb0lyL^8RNW8CY;kecZGShtY zc3a-w2kU}vdy!=x4<3887QXb9x%wug0&0!pF6oB(yp|t!c9FRx`VP9c+BEQFJDV+Q zB3+1f`*Qs0+cz)eO1r(6ZI=|isX2e3@X$v)OURE^SyHax->moNTg*bO4txb>c+;>t zyWt$wnuK(8fuv;hj7Lu&p+q}j@nhhhHOgJNmP5BFD=2rz4Js>&$Lx;d)f$v_7|Lk% zsFsgHjxp)~-6iLj^o(_zRC>z!@Lo8uQ?$W0N7!D(<JXGamtTbDQ}51WEJ=D7tpOsh z%y9c;{JpPD#Kw50>VC-9O@!3`Q42HF^;9>;zEyuKs5>ZEgUrlM4=kxf=)m(?2N0JV zzjh{62i!2mt}?z8iDdnCBKX23n7A0VVs-nt{qE!lW)!@SLlTzZ=zM)L^nAL!b9>vp zzk%du9!}L7A+Dn7!%xBfWu_)q!3A~Yfl{?2c<8o1$DexJpPZXCYJk~k#?L{|!|A+r z+!*lYx52wNMJV+Bl%FXMZWe%#uOWw#a&(q!sW*TzK<cJda9nK+0VLTb|6s!Xgc6`0 zoXZiDpt%AmrV}XiaepEGVQ<i1^N|2xZ~=?#4bjI<0XXr2?vK|*ALZKzScDe(BES~T zGzd4|$T`oe3Y|uX9AitJsLx9p%(uGcCkNygjd3*_o+K}_>IujAqq$j;&!1C!6)UO; zU~PbM`>&UC|4e6FmC8m}q&vcc^slaFqg5#_kfVcBs-pZy_*fjKf2{Yp`)PiC=$HW` z*haq4G03C-Y24~NQJzg`<uq+pT}aVvmN5qYD=Fy20ZQLPB|_aOFYr}2CSc(Z2Kg5^ zrfE47z?Th3U^G3z8;4WX6Y+_h>GP~{#2T0Sj+bp;gu(ig6nsuh$6XiB#xV4(6YA&3 zfk3SYP!Kq0l+6AV4($P$$p9b!*$=bc8Ua4E7-{!%7U}h}@649^+zhJGJwW0Kdr#nR z>cOU+!wO-`qS`;t1N`@uaa!Gyah3>&cMtmKEd^H^Fg<4ceq$+%DJ<E|zI1pC?d#_W zF@8KCIGG5QRJeBJ&CyBXPSB?kh`l+%fw?Ta2`IJ6fhXuhP&~X34ktxl(@OUmHl}(& zm3n*1adwrkLZDW)H!-G<n>jt0`EV!N?GPN_7tLeqMNz1Dv+?FDx%dx1m|4QUROzND zu_rM2!p@lE*mgJ>qJCu;2EWu8ulLL_I4CaInhYzvh6=#BKD_{b<VEoPtZ1MP%C{;` zreqa~{ohbwNb58e)IB4LhFW^w5gL5CE!6><Go6M)aa%<u8eos<C)a}e`2pH2Cf|_| zmXi#NMpwNrw${@6r91c*7jTXTCZ_k#ReYc`W=wdhbst#LoH}GW=sN);Yj{E5gzMG3 zMJ7Y<4d#Ziu8q8=p!B*jcd}05;&l`<WTZ%Ta46CBmk2>DJ{Si1A?{@O_kxQ+$NuPS z_HlxsI}Lb`G99ty?S4lnWORH>^)rYM^`?R_d-SKq-pW_fu4k0TEliT+#&Mlm6AkRX z2&QQNxH?MnM+s#Ykqv%KAg9g61))EEG}H2zyZo>r5fE1eqn>L6#DE&}0G+{KmFROV z{Chk2N%YHsLi}B$2g&ahTykS~ER%b%eQF6%Q)Z^?7&aJ5<aM}5D_;7=uh$y^J7?S! zY~upW;)ZTln!y|Aisc@XR%!E6t*+c)$uE^wE#SJ*ZRvHyEADM&N2H<?EdgD76UK&z z;tmck^tS>w5sy>ia}-ByUpNpN=!2lvv8i$ghQr+&(bEh!)1fWGEElD7>n2u8ZIbk} zpL0Kf*e9F^tf)wET?g<0EuFkeu;${qrx=r0)J?^0n!J5rj*uCgdG`-59_dRCXG!@% zMa;)s43nmJzYvrMqeo6YjZGlIoe#upL%zUV*vt4Q+OH6h8gDO`4HZLiQum9BUu}VW zcW}WHWvEL!SO9+@=A`p)erh~$%1(VsF*(><@R0~vuCG`{wx`V6e)93=wDV^9y{*v% zPVR=I1!<yDxhd}j&iAuVo%!*3%idtiaVz$#zn8>6|L~E#IMe!8->MU8oJk-(bb-1z zjEK=c9Q^y&pCB@<!Mew+0iim9spr%{Z@DDt@%VmkPYj9L9wuqOt?+}7PoKv;4OcH> zU5rDX!+9QU-{t2y8eTAsbC+@rP}j8h?Xn9Nk9JcEW=&S{DGabtZzyOz))I`mUnkU_ zBz&tp9ro8sb+!go)nt<llyi~yOPT-3^-1!oHxm#fj$ZY<`+13Hcg9qMq`Lqhxrv#p z-3$JCL|bbu??9bML}s1qA-3)nZHFQFAoNZ`6SUnoP9B={^6rv+w0nZB(?u2^3vD>t z80@a3cINooIwB1@IRmF#Qg^}hV77^7C>I*AKfpmn2&)Q@W9E+9-RDJLgr~YsM|TJy zVE(}1!e;?>stdDL6~x4339JBrp{2EJyAiKtvi8_P;z8_{k5PC@4J-bZQy=JV6e<H= z+a|Ko{)w(6)pZqrC0=#FIU>~oa;I~<NGihIQF_>Ki`;`oxtvh=wqOGZ!xxuTa@NKh z`}+E_N>uIj&0>@QG0v)uGF(tCNsPfeUu$QpPYAxEjycrs%I_p-?w6umXLzsGkHbr# zF}L`WZrbl^ov5f7f_w-qel>Z`Udk578fer(qz<JSmMz84kF5Ds3y!Cpz3>U9yX;d< zCw;=BBOijMnw$!Ma5Z<KoQR`?NyIx{TELO#R%G#2-TNaoHTT5o9@zPHKy#<1ld0EE zl(PedH|3Sc*EFbZS@6_mJ}w)hc!Z0+iW>*pI0PQ=+7K*+HRtk#4E_k@4wg>F1t^<q zF-Go)*hjO(P&yoenM5<T-Wg4i-3{sdj3z1q%0(Atq+(nyobAHGG0_5MEEe`c>g0Nd zsGtr=5HMP<{`mN<;UIugL)D8&JCwQGBXXkW@Re+W>qw1Y-r!?%S3T73uHojMtA&|~ z&11D*_JVrr=VDOD?cC<aD*9(vnFL<&-GP9<3{bJm^^kV4B;Af|yZK`6#FR3eK;4h) zGYGrpGrtoOCQm`XN<g-ZmRRw6oC_HBTynf8J0WGBWt~V(j>n5ja4~ly|4sTByH3Dg zCZD<tc-z2M0HmS*TEvOPudCai=csFpFu|H)uImMVR<x7y4G7Kzv<=w$6BpW8a#~uu z{4aohWhTJfMIp99IWl8VAQ!EXOJ4)af+`$=#&nBcpS;C@4{-A}&*hW)MxVU?KwjnK zl?{0xn6n19&S47LR66zaK?+ktaF;e6rZ`eRbS;+1@WV5t%%XgH)TQ*7lx0m5;-ycd z)Km#r8o!<7ocT=P1{2{f$~Mkhf#UT`3^smM1guUb^u`LHg#$KT$3L(5C=WEGEtV+0 zAq`QN7PXnIa`D>A$8Gc9m=*BfZuH5rcKv{QEBF;G?UggQJJPljy!kNk`Itp_M(+$o z7<J`QBv+ZD;3>AO6SD=ZmMtFnh<Oc`$WhTw$eQx<C+zHG2`4cd#G${`3iKNwFa3u0 zG8pHSW_8$lC>sj^{}ebtK_-LL_~3ZLsZB!oPKU0YRw_f;U`ssz!|Ah<RGE~*H9Ang z6$&rgtwt}Y6Z*@s@Mz(YWeF$ya?lMxwm7LwWrNr@44EuuZ~-$xVNXs+*c1JHLt933 z^{O;yBL+M>gZl9GqYZRJ22M1(H^aCu<yv|*5xf|%N%3k^aIC+wKcawrYYf(4^lBk# z*k_Er6u)8}sJQylzHN^g?dW2^m)R`tCQQk8k%)-(ix9?D<>*8X{^w$n=v(Mq7rR!b zg+Y<qF=T2ri&O+Z1U$sR6k;cCtHjagmj|+nMQar=<pQUxKm7-*COm7KSb0yvODYc( z3=l-wAco71fQn*&Yl>{?>d>T|f({hLVf+oLVs8d*iA5^WJWWJYWZu+ydUhvUGDj1% zwL=7use-6#G}(1DWLT`#>06X4$4z7ABWg&oSnm3NqEWyZ6iUH(w{rU7%T-~$%c6s# zV<y?6^5TC(If3w?`k=!V-yxnodVGIZgcSM5fcl(YdsXm7adQfT>wH+->IP3h0=>J# zz`Ra9yMDH)=2*JA0*d>eT}Eqgfr{#)e(GYs9@_U}|4_|{LAHU(uXpsJ24`1Xz&>-H z@#FqOlybN28`KPs=Ery8T*Ne%KkU7*c0zHFVUQyS=7prtt*p)Qy=4Mo5I$0uuP15R z77RHrxYy4rMUrUkLn@S3clEfxd3&JmB~|-bipwo{I3tH2L~j=I>#OM|s(D`|3pAh0 zUW2S+VdOxPr1`F?P2zl#@&~X=gO3$-*iE+@XyhJ^#ZI`}bkRGh7x}Om2*|H=K0~^O zmZNPEk35#<j*mm*JBy%{n8g}*nG}$YZvznX)@V(9wUli(b5lc=95Qfo>m{Vop2W<o zqltVY0>-s8B(f=EzJZ@aP;qu1>&e;ScKD%iKAg4Tq)b_$;N8D;wjGsWk;v}c5TClq zfCv8wyOH(1I=>%dC)!c84A$M!<?vK0ubX`$spqpQZC^L`@>*TEs5zIK`L4C&xK4X) z2ddSMQx(F?;D`C3xh$5h{+Y;nb^4Q=m)?Gmy7Wn<i)t|<(Kj^zK3h1u>9?Z)_g2vE zB7=HY+WS=0o9!Shq(%qAh;wSuM!mFg1@;ZV<q0P|TM+$8L>3v^cCJbmtSpWl*0Kw* zw647Ws8GqNK@!?afO*I4u(J%kzd6G`iZgFmi&3lJAifM`#kC80F#2|+Q`SYH3o9`a z*K(=Ip!V?W8`uHA6${Yg8?e%GZE9fpzJUl0PS%=8#{>4$gHwX%!G-c;QAc8gC4GeL zt~nl{C<+|jv@c-V1@17*5PO4Tn4PpFpY6-TU}<=Z;OSN3pYF6t^<@qWw?i^Rn@tr} ztg7~^VI8ywmW0ZFAdD3&_SVg2_!fQ4`Sr(;2M^Ctl90QogPu`Q#gcxx)xtaEX;%-U zUm!4lM47xwNNDJV2@NeY>AbcdiP^>+HEat9le**aIxXCgqgVdRUhC9|HVYqn#%0)2 zxv5c=L#280+fbQP?-Vm6KxWrsl!amnv5h#TFYMIv{q2kpz;5WtSAoqBlY@y_e_xH- zw$D**E#nMOplmM<Ew%L~ai&k2jnJo{V;h4dVN-9zy|*<#v#*2v_9V_c9jyN&Owo-e zZ6gb3P(5zJojJTCBhfLpBJ-<0Q$XAge0A<x%u_d)&DM?~bXq&9g@92degL%!`Zmo( z808!6Cev3%h%bIn3gY+F?Bpi=#(kdHzb6v#fA@rqu0msV1tWM*gRAClQh0&nb%4iP zW6B_CY91*|moPG_<iU1h*kr;WmqO`-md9$;Aq3hS_^7!tzRI{gID<UW{{fOlIfu|F zu2Cs$hxLdRnNV<<xkxM89nZ);8y=oOFc*yy|GeyS4+Lk?StzoZHP2C7;b=XY`~Kk1 zuoZ4gV{kNih~oo>9__UXU|@FAyWqLS>QQQiV7GbXv7|RSG`c~HCV@7nv$cGJ?}^Ta z)CAPb!m0O_B3MIY`@s9}fz^chM0IJf003T+003zJ&rHR{)WF61KjF{XHg=orh(9@c z{Z@QyJx;+zpEUl^MHVmc+PiZ_(9}Rec{Up>qN&aW$xEGUzrIY~NFoXiS4G!1&7%97 zOZM#8Gu%u<3K}Qpq12Pcx<wXK5>4X^UHBN&QQ;$EQe9fsjpQ2VT4>C%=hUV~54=<b z%=!1}T@mgM#1U+p)|C=$(M}zP1~aPs@pe>^_!4MS(XQ_tlL5%-G2<=?hsWvKBq3k- zoSBt+xrmu|_oc(+QIjgk=CXrCK)G-QHqyQ$7J#c!%oJ;<hSEC?hS{`Plb7Zunln*^ zai%8y(vu?69DdWO9Re`+^`?(psgn-;@~L#^Sut^<u>J1za%_K>ARsg$)T+q&^9jm` zqlwO>jci>&T+}KOkU9cLmmuiOfezyzjj8_LeSRa-j+*Km#kg4vP$q(2$ZKP|_ReW< z|K5FE9FTs!tGwNOT^~QE`@0*YmSRi}3d(mSFcNy$gmz^u8^^epWoE4xW#q5VaiKc4 zg%_F(p&DfLeW)Kf<!h)LMwKrl#Dme^tj*8QW2c4mb<8o+9&y4P`xP=VjGE2n<o5|P zPDL~%8OkJ(89-iG3YcmhI6IS_XU2FVX7X2`Y4E_M=_-8;hMAdW(A*D(QAl9heu zB5?RxU>~X0PG46WB`US^E}m!XFW2E$N3_eoBoIWWiefTm#P@4AL>Fr0`P!NT-K4?I zSO%U{kv|46SqyN5vll*c2~8Nf`D<VdgJv?Xi8A6dYrrc@#@qDAB;Ms5pAEE<DHZ+v zQ}SP~n3A)s>~l_?Hwk-+^28AawDmb8`{-wdmBF9FKR#@OKZ96m5TS^#;$3!{cW8&C zFJnK5q3P~9+@+ZA`J|_78I2nu&;URo1XjA-s3F>5&4%d-ehGsBd?~^(;!QG>K<v2C z88=wGhmZIv8koI??&d!G944&=pkKoCNrHgI;08g3=(mFRiG=64Jty}#iN^pLDL7oY zN=vpk8D3pSZOOiX;=S@tuY;Nu6#aV|pYml^`SERUXPFHuX-nKcwFn1ShlA`+!#=%s zE#Yppnmu-B6zDH6Sc37d`uPfnF&mTM?s}&Py1EjmpU9x9!v$P+eC1*624Pz<*#S6R z7wU_)oJm|>GPf4njCoLn=dQaCIDF3fvC2H(BGXmu-)|PR_F?wlhlh`!o6|aphi~D% zZb!e;9@%m~9PQ>MD`g@OfxDb@PLPo%2O9@jKOHS6%c(a#Gte--;VuJ>gCaeeL>S~t z^-=a(GZ|ORIbJIk<u;9`t71h<Y66zPhU)USa1f*a;!F^#y!lsB7)~CM(rLsvF41-r z59Xy^{rt)PsKD4q(A0+o2>Ul|%;?M}sDip5L+(-=N2Jt|G_3*<ZxM<OR|F1dFPLc_ zfyoDmZtZ1)Y$v6tVwQVMA@6<%DT0_<UL&P{tvZqE&?iU~8xQo!$x?N54o*Xaa4qO9 z-Nqz|I>Of=#uAfMnsz5xR+;>GQB!5o^r4#0_NJI_mgy7sBz1kU9Zrk(micYqV~6dO zYp!ptH1nA-OZRtmyPJB8-dZ}jPN9#Q(QLi#;ADk8%c|1I3OLVe7neC;rI2Y6zQrED zE@3&Qw@oe0Erdn(yH~X*Ux~I4MlW;&a$c)ga6Ydc#D@?C9%P88_f<IIkcxEYLgiT? z-Wh<y#-*Ew<FDZeS<`tS+!pR-+IkxB66_5W>p+GDR{jP^tJwmW^w%DDwPvke!&>t% zrG}V7tts0B+=l~K;c_;5=<DN1RTuz)-Yktwu7kkrZ?x^htKEU~PuOE{N7?V8Z7~-y z@c@a0YAT6q3W<*h7ViQ}mR%@#U`VC{^T`O^?2wTWJHkV9^i+Lh)Mp<9FNhe<TDA){ z#B(I9+AwkTVr)LN7$R!s;9};P)@@OOEOUU(mcD$-8q%4gn}I#md>-~EWEGy4=dESy z93m3TK{`j(*SDCl_5g1L3}wZh!aHpw?|W<Y{~$2%U`Cq?>)2zVVkK==;&OfnTB8Um zOF!q|$jTM0Nu8b~i%h!aDklZuCaXoC7)8Y04qEX8<`asUp-^*$)~w0zS)&sHu5J#* ziJ?bny;9r-#&T#mz|yIAld?8Hzg9ROV~x%?(e*6)NIfnCOk%HBt#sz(>bc9>!iPaU zo(k+2nQ&($hZy;jx=Uo*-vHoDSg@d>i3*m+zF$5VPm2n{ixmh42%lZ~Y%kqFOH$?3 z0^5%>qy#CqVnNvca}0gT`i!#|EJTy}wRDF4r`g<6aaXBNbgo3iCNpyh0Zs#33?mGW zaGgGTi;HE{svh~+o9i{<`m1u|1J&0YOXxX|bl1>3{K8fPBluqWks7*nLz%ow16XsQ zH&MtMh}K@r3I017JnDz=L{I=6%!&{z)Li9NpiPG#8<40{ln^Kzw9=EiV(9PElgS1P z)EaF_u*hS*_I4?D-X0LXu5%(fx`jMP5S8JZS_m1P8B@%}9M@4<SqwwVm{QV*vV=|k z0L~_5h-!f*f?@{%>H`iu1#FkT{QKZFK^?YA5{TD|4^_EKA#BJpz01@BH0;BAV$O;q z(CmiV7pGdU$$gbVrP#qWkHWMnS~+a~6%u&h{iF69i91Y5g>@2&R^W}y&l{2w<FJ?~ zJQS@h7_NE4N^ysM9gEp7O(8Y_NNR|jy^;&FqtO6xq1%E66%4<3`slG>NotB(LGZ*V zfg26jmI0~bVQ@X@ShX2`erSNM0m#?LP%<pODAGNDCO3Zt6dFrX{8f|B<WkPeDRmI} z(2Yh93Z%OZ%`i{dfalhYQ?e%fXdKXfWRshH$Fn<4ho_p!p${QnS95i9TW&13bkm~r zQ=BGDx&0g#%b$v60-|)WEEdKVP3`!oQc1*yXh+>+Xi`5fAnldBH%IU(jtE_xOZGLf z&5U?3eYX-ndZK<fL4$7Fx|Jf98Y|AR>V^h+$V-ri`-<=~R=^4yF?pT?mZNTVXP>dr z{HDx=;UO-%ud?bG{Xw<BQ^7U#I_UgRs5FZV>Z<1u*K+X?3>gTO4n;HT&^MCUHITMR z%m!{bKsOx$9#@6g3m;6ddWPdVLLnW@dK8YQM&Efu)6yPce&3H^z3+3JqouDfE}`X` zx}``3S$u1_!W@geQ;zl-09F9#I(DKUz8~zfY*elW1*(5e`yJoQRiYIZ*cuJiSF9<# zZyz6RB!oT#A~CwhXNQ&3Df;lv&h?)$Lnn@^vpc-qHz*Nsn6FlHPWo{2z2&{LO&^9) zK>wW$?{|d;XNhkR5-F*>OC>Mw*LH^JNUzdw#}%^43TPi>)Sga6gSUs%z8QVpot~|4 zXHJ}VK>OOSchgF_0ZH)7Y+0fMpHhlO(HWAMgM6<<p0@Yr!DB0jom0NF-cUO}o!#JC z+QG}eacZ|eI6dth?O*OcB}B7ZbfZ;Qnxx0yi1r*@^%T{BrKxdONfNUbf#Xln`{g96 zbvMx+-GA0VeI##K-<*=`x%^&8RCil+%gD4ybF$noS@>o&FzsV$xMiy6T4m3WA6{}( z(`)b|VVphHUsgR_->8TKmf!ds`+w84@8}Y@aS~aSyGrzkp6{||x7mR2+ZSq@j--eQ zI;;961U=(Fs!u;B)tW-q+LT6rMo(C~xo>XUGs^yD$O&TczW0na^1PX6jldzp3b;qJ zTOw7jfR+pS4Lghls_^5kP$LXYVNW8Gd1Q6i&@@oS=39fI6#0I`5k-9Fg_gP>fJ2KH z=?19{HGrh#<fL+#-SbpYr^XLh&}-mp2dm;wK0NUT9xNJE3IM{<=R;thl8geF<gLV+ z$WNIkJHZ<Od=33&i6K<VMy$}4!QL3L7%Vk=owI{M1<5pmAW)vL80cT!tI&`-D7D!l zW6Q&VbnN|yKgG7&Wv=|B&g*h)I6WkoFk~~`t;Su@Cv0_AKaeG1@X)T_Pua)eUqGkc zVBv;|NnXYz(IG?mEggP$U(sp0ZgPuoggZh!NoAqGn;h))z|Vis9;uZb9If@Qa!oC& z#;?sRIXuk%>i1$+c(6BdcCQj<vuL{9MTap<Bwdrzyx0A8j#I+<mO}h)EKXHmJ?eRA zTg3>J$!uK7mb<i(x-2VQri61kjJkTT!Q~dz8YzzuHCh%i6^%D%DBIIh?x&RU2<B;L zC>vEH-CiD(*zUt@u8Qe#d1UYl8kC69SM{Hv$1s4ErJ?p8JvREW5A%kK?p~1Y3UKAN zpY-k;zv=~8;QgpCu^HRD`4qq;Ks0tH$Ck^&MyqYl3?e*0updxGo^87vIW%gBykTg6 z!X>l`;*=^D-Ac3h>)ox065n)daH;#1>L3|)b5t6!eWTScckOl%Vr-^nBShpaUd*;y z6*+1M$ZPSNfLQE#Zwi7eits@JLZ3RS<*h}K{T-@HA&FP{*RuAye%YoS=<6b$M>7es zhT#~VIq8WY!4B&K?qO0Z>BTmdgU$tXZQ10!M^P0`2R=q&wotU=;1h{T$VQ|<B;<Y5 zs4v;{CQo%hu^8Ldu}XBgLFOrrck?M##PcAA>o<a@qxGjE;;5M3Fw^BFaadu1bXl-3 zCD%4*<d`%=9jWh<v>Gaz)?RhQRK{k>K%_I~L>`O7Il#?-;(zMn4Nn)p@&7Q3=YJJG z|457f9%yJ{>tgdCQ>MC&-C-NTPmP|y5$_o9+Y>O5HMAWqAkda=<YsqEpW%KX$%d&= z5|G3?L(Avx=RZ>>$*9|~u0qp${Ln&|!%2o&Xis}P1@DbBJeEyzS$2%#CVmmSc&!RI zWGh1TdCQaiVb`5yVRegEy^~lyUo%<$wdgzkLAeUxLWrkDH<SRExw)793ID-s%Vt;w z`3>i7O3X<Uyn}tGgA1DYX5>m|kgd2;wV^U^hFXj2m9O>InVCv5-j;5{{l)%yd4prE zB!LZ4$t>K0b4AsV7aGdfM7Un0P|0Jp&Q=T!nwSj?$sRH0GUenxKI=1~l`w5Am(DG` zdU)bNV8~7ed&L5kL6#RO4u-=){p?R)%%DYYE3NNtskV3<Om#&wGv5+p8JKEIhv+~| z^c!-veDdJtK^e%wS)M(j)q9n&zgwiB*t~fyyQq3FST<u>cN0QtQOXH2d|(0@u`6ma z<y$c`6{AjbsfWrI06f<1OEL~;&`#@&*!_}dk!a?9XNqUU#EB$XJ-7^`$dB+_8KP(K zo2<99Aa0heaJA1i#(5*EeZQ&(_dYq+TR0r_eL7P&Jf8)>VCIv1<pGqvMT%a6qJsD6 zb2kb}Zs^6mtcV5_cDsUTZ9XQ7W1+6)BFmTsC$!4&89bGPnK3@LV{@o7P$oT3eFPAc zvYfgKFw+@w_&9Axw{Sr_J(q`k){kTGIEYXmp=b9^!9f4*?(c)`b6V~#9`3L2uRs@q z-b{a5ly{;S7FjM{^j56i!)a}@!Ni~qEs&OPueV#T=FaxEAO2yZ;NGd!`?1wUG89cn zycvzl^JEqm^|#zxM(|6D%5O%pT2c20>+}2a+fT>WdD<o5N$PhYLc@n2rlA6}<zR{& zS;<0;$v1c9A+$bgpj-2mm1mf*s9v(u{a`M7Z!0$1Mx+I+8}K4I(RFnE4v@%5wJc7w zEE?i>odFd*__seC;29=tvbzDSZIQbGEAdkwH^pnU0<^1r39CX6mri_fF$jX8i?}v) z;W!!=ST(&&D!~UM7dl>Wd19ITxad_x%$SiK`T0P!G#Gz2GPm#={02>=*=4M%eK&b1 z<Lg@~){y%nM$z=mizMQtu!D29#aNMomIXYCmSCJN2i8HQ5{IK45mF@L1`_0GEBx;n zJa`;Z0|`wDuHX|SC^zIWW>rkrACZnkD0{Ki+x4J^cvFmq_@KN)2`<W}stDdScz(^> zg)9jPvXjuP?S-xDKHNKb-4|xc#QtGFjB>1M=|Oi9Ap{XGeRELgiN8qoFB<;#Q^8zj zF5Mm`fC#-}!jOYchbmXx3s53Yp;yw{^B#!6PB5g=LqfhG^5P;bnNo{YEfbWrR=ylm ztJ$mw<P{La(D&z$-gWwl#hz4!ko27pt^};@e7jG!r)dtabQK%TrOP`km><*8{%wZ% zqzCRyFPwOf{0Aw&hzQy<VUS4ti1AbIWDcj#seqm}HkO5!l7RVET46tZJ9Ba*E%go~ zZA4ABQ3Nt9GJOWw9IFW)$LEls>N6)vrFLn2hv!<{$D?)@W`^3-3!dqafkvexZ7u2Z z&c@e|Qt8)v51wE(Tu$=~Ii9-dB@Cw}rHr~*K~k|Cn6fA~NHT4CBmA_Azps-jFW|D8 zCxAaK!DJf?1-$x6V+=<!C=BRbkU{~81p8c7dR_$rMS5I*F(TQ1e+cz2^RsW5spECH zs+|oblEv+=GfLj^lbbBI`<q!wh`jm-&}vHD6E^rt3=^ClyoFbm0xm`*!rUPN7!VRh z4Ngg{`8Hy(D#VSaDAM*Pn8%Jz{w(R6DhK=$6vwW{|1!zS#1Cm+oY#F98{IkX#5VF# z3KMFbv<IXKHPz(bNfZp+cuV98b!j}n1#?J>mt02S_@r9PJvx;?PcYd2k>lgtj>J=0 zTwX<m$&Q5H^4NdDT-0Utwir~35nH2k94gPRR4Wl!0c?AyOL+b`d`bUK@gkk3c^aMo z0`<WBqBO63e`F(u0*tftNbpyX$Z(<6E$y)7G(yDPJ&FR@=9}1+(Y)r?v7k*%vmh*u zyWJ-WI#m9K{cb#brgCBDRRlSsAU2kYqNv+}cWLUld6%l0KVN1$B9+q4;+);{Tl9+W zGT}YQ-`v&UBN&(+eDaajUk~hea^axMci~3X?vQ0_Jl{Xa2!tBE!bZ=q>N^s=Jhnm< zEshH=*6i1l%NA0~;bFSia^^Hd;>>YiuS;v=faY*xDw)n^q(NYPvA+laru}i`ir)>T zMZ!Kia^`+FZ#pw5koRG66YjP5si?Scy1}h)I^+VSnL!Hx-d<F=K#8D|+BMei325+& zHiNg!ydTt*jlgbc_vX*5P5mO7!Y$;5ZPD#Plkq}qD0;Z&GO?aGvv&{4xU<qH?%0U? zK&lnxd5?sTsEVwoJW@)!=+mqEiXDL%@i-PXFY;?o&?T7E-ER&5xB}JYzwT9HX@RjF zYl1WcdHms$I2oPl7aD*2Th$a^0&1KPQ*ztCFm2t{>nM~MVME4)UlwXbDUr=3dd!*R z&Ps7!IqER4E7!A%A+9wQ_;k`MK!7xUp!eU$vHwo(|H>r(uW_uCiQ~VP-2Y!BF`B7z zMjj#nKsxXL#bf-RWd=^p#ulcg|9FgQEiI>i4Sv3-wdq7Ee*%yrc;*W!CsNg7-MX9W zl~TPMR4D1tKtc%(;q~nkhPIP_eS2<lFkonl+RM-SNh5M{a(eW@(9qEE{!-6oO0QZf zsZvt7eYQ<RiY;@eT}Y_8sG>GgvQesbGTJy*UkuvnoK!@tw#S+_epai-nPgQ~smheI zY9v&g+N4@9S9W@-Bva7a9g<Z)NflNS&O8QrPF%oLsx-tziRsWO>&#@@Y?YaPp`!6C zS}ZHC9}+>xGtjGgw%1MwujUo6Uq}W_gq{zug^0zebRl6gtgOhuJXvKA*;LacufsC; z((w!HrsksTDQypF(B|^<xC@YtsOc`MrqE(~y&NkRn=sg!ri>uv%|K;okR!qi{z@)i z+oGv~cQ$HdCyM|ZKR;W)G*?8q>)B13l6+}1@CA`a3Z^J?+A`y4r+mJpsdDMZtCOj; zkYQ?&GnvHy5{w(skD|pw1xqH9E^3^0%$H*dtr!RsN8Gu@6{Ku;ur@(TPk-#^)ZffW zT)LOy$WThf+-0@^$+6Ku{-&|cG|GVeBWfC97OflS_n3|<ffRDs1Bk8i8Z7yVchX#c zAD5g0etE6pqycIOsx!1CjyRIF;F$kunxbBuAY6NuDl6nTtKes!SLi&!S5^ljt3DZZ zkps#rnL`a$SmK<+^EaF^>#4H<z*9~dKCGtYROA-<yn`K~*hV&<QrKrU!l`0d#hk0e zF8UFR$asN?O@43~iH5)y8Kr*YPmLsa+-y&uZv&v&m~VMQE~$pMO9=DS<|2xTBEn>4 z<^PTxj7)SlD}zrJBCg1ShZS5NXn1FDD-?LYC#kN@q%Nin4P<O+%I?`ikiA)rMiZm; zCK16ABu&outaT#<A(3YC#E^i^ikL-lx$GUWW$YKQNu(|FEhooQgSY)bF9G~g`$`F4 zeWQBsrI4D2A}$gPw7U9;MGP+hXa^%y*UXTkUS=*?2NM@)=q`1$I5hrKEHbNFb`>dn zRvjz6+I@BO^4{cGOhN6k+|7=9GJ+(qY}hN$?ZmXfe%FrmRC(){YC*nH$bk)G*gozU zi6)1cK4RjdH4?JjXgn(p@E1uZc%l0~Ta&#&PG0II>>I+4=}YmKoTS8MT)B#LjsRe^ zKd!b_D*_xeK=Lh{LjP*wgI3hyFn<QroPG6Oca@K2yVFH3=tocT@@i{=o@)P)BO5T{ z8j-Ece<VH|uL#RBWZ<t*OHqv$R++OvCm1!k?011<;J$%%XA@~(xd7E>tH)@EM@dTQ zBu%+;(h74yh~%PWv^hjtz=t!Hi*F-P3BUSV29gbZya#4CjcsPGevu43{%ZyJLFafg z>xoJq_91OAw#zr+42}7V9JGjsJ-EnD0O~mIr07<uJKaLLIZDHkS~d6YMU9bRSn=8+ zUQb`>y?1b6$a**!;Qiv@IKTxv`pzkJck45`bIAk#!mhW6F~G~JTNVfUZmiMEbp@AD zqzoqJU!Brnq~=9OaEr_BsbxJ8^0ABFLN>c&fWW#a0Xa-d6)^C4tM_s;S;-xB30mj{ z4E|QyMjEK!<AFhBJ!e<@Y}yaE%j(%G&*z`XhGFx5zn#0|=l$yW{1S$@>(}=C^GyA8 zMv?)0R{RYEIR=uE6F1yp9i^PpMfvApUq{3I9fTEm(>+@t><xV6(Fc@rI&&5A^axsl z+4LpDX=1kZj&g{&k&mI9h=fGJxh>=Nq3Liy@H@PVDeBQS!}|jZqVg*@Bl{_TGzxXR z#RPQ9A%whZT-UU5y||;10yoQl=w!{r1F1gwfkr=#*`y8x%*}#H>^?Hm$aniDMkm!v z-U@MB(o2I6Txn)56L=j5zJG$95uAgYLi=FE`npu5?~k9~sc#-}P8aks^(W;fLAmdO z@Y07_HZz1IDT<9a$XlkF3(>fGro$B|i>9yr<h5ZimEmP^D_=IjrUIi#h9q<6&?1|! z=3c2QYi9<L{>(A)9Zdh$1q}9#_qHjAwh<1(lprWU{KjI!LnYRH4tTyrems_6Ai9Ep zH${}`aRJ{$j3{7@LY*@7>kyEi)AY@L04}B9r<~DRWno>urmt8}D6UV(Nywcz9y=Ku zMO)?H4t<gP;<Wv^C;0j}dj}d7I|B`IE#SM#$TY;10o@CHPt$F?BHlCFN^+9-%3>g+ z(`e!*8;CVHr|cR*w-w`0`EFX$)Ngt40hk!PFH4((FTm};zB$Ga^#&_>%%_g(86kU- zAgZ)>se=!sbMr>V53m!|r^X$mg?*;1zW|Cv7(aL)QXx&{@!`X%gg=5BUimhm)KVBP zSmr0ar+UltYGbNBiUFm@(4i3PzBW+%%-5@bhL`Px97xEumwk;OKf(mKn7}`=mbX!b z;ue)NT|}1DDUP$cr(KZRn1pkm^Fzxkes8{gP5gRYMS;z_iDcW0;ezj!Zr@X0zbx;v zCg?S1-En_PfA=Q1+aB=d;HXMIM6-`<S`Cd_-fOW0HWTQ)y|1lPaFc)}$Jv;*)%k)S zk{v{O_KLq76{F+{!D3fp(X+W6ZS4X68cuyO1B$Xcc7L59&C^)t+4=Bu^np)^Dkd-S z2^Hwppr}vg+A6~!_(Z$}ti1C=c5!YXDd;N>zyFPckS8DBCtyUP0pehYZQENz$pQ3r zf!V^C{oVGah;9p_O%@9nyost=5n66%y2(T#C6{!J-h;HHu|RRijWtyKE3Xkf_oG&^ zS9aAnF2J1p)t4&%1^kF{*~c)?kmnqO9UW1#Xw#@bje?>3nZYpHPsFegGOR_bG*&9- zvb9FiZd{q#6v)XgR(RN*>n`7o2$#fyHsVQ<Fd1TbU(w{y`zj3Z7hSPlMoq5%d(PXy z@G&HxnW$Orf%UrJFA@B>;c=KrV=G=J$J5)ltn2)ZG{ByrpO3vkh9Y>-*z)kVeps>1 zj{MqtN>h?zL*cqK9SAk`<T?J7iz4CMhwQTYupJEcs_e~tafjdU$9J=2MMI4m`y1Xi z5!CUB)?(tA&unxWN#()C5KTb$4gN3|in_j(j&SUv4?;`z%Qfe0Vov94mvH}aCQaQy z{<mZG-vma6Ybi2j+iK(<?}C2C#Xo1N@^lR*(~Z}THE7((APtjk)VeTr_0bV{E%dSZ zc1vD*CJ=CFHK@fCoHpf+fW!U?EuRFWmdtgjn~1of;(%6UBH%O2E0LO@-@p+U=xM+^ zvwr%C4%|i8goy&kSPtuz!Qj!j1Z;Va!(&PBepL)xKZQ_F#AiWHW<^i^N?EO>-3M{l zc_3Il%ek^dmxl)~<ZsT+2NRs!VQ|{bIfI~J|2`N1YcT5yd4W5~D{)l6pi^Er`l#HK z=<-~oDP-)14`bO2@06>U0k@cpp{xsi!+M80AROm(V(^~6#KqOW@>mjZvsoS22c0HL zr*eKo4ZHsJzmdY4WTGLdW4m?EXUvo06-OL3xT24OQ`PDqUFBbDXUlu9pL!NxpStt? zyidvVZM)tVe3YF!Qu~V`hIQVP71j~S*CNd5z#wIQ0y<7;+a%32s$$KJy?z9UBxe^` z#>H%}9hy$K&6O&ZFtm5km}_)6!eOX|jg$$}lRadg#0*a&8?#IWX{t;c?E40^487lj znw}Z-D$XR47}<;E#<N}{>|gNQa4v8FVHbZD4HBf{mS)GfM&hsbUPFHCtf#jdQqJZF zyB07?&%pcQAez)GiJt9ao$-YG&w7+(U|YCR6b@cUH0Jc{HU5l3FuGD2A-Ej1C1~Dt zEnk(mm+6~{zuX53=7#k>-OBrIrF|Ah+l`15+*D^X$IM>lAO4Zk*x&D=R;+Nrmn5l5 zQDQ`EWhgLJm{gb~s}OcE*!V(4iF?rNqTH|uGrARaZVIH85qjra#2uP5!gQYc>o_{j zai~;b!o(uiIL4xYM^<M%YIStTfPKD(r3J6k)QJ?=KU*R_0r|<|!TNQ`Da%`nIrMU1 z4}>I==RDl=f}HGYt1Z_1>Q?UCfZ3h$mujiBahY^o&Kj)FA*ZR`;UT1AljWD!8pd0} z^UvW$IE<XvAI>v7Es39Wp!xY_IORZBl0PQ7Pf=35ZYmro{yw8bA$1mL_IRlq7p@?- z3+NpUMqW*<n3AmqNOGhTX(N>HywOdhRf8KQahqVzcByp9Zh-n(bpVjx+e)Ll4Q|AB z_Pbd~yZWzpYlsNHHg{$irF>g{P;mvNe8gy@xV_qyTDHBFc4-B6+95=hvx*pYiKH7E zp2cb7s${towVP#rJHL&c?aTqcDBgB6Q%3~jPU|Pi69}~JV#GGf%D0zR&otp*u@41W z>nlFLFE5=EfZ`gWNlNVyBHg|*h*$^OW-*aUZ9B$+v@5H5xhn33ZFvEOP{j-Rak8s- z?fNM?3d8i})jmZ<<@@Sp=aa_W&J)UGJlA=Px+3`J?{%<qGJ0L4$+=+9nMyUilOoVA z!h&>(Tl4xnGo2at0dht@)owlkVKekW=8m6X9{Q`nPUCla8MpFoL~uDj;|4T$6_fmf z_Jj&f-MVQH;`-`OuC4jQa)@-xUVomo%g2z{dNT$3X1^nMl(p;*vO;HN>G$hQ_sHH( zGi@}%v~`otgd_P_v`zGuAC`L42E=syBsmkpe2~c^cGv>8r?Cvc_rs@pA{n}C+VWEr z3!I;4+K4jh$#9PIh(x99cl#WZECc;m;uQ9gu2g{IiWrz^ph>4PHEj(8o+7jyg58H= zI5*ePUqse5uANS!RI%^XDno{$6Z0`~;_{~jt{Kv|XpBa|N2Oi6h7u6|p6o8n?oP)s zC34OoB9#=~`rJS>Vw)E>`5!t<1aYG$%&<u@QHGto3t1LsD0XE|Jc^ZvG1`DPfc<FV z(k)l#RU|am(}wx-P~rfwQRJpqPtGaJKa(|W+vp}4d9kI8(YdyAc=7BMe@I0yv|X{K zrWE^Qwwu&rBN(FXzKUwhcy>lf0?oeuKyhIoBOmfRV`4na6>y<86T}PJ=W0HF3*F#U zwkju$T}QXkUyBd&bl{l8-@|v>rb9VcI)^cr<d|p^kgJY)mN`V!0WKew-ny$#@^E7% zdbXn4+yfSX&UgHEa?1wz;jfVE2BvK02tx=WWHF5MxZW+|Fx27sy$O(0rapSS(y6;g zvhrS(d$!%hCKma()!WdKU7(&O5L%vx?BCUqgMBe&LJR8g?lRmmZ2)MNiB&bN#`%u= zY~0{31|NC4W%-Wku;?XY+N^Eoe&D<^qQ#2+^+0R#9o^Q(xV$#Dd&nyM*{;q_aFi86 zUy$!{JLYUZrFuqZOiv1(M$3nMLKu*0{LoIG9iD&zAl4r5vOs<jyE7WBZ*Q8#qeI{e z6B#YVE$t5a8zjR!y-*gJtF|tTG{jvJ=`k#m+vraH^DOnCFYiaq*xik@8~v7%P8Ce_ z{Bnz$vv*PK`S^1C66wlg+7f3Pc(~X>vOB}c$;!C`5r1@JAbCBy>U%YV{mh5+zt}nl zCQ*RENUv?%_P(`k+qP}nwrzXg+O}=mMqW}$-CgSDM@&ukbbmc;+W$5jLfkb|Oxh?& z0UsN5#y$JO;!-e{Dr$N2LCho4#>PFX=mZ@tX%;xmt&!VWSKIxk`Hx%K69`~5#&aeE z&D+0z)%UR}2((S1M!z2krtTSkO*BR)Zq(=THoc8#+nMc0NkMryx7K`jg%N21*Sa>V z2#@({jC=Cr@fu=SmsHa!#27GsWcYdO^q_q~HeY&VuL&cn$8qDW#AJ7oRp*1?Vy$p( zW}uVC0Edqb)h{UPZz+tPoiH8ml}(TaLBfJ~sfB$RE7tjF?<_^9@LX?4%Qm+Qg?ZHs zF&&rc6#C8sBEFOx`NpY{o6-kPfa@H&G#$jKXj_`p8Zo11T9X{qL&_hFt>;}J(zLEz zQ_)Ij#ERB5rFMffqJRol4ZsAkB|fklR=IPZJ~*~*<ZCyUO7<X5Z}TX!&GXwTC`$&^ z;<B$%KcvFA4N$8IXG8tcM`W(f&r9-Fwri}Hdk|a9^-lD~qIrWH`VNx>H-i&*o;f?R z=+kKCbB53_`-7H{T?RBdxn5FJQ&H`s3-kvFjYYt$j&1GEeuIr1ZCq*}9A%Nkuj<Xg z9ur{gQh5{Z9WPaf>)w;cq*l8-;}a8Hb2lU>wMm(~qwmTNN*NRDon_2;7<9M2k?g1N zyl3u~I-vyT_YxY*=S3;c=#dke+Ni$QiL;gXEqo~VJmt|7T!Y$$3h=@gU#`If;To_F zK5iD!wDDBb%b{%5E!+T?2uX9Uf*Ugl>C#WL6V(kNx0qG8w&*~er8hL|-=5Q_y_GQV zc<fsl7)%@%#5l6W2smvvT%1#gMKz8Q_3r&+xx#qE%Kdk?f1C2Lu*>a50qw#xfh5`A z{|g#613;9x|IgFv692!FiT}sbvN3TqGx<-*2+n5gChN<#o`5u-C>)MoAHcduCbVXL z21-WrCj$h?df}+0u>>k<=g#oYXD)Odk&nazC?|!c6cPMv%<0JB&em2}+IJF4i20Gg zl}TM?E8;bn?$w54b&31)odk*u<&s02r4=IzwL3?1VU5&!7A8~~%%<x2vGq$`jrE=Y z)N<^%W~7VO1i(?N{9XMH8f>*Ti=h(B!?2QGpP!_=?yDntRzbN#m-ZB#V)aNR9ZO>P zT&bN~^Uzw#FhG-gMaf$Ok9eO2q2}90RP7n~KY6aRJ3!RAUds*y<wZGBnRY4g<Vftb zUR@&C5lG2TQj^9qEW0uax=4Z5Ck6R#1`dB*ZD`5jz?*j>br0E#5Va}$GVFh4GJ!f> zDM`?RMI~Vc<in&LhmO%Y+?Nj2w$YPTX_bY<i0Ua5guiElw+UuinMyW$l$U~4c_LB$ zsak=KR|4R#F=S5dP=0}>3TT{}0J7Dh!h|u_{N<UBXef1G8H&2sbcKze1o9;hs})m_ zpgu39@2rMyr9l@`;vmXAFnZz&5c;R@gW@HnQhpwyikyQW8x#Vj0x+SaYFEKA)?@5U zayqiMiaPbMRt2H~xHdsH2@XD=rd+G*P0)G+Zv>^YmTVXlH-V>1XuDFGgl$>{5K5yp zoM9Yn#gG=o|9xt1&q~F0P4%*dy_tB-43tW-4Sc|C5nT?oJOY3@@<SdcML?hRNu<b9 z_thV}(sq-cV!VM1+*9j`)NpCCz>aO10lZNjG+|%Kk*X8RI^Pi39h%e))ZQ6(4fR<; zrEeAd+tO5b{w0J|5>mOaSU?FZ85q)*ZAA$53dFNguz_Nw5ntbl7^#|{D2RN^eA%-a zdadR*Zu=hBKJYkj^JFbx!R9#AoGuy5%~S~IXzH*>$4>7Fm7sEMfg-&$20gI(qj=c^ z;EoB1-fM3zOR`4HL^{vXEgV9Nb9vk)gfn(%Yua>nSPml3ABa9$l8G2<ZVq<b6v5wJ zLJLdGv=ZP@A$%HQW3nqR!u+8FOz;%COivcO1yLi?t*3^@gnXR9e+HO)gC-w@Se;Zg z5S0kfF=LH=Bi(;>Vd0RFRiw&*%Rua`&GWG)fUVNgGSc9xUlm$kImQl5{k)h(L0@JX zZ$?$DDK5@w16A6KxW5@HyMbtOzRkZ&@bF&#nraS<d31TCo|Y8mhhI|YOZCuH39v2` z5JiO%C87KGO&~ud{#!Q{nlS7c$+*vnr<J1`V4RqL5hI{5fJZ2XNcZhVDhreV5F>hH zo3*~a;pYynx?5a>r#V;7xF}{;8^Nqu1!%HQxuxC$I}~f+0g;Wu3j#RBD_z|t`mmA} z6=nesW(8n`yTP>!NW^2_H56?(rixYrcMe|tCJr(C+!%8{HVA0_fXY9+0ED-uWMxA0 z#$a*eCHpHR1<Js13d5FhHqwNv9#;x$l^&X9@AU5p+Cz^Qpw$>B#2)}mJz1Ae5Hfmz zp`)4p5oS_I;YLt?t*yzvrT`8Qchf`SQ5)yNZ$@b7e;g&yD(3nM>Rbx<0Q5%HM1iJV z<L?1`P;<Zdt^>Fjiu^77XIG-Ec;NX+?)BP`_Yx2-t*YazlM_=`BV~Rz0j%X(t51Qh zij_gZFPK{A0OtPQ`L&-=(>I|o8d^)9IE(=>t=VY+dV`v0!GE;RM@ZUZ!7ur?OZtDs zi~=716<!!Fe_!~lZ`wD5`Ul6hM4sOt3brp`^xum0W@w;qxaukFzZ&7I?Z0Q}GxU5h z`iCAsbsljA1q9Xj(Q09NMrLU3Y2Y9-@E{++V=cB(im9#%c~RSyG&p#q525(GtcN!& zXB2$<(fIs7+*ha_tUvl;b4YaoIdE98hw!(%T|R7CUfQ6qu?0+PD%0z7C&PC7z@e?L zqd?UWAPC%!g@7#p*wR2p9?<tnzy@$<gyMx94}D(pf<GZ|h0Sm0Ht@_TU!Sm7!vcJF zSYwVqp}(%Z{^vIV>LYQztW~bol8^E%n}#Zw@o$ATIL_p-(8<Dqy&7z=m31Fw2_>Jc zwN-FL2r}RLcW;=g=TRGgzv?(U8)I>!UxD+Xc*MQ}Yz-waBMO2C1NZ^?BL$VPw}f!h zfET47P6pmGGpW>NV~$iz-e$+p^x_I;O&XMJJ1f%waa?wC{YEnq!5z@77zroe0e}yu z{YH*E+Fftp7JayY?SJWxiOWETZmL|BO;Of}7GR8``gGCH=;nH@v~t8P4Z+3an++h5 zGiY)i^WP8RsmG`YIz+_l`cj&YBJ@F=c$jKOL(n+;1I7k7%wR>6N*-0ANC+}52>9AT z>)W`KxGN#`#XW=XfgBeFwmheLN%hIlm?qyW_7g{T6O%fghicR;HSfd`h@Keo^sfn1 z>C!|!pY#}F5tNCKj!mnrq~(Su)5btQr4Mrr`qV9N=M%6AE)Yp&4_NeahFFuMmpJ*1 z2*4%1x&<~~>ww5(jGlY?V6z4z500N?VlS;CoeafHQ8y1o@@Jv@H*ZShcC`;h|CZ~L zpq>Ji2zVw8JtBtspbhcEzM$iJc>zO`rJ~1PRU%v1hR!1lp*@RpPQF=%3s*02N@=%E zjYq6|Ea!`iQLN?=u)`14Ho=G4^nqkO2fy<kyyNKe!CP^x3+$|biSZeSF%wnvaQat) z(H;3$rYq>LRhAB53{b>w&g3tfSz$c#)5Q`7l!9;~I)u|+uAXa|8?Kjv*{MI?^wmBY zOU&=`d@zQ8ey?k;t#%g&0t|RD@&m*K=p!8BIq*S^`ScM9kVQ}Y!P$tPHTS9LD1}{u zkB<N)Ccwm4-jhzs(Z5hjHqWT<IkHp+@NcP1!-B21=+=(Yzali|jg;oozgq+N0`ta# z+25k@XENgwuYj<F4vc2!R!O%qCrQtex({un1f$1vSC6GyNfx$eU1~dAOoepBj+C*` zM-A%!Q2aXWEe_oCxSffNIVoHhYHF;H1&e_dM&nAEzOA0%pAjDB$ML6%W924p5`^Si z(~o=}`pc&cX*MSa(Fcl_@x6D219V*H1doUdc(f7UH!Fto&4J;+M)aM5DUlCJ)azR3 zuO#y+mN10l07Gzx{Y%L~pN`C4IkXwY4-<4f7>un1>kgADc4>=W=+Q^9#=+_6YVB|i zS(f~Rf#1$XHv;(^zsW)L>H^1j5MqMeC@n&`4>_ZUQJq5lzHL@M6UihsrG3j8of?=> zK$+{#=HC9;)V`QC4DR6zon+xUyMo}iM8{Yg;88!0;+G(%)lP?XDTLl5u;j{a-+H7H zqu?j&;OrC)^#+Jd_)Z$@kV`-0q}1+t6LtvEKKD1SmdLSTHCONLSTqQ5WeCEYWs3?o zVy*(JKMR2ee)=?5NnBpxsK7L5hMFla@Gq=Es(D!_;i8fp)ew@j_Q)5B6EM_+Jv{XJ z5`CvndoL(@;QOHzxhT%a3|YV&vr(>Af#1Le2dG^~vR32Rysj5xHEJ{3CAhP0?w)1* zUjyyEGz9=oX0}8vh}4)s=eiNgOy2GOB+$w~=fJ@^M}x?I4-r7LBQQIFVJRrzecLIg z7}m!grN-SF-q)rMnEz^q;m1Qhq1YSMl8G~n<I@eL73iljHV0QjKpQgioch)ybVfXP zz4U-Re}bZ~>&;lHnvAxXLqPMad$J8nQfP!dLqID545&0p))4)VJs`c+Y&^Px>0=`p z`VBqd;sF12&^*IV6~TAhWY$5ETKvnn`;$cpBr4U^%oHocSXz`RUtGpZ(q;|CMGFR} zRA7?Ddyw(^xIYS@m}odeD6OvjVnQ_o!Q(M-g4g94g>oI8J>Pkmijg(HQl-=b;{tTo zgWB-vt8-paZVcHSdA^F&4KNVzwO|T;VZQ7TVQZ3SSL)GGrx>Nnp8&6XA5f5~3ua6Z z(~DX|as>LRFZBSNEj`K8XSb7YAXKe73mGiGBs(?+>kvfuo}i-;k$y+Yy{8%pnHB$v zk{K=_#{WovS@J>Co!9n%#}mtuTs63tt;woN*^|nHm`>IAK2Fp?j^y`TM|m30w&^Y0 zCbxQ=&85Ff>;NS<06Ux<?G$xuuRQ{-QFIc<>$|7k**L<eSaf8T$<GE;V~N4Rzlz3Q zg8=%TSRbO8a-$bdC?&O8i<J>ETDxf``r9hQ_2mbXTNJ#K7Ib|~enBZOaz8Q3CK?yK zJ)I%vF%6|RqOHsgAf{F}lWT{rS*D){!!h$E6!8=-KzJYej3MHMhS}`eK;PhT$WI4O zpoB?E_=K0ql@N#1eJ&9GIlys<l%~_zYIgh^R2QW%fuc;L`>y)xI{)J3RGN#W*ptre zB=4F)!N!AlU;O$F0vL#Hq%Q|WU$_Sd;lN-irOi{<YPKyt)X*X85Dkx2x0jxQXTY_M zFyi+%-rqcM6elIG&pRAR3tx=apxWuQ&QwYetJEVE(5o6TTZx&gnC-*}crNj&zWCQ^ z3fm3OhX}Hr4i9qv9$m^XXiQMMOZIAUPyVa$LI&WQ2PWQnWIKZAIPYF>`tBEa#KKk< zfse8yoAY|1u_*`2PvneQhXx{tH(x8dkPLzeD0}YRH_g<Kb|$<JbfzT3m1VGA$+CT_ zF<y&rq62zu7zG^eBG)SOHNjzMjJtAG7#ui>L|JWU)6r2d{=#Svc#OSt!G-5(e0`7y zH}x;z2k}g1e98u+Qm#`3?imYwY;IOomfLb3{YY#h*-SDc4sm$jEo!$e`eh9>xXD}C zci)m-iSr<;E$LM-wlHk~(EI@=iMYTRP*kONp<QQ!a6y;+?Vc<Q12YJKUj2N&>!s(K zZ%`Ne7l@|xCZXF;rOh&>W<nMidLY!SINJ)0hs~<UXviR>J`Hit@0&r!96VBS(OYmw zFsJ^fdMb@Vvz#Uv$6=BXR7Qz1F_oUoQZQ?ZtWw-|&b6r55Q~zC)Ri8$Ync%Jn|Yue z2xA4l*wKt!$z1M;qBfFY=oD8#P6Uk6QaUNQ477@|B;>}NkY{g_P=9y-Dfybi;vESK zJ2;ZtwY>!fIf`1J(+nR@wZu9zBs4zO>!a8&NEm#hp^OWEy{|$*HrQX}C0dS~dZi`% z@;tn`jMcd1Qw+9m!ob+$QV?hFnv+v2Qd<JRSQI)`s<%pXj7f`_MOKgtAf=Jt{_Td+ z*kHwI=S(}#qJMIE=6f2ChDy3-qtS7iSiKYo*g3v6E(Pnhp4L4M7D7wo`3I5*cp(Q$ z(iJpzxb?cBqKe*~Vez)U1yAAL=1c{3;H_)NLfqTBj;&Z`;t;B@#G6N9%0*UsTS4f$ z$VS8@h{40{w4wv+5)kF)-bp;3EW|*U!aQ4I6Gq?^4i*P*vT)D~B<c1A1wL#os6Cia zrlo+l(9+jc%(24^-vw+QmeBt8U-*7L#fieu`<-X4oXd8S=``sK;hw^#{7w;~8}+cb zUD9b8A{Q6uy=FDDa`UDTZC>ksMh8p~nfUoZ$@-8frgwckTLo!0EwgkF#QiC8=#^h> zYSVsDpk8VO0N3i5d0yTf`36_c#X&TtC7WKx%iOUNV|2!o^T7IiN=mFWL1=C!i$J4o z9ubp$3Z4+C=WaEe@#({horyMpO$^^>yDf*m3wWwX5;{7ZU7xe3F1TT(dj%1vSMO`c zLO`5Dpk0;GUbb#Au&MG83%bnV(Z?zWaa@dzJh9u~&v*2v-Oy9W0n;K^iy(zf;Z=~` znla-<-j{3#a8Y&m0~+^^|0IB$SQV4i>eYHvsJ^x<v9aOxwp?Qaqxm=)8yoo7`p8PO z1oLtR)qzGQSBQ-V5IK9R7!XrG4KC*H_Mz03yiXMRd+-Dq%RI1qKX^AR&0=dde1h<S zXP`q=nH+?L)@WWOM&ED8<CSeeY^NP^toh#nTL;{fOlDtF?X|$wT_?`pU*)pZE-`H! zd3~@i2kW`*p6k!nyaUyT<%BVJcQwh{W+B|&g>V9oHa4VqI-|43^gfw%Y`U~3C|3xJ z6}^15CH&@4{}A~PSt0#3mmGog*q$pLCEsU|UYIyKU>6nvXr)~WPQaH!NT_M}jHb08 zZFKo|7}v<6?lb3`Iqp@jv5M#s6U(Hnv&GzdDvsx2LbtXp@)trV$GWXh7QOPww>_O^ zPQt1xdpr)TxIQn150!=yHBpUX9~Sk#3Ufx8&noG@ioOYF$I?J*r@?Dyd*2<FGb;x~ zzVA;*HA%e2VBSC@+l?FvJi4*X9Z=~p?sjr_(o7>J6<65H;>=&vfvc^S{&CSPZHg|8 zvY`nt!~5P<h5nY~U*|_R`I*j(P@{v}ET;2z-C_mtoqKi#3Mej>0kl(L&`@=32N%pH zwBLGpznjQ`^CYZvOt#z4>p!sHvw%Q<7N!|7b;?`MAD?<4e*#1|xbC?GUiT=*E5(`4 zS`u%5&jf#?viG3iS+w(pR;~itZxUrJpMQb>-+&md-!zEpzrJo4)&JKOb8>dHu>a?Z zE&oIB9<?I+uIdSF@od?4q|P%_00FBVB}N4ZB{|u<E`krL+mqhZ>emz}Ca-1{{(ALd zCL%o>`J@&4lsmBBN*>v>KTfkT73+0-x>&i)Yg?Kmqu$!y3M=R^mrNzsx3G(6U57L* zP!YDNS{N(|sJ4y;R~vhLz{+9O(7i=bN@tR+ij>f~Y96S;X=JE^t7vFM3gu82q}Wn3 zB?d;QSCk7A$4D5bKzmsxkG)WN(NyNFt!R3e<U$+WUsJ0#1~L$%sb1tXno`DksTc%S zySme~NMh-*If8dP&10b<U8Yots{E~N6wFW-Mk)zmVXb*ql=hKtW#7w6ipi)Uv!J)u zCp8COGYttCcP3~a80$cC)x$ns<yo=G7)*BA8F*mOs%e?925M_A36Z`E&~6L^&=f`~ z^)wPms1j1r!eB>r+va*HDbOL6<>andZVl!amw)_;-VQ&uj80a?QAr%1_-oT~&aAN| zkh`@cCyyoB5AI^p!koIYRo?AQwbx~2@Ot#yN@5~Tr^_GLvAm`XhKqdLiOTiaP7lyY z+<#n{utA}U-AU8@;Fz6&n#!cwww8{fb>)?(zgqF8Zj|KjBGf}QM)YgWm<Dieoi0(N zc<*!l6sY>Fx)igpfOZ$Xp2UTung~k<FS%4R4G`v>;T=3Yt@4n<#35m|Uo(+P-dNZ| zUQXmDQUL%ew8`GgAP&ZFvxyY6K~>iv#=b9cF6`V+DrLu@c>)rU#t6oqY$Ln**hC0U z##T01N*9uXoz58&=|@i7TCX3T2XTd+BH^i`F77)7>^)Z*44aFx6bx$8u&L#KMB#FS zHU~v8jUunAhCvLO&ec9BLga<TIk89DvW*7ur%{HA*Lpk?f|@XvA$sVSJn>xHTXiXw zRw7r3L@}DVg^Vpg$y!D;AuMti<vpIa^PL*w2bO@yLIMzrXo^Xg2pS+NMF?t|yEwiT zLe|F82r7Nv24(WsQE*80K<>QFPko?~&sjDs>@jmd!L&8y$Y8Yeje=7<NI9DX{UuJt zr`r6{v9p99eXhLY?9gN6lQ3I+vNP+IP3<7)c}SqVf&~-2ogx!SyzGr)Tn;a4#-kXm z;6}K0TL9r3#MTSbNtUR(98xV02o6FKTte%GHTG=18N$f@L#<+)cr?aM{}q0@)peEv zz}@u59Y_kb^P-QHps-L*-(5@$R9bRx3_3k#N)81ggf<+9s|FqqzJ<;elrZ>w&vrMn zZ*0zAg_|Nj_)D8|BXp^(FFb%<H6B!pQK5~iSU}jXE893|u=qpb*y7=0xr&4m4n)>e z;Msc8FezR!c{V*_YqM8{{}yV8xToS-a*G*mmioSD%zvvBjZS)NX|RH$WG2`5BXHYw zTXo&5MJFPTpbS3`JN#V(msPTJHg9Q#Z|KV^HXmOyJ+-XsQrg=J-gWqaxBkwGCL)2q zHtQP?20IzVdID=J`Bh4Ywyp;I2bn#TuVHMn74pKHzt!gj-=n~d)YVJJN|oPRZ)Kf$ z<@<d5`uY>UZWZ5d1)o><-_l~adjNzDIY_u>gceyR*8%4OiH0Z|u$zG18=3kw9rs%c z11Kv>+%Qp&i^VNTf+}9zei!D^em^abc;XRS;iH?+`CaO_`)ugn+i5ss9_eP^zAtSX z+^dz?PPC(2>ae8Goq(Ke@>+NGcllUf7|NE?z!B|;gitAmdvy0YMKx~tx$h$kD|NEr z<190i2}bMCm<^=EsU;8^q@U~TQ^8}zhA4Ic1|oY8I`<|;^-x9sIAXXR@(O5KeHzFS z1CyhC&LWzc37d^$eFlbPL9o)qlri!kVzxhC%u;AULtB=Khd_y@HU``?uT!+jzegOD z`b4PSCX^a(y)4~KYB}9GRcC%?*zSxOE&{4)+5sY21XAt85duN9tP__5df>6s5K_WC zL(0j?4bL{p7vgM!))!yF7wN1Sz!AoILQ=MqY5n_OpX0+o08;WtvVCErL<`%~{etcx zj#7n4Cc!U9jaS$EBBgM%$(^gMwkxFR%|{xtnN%@YSd1werC6PJK&j!-Z5a{&MxS1) zlu&9`MO4<FJq7mEJ;3YJ>7zhCJY*1HS#Xb?lZ9q#Ep9ai@Z2cvJXog->(p@5cEO+c zU_iL)ch1p3<DPf^U`ooV5h}NoFh|!K>75=vLmjpa-1tz}l15`l{Qd~D#+zfP5-D5Y zj&MXneVDf+9gb7$wQFm)hxhTJ1aqM7Cv34Vb~eJ{p<c~5M3w_-x`XM~KXX>g1dDUw zX{Jl|@S?<`KoMX&BM}N;o&>I*$>V#{*RINo3qQ0o@b9&kM+_JNuBp1{eZQFIPup58 z9ZW7&AV(Yqb|Z_?{5j#Gd(?)%ii}*uER!LCFp|zC#q~EE90|z4=j=E?u#MbN>nXam zy4#6|KD9X|5oGyW5frXb8i3AwJ4^V&xx7$nt>9iWub;(Cow5{lJVk+j(JcIgC9JGa z76Nb<wtyU7nYBn{h#5yz8wl)JvPSILI?R;v(-RgXBPB9Tx7VMjN{lE<+0fR?sFs$- zm7I4CW`#?KUHeQ=`K*UF(mbL4^PW}LEBmj6UBkB94aq2V5C+AWmwGt8wfGC)J(+R0 zf=w0BRmP@POO)wAzp{mZ*o<mvigGiiFesvvd)Y0{vSyeu5@nrwIE;&OzfHX-k4BZ2 zCXaTUb+cER9~g5!wLcAN4H1W7R9C?V>oFVbijAroQJbJy75^o)bB_E-g``+)exjrI zoME{i+R<+$Fcl_8zRD|-i126<vlf;S*YkMEwL=#lJzJx^klr(;F%rm~vFQ6Xqf-hG z#p%*{UEC06aR9x~=IlQD=Tg7TrT3BIQ1%K}5O^s>xGc(^bx^rW3Dt#s$hW+Ly&Rkj z{#3zua4y+mdHSLac=%h;ZkR8xJ!xOu?;yP(pP>DX!7q_i(58Wem>ZPtJov{-ogQIj zg@Isx{(JClTXevZo4-Jhfija(<=8rA$t>g9e9Dnxf2usAWltgLBiY-|-Klg_`_)l1 z-;Gas02J+f>Uk~=<!m|L^QK=4cF*wQqOcT<pS7E4i-QSiPqIXZpPutEHr+`a%I6Ng zf5C;FG7r02rqh7SI1%cK<pq-#LEx}17<|BIwd>h@zV`S;^V=Gjv7SyM%wV*}AenJi z;CHg(3f=E!qXi}?mf#=(p({YnkPiCXN+itQX^*a&Gu;6RTXi`-qhkUK_9hWC1utY< zW^vLx!r|7~KDWT}&=~`n*I_*P)`y8B&&3y=JSGuA3ur&+yU%?Q;vt0D&z9P+Dc)%b zR^`r!+y!77UYUv&G<E)KvORdo%?Z!hTY9WwA~-7ATO0Gu=^&MAHbJpRO2|jW8rsc% zM2*ZMUrnq&Q+9&}8f&~18bC(U5XEhIyQ0febWos#G;d^q@%;2~XuL~B`6OMl>*r*X z?;oc{>ba@)@F&LRFIKiY4ftY*fQ4Ou!0Sl!;U67g=$uXV=!$b<g8B7Bc7)LErTs;P z1&D4B_7tqXHm<5nK!!_ynIWD{0>_9g;)Rz)(fU$>#};aZw=^(tdV$*$5m+vx(@kt% z_S$pyl>ElyA{iiQEAI+HHqu(@j6QbDRUqV7?5DDH?xZRtd1p?QH0iL)MOyCKpRlY< z7;KKZ8@Rd?4Rgo#=iSqy)q^DHfBc+lTWwyyCbk*rp;M;j;aQ*2Kbe4d_(8MX>}DGV z%2OG(to$ELPc04LT=lnicdy#j-jJm5%H$?w7h#BvG{qUXh(KYhMc%9LF7_LY&g7uh z<1L2hjW<;preia!-7?WmZ%Tw7B^3Q-N$1s(ad9nLAmY%%r+758?N(PI8ft$Jk63U$ zV78X>M)RB$x(H2=J;wE4G5c>P9{5tAFXa@D;~r+mi<7^)c!4W_j*Fj^G(W>NXBl6O zF_Jc}<`jG(z0#i{QDwDn;9D{1G)-U5CKz}-<}EKvXC!&=P^Xn${ahY9i)ZcVbdB+x ztnVU_wn?WojNRiz!gBd0lRBq%DOBdSvBEejvDNg(L#fnyRODvOu_>7pU7L^9Rs6bL zWaq^oa)zPW@|@_;OmpG$X6BqqV(I^AgAcgRy(NPduy0GYTYm}`<uw@6d#xODyLY&{ z3Aypw{{8ec*cX2=KW?f%XSR>4dvo{W9Tq#T#KzHcko}}!BXeBw{*&L@!$$_SgkKw? z%$^kY6CXG`NemM`;X%H(kibhy#?c<e(^2KSD!+iQ8Ju)X)pOSBC8IXQWcbYkre-sr z2S1k=+0K){%ifdT?|r>%wmyK%3ggYmV#FSCee##i)l%v7#*zDuH}R1n<Ei|t*?4}o z-?UPt>%~!)EfdW-bLYCNdZSq?v{{!)Wro<6(W$#zTlNxhLZMiTpAa>`?Bls9Kp-hK zZ<%5%Fb=z%zFfH+a+O!O_wrYq@EKp>eIjQ;2V~St0xo<ATy47_F3jeQDbx#SNvKXq z=Y$Z^%@(f<N6$>As>cBAX+S(=KdK?WUuUsGejd_}hJxa)u|A@-chM7g=ChY!m!ba~ z{wl3YpZkxto1py+uWXs$|B`C8+Qqk~)Bph5-2ec{|5L~P?*N0biT!_;aj$)>k~YWw zZ502YuHD$jXP2<!?Y8k++-xjFt6?*x8j)-^%opVI7p}#iLd*fIZ8s18em|((0*SX8 z=N%qkFk-QWPyE+WGttr2{gGX+oc2nwnb7QRw{m*ZOF*?OyAWKyRCHM{wP?zE#c%AO zRAwvVd(D}c;Jmz5Q9ZKUTv|u-)z;STa=<>Rnds=T03_(jHrw=0v)M4s@>y&<-p;Y? zv}^1c?>SfAHJK)xWW#Q`*}tyTIQ3ay+GYcIAWLd%LgEMNYpuS`zwDaTY_U=)yTJ2X zYk|%09sa#l-o$@P`RtlpdG+uqi}YBzUVTNs1GvAL=;XY8^(T7CcHwHFV>{;Dyt-t0 z=^o>SNMw#3ZN2gWEjnu)pBy&4|0c--T9VYM-thj$6lKsd_UjpCGnB>v%-<%SlX|Y) z21<d9QdPMitn=a-v0&>QyCj;dq7s1XgWOQ&&|iRZd6YC++iM6$dwmZQjI2dTDAlN{ zRRUe8k1Yr}s%~QWTCIfb1H<5$Qry|8_`$}YnDY59RK;!XodcQhhi|RywezpPV-Z7J zQ(OC`3lgZXWDN_uq`Kww5STqgQ9mnGx%Amgx!V!=^$%y}Pf_Iq$kf&T3bR}Wk*{HT zPPTJPX<VE++<T`!Cm(DFKWL@A!*y<*{*0PT1A6+r8h%YJPHVc1ilXuQtFuLOb5?e3 zNDO}__tyCC3nyoH^0YWCvwGbYesN?vz?{tZ@_M(bdh8-r?dEX5O+(E#1wRGue;&-4 z$v|&PNI!#^Z=kl9stGvU1Vb)-!gQzsNHoDcg>Yi(=x~pH0k{ACHOW=XzK9e+Y(|{+ zHyvRmcy`D{{tDJh<{v*|ssfzA7xL@0FC7^hs{?64+TOqAj}uH4I3!e6*jP!ftBNky zS38wgt`xrp>MwG?vm~IH+B9)QF6|hywy=i=qSs#J+*KgE9YS4~3z{b20o19L*BUNb zjwQZw=U8VC1sk^cFEA4-BmpeA`T@3ZGbNgwe<itT@T383>#^l-mEAM9x73ijv7N!h zC2%xMN1XvKzCH}RiCS%b$|$59+;#+p8wEJxuVKPBgiwl}fz9A7i+=&$-E(QEiql`J zTe@kfOPr8DGq7?{QGi^aO4IfnK!u_dDFG_{_T9Tcg_SLDXK5_X{wpk6W@~q{^GiuT z1JX`I=+Ht#t19S7NP0K$Txhy^Qg7gC4TMzCXoU(*^TgnyM*7Go!R?Ct6h|tn?21*b z{QB;FzoZk~8!q;HRb7i9nON)4Ri#%7L)9AkK#n#|y}&@m@E$E?xPkUL`nv!n)dEN- z!GB^00VC_C-S@cG&>py20J0AYs>+b~m#hM5X;>UI#~=#m4WYPV{HtE^Afr`}*(LQe zfJT<6Q^%NTEbUBj*bFlyjVXz36+Wz1;8ub)=DR4I^O*fK7dnIT(Mv^&IQQVrY;~@o zMW_d?ajHrIp7moF8`$cc1qFT(S+jW$Va3+)@0gJ62-E%91+`)JAb)hg%kT8)G3LX% zXdhLECm@N|Ds*B$;0cUGCCv*U5SO58E`_;ps+G<=jYN~t0AR&vfY;XC{AW!N&FFRS zsFKp4K(W**>tGK@KG3DcT*ecFTYGAD;9(u?ml*?|EH|Xxq5o8!z1G)E#<`kY-qdXD z^FOg`9Q*e+gCl`PF@L&ElhVVa^#NnVa0x_Ne-z9U{qoa(-Oq&vB?U+uw4g8C$ippo zqM#Knuc3giKw{t<=Yf$eA<Av{gn;UzH&cT+b)@&3^QBKms_>v>^&Eiw3VO*Keq1q@ z{fL7#{8^5tsHBRk_}ZSk(JWp{;HAB@PMVdx4mJ)jT#V=w4OR3|Ykb-qQ1uLhUMsJq zAndjQ_*l89NWG3FTcFfDW)o4cM2#bfRBh5e=Pa4Vh!DHHXR)IsLTV8<qXhmi>QSLP zUE3DF6`?7d*8DrB4q3t>?L0^5RINd?Xlw+}@UZ%aLZcxSGdiAN1Q9Kaqe`7R^|eN& zuQO{~#m39eNXMA3NYqix73J!sxb>Vi;l;w7xVR_3r>&2^I)>@v(yQes+8F5Iikdm( z#YYWo0-0mYe!DM`LZVw49R^AC%dB5T{!B%`n@~sXv++WT)}J33kU^8@DymXjf{!3s zKt+@K1eAv*p8>ZKO+2(n2;%ON(98NoCE@$EhZqq~D;}Ye`nS^ToajiiDiTQNX}}qE z2{qb4>1Awy04>0Wi;-XBuc@K3KFeOP48Qs_ePq{c?{?7H7n&z<6e7MX8hTn<8ag_& zJHa1C^kT`ym-Q^i?b)V0{m&e;Thy;d+pxt<wa2SLyrbAn(+ElJ?Rm|m>1DDo2|)^= z+$G7(!J3WQIq73IwR(`$oJ!UiD`UY+^wRRJKnObi>)(CIG*PjG8#DLE(Z;~C_P_jL zk5??UKp$W$;PKEQho8RCYrlAFc-TTr7dgGXC3q)GfPTM-_5kdC1z#gpbN=s};Cuai z$2H%FreR)DV>@jOK5}21_?KNgcdKq(AAJ3O+tvnIRGp<KyF*2D5W>YM3vE1mQ8gWA zfk5|O+voWFmJ{~;*T?jJU?=%@OW@PdH<<-t!R|#=KM#5(l~6dfPjxG>Ea_&wpyMAJ z00Zp1D_NlyUt0Q1{xIIUEF4#_p*OwdutFZayA~TjHR)F&L8t@N_T{N`eBJN+V(F+K zuWnEDh;3Alv72AIJO{{bBeqofgZLNUE3!+|M!KBxS=)(h+H)Y9Yjqp4wA*IWwOR)Q znyD=6s#YyaWFL5CeICz-CwC^hCm<6Hx8UD<a{7M(?zh|kPyNFjF#d&2k_FrWW}XA^ z1AGhKupKZ54OxY0qy0PwIjMi|!@8~~yZ)_MENNlDHFvf6AzZ)D@~u#BZ9e1cgB(>Y z^o4=T*c4bjUza^JjRL|vZ-a>j4it*5&j9$E2WWs`oUKT)o~QB{$@#I5U1E;X)AbL- zu;kK~p0NH_r#@KE`0=EwG|X%Y^113?eKs!828=4WsRu@fhJ5Cyer;Vlz?Pq{?jGXr zXTP+&oxO*F-`)p2DmGo}NQ^l)b+!Xg_7W<#f6gTPfUT-#A_^43zjOt6?`MJx*HD8a zhQ$F-yMpT<ct&tT-Rd%>Z3SXv6ooJ?Ns8LnIOG)sg?(Iqq_zToJ=`C%u=EPwn#<L) z+liUD1P!$w4r%U%22JCoPd2G#IE3N_KbCnmKk5TGIW*8X8l#br(3*%Q=3ngg3vWLw zod@|@qEgVb+_(Ymfu9(!Tc^T^MHiZAXcvLo`)j`t*S*##ikU5CXc+)*9K`l~af{aH z5z{y<XpcV9)Y3DosE_|)nn`WOu=`RqbKj%-yt|q1v{Ei+S`x#-9eGRZ_1DNgw`Vs! z5j=YC@xMk4UIa6pl;!9R+X?vhJfW>xzQAj;61u6n!nJI0gM28S;<^?Y+hKn)wB#U& zk?3N8XEV08wp1L>*xaAJ*~oH}S<}O*hl3&+Y`v;jENy+E$XD3)p>wO~2_ZOhwhs?n zn`)|1>A}$%D2GT`VaU=EtB#CMv{F+!_n6ZhkAAix6N7|`;-a@3w;%<38+Z?}cw(}9 z5|=Yja^P80Q<@T_m&u_sJ9$PVL=mb&DMwKHXTj0P0WiT3Q69So9+n_i(*Gf)KuMEw zZlXz3Ql3T@JjQf9`<fo3+qkqoNJj~<&LqhM^HSH{o-lnu42HPSB00N@B4vkDLGf4X zdX+Q#iSt~#>-@Rs?KAp<{kn;Jd0Cyit-4~Pdo4(B@(uhfrQ(eJNx&U>L5)&L&d--{ z8yS=gR>%RaSuzCIt!$|i2B%P^VltqE+i!rhja~!R{D5QrGd2tz04>CGtshVjS8LJs zmd|CaCIk9bkQBdthvW*h=mvZ_sN;Y47cerPF=Fw1{`Va6s8<ZlXu31U{wQ|J9BTL2 zT;Dh^mp8#@O8MQ`^Nrb!Ad>RtBh){3t+}E}jxBFaL4`#}PT6W1>>K5c7jYf=gl<li zF@Rq{q%OZpCU^jH)>S6r0oDAuLg~z*(w~EO+NGH7mYXd>%xkn6bqi2iHYp5(&O5a! z3dG#{_fF3+_j_}<0l0YC#WL<SD;al^-Tc)EE|Bs=<?E7$!v0pex8iwU%~M7fgHTA( zGD4H-wwuJNapXd_C~9fDC2QDJPo-Vllze}k#1A6POBcnsfreJ2O!l|Ft<^tB70s-b zrGjyTAF&2pqd?=T@>@p(xg2y&YSR|$D#){=@8ujC%|i*evd9+gQwI>c-I_Nih&gk2 zah<5xib*O#&JMfZd9Mfl&F0Gb4EHm69HGPj%O;Hf32`AK%^=TCeCk6vV3b$sV89*) z$d>69W;r;};5)Uh`cMLE_G+UKcgpJ0Zi><zX3%Tc00=1~oNohbNP#nylZ9XL%#y6R zQ0j5uDm#q@zQ>W)%pgyL-%HPH;V^#<*HXN`MBo#AjX(9RI?FK%<UwuS&ZbYU3VD|< zt;JKDjtTeu&yG3_&Q{80mn48AN!xm3;|bs)M99L7*BhXYHIHaKIwFFCR)qMcENuys z?jn;UuzLJ!PVy#amMrr`k_RhdHF<;qT*3}9OX7rh`PFXIIM5*P*5Q>EB(C3cmdRDM zFx2IK?NcO)MAO_SC(ZSh-SjTcdr59QT!%z6PR?*yZggRu;Q{kM>IY7NvU$>gB&m@J z$<e}_by#a6B&@j)WHqC+vQaRg&Tlayk%a#|R)Z1AU;ban4|Eqa{+<GTax>0!LizAm zni!5s8&WX`8=_#@AOMG5d8*_ntvLalDRz=+mM#2CmPolEM#(u4JqySl;ho?Vz_a&e zo{aMnxhBTV_t@cc)OP^mOr!)%A?}F=dtjNK2TXvFd0;>V0~p`F*<Y~yh|w`;2%I+( zEt&yLQWKay-@uV#j+@IaFsM`JoG+`{i3<~OPJNdQA3^iHwFR(ev2T+)hI(WG(cg8f zUqIK<W<&x~v+N4m@0asZUT;m|z`yz}0}JH2<+;IdT0E|COs<HPzLXB<nr|b+v!SjA zCK>j2nIc;|Hd)GMO1*5?t9c=x-VOX%l*h!(XO9bY<oCD;CusZmHc+=e%tSak)F5W8 zv$ER!J<=@$4G|#ZHoZNZWw<P5XHHkg=815z&7xszBbM6^K=pqLSx`5TrVdJsBHcCj z^7|hOwmZpCC(z_d^I-&{=1=~{qbC(x{9W<TzyhFjgzuU&Hw!h@5wya`-3`hab&tQ| z(nro=^CNK=cJte~UJCu_7J^LkHoI!ezd^vtCt%mAMUIrR>!5tT7tZAjf*B1(5ZXgT zuq7+a!03PXzP=cN@D-4}2e3on)qOG(K~zzg5h~@!j3)4UN}@H&G6%7pyK(-`!!|6g zr&#@V;(Xr7U1x-7q_R^9=wHoVJL;M_d@&>Pmknq=&vH>Ju^})dXIkHE{oCz(zdf3K z4=ibd7m2HAf_Ix0=@cgbRil7g^rS1T6a?C{@a@(B>vUYn8NrJTdpB6<FZj5~2k}(g zUCJl7dKS+EWWz!ljxxmq_rL(~Xa2}0M`DJltby+jp`M~!8|U0-LemUj-0i9OSfek1 z7vwfkIJUGokRzPrlTqoS94E-+2X8>^IS<9sp!wrceJJq}cnZ?16O!8oqC8y62|`}m z93dpUO>Sz041KCj&TYqMDcjVw7X&Vp&Dof6XwldPk4bAoQv{NDKf%q-d9ko?p;_m6 zyt-2)a+Zlm&rP%2)T&g&zhRguzllu};2)yWEjg3dza9im5HOUS$3PgN=f&B^rjJ{1 zX3~De1PU8f1nl{D1nT@Dy@4=PR--B|@Xmlj&8TJL{*yyRXYpkF4H@^OAW(%;26DE; zaM_63wG2|b#ra_wF;-bZ>0IO6$?IVlPX9!v-Dvg5+;Fm!(mV&7ZLi_Ne1DF+L9TF_ zyUfQBr%k{Cd5gGQX2_`mpDwB^1+f$`--e9m)Tu$)9#5zzapz~BEVP^H+7MzHb{CVO zo7P$Sb#(N+b?C<^{49$n^ytpOEb^J@F0RLE{9lLGf*pI{qOp5Qou9aWh_-o3<sT$M zTDUIK?w+u1z@Kz8>S2$D3juGiusNgnl>6MV{1!juKl*MpnrS&l=utF_kPYY00fpoD zW|}t=@aYtbOFkO6MnY){3vdcZ<{NR;GisCQIB|nYLhUbJmn64ic`9tcb4GYL_^W?; zH|7C<l7h#ZKqK(pDtKkcD8U1WX#;T50022jHG(QU-gR~kdCN>bqR=7TIpwFy@Q`Xb zU1#F%B!JAvI6NZVX6B}2r_%S+>o`r&E-V}}78y`|e>%dU=8|10vwXnG7;c`cYVDlj zd1eyOP2qh)nfTf*+inf(<u$Z`Tf&)2rUQ5`q(1ASVA}m<CtP)6c^Ig_0}?6J7fSS_ z0!tW&yGnh)(a;!>b6yJn{SuU-j?|5cDs7)$<&BD)24)928zmVxQ+z8*?zc^68zZh+ zKUj9x*&F^aGDqRaX46{f$Yh`8+|B0?Q3?2fhjgHvx@lxz#!yGCd(+_X`0pUWz9EQB z707uF%EIt{ljpz^s?Sgo)BxO7h+yO4ZV_-iPICY8h<>z&q?+PCahi~2qAfE!8+))o z>j*Mq2GyRmVNO4E2)$f4^;U!e*bHO$OAIaM0o`r2gzi+?Zy=(+=!@2*l(5t&J4CPq zu;87lqB0)Ls+6?f|2+kIw>Qjyk>&DqsN|x4Y`&Je*W^ufJJ0UuaHiGewJkS*<~G1E z1n3hnQL51HGlq+;Q;eTW=(sYcG&S&DrG%ph3lmD!*muC2q;>8vV>g!No%7i~$WV!n z2JyZECBSgRahjOUC^A{K!!5YM7nLNKe-Ln#F59om4O`*#eH%?+C+w~7YwjG>d*(pp z=s|_h|Au?QYUW&it=`qUa&sREnOV-B8l>#*1CS2>xeE#emnWT#@=cMK!*C;Z=i9UR zSKE^NcU9Be3+}jSn|cGdCn3`Q5p|O*RF2)D7kz^|ti|y-fOJ3x?@5OLLbjOKo#I$X z4;nvh!)j$U$X_EN9+isM52(!vccCym?gmHuDit;xI8xg_P}?T?R7pkZ)J?aroqkCN zbU!_Z+f?!?3#P4e<OGEwvHXOKp<^f}M?434DECXjc=N9)x!H7}%3kF8VzthXW1!}a zP&ruKD9OksCn+$ygufV(LPmPp;B*?(KM{gkV`6mv_{!A2&FRE3@>K7u*H_WPrFa(x ze{3`tl{4{@*0TLn)}Eivd>gGvu0yPgW<Q=JAL8l4ng;(F15ye^;y$?Ge?nq0ktKJL zN3ecL4x)1SGwytA%-J@2ENTfS7f)PN7}zu^m|;(HfGDQ`LYNM`pz6FtdB+HSs#2ND z8WlOtF(`!51N^s<Dy+--^D<5Xt@W_RxaYaTG4EKN04z+mZ08+u3K)%|A#IlDG`O&T z(&7XeFbo$iJD;WMS5^jrJWQMlxu;GK>=~bRqd&W}C^ZuJEs94EvAWoas(WQB;&ZV$ zk3*g})fAUWzn#?^kJ&qYYe{x{N^w2Dchu5a#gye7(S=gEElXVufsFjI8EYBE<p!p= z#A7KB9tG+qFkrmXM9?SNlTKnFf1P<aMcjsQ^R07;JH15QZadXM(-6B0kjKxB#zTRA zg|K86n^!jNK7zx$Gq|ChigFnHu8XVDYOJOL7)BSKQHUAP4Av_yd@~CaEFqnI+7zn{ zU)SXJy&hd;zxH`uVHD_ifk>HIUQr%G6SOciyNi(fYi~swvN39ys2SQAS<<7bF>Ni} z$XHI+83x!Wn~)kRV7d&}-)p@E#b9Jeox>b7^COoi#<>xC@17OYL`W{L!7hxWOyZ}% zbmy~-EXTHoop1Fv)y*L3gcW5^*z)+&7e2|DnAII<p2ueXYV+Bj3ZAwa=*(HhD=TKI zBtisI%>;h|I}h^b#Qa~DF(1-Q6V}e^9GIIf{%NTMqIcejGy4|%%}_><Br-1CKn71O z5cGXDMTTtIFNo7W%d8j_RDNHkk^|Hd0ZkXvcQrOJsHrQp<_yA;l4Fu5=PDqt=%bG; z1@s!mHC6l}IVRFa`fIdBiJW5JxWjqk{X}tqn$x+jj0YpPLM=WsQyv@B{)<&HPV8c$ zog`kC9UTQtw#;*Dr>g~76DI)-t3(1)InyROWfO{PT)n|%n)E`5^Z8GMA_nMvF_hL# zn-qUfRi=uq6x7H)$w+crgW5|v&dJ18`_%M;2Wds(-YA#qDbU00CP%&@hPCfxMz=xM zZ#j!+E$}9}h&zq69|in2Cd_m+SNkH<#FnX(-P)+fjOtwEUs<n%`(LS<X6?Y|YI2!k zR9Hu8!vu?uiTEPQ#6b2j5F97Ph|a*rCqeuWXrx^(dINYqg<L|FJQyUHJbLs+4x+&+ zB)Z>%8mReH6dpR_@aL+*iEhdXXlLbcm%s$GUAS``zvci?HAnw&S8nWVG8EjkIWHMb zT66tTlc3ckTC7nmGlYh~XL^!kb9zV6q|#OvWf%gpu76J;u;jc`36w8yn~Fcixe17n zn}mg>q~KV7W$Sfs7xz6bG2KfiIn3b^7C0s(g!k%%``~XcE!1oYhiWliV*tw0Y%v$v zU^rS5sMfaXGe33ww2%`I>f*uI`LzU^xq5#EK<ohq(}orpC_^MdFRINeZ_LeETBE&b zXU@9#Yv6N5Y=q|^7fz{!Mm1deNK9bV7YL^s^Z(Sz<}*>h)Vz4W7pqSA2N`<ISP<x$ zM|%Q@e0V?2<_A|-Tpq7m`W3z3&G?>62R%CXF*3%wt?@qjmC#gCbE7o^br7v{fs)Wa zyfBo(jzl1IY!Ii~rWB`vu+x1^{5^pXZVxN93fLbL`y;MeUsqi&%V>M7{Qes6kCr!4 z5Y4*gUjC0(*<A4(Gn{rm=CJPSQ@yEUa*im$SrkQSJN(K(o)S5Wq`f{QRy|#$wBO(g zlX{2hCf6x$UUQW{Qi5=<+5mlxfjkKfCl`G0V>Af^_OZAurUWj_BX>fucjs?Jkleyz zvzutSS(h!C-!@&i4$5L1+xf|S_SYY#(uA3-@nmsXIH=^}J9NM1xKWrV7JuvK<in`z zGdn1MS3r9eRP(KgM8!BxHp*gsYT!E*m|?p>H%!p&Z`YzLjbpbgXXz8xHD^iTrHR<8 z?3sD0QqwmhcZ*yUQyt!8Q0-*0LeAHZ?>+az{RsQf1P}g{&VVypmmm<N9JykSvhiax z^NMNm5|UJverlBE^OD?*z5Cy<FdM*i134GVqs4-PAuE!R1Mkd}LE9H4%8#vm{aeAp z^<l(+bxv?|bGEd+uomqN`g5cVT5G4{nYK7{wxE}nF3-Ic(cGl%O?+6sSY@zpp^qhi zYRN5_NgIvhYM<K7u!f^^5)B#hLFg-Of{J6(Xn-Y-+vdhiio_I>DIbFhY8(8N)Ycke zx{7=_F5hts2s2~pRJ@Wz9ad>FUQ<8T24V&|Nbn#2e<1t46FtaYZbA7ejnXD`e6DW- zju2B|*ENIVXMcQYmX{)!{bpOBo|J{&*`)Xmhzk&U9i=4U>0+urjmTd7;k9P#P*AMN z(<0wnrJ*htAE8mx0OT&}(Ujfc8NV0WqHbK}N_&}6;grb3V=LK8+aobj3_Z@GekA5L za{cx0k-#ZRkCWgH`H@%K5D8|bPuJd&KecpUY<yeVW^C{>8s_5Vye?b}dRac`VT?=2 zD{(HCKOHTM@5H0R(Lw!qTRCQxdh{paEcyr?#BgFt6EcDIs5Z2YWn?|fO@ytUx;>Ll zjygT6xs&Ke?I4q=dXSej=5f;i*zjQw*@dR=M<-54GRNV$^W<Ba^?(E%cj&9+*gU=m z`>#$wy=*vq531p`(|w@kS+Tc4i>(Z(hDB1qKI+DP)H6OwPprEk-u&q`ACtMZG1&h> z);acw0&HEnZQHhO+qQAqwr$(CZQFMDY1_8n^G+s{+&drY2UJqEYVXuu&l=iPz7e`d zD(j3In#*8A<@_j&aFRZaz<D0*TDUzV^x>Z()|%EG>P1zSoYj&`%dDaYzbYdvb)Dy` z>D5-qST4xSdom(MvuMB(r4cJaElR2wy~B-)o{y2UZkYw+Dow~n#2pBxuekb(Pjfy8 zUtKJVIpW(LAgQNu`%bYU@qNinrQM!7{YrOKGCYux6Q8V(Qg*=XuOBz~h%H_RkO0x` z_3uFzeWrc!B~q@hv&0t~hneH(N<XkhcEFCFCWCMoG<ByQC}Zx1>lUsQDSS+bZm>^E z=`urVPW4$9seEbS>uFvF5d`JQh_iz`#itH;OZldw`nrg;>OHBRwLS$lz*)O|y6@H5 z9qL@SEVCWQY;wLrNSO}El=@<ZmA%PD^uK#j>easxja>_a-QxsGHzHaZmP71|#6Nd# z$e86}3LWlZq1PiL#M6Z|9GpR*Cu!w9h_xCr=3t@EnSu5S7^J~6BTt~)jAHI_ayWDP zms*_e?V@*zLh=#Pvz9C!rnH`2<RWY_0e3_<FHS{qHB9i8RsR0N1MV{x{CB(o^iqB( z{M9(?_Gt#GaF4=xi9iSkAzpfm;_-n=zZ&gQ)FDEAqi68zZHlyNH}CSK0gj}AoyBKp zJgx+#Uz~X=z@@pO$2Yz+CqwE(#0#$uc&<05eC|n5i)SkwS~xA`ffB;1RVj9{SJS9Q zXC5=b>CC7Cw2_&o#Ov*B?TCyiG0n(qG~wWm6a&y#;eoN6{_mXjRLG8<K~gpvuFMUL zL$vx3cG9W(hSiZ-^^VC#f1Wqr3o_H}MA1Qw+~Gl`)7a@s0!8(2cnA5SG`{;NCNyhv zTQ1LdwSdAoXfDhN+!LzxJ7ZIOGlVtJq6=_RzfN>o{G<AYtXJFVrMrI%ou<0)3@DUo z8qx*}RfdYK12Y{u-HbdP(&6P1+-iZ;>`GN=tMdYSy21|qVaVZ%!v0&*g><$K;7kR& z$hDXyK>}K>s(}@$%?qtApyiaUhSxhHbkHq06@tFQHB0X(c~k7OcTsVNjtG4i%rV>r z3IQi;*1bui^EvmbW){G1)^ZMC&31J^)f-L<D4V!7Y}8+pw<0#HN-!t1vYsK!yfbQ5 zbhQ3IBvYfkq7<>%7Q$trz%%J1%2%O_5aL3qI^)Jz)>>`&%i%s5xS#F04tOC2GO0Wl z$0P|DxQP$R5nw)IV+C}$$@lqVq^Ej&w6o=O@xKW-o$B&FS)ZXs@#(?fe#=1a*>k&~ zh_dc-2f%)PuyT(er>uJ^$Z&@J+M|!b@3TSlAMwKEQxx8ZPaLLIT1G9E0D=K+ucI5} z!m$`}@S(#6VRMR`$xzid_qEYB%2A6JFR7VvIUY%rCc|g*F+FX%z|tQ%@77dOY;zXf z+lD1X$^P!NZOSJ`?zrMcRhPsKBkWIju$Jqs6$cCs1QO)jpcL~_9X+3GMnYpS*YM(q zkpvZgNTTlF8UfJ4xO@gV!72XH^_%4qY>&)?oGJ%mw!zl#bGrCkPtcBw);Vj0M@lv< ze0aN8*;9Zl@G@W`>Q`!{$`E3d{9}>=igEm}<Fe|a`3RM%c=5H~>ZAmHA}NZ-%9XP4 ze&K}<d*jekJ94;m^mbZS#?UnZ$PLbs>a0jhYX$d36NKLwpF0K8iAI~5=b>E?b0A6z zK<tLeg}~1ORSYvBM7P)kq2Tg5wj5?A)a&a19`!Bs(VydgeDHohQmOhr6blOkL)o5f zZo8X%gFwU#2O(L2rUD?D{Jmdk6^XLC4Ri@~J)%)1nu`_o=C-3Gd)#M}P+CTu@_gVa zoNyniCjP>iQ0Cx5rc57px)?CM8)1)H+6((T7;f`OEVX;DsI4%|f^T7&+QK~-2;V4p zAs37s)Hw-%5qmLD9-Zh2(yWz(kXP1vf|$g;*x~Xup1y!^^{-hxmn)VW+Yyvu$yJh( z*kxg0razPB;sm%3eJTTKxVpl<{vc(!dUCnNs7UT&@4zECC(Mn(&qRihqVt45n#->^ zK&!%n9m~o(PKrlNoITp@2T*(ec*JOKdr*g<@i~$Ej5D(0Zy8M#SBlNyC#;@!c_j{$ zr+_p(I-01!!JGxfRg^4|^Nm>!cT)c=G`AlCVZn#To0?B~I^8>hoBN28m4c!Q{vi|? z=fYf`9wE6L9NWEneo9^;445o{eX*>sF>uN~uo@)AM=I}W^hNK`sxFxwki|`9SktNQ zj94T39piD>@;P{KT~9dvJHBt=&^RAfDQ|%*yxuCCL2Y=uU`Pk%NTI5#Ft7#?^E-%N ze|9O^9&FGHQ1rz}FMRn5mX{M8zQh$Wln^XUj+?c|0qOZ$Ky_C3*k;;l^L;7c>`=3> zo8~Igv6)GwKcTzQssZ7VJv@Q0?`IT=#?3&fQFN;>Ch*~FMrC4a+xpU~em|D!l=_18 zVD7eOm3PQ)hbeYonSc0$1HuIDoFfsD)F$e+mi#<eJ;a`)ucznfRwy~GaXXk@J??9T zt>tscStZK1o8sOPP>UA?o@Sr<QNC))Wg6Lh!nbW1yfh(w$VLVX-402-QQ@`oJ$FQ^ z>lxi)NmAJTvU8}Z18PU2ZCmq~Zw$EnxDu3;0+H$Cyh(|BR*w2NvKmt$1_M>|UQUrT zSS8@+rguXf^tcZ+zSz=J*^FaUhAuU&vcdfl%7i~YOO9!KT9l7!R;J%-0Y2{C9oE8( zvN$$A&!yqtTXdvnW#H`dsVJ2Fma<>1H;46&UJH<6-Wh_-#_6se3Dxj%O%s&$QY*c% zkA)ttrg`$>X+#+r2|QaWl$%kC8`T(IW25<{(qiWZn&Udb@&OWe$`0Np3hopSBP_Bc z5bzfeFt3nvVxF~=%=-FqkpGf~c`sH=zv9R8j)2Fa%r_M!VN${s&;dx5$Y{6+mvBhl zWNj>hKCv=!6GJa7%ol6@{$IHKTe%<aA^>Boc+rnO3nppWC^a(HMie3Zp19C!n=hbi zAX`)IP@S<~3?Zgjd0D+nq&UvNx{sHjmo6i&CSGhsS~$-66NJSb&*KK&(x!+>|DkJ3 zGd~+fE3G)#fI+UwL}&{)nE84BojhysjJI;4J)^8T4Oc1Kd!Awtr-t6CU@9k9%90R# ztzJ8RoSn?7vt%dFm}|v=bAZ_!dzkR+G}R7=Q4<E^Sosqo`4__II*Z7Mwa9i;7T7jL zOSn?XK_yvdJdo?sRl8%hFsm1M&sV#Bz(++{uMo0zyKq<aQ=UrriLj;;!)%mu-7FYB zTUTj9*&W9E`2?!WtAlgC4cd!8#^27F)LG|+>oCtMH>6bs!@tkUNSZ*m^5#eE*DS+P zVngCWN_9zS{kwLsJNTktM2OBtfePM#)+4fpE8tk6{IpjWoe%mj2S@g-?#_PUxJiW0 zJI`9Km!uw@`^r4q%nYFmbG_S8h;ur6ma@$7ut_Ls3Gi51q668Ypq5A|+T15h+;f3B zLC4RjP~HI}a8DKShwbfME0Q1AS)_SU3Ss}%TPY{vC&5_XRx*>SFZ<hBZfnj#-`jk4 z&}uoBlNKe?Rxejz{XF-e+wjq$<hIyxG)A6dWhefkyXp~WH^w=NjmaW4T$282TsT?4 z83fHl?F6bW0_Y+U;_U-U!pb877a6X~lO|@~9zOQ)auk;^t_PA4H)4E)QXbbE<u?(* zAcx~f634*sH_wt*MgWmT)#T2YoM2GABGz%+xdiJ*=2j7uVhAqv4ltEdx5m-AT!z0i zw){c3S{@fQs5T3CfRIJ)4_E@-@A45Hv!syofCQRI8bJyH5m3{*oW5zq-J#HRmJdRx zbOpN^Wv5A;ObJ4d4<dnz;rB<ZMOyKwA!CgB{)<D5wh5DjB3AcCmE`U*a}PjZ^)L^u z!MC<0tX#`M1BD8n%DkN(uJ5^m4r*P6O2@%a?BWe>85G|69|j;CsCIAfO=yX(8o><P zi`Ivm3-kV8K!y{53&+Cudgk3U`m-APK{c})Jq4eqGTQ_tsM!+Xh{a!83C84<-j=K> z9bvFu^ZIsm#(80f6)xJiZI>>|#UTMHUFS!i6VuevcRobMSI!tJiT$7slO#MxHXPu8 z^W4$+CrhhVY5?VA-r2WI8;CABsPk%uI^pDZ6KK}z_N6O?ZpdE%W#^22yi^q>ygGNz zsU?9i7uPmyW(i*35Aq8mbsIE(BJ%PGF60Ht<uu4r!#%%=`TbVGn%Y1yk?nhoV&(#3 z#_Ej94_dz0*7*x7$mpll0!DBD**Tj@x$xsp?I;0$^dfGJ=1Do%iSG0`96fD$*4+bT zJ!4oIJ-Lupj}U4=61lGRBISqoJ=(3j07j-G|Fr`}Wf`+Y9cEYMGH<OmwtZc(u?ufz zCqFa186y{WM@4%n<=%J%CO+PQP+FSH0Xo7`J`yM2%g(_CZ@EPGZ?Me$hQL^Z`pOmS zc5d$5HFJPSer{#T>TwvR{xXvehY`BAXPv<g!9_Nl=-!kLZi;=j0|=u@=ilba_qG>% z&gk~pUWB4JI3>pH1T!jYn^Iu78+0m~(Y7AKo86aDrn}gF1qiU}x4S>Kxb_jC2J3ir zkgIKjlKhNqhZ(Lsyulr~-UaiPes1999d!t%OA+hJJjW&oEHq#n#-Zo+Np=&}-RdK4 z$b5oY6bd+>Uiz!m*fL4m;xEFIr+tfUgAkeuMDKwia{d-%8Q{u%YmXSg$;gdFwB%cw zoZ@;0+<)d?H3p-$p8-J4H`y=6x>vDkuT+J@U7RWYcGoy$o8_(StESkgi{|p`n#s5I z{hSfZHer@5egL`v)-=3?(SG^sb3#=XUHpRor-QOH49ma(1OVU-3IG7}zs7R^4^+(9 z+{D=G-*9e<nzr4A7=rJqdbGfJ>xBkXT=}WWIiUqHtK`c3O8}8o!<aj^*V<oQ^8Cu* z9%d`LP<uI*V*kUO&DR;5u%Z1lq$YHV=74S+stm|5`$rz|5~pSYs4y@~7Au+1p?w1d z4BenjHl-TD)fhH7Qzj?IFCto>xe2hB5Tz{+6*Wtf*9SMH2urhy9k$J4rxt!xdiOdg z+@Z%-z4MC%IO8|rbPUsU3*zk3u`V?eW*Sd1noM<;2)*wfwnEC+lhre&AR_9K%E4v) zkslLc9Zrnsy<YzgiwRNVVn+q5Ac0V|zeH!NtTn(j`3Q}cuw+3SxA1+X@UL_5?2q*P zWpVONn`Un!g6br2#9%Q?yn}+Vpns<AG7)`=AXHmnpHV2JrgcEn^Y#^~v*G;thHr5@ z05$!o69y5UPIsK?aqSjUydl(<Aw;!RBk|z?1|AX52r%7~`XD78wl#>x6rpTm3V87M z{T1Hf+E)IMkEwR)IazWaP4LnES|Oq?h$vE;K|X<63gdb#7z^a(6KxS*lGPPjE`%zQ z8@I7)Yg&8X+Xz(VVmT1hsH{S!7`C6I3pZcYuM~gH^@%Tbz@V>Y^j<AWH~Kt`;;mbZ zvH8jM%xij6^qo<R;7{fyRgMho+Z2rRSCD+t;K+&HeGy^jY3u=G19MKqBey+EqCQL2 zYH$B7E@Sb_<dxk|wY0mGnhl_J(LPp)RA0<zIS<uJ=<%rHe^OLnSbUK>*IhSj&|E{) zF&Vt>sYbA0QcyI4lXH`c10EQ9L$lep2olpTy49X`qs4tM{9{xi%i5r5k>Hmc9Sc-~ zd)m&IQj7jgL)%sDV_*m3XEYYT+<6>$l#k+ibd)}UvmbMQ=*Jq4$_gXlHckHC9{cXK z$Qex;D@6NAcjJ@yc~L@1xQz9fzN<j@YM~f#IjEud>uk&+|52`)ao!c%4v_Buoi!1L zI*okixi<ENb7OSV=HX1l%bAqhoy12!>9%pVL<aMt)FkW>BkR|R8;aFK=Unw*0bmUA zT3<*a`_3V2?1&(m7ENE9#xPsP(5k0;f-BrG7SENlm<+-DaYhYBh7Kb`hml?%z;_~j z86bU1gihQ(@%U)sM6l2B;&YL{bEH(QuC6{niXguHbNX8)$F(8}pMMv1^ydOM)3)tq z7BllBYG7~|8g2V;l-s3U#}jlsNm*Bk;pU`b1n4ey&GzXYLwCqwgPhjYgf?&R@Ix=# z*^>q~Iqjg7Vf>OOgb@Ue_#pgtYWid6zMqFIs2Oil)EPIF1!Jo;9avBdBxf$MgGAS_ z%B^#!;-4QNC)2NKL8jLIb-6LN2i7&gn|3LTWsAakE~3wd6xAPsWQ`(266eqUyy*Pf zucNcTUqqf27VE}HrpqwzHtKh}y<J7+57L2a*`n~j*|x%<HpG6O13Huo5J@SUhm%D< z<g`|6HflF&dCr?hz)b0&Wcx5LoGa*>JO%cC#11uz+zSJK|0zp)ld1d>|77XopSk<L zlRxZ?EX^DZ?alv*QjYqj?I9b2&zd^ItT2hdGdPBMfHfOnG?!$<Wg!KOaDGX{j2M|} zqMQ-R=WWjZNGi<+Wo4K~@rk4R$!N4#kvJjs=w)WbioSx@5X+t8lsapDiyHT6hT3Fk z-Qj|HGKp=Uic1W)kR<a-Rbqs0QL~9MbY^l=y)XDrr6F$G%e+4%+MVvWtV%YdvD1rc z^4l6_gVd>Yubt)i2C|(*YmIt;7^ua#A4`DvzDwd&zpbjOih&d@7K?Jc`b(2jZfFpS zD5<mJpXlxOZ^?PM$y+Sm!Fbcmg4$rfmGZ}oL&4*t``6RY_ow5I^xyZ*AvYB%T=heG zFGX!a{MW?<-mw`T!=5qdU-k-ew^3=Pp(j{LJ&rrnA{(twl&zW5-AMYTu1|flrCX=s zJ$$osG~<%V!uFX%r3c*RHItbb3p<r`G28E=)1y$?k_MergLEzCE>PfHbTvm0tBuq? zK(qlY77oIybRkMyXb{6*9@$&nY_R#-<Vs#R!Wuu#wa74a;}$;$%KK`PKfc#7@W4Nn z5b8C{)Q73p$qjE-EgIK0w+Iv8Q`Rr`lf{6+tg4yf!22O@Q0+Pcx&Z|PH!U!9#H8pv zAqV(=|0bWaFR|sRG?)#sY8be$2vw6L!nx0b-KW;&`#4<5XI=XVqapTX*bO8(o>x*m zR<b=3HJful?I?5wS^|KI^dcgOQ^?z9)vtu0*MiXy<9~JOc%!jf#qk2IS_2c8Jl)LU zNQ2@=T~x;ClGa7tdgBOF<AUZASWB!F+yF<KN2U#XhO!&Evf{NXeon@O9{-3$366P) zD5EnQIWFr{U#}B6Tt$`hoTD_Xgh|#_E+j2M+Rj#@YmO)}-5^Yk5jkbab)#O~)!60X zUF0T2QgBj)8;sX;bG4#{ndQrb^~^gVD5w*f<P(G9ld`04V+Q#*bnuziIPWS1@luH8 zr=(nP^fc1#)^U0fXIs+HT*-Kj`i}_<*<xShZg5y`Hn;X=!Brh8?JbOxj(^8GVtWR> z=t<sG%lxQ2#LFXl!>wvhapTe3?2i@f=7euxE@Hoc*H5+-yV23MoZFK^4uRns^A<?l zeqz_X8q8J`4mYZq)-{wM<$+zWTA{Ex%mWMO7sad_R|?;NCpH~Lwxm(y9xi7~6Dc09 zj#fxU#;9P`dXcmb-N39MXuc4s&wwqJ6Gcx^ptgr!u50~WvEc)>=W60C9G^HLB~idm z&&GeP?&PPw+^f$TAFHxy7>F8YE6K7UukvA9B6SvA;x2uhk2$$;YTTo3jTt#G=ZF=_ zhU1{|3$}nsR4a7sub)|p`+cc>qMot&>doIS+1-ZfE*-NYM?#4l*>)((WVvh(cuZvJ zVH4;ng_1$8=Ibj-bf?~STO!A?*mq8I4jW;|(ktC;o;Z>$w>k{ZvW-mqllan2zQV(0 z8PdA%y4lI*eBVi2KR1XKEmtc(cMSR6<k<3vh!d&GEf))I#KOhZX(w`QhF#grp|a74 ztR#*-nd>?sOG5SBByF;RIG{0v?Me@E50A2#cJyDlMOa%3rp~|s0N4MFPH6vCR5`gA znYi0K{u8g7^ljTs0+g}aM-+;#h!Prj1&gFc*BmmJoaR4CrIn50K8Xbz{QzYXoA7t& zU|uQdPNMx^e&Bh=9AU?f9`fd~_KF0WxO;=35|S7x;kbyU=3Se_laA{&X;UKy6dDoB zDaf8e#YZv}PXfi3V3X8+w$nq6xH`1)KR#C~YiEk^bqnYe@B}iySl=WHN3Wi}k8QR) zmQ@$4>N`*MiN=B?MK67-yQm~Vu7N5pm)O~W>`8sfG_@{E7}{+wsPvhdZX{M*Zuy?u zb1Y%f+rN+$5Y_2ts?nrCzJN7pC1#$yVyf&co*V`(c9=5}xVF|?`kdCBYtjRga(?~C zgkv$9Q0zzW9G~B1MwJcTpMep^_iBBI$g}}$J`sm#q<5IXAPb$QyRKgZiDxCWXi+$^ zXCPF#V1twHPorFPf_A%`EBw!NKD`ZBPBno$1v%WCAn4($MtKbm!A&sjTC8(xz(6Xb z=2Z<2dw(md`;}rVKcporm8Y2&lNAaVok<Hgw3_fSLp-;>WZI?{T={v4pGHC2^s4(t zMaivgmJg^{n`*wW8nDSn%gj0E)M@ThDCi8yDvSs2bW9~lO_jKGP{%Wxsic^2g_$c% zKDuc3n3AwiK=%{JF?W1M3~Sr$203{THIMBD?{x53_w%Tm(C5qz{~4Fv$a<15X;tjn zoodkauDG+sYnsQ(sY_pLf2_aLFi3pzBHC0NAz29t^k&YpGmS@z;N<Qp>gjnw@%Mko zoq*ELpN5KBFeOo{*Pu0{y^6UGBMzFih^a3Y$^CSLx~Xhka$P0T$P^#@bsx}1oKeQM z$W`}xxxS;E^{dbugJs!yweo_*?saD3!@G3mir|v^DH`i=7parhJoo5kVvV>W32O-g zs5fQ!pjIzKs`nTc>uWW_3ppbRXZb5xYHGv_4SIIQ!6Eo6DcukF_|ic#p1uwWIvuH? zPAEd#m9=v{#sxRfLY|Yyw$;@=T!F79y9?OGFZ#_-V;9NzG9Z%zKUjZ=J!fE$Ns!gv zNS&M!dQF8_(KiPg*eQmH=Xm9P{zx}yI_Yn7V^7sz#ck?&KPMQ}r$PR}{oQ65?$}+x zx$heMSzF&R8h-Csg4PUsQ5pt@qmwQy&wUuDw5Buo0&ed~j^Ot45=A#VaSmRa0zEY~ zE)oVVC;b8a&nn|z@ELCLzx#RhZ~ecB(Esa6bT<4)RCfM%O?hJn-~t6;L~gxDNbt~* z7#&FuwwuI-#-Tvjh@}5w`R$r8zAP5!crf$AD!3j>Y*pH=JV<0pK`PG1>$mBWK^TjM zqblWLtDsJg3_k+C?&<G`0c1kBVww#G*}RgZP#~%ds3m(We9^R(n*<_jEaS7k<{bA1 zn1y?FY&<wf!|CKy-`L+4ZNdVgDKv-}zc~J;Csj8sEiaQniyp>F?sf|#(*NhSY7T}o zq5%N_5dKNff6LE5B957v$^XiakCLJE5Cg(@uD*kFv`ynRI1DaK7*RO>)-XH~F-sa- zIvMH2*2`^A@ys$|s*v!<&9>KVEk)#tDfB=^rO+wiG7o?ogjXC;u?s=UfaWoW4+w<y z0tCx3q@``JT4^S_=s>!2Yk<Awxl!1d|0uN;PkCtzU4IUf4l|p7+V~`|{hb%(3Zu)L ztu41&Fws40$4}+kP4)cHIx*1N4wA_bzrCA+R`OPa?i{P()$_iCPnW0OZEKZRd%%MB zXm=a{m?AY@BLkU&tBB9{`dz5+=N=h@_ULUOATQj|4km^!{I?^BaCm**aT?cX464^R zI&w*?MT3ft*>7@lkuw}C*4~MTMKg0uUb!;50-lsq8BsV9fb+Cd>UyqqzpZNl89|Sb zWCWb7KZ?odsp%3&$gVoy%r=&fRB{d$X#^rOlcoJQa3m$Hh%whZ2ySa;nqoH;>oe2p zbZ4T)lQ1ctgX1`0jx4f?-_<K^<}+=>v#XDt!zMO2{mCyk(;TJ#Y)#%4$K_`SIr7VY z9m^@FDewq3007X5|NmIJ*#9wf{$JAfijS2OHhbfl+jpd9m7^pV3o`j>(kzvBBtB;+ zyMt1zlT)P%)oM77l#|S?y;*2#hvx6=rhI<DK)h1p=YFFlI};>}KiK@OEsw{;(b_fB zZRC}ydTrXq4BsZ(bD{ZWW9JN-n}>^b^G57X3su#@hE39tU+PBVI@(HOIhx7Btel*$ zP1OEwQ|+uPk5n3ao9LRUNy^H$?!uoe&kk+lq^@r6jib-2((#9tN)2bwnbNmqRN0ie zwNEqrsn%{ybXzq5V1aS|G$=^2$Ddm3Ce>|gEn5Y4QMJ0WTZ34W7V}71MVEIj?URwx zS_BEkDTIE=p(y|rRX2a2vv@bmX^^szda)=MICrh+Mh8{(IgG(!Azngza`InQ7Lf89 zc2Jmw6#U29;01$N07&0{S2J>Q`KH^~jj+OXqE176gTk!pYXpb-b=ZD81i@_|MU{c8 zEZr5krq2VCsSC@VxtoVLP$S(JrqPAuR1hnQEPG<ZV+dh1G?)NokoM+`sY2RJCjGQu z3JBzDUnk`{Lnk|X>Pu9A0Ul5@0JePgsLw7(hcwy;D7PNnwb$ST)?7pYLWq7`pUfUS z9vucGXO^;52LC0?NSAxYx=!mXcHO9U&5lPkwW-DcWiZ)mhbfc9rYqgvJk#UIf)!Mp z9Nm_FU0{xSOip>`$1)PoP%X8WVtE%&QaK4jBR@^ShGJceogi<Yyjve95MYR4uzEa> zT?WkcVxCP^lQImjvu$5(x;reBEL`uPaXPHQD-(7gbC;<!XjgOv;y2N&Gen_aADT4o zT(JGS-e8kGAVktEn{DR}qdowFLpORo8GsOM9#B_6PZyIZb&t)BefOv{k^P0lK0|D$ zVQPRJle85A(^7_{9vHxQXV-OjgD)TOH^V@J_Q6}j4hHxzPcITgQ2?-jt=5gk-mrwr zO4>A0;8Sn-cCWBC%<-ktk}j_VUXzEH<k+CitvWVQH}%5`&<o{7*QFluKX%L<g(FyL znb3M>3}<%j7pOQ`>3NAVWP{!o>fBoseM6?n|CkBN#|%jhj9nwM#Qw})4Yp=HHxRA| zJ^ka@9rf+Zpal5xklV6OeyLqxKS5<q;Tm)|kt5*d%u6Qh$JaIsJRy>HfD`lKh_C`} zqDvFcbLV)I`rd60QvfWsJTvkDs8e-n^}^EYF@fRU*!_#tA6WAm-jnRw?P~2eeFuYx zm%A|-J|fj9b#oX?oz(Wpo#qPdH_iL*U$#}<o0z=?=&EveC~fu8zCpU+C@c8;ID{`> zS5MU<B)DgMd#%P_CsANG1Y5H%>O$!IV{n)5fb!|U^M+pXz(dcS?ytEGT`%a!Z5`K^ zT8W3)>;7=M5oV$g2>trEsgCsiVgQ0uZO9vIEW*3SN%jw(HhbV<a>KldWF?WEFD^X< zCu;{d_QoM-<BWNoKjB8lqN3b}l@Q4QbT~3#b+|CFz`)9;K+-CF9D16n{o^d~UQ<n9 zXr-{H-FuG%X0oHGuDpZTTQ_W3I11nlxBg>CL+nD}D2%6cyFjh!G~Fytq$Cq$h;eWx znC%-jJyvKGO*Ho?YS5i^!!S{CCOuXrAb$(=7q@cSQ6s6ESDLQU3Iz=z)|3Ha?R}jc zF%ol^aVGQt%%RNcja@-su`0L`=SnNEk6hc~Pv!w}v=(3|$wKoxMf<T35l0$_G3QYY z#O$?IKxZyWd$$R3Foj;i$mY>N6UoR32|W7IbI>U@MBG>Oh5FSIC))2Y6V?5f9-5sS z1R@Nq<BI`az>2~47&swvKU+zdfV^UyruSvG4@-zAO+fJzP7n%&#+%wk<iIHTQf$Jg z^DnX7Y-;`pCDs!(Rl?`SYq}C<0RcoP6~u2Gw}C<hfdE1N_2Ffymf;-5+L^8fdkEsa zv(sP(_<(3bva@wZM?|MUFO=vA(=7lNC=Ol+8D|!$dw>t`ZUsVeP<NA3cR=7KP3>{m zl-Ls+-K`&{OoPDW)-8%}dy8p)jYRLihSe;DmT&r~5Wtip52O~X5KN2Kmtj6IpAk3g z)r<&~qC7k>bb*p8g@PKaxG3363+poz0tfjMv1qsruxta%gz7)iN~4+?V7Bo#*7@U) z?$NY+W5xlVFGs*=W4_qw;-AqCV#;q=13I6?T(cIzNDVSL6Pcc;qvQt!QoSbOYNcT^ z!fvlUv_h%~Qqu77Qi}{>Ofb_GV*o~H0q%qO>+yp42f158W-X(n+pTP{M#TirO+-X1 z7GBDJ>8Q*8gyMr@6oT=1vPRp5%&XSH8Mg$N!+25=$<x$+_D!fi73kG;KkghEQZclH zq2NQ6v?IOZ!Ug(MreFgoHb*A%?hs*<YDG8SAJ7f|*iQ@Bf$zwRBDumcV5rYPD)bUx zZDT1pK;pEqR*a<Kcm_5ei-U?lnvP4TI;5Z(AI>OAoJ*V7$On>WBez_)Nt2i!dr9)* zj-tMmW)PYN@)rIJ%1D;H5~d<rOhDX&xQmioWdMy6dfI<Nw?%~ZuGf}Nh9`hhkxBSP zHXE>_wZoJ_d#B2+9$K3(AXwYaCtmKbMQ??>dx@Rk71BO^ueL&>^lyAHV87;(uu@}( z=H(0Y#?50Etxok7r;%5)9ym`#Gq1&nk&Ny$2h3c2h}BNe4uXVTy6Mma&~^>52*T2Q z5by(7y!sBvv0xUC9AOoB1tjb~8<_#^L2~_P>zK{2U?7f^_Xa!5uboQ(;*W>RxLLdw zrR>H$<jk7QHCki!C&;iOdONHA#8m3CCkJU%;5+b5xiXmvO%M**z_7~1q!7-aJY2%% zItcX!^=R>-kh>N2Ns9AkS?&RrDoj|&r8a|PlaayZ03_%$tOT7DB%JLI#Jd7FhIX0= z8c+gsTd*BicCVT4Hw^rKzD|_qGvrI)XRsI3XkT8yR=QtE<)M1(;S%N#MDou>5;-}! z5kk54chz<mzaky4ChK$PG!g^qyDtp&bYt2vQ0zgReRkPK7tqh`HymsC)f!ZAoW;u{ z+De!=@63+>YF9G;Za`pMvu|b(z@mSaH&cV~bONp#N$;6?owUE>V9Z%?<fO^z1F!Eh za<+^B9ZX=F`=ov^sS*E-uld9EHp+6QH)mf-UqzAifj6kxq3sQg9=*QL?<M@>TAH2S zjGwy~;IGHTjK2O?p88`O|L>RY&(a*d-}n8BikUp};f*`hzU%mxInu2J8VjxFk>xZ} z6u!HgULgv==>hCb`7J?XYqKBVT7eFF-JSH3#1UvzdfwjW8mZ9_Yk41lx75~mqt=Q) zd8d`#IN2i-2eQPg%MkQ8ra8btrADXk)(7v+21{45px)3tAlQYCSzGL!fE8BtU(oR1 z5dF7K_qMR1UXK9%tk7JT0)GTLsfoeth>`9?PR0a^K)X{pXMfP~yfg+K1}h?)chA^N zN0vl{o{q&?PKGUyE)5xQU&jkZt)^!l(V!5Z!f5f|bmA4+R#L5Mn{#|76dGuW<%hzc zTLE|)r0oPrOZ79xHX2{OIvj%B!0#6Ls7)#sLbJzOG=d1YtzlYZlF$xs_8H%<ulwVu zs5bi3TY6sxT{j8Z7T^MVI1m?AU!ZEcHIp@bnD;0Rt9EQWM<QTya(>;f*RO}~s<sV} zm#@pZzS%})BXKh+jh!CP$D4=m<6j@vK5yWe9i1<Vv`^fpvv}*6Yg4&K`lfqioR#@X z@}(=uIDg)Z?Ck7#-C;d@>B`x&c-kmcw!ESl2R{6^5v&`msB*K^VA)2qT@$;D%00O^ zKp@|URH@cG@M*y<5l%}pRxNeA2e@FR4=jyXp>}gyZFqQoE++U)Mhmhkf4F893eAI{ zhv!gqah(JST1skbo)3U%6q5ST9OnKD2!2AEbH7n%onueE1^1>)$A+EHpQL>L7>`t4 znl#cTf`kh^P>2j;7D5iV=$s7ieXPCG8Skp2xoAl6z#q(3g#tl&ESAwrDt3UIb+%i7 z@#D<~a1Kbpc~v*$^cvfHuD05l3iHJy*mW~m*h1AaKoLXCZ7&K3Pi{mfptv}^0*uS> zVf}lOe}SfpAft@C*)reW<iHvTN?hWqeso`R^f7%1p3AXe{?`5-r)|N~f;?{BTwSdi zh?`0sLftZ$ux^|<u`aS`&MhtxbiHC>5Rspp;aa2+A??h_WN1zHWKaR>089PUR~GS7 zlIkS*x_~BmL>_%NbF-98B#E(H#tg(xv$cn46q>%}^RR=Kq!Wh=#TsJ#bG5bKG79nB zEKzAD(oCEdS;Iq{nc`1Os1Wy}QBIcv=-oAl4(yy#Uj#d8*Wkc2=QR)(sofIHA2xa_ z99KxDpIA>J2g3k_#=PYz*60LF9FhIkwIa}aval3{UCdjyxiY#r88_(A%T_%ybXZB7 z24?Q8PLFcoATJ0eJwC>?3D1a(-28tC6whF3-KR}E3brvl#?!yH2N;V;D~BrCV=;-b zmrT*CUkWqanWF5fo>sZ}IT{bsU@|*Qro)MrVGXL*WW9yNI}x#6k)hlhM9ew1<1k}1 zvXl_9*)W9mb@Jt+#g@L%0xG29z+MOtfN4AOj|;^vrhl3IgI2azZ1!u6za`*^&{hd= zWF=`cM;2`U<nxa6^;Y5Z|6GhO!p-cV72Kho*?`0@HwOVp*||MeQ_J;`6FZK2i()_I zT?tcey8jAFU{yikB|KdzecR3Z>vnK#cy}~cMepn8ue2DP1A)S@kkNPkPD?!<5eJcO zQbQ8fGm1jwyJ7h?ex7JPHfo@67pT6Lf~G0@d!5szLhm+O&<uZxwuWwx@uHn%p`<Rd zd$wwzdl;`f=k<>4dSf2;Kd;;_xO?S((`ALKh~<bl9W5)q&W%A>KgEoJg@d;1-cLlm zKr^xt_-K@Z2C~Xv$AAEtFEgP(jq0$xi!r(OW5}LzGAoSS5SqGVui~%caGZ73ECq~y z_dGa;>>4j%I_FK2)*#TMU(16WIe9i*ey|}>j1@V(QP{l$k%8wazKF7Anig&R0_5R~ zV>j6dh-Km6cri6(`RKrv*oV8qMNY%UuQ%k}k?^34uO84<Ir2GL-&2^?b7^QO%{|-+ zZh{Jkm+@p|Kc!+!mhN}UH~j{R8u<&80M$*FUN0ZPe&q|REw8dd4!wq8m=J4V6AxB? zR>TO-;RSJwML<AUMg><d57zef=HeI;ejoaz_&dUe@$j*JLz0j9#JzuV!+d$}UPUIa z!p_WWXA95070O=cWNHby+lMS!>!9X!a^m@Y1f_4PIIOKD$^n_%>v__SP$OcIz!ed( zhB>RR$QwoCu(LhJlCN8NEup-JC7tQ$6}~9>nFloaP~1Hg3N(wr&Olh58EzzfT1Ilo zi{6`}MLuph+{yitHMJE9x|C6>b+L|hY9L(OUJ9{ujMwR;ZRgrW2hH_@fnAWqy;c5m zaLm}2XC<s(1FvZqbWbzfl#1C@L<lA$75eA$^#9EyQe{*!MZ=FOvQ7Z(&G7LHrFhH3 z^`zj~B5*1Lw8yf1J~9sQ+Swge#@-ZKl-?-dwh}KEKm0*qVSrTPRaTm(1h1(mwiznV zQ7Lv;E3!p`0O#z-te|*{^8)8Su>oh;z>DT%bX@$^F=ZhaS1`AibOe=VE*ri2Q%(D3 z#va=B+eL8h<fjR@Oc=_98~^m7{?V`~f<GuMk99jie{pKugkL2o*8Z{FaaPUW>`1yK zF01%rETLM-9PS<K3@Z>0v7VnJ{o71NmmnwXf%0r#fNbP?LYiGr>LoDeL*Gm~_t&NY zz{Q*pR}g2tc1m<ZalPine)gnt=x~ZV08qWqu9e$=wQ;w+T!5YijsgwwuYFlpkk-f{ z{gN>q;&0jR_ul=x*$cPdw+EC^TizEwSI2MX5|?{^Y~I?iB6FaOe)xc@cqvYzYvM`v za4+9-aS%Q+Lry_n=B!g=NkXZ&bSF5^{LmQBON9RyA;zrZ72UauyDaOK%6sprRpltW zc0u9?|IA;}odHm~u0W#;n<6vJtbtTplx{P~EcA`prI%E;KngJXi20%B2+=m;hT&nT zSK52TWNF7=e*`fEqGRI=%wF;pA_I~N#aC8Oa7KId-#MM4-uO-QKW3Yr+&^6NoT`5c z6!LO$1r^n34?|zv`NjM!`I9%VqDE~30+HDn-%}Ox68D8tN!-2yw7@C^daZ<(3;`l` zl-qQS=OCML-;mTq8sCjwa6l6nTC$3YO@aST^d9fcuY}lyWc(iMpx;9PM`^DT)--wH z<qUvpDFkxzBT7r@(ndC2z0S3d?#`L+*7CL4TkP##8m;hV7@B+TJdenQIi*iCx1%bX z>pzolS^2nc?Vq2!Fz&`TF^_XG#XOQVn3hAvy4C|$`zcNzL*$yUyY&3D)ra~)+HuU{ z467#;ZNItdx_7lgKdp?W2`;x<nhH^nkVj0Q%pdBm6e|9`u5gi7=mj46`#=suYfBm) zI}7#?NeJ8Tui#+%51K#Eov-cF0W&lcB-Xuw;T61K+dur~2=5lohOZDvmR+IF0m^OB z#f61HMFPelWyd`c4>}zMbgpihtuCuyqm#^l5%|yazy7QpW_F7C09o<t_jpE3bGOlC zW+|4c4e*7~E7Cd^6&rjwZpufTHSSn#6GIS(y}ZvlAlzdqWge;)gNp+eyH5+rTF&f2 z{4E<WFdqUq1nH;SQ_dJ_(x%mL4Q70cNZsbr<E%i*=$xnM%}F%k@1QLRjlh04dWmp6 zIg+ckVb7tmxbNV?yFyHa%^hQ%%SShQ6ZXl8_?(F1s{;;Oe4m+tUsXPzEy_sl1gGWT z#5|cLGK&GDTbrE)XRF%_rs~!4+9q#kO|j#%7Ce9fCMW3;DN2aQOeZ3=D5b;Ww6UQ2 zYB<y64hb<*C)lE>C{pJxv}Ot$^g!X6jpovX6J9OW$B>Obfr&M~wg=z*xSxDB|9++W zgaI=xGxck7NGt0sWdHZcl}wh)Hu3a4ey(9R4Ir}6EcCrKfs;}rn2iPaY4$q4!mxi1 za{<IpTjUD1IF-|ymsA#<eZ0o(Zx@ay5FQN*NH(<shg80YO<2Lxp_ox|AGxO0MB@O- z3%CzH!atvt^=!`wM&>r#K!^*gn1~x$gaimXgpWCl4$9A{1X<!A)xdD3`?p$TuoD(t zC{#OzEnR3b`{%v_ghcja5yubwxxV+fq@pNNq50SHY|~pm-}gt&7i=UjId1~8898{X zw2#gD;#*$^x*gG8y{kKp8K+3=MUKtYrMSH(`4tymG39R@+_*4zc0ro+<$-ECa3*z& zWxHcD4tLX$&Z>x>acV@u!v*i3ZF9$Mev%=YZCF>Z?(D6AE4&pCxx&xOzjGAUZk#V~ zU#AXBe@SGQIDS~t*?f-26X*SX)Bg~WHST(Wda;rN?{gG@nd$!^W|>VJz%)5yKt{yk z<cxd83oLe|OL-MPe5L>9f12*iU;dm<krEPVMdzzqT;h1;Re54j0l_J5onTu(^2EdZ z;F4*!M8ReJ0k>&{FOF!a^zkQXEO!n3U=dBZqC(?*<LQ*RV(1eCs)j8S--4;CG%Dh9 z+Th=5#PYxp@br_t2UY?JILHvCciN~2niUr^9HtbzbUbKD=o3*L&VPa*!Q=7;v4D1h zxh-CV51{C=L+LxLJxVRU<Ctt(i6oDZ;z{pNJxVdfDpbG6a{?1&sH1q_36-b*xFzLZ zDyb~E1c4?f0Ktrnk0CYYV217od0_SQq|D2J=Zf2w-YceHrTi99C{7&09dV=Kgmmgj zT~u5^9M^fz`CGyp1v)zIP?#nDE9#RI-FNy27;nngfw&PqyXkN^D$dfTjEoBqKWW$y z8EP#_jR$F7%8#JQ(p+@}?2jC6c_#F?woDErQ%#4@$qy=OVg{q@l`xtFc!&lHb^SQF zt~SxpKlJlSUzmq(h|VmnM0?^j`qgb3p7%HtN*Gn#!<Rga;yjw;Cpc?akQ!rWJ|Jm) zOgCOJk|G{7PNsT_dPShMmDEn%*YWTYj?Puuy=GdFvKkUR-{5=99)j|5TK5Kbq<<z} z=-<vrVJu3-QuQJiKxI;fhU$xK!>Bl>5FM~<$_3CCPs&T7K;88;yMK;w87x-$j}w}9 zX`g|?vK99T0yL68;B!P`0>4N0drm7i6a|(%nK5r)@hw@=*AyJl3#rn$WMX$i)kFXQ zJ{()ISUNc=_E>rP;Az2HV8~<GLrG)1cuX!|BuH{J@d3q26D-(?C2;U?QNo{k?#xP1 z?|CNCVH}48)P-gf_QI4>{V-CKu;Y&A7tFj$6b#`Ww_gBDZjA;4y^Jj5&-_hZHYlOZ zwG0$3{2qi?<QdUXC<2w^3M~!0{AP!{{PLl^NbaWJI17?<T>kH~5*ibXV~x(cBj-oR z<g`ASb+$>83ilNl#R=a&KP5YfU~6STg+w=M)O?`UnxiVTf6rwjn3`?xEhth^vd>FY zR37dn^La7b_2TAEAJngx&GZw);kvlhnaSKvTgl1GTJgktuoz+)Myf5JK@CL*b5%c4 z{CSy8f7?}JA$YZHhX1f}oj7(4S${wRevEK{2q`>0VpGzwsHBG_cJo||S+#;(FuU71 z$Mqc0(E?MNq#vX(jRH5zz&z;eG_G5B^`!DRd7wFTkUH)dSW=V({~O`3&kkD9;%5k5 zA-#(ee<@pF>B9O4#>?4KO%m^@8YR{l!oNIs&RlRNP&`clNRWv2-Uy_g$?nU|&v<4e zZ&AJ`ay2lZ<>sgch^FdG$#Tq5-Y5pxHI_RT#!w+GYzMDo=7x-RS||nS``*xfXa0P! zTgL>c*ue{tkU?q-QU6klsZ#Mgm^AwL^P~s;%x50LOLKp4THcn<XW?p(U|W2yUF)!n zNi#SGFAh5_Tx%UAGnQa}qa-Jr19HY(?kNKA(?gZEmYDx{&WL)|#$gV<IMJDuYJz=g z=Y%xgN&7ERmF6FpPL{RQC^}Sl%+5d;Vto;3&FwAa3gkAC<0-`xdoTEU|0lTDhh7u$ zw=stO&vNk%`_dpilNt`=R&*DW7b<4UVeaUxaF(DAP&9Bomg5+*5GV`mpwTJO#PBxB zmBxv9r9`!ou<NFsUHX@eemAO6gS3<379mrUC~2&wwRT>qw&GY*svy=E*3Y(?1;7 z#GUme7>e>wc0}xHl#Z(WtOFB$qYKf>zFAfC53bi~+4i9+C>%R+PI%@F#yY{V=11R) z{T5z7;H{>SqDEj=kUVP3B3tMQ4#eivsrjh6cq;B=$BnG#oe_zsWje5E%nU*H&}t^B z*D8V$7Pg$03l@eS;*a+i3}WzZuo=^%b$qmuHmAePDM2(C8!@xclg|FxZr(<>mZON~ z?cIB|4#8{h9{x7j2}1__61=WzsnSpUop6GmMl-}X%uEyS@&mrZ-scKWfK8LurRS_+ z!#v2QtT{Qr!{A{?;Xc#4*>^LY%vIGIvTN1*`E?_4Brk}sRrD$hsaf3D9(>cqZy+e% z$w0jzi5z!$UX^|YTqReSBTz<^Xkm0qIw|Sv;E=i|YwOAg>`T}uI;;Ao_G6}r%976= z*IRX7EOhZfqrgL&mEy7@cZpT84Iml`su^GnSY<&wEYEOGag*Z^w1@~mhMII6dh(dl z4DUv%-zcN13O0hqvqem9&E5e6pccz$vs6hH)RRVmjJ^s5+&Pi`;jq_dVgIrupFPQx z#_k28=i$r-IHP-3iChdndIVQz^oq{i_f&YOY*?WJn&kOtTKX=z%A<;eXgal4z5(j~ z1mmHLJng%Ko$t&*$m*`ZI^!FNns($=0L))?U3n;Y1p(RA>#Uc4fum;TDv{yUOkI;c z0z|M>H-iY#Tk8&J!Bw@=(&}^SqrXdq1nFH_pvWMESwZ{3YPt`;dlF=9(SQ5+i(eP` zR@c)ppMTrr&<ZBU2Dah|hA&lmHFey0$te%m?<#U|WS2hgU_y{N(BMSgnt`MHl7=zF zGvH9BHWhb3s<nz4F+J*<seJr<)bZnJlN0@pt*(<4$~KTo#W0c_g+p2l$2McBCFn<< zGN2Nn(x(gt*jXX<?(h1COBI9Xjn_v$fdymJ;U>oLX=Rc*LL7`Jwv1%qQezoRtK5E! z@f?D|VL)o%v=fd1bOa~WPDiu|n$nBr7ETYUn6oo%q(ft#uj$qf{^21XL!hJ0Y(owq zo^Lm+{MZ|3GSaH}<h_=XSV@Dd<aBvm^@+1`mF~b%{L-aK$L>iW5TdZBDXn~CQ~X1< znnBNer8h}1fBV1hLLy&mXHhj;-rFKhz=cSgFATYOfXqgbn+gzrUJbw&PqeKcV+>f^ zU(fE3Exk`z@a8*_!WQ!KB49Nn7TwJFB*%`yOOcRAZZky&AjEd2Icp*?lAK~*Ig;(_ z&ee=c+C^Z|NpN3#{V6V#vEO?`Z_!w16rm`V&LvA7?mfEq+Dt)0PY<kd);uU;*d9F| zA3xRq__PN^m=%4QG8S6#9vFod<$Jy)6D?M>#|@fE3lb+F%XJRU@v0UMj447~EsG8; zDRiX8YI~!E;KdM@fqwLA$23nhp`v~(<!^S<R+ytdt#Hcm)`*7-0B-`b5ptrm`^V?J z)|};fPkOwI9=T_cjEE1Eh_0XV0yQJA0#irUGgA{7NH&!w(E!kp%QSRaXCfk9V%D^` z0GWu2d=Q5?6houyH1t`<D$9?_Fgty#{^29>^Ogn{<oMJ#)k0R`>x&hZ)?YGB+tv@f zWwqFlk*j|SG}=jcwt_s<eCbQL&(cQO%3vH{A7}yUttV;nw99|Fuxddnqmg=a<TQWh zHAmw!&6U-PB8Hx<e;ZYn-B{Im$Q{k;B{fX#8MY))WJzERRso_rSg|yG)z!YU`lB>b zN!6@Tsyuco`OUvNs*9zwWjEiO&}b!6ufPXHqDoc|=o@8>t-|UM<E1qrJzD4$5nvh4 zt%W!2;9e2cH>#SR4#7XZve9%ZnD}~36)ZxuW236<a*uSm+k}@N_d{d+VN;|zjr#P+ zVM?30@73Bv4PI!*%5)iU>~3uz?(=ucSX(Cqpca^Sg}|tPHRfN^C3f)_lCSc}6P^vi zOjxuuVua0di5`I%E0vkH(s9D}>g5&>(ts<}6(R7GgE38LR7O=<F^R-24^95t`1l(- z-ZvnlGu^HL(bNIW>Q2W$a_STxU8TLeE`bg|YI<d*RC<Q|ZKlR#CE;Cgq;Nx(8X2^% zz8PAsG8IF03@y<>F^&8Zf<UWtk^oDtH=C0paqrcOrcQFVd^P@6j+e#;P~Uc`Mz?Lt zBRh=sk%27A`ux!8n*Z80^I@DO!dSuhH{w;9GcpO@47YnQI7<|hGhU*mGfckXXRLX4 zGqPt~HqxFxSkZIB<~(Xl&xREmI@ZWl7Hca*DMaxLDbK692|E9uQ@qN}X@=^_;p${V zm|EOH3xvM7AE@tMY#%ruSCNo71lZfNdWuJ(d;{E-V}qn|a+T0?(O$;te0|dB(j*cJ z*W_IvSpkvrjvV#9&3quJ*Yg6DJ@z1rKknl*mtbrFe(tpeq*wdE13ZV_ES~#&U*n&k z%NvAazwa^6+Vp(?GHca!tlVblDRYN%XU79>L<!qA)4!r$JktT2H*Urd0|16&8xjEH zzn{Ggis_q9C#uHd#DNg#;}cK_x4@8nSPWhH@V^_P-TVVYZswlG{?#AfsFZ#{!;N|Z zuM@@e3X&a`k){1gKXVF8AOofy<JjV{8o70C522;us#rezO4G>wd3V}SR4BYYh4c)H z@y<^_Co?-@&UX}Zt*|q6fZAJ)*<IOfNNsIZBxz(&ucc-%?*X+^JH-zz%Sxo{<=M7y zj`-p8>nOG}rTQC}EDK4`gq1{mdHkqIlAP-lqMdF10t9a&&Te7wUDdBoNOGGu3e^CV zg!+Fg!Txn9<pOA<yT=#In<SRs2Vu7VL)SURh8A_*y0&ee+O}=GJ+*DSr?zd|wr$(C z-P`Zx=1bl;xjWhW-}tq5)>@f!K4TCpV6=)o>qkm>5gvTJZrwUdXBE5z`9RTBV7J8G z!_;sea2&aQwVJVj+K`@Oe^49*VY#-ItUW-I&vhBQY08FvpKu639C1Eyk78uh6+~=L zi8Ke{s<?33U`C5P5po=wt+G0Fgh(JLP1NEMJA>%=+Pa(b8UqaItT73_%U^7}Wws&Y zXa@!E?hOpg;5oz~YY?tHsy0!0Ma5cqD`?m*XFjHNOiH_Sp&7#_s+Zft7M5rh^nmLe zXm|7m4VnnX83~bC64eae#kn$D|4=gp7j}T>zU|50!CY_=OR+Cl`lw9T1a!{M_nz=7 z^o0D87G;nZGa~V}l%9zSm1YechC;?0)+a0hS1p`sTghEe(fE20Jir)k1W!6j5FkAs zX`oF~uJ`6#B*CLS-8T?6LGoR+oND>8=cx8g27oEya1RNS<GEH#+ptzxVPPn-E>|H^ zVP2|CVZ;v6#-+!Ve;e*JpyI64bhv+gU$`D4_)*U4biw2e;D|7U9;0JarXKEL$3sbH zD0XBGvm}&w2R#aG{$VfnJr~YtFwWt4bN?lPl|F`?XPf+6BV7z1;S#zh@<_f`$kgQ( zO@YS#M0ytniEKznV+NZC%hg*#HISiJdS0<U)E6L;>p~Yvy-mgN+z=jD>w4%ny>}8Q zsTMti;yzW0)%ZgE$wVIdd+Wt9F(2VhE=4L^NT%6dNjDR<8&jV8eXCO(V=K~RNWS<& zGwnIRdw+LY@`>!ysbAP$)@S2Ym#K$oz+T!b4)te~rubrMqesCl08T$5Z*juooDru) zThrfBUVA=)7S^U0&h2c3$mP!uA?CKP*S6Pi*rPX=UR6JJ#5VfjGGQH-VgC3uk84Dc z8JZHur*`=<1rK1`U^|~26N1pq`M<zbE`xL_Nsq#kdeY3$wY1hG6nA7U3Gd7xL$<@_ zS(VK(XtqPTMW8-A0158akuA|>TYd==glbjqz`Vaz6w#dyrZLrfc}n7@HfLAgpNg3i zyZfe5o<xuH^^&UcSx-y?m7b=A^eOxHQs~6Px<MEJmJA;qPmKX0Oq67_skTazMG5Ze zINd#+qd8Wczw}FHK;MOU7j;T5!%^7n&dQ1R+Ud8e2<0kyZQ`3cu;KD{#b{^4WPylO zxg^ZcOVr7^-B6P8U49LdG0uXn^<LkSGry}yIol{Tj6+C}FE40I_`x%Qv2Eccvrp-I z@1>a`%Q{35{pr_C*Dwq(TsHFES?5mSeRw$=vy>~+X(I_~6+l^2z{KhZ9s59f)&$~2 z3_*}$0i_8vua*bXkyVaNdE)n=l<9>8=_<3Wp=uT;W!SEc79)OWSsXMf%)P>kkfvAO zj!n``bEfO%UcCep=V^@{;WkrqyC}*Xu2Y6W%5!YWAC%hNa|K7s{<t-gR)GvCkd$Fh z)G1w-5-}ohUq=ih{ZSf{PoRg=67WIOwB=&M2uB8sMofqmGrkwr?6Tg{GQ)TyM?4eo z?p$qH6|=nQhDG(d?&pN?<>qM7RSdD<Y_%oRSMkT&sH@}jPW})F<Z;NZ2u(+d$0w-6 zM;ez=mWMFhE!*akzgn0|`<_>GuPN9iZcf^LfIT*cB*OzA!G^?9Jt*&Is}z#>qV9+V zAwcY;P}hVTP!h!WNY?0bz30x-N(q&5V)`Q?2Uz=g$!YJ`SB<pBO>g=~uhix$Gvy4* z5xm8pL_^nKxGvYDTf1o>I#&FzG_&)D$dwKdPkJuAyN2*>an_RyMtGT73(Lyb@E?xJ z82v?h6WJo-1TWbdOQ`B`!45_ict88u6k+uv^)tf=#r=SSuuvIw{~le{p@OrV?$+=R z)a&XX_(vZ`H+Tto@)^jW>=FT0m{nU&3c%+Tg?_Z_1JU5Z?pK}tI&rSC5)v;xxqB#h zRna`np|kLKo}k=1WX<VM?ZZ0&0%p7&fuHWcGkr_^*o%I*jKl(IJ}mGBQ}O48ilhX* zQ~mwli6la2He6mjzWSYX|9$R2Wh@~uu!2aFUj8?Z`Dft$;%cOkc=Wpr-@K)iUgP%= zi{NgbgMq)~`x{GvP*CB)u@9hBAO_JSyV3&lZk$78u@pjyo}b=om#cl+oP^?OqcZw7 zJYzZLsf0xlhB|Q9%X@)D9ZRW~2d@J?X*zEQnoA7078Kkh^tjxAQz+G%eLYl?R}gro z(7i)>a7xpQnWBdnXiYI~w9F9ht`ifrzhhUU#FDx=@I=R)yEdtCgerNro&X(kzHpj~ zp7RB^J({GCaF6%*1sX_}T&v?brF1b8Eds>A>dU8`-RumW(5+`lut#Q|-?&}t({Z=c z|L#NRGP!u=rszP`;6hFm5+FjE*r~E%Z|I={f;$Hfg8$nt!yju$6HM(npfg3v>$`)V z)@Mgu!?ZKte1oWkSpPI@+5?}u3xeN+YBDQt556-R1C}1CXf&8Lf3_5XpW_fn%hXn; z4!UUCyj(-4G`-r2Kv!DeVh^Eh{L3Q(n8*^$RM+O5PR?D8Hr(;PZEENN=Kq&Ya+`m5 zh?<us68@79N7QPUnGO9nza%pX_LfgZbEkglr4yZrJ0VB*k|(x5%}gsVDwcko1J1IO z&nJPsbn1uelzwUN$WrYn$@=Ho++xO2Ra3F{Py~BSv~bJu%`kc$CCdDtSV$?qhMIu; zXv&GC3~)njCRsmJ7bKNsYDyAPt9>M_sR|<QfrBxV_@MG4n0-4gAG)d){bBPuRTRTo z)8}6T)TR*>-4wMH^S@h@gtew8JW>P)pI+k9Eyb<dd4MpLFA@p%K>De~BEHjz*{R1y zJU4zOwjUL_adj|hj`@uo?UWDImitI&H@7L!uX&wo&P1UOQLx33VM5I6f3-g*fgFs^ zhxeUSq#?27u3(z}m{Oi7#7IRdnPx30_A#)_Aj0T^k%?ufQw|`2mVnQYvn1EEpDD&Q zWW!35NhYTjk7b-_cZS22$_a|;65OIv+z1wfA@iS!W;>v%R)?{5XDUwQfth_CfU{{u zP7luOWr79zrg@F{vII8V(FXH?rt}XDyxxItec8m8fx7-9sGdS8M#K0yhx_qN0W5<& zPkcztpVyZyw8lLYi%(xf8v>gFJtMGtlZ=iWV=qBi3E8-nHp-05)Cv9cyCQeGWCjmD z_Nsgbe&?c)LG*b`KKp}I=(v)b<73{5Yem$D#~K&;-UG+Dpr;m1K+GU-6=dA2?WyQT z8Kt<a?9~s@#esTz0UfMv5#yuSM8HE9*`hX3wf5{A2Qi5K38W^~l;kgPDEZuaPq4O~ z<(O`$UWqsUZLKVf-FYCO5p>{RA11*&&6*Jpg%3&eZz*h`<tYa@!ac#*cX<M1vD4Xz z?p9vHnW_31QxZGdreRMqI;rZ$|Jzh1AM#xBae96vpF-_}{*W~8OUgR1{7F>=_Ex_O zR46fVN~e;?MDok((<PC|z(+zEb0l`mAT4qkenHH6b%pElU&g==2p!v8IPdWVE-FsS zYMN%YgnUPH&?+dn#oVGbHt5zrP$Bv>i^aA=`hg2nP}{oE5*=}z{D#QL52_Xv3>s!Z zE&9M=_lFxQQLr>4H(o%d2Xpo~EVQIp=NmirF}L)xGH)W#AeENc3G1t&dJ<g&qQB{_ zyt`)huCKm)4;s=tkHlwk!)m;luZf+>z^VvnAZuuMcEF>7unaM^@eu#m6GoR<!&P~R z`oi-F5CpoS|8$zk#}e=!WjONd^^{emlXRa6yT`t~CMAP2#KcQ*!)E4h^!MNf8-*zv zwasS~**|z3QTt`L?}vJ+7u)E^JV8<>>|A$rw5LnO+8&DPYo0x_CIOFi&334?uay!H zkRIA$jlcnM5j@_4Z9AjxQ+(<=MAK7Y>>%CFp#|kKJh(x6JTSmJY%V1E-tqp4DjT_q zx+7jX1|9Cp*dM4y_F2ZSyPmbVD1f(pwgU2fe$nYZHbo4ZeO>v(-lpAP+RBJznw?<T z<OCS^G`<73tc-Jp4`G{Wu2?BOEFw!Jo#icFwZ`lJts&Ap0d|n7-l&|v$iaS42D#{= z^gG-+G8GU3VzCG3G$X9i1K!R2P9z2=O7Vd`seeb9;8fq^*MPzk(D<`)7{;&=@migS zK(PYH#pe&L&noC9voR}Amxf}!gK4rN+3w3;vbtGBLNs;R8cA6ESD{ZA6Rzi>07T;A zRVlLuj}sv9I3A|lWVQFxVqbi#%oH>9I)G{409K#*@lmT|5yXN@A}bM-$Q$QKH#X?p z#3eKXTNF<O`QI%tTpUnA!v&v+%AfX_F>1U43@QYsitdJgXyhi&!~nu+k?`EqLf~M0 zCDTPU-T$sUTqD4$^84A<@du<?<GVx|2iU;CFuj}e<yrpSRH&3$kA^TYVgV#WfgXdk zJXZ`oMV7F-?t<t1gGnYAV*xp0z3?A7V(u9IyUPK1UQMOGI7-^4%HOrw5#HsLiGU3H z*?lr=#&C(@W<X8Xfr;>(j(?~u{!$GB%*U<ijG?rAja4pl5N9THtDX>S`TR=ck{(mS zfHl3caot>Kod)5M-s}&KOz~Frh(<+cz)hl6+a3^TsYz+wO3rmqi;5X)6BOs~p;eB= z%&S17&9W-xF6!5+A&*SVM#{2PY#v+<t^VpjW^JI!8{zk30Cd8@aGgEv)Imc(4`~-U zM>&%{N2#-l%Uf6`@JUbN)hK|O3!Z6cZ#XOC@BRZE-D8+;1f#sPoc4$4rmbxTo^!Q( zzTfXr;2B~p|7DE((w}7Di2xTaF!FLbsVbo&AIAuqHTU0NRN$)-PMTNFViB>nrg`GT zZDZap&kqkZ8ojTw)-0gG^&{Z?cwktoQOIh*-wphvUU{WuWR#%DJq*6^SZ52QaM=6e zk2U&CpL5lp%KoB@4;YrRe|Y)|*V;|9VTBqAyEmcNyBMo`CVaqmhATZbQnkY`f2Xw% z6<tKDQ6=(IHL2vFGgeHQ|M0XVN=`YV?2H#fhi8c*QNtgRngQ|CaZo_KCuI$3{9qMV z?+TfvIT*_gm?<N5dyDQ@Q}S9<4A_e<n;X<Aj%2&lN7Ob{fJKMf0$3RAc`GeUY@xUw zkgLen(B5Ut+-Jfm6#Gf5_)|UQU98*$hr;7iQW?rr2b%u8=k<jlDZh&H%lnhCN-;s~ z3a+`HtT`rT{_TGMUco=Eq1)+9|G9kz`hHLZ!?6p+^N{oZI{j=9{0r|3{2ubj2^*Ze zUuuc6v<DS3c2+#F7%@31<b1t*0L&Lw)i*K=Mlw?rN)YzPnpM!QVAcSA$|T%uDaCk^ ztpbKOMOy1X<{uggoYiBz6^@;fqQ7WxZP`f#kXY4M-49T&(4m8F`3K#}9!nHx6m*sV zwK+%wzde6pq~ZR*3I@#D-xR#53Uzj$8wkScqZ!^8P?PvX4>8?FCOB&Bj^JUX{no2I zN}}Je>-ab)K90WGdT;1{w{3+aMgH5<?#k{jlfhlu9qsO%o{y)ei-{kfC%DuZx5p)~ z=tfiC9P5_?bF|M6oHVne6=lkunM&~SsTR?Wd9*fXfREc7<5dD(*Acxx8RV(kgDja* zx{rdG9s2kl9UXJ;T3sDjvv}19=m_CrBxr>VsL?Aa^Be`eZpnKUW@fqFjHtz4cI(dK zlb<hn8WGAMEjAzRi{8fy2Z3~ri9t1L=^x)3yd8U*1n<SAGmDI<tK8bfsMYK0vA0wt zVh%JM4i84bZPMw1z`6cvSFAum9?^QAab#|c6y=lw@7Hj;N>DJ)m7U@;h_ijQn%oov z6&_vHH&G!{`)Qldtmff(`=cp@eor+YtI^X7%bjG%B;6s{Hz1M}gRyjJsCv1=*4yQ{ zZ2e@fYyPE*4R*}7A&CUI{q4AR-1@xyVfM=7TWbU~Bc?UUtK+YLTGfX!Iz)bi4YOey z2Z&2lgHR?gF*_z#r(8b(Zv1XQZBM4<nll>&wzwXhFZ*+wrNvl1Z72r1D%oSk-j-F> ztzUM~J9=Ni$w0*>w{+Y_+Oks`{i-Um$tqQC!RjH&hSzxa!u1mR;600X;-!qp2Zc1@ z9vG%8$YQWLFfDA(27V&kF^%=#fi13f*y4{IDR*9D#WhjS=i&jH_*vt}1g*oGnWx;) z+FNj<x7*LQv}LlW&dn-Hf;)_n5QHParbR;TSV9t5<RWjb=V8?$Xd({Q)=y&IyWgBD z$|o%W*(%4qeU~*2YnNQl(Y5n>VdPYroQ%uWSA7>f)4;Xydq-~ZVA<AtVPA<ib&2FF z;{Ef(-Q0<V8@rof!nCJNrx_lmI~X!0fW1!pE&uYF6Ay!o=ZjzTVh<FYCZBG9hfYG{ zCRn{X1~G#uX<K#eRWJoHk#vhwohp7<8}n~v0tPBLrvA#?Y2Nz%;qf7;aXC%|Y}lk> zH_xWrV7TK2P*T*y<#SPBK7$>UyUh+zJjkx2|HiPuL-6)S*S7#Dmh*eH=b4VLsw76I zOC?_w2Pn2Mj9kS~A(jIwAee*IF$Q5ruIB5M$O{0TDp@0Wa)%AkCwLio9KvJrF{e;C zFgZ<Nvc+Qh;$a(H#l9)^V-u7EA;t-~IEz_!BYD)iYdwvohT*ekixOv9{H!CeGn)2- z)Guu(1;0N00sK$)@Ry_n0Q#TL@AJP%rIWFv6P>QExsAD#uI?|p1`P237oq$Lt=;Y1 z>He?C(ALVz*zkWa%>UNlpV=WM{X&kIzmVhq=1KlPjg5)9>8~V8amr$o9-;e!0?Z>8 zm(|Bl&{l?UHfZ)b&p!a;@J_R;3K5IrO!561w~7thMJiVmT<YN~vRfe)`2>hsr=Jlk zfsyZDwnqvh>>j#TX%Q}JxLj11r<a@4Mn;X+%k9ze!WCPGzi0GfI(;wbT^Hb&YHGN) zGOUasRbiySCIYF0_h^OARpElpU|M>LH)-a%+<|JN<%VNKODNq36ER7K>unlOaxbEr zpP*vyNPs*8Q2i^S{IX+=;B3Mxjv|S;>3LhlYeOMiG#~q*$(1E=Gf)kc{6f7H%uq9F z-cJ~p1M0W$AhA%Qm3+`&(`C>(XqJ?(M(kG~h*29kHZGU-&+i4nLm)v9mgzLFwC84Q z_8IkWm2w4ZbJ<*8sH4shky7xXi&t^t`StYl6$Vrqo){EDI)>m@&nS6AC6X-Q({>Ly z&UiQH_);jHr@ct`pSwd9QR<}KkxA{!a<XyoxpEB>TX^TpApOBjUC-qbFhW6XlkrZ7 zLP(x5FqApR#`Wn?dohG@`}ijR1L#I$+2zmwLLGrCkDrmT0RVcn000pGW95G_NB!S5 zoZ_~O*$|E2d7^qZK_kGFYUF{YehwPt7;v!z>vTboqYYUg2NG!#jiiW|Qp}zseY;VM z@wv~#9C_J886>XjsHnJDQdwH+Y;Aolo4T)@G9q>{rYUciHeN6qstVOwYcxnXnoUd= z$uI)tEiN&i7q3f_(KJ=bND@KT)z+3KUCX1qP)(H4Cb`=FUfj`iFGa}x)=83axpAqI zQEK1=GDy(T!Ta62{&{3tr8mpYKND<tx3Of&MWuCeA_ng(HkL?Ei@0bdHVy8Glwmgn zU}bb|-oxBNuN3olPNcg~R>!E&mh2Y<-Qb%gN${fDYH5&JYsd{v>@%a*G@zvKkb$?U zeECx5o&@#b$u5PBVgLiWrlkSkSG7oFR90wS*MCs(3EWq<VD^6Rjj-+XeBF!{(oUe| zg#IJ7fT`fvO^N*wP8chC#WSOXJ7V3)$}Vn_)}$DRx&u)@?(IAesuDX5icA!No)rB| zzgI7Dkg+4<Q}K)YBM)sVqOXS?P^KOX7#MKV8*avl81oV2&$N*OZ<Fw@+I+p6r1P$N zJg?i}k*^xXh%Wbcp}`+?J-9|o51a$!RiFSwDc*>hhDE?*a<F2|s6L5Hfy9}3<qvGB zA>lU@dY@%%(u(u52HMK(BNX1Rd43nd&4l3e?|^&{zFnnwIE`5y2>-24E-CSbf^OQ; z2+XNNO4A2}Og2L2h4(VVG9%y)01o-RY@;Uy)*2p_!j=A^@}~y~0d|`Z?tmyXis;e^ z(emn?t|kV7otYorw59^OSrvm%29jSZ0GbH1)$h&ai9S5t47JW(I;%cwLlt0&Z^D_l zqD_Cd##og-J&3PN<~Zu|iLp8CL&4GA3eX_jNfV!uH33hw>GOo4;>hLdu`sT*Nh148 z%0SB5N`KYaI}s+?`cjZuZP_XhRx{p<Rv=rvRF%q%gpwepiPUls;Pkx$Anr?T6_Eh^ z9zhx;D8?tTWrUkS$XF+(iR&2(?B_WIH<Hxx_jqQ7#Rz!l!<8E~0>x+m-VF{$H!4F^ zlY^kQd`WiOi3|Xru+B@k1%^?;fydS;pk6QwSm}jvn6mgG+iNOfWhHNhX-+=-Tbu(h z$CNuXQ0wWp!P(K3L|}UIde0~VJL`>&7*K`a4))_G<6sUwk%CHvrL|^{^osL==l2^V zZwCW_%)=~v%GGitjz(vo(SyQQ8zHb9APs<l(FjrJmQd*VPTSk;ceg%aG*Jm;&K_#q zjQWcXrBjL*ttUMoJB>cv!|_|oHVy|zCUx4beIv|*=BiMA0P|JSX!2^+;pI>26TgEu zKfQSwR{^VIdSL<6M*M@2R{+WzJ_{judGxxr#x12V*BHQp0t{V(G*2Th+yvTYZ!zUF zw6&XOMxv&l0%GPr&`hLR^R3BII0l_m@b{?UBXdr+;(zVmEru9P{RaX@V~yfofSAVu zQzJ*uTUb3jEIV~Lw<vu8kDou)+2S2<9hOiYKg8O|^q%XLP{~)F#sw~L3`f~JNxzEI z&^Z+j)@=IVt&>$9P&!eVV+M~Z0T+#DOO%U00?Q=jC=n=M{&wQ#EYF2Cr(r9(dzqL0 zU=r#m3b*HNBEW}Xos6tjLVbGQTbV&l8StdZqft}V10L52WS1X)+l!MT0wrr~-vPm& z{{u%0UV3y582vm5b{8aasplx&td9~^3t4B()>*<9V-V)B*Aq-AlU3alO_m&hwfuvK zh-w~cY8t9B@fzEB##;%zzK}d)^3(l(OxLHJ&_;ZX8VkmXseb!VeK_kz!m9TnwA4JQ zZ(q)kUXxEKXjw`($fUbFMT-#tk11**IfP;aA@{0<JwThFG8^(5AzA4;ECUK?i6I?$ z3h(P-i>*sZtxBKe!7NGDpx_JI1ry1Tz6STRbGrPX)E(qvvGW5pP61))v@oSB$r9?n zx-e@qPrj5KQ?Nb`T(fc&JDz60Ib3(M-e8ZC9DrN~v~pD^YMC55fLQqi!^3=m_1U%+ zjKFLKePElP!T#&D9R{=GfhOb6=LhMb<rR!zQlX%{H-)1;L?8-IqY!z~r`Jtv2!Cg- zgI94&iIw?pwG}~4$^MR#BA{6aHoxE2SNn7=*V9yr`?!sDJNj<1f%j(e{2wAiDVp#u z<SaIz9B7#)3Ypd|1Hg4_5opaJ;hC#QOyosLW%^zt6E22gyk4&s76eX@9KRV^H$5KZ zM)#$=&naSqRl-lr5H?S*3c6g^nZTiR>GmcF3aJa?S;0AR-Nf~Z>#nJMb$N{6)#gC% zfh2s}$_zNUadC4l{+^;{$08FqyX(y9Zi%T|b_{{zk!A>DW3VICq+x`|iqSbhtos6s z<u~J&+1%}x{}u^!khLDl57k)AJuKLPzOC~x3i+UOA(#MhE!ww?rb(kA7TAKBg$(6h zLqQ=153#=jAu*J3jyT%M!z)<N7sJx%Bq7KFNK`Uf7XWiw$Gy7M$epu%qkUlrImv92 zJOD1Y>!0sGaxxh1{)N{O2|_>HteacJ+v%U!qAkT8ckuOK)+S10%p1l?r5@tE{`F1V zP~7THJg7l%W_~4kvG5u6&-t7-_tYX=u}`!TT+vS&Bl@e@jw|@lh!r*Vsf&$So)2(v z{Pl3eh=CBeY-YP7Y6v2_<*53%ac0GkX_tOxL9FG2{>Vk*8tLn%`m<^=`~#2zcD^6P zP((E8P(x?L!i-d_q?}WtA4{x#kS(zuvPpk?ZG6S+i9#c_l~SiR%ElM~$jB|~g6;** zqt`>)Dd=71m@q=2R5T-pQ@+<nhMT#-7R_53Y1Eo2<kFKLee`9*t(VFaGtQiL(}c@E z7F+^U0@eW1&W>R}1TSCkB_ZG@7*G1S%w=y{u8!ATKXBcewbx{UeS}};0Qyr$v?{o_ z*Ga*bTE>jxkIA>c4#HyP*7sZZ-^_TOuu@PxHB4!2ToZ4+G%_C*`h1h$n&;XCP*&EG z{j}g))8d0~_V5ddKu-N^HF6f$K$@Ev%UX_f`p6QhX=4v|X42lzuH$57^EJuv#GAyY zxFl@x2Z7CPgNhY5YeCVp+-Jt5Gr*SLlpGu=WCSQ=Q!}RVCMAH5gH|)i&wPu;6sx66 zE2T~3fQje!-}n!3MPU$fJ;QUWMr#=(X?6<RzxIUM@BR^p7C!0ongM-vru5EXIfJvs zaII3j95J|N>h8B9A%!mMO%0(PMr!dVV-viSm2s#nyUvpHvKb^4=A3#0Lp7ufyWA6k z*#DUBdbqFX-MDJlcgEoJvbUqylE-B>=DuVQLj<z3QnSgp?;1G_*x^{7E&z)&I0ZdI zJR8#f*oTPg2t5<%*4^Pth+y)Z5Ewo8b2QpdZDU%0%^M>8yOph6(THoha9$ntVzYya zB&!sufHURGhw7$$$h?%NpUp~Kyy97elwcJ*hilY_a;Ut5qb{?W2T?YoWi?KNf24@` zZ0z2cFn|$HBBSxZa8$bH7sk<q4yq}5$nC?+06LQ;TH)w20Rj~2CuemU0hL2dvqlKs z++9%Jw2L}ok{Jh4Obc7torPG>()D2zBHjFca-Fe+WyvE)=`d_M;;-Zoc&ya=iPHM< zKz$SvpGCrl!DWL7&`>A94M9oEt@4+{vFYXW0d@e}f?)eB05isj3^59evt2pU(H-Ch zybIw^odBgGCU%0A_oAu8AT5)tdAh#BGF;@J^=i~B^aBP0?xerC`h&gp)*Cz<heN~6 z9y&xx%hu!KId~|<F1PW%n8%&-gb!UkNAQS)&Yq+K9gO*-=TnJn-ZhvoRkev@YfCW@ zGDjE6y9BCV1h=Gql_iO*;tc5I{Dnh*Ly+ox0zmvmt<r`-^E(A-unPWG$_!(|bjh?q zqTy3%%w##CCutOgXHZ?-nx3B{p`d}N1~KdqC#*tydKZyF?pRWinm&>`QGkpA&YlwT z$Wl?x5a)O&85eJqZP_`i@@tOMZ2cSCgs?WR9tqalKW|TgRZuCxDsvinx=<j6TqG6v zHH|QUd7if#IDqQNFfOk(!CN&fretQh6n*3?d=?Cqh}I3}L=8?2Zu0L&pV@$7IyER3 z`Vb8;d49@nJ<U)C(5W39!@JF{1BX{1y{3P<*>QRp(vku*k1bvnYWO6E7wr&Z`6?uV zba!0P2qB(e!qZM-`KSa`^P-Me$a&rso%?$-mW>71jB7d~+Glu`oglTk`JR`#a!kqo zfY3o8+&RTArhJ&PGn0tHzLHkdt>~EB)Cf1ZxJ?>rhLnoxV~_aA;9X4Sq`8jiBdOs< zym3^cj*12n7d&-Sr7=bp3u&>~#mLm+MfC}_XecA^FO|?FFKIQDX{MvK`9ikC3eLbl zQh&m%|5?|Rd{nftj;V~Qo>lz#?E&QH`as4<<y8{rXC!3baj1VkFN2EL2GSL&ICVC2 zu<Hl)R>MEDAYkZ#0)S16`eug>K<bElw+_>&_K|#@P>4a?>ADhNo7=UFc(Nj4!*>Zi zm6}BZFaa3hHhb{%jev2429$}Z;(hf($jNz1jox(;NBKYS5`ee@kqMuRzj;7Q`i{sb zhJq<@r_BDTnlx$g;GXVC644=-WPgdlSa|DsJ^Vlz6VGi>u&<RK@pba9iq>`1jCB0P z2qBq8Bk}@}dFWwRRlShn8>xXH)Gt^=Np<Qv^;$9&!yYp7JAj`-RAmlRQ9`R3MpIM_ zSptY?-bbihbT};R?d|V~3u65O*W|Yss@RI|X&={Qo@l}2;=}tm_cW|loUIk<K$6az z=j*{riYX(k12;OSrk(brRj_c_jU;Pu8u(+{@GrH?>8EzV=M^E+ev6+-?OaMeSw)M1 z5w&%eW)t37JY~nBshq1=yQ3Ogmb_tw#+L~*hbY@gK}vz`(Fp^K^}~_AikUd)L$qgC z`DTY}?cH$D;LW2?ZBuoQy*R!H_;fF>66(YHyS)vr>YF^xLvkm*Zj&9#OWZ8WCpG|O z8L13{K>-4VW+d9SWtRBMr@Z$Z#lS8E#g3L;up2^Pj4FS)q5V@AxkCR|6X~EE72o1w zoDYwoP7(ZAWk6vLOoQ1)2dAGv#PC02vd9+kwXs(~;!WXP<Hvi}^Rt^f;3El_2kV~| z8$?%BA8FkW@;fsXkYZ(#kPKD9)D)w=BL5}=J-r567F-7RfWCmj_Q%}38;EAmuEWb) zFyn??;=%69iMa3)L(6W&NP?D=!J<7JWFZ%=w?eX7FmkGw1s*MAWF)c0aTHY7v6}vK z2blq0cIQ|y+`nTERVgE95%Vc%kj$!_qdMZ<)Sb)0Pip|;ChS!L{8{f`K7Kx{4W`+f z5L`M)C&zf99@0~RtGrUiajZn&zy^N=m78=H%p9lN9Z|2WP<M)wh1L}oKj-AwJM_)5 zX5^L?{t+Z$;mBZgN$2+tez;rx!u3%zmq~)glj7+|V$TVFbxm-j+%h@-nlHp&haysF ze!^$0+TG!c6FT2A&ilZ1c4%oH6Udlp%nnM9QhAY$xWj2}&fpevk~0cK#BMdd&k+kv zE95}h3wIy};kF`7CNJoVzb*mEJq-o^gjDT=`ZfdEdXS_U3v8ieZm;HsSw7&(f0ZYS zI`gSUAIJ~HeSe<(^$oy1q(R<+qSnh3;bQGkxHL~*lSJ>-h#}r+iz<D>NDJ;`)Jtg% z>l3Xot*}{RPA;+f&<Y}w&^&fm^rzkhv5bVO-xuFd7Z*7-Jb)NssrM<wzZJ2?OGhU> zUhoTE*gmCDgtGQLT#-fmV*Ej5go6Gto%sSuHI{XK1N;neRsmMt{WVjAQI#h1By@^K zNO~(@(Ky?8xORU2z?5(#iePx&+NAE7Btld(S}?qg7b@w81+Ei_ZCEoT5RKutVfDnR zJW7htA<d>9e8{`_<2E`J@`XHvmV&QK83iw~r*U?0HITyIMA(mSOo%(%W=K;h(1;1f z7MZV~w0S=z+Zb-q^BD?j2HFdziXlg){Q*pFzy{+GR_Ns+t9MHH3UW2ZkeEr=1bjzz z6_nN0gxBP{8ZL{*QP_6WSJWRjYl*<e^3U~I-m+a&u7H8pvtVp&8IA_mijYoEYe-~C zYF2}j#a<HRqLnhLZb4g~o1GXA{_gPe9x+jEj7>PE`0ld|yF+}1p14$&1&QV%3={j4 zDEl;Tsx2~*uxdjl?>zlABZB~oNUf&15%V_KnUXq~hZ9^hq_2g~=hf~DsCI!Zoftn` zE9lUJ>707aGYDPsvTz%9)$;Zjo0CmyV!%IIE}eMrT7o!0>du4~PLDEPtG!;t@bpYK z>%?pT5z-M>wci2@LTwh(_^KD#2N417QWNnWO;k+cR5dM%S^lp%*bDc~yo9NqJvAxW zEcl!VsP<aQsmfcP`x^5a7@fe<lcqEM1VO~aHjFfq@V7DRXK8I$oCK`Br*8zT4`hn% z_D^T{vHt58{7yi7XX_eo*zuezkZ*-ZPiRjcc@LWx1jm&pHz~HCoD7!V;`<c;8J!Q- z8|FpW?Xf05%vs9foaiFp%Qxb#9$8{c;PV4<?iLA4w|E{rf_Q{~RRBisjDMh^h;2(j zaz{hfTZYPLMut=)dI})!C&>oQN+|?4@<`Q@V&MnKP9wnuVKG5<;Lz~<fJa5tlpgvg zNM<a`XAecU$Wi*fC|$N3>|L=3D)3wG-u^W&kwv#Zg_y#loxd|Y@!)c$+9R(KP9J0E zdI61@^c4Jw&3ZhQuN7qoh{jBQ&2dkdt>NtFy-7mvu#4dC?FGfp{mllKK$?D6bMG=* zhoZmW9kV>jb>U+#(x#VJJJ1n)ABZ0)guWKo5PWMd=e#VsM|3o8#a8d-EpH9|{n3HM zrF`N-cPN|wBIvM}6N#ulruxKyt=(=K1DzBH%#d<Oi8-V_r*vlJ6L-vB^)=V{wi;HY z8skx&hQGSDa*mCyUdad_<n3F)>UA^k(Q?V+d4#|1!NsnY?9kv2JHl-0g|6OIPXuju zY*jK7o?)#w2Vd<Dyo&?5kUrmVskw5%sT7AKGTv;G{~Enbsx&Pf*=>%`2XvP0L7<e? zKAX;E^uKlpzs~@0Ws#i2QDzkxpzEf@S>K&z6(-+zC#_<)ljQaB_vbR|>|@Ap56y|J zN(NlSn++kKu(r&}&AYU#iF>61NF=OIu!D+mCUQf?g*)Am7Q%9fr|C$ngtYb7obrVF z&&NN_vjm;{6)#LMqJB=0H1>$h)*2JeTKIRkWR%z)1boh^wVr8e&`($xvNiH%Acg)( z21D3_yg+s8;ouf)Ph<#r<<5OjM*+0gD@33q;)_`yq*%JYKUw&2HeB%xLCg`sxlfmp zSi+e)xc_+yc3eIwrGl-L&G20QJjf~{t1&SyWyw*USxERNj3Pcsh^a;8>d{zL?mQDO zU;o7<fL4)j`U6H`gNg2Tk?z#-3g&CXm_%nlmkpJ_M3B(9*UBJ#K3^4QCC5Ox;|S>9 z1ts71ew2%1PGSWmxC%JJ!7|ITmfj(LaXW-kR(=ju#v||1@e0Q`aG=B+(9JFnJ;N|n zXlmQA>Kd{-C$8$fdgDcVK-xu3w0Z%Q+#Na%mijrCM<7F&S;(1zq(sShH7hG*X-6)s zuDc5!GT!qc{%x9~(K#p<zWl}Lx8YvrVE;P}m4BJ^Qq9Nx`PJNeK`m#O8S<E>^iTV5 zXSzC1RJ1R0SD1)<;pDTpNGlECoVx&}G`tHAX`d4&7>kD+(^Cm7L@~Vh9afYwq^~gO z=~>F<A8AJG#68Z8v(hnBxQUnxZJSw$loUW#l(5P_+pP_SM#ATldrB?z;IF@(Q%mHT zB>k^b$=WP?YvTtas8|OhDA)<D7w8!@A%9to6XZ8xFM%*<q*J*V^Jf@}Q<v*Bsn%iC zq}cdM*$-M3&fKvLxVp}|6pcjh?`^`JiQGE7QpQdaK3q6eQhWMR)cR~9D1%KrOAe9@ z1zeW_I4oE0lGOe5q(lw~G1rL!GrW=bIy<|49u7SH&tjbL-@X&7=Juc5k2D&5ypzXq z&Rdvklaa$Zl{$27UjGnjlQ#HvWq!@1%T0(q&u~JR$}TxPZl(DY+quhl`?}(x91RSV zVZ6PhQCjnEpjukZIJIif49Rr-R;t*BQZW>ZorDGTg{;u{>sIiqb-O*ue8kSb4oX~C zY<If6e@Gb#;ix5VJzH=8FcoK|#MshXV2}6nlZS+1U?Is2?cgAfji1STSX}g$6pQ;r zwaS!@dRQ<{1o5+usNdPLa@bn7ea)pt<XPrk3-Jy<%VNl;O|}HOR*u4vN1agd#JFkU zwtejK){6A$UL3aul8dF0RNGv$@*eZJ!A+sLaS%8SJs}>zrlBYA!pF=XYMs)>U<6tj zqDc7QGXQCHvJ;nIgNr!i9nOnZYs%-`o|(%@ZcR94ef4kM^<1)MP1^zEXf+v2cR5Pf zE&=}@gclFD!{yre4LwM31-$7JQTr!~H>Yqo%Br;7IRErd@~u}%7q^IfgRCmDn02}? z&!Q<!#1FwO($$|{hr+gWQfH2nuHRTTK&Cx=PNoY(MOox<BOVg`CxN<bt*PL~tdq<C z9xs`pH|S{C#J>v$IRQpWtxtM0n~8Ds$Zu$};nHhO>%I8%j4q0<VOj9eM2Mtnvqws0 zmQ@^6vJ?F15*~LV&Wo%94t_4R<WHU9>Q_1LJ{!lwa1sAVbIRLEo$zep=m={c{xZ>s zVBw3h`;QuQ6+sQuCrq1|n+%9(;H7(V+-AUU)ZmGjMd~>$mL&*rl=E+2+1?wnm14k0 zxTPS|xBNXt)16dI!)&`twLX)EQO}2x$o=5lG@VUhXYT9fJs`xf(6<wUWL$1z8&CI8 zL1xvCV&7bR%y3@ca|LO>$|lHx*Z&-Caig0#ZD|==M3{p{xtxkq?oC9{T;;Mo)l0#> zcIqv&niL&S;8rC&8@ZJf;yCaRjv{r-8&PqurDVSpX;>DdyoLz7IT3aOk`3dWtZl*L ziuHQo{?fl6J2x9w5$yy`A>2SLQ=f%pioJ(6{$MHn3^6@Fm481-dDK;_CLgXQ6X0n` z-k5<*jQr)i!kLaRSn@~LxJHOLV0{Op!DRBHQWW+1WUzeB@(UC#3(qx)Irg*tBp6!a zqTw1&zNnkLc6)AyzEs9P6Xl4sWVj>jH{#kzF+W^hp8p5@{)C_PqI4nA*<qk`mz({j zep^BI>!h<Gl$4-2L9^wV`uSgGuc?c^77o9iRuqBX_dhylX2w=_#tx3ZO|&j8PsfAi zrk|Z0Am@3)w9&{b(uHW7#5OLClBdl}Lzk`N?YVLs@gS>f#Xmx^$xB&Z*DtPc07!+0 zBcxd#q}YfcaMK^4^c%{?TcL{h#>Z+^n{-_lU>Q~m@H>q%$sHvs8qDX_V+}`hbM3Tj z`D-c~_B59lIZRv9gT@6GZIesF+%u|f-4;|HuL{R)m_DkPr28sMM#hAWTs;()&<+h1 zE^vC~%k@(ka7-TP6O_n`(4RM>QEDBcXQH8W5QHqKYt9PG;n@-MU6W8gWwy#5PLKau zTMLqcJ~vn`QG_O$J~tBgp&on_HN%`WB8s%VSI0S*a($G>NVQDdmoEYK&zBz5ln8An zs97u_*TV*M(8^}79FnA}JVVV@D^wD0V5+R+Sim?MiZj5tVKk-tYaA$j-k%?(gPJ%M z;O<5Z)jQ*IXY31T)g&(pjnFJ@lX2~lb+2RyjP{jT=VjcZcV;dwRZ-zu!qiP&$I7+K z+s!T0DjT#>k+7HofOIrWvn*!i=SyXh{D6V&x63twBMOMW2rC9B!=XWz@C~qt^m!Jr zI=ZYaV!tY@k7RW}*T-=-7geIk@~E1|>0?d*5ou_nW2-RliykGF4}opQu0T1}09Lm7 z>os6$%QZY{E-}ZgY{kOuw#2)wY#M1=8Py?IU~5dECMwHC|JCl6_UZO=kNfnxyRT96 zmGbd;B6WX$IMUVaZgq19J?!D)<P4Y(Z>K@Zr%iln%C6U+bXKAxvcC^>(?GNE_;Z`` z2bP~ou`YvJMFI#x&AH;$c#XBi2*14g?Wa9Xrh)-D9p4Z*n5G-;VfRpYCvhVIPL*CC zK<*pZFQ0pEwj}{-B21+R6~sxuNHiA>abSEoI*4y_@T@Tqeg^r16twNGdANTT0WXTo z58fqUPn@r>d{WcIy>)J-;h@C<I0tyFYzbUofRZPBmU(~nmpr#b)sT=-trBjqA&F0- zQYh3Rq=s!SSCsQX29)XL@;zk`1uUQ-uMw-~<K<<wfK>@xX4z20_!A@m^rIAj7#8E! zM;HEC6P&%c??w`l%V9z$)DwXOpPK1UH+vGMe!aV)k4*&x5oHQtXNpa|Z88O`B|y99 zy}P%V-bvEICfIxZvWbyGfNhstPee=^gu+H+bx^3vNRl%hYbchiVS@qctkahpURWi_ zgao@YJ@#l2SOG<&lMr1R>P??Pb~RCf2Gb{T;;7&196?-?gobWrOm-(*EYcw{3N0Y; zN0{XTj53K-rD_x%$!N<wLMhG+KbUeJ5oD_xKftaB5(FJ?$yH=m=2K9+TBL0Zp=~0{ zAb+hat~KSHlL`vL4T!gJO_HcvZ{OkMlbi~_=5DvH8N?n3+@1}pI11w-&~`3_0S$<M zc=bC*F^QnzlVJR*tGaC`ZHM#`Et8w$L4d=XCc^8a%@(-Eb-xkbs?@9(0GZwi-Y26d z*M;=^QwKgRk&Jk%%-FuFpKuR3C?Y=rN7{?v%4`(5mIj$`(k%?N4#2L(Bb!DXatfPU z`h^=BzzycrXYwIXdi=Q<2We{L>}AL2MIxw?9Lk{HbS((aM1k3_U{~;kP0ip~>7g(S zIEbDlhw%h`jxauL08u&e1c%fepV8dvF;udi)q@L``l+St@w?EJ)BvXBtAjc;@!@M= z{EYdEVae-8xnadbmt*H+{O#M%fmf%BlR!?9lA_Z(r&94=Au0X`Gl>#Ag^8PT3*pVa zA>x}@v#lh$WdCDrw&QhNC``0<gj84S_)D{}O}+@+@rXwrC9mU)7k*hh=|f!a!6!ja zfhKpzx)_k}BV}LxpoB$g$tsDG91+`pLTYeGAy==M=uEbCRs2#GJZ-Q4&95~3scBOF zHcc79c~(Rxg~hn80K{3T*t$BNc0?#&NCn+AIi@Jro9M#gL4lDA1MjHJ#$GY@m&POy zn=9Q>UhjaSE8}8UpfY_{*mUIibj$ps+)g@nM|J^&Cq5c84C1c!#JxI@?|YP4N58p9 z^!ZAGWIm<}HPhIBwlLn8n0sQ!Py6EbeTS^Ew$C1#sfG<jm`Y&oq5DgF@Ob5qc`L!s zvSdpO#)!59>iSlt?^IyJqjG#SO~h!^kf(4b_@1hp-U42Xr-!3f$u}{tRfQXr!||$( z8jj&1n6~VF?c+$leeHW`2g5^ES1B6pKqggDm8k^8w^(G%^2otI>r57Y6cJf-nEMYJ zXOmV~Q^rCf3z-px6J4`e7hzVM*DJEz8?TbZGWMU<mj59eD2hHyA+&|@p1%>xA}}1# zW&&{F%hHztacYnzyw{jzz32f!WNBaS`Y4`Gpa`zL;w-*rj5J_>sAM2fTqF#IL>Yil z{Aza(f#s3?AS=TRdg#Q~us?AiC3vg06^E7Dzbrht>sOfu0+AEWYfiNOqN?DQt<}!- z>NI<NpBj=&`H@Pcygy&~d&O7{0*EM0H9sqj1LB>22Tp0f+DPxnJ2WdeX%ZCZjxpQ? z#|Oqu2B7OG-85SQ?c;qa0zOht63n@}Z8@rU5<xyp^@o6vUmZ`g7=iDyy-d#k3aA-$ zAQZxJeNi^c(25~^lNBW4)|~-58yz3^IB%2&q0WKCR;4HO^$-}JdhhrTQcgR?M5m=+ zKILS%KHeta@o)ezFP{!|Z=>r6Cc@(%BYFnwI3w2h;6g%P)&#h@w>6Zo8~PY0EbFJW zf&Tb!Oe|Tqh0}a3x0ot!gNi;@w?#dw*PCDgMJ;x<hNcHQ@OWZx6QcOplNXjjv+h~Z zlP8XN@C}@fJujLHu`T~3q7#<{ogKYB*vx8cov*=v&}@KYo37ji?siW{ip&0iHo;~_ z`<w)SE2w8CtdEg;%9MH|%IM`UADBF@@!xK8Ozw}?3xWhmj^dCPvh6k`$csCV|2A3s zrpAhI@u|zMA28)}HSY=xRF%69+LXUYUv}(~W*?Oa97Er!QV^L+eQwSNZ=~OA0`x|= zEOM)bvR1;`8U01(VG3<)HR)UXd!*+}pprU05kAAoX>=+tGH`rv)zx6b=(5Fj5BW|l z*mY-Jqt;}x-fma@9__WBGh@Dieowp6h+Q6*EKbwQT+<-LX^M_ZKEM?qxgo+p)0bWH zH*9;+x~{)?lakfM0r&}Q<Ya*_s~&y%<a6P|Fh`-?la-I_35Qa6MhQF+j<e+*7vIee zB#KJ*71x^;WHl<sm0Ge_th29X)?rck=Y6vs%Xww-5)!DTK<f#(p>?M{L!yKI>8<Ek ziLthWrd6#a$SlfUI<h|Y3D<-tV`@YWpDzx%{tVSVzWJZw;TLss2pZppu9uIUeTeZM zR^}~mmJh@#^7qpPFX!CkZ}sbD1jn}-3%^N|6?3S*<LZ!proapVM&SD6%ro>da)X?p zmxot%>IYdi#Zt4Rcpx?m=S+yMtqW`%Fdl}Rm%k&#!Vvs&_~ZDC{pK>8^X5y`dR&WF zY&G#8&l&X|wsgj3kY3tJRzyK?xAUsnA+x4Tm&KoPanCH@I+?GZe9<OVi-D7djrwl@ zd@%WP@{NlOb?%9#vUSTqU?-vp^Dn3-5m6q1-x0_gv?;4`mg8Z@3Tumaokd7-E9>es zMWnwAZHLM8#k<%{2x=X<3s6Gg_^4@mV8tp+8?;x&;9VWz$?0#%A3xUw1C^o#-=wQI z%PNJUZUcbByfE3@UD&*PZ~QaS4D(ybjTtLmg89a>^Ufq{#{~W!^0nXkYT12@l*?C0 zP|r^(%CLKURo?QdJfSg_P&~v<xseB_yoHm(ewS*Gc7%c{X`CS2T@^eqL{#QTd{e;& zKd!zpgw1?x^?u2Vl@IQ1f?aS8yTaso%b=E>9i{XonMG<JR3c-ALHVYj=z0~+COh+5 z0P%(KSCJRwaz|=*DpDfpIrAJaCH^Q}O=F%20*I)myunlIdzLHSHUoCaTp4VQ6P6ab zfP>-_Wr~iA2&Oc6j@WTl35+m6^CB<FSPlZ*825>_1w*ko+0Pyeiyc1Ov`X$4$f-ch zJsaTq<4JG=7p~VlCJCkkvO{mHbp9(Q$Uu2Mpsz}Fej9x^pl!A`?3^VudyQ*)hijXY z`}A}(TMM`H2>jjIVNAMh<Zyn7&sSlutepIlyMs5bNXWd>QMOmfwFz)-Ok!DlC|@pE z5;vtl;^kRgbBOWmAHq2`fO)T0r;l}Yn}5VaEkGWY65WFZnuNYJG9Pe}cCOaGwxV)h zpMG?{u9hYq4)M`#xBqr~m@8%qymEE!%WZ|@*(yC=9&Ej^##iS&6VL>neF%SPowkL{ zWmUIL>-Y13t9hNxez|c+>k{_OwK0dGyc~}nFtvVD!s1|cCcg$l39=dnQI1_RT-N|R z_9b;L2XleKMRix4e7+p(9b&)Qo?Wkyx26?R!^G{SE1p~juX%OvN|`{*PQ_PEVkw)u zQ<M$zM##NfOtXQ&MYGMEy+=}non@;Cj0~1Wnl)<I=)2t3jtaM$)(lm&Y*xi|c*M-q z_(QYY=TT!aqP!Y~W1(<EPYAUYTP@~+4HJSDdGt@QyGOMR$2d$iDSC-r@K4H*f~zg6 zpsEA2Nvoq$?4PGYCuP|z2A_La$*#`Qq$b~I%`<cR2n1`}h{fe|6fHyd%SivM!i2_3 zvxD`043?N?1Lp%3qaXG@G|cve+pV6~{>$=Y2H_NiQ~UUc_r1vJK{`S1f4ayHBja5k zpwUnHn!kaC{YXP#v<lrIP3-ieldMHiw;=tf96jt^&kuXyuT_QF;0#KbPAd1fEGYOs z&$ZqOj2ITkIAKA}+GHqI3C|Zn6UNuTt_)<9$!6sQ0*JKZ5K&P*Ms_?J-KsS@0E3q} z5G%J8`k8lRl;esrK=EDh$*Eg$)+mP57Q{`Scv15A&Ao*|<Z=S|?V{72%>px#UKW5C zp^I8$$3tT$b`amuH3U!i9T4zeyK!6Kc&LRy#Ggqp779rQol(VWsc^w#(nrK-Btabp z!*U?7Cv*)3iRO$4obRSuc&&4%=g7%Ti;Ar=`$j8-&jr2T0SZ05`!IVaLSs)y3tP(S z7aqKTAYJ%Wv%!K3SipIG3jZ4#k?ZS@B<XUOp7lE#fBQR&boOg08QBhud_JQj59<%` zwXFo4Qo-uHdKsd@bQy#Wj>te;T{c?Tf3O>JaQf|Mxjg;D$itx<83`8}^g5EujM%~$ z!uN>k+>4ey?MP&RK;xAv(r4^{rZG;{cP`<Z3EGX%U3&huXbYD8KER`dSbKIkwh07> z=u>UkCQ*umRm2BAQ}B$E+I~WXvTGNu%*O&1Dc~^}S*sS+5&Dtde5?v+VoCl6t`0Rx zmg1!Qd}yST?B8a-x+J`{m)9}G?I>>@xi{2|pi=*r@a~)-_)ZNG^&f8>@`FS08{PN! z!_xm@>m7nb3$|_1vTfV8ZQHhO+pgMW+ctOEwryLx^y=J*AMww9FB{odjn!G16JyOW z2B|K?S#4#UBxWb9o`;`9U3}h_ot%KN!aPJQa12UtES!Nw1B*vYVXbMf=jt;$EHM}g ziHq01*~qHvy4rq5Q<8;$<)d8q4Yadz<j0drfD(1<^dgakNI`z>R$_44j5AO7iA}pa z_SE>*(4WgQFz+=(2EajoTSfIvX8UWpM=zcR`;jyunXzSC)4$gF(1+H4@6Y7I?3pc* zI8O9MeLXo{Dr}1EWd!YLpRO18+L<-FdZe~R3kyc?Lz!+5=X{}Vk=+#UM0Fl4szz!B zPBKyblmlzMGJ}HoNDaiS?ooCBd{*X(;VvF%dG=s;`P&4L<2v;6-^?<ngxj%WUqrQQ zwu*klz^NgCy9VnlqlgWlF`+O?f7Ujs;8IL=5(AebR)svCGQT}8f-++~E%<r>zYftA zo6U`L{zS8%_6HSl3Fg1!F80*#Q!qLph3{S|bZ~I|yu=N@_x0M}b{RhEg0NyvlbOgq zRR9on(X>Fp<s0gmPR|VPK?cNqI^6yRYZ!GN;@tf8wR2K+0;caMj(&P|bMFX*ikI2# zIEsqrt6*r((Q>x2{|i+(IRZdRW8ikgsw1=OZkl}J%WN@+!a8ZgK@Kq3_7%+`dYxQ3 zhbMQli{b$2t*X`qLh06Mw`iSW(=In>8f`**Kk4h7XtvgVX3UYpo#nk;+8s1&Bylo{ zXhoOmIk}9s=41xVO_id?uF|53K`m)Xs4Gj}=D3hdUZK_Ml3c3okYFuiS;-MJIn68S zU0B{mAOlKSQ!?^o2@um>0*1YNl+Oyrtg%E@rG9;q%9Z58hOUi4KRL1!vsuZvaj(R+ zRQ3gIn=eBHyGl#-S*WNorA6^233ZBfH@k<M*H}N<rf@9|fkqzO!@d3)6nfaIp}2-{ zV$!Q0$dyDXyKPuJh=Ah+o>ZKcMU&=|6<U&=IUKelWUEUn`QV~<UfXJtnID?_`K6}O zn~H@DE7Zx4NGw)x#xd&I(QD1;RL;Ddr?E!7U1?*Z4VZfhOi7J;TuXqy-&FZ8d?Upb zg<Jdn7u4ONQ8qa}8SGB?IHi6N2rt_C;=tnBEj&z}?$c)2k5Zi#V)tF3y-E`ME)TP( zTi2HU1WosQyIByXj5}i*9xWx*Y@6Pzg~C+F_bD~1#p>vC+1jVaLWk<1<_3EdY#7_< zODmJToY{3zxm!PBZCm)g`wXu8!X;xzDze-C_7@Jt#x&=`tbKUWo%H-MfcY?wbj~H$ zRxU{n`UQK+fqfM1hM_F4@s<SN7JmN}CmF7?A5jmy@^GGB#c9g_<ysTt&4#;dLMOfS zAg@m8JKU-={JW%=*1~v7lwAco)~o2zcXue8{alFs$!0iKTKpO8KaEYy%lQ)8CAK^F zu7xA*cPFx$9|T(+lMNNZPJg^cc<V$dY?<aBf<U(8DJS!tR)bS%JsHAC*rMe5dI!3~ zG^#_4^ETO*k;Ts?@-EmhDl7o^0h~sXnPqnFY~TInAc=V2uM-uvv*w9?^n*6Dk2Y*O zF;vBH9eO2uedF1$5-8&X&dI(qz9V^t(!;dLku0P5_aR-qly^bp5B$v7#CP|aMeFNR zzo(sSI<SW^nd?u+5T{7hlBfFjDkey>O^7&ha~6^H;k%ai+J>}Zxuui&n0>lvO&9!j zA>O<!4`}shLFGgM@Z<MoMtdV^b^e}s>F^-861_CMVpac|cYl;P<#c!xNQ!E%AXyYN zl`i4prRqpnji!Pkz|KjpZREY~K4ygb0JPUwfOZCP3Ff9DT5t`FoQ6%aj*qOT(;hK` zjyM{c7zF--C;O>1uel<3YMAH1m<P~!<le{wi%P(lEG`xFQaikw1Nw_^D|hVE|H!01 z&52JK#C`X6cltQqzg(<Gufh*FxC1h{JpKGPzvXtywZiR}s}c(OzcJeX5LOHwEPpZD z;pAPbU<Q<tUpGMW9>s!x0HJVTUFXwm7+qMx3tk&wERf?3!)^M7^Qo;|YxpOj9!a+! zPJWS{td&3o0xDXVLF!CGH)^vqORWvn{<T=!COXg~tvgGpA*qeLsaUbNOjd$p7@Nsk zS{@#nEx?Z6@@O%ET~J#=qF##)!%kTOUhwi#2ZcFLY4|f+7ab)+%OKLbQY)zJq83w< zM^S$)?%v;5=GA#^vvn$kwG`Z!GrSB4j!qPDk10l;4}ce!GJzKfoV+8y1>^wM;m-Re zu?|5}r6UR%Yoa?=atSpi*maa`8T22C)H6pM%$hVUmTpva(dxw@R)FHXll%Hb1ELY6 zg6i{q=|xXsgeCaG=}h1{fN%7xrk0re;un@Gp_J2-+$iV8ogm4o6H?o^47SKMnbaHS zF1A7F)iN`GioT>-epKcDytg^U(8%`qhg3Yy@NP~cKdsQm*T`ZRKIX_a;BMWH*uKl( z2@SDB27jax@c%FJQrVce!+%WEnqdAndFg+?lau*>!1QA^+4w~Ugq~j`><PjeiQpl} z5+X-~k{%i)2LUbB#4I_aTQax$99^OzfAcFJ&iU2=0VDOJgwhv}lWA|`c=f7f?gH#k z11ONvhfK=Yy&b4@j)9L22TGP=(#1>~gm!@KC>FRNYC~FB`K+W0h#STGYEt>Bscl?F zV_D12V>hv;sM3~?8(@%N#iIc(@;|_lS|Kd?th1n)%>;LVRMl|hW3j*9sNvc&unxc# zLS_o1{Z=hd9;(u;oVeeYYT$yUCZMX?ZHyUN?R|}{ao&I8Vc9X(&&p+j^s&^us7+=S z?0NFjVW`BCpPO2eD6MjyqKS-{g;SPlD2pZTs7qrF!;yjs&M^F|oY4KQce0`Z&IoJ! zfMk_4^Mw)*w2U)s4L-nXOZsLe_<-BD1FU;6Pm{`#|E0^QLHk^0`GQ0-oj@TAFBQyp zNvm09Z*>PuR?VWr!H@UV;4p>3Zxc~v2SsK_<KPu{HuZ|vRvTUEJrJAd;9MJ&&$GB@ z|H8sUcx6f^Jn~F>g`yn6SMqVtz6C461!&bPp)8lvOt$t`$W!k*Ad=Y%r@uVJk0r-| z`fax;J96-Zlo*M<(sVP+Q41l1D(5?)<McU)!q`HSLHj3JP#5au*Y=N-pudA0e=3d4 zWQDo4kY00Xq}Is|^MDsqo3iE}kXLQn+q`hIDy{kzPb$tpaGFB0cZ&cf5LWEwj!Dl; z$j%`JwGw<J@pFYbBIqd(n8vr;AY0Oo%x+f?&xLftHfWacs!DdHhzoIP-a9%xdVa>O z<Xd+Fds+RdzbyBI0B4V#$@WF|amrP`t#U+)+L&kk#k(&5Y}O=b%U&C5H$BB2yIk<y zH5UI-<7UqtRV?){oJKNxTLR6uGlTccb8{b2Zewb-oj&Y&koVsF?m7W!Iw`i6;iR^; zV_V+mLegK%kw9iVbG6ugX?Qs6%W29L;kBhYB9`lkVXn0)iS5!^pGvEE3G_QZa4g+B zWso2D(pyGWuiPg?m~QkZ^6tndcJ9AJSf9V7{ZI}=r^v;&=Ko(*J_QxF1FBypmpRA( z&wKEHXnFtH{y#JM7H-`?TWn6ZAJh(Aj)~3(cSp<7_EeY2MDI$qjV9_OyQ+;lcMK$E z;cX=S0DEyGTwkv)>;gz6{VCZ;8l=-OfGnNZU)yVwlasHfYW1HK58wsqh#U3U)&=yx zi*BMbWrCsN!(QrX_eoP<2Pe5YgR{-ld3Nh#)zl8@MYv-B#~tX~d$%RULHW|z`-c3c ztC4DCmGbKb{73fw<qUSE@r=rmA`2+N#rYc*z57qj1gPDxs^4d1R*~FVp1}^b)_oMP zDIQKS{j{kw1$fB4s+q{PnP>&lN7G)Ic});3{}lS6LNCi-gS+l9_Bz#fip*DreN+be z0{7d_1qhK+B>q+*N974JL;f}Z!M({4%}S;%LTkY1=xLv}qH=?ZT)VLrv<Z_9iaqa< zlmqje$e$aJlzOqsO!ebBB4jDalw~1nKl%VlyOd4j(zN()Bv^*{04l>J&OfobY4ve; zQwY$>;WBQe*Lxh=wRh^#OBP)$Y1f>GvWd8z2YMzgl-7(_=Ix#6W{r6%Y(Uq2X5f3_ zxe4+XFd@B3uF%SV2h(;)XsvecsH!~IpOl>|LW+<`o(`f+632myc+)wZvKKLQm?F5T zLuOPhE2QWJibF*g=sFGCL@Y(<+q`Qhv*9SDK3_TOsQ%W8Sd<I-o%Kbq^_^<5kg<5s zV--yBu&88X>!rP~RO%ZDt>c6Uh4HhtimFAxlQEp`gNUPVX!1d+O6rBQiSF2*TU*fX z2CqHvH0y7xmV#&lj^PGc3=WoG!m$uFZ{fW|iN)*K3VwI)h13G(Q}7f*#8Cm7fxx4+ ztA;pSU?pV%<hib&u}V=POwAE-s4HTyWc3Tq1uk-3Mi;_3rNx2L0K-cgdcvm?H{;h> z3m%eOh@J6N!`Vq10w0D)ftQrqIEgyVRuQPECgDQIT$`?jz)fJ45VQlYhS8`SzzlT* z`hk_G=4JUz%6G#wG$WE9&1AOn`^IUhgr_WDuj80H;|5Vz)xtq-6j(bo5ru>ZSqS(5 z;Fmp5FqwM`?7vj8fh}1LO8__rc!v2yAeI*=3P@|9_YYgM926)T(b!-Sm7ZVJrw87a zhahU-os>dDdG1&eU)pNBzM4vmKs)Xk<gAc87z=S6dkhF3!ru3HN(XR|Qw&R?U;u~O zMg}4fH$%V=R9Yl(qxNlXL{PL7XAb9I<#!fw!d%EMO1M}Dr|uLGkKB!M?!oEY$djC0 ziT;~bV_VMX@p;X~1sNq?psAez?-v8y4w49PG+vQApS~&NF@VVoq93@2;mI`F-Kl>Q z9rzn)IAnG*qP){uNO=&M+4DAX&@KqG+wa4`je>6BmJro~%6_M5?M5}7Xas7Ii55!- z8{)%O&S8DGhrq^>{EroeD29Wo9mpA%lqM)1eRUpv|0H`I?G^j3iy+bvqrP`Q6UAUe zxcnU86>-ug7#a)Y-LTS{DFnL+vJsH}dkYk39d^@ia%VzJWA4qv44W;I_||<$M?zGi z=vkWD6NzdErpg?i!2=hd@M~$HUz8>-7JDGk1N(~Xu6FGekMU)y18IzatlxB#_!NOw z$SBE}EKz1B_^|x?!v*ZM3%<{LEU}XZ{619FfeK1UHXxe;eSKrAVsvAqp9ajw(|KnP ziB^CXT89}F-VM&kfBt}Q^;6i#+jlgV5r*slv;Fc(Fx$0z-Nx)K@l8XP1;HAUs4J0# z8zJWrj0vn2-Fyq5j3I$rD0leoa!RfdIPNNRam0vO6ydoPK!`9esC#gNI6GnKJW?H= z(dQ&5sDIkdvAaDvfdaeBffF<jO9EWDmhwDW^+_5^XYKpm?omE*n}No$ZwqS;TCJGT zF=vN*z2$y23--g!#L7e12Zg@h>I*YiVnX;}Ipa-)e*>A+iq8*7SxxJbyz8SV%_HHQ z+bPyLV312{-o@dO#pM9?I05*eN%)3zXc2mB?(A9yKk@Vp&$IUK?68ivo<K_<9Ildv z55PgI)pRx5P|_PYKs((_+zUebJ3|l(L)+(CvQN5xV>Y8a0JF^<TEe1uVaLR@+%Gtp zyj@nPD+Yxyo3c}0V2!4xFz3#5F}*rF{z}=mr)cWYE$L_1kjk^Ib)`vV=o1%;s@emu zeo!gp6@$zKL1vp;&pds&f9oubOzMYh(u@Vn6gY^(X-pNMJi*bp!}yOEHv%{zIrI_E z?OlfvKYKa?lb&FuN;c*<fVu4;DF;y_7At?zbbV;35esI|3^|Q9#zO{|7R>z3G!^;1 z(z)Wgr5co<YSDOH(UcL2=DPEof&A0P%rTv44?+t6{)*pDO*_T`Y?ojnSf>!MK79|u zy4oC9buq)Jn;5v&(H*v=4wlv^<Ws98e~JdvH<5rqiTfR3DBoK?!YoBmr&Ow_VU~m* z!cb3S*`^pd!vN5L9Sy>kNDik@FbsHn+plNUxuhY(VG&O2zIQ@BxcsK@){k54Xr?Z| zv(C4#7c|>VZ5@>4i}#1?xS?d~{Ng3T?T83}YK{Ie^qO4sJoet~{wKHqu36)<0lA7! zFs+hosQjqY+x3OboA-Eoi6YLu2Q34qmN-B%HVtzsxb$TC9~F261_PuSEz5vg3?|{s zAApLGShsJWzhb!rQU2?#{$UJ4(?lWPf;NI#fWB1zVWl#cNshypzWwePGJP47;s}_< z9!4>KLe?WXkxBQ!CSnAe@fyXgWw?W$NslE&M(fEAJ_eZ%KtWH38F&-Z&37GUB;u>h zENRRMlt%)XXg#nJawks8TW38NLq?5!6Ut#J%nhdqogJEZ^Si7X`9@kRc;~n#Ed<RP zX&{@>9;hB%C_V~0LX4gEV}*ai8e=NT9+>w)jS{n}_1VPn1v?9e(S=2%t}|g+T;h*! z0hSnlK4k-JRkK%U(=nJx&&a(1<Vld~Op!6eLKPd0@4_bZxLdc`7U*4n6-Q_;>tWLf zW)^L%2SQ5Z!qLAX2Zsnc$s;a6g@XqWtWo?Y%rsV2%E!pF&@E)5;N>EyDDfo{=Q=Vv z21Bd;c_0r$$ReaS7Z0Td*E|K8^B0A9*mPB6&5fpURQJT=KYos1Yu6?(hGwyX2p*bY zMKtP(5%2)F>!snKfNxX!>cf}{7|rfFVC>6i+$>%-dsZtmIi>SrX2m{l`UMy$s!@dH zb;`}rZO@n+v0m|kMSG7y>9$1e<}F&v>jS0h{}sr5^4t@6ZE9eLQNY#p1<1eKohl5@ zwtg9n)?vTCbE<KicVsjdtl;ZqR8zeUJ!DDt+Fwv{mZWSv=ZeXGBt*My1P?fvkuA4t z6#MqtCaXRnEyn=1va{Viy}aXs=@9di|M_@ZoX%dWYS>_U`m(v`9kQ)q#(0c;^cln# z0PjOx=K{Sf6MVCKIQ-L!6ui?B)RQuj#}Rw-%u7bY&mJ^ETK-$ebkaxqd?qtgD+z^l zN0x%k&a`KwDe@uX(HY;-51LdyMZ#|U0KU-<s7LF4_V(W%mOTdz5Qzlp@Mq39y%Po1 zDo5}ha(08*yeLO@(3W?T=mPwf6DiXof49SWy8scz)OoHhsaEQ1S^yoJw>PGetS%D* z6LzyiclS$M)fM)L|6c08wBLq4g2b1e-jaY|3iqRUc4Q=#oOqHYb>2nvKI$GUgeslD zA%dGAV$w$Oil=#mP5F(Y_4O#cjPmZ(-8l@5p7_!<W`x9@K+shpL&rc2Fjs$+hcYPO zS~eObn967WbH{AuWa>lWgK`fS01vf+Ogk+t8XGO*0<DWESwmc#lI!uN&~~~@fK}i- zByvv8d-UAkwQ;bfKp~uNqF;sLW@9;+=43-)ayGRQ1cI|^VdLYY+v?Z-oP-s4w++FR z0n5e1Mk3&y-#)SPvjff}92cZ1buA7oZ@I*3fNk)ih%9nZw0ZBw929p4k6=)kDrCiX zvOP@CN3cB&kDVo8nPB5l6S2a$#>7Z1+e5>{?$OOPDIZ-r&dGYduww~`-wUAaS~<)j zRYEqEM`z*h=S`5VtfA!;C;kzy)jaz_h(kQv9ir7@JYEVJk>wbBpr@9d*YB7|saplE zx2u_03Eg5(tk~oi(c|?pTC?Tcm^HSs&A3_F1|kR&``O+O4C^%f>==L*f0+^8PtNZ? z=LPW$&*=$4r{L9ZcwJ%%3fXuL+>4P~B?=Chcc`Q-S|5mJ_MI^qw19_NpD6;-Et6Y9 zgdU59=4LG6kHUiR2ayg}<$#bl0@U}~zelZyLZ(4TSfJlthd>85+fHk*_+pIox%Rl~ z`1CDP<of4|ZynWPTMHc}3cIbR=VeRL@Z7btFY-?4wv#%nYlY$-YaBgMrm5%`WvhQg zCh7DsGx^5V!YKPWKS|<l)90N@raXZu5zX&uAYH?_r^!@@-vAgjI6;(H59+0UML)m_ zRgnU3**xjhJtzri(iZ-!G^2S7yzahM2+}0Dyx9oIAT|yst?QlW+%4|FmFl}s4Rja; zN?02L4xHppquc(sss})V8r%}NoM4z$-9#WT^$})h7l2o+50f1XQy>&Da$6bPBl$0B z4n?2Es}B{j+dG`&jr@*Y3qU(siO?AuwPdWz9lh_LkUleVSLVQP(8QOLW%Y&S!X<-T zt(_zDmz0sQM{3{kzyEw}kmTzn$T)0H`kxfsC|IN9c`AgSqFN$?0~ODCDPW&7>DhB* zb(Sry>3x|c%&v0k`Nd9fq>V$jJg8#4gdq{aT$^O3*m5b+<5W?0zgOwK++S^)qWvjz zui1VM_%viRzP5E~Ke(l4%+S}nt=3DmItS3ab%cYx7-KBTx+#q6s;fiJRfL6^;bgBO zoJlWF@;E}<+QEAd?yXsr@%d%nL|+`sV{0(R6`0{a2Hl0l?Ki+3NMO@E%Tz$85^u?$ z?Zk%Bmsa^kQ%e9eE2gq-Gg2&YqYUj5Bk`v0u;M{jEor$#T1nA&M|qP2o=KPBW={6B z9_G?N7NkDu4G;f8q7t{4=x{}CWrV^(tBy}gU59S2sjo|xWyDeYQ2C)ci}%ZV>@3ng zJ`-4yTt7TK?9yK7`J!9H`tG9omi54lj5_&piAC5~)T`xGOiy!-;cx2}Dr>5y8elo5 zL1^-@sh#d*zwnM#CXs^?*a}IlZm{a!A%^zU{b~G3z}=fym=sLWd(Ox{*28%>j>K3r zMbB@aJg<x&gwa74t*-8M<~`GZH}f~c9jDXtuT1Qpi6!>H&h|{xI1B5ap(odA=%#af zf#+*wog-P~Rlj*$bu^O)uWK&W4)iZcv3K_uB?*MugY+l^)>UWcFhQkIP%7p*qK&RY zejzfn%(Uwkb`qb8j75=HDA70}!Y~6aE}%@6>Ach9NBE&J<7gVEEw&JI@|kSw{k!#j z`go1slpGk}*Cn}VNBA6J2QZYC=kM|J1^=r>Vt0M8Ate?YYD;<pO1jkiGb%4{G`Cv0 z()EXTjnZ*YtTz-yJS_CZ<ivg0&jnT_Vo}9kWv3w+B8dWjjJzqzSRNe_J|GEsM$w+E zA#tN(sR&C+e{_N)CUmbIjlR)!{jb<Jg6SWXhTPTOUHPY@A?zw&6kns>ie9bXUT<oJ zD{ag#;UUoVdW&!S@JpG2O-I|72Z_=N421qW*FQ(KzPnnWP8=&qs;7xtQ;i_(sqdq! ziX-=%a+%EV{>~n1T`ZUVk$)-5cW+YK>ia91(Vvh=X<Mx%k{As%JGINyz~0IWQSnMp zV?=|_<eAP*Lm%>&tl#M~+z=WyhjCORp7Gn9zcj(-`huX(^-MB@JhN}5W$3<f6|ZH( znB+V7`2H}$6$du)74$Ax=4ZQ5p)GrJpRYKZLq^|iPR-cf(0zmdH;_yfvEa!E1^{3J z4*-DiU&FrtgRa<`*jU<`{$i9V>bCZqYzW`A`i{zI5_-1J*OPnGpiv~EKsGh=7;8|Y z2o{m8EfOT+N^V;OKfdn8CX;fFtw0MCM?E~Reo%uB1F@kon05w)tZS&@GHR&EoXMek zk_a*+Q}2xk_4@{q8@1`7^&6G6pSkFn=4uz$UFVaU5Y=1tHGn)-tdUd4VhK|QO-ZDU z)^SLK3@|~3N=n)ZrM`>ZzRb@$mKlysUCi3ZAkehb_1dZtq%5cYC?jm>AmoG|l4Qe$ zYl(A;R-PqFAsgZMr_u20{`gHtQ;n(nIJ>j14JSc5&SAOdT}_vr^`GgE!_vbomY#<I zcZMSlw#vq=Lb4evXGzIF+D(n{LKjgf!|J(CH6?P?%}KN!=fbTA>$Vqr6C0HqrO|~) zWM`oLkTDs$fhi0-8!0GvXi$b7YMF<<fllG}%Az8~PHF?U3_z*V(sEn)5(r_c8fp+u zdA7lDoa`BN$+X1`k_oDupxf4Bipu8lOE}zrivIF1X49};uhT#TRf|kwN0{dhbJFf~ z)Umrv)w}__-iR?o|8l6*J$UC?2g(k!=>=AaNXHuXj|*>Q^797CfmVY1x!)*2UoWzy zrfiKBX-@8+o?-L?1mAe#wK*f*|7GpuhU1~b-|jUBb{U^@M`Gj!+<oC6D--a(CJ=ds zte(POC8l_0+B#9dl*YS;_(JHyuutU~_88!$YBOvJ)d-QFf_yKWxu`En)Gt*R;uy{R zL=s!ZizAdwdl$AUzSheA&cAp`e<y}U#8*}lUUUAH4Sa$SAJT>NjbKy6Zyz&>qvVLP z>Uv|nC}@D2Q7opA;r@UQ4CI7~ZZZ&F8J_<djEzVFMNN&BoeehdlEf^<Xe~(g8wx)t ztuyl;2%!wg8l5*VP;+9g&ouR!Y1}-M#3BR-V}(aiQn+SQ83wvrNO|sCS`&I^bLtn= zmKfLx8hwE4@REHa(;oKKKO14Nv^tABRVm!Q5GRMHdU6uMrOM<KqFdqfdQ-BXtmm(^ z-Lk$doqwA=dRQF2{}?xA5uPX&X2TTFP4}9pB_%gT*Q)ytWiIhY$Rx)zcCM~fR3BS@ zJMdncqvxLrD`DcAnzeV##}<1#^$n6ZO)t}$rS!)dW<j!6G=V{$`D1B!vEU|AkG`7O zn8iNdrDkf_--cT0bPiT2sYQ#zV^YLeaBwEJWkD0J`*P@bRlt&xgpqVAGSoxXtmUr_ zq4|)Vz-B*qpVl<_n)f3^&#x;_HzRzoZ9ZjaOBFd7MS_5c?mEos3A&8aBA0ww8rA>< z4(b}pp|qJ@*N`)Nd7N}fC?d|Y<9ynEB)e<0(r6>fTxA+$W+G(xMWSG8i$mKxH~`wu z4u?DGoJ$^GeA-RYD?1-U_-zQ)o54$zX+2{XOcs~tNxItnPo;8^lT<XD4z5#2yTiP3 zU?rG<DcZPK7DxurugpQ;Di9T#Ba`0`?Iy?RSG0Ev(?eO<H@zqu_I+pX2DlxXfTqjV z3e>{(W8zoWYHG#Ip3kb#J0?$lOSsKiZIAA5XBu}4gsvQW_!pVfW!dYZ%ATMLQ<jM* zb?aHouC*Jgh}?ThtmYuPvLnhnk(h%y760}C?iR67d=f$QmS$+H_~~j8A^S^U|C-4t zFv}dhT*N5XTx_%Ex&b%i=lCcDhkG{p-f>(TY&}YPdMUI*w*8#AS{!%85v=bjh%^7q z;>8p()`d+~gjh=ZOq?Ia67mtg9r9F-`_oLB;YAz=B=AqyMhs-$kQ;73`Nljaxu?`F z1Y4>^FKmXS!vXX{4UPB<d<aqp#*p~AMl$J$lVNGIoyrksl!T6iJ`-eBSSMgWuIRGy zI+3S0VpKPb&+zjkA)mYx$r{FC2hGLjXYOg>BV2EEbeA|fH;0^(_?6pMh^QAj0r)vM zWyzX-WyVPaDx8IGi%Kd_6+?yh5i)Go+L*|dQ3r*?rh8geWfS)yV^%zgL395ms!CP& zq~7RK4Cx;g#71_1iYLT(meRl3MECCp;HYEwYNcipKEYbMG<xmy8~+V6R(S?EOu1}2 z%k0mE56%a~&1;+H0@V+x2V<O4=7k5vPS8{K*}s$%NHGt>xLMfkRPsS^+9@<G)vBMJ z8O`l9pZ_c8B--1x(SiT~&_M$Lp#NVv$HdUo*52+vDM#%$<%q%jrX0a?XcEJyIX~7t zK;fjq!LStz-uzHQ2=vG}_!POeE@dRwHGlkDxw=|d5}@oi50bA09|<a$L2gC8tP?Cz zNU)Z`Aj>5O3zT$Xq49b<kRj9Uak}v!18<SoK9rzT4F|P;UbJdb%nJ8>EkIXoOgicX z4kJ~;`6X85l9?a|nBaibMny0zUZ2J59u?U@uOt^P2)N3MewU<Z@UvroM_Z+})}yZ8 zwsIzDZfY#Yy`oaoV=(KCG@X+Acc0hq__{JOQf3>5_LAGSDE`8=g=N@d0@p@*T6PaB z2}4O0uX<ChBBl<usQMEb%9V4RA4|=YAPbdgs&8Otl}bfVS;I!~;<ueXZ#~6Q$aX`W zH3su{dPVdo=|OD4$Vb&N_!$G(E`YPh?j^yf1p|B#K}AHg0o!9ZPo9y4>g@SFqZ>vC zc2Yc>XQz*khLMW|E}oZmu0OdTwf@2Ju>!ZNd}6MP{g5`OpY+4QbtX$34*iuEL%;sV z<B`nF7x%yp$d+>>dI&;Ul{SV)2CJqxoT&ruN-;=M;jrg8Vdz7L)?8);)|E{3xl((X zQdZeUPzBU$$d61!j17es0!4vpVSKww;yk2x6oJqJ^DE^TsA5bdjIW}2fu=fRD<tE| z$WYOhrqk~BuSF<%6|XR<p2MNkWy&6HnJiwPCb^b_$@B7CIEQCx6r7H-{xybX>(qzL zZ5cwE4M2xy`s*3S#NW|Mn+2GB#`9Qi`qNgGv2q2Lu%d!1!n$Y76>pn}9sFimYFiwd zK8nvu^ux|Q<Czh{4yjL>)@CwNnO=^~dV}UHHAqX*q=|=>F~7N-jQpHu_29k{?fl@B zj>j2=o(aPK6Nc0RLH`^?N{?%X7a>%f?a5EL-79i7eX#D1u7y-C%uF+3!O`(J>?RIq zw+qg(OtJZOy`<xBLu&mIBjOHATqesv_5m(++#nyotaSYl$giN5o(XvYGLp7$-r`Jw zWkLCBwwesH^ycPJYmu0s??!v#KH7{)QT+{hRawHe7Ww!@7{Hc)_%htumxl{EcbWz2 zJ`J|9JGaEr2A{6;%X-%0jrPL4BFgbanb-66=Ihrm8XjKWVeagUgPv)WO#%!XT4mci z<O1mKgnLZa^*VF2w>-{7Qc=w2D)&JUZg6E;OwPps>o*Mx|KC%|2Oh1wDQqG;kkv1- zxU-`rx)0x+%@SdhcsHrUT#uuu72El-+B`*)4bd$7P53-MJHK7Oh?nB^UaPgsVLqJj zgQw0vsNyt#PrpaZuSFO0>!OeUc~xoo#3oW&y49Lv{I#*?|LXA#jaHkRQp{BC=p$cM zuLBkIcIojE^zNW7@sZ7o+~tMeg|i+-Kj{$Q^lzH7J5vRojOE{6e-`8gOO9TyAWt&= zuG$8x;C`+B`Ck#H{~)-S>vuH5{<o_AZ_UvERJA6i4*&V>-lD$!pOV&pRbP+<f&?%s zz|~%?C!BO$%|JSg3!A|stnW8tLyKCGqzqB~>wS&;muj0&tHrB<=IP!Yzw3K<b#;YT zZOqwbu&##v0s#f%F2u`J0^Oc`2a|-V5J)9(3^YzuxCsTC79Vz4Wgv76CZ!N3oq9{) zFZA+ubwvXqqG@@U-rE0LT^?&mjpfWuG`dv<(AJeui`)c-ja^f7{oVf2)kv%V=uAqe zw=R}i(-lQ(msx|_N+%Ci2cL)=)@MJc1HV81yW!pi-;yA@aBct|)Vw503s^(b=d#3) z{4BLf7LG`YQ6%fXlR`&LxF3clE^;)PdjhZXE}Fp0UY7zW4Ha+U%Ic_Ke67I_eS#?y zC3qf**Kc}A5L=E5#xdQp5w%m+oXkm4RgGXKr{yr}eY^i-O`I!;C{v3-8mX_nJkX!* z2;LafkGxfwOV>1`VA9U$W6%87TiUL=r7`vtXo@k?FZ=q&xa+*t@8K^BE$A?Fle{Hw zNQL6eUEi0fpfv(pUr0wy5p<H`$}Qar*HkUSV6K^7U+?8*qaPCNfD9r?H;?y1-F`Wb zxdW?so8W=S;L}=|a^8CeNvX0UX~?A8{iKGw`%ppgfZR1Fd<qiyib{GM>1#j#WW$oI zmku|zL{(IYpqa2aL>$J4E>3{hG?}+nqgH<=Ur#0u9`aMk@Ffq8>Uve7pn83f8+VZ4 zNE!K-ZL0YiYncC5ke;hz<4vkGEG@wZ^&;A?i=yV;ws;|n0S`5|NMoK(bwtWjDE;^} z>wn|l)#dx=%lFam`R1ne6v%Ra(&W>FplEXE8083YgQ=YnGfhSYu{@Q3$QjWx_DZFo z9&6KvT2I%Or~XD37JSI9OrOcz0}h_YpG!2wxq3GFSt6e{HNjj}+Ec*I;KgM>0Ixp+ zGlTWq`@kZI8nre?+g(6~DEXaut#~{)1d7W*Oie-pLdLhmE_)3M=W{r*9GPK6`cK4w zmjjcHR?nQ9Etd6>0@me<OAEIF?srLUvY+DGkWZxf-@7J>DDu7y<5!HyRkuVd`Bn9d z+f+Ue#?+)mjmKvnPP>S7A3C$EE$Y7*uZ^s!s=ME3vu4bR!q*)YHpSuY0|k;6R|3q` z;bXhxWa^^g5;KAa6i4vy<(WRg<Te|x>}XQD#w>_F=qbH<J_@d!br#!2d!*L$O!l9N z@vohb>t<s`CaDAB3l^UUVbysq6i3^hqb&vDZmY0^WHr>;gajAuozEQU^{?05$D;eu zex}Wf97JBV5q@Sg!D<#5>=s@2k7(BLD7l2gY&shhd<PsNK7(0C<}9`6xQcHp+*n;- zn{Xj-Y5B-_$U?K6-tZ-Ur6CAwF>wof-Jz=9Dtn#c2YoWzHv>wm-SvcB28!m$Iom8# zE5$2g9DFegoLxzMs8!UZ*5SISRZGDRWpKsnR{2S51cl@E!4%vy9+Y(u%7_iIK@MJ} z(PT{j<hILG8Te%5?sB|Nlu&Xm=ELJ9i_XL29n+1nrEkJ20vWzo*O#I1%!USC7Tu7s zY|V_a6lgG(SJ&0D7^3ZnUY+B6)2q*u4yf<ZE!MY<R)l$P>A<q;(wMg>kn#+(rp*L? zHKQnt6H&0C1_rr{ts8eh-8%5L-3t={f*lH4Gn)0H_{FDJtS4W-FmZG`Wun5X5A52q zbyUIf2dv?ipISrrOq$}nW^Qh4Z_#X87F%dAcI4b}s>oQGfY%aKnK7^WrWV?_LtWCP zu?ElC(-m3eY;FT3U)N_Cuha5kg5>`jLRcRK?jHP|ak_*2e+FAiGqc}do1?ZF_Zw_| zSJe@2f=L7v8*Z`ySuGAxbn|u5b&CY{us~>IZQLl8NGqnH1iyQPFK)JrH~)#Q+)X*0 zOvzR)TfvVcWn(fFt7LvU^pb<pOCzy@LD5rngs_`W-zg%{Im4n#OoEKlC-6IX#;I4S zBHUG~Z6KKjDOE+SwT=)kNc-=PkeTc*0U<RWbN;=unoKbvI=lOwS_1yJ<e(^X(5xg% z4DdxE0H2PJd!on66^_{$ki<HW3HZDj{x#dxb$f<?>)+eEy*-_f|Eu|blT&F$x7UL_ zBcO!JVB{w{E)S8JLybQeaVEsX902$Q!DTw)rR1HRSj~oRT47oB2WhGC&mpNed3soA zGTd5+YBZ;`GpRHm(v`@Pkm_R4rL{#r4R(O`XYcYSV)5iBw~Wt$=__oTLyZTZ;@}Vx zGX}huEo*5iQ;nuI#pgCdBEr)az%?Xf5CM9G+i~-7U(dZoH`xvdh(BqPrGyw<0rBWA z1|G0~h$l&B;u5Nvi9m8>{9KQn9t0to{a<o-DIxQA=GsKWBIj;`Pvx+4{isO`h9oCV zcDCGtdDLRMHVF54$n$uzVnC9=XRz{C;d5%wZAiH`T5TA^0QgiS+DIs|H?o1GM`Unf z!%#7X<_zqT?vt>Y4kE>WIZyuejK5`EbgJvptCm5Sbrvx>LuP!Q9v_X1)Pk2{(2Nmb z%^s#TudHfS*XF3Dn-0y3HqylO+IjH0&!+7OK(dQaT{6q3(}X#gAB2b)vTyV02@Dp_ z4-U8d+@m9<oW`zjAUE)0E51?7?@hR&DbzUH@pfPpHxD36ox5DlZSb|w$A*+I;!*7} ztaFqmsYW+mJ+Ia^wR~sN@>e$cV~Q8`Fh-r<daY=*3pnUx&)PXj8#%-XAa-CM(4&aO z`=vgnjmR}KFt%1((YUz_X*6=c+aD7ncN1)LM)LW}MfA0Z@%hFLqUhGFsUDL56p4rd z&$!^o1KV2gvrotD>R<;6>h8|J9+_#7`?bF>dpcM@1yGGz`r>V|xi*^UD2yIDI`al7 z>6|Wc8g#IrAe&=fhu`Y4-@(L4eoEAsab|0L-;t`!+(GwUV}noxcfBgI3GoCk*Yr?G zI>KZ5h+jP`;YBW9H<HZ>>ztJDXJC({na`MnucHf~bZY(D-W_vG`1pvvc#$`KzeB;2 z(V%{cSewzW8(qt(!pn0mP1ZqG8v#w;{U!X^c55SSYd5$bJP&LcqJqIvCiW$<uUoqV zPk?i7fM##!x|O)>wNKh#cK5x5|L<EL_;eXz`>V@7@mpr${9jF+sgskvle3GHsiE!f z<&RbAx6A$=aC}59x&}-r=O;koWjQZUT2|ZWDM>sI4Rp1(vC3kpqWIZqfbAmLD1HXK zGdBA?nf74Inug19pTmS6bmR$IiX8@QL5G^t3YtC|9dDX8`{TL-vlm~q#5-olvNDK% z?vv_A@&j^9Ua`SE#gj7tS1#6U*MK-v@Vw<IecxWMhJ!o0`$vHuS9^c>z*TscU2Ufb zZmKSHkjiLN(o2UEt~3Y5O{vWSeK$UEc}1pGQ+tF(zPfgrK`Jy4sGW(aC`~M{`frJC zs%dN?4?(7Vk0_%E(Qu@aX9^&#bfjc7Ejee$AX&RGEH}5|XK$Iv8^vc}U-mKLWkh(< zmu)V^$em*1CtWlet(yV)H1-1>PGxP-9k)##9fj(^2mIZ#g-rGU)Co`6DWzWn2nA&a z!jEh84@Xev3AffoEg%R-Wz#@MjZ*cTBq2_S2MuWpHvz<^d;xR;<5E;o$ZJUJ85VWX zlR+m$P4?;Ifj6akk4h!QDu*-pW4Jdp)>ib%s#1hSq0Fvw4w#GFljIL~fP29$<>B0I z+mNqnfSl}20*ce{o>yCAWq_AXW@LR2<Ao5Q3Hbaeh|XT$5J#b1ba!cr*K~<-LukP9 zan{Y-Ua&VunM}8CHwa_LatXylVDk+17$>}bcK9tmmpOmzQduW+g(Xq)u`bN<^}P(= zjyGD~u#XsTZ<RSp8Y_Bn?rvJ_3e@VQtQ0mr7Ea!-4i;TK5@#%yfLpB<h4sS0GpEZ8 z2W<aENn_f1t=hNdE;Q+;XOz(Wn~2~qY0rns8I3ONZn(ElBm<qwkSe>56S{wb=>z(| zkLk8RAIj5j7huTx|2n2-zu>mHli}}f@_S5&v8{haE$+MgK&{w?sl?|yUZkri|8#5- zcf1^2F8v)Z<_is!7&H0>9SCTtGd^E$=mZc5D06a5v}R*vk)nrhjCT8Sa&mN`*Xu*1 z3D-+2*U6}d4_Z&|$W1jfmI@I}2c1)GL>~yL?FZMdNab6r&rg?aDKX8hcluskvyLhA zCmyZfDOSMMQi1?a3M)f{{mhEeV%NqKe5u!R%A2MGS+#c<0;%^AHkVa5$m%tkP-V7m zMHT>FDsA0tfk|{B1&Jk93539$?^`O|sy7g7qLbM_3u&892FQp=_}DV}2q5vGcT7|v zHRuQF2T8rvPu9T!4*?>^0PI0~tw@2^&F<Lq(M7h#>1qPj8m-J)#uUL~279|BA8l}N zJ({kIbAn_tsyDa<&yRpY9D1fns6psUl7ML3(n%Te(55EDmUsk_NH@&`ASszFEWxHm zeFMUc(#$1S03fZ`s$9TABoOpk#{<M#s$tB3R}*HR8f!g87kR>{jj}qZH5_-blE2@d zaWVt?Mi4)KYIwjdJNFI<0XYGs0IvBn&tXINGqXg!r@TR+7iM${fP~_(cO=8w2nuMW zC0Er+xD!v+D*+k$`yx=4>tq3zCGBS`!2GL$Vk-6wLlFMqSPD1C%w=H(l{C1yJjORq zKz+TX^&E<Mv)o_~;D2tRSO8bI22ma<1r&M_I_0P9J`o<&4>@P=itQcAg?2{kO9W@> ztE~KE8lPpyo_%iJsYEL<K|WR6geRt0_8U)XL!y6B>kGy5i=&vExO1bK5Cda-_n&Jl zt_;KZ0st?ep#c?4ZgCTzkb<8`u6Sj*HU21l;<m}Ay3f)75*uG`rd@qpzpnmmtA1WQ zXQdH)w5YHjM(xlLv?ww**ySe^JRxjEu>zt}uKcqpuLZq|_=XA#_gXLDN}z3U{^b~( ztlLK3K<-RdVoyn7C2KQet#i&nU@13Tc8c~67{Je~GU%&?uKj5OssjP)M0O^nKUP>o zqeVC~n60e^^oDx6hfQZ7W}MCS|9W`mKRC6guhY}l?d|sY#*JR1&Xun3(M%fr+n~ND z|5r&bRAri7id}!<Bxy6ilKm-+e`jpiv;S$2h?C^ZMQ)(_k?I|Q%NtATd`CP$#XzIn z_6?%i#@>G3h$eqI<~zH|zD?+TFRDf9-H+Ub6oC`OmM{!ZAfQ0nQ|(PXuyt88GfJr_ zm|++bV^=q<p<I`G{E)4x9k}S|EXCWl1Wu$`N`ga6MOJI+3g1s|IBeW@`6NFFzcGhi z^siz+knyS0K*iMhfpcqZrH|#16C51rxKjTM?`p&}Lm8*E18(cFdr(a%fc)-~?Lspv zg+qLlaWz;9AnKE#a^gHty&7c*xhCjdId*G!iN`v|C&D49-5LTSQUkJOKw#Y0TF`t1 z&Y6rba4O(OpOeMVqH{?&w`PcH2ENf=y@}#bcc^FVWR#iLjml>QsDpZvESK@=)xYcL zHtCnnV%mW{&<)Cjy5bt^F$4mFQq^<9D$biXxOMEkggrV%dyEduLc-yAfd;5<T@xZ& z>Vd;-#Y`Yf0j^KC$MvW$!1mZea3^wsCcjV!e4@Vfy+eU$!pUgI#PG^H(j(u=M7*`u zxq|dA7s^*`q_Hhb#gOL^G=s~AYGek^Ti^3gel<<Y`Y=sMpxmy$WFpao!;rTnc?L^? zJ|2*#f1b^YsO@gM1_1dz=nN1csHOIat87`l9?s1Xv6GWzTdQY{+)}5!_qroUl>G)e zD|s+3^v43n@>cl78yUGmtWVf?`!BC&E*5bvDlf7pjEJkWeiN+`){dJe-ElQje5F%6 zrBXFAd9)qr%Z!`xYm48(a2Hs?Qb14Q(ahQCE<!onUf~!&XgOqa+pR(p<Mtn-ktVx| z&U1)t{DxV=JSM{F*l$u}Xh_WQ#w7adFfJN8TRbSQ3DG5fk*LzSUmzX%U{77)9kFRj zeLLzb!*Gi`h+IU?d*Q4SnxF>y<E_M=)v{RX8V^Ei(n<5mQjrauD-E1<9*{%J#O!QR zp(Ec`$r1m^(dsGAljjtwA4C`W{eV4lM-xB*77>`1+0q%Oj{BgObXGA*v*xgsTF6^p z4&t4j0h<)5b=5dY=J(@0uGOK%jJ@c|PH-~zE^aK=-a_Xi)~^|1%TbH!$ZMuR^WdK{ zgy{gGBKjRTid~f@ofs_awZJX-G`Jk#2qm?lgz0O6*v<ZN@qM(9?gkmDcyq$*22Tk# z2uTDv<wERw|KM%)x&zYTm+fI398?vw$fCO3h`~PqQXJ`Ede%voFEuXBy*<(gYTcdc zpXil$q8e{EJmFj_K1MNvqitY2k~anBc@mJ%G=i-rUj&^{cYgRj=ICa}n`3esoB}^+ z!&1jX3?2K2TqpAXIT*|ygKJDildUcG`9xa0w+vA3SbBn?_M)%QfS@>Cu!Zk(${#R* zt}vHlg}y2<S(<&AStmPfa~eleNKw_RyYnU6Z^rW(4$l#Rf#vby!Z(7vru!XqX~Km- z17E6}kb(^T;!UM_z+f;*@K!*rA7qv1!wD{2!U57yy!&S$)kv6(%&H^QP}oq+BiY2< zL;bzw6rS`Qzh77f*NT`sk3%soc*b3=ut(2I71juFSSv~aTKA{swqhENUl+#8p-x)p zL<6%+vrDO<tx`)(m93oD-g%ixHNq%%EY>q81Q$VO$<myMz24{mp}yUDY{co9Lj10L z^G`}fcpry-Hc8As5nrgRN28nv(gw`Ar+15gooJtf<yZ?8V9yCN$MpnCgq{^mW`EXc zEA+jLTiDcpr^--y2COV=NZji~l!zv*So}gEr({QDofXdcGYxf3NNv9Pq_b?B>uuqQ zL`8Bm@N?q&eGdC0+6LTwejwR~w5P>CTNQ$?AF2iO!X=Xobw~%5Z{#K|*53>E-ws6= zmcZTp18tW6N+=c}JIAtLfIK_;*HOlbJqNYR56xUN8EcY<sU*aRtdVXApHniqUUs5y zQ(T>EZHVZR0ux6lR~8N#s6v@Y2;yV*^MRNcr;9*Q6Jnv1FF`1+*M<>_Y%^>97FH^i z2B~NJEIJVHX+wqUuLp0=_J6rrbBn{0c6~8Z|B(pfApP4RC&-d79_2Ii8_%uh7yJkM zqXKBpFm*~;O3Cfdr(_;F8@uI3SjlFjuD%x#-5$u2big{!Po1S_@tgPcA(t_M_iF`3 zP&+lHJl4-dpt)W5D1erQE;cMT5bl;t2?mvWn5M_geZ_%8EI8ksCwgd0Q>S|0ZSGf> z|GeY#rnL!#enXh~1EYRjve(h;bFn%HHUL0kjRMPpK@8ImY<60?e=jz4=e-oVRc0S* zCaK<9Fy{l8WSu*wEMGvR24eHIE?J6~_-7ZM8~4S_AIR9c60wr7Y85ue$K>5u?TIWH zaur?TN;9~yP>T=#RKT;}oxPkz)^nYgB(18GV|kzml_6G_$o@g6w8QXRtB)`LQKC9n zyx(i*PzzUa12w~rsvMlB*Zjvcg~SAzVKO)gBHZiC!j5kaeR`EeHO&?d?W!a46#oH- z9BvoDwPY?`NeOZLF93E|*PrbMwxT6POrBm#vYaFOB2LXo@w>Bh&HgGX#R&gLFQ<W0 z2mU;ca#-ot?)h!ztRYZ3|ER+kAX}|9ez3!qP&?sg6)ugp<u>T7-qXm8W0}wq)W@nd zY~gg;&dMMXFYNi345knV_+dtVchP~X$L@aidVvBJ;hfKe^w;7oxA$fTf`WZEbQ{h^ zyz{t0S9V&y)j&Syjib+nMFWpF75`O^N9b742{>xCj?3-iW<!wt_8lazg=?g28spJQ z48Pw#{@#86*D?cU*Gy^7@)G3s4-Pj}(efYz@Aw^w^`SKia<%0Xn=yGhl+0R_oQe)U z=lp3KA(%MC%UnM7J3lY*+&e)VQFjU!m*b@boL_Y|wpf+2<^sjxB{X#xjB64>PHDMs z!!21wG;Vh<kohrN<#0AWkkre4X9l0Zu6f5_k#JmVUvprMvCZTMDrKA}LKw!XZ`>nt zY?|}(s5LcjQdgnYx(j%uTl%vR7(GxUm(dY|fhr4+rEPrQ%SXHIzVll*$>=|~m8ttB zO_jIbdo4^2$L^So`b5hYVS%pl02jE0RrE>$r7%wNrM@sp8&Wz+S({!<@gz^FI1xWP zB=-#Nmf@AosVE94g=9eoKwZQW_k6(HaPNTz^lAJAo-VvW)`<rf3?kT8=gN8J1>Bt` zd`nsWa2gn4Zg|#ZB}G~?h~b#E{)6cJyxO1GQVks42Jv*G1l-84Yn;Tz{69`WwyWQG znPI*|pt<V>DRcadJSrM-Vgb&!rWOz*fNMzMzT{u)a}AJ_oT~5wX8^J-j#O~ngNWsm zX&*8#JrXpHp-Poz+})5$ft&qom{7Alg@H~4a@j-e7!OXi)Fj){iQ6s2a6lC3xR^?F zpo*^1i-Lu5Sp<Y!uJ-{<J_{n9B>+k1mzTA51zJwu`4xqtJb!Ob76*_=4-P|c>cW9S zQ?nlN9i)Nq4$^}JVYp&58So`;bBaGYHMsjloG^pRD9RW+DAfGaO)0I$=)jwUYacj^ zCTYt72z=&9;q8}kFw%SP%RS~-D5jIs?GlzzLyjRmw>fNTgATzf@!$8CUch*!0U6J! z(a7##jjF5&vM|_gNZomXRwyR^oh*qsE8wH@bw4qxGs6h#-ml$~J%7woyJSGUTZIlR z7vr0%7ItaT+(rI-VwtB!5)eSLm!p<=^aAOwHrg@|qOAhu!3SSzYvj^+{(V*fe{_$+ z(oK7J4N?D+1-j$>3h47E+M9&5kWCu`Z*4JmkrgOp@yPxmirbfY*+$rz<dXBR9xQaw zq^3>dhmgqk$5+GhQa=hn|CL_>cpdO0%BP>>rV%V@QEf0s51POT7Lo&uR!~{0%Bi=D zKp^_Ky+m7HO1QBuubG@k)RM(HWtK|jddGA?GSh&5;7W<a7o=Bg<kX_1^{ooo1s+h- z|Do%h+CzbsEWp^dZQHi(oY>BZZQHhaV%xTD+fF8Tp5}J<{DfWKR;^m-Q+z>q>_A8_ zP1J}66gj4>9m3X}adbt#KJ7#z@hF^GR&!D)$x%#HTy2B}DU|ceNrTM6me!|=u&7Xf zdZ}zl-AyM%F?+@5bFj}n^M_2o#VxA6x#Q<N-9TP#^9Rl2>u=)JHbcQgwqM(Ws4Kwt z*?6H|goS?P<;%q$aP^0R7{BXV(RF~cb}>SlK)3Ug`}slCc_NTvpWd(T|7+)2fN()q z!f^uq&(l|5hgoy!F-Q05@7SL*yNw%Upb-jldlUJdTNQ_+{eJCy`D7}6Ne>?BMkTo^ zkdo|gLxZENn;rW8$kWIh!Uy9A`D|+^cfs81phn{9_L%Z4xa|R;U|%n^iWeY<7R>RF zK~fItXIarnI_lo2R-KRSmpmT4HSsbP>?LW#JGgUXr_?(zJUq`omVWnaUH6vHdjE|U zXaTH3VE)Rgaesj=!v6(o{eRE`BZFVz;eU{=_!-MzI@<7!57Zv+Jkvfn#w7#T25?pF z0LGBx;qYQz;7`@WhP;{wL~x&1ZQbYDf_#0_y<eMgEu+6P;D_?O6U=UaB!>aTdLZUp zmJWyDlnp&`z#NK-5RW_joCAeH;xOP^RTN#I>rUDj6cnn(v$`uQy_5uHaBJX;DOx+w zysV=PsIJ6a`}uYSwQ=hHZyqP4p1ISM#BqD?kmA)7AAL1Ida?w_b|GaHis30XrLtpg zDk5+ZR{uH#qt;1zyw#NjVSR}T`=AiSf%CL>`w1Jc*ydMyscpv^KSZABqIeD=Pd}mR zq5cFf81EjVy44nZmvg1%sm81Zv#6_=)x}TnQic~i{?>D$JKw5ZcV>3`OxNrG2<3BC zqD`Xwt*I~+007qiuB)Acje(PsiNkL}yHciN))`QOUwk5{+acnCh+qa}sOMx=7l&lb z0F}@oG;JLKrNg7Gi8ESivhg8Q6ACJj>50hs_)Z>V_l--9p~(B91#SFF#mix;Y<{2@ zFa*Y135-ufEL<js$p;dpOi<*@Q^-*XWm{`4y=7YS&B_gBA;OVLSZUJ3>T9GVo06zZ zhVAQWKA&pO-*O>V(GK8nAgbNrS;@HZ#d7yLP&7{eG#sxAFg}v)JsLc7_<DSg`iL#h zLDM;|YmNKNfeaC4fg_9=-<hjVgYrJC;-D$xqd}YB2ZzhkZR78XicS9NvLTG28V$yy zc9LJpd$XN(V@3{u8o^`McyXEny6!pT5E3eEyHG0cwzb;lTX%6}Jt<c^gNq+&HT!in za<nz1Rsu*bJ$9!60Yx__bYKS1Hs>39v!9gOZu5hW+7%~104+S5?F4LH*Vl8tQnQ=? zJ*qtu>+5ZyG?{un-j~U`FAcjb^)@dYH7WF^X9B~hF%3;cqI>`>uOU9U>^iIV0@L|r zQgu8!>`h<Coa<y~6jZ~bzs+^tP^E<#1YSdPMi@tJEQ2=LZ}&`lRLMHWu=j0E;Is}- zjjyJ2`H`xxTZ3O0qsLKGU5jn;8-7b(&EE$c`fqi*JNSbIHf6gRIQXHJA5J-|FZ=+n zW4OtC?}#LT-omOz*BTUUoXhUQZ&V(umu@BIC3I+M&=PuqCqbnU726ILE`Gr)ouhgN za$~+9vj6wNg>g%D&HbH0>HO;K(f-#4`-S-I&3`x7k(zYu`fq&aiMqz408w$7S&_=Q z(1tXA<vg}fyb(MCYq;`8c?!|D=g9PcoZl86H7aD!G4_m`=_luZl3XcdF;w$LbtN3) z8u=2mpunV#mi!s|{0~vbGCFr7-y74WOa<doo8mIc5}}@zl0<SXfv;0`Or)g8AHket zmN^HO;rNfFz^jGa`>R>jb#d~`1S@n?sX&!VZyWMKD_GmW<Oe+Fgz+a2nX69$NrYIJ z5LzO!=On<)7nEs1?VwgqP{G%3Y5}R%?&8l2+s#BGwG8l)c@oOthA2%6D*P;VIfP_D zlhr81W{o8!Sdx{7(pui=d93Ivd_GvN(B>C^h5ZNf2#GaUi@*}K0dn6{(_Zd6ETLWu z_vU6f7)2;_hc{P~$Tz5kmk?c_!n(c3MJ!3w4LBfx)C{tRh2^N3`LoS)Ob`&?9GBse zHtR!lDhijx_wTfC0r{F*QE!R~2rTc|RG?fuGdolCa|DxQ{os0|Wc|tXhsb*#Bt`3K zybGk9W{d`@X~rCftIQjE5bh@9m?_N;ExNRJe;XT$y_(uLV9`jni-J%z&JBEhj947_ zUP<wYc*wLb1a4;2bzSC2k~BnK`olnr98Ie;For>lf~Uq=+@1l!KN<W|j5i8h?))%( zD44^z`;(RRcEqY+4cTEy!77gY*o*`5a;9EQ>W}W9Q8f8Hm^L3Sm!Ll$My?=lK-V?e z_A1}9OI6*#*(oKkyL*5$u{zy2Wu@PNYwI?TkS>@rr0rd`6R!(GuKjrBNq9xw>c5f5 z7d|cwl8$;B*+9YsV&MiDBEmnolc&u@KBhwoG15Hb-RTnFyl`-Kbob?lYd;krAuVX* z|K<K;uRF{!)K6qw3*Fcv!K9U~C?Xz3XdybW7;YyZ)8R*Z{xxtJEIel{A>Al4S*zV3 zYY$c|uQ0EU`Q_VAV9b(nT6ENz>#>}tx%dU7V3roCfIpo2O^Oz#q#%+>*}NzI%MDU+ zoI3@{y%FW^L@I33jS?nz2__I^uRp&qkWyb*xy5sDb71ltde!AHx-tvjql;KH%zZ}) z{p)GZbfZfpoY%T~w$q;Oc>UP6P!mt3t1y{m=wk9pS~*GCpso9G@<gw@NUN$FyaIN* zMm}6hY-?^MqA*O&v*MJ#Kfa`W*lqQFSA`Y(V$g`wP5V=blG(7X(Yko;Wn$W$uVC|G z`iO8)kg99N)O`9;LxqKh*&i}Jw~YVaM>-h$%U169y};!CzmBwpt?B>!UMy+<l6W|f zd}s9f9{5&u%fzx`@${|XM|C#5SGP>-#`f`Gg!M_<m&H=Nh_YAF4}E(um5?OE6OL9b z9Kp^dj_vt;n1!bmENfOeO#a0AISh~SOogrf!w65{oX|jGsMo)XFfs0y141l@#DDVW zN=$T5p^Imd>P|3klVTS9HHp6+eS#`Czl`5_qB6yMWA^uq^@;+*=rSP6?b!U+ObnPH zE(Jipx>-dGxIl42gUt6alQBFDli{s1uPO+g$_EP)XRlYpXWkPe9mu0myNlO2bJk;e z=5Yjm<{;V-RvMlgV4M+z^K{@_tVpCFA1DYjZrt0jdkejRRVay7?{In^oBOp>W5?tE z_v@p&y|u0DU05%>e1cxR$NT*>p5AGU&qggh9Kx@LlWaq~btZq3e&XhWGfKU0@a@5z zco~zrmEg+kX<=+k5&^jWkiETKsdcL)7u6E!#s3peP7cD$qiI&*G<LJLvbQ&!FcW!k zxTlZ@3M^~rfzbU0{+?7e%`h7O(><vJlLM}VIkwDq04h^;oETNrCu4A98pbhpvNNIR z{Y;37H``yVBgn0&qfxGr0Ie2d#?rjVJ><&YszVH+vO2<v@kaqOo8g+EXtH~2b~zr_ z9EbVXQLv%VKT`AO{PY^dVH07ScA7Y*H~H4|qi;iWvks9XrV}S1koKY~CE|R`#RbvQ zMh`c5eHzUIiYU=$&v}*Wl@a}Vxegf7O?Eq*Jqs#J1F*3K&X88R1zvQEfub@9QLm05 z;0n3LvIP*H1sGkDn`SB_%925Z%I=5c_oEFS95ZlmKHF%2cDh*}FRrWFl9FAnqGT)i zL0MrWGEdMZ?lY*8Ia)6tBB{x9t}%w85*Ut$IjGP9Z@)dud@<B#KPXxjP=gf@iBhwt zYhjbQwewgh0V6f3Z+{w5b$nRKu=7ls=mLqyc-;aQeh@MgT`KLu0ifOD!K4QBwD{HR z@S_*nE{3DZsw<WyTvL+lLz;cTwWI<`m{H&d<x<O(iAK(Dbx0?t;^T<lE@-aJbgT@Q ztv7h1SE9X9k*C$~KL^1U<as^zMZ!h6rd|b{=h^Hn(@+a=ZZ4_UJPdxs9U??riiXHt z(db>IvXa6!02S6TR-NoG9<O7uq+EA<Q%G{?!^J;27Go3(ykc-h_GdGzTpNY`K5Ji< zE7DXNXV-<Qm)pTB9%$=5MomoTV2rR!8+d-~TxWh#f+1*>X_S@GPE;jH{?PwO1y5eC zP{b`LvP#P@9a-~0L{l7-XH#YsY+t38A(y^KUk<p5I7s%XYai}K0gjd5ex!P$2vg%3 z&W3OD7mV~u8w*`}HIAw=0(9Iw@qez+r@X<6hvE>^IKVm1Kwdb~k&t9rTLQJ<o-91} zXBwyOQiW$ML^h9N37u=(m^<Jut?eqgMAvvDZvn!FT%vA7<PNx#n67zVpQ5W#vJ}TI zadJI}^jJ)0dBaNsHy)sx3roIkZN%ppDs*FFQnd;SP?7>hDUC3R4_?3|<VQHd88McO z1iybFk&+NS_#GR|5mCIi<w^E}y(m5H4Jzr#;m$2Zkmmn`2}O%O)SBJAU_^6V?!W2| zh7L)<n&zJKqD>Hi+hLese{~FWw4}b%{<|3HlBCwtrwpHSezM{91hAn8-3zY(;`F>Y z0GOi`oA__Zh~)uOD|JNjZJ{lmbA~uJl0pLj&v;7n)Pv{^BOH`6XLGDnZWPam%pkV? zo4M^gt_(LI%{b7~0&<vX(4Vxv(Y%UI7y?xDA`c8}+FoP;fqdU_z7IL2W(LpweC#Cd zVB3-SC|!J-*C9WAMk|b-_kNq$cV+NI;<BQTZ)}MH$6q<c3&446@xD`9xJGDiu}bQ2 zQ381h|8`DhcevAdO`YWoJD=xyRFgf-CUIsX`)>s$y-TaCvTn12u7*Z8|7eGPy}0$< zL5?^SL_}re{rUW!UQprwPQa&kN9oDMYnUzFr?_kDzq}4BXfR_4oRJ6?b7IgOBYm#! zq#^@s$^=5QV0J4)Q7TlfhXLNIs%5sm9;GuS><HbCff|4wp-;nCRy$Xs_t>{qm=Ec) zN+K_oc)nU1zHz9vt@6~4Y^pvA$&IY{Zohv;whva>8MqS`CJyae%1=>Upg)yfEk|aP zVzbV<(XDr17*?rMJK=rEef5z29^CI4v)nMjUh_ex)h%vICMkAPLpMFO)pew$R&`oN z%6E@d6I7Y4MWS7QJ-0{e@oIFqP|7xrViTB?7t9E3XMbdFihaC?Y=WMm*UrY31Z5q& z4(5HHAAvWaYei_%JU}6LGN^RRhz!Qv=Fvr174Oh!`ovTv?kNNX6i6xB-K6l+`<`l8 z-4IdoSHr_mpva8>2vq->C?v&e5jW(fGP9NII}Wg(-N|}xD$lIrWn&wezN}+*yxhd& zW*?!utc&c9r-b4+Azxew{0@cR4T2>ZGL0e+eB(79J(k)232KR>&2%)vP%?95%(7^P zbn9IKRIK7lUiD^T!l}Js*mELs#+EOVGD9vOe-p8^2lS<zR2ArEF4hzZlcV-<N3CP# zR<N3osLr*JS@Q?1O3y(MgMr#I=-Q#LRUfIs+N8wdqOQv==sRZ6;;Y14htu1MAy3ky zm9q@#RNeAjuYUiDgy)$b%0S0dpyY|L6t-M%-R=&4s>bPP9HINLQ4KKhZs5l9x7B1L zwynuL=hI`q_-Ca-dl4iG0?>03@lpZr{ily$DeZVD78PK+6ru($vE6P}w;Nn$2L=-r z!44Y}mG)oLT}NNhHFq-kJKp%`)RC)z%?lRHmpJI)GAV|BA>)CdH1`jhno%JQ3bYqI zSf@8SDGiI%bQb^>Cex{B3%7epmrit>Vm+Eoe-Gv(NKved7r^6tP=zi4DPl^HFSS|N z2+Irbc;m*n@VPm+g8Wp>!z*WT^l2I#x3qmPrX<!v39kVTUK6B;`rjz{Y>s@)h6+Mn z)R~&dwXT&v@y}v4)sF6nx=TqFexPTuW^dt3{zVMbBj6?7w0@Vj<A5Gpe(jQ}GU7?$ zsBMAG8bWC;KcXsKl9SyA>sPQ!A8cr1j&E*~6e=Pa-Y?e$IE&PugsR`=brzH@U5{Z0 zk8ihe6NS_(gAL>9z#m|k)cDZ83G=xmd61F}lcTtfbki<jy9QVwqY+}!H=8%uP*`k3 z+QiB){}c@m?Py<*?#z#pddmo-h!so-lYcB4)bM!rARC-mbS2bNdk+7x&o{ltZT$UL zxrF+*zPDqeSjA&Q1!r|j-`+ZY8xsEM-{$NRLROTVsIx%xQ43>cZZ|+9oG57{X*mT* z>2|97p%treyMXh=+HwIxf$Cb76GKpjtU8cNt8PeLxRPr3!k#0$LszSopM}^hvGO<V zL;?%M%Dtj{+?=4mFW&V=;?51QH&uKR3>%Aoo)(QaJnxiI&`^x?P>(3FI*pC!CkO!y zh>{WM{?(HOs|hE9<sjUy(}?7RJVW+}yOcU}UqJEx?SK40+t5gPP=A}sJ&6CyWY5Ch z=(nZ3`t@$h{13H>TI?LK3_{8yDyc%XF_f33!{$Xzb=2R#6`Czlw&8Kbb(jp_xS@sj zah4+D_SKzl8ylXIdrP3EsL7mWv}7qe7~De_IuMdYs3Ol;W?@CGIeU5zqr3w6aa$Yd zb=@eoFAR5`FxkcjU~9W(rAfpD+2=E6%kDBjf72Qq9c$YkSL;teRnqlstreJ~TVs7b zz(vQ@vFVq+XUiB{ZqH^fRDZz1)apc3K!_j$iblX+gMwidX@+L$T?@$kiF<Hb-w?)0 zzJ4+4KVSm*@Z_!|Folr{bpyP`Hg?3Za*ov~$7+_zlyiCFDE(0P4Y0y3n>WVMd5zB4 zd}$5R9oU6AQKd5T-sH3n@01mP&@qPYK-fYs=Wrv4B4(&0AFzVYwVA*Kb)k(~nbej= zz`c#|)7A{1nKmFzf@(_9bb?jPbaWApB=oT;TML;Z<oB~*c@4{f(8fY;@%Sd+25z+~ zBTk#S^H0Ic=W_h50EmAGPLa?a+{IQ4GgGpnrqDE}r~Y~6L(Yy@rIinoqmhfiv1wJ~ zwd2WGHI?TlvT<qBJr*KN3rcOzD9Du?!Hc7ZBCVvgw^WM4^b1@oC`*QW7r{Mi7pm22 z^|iGp0u#@_^s*T{_q$2)Lsm__aoT+q7mr_FSK)h|S)b^pLGYy{taVN9s9a{V2<Ywg z3kVm4Lk;{G)~Dqc*3MEjDik_j@yp@2!=gUY!blW4#MYumJU`{LAfQeiu)KywxOYr< zj+V*ZX==&&Eu?>B?DVzh>R9MlHfkZ8%)=*U+s@r0P!>~dPK>AF$#T8BVRR%im0tkL zRKGxCz|Pk%@r*YQ5&7H5ch0)46XhXoU_r+noeB}6HIi9CZ9bz~F+kpr^(@VcImJ?X zcKY0an0&1_H8`H@qcI%iG6zU)`5!7-Xf1@52y(`j7OdN*Vd2bW7oo*N23_w5-9dwp zC+Ma_E0g>kZ|s86N3S9IBmx$iF6*YogbDfQEJ&M~;%w)lp5>;6_NbIdYW#1eAxfKM zlIgo8!6aWk8r|ue{=XZ!F^xe}qu)jjisb)l<ZMhF%>IL<PHFzCz1mT~uYc7Tt#Lt> z)Yhx1&_=d%Y-ebBk`cpK3T`0;(oR*XcBw@PZi3$*(}be&Nr!`!AqHUU5{G+xd+y$F z&JGUsMStR0_L359rbat5Nhv8iiU;jDrw&)yYEJE1X#Dvx$10QN64Zz&CE5$mVmsSm z<>cf{8#=8m$t0$pkawDh=uNfNQbjJY2}rgkOS2;5Xf{gn2stN|-P&wLbk!vpl^>6~ z*R0NEGttvAV-7oVykhC~DD0pO;**@0(NI8UC>$y5&QppP3now5{G0wLdw+L-empJd z?c1H`+0y;1p^+NbnDS_ccV0UL<sA`>EURE%UwCB(>Y>Jf8Y7+bnY`;{|8P)7pD<>2 zQ*@f}7N)PT!A*Dw4N;sHq$YezVPFM?=fIPi)~ngp^X=k_q^IN8^VQ7BodS|Zl3T+y zL9}B>K{JoHEf4D;9LkJM{Y_JXs8II;Y50-F)U&*0Do@b1%uR<*SzgN4wI^N$cWZ(G zdDQ?R+30)$z=;5*&E%L^i$iIaXW7H#$*ud0Q9CWi?^P7$bXhnKfJ47FpHna@3>paE z?F{h08G<w#%Nco$Wup{QTB?$X_}pPhafy~z0MCjHEt142{Y(2bw+z=>%SgKp1ouvR z(pJX3q0xByS_BXL%0IkB*aq;#O0aLu#!dROdUO^uIuH<ML#0&<iO4#(UhgkJ9IlcW zHQmyXd6PQfdc&Wc2zvL}z^?7{<K^@B=?+Ug+(&VMor4U&6ElRhvkxD>xw~C=*i4U} zKR0(Mb;V4NUsCC)3J)xGl=_+y{Xu2-yu~>2T)eh4558cO-6QDcL$xdj!_<P*d(woB zQLwT(=ma208kzz7IA2;ucz)8y?pB}ftOY>=$5_{$NnP^5^CCSf(2p!DqWO-Kwi)p4 z3RXm4bV=y*4h?1(Je~+@k|VB=eq=r!eLdE@cE>x*%&X3rmi2JFZBtb-XQ-HkVT`ji zEl#_G8zW(;Z|D#OnJgRI_piq8A8W~TuEHBzWQ?ZT=7_m;5>bk5XuT6;LALxBDoRT0 z5_QqnZTSFpWgyhRFkDvM%H=%q1(q3#Hs9$3;>b13PNS~8xY%dI(AXDeXwBR)nb<ym z=GUE)<K>u5`>_^>k1#dNo|&QP7sGCP&=M_rhLL`JlsXvN?O;`Y@kmIrvM(H_M?f+z z2tCvO+&z0E+7qV)7$M{3*wnZ@qiLn6dtWu(#~QAh&uta=rN;hN+tR`5A7joZR^S6P zV)d1F_8eRxthm`dA8|})OQQ0HMiUqW;6ZpTxD{)P_dIq45-L^6@Ma!a0A-Plf+KEf zCPcqMT!3(H!?<q&lY7*aUJCL)mAYo!!VQgNC-n|Vk_F{sZWc85c!sJ1St6I-mSaRd z%-|PQ*rY;<Lem>f3$X$siqNTTe%rq){vx^v>)acq3qaCVh9qywP|(FYAYyUtqbw+X zj#2*p{IxOtDqc&F$JMx)3@d&x+lF_;4P;av_;#e1edMY_Ha(G^JnjPKT^qZP{GM(H z%DV+n7^=-=fAj$4{j;?G#;Z_1{oNKc2)rHIq13#lASnXr7hER0{mIx=mXJ#(hd4A^ zZB5ctDmXD81iY}+ZE`xU{}R*e-Wuo^$?HTaqpl6t_J_LQ4!S_H0(LYK*OVmSXSr6> zk&#B#HxIz_fS$TkV0`NqmvN>h$c5<#oAiKCeha^%0vAlbVYqxz?Fc4;%yz#iXb+rX zDmV`Rc|}P8iju?21)>*XCGRg1I13^+>G{0CB)I6MDxtEekQz5pXB;VT5z$<#o*qBf zSr*I0y$A8;l2}yF#Q4tNiuC8RXn1vGI7C|e!b(Cj<xC;_3v>+OLGe%*5{|G5UlD;% z00zEr`$7;cq7gt!xkYjoE35(w;NKpRe<<Ce8>PkCC6H2YG|!Brm5wRRd?`lVa{?gp z66ax_y!>lQ*KalS>`D(nPNu!`(=x{^TvkzKoCekG3i+{5jaydPZHcN^RYkUxB7~~_ z$*+OGH<Ue&wzWg7MiLwbI6iTO7Lzlncq%M~-SUtPU%z<&eu<?(-6$DX>_g%?=mKNU zR#|jw2cB_CZW_o~TqIbl*(4OKle&2l8rQq$Sg8e8)*Y$I)9jT5NJUSk<~{O_tU2&` z>tbZma<9R^tk!yQU(s&sd-PIIS=YM@pc{p{$nq+$mkR#Wre}u`YmJcyqESh(otZ6) zZGpv)fhCL>%!_V>rfFA9`v<pco<Sos`uWbm9F)E}W^oP<f1OSP3QyXLCyK{C3y*RT z61`KIy=AkrHqD&n2Z($NxpPp?6@$L^;9iX)rE8oKX&Z+S?^clud+$9~(toLlzAWK^ zev`OoRD5Mq%0lOpvhR3&%!J@9PZOW%2q2Oeh8oe!z6^=wVuq{VAi>=0FJo|T9`cl> zi1zH+1a?|Ex3rSo9%s3&DsaUG=AlPC9^`lt|9PIaxMalHoU6xsucm&l<zOZeIs|1a zeiRTLX4$55=ugG<!zIR%wBM)^3K(M($-{#+($9BIBfr>pOpe!N?mAc6PA%|B%|cV5 zr83PUZU1I>m-nx`%)vruXc2EmFKcioHRJGXhl~dB4N@MtfzS@w;6P@OSz`${s~2$H zt{y!m6P)V00&xtRp*6cloti`^T#o9g0hv<GD+m6zpEPd()RcSCkBN$I^}$h~C$)+4 zX%a^#I{rmT;u5x!hqC+P04}dKUM1zFDD;u&PcK{=QSYm-x0lC)6(-z?kr117AVrK~ z_FtS%7`Dv~Ubp(!RZ>GYD-u!nNSlNu+%`jyXUwlNVWA7?+m`VtSrUid{P@!}Hkh?$ z+KBjz3hcd6Xt0PxROv_8qHOZE`|;?&K0@DM<(TQPS;Ny(FND{UpdUT*RU=0O->kH` z#^98*n0NkoxYAXpxk#88dH;hyrAU6oxMPd~WEDXL7zy}JJroHJiKK@Ha(HUKTCq=T zSB3A`>p|~1bBo3Vok|14?0V%pa^<fa?DzNz6nHo#A7TAj<ox*}C$G3Bt4UoQm5xy> zSqtTlrs-ggL5Ed--r$C4a7CO+`B||@k$0{Ue*w4%%!TMbN7;Vu$pfz&!F2f-*`byU z99}nOMap99UQjliK}cE}3t;#InpMBWFHBpM<CGNCj!~`COXof3G^^#|k(SPx>_&c^ zUL1`17<%_s8x>H#h9>MUd~A1VR<~O0Z$;QM-rr|8X}8gyz-qH{>>t<)AJ^!Rk4vu! zkJ`)aOuMYgpPv-{Lv-I!yKCTEjZ>}KX_)^GM0dpeats5tHf~;68k16NsFuvjoINZH zX^e*NmY=JgPMAG_$qCgQ_to#Em>Rq3Kb8<*_uBA<5~&D!kfIgs4^)F0?7a9s0bzZR z7LzF68yTaR036};OPv9XZ;{P<O{B1Ebh!f1Z+8^S)R0{9m0qHy){So0X~CgN`EiVg z*I+Q^0RSAS>&q{M>aH}7)4N-8U$bHXrcxKuHz<F#Q+XiLVmK4jVi;Lp|3f2O7%m5P z{wrPqqxqfp|F4mujh(TxwaM?q-}wI>GhAtH*dF}K*Iv{S&Ii`3X$r@*BTy*Tu`)xs z`iA=oD4_fk%%!7gDu|LrG%kB?;@UM9rxIvy3<0_QXd@5*Oy5R@9JqDFiQy=+rCSrV z3wMynz+gGVg2A|-?RvjAu^5PFuaaZa+e;+PX!W=B9DeQ8T_NG}BH^rE10T~ZD00;o zcu5}gz6mfC3Tl#)M`6?52_vC;;2XsT;!I+Mw@QA;*6HaOEa)HDJb)55=ia5H5uRpo zq#3yf+xGDa|G`A0@SCyt{^CCeUAU*hwJ_OfT(f3MU*$`SbFWQ$DK=4l<LgM5LFmM( z7mT_swI2`bNzZ?)F&71PAO;_j-egM~6sa)OSBEUHU{vhv59(lvd$28K*1<&HHX|PQ zzEi@XMgXV*dS-*qZQ-{<HSk8$!EHG8*ey83cu3eghFzX7%D->w47A+amy}48&U!zs zY8{7k&2+TU!e$2QB!*#uWL3&e>y9xjxUQX=W-Bg&!Y^(5Dm^K%YnWL(bP7yaHIqe~ zTd+pDL<(XTOwd$^X<5_8zGIQ4{&&Mx)@L1C>~e9MOmIBE>KfpR6OQ92U|(1iJ!)i{ zLDS1Tq%2%DIb0Heu#VT|z?R8oB5LH^!<PCYU&-$zItD-@<Hbe;W1R&T>fb{~pv29_ zD^c3}#Wxyeli@+5MpKJOmd7cCwLDu1E}sOpNn&1Vs5~6p89aDANXgrsk%;L91-(Hq zb(95=lkD%h1g2XViuzNv_pJl@Lvv>ufQqN#(Oxr8HioR`^6t)UoLHrU6z>WHijqtt ztm|hVM*!9ShX^AOhd!Va4G~XVuucnRd_PFpy~|%xAq149=}#Eqj638+t?W{|_q(5N z64cp~xkf1gY_Wmu6c8Pnn_`HT5|b=BPz_N6gbb$C5U}Jq#wG~;Oru>jQ%ZG}^}xN9 zp#`gvMK<U}{vbWo3~$QGI3U|+v4Ac-=)D$@1-&(>;709CKz^JjO2i5&ip?7vmKWqK z7p!EOoA<6~*eKX^lzUeV(|RBa1jj6Tnqs>*mD3oaiky^+Tt?YtG-#eG!0JaiaRf`2 z5yfg24UIuUOjQ~&g^0P0+g#X*ctRf`o^bsx;tUbKBOIcev!tJwRBo4k<h}wC;XJUZ z=g320Y%N7nel&n4ra+X^jC}0&?O}XUg1AHXVZ6ZkY1_@lzUhQbtchItCLaRnD0_#G zVgl7UAXc#EfU*)T+p~r>m&U=1^Cfo5o@kf%>)kgm+Ey>tJ5`x|h4Qon`Dn>;&d8Xs z3jkO9Txv^D-Yfs2ueA+nxi?8MBd3}E91nkqMRCr&Ow&OoD^BLSb+%1I#wk@yc0HX= zb9}qCgKq>X*E{1Nq8WSGorJC9>*me<BTwaU4_cj!2rW9w7wDWUAi+kNu@TwVmw5OS zyt!)9vild|2GhPe;3{|wlQvK3;hHX-pxi_i2>VjJripa<0NAyL*5S7;qFM{Y#jeKy zA4z`MH9Uur{sXAE)&o5AhZm%hhoF4^^`#g;oC7sBtCY?0NXOI1M`=^r_s5?6!`GJO z*XpqtnVgu3ivRK{kjd<@b33;Ot|4{br$BREY@sfVWqw@!Pm*Q+u_$%wJ3@J~s58)G zQ-N6N_8+W33E5TGS#ue<Xz9GaYs_rU)BTyOuqS-(gnS-x&dTd8w?IWnCaBUFp+<Pm zdw{O>%AShD-wrq{cxN&8>NsR=k(|HGr2QBC`(sAkI-w}RDZu0w44wd>9zoQ(I6r#; zg#f*7cyLsTCQ;_>yVVMy_WZ@#q8U=SfvL7E8qTP~NBgyWZE*#*@+YU|wz4v0nKO3~ z8qmv~`8l$2kc$sTj7v<-!%d2Dj<sOE<9(2#a7){Y{M`B``~0cd6o<#N2YOG=Tt2Ic z(A8smoO2GLBXI%~#W#{<snv}NbW`UQ$GE|67fDz9gQgl=c|YEjmXglyi;cuTdhm2Z ze<FLO8C@xl>4r9o(>hQbQw`-*U$)R4mws@$%CkOz{u{eMTgoA7`F$L`e#uMN|38n! z?<m^t7r>~F>w@biKp1h&*>6rMx(9%}hHHNXC~6ib0sIHZrE<_AZGpvI7H(m;>!8>3 z;W(ZM%IySfz7Zd*Uw#E^SsGT}Mn)7uVQbFyXkc3<EP!wF$gAd<XNj!jbmlBFtYM^t zCztLfGnhsHF!2JD&{U=$B#m3-;-E!`%><TNHkJ$+y<f}Sn~o#LTu7UeYB#ixeWXZH zu&*ijoMqRKxU%l6e}w%BtzAiJ^;T_#D!<A8dV$2%ulwKYwIp5ax%geD#V-l&fAeT; z-0j`}1A00u)cvyFP&S`XL($gaFzV5iC<u+D2z8ktRed5vt;aMR{``28lIJViak;tP zZ1GONfp@FcVzrZc3qPJNMI)5NBWR;CBCG5x*Iq*f`}`X8E_LHui^E>CfL-7or{J8L z5#W}WbDTu2`P>@)fsIO^dr8ZRju#qqR3Z0K@Yhws$1rMFCfrS$Ids26O16W6gvC6> zb$}&V;SkN6PFn3gSm5?JWvY^-uA~-qcoAA0aio<`t(g$CRx=3y!ZRfQhQU1JDyEs{ zFHl#?SX3F0_=9YUy&<GBKr{~fdbh^zJEgJm`GGg>^FQJ|CW`+Rw*4Z)J-=|)|1OWc zfrI0JpsXfUS-aoQLf417Om-kiFImK+62>i%Ae$&`qugvOG|C!ipG0ArPPmFBRe9~| z&o<$c38x$zFAaS&2XpK|I(zKHn^)+v=4caOi#SDQg})R=WXROX!<tM=G8>$=ky*p$ zDN@C-BFN)Mt_i3;*uQdn(0f6PK-cOkWpo4r?iUzGMfB+ywBxx|yhy0%tF55o2i%rv ziKpIR#)3MWpK{SU^TdO82c=3gj&%D1aCiBi9qhNTalJbwjXyzM8(LgE5HNn;3a=at z1&kh(7fPwgG`Hh+cpT1SkGd;5whfwEv$DHnNz6Fd@Q@^xn6pey)D{c}?Ij<E=fnoE z+{#9^JId>wNVf8sh>`uea+DhH-H}4gd&hG`X(3A)zX6C{YA4mutb>_l;QE$B$^kL7 zXm3EBYrg;FP-y*+xK}NMEB8QdrnM6Wz0HMIo<>Wp?fgjpZRHW7r4aNgtw3LzV<7Ym z)Uy5H&Z(q@wu^{<XmJvjKj(B}t}l)%c+a}8azR|jK=yc#EPFTX7lEd}DZm_GOe&Z> zOSXIC$XcdHEuGeCSe?WU9yEvZuB}b!b?DW!oO})};kRU7*n|!C@BfUvTJ%EXPqPq* zxWbc0-h71(&xxbf^)#sn<EN>pUL}Bv^p_}!jUig}k0(>CC`yX*9;;{+PoD#=Af9Xo zKP1XsEmnDZ%BOd1{Oi~;wnuaV#d_Buom2~U+Jmdc{E@B8kRuoa!C9OIC#Ig$y7ObR z?x`X+k~P0Aqku*zWy-EHFT@iiY{*HoAsN)`rpt?i+M8G*G0Yk0AKC$Wu<CF83zZHo z{V9+QTJK4%ppI~<%TF0+S>&7wLM$!`WK5W-!a1jZyd{JWQA`iDrDUS8j3UEX^1(nv z+zHN?l|JBKZ{57M{n>WVW9`$izY)1p(@FHUYEI>p&$=UfnL^vtotby%_Y+Zp%h|c- zHx?Ju&+I;;i@oe)1jrwgD8%&?VES469Lsva*QV0et)1<wB@1zud*vp=eQ^>2_h6UC zAWOjm{R!;_L4&!dX3^By+O=s-umw%5BwktvcwJ4U(&6>ST#kUl7%5ZO*-MG)UV<Yg zv40_&=2{PeG(4x~swA@bAkS=Sr-n?$5H5+pef^ZXgU<COqfGyGwT0;aX*qNpT=w+7 zD+liU<0p0u8=Y<M^9<9Ed_-6_Z^K_J8(q@ude%(FwPFYv@r*1+Br1Qf8l$da@8EBi zz)^A5_#KGgqUj?HRm${&X5eLkOMU43-*@3<o;UpEucw6s{{LlDW^ZE8%EtJgdtcMS zZj%k`o3AGTm!6<}X*4{Lch|5Po5U6|&pB$n*C!1IBxbO#Ekz``$dm{Bu;=FEewLGO zK3e~mpR&Co>_IIm$)srUoCn=a*jW|4_$@0*?J;pU*@Zg3vjjesM0OZ-Dm*m}*;5^4 z{B7l0njuRoI-HbYqp0+9Nvh-B+HS6nuLYQq{FH(|nc$D0yXBFPSwg2F<8vf6a)_?R zDA^RpEU$%kE-YkaD>;QsmFXv|z0Fi^;R4@W%(~>-l^lH>tXFVExOGn@-?z@Km|cvM zMPxO$`;z7^$NJ=Sv-jO~$gQ^3Tk5LfU;ldDzL&vUjoK|W_snvFjOkicmTt`}o@}U8 zDkXg@D^*+so?Oq5r8Q~Vfb8&gcf6k!rsb$uUZhB~#nL(3sieOcXiK6jvK3x3DoM(+ z#!!$()=BgpLkR|>P!bXDXV!KSfhjwhgZ`ThOrw^820Gy%GU27DTn@k8Kn2J;|LYK| z^GC1C+cjSI>-#QsXXjfi#h5R5hEJPV;6u2tyHU~9-P<=ap!+saeKHfOr2%s*#Ib$s zc+R>3*0&_sa1MqqsAgtc<harkTyN$Qy#?oJ%2X2&j#wRC-S=z53OT$sTGEM+*xsO< za@G!Yu_{D$_@d9ll<(MD+E}?5Gl*HGYIT@R<JHTD<PcHwGx1MEgyyze`csPY>U_X? zgKzXWtjSgDB7&6KRey?qk2PP(rN2pZNhT1A9}U1%zF)%iRnYY5;;&eGaSn>&SI0Oa zrt{%x(*!ot)lh+0iY<~M<QW$g02=hXIJlT!VjEm^FF@e031$el?5n^UU4>$b-j_kF zmF77?MXo+fHg6HDDBO?iAp5Y7kjP7n8kn3tiecHy@ORK|6b_YAbQ>k=5Qt@PQBBq+ z6Dy*hPiTs3#GAUPlpi=mwQ^i@WH08L0KJLGP!Dr`5aEV%eUD4k;Xu~+h8CV!1!2XA z(>2_W7NKy7Z2*Lc`VVX&x_m!-dw?h=PfyvyGBg_f`HWrw2#=Byu3SBhM~Xi?ssrW5 ziFAH~cF7)H6vL3i+$rg^=R<E13%ue)l3lbJQYKaUDCV23q;58R5fEnUbX5aRV%FX~ zgZZP&T`U0+f1{qk)$4^{YSHR49UhZuyD`U4y2HX$khC<m)xB<9pt&^~4AOQ}PcEOT zBLeh3*swm&MS$faxh9=t?a5MRgTDEUKl5qt_TO1sf3R`l-&8DL;h#_ZzRFr{v_EDE zD_WrTR=}tt+4f-3w;N$=x0V!!rtOK$yv|*GLH}$#X76C!4SKlbq7o~QpE%bb6al89 zt?@L+9IjhCid_g`uo)YZ;nm#Df#tjnN8lR|L8tGO`TOMhs+4y^pM|y$@<4k4F8q7m zHHgRrP`pJwF`U3gvFvVoMQ(1YCjSb^;$Usq0{HK<e*e9=bE)gw>1qe_pYs^?-gvX! zU&}EUP-U&WXV9osB*oQE0+A;@gr))FnO~IjhWM7ZacnjUE)%DcSM`Y6=4SRN@qaP5 z`MD^bbo{zm+h)I3^=$`!N5U(NM+d^B0RIfoDC^g>6d;t5hR%WE8pV8v)dN~5>g@>( z>Q3w3w4cXwgyLv`nuO^tjIgBri|9faAxek|pw0l_?b$*QyDk2hZeW#Dusz87ku0+H zz|{!Xa+>Zofvk8)IT&TneMgIuk*PDCv+la9(z`R%2XItkOZ#{0j)viMjk(HvEJ5fp z9VRq_pjm^jtqmfSc`m`vP?*Dg^PhoM%YF=j@QHtn_sq3b2ji*+$UhWu0n@U)4ndK; zsot0av^gd}pGW>q+Xad})veQ|$|hAvW#iU%Z)P(0D~UhIPFLGl$Clf_gN`-8PC$I8 zPIZe}nLF$fHBz)y%+TqJASak=XwM|w%dqBB*~)xZidG=xzn~Q6K9uW+;u?EeL7>_H zIA%flr#iytgumI?Xd^CFPRSmN{sxHAwm;DUB}YYGe_S)tWlY}@(-_@Uv^H^PJZ)Vr zote!kAf%{sbMjK){8Ny00hiW#obwLR!YuAWX-P=Y5c_fn97f;m#HeEB3+`v7MJ~_F zy)+<Ux~!k=OR?Dip|>kV2K*`oOSVyRZl9%*dq!$WVX)&@y&6=Vn^nO;u;`<PQB}w0 z??tdi{X;`w<PJ*FXpAaCaeI@O58b99s|l2l1~+WM=EeT+t9)NK+qRRQCRbFv$;-hA zA^OSO+hio5JmlctQ0vfhXZbuZJNEp6?Qn_Jw5Z#V?20JeNb3Glv4FTqeeYnWR~ACe zeDGp9ht#^5Fdcy(X2^cfCXVIm=62oX_NBsAj*|@bK@K5VSALImYz<7&ZGL(Z6iLV` zWA6N%L~8}~KS(61!x(9Un@(4H`|kGCE3#||Y?gq3`1S(?>BgY!p?M7(rzBz_d#ky2 zT@dL>0nbED{QyCM4Ya#JY7}bt`Byj>%t)HmdBl9<M%fZP;FSY!<dB(libDI&IM<r4 zI)=LFpFPjULWra^aecjQ5RRU+d9@S0ji2l?wsq>m$vyWK?i{oh&v#s$RK3aVccI1r z?o7{Ngp8Oy@N$9Wv?I8WMbLQsG%)I7Jw9pN3dTXbI1TJG_yk^MF1w?fp0uBzq|JZ$ zeZ$wk?{LsN!E^|&@njjDN&o`_?pQP6=&>Y*Lwycg&kpnd!p|EQ48mEnRe?cn2s86| z>aGblJTJ3y#Kj=t8uS1JAanZ%>P~OWlM@C6FTGO)0Vgct%zNK3BpG$Ra~E}$_cOfK zrION0=gC-0i5Q{y!SLGPh*039!K-m=1T(D&k2sye7S6SmJO!!ZjWifTitsQu+d)GO zCo;V8y-Vva5S@EgWx1I&H7{8tz1F#YS6Q0dtwx%QPKwBQm_^Oj*%h0g-!@q(<#QT2 zD_|ZUclAwpeaN~H1YV3?jozKdM^P!#o_xFc9ee{V3O7o)1{4x;yPB`z@*41^Kd3nh zu*SCIW<$a?*Ckb|FsTarn)x+_bZ~&Ha&K+~sN4qh!1L_N!?p&{v%E$G!tM;U*Yd#v zZCE}A$piEv-%sPsrXLuOT1{%SfTA9v`8T5tFfJ2JURh(}?7D^jy$Ee>@?`Rm_JF6* zsIc#?2M;5M0&?{xptQbgl~XPxuYkmU2#{LN7KUY{V_*Z(t7I^)L=V<?E9wF4_g)^^ zWysqjp$;<K8>LCqdDc@S8^c?v6xI?3<)UFbUJ^Eb1?^(7S-T>KZ&ZIZh1K`uA|toj zSO;iITmTe#v5gV~i*+>?e>VK~=*ziOJiTfeA;t(_$rXJ<e%^uFmd9)WkB(CBT^n}v z*9|So2zn(g%-!WDLZc%POkKG!KkupOn-yCrr~HTde3p8+v;biQh4VkX(<7`>*&Tnq z(>)RXFL1!#-O1d}*2KlY`uCF<t-d9H`0F(}qh~-1FL_<xmf~5iw%&~+A=(bar67-5 zMU?2K`GU03gqwnL*W)_u(ydRq1uk)!e&_Xhd@^QO*J?VPkbq@6s%BZBP1xE>E?de~ z(Ac@s$XLYMDmG?f=Bk81N3$Ts{F*5~M@j2C_-q<>g+hE3lgtle4uU**ohEP)VvjxP zZp7KPT_@43)|5grtah7*)Fv7&n2D`4E~Yp|Wk_le8<C8b(}k6WF%2@HcoAT6k+Ip- z*^C2M3>4H?KsMa$3f9S&no>QQs%AxIy@d-AO=0PpSAn^vsN)ZgS_vk5ixr655bA&* z)QI0JrrAOO4Nn&lvJWhkk)oY~Xc++GA-y?sO(51H`&?g5ZD`p>p1dh9^J+s)YO(H+ zDjqpR6ya7_)GU&5M=3t?p+5CazOXIGCv7UzxKy9snMm;0-Nl_t!c}mxUd+aQ$|K(@ z<fr^vt~G>8VhdKTY+6(@1_wedXa^cn)8CcwLskaVIs><|z+Rvf0j_UY-PU?l0OlBV zW4^h$FxFT1$K~&L+v~yO#?so*n+Vd>Nim8xAI3m25HJPRKfMUG#d+OZkQ{jyUwxpD zJ!ljbM=SNT`_u?-HR=-Z*fK_$jeZSS0~8gj_4I(Rt$8kDv87EkR`f}ks>YO+z{qy$ z=07UI4gfrKWDq#2Aq^<81;h@Jpu=Jid0#xiHixj@bO@lv(ZQ2z#MIi-H<oC@DO7oM zRsiBL-k?jePF|ksK~sO6TU`HQi7ILeJME<{fNQlZI#B*mOCaPw8~4W$H0(W<z2wy= zxIscQ13cVAe~#Tj?)@TV&8BF3bigK82ijw(qQH5^iiUiRRsz5w$pY$NW~|yag@b!- z=Tf*yqVXj?u|KuE9m*f+P^u+-Eow=4s!sVKZL=3Y476&?gOwQMRI9O{_#1&K+Oo~) zHQHqz<pGLQqSp$oatk~5<Qj?!ILPhddj0UX`H7?up(f;#69+zNqA_LNcu*LGEzVY^ zybY*vi})&!1fx^j6$mA(5BwC+p8E#Z{>PgcCVkpO2lc312n^iT3D3-UFHK+m3#%3K z3IxR(;}7ik1}D)9IypME0~JCO1|z-y1`1ik@^wW$FfexMKJ%8!stv?Ie$1VNg^NQ^ zV@AqlObS1lTOcoCDwsm2O0Dosx_j3JoZfRN{_MYWiZyPvKLx2JHdq2yt50rfTa2&V zMKJM+z#q)nMV!dVeLy7M3j*2&a61L-VM`XboT^ZdNF3<K0RhSxe{3&)jB|_&8ne#? z2~TwPE1jPLsRzr#Xax%oYVCRO(Mv49rAA>-<=`H{RAr?jR<>-plmNP9hk8vbd9gHY z$mFJovzhikFooAl0AAkDIHv)vVVfkgM!FZUEA$oS)+RbAWEXmQ#Sc4<``!$I65@~# zC2|9O+-3KP$nZ~&LSHZ>IC7+(z4(Z;*FaOB3eGBhPJy%5@0I>_<O2>_!>3u{15-j^ zc<icak5q4cBb9Qr=|dP5)FHw`K14s{LSu{_-`XqQFcM3CK6+7yBxO9>sOO`_+@~O{ zHNpVHgKaTz2@TkJ9AoS<(F$x!n~l0__QJtA=Rv<%!ljBbIg`L1Z{o^r;>Ku2d+CeR zdTaEKG0~`fp&kye_%KDivvBGL$dF2O;LnA#xHe}Oljz85Pn+~>R8-sjXyyxIE9j?5 zow&mt*h>&(v;vxXD7z+F6|p7L*gjQj6d6HJXO5Nb;VkGq_-dp@Mh9IgT`1vL_6}V6 zLxfEK!0`~g_`H6Pi+;S8=N}C%WdQ?ecet?*{4=xT$cj(BxGUxGCI<u;4*Ha<>>=Zi z?!${$_C@X<mNOql3az+v5nbJsc?Huld)t!lzMGBDL`R;k#NUj+K%cto30~$%>OcpG z^28K|+p8Ck+><kvy*y%i0n38F21Q&6e9sJ}$zdZTm1rEf2P_bPc(4{=M79}VVQ|{- zN*ib)r=-AMF3){6(wj7ZzRc?qrs&?w#6zR2H7-cqrt#gD8IBfia22Aiu-=P?EmX|_ zeYjZ)95e$WW8U^PN;o{@1o?T)f-`>1LH&fC40Fc|o!$H^!;ggb_&mjl%Aa6Ma5}~C zrDH`UBl`h(odXFb44p3X=teHpA}PJG4-@=bIIOSB=bsvq9o|FJ&$Q8eVvpHZo?_;C zY;Ym;eX@7-{{}N4{<Ts+{erq7zZ1{@o%7*fVsGc*^dD2?VC7#vs{qU|pS9`?Fs?r6 z(w4r`gkH5qb?LggdKM%L#96<Dxdis>iB&HXLFgP9|K9W2lermT9~D=S43}%w0vav4 z`A|O=RZB8$pxM8kQI;g=HlltCXfOU-!-B`o%z3Re{6Yj=1*|#{@TG|kLY*L6Seo*? zqL|C*v0CgByvszv=EyAlE6KCuDqcxdC9RpOv#y0Jg-WPC><{`oyc_r;hF5^0p2dU) zz4GRx%7gSuMIy4(%<UL(436t8lstJCP^cw({}!~@JnHi(+q5^xH(A6Nbi80^00#^@ z#NWqW#sh1$+up2lyfdZeHWCtS@8S}E8VS=A^jGfcmihNRCfe~wyT&wEOC0pJolSs5 zvhwY`*q3ccPYM+u<;Y~;nc%DgGa|M-AbTo(#N{pCb~~S!y&rvRcCtC~U{W;{nDE&S z0~a^PWHk?MqxnOTp~AkX7m_8`^b8CRTk#~L?WTvX8@pS!oRP$FCkqrjXiKEN1CqQ1 zQnG>`4nS?{%1iqdIY}ap4KDnjf|tKIk*Sh8|9IT=W{?VVCkB6(YJW17Fg#?(+p?_M z7`YSIi<cuOPoDBzP5*jt9iRg3NMINwpJj<Fw^vbo1{Zg=ZY`-254gp<7)M&-PnKF} zoY;d76DcNQfhpZvS{*L^PxEZ*P>w&VzaTf@uRZ_&-YSj;Hulyg|Jf?ZziQfFOV{oX zb(kdJVi7bg?lB;+P32foU?)dr82^+q)()idge2Esf}bwtBx~uE&a3-faL!meU3^TC zzCsG<(HJ7nkcw*UGWX}nToD8iXw9W{j@oM8pGCIvHf}noLTY;(+9l*iabJlGLpK!L zx$K0xLCA}QLJBK`OU>K5c)3A+tmK4A!@5x^TUswWD}z{~my`UTC^qQc<VZXNOC87B ze9=+Htl5%9OW@^n48G&}ut^Kz;3sWF8s9+vCZudwt$KiBTo_V%v+bEDfM$eexcyTi z6?0$^!l7AE2~ciR9o=!@NdLA%7I!Vj1FdF!Y%*RJIV{)gg?b^1$Irmxv;6qtL@L=e zc|Z7DB7&6&PPe^r{{`<MFb76^nYzFzcC@<{F?=b-weVP}6WKH1&8t!X1`eQhUUjZF zZD~}dl@+jq&Ov--nz=!cAuWZXU=?>QiAU6DQ^ht)sJC_#$t@`~S!8?AFa!y9XRa%@ zenWLAXExM6@>l1E-1eJ`P3(LJVwCZV&IZS2<eU?OQWiVMxOS0FuKauZkrY(2q76|B z<Syu#bfpEZSO86}<!Wxrrprv*j{|aPhGbON6TYZt3`e^fSFu1r;*@`%GLwxwHsk|q zkZNPX2*S1Z_<xQrc9&P*|J5y*K!DLIhSml-Z~Z@Py;GYYK$mn|w#_cvw$WwVwr$(C zZQHhO+g2A&&3w<fn3?xS<jx(L5i8{HIiHxqPQ~F_lPl&rr!DYAUh8452-2Rh!+3vw z-d4s>%fC}!&wSG_e7$48SIfT#J)iKzddY_W>MX0>y30JZSd-b$_HVK8OtEUHpKRF^ z*6(0NiZ*(1RV+18u*v3#)VU_>7{D5zq;$BbdFwPVwC)k3)MeLAWSM@ZV}2{tn0zjL z0PMyte600{`C=U@LaQ<3QM6W1<e4dn(wmIk!f@|9OEYofax)K)px9w9TK;w{t6tew zZfS%5$j9a!RNLtb_ah|QoNa9BgVl;-uS=LxxF>Pb_23-YWmkO`2Pp*Fg&g@l%c#Dt zeQ}5H@<-e!AohrOy<JVG+~eMn;_8~Ur7_;6yZ@_zCBCtzZW(Xg?X}*clRrn+0TDdn zt!OvNu6@ZK87VTDP1-`_vXv~)P-Qha0d*e2w+FWo`H?tBI{85e)KPHRzNc}d5JOKy ztYLUt!(j27$J*6oYcGK41g?sW{zA2&I4=7bm^6(wu2@LLGA3h@tQtg*_Kv^7a5*F5 zj~M}`7H-`RM!2SnkFB--^z@F~cg0&YpPO%?jbGQvMvrmny+S6UA5O=wXefv|Z#dT@ zB6aNeFcFVXjdpsI!D7y0vythXPRM}~O8=tIKWTdVUl#|u0(cWCH~;|hpZ_=wjIH%; z{#)T(tN%Zm{_C%Q8$LnquUab`FlJpdD4L#R1PS>sO`l&<X+XJvIYFWt?0whuuYzvN z)mZhboRx=|)rA}P&giXA(d^8Ojk-+m2}6E>$+IbGxQRq!8fD4M%!~<BPHON{k(|Ex z^PpAA1R+@mZF?Kp&9>&BOOE$ss%xE7XLUBZRn*x88}_Nlq6xJL5g<#B^j!IqmNKI$ zj|50b^JMd7vF@M4o7BupyaM=JpWg0{_8akn@qOS)2~tgSU(vmM35If_Vv;p%c?$Dt z!WUPOMh?W_Df1D&#xQa9F>+IhdC!uQ5m1U*HKmo<yaaRu83{_%$Owf&HWug>RoSwV zYQDWk&2hZ+wm?<ezGO!x7ft)=7P66GG51S}-^9t}B>8>=nFY1g>r2U)B>gQe+)I#f ze4WZk4effeSmchUsTYy`;R|DCcA}7Z9lm&S>I$H+<cJFyc})i-NL3gK+XiHZd@xh+ z0dwS(s)Kk+4tBem;;`W%l&y^*tD6Qx3kAs#IINF4`waaJG6+FzX@o$@oH(a-fGmPi zhG0?p9J0|lmO|Qy$HNTBw0c1$qv2vpDzV%UxCmy1NnC$|39`_g*zsh7aWx*)eK)F> z0IT43q9>ThrNc;ZweG}$&H~PaVIexmr#>{Qq78$BBVhYm{rG*bdhhtpq&@>YiYw>o zk@H+GnhHJWyp&@;emq-80%_{4(J)xR#IoqplBiNx)t_W9a$XS(wU(0mQ3lA%?6H0X zF{?s+C`quEFcY}8IX67ZC$hS}zL;GgIlb(8rGq@iX>0+OH^ePnR<Lw^n8+&Opq_iw z$py%pURn^%rh2x$la<HJcK^t?vq{K^eDQ8mMo?<9q_VFvlnw1Ip1Kt)!DH`WP%(fY zmf*U82wpc-y_7^Y&M2_l$cBI#VG+RyiNRBRE9!bnGj-QY6-F>N4__#{&&s`mZ4I*} zkZP^eEW>tq4Nlivsg3D`ZMYShsKaciF@3WJ`eEjaxzFe1^Wh7J^~<TA)WFED_WLiK zSJW!ei;f@mN80Tl^dj?;!l+uJBh1u5(KU6+emTo}%kXRL9Hb5mYJ7@Nj91TMSuW|* zBZ(pPWO{>fz_MXD;B|g$e671`Dy?=q{SkmLe}N~Zb#91FM6tgKKp=-8pxr4XJ&hYL zgRxaAJ_s<^(w*Iy<s=qru);Ir(lgEV`2vIn|DwhJS{{+r;MJi?pgCs5XHCS4HAkIm zZp<z3udHoPZ)Vb7=LZSr_tTpBURtr^Y26%7F3AnXg$wU*^yuP)r5vH&V^I+Zy{n;P zcydqD#%2V3k^;S%2(|~XyR_3~ORwI`KiJ0`Q?LB3EwrG*11UtN|HObET@=bwhT-tD z4MYy=V=ZdAzbCZKKvFj7-8J*AjsBMcow8fq5!46Tny^j%S4Z36`Q2PE^#PHz+T04c z(P7NVe=1RjO@LfaUua{RURE;-hRek^(vjUB@6{H;;HN$RQKU0TTTjLeUGi3fJK)=> zl8B(bWnswIBPTHmS~MpIm~IG6^ckUutYpN$R5=tHwEs_qt=1gTr{-rr`wxBi*;$v! zamrNwoOyU4?3%zw9f<>V_!;@&s<YE1ob0wrX3htrBzw_um`>-mnYB581M+PL{*4~g z^kNf-6;t=nPU5PjJ4gNA>^L~7yM~ZsK?%5oD6jmHRekmm5~h3=qP-AsQe6l6R7k*y zT*=(ype>BbR!pp5qCIojqjS7AdS4(jt6P|mCn7@x7=1rP3~+#OszgejD?X2O?j@e3 z3!o5(H(5v&FzPmEhwc(9g_QP-4WpC@JmkXCMeEMnYa0u1X@3BzFByNpGg3~z*3Ld8 zw0OffVXA;#S^!fhD6lp@GRrxy)1ePx>@00M+YPta**!zoKk|iZ6-n4z|8O5kbposy ztOK$<_nk4N#^ksZ5<A>oPY<1@=F}Sk3$p?aG5mYXg)ikW6baa|xwnKtV?cC2%SxN> z)cPjKezb-3eMpSpS%R#AQ%Q=snGmu(3>}vM*+m189<a0Jt%w|`?z;<^)i1p69>gb3 z+#4&7pJ<*{e*$&SR~tH*hzCN$SZbo_>ASipEwJ=VVYXW~jSo*hYd<qNUDUWp#5jI; z|A+G7B;W#@T`j;9b4vWIfzJeb+i4CE!4#MX`sxN4RSQ}edeoO0i`T9z(waEl^x#$= z&kC17zs;I1&x4^;KCmn_tbgjEe8)7?%N|S}4)cHIBJCXIjp(QI(7bSw_KH_mPpavC z&K`r`-4R$^(KXTvGdapVzpzk4{YKwy3jdUlPXTND?|+=YSunNjnbl@<6EQyHbXSgP zi+ERKbrEBA1I^=PnIs_CPU64#4;xP{d&CuXS{29w<m8MO`0fDxnC)Z=rJ~oWx@{kT z>fHKS_^_UJ0OoD*{ZCN_z_c{J@VjZF|F&TLUptATt(D7v*-4JoeybDMVZ3Km5z^7Y z@36e#=Dg2ZK%uBPMKaZ|2r)1kq-BFP5~#+r?|11)xO$EboD*1K>%bi9r-bg~l7t<3 zb8F?W!_q}z%a!z4A?i^!$VY+!X&P_K@|My7(Df>S&e=qQ2}ksPKT=J~6WYD|hLP{O z;wJiXwXpI2fTRwDDytiv4SMUJr5MBF*G5p%GrQLHSMe9OuF;XO5zjP<8Vi1^AQcHY zl{ou_%HHzUe!oA_cKO~eZfCyy;8okn1{yXEcVyO;I<2xxvLDlxDAAaR2;E-h_b^9a z^n~5^{1{l+Jrbl(o7jt9?L}s9)<18(8gKx2AV~=nx|4H9t9SVA`h><R=zY!UxUh@q z?m6!w*)mwpuiPO#4k!_zi<pa|viP{f+^xj&F6C-FXQUZpTqFn@m%n(s1bmOY^nq}@ zKWUDCRi+f38`KWJX<h(oMg?lZT+%xT6xHZ&V4#WC&$l<l3{iHN6cscQE>C>z?cp?< zNZ7#{{!p=3Z|QIonzQb!(jwtbJgw4ZodJPM={U^;G+st4n3=T6a6~qUkGq3c_NN#5 zvt-t<WI6f8%T-ndWUB9-9><aNoYH&g*N<1op<_HI86r_G8M{oh_yDgnx!Xp9rD>ZU z31@9?Uvnwa2F_>;Z&QC@$(-vuhep@-;CP#j9?lC1k9}{zlruNc{!nnRS?kh(qGO}U z=O;<=QOSfqjGH)3_;y$qsxv=F;ima$(9FvQo63x;D?p|^vNxfQ(3SlN8<Va|5O+p) zNf4Nsa`vKq^2v2Y?Xp}9XF?5xMWf>o@kJeMLlWQ87Q2h}L!KV@zmq*`ZTEKT3Qpu& zsq&|Q<*-fpt=qwR5KU71L--eip$=h_=E!YgvHWHJL-DwO8)yq5SdnE+fDGwP+1hB~ z;cr{f9slqq1jP)*=scY%mhw~rXcU=wiJ33Qm<qP7m=RXxz-Xi>3keClNStFVoE_N~ z?|srC+OZPbV1*w)Bqj#T@h{?tEdPFm>UMJRGv#FwnrZ+sfRzM%t7-|cS_%H5`0}}W z^Jaws+5=P=_&mdLlwSIP?V%>~Y$xv1+IN~2ZB^Uz`u;wx1b8weY0#4L!|G;p$;M>e zg<`dNKxL~%H2K=kCK^j>NKwN!>y2K6E$5+MZ4qsf#}Wd!u7E$@&El(~wR{blDf%!@ zCPxQSG6gZP8?MB3@Zc49N9Z}D;t$V@1<R9)o~>Dl2p#MO^>cQh)q5|_3-dOsLM#Wo zE5Y@@^(2X>Kkk~wH6VXjZL<gbA?I17+!`(WdjIDXI7YUgEd4$Hq#^%t!*+5uFg5<K zKD=w5uV1~L(mSi$?_3kN^-Ru{h+~Iwg|aEFsb#m_vR34&#ttmda#qjUP;^0L$Ts%l zbIX&EAST~p<eBi)GNNY{d35;i<Z#uoX?&r}NZtc6T-JQwSfE`+H_};I)c(s_wJero z*Qk>G<ihi}1~{RN+Ff3!>cMGnLVE<7H1azoM0rYgurRWNS>i_des$btq$f7QVMIq& zeaiG|v?^+WIL!EYns(D2VpH>x{m>nDty}L7Dk9*tc-FT(z^$YQ)fi6uO&|xnQ!sh> zoH5KBfAp`xS3oMdVq+vdIWXuC_S<Jggakh%pHJf7Tek-+i*5+09gDF?ZtFUacpFB& z`PR2O<#O`MVIEnht$o1Rz=VHl8dGX$N1(5Y1-)`ne(;mbG4Bme=*lReKA1<ZUDPHZ zK>lnr1p~T6{SVeKpGO4q;_!*g_lIWj;O{fLaoo%c^`0p012+U=yMl+un|nFHOIfxH zu3NjpB4B-hv=ghkC7H0Y5`wmk{%`~2uZ>x?1=guI#X#DLT_|#OIC4@d+^=k1TBZ5A z!mo`k-O9<-@w^q5esGJ$GYb8J0P!Hk$&SN|_%WHGehqs@l^c{2f_P1DsS&MINK*c| z+|TuncpoSq#dk9g%*ab=$GhT7x&-37r>}22K#&1Vfi$140yvPOmI+7$kqLyr-g7ip z#cu3|4}o)ik&(tQqWwCM99?TcyMH0cnq89TJtWFY*@Cu=82ibrAY-9gBpks|Xx-)W z)(h$aiMyfzVG<)o_W<n?sPY2Ix&7<~`o0xj6}qAuQmN2k=74!90dGRKcNk9zByzO5 zm{`?ExW&|x>y;6PK_!u2dI6Zc$w_}Bh!3bjV!Y1#wF{Jx+X17M6rG<DUvZ>bd$F~~ z$)|bQS7_PTX_F5GPNmN0LP|B3wqQp@c>SzB;SPFQ8Mj*$Zb&LN4Pf{^6JDi!edv9< zH3F~NWxUPFHaiC8iheDk?Tgg`C~ld1G-MqY5lkl_IxX1Cdiu6<L<G3queHn)8`pCy z+2?6FWuRUEd_W$azj9(48vqc^e5SLh=v}wLFyG0ZD4yV~t9xce`k1EQ=ZT;pukZV{ zCC(frSBsS9_C3o4wFsv}>GZ0gYT^I&BZpyn_NGpgE}Gz`t*GKn?cwkT?Rd?l1A)QX zIOdK>5@qt{6jutIwVGTF<iTwuN)y;|OH1INt`PLzQn2Cm)v*Ab@So;aytc!2g6f(y z^ZJ&kR4MU<<{|EEZ&dV`W#9JrymvG4r26O1RynU{gB&!nm?IAGi)rK-EKTAMLeM-d zR!mV=p%(8@Mda#yuT_LXpc|(tpcc-P-64cL3*aIwn~=E~sP!`bLsV(E*VyANG0vzG z@fU#5wAaI}2xpRlWij7J-$mE;_AQ$z$YFsI^_g8uHN#_r8Mw^oeu^eAL0cXy5xm73 zB*YlWOEBRk$OX{ywG$Ip;gqMkB^Z^{eAyUas6`6LonorMKu~zx;gAZy&GQ@zk6VPY zaYE@x)NrjRjB`Yg8i;^8nYmdS^bv_&oH@de@ez#P;NvQTRnH`OQD-11mzE1+uy!J5 zp|h;dSXlPl^n1;1GTBY+EFaeHS*`Gj+v3yA-Fx0}+#+C4ljdaa=T5GYd5&<Ii-mM4 zZzlU8gz(G`e0JV*mlw$J<KK0SL*?%jK4X4Dq00D}_;tvkRe=5}TnIJ)tLRHx__l}h zVoFXZjq0e@3={~BE8lKtp`Op8Y8{-tK`EH)(Au_bet_@g3I(U`<wUQOpJv?A%Y-Kl zmI~%!ePP!+r*XUPPZo3GgOEipLe2#CQv+H>UrnGN1~i+E2vWQdo-MYHld<dEPI-nc zk$Et$73aD-NF{zqUiOH##>;Ja1AB;*1h!WbZ%<}EL<381YLJaT5A;BMLy)41^kn4l z=Oy1WRs}JLbmAcP-~jrGj}HX<2DYva(5&10ZjpEgmu8uk7vWTFMH|o}@B|L*qi>xK zb{#ga$S?1{%P23B-5<n?R}@9K@4cKoIqDCmhaEyau~({nO$4R6qOnY@T`DNwb~Epe zy4W&Mp$y;rmy!TmyJc0yYrR6CZA*M0F+Bj-?0Fw6&Ea5(QC8u`1#m`CK3MO(8u{b= z-j+v>D>L;%Xf-S`<p$dR28#CX3kET~<zRavIDPsqz?C(JT;cKLeHZ6aN4tLI5Q$GN zm_eB`1wa%aM18j~KuvJO)-z3gn`7PRQ2ejy9$kfST`aosAI`s(1$_M{=H<Ke)tbSO zE5CtDz~TU2%V5(Ob6Qf(P(8CigS&cLY#RhJPi!t*q=$7)zdpM!k-ZJFCwEK9S?RvR zUCNC)&E1({(p>8G1c^S7EY=4b+_YXcCtUx7sINu%i?mBIVQ`F?Y?S6eid+CnzaSVH zTl*HsAvgdZ8yD_>+2|~ECkWiSvull=zK%H{E{Q8HFrrLvT5&eM&aR8dAV#J?rAmA) z*M`%`FPyvejHuv$)ObSbKe7;@6NqVmiI=!i3fTa9yg*{0b^xqS)qHXU=frca>BVzb zbSa?h*U;9yN`j!ybED+|?nvK%q^i?wfwC}XgodAxc)8KWrCrxH&A3FSKF*i<qltT1 zZbNQe1F-4CAas=-jn|k*COU{j@BFK*?8uftP2meF{I_9UbA^f-gaIm<pV0)q&0-V> z_&m7Ta~Qc7!tb}At-h^ZK5vpBZ27|1=E?AI_QtP?AtM~va=EdJ2WY%G1y5<BMd$=f zd2grWE|UC1@prcH8+=QOWw?(Jz)5~C>eD0=7i9}+doe{g+6wkV_xygDLB2b!nyemR zjmn2ImI{w*h-n<SThTA+8q^MHrjhtT194;qjP_WIc9Lz;%PW0bZ#BNbBQ)5gk#$J= zeBV4k_kRG8Q(NHdjNh(-tp%k#f-A<wuGrHO6y-)hC3xDLTYo}_q+4%LZ<_Z523TnQ z5BJ$ICAN8Q<y^J=3^CgJ>v;O3&oQqCZaRtS1-H8K=LELvYSZ5?2pol}opG8zx)2$` z_ejS~0hOsZ(R?H~8o*T!h-z0~)1+gr-=!Chm3qB3vZMgFkG6D=`X6X=5;v*3z9|Al ziyIG)9j;H$1G#oVG~K`#wM*gIT|10+^$F<i-X<)JhhC&7RRD3@GIudiu6qX$b)V%O za!Uw0jjes(N;*1xUb?;`w5ztRpTJZ-5iVdImr=lX=$ay4)#MBJ?J`+%%o<zMFPdBZ z&XpCTj4R4`4;Satzq0J(0rN<nSYt|8y}5(qC-lsxg{ac$0@&fu@_rom5$}6d7=Qz? zE^_GCW2Euhvea_8*n{WWS1OE6b)emSQLWwiAkmg!5Vh3^V&kFo00qnKF)7kb|B)Ov z#Z+#P0Gq9jScO2T1mC2lY*TLFrh)!_D(PvWu!MHt9zHEL!x+Jn70Ai8=<suqt~Si_ z;f)Mv;-ZI^k&ClW>4`mfRA7cVj4j3gai`tAQIHZEwZC(Eohl>Ntz?JI+aQ!aEMzGu z9X)SP+V4v9wE@z-n)qhIHvhAIm<+ZzEf|^9@>`TWBL0t~owXKdZREvfF<)LO%r%Hl zC}Ng>1EX7EAa^E=s6@w^b`&p-$?0<Tb_f@176;PWIlDUbn$T1U`Su(!8o$X4O224F ziI*FwFRnPhWBf+SJ7R>isXvaHoV81I|LO8@|M_y=o~sw-;|@hG`XAm84Q&KWSDDdx zgnb`GVpG%cf*q>hhxR&Pv?!ZfhxceA{dDZbb-7vRwY68?e*LwZo5p16F_x;aXfs_w zwb@*}JKOm<eYxgFeE(B;UTM!7nU(CMZY_sqv3(YAm@t+yP6H^JSGt*4y12wkpL(3* zOUx-(S1m+#H}u3AZcZ|!=G_3Zy@bjMx8ILtPrs+LNV8TWp+pJ}-n9`Oy`8d1Q<&=- z-cqcx?@4?nATvS7=v{z~L9|U+HH;`LR()-}a_|ewT@@qm+?e28KHA^sKof?RZ7R8! zRaku@&nM8;P_}@NZ+q%~%)>NRJ*3KcLY1?3x1Za$l*6*hOj<oPEV96WRAtth@LSse z@_X-6N8N`~?RFVt2CWtI>Ws{Uw=>3~PZgN0d*gJSflNCoLP)uGxQs1nr(}hbcf_|Z zrs_O{fr@}FxX)dX*ZBtjKD$GZtVO6(o#3eX-Y4iDNczRpg8N(Lfirc;=z1enVNgeC zyL2aDU)WvBp-?Xj<LLnwhc2<;Sxa_!%RJ)!v_^<D=H%{h=x68R)B~UHJ^W(&fV2Q4 z>?2fsfVY89eA)8_ha^&Vc!2{Yg8-};shFqGb03{D$Ux$6NU7++e+OW9bPKS(Nvn!< z!#v_A?$MS$pcukbcgM=|C7B<Dm0fAIed4h`M0{G~ua;D0q0S(OK~@2$OvYHCuJwwY z>H(E-6-i6)9uY}{L*xmmUjiWkJgqRTy=6Gf*&${tQe;X7G3`Y6=sF$EvVdBz@~TJ5 zq|_s|Y$;ck-ssctyCJ2PS`}n`68weQ%Zp8skyZ96qF$OOfC^rr2k$98=If1iip@1N ziwt2C2Ls%gu;PC!)U?~hD!sxfpt9J)i5`&2kGjYK1z2UEYI!43KpDXSO}NUbpvylI zYt7tb<(CM-gZ=ornaGaP^oR_16iE%-8L`X=U^f>nY|RcKxYBfYwRk%p9b_I&SKT8R zdQFR(!&KG=fO{45$N}+^Wri7q$^B>3;C<&nStpqw=m|GoZ}~-MJOokz&8C1B&sW-~ z(ufY?gcWwK<AToneS-*@tO4i^->vjiAQB>3m_T}z6q+^js}2OwIG|Pa*pSzxHairD zx6BCeCRnA7o7B@{@*ny}O$JuRO$?-Mfj$LgmZ5?uH;^)(>RIEP#U2xsRIEIs8Mep# z59Rn61)w+^a$=rPqH3(n#2pnjOVl_k=;T~LsS=>_zXEtEYOO>ZL5+UxOi}E{lqD<* z{7mm%k<dLu&P((t<}wI#Fv!By?g$T`tYEN7JhJPmR}!K9t&%zC>6G%~G#ar=j<$h= zXFPOG2YGCZCWjG2c>!UtYTNm#j|y4pm7|5PcqUO<$N%6JM+fIQ$U8&DUb>i7SKsX5 z{abEIG!++`9)$9d>oo$YM~zrKjd35^w9R8?gz(bIFDp`Q^#n(gk(xq?r{U+^Kne%E z|5$l-B~jRZB^a8Sn^O5oLDK;N6-;vkr3>_cL{<zUuKsiQz>9(NB89Te|K9ca{(hdk z)&0J+9^}*uFspoo`}5AOIVu1U+5L~Zn(@`*0#zL+xf(~Bo7T<u@I8B&=^{Ng<HDC& zo9O*jGEJOLA*?SswHEtqo3a&f9`?4agf^w3Bglp|F5I<=z8cZ!I_J88xPRHe>cd*A zo3xAbjZ4ByWjOh)GB^NnA(7^k(8Wr`t+_WrnC-VT&iZ);)p>uyCZn2&rz3$VXkvb- zb*{mVLOd}XHb4c?@9~rl>IGm24x8!}&*!*~E=>xZEHH`9mR`c$!ekhwskuJR?dI|k z(yeyGU7zjol<j&5LwXIL@#VoN-GFvkvy@WxheQZsFS8jLGg0n>F%2!a1_hC`ZJ>CI zW6Xv-4qT%!+i>u7d;H)bZHhA@l^r>+Q=860xtiRziZwN1aRhH6A+o$UjXWH*+87E9 zR7f@}#dq!$cjUdwS+45dX*rC@NjgFwf)E5C7fyo;?Kx|w^Ath*VOqSK0>dM8L|>A- zzvG(k#RH5v0uAfM{|UfmWYOuK4#9$M0vbcjaf+}N-h8QmS!ni(^Anx9E9meiErw+D z={<*c*YTf9Ja7-_q~p7e67&b<E}OEH?)~;1JUHfy2w)e`Pn?P8QAz;VX}46F=mY+K zQUal5B$NJT|71$&W@w#PdZ9D`ZBLx#ZAz;|XB!_mT4En5XxHV~RuI!f7dTCzk!tAV z*>Qpi0IFipfgTyFhE@gvCxRpxs1s>eIs<CF#GLo2n8<mKW)INJr!2h;$AG(nEL*lW z1Xb=XsCy9j$57!3kQH8|{Utye5$A?*rkVPjgi`i`)Vtxyp6`U{BmDYbrhepU-Aq)S zp^-jYoOx{W^ZWr$aiSNo4^V)*c^8{>LkYD0adfsYCVLoo@k5lwNAA<SauLY3h%%T2 z<J*uV0?Jq4x0n+15T-eJHZNiU!0E+#AWX_&xLD01WLMLwR~2VXzd({b^t~Q<D|pt0 zH>w~3;aOgew1Gt|9|cH@v^^JpHMhI*C*A@n*ck2@J#%4ReD!1lw%$^4^<Efd*jvj8 zT|RIwr{BUHPHqfpw$Z&ueLM;zygwa8hGOFA5dk!0nRBnX8R^Y%f4ip!m)|M`wwm0@ z^Y;6j1o)ePqwoeNz%|=m4k;CM%?wM58JiRr=Q9B<Jy5&_>xfJ<m<Z@~g{N5FdM3G} z!8An5Wk3hUJPW~esIm~hg;cq6Z5td)A8d1!A<t2+F<I)t-a9#8djBZn27}O#?qVi4 znEdsm-%Kr5_;P}$sSu_bsvgibO>s=4bihD|j&}}SF@vB~)X$D;$17kTF%$$6`r}~@ z>oMRnvFIv4rjAh!VL-7rFH62h032e;y~ekLWz<ZwPfJ>2Yzq3dg|xrQbMpgIJ2lUU zSwj>){av@NQibU|mYylGYhe^);JN9oB$Q8+y=XOLfjRiRK9e8+!EI4zYi2%h_jt|* zq-+s=dNCw%Q~?Bv4qYuy8L25*-|;eQ_G_;~b8-QyJuY;ph$!pFgqQC<e-a1`Umf{y zh5mp*J6hs=9gjL?&&1>dJL+P?XRsEw8I4QGHpgHO#VQvK;q>O+c>49N{t@-&_Vtp{ z<~mBS9Ba1p@k;%)&7aQow^icCD(xL4wQe)TC>1y}?G#B0QtgI>g_Bubs&7PIg|(nJ zp-qvQ#v{4NtCs-U5h-0sE;TgC1z8bu0n~yA;>W9nQ4>1*vYw{6Yz)43FZOtYJWbK~ zJENPVlIhMDa$c^M%LOc!sgMP*-4rI!kCbz)>|;8z!<CYoP{Z{zmt~HS*#t}%am+ml zeqE_3V*SHLJ|M0J5_3(KcgDg{*_;1S-DEEPC;v78BQEpFu3+&UB##5YYGJe8P{mj1 z`PFHswETL8%uF3Q+0qPSpV2strgp=wqB$n|Ns_EkK#nvTteg7VpA2dK7N*yB?bGWq zA4NbzJO1&CA6L_atZ~NG{NjgYtTva2Pp!GVqNVpwyRqd1N#hCIe1jFE8*WvM*LgR( ziJph~@REY7=ezPx@wkf!%4=RkQO@8;4(w%yLj+qy$(O_qzcMlv7`RJ2*f*GN$lv(; zQ0(Q{VR=HDKAr<m3({uhnH9+cMlExGD6aj03VyP-bD3HoquP+swJQ>*T#%0QTcEYh zr_4lPVtUW%0u5%qI@9qKl6A^UZc5$cFj-S8*mI1TocO{PC;^z?zQ?;e)f5T$uwE6L zcgZd}nMgfrwQMUWJfY<;!r?2MQb}Pz&O||1T6&lj(=G-vG9&i|u4KG1&We=}hN@n0 zc|fT|=^^>(oK27~9PnAVm@lNcE+w;#cKE`GNgjKD|CM#WjdA#A1waR0BNaR3yT+%@ zx|K;Z#wpZb>Ac8wAV^quMWKHpYaclwkKe!~G;(BKph+j>^`$aM&<R&$sr8{N^#S;_ z-s==S&3@xqkg}S+thmi-pmVTeP)T2|`pV}bmzdr^|Lcn@-P17h0uBHGLi?BG{(l1W zj=zuFPR5Q-zpvZNTEF~1QKVn~Uw;Fh6bB==xQjjg9&0-*4~Q93o_#H#m4Dt?v2Y|I zsDrUM`NyuUiR*iV7nw*g4?!4-gR9F+dTO>h?dY~uU>v2&o}xouPG_6lJL+m|%TU4i zEdDo|@O^9C&~AaDLwv&Yiak%lH}9Ocna$xY*KTo=@Y4WfV6<SY=Bh5`!H(t+yNVAZ z&#ac4QB_wdF|#>BOtS8FSWL34<^@>Kx%N;K>*%$7?3qnGQWEX3Q>zIEB9Y#9VSPX= zj1{uBmR6T|X-#A;_99y<aaUb<;Z+{dwiWCqY;(NBm(ak{;b<S(V<$8y?+PtUxNNc~ zz+YV?i)(A^p_?o14hj5uJctLwkkQ<2ggV#(qB&+FP_VtAJE1|t4`4q2MUDNpIV<9A zj*g+mM5-9*H8<sK&{(3hhr!NS_>oXqzMt<)uEWDk_**~QWc>!ntL(o@W&w)uu04oE z<@>2zBn9{|Z8F=ih`k^cu2Q10cIGpB0d%euKu8f3fmb?pBt0eH@2NdJ9Thhq`y)jf zIm7d6@~@P@lksF{XaDAkgBaK=ioAgI8MJ?|&O^?!yN{Kk%F$KFV{AWJ)#I6aZ``S{ zASF+p5!OQL@BkF8T<1f<Fe4Av`vx|=$Lv!I*ZvqA*Rn{PJl_(6wAFvI8gS`RGoeE! z6A%S0`IRG`u}Axgk@Liw3lUM|wFb!;#`w^X+rd41u+m4vPcFhj)=k_@5q%d#1Ea#r zOfcwMLHcHh_gVD{<zB=vfp@g_Fj`*<EA_vj3_q~%cv}bsJ6W$pTc!=W#4l7*ebAgx zHT6F0B93qEgX%xd+Oj+=L^7r`Gy*p9_gEDb-=Zx;MMEz!_+P1d(LMB*E)m!i@#t58 z0RYir`BZHN$W(fR<jepmtkA1uP=?~CVLyO;f!U_NVSul`?CUv(G3n6>pr)Ytz|4ML zpbs%y=`l1~<PxHMO!=d5eY20P)uA>(d~y<$=7T_1eOm(!J=ZxNl&U^rI;46=SqI|~ zNjvnZRbB1s*Kyepw1Y&bID06@yS`R{tuEUQ>!b=6jy($ztH5yRf5Jf)2}p%xZ;!PS zvh*s{w2$#XG$_#5@v-nDls5U?pMH$A$Qy+P=i)Vrzr?g>9q)%+FJ)giE#1h~zuTm& zZ>G}*P?er}6E`Y>DHr6kb7s`>C=X_Nlp4+i)K_oc^4&c;NO;PbTDPiuDDr<Uyt4f9 z-z_vynxxSJ>HnEsw(mK5(Pu{;^?mdUo&4KACE-Lndx#I>)4%k&66A&M^93(nI-mz$ z*$OLQfoi=IWz-jEiVntXPpO9i%6!LlZ#PBDwjkB?NMEH)q)TvO_H9*1u#>Hk9i@gd z(4kUx1mzCUAef{E-EnsUx0gaW=A^f~iQj4yCb(<|S)9^2BKL{s@r8*a(z4Iw7*{D( zk*@!_L&AS9Og>yJgF;9RNP)`(#jZoiGsd>^(BRs4BtNQQSx`Mrv2c|hgAIemHk4f% zo${uc9|3}osm)PFPW0EAqEALRJLGVp<HBHX6vsAJ>^)nzu^<}`G@ibjhQ$<oMJw@c zfJb5u8ZO?hNJjKXvd9a|&=tezuK)tfK_=(hqS~;k`lLd)QIVF;`NL^ph%M}Yi_0>2 z#;`bGMUr5(vAfK%>$K7k7*Dq2BMPwHhdiO+#O=85A!t!js=?CuNA$~f@tgmQvhHWc zxP%5fTNR5Xzlw_o$DX3`2eA6n{dAL<`gDbUj3AK)(17ml$AG<wlaU_|O_TqfZR}~N zc$h=g$|)<34;HBdk~!*`*9C;^eNMq4ZkT!0xniq1RB-?8t=frJwYx--c-4R~U8Z#E zfcRD|OJro(HBEG$=G(W{!RkWSIX`k)!{nezJ$ccpcklQE9j#jw9z=I4`!8%yGm9`7 z$2<}>{u>Ak=TS@-ol_?>(PldiucuL=R3`hB{qQDw0=Vf0kE!01iAHXh$I2h<<@Dz# z)tPC?h0bt}p_Av&?Wf~gl}2fc9g0XI<4mh<_M>i112<0(a;1m1GeCo=pN#yaD?oOn zRhDN!W-CPgJ%`v2f@VX<HyyFFe5)54;1Za8m4lC--<XbkVh5V+RWvL#wX8EuGJy_5 zo9Cd-gFY3I@nqx-UJZQz_IRe~eXEc9Y!T(+8zn#rlbs0w19}EbDi~n%IiIQsRomf< z=H!kzS7z9~lU$VPm&tuIKdoV{I;hNu8><!PPyAJ-f_d&0L86J>37#wL!QX#y-YZky zhoabt8kPP7CW*%_k@W^FLxl>9T1p!Caka`y)Mc!qgUf}vNXV@@J_}9kA62JkY}bEH zrO~T?*%`JTjCPsD8nb=YG~cZj=rleILd+<ilhP*m$}`vosW5e0VWG2t{~jT{HR(fJ z=3?8Ah0Ow4RIWU%_oQoyb!47FCe2OW(%g8^=L+*8CUa`^I4uA4Xs~y^F26IIf`+XY z)@LM1;m6KcEWl?kGiU4apsQ1$h2BzREmt|bS)^A@5NnunRQ7D5&jNUNNpHa*w$S1~ zZYk2wDl#|?rY%YYD(8Aa9dq=z>R&0j8C#AzE?%U^w4K<L4meK~)=bLj$S$2=`c!tk z-Ycmh{6Sqj)qaPhk6m-Kv&0KRlov$0=EmCh&$BkuKzK}q%3E11xK^pW_Mnu0px!Z> z7FqS%t4fLonaq)&{V35MBHK`g+4W3U-&KLtB}%ZmTa;nF=$PgGgMIbSsqRKzu*KC< zh3lpfl#cib$nk8tIYE2Utq8h&Kl8}p#UiV)X!er&g{4tf-Q%tpwK@1r6F=Ovwe!aB zAs8#H3oPL6^lEh3;)ER+rT*L<K{6nMwCO{n_j*3VSaq{zZ>nK$$dku=FKrlVsVjJg ztjHXGG#cl%NFfzRRW%Dk*~O$g#sY88hB1EZph?7KE_9V+sPbN%zGE4tP#je=!CYZP zLF+zo7oR;U`Sybw*;dQ0MZb=OfX8RLl;b|_xDD1b%>e>!h~ieIls42`<B=^;b)vr< zVxRS;u5+yyaq;`;RF~u5h1qiBK@(3aR}~nnpY-T*9v4}4zC4qW1Le}$S&`uejTcVn zGrc~v0INTzl;5@{g=WQgHN0XAh!{u-65yM54o9W2DXfcTEWc`B=wI?Q#m!&j_n1!h zc`0@1IxgAgm>}Sr?T*4qD5jK1W>oU}nOBEb+xhg4^wf@8JIyJ=2F!7U#LP99l&1_s zRzxd&5gYQ~Vb&;RFK+~E$>Bl<3vA%a&$+0#&gs7Vz&WQPt&q?VwkOv+-SMVr!kxE$ zeYO1t<*qFaPBMG-JJ+aj2XTf1JIjp;0iXG58yg1q@W)RsxCP7S;3Z<?q4BPn5B5_q z-3KVDxXjceUdU{Nhe2J>03R}HQx28C;#Y@GdPL{UT#@oNTjW4n|EOIJj3rESYZQ(n zm0m;!<@FmG9x)ngJg+V)I(^OS+qc|LC6UyOXSx>PxG}F1%o=6VKAaVrAO4a_Um_0_ z2~J$NhX6i!X0^t`f*4_r8FJS8%^lx6d>`Q*n;ol>jY&vI77gW`pz5w`R?s+M>54M3 zmh?V#SiC?^vgw+yml1mtv~I^22>t|{qwi93{==?RzDmt5D%WqnlrN_9ND(VVhRLL7 zkM!DADQXbFKXEfl2op1IY9+Ds0K}A+EzmxUY2dw8@`$I*Et9%fQ$G%7{VD>E@7Foy z$t)Hh7KGED4t`CM3SR9cz*aMXsY}BT`mw%ieyAb5BmjztJDa!3FPD`K9T)8VuqSdD z4x<74McDB|tj@CqJAmSCjaAU7$MF^#fFdYhCtDgf(enB~h4`|xImI%U<1^4{N*qGL zr%_34(~4-P^6CB*50a}n4?ff2ABiU6w|4yzZ`-o2yGocaEud_<WG9YtyFyH*XipNC z>|P@KVXCAtQ~-rmBuDO1BVD6*srjqTx4?u?vWiemrdv|`jfP$(E_Sf!MEx^V(#TUn z1aaiH9#iolOwtzq8x{OQY4&VbW|Fu7`a*sd>h1x1*RT;@vZ)9V(UwpacEL~8W+tsA zOg%-FG1$FXfEP5_^W2ypSJ6R3G~hoQ0?bc@glKfw)6$C6I>9P0f4m4c;K;HvPMN7V z*;C9l02{PTZ|1fM96L_Vty!n?#9fo(m%Uh5BKW4EcKjxho}^2^qpQg)rVcYc$mY}9 zg?l+KNQ?tTN%*kEBS>jUB(&@8Be4dqu&@0U8X(pOh!Qux4Bh07_3Gr{qfl-nN0E{S z4w6fBku)Hr2B6qWmt=hJ8rQC(5AQ)}gvrAji-CKkdm?s@CFe9VRUSnw5hIxF^oi+{ zxW}#o7R(t`Kvd(+j{-X_KzqS|<1j%bL;N-Ul$$2AR95S>C(O8~c-SlafU*Lbm0sBs zwE!*9)4v;P5_N4E{Qt>BWyw}_-6-i|h8T&%L3thmcxladsU7!mONN)HC_@j$CN{Hw zRlwcJJDWz!vhPS?eT5}=RAfG`ST!LJrPZRVm<8uH{QY>---J#(W;Hst-^{W!TZgt> z$K37&T2z>B6fBf)27(Wg5E;o02IkRH>;&;E8SyUJ>ZiSw>}U8Wb*og$?~gj*FtsZ0 z^Vn&lR4b>9?RX@R(mk#6PKrdTtE%$%@7<=MKjeK-PF~D)jl_1$uN_OkSsWJX8Y85= z)Qn=raN8{OWNV)k8L+2Y^=05$RVgTxofeveDO$E_^jt`nYit|#;>AcSQa~tC#@FL7 z;TMBxK}A~ReaJ}RC%H@Xs^g)5KLsc-ut*&6O6IIKa}2>$eal9_q#KDd+8j<>0H_`{ zkrXL1;MF9_l`>!DCbtVsUDoVB088`}57(z8IH(37EU#$P`76$giZDwfvyOS&^*3J{ ze>>Sd79Wk_oR(NQE5eGzRfd_v8>-*i%jn73aar+`{xs*5Ay+Us17_+pO&a%6T`1{p z#*VJ$>>SlJDN<yPuMsxliH378C)as8i#Dls@(8#3HLpohje!3#gRR*>K&4c7B|^>8 zSQdq=k9!JgI*f$>7XX3As~=HbWUkmuqNUqtvuclL0{a8}KiyxN_h~#HAOHXng#S?> z|LS9RyZ??`O>6AhX0apq?Dh^Q;7zD_R!V+c(k^7vTGT51$q=*NSWWiVM-L9>*(6q1 z{@S$+1Uzg;hq0pNuIjs=^4ev4F@p&gYZ2M}vgtMAUv~f=hl3EgcS%JI={aEt1RY2M z(x(!2_~ySo*ru+SUZ4*0_zumy+@wq0rR&ww*4d?<TXy#Tw=cRwS;G9f?z5^k>O>Nn zBo0c=fHoXUO(rMZb1alQLGXv%n4b{GkFnxIt}u+Eah+O>ok~V-D00!ce~HTH6rE|P z7J*RM-+R*?CBZDl=O4DVDMgYmN-BnqXzUpsYNV=T&Ly&{Kni0(^E=JB6f5}SWjl*h zKH{v57v7-}svgu*2*<Bj)Cvo_OrsP?am(j5t|sfPugxDPA$UAa=0#s<g`<(77&HfP z;7t8jR+4NxLoO$n<cCr;X?0_8CaV`l!m`ac9RmbAeJ`aVQOCbpujJYsg2FBrSmky0 zClRBxDz#7JtU16fng+?76}t-H1Rn0AX&_tkz!~cU&c!D)x3W!uRZFWrcI8@nMbUsk zSy`R;6@3F`L%=4nUBoG|*tc(8+eM!BuQ(l!8S#;vF5$2f4eWB7B4w4F6@|Usye}Oy zsi^b@MTNs+(Gh_Oc1{H>9&IB_&5c}h82j~EOR*$uzFH;WOxE92;EHRDRBLCw%eH*w z59KRBD5)l<a(zQs^=VPQn12$AB_jo`%ngT5SLRK|5Yt#uQ}p}2+&!a^V*Zio*`m1D zV#Mp8icb$CRa|$T{x-I}J9#ucz(cGIBJh~)mYg%AjywGsSWdGFY*c$&4(&Xe5b%@E zONQR9uvW(Eh)NzRf=$DB495x}?BaE~Qj6UxGZ+3+*GTBqqMHut+Ra!prEsr(%4=;8 ze+v|}_S<QWdGsk@6l=IMW$3AzxJlyl!17()ATA!tJ9OJJXP=+PZ0&ODP)RS%RgUGQ zl-X~m#3*n^ZTcGC@aAQp?_cW?w8vsJSIOd7xM}7TBf0eF&mx$O^8ZxgbtyJ3#VoHM zEenA)9u+b{f;sx)w?3MVOn(}T`_EmTN{odMXC~IC)u_Nu&ux;;%qco}z7B0A9mebs zaH+t`@o>=l?1t8RAYlSGL>5+W-%g*zo(`2%K0L86NY#6fR8#rtk$!tkNJ*t8DVi8` z6ycZ%`eA$6T!>UVyIH3mee?hj_>{2AtpQz_zVLRx%?-L^gR}(JX?b5{>gs#?*YJLO zy8H7UcEdx25*T3T$sM7i>hi{q{(XT+bU+YnBI;ZF!^58t7MnBtdc2_O4vGzN<A<_H zF#rjOb94MxkM|UH!{ngR@fi$y8TxN{g>;JTyazU<RGC}s1gPAo<9%_<o7Xl>gz|>L zJ=DHaZL|_Z0XM8#R`G&(Hv!BoQTrD9aOnxC!Ev;{BKZ&09?R|}&xG|Gq3`=4*}=0! z189=S+30NQBsr1h)P(pWFwDoKgeeYT0cIpJ+#u&I=&%x51VZ+4mi<pCdlOp^S)U!Z zkE0tdN>8#3IOhLmARd9suZ{nj*4=&?i2t|C*U3TO#?j=r(D%OsqSC))G=%O;RfNkx za$$mvKd@H*%s2h19cAd<>>>12(Opp#3M3Sx5^=u}#O4GSW80Pee!L^>nY;VxLY2y8 z=im*=-A4FU>=PO@RqRdr*qKHooQ`FSWa0+%=;tZ+`G8##ack+pjGpUtALC7N<>|!& z2IbPalny~IgKvm5&Bb*j_&9>ZwS0^D4#aU}QQ$10OX$<;C3sLZ=x(aJUO1&0W>iQC zpJAYCmKL))4mbYPMzpHIfhHZS>jbOxav*ra462WR6BA@h*cu;VrPGI7)?{CiNRow? z@AM%d<pYL`Xvs~IXNgV=neXhGJ;U(#^qKK{zTjg5M%<$)5A)2`N^hoVQVB&$!eE&N zhW=iZES*ta>Ib44RZxqFmTF*NYZ(QcQJ3gfcvm=#p|Eq=sW8lCJHPDGO+rFTiEHvF z+GsVo+txiJOxe^EM8;^yfZ6I|O^Bu+3=H@{Q~n%wE?`V%7qBDlIhOd5**!f;&cKD< z^0t;&u0mgI{#V4J{=~Sj7ndS#8C5l=&2z%IN!$v~Sffy1I^B8Qk)5BJZ$>P-(y73O zk<3#=C(#Os%v?2}!IPym-@@o^xQy|6aGiSqL>_!J!+-6JrjneNAL6A4jqx56G(|RC zxzcUkbVx5{Y#hV0R!3K>=!gMJ8X%_k3Y`ThvptRSsfk7K{e@ML@+-mf6@1}4>2&_B zNUd_bwDL(Mia92Qwc&4xoo3oL-mZE^+}8XT%&}c3{t3S|=^4jss&zX*yn$fH&?K!Q zciYeLms3uFsGLe&uJPs~D=@UNNNRCLfT1q~`a9<V+LsE>sOiE^+8eCQEaJc+T+Bsn zxYZ+rx88ym?0=_)R=ny^*AdS7rI?5*RV>W}{fH6=BW^2W{)PR$!kuk6^tgPtLUcxV zX_JkiN53kG=S5?v2!=i3pQw3J_KElTE7guB2^qnnhc~#0VySzD)`k|@K0UoGJ%!L2 zz^#Za8%zPI>5L<r^;oAi+NzLOqct|&4vA&G&Xp3Q9MO_0naIJS;2_))p9|%0jd`#B zV!@A3rqtoP0-DZ^)HxQqz$9>D{w3$FQDnP0)EtphWI8Lm;zp_M@;dn?r7G4oCy0?x zA=_K}X&f!UuM<SZdgixEhuF7M9k^p|U6unbi*5I&W#IXVv*-%Rb$2@FMLu#H%T9AS zzB2Tg&)76*zgIq+R!=7D3pD*CQyv1s5vcy>yxFJVt8zH=Cpn2P5A)}R$5g_?Xt&3- z96~D~x0>7TMYQ=#Piiw*n0Vype?d@%PL=3*e>WEY|CkT|uXW}B`PGwEc5Mqq5qwT- z(>Rm;Qu112Q?^2N92UBX6%yHUgIDN>HmRYvwY};KK5w(tVE}~|FM0JEA3aYtZPlw) zV$E=m1`2{3Soh!{ZkF(mz^8KOs{nN~HrUH^`7e;7`v<7jmMu_08Vm?9U;-AFP?btg zKu)K<sg&<~vQ?btDXEKT=bkuy>t=+uGjRT9#5hPiYNS4ts~%NP=`R}vGpAMr+ZCvG z<^QmW{*iYy&Mm7Oy5(tFm^Q$XV==R`rK%D+WW0n)&V)1^8aH`!rCKW9m)AJIaGeY_ zU1wI?S_b9KqhI1q^z&M1?w49T8S8OYcs6<ApN-n&_zkM`-)EQIy%b}nN-<%Gd@HF& zE4fW(5*2sYH>?D%c)wzS2-K8GxD~~<`qcH%&Clh#;R4VIDNABGtF$h`PpxCI-kE7U zJtPQfY^;T3b-6lqqpO_s@a)sxX@I8aX=F2W5k^)>ed4@3chi?))cA&WY8a*h)0xWL z(sd;*iP?Lyl`lzOw)9?+N+j3K7NLYBYO8x(E^+E-eUWYxK-Ja^xMAs8;RW9{wyuA- z>=eU9l17+QMft)6h9|RdV0$g=f2h(pGf&gyoV^sTKPiTz9rNnkG~6hYFZ@s^iTF{u zb@jIE4pyUi2G5=M=bg&Z0=mtcTQVF5ii2;M6noiM-v{E5a!nq{`A%jba@>e~f5w}D z1IM1g^RS{IP9rkuZx;}bYaWXoU=lyij(H^1WnPCd7StgrgocAl4+w?1j$36kI?EV2 zet*G1=WCT5@mdCPS&=F+<7Vd-bzaSuJ^`gncW$+s6W+{xg)A?aax+kqK2W_KKpO7_ zHHv5t$YYGF)%DexOsFuX!Vw0H$U~tHa&MuwVNu@)CLBYaWg*KgQk2a~iE_}~ZGu|7 zKj^9CX%^7UCklI@RESWT_)MwgL~xIeh<I0(FUAN!OXQ`xUb(HO&4QR+{o98dV6v=) zT65%Ww}5sK48Ol_QKD$gT(@GQ3fFEZcIBb-XAdSRFC{TQA9kk?m>U**7zC^FefDJD zqPe|2>6!T!Z517g#Sk^m!fvpdinNjD+&~AnIO1VUQyDl84HKuu+~(J=dm9|&{~c?O z<^^)uu4v-`qp{NN_Q13U|NfXiAdEsooTCf$W**Fi6;s5Z%QB+|scSn0gEr15KzTMS z0Z<dNzR{85XGbF~_aNl0BAD}SnZqC6>LuP2K9CZ1D89vlkJ{8D=TLa!P8gz>f?<=S zf%p=F!3?HtK9Q+BBLc#1^E_pf=3F(9neI+EMyWGTF3e0%@jrRP=<XJ*wqmT@eV~;r z31S&BQ|2s0Alodo)6K-}1<`rUkmXW6b%VC$Ad&-uRHMog^?ba-H<}snYIWjrBOxr> z2kA1;$5WH$wwY1`E~hx{qAMNA?d~P4U--hg_JG;qgmyf_|4)@vBEc)N{R^Yet@%$$ zx&L1xUGr!sZn8Vvexr`P=@aKx88O?|WLO<-TOBMJdmGQ%th2Bb2M|jLX%adUvltE^ z|NLR*4FiMp10Zo{%gn5JRv_${*EM7IvpG9BIAEoqYREHINKn>^yViHbU)CSxU9cL@ z73-0N(kOOUiJ_yV&Ee5ReMj3VTF)rj)Yeftsx?=S9=rBw6+iOiXRg6`xEE^Bhmcr) z`r@rlK+HEzM_HzR|CeKPA*)=!TC3uucp=L_9+s55h&wQQMv45}jyu!4XKi0e$2Af; zALYAf^-3Yjv<#H<{?CCpNAkrWP}4TCNTGUPy?Mq={z$gbxM6LM(A-(!|KsZ&gKX=z zY|*rB+qP}nwr%H5+qP|Er)}FiZCm^1S2ymfbLvH%88O$t`D3h((Z=Y#x3*wQbVzNu z=mfUQx>>T$M4Jd2uG`XOO1pDf=Py0kJONE(7wvu<UEizU<k>Tu+NR^Rcu9cgSDXc# zV~=zfQ6<aQ=<i=NEI%&A6E~JsU2V~7Am4kKVKI!@>njr}E0;E|)QnQ&KaFAdWc(~} zBC{?H{DKX6>oz=w*#13%h3Z0_S(8EG4!V|&Fpj{Oek47;_qgpp_5J+(70%!FwOQlg z;m_?`mPZ!7+Sa?(ll9H!`(pMEZT)bZ_I@|oc8^$P&Tnri?!y{*Vyj0I-D#4Lx(_W` zVYA5M`)ZfsSMe+wnDW~63Kcwwp+7Gg*fTt&(p(8RSt)_OOtTwseP%WS^V3w|qn-o{ zY?*4FH5h(tIFy&i>pf!ANW%$BiXR62eRGSdKFFkr82YrjoiD>yMd<lfr*pDT`q{sF zKz|6Py}WlncoZLha>zf-z>bZd8N;s!H;qQ!7q*7avU|hK2w|u4@m^)lx=e71ISofQ z(9nd*AP9FTfw2_ILFX=0AyYjCX%{w#*kPj2q%}<!jGTT2D!#?1t9zZ5{Wtq0`~^+a z`!uQLtzh|#YaFTc{kuDMEaz_f>Ztx1ln!z$EZ)og16bT;x6qRiH#%(%lo-vsus?^P zKA}11Lvg$L$=y8K?|IJ_<_ik^*m{L9F~Kq(50F+sfFP<LoGd?%-(e?#*YTK~_WCBH z@X&u^74BTG7sQpP!W-}1mC0MG3Y<-{=G1R6RIY*U!@8mX<~Z`tZ@Y{<*_}f&g?%@< z^A3{qB;-aW!LE30Zg##DzJ<axF4PAGYpT*4?(JZ6mSO6f;jp=c1;c<qaB#~|uG!*t zM9)7MPpb`V;ZJwl(YKw3!Dx0?*wqAHkMD12-&}i#eDZH?japD9KJQ1i*Vn$vWJ1cD z_3-Oq`1^CN@`-p4_HZERlChA<@D^;T%6wfX^;lyoe}HNSJs@T^>~CjN(zQ@HImr)# z)QRX=XtRbTu9!ML1=h}`WW#3GS60)lAyATWi2dKi@qWZPuzIGS-~C@Z@<nkFty zrJs9VE1$#N@vmbL_7C9K(s-CCF}8h!ulcloThG~&J1r13x;Gmo;2<p+$K!W@a1asv zYHhgf7i6cTHFqM-YeKD@)G8%bYQRjWS$RSVEJ%c?1v}^7D6Z^4!IcR+JQgFmu%gy7 z{3Q@bwwp!}r;yRf%akJ;<;=AbZhmIfIyuQ?3dV@hn8I?CkKHTJ1^8+mzY5_!vIVQX z5QS&+>hHoP<~(wN=;>ScA`M2{mIpSCcHR4X^<~g7>I{EdhFO;Vgk?`U&H;-pVEub$ zl+2+L5{GS+Zv#Ecl=Guqs;OB@fc&G-WH)!j1b_HAb1Tc@rnA5{(za)dxo#v=Zq%hg z!fjbVs4ZEQy_-;<-n@xcwrL&a^TsF7u9AhCzL)oc6U<LMaK3h7Q}@QVWJOG&TX}VZ zF_ZRQY}1Q+c7?49YQrjy4YVYJm%TLvd!|7GpYRfyR>!9g%xicMXVPHu$(VKEnxw*p zIFx-47|L>UU4a%P<`-6+OLLq+lQ65#Q+0^2>~P?)l+4xgEex!0qbl&*JbRuvj0rTM zvf!A@nsd3QVrG|QF4MbaE{K544K5g&o(ZG-`yhv|(XvsBcSW-i_4b-pW#Z+O<+zY5 zYU9J#JswYuaE8{RS8&hU*=||gm`lgu<Eb<CTP6aZ(GZ~8R}L0VOkJDzaeqE*ROs_2 zNtM*JZI;n$bt?MVJL>!i(N!<)_H;V)R%+cV<9d$98>^h~b&j%QLUOc%KRQ~!{XIF_ z+nuid?W6wu5Re^I@U(79=}cct|INY?uW$g71{wM@s8bp+!r<7ih1A#VF6q`cV<Nee z^z+kB5@0R0DW8eiBZo^p*m}Cq1EO_X3TRe3{k|#StkqkoG1Ky#HokY~S9A-9>WiNU zSxdX8qmEicUb@HSHm<;DiM0~LN`fpK*+#^xzx%3A5LlTMKE7oEn*z@HDC}25c#Dr! zxE*b0^t2Wag1hb{M{KYjj%VV29&S9W?FJu;c1ogKfTG`zOHXbeGLS8o;2Q5Lu3sT< z9D&Neo}^y5uHg+k*LI9DMV{KWox_XYJU!PKV@f>f!L@b{3Yvy8vig0eX>w}>KIdx| zg&sQNB!e3U&ZdfyTdz#}P!}Ho#SSK4Fh~6(g`?|JTW~RbNcT5&mcQXh!r#<N`!!5# zx)<3x*&6!$OvSQyp0YJVA$puYOops*k&L8$Qf*CbFvh{hELe$f^OUQ2?{0by(Uav~ zC=(m)Slb-nUX=TtOf-I)(PAHP6Cd$fPkXb%@7LQppa%gGSkN)_kYf7RhGE?4+Kmhl zm)-MJlD&;nAzgnbVS81gz{Wimqy+6I^Dek%p0p3F3M)DuuuYK5Mm03a4fuX?T@-en z+y+BOMIu85rR+wSBzef4foqteORcE;1%+w<g21nSEW%K!WnC7XZDSm7Ari+#6b94m z&KLrn%?wla-H>5Ge6C6D{<mIeY(%>6xZ*-VFtYqX>uEGjeSB}Yx24khohR5;1Zy6> zv}EL=Jl+Rg_IlIic33H)#cy5w#fEj;HY5XLUI|2}(e16kI3)-GgO12q#h&#D(vdtu zk{>1M>|gP0n7WbDO?D+uncpcU#3ccHg*L5fLN5MaUc$bv5g5;c%MW5EO&>4AS4igh z>X+Wh@ciwG$n18^mdza+M{l6^Z7dtQ?3>e;ifzy;%<+d%>pBBfhN%tnumU3@*xtG( zdqCGDNPi%%wE$ku5SGwEng1}X=Ef6CsVZ_sD$2>%JSvMP;}xPHki5xdonsbCiw&S& z>6&}bUu1$E4Hy`&U)7hYD{^lwjy=OI0745dPZQtRTS3@!MQrfqiM6^TG66=?qOFu- zXvr}Q*8!Wbt%mD^Bk<f5AG9FpsB??jt&Rc8cEMrB5&T+3-o0fC#xep`*C?JH4>W7? z9LP%WSIgsgKHfQYQ^!5@1mEVF7p9E3#lsiOllnGdACoJv;~in)AX&15mB<`KgCp7$ zdAKIByw2ON6O$8XRLz$GNeduK0Aew13<EueDF_PYIDYRkLKIM<d0u3y-gDch6R&M} zO)iC;x`#rB+_;o4A*qQ{Ae_|rIUc3ZbRX>N3|uO-C7a2TqnrB+;f|&quTDZh8sFz# z?8_V{Ik=Yg>ug)3){z(+F{;lf1RvsD)T1jEVVr_owe}s&#A;S)A6?PViopNPACpX* zLOfDU<cxcXcaC)86-MWWDA*2kh_hTDmWxCN=qx#d1AClDyl@nuD@BJpNekoX{+Ww( zm_EC=*;8!skh8-_@_S4%a}gHtGt(@|f|Ut0$%Xjcw4mmEg6kdn+1~G6n)50N-ZUUi zKCWIo)h77_&<3Xhk0RByPVt?6B)>j(FQoy1$PTqshxkMAtV{stCv$i7XaQR_Pob^Z zYN@GhkW#d56y|H22ooJ-Zp%oW#bF!W*8Y+9!C-9N>ZXws@Nu3CPfr<HnqYCV;)Dgr z2P2nht9lcpZ7?X)sTEM|>K+6k&HSL&t>?JxL>w;6%RO{^&y4QGz`XIKF^=}!FySaU zbGrltTUtQaD`<UYxe>-W4mwMP6OCBK>33`dB<yhl8^BEag$19^S#5Ng&ZSmcAsoGw z;(xtoQ@vjrtGcuV$QwM3)7rO?oacTV-k|<LV~lm0QHm6>!JX5muVb<0DJXhU2_t>9 z!>nY;CWu&-sahMR=WmcT&0@t<RCCFknG&1gswOOZLAaN<fe?%ZkK2@mJis-0wkg-_ zxZ2|=qNk8-tdeG68AO|CrEgNsVV#t^oKG#gFlK#tluT9hBRBIQR>yHC>+a*|sAwD@ z4I~z<7c=I;V+KF<7)7XgC}ThDr^qP167j%*^c0_Vc7L8p5fDJQ$1Wzh)1HI=4o8_| zTsH^sWh|2c_ru_P!Hcgd&?A$@U!JL|n+nx7(A6ch(@GE*KQwJ>{2n&SxczhC7!j3; zpMfGyV!bSi9gpehDO{V`$&3UY@_wz?-F40dt8O~}atQhdO+KS_Ocfnv79tv`z&#qn zKgkTPtk+c?viWmSv}gICo(}e-j%Qj<4gI(hU+cBCuAgP-iq`-?l}9Q0fZv_`aZuF& zIKW(F>TlciF~H2xOmUf8Z_Ktdc4kFR{KHOs`K;*;A9~J_1f4|FY-nmfHdqR#<>iVm z<AXAK03oUeMt{TA!D+uz+!a(rH-CA8Av%d*5m`f#HV>j(ONh5UmTpFvvH83`3_BW> z@$z<Fm)*mu{LVOdNDj&({*tZNTkwGeXBfrD<HKm_1&I}#RREFd_HTsK%efr6hoPKV zdGKvh`V(aBw(YlqS!quVf^D1koRW^q@q`Pv$}zwWrb^X2V}Wdq!evG;m|xY^@ih?0 zaIm_cJYm9i4T-4kz~s^myr9!2x{8q0QBw4l7m54ao3^oL9`;+aZ$}Hh^T0-<2d2U{ z;>tj(_PEly6J3WDs-`$j?3U1$QD?0fFhsnS*8;8<Mdxa?)8Qu4*Jc*9^zb&E*>oe7 z=)uwK+hi=%!>I;O@hCpj83odjK&b_9Y@SHFX1AG_kv_UVL;AFhvz8I{v=7B3ntF(x zTBOhoM?zAaQkB4%JdB^?#!3z(jTF;ner`$1aqYDqETyOhGSkq(U#yorA%n)6vu|L) z1=di%WB1SXI<83Ej1$gZrR$QX5*38aEMPQ54qA6^$kf4+(K757=^aQV8|v`PWO#hi zbhj0W*jTm}#Lx>@h4f3OgrZ2Q*uV?#5O)Bvl}#zzpwmEglxwtZrH`IQ@UsyXYMb0T zpl$gz1iex7G@wP!0L~O$RCZ%*`9<EAd}r>*y~VB(9m8ynX3ob&C+oQ924MMu<Ip<7 zS5(VR&|Ki;)FL+9<UHhdOoH`M0QtE9k_R|v`KS|NkBE+(Oujuj^y|SDn|B=0)amlD z7{<J#opw$R)A>H0p#9GxSGm>Ih41Cr_Z^vR*sHi1vgc8;&5-5WNpF<WR^5ig6GwbW zMw|sCEC8nB0AshvSq;dWY4Z$DM`T&%lex>nH_m|q!*2K*YG{e9^@y}1rJ<I-<aRFV za|#2itivyaf0fI|d&0kxf<c;d{~$h@ZcRwL4+_TRksnnbPu-a~EOoP+o<#ui$wH)y zfWP-r9lH&DMrtH*ViS_h{*0QOKW(3u;T>_(eGl3g*L0q^E!*R`TG{|sBGRZL?x)*T zmL8NZVj;uJIK9eC8bA8CaJ7%YAdiW^I6f?bC7<M<+f`Z=&X^;4c!>d0KOJv{c1*%% zdpU`|yah^`(@u`wTR3RZE%7`?8n*p`KI9Qx3U_@fBtwM8L3@IQdNa$_&(+|+&b7L= z!tb4D6->71G!O>qi;v;e3u4GW39B2vIFVkxFa&mj+Ykj-?>V!{!2r1&HdV^-UyCr) zG+@X3_tSvbni4FvKASWS!fjbUD}>u+n<47wrIBezKkPqO=U`lKht(_nR4Zo-Afs8Y zd~S{Vszb7}8vIdCTjs3^>KLvfvZMTTPSk3No5<9zG&i6#+eNNSu^K9X2uML27LWA) zerJ~W?^bOOP`oEfW(fp1cjlB``1a5b&J*d3w{Wg@c&Z!9P1p2x?mQn@0p}?ZENK3a zBYpYD0b;-|rwD(;wW`~G8Um1gA2Vg%Y({)Rm8raL0~vLI)qItiVp;8nG0B0?`zbRd zs{{F92mC$RF}3QlE6ev|^1qvF8BR`Z!2sVO)@o@}M8!M!B^D_fi-sDtS0@Bl&$9<u z=21?)=yr~NH|haCN$dA8auV+vi_5^>+<O*vDbc__Py5ca4kFT<#ksREJ5DnhMY~=6 zH|dnR@nJ^T8a~aI^7-)h2LqtK=YAup3*lV)`563a-Rkbfjo_VrAd@AV#kL=jouuZw zSZj~G&`#>yaxzvo7Z&I>;`s-qP&`l=qlfQ?OJE@vsO<Kag5sISHn?FJfvv+T5?puD zxhO_u$q4aKM|7w*)))Q3ds<rM*mqr^sKJ11dX)6xb;N-Qf>=7md1R_2;KzhK{Mmj0 zk09%O0bh7Uu9G4eFnLSWG~f6hN~xCw0$MB})(N%W_4KOOIf?JEb?V{Uw9-CS(hE(i z|A0g*%VyKBy*_H3v6AFen37+a(uy7Rvq-V*yXwDvLlEXrAE=9c3V-L6qX?nGfy_LQ zs`=*Qd6f@ST}awSCj4rty2Gr_+B5#Rnyz5_>O?t7JyBA4weOG-&A|BfgfPR73NyKT zBW<GO5Yr{uAI-3SZR79u74=i=Wu}<{_zRQ*<KWN-EBGx(b=r>8R*PLrE`O}HtU7oO zj@@p{ZI9y?(P;t!V$r76mr>O!I{}mJ3xNX7#k@logYR7s6$wk~6l5<}t52ry2FYKA zRwo-8eNG=>rXt+#CmP>$|FjwI^EKkXcYd*IzdQEpZ<Rn;nlVRDX-a<c!$P`}F3eT^ z*ks+V2UM&LeC){u`<A#bzKgi{vLdc(IWewpPWSng%tFmosCyxmL<~?9zA7kY)T;{X z+@x$`O%?d#1x@>o^bbiVX6X5jE)=@<8=B6;O1bMI)bmo;y7flBk}SGCtERhp5+<tg zx<p!_XQPbhRoYH<1Bqkn%3*;J3*oFbFUP+Aodh7H&8Qe)o*AY6a~Lwap6%p)+h4iF zb9u$@R@3P0x=&eymxbeSXWjVcZc6gvZU;J$1)A(8-5!L6vjGs>Q_l7^(V`n&fK`!# zqrV?0eI2K7zEFF|p}9C8kaCDd)_kU**=1SG)SU6eZMLK8Zx$PWXI0;_V;&DGeXxrE z>Iw1Fxfl7WGv~z$ORK_+;;2>{35casw!hQ(CI!@~m4x7^+pXpKrdrYJ{qYW0d)<@5 zyweu{uo*;7FAxGkijN5&P764dZf(PeHwC)PNg5kgavf`Qw}ct>%UnoDQ0ZAn2hb@M zW!Y_e{tEzIZC%ltFsFqbe5StDtX~5=Kvrev)53PA%4#kjtty70E=e4+t{4PxfvW3E zAfWTaLi_Y7NoAGnBTJ?Qbd@k{$MN~oaz^gp2Vx^pSci>~V|SPRsNs=~O^rBYXkjw& zLO8%O&nhQ*8~b`=7fRbBpABp;Q7#VG1)qxtxjQ(=qxt&qCi!qGyXbT>pC*|7oB0el z*)P(?1E00TbZ8`UD0FZFS;{I+S(fMRddO{Kqm^lK!`t;OZt3|ZcLQ$VCh5g$O;Tm2 z3fT#XpG~KuZWY(&S;CX`y``)xl)*;c6E?T|%ChbXNXwut<&tV!(;baM-zgc=Gd~Uz z<oQ!cUh@xCDlSdz_MHO%33XCpo%rG`q<(v<F<{(nKOyrFa1>5q57nRHHU$`K{%iN2 z#nmIKPtc@xkea7P&As|Q9yFTgxYW8RRmo^>b2XM_PIEikvi9K|w9Q~CQpOywNJ~f= zk8R<mFRWpwkS^D;*NS!#b|zwa|ACa!b|LfkdSfobA|+eY`iAk0(t$Aaj4G$RvNGSA zt5v=>0>m*@aAF_N(OH`gL*7U!Tgu8_L(dstmt~RAx0pb?YVl=Dls#4B5&%q}FC5D= zn+%>V3hDF}BE|M{Qe()Bz@8Q<{Ns_El(*|c;8aNHRY>Sh2s*t2*-5u<%F%aHo>Jm+ z8S`#i){k6`Lq~nQa|bu|I(k_>#l6_LXDYLUagKx6?P>f}8fdjyJk_@z*2iTZP5rvo z#h6r}=V_dsvUnT=S(strRpM-Ux)=L?6k~aMlga-4mt1u6_phr2`JoE;Qit*6!j~z2 zU`lgoYADE<V+x4Opb!5vn?;)pl)5!)j!?iPl*fULIAV|U@Z4kF`NhxlQ^?~ELG`YF z>>M(v@SjXtF~g>ETVx*YGm4czKY<I}g007GvSMLrGc^Rjg6^OpQmZL0Gdb0Bk3$B} zM+`n4$5fi2Tzvn=-nK8!%dvue$A7a_F&DnR7*{&3K^w}<JW8DZEwkQ_ODMG66hul& z*q=OJXxmUy2t*8GD+1?x><^q537j4U6#;COyw69Izk4b=2as#2AY>C@;X22CMv{&^ z4RO>U#ZfWihi~!5K;d~hh_6Y5C<bIt8&vwv8K3zV^xq32nPJC1#y=exb@TxLlcf8n z0BdIH;o|D_4|(9UmahFK7n1L6-5wcy>hH(~wVl9epc!*uo20_$9rS(&%B;o~^;8(e zneLumKXVDG{V<Z>l8V&<rBYMQ96Zb<;)mIJdHiJlsULv}yyn!2CdE9LYo`55S_3zv z2c()xGq0zYmza1D)y0w{<4h&lNtn&PGyB=dnKeW9Byr!9siZ9YKM!|QluuN&WGmUY zTN*m)oBM!tUf1;m$*jeB(z;(7)3THI8m^cHK%JJiISAj_WmI18ywEq(INbdo(%Ez$ zuNe=?CQ_d5d6p8z379S3sH3LWs=Y7yR1<70Qii3{Gi#Wwd&fkw<VpK{%iKQ)>k3V* zlj^pug=7YxRwQ2CvJXc$1`lmz!>g+mJ|16ON$`v7hgtHaVYbO-%*|RGZZGefhno+F zrllD`zIi#U3)HK+QP24==Bd{j1wP7l7LrJGUsC<TBx<>>IbE}fN*-7_G@<o|CX=m* z)Bt1vk{NR>Z_va<b%DRS=848%wn)5-&hUyvGe{uSX~?2L7iiG%%qb-T4O~`O>(dTP zF|XrwzNI--;h_1^2MPmVY65WdzepEi?m;Mq7E`}x&@2#L&`)*<Hpnd@5xJF42^sm( zrRd&ThM|g86+A2P^MakTgo%||I?p($hb;PAvOSJM9mz~g9bs&f*i-SwxTc9S;jjs$ zR-!>~^~Bop(deP`NT><3>Xv%YlBj;#p83{q2q4fDduH=hkuR?B2VsT0Y*Tfy`vL_Y zP2OMhn45xK`4ae(R82DsWe*+;*c1w=#eQ*a^xNzuF;-qtn_JvUZQK~oDX+R)%#U1k zrST3)czY(ZpAx`vMS$AR%}(mBP}aL8L3x+$#?8lWdVU;=ZV*%5qPdoR5kYJeQ4Qff zKex-?c0OA@I6zgoObi*B(&Djj8+=ya4p+VWLfma2t@e)Mpl?l$!K6`usC2`hefQhF zq?w!8Fy|SpmjrRnq?7iRU)~(6!<**YpAo%59Flqsq&m*hY+xT&@CgvbaA5``SJLy0 z`w`SpWxr5`u99yR^+T>CoQaXUriqwpr&NzZYKl0>3b0^1)yCN5i_!ID`m4y02^cVL zFQh@~hoKw7)pwEOL`R;yaV?6a%9>V%0*B+Doe%ygyNPA3H02I6<B><p=*%sr*@6?k zuK6WqR`*i3@_2EW3FD*eY<*BOm7N}}NdBirP$J2SvLc;&K-=C3^sX`K;aX&dJIr}` z+|z%*?<VK%WlyHp?uH*`s-jf$`W|eDnC1e`^Cj{QA^lNs*uR)q<ENLj@4DO@(X-tO zUwqNnu7*gS##pq`1x=?$lp`w(teX47zcI)4e9Lb)GQS37xrp#Ud2eIqs-$eMd!^T1 zklXC<34&hyC87-%`0X2cd0fXZxb7SY@sE@PVOQ@D`*7YWg0k`^eb*+vvayaeZSw93 z0mh$4W4(n?4<^}Qad7s6H>8lDgu(rX0#bDZx2R?0;l;fJrwqh9;P%#VK3rd!!iEw> zD!nHfM~0_<`dItPwexLdJwLwW@n(5{Tb~pJ+YGOqKb-W(ve)X6VVb^8_Ii(G5^%an zT$nc>=)9N-gNkKRk%jz_PJ2syR_zY=IN-<~>DttBjttW1&gMsyaArqF*EQ^3>dMD~ zc$YQ<prEbbc-3iWZbyN&ICCkcdY7G(th0W*PSa7i583Wit6gi1H+Pk^ypmpK0~@gE zX4CmxFUlMZ>PqYmST-g`_O{j7FtBc>MmM(A-nlCV&dAiW^EQEvVfieOX1k+s8?K#T zanxNR`mKGse=lAQ_?GqpntY#rby)sxda8lDzHhYCJPU4X8r*_<cAZ0_4Zx-&^33M2 zkkj7l3aIK%oe+gn%8iiEPCsZSJ=aN;Aa@Lm6YowHi6w0tKq61DFm`;5KMUuM&$lHA z;0++zz!jWh3SfDZ65rh{wbv5aYPP0OK0`xfr`>VkEbljrs*iZT4A<ed)4rtG#+-LZ zjtpYQAxdZ+-LE{0H8qVMMq~wbvF0O0q9UfCI~4p)3(nB)#Q~Er;$V$Uhy=${)R~`% zsgsJX?Ri(22=qaKWjL>>u>_`#7ec#fP*qf~ie1eEMZIZI--?S4y276#6c~4eyOx@g z<ujahQC8B4q_QfXSKft<sQyz?G;}_3^z+jTI$a|#nja%)d5~lrqGr%T`!J1ou~MbJ z7gw@_pk4W(os_a(Rf7>AxRzQrjg;VvO4u>~s?Ize@x)NTgRL`k#}d-1fF9<26X{Ae z#S8fpe^<{<3V+wNVJT+;5xm?GA*B>n>qbQEm-Y+EvokWrCN#5~>V03{-zM=$Y|%lT z0nXzRFzZWSQn%N%*%G#Q&R2`7qfN3EfVRE!<wNj^x;Ao{+_$!z@*U{+E-Gt?fZBE< zZF8}y9?mTtR$U1ysl7Wi8E=e7nRnGojLM6N?b_~9o__C4@F>%1-TO6$?$L2|?{<YU zj9z5JX5RjD<ONIHDqH%@g~LbLB6_3#>Bs@X_OS8xblI;472xdGBp<t+d5Jjowol!q zO`jI<lZjAOTv>?lj?TDC5i54vRg#GZ6o5FEqwR3p1Z&?VRWJ`7KK_jyj`hYrs);T* zdbrH%FdPd-d++JvVuL)D^RC^%v7_li`hBbn#FaM?yW}MrkOy6TkyzCQ^Al$0Lg=62 z+Mqg44V9uP02TKTir|MK?FA4QcPM?sg4M;8K)(R~T9+qjyzr^Lq=zt7P8+ZY0E-55 z0G)v(`m5aPW+vnH`K%QU_)ozi1Oaf0F>BDYqL?ih8DMDtp8M4cNRX{wW~p@-Nkhu$ zE#BXdOEQSq0Fzo6m^xLJK5wwfaoP#_YXzaA2Tb`q)v$3u)bJW$1GV1b+*-5j&t~|w z8;v&R5fcNECdH!KoI?@n#HoyIxJn4(E8J75Wo*Bf@@P$&YKkX<3+vA67dy*-)!60% zRIfnqdOsP_o;?DPTZyXl6pANl2$vTgR%)CI{wq%yFQ(tJEb@h<`P_lh(W8f;9lVGs zfBl&w+#NCX!2~Sz5KLh(`8rmqTdH}Qwd-T3ZVV2aB>;v-@0nm3gYFOypXtI9h_mz7 z^6v2881`kj2-!?s;BQ(AG+??Bt4V<+*RN+vbXJ~AxQk9fMNqOrqa6%Z#Q>$Y+b!0L z;F^SvGP?~+?^!U671kuZaJGHU0fiq9nVyiCLfNmDF<LA~GWyP#+~yH`wXAs&RZ!hn z>@0F(LU^^ut~I5Bk6seXMdsHKuZ@MrV?<tc+|a_#)P?;Bl*d&nC}c~;`VtjYOR7za z^{RrUr_swrDrD?Ca(borN`q%kYE8t&5(yU7qv|qV_zR|uFn;M&$F8z*<L((XIviI> z7Tp@83{8X<8;HQ-b8)h%qHChHR4MhI-G%||*mJj$tJ5Oyrd?g&=U`ET6;)k-(ABht zc28fInYPx08NHrp`MN?UsBSq>EA-!9cVpo81;$@C_IB#UwFjxf`)hn~Hi*`^ZQ1an z&y)lJIm~@mYk-0GPgj4^?$-YT{5vrHnehhz`se58LH>FF(Tu16XU3o2)xpHj<sVE6 z{69Xipi#pU`{^itfB*nM{kKotES)V~?4ABmoB0t{kIQ2CM^rt!G3;0JJus1NN#(f| zuM7ONm3KLniGf5HDE2xT<=0aVBLa{lT#X7G#K-~rtUX_sR*hbY@{YzB^%)MA#YW`- zkjzCvb7`nh3aQo-9uL<|um@+pa6H-{0k<)RG+InKh>&?}cu(u~?(@DDS>x)kARel2 z@DkpQk-|}-=y{#2w_3q5+3j)!^(5A!J=xJa1AEisKKyuC@S*||_5ksv{0ReiJMyy` zj0l9PWvOt1ygA`ZE{632{7ctWmk2gcQS22Wz(<}*EdDR6OEA6g)vD&NS@EB#4fNi5 zPSZ!Q9)vOEkgY!O#!FY(lbyE>`@N>;ON#v{i=7E#tuVLfS0=1bobU{0BHOuIme!uN zIqVTw^zWf(ACywMV2@MgF`&D9Jl8tzOTWs}{{4*DUSUgTDU)UrWcwgI(A4-+DeAn^ zgu%+_Kil3Or7TYupY@(|_X&PJD8M}R_wA;`z4D1)={JgWxc3hb@{LX+UeTT=Xq5Q0 zN6}$(*c5H%(>HhRLo`W=m72;@f9?=hx_x0)uA)+^Ra4YAYRIcw2^IHE#*3oUaE>^i zDvGw?WwzZEk=b9B0Ds4Gn2FSm7)>yV(KU!`wqVHjd@39lKtV4_H@EzCb1nl+FSk4? znPEKNW;<Dktz(~RN*^o4a2s|9>EL=yp{cfwM6p8<u_p&|S2`)DQC5X}i+V&QlZ^*G zoHPIXt6hVOSZ>&h+_;G-OS&UIeh@O&HACA^S9CnO!iyqPM}NLeO}WWAS9%XNS?)H} zj5b}%6DwcYT$s-CeWBe>D=)ZXlmY%WMy&C$!qy-TuexwuD{y>=;RI}$8E|wM1<Z1n zt%Y&}s{f4*JHMFjSLb3hF{4zXNoahZoMK5rZSEZAp^HFZGlBOzOXfVR1@A+Zhl`Lc zPrl1`D1B1f3h4ce_Hw<yEDlvX`8w%-=NRXdjhVhmM-E8$Z|6!`KH5z?`S4<Ohp)W3 zj;(Jb@2*U*OAAeFNx2J&gT&e>ZCn2X$j#zZ)fen2Zmvk^L55EAmuU}WSavw_&|^Za z-mbyIl0W<Vf7Ur%%>1($WGpbg)5FJq;rNVj84;!pdFp=tODC_e=&Hhvl?a>MPq0e- z=(|w<e}VVUype^e3B9w6iH)TZJrli<p|h!kii(1=sgs+j(@!i~=BWJNV0@q!J2zGC z!11aGUXt3@X$`xo&3r@=O$8PXTv=Oka})LV9W?-=39d;6P8_xG+3C6iUu%Ep7pjHb zf{JqAygC4zX2xpwtlpNSe#P<E6jiiyfvl~Cat$u5Zg#xYU52rPCYAX*2<X?6poBxh z+=`kwz~E7-os*IP+jKs%_oL<#@AofSBzLiGol!$0<*F%(DO7>7E}%a(3y*8NFwi4K zoHNe?Rxca^sb&@~ipv%#&@rB30H1+Y<(RRjnaFnKNH7Jfk=?LzM+xz)<F)=2+ok_> z7Q46(o*>C=p=a%2sE;~G*FD7%exZJSg!Qu$HASkncE;ne^E`lwbc%-9E^8Dxs%2v! zAjX`>`{*(Rvc<7YDAS^p`8q}7m}A(W)=f=rB5y;wUdofvMk|wOb-bjLqhM+z{jxal zu2jTnc$$QkRB>UlvY}^gWhj*N_-TDWjF}Hjv&E9zpbPQ#m6qk2#kBOwPoJ<TQ>HCr z6rML++M#~5rLLh#q#g4Uk8b({a@w}5pQN`J6JCp}^}Ir2Ui><uqkmRIeXw*|MrnlD zKRzRzAZ-yvbpYE`(4b<zMuQu~5Po=%_v-h2y9}-Ywd%+n!9iN^wv$zsVlj3f{1nAj zGlR(k?P+)9NGfn+pLRNaw4VD7PVwX@;=bzk;MBTso|Q>HfaYJvNkf7=V^_}z!w2O| zLbzbvyN6rupXRT@DrcdUmy5#{7m(pFhRaJVOaW})AeX#saO<ZIMk_D$mT5jXJvhjv z2mbvp19AU2C9&J5ea??F9{ju%|GQHPi%b5yPwL9@LJu$?Y(G%L>1_1F&<TN~gQJQ> zoslV7IadU*RJd2n`P)-U&1>%*_-+HKT;9NtA=tQw`6?blorQo%@(qdK)+b)5KnNFv zaJa0n8*I`k1Q#7q@ae>yI@m!g{*HX(_Ut#0!+A+rg`<3vpVWMnEzS6KcRrH3fwaOH zFFI4Ysvhf-WfKGDLh;Z<g(POKq+z#*!ed5If@1&Qd*nY$l;m1#4*J>fsNewr$p6C< z?d{Ag%@qutod0jsb*pRJZ!n<vov6PiNNM^D6w3;cD$i>P^p(n@Hf}ix5tF)^g)Mx4 z;Bv!tU@4W>z*EM~9&x`UNkk?e-e^v}kjOC;Iy3#b!ONiKX$;*3-y`%eaUMhZe90LN z^gbk1>kuz>44*AQ>3|U3PY_K#pp_OYvb=S@|1xZXyJq84?7-ob^IKT=L9R}X3L7_T zO<%OE-`e^hIexQ1A(Np*lAt`~@PO_g>$npK`KzQ5dDXv~KNW>561<Q;%2{FzDtVZo zautJhpRu*GvlB0`Kc?ew*sna?6qc=J89FOkrsaa?X*v!V4TcV%5-%AwI!9oe1)~3? zr15ZK1Y5wi^)AR?1Fzz!*uL;5h)Wi_M3rF}VT5q<5B^0)6cHlW82Y-XFKZ&<ggFm* zMnte~M7~T3{9r|+13>+#24!_yXI^I?tqCNegl_Qae&S^~qz)d|0+@hq??ZNX2O}3| znZ^h`y3Oy6c)vYqvKH;%hH?8K9MaZwAih;Ti{rU6&fZMA@c5fG(AvV$MX2>Oz&%Yc zl2ZZDy%y!~27b>$ZJdTt4|r>ECWeyRG}a2Tw`V@BqHOv8Fc`XoXwTVw?KV#>r;=TZ zpVvLDMx)&QMILq20Gjm+xHGX(3@rg8zfKF3LI#k8hu|cy+lQkeO{cwhU9Z&ypNtwV zxfkE!n(&+{X+vRl4zweko91+SOeKk8lcBB5N171E*BewSD{$6SR*UJrR1~v7#PZr8 z*6q}}o}3QoGW60rhl;N(nNUHibYS3oxa_qmB68U-hLgtUoSqoEHl+&-t5H(lR0moY zzHM+ExK9}i=HuSMy|abAcHZ5*&#p<kGll#0eY-OR(LpE9`bJf);z1==|HL5mhdN?U zMCP3b1B^KS?w>BCK^v^FZ*?V2b&1DP&+Qf!DvePz1lo^|l{*W(mTEb7(#u90QpR^U zTvIe^TtaefWjMRcPmPaM#|Z4P+17nj8wrcbT)Aixs)VlvwAl3RUX9c4-kpJcr+r<v z7z4hTwE=iat;DSh^LYDnw`7yEsl~trdO)?=tEg1vMqE+1aj-}S5_HLm9XGGmM`67E z)faxDo%&56siAQ$p^4Y?$gr7TqX`nz_as^qA4gGcbA$DFkYdUxVPH)MwaUj_*Tm36 z;!3W}w{BH8<%Rx&#v0y@_ukQzb+<TIadG1IU&yWgYyT{7T+;p#)#L*O03iHNacpmG z`48~5oK~5%`^iyV52)TC<NLrwjuw>UT7;+-X;7L~9)RfNu`6)4-#u=4vXY2Gv|+~x z!_PC9CT2;rn=n4Ar#|3Z)HL4hlhlClTu(r1A**cS%vdF0IYAvQ;IhzWlLy1%Qcrgz z0cS27DLk5PCL@S{wU|RT%@U&tbigYU+iTGyTM0^LTGE_w2sTs8_?-`1%Bp$M>QQc) zC>E|WaPAV85-V#jwWkqfC4Fa?sFa-QF+GK<&~RBIA&`9=eu-}sWZWcd(=b5KQ4JP) zK1O#ipHk9+pWB>8H4=;6tdr`eiaE0}1ZA4Ymxs?Xba8r;jsvt;Dq7DHu30zO2ELq@ zL%#b&CiJu_P(|XkJQ`Z(7f=>{8qJSGfS5lJd}N}3a^mSak$&-lA(9ogo8pZWS}{i7 zK=I~g_Q2uio!Q(Vw^tnLQXAoxz6n+BnmKVtU^f0OgTkhrzBxet_YAv4r@SNkV;Sn7 zIVIBn-7*p?vNHdjUu%+Ptp*ujMs7Tz@Uh3<0XSjUC@$w>`n{-!JtDR;H=N>CS9-p( z{dJWyU4o8~#^2u-!)z_@<!&pO+EGVTb2VsW_hhQ!QbJ^S{+tC-&a)n)T){7wLCqE2 zL;}q@>qsUJR3Z#bp1ZEs&iF-I=7ztf<cB1K!Af&N8@N*8;SFbrkNr%?^D|@R+nj46 zALm3McGmOtCF2MJ^nhkZ(AL9LhilrB4&DXYX~TmSWz$FvOL6H7ojk^hE4Xx<Pfg1i zS{AT^Xo$({MqxaeCqNF%5ZcpOZ*MJk);0f~Cd2h=PyRBWW=v~4Gb#%gaOa=`U*sXc z=KC@g_npDj3HzCEIWdaTe;`F^4KwC|RX5XlnZk33`-TReNa6?<QvDi)4ti_%saqxP zAh>7I#(^$h^SCTs2=UM-obkxf2>Qi`4%RW=y>i#m_QwA2_Q1O1W%2s4N6yc#NASPf zL($dL^}hloCsEcqNB|-9<~uR~Qsu4qoE06%R3o~k>z9s_sNb_MrfW?@Z#cUzV5 zOdRfsfMfQe>juUdXx3&GjLPcIc%f)GKDQq+toZ{)rB-DDK}o%V<i@XRY%5HuEv-vR z_n4(R;`Sw7AoX0bcvsk-LtCJ>@`e;qAo!S@#3~9T%<Q;853<G#W@;5X7@bvE_q(Us zK7!n`dJP&dH{+D|05G(H(V%4|lPXWF0Pk}j^F#7dJ}v$F=-+3`$k30uVI>oqvMD;0 z{T=HViitZHrmmzdEcP1D9z@#s<X7qqF2ksO5lLw(Ygnwtm@1@d1i{w*LPKDxDKefV zW^Kgme{kgJ_xN7lJxBj`8HOYKeF{IyDd8Vz->^bvpAf=lWxQre;6i=o#@>zGGo+hT zD<fzES&I&n!S4GFfM~2ptYpNIH&&w@b#Zk9u4-Gnj~%&3@jupaK>bY<FyVi{`oOmc zt@5!UZAtw7%{BPDce=Z(CkgdyruSct=Ko~4E<5%q@gKW2{G1Y~|9877TmB;o`|o%x zCXwGhhyh{Poo`rDH}H_j#iTsBQZcE3l<EwH@Nl14(8*@iUFB$VRY0LYSe1nMujkFl zLz6wh$#YJAE8hL_K8qcEm~ZSiGH>-WZeC5mZ;nVSd-}c{>nX^<8L!ZmGm*cO>(Fba zK)EFstDq*a*w)2pw`M}*omhAz)3?M-=|QGxvU8R=qA95&O@(@QMWXvPEK)`Kw1(p> zI+gn@fw%Mdl)^;{a!^Bl>FOFmjYo^e^+ZdqFl{flMzE#vB;z^_+tv7g6A`dfa}_J` z=ac&#({O*ZFB)7<L)%qiMk8x(z_I|y-?C#fp6~!`EUBvMHIT}pV+lWZb)FaEQ=?7c zwH-#QuoR5l&#J30&czOvg<GFgIgZ<970`ifCToxMSn}f1vX#g3CT>-3^~(x9abJfm z-nk3!iFCQT%jZ|hFns?9IL@L!0o?k@X)QnZB>w?9D%%@do4Wk>dF@E!|A}+<6T!<u zErVqeX~P!xE!b%UwBV+i(2EhwEsPThC16RCK7YUC@;_p(H^kV%=Kx}ZxxW0C-^sZS z_UXt>vd-8F>HvEJu+p-_C?F!;z&(GiFK;G<f|>x4ES{6WMmQcm4A#lAs*og8jk9DL zPo@c9PnM~Mpq8qDd6TpuavBCv&63wAz7KGcOT)RvFVp%fsVd?pych063!4;pAHJnh zLfXEb{EWWxdaJg_41Ytqa=U{h%>_yBRdmr?4tY9G(vUcYy1Yl5D-Ypkkx4bjHDl5L zY9^P?aQzAYS_Ne5Cz|G<AUEk*^lZeHBL-I~?{=cmjlKVi=OvV;Sr8kMca9_EM0Yrc zEqiiQL$IXaNMCAvTlpyDM-)u3dVP29`~uL}+S=Pf1F%tuu|*vnGmWJ@UPx>|9nx(7 zRRwsqkUVp<oO5I6gkv>3VfAJlol=39#FAwU1Dr6&M?9eN1=490AAD1LpiDvT17i_! zF5Ezol2aTTmaou9F8l3pq0tt2oH}Vj{H~e+m0ZVaO*E~**>y*mw=yW)a2sQe`1&a? zn7PYTJINwy2(`2iFBggrw4|6zq~;(QYC(wUdZ9{%sUhu-A`Z7gH|HV35Ps=0+MGa_ zDdCz(7%SEGySskVYfj1}qsCPkrSdM)xYj(etCytg1hIo7i{0td7Tg<oz|~zN$7zkg zZEQ7XAK-!{IjuBvt+S;bfxiyfA<W%fTc&C28j5Yow+Gk8Z2gbZu#Z=f1(0;+mNx7N z(eFX(6X3eF@<tFHFavMwD4SDUc6`5=9xmH$w7zGG(jW!DrU5P>6K0muXY9re_JsL? zA5i`E%DaKKkbk&jpieOtC4GON5b?lH-)-6N*1xhU21wi5VIa@m{$}#W9Myfct>lUQ zVWc1LO#3SVt5fMO`0wuNUJ2JY<umivZ<1qfaS|ap2~wpDl@Qv@3jEn>g7Bnl$7UUu z^vm1M`U?ITjXBpa=rgejH<TKDE<jDE>?b1<2Ve<vKAwD35)O1hBdii31AS3ET;JyH zjG|6pan)S(2Ji(v;=zJ%?ygh&<*QyiCAjc&=OD}NQKxEG%fEQVC{QBo*9klH70*%= zh-sjd;9l1l;C`cmFeTMFlASHIR^}<pv&+U-S-yD{oHvZDXA2YO=5uV8u&UJdV;srt z`h#`>VL2a=-#;v9u;UO*`M9wGPm(}7?j3VCx_TGLC_(ciDu9gfhk>MP4{Rc4ew>EP z@@adW(7Q`Kbwc{K?jVMgjkjm(ns{5=upXhnnEk!1)2gQ7<AR>P%`|=2u1E?(2b?|l zCy)KLc2rTt5k)@9sPd9R)UbfV5|WWr<B0ej_yT(=5_3F~Qt=eoDjQe+F#RO88BWGh zym`}VQ-{k=nPWz3b~1FsQ*VS}E^2Dovoroa-UH3G0F;;0zF)_mSHqj`=r?pTommKN z1?kb-WA8thA<L~X!zRn}cltbr0ZU-(lkB|8(S!ZoEIqs%3P9e<mkYOVW<Wt_xoOH< zjAFN=^Fbu+#8tfjAD+PGlsxR2K|GeZbVe(&KFukVKc&882&JNnfekR`$*>FZ-n*N- zKbutLrI+On4PLivP}APbyV1?XTp}F1M-di}?jvr^>HmP_(in~-kv|(&)=!co{tp|L zi<70DxupC*pgcuc*7^tbb)Tq1xLFcj7>@RVamNg3fN`fHWs6wK;Bv1`tX!_nI`*v6 znDR+JdpG8O`herB41+P1*cNy2TnVprttL}L!fVe?HdFmNU&sZZmnwnGHN6_-d(7%{ zvXLprtl*0TeZLJvVwLy#snq*GNbgHA{qc-27=dxOeg0EG>x{$kHNtk=S1UphWC_rq zG+>L7l0c~$2<RYbC!LRjJPze;BJrDM7^W~>T28T<k%9?-p9;G3_z$An?`^c}yggY- z>8AB$vnLmpu(OnB+L=2fdKM$?F`Pfm(eW459fl%L;T9sZh+ubHXA~)pmA(`z8*;{l zqh`)z1qjARRmL$iS5T9?Evd~c9{r?lI3FP76MuzSa2Idwr*yfU?GCb!1e0n_1|Y~m z1N)7@wU64_!sw^Pt?YwQPn*?uFnKVl`PyCI1ep~xe6EJ&CGuoc+U>F_gx)5j(A+{g zWOVHKX~C|{UC~__EkB%%QnPh?bSMv=S(jn!SoZBPpU_G(ZuHaf{~a?noK)8m{@@M+ zu>WYvA6*?M5liELID~)hCHw6*e@yv74RML^Tj2o^kj=)T!RWLTC-qnfC2*gjLP3;? zhEx0biP&^9GKU2>RhVEQPTog8nXP3zfL~?8M(~PC+k31x9XJTvx=TT91;$SG40B5o zMrC|2E3HMta=oTYbj<yS+j?*6Mo>j$#z4>0G-eLQuae(s+F)Kqq>)3#i8B6Re$O4u zjo#XJxO0rjulH9RV!S*<HVURbT+(Db&M`$wiWUcWoZJctL0K<(vB59W+3wtADl@@8 zEf2;ZLN4pxF<6J%==<ywxpP=xsN^|YS#s7CL;1zC=3f``w}V)mMdc;$08wvOK4LDX zgeUnp7#D(<AqysSHa6QYcr79)-m;E&^qm7cwD2iB<+~Gy2?=wv;$b_(+mhXus95(z zIQ}VNGw?bE<sYJRU5#pOnMvMSnKNAgiX(A|HH45zVKmJw^|#d1cI;h?$Pe5KdQi-F z?eox5KUZWW7TF-b3-omLPyk;z<#cz))ZWV}DI=LbMX-xUcI(WNQDekf45uOhHbuxI z+|seI8qqrw=+*_<ZJT-f=UTQgQ0X+74x0<%&~%*MF?g;?`e$90<G9Y}(|+%F*_h8g zxlhdi=oin6;Km7L{~B4O$ioNOr)wEnEx4i39fv5ld0MGxsgvuP#>i1g+!5hZ;uPUN z<KF+fcI~p33AKyAu6Q-mB@9zrtLGo#bpiB%`E--?Oh5ekNtg7mO;rD&TawkHvA;h_ z!tKY){zJ0($HQbS|BI#CvLr#)Zjb;W<R#lEIfYgcAD}|DEoH+_rZIv_<kC?jxIaX4 zX{!fgv}XTXLUkQL{()}?M)#I?&Xd6oz7*ewz=HtB%RFIlF=0eFrcT^N7U|HlF9r^0 zWyWK)KaWA9P8%+c49Qq(;7UiOI$0-0W?5Y+hh#+qm$;fZbY6`ncb)1@2@1@zC9`?# zQZbw4vaj8wgsAY!qqF&yY(b22_WBnrq(WnG7PrSU9H@$FO3sV(27Ig%;yA*y)O0=O zBfJ<2LJj6<3dIA1v$DP?a)Ib?nFR2zX-%d}^CnSu#^XejH{R>-&D19K@m`I^@bg(a zasl5Qy7x(T+J)Z#fkwsWSa_*^PIMqY_n80Tl>N{Dr~F?D_n#PTQJ1y<Cq{qLEq6)K zD!xJfpc@W@eOT*n5W##6mjSs{6-m2=?{~@XaN1F~1)6EX`oou8N~L0osg$ME;zVZ& zS}8-bj8x8sI;i1LgQxf39x-F^(3!QIB8TKy*a^k;ejf0%py5lhZSuV-X@xvQ_n<6k z$)bmV&#{lUm3#2Z*98iC=ZYMAHmzP={sivRn{VX&-zW(!G$6mdS@Lgc+@80l)Fk24 zU4F%gvbv=@S;1JIJ&X*9a<_Fwm=Aqnw6Ktc^r<kFn-LIKp+YrREpGwL5*h7rk~w`H z>bMH`_>lG%|4RS4<OB-_(-+#RkK0qS=|teaHfB0$7AVU;EmF$hroaB(?o}DLrPRc0 ziWaUo)J7JI`w2#)HD)a*<1<AR*kGOt<+qOniHtnR7y_6ODTaBWSiBt=`^AS#dKRVO z*&il37*);hib$R2`%%7=PWhlzIj0nBKUWZADA@i9d+0uP+1gA=lqc^uX_{`kkgu`I zVqDRB)Fdqh9tQ!jf(d`a3dT1Mq^+YS70L&nxzWI&Mh{=9C2b3)AG@Xv<@d^-zFx46 zx0Psm%2ZeBV%}j!OL}8@jfy(8%!s^pO|Fyu@oAjJgi?)N>W<-zLQW3)Q5AtXv7Fip zdhBt9M-=IUwa*5BoSX;n2keR^cv3e9z6ik_+CrvGP;4Hhl(2T}4mn<^!qPgIB=VYa zDSHGOKgm?`UY;<5A9X}8_n{_=Od8rfB9Q7;rn5wlDn(50TKni3#p}^9p>o51dBLZx zzFa$e@Gn#HT)qc#l<@XZ^?ECi+qALVi<o0`*`lOAbKavWveaLFhbLAZznHPKgwqX% zA>6nOdolE&R^Mnl$YYAWr44_;WPZpQwA=Zf_YTJtiOX`S3eKV|3+fFP8D&F`7DKt* ztxQ|SY!&~>_=g0LQ^oTt0SCeKwzOYGlT$<jZ0^BLT5H9-pyW5#fk_8+9GutavI0?+ zi(IvKDp8f(51*#GR#@)v+nN}+O~qm$471ekE#Mj#n?)arb7ADdzOQ{o4Yn1!r1%eZ z%gycbn+}9WNO~0X24nF5T3G)lSp~i7HKY7g=_GLhe>x8TUt#_KSFF?5H*&SKaj~@1 z*Z(Klu6%8iHrWzyU*1rCR`7Y;EOSQP63yMO8{1lu?K=`%en@2Xmy<G;6j4YRg%hA8 zr=$G8Tn}&wb|frq%x`q)v`MgI#ERxET-bksuZ#XB*VLnLTJ>D+uS+$NR!V_hIOeUm zZZy+==G8QVRyy<sIAuSK71B-komHyqrm307Gq$t)lvO&DJ4;bpyG!4ws+mAXo$0mQ zyjg8eBD1o+?CbY&cxbZBc6(%}zH(8$b)DHdD<{wN>g?D?jQ`-4+I^M~FZQXWVv^cz z%NOsryV6wML1w#rIH~zu37BjuyMamTFXy+L)CsWXf9*|E(mm#`k@rcINB_({^ew&q z)O0E8%RcEx1O56&RY#?E#}A>RfK~_vqV3|Fh0g2ivCWnzA?rW!e;9kG2FrqNTQ_YR zm6?^cZQHhO+qP}H(zb2ewym4%oTt4X*0~Sk2ec6}XY|pcf30imy|}-H0|etiAZ#R~ z%)R-9Mqh0&c6qqLQqSQ6GS`IlxGh=v_iFAAcBnts|9g0b^Zhc_W?>ElL9_0NC2mVM z=1t`MzA)6eg8ccquO>b5mdyOHSrP2(EU*qvWgK^UP2GHk+4^Wc%Pc52A&pg&J&F`? zQk=%_CY~*wZAYio{Yrdf554dJY$?W@p*2zs6uXs0m&>8L-;Y@Os8C1xbnfSMb=?{h zZ|ncbm78XM>I6|A|9%#9dG~rgjha7=``brrWmWq6mcO<#fiQeY>yy=!MCQE&=}*r9 zkO4d*7JbTr{cYqE|Mj=%?Q!pOe~-|OnHKATy3>s<FyA4&#aQQscQ+5DH+QO^cVQlH zDi!Z+!tr2>z3Ql3eMIs7KK<<;)BA1H>n*GMbJ^>S+WRg0?Vj^}-eMn=$=HALdk_Da z)h`Q0dE0qSEBq^AGY9H!UrRSZnI-hwPxj%>T(n}T@N&i%_-a1aST=_XQ7mJ~w&+wn z{>^I*<>4(o=}4EKjCP0=qw^IOoW8r%friILdBOEHIVMN9d{Pot5RVp=OzgIs00LU< zw8Ia=dj@+T;@ElXkv(CVoh;wacc*D8rtvF#7<gT7I<UdYhyTqF=G)oFek_eYQeu=8 z2M}k=LvC^xlXE|6PY<TENNk6NSsp?dKB;RPkR~l-6F^Mf83jng5Ey{peWxWMfMW+~ zyjR^N`u!9J+I=o@$GGn}>V*wdhe?{b$QZ<q)V$#Dt(jn>5jybN;npzu@0Kd<>`V>1 z8?h0E8VAKYW;WDpTYWJa<-}{y?{;C_nAn9YDKbGd7nJ<%q(0@ds-4v-N<6A@N(oX( zP0i_dpqi=2!Miq9Q{`wsg~_|iXY!DNGgX2jhlrl5gblIjK{yZ(>olNHmBfj82$%{f z)q<F*zvOp;^VZp?+E~<+_P<S)Ov7vVLqUdRvCg#@VbWAn&Vc+pT;r#ss39H&5W(qa zAri%gB(o{_$7n&>(CaW)Jep|&_vEUb(~xE1U__PmKE5Dbmu*lhdTq$I?7#$5QC=Yt z`ZATSjn6#7Ts{Bh^!Du8wkG>rc~htWCS)#-iEcAb)gxZZw85f}ly`&|dEtOx;Y zQFuHl1}}4|g0M^7O!??y+Do}0o2!>6>vT_h17uW4IMW2<Hyr1Kr{=QYQW8D{ob1Fo zd*D6fl`0cELAS}sm?b40#DM=Wk;yTHshCnjEpyNu9PW)_q89xUPU4^Bc#|{<^2A^P zxe$R=OW;yyqCx|QjjaEwB+sg$b$K*XD)nF~e8Gm(WZ6IyNfg~yg(pDPdA^UDsj1%9 zbxV;62@zL(b%-;fMZ=P|OT(Ba)YAMA9Wf`RKkzGDNufu2i)2jwU~*yqB4V7cRy?hc zxz_oiI_M$UIREpKkp%e=1sJ2Zx}h;BIZIJ%0vD1ks%h3ShH13AN~bZ!fIORd)|YlB z+&^`1a1ouDg9{X%6D6xvz2krZ)Fer#%kSfDU}Aw1<y!EK?Zr^@Mr?*_%ouKT{)+Cc z<3};A1$s7#isJi~1%hpAv&)k5PEE()Ryu8{MQ_7ui>Ee+A`j=4;GZMw%pGlp;F3wx zDs}z7gcw+P`|ZAyoPXk+w(^8!G)6$W1|6-?2P>uR%sg7v%|ta^6JhvvZBr{_z09XP z7TLt9Y6<OYDb9H?0;tko8$Q`DgGWXPe9)(95sMc-2YpGMG-sLYR}$M3FyvRl%Wcvf zMAWKWvHWj+m$E67dW%Nk1+^jmvVHlM5i8=(*t8QtlGPD@eiP|uR2@$8MDm)xTtQGI zdyS5F*CVcL20a?_DKjUSQrS)t3)XvHZHwUkake7~C?+97qgUM$L0ZbNOs$<U0UaKn zJKWH7Y%E}FEuQ=SI{BP#v;RnvmHn20|3%v;`S*=;cX6&iew!_D7IlT<iVKYJk35TK zZj)af>Z~!dW^mr5T)=mmnKwEEp)H3L){5G*`OLx$5!B4R1E2mgSMTU`Bjr?}okIg5 zyj%XORinEI=R88WJmOxZp#%Q5*ck5DG8U)YLaypfV8;o?)}?LD&}Qv1v2HJoz0{oX zk5H#ydqMc`I2M>09$)}}XmzR}sis<aeEt1?U84_>R-`}7&_ClgnvZGPr<jx~arg;E zEr#=@u{|0zm<qq+uWp=!uNYv}F3ATrd~u3H4$Kpd0L9#sLK}~YIwy=$ix3F{hg4dF zeeB2QSOh7nvX~L0_%@Aw?~TP=2#!3jU?f44DCRliP}ESC6=p6PF0Ev@70xl}=sl+v z{1*RdT4Dt}U9gn8+6Y8?-D_70fr4<Ed4dt7rd5U!{PY;&FzKKLrV>6M`<BT%h(_ah z;XG}T^p;U0MRtLp0j5aiddz{|eD@M4y6~eEq%T^tqVW1nss6frSX5<BEE8J`YTpp{ zLpuAA5>GiDAQ6NQ7!yMb;T0;~li%@)@0TVEN<@0SqU%=Jcl5XR1_w;Xv42^|$i76p z8}kc=M)cQX6L2>AWsq`&7zeZu5T$5xNv92&)vqwpv<T<eRg86eqZGRSuom^#ULd-y z5Z-EtbSipLF>X)Ix2mJDw$8(_PEa}adliSILx?awWuKag00+7fV*h2d5W=V_c}>F9 zp`J5{lQL_o(at&gXb2Smi`UZ|B-+;-AjnwFK-Lm~w2YA?(>K2ns9h=U0VgBLl!Odo zC+QASXoihfFyhLz)+8t%H8p6o)-8%-30cZDE~rQ8QFY>^T_ii2kt9)i6IHpHs~|6> zQvy#I<q(s^Hc14ArCs2%!u1bH<-J8Y7sWckeC(!)G!d6kE>D-T`$_ig{DaQzGD#dx zJitz57&rR{u|AtO@+Q@&Amv+Ro!Q)=<7rXEDArB+(MiE;t_VSU0YKtBvjyI8S)w)W zw^{M8!XUAw+T&}rm+NXN@6gVthM**y8j_aIA5cettQr^r+fV~v+l7@lHqgLf*g{z% z7jXn1iS6hBS!zE^A4_6Garu6n*yI3SDIbtsq`c_J1zd6mcM-f4%vT_FqqysjM)H#a zixIy2Zr-ABQUj~H4Xx<7V&LlQEaP1x;sJ3C$9;^H_S%th*UtN5E?o1*p*>xkcI|Nm z2%lEbP~SK)%#effd$v1NvxSX9uh}-A`XHcdp2QhTHXGR#T4dmpac%%C)wP?TkL7p< zwBVTGvnfTXdc{|skaEve$G4irv_TIZ+({Wu4XPz(>7dKsb7B$IL9xG_ilrJA!{7Go zLTd@y#P9GWITq!xRT-ORvP#LMmWj`}i{hSm8hP9$e;Qxr1h=XwJaKZPYQqHzrZzrP zfHq)DF(dM^;1b3?OJh^Y$2CZxv=OnQZ@@5Feur!lzejahj?I}~B04_%d-aU{#+G$K zdW<M^`?RvubV$5nmBIQ~B-e!=125G1k7Y?14Uka>Uk85#0o%~Qg&G$$KW03p4OY)v zbB7J@_m!zBqY-VwXvFlF2PoLwx;8}4GCCbOl|Vrz&1Zu-D{x=E5~NV-7A1g55YZec z`wtd(Qs}*BJKMNR#?=uo7hb55F4F(4ge%eE8l_<oDuIfabnz2(71%z&J%>Tf(gM|d zg>J^|TnL_+_hE|hHctpgZ8-dK&}OZQMy;aDwyI<1&Wjw-L6+td%!Ffx0@`*4MSZBH z9L<Xy!noX?O;~&O{}F7XS`=w}R^p(wQ=E`OW%G#WbW#S{0xvIfyAIS)1KxTr(CBtp zJi2F&{;SG9fm019XaH2cJ45}0F<nJj8wMQ1J)q8zd+|&Nk=ginDh0kSO$I4NI^ZC1 z8au{|;!`3zsz|F+X{LJQCE##XyL`J%*|+x~!EajPRK~>&Y>Pgs(#oiQr$qh4z_FM3 z;J|A#m?Kyu7MXMDfH98M)7-_%KlAWi{7<n;>ciH-hF03#?S)nA!{}6KpTj*52I3t9 zv0EAt#l?^MC4iK+e98em?5$9iI%Ld0#N;7vZAhJJ#3!5?*pyu$NjqQS*6sw&(zoF> zZ4>vA*=h{9(ENgkJ$=#xI;G2&HQcQp7I%|I4zG+Dx-1(#z=q)FI#0Ia)0Qm`w}=rL zV{wLP>89vc(sPT<nev8c+lr!UmhX)-Gw8>Z4H3r{j^3NPvI0p3Rt9)V<K=%-a&LV@ z%ks)aFUw?&Bab}}`3R*l_HA;pGIMs(-Tog>JSENfyw5ro>`L)Olo9?fF6(qJAA-ph z;jaSTY49pWqNUWrsBf<xlk~oBJJAE2-iw~+ZRq9VRgwu@eS$ZY2WAVg*h_<FCye!P zW?DH2z~bC1WAlFT0AqK{vxO>i5$)yYwr1NhmgS*rvOjp%Ag>2PJ}(n=989&<Rm@|J z;tOz?Qzs&`VpKzCK~p++d}5r1a9k*OXOB+6L{+MtPGW$2lxUk1n(;YyBK0kZj^i2` zux#bvHBQ*Z1iC-fc<E^I(Z5$}W*f3++oqgKqO!dTq;F-S)OT$~BRIR}DoHwpwZu6K zkOz^OoNpJ2zLykGO#XWHd2gPgL>wA(jnmGPc@v=4$%X}cNl2UJhtPdoa|1M_P#=HM z7BJpUAIqkK=qAfBGAZeTQ9=2@_msSo42l*GbQ%av`Ye#(VjjWLCyx1m*VpxW9B zBKeJScFO|Ub9yU*9h3|l`|-28(`ta3a@PJt_?5?CqK09h3cd^iFA#cnkLq(}rjt@F zEEp}`%^3pQ%juWCEy-cxAw?PD5Aj3M^R!FXCEaQ{T&*MWEX@7Eh-8|+y4$@Ug2FL{ zj1GI4o8L<ezjVWM&ZG+-H!QPafpeFe{~|3Aku{^mD#FQqn#ArK=Jkb0pEd+i$8}k+ zuJ$}K*_Ss6XpX**-Gh?L+)_^o{u{tE)i-Li9qf0L3b#hVH|z??@R`kFceK?Zg-iRz zLC;sB4+A=eqGpieVsQPGI+dtGpXJ_=@eof$inF}-jtZe!Z@u1ek+2z-e}U^|uAd_` zWac;zY)tcyJt>LXTQ7JPBr74?WWcP{BkV_E>$MZ+Wq*RS6nRJRus7ECX29WevFC>( zmRu0u^dZRS=>u9C6xZqOtqxB(;U;-FK1P%l>&TW7g<;6NlOD%j$FrX*>D=!TvNS;A znOEkz%HMDr*lY#_x2$Z(vTvrw$foAd-)M%;rY!Ho@^UQenlWrxPhN&J!gR@|c>fAp z8`4ay>PUzRm~}sjESfM=$$wTtgWtPqtr|Ju>h~9jbj5AJYB$C1xlFZp=i#>JZG$B0 zAAj6>MpAF%ZJ&pCqNUXIiW61LX5ea6@NzPJxYEu{9?J{YgB$M4jpo~nS0WoTzD4y& zALSemNdvrFx{PD}X}0g*a;E0K7uu?GJr+OIZ)*Zn$Ljp5v#C-TYxx7!w#r_^^$zSJ zGU_NA8U@O+<!Gue3CDVN&F#@2;|S^8Nkp!Eb4@cEs~<GMUxARsk74xw?8m>@d;iy9 zO3^#a@GBHD`y5Jq))Hn&K+AZ*;+xBGusy`eL#9LH-k0XCP1SY(#B5NmF;wf&{$x+Z zUPnIN&S<b!9bC^CgLA$DUyL&D9dc?v4)-(T8I7n_3VE{_vU(nb)jS~bvDWJy*sClB z2x&>`rBEc<>psRWaEvifGzW8S@6efF+OC;xHp&j{K1gvzma=1)Y<jzVS@;9AB-CH` zb)w8kbb&Y*e(t=w1SjZ~uC!S&YJ{yxbo6fSH&p*p6tnL|;sE5N8992O&6=b6Y{=qZ zbG<LVh(B0+J!kDs%=^H}m=ge}`m+4}hVA?MwCJo#<VAv0<NP**Gp|6$u~KTj{A*dl zmm*Qo`yy|c>4xK$^i((YV_EcV>44`6#MfgpHRo6q{oB!Jv|=LgoFkfT$u&p8WtXu9 zbkx7laoKj04~bV{O@shqj%KE|T6v*!x|-5Uf3f5!b69l+`~(dQ#$f#(gzKG{pafnu zW9#~oX*^s>KsyQsOsvF07lQ_EJHK?k@F4&&?d&nAfN8s%GL}DL4+|4}ncb%QwbgFA zOm4c2ZaNKNI_-8UZC2W1_4kWo|Eg)1T}tAytQb#Dsc|(LSqS=&488a4*WIBqU84m2 zx~TofR-Tq^k*}RIMSD~dAhF|=x!D=3wS_4(B?bn9);CXgxvBR7+Cj?+wU*!&jz09j zxpAl5MU@_DF_;765n81WPC1|fgqp;9<gs(fS;aGs`$KvYni0#}$0v=BaW>#H592E% zXTJ~^XhT~TAako34d9?TsqH$|1H$-Gx)Cs&+G+{~SaSyk3~3r*rv!~7pNVc1kFJWY zwEQUzkf}q}#w4EwS#J25Pf5U%T#mi*C>&V1bmy2OfYW;|3F|zFVra<*2VdKDDH6Kq zs+H(br5}2YVWHAe#od<_=q}P^S)fn$Ptq{iF3dYT{ky-wVLhqtY~$D8v?asB54$YU zWq12cS@QLGDph|jU3r~}<kbed@A_jYQ3a$52^PpYMTx*Zo<NnLpBOa$Oj(FAZQ2Zs zl(ICO!JB>1+;<p582$$W1HP0^q-*72D7My7JjsXnj5?F}&jD?|(Y=9mN41kyuu0$H zyA7$T8)x0H3y0!W!I`s+jWK6a@&cwMy7Mxo>|cclzCW1Z?YBuY1g4wkg_S~TuD7C2 z85N}>o)O#eM19C3DavXH1^5}{0iA^`eYyRCb+c|)_rC|_+&gzhmMu3>*#OH4I$dS} z{sX+iL7(2|+AP;B>1=&L|05XF=&6i_C{r}*(mG3zSav$<AekcmkjR`+3=&&=W4#@Q zEjd~<ix{g0nPq?F5a9D*vY|)Y)8PAWMdmn-e|#iw!vu&%dQfCI)larV>|s)%edX9K z1eIYfno>B30Y?}G$rx4|_a5LjADDvc_o%Fox%z3b=dGQ0<u?4qjeg_fB}z*SDy7#% zn-4%`HeL~G|LxnlUHL%G-$jb{{fXpxfpe_Cm#KQodtUIE{NnZfA2UQ_HMJ9L|CYo+ ziPLmF9nsCN#0-U5<|<360xJ0&#e<Q7Un*T8>-{;;s1hh@ZCNxnL6y59CgX5;4g(_b z#ziPnhSAy^Y<>A4kC|aS%N_GNW$s^y$k4UYu$<nsJ=FE5!TszloWb^{y}69MPE)N> z8%a*P*6!-fH0n6n+9{U!y&(+jj`>8Ns=M->j=_Aap-@PH`&S@_!{w-DRkr~&PTopi ztWBSX5OdOBaGW!vsCsdrh4-@F&AUS5_QqJ3RT$9+G20MD7|{*}B$2NbRBY_~DKm2g zGbz`-)(z{%FSR+kS+0P0KLJ(&?b)f5#@)Q!<CE~=q#CqbSmq!+<YGlJ%rmTg{GsYz zg~v3^3aylKtiHx}*yoYxD>64`IM3vOY)!7(;Uu(Jwn?jR#`A;NkTpUbZlve!-_``) z>|Yo5KhE<wHYdlcaJ`La1Vc@Zk*$h)55DW;QTM<r!r$@ckLu8Cii)oUJF820PIiMG zQ&ysl!xfOru}hMk39QL-U1n>*MgRCDYcWYAX$ofyPC4Eo5=I%V6*fmYgHZ9o71V+R zG3vghj|EA08g6YOGNJxF@U$?^g<>Inhs=RVs{66&T0!mrj7IKI7t*Ft;|*{${Arlx zo>5@fcg*jJm*o1<GWx#WlTU2pavLQdnCXU(Ed|-sbzE_>5HnH<L7;cFJ0nn-&Be{n zJmB_UbD3WO=Yg2nl=(LHF&*{wl!9-dFJyg7;?u>I6Rob7Cy}SPCHqkyFjGW%P*k6@ zC?UDlbE*%)Y=RB0zjRJ(P{5FleKZmq3fbgPzTZ7SOHY0T>;bf4^(G(5JKM=-nT=OA z29*M0BV-vRGar4k<QjE_dZj!5sM)qgnV81zC*tsgslgn9Hgb)b7tgtz(CbymvHX$8 zR-&GDuw%DKC!*MnJuV`T8%a$hHY<niie$8YFpL}PRba^F7zZhp-1wv<#ialvp@pi0 zz;<Cn(_mdh6pDJv2c*g4%=~fxx*U%6V${i1mHou0{0k`9%&8nlfvZ02mYe-cDx{EP z`tI5Wx&rRdizhB|I-fDy%XN?L?MC3&h1K<J=T__JDHObTg#^rdjkr|%PS^j3ZM_fL zdng<$rRM~a7C?Ct7-3nbP)RqbUH{k7`xet$te-xvx0fRbIux~s8wxunKv<Iwd>v6e zJ(j91N9)8xAlqD#c~vJ@ESh6s*O1HOPi{+9nY~rBw=}tYQ*646GfmLlV96k2nm{79 z>FVhjH5Ei3CXE)8yFr3es3u;T781SJxA=hY0RO{qSo5%RO8;Lb^l0W-rF}v%iP=Iz zv-cI1GA&Icj~U!`(dGS~!&K-$&9j)>qdY#vW^;^VD>{jqgeBJdFl`34My*=s_=nay zHBDY5?dYOQ#jizO`-sR_uyxx+IWULtT~U1dQSjmk0ijT9eP-9sb|tIj;nJXk+_&+G zv-2^Ke~J^351amE(Yy}H92XLt5b)f=4CIvxHz`JAk({+?Ew0&MS=#P_M>HgD%!%L* z{Wy=_cj54}ix7!CA+Ef1Gy{58_Aoxk<4|b;5>IPEJ#wavp(Ol$19zo(gKDVXug2hw zAT9@b5|yCM?Dut^vU~P#u0S4Do6v?WDFy`x-@}3xWbM^!<nRl%4oeLqL(Z~>-_27n zx?avlQS(eWjX`-(AP$XP^&gR_cyJ5P#|*5;bvtZD+6Kg-Y~l)2iT>mGCYYVrh>QI% z!(*xiE~nG_opl4pF&fcj<zx)fd5Y#$T73T-s<uHX1<<uozPEzIx&3Qv7=v12Cz>3| z5?;7%Lpcm(;SV#thXCWK+NXc;e36vc#h_S9#!QV(3did1y^Sh^^CDFN(fmV&nNRvh zh5dM@usSeIS_SmxP2thPq_AiwzhPhdAsL@P7ki|Et+-E9;e`(8`evI^G!|o4I^6t} z4K)k3OXUGWRuV2%Gbf21m-zE^Vj!pQ2Z{&4z%ky#^CLbNPnNuZn*%pDtbWR#&2hvg z&-@>>B5H_AAgDWv-BjGXlA9f#t|({A=Ps##t*c2mtYEvMJ4Vm}q5uN2@x+hr&pbD* zJxQuTBIETXnRgCcwg%!0w!$eOXlWBjepEd+<HjmBh`K79sHpidAoJXNX48;kEZ#jA z)7@_-+RvF^`;bgunOoXD=I6F@ch1=LpC&Ce6}I2+E2Z#e=6=0^KPRTXZxahQKR3ER z?^+FJMo*;%^hDRo&8k?HHsE>hMwjjX;5Z8g4^%R^Z++d)MyWPf^f<cMM&#Rp#3sRA z{KflM!-|g}cA(lT?ww7GqG|dTgK;<X4p&}hdq=l9Vhoeo*_^V&X=+BySG=1A(<*!% z*8I!){P+vKD1Av6j1%HUN4K;!t69IwXO>nIjMrSBO>G1Zy?3CN29-lCf-A+u`*1R! z{#8nx*yg539Ipv#4{`0UR{P5eRB|P19Hr+g-+pbFrba*jHE^OhID?_%#!JhVRy!|I z-Am-AOnSLlzgJ}R2f@hb9e6}+CK&KF(cI;xn<@6kb0qC4dK6%y8~flM<2SP5;{*g_ zpUAFFmBx7I;feLwUI>|R&Qpy;SsR<|&U*i8bdennF4EpEID_jUUOE^(i!n-sp;Y8% zu9t9^Jg?Pwc}flc?HT$s8c;PYF;q=zE;RGllE=+#!pZfM?@vl7@<HKepe!&u14Jlg z$P9~S^7usmVi?g<+3B)(*g@_ocnzWr8$#(PMes&ioQI#{E>M7!|0C6vRWV6YPKz>2 za<lK4Y%XtzFstt(>B%N;X)D%*t@a^V(-zyYJ3`A>hVJN2S7qF_VR+MT;oE_ItF>%G z@oksihm|RUXdiEkCr>&@X<l+G6(f4aNeZ?i=7*>kMeCAcvNe#AXHa+#!F&r^+fV7? z6P6-OG0}eU&|aJ3^$4J@MX1LYWuuOE@6REc)xqFJ_~Ne94lk3cuN?7-LJ4?ggmGOX zQi~w;Q1;>y&{*)$b{#fBXRH#Wi!Vh7*!*U5y1QF`@ZeZ3k&6ZNzL+xmkhlE2>@V<0 zr4@gcX+$>zlV&<X1oq=TP42MB70iJ?=?HxMHmB+L<Mr{Dxfk%Z?DzKUlM(Ub_2S&T z0{V~*_^^fO)}MlTx2*U;Ht&Hq`2dn;i>%iz>G#G1@t`J^Iqfx#Pbdftnw{O=b-w7f z>ExBM$&YdnPoyCKH_EA+uLpv8%wme4exY8MdTq>Na)qhH=V$gxQbm&=m#)i8N#s*@ zRu4-yd5+yYN2+!|O@=c7)YXakCA8@<M(AtBW(cPt{Rawd)5zzu=E>xYXKYZMQp1V) z=<2<k?HkmG*RTig!HxSH4v*SEXkQRDe0bG8Gq%SI){Q%8{YdI!p_$K1%D(UiKb>9J z%q(E=#Ei7+R44ej7Appy$ydlEqgpnjNoql6(PCagQGz^fR7z+T<ttv%se2_WVk%~_ z6r<M(R85_ETL=Ol{nE(dd~;dg`8-8Rv^_6bo$Pwri602u_BMrJW~IP_qh8}2oLhag zE_~R0xMIiLQ2qIzx8F;qYY)Ybxj}+RRQiu%dTFRES?uE>3N8*K=Adp!nh>Rj`m(#{ z%W-zf{Bk1(2E`w9xKzUIYc9dFo?E8S=ST02$KQTW6@z(LpJY-eCyH*yFpw(^si4GQ zyaqvRrPhJYUFfrJrTyARZ-f1k-$7RzPi^b2*j>KnUoLQ^*`_?rQw05njp?M7eY#wM zoD($<2@*0XH2|!Va)Oa9J1s^$=<23Pk_?STjyH~U)Wm&D90K}mz*iZW3&PuoIRLUY z!W3MitUD&`FjxdL8X0YK^6a)6DWAX?cV5C=J7wh!nXqT9y?njllU14TGtBBI-tJeC zy`kaaTtFK5d+f0&us>H8$u9Crqhx%Y4{aj2I#6|qJv}Hr21L=j8GJ+TL-Av!5H|=D zHy?rt-j|!I&EY{L@pouju+zPNu2<~-49~=EO(Gwh+Q2m#wUyTCx}JyJDr&nmu(o1X z(tIT7L~f4@qW6m!bnZvm4W(?A-Wo8M?!W$0{7lqBbu))bIA9!X7;-H*$o6I%{-0`U zYuQrB_QVmgYRpYj-8wl?naRX-Brs^Ab(lK!4K#Nixam1|2x#;+RhE|qW<gb7fIAW) zF$G3``@7A-bn7R9AxgD19U}LqqyK!kli`5*({Q^7$kND*vq_jI^(8dWotU_JTM3w} zuBpCywL9`|b~PE>rW(gcXHPf_Tb#5O-rX~p#-uKuH|*+@HIg$GcYgaB4T*STCfV$W zczAfRlkbJBc(cjDz^b<|x7fXeOGCDtw_VnqjMrfj6Vh;vPNq_f3Z}d~VQlb%Ocayj zyeZ5}T$}s4{HCy$o2PPg!{*%Cz=UO-XBtrrR}u`HXfe_<#b-@DifHI9S+uC34Wi35 zyWD(u2W!(7@lndrceV+`sdAi@IF=%B0)oKFWW`GogF*zmeViC&xh55*1>nYB+lNWJ zqNfqk`4;EqmrIVkymTT=WBj)0)-L@fVO<>`K|cAl?&H*oXusViqv&0yh0Ug8qAVCW zP3ql*47wK@2CnUmWG6n}fePRKsH$tsgzz##pK2I0fY%fy`)T0#2<>@zm(YC&ro6hZ z0s@02??ndq#hqdfoCPx|N3nnWJ@ovQ+*^inxJGqQzHpZ{UEQ{Mn#6BsKXWT8Hk~I0 ze{LR+1p9TlKe0K@dnogd|6Ss__<_{e_E#O_Tr<_&*qp*SsJRBZZ`t1G*cfH(kv#l* zt}SUd>-QWtki2oZc#%VvdPQGExq~0C?nqXOOPH~c;uE+V@(tFn+;1&WT<X03&Q~e7 z-mJ2x{UrmH!?I_VDxdPrR${8Wmzb&%u;kURzd4yZk}AR7)T-UyY=#}&#;+E3$~T3` z)CK)Ib%~xK=qhTd01eRoM#{G~Loz&bS&i9-jLkP<IA7nfVBUKi&-t+`s6om;MR_*} zrG4>ht*@>-EBoc1b~ur(F2}4R@$e>djf6pum7+E{Y;A*Btdg0fAGJ}Sl7+Lh<me<; z;wwj%1vW)eokRadUvRqmCmr9K;MNzpx$~$&F+>%hna+x%EC){>bhHYr^_XgwQ;<W6 zSG;8Nj`dx#ZIOA6=-v94%Qk!@X58gOP#FKleM`D^vci$%{tJzesLNEv=$PF+)esCj z+ru&&`wrR|hHVvviR=54x<-@UrrFu89&+h{?#tP6?&U)lD0wMs0gwIC-?e}Nl><Ug zpNh6)874X=m$ly1XX3qi@PM59f>b2qiyy~gp8C4Z^1+b{^z{>UHq0ejoezR}2<ZdA z?}T=~A7b>llcp&-)*pu8j*%#kD}{mZ&hPOq(fLu*O|@1$h^H*j?u31O$aH<y<b1ca zJ6gOZCq0NVg-w>8C<kPVPIQuWj$hGp?k(#1YXJ2P4_+xGUP(z^(U?NdH%*B5H+31E z8fKpQG(LcTrfS$c;HzMO62L#)eiZi-wYEzPC1I*xzBYHeE2h6QKcohmis7;TMBi~( z?!%y^gAo%l_C=gq6YhsiZp;==tFyd^X==#_{b-c;O!!W|wuMg+<rH|Ylxzh5#aS1O zo@5iUQt2%6x|-(meNiD*pmN3A2OA;Sx<%yx%5lPOQ$oh@)87Y?<l+{39j)3&v2c0G z)~h+(9)Wa#4515`6kw>&>=R)b?&c1J5zGeG9r%LtG)Z}Ba}fWB*E|z%;-Wqgst-0h zVQ_3HxdiLCj9<ZkG-hk!w{DI+j%2{FW?FUegZC5sH0|oG@aZF_0ZumT1Np3}*I{AB z3Eaa<Tkrv%A7J7-{~Gk_Os%cx4$wj5SQirCV`0aggK;}fD_?$?m}CA^!(!azwZj6M z)`5m*N#eG&$jk}E1M^aET`l(2jWTbV487}Jmp8XkH^f)Io9%uO*;gh-`G_@4sOGAT zr;pg5o^TSFq(U4H=tAp5rTJIpTn~>(DGTa7r*WhHqCk7>NFbNYD-S*IU#9mB5J&u@ zIjIf<pY}wo@&%q)ovkwVpRz`Nod(LPwxeQ{j&k}3&=qtQNY741!bpD#P>Z~MI$k8u za`XdO`L-zoG(;MBi<ca_jTyGa%o2x)(3C?Q8e1Xr&^h{C?BreOLOAMq>iZ8j*cd`v z`!RCMMk~Zb)~7+2!os_pOll<cm>mnX(B)rFc{&6enSXnkZ(WbSug>3}>hJ5%@C%;) znk*DWHa;E65k3Tuk-*;?a~TJSeQrrwX`+a!L41hmCWKK*6fIUhK2NTOMsQB)nP`%8 zkE55~-EvogJD*GP1FDA%DARcrh}bLM0-CeDFR7>S!SgX^`^l~oKSyVyBAwn&>Ao}c z;5(}8z)EWht{A1PMi`38FEq5<s$uA@{*~#0;eU7i7f~p5A-NOlDa1R4mh*iO?R$N$ z_PHSa#UjXe2&ry<87>!b8%xqHvuyCg&F-;%Wf&}ts*1ZD0@Vt8K|eY}2qtOBy6Azg z{)w>M<rNU6wa%?bhs?@HE^ZQ)szh~qBa${`9-`ZAnx{|RWD}h;OIk-ni2)Z&bqox0 zVaXstCAKfl`81sy%-K!t&i3~`|0@J#Bh-uq?ib*WM*{%B`!9W!`v129yh-iMW`hmk z`=%73miHWOi_6bnJ5M_6AK(dJR)HoJkbjB@3u44#LXu(67hHK^k<>U`1}wrv#zAJ} zp`Fx(gjn{epzxcCf|z{s4)VI)HKjBtsTG6x0*+WPtvQEeQc7azz>%|uZ_8)Ak2?1h z()bfmF}s~KG!(<z0F*R?NRmX<AW>gWk{R_``*NO1Re5A-yTL8Igs%+E??5ta_CdY3 zRw+2)gpwr1>=H%Lf3I0y1P+C_cV|aSULR?n-=+?vY?DzcCsO41<Q$K6ccc275uGM< z!aft4k)N|2wM7zz;Ke@Ihv-^?MCm0RtYlL!$soo^?nS_(=YuNDOQ!kF_s3J=5%O?a zOv>~(>3Zd4P@wJkO&TlAGgAIFd1+WdDw&B$ipNR|3eqB`#|<>`)goIK=ZTQwjdA3H zA+*K<SBehiJTngZ(n$?=e~Mfmju4%w+b+gpYO%D>Tmoo)Kw)ZSVp@km{NXs(tik?$ zhvb$Yxb4!!(ms)ZEkI!fl$pH3v)p3L2M#{%798p!F+sGb#&C?S!BGBH>uC)7^8}mP z;-oVYK}tjiO6Yn1k;2de!4mQUsEhK0e(ze*LgPYm%eQv1nCGc%B@urerqnl{#pZwu zsqEtcw{#cLEW8)Le!)DZW01Kkj(hw8Lt%><ZEPaxHq8gOtPpsoI>5;oZ1$c}Ab4q4 zebpb9D~4f19p_PWS-d`&P*)r6=#~U4Kpcg{EY~)CeNmCzjLmM6HE{a|Qfm!Z3w&I5 zz~Y*LU^D`GF|a{bM?%%PoHi=(5AAi>A}4GcdeA)5UvY)A{I(FaPP-%DCz($tW-PTe zm8_=k=&>td4kkp^Z&6{YX^1Cr2dS$Vs-a9-fwIFm4(K$k$Y`n;EQJ*B-)pE5tW@Xo z)CN{<RyE%5Ozln3dv#Uiph704I<A?Q_f;{O3H}o5mA0D0+7~7u^&CbKN?~Qv6XR)! z?yr<P)Qx^`X6Es#0&}Sn^{{gx(=!CJ7eEY@!f2WKDU}a@?*k#+n5{C{wx<Ds+szft z1cz*7RcaqZhbJ`9YI9ZURSut!SzKqDLDZ?n$XlS$JIQ`EIMrQ!K<us}jPYW!O8*E? zF#ua69=|a;$$qrfwuAQN9pq^vld#Y=``gXpFPG0u84tMu-MhC?OmnT;xy<757%>d( zN5O_w(TWonQmiXK%Whb!tZILt5x^0?hUtL`*u*aiBc9!s#p_Q2cq*Y?J4>)ewcA*~ zlS%uM%`SV@)M}7iVo>BFUPqO6U^zzHKUX2MX|UMbP&l%G)3;d}%sn+f*lVklNz98! z=&sTu@}AjaTtlyhf(zcTv*?G|OdDbYR?|Pw<+5_-5C<Gt;TR?PGpgpqJ*NJEs<bBI z)A&28WwFD3`&BkZH57($-emHig-4w%gxu(FOXjX8bikqYjX#)^?(O)MvA7s;F`*fL zQ4@4hwnH;krz0?oYgacn&gyShg|)hAHiV4_y1_EyieB#W{9(|L**F_m)4t7ayKiB@ z-Oq+t=t++N!5jw9opZQ^#BtI4zFBnOF2cqkng(73g-2A5Gb7+Ap&b~EqX&U_k9_pU z^FFkZbYieOcLmf4!dCNbO-rxKCJhb0x*4C=Vt-<inHrd?k+V%}ooPpH@p(cQ8@Z`V z>1eoevijR7?JV^-G+^-ny{9?&g9oQ2hflP*p|lYF4OJdC;hg>Le-m4D>QPs}@ZIq5 z-|2s$lXVSkt*nd<oy=`*{*&xxo~$%A{TuDp`G7)6BTked7$a7t^psdZq2Nh&6hJWH ze^jY#sJJ1tchy1iNsKQ)4(T$|(9jS$K=*l;L6{^!khJdi5nXH0mV)F++E`Cn<}@d^ z8KRE?Qap#++KhINBcyD3u-jlk_}X85C>oB~8NMlei;9XBFM2zbzbBIkk<dJxv@j97 zj_iopM=MOr-}`)PFIFSi49^NEStC#mPuwsYK}?Cau07&{0^=}N6$~V~Ev6cm)wGgS z=M6%|ZM6?@dbaTIkiFc}@U<Eh4pUExY!5?IGl>|-(DgQdkBZZ&vB<DvOJsLuXgg6- z4;YWfd9m&%<4(0-gTbEa4m>>RijzyycCM{@16QK#FhHVO9=9P11LtjHw>?*CIH4~h zBA2+rElZLliIb~d;MHAW=Z;N%hRKOn4sl@N+g-bm!ejh_>=jYY;7kYL1j!th>l%@d z)j~u_@!YTI87tJ3bxia6l|Oi5EPeZjL;J!b!Ix84BBnDEATXInU-xCuI8e5e!A-#M zE?rZE^BSxhJ>+~viJc}1r$tzn1g|MYcDa4P3H$u34Ya$Ekg(Dmnv*$_X14pjAPr?U z*knK6-8BvMyc!Q`9$qTEqrk_>(fI*z+ra$dWVR#P;2&$JTk;%DbLS`U=l>#z$WPbr z=ze!h@GpV&UkD;y6K5MkCtF*q|GQu0`(y_HB6K~VBGYVa>G9@;ixi^MdYBy)uOXz! zsA{26$gzHT<_}(b%#wlY9Zz}Mf{_(FW4O*zM$c-3@1g1seawyORXtO(t=lZBDM&z` zWpfLpff+yN#RNJ7UQZ70>X=cxU}18RVPbHBIkEfT2Gom~flV>(M%vvmz(LmGoi?K4 z;CK38(6;7v=3Dk(*p_pYj9&Gh)`Ev_k=3PPN5d`63vS$p0NOUY6WZzE?uarWRTlTD zb!ZPPNmEtjQBC+}Ec!BuJCB#;1yvL5WfI0~xWNb7LDF1a!y2opHW)E<(?uUqalg8N z1#f&%^q6q|2m~=Y^qv`MqtE?fukhCaDaZR<&I9gy@-LU7*kGAf3=_62p$kaUgk0)@ z?qLnJQ52E4H;-;6&!qnot2EOwdk^@1<)**j)_;NCbj@s?9DkqxgH<MMS!~iF1mAq2 zfWHuA?!E&)7s&m##mPx@xE7<VVQp9#M`K3UUv8)_SFZ&H$ypeE7CKIQF}i54>@YiH zWRb#9&4$}OSQ<pXb@olNxLaJbS8wmqwS{#``H7Dcf+7_fMRp9C3KO~KQH?^&N0&hp zqPg%lct^9c%R@xU-4N)r;A7?j16RtA0CE`xX&orAZSWwu!w7&F$2n_Y5R#rj;<)p$ zoF)jE9fNapkTRrzjD!QpWa+h4Rq(ro<fJ5zO+iQQd$TdOy2@k!ncpw(17A4_m9@02 zuAz_we*Z&hQk1=RQgVo9yHhpHz1~a>G*g%q5R3Z4<wHpeUm9G!F6@@QOk!<kY83V6 z@N&|+%`J-_ak;>erM*U##|!12^2r5Vxt)oTi?(I|lIhX4r8+8lHt1NSO~jj2MEi%v zC{s@u+DrKm{)Oo0BKZ5r&9Dl3U~OB^vbJOT(7o@6bR8r?z_zmo#oIX}mfFhvU^48- zjH>6+vQggM-Ta64e=cFjb4!l)FBQ9i4FG`qUrJVU+yAWIk-D|*1`ERXwJtvq-Z?-- zs5kShzIdYEaD<y!7Fe1-M5EA%hLOVGa`x+*9UR?9njz}?=GA~swfQ@zm)q+$P7}hR zm{Y-&ThfF}27KnW-B1AEElbR}i6BMyv?<^KUg+n7D_ciVM)4JL$J|DMx7cu?aQ71A zK69x0P?#LrdzhiyePqybaJX1eY_As^QN|yWPpg{%VX3zbXmP^kk~zj|yy*TXentvn z?jZB~$x+Wr8}&`NsDdRkCueWRyIXri9iGiit)1YP2Hu)dqCF3$2!#ri-!UYQFgY{F zD+*BTF1|A*gh0A;DgBXsjHuVf1<VIcL-1NhnCm0^?jhHKum`eCQ|a;ne(csp4C#Jr z(6x*BbUcvgq^3HGY?-<WL&Lb1FW<V1<a~8JX_aZ#Vo6X#`*j9q$5B~70?PtLDjlg1 zh18Kbyh#pz!5$fC*!4;p)xB_PhD@7K(@|goGUC&Qh)0~meZ?WeoFays=l5Gf;~|IK zhBG7LJV0fF38jYw(&5_-L!&WgRbi<43GrgLVhmHGDSb<?e;ttI;WP`GjyUpfg=~?X zRYI*tbEFd(Xe1z4k3#q|DzD-~gygH>1vv>dA)1X*^iEu)tym(=k?jE2Y%ycg+R84q zCnS~vy1$^N=AC-wsU2|v1E!&py-bpK0q>UL06&&vT!hTR?uZ9wA*97y=>ql&h@TV8 zNKp~A>wp~1!XX{Mkmwy9o1jK_(%ezY+X2jOi4@`i6ExFX&|PP@l@}5YQ9WhH3?{_D ztN``Li|}bz`GCPCk*60Gs)kJwi1H9XT<j=GE`Wo`mx6a!i@U(*tDU|xXBGnNVOPqP zP3QBd<8a#!gYhvu%lb0--KE&%UgLyY3oPPR<kA`>ofvVWrLXd8+UuC{6?H>U2i_EQ zt*odB%d59uAqNqlzh#i~&az6*{P||dq?g-BJi(xP$*ILctN>-OPmo|O(;Os{YF*>$ z!i)_+cQHzE*Mt?Ms_3`a_2L==ya4cJzf#{!fSJ%bkai{bf!}g1p+bABkFMl5fCc)i zG_W=Q4qkvZ^RC1Sf)GTBpgueCEZg1EZE}C}BeUD|D)<A}$E&&la?_FQ%ylKh{xg2^ zG;zP0c*kP#3$PH^?Y}nVp3F_o;%?m8dNeIM?{*ykVyATBuDuk3Vd2+jRsADY)|ueM z`{egxpcc=LTi(3e1FDS--Eg!66p6dCvzRH0{EvIP)4+FY4Nu>+%klKu%Et@qKv-a5 zcF*V=CIBa77Kgv5R6Tz{OZuv;j)@V5>Xahh<xA1KE8g;7l?wJrLOD(lbTqTTLJ+cq zj9sQoUbvAtmw!D7s1fZUV=@qy1r=$QuoSu3RW=QoxZJ<NZn=v4r#5~#r-K2uX3()R z7zkyxCaJ1zy?hsoT^42#HyXAi9$spMKK)0vYLa9Gpj4=pKKljAr<7${!<QOc<&32& zh$@~!4{RV^Dd_&2^1{aT4}tAJx^2+<FBRBn7<9dq#v|4yq=@N|M#!?#yql>YY{61n zRF!tm!!Dd;%x_4_iY#fww#qw<YUWa9ksMSet5kc7VhfthBUe2J@q9687d>i4-05qk z6~_kC$SB%R`be76CEh0#!CtVVCof;Pnr{Kh^;(40vuoJyVu9_gr2aBce6N|nD#D$S z_IBboI=$!Bhz?8~!IVl&2W(gECMiMTX-0XBjU&_;MtOK9+d{yMyZGVI_u=qA3U*e! z9EX^=f@Q<_em4ats*Y~`m6gB~Jh##d+}vb_mmm^oE;BbtJB_(i(!k(AzU=QAPBzTd zw9g#1>6Wm5Z7O_WJjvzvB&E24CvZClR0m~-VJ3wzR58fsExeUw$W<FHv{)=2E1fP} zc9VH)m7h)Wj6?DZiqk(gr^V7|aFI3tE^)`UaW8nC=MU7LbtD}PxT+CK4%7!np2kAv z!6LjnMxLqd7C7M@Y#pyhn_b6Z=lMN%x0{1$8huW1+lR$zl&t)?Br=TI_M7&LdA6TK zVr@4*d%!2BZU=1PTY6N#i{~O52o%l%Pnz2H6?n)b?bChr>?v<GwuDNRI4L-X+OurN zF>s-t&;s_%H_*(R6(N8w3GiCp&k6wPqn*>mn+HTbG{jZtt1S6=&>9H&InkQ75%ZSm z|93g(aF<_^@p~Yh{XT#H%fV#fXlwJIqscm^4z8aLM(D*Sc%&KX4FHUY2mxAi4WU9| zr3%JcVTixXS5_<yA@i!k^`x%@v%kF-$NyyL4ZyR5f~^4rH1&ibJ^<KIOU7u7(Wgdu zZUMk(?rG6QL@Xc{zY5Wa#_~W^UT!6J#k4}NA<%3!V41@|VA!O9398-fOR%WG@}1P7 zMGBn_6OpaNM7RA~2BYGiae4UKv#Q7cDpOgmw;>aM^-%rRP5;d!vNmG=kF4Mrdieg| zkN{Vop=c&~7IdU+EoTQ?KOM>lJ}8lk8uVEWX>i*P*PX7HBKz|{kHG9^*y4qpFm*W~ z1yK-yE*neww_@2``4On3al;Xk1p=27Bn=BIrpqQS!h>i@55TZy>rg3~bJ@U#mw}x= zq+~~*K%$*L#?{@3h|skFbFRfU{4dkpz~W%PBjDXaotICha{gxlai8!?ng6Rt>;Iw0 z(M<pU>d}M?phFP;>4*^j^cPYG2&d_zPRoaZucFou0jSo|DB$|6^?jL4pPq_ZfUnj~ z60jCF=GjikS|P6c=Qj_Q?oSz|<w8nr4`l~%0U{?EIvm9(c#!4*y3p{N#iG6*_q^lz z>}cT6DFSM?5`~yZZosDP6^W8S^cHi{l>GNfrOfR0!bv(tfS(Ph*|-dEv9f`wV>~kD z{gg4Z;bcSQf5oe+uf^Oo{;G5P{iq=LFT3zp9RoAVe*`Yc@&89)=R*XyK*66dyWL&n zN5EmGVwAF!X7d#xudXHxpuxdHds{}%#O&<hb6h(*fu_G6h1C6Xd9xAYWCjTw;MIld z&5_Q|doYO%K)eypogkqsc@h>b#4=o5q$pt3Xw<H+@mltsfC52CVq%xARxGzbM@a{V zGg-eh{a#)&Yl%4rW;%-?UGP_$Q#pq0G?Q6sK+H~$g-uDx=+0T0^}t1?SVQyuzV7;X zBwo|g?`s4GLHSh&0q`F>d|1n4yZQg0W@dVZ|D!oh8tONTGWdmih>F^*k2j8nD*%^| zsK|OfrbJv!q&8<eO`=KIj@*Qm_J)3^bN(ExjJb@Wci2p_mNiJj9#gpOaN57}&7mRV z^peOSVgce)u@jM;t^T(_n4X#3o3bn;GXquWpA)q&3P-{a3?5t^gjAQ=qc7qf>8oKM zru&7#DDk8cir|me8}U)TIp^~WHQ$c$Ihhvs-<KTeK}qk54w|7#W8-wJt4<<$%_kDS z%c?m4r|O2$Kw{C~@akfW|Hn%IpFFhxsLoE;wmoP?4Y~QE1bH+8vjdOpv+;wSEnpH} z(~>L-DY`TQhvpAYj&enrs7-1-tGUVV7L$<3EVnIq!Jol*e|0Y6(i$hN@!`gkjorRG z)6%h!<<5=tIwLMMbl31mqUCn-(DwM$0v?ShR_#7aEtrIh1cg#S8Nrt)+tl$KQQSgk zqo1LOa9vHAPhrjV#T=xN3ftGhJV?&_&VZRGeN9>kBuZl}023eCA1`|y=7Pq9HITFk zJ)vXz0=hsIFRKz9Z*E58!ZM1wUAEwgF|@%#(L$0W8YdICBPe6=j+^|wkY9Bo(|;UB zVjwB8idB;`JXjh`Dj9tN4I<C84D7da8xFG{mJ<T&PY8pEVAZOiPZYWYLn9<Vn_j*0 ziP6JH=mDkFWz8Z06RbR!tPzZnUqwWPu6Aqh(@ye4@EIm^iHD$zB}f&Y)Z}ZI2O}%k zs~TDkV<1!=g%@Z{2>}TUX?B<*M6%a6ANDSatCC)AJmw?_h`WKE+bdYDAYk9F*jOcL zQHZzE6MCujERqU;M(j^K4^hn3Aj-=pNKs)TC|4po2NG4s{-#@eAX{Lj>X1k5aDJ4q zJ2YIB^$-=DK0y6v4K8w8l0Ayqf`Gd3=TgqWtnUbs3{`NjP?HKr(n|t5<y075>0R#j zuLydK0~e4T$e-^bjP7W@=`YRdt=oC_K3Pt})>7Epfy71lnvm?+Y8C2g%pen%ztN#^ z-WNpgp5U4X$dQdv=`BW-lfiInhs0gTZj}@PbhHULgb-5^qB0GZ@_V%6F%7C8K0hZz zzC02|#ma94?Qg&ZUF1_nGZaRfJa*`yCWJySH37u7bZF7pWGx4mqWgB89JX#$T0j2p zRn;HWd5;oFtkU-=ao_seL7Ytz$GQz|`{i7JrsdKK9xzp*(P;I2J7{o-f;SN{(}-j! z>GIvXEX7k+<JHB5(5P|}*xdB(77vCUU<k~kNw>wj=7CiiD0#wTi=nEMZFiy-auovo zRkPdyd7JA`+$;GW6DE8ujJ~(=u(Y@;dBUCbF}rk8_pd6hvK2?4s)jbNxnipF8IVJ* zt&$skKF~GzX71uUWvol>;avr6OwEI{c%Ig$r{nodKBrQkKus&^IC;7rk|DDuaaz8M z3FSE2Za}WreMZ6&bEr&l#q~xjuNG$}0KMp<!eM>zP0HH9fpG^iqiP|kKYYlRJX!5^ znN;J_K&tsSVO_O{fB&|+KwxwKxvjdtVi50UgJqV2H24!l@-knr6ZNjX!jh8ThKNOf zSgZl->VHTV8fQ_jQvzX#L<t^U0u&~zGPX|U+5zAn<Ofgh=aoH3_UB&aZ|&^zppxH~ zASRt6|LztAq!pw(mIBM{88WM1uf_6Mq@+^-1Hzcs2D#V@IlxN!*hdk}27g~O{BT%3 z4$RCA%JKIgL=MTR%Dz4=HcSmD#nK$9+EhxP!L2Fu^Q{bnc`JF_+fk|RQ_AXdY90Ac zHUtiNa+poK{v6+ffhCF-tdP@0znmkfMX@Oc;}Ot#sF6_GdN5d1<!%Gul>7fj*gJ)1 z+BI#1v5k&x+w2$}8y(xWZKu<*Z95&?wrzB*xu56#W{!D#=IejsI<k?Sd#zPCtLm(( zI(F<?SCl+0&@NIm+feVy@D5|yKF?9FF1ek{b5zdG+jLj>;oHIf4)_+hhumQM8d+ms z-bT`I+jxS<kEzQpITTeRXZjq=m_;F{>*-=1Ux`DkNwT|*i>sG0D}~zigqPCY6E&bB z-Ma{h`DOB+nelc<7<Y}(6<dj1Itse$Gm}Q7447EVD5q`9eg=`0coK<^yI)vPYH#G{ zoTanCw-B~}9GfvMJfp6%M7|@0H}jPZ=yp7zn;xCE4r5#w!B9|gchY~^s-x!1(j~P1 zMKfkl+rm&+c6&Qk*x{9var}#*x>GKj-<d3SrIv1-LiGNIiXq#wxPT_%;Qg^<vH30Z z_Hyd2>%*<{diybs4=-}5DO7Vw27kKzuCPM7yyo8bf)C9Yf@JnQPQcCFgBS0N^<Dm8 zpl|l!<Xy(qz<m9o+s$S~{HogbQQduXP3JMI{`K`uo5Lp^djhhk+Jtc|hBZ62`#M%Y z`?Y=VWcr?(2bbPm<$EmMrrC2BW%)WQuTm~=CSlBH@#7-r#OFP}XIO%zj~jn_RjOG_ zGJcGx54B8Zc*;-*|Izb765qid6yk*(Ew$Oj5Z$OFK?$+%NTkVbiq47`#(Dq7U7xT| zY0T&3&{$p>18VWRB7w&Ytz@&NeZ}e9e4}T|hA+eD(BLCZ;%$=ml=C!qWbthrM|Q>w z+p7yA)zTgT{CmRIuiw$_KWaoH&TC(?_vZ2SEEfv*AP^#!xTok-;D~)@r;_U%O}ypE zbzgm2lH>&D`#!lBI)?}@?f3LwJhwQ@yq0%DD<;#RJ#FC{tX0vjSY0v31w!Ojnws^u zf7CzPUOc|C;=Rq}=Z~JXgShP9{9Yzl+Ai<(+FZt1Xn(wxR^<(}zkC0*6wYb3&Gb)h zkAyt?_yw+;PWN7P-+c5U|8chuXVBLC`3F3n3hO{n1Awg-fGhqRJa+tdC82qx%6H2( zMl|oo>J~RdnXhArj8+W|`e0wM(+~!Qs3K^2u<HsWZ-1OG$t3_LKqdG@9roNbPPS$r zt(vD*S#|mc*uw<R>oud%XGIlZ$d7a!I;9nvC!lGn>ODq;qo@}aG}+NTKy^Wrg9o%0 zV%u_x+7(xL25<#{tK!G#iTW-mBe}Ybg^6A#W4U^lBoB>kDLaPWk+!zBXI35xm|!vL zCl1`FJj-;v`v_=FRuM37D<(n5TU2YbHez9xCkEMB!Bt$E%E4NVxDay)P}QbrfLx}` z_2$+zp$fbE=gils_Aqz_oH%(v69{<H+=&h45c}fz6x)e25mf^@@o@uHw`r2laUxdu zxl}CWP8Fbg{&I=I4~2&Cx?J@QI}ELLL70wnjW9!~U~Ibg^XJtU0p!OF6~hkb`Hquk z6&0)>@f7B6DObZbBO$MWE#jpW)s)rK9tUcO)uGBl9psm}OC!X%m`u=(=D;tKSgFGe z-r*`R+*iQdi5z<D&9Q?$>&<@;n-CYb(^e&1xy%LlL4{T#?Z#BLD`h+E5dOR?k=C#j z5(a@vV&!`#tbW?bm(SN-a9+M~fzm(menrkeii{o7PW~id?2_0-`pw9Pmj1SCc0|5G zUHogZ#62=9GwXI4=BmPDlTKzJm2cq`=BjsP#GJqRylAuGgH@%YT#M(T5hA{k^Oh%V zHeT+=Mi%udepsq?YGL23a@wpKJAHVajeH(|6MxUz()06n_TJyrl_8+5ht4qI`v+Mg zKSkfi?8vnvN^I-f-hOC0u^#G2=&<j9LqkU_=pA}MctZn7bN@96aB^_|pW%V=UuF9M zQO+a(0A+d+<vvinph3?MnPnn0uE`eXv*mccc&OqbdmK$QL0|@AuDheD>?7JS3v8`8 zDH4e@zb2^i!9*PJ%ADqnlG^dQD5euDL{|7wE5Z9fXD~z)>O(o1D!(K9>EWN#>z;x~ z6=MWjU<d^j8fx-C4iqc$J#E(TU^La>dT&ngXyik7=}1qwRFE8Mp7MpEW%8r)ibnV> zH2lsH1K8F#`x4pQe%6*i(m&+-pDS#}DCU*tWJ0w%@{%?ydK7T7X|H}~ySfeBm^le) zkB+Tl<Wq@0{B|UFL!6!xMl-DY%fiB>>(?lZtK;iq7i}*JcLNN4(@)(B))eq11F<X$ zw7_Rc6prwNgJ4vV?DpcC#1LO*0g}{Z=Zq)xq7>dwq$BJ_#Q(nS9ffP2Z-Cn_16Tpl zzlB`x)=v5cR{zfU{+_U95y*%(c;y|T-L(AzqytwmNE9jd28vS%)@VOAT|lZo&G(*X zZknNX^q9%8#bp6OMV@1;HYfm;UL|DV_j8s%Orx5QApi>8&;DY7kw|=f|0Y5bdgq5k zP;Z%LBSuhcE^3?!IQ>~Na?H95@(+Cz`-vtjur74U=vfbvG+uG1Sv!S9Lv<lXII;2u zvUd2=)0zeY^c|1{+6tZTgOUcdJ`C)(Eq1f}3Yoj>@yxg!$6ek^8?e;`T7^Ig&63R< z63_>ln=W~;d7Yb(_>5CKc5YAjO_jNyMth=)__P8izZuRu+?EFrGS1@dM+ZhbGR=UJ ze*7qzowtuHH$i>1Cu7M$&4>AMX2%Tq-f9e)swJ#;<Vtho<g7cPKD9a9G7XH>%Q+Wx zj)n(GK)J%8^`xgq@~$<?>O|96`ei+^*fb8-Chact`Tw;X_Mc$TQtfx<DBu;s{*nK` zyoQsRgR#C5U=XUVm93$^)j#o(ckF~kKOveJKna3vP<~R67j5S;zUHR$`(Zkw(us-M z!nnV3+lTl!{I24^bd3>UxW-ztk`Ex05{1z>&0?=(y8daZulXhMj&kx%Mtg%0Z6(UX zNne#8`=Y$N2p1!VZh@?YY~Y(6G>+lmnPQH8ApP>d4lMjV3g=O*hiyB>yflQfScYIJ zmL#e%M@5j7Gx)nxKu*fbp-^k+(&L)UMMtqp+PxNMPxFK2gv#ncEatUY?D1#QC(Gq; zV13VQR;>1=y7vdc_df=igWrWeUjweg6EGL{FC`Ow1H*rAAzJA_GYZetkS=m%cb*|J zkNv6$n%P3J)`%#<G>TB^nv5hIK(3Zz>(1$K(ki()iADJpV`CfH=$>+wJYdT%qVDag zL(%X>=ECZF(D0LgQ+Wx$pm}5P4ou5zUG>&cMG`2S(=8(K8NRhH)%mq^QMQI>rK3sB zot@f*Y#C|%2#Khs8LZkd{?2<ajj#d4v(xQNnQWF*FP7(+JG83a@_BoW-U*aTgH(8@ zDzTNfl}c?`(d)7E&4Su`G@GlRT=n|o&S~8Ll!Y8;XB{pb38!TgYx)X1;(m`i)YQ@^ zmmEQi;phk)_sE^l40RKkM1-mp%^~!eifILr`vb4!_gB_36dkMyvYWk`!<>ds)I59X zCCE3<V|NIF9A~g!evK_EzY}%@{QQ+<t*NKncWX+Ort=EfDO1R)aocz!4EMyXk;<!? z2Azcz^^j&v&=xt*82vHwnQnbGd);%%6o(w-8jXaz=d=}n+TH;%8%%$q^xhwEPCgPH zuK9jd_Fn5`svrJ^?t+te;?*wDm5Pytra!G?MM67#h$p*D+i4IQBqS|(H)^smBjjZE zE|_#aO?{Pv;OWKoT3shOHx?B1092U0kB2qq7R6hL#C`z6#)|7o|C;kM`;%NK^(QB` zbMQoXqo}Vz>&grKe;c#Ks_dnIWTp21AS?eNgheOH#m)id++RJTkz9~P3L$96QTsQ9 z!9spO!KA1dw>}iq+L;iqt+0A|kV^{B8K2Bb8+vYD@yU^MTpLTUp>|N~2F6g0*=*A! z*dWbLb8`zC$ln<ffeU5~()B63o1*-tUyv@f)I@-;WGw53rv&cB=X;!9lGxVJYS@Ni z{VJ}gRewP8+r#f2s8r02pk?Du0V)OsVite%MEAJ(farMt=cpCCrWM%&l|K85PJurU zHp~+tZ6+t$bt@k7D#mr@KGE2uq+9@oukD8S+L!Bi>m#NyTGi=OB&OJP@WAv`CxXOn z9)}RROPG?3?3br~rVjr)T39n9`@-!nI$eCvHiP&3)-8BRrY!fw6zAwNNmQhso;tD) z&jOa#OAXIu+KC-<ner+KozXbUEU#o4BzruOQpITNUD9s${5@oMSURow7TQs>ZvxK- zK4cTXiFH<B#0k!{dkS;TEYKJ=U!_KQgmxRX3z+F?4uZ=QHROw2ie>p^TcSogt<LdF z59!~f?XY_t)*A7TmZ<ugwZ6Vd%;@X{KA?))%irB+rv(`NUY_8{*@-%7{CxI09e2Yc z@k(eTpy%cqx8=YZwl(67(sCEkmHb7oJ7dt8gdfMqng1bNce@TDQ@cB@H>`%Gikx*s zPjGgs`X_sEbY)Y(0AOOB0Uy!-ZfXIG58cfFt))xTj83UgmeLN7OREi!(+x9FD?pXf zO2A9g&MMfAm+$XJxWX#b1c#fKfVQ5dX=_uJn!|{eo}><tm5O4U7rKwA!U$sFfminD z{WhS9lO@!m!I_*>o^Av!tx=qwo7)5V!|-*e(dSSCgqi5?ILJ`m+|cqremOH?Myihy zE#&GB?Witr6_S#&B*~i5YQ~)mWtKhJECDrv^7113Vztp3oDIXDbn7$wlhmA+`Wlkt zH!PI#v`5QgC}xv89M29fQTl~PBHy{WefHJenpaA|be3`n++AK$2D=&h9liB4#nA=J zNz*Z8vuutmkrj%f<RLCvo=4((5m;}DJ7^LN9dBX$UR`)Dazb&gmnB+Y?QyNpifXX5 zM1m1GhMHX{x1PQzME=Y7)O_e4)hDh5(q?2_^~(0am3lFF`TQ1lDK*jqq;)UK<OOM& zmfFO_)2dt30++>)b|i^pD%ZT>tI7gAxV!gJk(wGmO>Tw|vos>vNBL=Nyq-|X5mMJ3 zp91jM6u-6LsJe<<UgoEofqeN@cb1)!?A@^&wTJQFMgya(Bl9Z&{oDXzpuZp>!~YDz z{m<m_3l*8jwSRY7!ky<rw;-GsA}!agJ)oGK3C+%-1klDB)<zhy?#XQEZq*td875BX zpV&J>8J=`<q|M+sfjU;P1oT2qJ0c8Lno>piuxWchHjfnB1O6dGnM#u;OWwMaq`cv> z+vnlHQ%ep@&5C-uJV-%iYlVj1V=HKkJzjL{zq2#8L@;d8JSjuAtUo4TSfg{mG(L;j zg<}wo88}M&fc%OitXX>_yMpP%1O1>V5jaas8J#*S2dfdie;oN+3r5=@n<=$0R$4v3 zPmdKixvPoG2ufL_PY>7gx!eNcz~P!r`ll0jA<?vId;#&}*9W$x(N!_2q}~IXkZ)$u z)nCiM=;78nSfP<%ehFU(o3~2G9Pay~Z@5@^9M``H0;w!?`UOIA=6F)pK~%C~jNejj zNtGi(5v+-qwd8YMB8BJt`_}CZE6DSKy2Qa-g;g|(kXF>rp6LvN5<p52XBOvXKIE>h zfm*5qsu}GZVWtqD#yvf$)tA#om%z0Ahz8cR6XQ0|6(z35b#uo=d%nZAY6Du226wbL z=M$xRevvjVp0{~Pk@Xj69YhA$4Mpl}<;|I<@@yD#kG&I=32Y%W&h6$U8iXjgp*`KE zJy{s{<CiZG^=y7E77X=$il76L`krFLbQ+!N(sxU4n$;fR)uTK@t9vrmp0d^H#kldw zbO*EyQ7>S`zQM;~U2jm5Im176aDeL4u{Y^@<Mf%vrz5Y~g<JBv!xyDFvZH|io%RX6 zq96%3W$^tE`W*&LxXw?2&+-85UH!|1*gx2J6KWc<0D6eeV>Mw7q9tU6uV3UBXp|hP zb#}pvmjOmFP3*OP#R4hC6=z@X(9n`T&1%G)k**5P3|soi1IRjFKL{aoBqDhK6h&&# zXQu|GI>JMB=30u18s%FBT-4iCV{)Qj0YC$FLA0@C6(zl~W&3eR&+up|tdAVf&zjie z;0MspndV?0bk{%QegQqRt~p>v<7={KEC}EuzrBiZ6+@9K4_)<dN$67w_pPgVU%Tp- z@XwdLtZluS<V@2*Og&HoXpv<K_pWDy66m(qYg^+xm;(!2ABTvV+^*NeUM|OMCC+e< z$+Zz{*9e7ujefq=NLUTa7K*V`PU=y=tozzqVzsQ%CcXi&Lp?C;#~29kT8p}aGr>$y zVabT)JAL)3x-)tENsmvF@3cZhM`ylSIDu!sKvqSIj4Ck=;$X-U$0*|jjg|lAEykuf z2*w0iQwm1PBlrMzN+GTvfMq0Ho-A7188>R8gV6q?yB2(PZ~W*IJ3lT?s^;5ajpT3) z@RZV6PAF%>PmVL-CzHLsg_pjzWvS@ZCPfD)7w$}Q5pY=S)u4{g0dRv07~2aA)RXjy z@eiD6X4yvaR`1?g-_*ebr&jZn9D&e;rqGdb4P(&wBKB|Q+>70=(E4BdD@bU6uDAxr z1$5aVvA{K&r@j}XZC5t%5=fCr%O7GodnC;rKb_&%i7}-^m}%4k3GVl_q+m&=fMR2d zA@Dc?I}QTR(VP<K#U*p12OT=EY!9$5!iQd>gX6C-KT<nUjSapyxjSGTATU!>)<i;f zZdqMqK_@$CfE@)C7UUNFI7-|>pJW|yMQmlqRrn_E^lL(qLl-B8pJ7om9X-}N1~evs z1MRXWXEKk<e=yKzUw)79?%7IuZhVRwPoKGNE#j&^^yg7Zi2qE-p=fJl{YVl$_xoO= zjFY?}&wV^a?Izqq1mgX+Z>3|0=i~2h3p&3ZK5Stq-)hnq?wHaXBWub>sS*#E#HvvX z)vIt}ki>Lv@q_0fbUP>u%q;Teb~P(!>1A^JmM(23TT9C;ab~|dq-|m}X0=4-AF8&6 zjPPt1q1w2=jt-6@8fCP*T!^5fS2>6)m!<ZBF&T2S(P<}|4sxyRnj{?g?6~z_P!nl^ z{~oy%mCY&QVH|>9Q6&%4J46m`A9gd=*W5iX9;5@p7UyS6b}-qFOqyS|4~rNl^0VHQ zD`y^pU>7BT7wfOP8fNK5tlQ-(wR86NvRgIP4|fysUKNDe2hAm>sM9Ob5P5TnEFPr? zuMXa#vDv)+jc!|7u2ky7a#97^ITX3#`fJqjTD6m12KJRG1jf_#Sd8gJ^C(eF+xkjy zFL1;E$0-71`;@Iq_TD-F)Czo6sN|i>f!|p(T??c2;ZKK_ok!ILcT=@WcTF6cOAZ$> zNE-Pju8nkWsikep_IE8*x2|)B9Ue{lM8sB?LDWb|*JbQN?#W%bjeCzhtZKaWuvTNm zEKC&fg>X?0nO)2Mnej~ps38P2YiScBn5#Odyc>GGy4<Vx5I4<4t@iU9Lyrq~}l zC;@)0kc8K>CbxgsHhB1SnF0X5k_JGTzpxe!ZLO{KZH)e<iF(I%BJ>iX1p`7^Jf>1S zTpmnsh>dTMNo!KH0{)TaTRQtq;ejVYlh@uyK;Cu`W)G_fInl2E-JMKPvSrwlE?x2^ zI}xY{EYHa0Qu>IXilmZM5jwHUU-TefNkm}iSY_%J>Eq={@x`nKweK<L(mAZ}yqdTd z%jP@=X7zL+u^?_lQYT)&VOswDp)4h3;M}(_F8gbbi#GC`?+)B`Gg=Ud@E?aP-;r=8 z0~|03fFu6Gh6Nnb&fLma-|F9pJvpuiu(b|J<mx5DA9vR;T{xDW?l-IjB_BAdkvm>_ zGMSnt#m$zQ3wDjPSt){0{jAyMB~0~t3UK3{eAPt*kvUv~*Y=>sx@}NNr)7_D@da?( zMTRNw8qt9C(5i;H;(kJEhs=$Mp=g*lEvs9{@OQoJJlvl`MEx)yaAb5+0S%zq&$1Er zvonOX2&A1Bi)K`VYKV5K$g)9V_|UDc=SeL36;Zw=VymG3IM~mC9bvlZ%WA6>Aoc^o zA>ovnPBc>?F-AfV)?A7<&1+*aY<!(fjp@7tZ(nf}^56;`O>`cPQqMfUv~%Ve=zpBE z-4Htlwihdy=4*E^*|$$Gbuy_oIa%+h4deLv_PxVaM4C%JO>gMj`w*v^Zs1<fm1M_{ zb^pPi)Kp-`C<EZ(Qslp5HQRsoHUf4a|C18vRNDX~t&x1EboG~|VKi4@a9F3mnyW3F zE!FXz>%-+Pr-&*4(3Z4Gf4@&OUio2k&=<&tC`tN$`Re4VDXC!Mp`_rGJyVCGXnN!o zShpsn)B~M9;UJbY3Ru~lQ}fgK@D5BdwNEs#0VgAgu_6nqywkXkMO$CxtE?JYg#Iri zvRqyrTXBQY(xRXpp{Mn5ozQYLNN{&LKKPx&Jd`AgfU*pUSYixgXzf^bJGzw`Dz)d4 zSJz^E0(IkMi-;R-V6EIq*Wnkq#x9{n8p>OEhaY+I4D!8@!<vy5%iE~jz+8I@rmUw? zqM0(o90<k`hD{!GKZI(|X?ghh`88R$Ix{Wv7%{ZXdtv(!P%8`TTbbB|wgNYU#o)u{ z$K5)l`*jCPe<da?@ars&AS0`^+zZZp9k`>{?_p~}jXF#h&GC1$i*kT9$9+`C*%Eai zJJ!4vo}r&)Qb@rxC&sz?*>55)vTfC7_5C89{SiaFGB-u_7?IAr>Q}gQZYL~i*3H;4 zbMx7i8U{r%fxtRaX7ygZWgD2JdRo4knP9q6C1HnS$jLFvE;*j~F&iCFGrhpgsef|7 z9?E`GCYZLPqw542u`~n;lA7mKa}5WD%nO`aDid*YZtu$vN(O22TS&?b#^iCcB;>*V zJDvIMuANy5m~RBYh{-WRic**fnF)EE&$ljv)WEm{XGlHXNK?eBr9ef&2w&GVCGwG0 zyUD3_VAvx*TaLK|<t2!flknEiBznY3*;M@^vz$^;P^Bzj2de62No%16u@=&)f@E}4 z18jx(!MECs62Mq^-a;Yf^C?<6a?*}>{F^eB$x&KsO^6qqu4JHR%AIcR3hPi$&D5VQ z$`sak!H1@L;UqrJU)5pZdt~K}0~7ausP>3SUYg?z4Sp}PI4TA;ZU2H7H;7(NsSVp> zNeJ$=r@LBD=6=i_bLwBljJiK5eF!$akkmF$4B{3PfI%l&MM0g!LdZOvPj}I1&3^J> zqig+YsArtIR+9EgaGVO(R_lQ=T@VOyGOO=2=oczZpIHq~#V<{@|29_n4j)@h8OXnP z^e({BSv79{^o`Ch9-f8QwdhnZ<#+B-zwHw$N(OI_e%>)328!J$`~5*{{RdIIbBmd> zx2QeJH_&MS^puXzb`^#JgSKbwSRDU0nnhFbqP8m_HM!|y5mBf@IJRlT#vr1@!C*>h zQjc>JnkHGJ){O)XGhxLUg<n<0dfMQNQDp}SV&eNpdmo5grUe_orY-_(>R%m)ox84s zvFX1Yk@$c63qk;i>5;59=m+4mitwAf@EH*`h5{lq^=2u5dB)2%p6LshrQdqIlZbZ{ z9P;NK7eu()rklBEnfJg=kj0xV6n0~chn1>eIx?p4{?<;VZA_3La`bwGZ2l?7id%NJ zApSi3=R;YcHc+ApH9>(BB6Mn8>d0o+tPX+s3vGH+QeyE{2a>Fad2?QxD(L>}OSoia zF5MkmWWH^Dp2ZdB*l0$}!|IvACrG>|iWl9^yGrG=O|)E#B=NY0O_5@x^4=a~QpKrz z2ogfk_O*Ko3FwR4l4w01CTkuKTzlH=GGnYyye)m$RaHAP*vh@s<ylNKkHX=0xs6d; z+jdl`F9@S>CCU$~V$sESDcGC3^Sx3PQy;$mCn~nT<^^gth-J+N-oF_JM0Qg9e*bf` zd{js+`8dE`EB)PF8<{&8>KmH<TMn>}t+fHH;SzE62KT3>kCPBg0RdTMkf<xjghPz@ zrUinlm~5W&{5q=IUa$dmd%0l`iJ?;`4*QjKV(};AsZ&aK0axtc(h8it&|QV@IFpVH zLA@)#9x^qXQJ4R^QUUTVo&6WiL5&AH&HnEc9ot6%`}T-ePb!N9XsJo3_I^<;Qg>jX z9Goo42%2XXjOc>U<_U><99!g?trxVd<x&ICrNi6zOY*fbVFS?Yz~60s4b*}wcP@nM z;h&3b+h0C3)I@6ML))r6-L`T5J2jwj18lAZxCm^3%lS)Nl##QwwY%<ra`OL*q2j;w zsE#9aPp9}3P+|bJ0S5&7{tMBVJ$p}oV_;`4UG<RcENfEMFp)j)-pb0&;-<AfLS_^E zvGoyePbQyTOUmHzEMX{QWmsRmr|4<t!S!ssiz+rkdKb%2<I!Oxpib8;zDsn3C+aCe ze)L7cpr*7SJz$xM9jO=c{nAmGD%<>;0o=ti=O1JageQ{f>(CyLlr*Z3s$@N0P0Aw7 zaz|U7QJR7A2G@ACEgP3&gzgL=&+!Nx(ER0T5xY95=?_Efa_WJkPQfn3X3`Y2*bY)C z5l`mgZ&X$6p*BSLJc^Z2FE|<=2abNC^O_!Mg~cMVisQ)yC1z5r$-Pn{DyN2_g^q^~ zv!>qoT#DWgt9wpZJf)@Vbs8;F5z<;GpUpu?ObEa4qCeD(e*KK#40z^r*PZ=FL(LF9 zqR^fH73+HbYzx~^H3hS8;&?vziU&ojl#ZwT?vL`d`C$auDd2tNVg3rcjNJ^4?f##p zwdfx+|EwAl-FiiTPKK8Z)!R_4*nl`Ei?eP+HA#e!F&^2PqQ?!75cZUt={zK&8c9%M zX-wa%h3Z0%9oidg#;djBuSwaW3uAB{@5fo%(nM?we&DNd>lHDHfsd$R2oMBe83Ge% z!*@ACX{#L^HV%mnZi9#!NNR|W6#SfH^6>IBO=8cbbAl_x@l-foXG1+lh^-wvTSgd6 zH0}K&kK(kqQlm3)kM3n<_7kX!FHvpbuqu;KLQlRt0gL<r8fh8&=JMRQpSiF9A#{0< zwLy5xSJ5qjCM-?bWEoSnuc_s{d)*`QnyYVetlPAu|N6|0s)cZ90U!LQFa>III{Aml zZwjZ{j0EyqaLwKMT)^Lj#*^N#UIYrPheH1wTEXP*R_h|p4iPKt)m)10w%lLP(;?w} zpsIa5j-R`X1ZgK-J$ALgKg=^7h9E#A)W*v1u?x5-@ld^xLl>@11{_jr{S=y0Zl2^G zRZW(cjXCvC&M8|1O2Ul?K^9>f$`q3<hn?EVj6tfa!oaHxd7fqq`J#K_K4ZYaE3!(Q zWcp1GK`y1s1mh*EziPFFQoA-%Z&pTsW^O0rJwzvJ#Ju%rV&Bj1$G{_erm`j%gumIz zNO+SQgHV5JUg~x?+*53wUCobsJwg7rGFzNfk;?*n$u~g%4I#k7S-ac0GdTT!*6^4Z z+Bp3S&kK_jYyr)ULtD>iKOSbK%(II4L{>>7WFm#Eg7h2Gio%fQ@=$mjGE|-)SdcPE z%I8B5Mg+F-K0opHEh=4xGajP7zE0RwH+=2losg%;Av2{k*nDnM(AhYsvaaKdYSo{E zp+!ixc@R5kaYxO<czH{OfSs4v3wFcnkK&DI@%&l!PDq%6xUT=BaHg}5kjzI6*8m08 zk0vDl`Z>ToL-Z})_>!D#UN`fuk8Ls$DONCRSC6X~bc8-0!2pQqhReSrqr15pNaHK; z&9Oe|D6=V>b}?p{Bjsc+=P#5a`3$)MPue2Za1cu`gJ=N|7A|o40tDh$6IGv6%4_ok z)0iKC@QWmvGcTin9$Sp?%h8)XNX_izISlBl)DQ=zjEM!+0IfYWq9_by%<-+mVT$MA zI$t9}%A6FGy+4>L1+Bd}4<06PWdZ3ER$HjO5K}=p`%?itzj>f(DUU37z$z2$j|W(Y z8VAF(_)XB31};5dz4M&V3=qm$v$t}0UHL&qVV^bPJ8A?FbNv`i;u-b%?+A+6*0O__ z0hP<JtrzTJ3E$aYl$yl-m94WCFPe_Us98eVF7!!C<{6qDZD}l&)n*$w?p<zrm{W-w zuiz)z0tRZ5eeTz#KgG6Yy!d~Kx(c*4HXiH!?!uIslx?JJc9m+PXy97BpZ1@2)_}(F zUW!Kk4DyVPF8f&88yCK829O@YoHu^3`{paZ>f^lIX{vO3ldmr+kPXN$FE|_DABAWU z>5iW<7W9yD|5U_r1NHj#3q}JPDTDh1_P=Fu-LoI02q1(1(71{JB8xWG`c8&s|M--` z*xG+czafA}iAFad$jRB>CTs~;qDpAO@Ql-zazOC*)C#o5plIHE>O%BE?EuAs+@*-a z@j~LfaZAkECdPC!ROm_0qikVupR5~7Ef``XvXl5lJ^2(_dXKlLv6u_oCo8>n6hd3) zqTYK-h1u}Uev{v|MnGt<lP;Gs!a9Ee`SUC;v&3uBuywp_cJjs5IP1M|F%4{D#-hjT z2IbCvHud<A-QnnEEI9}Oje7&6Hvf`Y|F0I&Or<e^bw=tqqiMPkWmhNgpd!o>-ROsM z2s0Fs@??YyD54C760Jj15%%3GBps_m{apwOerag(qn*cpeyJS>W-q6N?QyUKX4iQy zJ30jME_RMGt^1Tni#(1mQVPfsq~NfqLCWohJ_5#6N{<Jju3^w!-K(<;BMZ4fWs~Vr z+F#Ed;|Pc5-Un&{-ruqaZH?;dwyN0<DTN(LD}~)r@EI=RqI7;Laa;sJ<20_dhIJr~ zrIoLbVT|81RR%L{EOErPxY-kHjn&~D7j(ZC+{pAy@t_)Odzu(4A4{Rx3$xx~9uUQC zT4aki&3saZ9|tRmV7;}gh}}z>juUfIQ0*F3$7fV7bWWZxspMw@X}=_0rxA5e5X<24 z>z+ZU4*#rZ+U7>pWlA}%OvvMg-dU6I@Z5_@Je_W<R%PQvD#q{apN+i(?*u1eS8_Ql zM=2(%Dh}0w?{HY2NUN;$S5MAPwiHhH(NJwtfur5J1D}PvQ%U)kQFg)r5SEB(%d0fs zf?NFk%CM@eAhT7UIwnO4hJo%rq$`Y^9;c&&%T@99qA*7fac+|%lZN&St)o8quFsC< zW`Xwwqr-P&pv1_+o)Y$uR&ONS2irLtMh_muQ64>wV|X0gtWw>^Z8&3j?7{fi7L+nV zI<4EZ;O>e&vl-L+ls-G_F)gf(=7*p$WTWnP?-zSepFbjkA$l|&2Y|Nh0zT@$XpOUz zvxD*fy-n@kjFNK<6G|W>9GGY4u9V_7WXtfkETL}lQN1z4a=#9Kc2YcL*R~BIerZ3O z@I5#L$+}%+4&p5r)f5#YD&PLBls0`z&edd$kof{^wZTx=d<bNj$t6Rh%l+r3i$I(g zo(L0I!|^G%)<!Kl&Qq*p1-X+yYD{TKFc4n>x{e9}l6e37FE{-UharCKKXj&#i1=kN zG-^E!=z;<fsG3<(MRpaH115X;s_!e4dA_k{g0u3}Y_K$0o`?2BYH=$~tkab66~qNI z7O*bA_7Z@%E|Z#&M!cbtaBze+FTqfREUbWx6=2}1b{?P=$?RbgNBU{lIF3kd+8WwW zr3?EBRi&URth(j%=xNm)V6-*ec#*PPn}IpZme_vK!D1^g?ydN0*5UKe*-tA!pQNS9 z@-1s~_m$h@w9lBaOU-}%bUk_vo`$@P4S|Qw@Zx*AURpTmNVX`vAit&2eNp{2WC4fR z_x)#1?bk|$^zFe^*GR{Zsn#p^qz9C&^u`6z`vaZjT2Sr|R{dal*hAdMlOSG-n_c6~ z1BI;Ti#IS6N2x>&`~g-j1cq6bUdvOdf_fumBm7@nPH3Zfv7~vwIYknWJKKH#0ExVA zi#(lxJMRYEKha<A-q^|3?%zW1K<rl-z@|{q$1H;%(95`l)0aXVh2@t*adFr*nCL@- zp06?3v+FrkZy%o-0*-dD{jp1`{yH77$Trv{p+l$9Mpn!u#M;JA{4PW-^3GS1FD03k zS`bddX26ES24C+{<+uWoR!=EKf@jCQ9-m`+u+oZ3cbxp&>+57r1(gf}9fOP{8B<4* zyh;Yu4U~OK26JjA+JA$=<6eu7#RqBu?^RGcAmX*{-@l$GS(+$@b*=j6CXE<gP=7GK z{NZPYyQzbt{y96~`-`73HFmPocl=jVRK`1DK$wugw)y3l^b)&Mdzo|e$W-ye)WbyQ z_jwrwNIgY2H-mU!%kf-7Y=c>3V&QO5AcPf;;5IQlAY-HT0JI}PvKXK4V*`nxP6oUf ziovYxCVtG*v(D4~voPH{N$D>zic*6*gCAD-<rtDgO1(`B-y_9%R7SK4ukN(;IxQ!@ z(e^4^e{6aEq5Z3e?)n4(Z*mUxe>w}GWdH9&Hmdx`m%P%rIRPyY5axi-Hb8+COy-V^ zP(o?uX_Nm}M3>C@c)g_xM@}6yFB3M90<1W?q;1COM=n*G+V)$n#RwEiu`8QS1`@@b z&K1&C&!={BRLj*OpQvuvVFC_PJLY*Wq+)FldMw_2^9H)pZ?rHEeSp&x^(|M$jUHe9 zERR<fi;E1gTdnJ<oDpwWv7(<2m2**98z}LkfL;&#AyuYWdVMRJ>xD`7+n-umouD*n zUl7kFe78EV5%~w$JH|<K9HB!2Rl%VVR7KXA|8)(cBAl|xkgq{=T=b)ie@EhC*^Y=* z!$QUuA{Z&fVyy$+Lu`Wr#yw2xkWCm9Xc421^k_Sbv$wTZ7Ynz5$-^>Cj*e@=y-}Q# zBz3ps+HQ<p7sZ4UX|Qo8R9y~}>hR2%Ntl}t{Z|aAs$$p65;n#)TmFd}2~k3Hu4Ws< z>#KtNNbb}M@{RMM75<fZET>-UJ;H~{-!O|9RCUkh8~NmR9rCWy71L!)FxkRPoEf<) z_FLR!7;l4u)O$06cr2T+J>Hf-QHcgg?^&rvy-{CoJYM4(_Ih8sm~4OT+NLaVs9kn% zq)(0DxT-jraHADVCFsGWpelJ8nsdG4L;eI6D_EQ5I|Q=eK`@n!o==rLSN{!@i{^9w zE%p6Tux~F0Ys$=p_9~0uZa8~G=&mh>g$*^6;hd%C1SGbKi)O2$RR@Mx=Ix&S&ko2& z5?=2TfSR}h%*0=)(56<l|7Mk=V`~ut1mQwlv+}}W$t9eO8SPo^S!9EL2J+;DFD0}y zUq9f7%32s%efa2pHpS2lvl2S{x#&^YePzJP7~mv3XKq1eHnz(<p{AE?Fwgb0%S4d! zP+UZ-HiK<93>q`8f*HQ!z1*|i_C7*QOBF7gq5D>$-~k;i)*I4T$_zEyJr@B}Z~Zx> zN@J_)Gf(5&T-HZ^uWO84nM!~Ryc{Ir$f7B1;B@;(v)O|Sld>`3fX4to^tVHLnA`mW zI+K<D^+X*<G+tJPafl_#V0?ZaxfnK38nSgo((hqbq5NI=JoZ^&q+WO99wsi0hhoyZ zRp@1i0d=#3@#4-hBOh@IYAr3K0VRnANaLEfUVW#~;1%+w?P-pT@|TD_Ci<O{VFI_K ze3f@oGI16}F1Zlul97g$vLxCmjO!hl`tB2^VFPO&!`vq8epKY*=qM_N?A~f-8dx<h zf<6WF8CsYlQlW+$D4e-y=-g6jU%=#W!WY-Mg_{7-kd+-hWQi!V*^dDUC!ov_dt>B8 z_yW;Z=dKgOy525V!>fbb^>JU6CiX=u|F=BWcYk}~O?tgVS3c!Q6T=AebkjftgIF?0 ztKZDi`fc&>LThn{)o8)fUq;<SvNVWWN-mf@*1-;0zd4)CbCjf@!DT4Wr2$qUVnlZ3 z-feNh+Py?Z+OM%mjXXn@sgL5mfm5sF>!V1$0WDb6eg9l3H(_h@T`lwZ!&{YK<z<Ed zUg#X~LVvN?X8Mk1|5M*cPL#I9VL}?ZdO-tit>|GhLQOF+ljTbh%2a|jaA9j#tibT- z-|%`Fb+Bs=U*i@Eb$|OP$1;C_=_+0q#nc80SFl+4dS@*)t6_!_1>b`uk*MYZX50Y_ z-Lzt<+1sfy$vpT=ulHgfnx8;ezyL;iSoK~Ots)&oABq#51rankgJ`e!cY)bww|X^` z*mh_=&Jv$u_7@yKnq?{UT&pt_y=-O+KL5HnQh$45OOVKryrC>rxKZvF9Li#~>ba_| zo02T)Z!5}%LeXe*E8FFm@ZGI-)b^VqvwbLxQ8+(2+BGY`T<;)sacd<F@KDub{5-C> zWQZ_?S5XL`?a%fr$o@3;I4a&c@yHkV!>jx8FmV#{L!P?dVv^gPj>%x7|4wFV8Wf96 zM>nSF;(}-UerRkbUGD1X_NA>8$fQIZLph<S#`$+FsEz|-JuU$YL_@g58)_egGB%Hl zZK-C;m5YH{E9>%9_E<``(N%z(J;~{B1BjH$FlVQ+@=Mkgi>Wj&_|ZS=REF`WOSphn zJO%hGLV!{EUvdp*#`<>l|3-I;6H@(*NL@!%hfSsZ{DOjCd7&d=zAzL9S!LP;nxiB@ ztu=F8thO{^3~-U9UQzUMWHy8L&rmsnv2mV}DChU7onW1-gu&qxd$>;zZ%;<ST^24L zoh3$&VwYm^dg54HvJvG4q>K7piZs$dnK!}#_e;-ag*ko2$Pg^!I2VM%3(f&{9{Q>b zXLUX!Hw02;yQ5cmDF1_u2(=AQyLS#X(0(DSjZf-`Ur4SPG&{6Jbb(PGIh6L<48DqU zv67NSURmpe&Z)4Ayws+kIQjYk5<l|?-G`2cps$EWVHXMvZ)oy&dV;QSv9W%*cx;5x z{uP6ecV#_C!PZUteXe^Cji8sekqq0p0(1$n`gi93U}sE}o2kuDj?X_VST>cTw<6$1 z|A_+tJC*;}n{~3bGPZFtcXBr}Hu)FW{QuDR>_)!P@9d6BkIS%0P0C5q4Nr~J)Ph8c zVWgm)Py)5qYTIFv#0izK#+By-DWi@hr>P}qD5fT)sYX?x(t`g%*9$@M#iIsL$e1wx zuDDK4cK@Teh3Wu!!il!=O2c_f4$rIOgp~tY!k!7$NEVH6&Mf+M7{wYkUNTmq@!soQ zTXBBQ;j~GEl-T{MRSrMFJrG3025j{f<f4%Ga0Oz+jiU7kuG5ppoy9%yk}6-6);xlp zf>h8$94kU20)dHUk5`~46zK*5%%EXpAI=F^O3B}W9-R9+AkVhL?b(od<;$Dj{>jf; zyio*k9SNh_IW)RD5!rapBrH>Veo>tsGC4PXCZW)CL9(OP5{jT<ibPV#_G2QrPPPSv zbyf!!mb(X=fNhFGEwp0u-qdk*Nn5aqW~JXka;`}7Obh&{Q_aUyzvB9^qD<;F3Sql8 z%n=1FHJ<JZVcUxBr6O5|tL=4k`e`*Dz8?rxpQXZxu9tcpLlYTlM5qeNzo6JU0Idl6 zMT3GLV|~Bq?N+A{=SLTx1gm7O<7+BXY@>4JdY8JZdKMb`9*bzOgwPehRx;mOe~;OL zvT<*uTZfT`KrSH*7}d;W%+qQ+v{lDTAvxJ|5L2!tA~DD;87A4fJu=pSAv{RxQQe!( zuJ2ToBm97?UD|%ZqUXyLxhi&Ls$|pL;OA3$t1}Yq49(e;+a9H&a?Ef{IQWG*H)Xb# zO+^FMz$-5Q1A(;(s~|2`|AO$)CcW-SP-U{oK2;Zd11OB1*Xp-jHZ&CI6Ocw$<q5Do zL`DvjXyZl(ix_HfHJ7jq_yul1xjZzM9}KXj#=fB7mB}Im3<iawq=28MY^260w1DFD zQ`1xeU&0M6dYH+!n*TP&Y$gIGXnse%ht2yBd-w8;Da_xD(v-VepfK{9<{?qWPq1IU ze&vetdrGS4<sSs!R}ylV|F-o}ajEJDm@_M;s)B>aYKc%W3*C`22Wl)>qKDGWEq1{F z!<(X~N5k|iJ8*NJ055=))ITT9yYWZ}n?6ynyyM(9e@)9-8!1N_1*6^K>dbqF!%a*$ zlS~*Loer69cI~8$D?IPjg`M6Q{b;II4ONh&dyF_2>=@_VRRd$V0FRBTBw*1oSwM!E zzBWNI6MKiL$XpxuJL^Xm!BIi(o#C2mMlM*g$PanHH%*Ro$CDx4-0n34OwbokiBJ{T zH#8-fQ498OhFFNIv4yn9zsS&Mhm}bMRDwWCeMVvm%8Vb)<m>Gdrt?XY$aoh>x1ch# zj+xBZ{H$}P+?nDaOxvAG-)5GO>vm2~(zLr!CPc%E)UU*!+c(W19N+YK)~Z}F9+!iQ zuFg|*FFs)>)~wT47b$;>mLEwfh#%H$%xi~&hG5i_XAZqNiPAH;JQ)3!XghH<lAa<l zf=ZE@b!E>D#{KpV!_nBCKZZxM^gXZYu+6f>8ccWN?BuaLy;SZN1G7{M|2vve#Fpes zqvzh^?WaeZFR@xC+V)ihg^Dk`F1cKCR9~UFX3O{knuh^9XxsSgux>OAR%uu#<40v@ zIh;s&q@ec^rf*ynTK90&klD8o(Qz;<W2?zPHKA~IN3#4Al!)3xDsMV!9LD63g08le zPfp5~oe|ngM*4T3d1xQDZ~1H&61v~bU+rkO9aI|LP`mqVc*~>=@mX4sAni7v5BuBv z?7yDnaw}CUlx<Z{TTuHK6HsDRBHCjg51mjOn)*kP@=YzDy{bb;naXKEap~UNb-BYw z4bR=c#eV^3pcddh{xX;OP#PAC^jKb^<Q?^$Qxe)Al=erRnezO0^H#Q$@JiS3G!@JH zU<sqo=sC65hM=}Gj#RWnNpDx=Ljoo{X_n8h)RJ$gQ}@E&u5*1kED#x|*{FhEfW2>h zQ9X0Nk<>yc9G9T2DSDHgu164#5Stw3c(>p;TH9gDz%eK7k-DW|wR$%c!mLx})?*jW z*N0TYFc!MgrfQd>aF>Ww(fC`s@HBJZ!@@6Uzx%>(RVhUx9l*Opo$({sEoa}-3N%!z z_&VTK3Z;Vq;b7Tbu!RS#GPh_>v(l8&DN1_Md^<#}f@i{ldqidk3+*9-^i!K!_&pnq z3yP}uZT%YfwXvQ^qaD`Me95IdZ*(<L0saQNOz(NBe+YJM=&p%SG7H#}cw;XSaS?>{ zBgRe-jCg4OBpZ9BK5J<^=?iFq=tcTxj}C3k9QVAvYc#Oo*ZGP@&DQKy?(U$fsC)h3 z^j&_wvr|8G&njtVMiXAr+|EH8Bi>+YS~VBE<<2wA4s@5xzEI!*NGiL`;L4tqAD!4j zG;2F7(BD%kXg~2E)kwCgeyq2FlTd{5E*#6ZuoWy5@6<0X1)y&^T-M481KER1x5MKI zxXYuTjpq(CZB&UwYmqa$e$-0-S|o%$*N2Q?<RFfBz0!i^h0?Lc(Cmh`im(xWeL-M# z@Ou|OM?HVFDXu+dV=bX}udGhn$8TDsIBUHW`$TR_5v=D8AhDJgE7PT%K-Qy>&pp+i zPi1)pS8UZ@b<}d!KoVM8JW=a?qEaTpUithZ5pc#fRdWacS9IWi?Fcfr{x^oHRQ|V& z<8!2@-ocuPDF9@i$h})x0v!XDyh>z*%owg>$YRxC{L3dM6GF3O(P9ty`SQ*C==IWS z$+@{~OHw`(p#&6It--unkfbaNdCU3xP$|z0kh2xl7APKCCI}66GNgQY$u{_%;A(S} z8=+zWjf+VzuiZrKr28*1)Dupk4+o6OSxN+;1F7_D{@@#SAye^&&25|@o65%4_mC6? zkUlgrMAIcugc%fayU5D~q?n8|nTCL|$UVyd!0IalSs+74*y_Y<h(6M<;rE>ArC$|3 zj?KjxQ4jWZL7$$m`U4Vyp1N#z*$&W~xar(xj>D8QAy<X)0wPgT9}c$Xpp7WANXMcK zG=i8b6)!bP?o$h&PxANO`Hj|5gVPP<q&;yvkKDgKILr7VoA(vt8`md+MmUsRR~3I# z;78NnE;9aDoliNvhmFIJ*xJX#e3ruY&M|E_ad#D}(Z@g)4VIp1NQyr#{7q87XuknX zb5jCYojuE_CO(nN_Horer%@~7NJULN7L%qAZF00r2VHpF?!cV!sV?yxh8Uz0nL229 z>!`S|`^4QVL3G5Bs#GT=h4dIDdWc2zeH-XK(+@M9sUKES-5R!!JJTmWqycmR)xyh^ zu4eH(ajiUsgbgB1;J?c&Rz<Q%#sF{_0{H$?cmPO<t%KA5ps}(4v~&O(JS6B2r2xGi z;7lxcv*^r3C?DWtsn71=T--23rRZe;Hg?FDKCQnc&1NV^cl56-)6`S|Cfpb}xEOH9 z!OnewM~PLbQ314c{L|eLs9tacyNC^9q~&ka@&{zJd2|dy1PC!5%3mR)xs9V8VCd>U zaY7@8wcH;2lc#TR;j5zjAYxd5fesZ^bbfKunz^vb0lDlATQtuam)<X*Dd}C(!cN%r z5U~E(>_=DA#q%KF6&i})HntTQhKXh1K*Y5*%nFvNR?lwM2UTsY^pX9RO4lcex3T1v zT{4=7L2Sd+i*3vE9pVGhq*YG4Buqct>6f_T*3BW0JoU2ZNBM+`JdNn@B*quE3aL?) zL#owBufW&Ehixt|^sfE8WrvB;c^b87&LAY|Gf@+XGffB_6RkoMk3^8S>eaQiwc)7Y z+Bz0R%dB{jt^>1!V|RWX)sG$6WR9v=M2|r4cQoh}(hcLQAGFESA=%m5ORA7}tdt2! zR-aFo@<t9{1)<!0*VaITHl3IoB_oerxrP)95Eu(v#(Sd_Q4^m5FCU1S>Sy>B;TZ_z z(BszDmbxxyl0V=y*aoui!>s$QN^^+8b?)ZaW>c_eseXhMw>TT4IosybBL0mjE!u?R z?%}z4BwM$rz8t3M$;+FgEw$R_sC;JV*?=9J;VBOW#&pIyK2<*v!xT3g1c)i9+ktYF zc5Z6I9dc!j3piWl`cBW!ewk9&cqFc7{H0uqQ1vYvucMiI0lJ?WVdY7__`DC)4V5=C zSBHLp>^VU@vh=MR*$z{zU#qU#vZo+kZ0uoHU@&7e>iZXN;QN$*Y6)vE`zTb8{M~sN zdqNX+QHi8grkxr~w)oty(Qp@(nBGXP^T029CMrkAf$jblwX-;{^m+WlU6*#9%0nMZ zW7bIh{BCE8M>>ynqiHCNp7zO>%wvzST4mY>_g%$PiYEtal!O|Yx3>8^AQ#dA7gp4< zaEgO~V=9$S>93Djap?3D1zi@JQjNaVMDa$sG@^}fyv^|PH>Z`Di5YT^ipqz!3sw~R zB8~d36dQ9yJ0u%Q*j_@rTk$nfcj}~XKV%q>vL$u^C!99!pv4A3*e=pCk(`VJEm-bM zi*P{yxeKFsY*Xx<@)hH`6AzDB?r<(mH0Z4Gp?PpBc<X&J=o*ax_aQ}VKqjOR=&wxR z?(E8dyu!f~-}$&le<efly;G&+dCWE6M%4wn<>05?!e%LWRIy0+mz|35C`!ZRI-zbt z?4Sw8^#Z&ps%JRzDbkxTrKD%Tnw1bF?WtEPw;l{}E=p&9A7slM=2+{{gx{?f#u95! zW_TDCezdyYvx{2NoGvKtJ~`YHPy^pnJGS&Vx2}+DVkc?qZq4PQAN&y5DpxTR6y*5! z4fh4VSi4f8Sp%3p1op02DMJMpG5*&`N04Hk1`Y>b6ZzL_4${<G9xRdV&yuFI1Z+m! zCb>6_p(I&xm*eipndfEwFM@1i*g)9XfPqTKyLGZ3vL_Jk$pZq#@1KRAkF?#Jmhede zBI~a0+DaoEseW_FiIDX~C`VRz&(9B3&LSHV_tRltVqtI+>uQqwbiI7Z*4j{d)qTE0 zFR!#ufm`_mn`aq2YgsbZpjV-Q&SrU+0|`ko^pV`OI!AYEhauia+9+X`Lj<B9FdWi0 z2tItXv?Sy?$raNT7ZLpqECb%lUP8Mte5M#KGL0XG;Axvb+p{go$R|zhv(Bxl&2u(& zTae_ps9zmPOnHtI6_|X=7vU%p%MoSrbEby2GWet4(yQ#@$>bCUW<DBBs&OYBodjn~ zOqIh0(eoU8_ccG|>Wf1Sm%se??%Q@^dhIxI{N`i|Bym%QsE!1}S-g3s*htM3sewNU zgX-P5zA}J>OEbWv{-wzFpV_Lx$_oFmf&tAloX35d*SO1udVwa+gcb87=1>($Im<_y z-{Pa3bPj!gB^nn?uGjeOwaaa}9wo}Ug3?&HAV?X0O`~k{PqkC`n~_Ieu;ETcBX^bD zP&9TjIA7Zz6B)@wZ_1QNRBlkyNj6^y02-=|>rRqq=cX`kkWbKzLnxS3QPPVSQ81st zFR3i_0d1|0CUZ-QG59GtVUM8!H}#bAwk81RgE1EA!OulEva-EE8?>ul!(2>L8i-pU zGM*RTcu8opvVt!)8QuTIbx$l^c~BSJ9yFLxL_1?pyZ(m8OgR~M29gV_!-eiN><k<M zGR5LFtkN?m#f<@GL~91UL45YJ2Uh`q_WAwg#+^rN>gdFwUGq(f?;X^QG*dd5$gM^! zD0q_YQXx@Z&P`r%5A?98`u=p6ay-JyRaS>>nMBbnD;nJyZ#Ns5<VCZ+=;6y3jSHM8 z&*5_ZUU36}PCi^wQr860B|2xHHSKTw40-Uf?~h(HJE4hu=AX92?K!ZWik$NY*Cmon zz;xM|ai~>dz>%LHKk0US;iP=2a>=jZKEMCGZ)HXpGl1bS{J#kM$LPGeuxl7@Y}>Y- zG-zzww(X>`t;V)(J85jAvF$W@&tCWQzGFNc-`(%W9_Qb+*SU^;V9qtCvi@e@k)1cU z-t%jhEH9!+1?%rRoucVYG4QK5Pmf<katM8q`Pxt;N7*kHPCwVvtOI?PN1)H795{<` zH!=AP5ioX?9b<G(ZFj~HmwRUT(E>)xkRpx<V>=8$J=zJZbf9thxC~A%iW5A*AcFdj z<pw|H9jI3f=&{l9FyI5Gqb8W3uZUHA+;<%8alOWL^v1kS<%c@9zrXxdeQ@%1R*(ZI z59?U}u0EVi9RELM>_rWk!~%AJGB%AeTc}BMn9hfEaz(=LQKDoiN5-+CgM8ySn+xc0 zaYK%&8a)ZjKz{eAv@~l@Mi#m<y(b~hO-2NLy1T3|Uc*2sVN%0k*PRg8NYF`NMEexR zr7MT`f`$x}vi9R=k@iAwoTaa0gnY5}mZD>r$+BCHXu)oi3_QPZPGbJ({gj#XPq_IK zR08`OY>{EOk%aQgIn1x1KGH7)$5dZ>_yr^U8K)Nz5IC#9QJJ!SvATDQF|R!Qh@kw% z3ExYQrYsc4<Ee`pkR!?eDTCVWai8C0qDhnZBbPT!L$-*4bl(w-F&S{tfVJERCcJ)a zhU9tIOuo2D;qXE-1IYag)gC0SGY>!pKhFKggUMge?->=v*}w=TcwK7$HFhK!XaPtB zsaRb!o&j+c;&9hoQ59or!W(b!DvCJJM?-4%+gv5fo`QKS_4N4AYC~3tqs9bVrhO6- zrBCx)kl~OdDmecy)Non!@`EGmSj2g+dLOM~8eJ+q-ls0lSL6{YMK7MV%B2o2>VX@) z*t`h4u1)rhnltBN+DT62e!1KrS6v|aI;sS$8j4C;p?L$Wn#UjZDL%Z0XvvNlJD8Ga zsvhp_!CDvj9)8u<WE_ZFM)}~Uj0Z&PoY&ZXPyH7}NXN1hAH7>|iow#b5L36-ZjQWm zfiGBDs-`6EG5+yY(R}nSnHN|m@VTP>mReju%eKAX#OR<SQ(4;9!ejN=%bF`+DbG%p zC@T(@p-g#41)ZvH&e~CeQM7I;H17t=Y%;O;CfKK<9BgrphFfc+VT`obhKs66;UpDP zU*0l@A5k(<V{2#b!$lSfrTA@~D&Irq8{DS9Ldc{JUoT*1^9iB8vUkv-nt?`(uvob- zl{NT~EVR7DF=oxOX-`(|zTAP=*hMZ+<*GN<Rz_lsN^PfXi#bM%NLjga5xQ-f^2O(t z{i*_`Ijgj|kge5hwAcKxtn+(CbKiOqH!E@x`ovOR!Jyq&Y}edtHl<u4?wZ9j8C*R7 zvR^b~(_SI!qF623#X9V}!{Ij_i18T1U$(?wyY3TSV8j(fJ&EDe9B_Il38#?<?W1jj z4!ub(TIod&(Vk&9JWUzetJgr7o9^gN=bqA?wa(XwFIQsSu4fNRbk}T;jwIoZ4@)!` z%7HfDJH44GQ`zcklIwJ_oG+0K?)`%9w@o*Jl74t<km?bR#g~y(w*;1(v${`lO5Li% zFK=7LtT7A>sJLV5dJVIj+)9NmX3t7B<;4B^<a>RL($(~dy#F9bu%14ynQs}j#=x-2 z^AL2w({44>MlyiFxWrOp#d034XETZu?tnUX#>)Igl!5+oh#W_hefX+GTaaIIVY>{p z^`g@LPT>DSuFCenB;}U61u>69_!@D*q-c$ZS%9kG!Tqa?$ons-NJ{e)<qH5N>Vy9$ znD_rmwl=A)$E*_~`aG#!r$%ph^B1a?x4+e@G?jUJ)zJ!D*GujBfvK;)`#^X@H50jZ z(IKP+PQx-DF|u%pI|#liGJiy^l%XmX#J(dJjOawU7MTVl5{rH-2cX8U!Vv-Ilk2=c zouj%L!KQO?<^A$%uJ<ZXHI4gMv?Z}cGz5|ab9%0_FD=L&UkQuiD)b6VPnSDvg7o+A z{ccZ{%aikmh(rxPy{y<BL$`eRC4bVbS+n!*f59veY75q(_#$i6NPerqMzD0Y$EBv` zCKoW@-6I&+se|RW(+}LIA7!OKW$9z>%q!W#ah8jJIyz3$)l2O~1bx4?(z;EhmRy)x z8dZl#?v=NSWF~E@B47*&rX9|57zDA2S{s?NH#n1wnT_AND^Yy15^$60c!9}O&C4hv zMI4S^f3z(=I!MEQ^WafpYA};WrvHxhOR~IQrz_bgfj`dQ`R?8ZLO9xP$eS`w)(-Cn zA9XZv<$a>8%eA4cK>F8OsD86+-pEU;=FjwV4{e5mSN3_S@a$h)nGnPjP>qF4C)VF* zm5Y3n19Db>l*{_+t_C)m7vDr8ie1Ial*?EXH;llK<A%wj66|1p%q${zSxh_|&P&@D zjUm4szLOa!1{df{%8*oA(e}%RYU|BA>BL=gNczR9XjNx2lUgM1SM939f{ay4<C|}I zkZs`&3maT^!JZuuj2+NfYS5=}SRQ;AH<6@*$Eoo25u?G(?`0i!QQA4SD-5WM8P0!T z+Q9p%e}NR>$}1TP)HJWuoHCbJ)H4hL`EY~D3Jw{@rkp&G8ES}peChaetEua5-z{ib zmhXw<dqU<7t{Uz4&GcNZZg4}~lZvu_A0H+exz>uBX5}@HQv2=`-y9@Bi}8r=y0$F0 zf)I$G@4C)V{xIcTCBXWoDBaXMnK}=}VWcax5jED`x_PL)z7mxt<8ZcUk!q>&%*vV# z<E5Qs)Q)d;sN+bV$Y%ZN^Kxqn(}YuCwBOWa8&BwRA~4Vt1gHN%>?x6dwf~pyk+@$K za57*#00kH%kpX`H7lh8nz|qRZ-q675zncy|QvebsKkV>Lmt1^Z@EQcb7ZoHdFk{rA z*@CN!Tr7dOyGty&y%dZ2_wzGCKgtRT7&eh~_|Oh#Ntnp2NDVXuLqz>u1esp-ipC;( zT+IkjCJgFCn<5iDnssw6fcu?ZtN~z<)8>Xisr*j$Pk-MDh<LQK6PBtZs2j`%)_{A- z(sN&A*$lhd_dNf_o&R*$gU0jMa10ow>eCSLKCXc0A96YWNAFlk6_TDxhFVqpCxaqn zDec(kv=ZYSi`u9RlL{rh#Mqd$;^?Ft)uhD1uRjwJl~HlpsV`8bhv@ql$$rUX=z)}Y znA{9anUOS96yLZyI2))JC5c#Sf@sKCc$->D@reM`{Qpbr_Im-&#w%cDJ%BR(AA=bi z3mX6%>;IUBM8^r(1_Ai`PnrEvqJp%hDnu@`AosANpaa5i5~w&9tv(Xi3qS&kw2&`X zN9#9;8=&>>Hexf0ZUQ?4^dcW{fu0@Pk%CypA##Sp@#6$dQzKeRz3rpa%QaL8{n>(v ziuv89^9pAce$9)jQtK(wrltDVXpFNMt|8bnsyle6?cHoQ8T1|n8bR6h(;vJtt-9+} zJZ@e@=#QAG`I*!NFq!U&zN}(j6W+%dcan0<9P(krguRjK;2wZ;x`+#GFflmg4wKQ5 z%0iu_ZH`RWjnu~{h)*ppf+cN!)1tL%%yqjw8%oc8j~ag&GSBlfz2bBKYGcG;mSXpR z@7>%xPUIzE@5%uumJsm!znxeY0R8FDtjI^c?+>QjbwmTsb56ei+6f61Z1U9VuF#oO zRCrMcS1chzuK&5!I0qeZ$SASp)#<^)aec02E-xs@DoQ21edCb7#eFG6kzUg>Go0VL z_w>uql8LlGw$e}<;@cOQ0tdWQgM0s-<6>ahaZMF1+MTE?Dm{kzuo`McRXib%?>h!V zj~!_1c$ij|5lAWU2?Bge9Cj;`rClvBalaL(Ixv5<t-A<lEy$}3W0jZt2v;E3zd_1v znNS^h&m)Sy9-7T>cRJb(ephiS4QAk{SdP1qwH5kt@O7psbveEQS_gY3J}CK>MAtFn z0b%GbtCVoVszh<X9ytM?e?+$}fGBFsY;WNFAI19r7eYKy0ZUKy>zAa2j1nc?I1{xv zAVvUq@!}W9%G~;N=yq56;^ctrY<zVl={zL)JW(LoqQ8!OCz{*q9-t$g0N}#^Sc;t! zvx$xS|GShb^=&(BE=1p>np7!#V6-APi5iN$VoR^n0SOyTomQ`KV&fDbZPyFlb)5R& zXLiF|di+FEU#K#gct1>eva!BzIkmoU1}V1=5l)gpvVoly*JXCLTeDXaj#YvLhQ~n@ zS5-E`C%+Xr!MD(7sHa+htifZ3zkV!1cN>dVo-6ii6)e65v)_iQ&H`8A34iriJ{cul z9G<Pbojh!2VfhO5qeO8s57iwY2Fr9YU^cfFia-sRC7He2M}C@=5}yxHN*R6pD0!~Q zjsKps5V7_w?(~7`KugZ^!RE5S@GI9M20A)w<y;YuF998<AKfl%${lo>%=XEUr~Nqo zhK1dYzgQyxM~ZF!`VzZc&BuKn$JNhQw~sWjE^$2Nc+alzv_U%`<AzR@9BgZ=L*jBK zMOCf;Ng!Qyf;ag(B*|_Yqn-fET&CTxddLT2%*fi|xsg{()c&ZM`CEaIz;(iR6Zzgi z#ss(CkdK&2Mb|JqGf7d;h`qEZg>cXdL~_j$P(nj0TAAaB70cY-`R3(tW|m}A@py}` zWu@D6#k)l-X{_im0nGkqR;}t1E-oHOOO%FcV@os)UO%r}3k+^2jOcz=lwvDoI&@g` zl+wls8CmT%NQ*>C6l78wJka$Y;dZ0@%k`NfqKYPQ_wVnpRyJxV)=W`~&2P{(dM58% z`1(E^KAidId~{3WY!iD4NYmmhz=bRuAS#}8LojTkS+E85<>E=k*>SlDc8qav$mG;? zhdabMLMIuya1g>PM#oYu&Q0WV%{^)HPcsJI&SVcpfzJq-Qc8}8xz<_b*a@KQ=^s#t z2f(HBlJlrUH{VF%fs1(8mR0ROrfOphp_`j5^5yxn?ZcDcn7rjiIP?ED&$$deJjjD` z=5Nj6V~&N&1bJ#)41fM{8#i}Am=@DMf!LI9w0tM0WiJ5Zsd|T6T$i0#`4csaR-^6* z-z*!1cU-^nJZKSK7-fG_s|7j|sydfiK%?P>193yRYI3M~q@=8;+H4s$M!zA`R>Ru1 z*8uxUM?%!|$mH^D36Zy?qNCFJSfxM)QlA05ivy6(PaJ)F+52$FG&c+fPq7XB66tOW zm!oTcakU&X@4I(kdZPGP*6u@-Vm~+pj{96^7*+wlcYJlCO>{^nQR7TxCQar<qIK@* zBJ9ku5!<=`a9>qiPR|C>`qCf9=o&A^!Wam&#NejN-nbNB99YkSaNc6-5(oJ@c&CEm z$7<!~Iq?hgzx~X}k;zvF{r-XlVmE*b6;N!u)IW~MfI~~Qp9peL(0qFUO7s0Zr37s8 zcuw(m<<zenP6kM(B@|uw0A;ou(a0jrW<ja2-S!gKC$q8prK7nTm4j#E6mcAJo_5Jy z{)&=0U$c;{Cj;sDGBw;t6bZqgLk@0Gj#!Kz$pZdsUCF5VE+Nk+=o^BF+4JpcqnqZT z-5B<a1W9UveQtCrPtutWyVm`&VFXH^#%AtIfysqDbul?ab>>IV{JZn0Gnh86@u;zU z_$57Xd96cT;*rZc0>0}bj?F-&4y)GMY&bQcLro7N?QI$x-y4gS0&~7xS%h^Jb@Q!s zU#AHfh?OvXr@hV-d}zR`+=7o+d?-$OG)ois-r5gJb?Ui$?FdtL=uApXuB}7SiijXS zI;_9{D>HyoFWmcTZV(`<?P0UKLFKjP6=r}<VFjlAU<1DUwStCe9D(=XUFSWonHVva z?Q)|w@U+Vz+RK;M6=4}5fwH8#GcKo9J0y<M=$aAw%je$2<xz{o_t?F2Ll4a(TUH^s z4L3x2#sYhUwd*3a??`(&!QSIAI6*K)&-SsYtOf_E6Ay%)EWP=y`J!iOGdV*m>O8Ux zx(4aXG)NluOYISNZ+xy;hM0h(<sS5?YjIV{F3W~*lw7viEk5|Q7=!o7mfL(;5;DR{ z_rdwn&tR_zVGWX9`}UCt@B+PX_qO5!1cW)sa+m{((~gdt(vR-RoX($+txucxsHU<& z%(sV|=D~e1e+%*U$Ue6HA*m1jJGt9i*qi*vX3B~K_-_y*^1F5a5|y$1mKzvugrTz$ zWns>RO>7V8e7qgm+`Lu@U;oAJ%3(x84&3l6$wmN6jIY1%5i5ye7<*F4j|IELV=zEK z6PQdnxt>~ekvum=Yvp2B935u#&fAa&JgUb}v}6WWTBV!N-(|Tw7q;-w28b<Q*CU=g zR5i-kNEnFd71nkfegtjO<NarWahH}7w%E_O-c@S3AFPvSzmE1_6_g)^is)rqL}VWC zFY$(0>$q|riT)ON+}l-BqX*Q35`cSoB>#I|V{dKXY-;Cd^QW}QQW^VmNvP|L2J$)o zOEtpDq#sP9xSfDj?oTpC$Ozn3>sJ3roQE@R)S|(N6W}TYWHT?8J5KDH(dE>3e$TUT zCE8zFiH|=KW0&IAGfWpNM<LMp5tBgL6F@MpY4eM5MB%8}W#+vqByclXA&8qA_!6E> z)f1=bC9P656Ya*R%&{fgTmpZD8WlQcWTkh+emgF?dU_JE1icp?i<w7xM$h&u8T|^@ zF=^T`72?Qm#J^8`ZNwZa@DV~6Po97$a52gLje<2XyEKl-bG1lYJ_BrkRk^p2@+waI z5@Zk)E23Ubk}l3%w5(*(_A|{~(4y6l2=TNMq`?OABj2@4S-y1~nqo6;p{V<sk){#N zOp!iP(SkH#8(f~(v1G{eo_dG}WP;<#ukKC6jwKF)Tr@5MpDI+I#nA*rx{TM{EB!X$ zY3V9Of)XwFkP3?mFWmgIYajPbD!rv;Pu@e`Pe=!+ka077KqF)mnV$t9P?o-h4(5qh z_o7z!Z8dx`AevHVPoqbFg)wKEupg1wd_2i`^qkTdTCDZx>(qPNh#>KGDv!3eSXL$l zw%utwo@CelWXEk5-;Up&zG%<)J}Hl=^$59(y6-w}P{KbW`*z?|7nG#Xel8ddF~{rH zaoTYO>HAEwUvfpfN&17MXjB%+`7X~h;F(X{YnxbpM_TqUeZKix9h&&=bx1EkW{Jg+ z=um-2A4g3v|Bat}3Nx&sshJ!pLyNXu<oPeD7Uk|JT^>M`{h>e;{o}aV*_+t@XSdO$ z$ZG|VY1w?Bv5f?=3?Wa<3y$%hbGGkD)!$dQ1*<15WU+ZY^NL3v`3*@V)VcC>dD7Kk z*0km7r&Th6Q#z#)d#&G$n?l)D+O&UN3hb!PrMv_=xjQri5i02W%TeTN>q32KX-sVw zs{*=}TY0sRnp|mkMKls|XZ5SaAs2`za0qTvpSpEBIRUQJt)AFQ+2N=_hlg6apD}J- zO2}uVsIX@ag3yn$3}u{EK^3Zk+VXlrm!F?EC3vO{SS~>WTR;6IYMWIFG+nmAAISEC zpfdU=d3iL1FjYtzL{Gbxp&}YIugr+*&4HmwB`0nPah({(qBw_Mk6sY+u1n=I7x0g| zM-|`a-_^uBEUK?D=p$aNQ)r8XVKz*EjM^Vt3-R-Rn~Qd9#<jI?#~m}jPSYDZO4|O_ z?&b5>zzSVKu!#o{<%Iy#55j+Ky_3a%9ZZ$FUTg*@K)I!a-zQ(O8llT8!kJ<==v-t{ zP*}WS(K5JGU!6nWsd<e3>16t_M#j%t?MG-(+RLOn5B@Z4IMo=e?--ISY|=rH<3Rrp z-V!IHU_m|(_REbGV)E=u0eS<c0U)fXcVfBS4SS~;Nbr-5m4WZjVM2WsA|GU-+kTAB zy}-;F0+LW{AFLZ)bUCICaXYCF*!l*sbmI^BY1&u@AFP4TQIvhudfvQ~j=g$?hhf+q zwNNLt*TE1?W{07@pVtoT4Fmy22hUQ|&F{g3$+*iJc9)utCNC)$9&}v}D&r2Hu20>9 zwwap(MQ+V*HF2R`n^YZo9V1<qi<>nZS{-C=*=HS6ZtKjCmuufNbwjT&-Zt0PoWlZ* zd{7!6M3|gbwA|R5Y?oaxd7f(5A)c0mhU`JCS{kxcq%L~6e}>2tAAnaxn=yDLo%z^d zmFNGi-C&>A`q|OY0oU%|P~W0janzd9({>efIroINyS8b&X(Yz2*L-<vCFDi3yy|;Z zTCmJlaalUu?RFeiuzccW6Zih6gOOVxj`)2K_x!7{SUgCI62-*L&U1E?wMkX@WuqBV z6r90egIefY))?mw7>x))^!5jHHk1U$aEV+Fb>{Edo2Tvrg2$G)41hSdO)2Xe`i8I5 zk@`9Zk&oK`q)cuQR6&b=cPsmRtwt*FFf0@#rLoAk%<Vf!$orn5X&3^$A?SSRToqSk zwK7OP@XvN69SKYXW@LeAf5zEk$ZR(36384@S%2baSx;7>&r_){G0a(sIu89Y1)zJl z4dnPbZPiA|5Yw-c4v6!7M_gpP;TD;xf_T7FUy9PPk_2TaF3$#+zfy@SYvS{R`%<@% z`h7#{83zj0UnzoiN3Z&ciyTB`JdvbhWPpkp{0s9))GHN41EX6w>=YWrbpXS_Xw6#B zzw=kOU?P%J!C0_>j3i{wg@Pm|>z)IPqAZXsDW&+BRaZTsuKX=?xvGnWQufdM(*qqL zx|;yS9o#usk;5iZYR5_Ln+m9J9EvA%M@()za+uj?>Pfa=(!(mvixE!N<~I-r2J*4g z+gA-HaNj@Ahg2S;as;)>WIGnilEN!8E$ZyXQ#gTfYhmC^GQ-5-9k!wB$e3`UCN|vZ ze>D>-cg~Fu)9vM965ku^<`f$ry(>zL&)un4lPHM_uoY+@i6<KyMiZ5ofX%QMDgVZo zPU@jCRzp^~j^=mJ+(EQoX-gH%yq;Z8;K%QVH~CIRNiE?M&PFAju2m4MC>g;tRw^kd z5#Jz5Uh*WydkY+ojOg!_kFP-MetS#x%_j?Vcem?cE*Upk=L%Wi&cN)hPG`RW*&*g) z>*4*BLIaF`3)Zydk;xkqvVi<JxU}8|UrtwL@C!zZu*|nEv@H4b=B=>wzyu~G<TMJ? z>`t)E?jDS>v|;1ITq0#<GLF>tj<V6d>Izl6R2@gvMn`z6!0QE_w#?eslcGyaA*rN; z-F69h^l;dB!Ua?m9O&Iq1+yJcuwL3cnJiQm0=xmKRk*~GiZMZpFOpe{ZYf!YsEJR< z2CEi>(yDlqsizzAo%l`UNqOP+;s)Rc#o%u0O=xxb_360RsY~YfCe^STm$){2$3wz$ z0#%|1l%Xw^DO;hWKflDBuD@MQrDABq!X09j0`9`j8Q#apLngI-shEsBExkon)k1hj zMt-IyCb<x2;iI2prjZsEBJ)GqOZ>X3C2FL~!NRE^roOqP&lDbST3odt((<Zs*zEeX zYi>p}nUX%AL1yJ@Z7De{rK3=1tgCLf-2#p(%+W_lEhN%a1lu*%`fC_!Od0&1#{6zI zsYnmkBVv!de+HEeCLe1>vgvpkX<SXGwV^c%NAY{hENs(>W{3EB!ltz;3YYHhNzNP9 z!elm}BgQk!NK18)+ghw-QN3jMQRlFU-8ymQ_;mbRQ8B*kgZh+=0W{etdK0kOBWtpL z5%86<Bs~&tDoLNAw!_Ktr-#CF$|1XVcW7D|>6#Dt-N83|VP>bwHFRd*-}Fku&a)p6 zzgMg%g-njn2)8n@K~@z*ri5{1w_hF>s$V-KB)=tpmwjci&jnXMea}l8(OaHsy7ero zG3lidIqguvdOIxC-1+{0&(3_(aRsaa3I7WApGn^lpg?E$UxiteqG1OR3Gv(hO%sNu zuGA~#$7>4l_1r?pO<$rVG@sE}t8Q^LpJL3WLGRnRt2f)!EH>qiVyARGffG24RuH&q zsQ?R_(P35}gYtFN%z0lWSm$ynz0$%jf^y1(VQG>HzJw(ge3qX^kQ~=d>NQx}Hw*a& z$h=?S((+gwlQc6jUaZlLjEi4}i%W}36Ia*MQ1eVZSt@?XDrHERy$tiZFe6Hs!g3_f z+zF=?G$SsQRkN0@$j~bTKg^cQs814i>@0=KS?&#(k(0rYJ?%ywq-!kjX+sH(d}Gs$ zl7Fq;`3_S}Jf~sn_>mDm-h^E0-Ro6NR=;u=K<2#yh&8aEY8a%@#OLDEQhjX*NLws) zsr3XY?K>FRYQ7MlFewKK9oFRQI~+xm7egB$o`&54gosJzh|;h>Dq!62>LfasvId;L zUX)5zx33AVN*2VtR?sdL(yukXwh+icX?b1mI>2LY-HgtK#{ZZ*a8vFiShUB_F^;ph zqGglLIAYJ!-`y8_!m}ZeE}Ct@gmP9h=91!CFD^N(Cf?KF%E_xOR@G}cl_AP{G?|Ce z(lQhOE|Ecc$iQg2ma>E=cA2bJlePB9W9JiJ`#5MhZ%>M0hEznEPtN+uZAb2>dEVTf zb+98kXV{d`+2zC`#KCIN;>%lkXS!f(D}D@cZR`mhw<zU3OvOmhz;5?&lhx+ylA;<& zxVC^Q9m&|zRH%d4)}|Cl`ng7({M96DyoHJT44f-(!`X)VCXc<Wdn#0!^n^djV8Sb( z>4~VNGDu|pT>8+0g7@pUIHK<pbytG*v{4A^WVAnZvAGO_SK(z<SyMzwSBaFu!}HIM zDeUtRj{L?j+FHeuq1G4+WS3~C5xB<E4$()fxJ;jVMF7Of38`G2wiQIe`iNsfShT@* z5lWBY<n>A0s(I=D*r7HL2q?q0gipx1?~K>!Arfe@59!CR>GvtZc$F=wYQYg3wa9rJ zSD}{QI3f)`C6B8DZz~mT<+R?_c50^FS}hEEQ0{~VuL8V&9s>Pv+w<1`?d<2a+l8>s zYkU>xg0Ub**lu0C8FhL6;$eI#@3GRVgH3RF?bkOl({jEx<%Bpagh;>}DD-{_a)ol7 zjanpl5%0VXw1PtWrq0`E4>?c5^opAIw$lcZDTZ<T7kfhcOo>SZ5ULvhRP!G!p8qkV znN-`h14y|0PU|@!BRDIjOLh{f_)%)Hp}Ej#atA5j?2s-GPt-+UeA%rz$|S&n!P3dI zem*dKd*5_BYMAx_mQou;XCL?L+kw)igfa^@(7Qkj#Sk4^PGf%WNRS9w5)B{Mi(i^= zbvDllplehe29i>eCZgZ9ac~*Zk4aNXfkKP3ouHkh;tHAI;tiRlR0)rEK4B&!F=;L^ zo4t$QsWT|X#foD75lOg$nH)&N_CssO!^8yfOA}WMeNdXvKqfO$DT8^Cp#|iKG$c9< ztbvL-L&|d-f7;A4l$^FY-8Yf&5DFq`<2L8UkEk|Mm|Z+4P^yX>MD#TWvNXtD#W^wi zeO|0rQG)=6346Oy;6y4?NzIYMh9*IKW*;Yyui*4Lj8=muFTV^uc{F6tK01*i_r^N0 zzFG|)c(Y|pPi4%S(8;1#jCE?<=b>MfVA!G4ZJCfvnT<pynp!W0ove1e7%#$thwCo^ zV;4;5ztZ)MPZlr<E1-hbtjmK_*^X9eJNbj?K$v>t)kf=UY3=q&^;|=S)T~`sSrx^y z{ethEO81H5o<UEOsK?p2oL^dMpd*!2jtG*~ku1jZZN%xRp37%jJCo~~O{uQpvGk&g z44gT>9_<<jdE2tO+tq2&v~S5dTO^Zj$po;}d=2d(D83)P(>Hl}oa4lRFrCS-+o#QS zxre=*+dPEqz;lO=g~yoamCT@t&i#hzECZEyL@i^$P;m^1_+}vZquvZ^5VkO3AcA@R zRTGZR|K;?EIVh5f^?uBEfHnYaa$&odZY4-U0!(IS7%mGNyL++}UxA1)7aSQA4N8fL zR3_kdXS`6@xBxm$lyS0&#;(jMEsVHJm4Y#_88L=E96I{Jh>TMu&J3S!nC{1%lOhgt zwfV$?yARHu(f!Cskmgl!BiS?l@{kVh$FHz=q>9Trx?gw-Or=9IwysWWd6S(kqJi6r z6+a%f2%bpiy6u<nuGW?(MsUEn+wlGNrY^iXuQwCd2Egyt)@}(7^uGN#)#$WUo7eu% z)WJ~28}Jmi@Trl&hdld9befDHUL?P_(;Thmv{iB1Z|xvInMD)+5S3$}YSoA9YBZaE z&hzo~D)#&rfS9Bz+=2<{k3|6trhi-!b~Lef{BIPa{71wT7JxE2p93xhXs+l#)eTe| zVR9%a`Wuzhe@*xKjaqYYD5%egkvQTd$L#4iZ2+1^aSJlT35M?e=mf`rT2!uSW)#Gc z>8|DPTfnPf%5CT0sT~K8AU1(yNe1ExuXjvxSUMo*%$2X=MEFbu5^x^RvcJRzJs&L| zllV4o5Xy@wls%X#BfPxb_Zy`Xye6~Q!k4QOgM)O`^@x$ligVHavIDMQt@u;dQXl)# z)4WYYA+VP0snmB&r(=8)<0|st>z+Z?Y5QOy6x#6|t`s110k5*5fN9<;RN6_k>kz9F z&l94Lj#rnM`%@Rae{ZKA@`7fXo0=p)T{CKVA)?FpthVbNNwb`PEfLFY2Y(vA`b$X8 zth5~TkJith8ieG3?|dCioa|g2js8bfZ*Eo=V6p?g<CUHNgEr0U3%zulzm;+A7na$f zIA}0{?TyW9u^a;kIr=c#Z_eLZYf6x^{;$<gG(dT57|^6LI=f_$h(jJvwwM~hg`nz? z^wLk6yP8N-=;-QIsv$@(_8I-Xfk4u_wEU^ej@o|6Gzv(PNmRA_(WCZHHb9q`Z)ETn z<{o)1e8<cZTiK1Ec#>9M8>C=32R<Kx6BnRJTDHf~T)k}0hpts>)YrqS1Tt5q63G>C zt@iXWf@A3OAbfb;=(XI=LKBaV(AjE3i$V(I3c7^J*hi!^)WOXOW(%W<8-IW1{}R`c z*<y^C5*+7qC1n2LP3}mYRGY8(HE)`V$wROydvJ>;+s9%!qfJ3WGDS|p7+}jzg;joE zRcAyl*B%%D@mhZZe|O(El7cJiZavgoZ7YyKVC{-#bI|njvF`VAaow5MK+(gFb#710 z2LC`P<LdqOUm_XR@wC%_Owif^!V>?O2sxS>aWelm5d!qqu>fVT?mHUVgZg-RcrKOr z$J)&^)vEST{Bme49#ER$;>VewkJlKKP!3}i_`d9`jIqwuTDMB25l5r6$p*PMUa!|B zeFN5Yy8@X;gPb;yzT?ZV(U{ymM^cG3Z8Fjo5d~YBvD&giK4uNoVPdaYnTc*y%IHJt z2<D+IBfVZO-yosE<FSfBGo5Mpok1lqUMR;%at1$`T3cedX!Eamewk$93&@H4WB%ht z_4rSflF{dB7p@%_eV@&e=u?U?cIV|wVkRM*1PhwF-f>yRQ%secGZa#V{Fy5h>g_m~ z+Nz2JK>{8FY&<L`P@ch#p!<@^xD5g0^H1ca=|5QRwKYS9f>pW?ju2|elU7JNRg(_A za^4H~f2X5HB!yboR8@`i1*^M_p(I(~amyhuTp~b0edrvLh6CeaI|0W7h08bla93QJ ziZi_psPG;@%g4edw8g!)Ir7Mj6z<S-2{arUnzA~UZF&;!r)5oprsZVCTgBbY3n{?+ z)f(#xs_6Z`XtvY_OWQB!$VxuZ+4Ajl`>}krnO2aui|o_YKPQMg?g%C6yv!KC75o5q zusD2SJY(Lvqusd|k`-z(K%cv`0C^!jVf7|V*V~_@yiFFDUR@XvhNuNCR@M-(o~ls& z-ZvDr6vq{M-g=NFedxPP4!enZ#?VQ|@EcUUIt93>9Q<1ca7|5au1Cd3D{xnL!Fi8| z^Gx8n7D~wJyP95QH6rr{HO^X;>sQxUs%lvREDvcc2A0?6{)22Xxw2BEoo>dHNf6H7 zVSW!mjTp9%zrMqkNT;`dyh#oL5BWbn8Fq$t0Pz+_CzJm^$1X7f(1J_=U+!#TDX<eD z&o9frz$C3pG{v<ne*LZ@KufYyCF&f@psbWE@x64V3?M-my2eRQjFT|f<uk5`>nv&W zP{iSun`%Q~?X@8Cy=*yZP*au+tPUn=i8Hu)b(Q|U1Cvg7uV>MuSZ7+ro2-4kc>LGG z^VW#gCjbk^1B4XeKNjv{Ywz(VgH*}?{G)8v`9$-mPKG)ShVc8#%`X|!Lh@+;$v{%N z$19uH{9JcSc>nW>bLrw6RiX1NX5=K-C9zUzxt-Je`VfN@-uU1z_vDQo>Rb7gESNFP znb^f5Y0p1N^Ug9N!6{WlMI`SCYl$k3<pmW`aZ(r+p3@%Tx;bcKCVnz1rC1BmFH-i= zFm%kL))=jPOfxe-LMPa7V}EstWB4Rx)Rb&w8<4NA+6vW1;H03#a#2eBW)MQ9a17yL zLnphp>Q?7M+kNYNvH!K^^DpTCYNhx_8n6o;F#k-YPA1O(VLDB!GO=ra2peiq3j+a3 zj>kx4$!lHC2<xhAjTk|+iPGp5F&AsamLEPNh=e(ad?|?zTR7ih&9Z;P>%N;F5H6$R zdO{=;6zN%k5fm%fx&U>v3U&ohMm6K}RWOH$(Ua*(MD5bJooybE6;wrcHZ$?qd8c|F zk_UzDcH9Wk&>M+M2KI0Y_)@2b6Q}f!{NCU9CqtMI>&pG{i(@X7&ip2ryRZ#RDrMxL zzsIC^yc5fiTgV_^+3_fsSnjm*ga`Q5Bo)L+5Ndj55XlDm>RTU2l|6jJYGp`#=_z8K zH;%#V%GW`U^^lnbtRTW@mr=ev0VkXc^x}Zw+~3$#n0T{sJnt-^x)yZn=D~ZjW$Xv& z+@U(BEv=ii1!=V^`$)&Iv-gaKl(jQ=s?l`j^GplWCLYA~%~`37d{#i<FB$Slzwdoc zviDhrRa_&#tWj%*mVK)UoVrMg``%*GpDg0P6_I2@JUyE)c~d;tI(drDTcDCDAyuAy zB-kc|kvj-1qz3ooZ%>---MGqOv-q0_CFai1w6@4KC&r*lcPQbbyop|41t431+A#%R zy;=M5JB!75n<;F3|0op4OrfTeOb4}bE_brV!e2M1rmzXMD-GTOBG3>g(IIwn-)*hd z?SXr3-U2O@{XF>w{LooQ@zPXAt)#GzTXhBUrA*ipJIj6dn%0UNPYS#?#df=g6FVE~ zha)#%=j>r=l1q*3$lw+swp>xV)jjWzup_2U<2JPV^Vq%wlKgMF2OvVd4u|}z+f`|i z=f{8bRN{;9(K&!nL<5u{{~(0{Y}Nk+WN^&?@fYVB^2<PHh2ansh$Rewh<lgLFrP@0 zjaZt`XwG##pxu7rJ*?Wv^qiDef+!*4il|NI)-Ul(gwq|!vL=v+L_xiSgq~AlEvp3G zZ%`N7)=aL|hkd|SURG{fPp;+ajVD$mEP8KU;uy7Xq#((V>f61mF)GFLtyN+{Cy9Kf zbnF}P06!ONymuyaGWr~!_m(#Fb$bE-ixoxBwz*XYcr7LXuJDiK?PP9Z?fPF`HL3j5 z_Rr_$h~^P8;U|Q!Ws#~jb@JoP7Geah8Hj2$-YR@>&(#_zvLI!tV$mU@$;E2cs+y$L z(vDm5f>hkuxH>|t=ZHRau-WzbtfL)mAjZk4Z$&3-It@61OA(_5Pmlt%NUNhn0_0Fu z;1gQ8<3d~7gypnTFpDl&vqxdxvSB7UznnjraoWP1+3ILKOJIO-p@L+jsl3J>9GGmZ z<Dudk*v5b{v%$4V(MV?GG^(S~!tOJQTWK&=*jKQliw|#DH-u|T*5ba?)7%LhJS3;m z!Axz868(sCO?^sb`Ho{T<<bryiCZeeAsx}Y^o0`|xEE3D(E<Wl%!{^#OZ7?`e5re0 zLYSjYRX)uu&$vZ161(zI&s6I#SM)nr)~)4r4|wH1{-?~VaUiF<Ex)d_@|v?|^W#TY zM4YYL%tMZt^W=vOq+E%DtFxWL)-9Nvx2>lWX^LNbgMYmdTtYT+Dv4er`j#r(ARsN7 z)SN76>@BsNd_q>y+7^~=Mqevxx>=c1Kd_n4$6Dx0YU+0nXQVrR8KF)nb8pG*v6A~P z4_s9yANvti`QtwO>90x}{sB$J7w~`of$#p2T>$Dh|Az&q)CUmLgY9~v@ir1SAdt5Z z$BjqC<=37;O<vL=nd2laTlvTyq_fHZ;#GdQySr=1XpHfMq^OtxQnnidYh|GTdXfXi zPeK6Bww5=hrrS9%zG-q4nbn8cvP5I=<~iE-w)X;Jd*Dq+M7a{icCFpC1yjF0Z4Ij4 zJxsx|*GS9FkDEqdVB&pP)h>FL94zl+<=(d7(Zvv{qPg-fsP}s&<xr$92uQ+-ZAZ(M zRvs#6*u%AR$o?IRV4_)c1N^bMMS_K!MYjg(tdxPA@XfS1mTi8Ai81nScJixFFCmCe zZW>nwZ}qC{^=1G)f4#o<?_HLut^HRFd>**!;h1A1MVSgFmDo?JhU7ZO@^SX8vG)#H zDc7>0N(oaL;iFLryb{3{#*Sk`{tD6>Y4t{)=?TgCS>dC$xH<+$nN1a%(#UVC>*d2( zLV9YhNZ;X6xjeeZb~Pk#eXwFNOX?FxS6U;_C1s@C3E+9~LM>E<oF;nOr8(T3g>2|= z|FY+EpFlA{0OUiG|39&6VP^Y3o|eH$>oI^rJLJj(TB^&Sh;{A}xhz>7IJ^n8lu2UE zJh-!uv^3K=cedN7Phy;T%90_UX#3siI*JHQ#ec2kD@`mV&8cMLsG-M{t`nb-=?lA1 zV@2pu=|s7rRE^`-#zKrfSPAcJBO=3I@E$fLjn+Nh<dIYO^_*8MtMttpIaA$a_<aON z>dbL91z|Uf#Qwu7Mee5^9}+Y+qfR~eaT&wMdkoP%m`ww7*;KT%7!hQx>LoC7@-u#= zO%0I)FixBb>X!f=PT_N|?f`wuN^x&W4^<@y^%Kf#6*SBbzh=<59HbC1D4h)MWCMiS z8C_e#d_G+1XBhgBwQ)x(CIei!vVhV#kRo4DdEu0}pnV0A?JsLH+%2||yjEZ<Q_%X8 zStuXb+GjVmnn|s7XzMs$?cQStMXEk)L}oI9_8Dl}D}u^`Bi)S%!y<?^(ssLRg$QwW zFCUL!D_3u<4L4}t(}-%QAxPT1ml}gg!VaWSU*!c;eEIdh!#)DsA8|d_jYkp=UWbx& zn5gaGqHfb&;FgP=N}0c6Z<*ebzP=>k$qpAKh7go9TfinAqFI@EX}_DiplOp6HH_7) zeo!YKn#>g@yx)~}7d=mWTvx};3(F-Ukk903HIucsg9%|e)))BeR#5l29eM?3<e|_c zX2R@!KJ%rTd>ZL&4g1J@N5{7f{~GX(c9_f!{%fY+-~Ri(7;u^{q5gS$+R5hs6WqyC z{U3ACBemnyFB;$Z{a`7V;JKX#X3ptLnX}B3MO5-TOnZD2=!OXD=8L#6*<ZwY`4XKa zL(vKfR7|IpO=gig*hYo~Eq8g>eLf%VK4b#Of=xZzwQN=<CuI;CK%Ss4Wj(@&{YWWO z0%uTXBHcmlX5EkLV}a3W-@Qdrq-gc$Yrm%Taye(ow02!(LsMcMjZ^;eJ`wm~g|1Yo zaR4ZRPgvgk!KWZEdQo@q^D2D|zl+{{RqU2{MoKtC<(7LyDy)Zwec{_>Qg9R0>C1;n z|Mnpp*BZMxb;4>H%V49XjdBaesvFo-b%H2PLzVybdsKw>cabQt0;(+*!<dsJYTmd> zXH>h})3plEfLamJL*VlB{ab!1oiVBTa67*M4G31nM?d?sno!chvDWAdqYd(D*_M?% z*ro#&$<O(DqOfrt`9hXHqEx}@Xj&n8{~ds3DS<MTr3_DX)Rbg7aHUrWdBhLGQg|-z z4pA!BZ@CmC)UA}dpf;%#Bh7W!Y*w+#A@v5S*6S<CiK1f`o4klutWu#aA=w}J2X}FX zYfii&>nKg(KQNBCdUPF0N~$DOSMe^FCgWs{S3$UHUstXI&S|bc9S70HYbzHak+wfg z1_gp_-ANMV%}?GDGcs9NwGqFU!v>m{oF~h_z%_N$aj{3;89Q2(^-R(mWDwca-P|!c zhuEc4$F?QZ68FC0?=L2g7-;oour*xM(u<8`NB|JTFJP!jXjU5)nKahV^0jb#9}qfB zmvqQ6;%`AdAx5~b&3I&-<8ml;t4d9<pCIo0xa1HSI62)SNP}L$)i^#corPu@)ZDt3 zA}lyjp;c?=XN!mZt`bcyA==v=Z$_M1^EgJTZBjo;7q1aZpI&lm77<V~R&)ur#QyFf z)J0#d{*-R4fSmV_ah#LAoAIC8O(nJu8X$E0#{mNujh!Gjl(%oiqJ@cc;S|~iPDc3Y z@oYmFXLR@TE_nZH9+Q7-|JN7#nN-l~m;+D_*yawo{VK&_5|-M+G4Y?QZi{?XO##}{ zHiNl6GsP@w=F^6{f#4V>4FQow!$mVJ#mM$Z(Wo&xqhwm@T_c}Zi>vtHG}6aDDg!U| z#a_7iJZu1`r9du`<n`*B8XSn{5#PW1BSE>7I88tVqXT5KDF63P5nw*AXJlvV<ZNL3 zKaG-)vQ9+?BWCxL8mOy-CTB%$hJUR<dD&!)4C!z~jDp}%T~%e32wvr<Z#cG`(){M} z9p9DN*6|$>=|K+lPhNZTbg7HBOM`D6+8II~*~yDrlV8s-BaAM6(|ueEe*Mr=Ya1PR z+O(dQp)0^MQ%k0p(PkL(K!}(xs>DV_)wqMr&rgx_?b@$dA9qk?9PEaOa3a}F&%+eh zN|SZnSYjKkD6b?xwoB1V&*zcjWz3kwH3M=;V&3;}FUur-@JS-g0vd}2g2WW*iYE=b z(V~T1bp6cUo6!?YA+%u;e>VyJotJ>X1x<C9feA%aU@<Y%-v|m1wO9a8s<{{RL}HY` zElNucvzBLfLVYP8#AV3H3`=4Gwi+pFm4{J&kz~I?QDe+8KzD(|A&|#o9!rHGx`)$m zPLF|Z>8{j15RkCjeaN)WoV5{qi%)Wc8=Q(L-OohQu?{TKpN1r-gv{@wCcnvrl^6!v zvQk+U)=`f1Fd~9#QI(BsO))b2I*$wl&(yQGFlFP7EJUDTX!Gti4lGp~tje9r0;)&5 z8|!Te)~tA8TB);uZEbeCWc01(c6F-Q3I7GZ@pIZZcB*Xt@;zo}CsMU}EOAFp>4<LI zfBw<!gS^SkF=_6o3HVv*dJ@(9ea$kTH9Ps2ji*Jy&9z*Ut;U-M+e#^dS+eLYhRwH` zIwf0dQPo)7)snuI0|o*ziRpsEAfmc3eM(NvA%zNL$y$i^jIGeIE}<6Y(F_zUZ+?%8 zHgK*e<Jy|A$&*qoze9`XDJnKJNza?t7~H~1eFzh^^S|1rZ}zbXB!D_o0+3<;aa-lj z`TTDJ^CtkKRkb45_%VH^YcP)^Vaf#cp#9MmIb6WBM3gku1;fV7m^r6l(;PY;m)&gJ zuxyqgB*>34ub3$S!YXTZPFX^J)KqPdrVZtpw~>{YS@rFi2alevy`ZK9#RO(FojKvy zirI_6sB?GlY}`a1yQwLVIh++_&?iG;v7A5nG$l(Lc!3D2YUQ7{nbXwxG@Zh+Cc|Rc z)07b-nEe&qE(-7{&AkuPp8}~1k1p6}y|Jq2UX)rl!Nf<`)<rWYquyv@e-1ILE=PP; zCWv=F@ZeYRiu8zDealI4Xj~7vD{aBNINT*yWYigNm~-P=m6A3|aKB@WK|i~2gk<6R z_SAdIfK}_lQ4REZ<f9WD>7Np_=fO1=WS)-ZF8&*yltv^sCqrx4A6#`={$5j~f#u3z z!Y8j#4Cr2|f5ia-typA)a>s8DRf%@qnb;2(f97ran7d${dH$qv==fN4)6ZMD0VS1J zAcu9THt8@f357g&nz|0oheBLez_&AUn?;m%>u!#1JL_qqnWo!b-eSd;V~RzFd8p%C zu-k<Ut54p5^)6ypc}#@=Aey(Z4m(l}$Ev0%QTGDYWQBie_0noSMaj%=<bZ}!(<>h~ zq)UxvjvfD=;>2yO0QFoZ8e^DAf-6)b{`o=`Tvmt=(4?#hvy-SbSyi9!o}elawsoJ) zkC*7m?^@MI<#jR<cV$eF{uqSA#Gl56R6uJS&!goQnqT^mWo-gZ+5DG#UB=Y~5i^Ax zIfS8ZFMwT}?CPtZtqg8N4G0b-iI0=;LnKq7OHK5QCt7v1hZoQEI1kq;%W;O`IXo|; z&>KkP-ytl|SWFmP2A$cRRJQPxeRCWr!V~$T(;F))+0{jO41I0V<4Kf(bS|rk9&ibM zks{6kd7&8hwh=@5?b&mTD9y5Xk=p|)%dVZ*h~^EUwhn#{9EbB-j6A}Haotj!rkyFe zeXOv*Y*L!TKUDj4l^KA}W6EY4*W>tg8LYw{kUF3^FfuL3ThB`rC8j|>;Tb7fp;+)y zP59d)TY$I)_t8w7&q|D;&j$EdCHMOFI9nFuXde9~l?jVB8cz*KZo7a$`&WW<Huz5g zCpdoE7LeeEuY90m)(v?05fUkx5R>0EGUXRw=7cF^)`m^4YSN;I^OT0DKDf5cxNqD& ze%i-T7J5Br4*%ry`h4}B$j)w9ws>7J^T@s$7ExkfPZ#pqSl}$>U`wu?C@x)GpPWg0 z5>ZQL2DTM;u7*|_-&Q)GL2wG*uD~i}XF{JDQg2ZtAOWK?)TUq>XZb<hsVr?NVj{@8 z^R8H|O2Pkail4?Xw~J*a0LvT*Db9?nRO#58pfBsS7cs6POYvBV5Mlbz(rh(RFb5cL zVcg7nARF}+lz$$06$I?t?tbBsT@e2GH^{}@XvlHVBuOwmT+Ohwe$5%;{JJ42zd`(j zWTkx8@vr=#&8&)$Gg4mE$K#@|%U<uxD9gMz%}`|W80a_+osWOHpTK19;$#7~*b@M& z6a6FCIy+j}{^zANq^1|S$By{@sRVIjzyw1N_#z{(h_1-q-VMMKpGS@&jy1ADm7s{` zID5Ot8*??JST$|{odPXLn|MEr6)m`daE*QFZ<ymeoR#R3PCz(kxXjZRZGfHy2tXPH z<*b3z6aefr&3a_Nr%|=pZ?H9}T50ZTl1ZFQq#GE85A3gDhn~xA7Vf&l(wpQU;p$O} zuVS1qndr5_)l+pe{DDGD4zV(sJH=Ody=w$ukbV6ykbO&SX5QrzjT7t{3MbGA1zu@6 zw;Lw@=q!dztgR6>#7Tu`se&;GIR>?cnt;NK*i?Zn@wjg>QKDh~vKr$p$dDv<|0yCy z0hT0f-@#OUjnaSKu;2Fx`W7uyGLQ3k@)Q#fmfn24dwJCng(Y?=zotCEsyCnQ1IvBQ zL|pR%J>zge;h`^@K_!HOfi(MMFOXS46^PQ5(vOM=Cb<TQ8$RkUK90`B9^tV=u0R6k zk_LVtM67lTAuXu;%7bf6IGUn}LE@~(5Q6F$3SvYBqLaybSsf$pe(k73P=i1vU3F6) zcC&FV`FecmQ9+tb`<7(+Ocrs01GBkThn9t)=;Sj>sVOOMCVO1eDkplXz5}|(F{FD+ z2bVi?tZfxL?&Yt1yz7dYQ`&lGH{*P@<lXZtWPL_yT3PMfJHjZ*ihH}6sig9Y<=sr$ zJ&9LAF(&sjmegt4V?qA<C~$>3I8_s6Q9xSD{;eb@Fe`kPFUL$Qp%?Cv`xkvQ9a|x| zf(X1kg9ei0R_EI{2vf)m$f2&1x9>Isx~5PC5MI<%U=sUaZyxkbgFHFz@+R#R=y^R@ zaCG)FF9if_&5#R^Jd%|{i^5SZ+&RV<GdC;IG_iFd-aH1pRx`Al-dh~O>+6ff*J8FC zG%E-Scus`32s|r?gtW4+y;7s!J&EaEe>t<u9_D2flk1V;!9LVzV3lOxAXgQd-8W3p zmb$UBcB0-Nwx`ZERz?xkq%&R!GF?DF5bQ1SOvIuzxC8rhtg;s}e=Lg3A$xFhyYZf+ zbQIa)!Bq^ff+Za_FbZ(2PfhJor<N%AJU5avvr8Hv^4cyc<^-wPh-O`EqLkd*7@X_g zy<j>)rH-sujKH1oNZ0)Ub)-kt#7*}(EWaGk5SU+9I*+wsB53d_BUA8KKWM5x(V|93 z&l;lRWvo5wrNPGy|FF@(eCpC+i(w};C9~>TewAq^bk*|4%$s$#*Auh&eENMfi@E<- zCtRI=R00X8qwxR$=D+Hc|6<$D`oBh@N~JI2&lQR{8qlu7owXS$q8UL1UQ}RS>k|{G zNW<v8RqBMeVb0Z>D|J_FvNIXMv-Qr|jFPl?R!<6i>oXelh>+1>6AkwDe!SAU;#yxV z)j*x4(!6(dZ)&wxDi9A=tqL@jhPCyq+(4t)-mHe~K{Y3L!kow}P+eQreZydX$D08p zO%fM+Uwm9+jb%as`}*x{HWnN1>tt9G>ny1}aKkRfFTq-cv$py<f?5jDIr0~UQWuN2 zcI2@>hOq#7Ff()1^PFC<vmvt^sZ;ds2f{?-8@TVkL-gcc8l(m24C7&BZI9=?Zgzi~ z(~Bse#-q^m^g(4g`#9OoK38tQ#dL@6UlD<o#LH%CgW8MrPU>sYyE+nBNco3=n=M3$ zG087uM6d;@8mMlXMViiP)TrOP%f(hgy^L9UMKd*hDo420)YP!4YYZ`vH)f#5-&+{8 zeJP-aF-gK*eosiY`G5F2r!K*kZCj^p+qNog+qP}nwr$(CD{b30D=T$#?{i+xYVEFv z`3Gi18xhgR*GGq)u2gqQd{pJ>GW;Gx9uO~YNjXK~-gH7XTX9kFVHVVClG!<P!k*Dp z4U5dgNOHn&sl__KZMX0x%YlL^VZO)<(e=D(o*}`1k;M{mbYg|~jHa*ibjOs|&CDkq zslXiNhb^k{|Hk-24{Z@9%T}SoO<d~Uj4>D+W=E@?sTm*3`G93y7WwWP-3~Lpp7+g3 z1uuIemti}^{|f@+Oy1w*c%*l^hW~##Nw%L+jsM{kYJcF5e^XbT9qs;ixZ;5A5W#=w zk815q2@T%-g%{ASXZGtoki|ws{XL59USE*;(wbD9)?CBG--pNR@~OwMVQ9q*APb&* zzyoCUZMMKYs6nNrG?Rt#J1_luvKc((I~uchg+QzIMY93YVj>0C;=M!sTHzogds`-) zcqmHtYscLPB!KG^P>X}W+`K_cXdciqi`f%YX;$XQDNHbO$OQXFt|2Q`<~Gy5p>Ag= zMXI~XAsPZ7y8KuA>}pmJLSB&=JbLx3{G!#CMMXly7@P7UV871Ar57cY?YmwjLzG3g zM25RmN*5ZeHl0>=iI=sGh|<*4NTj(+h-e3QU&`jyn>FN8+eaMZ@Q3ka%8PI1de=eg z$;%F-b3Pk2kH+TGi}2zGEJmto-@DdtR{iKDGT<j<>8i48bs9fIiC$0rgfAt<oEJlM zL5k@tj@G}}cQYMn#F3TU`0-W4NO^Gja_|4)ooH&Q?&JSi6=e8-wGlhH7}`798JYY~ zm`BbJ9=E}X@`J}2RKOQ0f-Ss+HcRxx*NWo`kVI1xqGSyFFAvXHm*KwF_5F550%D1e z1t;L;^Y-qY&CB6JDK{jZPlYCllA&{#E_Tl5dV)_S5y&p+QGBy!5O=3}e-pUoxO8g3 z_QouBCa!0PiFNqwnNSSY2s9l<^btW?;F?jrgQ*abU5A^eITb9(4cbGF-vu>bbeIR` zYCj{NX^D#xWtX`p3%Rc!d!8M3n-5f&(LOqb@^!c<G^0XwkLUaQ9-pMA-hKvdAUwC` za)V3mna%BWmKCEd!x1E%OqpW~id?%Yi}HoN0+$s;v_J+jd3M^~YR=YHyTW~;Z}+## zmC~v%5hKl0gj&lO6Z{(1UK9b1E23VQITv{)j%cDhl8Bs83q=UnbAwm76JgbP(wlWe zb>VwVo*snk_Vpc>_)X|Q@O%bVp`%#|yaH*WUzPP_a17dIL^9aC=_K%{lS*q`H>Xvz z0=D)X;+E|wQD_4+xoMxPJJUjLv0)gsn`j^d1U+JczebH>xzEvp65WeL=zzuekg6!E zDJ8F74%EN-P@p?(f*Yu>Gt4@IRf-ks_4L-|i**71yRd`&UTP`U0^FGEU=#3|PTfu5 z!_J<b%MJL`@r4?P;?Y@OaJ@~HcTLVrRXL_(Af~a`7N(;T48TL$(FAove?=dK`#7X? zv_iJ}Vya4Z>S8pj&q!A7#@S$2?2l8H0RD-*8QS(<0*9R>*)$pR>{<>1&U@6{m)ik~ zl!d2sVg^qo4`KV6N2pf(o3FVksxWHc2zP@sqmdiMA&?+^s(~rZ43g4)htRf^($Ypc ztyoyA>~9zgAhnMhiA397jYkd2TNe@ZdBMMy3r&GoHj8Nip5L^PR3umY$tW7Gb8M)A zx()f6i5rOoBMPB<;oI5fX15p=B@Du-g-Djd9v<fbQlQexy5gRBg?CBN!W8Nc)5;Ed zfQF9G6Yo%Sb~=UfHw~Sx?bT}!t0@_I)z=HQu6kKKW#fq3PQ%!L0HOeM*;CR!d&t;z zo=Pd%wqJHV<f5CkA9-!&WnLF!b=>Tr)NmaO^lSEUQLBUeefayLy>WE}erxLRRT_~q zsmeLX4A)&`tfP_|Ou|+gv5-5#p;OeLk2d5&9Z1bi+`}rlI9JPQ+Srz(rcZ5-Tu75{ zIjyU&Mi}Lq%;jNF8-_q9=0V8-hrA!c97_o}si;Uws3S>q2JJDG_%QfzBvn$0A|@!P zqRt;Y31)3up5?HX-d#L_q9`9i%l3u+PQ$Jk-U#l;M=r<$yBIknB9=}VrXULmV0+78 z-X16LP7jM(H~xD@TxggMJd<b6ltVjsc5cAw`*P(_AhxgWX}6X&`}uZ$#^jk9ZH1n` z&!`E4$F5bkH~4TwpO$pKrl&b+e!~;2%krE$Kb8mPo6^q6oApji4^fEk<j2^w^3S=u zP$ikG(?5rU_Rlnfe|tiB*cjUVH$l-Rc_4O+;75$~h!pQ>R;0gfIQ8+D<WW@-4j)&S z0x&Q$dwluDMUrkouh5%^Umvr-_$TT$Llpf{{5Pu@pYNB)9c}3Sdzyhghb=Ka<PhgU zs^Q5!C@nm2#DT$Jyz3XE;ogA3?44=%38?i{sFAPW1x9M;2Hq9W4dA$O6X4Lf5AqDP zN3qkbvxkl))4l7L#2r#D66;})p}U+$!Md7IuvIyr@mu9S=+hR7YRJs*u0f6<4;5=8 zeV8sdGT>VdihcgOC{_#-o_z<DZzzF@CCK9s&VhI)+Jut5o|X4AzfoOlB{h7rMRe3^ zIx0kGg-wdAYXP%J*@4f{?E;A;ssnFx`iE=G1OqPOhIseJoh#*{5}nXXNq+54Q=-F4 zl#(wI+oNe#J)P3f#)pi2vyib!$wo5bkvaFsik98OTOBlOWi)GHHs@+Fr}4&yt@zA3 z=9f+j%ANh}elxCQXSPSEw+bNA%8Rpy6u76djH7{iOysI8S4mVJ&AE<WryWYygftFF zXX;1s;k7x(g=}5@y#jlwLL7(Hb0RWl$~9S!!1Sj^bI<!eRSn?S=pGkj9=YAUSv6-t z+*!{^$z}I#=6YfWA7+34!OR62s{640>;r|L=j6W;1s?w;TT;}v<2G4g{9g44kMJ^q z7lt}}K-c_Rw*7%y@#<YSg%BVjWymkq6UoOr-2HqcCiEn?3@3w(6i7zm^29w`G<{&M z2ci}@5oHN<-)8!dT4*<*@Kmc*Qd4GDh@4|s#R@J*I|)`Y`gac(MZN3&_|0?H!}(%b z|GZ|s7!dOg^3kT_upXE#+6;T;5Z^0pDvNtm#W><Z^8ErdQEoACxm8xF0CreoZl9<G z64?_B)B(=A@@)qB&>yRlUGCpJaAW1@^>d^O3!2C~e9$TBE?cGPLe85TUr<f)Aq*4G zl21|9r2ro^qS4h-auLiBL{Y`S2GO1)S7?p}(Y6Y#B~0qmeh^dAyf=EX68r0Bmjb>8 z;A$P$JF}|hdd(aGn7(8W;1;yQtK9{^0ubWie)Fo4n#3oVn!@>)_V>*k=ubctM-Z!R z2y4JND9*;4VqVK{q2hov(2JV(Ujn`Cxcm!%9kluz+?0Naa6`scpLImvr93_?D*+%* zD>Io`(&^WjyZpHbK>FH&ytB>N)I7Oa2%7LWk(P}0?jO+$&V1avo=8iCd;(wG3YtYq zeh5zBmtKP?hE;(}>2VmRnmVn45krcDB^k*G`_pb}EH+4(HnuNz#!x=l)FUh;DCXU@ z)`crSS|=HtnU>O@CCsPzN9mht;!b-n;?gkq*xi@cpsu_cDH@ryj;1E!0-#_oP_y#Z z;>e10=9<bK1dX7{P4RHByNDs9yb4ixQ&Gg@*i%~c6kKK;O&w4ebsG7Fz3116+ORP2 zBf(}(HUB8rqt^3m-dsU#y1~EtHYo$3j_d3cl*z-A!L0Hsk(chrf2-0&oY~D0QVX!% zW$h)j`-O3dgM)dzMbFH_k$AKry<ds_9TN7KJR`sr0b{-lc(%=ApvpXyGbKT3_FP!> zZvb?{?pf>OkZeZA*eTo82a32C87fyvHbXgRp~$QTU#ug5nq0gvfE&`U9nxq>scBaw z_`ob(KTq5kvxq&ifViwavUXg)eY^#pz$HR_Cy#k+XDcle;!)0WohTMIn{}0L-93Ns zvhI!-1Z)LWA$iBtI7Yq)<Ljorg3-~C5E?c01Y0I8^SJRVaaS(a;cdE6qg`e|Z13sE zZ|u3^TKL(yc(;IeWSb|4$mw&*%{4VITs`P5dH=7KHCo*dJw~18lw?d6Q(e<gQk^$L zO<8q(3U$L7PVgxp+#cZ+HS%JwbBErNL3wJv#bx&H@HYUWLQ~M>1BiXn&Lv`dA9~&I z<}>o;5(*M0nxt425g1{@i|Yhxrj{9l&16zocHTw?<#yRPqgh|Fz!iqqP6B&-z<~;& zHVt=_3=+p+IC9vip-z@f_9}ys=Z^`)KYUz*GkcWBoxepvP7~1`1*saFChy^FhZjCK z!|Mdh1vm&}g*s69HjY@)k(s77e28v9Vmsj3*FcZtIdbBF^ATJ708m0wPtXjcirw~w zoAiQ^3D|<ZV3I|7;Z|rChQzrwf-?5cgUE56xiv><0W^Z!hXi6OBcbGBfc}(iQ0*bu zKu^F&>F0<yqovsN+cQhLz`>M^zRbS?JU0??zR%|yvG~om+tzz=Ssx$ypXS3=)>j;{ zD@)VS<Av)sNB@O7ZLGIOdFIlpMPvnbWi51j#cEkCbVnR7DR-+@RUScN4V`Dp_;8U7 zMQ<e#SrQEN#`0<+`1PPe>gc}y@O%1N;^TQB4<~$#$)5Gh$UMfQ8$TJ&;p}^I<w!{u z$z`jt{rb634fEiXZ7Qzd=+uw%_#Y+LLV$L6{YSy8{VBP`|9=I~*~G@))WZ6|tkbrd zP1F`E!q<%+;aHe;e-FOCxP&<c4#|4_X5yd&#yVczauTUnVv>uWj~H>^&EFRsbHb(g z>>3YKmPhf&#*+D8PCCR(2@#$1qYZ79QJ;QpGe47><Gyh)awX1ovqzs_2RqZRvB{ec zm`wj-V^nw`HUVeLJDOwH-GvEyC@+wu0P4bIb+5i17FZFC!Ki_LotxyHQ%Az#^OoT6 zd}S~cXdmU<LKz^L<`lq>;|A9$-(x94>uJez)WYg+mklh4q3k{{gi%b8wx!mLC5G=O zscqfi0Jj+D^q8`-v5{10ftMLOZrAl#MBDba$M!-|WGMX30oCX=?EBaVON=JtCXfNu zlv{*sIhhm`AuL12>B$_X$6zeKmKoAXk*3;M;%MWyWF$85t28F*2IB%I1-}FDmwD}q zzZ<y&Bi`J;xpf!{Qwy?y%zc%%l7J=^L|zFpAISr=`4y)QxhjT3uW|s1PhSmwFuoe6 zIlvJHk)zZO2Iw;LsOgWcS(Z^{KVkF|Gl_QFI5{@v-qoE1f4U)Nzs13f1u@VfdDQ72 z@TWwP&c#n!PI!Tea_R7Z;1nzkihivC2)Y_f?R>CNuqqAiu;1GxoiCniow0@iH^hYV z9U9#*wWU_=1ndkYHeCnQ3CqF13pIT^rw+00U3dJn$u@48`(z-9-k*)N{IT$k%u*Fl zvvmj0&EV1g{3A-WW5B5|l}PBgfUBwaw;p*3IfZHzv%j^CON_q3{I{*(pQP<<|KvyY z^-$n4$6sCDRr-3eb=Rki?q@<Tk_ymm<MnEdJ*pzoG%4QOy8ClLFzC{0B_a9fO}8o( zqEf`@Fka86f{p~oDMF5=Djd3<JJ8f$xoVEVeA-TXU5Gav;M}GxdGdLl8yHg&#jnO! zK?Qhpb&4nk2iLpTj8jQP)D}f!*jmZiI%mY`9TMg-uNtTQqe8r|jH4iv8%*^KiTEov zTB!%k1N=t%*ztcaP?eoaJQD#8`^7G`)bITuIC{@q`6Nelf+R1E@02>RPFzzZ8PsF^ zfO1>!5hXt%JjHy(v*^}Y80H=h)^SdMzNZ6^*Z^`*NxFcl*0%vj6AU<S^v@XTRKV%{ z4VWz6U*&rc@QcNPz%<6(e%$nmcA<<pw&tJap9DR`D6m(r%1>o4^|APC@WjS)TVpql zDQno1?(sE_*Ow<Fdu&dXYC5k~=?55H2b?E*51)p?yt_6*Lf;j9zR=l|fAwU{a;Jg~ z%9uF-7jXQN!-2FMH?$;3;o!Sk7z6Lj9ZC<AHf~apA2)6rn1TNcYMn2*{Ql>~+!q;| zwg2a0-u}}^|JTak?(F7h@L%EPI(fkI@IPdgZ)AY3;@AlwOqcxHVIxiRoDhUy>u5}8 zWU-`CV)tk4H4F7}LsHLG1uuriUo)26+qBF*SAMa&Yv{8aw}8C?JbX%;{jk{P;M>lV zS9*!y0W=XC<LJ9!a8I}eFBoXG9z&T|6TpwFmO>lI4J7Z7Wy@tb%PI*e;${`|iePa< zjTjr$`<mkvNI<;qX36+!L&y{-G7adnm}-TqwoC?3yjYSL-taMt`9`>!13uhSg@LyY z5*7Y8Eetcg;T_OlvJ4Q}sGCK*h0~xt2XJ`dfwh`Dz}3WefL)#KJo(z&H|{hNTwKni z73v^#XTti^J?kXzFU$FTB1jo^0~*I2fPsNmzC`5nnmSx5uJ1al9mdm3X}Et`OuR14 zZo-8)83Fpig3q#vh70pN{D?A5Gp4;+G<rGDTeVruy0kg=`?Oi?hPAz>6FNL+JUbjm zhtm{wSuc6K-@y2CMjmhy1vVZV^q!>s|DaLGt%~5UY-3#RdntYKXyGiI8<iG@?&s6b zXcadW|HXX4AAoNlUI)EHp19nzEB%5Wvpe#^zN#wzJr>DaCkOHkIxzMP)s;(Q3P14= z*)Bnvz{u=p1N8hH5C8Hqb2fK0G58<#_lJrM{!xD)QIm4lK;8h5WTNj)yU(W6OOpd? zv@~gy-0m++{Oc!g@T8CUDQ>5;40eJTUj%vW3Yy`}qx#J+<xyZPwcKMUjcBNHY63b% zGoX~W)y3`zn$bE&L2hKwML<*gOP)gyV7Imcn34rDOBG03%(eJWtE2GYk+2n4tY!Pj zsom;yf;2!rQHz5=o+b>o3h9v7phZg1RM3p~S(D*p9Ic)ynkx@G)jpgWgklPDv*41= ztnP?R=KBVr+0md6yVu9mA#)bl#e~k-^~4`rSB8TMBho;c8uz4u^aQ=KbH^bgI^w(b zyEA>5h@(lItsmi&S>BwZQF?2;l-%w6NcY3Bw|<wfF{Niu6M9@~2+8jtBh*@!Ik>*R z*&9#c<RGi?jy~v(sHoRG!rp6AAlJPhVi=UI?tII#^!ucNUV~q<i6}|arK5?XBB_Dq zeljeoP2$=uyWx+1wZ`5|eg4B35$f~Oz4&u*EBvg~zpav;frY8*e^>&WI6kXk2AB~y zKd1p($YRzl#j&}HZCeWMHb<1-EyzOFkQxCyZFSpE^v;QMUJJM90A}l@K!BJaHZXvl zzpH4{_t&Uh{F!a9J|LnXQs&c5R-Ov1j@H|I*a&NPaGq;8M#it)DrG=NK~Tog!z1EB zbG-|fBX>zV6sI23T=8>Y>?1{zk+^D7HbiGd-2doyQ(*elTCW=ry_9@lQMGL-_A?W^ z!Q0Iw2~T}OXj+x%`uzE5wyJ27Nl`-N+1hI9+|0Z@=Gz2N<Un0@Y&B2oi@N>4ax>~v z{^{k<o?U?Y_rhgoWnydL`QIx|kE&GM1uM$VPn^$1C|99%r;ud{fB{)%JcGnp50YR4 zBpzAjuq<vF@aHp+FX)hY`$Z|upvM~*eO}T>;D>HKSXAY(i;t+7Amr~roVl;)jVAUs z<7g~tuM(dc+5GcONX>ob3t^Sja%1JaNTNZG!=C_2T@EV*oa!?~6`9F)eqNrqc^rbf zUa)Qvl}JX<rCJR9B6)SHN>2G>AL?t?X2T`({rpo@(3=tLLlH*MTbw_MNkXKY)OR1t zoZ@BWg@Q=~lcRAkd6(ZAe15*`Bp&)}kqf=+$hC;3IBB=S=B9Y1kJFZzpnT2OA#V1k z63N4<+BF~0n8TyT*}q^5&`_XKArEa4X~3#Pm=LlwP>d7uczlr9V`Ere7qxbN5tU@^ z1U}14q<zUS3v{c$oj1|y-W}+-8)uSP<Z^?P9qujluE1n1`5QTlJ5!?8KAOm#Yb3>4 z?47P;7~rtU9UbAp97?5h1AB-Vppi_@OGxw<=za5hjb2b@N|TA=k~0$24z9)BWNf$H z4-%XFgx^@--BXcyrf9K9eRgyxD%4LkmDy+P*2@OYm0byTM7G$NCHG9RfD&?|_1jFX zujyf?Io$C8Q4e8EvODOEdL+=KY3LV5vQ=f+@N1W+YASD~XB&|Z7}vB&c%-;3T+A=y zNN;(cB^zWTtR%!}E7lf<q9!)E3BtkwS2#7S;GV<NutUajsU6@*<bW0LA0}-EK;Qn6 z4wEbsDrwS3#kyefX>)SRXwTA^d|-*jhlRbIh083R&Nrc@q}Ik3wV$)#gf9w-*p;9@ zRk3pT%GUUu=!oOzR|Rsesf{)C06W2vB9-nA&T~v_CPqy7M8H8&tUav7&X9`jdD>E0 z8f#Jp{7pjnb%EJUtPS9P#l1)y@T&6+Ki>fPqATxGl*|}j-5|XXfJU`gmoH~JfNr%? zu=|WH7&KsXidt#Rv8PRq>d$SI2PG}TS9&@Rd?JRx%iZlKE;;Q-?$)G(_~N5pZr+7R z{zqO7rLm!|>DskSVR?VQq_szwMU>H6<a3K-F8jGAmjC!=-6Lq$%W<_Z<R=l-oLECh z<(?5~=s94F{hgvyv6e*#fCzLL@ywZ~8|ERfE<Qiy^?~&ZLOG)ABG1LZjLPx1jRNz^ z*KDsBCy7o5eBAjR<bkt`+cs=nwZm$GbUo|psez*CKm|#Se_gtrn(72mJifG^hIJ^# zpCzF?D~`7=d7UJoU*h>0=n=g|O3#QbD(E_rCW;~o^O7hd<;2gIG7BqU_;4e?>&25x z=+@C+NUbz!qH{VNWw(z-1NOV=_Mi2)Qg?~CXy?wpOAx<-{%@_2M69`m_yNa<QU0|x z<?Qjl6Y{n()@`xb?tJ<J!|@M5BKi3F>A4*Ch$4d4D4`$*12;-YR!}PFI%1dWZl~h4 z9qUL$-jxAc$2!>2%|bbi+-kSgTYteil@-;GV7}{Tt25N1RH3w~s=rjDX;p}PMqi5% zs}p}&lX`C!m6NDh&$`qU@93b6v@(LDfN+t)i!HLd>N5QKD3pVC1r>Y~4R~5@bcA+2 z6h3@yb}ZGXcUu5Dc{wYd*QO1B>*XPu=b<?J{vHi@XaY+FWsWOU2wjaj5K%~ey$Ta| z`0DQVgJWm}s$Kv>lyY@&P}qa|_Q#s0vt{!Op8yRnkWtt=gGkswy^Y=}p*1qQN&@hb zl?F5%V68DiU7f{kH-pw)NYS^Q#jVm(#~5ATcjgu;Q!bN?IKX;}6|;8G(Z(tQSp{qj z&~Y$<)BjWgQ1MmU>JWAPtG<e|7c;RmU%qn7#MJd<tG#cUvS)QWuQJ-Fi=K9k79DOm zS4!}5#-fdmQ3W7hPs31Ypf{BGf-!oMEF{D~L0zzvc%U^<Ssm>v$Tf%TNQ$J>mMJWM z6z&Qg->5j+3OmenGCr{>%7l)w|BMwoBu<W3Z#!O_80V=zX1_zl?L&}=<fce~)l@94 zD*%92{*peboFPnrt#FmRZg0{@D*gy;4u1xtAj+3aN+y4j9u8*2j6)VICTxYwBuE5E zkwnO7jfc1-(+&V(uFSk|F9s10Ctg7c!P6#agI<jv3z~I56zDNY?hWy)ay{V4Vd?|) zCIlAl<0ji<K=)Jp*>JsM16zK;=N85tvh3YB<asiTemiw--)pP~dE9f3L239Bx|Nek z9v7$03h7nDPP*#yC*?urC&B7I7N7MOJY<eus(V0T(2xi-X}LxK#h5mYC15!ET>_g< z48d!W7$;C9+DZmoe$C}smlV>xR)ug{BjlPoc#NR=B9~7Udna8Oi=ej(eo4$>R_@ch z14MdSeo6(!Zlh7n;c`%lUj!Zs%3EZ)t|c-EAIh=q>Z1qHDHVyiUId7ot|S9YREeT& z>L~;S{3<fyL~I@l)xO#FjPu0MRvI?|tWh3=1_kp&2M9zP2PS7)2gm-NU|7|?syUx? z@q0s35G!KAW~g-MXdTW@vu4t(QE#xbzXREp3ul*~avhz%22zBhwrjJyBP3rXwdxrs z3aWTh2GEleP9i63o844e|LfRtqK+f=4!266<t$$cK!~Nph?S%|O+Q?q_wJpfHNk)q z{7oL}B-)qU%tOBeB8kaS0ZI2N!DXC(KWp<9>L@SXvR-IF-KvHUvD9-hiNexG;2MA^ zBx3YshwtgH2;{p+?gMO`XP~-4zYUMTRoikT<>c1W{dE3#ZpgoJ-PoYot%@8Kzl+pa ze7DBGN>28Nb3KtYg+X_-I#q~Pp+{?THQ8B)83e~Ohhl<!gO!iFG(w5p-tp*q&BG}a z-wZ6-*QZD&sdBAtJ#mW1TdqzVn`{4kBCmEqcv#va>nwNq?P%FD>udwS(a4r=gvJZJ zf+4Jw4UF8BzEUtm-;g4~%mS9fUz@)`pG8D=(~c;q{I!7mj0Y_EYBnm5x_D+_W;W}G ztKN-`1{XKqOZtFqIhly?JlAu?ldd#Q(FY|3aM489kVClh&N+h|$w^d+t{!o@d%`X` znxCjP1_QBtX<(EqLkVSe^?U!DDlu7XBxY0t^6M_5^E58dqDb#8A&@6L>65bFeDT;q zaS=yOLh1o<+xkhM&wt<yyAj)X^yq~?mb8e?Ys%7d$&2y$lYb(26X_3lVmR#QCV42D zcfACccV%rOBA|<Q9D639+K0vg7yG;ha<sd}@8RTPr6Z%&=i@|7qYNPrVb)ZY1>&qB zmCMR$wr=q{H0KGHp<!w>_P^dE$R?1BMzYin!ZV4lK@MzxoPG<Kw~G{Q`BHD?*w=;B zV0m0kJ1CXlXd*WdE{zB>Ju(%2JRXls!%#(R%G%GeK4?<x5C5bGui>8E&V~YBtRk}r z!m5edo|ln^^;gA#m}swF9i9_YY+Zoc^qLFru!wqj<K*r{4T-XRi<y1?!-6C%l5jio z(?Y)%`u97yhyDLzaLYIrcEoMZUA~|Se)TvMy2j8fHIX)O<g#cx&<%%$3@`zi1;r6! zLy+SQ)gOO*U)^C&7Yyh<X@Jy)9&o+A$>Xx?t(2yJX^D?awN6EnV*71mrqHZiKGw~O z7KJ2Qjh;eIu9PT)`C1ma8?FF>l_0-tzoOrU?7QOVBW%QV$5*rmghF;HdtdwR092<x z9s%-+*%}i)YLk#9g_vFi`yddZQfAp<&N9~m8s+jpyacJ=QL*tEM`SS2f@s-I(oQzl zf_Qzl^?jMJa1JnPBxp!G4meXLMxPF&OQ{lh^g~i5I01toVVEeVVED|eO-mX_mi*$@ zKa{KR~YWL{3h#CjTYBFK=`hw)n7!YG&#BYvY$rt_HCVc__NC1c^m_EEDwl*@jm zRdZOZ1{s$iK4?`)bgEgu+IaAzT=ul)nk^*;`}UV(&7ut%k5->GHKKhO2`(L9R+Phx z=P}az8ZyXke018oT?IP%Wup&Txc1yIO!?T|_E<{gG{5<nL=ilZxYH{qi3JJ0x#=<R zm^}+#Ul{$jK%Ro2XJ=;8I{bT`^us<7KW~Okj@+8Aipb}%qdiCg8D;%*9RjqAU-=M} zpo@fXhOP!+H=uqQAOX2BzR)pHl4)i%gr;{%Uo&DtGRSx)kTbBM=9(K){~~p~z%lAJ zIBN9QXb6u}mHVJCVyR+?ie=LQf4z7Uh%(kWU1cCm#fqHBP{asoy{P`3+J~-5{&*q7 zRJ>^ud?UgVEKnuPFVqJq4JJjLU`k=4<`YY30)P7HX276IrXEWo{T1v#WK8Lg^Wf*~ zenyNt2B)9l7|0s}ypAaa%`6)Dns}T*!k{OX?qGrqwqR&4r499LW%|;qF{M`4otAK2 zfevRTwZo|hfVrC|;aY45saIPu9T}vJ5^z}jDM%7H=qJ-Ll^_EJ(^_$d@0L3W*PE=B zn`T9w7xgZ`rqc563G{G{+lIz0_hAZz)_@E}#4Oju#qY(y@8L55e{xia8l%WEqx47C z+|6JiuH4Y#-WRV9<hr7Ll*ce<^hFB@UHi|#uyUG|NfaH3m)Spn#Ow`lhehgTNN@+y z=oRS+(JhJ>20Hx0imJKz430J_pf(Xe3@j~h`TIk2JXqC?eOG{DAwlm}9p2!k(8#PX z{l^m*Bl|A-vDx&ZMo&u;{lW{E-2ofH$k&Hoyhsng;upwC%`=H?Fh4oKrmTt%Mp|j> zcL)X{t0HLvh6CrkPaP3i!2ubOs0;I#mHNe7U8};{e@nnE08BSXxgM@wZO(|d@vo{x ztVj52r8mTxB7=FrmwmC(Jvt!1ZJHXQ(8{P{w9yD-^1_#RL#_``n$sf)N#|g}mG}{^ zBK*zM9TqTj80Ky-73%O;fy3EK0Pfh8Lgh4!1Zj!ep$B?k%PFYDk&;C$7dOhZW7Gf$ za$|p)VSN`WrcY!Eejt9nGOJbN&b?*OxU<Ih`@`Ns3G#8Om;o8zzyV<vf2H0W@9xy< z*j}R{nAlxnY)mH@H#E71!IR~FP>zah@35DHM@Z@hdCMXGuw=^Q_?12`*_5)SKT{}N zL15Pp0@lvi){WTVkA+QDqA%TRVpVYue}uAUQO&m;if!M}%fh}(Y-s!`j+unTEmp>` z?j6AUwr@1+ACy&ph(S4=bpET@h$E(PBeTwPX_&nG#cNQo5`fQ6Z}3c(LOyRPM0!H8 z?ALbDU3yAl7UzpBrT4ZEyu<nLGGTcS7devBDRiwG-K<YJ2&2#0sj}(Jb+}8dXu_35 zZA%3yu%sIZh{{<-iYC~hv~?I@YuLTZKRo=<G!Itg@{hNGz@H=5WnkG-G+1ubuOYL{ zajjHo1sCxYJZ~3OGFu%cQ@WAD&eIfk2$~lF3w4VDTzv}lVXLmPX)^QsPjsKmjDmv2 zk2HwEi*hXntt$z=v$_4VTF#t%L5;|XK?I_C=3r$QC<sY|z@xYgoMN1LX2O<S_iY$S zD;S0pyiJT~qZKr=u)z&t<wb;d2Fp6+`DX8iWBTc>P_Mm((}DSJ^OY=57I~FHC<9B~ zCwr0!FVcglb62IaxZZHfA5)&|i4ZfN?ogd+2JxfNp!1`b9OP38v+aI>G0I0mgJ0GV zRt{_maO=GElbnt>%9J>;1s!-f92Hk7@F@-}x?zLpaFizN3Xh`6)<)Vv>2Sw5I52XH zYxpkdcuR6-^SzQGif{203;0aBcyq2bWS%AJA;hV-=2gaH?<Xf3`jcjXJaH1b#a{ad zqhDi$%Lqty5Dr<}doUGOKH#{@Qh5OtWwWCx^mpx=swj6zy811nQ8qm_uQIs0&Y9=C z#%MtA2G9?AG3D3smZP+QxXAJYuuEtw_{?Ag0*a7o@A+ZY%;TVHVSoBQcVyMaZ(ZHE zSS~t`?<_-rY&vf)++D+uMb@wqwhPuu_Qf8Z!snmw4^bft@s_CCGS;4Oe_xMZ^}p8r z8W43SgC#pFzn;0-H(Hls#kaC(Digq+r5!q2NgZ5j+6utY{E}r)n4Rr^H(QJ!Pg%P` zi~zkI+w=3eiJcrZJ|8PK>vU1H-^{)I9^vHD3`4n&hv+>8)q_*LNlm8EZ1#N38CD{J z{E$BDC)+SMUk~oEyRGl%gT8$618X(&9Eje{h_D##`sE5Jh#%RqS|S{=zbwN{+udnF z<+u6QXYZ>_AKMS%T|Vzg#bd&~mUQ&NUj7u`y`#X$5T+Nq0*?%iP`9=}U&TTOybn|h zQz>xB``I!hL**Br+|geZiB)0UbuHq$394DWp0B^N?}08|OThC3#aBd&-K7F^T|`Dr zO9ee72Avc^N7$U67=e(j2LfwLxN2HB$?S*^vQ|n028hlz1MM|3QR_IulB<|2rm0MH zF!(hA14FIbRv^Wh-otp6X7*WmzQl2an%*uw-w1YMoCV(prqOH*W8(|Oo-j^+11md> zcM~t{B7cY%CTwxP)v0}B4(f#)t>9cVwpr$l^#4kS(~wrqX5r>6p^>XIZ!1@>n8#NW zvkeFLoFlq|xyqGNDcK4`&|iwiKmYt`ba-s98Qyw#@O;?bzF%EwMZ2K9odNnV3-;g_ zcb|1~d^2>uU-t(W*I{{g0J<tfN45Buk62!K_~*S==EKB$BX`EAZOZy7x+xi4P-DS( z>{#k-GXwyZs~m#!?4IUuU)*IxEZo;zC2Jza2?0R~JYOc{^1A2C1ZF~;J(<`|xCgT} z{h@SG0qu9%DEGci$Yn@d=rtl<YSXoEzP$+GxPEw)9Kn->fR0a();SqzcmD0oLal`+ z<_CrL7bqTlqT=y*-ea0H{kQGgN7GPl0GsXbnl8d`I0Bdnr(#s;0WdrjIl-a&SjTn# z2Vd$ZZ1JF1hCAoG-I_GPj)c0mBN!Ojoh7`ngot$<pRw9CuNpiJ5B0z&LMe7)+s^f! z&nUune0ZREYLcdU0mHGi!MeKOK6$9xHjGo=-HSQ;foL$7belGMKFDp!`hs0NnsEd? z`(SJsShVzXoUMN%gfxhioq@xa-v~WF8TN#YH`q~jk-vi#6-6Z65t?nO#K94!4NWz0 z*n@AwsE12iYe%02JNx0mL{^_Mda3%0Pu5f#py!t2YY_8q$4uER1@xA^%qlbtvy6Y; z$f-(JjB721Q7nwI@ngFcFWtp1RYaiPW{B!?<^NGN)HuemW7&Tusm$O0yqShdgz%;Z z_{c7>W-&f`+LQ!oDr}Y%zNp`CA>+b{bAk<{?9!6;6bb3+Qh1KgB|!1DolCY?R@j2- zw*d%~r7JIKE@A}lU*G4zTT!k}YlYpiBZ3V{&7qEV4IczX424eIE1}YXl8vJl-^k_L zo8C1X5nhIj%8&t5Sa1SuYMhnRT;j=dk8-t-H=4K8MkrFXKy|MdXLz+83*lMG_d3Vi zmmlPL(w8EwQal`7!&?B@oM4sLR0SpbDPlaQ5Z&V+iBLQ&A&>d8!PJW6hSjWxWr<-% zisfFC-em_;<ei_9t`57MGg76?+td7#aHyYOmUe%g5#X)|?;i6hr3&L#Uy4wrdh4a+ z@$EMg$B?oJ8n(6AKVeo$?v28W^VSi@$x!pugEyG8wO<~#M?CqH0^hDig4r6b|GF9S zmc)jJW#KhtC^1&Ckl4Wh31Q~IvYkWc|2n(c-&3Vn3aWKhiw5Mw8bOC%1Bx4t1k*sR zkUo1t;vPH$X%mJ9{E7-Hk|AkF{Qj^^!t7G09LP;b_j(F>7xNv+MjqL`Xc@YCG&Q1d z3DMk?dT#(9i#9!-0o}7=0^YIE#dI)j)9a|v=!bQ}x_x2K*oO~{fZ>73vb(p4Q~uKp zVCCu@KG6dR1#z|x7am=eY}@wI{<s%on)<jJLylStyQ@OLb7?iG#6lhvz|cAN#srmJ z3`*yic;lH7dWM<N5i^A5!<@a*7*1474Wd2u+$}5kanr|*DfT1|9`stG?sD4D@=9)# z?TdGe{xj(#H$!01>Pr}SV{CVRra=sIRrh5%<>g}d8r4cZG9R4Dwj@}lh;MKSAVu?U zKm0RoL3vRrbcV)!jGHTPv3Liy{N=d%D?-Kk=S~Vv{27;8Ya2<pal@5o?G8)=bcxnC z5S%cn&NMEuWU|MUXjlUnv9n#HQ_eW-HY(UA$k1jP+-{Qp?H98>^OWp84dBzFb5}8- z7<KFw`M1Wf1C8BYH~sdaw2dAGZAb(Bls%th93&SC6r$hhR|OUcLr3;w_ng9wcrI$q z@7R0vF%7z;A<@{4F8r1&^GW1~<%WB;_48JkhSS^Izg1+w^2v2902j&2JxNx)Q9+%E zO;$(-Ps2x2{Sb^>9_fEc+vMq3mgNuQ9oL8ZMatHRkUgM)8n!pej9ii}aw>@tr=H5D zpr(h~STDRtHtva^;kwfVz1kwk=4-=O5x%a2S~UOgypj!-p!T8t0;?6WT%RV~BzG`J z=Wcknapy8$7;+<U7~C=JfQvNI+oR{#amf4K^D&~HxJmv>DTK#jEdE8?=srjQoDtVW zVt;}!6#9E^KYscWzV{NxH%*v|e9}h0#|io(__LBRJY@CvQ!KuEJm^x5J{1RwD@&lM z(#bLse0Pa={ZKZRso$EXrrBg5FrJ?B;jfO8;#d4-$Ch`7i6(^Ozw-`eAF8_tzPqLn zAzqpj4=7S1TS6@~xtz{re_El4JPu__y!x-+EOF+a8Cas^#x`hL;L@ur=P+0HVwm>K zUkF-%$>^E7)TMKq8ZPq4w7QjIUCe=6<vP4;iPT)v^R@eo>g-N-T$0Z5n8ygOwpxJv zAz&?^HM$fEe_V^(5Z~vuj1#B7ghArJ726oc_Yf`cFF~~#VEGb56Q=eoj3zMh{*H4~ zxwcw8c|Y>|2UD+zrqDO^<Ly;t{nz%Bi>-yTvx(Dx?T6FYI(D0#&)a>1{P?v>=5Y0C zfIv4ZTg^K#EH(-3UXU;WsT5*SG?gSNWUs5YcT&a15~;>s>i(T*Bah>cA`gFpUi&9$ zACUxmnB+KqN6(t#ck>|(HHbOhW;_zL_r^*Lqm6L+k-Y`ZZ9nUw`*qDM(26{LZuHaU zzVT*FBT2sC`Oz<UpO(A_%HK@eJ-F?5iD-njV!rguS`7QxNsQ;Q`}N<sj4-Oy9kI!l z^F3;Q^_Iez?iL8@zxLQ2;XNjv^uG4&QI0E|cpafGM(3}+`V#0v>$Vzt_VY<)i}5Qk z)sNc!b$Z7AqK6)s{M$2^_bs~giswY&P;w-+IR+8d8m)wK&jWT1u%5+dd&?lk{K+Yb z^XMEm&1??Vv`(Y#)rY<GxI6f4r`BYzfv*)gpz-IJc2JS?9*j(!HFtO@NxkIaS@d!H z%d$b;nziW;@Agj1ph3K$rSJ$@Ix__R7DivqL(r0?gviT$Dc6Ve+z_^NKntY@B0lDJ z?~kY>&B~njpcDa~NjRfA<Y<V%lt7g@+-L2xC#pC~-i2kF5u+@(=802i)9@>WsmUmC z#D%VJ-^15~n*=4o#hD25p-o3pxF~=~@l*$a8+4>T8s~He5ZG@`zwZy7^zbM4{a=BH zI7%<@YR)5di6}~v6wt-jd=}SMSiRuh{gDwCkdjSE#=M*fJ;fpcheeP5vi8Fc-8hl$ zQ?O$~m?G(n&_!leV7U?*Bk<WM0d1rtNkM|BSG`lf1+*NEv`HXJW|8p>0@0TRlGl9* ziy%o%O`7OQSD6tJFygK?6`pbB+I%(wf#fE9fp4+|iqT>7!8a`}cmqDhSdoYHgW{SR zcOihFx<h4MjOWL^&~!aCM#{wb{rp^GO3@gA*nmZs^CH{!?<D6Vk891RgpTIqaCZG4 zlzn29?g8OZZ6v1%x<Q#%pm-#rt6o)SnC`TQsxu^;-@zOkM&sVFTH+%1@Rjy-+)XL? z)05p%<J3bYBhdy*=43F8Q-ucRGL5iaIynGOsgAIR&`X4A>nh$}A{vl-Z>{@nvivBS zxmm^eZ`id>(MMfe>io?^vfS%MZL=1MAS(BFAO&K8-v0Vvq;ehTg_QvKktTgffZtj& zpOW1yZo`|aWdcB;4xQdOkH2@yhZzg=#_MJv?j$TL5+X@fwC)Y@OspxA375g$X~tim zHnUVqHYJ1_O%>%Wyz>=Xp@Xl3N?sKaMDNqhR<y|X<fW@EBIHKykIf>OVn`$^!m*K{ zhY-h8-rLg3kjLsNoypEz8l?}9|B9Ac*)ug*c;Zw{s5mMt{xNnHfW@?H<T0?}l+kBu z8(ualE6!CjmS1MpsHP-qHUn5iH=TH2`>mD(iC=DvV=?#0X!_y|{a6x5I`l<2N1{N* zav&6&Lxj#X4MS%FF0^*Bx-&u(kfRTkWU~)5Di41^>~mwE@t*!H3$2I{K8se?{g`=X zz7k7wnK9iQ*W(cC8Capq<hTsV{mWfviG2oPkj@H^9JFx}<|umKIz^i)88^70cRXli zw04w_63pSe+0sedqDmr1xg(Cu6Fh6+$tv>@t+Af6)p%sc-=8VHo@2wVz)F`Vpd5Qr zz^6SeO(+V0hVR%wlt8W<oPlCSSSvkAti)yXYA(Z7`QfQGq?L0yWre^NiUxcdeR~44 zOWcM^{uSm<_OhUpC~<as3AXL`o){D5O@(GV%8(@K^Ri`hp2c|YMpHDSELZN>2-}N} zheQtcMv;J1wp!ochaPb~0<dM3gCuAE7ASngZN}+`-*NPPmA{*unD(Zc^87nPxajJ+ z=V<k{0Y7e87!n2o!Ui%EK{|xEJx5>6KV|U9BDKbW_r>r}wbKV94}G#0(<iS)Y3yL) z;)Dg$D5lqiI!x4e*>EeGxL<1p^s?Y1<d^Xy*~i#mUF(A#XsRVFnT~HKnc}7l2N($C zBT%29?UHd<YV>sR;2Ryf0KfdgIgC0m&vYwkc%RQaA-RH+X8Y7)rb+;6<jziow6LVW zx_yXBB!(Y;3Y<DGbYVB0d%G<p$A0Iuff)D_vbl`>Bb6DBfJ&{!1U~^Pipf;6Cly2a zPfk3#%EjA+GT~DQ2IfH2ow-xX8ekz-!G*rqyTX+4pD_J4Q~LWW?K12Vu*6YUIO{zu zx6D}(MKn`H8IwECY^qxI+c~ac{ef1L^=a@nOi>RDC<N?Rm3&-8=Mrb(A}8g!1)PV( zP9L1o#e8g=bc9YZ?Op1wNu_P_$xy0!qM!$i`9HGVC`RCw?CmzGAg<}VYwtatmRA&W zq$jCYYqAo0_4F;iR_ND_ZG`nxOLpxeChY)D3kQp(mWP}S&K|he6ekGgiypm(bS310 zzgJ*zVG7v}nzJqrr*&Js_6wxn1l|S1R5OqzVTJjs3VkiD_!FBw1=bilk#D}~OXbAO zVqTCJnJwRV)dT9}-3%&G?65g}<+F?JT2q1$ms3l28pXg!?W)!*9;xdI_3q5DE{CZm zS0GoZFJSEpB^EJ;OOYg$LmYWhldq`haTNEQcr=nyU>Q&wsQjfIU1(RtcM{sCDiO%= zfwp@qbZe@DYJ5W_Lxlo3me5ECtBtyZ+!1w{Gn2Y^_!qt8t1@a=D#t%ffv~6chnkr- z`se+WJ;8hrl0^(>^E={Yf()`1eY`cxS?hd39TbBxeMd5Yt|I+#arl9lMvpFvR?l#% zQwE#4I88!d#D25YwDop`gd?kS4w1$8o8v2AZ_19XvYvV6|E4vF7djsI6N3Tr$WoEA z>e`)O(vc@1@?z4En!*MkQ#NDr3DuRbQ2lggyF=8J`89cSSX5b)!cuiR3aY8RZ4vp5 z$^da8(_^}H{dp~1?kHCrJ;!8Tt)MYo%I6m9^%NMk(#?9^Lb47fpSFW9ouP4k)uKb6 z{dE_30MYvTw+*P2bJ@nGa78M|aZS0>5}zohcxo|xm=my{gfw)ZNm6!b-W(>6yb%s2 z&B}$hL3?$48k`vX-f0k&S>xq~qqE851Duo#3XG!(Y!40pMsXeJG+hkBpGWesBVr>` z>3I^wMR^Ue*T?O02}>8ZE5v7_in*-QIZ@Y(AxrDj+3!K&bu(ty-dG2&&5$d)Llj~@ zOUtca)vV>LdR6rR;!Kh{f!%3)P_Wl|s=hqBs!gT}XOhM%MT}d^WU2|cqcXB}!b-NY z`gUexA&(?JQpPX!6S~zF^Jq`>Bc3(X4I!9udH>LyvReNoe&L<)YG(-#Q*#Y2^lzza zPF5uIR4Qx{MlBAK;sW&nMrwo?NhCs2C-7^qB7JTO_cR>!$hzM@psam@QOkiJUu7=R zzcz$^a$>D54F3aV)nMq@9kO2W^a-|mIY8H)ZD5Cjp7{^e1A^MB>D#;_frMHv#-M9B zl6KU5e<nPn9Z*<wmcmy|*(s8UP_9-Qw^l&CjYT#pZZy_4Mx39nT%_yl4qs^*SWkVO zpel@9q2*e}Qx0Fj=^{F)sRi<8LVX>NInqOunl-^Vs#-ZHICLd;2bb$jaqW{|mF;#N zXXoVPaKz@G0cpS=@(Zd2MBja9V>WUers@%Lnx?4Mfu?)deGKm3n~m3|7m24dpMfTD zov*S%E7xx!)D2t!<pwAZ)6j#&E7X9&qBNx_UcPuOT}$mCq(SX9))JN7b|uaknpNzL z*7JqSY;cv>s!_bj)uhDM+HHL&y+!ZqZ6vmrvjafNCJLglN)Q1P#c4o}-id{K9n?3z z`}HbZ@wWx$31Dw^Dcm9S;njzd6#zuXqcrCPYR-xQ%r>Il!IZ=?K%1mII3M31yFx*- zztqykte>A>(x3LpbmNPd5pa%^XO$~c02>H*c>yl%nVMZtuEvcP4KQW>IYDY8ov|Wp zRdO#3I7`y3E%D5_!yQhPzr&)BpWgfsdT7(Wr3i>8q_(2gTY%FflIkz%t!s_M1X4-~ z&`JjDQb<z=4<DSK@u0mbAaYx`gR}2PR7w}!KnRhiL<gTc_FWPLF&@Qh>U8{$nrz0j z<i%3e(c5p)Ax(lRo_?)tJ*q<%&llF&Q97^D(*_NWx;4i>gzV(*`(r<7J_ZF_A(BU9 zK0fkJa>IOcYGnF&{C4ax^64q<pci|2l}4;CmQ!t^m0xvu6p%$ELviV|j@3>9k7meA za2zE1;rAS;pS0mGOLYZBukpKGk>$iC+j;7|6%zy`|J&p_G<td^2=_|lFk(9nYxrUs zQ+D)^j3GA-9=+}mCF^K-A&82XIjl#er5~pzu(CGF?E5Jc6U7b8`6t0hgA>WM@Yr z7^#rcG6TCUYkSxR3wF?SsqF$zCV{z8&!T^712#69GXZl|2a5-r6%J>2e7`YyYO4)4 zmsGFr?(V+*GVGsua_~PWP2kZ^t|3bBCj{9~T-!cvo7r+WVyHfqd+8Clt+*T7Aoy5P z;FH6J(kP8_KOp~LZ%_?ig<8m#VUg7I;5}WN5s3;mAdz#oc>kK2tCB``T!%V=eFR`R zZ4<SvhC%uX*2mUhQ_*A%hZCUF;FhIQ-mr$t655?<V!21lA^)>09sAYcC_7T-Qlal^ zC$<RutCW1r`O}UT%TgG)F894xX&Y^}rs|lsO!P)4(-lL09@HRb&R!1HdForJDqC`# zT9&mR_C3snCS$|Z84}@pH=ay3m%MS?ZE}~U<VF2FJ0a4Xa$)_FfS~<_BO&|qV0gTe z@aiRGU&x>?=1~GL8QJBzl_R;UvD~jSGIMS}n1GHJP7uG7!qD*guNp1h<q1QE*FKVF z<)4(oN3e5Y99@2oSZpbHBHcJhN10*#vB=y&XUKh^Uv_#LXrEeBgFJ&Yf6KCt9o_5J z{Y}*U<b;?qbqA^^D{pMKd!?;^ybfYmY+u|g$#4o@s#%QkLT%k?8IqSsauQ>vO|w^A zx|pzJ+$%8X?zg7&>4DNzlF(E{o}T<n+&m39-%26b|4iTX0!TLC`r*bCe(e54|9@A> z|J1+#PQPEq`Y%|eqbHD0AKUP2qm&;IkT!k;Qjx#{W>Wwk#z5d&5y>cWi>9OXhpmE% z({ZHdvbfIojh{BU31TLu|I@43LZ}%vQmX1@2#1=cb<n0aE)<HNRys3=NiQwB;{cn7 zPpZa5j86sJS1~r`rAeqSDqDfzOsg(Rw|*Kr4)oP&_zK{(tlaM7Ql4J4yd-$%k}oou zI2jWt<JQSs8X3E*tCN#QMX3=$GHYd`T;R`eqsO2_S+Q=IV5LO--Ny-6_ls#qT(>;2 z1}#cZzGnP)lv~}U=CYWdcYU884LXRgrD#jyB*ueV<@)7&pa!FsJIOf$h_uFV&+kx_ zv|uSCH7-#p)k~APBq4ZwYA@|Z*l{Pij`2t0>0vUVV`JRJ=&NT}37Vx^9FgP%nU?4` zg57V?FJ&Zz?Cfli9r`(n4V;Q+^10$um$K%Dv1_R1?HWl-^ES(4ezP;k`7tfkM_92- z`f*K4gSLSpeN67-&YoJ!-eab^^;4Ylpz(N_uyRw<x&S`a6WI;C4$Kk{QjS*S%2+{G z(jdOp@}xYxB4qWjg|4JrOwQ4^DS4xdR5$Hzar%K2@7XEYl$Tf$P?kk0{d=5}XY;#R z$V{%vuB;chMh=pU9ZZB_>MYT4oaWE|U(G#TOx!{`yZ~@zdmspA#1t&qYAv07?j49a zra3mEf8q2+KXQA_+>bcprg_6KYL0MbAH9bDA9L>*o!Pc-3s-F0ww+XL+qUgg?27G* zZ6_65729^jc2eJ4Yu|hBS#{f4d*6RwS{r%)%{Cs4M`O<3;Wt$QtSGBcT}aH)VKqWQ zyA*MD@zQPPc<U`<W;@IsVmM6rn0pbooGpf4csNTGWe8}Am3$4rLHCI|U?&zJm|OYQ zuaz!_xMUC&&eYN~*B5Hi-<G%927|j$ueS51S8UB38G_{Vu&!e#tm16@1V`XPFVn4z zz-)u_$<-9SX34HpM-dwsV)jb=j_k28M@E>czULx%lm!uYgT%v<n2E!AJJ^zd;HFqK znY*`0hi{(+>zreDco&zEanYCZkXxHI2|j_xDJZe7_E=X|N=`>^11DaQTY@m~(%^-| z9ROQU))+2}RI15S!0hAi*J1kz@fMf0=i<;W!`26?&7YYuG{$Tc-QpvW!OgSR9a%d% zeR;l^#~VPufIXElK?$2Px@h#;_oegt*m70~*5=Sj0y<gtYJ!i5&Rk2@zMix?c&8(e z7twcFNrkWj3a=Bn5Rz4%$&j2oVu#~*r|>wZ>=AiG6Z%P65Yw5P@AZVnsdElYC~N$l z00%uBXbBgOTH<e-nx1<SJb!s28QTFLgZ-cn>yX1GUq{L_%LUAy;(V!xmrQ19Ok8!) zRf&vS@4llSYq|Jvhd)q_$=23TyXKO+tuX^7g)HY(ljBD;Y~q@U^y9)tF{_Ip3t9i; zYEKz4o8)QE8O8f~9n|lOQJ_CD0%7EK>5)dJtQX~?U1oH%g7Ucl%xv)ykPYCKb!MBa zG;uP4a!9YU$O*y9tbPLbIO;9?(*sUpmJq#z0PT8}Gm?k8cs;{!{leAmRjw9I8(<xI zOn7a=kqs^tP_sDnc_W}{;>-d#4Gl7loVsY}hhUwvV9cV*jK>%rYbDpEp4VR(-J{KG zTr)&r!CLGV@iy5!WP_I(ZI<4gUJS!%A22w$bH*4H?g}=)SY4=F3&_HNOpZpX=O*s2 z;5tjhQUSIjQNc@Gu`+p$(<YfjG7Z9b-OvMK*Ylax$g+p$GmIda7J-wrd9+lMO9DKD zs0|sxkiizAxN^$q2;+>#4DRp@CMnvlBMKka-QXwKd>+&0OVLrRE#MpRuTszau*j6i zSc@XZBOz9W>0{3bqAE5WmEY#>ZP+q3$@p2sGLZA4`)09e;rr*MuX1q+NQa=6SM(-_ z0)&XrES5<c{oFalC<G<M7T)Y5p2V@J+&F(>raIsjEdz@+tQ~plL*XUiyK#rE!=N6n zE-h8lyXO3`TW)k6j3d0`TgUAXE3nkvPdo+T(X9Alm_RgEi}Vv>S#cIlZfHlRFYWkO zm!{NP3NxQ^wTN>}N`J7oIJRba{z{O1ofCCPip_=%Si5Q84f8ifCi7N*`v9I4_+C(p zG9idG_fbomp9g3rKn#6m2+bAs5SHW2Cdu`gFCXTgSaL99WAWq!lG@WXapW{~W8kgw zk59ULXY{%Ub1d-1s7U>T>^$|7LCnLJJH2tcV;#SpMVjgEA!L166&?RlqA8KuaCn#| zEG<du1NO;xoiY|%8y;SEL^ZNGOHye!zAcHp5i95B;&-!JFOq~I#oTh}J)GIGxd+m@ zp8KkgP8#Ju^g2TkoH^2}(CLRDvMLw@H8B9<!S<PWJ^HgwdV?2)(8lZ`4AxYUNn zUxJ%8Ycx#Uyvc)UIEVE#iga}bd$itP)3Z6}B@>b7xzf(Ff#Nb8@vTFEZ&*eFQtWi6 zE{jFIadEs8Ca{2k;oT2J1M6&w!LIYfY!3b~)Thgp(I{P~G4A2p(v=?=RH)w{@Tby> z<Q6&r*rw}${%i}7djAVuvPpF-5<pAn`>1cYEDfzm@PN78;>5jd;Lg1WHzWmFUBKSu zd^YdqdFsbMI5}mVXT5S0rx8(jnP@U*e>}TjEzPkyy*d(s1;gse(;`IkVkb5-bezy8 zOg5hqQ9nC06Y`lhEBvQ;udYltWaK4;n$SmQHR(X(JFTI5&~D31Ua%U|f)}Gw15dF& z%~x!4s2_|<C@(&YS}WuZRiH~;g~9XaHf93M6c2G8l|!U({HUB|&+*ua21R%k$!eNJ zJ&gS@z3Dtog2jgpg&Ua7bb_td2xBmmM0;Rr)On@EkC8bm=erMcy3KEIr}TM3TtZIE z3iJW#Q7m_1W_RZhFS%&sOgU3fI|-W7-2qia?HJu55qB?8E#Jt3cb%27bORhA7}$E_ z;uJOZM*`QMEOe20YY-nAtwR(TR`=Cg&{x~66&))w9r{lS6R}Okq@XdVk>J)!=|m{q zDP2h1=`QypT8-jX+1+-hyTwqphb)YbJ=UYpJ0y)^;HL)TAjL9f{cK{!yYsee*I{O{ z3S!H2vSYSP&OyT*;-?>MhJ4Wb4Mk&@htCMei+q2|!;;r6YJp25u<oLN(&kxf+~(ho z*21~6YrCE!TB6?1U>QzeY*gb7fkFS-7-#O#R@U-jeW{Q~qp64f0o%HGB)Qk3#R5~1 zB8x3SZ#jQ@{3N(mOVTI1Hz9PmvA_aLa9QNm<i=S@j&I1N@l0>RjdSkVp2i2FvZBR& zpLw2MY1ntUkRq(mah-8GXp|xz&bnkMKdp}`C^2UYe}fB!n@GWxNwVWu(v(sE^vB%@ zQei7|KA;}l2CRR8Wpw>tyfd;B+|U4$Y|v@F5)BDsjQAee9#O(LX3-u9#O6u@4Fa`O zq3^MHXp-Oiugn-2XSz@sp$r+Y^SsoZsQRW^+ycy%OhLty#wZ6oNwd3o<t+vY*}J!+ zR5O#fq1OzH8w+On56-$3&(VkMwP4@K^?WUP)Xf;dax;7yw}s^-uELV*RB_^Ftun$D zM(HO7A00Y#&LK0M50yCe(1jKs6r1k;X#2&ru-p9sqNM=VKa{=h7M8{qCV%z)ywRP| zeFBKVuU-*gIA9xKI6Kfe0<~dT8l<VOni6)gV-bhvvI=M5hp%Sr(<2R^?TN}#ejpcx znJ$uI6^S`*$pk>ii}TEcaC?%um0hAwrYV?$X2Bdm@}QSNFXhv=;ypOr+Oriu0{M=* zZTvWId8^uhhd<)?&1vMLCCq>RBX)co>{dVkuCBh5{UhX?hpmm1gYo~GTuf+L+wV0a zzhCGJvJzy_sxa7j&49r+`g1t8f$u`tn6D8tf@<a4Hr1CV+>!sR>FzI)(5%OM48S3* z6NUfE(!aBVMXhc@|3DM7n3R{QJ2^@^rFQK3Lfzabu(j#(*;>q9q9aAKPH?5VP$t*+ z8v6&ob1$(=<SlNE6LQ`Ts4tk|%TJ;`Vj&Wp(fBJ<29tKL>?Gm>ut}1}<y0l#Ypsm# z9Hvqq+T1l+xt1(14TpfpU<tvEEF(dYW)ihNzHhyH2pBdwVJ#=?(27!ivcF0OjXb3z z14(C_C3vSG87zCleKXWIv_Otp@pyCi1!@Qxgxm#PdggWp?!TFDv_G@Z*iOk`z4MJp zmZ0#XbB{lv8%bfl2!gj3oT&;1g5N(Up&_o`e$aBOTZl=CaZ-VD)S+tADg?qb5>w{2 zBeNtBcl2U`Vi>wWAnA;>%q-Y%+fc~K_S207B3apHKZvGGqXHWbl3;jpSrYg(73I>C z@ffCSo!3lf_ls1hHyX5Tk3O==ftq6NWNAcf1vGvO7<-$=uc!e)uGoCgrH;u_kXeHU zIY?H2nKA`XNXDSdI{$pWGimReDtZN;b8FwgxtoYBE~3cc&Qko9y5ljE*n1WgYW(Q4 znlk}$ZNUC*+@;@}@s%g-neP4PouS8?J}kwaz}3>T5#x<Edc%hYU)DIf@193}m>)h5 z&$_%>L2uU9EoU~rTv3Y+{+8TD=w6P?l*3!=FUCr(y*2rF&E@;CV?*{%whvhJ9CG)% z%%8qDoLw+vvJ9N71QWJPMvOR*1m4!4C1be6L53*X@jFOG=pLe>axOBNsV#(@Nl1Tf z-hGDjeW1PkkyVBdY0O;Gs5e);_f5(`+CeH!&ticvl|QC|7{$?hLb59+bBIy_<GC$g zAYSX1iB7_xe&addM^_K;<`MCgDV?_6LewJPLg9Dc1JR(Fo#|r(o#4$*5DUYWnO(FZ zu3l03oufo=EjK>)My#>(m<2Vz-sfywwn+((_E}rs_JiKtOd}W`G&51YlNYbb`c1jE z{_aGL{`mDeGdt>L&}JilL8$<6aPzR|dRW&XN4?gWaw+g51Kd`d^~1hrc8p2!508G% z;Ocp#Puxbz3SvCGgW8Ao3nc2ABO!|A^Mv;Ar_B`YH+O|`3Og-HBO&bl0v(Rh8iq<9 zeJ%*)p(yEnV~4W8m}nTi7G8xa`0D8z{Y{yrPG>;vU6wO^0W=r=`mVP}Sh!L_SUs(q z{Lr}jn%F;l1;S_usWr1nmP3FQHD>4p4}6o&p-$w#B59omNFe36C||z6F8KDh7v)Fl zj<R2}Og`@O6YzOIzPoR^T`*xNZ?nwGTLxKOQN=9rlyd}<#l-i)9&2iWg*}Ai_DY9G z8!bO>`>B1Z8Cj%MZalO$Oh=0klt8Ahs?shO?1<z`00qGTdSu>CaS*^7T6;&NN{t3) zquQc?BC=e<cUd^=Y@SC<vxTe_5;)dcw1aCyNZ~@2bQ+yVVLtT;K~HRlDwNNV8a9dA zHsE0D#jRrGEIDISn{s|R!|5Cpg~o(1_DLx0(al1OuXib%I^KN@<ilW6uqOxI#EqJU zQh{R}ZQ>5FNz`zXnw*r6XtaDdnMolK6TatAaNqGv7w`-(Z#*A~d9b>VHaljf#!mnh z$XkhCZ#oI)KMTS-cGf9fIS6dj+{_mSQFO^I#>-`fx6HEfiZO#xjaTih!i`m+yVwkD zoq`^DXiw0u5PvQhcax9Mau}o2X?Syt#A+PP0XKXlIf}vd+D~A08>wY|+((sK7{Zrz z!)`aRz~-h^wLD8Ys2|!HvV&WUz>-R{L;qYFn=y!MYJGMOb0!>~F}hk{G@zRJvxzp> ztwT_4Ks4nmsxlpcr&&57o&Pj&z7!s<W2u~NzjV`&4#<TvnO2RQ@nVIX(VEEnA{YG` zlY*JJD`{0HRfk~e1vUO+M&^5W<vJhV=M9ap?`f89N){y*pdZ~fgHbz6(@GFFFUxN* zkzfNal(lLG7x^f=eKQ(dWq!nF`0R}b-+Rj#Jyd(Ya7kGNSI|YodfCs|S_leQZv-Ry z5YnZjR-85(FG#(U2cckuC=a`jAXfmdWMSpe*5Z}UH?z`RP2!+9ggJmR4uXv4)d~*L z4Yn3w(k)Wm93Q9PMTJ;&g*Zxsum6Z4X4v&WO2R+>6>TPZi5u#{TA$0+4Eoa`qM&I7 zsG^*Sg5S21i|vDyq6U7n8Cg&&-8#3ydT5@TX(}FGF4Gwt^ymi{h<788GjMpo5kj2n z%;PVg$f-lVT{vOX{0>1qJ&*Gz9PrYyUXGE|EWRO%PDTy%SOGusM3+Lb=9E^bumLWG zrEK!rO;aB@AJPx$Qp=2mc~v*LCQ&e3hH9)G!Z9y2OS`i9${{sb1vtNW40z{zF4Mar zymTNYGL6={JPrt=imkU1i9xh!e9sCxj^)Zrs_Iz$PoMkR&~GkCiEHNd#d=#!5Ifb9 ze%>{ZX#lv_3pe8ePDU)~Ie3Z8s;77bU2C?b@oRK(#thal(9vc|47#Yg-O_z_?^wU< zyPAV=r+j3*r6<?5n!>ot8w4Y>aJXkvalaX9smkuI?1+8KOuhuZOfs?37tmRI3_7F> z0$+%FSf?2*IVsL_{GJwawPl`NjL0U!2@i^EaJa2k3=i~>T?z*=xn*6_tP#9v*nI5) z<_-SQf)RH}hI*EaX)i%k4x^tr!lCKdbVaSWp&B(AqG3bSrJYft2Vb#N?T1igydTTE zgMNB|GmG!UAVuOFFeH<LWta=}v%(TsMO$$*<pXY=5mUjD<BFH!y4y}RER@a63+U#{ z=j4rAcI(x%7>@Y*jVinMTxE$JVex+JlOYV<2Y#rSae@!=KE>uOftG_A0(#BepXI;4 z_lGZVvxhf6XJRFNrEpQzzAhXxwnBcc!8AYS+)_>st`nV`+g?qZe_+P12@lG(empNm z(_b?XZY@3~PW4;h&(FN?x*uoFT)LfZQ&KE(JJmFsmEv5lI&-~fmp(;tUxj0wb(?vg z61lkK%-z4T5)o#UNpkh>#bubz`V%_?Q2%`b^k9Ge(E<M3esTE1@YhGA7N#Z)&Mqc^ z;)#Ko!Nk(p#nr{q#+gB1-xBaXkiP!!yC9H1sxnZ7HG5^iLtFrh=|6dxsjY{xvAwMW z0I%mS`bK3nNO~$+YBh;51|^A!DhavCDY}tyNxI!#DVotKRf!+8^i*^|#;F!*CdNcY zs7(&h_c4+wgMq=avUao*oRK3V(R6%BwvDhUsadN~@9yqe{@CHPW;(McU}rgiMfE@L z(A3t@(uP4+QdU&O)Z?!gpLC<@pn;hXMP9uJQ`~c({Xx!YX^l*~2b_aoR!B*`a{Vr; zmpLb{@cRq(z$A#x8EE&V>FP}n(A$BfE7W#y<x;i!rntW!at<x&|16vPWuS+vagp>X z9)@O@m9hjccXiXshQo*Lv&UuhqWD?RoiNK;tbu-`f4tWpeMx8OOsofB<M03wg?}r+ zf47zNZv|48(T<EutB;J+jWAFvUfnGzI*eC<qV$r9>gZ6FSwgp#4GxC?EW5T8xW67G z6BW3}9#m5x`wXoWF8Xs&K#HavcVM<rX?kw1e?Cn`GeHY>R$V5rxN$sFCAtxqCbvJI z=wT4q-q1|XNRcc8Y0%^DPY=mj^XF0lY@HEs<w*1I^#3nb2}2W;zkDN0N!I!=re*b~ zWuapP$}<IMZor$5hF_VYP-v>sY)zX-SKnV&qSp!u08qrFvtCMtkMkb&fsb@;O#*8P z=5-FQN*#?+`$<&LaM2dY2)SrM`&*LU6+d;x%^IA~kZbj$9@GXOQ;4~1r&{ybsX^<e zS8-rFWM(=Luc7GXOJ&kZ^5D}`z<W>Ls}NFZ-XCuf6=cve@!3~A(6)J&2r)OlvaM(g zSZ4F3ho&Oq)h8u>DyC|$?--7+CntyC=2M_&GDv>x01<{HDJ`KB&R;K+E06BUs1)J= z4-|}q{d}p`i>=$K*oCTCafc+87nPS&DVJ13%)CqG+IErZ)Ad}lzH^aAKDnEWwZ^)d z0dbDUmwhhYc;;+VJ^3LG1FnvFpig#pMqIKXY3B9eim8O{knRXJI8<V^3-?3=&Q~04 zdtds)SSpE<+FqX41=}tC{tt|NInAH;sQ{zU0u1zTjbmi!Y|74RYiauzEJbuo9ef`n zfOpk5WLS+l3sJb}hDLQ37f}%xRXps2w+vTHjQn--$edee1iSYq=j67QXT5YFs|#o1 z(Y*4KYw}ngPLwciud+j_Y7^^*3~C`!iB=oP6p|r&GQw>(FNv}0(T`PD?uMR1veT7; zgB)I*b)~M{JVa``g#4q^i)sqoE{ftFhJi-jr?EMe&H3Wl`s;|-Kbqc><b<zu0NFwT zkQ@KNxAU*VZ*1|KRQ9j4AFcSmJrB}Nl4#8G4Dr|iCnW13c{AF&BDy!e9WeLWV#t{o zPGBamA?-Bh<+bDqoYyLqLC5Dh+k|9J!iEVbY<9#IcW`(y57YT^l;EY7wbpYE(($<u zw48E!qk9(nXa8Z0fTWX2QX`GQ0c*LS5K6+hBRB%zCJN)gCk}-KxAO6LOuz@QX%`ol zDb_#7){cBrFH$Q4>Sw(??6fI>REDQ5M3&jBPT#hp=0fEPq%LQ8_+714j<x2U>3ht7 zu96YKVxbe?S;Wq$?DnC9WBck##IVq^JAc|y?Sayi8M3ZriF3M(26Nwf(oD{}GPh0x zv<%RP8ZB=O<##$FY~8NpWl?U;u4aOV^XFt)6R@SC-z6g(iOoAsBKJ#kZX;rP%l&42 z|4_N}<Pw<xz@=gV>)(beQ#)gO6H}+ZATy;UWtAl+q$lMiqvfS&<=9weM(E_0RHmDh zug0dNn|{ZjJt@IbD%uGK%F%(EA)f9LVyXtv?);9oH(*-oKf?ZQVPjP{U<+7)RQGQ^ z+sxF|<gbb2Utz~bQ5rzci@N@ZYHykM@Ci(}#epgi0+K1+fRLPw0jRUnwrbYMT8-%T zOX8PNNwd!;#)x?g&z_C&vVt2kjHJQ$>`--voXWFq@+I3d3c5@Ba^IfG5u;kh3hlx` zzUMdJYAXjnh$~D|je-qzmmw)}pVpC1`i82_>;Ebw(9|N={K)T2qY&%ZHPM7EyH_f# z9tnP;a*FxFGke!bIbWMhk_%w$-5nL8Miwha-F062sb+)~6+VPO-$3ru9f0w75Q!qE zFLs3hJ$M360X5+Ff5`U#IsxXUcBW2-F7|(^p-SN|Vy(_sG%jjR{@iW3pp6HZB5P%J zi^LhR!z1~U5d#lxImu10FOoK_MLlRx4M>yafn2zr)1MU(t4;%FzV{S7nZx{;MpS$q zm!xT75kJm5Z39ckx4=CND<q7o6aXn<Sx-}-Ti|<74%%jK!*%dxC6>UUNzMz2?`!n% z(<Ugj&D8$3$o~zklKQpvF)#(M?fuMiDx=%apjm$Q7yS$P-(}URu2}qS^HqSX6#q_E z3ji?Gze<p-I9d20Ld1~A?7$-Od62Zm(`rvJp?xe#M}?lo;hI4Hsz~;qHjuZQ<K8UL zaLT1sffht)I@#IzMy8r(Rtel+wgya8`;4~}uUIn-1de`;Y2H94;3?%dqq0UbTS)Kj z9PQ6?R7Brp>l%wD*HIlR3R%nwhOPIrW<D3m(qxHC<oJc&aBh=qXS#-mWznCRL516h zV@VFTlV<Ev8ZUF&T6~W@2k%o4I`fLt;^4J;MnPb0s9NZpAzJLtdII~q4ifD2MneFy zfBH}CZRzqabN_Yzq5(wO{e+03k6eSa%C-1}aYE|6B2-2vn1z5spK`N{oD8*X)NK0} zKekQ=NJ^G|a$AU)-XU7G2acqken8iJx=;I6J!fVOC6j^QV{84L2Ru&6?n9)Q`jVJh zy~~Q=hPG!r5f(l=@vGwk0{B{_g^FHrVe6Y{kelOyaUvM2IRV7#A1GhxUS9MU070?= z_XYnpeA@yb;0?|H%1uS8(st{Nup3iUjc1@2xP$`0CJU-w0r`j)-yTZi$<%Y2yL>3M z_nt^-lwHvACB3~p^Cile+KlavL#)z|J#BHl6oq@ca)m(TzRi^-7|^{&EDA-&QiyeL z61o(LJPgpMj<p1L#<ks#6=CC4X6n{3PKF@I+{&YyI{WIZ^f;ZuT4F_m-AcXLO6;Hn zuqzmTIVwWo#pF<|o0_Ve*Q$0z$LpT4(GZWd*V<Vv*zmLw{S2|d=5)^}0p}V@O-!s8 zT!3m)=_0$G>mD)ng~96_%N(qt+F*odh_utzA*S>g*ZiongUrtOP$yqXAN6RT2G?yy zhY<|#LVz8Jdf7e>qK~Ua@xn5;Cs_?XxSk71eY8v+x7p=YgeXoHFL~ZmMk{c-FfVJ5 z53dENJo?6*II<tkKgm5mulgA+PYea`X}?!I8qCWhH<M|4B`6H*j7%kMXJp?IlAU_E zOmn+8&s;|QyA#6X3o}H4UoJNP`fM>{(wSjq)d*q%q@GB-fwDnyoqBhNTPt=}>mhW} zwI9<et8vFBS!yrdQaKB%OrRC7>zPbWu939uV=o`nMCA6@oo=OI0rtQQIMdw^E#z$_ zFV!F{KF~GZC-zQ~g(Y5A1}Y;I%O;>-K+i891{0TOkJl@mKE5)1f6yaWxw6$(1N=Yd zKM4Z=C+K$qydOZ4Ec(bh2*FWhfuJy5)Pih}Xv8vOkU^D>bpN2HJrJ@zx#E3zOTd%U zDtsQUW8a`4cI-JBF)@8MJT!Xr6$_b0D3FO=%-ErY(g>04(FX5(YD|I_TxLXmv>GVM z8Y<>wJi&BaAht5JXm`*0VF4@?zkX4lQ#2lhmBP~fH;)xr=Pd>Y;!{eP5)XR^gBT6o z#{~>2R{7jnS--r*>jyUM&M~I)!(Z$Oe?RjX?s(zN0Fh4sBLCZ5=jdwh;AHu~tGFzM z89?lV^**9`IY!=th<l;~&-*3<4$qYC*Dd8bC!+r_@8sS|!{+f7KDl?fqnJ@-?HJxw z|M**Qd3i8V7o1s_S?nCV50l=w8T}w0_m_NRd~!@KNr9T6-avh_W4S@3M9}(RPo&go z5_mXp4k>7OyP%wt^Gfm2R0E$?1LVc|vxLPk)1Sy%r>dF19FU*q&P1W@KryryUxpDT z>cE|8g%8(b*jPuRw{M!XXWl#2md>01h!Wm!g+|-}(K!G`9yve_|Cr+bYH9zXty3Jc z0^HyHX2mWIWMb_Ea~kQKMxYe6sZ6FJ){G#<n60V7kEsyKcp&~_?&hXu{Id#(xzuF} z??r$z7{WC39tr;EHz5x-B4F@s+0OXK;6v18B7FV^8@dZ?XhKG%I;{%72y})`U{%lT zs@oH4L{<$!B2TtxQHN@Y^Jt9J>!Qa&QB#U}Jlx_jGZKlw#!Z899LBGs{AanBuYFM} z+o%X&<Y<=H0iMOvjVg|MF$W^Z-P*i*iGw(2&h`+i@TJaU@G*-gG&~U4vNm;QExjm? zC}L!3$oD=RB^(KCG%4^MqJ@a|PGtVxtOGnng>#kr8@X({7YDT)e7yDS@EN{Jc(9jW zJl^QgGKa9lUrse6O|?*E4eqUud#$4BD~CGXDAXq5N4!q^yD+8ZU&8vs0g<TNr|XYO z3!w4;nkUfzJN5teyMG)ge94c>xdc3=0$|X87%2R!Uis^3h7Jxkmd1uImiBhP?dq!V zFT0-7pnFONmLniGDo7#lw$QQiVv#I9e1^OeN<jE{m2#Uc4!AHh{F;@X)fqI!^9V&w z5I}21QrS$(2wDI!zREpRd`!1bX5kKO8x)I`J%f1$=jegp3I+Fd510DtfZFmv2kP_X znN2;s#<F?*Vbhl{x7DrNQDq2G7IThJpa|cTzB%nHEFPcL0%5FucEz>2#KeIuZam?8 z2VdTJIU@^FB9YBaa{ElvByB<tZ#58w{6eNot(@_S{LLhp;SrMPY4lAwVtrdY;^v6T z*2`J&JJ|8Z0w=DDMxu0!XD|K*fqH%=>%$*lFTB+vGrzNc9bo<2NboQ1xtdto|JIvs z6gNCD6RgN<X5YaDw3C0bKz?qnMm1u3G>K+JB4tkCny~<DOWjtKxzGeK>BtN;%t}&X z)N-rAd5=<k^?9@IC6_J#24;h}&AG8o=n>C*_jm;mo;*<I-oQp{nIbHN!xOy6S*bft z><6t&{UWvEYD39Ok*T9r@T?hUwqUy@7~A+C_|MGm1}C-vN&Erp-`1{wN%Ol!{Vfa~ zKo~$Y2lL9=O_l`Z6M%*$g@$$xSxBgiC;PSU3T+o##5jS|Y2-}<mB1+ytRj6Xkv~en zWENQo{pb?QT_N*ptIhdn!Jsx0LUAMyMV5L-U!Wl6_4}v-?`8=djH+Fx%g}wO`j7fP zEo<Mu2jhJk6Ov%ShW~RDSla%tvPCzF>32&l`k2{gtStzy0L2~(zXKg_iEBb7mrsu8 z=>;1G#_;gE^=r5aGLDdfhSmf?kx(*;_8p$K>DVc<eM8#qw*|-n7N8B3$NtbCTJe|5 zh1_&Lym+6c97OT(g#dP&Z$FRqRcX_(PNu9On|>&=ya8B1j8h+A0r3CJ0!ym4md*fS zk^t);W`+OlH2$|LOjr815nwlbpku25HXvKZ#Oy=M|0HJ$kmfT$8d_5H&8zH927B3f zHc28h)AFhlz0kWxIMnkAvi1J+8LOv*Pgm4G&a^?<rD^1U6?_45>EJSsQZgOj#7gz< zD{cO~_JJ#Y1cNl;(FMS&6?DJ)qZPFg8}IZ1?ELrWj{&g$F){q*KenzmE|!3z;IAP7 z+xTAvamY?*Nt!9L5(1(HMhJbIL^a`R6ytU&XWIzi8@U%Z0)8HSWRUuz+;q&QhCqQ# z<+9qYrKJY#GWl9!{u(-!(dPLb{b5nTtr+~MJc)%gLB=PnC|32@DEF#+So)@xH7A|V zYhOq(N`AO#rXblNEGrFwc`E4t-KmCU6~OfX5?BKk^M53XoxR=Pg&Adn_Gg3@nCUDT zu@H3nB;MPbw@G9b5i*d%4!@~~^tKj6V6A3QL|Q6|rEUc)DP0Y22tW6<+A3SxG$qZ~ zPnH6$div3e0Ke%LBVg8ur)wO}i`qsucL5<F^7uza`0pdv|Lokl{Qqp`iX^S}5Dn0C zD`5ST-~S$80AREJJ|h>6>HS?wid;TNfHB+~@Z~}X4g~2VrMS$|+p-g_1U`Eepz%x? znr*@kXGR3dqdZ&5P*Mdpk28Z1EXrmup@5YJ4>Hi7?ItI}l6Ecxk*Ls_x9Iqxdi@$v z7Q*h;vL1)Ruj!~W-@q23r8(|HV}3z12d?bFL%S7|df+}-mx(AJY!zgj^~>+(D*dB4 z-d?z5zXr&}3z&-hlV$!(E>{;bea_zz)i!RzDufZxB!7v}q}Cu1u)uiNT(|Ygi&UbY zks(DyNsm(4>LRaGsWC$_JIowUH4dFY5k5k56STMix#GcI3i^>q^WgxtWZ6h5Vr~lQ z;E1>5A5-MxJR>Rkurw^W0;v%r1ef+%RG@-}(Jr$^Q-5hla!ug0Geo@fv@Q56g@yUh z^uUeDHO&P&zq}Sn-e+p|Pv6<g?4V-_Ej7U;_ANEB)7&(mC_}E2<D>U3@zx+vMt2fn ze<|B~nX1_jUQ#Y}az3inh0XKQ2l+lDPl)zu+%sIaah^B0C0^05brTGp@T?!}kZ>XF zO=6Foz5UTv=O3kU<^q%!3Ft&w{^tYuuOs&_Ix>J?{H@Z-QvG4~n}6_>#v>B9^y3o{ zok59Me7@|Qh$Mob0OS0KnQS~Q2KVGr>b#M1%r1Rfm~hvZ1}6<mO_FwH8Cs-@<azXo z0eWGKVCS0wBH^thQzgXgJfo8B>|<D6t?3;`$?{x1ZoBl^gg_>w*o1^w;Np#X2t9XV zrgvuHc}uuHBUeuD)ltN}6x&a8XnRHcP-z8Kj<=J{n`>R2-~4pOFZsX@e6vCpVpk>> z4ph6hB<Z%?A>cg&)vhRcD8R9)Bq=G(2*!o1%jO1ILdX*naRD5m@XYS2U~O?XZU;YR zHH`gfG~i}eQch&&$5)c45DQ2#Jy9~ZfjciN(zADVZD!lopK|bpzVr<4k;=#Yk}5?U zl3|Q=wDMQSD*w@7$Ph<DUTJ=}j&gSa(v|?MCEdA+Y*TVGK&E?i{NquqBA#e%DCLs- zJa0E)c{mISLzwTtlg}(~^8NW2bN9o&I%Jul{U^T09j^J&3;3hnO+*E*N%<_Vqq;cl zz8GT<?k`NftVBuDC|vIu`z0p8YsuhB*@240ln!qYF37q1;e^uDmP;yzrx{cd(j}Zc z!y#daDqJ!}DC~IT-+z1q_W-iVgvOvbgu~i)`eHJ1X+Tp~M`i)|=<HK0^?Awi9?h+9 z>5nRR0mxjSuV@9<7v@6auY-?@v|G6LIueTYx!KKh?yHA%^CHh{a6dyGI9g~Srq_=O zgKuhTBjLgXJ^5a(LLAriHshtS28Eg*1Gw%<2uf*zq@(*k3m=IvOdx)q%QzFu4d9nB z#ANL2EF>9B1b@5q?xZso{$+3=(Qf;B%iNM<qgQ0%>e_u_B;&)bD~vYqp=Lvvg<S}1 zm`xb;`QthA1aHUtgO16%K!9Uvx>Q{D@hgOrXJ-yV_aC{_>*18{2~dOY0~j{{A$R`i z@DBF>BEsrSR<vIuL>%6Fr}C&Esq^-i$WpOoNN%etukR9x54q;b>-GOs;mZH+KM*Y` zY4&~Y&17KX`qg<@x5_lgrX+2daFMc%t_=xKYzy+JaNmE%U5rlbJNlh3-bdv;<_6QA zn=jQ^At7#oX~}l{F)dBXahv{u`rr+8fl_mRTUTnrRDG0_5o{Y%RxW~pmfh_NvG7Gj zt{6^jE0CVCi6%&WQ{;CNHMZ1&YTc`6Z$4rDF3-gf{f;mEDD1uOC8&t6)`Y4Uz@S01 zK*Qk0MW|6_+S&D=CEDKwRs7_?{AzHjJq?)7H?S+AjBz9$d(r@6AZSeinzAQy1;Z{U zS@w5AUwP#@sP_i8GB)Tv1HvRJD(Z$}1VJYE1YoJ<PkFKQt<W3?Nb6hDT%t19ci{#e zM|0ORj5w&9pX1yZg|A?NW!PcWB$S#<1yBw?7CB(I*n#QhVy+u!u11q@@TyNvcQ+>2 zc@mr{B||g@K0zdbW`=ShJ=l*}dYv#z3Aq<AcdPrHA=JHumIj|VTCE6fK^D2JMPxZH zcy?KML&)%`L2$}U7K+Gx!VOBldvY%`_^A`6$|HIf<^qee>NS~v&b=MN!IJ%sLIwCf zXtXF`Z`)(5BU8N}v<@RWn-rz&y&qM>?wu93fe`lM7dO-!ny&+5=E(COSC=EbU!Zva zP81F3%>SWm`M;d#|L{3RD~|!_HW52sX*gFC4iIWlsh<7IlRYbmMGO;dj6nijwSW2a z_A2hCj)FC?aJN@}O=O`f*v#i+v8i$#fNx`1sUTz3V%03(9xyq=wb)(6g4#S1ohUEI zCM1ID4j6-Br*;sdOpLzD?%Y5=i3&mcnOPvV%kwy$Ddpx?+;HEyDqiYi(q5{-jC=vx z2y8Xe2aP)WM(+zJnzMLs4yydkSi5>oSUj;7fp;2NU|VZG&;aU<xpeLwQk!w(hKyYE zxYeSMvLxb_(FG>vg_~v5XDFg~6eMKwaA2*2T{PK$aXhrPJEWu!VV@aK!EQ7%hF<;H z{Dq2-?n;7Tc+L#yNi;$1-PpS{Fu8;^vOH#zAleI(3<Rzy?ALjgjtAkhx%_sdI19Hz zR<OyD`ROf2z~^Uvd9S(~M5V(&rX&i<Gc-Z~TRZ@=*#E<U{yi`Go&Teiw59tQQGG9K zMvN(n_;YK&!ZAQH3*zyG1+(&o4;M>vNpc+3aFiK;Gsbx!y34$Dq7Nrts;m7uV->1< zNm-k|fqGVla9qi_!j8oUDT1P@`OuN6zcrrfu4+_#?E28yqHnWT2{iboa0`ASc$BpS z){b2GUI-Ym$6SFyRymc#@3*3x7$cNGVFPo>S?9hH;;`^=UI>GTv<9H1S#fM=pFL&C zqCAgK3M}1%|I$FRxC07<NtQb(ozQ*PWMtE5Jusbxyj-CZbAJn^HolfNnw&A(=6N(w z=@rXD9hw(v29qw3Dd;jd|NNd~Q)h4$l1JeZZ0Kwm;WMLRr0yd73rW(DEKSiV_{o1p zcEq2CbHLAj@^)UYExSN>fOle7<%+p04}xNi#hR-i658-)1BO;Mf+Cgo0aK7g;wkdu zj~cm<$1+G0V2W_CPoKE{^Mb;})z<dQ-^vPCl{LG0Mns<}b&XxJDj`0DB4X%sm?R|w zFa%Y!FIhjTHHX=|T%h!)o>DfVS9wTT_F4}zu@J<f2V^tG=ynR!hT^Tc*!C;jbR?;< zorbHdM?7a0R9E`XVW;thBYNeU!yt0KK!g>`m|Uo#-3>C8`qUN>Ph#{qtD}t!&Lu&t z`2ue)7BNk+`x-CPZe}|%=N^1<OH!z)0zy)x6Ch&XhQnIdoLowlyNI^tjv%5`E|UU6 zy?dy6?-~x0uhz^dC%kXB^Og&gRMAl8wVY61JSBV<onzeaiXv4-uYNAaL~i=)pe?WJ zKfS}w!Nu|mPuyYt>{(KVo<V)zh@e$IRNkx^=}d@IIW2X>D-im$$^q%q_{bPny_e8l zT|pO?N2FcQ(Pg#POoZ70mTDp?E`f|~oAB_JSU>LjrBM@_AYHL<?7W4^mkgL*{fo2{ zBJ>C((`Niu7kR42a%PqN69B)cr|=@TUHAqSgJ*p}BEcme^~$Zp%TSJtX$3`$DF_w1 zd5nUz+0^)CWsU;N8Z);N7oQAX;CJXJ45LdV<O-@FEnDBcannl-#?Fp)0?+nOROZi; zA^IlA4sb+-$)lok&JNrsF!v!DWa5e5IK&R4`a9I2Bn#qZjMux%hkKd!&n!8psV*dh zbbYET0f?S>?A?41#yhbPKT_xhVe+Fd8Bf083k5Q$%+jU3bTl~J_y_46aeul|0Y~LT zdm~6I*s)QVQc_s2F;Bx_M1xY_$cH+VUH(q5F6}$E!QF-czD?k=c*^K=4_v)%=)QS> zLYQj)=UYR13$srofbfwI@LBHvSAR9Ox3e=f{@)Y2=!7-9U_t;B&zFc+7nC*xgIV&4 zO?F<=PL@!@7#uB7>gD<G3_;ay-@ne|U>$z*lXD3=-LV`%5`5f*b7a>Le{{TA#g~uK z=5}nEBXaV%QskzA`+78kBh&qZ#5n7$X$EsNO7(HlXd;c78I+ULT2WDBCnMEmH5qo6 zqLEJ0lYJeN!&>0p*Dl;Fg4C}K#$wI+G{K?{B;M(mQUqz*C6oCk(-K0RLqIg8cI#4I z2MdF-q5IP&R)dA;hOttc+y!h&BTKuBtg}F5zvxu7O3e`7AkIXa&amdein?TBH4EjM zThEm{H;gcu!w62=;cbFR)wvgm64jlfwd>jpKSdvAD}v<_JGwLxKQ+=rDK9!*XKHLy z@tb#f^r1fSDc2p!-@AWm{&`#{xKE$-SpRXrZU6T+bOT4%d9V3ew;MRii_xcGIlWyb z<9gpo(Pp(IoVFa<rwb=Gv`9ihNEC@yP)g0$2jf%kCBllG{t3jlPY*DWgBM0X-NKQa znoRpMkc#XcUk_OY9wki(mQ!-|NwD9#@{pb*!6(64+;*ln?1KU@X7IGg24W?95LZH2 zBjW3(3m`|9VGTcs@*q|B$bDmmhm1|W5}_-om5B#_O^P=U*+D^X7wIX^5gst&ZYCZ_ z=0AKNB=`I(vp3Wt1l3Qui3mQ8Ss;u1_3Y%vNp@GZewU>MHugFcJhk1}qk}YxEKS$< z?fSACR;lZ3q>%NRiL_TMyZ{dIZ9S-QnhTU}@L_aV^EH(78c|kYng|<gn20%XdRUk> z#?fRL)uhA=_j%s!>oE}_E22RpCpL7O7^ZN}Il>Gcyn{$4k(L3WtL-b_5eq8xe%3EJ zhi!+xAw=18JU3-A<<G;8$UwTcn7cJ@temNHEeb4<=N|n{L%-m`Q^NVE6wb$)rzRl` zKJi}>)PgmBC+WjtSz*IOjI}-lChrwciUh?M?e-B`9VU8l@FgSfO^9PLT0FrtHrp|f z?+LD+j!^1O>=~v8(y$qGu4#eKPJ+&+r>75|O&Mdp`r*HPTZ@v&+Z;9~M<(Z!gHP+W z{!F^;W&nrY_EFL_JwYQ9sL@<aa2S%p*BM_GU^rnP^>e@7277U@V>x4sXL8}uf_-cn z_SCaFR%T(kTUUPmJC|RY-q;ZLU0A$yP=m%9X!XtXcjM(<?z$<sQ{EeIuL%X2_(%|J z<HAqA4vt|U$5AsD>}D1rc-n}0+)=nEM(m(3qz-Qb2ihQNTq&DD_QiPy76o|Gj5m;J zOrMv<mm}R2(2YZQ;S(4IL3m)$&;0N5orn8E2W)~i#u%^P#}Ej7pEa-Cpq!D1q9X#R z?a;TBRj*vVhdI_*@&o+#GtMz-;8cf-IbKZebfeXVqJ9#f?7d96G=BE5&7cd5WB_X% zP5Y?wIg7n^Vg03tdLIu;&s&@M)8j=nCw4-n5V~hKl8K}0$5Vyx9JtZRBa3nNZ5hzG z_#Dn&mvBVLM=XSoRg8F|8~)aKrT|D!X$Scy2MkikH$wO)E0QZpQh_Bze6@Hy$Ea`D zI^FD%N?D<XsN}0(nb_>2e++nxtCtWMo5Wn+2kzV(%81=^zq|3w$R}BClOjEh(}ac1 z5H{Z0hYCN(dv4hv_`U>B!jnEd?Z~w0t;MovoU!O!sP6oP3QYxxBMCa_CADr@-m_i# z<QEP@^7@Ir$_5m%d4Fkpk-0D(;c#Wi_~D-J6`Qkbg3xl>RAy#$=aCAo$WlVrOpz5& zqxc$ZAA|!PpDonq68HV;8|0em84ai%Z_syIj91bj?lEKw;l{z?3RJb}wAoMUj*FZZ z1MxYkw2Lup!C_|<EvZb~;#6J!iqFH!WBcB?sJ((q$VPFZ_C?OE4-|z!REOpM)X0Gc zxn?1L6&i*!bJxwnAPCD^>qdkR5er5-ScE08^Xr?ODiDwkXa0zUo_3a&w}wn@bJDyV zB+i9XM@4)WR^|w1MVE$o>b+f<k<9b`E_bf1*-;;9Nv*U|+P99Z4DLcsx`sBi%$K=s z)#AzxafF4P#sv|hy03J#^sbYKp_V#0i4n^zKEC>9Ml$xp?r4;^iyk9R->_VGVrI7! zn_w5lzrOIyj<?NB%-pADu8cExoj9X~e`)iqm8{Itzu_lxY;qO}DKP2`eSnl^&e&uF zwPk{yV;!FRD&&ny73UA!Rx6$k>YbKxJL3vOPr=G~X}<V=tZg__Bv&7F2Hj6&kQ@d& zAxa|=e?@;67AuX_a^(@{XKhds4pPQzc4W!CSqJ4O$6>ffG?CG3mmWuVIP8Ch^P>X> znT{R@ffgD;4hlCS2YI`;=liPYVtng97;azJCVk@@W4=hOTGU&}?foq+L-|9c_sLab z+w`7j3#`$l?QZIbf3*~+9@=olp1`EGjkA4iLyofY?j)jjv-}e2bTEa<C~t}IY*z%m z5*%ynr!G!NYaV-aofDJ9tchmqU&$c*_$^bRqO2f~UCM8oZ2A5!9=eIuoEa|BZu!+D z8No6!x5k$!kHx^{jc`5F*F&2@c<rDu`bZjSej**?2HaR&$cUl{%_2z$5W=9u`{iZB z+||DHrW&Bu3G6!JhOasvP=XVx8jLwnVF=6;tVEUi!PC{|Db+|9>D1Edk-ts?_V!=O zil9^ds_RDsHHO_Pb>ivzLl!WJ;WtRcNX-bUSTa#i<IB6DEW?&1pLCKH6ZVdBD)oDj z?1b&VD=%6X%K$eu9LRWKl6haEo-3W@ztqeNL3CG?C*7FEbyyOg@86RPmL$DOH`-Ma zr4Gn>r)8T$Bj<0EiPnvqUi|7K9Zq5-R!kbKHch!%{7T)B%3|IUsRbsw?LiOcnES;F zYin%=dwZ*U3;!uqCKoRIGe)qR_h@aOC89`_xLfU^K#Dv9m7|5ti054BA-Q3=1yx$l z_E#O5srPBI!9B7WWIV(!PH)&u_?4aQuiv=};_C0L6Xgo%Gv*0F_i2(2z=bspr5xR< zKMBDDX}BU2K{+Ne&{6XCHGdmJ+piBJo7{m%!u59&VC3S&R~t7>z7lAx95dvGH<L*e zU+yz;D}tP?NDo=q?9ptGm~CW4uGPJ-jm#8gi#q}d$CU&oz75ahmsp~3GL4t4UC`K1 zLi8vh1F>IX^)}R(ougX;U|0C7tW#5yS}*ow4Q6w)flxJUto#a;DgUIY-+>3^DR@Q) z{z+CrdFaQ?>h^ay>ouDxRmT9N@HUw;09Zwv22uE$5=&eh_f<c|ayF<9aw(EVa9p`h z3oRd5tmVABbQlod9pQ!Cn&5kiI=u9*(@2n+&0w&!+Da$yG@(1TFQ-%@&yAFTF>5&P zTgF5ZXh?`1h{}>y^J)I4j?@lpAO!8MGy%lgftC7GhbQAa_>?QlMK~3iE`_T^d`-$w zFW09A9rjf_Jyvg$d#hYu%L&T+=5;zV$J_}#^zEN{agh7zy5h_$q-oaTgxb`aVjE4y z?YuW?lhQ!Z(qBpBlUty#<<r!NDn^|iw3Mij13QPMDi_i_fw9FUx0eT>pgz3tIErak zW};Udy=fJvzNZ22atT2yR59^Y)Cz3YmLLhwqfq~#9EkZmz;-CXg-H{1O(K!{MA+`& zE?VAX+}MEhQ)VOcmbN6MqamEq_CZUwXC2&HMy_5I>6<=7$*(@veE&!Gba(AokGmpA zB|~%Wg3w=qc=_xSbHvFUqg1XO@xiMfYLS+upbBW-s*cJ{9-KR$l3=it1j#4Dn74?0 z4Q4x{R2(u^&To}pj*Zec1vz%dIOFy7j&w#-L7e2NxkS0db&Hb9I=4$COj~WBkz2D- zkH}r${453VS=Cr076kSA)<yz5Tj0NwjI)UVaK13urk1ZoKWAktPKiEqZeDpkgbn7S z=rECH2aJ_CCqaqDsITItfNk@$6SRUo$J{zAu11MIb?+&v{7Bvpjm*HjIVCTHf7<@M z9Sb60djo}FG4fqdaCtbt1<o<#(Q!QR?Md~ff7Ffopj%S49H@2PFl<HqP96%^xk8i` znW6A$e?K7goh5{#T|sXBkQ;&(G#on9k;Wq`A4DvsYBQ=-X?w6|){tCZya(nX3RTN4 zsTj&XbIj#*<h83Y9zOE&^QKdJnT`JAo>N9ECLVMRLCDE*pH}reO?4&bqUri8_?~aL zimcfccpa!A41b~qOhiD_sF$>Nw?dd{ArqXZu9Iwmtu)g&gigAdJ*ThM6ZL%^`PQ0* zd{6JyBE<g2-D1EL$cLNp#G!9}k_ED+mh|gKQ#qC+8<0`H7g0|qjAS*vLW&SH{8l_u zu|PMdUnkYfiJJ_)FT-^St2cVABO7xta{DmbbEJnM#&#b`Rto%4i8=}6!;!t&L;3C- zq&m)atJn&Z-b?~mypVHamb{U<9eCqwSsiWF;E$BROs_p@%E|T-G8#_%lF*ZP5zz9; zr3myz%M$nH?UCfvV1-^0pR`ie;1v`5JyoY^^?9NBo?N4(z*WK}BVaf}uIZy{g`Z9Z z>L*t&UR!`$n<b{16MZgaK3k+5&Fz?tc@N_G*^M123qmz2XqT(AVE;lLT|L<mx+tB4 zfNgcRq6?=$3KMhJa%Spy{37ak7jA@~brO$4%ZDr}d~mHUe_leRC*cem8M%y#v}N{) zX7*&e?g__N`y3R-yySC<D&PDLy|KCCV(Xt6rN@+nV5N0l4f&vWTBrJjyn^JzrLC1C z-lK#4@MWU^U1pEsvCv<efrmH7zM+JtB!EH2n4KHT!<z$7RxV_*J9=WyfNQ@GHyt4o z+r@Sezjc6c1S+nWubUr=*~hJ<NnH_}J_N1aL(I7S!FOX1jmG0AODBABdS=ODOL{{u zy<eoI7yeXUj8+}?p#IHKu}@z&#fVJox7xRyM4nFL0VQ@_W><&vfrF_<WoP^EufPir zR3BR33~32sEYHKy1pLLc@gc9jntQ8sVyKZ}T)E(Xp?681fnc#_mE3~nlWLH|8T{_| zXUE)u=Kit=nC^^Y{No4LfUeEK(8c2SjIByz%O0Bx(f6n(O&T8<t;k)nh9a-n%Il<G z(pF2i#VefHBn3#v?VNWFr~duaerQu)fJpii)wd?z4>O)@EWb_XmS@gjm6k!m2{K4F zu+!qY%#Jo2_G-c(l^{XkanQt7m5uPpuSL%A-|4?;q*;Qj!efTNd@MqDnTS=MDfMX= zEWC!W-+Wb@0j|Om`TS++=LqS-&`jmc#9=cF%V(g}5~YbeR1X0ZYo_yl^V!v}2-J_U z+U73~k-tn!iO>2erHwyQOP*?S<NcE6BUYazoIg+<X~}s$*j(osc5^LbprfOf&y?`^ z63}7#(CxD(JwTVp>>iDH+K%I|S=ikLiZugqq}k@KF0k9weLUuH-28p@dPx)O5=T>x z_v{N#8g%k8uIa?c!8SMBB`>B^)YSVP1vAvfd6Ta~lkB%J>ItwcWZV3!2Yn!Z7~42L zHS%hUIUF@J^A`vUUM2XMD)a;~Cb;*6e#A^DxrO1GONn_#?4?I3hJ&6Xl4}iv5*ks_ z${t57Tjlo5H7|uTvm~2I#9MwYE8U_i-YHs6XGM<*WDYpBZqbl*b^U_0NNJ@0W08iz zYwW6}!0=|=m~O116k9pdvE7EJlr~1#*m|cyMl?#YAd}MYp04i*w+lT$zSjZ~RV<0S zZ-1M$vQbm1W|C5TZk?{tGkM?A*Z2PL{xl%xqe}*7i`Yw0h8AZYE_BHdQR!zF1j81Z zC0lTBE}m4JJ(sIc`w#AQ*_@iLaK|_&=p<uT4nlaP=vb<S+3|d?*+*@GDaN3i>Fj|h z@M%FaN~zH>w>s+_dqI>vg98eQK)5tsavs&_=4)v@a8d8tvZ|ejG#!jVbPLl3zPtdo zeRvWa)7RVx7lC(+oQtoA2YFB~0xdaw%&}0JAdig;;ZLbIakB@6=`n5Nh)wy%OSkgc z4uUYAYPYDxb=irPW2j-YnsupsGi(svaeXRtphb9LlzmApmgq>R8eHmujYj8=#0}wU z$zLrZrQ|%-XUeEC`iz)18&<cx`q`J;6QZ7mCzfVPh`gngoRrUgR0@7W>NSLSbp+BK z!!dA>y9<X*cgJw_6ko?Lk?FE@J-P~zP|q><zI_9xCyI|{?K(6q_J>2@xXX2cVHNa$ z!`C3%K!<b|Gs#3|(qc{|TH}r`!p{6LY&Y8%?yH8&>DeGwUz%!yuK8>tf`LFw3~r|4 zjZ5*#k@Ykf=QXA-ae%L#cQQEsN3HxE=l{ppI|gamEL))MX-vC&+O};>+qP}nwr$(i zv~AnAZQh=J&b<-)i`Zx1A8$lOyuY5Ts?4m+Tx($$<ozB?&z8(n0r7r81GetNgzzh} zTI?N#qrspg+m8o6$Zxtm0HN?cPbvmoIG&aJsF>W%W~BkATSU}?@l#;R778y^YvL0R z*=a3ydNLllUp$(vRy=sdPZGfp;cOM#;VCbk^)e3HeA1PO{hrzlM-=4?IAq}fVTndb zPUQ7n?MOt*a}0Vu`L)gmmo?X_JiK8N+=*gCi<6-2-{VZB_#~0>uw&619fBk8W?<~H z=$}}?SsRs&S8H+v$+I(uG>vNM6pIwija}RYo!dOnArii{&Fi%`WY+{(V!L9Y!GuvA zJka<c*xI7H?%hx%@1G5DWg5~^*vY-zd7a9yD_l(HO?jQm_fU^ku?Z6^_fVAZXq?LH z-P{X8cIvisZ3k6)=s<{1tf5KL3<oPRJgBq3o8iZ*9qMs4+h4=Z>1w&NPUgPq9-@m* zVh*bCU<tOvUQR(b3d?oyuKAwZgbx?Zbh%#Rf7)Ri>F&kl1iSRJ(R5L3dsJGzW<Uh7 z!6`j>*Yn=c@ll<?>)54zT^rdoOG-Ad1v5xt+7x|=vEw48=SX8I&c^jHFpe)t+v>5g zv>F4x9SfM5D6Q$O>7r|KBQae);yk<*vKk&>3OE)0rRIpEJ2po+U6|L-Y!7nSsi?Ae zhhg0-LORR%78~qZn8ssh(|N8m0ReWo^Wc1OEYLknK$T#}rgbO`EMGg+rKKnz7Is#w z4C;X7w5{!?<g;@kyL}A2`Dx=G$w&&2{`PRgB(MkS>mU6wjy_dK#P6c2_;*qDKVxNd ztqd5xOa4ZV4vuz){{zEJjN=vW|DMLU`alNOa^elg#YbA-%TfaAi5E$+BzDA=<TGlW z^|m%kCejkHAH(QmxeB~a(Nl_1MNi%XXD5LHz9z>}X)tw*#@BfaU^hHPCmo9O_q}H$ z8}j)K<jkEG8g8RnKmXOYm_aIzh5<wEG~Hww$*o&cLO1oL`^HNAm=Jl#>d>9r-<L1& zn=F;d!C7=wh5JTNP81k)bWE+ZpC`{C@}5q%vRLC6Dodc2cdLZ&o5_gF;_jjOVf;g1 zNZzbSE==ELo5}{1Q*=#BThQ9YYl-l<D>JE)x61|&`6J8o=TGgl#0$C)crVsIR6feo zvF6L!oTtG`6hs>hY~vRM>%14NZO9@W{ohoABh3F8n93McP3iqEQv-fm_CL=A|Bq$$ zOzppm)Bg&<F}zm)8Up7YNM->XnE<4m!D!IbspTCO(~%IeJD-;zZ06z(k!frTJ9tlh zvHmovT%vCR$ArK`5fLu&7$aE~59G!f6lq*kE0wW>ZTXoIwe2-5c+CWUG%w)K<Qbjt zXmCv@HYel;c}YKAL?-4MypXS^u&F5SnB&7SCJb3;a(>?MU}bF6bUle{1SLg(vE*nu zU7NYGCc3qOA>Q7xHF#e6e4_ch+_9N4XX@7L@m#(ACFpS|;EihUlgwZB?}cL}&uIjM z@7`-U(to$YUm%wLzknbUDr0{KRoc%ez@KzbY$AyP(|9U@kwn8DrypbP4ZuN&D)pKW z$YK<B@6X1NiC&0)c#V|6Dvb|~PhL_=7B8J`q~jApNc$4X!83r)7RN^5&3eX-G>hoY zpFMC!8@DI~9iv<ji5c~`?`v?znLXFLpv7S%6;Vp)ieQIf;{3MnE7UoOS-%}#)=gP? z((Z*}_K6z1+l-+}`W?3m0rCUh_iCnEqMwYE-YSPAzD*T`$U<gD0+Sqt7@X718Yzg$ zi8B6rzy}+_G9j}~UW|NWe5HrI07s5z`v&A7GK<HQL%KzT@I?R&h5z=m@(TVf?~l&j z0FM$H9EpwjWq-Lj?!vU6QkCkD8eTms>>>^QQQzg_n4oUTWc1zW01V79yd5CDbWl0) zD7M5M>yUBM+q?u*P2bgd5962a3B|N?rMwqyR1=Z-Kl>X@MYAONGsF2#H~jfP=B%~$ z)JuIt!`*jq!aW2_VDmn5&=P|3X-N3JW-m}g{boOB=y^qhN1Q#v;gD+7ewBEbc{iG% z0%WrLKNmEKLFtemWaA6xkgq$I*d;UU9)sa*S)3@tr}==4=>Q914SHBd5P^+1U!@t? zBQei7C2Pt;WW6oGBwzp1iP8Xa^uv@)9aWL)^DC}Jw)lD!$1R4J-1(JUo*KnkoG2xx zZ<NxdV@Ww@w7CG2^<zheB6d*<tbE;Jkhw&0X+oFf>FI@!Cg-(A1fd~E<(DJshn|_U zG>X)6rmO1qt4?OZ0C<y_iDZYpn6sXhA~d@mTScgo@dI-UW#xKUi(Foq*VJjjxDD@A zl=UI{bBPnRwD_Y$IdC?1#zE7tI(QTfkV<>*UK4C5a?9MjLu%55@WMi`tfH{alW(0Q z8;Xh-1!j&l0e`-oa?s;zN<fxp&W@XD69?QyVQ?Y0pOtcL(B(09RgN^7eziZQe=_+X zg&4+$`KnG+aXa&*rJ;2Q5m%-DY!s%wt(V*tM{89=hTNcNN3_RR_>(1N@qK6hSwCHJ zvApd{q+j_QSu=Z;->X;bTDU2`qR^oJtoPyWZu{>pO#|J6Hs$7irBKcwfqu(2LCxso z&wTkg@nNOL&QEnc*whIciFVn8^V$=AkG`qSv)4&rp4#wIC-WzX+txD3R<sfWETM;g z05~A2IiVT8Jr&8f+B3m_Y_9!}x6-$F`s=C+6~w=J(cn7Yl#h=otw<BJezw&#uz}nj z6XXW#1T3Nz>luzvkflp{dk(oR*NUo{c%nxdb-r|_#=35hT86Tv6P7lVg>tcIV1qw> z3cF4YulpHE1Nj*yP7eEFq{lkyx5P;p?1w8UB|=tSUE6`E>fM8F(i+)YO>c_*l!&Wd zdoU8hK?%>WG3~Qu)1F9<23|7jNQ;~-zp#6>{2XzQm!s7mmM|}zTTzFI8YU!}J7LuF zI+Wlsy$jg>^P|T+>fT<j!l^1bzfR`U;5dxk$wj$YoR-MkYV&dd{%r@q?zSMoM(b|8 z)_Gk|bFSw0R?G%MrTwtZaw`V7X)W9iRmpyTJ@ZKhUQAs<OddEdEyfwvppX!xl5tq$ z+8Fn#bBbz)^7ILDIZ$4<|4|0o0+v0JVWW}25g}akSk**F;50$TwZc8D-E7JD6>?R_ za$k+?)<$|37R?nW#*GNUc~?$Ud49~c5(O;C)N*zrf%K?g!=;+x3u7jlAP-w2Q)3|_ zC((IICpbFKMq>KdG+JIV+nhS?2kC%G+R}kG0iyQGS1eji#E3)Y#}3czbN`-`z%zj5 zx)N;ZJ<{xKyOIX3gU{G2UZfH#B7>c?@<Nkt+CoUU3k=I-a~}-Fx~_BYs<!lvG!f^% zVJPq=h6#F&+=!Wt9L+Y}TXc!1y!vo{JG2|oJHVrm#(SS*E|9E#g=lq05_8!u?&JKm zHqVAMU82oxlKZ??sQ6&K^xHq60)*7z2f>g40KWfcQ}KV#oZ~kz`d_i317po<t@(CC zJ9p^UHJOyBXd9SQZCE}tALdw0f>x6Xc%9%Hdm>pSA;*o^2b$cUTnRF}-3<Uqw!Riy zBUVhg%C7KUR>e5|S@oeEDbeUyX_m0;Tt&BYiY0Exy?m>C<VWMn%MjzMe4l${0<n*n zR$lE9cvz(EP6TWb%z&z;Gq!Rn5yH{xmHBAG$hXnkl(>TGXU32EC3$0pqfM6@M&Y-- z2}|?dYpw-Z<Hvg?CaMW)x+crhCD<mVS&gat7flG%`gFZ<R?7?Z5q2h{AXNaCS5LZj zCW#MNeXH_%p!}(>;}#F^iS=8U#W<gnDCwo#+qLS^bY1P2Yo}TH?H|X!%Gtlx87|~Y z?TtLBg6#X+s}(3E`WB0bG9k`_43ly`Uz#%NEgZe{ITLCuDZ_U9-h~)+KZpoKY9CB< z!QKrp?w%%*D%$3(1PDXgvrKPW0!vkuuuRPuB)NL{Sby2k0khSqUhxz;<1TQR%y1+S zq(8tIx&_O1^!@4}`1_Ygku%`}huO@SQz(G6rMw8hQ0qc?!v1&HHiEqa5gryjsCa9E zE$DZ88ZqwW!c>i{Vgx-@J^l4rr4<x?mwCM%hlx=q;#I>{)GKym@WQj3&!A3Cqf9Y6 zpJ_I{j@c;|kmW<GUy(=;5?M;wT`6;4%`liPEuD#q$j5whS8!w~eE1bOkiUflbx3Dw z9$nq`;4^zQuB5O5`XXrOQjBRDU7t^&k9DgiZ}w;S;MI*v3Y-xbDyp7NfG-_b740=3 zFb>@{6i`hXV3UzgB^tQF^8c=M%)$P5*%KGMoQBtA6jF%>$@%}T^vr|)F2SHMbj^j9 z)8Lzo0xHoUfNGcsEGEF!8G08ZYM4^IBP!AK%?Fp$=$eeamr%1cO!^iRsOk)Zi#kaZ z78A%#hmh*_ft5XKH3${_L+V#b=~QqA>-myVPvucf1`7x_;1lcRhgd<KcmR`NC3WDr z`K#S)L_fWFr`s*uSsUQ`QK2lO@YGojOY|rO%vkd}k8_~(@7dyD47a+cNhanGo27su zqgvD>RK^uz^UkmBWWM+_nsO5n;b509N;LIy3iY&Adf9Xr%sU<BhN!-WQy;{vc6Kdy zPAz*zxCVm2nbr`>8i<sP&JeMoAdExNj6=zccQWex1?r{bh7s=i5fBI1MZwM(-<7C7 zlR}61=v7E!o6K^XPIDOshSeHX)L<9+9Lj7e)TZI|KOD_$U-6#GVy}2=7HKcu@#|<Q zGua5z?TVsWr%BGK=f|#q;?E)H&Rh*DDK|P$+Q?k*lwPK+BlPpy-Jhtx^SIIJyO=o0 zE;}-f2#r>}N%Eh)tof=zx;`dJbhh4|wi%AwR>U3%R<g7d@k0SVq-h9gK;mF?A6BAz zo#2ywS0=v!c9pBoKNXYVTfEjuc~sWSa7d*1Qd<49pT&P)&*=kGz1!xouCumH6VM0K z`WcA(<pko}L`yYG=X1%oh?6Ta_1fUL8nX7Y)vX3?aEg^Y9m|(SEKBl4se?ok@iDbM z`X}_$bIYVz93Z|KoXMN!S%|bkJ&AUriUJhof>iB46Yjx>E<^TUcmbe?i*p;>6~Sr5 zc|4S&a4S)&v#8v$0-%gFSn9(yS;Za5;EP)_pH-t9`xT{T2bczJC91^^7ki~$k&41R zArF-brQ0=FsDYv+Ux7a=Yc8XYEH^-iD!Ga=vi@S#&jRDK8x0`Zes($PC%pM0#*Ufm zE+!<q$f-FknBjtuGPi|k7u&#MyQxmu{QZGzGX8;EfI-dA+(J+a)f@eDsfIP{Bqyc+ z2MfTe-txYxRF^%X5+RwZL4J{5@NHJ>40Wj@T&{|5$*|{UBXlmvrG+|^-JJs-MmBqD zc0Ia+ZZ=$#qWpAa!VK+iu>$*cLNQ|#+@Y@n<5Kd>hz;QtkS(r-7lkc}i|=+u_gRtY zTr7w%KM0a2`kO<1_RKeZIjF-ewz>=pA5gIF8(DH}L$-YeF&DE$Ptg(R_R4vJZKn^N zEv)1s;R@c~zUsID7gHGC3HR>lO10>V)p(Zw7v-(@#R6g%F`?rg(v$cuWBj-sLF~P* zMO7_*Avb0qW%v2G4S^5~pkb}R&-?&^b0KgB`rg*Z;R9Jw6{RECJ(<_dB{BcsB?Li^ z{UuSTOYq8${lV<J`dByOT*Qud(eSo^0+z-&ed3nSPsk%xThuH`#6CrHs4EW|JyW`x zjPtr1C-=<K#mBfQu0ae&2)|{EiMST2bH`e#RGO8?%$UpQKd#E+_|=L8ny!;TBX9g% z{+wP*j?}3qb?L+dd$+yeBCnRM0{MhZ^Q*(osPm$v@xWsA5zl5R1z7k0=`C<sx8dOE z5Bh*BA;PBVfP;+s=y*p^pj>+33=!n>bJXU&Ii@hwO#bC_6o}kI*6RqdlW?&k!MT<F zeug}O+^%J`u^)aClloyMJF&=3lLiG`X45fQ7&_zv%*5rNqAA!WOXF?To@}Tysafg< z;f?BEa81VHS}tACemq!Qa*x!QAOl)l@|<v!V5VApnt(BO9ZIuHx!VcEKz6I9+s7Os zU6V;z-GuQ+e0z;;N2dC!x!!R8Y3P8^u|7&1A_Rjau#gbk`&U><!(DmE9-~H(X&X-t zp6;|5@Q=~hsz=$UfE$q*<wu5*vrTha2-F-Lm}53_J*V0+hwV3YKs?Vl4D}zmeELyW z;WTqfoGv$Sui<0J87I8pRI>Tj<;DSVfgLf82f+8LH6a9LIJ0dy#57_~91o@b^^4W} zlx<~4C57V1%^Z&cc?bm|Pc?<Qjm%AQbCN;x>q4osf%QX;vzT5@&iKh@zm~(DC7w<) zLi!p@5L@P(^GrvYgp;nZRKqsU1S=4f+oc5BnAq;1J{k-Ugp^QCp5&p%Of0)G48Jwc ztQE0EBbAfGSmCCsnO8TWTs8!<t0szF{GCGdN3LfDu1)P)+TedZj<(5C!%U9z*eNkF z@RU1GLO@}nblHT`f2Bg995>Ggio-pmK_x3VRQB(X97>J6NW_}OCwsT+sKy7qEhHha z&<2^B6Z=506OPaEfd4>nZ@N-K&4J>MnKU%`F-giPj6Eh45viU&zQKEZ#m-dJ25bgC zB`ef-KBw`AXomK|nQ*;0^W@1?Vv$aC@_3Xjk8y+m;%%1|{%s)~!tbN@&UBo8+u{#E z1EfFZZ2(&}^AYzB+f!-EnZ>&PdGS8|nTOVVSHtXkxL9J;O0eF>bMCDEM{c7|NGM8M zG+-;q!evdRM_`rIBs&Kd9lv%c$ZtWv?A7;laBhjLC*8-J!jGt>ZziH$T5y>{iLBox z3|W^SNN-W~Vfu9=XtT8B)M>hDL6x%Na>;H1ZjyAQreS<@WKttus*1Pb8x5ejD4nL_ zjhdgeJaDhDoWE*V(0Q1xdi)L}*3CR0MmiIA<U_J`EFp&80343Cw?M*+nAhj*#=mS6 zmu^RtVwUBc>uC6=WHZ6eY<$-ib`<}VH^Iu_+@XZMhr?>#6xi6!vMhYv@Y0}hJbCLb zIx3N7FUrP_WjjMDNZ#%XC|`a3dzqug*th5z1OPx9?f)8W{-0F#Ujww-4kmWr^dH{{ zoxe}13zZG)HD);PrEh%)SX(eAnzcUS3c)$T0Z7J)>9Rfmeq586jG82o$h=t>?~kYg z3Hdrgjx$ILmG>BtUC$e5oOb;NxjI5Od0Sg?7gf>15lZ}L4wUp1jhZ~2a3H-~C4W2V zE0;=ry9x3>#Q1bEdomUH_=eJPx7O0@D-vIAeTor}=g3l4_Q<ByoK14~d=-c&B1Otz z(6OaMCbOk8Ni4xB5jT5Rex&`#vKKBDczDUJ7?jQa7fv`ubL4a^1CF=Dtn2ghaVjHK zq#5mK_**T=VHv!*rnc0~>HB>~K0qW_GD%8m`+eSAaTWL^^M-`OI(RoHv8*majpKlv z+mPz<rpZe{76Q-%J))5*gGWz~G?i|ra}?lpC3c`(9+R>wq?*Efk&Ai)NRwIkI9>*= z&f^tZsI*(6pXz{lWOS5_&4S@R^08R+yk6i92@&Bl9TE^Pwa9wF<%u=WRQjzti%=e- zDjOf$PBQ+B#OFrkLi-RsF?FYrUCuoCvW5F$DC+b$@~B)ZaQ=g&MND7c7&2)+oxbUw z1MH9=nlYUnovkkkvuAf(h;`(IbFv926;<j1RS9usP7g8uOeYPAYqLo7#sd=6-27F* z4P=6Zs8$&?=+Ft1W~54V@Tf1Lp>F1cV4a9fdu3Ql-24)ksx#XR^bgz1wluk@hnKbv zEXNg(4UZFp`rDxW4EABD337x6c!oQqusdxG(NI+#mVd;U74;(Ryu!I0kBU?INaIKF z8OXXV$LrH?=|rPnPy12!T4ZTOyFPbWSM9i-m@MPY+LrlGBpFhSc;TEie+oiNhrsC1 zDS;GHArXL-!OM?j6QNMWr6uF0!{Mu=6g?0WUBRbhLNc})>+;ZLWdyy3*jad0*BB|~ z+!rDK>ioQvIN7gN<R50h7p%oQ&$2!9^dM@a^i@z1P?Ae*WXV`^0m>iG<UuT(J6xSh zdY`8NYUj>I*cYL;)RUOOjRTxvPIS3EofT=!PnJ}*TCHFGI77a3rq>Q;;uNW#Neg#G zP!MUbrb6HXafO6sW7<%J(<<6a?J{C|)+ZhVi|e!5rGuk&aurg!$5%jZ@=;}Bw*X`( zhIH35e8GWMUhCG{PHi4+cbqg~@JyCxPV|#F-EGu9fPupC)#L>PpGtU(pjCl+nfx&k zT%7Zq4I+r7s!h;l-x}=;MV7RlLx%uX8=2n^ZD8Mr{s(`x*64QKB`oR7pus}4leQ|_ z8UrRf-TxSs2x)3xNw!dD#2yneUZ-LZEv<<Elc9@1BHz?mpU<pw)gnFd)29(OAfF}w z6-guN)FZ_t-3BMuU?Ee1HqGXO)?v7)c>UG+a8bZ}?FzLIi4!&W?ND;T9mS&BkRYk% zo{r^Cy5sWtbaT8v`V^$bc2MbI#_~>U?%h^HWDQ1IHI@M6RDHgt%iU5yQ|cZ6;1!dE z%iebW10misj2jV#R-8<Vv8pnkIoaj|W~r}kS$}Mu@Wh(Orfn1!SlcnTN3yW?#2P+4 zv}L$X6L<xuhJ?fr*3;O4+sf#%-M#Nld7bjzUj1z`8GsFuEh2;J2iDpma1qjvM&JTH zG5ZqBgBu)BCQ*F~&1cu#ee~Y5T6h8e)Y^{F%XJV(DaSQFDgTFKsPIbF=rIrPa`Z46 zT3N3q0Smlf8e_g~DkrM<%f$zNf(X^e!pcGKe*nJP8wUDd{$h2$>rh1hr3UrQon&MC z*L8Np(tR`ZAq6;pgas!p=eV5^ukwv3kY32l4WQ;SO4pW9md0sZcU9{RVaFk{FQi{@ zct&%Gov5Qb<uvfhQ1hI^G3W80bTLo2edyHWB_QPao#91ltpOizw#DRNb9Ih&Z>tXV zZ<Fc9aQOekz8Kb_h)m6{<~B7h#3RwhB*4oHdLzos$txnlb62ntFlYLu$L=S+5?Qi_ zV1>Ospt>)kxIq3eshu45D#6-+$v46So>_CG`iX)2R>ezQan$XzISl*WCR?u0u)q?O za)z)<{=->jy)F)Mc#Ccs9SsImUaAqZL9)1^)TL9Z#)jK=-~3~uPK4oWlA_TP8?U~Y zBMJD(TuJY|{z5zE_rFJ_oj6pY-`~az{6^6Ix1ye<z3E?bE+|S`e~TS-yeY#S<DqRI zOmF-&=#Hu4_pzk<<&ca|i<B-Ec-j|#T=?=7Z^34(&tYPfDQ!D0EOvDh3!hxb4B}*+ z-)jQVK-03y_x#532dPg}RD*(8&qd}^80n@L*r-rP!Gu7UJ9s3jTzV;<Fsz$P`I;y{ z3?dr_IUc-=?NU!B4M1DWk}h}x`xwHL$QJ>bPP;(J*iRLzG&sQHbEMX%nkGdtKt4P# zB-bEFG#zvDDdn?QBHaVpK4roPXM4~1xfGfB^FlGpS_F!Uyfmq78gGq3*%e4bHWI4z zby~gPyfi2y`YqR9>zv=J)V{)gNQch@Ad79Ek2NbQF^V_1gra%I?3)0Q9gu)F5uf@7 zE~W_5*)(ll2j}X+=En-TWvi@)Ejl!Ytb?NH$oMCpEbys-a`8~d3=z5+8pQ_1)v@h# zvsI6<II+_J_=Ou6sdc(lE*6n6f@Z7N^Qp!I>L<M{8?#&Bl!$ZmAiL;hwF^tca*%4f zOvf)L{H@Hf(+%|HdT#-ssFFe4#>lsc%EEnrkW6qi^g)b;;a8qqF~!4(n#Di*YfW9B zEG$fO+k9-j=O?ZwN%_NDvoF+6rQm-|fJy^}u}?68D~Fq@=pt-;Vbv@^5LtYhG=tu9 zd<o+FY@T+Y(FU#1S(YrOelthJqHTR%6$ZZNVv}dltY2&hoy%*}ln>B*Of3&SsY=Fo zwe$R8{cw9iv@}U*?6ji#ebEcLwi<N;qrZdPTDpFJ%*b(L>TLP>kLIEubV20n_eH+@ zzSRG%1!ni199dibJ%W+3J!0RT#lPST-;M@3cbR(`g=Lgw|1-&?n5OuR3R5|D`CM!3 zS%*J);D-qGcVAmSy_su{YKf=40N6;EUZue29=vpNLIbcpesDevV(F4~MRo|{G1pkS zC@e~q-e#c<3Bs^kyFA&z*-eGPXC)h*^QtEOz(X$&*>=oIe0hz4K1%elQJcptv<)g8 z2d|{|fm?%&Bku;n3tmH6b`4ha(DamTb}4Z9s>6&Wqh?z(l2m&oc}^$qB{PkCgmn<J z12sQG{z}7EY}9Sd`%MncI}v45?Uui1oRG~Lb3;v7z{Sbyq)pkQnQ~Nnwg9m}1W|vx zh}HEEe3ra^k>j6l>;3$;-hXq4|02)*Ydp>9S~FnYp91cl13ZvJHl47vgazPuf?-Iu zh7m32ruKDDN<(5`Z&&rkh(Jm#ZfR)uKGnXFJISHYnz{m1EKD|Cvl-OWSHM45o~PN| zCZK*~4`3=L$vdH?lK#Rn&fdovW;MMrALagu8TH0i++!@elvCP69B!1C+=d8Q%FyR- zY#qmXwwJBnLOg%MhQAqN8zx?ng}1U~vX*4>*j^WiAHPJBW#sVZJ%&KprgBSZ{oVQE z_#cdcKIXqsL%zSd?AvesXVSI*$)=rk?X3RtAN>9J3k5FeZ$hPwI|?g{LO47;Isr3K z1Oh+MemKEcQJUC5ec@X9;jSZ#qkQNFJ<`n6p3teM%dG6H@p3gLFo$CQJXz$Fs>3>v z%ZaBts2$%U;sD_)Q?Dv$<+xBdtyMK#&(^d<Wzj{r`Ck~h%f#R=y8J>7iw*I%s-!B_ zI<|<t5A-lTLQOq>jlDJNK@q&^gKLT7wLNjSOq%C)BvC&kO?~+Chn}u2qW)Z;<8qTW z)Ao}){SaFxdv`(-G<8dOv7CCh;xwKD=2j&@K{lHZbCBR0PVJFX1@kuwT_ATQP?lqr z)HRe3+Ml?E^;JirvoRH_H%rHKo=)oA(&$}4&q^rJRQm{(%XUouRx}3={MlvJV{i3q z(qbq}D&G;miDYh{2PHU8(LQ4vq0p|KM8A-^a@kNShWC2Lv@CXJ0y&)GuwOHe2KK9M zXtYQ?c#5-TDl3t%gCX~5N0YI9A9fwSqhn4~0Y_{H<-ih0<DR?Z>HaZe2b5JhUi<aA z-_g-@<}2<N<KLqYBSja{-`xcHejoj3PQ?G+O+#ICJ42(tn^`DdEA@9XKah)^ObAjb zX$8rF40V75@RLgF*6(N2ikuAE65hF-;##2tox0UMrmi+RoZ;8hOreVC3g*qAno0vT ze2=Se4Ega66bDd{<y`ne)sT?1%qSA}kpnAAZ!*mxPZ1pppX1<?de3csXVy6I>E=4w zv2nm1*ka00aYciVfo)qLz{@GDr{KPoEhz87%QrrE_X<IDqYAn2r{@LFW7FerPa=rx zSOg}im!qYlSBOD=5&-Hug0MVku>mgi@t*kg{!ZPh5s0koPU`qWW4yvCrY0rpcBF5d zY3oIgCl30=kg_S)e1`W)dq|>rt=Ff55HWHG*ioG6+>~`sQBaesng}pPia8t?qwkez ze`#kI->A<`5xS8fj3>Pc)tJI_LF%Qqv|b@y%eT78d~sTzYZ>q?Jn5VrT#jOuY>}<7 ztBxV}v**xyn}1p5s{oqaN7ZeBDCtvgY-P}iPoE`v!_-yl>BTt?_b1ZMZw{aY&HPO$ z|KzofUu9$4?He@}TmQb9w1(>yf5|PozOUwgrtkQ-&HP_N+e*crZ@~q)jyq&~ar_w% zfZ3{f`33$pV239-K>}d@Q>x{8=JMMotau)TGy?(=DVKMn!<UJAc~nhbbMsOs+;TOH zAKF-GI}H#!^=5q^^i*U4){%EObS-dR^HOF+qi}IeMJPSTGzG)MlmXdcfBXRST~3{o zp(0Sq0pjarcKPWN2V9%ODfvVk^KXll5xQCqf<`Qqd1DfL)mTB2g~c!=nt*?FsR#mI z(-Z^26~@8Xnzq=1x7gsLOVymf9>+IH7<6b=Z_hD!EQR5BK?d>RPr))3<dXwQ2n}PW zB=!s6OjG!1tV*P~m!Gs=#6+C*pV+)j*&9@z=__MeqL<xsY%JB4sup6GZ_v=gXFxg< zLDS8RLX@88Ja)!PZIRk*DwR31O_GaZA+U!iSVe5pA0o}R&BvojhW4!f{P-fkxf*%u zJY651fqQ=+k|&3xFQ8|S|7yigHu5t`zvnSl3nYN%OdT6)q@EC-B<icj(|1jwT%%MB z7{w6+A$e1rEYek#9kZUf*i6>+-YEPtyB@$OUH|ANiPxGg<iHY0ivNLDyy3xRp{}_9 zyf8H5nCbik|HXZb+#+xd+&9Sb^Nmz3^3?iTBNtmhMD0`AW6q=cih1H6N#~WjG|BUK z(h2bm7yK_tr>?1y{@<&9pv3i^bi!>sP{3U70#qbBA;jcC!~g-#7Vxv9`SFM=@CD$5 z#)V#ZZxNJQkJTX>ln3*$adTr|?O{Pxiq*V@jgf2{7B&QS#ZicEKl*m_8{VWt0!6=t z!8aEZjp1lh_-;9`t26|ASM6hWt-w&7Mw;fDU?XEWhEYhM@6%6^Ij_@83Wn=ljOG`i ztxcZ~WTGty)w9SZK$!hpH>te8HA<$t+7SF%zEGwrK}J*8ke8NZw9?`ZFYNO>l}ny1 zmG;{ecZ^Ket=_J4I~M`5<Xa~>5}#OhSf-Lw96A0>?=UQC6b)c%BK;Tu^&f+qaRi^b z=v%EO7ESmpdJQxMalu&{mhAq52sxBez7$KF;ncu41}z{<UN1VCrIm#v)w?#<>|VmL zsV0P?vo+CbUaq7iqkeC$l)2tRNZJDTsLr2_r4JP?RpnB4b`W3YzjC9l(M$^RVHB)) zdQsPw?_1R%5(Vg#o#}(dE8sG`zI#vScJGpi3r*)B(M$*{3q=qjy+p7v7(+C7a^Zo( z$zxozOLeZ{&ypbRI|N9An5op?gnc&|djO$CI^ry;ymM7if4i$Gdgn-hoh6)X>?hOM zsG%y`8vL<6`$rOXV=-W^z;Q!Kl)k053N-6Y3nn-an!ouQAyYc@ywC&7aDrZt3``#r zepC4-*|AXKhjVi3GnsnKa#qaZWpmYa;!)9}SEZ{dIa!%*I}|t5%~r*F<j{)7LWc%j zy?1}a*oDUO8x6V_t%H-Faa^XD*v){QmZWhaVm>6om;nv;aIe_2tRmI7A~$5-j2Zhe zzjjOFYfmeSCdo+bj-9QKFat61Y4|Ys;H97OUJl8Och2%cQi>{aW6l1UqZ_4p^qd0u zQx_s|78`QC`@vjxJN_(MNy$)5;$|v7JL_s@>Jj4HrcBRnMTE^)?jWRGyX!1a3r^*D z0zcKwsw3FsETjoIFUuplXWnQ;C38C+=?^G*%WpRtq~)pB70d<qE9M@@Tc@vo<a-Ww zXOX7g`JTu3@4vWKT`LDuV>?|ZQwP_-_eU|57t;SPbEth-mqJbMHg%5hF|wE#F?)<r z7(=7W+p6K#VUIiERI(KeM})}el<v`lz?!C->^NepZFggIK>Ro;)LELvg_+%r^I?b_ z(>K9|<qHiPoAHMLCyeo#ZK~~@(<dB|=Re--)h%5-zCXVy!9@RSVAr)X{{Qs!E-=ll z*PCzNJ|O{L;5e3-1us_{!VhKoob14;+k!6EgBgDEQOvZpng}Yw{(e0Zw|h<?m9vXu z2d%>aGG}7Iocwt-u(GaXfWj1QsAQ36p&VBh`e+3Ifr%V_cg#9@=aRYS>JUdX!TIU~ zr7-|qdN`xX|Fv7~HD>mDYQYqpv=78$sW_gmxMENca>rKeh|PkO=H;ms8$?K$Eu8$k zMMaGe<`sxT`l_f-ucjPL^zkIm8@sdflu{*lX_FYwnt3q=68)ghm7ZIEoC&h(9$KY! zIW;ZY`>RPi<VtHYpUE`*9*a|cyAZUg;JN-hX@)xoY7q&{aDXb*%N6C~gK<myx&wER zW}rOXEa(n>ZOd-5`lQWu{+BABoCiaCWm2(;qc3+AwG;}bwWF6~lN$C1nDmh_N^&vE z_0zz~P=5VEs7L#|gO``Bn=6F0cZODl1>f=9?e4wHgRbl2&<O;4>$6R0R8<uKTbPy% zwp?5ThXx~S=0qH4)!A{$>b130d6<HGF)YMk>#DZEj~_qkRewO(Ov}zGth8EQR})Ix zf~HH|SOzdR^JZV|naN)*uy$=kYkl=n?WUO{3GiB;9D*IIyTDL_ogM{HRC5yxA{qx8 z6-t#wSR3R<T&0;G1OX=?cTQl-)J2xN0iNz|4e_Ce&?EnG-5Zn=u}z;t^l7q<J#>)e z1$5Bffi~<Ir7awvgwW|tZ8<y&fdcT+-}c698q4md=v5YqP^g{pAr^(g#+@$0w<LqW zFWrPaR|jl1PfyC)7Y@LQYp<;5Sm%8u1&}&$M^qOPvdd$Hw1#RA#Sp4BaW>b;)m$f3 zC&sfqMr#My7<7cq05=s^S`64{VX!2=2Qh-?TNgV7Ham1nb5<BX7&uYEiS7hV?W|gX zvdR^g(^-q)eP~8_hY)uW5Nz-B3qGcvRDbP;_O1Bp7wy9VBgGf=&x)xvSTH(9eBevK zE|$u+*kw|}s0O`IuHL?Xby+oGtPB~1ld#TZm5fDV%DGqU;jUhef-9Z2#I_D<9ArxN zS?w7JE8)%j!^zK5a|3t|^awtzI>WPmvnL~7BAE)~^APwOK%|c{iE?<@jcNi$bK}^> zeL_0&`PMT@zi9@bUOkRz9C)N#LNf>_KM#+!+@>{TIrq-b%2w-JR|!QoIQ^Nr>L;eS zB`0I|R>GhpkF$;iq7ups#`p--%AZdHT_gU*pIP$Q*nwy4B26@x@tAgF{v`I@($1LR zM@d(FaB(EU#C(2iF2}CfSQAa_&-m8p*XkB40z!sp9U!0&A9*<4!N+vP@^S3ImFa9j zCtezQb$to`6+hXTWxMD*1jOY=O~&VtcxQ5SpG_=>J+B6m>ARe5Xm;bIG8&cu2s1OZ z(@7+&9QM|MpjStX3H`SSCxH1OQFOCZTS<?P%?w$!oCeBdR6&%gY$R_v-W!*aIFi_o z-Q_M^ob1OFsmjwl38;i16vIZ<N5d>!<R&3L#X3syb!C%a_A-iS75X+B+%H+Vpkg=c zKpoW97H`1X>{T_4*npin+7dN<(P&!LL2=dSH)m4<&I85-qhCNmuqgX^)L1lh1-+5m zc1m2OE@j)bl%FRBt`tDeDf%R7lc=iAjTZf@&zv;vLe<xf4QBBUzjSH0q%}8oe#{po z8SrjlRJEEx<2NH7;;Kyv`JpO!DJUp-)zx%0S*}E5)p2~yCZW}hF10CiyZLiGD8TWq zds)KkEphXDO>c(YoIU%hku>UFR!X`Arw_dh%fIzqd!u%q-BAWzF&eDY>SvRvnKuKR zq?x1k&8?&u)#xT%4eI$Xs16(SxkER)6Qf(k=qE2Y#fyMF;kT~pZNj-^T0-*OEs|2? z8HJlyv~D;mSc|3?&!q9`Ej~q+5BZ0bNv2$fo2)S!cCd1;8%#85;t(1*t8g=`%VE;b zKKv>UHcUSXw*Gv)lcuR_wW0&ny><Dxo3h?~|Ddtn>^w;LEMz}F55<DYg2l^c_Y)Hm zm0p2C$ye>5+{3CGW2KYYK#9*54lEiKI8g~50cNMfow2T=SK~Wr&uC~=-imuu<_6;@ zJ6n_5H(D&W*X`hoXJUhCvk*NNjYZ)ulR*a_TS>424?Bd2`pI1<#AtpAZ=zK0<!3+) zD)}s7GS^c)SAo$Y(WPGu-%~=p#x&e``8fP4%cLX9`@0O}u^<N=r=~Dg+*5dvQHyo1 z7tMqCkKN833oP)QG0v*v%m!14u~09ziU|vNR%P?Z#!q7FG|07n6gXeZ!R^34Sx%A^ zwQ*=r>KA+7%ka{hSpwU>QVMi(J(UT*%_h0(ZFXz}ojcKbG<4wSLW<Gv39cZA=L2nw zKkQ*~k|D@ZK&4i@MTKhx?C>0UL2|GpU0GULD~t+^5$S)ZCj)B0(DOKV&62sc9U{_x z65M8mm!8@g2vhUh(R290K6?~OdP0diG+ez}y58M(qe7#ehhQv$$jbyvS6vU>%BgA0 zq^f4T+pnStAV=}$hb%CMuvK^04f24xpHJ`oxyVdk)Y@9hk$4)nK8Va0l$;Px*in%D zb@a=z+ymq!=+|y~PI=1$ZsJya5HD{V61B}k398R!$AxP~JPH0jb3~Ma55Z68V$}ix z;p)EX%tFc_8bz0}b>GRW=d&6$cd)s(n5BKcd5@>p-Lzq0ktw2Qt49S4-fV@T#*ij> z>_<V3V<8AH3Kk+*xIa{netmzSGTV?(GvpWLhQkVaf5r^`c4-X9ap}y_h`YJu7EiCd z$!Jgnb&yLJiMq0};tWq06yg{f7{r%@pUpuR$3lu`zW7Xkvt0B^zl}WIFIdiDdQi&t zlu2F|a|s{9AnqZOl(0o7I_({%8_u`CbKOTe(n+urcY)IY*IWU;`@Oj?Q^KEK2uwJ@ z3#SAi%X?*zwEAm=Pka7W0wS~qXhv;=pPmjzrtn0m(1D!P+EidCc=1sP7|AA9?HO1n z;w>l+-kforBT~&rH;v7!9FdK{;dnx9it1?Q{yxm}gLtT|-L)_r&DsLfSJ}BwE7{pQ zQWpi5`u7*f2qd=Nff$i;ox}O7mzwB?V4?D?hx<&h3%s}*rzapr?t$B5S)NMFkg4wq zU*Y9$6yc2nd0x&XNUs+wa-?|yq)j6g*JDpcT1FY)pvnTk-nS038{N{cYkoA8Qq_p) zb@#z4*zPI4O_I9eKum@C?G~|~<xqd`wzZ0}3?X!-u|+QT6k&GAvwfHrIrB7`eXD%> z(BW%jIOOa#U0JBfhif4Yc_bKmEG)Xp$GD>#w4(4d>SCc8#FcqL5AVD;M<uI9W3Et9 z`@jLxm2GUfUv&;}Bzgr*?It$uIN;tOrZo-367uxGRRtXN1jlLvfvTKnbM(_QbD>5v zrF)UziZ60~CwdZ!WlE9xw>0be#k4x3anM}4<dcXpiFXC;h0F{{0Cnzj@8we7Gs~OP z5~TFOe3DO0tUorT@4yD2*pzU3QgF|^?!mdIJ2B|!In6Xs9Ts)B$r$>um4cBaM1Sr@ zwnwMT8+)xbU&~x>q;~9aod_B?O>Rus)_@T;b2(LU;JiOy^CQxa%7&KP5QSyqS?`vV z436Ir9Nw0ml_4d4^7im_NS6oVMbH&?W`I{Ae;U7+kl>|)s&XVdacD$diG)>}d2DPh zu6Wl}h5B=6<;#A=O6?RtssrdipIrJxEYw_>0I<#}`^Ju>dWZB)gyReiY$|8D`(mLz z$-0RnLYlp?KDtR5m&5;lrKj-i$i-8!Z8;9(qE)$ixk=>D?#y*e9=oi#d~(?-m%Iwa zj){MJ0(5VB4`{%w#dv9YC!}M`A878-n6<zOFAyWl(tXzd<;@{7l^}4euw5$J?TlgS ziQZhMR@#^)mL-WIwywqNRohMya^fk$bqmYpn(3vwi=Vj-2(S|j<syV)BeA;60sr|W z)VbH+y9zdin`i@l8u&V^`2~a4Y`%Ajl2&DKkuFl$rAN#AmL@lkOW-eHoHF_ghdpTb zqkYg)Y`;0t5lY#?dgqyoIIhwo-v=_f6A<&Ov<YBy!0_ldv5ig%3_my=gov~kx9+;J zgdxL<$MR5f1112t*z6nB(f0P%(^L3GwUXJVH~Icc!8?AdT4sfgbVae9bXmd{AwzCL z^C-^6nM6!Le(>HiXgd+!P$utDuaVC9=*2Rj?ch`Q7tbF1=~si)>ogo1^=VVD9{a$` z)?H(@!d>}T%jcKb_Gt9Xa{N<Y5cvd@ZaEdO!Ph@1zO@UU+q{9l1*l;DFIC@vRkU>N z>~#P2;2KbtHUBPHcs*6a-Q$tDz*N-ys84G2xAe6#q+AmRAN*#@=a1D8wL1HF-59B* z;&56-5_-cWf9Y(yy5!DWWw`A_3MEceAXFl!=DLbl?@ucdfUIL8hBdBNsB(-+Hlh&3 zT`+QjZ%?;PDp5&ffP!o?8sdt+uq*vJQ}{?v<lLXYhKN!3J4-8kmV`wDz{2Y&eo8@R zV39i&qii=|?!#J$JbRtq(&M&~_Ic+=2Th5J{1yT5?^|DT>b-dqs8^62!Pxw2!PGfz zy5?816r>~SeDL#<X`2djyFWkq`C(=>Py@UY$cU03UhLu|Jm_0`48zAzG*GGSHCU<t zG_UX7de+_MN%NRZD+Uphr2#ZDd0^6$U$@=$7}YwgycA#nJ#u+dh!_Jddy*B!n*ipf z`rsReRFFHtM2^*%0wN)VBhbZ)+Bu3Ap?tcUn8P^cAFBonU>vxbW1DM27_FMm(yLXZ z9qtw#;S)-#Jz^qknm+!R^iAP<k#U~CluL^RC#6Fal$Uh<rDU6|1LKSN$I22WQSL>D zE?9+peTjh#dZ7|1_7W2E7f27AtG!^SC}5PXGbD}rDs{b8wpJ|0W1Dy-N$L{r(Ji3k z2MQ!A<D@<LtmiPP8xK)b3bA04tRanG;~yiZ8p?|Y-irsg;X}Gf^*jD*b5-!Y!QfC6 z$sWz6S(uU}v$%jv&L8j5+Pb$+-oWNZtPoTii(Qf|6>+F|(IpYKt3Esm7qrp%x-{4D zw862PKDLOfWWVF2BY}9k`?*t1Wk*_jkhh1*%t87D#InH(WSxUz7|P{D><9|-n!Eh< zD8Tq*=j*3zi%ZSw4Rx6uqr<6_U14{I+0-zv9^8%9mSDa^1GHnBV}oMY<#sDfsl)Iu zwrRSF>>N=7bwM?0r?p6<{gh9kW3&~MNws;qGj%p}jOYvUQ8iV;%+)wy(5nqyX*UQ~ z`#AYpocIk<I(8M7`R(}CaivA(wS{T>oaa^hrlMaOT)2`Ctx{3i(VN=fQUY8eizIgY zX+LW(-%3=~X`s(;Ad3^8*c4zR4D~c4p-V`^<{ALGANn4|4u7=*iq;RfE`k!v-2z9j zVf9JnJk9>9)9gt{oisp`%<95*{q)<HJ}AtBb-A?svs(wrF7td<i?-ep_0<m(bG9C# z591++-GaOH4ERMEAYAe~bxrf{C27wh{;a<pM&xhk%75uE=-RtleQT`!3uzarFjn== z54-V3p>ilzrC7>QUik`ok#|8J2Isqjjtm@Vn}2`CF&Ir4b+KuInbN0wIF9LXPlaea zB7xx{@S08w8A_$Ir$~2ER36XW+aRU&36!C{q;Q<|qq?VgDfCpGXD_pc=nbZe!v;a@ zF&`?4`!|+gSYC`$K|cakO*j92Sv=ggM3uC!Q3i%_%pE^axp)ysSXyQ^c&WjWlz9ke zYL^VdUW})nqGCslbM}hzl}WS`gRFRqaUAB<Phg#K+@&Eyf@|7DHL`d=o)sPsG<t-% z*_|=>;+QKNt|mDV8f~sKd(8QYpea+*0)t9$=M*+7skZ`#3?-;Z7B5vs+fhC@b6)%d zd{6Pn-Hx28)hd(bffCQ7Z}B_<lIfxJ!6_~gA8~_`*GM<i;Jsy)*~;-DX$*ntDFL*W zo?Dk@c&V8RK{zv-6?msmupMYtQ_t0bKx!syIX9rY@E)V4eisL);aPeN0nfx;i!0$R z_qUJ}@pl~J<RKbp)~e&U4!$=w79d{|n>NV!j)D-gdkHoFUqVmT;t#>Q^J;187c0U} z&oXsS`6NHy%<?~KHfb}MAkJRU*tsP}`9|kjYq3YD$a<{F=4!a|C3dId6#oQdavA9L zE)1PuKQQvNF`X0Qcr<G!W$)=fiW#AQ_sstN+-&Y$i8Xz*BU~c?|2sF<cK;G~*iezQ z-edkI(N?Y*$%Xg^qxEPtY~)eZ2?8Uf14&Bz=1$KJ8}Y{ii4NU-zQS}-jwWHUjFfT{ z*4#gfz-*0kw#wEK8LiRP5>{Lk(C=Gq*ba|u`H9BHYxgsgELG66#Y*Qi7dR$?7HpnY z)PkrfMeE>yvqG+IGy#7(iFmJ}5f-g?QQQz6KVPy8?w~sqsu&l%`g8FmWyYJS+bYGP zv%@ikKn@3`BIkb7cOWyVuFOu(#!Ok|K1rbdxpYl>2~_3bMOe0ACfLXPqcUc;;YaI} zUdFnR=HY5|SiY{|;ab&Qd0$s^O<A6c2WjGohw}BE7{$F`BfgM>y7+i@$&Po^`4FM{ z+0ELt&V%+SM*ln}JVFU-#9b=lDJdAN{!L;fPnY&&>umq!=6Bl1VPlwd45`h=hl5IO z$imyPD5GY;`%CUNo_Ji8XsMSF;OAt&o(zt5VXg0hhxonQPP8mDZvw;D6{7B;y9Xtl z0&p-<<2VDRr+@2$0Io1mc8Q(>svcplF)uvR92V08q(ZDA$ZsafMe#1Ir;a#2o<MZ5 zteMdf;fU==Y`fB6>18<)P@-h<Y)n*XqAuTQoy$g9`yaP$V)*kU8=O^jc!nb&42G_` zzAP;mjNJQ7>>=^ft<-d|w$_~Ecj&)36P$HQWqyE(;beBtO()${iq|wtiYIWU;P&~z zrh2@;)^u1!CMo$!LPccQ%_EAaQyu&9N7v$`=~*zjLiD9F<MtBww_;H+!?)?=!HBfA z_dfZ$FCkrOcJSR}>iddiu!B_J1%NhqLB~qxc^S9yxX0b&TC`+e4Y~e_PeTtwYhud5 zg0CMiL_Ikr(?K~a0#r16gnngh$#rk*?+>h<JL{uPmK{X}Xbq^Xit(XS#bBeyKSd^R zR8UFbVhWxuD+(YvHydg|?%t`88f-gsq({$cn=9odb7jOrm_^j7QqFG9odb3RveRyy zOAtoxM?b4JI&NVM+cfom7oo1QyS=Hi0e@p7>C(3oA0S`*vIsQ;9MD&)TQmTM98P^w zlF5cYYQ{bf$fe-UsX;9JRbo?lmrI$fQ$klpiDR^H6^LcW&lHB_iTO$^fkIi&MEs{@ zCU^zU+saldHb!%Tdsm1_6##s^`qzg3N2rMgbdye-nx2vm4J+qL62_7S10f4IS`3kS zlW$La%}Z_*o{}dbU4z;K9@f4q2$KnYASNKu9T}}T(J<nHdCj9wn92~C0%`#a55y|* zQ3#qlD;fU9ElCW=@f1fi7rly8ZIRx%PupogRXalxlSkiaU>ik7N}Qf8T!?eJNde!b zJec<K)!Iv6A1Ynw4;<n_2Zqr{y8DrmV2>Z?zh@o&d$*L^PoljC(ZhZ(x2>J6;kNGw zsJ9Ac#%ZVWNN%hnln!HYrl+rQuCt$i2tv+|qJxQBoe-#S4U|P*#j|6jhu%4Ne4wsC zd0p!fm<LM?wxyNK3GexXTCgJi;Od36pw-bhyi{VWv|GcpJUPbLuVO3ulicA78^toK zL&jh?ZNYCYWR0M34|(`x-1rGNHlG`qg3FemZm>dS!9#BrasA%z*Fl$-V%AWVTdiR; zt~D>?(8;=}O&LXP6O+>-9okHuT-JiYo~YovKi^B2^HkVBxJUYCW@*+E3gbk?q7U=9 z-Kw;j4IyR>{1%++EVUWsCjXMe5ldurNsJ(QjG-~iHmg`S08PvIy3CjCr?<%mzuBqd z^dtFrf9R)h=;9QqT92PnGz!e6V@e6#XR136`qwJSM>N#rSOT-AR`W{m8Wq+jZjE<B z&$~lLnowDi$5U#CWlHm_jZ%PmJ(rofE1g5b+4%-1c~NO5dyu8cRR!>2$YBtRUozz8 z<_E6iXZ+bvF6_%!i*F{Gx46QafvcuU^ya4Av~6*UNevB6aN=PI5Uls*`_%$*X}967 zz#!=7KT?Jeh6%uq@03B}yNmr_29tFEsuNk;{B5&zC~BI2t8#jN6Hw>#k&F-Zg26m; zE<X|r>ba`}zmSepE_in_B1@WQ*4DhpoVRctZKk`&A*n(54TX)w(aMrV8W2U(y_Cd| ziMmR3o`vxvRo;>#YKy=Riw9Sx_NOchqkF2cu0;#9b@@-}*Qrp>GKSGhMdIZ&&elKi zgHa!Gn)b;i@bj8vO);nQ&B^)n9|c%MgA)^@Se)oWkJ%<SkR9%yH1=`SN{uns>`S<X z<?laEG*l8EDXM6<N@qGP&4iwVb~t2wD}V@zZzS_FE`o8%DRN1z(VOup6?|M8^;|aQ z>Yn^qm!Po!<9F5yMSSOHWKzj6x*+2Nmf8bh4+*O5P||Y^2skw&MR5@h;f9K`hdY*D zncx!yNr6fKBOMfmC7@y>Y4Y$(-!^%2)njyC-D+Da;r}7*oq|M(wr#;(wq3Q$wr$(C zZQHhO?6Pg!wr!hT_rB=pc;}<fM}B8S=3KeP8aXh#xl&HLZ0~5$<35v6Y{lGXWY*pn zerCSNWi-{E9q&YfeWBTb<cW&#(Xq_gmvamezZN3_vH2)R()5r3R)$$qn}5)$NyuFS zta5F4<n~Nacl1nX+cnxG?=4-Uxf;mg`m%B=5DpQFM3G+viGh40J)X_!Y8pD8$DQ#l z)sy4q{PiFI+EUnu9vglK;NLH1{4d(7|I<1AKkq+TdF+1_DP1R&BKz|MNb(v4VP-d4 zrvPmE<@HD<Y7SSUE|6cIS^t>Cv;TD)c}`5a4Y&EbCL+LuEAiD9_w_#@62IuhkRx&G zeJD`?)mQ>wEYgwFiQe-2!UV|b<90#j#jZc7CFp(uDX;siGsKIz`H%gT7KFQ8S3np; zN0QN3pYIu+lZ(bfn)J!fL_}S{M?cEoV1iop3t_Q1aHp5{1C9)kBIT$&!|UA?+EH6; zDan$&AGUbw<_+#pHb*&*C9UeOTYP85T@(;*JaO*L8d{x&4@>IYkj0`hsNA;}MHKw2 zT1z8S+&T==HC!x$aUpOFXShVYsC_!>VoTtzRio9>?wPecQ@ysNPRCKLPRCQ->}P4G zGU4T*CqE@~?VpQNVJFt0Z;Nn<$}uEQqfYtflPpUlA+$Hh9*tO++-4cZj!N7XY1yFs zo@A=C!_eVXcNJfc6Cb6@q$Ep^vmN~X`4SoVj6#WW2B({{jHPq+6>?N4BYhSpNm&s; zF5SGF;?!1Q)^SBiUPt!&I!&V|=^TXjW_-Vx@`xeY$Liz_b_T85rRMU-u;}W~>wh4= zlq4Pas9zP*J;MKA9@hUJ^$m;+{&&f%ZP*;LKzqNaqHm;%?;Zm|H}ctZfY)a7C$4ov zg9gw#5eyQxC1g!~dXtaiOx#(|)3CtF7{!Mk(u>POKoY;?vny$F1$*g=8~2OS$-50O zdTnFKiE9WT>-?^N9a7`7l%0g}a-qHA>PrvD(%UyRRaE{Rh{dmQW}Y4>n&6EePI>v( zYUpk;i9sE(D*M^0s~L*5K%k9MqCloGoy~iV<3<EvfQ=yE#;^)mtA{gaUG+1p>WV?5 znbxiL_AQUNb+QPflV74pvsHbk^57PrUc>ww@=&_hW~ZK0Ud(cMxTWcQqe2MC=8v!+ ze3$cLTQ^hyZOcmwTx#YMP)BEv{>nw@)63M>7xl;I)BS|uUk82o_x*hW@Op+hj?VFg z9t#^4(bk67EtkK*_}`~!Izys}Ew5$G=!-U@nPmZo!XuAY&BzsBpqh=#;K&4$^`^JP zmCYLs*Nh;=Du6werh`yXm12VR3y42@WFd44<dP>X@k;?Ij9qv}_7XIFVg_^Z34#a= z!yv3tRZL03i?@r#wDyPSyimSBjGd`~fBULDQozaGmd(&Qf%$#%2+VhHx&r`VLIz3U zqou`CSTWWBJ5D>3f>hy2T+-9YilC7a;Xg38Mr`EM{}}+jJH<gu8qf3IkBPW)<b1?E zgyBXYu9v5cSXH<nq*<Cg6b58n8SA7{2ix?$>uwj@>24k4d4e)ee9A?aKk%L&B^VRy zl{ji}zsDFGEV&pyBig^-*=II^Hl?9G;V7ah%3;w8h6MAeNrrReH^)iXWOk7l!(fvm zsZFQ{A+8ETzR@IyxT(EYqK3#WEIgMUfYnR8H3O+jS?6V3kgJIFX1<{(s#vCsVsM`u zcsIo_ccQD2#8b-;DA+w8swL!i;#e+XUu;g}f?kTnmj-hFN}J`wst<MGx5oU;nns0q zhmQgWQjWuUrUaGF_!mErJ%X>Ll^WL7$|o^ha!$t_tz>o-v*Wz$A9D{+l84HPT1+?o z>^aqkWS@c4VIIKyljKcjZoyw6ucyclgKI6%gPv?GpPQNM<9Ob9GTy~RwE`qdwefb* z2qorzEVXEhHOXQ=KzFoqyfROFI4h|@LVk-Fx&5Qd3bUvy0GTomO1Y|AJO$kinQ{pc z&CQWWC;9hmaABgrnTZ=&pQNr^-R1!;H1;gCUtsH3B7{WLsa!6entL<c2$t@y`Q@u4 z<6t}taU(aPys4c;Ce_RjbfPxT;iB68@Q{kzhLg@FSMMu&MzJk@^qhBJ!{Ovwy4lJ{ z-{wSIVjUt9=&|<1w9$;j*;YtM2zf2QxIGo?srD*?^%v^)+LQ_?>G>wciZuGm?><;X zW?YBo@_dV2@J1vC-6K{SWa==7$QMz;-E3y_)Mjaz({+yh9o0GJtGt0W6cO4hlY2>f z(h-sEm^Qpf`5#?1iIMsT^_|U?O>aX^aq@**c0B)w!77ZhprtNBp5Z-4eQHXPGN$KS zH=8mNoG?K8UQI3Lk3k`PwQwuP-PK&<&gYG>Wnnbg@m6)i3XJUy+1r0;Wc#Qx=<$DL zRvQ1E^)N8E(RVa7|6hH7w$lHn?<XeL0uu|dp<4!=niaGGYQJJY4k21-k`s0y@`(QV zGFsEw#}i+8+l4#7<K~IDN8ys6!bTnjOwC}&H%8X(OJJYuGb0NxcVd}{wi92HGj}q$ zoUn?YG?j3euwulzPe@p%h&XI#Na<y!p{P^`VoqM9oelN1Fn5CHwUflrXskVi(>N(i zUkvuIp;I4RU;^~j?t8(5aMTmf6Na^*4jw}YAW%XL$5dM>0Dfee(hNsUjLV|N5&(0} zRte(@D&h$k-@|LD^zKuD`U{LU28ff>U8TY*QgNJHPI%G9ric0w;g#wJq+~ENc+Axi zQm`D*N$e^IX`6eXAt2}z8r*v18&mK%vR82>k!k{?t}#@0@3{Lw3|~B<@T;T85(_-{ z1GQPpYg$BWLgtbKbb63d{4N8KRpiZr>mq-&T9&j8RuL3vQ+DpE<cHPXQ*S@c)>P;q zGXazL_<-!Usv*;&{)Qj~Q9b54Yd^0+@(OBuF(eM%^<#}*Ow@;Nq;5iK9dKgmIa{Vh ztb%UDC1vC_ahdxY=ydtYSWJHi!3bLYSXCh|Lm<UB#sT$#5ySHf<0!cNwyS)4axb-u z{ZU8)Rl28eplyiaO17T2-yY2e=b6sS7-gMmcQ`pGYnK1C{_gVgtz(6^<)>-;E=N>U zfc|86PQ#G2C#{^mQ>J6d#{OThx+)|DY3#<c>nZ0f&^O@!&5_*R2zbW);-vWBUGu-; zBy$@xW4HgsNeMd^fpo~jHxI~djRQ{rK`>Tz6?mwd9`q6rWy3UjvI0`--XBM?njO+D z%aLh=u{T~PCuPZOdRyYKo|AA2WYIuDQ%Hhn?b7Y{H>rhIX*<>aP~daG_)08|*7gNS za{LYy>5In;MEwhA$oT}}xvK756FTCkXtLr#>}E;-+!Kh~c#$u>x}ze&Z#bsbWqX7n zo5HlJB9_wNVYQGD8!O95U%wGf0Mlsq3!LY(G^#eutI?MPvT0FawpzOX5<qR!U0kpC z!F5+f$A@%Vm^s_5dE6xkEYYph$9J@(mQ+MTb!`zCDPE^nN9SNA{yFU<bujNiyV`q| z2{y{pP>x~3+UG2{a(mze#cc6dux74wKI@L$L0P%fIH^aV#xmG8&9zsnUodl+@LNO$ zqyOVi)*!^I7NoXV-_!Yr8}XFETbTK(<hbT_@O=3Acp`CC*Dmu85ADNwWG^}!5OyBM z@9eg8rtlW30N;!)M&OWjw=E0U?tzB|bS1yeC)KE$9(%|j4V~e~@a;c<Z{ZCCJNK`^ zO!@cvuP^lf4JA`l0jD9Crce=^pj8kXr;?GT*^{6gpHU7AwL@lbgu0KEKyGZ!^U^FW ze=V4Nj6$rQ>@<jLtO5)IKmqxW`v<=2G$Q{7&52_J{5P`Jz}ft_3e)EQDEqyw6E|2A zZeQL|yq0mfT+FjaToO!Ouj^Zyk!;%%nzVD+b(az|6y%YK=mp~;#it^Dznl-S@wUax ztxRvUXf%niqD2bl%$!*Lz}AF2NY!;{8&=$x`f5^)Bo&gO=8w6{uIo)SpSjgdpcD>0 z|D3WO#t3M~cVrf8yQr(@a*b~9JY^P7=gg2*SMSi)E33!TP-b{6HEvYe5=kv@EqVKV z93C1ivD_Zns4ibrZeC|J&&bF!ygJ&q5aK?#q;#Fd$BBGuC>f=6S@XpC>?}7_wvkvb z9Zsk}mjfgjORr;4`^xz2B)0$A^}Y6_`qwq;s+RjnkW2f_IruHP_SA4G?9DpiLk0Ev zMo~kda>om<B!^l639RYlor%Wn?Y6~|D<<tbuCe#x{ucfx7#AFVBN=(_%_lVaYJ0KE z!wrUd4hMj_CalM8$;!W1b9b;q{kgvK;ThKV%T$|%IS?4tx+9jjE!~(mk@Ne)Q0EHb z=j*<j^u${-^TTFEu&=YgIyjYa-03xS^BH>Uqxmeepv;6cR!#OOQou=Z8mpUlwq&*) zjaK(7@sU0B!ULe?AMOmTk!qmWtt^^c4%PjB#L`E(I^w5uKd-Cn)|hx(|4**mH1ks@ zu=@D-v!Khn*Yj!A{AnCsAFY*D$?IGG+R6m{@FlHJR!<U{_Y#CZJ;R?2pb@d?Qx2?e zBcJ%+<chb)z0dtULN{hw%m?aDH@3ihhwK((ofqESJmB8kseaytdEBW~+_MSCgDv)| zqcZgo#rOO4w|fllw@t6NtnSZcuQzJ%x9qoj&i8qXeGn#N|H<z?{AX6bEM(<v=QXYH zuY}DU$h&<l-2`Qp&~HE4hc|Q4@}<Jd8DF5Q`CMb!94-X0j3L{iQ}y^auQlX{xAdeV zU4Am!AyV|tR~Rt*?h*$Y9v9^W*Vp8j9Nn@>Nf<#~S`ad^+in7ID6!KHKXC6EtbvGQ z=dIuLmSuLbd_Uivrm2|5uk2x<b-C%l1}h)_H$UiaXCM2qH2z45QBrIG>@5$u$z2T2 z{ir=X=*}Xs9TsMJaACNlu5AFCw2aL^V)D+&fEtECfB4;ZS`q>{b`ZyV)m@_BPobgQ z=Mr~}`;Mbt*g$lcq?wD1f$d1m3-E4#iy4g2fYuJThRJ_Ns-&|sHRx`{Mig=!1oxQP zP_u3I#b}fhw?V(#g>hqI7q+;_1jSrXGUX%Sl+UVmR;MWOsKzNJNFg;fr{95UrXCyj z+Eh)Iqx}>*?=GLoLk8AV36dN<daeQ{#HI)SKs>C|fI?LgJLVx^Dx^dUe5U@A-v!oN zXP;_gQBT_cHdQhWx8W}Z8K%WL*ItB4Q%xBI((`bQpN^u2c+{T=PDcxoC^kfyO~JoL z3(AIGhq>a>OcOXKSM8jJEDHxCs;u|%1?jqMgIdvRL$;*{CK!tH3W-pcsdR08<`L#% zC39epJ3m(ev}+(?{P#JG&hf5J?}N7MlpMtM_B)C|@L=8VI;f>Z@Suyr<4G~NnM>t_ zUFv4aM;Fsx$_3e6y*yc`d)gboqe8-&Cg|w*cON`8mj#!Sa3Nr1C(hXe?;)>LnOF(B zO-9BnDe1rl{D+B5jv-9{C^ghF2hG7?-xwxp(Jo;nDkaC8q)Ct_1`EiA2&7s9mqHU2 z8aQlZ{Z}P<Rt>GoqM1^u2TR}zHk>9)2bxHt=(fr||74x#`>2_k>TO-O6q%3^am81M zI3rjzENQzmjCn#X%@5HLb5i;Py}}k3dZf2V#?%ic7xphA#QAE)(+Zhuogb=$9FmRm zKQ9?ckPlISGJ2~U8iSCt6tyOBA=;vtW*wuOMysoI8dD6&vzcdoX=lP#s(XWp=)@dc zAoH9kS*_|F2MnMjNjhDAA8!K@3luBYf^BRshMG5GF<fKBaHH{;cW)g(ifJv-vq_Zy zyI)x#*rqnSEH3N(4LoY4(}rC1HmtUIYI7*^a9#=iIkL{&(PjuPo;0me*Y8V+fswc0 z?mNk;6z8;+CoH8g0@O9=XoWghDQRcs(W-7Hs^OXl!?$ajS{dtQKIO5<CQemLXkSZl z&Vv>}k^b87$$l9;GJ@xWI!%jMyzn{bOX{RK%VfWj*q(qUzY<<<lkOm*R^^K2f9t!H zO_|hNGzu@M4f)MO&bN$M5qHL-od}Ywj_~uFNI#?MaFQpI*YxEIf+X2%biBJBaa}X$ z(TGo(Il++1c9K}I-t%f(1oMxx9Z5ho2@x8->Xr!7Qifq_?TiWN@c7)}hMHqz0bOhH z-1pbX=X9H`BuQ5GTLP+#womf!8|UugTz~vFTi`6}3dIo@7~vmz7SG%!zdF=eV`k0Z zyh*u$>ozlQbOuCS4k@e^wP*90g&rcPnR^F1tu$Bf=yfCIRG^(h1tz>(#?z|NU4(TW zp<EtuuhP%~ds}P__iGu8Q*I$w^(L_6gk<Z|wq|Iv_Lx|=m&RIZ&Zs2R>DOKm#`~qF z&2Rw%_(Q8x1xYp4%Hr$q_v;#c0JS3hp@%As+h{(fX`f<Js>I<Y6tx)6m&W#JP@&8H zj=#FG558i6RJ$Y}*zm<E4mmJRI06)NPYP{3%IloaODsYp2pm#r4fe4fpJU;rtV&}> zjN;og_PsY2bHO?Cyn+!0O`@3Rj6+dESyq_2Xt=bJ-BviqprZGjTJT%^r)h~5aCJdb z>S`kp=yk7MDFh0_W#$P+5SvyRM)1>Ph{L3V7MP0peC%5$>%be0<Aw9IMbcYFjTG4h zf(Do(nd>nIdh^|jA?d=8QV_pr&5FY7H>LXP@?lVvIWbLaEvS7%*bnLKLyA4+bO1%* zKcGzv(S=v2bWc*c&EGFg7L<tedPUc*u<qz@?F|l?j$`py$H=}!yc_cig+}z(V-v79 z`ehJvgct|34-lkia!IERnboh*)3gZZ*j0>md!rP({<0SJ*IppFtq|U7h;%A?QT?7Y z=UdfLSzG6!StqC*`@Q~!q=SnvK4qVpiU0+=5@O*oS_q-n6u%~6=upoY#7UX8)oAA& zeKdp$fX3_T4HE6^4G?6kW*}(^Kv>2|lIfe@2-L3p>j5Jp$&`c$VkhYiQfP*WS1{tr zwALi}H)?9oXsufm#}cxXYg|x|+@tEmNxMjPG$To(^d_ouGgnStLZ<|lFv=k&iDi-q z1Vg*PWrgD(lFECFd@hQ4g7Mf*6KNtYqg<9QW%rZp+xZub+hvkCoOpnp$S`j94Sanz zZ{$s?Q9;VL$U3vRLC4dgh*7MY@}rZ2*IW^t_TmqT^UM}l!)3A7xZh@ZWw}9OOSQ+> zYA@H-Qr@ARPYpqFHZ=q-oj-t%09iE<JeHvbzP1Z1acrQ0!?1<2L@vSzJ|f%E0g}{y zmOiG$hT`)5II+nAoKijjyGU8lkqemQ4$dN238=3?>c+pWzZ%I;3M@wW?z?%5!buIR z>Nd2Z<BEZ+ud|GIjR*(CF&y_XQrc@r%3V9}i@C7P8;ACEaoV-V<-mMeMMHh##Lz<y z%J13kkj)l03cY6AeCmS$u6Yt?EZJ;iQ>c-FPsX_cFjUuWf<Bhx8Bl^_hR>!HCF<p0 zc|yuPR~_GK7SjejxUeUs*fl7Y7$t))c<00-s)J&9oQfqH<-_0h>_Te^+Qjd0#W@yb zFjX0wX0l4jq?U=#IE&(*xEguf#eW-L<^;E@DLk=rqiVwi3Z^zbQvf$$N-!ewF<}$N zJxgL!%EmQFAhi)Nqi;YlSm=ewh~J~SEXU?dFA*G{{k?j|TtQa5AUsACx_w$%YC0rd zF-u|m%aiLukAW8I{Kv8+j0VW4gRg@>f`Dx3U_*@ynjbSB(*~>Ot+~U7_xnoKlu?N` zp*3Rq%K{W^Ze1H9XBnN2oQfeKlIF8PofWvRe#5~jb&C=}C5UJal>G<)byDcPXFJ=t zOUBg^FBe{@kuK8XRlpYOaE;Qi2o*y{OuG09x(aL`;G9FFWNCqDzCtx)bS?x>%=<9K zc$+7Lqcj};IB2t0MWa;FWn0xTbLT}4=pae+31-4FLjrC)gP=UrQjX?D4xwLe&nB!r z`~L{GQ7wwJJu7k0+9^&*p|E*GbUG;mZ-JGSx?Klqr~z#~7ie@lEFRr6N8_ooPheNW z3K{^G?aomDpifs()`kJaa1W?6<X$`zf@e13O{KuqrO6<sNCzAQPGiM*QG7~7M-^#R zD9u!lyaXJsYL{)dDf{*wB=}8BoXWVEfo{=9RahC-?-Z+_7&!J49~^j126F_9#3FGn z9WchRdYZdf`DY%!i~s$nlKQZ9u%VSUcY9&g`Y<{b+UIc3gN|^=K<t(VNOAF_e)&gA zTR!E09_CgkOC2Jn5+QkrTN^^B8sQ0h1}0?}P}0tqxV1Y$v*c|!P20qMWVRX|HZ;E= zVo#s+fKKVMWesPmhsE8bk;5w^hAzuS51=8qxz3aA__Sq<!!2S&##o#oTDmFvmGs;q zbEd2z+P1u?n&o@r%na%=WkbZVg`@YTuCzc>ft3M{(s;RYO75+1Xjxvl=w+F#apbYb zAs@a(#=cE1R%XsFy4(NbiKn<ZpZ8hEf?X+|h%&<e#burD<wG#JJp5I_I}J|7NVJ4n z80GENW0Kz2Z6|tw(|ghLybY~Pyh<{Gt55Kz;=pVn7Hetn?1ZuY%}gr?9!Q*fWo+Ir z{?FLm@@%2XTts`>xvkl@jAdCUo9qv+HSp_!kk88m9S2iwbrthiqxb?W#?*<(tQghM zS<sZu9iJFyAuJa%?%AUg5K)zCr;`}K9wqALgl2q>ok)EPg5$VG1`JynSd9~wF@f$+ zHEud;e6+QXu-S&}*|sUClBjI20_j_+DD_=i(Fpdgxk{2wVJ&ga0>nXNCg<BlqVFXI zB$K~hecqd=C=rLoT;sI!WZndbb+Tc>UJ~MF*&$RP*W3UNDdYzp>H_-P>0{|s5Zz=c zdL|`ZFbW7C*q)Mil0nhJfldRVNuLEGY|JBQ`sCO&$~Ke=He_2{K_tIX&TeTSdrof! zkb{zeV?Ta&cUlb)Q_kAo2*0uz43sc*6v3B4panwj?ooZN%yd$!g$1L<yE#K3dpZ4* zw<S3YT*N3t{2_iwdY*Rax};kzhpTl2o`t!;=#fm*S9iPjLy*{}5Yb@|bMt$N;g@c> z&Y5(<<A$YHEU@l!^IxO|BC=+*m_^vRPm@@E!@R!W>C=XQ>Nqaz)zzLyCj0UR0nO3( zv3rnmnOo{9!FT~YQ+=aG+rfS}sjzDle8a8)44>H?c1K$sQaH3<9Q1s}`p_U_$Z7^T zE(X_6sZ)t6^jYo=84vM9q}a=A?<nA!_15bh7YUnT`4>1|=K488LuQWiK*ltc>`6)7 z-g?2az*z~|CIe<A9$`NUTd$qaFZ&auB}hAhhrO}BHv<l*i#<OSvE+jIrVl|rPajay zAUIBEZ*{oB2{*~Z@iC&bm`ApZ$P7c~o%GoDI-dPhN#}l#5G4T;&%83%RsM$4KxQ+* zIHjdKmVGldMm9Bv{zfx&Hl=wdmX~8$*NkDqdh#-)5vEHv|Msu2v?0vIs*Z%HfLQmV z$f5}|mHcNVH2A%%)~b;bu6|b_;uW_6tKAg4=Q7paorl|=w+*7GfBbRl8A-j3w|yS& ziI!5+D|XaBHUn3qf|rx&!<BYs@>pKj9-MGrZdBi1++x|7@hz%H`Y7jk2pXW>l4We` zPqTghmNPZ?z0g*j>#_Kuep?g3I#%abolTX(Sj!*CwpI2Tu6H08kx@s{&?pd&Ek{#* zNm$miYi^JJ7)J=_P9k#Un`@fUSpA?0{&M&vesrVvXFvYM-g`WQDMjxv!>>??>~l!* zSxe|40WISJi*GK&!S)a<519^)dtaKnHdWXC6SG0N#!#(8`;$EtdmZ_7JEOr`buc|+ zbk6y5d@;(ncZjL|IGoRnXH=qADWuJR5Y_X*tmXlckF{R!KwhOOfQU;{FNGq>UiUG6 zfn$t;qB$6Adxy^a(ss>kvr%@S_d$v)vXmXWWYgPa%fcTZ#i9PXuM?$Cq6@^iaC7I? z#n?fwbS2G#Q6p?kqN8_ne@ARCMKStbBo2U2nvtRh+N?R6&xR}xHrM;&i}-`J*K^kH z#Jmrjj5+^cS6`OB->`jOpB9}}iM&W~YMkF@aOM^0I95o_mwhcu_);V)dSB!XGu?39 zlAh|uek_Z=EgkSY0sDGvrsf=rqJ2C1jFwLXo^wRAExG0>xa=~vfQ<SVIxgF8@*(mn ztcehS&(X}(Rx2-bPFGWU=`R)^We%&ZfSsU%LL02#19QC-6BNU#W^7$wGL45T31~+_ zgNhYf=%Ul0Zs(WG7d`|4q@6tm6)<ggQ^xW~>|tVHEwkHnzqZ;<m-(oLtqPp92Z zrOirPtp1)j_OF_D*`*{NOaI}@DK)M}BMCtrlA-mU{jxh$rfZY{Ul+BNY-MTL7Wvva zQ?y6L0TMe-nVX%lT3Z-GQ(~aNsD1Nvmz#PYARV-vkZTEE;b=n-oEvw_T~z6j7K1s^ z9-&qGV3Y$IfGA0<M;<$uoK-yII6tH}p&7BfeSFer=w|~y^U%IBa`p>xfi|?I0W!Cm z(SIB?C$(LtdVm=}N;U#!Q(H|T0c!3*fgnr+?3AFe<ulQY;?Y#ml$JlG|77Y=wK2(O zL6jMO=2H@|B$r`rJPHR^EZsTg2w?YKOTsu0A{$z=!NS#cU5bP*x@skQROyFaqg$x7 zRB`tu1-gqgSr+J%RZ1Er+l6_Dr+@bsIIJhtoo#f#x7m_m;)h)p>$1BIX3zL~Je8<F zm#n<bMDl8b-go`Al&At!g#Zm?ouY(iA5Wl4&`%5+f2J%%pEhj<LQGj2&fv{HXzn|V zAq@Wujt*DCCepR?Fce$sD4yg)d`6u~{P%!1-{{^zx}(}jE7+v(@ZE+~)s3@m*o8y! zs^H97#>SYlDR}|I63uxTLl#dVg6}Uzc>8VA41ww9d0~Z+n(M8oQ$~4-h-buhJW(Ig zNQ$x=d;xw2c|d0&OJ8n(VBM_S)&1{AIrq+;k!8ybSUSLRf<~7afd2reaL}jsxi-r+ zOFCO$(EkX^G<qsyA<7huvb4_9BbJ?xGDxO~KO{0I6obgt-dJykZcC2Z%p%6BL1x)s zF$C~Dm~80L_B8nZi^v?O@sE$>ZI}R1Ne_w)r~1iuh&@aSw67eyg&;G`MN<j~(P0Ur zAQ;0+;@$(?<^xl3{2rC{F;+h<_Pn+8uH1&dxY2HWyhLe<L8SDWX!HM2nT?l++JF1D zZdW`|^LLS=et#l*USJ>V?`5jq@}3twCck)n{|_0Wv6|Wmx_?Vzpu}mqo{r$=S8RsN zEOV8mR1TSZj_kq6z%P}qkoEqYXH)?gwYDr8o1n_w5R-8@JckaRc;g}zDZ^;(4Z6O3 zkjKn0p5=~loibM$A~JNXG%Tk#Z4Y_<X>dP#3u~~wX>TqguhUd()JBriuC=>*GmSD% zwswjses2g3vtvHdr|Paer(-Z5YbX>_;Qkee?r=G3S=DU-g`Kz37i-h!A;g@72ZnuS z6jd({xbR-uyLne=+};@LvI;HwAZ8n)2rb&dfGF~{f`Wy0KV@dFU?%0d*ScZd_@y>S zH_H|9?kB)1pglWv(zu(Kdwdc;oK%CF3&R|Qi}X)X4C4%QAAhL2SK%=Yqg*Sc470DX z9p-r?`ijhr8P+p7AX}5Gb~p(&mTl7NoALZ0He`)Zha2&Ed&3otH~W``{U7Fe9E+3V zRk+?pG=iZf$H-Piy$9d*@u+*?6&`QA`J+1YnxgzG!OrRuj+5PB$CQ<5<8TG|a_o|% zX99DwOqbajV9`H5$y!VjQJTUTol}l?h=fr_YlY2`&LC8La0R6xL5#X@>0?3CorYVR zh)k$I4=gQAbK#$mzC-4~B-Q=cbgdwF0D2>Ls0(S+sPP6ED*iNdbI&Lc%sa;S#7lDh zXeoVP@5v{YahZ*h5A<}y$CiR@>N<`%S%?{_gdpI%+MN*y^ycE`XC6@dFS*RGfc-$s zY|4Ba`<RaMdP>1J&=<14CGqLv%86Rn%ah3SuO<6YA0Sgic~DfJvnV0C)^n;4-fV&m zw!dUfYf!+DjeRr{3lhoXP`=+iKub@41oQ!<Vf7{-(L3A8W|@swHU@<Pd?RETIWr$^ zv-lcig?gns{;1iuMwyt#?kD2#gsH(Co;GrgnHSf&jL_><$g%8^$5x`Ab+BW%NGIZ- z9eZ3v9yg+zNNiRP+ZD-Z{a_e3=BvPv%P}@$D!K7Vaf(X;dO{0T2Z8OvhNi)~h$tlW zln-!|$C>%#{&g8F>&2*(t1A16PuUk>u$faCwgOju)-5;tmsCh0$@JZ|4OBVop%+hF z;&eV^wwLQ3-P?`8FAJ;d+0L!j(NidR@d^Qu_Zo4j_MNU@iDkVH(t9WzE2ZZIoEAWN z5*T4wr%*vRsa=of=zWV}E!IyT*W1ex1Qm+X!wrcQ6CkWf2eyu&o*qlpmZNpzA&_mZ z$h@kPD;CYMuxrTW@i(`ns?^@9*;|@iz9}}{#hE7PZm@U|Ax$6=%XIbhjG79(4}(UF z$=x8qDO3|TO$(9U>sx$4c!2+5IIMZtIi(+u2`!pAR%xG5Ok%c>(CmFhrBq83(PIW@ zU37WB=P(uOZ}Tk1_9&0fKeIW;u@#*}O~PX9edsoWTBBC2bNoYVoth>ul6ExFrGKwQ zUHb?~S1@(kL^;rha9vS+`%!S>2?3#yYkg+d&vwPDW#Q5wgWR|AiL>)D5P$zAARRXS z&7yf7k~uCUI3eJ<gC58$5pGh9#w0mw(OO)y!L+p91B+-#+L#l;8TxS^z3;;2XBQz7 zc|uru>1YP<tmt8UkjJLd03e>$f_&sm8$(XOdjoT&c!O-H->*jJjUX-qd=izQ&FuGe zp0a!PZ!Sj~Rh!U;DgFln2DXO@BgopT*T~@)Y8{puMuwDS4Y!-8V068lkF4gIavFpD zpg<fNyXrq8QU2f-o{te&kK=aOintAcP1(d1rV{<v@l7y0u@MKWGQ(r41vaPC`ki$H z+c6r!W#wcH!g-43Ra$)i8?v@RDFwi_QNFjF!@2!yYZ#qcVJDg#(GpI$Z9_Q>dEqZJ zy@vqfsM@E0@O+V!*u|h&O2$l$O$x{A?!ApFgYzO)0nz+JxtUM;N4foYrm#8?bXqyo z=1t+z!lbZhC%<7|`ym;hKNowXfvvbtQ{jaU=lW)wQ8XrFRyyqblnpftwM)eTLsk+F zRWm1v9hdm?bYdW<@2_a&&%iP6!}B9P7f+VFfSUt1H;jJDp3QN@CeQp|)FNu|3P8v^ zirrM4yyBZ3o~|fo%jYhs%GT8+Y*x@+(H$eG0MS1Jvhl=^?$10otUXDpK_cVz#hG^w zT($<{3%0^3z^G{xh<;Q(Hsi)BHVC>Zn<yyxF~IZOduG!RV=Uf17t`HuCfd)LU;7YD zUzuClJ?7`Oa(B*H^`9m!HRZP7?<*y6X6Amq06!<DzHbu?H$OMJKkr%%W=2mX2J}SN z%gw5o6*gdb??#vHm9U%zg9j=Z+_%1NXQNabEP5PWY$NjRfMSziF8<>Et6~3+z;__q z%kP~{i=t`z7K3p%^bS{EXM0DtIbsZx+S#14!)a<p%vZdd1=GrX9M=5H`26?_y(oQ2 z7mO3)Mn|`_HLF>_%4U{U6O7kfpG|E9550FFmj;zXErKh=#QU%_pDHUPPHc12BaYVu zwTHO&SF8PH1uD1_HICBrm2bZ`Oj9Gk0UJ1x9h^bYaN?z9ORAlhsO}|lQzpIKtl!Hs z`h%cl^bR~CHWLi^nrQBF)6Eq7<2imqk39-7(2RX>j`17W@Ua7eu});yrb=SG^Kiv_ zY%hdNIOnOxA+3!~c4xi+HoC|T2N!8?7o5TN5HB5!p2Zj?LQ^VoGuKPFOP<$iyga3b zV{D3l8V#tL78|N2H5ZzBY{}zfHeu)b$@eEE6!{?YGf)<oodLj?Fl2^BGkJWXeKCw^ zsqA#wJM19!6ubt}h7BS2lfru=F3!VEaTh2+$p4k<$||2EDWgT6CArymOg5J{grC)S zk@RE}x3m>&!czN?tZ9qw*d3weD@Aj3r>ipV+AzH7xA5)2y46}Xq4>7T@59U#L9mZE z#+4_XqcktRm5LEP<0J)L5%WXPi=uT&G1(f($TKLs2WP$osqLrq@Ci#1rkH5IcxbOp z@p}BDu0^QF7iFW4dhgF6n$^MJMfl>b(+($-s;?aJiA)J_W`urSBT@@5^icZZ63|%i z&~_a*L1(NIq>C>_2hjXxbGo}*cJSa>CXtH?_`aAj`;fQ%yzDRVNTn5jmT5#c1D$3% zLInEbK27eh$Q8_iHt7g-{5GfQ_v7{Pmbn-3w(R%z?2{4k<Mra)yaMu&4e+pq;MSjl zaks4aKsN7zJNW>dW{afPE$R2h1OA{Ul{xJ-jZY{D1(KcJ-gUm{w&~=RvB{5o5Kp8a zj~C@s&DR6YJZ3S)Prp#FOT9K`F}cE2?DI2wC8?sxk3-kxr6lqxJFAB&n>@#Eo+DMe zpC-c@0OIPz{1V!97$fwxVl#x@kp2USx@qL|S@UFa#xpi3PO0I<e023*#`X>3!)w@s z`{2g?4U0={Aha)t5<a}@o*CQY1>?pYw0<OYvCz!tC1qdugP+bWY-ScPcw$Ccb*dA5 zT#Ffl&*Up)l2I+2(ImAXvuH6dp(sHfH!3AGi~JR@=+wQE6)_dFSc2Z`1fr(Sye$Nd zk9KKfalW}M@O+*kCEA{stWI`4?ZgiVW_z1LFtbu%!BMYq4#urMS{FWSK3u+IZm9lT z>FxKD>Doi_V{VWj5|#d=m|hYpOBVZhh>U~Hh%u-ek|spyp}y?y`Es0{GQZr2j!yB{ z95$6O`<m-F_w|-3^!d?y<57A}NyT6u#wVH7$%&$yF%0-hLn<gS7`H(XOR06Ba~JBY zTWP=c(c56ZxPT){<Ed@k6|2kF{L2NlB-@myd5WOlurZyqqEDAAkaMEuAwfbWrREQ- zq?}-6%T9|C51P7Zk|aZ;k>iac9W`;^5{H038_-oo=7R7xLe3vq8(|8rQPv$3c4$n3 z8I6p#IeB(ljg(Iy^gAzMuAS1dhD?|<)?U8e@X4yo_ZepO6L0sc$llO!aV|g&{5|$q zWSE~Ti)0shrBO1z&WAP;939BI#GW4H9s{E2-3-1V_o4W)67U=NiJK3>1n<jD)#mUZ zlK4B+Etu)vzt<~ve}`w{wkDAdPHkYDjM_@-bY0IwZWXoN8dzJgDri0ubRxIM1=0FN z3_ABC?S@jeN^T99OZHziLS2}sh3aMwm9RlM*wE!#u#xP|HvB);)Yh`4knD*gWYw6P zrn+@<ATyJR=}4eaMe8th>Kkb8JaE!;?BG%9ZK^CU4a|b7z5sS4LShPx{PuU7gXz{! z0z;H)YdS>kPe&_#xRYUl_|tH@2guUM|7DXfPwGo(pgA#d^R^N&S6x$m^=fzI-Rx>I zwoNsTk<OlQ7PdHPExfyDFpWuFJa5?5Cu<~UD(<i;IT#Z0#!Rx=5%KWwVkO@TS@C9* zgMwCXUv9B`373RyId8kHI~lJ-Cnluf7@bU|7!^!;c|zOZ2AL=($9Yqj7rQq1b@@$U zE;mo*=!VU?vw;fBIL|bq7_KB3HqoM|Ws1+5dKA&nTe4_TLm5PuYIeE#@DA3dE#f1W zq3vuFhEwG@DRC@C-UI}Jk;#e|CkBNGcKbLn%5qIAN(;b_y|xdNc12Iar}Hh&%`X=p zd3otXn8x^R(XCzjO~SZ3J_3L8Yu(4G714gXO-9kXP79k&$3$5$a+=h;2^n-RGz?tZ z8_7<5yaN`#`%zWbm<i!#gg(_UW&o`zO7_#h@)6qe@GhbG4orD<Uj+mPOWunN@QXXe z95@SRP>y2#_<QL2E4jA}<#3JaAb;U3Yr48^^E8Ry&VJ^Wmv1^x3jW+Y9trmAa(`lR zn)guVA6H)Dy7&Rt*!EW)V_!4X+}NDLI;gn@x^LOu=hzr!?2$bDa;`0DH|zHtHxRsW zxww%-mU=~BM7e_>ukMIeic1)=5aJU!8}bd-uiS4fQC#Z0{?1n^x8AI>r~SnP6~nS; zmMWj}%~oQnyq6fN5isP{ufx8(JCZ8F-PEex-fX`e>&CAZcFH$}NYn-WI(3PjA!sUU zsec-v{Ed`vZH8oc=CT^I4H=tn#IV1<V?n+5IG*!kRZxPIeTwpK;7j`A*IHj)cUJbx zJngU}TV0M>N8;g3<{Alu9xFs`u-V!MF<B)uOFn9&KqL!iY01$@ti)H2EDLOkq&kNx zM_;hJ`X?RVnqb!#xw-SGLC{4NpqS47MOhA>Jm_c@SnDy>ET<ra60dm4<{j(1WZNR~ z8qvG;FPCokNX)p)i6ArL#eGY<b+W>e<W`18NYrJjqIb-0o@xk&o$X<ojeQ4g48ydF zLdW%eNnN8#Z`17TRu8%KK=tM9IQR0Q2^7DSwt&Tc>F-)VgUA7*rB6lMu?!O(lgnCf z>ND})Ja|A%eL*Oa@x_m0GEaS7XZc{u1^W7lIveH^t<DEQKZNvw-FHGc-w!c*+)2|E zAL|c;bH_*&$dy2Yd*}Cf7wh~e>84sM9>h}?Xm`RqK4iK+YjVEZ+8r%klan4qnZhJX zPm}?$MJGDRI>)c*IrkRz;2A)E!+})@iC0ikmp7)+^Gy@t;-xO5QA5vDpT-C9&r}VY z2YeL_Q2y}`w;#p1M5*l(Lr$3Lm#@v;?uzN}%nzx-qGEWgKhbv_misVh>0rcwh<y>~ z)`b0GlN+;z)#@zkVVYX<K|31dJrll@uWjKIL_P)DD<K=fe{t3Yr6<{hs8BkKysoDC zd|y;Z6{uM8_Q8S=wr){5fOMR&+mw(o{Pg!hAi21OT1Tz+Q7l|uvh`{Xw}&SkAcOD1 zA^kJdXZ8ud41042%m``&;|_Ged77lWv^j|X!)u<2J8@B;2-yddoiI2ylw6GY3*%SN zA&l9Y_^q2Gk0Tk-t(jI`{NVfqKTW%O%YFKYX@HUq`+z@d>UCIHu><!o(-wSy<_DO# z&c6n|I#X-Qy90C(IM#*4_gL6*=Ahk<)5?|~Cgzy`*02~idF`-(q;;U8T9UXeEi!Wg z@<6}TTUU#HbtBK4CPVFd*X7Nv)D7{K?Pj|lMD~?RQ9fc06RNpt<LV>yrze~QCaDmI z1Gv!oP-)`Hoa^BdDP=*v=QM8AUleGM9SP);dF7$yRc3nM0CL1Xnv?1<@M%xPDqrA= z)!8az{Vi?e*J+@vYCHOe+)+mV0J4In0^!-INEqo)0b-H2PsfW0QigT_Bi}Y<fQmo^ zXYrClw=u)km|5)bo5FsGO=Bx$9y&*#i<P_!RR~KxPksO41`|VQYd=PA*=U83$oe$s zQdoGGlS%!nj@_|P3tiSuewhxzLgL?E=3Cd}@2m6or^0*v8GgalUz3HT$i}B5Il>3$ zF%rP5F_&=w-{+R3l_rXq8pH>mZh{|`MAl;E<MZTdXawVwo{1(o_c(g#-7Rw^xbwLr zKcITZfHa*~0gt`nEucBe`;vMJA3Ps(wx8@e@pE)GD$?ogl<qr23%;Ye4y>@I;EGYo zYJ{ek{6a;&tr~{f>R*{27_PkQzlcJn3(1{WPa)nZw4Cn)Z{O>4wa*3a|0jZU2cPQZ zm*H{|x3MJMGRp=x-0U9PSBlQksH(WjAyBQb7xbeu1aFdtq>C2#>YoV1T~-cWQtRA` zc*v}L<l-h#p-NPzHzH|6<{`S>rg{40O*YXvv!r!Ilo)WaRL8&|7nTehRBZd=oKMra z!JOUH?reYG^B-Y+n|5`#U%yIS4zU03ZSi3EADQlul633>-LIWVHL5WcIoQud89v?x z1O&e-hzyOUvYgC%xT^0?sJS7|H}=YF)@T7<=Jhbv-PU9~Din>F94HHL`_icjsJQfx zVyYHSewPpF1^9Iff|cZexCuc(&Ad&3A7f}ZuDrRYOq&s~YYY6g(5*!>uwLp3;g-Gk z*yI_~kN-Edb4k_vC`iBcz*}B)gq|+e;`B-2Q&*%%67Mx2GGmOb#t76HcIGg77&C0Z zUyQ${WGEKc$LV7Uusd6jUPAGIQil!p*>w6II)+K?MQ7p;kVjS>WTt5-M?{Fn<JRzu zRrDNioDZ$vmE~-9tZ}4`3Rff{q9)~V$Xd4I9*<lZu8Xac3I3(3NqmyG4vH(*NNVP& zqz8^RhD^0srW%iuZ!1OR_ka3yj4{1ico4f*0Pl`7*B%{<ibHO-RZfK;Mi$UqR6I>w ztnj2^wCyxHxF(c5CucIjFhjCP{L_@AsIO$$@^IPnX@25KOk65ErkrPe`H?ohxnQ;; zRNYaZ?dSS&8u>uwqbT}v<w|SNWt{xbW%@hm2g>~r$q9^<%;Ww*`qLTvBFKw2j4<<f z?6H_>I&dUzhk(3Rvxxd&Cd0^R(XMzLR)UTiMccBSGA=`BeD2Rk<U)cPuQu56O<5x} z*))`t+&0Jy%ppfi8SQ?4pIy0Y`+v*4>Dj>1SAT~G_}32qUm_F?<s58H%&q>{3SXrp z{XY$$zgGB3YTVLpWC?ns{Kk=an#HRjV0=yol}aiMahn6*op>Y8X`A|u{$r!5$xK#{ z2jn%EVGp{P(Z?fDQA|sXTS3w%gyCCKZn@XV<o#qs<nGaR#n)|jb0$?}Nc(<AdZ&G~ z7GxibC?p4Xm{pyc>=T_vfsemgBTj_Lvz;h!oB4yUI4*I<7$BiR?&tEu9%Wx?o4p;J zuO>_acT5P2-;YrTrBXx569=VbFx&dF{ORu&pHm?Om!Wdf{%!KHPZYu`$P~X$>g(-{ z-e&yE`f@evFspvZ`kj7`A_E0h&})}kAKASD5>h{2mzgghzy-Rc-QY|XJuTD(-J?RX zfu?66GEyD?D~K`_SJ>e;1^3w${GP|CD(q?5{OfF1>W6w1mfS756n<{w*4!aTluA*v z9K?n&@8Er~CrZOb<SLJ{&PB92(hM1(y^uKi7{|hWye&)O?4hkXJ#<!`%birtg-2`G zIReO3rfz_;D!xb>KGyU7$UY{Q$Y%bn4M8Vo;G$hdF)tuh`JXRToua-cY1ZL|eh~ZQ z%HC>Co7h#RZ)2;LSl@k9i*~cufH~e65#9~>ou=ACxDqTKY)1fg>Z}fw5)OqdQ+BAI z|8SZNp@RF!{_6juQ2+aI=vx`v80kCw&*x=I&H8^jjlQdO`So-+`5Bj8djA+w$1!7I z;)(kY>d*s+@JW(tl2Ka_WGnr2i$)-lXVx2!P-no!kZp6n^e+q$HaPc5iX&3+n^=O^ zdr(mz6cOtpJtZ*F8n=ZT7ag6Cm5<&VGkV9X;<u&9&4UESZ5j!L94g{7<|v{jfW7(x zxkoWBNaq8auulW?!*AWDF{G)0#JN5l8Wui>v7Dd^!=3QMGg1JznLwu`ZQW~CSC1#8 z7W$HJ`1yD-Lx6u1$0d-B-%E+iiT#ljtXJ7bN;tYldAs6nJ7AX@b>nh5oQptMAz-T{ zZg=4Y@)9>=-n@6LvMaD=&V~SOL|5jMjI7_yT4In8g2+RlFW0e*k5jU(r21|t7{-M< zAt5EgR^^P)C@*?L{A|}K)Dhzj<b99<<=yo-fv|Y~M=>RVzvZQGoaZCEoPwtflG(L< zA)??$#J%isX{5F(BBe$sJ;u9Erwx*g`s58^SjB5ht?<Aevn0rCLc9$<vZ)d4T{^(; zD%iX?Y!VMwKwj*&$h+10HH!WSF}gALqM!TcYOGj+GcNFiir!Af9PZB+);GW`khmFi z*2Ec=^2g{agoOWPG_KqD*oUjpBMXr)X3AsA&W2*7M@fCxt#2oIx8cC-8gln|$h^o@ zU;`X-26L2p8zA%gm>JwjQKy!od0TmY>9tEe(Bd_gc2>U6rTj}fgPJD@lYeo2NZHxY z*coQ!!Swvz=vE_Dkw;2}hJtmx%+NJD#rZk9Q7`#VT<t(+BGMqnt*1lLZRhEi9ey}x z;S;~2@EiB!YSh_wK?C<oBMvn`w)o6+3)?NF)q2s_y>&6Bz2ecwU`7I`Y+X~CJA%$g zAIsPYZ7GKjlO2y$NrZ*WH(OWN+Q`^|+Y>MicMfmt?j+;=p_~m@N>2>%bqTU=O&wTK zo}};L#;<8lK&stz+yoS^l&g=i%GF~f&>tIieL)z)OvN#we=|MWp_&qqlshB7EL%)@ zKnJ}GQ)Ds}CH?4rdddAF+d&UV_ty7p)t9v{^BNrMy@I4ByV>eucI1(}H;4@lFo%1I zLNLZV!&A)#x$9mR+puTeA$-C<UR9ID55jIsww@^%(OP)8Czlsk&>wW%T4sy?M_72~ zY$IgF=Wk5M1AzAI#QVt2D!2>7@*2lxpKM|sZYvCM>s`At(rdLmtahH-jR?JIKl;ds zwhbysG&8`>8MXzPS8#wSfRZch#FJS0!FEx1n$wIA84KLj#f*CkJ&X^2s*-{LaeL$H zZX2(hR28cJ{I=(^Rn_*wP{F2;Zep`+Vb0xI-dd0UooN&&TEejcyawP{o6w3_zahU{ zc(MdWWI<e>VQbIbl!GwjF8W{fIZ`|<fN&b9AjKvFx*Q0+n;`~IK$Uod20itt_-&u| zj?PzEQpU9DYMk}HlZpvWQ?@U*j+R2->za*|(8#mP?HCi(oB!w(0HTI`m;BwVG=H;O z2!5?B*6w!hwEtUzVP<SZ>*!=;Wo|&r@V`!8L(~5?zFqw~d4FSDex1DVV}eNn(UG8C zy=JInF7kMMEi~}9x&eH7Rdtgh1SkpjB46*Y;@V{4-FhvWkNF}5YKNO_j&5vBloI&N z@=Y1m|H#6sC5IXNH*aS;W2O%HC(=3%v9J?Ah`%l4oU#R_e2gJSwio8jhK$rLNZ6(f z>&qn=d&(tW0}JlKVvrit>j_-1^qi6_s}1T=QDkp?^y5n4G{iFco~%;(Q<9V@!kUi? z7(d*4yavs3a%M-Trlh6(Zqkvx=0t8wrSe|Qh)s$<xPX$zI7&X**A_YfoA_1;Y^C!N zt7++F=Hhr=rMrC$(QYa;i8F0wWuX{34K)+HGP&~#b|DTl`F#lHtfhQ9b<}>g3=+^# zy7F`_Pb7yJpVNHZQ}dBK+Zxzk^%DXM#+e6d)>E3x%%sMVr(z-7s+mhk6_2CEti{+i zmvkiXlUb1==L?2FAld?EVu%k1rkHd*HaPRhN6Ae8TE(d>*+7+SXx=fHFV1De$lz~i zd`P2jWNVJK%3u!3veu&1hfNyp1)JSF$PpSNva~v1oGK}a|FiRr9fG~agX7F&X$%6t z*qWb{BxaK0J2%;g$ZcWu+!$xvjE}k7C6#V^HqFeU5Z-2-_Ih^+p&hdTGny!xf87LX z>z$<mUuqp&Mp;g~<O9q<=KT?~G)4+wt6(BOuO}({GgK3oQ8DjOw=Hi?ZL1EOg{r5h zFX|Aa{}Y0IJ%w?GAIRc%WrU<ps;qdE@{r9pu0)Q^2-qcUds|w+0l3$;2T6t9wo*+6 zu^f3u6pnnj(W)B8eTI<Wp$zfQN=a1Mviw+;3h{y$p$~@WGrdJ*Su%GlHIldYAGi}I z^YNU^r!a9E-DK7?RR%V@)Uwe)I|oOA!%zbukctx_H}SfwKU?Cn*S~rlHpm8~XRPC} zVs9sQ$~0W`7%;rq3?68I`BiMS5=tJJscqdqrGlbS54Rd~zQwLz9Ey=CK1+cXkD(!O zk{g{|EFTN#&$4J>tAwJvFEG`xyYFU!3;qR(X?cZ-Fv_0mpzgVeTMGbjxaVl^Wwkx< z{qHi@S%f?!Al}k$%0!wC;>xpXen|bK{EE4@-!(J5%DH;y8_gGDKADX&^&WP6KiOI_ zDP9rK#d<(7lIHbwcw^(3QYzk{+;gh5wz~4^WwKnrT?&uWRbrk^4OHBy(|m<qbHTUJ z&O7S{c!KUp&EbC`Mobb64y+Avn{AMQntk8@0)^}d{yrh@MLvur(!!3DM}bhzaeHG| zC1Nw0!%e58gH0RYl_Yx<85=PmIF=rVu+am39HBh=>;H$acZjk@*_uYn-euc1_b%JE zZCCBGZQHhO+qP}n_3GU3b?$Bar?FOdjggrvbIypE5gM56&{n#%FFN>xdWB4%I&tpO zUe%A%w)PS!q=*hAt>&x^F-huI$tO)9)-S<1#D~VU__6sTti`MNEq>er`s~nf^W$=b z!4h}=FH0b^5BvS9<T8-sEio@z1}5IM#@MaLdx{_{463~hs_oXrRuVEq<6pe@e7?+= zGVe%I?^h|sr&hO5PpG#63;IQQ%Elh<&y=_Fz7<IQ_Fe+6K6T$pjE)j+E752yZg{!u zHIEua7p8qo-ZnRiQCgB2+A_#ozC;MSMz*ikp)Go0C7C66$ftU6TOFaRMrWoYoqnxk z>h4EyaL0KCX#{rh(URBH{I`1$a}SfXpmb7~U#)GH>=xihRDO*kj3AjN>22Bc8?RAI zVrp18h?fnti~8KtGa`*$eAV}oA=JUG@hMD&wvhMN*45yPx}1S3qVc1yy48PMS7e>r zdGQ05*9(s;F3`=EpMnR#1y{qcM3YDgOxD>j&M1ssdL8Gy|0m?hR*i@h_Gb_o;Aaq- z_+PJLhyU0rR4Vp{{ikuD3c4HwWJ~6U3(_)er};%Km1|8Wj6R4<Mp$P^Oj7aPMf69y zWA&oRcamVm>pJqsJ}Z@FsbS|``iy0yiD{|7v%&)^+u=u4<KU2b%^pZW%|RE5N*PGU zPf-gseb<%$y`AQRj5<#Kes~G+fshpWI2(D`tv0)tGhSI@OHyXlVg$bN7{#D`WXpjn zW<B;{JTz7vy~xi-3eYRZfJq5d{X&h62)r*aCF3<619@}snZdLg&^-NYii04Iq3z@h zJsOP6J;ez25dN8Bz{C@{%{f9NTmj>JKpT}$e*XY5|0_}CkiLE7V8Jf(m#V`_JVr-1 z@Ua!x{VVnk&V_?jnO69k0gh``;V)ubJWte+Z-SeplSh`Y#=$SlE=J?_b401}$gl)q zlxnhf<s!r}BL+R816fU%3eP4vpjV%+IR%mjZPjE|WR|!_h9n`rHKps>TPm%94mAHn z2+f=Ou-r%+B|icdj0iocTt+wa{DmxK={>Ukh%w$4?NLgS4F7B9*NfELlzQ3r0&EGL zq)>sy0$GV=idmVkpwJ2nF`?+oArI7kwIh!Lu4jS$L@2VpLB^;va<d4lLHJxTaGr>v z+`xFTwNUo7ft$gUgienhJGupHaSoo76O~no;!jW_tXzvgaIAiQX@x(QK{}R!nFp+` z+M(=%Y_8{z9|m9ln*!Li>~1CS(~I8!oT-01Pd}5n7XS1bo^o72A%H(wK~%79#5?LS zu%H6%4wMN_C#qqw2lbwUNfC~@E!|ylaYIw!w}xDu?s&x<FAnW;`{pUE=ZNe_y~+Fz zRt%CGe>7TZQQW@iFd4~N2lHKsNF18$*L%jUj6;J11tm5v6?3)dVE4Zn$9|Kzonj0t z6joqp!EydF)X4F&CF(yHNirkq3{DoMVuHwKoifn!v!5d7#WcU%!;VkVdy<ff_;Tk# zKCZmz7veXWnYRHLwfEaA#gtoOI!GHLq9YH{D9sz3tsMd@3!}DEs^m-~<$;T(Q|LoN z7K-)HL$#8+gCQ-KLL=O18CbUchPj>sTeM|gYZ|FlwwFpcaPAH5G?p&09~0%syr#1n zkvKBjMYPXk+*60$1V5f{vwu0AIs0_8Y0_mr67G1x2fy6^OY=~r_@seZ_4xWfe`;>* zGXj^NO*r}K`~Gbctn?gA|Jj7Vcq!|CeE7iU4E+PCxUoicrOQ6~y3hsa0xcngKH336 zbn+d_)6?*8`E{!Zwv^QA>HZ0#u)NH2MqN!=-7z03a_YOd7Jb0Nw-OMkJaYb!WOWSi zCw&B_G7#t4AA|||no_IUsa%}q<WCovb|4{bQ1e)@flIQcSb!;1MHJEGJKL1`=|MQ= zIl=^qLXBGg9Ys*GQdkcU_tO04Qv(0iS52rWVX)7(G=NAMF0Tvpc~jPB!P2N2EQF^l z7_o{|=&du9OS|8@6kP^?9I;9s1b)&uJp!T~G4WogkLV5aZvRYu*RP3*`_^=m%3dql zghC1V@`0GfaV?l2sEY*AVc|QX`#px3iLD9NM#HkHr$qa0Mb<LMwB-1tsC#}K&!-L4 z;9Jo03q>0+o{xE)@5%js4`4^WkJ(_+{Yk(a5C)>0RuT6l%tkfX?v~uv`-W3a?c}oX zub@vI{+Vb!jy0M6qtG0k<4gAa!q|B9<c(P@gOC5g0e?i_9n3#}ovfeh_}?mHE5m>G zEmBe99~7tSSOw<7sJVrrflUC~NG8xo!PKVr=PDk~t1A#UaN5+_j@?tyPv7JfWoFu* zV4LQ65xN`|Fv3Qbf_&!TD2oi5&hFy8a7twQC2La1?QdCO2eR#$+|wJ!tX=}*wNFKI zvmetF|L}1wM@}LQr8ch~*shfu@O-n9a0>fku$o_p%-Q;qL}E^9zWYJ^x00WYYZZ{8 z5kokh=~Q%YB%!W^Tq>3d`4Ah&@G-I?){DaOF|Cx9g_5KGE`{Tku+i#Icsv7T7+0cv zv1Tw4ma+c`NP26;{L8w2xLL)L<dl${=nHr|#@`D3>B6Lh<7Zr@MCywjs*C4$LkJ$_ zg6P!AhuHC;USN+8uxVi?I<LVU;lx+?$w?mp?`Xj60A<7Z-<lrAMDEfm7zXZl7FO#K zcsol3?kBV2U@YJvLm%!27JD%dLs=1>4#%kIhT-HyqcnsUA5vJjdN8%huR}jE28*Xf zmc9++Ri5@Sl|a<@yj$hg#5H;YI<u5+#p8MaRdRR-YFNP?F&n)h*ukziRYLw<;7Hry z=y#c3Zg`r-9S9!eGt;D|4b!L$$-A78dzP+H1{Ek|%NNt4uLB<8+c?+_XFlcu;_Na$ zP*~h%x|Hx&7rtuuV6R3>cebiQabWXo=W1zxf(R6@hb;M#>em5}GyZNtsKIEZXtfes zp1R59i|0zyiF6}W3#G*5`i<l0I<A|(+N=S}U}Z_1Rw8bOQ<p~3s?ha%|2KTN#%2Q4 z{c~Gb|D3shiF`7!G5im7xTsib{d4FxA5p*!?EnTPd|-hPb8Z$0Mq@042xR9NNX278 zqQmmLujPsk1`;C&GROE3DYiD-pK)D5r*q383La(G!RjR&r``Y-6i7R*2p*(URYb>m zq_X?PO=&12|8PQ}eIMN%_|*xYP=J}isM)QdX^+&YLleFj0lCS6KL+&|-;Yca0^|M? zCzmi%Bp(OOP5NW10WCGCsF!J8Me)M9w`#cqn>2pNva!*h&!Rf8nlR-YWO-~M`T7(O z6l7b<j5&K%{LcIe|LQ8ct*q?5!n{yssmW1>A|{!*XTs3v4TNiT8RdpFz~N@&3B|RO zwsy33j1`#*gcP1w;tl62ZMor!@)OYxp?`92dcCb)Nqt{p$3VCJ*nQ3>@Dahu#6^Lz z+2$c2qv=~N0sZV;(=xd$(#N8&0O`@E3~8|9_Lq2i)d#$*d}i)k<ws`kLg9yW)*97B z)Ht@o*MY%7s8_;HF`IqwkH$ed=}1I6EV2@4Fy)$5aSPhT<M_sv#1$rd_vfv~yTfKl zzz{b#gJDv)W&Gpmmm}QR7hET<Z*+_WYHGB~=(Tc+vE68=%b78!Lp*KwWY-nmfA7Dw zzoB0>6aYXb-M^Yj{ImZC|7Yodc^y6f`^o(q%HpL4N71dlhQIu5M;FxzE|f!A;1731 z*$(N|f(WLGRdDLf^0s?Z{7UG&Q%(-Y>5%|g?Wfnv>!;I86|y{%Qu(NG7|yKYXt+h+ z<;{IwRxGr(tXj7l6H)gZXYqN)<j8e}Zk#!R2?&|CnF|xr(P%{Xk@)mTy8K_c84uyF z82)g@p82><^Y`$Y@iM_4BdxY=Z#f??6@Zn95E;oMKVzhE9IfJC77mpS1*KaIohYjV zkwEW~QluXK9BIHcK+KSItbQMfl6aF-p)viX#WJ>0ANE}-@Gs#Ido07Ahe%0Otf~n; z(yG2Eir$|GxN2Rlx5j)ANmPVk9i(CARje5l=mv&l38bYfmx6pQfV@~~e%RJcl8gY_ z%b2Lr<8K7Mmsv>Gxx}3WSOk3dLP1DQLP|G^h80?gP`9y0#=F?Ios<)%w7ME~K)Y3e zTZoM)N?EZiz@+H2mMTYzO!D$#d=2;{b@`8JpTC^_BxBzd>)w-U!DVQ?qr$;uVo{U< zknmAr0!d1kClpd+Gpb2U=Qi@nAn^&?DL4J#iO&Ogze;vRq3_^xN_lQfxl@Vh(61PX zWFmm1;*y2#tzA+j=YG2Jjg|~n^W!O21FSe=3i2P{aFlU2CVW|CH$Kb>roIAXd722_ z?3h;%FKJxPMmM*`83FoE^&M)0=a9W+&_xtW0n3r<8vM+x=}2PL{um)|v><hPcMS7Y z?g?4rq$WbQa+3UZ3E^l1iPS@A`WI!Wa)vFBg`nb?Blpm!TPaiZg(PEv2c=l?CWFAD z*IRQ0GDblZ%jtCPuMRkQ`;v{Kh(~l8k)4=XhaOgLRik%Z%Ugf06SyG+2!0;Otls*v z9%Y-~=$&@u4a~SzcD3d~S$%8zL(GSNBFKkEIz@QCAOxKq)vzv%tV~TbXDuM_9cU=6 zxRPp}v+I#z(Di;Ii<nzceGt*d{wQTK@%xC``yRC;A8kt<3nbyg9g9~-JWMSq)(-Pz zPqsA^dw72Q^>de5ubqbL9JaL{xBmIQnifxpbGJz|q6ZiTw4=qA*59W`C9EjBqR4oi z7=J?WDEQu>)s)I=tc6z^!F^V&`0lKUvRPu+zQLALi=s#aY;I$b2IlpEn#|4cwf}xN zu@t@`@GVXh;uMD7Qb!9M?y?Q53$8m(*0`gYV_=W0w<Sf=Z(g#~U&UF8mAMzJ8+bwE zV9icI6S%_XB(o#q97q+QluMik2vxJ-AEy${yU^FvIF-7a-#45@92Sdva1o0G)WhE3 z%}ec%g^0z&Iw~VXG7Hp>GveBl4#wQ0@d$X?eZifvo9fzpV+IJUx6=Qlj;O7t|ESay zbWs+UV=O&q<^H|I!2Ys!0mO?vG}`75=_iamB&=eF<O6Gu-tp|ay`s7W{=9FCo#6}^ z)mpMafDv8~E@xy3SLQ1TC2Ai@=u(@CrYZ2_X8dB}=kRW<X4R<X#FTWG$MFg4Pj)Os zu2cNkS;!{s;!FK55qzgHaL@!!ZGb>O?1@*An82e4@*yKfKdq!Gu(%WT_k@;T%4s~E zD4ZG&c7i7QL<+=S(l<!-DF#mgCwM?j8pPI=A9g@(oIJ#a6@qY;Wnor0yHv7yw1`r^ zq6E!~!OIbpTv@BobMdWkvCNq}1fIG$#rk(h)t?!R0}l-7{XV%~Aj%Q*dc8Sn5a&N} z-_OkloEJ7d{(1%3n<k3$#5(|t3aZH)1OY4$?j}JIzmLbX^7Us~>DJKe&meUpMAt!X z^T43izpD7_rg(-&+yW<YMw|Goc%AHTkML;QdxsD93csJ&UEG^LE|a(3Wun1>xsaVK zeNJZkdwbyFhuC>9Buy(6SFA?mL#kDuEXe6(8+#rgSfHv$FvX$YVo`QX`GT1g+L2|Q zZydsfXcLfp#BFMc<rdclOiP?~kNP>*(OpCLn1v8>DtbK3kx|Bv4zBltJo^A5SwhXY z04Y_AnWz8C(<iNRLjexi3nzk&O&|oWft>^_nJYw;n&=zjukzC*<nucXo!>XvZ`M%t z_ay={^Q*5q5F1akGMytew5}#2oZ9wd%QTKB7hSL#6+UutbJMwE_MW)cGZs^D5D2GR zX2b7Z(#kX|iZ}h?s<%k<Qp`zSpU-=`mrIvVL-o}!>MyFngDiWazDV)AUb@6yIJ$?W zzu7ACfSZHgZmG!~b?zYm6Jri@r4Idk!_}ak{CpEuBWe|`(uHt433}b_+LAKKBh|Tl zEXzmqqXg~9rpt5rgRI>2ixRT9vOR;Xz&o0G5L$?YC~RqLz#<>0=eG;{^l)lcCC7dg z&;c#fPA63V8S#!8bXJA4>R52EF)k-vp6#cgy)CWO+auQ$$xQ8&raB4MpX3}<?la9j zeJ)F2yV{|hH~q&?pr`$|yrmQV!W~~UE$DB$=i@??9br$!`HK$vQM_@&&BfiAa!0Ga zmq}^b+L!ozPZ`@Z_X`W2J_Bk;O;f3Sm){FWX_LuaxhqjlRRXn<?<+{^i^Q()xO83@ zB)bnV+S50f<&<?~ULow)SDP(jG;+%lxyZqm5FC3@j8a{tms*(b^?S-UtoH#Gp%5vt zJ^|*UzCFwvB*}^Cj86*s5EHn(eH3NqmF;lM%UUC%2E@Id!J#w9BJVZrnqDwm;ib$R zcFu2YHl9<^McA^irIr=!n7%wGXK^50kq+zqM?>x^uU&Z59gz*XHJxoIW10(o<<KLY zI;^YnP=$Zf-7NaddTO3XC$(-pAfI~Z83a7aRaNp0pfgw~1>Je|+CDo<G*~)e79Z-0 zi&z`kvq8l7X!p9x<8;CE`388Hsw1)yB>k1>>vrYM6l3%A@g{9w(F3C=6z$KVP7;D` zpiiRuL$`@EBZ(<>a@v!no$0<s9Fm<c*3X4DEeI>RRM(Sz>*(^^uKpaM`IYX0Z=&pW z0GL2Wt+mV?)x%GSSQFFqt~Tl9<ebwMz6>yZ$nPp+MfPw5ycs{}+(bxYZ80X*TbO@1 z9rpoZb1~lx8EO;ltm!%l8(s@{MotqwoocokF}2Kd^-<Fq(ClEi;#oXr^@UuUr&oIE zy?jBrWA`%|``2DOUWojW?#oPohVQ5z-lR#fOSydh^9!U3PlKxl=C@zr=FJ>kc9KT7 z32|zbVXo%%uSewhAiSCa@xiJ9VvKs2e2Jz6MrRa~aA9LhrzigU*d6EaiRTfG8VmkJ zgYyk4rj=n*?-fgcKsoArEBRR12Fsqu9wS(icb%WC3J34VAYHY^g5Xq$4=hVrJpl#& zvGnE0u@<--&D}_`(pUNk(!!<A-v^h@dVlXU_lq^su2(7qW?$GZGS&>4Z}cXU<}}Hh z=tfdC0B0-Fu}?{>7o70GVBKdNn}GywW=wMCt~B!X!b?@QuN<X-N#(5I<aJ%UOtMWO z4an(RHJ<9)^+xIsHF&z~9kr1r^?MBt<`^)gy3WKIaYNTmT?Eg_VXOR}R_A<Q4m`c@ zgnG^3K5&TQttzvKsUK6=k&_ACPrylK>q!qah4%#DWnkLqph=a%04=}gQTOBNclb>G z%hufJuuvvXw@E6y{F8A;`-cna$ZBf*V-Jxgsh()`$4>=>_NAq<sJj?nzIm212$w#< z|GVPtS`7UF`{{UGe>&}du~GY{;{8AW=*F0CivfIOALs8d@aBV^GFu!d{&okN%NgrJ z5CciV5}5_8yY1x^7iX>$`I7K1Jg@6{oQ{18(Z!e7mSg(D10oYk%+l=nfGB<RRLyD2 z8R1$YPhGk;14Q>g?&jFdKs)b2bjLJloJ7-Ar*3<8f1G+kQWR@hLkLrXNo}E-B?Hl$ zjBva^cC=<}OKa&en3?ne59U6cqbB{V*UBwjp@U%plDh!m?MP0)b`i)6!9(gVXEY0+ zY@Z#9yq0ElJW3G=BSRRV6B#miwfzwc4sx_8ck^bO)=n3iq!f+GdsH0Eg*4p?=#9@u zal6yLY^iG~9nTv$T@|3q(<|hZzVHUSJ&)M_dm(*`4JA8&P-E4fh5R?=iH)U={a@Gr zG!K<glTd!V$imO}$Y^Wm32-}kiN4l|_Br_cciy@WXA|TT>n244vtza_!Pktz^}e!> zwE@_m#3OQ|G?1g9$~g}GXF#fpIkLR%K_&qOYOr}BM1ir!l_c0Bf4CQ5p#*LPHCFJp zwyoz6?^`)^*Pl+X1nIVO8XIcq1uo^c7gX$9rML0Z-o*`egATeq>vT$Gp1M8fWV;7M zSb2fje5#}wPRlBk)x`bg8K6F?9!cka%TDvO-}BP-KHq@+_X-RlKh4Abz-&W^002M6 zc>hl-)5hAt$i?A@+x!Q&SyUajUjL1}c}@l4fUquGpK>Y**i13BPb!2~iRRSC0OFTq z8eAd?MVvU`y)7Z1ASC8R{A&#ZkdN?;d&(0>`v4{F+`e!|T{Hoz>NJ09l`?Ee-ZFLz zCBg=N9hKjlIRu^)sVjUo;oy(jA+vBcd2yU6fx&|-7!T&w1^9b1S2ju-r1Tv+jT2~O zB}(~_yz@h(PeMyEBN}mdP@_#*$9W(zg;wlCJ3@4sK%>Ge74s;D9J7G4qC8e0f;&E= z0{nCnep;pK)Wa2y`ULkBd}py{GJtn5@ur_b!F9?RwDVcrZJ=#kE+WDrdnjQOD>^M; ziyr{PzJ)KhW^Qjb!<)tLCcCGV%n}?K0fkzQ2MP}J*X#bI_7F0vLITIOz>kpEW*MNZ zhuQl-VK+cpub6POH+%Z%yXiY8M})#ua(5SFY>RhycZ{r_Nie-Vk3{oqBr<%ZsH$&C z>X;nk-noEPcnaol@dJ*k&!vg{ukm|)_r9(^RChnVlXNfa5rbL0oO-B9qzUMYu`tm~ z&(UQ7X3`1#Em_qwM>&T;ba5+iiLRv)DP;K;0d`QW{VqACT~V``vJnVf7<tQhJIm;N zKd7%pZBLW^fnopJi!e(Tq6P7X7er&C^Ok9qDbviiQ3iNEhe;m{n7;853_c(iHvc@6 zPceQcTL`AhZ>2P2|5+!ZVNb1#j-cbLyGkGDqp-9ien8(0H9-~Lq0}m9h>Zz0+1^0M z4<%bS_HG+LKzSoM(K1CW|JudNte)2pw<uYI1p#In$y}1Rl^T$s2}f4p>0KIGW6jfB zq}TBm&Bz~@ltF8kim_vEy*OjZIYom)Cb=_{gUEUCiqMT2(xd5m5=|sA4zuHpTrEvS zr;y#{AJ+&aa_%|Bl~qpvsJ>js78&_)6ba11zUJP^zZ%<lM@gv`CHFOIi*F5?&2Rm) z2RLSqs>LM(D8Wj;MeDHcDGbfj9Vcnz;*KgZQ2u8v;R~bdJnihHwMFTj!)J{G8AoTA zcrgeKKrl_4CL;OQaH;O6M9TDY{_4o+1JnRM9_Kq-PrRV01;uj^5qe0WGV2zx;jU7* z_pgJe+wmO*GKc69jV6ipsYj{p$H01PiUnt1t}~zaZSR*jE4YB|u2O->boC-{$D!|r zB9-+jOMJAKL8-rQ=F1x2fow0;%2%;g%%s}ZPY0@tJ;V78L>{AnfFO}LPFN5^K7_gL zNH44x4NiK}3@a}cAj#+i6orfX^d0yg#RR<XwvF06A;5bb)*cqnv4a;Goa4NYrERuQ zh-rYf&{ns~JV7MX4LNO`hd+GQBoObX>F-|`HV8BC)!wq-cv!gsJ0||}*wFgcxF)w& zp}kx!T-Lj6XlA?RyYc*w%_ai()6Z270HE^oM*S-yXZwF4(owzUKOBnS^;?)Pw!Z%! z9DIZ9nkJMLFhgg6F0zKSpjD+R74z4-IHTFARqCoM6V*lkBXcg9u!I+Co(U#`vCSri z)58GZ<HcmLaVQ)f^&c+Ljulo>&Q^3&bIuK&FvlNtC4sUFnTak=mh<11nEq4Doe`_6 zH;be}>n-poC7ZQ!(B!;c@(5#M)8$+{C!eDPs2~H5t}d4BVsVEe5t9g+{_u2ug>h_! zZUH#6V*)TE=IG(y4P}(6v8~n!;+DAi)pMD--aX#R94BUdNvSO>Tk7!=V}nd$ku4Wt z|ASf8xxv<g{;W>O&%^U?42Qj|?SCSpGGpZ=`stAax9_R049IGCgi)ZdN)#w76gLD7 zMJY7OdPyl-<xacVM%OC)|IBc1Zp7bjjzgj~&VkkN$(xh(Fta3A_pn`JhzJmr|5Euu zv^m8!;QJ&@Ab3)R-KSfrQ6z`pBgiAb7y#Xsm3$#w$0pL%hv9<=Za*K4V-4p&-gaNT z5S-m~#ENHozc^d5g^#<%W>;->*z5L$+V!JtS;n)hua8Efc6A_h3@-kvHqAl5j{C4s z@pTx;rSdkYIoMhI(mTD%{?5%=40Zr+wrOFm<o20|%$ZH6$4OTj`C>XH8+BD_E0ww_ zY+zB3^%HnR|GG}rKD7mRZLf@ywQ9Y71N-mSD<e7fnH2cv_4<MT{*7+<>-5jF)u}9D zvrZ4w^{#Sqf~rjm9MsDTj%)!7c-QmR1Ivd`-n3zEXQ*PIX4vhuFdm0Pwn}@RTQeG+ zE$W4g(^N?{i|?8sM^Hx7m-@jwfxZvXyB(c8c#pm<Qye1t9S732YRH|s3GQV9!Eit5 zaRGQ%d7RljF%%ttjT6y`aR5w2GKmRV6FtO@nuj=6h8OWmV<5q*q!B{g{%pg&7xP)3 zUj0b5ZW}q-a{K|%Go7#N$MBpxd}r}Rf>a}1F(j&2R}ym!gOvD$inkQv^qtjz<Cfr} zbTLON6Ka>Ti-FNZsZ_RNZ?3cxJ_dFYPjv}h@^1x(x8s$&mt>4RsVR`8p_csn0`KxH zvb4-h!~!KLU%0wmJR_gXgQtiq*p7)Cz{q`V(D6@v3VC1HP3>Af2U=UgubN)Ha`G{8 zLXy1Th@@JS2hv5yL&b4^p)i1Isq6N#+*K?{N!Lby>P8}=^D@dtC2Mr-0ZjY^mQj9{ z(chqRH2Y;Gv}^}SghT1l>MaE4syx5QP;2?MDrdn_5TS-5&`{o-g7>CFf6@O4bz!Aj zwM$LLxD@VQ>#7sHN<ZdNGw7f_8(sH<MW`r3$_-h^`nr`s4l~GJ7zN>BOoBz`&WqK0 zA4*O+(AsOY-f9xLlw?;ePZ+GoTuT3BockyA(+;M$(HIE#j-)sx$#EE`TsIY^Uc&Af zN*MKASx0dj8x5E9qEkUKM0Hj)P{%!0impSqDXLhbv{c~%U;Y^PavQeb36#t)J;?dv zocddbx&%HfwB0tXPjbUWvYpJcF<1YVqz4JEl%s+lE+fL+7kVBkcb=QKJJAt6J|3KZ zC2+8#JoAvGkK%chk1Btp^WGMSWS`4O<?Ez$#0?gP7kiL@c3!^tT=2a6G74iy3@~fl zFPv**2BB>3>LN4yTBdjnA3S6)z<M%96{aG!)sgq2KXQ<cL|xQ2Km?KbQ;%{!y?ZLB zmv9jj==a-C=~|cLuS^DtkoS%`UoY4=%Oai~k3}X!t8AoV1uL0YlcQK;Rp__nXzyUo z>B6*cA&T4Lp(ha{c;`jTx$5IBBZ2q-T<0a)&OFjT<VxEQ!2T~)vZ0=Xk%O7le@K=~ zOsyXY?u}Nr--&h{|9C%?gTF88uOP0ATpW6n{-7Rs<j|-<u{?ZvsZ1RnT#{HqA@PFv z95Y!SVpi7<n=PDZ>aw5)m|3F8Cz?`tD$0m3y_iO=Aw+ac(r78U?Wp<WN4xPg^v~HE zzXdale5>Uin9hz|hPJab#Nm_bG?}3gk0C3uIZom3B#J`sVvpU$+TKr{FQ%|K8aK8t zcK1X}olFtATJR;>7yYqWVxdm5lTQwZCsOWh!4U5_s*k{1$Bbo&WaRUFzZVuz+V-{) z(I{uRhudeF?=Pp<V=xq!aBl6dd@n(ao1hi0nNx;Cmuc^1v%qO`+GmEbRbg?D%wiaS zH>cy1;b-#{qz$W?!NEYm+1iMUXXJVSYWjrzxI(b13~n$~dh%m)KQlr6rD&cvA!sLF zo?l7m4#UeE@tE0<;@C$!{h?L`x78vonHkk#Zm!&)`kBc9_)2|hRH|_>`j70}_@x2p zz7%Xbv>RM4ru-{1*s*g_eU751dOajNEzvM<qHedpqrS#Z*mIh+4Yjb*uZ3m@W%p@{ z3NCs~gAzJ#^F(Wv_Bp1Ml;<K>XqQKw(DI3-HFgIX%0wq&`=2?v!fgD$4LNGyTDT#i zs3u^Empgz$<dc*m#R$a?7QilFt(R)qV;TEeXJTJzK&(a6(Ke!`w9i@<Ys`CPyt7{R zBGwZ0`9`xEl*S~}bSnbfVWWNf{P|115qY^JBs7mXA=;!of^WUgQLWYIc9L)k`2qb0 zGcSAkB0F+>2a+NgrA>C&p?oZ4;6yxh>G<>;s(%9^3?A>4|KYn#*otDrhN#HjWzL{< zq^7(2o29z#y;vw)wV#AsU>JkNlEN^HS!3wn-HriJG$@s7oWEBP{jh6=2{RNst49hb z8$k2RA}&ESP|m5xDjOf<<rl#rOa`d$c9358<KR^*sC$K37My8oy%mW$hNB6VCXJQ< z@|vb3fgS$LF9VY6#=Cx1Zz%`y_H;5{f=fcX)FubxMFbGPbgm4^r&P3<hyp}$#lmGm z+bHj#K5}``2pa$Jxv{w3#TFGXotx=qrtbywRN&KyOsxeRbW=t_JK?hppD}etyyS$2 zY1!AZGrN+%PDD-1oUn0>Z2Q65ysMm_t)cogm!jr9CEi`g{4V~_em_$!eeTt#B+5HX zS}uc=r8;NK!t6VU5~JHmk_Te_bg9&S)A6H>F6d8#E+{qGk9+v2f@c0q_sn(jDHV(8 zGU`CU)8QmH<E4iWMD5$TZgUi>=MWLaFOCC%v_;E)#er%Tza0a%76bHzjzkIFUOAfg z{vu83vWw?D>?+*QAr5jjfakcfQqTn0O_N5}g^o^ZIS$M6hK#D+GNgL!K?NjhIQ)J& z$7B^DgJLPVXl<h$tFz4oDo@MT^`M)OB}og%D-Lo+7`rL=<6Za(56AN=%PPe5Lr6** zg^`6YFuqvqkG;37^Qy9^)#8$F4bAFm7;2uM8-QGfKLbtPdg)ctGPAG1^t0v@NM4=F z@Bm~M;-<5Zd~O3^HXxhqi&Ux2o0or3UEfuz`enlnxShvN_C&O6U8VfDtsH_B+)6_( ze^@0sCc3mn)`$j26B+ocIQfv}iD+Uh4e6w%nt1^<sAN+JZ`d($*F5OtZ?sO-0~?;Y z(>xscTEbbfMSuJ`f|bWb_*uSjW@&!fV#eBo_~+^TGz4Xh&N)vcrHGW<BWd^fTIsoo z>!N<}=T@(qEjZOjUc#?Z(5RU4;o(K9nJ6Reg08}J)#~<{w2eu_U$RH)o(SS-uwt<C z#Rw;}u=1)|n$)fX5ySA8-H)?g!e!}sLtOcvTrs9L0|Na54QwCm#D8mH);UAK!9%Ox zVRsIh`8;WJJn0(HEboTk6$9G~{kgXx36f8BSwWK~t~(ydpqkEIa-6O~{<O+rd6@Co zc$)`KytvLZrai$-eFSL3SAqJXRm9&w7;!6#mU<`^Z(#+H^!s$0$Z8ZH7-&$<9Ue44 znV$f%*&wsUOEinLg)+@fF{sY}2{sK#V4NIXAe;leAj@Hx#C0;_N{22ld=|J*R}S#< z2_CE=v9Xxj8x$6&hkED8r$>g{-2?3snd6!s^LsW|W1~CSz8w+8?5Vf8c-%1SD$lxf zFZ})=oykfI9Ml>l0Dz;=zecG2FurD1dY1nw{xmji)_)^<KkE?e;4b|+@#Dh2!u7k% zGuzV!cE}V;b9%0#2T{+%c_J3iEg87d@fJAqUkD}<Pe>02AT;s1?<EMvj1)^4h~Quj zZ%QeIj^imBcBnP5JGe3MQz%CFG#nY$IQYIs5H5G+e<ta*-|g9E;cA<blKex6%(UD} z!tI<HO&s`bfxNrJAi;mZ{tQXRAe4JPK$jB80Ht}1<TXYAsP}|`=hKCc0(Aea1CQ@j z)N?{hiwb>>`cBNBdryk@q0ZS3o9m$<Refubt`AP=Dm9~`E6*NMECt)37L#c^Mnawj z6e4m$>*VFRPyM~CttnRCnf_ZGUSJ)*y8fh~p>H-Bj-1V_zDF-igqJ)Y*Ei30^sd<a zhHQCt2;<ilGVl&DkD%FE0!ZIXp?Wa?XTrs4VvQ0vsG*FDTcP{m-JLzr@eM&JMG<GQ zHs^-7_O@H|vv);xMne7GYyac$^VadUhCn%8Ip*f%+138$+xFYcd-U@&+#4^rq{7DZ zH+5(b+FAc&a^myLuAZak`ze$MO0fM)^Epo^%7=n9JW)4Kg>^%0JIl;QB^FV%^Z7UO zR!OUy<4tYE?I8=%2Ss1T3)PUmvgusm8C_bDJ;}|KD?hUji-cXvUmQGERKbhH4LwGO z@30lzC8(y&!24#Max0~w$s_R!t<p8AkUS9Z@uDy^mNJevKYqjwvXs%^IQZa7P(<7~ zdh2*eQeW(^G_C}GFPpdBDT6eY%Lx4>a=SIOUmU+3=Ms9H7?595{Wx}Iq0}Gz8L!E? z@w3xkD^=3~D{%){aWunZR+ENcj=)c@^gwrfT(nTr+DN=IC0laRr<C%<lX^Q747$*c z=T22e;jYqTgcOTY&}us#I*b*^MYa3?#4|y>b>=14vyn)M!4GjmNg!t9RV^r!o3h#J z12q~xht+Pw3%O66-NtFuL^N;LOhan~LQLnsEZ!%1k;97AoLM+t@fZ0FPVw%#%>k-f z%|Dqd@*lQAq}mj6+uCvt@)ZV%c4SR}^24DetZ5y#^7SE;LmAqlBs%UfgWL~@5!DXV z1Ied~--}3g_fP$qED#00laJvWI+p1>)=pfH;9Ovo@~yAWQ`i`o66l9lb1hI8)G2b~ ztnO>AlOgw<TlSaLCgUp|KBUPZoV^Ak_1*s{M_>nl)Tg&3mdOHwQg7!ENmuOu#rZ_P z=^oR%i@7>s>)8Yx48}q@Fw44_IbVN54k0bH08a&Cfc*UT$H@w{Bdg-v^re;K&CMAL z8^Uh82%S-H8|wA#u|ee}OM^a`HBJC)RKQ|szlt2XxK6%<Ekd;A3SFTqi~Cpa1a(B? zr;>DPRwF8h|71H$id70zIEx8t_|%*wM{06CjK&I#NinYQqTE_eTGbBmXPY(JS&b78 zX;tYf^m>WKVgu3^ZU@iIuUnH5#QbY=$Ccb)#@8jek_Kiyvb1#pcw@><H^+-x-2BCd z1AnRx-k~;7B@P$=)KD{C!(8*3aLt3&J%0qR>3U`N7S&(*c5(E<oN$S`fj#(gTGR1u z3TlFH0h1X%-zKBES5EIFodu@sLLkWH`vb$)GC|zj&Bb~FCIC-T0(De8qaqYwm3YS; zS@rFKFnKLV3g}IbOQZuHJ#)dhVg!1PKZP_~U`SaB^^y#LYi;YB8Wvtyy*8G$Y7Bfn z6mUrX(F#t&)A8{pM?5@Tp{l3HjlUsQTOQ<yFuN>N<Xq-z3!f=HIgkketq-I+pDdY& zHgYXZ_ptF)#RxRibak78KNQF0CT_!E1PQsZBf*hwx>J?~T>QF-sznxBi5EADP5c{d zKEShOyX-2o;T;#J$Y1Ly5?m_;s-Vn`0oD0|6#LZXHb)=cvIsgx2g@V7(<)Rr9id?* z0*IK~|4}R?c=C}${*qLP4NeAYyk9>k!dx_PEmeIPsdijfoBxus8%pX2WlhSH%8@JU zH|$}J!!G1<cxlMWfFr?dIZuuGOk-t_Y}C^0jvmuX<@zmUIiL=d5mI!|(V{YA=1<V7 zYJ(N$hQraEsqbSgLO!dtE(Y?&OpqS}D|he<oao{_z!f)%ShG6Yds7hXw^DLI<=mcy zQ+7>^dbK<H_4n63Hu?G=Ucm>~5U~5=X2gt;EByBk?Y=>#E>yN!xMP4|=+TeSyq1Iv z@D!j}@DnxjPIE*`$YcJ!TPl5%ki`zV`S?%P_&0cQ{2(BE^pPZ(ItCtrYY`}dh-Q-> zPeKlE9_o(uef;}cZPO8p{<_Ap3h3m?);$!1dBGUSj_Y)nTL##DK_bUY2$9;eRL<T; zBnlx<A2B5Sbp$=`6UH_<l4}}^hs;Iy2|;JIKy~=BP~^^?9QQKnWrRXgv<O9E#{~*$ znE6J`IjPar91c@Nhx|kRP63K4_Ak~v4C(w58K7rnljZg?wqGo0xRPZ0MW#-)`TV_R zQYHMCZ30L^o{N>`TN*;;HRx)+1jR=xw({j<cqAa!lckspZQXBq{mQos*BROdN&b%b zybUAXm6DE!jYQW?TY;wA67+eGrKm)$CS^+Sooa{g?+Iy=S0mCb8|7LGWMdqO+Z?-4 zaB2G}v=iy4LZ+>oR|{SZIY+FYD}!&DpzHqab}(cGD(JFm)L!DV!lx@cT8W!~!FvC6 z+D$2a*!7-AMrps3)3XzN5pt*LwIq$u*`rBI_UHTe(R)eZ;D#GpfhR)FBuC#)AuJfC z+hw$2!b4#Q_sR{VpENqpuzK}YUZG>7)`cYm5>iJW!U7|!Tzlq9b{%^1eZfM~rzL~& z41^qBDgH*%EUGcw$nK*Yhw+DOa!oXgO|0y^+lr%e9_-o+Feg;cK3C?Hb}RB!WsK5O zGPI{z8o1w4-k6=<QUELKbx|*N3tibRqYcknZ^+|LNzj*tcVGw^&X)@<mjh2E2Mc8A z^mWfWsA?<4NzkUq02FiY%m^!5go{j8v}f_3%}}5cj51iD^qrshH3AKJ;YSz@pRH-B zm$%|#0Mm4^V38HXt+XBTjtn2YEw!WmI7cfKG*&#s*=SzIprTgBY%(*R6vR25Uuf#6 z{5R`X9#ctGpsDP@d<Y?H$-HL6e5wv-a+bV6dc7%=Wj>Z&kwuaN1vcsT8OMd8*ZD&} z_qy~^wm`6@;a7NFOgu5C`luC(-LF74swRUK^TX9pbGA2o$-!pm7ON@cMyHNcv5&BE zPhW@orYD}-Q`6+sBY}KEhgCCEveI%LJHiBQlx(os*+C|V1!?+5#y3HshOk$`fa4!^ zlA(?Z!sF$D(<lDCE*_x9Sv-Yh=A=RpYPNYG=Ct)tOUob!h{(NEJ?uAZ;ebtK6)bkR z&Qde!*Z(AK`Wlk3SN)J$=|7|v>HnM5GBh(b{{I=ROBIbDyaL&4xf*{anB4zJmm)|H zOr3f>j72LMFtQ!3AD%q4PHnz`U*54(r@KF#SUd^4m{%w0l_1vT3eD^I1oRDsN*=#= zR`^_evbqW?wp`6|2CLVEzpA>*ghz`ZO^g~&1(Zg@CClm|ieMsKEH}CSTrQV9uU%X; za)D2%5SgO@4&~6|Ao>Saybi)cZ1}BsU_*`M5RTE0K?K=0F9;^14)DmO#LoBZA`&K~ zg|P<gS^+t|JBp*4*o`DjJc;z6Qdqi^0+{FlI2w(ujbw1g=g)qdDoSdLpd~EV##@ca zwioVT2WZm3Ts7?@;j1a`N-=Y;LhhBSIaQg9ynv2D1O-OehisRCb`m5v(!~EGM8H(? zvyW_`r@%j9N}}Ww&*9?B;1%=17vr-JBDuZ7!7%l_H$jt9T;)SD;&)NGuN2b_x~CCc zF-?{dPL}BJ1acx+DI0&VFKw`lQjr7UCJ+1?VBa3lD)Q`ciNc3{PjQS6`x4klPqr@8 zZ-mEq_oJR49HXKX4V51uUo^v{>Kg-*dxZSRZWe;(1aBSU3$b8LZ)Jb>+EF)Hx^d^; zKN`6Waj6Xa=@ouANj!X7IMDqD0k^5#4m(kd6*>IGMdxO8RUe1jSC@v?a?dTMk!q%Y zTTRZJ)sojPki21$W8YkTb6-xWS+tT5V`DBsgl{v1`&|zyeO_V}H8p>Am2ny4JjiWy zsdh9y6oKAovDXNQT!^1!qeVi~&6iS$qK+dGN{I%FVZZ*aM=Q5?@%&XGyiieCLM8vO zKoSC;%3(ddV?x7r(!x0$=nDr>r0Bqx0N~p;eXBa}k<L9y25Ee%Ffj|=z@Z3UK(MO; z`V9dvlxCnM;Uglq77rGd=-!92P`i|52(Lg;NMT?~cd+yT8tP@%+b(7?6+l>37o)gC zeMB>-!cUPVC(J@(zGXB$yE_G>i^U=9&w>^wiR|;-#sNM;KHeJ6Vrr>@SiM@f!)fwP z&Uk1)(ufIIcg(&ve@t~G`@xoSC@8(h;?D>J-_GW%t~kP`n$`Oo(6x0%i5_KGcMh(P z;DB{_Q^@x)m*a^6bJH2_^u7I^?X*4m*?WLA%ay|}7of;DH^SH1m|4|so{l#-HP9UP zUAB`*Gd@qw?fvPffAVr~68vZEO}5y%`e;`ZZ?RJ)zsy2f0%}CBsp92Nsc6h<xA@q| z%#$cMUws5a6D3rINQtz0f|F(5b~fp~Y|*Bvb`jKx8?gMn(04XmHr&^TAV^n+doFM+ zQ?(Pm-J3l7jw>`C9GHiGtS4b>mNo{<)Y@jf*p{|_N_oZB?JddS?^hj!XO&<)<J}9X z%i-RlrhbjMw5vNa`bbkh+5Ny}{j}c<JQ2dGQg^jYxIE7)44T<w!3vu$SP7RLY-GB< znf(>XeEn=m(v@M~oe`1&TVUmrj5k(!pH8`1XeI~BKU2Ycdw%qh{*PoK7TvkVb*$xI z#3dPa^$|k;nWdjeQSDol7O=NYI(b%5n<Do+i`F~-JWjuy50(V(F%P&wEYEp)$E9c5 zFpMes2iX2vB&^_RlO4#=KNKmuuSr$&j>x|o+0V9MenS5@5rPaUwi^0Fgy6yaD-mL7 z_8$#Hqmq@)kN?V7mW~}F-bf4uKD=lmZ$f2HL8U;2fT)tu;h-8*#_zh!+7I`ujN~G# zw!~(yl8gJzzsK%be-SGgYm{P-mSc<q=TM`7Dg0%jz)VVOVXE#F4`HM$XMB~?dON63 z9`b5j;6=gWJNoXS!Rqmm?EZSh2`}AaN-Cr}g^mW?b_5`n6Ux_`BE6UQ2vV}40{xiM z%~FbN42epZw9oRhvZ^z}fq6Ng2NXT_7ltQNLapgG8#{JYKs21xwQp)~2!<~uCc)%Y z?3Cdvbn3fwHr==hB~g=`1Jef8^2l^ay&jwwm*1aSN_Sy_fz4mvpI#Wc@sqM&T}RjK zLpr)UP&M$mxulUnZFPcvllZHM7$n=}JP(1*!qLVF3RZo7es-o*KO3+&Jv5%C^fw-J z`AL+^VbuY(i!;oNCo%?U%JozNl;cklsS!@8hw7OTl!rm-lSqE8@SA~nP~V3T{}vG< zu*_p7jvV#TQFJQ2GeLelISgl`i{g}i*-(0EQhw|UwR51IVIf6V9{;-#Y;84+-m+C+ z)i!9#Y|m7q`6O>!*xDPur`b5V6b$no?vJdjoKolvZxRa=;j54Vq186`@mRw``%oUp zO8Su%X&+Tv*;G$}wr}l+=2CZow+Uu1$#%BhK>mPg9fz*RW+%tChDkfGdC@QPtDC}( zf95`|PoS`1xqTn}B8;jfJSP*%W=KQO;aIFJ(#rlRJX8n>yFFEX%9F^jZ^s%&0Q<*4 zj8&IaTm&q1zt=3VWI_EPsd*DUgwwD1TDJ%S&3XNBw|y<)_+!o4X?EZ353*>$1MsZ- za%91Sk3>cqVD!{srvb@dqB;Z-r0GgnLWDhyT@l1>WvnLR$#r1;F+S&xrI)SS!Pn&Z z`csv!Tg$u9Y0~5Q@!EHD+2f6X8PgU?Gu-_WNV5-2U8!7wqU!_;=U<zF`0<Un!ToL0 z?F41@h~mJ1w-vc<grnq9)KxWZ1;LhwoWG?5+kcUnka38N;gVR_+RFU0abtlGcILYL zMbOMxRY?0z$kVAR+s+ZKitOYVk`TzbbNY4+dWEbvAYH^ocWeDX5N+dcA&?ESrNol+ zz|$!}2Q8gGa?|dYcd6qKIF&1v+4RLNR`7+#={4)kK9Pys4Y@+F=M3a}n$=I^2j|*> z=cew=ykN`4d&{$bUA=-~z6c1nB!F$z`@fN=+fwZM|8OlUnEzh7*cdn%{rwL$+^DK% zvn~w(Q@ZTr>(_vVJ&ExG{q%Z>NhExE>qSAes&W#kEOZxfp+9p}tMP17e?kEu_i*dC zCp`XA1E&osf29SEu0rio7q+s<Xj@tM=0o5S>1&M7Oj}vtFurW=q-1mu?(eTt!_9+; zG!=q%EW>zRBcW$XCQv_(J?Q)nNC_r{yw9ldV7w)oNIC5N=F<B~LNaq530@uWlkw>f z$uL$<IoQMKU-~`%IAhp;zj0~Yn6-^Pb4+K-)cw~O=m~hdbaI(fTAH|q{Et6sK%OEm z9HB=RsqM;ypCev{4)<*#)A`$*Tiu2=C$kQ9MrLrwXQ#jNI@g<0mP+P(N0yEC^3_nR zLi**Gb(hn(-HeWROV;0&Ni_2zwd&2yUapbi{>t|F?+vV#ef(n^TG$3&iQ)8ii^5fX z_vZBLP2dRV$Az1%RjX&*Hl4QeL~MDY@THL=mb%S~?wrJS9v4$kvdwFhKKh-lwl8sV z;QbtWNcr=55sSBUtIsOz-sS!xf0h1SYisk92&tJ(5AXAfK-Wk>bN{jTDd?!)%{%xc z)P-bjgz0U1=P)lqI_|JlSL2gm&kypVJbD5#^*Lfm=>VI>>A7=BIbdO{^OU04jfq!Z z-cx!J(=k!x6(xM9(7r2QUC)Do+H&#-S<nznM3E(KundGX-cne2=FI}gSWcQ;9=)f6 zoaNFA%MM9TTtg-!AwApQQNxe^b=?gEf5f9JFnMC&4kAKPFcRSt><hM0jD-hs0Mg%T zpz5TSn_$Tv@EhCp)<}fg;c_-3r>+8>T1Ur8<VwZTco5j<z2LE04B9gq`V)H9iOBY+ z(-NWbWM@q^2!d<kNBZVy*9@Z_EluKxW>Uy@X@9(7I)KuSbfH6@?u6LQ2ivGy_83%Z zb^VoM4x%W4Yd3(|C7H|~+mPIVlMuUeq!3~-^Y#Sc>13)GOL1XA0Tkx<U#=s*;kB(e zF3O@q<`n=-<(F2$Y2`^FG*DO0IT)rbZVkws4p-dL7;wF)Sw<;v8=D+L7&Dw8)=)XO zPo?+xW(5k64mw_FtLgEKo^N^pc|3+9dO#44d*{~bm1pu-$V4u9pmDIgw$a4$KD!*g z9g^GT68<D5dn%VgF>?mzV4r1x62^@YT?;~~<BWo4FP_G8cfHCCcpq&r7J;lA3U$To z51j=i8)uGuy0t;t#b_klNJH0RE8y&!0jjoYT(Dcm8=J6ebP&|vc=oLf$`e!;xUO0~ zj;R=bncam4bQr>$9)P5Z%%0-}-wOLQi`DSWwqCFBhue%i%>VXhF5^^}NEtfDj!E4e znMlAxkc@?z6Rs*{UT~zY2r-cC{}yFJagwuERg^qQbx>x*2>NAFB3jANJuyQlC`(D2 zI3Ye=__-G4D@?GsSAg#T<?kaK_foJW;~-lk!aU)CiY_D<pQRYCISrnuJ)S|HPVH!F zGvq=yk22=oZwq|WDZ5VX&2z#=B#f1c9bQj7+aOKRNImpHs6r#Ps3eQ>n;jN>?2x&t z;+!f@fW+Bw6kB?$k&^mew$u4U`YKs*ZwTYJAxN)Olx*6Z#9M-LkA=Zql8yS4=bp!E zG`=`tMz9Xn=ZudWQ_0TDhN)q|0N&bghnyUpt>EVibO+-}=&ckPaqYuGS7yf{TM5_5 zhUJ(r2YLErmU4H!h&A&*j;Gx#hQh`WGjpCff_CE}2N7{5sgo4eYTNNryN|Ga*|W}h zEL`WazcArBv=pynwvr;1hSnw&+(0qOU;3i)2aP)|{vpR?uFlYz+>ZVxG}90P3m?O* zF3~3~M|sSBV+(LVIaCG(v&ST)Y9Il|euY2lYg~I?Q&~PD5`7++NnE-ni#(jcYKanw z-=EZTDviec?%iZQA22a6G?@AGAu68oRs|27$a-54_zVSO452A*t}5#Q9TqmRg{`E` z=i(fykiw+jP6e8Aje(88ueiC3CL_<#pWs?rsc3HaiYeX3X~uEte0kXTOZc;NA=Fmx zUKtQ8Fvl_mP_NI*78VY@T_-dXx>6189kR8KYFZ09hWjYZkP5=BZEjzzC;SNZgiEr# z^DTFz<^3P*A&2RQ&@MU@9af=)QiQ^v6dov7)K%{`AL$uxn@V+oemoK%?-w&5voT*h z|6N)p@2sse{4~+BKOfqEX{H^mtXy>*Ozn;I49%=f{-Ip<|1YXphGKHEGJ<ZDTwF?W zbV5R9WP)adj#6%Bua61`FFPe339mXv6~@3G@PDKS2{eI;T|XMBSMYz;Ng3JOTig8e zlb%W(3j6f%SD#dz0`4dd4s{Jtj<Zy5Ai?QWSNI#UG4x{OFVD?j79_g-&E=m@pRS&> zLG$C=2JbaI4eR}S%{FPl{a>YAo!mTEUyac+)9?MIi{4pejj4<K=e1_=oje<t*UihT zALl=_2D7*_!P`>6upM32w}pTPy#s2nS#b5_QV(hk80>K_tD{1Y^g!8(QK4R_ulR{6 zMoqeN@Y({>Q@?}h$GIdZ5lNlQQw@r0QiafsMpEQ@(smlL|3Ak5DN53=YZiv9x?I&| z+qP}n?y_xm*|u%l=yI2B+qU)P^SpbHf4uj>zW;;Fvm7I@HP%|!oDnl3f{W&aNi$iM zF&Cu48mp)=vyG-uhgTH$y06&VHf-(ZO!VyDHzL&L4$i29>e}t5CjxZ+p$3^Z7+`0$ zOk_vZHEmXcl&H<c52>s+X|`61L`tm>ON8Ob!K!oMa~jH|k=e+ei)+Bwf;;Y5oJ=Cb znP`x7q7DU>%J!nX{D_WJqr8=xpvR-C2`VF`{P+IA0<N6LC6pS_0xBeb(G+wFmsKkY z1dzc+S|@Hi7PzK~Kh|*oW9S{$;zPA#PA)qmTS4ORLxD<!5ixDp!i&C|zUQ&6BpWhf z^+huhDFd%O)={{g^a6r93jfRkb2hZ%H=$zax`G)(CV4&I2UJaEAU#Feq}|9-X~=6z zI9JpI+J*fvZ1s@WCqtbo;I@Q=EZS8@!H*9PQaO*5rLIa%w@)*U_qPBSslYB3E-?cu zvT#2?>JCOx>7jiCtTB)nsIj$Z16(p7#jXWNqfU`jfsoN(9pz07MO=(<63mUa0^P{I z!Lql)Ie5QE(;<v~Pz{v$m!?oGiS!^X>~CObSZDe%b-Mqe)>SC{5q6HRFc?+%d9C;J z6X`D%;yfoa&cZMAb=@ZV&iBvF@p)IbwJr9#AlN9mw`(9gpC7T?#7nLoXnu`}lE|<< zKYnuAO|5NhPG`g;d%Xo{mQD>05BlX^En#+Er#f~A=tcdL=Ny%4GPeTke<c9d`7cP; ze{2zrZ0!GY0w&d_?baAjx{qt!mFNjftb;8=C8|}bbyT667xPLXHRasNmT<Tmvu$3l zk2y#LqzOXtGLWC1yYv7$*LJ^PT4WFz3;kdE8R!h2HP$60GW^p+z|%0rv}t3YGL<;i zs`l;vmrv1EXlkK1+0cT>{)8KM3nBKgNUZY0?sa9@`cu&$Ind+DMMObhUv!EkaGEKi zvhMM44%8oJfqso#2M6$$`>s%_GJTJzQKp1m*y6x#)1`$nnPbe&t6AUu1Z`dK4);uY z38C-9rMYgABcU<Ti<N>t%Oky`42G$SiON|jXtIf#D}&*gYwDE<E6x=2#&Q&ot;J5K z=HW_)VCoF%GDA0~Yqf=*&AGCh*vtjh29@ve2%|u96?eZz%g*Isg82^m*h!4Gdsiwq zxB|WwD=6WD`+x(UN8jlTTaZXQbMXZ;2v8R=G@NwC8+fH@&A5v}z}u_SIFhu5#K6H| zcC$lHs`2=$qgAj9ud;?O8Lxk0J9thocjM__BzvnMxn*z4D*G<lb$&Wn-qe6Htsfn+ zvuqoOa!21U^96o3ZTsPM`I&@(=g(9K*l=RyVG%A$(AM~C%@jBBh)YnFZi3S`t0C`2 z%ci#(u*COlcBY#jHC9#{%;ALd>&^BVvk%8IFTpe@ja8uA#E@DvoCr^2yhC=~Am&{` zC@SGF-&LQ2bmiN3c7=%dUWLqQ@SFSC#>%|K2tuD>NrFiRGZha3qgA1hpXTHi{{C9# zq;2_Rd#Crp={(P8TVea-s%yNkld8PSyMISx<e9?AJ^tdE<DlgK73uQ*?JrOvDK-C7 zycXx5!W91aX>~WicC`o0V^aLT-xdGat`@fTF8}Rc7S(hjvlvhS_GkyQw5L2x3K4ZM z()lKN&M-E)B7bAgZPU^u5;R@k&VMJGJL3#vzST8#mvJ6%WoEtSM<>uShQ@@8_pixO z3Acc&C_3gHDEGDduJI{20=TBAf&&;xg(^pwq>mtU;Zgd&qP(pI2@+msaCK-r(JuA! zP;&2?wzZY_Bg`Z*-6;{`(M{U$ZRNXNvT#V<*XMqoc<pQMM~AUMVA<K#p#Z&AIHe8I z6wzMRUku}Fg+3RBOEU;$9bMs@m0egvg5+Jo$2s8fAHT&$*(nJI>M|!oz@?r4MnWSg zNsS)sYpQg5ry|%a;CM3gwKRl?##{*SNVZE0Wy~2kjZ9fjDdjd?nCr10T;je)iLb3G z!zKMiBBT03W=eV&Yuq*;4oWwwt&lc8ENfdp;LhMJ7N;So8>-@vPSZgbT{mj;N8FR~ zi^J;$!CS8ihy<F4RbJB&j@7C!tz%<FcA}_o&wq5k;xKMPCwO?Tn|U5Zt!7zJjG<um z;7dv9Vz~*ytHwc1OO8)CWa6f*ixhRupE2ca)zb4-H|q0JBX+g8==hedZ+`L;g&O!X zc8|poVd|n4=W(&i9F&~k<EAu6ps;IE$6RP+`nWfD#$>_YiLvP|nhVp7pA+3*q;b0W zo*t+lJh2DAy)}Q6V^_M6bZz0ZM2z9Q5yH!qy4eVB@K9}DlUKH>q6Lc!ZlZt6S)3d0 zk1({oFsxdn+V30(zQn;Oo`GKmgTiia<8Yj8%?+j{j0+DT6M_bNxjTP6)-}C`>MKqN z8U~HKSl_vCm*pf0ML~)>6|yT%)2wrdJvyj;*<pR_yW`b%&inO`nUcx2ZfisUhE@V# z4gPKO+BpJzXPiy`Yc+bQA#1e(@S%NIhjf$#pak?_!Z7|wkU<A^{&@XF#@a1|V+rvG z)=%5&dS5G(TBBs4Ep?or(i!Z;lRRCBzk${Mpo0M%%2dc3S!~XV*2_a0gCe<TH{7%k zBj)W!pC4ilECxj*llq0U#n6H39YvN!SiN&l{>bt=-oz>Al+q?(J*`QSy2C8g5Lba0 zUA8IgWLz5Bzas>46RG~h;}Yc%W&w}&7O&TxQ%F+A5nZNs?;PvpHRA|i&nh3*3GIV7 zdm%{<6P)7A{?-$>FLp&4=T*jLl1y+xK}C^-y810;7vgP-IOD4=IPu(;OKx<(E>}}6 z7)}jDXaMjFpk%DxBM^gFM(pvc<-Y`GV#fN^SL=s^Qh`}u-O`i;NP`Szpp;<*R8nSE zdVlFR)sOg@41l5V4)XP*A~jF^0<NiH(|*T<4sw(+2uHbt`(?qbudH{IvpRpid3EVC z5uyuO+6P)rv2E!(F_0#YO_ByGjO+<rRnTRfcaWTbOJz^tkzFs6v`{rD<MHjv!<`e; zq}zXg1%wB!a?3a*4K7v!;T0Pex2~3&$#A^_E40AmV-^QS9gfO!I!}6^^LdgTUQf!v z?z~fkYpynRWuqVHRK_ivrmtj}zY(2&pH|?BdBM8JwR|iPtThvUTCsj=bdZMj(5+ZM zT6MsCxynJTJk6UK4{E!)XfoaZi>}j^=i7GF*h=8E)0P~w^`Z6HO>H1Ltt5&~kXGq8 zv!if_MS);}GZjnj?;$zXl6F69IZB>!<FLUg{D&O4fS;FhYTo==E~TZho`o8+Q_v*0 z@t@$hCsQ#i;x~YnL&f{v1MSOK^o3k=EQVSm%z_Xkq%}7`)jwAT3q)&A_$P(WlNPR@ zZ^0Rw?KpRy_bmL@VPw|3IY`~evkQ>bTR7T|5bl#-tMO<7UtYkM{&h-U;BeIb<6+gk zshMZa>1*rhYo*;isbz*9#RK!Pf#*`oN%77uc?n1~&W-u^M8q*(fQ^X$=F?Wy)VHAj ze6Cz+0s#|2aB&BZM9=8wEYCkn7=0=yW&yylvI0QU|8}e#4UC*E>}>xFP-m;j+GPQb z)jhS?n2EAtixM5JWr0{V)nkH`<eGGo5IFc!e0{l1g0+t~1a?Cmc8cs1LV(Bhq&wdQ zr$6?35jb>l{tQjyFG=LAURa{4>T20PzmBMw6}#%KNQsNb4_B3LRG7OoyBP|at3~XE z{!*JTf_I7q>SwXcL;f<Yr~uQ7+Hs9mpwxPhme*%zMVh}+Vp=zUNUEB}Dn`XtKy5|4 zholQu)E{A9RiQjvqQnCe%vW8Eu-E)iYgtVDQKQ|O2DzK^_nQf~A77H1unb~BsGcWC zx;=jywtWYcYgTs;OZ$!9qJH7hcX>kE(>?~XHtnR3|HEpgD<c{z;DGxw)*=P^k0lky zGaevpP?%x_%+tAP22&eMus1RVJ3cm~4k|S`ptg<!W_R2I2k5i=5!>F56HUSNHDGYZ zS^1eKI2QeqIfNntuCe7ETj70>_@!$x=T7Zw<O?{SU<42`;NyzgdNR+5nL-l(l136K zlQu&O=bL1;FhPY2-#BMgL!oKxoSRZuWSEq*vb?fs1)Ye5|CEYU&Y}wJTQVe>jFfX0 zFk|<uKoW{woC-mPb<x_aZ8mhKD#3?iH;?VoR~r}OSH0N+k7U-HNoT0Hq#Pq}>D+ao zdO~{jnXO<M9x$ID_w%lvfg%Ed>M1FzLBxXdPwTSQ)!xSA#&J!XXW_(*S%uU>xGnOf zHJ6TC)C*D`e`Q~dRkh`U>;2xV^r_A2zS^eI#g*d(<^u4zQw0b<q^NiuA1(wfVXGNo ziR7`!5uqya5#U>mJ?%b}GoN$iA^pvQyhqYe!c5<Bi~0=o5!#x5J$q+;CplyCo+qdZ zH{S`RLM_&bOF1q+;*<d5t(*5^%t)VxS1LL}+*EjnVgq6-<a5n|Gb?FrkLh3!GEFUd z16=p$&%x49TU;!?4rx>VBrDUUL^2-G@92BPK&flS`%<^x#1}|Go1{??S(1jYx&^(@ zT3+0@vgx+v12-|n3K$zE5~R6`mCKTF=VWGiL^f%8>8a+w4B=uoYT7qqj#%#I$`&;8 zwD<gvS*SY|I~X-s3Xm<yK{tIe+vHhbGefzIas^WzIG3f<6P{&9Y2<*v|6!@-h{)*$ z0l<|V(En93Z0c<Pe;Dy%4Zyr48-nk$o?sQc6F5Emd1gJDd1nh`9GT!gEXWVx;?qWA z5hB$DslCs~PU1&F&7_Ma4||Et*x~oR+ZatnDu7fg)3{Q#@1lF+AjahDp2pziS?=oC z_J%L<*EN03=LN{LQ}<Ng^w(I>W8pM`pjp2>9nA^cfvFmxdYF*uI_PLhPvCiO#ICMF zs>q?;U3D!qy*E*1lSzGN!uTApy7HAa=9Hh(>gr{&q-8~~JKFO{bMH{x1GWaaY0NOZ zJG#HTJcJ;njff{XP3W><%lb^9^W~JDgR!W6tX`CG=-f7$o626PBzbjw=j`hC>Ct~- zd~Nz8X;;%j_jxhi=}S*v!}m5CTAP}SHldMitbI5BN^BlDF(+e-`uja(Qg?b9Z3`a| z_Wg_|lMt4LJ-}yK{cy402xPX<2*%CNUuz-Iq#~StZ3c&)uSGGYXb9DTL7uSfxd19m z;vyovM3lQ3KY68ftLF5h25Wa7pw9xR|HvRMRoUxIuq>ECUR|f=SRmRPpQnt?E}mPr zz#nUXYsY2!QF?GrVW;<LW^E?#T$<N+C2#qhwsq^NO$_={o-xHT4AA~|O@L<TPhar1 zA^Zhy?ZHwbVqe+O8j2>~qBg=@?gXesr)Qg!P1NcG_<_c0Kx~<2f_hfhf0M4_UqGGd zv=eA|Fc?|=D(O?xQUcB92CaVrJ}f~Q)wRq({YaOY@y?skYl66&$_+{;<`tw<0VZ6u zlqd3T5NF6^)Z#W}qZUx@bM;z}o-I%dqy$-w5##x^Y2-=jGYeLg2B%yY@(5XB(;MBk zh-d-JE}7beNukTmoG*U91%BXc472XxT5sd<H;Yf87sjM0*>(ca`Blqv6Mqa`17EIL zEi7hWdgkots7raLOiXzIE@m@Fa<<6W*s$zx5M9yamt@Cwf`%-02uGLUBsZm94JVU4 zim3e9{leg^_!iQ1qD(F9z${jL<GB_!e+wydV_RsSNsT9(RrR{N7iNUj2}w!{i5;2e zPuU^7>J>Kjp~U%aNaRA}74yPKbknQHh!6XTw0KUs^g#&%Hz`o%rhh(h)HW;#Zhe;E z0~Z^&Y}n@DruRTvs%kq}uD|vp`bA@3=jg`dua*Jh0S<RdQsH^ScSh!f`}$v2N^0z1 zo_w#ZCxli(k%mrGST?In_|2}0hiu$%g`EaJZp#`b5dmsC6&Ws7TdqTU78CZhYhwln zy3EM|cUU49FVZCbh6KythT6K$Kg4BZV>o97ZV$Wd{ZyqXhr?4J3M=XtLJdfET$xa5 zjdmxZ@6ogL$T-wQ2Gr^-)&36Qhj6SU^5&2PY>mtn)d(RC{cu)9AluY`?)@8#G?*}H z)|`3^f(e-)7j|SzGn+D`8^LfL&NoJpjaORQ^uR0^OPow&G_On9!#ohqxw~A^y;3V6 zCdx00xD01oEZp2`at13v-Z?*Xjz`w@=T3?ziG@gS*Z+diJ-H~>uW6F&s<-MtE-!s- z9+z3;=Bl--O|>Nip1*~;-a|c(oFz3*%)}?&DTHq%nr?U!Uua{qc1^_N5~E>A#ujub zYznG*GL?>Qu-t6dDMQg?o*iVD>=NpMf5s-bAd86dKXP6b%>&!*&p*<2jWf@GUVfOI zAT8#5pOR?}+72U2>!o5kHCjvxdh4exwxlI11RZe>*kG@;c&tFeFnS{1691@G3%w=r znBC}qm=TyCI#hFMY-47N3$#f!di1sScehLu?!*JhtVs#Gu{E+yyNL)zzbJ@A_Z!#x z;&}X>EUWWCG`wC|=temA_;`AE=Nj@I`0Q^Wx-o6Gk2~qk&b&Ap<;DnK$A5A&Avovg zf!M?gc9dK9L*==K6-fSBj(>q&RIZ%SLkoZQ%A26ZnJM35PMO}l+KT7xe(@)sDMIp4 zb~#1vL``y6E~KO<jdW>*Is^i-A9@ihp`lDcY@{zZJ?)qNuia{LvA4eiA2Yw>p%RV$ z6b?Z17b6>Uof0iego){uNx@O?=b@Yj4`j*8inCF+&73)><NQe(iH0C*7Z$L~%IW+5 z-+1s4WL!Dce}uh&nvUxK-Q$~?*qS(67}@_Hi_Yi-S%6RK&yed^RIZM=VDEZi{oqYE z3}^0S#W|I$zk9KoR}xR>#|yF&VoO8lFz+74>OymvVBPga&jMpu#|FWA5DTOwH$|Dr zZj^rvFO}Wrjh!6>+IC3@SGQK#x#0A$=jM9v5dZ2xFGuqD+q+Wavq)ju;7U~lkG=n} zdb;ukR+okp1fWr*8jMp2Vn&MC`xN{}C%eWHEwFa*Djsti&3`;7KZcpT4fU|B45#}; zo_SomuS#YIvZGIotYlah0Rh<Dqx{a}2+|cdNR7ngFWcX|o+bDwHe@;!IDSQ+VahoR zBox%jQL~18T2lhkR}-K5W*kL33OYBkTQ%a)fJnC?SlHWrNR!ZU>9+c^!oN>g+@x<{ z|9ge4NoGT`04qxlSY@JrSz&;$4*>9U0T@#BGav+AdqnV-D7*u4zJL~>+bKzz&m)ZB zkB026cd8{s>}<InWg0A@;OsDC)%LYvAoUL#U+hw4Yh96QEe_BYK<R1?nNmB2M3ql) zoT5k_K&(K4J9EkAu-pz+AW*i@7O?V;U{{vIat;}NW;+_{-R7?psLepTBrM&*$sL=v zpceWWV8A6%c~oyAZ{NU68+l-VK=}Nl$<C_wK+Xp|7c*cykNE%nZ00rwM*n%ZX5~I9 z5Jo^!5H+}GIl^y61T?KC&lAz#3i8D?tDCEAY?+z~Dmpz|)(w{}V6>1&38M#xljGyY zK-0FFniQkG9Y~x;#re>`l%>0VXf7J#Y@CJ+bSX#~Lvv18kxRl1?yA1~fpirrl0a_} z<z}xs<!vhRDAlXJ>?v1`uM9Dz$Qj^hqG|5WIGU3iM46{V!S0xRF@#3^6tZDRMFX)? zkY2?;f-4JZuCpR#PMPxBdHn-91l=xlu0-9#;7>j`l18&$Kuo{%7Er!G+GqT8fEsQY z9SVdWnnrMc*gjD9$7zzViTdbQf24EA!$;w47{r6^UoBKrh#7avbD8T6!}<dRk+*hs z<5dUU-a}B+Shr`%PQx;quh_DlQ-gM-r6CcDhsNye^sb02%pzAaxhwnv-1Gu3C&wmS zEfv1~>dnr^YM5PnbH(Ir?o*Z#JAA&eZNb7kAlR@O*Wox;pPT}(dLtP*mw7w<Hs{Xw zpEUgyS{O3ieDM#Tm)f8DsLcrF5>7FsUyTBY1Bv%Nk8hKLwQf<v*^A%~#BE-d^Z)Qv zVZ|c=1Tq2vt^WH213;Jae@o3v4QV?-KmtIQjj%55n*Ua}4h9X<DG+DvpLfb7F{k@e zIIp;VA_P!tT3&p;!BI*T7>_0=qJc%c=}SNGaBTk>G4LQvCZs6^Gt0!R)OBg6Op*~e zH$9x;!Acpht~8cwlF8|1QYTr&UL>i(<BdOJ9UK_A416bVh*ej@SouxmM3FXP*)Q!7 zHiCMXWy>LQkU=`)*cdfJJO9!|^D{BvE^I^5NFY3nr-0;x4o3Rho#N)*%wMDtbA4@c z+83hume<sfdh}TSU{6@2+qW|f@#E*kPD=i-F|%G)9K^x0pFyal?+=Z3Bl~xr?F8yf z)8!JZv)r0@Nl92?tH8=ZWF<ve6b!{kYD6btkm0>oX|C5nj$)5ECaSsg_tv_rEq9)3 zb}9Vbn(FRVY!D!xT{(lg$Gj&A*s^>ccs5C;BERBo(e~+)P}_XlN<xT=L97nLx}|`d zPb^m{Z%&rzb<0sx?JHQ@Ur+C@I-#i)0Joykz}Yw-@No^RW%~4SjGrw+qcSM8ITaXx zR>7#WjSqFPzbGy^H8vQbBy=W|HUgPUT^TGOP+F>Pg!&h{tTpZ^6U#V3H1hRwaO4<P zbO^Qc$l7gkzY|rg`{Y`O3G|qnN&x17h-7t**spD*l6|zU=%hbZffV<A1Z`Sn#GGvv zasux^aq<Ozv<~jX2BA3*Ajq52+7K}ubam?S(bcB3-qPz`;oGL@y;mr&Bw#Qnr*m}A z^tK|dO4|Hr2t%O@z!Kg{u;WL@Zja1K`Z=>H#u{)DiG0ApfT$Ic1J}BwtPF;*R41x) z5b#L5U)Jq-G}$0Dky6ct=ZQA<<L`M@qt6o(%R2~8Ah8Q{tOY0X&dG2XOPH2^gi9Z{ zn@HzYGFkF;K{AKHK7%DCkNIacDS=RdE$%t8y1{j8`Ur}aTh2j|=xj!+@({b@wFYs+ zA<O(1CT!@+6UTRs5qgnkp2#}T86;hOoslWoh-PG{u{hX#JG!Cd!I8Se%tJ?Qwbw7u z#Q}&2W#k3hOg^1t*#um&{*(&>j|{2t;*j#D1`ZZB?mR=-A#K_nPB{M@0=R)Duv?lX z6plX;$z)y(964@|0a?hyQL$fXij^P+n|cuT`=d}&YYic{;4X?zJgV=w1sf1154ftU z8hiuY6rya4x7rM&whQ-<Qi92|)dqqx&(j25K&>XfXYhTyn8&GZcn;SN^28B&T*{b{ z-+|YYOf69bPoHK4yYxwmITPm_{){lhm<4xmG1Fv-c~WQ8C_bL)U&<`BPiVkm;%Oug zm-XInHmX=`$CJYqf;jGr%<@dl$10;_feV>7{KOi<1mBxHB1OpAkn>TXS;zb&Dus?{ zSDG=l+=8Nv6dWI(ZIv9MxF)UShQuSS)az$<yq^#>Hf5j1`NMVv-?ILg%~RZ{O~aFW zV8L!ypPB_x1zwQ>9lWX0ezwr0wXR%`M>aOZP?#9(_%j1oeEF8y`2p`irRTWzJwohE zZ$0(`Ia|x?K|s-YQlINve|6wSpOZOpaHqY6uYS*H!=K+i@%L}_b_Tuy@qL}P<v<-j zY~ACkB_6p74-o|7rw*@!{``XDnh9@sqNPIx*|3VN+z5;m3XIFfFE&r-sBD7P4RcV} z`v&C1RWP^Mv%x7*ifIxqtJH6uuci(uHR?01-*PW0doE8`<?-#uO<TYF(4d1`8!@z( zXS^x1^8doax1wK-4qqVSR8*QgTW0t%37?b872tbn+8>c}Tei-X7e4k}#(;n0$u+nl zwc&MLv?#av8XbdRdtR0S<@cWN6l90tMI+j#2)=R}na*6`vom#nf6qJs<y%u?@px;w z+(Fm>$Z%7luA(S~P#`?yM?f%=dp3FzWk|@~UZuD>4zVXP8$V0KIq&@j84nfqT9vad zsvzo<vkoz37H5F}8**qO1V80ku^r##8S83|jmzv`H$W#pE`r!^FOnsc+m?d8!`+y~ z?wa7|)9h6HJcQ>!cM(->QrB3=*_{-A>%=&IuDPwZrL#bMcqK>Emu>USzoINDZ_15k z3rOpGdbejs@1v;6sIYA>BI8<9WF<VW_(uNU<<VT0_zoSQgwO#HP5<J4W?^GyZv0=6 z-bLv@MFh&9W9lsj7zg@*&|tJ?0?0wM*?A<XweFP}7D^9zmru_|$<|^iC+KvKfr-qO zPePFpf&LV17ZRDDzjv<mKOt~(@x91*XFYK{kK2RFUFB+Vq|!7Eq3%J}cbXIH!u$ug ziq^4x$PYHj1%zJyo&;<`*1Kjvrqq_PcL~i_h14q&6J*0;={wgDIh%sABiH3%mz}?X z4wJ+gKpFeionof9pJALPNQZS2vttO4n7Kl$wFzm=m)0gKOXZ|4WdbEKs<a|q#dN30 zFA+vbSG17)RH0|FGjk1n6&>w4O%f1y*V)C_345RK_x#q=o=lbulub{qv)Lhi(WUU1 z77T&b+AEVPsw8TkB#BpdvEdTTdRjr>fb(v(!Q({Vg7X!k^q?$Adih5^S_@Z&VF#$y zLIIzDA)&2IJlyOY|7+~d8^s70$bbO){(Df$B24512>lgQGT0nvDjw`5>>A<rS{Y$B z#`Gv)swFQ>iNRsBI7*~`<VU5|m|c-cvf;@~ql1q_B!qAEAT#->2(dWHXK3_W;BzQ) zMLnGQ#uX${W2jS1iMO2P7tWKVN!~yFb<Qi1jUNGjR}Qd8{snZlwy-rZGB7g#pG?7c zc`Fb?gu!c{2u|DHtKtVD5(SEfIsVkG{ss)Ujh`+qipm|I52~#st4ByuypYWMzk5pO z7`X5WW&AC!!NcYyS|BpkpI!HE1gIR{3*w%Z850`k?A-^ZM>O6qIs*H|_g&{eHBtxG z5qPK9Lh9jIA247wt5`6b90e5r;yBK80UIkbq7=5|X8(<E(jm(DTXyaV#*Qt45g++O z5xiJk>9U?`0Nw1C<ue?Cj_Ct9Q;$fmo-^=Tv1|GUdB;3i@)&&FJhVO$(Kqs&8u#?0 z^%3GNE{Rt2Qok|eEM;I*ia~-pl;O^*gRVc}R~+~8gv|RtJas69l#`MHz1=SWhx;$b z%i7Ke5V`lC8ogRgJ2sO8#phUEBLlr;EQV~<xJ1;3lr7(;=$Dv(JXr<NI52WPRwmX6 z%lr8<dM{bx>XU)T?)!z)Ar8J1SbxP<DuD?O0vd4uF8Lu11XN5T4r1IP*gk5SMa6T4 za{R1t=i&T3kPITBx%>^#L_-^vpGR=}QDha0S2%PzGtha#7>1}ZkkCit5DOZSqK?EU zIhAYHkfjF|?lJTqhA$8YwRw@?wA_gK#S>TebK`Q9c1}ZGXxg;dB>&i6T=*JZh%j+5 z=-uuqiDX5{b@TBE5S1SkTX1BgEk)R?cL`<8I@19RVLWlOW9fa6KANsYfwP!FUw01W z?acSB_ABO)98C7sJK6roAlI;7Gc(`o0BB-_%qVkX)z~occP2|o_MzvZR*Jpx&Pp#g z2s!u`_KW_`ZxDVAjz7(HhjuHZhF}Gfc4pRz+?g`gj&_L<nSWYGLv!ZW@y-WlLXNFJ zRzE!6CbwB0zgm2t1I1-q3RH9OGqV@hc1}%MH$xN$vh??@NPsR_lXXuNt(I@m5wj+H zrV8scHDq>K*7(3Pq*`=oK1OcLyIDj?kVL*rt<VA(9(uqHTI)Quz%yT)_P-2g-4iGW zioZ8z(()QWfsdm)`)V;N_q{LuGc&P=CF?kf+vWlXqZNbUqjHmu1hTpB%`xTS{jcvP z(g_PrY=P9G_HbWFZ1e(q_$m|2c_qcygeFdARXZm~EUYa$=NM;T9O)#x*Ds@Br3eHc z2uj1pRj8;|V7B1ekmyws*|=q5#s5mn%Qp^H#<hSfX<tHG&!64vOb-{!n_*g;m8o4? zpy8CJIc0@;r-Z@plKpfYyy?whygO<wii)(z{S4Hhl~bfsDVhK?h}kZyH_=U*GKYLj zL)?pKg|t3lC7pFP=0p<n)ap^nVb`(?NgP|DZtdk6Qqo!HBT{m^OhyV3A!bR<py(6) z#Td+F$tPVa?1bnWY^<9C<Q|$pGCz2K!0V1SS<HQQPwpY<A9|F*1T37LlBtDAU3g4h z>`gc1#W>SgH_`me|GP&astt`+Vn4C+LNoM66>3bm+pH*e!fyPBaj3qWqIM8_;A^aw zOqCXi25ZB`$95BDg~nPUBIhE@Q-@yu_2sKt`I?ac?E{TTZC7?{_g8?_BMp{#E-yvD znBmt5ex2z@84yt0kG=2kK^M9y5IL<|SGte-Eorh9X{dFm0+f#MH?1$d|NYwJ&b;+N z1_J_u1O#dR%PkIQy#Fs|)2y-Ww8nwpb6mRu1<%Ny)Jc{;1v_uN)_JO(=uEfK#egZs z##A<vAfG^ZrMIo~3K4=}2{{@Vxb((%Y3bHX?Y7%zk{_&rJ>we3#D49(i(kFN#%yhZ z{(}#zns2fhk-sV8Xk$&VC^1t`oTQu$PH~Ywb6w8b`3IeRaE2hO!D#e?^;&3ce+QqJ zTDva~$9yQnm+z43-*<*FCCTr*r0?OI$sQ^5V56^~w4U@Nnxg0Da$(NIHXj*kxVX&% zV<MHRU1HEw@0UjQ3!pDqF?$_E@Z-c;kJ|mi&N%jEytn+Ix!hLLi6Cd8)v=6G_KkvC z!@hG1U7^YyL)}w{G;Uk!MXTO@EdP?kCUEP?hyCH9kDf4l5JkHB5O=}w-MKph?oU@L zQF>SUtN1}O*InFCRYeeKQ_%JiG)MknQ%akRY-1@A&oT1)xL@@_>x4mC;|XDseKbEn z$g08f;~J0kMAOrunL*E^@Jp?otUWsL7*0RhBH|dkDW3O7Z6-*(*+G@@600aRDrCll z>gG2%lo6`~nJIji*jj(q&~2o882DqtMTn<qO&sj7R5fYlshC+pTU;G!edZikY;}Fu zyReo~@~Nh)O3%RQ=aYg3_UF0yp<q6*1iG=CJi!_;a70!2;z}SE*o_|hdv|fk))Xf7 z-Ebz;!B!Ni?8;Lr&rdiy5$W*~dj)HyO+FP_vO>lxNn0d}oO~ivHjBD7N%3*YA;|pa zXN*~+$VZ=On=n+DWo!`mSP&(T2I+1xS~$p!p`i=@a{M#oI&Gc!-kANVBrcp17o9Zy zHX)=5XXme+pec1B({)7v+&IrDtjSS^g0C1Z241RUS|jN_=<SFa@s|Kmj(rTRHamQA z3b6fn1YCYQ8oKS2EF*B{L_r`$8h#x6&=lM!K5f}mKCUXSm`Q?2<-&~y57=nk70R#+ zzw)s2zzy%>a|8Rv9ixqOd%CERigIFC%}yym-4}V>*Da`weBn1|ylUy%&P#0F!f9JP z9u<J0yb$lP;-@jZ@fU_nPM;a|VF#;|E%DX|3YJ+M&9C?9M`s;FLX~~Jc=cntLy=HK zp5;VK^4&eVjev|JD4AuXML^ZdEfYBG#xGF~9*?tm_gH|9%2Wy;GTL;eYn4)&XlFXq zldJ9a0(xP4+ule0tkhNmm^ix;w#A?f<4+7-=VWDr9_7Nd=l1aO67?jF{9ehC6XAjp z4NpDS(?D>i8v=&orSfY^nl?^hxl)%}#%DS1t8F0s32@a?o|l?n=&2P={3#(%k%zQB zV(xu2UJWQ7psGSM2g^J??4pTW(cb9UBX!a7T9)*p>re$<2OsJ2+`+54TDHY&PDB8O z5;%UeW2v$AXx=Uxn($#v-MvUeeqbX$XmYMSN4xc#!dQ9q1np`721bf`rb~|I@?Cj) zGB`t%Mvf;x0_l!Z+mVDJL|pC&<M=h=u$rdef`)$37)b|fIbnc3toKo;Lr`Pfb@1fg zQ`tdPPk}1O`x0oECMWM}A#$5ND-i5uZ}}M2z-3I;pPYYxHBtc@@1q$)w3w<6vTRH( ze&4$eeZ8RtVkd@Swd<TMxxZ^8>m6Qh-x7PA-R6(xJzL9%i?K3sibl+q*nGx^@tNU5 zPjUmP%Wu_O@{#x}k4qn1v;Hrpp2hl_v|rXN-`*O~)Hje7-LWyliL-C}RNi*gUiT-1 zFUF=JHP{}5S514tc>MnWG|!~Qb1VUHR~z2{AJ?jdwV|E+f9j=0%yrv!wuenU!8Ujo zAlhCAeT<()C#0uJc@{8#*CP!eo19UQ^F?Hfuzu_H2uCE8b!O4QUkX#jANwrd5{-$- zEKr(^f*3eZF^P7UM1%)9#(pI41d6lStKfO0W?iWs#lt5#bsX=+%j->2@==VGA-TV% zOM-45ly`4uDZOOwowmwQAVL<oPyYDR6YA?I@uMy+WF<FFU?||nII$bcB9n)RS7;Or z7M+x&U`l{36IO2ycr2EQ0sNt4N-Tmh0G>xX9gr>}R>aLEjVG|H898H*{cxz)Mfg}Y z3Z9mU9769Ub@uaWf+WevvL1-7f8R7|oyJtJBHQsTJiAHf4FG1e)1r4&Z8PZPr;ICR zrrOCS-l3SXt-oP+H^^?#ddoZ<?C$}gJO$CHdYj|a^E9TvBl*){#=69++O%g<<&BBX zo{IKE5eBgcE?Zu&_2qktC+!|r3e}#9^wzF?q0dh(>agM`hE**BQGbOr6i)|1&!6&s zTdO@AD~+~-OIIsL506ztMu{)YB2H-mp$@tS8qLjo%BQ`y2Ei_u$#&tI;7%py<&INI z8$2OhM3z>_I}9W+17Mt4phM2;%7=}V@V3nYPDXgQPA##4GU;-q<>5BtvFGR^JW7)C zy<y_%J=bE{y?6qU3Lsh{Q2MDT$(}G!=)z>78HT10n}sTDHZ~3OI5H)0#uU)VXGjX7 zfS(LEp9pPF*2O2FLL<fkjrMddP!*|fX+pAGc9nN(O5Q+=`60(F$Cfs6xQS|0794x4 z3!l;XTA)SJNW5LE&B?wP3R^Sw^_gK|N80CEv0(}t-tRYD4h4O(N{@n>kyu}-9^|7| z_~ZSz5AhD}vO>U29@Wk^M|p{%Wgu}B$2eB#@s#?71(Fh5RzmLmby4L0VT|z@WKbT4 zXMU`M%!VQoNc(Dsngzg&9wWd^=3x~EZzQ5SCAuAOL?U|CbAyFY8=wo5?GO^mA95aW z-YgJA<4XuZ4PUv@i$}&m04x5+EA-+rVIo_ntnjmUe+iU9woUxwj-h5o4w4#0je-mW zU<m8_#ZMJM3DLIzFoUEHObn^D#<3p>^Gh|&*bPp|wDQ8%m}&?ayd2)c4KBkVwYG<k zpqv}!<5MgOL&Q1b1999eK<It@E8aOWf}p=7PC)F3g5yl6x!J5QI~NPEFzgKx!*6Hx ziN&jehHj#CxAr`I*zqe3T`ZcNa#een7)y%qq44%*cr<H&5`$Q2qS9M2A7P19N}H{@ z!)Y<v$>=||<Crs{Aw%sgJttdPx{Q~r8V>L0(Q3;jo1`_BjDf2~0lB#?<E!g@d|5cJ z1+yqe-PIt&#ANJN7<RJ4SHFDf?Jt@z@N0|*F{hLVE&A%JXr%VN_HQi(?>?Cf{`pLw z?@?-%I9g9X<&R-Y`I4RRNtEb3vwxW5jw_JIgmvIEm&a`g4AToaeDBzZMqwG#krFf$ zNJx5Or@%J2KK=UpP8HnlJ{xCc&q2(36r|H^a?$~I2*ihDWpcE7owT($6vkOm+cU*u zN|rEdY}d4_3Qv!$`l>q(ll+ohi@yrmf5S+B?kL-=Lw3{jFv0{d!&g|qSa*CQAjeo_ zIC1&>Q&Y-hGrv3S8+fK4<87Oo&VesHt~Z(d+1TbR$PgpDAj&}q|L7vCT>C&KqvY={ z!v7?>dbNN|x6D02$)7}!v&!)1nx#$y-ovbTzR-}VrNh;f<@6;ZJI`-5!J#a38rEV) zr={aTCEl6rn^!2C^xp1Dsi=iwx^nzerqxIWL%(3CV~slxk5c#O+-65-0~0GZxG?#3 zsz~=@VQnLswYxeJ<`nUrtsSzeC!n*+b9mW9QA=KTk*?zf*Q_d2+)9ZBkmpe)AM9Ka zS_52&3Z(7UE|7eohapCb;T-S#Nod^__igZFF5jL()+j6j%F?Q0^}Jr!9V{!>mCC(I zDV(BTKpUO)^;DsVsynK~#2!L%ywaE09P(n~5`?OMHXNG+yqgJ-7p&!p@|RMnf07Z% zsVtNa)LB1cd8PJ=4)~e9JD)0VQw*DOesi4HL`JsH99zCGswQ%)Bwd0~xwK?;fG<Eb zWNCY`vZn92v|L4^j9uX|TjKbjF8P#fT%@JKYmSCe@2@u=&INiv@PhBU!Wo&|q6Nav zQCMF{zgKVT0KtNI{Gf_U?LWtWSl_Lcx&gR|C;&hE*CxQi+Q{I)^rEVK7hC`X!qz(q zyQPwcA6Z9PVEA9bYzZSQj=FkU)Un?6b$kS!jC1ej&-e~2*0x!xdB260OPpV~ym^%I z{emem{hCi-UD&Hw(5-z`vA?SB_3|kW`oWn9W4r3=qO%IzHvdW#vcxzEQB~!ql+5)f zPpeW|Pn-I5z^I8L>}^oz-WAG+{3LqnXO!C_9%niWM#0LzwX;Z8*WZQ~OeVL6&D4?7 zo7g4pa88fXY>QFEb|&1XQcBh3C!D|hM|Oh8X<zYwfUM7d2eWJp9G%Sn$2m@#dTd&i zqMT;r|EFEDpQv0=ym>g@qmrhjnwpqyR2rF}-rbXulvS3XoTQxzi=~&Ipp<P6poof< zAD;kr<oPW0|Imm7YTyIY2IJ-Bl?Bt`2J89Y$>Sn`w^ot!F^mAuFs)-E0Lh0oHed{i z=Ae<Nppm2%YX&Ytr}|l<U<1%#NrQl*0RA{6kg({oLJc6zIRyZ4(ZK#Im->HGGON`T z?J^kv?VP&CnY5OPvE-tykY#hJr1#WT1r?M2fTVtB%j#H9t}FrYk5dy&dB2`_ukQCN zFLq<A$`u4UR?70pK<ow+8lv`QDy?dz^fZM<wp5}dE^Qk%u)i5+o`-z~w&O&QZ7HPn z%|GH1DUugbJ}OlDQVs^0WR1RCs+05>_+cb3Z|_Ch;9oN>p1ENImev-?AhVD+0ztqw zvF&?MxY~dzT?yJqrsa(tdPv|~6$Li-zg*0}Da!;f_*;fj)Nr{ZFv(fsP3zaaMTDR2 zMpKV@X^0Fk@bWm(tOR}miOy?9l(@`94=l}cFuMPa(!OW3Bfc6%0w2Ud4@(J04cV0u zt07Zb1aBxGd6|zPW{~w)?E@o{&@90mK*&YBde+5IW>~5(KT&Y01zBVF-FRMIq{L4@ z8ky%X4hZ(4;A!|pT%R?xLgJ92QSKjua-cP<;7D=HWm`_Ka;=$J-&ibD8%MWn*1ySe zs@{F<)ZP7fA*0I0Jn$B<+bA?@t~qyJ^f3ctnbKqiKAVklvx=!As!MHw?iL>D4jrIn z%IDyaR?+p^Gl=h@!5~Qpt38uQyO+rCCS9Oe-`Idb8&BSIn9xR*repIFS*&+?2Y%(5 zPJ2s?q6($Y0a<t9)WNGYv8|St4WS8sU*nzeH%Q^bGhQy$U!EApMy!IauEDmKNjqz9 zc%j^a4YL&Gy!PdCDUHvQ%<s7ih^qtE(G1|~K+1eV@$TxtqH;I=U0Wmv#t$cK>_$?Y zIR$0&eD=t=eU_oclw!GF)RUSd#}TyglD~x6aVlu(dds%I{@g~W1fNkNFqll-->RF! z3UF;slGWi`!3N{+)Me;sIgIq*3;lHvwI<*~YI>Y1O#b>0FN?z;El&u5Z4Cj?{QV1d zYXh(vcJ{D0`9F>plUC~tC_U%3?sdXCGmaYc19AQOsY0zk6S9R6Y-!928V)2K=Rcl* zCtSP~#$+-cMcQo5U+;Ub;Ok$Wk;4SDDMTc6Di6{U2G5f2G%7$Km=s3`)*4<@@KjOo zKu5|~jzLv3Ng7Q0F8{jZ42Ja`94V<G>Go1AZDvOCp_(9ujo>Bhp!o2<?T;QrK(3gv z?+H4H=|5gi0@4-qYoUFOo%r-bndbTHsN0)=w8T`)Rvs-wJj!<0I;wp>VJkk6J{yeK zl-QrRgp^XJL1VkS@2%2plO24YJqwxoX53$Vcq+;$IP{%IimOdVDvVX*1!c20svIj% zAOBJVRt338n5|d?gc){bfh5&dgpjb_G;VQB#C4MIwH)C_^>f<s$c$cP1{@<fS#dOO zaeAj^vGtsD^p0n##|+qeGV|0Jp*HuJF_4GaTO~T(X)&xiQs_?uQ>7Bd8G*UdaT}E@ z6V@+Tv8N6ihcDs&OoG+%39Xlq;MJfY1_fp+FL&MW>67wsBld~>Lgqr@wn}<mr<vyv zMm>OK^SFs%zD=!-LqFn$5Hlfi<h|{8NaPBvv+;$gp(Xh-2HB9e;W4rF%7-+`cgKtH z((IMqrK8-~$^8t{+aV6?)mrhHFp4TnfYWoGWtJ}3><0~1I_HOEa^0`~NyV~GdG^!Q zlUL>d|6lmD%pZO}6XgcpsVD#Ng!+p>E`<viIthUL_sbG6mhj)f6ZLJULN)}SXT5w~ z{Dg@ao(Em7^if$(Z0?-3od8L?M`H|3`j#u>QPDfjD?L5JUcaAmznAYobJe!LX09v= z*Ky@Zk;G@EmAXam+K8>ACE)aab4zjzEC>OAv_qf;1`CxCz(o5AN*Ti3*2XEtsH?#@ z^B6%<>MAwM_ekbY(cPg}(JUOx^jBsXhAijlXX1jSZ#{Ur_FRJsve<{Xa-?q)_8W)4 z#_*a0$$z<DvBY(=|DN*C;Z1k<{>}Ha5pUPI`&X_15KEG&*3J!OJg8LrVSu-T3*qVO zxRU#FWFbei<-EP!9oHkr(_khDBIf;9V^L7Hyc~blIzBl#-~DiG3ndQEN;h*^UHbJT zYap;HA%`IABByz5mi~u0)F29sf59c%bxov^_CcRd`)D?6po1MK@o%-bJPM+y5sD5p z0c04`pdb5Mk--b+TDEtwODY0LwU}G|xK#6&gh@~*c$-^WT9CtxkonK(3fwUANWcwb zZh<~;e!r$r>fh!v{h-^7J-D4pxheA9KjCH8i~rcJ_?{~tiORGCT?&N?2(46b;MD7F zp|YBoqau~ca{E`y9G+Dc(A^2Eb|^rU3Z>gW?BeP4w9e48PYRGb)3XIbnb}fSs$71r zH@<_9g*;*ot2LJg7=j6*9aGJ<xktX{9GZ(8EBBfhr<c>l-nDx*@r<oGxP&YAZzYXo z(JT(^9yo^9|Jacx?AkA|zpWd^dRdorRa(T8gi6xY;42bINin^OWP;*uvUsIZcr9Un z5{a*vDoDvxd+1<p9-04xd8S*&y(fhIYN#t7X?;1!wW&wW=<TuSsGC|;kwbd?0IFMs z9<+LPaph6Ee%aY6R($q%u(DPpsc*N#DP{Gy+HiBpqQ~FTET-SVPj%6%65X8|`m6U= zoDdi`Zm>$YH&4!H6va1MhgZ*ckWz*-GNKLXwqmo;=Ie62!!&mSrX=*Z^=C{H&Riui z7O$9lQ?7TgRcrkb>hEiPAH-Pp2p3Wn>*808#p0l~A{3P=zrzo}q0vM{NChKU#HGV- zb^U(KfJb_6G&WUJQd%m2#TJ4Xn}{yFwDoC;LT6-Guue2|s?Sn`kIsUh&}A|$#=!p7 zQ}It5P99fl)|rYH1v{EjOBFHWO+~f>##mM%emAKI*}A6ni?mqFvCgKf)M(NWRmRRh ziFCYf^#7=3wP$3XmY_R64My|EA}e{?VRYp6wUj>-Zuqf?!+%o(j=|9r8pDmBE6E>C zj=WqhzleqBe)R|JDZBjnq-g|>_q&z34b00u!(%`1bvkdSd_NZFo?c)#f4v#WeZb=Y zHZ-x`ijFskm^||ucIA-^jp-d?*Vz@k)lYbv^B6W~vm1DA_YbUVSNCxyD6PWN^xi`x zerL}=P#|LCs^b6vX;uR$#i#)VnvI9O2mOEFOy(xW^iIykfQfo~CIeezW_lYtV;5@^ zQwv+;|B;V1t8W8dTL?bWdW0*|Tz~%j9D;0$W7UKragexUpb(^J$;uc@uv*=`+rB1Q zP4Oj9vY~e(*>o#?=iAg7pg@z1I1r5q5D}s1`b)@PI78FvfCVj=7KjHXFF9hdm*;U( z47WDl8s!+vbrLuD^GiDzDaGt#FV92@IRToZ)A?Qx5TOF0g7y;IUkagt5v_J*9fQ9) z`NbegD3ACS#B>EiA4wr!&aYL|dUfT;-uPJX-utz4g{J<Tt9BK;YIzdWxhX%HoU<EO zD2Jylj@|<oIqAhX+_<UCzCuB^Ma?-+E2+0cl5<wYJI=xwuB122RwEC>a{pP~jC;(H zCE^^><o$E}?H<nCLjb$az=$b53JHsOZ+4??SLOBGxwTR=8tk6BhT~J`%hDa&BT6EC zsec-Sruiy%C%1o?<G>)|RznF5r~nLJ$V5Xz#td|m`eH8vS38=Z3c1ZIfqT}hw}^Nu zm+;wwiFH=gQ&O$bSPu8GB|M*`(nZ+iNaO<!<-Pdtyv^ynvg=h!%$l0RU6XI_*R948 zb?f24MJy9KxElM9$h|{8?BvutbL>ooAF8-y*DUe9;B}ag5B9M`dbkgtwiU4t_M7_k z1@Bo$Qb>2Gzia2JeEQ~E9Ln$9uP@(FMbeADuZds-ItH$fjU}Gqd5#g8-sIBZdKu@I zA#)`i!R^aNUHtGwEqmjR<M0{LG~X1@N%l27B{52qi=I()`@^92j8w&I)*raosv&zI zZ`W!R0m<IbA%}dm2w>3_#f)(Sd0aG{sBD>G1A=cT8Rno=Kl(`OlEv51xA<={1rYn` zQJAux^O#1%Yh=NT66h`mAlp`yJusdMD1#vS9Us_Xs?=!hXDvc{DN;3s9Qvf-Ff}E{ z)ftn`D;9n~Fj3fFH>Ee8|CsamK~y&(=fQ5ywIsnB%Wg53x7t*?c=f7=4ONlk<!3c1 zG^qp2@26>adQ@J)%QQdaqE0bXQs9pHWVdIVH|%BgfN_c#BYM$ypi#7nJnxv$lpgZh zs3kjDiYHG9Z4NO2HEWxc2!}6gSSu}3+WHlN^}%|+L~BXz!*9kOX#TjQpjBvU1J`JH zUK5^7q<{x>DzF!;zwT!VEjBH!{syWlKLF=>St9TPCZ3;k;}*rMrglh8+1$cGl#1L8 zNw$Pk$~CMPz0%@2<C<I5l)5L*e((>TOapdHxd3p4@c}LJzwqBSPOe6d{}nLXME3zY zWWZ?i?+7fq3<~((zQ7@5W0HttFb#;m^+;^8UcU`Ul#%2wmwi65`zCp*VIss%E-UeH zjSz4Cw2>$@nIn!N<}<TC28xAZ4>hP5zjDf!lxO<qHlXVGI)J(1g-NR9mHRlCtU~7( z;a>}e<&c0gEUxE2wgnC#a->^e4&Nfp<RKfYYGak(V(%}O(;d;w`I(x~$-WdiudeX7 z{$oC(|M(yS2=IV&fX~0=580YHJN|bLS0}Dd>MsLA$hCI_Fg@*GJ^?{Nd%Ff`&&1*& zmP@!IW)mjW=S$i7X6H%5xQUPN@5zUL;u8}gETG|V(cN&mYbx-Z*>ZpMs8gS8lyqx` z8%Fs)>>YY~e;aj_U>G(r4o}o)Q_{CUNNJB32foOP5<h3ppE(yNnUm+t6<Ln71Ri@j zV3NyY7@^=wdJ*k`OgDbUy<Ef0q!pB4zKzbPCqk-Cm_*3^15zD9Y3n1t)AMXUXwQ<L zCKQbl)$6z}s%njwpthT6GT)qjadB-6uM_3B*kF2}$m$B^knb+K==6OO<D}2&dD;+h zpRXnsG>#$F&IMCQH=1YaPd9gb{P^F^H~iS4OdVjG2m@A-=>L7=+y0N^Ua;Kwe}=UG zJW~TE6K8N9)XK=aULizDiX?Tc@kprGde~))381eGQUSBPxGY;>0USd@5N3X2u`Li; zyC70p15m`MU}xcjp9B(}`ud9kuL7OxDK5a}T*ZRWhGH*RM8T}iQ;<y_{@@(PsMb%b zjg!v{Q;$LPZ@ROjvJUuj6P78g;jfZ1@<i{K{KYms)zJhVChV-;1D}!Ye<t7@5f}rY z`@meiqz<97t4;g=(Utu{b$i<f(8m-1E+DgY{=e*TwZ^s`7UzH1<1}f!AE?D{lC|Xd zC6>R>1|)5?blQH06B(xf|8l+LUB_<tJhvO#(i0$*2BOSt=KV6|$-(s9a%y|!2vTVq zB$y<HWCc4fsn7b;Va--UFj@r?7#;^rR9)2sm;7Gr1lLNJsgY&@vId6{{`R#5-EAyZ zb)nR+UAXuj%yt*5Hv6L*SA@%B`E-P2acH*cZt|#wnVAbXwNz;`AH`h&*^2RUz-(?U z6rSoiR@>~&KJwe7jOb#3LfYsnwe+PnFWxt4A!6-C!s!defrjk=WAB}sG~Kqf-AdcG zZQC|0ZQEvLR@%00+qP{Rm9}<{vDS{*a~yo*JKr<n{R3~0{<Pk7-8c6qi}M2AUXFPb zWMst3g(41b94b^Fs%_?!8}KrT&65Fl+ez#V6RRtKk-9&Y6wCbeAIvsYZ?}0YmtQ_Q zy~Odg@ncCR`?dvV^;&soH#DN8AX{7Q5`SirR8;$(1kzL{coMIJ5^T58>hLhkWZHgJ z4SIu(8d}*uH}GhR+8sAB^5qK&T*vtu%lG`Ik8|w_`iz=XbP2&Rl@#>|+fR*92nD`` zC)F4R#y6m#mN^MqvB>F}Z(0szWJ)v<k2U8kE!n0i+AUm3Wk!wiXY@O_Y*CYNc6LWt zA~#SSU817%9KUYK*T0=Gq!}+S!BonyZ@1zup^g$VwA`(i7KxC^&mh--py@lt?n3pG z>otQ%7ER#l`=9Z%V)Ycc*!%`fgGb_lxsT7o(Zji4_Ggzg);6K1fHXDM0!+}dKD^>- z7Z}|(iaAR_Zw`)Rj4g+=VEZW7hD>&KSEzlA17w1sGaEjvVq`SM;@m_Y$J~=9|1|yY z+nKC^2+$b;6LQJ15SLoZY+C`OeZ506aetT;9#Za~kxe&JIG`e4HKmoik11McgQ#Z4 zi@dphEC;YeSjKNTVNU!XX4!v&j}CJoocLR^c^RW2GJu{M7DJztZ)4^T@l&JPCg2<M z443cZH0=bSJXG$Gi)yptE5?ySsMTwed1qO`ykh#4=Yb1xLdg3PTFg-qkkvR;{TmD} z?Fs8cRT6{E!X;%rRA);m(fSM+w(8fmJ^NW#+T$XghbNb3iwV3W6&;i=Mk@r;5qkAu zo$Ud%$FcP6WbZ@4Q(e*QJ;XL}i>15FosX~m#8tCRz3$!tX$fMZnY)gRi+;htvE6?w zVlWH*ddF2G*hB?)6gA2~WYA!YCs^l-EX2$h9k!Y43-wXKX7{KUtt&}3LREh;5=Mig zCImH6_QEFnVb6RXfb|wt8$ZC?&NCGdJ6a<*&yHJ=$2XpyEt97P?E8WNWZRDo;a_C4 z)HeoCheb_s5D#>i-*kHjOyzr#R1C6sGN<@aF}0V?P6y7ggro!OugsDy8eXW;Bq$ZK z+g9xIWIB4kbUau6^YEE4NgPX@yG?SJzr1+P$24f`Nnbj)R24fMNkTB-h>Z)FEgCI3 zQNVAlGZ8t@Iq3Nmbwdz7Yrai&WYa9T3(bxmFG2NpuPe>ZC+UobU8}z65IiLhBU87f z--!j>wNcpwwPweV{JZnWGw9YXvB=T9xW(Ntxh;d8;^E6X0zT`*4ov_h_N!J}ELhdS zgN+X&ZLR7Xz70i6zq5Z_nTK>1cJZ!sU8f4^ixo5YQeWo^KGb7WY{AAVJ`^Q9nx+c) zw)6p0oO$eCJ3y5lIS~_*YH5?Tz{81;4Cx*0W%#q}hI(Di4b*UQyIb#WP<XC+hUjCG zS%N4(ScC3zmQyi|!SNivYrp3<5yD5a{Mo4aeb#9o>FLAc0=Ep^XuhPgGbX25GboPK z;F2D^=Y4PN{HRIfbK=&qp^M_4C94qFiXEgpV~#n@+<BSOd#tq_XXkzt7$=ydYxCGx zT8)L!fdj-!lGgOrblJVMnV7B_aS>h$S&i^x8YmU>rRJEcCpJecT};5iVjptErKqxa zmubT%LN3en78mqdjLvI#%XPjq0TFJc>+oV}JkT>tSe<Csu5CCBG+#H=t+gm04sK4e z4C;{VtiAoF<g;rsyJH-@<!SRC*+dqA@%CubEU*{qpYw%r;;ycz@7~=m+`rBjY@DqP zj2-^&+*KxT|D{DcynUyd(U2@lNns!;j*<=<ALFKnKeQe|N!Ci7g{@nYS$n;_&wP5t zE&=0hf$iJ%*y^gQOSjXj92=B?JW>`SZ=T{^E+dOr)z_Td3&M>rXd_*k94;bQ3p@UB z9;4jkhSa4Aj!RK=CK4RnN)a%aBAv042voPigQ4X=sz7w|u#_UW!&sDuY(^h|#~~=- zQiJ0Gpp<MTG08M4U05TsCrObYy1);@1{zNft|%hHlsOLVrVz(?EwJSHtbr*T7s|n{ zY|K=_2Vr0Wdml08Bc%~1a)bKj_5=cxzRqBc94_O>+D3e$bFKj<K%KH=*BxbNvR(hs z;~f0$L|Bu(PUL4o-HWVJo^k5cm}ZWAO489*YpRz7AG9m_Tm|F1L`zS(oVnCxz_w7v z%H+Ow!QCKW>4{54tK^?<nW8<p>)hNUOHYXSW-)mu3mG_MC>ZztJ8Wady0k>t_^ekA znQ@jfL+aMwn5^J#KrPNmvoTbUNl_**0gsenJ!3rKl;m{&^pZ+jWa^=v23E$tT3xyC zm^-MZE5Oq?&enUc9R%_7Vgavs5&loqSPA5Z_+fO?y{FW!l*HWYdIQVTXoxwt1Vs4b zv6A#{=~zndr%}IdusyY39$AEpoW0yzCEFssM3ZCRRV0Cvi<{AIep3o-{UeS!4?P2j z(T3%WsoJTX!&+yI11-bU@JJw<PkZNcR9PTpt0P<Lbp%#tLvoXFi%kOA(pKv0JN|UD zv2M(|x$7bivn4&<$UN2pUyV=tPoETeG5P7piy(XCtSvWeo$YyA_vOGpPL+|GHBGDO zEKRjH!4j9htyL5lgvJyU=9Li>nurpCIQOR(0&mqZCzO#S(Uj1pd>QcCNs3*uU0nM) z{f(OF7P#?-$BZ3P2!fA2mLl1kzhL7em6zC9y+86VZ+)}S7x-<?Gm3`?8A{<lBy4BW zjpnMmANq8f&{oyMU-W$}-4(RfDA!TPI9N8W%#PhBTC(5&v1Q%mLnedtolNR~e?I?G zci7tfW$XR--fisPY+TSc_{Y(N_>>ny-5|dr0;eHR4la_!EVHyALrPjY)65nsb^0fl z%n^KojgRSTLOd9=ua|P(e-2gq7uX?CpybUn-)hcGO;lg_^A=P;2tnrr9yfv3I;ge# zM3uK4-a)*3)KD<vvIZ3rBa?JE!WzeHB*OYL!dXmxZA5<4MC7>y87TOr#QX&f;bh)E zvo0ZW-L=n+3S0Pb`|_|yD<u-q!&{k-igV&M9)V<0Z8+YpmzGq1gmB8Vo`cA)iw0td zEma;V{pTV$1qfU`DQ(EU$^jy>;)s?eu4;N@A9Pq=j-7yh^YSBcyTHZkJu>Zasdy?N zVH=Z8)mQIi&1UPW?W@hpP!&$b721EM=?Vf<jgsGOU%GD^jqLxNrrX*XJLo&v{*4P% zrpkOfMSa&guPDHe^266hb(>N(-%=pT!0Wgc&j!E&yk?#nsmkJGGM^u?rEpS=B$23- zx%vgkaKQ(5?D?O)UF(bt7dgyIfitx~Im2KzGE=>&_;E2ZzE#vTMWB6oN1I{Zu#ep{ z4gfVJ*U$@QupVu-*g04w&vjtGK52C%36w4t%o76*)8#@}Ypm7r=c4PpsRn38OWMFQ z%h}h3T@|c@-I)}SZ)iNNjLKcT-u}?;$#sg!{h6(LG7FB*BS!*DT1Pp)5n{oaLfURK z^!2gp##{OL@FqbbPIV&~C606<9y3$g;cm{T#@r2%jxpK`B#viZizQD_7i{bryLFLy ze-ke4FimXp2L-IIX=^BE2vGgCYdG#xKCm0I$Ig&g=ScYI^>)$dwVnwiS<WFXL1mVN zpbeau01{=)%K1j1Fon3gGj+?8Wd|hb5Cb;&sbAjBdhw@M3_xsbhf%`ZDI-6QF<7W7 z!9{tE8<PVBh7qaS`wGCkow@qNQ?I-PA(=PF6t)XxO%{9uJel|Fk%v%}_~!%@8GDRj zIGPgbaYe~gh$LdoL}?G>7hl)&?kv~-HjXX#a1!IiB1JiD!464OJXZKsv5SoP*{Co{ zZ-!v1gyZ!`0!QGh2s2nBvgbWw^@33~v$3F2UL4>6mheozRAtdG-9%DBU30Ds%G9BU zFjkDzb}<XLr`W4a?-`t`y#-apXrX-Op%(7C$5z(zjr`H+d^S_At}6|7`U_5<!=kc; z$8|sYJr-eKt6K%R5n))B45~i-@yiNgm#36!$*?e{=+gLO9$&$cDuSD03HU8PlroL5 z91&p8z7(u*!AJNjr`U9CMFhsWO_m0S{<)aOOsa0sz+)+X7!x1l$2KG+u2my-q)sxn zn5({X9VF4Ix%-pfTS)7R5wh#l1x@KFjHS(W&BbqR>M&+rE=(nsQQwYqJ*YRAJ0o$V zr6L6Ms4fO3hN3&N)dL^!o<zlwmS+%NvF17^eKSOAzIQUy4@HErmW7v$k%7P9Z71@v z_H0N?QnEawhDonLm>vVo^|Pzpgw9Le48@3k_SmysAhz5J#*W{^_S^Ey71dmEDUAhk zuOaafAT_I!@U2AvZVwsTeAr#gDv;TBuV_S?u0eJ>OoEwIJmREP3%#xRZFc9%T>Mz7 zvK~VCoS)fR56w6}&Jmcxv&gd4iH2Fsfz`WQGr?vsh^a;h;3=gZgdK^!QGYhKUg2C4 zQ=)tH__$MjGxhy(;6ziAkK365W<Ud{mKz^?H`pQOqBQ1i!Dg?elDe6pl;cbK!-Apv zS_pvpRk5aI1k@uD=AIEN#F=#l%KF(mk(HXZ(ND(`s5J>tg9hZ@0}UL6Zt@t7gob}y zgYug2xBzXPzq-j!&8EN30}B;-gTK1PPu>BEZIKzR_lzasN!!`EZvWHKQ1-Qvo9r8+ zknv}z`uN8b$`268O$YqDYw$hT{MT^wT}C@N{vGBoH8yScTM)jD>v9cr9iX)R_h!)Y zUCq)SE@{&?fotmrS?Iyl^Q>9xO2QUMU#_^`5hYR!GW2GPK}V`&CeebKx>Iv+Y2QMy zHc+rtSC2?6b)Tie?=C{Gy+zYsNw{KyTrhn528p<)yJ9x8OtCUziwc69hOo1zM{)~s zrdOcSt+_l#_<_w`QgynlbR2G`<6PZcel9rImJ0NPDc*9wc>#4}-;Xz1_UT<p%|d>1 zx0`e1$|xMKB7PMQ%Rms9ccCOa!Elc<z2dmC(3GuS;LPk;O}mdDp`o=$J<@P(E3}p0 zaHx6I7Ha#pxIK}{YAWjVus$`sq$;=Qp|y|mU6uKMIXqQ&UziwCHQB=&V36|{tj=9D z>(<xn-ew=wL;K7?^4(o5N8H#1Sq6RlG+zQj;=06AoP(@3t*>S~>EC-Ae>-2)O8vqg zy}NjS_u|pq+TPltNs_uZxA*2!jl0<UxV_!)tC*tfm>lDw#hWc6V<DTwNTf}(RP__m z;8n%_(plhH%HT8R`yy327Azzx+)z^7V}pR;IIE7z4nXW<1?k^`VV|rwrBH-imDix6 zsIQ|%M+&nIkJ=Skp8aEWqh(+oit4P#wGTjq;V0OKz-idBbp#uj@Ri8A694s-r|78! z$?dEGae?7ih!o~kR#P|LA9Ufd#-v(k>$kM3I5ZNp5P&psB!pXXt(I*f#G7QGDDu7U z&wdygJO1+*zG9{qtosqz&#?%qH7iU0-@Vx9MTUE}goM9?s5E5WA(k(%8n}?LGs+%~ z=Hb2DJGyjRJOx;+UBum7Ja@K$n!uo;cqrbg`eDGpl&`n3f7she%`Ri#^f~V0LJQg{ z3%i3T_R{jjhFx8z#ew^e!{TwTvV2BazCFlu=f`T@iD##b@q|G7HNlZ~XFgj50&zJ? z&ShqL<vezh;Os9kT9?!>+5-byPZK2_iRJ*jf~jd8ciE_6f=uUlP{E;(UYdQ})~9EA z^d6Si)F?0t6Yn24nNb!*&mC>D=K%FJ%AlASixC%^xT=s|%oEy4S+vb>oQ<1DcxovF z+M^VIn#{6IiEDd)T$YWnhi(IJCS>F$L=@attmsct^O4D@0wi-v2eryvK`tNg|3C^- z^1IS!cYXPpW|k#CMeIi7-yy<Lj04N{2+YKc?vupZmk1VSy2Vy(@rFT@+?lNxCUgY^ z3AC^~5gix&gFupd2tow?#)y+ccC?&d4|KI*Sv6q!>*?id>DU5ptU6h>e40%+IX6p_ zx9jux5KpS%b>HRYTSBr*#r0Y~g3QLz<-y$k0q)+o_SNZ5|CC*RE>+@K;8_8G#iO#7 zf7U{B-@)0jIfamO)I}h{E98I`)wNAxh8W=PLpBuWtzS@kM_+-nP!6v?B)8WR)>_Fa z&z!+K2wlP&3Gsw;iz<;wq2?18vC^>V7jPOl3k1cvqOmYi(oV9B0&kXJSy4F>xOsmZ ziS#@~a3()#rz>D4F7dm@<rD#tZ<%@@$8wIcy_&>f9_g6jtmFeiYcE+J%^<J2yWJxL zVM3-uL*vG0kLBKBiI`u$e{6DhtmpBnO($|scT+xz33aZZugR-a0}lSAI5Qn?E!K5w zc6zI<G}rKl+Tw)$)-&f0zO>RNEHWy|m6_TBi1~y(TTd(=f0#>zq<xxVbdDLEw{PU| z@(%+@@PHBlhV^Bvh&h;aX6{fe-dv2CsL5C#-10PG6bhq-;NoXat?S%%?h?xu=C~T~ z*+352Kk;9zT6-9!B0q5^3AZ*1$KNf5J5)(X<Oy-J7xK-PG}J$(e_IgV8UZIWEFefM z&L$_Y<=5}Kw2anZU2BXwJLDmD?wk|LOu6w17yRnKF^TUv6&ZLYw2`lx?oF<tTPXmB z5)OiDvj(q}7`onJn=+XH;^fHO$!ZZ`H+j^%&2Ra^hb2(ddDbvXHQovpIEzI<dz2aq z)dTPN$3CiwVQlz-nW2wN|7&4`j>U3ai>j~cttd>x%CQL^TIF07<!nfob@UYH-Wr69 z{uZD0=*T))Q}y<OpDH83f&{#67S{I7Lvkk|Ax)9hT=}A`LZ3(ZM>8HG%%<I%5)A^j zImrHjo#dXO61G(fxoUxfOxTY)A9z4r9SR_VU+DVJc59M9u&q{Xh%smB`)7-YILhXT z;tJ?ZX2?SLdgPY+=&E8Ul>iHf16vWHVdP;+J>@HGV#Nw)PEVF;`Ad?`aD~a^my3Hs z&U7bnC>=0G@3;*-(HN&;b0UCzP_+Z+ULTAF#QhkI-HL8?_>}1y=Ga}mXb@G<Iz=Q{ z;gz&>>d{GXSslt`VsY6e`6BlLicU=k#wge5-OG(R)ng$P&h)%QN{Lz%K<{D&MtRj| z(p(G4V!Es5AT9)dpz9=C1V8BU6}YwKs;f${1?>HL7J7_aG3E?$fHMYMBT!m2K<(R2 zdec{PTz@^F!M8A9UnWqMX5M{)r?tr#s9@4Nd=~nBQ1h{|6IXBWOL%uU;d=IHe-F`k zd!!D{+BP96Ed}{H9nIplPq1oYb8F7L)z*3b$zt^?xR;7KI504p)^6hSBcFcvLYb99 zQQIFAv-wY?2}_?aJx=oWfmHty4EL?b;{YB5^uY80aI|<!R_pR>n0F7Inb5cVk*+;t zig2DIgMTzVJ%_r-6}h3zaCEv%CeFtS1PhJYWT_3F;k5EJbgcOz)Kkw6I=0gXlLUtG zc2q%$)24(jTNpD}71;0gpl(V9-x1@Z4DK~)4qO<c%vi>*y8yRGotF^vp4g8UQBW7o zx<urs=WXhNhmG|EdMr+tuxKqbvewB~?0utyNava_R>L+Ab`qkO+sV53e^gA4j`wz^ z-*u?ich&SSb?D#u$=~%-p<=Dg_iSVH1BDHAbdtXiHkQAPk`<JA4uSx=uTahTV&Pc% z;OZPbm|ufZqmH<%<LUj+7K&)HUb<un+h2QDfu@UgDNuFTNzV%TW3fVEV6L*~l5PFv zh#Iwo^0Ayp;amD=kP(4^EC*fTk2t=01Y+ypKh08P@jVW%nI+L%hNY}62`c5u8Yc<s z-&)<o;BLrLu&;DtS+JWTMfP}XCgl-HyZe?SE^B^pE0Ypxr(+{+uY=<j)kteTK@8Ym zC@4dPv-R{P3>oGB;o_LIIXr6pj5S?1|4bBip%xjhm3ST&fDH7hg0N~#s<I9~im`mD zJY!k6`+z~K+nUmD^Wg#QYo(<ku-|F1BS}oGs-`WG&0o*e5tw19MB3b6E1zZ{D7VB< z8jk=+O(kGvGHeJn3C5;6Uovz8fB<^ZbXxiaC4t(Eb+wY|E{4l~hS2-!9I`k(()pXK z{|}4M))yiU>Bm1h28)WR43*zYa^~O3<iCud?2HWlY8U*KNB$+~_oOS31Lq7#19w>i zIHaL>u93sv3@y`?j1PY=upU98KvFvNdF47xCsk)O+xN;y&5!u@+zyB9K0L2r*(()y zM4@jy8A#ZDbZQXUP%(jX>79y&S#Bi5ba--uV=F9bJ5Pqcg=f)L0~BXnR&c!yJ6A*X zOPns@7}=fhR)M(i!;LO3qDZ1?ehwR{oHgn^`3yexNkW0x9N_s$gz#`XG-Sk<S_$FB z3935`lIb;7BIhG(ft-M_xzPaoB5VZbV{L{~C{QL+T)BAg{U?A?ZOGNArkGY&#FefN z4Jt^vnHE%=$s0<nB>b&E^UtaJqJqS{7lYVbmIJ6nN(74+{cMHG8w(K|8ynQ{jH*zx zG7@O2Fm4TLVO9{ic)_X;KREVOFdoo5Z!cg?e*Omh$KoAzG}~ggJ_ivWH?hwh6c$u9 zpp)hTwK*PX5y_ow%fZ8>o~I~+)U{}?{BoY#GFfJ5ImDhp2beEapLXt?7ve53{pHY= zU(}uHRn@N3tuJ{ZFRef3s<1WpNxNP|K{%;R7SrNgZAWjNg}1aS8^q)*3n*1uekU&@ ztj-Y^=N)3wySb^G5uC=v;7U(d94QWx2m{h2myvWJvG14bCX*>n7JV?uCBwfW47Fv> zLfrz8ddX`-chjIG(66puBjkF7$FrajgI0fAxj{{)-_%K$X${p3ry&AYc$M(#DO;Fw zV_GTJ2+vK0?U&$EA*n0|vrv_6;ijSTk>!9p4PDMA+zG;kUoZhvhTM5@qN=l0C?c4v z&oOWwvlwM*G}e-LgT?2xqyhRZ_cEOPu+7DjDlf9CBnY9ONHM46Wd@v$yS!L%I`c3i zkrP(GjMV!nOCSd@sjw7c1@Vv(jU&vBX=c(oHP9#La@<mR@i<Ty&qPeKv`GyJ>sLzN zmJb%M4#pU%%u%kf<lDwV#pZt)2?=tgwi&cGZ<vw0XV<}?<)$@D5b$dTZS~rX?mb30 zoQc=_p<6E4`^e$QcbNfbekfG%XkET>AT<(y2i)8OCS=5eevc?`Zg$>Km~wOeB3z>C zIHB120x^TBml(HNn`w{J6+{1}7Yie8!5F(~JJsuy*@5(&ki&{&Tb<=wOJf(0+(68& zi9X%G&0Sx%xgw?Xffh3r>77(2m$y&C{vM;Jojk#x!8WiCI|`-AM2&}iwvD!t0`D?> z<mM`$#_(g+Jlel1&Pu8t$HHN1a9-+7)v<*MjJ;71C&Ky@ofQW)DhTVWINyHARoe}Q zEGF#1-FQ5e$+HM1s5M;36KG(np%;jb@TlUC@{!PCv@0vMmr^pfZJ2<nlO;d&L>697 zOG2t%BjzFQic<$3G7gxBbK5U=eVJYU)rV(%KyqG0lD!$(G=jZ_m=1W+6qB<|7YL$( z4e6-}RSwI-04cX!f`o8&Nhru@@8mlk!$j`i(`4u2USrwE?1g>{jOYVMUW&6EDk@IZ zCN-zS2ji_f8%U1m`pFb3ERIWmN-0b$ab?mcp}VSK()Xv~n(<8Sc7<*`jL|vBNzsql z>$3Wld+z2do@J5K_YpoqxTw^Q9c;|)Qw?_#{E!B4Sfi3qf=Zb3h<LHmrd+brVc@ZZ zEG@)FV7?pj!^%lYWTaw=K|vD3v6Mo60ej+oPHPiC3ZUhhkuzc6l`o{NN<0}|LGQ(~ zpfP|pV!JRAL34>_d6bG524d)cbtr6i{DjAeviJzcnu~1;3ZQbM=HHcy0C{eul>B|h zjnfa!WBkee98*g2Lc@$-1_dpM12-&T1V5JT8{dPkjKsQ33hqo@LmDekVkp2E3GH8M z2c;>pT~MrPyoKEKGec&oR0&8n*E^RH0$S5fmktE~w_O);8Zc<ChR8HA^TOt^0XZ-> zD74^<g#{vd6M=UkOW-|9FEKs%Ayu)29z5j)nKSyt;*t`(u7xZh&|NoOR#8rsMy691 zT~v`F6ZX2hJRU&$MkH8ws;29XS~7dE>#+5tUIyFh&nvvCA|=f9tOQT#K`hk{X?nJ5 zHrLl|?9)ydxvm05QmiLv<*`XMp?(H=f)?=L;a>%*VT}?=Fjlg)lZS1xL>Wa7$m!=r z@uDMc={1W;r%xpvR`Z^ePMY=W<qtT#t+#ALZ3F=_tZKT}F2eT;Ye9ONV~-AG)?CXM z52Pl>I(EDmn?<w!lao@EU#QLJJyS+>ZjG(?qh*7%597d0sp%Gf7J6+|12o^rh;E&~ zWQR5>PTK2Lnk9P`w$G~9qA50Oylh?JK>9DytjoLOvDd(4Wi--zUZyOfuE4#}M0DhQ zZ@SXc9_R17iwCIe$iS?M=NmZ#&j)6E+b)@Y!3>m21*FaK&S+?EfaQ5l;)bwA1LgMJ z*pu5uwf#l_Txc`^^W69>HuJU!$nTHcm5-<Uy<7AN8MGdP-7i--!uEj-AtHY~HRu&O zOe;%zpo<VvUu!cIZN0fchvK6}{d#3}iuF_FkfDpJK#9%AOgkd86`RxXya=i+;OED( zwN|40Ev76#KuCu?M5;3zNO)Hpw>ym7IP1_$f^o&Ub|8sjt?H47b?Oba6-QTMF&Nk% zpi3yB+8?D-PNTCe=#<+U12?tuv*i6x|L7b+4SRxke5Yh8-~7kFrDf)Zmj5HmF{$<~ z%fb1-xJOf-LVG(LilS#ixNGt}v9RAsg9rklo<=re$S<pOUtQgv75=)_q<q}0iyOD2 z^wjA`@Q(Fjb%0yLDiQTIph5fSc)A~sH{={=Jq=FuPfyBKzn%T(g~6uu@rZDw-`O9V zos2G_>UM^OJNYq^YZYQm(jN7;`1sdvlBmXONtY&muv0K&D|GjZB^(-62gj3xTy6XH zM#AdoVqoHLBX316)gOsoZY(t)E}`|*79|066){XfVgG_Ze3k;tV+L9mLe7m?6I4F{ z1BZ6o=x5M;K9@8k^<SBGtQqKSM!!L^NlT}TynuBFXaavO+*#z6!m}5zPqFzD5g=#^ zAP2<l4TGA-Iv|g_cL*0i@&?iA1Pf;t07Zul9xIyonz~ONgT0))cn+C;f)zm~;uC9h zu#P@;=Z4g1s>JgGx)R>pC`+iK8g8GZXqXJLEvD`3x8Z}Al?VVel?X808*SLQtmWre z6`PYU5WvK0gp0d^xslj8*(s!r5Pl9GhDgTJOE0o7B|>&f>c|POb{bGH&Lav(IqQIW z#1kGxEbNO=;=>ll#iH;o%@zibM=VHi0v!pjfPgVpPdmI-3}sE;tAec>r`!^j3b3wF zloaZ|hQXcx0wxcmYtaKP^whuQ$jdN3s)oNcw=G6r!mDiycZs4;zAZp7K?^I!&Sw|n z`Pmx1$5Tvv%qX5`9IBUAmPgvH7Gq-i!xuP2zjco?4z#w<nb=#zGV>yPX1)VDF!)E{ z!G|!h^b&3huztPI;qYtFYo-3rg5s77a;+p0!c8lmWqyi3{bLYB?@G2LbC>tPAE~kr zwD<?BD(DJLbg|iJ?-jMDshYP(Wj1bt8CM^K&u1veT?&l>w3S|)5N)(@5=SD7yjn92 z3}Tn8r>@lpXI$>1Jo}e6-p811NdXg55)&A(3K+7^`VDd~BDPD}5{hCe4<PGM1yxZ$ zH#TrmEw_gO%zc@nT}HHN><Sx;TPL9ska1zBlY~3L%J1fhqUn5pj9=BcUJo3NYm`n2 zK8cTiksskcyNCU{2t*CA;2B4d6^v#mWNw%02V#-l7KnM~;<d8%5mh9af@D5m->nI6 zyqi4}TE#PKGNdWLQN-}5TXUjG`eU**Qc8Lx2t9n1$Kt6ZW4g4bquo|j1Z4>jsM%6C zSz1s6sbbfb)r0vurd&}5_|~hzp(E9ylY(RkS!fM2YStPMj@iGK5jy%Yff;Q=NOmL~ z0Cg;N98V_If}v>@=wMNRF-T@Qs+4S6v}s2uRdKUT=xn*_$6OvENr=T+1a!Q(piXBh z@0=GR>8x2-9mDUaVwvm3mbPk;*9ou6*|M~vi>cu9936n`Q_(6ov^@8$;`bt&s!dgo zHQEFnDvhGdOa#dk7%uh6AwjT74<Ir891kSoQ6e29r_N#e*<}5x!m_wye%hAMWgU{L zD>4FQxxA3qH|tcEkTmv3Wy!R~jQrNY{W>49kSWdUdO=Cx(2>1)Vp^VsGYzU&?mwq+ zNq<bM*1y-IsSy5EGuH0!;N@g%YxVbPz@)0?Ur>+FQ#Jer2}tF)`!0i(e3MN=<>-mx zqh5ppHpje$R2Fe`VYiQ19FlI{ibQiTh(?^_%ak^!hhNv3MXo$4F{S!ziV}@*)y%=T zhA$d+CQ)DV$sVzu2t|RYMWw%V7~rY%=riQvfo0dw3C^fv-3WzfGt=|;XqbjrG&^+; zee>rU{gt>mGDBKo8icGv#L+$5_3=-0<>Pb2Pd>XH5CI3d(KyL_lW-i7=6bW*CFMZ| zR+D{vy;S@2d$dHz2E-vIzpXBTc&G@S%m4uY=tc1!(=tI*3dSHppkK46rxZQhUJ^(3 zMjm&UcbE61!V%|sJ|CwhX<jYan<UFmczAzoitg$=Go*$wu8;B_hl=0n%e-5^Wf7NP z=Q79xs}9R3$(OH6B}u4q!I5g81(dXj>=b9<DQGt7iY>b&)BnIPx;Hlzies&kOv(f5 zFk})ViQGZ6>9vUVd82m>vJ22{=auf{{NR8C1TqbXhrFL6RS#)eY|P_#j!VMx06f>v zh}Qf5{Tey^5upEu09)cQJWD_&+TxK^y3DJj3YOA_ISRD^8<cd6Lta89Gl&@0`iluD z2pPVXf{DskMKE2#5`C!($Flfxh}Xr&Y6tF~a2If;prAe0PaMsG%9Gkf5s}mt2EF=8 ztp*b;^`^mN`li;zRWXb9Z1?@QOXCi1^yP9<WJb#N25r``Bi`1{A6<{lr-@(F8!PC_ z8L2@}vO?8d9(lfaVqh~$Q9}(lXHoYI`tFqVWM#hc8L(N!;y?nCi;z>KDlyncO5VXJ z#yjx$B?tc1pe&p#I&y7#L{lcM_-o7ZNxKnCjKTQsjH_UnQrY$%C!$U7K=eVhoa0Z1 zEh0zdq^J86RkT7_5o#%uv=!CKO9scA_j`uRX+*nE)m(>bL0e&v;#FL2%<%bClt=_f zj=vvQc{Q}RS#N#`08_VP{3h_UcVgJg!GV4)gIW_SFK(7QV&&6LV-4MuK;|ZTZ6J}# zMrz38p%DuTo<ut2lI~lsdTBfG9}&aTPQ9WuHK+4$D>f}0?W1{SS(u|$O4k(@L}7(1 zGIQ0@lG5xS_3zV!b($=vvRJhpkC9IQ*q~FC%oVvKWjBy-lHIqDt{pu05`7;gn%Yk~ zelwL{xi8n~rVzcDszf==Q$~71hHZzkyt{44Ne8nzi1Gu4YzAx|?t3J2LmjqvJO|f9 zz6g~a(^sn0HqPmu)@(kHiv$G#(=5~!0farEied}*5`daDaNWL&m8pZUrJ8<5l+2jd zWo~9!nSnSKM3T`Q&`~g}I`KX6m>%c*&k*r#$~g!9z51XI@~@jic9y2jPUcpBH8diX zwf_na-pkc3GiYev@Wi9uH?ix4zI3GTjX?munxurV${5$d^5vtd8irWB$|-<-zMtjj z^hd&GCpa4mth1K>(zRYSt#&I7;9XyFA9jHr<9zqdjeIwJMuS-+=~^ag4$}6!NX6vC zHEpQP>R`;DA>p1@QgcK6eQ}aWLhfo8QOfI8?IUr1&8=!^yYmSf7JX<v_yf#51Vw{| zYPLa)VmtDrz*vzBBYFs=1&Z)15IP&tr99B_+N@1|9P=qWe#D6|!tc?&T2#0i{Jn)` z?*%fW*<FOgsoH-CUB>eLc)wpMGObQ29SN_^Kolc;y*^xvf=~g=0q<0jaE109<J<`D zc5%FRKmM9d#7()C0*iVeuHpricY}_NO07~QLPYaSziia3gSiy)1lwCV>#k2?GkJyr zYd7kW6dgH=9T+b`J8y{f1EoH@aSGGEjJa#+ynt6)u6^~9yTG;_y%i>LUMSMXlY)#s zILSfAAq<2DXUa9qKb|YshKxsb6pJyHiY{1*q$v~@Xrt%u^&FFwO7qa@nPxO1CPb+t z;!x_<NQqPgGF^RB@hWw{ctl=p3h*|HxG&m3OH$TFFE(yro9c+jq*{j#WsteM|5NjX zC#Y#NeA9QX+qgXs{CaN*(-YL?{cf#5IF@bw+{3Y&b5|txpiocabi!)g($5@Lr!67* zANT2@YhMEA`^owJP2~K`r1e{a*wN{KEiXD%H0Az!Z>Fnj8o=g8&&Nrv%Rw<ql*^dO z{ry>g{yZRtbtASn;)?k6jKV2yjVW*fyq#G}cI+CjmAeDbF5O5b-vcyeh^I8^+86dG z6Srne+9FIc(jq_L1kc)c`aSFAH;XZPPx(HAa%0$rL&rGiCTS4y41~f!Mj*OOA*~;; z*E&khn$0GxXSx8wV(cd<oOV@cNOma7Nj;})I6_5`J3JSXpD%RaGn5}XY7;lhEH|cH ze7(*fB_LfOjHa;{5gtIUTD{8tF)@|2bycs6G@!Vc!kwCpr~6rYbSdhjHqF{pFUwSB zs8qYdE&&)W^Q58(QNg3bwS}xwDf7pVji+bi*c}s>bkwXFZVN=gHRi`H2Xj&bhvk@p z-XR5)oA~k+zwG^{$%neiLWCeD0gqk26`+{Gp)GQvWdyGQGKobU4|sVRfF(a(^f1x= zX-SDqp<5oB4jO1aJUutNmXkq;miv5T?_*K>PQe@h9`+veR(k0xT*_vd8MpidUA4A! zM<?~sD9OWn#$Z@sP(HAA#_*NqGY@&^9f7%mr+52S+PqD7kcNc&EAu+NTwUBnJt%m^ zy0LBDw;8O4qzmF&m#Z9`%-p1!`*htSD$#C6aRXxuF$I`f(j%|w9AhI?eQ;&2ffF41 zYyNB+%TECbGqV<q`<K9!T6A`_$*l$-t(R6@UJcGozFuUS+lO&qiaAHaW-HU{Z1240 zHUfo#taa}b8h3=P;W}&6>U3=#3zuZ&TP@&)XHEf?+?=`=;2#Uxw~s&w)XY~1bgf&~ zv6`s<j*Zt}bNmlU1;iZEX0{-ifG!D-k`VZ2mQeg}p-r>D<77n9FFUApt^LAP1txo~ z5lTB@El9ImrBD<k-Lk06vGohU5~0CMOH>8w=)j3QMG0#9+bM;__W@QY&p<MJ!xkS^ z@fSrb0qPS<X)NA&|MWD{d};zcd+=KCFKt(QD(KJW2vNhF=0`GM5#(5A=jh|kybfjM zZ6dfYr%qD@OemG$KslFlc|(`HusPd0amWSk<SlV&cpy@ev{r7{CDoAqXT)Y=LUMcm zj@TXF-jx4Vk=p*hI^UBjQsLjU+qcd)bbB!APSBinZpxoImvun`W`a=~;Qf_-;R@>3 zxDNHNw<x1w7oDvm6+D0DtYfzA2uCPKr3#h;MFCpJsU`Z9Boiu-j$m<OJGOi7q(eoF z8Acz$`3+{<sKQ4@5+-`%sR2-nn;syKuCTdaTWR64cb1!7uJA}W!=lrt=8Rm5glqjc zEK-pH@0jV5mIy?=qF4qqUE1rx=c(nq5sp{fLgc8zl?nM{J6(z7^SPWVZq?&NIO&wc z<3a8@OnHjZJ}n6%Fg_gvCxl>t2vMTxblD=;T_;9O;H9FrspI{q6MNKOj^~lSf8?U; zA3NX-Wq^{YrD*7h%EXikVnUVs`gx%Lc)77vC|~?0OoGToZQ<l0+m^IGYwy%xE~mbj zY$7O0XDIVDIHcG|XBOw0g>|Q9vT|ss16Bg3z%uEx%ZV-aA<NhvBGiMx#DgA}M<tdj zrD=2mx5X>*1f+U?v|%$l3z@2&cZh{sWKN^+b*|lgj_8=`&D7q3nu3ZfTWTDp+?FD6 zuo&!W)4GcDv9>0hH_@66MJ#=GEEx;cY24mS8eGrG{Sz0`b=EUae?Q|Vm=B8+qn$0_ zk3Ef*Q7SB+`eg9^Wa$L*ft;&`v@T?RX0b{hI>UNLI;<sW1bB<0EcbUcv{7B{y?gOu z;9)UvqRo<J4F)_OE)gw1%SBPAyC`<{5zenPdDqo9jzfB`Grg5ePQ8!&oHtsTLf8uM z3Ps|^^X7FlrSjqmXr|oHi*VjQzt|m+MctM>81ey*LuxixYcF%fFTs*7J4@6e2Q_fs ziQ?rTT%HV{iMZakJ-95@ZJq#mqU$u~RBNZmsi~B=j5^I8i3R2`@?53YfK9iOc;S!+ zJ9<cdeAV{l4^;1N&}=N6Z2XvqA1n%Cr|Y#s2!RJBY@}dpkn+(+M^u_<>^qU@cjI;b z@=Ea&&O^0@rm;57Xe9x$j3Cm0<7Yg!y$b6i2W~=8)W4;SHfNW+z8KMf$J%9R{`v<% z^#28F{zc}%&eqY~?Y~UTf0>&9GBy8YYW~aA{Qr!p35M%HB>v8`#Gw8?&$4qcxA_}L zK2(wUE{xH?^Q?{;DGg?<B^=S5z$k4Gb;$KfbfR^Kd;%#!#k$vRxi~5=d+g!S9ONP) z-YvLucihujClJ0#(q(xF5Jk!{8D#BVwh9Rd|Nb@fR_QoJWX_~Wbtqh@hd3`;^hp#I znYqUSOQm`Y@8*1OgvvsBP^bKS$xuvbA_`x>=4&8Zc(39>fon@541wzw!J4=r$S7^s zL(Y8~g*8#eF)Dy3*P0fQ>jw`EKOB-$evL2|Rwz6>p@VJ-{UqCMaB@0`<}VuBmf!4A z2=BRt>Q^F?gxbUtpH@odBb)f-!Zfv>X0*Ui_SWh~g(Nwy`?$j_UTt933v*(GeWW8^ z7zB<G!Lq;Tr;Z!o(&=TkaMt)+ThsY*wZCX>@FeXznNIi5R)^-Cz;9y|k~ycW1TU(N zioQ6Say<(2v7jEf&VzRjwEQyYzEoq6-l`MLTZZLI9bzG98%*Uacc$S0yh}QSL>mL; z_cCGuICU>h8V7TmR#rsTrpT9C2*bo#R93qMA%hx&$V4K{vob`58_OnZWZ~KB)h+6* zYKsF%E7q1x`cUSlQqH65xjAd*C8RRz7DF1f%#zw+6Ylpu?smQG>LwJ;vbr^FdJXYH zduEIQG}-et&6KK6IM<qoW`n$0Ky7%}7T2Acsh(dWnjm;>Kq`}Ut$VZyGmAzxDU^U& ze2k4Olc(tV{04ZBm3&luU!2Ye;c`BtHL-pFAz^`iK_9sEz4rt0f6;((u>H%v`)>e6 zS;lq)|1X!;qb9t(F6Fb{InlB;!$je-^>AYXt9=+qJk%QN_baYnovU6g5{0k8{T)w1 z?!OChE1~pwV%-nP<UmbX0?ZSRz#X6KQ^~z{C2v$9Aa#w=$TuPcH2P5jCAoLH(nN3K zxghzVhagS|fBZ3O;=RUipw9)yyLO9_M6VKYHz{G*>H+wsM0ONvCa|5yl1V3mxt{Gn zmY496UBmg&aCOYE6RlfsjVW2?cW^Kv$6Tt1zBCmAywNO5uC4wIyTlV=L00mle%hT| zDX$|uGMBGigka}^4ASj#OEQ>)LaBnS_!L~5t0MM9Z{FGk_h?CTf$XAzRnYD2vbocp zxm5;%9}Z&G1jWeOU+Gs-6%TJQE+qTaRt~>X0G9@~pG-5tIpe$)Q^QTR>Q)|n5;`|( zh9{u;Ik#me+9iIoFiEckGqyo0Wf*ato^Ya174;s<gdqXi01>d^CiYeD)Hk0PYV5D^ zxY_arpC(i)_?bwxV`4LF-q3g-nJ~7dMPwnvCFw`k>Yk3*AjCXnstRB4EuWNkCp*8| zZ{Oof=h15+)t@$SVwpdFKfb+fo2N=-BM_#4o9;yx=8T^pfOyGP4hF-!q6HPEPD1<s zLuZbx)Q=_PI{-a??;8DEq37tN@AN-R-%lyqb$aOU0Cc@nFynwx^V<)Vsz%Ds07Bk2 zxd&c3T{dtuiC}cKPM1&ewp=ESW*u6%M*5cM<tBFQ1XClQsubR6(_&u_saoRDc<`tI zJ^+MwBu=O3^4bKBHLYEUDt%MWYZ)8eD=yH`uDd8(h$su0(Jdyat5TE9nF#qewC@bh zxdz<?ER7FJP;YY30Q5a><XoOK;%5jXR58qk@a_`lT)DX9QJuYeA5+`|csNy-HiO%o zw$c_i=k5h#Y$BYCNKDd2s}&_oGh2<@^iDsc%rD$7gg@^0*G_h3!QhSB?z_L=gES>+ zr;(IVeGQYyBys~9;RQp!+3wxG3pa_2YecG9ie%6#16jXtYy6l$2P2xXxOQRcoKtA1 zKNA6`(Y*-^8eHGi+0d<mQr_}vOMN&*S~V)wdc1D5kYYN2gzD741H)qEmdw}ot-m=R zP<iO|wQF8qmv%>%z6=OzK+{T}!0>OgY<MhvQC+~|xV9ZaK9!g}+m?jaWuS~JO`tq{ z6*7Z4WpxhFwzql5=BnBX%Mb+kaL4y9_^VCs`EL5(#gkG;VceOyZ>J&M58Eq)iS>>m z>lWn1m5eO_;T+xvgD>*{zwk+f3-IlXG*Zk)xRCvX5Nu)&8p>Lx3ZP`ifxNM5w5LP1 zg&`Rhkx4f_G2?{rRMUetfO-}=StX^lY)?lF^O`RB<gU`c-_Iqb^LB(8cj0=SFdfN_ zBYLMg2=N$jmo`lrkjTor$tdY&Vf!+g%2r6Y*Ihu`H}%(iK@!?#Aa6K33*tIOmTkaG zb_6Xk`fZz|zL`q6M-M*cQ$79Ey6xlhh5o@)lBpdqYaVi`RSvU=*I(9F#GW`o_n$or z)E3uYINw3e_WL>${-1F$J16(Q0$gWIujOxg<e{q<D%^{_9qtM)h|9vu6#0vqM`A*R zZ0!_M0tuYYCn@Elwn&_0DV?q-sBO<}oQ#RaxfyVfIq02E=%b%@Za>B>)m{Re70H1- zW(pMYfSAOG=;+5a*P0qE7=Eqfq?KJzZaN=H+X;>#n@2(}Wm{!2*@XQl>&>AB=P+(( zr^w#x=j9O9^OWVts=u1!m9ZtR^NxaRZ>_2YAc$SZ;l}&JHjg|-VaJakfF4A&s9-Hd zgY*7-LDZYTS;{km-!Ld{9+oILqMN!qhPAVq&q{s&7Rpn3%|wxL3w))bR}<v=L=5-> z<Ad1y824u~Z`#~=<!W4oCkkt`JAkocq+aMUW-Exrkgw(DR7B;F@4XE0@aXeDJcd0s zWv}zU4^`*;fdB1qUH|roagG*%{f!SV@|0nal%fXLlSg(^$7L7+&_G-Q90YH>O|gV2 zJ!a77duPh%dw&ky!ci}+XVUBu5DcN#&%{}d5$^&H0QgE?>v}I2cB~>*5u`QYzwN+C zsZD}D<5;R2q#h2()zekVKO0j-6P#70G0uXFH1;eQoZRF>CVh%P8c7SYbPH+ogk4VK z?*y1u#8K%4z*jXw6!>OPB>s+ep8W>%pZ6z6Ik|M<`|o{zKav#xa(~=)4Q;LM{`Z|+ zRF<*a|8_G!SB1V91gM0&)KBJzTCE^3X4~YSQvw&PJyh0{(vhIO`FzFE{IrN~kdo8J z3t+J|+43aHm8)?%+!8E<K}HF8))|rJuPNKeSFr=pBC+EkV5DoX$5&2u{c5TUC=fl~ zDHALMYPiu2jMun{=#w+Y35;al<5XH)!+?Z?6o6I<#19ae<AMDBW5dm;y*i_&%S=o> z!`@Q==VWKq2)ecZ33_;tAtTAJZ1y2&67V87+9JKo(|$Q_Du(+=%~@3zy|B%og$>*w z8A3%pY06V{sEQHAH704S&2`gDmwQ4a95b_NKiPizjGkl7mSsqV4Ftk@kF4EzdQ_-b zo<%!Ppk9_hy2?o#)SFB}#?<RHgHj}fg<?d}mWI@a=2lJ7^GU1LG=Jr(_{SYDgh^lF z?Nx&&os53GKf2>)pib~nyKr<&!4}{6bTtag<CM7f`sMPHkj&iHP>)<~yFl%>%d*w^ zJ{Mh#;4NqcOfo9cqlT%ryrBc_S4gZ9Bw3ZH%(U7$hEK9$i$bvpD&66;K*$B&)Q4~8 zfTy>vH~e<>+3+hAVKCdJ^2sl4WKY^d<?Hd|{G?dNQa~G6mxW3nzvf9zPG01Olk?Ai z^m9-=WoY=nuSVSe#on~Lp_Rekd((pnGE#qQ>U^L$Ui7VmgXPwEDsYG54k%HOG$66f zkg`Y&emouJZ$_4jBP?J;#yf32jY=ajL`Z7VgT>odg4P5pjF`gPzsjPAUlRw$fk)pn z##%w_>NnT~J%F?2HL52y@-Tr`eKb$>L<%@7$&sL!3sJAe2%BI^B#Whvtt<ByLSmkL zRh)B5#%`8RCG?g%K!FRcwG0Ka+3SaR5J;4v2SfCJ427xA4VkkEI)Jp(93+FQrh}h} ziD6?lQ(Da*1bIYJ@MZ1Tvv5qA5m4+3qNkFYBZbDO_M?X7t@I4S{z-I7rG@;pfc1*D zz?4)t+8l4yxtGTK<?gA1=Dj*dx8QBe(wRrPB$CSgldEmY(8wjbhVH2b@_RdvT@J#f z7z$~6WqApv)BF6B>DM$gUeuPNFSMLAa@xz*KT_D`f~kB{@NaX`?|~2TzkQuXwtuS{ zEo$ig&q{PL{1RH@HK3CxMnq%Jl8f1|Mv64(et2|1npsvf$pvxB+U}1iF~tI-QC~!` zbI66*k^Rk^KW<M%6e>%CXTsKtMie5cC)Sf?Kl`li=oVcpe5x8VNUy+;je04#LnV<^ z6wJJ)9e8ADtBXdjq<iPd6tO_BXUv2)x_nV}OKpr;WU4GCg+-p3UkC$HTYnW}wfXh3 z$yl#!TUkDT<O7zeP?UAZcC-AnL02o2Q?3@-%@Uclv&gn39zFFxa6NMFOEL*Uds3!E zPp!3}h-<cuyB|IsUnTbQB^RC3aA>;r@9H}ba1ik%&iot=7p|sgNN>%}3$5upAr%ui z9XFpN*B}nJz7>!p*k(hdb_kWGWz5m%94B1`%jsqg>#TKYd2nfCQEK@}B!?tM#yY;! z8`6<mn62&g;!QQeFz?Y~95)G_L)?POGk3dtG{<s~fGSF~O+j`_T@bE@BK`Tw`0`2P z@qzk!RDZCy2MD2}j7BIoxM!Pou5R!}SbO7;qr_q=4)JA5J4THmyB(bQfps&@p-@Ih zav7RJT1YO<MH!y4V~akFJdp%_+6eCt#(B*E)?bL(_+Zgd1|i*+U+f)$9ADs+pMaql z2UEOAt)IBH2&$tt8OGl&1$bULaIInv`<l8VnD=L{O+;NEM>wg?Gz@@X?OU;M;W{&r z+#HRm%^r3atiO1O8zGh@=^VQT<28V$8H3kI*b+PjK+3R1?JJM+Ip2OT&uM(NwLEBs zgeom91d8+wQY9~zV{)MhvC)81n-O-tqr+jBX_(-W*~OrgiI!18_LhYkPJPV+j@Ow| zjMe-w0E9u{_XX{HgLW-1&>*B96miT5kdVZW(LWHjCsx!r_@O!zLv?_USW?fz36x{W z^981G*%Kxb>BV+K*&-r_*}EGPD_Qe`7ExAmNei&x1Ndsc_*(G?p?NO*?W2VNuU62! zL#Gg~2$pv;y5F7eZqDxB%3YUx_e810MP($YE-z-}7?wzJ#5_@S5nAgY^4b%4j!E?2 zN42~$F6%%Gr82Hw8bO!LT1|JUMe+$AxS?zToYi@ujy1y4Bs;xPkJR#~rdF1WU<X}N z9suJ&k)5?5#Yh^W*|L4uevq^9(FUe^fW6>AW!cV<YPx7ZCb>#TlT@f=lHO~Zt&Yld zZtR<vj7ePPn-_e$PyIdsjbxDhT${D^-(>GBmXCB$=woKv7h>WX?dbvBQVQbppe|1! z{VAKB<E>Pyg^U0e9rkD+LI_f41No($fu2<F!exKn8(3M~be~IPh&hjfpoZUO;XTQ0 zE5){hF|utDeYKo`+>3Ryb4bZ{0+@|9%cQ1-xTuTE6@fBXISLWMwI&-6rjjBF?sui? zLPKE-i5be;qgaJbEhQ>C*BiC~=B!<_*wU6F7c}fAD4#Et_pK3S+|L3GXDdlJd;~7+ z4=?IS{nA7G*ks+z8(d3SHzMv6@8p@mOB(8+vdM7<zTd%^Dgk2%eVBUy%LjcMvU}qh zw<M_6DyAL*TXv-~i@&bNpn<1{g>URDMhXDnyu9EIB)jg6&I$ztRTtStaxoG4@fu{w zkimemDYo0hm5)r+1Gz6IwN?b==7bPb3~n>5|AcYFWcU9N_Kn?@t=+b<ZQHhO+sTS; z+qP|0Sh1~&Dz+<5Dz<g=zNg*R?%5yiUjJY{^O>`a-bV+s?g<CRl<YZ^mcQ8GZ7f}( z50^_txFAN%KzqaMv_@Ls)N=>(di$?(UYEY<Wvg~CZsPNTVC({y+6a)9Yb=CfQr|bv zZf=3Ycpi%Tgyo~8_Ri|x(kO(GQKR4j&X+%F{*IjNHn2Q<%ER0UnP`iGgQZ-3(v9$l z7bbPf3*RVoBG$oSy&nZWcg!{NgtCtg+**keMx>Ey=0>|BM?vf3LF&svY4G;L3hyP` z<pU`u08M?Vr}2BcThMk>l9xAO{`pc|^Yw-2))eOz)euzmdkMF;*=%a6=Na1k5Zi#Z z>b>|Sn}ur5e532U)-?~cG=MLrO>%zbKnZ8(%46U%Smnn_#$IiuNRin`H&SY&vs^Zv z&j#T<H(*s%7BLR9swCZ7SgCLFV~e|vRZQ<dY$3#Ak82hZ+Ph!IUEQ*AnfzA~uip(h zP1oLV%OB_0>F#x>+GE9Wq}I|;jpZe#l6lbkm&GJAPxjJ5axFBVWSW7ijhD5~Ko?8d z?Pt?LbZ`tr!e<&iIBjGlFf(%ogeNKJ;TrIlAxzlxJYen`&-xW26_=QMI%8to75Q!^ z-M7)=uDK9>uEPFyw+OFFHlejw`y~i~6ev3WB<RN(p~tEuH<>#wmxt>cQRy05Rsz`1 z{G{JoqR|~)Qz{$IL68DQaxrPm725t_yuWZuICbk4c*Fa;PXWG<{0rn*Lf!EtB|{W5 zWcE#+2Z^^j0J4wrB8Un})CLM$01wU5p0GUm_=5l)0(M5kAYxkE&nMwY_Q)$6Oojpo zv_#_q*Cxc9@QCwj^W7L=zIB`5GH%nnZ<K{T&#yqdWZT1w#4Nb_ZxxF=d_-xI{YPsC zkD}6(9D!;c3Q-X+T0ihe-&oYxjlt<V9}H2=1sSZ2H@RFIOa)Xr%Gh2Smu!^D<toD| z81?)7KNb+14tS+yofL0q)5``C)uq(?OZuR~!o6V`F;}=NX=pC2#ytA}ljXt}0<n1u zRg|_wEv>(^B><6l7JiXdw!ig~eFLI$%N0gll-PLYhsJ9|ke1!LSx%pkzXlQlQ&j;U zrr_Vy_WL3g{y`9afnzPa#&rKl%e*D0>(LW+b-?&Clp|Phx@iat)1#nMwFbmwhPaeP zNhR87Pijv}VpLHHP6d<ZVSqzPdeQ^OJI`Ju`)>fw;Jm<D(N%+gyufH<jykl^pf5q- z8d2+Ezw3FNu*WN<whf&R2k-jVKf?6Z(S7!czXufG?|JcG7!@Zshi_8%zgnkwW`u8# z9oSp9F?qHa40T})7&tyW(G3WsL1fHxtRKU>j|d4cjEK@@p1V5+m#i&_&SSf{`yV1f z@{?6#a5MI0@${B_A1UV%eIxHt{d!i7mu5lptTGH!#-G%j%i@&<eJFTl?${imV(dlX z4JyT?KgJV)an;n-4LgQ|=$8`iB@p)_K76S-q^TyARo`$Oe|AS^e?EMSIu!dof3S7@ z0}i58gr2Q_|G|fEtn@Dm9A`75?}S|Y|1yd{)CTNJzLC-$I_ZN5dG>@gqtwL@{wspc zHmXKAxL-OZJ{Ltf?;r8fL5V^O)-F&e-A_j^Q=XSQX`LiOcKn{HX&MkIrH`%PJUnJN zN;OUSJKEOtuyNS$uBAVdctSMa7mBEvy+%V#q)={bb<2}xzk)}<v$UQ!?1&12SekQ6 z_qw+1J@$Glb_9wM)P3d)?}u<qgUBlDAt?QXh~>#)%&JnId=v4Wy$}%<`8gt{%ojrT zE5lf##cJ5fZf3?Xe7L#{SHUYS*riC!$)js)=gRrRu~TYUfcc>ZLAP9$ZjxH~%4E~L z1ANfOUiU5NCH@%HLG{U~x!LX=!Tar;bEmHlE|8XPvpoA3Q1<s#(C3M|?EWqs!GI6r zYb6Vr>4@b<V2O3G41ftDr%-B#$RqHp{4pyNH;<S*p%u|-cFd343;5b3i&!t?Q}@h_ zo_W1&XmQ{g<;6|^nZL97;tXCOoZHaSk+ifLj7VWC$CP-#$lcKP=%M3L7P~@g&}qK! zr(>tP=tAHnq$l}R!zGhGbjqcgbCvQyo!!pA6WbaYvp<E4pt%k(EU6m$Et&sun9#%; z3DaUd6gRO=r(9c3Gnv=N2sbA~^Mu8~%d}CllJxP9%OSnz1G4=61}J=A5Ssu0sP1h0 z9~^CK=4$p|$fTawU=7L)7w*A7j0iw+bU1NO6tX&Ywd4&OYRBxr@@;KMoA-I6={P9V zx7D66pYiYS%I(APt*vmHRE?t3i}Ia9HDw_7Bb6i99B|2o*B_#Sa1a+!V8HRE;GeB2 zo{FP-p7`_OXqVkc{1W?tkVDyY0o{2H`32){sS;(f|JA0sqwE54j5gxC7Zf@2S7&Hq z%fX7fAz}$w_~#Z|{2!u@C|xJgr|SKi;_mOeKUB!tS-Y|KJ(VfwRLn(pef7gro-Q(4 zOZeqkdPzCk>Ayr1VEq0^JnQ`szLMYf$*_IDI{yM>T)vxY|I4LJsqrfgeOqwvXzgvu zQ-0`{Y7iNU*1N`3jY%^MSD8R=P#<jX*Kt^hD-WyiiQR79b$MJV7}-�rUSYRc-`l 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%0AsP#prKBQ|)?S}Y9*eIk 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 BvdXwSu>>gIR~>n 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 PZSHlwU7W(MlEC#p;WDU} literal 0 HcmV?d00001 diff --git a/tests/unit/test_network_lazy_wheel.py b/tests/unit/test_network_lazy_wheel.py index 331b87e7c88..cf0e6213d3f 100644 --- a/tests/unit/test_network_lazy_wheel.py +++ b/tests/unit/test_network_lazy_wheel.py @@ -8,7 +8,7 @@ dist_from_wheel_url, ) from pip._internal.network.session import PipSession -from tests.lib.requests_mocks import MockResponse +from tests.lib.server import file_response MYPY_0_782_WHL = ( 'https://files.pythonhosted.org/packages/9d/65/' @@ -28,6 +28,16 @@ def session(): return PipSession() +@fixture +def mypy_whl_no_range(mock_server, shared_data): + mypy_whl = shared_data.packages / 'mypy-0.782-py3-none-any.whl' + mock_server.set_responses([file_response(mypy_whl)]) + mock_server.start() + base_address = 'http://{}:{}'.format(mock_server.host, mock_server.port) + yield "{}/{}".format(base_address, 'mypy-0.782-py3-none-any.whl') + mock_server.stop() + + @mark.network def test_dist_from_wheel_url(session): """Test if the acquired distribution contain correct information.""" @@ -38,12 +48,10 @@ def test_dist_from_wheel_url(session): assert set(dist.requires(dist.extras)) == MYPY_0_782_REQS -@mark.network -def test_dist_from_wheel_url_no_range(session, monkeypatch): +def test_dist_from_wheel_url_no_range(session, mypy_whl_no_range): """Test handling when HTTP range requests are not supported.""" - monkeypatch.setattr(session, 'head', lambda *a, **kw: MockResponse(b'')) with raises(HTTPRangeRequestUnsupported): - dist_from_wheel_url('mypy', MYPY_0_782_WHL, session) + dist_from_wheel_url('mypy', mypy_whl_no_range, session) @mark.network From 7ddbcc2e67889b53a98aa115dfc01542a39c00ce Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Fri, 17 Jul 2020 03:25:53 +0530 Subject: [PATCH 2270/3170] Return early for clarity --- src/pip/_internal/commands/install.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index c89f4836e20..0b507a30a74 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -507,7 +507,7 @@ def _handle_target_dir(self, target_dir, target_temp_dir, upgrade): def _determine_conflicts(self, to_install): # type: (List[InstallRequirement]) -> Optional[ConflictDetails] try: - conflict_details = check_install_conflicts(to_install) + return check_install_conflicts(to_install) except Exception: logger.error( "Error while checking for conflicts. Please file an issue on " @@ -515,7 +515,6 @@ def _determine_conflicts(self, to_install): exc_info=True ) return None - return conflict_details def _warn_about_conflicts(self, conflict_details): # type: (ConflictDetails) -> None From b419ca73175aaedecd7de3400202c69079804e2b Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Fri, 17 Jul 2020 05:40:46 +0530 Subject: [PATCH 2271/3170] Fix the link on new-resolver dependency conflicts --- src/pip/_internal/resolution/resolvelib/factory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 81f887c6b1e..90c03e3f9f2 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -453,5 +453,5 @@ def readable_form(cand): return DistributionNotFound( "ResolutionImpossible For help visit: " "https://pip.pypa.io/en/stable/user_guide/" - "#dependency-conflicts-resolution-impossible" + "#fixing-conflicting-dependencies" ) From 97645f1022c65e9d331913f8700f3c66d7826b8a Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Fri, 17 Jul 2020 06:35:59 +0530 Subject: [PATCH 2272/3170] Cleanup NEWS file --- NEWS.rst | 27 +++++++-------------------- 1 file changed, 7 insertions(+), 20 deletions(-) diff --git a/NEWS.rst b/NEWS.rst index ac3c8615375..f5ffec1bcef 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,38 +1,25 @@ +.. NOTE: You should *NOT* be adding new change log entries to this file, this + file is managed by towncrier. You *may* edit previous change logs to + fix problems like typo corrections or such. + To add a new change log entry, please see + https://pip.pypa.io/en/latest/development/contributing/#news-entries +.. towncrier release notes start + 20.2b1 (2020-05-21) =================== -Deprecations and Removals -------------------------- - -- Drop parallelization from ``pip list --outdated``. (`#8167 <https://github.com/pypa/pip/issues/8167>`_) - Bug Fixes --------- - Correctly treat wheels containing non-ASCII file contents so they can be installed on Windows. (`#5712 <https://github.com/pypa/pip/issues/5712>`_) -- Revert building of local directories in place, restoring the pre-20.1 - behaviour of copying to a temporary directory. (`#7555 <https://github.com/pypa/pip/issues/7555>`_) - Prompt the user for password if the keyring backend doesn't return one (`#7998 <https://github.com/pypa/pip/issues/7998>`_) -- Fix metadata permission issues when umask has the executable bit set. (`#8164 <https://github.com/pypa/pip/issues/8164>`_) -- Avoid unnecessary message about the wheel package not being installed - when a wheel would not have been built. Additionally, clarify the message. (`#8178 <https://github.com/pypa/pip/issues/8178>`_) Improved Documentation ---------------------- - Add GitHub issue template for reporting when the dependency resolver fails (`#8207 <https://github.com/pypa/pip/issues/8207>`_) - -.. NOTE: You should *NOT* be adding new change log entries to this file, this - file is managed by towncrier. You *may* edit previous change logs to - fix problems like typo corrections or such. - - To add a new change log entry, please see - https://pip.pypa.io/en/latest/development/contributing/#news-entries - -.. towncrier release notes start - 20.1.1 (2020-05-19) =================== From c8b98f2d3ae2018222d41492744851c1b6de2233 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Fri, 17 Jul 2020 06:41:41 +0530 Subject: [PATCH 2273/3170] :art: while I'm at it --- NEWS.rst | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/NEWS.rst b/NEWS.rst index f5ffec1bcef..70f64faa8a9 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,8 +1,12 @@ -.. NOTE: You should *NOT* be adding new change log entries to this file, this - file is managed by towncrier. You *may* edit previous change logs to - fix problems like typo corrections or such. - To add a new change log entry, please see - https://pip.pypa.io/en/latest/development/contributing/#news-entries +.. note + + You should *NOT* be adding new change log entries to this file, this + file is managed by towncrier. You *may* edit previous change logs to + fix problems like typo corrections or such. + + To add a new change log entry, please see + https://pip.pypa.io/en/latest/development/contributing/#news-entries + .. towncrier release notes start 20.2b1 (2020-05-21) From cb8d81d1356aadf74953bc2a72fc7d10dd1eab17 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Wed, 10 Jun 2020 23:11:28 +0700 Subject: [PATCH 2274/3170] Nitpick logging calls --- news/4aa26440-8fdd-4a8b-979e-64f975bf7a9f.trivial | 0 src/pip/_internal/cli/cmdoptions.py | 3 --- src/pip/_internal/commands/configuration.py | 6 ++---- src/pip/_internal/commands/install.py | 7 +++---- src/pip/_internal/operations/build/metadata.py | 3 --- src/pip/_internal/operations/build/wheel.py | 2 +- src/pip/_internal/resolution/legacy/resolver.py | 2 +- src/pip/_internal/utils/compat.py | 6 ++---- src/pip/_internal/utils/compatibility_tags.py | 3 --- tests/unit/test_compat.py | 3 +-- 10 files changed, 10 insertions(+), 25 deletions(-) create mode 100644 news/4aa26440-8fdd-4a8b-979e-64f975bf7a9f.trivial diff --git a/news/4aa26440-8fdd-4a8b-979e-64f975bf7a9f.trivial b/news/4aa26440-8fdd-4a8b-979e-64f975bf7a9f.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index 2a4c230f6b5..31f07352cf6 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -12,7 +12,6 @@ from __future__ import absolute_import -import logging import os import textwrap import warnings @@ -35,8 +34,6 @@ from optparse import OptionParser, Values from pip._internal.cli.parser import ConfigOptionParser -logger = logging.getLogger(__name__) - def raise_option_error(parser, option, msg): # type: (OptionParser, Option, str) -> None diff --git a/src/pip/_internal/commands/configuration.py b/src/pip/_internal/commands/configuration.py index c5d118a5ea7..ed150afff18 100644 --- a/src/pip/_internal/commands/configuration.py +++ b/src/pip/_internal/commands/configuration.py @@ -266,10 +266,8 @@ def _save_configuration(self): try: self.configuration.save() except Exception: - logger.error( - "Unable to save configuration. Please report this as a bug.", - exc_info=True - ) + logger.exception("Unable to save configuration. " + "Please report this as a bug.") raise PipError("Internal Error.") def _determine_editor(self, options): diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 0b507a30a74..2f99b3ff495 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -240,7 +240,7 @@ def run(self, options, args): install_options = options.install_options or [] - logger.debug("Using {}".format(get_pip_version())) + logger.debug("Using %s", get_pip_version()) options.use_user_site = decide_user_install( options.use_user_site, prefix_path=options.prefix_path, @@ -509,10 +509,9 @@ def _determine_conflicts(self, to_install): try: return check_install_conflicts(to_install) except Exception: - logger.error( + logger.exception( "Error while checking for conflicts. Please file an issue on " - "pip's issue tracker: https://github.com/pypa/pip/issues/new", - exc_info=True + "pip's issue tracker: https://github.com/pypa/pip/issues/new" ) return None diff --git a/src/pip/_internal/operations/build/metadata.py b/src/pip/_internal/operations/build/metadata.py index b13fbdef933..cf52f8d8f63 100644 --- a/src/pip/_internal/operations/build/metadata.py +++ b/src/pip/_internal/operations/build/metadata.py @@ -1,7 +1,6 @@ """Metadata generation logic for source distributions. """ -import logging import os from pip._internal.utils.subprocess import runner_with_spinner_message @@ -12,8 +11,6 @@ from pip._internal.build_env import BuildEnvironment from pip._vendor.pep517.wrappers import Pep517HookCaller -logger = logging.getLogger(__name__) - def generate_metadata(build_env, backend): # type: (BuildEnvironment, Pep517HookCaller) -> str diff --git a/src/pip/_internal/operations/build/wheel.py b/src/pip/_internal/operations/build/wheel.py index 1266ce05c6f..0c28c4989dc 100644 --- a/src/pip/_internal/operations/build/wheel.py +++ b/src/pip/_internal/operations/build/wheel.py @@ -27,7 +27,7 @@ def build_wheel_pep517( if build_options: # PEP 517 does not support --build-options logger.error('Cannot build wheel for %s using PEP 517 when ' - '--build-option is present' % (name,)) + '--build-option is present', name) return None try: logger.debug('Destination directory: %s', tempd) diff --git a/src/pip/_internal/resolution/legacy/resolver.py b/src/pip/_internal/resolution/legacy/resolver.py index 51a1d0b5dcb..c9b4c661630 100644 --- a/src/pip/_internal/resolution/legacy/resolver.py +++ b/src/pip/_internal/resolution/legacy/resolver.py @@ -438,7 +438,7 @@ def add_req(subreq, extras_requested): ) for missing in missing_requested: logger.warning( - '%s does not provide the extra \'%s\'', + "%s does not provide the extra '%s'", dist, missing ) diff --git a/src/pip/_internal/utils/compat.py b/src/pip/_internal/utils/compat.py index d939e21fe2a..31fd57d3d11 100644 --- a/src/pip/_internal/utils/compat.py +++ b/src/pip/_internal/utils/compat.py @@ -121,10 +121,8 @@ def str_to_display(data, desc=None): try: decoded_data = data.decode(encoding) except UnicodeDecodeError: - if desc is None: - desc = 'Bytes object' - msg_format = '{} does not appear to be encoded as %s'.format(desc) - logger.warning(msg_format, encoding) + logger.warning('%s does not appear to be encoded as %s', + desc or 'Bytes object', encoding) decoded_data = data.decode(encoding, errors=backslashreplace_decode) # Make sure we can print the output, by encoding it to the output diff --git a/src/pip/_internal/utils/compatibility_tags.py b/src/pip/_internal/utils/compatibility_tags.py index 47d04f078c1..4f21874ec6b 100644 --- a/src/pip/_internal/utils/compatibility_tags.py +++ b/src/pip/_internal/utils/compatibility_tags.py @@ -3,7 +3,6 @@ from __future__ import absolute_import -import logging import re from pip._vendor.packaging.tags import ( @@ -23,8 +22,6 @@ from pip._vendor.packaging.tags import PythonVersion -logger = logging.getLogger(__name__) - _osx_arch_pat = re.compile(r'(.+)_(\d+)_(\d+)_(.+)') diff --git a/tests/unit/test_compat.py b/tests/unit/test_compat.py index b13087a1dd7..9c54d3d17eb 100644 --- a/tests/unit/test_compat.py +++ b/tests/unit/test_compat.py @@ -125,8 +125,7 @@ def test_console_to_str_warning(monkeypatch): some_bytes = b"a\xE9b" def check_warning(msg, *args, **kwargs): - assert msg.startswith( - "Subprocess output does not appear to be encoded as") + assert args[0] == 'Subprocess output' monkeypatch.setattr(locale, 'getpreferredencoding', lambda: 'utf-8') monkeypatch.setattr(pip_compat.logger, 'warning', check_warning) From 05bdc69aa3193b7ba3940e6d4e19bac639274d6e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Sun, 14 Jun 2020 10:53:24 +0700 Subject: [PATCH 2275/3170] Make test more explicit --- tests/unit/test_compat.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/unit/test_compat.py b/tests/unit/test_compat.py index 9c54d3d17eb..1b7a482a2a3 100644 --- a/tests/unit/test_compat.py +++ b/tests/unit/test_compat.py @@ -125,6 +125,7 @@ def test_console_to_str_warning(monkeypatch): some_bytes = b"a\xE9b" def check_warning(msg, *args, **kwargs): + assert 'does not appear to be encoded as' in msg assert args[0] == 'Subprocess output' monkeypatch.setattr(locale, 'getpreferredencoding', lambda: 'utf-8') From f8b06a3906fe7c7b9f004a9731d4ef0000a22605 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Wed, 17 Jun 2020 22:53:30 +0700 Subject: [PATCH 2276/3170] Enable flake8-logging-format --- .pre-commit-config.yaml | 3 ++- setup.cfg | 8 ++++++++ src/pip/_internal/cache.py | 8 +++----- src/pip/_internal/cli/cmdoptions.py | 4 ++-- src/pip/_internal/commands/cache.py | 5 ++--- src/pip/_internal/commands/configuration.py | 5 ++--- src/pip/_internal/commands/debug.py | 17 +++++------------ src/pip/_internal/commands/install.py | 2 +- src/pip/_internal/index/collector.py | 4 +--- src/pip/_internal/models/search_scope.py | 4 ++-- src/pip/_internal/operations/install/wheel.py | 4 +--- src/pip/_internal/req/constructors.py | 4 ++-- src/pip/_internal/req/req_uninstall.py | 4 +--- .../_internal/resolution/resolvelib/factory.py | 12 ++++++------ src/pip/_internal/utils/subprocess.py | 6 ++---- src/pip/_internal/utils/temp_dir.py | 10 ++++------ 16 files changed, 44 insertions(+), 56 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index d3a6244f39d..04c72d8e3c1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -21,7 +21,8 @@ repos: hooks: - id: flake8 additional_dependencies: [ - 'flake8-bugbear==20.1.4' + 'flake8-bugbear==20.1.4', + 'flake8-logging-format==0.6.0', ] exclude: tests/data diff --git a/setup.cfg b/setup.cfg index 2415d2d2b89..45fd58a3e7a 100644 --- a/setup.cfg +++ b/setup.cfg @@ -23,7 +23,14 @@ exclude = .scratch, _vendor, data +enable-extensions = G +ignore = + G200, G202, + # pycodestyle checks ignored in the default configuration + E121, E123, E126, E133, E226, E241, E242, E704, W503, W504, W505, per-file-ignores = + # G: The plugin logging-format treats every .log and .error as logging. + noxfile.py: G # B011: Do not call assert False since python -O removes these calls tests/*: B011 # TODO: Remove IOError from except (OSError, IOError) blocks in @@ -33,6 +40,7 @@ per-file-ignores = src/pip/_internal/utils/filesystem.py: B014 src/pip/_internal/network/cache.py: B014 src/pip/_internal/utils/misc.py: B014 + [mypy] follow_imports = silent ignore_missing_imports = True diff --git a/src/pip/_internal/cache.py b/src/pip/_internal/cache.py index 4a793b1f3e4..900c2a541a3 100644 --- a/src/pip/_internal/cache.py +++ b/src/pip/_internal/cache.py @@ -228,11 +228,9 @@ def get( continue if canonicalize_name(wheel.name) != canonical_package_name: logger.debug( - "Ignoring cached wheel {} for {} as it " - "does not match the expected distribution name {}.".format( - wheel_name, link, package_name - ) - ) + "Ignoring cached wheel %s for %s as it " + "does not match the expected distribution name %s.", + wheel_name, link, package_name) continue if not wheel.supported(supported_tags): # Built for a different python/arch/etc diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index 31f07352cf6..b2f63bb264e 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -831,11 +831,11 @@ def _handle_merge_hash(option, opt_str, value, parser): try: algo, digest = value.split(':', 1) except ValueError: - parser.error('Arguments to {} must be a hash name ' + parser.error('Arguments to {} must be a hash name ' # noqa 'followed by a value, like --hash=sha256:' 'abcde...'.format(opt_str)) if algo not in STRONG_HASHES: - parser.error('Allowed hash algorithms for {} are {}.'.format( + parser.error('Allowed hash algorithms for {} are {}.'.format( # noqa opt_str, ', '.join(STRONG_HASHES))) parser.values.hashes.setdefault(algo, []).append(digest) diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index 209614ff6d4..013d088824d 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -59,9 +59,8 @@ def run(self, options, args): # Determine action if not args or args[0] not in handlers: - logger.error("Need an action ({}) to perform.".format( - ", ".join(sorted(handlers))) - ) + logger.error("Need an action (%s) to perform.", + ", ".join(sorted(handlers))) return ERROR action = args[0] diff --git a/src/pip/_internal/commands/configuration.py b/src/pip/_internal/commands/configuration.py index ed150afff18..8f442689d79 100644 --- a/src/pip/_internal/commands/configuration.py +++ b/src/pip/_internal/commands/configuration.py @@ -105,9 +105,8 @@ def run(self, options, args): # Determine action if not args or args[0] not in handlers: - logger.error("Need an action ({}) to perform.".format( - ", ".join(sorted(handlers))) - ) + logger.error("Need an action (%s) to perform.", + ", ".join(sorted(handlers))) return ERROR action = args[0] diff --git a/src/pip/_internal/commands/debug.py b/src/pip/_internal/commands/debug.py index 119569b1886..ff369d7d967 100644 --- a/src/pip/_internal/commands/debug.py +++ b/src/pip/_internal/commands/debug.py @@ -29,7 +29,7 @@ def show_value(name, value): # type: (str, Optional[str]) -> None - logger.info('{}: {}'.format(name, value)) + logger.info('%s: %s', name, value) def show_sys_implementation(): @@ -102,9 +102,9 @@ def get_vendor_version_from_module(module_name): def show_actual_vendor_versions(vendor_txt_versions): # type: (Dict[str, str]) -> None - # Logs the actual version and print extra info - # if there is a conflict or if the actual version could not be imported. - + """Log the actual version and print extra info if there is + a conflict or if the actual version could not be imported. + """ for module_name, expected_version in vendor_txt_versions.items(): extra_message = '' actual_version = get_vendor_version_from_module(module_name) @@ -115,14 +115,7 @@ def show_actual_vendor_versions(vendor_txt_versions): elif actual_version != expected_version: extra_message = ' (CONFLICT: vendor.txt suggests version should'\ ' be {})'.format(expected_version) - - logger.info( - '{name}=={actual}{extra}'.format( - name=module_name, - actual=actual_version, - extra=extra_message - ) - ) + logger.info('%s==%s%s', module_name, actual_version, extra_message) def show_vendor_versions(): diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 2f99b3ff495..ac60aa53196 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -436,7 +436,7 @@ def run(self, options, args): message = create_env_error_message( error, show_traceback, options.use_user_site, ) - logger.error(message, exc_info=show_traceback) + logger.error(message, exc_info=show_traceback) # noqa return ERROR diff --git a/src/pip/_internal/index/collector.py b/src/pip/_internal/index/collector.py index 068bad5ce50..e99a43d1c59 100644 --- a/src/pip/_internal/index/collector.py +++ b/src/pip/_internal/index/collector.py @@ -529,9 +529,7 @@ def sort_path(path): urls.append(url) else: logger.warning( - "Path '{0}' is ignored: " - "it is a directory.".format(path), - ) + "Path '%s' is ignored: it is a directory.", path) elif os.path.isfile(path): sort_path(path) else: diff --git a/src/pip/_internal/models/search_scope.py b/src/pip/_internal/models/search_scope.py index 965fcf9c3ae..d732504e6f5 100644 --- a/src/pip/_internal/models/search_scope.py +++ b/src/pip/_internal/models/search_scope.py @@ -95,8 +95,8 @@ def get_formatted_locations(self): # exceptions for malformed URLs if not purl.scheme and not purl.netloc: logger.warning( - 'The index url "{}" seems invalid, ' - 'please provide a scheme.'.format(redacted_index_url)) + 'The index url "%s" seems invalid, ' + 'please provide a scheme.', redacted_index_url) redacted_index_urls.append(redacted_index_url) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 1bc031f376c..8f73a88b074 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -308,9 +308,7 @@ def get_csv_rows_for_installed( installed_rows = [] # type: List[InstalledCSVRow] for row in old_csv_rows: if len(row) > 3: - logger.warning( - 'RECORD line has more than three elements: {}'.format(row) - ) + logger.warning('RECORD line has more than three elements: %s', row) old_record_path = _parse_record_path(row[0]) new_record_path = installed.pop(old_record_path, old_record_path) if new_record_path in changed: diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index 857f7fff5aa..9fe4846405f 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -173,8 +173,8 @@ def deduce_helpful_msg(req): " the packages specified within it." ).format(req) except RequirementParseError: - logger.debug("Cannot parse '{}' as requirements \ - file".format(req), exc_info=True) + logger.debug("Cannot parse '%s' as requirements file", + req, exc_info=True) else: msg += " File '{}' does not exist.".format(req) return msg diff --git a/src/pip/_internal/req/req_uninstall.py b/src/pip/_internal/req/req_uninstall.py index 559061a6296..38b08213e65 100644 --- a/src/pip/_internal/req/req_uninstall.py +++ b/src/pip/_internal/req/req_uninstall.py @@ -612,9 +612,7 @@ def remove(self): # If the file doesn't exist, log a warning and return if not os.path.isfile(self.file): logger.warning( - "Cannot remove entries from nonexistent file {}".format( - self.file) - ) + "Cannot remove entries from nonexistent file %s", self.file) return with open(self.file, 'rb') as fh: # windows uses '\r\n' with py3k, but uses '\n' with py2.x diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 90c03e3f9f2..0d73ff978ef 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -377,13 +377,13 @@ def get_installation_error(self, e): # satisfied. We just report that case. if len(e.causes) == 1: req, parent = e.causes[0] + if parent is None: + req_disp = str(req) + else: + req_disp = '{} (from {})'.format(req, parent.name) logger.critical( - "Could not find a version that satisfies " + - "the requirement " + - str(req) + - ("" if parent is None else " (from {})".format( - parent.name - )) + "Could not find a version that satisfies the requirement %s", + req_disp, ) return DistributionNotFound( 'No matching distribution found for {}'.format(req) diff --git a/src/pip/_internal/utils/subprocess.py b/src/pip/_internal/utils/subprocess.py index 1cffec085c5..0f3b0ae5ffc 100644 --- a/src/pip/_internal/utils/subprocess.py +++ b/src/pip/_internal/utils/subprocess.py @@ -241,10 +241,8 @@ def call_subprocess( ).format(proc.returncode, command_desc) raise InstallationError(exc_msg) elif on_returncode == 'warn': - subprocess_logger.warning( - 'Command "{}" had error code {} in {}'.format( - command_desc, proc.returncode, cwd) - ) + subprocess_logger.warning('Command "%s" had error code %s in %s', + command_desc, proc.returncode, cwd) elif on_returncode == 'ignore': pass else: diff --git a/src/pip/_internal/utils/temp_dir.py b/src/pip/_internal/utils/temp_dir.py index 54c3140110c..a24badfcac9 100644 --- a/src/pip/_internal/utils/temp_dir.py +++ b/src/pip/_internal/utils/temp_dir.py @@ -183,9 +183,8 @@ def _create(self, kind): # scripts, so we canonicalize the path by traversing potential # symlinks here. path = os.path.realpath( - tempfile.mkdtemp(prefix="pip-{}-".format(kind)) - ) - logger.debug("Created temporary directory: {}".format(path)) + tempfile.mkdtemp(prefix="pip-{}-".format(kind))) + logger.debug("Created temporary directory: %s", path) return path def cleanup(self): @@ -267,8 +266,7 @@ def _create(self, kind): else: # Final fallback on the default behavior. path = os.path.realpath( - tempfile.mkdtemp(prefix="pip-{}-".format(kind)) - ) + tempfile.mkdtemp(prefix="pip-{}-".format(kind))) - logger.debug("Created temporary directory: {}".format(path)) + logger.debug("Created temporary directory: %s", path) return path From 6fa4a9a0a7d4ba30792e01aaf332f11d197a1b6a Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Wed, 17 Jun 2020 23:02:47 +0530 Subject: [PATCH 2277/3170] Clean up code style changes Toward minimizing style changes in the overall PR diff, or toward consistency with the future use of black (in cases where I wasn't sure of a good way to minimize the diff). --- src/pip/_internal/cache.py | 3 ++- src/pip/_internal/commands/cache.py | 6 ++++-- src/pip/_internal/commands/configuration.py | 11 +++++++---- src/pip/_internal/index/collector.py | 3 ++- src/pip/_internal/req/constructors.py | 5 +++-- src/pip/_internal/req/req_uninstall.py | 3 ++- src/pip/_internal/utils/compat.py | 7 +++++-- src/pip/_internal/utils/subprocess.py | 8 ++++++-- src/pip/_internal/utils/temp_dir.py | 6 ++++-- 9 files changed, 35 insertions(+), 17 deletions(-) diff --git a/src/pip/_internal/cache.py b/src/pip/_internal/cache.py index 900c2a541a3..07db948b9bf 100644 --- a/src/pip/_internal/cache.py +++ b/src/pip/_internal/cache.py @@ -230,7 +230,8 @@ def get( logger.debug( "Ignoring cached wheel %s for %s as it " "does not match the expected distribution name %s.", - wheel_name, link, package_name) + wheel_name, link, package_name, + ) continue if not wheel.supported(supported_tags): # Built for a different python/arch/etc diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index 013d088824d..747277f6eaa 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -59,8 +59,10 @@ def run(self, options, args): # Determine action if not args or args[0] not in handlers: - logger.error("Need an action (%s) to perform.", - ", ".join(sorted(handlers))) + logger.error( + "Need an action (%s) to perform.", + ", ".join(sorted(handlers)), + ) return ERROR action = args[0] diff --git a/src/pip/_internal/commands/configuration.py b/src/pip/_internal/commands/configuration.py index 8f442689d79..f9b3ab79d0b 100644 --- a/src/pip/_internal/commands/configuration.py +++ b/src/pip/_internal/commands/configuration.py @@ -105,8 +105,10 @@ def run(self, options, args): # Determine action if not args or args[0] not in handlers: - logger.error("Need an action (%s) to perform.", - ", ".join(sorted(handlers))) + logger.error( + "Need an action (%s) to perform.", + ", ".join(sorted(handlers)), + ) return ERROR action = args[0] @@ -265,8 +267,9 @@ def _save_configuration(self): try: self.configuration.save() except Exception: - logger.exception("Unable to save configuration. " - "Please report this as a bug.") + logger.exception( + "Unable to save configuration. Please report this as a bug." + ) raise PipError("Internal Error.") def _determine_editor(self, options): diff --git a/src/pip/_internal/index/collector.py b/src/pip/_internal/index/collector.py index e99a43d1c59..6c35fc66076 100644 --- a/src/pip/_internal/index/collector.py +++ b/src/pip/_internal/index/collector.py @@ -529,7 +529,8 @@ def sort_path(path): urls.append(url) else: logger.warning( - "Path '%s' is ignored: it is a directory.", path) + "Path '%s' is ignored: it is a directory.", path, + ) elif os.path.isfile(path): sort_path(path) else: diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index 9fe4846405f..7a4641ef5a1 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -173,8 +173,9 @@ def deduce_helpful_msg(req): " the packages specified within it." ).format(req) except RequirementParseError: - logger.debug("Cannot parse '%s' as requirements file", - req, exc_info=True) + logger.debug( + "Cannot parse '%s' as requirements file", req, exc_info=True + ) else: msg += " File '{}' does not exist.".format(req) return msg diff --git a/src/pip/_internal/req/req_uninstall.py b/src/pip/_internal/req/req_uninstall.py index 38b08213e65..69719d338e6 100644 --- a/src/pip/_internal/req/req_uninstall.py +++ b/src/pip/_internal/req/req_uninstall.py @@ -612,7 +612,8 @@ def remove(self): # If the file doesn't exist, log a warning and return if not os.path.isfile(self.file): logger.warning( - "Cannot remove entries from nonexistent file %s", self.file) + "Cannot remove entries from nonexistent file %s", self.file + ) return with open(self.file, 'rb') as fh: # windows uses '\r\n' with py3k, but uses '\n' with py2.x diff --git a/src/pip/_internal/utils/compat.py b/src/pip/_internal/utils/compat.py index 31fd57d3d11..89c5169af4e 100644 --- a/src/pip/_internal/utils/compat.py +++ b/src/pip/_internal/utils/compat.py @@ -121,8 +121,11 @@ def str_to_display(data, desc=None): try: decoded_data = data.decode(encoding) except UnicodeDecodeError: - logger.warning('%s does not appear to be encoded as %s', - desc or 'Bytes object', encoding) + logger.warning( + '%s does not appear to be encoded as %s', + desc or 'Bytes object', + encoding, + ) decoded_data = data.decode(encoding, errors=backslashreplace_decode) # Make sure we can print the output, by encoding it to the output diff --git a/src/pip/_internal/utils/subprocess.py b/src/pip/_internal/utils/subprocess.py index 0f3b0ae5ffc..d398e68da53 100644 --- a/src/pip/_internal/utils/subprocess.py +++ b/src/pip/_internal/utils/subprocess.py @@ -241,8 +241,12 @@ def call_subprocess( ).format(proc.returncode, command_desc) raise InstallationError(exc_msg) elif on_returncode == 'warn': - subprocess_logger.warning('Command "%s" had error code %s in %s', - command_desc, proc.returncode, cwd) + subprocess_logger.warning( + 'Command "%s" had error code %s in %s', + command_desc, + proc.returncode, + cwd, + ) elif on_returncode == 'ignore': pass else: diff --git a/src/pip/_internal/utils/temp_dir.py b/src/pip/_internal/utils/temp_dir.py index a24badfcac9..03aa8286670 100644 --- a/src/pip/_internal/utils/temp_dir.py +++ b/src/pip/_internal/utils/temp_dir.py @@ -183,7 +183,8 @@ def _create(self, kind): # scripts, so we canonicalize the path by traversing potential # symlinks here. path = os.path.realpath( - tempfile.mkdtemp(prefix="pip-{}-".format(kind))) + tempfile.mkdtemp(prefix="pip-{}-".format(kind)) + ) logger.debug("Created temporary directory: %s", path) return path @@ -266,7 +267,8 @@ def _create(self, kind): else: # Final fallback on the default behavior. path = os.path.realpath( - tempfile.mkdtemp(prefix="pip-{}-".format(kind))) + tempfile.mkdtemp(prefix="pip-{}-".format(kind)) + ) logger.debug("Created temporary directory: %s", path) return path From d363b4a42719bf5fa08cebfc90a6685773176175 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Sat, 18 Jul 2020 17:00:42 +0700 Subject: [PATCH 2278/3170] Replace tabs by spaces for consistency --- news/a64123c8-5a0d-48e2-8def-6b56b9d4b8dc.trivial | 0 .../pep518_with_extra_and_markers-1.0/pyproject.toml | 11 ++++++----- tox.ini | 4 ++-- 3 files changed, 8 insertions(+), 7 deletions(-) create mode 100644 news/a64123c8-5a0d-48e2-8def-6b56b9d4b8dc.trivial diff --git a/news/a64123c8-5a0d-48e2-8def-6b56b9d4b8dc.trivial b/news/a64123c8-5a0d-48e2-8def-6b56b9d4b8dc.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/data/src/pep518_with_extra_and_markers-1.0/pyproject.toml b/tests/data/src/pep518_with_extra_and_markers-1.0/pyproject.toml index c4b9b118f54..69079726c64 100644 --- a/tests/data/src/pep518_with_extra_and_markers-1.0/pyproject.toml +++ b/tests/data/src/pep518_with_extra_and_markers-1.0/pyproject.toml @@ -1,7 +1,8 @@ [build-system] -requires=[ - "requires_simple_extra[extra]", - "simplewheel==1.0; python_version < '3'", - "simplewheel==2.0; python_version >= '3'", - "setuptools", "wheel", +requires = [ + "requires_simple_extra[extra]", + "simplewheel==1.0; python_version < '3'", + "simplewheel==2.0; python_version >= '3'", + "setuptools", + "wheel", ] diff --git a/tox.ini b/tox.ini index d3f4993bd53..82e9abc68d7 100644 --- a/tox.ini +++ b/tox.ini @@ -25,8 +25,8 @@ setenv = LC_CTYPE = en_US.UTF-8 deps = -r{toxinidir}/tools/requirements/tests.txt commands_pre = - python -c 'import shutil, sys; shutil.rmtree(sys.argv[1], ignore_errors=True)' {toxinidir}/tests/data/common_wheels - {[helpers]pip} wheel -w {toxinidir}/tests/data/common_wheels -r {toxinidir}/tools/requirements/tests-common_wheels.txt + python -c 'import shutil, sys; shutil.rmtree(sys.argv[1], ignore_errors=True)' {toxinidir}/tests/data/common_wheels + {[helpers]pip} wheel -w {toxinidir}/tests/data/common_wheels -r {toxinidir}/tools/requirements/tests-common_wheels.txt commands = pytest --timeout 300 [] install_command = {[helpers]pip} install {opts} {packages} list_dependencies_command = {[helpers]pip} freeze --all From 462d6ca5904f934f66c62db640ee2d5e08b9bdea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sun, 31 May 2020 14:01:29 +0200 Subject: [PATCH 2279/3170] Deprecate install fallback when bdist_wheel fails --- news/8368.removal | 2 ++ src/pip/_internal/commands/install.py | 24 +++++++++++++++++++++--- 2 files changed, 23 insertions(+), 3 deletions(-) create mode 100644 news/8368.removal diff --git a/news/8368.removal b/news/8368.removal new file mode 100644 index 00000000000..646c384d78a --- /dev/null +++ b/news/8368.removal @@ -0,0 +1,2 @@ +Deprecate legacy setup.py install when building a wheel failed for source +distributions without pyproject.toml diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index ac60aa53196..384099ae60d 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -21,6 +21,7 @@ from pip._internal.operations.check import check_install_conflicts from pip._internal.req import install_given_reqs from pip._internal.req.req_tracker import get_requirement_tracker +from pip._internal.utils.deprecation import deprecated from pip._internal.utils.distutils_args import parse_distutils_args from pip._internal.utils.filesystem import test_writable_dir from pip._internal.utils.misc import ( @@ -355,9 +356,6 @@ def run(self, options, args): # If we're using PEP 517, we cannot do a direct install # so we fail here. - # We don't care about failures building legacy - # requirements, as we'll fall through to a direct - # install for those. pep517_build_failures = [ r for r in build_failures if r.use_pep517 ] @@ -368,6 +366,26 @@ def run(self, options, args): ", ".join(r.name # type: ignore for r in pep517_build_failures))) + # For now, we just warn about failures building legacy + # requirements, as we'll fall through to a direct + # install for those. + legacy_build_failures = [ + r for r in build_failures if not r.use_pep517 + ] + if legacy_build_failures: + deprecated( + reason=( + "Could not build wheels for {} which do not use " + "PEP 517. pip will fall back to legacy setup.py " + "install for these.".format( + ", ".join(r.name for r in legacy_build_failures) + ) + ), + replacement="to fix the wheel build issue reported above", + gone_in="21.0", + issue=8368, + ) + to_install = resolver.get_installation_order( requirement_set ) From fe5682627a5b0897ec2c6abe2360eb0ca46a1132 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sun, 31 May 2020 19:52:31 +0200 Subject: [PATCH 2280/3170] Quote 'setup.py install' when calling it legacy We want to make it clear that it is the setup.py install command we consider legacy, not setup.py itself. --- src/pip/_internal/commands/install.py | 4 ++-- src/pip/_internal/wheel_builder.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 384099ae60d..7ed6a05acf0 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -376,8 +376,8 @@ def run(self, options, args): deprecated( reason=( "Could not build wheels for {} which do not use " - "PEP 517. pip will fall back to legacy setup.py " - "install for these.".format( + "PEP 517. pip will fall back to legacy 'setup.py " + "install' for these.".format( ", ".join(r.name for r in legacy_build_failures) ) ), diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index b9929815810..fa08016bdfb 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -80,7 +80,7 @@ def _should_build( if not req.use_pep517 and not is_wheel_installed(): # we don't build legacy requirements if wheel is not installed logger.info( - "Using legacy setup.py install for %s, " + "Using legacy 'setup.py install' for %s, " "since package 'wheel' is not installed.", req.name, ) return False From d924b16b0db39c316176027260d06c4c8f6f22de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sat, 18 Jul 2020 13:45:23 +0200 Subject: [PATCH 2281/3170] Give mypy some love after rebase --- src/pip/_internal/commands/install.py | 25 ++++++++++++++----------- 1 file changed, 14 insertions(+), 11 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 7ed6a05acf0..454c0b70c4c 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -356,29 +356,32 @@ def run(self, options, args): # If we're using PEP 517, we cannot do a direct install # so we fail here. - pep517_build_failures = [ - r for r in build_failures if r.use_pep517 - ] - if pep517_build_failures: + pep517_build_failure_names = [ + r.name # type: ignore + for r in build_failures if r.use_pep517 + ] # type: List[str] + if pep517_build_failure_names: raise InstallationError( "Could not build wheels for {} which use" " PEP 517 and cannot be installed directly".format( - ", ".join(r.name # type: ignore - for r in pep517_build_failures))) + ", ".join(pep517_build_failure_names) + ) + ) # For now, we just warn about failures building legacy # requirements, as we'll fall through to a direct # install for those. - legacy_build_failures = [ - r for r in build_failures if not r.use_pep517 - ] - if legacy_build_failures: + legacy_build_failure_names = [ + r.name # type: ignore + for r in build_failures if not r.use_pep517 + ] # type: List[str] + if legacy_build_failure_names: deprecated( reason=( "Could not build wheels for {} which do not use " "PEP 517. pip will fall back to legacy 'setup.py " "install' for these.".format( - ", ".join(r.name for r in legacy_build_failures) + ", ".join(legacy_build_failure_names) ) ), replacement="to fix the wheel build issue reported above", From eb6cc7f439cd6d1da6e401aecbf5d2f3d1ca9074 Mon Sep 17 00:00:00 2001 From: Srinivas Nyayapati <shireenrao@gmail.com> Date: Thu, 16 Jul 2020 17:21:14 -0400 Subject: [PATCH 2282/3170] Add examples on how to install package extras and sdists to docs --- docs/html/reference/pip_install.rst | 9 ++++++--- news/8576.doc | 1 + 2 files changed, 7 insertions(+), 3 deletions(-) create mode 100644 news/8576.doc diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index 973de8701f5..d5c96ec29d8 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -895,7 +895,7 @@ Examples :: $ pip install SomePackage[PDF] - $ pip install git+https://git.repo/some_pkg.git#egg=SomePackage[PDF] + $ pip install "SomePackage[PDF] @ git+https://git.repo/SomePackage@master#subdirectory=subdir_path" $ pip install .[PDF] # project in current directory $ pip install SomePackage[PDF]==3.0 $ pip install SomePackage[PDF,EPUB] # multiple extras @@ -913,8 +913,11 @@ Examples :: - $ pip install SomeProject==1.0.4@http://my.package.repo//SomeProject-1.2.3-py33-none-any.whl - $ pip install "SomeProject==1.0.4 @ http://my.package.repo//SomeProject-1.2.3-py33-none-any.whl" + $ pip install SomeProject@http://my.package.repo//SomeProject-1.2.3-py33-none-any.whl + $ pip install "SomeProject @ http://my.package.repo//SomeProject-1.2.3-py33-none-any.whl" + $ pip install SomeProject@http://my.package.repo//1.2.3.tar.gz + $ pip install SomeProject@git+https://git.repo/some_pkg.git@1.3.1 + $ pip install SomeProject@git+https://git.repo/some_pkg.git@1.3.1#7921be1537eac1e97bc40179a57f0349c2aee67d #. Install from alternative package repositories. diff --git a/news/8576.doc b/news/8576.doc new file mode 100644 index 00000000000..6c15ba72721 --- /dev/null +++ b/news/8576.doc @@ -0,0 +1 @@ +Add how to install package extras from git branch and source distributions in pip docs From 6ea15755ebea8814cb8660b2a39505d90a22ff9a Mon Sep 17 00:00:00 2001 From: Srinivas Nyayapati <shireenrao@gmail.com> Date: Fri, 17 Jul 2020 16:07:32 -0400 Subject: [PATCH 2283/3170] update based on review --- docs/html/reference/pip_install.rst | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index d5c96ec29d8..d6ad8fec1bc 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -880,6 +880,13 @@ Examples $ pip install -e path/to/project # project in another directory +#. Install a project from VCS + + :: + + $ pip install SomeProject@git+https://git.repo/some_pkg.git@1.3.1 + + #. Install a project from VCS in "editable" mode. See the sections on :ref:`VCS Support <VCS Support>` and :ref:`Editable Installs <editable-installs>`. :: @@ -916,8 +923,6 @@ Examples $ pip install SomeProject@http://my.package.repo//SomeProject-1.2.3-py33-none-any.whl $ pip install "SomeProject @ http://my.package.repo//SomeProject-1.2.3-py33-none-any.whl" $ pip install SomeProject@http://my.package.repo//1.2.3.tar.gz - $ pip install SomeProject@git+https://git.repo/some_pkg.git@1.3.1 - $ pip install SomeProject@git+https://git.repo/some_pkg.git@1.3.1#7921be1537eac1e97bc40179a57f0349c2aee67d #. Install from alternative package repositories. From 319e69f14f09610cd309d91aacb00c800095e08e Mon Sep 17 00:00:00 2001 From: Srinivas Nyayapati <shireenrao@gmail.com> Date: Fri, 17 Jul 2020 23:36:53 -0400 Subject: [PATCH 2284/3170] update news fragment --- news/8576.doc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/8576.doc b/news/8576.doc index 6c15ba72721..82347ec4bba 100644 --- a/news/8576.doc +++ b/news/8576.doc @@ -1 +1 @@ -Add how to install package extras from git branch and source distributions in pip docs +Document how to install package extras from git branch and source distributions. From 168460a8e04de4bd7dc80f2fd97c0abed93db508 Mon Sep 17 00:00:00 2001 From: Srinivas Nyayapati <shireenrao@gmail.com> Date: Sat, 18 Jul 2020 12:25:15 -0400 Subject: [PATCH 2285/3170] fix url example --- docs/html/reference/pip_install.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index d6ad8fec1bc..7be7bcd256b 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -920,9 +920,9 @@ Examples :: - $ pip install SomeProject@http://my.package.repo//SomeProject-1.2.3-py33-none-any.whl - $ pip install "SomeProject @ http://my.package.repo//SomeProject-1.2.3-py33-none-any.whl" - $ pip install SomeProject@http://my.package.repo//1.2.3.tar.gz + $ pip install SomeProject@http://my.package.repo/SomeProject-1.2.3-py33-none-any.whl + $ pip install "SomeProject @ http://my.package.repo/SomeProject-1.2.3-py33-none-any.whl" + $ pip install SomeProject@http://my.package.repo/1.2.3.tar.gz #. Install from alternative package repositories. From 3eef588a03ad406794d7d35113f254756272aab5 Mon Sep 17 00:00:00 2001 From: Andy Freeland <andy@andyfreeland.net> Date: Sat, 18 Jul 2020 15:59:28 -0700 Subject: [PATCH 2286/3170] Support '--use-feature' in requirements files This patch adds support for `--use-feature` in requirements files so that a project that wants all contributors using the same pip features can specify it in the requirements file. For example, to ensure a requirements file uses the new resolver: ``` --use-feature=2020-resolver boto3 boto3==1.13.13 ``` This is a new version of #8293. --- news/8601.feature | 1 + src/pip/_internal/req/req_file.py | 1 + tests/unit/test_req_file.py | 4 ++++ 3 files changed, 6 insertions(+) create mode 100644 news/8601.feature diff --git a/news/8601.feature b/news/8601.feature new file mode 100644 index 00000000000..3e56c66ab1b --- /dev/null +++ b/news/8601.feature @@ -0,0 +1 @@ +Support ``--use-feature`` in requirements files diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index e120ad91b0f..f991cd32d92 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -62,6 +62,7 @@ cmdoptions.require_hashes, cmdoptions.pre, cmdoptions.trusted_host, + cmdoptions.use_new_feature, ] # type: List[Callable[..., optparse.Option]] # options to be passed to requirements diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py index b22ce20138e..b7c3218510c 100644 --- a/tests/unit/test_req_file.py +++ b/tests/unit/test_req_file.py @@ -382,6 +382,10 @@ def test_set_finder_allow_all_prereleases(self, line_processor, finder): line_processor("--pre", "file", 1, finder=finder) assert finder.allow_all_prereleases + def test_use_feature(self, line_processor): + """--use-feature can be set in requirements files.""" + line_processor("--use-feature=2020-resolver", "filename", 1) + def test_relative_local_find_links( self, line_processor, finder, monkeypatch, tmpdir ): From d6b0481c8cc4ad59320e1ee1fe2981ebcae93934 Mon Sep 17 00:00:00 2001 From: Andy Freeland <andy@andyfreeland.net> Date: Sun, 19 Jul 2020 02:35:50 -0700 Subject: [PATCH 2287/3170] Add --use-feature to pip freeze requirements parsing --- src/pip/_internal/operations/freeze.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/operations/freeze.py b/src/pip/_internal/operations/freeze.py index e15f3e3da0b..ddb9cb232ce 100644 --- a/src/pip/_internal/operations/freeze.py +++ b/src/pip/_internal/operations/freeze.py @@ -100,7 +100,8 @@ def freeze( '--pre', '--trusted-host', '--process-dependency-links', - '--extra-index-url'))): + '--extra-index-url', + '--use-feature'))): line = line.rstrip() if line not in emitted_options: emitted_options.add(line) From 7a3c8026261e0a5f5c0eb8ff07005fd0605bebfa Mon Sep 17 00:00:00 2001 From: Andy Freeland <andy@andyfreeland.net> Date: Sun, 19 Jul 2020 02:44:12 -0700 Subject: [PATCH 2288/3170] Attempt to test --use-feature in pip freeze I can't get the functional tests to run locally... --- tests/functional/test_freeze.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/functional/test_freeze.py b/tests/functional/test_freeze.py index 3792beeca97..ef22169869b 100644 --- a/tests/functional/test_freeze.py +++ b/tests/functional/test_freeze.py @@ -548,6 +548,7 @@ def test_freeze_nested_vcs(script, outer_vcs, inner_vcs): --extra-index-url http://ignore --find-links http://ignore --index-url http://ignore + --use-feature 2020-resolver """) From 95dfd8b5a7855514c16e5352f5f814c714b62a7e Mon Sep 17 00:00:00 2001 From: Oliver Mannion <125105+tekumara@users.noreply.github.com> Date: Sun, 19 Jul 2020 15:49:41 +1000 Subject: [PATCH 2289/3170] Ignore require-virtualenv in `pip list` --- news/8603.feature | 1 + src/pip/_internal/commands/list.py | 1 + 2 files changed, 2 insertions(+) create mode 100644 news/8603.feature diff --git a/news/8603.feature b/news/8603.feature new file mode 100644 index 00000000000..1f8480baaa2 --- /dev/null +++ b/news/8603.feature @@ -0,0 +1 @@ +Ignore require-virtualenv in ``pip list`` diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index a67d0f8d4ab..20e9bff2b71 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -39,6 +39,7 @@ class ListCommand(IndexGroupCommand): Packages are listed in a case-insensitive sorted order. """ + ignore_require_venv = True usage = """ %prog [options]""" From 2152a51b8c580570a105cf8b0139ad77f60f3720 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Tue, 21 Jul 2020 15:54:09 +0700 Subject: [PATCH 2290/3170] Give metadata consistency check its own method --- .../resolution/resolvelib/candidates.py | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 1ee46430292..f631dbe2b94 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -197,6 +197,19 @@ def _prepare_abstract_distribution(self): # type: () -> AbstractDistribution raise NotImplementedError("Override in subclass") + def _check_metadata_consistency(self): + # type: () -> None + """Check for consistency of project name and version of dist.""" + # TODO: (Longer term) Rather than abort, reject this candidate + # and backtrack. This would need resolvelib support. + dist = self._dist # type: Distribution + name = canonicalize_name(dist.project_name) + if self._name is not None and self._name != name: + raise MetadataInconsistent(self._ireq, "name", dist.project_name) + version = dist.parsed_version + if self._version is not None and self._version != version: + raise MetadataInconsistent(self._ireq, "version", dist.version) + def _prepare(self): # type: () -> None if self._dist is not None: @@ -210,19 +223,7 @@ def _prepare(self): self._dist = abstract_dist.get_pkg_resources_distribution() assert self._dist is not None, "Distribution already installed" - - # TODO: (Longer term) Rather than abort, reject this candidate - # and backtrack. This would need resolvelib support. - name = canonicalize_name(self._dist.project_name) - if self._name is not None and self._name != name: - raise MetadataInconsistent( - self._ireq, "name", self._dist.project_name, - ) - version = self._dist.parsed_version - if self._version is not None and self._version != version: - raise MetadataInconsistent( - self._ireq, "version", self._dist.version, - ) + self._check_metadata_consistency() @property def dist(self): From 5d152912746ebe92a04c5571d6b59fd9e4580281 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Tue, 21 Jul 2020 16:00:28 +0700 Subject: [PATCH 2291/3170] Use lazy wheel to obtain dep info for new resolver --- news/8588.feature | 3 ++ src/pip/_internal/cli/cmdoptions.py | 2 +- src/pip/_internal/cli/req_command.py | 1 + .../resolution/resolvelib/candidates.py | 36 +++++++++++++++++-- .../resolution/resolvelib/factory.py | 2 ++ .../resolution/resolvelib/resolver.py | 9 +++++ 6 files changed, 49 insertions(+), 4 deletions(-) create mode 100644 news/8588.feature diff --git a/news/8588.feature b/news/8588.feature new file mode 100644 index 00000000000..273715bb009 --- /dev/null +++ b/news/8588.feature @@ -0,0 +1,3 @@ +Allow the new resolver to obtain dependency information through wheels +lazily downloaded using HTTP range requests. To enable this feature, +invoke ``pip`` with ``--use-feature=fast-deps``. diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index b2f63bb264e..ed42c5f5ae7 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -913,7 +913,7 @@ def check_list_path_option(options): metavar='feature', action='append', default=[], - choices=['2020-resolver'], + choices=['2020-resolver', 'fast-deps'], help='Enable new functionality, that may be backward incompatible.', ) # type: Callable[..., Option] diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 50a60c8ed5a..78b5ce6a141 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -271,6 +271,7 @@ def make_resolver( force_reinstall=force_reinstall, upgrade_strategy=upgrade_strategy, py_version_info=py_version_info, + lazy_wheel='fast-deps' in options.features_enabled, ) import pip._internal.resolution.legacy.resolver return pip._internal.resolution.legacy.resolver.Resolver( diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index f631dbe2b94..c289bb5839c 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -1,16 +1,22 @@ import logging import sys +from pip._vendor.contextlib2 import suppress from pip._vendor.packaging.specifiers import InvalidSpecifier, SpecifierSet from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.packaging.version import Version from pip._internal.exceptions import HashError, MetadataInconsistent +from pip._internal.network.lazy_wheel import ( + HTTPRangeRequestUnsupported, + dist_from_wheel_url, +) from pip._internal.req.constructors import ( install_req_from_editable, install_req_from_line, ) from pip._internal.req.req_install import InstallRequirement +from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import dist_is_editable, normalize_version_info from pip._internal.utils.packaging import get_requires_python from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -142,6 +148,7 @@ def __init__( self._name = name self._version = version self._dist = None # type: Optional[Distribution] + self._prepared = False def __repr__(self): # type: () -> str @@ -212,9 +219,8 @@ def _check_metadata_consistency(self): def _prepare(self): # type: () -> None - if self._dist is not None: + if self._prepared: return - try: abstract_dist = self._prepare_abstract_distribution() except HashError as e: @@ -224,11 +230,35 @@ def _prepare(self): self._dist = abstract_dist.get_pkg_resources_distribution() assert self._dist is not None, "Distribution already installed" self._check_metadata_consistency() + self._prepared = True + + def _fetch_metadata(self): + # type: () -> None + """Fetch metadata, using lazy wheel if possible.""" + preparer = self._factory.preparer + use_lazy_wheel = self._factory.use_lazy_wheel + remote_wheel = self._link.is_wheel and not self._link.is_file + if use_lazy_wheel and remote_wheel and not preparer.require_hashes: + assert self._name is not None + logger.info('Collecting %s', self._ireq.req or self._ireq) + # If HTTPRangeRequestUnsupported is raised, fallback silently. + with indent_log(), suppress(HTTPRangeRequestUnsupported): + logger.info( + 'Obtaining dependency information from %s %s', + self._name, self._version, + ) + url = self._link.url.split('#', 1)[0] + session = preparer.downloader._session + self._dist = dist_from_wheel_url(self._name, url, session) + self._check_metadata_consistency() + if self._dist is None: + self._prepare() @property def dist(self): # type: () -> Distribution - self._prepare() + if self._dist is None: + self._fetch_metadata() return self._dist def _get_requires_python_specifier(self): diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 0d73ff978ef..d10e2eb0009 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -82,6 +82,7 @@ def __init__( ignore_installed, # type: bool ignore_requires_python, # type: bool py_version_info=None, # type: Optional[Tuple[int, ...]] + lazy_wheel=False, # type: bool ): # type: (...) -> None self._finder = finder @@ -92,6 +93,7 @@ def __init__( self._use_user_site = use_user_site self._force_reinstall = force_reinstall self._ignore_requires_python = ignore_requires_python + self.use_lazy_wheel = lazy_wheel self._link_candidate_cache = {} # type: Cache[LinkCandidate] self._editable_candidate_cache = {} # type: Cache[EditableCandidate] diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index d2ac9d0418a..43ea248632d 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -49,8 +49,16 @@ def __init__( force_reinstall, # type: bool upgrade_strategy, # type: str py_version_info=None, # type: Optional[Tuple[int, ...]] + lazy_wheel=False, # type: bool ): super(Resolver, self).__init__() + if lazy_wheel: + logger.warning( + 'pip is using lazily downloaded wheels using HTTP ' + 'range requests to obtain dependency information. ' + 'This experimental feature is enabled through ' + '--use-feature=fast-deps and it is not ready for production.' + ) assert upgrade_strategy in self._allowed_strategies @@ -64,6 +72,7 @@ def __init__( ignore_installed=ignore_installed, ignore_requires_python=ignore_requires_python, py_version_info=py_version_info, + lazy_wheel=lazy_wheel, ) self.ignore_dependencies = ignore_dependencies self.upgrade_strategy = upgrade_strategy From 4efae5c21a60d3475dfed45ed12acf39e3b55c21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Sat, 18 Jul 2020 16:45:12 +0700 Subject: [PATCH 2292/3170] Add integration tests for experimental feature fast-deps Test pip {download,install,wheel} where requirements have dependencies listed in their wheels' metadata. --- .../packages/requiresPaste/pyproject.toml | 9 ++++ .../packages/requiresPaste/requiresPaste.py | 3 ++ tests/functional/test_fast_deps.py | 50 +++++++++++++++++++ 3 files changed, 62 insertions(+) create mode 100644 tests/data/packages/requiresPaste/pyproject.toml create mode 100644 tests/data/packages/requiresPaste/requiresPaste.py create mode 100644 tests/functional/test_fast_deps.py diff --git a/tests/data/packages/requiresPaste/pyproject.toml b/tests/data/packages/requiresPaste/pyproject.toml new file mode 100644 index 00000000000..7ca1bcc79c9 --- /dev/null +++ b/tests/data/packages/requiresPaste/pyproject.toml @@ -0,0 +1,9 @@ +[build-system] +requires = ['flit_core >=2,<4'] +build-backend = 'flit_core.buildapi' + +[tool.flit.metadata] +module = 'requiresPaste' +author = 'A. Random Developer' +author-email = 'author@example.com' +requires = ['Paste==3.4.2'] diff --git a/tests/data/packages/requiresPaste/requiresPaste.py b/tests/data/packages/requiresPaste/requiresPaste.py new file mode 100644 index 00000000000..c74209e44fe --- /dev/null +++ b/tests/data/packages/requiresPaste/requiresPaste.py @@ -0,0 +1,3 @@ +"""Module requiring Paste to test dependencies download of pip wheel.""" + +__version__ = '3.1.4' diff --git a/tests/functional/test_fast_deps.py b/tests/functional/test_fast_deps.py new file mode 100644 index 00000000000..b41055c5606 --- /dev/null +++ b/tests/functional/test_fast_deps.py @@ -0,0 +1,50 @@ +import fnmatch +import json +from os.path import basename + +from pip._vendor.packaging.utils import canonicalize_name +from pytest import mark + + +def pip(script, command, requirement): + return script.pip( + command, '--prefer-binary', '--no-cache-dir', + '--use-feature=fast-deps', requirement, + allow_stderr_warning=True, + ) + + +def assert_installed(script, names): + list_output = json.loads(script.pip('list', '--format=json').stdout) + installed = {canonicalize_name(item['name']) for item in list_output} + assert installed.issuperset(map(canonicalize_name, names)) + + +@mark.network +@mark.parametrize(('requirement', 'expected'), ( + ('Paste==3.4.2', ('Paste', 'six')), + ('Paste[flup]==3.4.2', ('Paste', 'six', 'flup')), +)) +def test_install_from_pypi(requirement, expected, script): + pip(script, 'install', requirement) + assert_installed(script, expected) + + +@mark.network +@mark.parametrize(('requirement', 'expected'), ( + ('Paste==3.4.2', ('Paste-3.4.2-*.whl', 'six-*.whl')), + ('Paste[flup]==3.4.2', ('Paste-3.4.2-*.whl', 'six-*.whl', 'flup-*')), +)) +def test_download_from_pypi(requirement, expected, script): + result = pip(script, 'download', requirement) + created = list(map(basename, result.files_created)) + assert all(fnmatch.filter(created, f) for f in expected) + + +@mark.network +def test_build_wheel_with_deps(data, script): + result = pip(script, 'wheel', data.packages/'requiresPaste') + created = list(map(basename, result.files_created)) + assert fnmatch.filter(created, 'requiresPaste-3.1.4-*.whl') + assert fnmatch.filter(created, 'Paste-3.4.2-*.whl') + assert fnmatch.filter(created, 'six-*.whl') From b9b2c18735ad1ccb0a11ba03c92db90b54bdb853 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Wed, 22 Jul 2020 05:07:18 +0530 Subject: [PATCH 2293/3170] Upgrade appdirs to 1.4.4 --- news/appdirs.vendor | 1 + src/pip/_vendor/appdirs.py | 4 ++-- src/pip/_vendor/vendor.txt | 2 +- 3 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 news/appdirs.vendor diff --git a/news/appdirs.vendor b/news/appdirs.vendor new file mode 100644 index 00000000000..4e4ebd7278a --- /dev/null +++ b/news/appdirs.vendor @@ -0,0 +1 @@ +Upgrade appdirs to 1.4.4 diff --git a/src/pip/_vendor/appdirs.py b/src/pip/_vendor/appdirs.py index 8bd9c9ca0b8..33a3b77410c 100644 --- a/src/pip/_vendor/appdirs.py +++ b/src/pip/_vendor/appdirs.py @@ -13,8 +13,8 @@ # - 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, 3) -__version__ = '.'.join(map(str, __version_info__)) +__version__ = "1.4.4" +__version_info__ = tuple(int(segment) for segment in __version__.split(".")) import sys diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index e032f5f732a..5f3308d1475 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -1,4 +1,4 @@ -appdirs==1.4.3 +appdirs==1.4.4 CacheControl==0.12.6 colorama==0.4.3 contextlib2==0.6.0.post1 From 11a64cdae848e17e263efbc26b6969059bcc5653 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Wed, 22 Jul 2020 05:07:42 +0530 Subject: [PATCH 2294/3170] Upgrade distlib to 0.3.1 --- news/distlib.vendor | 1 + src/pip/_vendor/distlib/__init__.py | 2 +- src/pip/_vendor/distlib/_backport/shutil.py | 9 +- src/pip/_vendor/distlib/compat.py | 2 +- src/pip/_vendor/distlib/database.py | 2 +- src/pip/_vendor/distlib/metadata.py | 122 +++++++------------- src/pip/_vendor/distlib/scripts.py | 13 ++- src/pip/_vendor/distlib/wheel.py | 38 ++++-- src/pip/_vendor/vendor.txt | 2 +- 9 files changed, 86 insertions(+), 105 deletions(-) create mode 100644 news/distlib.vendor diff --git a/news/distlib.vendor b/news/distlib.vendor new file mode 100644 index 00000000000..ba8d7633c07 --- /dev/null +++ b/news/distlib.vendor @@ -0,0 +1 @@ +Upgrade distlib to 0.3.1 diff --git a/src/pip/_vendor/distlib/__init__.py b/src/pip/_vendor/distlib/__init__.py index e19aebdc4cc..63d916e345b 100644 --- a/src/pip/_vendor/distlib/__init__.py +++ b/src/pip/_vendor/distlib/__init__.py @@ -6,7 +6,7 @@ # import logging -__version__ = '0.3.0' +__version__ = '0.3.1' class DistlibException(Exception): pass diff --git a/src/pip/_vendor/distlib/_backport/shutil.py b/src/pip/_vendor/distlib/_backport/shutil.py index 159e49ee8c2..10ed3625397 100644 --- a/src/pip/_vendor/distlib/_backport/shutil.py +++ b/src/pip/_vendor/distlib/_backport/shutil.py @@ -14,7 +14,10 @@ import stat from os.path import abspath import fnmatch -import collections +try: + from collections.abc import Callable +except ImportError: + from collections import Callable import errno from . import tarfile @@ -528,7 +531,7 @@ def register_archive_format(name, function, extra_args=None, description=''): """ if extra_args is None: extra_args = [] - if not isinstance(function, collections.Callable): + if not isinstance(function, Callable): raise TypeError('The %s object is not callable' % function) if not isinstance(extra_args, (tuple, list)): raise TypeError('extra_args needs to be a sequence') @@ -621,7 +624,7 @@ def _check_unpack_options(extensions, function, extra_args): raise RegistryError(msg % (extension, existing_extensions[extension])) - if not isinstance(function, collections.Callable): + if not isinstance(function, Callable): raise TypeError('The registered function must be a callable') diff --git a/src/pip/_vendor/distlib/compat.py b/src/pip/_vendor/distlib/compat.py index ff328c8ee49..c316fd973ad 100644 --- a/src/pip/_vendor/distlib/compat.py +++ b/src/pip/_vendor/distlib/compat.py @@ -319,7 +319,7 @@ def python_implementation(): try: callable = callable except NameError: # pragma: no cover - from collections import Callable + from collections.abc import Callable def callable(obj): return isinstance(obj, Callable) diff --git a/src/pip/_vendor/distlib/database.py b/src/pip/_vendor/distlib/database.py index c16c0c8d9ed..0a90c300ba8 100644 --- a/src/pip/_vendor/distlib/database.py +++ b/src/pip/_vendor/distlib/database.py @@ -550,7 +550,7 @@ def __init__(self, path, metadata=None, env=None): r = finder.find(WHEEL_METADATA_FILENAME) # Temporary - for legacy support if r is None: - r = finder.find('METADATA') + r = finder.find(LEGACY_METADATA_FILENAME) if r is None: raise ValueError('no %s found in %s' % (METADATA_FILENAME, path)) diff --git a/src/pip/_vendor/distlib/metadata.py b/src/pip/_vendor/distlib/metadata.py index 2d61378e994..6d5e236090d 100644 --- a/src/pip/_vendor/distlib/metadata.py +++ b/src/pip/_vendor/distlib/metadata.py @@ -5,7 +5,7 @@ # """Implementation of the Metadata for Python packages PEPs. -Supports all metadata formats (1.0, 1.1, 1.2, and 2.0 experimental). +Supports all metadata formats (1.0, 1.1, 1.2, 1.3/2.1 and withdrawn 2.0). """ from __future__ import unicode_literals @@ -194,38 +194,12 @@ def _has_marker(keys, markers): return '2.0' +# This follows the rules about transforming keys as described in +# https://www.python.org/dev/peps/pep-0566/#id17 _ATTR2FIELD = { - 'metadata_version': 'Metadata-Version', - 'name': 'Name', - 'version': 'Version', - 'platform': 'Platform', - 'supported_platform': 'Supported-Platform', - 'summary': 'Summary', - 'description': 'Description', - 'keywords': 'Keywords', - 'home_page': 'Home-page', - 'author': 'Author', - 'author_email': 'Author-email', - 'maintainer': 'Maintainer', - 'maintainer_email': 'Maintainer-email', - 'license': 'License', - 'classifier': 'Classifier', - 'download_url': 'Download-URL', - 'obsoletes_dist': 'Obsoletes-Dist', - 'provides_dist': 'Provides-Dist', - 'requires_dist': 'Requires-Dist', - 'setup_requires_dist': 'Setup-Requires-Dist', - 'requires_python': 'Requires-Python', - 'requires_external': 'Requires-External', - 'requires': 'Requires', - 'provides': 'Provides', - 'obsoletes': 'Obsoletes', - 'project_url': 'Project-URL', - 'private_version': 'Private-Version', - 'obsoleted_by': 'Obsoleted-By', - 'extension': 'Extension', - 'provides_extra': 'Provides-Extra', + name.lower().replace("-", "_"): name for name in _ALL_FIELDS } +_FIELD2ATTR = {field: attr for attr, field in _ATTR2FIELD.items()} _PREDICATE_FIELDS = ('Requires-Dist', 'Obsoletes-Dist', 'Provides-Dist') _VERSIONS_FIELDS = ('Requires-Python',) @@ -262,7 +236,7 @@ def _get_name_and_version(name, version, for_filename=False): class LegacyMetadata(object): """The legacy metadata of a release. - Supports versions 1.0, 1.1 and 1.2 (auto-detected). You can + Supports versions 1.0, 1.1, 1.2, 2.0 and 1.3/2.1 (auto-detected). You can instantiate the class with one of these arguments (or none): - *path*, the path to a metadata file - *fileobj* give a file-like object with metadata as content @@ -381,6 +355,11 @@ def read_file(self, fileob): value = msg[field] if value is not None and value != 'UNKNOWN': self.set(field, value) + + # PEP 566 specifies that the body be used for the description, if + # available + body = msg.get_payload() + self["Description"] = body if body else self["Description"] # logger.debug('Attempting to set metadata for %s', self) # self.set_metadata_version() @@ -567,57 +546,21 @@ def todict(self, skip_missing=False): Field names will be converted to use the underscore-lowercase style instead of hyphen-mixed case (i.e. home_page instead of Home-page). + This is as per https://www.python.org/dev/peps/pep-0566/#id17. """ self.set_metadata_version() - mapping_1_0 = ( - ('metadata_version', 'Metadata-Version'), - ('name', 'Name'), - ('version', 'Version'), - ('summary', 'Summary'), - ('home_page', 'Home-page'), - ('author', 'Author'), - ('author_email', 'Author-email'), - ('license', 'License'), - ('description', 'Description'), - ('keywords', 'Keywords'), - ('platform', 'Platform'), - ('classifiers', 'Classifier'), - ('download_url', 'Download-URL'), - ) + fields = _version2fieldlist(self['Metadata-Version']) data = {} - for key, field_name in mapping_1_0: + + for field_name in fields: if not skip_missing or field_name in self._fields: - data[key] = self[field_name] - - if self['Metadata-Version'] == '1.2': - mapping_1_2 = ( - ('requires_dist', 'Requires-Dist'), - ('requires_python', 'Requires-Python'), - ('requires_external', 'Requires-External'), - ('provides_dist', 'Provides-Dist'), - ('obsoletes_dist', 'Obsoletes-Dist'), - ('project_url', 'Project-URL'), - ('maintainer', 'Maintainer'), - ('maintainer_email', 'Maintainer-email'), - ) - for key, field_name in mapping_1_2: - if not skip_missing or field_name in self._fields: - if key != 'project_url': - data[key] = self[field_name] - else: - data[key] = [','.join(u) for u in self[field_name]] - - elif self['Metadata-Version'] == '1.1': - mapping_1_1 = ( - ('provides', 'Provides'), - ('requires', 'Requires'), - ('obsoletes', 'Obsoletes'), - ) - for key, field_name in mapping_1_1: - if not skip_missing or field_name in self._fields: + key = _FIELD2ATTR[field_name] + if key != 'project_url': data[key] = self[field_name] + else: + data[key] = [','.join(u) for u in self[field_name]] return data @@ -1003,10 +946,14 @@ def _from_legacy(self): LEGACY_MAPPING = { 'name': 'Name', 'version': 'Version', - 'license': 'License', + ('extensions', 'python.details', 'license'): 'License', 'summary': 'Summary', 'description': 'Description', - 'classifiers': 'Classifier', + ('extensions', 'python.project', 'project_urls', 'Home'): 'Home-page', + ('extensions', 'python.project', 'contacts', 0, 'name'): 'Author', + ('extensions', 'python.project', 'contacts', 0, 'email'): 'Author-email', + 'source_url': 'Download-URL', + ('extensions', 'python.details', 'classifiers'): 'Classifier', } def _to_legacy(self): @@ -1034,16 +981,29 @@ def process_entries(entries): assert self._data and not self._legacy result = LegacyMetadata() nmd = self._data + # import pdb; pdb.set_trace() for nk, ok in self.LEGACY_MAPPING.items(): - if nk in nmd: - result[ok] = nmd[nk] + if not isinstance(nk, tuple): + if nk in nmd: + result[ok] = nmd[nk] + else: + d = nmd + found = True + for k in nk: + try: + d = d[k] + except (KeyError, IndexError): + found = False + break + if found: + result[ok] = d r1 = process_entries(self.run_requires + self.meta_requires) r2 = process_entries(self.build_requires + self.dev_requires) if self.extras: result['Provides-Extra'] = sorted(self.extras) result['Requires-Dist'] = sorted(r1) result['Setup-Requires-Dist'] = sorted(r2) - # TODO: other fields such as contacts + # TODO: any other fields wanted return result def write(self, path=None, fileobj=None, legacy=False, skip_unknown=True): diff --git a/src/pip/_vendor/distlib/scripts.py b/src/pip/_vendor/distlib/scripts.py index 51859741867..03f8f21e0ff 100644 --- a/src/pip/_vendor/distlib/scripts.py +++ b/src/pip/_vendor/distlib/scripts.py @@ -48,7 +48,7 @@ ''' -def _enquote_executable(executable): +def enquote_executable(executable): if ' ' in executable: # make sure we quote only the executable in case of env # for example /usr/bin/env "/dir with spaces/bin/jython" @@ -63,6 +63,8 @@ def _enquote_executable(executable): executable = '"%s"' % executable return executable +# Keep the old name around (for now), as there is at least one project using it! +_enquote_executable = enquote_executable class ScriptMaker(object): """ @@ -88,6 +90,7 @@ def __init__(self, source_dir, target_dir, add_launchers=True, self._is_nt = os.name == 'nt' or ( os.name == 'java' and os._name == 'nt') + self.version_info = sys.version_info def _get_alternate_executable(self, executable, options): if options.get('gui', False) and self._is_nt: # pragma: no cover @@ -185,7 +188,7 @@ def _get_shebang(self, encoding, post_interp=b'', options=None): # If the user didn't specify an executable, it may be necessary to # cater for executable paths with spaces (not uncommon on Windows) if enquote: - executable = _enquote_executable(executable) + executable = enquote_executable(executable) # Issue #51: don't use fsencode, since we later try to # check that the shebang is decodable using utf-8. executable = executable.encode('utf-8') @@ -293,10 +296,10 @@ def _make_script(self, entry, filenames, options=None): if '' in self.variants: scriptnames.add(name) if 'X' in self.variants: - scriptnames.add('%s%s' % (name, sys.version_info[0])) + scriptnames.add('%s%s' % (name, self.version_info[0])) if 'X.Y' in self.variants: - scriptnames.add('%s-%s.%s' % (name, sys.version_info[0], - sys.version_info[1])) + scriptnames.add('%s-%s.%s' % (name, self.version_info[0], + self.version_info[1])) if options and options.get('gui', False): ext = 'pyw' else: diff --git a/src/pip/_vendor/distlib/wheel.py b/src/pip/_vendor/distlib/wheel.py index bd179383ac9..1e2c7a020c9 100644 --- a/src/pip/_vendor/distlib/wheel.py +++ b/src/pip/_vendor/distlib/wheel.py @@ -26,7 +26,8 @@ from . import __version__, DistlibException from .compat import sysconfig, ZipFile, fsdecode, text_type, filter from .database import InstalledDistribution -from .metadata import Metadata, METADATA_FILENAME, WHEEL_METADATA_FILENAME +from .metadata import (Metadata, METADATA_FILENAME, WHEEL_METADATA_FILENAME, + LEGACY_METADATA_FILENAME) from .util import (FileOperator, convert_path, CSVReader, CSVWriter, Cache, cached_property, get_cache_base, read_exports, tempdir) from .version import NormalizedVersion, UnsupportedVersionError @@ -221,10 +222,12 @@ def metadata(self): wheel_metadata = self.get_wheel_metadata(zf) wv = wheel_metadata['Wheel-Version'].split('.', 1) file_version = tuple([int(i) for i in wv]) - if file_version < (1, 1): - fns = [WHEEL_METADATA_FILENAME, METADATA_FILENAME, 'METADATA'] - else: - fns = [WHEEL_METADATA_FILENAME, METADATA_FILENAME] + # if file_version < (1, 1): + # fns = [WHEEL_METADATA_FILENAME, METADATA_FILENAME, + # LEGACY_METADATA_FILENAME] + # else: + # fns = [WHEEL_METADATA_FILENAME, METADATA_FILENAME] + fns = [WHEEL_METADATA_FILENAME, LEGACY_METADATA_FILENAME] result = None for fn in fns: try: @@ -299,10 +302,9 @@ def get_hash(self, data, hash_kind=None): return hash_kind, result def write_record(self, records, record_path, base): - records = list(records) # make a copy for sorting + records = list(records) # make a copy, as mutated p = to_posix(os.path.relpath(record_path, base)) records.append((p, '', '')) - records.sort() with CSVWriter(record_path) as writer: for row in records: writer.writerow(row) @@ -425,6 +427,18 @@ def build(self, paths, tags=None, wheel_version=None): ap = to_posix(os.path.join(info_dir, 'WHEEL')) archive_paths.append((ap, p)) + # sort the entries by archive path. Not needed by any spec, but it + # keeps the archive listing and RECORD tidier than they would otherwise + # be. Use the number of path segments to keep directory entries together, + # and keep the dist-info stuff at the end. + def sorter(t): + ap = t[0] + n = ap.count('/') + if '.dist-info' in ap: + n += 10000 + return (n, ap) + archive_paths = sorted(archive_paths, key=sorter) + # Now, at last, RECORD. # Paths in here are archive paths - nothing else makes sense. self.write_records((distinfo, info_dir), libdir, archive_paths) @@ -476,7 +490,7 @@ def install(self, paths, maker, **kwargs): data_dir = '%s.data' % name_ver info_dir = '%s.dist-info' % name_ver - metadata_name = posixpath.join(info_dir, METADATA_FILENAME) + metadata_name = posixpath.join(info_dir, LEGACY_METADATA_FILENAME) wheel_metadata_name = posixpath.join(info_dir, 'WHEEL') record_name = posixpath.join(info_dir, 'RECORD') @@ -619,7 +633,7 @@ def install(self, paths, maker, **kwargs): for v in epdata[k].values(): s = '%s:%s' % (v.prefix, v.suffix) if v.flags: - s += ' %s' % v.flags + s += ' [%s]' % ','.join(v.flags) d[v.name] = s except Exception: logger.warning('Unable to read legacy script ' @@ -773,7 +787,7 @@ def verify(self): data_dir = '%s.data' % name_ver info_dir = '%s.dist-info' % name_ver - metadata_name = posixpath.join(info_dir, METADATA_FILENAME) + metadata_name = posixpath.join(info_dir, LEGACY_METADATA_FILENAME) wheel_metadata_name = posixpath.join(info_dir, 'WHEEL') record_name = posixpath.join(info_dir, 'RECORD') @@ -842,7 +856,7 @@ def update(self, modifier, dest_dir=None, **kwargs): def get_version(path_map, info_dir): version = path = None - key = '%s/%s' % (info_dir, METADATA_FILENAME) + key = '%s/%s' % (info_dir, LEGACY_METADATA_FILENAME) if key not in path_map: key = '%s/PKG-INFO' % info_dir if key in path_map: @@ -868,7 +882,7 @@ def update_version(version, path): if updated: md = Metadata(path=path) md.version = updated - legacy = not path.endswith(METADATA_FILENAME) + legacy = path.endswith(LEGACY_METADATA_FILENAME) md.write(path=path, legacy=legacy) logger.debug('Version updated from %r to %r', version, updated) diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 5f3308d1475..aca6e357ec0 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -2,7 +2,7 @@ appdirs==1.4.4 CacheControl==0.12.6 colorama==0.4.3 contextlib2==0.6.0.post1 -distlib==0.3.0 +distlib==0.3.1 distro==1.5.0 html5lib==1.0.1 ipaddress==1.0.23 # Only needed on 2.6 and 2.7 From e84d0c18138e7950c7b4132c7e5f3755b26292c2 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Wed, 22 Jul 2020 05:08:06 +0530 Subject: [PATCH 2295/3170] Upgrade html5lib to 1.1 Also, drop the no-longer-necessary patch. --- news/html5lib.vendor | 1 + src/pip/_vendor/html5lib/__init__.py | 2 +- src/pip/_vendor/html5lib/_ihatexml.py | 5 +- src/pip/_vendor/html5lib/_inputstream.py | 55 +- src/pip/_vendor/html5lib/_tokenizer.py | 16 +- src/pip/_vendor/html5lib/_trie/__init__.py | 13 +- src/pip/_vendor/html5lib/_trie/datrie.py | 44 -- src/pip/_vendor/html5lib/_utils.py | 40 +- src/pip/_vendor/html5lib/constants.py | 9 +- src/pip/_vendor/html5lib/filters/sanitizer.py | 20 + src/pip/_vendor/html5lib/html5parser.py | 734 +++++++++--------- src/pip/_vendor/html5lib/serializer.py | 2 +- src/pip/_vendor/html5lib/treebuilders/base.py | 8 +- .../_vendor/html5lib/treebuilders/etree.py | 27 +- .../html5lib/treebuilders/etree_lxml.py | 64 +- .../_vendor/html5lib/treewalkers/__init__.py | 6 +- src/pip/_vendor/html5lib/treewalkers/etree.py | 1 + .../html5lib/treewalkers/etree_lxml.py | 4 +- src/pip/_vendor/vendor.txt | 2 +- .../vendoring/patches/html5lib.patch | 55 -- 20 files changed, 549 insertions(+), 559 deletions(-) create mode 100644 news/html5lib.vendor delete mode 100644 src/pip/_vendor/html5lib/_trie/datrie.py delete mode 100644 tools/automation/vendoring/patches/html5lib.patch diff --git a/news/html5lib.vendor b/news/html5lib.vendor new file mode 100644 index 00000000000..ed774270d45 --- /dev/null +++ b/news/html5lib.vendor @@ -0,0 +1 @@ +Upgrade html5lib to 1.1 diff --git a/src/pip/_vendor/html5lib/__init__.py b/src/pip/_vendor/html5lib/__init__.py index 049123492e2..d1d82f157f8 100644 --- a/src/pip/_vendor/html5lib/__init__.py +++ b/src/pip/_vendor/html5lib/__init__.py @@ -32,4 +32,4 @@ # this has to be at the top level, see how setup.py parses this #: Distribution version number. -__version__ = "1.0.1" +__version__ = "1.1" diff --git a/src/pip/_vendor/html5lib/_ihatexml.py b/src/pip/_vendor/html5lib/_ihatexml.py index 4c77717bbc0..3ff803c1952 100644 --- a/src/pip/_vendor/html5lib/_ihatexml.py +++ b/src/pip/_vendor/html5lib/_ihatexml.py @@ -136,6 +136,7 @@ def normaliseCharList(charList): i += j return rv + # We don't really support characters above the BMP :( max_unicode = int("FFFF", 16) @@ -254,7 +255,7 @@ def toXmlName(self, name): nameRest = name[1:] m = nonXmlNameFirstBMPRegexp.match(nameFirst) if m: - warnings.warn("Coercing non-XML name", DataLossWarning) + warnings.warn("Coercing non-XML name: %s" % name, DataLossWarning) nameFirstOutput = self.getReplacementCharacter(nameFirst) else: nameFirstOutput = nameFirst @@ -262,7 +263,7 @@ def toXmlName(self, name): nameRestOutput = nameRest replaceChars = set(nonXmlNameBMPRegexp.findall(nameRest)) for char in replaceChars: - warnings.warn("Coercing non-XML name", DataLossWarning) + warnings.warn("Coercing non-XML name: %s" % name, DataLossWarning) replacement = self.getReplacementCharacter(char) nameRestOutput = nameRestOutput.replace(char, replacement) return nameFirstOutput + nameRestOutput diff --git a/src/pip/_vendor/html5lib/_inputstream.py b/src/pip/_vendor/html5lib/_inputstream.py index a65e55f64bf..e0bb37602c8 100644 --- a/src/pip/_vendor/html5lib/_inputstream.py +++ b/src/pip/_vendor/html5lib/_inputstream.py @@ -1,10 +1,11 @@ from __future__ import absolute_import, division, unicode_literals -from pip._vendor.six import text_type, binary_type +from pip._vendor.six import text_type from pip._vendor.six.moves import http_client, urllib import codecs import re +from io import BytesIO, StringIO from pip._vendor import webencodings @@ -12,13 +13,6 @@ from .constants import _ReparseException from . import _utils -from io import StringIO - -try: - from io import BytesIO -except ImportError: - BytesIO = StringIO - # Non-unicode versions of constants for use in the pre-parser spaceCharactersBytes = frozenset([item.encode("ascii") for item in spaceCharacters]) asciiLettersBytes = frozenset([item.encode("ascii") for item in asciiLetters]) @@ -40,13 +34,13 @@ else: invalid_unicode_re = re.compile(invalid_unicode_no_surrogate) -non_bmp_invalid_codepoints = set([0x1FFFE, 0x1FFFF, 0x2FFFE, 0x2FFFF, 0x3FFFE, - 0x3FFFF, 0x4FFFE, 0x4FFFF, 0x5FFFE, 0x5FFFF, - 0x6FFFE, 0x6FFFF, 0x7FFFE, 0x7FFFF, 0x8FFFE, - 0x8FFFF, 0x9FFFE, 0x9FFFF, 0xAFFFE, 0xAFFFF, - 0xBFFFE, 0xBFFFF, 0xCFFFE, 0xCFFFF, 0xDFFFE, - 0xDFFFF, 0xEFFFE, 0xEFFFF, 0xFFFFE, 0xFFFFF, - 0x10FFFE, 0x10FFFF]) +non_bmp_invalid_codepoints = {0x1FFFE, 0x1FFFF, 0x2FFFE, 0x2FFFF, 0x3FFFE, + 0x3FFFF, 0x4FFFE, 0x4FFFF, 0x5FFFE, 0x5FFFF, + 0x6FFFE, 0x6FFFF, 0x7FFFE, 0x7FFFF, 0x8FFFE, + 0x8FFFF, 0x9FFFE, 0x9FFFF, 0xAFFFE, 0xAFFFF, + 0xBFFFE, 0xBFFFF, 0xCFFFE, 0xCFFFF, 0xDFFFE, + 0xDFFFF, 0xEFFFE, 0xEFFFF, 0xFFFFE, 0xFFFFF, + 0x10FFFE, 0x10FFFF} ascii_punctuation_re = re.compile("[\u0009-\u000D\u0020-\u002F\u003A-\u0040\u005C\u005B-\u0060\u007B-\u007E]") @@ -367,7 +361,7 @@ def charsUntil(self, characters, opposite=False): def unget(self, char): # Only one character is allowed to be ungotten at once - it must # be consumed again before any further call to unget - if char is not None: + if char is not EOF: if self.chunkOffset == 0: # unget is called quite rarely, so it's a good idea to do # more work here if it saves a bit of work in the frequently @@ -449,7 +443,7 @@ def openStream(self, source): try: stream.seek(stream.tell()) - except: # pylint:disable=bare-except + except Exception: stream = BufferedStream(stream) return stream @@ -461,7 +455,7 @@ def determineEncoding(self, chardet=True): if charEncoding[0] is not None: return charEncoding - # If we've been overriden, we've been overriden + # If we've been overridden, we've been overridden charEncoding = lookupEncoding(self.override_encoding), "certain" if charEncoding[0] is not None: return charEncoding @@ -664,9 +658,7 @@ def matchBytes(self, bytes): """Look for a sequence of bytes at the start of a string. If the bytes are found return True and advance the position to the byte after the match. Otherwise return False and leave the position alone""" - p = self.position - data = self[p:p + len(bytes)] - rv = data.startswith(bytes) + rv = self.startswith(bytes, self.position) if rv: self.position += len(bytes) return rv @@ -674,15 +666,11 @@ def matchBytes(self, bytes): def jumpTo(self, bytes): """Look for the next sequence of bytes matching a given sequence. If a match is found advance the position to the last byte of the match""" - newPosition = self[self.position:].find(bytes) - if newPosition > -1: - # XXX: This is ugly, but I can't see a nicer way to fix this. - if self._position == -1: - self._position = 0 - self._position += (newPosition + len(bytes) - 1) - return True - else: + try: + self._position = self.index(bytes, self.position) + len(bytes) - 1 + except ValueError: raise StopIteration + return True class EncodingParser(object): @@ -694,6 +682,9 @@ def __init__(self, data): self.encoding = None def getEncoding(self): + if b"<meta" not in self.data: + return None + methodDispatch = ( (b"<!--", self.handleComment), (b"<meta", self.handleMeta), @@ -703,6 +694,10 @@ def getEncoding(self): (b"<", self.handlePossibleStartTag)) for _ in self.data: keepParsing = True + try: + self.data.jumpTo(b"<") + except StopIteration: + break for key, method in methodDispatch: if self.data.matchBytes(key): try: @@ -908,7 +903,7 @@ def parse(self): def lookupEncoding(encoding): """Return the python codec name corresponding to an encoding or None if the string doesn't correspond to a valid encoding.""" - if isinstance(encoding, binary_type): + if isinstance(encoding, bytes): try: encoding = encoding.decode("ascii") except UnicodeDecodeError: diff --git a/src/pip/_vendor/html5lib/_tokenizer.py b/src/pip/_vendor/html5lib/_tokenizer.py index 178f6e7fa8c..5f00253e2f6 100644 --- a/src/pip/_vendor/html5lib/_tokenizer.py +++ b/src/pip/_vendor/html5lib/_tokenizer.py @@ -2,7 +2,8 @@ from pip._vendor.six import unichr as chr -from collections import deque +from collections import deque, OrderedDict +from sys import version_info from .constants import spaceCharacters from .constants import entities @@ -17,6 +18,11 @@ entitiesTrie = Trie(entities) +if version_info >= (3, 7): + attributeMap = dict +else: + attributeMap = OrderedDict + class HTMLTokenizer(object): """ This class takes care of tokenizing HTML. @@ -228,6 +234,14 @@ def emitCurrentToken(self): # Add token to the queue to be yielded if (token["type"] in tagTokenTypes): token["name"] = token["name"].translate(asciiUpper2Lower) + if token["type"] == tokenTypes["StartTag"]: + raw = token["data"] + data = attributeMap(raw) + if len(raw) > len(data): + # we had some duplicated attribute, fix so first wins + data.update(raw[::-1]) + token["data"] = data + if token["type"] == tokenTypes["EndTag"]: if token["data"]: self.tokenQueue.append({"type": tokenTypes["ParseError"], diff --git a/src/pip/_vendor/html5lib/_trie/__init__.py b/src/pip/_vendor/html5lib/_trie/__init__.py index a5ba4bf123a..07bad5d31c1 100644 --- a/src/pip/_vendor/html5lib/_trie/__init__.py +++ b/src/pip/_vendor/html5lib/_trie/__init__.py @@ -1,14 +1,5 @@ from __future__ import absolute_import, division, unicode_literals -from .py import Trie as PyTrie +from .py import Trie -Trie = PyTrie - -# pylint:disable=wrong-import-position -try: - from .datrie import Trie as DATrie -except ImportError: - pass -else: - Trie = DATrie -# pylint:enable=wrong-import-position +__all__ = ["Trie"] diff --git a/src/pip/_vendor/html5lib/_trie/datrie.py b/src/pip/_vendor/html5lib/_trie/datrie.py deleted file mode 100644 index e2e5f86621c..00000000000 --- a/src/pip/_vendor/html5lib/_trie/datrie.py +++ /dev/null @@ -1,44 +0,0 @@ -from __future__ import absolute_import, division, unicode_literals - -from datrie import Trie as DATrie -from pip._vendor.six import text_type - -from ._base import Trie as ABCTrie - - -class Trie(ABCTrie): - def __init__(self, data): - chars = set() - for key in data.keys(): - if not isinstance(key, text_type): - raise TypeError("All keys must be strings") - for char in key: - chars.add(char) - - self._data = DATrie("".join(chars)) - for key, value in data.items(): - self._data[key] = value - - def __contains__(self, key): - return key in self._data - - def __len__(self): - return len(self._data) - - def __iter__(self): - raise NotImplementedError() - - def __getitem__(self, key): - return self._data[key] - - def keys(self, prefix=None): - return self._data.keys(prefix) - - def has_keys_with_prefix(self, prefix): - return self._data.has_keys_with_prefix(prefix) - - def longest_prefix(self, prefix): - return self._data.longest_prefix(prefix) - - def longest_prefix_item(self, prefix): - return self._data.longest_prefix_item(prefix) diff --git a/src/pip/_vendor/html5lib/_utils.py b/src/pip/_vendor/html5lib/_utils.py index 96eb17b2c17..d7c4926afce 100644 --- a/src/pip/_vendor/html5lib/_utils.py +++ b/src/pip/_vendor/html5lib/_utils.py @@ -2,6 +2,11 @@ from types import ModuleType +try: + from collections.abc import Mapping +except ImportError: + from collections import Mapping + from pip._vendor.six import text_type, PY3 if PY3: @@ -30,7 +35,7 @@ # We need this with u"" because of http://bugs.jython.org/issue2039 _x = eval('u"\\uD800"') # pylint:disable=eval-used assert isinstance(_x, text_type) -except: # pylint:disable=bare-except +except Exception: supports_lone_surrogates = False else: supports_lone_surrogates = True @@ -50,9 +55,6 @@ class MethodDispatcher(dict): """ def __init__(self, items=()): - # Using _dictEntries instead of directly assigning to self is about - # twice as fast. Please do careful performance testing before changing - # anything here. _dictEntries = [] for name, value in items: if isinstance(name, (list, tuple, frozenset, set)): @@ -67,6 +69,36 @@ def __init__(self, items=()): def __getitem__(self, key): return dict.get(self, key, self.default) + def __get__(self, instance, owner=None): + return BoundMethodDispatcher(instance, self) + + +class BoundMethodDispatcher(Mapping): + """Wraps a MethodDispatcher, binding its return values to `instance`""" + def __init__(self, instance, dispatcher): + self.instance = instance + self.dispatcher = dispatcher + + def __getitem__(self, key): + # see https://docs.python.org/3/reference/datamodel.html#object.__get__ + # on a function, __get__ is used to bind a function to an instance as a bound method + return self.dispatcher[key].__get__(self.instance) + + def get(self, key, default): + if key in self.dispatcher: + return self[key] + else: + return default + + def __iter__(self): + return iter(self.dispatcher) + + def __len__(self): + return len(self.dispatcher) + + def __contains__(self, key): + return key in self.dispatcher + # Some utility functions to deal with weirdness around UCS2 vs UCS4 # python builds diff --git a/src/pip/_vendor/html5lib/constants.py b/src/pip/_vendor/html5lib/constants.py index 1ff804190cd..fe3e237cd8a 100644 --- a/src/pip/_vendor/html5lib/constants.py +++ b/src/pip/_vendor/html5lib/constants.py @@ -519,8 +519,8 @@ "xmlns:xlink": ("xmlns", "xlink", namespaces["xmlns"]) } -unadjustForeignAttributes = dict([((ns, local), qname) for qname, (prefix, local, ns) in - adjustForeignAttributes.items()]) +unadjustForeignAttributes = {(ns, local): qname for qname, (prefix, local, ns) in + adjustForeignAttributes.items()} spaceCharacters = frozenset([ "\t", @@ -544,8 +544,7 @@ digits = frozenset(string.digits) hexDigits = frozenset(string.hexdigits) -asciiUpper2Lower = dict([(ord(c), ord(c.lower())) - for c in string.ascii_uppercase]) +asciiUpper2Lower = {ord(c): ord(c.lower()) for c in string.ascii_uppercase} # Heading elements need to be ordered headingElements = ( @@ -2934,7 +2933,7 @@ tokenTypes["EmptyTag"]]) -prefixes = dict([(v, k) for k, v in namespaces.items()]) +prefixes = {v: k for k, v in namespaces.items()} prefixes["http://www.w3.org/1998/Math/MathML"] = "math" diff --git a/src/pip/_vendor/html5lib/filters/sanitizer.py b/src/pip/_vendor/html5lib/filters/sanitizer.py index af8e77b81e3..aa7431d1312 100644 --- a/src/pip/_vendor/html5lib/filters/sanitizer.py +++ b/src/pip/_vendor/html5lib/filters/sanitizer.py @@ -1,6 +1,15 @@ +"""Deprecated from html5lib 1.1. + +See `here <https://github.com/html5lib/html5lib-python/issues/443>`_ for +information about its deprecation; `Bleach <https://github.com/mozilla/bleach>`_ +is recommended as a replacement. Please let us know in the aforementioned issue +if Bleach is unsuitable for your needs. + +""" from __future__ import absolute_import, division, unicode_literals import re +import warnings from xml.sax.saxutils import escape, unescape from pip._vendor.six.moves import urllib_parse as urlparse @@ -11,6 +20,14 @@ __all__ = ["Filter"] +_deprecation_msg = ( + "html5lib's sanitizer is deprecated; see " + + "https://github.com/html5lib/html5lib-python/issues/443 and please let " + + "us know if Bleach is unsuitable for your needs" +) + +warnings.warn(_deprecation_msg, DeprecationWarning) + allowed_elements = frozenset(( (namespaces['html'], 'a'), (namespaces['html'], 'abbr'), @@ -750,6 +767,9 @@ def __init__(self, """ super(Filter, self).__init__(source) + + warnings.warn(_deprecation_msg, DeprecationWarning) + self.allowed_elements = allowed_elements self.allowed_attributes = allowed_attributes self.allowed_css_properties = allowed_css_properties diff --git a/src/pip/_vendor/html5lib/html5parser.py b/src/pip/_vendor/html5lib/html5parser.py index ae41a133761..d06784f3d25 100644 --- a/src/pip/_vendor/html5lib/html5parser.py +++ b/src/pip/_vendor/html5lib/html5parser.py @@ -2,7 +2,6 @@ from pip._vendor.six import with_metaclass, viewkeys import types -from collections import OrderedDict from . import _inputstream from . import _tokenizer @@ -119,8 +118,8 @@ def __init__(self, tree=None, strict=False, namespaceHTMLElements=True, debug=Fa self.tree = tree(namespaceHTMLElements) self.errors = [] - self.phases = dict([(name, cls(self, self.tree)) for name, cls in - getPhases(debug).items()]) + self.phases = {name: cls(self, self.tree) for name, cls in + getPhases(debug).items()} def _parse(self, stream, innerHTML=False, container="div", scripting=False, **kwargs): @@ -202,7 +201,7 @@ def mainLoop(self): DoctypeToken = tokenTypes["Doctype"] ParseErrorToken = tokenTypes["ParseError"] - for token in self.normalizedTokens(): + for token in self.tokenizer: prev_token = None new_token = token while new_token is not None: @@ -260,10 +259,6 @@ def mainLoop(self): if reprocess: assert self.phase not in phases - def normalizedTokens(self): - for token in self.tokenizer: - yield self.normalizeToken(token) - def parse(self, stream, *args, **kwargs): """Parse a HTML document into a well-formed tree @@ -325,17 +320,6 @@ def parseError(self, errorcode="XXX-undefined-error", datavars=None): if self.strict: raise ParseError(E[errorcode] % datavars) - def normalizeToken(self, token): - # HTML5 specific normalizations to the token stream - if token["type"] == tokenTypes["StartTag"]: - raw = token["data"] - token["data"] = OrderedDict(raw) - if len(raw) > len(token["data"]): - # we had some duplicated attribute, fix so first wins - token["data"].update(raw[::-1]) - - return token - def adjustMathMLAttributes(self, token): adjust_attributes(token, adjustMathMLAttributes) @@ -413,16 +397,12 @@ def parseRCDataRawtext(self, token, contentType): def getPhases(debug): def log(function): """Logger that records which phase processes each token""" - type_names = dict((value, key) for key, value in - tokenTypes.items()) + type_names = {value: key for key, value in tokenTypes.items()} def wrapped(self, *args, **kwargs): if function.__name__.startswith("process") and len(args) > 0: token = args[0] - try: - info = {"type": type_names[token['type']]} - except: - raise + info = {"type": type_names[token['type']]} if token['type'] in tagTokenTypes: info["name"] = token['name'] @@ -446,10 +426,13 @@ def getMetaclass(use_metaclass, metaclass_func): class Phase(with_metaclass(getMetaclass(debug, log))): """Base class for helper object that implements each phase of processing """ + __slots__ = ("parser", "tree", "__startTagCache", "__endTagCache") def __init__(self, parser, tree): self.parser = parser self.tree = tree + self.__startTagCache = {} + self.__endTagCache = {} def processEOF(self): raise NotImplementedError @@ -469,7 +452,21 @@ def processSpaceCharacters(self, token): self.tree.insertText(token["data"]) def processStartTag(self, token): - return self.startTagHandler[token["name"]](token) + # Note the caching is done here rather than BoundMethodDispatcher as doing it there + # requires a circular reference to the Phase, and this ends up with a significant + # (CPython 2.7, 3.8) GC cost when parsing many short inputs + name = token["name"] + # In Py2, using `in` is quicker in general than try/except KeyError + # In Py3, `in` is quicker when there are few cache hits (typically short inputs) + if name in self.__startTagCache: + func = self.__startTagCache[name] + else: + func = self.__startTagCache[name] = self.startTagHandler[name] + # bound the cache size in case we get loads of unknown tags + while len(self.__startTagCache) > len(self.startTagHandler) * 1.1: + # this makes the eviction policy random on Py < 3.7 and FIFO >= 3.7 + self.__startTagCache.pop(next(iter(self.__startTagCache))) + return func(token) def startTagHtml(self, token): if not self.parser.firstStartTag and token["name"] == "html": @@ -482,9 +479,25 @@ def startTagHtml(self, token): self.parser.firstStartTag = False def processEndTag(self, token): - return self.endTagHandler[token["name"]](token) + # Note the caching is done here rather than BoundMethodDispatcher as doing it there + # requires a circular reference to the Phase, and this ends up with a significant + # (CPython 2.7, 3.8) GC cost when parsing many short inputs + name = token["name"] + # In Py2, using `in` is quicker in general than try/except KeyError + # In Py3, `in` is quicker when there are few cache hits (typically short inputs) + if name in self.__endTagCache: + func = self.__endTagCache[name] + else: + func = self.__endTagCache[name] = self.endTagHandler[name] + # bound the cache size in case we get loads of unknown tags + while len(self.__endTagCache) > len(self.endTagHandler) * 1.1: + # this makes the eviction policy random on Py < 3.7 and FIFO >= 3.7 + self.__endTagCache.pop(next(iter(self.__endTagCache))) + return func(token) class InitialPhase(Phase): + __slots__ = tuple() + def processSpaceCharacters(self, token): pass @@ -613,6 +626,8 @@ def processEOF(self): return True class BeforeHtmlPhase(Phase): + __slots__ = tuple() + # helper methods def insertHtmlElement(self): self.tree.insertRoot(impliedTagToken("html", "StartTag")) @@ -648,19 +663,7 @@ def processEndTag(self, token): return token class BeforeHeadPhase(Phase): - def __init__(self, parser, tree): - Phase.__init__(self, parser, tree) - - self.startTagHandler = _utils.MethodDispatcher([ - ("html", self.startTagHtml), - ("head", self.startTagHead) - ]) - self.startTagHandler.default = self.startTagOther - - self.endTagHandler = _utils.MethodDispatcher([ - (("head", "body", "html", "br"), self.endTagImplyHead) - ]) - self.endTagHandler.default = self.endTagOther + __slots__ = tuple() def processEOF(self): self.startTagHead(impliedTagToken("head", "StartTag")) @@ -693,28 +696,19 @@ def endTagOther(self, token): self.parser.parseError("end-tag-after-implied-root", {"name": token["name"]}) + startTagHandler = _utils.MethodDispatcher([ + ("html", startTagHtml), + ("head", startTagHead) + ]) + startTagHandler.default = startTagOther + + endTagHandler = _utils.MethodDispatcher([ + (("head", "body", "html", "br"), endTagImplyHead) + ]) + endTagHandler.default = endTagOther + class InHeadPhase(Phase): - def __init__(self, parser, tree): - Phase.__init__(self, parser, tree) - - self.startTagHandler = _utils.MethodDispatcher([ - ("html", self.startTagHtml), - ("title", self.startTagTitle), - (("noframes", "style"), self.startTagNoFramesStyle), - ("noscript", self.startTagNoscript), - ("script", self.startTagScript), - (("base", "basefont", "bgsound", "command", "link"), - self.startTagBaseLinkCommand), - ("meta", self.startTagMeta), - ("head", self.startTagHead) - ]) - self.startTagHandler.default = self.startTagOther - - self.endTagHandler = _utils.MethodDispatcher([ - ("head", self.endTagHead), - (("br", "html", "body"), self.endTagHtmlBodyBr) - ]) - self.endTagHandler.default = self.endTagOther + __slots__ = tuple() # the real thing def processEOF(self): @@ -796,22 +790,27 @@ def endTagOther(self, token): def anythingElse(self): self.endTagHead(impliedTagToken("head")) - class InHeadNoscriptPhase(Phase): - def __init__(self, parser, tree): - Phase.__init__(self, parser, tree) + startTagHandler = _utils.MethodDispatcher([ + ("html", startTagHtml), + ("title", startTagTitle), + (("noframes", "style"), startTagNoFramesStyle), + ("noscript", startTagNoscript), + ("script", startTagScript), + (("base", "basefont", "bgsound", "command", "link"), + startTagBaseLinkCommand), + ("meta", startTagMeta), + ("head", startTagHead) + ]) + startTagHandler.default = startTagOther + + endTagHandler = _utils.MethodDispatcher([ + ("head", endTagHead), + (("br", "html", "body"), endTagHtmlBodyBr) + ]) + endTagHandler.default = endTagOther - self.startTagHandler = _utils.MethodDispatcher([ - ("html", self.startTagHtml), - (("basefont", "bgsound", "link", "meta", "noframes", "style"), self.startTagBaseLinkCommand), - (("head", "noscript"), self.startTagHeadNoscript), - ]) - self.startTagHandler.default = self.startTagOther - - self.endTagHandler = _utils.MethodDispatcher([ - ("noscript", self.endTagNoscript), - ("br", self.endTagBr), - ]) - self.endTagHandler.default = self.endTagOther + class InHeadNoscriptPhase(Phase): + __slots__ = tuple() def processEOF(self): self.parser.parseError("eof-in-head-noscript") @@ -860,23 +859,21 @@ def anythingElse(self): # Caller must raise parse error first! self.endTagNoscript(impliedTagToken("noscript")) + startTagHandler = _utils.MethodDispatcher([ + ("html", startTagHtml), + (("basefont", "bgsound", "link", "meta", "noframes", "style"), startTagBaseLinkCommand), + (("head", "noscript"), startTagHeadNoscript), + ]) + startTagHandler.default = startTagOther + + endTagHandler = _utils.MethodDispatcher([ + ("noscript", endTagNoscript), + ("br", endTagBr), + ]) + endTagHandler.default = endTagOther + class AfterHeadPhase(Phase): - def __init__(self, parser, tree): - Phase.__init__(self, parser, tree) - - self.startTagHandler = _utils.MethodDispatcher([ - ("html", self.startTagHtml), - ("body", self.startTagBody), - ("frameset", self.startTagFrameset), - (("base", "basefont", "bgsound", "link", "meta", "noframes", "script", - "style", "title"), - self.startTagFromHead), - ("head", self.startTagHead) - ]) - self.startTagHandler.default = self.startTagOther - self.endTagHandler = _utils.MethodDispatcher([(("body", "html", "br"), - self.endTagHtmlBodyBr)]) - self.endTagHandler.default = self.endTagOther + __slots__ = tuple() def processEOF(self): self.anythingElse() @@ -927,80 +924,30 @@ def anythingElse(self): self.parser.phase = self.parser.phases["inBody"] self.parser.framesetOK = True + startTagHandler = _utils.MethodDispatcher([ + ("html", startTagHtml), + ("body", startTagBody), + ("frameset", startTagFrameset), + (("base", "basefont", "bgsound", "link", "meta", "noframes", "script", + "style", "title"), + startTagFromHead), + ("head", startTagHead) + ]) + startTagHandler.default = startTagOther + endTagHandler = _utils.MethodDispatcher([(("body", "html", "br"), + endTagHtmlBodyBr)]) + endTagHandler.default = endTagOther + class InBodyPhase(Phase): # http://www.whatwg.org/specs/web-apps/current-work/#parsing-main-inbody # the really-really-really-very crazy mode - def __init__(self, parser, tree): - Phase.__init__(self, parser, tree) + __slots__ = ("processSpaceCharacters",) + def __init__(self, *args, **kwargs): + super(InBodyPhase, self).__init__(*args, **kwargs) # Set this to the default handler self.processSpaceCharacters = self.processSpaceCharactersNonPre - self.startTagHandler = _utils.MethodDispatcher([ - ("html", self.startTagHtml), - (("base", "basefont", "bgsound", "command", "link", "meta", - "script", "style", "title"), - self.startTagProcessInHead), - ("body", self.startTagBody), - ("frameset", self.startTagFrameset), - (("address", "article", "aside", "blockquote", "center", "details", - "dir", "div", "dl", "fieldset", "figcaption", "figure", - "footer", "header", "hgroup", "main", "menu", "nav", "ol", "p", - "section", "summary", "ul"), - self.startTagCloseP), - (headingElements, self.startTagHeading), - (("pre", "listing"), self.startTagPreListing), - ("form", self.startTagForm), - (("li", "dd", "dt"), self.startTagListItem), - ("plaintext", self.startTagPlaintext), - ("a", self.startTagA), - (("b", "big", "code", "em", "font", "i", "s", "small", "strike", - "strong", "tt", "u"), self.startTagFormatting), - ("nobr", self.startTagNobr), - ("button", self.startTagButton), - (("applet", "marquee", "object"), self.startTagAppletMarqueeObject), - ("xmp", self.startTagXmp), - ("table", self.startTagTable), - (("area", "br", "embed", "img", "keygen", "wbr"), - self.startTagVoidFormatting), - (("param", "source", "track"), self.startTagParamSource), - ("input", self.startTagInput), - ("hr", self.startTagHr), - ("image", self.startTagImage), - ("isindex", self.startTagIsIndex), - ("textarea", self.startTagTextarea), - ("iframe", self.startTagIFrame), - ("noscript", self.startTagNoscript), - (("noembed", "noframes"), self.startTagRawtext), - ("select", self.startTagSelect), - (("rp", "rt"), self.startTagRpRt), - (("option", "optgroup"), self.startTagOpt), - (("math"), self.startTagMath), - (("svg"), self.startTagSvg), - (("caption", "col", "colgroup", "frame", "head", - "tbody", "td", "tfoot", "th", "thead", - "tr"), self.startTagMisplaced) - ]) - self.startTagHandler.default = self.startTagOther - - self.endTagHandler = _utils.MethodDispatcher([ - ("body", self.endTagBody), - ("html", self.endTagHtml), - (("address", "article", "aside", "blockquote", "button", "center", - "details", "dialog", "dir", "div", "dl", "fieldset", "figcaption", "figure", - "footer", "header", "hgroup", "listing", "main", "menu", "nav", "ol", "pre", - "section", "summary", "ul"), self.endTagBlock), - ("form", self.endTagForm), - ("p", self.endTagP), - (("dd", "dt", "li"), self.endTagListItem), - (headingElements, self.endTagHeading), - (("a", "b", "big", "code", "em", "font", "i", "nobr", "s", "small", - "strike", "strong", "tt", "u"), self.endTagFormatting), - (("applet", "marquee", "object"), self.endTagAppletMarqueeObject), - ("br", self.endTagBr), - ]) - self.endTagHandler.default = self.endTagOther - def isMatchingFormattingElement(self, node1, node2): return (node1.name == node2.name and node1.namespace == node2.namespace and @@ -1650,14 +1597,73 @@ def endTagOther(self, token): self.parser.parseError("unexpected-end-tag", {"name": token["name"]}) break + startTagHandler = _utils.MethodDispatcher([ + ("html", Phase.startTagHtml), + (("base", "basefont", "bgsound", "command", "link", "meta", + "script", "style", "title"), + startTagProcessInHead), + ("body", startTagBody), + ("frameset", startTagFrameset), + (("address", "article", "aside", "blockquote", "center", "details", + "dir", "div", "dl", "fieldset", "figcaption", "figure", + "footer", "header", "hgroup", "main", "menu", "nav", "ol", "p", + "section", "summary", "ul"), + startTagCloseP), + (headingElements, startTagHeading), + (("pre", "listing"), startTagPreListing), + ("form", startTagForm), + (("li", "dd", "dt"), startTagListItem), + ("plaintext", startTagPlaintext), + ("a", startTagA), + (("b", "big", "code", "em", "font", "i", "s", "small", "strike", + "strong", "tt", "u"), startTagFormatting), + ("nobr", startTagNobr), + ("button", startTagButton), + (("applet", "marquee", "object"), startTagAppletMarqueeObject), + ("xmp", startTagXmp), + ("table", startTagTable), + (("area", "br", "embed", "img", "keygen", "wbr"), + startTagVoidFormatting), + (("param", "source", "track"), startTagParamSource), + ("input", startTagInput), + ("hr", startTagHr), + ("image", startTagImage), + ("isindex", startTagIsIndex), + ("textarea", startTagTextarea), + ("iframe", startTagIFrame), + ("noscript", startTagNoscript), + (("noembed", "noframes"), startTagRawtext), + ("select", startTagSelect), + (("rp", "rt"), startTagRpRt), + (("option", "optgroup"), startTagOpt), + (("math"), startTagMath), + (("svg"), startTagSvg), + (("caption", "col", "colgroup", "frame", "head", + "tbody", "td", "tfoot", "th", "thead", + "tr"), startTagMisplaced) + ]) + startTagHandler.default = startTagOther + + endTagHandler = _utils.MethodDispatcher([ + ("body", endTagBody), + ("html", endTagHtml), + (("address", "article", "aside", "blockquote", "button", "center", + "details", "dialog", "dir", "div", "dl", "fieldset", "figcaption", "figure", + "footer", "header", "hgroup", "listing", "main", "menu", "nav", "ol", "pre", + "section", "summary", "ul"), endTagBlock), + ("form", endTagForm), + ("p", endTagP), + (("dd", "dt", "li"), endTagListItem), + (headingElements, endTagHeading), + (("a", "b", "big", "code", "em", "font", "i", "nobr", "s", "small", + "strike", "strong", "tt", "u"), endTagFormatting), + (("applet", "marquee", "object"), endTagAppletMarqueeObject), + ("br", endTagBr), + ]) + endTagHandler.default = endTagOther + class TextPhase(Phase): - def __init__(self, parser, tree): - Phase.__init__(self, parser, tree) - self.startTagHandler = _utils.MethodDispatcher([]) - self.startTagHandler.default = self.startTagOther - self.endTagHandler = _utils.MethodDispatcher([ - ("script", self.endTagScript)]) - self.endTagHandler.default = self.endTagOther + __slots__ = tuple() def processCharacters(self, token): self.tree.insertText(token["data"]) @@ -1683,30 +1689,15 @@ def endTagOther(self, token): self.tree.openElements.pop() self.parser.phase = self.parser.originalPhase + startTagHandler = _utils.MethodDispatcher([]) + startTagHandler.default = startTagOther + endTagHandler = _utils.MethodDispatcher([ + ("script", endTagScript)]) + endTagHandler.default = endTagOther + class InTablePhase(Phase): # http://www.whatwg.org/specs/web-apps/current-work/#in-table - def __init__(self, parser, tree): - Phase.__init__(self, parser, tree) - self.startTagHandler = _utils.MethodDispatcher([ - ("html", self.startTagHtml), - ("caption", self.startTagCaption), - ("colgroup", self.startTagColgroup), - ("col", self.startTagCol), - (("tbody", "tfoot", "thead"), self.startTagRowGroup), - (("td", "th", "tr"), self.startTagImplyTbody), - ("table", self.startTagTable), - (("style", "script"), self.startTagStyleScript), - ("input", self.startTagInput), - ("form", self.startTagForm) - ]) - self.startTagHandler.default = self.startTagOther - - self.endTagHandler = _utils.MethodDispatcher([ - ("table", self.endTagTable), - (("body", "caption", "col", "colgroup", "html", "tbody", "td", - "tfoot", "th", "thead", "tr"), self.endTagIgnore) - ]) - self.endTagHandler.default = self.endTagOther + __slots__ = tuple() # helper methods def clearStackToTableContext(self): @@ -1828,9 +1819,32 @@ def endTagOther(self, token): self.parser.phases["inBody"].processEndTag(token) self.tree.insertFromTable = False + startTagHandler = _utils.MethodDispatcher([ + ("html", Phase.startTagHtml), + ("caption", startTagCaption), + ("colgroup", startTagColgroup), + ("col", startTagCol), + (("tbody", "tfoot", "thead"), startTagRowGroup), + (("td", "th", "tr"), startTagImplyTbody), + ("table", startTagTable), + (("style", "script"), startTagStyleScript), + ("input", startTagInput), + ("form", startTagForm) + ]) + startTagHandler.default = startTagOther + + endTagHandler = _utils.MethodDispatcher([ + ("table", endTagTable), + (("body", "caption", "col", "colgroup", "html", "tbody", "td", + "tfoot", "th", "thead", "tr"), endTagIgnore) + ]) + endTagHandler.default = endTagOther + class InTableTextPhase(Phase): - def __init__(self, parser, tree): - Phase.__init__(self, parser, tree) + __slots__ = ("originalPhase", "characterTokens") + + def __init__(self, *args, **kwargs): + super(InTableTextPhase, self).__init__(*args, **kwargs) self.originalPhase = None self.characterTokens = [] @@ -1875,23 +1889,7 @@ def processEndTag(self, token): class InCaptionPhase(Phase): # http://www.whatwg.org/specs/web-apps/current-work/#in-caption - def __init__(self, parser, tree): - Phase.__init__(self, parser, tree) - - self.startTagHandler = _utils.MethodDispatcher([ - ("html", self.startTagHtml), - (("caption", "col", "colgroup", "tbody", "td", "tfoot", "th", - "thead", "tr"), self.startTagTableElement) - ]) - self.startTagHandler.default = self.startTagOther - - self.endTagHandler = _utils.MethodDispatcher([ - ("caption", self.endTagCaption), - ("table", self.endTagTable), - (("body", "col", "colgroup", "html", "tbody", "td", "tfoot", "th", - "thead", "tr"), self.endTagIgnore) - ]) - self.endTagHandler.default = self.endTagOther + __slots__ = tuple() def ignoreEndTagCaption(self): return not self.tree.elementInScope("caption", variant="table") @@ -1944,23 +1942,24 @@ def endTagIgnore(self, token): def endTagOther(self, token): return self.parser.phases["inBody"].processEndTag(token) + startTagHandler = _utils.MethodDispatcher([ + ("html", Phase.startTagHtml), + (("caption", "col", "colgroup", "tbody", "td", "tfoot", "th", + "thead", "tr"), startTagTableElement) + ]) + startTagHandler.default = startTagOther + + endTagHandler = _utils.MethodDispatcher([ + ("caption", endTagCaption), + ("table", endTagTable), + (("body", "col", "colgroup", "html", "tbody", "td", "tfoot", "th", + "thead", "tr"), endTagIgnore) + ]) + endTagHandler.default = endTagOther + class InColumnGroupPhase(Phase): # http://www.whatwg.org/specs/web-apps/current-work/#in-column - - def __init__(self, parser, tree): - Phase.__init__(self, parser, tree) - - self.startTagHandler = _utils.MethodDispatcher([ - ("html", self.startTagHtml), - ("col", self.startTagCol) - ]) - self.startTagHandler.default = self.startTagOther - - self.endTagHandler = _utils.MethodDispatcher([ - ("colgroup", self.endTagColgroup), - ("col", self.endTagCol) - ]) - self.endTagHandler.default = self.endTagOther + __slots__ = tuple() def ignoreEndTagColgroup(self): return self.tree.openElements[-1].name == "html" @@ -2010,26 +2009,21 @@ def endTagOther(self, token): if not ignoreEndTag: return token + startTagHandler = _utils.MethodDispatcher([ + ("html", Phase.startTagHtml), + ("col", startTagCol) + ]) + startTagHandler.default = startTagOther + + endTagHandler = _utils.MethodDispatcher([ + ("colgroup", endTagColgroup), + ("col", endTagCol) + ]) + endTagHandler.default = endTagOther + class InTableBodyPhase(Phase): # http://www.whatwg.org/specs/web-apps/current-work/#in-table0 - def __init__(self, parser, tree): - Phase.__init__(self, parser, tree) - self.startTagHandler = _utils.MethodDispatcher([ - ("html", self.startTagHtml), - ("tr", self.startTagTr), - (("td", "th"), self.startTagTableCell), - (("caption", "col", "colgroup", "tbody", "tfoot", "thead"), - self.startTagTableOther) - ]) - self.startTagHandler.default = self.startTagOther - - self.endTagHandler = _utils.MethodDispatcher([ - (("tbody", "tfoot", "thead"), self.endTagTableRowGroup), - ("table", self.endTagTable), - (("body", "caption", "col", "colgroup", "html", "td", "th", - "tr"), self.endTagIgnore) - ]) - self.endTagHandler.default = self.endTagOther + __slots__ = tuple() # helper methods def clearStackToTableBodyContext(self): @@ -2108,26 +2102,26 @@ def endTagIgnore(self, token): def endTagOther(self, token): return self.parser.phases["inTable"].processEndTag(token) + startTagHandler = _utils.MethodDispatcher([ + ("html", Phase.startTagHtml), + ("tr", startTagTr), + (("td", "th"), startTagTableCell), + (("caption", "col", "colgroup", "tbody", "tfoot", "thead"), + startTagTableOther) + ]) + startTagHandler.default = startTagOther + + endTagHandler = _utils.MethodDispatcher([ + (("tbody", "tfoot", "thead"), endTagTableRowGroup), + ("table", endTagTable), + (("body", "caption", "col", "colgroup", "html", "td", "th", + "tr"), endTagIgnore) + ]) + endTagHandler.default = endTagOther + class InRowPhase(Phase): # http://www.whatwg.org/specs/web-apps/current-work/#in-row - def __init__(self, parser, tree): - Phase.__init__(self, parser, tree) - self.startTagHandler = _utils.MethodDispatcher([ - ("html", self.startTagHtml), - (("td", "th"), self.startTagTableCell), - (("caption", "col", "colgroup", "tbody", "tfoot", "thead", - "tr"), self.startTagTableOther) - ]) - self.startTagHandler.default = self.startTagOther - - self.endTagHandler = _utils.MethodDispatcher([ - ("tr", self.endTagTr), - ("table", self.endTagTable), - (("tbody", "tfoot", "thead"), self.endTagTableRowGroup), - (("body", "caption", "col", "colgroup", "html", "td", "th"), - self.endTagIgnore) - ]) - self.endTagHandler.default = self.endTagOther + __slots__ = tuple() # helper methods (XXX unify this with other table helper methods) def clearStackToTableRowContext(self): @@ -2197,23 +2191,26 @@ def endTagIgnore(self, token): def endTagOther(self, token): return self.parser.phases["inTable"].processEndTag(token) + startTagHandler = _utils.MethodDispatcher([ + ("html", Phase.startTagHtml), + (("td", "th"), startTagTableCell), + (("caption", "col", "colgroup", "tbody", "tfoot", "thead", + "tr"), startTagTableOther) + ]) + startTagHandler.default = startTagOther + + endTagHandler = _utils.MethodDispatcher([ + ("tr", endTagTr), + ("table", endTagTable), + (("tbody", "tfoot", "thead"), endTagTableRowGroup), + (("body", "caption", "col", "colgroup", "html", "td", "th"), + endTagIgnore) + ]) + endTagHandler.default = endTagOther + class InCellPhase(Phase): # http://www.whatwg.org/specs/web-apps/current-work/#in-cell - def __init__(self, parser, tree): - Phase.__init__(self, parser, tree) - self.startTagHandler = _utils.MethodDispatcher([ - ("html", self.startTagHtml), - (("caption", "col", "colgroup", "tbody", "td", "tfoot", "th", - "thead", "tr"), self.startTagTableOther) - ]) - self.startTagHandler.default = self.startTagOther - - self.endTagHandler = _utils.MethodDispatcher([ - (("td", "th"), self.endTagTableCell), - (("body", "caption", "col", "colgroup", "html"), self.endTagIgnore), - (("table", "tbody", "tfoot", "thead", "tr"), self.endTagImply) - ]) - self.endTagHandler.default = self.endTagOther + __slots__ = tuple() # helper def closeCell(self): @@ -2273,26 +2270,22 @@ def endTagImply(self, token): def endTagOther(self, token): return self.parser.phases["inBody"].processEndTag(token) + startTagHandler = _utils.MethodDispatcher([ + ("html", Phase.startTagHtml), + (("caption", "col", "colgroup", "tbody", "td", "tfoot", "th", + "thead", "tr"), startTagTableOther) + ]) + startTagHandler.default = startTagOther + + endTagHandler = _utils.MethodDispatcher([ + (("td", "th"), endTagTableCell), + (("body", "caption", "col", "colgroup", "html"), endTagIgnore), + (("table", "tbody", "tfoot", "thead", "tr"), endTagImply) + ]) + endTagHandler.default = endTagOther + class InSelectPhase(Phase): - def __init__(self, parser, tree): - Phase.__init__(self, parser, tree) - - self.startTagHandler = _utils.MethodDispatcher([ - ("html", self.startTagHtml), - ("option", self.startTagOption), - ("optgroup", self.startTagOptgroup), - ("select", self.startTagSelect), - (("input", "keygen", "textarea"), self.startTagInput), - ("script", self.startTagScript) - ]) - self.startTagHandler.default = self.startTagOther - - self.endTagHandler = _utils.MethodDispatcher([ - ("option", self.endTagOption), - ("optgroup", self.endTagOptgroup), - ("select", self.endTagSelect) - ]) - self.endTagHandler.default = self.endTagOther + __slots__ = tuple() # http://www.whatwg.org/specs/web-apps/current-work/#in-select def processEOF(self): @@ -2373,21 +2366,25 @@ def endTagOther(self, token): self.parser.parseError("unexpected-end-tag-in-select", {"name": token["name"]}) - class InSelectInTablePhase(Phase): - def __init__(self, parser, tree): - Phase.__init__(self, parser, tree) - - self.startTagHandler = _utils.MethodDispatcher([ - (("caption", "table", "tbody", "tfoot", "thead", "tr", "td", "th"), - self.startTagTable) - ]) - self.startTagHandler.default = self.startTagOther + startTagHandler = _utils.MethodDispatcher([ + ("html", Phase.startTagHtml), + ("option", startTagOption), + ("optgroup", startTagOptgroup), + ("select", startTagSelect), + (("input", "keygen", "textarea"), startTagInput), + ("script", startTagScript) + ]) + startTagHandler.default = startTagOther + + endTagHandler = _utils.MethodDispatcher([ + ("option", endTagOption), + ("optgroup", endTagOptgroup), + ("select", endTagSelect) + ]) + endTagHandler.default = endTagOther - self.endTagHandler = _utils.MethodDispatcher([ - (("caption", "table", "tbody", "tfoot", "thead", "tr", "td", "th"), - self.endTagTable) - ]) - self.endTagHandler.default = self.endTagOther + class InSelectInTablePhase(Phase): + __slots__ = tuple() def processEOF(self): self.parser.phases["inSelect"].processEOF() @@ -2412,7 +2409,21 @@ def endTagTable(self, token): def endTagOther(self, token): return self.parser.phases["inSelect"].processEndTag(token) + startTagHandler = _utils.MethodDispatcher([ + (("caption", "table", "tbody", "tfoot", "thead", "tr", "td", "th"), + startTagTable) + ]) + startTagHandler.default = startTagOther + + endTagHandler = _utils.MethodDispatcher([ + (("caption", "table", "tbody", "tfoot", "thead", "tr", "td", "th"), + endTagTable) + ]) + endTagHandler.default = endTagOther + class InForeignContentPhase(Phase): + __slots__ = tuple() + breakoutElements = frozenset(["b", "big", "blockquote", "body", "br", "center", "code", "dd", "div", "dl", "dt", "em", "embed", "h1", "h2", "h3", @@ -2422,9 +2433,6 @@ class InForeignContentPhase(Phase): "span", "strong", "strike", "sub", "sup", "table", "tt", "u", "ul", "var"]) - def __init__(self, parser, tree): - Phase.__init__(self, parser, tree) - def adjustSVGTagNames(self, token): replacements = {"altglyph": "altGlyph", "altglyphdef": "altGlyphDef", @@ -2478,7 +2486,7 @@ def processStartTag(self, token): currentNode = self.tree.openElements[-1] if (token["name"] in self.breakoutElements or (token["name"] == "font" and - set(token["data"].keys()) & set(["color", "face", "size"]))): + set(token["data"].keys()) & {"color", "face", "size"})): self.parser.parseError("unexpected-html-element-in-foreign-content", {"name": token["name"]}) while (self.tree.openElements[-1].namespace != @@ -2528,16 +2536,7 @@ def processEndTag(self, token): return new_token class AfterBodyPhase(Phase): - def __init__(self, parser, tree): - Phase.__init__(self, parser, tree) - - self.startTagHandler = _utils.MethodDispatcher([ - ("html", self.startTagHtml) - ]) - self.startTagHandler.default = self.startTagOther - - self.endTagHandler = _utils.MethodDispatcher([("html", self.endTagHtml)]) - self.endTagHandler.default = self.endTagOther + __slots__ = tuple() def processEOF(self): # Stop parsing @@ -2574,23 +2573,17 @@ def endTagOther(self, token): self.parser.phase = self.parser.phases["inBody"] return token - class InFramesetPhase(Phase): - # http://www.whatwg.org/specs/web-apps/current-work/#in-frameset - def __init__(self, parser, tree): - Phase.__init__(self, parser, tree) + startTagHandler = _utils.MethodDispatcher([ + ("html", startTagHtml) + ]) + startTagHandler.default = startTagOther - self.startTagHandler = _utils.MethodDispatcher([ - ("html", self.startTagHtml), - ("frameset", self.startTagFrameset), - ("frame", self.startTagFrame), - ("noframes", self.startTagNoframes) - ]) - self.startTagHandler.default = self.startTagOther + endTagHandler = _utils.MethodDispatcher([("html", endTagHtml)]) + endTagHandler.default = endTagOther - self.endTagHandler = _utils.MethodDispatcher([ - ("frameset", self.endTagFrameset) - ]) - self.endTagHandler.default = self.endTagOther + class InFramesetPhase(Phase): + # http://www.whatwg.org/specs/web-apps/current-work/#in-frameset + __slots__ = tuple() def processEOF(self): if self.tree.openElements[-1].name != "html": @@ -2631,21 +2624,22 @@ def endTagOther(self, token): self.parser.parseError("unexpected-end-tag-in-frameset", {"name": token["name"]}) - class AfterFramesetPhase(Phase): - # http://www.whatwg.org/specs/web-apps/current-work/#after3 - def __init__(self, parser, tree): - Phase.__init__(self, parser, tree) + startTagHandler = _utils.MethodDispatcher([ + ("html", Phase.startTagHtml), + ("frameset", startTagFrameset), + ("frame", startTagFrame), + ("noframes", startTagNoframes) + ]) + startTagHandler.default = startTagOther - self.startTagHandler = _utils.MethodDispatcher([ - ("html", self.startTagHtml), - ("noframes", self.startTagNoframes) - ]) - self.startTagHandler.default = self.startTagOther + endTagHandler = _utils.MethodDispatcher([ + ("frameset", endTagFrameset) + ]) + endTagHandler.default = endTagOther - self.endTagHandler = _utils.MethodDispatcher([ - ("html", self.endTagHtml) - ]) - self.endTagHandler.default = self.endTagOther + class AfterFramesetPhase(Phase): + # http://www.whatwg.org/specs/web-apps/current-work/#after3 + __slots__ = tuple() def processEOF(self): # Stop parsing @@ -2668,14 +2662,19 @@ def endTagOther(self, token): self.parser.parseError("unexpected-end-tag-after-frameset", {"name": token["name"]}) - class AfterAfterBodyPhase(Phase): - def __init__(self, parser, tree): - Phase.__init__(self, parser, tree) + startTagHandler = _utils.MethodDispatcher([ + ("html", Phase.startTagHtml), + ("noframes", startTagNoframes) + ]) + startTagHandler.default = startTagOther - self.startTagHandler = _utils.MethodDispatcher([ - ("html", self.startTagHtml) - ]) - self.startTagHandler.default = self.startTagOther + endTagHandler = _utils.MethodDispatcher([ + ("html", endTagHtml) + ]) + endTagHandler.default = endTagOther + + class AfterAfterBodyPhase(Phase): + __slots__ = tuple() def processEOF(self): pass @@ -2706,15 +2705,13 @@ def processEndTag(self, token): self.parser.phase = self.parser.phases["inBody"] return token - class AfterAfterFramesetPhase(Phase): - def __init__(self, parser, tree): - Phase.__init__(self, parser, tree) + startTagHandler = _utils.MethodDispatcher([ + ("html", startTagHtml) + ]) + startTagHandler.default = startTagOther - self.startTagHandler = _utils.MethodDispatcher([ - ("html", self.startTagHtml), - ("noframes", self.startTagNoFrames) - ]) - self.startTagHandler.default = self.startTagOther + class AfterAfterFramesetPhase(Phase): + __slots__ = tuple() def processEOF(self): pass @@ -2741,6 +2738,13 @@ def startTagOther(self, token): def processEndTag(self, token): self.parser.parseError("expected-eof-but-got-end-tag", {"name": token["name"]}) + + startTagHandler = _utils.MethodDispatcher([ + ("html", startTagHtml), + ("noframes", startTagNoFrames) + ]) + startTagHandler.default = startTagOther + # pylint:enable=unused-argument return { @@ -2774,8 +2778,8 @@ def processEndTag(self, token): def adjust_attributes(token, replacements): needs_adjustment = viewkeys(token['data']) & viewkeys(replacements) if needs_adjustment: - token['data'] = OrderedDict((replacements.get(k, k), v) - for k, v in token['data'].items()) + token['data'] = type(token['data'])((replacements.get(k, k), v) + for k, v in token['data'].items()) def impliedTagToken(name, type="EndTag", attributes=None, diff --git a/src/pip/_vendor/html5lib/serializer.py b/src/pip/_vendor/html5lib/serializer.py index 53f4d44c397..d5669d8c149 100644 --- a/src/pip/_vendor/html5lib/serializer.py +++ b/src/pip/_vendor/html5lib/serializer.py @@ -274,7 +274,7 @@ def serialize(self, treewalker, encoding=None): if token["systemId"]: if token["systemId"].find('"') >= 0: if token["systemId"].find("'") >= 0: - self.serializeError("System identifer contains both single and double quote characters") + self.serializeError("System identifier contains both single and double quote characters") quote_char = "'" else: quote_char = '"' diff --git a/src/pip/_vendor/html5lib/treebuilders/base.py b/src/pip/_vendor/html5lib/treebuilders/base.py index 73973db51b8..965fce29d3b 100644 --- a/src/pip/_vendor/html5lib/treebuilders/base.py +++ b/src/pip/_vendor/html5lib/treebuilders/base.py @@ -10,9 +10,9 @@ listElementsMap = { None: (frozenset(scopingElements), False), - "button": (frozenset(scopingElements | set([(namespaces["html"], "button")])), False), - "list": (frozenset(scopingElements | set([(namespaces["html"], "ol"), - (namespaces["html"], "ul")])), False), + "button": (frozenset(scopingElements | {(namespaces["html"], "button")}), False), + "list": (frozenset(scopingElements | {(namespaces["html"], "ol"), + (namespaces["html"], "ul")}), False), "table": (frozenset([(namespaces["html"], "html"), (namespaces["html"], "table")]), False), "select": (frozenset([(namespaces["html"], "optgroup"), @@ -28,7 +28,7 @@ def __init__(self, name): :arg name: The tag name associated with the node """ - # The tag name assocaited with the node + # The tag name associated with the node self.name = name # The parent of the current node (or None for the document node) self.parent = None diff --git a/src/pip/_vendor/html5lib/treebuilders/etree.py b/src/pip/_vendor/html5lib/treebuilders/etree.py index 0dedf441643..ea92dc301fe 100644 --- a/src/pip/_vendor/html5lib/treebuilders/etree.py +++ b/src/pip/_vendor/html5lib/treebuilders/etree.py @@ -5,6 +5,8 @@ import re +from copy import copy + from . import base from .. import _ihatexml from .. import constants @@ -61,16 +63,17 @@ def _getAttributes(self): return self._element.attrib def _setAttributes(self, attributes): - # Delete existing attributes first - # XXX - there may be a better way to do this... - for key in list(self._element.attrib.keys()): - del self._element.attrib[key] - for key, value in attributes.items(): - if isinstance(key, tuple): - name = "{%s}%s" % (key[2], key[1]) - else: - name = key - self._element.set(name, value) + el_attrib = self._element.attrib + el_attrib.clear() + if attributes: + # calling .items _always_ allocates, and the above truthy check is cheaper than the + # allocation on average + for key, value in attributes.items(): + if isinstance(key, tuple): + name = "{%s}%s" % (key[2], key[1]) + else: + name = key + el_attrib[name] = value attributes = property(_getAttributes, _setAttributes) @@ -129,8 +132,8 @@ def insertText(self, data, insertBefore=None): def cloneNode(self): element = type(self)(self.name, self.namespace) - for name, value in self.attributes.items(): - element.attributes[name] = value + if self._element.attrib: + element._element.attrib = copy(self._element.attrib) return element def reparentChildren(self, newParent): diff --git a/src/pip/_vendor/html5lib/treebuilders/etree_lxml.py b/src/pip/_vendor/html5lib/treebuilders/etree_lxml.py index ca12a99cccf..f037759f42e 100644 --- a/src/pip/_vendor/html5lib/treebuilders/etree_lxml.py +++ b/src/pip/_vendor/html5lib/treebuilders/etree_lxml.py @@ -16,6 +16,11 @@ import re import sys +try: + from collections.abc import MutableMapping +except ImportError: + from collections import MutableMapping + from . import base from ..constants import DataLossWarning from .. import constants @@ -23,6 +28,7 @@ from .. import _ihatexml import lxml.etree as etree +from pip._vendor.six import PY3, binary_type fullTree = True @@ -44,7 +50,11 @@ def __init__(self): self._childNodes = [] def appendChild(self, element): - self._elementTree.getroot().addnext(element._element) + last = self._elementTree.getroot() + for last in self._elementTree.getroot().itersiblings(): + pass + + last.addnext(element._element) def _getChildNodes(self): return self._childNodes @@ -185,26 +195,37 @@ def __init__(self, namespaceHTMLElements, fullTree=False): infosetFilter = self.infosetFilter = _ihatexml.InfosetFilter(preventDoubleDashComments=True) self.namespaceHTMLElements = namespaceHTMLElements - class Attributes(dict): - def __init__(self, element, value=None): - if value is None: - value = {} + class Attributes(MutableMapping): + def __init__(self, element): self._element = element - dict.__init__(self, value) # pylint:disable=non-parent-init-called - for key, value in self.items(): - if isinstance(key, tuple): - name = "{%s}%s" % (key[2], infosetFilter.coerceAttribute(key[1])) - else: - name = infosetFilter.coerceAttribute(key) - self._element._element.attrib[name] = value - def __setitem__(self, key, value): - dict.__setitem__(self, key, value) + def _coerceKey(self, key): if isinstance(key, tuple): name = "{%s}%s" % (key[2], infosetFilter.coerceAttribute(key[1])) else: name = infosetFilter.coerceAttribute(key) - self._element._element.attrib[name] = value + return name + + def __getitem__(self, key): + value = self._element._element.attrib[self._coerceKey(key)] + if not PY3 and isinstance(value, binary_type): + value = value.decode("ascii") + return value + + def __setitem__(self, key, value): + self._element._element.attrib[self._coerceKey(key)] = value + + def __delitem__(self, key): + del self._element._element.attrib[self._coerceKey(key)] + + def __iter__(self): + return iter(self._element._element.attrib) + + def __len__(self): + return len(self._element._element.attrib) + + def clear(self): + return self._element._element.attrib.clear() class Element(builder.Element): def __init__(self, name, namespace): @@ -225,8 +246,10 @@ def _getName(self): def _getAttributes(self): return self._attributes - def _setAttributes(self, attributes): - self._attributes = Attributes(self, attributes) + def _setAttributes(self, value): + attributes = self.attributes + attributes.clear() + attributes.update(value) attributes = property(_getAttributes, _setAttributes) @@ -234,8 +257,11 @@ def insertText(self, data, insertBefore=None): data = infosetFilter.coerceCharacters(data) builder.Element.insertText(self, data, insertBefore) - def appendChild(self, child): - builder.Element.appendChild(self, child) + def cloneNode(self): + element = type(self)(self.name, self.namespace) + if self._element.attrib: + element._element.attrib.update(self._element.attrib) + return element class Comment(builder.Comment): def __init__(self, data): diff --git a/src/pip/_vendor/html5lib/treewalkers/__init__.py b/src/pip/_vendor/html5lib/treewalkers/__init__.py index 9bec2076f3f..b2d3aac3137 100644 --- a/src/pip/_vendor/html5lib/treewalkers/__init__.py +++ b/src/pip/_vendor/html5lib/treewalkers/__init__.py @@ -2,10 +2,10 @@ tree, generating tokens identical to those produced by the tokenizer module. -To create a tree walker for a new type of tree, you need to do +To create a tree walker for a new type of tree, you need to implement a tree walker object (called TreeWalker by convention) that -implements a 'serialize' method taking a tree as sole argument and -returning an iterator generating tokens. +implements a 'serialize' method which takes a tree as sole argument and +returns an iterator which generates tokens. """ from __future__ import absolute_import, division, unicode_literals diff --git a/src/pip/_vendor/html5lib/treewalkers/etree.py b/src/pip/_vendor/html5lib/treewalkers/etree.py index 95fc0c17030..837b27ec486 100644 --- a/src/pip/_vendor/html5lib/treewalkers/etree.py +++ b/src/pip/_vendor/html5lib/treewalkers/etree.py @@ -127,4 +127,5 @@ def getParentNode(self, node): return locals() + getETreeModule = moduleFactoryFactory(getETreeBuilder) diff --git a/src/pip/_vendor/html5lib/treewalkers/etree_lxml.py b/src/pip/_vendor/html5lib/treewalkers/etree_lxml.py index e81ddf33b2e..c56af390fe2 100644 --- a/src/pip/_vendor/html5lib/treewalkers/etree_lxml.py +++ b/src/pip/_vendor/html5lib/treewalkers/etree_lxml.py @@ -1,6 +1,8 @@ from __future__ import absolute_import, division, unicode_literals from pip._vendor.six import text_type +from collections import OrderedDict + from lxml import etree from ..treebuilders.etree import tag_regexp @@ -163,7 +165,7 @@ def getNodeDetails(self, node): else: namespace = None tag = ensure_str(node.tag) - attrs = {} + attrs = OrderedDict() for name, value in list(node.attrib.items()): name = ensure_str(name) value = ensure_str(value) diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index aca6e357ec0..e73fdb02cb5 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -4,7 +4,7 @@ colorama==0.4.3 contextlib2==0.6.0.post1 distlib==0.3.1 distro==1.5.0 -html5lib==1.0.1 +html5lib==1.1 ipaddress==1.0.23 # Only needed on 2.6 and 2.7 msgpack==1.0.0 packaging==20.3 diff --git a/tools/automation/vendoring/patches/html5lib.patch b/tools/automation/vendoring/patches/html5lib.patch deleted file mode 100644 index ae9cafe2d8e..00000000000 --- a/tools/automation/vendoring/patches/html5lib.patch +++ /dev/null @@ -1,55 +0,0 @@ -diff --git a/src/pip/_vendor/html5lib/_trie/_base.py b/src/pip/_vendor/html5lib/_trie/_base.py -index a1158bbb..6b71975f 100644 ---- a/src/pip/_vendor/html5lib/_trie/_base.py -+++ b/src/pip/_vendor/html5lib/_trie/_base.py -@@ -1,6 +1,9 @@ - from __future__ import absolute_import, division, unicode_literals - --from collections import Mapping -+try: -+ from collections.abc import Mapping -+except ImportError: # Python 2.7 -+ from collections import Mapping - - - class Trie(Mapping): -diff --git a/src/pip/_vendor/html5lib/treebuilders/dom.py b/src/pip/_vendor/html5lib/treebuilders/dom.py -index dcfac220..d8b53004 100644 ---- a/src/pip/_vendor/html5lib/treebuilders/dom.py -+++ b/src/pip/_vendor/html5lib/treebuilders/dom.py -@@ -1,7 +1,10 @@ - from __future__ import absolute_import, division, unicode_literals - - --from collections import MutableMapping -+try: -+ from collections.abc import MutableMapping -+except ImportError: # Python 2.7 -+ from collections import MutableMapping - from xml.dom import minidom, Node - import weakref - -diff --git a/src/pip/_vendor/html5lib/_utils.py b/src/pip/_vendor/html5lib/_utils.py -index 0703afb3..96eb17b2 100644 ---- a/src/pip/_vendor/html5lib/_utils.py -+++ b/src/pip/_vendor/html5lib/_utils.py -@@ -2,12 +2,15 @@ from __future__ import absolute_import, division, unicode_literals - - from types import ModuleType - --from pip._vendor.six import text_type -+from pip._vendor.six import text_type, PY3 - --try: -- import xml.etree.cElementTree as default_etree --except ImportError: -+if PY3: - import xml.etree.ElementTree as default_etree -+else: -+ try: -+ import xml.etree.cElementTree as default_etree -+ except ImportError: -+ import xml.etree.ElementTree as default_etree - - - __all__ = ["default_etree", "MethodDispatcher", "isSurrogatePair", From 413968dd170efe51a2015e450d93cd9d5a153568 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Wed, 22 Jul 2020 05:08:29 +0530 Subject: [PATCH 2296/3170] Upgrade packaging to 20.4 --- news/packaging.vendor | 1 + src/pip/_vendor/packaging/__about__.py | 4 ++-- src/pip/_vendor/packaging/_compat.py | 4 ++-- src/pip/_vendor/packaging/_typing.py | 29 +++++++++++++++-------- src/pip/_vendor/packaging/markers.py | 4 ++-- src/pip/_vendor/packaging/requirements.py | 4 ++-- src/pip/_vendor/packaging/specifiers.py | 26 +++++++++++++++----- src/pip/_vendor/packaging/tags.py | 18 +++++++++++--- src/pip/_vendor/packaging/utils.py | 13 ++++++---- src/pip/_vendor/packaging/version.py | 4 ++-- src/pip/_vendor/vendor.txt | 2 +- 11 files changed, 74 insertions(+), 35 deletions(-) create mode 100644 news/packaging.vendor diff --git a/news/packaging.vendor b/news/packaging.vendor new file mode 100644 index 00000000000..1c69173a95e --- /dev/null +++ b/news/packaging.vendor @@ -0,0 +1 @@ +Upgrade packaging to 20.4 diff --git a/src/pip/_vendor/packaging/__about__.py b/src/pip/_vendor/packaging/__about__.py index 5161d141be7..4d998578d7b 100644 --- a/src/pip/_vendor/packaging/__about__.py +++ b/src/pip/_vendor/packaging/__about__.py @@ -18,10 +18,10 @@ __summary__ = "Core utilities for Python packages" __uri__ = "https://github.com/pypa/packaging" -__version__ = "20.3" +__version__ = "20.4" __author__ = "Donald Stufft and individual contributors" __email__ = "donald@stufft.io" -__license__ = "BSD or Apache License, Version 2.0" +__license__ = "BSD-2-Clause or Apache-2.0" __copyright__ = "Copyright 2014-2019 %s" % __author__ diff --git a/src/pip/_vendor/packaging/_compat.py b/src/pip/_vendor/packaging/_compat.py index a145f7eeb39..e54bd4ede87 100644 --- a/src/pip/_vendor/packaging/_compat.py +++ b/src/pip/_vendor/packaging/_compat.py @@ -5,9 +5,9 @@ import sys -from ._typing import MYPY_CHECK_RUNNING +from ._typing import TYPE_CHECKING -if MYPY_CHECK_RUNNING: # pragma: no cover +if TYPE_CHECKING: # pragma: no cover from typing import Any, Dict, Tuple, Type diff --git a/src/pip/_vendor/packaging/_typing.py b/src/pip/_vendor/packaging/_typing.py index 945b39c30a0..2846133bd8d 100644 --- a/src/pip/_vendor/packaging/_typing.py +++ b/src/pip/_vendor/packaging/_typing.py @@ -18,22 +18,31 @@ In packaging, all static-typing related imports should be guarded as follows: - from pip._vendor.packaging._typing import MYPY_CHECK_RUNNING + from pip._vendor.packaging._typing import TYPE_CHECKING - if MYPY_CHECK_RUNNING: + if TYPE_CHECKING: from typing import ... Ref: https://github.com/python/mypy/issues/3216 """ -MYPY_CHECK_RUNNING = False +__all__ = ["TYPE_CHECKING", "cast"] -if MYPY_CHECK_RUNNING: # pragma: no cover - import typing - - cast = typing.cast +# The TYPE_CHECKING constant defined by the typing module is False at runtime +# but True while type checking. +if False: # pragma: no cover + from typing import TYPE_CHECKING +else: + TYPE_CHECKING = False + +# typing's cast syntax requires calling typing.cast at runtime, but we don't +# want to import typing at runtime. Here, we inform the type checkers that +# we're importing `typing.cast` as `cast` and re-implement typing.cast's +# runtime behavior in a block that is ignored by type checkers. +if TYPE_CHECKING: # pragma: no cover + # not executed at runtime + from typing import cast else: - # typing's cast() is needed at runtime, but we don't want to import typing. - # Thus, we use a dummy no-op version, which we tell mypy to ignore. - def cast(type_, value): # type: ignore + # executed at runtime + def cast(type_, value): # noqa return value diff --git a/src/pip/_vendor/packaging/markers.py b/src/pip/_vendor/packaging/markers.py index b24f8edf934..ed642b01fcc 100644 --- a/src/pip/_vendor/packaging/markers.py +++ b/src/pip/_vendor/packaging/markers.py @@ -13,10 +13,10 @@ from pip._vendor.pyparsing import Literal as L # noqa from ._compat import string_types -from ._typing import MYPY_CHECK_RUNNING +from ._typing import TYPE_CHECKING from .specifiers import Specifier, InvalidSpecifier -if MYPY_CHECK_RUNNING: # pragma: no cover +if TYPE_CHECKING: # pragma: no cover from typing import Any, Callable, Dict, List, Optional, Tuple, Union Operator = Callable[[str, str], bool] diff --git a/src/pip/_vendor/packaging/requirements.py b/src/pip/_vendor/packaging/requirements.py index 1e32a9376ec..5e64101c43d 100644 --- a/src/pip/_vendor/packaging/requirements.py +++ b/src/pip/_vendor/packaging/requirements.py @@ -11,11 +11,11 @@ from pip._vendor.pyparsing import Literal as L # noqa from pip._vendor.six.moves.urllib import parse as urlparse -from ._typing import MYPY_CHECK_RUNNING +from ._typing import TYPE_CHECKING from .markers import MARKER_EXPR, Marker from .specifiers import LegacySpecifier, Specifier, SpecifierSet -if MYPY_CHECK_RUNNING: # pragma: no cover +if TYPE_CHECKING: # pragma: no cover from typing import List diff --git a/src/pip/_vendor/packaging/specifiers.py b/src/pip/_vendor/packaging/specifiers.py index 94987486d4b..fe09bb1dbb2 100644 --- a/src/pip/_vendor/packaging/specifiers.py +++ b/src/pip/_vendor/packaging/specifiers.py @@ -9,10 +9,11 @@ import re from ._compat import string_types, with_metaclass -from ._typing import MYPY_CHECK_RUNNING +from ._typing import TYPE_CHECKING +from .utils import canonicalize_version from .version import Version, LegacyVersion, parse -if MYPY_CHECK_RUNNING: # pragma: no cover +if TYPE_CHECKING: # pragma: no cover from typing import ( List, Dict, @@ -132,9 +133,14 @@ def __str__(self): # type: () -> str return "{0}{1}".format(*self._spec) + @property + def _canonical_spec(self): + # type: () -> Tuple[str, Union[Version, str]] + return self._spec[0], canonicalize_version(self._spec[1]) + def __hash__(self): # type: () -> int - return hash(self._spec) + return hash(self._canonical_spec) def __eq__(self, other): # type: (object) -> bool @@ -146,7 +152,7 @@ def __eq__(self, other): elif not isinstance(other, self.__class__): return NotImplemented - return self._spec == other._spec + return self._canonical_spec == other._canonical_spec def __ne__(self, other): # type: (object) -> bool @@ -510,12 +516,20 @@ def _compare_not_equal(self, prospective, spec): @_require_version_compare def _compare_less_than_equal(self, prospective, spec): # type: (ParsedVersion, str) -> bool - return prospective <= Version(spec) + + # NB: Local version identifiers are NOT permitted in the version + # specifier, so local version labels can be universally removed from + # the prospective version. + return Version(prospective.public) <= Version(spec) @_require_version_compare def _compare_greater_than_equal(self, prospective, spec): # type: (ParsedVersion, str) -> bool - return prospective >= Version(spec) + + # NB: Local version identifiers are NOT permitted in the version + # specifier, so local version labels can be universally removed from + # the prospective version. + return Version(prospective.public) >= Version(spec) @_require_version_compare def _compare_less_than(self, prospective, spec_str): diff --git a/src/pip/_vendor/packaging/tags.py b/src/pip/_vendor/packaging/tags.py index 300faab8476..9064910b8ba 100644 --- a/src/pip/_vendor/packaging/tags.py +++ b/src/pip/_vendor/packaging/tags.py @@ -22,9 +22,9 @@ import sysconfig import warnings -from ._typing import MYPY_CHECK_RUNNING, cast +from ._typing import TYPE_CHECKING, cast -if MYPY_CHECK_RUNNING: # pragma: no cover +if TYPE_CHECKING: # pragma: no cover from typing import ( Dict, FrozenSet, @@ -58,6 +58,12 @@ class Tag(object): + """ + A representation of the tag triple for a wheel. + + Instances are considered immutable and thus are hashable. Equality checking + is also supported. + """ __slots__ = ["_interpreter", "_abi", "_platform"] @@ -108,6 +114,12 @@ def __repr__(self): def parse_tag(tag): # type: (str) -> FrozenSet[Tag] + """ + Parses the provided tag (e.g. `py3-none-any`) into a frozenset of Tag instances. + + Returning a set is required due to the possibility that the tag is a + compressed tag set. + """ tags = set() interpreters, abis, platforms = tag.split("-") for interpreter in interpreters.split("."): @@ -541,7 +553,7 @@ def __init__(self, file): def unpack(fmt): # type: (str) -> int try: - result, = struct.unpack( + (result,) = struct.unpack( fmt, file.read(struct.calcsize(fmt)) ) # type: (int, ) except struct.error: diff --git a/src/pip/_vendor/packaging/utils.py b/src/pip/_vendor/packaging/utils.py index 44f1bf98732..19579c1a0fa 100644 --- a/src/pip/_vendor/packaging/utils.py +++ b/src/pip/_vendor/packaging/utils.py @@ -5,19 +5,22 @@ import re -from ._typing import MYPY_CHECK_RUNNING +from ._typing import TYPE_CHECKING, cast from .version import InvalidVersion, Version -if MYPY_CHECK_RUNNING: # pragma: no cover - from typing import Union +if TYPE_CHECKING: # pragma: no cover + from typing import NewType, Union + + NormalizedName = NewType("NormalizedName", str) _canonicalize_regex = re.compile(r"[-_.]+") def canonicalize_name(name): - # type: (str) -> str + # type: (str) -> NormalizedName # This is taken from PEP 503. - return _canonicalize_regex.sub("-", name).lower() + value = _canonicalize_regex.sub("-", name).lower() + return cast("NormalizedName", value) def canonicalize_version(_version): diff --git a/src/pip/_vendor/packaging/version.py b/src/pip/_vendor/packaging/version.py index f39a2a12a1b..00371e86a87 100644 --- a/src/pip/_vendor/packaging/version.py +++ b/src/pip/_vendor/packaging/version.py @@ -8,9 +8,9 @@ import re from ._structures import Infinity, NegativeInfinity -from ._typing import MYPY_CHECK_RUNNING +from ._typing import TYPE_CHECKING -if MYPY_CHECK_RUNNING: # pragma: no cover +if TYPE_CHECKING: # pragma: no cover from typing import Callable, Iterator, List, Optional, SupportsInt, Tuple, Union from ._structures import InfinityType, NegativeInfinityType diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index e73fdb02cb5..7767c365994 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -7,7 +7,7 @@ distro==1.5.0 html5lib==1.1 ipaddress==1.0.23 # Only needed on 2.6 and 2.7 msgpack==1.0.0 -packaging==20.3 +packaging==20.4 pep517==0.8.2 progress==1.5 pyparsing==2.4.7 From 28aad200b39a0486a40bec84a809deba311aee81 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Wed, 22 Jul 2020 05:08:53 +0530 Subject: [PATCH 2297/3170] Upgrade requests to 2.24.0 --- news/requests.vendor | 1 + src/pip/_vendor/requests/__init__.py | 21 +++++++--- src/pip/_vendor/requests/__version__.py | 4 +- src/pip/_vendor/requests/exceptions.py | 9 ++--- src/pip/_vendor/requests/models.py | 12 +++--- src/pip/_vendor/requests/sessions.py | 10 +++-- src/pip/_vendor/vendor.txt | 2 +- .../vendoring/patches/requests.patch | 39 +++++++------------ 8 files changed, 48 insertions(+), 50 deletions(-) create mode 100644 news/requests.vendor diff --git a/news/requests.vendor b/news/requests.vendor new file mode 100644 index 00000000000..4e61b1974df --- /dev/null +++ b/news/requests.vendor @@ -0,0 +1 @@ +Upgrade requests to 2.24.0 diff --git a/src/pip/_vendor/requests/__init__.py b/src/pip/_vendor/requests/__init__.py index e47bcb20149..517458b5a25 100644 --- a/src/pip/_vendor/requests/__init__.py +++ b/src/pip/_vendor/requests/__init__.py @@ -90,18 +90,29 @@ def _check_cryptography(cryptography_version): "version!".format(urllib3.__version__, chardet.__version__), RequestsDependencyWarning) -# Attempt to enable urllib3's SNI support, if possible -from pip._internal.utils.compat import WINDOWS -if not WINDOWS: +# Attempt to enable urllib3's fallback for SNI support +# if the standard library doesn't support SNI or the +# 'ssl' library isn't available. +try: + # Note: This logic prevents upgrading cryptography on Windows, if imported + # as part of pip. + from pip._internal.utils.compat import WINDOWS + if not WINDOWS: + raise ImportError("pip internals: don't import cryptography on Windows") try: + import ssl + except ImportError: + ssl = None + + if not getattr(ssl, "HAS_SNI", False): from pip._vendor.urllib3.contrib import pyopenssl pyopenssl.inject_into_urllib3() # Check cryptography version from cryptography import __version__ as cryptography_version _check_cryptography(cryptography_version) - except ImportError: - pass +except ImportError: + pass # urllib3's DependencyWarnings should be silenced. from pip._vendor.urllib3.exceptions import DependencyWarning diff --git a/src/pip/_vendor/requests/__version__.py b/src/pip/_vendor/requests/__version__.py index b9e7df4881a..531e26ceb24 100644 --- a/src/pip/_vendor/requests/__version__.py +++ b/src/pip/_vendor/requests/__version__.py @@ -5,8 +5,8 @@ __title__ = 'requests' __description__ = 'Python HTTP for Humans.' __url__ = 'https://requests.readthedocs.io' -__version__ = '2.23.0' -__build__ = 0x022300 +__version__ = '2.24.0' +__build__ = 0x022400 __author__ = 'Kenneth Reitz' __author_email__ = 'me@kennethreitz.org' __license__ = 'Apache 2.0' diff --git a/src/pip/_vendor/requests/exceptions.py b/src/pip/_vendor/requests/exceptions.py index a91e1fd114e..9ef9e6e97b8 100644 --- a/src/pip/_vendor/requests/exceptions.py +++ b/src/pip/_vendor/requests/exceptions.py @@ -94,11 +94,11 @@ class ChunkedEncodingError(RequestException): class ContentDecodingError(RequestException, BaseHTTPError): - """Failed to decode response content""" + """Failed to decode response content.""" class StreamConsumedError(RequestException, TypeError): - """The content for this response was already consumed""" + """The content for this response was already consumed.""" class RetryError(RequestException): @@ -106,21 +106,18 @@ class RetryError(RequestException): class UnrewindableBodyError(RequestException): - """Requests encountered an error when trying to rewind a body""" + """Requests encountered an error when trying to rewind a body.""" # Warnings class RequestsWarning(Warning): """Base warning for Requests.""" - pass class FileModeWarning(RequestsWarning, DeprecationWarning): """A file was opened in text mode, but Requests determined its binary length.""" - pass class RequestsDependencyWarning(RequestsWarning): """An imported dependency doesn't match the expected version range.""" - pass diff --git a/src/pip/_vendor/requests/models.py b/src/pip/_vendor/requests/models.py index 8a3085d3783..015e715dad3 100644 --- a/src/pip/_vendor/requests/models.py +++ b/src/pip/_vendor/requests/models.py @@ -473,12 +473,12 @@ def prepare_body(self, data, files, json=None): not isinstance(data, (basestring, list, tuple, Mapping)) ]) - try: - length = super_len(data) - except (TypeError, AttributeError, UnsupportedOperation): - length = None - if is_stream: + try: + length = super_len(data) + except (TypeError, AttributeError, UnsupportedOperation): + length = None + body = data if getattr(body, 'tell', None) is not None: @@ -916,7 +916,7 @@ def links(self): return l def raise_for_status(self): - """Raises stored :class:`HTTPError`, if one occurred.""" + """Raises :class:`HTTPError`, if one occurred.""" http_error_msg = '' if isinstance(self.reason, bytes): diff --git a/src/pip/_vendor/requests/sessions.py b/src/pip/_vendor/requests/sessions.py index 2845880bf41..e8e2d609a78 100644 --- a/src/pip/_vendor/requests/sessions.py +++ b/src/pip/_vendor/requests/sessions.py @@ -658,11 +658,13 @@ def send(self, request, **kwargs): extract_cookies_to_jar(self.cookies, request, r.raw) - # Redirect resolving generator. - gen = self.resolve_redirects(r, request, **kwargs) - # Resolve redirects if allowed. - history = [resp for resp in gen] if allow_redirects else [] + if allow_redirects: + # Redirect resolving generator. + gen = self.resolve_redirects(r, request, **kwargs) + history = [resp for resp in gen] + else: + history = [] # Shuffle things around if there's history. if history: diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 7767c365994..53d5c02867d 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -11,7 +11,7 @@ packaging==20.4 pep517==0.8.2 progress==1.5 pyparsing==2.4.7 -requests==2.23.0 +requests==2.24.0 certifi==2020.04.05.1 chardet==3.0.4 idna==2.9 diff --git a/tools/automation/vendoring/patches/requests.patch b/tools/automation/vendoring/patches/requests.patch index 75bf25ab284..08795ad3a3b 100644 --- a/tools/automation/vendoring/patches/requests.patch +++ b/tools/automation/vendoring/patches/requests.patch @@ -21,35 +21,22 @@ index 6336a07d..9582fa73 100644 # Kinda cool, though, right? diff --git a/src/pip/_vendor/requests/__init__.py b/src/pip/_vendor/requests/__init__.py -index 9c3b769..36a4ef40 100644 +index dc83261a8..517458b5a 100644 --- a/src/pip/_vendor/requests/__init__.py +++ b/src/pip/_vendor/requests/__init__.py -@@ -80,13 +80,15 @@ except (AssertionError, ValueError): - RequestsDependencyWarning) +@@ -94,6 +94,11 @@ except (AssertionError, ValueError): + # if the standard library doesn't support SNI or the + # 'ssl' library isn't available. + try: ++ # Note: This logic prevents upgrading cryptography on Windows, if imported ++ # as part of pip. ++ from pip._internal.utils.compat import WINDOWS ++ if not WINDOWS: ++ raise ImportError("pip internals: don't import cryptography on Windows") + try: + import ssl + except ImportError: - # Attempt to enable urllib3's SNI support, if possible --try: -- from pip._vendor.urllib3.contrib import pyopenssl -- pyopenssl.inject_into_urllib3() -- -- # Check cryptography version -- from cryptography import __version__ as cryptography_version -- _check_cryptography(cryptography_version) --except ImportError: -- pass -+from pip._internal.utils.compat import WINDOWS -+if not WINDOWS: -+ try: -+ from pip._vendor.urllib3.contrib import pyopenssl -+ pyopenssl.inject_into_urllib3() -+ -+ # Check cryptography version -+ from cryptography import __version__ as cryptography_version -+ _check_cryptography(cryptography_version) -+ except ImportError: -+ pass - - # urllib3's DependencyWarnings should be silenced. diff --git a/src/pip/_vendor/requests/compat.py b/src/pip/_vendor/requests/compat.py index eb6530d..353ec29 100644 --- a/src/pip/_vendor/requests/compat.py From 2ac80f43360d17537f3acc5cda7e4fdcd817a321 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Wed, 22 Jul 2020 06:58:17 +0530 Subject: [PATCH 2298/3170] Upgrade certifi to 2020.6.20 --- news/certifi.vendor | 1 + src/pip/_vendor/certifi/__init__.py | 2 +- src/pip/_vendor/certifi/cacert.pem | 273 ++++++++---------- src/pip/_vendor/certifi/core.py | 40 ++- src/pip/_vendor/vendor.txt | 2 +- .../vendoring/patches/certifi.patch | 13 + 6 files changed, 177 insertions(+), 154 deletions(-) create mode 100644 news/certifi.vendor create mode 100644 tools/automation/vendoring/patches/certifi.patch diff --git a/news/certifi.vendor b/news/certifi.vendor new file mode 100644 index 00000000000..ddd125054b1 --- /dev/null +++ b/news/certifi.vendor @@ -0,0 +1 @@ +Upgrade certifi to 2020.6.20 diff --git a/src/pip/_vendor/certifi/__init__.py b/src/pip/_vendor/certifi/__init__.py index 1e2dfac7dbe..5d52a62e7f4 100644 --- a/src/pip/_vendor/certifi/__init__.py +++ b/src/pip/_vendor/certifi/__init__.py @@ -1,3 +1,3 @@ from .core import contents, where -__version__ = "2020.04.05.1" +__version__ = "2020.06.20" diff --git a/src/pip/_vendor/certifi/cacert.pem b/src/pip/_vendor/certifi/cacert.pem index ece147c9dc8..0fd855f4646 100644 --- a/src/pip/_vendor/certifi/cacert.pem +++ b/src/pip/_vendor/certifi/cacert.pem @@ -58,38 +58,6 @@ AfvDbbnvRG15RjF+Cv6pgsH/76tuIMRQyV+dTZsXjAzlAcmgQWpzU/qlULRuJQ/7 TBj0/VLZjmmx6BEP3ojY+x1J96relc8geMJgEtslQIxq/H5COEBkEveegeGTLg== -----END CERTIFICATE----- -# Issuer: CN=VeriSign Class 3 Public Primary Certification Authority - G3 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 1999 VeriSign, Inc. - For authorized use only -# Subject: CN=VeriSign Class 3 Public Primary Certification Authority - G3 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 1999 VeriSign, Inc. - For authorized use only -# Label: "Verisign Class 3 Public Primary Certification Authority - G3" -# Serial: 206684696279472310254277870180966723415 -# MD5 Fingerprint: cd:68:b6:a7:c7:c4:ce:75:e0:1d:4f:57:44:61:92:09 -# SHA1 Fingerprint: 13:2d:0d:45:53:4b:69:97:cd:b2:d5:c3:39:e2:55:76:60:9b:5c:c6 -# SHA256 Fingerprint: eb:04:cf:5e:b1:f3:9a:fa:76:2f:2b:b1:20:f2:96:cb:a5:20:c1:b9:7d:b1:58:95:65:b8:1c:b9:a1:7b:72:44 ------BEGIN CERTIFICATE----- -MIIEGjCCAwICEQCbfgZJoz5iudXukEhxKe9XMA0GCSqGSIb3DQEBBQUAMIHKMQsw -CQYDVQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZl -cmlTaWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWdu -LCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlT -aWduIENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3Jp -dHkgLSBHMzAeFw05OTEwMDEwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMIHKMQswCQYD -VQQGEwJVUzEXMBUGA1UEChMOVmVyaVNpZ24sIEluYy4xHzAdBgNVBAsTFlZlcmlT -aWduIFRydXN0IE5ldHdvcmsxOjA4BgNVBAsTMShjKSAxOTk5IFZlcmlTaWduLCBJ -bmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxRTBDBgNVBAMTPFZlcmlTaWdu -IENsYXNzIDMgUHVibGljIFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg -LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMu6nFL8eB8aHm8b -N3O9+MlrlBIwT/A2R/XQkQr1F8ilYcEWQE37imGQ5XYgwREGfassbqb1EUGO+i2t -KmFZpGcmTNDovFJbcCAEWNF6yaRpvIMXZK0Fi7zQWM6NjPXr8EJJC52XJ2cybuGu -kxUccLwgTS8Y3pKI6GyFVxEa6X7jJhFUokWWVYPKMIno3Nij7SqAP395ZVc+FSBm -CC+Vk7+qRy+oRpfwEuL+wgorUeZ25rdGt+INpsyow0xZVYnm6FNcHOqd8GIWC6fJ -Xwzw3sJ2zq/3avL6QaaiMxTJ5Xpj055iN9WFZZ4O5lMkdBteHRJTW8cs54NJOxWu -imi5V5cCAwEAATANBgkqhkiG9w0BAQUFAAOCAQEAERSWwauSCPc/L8my/uRan2Te -2yFPhpk0djZX3dAVL8WtfxUfN2JzPtTnX84XA9s1+ivbrmAJXx5fj267Cz3qWhMe -DGBvtcC1IyIuBwvLqXTLR7sdwdela8wv0kL9Sd2nic9TutoAWii/gt/4uhMdUIaC -/Y4wjylGsB49Ndo4YhYYSq3mtlFs3q9i6wHQHiT+eo8SGhJouPtmmRQURVyu565p -F4ErWjfJXir0xuKhXFSbplQAz/DxwceYMBo7Nhbbo27q/a2ywtrvAkcTisDxszGt -TxzhT5yvDwyd93gN2PQ1VoDat20Xj50egWTh/sVFuq1ruQp6Tk9LhO5L8X3dEQ== ------END CERTIFICATE----- - # Issuer: CN=Entrust.net Certification Authority (2048) O=Entrust.net OU=www.entrust.net/CPS_2048 incorp. by ref. (limits liab.)/(c) 1999 Entrust.net Limited # Subject: CN=Entrust.net Certification Authority (2048) O=Entrust.net OU=www.entrust.net/CPS_2048 incorp. by ref. (limits liab.)/(c) 1999 Entrust.net Limited # Label: "Entrust.net Premium 2048 Secure Server CA" @@ -152,39 +120,6 @@ ksLi4xaNmjICq44Y3ekQEe5+NauQrz4wlHrQMz2nZQ/1/I6eYs9HRCwBXbsdtTLS R9I4LtD+gdwyah617jzV/OeBHRnDJELqYzmp -----END CERTIFICATE----- -# Issuer: CN=AddTrust External CA Root O=AddTrust AB OU=AddTrust External TTP Network -# Subject: CN=AddTrust External CA Root O=AddTrust AB OU=AddTrust External TTP Network -# Label: "AddTrust External Root" -# Serial: 1 -# MD5 Fingerprint: 1d:35:54:04:85:78:b0:3f:42:42:4d:bf:20:73:0a:3f -# SHA1 Fingerprint: 02:fa:f3:e2:91:43:54:68:60:78:57:69:4d:f5:e4:5b:68:85:18:68 -# SHA256 Fingerprint: 68:7f:a4:51:38:22:78:ff:f0:c8:b1:1f:8d:43:d5:76:67:1c:6e:b2:bc:ea:b4:13:fb:83:d9:65:d0:6d:2f:f2 ------BEGIN CERTIFICATE----- -MIIENjCCAx6gAwIBAgIBATANBgkqhkiG9w0BAQUFADBvMQswCQYDVQQGEwJTRTEU -MBIGA1UEChMLQWRkVHJ1c3QgQUIxJjAkBgNVBAsTHUFkZFRydXN0IEV4dGVybmFs -IFRUUCBOZXR3b3JrMSIwIAYDVQQDExlBZGRUcnVzdCBFeHRlcm5hbCBDQSBSb290 -MB4XDTAwMDUzMDEwNDgzOFoXDTIwMDUzMDEwNDgzOFowbzELMAkGA1UEBhMCU0Ux -FDASBgNVBAoTC0FkZFRydXN0IEFCMSYwJAYDVQQLEx1BZGRUcnVzdCBFeHRlcm5h -bCBUVFAgTmV0d29yazEiMCAGA1UEAxMZQWRkVHJ1c3QgRXh0ZXJuYWwgQ0EgUm9v -dDCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBALf3GjPm8gAELTngTlvt -H7xsD821+iO2zt6bETOXpClMfZOfvUq8k+0DGuOPz+VtUFrWlymUWoCwSXrbLpX9 -uMq/NzgtHj6RQa1wVsfwTz/oMp50ysiQVOnGXw94nZpAPA6sYapeFI+eh6FqUNzX -mk6vBbOmcZSccbNQYArHE504B4YCqOmoaSYYkKtMsE8jqzpPhNjfzp/haW+710LX -a0Tkx63ubUFfclpxCDezeWWkWaCUN/cALw3CknLa0Dhy2xSoRcRdKn23tNbE7qzN -E0S3ySvdQwAl+mG5aWpYIxG3pzOPVnVZ9c0p10a3CitlttNCbxWyuHv77+ldU9U0 -WicCAwEAAaOB3DCB2TAdBgNVHQ4EFgQUrb2YejS0Jvf6xCZU7wO94CTLVBowCwYD -VR0PBAQDAgEGMA8GA1UdEwEB/wQFMAMBAf8wgZkGA1UdIwSBkTCBjoAUrb2YejS0 -Jvf6xCZU7wO94CTLVBqhc6RxMG8xCzAJBgNVBAYTAlNFMRQwEgYDVQQKEwtBZGRU -cnVzdCBBQjEmMCQGA1UECxMdQWRkVHJ1c3QgRXh0ZXJuYWwgVFRQIE5ldHdvcmsx -IjAgBgNVBAMTGUFkZFRydXN0IEV4dGVybmFsIENBIFJvb3SCAQEwDQYJKoZIhvcN -AQEFBQADggEBALCb4IUlwtYj4g+WBpKdQZic2YR5gdkeWxQHIzZlj7DYd7usQWxH -YINRsPkyPef89iYTx4AWpb9a/IfPeHmJIZriTAcKhjW88t5RxNKWt9x+Tu5w/Rw5 -6wwCURQtjr0W4MHfRnXnJK3s9EK0hZNwEGe6nQY1ShjTK3rMUUKhemPR5ruhxSvC -Nr4TDea9Y355e6cJDUCrat2PisP29owaQgVR1EX1n6diIWgVIEM8med8vSTYqZEX -c4g/VhsxOBi0cQ+azcgOno4uG+GMmIPLHzHxREzGBHNJdmAPx/i9F4BrLunMTA5a -mnkPIAou1Z5jJh5VkpTYghdae9C8x49OhgQ= ------END CERTIFICATE----- - # Issuer: CN=Entrust Root Certification Authority O=Entrust, Inc. OU=www.entrust.net/CPS is incorporated by reference/(c) 2006 Entrust, Inc. # Subject: CN=Entrust Root Certification Authority O=Entrust, Inc. OU=www.entrust.net/CPS is incorporated by reference/(c) 2006 Entrust, Inc. # Label: "Entrust Root Certification Authority" @@ -1499,47 +1434,6 @@ uLjbvrW5KfnaNwUASZQDhETnv0Mxz3WLJdH0pmT1kvarBes96aULNmLazAZfNou2 XjG4Kvte9nHfRCaexOYNkbQudZWAUWpLMKawYqGT8ZvYzsRjdT9ZR7E= -----END CERTIFICATE----- -# Issuer: CN=Staat der Nederlanden Root CA - G2 O=Staat der Nederlanden -# Subject: CN=Staat der Nederlanden Root CA - G2 O=Staat der Nederlanden -# Label: "Staat der Nederlanden Root CA - G2" -# Serial: 10000012 -# MD5 Fingerprint: 7c:a5:0f:f8:5b:9a:7d:6d:30:ae:54:5a:e3:42:a2:8a -# SHA1 Fingerprint: 59:af:82:79:91:86:c7:b4:75:07:cb:cf:03:57:46:eb:04:dd:b7:16 -# SHA256 Fingerprint: 66:8c:83:94:7d:a6:3b:72:4b:ec:e1:74:3c:31:a0:e6:ae:d0:db:8e:c5:b3:1b:e3:77:bb:78:4f:91:b6:71:6f ------BEGIN CERTIFICATE----- -MIIFyjCCA7KgAwIBAgIEAJiWjDANBgkqhkiG9w0BAQsFADBaMQswCQYDVQQGEwJO -TDEeMBwGA1UECgwVU3RhYXQgZGVyIE5lZGVybGFuZGVuMSswKQYDVQQDDCJTdGFh -dCBkZXIgTmVkZXJsYW5kZW4gUm9vdCBDQSAtIEcyMB4XDTA4MDMyNjExMTgxN1oX -DTIwMDMyNTExMDMxMFowWjELMAkGA1UEBhMCTkwxHjAcBgNVBAoMFVN0YWF0IGRl -ciBOZWRlcmxhbmRlbjErMCkGA1UEAwwiU3RhYXQgZGVyIE5lZGVybGFuZGVuIFJv -b3QgQ0EgLSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMVZ5291 -qj5LnLW4rJ4L5PnZyqtdj7U5EILXr1HgO+EASGrP2uEGQxGZqhQlEq0i6ABtQ8Sp -uOUfiUtnvWFI7/3S4GCI5bkYYCjDdyutsDeqN95kWSpGV+RLufg3fNU254DBtvPU -Z5uW6M7XxgpT0GtJlvOjCwV3SPcl5XCsMBQgJeN/dVrlSPhOewMHBPqCYYdu8DvE -pMfQ9XQ+pV0aCPKbJdL2rAQmPlU6Yiile7Iwr/g3wtG61jj99O9JMDeZJiFIhQGp -5Rbn3JBV3w/oOM2ZNyFPXfUib2rFEhZgF1XyZWampzCROME4HYYEhLoaJXhena/M -UGDWE4dS7WMfbWV9whUYdMrhfmQpjHLYFhN9C0lK8SgbIHRrxT3dsKpICT0ugpTN -GmXZK4iambwYfp/ufWZ8Pr2UuIHOzZgweMFvZ9C+X+Bo7d7iscksWXiSqt8rYGPy -5V6548r6f1CGPqI0GAwJaCgRHOThuVw+R7oyPxjMW4T182t0xHJ04eOLoEq9jWYv -6q012iDTiIJh8BIitrzQ1aTsr1SIJSQ8p22xcik/Plemf1WvbibG/ufMQFxRRIEK -eN5KzlW/HdXZt1bv8Hb/C3m1r737qWmRRpdogBQ2HbN/uymYNqUg+oJgYjOk7Na6 -B6duxc8UpufWkjTYgfX8HV2qXB72o007uPc5AgMBAAGjgZcwgZQwDwYDVR0TAQH/ -BAUwAwEB/zBSBgNVHSAESzBJMEcGBFUdIAAwPzA9BggrBgEFBQcCARYxaHR0cDov -L3d3dy5wa2lvdmVyaGVpZC5ubC9wb2xpY2llcy9yb290LXBvbGljeS1HMjAOBgNV -HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFJFoMocVHYnitfGsNig0jQt8YojrMA0GCSqG -SIb3DQEBCwUAA4ICAQCoQUpnKpKBglBu4dfYszk78wIVCVBR7y29JHuIhjv5tLyS -CZa59sCrI2AGeYwRTlHSeYAz+51IvuxBQ4EffkdAHOV6CMqqi3WtFMTC6GY8ggen -5ieCWxjmD27ZUD6KQhgpxrRW/FYQoAUXvQwjf/ST7ZwaUb7dRUG/kSS0H4zpX897 -IZmflZ85OkYcbPnNe5yQzSipx6lVu6xiNGI1E0sUOlWDuYaNkqbG9AclVMwWVxJK -gnjIFNkXgiYtXSAfea7+1HAWFpWD2DU5/1JddRwWxRNVz0fMdWVSSt7wsKfkCpYL -+63C4iWEst3kvX5ZbJvw8NjnyvLplzh+ib7M+zkXYT9y2zqR2GUBGR2tUKRXCnxL -vJxxcypFURmFzI79R6d0lR2o0a9OF7FpJsKqeFdbxU2n5Z4FF5TKsl+gSRiNNOkm -bEgeqmiSBeGCc1qb3AdbCG19ndeNIdn8FCCqwkXfP+cAslHkwvgFuXkajDTznlvk -N1trSt8sV4pAWja63XVECDdCcAz+3F4hoKOKwJCcaNpQ5kUQR3i2TtJlycM33+FC -Y7BXN0Ute4qcvwXqZVUz9zkQxSgqIXobisQk+T8VyJoVIPVVYpbtbZNQvOSqeK3Z -ywplh6ZmwcSBo3c6WB4L7oOLnR7SUqTMHW+wmG2UMbX4cQrcufx9MmDm66+KAQ== ------END CERTIFICATE----- - # Issuer: CN=Hongkong Post Root CA 1 O=Hongkong Post # Subject: CN=Hongkong Post Root CA 1 O=Hongkong Post # Label: "Hongkong Post Root CA 1" @@ -3788,47 +3682,6 @@ CkcO8DdZEv8tmZQoTipPNU0zWgIxAOp1AE47xDqUEpHJWEadIRNyp4iciuRMStuW 1KyLa2tJElMzrdfkviT8tQp21KW8EA== -----END CERTIFICATE----- -# Issuer: CN=LuxTrust Global Root 2 O=LuxTrust S.A. -# Subject: CN=LuxTrust Global Root 2 O=LuxTrust S.A. -# Label: "LuxTrust Global Root 2" -# Serial: 59914338225734147123941058376788110305822489521 -# MD5 Fingerprint: b2:e1:09:00:61:af:f7:f1:91:6f:c4:ad:8d:5e:3b:7c -# SHA1 Fingerprint: 1e:0e:56:19:0a:d1:8b:25:98:b2:04:44:ff:66:8a:04:17:99:5f:3f -# SHA256 Fingerprint: 54:45:5f:71:29:c2:0b:14:47:c4:18:f9:97:16:8f:24:c5:8f:c5:02:3b:f5:da:5b:e2:eb:6e:1d:d8:90:2e:d5 ------BEGIN CERTIFICATE----- -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 ------END CERTIFICATE----- - # Issuer: CN=TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 O=Turkiye Bilimsel ve Teknolojik Arastirma Kurumu - TUBITAK OU=Kamu Sertifikasyon Merkezi - Kamu SM # Subject: CN=TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1 O=Turkiye Bilimsel ve Teknolojik Arastirma Kurumu - TUBITAK OU=Kamu Sertifikasyon Merkezi - Kamu SM # Label: "TUBITAK Kamu SM SSL Kok Sertifikasi - Surum 1" @@ -4639,3 +4492,129 @@ IQ6SwJAfzyBfyjs4x7dtOvPmRLgOMWuIjnDrnBdSqEGULoe256YSxXXfW8AKbnuk 5F6G+TaU33fD6Q3AOfF5u0aOq0NZJ7cguyPpVkAh7DE9ZapD8j3fcEThuk0mEDuY n/PIjhs4ViFqUZPTkcpG2om3PVODLAgfi49T3f+sHw== -----END CERTIFICATE----- + +# Issuer: CN=Microsoft ECC Root Certificate Authority 2017 O=Microsoft Corporation +# Subject: CN=Microsoft ECC Root Certificate Authority 2017 O=Microsoft Corporation +# Label: "Microsoft ECC Root Certificate Authority 2017" +# Serial: 136839042543790627607696632466672567020 +# MD5 Fingerprint: dd:a1:03:e6:4a:93:10:d1:bf:f0:19:42:cb:fe:ed:67 +# SHA1 Fingerprint: 99:9a:64:c3:7f:f4:7d:9f:ab:95:f1:47:69:89:14:60:ee:c4:c3:c5 +# SHA256 Fingerprint: 35:8d:f3:9d:76:4a:f9:e1:b7:66:e9:c9:72:df:35:2e:e1:5c:fa:c2:27:af:6a:d1:d7:0e:8e:4a:6e:dc:ba:02 +-----BEGIN CERTIFICATE----- +MIICWTCCAd+gAwIBAgIQZvI9r4fei7FK6gxXMQHC7DAKBggqhkjOPQQDAzBlMQsw +CQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYD +VQQDEy1NaWNyb3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIw +MTcwHhcNMTkxMjE4MjMwNjQ1WhcNNDIwNzE4MjMxNjA0WjBlMQswCQYDVQQGEwJV +UzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1NaWNy +b3NvZnQgRUNDIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwdjAQBgcq +hkjOPQIBBgUrgQQAIgNiAATUvD0CQnVBEyPNgASGAlEvaqiBYgtlzPbKnR5vSmZR +ogPZnZH6thaxjG7efM3beaYvzrvOcS/lpaso7GMEZpn4+vKTEAXhgShC48Zo9OYb +hGBKia/teQ87zvH2RPUBeMCjVDBSMA4GA1UdDwEB/wQEAwIBhjAPBgNVHRMBAf8E +BTADAQH/MB0GA1UdDgQWBBTIy5lycFIM+Oa+sgRXKSrPQhDtNTAQBgkrBgEEAYI3 +FQEEAwIBADAKBggqhkjOPQQDAwNoADBlAjBY8k3qDPlfXu5gKcs68tvWMoQZP3zV +L8KxzJOuULsJMsbG7X7JNpQS5GiFBqIb0C8CMQCZ6Ra0DvpWSNSkMBaReNtUjGUB +iudQZsIxtzm6uBoiB078a1QWIP8rtedMDE2mT3M= +-----END CERTIFICATE----- + +# Issuer: CN=Microsoft RSA Root Certificate Authority 2017 O=Microsoft Corporation +# Subject: CN=Microsoft RSA Root Certificate Authority 2017 O=Microsoft Corporation +# Label: "Microsoft RSA Root Certificate Authority 2017" +# Serial: 40975477897264996090493496164228220339 +# MD5 Fingerprint: 10:ff:00:ff:cf:c9:f8:c7:7a:c0:ee:35:8e:c9:0f:47 +# SHA1 Fingerprint: 73:a5:e6:4a:3b:ff:83:16:ff:0e:dc:cc:61:8a:90:6e:4e:ae:4d:74 +# SHA256 Fingerprint: c7:41:f7:0f:4b:2a:8d:88:bf:2e:71:c1:41:22:ef:53:ef:10:eb:a0:cf:a5:e6:4c:fa:20:f4:18:85:30:73:e0 +-----BEGIN CERTIFICATE----- +MIIFqDCCA5CgAwIBAgIQHtOXCV/YtLNHcB6qvn9FszANBgkqhkiG9w0BAQwFADBl +MQswCQYDVQQGEwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYw +NAYDVQQDEy1NaWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5 +IDIwMTcwHhcNMTkxMjE4MjI1MTIyWhcNNDIwNzE4MjMwMDIzWjBlMQswCQYDVQQG +EwJVUzEeMBwGA1UEChMVTWljcm9zb2Z0IENvcnBvcmF0aW9uMTYwNAYDVQQDEy1N +aWNyb3NvZnQgUlNBIFJvb3QgQ2VydGlmaWNhdGUgQXV0aG9yaXR5IDIwMTcwggIi +MA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQDKW76UM4wplZEWCpW9R2LBifOZ +Nt9GkMml7Xhqb0eRaPgnZ1AzHaGm++DlQ6OEAlcBXZxIQIJTELy/xztokLaCLeX0 +ZdDMbRnMlfl7rEqUrQ7eS0MdhweSE5CAg2Q1OQT85elss7YfUJQ4ZVBcF0a5toW1 +HLUX6NZFndiyJrDKxHBKrmCk3bPZ7Pw71VdyvD/IybLeS2v4I2wDwAW9lcfNcztm +gGTjGqwu+UcF8ga2m3P1eDNbx6H7JyqhtJqRjJHTOoI+dkC0zVJhUXAoP8XFWvLJ +jEm7FFtNyP9nTUwSlq31/niol4fX/V4ggNyhSyL71Imtus5Hl0dVe49FyGcohJUc +aDDv70ngNXtk55iwlNpNhTs+VcQor1fznhPbRiefHqJeRIOkpcrVE7NLP8TjwuaG +YaRSMLl6IE9vDzhTyzMMEyuP1pq9KsgtsRx9S1HKR9FIJ3Jdh+vVReZIZZ2vUpC6 +W6IYZVcSn2i51BVrlMRpIpj0M+Dt+VGOQVDJNE92kKz8OMHY4Xu54+OU4UZpyw4K +UGsTuqwPN1q3ErWQgR5WrlcihtnJ0tHXUeOrO8ZV/R4O03QK0dqq6mm4lyiPSMQH ++FJDOvTKVTUssKZqwJz58oHhEmrARdlns87/I6KJClTUFLkqqNfs+avNJVgyeY+Q +W5g5xAgGwax/Dj0ApQIDAQABo1QwUjAOBgNVHQ8BAf8EBAMCAYYwDwYDVR0TAQH/ +BAUwAwEB/zAdBgNVHQ4EFgQUCctZf4aycI8awznjwNnpv7tNsiMwEAYJKwYBBAGC +NxUBBAMCAQAwDQYJKoZIhvcNAQEMBQADggIBAKyvPl3CEZaJjqPnktaXFbgToqZC +LgLNFgVZJ8og6Lq46BrsTaiXVq5lQ7GPAJtSzVXNUzltYkyLDVt8LkS/gxCP81OC +gMNPOsduET/m4xaRhPtthH80dK2Jp86519efhGSSvpWhrQlTM93uCupKUY5vVau6 +tZRGrox/2KJQJWVggEbbMwSubLWYdFQl3JPk+ONVFT24bcMKpBLBaYVu32TxU5nh +SnUgnZUP5NbcA/FZGOhHibJXWpS2qdgXKxdJ5XbLwVaZOjex/2kskZGT4d9Mozd2 +TaGf+G0eHdP67Pv0RR0Tbc/3WeUiJ3IrhvNXuzDtJE3cfVa7o7P4NHmJweDyAmH3 +pvwPuxwXC65B2Xy9J6P9LjrRk5Sxcx0ki69bIImtt2dmefU6xqaWM/5TkshGsRGR +xpl/j8nWZjEgQRCHLQzWwa80mMpkg/sTV9HB8Dx6jKXB/ZUhoHHBk2dxEuqPiApp +GWSZI1b7rCoucL5mxAyE7+WL85MB+GqQk2dLsmijtWKP6T+MejteD+eMuMZ87zf9 +dOLITzNy4ZQ5bb0Sr74MTnB8G2+NszKTc0QWbej09+CVgI+WXTik9KveCjCHk9hN +AHFiRSdLOkKEW39lt2c0Ui2cFmuqqNh7o0JMcccMyj6D5KbvtwEwXlGjefVwaaZB +RA+GsCyRxj3qrg+E +-----END CERTIFICATE----- + +# Issuer: CN=e-Szigno Root CA 2017 O=Microsec Ltd. +# Subject: CN=e-Szigno Root CA 2017 O=Microsec Ltd. +# Label: "e-Szigno Root CA 2017" +# Serial: 411379200276854331539784714 +# MD5 Fingerprint: de:1f:f6:9e:84:ae:a7:b4:21:ce:1e:58:7d:d1:84:98 +# SHA1 Fingerprint: 89:d4:83:03:4f:9e:9a:48:80:5f:72:37:d4:a9:a6:ef:cb:7c:1f:d1 +# SHA256 Fingerprint: be:b0:0b:30:83:9b:9b:c3:2c:32:e4:44:79:05:95:06:41:f2:64:21:b1:5e:d0:89:19:8b:51:8a:e2:ea:1b:99 +-----BEGIN CERTIFICATE----- +MIICQDCCAeWgAwIBAgIMAVRI7yH9l1kN9QQKMAoGCCqGSM49BAMCMHExCzAJBgNV +BAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMgTHRk +LjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25vIFJv +b3QgQ0EgMjAxNzAeFw0xNzA4MjIxMjA3MDZaFw00MjA4MjIxMjA3MDZaMHExCzAJ +BgNVBAYTAkhVMREwDwYDVQQHDAhCdWRhcGVzdDEWMBQGA1UECgwNTWljcm9zZWMg +THRkLjEXMBUGA1UEYQwOVkFUSFUtMjM1ODQ0OTcxHjAcBgNVBAMMFWUtU3ppZ25v +IFJvb3QgQ0EgMjAxNzBZMBMGByqGSM49AgEGCCqGSM49AwEHA0IABJbcPYrYsHtv +xie+RJCxs1YVe45DJH0ahFnuY2iyxl6H0BVIHqiQrb1TotreOpCmYF9oMrWGQd+H +Wyx7xf58etqjYzBhMA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0G +A1UdDgQWBBSHERUI0arBeAyxr87GyZDvvzAEwDAfBgNVHSMEGDAWgBSHERUI0arB +eAyxr87GyZDvvzAEwDAKBggqhkjOPQQDAgNJADBGAiEAtVfd14pVCzbhhkT61Nlo +jbjcI4qKDdQvfepz7L9NbKgCIQDLpbQS+ue16M9+k/zzNY9vTlp8tLxOsvxyqltZ ++efcMQ== +-----END CERTIFICATE----- + +# Issuer: O=CERTSIGN SA OU=certSIGN ROOT CA G2 +# Subject: O=CERTSIGN SA OU=certSIGN ROOT CA G2 +# Label: "certSIGN Root CA G2" +# Serial: 313609486401300475190 +# MD5 Fingerprint: 8c:f1:75:8a:c6:19:cf:94:b7:f7:65:20:87:c3:97:c7 +# SHA1 Fingerprint: 26:f9:93:b4:ed:3d:28:27:b0:b9:4b:a7:e9:15:1d:a3:8d:92:e5:32 +# SHA256 Fingerprint: 65:7c:fe:2f:a7:3f:aa:38:46:25:71:f3:32:a2:36:3a:46:fc:e7:02:09:51:71:07:02:cd:fb:b6:ee:da:33:05 +-----BEGIN CERTIFICATE----- +MIIFRzCCAy+gAwIBAgIJEQA0tk7GNi02MA0GCSqGSIb3DQEBCwUAMEExCzAJBgNV +BAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJR04g +Uk9PVCBDQSBHMjAeFw0xNzAyMDYwOTI3MzVaFw00MjAyMDYwOTI3MzVaMEExCzAJ +BgNVBAYTAlJPMRQwEgYDVQQKEwtDRVJUU0lHTiBTQTEcMBoGA1UECxMTY2VydFNJ +R04gUk9PVCBDQSBHMjCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAMDF +dRmRfUR0dIf+DjuW3NgBFszuY5HnC2/OOwppGnzC46+CjobXXo9X69MhWf05N0Iw +vlDqtg+piNguLWkh59E3GE59kdUWX2tbAMI5Qw02hVK5U2UPHULlj88F0+7cDBrZ +uIt4ImfkabBoxTzkbFpG583H+u/E7Eu9aqSs/cwoUe+StCmrqzWaTOTECMYmzPhp +n+Sc8CnTXPnGFiWeI8MgwT0PPzhAsP6CRDiqWhqKa2NYOLQV07YRaXseVO6MGiKs +cpc/I1mbySKEwQdPzH/iV8oScLumZfNpdWO9lfsbl83kqK/20U6o2YpxJM02PbyW +xPFsqa7lzw1uKA2wDrXKUXt4FMMgL3/7FFXhEZn91QqhngLjYl/rNUssuHLoPj1P +rCy7Lobio3aP5ZMqz6WryFyNSwb/EkaseMsUBzXgqd+L6a8VTxaJW732jcZZroiF +DsGJ6x9nxUWO/203Nit4ZoORUSs9/1F3dmKh7Gc+PoGD4FapUB8fepmrY7+EF3fx +DTvf95xhszWYijqy7DwaNz9+j5LP2RIUZNoQAhVB/0/E6xyjyfqZ90bp4RjZsbgy +LcsUDFDYg2WD7rlcz8sFWkz6GZdr1l0T08JcVLwyc6B49fFtHsufpaafItzRUZ6C +eWRgKRM+o/1Pcmqr4tTluCRVLERLiohEnMqE0yo7AgMBAAGjQjBAMA8GA1UdEwEB +/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBSCIS1mxteg4BXrzkwJ +d8RgnlRuAzANBgkqhkiG9w0BAQsFAAOCAgEAYN4auOfyYILVAzOBywaK8SJJ6ejq +kX/GM15oGQOGO0MBzwdw5AgeZYWR5hEit/UCI46uuR59H35s5r0l1ZUa8gWmr4UC +b6741jH/JclKyMeKqdmfS0mbEVeZkkMR3rYzpMzXjWR91M08KCy0mpbqTfXERMQl +qiCA2ClV9+BB/AYm/7k29UMUA2Z44RGx2iBfRgB4ACGlHgAoYXhvqAEBj500mv/0 +OJD7uNGzcgbJceaBxXntC6Z58hMLnPddDnskk7RI24Zf3lCGeOdA5jGokHZwYa+c +NywRtYK3qq4kNFtyDGkNzVmf9nGvnAvRCjj5BiKDUyUM/FHE5r7iOZULJK2v0ZXk +ltd0ZGtxTgI8qoXzIKNDOXZbbFD+mpwUHmUUihW9o4JFWklWatKcsWMy5WHgUyIO +pwpJ6st+H6jiYoD2EEVSmAYY3qXNL3+q1Ok+CHLsIwMCPKaq2LxndD0UF/tUSxfj +03k9bWtJySgOLnRQvwzZRjoQhsmnP+mg7H/rpXdYaXHmgwo38oZJar55CJD2AhZk +PuXaTH4MNMn5X7azKFGnpyuqSfqNZSlO42sTp5SjLVFteAxEy9/eCG/Oo2Sr05WE +1LlSVHJ7liXMvGnjSG4N0MedJ5qq+BOS3R7fY581qRY27Iy4g/Q9iY/NtBde17MX +QRBdJ3NghVdJIgc= +-----END CERTIFICATE----- diff --git a/src/pip/_vendor/certifi/core.py b/src/pip/_vendor/certifi/core.py index 56b52a3c8f4..8987449f6b5 100644 --- a/src/pip/_vendor/certifi/core.py +++ b/src/pip/_vendor/certifi/core.py @@ -9,7 +9,36 @@ import os try: - from importlib.resources import read_text + from importlib.resources import path as get_path, read_text + + _CACERT_CTX = None + _CACERT_PATH = None + + def where(): + # This is slightly terrible, but we want to delay extracting the file + # in cases where we're inside of a zipimport situation until someone + # actually calls where(), but we don't want to re-extract the file + # on every call of where(), so we'll do it once then store it in a + # global variable. + global _CACERT_CTX + global _CACERT_PATH + if _CACERT_PATH is None: + # This is slightly janky, the importlib.resources API wants you to + # manage the cleanup of this file, so it doesn't actually return a + # path, it returns a context manager that will give you the path + # when you enter it and will do any cleanup when you leave it. In + # the common case of not needing a temporary file, it will just + # return the file system location and the __exit__() is a no-op. + # + # We also have to hold onto the actual context manager, because + # it will do the cleanup whenever it gets garbage collected, so + # we will also store that at the global level as well. + _CACERT_CTX = get_path("pip._vendor.certifi", "cacert.pem") + _CACERT_PATH = str(_CACERT_CTX.__enter__()) + + return _CACERT_PATH + + except ImportError: # This fallback will work for Python versions prior to 3.7 that lack the # importlib.resources module but relies on the existing `where` function @@ -19,11 +48,12 @@ def read_text(_module, _path, encoding="ascii"): with open(where(), "r", encoding=encoding) as data: return data.read() + # If we don't have importlib.resources, then we will just do the old logic + # of assuming we're on the filesystem and munge the path directly. + def where(): + f = os.path.dirname(__file__) -def where(): - f = os.path.dirname(__file__) - - return os.path.join(f, "cacert.pem") + return os.path.join(f, "cacert.pem") def contents(): diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 53d5c02867d..4418fe90e8e 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -12,7 +12,7 @@ pep517==0.8.2 progress==1.5 pyparsing==2.4.7 requests==2.24.0 - certifi==2020.04.05.1 + certifi==2020.6.20 chardet==3.0.4 idna==2.9 urllib3==1.25.8 diff --git a/tools/automation/vendoring/patches/certifi.patch b/tools/automation/vendoring/patches/certifi.patch new file mode 100644 index 00000000000..9d5395a7b6b --- /dev/null +++ b/tools/automation/vendoring/patches/certifi.patch @@ -0,0 +1,13 @@ +diff --git a/src/pip/_vendor/certifi/core.py b/src/pip/_vendor/certifi/core.py +index 5d2b8cd32..8987449f6 100644 +--- a/src/pip/_vendor/certifi/core.py ++++ b/src/pip/_vendor/certifi/core.py +@@ -33,7 +33,7 @@ try: + # We also have to hold onto the actual context manager, because + # it will do the cleanup whenever it gets garbage collected, so + # we will also store that at the global level as well. +- _CACERT_CTX = get_path("certifi", "cacert.pem") ++ _CACERT_CTX = get_path("pip._vendor.certifi", "cacert.pem") + _CACERT_PATH = str(_CACERT_CTX.__enter__()) + + return _CACERT_PATH From fe7128c662f7142631974cd9fae98c0b843c7940 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Wed, 22 Jul 2020 05:09:45 +0530 Subject: [PATCH 2299/3170] Upgrade idna to 2.10 --- news/idna.vendor | 1 + src/pip/_vendor/idna/core.py | 2 + src/pip/_vendor/idna/idnadata.py | 107 ++++-- src/pip/_vendor/idna/package_data.py | 2 +- src/pip/_vendor/idna/uts46data.py | 492 +++++++++++++++------------ src/pip/_vendor/vendor.txt | 2 +- 6 files changed, 354 insertions(+), 252 deletions(-) create mode 100644 news/idna.vendor diff --git a/news/idna.vendor b/news/idna.vendor new file mode 100644 index 00000000000..b1bce37afe0 --- /dev/null +++ b/news/idna.vendor @@ -0,0 +1 @@ +Upgrade idna to 2.10 diff --git a/src/pip/_vendor/idna/core.py b/src/pip/_vendor/idna/core.py index 9c3bba2ad7d..41ec5c711d1 100644 --- a/src/pip/_vendor/idna/core.py +++ b/src/pip/_vendor/idna/core.py @@ -300,6 +300,8 @@ def ulabel(label): label = label.lower() if label.startswith(_alabel_prefix): label = label[len(_alabel_prefix):] + if not label: + raise IDNAError('Malformed A-label, no Punycode eligible content found') if label.decode('ascii')[-1] == '-': raise IDNAError('A-label must not end with a hyphen') else: diff --git a/src/pip/_vendor/idna/idnadata.py b/src/pip/_vendor/idna/idnadata.py index 2b81c522cf5..a284e4c84ac 100644 --- a/src/pip/_vendor/idna/idnadata.py +++ b/src/pip/_vendor/idna/idnadata.py @@ -1,6 +1,6 @@ # This file is automatically generated by tools/idna-data -__version__ = "12.1.0" +__version__ = "13.0.0" scripts = { 'Greek': ( 0x37000000374, @@ -48,16 +48,18 @@ 0x300700003008, 0x30210000302a, 0x30380000303c, - 0x340000004db6, - 0x4e0000009ff0, + 0x340000004dc0, + 0x4e0000009ffd, 0xf9000000fa6e, 0xfa700000fada, - 0x200000002a6d7, + 0x16ff000016ff2, + 0x200000002a6de, 0x2a7000002b735, 0x2b7400002b81e, 0x2b8200002cea2, 0x2ceb00002ebe1, 0x2f8000002fa1e, + 0x300000003134b, ), 'Hebrew': ( 0x591000005c8, @@ -389,9 +391,9 @@ 0x853: 68, 0x854: 82, 0x855: 68, - 0x856: 85, - 0x857: 85, - 0x858: 85, + 0x856: 82, + 0x857: 82, + 0x858: 82, 0x860: 68, 0x861: 85, 0x862: 68, @@ -432,6 +434,16 @@ 0x8bb: 68, 0x8bc: 68, 0x8bd: 68, + 0x8be: 68, + 0x8bf: 68, + 0x8c0: 68, + 0x8c1: 68, + 0x8c2: 68, + 0x8c3: 68, + 0x8c4: 68, + 0x8c5: 68, + 0x8c6: 68, + 0x8c7: 68, 0x8e2: 85, 0x1806: 85, 0x1807: 68, @@ -756,6 +768,34 @@ 0x10f52: 68, 0x10f53: 68, 0x10f54: 82, + 0x10fb0: 68, + 0x10fb1: 85, + 0x10fb2: 68, + 0x10fb3: 68, + 0x10fb4: 82, + 0x10fb5: 82, + 0x10fb6: 82, + 0x10fb7: 85, + 0x10fb8: 68, + 0x10fb9: 82, + 0x10fba: 82, + 0x10fbb: 68, + 0x10fbc: 68, + 0x10fbd: 82, + 0x10fbe: 68, + 0x10fbf: 68, + 0x10fc0: 85, + 0x10fc1: 68, + 0x10fc2: 82, + 0x10fc3: 82, + 0x10fc4: 68, + 0x10fc5: 85, + 0x10fc6: 85, + 0x10fc7: 85, + 0x10fc8: 85, + 0x10fc9: 82, + 0x10fca: 68, + 0x10fcb: 76, 0x110bd: 85, 0x110cd: 85, 0x1e900: 68, @@ -1129,7 +1169,7 @@ 0x8400000085c, 0x8600000086b, 0x8a0000008b5, - 0x8b6000008be, + 0x8b6000008c8, 0x8d3000008e2, 0x8e300000958, 0x96000000964, @@ -1188,7 +1228,7 @@ 0xb3c00000b45, 0xb4700000b49, 0xb4b00000b4e, - 0xb5600000b58, + 0xb5500000b58, 0xb5f00000b64, 0xb6600000b70, 0xb7100000b72, @@ -1233,8 +1273,7 @@ 0xce000000ce4, 0xce600000cf0, 0xcf100000cf3, - 0xd0000000d04, - 0xd0500000d0d, + 0xd0000000d0d, 0xd0e00000d11, 0xd1200000d45, 0xd4600000d49, @@ -1243,7 +1282,7 @@ 0xd5f00000d64, 0xd6600000d70, 0xd7a00000d80, - 0xd8200000d84, + 0xd8100000d84, 0xd8500000d97, 0xd9a00000db2, 0xdb300000dbc, @@ -1358,6 +1397,7 @@ 0x1a9000001a9a, 0x1aa700001aa8, 0x1ab000001abe, + 0x1abf00001ac1, 0x1b0000001b4c, 0x1b5000001b5a, 0x1b6b00001b74, @@ -1609,10 +1649,10 @@ 0x30a1000030fb, 0x30fc000030ff, 0x310500003130, - 0x31a0000031bb, + 0x31a0000031c0, 0x31f000003200, - 0x340000004db6, - 0x4e0000009ff0, + 0x340000004dc0, + 0x4e0000009ffd, 0xa0000000a48d, 0xa4d00000a4fe, 0xa5000000a60d, @@ -1727,8 +1767,11 @@ 0xa7bd0000a7be, 0xa7bf0000a7c0, 0xa7c30000a7c4, - 0xa7f70000a7f8, + 0xa7c80000a7c9, + 0xa7ca0000a7cb, + 0xa7f60000a7f8, 0xa7fa0000a828, + 0xa82c0000a82d, 0xa8400000a874, 0xa8800000a8c6, 0xa8d00000a8da, @@ -1753,7 +1796,7 @@ 0xab200000ab27, 0xab280000ab2f, 0xab300000ab5b, - 0xab600000ab68, + 0xab600000ab6a, 0xabc00000abeb, 0xabec0000abee, 0xabf00000abfa, @@ -1827,9 +1870,13 @@ 0x10cc000010cf3, 0x10d0000010d28, 0x10d3000010d3a, + 0x10e8000010eaa, + 0x10eab00010ead, + 0x10eb000010eb2, 0x10f0000010f1d, 0x10f2700010f28, 0x10f3000010f51, + 0x10fb000010fc5, 0x10fe000010ff7, 0x1100000011047, 0x1106600011070, @@ -1838,12 +1885,12 @@ 0x110f0000110fa, 0x1110000011135, 0x1113600011140, - 0x1114400011147, + 0x1114400011148, 0x1115000011174, 0x1117600011177, 0x11180000111c5, 0x111c9000111cd, - 0x111d0000111db, + 0x111ce000111db, 0x111dc000111dd, 0x1120000011212, 0x1121300011238, @@ -1872,7 +1919,7 @@ 0x1137000011375, 0x114000001144b, 0x114500001145a, - 0x1145e00011460, + 0x1145e00011462, 0x11480000114c6, 0x114c7000114c8, 0x114d0000114da, @@ -1889,7 +1936,14 @@ 0x117300001173a, 0x118000001183b, 0x118c0000118ea, - 0x118ff00011900, + 0x118ff00011907, + 0x119090001190a, + 0x1190c00011914, + 0x1191500011917, + 0x1191800011936, + 0x1193700011939, + 0x1193b00011944, + 0x119500001195a, 0x119a0000119a8, 0x119aa000119d8, 0x119da000119e2, @@ -1920,6 +1974,7 @@ 0x11d9300011d99, 0x11da000011daa, 0x11ee000011ef7, + 0x11fb000011fb1, 0x120000001239a, 0x1248000012544, 0x130000001342f, @@ -1939,9 +1994,11 @@ 0x16f4f00016f88, 0x16f8f00016fa0, 0x16fe000016fe2, - 0x16fe300016fe4, + 0x16fe300016fe5, + 0x16ff000016ff2, 0x17000000187f8, - 0x1880000018af3, + 0x1880000018cd6, + 0x18d0000018d09, 0x1b0000001b11f, 0x1b1500001b153, 0x1b1640001b168, @@ -1971,11 +2028,13 @@ 0x1e8d00001e8d7, 0x1e9220001e94c, 0x1e9500001e95a, - 0x200000002a6d7, + 0x1fbf00001fbfa, + 0x200000002a6de, 0x2a7000002b735, 0x2b7400002b81e, 0x2b8200002cea2, 0x2ceb00002ebe1, + 0x300000003134b, ), 'CONTEXTJ': ( 0x200c0000200e, diff --git a/src/pip/_vendor/idna/package_data.py b/src/pip/_vendor/idna/package_data.py index b5d8216558a..ce1c521d23a 100644 --- a/src/pip/_vendor/idna/package_data.py +++ b/src/pip/_vendor/idna/package_data.py @@ -1,2 +1,2 @@ -__version__ = '2.9' +__version__ = '2.10' diff --git a/src/pip/_vendor/idna/uts46data.py b/src/pip/_vendor/idna/uts46data.py index 2711136d7d2..3766dd49f6d 100644 --- a/src/pip/_vendor/idna/uts46data.py +++ b/src/pip/_vendor/idna/uts46data.py @@ -4,7 +4,7 @@ """IDNA Mapping Table from UTS46.""" -__version__ = "12.1.0" +__version__ = "13.0.0" def _seg_0(): return [ (0x0, '3'), @@ -1074,7 +1074,7 @@ def _seg_10(): (0x8A0, 'V'), (0x8B5, 'X'), (0x8B6, 'V'), - (0x8BE, 'X'), + (0x8C8, 'X'), (0x8D3, 'V'), (0x8E2, 'X'), (0x8E3, 'V'), @@ -1205,7 +1205,7 @@ def _seg_11(): (0xB49, 'X'), (0xB4B, 'V'), (0xB4E, 'X'), - (0xB56, 'V'), + (0xB55, 'V'), (0xB58, 'X'), (0xB5C, 'M', u'ଡ଼'), (0xB5D, 'M', u'ଢ଼'), @@ -1299,8 +1299,6 @@ def _seg_12(): (0xCF1, 'V'), (0xCF3, 'X'), (0xD00, 'V'), - (0xD04, 'X'), - (0xD05, 'V'), (0xD0D, 'X'), (0xD0E, 'V'), (0xD11, 'X'), @@ -1314,7 +1312,7 @@ def _seg_12(): (0xD64, 'X'), (0xD66, 'V'), (0xD80, 'X'), - (0xD82, 'V'), + (0xD81, 'V'), (0xD84, 'X'), (0xD85, 'V'), (0xD97, 'X'), @@ -1355,12 +1353,12 @@ def _seg_12(): (0xEA5, 'V'), (0xEA6, 'X'), (0xEA7, 'V'), + (0xEB3, 'M', u'ໍາ'), + (0xEB4, 'V'), ] def _seg_13(): return [ - (0xEB3, 'M', u'ໍາ'), - (0xEB4, 'V'), (0xEBE, 'X'), (0xEC0, 'V'), (0xEC5, 'X'), @@ -1459,12 +1457,12 @@ def _seg_13(): (0x12C8, 'V'), (0x12D7, 'X'), (0x12D8, 'V'), + (0x1311, 'X'), + (0x1312, 'V'), ] def _seg_14(): return [ - (0x1311, 'X'), - (0x1312, 'V'), (0x1316, 'X'), (0x1318, 'V'), (0x135B, 'X'), @@ -1553,7 +1551,7 @@ def _seg_14(): (0x1AA0, 'V'), (0x1AAE, 'X'), (0x1AB0, 'V'), - (0x1ABF, 'X'), + (0x1AC1, 'X'), (0x1B00, 'V'), (0x1B4C, 'X'), (0x1B50, 'V'), @@ -1563,12 +1561,12 @@ def _seg_14(): (0x1BFC, 'V'), (0x1C38, 'X'), (0x1C3B, 'V'), + (0x1C4A, 'X'), + (0x1C4D, 'V'), ] def _seg_15(): return [ - (0x1C4A, 'X'), - (0x1C4D, 'V'), (0x1C80, 'M', u'в'), (0x1C81, 'M', u'д'), (0x1C82, 'M', u'о'), @@ -1667,12 +1665,12 @@ def _seg_15(): (0x1D4E, 'V'), (0x1D4F, 'M', u'k'), (0x1D50, 'M', u'm'), + (0x1D51, 'M', u'ŋ'), + (0x1D52, 'M', u'o'), ] def _seg_16(): return [ - (0x1D51, 'M', u'ŋ'), - (0x1D52, 'M', u'o'), (0x1D53, 'M', u'ɔ'), (0x1D54, 'M', u'ᴖ'), (0x1D55, 'M', u'ᴗ'), @@ -1771,12 +1769,12 @@ def _seg_16(): (0x1E1C, 'M', u'ḝ'), (0x1E1D, 'V'), (0x1E1E, 'M', u'ḟ'), + (0x1E1F, 'V'), + (0x1E20, 'M', u'ḡ'), ] def _seg_17(): return [ - (0x1E1F, 'V'), - (0x1E20, 'M', u'ḡ'), (0x1E21, 'V'), (0x1E22, 'M', u'ḣ'), (0x1E23, 'V'), @@ -1875,12 +1873,12 @@ def _seg_17(): (0x1E80, 'M', u'ẁ'), (0x1E81, 'V'), (0x1E82, 'M', u'ẃ'), + (0x1E83, 'V'), + (0x1E84, 'M', u'ẅ'), ] def _seg_18(): return [ - (0x1E83, 'V'), - (0x1E84, 'M', u'ẅ'), (0x1E85, 'V'), (0x1E86, 'M', u'ẇ'), (0x1E87, 'V'), @@ -1979,12 +1977,12 @@ def _seg_18(): (0x1EE9, 'V'), (0x1EEA, 'M', u'ừ'), (0x1EEB, 'V'), + (0x1EEC, 'M', u'ử'), + (0x1EED, 'V'), ] def _seg_19(): return [ - (0x1EEC, 'M', u'ử'), - (0x1EED, 'V'), (0x1EEE, 'M', u'ữ'), (0x1EEF, 'V'), (0x1EF0, 'M', u'ự'), @@ -2083,12 +2081,12 @@ def _seg_19(): (0x1F80, 'M', u'ἀι'), (0x1F81, 'M', u'ἁι'), (0x1F82, 'M', u'ἂι'), + (0x1F83, 'M', u'ἃι'), + (0x1F84, 'M', u'ἄι'), ] def _seg_20(): return [ - (0x1F83, 'M', u'ἃι'), - (0x1F84, 'M', u'ἄι'), (0x1F85, 'M', u'ἅι'), (0x1F86, 'M', u'ἆι'), (0x1F87, 'M', u'ἇι'), @@ -2187,12 +2185,12 @@ def _seg_20(): (0x1FEE, '3', u' ̈́'), (0x1FEF, '3', u'`'), (0x1FF0, 'X'), + (0x1FF2, 'M', u'ὼι'), + (0x1FF3, 'M', u'ωι'), ] def _seg_21(): return [ - (0x1FF2, 'M', u'ὼι'), - (0x1FF3, 'M', u'ωι'), (0x1FF4, 'M', u'ώι'), (0x1FF5, 'X'), (0x1FF6, 'V'), @@ -2291,12 +2289,12 @@ def _seg_21(): (0x20C0, 'X'), (0x20D0, 'V'), (0x20F1, 'X'), + (0x2100, '3', u'a/c'), + (0x2101, '3', u'a/s'), ] def _seg_22(): return [ - (0x2100, '3', u'a/c'), - (0x2101, '3', u'a/s'), (0x2102, 'M', u'c'), (0x2103, 'M', u'°c'), (0x2104, 'V'), @@ -2395,12 +2393,12 @@ def _seg_22(): (0x2175, 'M', u'vi'), (0x2176, 'M', u'vii'), (0x2177, 'M', u'viii'), + (0x2178, 'M', u'ix'), + (0x2179, 'M', u'x'), ] def _seg_23(): return [ - (0x2178, 'M', u'ix'), - (0x2179, 'M', u'x'), (0x217A, 'M', u'xi'), (0x217B, 'M', u'xii'), (0x217C, 'M', u'l'), @@ -2499,12 +2497,12 @@ def _seg_23(): (0x24B5, '3', u'(z)'), (0x24B6, 'M', u'a'), (0x24B7, 'M', u'b'), + (0x24B8, 'M', u'c'), + (0x24B9, 'M', u'd'), ] def _seg_24(): return [ - (0x24B8, 'M', u'c'), - (0x24B9, 'M', u'd'), (0x24BA, 'M', u'e'), (0x24BB, 'M', u'f'), (0x24BC, 'M', u'g'), @@ -2566,7 +2564,7 @@ def _seg_24(): (0x2B74, 'X'), (0x2B76, 'V'), (0x2B96, 'X'), - (0x2B98, 'V'), + (0x2B97, 'V'), (0x2C00, 'M', u'ⰰ'), (0x2C01, 'M', u'ⰱ'), (0x2C02, 'M', u'ⰲ'), @@ -2603,12 +2601,12 @@ def _seg_24(): (0x2C21, 'M', u'ⱑ'), (0x2C22, 'M', u'ⱒ'), (0x2C23, 'M', u'ⱓ'), + (0x2C24, 'M', u'ⱔ'), + (0x2C25, 'M', u'ⱕ'), ] def _seg_25(): return [ - (0x2C24, 'M', u'ⱔ'), - (0x2C25, 'M', u'ⱕ'), (0x2C26, 'M', u'ⱖ'), (0x2C27, 'M', u'ⱗ'), (0x2C28, 'M', u'ⱘ'), @@ -2707,12 +2705,12 @@ def _seg_25(): (0x2CBA, 'M', u'ⲻ'), (0x2CBB, 'V'), (0x2CBC, 'M', u'ⲽ'), + (0x2CBD, 'V'), + (0x2CBE, 'M', u'ⲿ'), ] def _seg_26(): return [ - (0x2CBD, 'V'), - (0x2CBE, 'M', u'ⲿ'), (0x2CBF, 'V'), (0x2CC0, 'M', u'ⳁ'), (0x2CC1, 'V'), @@ -2787,7 +2785,7 @@ def _seg_26(): (0x2DD8, 'V'), (0x2DDF, 'X'), (0x2DE0, 'V'), - (0x2E50, 'X'), + (0x2E53, 'X'), (0x2E80, 'V'), (0x2E9A, 'X'), (0x2E9B, 'V'), @@ -2811,12 +2809,12 @@ def _seg_26(): (0x2F0D, 'M', u'冖'), (0x2F0E, 'M', u'冫'), (0x2F0F, 'M', u'几'), + (0x2F10, 'M', u'凵'), + (0x2F11, 'M', u'刀'), ] def _seg_27(): return [ - (0x2F10, 'M', u'凵'), - (0x2F11, 'M', u'刀'), (0x2F12, 'M', u'力'), (0x2F13, 'M', u'勹'), (0x2F14, 'M', u'匕'), @@ -2915,12 +2913,12 @@ def _seg_27(): (0x2F71, 'M', u'禸'), (0x2F72, 'M', u'禾'), (0x2F73, 'M', u'穴'), + (0x2F74, 'M', u'立'), + (0x2F75, 'M', u'竹'), ] def _seg_28(): return [ - (0x2F74, 'M', u'立'), - (0x2F75, 'M', u'竹'), (0x2F76, 'M', u'米'), (0x2F77, 'M', u'糸'), (0x2F78, 'M', u'缶'), @@ -3019,12 +3017,12 @@ def _seg_28(): (0x2FD5, 'M', u'龠'), (0x2FD6, 'X'), (0x3000, '3', u' '), + (0x3001, 'V'), + (0x3002, 'M', u'.'), ] def _seg_29(): return [ - (0x3001, 'V'), - (0x3002, 'M', u'.'), (0x3003, 'V'), (0x3036, 'M', u'〒'), (0x3037, 'V'), @@ -3123,12 +3121,12 @@ def _seg_29(): (0x317C, 'M', u'ᄯ'), (0x317D, 'M', u'ᄲ'), (0x317E, 'M', u'ᄶ'), + (0x317F, 'M', u'ᅀ'), + (0x3180, 'M', u'ᅇ'), ] def _seg_30(): return [ - (0x317F, 'M', u'ᅀ'), - (0x3180, 'M', u'ᅇ'), (0x3181, 'M', u'ᅌ'), (0x3182, 'M', u'ᇱ'), (0x3183, 'M', u'ᇲ'), @@ -3160,8 +3158,6 @@ def _seg_30(): (0x319E, 'M', u'地'), (0x319F, 'M', u'人'), (0x31A0, 'V'), - (0x31BB, 'X'), - (0x31C0, 'V'), (0x31E4, 'X'), (0x31F0, 'V'), (0x3200, '3', u'(ᄀ)'), @@ -3227,14 +3223,14 @@ def _seg_30(): (0x323C, '3', u'(監)'), (0x323D, '3', u'(企)'), (0x323E, '3', u'(資)'), - ] - -def _seg_31(): - return [ (0x323F, '3', u'(協)'), (0x3240, '3', u'(祭)'), (0x3241, '3', u'(休)'), (0x3242, '3', u'(自)'), + ] + +def _seg_31(): + return [ (0x3243, '3', u'(至)'), (0x3244, 'M', u'問'), (0x3245, 'M', u'幼'), @@ -3331,14 +3327,14 @@ def _seg_31(): (0x32A7, 'M', u'左'), (0x32A8, 'M', u'右'), (0x32A9, 'M', u'医'), - ] - -def _seg_32(): - return [ (0x32AA, 'M', u'宗'), (0x32AB, 'M', u'学'), (0x32AC, 'M', u'監'), (0x32AD, 'M', u'企'), + ] + +def _seg_32(): + return [ (0x32AE, 'M', u'資'), (0x32AF, 'M', u'協'), (0x32B0, 'M', u'夜'), @@ -3435,14 +3431,14 @@ def _seg_32(): (0x330B, 'M', u'カイリ'), (0x330C, 'M', u'カラット'), (0x330D, 'M', u'カロリー'), - ] - -def _seg_33(): - return [ (0x330E, 'M', u'ガロン'), (0x330F, 'M', u'ガンマ'), (0x3310, 'M', u'ギガ'), (0x3311, 'M', u'ギニー'), + ] + +def _seg_33(): + return [ (0x3312, 'M', u'キュリー'), (0x3313, 'M', u'ギルダー'), (0x3314, 'M', u'キロ'), @@ -3539,14 +3535,14 @@ def _seg_33(): (0x336F, 'M', u'23点'), (0x3370, 'M', u'24点'), (0x3371, 'M', u'hpa'), - ] - -def _seg_34(): - return [ (0x3372, 'M', u'da'), (0x3373, 'M', u'au'), (0x3374, 'M', u'bar'), (0x3375, 'M', u'ov'), + ] + +def _seg_34(): + return [ (0x3376, 'M', u'pc'), (0x3377, 'M', u'dm'), (0x3378, 'M', u'dm2'), @@ -3643,14 +3639,14 @@ def _seg_34(): (0x33D3, 'M', u'lx'), (0x33D4, 'M', u'mb'), (0x33D5, 'M', u'mil'), - ] - -def _seg_35(): - return [ (0x33D6, 'M', u'mol'), (0x33D7, 'M', u'ph'), (0x33D8, 'X'), (0x33D9, 'M', u'ppm'), + ] + +def _seg_35(): + return [ (0x33DA, 'M', u'pr'), (0x33DB, 'M', u'sr'), (0x33DC, 'M', u'sv'), @@ -3690,9 +3686,7 @@ def _seg_35(): (0x33FE, 'M', u'31日'), (0x33FF, 'M', u'gal'), (0x3400, 'V'), - (0x4DB6, 'X'), - (0x4DC0, 'V'), - (0x9FF0, 'X'), + (0x9FFD, 'X'), (0xA000, 'V'), (0xA48D, 'X'), (0xA490, 'V'), @@ -3747,16 +3741,16 @@ def _seg_35(): (0xA66D, 'V'), (0xA680, 'M', u'ꚁ'), (0xA681, 'V'), - ] - -def _seg_36(): - return [ (0xA682, 'M', u'ꚃ'), (0xA683, 'V'), (0xA684, 'M', u'ꚅ'), (0xA685, 'V'), (0xA686, 'M', u'ꚇ'), (0xA687, 'V'), + ] + +def _seg_36(): + return [ (0xA688, 'M', u'ꚉ'), (0xA689, 'V'), (0xA68A, 'M', u'ꚋ'), @@ -3851,16 +3845,16 @@ def _seg_36(): (0xA766, 'M', u'ꝧ'), (0xA767, 'V'), (0xA768, 'M', u'ꝩ'), - ] - -def _seg_37(): - return [ (0xA769, 'V'), (0xA76A, 'M', u'ꝫ'), (0xA76B, 'V'), (0xA76C, 'M', u'ꝭ'), (0xA76D, 'V'), (0xA76E, 'M', u'ꝯ'), + ] + +def _seg_37(): + return [ (0xA76F, 'V'), (0xA770, 'M', u'ꝯ'), (0xA771, 'V'), @@ -3935,12 +3929,17 @@ def _seg_37(): (0xA7C4, 'M', u'ꞔ'), (0xA7C5, 'M', u'ʂ'), (0xA7C6, 'M', u'ᶎ'), - (0xA7C7, 'X'), - (0xA7F7, 'V'), + (0xA7C7, 'M', u'ꟈ'), + (0xA7C8, 'V'), + (0xA7C9, 'M', u'ꟊ'), + (0xA7CA, 'V'), + (0xA7CB, 'X'), + (0xA7F5, 'M', u'ꟶ'), + (0xA7F6, 'V'), (0xA7F8, 'M', u'ħ'), (0xA7F9, 'M', u'œ'), (0xA7FA, 'V'), - (0xA82C, 'X'), + (0xA82D, 'X'), (0xA830, 'V'), (0xA83A, 'X'), (0xA840, 'V'), @@ -3955,11 +3954,11 @@ def _seg_37(): (0xA97D, 'X'), (0xA980, 'V'), (0xA9CE, 'X'), + (0xA9CF, 'V'), ] def _seg_38(): return [ - (0xA9CF, 'V'), (0xA9DA, 'X'), (0xA9DE, 'V'), (0xA9FF, 'X'), @@ -3989,7 +3988,9 @@ def _seg_38(): (0xAB5E, 'M', u'ɫ'), (0xAB5F, 'M', u'ꭒ'), (0xAB60, 'V'), - (0xAB68, 'X'), + (0xAB69, 'M', u'ʍ'), + (0xAB6A, 'V'), + (0xAB6C, 'X'), (0xAB70, 'M', u'Ꭰ'), (0xAB71, 'M', u'Ꭱ'), (0xAB72, 'M', u'Ꭲ'), @@ -4058,11 +4059,11 @@ def _seg_38(): (0xABB1, 'M', u'Ꮱ'), (0xABB2, 'M', u'Ꮲ'), (0xABB3, 'M', u'Ꮳ'), - (0xABB4, 'M', u'Ꮴ'), ] def _seg_39(): return [ + (0xABB4, 'M', u'Ꮴ'), (0xABB5, 'M', u'Ꮵ'), (0xABB6, 'M', u'Ꮶ'), (0xABB7, 'M', u'Ꮷ'), @@ -4162,11 +4163,11 @@ def _seg_39(): (0xF94C, 'M', u'樓'), (0xF94D, 'M', u'淚'), (0xF94E, 'M', u'漏'), - (0xF94F, 'M', u'累'), ] def _seg_40(): return [ + (0xF94F, 'M', u'累'), (0xF950, 'M', u'縷'), (0xF951, 'M', u'陋'), (0xF952, 'M', u'勒'), @@ -4266,11 +4267,11 @@ def _seg_40(): (0xF9B0, 'M', u'聆'), (0xF9B1, 'M', u'鈴'), (0xF9B2, 'M', u'零'), - (0xF9B3, 'M', u'靈'), ] def _seg_41(): return [ + (0xF9B3, 'M', u'靈'), (0xF9B4, 'M', u'領'), (0xF9B5, 'M', u'例'), (0xF9B6, 'M', u'禮'), @@ -4370,11 +4371,11 @@ def _seg_41(): (0xFA16, 'M', u'猪'), (0xFA17, 'M', u'益'), (0xFA18, 'M', u'礼'), - (0xFA19, 'M', u'神'), ] def _seg_42(): return [ + (0xFA19, 'M', u'神'), (0xFA1A, 'M', u'祥'), (0xFA1B, 'M', u'福'), (0xFA1C, 'M', u'靖'), @@ -4474,11 +4475,11 @@ def _seg_42(): (0xFA7F, 'M', u'奔'), (0xFA80, 'M', u'婢'), (0xFA81, 'M', u'嬨'), - (0xFA82, 'M', u'廒'), ] def _seg_43(): return [ + (0xFA82, 'M', u'廒'), (0xFA83, 'M', u'廙'), (0xFA84, 'M', u'彩'), (0xFA85, 'M', u'徭'), @@ -4578,11 +4579,11 @@ def _seg_43(): (0xFB14, 'M', u'մե'), (0xFB15, 'M', u'մի'), (0xFB16, 'M', u'վն'), - (0xFB17, 'M', u'մխ'), ] def _seg_44(): return [ + (0xFB17, 'M', u'մխ'), (0xFB18, 'X'), (0xFB1D, 'M', u'יִ'), (0xFB1E, 'V'), @@ -4682,11 +4683,11 @@ def _seg_44(): (0xFBEE, 'M', u'ئو'), (0xFBF0, 'M', u'ئۇ'), (0xFBF2, 'M', u'ئۆ'), - (0xFBF4, 'M', u'ئۈ'), ] def _seg_45(): return [ + (0xFBF4, 'M', u'ئۈ'), (0xFBF6, 'M', u'ئې'), (0xFBF9, 'M', u'ئى'), (0xFBFC, 'M', u'ی'), @@ -4786,11 +4787,11 @@ def _seg_45(): (0xFC5D, 'M', u'ىٰ'), (0xFC5E, '3', u' ٌّ'), (0xFC5F, '3', u' ٍّ'), - (0xFC60, '3', u' َّ'), ] def _seg_46(): return [ + (0xFC60, '3', u' َّ'), (0xFC61, '3', u' ُّ'), (0xFC62, '3', u' ِّ'), (0xFC63, '3', u' ّٰ'), @@ -4890,11 +4891,11 @@ def _seg_46(): (0xFCC1, 'M', u'فم'), (0xFCC2, 'M', u'قح'), (0xFCC3, 'M', u'قم'), - (0xFCC4, 'M', u'كج'), ] def _seg_47(): return [ + (0xFCC4, 'M', u'كج'), (0xFCC5, 'M', u'كح'), (0xFCC6, 'M', u'كخ'), (0xFCC7, 'M', u'كل'), @@ -4994,11 +4995,11 @@ def _seg_47(): (0xFD25, 'M', u'شج'), (0xFD26, 'M', u'شح'), (0xFD27, 'M', u'شخ'), - (0xFD28, 'M', u'شم'), ] def _seg_48(): return [ + (0xFD28, 'M', u'شم'), (0xFD29, 'M', u'شر'), (0xFD2A, 'M', u'سر'), (0xFD2B, 'M', u'صر'), @@ -5098,11 +5099,11 @@ def _seg_48(): (0xFDAC, 'M', u'لجي'), (0xFDAD, 'M', u'لمي'), (0xFDAE, 'M', u'يحي'), - (0xFDAF, 'M', u'يجي'), ] def _seg_49(): return [ + (0xFDAF, 'M', u'يجي'), (0xFDB0, 'M', u'يمي'), (0xFDB1, 'M', u'ممي'), (0xFDB2, 'M', u'قمي'), @@ -5202,11 +5203,11 @@ def _seg_49(): (0xFE64, '3', u'<'), (0xFE65, '3', u'>'), (0xFE66, '3', u'='), - (0xFE67, 'X'), ] def _seg_50(): return [ + (0xFE67, 'X'), (0xFE68, '3', u'\\'), (0xFE69, '3', u'$'), (0xFE6A, '3', u'%'), @@ -5306,11 +5307,11 @@ def _seg_50(): (0xFF21, 'M', u'a'), (0xFF22, 'M', u'b'), (0xFF23, 'M', u'c'), - (0xFF24, 'M', u'd'), ] def _seg_51(): return [ + (0xFF24, 'M', u'd'), (0xFF25, 'M', u'e'), (0xFF26, 'M', u'f'), (0xFF27, 'M', u'g'), @@ -5410,11 +5411,11 @@ def _seg_51(): (0xFF85, 'M', u'ナ'), (0xFF86, 'M', u'ニ'), (0xFF87, 'M', u'ヌ'), - (0xFF88, 'M', u'ネ'), ] def _seg_52(): return [ + (0xFF88, 'M', u'ネ'), (0xFF89, 'M', u'ノ'), (0xFF8A, 'M', u'ハ'), (0xFF8B, 'M', u'ヒ'), @@ -5514,11 +5515,11 @@ def _seg_52(): (0x10000, 'V'), (0x1000C, 'X'), (0x1000D, 'V'), - (0x10027, 'X'), ] def _seg_53(): return [ + (0x10027, 'X'), (0x10028, 'V'), (0x1003B, 'X'), (0x1003C, 'V'), @@ -5536,7 +5537,7 @@ def _seg_53(): (0x10137, 'V'), (0x1018F, 'X'), (0x10190, 'V'), - (0x1019C, 'X'), + (0x1019D, 'X'), (0x101A0, 'V'), (0x101A1, 'X'), (0x101D0, 'V'), @@ -5618,11 +5619,11 @@ def _seg_53(): (0x104BC, 'M', u'𐓤'), (0x104BD, 'M', u'𐓥'), (0x104BE, 'M', u'𐓦'), - (0x104BF, 'M', u'𐓧'), ] def _seg_54(): return [ + (0x104BF, 'M', u'𐓧'), (0x104C0, 'M', u'𐓨'), (0x104C1, 'M', u'𐓩'), (0x104C2, 'M', u'𐓪'), @@ -5722,11 +5723,11 @@ def _seg_54(): (0x10B9D, 'X'), (0x10BA9, 'V'), (0x10BB0, 'X'), - (0x10C00, 'V'), ] def _seg_55(): return [ + (0x10C00, 'V'), (0x10C49, 'X'), (0x10C80, 'M', u'𐳀'), (0x10C81, 'M', u'𐳁'), @@ -5788,10 +5789,18 @@ def _seg_55(): (0x10D3A, 'X'), (0x10E60, 'V'), (0x10E7F, 'X'), + (0x10E80, 'V'), + (0x10EAA, 'X'), + (0x10EAB, 'V'), + (0x10EAE, 'X'), + (0x10EB0, 'V'), + (0x10EB2, 'X'), (0x10F00, 'V'), (0x10F28, 'X'), (0x10F30, 'V'), (0x10F5A, 'X'), + (0x10FB0, 'V'), + (0x10FCC, 'X'), (0x10FE0, 'V'), (0x10FF7, 'X'), (0x11000, 'V'), @@ -5809,17 +5818,19 @@ def _seg_55(): (0x11100, 'V'), (0x11135, 'X'), (0x11136, 'V'), - (0x11147, 'X'), + (0x11148, 'X'), (0x11150, 'V'), (0x11177, 'X'), (0x11180, 'V'), - (0x111CE, 'X'), - (0x111D0, 'V'), (0x111E0, 'X'), (0x111E1, 'V'), (0x111F5, 'X'), (0x11200, 'V'), (0x11212, 'X'), + ] + +def _seg_56(): + return [ (0x11213, 'V'), (0x1123F, 'X'), (0x11280, 'V'), @@ -5827,10 +5838,6 @@ def _seg_55(): (0x11288, 'V'), (0x11289, 'X'), (0x1128A, 'V'), - ] - -def _seg_56(): - return [ (0x1128E, 'X'), (0x1128F, 'V'), (0x1129E, 'X'), @@ -5871,11 +5878,9 @@ def _seg_56(): (0x11370, 'V'), (0x11375, 'X'), (0x11400, 'V'), - (0x1145A, 'X'), - (0x1145B, 'V'), (0x1145C, 'X'), (0x1145D, 'V'), - (0x11460, 'X'), + (0x11462, 'X'), (0x11480, 'V'), (0x114C8, 'X'), (0x114D0, 'V'), @@ -5926,22 +5931,36 @@ def _seg_56(): (0x118B5, 'M', u'𑣕'), (0x118B6, 'M', u'𑣖'), (0x118B7, 'M', u'𑣗'), + ] + +def _seg_57(): + return [ (0x118B8, 'M', u'𑣘'), (0x118B9, 'M', u'𑣙'), (0x118BA, 'M', u'𑣚'), (0x118BB, 'M', u'𑣛'), (0x118BC, 'M', u'𑣜'), - ] - -def _seg_57(): - return [ (0x118BD, 'M', u'𑣝'), (0x118BE, 'M', u'𑣞'), (0x118BF, 'M', u'𑣟'), (0x118C0, 'V'), (0x118F3, 'X'), (0x118FF, 'V'), - (0x11900, 'X'), + (0x11907, 'X'), + (0x11909, 'V'), + (0x1190A, 'X'), + (0x1190C, 'V'), + (0x11914, 'X'), + (0x11915, 'V'), + (0x11917, 'X'), + (0x11918, 'V'), + (0x11936, 'X'), + (0x11937, 'V'), + (0x11939, 'X'), + (0x1193B, 'V'), + (0x11947, 'X'), + (0x11950, 'V'), + (0x1195A, 'X'), (0x119A0, 'V'), (0x119A8, 'X'), (0x119AA, 'V'), @@ -5996,6 +6015,8 @@ def _seg_57(): (0x11DAA, 'X'), (0x11EE0, 'V'), (0x11EF9, 'X'), + (0x11FB0, 'V'), + (0x11FB1, 'X'), (0x11FC0, 'V'), (0x11FF2, 'X'), (0x11FFF, 'V'), @@ -6014,6 +6035,10 @@ def _seg_57(): (0x16A39, 'X'), (0x16A40, 'V'), (0x16A5F, 'X'), + ] + +def _seg_58(): + return [ (0x16A60, 'V'), (0x16A6A, 'X'), (0x16A6E, 'V'), @@ -6035,10 +6060,6 @@ def _seg_57(): (0x16E40, 'M', u'𖹠'), (0x16E41, 'M', u'𖹡'), (0x16E42, 'M', u'𖹢'), - ] - -def _seg_58(): - return [ (0x16E43, 'M', u'𖹣'), (0x16E44, 'M', u'𖹤'), (0x16E45, 'M', u'𖹥'), @@ -6077,11 +6098,15 @@ def _seg_58(): (0x16F8F, 'V'), (0x16FA0, 'X'), (0x16FE0, 'V'), - (0x16FE4, 'X'), + (0x16FE5, 'X'), + (0x16FF0, 'V'), + (0x16FF2, 'X'), (0x17000, 'V'), (0x187F8, 'X'), (0x18800, 'V'), - (0x18AF3, 'X'), + (0x18CD6, 'X'), + (0x18D00, 'V'), + (0x18D09, 'X'), (0x1B000, 'V'), (0x1B11F, 'X'), (0x1B150, 'V'), @@ -6114,6 +6139,10 @@ def _seg_58(): (0x1D163, 'M', u'𝅘𝅥𝅱'), (0x1D164, 'M', u'𝅘𝅥𝅲'), (0x1D165, 'V'), + ] + +def _seg_59(): + return [ (0x1D173, 'X'), (0x1D17B, 'V'), (0x1D1BB, 'M', u'𝆹𝅥'), @@ -6139,10 +6168,6 @@ def _seg_58(): (0x1D404, 'M', u'e'), (0x1D405, 'M', u'f'), (0x1D406, 'M', u'g'), - ] - -def _seg_59(): - return [ (0x1D407, 'M', u'h'), (0x1D408, 'M', u'i'), (0x1D409, 'M', u'j'), @@ -6218,6 +6243,10 @@ def _seg_59(): (0x1D44F, 'M', u'b'), (0x1D450, 'M', u'c'), (0x1D451, 'M', u'd'), + ] + +def _seg_60(): + return [ (0x1D452, 'M', u'e'), (0x1D453, 'M', u'f'), (0x1D454, 'M', u'g'), @@ -6243,10 +6272,6 @@ def _seg_59(): (0x1D468, 'M', u'a'), (0x1D469, 'M', u'b'), (0x1D46A, 'M', u'c'), - ] - -def _seg_60(): - return [ (0x1D46B, 'M', u'd'), (0x1D46C, 'M', u'e'), (0x1D46D, 'M', u'f'), @@ -6322,6 +6347,10 @@ def _seg_60(): (0x1D4B6, 'M', u'a'), (0x1D4B7, 'M', u'b'), (0x1D4B8, 'M', u'c'), + ] + +def _seg_61(): + return [ (0x1D4B9, 'M', u'd'), (0x1D4BA, 'X'), (0x1D4BB, 'M', u'f'), @@ -6347,10 +6376,6 @@ def _seg_60(): (0x1D4CF, 'M', u'z'), (0x1D4D0, 'M', u'a'), (0x1D4D1, 'M', u'b'), - ] - -def _seg_61(): - return [ (0x1D4D2, 'M', u'c'), (0x1D4D3, 'M', u'd'), (0x1D4D4, 'M', u'e'), @@ -6426,6 +6451,10 @@ def _seg_61(): (0x1D51B, 'M', u'x'), (0x1D51C, 'M', u'y'), (0x1D51D, 'X'), + ] + +def _seg_62(): + return [ (0x1D51E, 'M', u'a'), (0x1D51F, 'M', u'b'), (0x1D520, 'M', u'c'), @@ -6451,10 +6480,6 @@ def _seg_61(): (0x1D534, 'M', u'w'), (0x1D535, 'M', u'x'), (0x1D536, 'M', u'y'), - ] - -def _seg_62(): - return [ (0x1D537, 'M', u'z'), (0x1D538, 'M', u'a'), (0x1D539, 'M', u'b'), @@ -6530,6 +6555,10 @@ def _seg_62(): (0x1D581, 'M', u'v'), (0x1D582, 'M', u'w'), (0x1D583, 'M', u'x'), + ] + +def _seg_63(): + return [ (0x1D584, 'M', u'y'), (0x1D585, 'M', u'z'), (0x1D586, 'M', u'a'), @@ -6555,10 +6584,6 @@ def _seg_62(): (0x1D59A, 'M', u'u'), (0x1D59B, 'M', u'v'), (0x1D59C, 'M', u'w'), - ] - -def _seg_63(): - return [ (0x1D59D, 'M', u'x'), (0x1D59E, 'M', u'y'), (0x1D59F, 'M', u'z'), @@ -6634,6 +6659,10 @@ def _seg_63(): (0x1D5E5, 'M', u'r'), (0x1D5E6, 'M', u's'), (0x1D5E7, 'M', u't'), + ] + +def _seg_64(): + return [ (0x1D5E8, 'M', u'u'), (0x1D5E9, 'M', u'v'), (0x1D5EA, 'M', u'w'), @@ -6659,10 +6688,6 @@ def _seg_63(): (0x1D5FE, 'M', u'q'), (0x1D5FF, 'M', u'r'), (0x1D600, 'M', u's'), - ] - -def _seg_64(): - return [ (0x1D601, 'M', u't'), (0x1D602, 'M', u'u'), (0x1D603, 'M', u'v'), @@ -6738,6 +6763,10 @@ def _seg_64(): (0x1D649, 'M', u'n'), (0x1D64A, 'M', u'o'), (0x1D64B, 'M', u'p'), + ] + +def _seg_65(): + return [ (0x1D64C, 'M', u'q'), (0x1D64D, 'M', u'r'), (0x1D64E, 'M', u's'), @@ -6763,10 +6792,6 @@ def _seg_64(): (0x1D662, 'M', u'm'), (0x1D663, 'M', u'n'), (0x1D664, 'M', u'o'), - ] - -def _seg_65(): - return [ (0x1D665, 'M', u'p'), (0x1D666, 'M', u'q'), (0x1D667, 'M', u'r'), @@ -6842,6 +6867,10 @@ def _seg_65(): (0x1D6AE, 'M', u'η'), (0x1D6AF, 'M', u'θ'), (0x1D6B0, 'M', u'ι'), + ] + +def _seg_66(): + return [ (0x1D6B1, 'M', u'κ'), (0x1D6B2, 'M', u'λ'), (0x1D6B3, 'M', u'μ'), @@ -6867,10 +6896,6 @@ def _seg_65(): (0x1D6C7, 'M', u'ζ'), (0x1D6C8, 'M', u'η'), (0x1D6C9, 'M', u'θ'), - ] - -def _seg_66(): - return [ (0x1D6CA, 'M', u'ι'), (0x1D6CB, 'M', u'κ'), (0x1D6CC, 'M', u'λ'), @@ -6946,6 +6971,10 @@ def _seg_66(): (0x1D714, 'M', u'ω'), (0x1D715, 'M', u'∂'), (0x1D716, 'M', u'ε'), + ] + +def _seg_67(): + return [ (0x1D717, 'M', u'θ'), (0x1D718, 'M', u'κ'), (0x1D719, 'M', u'φ'), @@ -6971,10 +7000,6 @@ def _seg_66(): (0x1D72D, 'M', u'θ'), (0x1D72E, 'M', u'σ'), (0x1D72F, 'M', u'τ'), - ] - -def _seg_67(): - return [ (0x1D730, 'M', u'υ'), (0x1D731, 'M', u'φ'), (0x1D732, 'M', u'χ'), @@ -7050,6 +7075,10 @@ def _seg_67(): (0x1D779, 'M', u'κ'), (0x1D77A, 'M', u'λ'), (0x1D77B, 'M', u'μ'), + ] + +def _seg_68(): + return [ (0x1D77C, 'M', u'ν'), (0x1D77D, 'M', u'ξ'), (0x1D77E, 'M', u'ο'), @@ -7075,10 +7104,6 @@ def _seg_67(): (0x1D793, 'M', u'δ'), (0x1D794, 'M', u'ε'), (0x1D795, 'M', u'ζ'), - ] - -def _seg_68(): - return [ (0x1D796, 'M', u'η'), (0x1D797, 'M', u'θ'), (0x1D798, 'M', u'ι'), @@ -7154,6 +7179,10 @@ def _seg_68(): (0x1D7E1, 'M', u'9'), (0x1D7E2, 'M', u'0'), (0x1D7E3, 'M', u'1'), + ] + +def _seg_69(): + return [ (0x1D7E4, 'M', u'2'), (0x1D7E5, 'M', u'3'), (0x1D7E6, 'M', u'4'), @@ -7179,10 +7208,6 @@ def _seg_68(): (0x1D7FA, 'M', u'4'), (0x1D7FB, 'M', u'5'), (0x1D7FC, 'M', u'6'), - ] - -def _seg_69(): - return [ (0x1D7FD, 'M', u'7'), (0x1D7FE, 'M', u'8'), (0x1D7FF, 'M', u'9'), @@ -7258,6 +7283,10 @@ def _seg_69(): (0x1E95A, 'X'), (0x1E95E, 'V'), (0x1E960, 'X'), + ] + +def _seg_70(): + return [ (0x1EC71, 'V'), (0x1ECB5, 'X'), (0x1ED01, 'V'), @@ -7283,10 +7312,6 @@ def _seg_69(): (0x1EE12, 'M', u'ق'), (0x1EE13, 'M', u'ر'), (0x1EE14, 'M', u'ش'), - ] - -def _seg_70(): - return [ (0x1EE15, 'M', u'ت'), (0x1EE16, 'M', u'ث'), (0x1EE17, 'M', u'خ'), @@ -7362,6 +7387,10 @@ def _seg_70(): (0x1EE68, 'M', u'ط'), (0x1EE69, 'M', u'ي'), (0x1EE6A, 'M', u'ك'), + ] + +def _seg_71(): + return [ (0x1EE6B, 'X'), (0x1EE6C, 'M', u'م'), (0x1EE6D, 'M', u'ن'), @@ -7387,10 +7416,6 @@ def _seg_70(): (0x1EE81, 'M', u'ب'), (0x1EE82, 'M', u'ج'), (0x1EE83, 'M', u'د'), - ] - -def _seg_71(): - return [ (0x1EE84, 'M', u'ه'), (0x1EE85, 'M', u'و'), (0x1EE86, 'M', u'ز'), @@ -7466,10 +7491,13 @@ def _seg_71(): (0x1F106, '3', u'5,'), (0x1F107, '3', u'6,'), (0x1F108, '3', u'7,'), + ] + +def _seg_72(): + return [ (0x1F109, '3', u'8,'), (0x1F10A, '3', u'9,'), (0x1F10B, 'V'), - (0x1F10D, 'X'), (0x1F110, '3', u'(a)'), (0x1F111, '3', u'(b)'), (0x1F112, '3', u'(c)'), @@ -7491,10 +7519,6 @@ def _seg_71(): (0x1F122, '3', u'(s)'), (0x1F123, '3', u'(t)'), (0x1F124, '3', u'(u)'), - ] - -def _seg_72(): - return [ (0x1F125, '3', u'(v)'), (0x1F126, '3', u'(w)'), (0x1F127, '3', u'(x)'), @@ -7542,11 +7566,10 @@ def _seg_72(): (0x1F16A, 'M', u'mc'), (0x1F16B, 'M', u'md'), (0x1F16C, 'M', u'mr'), - (0x1F16D, 'X'), - (0x1F170, 'V'), + (0x1F16D, 'V'), (0x1F190, 'M', u'dj'), (0x1F191, 'V'), - (0x1F1AD, 'X'), + (0x1F1AE, 'X'), (0x1F1E6, 'V'), (0x1F200, 'M', u'ほか'), (0x1F201, 'M', u'ココ'), @@ -7572,6 +7595,10 @@ def _seg_72(): (0x1F221, 'M', u'終'), (0x1F222, 'M', u'生'), (0x1F223, 'M', u'販'), + ] + +def _seg_73(): + return [ (0x1F224, 'M', u'声'), (0x1F225, 'M', u'吹'), (0x1F226, 'M', u'演'), @@ -7595,10 +7622,6 @@ def _seg_72(): (0x1F238, 'M', u'申'), (0x1F239, 'M', u'割'), (0x1F23A, 'M', u'営'), - ] - -def _seg_73(): - return [ (0x1F23B, 'M', u'配'), (0x1F23C, 'X'), (0x1F240, 'M', u'〔本〕'), @@ -7617,11 +7640,11 @@ def _seg_73(): (0x1F260, 'V'), (0x1F266, 'X'), (0x1F300, 'V'), - (0x1F6D6, 'X'), + (0x1F6D8, 'X'), (0x1F6E0, 'V'), (0x1F6ED, 'X'), (0x1F6F0, 'V'), - (0x1F6FB, 'X'), + (0x1F6FD, 'X'), (0x1F700, 'V'), (0x1F774, 'X'), (0x1F780, 'V'), @@ -7638,32 +7661,51 @@ def _seg_73(): (0x1F888, 'X'), (0x1F890, 'V'), (0x1F8AE, 'X'), + (0x1F8B0, 'V'), + (0x1F8B2, 'X'), (0x1F900, 'V'), - (0x1F90C, 'X'), - (0x1F90D, 'V'), - (0x1F972, 'X'), - (0x1F973, 'V'), - (0x1F977, 'X'), + (0x1F979, 'X'), (0x1F97A, 'V'), - (0x1F9A3, 'X'), - (0x1F9A5, 'V'), - (0x1F9AB, 'X'), - (0x1F9AE, 'V'), - (0x1F9CB, 'X'), + (0x1F9CC, 'X'), (0x1F9CD, 'V'), (0x1FA54, 'X'), (0x1FA60, 'V'), (0x1FA6E, 'X'), (0x1FA70, 'V'), - (0x1FA74, 'X'), + (0x1FA75, 'X'), (0x1FA78, 'V'), (0x1FA7B, 'X'), (0x1FA80, 'V'), - (0x1FA83, 'X'), + (0x1FA87, 'X'), (0x1FA90, 'V'), - (0x1FA96, 'X'), + (0x1FAA9, 'X'), + (0x1FAB0, 'V'), + (0x1FAB7, 'X'), + (0x1FAC0, 'V'), + (0x1FAC3, 'X'), + (0x1FAD0, 'V'), + (0x1FAD7, 'X'), + (0x1FB00, 'V'), + (0x1FB93, 'X'), + (0x1FB94, 'V'), + (0x1FBCB, 'X'), + (0x1FBF0, 'M', u'0'), + (0x1FBF1, 'M', u'1'), + (0x1FBF2, 'M', u'2'), + (0x1FBF3, 'M', u'3'), + (0x1FBF4, 'M', u'4'), + (0x1FBF5, 'M', u'5'), + (0x1FBF6, 'M', u'6'), + (0x1FBF7, 'M', u'7'), + (0x1FBF8, 'M', u'8'), + (0x1FBF9, 'M', u'9'), + ] + +def _seg_74(): + return [ + (0x1FBFA, 'X'), (0x20000, 'V'), - (0x2A6D7, 'X'), + (0x2A6DE, 'X'), (0x2A700, 'V'), (0x2B735, 'X'), (0x2B740, 'V'), @@ -7699,10 +7741,6 @@ def _seg_73(): (0x2F818, 'M', u'冤'), (0x2F819, 'M', u'仌'), (0x2F81A, 'M', u'冬'), - ] - -def _seg_74(): - return [ (0x2F81B, 'M', u'况'), (0x2F81C, 'M', u'𩇟'), (0x2F81D, 'M', u'凵'), @@ -7765,6 +7803,10 @@ def _seg_74(): (0x2F859, 'M', u'𡓤'), (0x2F85A, 'M', u'売'), (0x2F85B, 'M', u'壷'), + ] + +def _seg_75(): + return [ (0x2F85C, 'M', u'夆'), (0x2F85D, 'M', u'多'), (0x2F85E, 'M', u'夢'), @@ -7803,10 +7845,6 @@ def _seg_74(): (0x2F880, 'M', u'嵼'), (0x2F881, 'M', u'巡'), (0x2F882, 'M', u'巢'), - ] - -def _seg_75(): - return [ (0x2F883, 'M', u'㠯'), (0x2F884, 'M', u'巽'), (0x2F885, 'M', u'帨'), @@ -7869,6 +7907,10 @@ def _seg_75(): (0x2F8C0, 'M', u'揅'), (0x2F8C1, 'M', u'掩'), (0x2F8C2, 'M', u'㨮'), + ] + +def _seg_76(): + return [ (0x2F8C3, 'M', u'摩'), (0x2F8C4, 'M', u'摾'), (0x2F8C5, 'M', u'撝'), @@ -7907,10 +7949,6 @@ def _seg_75(): (0x2F8E6, 'M', u'椔'), (0x2F8E7, 'M', u'㮝'), (0x2F8E8, 'M', u'楂'), - ] - -def _seg_76(): - return [ (0x2F8E9, 'M', u'榣'), (0x2F8EA, 'M', u'槪'), (0x2F8EB, 'M', u'檨'), @@ -7973,6 +8011,10 @@ def _seg_76(): (0x2F924, 'M', u'犀'), (0x2F925, 'M', u'犕'), (0x2F926, 'M', u'𤜵'), + ] + +def _seg_77(): + return [ (0x2F927, 'M', u'𤠔'), (0x2F928, 'M', u'獺'), (0x2F929, 'M', u'王'), @@ -8011,10 +8053,6 @@ def _seg_76(): (0x2F94C, 'M', u'䂖'), (0x2F94D, 'M', u'𥐝'), (0x2F94E, 'M', u'硎'), - ] - -def _seg_77(): - return [ (0x2F94F, 'M', u'碌'), (0x2F950, 'M', u'磌'), (0x2F951, 'M', u'䃣'), @@ -8077,6 +8115,10 @@ def _seg_77(): (0x2F98B, 'M', u'舁'), (0x2F98C, 'M', u'舄'), (0x2F98D, 'M', u'辞'), + ] + +def _seg_78(): + return [ (0x2F98E, 'M', u'䑫'), (0x2F98F, 'M', u'芑'), (0x2F990, 'M', u'芋'), @@ -8115,10 +8157,6 @@ def _seg_77(): (0x2F9B1, 'M', u'𧃒'), (0x2F9B2, 'M', u'䕫'), (0x2F9B3, 'M', u'虐'), - ] - -def _seg_78(): - return [ (0x2F9B4, 'M', u'虜'), (0x2F9B5, 'M', u'虧'), (0x2F9B6, 'M', u'虩'), @@ -8181,6 +8219,10 @@ def _seg_78(): (0x2F9EF, 'M', u'䦕'), (0x2F9F0, 'M', u'閷'), (0x2F9F1, 'M', u'𨵷'), + ] + +def _seg_79(): + return [ (0x2F9F2, 'M', u'䧦'), (0x2F9F3, 'M', u'雃'), (0x2F9F4, 'M', u'嶲'), @@ -8219,16 +8261,14 @@ def _seg_78(): (0x2FA16, 'M', u'䵖'), (0x2FA17, 'M', u'黹'), (0x2FA18, 'M', u'黾'), - ] - -def _seg_79(): - return [ (0x2FA19, 'M', u'鼅'), (0x2FA1A, 'M', u'鼏'), (0x2FA1B, 'M', u'鼖'), (0x2FA1C, 'M', u'鼻'), (0x2FA1D, 'M', u'𪘀'), (0x2FA1E, 'X'), + (0x30000, 'V'), + (0x3134B, 'X'), (0xE0100, 'I'), (0xE01F0, 'X'), ] diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 4418fe90e8e..a0eb68d766b 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -14,7 +14,7 @@ pyparsing==2.4.7 requests==2.24.0 certifi==2020.6.20 chardet==3.0.4 - idna==2.9 + idna==2.10 urllib3==1.25.8 resolvelib==0.4.0 retrying==1.3.3 From 072b70b9bf7819e87995728b480eaa71622b16a8 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Wed, 22 Jul 2020 05:10:07 +0530 Subject: [PATCH 2300/3170] Upgrade urllib3 to 1.25.9 --- news/urllib3.vendor | 1 + src/pip/_vendor/urllib3/__init__.py | 2 +- src/pip/_vendor/urllib3/connection.py | 47 +++++++++++-------- src/pip/_vendor/urllib3/connectionpool.py | 36 ++++---------- src/pip/_vendor/urllib3/contrib/pyopenssl.py | 9 ++-- .../urllib3/contrib/securetransport.py | 5 ++ src/pip/_vendor/urllib3/exceptions.py | 19 +++++++- src/pip/_vendor/urllib3/poolmanager.py | 24 +++++++++- src/pip/_vendor/urllib3/response.py | 12 +++++ src/pip/_vendor/urllib3/util/retry.py | 3 ++ src/pip/_vendor/urllib3/util/ssl_.py | 13 +++-- src/pip/_vendor/urllib3/util/timeout.py | 5 +- src/pip/_vendor/urllib3/util/url.py | 2 +- src/pip/_vendor/vendor.txt | 2 +- 14 files changed, 122 insertions(+), 58 deletions(-) create mode 100644 news/urllib3.vendor diff --git a/news/urllib3.vendor b/news/urllib3.vendor new file mode 100644 index 00000000000..f80766b0d06 --- /dev/null +++ b/news/urllib3.vendor @@ -0,0 +1 @@ +Upgrade urllib3 to 1.25.9 diff --git a/src/pip/_vendor/urllib3/__init__.py b/src/pip/_vendor/urllib3/__init__.py index 9bd8323f91e..667e9bce9e3 100644 --- a/src/pip/_vendor/urllib3/__init__.py +++ b/src/pip/_vendor/urllib3/__init__.py @@ -22,7 +22,7 @@ __author__ = "Andrey Petrov (andrey.petrov@shazow.net)" __license__ = "MIT" -__version__ = "1.25.8" +__version__ = "1.25.9" __all__ = ( "HTTPConnectionPool", diff --git a/src/pip/_vendor/urllib3/connection.py b/src/pip/_vendor/urllib3/connection.py index 71e6790b1b9..6da1cf4b6dc 100644 --- a/src/pip/_vendor/urllib3/connection.py +++ b/src/pip/_vendor/urllib3/connection.py @@ -1,4 +1,5 @@ from __future__ import absolute_import +import re import datetime import logging import os @@ -58,6 +59,8 @@ class ConnectionError(Exception): # (ie test_recent_date is failing) update it to ~6 months before the current date. RECENT_DATE = datetime.date(2019, 1, 1) +_CONTAINS_CONTROL_CHAR_RE = re.compile(r"[^-!#$%&'*+.^_`|~0-9a-zA-Z]") + class DummyConnection(object): """Used to detect a failed ConnectionCls import.""" @@ -184,6 +187,17 @@ def connect(self): conn = self._new_conn() self._prepare_conn(conn) + def putrequest(self, method, url, *args, **kwargs): + """Send a request to the server""" + match = _CONTAINS_CONTROL_CHAR_RE.search(method) + if match: + raise ValueError( + "Method cannot contain non-token characters %r (found at least %r)" + % (method, match.group()) + ) + + return _HTTPConnection.putrequest(self, method, url, *args, **kwargs) + def request_chunked(self, method, url, body=None, headers=None): """ Alternative to the common request method, which sends the @@ -223,7 +237,12 @@ def request_chunked(self, method, url, body=None, headers=None): class HTTPSConnection(HTTPConnection): default_port = port_by_scheme["https"] + cert_reqs = None + ca_certs = None + ca_cert_dir = None + ca_cert_data = None ssl_version = None + assert_fingerprint = None def __init__( self, @@ -251,19 +270,6 @@ def __init__( # HTTPS requests to go out as HTTP. (See Issue #356) self._protocol = "https" - -class VerifiedHTTPSConnection(HTTPSConnection): - """ - Based on httplib.HTTPSConnection but wraps the socket with - SSL certification. - """ - - cert_reqs = None - ca_certs = None - ca_cert_dir = None - ssl_version = None - assert_fingerprint = None - def set_cert( self, key_file=None, @@ -274,6 +280,7 @@ def set_cert( assert_hostname=None, assert_fingerprint=None, ca_cert_dir=None, + ca_cert_data=None, ): """ This method should only be called once, before the connection is used. @@ -294,6 +301,7 @@ def set_cert( self.assert_fingerprint = assert_fingerprint self.ca_certs = ca_certs and os.path.expanduser(ca_certs) self.ca_cert_dir = ca_cert_dir and os.path.expanduser(ca_cert_dir) + self.ca_cert_data = ca_cert_data def connect(self): # Add certificate verification @@ -344,6 +352,7 @@ def connect(self): if ( not self.ca_certs and not self.ca_cert_dir + and not self.ca_cert_data and default_ssl_context and hasattr(context, "load_default_certs") ): @@ -356,6 +365,7 @@ def connect(self): key_password=self.key_password, ca_certs=self.ca_certs, ca_cert_dir=self.ca_cert_dir, + ca_cert_data=self.ca_cert_data, server_hostname=server_hostname, ssl_context=context, ) @@ -406,9 +416,8 @@ def _match_hostname(cert, asserted_hostname): raise -if ssl: - # Make a copy for testing. - UnverifiedHTTPSConnection = HTTPSConnection - HTTPSConnection = VerifiedHTTPSConnection -else: - HTTPSConnection = DummyConnection +if not ssl: + HTTPSConnection = DummyConnection # noqa: F811 + + +VerifiedHTTPSConnection = HTTPSConnection diff --git a/src/pip/_vendor/urllib3/connectionpool.py b/src/pip/_vendor/urllib3/connectionpool.py index d42eb7be673..5f044dbd90f 100644 --- a/src/pip/_vendor/urllib3/connectionpool.py +++ b/src/pip/_vendor/urllib3/connectionpool.py @@ -65,6 +65,11 @@ class ConnectionPool(object): """ Base class for all connection pools, such as :class:`.HTTPConnectionPool` and :class:`.HTTPSConnectionPool`. + + .. note:: + ConnectionPool.urlopen() does not normalize or percent-encode target URIs + which is useful if your target server doesn't support percent-encoded + target URIs. """ scheme = None @@ -760,21 +765,6 @@ def urlopen( **response_kw ) - def drain_and_release_conn(response): - try: - # discard any remaining response body, the connection will be - # released back to the pool once the entire response is read - response.read() - except ( - TimeoutError, - HTTPException, - SocketError, - ProtocolError, - BaseSSLError, - SSLError, - ): - pass - # Handle redirect? redirect_location = redirect and response.get_redirect_location() if redirect_location: @@ -785,15 +775,11 @@ def drain_and_release_conn(response): retries = retries.increment(method, url, response=response, _pool=self) except MaxRetryError: if retries.raise_on_redirect: - # Drain and release the connection for this response, since - # we're not returning it to be released manually. - drain_and_release_conn(response) + response.drain_conn() raise return response - # drain and return the connection to the pool before recursing - drain_and_release_conn(response) - + response.drain_conn() retries.sleep_for_retry(response) log.debug("Redirecting %s -> %s", url, redirect_location) return self.urlopen( @@ -819,15 +805,11 @@ def drain_and_release_conn(response): retries = retries.increment(method, url, response=response, _pool=self) except MaxRetryError: if retries.raise_on_status: - # Drain and release the connection for this response, since - # we're not returning it to be released manually. - drain_and_release_conn(response) + response.drain_conn() raise return response - # drain and return the connection to the pool before recursing - drain_and_release_conn(response) - + response.drain_conn() retries.sleep(response) log.debug("Retry: %s", url) return self.urlopen( diff --git a/src/pip/_vendor/urllib3/contrib/pyopenssl.py b/src/pip/_vendor/urllib3/contrib/pyopenssl.py index fc99d34bd4c..d8fe0629c42 100644 --- a/src/pip/_vendor/urllib3/contrib/pyopenssl.py +++ b/src/pip/_vendor/urllib3/contrib/pyopenssl.py @@ -450,9 +450,12 @@ def load_verify_locations(self, cafile=None, capath=None, cadata=None): cafile = cafile.encode("utf-8") if capath is not None: capath = capath.encode("utf-8") - self._ctx.load_verify_locations(cafile, capath) - if cadata is not None: - self._ctx.load_verify_locations(BytesIO(cadata)) + try: + self._ctx.load_verify_locations(cafile, capath) + if cadata is not None: + self._ctx.load_verify_locations(BytesIO(cadata)) + except OpenSSL.SSL.Error as e: + raise ssl.SSLError("unable to load trusted certificates: %r" % e) def load_cert_chain(self, certfile, keyfile=None, password=None): self._ctx.use_certificate_chain_file(certfile) diff --git a/src/pip/_vendor/urllib3/contrib/securetransport.py b/src/pip/_vendor/urllib3/contrib/securetransport.py index 87d844afa78..a6b7e94ade5 100644 --- a/src/pip/_vendor/urllib3/contrib/securetransport.py +++ b/src/pip/_vendor/urllib3/contrib/securetransport.py @@ -819,6 +819,11 @@ def load_verify_locations(self, cafile=None, capath=None, cadata=None): if capath is not None: raise ValueError("SecureTransport does not support cert directories") + # Raise if cafile does not exist. + if cafile is not None: + with open(cafile): + pass + self._trust_bundle = cafile or cadata def load_cert_chain(self, certfile, keyfile=None, password=None): diff --git a/src/pip/_vendor/urllib3/exceptions.py b/src/pip/_vendor/urllib3/exceptions.py index 0a74c79b5ea..5cc4d8a4f17 100644 --- a/src/pip/_vendor/urllib3/exceptions.py +++ b/src/pip/_vendor/urllib3/exceptions.py @@ -45,7 +45,10 @@ class SSLError(HTTPError): class ProxyError(HTTPError): "Raised when the connection to a proxy fails." - pass + + def __init__(self, message, error, *args): + super(ProxyError, self).__init__(message, error, *args) + self.original_error = error class DecodeError(HTTPError): @@ -195,6 +198,20 @@ class DependencyWarning(HTTPWarning): pass +class InvalidProxyConfigurationWarning(HTTPWarning): + """ + Warned when using an HTTPS proxy and an HTTPS URL. Currently + urllib3 doesn't support HTTPS proxies and the proxy will be + contacted via HTTP instead. This warning can be fixed by + changing your HTTPS proxy URL into an HTTP proxy URL. + + If you encounter this warning read this: + https://github.com/urllib3/urllib3/issues/1850 + """ + + pass + + class ResponseNotChunked(ProtocolError, ValueError): "Response needs to be chunked in order to read it as chunks." pass diff --git a/src/pip/_vendor/urllib3/poolmanager.py b/src/pip/_vendor/urllib3/poolmanager.py index 242a2f8203f..e2bd3bd8dba 100644 --- a/src/pip/_vendor/urllib3/poolmanager.py +++ b/src/pip/_vendor/urllib3/poolmanager.py @@ -2,11 +2,17 @@ import collections import functools import logging +import warnings from ._collections import RecentlyUsedContainer from .connectionpool import HTTPConnectionPool, HTTPSConnectionPool from .connectionpool import port_by_scheme -from .exceptions import LocationValueError, MaxRetryError, ProxySchemeUnknown +from .exceptions import ( + LocationValueError, + MaxRetryError, + ProxySchemeUnknown, + InvalidProxyConfigurationWarning, +) from .packages import six from .packages.six.moves.urllib.parse import urljoin from .request import RequestMethods @@ -359,6 +365,7 @@ def urlopen(self, method, url, redirect=True, **kw): retries = retries.increment(method, url, response=response, _pool=conn) except MaxRetryError: if retries.raise_on_redirect: + response.drain_conn() raise return response @@ -366,6 +373,8 @@ def urlopen(self, method, url, redirect=True, **kw): kw["redirect"] = redirect log.info("Redirecting %s -> %s", url, redirect_location) + + response.drain_conn() return self.urlopen(method, redirect_location, **kw) @@ -452,9 +461,22 @@ def _set_proxy_headers(self, url, headers=None): headers_.update(headers) return headers_ + def _validate_proxy_scheme_url_selection(self, url_scheme): + if url_scheme == "https" and self.proxy.scheme == "https": + warnings.warn( + "Your proxy configuration specified an HTTPS scheme for the proxy. " + "Are you sure you want to use HTTPS to contact the proxy? " + "This most likely indicates an error in your configuration. " + "Read this issue for more info: " + "https://github.com/urllib3/urllib3/issues/1850", + InvalidProxyConfigurationWarning, + stacklevel=3, + ) + def urlopen(self, method, url, redirect=True, **kw): "Same as HTTP(S)ConnectionPool.urlopen, ``url`` must be absolute." u = parse_url(url) + self._validate_proxy_scheme_url_selection(u.scheme) if u.scheme == "http": # For proxied HTTPS requests, httplib sets the necessary headers diff --git a/src/pip/_vendor/urllib3/response.py b/src/pip/_vendor/urllib3/response.py index 6090a7350f9..7dc9b93caef 100644 --- a/src/pip/_vendor/urllib3/response.py +++ b/src/pip/_vendor/urllib3/response.py @@ -20,6 +20,7 @@ ResponseNotChunked, IncompleteRead, InvalidHeader, + HTTPError, ) from .packages.six import string_types as basestring, PY3 from .packages.six.moves import http_client as httplib @@ -277,6 +278,17 @@ def release_conn(self): self._pool._put_conn(self._connection) self._connection = None + def drain_conn(self): + """ + Read and discard any remaining HTTP response data in the response connection. + + Unread data in the HTTPResponse connection blocks the connection from being released back to the pool. + """ + try: + self.read() + except (HTTPError, SocketError, BaseSSLError, HTTPException): + pass + @property def data(self): # For backwords-compat with earlier urllib3 0.4 and earlier. diff --git a/src/pip/_vendor/urllib3/util/retry.py b/src/pip/_vendor/urllib3/util/retry.py index 5a049fe65e0..ee30c91b147 100644 --- a/src/pip/_vendor/urllib3/util/retry.py +++ b/src/pip/_vendor/urllib3/util/retry.py @@ -13,6 +13,7 @@ ReadTimeoutError, ResponseError, InvalidHeader, + ProxyError, ) from ..packages import six @@ -306,6 +307,8 @@ def _is_connection_error(self, err): """ Errors when we're fairly sure that the server did not receive the request, so it should be safe to retry. """ + if isinstance(err, ProxyError): + err = err.original_error return isinstance(err, ConnectTimeoutError) def _is_read_error(self, err): diff --git a/src/pip/_vendor/urllib3/util/ssl_.py b/src/pip/_vendor/urllib3/util/ssl_.py index 3f78296f656..d3b463d49f5 100644 --- a/src/pip/_vendor/urllib3/util/ssl_.py +++ b/src/pip/_vendor/urllib3/util/ssl_.py @@ -119,12 +119,15 @@ def load_cert_chain(self, certfile, keyfile): self.certfile = certfile self.keyfile = keyfile - def load_verify_locations(self, cafile=None, capath=None): + def load_verify_locations(self, cafile=None, capath=None, cadata=None): self.ca_certs = cafile if capath is not None: raise SSLError("CA directories not supported in older Pythons") + if cadata is not None: + raise SSLError("CA data not supported in older Pythons") + def set_ciphers(self, cipher_suite): self.ciphers = cipher_suite @@ -305,6 +308,7 @@ def ssl_wrap_socket( ssl_context=None, ca_cert_dir=None, key_password=None, + ca_cert_data=None, ): """ All arguments except for server_hostname, ssl_context, and ca_cert_dir have @@ -323,6 +327,9 @@ def ssl_wrap_socket( SSLContext.load_verify_locations(). :param key_password: Optional password if the keyfile is encrypted. + :param ca_cert_data: + Optional string containing CA certificates in PEM format suitable for + passing as the cadata parameter to SSLContext.load_verify_locations() """ context = ssl_context if context is None: @@ -331,9 +338,9 @@ def ssl_wrap_socket( # this code. context = create_urllib3_context(ssl_version, cert_reqs, ciphers=ciphers) - if ca_certs or ca_cert_dir: + if ca_certs or ca_cert_dir or ca_cert_data: try: - context.load_verify_locations(ca_certs, ca_cert_dir) + context.load_verify_locations(ca_certs, ca_cert_dir, ca_cert_data) except IOError as e: # Platform-specific: Python 2.7 raise SSLError(e) # Py33 raises FileNotFoundError which subclasses OSError diff --git a/src/pip/_vendor/urllib3/util/timeout.py b/src/pip/_vendor/urllib3/util/timeout.py index 9883700556e..b61fea75c50 100644 --- a/src/pip/_vendor/urllib3/util/timeout.py +++ b/src/pip/_vendor/urllib3/util/timeout.py @@ -98,7 +98,7 @@ def __init__(self, total=None, connect=_Default, read=_Default): self.total = self._validate_timeout(total, "total") self._start_connect = None - def __str__(self): + def __repr__(self): return "%s(connect=%r, read=%r, total=%r)" % ( type(self).__name__, self._connect, @@ -106,6 +106,9 @@ def __str__(self): self.total, ) + # __str__ provided for backwards compatibility + __str__ = __repr__ + @classmethod def _validate_timeout(cls, value, name): """ Check that a timeout attribute is valid. diff --git a/src/pip/_vendor/urllib3/util/url.py b/src/pip/_vendor/urllib3/util/url.py index 5f8aee629a7..0eb0b6a8cc5 100644 --- a/src/pip/_vendor/urllib3/util/url.py +++ b/src/pip/_vendor/urllib3/util/url.py @@ -18,7 +18,7 @@ SCHEME_RE = re.compile(r"^(?:[a-zA-Z][a-zA-Z0-9+-]*:|/)") URI_RE = re.compile( r"^(?:([a-zA-Z][a-zA-Z0-9+.-]*):)?" - r"(?://([^/?#]*))?" + r"(?://([^\\/?#]*))?" r"([^?#]*)" r"(?:\?([^#]*))?" r"(?:#(.*))?$", diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index a0eb68d766b..07e5f5a5941 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -15,7 +15,7 @@ requests==2.24.0 certifi==2020.6.20 chardet==3.0.4 idna==2.10 - urllib3==1.25.8 + urllib3==1.25.9 resolvelib==0.4.0 retrying==1.3.3 setuptools==44.0.0 From afa59d97f60b508d6f071995cabdc1275fe43cdd Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Wed, 22 Jul 2020 05:10:52 +0530 Subject: [PATCH 2301/3170] Upgrade six to 1.15.0 --- news/six.vendor | 1 + src/pip/_vendor/six.py | 20 +++++++++++--------- src/pip/_vendor/vendor.txt | 2 +- 3 files changed, 13 insertions(+), 10 deletions(-) create mode 100644 news/six.vendor diff --git a/news/six.vendor b/news/six.vendor new file mode 100644 index 00000000000..6c9e24900c8 --- /dev/null +++ b/news/six.vendor @@ -0,0 +1 @@ +Upgrade six to 1.15.0 diff --git a/src/pip/_vendor/six.py b/src/pip/_vendor/six.py index 5fe9f8e141e..83f69783d1a 100644 --- a/src/pip/_vendor/six.py +++ b/src/pip/_vendor/six.py @@ -29,7 +29,7 @@ import types __author__ = "Benjamin Peterson <benjamin@python.org>" -__version__ = "1.14.0" +__version__ = "1.15.0" # Useful for very coarse version differentiation. @@ -890,12 +890,11 @@ def ensure_binary(s, encoding='utf-8', errors='strict'): - `str` -> encoded to `bytes` - `bytes` -> `bytes` """ + if isinstance(s, binary_type): + return s if isinstance(s, text_type): return s.encode(encoding, errors) - elif isinstance(s, binary_type): - return s - else: - raise TypeError("not expecting type '%s'" % type(s)) + raise TypeError("not expecting type '%s'" % type(s)) def ensure_str(s, encoding='utf-8', errors='strict'): @@ -909,12 +908,15 @@ def ensure_str(s, encoding='utf-8', errors='strict'): - `str` -> `str` - `bytes` -> decoded to `str` """ - if not isinstance(s, (text_type, binary_type)): - raise TypeError("not expecting type '%s'" % type(s)) + # Optimization: Fast return for the common case. + if type(s) is str: + return s if PY2 and isinstance(s, text_type): - s = s.encode(encoding, errors) + return s.encode(encoding, errors) elif PY3 and isinstance(s, binary_type): - s = s.decode(encoding, errors) + return s.decode(encoding, errors) + elif not isinstance(s, (text_type, binary_type)): + raise TypeError("not expecting type '%s'" % type(s)) return s diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 07e5f5a5941..366f869f9fc 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -19,6 +19,6 @@ requests==2.24.0 resolvelib==0.4.0 retrying==1.3.3 setuptools==44.0.0 -six==1.14.0 +six==1.15.0 toml==0.10.0 webencodings==0.5.1 From 4272aa7980cfada541002adda4097355ecc9d0ad Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Wed, 22 Jul 2020 05:11:14 +0530 Subject: [PATCH 2302/3170] Upgrade toml to 0.10.1 --- news/toml.vendor | 1 + src/pip/_vendor/toml.py | 1039 ------------------------------ src/pip/_vendor/toml/LICENSE | 3 +- src/pip/_vendor/toml/__init__.py | 6 +- src/pip/_vendor/toml/common.py | 6 + src/pip/_vendor/toml/decoder.py | 159 ++++- src/pip/_vendor/toml/encoder.py | 72 ++- src/pip/_vendor/vendor.txt | 2 +- 8 files changed, 211 insertions(+), 1077 deletions(-) create mode 100644 news/toml.vendor delete mode 100644 src/pip/_vendor/toml.py create mode 100644 src/pip/_vendor/toml/common.py diff --git a/news/toml.vendor b/news/toml.vendor new file mode 100644 index 00000000000..401ae7a8361 --- /dev/null +++ b/news/toml.vendor @@ -0,0 +1 @@ +Upgrade toml to 0.10.1 diff --git a/src/pip/_vendor/toml.py b/src/pip/_vendor/toml.py deleted file mode 100644 index dac398837b3..00000000000 --- a/src/pip/_vendor/toml.py +++ /dev/null @@ -1,1039 +0,0 @@ -"""Python module which parses and emits TOML. - -Released under the MIT license. -""" -import re -import io -import datetime -from os import linesep -import sys - -__version__ = "0.9.6" -_spec_ = "0.4.0" - - -class TomlDecodeError(Exception): - """Base toml Exception / Error.""" - pass - - -class TomlTz(datetime.tzinfo): - def __init__(self, toml_offset): - if toml_offset == "Z": - self._raw_offset = "+00:00" - else: - self._raw_offset = toml_offset - self._sign = -1 if self._raw_offset[0] == '-' else 1 - self._hours = int(self._raw_offset[1:3]) - self._minutes = int(self._raw_offset[4:6]) - - def tzname(self, dt): - return "UTC" + self._raw_offset - - def utcoffset(self, dt): - return self._sign * datetime.timedelta(hours=self._hours, - minutes=self._minutes) - - def dst(self, dt): - return datetime.timedelta(0) - - -class InlineTableDict(object): - """Sentinel subclass of dict for inline tables.""" - - -def _get_empty_inline_table(_dict): - class DynamicInlineTableDict(_dict, InlineTableDict): - """Concrete sentinel subclass for inline tables. - It is a subclass of _dict which is passed in dynamically at load time - It is also a subclass of InlineTableDict - """ - - return DynamicInlineTableDict() - - -try: - _range = xrange -except NameError: - unicode = str - _range = range - basestring = str - unichr = chr - -try: - FNFError = FileNotFoundError -except NameError: - FNFError = IOError - - -def load(f, _dict=dict): - """Parses named file or files as toml and returns a dictionary - - Args: - f: Path to the file to open, array of files to read into single dict - or a file descriptor - _dict: (optional) Specifies the class of the returned toml dictionary - - Returns: - Parsed toml file represented as a dictionary - - Raises: - TypeError -- When f is invalid type - TomlDecodeError: Error while decoding toml - IOError / FileNotFoundError -- When an array with no valid (existing) - (Python 2 / Python 3) file paths is passed - """ - - if isinstance(f, basestring): - with io.open(f, encoding='utf-8') as ffile: - return loads(ffile.read(), _dict) - elif isinstance(f, list): - from os import path as op - from warnings import warn - if not [path for path in f if op.exists(path)]: - error_msg = "Load expects a list to contain filenames only." - error_msg += linesep - error_msg += ("The list needs to contain the path of at least one " - "existing file.") - raise FNFError(error_msg) - d = _dict() - for l in f: - if op.exists(l): - d.update(load(l)) - else: - warn("Non-existent filename in list with at least one valid " - "filename") - return d - else: - try: - return loads(f.read(), _dict) - except AttributeError: - raise TypeError("You can only load a file descriptor, filename or " - "list") - - -_groupname_re = re.compile(r'^[A-Za-z0-9_-]+$') - - -def loads(s, _dict=dict): - """Parses string as toml - - Args: - s: String to be parsed - _dict: (optional) Specifies the class of the returned toml dictionary - - Returns: - Parsed toml file represented as a dictionary - - Raises: - TypeError: When a non-string is passed - TomlDecodeError: Error while decoding toml - """ - - implicitgroups = [] - retval = _dict() - currentlevel = retval - if not isinstance(s, basestring): - raise TypeError("Expecting something like a string") - - if not isinstance(s, unicode): - s = s.decode('utf8') - - sl = list(s) - openarr = 0 - openstring = False - openstrchar = "" - multilinestr = False - arrayoftables = False - beginline = True - keygroup = False - keyname = 0 - for i, item in enumerate(sl): - if item == '\r' and sl[i + 1] == '\n': - sl[i] = ' ' - continue - if keyname: - if item == '\n': - raise TomlDecodeError("Key name found without value." - " Reached end of line.") - if openstring: - if item == openstrchar: - keyname = 2 - openstring = False - openstrchar = "" - continue - elif keyname == 1: - if item.isspace(): - keyname = 2 - continue - elif item.isalnum() or item == '_' or item == '-': - continue - elif keyname == 2 and item.isspace(): - continue - if item == '=': - keyname = 0 - else: - raise TomlDecodeError("Found invalid character in key name: '" + - item + "'. Try quoting the key name.") - if item == "'" and openstrchar != '"': - k = 1 - try: - while sl[i - k] == "'": - k += 1 - if k == 3: - break - except IndexError: - pass - if k == 3: - multilinestr = not multilinestr - openstring = multilinestr - else: - openstring = not openstring - if openstring: - openstrchar = "'" - else: - openstrchar = "" - if item == '"' and openstrchar != "'": - oddbackslash = False - k = 1 - tripquote = False - try: - while sl[i - k] == '"': - k += 1 - if k == 3: - tripquote = True - break - if k == 1 or (k == 3 and tripquote): - while sl[i - k] == '\\': - oddbackslash = not oddbackslash - k += 1 - except IndexError: - pass - if not oddbackslash: - if tripquote: - multilinestr = not multilinestr - openstring = multilinestr - else: - openstring = not openstring - if openstring: - openstrchar = '"' - else: - openstrchar = "" - if item == '#' and (not openstring and not keygroup and - not arrayoftables): - j = i - try: - while sl[j] != '\n': - sl[j] = ' ' - j += 1 - except IndexError: - break - if item == '[' and (not openstring and not keygroup and - not arrayoftables): - if beginline: - if len(sl) > i + 1 and sl[i + 1] == '[': - arrayoftables = True - else: - keygroup = True - else: - openarr += 1 - if item == ']' and not openstring: - if keygroup: - keygroup = False - elif arrayoftables: - if sl[i - 1] == ']': - arrayoftables = False - else: - openarr -= 1 - if item == '\n': - if openstring or multilinestr: - if not multilinestr: - raise TomlDecodeError("Unbalanced quotes") - if ((sl[i - 1] == "'" or sl[i - 1] == '"') and ( - sl[i - 2] == sl[i - 1])): - sl[i] = sl[i - 1] - if sl[i - 3] == sl[i - 1]: - sl[i - 3] = ' ' - elif openarr: - sl[i] = ' ' - else: - beginline = True - elif beginline and sl[i] != ' ' and sl[i] != '\t': - beginline = False - if not keygroup and not arrayoftables: - if sl[i] == '=': - raise TomlDecodeError("Found empty keyname. ") - keyname = 1 - s = ''.join(sl) - s = s.split('\n') - multikey = None - multilinestr = "" - multibackslash = False - for line in s: - if not multilinestr or multibackslash or '\n' not in multilinestr: - line = line.strip() - if line == "" and (not multikey or multibackslash): - continue - if multikey: - if multibackslash: - multilinestr += line - else: - multilinestr += line - multibackslash = False - if len(line) > 2 and (line[-1] == multilinestr[0] and - line[-2] == multilinestr[0] and - line[-3] == multilinestr[0]): - try: - value, vtype = _load_value(multilinestr, _dict) - except ValueError as err: - raise TomlDecodeError(str(err)) - currentlevel[multikey] = value - multikey = None - multilinestr = "" - else: - k = len(multilinestr) - 1 - while k > -1 and multilinestr[k] == '\\': - multibackslash = not multibackslash - k -= 1 - if multibackslash: - multilinestr = multilinestr[:-1] - else: - multilinestr += "\n" - continue - if line[0] == '[': - arrayoftables = False - if len(line) == 1: - raise TomlDecodeError("Opening key group bracket on line by " - "itself.") - if line[1] == '[': - arrayoftables = True - line = line[2:] - splitstr = ']]' - else: - line = line[1:] - splitstr = ']' - i = 1 - quotesplits = _get_split_on_quotes(line) - quoted = False - for quotesplit in quotesplits: - if not quoted and splitstr in quotesplit: - break - i += quotesplit.count(splitstr) - quoted = not quoted - line = line.split(splitstr, i) - if len(line) < i + 1 or line[-1].strip() != "": - raise TomlDecodeError("Key group not on a line by itself.") - groups = splitstr.join(line[:-1]).split('.') - i = 0 - while i < len(groups): - groups[i] = groups[i].strip() - if len(groups[i]) > 0 and (groups[i][0] == '"' or - groups[i][0] == "'"): - groupstr = groups[i] - j = i + 1 - while not groupstr[0] == groupstr[-1]: - j += 1 - if j > len(groups) + 2: - raise TomlDecodeError("Invalid group name '" + - groupstr + "' Something " + - "went wrong.") - groupstr = '.'.join(groups[i:j]).strip() - groups[i] = groupstr[1:-1] - groups[i + 1:j] = [] - else: - if not _groupname_re.match(groups[i]): - raise TomlDecodeError("Invalid group name '" + - groups[i] + "'. Try quoting it.") - i += 1 - currentlevel = retval - for i in _range(len(groups)): - group = groups[i] - if group == "": - raise TomlDecodeError("Can't have a keygroup with an empty " - "name") - try: - currentlevel[group] - if i == len(groups) - 1: - if group in implicitgroups: - implicitgroups.remove(group) - if arrayoftables: - raise TomlDecodeError("An implicitly defined " - "table can't be an array") - elif arrayoftables: - currentlevel[group].append(_dict()) - else: - raise TomlDecodeError("What? " + group + - " already exists?" + - str(currentlevel)) - except TypeError: - currentlevel = currentlevel[-1] - try: - currentlevel[group] - except KeyError: - currentlevel[group] = _dict() - if i == len(groups) - 1 and arrayoftables: - currentlevel[group] = [_dict()] - except KeyError: - if i != len(groups) - 1: - implicitgroups.append(group) - currentlevel[group] = _dict() - if i == len(groups) - 1 and arrayoftables: - currentlevel[group] = [_dict()] - currentlevel = currentlevel[group] - if arrayoftables: - try: - currentlevel = currentlevel[-1] - except KeyError: - pass - elif line[0] == "{": - if line[-1] != "}": - raise TomlDecodeError("Line breaks are not allowed in inline" - "objects") - try: - _load_inline_object(line, currentlevel, _dict, multikey, - multibackslash) - except ValueError as err: - raise TomlDecodeError(str(err)) - elif "=" in line: - try: - ret = _load_line(line, currentlevel, _dict, multikey, - multibackslash) - except ValueError as err: - raise TomlDecodeError(str(err)) - if ret is not None: - multikey, multilinestr, multibackslash = ret - return retval - - -def _load_inline_object(line, currentlevel, _dict, multikey=False, - multibackslash=False): - candidate_groups = line[1:-1].split(",") - groups = [] - if len(candidate_groups) == 1 and not candidate_groups[0].strip(): - candidate_groups.pop() - while len(candidate_groups) > 0: - candidate_group = candidate_groups.pop(0) - try: - _, value = candidate_group.split('=', 1) - except ValueError: - raise ValueError("Invalid inline table encountered") - value = value.strip() - if ((value[0] == value[-1] and value[0] in ('"', "'")) or ( - value[0] in '-0123456789' or - value in ('true', 'false') or - (value[0] == "[" and value[-1] == "]") or - (value[0] == '{' and value[-1] == '}'))): - groups.append(candidate_group) - elif len(candidate_groups) > 0: - candidate_groups[0] = candidate_group + "," + candidate_groups[0] - else: - raise ValueError("Invalid inline table value encountered") - for group in groups: - status = _load_line(group, currentlevel, _dict, multikey, - multibackslash) - if status is not None: - break - - -# Matches a TOML number, which allows underscores for readability -_number_with_underscores = re.compile('([0-9])(_([0-9]))*') - - -def _strictly_valid_num(n): - n = n.strip() - if not n: - return False - if n[0] == '_': - return False - if n[-1] == '_': - return False - if "_." in n or "._" in n: - return False - if len(n) == 1: - return True - if n[0] == '0' and n[1] != '.': - return False - if n[0] == '+' or n[0] == '-': - n = n[1:] - if n[0] == '0' and n[1] != '.': - return False - if '__' in n: - return False - return True - - -def _get_split_on_quotes(line): - doublequotesplits = line.split('"') - quoted = False - quotesplits = [] - if len(doublequotesplits) > 1 and "'" in doublequotesplits[0]: - singlequotesplits = doublequotesplits[0].split("'") - doublequotesplits = doublequotesplits[1:] - while len(singlequotesplits) % 2 == 0 and len(doublequotesplits): - singlequotesplits[-1] += '"' + doublequotesplits[0] - doublequotesplits = doublequotesplits[1:] - if "'" in singlequotesplits[-1]: - singlequotesplits = (singlequotesplits[:-1] + - singlequotesplits[-1].split("'")) - quotesplits += singlequotesplits - for doublequotesplit in doublequotesplits: - if quoted: - quotesplits.append(doublequotesplit) - else: - quotesplits += doublequotesplit.split("'") - quoted = not quoted - return quotesplits - - -def _load_line(line, currentlevel, _dict, multikey, multibackslash): - i = 1 - quotesplits = _get_split_on_quotes(line) - quoted = False - for quotesplit in quotesplits: - if not quoted and '=' in quotesplit: - break - i += quotesplit.count('=') - quoted = not quoted - pair = line.split('=', i) - strictly_valid = _strictly_valid_num(pair[-1]) - if _number_with_underscores.match(pair[-1]): - pair[-1] = pair[-1].replace('_', '') - while len(pair[-1]) and (pair[-1][0] != ' ' and pair[-1][0] != '\t' and - pair[-1][0] != "'" and pair[-1][0] != '"' and - pair[-1][0] != '[' and pair[-1][0] != '{' and - pair[-1] != 'true' and pair[-1] != 'false'): - try: - float(pair[-1]) - break - except ValueError: - pass - if _load_date(pair[-1]) is not None: - break - i += 1 - prev_val = pair[-1] - pair = line.split('=', i) - if prev_val == pair[-1]: - raise ValueError("Invalid date or number") - if strictly_valid: - strictly_valid = _strictly_valid_num(pair[-1]) - pair = ['='.join(pair[:-1]).strip(), pair[-1].strip()] - if (pair[0][0] == '"' or pair[0][0] == "'") and \ - (pair[0][-1] == '"' or pair[0][-1] == "'"): - pair[0] = pair[0][1:-1] - if len(pair[1]) > 2 and ((pair[1][0] == '"' or pair[1][0] == "'") and - pair[1][1] == pair[1][0] and - pair[1][2] == pair[1][0] and - not (len(pair[1]) > 5 and - pair[1][-1] == pair[1][0] and - pair[1][-2] == pair[1][0] and - pair[1][-3] == pair[1][0])): - k = len(pair[1]) - 1 - while k > -1 and pair[1][k] == '\\': - multibackslash = not multibackslash - k -= 1 - if multibackslash: - multilinestr = pair[1][:-1] - else: - multilinestr = pair[1] + "\n" - multikey = pair[0] - else: - value, vtype = _load_value(pair[1], _dict, strictly_valid) - try: - currentlevel[pair[0]] - raise ValueError("Duplicate keys!") - except KeyError: - if multikey: - return multikey, multilinestr, multibackslash - else: - currentlevel[pair[0]] = value - - -def _load_date(val): - microsecond = 0 - tz = None - try: - if len(val) > 19: - if val[19] == '.': - if val[-1].upper() == 'Z': - subsecondval = val[20:-1] - tzval = "Z" - else: - subsecondvalandtz = val[20:] - if '+' in subsecondvalandtz: - splitpoint = subsecondvalandtz.index('+') - subsecondval = subsecondvalandtz[:splitpoint] - tzval = subsecondvalandtz[splitpoint:] - elif '-' in subsecondvalandtz: - splitpoint = subsecondvalandtz.index('-') - subsecondval = subsecondvalandtz[:splitpoint] - tzval = subsecondvalandtz[splitpoint:] - tz = TomlTz(tzval) - microsecond = int(int(subsecondval) * - (10 ** (6 - len(subsecondval)))) - else: - tz = TomlTz(val[19:]) - except ValueError: - tz = None - if "-" not in val[1:]: - return None - try: - d = datetime.datetime( - int(val[:4]), int(val[5:7]), - int(val[8:10]), int(val[11:13]), - int(val[14:16]), int(val[17:19]), microsecond, tz) - except ValueError: - return None - return d - - -def _load_unicode_escapes(v, hexbytes, prefix): - skip = False - i = len(v) - 1 - while i > -1 and v[i] == '\\': - skip = not skip - i -= 1 - for hx in hexbytes: - if skip: - skip = False - i = len(hx) - 1 - while i > -1 and hx[i] == '\\': - skip = not skip - i -= 1 - v += prefix - v += hx - continue - hxb = "" - i = 0 - hxblen = 4 - if prefix == "\\U": - hxblen = 8 - hxb = ''.join(hx[i:i + hxblen]).lower() - if hxb.strip('0123456789abcdef'): - raise ValueError("Invalid escape sequence: " + hxb) - if hxb[0] == "d" and hxb[1].strip('01234567'): - raise ValueError("Invalid escape sequence: " + hxb + - ". Only scalar unicode points are allowed.") - v += unichr(int(hxb, 16)) - v += unicode(hx[len(hxb):]) - return v - - -# Unescape TOML string values. - -# content after the \ -_escapes = ['0', 'b', 'f', 'n', 'r', 't', '"'] -# What it should be replaced by -_escapedchars = ['\0', '\b', '\f', '\n', '\r', '\t', '\"'] -# Used for substitution -_escape_to_escapedchars = dict(zip(_escapes, _escapedchars)) - - -def _unescape(v): - """Unescape characters in a TOML string.""" - i = 0 - backslash = False - while i < len(v): - if backslash: - backslash = False - if v[i] in _escapes: - v = v[:i - 1] + _escape_to_escapedchars[v[i]] + v[i + 1:] - elif v[i] == '\\': - v = v[:i - 1] + v[i:] - elif v[i] == 'u' or v[i] == 'U': - i += 1 - else: - raise ValueError("Reserved escape sequence used") - continue - elif v[i] == '\\': - backslash = True - i += 1 - return v - - -def _load_value(v, _dict, strictly_valid=True): - if not v: - raise ValueError("Empty value is invalid") - if v == 'true': - return (True, "bool") - elif v == 'false': - return (False, "bool") - elif v[0] == '"': - testv = v[1:].split('"') - triplequote = False - triplequotecount = 0 - if len(testv) > 1 and testv[0] == '' and testv[1] == '': - testv = testv[2:] - triplequote = True - closed = False - for tv in testv: - if tv == '': - if triplequote: - triplequotecount += 1 - else: - closed = True - else: - oddbackslash = False - try: - i = -1 - j = tv[i] - while j == '\\': - oddbackslash = not oddbackslash - i -= 1 - j = tv[i] - except IndexError: - pass - if not oddbackslash: - if closed: - raise ValueError("Stuff after closed string. WTF?") - else: - if not triplequote or triplequotecount > 1: - closed = True - else: - triplequotecount = 0 - escapeseqs = v.split('\\')[1:] - backslash = False - for i in escapeseqs: - if i == '': - backslash = not backslash - else: - if i[0] not in _escapes and (i[0] != 'u' and i[0] != 'U' and - not backslash): - raise ValueError("Reserved escape sequence used") - if backslash: - backslash = False - for prefix in ["\\u", "\\U"]: - if prefix in v: - hexbytes = v.split(prefix) - v = _load_unicode_escapes(hexbytes[0], hexbytes[1:], prefix) - v = _unescape(v) - if len(v) > 1 and v[1] == '"' and (len(v) < 3 or v[1] == v[2]): - v = v[2:-2] - return (v[1:-1], "str") - elif v[0] == "'": - if v[1] == "'" and (len(v) < 3 or v[1] == v[2]): - v = v[2:-2] - return (v[1:-1], "str") - elif v[0] == '[': - return (_load_array(v, _dict), "array") - elif v[0] == '{': - inline_object = _get_empty_inline_table(_dict) - _load_inline_object(v, inline_object, _dict) - return (inline_object, "inline_object") - else: - parsed_date = _load_date(v) - if parsed_date is not None: - return (parsed_date, "date") - if not strictly_valid: - raise ValueError("Weirdness with leading zeroes or " - "underscores in your number.") - itype = "int" - neg = False - if v[0] == '-': - neg = True - v = v[1:] - elif v[0] == '+': - v = v[1:] - v = v.replace('_', '') - if '.' in v or 'e' in v or 'E' in v: - if '.' in v and v.split('.', 1)[1] == '': - raise ValueError("This float is missing digits after " - "the point") - if v[0] not in '0123456789': - raise ValueError("This float doesn't have a leading digit") - v = float(v) - itype = "float" - else: - v = int(v) - if neg: - return (0 - v, itype) - return (v, itype) - - -def _bounded_string(s): - if len(s) == 0: - return True - if s[-1] != s[0]: - return False - i = -2 - backslash = False - while len(s) + i > 0: - if s[i] == "\\": - backslash = not backslash - i -= 1 - else: - break - return not backslash - - -def _load_array(a, _dict): - atype = None - retval = [] - a = a.strip() - if '[' not in a[1:-1] or "" != a[1:-1].split('[')[0].strip(): - strarray = False - tmpa = a[1:-1].strip() - if tmpa != '' and (tmpa[0] == '"' or tmpa[0] == "'"): - strarray = True - if not a[1:-1].strip().startswith('{'): - a = a[1:-1].split(',') - else: - # a is an inline object, we must find the matching parenthesis - # to define groups - new_a = [] - start_group_index = 1 - end_group_index = 2 - in_str = False - while end_group_index < len(a[1:]): - if a[end_group_index] == '"' or a[end_group_index] == "'": - if in_str: - backslash_index = end_group_index - 1 - while (backslash_index > -1 and - a[backslash_index] == '\\'): - in_str = not in_str - backslash_index -= 1 - in_str = not in_str - if in_str or a[end_group_index] != '}': - end_group_index += 1 - continue - - # Increase end_group_index by 1 to get the closing bracket - end_group_index += 1 - new_a.append(a[start_group_index:end_group_index]) - - # The next start index is at least after the closing bracket, a - # closing bracket can be followed by a comma since we are in - # an array. - start_group_index = end_group_index + 1 - while (start_group_index < len(a[1:]) and - a[start_group_index] != '{'): - start_group_index += 1 - end_group_index = start_group_index + 1 - a = new_a - b = 0 - if strarray: - while b < len(a) - 1: - ab = a[b].strip() - while (not _bounded_string(ab) or - (len(ab) > 2 and - ab[0] == ab[1] == ab[2] and - ab[-2] != ab[0] and - ab[-3] != ab[0])): - a[b] = a[b] + ',' + a[b + 1] - ab = a[b].strip() - if b < len(a) - 2: - a = a[:b + 1] + a[b + 2:] - else: - a = a[:b + 1] - b += 1 - else: - al = list(a[1:-1]) - a = [] - openarr = 0 - j = 0 - for i in _range(len(al)): - if al[i] == '[': - openarr += 1 - elif al[i] == ']': - openarr -= 1 - elif al[i] == ',' and not openarr: - a.append(''.join(al[j:i])) - j = i + 1 - a.append(''.join(al[j:])) - for i in _range(len(a)): - a[i] = a[i].strip() - if a[i] != '': - nval, ntype = _load_value(a[i], _dict) - if atype: - if ntype != atype: - raise ValueError("Not a homogeneous array") - else: - atype = ntype - retval.append(nval) - return retval - - -def dump(o, f): - """Writes out dict as toml to a file - - Args: - o: Object to dump into toml - f: File descriptor where the toml should be stored - - Returns: - String containing the toml corresponding to dictionary - - Raises: - TypeError: When anything other than file descriptor is passed - """ - - if not f.write: - raise TypeError("You can only dump an object to a file descriptor") - d = dumps(o) - f.write(d) - return d - - -def dumps(o, preserve=False): - """Stringifies input dict as toml - - Args: - o: Object to dump into toml - - preserve: Boolean parameter. If true, preserve inline tables. - - Returns: - String containing the toml corresponding to dict - """ - - retval = "" - addtoretval, sections = _dump_sections(o, "") - retval += addtoretval - while sections != {}: - newsections = {} - for section in sections: - addtoretval, addtosections = _dump_sections(sections[section], - section, preserve) - if addtoretval or (not addtoretval and not addtosections): - if retval and retval[-2:] != "\n\n": - retval += "\n" - retval += "[" + section + "]\n" - if addtoretval: - retval += addtoretval - for s in addtosections: - newsections[section + "." + s] = addtosections[s] - sections = newsections - return retval - - -def _dump_sections(o, sup, preserve=False): - retstr = "" - if sup != "" and sup[-1] != ".": - sup += '.' - retdict = o.__class__() - arraystr = "" - for section in o: - section = unicode(section) - qsection = section - if not re.match(r'^[A-Za-z0-9_-]+$', section): - if '"' in section: - qsection = "'" + section + "'" - else: - qsection = '"' + section + '"' - if not isinstance(o[section], dict): - arrayoftables = False - if isinstance(o[section], list): - for a in o[section]: - if isinstance(a, dict): - arrayoftables = True - if arrayoftables: - for a in o[section]: - arraytabstr = "\n" - arraystr += "[[" + sup + qsection + "]]\n" - s, d = _dump_sections(a, sup + qsection) - if s: - if s[0] == "[": - arraytabstr += s - else: - arraystr += s - while d != {}: - newd = {} - for dsec in d: - s1, d1 = _dump_sections(d[dsec], sup + qsection + - "." + dsec) - if s1: - arraytabstr += ("[" + sup + qsection + "." + - dsec + "]\n") - arraytabstr += s1 - for s1 in d1: - newd[dsec + "." + s1] = d1[s1] - d = newd - arraystr += arraytabstr - else: - if o[section] is not None: - retstr += (qsection + " = " + - unicode(_dump_value(o[section])) + '\n') - elif preserve and isinstance(o[section], InlineTableDict): - retstr += (qsection + " = " + _dump_inline_table(o[section])) - else: - retdict[qsection] = o[section] - retstr += arraystr - return (retstr, retdict) - - -def _dump_inline_table(section): - """Preserve inline table in its compact syntax instead of expanding - into subsection. - - https://github.com/toml-lang/toml#user-content-inline-table - """ - retval = "" - if isinstance(section, dict): - val_list = [] - for k, v in section.items(): - val = _dump_inline_table(v) - val_list.append(k + " = " + val) - retval += "{ " + ", ".join(val_list) + " }\n" - return retval - else: - return unicode(_dump_value(section)) - - -def _dump_value(v): - dump_funcs = { - str: _dump_str, - unicode: _dump_str, - list: _dump_list, - int: lambda v: v, - bool: lambda v: unicode(v).lower(), - float: _dump_float, - datetime.datetime: lambda v: v.isoformat().replace('+00:00', 'Z'), - } - # Lookup function corresponding to v's type - dump_fn = dump_funcs.get(type(v)) - if dump_fn is None and hasattr(v, '__iter__'): - dump_fn = dump_funcs[list] - # Evaluate function (if it exists) else return v - return dump_fn(v) if dump_fn is not None else dump_funcs[str](v) - - -def _dump_str(v): - if sys.version_info < (3,) and hasattr(v, 'decode') and isinstance(v, str): - v = v.decode('utf-8') - v = "%r" % v - if v[0] == 'u': - v = v[1:] - singlequote = v.startswith("'") - if singlequote or v.startswith('"'): - v = v[1:-1] - if singlequote: - v = v.replace("\\'", "'") - v = v.replace('"', '\\"') - v = v.split("\\x") - while len(v) > 1: - i = -1 - if not v[0]: - v = v[1:] - v[0] = v[0].replace("\\\\", "\\") - # No, I don't know why != works and == breaks - joinx = v[0][i] != "\\" - while v[0][:i] and v[0][i] == "\\": - joinx = not joinx - i -= 1 - if joinx: - joiner = "x" - else: - joiner = "u00" - v = [v[0] + joiner + v[1]] + v[2:] - return unicode('"' + v[0] + '"') - - -def _dump_list(v): - retval = "[" - for u in v: - retval += " " + unicode(_dump_value(u)) + "," - retval += "]" - return retval - - -def _dump_float(v): - return "{0:.16}".format(v).replace("e+0", "e+").replace("e-0", "e-") diff --git a/src/pip/_vendor/toml/LICENSE b/src/pip/_vendor/toml/LICENSE index 08e981ffacf..5010e3075e6 100644 --- a/src/pip/_vendor/toml/LICENSE +++ b/src/pip/_vendor/toml/LICENSE @@ -1,11 +1,12 @@ The MIT License -Copyright 2013-2018 William Pearson +Copyright 2013-2019 William Pearson Copyright 2015-2016 Julien Enselme Copyright 2016 Google Inc. Copyright 2017 Samuel Vasko Copyright 2017 Nate Prewitt Copyright 2017 Jack Evans +Copyright 2019 Filippo Broggini Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/pip/_vendor/toml/__init__.py b/src/pip/_vendor/toml/__init__.py index 015d73cbe4e..7a08fe72540 100644 --- a/src/pip/_vendor/toml/__init__.py +++ b/src/pip/_vendor/toml/__init__.py @@ -6,16 +6,20 @@ from pip._vendor.toml import encoder from pip._vendor.toml import decoder -__version__ = "0.10.0" +__version__ = "0.10.1" _spec_ = "0.5.0" load = decoder.load loads = decoder.loads TomlDecoder = decoder.TomlDecoder TomlDecodeError = decoder.TomlDecodeError +TomlPreserveCommentDecoder = decoder.TomlPreserveCommentDecoder dump = encoder.dump dumps = encoder.dumps TomlEncoder = encoder.TomlEncoder TomlArraySeparatorEncoder = encoder.TomlArraySeparatorEncoder TomlPreserveInlineDictEncoder = encoder.TomlPreserveInlineDictEncoder +TomlNumpyEncoder = encoder.TomlNumpyEncoder +TomlPreserveCommentEncoder = encoder.TomlPreserveCommentEncoder +TomlPathlibEncoder = encoder.TomlPathlibEncoder diff --git a/src/pip/_vendor/toml/common.py b/src/pip/_vendor/toml/common.py new file mode 100644 index 00000000000..a5d673dac5f --- /dev/null +++ b/src/pip/_vendor/toml/common.py @@ -0,0 +1,6 @@ +# content after the \ +escapes = ['0', 'b', 'f', 'n', 'r', 't', '"'] +# What it should be replaced by +escapedchars = ['\0', '\b', '\f', '\n', '\r', '\t', '\"'] +# Used for substitution +escape_to_escapedchars = dict(zip(_escapes, _escapedchars)) diff --git a/src/pip/_vendor/toml/decoder.py b/src/pip/_vendor/toml/decoder.py index 20be459122d..e4887770c3b 100644 --- a/src/pip/_vendor/toml/decoder.py +++ b/src/pip/_vendor/toml/decoder.py @@ -24,7 +24,7 @@ def _detect_pathlib_path(p): def _ispath(p): - if isinstance(p, basestring): + if isinstance(p, (bytes, basestring)): return True return _detect_pathlib_path(p) @@ -44,7 +44,7 @@ def _getpath(p): FNFError = IOError -TIME_RE = re.compile("([0-9]{2}):([0-9]{2}):([0-9]{2})(\.([0-9]{3,6}))?") +TIME_RE = re.compile(r"([0-9]{2}):([0-9]{2}):([0-9]{2})(\.([0-9]{3,6}))?") class TomlDecodeError(ValueError): @@ -66,6 +66,27 @@ def __init__(self, msg, doc, pos): _number_with_underscores = re.compile('([0-9])(_([0-9]))*') +class CommentValue(object): + def __init__(self, val, comment, beginline, _dict): + self.val = val + separator = "\n" if beginline else " " + self.comment = separator + comment + self._dict = _dict + + def __getitem__(self, key): + return self.val[key] + + def __setitem__(self, key, value): + self.val[key] = value + + def dump(self, dump_value_func): + retstr = dump_value_func(self.val) + if isinstance(self.val, self._dict): + return self.comment + "\n" + unicode(retstr) + else: + return unicode(retstr) + self.comment + + def _strictly_valid_num(n): n = n.strip() if not n: @@ -96,6 +117,7 @@ def load(f, _dict=dict, decoder=None): f: Path to the file to open, array of files to read into single dict or a file descriptor _dict: (optional) Specifies the class of the returned toml dictionary + decoder: The decoder to use Returns: Parsed toml file represented as a dictionary @@ -120,9 +142,9 @@ def load(f, _dict=dict, decoder=None): "existing file.") raise FNFError(error_msg) if decoder is None: - decoder = TomlDecoder() + decoder = TomlDecoder(_dict) d = decoder.get_empty_table() - for l in f: + for l in f: # noqa: E741 if op.exists(l): d.update(load(l, _dict, decoder)) else: @@ -177,19 +199,30 @@ def loads(s, _dict=dict, decoder=None): keygroup = False dottedkey = False keyname = 0 + key = '' + prev_key = '' + line_no = 1 + for i, item in enumerate(sl): if item == '\r' and sl[i + 1] == '\n': sl[i] = ' ' continue if keyname: + key += item if item == '\n': raise TomlDecodeError("Key name found without value." " Reached end of line.", original, i) if openstring: if item == openstrchar: - keyname = 2 - openstring = False - openstrchar = "" + oddbackslash = False + k = 1 + while i >= k and sl[i - k] == '\\': + oddbackslash = not oddbackslash + k += 1 + if not oddbackslash: + keyname = 2 + openstring = False + openstrchar = "" continue elif keyname == 1: if item.isspace(): @@ -220,6 +253,8 @@ def loads(s, _dict=dict, decoder=None): continue if item == '=': keyname = 0 + prev_key = key[:-1].rstrip() + key = '' dottedkey = False else: raise TomlDecodeError("Found invalid character in key name: '" + @@ -272,12 +307,16 @@ def loads(s, _dict=dict, decoder=None): if item == '#' and (not openstring and not keygroup and not arrayoftables): j = i + comment = "" try: while sl[j] != '\n': + comment += s[j] sl[j] = ' ' j += 1 except IndexError: break + if not openarr: + decoder.preserve_comment(line_no, prev_key, comment, beginline) if item == '[' and (not openstring and not keygroup and not arrayoftables): if beginline: @@ -308,12 +347,20 @@ def loads(s, _dict=dict, decoder=None): sl[i] = ' ' else: beginline = True + line_no += 1 elif beginline and sl[i] != ' ' and sl[i] != '\t': beginline = False if not keygroup and not arrayoftables: if sl[i] == '=': raise TomlDecodeError("Found empty keyname. ", original, i) keyname = 1 + key += item + if keyname: + raise TomlDecodeError("Key name found without value." + " Reached end of file.", original, len(s)) + if openstring: # reached EOF and have an unterminated string + raise TomlDecodeError("Unterminated string found." + " Reached end of file.", original, len(s)) s = ''.join(sl) s = s.split('\n') multikey = None @@ -323,6 +370,9 @@ def loads(s, _dict=dict, decoder=None): for idx, line in enumerate(s): if idx > 0: pos += len(s[idx - 1]) + 1 + + decoder.embed_comments(idx, currentlevel) + if not multilinestr or multibackslash or '\n' not in multilinestr: line = line.strip() if line == "" and (not multikey or multibackslash): @@ -333,9 +383,14 @@ def loads(s, _dict=dict, decoder=None): else: multilinestr += line multibackslash = False - if len(line) > 2 and (line[-1] == multilinestr[0] and - line[-2] == multilinestr[0] and - line[-3] == multilinestr[0]): + closed = False + if multilinestr[0] == '[': + closed = line[-1] == ']' + elif len(line) > 2: + closed = (line[-1] == multilinestr[0] and + line[-2] == multilinestr[0] and + line[-3] == multilinestr[0]) + if closed: try: value, vtype = decoder.load_value(multilinestr) except ValueError as err: @@ -663,7 +718,8 @@ def load_line(self, line, currentlevel, multikey, multibackslash): while len(pair[-1]) and (pair[-1][0] != ' ' and pair[-1][0] != '\t' and pair[-1][0] != "'" and pair[-1][0] != '"' and pair[-1][0] != '[' and pair[-1][0] != '{' and - pair[-1] != 'true' and pair[-1] != 'false'): + pair[-1].strip() != 'true' and + pair[-1].strip() != 'false'): try: float(pair[-1]) break @@ -671,6 +727,8 @@ def load_line(self, line, currentlevel, multikey, multibackslash): pass if _load_date(pair[-1]) is not None: break + if TIME_RE.match(pair[-1]): + break i += 1 prev_val = pair[-1] pair = line.split('=', i) @@ -704,16 +762,10 @@ def load_line(self, line, currentlevel, multikey, multibackslash): pair[0] = levels[-1].strip() elif (pair[0][0] == '"' or pair[0][0] == "'") and \ (pair[0][-1] == pair[0][0]): - pair[0] = pair[0][1:-1] - if len(pair[1]) > 2 and ((pair[1][0] == '"' or pair[1][0] == "'") and - pair[1][1] == pair[1][0] and - pair[1][2] == pair[1][0] and - not (len(pair[1]) > 5 and - pair[1][-1] == pair[1][0] and - pair[1][-2] == pair[1][0] and - pair[1][-3] == pair[1][0])): - k = len(pair[1]) - 1 - while k > -1 and pair[1][k] == '\\': + pair[0] = _unescape(pair[0][1:-1]) + k, koffset = self._load_line_multiline_str(pair[1]) + if k > -1: + while k > -1 and pair[1][k + koffset] == '\\': multibackslash = not multibackslash k -= 1 if multibackslash: @@ -734,6 +786,26 @@ def load_line(self, line, currentlevel, multikey, multibackslash): else: currentlevel[pair[0]] = value + def _load_line_multiline_str(self, p): + poffset = 0 + if len(p) < 3: + return -1, poffset + if p[0] == '[' and (p.strip()[-1] != ']' and + self._load_array_isstrarray(p)): + newp = p[1:].strip().split(',') + while len(newp) > 1 and newp[-1][0] != '"' and newp[-1][0] != "'": + newp = newp[:-2] + [newp[-2] + ',' + newp[-1]] + newp = newp[-1] + poffset = len(p) - len(newp) + p = newp + if p[0] != '"' and p[0] != "'": + return -1, poffset + if p[1] != p[0] or p[2] != p[0]: + return -1, poffset + if len(p) > 5 and p[-1] == p[0] and p[-2] == p[0] and p[-3] == p[0]: + return -1, poffset + return len(p) - 1, poffset + def load_value(self, v, strictly_valid=True): if not v: raise ValueError("Empty value is invalid") @@ -769,7 +841,8 @@ def load_value(self, v, strictly_valid=True): pass if not oddbackslash: if closed: - raise ValueError("Stuff after closed string. WTF?") + raise ValueError("Found tokens after a closed " + + "string. Invalid TOML.") else: if not triplequote or triplequotecount > 1: closed = True @@ -857,15 +930,18 @@ def bounded_string(self, s): break return not backslash + def _load_array_isstrarray(self, a): + a = a[1:-1].strip() + if a != '' and (a[0] == '"' or a[0] == "'"): + return True + return False + def load_array(self, a): atype = None retval = [] a = a.strip() if '[' not in a[1:-1] or "" != a[1:-1].split('[')[0].strip(): - strarray = False - tmpa = a[1:-1].strip() - if tmpa != '' and (tmpa[0] == '"' or tmpa[0] == "'"): - strarray = True + strarray = self._load_array_isstrarray(a) if not a[1:-1].strip().startswith('{'): a = a[1:-1].split(',') else: @@ -874,6 +950,7 @@ def load_array(self, a): new_a = [] start_group_index = 1 end_group_index = 2 + open_bracket_count = 1 if a[start_group_index] == '{' else 0 in_str = False while end_group_index < len(a[1:]): if a[end_group_index] == '"' or a[end_group_index] == "'": @@ -884,9 +961,15 @@ def load_array(self, a): in_str = not in_str backslash_index -= 1 in_str = not in_str + if not in_str and a[end_group_index] == '{': + open_bracket_count += 1 if in_str or a[end_group_index] != '}': end_group_index += 1 continue + elif a[end_group_index] == '}' and open_bracket_count > 1: + open_bracket_count -= 1 + end_group_index += 1 + continue # Increase end_group_index by 1 to get the closing bracket end_group_index += 1 @@ -943,3 +1026,27 @@ def load_array(self, a): atype = ntype retval.append(nval) return retval + + def preserve_comment(self, line_no, key, comment, beginline): + pass + + def embed_comments(self, idx, currentlevel): + pass + + +class TomlPreserveCommentDecoder(TomlDecoder): + + def __init__(self, _dict=dict): + self.saved_comments = {} + super(TomlPreserveCommentDecoder, self).__init__(_dict) + + def preserve_comment(self, line_no, key, comment, beginline): + self.saved_comments[line_no] = (key, comment, beginline) + + def embed_comments(self, idx, currentlevel): + if idx not in self.saved_comments: + return + + key, comment, beginline = self.saved_comments[idx] + currentlevel[key] = CommentValue(currentlevel[key], comment, beginline, + self._dict) diff --git a/src/pip/_vendor/toml/encoder.py b/src/pip/_vendor/toml/encoder.py index 53b0bd5ace5..a8b03c7bea8 100644 --- a/src/pip/_vendor/toml/encoder.py +++ b/src/pip/_vendor/toml/encoder.py @@ -1,6 +1,7 @@ import datetime import re import sys +from decimal import Decimal from pip._vendor.toml.decoder import InlineTableDict @@ -8,12 +9,13 @@ unicode = str -def dump(o, f): +def dump(o, f, encoder=None): """Writes out dict as toml to a file Args: o: Object to dump into toml f: File descriptor where the toml should be stored + encoder: The ``TomlEncoder`` to use for constructing the output string Returns: String containing the toml corresponding to dictionary @@ -24,7 +26,7 @@ def dump(o, f): if not f.write: raise TypeError("You can only dump an object to a file descriptor") - d = dumps(o) + d = dumps(o, encoder=encoder) f.write(d) return d @@ -34,11 +36,22 @@ def dumps(o, encoder=None): Args: o: Object to dump into toml - - preserve: Boolean parameter. If true, preserve inline tables. + encoder: The ``TomlEncoder`` to use for constructing the output string Returns: String containing the toml corresponding to dict + + Examples: + ```python + >>> import toml + >>> output = { + ... 'a': "I'm a string", + ... 'b': ["I'm", "a", "list"], + ... 'c': 2400 + ... } + >>> toml.dumps(output) + 'a = "I\'m a string"\nb = [ "I\'m", "a", "list",]\nc = 2400\n' + ``` """ retval = "" @@ -46,7 +59,13 @@ def dumps(o, encoder=None): encoder = TomlEncoder(o.__class__) addtoretval, sections = encoder.dump_sections(o, "") retval += addtoretval + outer_objs = [id(o)] while sections: + section_ids = [id(section) for section in sections] + for outer_obj in outer_objs: + if outer_obj in section_ids: + raise ValueError("Circular reference detected") + outer_objs += section_ids newsections = encoder.get_empty_table() for section in sections: addtoretval, addtosections = encoder.dump_sections( @@ -96,7 +115,7 @@ def _dump_str(v): def _dump_float(v): - return "{0:.16}".format(v).replace("e+0", "e+").replace("e-0", "e-") + return "{}".format(v).replace("e+0", "e+").replace("e-0", "e-") def _dump_time(v): @@ -119,6 +138,7 @@ def __init__(self, _dict=dict, preserve=False): bool: lambda v: unicode(v).lower(), int: lambda v: v, float: _dump_float, + Decimal: _dump_float, datetime.datetime: lambda v: v.isoformat().replace('+00:00', 'Z'), datetime.time: _dump_time, datetime.date: lambda v: v.isoformat() @@ -169,10 +189,7 @@ def dump_sections(self, o, sup): section = unicode(section) qsection = section if not re.match(r'^[A-Za-z0-9_-]+$', section): - if '"' in section: - qsection = "'" + section + "'" - else: - qsection = '"' + section + '"' + qsection = _dump_str(section) if not isinstance(o[section], dict): arrayoftables = False if isinstance(o[section], list): @@ -248,3 +265,40 @@ def dump_list(self, v): t = s retval += "]" return retval + + +class TomlNumpyEncoder(TomlEncoder): + + def __init__(self, _dict=dict, preserve=False): + import numpy as np + super(TomlNumpyEncoder, self).__init__(_dict, preserve) + self.dump_funcs[np.float16] = _dump_float + self.dump_funcs[np.float32] = _dump_float + self.dump_funcs[np.float64] = _dump_float + self.dump_funcs[np.int16] = self._dump_int + self.dump_funcs[np.int32] = self._dump_int + self.dump_funcs[np.int64] = self._dump_int + + def _dump_int(self, v): + return "{}".format(int(v)) + + +class TomlPreserveCommentEncoder(TomlEncoder): + + def __init__(self, _dict=dict, preserve=False): + from pip._vendor.toml.decoder import CommentValue + super(TomlPreserveCommentEncoder, self).__init__(_dict, preserve) + self.dump_funcs[CommentValue] = lambda v: v.dump(self.dump_value) + + +class TomlPathlibEncoder(TomlEncoder): + + def _dump_pathlib_path(self, v): + return _dump_str(str(v)) + + def dump_value(self, v): + if (3, 4) <= sys.version_info: + import pathlib + if isinstance(v, pathlib.PurePath): + v = str(v) + return super(TomlPathlibEncoder, self).dump_value(v) diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 366f869f9fc..90d05a3a001 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -20,5 +20,5 @@ resolvelib==0.4.0 retrying==1.3.3 setuptools==44.0.0 six==1.15.0 -toml==0.10.0 +toml==0.10.1 webencodings==0.5.1 From f94580bad7e0d6df6bb92b34d153ade658d59c01 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Wed, 22 Jul 2020 11:25:24 +0530 Subject: [PATCH 2303/3170] Print output in test_debug__library_versions This should make debugging issues in this test easier. --- tests/functional/test_debug.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/functional/test_debug.py b/tests/functional/test_debug.py index cf7f71729c1..f309604df58 100644 --- a/tests/functional/test_debug.py +++ b/tests/functional/test_debug.py @@ -36,10 +36,11 @@ def test_debug__library_versions(script): """ args = ['debug'] result = script.pip(*args, allow_stderr_warning=True) - stdout = result.stdout + print(result.stdout) + vendored_versions = create_vendor_txt_map() for name, value in vendored_versions.items(): - assert '{}=={}'.format(name, value) in stdout + assert '{}=={}'.format(name, value) in result.stdout @pytest.mark.parametrize( From dcfea6ee8f0cb6d724cd43352286e67e170b07f2 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Wed, 22 Jul 2020 17:05:57 +0530 Subject: [PATCH 2304/3170] Really? Wow. This is a fix for the sole failing test in the CI for these changes (in tests/functional/test_debug.py::test_debug__library_versions). The failure took me a fair bit of time to diagnose, but it looks like the issue is that we're strictly comparing versions as strings. This is a bad idea when they're not normalized. --- src/pip/_vendor/vendor.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 90d05a3a001..06fa1358f00 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -12,7 +12,7 @@ pep517==0.8.2 progress==1.5 pyparsing==2.4.7 requests==2.24.0 - certifi==2020.6.20 + certifi==2020.06.20 chardet==3.0.4 idna==2.10 urllib3==1.25.9 From 3c20d5bfbdd6d68d84a644918f4557420f814427 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Thu, 23 Jul 2020 22:27:10 +0530 Subject: [PATCH 2305/3170] Reject setup.py projects that don't generate .egg-info This finalizes a deprecation, since we've not recieved any reports from users in responses to the deprecation. --- .../_internal/operations/install/legacy.py | 26 +++++-------------- 1 file changed, 7 insertions(+), 19 deletions(-) diff --git a/src/pip/_internal/operations/install/legacy.py b/src/pip/_internal/operations/install/legacy.py index 0fac90573db..87227d5fed6 100644 --- a/src/pip/_internal/operations/install/legacy.py +++ b/src/pip/_internal/operations/install/legacy.py @@ -6,7 +6,7 @@ import sys from distutils.util import change_root -from pip._internal.utils.deprecation import deprecated +from pip._internal.exceptions import InstallationError from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import ensure_dir from pip._internal.utils.setuptools_build import make_setuptools_install_args @@ -106,24 +106,12 @@ def prepend_root(path): egg_info_dir = prepend_root(directory) break else: - deprecated( - reason=( - "{} did not indicate that it installed an " - ".egg-info directory. Only setup.py projects " - "generating .egg-info directories are supported." - ).format(req_description), - replacement=( - "for maintainers: updating the setup.py of {0}. " - "For users: contact the maintainers of {0} to let " - "them know to update their setup.py.".format( - req_name - ) - ), - gone_in="20.2", - issue=6998, - ) - # FIXME: put the record somewhere - return True + message = ( + "{} did not indicate that it installed an " + ".egg-info directory. Only setup.py projects " + "generating .egg-info directories are supported." + ).format(req_description) + raise InstallationError(message) new_lines = [] for line in record_lines: From d34b099bf4d63a472274adabb5917f7a8ea3aa6c Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Thu, 23 Jul 2020 22:44:17 +0530 Subject: [PATCH 2306/3170] :newspaper: --- news/6998.removal | 1 + news/8617.removal | 1 + 2 files changed, 2 insertions(+) create mode 100644 news/6998.removal create mode 100644 news/8617.removal diff --git a/news/6998.removal b/news/6998.removal new file mode 100644 index 00000000000..7c38a48fd11 --- /dev/null +++ b/news/6998.removal @@ -0,0 +1 @@ +Deprecate setup.py-based builds that do not generate an ``.egg-info`` directory. diff --git a/news/8617.removal b/news/8617.removal new file mode 100644 index 00000000000..7c38a48fd11 --- /dev/null +++ b/news/8617.removal @@ -0,0 +1 @@ +Deprecate setup.py-based builds that do not generate an ``.egg-info`` directory. From 38fe3c2f149ae7664a637dfd49ff7fac1ff12243 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sat, 25 Jul 2020 11:11:36 +0200 Subject: [PATCH 2307/3170] Percolate --use-feature from req file upwards We explicitly propagate --use-feature options from req files upwards. This is not strictly necessary for the option to be enabled, because of the default value is a global list, but that implicit behaviour is certainly accidental, so we make it explicit, with a test. --- src/pip/_internal/req/req_file.py | 14 ++++++++++---- tests/unit/test_req_file.py | 8 ++++++-- 2 files changed, 16 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index f991cd32d92..1050582289a 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -225,12 +225,18 @@ def handle_option_line( ): # type: (...) -> None - # percolate hash-checking option upward - if options and opts.require_hashes: - options.require_hashes = opts.require_hashes + if options: + # percolate options upward + if opts.require_hashes: + options.require_hashes = opts.require_hashes + if opts.features_enabled: + options.features_enabled.extend( + f for f in opts.features_enabled + if f not in options.features_enabled + ) # set finder options - elif finder: + if finder: find_links = finder.find_links index_urls = finder.index_urls if opts.index_url: diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py index b7c3218510c..879f088a41d 100644 --- a/tests/unit/test_req_file.py +++ b/tests/unit/test_req_file.py @@ -47,6 +47,7 @@ def options(session): isolated_mode=False, index_url='default_url', format_control=FormatControl(set(), set()), + features_enabled=[], ) @@ -382,9 +383,12 @@ def test_set_finder_allow_all_prereleases(self, line_processor, finder): line_processor("--pre", "file", 1, finder=finder) assert finder.allow_all_prereleases - def test_use_feature(self, line_processor): + def test_use_feature(self, line_processor, options): """--use-feature can be set in requirements files.""" - line_processor("--use-feature=2020-resolver", "filename", 1) + line_processor( + "--use-feature=2020-resolver", "filename", 1, options=options + ) + assert "2020-resolver" in options.features_enabled def test_relative_local_find_links( self, line_processor, finder, monkeypatch, tmpdir From ccdfa74d79834bd315bc9e07a09dcc55d4476591 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sat, 25 Jul 2020 13:02:20 +0200 Subject: [PATCH 2308/3170] Reduce reliance on .egg-info directories in test suite Make more tests run with with_wheel and test that a .dist-info directory is created. --- tests/functional/test_install.py | 182 ++++++++++++----------- tests/functional/test_install_index.py | 30 ++-- tests/functional/test_install_reqs.py | 24 +-- tests/functional/test_install_upgrade.py | 65 ++++---- tests/functional/test_install_user.py | 4 + tests/functional/test_install_vcs_git.py | 8 +- tests/functional/test_install_wheel.py | 4 +- tests/functional/test_uninstall_user.py | 1 + 8 files changed, 165 insertions(+), 153 deletions(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 51c8dab4a6e..2657a8aeeb6 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -188,7 +188,8 @@ def test_pep518_forkbombs(script, data, common_wheels, command, package): @pytest.mark.network def test_pip_second_command_line_interface_works( - script, pip_src, data, common_wheels, deprecated_python): + script, pip_src, data, common_wheels, deprecated_python, with_wheel +): """ Check if ``pip<PYVERSION>`` commands behaves equally """ @@ -204,12 +205,12 @@ def test_pip_second_command_line_interface_works( args.extend(['install', 'INITools==0.2']) args.extend(['-f', data.packages]) result = script.run(*args, **kwargs) - egg_info_folder = ( + dist_info_folder = ( script.site_packages / - 'INITools-0.2-py{pyversion}.egg-info'.format(**globals()) + 'INITools-0.2.dist-info' ) initools_folder = script.site_packages / 'initools' - result.did_create(egg_info_folder) + result.did_create(dist_info_folder) result.did_create(initools_folder) @@ -231,17 +232,17 @@ def test_install_exit_status_code_when_blank_requirements_file(script): @pytest.mark.network -def test_basic_install_from_pypi(script): +def test_basic_install_from_pypi(script, with_wheel): """ Test installing a package from PyPI. """ result = script.pip('install', 'INITools==0.2') - egg_info_folder = ( + dist_info_folder = ( script.site_packages / - 'INITools-0.2-py{pyversion}.egg-info'.format(**globals()) + 'INITools-0.2.dist-info' ) initools_folder = script.site_packages / 'initools' - result.did_create(egg_info_folder) + result.did_create(dist_info_folder) result.did_create(initools_folder) # Should not display where it's looking for files @@ -406,7 +407,9 @@ def test_vcs_url_urlquote_normalization(script, tmpdir): @pytest.mark.parametrize("resolver", ["", "--use-feature=2020-resolver"]) -def test_basic_install_from_local_directory(script, data, resolver): +def test_basic_install_from_local_directory( + script, data, resolver, with_wheel +): """ Test installing from a local directory. """ @@ -417,12 +420,12 @@ def test_basic_install_from_local_directory(script, data, resolver): args.append(to_install) result = script.pip(*args) fspkg_folder = script.site_packages / 'fspkg' - egg_info_folder = ( + dist_info_folder = ( script.site_packages / - 'FSPkg-0.1.dev0-py{pyversion}.egg-info'.format(**globals()) + 'FSPkg-0.1.dev0.dist-info' ) result.did_create(fspkg_folder) - result.did_create(egg_info_folder) + result.did_create(dist_info_folder) @pytest.mark.parametrize("test_type,editable", [ @@ -433,13 +436,15 @@ def test_basic_install_from_local_directory(script, data, resolver): ("embedded_rel_path", False), ("embedded_rel_path", True), ]) -def test_basic_install_relative_directory(script, data, test_type, editable): +def test_basic_install_relative_directory( + script, data, test_type, editable, with_wheel +): """ Test installing a requirement using a relative path. """ - egg_info_file = ( + dist_info_folder = ( script.site_packages / - 'FSPkg-0.1.dev0-py{pyversion}.egg-info'.format(**globals()) + 'FSPkg-0.1.dev0.dist-info' ) egg_link_file = ( script.site_packages / 'FSPkg.egg-link' @@ -465,7 +470,7 @@ def test_basic_install_relative_directory(script, data, test_type, editable): if not editable: result = script.pip('install', req_path, cwd=script.scratch_path) - result.did_create(egg_info_file) + result.did_create(dist_info_folder) result.did_create(package_folder) else: # Editable install. @@ -562,29 +567,32 @@ def test_hashed_install_failure_later_flag(script, tmpdir): def test_install_from_local_directory_with_symlinks_to_directories( - script, data): + script, data, with_wheel +): """ Test installing from a local directory containing symlinks to directories. """ to_install = data.packages.joinpath("symlinks") result = script.pip('install', to_install) pkg_folder = script.site_packages / 'symlinks' - egg_info_folder = ( + dist_info_folder = ( script.site_packages / - 'symlinks-0.1.dev0-py{pyversion}.egg-info'.format(**globals()) + 'symlinks-0.1.dev0.dist-info' ) result.did_create(pkg_folder) - result.did_create(egg_info_folder) + result.did_create(dist_info_folder) @pytest.mark.skipif("sys.platform == 'win32' or sys.version_info < (3,)") -def test_install_from_local_directory_with_socket_file(script, data, tmpdir): +def test_install_from_local_directory_with_socket_file( + script, data, tmpdir, with_wheel +): """ Test installing from a local directory containing a socket file. """ - egg_info_file = ( + dist_info_folder = ( script.site_packages / - "FSPkg-0.1.dev0-py{pyversion}.egg-info".format(**globals()) + "FSPkg-0.1.dev0.dist-info" ) package_folder = script.site_packages / "fspkg" to_copy = data.packages.joinpath("FSPkg") @@ -597,7 +605,7 @@ def test_install_from_local_directory_with_socket_file(script, data, tmpdir): result = script.pip("install", "--verbose", to_install) result.did_create(package_folder) - result.did_create(egg_info_file) + result.did_create(dist_info_folder) assert str(socket_file_path) in result.stderr @@ -673,7 +681,7 @@ def test_upgrade_argparse_shadowed(script): assert "Not uninstalling argparse" not in result.stdout -def test_install_curdir(script, data): +def test_install_curdir(script, data, with_wheel): """ Test installing current directory ('.'). """ @@ -684,27 +692,27 @@ def test_install_curdir(script, data): rmtree(egg_info) result = script.pip('install', curdir, cwd=run_from) fspkg_folder = script.site_packages / 'fspkg' - egg_info_folder = ( + dist_info_folder = ( script.site_packages / - 'FSPkg-0.1.dev0-py{pyversion}.egg-info'.format(**globals()) + 'FSPkg-0.1.dev0.dist-info' ) result.did_create(fspkg_folder) - result.did_create(egg_info_folder) + result.did_create(dist_info_folder) -def test_install_pardir(script, data): +def test_install_pardir(script, data, with_wheel): """ Test installing parent directory ('..'). """ run_from = data.packages.joinpath("FSPkg", "fspkg") result = script.pip('install', pardir, cwd=run_from) fspkg_folder = script.site_packages / 'fspkg' - egg_info_folder = ( + dist_info_folder = ( script.site_packages / - 'FSPkg-0.1.dev0-py{pyversion}.egg-info'.format(**globals()) + 'FSPkg-0.1.dev0.dist-info' ) result.did_create(fspkg_folder) - result.did_create(egg_info_folder) + result.did_create(dist_info_folder) @pytest.mark.network @@ -765,17 +773,17 @@ def test_install_global_option_using_editable(script, tmpdir): @pytest.mark.network -def test_install_package_with_same_name_in_curdir(script): +def test_install_package_with_same_name_in_curdir(script, with_wheel): """ Test installing a package with the same name of a local folder """ script.scratch_path.joinpath("mock==0.6").mkdir() result = script.pip('install', 'mock==0.6') - egg_folder = ( + dist_info_folder = ( script.site_packages / - 'mock-0.6.0-py{pyversion}.egg-info'.format(**globals()) + 'mock-0.6.0.dist-info' ) - result.did_create(egg_folder) + result.did_create(dist_info_folder) mock100_setup_py = textwrap.dedent('''\ @@ -784,7 +792,7 @@ def test_install_package_with_same_name_in_curdir(script): version='100.1')''') -def test_install_folder_using_dot_slash(script): +def test_install_folder_using_dot_slash(script, with_wheel): """ Test installing a folder using pip install ./foldername """ @@ -792,14 +800,14 @@ def test_install_folder_using_dot_slash(script): pkg_path = script.scratch_path / 'mock' pkg_path.joinpath("setup.py").write_text(mock100_setup_py) result = script.pip('install', './mock') - egg_folder = ( + dist_info_folder = ( script.site_packages / - 'mock-100.1-py{pyversion}.egg-info'.format(**globals()) + 'mock-100.1.dist-info' ) - result.did_create(egg_folder) + result.did_create(dist_info_folder) -def test_install_folder_using_slash_in_the_end(script): +def test_install_folder_using_slash_in_the_end(script, with_wheel): r""" Test installing a folder using pip install foldername/ or foldername\ """ @@ -807,14 +815,14 @@ def test_install_folder_using_slash_in_the_end(script): pkg_path = script.scratch_path / 'mock' pkg_path.joinpath("setup.py").write_text(mock100_setup_py) result = script.pip('install', 'mock' + os.path.sep) - egg_folder = ( + dist_info_folder = ( script.site_packages / - 'mock-100.1-py{pyversion}.egg-info'.format(**globals()) + 'mock-100.1.dist-info' ) - result.did_create(egg_folder) + result.did_create(dist_info_folder) -def test_install_folder_using_relative_path(script): +def test_install_folder_using_relative_path(script, with_wheel): """ Test installing a folder using pip install folder1/folder2 """ @@ -823,29 +831,29 @@ def test_install_folder_using_relative_path(script): pkg_path = script.scratch_path / 'initools' / 'mock' pkg_path.joinpath("setup.py").write_text(mock100_setup_py) result = script.pip('install', Path('initools') / 'mock') - egg_folder = ( + dist_info_folder = ( script.site_packages / - 'mock-100.1-py{pyversion}.egg-info'.format(**globals()) + 'mock-100.1.dist-info'.format(**globals()) ) - result.did_create(egg_folder) + result.did_create(dist_info_folder) @pytest.mark.network -def test_install_package_which_contains_dev_in_name(script): +def test_install_package_which_contains_dev_in_name(script, with_wheel): """ Test installing package from PyPI which contains 'dev' in name """ result = script.pip('install', 'django-devserver==0.0.4') devserver_folder = script.site_packages / 'devserver' - egg_info_folder = ( + dist_info_folder = ( script.site_packages / - 'django_devserver-0.0.4-py{pyversion}.egg-info'.format(**globals()) + 'django_devserver-0.0.4.dist-info' ) result.did_create(devserver_folder) - result.did_create(egg_info_folder) + result.did_create(dist_info_folder) -def test_install_package_with_target(script): +def test_install_package_with_target(script, with_wheel): """ Test installing a package using pip install --target """ @@ -863,10 +871,11 @@ def test_install_package_with_target(script): result = script.pip_install_local('--upgrade', '-t', target_dir, "simple==2.0") result.did_update(Path('scratch') / 'target' / 'simple') - egg_folder = ( + dist_info_folder = ( Path('scratch') / 'target' / - 'simple-2.0-py{pyversion}.egg-info'.format(**globals())) - result.did_create(egg_folder) + 'simple-2.0.dist-info' + ) + result.did_create(dist_info_folder) # Test install and upgrade of single-module package result = script.pip_install_local('-t', target_dir, 'singlemodule==0.0.0') @@ -990,7 +999,7 @@ def main(): pass assert "--no-warn-script-location" not in result.stderr, str(result) -def test_install_package_with_root(script, data): +def test_install_package_with_root(script, data, with_wheel): """ Test installing a package using pip install --root """ @@ -1001,7 +1010,7 @@ def test_install_package_with_root(script, data): ) normal_install_path = ( script.base_path / script.site_packages / - 'simple-1.0-py{pyversion}.egg-info'.format(**globals()) + 'simple-1.0.dist-info' ) # use distutils to change the root exactly how the --root option does it from distutils.util import change_root @@ -1029,6 +1038,7 @@ def test_install_package_with_prefix(script, data): rel_prefix_path = script.scratch / 'prefix' install_path = ( distutils.sysconfig.get_python_lib(prefix=rel_prefix_path) / + # we still test for egg-info because no-binary implies setup.py install 'simple-1.0-py{}.egg-info'.format(pyversion) ) result.did_create(install_path) @@ -1109,7 +1119,7 @@ def test_install_package_with_latin1_setup(script, data): script.pip('install', to_install) -def test_url_req_case_mismatch_no_index(script, data): +def test_url_req_case_mismatch_no_index(script, data, with_wheel): """ tar ball url requirements (with no egg fragment), that happen to have upper case project names, should be considered equal to later requirements that @@ -1124,15 +1134,15 @@ def test_url_req_case_mismatch_no_index(script, data): ) # only Upper-1.0.tar.gz should get installed. - egg_folder = script.site_packages / \ - 'Upper-1.0-py{pyversion}.egg-info'.format(**globals()) - result.did_create(egg_folder) - egg_folder = script.site_packages / \ - 'Upper-2.0-py{pyversion}.egg-info'.format(**globals()) - result.did_not_create(egg_folder) + dist_info_folder = script.site_packages / \ + 'Upper-1.0.dist-info' + result.did_create(dist_info_folder) + dist_info_folder = script.site_packages / \ + 'Upper-2.0.dist-info' + result.did_not_create(dist_info_folder) -def test_url_req_case_mismatch_file_index(script, data): +def test_url_req_case_mismatch_file_index(script, data, with_wheel): """ tar ball url requirements (with no egg fragment), that happen to have upper case project names, should be considered equal to later requirements that @@ -1153,15 +1163,15 @@ def test_url_req_case_mismatch_file_index(script, data): ) # only Upper-1.0.tar.gz should get installed. - egg_folder = script.site_packages / \ - 'Dinner-1.0-py{pyversion}.egg-info'.format(**globals()) - result.did_create(egg_folder) - egg_folder = script.site_packages / \ - 'Dinner-2.0-py{pyversion}.egg-info'.format(**globals()) - result.did_not_create(egg_folder) + dist_info_folder = script.site_packages / \ + 'Dinner-1.0.dist-info' + result.did_create(dist_info_folder) + dist_info_folder = script.site_packages / \ + 'Dinner-2.0.dist-info' + result.did_not_create(dist_info_folder) -def test_url_incorrect_case_no_index(script, data): +def test_url_incorrect_case_no_index(script, data, with_wheel): """ Same as test_url_req_case_mismatch_no_index, except testing for the case where the incorrect case is given in the name of the package to install @@ -1172,15 +1182,15 @@ def test_url_incorrect_case_no_index(script, data): ) # only Upper-2.0.tar.gz should get installed. - egg_folder = script.site_packages / \ - 'Upper-1.0-py{pyversion}.egg-info'.format(**globals()) - result.did_not_create(egg_folder) - egg_folder = script.site_packages / \ - 'Upper-2.0-py{pyversion}.egg-info'.format(**globals()) - result.did_create(egg_folder) + dist_info_folder = script.site_packages / \ + 'Upper-1.0.dist-info' + result.did_not_create(dist_info_folder) + dist_info_folder = script.site_packages / \ + 'Upper-2.0.dist-info' + result.did_create(dist_info_folder) -def test_url_incorrect_case_file_index(script, data): +def test_url_incorrect_case_file_index(script, data, with_wheel): """ Same as test_url_req_case_mismatch_file_index, except testing for the case where the incorrect case is given in the name of the package to install @@ -1192,12 +1202,12 @@ def test_url_incorrect_case_file_index(script, data): ) # only Upper-2.0.tar.gz should get installed. - egg_folder = script.site_packages / \ - 'Dinner-1.0-py{pyversion}.egg-info'.format(**globals()) - result.did_not_create(egg_folder) - egg_folder = script.site_packages / \ - 'Dinner-2.0-py{pyversion}.egg-info'.format(**globals()) - result.did_create(egg_folder) + dist_info_folder = script.site_packages / \ + 'Dinner-1.0.dist-info' + result.did_not_create(dist_info_folder) + dist_info_folder = script.site_packages / \ + 'Dinner-2.0.dist-info' + result.did_create(dist_info_folder) # Should show index-url location in output assert "Looking in indexes: " in result.stdout diff --git a/tests/functional/test_install_index.py b/tests/functional/test_install_index.py index 1c778587fa4..e887595b937 100644 --- a/tests/functional/test_install_index.py +++ b/tests/functional/test_install_index.py @@ -3,10 +3,8 @@ from pip._vendor.six.moves.urllib import parse as urllib_parse -from tests.lib import pyversion - -def test_find_links_relative_path(script, data): +def test_find_links_relative_path(script, data, with_wheel): """Test find-links as a relative path.""" result = script.pip( 'install', @@ -16,15 +14,15 @@ def test_find_links_relative_path(script, data): 'packages/', cwd=data.root, ) - egg_info_folder = ( - script.site_packages / 'parent-0.1-py{}.egg-info'.format(pyversion) + dist_info_folder = ( + script.site_packages / 'parent-0.1.dist-info' ) initools_folder = script.site_packages / 'parent' - result.did_create(egg_info_folder) + result.did_create(dist_info_folder) result.did_create(initools_folder) -def test_find_links_requirements_file_relative_path(script, data): +def test_find_links_requirements_file_relative_path(script, data, with_wheel): """Test find-links as a relative path to a reqs file.""" script.scratch_path.joinpath("test-req.txt").write_text(textwrap.dedent(""" --no-index @@ -37,27 +35,27 @@ def test_find_links_requirements_file_relative_path(script, data): script.scratch_path / "test-req.txt", cwd=data.root, ) - egg_info_folder = ( - script.site_packages / 'parent-0.1-py{}.egg-info'.format(pyversion) + dist_info_folder = ( + script.site_packages / 'parent-0.1.dist-info' ) initools_folder = script.site_packages / 'parent' - result.did_create(egg_info_folder) + result.did_create(dist_info_folder) result.did_create(initools_folder) -def test_install_from_file_index_hash_link(script, data): +def test_install_from_file_index_hash_link(script, data, with_wheel): """ Test that a pkg can be installed from a file:// index using a link with a hash """ result = script.pip('install', '-i', data.index_url(), 'simple==1.0') - egg_info_folder = ( - script.site_packages / 'simple-1.0-py{}.egg-info'.format(pyversion) + dist_info_folder = ( + script.site_packages / 'simple-1.0.dist-info' ) - result.did_create(egg_info_folder) + result.did_create(dist_info_folder) -def test_file_index_url_quoting(script, data): +def test_file_index_url_quoting(script, data, with_wheel): """ Test url quoting of file index url with a space """ @@ -67,5 +65,5 @@ def test_file_index_url_quoting(script, data): ) result.did_create(script.site_packages / 'simple') result.did_create( - script.site_packages / 'simple-1.0-py{}.egg-info'.format(pyversion) + script.site_packages / 'simple-1.0.dist-info' ) diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index 7ec863493b4..d12e19b211a 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -10,7 +10,6 @@ create_basic_wheel_for_package, need_svn, path_to_url, - pyversion, requirements_file, ) from tests.lib.local_repos import local_checkout @@ -63,7 +62,7 @@ def _arg_recording_sdist_maker(name): @pytest.mark.network -def test_requirements_file(script): +def test_requirements_file(script, with_wheel): """ Test installing from a requirements file. @@ -78,12 +77,12 @@ def test_requirements_file(script): 'install', '-r', script.scratch_path / 'initools-req.txt' ) result.did_create( - script.site_packages / 'INITools-0.2-py{}.egg-info'.format(pyversion) + script.site_packages / 'INITools-0.2.dist-info' ) result.did_create(script.site_packages / 'initools') assert result.files_created[script.site_packages / other_lib_name].dir - fn = '{}-{}-py{}.egg-info'.format( - other_lib_name, other_lib_version, pyversion) + fn = '{}-{}.dist-info'.format( + other_lib_name, other_lib_version) assert result.files_created[script.site_packages / fn].dir @@ -113,15 +112,17 @@ def test_schema_check_in_requirements_file(script): ("embedded_rel_path", False), ("embedded_rel_path", True), ]) -def test_relative_requirements_file(script, data, test_type, editable): +def test_relative_requirements_file( + script, data, test_type, editable, with_wheel +): """ Test installing from a requirements file with a relative path. For path URLs, use an egg= definition. """ - egg_info_file = ( + dist_info_folder = ( script.site_packages / - 'FSPkg-0.1.dev0-py{pyversion}.egg-info'.format(**globals()) + 'FSPkg-0.1.dev0.dist-info' ) egg_link_file = ( script.site_packages / 'FSPkg.egg-link' @@ -148,7 +149,7 @@ def test_relative_requirements_file(script, data, test_type, editable): script.scratch_path) as reqs_file: result = script.pip('install', '-vvv', '-r', reqs_file.name, cwd=script.scratch_path) - result.did_create(egg_info_file) + result.did_create(dist_info_folder) result.did_create(package_folder) else: with requirements_file('-e ' + req_path + '\n', @@ -160,7 +161,7 @@ def test_relative_requirements_file(script, data, test_type, editable): @pytest.mark.network @need_svn -def test_multiple_requirements_files(script, tmpdir): +def test_multiple_requirements_files(script, tmpdir, with_wheel): """ Test installing from multiple nested requirements files. @@ -184,8 +185,7 @@ def test_multiple_requirements_files(script, tmpdir): 'install', '-r', script.scratch_path / 'initools-req.txt' ) assert result.files_created[script.site_packages / other_lib_name].dir - fn = '{other_lib_name}-{other_lib_version}-py{pyversion}.egg-info'.format( - pyversion=pyversion, **locals()) + fn = '{other_lib_name}-{other_lib_version}.dist-info'.format(**locals()) assert result.files_created[script.site_packages / fn].dir result.did_create(script.venv / 'src' / 'initools') diff --git a/tests/functional/test_install_upgrade.py b/tests/functional/test_install_upgrade.py index b4bad29964c..e45bf31483e 100644 --- a/tests/functional/test_install_upgrade.py +++ b/tests/functional/test_install_upgrade.py @@ -38,7 +38,8 @@ def test_invalid_upgrade_strategy_causes_error(script): def test_only_if_needed_does_not_upgrade_deps_when_satisfied( script, - use_new_resolver + use_new_resolver, + with_wheel ): """ It doesn't upgrade a dependency if it already satisfies the requirements. @@ -50,13 +51,11 @@ def test_only_if_needed_does_not_upgrade_deps_when_satisfied( ) assert ( - (script.site_packages / 'require_simple-1.0-py{pyversion}.egg-info' - .format(**globals())) + (script.site_packages / 'require_simple-1.0.dist-info') not in result.files_deleted ), "should have installed require_simple==1.0" assert ( - (script.site_packages / 'simple-2.0-py{pyversion}.egg-info' - .format(**globals())) + (script.site_packages / 'simple-2.0.dist-info') not in result.files_deleted ), "should not have uninstalled simple==2.0" @@ -68,7 +67,9 @@ def test_only_if_needed_does_not_upgrade_deps_when_satisfied( ), "did not print correct message for not-upgraded requirement" -def test_only_if_needed_does_upgrade_deps_when_no_longer_satisfied(script): +def test_only_if_needed_does_upgrade_deps_when_no_longer_satisfied( + script, with_wheel +): """ It does upgrade a dependency if it no longer satisfies the requirements. @@ -79,25 +80,26 @@ def test_only_if_needed_does_upgrade_deps_when_no_longer_satisfied(script): ) assert ( - (script.site_packages / 'require_simple-1.0-py{pyversion}.egg-info' - .format(**globals())) + (script.site_packages / 'require_simple-1.0.dist-info') not in result.files_deleted ), "should have installed require_simple==1.0" expected = ( script.site_packages / - 'simple-3.0-py{pyversion}.egg-info'.format(**globals()) + 'simple-3.0.dist-info' ) result.did_create(expected, message="should have installed simple==3.0") expected = ( script.site_packages / - 'simple-1.0-py{pyversion}.egg-info'.format(**globals()) + 'simple-1.0.dist-info' ) assert ( expected in result.files_deleted ), "should have uninstalled simple==1.0" -def test_eager_does_upgrade_dependecies_when_currently_satisfied(script): +def test_eager_does_upgrade_dependecies_when_currently_satisfied( + script, with_wheel +): """ It does upgrade a dependency even if it already satisfies the requirements. @@ -109,17 +111,19 @@ def test_eager_does_upgrade_dependecies_when_currently_satisfied(script): assert ( (script.site_packages / - 'require_simple-1.0-py{pyversion}.egg-info'.format(**globals())) + 'require_simple-1.0.dist-info') not in result.files_deleted ), "should have installed require_simple==1.0" assert ( (script.site_packages / - 'simple-2.0-py{pyversion}.egg-info'.format(**globals())) + 'simple-2.0.dist-info') in result.files_deleted ), "should have uninstalled simple==2.0" -def test_eager_does_upgrade_dependecies_when_no_longer_satisfied(script): +def test_eager_does_upgrade_dependecies_when_no_longer_satisfied( + script, with_wheel +): """ It does upgrade a dependency if it no longer satisfies the requirements. @@ -130,24 +134,21 @@ def test_eager_does_upgrade_dependecies_when_no_longer_satisfied(script): ) assert ( - (script.site_packages / - 'require_simple-1.0-py{pyversion}.egg-info'.format(**globals())) + (script.site_packages / 'require_simple-1.0.dist-info') not in result.files_deleted ), "should have installed require_simple==1.0" result.did_create( - script.site_packages / - 'simple-3.0-py{pyversion}.egg-info'.format(**globals()), + script.site_packages / 'simple-3.0.dist-info', message="should have installed simple==3.0" ) assert ( - script.site_packages / - 'simple-1.0-py{pyversion}.egg-info'.format(**globals()) + script.site_packages / 'simple-1.0.dist-info' in result.files_deleted ), "should have uninstalled simple==1.0" @pytest.mark.network -def test_upgrade_to_specific_version(script): +def test_upgrade_to_specific_version(script, with_wheel): """ It does upgrade to specific version requested. @@ -158,18 +159,16 @@ def test_upgrade_to_specific_version(script): 'pip install with specific version did not upgrade' ) assert ( - script.site_packages / 'INITools-0.1-py{pyversion}.egg-info' - .format(**globals()) + script.site_packages / 'INITools-0.1.dist-info' in result.files_deleted ) result.did_create( - script.site_packages / 'INITools-0.2-py{pyversion}.egg-info' - .format(**globals()) + script.site_packages / 'INITools-0.2.dist-info' ) @pytest.mark.network -def test_upgrade_if_requested(script): +def test_upgrade_if_requested(script, with_wheel): """ And it does upgrade if requested. @@ -179,7 +178,7 @@ def test_upgrade_if_requested(script): assert result.files_created, 'pip install --upgrade did not upgrade' result.did_not_create( script.site_packages / - 'INITools-0.1-py{pyversion}.egg-info'.format(**globals()) + 'INITools-0.1.dist-info' ) @@ -327,7 +326,7 @@ def test_uninstall_rollback(script, data): @pytest.mark.network -def test_should_not_install_always_from_cache(script): +def test_should_not_install_always_from_cache(script, with_wheel): """ If there is an old cached package, pip should download the newer version Related to issue #175 @@ -337,16 +336,16 @@ def test_should_not_install_always_from_cache(script): result = script.pip('install', 'INITools==0.1') result.did_not_create( script.site_packages / - 'INITools-0.2-py{pyversion}.egg-info'.format(**globals()) + 'INITools-0.2.dist-info' ) result.did_create( script.site_packages / - 'INITools-0.1-py{pyversion}.egg-info'.format(**globals()) + 'INITools-0.1.dist-info' ) @pytest.mark.network -def test_install_with_ignoreinstalled_requested(script): +def test_install_with_ignoreinstalled_requested(script, with_wheel): """ Test old conflicting package is completely ignored """ @@ -356,11 +355,11 @@ def test_install_with_ignoreinstalled_requested(script): # both the old and new metadata should be present. assert os.path.exists( script.site_packages_path / - 'INITools-0.1-py{pyversion}.egg-info'.format(**globals()) + 'INITools-0.1.dist-info' ) assert os.path.exists( script.site_packages_path / - 'INITools-0.3-py{pyversion}.egg-info'.format(**globals()) + 'INITools-0.3.dist-info' ) diff --git a/tests/functional/test_install_user.py b/tests/functional/test_install_user.py index 8593744db4f..24169470a70 100644 --- a/tests/functional/test_install_user.py +++ b/tests/functional/test_install_user.py @@ -115,6 +115,7 @@ def test_install_user_conflict_in_usersite(self, script): 'install', '--user', 'INITools==0.1', '--no-binary=:all:') # usersite has 0.1 + # we still test for egg-info because no-binary implies setup.py install egg_info_folder = ( script.user_site / 'INITools-0.1-py{pyversion}.egg-info'.format(**globals()) @@ -142,6 +143,7 @@ def test_install_user_conflict_in_globalsite(self, virtualenv, script): 'install', '--user', 'INITools==0.1', '--no-binary=:all:') # usersite has 0.1 + # we still test for egg-info because no-binary implies setup.py install egg_info_folder = ( script.user_site / 'INITools-0.1-py{pyversion}.egg-info'.format(**globals()) @@ -173,6 +175,7 @@ def test_upgrade_user_conflict_in_globalsite(self, virtualenv, script): 'install', '--user', '--upgrade', 'INITools', '--no-binary=:all:') # usersite has 0.3.1 + # we still test for egg-info because no-binary implies setup.py install egg_info_folder = ( script.user_site / 'INITools-0.3.1-py{pyversion}.egg-info'.format(**globals()) @@ -207,6 +210,7 @@ def test_install_user_conflict_in_globalsite_and_usersite( 'install', '--user', 'INITools==0.1', '--no-binary=:all:') # usersite has 0.1 + # we still test for egg-info because no-binary implies setup.py install egg_info_folder = ( script.user_site / 'INITools-0.1-py{pyversion}.egg-info'.format(**globals()) diff --git a/tests/functional/test_install_vcs_git.py b/tests/functional/test_install_vcs_git.py index b9f9c7abb80..59393d34747 100644 --- a/tests/functional/test_install_vcs_git.py +++ b/tests/functional/test_install_vcs_git.py @@ -160,7 +160,7 @@ def test_install_editable_from_git_with_https(script, tmpdir): @pytest.mark.network -def test_install_noneditable_git(script, tmpdir): +def test_install_noneditable_git(script, tmpdir, with_wheel): """ Test installing from a non-editable git URL with a given tag. """ @@ -169,14 +169,14 @@ def test_install_noneditable_git(script, tmpdir): 'git+https://github.com/pypa/pip-test-package.git' '@0.1.1#egg=pip-test-package' ) - egg_info_folder = ( + dist_info_folder = ( script.site_packages / - 'pip_test_package-0.1.1-py{pyversion}.egg-info'.format(**globals()) + 'pip_test_package-0.1.1.dist-info' ) result.assert_installed('piptestpackage', without_egg_link=True, editable=False) - result.did_create(egg_info_folder) + result.did_create(dist_info_folder) def test_git_with_sha1_revisions(script): diff --git a/tests/functional/test_install_wheel.py b/tests/functional/test_install_wheel.py index 26eae0480a8..c53f13ca415 100644 --- a/tests/functional/test_install_wheel.py +++ b/tests/functional/test_install_wheel.py @@ -340,8 +340,8 @@ def test_install_user_wheel(script, shared_data, with_wheel, tmpdir): 'install', 'has.script==1.0', '--user', '--no-index', '--find-links', tmpdir, ) - egg_info_folder = script.user_site / 'has.script-1.0.dist-info' - result.did_create(egg_info_folder) + dist_info_folder = script.user_site / 'has.script-1.0.dist-info' + result.did_create(dist_info_folder) script_file = script.user_bin / 'script.py' result.did_create(script_file) diff --git a/tests/functional/test_uninstall_user.py b/tests/functional/test_uninstall_user.py index d73f2cfaf3e..2dbf032ac38 100644 --- a/tests/functional/test_uninstall_user.py +++ b/tests/functional/test_uninstall_user.py @@ -43,6 +43,7 @@ def test_uninstall_from_usersite_with_dist_in_global_site( assert_all_changes(result2, result3, [script.venv / 'build', 'cache']) # site still has 0.2 (can't look in result1; have to check) + # keep checking for egg-info because no-binary implies setup.py install egg_info_folder = ( script.base_path / script.site_packages / 'pip_test_package-0.1-py{pyversion}.egg-info'.format(**globals()) From 44bb7164ddefc214ee67c9e99080fc6f053ec805 Mon Sep 17 00:00:00 2001 From: Emmanuel Arias <eamanu@yaerobi.com> Date: Sat, 25 Jul 2020 09:56:53 -0300 Subject: [PATCH 2309/3170] Fix description of config files User Guide This PR make consitence User Guide with pip config file --- 759f489e-80da-4dac-8d7d-13c471f1cee9.trivial | 0 docs/html/user_guide.rst | 10 +++++----- 2 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 759f489e-80da-4dac-8d7d-13c471f1cee9.trivial diff --git a/759f489e-80da-4dac-8d7d-13c471f1cee9.trivial b/759f489e-80da-4dac-8d7d-13c471f1cee9.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 10856e3354c..16585f12db1 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -342,7 +342,7 @@ pip allows you to set all command line option defaults in a standard ini style config file. The names and locations of the configuration files vary slightly across -platforms. You may have per-user, per-virtualenv or site-wide (shared amongst +platforms. You may have per-user, per-virtualenv or global (shared amongst all users) configuration: **Per-user**: @@ -369,7 +369,7 @@ variable ``PIP_CONFIG_FILE``. * On Unix and macOS the file is :file:`$VIRTUAL_ENV/pip.conf` * On Windows the file is: :file:`%VIRTUAL_ENV%\\pip.ini` -**Site-wide**: +**Global**: * On Unix the file may be located in :file:`/etc/pip.conf`. Alternatively it may be in a "pip" subdirectory of any of the paths set in the @@ -380,17 +380,17 @@ variable ``PIP_CONFIG_FILE``. :file:`C:\\Documents and Settings\\All Users\\Application Data\\pip\\pip.ini` * On Windows 7 and later the file is hidden, but writeable at :file:`C:\\ProgramData\\pip\\pip.ini` -* Site-wide configuration is not supported on Windows Vista +* System-wide configuration is not supported on Windows Vista If multiple configuration files are found by pip then they are combined in the following order: -1. The site-wide file is read +1. The system-wide file is read 2. The per-user file is read 3. The virtualenv-specific file is read Each file read overrides any values read from previous files, so if the -global timeout is specified in both the site-wide file and the per-user file +global timeout is specified in both the system-wide file and the per-user file then the latter value will be used. The names of the settings are derived from the long command line option, e.g. From ead83f38b485e074366dd92f4d351092385f056c Mon Sep 17 00:00:00 2001 From: Emmanuel Arias <eamanu@yaerobi.com> Date: Sat, 25 Jul 2020 10:00:42 -0300 Subject: [PATCH 2310/3170] fix news --- .../759f489e-80da-4dac-8d7d-13c471f1cee9.trivial | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename 759f489e-80da-4dac-8d7d-13c471f1cee9.trivial => news/759f489e-80da-4dac-8d7d-13c471f1cee9.trivial (100%) diff --git a/759f489e-80da-4dac-8d7d-13c471f1cee9.trivial b/news/759f489e-80da-4dac-8d7d-13c471f1cee9.trivial similarity index 100% rename from 759f489e-80da-4dac-8d7d-13c471f1cee9.trivial rename to news/759f489e-80da-4dac-8d7d-13c471f1cee9.trivial From c564a3d5414690060c7c41197e2cbed6f4ca5233 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Wed, 22 Jul 2020 23:01:30 +0700 Subject: [PATCH 2311/3170] Use monkeypatch.setenv in config and option tests --- ...3425e4-767e-4d73-bce5-88644b781855.trivial | 0 tests/lib/configuration_helpers.py | 16 --- tests/lib/options_helpers.py | 5 - tests/unit/test_configuration.py | 38 +++---- tests/unit/test_options.py | 106 +++++++----------- 5 files changed, 57 insertions(+), 108 deletions(-) create mode 100644 news/d53425e4-767e-4d73-bce5-88644b781855.trivial diff --git a/news/d53425e4-767e-4d73-bce5-88644b781855.trivial b/news/d53425e4-767e-4d73-bce5-88644b781855.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/lib/configuration_helpers.py b/tests/lib/configuration_helpers.py index d33b8ec09e5..3e3692696a8 100644 --- a/tests/lib/configuration_helpers.py +++ b/tests/lib/configuration_helpers.py @@ -14,18 +14,6 @@ kinds = pip._internal.configuration.kinds -def reset_os_environ(old_environ): - """ - Reset os.environ while preserving the same underlying mapping. - """ - # Preserving the same mapping is preferable to assigning a new mapping - # because the latter has interfered with test isolation by, for example, - # preventing time.tzset() from working in subsequent tests after - # changing os.environ['TZ'] in those tests. - os.environ.clear() - os.environ.update(old_environ) - - class ConfigurationMixin(object): def setup(self): @@ -34,14 +22,10 @@ def setup(self): ) self._files_to_clear = [] - self._old_environ = os.environ.copy() - def teardown(self): for fname in self._files_to_clear: fname.stop() - reset_os_environ(self._old_environ) - def patch_configuration(self, variant, di): old = self.configuration._load_config_files diff --git a/tests/lib/options_helpers.py b/tests/lib/options_helpers.py index 120070abb99..2354a818df8 100644 --- a/tests/lib/options_helpers.py +++ b/tests/lib/options_helpers.py @@ -1,12 +1,9 @@ """Provides helper classes for testing option handling in pip """ -import os - from pip._internal.cli import cmdoptions from pip._internal.cli.base_command import Command from pip._internal.commands import CommandInfo, commands_dict -from tests.lib.configuration_helpers import reset_os_environ class FakeCommand(Command): @@ -23,11 +20,9 @@ def main(self, args): class AddFakeCommandMixin(object): def setup(self): - self.environ_before = os.environ.copy() commands_dict['fake'] = CommandInfo( 'tests.lib.options_helpers', 'FakeCommand', 'fake summary', ) def teardown(self): - reset_os_environ(self.environ_before) commands_dict.pop('fake') diff --git a/tests/unit/test_configuration.py b/tests/unit/test_configuration.py index f16252f444f..0a45fc136d5 100644 --- a/tests/unit/test_configuration.py +++ b/tests/unit/test_configuration.py @@ -1,8 +1,6 @@ """Tests for all things related to the configuration """ -import os - import pytest from mock import MagicMock @@ -31,48 +29,48 @@ def test_site_loading(self): self.configuration.load() assert self.configuration.get_value("test.hello") == "3" - def test_environment_config_loading(self): + def test_environment_config_loading(self, monkeypatch): contents = """ [test] hello = 4 """ with self.tmpfile(contents) as config_file: - os.environ["PIP_CONFIG_FILE"] = config_file + monkeypatch.setenv("PIP_CONFIG_FILE", config_file) self.configuration.load() assert self.configuration.get_value("test.hello") == "4", \ self.configuration._config - def test_environment_var_loading(self): - os.environ["PIP_HELLO"] = "5" + def test_environment_var_loading(self, monkeypatch): + monkeypatch.setenv("PIP_HELLO", "5") self.configuration.load() assert self.configuration.get_value(":env:.hello") == "5" @pytest.mark.skipif("sys.platform == 'win32'") - def test_environment_var_does_not_load_lowercase(self): - os.environ["pip_hello"] = "5" + def test_environment_var_does_not_load_lowercase(self, monkeypatch): + monkeypatch.setenv("pip_hello", "5") self.configuration.load() with pytest.raises(ConfigurationError): self.configuration.get_value(":env:.hello") - def test_environment_var_does_not_load_version(self): - os.environ["PIP_VERSION"] = "True" + def test_environment_var_does_not_load_version(self, monkeypatch): + monkeypatch.setenv("PIP_VERSION", "True") self.configuration.load() with pytest.raises(ConfigurationError): self.configuration.get_value(":env:.version") - def test_environment_config_errors_if_malformed(self): + def test_environment_config_errors_if_malformed(self, monkeypatch): contents = """ test] hello = 4 """ with self.tmpfile(contents) as config_file: - os.environ["PIP_CONFIG_FILE"] = config_file + monkeypatch.setenv("PIP_CONFIG_FILE", config_file) with pytest.raises(ConfigurationError) as err: self.configuration.load() @@ -130,36 +128,36 @@ def test_user_overides_global(self): assert self.configuration.get_value("test.hello") == "2" - def test_env_not_overriden_by_environment_var(self): + def test_env_not_overriden_by_environment_var(self, monkeypatch): self.patch_configuration(kinds.ENV, {"test.hello": "1"}) - os.environ["PIP_HELLO"] = "5" + monkeypatch.setenv("PIP_HELLO", "5") self.configuration.load() assert self.configuration.get_value("test.hello") == "1" assert self.configuration.get_value(":env:.hello") == "5" - def test_site_not_overriden_by_environment_var(self): + def test_site_not_overriden_by_environment_var(self, monkeypatch): self.patch_configuration(kinds.SITE, {"test.hello": "2"}) - os.environ["PIP_HELLO"] = "5" + monkeypatch.setenv("PIP_HELLO", "5") self.configuration.load() assert self.configuration.get_value("test.hello") == "2" assert self.configuration.get_value(":env:.hello") == "5" - def test_user_not_overriden_by_environment_var(self): + def test_user_not_overriden_by_environment_var(self, monkeypatch): self.patch_configuration(kinds.USER, {"test.hello": "3"}) - os.environ["PIP_HELLO"] = "5" + monkeypatch.setenv("PIP_HELLO", "5") self.configuration.load() assert self.configuration.get_value("test.hello") == "3" assert self.configuration.get_value(":env:.hello") == "5" - def test_global_not_overriden_by_environment_var(self): + def test_global_not_overriden_by_environment_var(self, monkeypatch): self.patch_configuration(kinds.GLOBAL, {"test.hello": "4"}) - os.environ["PIP_HELLO"] = "5" + monkeypatch.setenv("PIP_HELLO", "5") self.configuration.load() diff --git a/tests/unit/test_options.py b/tests/unit/test_options.py index ce4fc9c25d1..ff0324ff420 100644 --- a/tests/unit/test_options.py +++ b/tests/unit/test_options.py @@ -10,23 +10,6 @@ from tests.lib.options_helpers import AddFakeCommandMixin -@contextmanager -def temp_environment_variable(name, value): - not_set = object() - original = os.environ[name] if name in os.environ else not_set - os.environ[name] = value - - try: - yield - finally: - # Return the environment variable to its original state. - if original is not_set: - if name in os.environ: - del os.environ[name] - else: - os.environ[name] = original - - @contextmanager def assert_option_error(capsys, expected): """ @@ -70,56 +53,48 @@ def get_config_section_global(self, section): } return config[section] - def test_env_override_default_int(self): + def test_env_override_default_int(self, monkeypatch): """ Test that environment variable overrides an int option default. """ - os.environ['PIP_TIMEOUT'] = '-1' + monkeypatch.setenv('PIP_TIMEOUT', '-1') options, args = main(['fake']) assert options.timeout == -1 - def test_env_override_default_append(self): + @pytest.mark.parametrize('values', (['F1'], ['F1', 'F2'])) + def test_env_override_default_append(self, values, monkeypatch): """ Test that environment variable overrides an append option default. """ - os.environ['PIP_FIND_LINKS'] = 'F1' - options, args = main(['fake']) - assert options.find_links == ['F1'] - - os.environ['PIP_FIND_LINKS'] = 'F1 F2' + monkeypatch.setenv('PIP_FIND_LINKS', ' '.join(values)) options, args = main(['fake']) - assert options.find_links == ['F1', 'F2'] + assert options.find_links == values - def test_env_override_default_choice(self): + @pytest.mark.parametrize('choises', (['w'], ['s', 'w'])) + def test_env_override_default_choice(self, choises, monkeypatch): """ Test that environment variable overrides a choice option default. """ - os.environ['PIP_EXISTS_ACTION'] = 'w' + monkeypatch.setenv('PIP_EXISTS_ACTION', ' '.join(choises)) options, args = main(['fake']) - assert options.exists_action == ['w'] + assert options.exists_action == choises - os.environ['PIP_EXISTS_ACTION'] = 's w' - options, args = main(['fake']) - assert options.exists_action == ['s', 'w'] - - def test_env_alias_override_default(self): + @pytest.mark.parametrize('name', ('PIP_LOG_FILE', 'PIP_LOCAL_LOG')) + def test_env_alias_override_default(self, name, monkeypatch): """ When an option has multiple long forms, test that the technique of using the env variable, "PIP_<long form>" works for all cases. (e.g. PIP_LOG_FILE and PIP_LOCAL_LOG should all work) """ - os.environ['PIP_LOG_FILE'] = 'override.log' - options, args = main(['fake']) - assert options.log == 'override.log' - os.environ['PIP_LOCAL_LOG'] = 'override.log' + monkeypatch.setenv(name, 'override.log') options, args = main(['fake']) assert options.log == 'override.log' - def test_cli_override_environment(self): + def test_cli_override_environment(self, monkeypatch): """ Test the cli overrides and environment variable """ - os.environ['PIP_TIMEOUT'] = '-1' + monkeypatch.setenv('PIP_TIMEOUT', '-1') options, args = main(['fake', '--timeout', '-2']) assert options.timeout == -2 @@ -136,49 +111,49 @@ def test_cli_override_environment(self): 'off', 'no', ]) - def test_cache_dir__PIP_NO_CACHE_DIR(self, pip_no_cache_dir): + def test_cache_dir__PIP_NO_CACHE_DIR(self, pip_no_cache_dir, monkeypatch): """ Test setting the PIP_NO_CACHE_DIR environment variable without passing any command-line flags. """ - os.environ['PIP_NO_CACHE_DIR'] = pip_no_cache_dir + monkeypatch.setenv('PIP_NO_CACHE_DIR', pip_no_cache_dir) options, args = main(['fake']) assert options.cache_dir is False @pytest.mark.parametrize('pip_no_cache_dir', ['yes', 'no']) def test_cache_dir__PIP_NO_CACHE_DIR__with_cache_dir( - self, pip_no_cache_dir + self, pip_no_cache_dir, monkeypatch, ): """ Test setting PIP_NO_CACHE_DIR while also passing an explicit --cache-dir value. """ - os.environ['PIP_NO_CACHE_DIR'] = pip_no_cache_dir + monkeypatch.setenv('PIP_NO_CACHE_DIR', pip_no_cache_dir) options, args = main(['--cache-dir', '/cache/dir', 'fake']) # The command-line flag takes precedence. assert options.cache_dir == '/cache/dir' @pytest.mark.parametrize('pip_no_cache_dir', ['yes', 'no']) def test_cache_dir__PIP_NO_CACHE_DIR__with_no_cache_dir( - self, pip_no_cache_dir + self, pip_no_cache_dir, monkeypatch, ): """ Test setting PIP_NO_CACHE_DIR while also passing --no-cache-dir. """ - os.environ['PIP_NO_CACHE_DIR'] = pip_no_cache_dir + monkeypatch.setenv('PIP_NO_CACHE_DIR', pip_no_cache_dir) options, args = main(['--no-cache-dir', 'fake']) # The command-line flag should take precedence (which has the same # value in this case). assert options.cache_dir is False def test_cache_dir__PIP_NO_CACHE_DIR_invalid__with_no_cache_dir( - self, capsys, + self, monkeypatch, capsys, ): """ Test setting PIP_NO_CACHE_DIR to an invalid value while also passing --no-cache-dir. """ - os.environ['PIP_NO_CACHE_DIR'] = 'maybe' + monkeypatch.setenv('PIP_NO_CACHE_DIR', 'maybe') expected_err = "--no-cache-dir error: invalid truth value 'maybe'" with assert_option_error(capsys, expected=expected_err): main(['--no-cache-dir', 'fake']) @@ -219,52 +194,49 @@ def test_no_use_pep517(self): options = self.parse_args(['--no-use-pep517']) assert options.use_pep517 is False - def test_PIP_USE_PEP517_true(self): + def test_PIP_USE_PEP517_true(self, monkeypatch): """ Test setting PIP_USE_PEP517 to "true". """ - with temp_environment_variable('PIP_USE_PEP517', 'true'): - options = self.parse_args([]) + monkeypatch.setenv('PIP_USE_PEP517', 'true') + options = self.parse_args([]) # This is an int rather than a boolean because strtobool() in pip's # configuration code returns an int. assert options.use_pep517 == 1 - def test_PIP_USE_PEP517_false(self): + def test_PIP_USE_PEP517_false(self, monkeypatch): """ Test setting PIP_USE_PEP517 to "false". """ - with temp_environment_variable('PIP_USE_PEP517', 'false'): - options = self.parse_args([]) + monkeypatch.setenv('PIP_USE_PEP517', 'false') + options = self.parse_args([]) # This is an int rather than a boolean because strtobool() in pip's # configuration code returns an int. assert options.use_pep517 == 0 - def test_use_pep517_and_PIP_USE_PEP517_false(self): + def test_use_pep517_and_PIP_USE_PEP517_false(self, monkeypatch): """ Test passing --use-pep517 and setting PIP_USE_PEP517 to "false". """ - with temp_environment_variable('PIP_USE_PEP517', 'false'): - options = self.parse_args(['--use-pep517']) + monkeypatch.setenv('PIP_USE_PEP517', 'false') + options = self.parse_args(['--use-pep517']) assert options.use_pep517 is True - def test_no_use_pep517_and_PIP_USE_PEP517_true(self): + def test_no_use_pep517_and_PIP_USE_PEP517_true(self, monkeypatch): """ Test passing --no-use-pep517 and setting PIP_USE_PEP517 to "true". """ - with temp_environment_variable('PIP_USE_PEP517', 'true'): - options = self.parse_args(['--no-use-pep517']) + monkeypatch.setenv('PIP_USE_PEP517', 'true') + options = self.parse_args(['--no-use-pep517']) assert options.use_pep517 is False - def test_PIP_NO_USE_PEP517(self, capsys): + def test_PIP_NO_USE_PEP517(self, monkeypatch, capsys): """ Test setting PIP_NO_USE_PEP517, which isn't allowed. """ - expected_err = ( - '--no-use-pep517 error: A value was passed for --no-use-pep517,\n' - ) - with temp_environment_variable('PIP_NO_USE_PEP517', 'true'): - with assert_option_error(capsys, expected=expected_err): - self.parse_args([]) + monkeypatch.setenv('PIP_NO_USE_PEP517', 'true') + with assert_option_error(capsys, expected='--no-use-pep517 error'): + self.parse_args([]) class TestOptionsInterspersed(AddFakeCommandMixin): From 5b1093fc7521857715938cce13b347108e824b50 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Thu, 23 Jul 2020 15:01:37 +0700 Subject: [PATCH 2312/3170] Use monkeypatch.*env in conftest and tests.lib Session fixtures have to use mock.patch.dict though --- tests/conftest.py | 55 +++++++++++++++++++++++++------------------ tests/lib/__init__.py | 6 +---- 2 files changed, 33 insertions(+), 28 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 2aab50207be..1ca4d45d00e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,6 +10,7 @@ import pytest import six +from mock import patch from pip._vendor.contextlib2 import ExitStack, nullcontext from setuptools.wheel import Wheel @@ -104,11 +105,13 @@ def use_new_resolver(request): """Set environment variable to make pip default to the new resolver. """ new_resolver = request.config.getoption("--new-resolver") + features = set(os.environ.get("PIP_USE_FEATURE", "").split()) if new_resolver: - os.environ["PIP_USE_FEATURE"] = "2020-resolver" + features.add("2020-resolver") else: - os.environ.pop("PIP_USE_FEATURE", None) - yield new_resolver + features.discard("2020-resolver") + with patch.dict(os.environ, {"PIP_USE_FEATURE", " ".join(features)}): + yield new_resolver @pytest.fixture(scope='session') @@ -151,7 +154,7 @@ def tmpdir(request, tmpdir): @pytest.fixture(autouse=True) -def isolate(tmpdir): +def isolate(tmpdir, monkeypatch): """ Isolate our tests so that things like global configuration files and the like do not affect our test results. @@ -174,45 +177,51 @@ def isolate(tmpdir): if sys.platform == 'win32': # Note: this will only take effect in subprocesses... home_drive, home_path = os.path.splitdrive(home_dir) - os.environ.update({ - 'USERPROFILE': home_dir, - 'HOMEDRIVE': home_drive, - 'HOMEPATH': home_path, - }) + monkeypatch.setenv('USERPROFILE', home_dir) + monkeypatch.setenv('HOMEDRIVE', home_drive) + monkeypatch.setenv('HOMEPATH', home_path) for env_var, sub_path in ( ('APPDATA', 'AppData/Roaming'), ('LOCALAPPDATA', 'AppData/Local'), ): path = os.path.join(home_dir, *sub_path.split('/')) - os.environ[env_var] = path + monkeypatch.setenv(env_var, path) os.makedirs(path) else: # Set our home directory to our temporary directory, this should force # all of our relative configuration files to be read from here instead # of the user's actual $HOME directory. - os.environ["HOME"] = home_dir + monkeypatch.setenv("HOME", home_dir) # Isolate ourselves from XDG directories - os.environ["XDG_DATA_HOME"] = os.path.join(home_dir, ".local", "share") - os.environ["XDG_CONFIG_HOME"] = os.path.join(home_dir, ".config") - os.environ["XDG_CACHE_HOME"] = os.path.join(home_dir, ".cache") - os.environ["XDG_RUNTIME_DIR"] = os.path.join(home_dir, ".runtime") - os.environ["XDG_DATA_DIRS"] = ":".join([ + monkeypatch.setenv("XDG_DATA_HOME", os.path.join( + home_dir, ".local", "share", + )) + monkeypatch.setenv("XDG_CONFIG_HOME", os.path.join( + home_dir, ".config", + )) + monkeypatch.setenv("XDG_CACHE_HOME", os.path.join(home_dir, ".cache")) + monkeypatch.setenv("XDG_RUNTIME_DIR", os.path.join( + home_dir, ".runtime", + )) + monkeypatch.setenv("XDG_DATA_DIRS", os.pathsep.join([ os.path.join(fake_root, "usr", "local", "share"), os.path.join(fake_root, "usr", "share"), - ]) - os.environ["XDG_CONFIG_DIRS"] = os.path.join(fake_root, "etc", "xdg") + ])) + monkeypatch.setenv("XDG_CONFIG_DIRS", os.path.join( + fake_root, "etc", "xdg", + )) # Configure git, because without an author name/email git will complain # and cause test failures. - os.environ["GIT_CONFIG_NOSYSTEM"] = "1" - os.environ["GIT_AUTHOR_NAME"] = "pip" - os.environ["GIT_AUTHOR_EMAIL"] = "distutils-sig@python.org" + monkeypatch.setenv("GIT_CONFIG_NOSYSTEM", "1") + monkeypatch.setenv("GIT_AUTHOR_NAME", "pip") + monkeypatch.setenv("GIT_AUTHOR_EMAIL", "distutils-sig@python.org") # We want to disable the version check from running in the tests - os.environ["PIP_DISABLE_PIP_VERSION_CHECK"] = "true" + monkeypatch.setenv("PIP_DISABLE_PIP_VERSION_CHECK", "true") # Make sure tests don't share a requirements tracker. - os.environ.pop('PIP_REQ_TRACKER', None) + monkeypatch.delenv("PIP_REQ_TRACKER", False) # FIXME: Windows... os.makedirs(os.path.join(home_dir, ".config", "git")) diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index a8ea8676171..ee243d0e6f1 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -492,10 +492,7 @@ def __init__(self, base_path, *args, **kwargs): kwargs.setdefault("cwd", self.scratch_path) # Setup our environment - environ = kwargs.get("environ") - if environ is None: - environ = os.environ.copy() - + environ = kwargs.setdefault("environ", os.environ.copy()) environ["PATH"] = Path.pathsep.join( [self.bin_path] + [environ.get("PATH", [])], ) @@ -504,7 +501,6 @@ def __init__(self, base_path, *args, **kwargs): environ["PYTHONDONTWRITEBYTECODE"] = "1" # Make sure we get UTF-8 on output, even on Windows... environ["PYTHONIOENCODING"] = "UTF-8" - kwargs["environ"] = environ # Whether all pip invocations should expect stderr # (useful for Python version deprecation) From f887252d33106bdd15aabfafe80ee29142be5d78 Mon Sep 17 00:00:00 2001 From: Emmanuel Arias <eamanu@yaerobi.com> Date: Sat, 25 Jul 2020 14:03:31 -0300 Subject: [PATCH 2313/3170] use global instead of system-wide specific on doc that global config file is shared by all python installation --- docs/html/user_guide.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 16585f12db1..9efa9703cba 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -380,17 +380,19 @@ variable ``PIP_CONFIG_FILE``. :file:`C:\\Documents and Settings\\All Users\\Application Data\\pip\\pip.ini` * On Windows 7 and later the file is hidden, but writeable at :file:`C:\\ProgramData\\pip\\pip.ini` -* System-wide configuration is not supported on Windows Vista +* Global configuration is not supported on Windows Vista. + +The global configuration file is shared by whole Python installation. If multiple configuration files are found by pip then they are combined in the following order: -1. The system-wide file is read +1. The global file is read 2. The per-user file is read 3. The virtualenv-specific file is read Each file read overrides any values read from previous files, so if the -global timeout is specified in both the system-wide file and the per-user file +global timeout is specified in both the global file and the per-user file then the latter value will be used. The names of the settings are derived from the long command line option, e.g. From 9671830c3e1f474203fee4f9638dca479e07ed3a Mon Sep 17 00:00:00 2001 From: Emmanuel Arias <eamanu@yaerobi.com> Date: Sat, 25 Jul 2020 18:15:01 -0300 Subject: [PATCH 2314/3170] Update docs/html/user_guide.rst Co-authored-by: Julian Berman <Julian@GrayVines.com> --- docs/html/user_guide.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 9efa9703cba..f2e49fadf64 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -382,7 +382,7 @@ variable ``PIP_CONFIG_FILE``. :file:`C:\\ProgramData\\pip\\pip.ini` * Global configuration is not supported on Windows Vista. -The global configuration file is shared by whole Python installation. +The global configuration file is shared by all Python installations. If multiple configuration files are found by pip then they are combined in the following order: From 906072a29277bc1727271ac121886e5158085d03 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sun, 26 Jul 2020 15:58:38 +0800 Subject: [PATCH 2315/3170] Test for conflict message from requirements.txt --- tests/functional/test_new_resolver_errors.py | 26 ++++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 tests/functional/test_new_resolver_errors.py diff --git a/tests/functional/test_new_resolver_errors.py b/tests/functional/test_new_resolver_errors.py new file mode 100644 index 00000000000..267f2130710 --- /dev/null +++ b/tests/functional/test_new_resolver_errors.py @@ -0,0 +1,26 @@ +from tests.lib import create_basic_wheel_for_package + + +def test_new_resolver_conflict_requirements_file(tmpdir, script): + create_basic_wheel_for_package(script, "base", "1.0") + create_basic_wheel_for_package(script, "base", "2.0") + create_basic_wheel_for_package( + script, "pkga", "1.0", depends=["base==1.0"], + ) + create_basic_wheel_for_package( + script, "pkgb", "1.0", depends=["base==2.0"], + ) + + req_file = tmpdir.joinpath("requirements.txt") + req_file.write_text("pkga\npkgb") + + result = script.pip( + "install", "--use-feature=2020-resolver", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "-r", req_file, + expect_error=True, + ) + + message = "package versions have conflicting dependencies" + assert message in result.stderr, str(result) From 3ff9ee151d7d14c48122ac3c1f0114b91c2d40fb Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sun, 26 Jul 2020 15:59:42 +0800 Subject: [PATCH 2316/3170] Account for comes_from string --- .../resolution/resolvelib/factory.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index d10e2eb0009..bd7e3efd9d3 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -11,6 +11,7 @@ UnsupportedWheel, ) from pip._internal.models.wheel import Wheel +from pip._internal.req.req_install import InstallRequirement from pip._internal.utils.compatibility_tags import get_supported from pip._internal.utils.hashes import Hashes from pip._internal.utils.misc import ( @@ -56,7 +57,6 @@ from pip._internal.index.package_finder import PackageFinder from pip._internal.models.link import Link from pip._internal.operations.prepare import RequirementPreparer - from pip._internal.req.req_install import InstallRequirement from pip._internal.resolution.base import InstallRequirementProvider from .base import Candidate, Requirement @@ -406,22 +406,22 @@ def readable_form(cand): # type: (Candidate) -> str return "{} {}".format(cand.name, cand.version) + def describe_trigger(parent): + # type: (Candidate) -> str + ireq = parent.get_install_requirement() + if not ireq or not ireq.comes_from: + return "{} {}".format(parent.name, parent.version) + if isinstance(ireq.comes_from, InstallRequirement): + return str(ireq.comes_from.name) + return str(ireq.comes_from) + triggers = [] for req, parent in e.causes: if parent is None: # This is a root requirement, so we can report it directly trigger = req.format_for_error() else: - ireq = parent.get_install_requirement() - if ireq and ireq.comes_from: - trigger = "{}".format( - ireq.comes_from.name - ) - else: - trigger = "{} {}".format( - parent.name, - parent.version - ) + trigger = describe_trigger(parent) triggers.append(trigger) if triggers: From 36d250da115a068c3a2daaf1bc92ba03f08f3201 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sun, 26 Jul 2020 16:01:39 +0800 Subject: [PATCH 2317/3170] News --- news/8625.trivial | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 news/8625.trivial diff --git a/news/8625.trivial b/news/8625.trivial new file mode 100644 index 00000000000..946fa4602f4 --- /dev/null +++ b/news/8625.trivial @@ -0,0 +1,2 @@ +Fix 2020 resolver error message when conflicting packages are specified +directly in a requirements file. From b795c9a7d6b39eaa5138d51a78d307fbaee96573 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Sun, 26 Jul 2020 17:13:04 +0700 Subject: [PATCH 2318/3170] Abstract away AbstractDistribution in higher-level resolver code --- ...4ae133-3a6d-4a6c-a4c3-fe8d78223498.trivial | 0 src/pip/_internal/operations/prepare.py | 35 +++++++++---------- .../_internal/resolution/legacy/resolver.py | 18 ++++------ .../resolution/resolvelib/candidates.py | 16 ++++----- 4 files changed, 30 insertions(+), 39 deletions(-) create mode 100644 news/094ae133-3a6d-4a6c-a4c3-fe8d78223498.trivial diff --git a/news/094ae133-3a6d-4a6c-a4c3-fe8d78223498.trivial b/news/094ae133-3a6d-4a6c-a4c3-fe8d78223498.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index a5455fcc8e7..e6a34172d79 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -44,8 +44,8 @@ ) from mypy_extensions import TypedDict + from pip._vendor.pkg_resources import Distribution - from pip._internal.distributions import AbstractDistribution from pip._internal.index.package_finder import PackageFinder from pip._internal.models.link import Link from pip._internal.network.download import Downloader @@ -78,18 +78,17 @@ def _get_prepared_distribution( - req, # type: InstallRequirement - req_tracker, # type: RequirementTracker - finder, # type: PackageFinder - build_isolation # type: bool + req, # type: InstallRequirement + req_tracker, # type: RequirementTracker + finder, # type: PackageFinder + build_isolation, # type: bool ): - # type: (...) -> AbstractDistribution - """Prepare a distribution for installation. - """ + # type: (...) -> Distribution + """Prepare a distribution for installation.""" abstract_dist = make_distribution_for_install_requirement(req) with req_tracker.track(req): abstract_dist.prepare_distribution_metadata(finder, build_isolation) - return abstract_dist + return abstract_dist.get_pkg_resources_distribution() def unpack_vcs_link(link, location): @@ -450,7 +449,7 @@ def _get_linked_req_hashes(self, req): return req.hashes(trust_internet=False) or MissingHashes() def prepare_linked_requirement(self, req, parallel_builds=False): - # type: (InstallRequirement, bool) -> AbstractDistribution + # type: (InstallRequirement, bool) -> Distribution """Prepare a requirement to be obtained from req.link.""" assert req.link link = req.link @@ -479,7 +478,7 @@ def prepare_linked_requirement(self, req, parallel_builds=False): if local_file: req.local_file_path = local_file.path - abstract_dist = _get_prepared_distribution( + dist = _get_prepared_distribution( req, self.req_tracker, self.finder, self.build_isolation, ) @@ -499,13 +498,13 @@ def prepare_linked_requirement(self, req, parallel_builds=False): # Make a .zip of the source_dir we already created. if link.is_vcs: req.archive(self.download_dir) - return abstract_dist + return dist def prepare_editable_requirement( self, req, # type: InstallRequirement ): - # type: (...) -> AbstractDistribution + # type: (...) -> Distribution """Prepare an editable requirement """ assert req.editable, "cannot prepare a non-editable req as editable" @@ -522,7 +521,7 @@ def prepare_editable_requirement( req.ensure_has_source_dir(self.src_dir) req.update_editable(not self._download_should_save) - abstract_dist = _get_prepared_distribution( + dist = _get_prepared_distribution( req, self.req_tracker, self.finder, self.build_isolation, ) @@ -530,14 +529,14 @@ def prepare_editable_requirement( req.archive(self.download_dir) req.check_if_exists(self.use_user_site) - return abstract_dist + return dist def prepare_installed_requirement( self, req, # type: InstallRequirement skip_reason # type: str ): - # type: (...) -> AbstractDistribution + # type: (...) -> Distribution """Prepare an already-installed requirement """ assert req.satisfied_by, "req should have been satisfied but isn't" @@ -557,6 +556,4 @@ def prepare_installed_requirement( 'completely repeatable environment, install into an ' 'empty virtualenv.' ) - abstract_dist = InstalledDistribution(req) - - return abstract_dist + return InstalledDistribution(req).get_pkg_resources_distribution() diff --git a/src/pip/_internal/resolution/legacy/resolver.py b/src/pip/_internal/resolution/legacy/resolver.py index c9b4c661630..a743b569694 100644 --- a/src/pip/_internal/resolution/legacy/resolver.py +++ b/src/pip/_internal/resolution/legacy/resolver.py @@ -42,10 +42,9 @@ if MYPY_CHECK_RUNNING: from typing import DefaultDict, List, Optional, Set, Tuple - from pip._vendor import pkg_resources + from pip._vendor.pkg_resources import Distribution from pip._internal.cache import WheelCache - from pip._internal.distributions import AbstractDistribution from pip._internal.index.package_finder import PackageFinder from pip._internal.models.link import Link from pip._internal.operations.prepare import RequirementPreparer @@ -58,7 +57,7 @@ def _check_dist_requires_python( - dist, # type: pkg_resources.Distribution + dist, # type: Distribution version_info, # type: Tuple[int, int, int] ignore_requires_python=False, # type: bool ): @@ -317,8 +316,8 @@ def _populate_link(self, req): req.original_link_is_in_wheel_cache = True req.link = cache_entry.link - def _get_abstract_dist_for(self, req): - # type: (InstallRequirement) -> AbstractDistribution + def _get_dist_for(self, req): + # type: (InstallRequirement) -> Distribution """Takes a InstallRequirement and returns a single AbstractDist \ representing a prepared variant of the same. """ @@ -337,7 +336,7 @@ def _get_abstract_dist_for(self, req): # We eagerly populate the link, since that's our "legacy" behavior. self._populate_link(req) - abstract_dist = self.preparer.prepare_linked_requirement(req) + dist = self.preparer.prepare_linked_requirement(req) # NOTE # The following portion is for determining if a certain package is @@ -364,8 +363,7 @@ def _get_abstract_dist_for(self, req): 'Requirement already satisfied (use --upgrade to upgrade):' ' %s', req, ) - - return abstract_dist + return dist def _resolve_one( self, @@ -385,10 +383,8 @@ def _resolve_one( req_to_install.prepared = True - abstract_dist = self._get_abstract_dist_for(req_to_install) - # Parse and return dependencies - dist = abstract_dist.get_pkg_resources_distribution() + dist = self._get_dist_for(req_to_install) # This will raise UnsupportedPythonVersion if the given Python # version isn't compatible with the distribution's Requires-Python. _check_dist_requires_python( diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index c289bb5839c..3d8e399a7bf 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -29,7 +29,6 @@ from pip._vendor.packaging.version import _BaseVersion from pip._vendor.pkg_resources import Distribution - from pip._internal.distributions import AbstractDistribution from pip._internal.models.link import Link from .base import Requirement @@ -200,8 +199,8 @@ def format_for_error(self): self._link.file_path if self._link.is_file else self._link ) - def _prepare_abstract_distribution(self): - # type: () -> AbstractDistribution + def _prepare_distribution(self): + # type: () -> Distribution raise NotImplementedError("Override in subclass") def _check_metadata_consistency(self): @@ -222,12 +221,11 @@ def _prepare(self): if self._prepared: return try: - abstract_dist = self._prepare_abstract_distribution() + self._dist = self._prepare_distribution() except HashError as e: e.req = self._ireq raise - self._dist = abstract_dist.get_pkg_resources_distribution() assert self._dist is not None, "Distribution already installed" self._check_metadata_consistency() self._prepared = True @@ -324,8 +322,8 @@ def __init__( version=version, ) - def _prepare_abstract_distribution(self): - # type: () -> AbstractDistribution + def _prepare_distribution(self): + # type: () -> Distribution return self._factory.preparer.prepare_linked_requirement( self._ireq, parallel_builds=True, ) @@ -352,8 +350,8 @@ def __init__( version=version, ) - def _prepare_abstract_distribution(self): - # type: () -> AbstractDistribution + def _prepare_distribution(self): + # type: () -> Distribution return self._factory.preparer.prepare_editable_requirement(self._ireq) From d77b5c234cdfb1c605bef22f3975fea191efe913 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Fri, 17 Jul 2020 03:23:28 +0530 Subject: [PATCH 2319/3170] Refactor the logging calls into a dedicated loop --- src/pip/_internal/commands/install.py | 29 ++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 454c0b70c4c..bf3d07bde24 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -539,24 +539,39 @@ def _determine_conflicts(self, to_install): def _warn_about_conflicts(self, conflict_details): # type: (ConflictDetails) -> None package_set, (missing, conflicting) = conflict_details + parts = [] # type: List[str] # NOTE: There is some duplication here, with commands/check.py 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], + message = ( + "{name} {version} requires {requirement}, " + "which is not installed." + ).format( + name=project_name, + version=version, + requirement=dependency[1], ) + parts.append(message) 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, + message = ( + "{name} {version} requires {requirement}, but you'll have " + "{dep_name} {dep_version} which is incompatible." + ).format( + name=project_name, + version=version, + requirement=req, + dep_name=dep_name, + dep_version=dep_version, ) + parts.append(message) + + for message in parts: + logger.critical(message) def get_lib_location_guesses( From 42c62a08f7cf5d7debfd84d4358ccf5a0ae8f583 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Fri, 17 Jul 2020 04:01:46 +0530 Subject: [PATCH 2320/3170] Short circuit when there's nothing to report --- src/pip/_internal/commands/install.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index bf3d07bde24..9edababe415 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -539,6 +539,9 @@ def _determine_conflicts(self, to_install): def _warn_about_conflicts(self, conflict_details): # type: (ConflictDetails) -> None package_set, (missing, conflicting) = conflict_details + if not missing and not conflicting: + return + parts = [] # type: List[str] # NOTE: There is some duplication here, with commands/check.py From efdb66ed16d6794b04a1f0acf3e11bdb495b888c Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Fri, 17 Jul 2020 06:11:48 +0530 Subject: [PATCH 2321/3170] Add messaging variation based on "new resolver" usage --- src/pip/_internal/commands/install.py | 35 +++++++++++++++++++++++---- 1 file changed, 30 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 9edababe415..2aa9a34e7ea 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -444,7 +444,10 @@ def run(self, options, args): items.append(item) if conflicts is not None: - self._warn_about_conflicts(conflicts) + self._warn_about_conflicts( + conflicts, + new_resolver='2020-resolver' in options.features_enabled, + ) installed_desc = ' '.join(items) if installed_desc: @@ -536,13 +539,36 @@ def _determine_conflicts(self, to_install): ) return None - def _warn_about_conflicts(self, conflict_details): - # type: (ConflictDetails) -> None + def _warn_about_conflicts(self, conflict_details, new_resolver): + # type: (ConflictDetails, bool) -> None package_set, (missing, conflicting) = conflict_details if not missing and not conflicting: return parts = [] # type: List[str] + if new_resolver: + # NOTE: trailing newlines here are intentional + parts.append( + "Pip will install or upgrade your package(s) and its " + "dependencies without taking into account other packages you " + "already have installed. This may cause an uncaught " + "dependency conflict.\n" + ) + parts.append( + "If you would like pip to take your other packages into " + "account, please tell us here: https://forms.gle/cWKMoDs8sUVE29hz9\n" + ) + else: + parts.append( + "After October 2020 you may experience errors when installing " + "or updating packages. This is because pip will change the " + "way that it resolves dependency conflicts.\n" + ) + parts.append( + "We recommend you use --use-feature=2020-resolver to test " + "your packages with the new resolver before it becomes the " + "default.\n" + ) # NOTE: There is some duplication here, with commands/check.py for project_name in missing: @@ -573,8 +599,7 @@ def _warn_about_conflicts(self, conflict_details): ) parts.append(message) - for message in parts: - logger.critical(message) + logger.critical("\n".join(parts)) def get_lib_location_guesses( From 43da7c82834d524247124f1477852c870e887263 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Fri, 17 Jul 2020 06:22:08 +0530 Subject: [PATCH 2322/3170] Make the YAML tests happier --- tests/yaml/conflict_1.yml | 7 ++----- tests/yaml/conflicting_triangle.yml | 4 +--- 2 files changed, 3 insertions(+), 8 deletions(-) diff --git a/tests/yaml/conflict_1.yml b/tests/yaml/conflict_1.yml index 847bb2b6af5..df1c6818be6 100644 --- a/tests/yaml/conflict_1.yml +++ b/tests/yaml/conflict_1.yml @@ -11,12 +11,9 @@ cases: response: - error: code: 0 - stderr: ['requirement', 'is\s+incompatible'] + stderr: ['dependency', 'incompatible'] skip: old - # -- currently the error message is: - # a 1.0.0 has requirement B==1.0.0, but you'll have b 2.0.0 which is - # incompatible. - # -- better would be: + # -- a good error message would be: # A 1.0.0 has incompatible requirements B==1.0.0, B==2.0.0 - diff --git a/tests/yaml/conflicting_triangle.yml b/tests/yaml/conflicting_triangle.yml index 666c37363db..e8e88b34730 100644 --- a/tests/yaml/conflicting_triangle.yml +++ b/tests/yaml/conflicting_triangle.yml @@ -14,7 +14,5 @@ cases: - C 1.0.0 - error: code: 0 - stderr: ['requirement c==1\.0\.0', 'is incompatible'] + stderr: ['c==1\.0\.0', 'incompatible'] skip: old - # -- currently the error message is: - # a 1.0.0 has requirement C==1.0.0, but you'll have c 2.0.0 which is incompatible. From 8db354260af5fb9de7135fc808fbab954401f03f Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Mon, 27 Jul 2020 13:29:00 +0530 Subject: [PATCH 2323/3170] Move the form link to make the linter happy --- src/pip/_internal/commands/install.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 2aa9a34e7ea..3739e20dfa2 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -554,9 +554,10 @@ def _warn_about_conflicts(self, conflict_details, new_resolver): "already have installed. This may cause an uncaught " "dependency conflict.\n" ) + form_link = "https://forms.gle/cWKMoDs8sUVE29hz9" parts.append( "If you would like pip to take your other packages into " - "account, please tell us here: https://forms.gle/cWKMoDs8sUVE29hz9\n" + "account, please tell us here: {}\n".format(form_link) ) else: parts.append( From 4e4951066d369ebb1923e50ecb87493012492b3d Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Mon, 27 Jul 2020 13:52:54 +0530 Subject: [PATCH 2324/3170] Reverse if statement's condition --- src/pip/_internal/commands/install.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 3739e20dfa2..3cefa5ae623 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -546,7 +546,18 @@ def _warn_about_conflicts(self, conflict_details, new_resolver): return parts = [] # type: List[str] - if new_resolver: + if not new_resolver: + parts.append( + "After October 2020 you may experience errors when installing " + "or updating packages. This is because pip will change the " + "way that it resolves dependency conflicts.\n" + ) + parts.append( + "We recommend you use --use-feature=2020-resolver to test " + "your packages with the new resolver before it becomes the " + "default.\n" + ) + else: # NOTE: trailing newlines here are intentional parts.append( "Pip will install or upgrade your package(s) and its " @@ -559,17 +570,6 @@ def _warn_about_conflicts(self, conflict_details, new_resolver): "If you would like pip to take your other packages into " "account, please tell us here: {}\n".format(form_link) ) - else: - parts.append( - "After October 2020 you may experience errors when installing " - "or updating packages. This is because pip will change the " - "way that it resolves dependency conflicts.\n" - ) - parts.append( - "We recommend you use --use-feature=2020-resolver to test " - "your packages with the new resolver before it becomes the " - "default.\n" - ) # NOTE: There is some duplication here, with commands/check.py for project_name in missing: From 9eb9319f7e0d5ab564be75ce5fab1f1317228c91 Mon Sep 17 00:00:00 2001 From: Laurie O <laurie_opperman@hotmail.com> Date: Mon, 27 Jul 2020 20:59:31 +1000 Subject: [PATCH 2325/3170] Document keyring support for index basic-auth --- docs/html/user_guide.rst | 15 +++++++++++++++ news/8636.doc | 1 + 2 files changed, 16 insertions(+) create mode 100644 news/8636.doc diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index f2e49fadf64..78a57d32aca 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -79,6 +79,21 @@ as the "username" and do not provide a password, for example - ``https://0123456789abcdef@pypi.company.com`` +Keyring Support +--------------- + +pip also supports credentials stored in your keyring using the `keyring`_ +library. + +.. code-block:: shell + + pip install keyring + echo your-password | keyring set pypi.company.com your-username + pip install your-package --extra-index-url https://pypi.company.com/ + +.. _keyring: https://pypi.org/project/keyring/ + + Using a Proxy Server ==================== diff --git a/news/8636.doc b/news/8636.doc new file mode 100644 index 00000000000..081cf1c7eb0 --- /dev/null +++ b/news/8636.doc @@ -0,0 +1 @@ +Add note and example on keyring support for index basic-auth From 1b2ae22e7b0b2d1963bec769d4f070bfbc6d932a Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Mon, 27 Jul 2020 14:13:59 +0530 Subject: [PATCH 2326/3170] Don't print that form link after the end of month. --- src/pip/_internal/commands/install.py | 3 ++- src/pip/_internal/utils/datetime.py | 12 ++++++++++++ 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 src/pip/_internal/utils/datetime.py diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 3cefa5ae623..8c2c32fd43f 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -21,6 +21,7 @@ from pip._internal.operations.check import check_install_conflicts from pip._internal.req import install_given_reqs from pip._internal.req.req_tracker import get_requirement_tracker +from pip._internal.utils.datetime import today_is_later_than from pip._internal.utils.deprecation import deprecated from pip._internal.utils.distutils_args import parse_distutils_args from pip._internal.utils.filesystem import test_writable_dir @@ -557,7 +558,7 @@ def _warn_about_conflicts(self, conflict_details, new_resolver): "your packages with the new resolver before it becomes the " "default.\n" ) - else: + elif not today_is_later_than(year=2020, month=7, day=31): # NOTE: trailing newlines here are intentional parts.append( "Pip will install or upgrade your package(s) and its " diff --git a/src/pip/_internal/utils/datetime.py b/src/pip/_internal/utils/datetime.py new file mode 100644 index 00000000000..b638646c8bb --- /dev/null +++ b/src/pip/_internal/utils/datetime.py @@ -0,0 +1,12 @@ +"""For when pip wants to check the date or time. +""" + +import datetime + + +def today_is_later_than(year, month, day): + # type: (int, int, int) -> bool + today = datetime.date.today() + given = datetime.date(year, month, day) + + return today > given From a89ede7da3ed1ae332095811877559443c0fc49b Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Mon, 27 Jul 2020 19:22:15 +0530 Subject: [PATCH 2327/3170] tests: Check only the last output lines --- tests/functional/test_check.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/tests/functional/test_check.py b/tests/functional/test_check.py index 4e9a144bf02..5cb41a97e72 100644 --- a/tests/functional/test_check.py +++ b/tests/functional/test_check.py @@ -3,9 +3,11 @@ def matches_expected_lines(string, expected_lines): # Ignore empty lines - output_lines = set(filter(None, string.splitlines())) - # Match regardless of order - return set(output_lines) == set(expected_lines) + output_lines = list(filter(None, string.splitlines())) + # We'll match the last n lines, given n lines to match. + last_few_output_lines = output_lines[-len(expected_lines):] + # And order does not matter + return set(last_few_output_lines) == set(expected_lines) def test_basic_check_clean(script): From e4f8e0a2b87bd3ad90fc883b6133512acbf9a0c8 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Mon, 27 Jul 2020 19:23:59 +0530 Subject: [PATCH 2328/3170] Change {matches -> contains}_expected_lines This also updates invocations that can't be translated as-is into a different equivalent check. --- tests/functional/test_install_check.py | 27 +++++++++----------------- 1 file changed, 9 insertions(+), 18 deletions(-) diff --git a/tests/functional/test_install_check.py b/tests/functional/test_install_check.py index 017d6256b6c..d13e93f5df0 100644 --- a/tests/functional/test_install_check.py +++ b/tests/functional/test_install_check.py @@ -1,10 +1,7 @@ from tests.lib import create_test_package_with_setup -def matches_expected_lines(string, expected_lines, exact=True): - if exact: - return set(string.splitlines()) == set(expected_lines) - # If not exact, check that all expected lines are present +def contains_expected_lines(string, expected_lines): return set(expected_lines) <= set(string.splitlines()) @@ -41,8 +38,7 @@ def test_check_install_canonicalization(script, deprecated_python): "ERROR: pkga 1.0 requires SPECIAL.missing, which is not installed.", ] # Deprecated python versions produce an extra warning on stderr - assert matches_expected_lines( - result.stderr, expected_lines, exact=not deprecated_python) + assert contains_expected_lines(result.stderr, expected_lines) assert result.returncode == 0 # Install the second missing package and expect that there is no warning @@ -51,8 +47,7 @@ def test_check_install_canonicalization(script, deprecated_python): result = script.pip( 'install', '--no-index', special_path, '--quiet', ) - assert matches_expected_lines( - result.stderr, [], exact=not deprecated_python) + assert "requires" not in result.stderr assert result.returncode == 0 # Double check that all errors are resolved in the end @@ -60,7 +55,7 @@ def test_check_install_canonicalization(script, deprecated_python): expected_lines = [ "No broken requirements found.", ] - assert matches_expected_lines(result.stdout, expected_lines) + assert contains_expected_lines(result.stdout, expected_lines) assert result.returncode == 0 @@ -85,33 +80,29 @@ def test_check_install_does_not_warn_for_out_of_graph_issues( # Install a package without it's dependencies result = script.pip('install', '--no-index', pkg_broken_path, '--no-deps') - # Deprecated python versions produce an extra warning on stderr - assert matches_expected_lines( - result.stderr, [], exact=not deprecated_python) + assert "requires" not in result.stderr # Install conflict package result = script.pip( 'install', '--no-index', pkg_conflict_path, allow_stderr_error=True, ) - assert matches_expected_lines(result.stderr, [ - "ERROR: broken 1.0 requires missing, which is not installed.", + assert contains_expected_lines(result.stderr, [ ( "ERROR: broken 1.0 has requirement conflict<1.0, but " "you'll have conflict 1.0 which is incompatible." ), - ], exact=not deprecated_python) + ]) # Install unrelated package result = script.pip( 'install', '--no-index', pkg_unrelated_path, '--quiet', ) # should not warn about broken's deps when installing unrelated package - assert matches_expected_lines( - result.stderr, [], exact=not deprecated_python) + assert "requires" not in result.stderr result = script.pip('check', expect_error=True) expected_lines = [ "broken 1.0 requires missing, which is not installed.", "broken 1.0 has requirement conflict<1.0, but you have conflict 1.0.", ] - assert matches_expected_lines(result.stdout, expected_lines) + assert contains_expected_lines(result.stdout, expected_lines) From c631de61b9ac2517bbe3b7f469f7ba59e80af437 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Mon, 27 Jul 2020 19:24:47 +0530 Subject: [PATCH 2329/3170] Drop no-longer-used deprecated_python fixture --- tests/functional/test_install_check.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tests/functional/test_install_check.py b/tests/functional/test_install_check.py index d13e93f5df0..060af80ef49 100644 --- a/tests/functional/test_install_check.py +++ b/tests/functional/test_install_check.py @@ -5,7 +5,7 @@ def contains_expected_lines(string, expected_lines): return set(expected_lines) <= set(string.splitlines()) -def test_check_install_canonicalization(script, deprecated_python): +def test_check_install_canonicalization(script): pkga_path = create_test_package_with_setup( script, name='pkgA', @@ -59,8 +59,7 @@ def test_check_install_canonicalization(script, deprecated_python): assert result.returncode == 0 -def test_check_install_does_not_warn_for_out_of_graph_issues( - script, deprecated_python): +def test_check_install_does_not_warn_for_out_of_graph_issues(script): pkg_broken_path = create_test_package_with_setup( script, name='broken', From b9ff93f7ba59cb49cb4ebe4bb5aa929bd2f7fbca Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Mon, 27 Jul 2020 19:25:19 +0530 Subject: [PATCH 2330/3170] Update test messages to reflect new reality --- tests/functional/test_install_check.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/functional/test_install_check.py b/tests/functional/test_install_check.py index 060af80ef49..88609422bb4 100644 --- a/tests/functional/test_install_check.py +++ b/tests/functional/test_install_check.py @@ -35,7 +35,7 @@ def test_check_install_canonicalization(script): allow_stderr_error=True, ) expected_lines = [ - "ERROR: pkga 1.0 requires SPECIAL.missing, which is not installed.", + "pkga 1.0 requires SPECIAL.missing, which is not installed.", ] # Deprecated python versions produce an extra warning on stderr assert contains_expected_lines(result.stderr, expected_lines) @@ -86,8 +86,9 @@ def test_check_install_does_not_warn_for_out_of_graph_issues(script): 'install', '--no-index', pkg_conflict_path, allow_stderr_error=True, ) assert contains_expected_lines(result.stderr, [ + "broken 1.0 requires missing, which is not installed.", ( - "ERROR: broken 1.0 has requirement conflict<1.0, but " + "broken 1.0 requires conflict<1.0, but " "you'll have conflict 1.0 which is incompatible." ), ]) From 3962f9d2b89a0bdb02bbf6c4d820f93e75353876 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Mon, 27 Jul 2020 19:44:28 +0530 Subject: [PATCH 2331/3170] Moar tests getting updated --- tests/functional/test_install.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 2657a8aeeb6..2185251d290 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1697,7 +1697,7 @@ def test_install_conflict_results_in_warning(script, data): result2 = script.pip( 'install', '--no-index', pkgB_path, allow_stderr_error=True, ) - assert "pkga 1.0 has requirement pkgb==1.0" in result2.stderr, str(result2) + assert "pkga 1.0 requires pkgb==1.0" in result2.stderr, str(result2) assert "Successfully installed pkgB-2.0" in result2.stdout, str(result2) From 3f88476e4fd046f572e7f5cc39d7fcdff328cb04 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Mon, 27 Jul 2020 20:06:55 +0530 Subject: [PATCH 2332/3170] This counts as a fix right? --- tests/yaml/conflict_1.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/yaml/conflict_1.yml b/tests/yaml/conflict_1.yml index df1c6818be6..c0a56199541 100644 --- a/tests/yaml/conflict_1.yml +++ b/tests/yaml/conflict_1.yml @@ -11,7 +11,7 @@ cases: response: - error: code: 0 - stderr: ['dependency', 'incompatible'] + stderr: ['incompatible'] skip: old # -- a good error message would be: # A 1.0.0 has incompatible requirements B==1.0.0, B==2.0.0 From b6c99afaded382e2cf189777633d5af01622f33e Mon Sep 17 00:00:00 2001 From: Emmanuel Arias <eamanu@yaerobi.com> Date: Mon, 27 Jul 2020 13:26:41 -0300 Subject: [PATCH 2333/3170] Add note explaining the Docs PR deploy (#8622) Add a note to explain that documentation is deployed readthedocs for each PR. --- docs/html/development/getting-started.rst | 7 +++++++ news/21984b58-7136-4ef8-a313-bad87fe5e907.trivial | 0 2 files changed, 7 insertions(+) create mode 100644 news/21984b58-7136-4ef8-a313-bad87fe5e907.trivial diff --git a/docs/html/development/getting-started.rst b/docs/html/development/getting-started.rst index 8b7900fb554..326543202f6 100644 --- a/docs/html/development/getting-started.rst +++ b/docs/html/development/getting-started.rst @@ -129,6 +129,13 @@ To build it locally, run: The built documentation can be found in the ``docs/build`` folder. +For each Pull Request made the documentation is deployed following this link: + +.. code-block:: none + + https://pip--<PR-NUMBER>.org.readthedocs.build/en/<PR-NUMBER> + + What Next? ========== diff --git a/news/21984b58-7136-4ef8-a313-bad87fe5e907.trivial b/news/21984b58-7136-4ef8-a313-bad87fe5e907.trivial new file mode 100644 index 00000000000..e69de29bb2d From bac3c8eb9ac9dc7d2454ee3e2f6695d5812babf7 Mon Sep 17 00:00:00 2001 From: Laurie O <laurie_opperman@hotmail.com> Date: Tue, 28 Jul 2020 10:42:01 +1000 Subject: [PATCH 2334/3170] Explicitly note that pip does not vendor keyring --- docs/html/user_guide.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 78a57d32aca..b021cb6dcf0 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -83,7 +83,8 @@ Keyring Support --------------- pip also supports credentials stored in your keyring using the `keyring`_ -library. +library. Note that ``keyring`` will need to be installed separately, as pip +does not come with it included. .. code-block:: shell From 2439d80a83d9cc23982972b9c9f9c8458774382f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Mon, 13 Jul 2020 11:39:03 +0700 Subject: [PATCH 2335/3170] Allow specifying verbose/quiet level via config file and env var --- news/8578.bugfix | 4 ++++ src/pip/_internal/cli/parser.py | 36 ++++++++++++++++----------------- 2 files changed, 22 insertions(+), 18 deletions(-) create mode 100644 news/8578.bugfix diff --git a/news/8578.bugfix b/news/8578.bugfix new file mode 100644 index 00000000000..3df7ed078cf --- /dev/null +++ b/news/8578.bugfix @@ -0,0 +1,4 @@ +Allow specifying verbosity and quiet level via configuration files +and environment variables. Previously these options were treated as +boolean values when read from there while through CLI the level can be +specified. diff --git a/src/pip/_internal/cli/parser.py b/src/pip/_internal/cli/parser.py index 04e00b72132..b6b78318a7a 100644 --- a/src/pip/_internal/cli/parser.py +++ b/src/pip/_internal/cli/parser.py @@ -11,6 +11,7 @@ import textwrap from distutils.util import strtobool +from pip._vendor.contextlib2 import suppress from pip._vendor.six import string_types from pip._internal.cli.status_codes import UNKNOWN_ERROR @@ -197,15 +198,27 @@ def _update_defaults(self, defaults): if option is None: continue - if option.action in ('store_true', 'store_false', 'count'): + if option.action in ('store_true', 'store_false'): try: val = strtobool(val) except ValueError: - error_msg = invalid_config_error_message( - option.action, key, val + self.error( + '{} is not a valid value for {} option, ' # noqa + 'please specify a boolean value like yes/no, ' + 'true/false or 1/0 instead.'.format(val, key) + ) + elif option.action == 'count': + with suppress(ValueError): + val = strtobool(val) + with suppress(ValueError): + val = int(val) + if not isinstance(val, int) or val < 0: + self.error( + '{} is not a valid value for {} option, ' # noqa + 'please instead specify either a non-negative integer ' + 'or a boolean value like yes/no or false/true ' + 'which is equivalent to 1/0.'.format(val, key) ) - self.error(error_msg) - elif option.action == 'append': val = val.split() val = [self.check_default(option, key, v) for v in val] @@ -251,16 +264,3 @@ def get_default_values(self): def error(self, msg): self.print_usage(sys.stderr) self.exit(UNKNOWN_ERROR, "{}\n".format(msg)) - - -def invalid_config_error_message(action, key, val): - """Returns a better error message when invalid configuration option - is provided.""" - if action in ('store_true', 'store_false'): - return ("{0} is not a valid value for {1} option, " - "please specify a boolean value like yes/no, " - "true/false or 1/0 instead.").format(val, key) - - return ("{0} is not a valid value for {1} option, " - "please specify a numerical value like 1/0 " - "instead.").format(val, key) From 7a0061d8864d5fab7abc3d0d82ad01a43a0d23cc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Wed, 15 Jul 2020 15:26:04 +0700 Subject: [PATCH 2336/3170] Update docs for setting verbose/quiet in config file or env var Co-authored-by: Paul Moore <p.f.moore@gmail.com> Co-authored-by: Prashant Sharma <prashantsharma161198@gmail.com> Co-authored-by: Xavier Fernandez <xav.fernandez@gmail.com> --- docs/html/user_guide.rst | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index f2e49fadf64..ddaaccd57e7 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -442,6 +442,15 @@ and ``--no-cache-dir``, falsy values have to be used: no-compile = no no-warn-script-location = false +For options which can be repeated like ``--verbose`` and ``--quiet``, +a non-negative integer can be used to represent the level to be specified: + +.. code-block:: ini + + [global] + quiet = 0 + verbose = 2 + It is possible to append values to a section within a configuration file such as the pip.ini file. This is applicable to appending options like ``--find-links`` or ``--trusted-host``, which can be written on multiple lines: @@ -488,6 +497,15 @@ is the same as calling:: pip install --find-links=http://mirror1.example.com --find-links=http://mirror2.example.com +Options that do not take a value, but can be repeated (such as ``--verbose``) +can be specified using the number of repetitions, so:: + + export PIP_VERBOSE=3 + +is the same as calling:: + + pip install -vvv + .. note:: Environment variables set to be empty string will not be treated as false. From a85be3f5557e49f9e35e8abcf9365dd3b6cfabab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Wed, 22 Jul 2020 17:58:27 +0700 Subject: [PATCH 2337/3170] Test verbose/quiet level specified via env var and config file --- tests/unit/test_options.py | 120 +++++++++++++++++++++++++++++++------ 1 file changed, 102 insertions(+), 18 deletions(-) diff --git a/tests/unit/test_options.py b/tests/unit/test_options.py index ce4fc9c25d1..df174ea67bc 100644 --- a/tests/unit/test_options.py +++ b/tests/unit/test_options.py @@ -1,5 +1,6 @@ import os from contextlib import contextmanager +from tempfile import NamedTemporaryFile import pytest @@ -286,6 +287,107 @@ def test_subcommand_option_before_subcommand_fails(self): main(['--find-links', 'F1', 'fake']) +@contextmanager +def tmpconfig(option, value, section='global'): + with NamedTemporaryFile(mode='w', delete=False) as f: + f.write('[{}]\n{}={}\n'.format(section, option, value)) + name = f.name + try: + yield name + finally: + os.unlink(name) + + +class TestCountOptions(AddFakeCommandMixin): + + @pytest.mark.parametrize('option', ('verbose', 'quiet')) + @pytest.mark.parametrize('value', range(4)) + def test_cli_long(self, option, value): + flags = ['--{}'.format(option)] * value + opt1, args1 = main(flags+['fake']) + opt2, args2 = main(['fake']+flags) + assert getattr(opt1, option) == getattr(opt2, option) == value + + @pytest.mark.parametrize('option', ('verbose', 'quiet')) + @pytest.mark.parametrize('value', range(1, 4)) + def test_cli_short(self, option, value): + flag = '-' + option[0]*value + opt1, args1 = main([flag, 'fake']) + opt2, args2 = main(['fake', flag]) + assert getattr(opt1, option) == getattr(opt2, option) == value + + @pytest.mark.parametrize('option', ('verbose', 'quiet')) + @pytest.mark.parametrize('value', range(4)) + def test_env_var(self, option, value, monkeypatch): + monkeypatch.setenv('PIP_'+option.upper(), str(value)) + assert getattr(main(['fake'])[0], option) == value + + @pytest.mark.parametrize('option', ('verbose', 'quiet')) + @pytest.mark.parametrize('value', range(3)) + def test_env_var_integrate_cli(self, option, value, monkeypatch): + monkeypatch.setenv('PIP_'+option.upper(), str(value)) + assert getattr(main(['fake', '--'+option])[0], option) == value + 1 + + @pytest.mark.parametrize('option', ('verbose', 'quiet')) + @pytest.mark.parametrize('value', (-1, 'foobar')) + def test_env_var_invalid(self, option, value, monkeypatch, capsys): + monkeypatch.setenv('PIP_'+option.upper(), str(value)) + with assert_option_error(capsys, expected='a non-negative integer'): + main(['fake']) + + # Undocumented, support for backward compatibility + @pytest.mark.parametrize('option', ('verbose', 'quiet')) + @pytest.mark.parametrize('value', ('no', 'false')) + def test_env_var_false(self, option, value, monkeypatch): + monkeypatch.setenv('PIP_'+option.upper(), str(value)) + assert getattr(main(['fake'])[0], option) == 0 + + # Undocumented, support for backward compatibility + @pytest.mark.parametrize('option', ('verbose', 'quiet')) + @pytest.mark.parametrize('value', ('yes', 'true')) + def test_env_var_true(self, option, value, monkeypatch): + monkeypatch.setenv('PIP_'+option.upper(), str(value)) + assert getattr(main(['fake'])[0], option) == 1 + + @pytest.mark.parametrize('option', ('verbose', 'quiet')) + @pytest.mark.parametrize('value', range(4)) + def test_config_file(self, option, value, monkeypatch): + with tmpconfig(option, value) as name: + monkeypatch.setenv('PIP_CONFIG_FILE', name) + assert getattr(main(['fake'])[0], option) == value + + @pytest.mark.parametrize('option', ('verbose', 'quiet')) + @pytest.mark.parametrize('value', range(3)) + def test_config_file_integrate_cli(self, option, value, monkeypatch): + with tmpconfig(option, value) as name: + monkeypatch.setenv('PIP_CONFIG_FILE', name) + assert getattr(main(['fake', '--'+option])[0], option) == value + 1 + + @pytest.mark.parametrize('option', ('verbose', 'quiet')) + @pytest.mark.parametrize('value', (-1, 'foobar')) + def test_config_file_invalid(self, option, value, monkeypatch, capsys): + with tmpconfig(option, value) as name: + monkeypatch.setenv('PIP_CONFIG_FILE', name) + with assert_option_error(capsys, expected='non-negative integer'): + main(['fake']) + + # Undocumented, support for backward compatibility + @pytest.mark.parametrize('option', ('verbose', 'quiet')) + @pytest.mark.parametrize('value', ('no', 'false')) + def test_config_file_false(self, option, value, monkeypatch): + with tmpconfig(option, value) as name: + monkeypatch.setenv('PIP_CONFIG_FILE', name) + assert getattr(main(['fake'])[0], option) == 0 + + # Undocumented, support for backward compatibility + @pytest.mark.parametrize('option', ('verbose', 'quiet')) + @pytest.mark.parametrize('value', ('yes', 'true')) + def test_config_file_true(self, option, value, monkeypatch): + with tmpconfig(option, value) as name: + monkeypatch.setenv('PIP_CONFIG_FILE', name) + assert getattr(main(['fake'])[0], option) == 1 + + class TestGeneralOptions(AddFakeCommandMixin): # the reason to specifically test general options is due to the @@ -310,24 +412,6 @@ def test_require_virtualenv(self): assert options1.require_venv assert options2.require_venv - def test_verbose(self): - options1, args1 = main(['--verbose', 'fake']) - options2, args2 = main(['fake', '--verbose']) - assert options1.verbose == options2.verbose == 1 - - def test_quiet(self): - options1, args1 = main(['--quiet', 'fake']) - options2, args2 = main(['fake', '--quiet']) - assert options1.quiet == options2.quiet == 1 - - options3, args3 = main(['--quiet', '--quiet', 'fake']) - options4, args4 = main(['fake', '--quiet', '--quiet']) - assert options3.quiet == options4.quiet == 2 - - options5, args5 = main(['--quiet', '--quiet', '--quiet', 'fake']) - options6, args6 = main(['fake', '--quiet', '--quiet', '--quiet']) - assert options5.quiet == options6.quiet == 3 - def test_log(self): options1, args1 = main(['--log', 'path', 'fake']) options2, args2 = main(['fake', '--log', 'path']) From 9033824cbcc4b6ed0cc29d0f4fbd3af721121695 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Tue, 28 Jul 2020 11:23:39 +0530 Subject: [PATCH 2338/3170] Python 2 *sigh* --- src/pip/_internal/utils/datetime.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pip/_internal/utils/datetime.py b/src/pip/_internal/utils/datetime.py index b638646c8bb..4d0503c2f33 100644 --- a/src/pip/_internal/utils/datetime.py +++ b/src/pip/_internal/utils/datetime.py @@ -1,6 +1,8 @@ """For when pip wants to check the date or time. """ +from __future__ import absolute_import + import datetime From f74c764c851fbce58aa4b5ed8df0f49ae8671790 Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara <sh@changeset.nyc> Date: Wed, 24 Jun 2020 00:20:14 -0400 Subject: [PATCH 2339/3170] Work in progress: docs: 20.2 beta testing guide --- docs/html/user_guide.rst | 105 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 105 insertions(+) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index f2e49fadf64..96e3e9350b3 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -1023,4 +1023,109 @@ of ability. Some examples that you could consider include: * ``distlib`` - Packaging and distribution utilities (including functions for interacting with PyPI). +Testing the 20.2 beta +===================== + +Please test pip 20.2b2. + +Watch out for +------------- + +The big thing in this beta is changes to the pip dependency resolver within pip. + +Computers need to know the right order to install pieces of software +("to install `x`, you need to install `y` first"). So, when Python +programmers share software as packages, they have to precisely describe +those installation prerequisites, and pip needs to navigate tricky +situations where it's getting conflicting instructions. This new +dependency resolver will make pip better at handling that tricky +logic, and easier for you to use and troubleshoot. + +The most significant changes to the resolver are: + +* It will **reduce inconsistency**: it will *no longer install a +combination of packages that is mutually inconsistent*. In older +versions of pip, it is possible for pip to install a package which +does not satisfy the declared requirements of another installed +package. For example, in pip 20.0, ``pip install "six<1.12" +"virtualenv==20.0.2"`` does the wrong thing, “successfully” installing +``six==1.11``, even though ``virtualenv==20.0.2`` requires +``six>=1.12.0,<2`` (`defined here +<https://github.com/pypa/virtualenv/blob/20.0.2/setup.cfg#L42-L50>`__). +The new resolver, instead, outright rejects installing anything +if it gets that input. + +* It will be **stricter** - if you ask pip to install two packages with +incompatible requirements, it will refuse (rather than installing a +broken combination, like it did in previous versions). + +So, if you have been using workarounds to force pip to deal with +incompatible or inconsistent requirements combinations, now's a good +time to fix the underlying problem in the packages, because pip will +be stricter from here on out. + + +How to test +----------- + +1. **Install the beta** [specific instructions TKTK]. + +2. **Run ``pip check`` on your current environment**. This + will report if you have any inconsistencies in your set of installed + packages. Having a clean installation will make it much less likely + that you will hit issues when the new resolver is released (and may + address hidden problems in your current environment!). If you run + ``pip check`` and run into stuff you can’t figure out, please `ask + for help in our issuetracker or chat <https://pip.pypa.io/>`__. + +3. **Test the new version of pip** (see below). While we have tried to + make sure that pip’s test suite covers as many cases as we can, we + are very aware that there are people using pip with many different + workflows and build processes, and we will not be able to cover all + of those without your help. + + - If you use pip to install your software, try out the new resolver + and let us know if it works for you with ``pip install``. Try: + - installing several packages simultaneously + - re-creating an environment using a ``requirements.txt`` file + - If you have a build pipeline that depends on pip installing your + dependencies for you, check that the new resolver does what you + need. + - Run your project’s CI (test suite, build process, etc.) using the + new resolver, and let us know of any issues. + - If you have encountered resolver issues with pip in the past, + check whether the new resolver fixes them. Also, let us know if + the new resolver has issues with any workarounds you put in to + address the current resolver’s limitations. We’ll need to ensure + that people can transition off such workarounds smoothly. + - If you develop or support a tool that wraps pip or uses it to + deliver part of your functionality, please test your integration + with the beta. + +Please report bugs [GitHub link or something else TKTK]. + +Setups we might need more testing on +------------------------------------ + +* Windows + +* Macintosh + +* Debian, Fedora, Red Hat, CentOS, Mint, Arch, Raspbian, Gentoo + +* Japanese-localized filesystems/OSes + +* Multi-user installations + +* Using virtualenvs + +* Dependency resolution for any kind of version control systems (e.g., you are installing from Git, Subversion, Mercurial, or CVS) + +* Installing from source code held in local directories + +* Using the most recent versions of Python 3.6, 3.7, 3.8, and 3.9 + +* Customized terminals (where you have modified how error messages and standard output display) + + .. _freeze: https://pip.pypa.io/en/latest/reference/pip_freeze/ From d51fd4799cd3067da493c15f10ac8dad9f8ae6ea Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara <sh@changeset.nyc> Date: Wed, 24 Jun 2020 00:32:53 -0400 Subject: [PATCH 2340/3170] docs: include force-reinstall item --- docs/html/user_guide.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 96e3e9350b3..2c2b4725ac8 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -1088,6 +1088,7 @@ How to test and let us know if it works for you with ``pip install``. Try: - installing several packages simultaneously - re-creating an environment using a ``requirements.txt`` file + - using ``pip --force-reinstall`` to check whether it does what you think it should - If you have a build pipeline that depends on pip installing your dependencies for you, check that the new resolver does what you need. From d8d6da6985586d2a1e921cb96fd0a642b8ef5445 Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara <sh@changeset.nyc> Date: Wed, 24 Jun 2020 00:35:57 -0400 Subject: [PATCH 2341/3170] Docs: add item about constraints files --- docs/html/user_guide.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 2c2b4725ac8..fede33db62c 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -1064,6 +1064,12 @@ incompatible or inconsistent requirements combinations, now's a good time to fix the underlying problem in the packages, because pip will be stricter from here on out. +We are also changing our support for :ref:`Constraints Files` : + +* Unnamed requirements are not allowed as constraints +* Links are not allowed as constraints +* Constraints cannot have extras + How to test ----------- @@ -1089,6 +1095,7 @@ How to test - installing several packages simultaneously - re-creating an environment using a ``requirements.txt`` file - using ``pip --force-reinstall`` to check whether it does what you think it should + - using constraints files - If you have a build pipeline that depends on pip installing your dependencies for you, check that the new resolver does what you need. From 0783460a7e1f902c62192f9d78ac7466a98b1b5f Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara <sh@changeset.nyc> Date: Mon, 29 Jun 2020 23:05:14 -0400 Subject: [PATCH 2342/3170] Improve coverage of conflicts from installed packages --- docs/html/user_guide.rst | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index fede33db62c..2c074f601bb 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -1031,7 +1031,7 @@ Please test pip 20.2b2. Watch out for ------------- -The big thing in this beta is changes to the pip dependency resolver within pip. +The big change in this beta is to the pip dependency resolver within pip. Computers need to know the right order to install pieces of software ("to install `x`, you need to install `y` first"). So, when Python @@ -1064,6 +1064,14 @@ incompatible or inconsistent requirements combinations, now's a good time to fix the underlying problem in the packages, because pip will be stricter from here on out. +This also means that, when you run a ``pip install`` command, pip only +considers the packages you are installing in that command, and may +break already-installed packages. It will not guarantee that your +environment will be consistent all the time. If you ``pip install x`` +and then ``pip install y``, it's possible that the version of ``y`` +you get will be different than it would be if you had run ``pip +install x y`` in a single command. + We are also changing our support for :ref:`Constraints Files` : * Unnamed requirements are not allowed as constraints From 5bf4c5637681a67f2d415b29de7847648afe4b3c Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara <sh@changeset.nyc> Date: Thu, 23 Jul 2020 13:12:43 -0400 Subject: [PATCH 2343/3170] WIP: update info about release plan --- docs/html/user_guide.rst | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 2c074f601bb..63200b632d4 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -1023,15 +1023,22 @@ of ability. Some examples that you could consider include: * ``distlib`` - Packaging and distribution utilities (including functions for interacting with PyPI). -Testing the 20.2 beta -===================== +Changes to the pip dependency resolver in 20.2 +============================================== -Please test pip 20.2b2. +pip 20.1 included an alpha version of the new resolver (hidden behind +an optional ``--unstable-feature=resolver`` flag). pip 20.2 includes a +robust beta of the new resolver (hidden behind an optional +``--use-feature=2020-resolver`` flag) that we encourage you to +test. We will continue to improve the pip dependency resolver in +response to testers' feedback. This will help us prepare to release +pip 20.3, with the new resolver on by default, in October. Watch out for ------------- -The big change in this beta is to the pip dependency resolver within pip. +The big change in this release is to the pip dependency resolver +within pip. Computers need to know the right order to install pieces of software ("to install `x`, you need to install `y` first"). So, when Python @@ -1082,7 +1089,7 @@ We are also changing our support for :ref:`Constraints Files` : How to test ----------- -1. **Install the beta** [specific instructions TKTK]. +1. **Install pip 20.2** with ``python -m pip install --upgrade pip``. 2. **Run ``pip check`` on your current environment**. This will report if you have any inconsistencies in your set of installed @@ -1116,7 +1123,7 @@ How to test that people can transition off such workarounds smoothly. - If you develop or support a tool that wraps pip or uses it to deliver part of your functionality, please test your integration - with the beta. + with pip 20.2. Please report bugs [GitHub link or something else TKTK]. From 2142d04c722dae69d4dce1ab9b7165aa91fb8445 Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara <sh@changeset.nyc> Date: Tue, 28 Jul 2020 09:05:15 -0400 Subject: [PATCH 2344/3170] WIP: improvements to list of things to test --- docs/html/user_guide.rst | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 63200b632d4..c79c11d3ad2 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -1023,8 +1023,8 @@ of ability. Some examples that you could consider include: * ``distlib`` - Packaging and distribution utilities (including functions for interacting with PyPI). -Changes to the pip dependency resolver in 20.2 -============================================== +Changes to the pip dependency resolver in 20.2 (2020) +===================================================== pip 20.1 included an alpha version of the new resolver (hidden behind an optional ``--unstable-feature=resolver`` flag). pip 20.2 includes a @@ -1130,19 +1130,23 @@ Please report bugs [GitHub link or something else TKTK]. Setups we might need more testing on ------------------------------------ -* Windows +* Windows, including Windows Subsystem for Linux (WSL) * Macintosh * Debian, Fedora, Red Hat, CentOS, Mint, Arch, Raspbian, Gentoo -* Japanese-localized filesystems/OSes +* non-Latin localized filesystems and OSes, such as Japanese, Chinese, and Korean, and right-to-left such as Hebrew, Urdu, and Arabic * Multi-user installations -* Using virtualenvs +* Requirements files with 100+ packages -* Dependency resolution for any kind of version control systems (e.g., you are installing from Git, Subversion, Mercurial, or CVS) +* Requirements files that include hashes or pinned dependencies (perhaps as output from ``pip-compile`` or ``pip-tools``) + +* Continuous integration/continuous deployment setups + +* Installing from any kind of version control systems (i.e., Git, Subversion, Mercurial, or CVS) * Installing from source code held in local directories From 0ec91a541f1db22581c755419851b2abb833dffb Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara <sh@changeset.nyc> Date: Tue, 28 Jul 2020 09:23:07 -0400 Subject: [PATCH 2345/3170] WIP: moving test suggestions from other issue Related to #8099. --- docs/html/user_guide.rst | 82 +++++++++++++++++++++++++++++++++++----- 1 file changed, 73 insertions(+), 9 deletions(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index c79c11d3ad2..16a94efc4d4 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -1031,8 +1031,9 @@ an optional ``--unstable-feature=resolver`` flag). pip 20.2 includes a robust beta of the new resolver (hidden behind an optional ``--use-feature=2020-resolver`` flag) that we encourage you to test. We will continue to improve the pip dependency resolver in -response to testers' feedback. This will help us prepare to release -pip 20.3, with the new resolver on by default, in October. +response to testers' feedback. Please give us feedback through the +`resolver testing survey`_. This will help us prepare to release pip +20.3, with the new resolver on by default, in October. Watch out for ------------- @@ -1099,11 +1100,20 @@ How to test ``pip check`` and run into stuff you can’t figure out, please `ask for help in our issuetracker or chat <https://pip.pypa.io/>`__. -3. **Test the new version of pip** (see below). While we have tried to - make sure that pip’s test suite covers as many cases as we can, we - are very aware that there are people using pip with many different - workflows and build processes, and we will not be able to cover all - of those without your help. +3. **Test the new version of pip** (see below). To test the new + resolver, use the ``--use-feature=2020-resolver`` flag, as in: + + ``pip install example --use-feature=2020-resolver`` + + The more feedback we can get, the more we can make sure that the + final release is solid. (Only try the new resolver **in a + non-production environment**, though - it isn't ready for you to + rely on in production!) + + While we have tried to make sure that pip’s test suite covers as + many cases as we can, we are very aware that there are people using + pip with many different workflows and build processes, and we will + not be able to cover all of those without your help. - If you use pip to install your software, try out the new resolver and let us know if it works for you with ``pip install``. Try: @@ -1125,7 +1135,7 @@ How to test deliver part of your functionality, please test your integration with pip 20.2. -Please report bugs [GitHub link or something else TKTK]. +4. **Please report bugs** through the `resolver testing survey`_. Setups we might need more testing on ------------------------------------ @@ -1142,17 +1152,71 @@ Setups we might need more testing on * Requirements files with 100+ packages +* Installing a package that has multiple requirements files + * Requirements files that include hashes or pinned dependencies (perhaps as output from ``pip-compile`` or ``pip-tools``) * Continuous integration/continuous deployment setups -* Installing from any kind of version control systems (i.e., Git, Subversion, Mercurial, or CVS) +* Installing from any kind of version control systems (i.e., Git, Subversion, Mercurial, or CVS), per :ref:`VCS Support` * Installing from source code held in local directories * Using the most recent versions of Python 3.6, 3.7, 3.8, and 3.9 +* PyPy + * Customized terminals (where you have modified how error messages and standard output display) +Examples to try +^^^^^^^^^^^^^^^ + +Install: + +* `tensorflow`_ +* ``hacking`` +* ``pycodestyle`` +* ``pandas`` +* ``tablib`` +* ``elasticsearch`` and ``requests`` together +* ``six`` and ``cherrypy`` together +* ``pip install flake8-import-order==0.17.1 flake8==3.5.0 --use-feature=2020-resolver`` +* ``pip install tornado==5.0 sprockets.http==1.5.0 --use-feature=2020-resolver`` + +Try: + +* ``pip install`` +* ``pip uninstall`` +* ``pip check`` +* ``pip cache`` + + +Tell us about +------------- + +Specific things we'd love to get feedback on: + +* Cases where the new resolver produces the wrong result, + obviously. We hope there won't be too many of these, but we'd like + to trap such bugs now. + +* Cases where the resolver produced an error when you believe it + should have been able to work out what to do. + +* Cases where the resolver gives an error because there's a problem + with your requirements, but you need better information to work out + what's wrong. + +* If you have workarounds to address issues with the current resolver, + does the new resolver let you remove those workarounds? Tell us! + + +Context and followup +-------------------- + +As discussed in `our announcement on the PSF blog`_, the pip team are in the process of developing a new "dependency resolver" (the part of pip that works out what to install based on your requirements). We have reached a major milestone in this work, and have a testable ("beta") version of the resolver, which you can test, included in this release. .. _freeze: https://pip.pypa.io/en/latest/reference/pip_freeze/ +.. _resolver testing survey: https://tools.simplysecure.org/survey/index.php?r=survey/index&sid=989272&lang=en +.. _our announcement on the PSF blog: http://pyfound.blogspot.com/2020/03/new-pip-resolver-to-roll-out-this-year.html +.. _tensorflow: https://pypi.org/project/tensorflow/ From af0633e63300e58453f1ce73106144a003198c67 Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara <sh@changeset.nyc> Date: Tue, 28 Jul 2020 09:29:23 -0400 Subject: [PATCH 2346/3170] WIP formatting and links --- docs/html/user_guide.rst | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 16a94efc4d4..13c0f8d278d 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -1092,7 +1092,7 @@ How to test 1. **Install pip 20.2** with ``python -m pip install --upgrade pip``. -2. **Run ``pip check`` on your current environment**. This +2. **Validate your current environment** by running ``pip check``. This will report if you have any inconsistencies in your set of installed packages. Having a clean installation will make it much less likely that you will hit issues when the new resolver is released (and may @@ -1154,7 +1154,11 @@ Setups we might need more testing on * Installing a package that has multiple requirements files -* Requirements files that include hashes or pinned dependencies (perhaps as output from ``pip-compile`` or ``pip-tools``) +* Requirements files that include hashes (:ref:`hash-checking mode`) + or pinned dependencies (perhaps as output from ``pip-compile`` or + ``pip-tools``) + +* Using :ref:`Constraints Files` * Continuous integration/continuous deployment setups From 483dec270a1d7c0e593f055800d74c8f1b59c69f Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara <sh@changeset.nyc> Date: Tue, 28 Jul 2020 09:33:31 -0400 Subject: [PATCH 2347/3170] WIP formatting --- docs/html/user_guide.rst | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 13c0f8d278d..6a6fd815145 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -1080,7 +1080,7 @@ and then ``pip install y``, it's possible that the version of ``y`` you get will be different than it would be if you had run ``pip install x y`` in a single command. -We are also changing our support for :ref:`Constraints Files` : +We are also changing our support for :ref:`Constraints Files`: * Unnamed requirements are not allowed as constraints * Links are not allowed as constraints @@ -1098,7 +1098,7 @@ How to test that you will hit issues when the new resolver is released (and may address hidden problems in your current environment!). If you run ``pip check`` and run into stuff you can’t figure out, please `ask - for help in our issuetracker or chat <https://pip.pypa.io/>`__. + for help in our issue tracker or chat <https://pip.pypa.io/>`__. 3. **Test the new version of pip** (see below). To test the new resolver, use the ``--use-feature=2020-resolver`` flag, as in: @@ -1119,7 +1119,8 @@ How to test and let us know if it works for you with ``pip install``. Try: - installing several packages simultaneously - re-creating an environment using a ``requirements.txt`` file - - using ``pip --force-reinstall`` to check whether it does what you think it should + - using ``pip install --force-reinstall`` to check whether + it does what you think it should - using constraints files - If you have a build pipeline that depends on pip installing your dependencies for you, check that the new resolver does what you @@ -1155,7 +1156,7 @@ Setups we might need more testing on * Installing a package that has multiple requirements files * Requirements files that include hashes (:ref:`hash-checking mode`) - or pinned dependencies (perhaps as output from ``pip-compile`` or + or pinned dependencies (perhaps as output from ``pip-compile`` within ``pip-tools``) * Using :ref:`Constraints Files` From e898267d94c5e7c9e41cdb1cf86e924818bbb3ba Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara <sh@changeset.nyc> Date: Tue, 28 Jul 2020 09:35:14 -0400 Subject: [PATCH 2348/3170] WIP fixing tabs spaces problem --- docs/html/user_guide.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 6a6fd815145..63f54d1fa3a 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -1119,9 +1119,9 @@ How to test and let us know if it works for you with ``pip install``. Try: - installing several packages simultaneously - re-creating an environment using a ``requirements.txt`` file - - using ``pip install --force-reinstall`` to check whether - it does what you think it should - - using constraints files + - using ``pip install --force-reinstall`` to check whether + it does what you think it should + - using constraints files - If you have a build pipeline that depends on pip installing your dependencies for you, check that the new resolver does what you need. From a910ef6d37ce98906964c1c542121adecb4378a7 Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara <sh@changeset.nyc> Date: Tue, 28 Jul 2020 09:43:07 -0400 Subject: [PATCH 2349/3170] WIP linking to issues Addresses #6536 and #6628. --- docs/html/user_guide.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 63f54d1fa3a..14d5a966bbb 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -1082,7 +1082,7 @@ install x y`` in a single command. We are also changing our support for :ref:`Constraints Files`: -* Unnamed requirements are not allowed as constraints +* Unnamed requirements are not allowed as constraints (see `#6628`_) * Links are not allowed as constraints * Constraints cannot have extras @@ -1225,3 +1225,4 @@ As discussed in `our announcement on the PSF blog`_, the pip team are in the pro .. _resolver testing survey: https://tools.simplysecure.org/survey/index.php?r=survey/index&sid=989272&lang=en .. _our announcement on the PSF blog: http://pyfound.blogspot.com/2020/03/new-pip-resolver-to-roll-out-this-year.html .. _tensorflow: https://pypi.org/project/tensorflow/ +.. _#6628: https://github.com/pypa/pip/issues/6628 From 6bfbce0adcfee88d313e942878287524888b4d36 Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara <sh@changeset.nyc> Date: Tue, 28 Jul 2020 09:47:07 -0400 Subject: [PATCH 2350/3170] WIP adding links to issues Addresses #8210 , #8253 --- docs/html/user_guide.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 14d5a966bbb..af508864e61 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -1082,9 +1082,9 @@ install x y`` in a single command. We are also changing our support for :ref:`Constraints Files`: -* Unnamed requirements are not allowed as constraints (see `#6628`_) -* Links are not allowed as constraints -* Constraints cannot have extras +* Unnamed requirements are not allowed as constraints (see `#6628`_ and `#8210`_) +* Links are not allowed as constraints (see `#8253`_) +* Constraints cannot have extras (see `#6628`_) How to test @@ -1226,3 +1226,5 @@ As discussed in `our announcement on the PSF blog`_, the pip team are in the pro .. _our announcement on the PSF blog: http://pyfound.blogspot.com/2020/03/new-pip-resolver-to-roll-out-this-year.html .. _tensorflow: https://pypi.org/project/tensorflow/ .. _#6628: https://github.com/pypa/pip/issues/6628 +.. _#8210: https://github.com/pypa/pip/issues/8210 +.. _#8253: https://github.com/pypa/pip/issues/8253 From 223e37945612be82fbcf4ba9a51721419b1073a5 Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara <sh@changeset.nyc> Date: Tue, 28 Jul 2020 09:50:46 -0400 Subject: [PATCH 2351/3170] WIP formatting --- docs/html/user_guide.rst | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index af508864e61..2330d04c7e2 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -1082,9 +1082,9 @@ install x y`` in a single command. We are also changing our support for :ref:`Constraints Files`: -* Unnamed requirements are not allowed as constraints (see `#6628`_ and `#8210`_) -* Links are not allowed as constraints (see `#8253`_) -* Constraints cannot have extras (see `#6628`_) +* Unnamed requirements are not allowed as constraints (see :issue:`6628` and :issue:`8210`) +* Links are not allowed as constraints (see :issue:`8253`) +* Constraints cannot have extras (see :issue:`6628`) How to test @@ -1225,6 +1225,3 @@ As discussed in `our announcement on the PSF blog`_, the pip team are in the pro .. _resolver testing survey: https://tools.simplysecure.org/survey/index.php?r=survey/index&sid=989272&lang=en .. _our announcement on the PSF blog: http://pyfound.blogspot.com/2020/03/new-pip-resolver-to-roll-out-this-year.html .. _tensorflow: https://pypi.org/project/tensorflow/ -.. _#6628: https://github.com/pypa/pip/issues/6628 -.. _#8210: https://github.com/pypa/pip/issues/8210 -.. _#8253: https://github.com/pypa/pip/issues/8253 From 1e5e544b5c760aa5fb9283e0dcc29f70dcbc7b76 Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara <sh@changeset.nyc> Date: Tue, 28 Jul 2020 10:00:40 -0400 Subject: [PATCH 2352/3170] Ref links and followup tips --- docs/html/development/release-process.rst | 1 + docs/html/user_guide.rst | 14 +++++++++++++- 2 files changed, 14 insertions(+), 1 deletion(-) diff --git a/docs/html/development/release-process.rst b/docs/html/development/release-process.rst index 8ccc5340336..cbfbce4adf9 100644 --- a/docs/html/development/release-process.rst +++ b/docs/html/development/release-process.rst @@ -30,6 +30,7 @@ to need extra work before being released, the release manager always has the option to back out the partial change prior to a release. The PR can then be reworked and resubmitted for the next release. +.. _`Deprecation Policy`: Deprecation Policy ================== diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 2330d04c7e2..ad95050afb6 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -1023,6 +1023,8 @@ of ability. Some examples that you could consider include: * ``distlib`` - Packaging and distribution utilities (including functions for interacting with PyPI). +.. _`Resolver changes 2020`: + Changes to the pip dependency resolver in 20.2 (2020) ===================================================== @@ -1215,13 +1217,23 @@ Specific things we'd love to get feedback on: * If you have workarounds to address issues with the current resolver, does the new resolver let you remove those workarounds? Tell us! +Please let us know through the `resolver testing survey`_. Context and followup -------------------- -As discussed in `our announcement on the PSF blog`_, the pip team are in the process of developing a new "dependency resolver" (the part of pip that works out what to install based on your requirements). We have reached a major milestone in this work, and have a testable ("beta") version of the resolver, which you can test, included in this release. +As discussed in `our announcement on the PSF blog`_, the pip team are +in the process of developing a new "dependency resolver" (the part of +pip that works out what to install based on your requirements). Since +this work will not change user-visible behavior described in the pip +documentation, this change is not covered by the :ref:`Deprecation +Policy`. + +We're tracking our rollout in :issue:`6536` and you can watch for +announcements on the `low-traffic packaging announcements list`_. .. _freeze: https://pip.pypa.io/en/latest/reference/pip_freeze/ .. _resolver testing survey: https://tools.simplysecure.org/survey/index.php?r=survey/index&sid=989272&lang=en .. _our announcement on the PSF blog: http://pyfound.blogspot.com/2020/03/new-pip-resolver-to-roll-out-this-year.html .. _tensorflow: https://pypi.org/project/tensorflow/ +.. _low-traffic packaging announcements list: https://mail.python.org/mailman3/lists/pypi-announce.python.org/ From d374b0d87754545d761957924b319f67d856cc3b Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara <sh@changeset.nyc> Date: Tue, 28 Jul 2020 10:05:11 -0400 Subject: [PATCH 2353/3170] Add changelog entry --- news/6536.feature | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 news/6536.feature diff --git a/news/6536.feature b/news/6536.feature new file mode 100644 index 00000000000..d63957914c9 --- /dev/null +++ b/news/6536.feature @@ -0,0 +1,11 @@ +Add a beta version of pip's next-generation dependency resolver. + +Move pip's new resolver into beta, remove the +``--unstable-feature=resolver`` flag, and enable the +``--use-feature=2020-resolver`` flag. The new resolver is +significantly stricter and more consistent when it receives +incompatible instructions, and reduces support for certain kinds of +:ref:`Constraints Files`, so some workarounds and workflows may +break. More details about how to test and migrate, and how to report +issues, at :ref:`Resolver changes 2020` . Maintainers are preparing to +release pip 20.3, with the new resolver on by default, in October. From ec736200f4d97e14c368535df7482e8e645e05d3 Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara <sh@changeset.nyc> Date: Tue, 28 Jul 2020 10:16:58 -0400 Subject: [PATCH 2354/3170] formatting --- docs/html/user_guide.rst | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index ad95050afb6..f7a022c42f5 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -1054,20 +1054,20 @@ logic, and easier for you to use and troubleshoot. The most significant changes to the resolver are: * It will **reduce inconsistency**: it will *no longer install a -combination of packages that is mutually inconsistent*. In older -versions of pip, it is possible for pip to install a package which -does not satisfy the declared requirements of another installed -package. For example, in pip 20.0, ``pip install "six<1.12" -"virtualenv==20.0.2"`` does the wrong thing, “successfully” installing -``six==1.11``, even though ``virtualenv==20.0.2`` requires -``six>=1.12.0,<2`` (`defined here -<https://github.com/pypa/virtualenv/blob/20.0.2/setup.cfg#L42-L50>`__). -The new resolver, instead, outright rejects installing anything -if it gets that input. + combination of packages that is mutually inconsistent*. In older + versions of pip, it is possible for pip to install a package which + does not satisfy the declared requirements of another installed + package. For example, in pip 20.0, ``pip install "six<1.12" + "virtualenv==20.0.2"`` does the wrong thing, “successfully” installing + ``six==1.11``, even though ``virtualenv==20.0.2`` requires + ``six>=1.12.0,<2`` (`defined here + <https://github.com/pypa/virtualenv/blob/20.0.2/setup.cfg#L42-L50>`__). + The new resolver, instead, outright rejects installing anything if it + gets that input. * It will be **stricter** - if you ask pip to install two packages with -incompatible requirements, it will refuse (rather than installing a -broken combination, like it did in previous versions). + incompatible requirements, it will refuse (rather than installing a + broken combination, like it did in previous versions). So, if you have been using workarounds to force pip to deal with incompatible or inconsistent requirements combinations, now's a good From 263f85b0add0b7d5f170ef6acd03bde87a09df1e Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara <sh@changeset.nyc> Date: Tue, 28 Jul 2020 10:21:52 -0400 Subject: [PATCH 2355/3170] formatting fix --- docs/html/user_guide.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index f7a022c42f5..9ef6dfffc22 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -1119,11 +1119,13 @@ How to test - If you use pip to install your software, try out the new resolver and let us know if it works for you with ``pip install``. Try: + - installing several packages simultaneously - re-creating an environment using a ``requirements.txt`` file - using ``pip install --force-reinstall`` to check whether it does what you think it should - using constraints files + - If you have a build pipeline that depends on pip installing your dependencies for you, check that the new resolver does what you need. From 66d39f942ce0c2b7c9ba1f2d10dac4e1ffd59904 Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara <sh@changeset.nyc> Date: Tue, 28 Jul 2020 11:22:57 -0400 Subject: [PATCH 2356/3170] Clarify on conflicts and multiplicity --- docs/html/user_guide.rst | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 9ef6dfffc22..702a97d0ca1 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -1049,7 +1049,7 @@ programmers share software as packages, they have to precisely describe those installation prerequisites, and pip needs to navigate tricky situations where it's getting conflicting instructions. This new dependency resolver will make pip better at handling that tricky -logic, and easier for you to use and troubleshoot. +logic, and make pip easier for you to use and troubleshoot. The most significant changes to the resolver are: @@ -1080,7 +1080,9 @@ break already-installed packages. It will not guarantee that your environment will be consistent all the time. If you ``pip install x`` and then ``pip install y``, it's possible that the version of ``y`` you get will be different than it would be if you had run ``pip -install x y`` in a single command. +install x y`` in a single command. We would like your thoughts on what +pip's behavior should be; please answer `our survey on upgrades that +create conflicts`_. We are also changing our support for :ref:`Constraints Files`: @@ -1157,7 +1159,7 @@ Setups we might need more testing on * Requirements files with 100+ packages -* Installing a package that has multiple requirements files +* An installation workflow that involves multiple requirements files * Requirements files that include hashes (:ref:`hash-checking mode`) or pinned dependencies (perhaps as output from ``pip-compile`` within @@ -1239,3 +1241,4 @@ announcements on the `low-traffic packaging announcements list`_. .. _our announcement on the PSF blog: http://pyfound.blogspot.com/2020/03/new-pip-resolver-to-roll-out-this-year.html .. _tensorflow: https://pypi.org/project/tensorflow/ .. _low-traffic packaging announcements list: https://mail.python.org/mailman3/lists/pypi-announce.python.org/ +.. _our survey on upgrades that create conflicts: https://docs.google.com/forms/d/e/1FAIpQLSeBkbhuIlSofXqCyhi3kGkLmtrpPOEBwr6iJA6SzHdxWKfqdA/viewform From 20dc509013e664e5c7dbe7befb28e4fe4ab24dc5 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Wed, 29 Jul 2020 07:53:17 +0530 Subject: [PATCH 2357/3170] Update AUTHORS.txt --- AUTHORS.txt | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/AUTHORS.txt b/AUTHORS.txt index dff91f93092..5304f51fb2d 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -64,6 +64,7 @@ Ashley Manton Ashwin Ramaswami atse Atsushi Odagiri +Avinash Karhana Avner Cohen Baptiste Mispelon Barney Gale @@ -132,6 +133,8 @@ Cory Benfield Cory Wright Craig Kerstiens Cristian Sorinel +Cristina +Cristina Muñoz Curtis Doty cytolentino Damian Quiroga @@ -183,6 +186,7 @@ Eli Schwartz Ellen Marie Dash Emil Burzo Emil Styrke +Emmanuel Arias Endoh Takanao enoch Erdinc Mutlu @@ -219,6 +223,7 @@ gkdoc Gopinath M GOTO Hayato gpiks +Greg Ward Guilherme Espada gutsytechster Guy Rozendorn @@ -288,6 +293,7 @@ Juanjo Bazán Julian Berman Julian Gethmann Julien Demoor +Jussi Kukkonen jwg4 Jyrki Pulliainen Kai Chen @@ -384,6 +390,7 @@ Nicole Harris Nikhil Benesch Nikolay Korolev Nitesh Sharma +Noah Noah Gorny Nowell Strite NtaleGrey @@ -466,6 +473,7 @@ Ronny Pfannschmidt Rory McCann Ross Brattain Roy Wellington Ⅳ +Ruairidh MacLeod Ryan Wooden ryneeverett Sachi King @@ -489,6 +497,7 @@ Simon Pichugin sinoroc sinscary Sorin Sbarnea +Srinivas Nyayapati Stavros Korokithakis Stefan Scherfke Stefano Rivera @@ -504,6 +513,7 @@ Stéphane Bidoul Stéphane Bidoul (ACSONE) Stéphane Klein Sumana Harihareswara +Surbhi Sharma Sviatoslav Sydorenko Swat009 Takayuki SHIMIZUKAWA @@ -537,6 +547,7 @@ Tzu-ping Chung Valentin Haenel Victor Stinner victorvpaulo +Vikram - Google Viktor Szépe Ville Skyttä Vinay Sajip From 127acd8c9eed66f1c0e2d251639b6d378f66b488 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Wed, 29 Jul 2020 07:53:17 +0530 Subject: [PATCH 2358/3170] Bump for release --- NEWS.rst | 88 +++++++++++++++++++ ...a405e7-85cf-4027-b815-08aef54a600b.trivial | 0 ...F5EAF5-F3CC-4B27-A128-872E6A4DC6B4.trivial | 0 ...feb941-3e4e-4b33-bd43-8c47f67ea229.trivial | 0 ...b9b9f6-12ba-424f-b197-10338408d36d.trivial | 0 ...046105-b105-495c-a882-cc7263871c23.trivial | 0 ...984b58-7136-4ef8-a313-bad87fe5e907.trivial | 0 ...99f59c-a122-11ea-94b5-1fa2aa7b8ec0.trivial | 0 news/3166.feature | 1 - ...3A6FF6-6699-4D50-B54F-716A8F0984A3.trivial | 0 ...8f5c72-1c55-4c74-9d23-295563e7a7e7.trivial | 0 ...65c738-859f-40aa-be74-97a55177ee40.trivial | 0 ...29002F-4AB2-4093-B321-994F7882F944.trivial | 0 ...0785C8-AC99-4ED9-8ECE-7C92B4358026.trivial | 0 ...c7a0ae-e29b-4066-ab22-6784dbfc45af.trivial | 0 ...a26440-8fdd-4a8b-979e-64f975bf7a9f.trivial | 0 news/5021.bugfix | 1 - news/5380.feature | 1 - ...8A4C36-D46C-428A-A746-62AE555D1FDE.trivial | 0 ...9B649E-EE91-4EA2-9860-4D13F792959F.trivial | 0 ...8F8551-DB46-4A12-987E-094EF18DAF7C.trivial | 0 ...187A50-7217-4F88-8902-548C9F534E55.trivial | 0 news/6030.feature | 1 - news/6536.feature | 11 --- news/6741.feature | 1 - news/6754.feature | 1 - news/6998.removal | 1 - ...727978-e22a-427d-aa03-11ce55d8f6f9.trivial | 0 news/7309.removal | 1 - ...9E6F3D-CAEB-4AEA-A53F-E623365ACB82.trivial | 0 ...9f489e-80da-4dac-8d7d-13c471f1cee9.trivial | 0 news/7625.bugfix | 1 - news/7688.doc | 1 - news/7693.feature | 1 - news/7811.feature | 2 - ...a83f1d-52f9-4fda-ad83-c19a3e513380.trivial | 0 news/7908.doc | 2 - news/7968.bugfix | 1 - news/8072.doc | 1 - ...A30837-433E-45F5-9177-FAB3447802EE.trivial | 0 news/8128.feature | 1 - news/8148.trivial | 2 - news/8278.vendor | 1 - news/8288.removal | 1 - news/8342.bugfix | 2 - news/8353.doc | 1 - news/8368.removal | 2 - news/8372.removal | 4 - news/8373.doc | 1 - news/8408.removal | 1 - news/8454.bugfix | 1 - news/8459.doc | 1 - news/8504.feature | 1 - news/8512.doc | 1 - news/8521.bugfix | 3 - news/8576.doc | 1 - news/8588.feature | 3 - news/8601.feature | 1 - news/8617.removal | 1 - news/8625.trivial | 2 - ...898036-99ac-4e02-88c7-429280fe3e27.trivial | 0 ...4F6DCE-FE1E-4428-BB4A-40D7C613AA97.trivial | 0 ...ea2537-672d-44b2-b631-9a3455e5fc05.trivial | 0 ...D0EAD4-42B3-4EC1-A2AE-70D7513C8555.trivial | 0 ...D1519B-CB5F-409D-835C-CF7A14DD9A92.trivial | 0 ...05A166-6B1D-4509-8ECA-84EB35A6A391.trivial | 0 ...060278-216E-4884-BB1A-A6645EC0B4D2.trivial | 0 ...B1CF12-70ED-405F-90C0-BEA7CF25DCE4.trivial | 0 ...9F64FF-DD77-4276-B7CE-2B2EFF935563.trivial | 0 ...F62E2A-7A1F-475C-95DE-004ED3B87DFB.trivial | 0 ...D7E4ED-BA3E-4018-B43E-D445DA8E542B.trivial | 0 ...7C8A6F-FA2D-4CDD-9C4B-D51412EC6619.trivial | 0 ...E77CF6-D22C-45A1-840F-AA913FF90F93.trivial | 0 ...4cd47c-e34b-4679-b80e-9c543d4c63a6.trivial | 0 ...4123c8-5a0d-48e2-8def-6b56b9d4b8dc.trivial | 0 ...c65537-8fe0-43d5-8877-af191a39a663.trivial | 0 news/appdirs.vendor | 1 - ...f2cc6f-00f0-4509-baeb-2e9fd5e35bcf.trivial | 0 news/certifi.vendor | 1 - ...f9c55b-f959-456f-a849-ee976ef227de.trivial | 2 - ...9b761d-5d4e-4ea4-9629-0afcc2636cb6.trivial | 1 - news/distlib.vendor | 1 - ...b541f0-714b-4e9b-8f6e-2f5d6c85d98f.trivial | 0 news/error-swallow.trivial | 0 ...1d42b8-8277-4918-94eb-031bc7be1c3f.trivial | 0 ...de5945-af83-49a7-aa42-d8f2860fcaeb.trivial | 0 ...799f39-2041-42cd-9d65-5f38ff5820d8.trivial | 0 news/html5lib.vendor | 1 - news/idna.vendor | 1 - news/packaging.vendor | 1 - news/requests.vendor | 1 - news/six.vendor | 1 - news/toml.vendor | 1 - news/urllib3.vendor | 1 - src/pip/__init__.py | 2 +- 95 files changed, 89 insertions(+), 73 deletions(-) delete mode 100644 news/06a405e7-85cf-4027-b815-08aef54a600b.trivial delete mode 100644 news/0EF5EAF5-F3CC-4B27-A128-872E6A4DC6B4.trivial delete mode 100644 news/0cfeb941-3e4e-4b33-bd43-8c47f67ea229.trivial delete mode 100644 news/17b9b9f6-12ba-424f-b197-10338408d36d.trivial delete mode 100644 news/1f046105-b105-495c-a882-cc7263871c23.trivial delete mode 100644 news/21984b58-7136-4ef8-a313-bad87fe5e907.trivial delete mode 100644 news/2e99f59c-a122-11ea-94b5-1fa2aa7b8ec0.trivial delete mode 100644 news/3166.feature delete mode 100644 news/333A6FF6-6699-4D50-B54F-716A8F0984A3.trivial delete mode 100644 news/348f5c72-1c55-4c74-9d23-295563e7a7e7.trivial delete mode 100644 news/3565c738-859f-40aa-be74-97a55177ee40.trivial delete mode 100644 news/3C29002F-4AB2-4093-B321-994F7882F944.trivial delete mode 100644 news/440785C8-AC99-4ED9-8ECE-7C92B4358026.trivial delete mode 100644 news/47c7a0ae-e29b-4066-ab22-6784dbfc45af.trivial delete mode 100644 news/4aa26440-8fdd-4a8b-979e-64f975bf7a9f.trivial delete mode 100644 news/5021.bugfix delete mode 100644 news/5380.feature delete mode 100644 news/558A4C36-D46C-428A-A746-62AE555D1FDE.trivial delete mode 100644 news/579B649E-EE91-4EA2-9860-4D13F792959F.trivial delete mode 100644 news/598F8551-DB46-4A12-987E-094EF18DAF7C.trivial delete mode 100644 news/5F187A50-7217-4F88-8902-548C9F534E55.trivial delete mode 100644 news/6030.feature delete mode 100644 news/6536.feature delete mode 100644 news/6741.feature delete mode 100644 news/6754.feature delete mode 100644 news/6998.removal delete mode 100644 news/70727978-e22a-427d-aa03-11ce55d8f6f9.trivial delete mode 100644 news/7309.removal delete mode 100644 news/749E6F3D-CAEB-4AEA-A53F-E623365ACB82.trivial delete mode 100644 news/759f489e-80da-4dac-8d7d-13c471f1cee9.trivial delete mode 100644 news/7625.bugfix delete mode 100644 news/7688.doc delete mode 100644 news/7693.feature delete mode 100644 news/7811.feature delete mode 100644 news/78a83f1d-52f9-4fda-ad83-c19a3e513380.trivial delete mode 100644 news/7908.doc delete mode 100644 news/7968.bugfix delete mode 100644 news/8072.doc delete mode 100644 news/80A30837-433E-45F5-9177-FAB3447802EE.trivial delete mode 100644 news/8128.feature delete mode 100644 news/8148.trivial delete mode 100644 news/8278.vendor delete mode 100644 news/8288.removal delete mode 100644 news/8342.bugfix delete mode 100644 news/8353.doc delete mode 100644 news/8368.removal delete mode 100644 news/8372.removal delete mode 100644 news/8373.doc delete mode 100644 news/8408.removal delete mode 100644 news/8454.bugfix delete mode 100644 news/8459.doc delete mode 100644 news/8504.feature delete mode 100644 news/8512.doc delete mode 100644 news/8521.bugfix delete mode 100644 news/8576.doc delete mode 100644 news/8588.feature delete mode 100644 news/8601.feature delete mode 100644 news/8617.removal delete mode 100644 news/8625.trivial delete mode 100644 news/93898036-99ac-4e02-88c7-429280fe3e27.trivial delete mode 100644 news/9B4F6DCE-FE1E-4428-BB4A-40D7C613AA97.trivial delete mode 100644 news/9dea2537-672d-44b2-b631-9a3455e5fc05.trivial delete mode 100644 news/AFD0EAD4-42B3-4EC1-A2AE-70D7513C8555.trivial delete mode 100644 news/B7D1519B-CB5F-409D-835C-CF7A14DD9A92.trivial delete mode 100644 news/C505A166-6B1D-4509-8ECA-84EB35A6A391.trivial delete mode 100644 news/D5060278-216E-4884-BB1A-A6645EC0B4D2.trivial delete mode 100644 news/EBB1CF12-70ED-405F-90C0-BEA7CF25DCE4.trivial delete mode 100644 news/ED9F64FF-DD77-4276-B7CE-2B2EFF935563.trivial delete mode 100644 news/F4F62E2A-7A1F-475C-95DE-004ED3B87DFB.trivial delete mode 100644 news/FCD7E4ED-BA3E-4018-B43E-D445DA8E542B.trivial delete mode 100644 news/FD7C8A6F-FA2D-4CDD-9C4B-D51412EC6619.trivial delete mode 100644 news/FDE77CF6-D22C-45A1-840F-AA913FF90F93.trivial delete mode 100644 news/a44cd47c-e34b-4679-b80e-9c543d4c63a6.trivial delete mode 100644 news/a64123c8-5a0d-48e2-8def-6b56b9d4b8dc.trivial delete mode 100644 news/aac65537-8fe0-43d5-8877-af191a39a663.trivial delete mode 100644 news/appdirs.vendor delete mode 100644 news/b8f2cc6f-00f0-4509-baeb-2e9fd5e35bcf.trivial delete mode 100644 news/certifi.vendor delete mode 100644 news/d9f9c55b-f959-456f-a849-ee976ef227de.trivial delete mode 100644 news/dc9b761d-5d4e-4ea4-9629-0afcc2636cb6.trivial delete mode 100644 news/distlib.vendor delete mode 100644 news/e1b541f0-714b-4e9b-8f6e-2f5d6c85d98f.trivial delete mode 100644 news/error-swallow.trivial delete mode 100644 news/f91d42b8-8277-4918-94eb-031bc7be1c3f.trivial delete mode 100644 news/f9de5945-af83-49a7-aa42-d8f2860fcaeb.trivial delete mode 100644 news/fe799f39-2041-42cd-9d65-5f38ff5820d8.trivial delete mode 100644 news/html5lib.vendor delete mode 100644 news/idna.vendor delete mode 100644 news/packaging.vendor delete mode 100644 news/requests.vendor delete mode 100644 news/six.vendor delete mode 100644 news/toml.vendor delete mode 100644 news/urllib3.vendor diff --git a/NEWS.rst b/NEWS.rst index 70f64faa8a9..755ca709ee5 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -9,6 +9,94 @@ .. towncrier release notes start +20.2 (2020-07-29) +================= + +Deprecations and Removals +------------------------- + +- Deprecate setup.py-based builds that do not generate an ``.egg-info`` directory. (`#6998 <https://github.com/pypa/pip/issues/6998>`_, `#8617 <https://github.com/pypa/pip/issues/8617>`_) +- Disallow passing install-location-related arguments in ``--install-options``. (`#7309 <https://github.com/pypa/pip/issues/7309>`_) +- Add deprecation warning for invalid requirements format "base>=1.0[extra]" (`#8288 <https://github.com/pypa/pip/issues/8288>`_) +- Deprecate legacy setup.py install when building a wheel failed for source + distributions without pyproject.toml (`#8368 <https://github.com/pypa/pip/issues/8368>`_) +- Deprecate -b/--build/--build-dir/--build-directory. Its current behaviour is confusing + and breaks in case different versions of the same distribution need to be built during + the resolution process. Using the TMPDIR/TEMP/TMP environment variable, possibly + combined with --no-clean covers known use cases. (`#8372 <https://github.com/pypa/pip/issues/8372>`_) +- Remove undocumented and deprecated option ``--always-unzip`` (`#8408 <https://github.com/pypa/pip/issues/8408>`_) + +Features +-------- + +- Log debugging information about pip, in ``pip install --verbose``. (`#3166 <https://github.com/pypa/pip/issues/3166>`_) +- Refine error messages to avoid showing Python tracebacks when an HTTP error occurs. (`#5380 <https://github.com/pypa/pip/issues/5380>`_) +- Install wheel files directly instead of extracting them to a temp directory. (`#6030 <https://github.com/pypa/pip/issues/6030>`_) +- Add a beta version of pip's next-generation dependency resolver. + + Move pip's new resolver into beta, remove the + ``--unstable-feature=resolver`` flag, and enable the + ``--use-feature=2020-resolver`` flag. The new resolver is + significantly stricter and more consistent when it receives + incompatible instructions, and reduces support for certain kinds of + :ref:`Constraints Files`, so some workarounds and workflows may + break. More details about how to test and migrate, and how to report + issues, at :ref:`Resolver changes 2020` . Maintainers are preparing to + release pip 20.3, with the new resolver on by default, in October. (`#6536 <https://github.com/pypa/pip/issues/6536>`_) +- Add a subcommand ``debug`` to ``pip config`` to list available configuration sources and the key-value pairs defined in them. (`#6741 <https://github.com/pypa/pip/issues/6741>`_) +- Warn if index pages have unexpected content-type (`#6754 <https://github.com/pypa/pip/issues/6754>`_) +- Allow specifying ``--prefer-binary`` option in a requirements file (`#7693 <https://github.com/pypa/pip/issues/7693>`_) +- Generate PEP 376 REQUESTED metadata for user supplied requirements installed + by pip. (`#7811 <https://github.com/pypa/pip/issues/7811>`_) +- Warn if package url is a vcs or an archive url with invalid scheme (`#8128 <https://github.com/pypa/pip/issues/8128>`_) +- Parallelize network operations in ``pip list``. (`#8504 <https://github.com/pypa/pip/issues/8504>`_) +- Allow the new resolver to obtain dependency information through wheels + lazily downloaded using HTTP range requests. To enable this feature, + invoke ``pip`` with ``--use-feature=fast-deps``. (`#8588 <https://github.com/pypa/pip/issues/8588>`_) +- Support ``--use-feature`` in requirements files (`#8601 <https://github.com/pypa/pip/issues/8601>`_) + +Bug Fixes +--------- + +- Use canonical package names while looking up already installed packages. (`#5021 <https://github.com/pypa/pip/issues/5021>`_) +- Fix normalizing path on Windows when installing package on another logical disk. (`#7625 <https://github.com/pypa/pip/issues/7625>`_) +- The VCS commands run by pip as subprocesses don't merge stdout and stderr anymore, improving the output parsing by subsequent commands. (`#7968 <https://github.com/pypa/pip/issues/7968>`_) +- Correctly treat non-ASCII entry point declarations in wheels so they can be + installed on Windows. (`#8342 <https://github.com/pypa/pip/issues/8342>`_) +- Update author email in config and tests to reflect decommissioning of pypa-dev list. (`#8454 <https://github.com/pypa/pip/issues/8454>`_) +- Headers provided by wheels in .data directories are now correctly installed + into the user-provided locations, such as ``--prefix``, instead of the virtual + environment pip is running in. (`#8521 <https://github.com/pypa/pip/issues/8521>`_) + +Vendored Libraries +------------------ + +- Vendored htmlib5 no longer imports deprecated xml.etree.cElementTree on Python 3. +- Upgrade appdirs to 1.4.4 +- Upgrade certifi to 2020.6.20 +- Upgrade distlib to 0.3.1 +- Upgrade html5lib to 1.1 +- Upgrade idna to 2.10 +- Upgrade packaging to 20.4 +- Upgrade requests to 2.24.0 +- Upgrade six to 1.15.0 +- Upgrade toml to 0.10.1 +- Upgrade urllib3 to 1.25.9 + +Improved Documentation +---------------------- + +- Add ``--no-input`` option to pip docs (`#7688 <https://github.com/pypa/pip/issues/7688>`_) +- List of options supported in requirements file are extracted from source of truth, + instead of being maintained manually. (`#7908 <https://github.com/pypa/pip/issues/7908>`_) +- Fix pip config docstring so that the subcommands render correctly in the docs (`#8072 <https://github.com/pypa/pip/issues/8072>`_) +- replace links to the old pypa-dev mailing list with https://mail.python.org/mailman3/lists/distutils-sig.python.org/ (`#8353 <https://github.com/pypa/pip/issues/8353>`_) +- Fix example for defining multiple values for options which support them (`#8373 <https://github.com/pypa/pip/issues/8373>`_) +- Add documentation that helps the user fix dependency conflicts (`#8459 <https://github.com/pypa/pip/issues/8459>`_) +- Add feature flags to docs (`#8512 <https://github.com/pypa/pip/issues/8512>`_) +- Document how to install package extras from git branch and source distributions. (`#8576 <https://github.com/pypa/pip/issues/8576>`_) + + 20.2b1 (2020-05-21) =================== diff --git a/news/06a405e7-85cf-4027-b815-08aef54a600b.trivial b/news/06a405e7-85cf-4027-b815-08aef54a600b.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/0EF5EAF5-F3CC-4B27-A128-872E6A4DC6B4.trivial b/news/0EF5EAF5-F3CC-4B27-A128-872E6A4DC6B4.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/0cfeb941-3e4e-4b33-bd43-8c47f67ea229.trivial b/news/0cfeb941-3e4e-4b33-bd43-8c47f67ea229.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/17b9b9f6-12ba-424f-b197-10338408d36d.trivial b/news/17b9b9f6-12ba-424f-b197-10338408d36d.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/1f046105-b105-495c-a882-cc7263871c23.trivial b/news/1f046105-b105-495c-a882-cc7263871c23.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/21984b58-7136-4ef8-a313-bad87fe5e907.trivial b/news/21984b58-7136-4ef8-a313-bad87fe5e907.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/2e99f59c-a122-11ea-94b5-1fa2aa7b8ec0.trivial b/news/2e99f59c-a122-11ea-94b5-1fa2aa7b8ec0.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/3166.feature b/news/3166.feature deleted file mode 100644 index 1d8e049ffe8..00000000000 --- a/news/3166.feature +++ /dev/null @@ -1 +0,0 @@ -Log debugging information about pip, in ``pip install --verbose``. diff --git a/news/333A6FF6-6699-4D50-B54F-716A8F0984A3.trivial b/news/333A6FF6-6699-4D50-B54F-716A8F0984A3.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/348f5c72-1c55-4c74-9d23-295563e7a7e7.trivial b/news/348f5c72-1c55-4c74-9d23-295563e7a7e7.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/3565c738-859f-40aa-be74-97a55177ee40.trivial b/news/3565c738-859f-40aa-be74-97a55177ee40.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/3C29002F-4AB2-4093-B321-994F7882F944.trivial b/news/3C29002F-4AB2-4093-B321-994F7882F944.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/440785C8-AC99-4ED9-8ECE-7C92B4358026.trivial b/news/440785C8-AC99-4ED9-8ECE-7C92B4358026.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/47c7a0ae-e29b-4066-ab22-6784dbfc45af.trivial b/news/47c7a0ae-e29b-4066-ab22-6784dbfc45af.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/4aa26440-8fdd-4a8b-979e-64f975bf7a9f.trivial b/news/4aa26440-8fdd-4a8b-979e-64f975bf7a9f.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/5021.bugfix b/news/5021.bugfix deleted file mode 100644 index 36606fd20f5..00000000000 --- a/news/5021.bugfix +++ /dev/null @@ -1 +0,0 @@ -Use canonical package names while looking up already installed packages. diff --git a/news/5380.feature b/news/5380.feature deleted file mode 100644 index df2ef032c04..00000000000 --- a/news/5380.feature +++ /dev/null @@ -1 +0,0 @@ -Refine error messages to avoid showing Python tracebacks when an HTTP error occurs. diff --git a/news/558A4C36-D46C-428A-A746-62AE555D1FDE.trivial b/news/558A4C36-D46C-428A-A746-62AE555D1FDE.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/579B649E-EE91-4EA2-9860-4D13F792959F.trivial b/news/579B649E-EE91-4EA2-9860-4D13F792959F.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/598F8551-DB46-4A12-987E-094EF18DAF7C.trivial b/news/598F8551-DB46-4A12-987E-094EF18DAF7C.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/5F187A50-7217-4F88-8902-548C9F534E55.trivial b/news/5F187A50-7217-4F88-8902-548C9F534E55.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/6030.feature b/news/6030.feature deleted file mode 100644 index 176eb903b37..00000000000 --- a/news/6030.feature +++ /dev/null @@ -1 +0,0 @@ -Install wheel files directly instead of extracting them to a temp directory. diff --git a/news/6536.feature b/news/6536.feature deleted file mode 100644 index d63957914c9..00000000000 --- a/news/6536.feature +++ /dev/null @@ -1,11 +0,0 @@ -Add a beta version of pip's next-generation dependency resolver. - -Move pip's new resolver into beta, remove the -``--unstable-feature=resolver`` flag, and enable the -``--use-feature=2020-resolver`` flag. The new resolver is -significantly stricter and more consistent when it receives -incompatible instructions, and reduces support for certain kinds of -:ref:`Constraints Files`, so some workarounds and workflows may -break. More details about how to test and migrate, and how to report -issues, at :ref:`Resolver changes 2020` . Maintainers are preparing to -release pip 20.3, with the new resolver on by default, in October. diff --git a/news/6741.feature b/news/6741.feature deleted file mode 100644 index 382e095a630..00000000000 --- a/news/6741.feature +++ /dev/null @@ -1 +0,0 @@ -Add a subcommand ``debug`` to ``pip config`` to list available configuration sources and the key-value pairs defined in them. diff --git a/news/6754.feature b/news/6754.feature deleted file mode 100644 index 561643dbd28..00000000000 --- a/news/6754.feature +++ /dev/null @@ -1 +0,0 @@ -Warn if index pages have unexpected content-type diff --git a/news/6998.removal b/news/6998.removal deleted file mode 100644 index 7c38a48fd11..00000000000 --- a/news/6998.removal +++ /dev/null @@ -1 +0,0 @@ -Deprecate setup.py-based builds that do not generate an ``.egg-info`` directory. diff --git a/news/70727978-e22a-427d-aa03-11ce55d8f6f9.trivial b/news/70727978-e22a-427d-aa03-11ce55d8f6f9.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/7309.removal b/news/7309.removal deleted file mode 100644 index 979b8616055..00000000000 --- a/news/7309.removal +++ /dev/null @@ -1 +0,0 @@ -Disallow passing install-location-related arguments in ``--install-options``. diff --git a/news/749E6F3D-CAEB-4AEA-A53F-E623365ACB82.trivial b/news/749E6F3D-CAEB-4AEA-A53F-E623365ACB82.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/759f489e-80da-4dac-8d7d-13c471f1cee9.trivial b/news/759f489e-80da-4dac-8d7d-13c471f1cee9.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/7625.bugfix b/news/7625.bugfix deleted file mode 100644 index 3a675f8d2b0..00000000000 --- a/news/7625.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix normalizing path on Windows when installing package on another logical disk. diff --git a/news/7688.doc b/news/7688.doc deleted file mode 100644 index e891c7e8c29..00000000000 --- a/news/7688.doc +++ /dev/null @@ -1 +0,0 @@ -Add ``--no-input`` option to pip docs diff --git a/news/7693.feature b/news/7693.feature deleted file mode 100644 index 4e458559110..00000000000 --- a/news/7693.feature +++ /dev/null @@ -1 +0,0 @@ -Allow specifying ``--prefer-binary`` option in a requirements file diff --git a/news/7811.feature b/news/7811.feature deleted file mode 100644 index 0b471405a9c..00000000000 --- a/news/7811.feature +++ /dev/null @@ -1,2 +0,0 @@ -Generate PEP 376 REQUESTED metadata for user supplied requirements installed -by pip. diff --git a/news/78a83f1d-52f9-4fda-ad83-c19a3e513380.trivial b/news/78a83f1d-52f9-4fda-ad83-c19a3e513380.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/7908.doc b/news/7908.doc deleted file mode 100644 index ec5ee72ac2e..00000000000 --- a/news/7908.doc +++ /dev/null @@ -1,2 +0,0 @@ -List of options supported in requirements file are extracted from source of truth, -instead of being maintained manually. diff --git a/news/7968.bugfix b/news/7968.bugfix deleted file mode 100644 index 36b282fc821..00000000000 --- a/news/7968.bugfix +++ /dev/null @@ -1 +0,0 @@ -The VCS commands run by pip as subprocesses don't merge stdout and stderr anymore, improving the output parsing by subsequent commands. diff --git a/news/8072.doc b/news/8072.doc deleted file mode 100644 index 71eb46f292d..00000000000 --- a/news/8072.doc +++ /dev/null @@ -1 +0,0 @@ -Fix pip config docstring so that the subcommands render correctly in the docs diff --git a/news/80A30837-433E-45F5-9177-FAB3447802EE.trivial b/news/80A30837-433E-45F5-9177-FAB3447802EE.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/8128.feature b/news/8128.feature deleted file mode 100644 index 9e180b50959..00000000000 --- a/news/8128.feature +++ /dev/null @@ -1 +0,0 @@ -Warn if package url is a vcs or an archive url with invalid scheme diff --git a/news/8148.trivial b/news/8148.trivial deleted file mode 100644 index 3d3066aa2c5..00000000000 --- a/news/8148.trivial +++ /dev/null @@ -1,2 +0,0 @@ -Improve "Running pip from source tree" section in getting-started -to use editable installation instead of running pip directly from source. diff --git a/news/8278.vendor b/news/8278.vendor deleted file mode 100644 index ad3ce7e4506..00000000000 --- a/news/8278.vendor +++ /dev/null @@ -1 +0,0 @@ -Vendored htmlib5 no longer imports deprecated xml.etree.cElementTree on Python 3. diff --git a/news/8288.removal b/news/8288.removal deleted file mode 100644 index 830d91aab95..00000000000 --- a/news/8288.removal +++ /dev/null @@ -1 +0,0 @@ -Add deprecation warning for invalid requirements format "base>=1.0[extra]" diff --git a/news/8342.bugfix b/news/8342.bugfix deleted file mode 100644 index fd6b9b8257b..00000000000 --- a/news/8342.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -Correctly treat non-ASCII entry point declarations in wheels so they can be -installed on Windows. diff --git a/news/8353.doc b/news/8353.doc deleted file mode 100644 index a0ed44ff5d0..00000000000 --- a/news/8353.doc +++ /dev/null @@ -1 +0,0 @@ -replace links to the old pypa-dev mailing list with https://mail.python.org/mailman3/lists/distutils-sig.python.org/ diff --git a/news/8368.removal b/news/8368.removal deleted file mode 100644 index 646c384d78a..00000000000 --- a/news/8368.removal +++ /dev/null @@ -1,2 +0,0 @@ -Deprecate legacy setup.py install when building a wheel failed for source -distributions without pyproject.toml diff --git a/news/8372.removal b/news/8372.removal deleted file mode 100644 index af0cb6e70c7..00000000000 --- a/news/8372.removal +++ /dev/null @@ -1,4 +0,0 @@ -Deprecate -b/--build/--build-dir/--build-directory. Its current behaviour is confusing -and breaks in case different versions of the same distribution need to be built during -the resolution process. Using the TMPDIR/TEMP/TMP environment variable, possibly -combined with --no-clean covers known use cases. diff --git a/news/8373.doc b/news/8373.doc deleted file mode 100644 index dc804c6e506..00000000000 --- a/news/8373.doc +++ /dev/null @@ -1 +0,0 @@ -Fix example for defining multiple values for options which support them diff --git a/news/8408.removal b/news/8408.removal deleted file mode 100644 index 008e21b75d0..00000000000 --- a/news/8408.removal +++ /dev/null @@ -1 +0,0 @@ -Remove undocumented and deprecated option ``--always-unzip`` diff --git a/news/8454.bugfix b/news/8454.bugfix deleted file mode 100644 index fe799f9346c..00000000000 --- a/news/8454.bugfix +++ /dev/null @@ -1 +0,0 @@ -Update author email in config and tests to reflect decommissioning of pypa-dev list. diff --git a/news/8459.doc b/news/8459.doc deleted file mode 100644 index 1438edb891d..00000000000 --- a/news/8459.doc +++ /dev/null @@ -1 +0,0 @@ -Add documentation that helps the user fix dependency conflicts diff --git a/news/8504.feature b/news/8504.feature deleted file mode 100644 index 06ab27112b6..00000000000 --- a/news/8504.feature +++ /dev/null @@ -1 +0,0 @@ -Parallelize network operations in ``pip list``. diff --git a/news/8512.doc b/news/8512.doc deleted file mode 100644 index 34630afc071..00000000000 --- a/news/8512.doc +++ /dev/null @@ -1 +0,0 @@ -Add feature flags to docs diff --git a/news/8521.bugfix b/news/8521.bugfix deleted file mode 100644 index d5b1da3a829..00000000000 --- a/news/8521.bugfix +++ /dev/null @@ -1,3 +0,0 @@ -Headers provided by wheels in .data directories are now correctly installed -into the user-provided locations, such as ``--prefix``, instead of the virtual -environment pip is running in. diff --git a/news/8576.doc b/news/8576.doc deleted file mode 100644 index 82347ec4bba..00000000000 --- a/news/8576.doc +++ /dev/null @@ -1 +0,0 @@ -Document how to install package extras from git branch and source distributions. diff --git a/news/8588.feature b/news/8588.feature deleted file mode 100644 index 273715bb009..00000000000 --- a/news/8588.feature +++ /dev/null @@ -1,3 +0,0 @@ -Allow the new resolver to obtain dependency information through wheels -lazily downloaded using HTTP range requests. To enable this feature, -invoke ``pip`` with ``--use-feature=fast-deps``. diff --git a/news/8601.feature b/news/8601.feature deleted file mode 100644 index 3e56c66ab1b..00000000000 --- a/news/8601.feature +++ /dev/null @@ -1 +0,0 @@ -Support ``--use-feature`` in requirements files diff --git a/news/8617.removal b/news/8617.removal deleted file mode 100644 index 7c38a48fd11..00000000000 --- a/news/8617.removal +++ /dev/null @@ -1 +0,0 @@ -Deprecate setup.py-based builds that do not generate an ``.egg-info`` directory. diff --git a/news/8625.trivial b/news/8625.trivial deleted file mode 100644 index 946fa4602f4..00000000000 --- a/news/8625.trivial +++ /dev/null @@ -1,2 +0,0 @@ -Fix 2020 resolver error message when conflicting packages are specified -directly in a requirements file. diff --git a/news/93898036-99ac-4e02-88c7-429280fe3e27.trivial b/news/93898036-99ac-4e02-88c7-429280fe3e27.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/9B4F6DCE-FE1E-4428-BB4A-40D7C613AA97.trivial b/news/9B4F6DCE-FE1E-4428-BB4A-40D7C613AA97.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/9dea2537-672d-44b2-b631-9a3455e5fc05.trivial b/news/9dea2537-672d-44b2-b631-9a3455e5fc05.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/AFD0EAD4-42B3-4EC1-A2AE-70D7513C8555.trivial b/news/AFD0EAD4-42B3-4EC1-A2AE-70D7513C8555.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/B7D1519B-CB5F-409D-835C-CF7A14DD9A92.trivial b/news/B7D1519B-CB5F-409D-835C-CF7A14DD9A92.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/C505A166-6B1D-4509-8ECA-84EB35A6A391.trivial b/news/C505A166-6B1D-4509-8ECA-84EB35A6A391.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/D5060278-216E-4884-BB1A-A6645EC0B4D2.trivial b/news/D5060278-216E-4884-BB1A-A6645EC0B4D2.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/EBB1CF12-70ED-405F-90C0-BEA7CF25DCE4.trivial b/news/EBB1CF12-70ED-405F-90C0-BEA7CF25DCE4.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/ED9F64FF-DD77-4276-B7CE-2B2EFF935563.trivial b/news/ED9F64FF-DD77-4276-B7CE-2B2EFF935563.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/F4F62E2A-7A1F-475C-95DE-004ED3B87DFB.trivial b/news/F4F62E2A-7A1F-475C-95DE-004ED3B87DFB.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/FCD7E4ED-BA3E-4018-B43E-D445DA8E542B.trivial b/news/FCD7E4ED-BA3E-4018-B43E-D445DA8E542B.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/FD7C8A6F-FA2D-4CDD-9C4B-D51412EC6619.trivial b/news/FD7C8A6F-FA2D-4CDD-9C4B-D51412EC6619.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/FDE77CF6-D22C-45A1-840F-AA913FF90F93.trivial b/news/FDE77CF6-D22C-45A1-840F-AA913FF90F93.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/a44cd47c-e34b-4679-b80e-9c543d4c63a6.trivial b/news/a44cd47c-e34b-4679-b80e-9c543d4c63a6.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/a64123c8-5a0d-48e2-8def-6b56b9d4b8dc.trivial b/news/a64123c8-5a0d-48e2-8def-6b56b9d4b8dc.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/aac65537-8fe0-43d5-8877-af191a39a663.trivial b/news/aac65537-8fe0-43d5-8877-af191a39a663.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/appdirs.vendor b/news/appdirs.vendor deleted file mode 100644 index 4e4ebd7278a..00000000000 --- a/news/appdirs.vendor +++ /dev/null @@ -1 +0,0 @@ -Upgrade appdirs to 1.4.4 diff --git a/news/b8f2cc6f-00f0-4509-baeb-2e9fd5e35bcf.trivial b/news/b8f2cc6f-00f0-4509-baeb-2e9fd5e35bcf.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/certifi.vendor b/news/certifi.vendor deleted file mode 100644 index ddd125054b1..00000000000 --- a/news/certifi.vendor +++ /dev/null @@ -1 +0,0 @@ -Upgrade certifi to 2020.6.20 diff --git a/news/d9f9c55b-f959-456f-a849-ee976ef227de.trivial b/news/d9f9c55b-f959-456f-a849-ee976ef227de.trivial deleted file mode 100644 index ece7751fc27..00000000000 --- a/news/d9f9c55b-f959-456f-a849-ee976ef227de.trivial +++ /dev/null @@ -1,2 +0,0 @@ -Refactor the commands by removing the ``__init__`` method and defining and explicit -``add_options`` method for adding command options. diff --git a/news/dc9b761d-5d4e-4ea4-9629-0afcc2636cb6.trivial b/news/dc9b761d-5d4e-4ea4-9629-0afcc2636cb6.trivial deleted file mode 100644 index f23264777a4..00000000000 --- a/news/dc9b761d-5d4e-4ea4-9629-0afcc2636cb6.trivial +++ /dev/null @@ -1 +0,0 @@ -Add methods for path lookups in ``test_install_reqs.py`` and ``test_install_upgrade.py``. diff --git a/news/distlib.vendor b/news/distlib.vendor deleted file mode 100644 index ba8d7633c07..00000000000 --- a/news/distlib.vendor +++ /dev/null @@ -1 +0,0 @@ -Upgrade distlib to 0.3.1 diff --git a/news/e1b541f0-714b-4e9b-8f6e-2f5d6c85d98f.trivial b/news/e1b541f0-714b-4e9b-8f6e-2f5d6c85d98f.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/error-swallow.trivial b/news/error-swallow.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/f91d42b8-8277-4918-94eb-031bc7be1c3f.trivial b/news/f91d42b8-8277-4918-94eb-031bc7be1c3f.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/f9de5945-af83-49a7-aa42-d8f2860fcaeb.trivial b/news/f9de5945-af83-49a7-aa42-d8f2860fcaeb.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/fe799f39-2041-42cd-9d65-5f38ff5820d8.trivial b/news/fe799f39-2041-42cd-9d65-5f38ff5820d8.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/html5lib.vendor b/news/html5lib.vendor deleted file mode 100644 index ed774270d45..00000000000 --- a/news/html5lib.vendor +++ /dev/null @@ -1 +0,0 @@ -Upgrade html5lib to 1.1 diff --git a/news/idna.vendor b/news/idna.vendor deleted file mode 100644 index b1bce37afe0..00000000000 --- a/news/idna.vendor +++ /dev/null @@ -1 +0,0 @@ -Upgrade idna to 2.10 diff --git a/news/packaging.vendor b/news/packaging.vendor deleted file mode 100644 index 1c69173a95e..00000000000 --- a/news/packaging.vendor +++ /dev/null @@ -1 +0,0 @@ -Upgrade packaging to 20.4 diff --git a/news/requests.vendor b/news/requests.vendor deleted file mode 100644 index 4e61b1974df..00000000000 --- a/news/requests.vendor +++ /dev/null @@ -1 +0,0 @@ -Upgrade requests to 2.24.0 diff --git a/news/six.vendor b/news/six.vendor deleted file mode 100644 index 6c9e24900c8..00000000000 --- a/news/six.vendor +++ /dev/null @@ -1 +0,0 @@ -Upgrade six to 1.15.0 diff --git a/news/toml.vendor b/news/toml.vendor deleted file mode 100644 index 401ae7a8361..00000000000 --- a/news/toml.vendor +++ /dev/null @@ -1 +0,0 @@ -Upgrade toml to 0.10.1 diff --git a/news/urllib3.vendor b/news/urllib3.vendor deleted file mode 100644 index f80766b0d06..00000000000 --- a/news/urllib3.vendor +++ /dev/null @@ -1 +0,0 @@ -Upgrade urllib3 to 1.25.9 diff --git a/src/pip/__init__.py b/src/pip/__init__.py index 90ce10888ef..b67e61d063e 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -4,7 +4,7 @@ from typing import List, Optional -__version__ = "20.2.dev1" +__version__ = "20.2" def main(args=None): From a28081c28ecf45514d622d31a790cf37405e01d3 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Wed, 29 Jul 2020 07:53:18 +0530 Subject: [PATCH 2359/3170] Bump for development --- src/pip/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/__init__.py b/src/pip/__init__.py index b67e61d063e..5a2f3c31745 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -4,7 +4,7 @@ from typing import List, Optional -__version__ = "20.2" +__version__ = "20.3.dev0" def main(args=None): From c7af6a420cb069d29fd57928dfbc600a395850f1 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Wed, 29 Jul 2020 23:16:12 +0530 Subject: [PATCH 2360/3170] Update Code of Conduct references --- README.rst | 4 ++-- docs/html/index.rst | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/README.rst b/README.rst index 4f0f210f0e6..395b642d60d 100644 --- a/README.rst +++ b/README.rst @@ -38,7 +38,7 @@ Code of Conduct --------------- Everyone interacting in the pip project's codebases, issue trackers, chat -rooms, and mailing lists is expected to follow the `PyPA Code of Conduct`_. +rooms, and mailing lists is expected to follow the `PSF Code of Conduct`_. .. _package installer: https://packaging.python.org/guides/tool-recommendations/ .. _Python Package Index: https://pypi.org @@ -54,4 +54,4 @@ rooms, and mailing lists is expected to follow the `PyPA Code of Conduct`_. .. _Development mailing list: https://mail.python.org/mailman3/lists/distutils-sig.python.org/ .. _User IRC: https://webchat.freenode.net/?channels=%23pypa .. _Development IRC: https://webchat.freenode.net/?channels=%23pypa-dev -.. _PyPA Code of Conduct: https://www.pypa.io/en/latest/code-of-conduct/ +.. _PSF Code of Conduct: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md diff --git a/docs/html/index.rst b/docs/html/index.rst index ff41fc42b52..1ec91558519 100644 --- a/docs/html/index.rst +++ b/docs/html/index.rst @@ -35,7 +35,7 @@ Code of Conduct =============== Everyone interacting in the pip project's codebases, issue trackers, chat -rooms, and mailing lists is expected to follow the `PyPA Code of Conduct`_. +rooms, and mailing lists is expected to follow the `PSF Code of Conduct`_. .. _package installer: https://packaging.python.org/guides/tool-recommendations/ .. _Python Package Index: https://pypi.org @@ -49,4 +49,4 @@ rooms, and mailing lists is expected to follow the `PyPA Code of Conduct`_. .. _Development mailing list: https://mail.python.org/mailman3/lists/distutils-sig.python.org/ .. _User IRC: https://webchat.freenode.net/?channels=%23pypa .. _Development IRC: https://webchat.freenode.net/?channels=%23pypa-dev -.. _PyPA Code of Conduct: https://www.pypa.io/en/latest/code-of-conduct/ +.. _PSF Code of Conduct: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md From 5dac5dc098df3e6a5b132c041b06ffa739ca6a14 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Wed, 29 Jul 2020 23:16:56 +0530 Subject: [PATCH 2361/3170] Drop CoC reference from new issue templates --- .github/ISSUE_TEMPLATE/config.yml | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/config.yml b/.github/ISSUE_TEMPLATE/config.yml index 3babf35bdb0..8e5c268c114 100644 --- a/.github/ISSUE_TEMPLATE/config.yml +++ b/.github/ISSUE_TEMPLATE/config.yml @@ -1,7 +1,7 @@ # Ref: https://help.github.com/en/github/building-a-strong-community/configuring-issue-templates-for-your-repository#configuring-the-template-chooser blank_issues_enabled: true # default contact_links: -- name: 🤷💻🤦 Discourse +- name: 💬 Discourse url: https://discuss.python.org/c/packaging about: | Please ask typical Q&A here: general ideas for Python packaging, @@ -9,6 +9,3 @@ contact_links: - name: '💬 IRC: #pypa @ Freenode' url: https://webchat.freenode.net/#pypa about: Chat with devs -- name: 📝 PyPA Code of Conduct - url: https://www.pypa.io/en/latest/code-of-conduct/ - about: ❤ Be nice to other members of the community. ☮ Behave. From 864f0e0efa2701c9dc1120e6a80485e86da500ce Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 29 Jul 2020 17:51:58 -0400 Subject: [PATCH 2362/3170] Explicitly handle incorrect .data paths during wheel install Previously our wheel installation process allowed wheels which contained non-conforming contents in a contained .data directory. After the refactoring to enable direct-from-wheel installation, pip throws an exception when encountering these wheels, but does not include any helpful information to pinpoint the cause. Now if we encounter such a wheel, we trace an error that includes the name of the requirement we're trying to install, the path to the wheel file, the path we didn't understand, and a hint about what we expect. --- src/pip/_internal/operations/install/wheel.py | 10 +++++++++- tests/functional/test_install_wheel.py | 18 ++++++++++++++++++ 2 files changed, 27 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 8f73a88b074..0bd7b28be4f 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -583,7 +583,15 @@ def data_scheme_file_maker(zip_file, scheme): def make_data_scheme_file(record_path): # type: (RecordPath) -> File normed_path = os.path.normpath(record_path) - _, scheme_key, dest_subpath = normed_path.split(os.path.sep, 2) + try: + _, scheme_key, dest_subpath = normed_path.split(os.path.sep, 2) + except ValueError: + message = ( + "Unexpected file in {}: {!r}. .data directory contents" + " should be named like: '<scheme key>/<path>'." + ).format(wheel_path, record_path) + raise InstallationError(message) + scheme_path = scheme_paths[scheme_key] dest_path = os.path.join(scheme_path, dest_subpath) assert_no_path_traversal(scheme_path, dest_path) diff --git a/tests/functional/test_install_wheel.py b/tests/functional/test_install_wheel.py index c53f13ca415..89d24a62fc0 100644 --- a/tests/functional/test_install_wheel.py +++ b/tests/functional/test_install_wheel.py @@ -681,3 +681,21 @@ def test_correct_package_name_while_creating_wheel_bug(script, package_name): package = create_basic_wheel_for_package(script, package_name, '1.0') wheel_name = os.path.basename(package) assert wheel_name == 'simple_package-1.0-py2.py3-none-any.whl' + + +@pytest.mark.parametrize("name", ["purelib", "abc"]) +def test_wheel_with_file_in_data_dir_has_reasonable_error( + script, tmpdir, name +): + """Normally we expect entities in the .data directory to be in a + subdirectory, but if they are not then we should show a reasonable error + message that includes the path. + """ + wheel_path = make_wheel( + "simple", "0.1.0", extra_data_files={name: "hello world"} + ).save_to_dir(tmpdir) + + result = script.pip( + "install", "--no-index", str(wheel_path), expect_error=True + ) + assert "simple-0.1.0.data/{}".format(name) in result.stderr From 3f9b326c115741a0a60e647e242961d80ccda08a Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 29 Jul 2020 18:06:25 -0400 Subject: [PATCH 2363/3170] Provide a reasonable error on invalid scheme keys Originally we would throw an `AttributeError` if a bad scheme key was used. After refactoring we would throw a `KeyError`, which isn't much better. Now we call out the wheel being processed, scheme key we didn't recognize, and provide a list of the valid scheme keys. This would likely be useful for people developing/testing the wheel. --- src/pip/_internal/operations/install/wheel.py | 14 +++++++++++++- tests/functional/test_install_wheel.py | 15 +++++++++++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 0bd7b28be4f..681fc0aa8ef 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -592,7 +592,19 @@ def make_data_scheme_file(record_path): ).format(wheel_path, record_path) raise InstallationError(message) - scheme_path = scheme_paths[scheme_key] + try: + scheme_path = scheme_paths[scheme_key] + except KeyError: + valid_scheme_keys = ", ".join(sorted(scheme_paths)) + message = ( + "Unknown scheme key used in {}: {} (for file {!r}). .data" + " directory contents should be in subdirectories named" + " with a valid scheme key ({})" + ).format( + wheel_path, scheme_key, record_path, valid_scheme_keys + ) + raise InstallationError(message) + dest_path = os.path.join(scheme_path, dest_subpath) assert_no_path_traversal(scheme_path, dest_path) return ZipBackedFile(record_path, dest_path, zip_file) diff --git a/tests/functional/test_install_wheel.py b/tests/functional/test_install_wheel.py index 89d24a62fc0..ad4e749676f 100644 --- a/tests/functional/test_install_wheel.py +++ b/tests/functional/test_install_wheel.py @@ -699,3 +699,18 @@ def test_wheel_with_file_in_data_dir_has_reasonable_error( "install", "--no-index", str(wheel_path), expect_error=True ) assert "simple-0.1.0.data/{}".format(name) in result.stderr + + +def test_wheel_with_unknown_subdir_in_data_dir_has_reasonable_error( + script, tmpdir +): + wheel_path = make_wheel( + "simple", + "0.1.0", + extra_data_files={"unknown/hello.txt": "hello world"} + ).save_to_dir(tmpdir) + + result = script.pip( + "install", "--no-index", str(wheel_path), expect_error=True + ) + assert "simple-0.1.0.data/unknown/hello.txt" in result.stderr From 127c5b026c9e810a8bfb795662cd1be6567b8b64 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 29 Jul 2020 18:14:58 -0400 Subject: [PATCH 2364/3170] Add news --- news/8654.bugfix | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 news/8654.bugfix diff --git a/news/8654.bugfix b/news/8654.bugfix new file mode 100644 index 00000000000..ec0df7a903e --- /dev/null +++ b/news/8654.bugfix @@ -0,0 +1,2 @@ +Trace a better error message on installation failure due to invalid ``.data`` +files in wheels. From 1fd5098b2480eee5be4a0033b43cb02605bc0fe1 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 30 Jul 2020 21:58:05 +0800 Subject: [PATCH 2365/3170] Canonicalize name in check_if_exists The previous implementation uses pkg_resources.get_distribution(), which does not canonicalize the package name correctly, and fails when combined with pip's own get_distribution(), which does canonicalize names. This makes InstallRequirement.check_if_exists() only use pip's own canonicalization logic so different package name forms are matched as expected. --- news/8645.bugfix | 2 ++ src/pip/_internal/req/req_install.py | 28 +++++++------------- tests/functional/test_install_upgrade.py | 33 ++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 19 deletions(-) create mode 100644 news/8645.bugfix diff --git a/news/8645.bugfix b/news/8645.bugfix new file mode 100644 index 00000000000..a388d24e4ad --- /dev/null +++ b/news/8645.bugfix @@ -0,0 +1,2 @@ +Correctly find already-installed distributions with dot (``.``) in the name +and uninstall them when needed. diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 644930a1528..4759f4af6f0 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -429,25 +429,13 @@ def check_if_exists(self, use_user_site): """ if self.req is None: return - # get_distribution() will resolve the entire list of requirements - # anyway, and we've already determined that we need the requirement - # in question, so strip the marker so that we don't try to - # evaluate it. - no_marker = Requirement(str(self.req)) - no_marker.marker = None - - # pkg_resources uses the canonical name to look up packages, but - # the name passed passed to get_distribution is not canonicalized - # so we have to explicitly convert it to a canonical name - no_marker.name = canonicalize_name(no_marker.name) - try: - self.satisfied_by = pkg_resources.get_distribution(str(no_marker)) - except pkg_resources.DistributionNotFound: + existing_dist = get_distribution(self.req.name) + if not existing_dist: return - except pkg_resources.VersionConflict: - existing_dist = get_distribution( - self.req.name - ) + + existing_version = existing_dist.parsed_version + if not self.req.specifier.contains(existing_version, prereleases=True): + self.satisfied_by = None if use_user_site: if dist_in_usersite(existing_dist): self.should_reinstall = True @@ -461,11 +449,13 @@ def check_if_exists(self, use_user_site): else: self.should_reinstall = True else: - if self.editable and self.satisfied_by: + if self.editable: self.should_reinstall = True # when installing editables, nothing pre-existing should ever # satisfy self.satisfied_by = None + else: + self.satisfied_by = existing_dist # Things valid for wheels @property diff --git a/tests/functional/test_install_upgrade.py b/tests/functional/test_install_upgrade.py index e45bf31483e..02e221101c5 100644 --- a/tests/functional/test_install_upgrade.py +++ b/tests/functional/test_install_upgrade.py @@ -1,3 +1,4 @@ +import itertools import os import sys import textwrap @@ -7,6 +8,7 @@ from tests.lib import pyversion # noqa: F401 from tests.lib import assert_all_changes from tests.lib.local_repos import local_checkout +from tests.lib.wheel import make_wheel @pytest.mark.network @@ -439,3 +441,34 @@ def prep_ve(self, script, version, pip_src, distribute=False): cwd=pip_src, expect_stderr=True, ) + + +@pytest.mark.parametrize("req1, req2", list(itertools.product( + ["foo.bar", "foo_bar", "foo-bar"], ["foo.bar", "foo_bar", "foo-bar"], +))) +def test_install_find_existing_package_canonicalize(script, req1, req2): + """Ensure an already-installed dist is found no matter how the dist name + was normalized on installation. (pypa/pip#8645) + """ + # Create and install a package that's not available in the later stage. + req_container = script.scratch_path.joinpath("foo-bar") + req_container.mkdir() + req_path = make_wheel("foo_bar", "1.0").save_to_dir(req_container) + script.pip("install", "--no-index", req_path) + + # Depend on the previously installed, but now unavailable package. + pkg_container = script.scratch_path.joinpath("pkg") + pkg_container.mkdir() + make_wheel( + "pkg", + "1.0", + metadata_updates={"Requires-Dist": req2}, + ).save_to_dir(pkg_container) + + # Ensure the previously installed package can be correctly used to match + # the dependency. + result = script.pip( + "install", "--no-index", "--find-links", pkg_container, "pkg", + ) + satisfied_message = "Requirement already satisfied: {}".format(req2) + assert satisfied_message in result.stdout, str(result) From 27b4980c6c747b10818fcaf11f0fe1bad72754fb Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara <sh@changeset.nyc> Date: Thu, 30 Jul 2020 13:58:47 -0400 Subject: [PATCH 2366/3170] Update documentation to reflect updated resolver feature flag Followup to #8371, #8530, #8513. --- .github/ISSUE_TEMPLATE/bug-report.md | 2 +- docs/html/user_guide.rst | 16 ++++++++-------- news/8660.doc | 1 + 3 files changed, 10 insertions(+), 9 deletions(-) create mode 100644 news/8660.doc diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index fdefbe1a431..157be28b678 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -4,7 +4,7 @@ about: Create a report to help us improve --- <!-- -If you're reporting an issue for `--unstable-feature=resolver`, use the "Dependency resolver failures / errors" template instead. +If you're reporting an issue for `--use-feature=2020-resolver`, use the "Dependency resolver failures / errors" template instead. --> **Environment** diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 702a97d0ca1..31887a2880c 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -783,7 +783,7 @@ specified packages due to conflicting dependencies (a ``ResolutionImpossible`` error). This documentation is specific to the new resolver, which you can use -with the flag ``--unstable-feature=resolver``. +with the flag ``--use-feature=2020-resolver``. Understanding your error message -------------------------------- @@ -1029,13 +1029,13 @@ Changes to the pip dependency resolver in 20.2 (2020) ===================================================== pip 20.1 included an alpha version of the new resolver (hidden behind -an optional ``--unstable-feature=resolver`` flag). pip 20.2 includes a -robust beta of the new resolver (hidden behind an optional -``--use-feature=2020-resolver`` flag) that we encourage you to -test. We will continue to improve the pip dependency resolver in -response to testers' feedback. Please give us feedback through the -`resolver testing survey`_. This will help us prepare to release pip -20.3, with the new resolver on by default, in October. +an optional ``--unstable-feature=resolver`` flag). pip 20.2 removes +that flag, and includes a robust beta of the new resolver (hidden +behind an optional ``--use-feature=2020-resolver`` flag) that we +encourage you to test. We will continue to improve the pip dependency +resolver in response to testers' feedback. Please give us feedback +through the `resolver testing survey`_. This will help us prepare to +release pip 20.3, with the new resolver on by default, in October. Watch out for ------------- diff --git a/news/8660.doc b/news/8660.doc new file mode 100644 index 00000000000..45b71cc26a4 --- /dev/null +++ b/news/8660.doc @@ -0,0 +1 @@ +Fix feature flag name in docs. From c3e1a153fd252a3b279fbd1598a4a9771a120a36 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 31 Jul 2020 06:38:43 +0800 Subject: [PATCH 2367/3170] Improve SVN version parser SVN has multiple distributions on Windows, e.g. SlikSVN, CollabNet. Some of them suffix the version with a "-{distro}" part, which causes the previous implementation to fail. This patch removes that final part and make the version logic work. --- src/pip/_internal/vcs/subversion.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/vcs/subversion.py b/src/pip/_internal/vcs/subversion.py index 14825f791a4..ab134970b05 100644 --- a/src/pip/_internal/vcs/subversion.py +++ b/src/pip/_internal/vcs/subversion.py @@ -213,6 +213,8 @@ def call_vcs_version(self): # compiled Feb 25 2019, 14:20:39 on x86_64-apple-darwin17.0.0 # svn, version 1.7.14 (r1542130) # compiled Mar 28 2018, 08:49:13 on x86_64-pc-linux-gnu + # svn, version 1.12.0-SlikSvn (SlikSvn/1.12.0) + # compiled May 28 2019, 13:44:56 on x86_64-microsoft-windows6.2 version_prefix = 'svn, version ' version = self.run_command(['--version']) @@ -220,7 +222,7 @@ def call_vcs_version(self): return () version = version[len(version_prefix):].split()[0] - version_list = version.split('.') + version_list = version.partition('-')[0].split('.') try: parsed_version = tuple(map(int, version_list)) except ValueError: From c8596e141009bb8536a43fda43ed335c55721f60 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 31 Jul 2020 06:45:40 +0800 Subject: [PATCH 2368/3170] News --- news/8665.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/8665.bugfix diff --git a/news/8665.bugfix b/news/8665.bugfix new file mode 100644 index 00000000000..0ce45846360 --- /dev/null +++ b/news/8665.bugfix @@ -0,0 +1 @@ +Fix SVN version detection for alternative SVN distributions. From f31898e18c5235f43cfd82f170d79ee5df3927bc Mon Sep 17 00:00:00 2001 From: Ed Morley <501702+edmorley@users.noreply.github.com> Date: Fri, 31 Jul 2020 13:15:23 +0100 Subject: [PATCH 2369/3170] Fix typos in the docs about conflicting dependencies Previously: - the example wildcard version string was being rendered with a stray space (`== 3.1. *` instead of `== 3.1.*`) due to the markup being split over two lines - the "Dependency Hell" Wikipedia URL 404ed due to the trailing `>` --- docs/html/user_guide.rst | 6 +++--- news/AE707F60-0ABE-4DBA-98AA-59CE8F989386.trivial | 0 2 files changed, 3 insertions(+), 3 deletions(-) create mode 100644 news/AE707F60-0ABE-4DBA-98AA-59CE8F989386.trivial diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 31887a2880c..4f276e46aa1 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -838,8 +838,8 @@ specifying package versions (e.g. ``~=`` or ``*``): semantic versioning.", "``~=3.1``: version ``3.1`` or later, but not version ``4.0`` or later. ``~=3.1.2``: version ``3.1.2`` or later, but not version ``3.2.0`` or later." - ``*``,Can be used at the end of a version number to represent "all", "``== 3. - 1.*``: any version that starts with ``3.1``. Equivalent to ``~=3.1.0``." + ``*``,Can be used at the end of a version number to represent "all", "``== 3.1.*``: + any version that starts with ``3.1``. Equivalent to ``~=3.1.0``." The detailed specification of supported comparison operators can be found in :pep:`440`. @@ -945,7 +945,7 @@ Unfortunately, **the pip team cannot provide support for individual dependency conflict errors**. Please *only* open a ticket on the `pip issue tracker`_ if you believe that your problem has exposed a bug in pip. -.. _dependency hell: https://en.wikipedia.org/wiki/Dependency_hell> +.. _dependency hell: https://en.wikipedia.org/wiki/Dependency_hell .. _Awesome Python: https://python.libhunt.com/ .. _Python user Discourse: https://discuss.python.org/c/users/7 .. _Python user forums: https://www.python.org/community/forums/ diff --git a/news/AE707F60-0ABE-4DBA-98AA-59CE8F989386.trivial b/news/AE707F60-0ABE-4DBA-98AA-59CE8F989386.trivial new file mode 100644 index 00000000000..e69de29bb2d From ea47920767347cc5cfed5e69e9429229e7b9b2ec Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sat, 1 Aug 2020 02:03:45 +0800 Subject: [PATCH 2370/3170] Add test case for SlikSVN version parsing --- tests/unit/test_vcs.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/unit/test_vcs.py b/tests/unit/test_vcs.py index 590cb5c0b75..93598c36739 100644 --- a/tests/unit/test_vcs.py +++ b/tests/unit/test_vcs.py @@ -443,6 +443,9 @@ def test_subversion__call_vcs_version(): ('svn, version 1.10.3 (r1842928)\n' ' compiled Feb 25 2019, 14:20:39 on x86_64-apple-darwin17.0.0', (1, 10, 3)), + ('svn, version 1.12.0-SlikSvn (SlikSvn/1.12.0)\n' + ' compiled May 28 2019, 13:44:56 on x86_64-microsoft-windows6.2', + (1, 12, 0)), ('svn, version 1.9.7 (r1800392)', (1, 9, 7)), ('svn, version 1.9.7a1 (r1800392)', ()), ('svn, version 1.9 (r1800392)', (1, 9)), From 0ef877339a270c760a51c3a61e55c2f4f86f84ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Sat, 1 Aug 2020 16:57:30 +0700 Subject: [PATCH 2371/3170] Make assertions independent of log prefixes --- ...81396b-ebe4-46a9-b38f-e6b0da97d53a.trivial | 0 tests/functional/test_install_check.py | 20 +++++++++---------- 2 files changed, 10 insertions(+), 10 deletions(-) create mode 100644 news/2581396b-ebe4-46a9-b38f-e6b0da97d53a.trivial diff --git a/news/2581396b-ebe4-46a9-b38f-e6b0da97d53a.trivial b/news/2581396b-ebe4-46a9-b38f-e6b0da97d53a.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/functional/test_install_check.py b/tests/functional/test_install_check.py index 88609422bb4..96ed6b6e18a 100644 --- a/tests/functional/test_install_check.py +++ b/tests/functional/test_install_check.py @@ -1,8 +1,10 @@ from tests.lib import create_test_package_with_setup -def contains_expected_lines(string, expected_lines): - return set(expected_lines) <= set(string.splitlines()) +def assert_contains_expected_lines(string, expected_lines): + lines = string.splitlines() + for expected_line in expected_lines: + assert any(line.endswith(expected_line) for line in lines) def test_check_install_canonicalization(script): @@ -38,7 +40,7 @@ def test_check_install_canonicalization(script): "pkga 1.0 requires SPECIAL.missing, which is not installed.", ] # Deprecated python versions produce an extra warning on stderr - assert contains_expected_lines(result.stderr, expected_lines) + assert_contains_expected_lines(result.stderr, expected_lines) assert result.returncode == 0 # Install the second missing package and expect that there is no warning @@ -55,7 +57,7 @@ def test_check_install_canonicalization(script): expected_lines = [ "No broken requirements found.", ] - assert contains_expected_lines(result.stdout, expected_lines) + assert_contains_expected_lines(result.stdout, expected_lines) assert result.returncode == 0 @@ -85,12 +87,10 @@ def test_check_install_does_not_warn_for_out_of_graph_issues(script): result = script.pip( 'install', '--no-index', pkg_conflict_path, allow_stderr_error=True, ) - assert contains_expected_lines(result.stderr, [ + assert_contains_expected_lines(result.stderr, [ "broken 1.0 requires missing, which is not installed.", - ( - "broken 1.0 requires conflict<1.0, but " - "you'll have conflict 1.0 which is incompatible." - ), + "broken 1.0 requires conflict<1.0, " + "but you'll have conflict 1.0 which is incompatible." ]) # Install unrelated package @@ -105,4 +105,4 @@ def test_check_install_does_not_warn_for_out_of_graph_issues(script): "broken 1.0 requires missing, which is not installed.", "broken 1.0 has requirement conflict<1.0, but you have conflict 1.0.", ] - assert contains_expected_lines(result.stdout, expected_lines) + assert_contains_expected_lines(result.stdout, expected_lines) From e48a0cb7cba70d7184f7a08b49163cf5a6451b61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Sat, 1 Aug 2020 16:59:07 +0700 Subject: [PATCH 2372/3170] Remove no-longer-used messages --- src/pip/_internal/commands/install.py | 14 -------------- 1 file changed, 14 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 8c2c32fd43f..77ec210d6af 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -21,7 +21,6 @@ from pip._internal.operations.check import check_install_conflicts from pip._internal.req import install_given_reqs from pip._internal.req.req_tracker import get_requirement_tracker -from pip._internal.utils.datetime import today_is_later_than from pip._internal.utils.deprecation import deprecated from pip._internal.utils.distutils_args import parse_distutils_args from pip._internal.utils.filesystem import test_writable_dir @@ -558,19 +557,6 @@ def _warn_about_conflicts(self, conflict_details, new_resolver): "your packages with the new resolver before it becomes the " "default.\n" ) - elif not today_is_later_than(year=2020, month=7, day=31): - # NOTE: trailing newlines here are intentional - parts.append( - "Pip will install or upgrade your package(s) and its " - "dependencies without taking into account other packages you " - "already have installed. This may cause an uncaught " - "dependency conflict.\n" - ) - form_link = "https://forms.gle/cWKMoDs8sUVE29hz9" - parts.append( - "If you would like pip to take your other packages into " - "account, please tell us here: {}\n".format(form_link) - ) # NOTE: There is some duplication here, with commands/check.py for project_name in missing: From 22aec424d96f48e0054505070eb57b873f0089e0 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Date: Sun, 2 Aug 2020 07:46:54 +0530 Subject: [PATCH 2373/3170] Rewrap lines MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Nguyễn Gia Phong <mcsinyx@disroot.org> --- docs/html/user_guide.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 4f276e46aa1..b0b25677ee5 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -838,8 +838,9 @@ specifying package versions (e.g. ``~=`` or ``*``): semantic versioning.", "``~=3.1``: version ``3.1`` or later, but not version ``4.0`` or later. ``~=3.1.2``: version ``3.1.2`` or later, but not version ``3.2.0`` or later." - ``*``,Can be used at the end of a version number to represent "all", "``== 3.1.*``: - any version that starts with ``3.1``. Equivalent to ``~=3.1.0``." + ``*``, Can be used at the end of a version number to represent "all", + "``==3.1.*``: any version that starts with ``3.1``. + Equivalent to ``~=3.1.0``." The detailed specification of supported comparison operators can be found in :pep:`440`. From b97c199cf7f2c308a50be8209ef29fb97e630006 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sun, 2 Aug 2020 07:53:02 +0530 Subject: [PATCH 2374/3170] Point to latest documentation This allows us to update the content users would see as we get feedback. --- src/pip/_internal/resolution/resolvelib/factory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index bd7e3efd9d3..36d6baae975 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -454,6 +454,6 @@ def describe_trigger(parent): return DistributionNotFound( "ResolutionImpossible For help visit: " - "https://pip.pypa.io/en/stable/user_guide/" + "https://pip.pypa.io/en/latest/user_guide/" "#fixing-conflicting-dependencies" ) From c412613efe94f3dc7e836352bfe0c86d4f789c47 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sun, 2 Aug 2020 07:55:49 +0530 Subject: [PATCH 2375/3170] Tweak ResolutionImpossible error line This makes it more consistent with how error "summary" lines look. eg: IndexError: list index out of range ModuleNotFoundError: No module named 'notamodule' --- src/pip/_internal/resolution/resolvelib/factory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 36d6baae975..dab23aa09d1 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -453,7 +453,7 @@ def describe_trigger(parent): logger.info(msg) return DistributionNotFound( - "ResolutionImpossible For help visit: " + "ResolutionImpossible: for help visit " "https://pip.pypa.io/en/latest/user_guide/" "#fixing-conflicting-dependencies" ) From b4632d080bdf8124006c03cb829ffb5186a5de52 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sun, 2 Aug 2020 08:20:16 +0800 Subject: [PATCH 2376/3170] Failing test for new resolver + extras + --no-deps --- tests/yaml/extras.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/tests/yaml/extras.yml b/tests/yaml/extras.yml index 6e2a1b17e7b..ac68fae4979 100644 --- a/tests/yaml/extras.yml +++ b/tests/yaml/extras.yml @@ -40,3 +40,10 @@ cases: - E 1.0.0 - F 1.0.0 skip: old +- + request: + - install: D[extra_1] + options: --no-deps + response: + - state: + - D 1.0.0 From 3ce63a62d76fac211861d42770ca6072c7c97edd Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sun, 2 Aug 2020 08:22:04 +0800 Subject: [PATCH 2377/3170] Ask candidates for dependencies even on --no-deps ExtrasCandidate need to provide one dependency on the non-extra-ed self. --- .../_internal/resolution/resolvelib/base.py | 4 +-- .../resolution/resolvelib/candidates.py | 30 +++++++++++-------- .../resolution/resolvelib/provider.py | 8 +++-- 3 files changed, 25 insertions(+), 17 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/base.py b/src/pip/_internal/resolution/resolvelib/base.py index a155a1101ad..7f71ef4c425 100644 --- a/src/pip/_internal/resolution/resolvelib/base.py +++ b/src/pip/_internal/resolution/resolvelib/base.py @@ -69,8 +69,8 @@ def source_link(self): # type: () -> Optional[Link] raise NotImplementedError("Override in subclass") - def iter_dependencies(self): - # type: () -> Iterable[Optional[Requirement]] + def iter_dependencies(self, ignore_dependencies): + # type: (bool) -> Iterable[Optional[Requirement]] raise NotImplementedError("Override in subclass") def get_install_requirement(self): diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index c289bb5839c..912f72a76e3 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -275,8 +275,10 @@ def _get_requires_python_specifier(self): return None return spec - def iter_dependencies(self): - # type: () -> Iterable[Optional[Requirement]] + def iter_dependencies(self, ignore_dependencies): + # type: (bool) -> Iterable[Optional[Requirement]] + if ignore_dependencies: + return for r in self.dist.requires(): yield self._factory.make_requirement_from_spec(str(r), self._ireq) python_dep = self._factory.make_requires_python_requirement( @@ -420,8 +422,10 @@ def format_for_error(self): # type: () -> str return "{} {} (Installed)".format(self.name, self.version) - def iter_dependencies(self): - # type: () -> Iterable[Optional[Requirement]] + def iter_dependencies(self, ignore_dependencies): + # type: (bool) -> Iterable[Optional[Requirement]] + if ignore_dependencies: + return for r in self.dist.requires(): yield self._factory.make_requirement_from_spec(str(r), self._ireq) @@ -519,10 +523,16 @@ def source_link(self): # type: () -> Optional[Link] return self.base.source_link - def iter_dependencies(self): - # type: () -> Iterable[Optional[Requirement]] + def iter_dependencies(self, ignore_dependencies): + # type: (bool) -> Iterable[Optional[Requirement]] factory = self.base._factory + # Add a dependency on the exact base + # (See note 2b in the class docstring) + yield factory.make_requirement_from_candidate(self.base) + if ignore_dependencies: + return + # The user may have specified extras that the candidate doesn't # support. We ignore any unsupported extras here. valid_extras = self.extras.intersection(self.base.dist.extras) @@ -535,10 +545,6 @@ def iter_dependencies(self): extra ) - # Add a dependency on the exact base - # (See note 2b in the class docstring) - yield factory.make_requirement_from_candidate(self.base) - for r in self.base.dist.requires(valid_extras): requirement = factory.make_requirement_from_spec( str(r), self.base._ireq, valid_extras, @@ -585,8 +591,8 @@ def format_for_error(self): # type: () -> str return "Python {}".format(self.version) - def iter_dependencies(self): - # type: () -> Iterable[Optional[Requirement]] + def iter_dependencies(self, ignore_dependencies): + # type: (bool) -> Iterable[Optional[Requirement]] return () def get_install_requirement(self): diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index 72f16205981..50b16a12e13 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -145,6 +145,8 @@ def is_satisfied_by(self, requirement, candidate): def get_dependencies(self, candidate): # type: (Candidate) -> Sequence[Requirement] - if self._ignore_dependencies: - return [] - return [r for r in candidate.iter_dependencies() if r is not None] + return [ + r + for r in candidate.iter_dependencies(self._ignore_dependencies) + if r is not None + ] From 77cedcf52f823039edfcad19af1f933926ba5d62 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sun, 2 Aug 2020 08:24:15 +0800 Subject: [PATCH 2378/3170] News --- news/8677.bugfix | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 news/8677.bugfix diff --git a/news/8677.bugfix b/news/8677.bugfix new file mode 100644 index 00000000000..e9efd827977 --- /dev/null +++ b/news/8677.bugfix @@ -0,0 +1,2 @@ +New resolver: Correctly include the base package when specified with extras +in ``--no-deps`` mode. From 32b5e43c79c3dab421637da5870cf4e6341eab58 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Mon, 3 Aug 2020 05:28:10 +0800 Subject: [PATCH 2379/3170] Flip the flag with another name --- src/pip/_internal/resolution/resolvelib/base.py | 2 +- .../_internal/resolution/resolvelib/candidates.py | 14 +++++++------- .../_internal/resolution/resolvelib/provider.py | 3 ++- 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/base.py b/src/pip/_internal/resolution/resolvelib/base.py index 7f71ef4c425..9245747bf2b 100644 --- a/src/pip/_internal/resolution/resolvelib/base.py +++ b/src/pip/_internal/resolution/resolvelib/base.py @@ -69,7 +69,7 @@ def source_link(self): # type: () -> Optional[Link] raise NotImplementedError("Override in subclass") - def iter_dependencies(self, ignore_dependencies): + def iter_dependencies(self, with_requires): # type: (bool) -> Iterable[Optional[Requirement]] raise NotImplementedError("Override in subclass") diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 912f72a76e3..46cc7e7a236 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -275,9 +275,9 @@ def _get_requires_python_specifier(self): return None return spec - def iter_dependencies(self, ignore_dependencies): + def iter_dependencies(self, with_requires): # type: (bool) -> Iterable[Optional[Requirement]] - if ignore_dependencies: + if not with_requires: return for r in self.dist.requires(): yield self._factory.make_requirement_from_spec(str(r), self._ireq) @@ -422,9 +422,9 @@ def format_for_error(self): # type: () -> str return "{} {} (Installed)".format(self.name, self.version) - def iter_dependencies(self, ignore_dependencies): + def iter_dependencies(self, with_requires): # type: (bool) -> Iterable[Optional[Requirement]] - if ignore_dependencies: + if not with_requires: return for r in self.dist.requires(): yield self._factory.make_requirement_from_spec(str(r), self._ireq) @@ -523,14 +523,14 @@ def source_link(self): # type: () -> Optional[Link] return self.base.source_link - def iter_dependencies(self, ignore_dependencies): + def iter_dependencies(self, with_requires): # type: (bool) -> Iterable[Optional[Requirement]] factory = self.base._factory # Add a dependency on the exact base # (See note 2b in the class docstring) yield factory.make_requirement_from_candidate(self.base) - if ignore_dependencies: + if not with_requires: return # The user may have specified extras that the candidate doesn't @@ -591,7 +591,7 @@ def format_for_error(self): # type: () -> str return "Python {}".format(self.version) - def iter_dependencies(self, ignore_dependencies): + def iter_dependencies(self, with_requires): # type: (bool) -> Iterable[Optional[Requirement]] return () diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index 50b16a12e13..b2eb9d06ea5 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -145,8 +145,9 @@ def is_satisfied_by(self, requirement, candidate): def get_dependencies(self, candidate): # type: (Candidate) -> Sequence[Requirement] + with_requires = not self._ignore_dependencies return [ r - for r in candidate.iter_dependencies(self._ignore_dependencies) + for r in candidate.iter_dependencies(with_requires) if r is not None ] From d957cc94c8ff9e83503f0a509ecbc90b61180775 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 2 Aug 2020 11:04:27 -0400 Subject: [PATCH 2380/3170] Don't set _dist until it has been validated Previously a call to `_fetch_metadata` could result in several possible outcomes: 1. `_dist` set, `_provided` not set, dist returned - for lazy wheels 2. `_dist` set, `_provided` not set, exception - for bad lazy wheels 3. `_dist` not set, `_provided` not set, exception - for non-lazy req exceptions 4. `_dist` set, `_provided` not set, exception - for bad non-lazy reqs 5. `_dist` set, `_provided` set, dist returned - for non-lazy reqs and probably more. Our intent is to use `_dist` being set as the indicator of "this requirement has been fully processed successfully" and discard `_prepared`, since we don't actually rely on any of the other states (they simply lead to a failure or in the future a retry). --- .../resolution/resolvelib/candidates.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 3d8e399a7bf..a203fa3c012 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -203,12 +203,11 @@ def _prepare_distribution(self): # type: () -> Distribution raise NotImplementedError("Override in subclass") - def _check_metadata_consistency(self): - # type: () -> None + def _check_metadata_consistency(self, dist): + # type: (Distribution) -> None """Check for consistency of project name and version of dist.""" # TODO: (Longer term) Rather than abort, reject this candidate # and backtrack. This would need resolvelib support. - dist = self._dist # type: Distribution name = canonicalize_name(dist.project_name) if self._name is not None and self._name != name: raise MetadataInconsistent(self._ireq, "name", dist.project_name) @@ -221,13 +220,14 @@ def _prepare(self): if self._prepared: return try: - self._dist = self._prepare_distribution() + dist = self._prepare_distribution() except HashError as e: e.req = self._ireq raise - assert self._dist is not None, "Distribution already installed" - self._check_metadata_consistency() + assert dist is not None, "Distribution already installed" + self._check_metadata_consistency(dist) + self._dist = dist self._prepared = True def _fetch_metadata(self): @@ -247,8 +247,9 @@ def _fetch_metadata(self): ) url = self._link.url.split('#', 1)[0] session = preparer.downloader._session - self._dist = dist_from_wheel_url(self._name, url, session) - self._check_metadata_consistency() + dist = dist_from_wheel_url(self._name, url, session) + self._check_metadata_consistency(dist) + self._dist = dist if self._dist is None: self._prepare() From 7289625734e07f425a616727c2c9925a9df724d8 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 2 Aug 2020 11:08:01 -0400 Subject: [PATCH 2381/3170] Remove redundant guard variable Now that `_dist` is only set on success, we can use it to guard against repeated execution instead of `_prepared`. As a result there are now only two possible outcomes for calling `dist`: 1. `_dist` set and returned - lazy and non-lazy req 2. `_dist` not set and exception raised - bad lazy or bad non-lazy req --- src/pip/_internal/resolution/resolvelib/candidates.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index a203fa3c012..387d6ec261b 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -147,7 +147,6 @@ def __init__( self._name = name self._version = version self._dist = None # type: Optional[Distribution] - self._prepared = False def __repr__(self): # type: () -> str @@ -217,7 +216,7 @@ def _check_metadata_consistency(self, dist): def _prepare(self): # type: () -> None - if self._prepared: + if self._dist is not None: return try: dist = self._prepare_distribution() @@ -228,7 +227,6 @@ def _prepare(self): assert dist is not None, "Distribution already installed" self._check_metadata_consistency(dist) self._dist = dist - self._prepared = True def _fetch_metadata(self): # type: () -> None From 7a5e043776d6601f1c425e7a18a4b6a1538baa99 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 2 Aug 2020 11:09:00 -0400 Subject: [PATCH 2382/3170] Remove unnecessary check for _dist Since `_prepare` now internally validates that `_dist` isn't set, we don't need to. --- src/pip/_internal/resolution/resolvelib/candidates.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 387d6ec261b..2fa8e2fcf34 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -248,8 +248,7 @@ def _fetch_metadata(self): dist = dist_from_wheel_url(self._name, url, session) self._check_metadata_consistency(dist) self._dist = dist - if self._dist is None: - self._prepare() + self._prepare() @property def dist(self): From 4d94ae4c40dd6a22bf0062c4e899aa23bc09250d Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 2 Aug 2020 11:10:00 -0400 Subject: [PATCH 2383/3170] Move non-lazy req fallback outside of `_fetch_metadata` No change in behavior, we just want to unify "requirements processing" and moving this function out is a prereq for moving `_fetch_metadata` in. --- src/pip/_internal/resolution/resolvelib/candidates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 2fa8e2fcf34..eefca5ed65e 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -248,13 +248,13 @@ def _fetch_metadata(self): dist = dist_from_wheel_url(self._name, url, session) self._check_metadata_consistency(dist) self._dist = dist - self._prepare() @property def dist(self): # type: () -> Distribution if self._dist is None: self._fetch_metadata() + self._prepare() return self._dist def _get_requires_python_specifier(self): From 8c3c0ade7831d7e82b2390d1fcd1f41c1b0e80f7 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 2 Aug 2020 11:11:00 -0400 Subject: [PATCH 2384/3170] Move _fetch_metadata into _prepare Since `_prepare` is called in two places, we preserve the `if self._dist is not None` protection above the new call to `_fetch_metadata`. The second `if` in `_prepare` handles the early return required when processing a lazy wheel. --- src/pip/_internal/resolution/resolvelib/candidates.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index eefca5ed65e..d810bb2e1ac 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -216,6 +216,9 @@ def _check_metadata_consistency(self, dist): def _prepare(self): # type: () -> None + if self._dist is not None: + return + self._fetch_metadata() if self._dist is not None: return try: @@ -253,7 +256,6 @@ def _fetch_metadata(self): def dist(self): # type: () -> Distribution if self._dist is None: - self._fetch_metadata() self._prepare() return self._dist From a72d04f734150992328442113ee9df3ce2f2fb00 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 2 Aug 2020 11:35:47 -0400 Subject: [PATCH 2385/3170] Move common processing out of _fetch_metadata Returning a `Distribution` makes `_fetch_metadata` look more like `_prepare_distribution`, in preparation for moving it there next. --- .../_internal/resolution/resolvelib/candidates.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index d810bb2e1ac..2a848c276d7 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -218,8 +218,10 @@ def _prepare(self): # type: () -> None if self._dist is not None: return - self._fetch_metadata() - if self._dist is not None: + dist = self._fetch_metadata() + if dist is not None: + self._check_metadata_consistency(dist) + self._dist = dist return try: dist = self._prepare_distribution() @@ -232,7 +234,7 @@ def _prepare(self): self._dist = dist def _fetch_metadata(self): - # type: () -> None + # type: () -> Optional[Distribution] """Fetch metadata, using lazy wheel if possible.""" preparer = self._factory.preparer use_lazy_wheel = self._factory.use_lazy_wheel @@ -248,9 +250,8 @@ def _fetch_metadata(self): ) url = self._link.url.split('#', 1)[0] session = preparer.downloader._session - dist = dist_from_wheel_url(self._name, url, session) - self._check_metadata_consistency(dist) - self._dist = dist + return dist_from_wheel_url(self._name, url, session) + return None @property def dist(self): From ec5b6d7b8091b80533d3f2268eb3df16ad3d19f6 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 2 Aug 2020 11:38:34 -0400 Subject: [PATCH 2386/3170] Remove extra metadata consistency check Instead of an early return, we fall through to the existing check at the end of this function. This aligns our treatment of `_fetch_metadata` and `_prepare_distribution`. --- src/pip/_internal/resolution/resolvelib/candidates.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 2a848c276d7..a0bc3d19153 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -219,12 +219,9 @@ def _prepare(self): if self._dist is not None: return dist = self._fetch_metadata() - if dist is not None: - self._check_metadata_consistency(dist) - self._dist = dist - return try: - dist = self._prepare_distribution() + if dist is None: + dist = self._prepare_distribution() except HashError as e: e.req = self._ireq raise From 45ab317610a0e713c515d405d39f79c8746a5056 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 2 Aug 2020 11:44:42 -0400 Subject: [PATCH 2387/3170] Move call to _fetch_metadata next to call to RequirementPreparer Since wheels can't be editable, we can move this into LinkCandidate, closer to `RequirementPreparer.prepare_linked_requirement` into which we want to integrate `_fetch_metadata`. --- src/pip/_internal/resolution/resolvelib/candidates.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index a0bc3d19153..d7f03147652 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -218,10 +218,8 @@ def _prepare(self): # type: () -> None if self._dist is not None: return - dist = self._fetch_metadata() try: - if dist is None: - dist = self._prepare_distribution() + dist = self._prepare_distribution() except HashError as e: e.req = self._ireq raise @@ -322,6 +320,9 @@ def __init__( def _prepare_distribution(self): # type: () -> Distribution + dist = self._fetch_metadata() + if dist is not None: + return dist return self._factory.preparer.prepare_linked_requirement( self._ireq, parallel_builds=True, ) From e49dcfdc35e7c941df1fc7388adb78c1981496ce Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 2 Aug 2020 11:52:49 -0400 Subject: [PATCH 2388/3170] Move lazy_wheel warning out of Resolver This warning just needs to be traced in one place for all commands, there's no need for the resolver to know about it. Moving the warning out of the Resolver will make it easier to change how we provide the option. --- src/pip/_internal/cli/req_command.py | 13 ++++++++++++- src/pip/_internal/resolution/resolvelib/resolver.py | 8 -------- 2 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 78b5ce6a141..fcbdb70c21c 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -259,6 +259,17 @@ def make_resolver( # "Resolver" class being redefined. if '2020-resolver' in options.features_enabled: import pip._internal.resolution.resolvelib.resolver + + lazy_wheel = 'fast-deps' in options.features_enabled + if lazy_wheel: + logger.warning( + 'pip is using lazily downloaded wheels using HTTP ' + 'range requests to obtain dependency information. ' + 'This experimental feature is enabled through ' + '--use-feature=fast-deps and it is not ready for ' + 'production.' + ) + return pip._internal.resolution.resolvelib.resolver.Resolver( preparer=preparer, finder=finder, @@ -271,7 +282,7 @@ def make_resolver( force_reinstall=force_reinstall, upgrade_strategy=upgrade_strategy, py_version_info=py_version_info, - lazy_wheel='fast-deps' in options.features_enabled, + lazy_wheel=lazy_wheel, ) import pip._internal.resolution.legacy.resolver return pip._internal.resolution.legacy.resolver.Resolver( diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 43ea248632d..db0300e3b8e 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -52,14 +52,6 @@ def __init__( lazy_wheel=False, # type: bool ): super(Resolver, self).__init__() - if lazy_wheel: - logger.warning( - 'pip is using lazily downloaded wheels using HTTP ' - 'range requests to obtain dependency information. ' - 'This experimental feature is enabled through ' - '--use-feature=fast-deps and it is not ready for production.' - ) - assert upgrade_strategy in self._allowed_strategies self.factory = Factory( From f0d4df10eb8f757f6bfb66aa5f3ed34629866447 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 2 Aug 2020 11:58:19 -0400 Subject: [PATCH 2389/3170] Propagate lazy_wheel option through RequirementPreparer Reduces dependence on Candidate (and Resolver (and Factory)). --- src/pip/_internal/cli/req_command.py | 25 +++++++++++-------- src/pip/_internal/operations/prepare.py | 4 +++ .../resolution/resolvelib/candidates.py | 2 +- .../resolution/resolvelib/factory.py | 2 -- .../resolution/resolvelib/resolver.py | 2 -- tests/unit/test_req.py | 1 + 6 files changed, 20 insertions(+), 16 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index fcbdb70c21c..76d83896c2f 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -218,6 +218,19 @@ def make_requirement_preparer( temp_build_dir_path = temp_build_dir.path assert temp_build_dir_path is not None + if '2020-resolver' in options.features_enabled: + lazy_wheel = 'fast-deps' in options.features_enabled + if lazy_wheel: + logger.warning( + 'pip is using lazily downloaded wheels using HTTP ' + 'range requests to obtain dependency information. ' + 'This experimental feature is enabled through ' + '--use-feature=fast-deps and it is not ready for ' + 'production.' + ) + else: + lazy_wheel = False + return RequirementPreparer( build_dir=temp_build_dir_path, src_dir=options.src_dir, @@ -229,6 +242,7 @@ def make_requirement_preparer( finder=finder, require_hashes=options.require_hashes, use_user_site=use_user_site, + lazy_wheel=lazy_wheel, ) @staticmethod @@ -260,16 +274,6 @@ def make_resolver( if '2020-resolver' in options.features_enabled: import pip._internal.resolution.resolvelib.resolver - lazy_wheel = 'fast-deps' in options.features_enabled - if lazy_wheel: - logger.warning( - 'pip is using lazily downloaded wheels using HTTP ' - 'range requests to obtain dependency information. ' - 'This experimental feature is enabled through ' - '--use-feature=fast-deps and it is not ready for ' - 'production.' - ) - return pip._internal.resolution.resolvelib.resolver.Resolver( preparer=preparer, finder=finder, @@ -282,7 +286,6 @@ def make_resolver( force_reinstall=force_reinstall, upgrade_strategy=upgrade_strategy, py_version_info=py_version_info, - lazy_wheel=lazy_wheel, ) import pip._internal.resolution.legacy.resolver return pip._internal.resolution.legacy.resolver.Resolver( diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index e6a34172d79..6c8ceae189f 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -329,6 +329,7 @@ def __init__( finder, # type: PackageFinder require_hashes, # type: bool use_user_site, # type: bool + lazy_wheel, # type: bool ): # type: (...) -> None super(RequirementPreparer, self).__init__() @@ -362,6 +363,9 @@ def __init__( # Should install in user site-packages? self.use_user_site = use_user_site + # Should wheels be downloaded lazily? + self.use_lazy_wheel = lazy_wheel + @property def _download_should_save(self): # type: () -> bool diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index d7f03147652..e780dafd56f 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -232,7 +232,7 @@ def _fetch_metadata(self): # type: () -> Optional[Distribution] """Fetch metadata, using lazy wheel if possible.""" preparer = self._factory.preparer - use_lazy_wheel = self._factory.use_lazy_wheel + use_lazy_wheel = preparer.use_lazy_wheel remote_wheel = self._link.is_wheel and not self._link.is_file if use_lazy_wheel and remote_wheel and not preparer.require_hashes: assert self._name is not None diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index bd7e3efd9d3..c36c45200fe 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -82,7 +82,6 @@ def __init__( ignore_installed, # type: bool ignore_requires_python, # type: bool py_version_info=None, # type: Optional[Tuple[int, ...]] - lazy_wheel=False, # type: bool ): # type: (...) -> None self._finder = finder @@ -93,7 +92,6 @@ def __init__( self._use_user_site = use_user_site self._force_reinstall = force_reinstall self._ignore_requires_python = ignore_requires_python - self.use_lazy_wheel = lazy_wheel self._link_candidate_cache = {} # type: Cache[LinkCandidate] self._editable_candidate_cache = {} # type: Cache[EditableCandidate] diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index db0300e3b8e..e83f35ca73a 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -49,7 +49,6 @@ def __init__( force_reinstall, # type: bool upgrade_strategy, # type: str py_version_info=None, # type: Optional[Tuple[int, ...]] - lazy_wheel=False, # type: bool ): super(Resolver, self).__init__() assert upgrade_strategy in self._allowed_strategies @@ -64,7 +63,6 @@ def __init__( ignore_installed=ignore_installed, ignore_requires_python=ignore_requires_python, py_version_info=py_version_info, - lazy_wheel=lazy_wheel, ) self.ignore_dependencies = ignore_dependencies self.upgrade_strategy = upgrade_strategy diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index 1aee7fcdf0a..b8da863996c 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -89,6 +89,7 @@ def _basic_resolver(self, finder, require_hashes=False): finder=finder, require_hashes=require_hashes, use_user_site=False, + lazy_wheel=False, ) yield Resolver( preparer=preparer, From f4603078cf0ba6415612a3130dc85c57f5337b0e Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 2 Aug 2020 12:11:05 -0400 Subject: [PATCH 2390/3170] Pass InstallRequirement to _fetch_metadata Reduces dependence on Candidate. --- src/pip/_internal/resolution/resolvelib/candidates.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index e780dafd56f..54ade93db54 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -228,15 +228,15 @@ def _prepare(self): self._check_metadata_consistency(dist) self._dist = dist - def _fetch_metadata(self): - # type: () -> Optional[Distribution] + def _fetch_metadata(self, req): + # type: (InstallRequirement) -> Optional[Distribution] """Fetch metadata, using lazy wheel if possible.""" preparer = self._factory.preparer use_lazy_wheel = preparer.use_lazy_wheel remote_wheel = self._link.is_wheel and not self._link.is_file if use_lazy_wheel and remote_wheel and not preparer.require_hashes: assert self._name is not None - logger.info('Collecting %s', self._ireq.req or self._ireq) + logger.info('Collecting %s', req.req or req) # If HTTPRangeRequestUnsupported is raised, fallback silently. with indent_log(), suppress(HTTPRangeRequestUnsupported): logger.info( @@ -320,7 +320,7 @@ def __init__( def _prepare_distribution(self): # type: () -> Distribution - dist = self._fetch_metadata() + dist = self._fetch_metadata(self._ireq) if dist is not None: return dist return self._factory.preparer.prepare_linked_requirement( From defbf82a8fba7c1e3e8cc0e4dcf83fbd313b33df Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 2 Aug 2020 12:13:01 -0400 Subject: [PATCH 2391/3170] Use link from InstallRequirement Since when we generate the InstallRequirement we set the link, these must be the same. Reduces dependence on Candidate. --- src/pip/_internal/resolution/resolvelib/candidates.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 54ade93db54..5da4f08a1ba 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -233,7 +233,9 @@ def _fetch_metadata(self, req): """Fetch metadata, using lazy wheel if possible.""" preparer = self._factory.preparer use_lazy_wheel = preparer.use_lazy_wheel - remote_wheel = self._link.is_wheel and not self._link.is_file + assert self._link == req.link + link = req.link + remote_wheel = link.is_wheel and not link.is_file if use_lazy_wheel and remote_wheel and not preparer.require_hashes: assert self._name is not None logger.info('Collecting %s', req.req or req) @@ -243,7 +245,7 @@ def _fetch_metadata(self, req): 'Obtaining dependency information from %s %s', self._name, self._version, ) - url = self._link.url.split('#', 1)[0] + url = link.url.split('#', 1)[0] session = preparer.downloader._session return dist_from_wheel_url(self._name, url, session) return None From 9e463916d0077d8db6f616c72ec35b80fa289af5 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 2 Aug 2020 12:33:14 -0400 Subject: [PATCH 2392/3170] Extract name and version from Wheel link We happen to know that this is the same treatment that gave us `_name` and `_version` for Wheels in the first place (in `LinkEvaluator`). It's not ideal, however the metadata consistency check that occurs in `Candidate` after creation of a `Distribution` guards us against any deviation in the name and version during our processing. Reduces dependence on Candidate. --- .../_internal/resolution/resolvelib/candidates.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 5da4f08a1ba..12128c690eb 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -7,6 +7,7 @@ from pip._vendor.packaging.version import Version from pip._internal.exceptions import HashError, MetadataInconsistent +from pip._internal.models.wheel import Wheel from pip._internal.network.lazy_wheel import ( HTTPRangeRequestUnsupported, dist_from_wheel_url, @@ -237,17 +238,22 @@ def _fetch_metadata(self, req): link = req.link remote_wheel = link.is_wheel and not link.is_file if use_lazy_wheel and remote_wheel and not preparer.require_hashes: - assert self._name is not None + wheel = Wheel(link.filename) + name = canonicalize_name(wheel.name) + assert self._name == name + # Version may not be present for PEP 508 direct URLs + if self._version is not None: + assert self._version == wheel.version logger.info('Collecting %s', req.req or req) # If HTTPRangeRequestUnsupported is raised, fallback silently. with indent_log(), suppress(HTTPRangeRequestUnsupported): logger.info( 'Obtaining dependency information from %s %s', - self._name, self._version, + name, wheel.version, ) url = link.url.split('#', 1)[0] session = preparer.downloader._session - return dist_from_wheel_url(self._name, url, session) + return dist_from_wheel_url(name, url, session) return None @property From 4e1bff741dba2a15035a949c380b440bbce13927 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 2 Aug 2020 14:07:06 -0400 Subject: [PATCH 2393/3170] Promote Wheel-related assertions to LinkCandidate constructor These are things we know will be true because of the existing wheel processing. In the future we may delegate the extraction of these to the LinkCandidate itself so it doesn't have to be an assertion. --- .../resolution/resolvelib/candidates.py | 20 ++++++++++++++----- 1 file changed, 15 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 12128c690eb..5e5e8ac7299 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -234,16 +234,12 @@ def _fetch_metadata(self, req): """Fetch metadata, using lazy wheel if possible.""" preparer = self._factory.preparer use_lazy_wheel = preparer.use_lazy_wheel - assert self._link == req.link + assert req.link link = req.link remote_wheel = link.is_wheel and not link.is_file if use_lazy_wheel and remote_wheel and not preparer.require_hashes: wheel = Wheel(link.filename) name = canonicalize_name(wheel.name) - assert self._name == name - # Version may not be present for PEP 508 direct URLs - if self._version is not None: - assert self._version == wheel.version logger.info('Collecting %s', req.req or req) # If HTTPRangeRequestUnsupported is raised, fallback silently. with indent_log(), suppress(HTTPRangeRequestUnsupported): @@ -311,6 +307,20 @@ def __init__( logger.debug("Using cached wheel link: %s", cache_entry.link) link = cache_entry.link ireq = make_install_req_from_link(link, template) + assert ireq.link == link + if ireq.link.is_wheel and not ireq.link.is_file: + wheel = Wheel(ireq.link.filename) + wheel_name = canonicalize_name(wheel.name) + assert name == wheel_name, ( + "{!r} != {!r} for wheel".format(name, wheel_name) + ) + # Version may not be present for PEP 508 direct URLs + if version is not None: + assert str(version) == wheel.version, ( + "{!r} != {!r} for wheel {}".format( + version, wheel.version, name + ) + ) if (cache_entry is not None and cache_entry.persistent and From 6c4d4f3b78f54fe305da9882b37a1d88769a4d23 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 2 Aug 2020 14:21:49 -0400 Subject: [PATCH 2394/3170] Move _fetch_metadata to RequirementPreparer The fact that all of this functionality can be put in terms of the `RequirementPreparer` indicates that, at least at this point, this is the cleanest place to put this functionality. --- src/pip/_internal/operations/prepare.py | 32 +++++++++++++++++++ .../resolution/resolvelib/candidates.py | 32 ------------------- 2 files changed, 32 insertions(+), 32 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 6c8ceae189f..8516ea75b10 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -9,6 +9,8 @@ import os import shutil +from pip._vendor.contextlib2 import suppress +from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.six import PY2 from pip._internal.distributions import ( @@ -24,6 +26,11 @@ PreviousBuildDirError, VcsHashUnsupported, ) +from pip._internal.models.wheel import Wheel +from pip._internal.network.lazy_wheel import ( + HTTPRangeRequestUnsupported, + dist_from_wheel_url, +) from pip._internal.utils.filesystem import copy2_fixed from pip._internal.utils.hashes import MissingHashes from pip._internal.utils.logging import indent_log @@ -452,9 +459,34 @@ def _get_linked_req_hashes(self, req): # showing the user what the hash should be. return req.hashes(trust_internet=False) or MissingHashes() + def _fetch_metadata(preparer, req): + # type: (InstallRequirement) -> Optional[Distribution] + """Fetch metadata, using lazy wheel if possible.""" + use_lazy_wheel = preparer.use_lazy_wheel + assert req.link + link = req.link + remote_wheel = link.is_wheel and not link.is_file + if use_lazy_wheel and remote_wheel and not preparer.require_hashes: + wheel = Wheel(link.filename) + name = canonicalize_name(wheel.name) + logger.info('Collecting %s', req.req or req) + # If HTTPRangeRequestUnsupported is raised, fallback silently. + with indent_log(), suppress(HTTPRangeRequestUnsupported): + logger.info( + 'Obtaining dependency information from %s %s', + name, wheel.version, + ) + url = link.url.split('#', 1)[0] + session = preparer.downloader._session + return dist_from_wheel_url(name, url, session) + return None + def prepare_linked_requirement(self, req, parallel_builds=False): # type: (InstallRequirement, bool) -> Distribution """Prepare a requirement to be obtained from req.link.""" + wheel_dist = self._fetch_metadata(req) + if wheel_dist is not None: + return wheel_dist assert req.link link = req.link self._log_preparing_link(req) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 5e5e8ac7299..3d5d8b8e811 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -1,23 +1,17 @@ import logging import sys -from pip._vendor.contextlib2 import suppress from pip._vendor.packaging.specifiers import InvalidSpecifier, SpecifierSet from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.packaging.version import Version from pip._internal.exceptions import HashError, MetadataInconsistent from pip._internal.models.wheel import Wheel -from pip._internal.network.lazy_wheel import ( - HTTPRangeRequestUnsupported, - dist_from_wheel_url, -) from pip._internal.req.constructors import ( install_req_from_editable, install_req_from_line, ) from pip._internal.req.req_install import InstallRequirement -from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import dist_is_editable, normalize_version_info from pip._internal.utils.packaging import get_requires_python from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -229,29 +223,6 @@ def _prepare(self): self._check_metadata_consistency(dist) self._dist = dist - def _fetch_metadata(self, req): - # type: (InstallRequirement) -> Optional[Distribution] - """Fetch metadata, using lazy wheel if possible.""" - preparer = self._factory.preparer - use_lazy_wheel = preparer.use_lazy_wheel - assert req.link - link = req.link - remote_wheel = link.is_wheel and not link.is_file - if use_lazy_wheel and remote_wheel and not preparer.require_hashes: - wheel = Wheel(link.filename) - name = canonicalize_name(wheel.name) - logger.info('Collecting %s', req.req or req) - # If HTTPRangeRequestUnsupported is raised, fallback silently. - with indent_log(), suppress(HTTPRangeRequestUnsupported): - logger.info( - 'Obtaining dependency information from %s %s', - name, wheel.version, - ) - url = link.url.split('#', 1)[0] - session = preparer.downloader._session - return dist_from_wheel_url(name, url, session) - return None - @property def dist(self): # type: () -> Distribution @@ -338,9 +309,6 @@ def __init__( def _prepare_distribution(self): # type: () -> Distribution - dist = self._fetch_metadata(self._ireq) - if dist is not None: - return dist return self._factory.preparer.prepare_linked_requirement( self._ireq, parallel_builds=True, ) From 21db4f3096c3a907a7168152872d36bba97e27f1 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 2 Aug 2020 14:24:27 -0400 Subject: [PATCH 2395/3170] Log in one common location Reduces dependence on `InstallRequirement` being passed to `_fetch_metadata`. --- src/pip/_internal/operations/prepare.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 8516ea75b10..55a2feff573 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -469,7 +469,6 @@ def _fetch_metadata(preparer, req): if use_lazy_wheel and remote_wheel and not preparer.require_hashes: wheel = Wheel(link.filename) name = canonicalize_name(wheel.name) - logger.info('Collecting %s', req.req or req) # If HTTPRangeRequestUnsupported is raised, fallback silently. with indent_log(), suppress(HTTPRangeRequestUnsupported): logger.info( @@ -484,12 +483,12 @@ def _fetch_metadata(preparer, req): def prepare_linked_requirement(self, req, parallel_builds=False): # type: (InstallRequirement, bool) -> Distribution """Prepare a requirement to be obtained from req.link.""" - wheel_dist = self._fetch_metadata(req) - if wheel_dist is not None: - return wheel_dist assert req.link link = req.link self._log_preparing_link(req) + wheel_dist = self._fetch_metadata(req) + if wheel_dist is not None: + return wheel_dist if link.is_wheel and self.wheel_download_dir: # Download wheels to a dedicated dir when doing `pip wheel`. download_dir = self.wheel_download_dir From c7ade159d46ba0b2ffc0cd1c2b61f721678d00bd Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 2 Aug 2020 14:26:26 -0400 Subject: [PATCH 2396/3170] Pass link to _fetch_metadata instead of req Removes dependence on `InstallRequirement`. --- src/pip/_internal/operations/prepare.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 55a2feff573..d6f136d0a2c 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -459,12 +459,10 @@ def _get_linked_req_hashes(self, req): # showing the user what the hash should be. return req.hashes(trust_internet=False) or MissingHashes() - def _fetch_metadata(preparer, req): - # type: (InstallRequirement) -> Optional[Distribution] + def _fetch_metadata(preparer, link): + # type: (Link) -> Optional[Distribution] """Fetch metadata, using lazy wheel if possible.""" use_lazy_wheel = preparer.use_lazy_wheel - assert req.link - link = req.link remote_wheel = link.is_wheel and not link.is_file if use_lazy_wheel and remote_wheel and not preparer.require_hashes: wheel = Wheel(link.filename) @@ -486,7 +484,7 @@ def prepare_linked_requirement(self, req, parallel_builds=False): assert req.link link = req.link self._log_preparing_link(req) - wheel_dist = self._fetch_metadata(req) + wheel_dist = self._fetch_metadata(link) if wheel_dist is not None: return wheel_dist if link.is_wheel and self.wheel_download_dir: From 8b838ebb89c521cd92dd7a1c7cec075c3b30d72f Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Sun, 2 Aug 2020 18:32:54 -0400 Subject: [PATCH 2397/3170] Prepare lazy wheels more so they are downloaded This keeps all knowledge about preparation and types of requirements in `RequirementPreparer`, so there's one place to look when we're ready to start breaking it apart later. --- src/pip/_internal/operations/prepare.py | 14 ++++++++++++++ src/pip/_internal/req/req_install.py | 3 +++ .../_internal/resolution/resolvelib/resolver.py | 3 +++ 3 files changed, 20 insertions(+) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index d6f136d0a2c..ddfa42989c2 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -486,7 +486,21 @@ def prepare_linked_requirement(self, req, parallel_builds=False): self._log_preparing_link(req) wheel_dist = self._fetch_metadata(link) if wheel_dist is not None: + req.needs_more_preparation = True return wheel_dist + return self._prepare_linked_requirement(req, parallel_builds) + + def prepare_linked_requirement_more(self, req, parallel_builds=False): + # type: (InstallRequirement, bool) -> None + """Prepare a linked requirement more, if needed.""" + if not req.needs_more_preparation: + return + self._prepare_linked_requirement(req, parallel_builds) + + def _prepare_linked_requirement(self, req, parallel_builds): + # type: (InstallRequirement, bool) -> Distribution + assert req.link + link = req.link if link.is_wheel and self.wheel_download_dir: # Download wheels to a dedicated dir when doing `pip wheel`. download_dir = self.wheel_download_dir diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 4759f4af6f0..816969f8e62 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -213,6 +213,9 @@ def __init__( # but after loading this flag should be treated as read only. self.use_pep517 = use_pep517 + # This requirement needs more preparation before it can be built + self.needs_more_preparation = False + def __str__(self): # type: () -> str if self.req: diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index e83f35ca73a..6a38ffa5c11 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -159,6 +159,9 @@ def resolve(self, root_reqs, check_supported_wheels): req_set.add_named_requirement(ireq) + for actual_req in req_set.all_requirements: + self.factory.preparer.prepare_linked_requirement_more(actual_req) + return req_set def get_installation_order(self, req_set): From 95efbbe588d66d99bc28b04f03290630a22c1a35 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Mon, 3 Aug 2020 09:27:50 +0800 Subject: [PATCH 2398/3170] News --- news/8684.bugfix | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 news/8684.bugfix diff --git a/news/8684.bugfix b/news/8684.bugfix new file mode 100644 index 00000000000..18e6ed9bc80 --- /dev/null +++ b/news/8684.bugfix @@ -0,0 +1,2 @@ +Use the same encoding logic from Python 3 to handle ZIP archive entries on +Python 2, so non-ASCII paths can be resolved as expected. From d4995cb89eed0a2d348e220c6ef061b3d816e0f4 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Mon, 3 Aug 2020 06:38:36 +0800 Subject: [PATCH 2399/3170] Implement heuristics to get non-ASCII ZIP entries --- src/pip/_internal/operations/install/wheel.py | 28 +++++++++++++++++-- 1 file changed, 26 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 681fc0aa8ef..f2fde0b087d 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -78,6 +78,7 @@ Union, cast, ) + from zipfile import ZipInfo from pip._vendor.pkg_resources import Distribution @@ -420,6 +421,28 @@ def __init__(self, src_record_path, dest_path, zip_file): self._zip_file = zip_file self.changed = False + def _getinfo(self): + # type: () -> ZipInfo + if not PY2: + return self._zip_file.getinfo(self.src_record_path) + + # Python 2 does not expose a way to detect a ZIP's encoding, so we + # "guess" with the heuristics below: + # 1. Try encoding the path with UTF-8. + # 2. Check the matching info's flags for language encoding (bit 11). + # 3. If the flag is set, assume UTF-8 is correct. + # 4. If any of the above steps fails, fallback to getting an info with + # CP437 (matching Python 3). + try: + arcname = self.src_record_path.encode("utf-8") + info = self._zip_file.getinfo(arcname) + if info.flag_bits & 0x800: + return info + except (KeyError, UnicodeEncodeError): + pass + arcname = self.src_record_path.encode("cp437") + return self._zip_file.getinfo(arcname) + def save(self): # type: () -> None # directory creation is lazy and after file filtering @@ -439,11 +462,12 @@ def save(self): if os.path.exists(self.dest_path): os.unlink(self.dest_path) - with self._zip_file.open(self.src_record_path) as f: + zipinfo = self._getinfo() + + with self._zip_file.open(zipinfo) as f: with open(self.dest_path, "wb") as dest: shutil.copyfileobj(f, dest) - zipinfo = self._zip_file.getinfo(self.src_record_path) if zip_item_is_executable(zipinfo): set_extracted_file_to_default_mode_plus_executable(self.dest_path) From a12e2f147997dd0932727150ae596ca489a41e78 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Mon, 3 Aug 2020 14:59:42 +0800 Subject: [PATCH 2400/3170] PEP 427 mandates UTF-8, we don't need the fallback --- news/8684.bugfix | 4 ++-- src/pip/_internal/operations/install/wheel.py | 21 ++++--------------- 2 files changed, 6 insertions(+), 19 deletions(-) diff --git a/news/8684.bugfix b/news/8684.bugfix index 18e6ed9bc80..528291d736a 100644 --- a/news/8684.bugfix +++ b/news/8684.bugfix @@ -1,2 +1,2 @@ -Use the same encoding logic from Python 3 to handle ZIP archive entries on -Python 2, so non-ASCII paths can be resolved as expected. +Use UTF-8 to handle ZIP archive entries on Python 2 according to PEP 427, so +non-ASCII paths can be resolved as expected. diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index f2fde0b087d..e91b1b8d558 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -425,23 +425,10 @@ def _getinfo(self): # type: () -> ZipInfo if not PY2: return self._zip_file.getinfo(self.src_record_path) - - # Python 2 does not expose a way to detect a ZIP's encoding, so we - # "guess" with the heuristics below: - # 1. Try encoding the path with UTF-8. - # 2. Check the matching info's flags for language encoding (bit 11). - # 3. If the flag is set, assume UTF-8 is correct. - # 4. If any of the above steps fails, fallback to getting an info with - # CP437 (matching Python 3). - try: - arcname = self.src_record_path.encode("utf-8") - info = self._zip_file.getinfo(arcname) - if info.flag_bits & 0x800: - return info - except (KeyError, UnicodeEncodeError): - pass - arcname = self.src_record_path.encode("cp437") - return self._zip_file.getinfo(arcname) + # Python 2 does not expose a way to detect a ZIP's encoding, but the + # wheel specification (PEP 427) explicitly mandates that paths should + # use UTF-8, so we assume it is true. + return self._zip_file.getinfo(self.src_record_path.encode("utf-8")) def save(self): # type: () -> None From ba062c3ed068a82b36d818f6c434fe5a91afb645 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= <miro@hroncok.cz> Date: Mon, 3 Aug 2020 10:41:03 +0200 Subject: [PATCH 2401/3170] When one keyring attempt fails, don't bother with more This makes https://github.com/pypa/pip/issues/8090 much less painful. --- news/8090.bugfix | 3 +++ src/pip/_internal/network/auth.py | 2 ++ tests/unit/test_network_auth.py | 26 ++++++++++++++++++++++++++ 3 files changed, 31 insertions(+) create mode 100644 news/8090.bugfix diff --git a/news/8090.bugfix b/news/8090.bugfix new file mode 100644 index 00000000000..e9f2b7cbb9e --- /dev/null +++ b/news/8090.bugfix @@ -0,0 +1,3 @@ +Only attempt to use the keyring once and if it fails, don't try again. +This prevents spamming users with several keyring unlock prompts when they +cannot unlock or don't want to do so. diff --git a/src/pip/_internal/network/auth.py b/src/pip/_internal/network/auth.py index ca729fcdf5e..c49deaaf1b7 100644 --- a/src/pip/_internal/network/auth.py +++ b/src/pip/_internal/network/auth.py @@ -44,6 +44,7 @@ def get_keyring_auth(url, username): # type: (str, str) -> Optional[AuthInfo] """Return the tuple auth for a given url from keyring.""" + global keyring if not url or not keyring: return None @@ -69,6 +70,7 @@ def get_keyring_auth(url, username): logger.warning( "Keyring is skipped due to an exception: %s", str(exc), ) + keyring = None return None diff --git a/tests/unit/test_network_auth.py b/tests/unit/test_network_auth.py index 08320cfa143..8116b627f79 100644 --- a/tests/unit/test_network_auth.py +++ b/tests/unit/test_network_auth.py @@ -242,3 +242,29 @@ def test_keyring_get_credential(monkeypatch, url, expect): assert auth._get_new_credentials( url, allow_netrc=False, allow_keyring=True ) == expect + + +class KeyringModuleBroken(object): + """Represents the current supported API of keyring, but broken""" + + def __init__(self): + self._call_count = 0 + + def get_credential(self, system, username): + self._call_count += 1 + raise Exception("This keyring is broken!") + + +def test_broken_keyring_disables_keyring(monkeypatch): + keyring_broken = KeyringModuleBroken() + monkeypatch.setattr(pip._internal.network.auth, 'keyring', keyring_broken) + + auth = MultiDomainBasicAuth(index_urls=["http://example.com/"]) + + assert keyring_broken._call_count == 0 + for i in range(5): + url = "http://example.com/path" + str(i) + assert auth._get_new_credentials( + url, allow_netrc=False, allow_keyring=True + ) == (None, None) + assert keyring_broken._call_count == 1 From 20663fc993951ccabe41b4d3744418c37f4a3644 Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara <sh@changeset.nyc> Date: Mon, 3 Aug 2020 10:27:28 -0400 Subject: [PATCH 2402/3170] Docs: Add details on old resolver deprecation and removal Relevant to #8371, #6536, #8076. Signed-off-by: Sumana Harihareswara <sh@changeset.nyc> --- docs/html/development/release-process.rst | 3 +++ docs/html/user_guide.rst | 30 +++++++++++++++++++---- news/8371.doc | 1 + 3 files changed, 29 insertions(+), 5 deletions(-) create mode 100644 news/8371.doc diff --git a/docs/html/development/release-process.rst b/docs/html/development/release-process.rst index cbfbce4adf9..44197955e8e 100644 --- a/docs/html/development/release-process.rst +++ b/docs/html/development/release-process.rst @@ -2,6 +2,7 @@ Release process =============== +.. _`Release Cadence`: Release Cadence =============== @@ -72,6 +73,8 @@ only bugs will be considered, and merged (subject to normal review processes). Note that there may be delays due to the lack of developer resources for reviewing such pull requests. +.. _`Feature Flags`: + Feature Flags ============= diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 31887a2880c..11bdc58182f 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -1223,18 +1223,37 @@ Specific things we'd love to get feedback on: Please let us know through the `resolver testing survey`_. +Deprecation timeline +-------------------- + +We plan for the resolver changeover to proceed as follows, using +:ref:`Feature Flags` and following our :ref:`Release Cadence`: + +* pip 20.2: a beta of the new resolver is available, opt-in, using + the flag ``--use-feature=2020-resolver``. pip defaults to + legacy behavior. + +* pip 20.3: pip defaults to the new resolver, but a user can opt-out + and choose the old resolver behavior, using the flag + ``--use-deprecated=legacy-resolver``. + +* pip 21.0: pip uses new resolver, and the old resolver is no longer + available. + +Since this work will not change user-visible behavior described in the +pip documentation, this change is not covered by the :ref:`Deprecation +Policy`. + Context and followup -------------------- As discussed in `our announcement on the PSF blog`_, the pip team are in the process of developing a new "dependency resolver" (the part of -pip that works out what to install based on your requirements). Since -this work will not change user-visible behavior described in the pip -documentation, this change is not covered by the :ref:`Deprecation -Policy`. +pip that works out what to install based on your requirements). We're tracking our rollout in :issue:`6536` and you can watch for -announcements on the `low-traffic packaging announcements list`_. +announcements on the `low-traffic packaging announcements list`_ and +`the official Python blog`_. .. _freeze: https://pip.pypa.io/en/latest/reference/pip_freeze/ .. _resolver testing survey: https://tools.simplysecure.org/survey/index.php?r=survey/index&sid=989272&lang=en @@ -1242,3 +1261,4 @@ announcements on the `low-traffic packaging announcements list`_. .. _tensorflow: https://pypi.org/project/tensorflow/ .. _low-traffic packaging announcements list: https://mail.python.org/mailman3/lists/pypi-announce.python.org/ .. _our survey on upgrades that create conflicts: https://docs.google.com/forms/d/e/1FAIpQLSeBkbhuIlSofXqCyhi3kGkLmtrpPOEBwr6iJA6SzHdxWKfqdA/viewform +.. _the official Python blog: https://blog.python.org/ diff --git a/news/8371.doc b/news/8371.doc new file mode 100644 index 00000000000..ffd9919507d --- /dev/null +++ b/news/8371.doc @@ -0,0 +1 @@ +Add details on old resolver deprecation and removal to migration documentation. From d98ff19c270667d4a52d714676608094d096c6f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Sun, 2 Aug 2020 16:33:04 +0700 Subject: [PATCH 2403/3170] [fast-deps] Make range requests closer to chunk size --- news/b7b40802-1aae-4295-99f4-a0dd48c96e69.trivial | 0 src/pip/_internal/network/lazy_wheel.py | 4 +++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 news/b7b40802-1aae-4295-99f4-a0dd48c96e69.trivial diff --git a/news/b7b40802-1aae-4295-99f4-a0dd48c96e69.trivial b/news/b7b40802-1aae-4295-99f4-a0dd48c96e69.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/network/lazy_wheel.py b/src/pip/_internal/network/lazy_wheel.py index c2371bf5cd3..16be0d2972a 100644 --- a/src/pip/_internal/network/lazy_wheel.py +++ b/src/pip/_internal/network/lazy_wheel.py @@ -109,8 +109,10 @@ def read(self, size=-1): all bytes until EOF are returned. Fewer than size bytes may be returned if EOF is reached. """ + download_size = max(size, self._chunk_size) start, length = self.tell(), self._length - stop = start + size if 0 <= size <= length-start else length + stop = length if size < 0 else min(start+download_size, length) + start = max(0, stop-download_size) self._download(start, stop-1) return self._file.read(size) From 57ee51c2b187728d7fbff268907c33c369718a11 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Date: Tue, 4 Aug 2020 06:55:17 +0530 Subject: [PATCH 2404/3170] Un-rewrap lines --- docs/html/user_guide.rst | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index b0b25677ee5..9680486b09a 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -838,9 +838,8 @@ specifying package versions (e.g. ``~=`` or ``*``): semantic versioning.", "``~=3.1``: version ``3.1`` or later, but not version ``4.0`` or later. ``~=3.1.2``: version ``3.1.2`` or later, but not version ``3.2.0`` or later." - ``*``, Can be used at the end of a version number to represent "all", - "``==3.1.*``: any version that starts with ``3.1``. - Equivalent to ``~=3.1.0``." + ``*``,Can be used at the end of a version number to represent "all", "``== 3. + 1.*``: any version that starts with ``3.1``. Equivalent to ``~=3.1.0``." The detailed specification of supported comparison operators can be found in :pep:`440`. From 175371c7e3f366668521dfa681641f59fa3f1d69 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Tue, 4 Aug 2020 08:55:12 +0530 Subject: [PATCH 2405/3170] Mention ResolutionImpossible in 20.2 changelog --- NEWS.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/NEWS.rst b/NEWS.rst index 755ca709ee5..852a7bdb9ce 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -43,6 +43,7 @@ Features break. More details about how to test and migrate, and how to report issues, at :ref:`Resolver changes 2020` . Maintainers are preparing to release pip 20.3, with the new resolver on by default, in October. (`#6536 <https://github.com/pypa/pip/issues/6536>`_) +- Introduce a new ResolutionImpossible error, raised when pip encounters un-satisfiable dependency conflicts (`#8546 <https://github.com/pypa/pip/issues/8546>`_, `#8377 <https://github.com/pypa/pip/issues/8377>`_) - Add a subcommand ``debug`` to ``pip config`` to list available configuration sources and the key-value pairs defined in them. (`#6741 <https://github.com/pypa/pip/issues/6741>`_) - Warn if index pages have unexpected content-type (`#6754 <https://github.com/pypa/pip/issues/6754>`_) - Allow specifying ``--prefer-binary`` option in a requirements file (`#7693 <https://github.com/pypa/pip/issues/7693>`_) From 68cdfa93d95565cbc40f1a158828ac4a477467c4 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Tue, 4 Aug 2020 08:55:59 +0530 Subject: [PATCH 2406/3170] Mention ResolutionImpossible in changelog for 8459 --- NEWS.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.rst b/NEWS.rst index 852a7bdb9ce..d2e25b18c8f 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -93,7 +93,7 @@ Improved Documentation - Fix pip config docstring so that the subcommands render correctly in the docs (`#8072 <https://github.com/pypa/pip/issues/8072>`_) - replace links to the old pypa-dev mailing list with https://mail.python.org/mailman3/lists/distutils-sig.python.org/ (`#8353 <https://github.com/pypa/pip/issues/8353>`_) - Fix example for defining multiple values for options which support them (`#8373 <https://github.com/pypa/pip/issues/8373>`_) -- Add documentation that helps the user fix dependency conflicts (`#8459 <https://github.com/pypa/pip/issues/8459>`_) +- Add documentation for the ResolutionImpossible error, that helps the user fix dependency conflicts (`#8459 <https://github.com/pypa/pip/issues/8459>`_) - Add feature flags to docs (`#8512 <https://github.com/pypa/pip/issues/8512>`_) - Document how to install package extras from git branch and source distributions. (`#8576 <https://github.com/pypa/pip/issues/8576>`_) From 18aa718a58b23c6577be18923a777d5f74620d7d Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Date: Tue, 4 Aug 2020 09:50:40 +0530 Subject: [PATCH 2407/3170] Drop punctuation Co-authored-by: Sumana Harihareswara <sh@changeset.nyc> --- NEWS.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/NEWS.rst b/NEWS.rst index d2e25b18c8f..56ea38f15f5 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -93,7 +93,7 @@ Improved Documentation - Fix pip config docstring so that the subcommands render correctly in the docs (`#8072 <https://github.com/pypa/pip/issues/8072>`_) - replace links to the old pypa-dev mailing list with https://mail.python.org/mailman3/lists/distutils-sig.python.org/ (`#8353 <https://github.com/pypa/pip/issues/8353>`_) - Fix example for defining multiple values for options which support them (`#8373 <https://github.com/pypa/pip/issues/8373>`_) -- Add documentation for the ResolutionImpossible error, that helps the user fix dependency conflicts (`#8459 <https://github.com/pypa/pip/issues/8459>`_) +- Add documentation for the ResolutionImpossible error that helps the user fix dependency conflicts (`#8459 <https://github.com/pypa/pip/issues/8459>`_) - Add feature flags to docs (`#8512 <https://github.com/pypa/pip/issues/8512>`_) - Document how to install package extras from git branch and source distributions. (`#8576 <https://github.com/pypa/pip/issues/8576>`_) From 64e4852bc6a9165fe6b7ab2888dbd4f55d46e5e4 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Date: Tue, 4 Aug 2020 10:28:35 +0530 Subject: [PATCH 2408/3170] Merge pull request #8690 from pradyunsg/more-news-about-resolver --- NEWS.rst | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/NEWS.rst b/NEWS.rst index 755ca709ee5..56ea38f15f5 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -43,6 +43,7 @@ Features break. More details about how to test and migrate, and how to report issues, at :ref:`Resolver changes 2020` . Maintainers are preparing to release pip 20.3, with the new resolver on by default, in October. (`#6536 <https://github.com/pypa/pip/issues/6536>`_) +- Introduce a new ResolutionImpossible error, raised when pip encounters un-satisfiable dependency conflicts (`#8546 <https://github.com/pypa/pip/issues/8546>`_, `#8377 <https://github.com/pypa/pip/issues/8377>`_) - Add a subcommand ``debug`` to ``pip config`` to list available configuration sources and the key-value pairs defined in them. (`#6741 <https://github.com/pypa/pip/issues/6741>`_) - Warn if index pages have unexpected content-type (`#6754 <https://github.com/pypa/pip/issues/6754>`_) - Allow specifying ``--prefer-binary`` option in a requirements file (`#7693 <https://github.com/pypa/pip/issues/7693>`_) @@ -92,7 +93,7 @@ Improved Documentation - Fix pip config docstring so that the subcommands render correctly in the docs (`#8072 <https://github.com/pypa/pip/issues/8072>`_) - replace links to the old pypa-dev mailing list with https://mail.python.org/mailman3/lists/distutils-sig.python.org/ (`#8353 <https://github.com/pypa/pip/issues/8353>`_) - Fix example for defining multiple values for options which support them (`#8373 <https://github.com/pypa/pip/issues/8373>`_) -- Add documentation that helps the user fix dependency conflicts (`#8459 <https://github.com/pypa/pip/issues/8459>`_) +- Add documentation for the ResolutionImpossible error that helps the user fix dependency conflicts (`#8459 <https://github.com/pypa/pip/issues/8459>`_) - Add feature flags to docs (`#8512 <https://github.com/pypa/pip/issues/8512>`_) - Document how to install package extras from git branch and source distributions. (`#8576 <https://github.com/pypa/pip/issues/8576>`_) From a40a4efdbe1b0ab2c44080d349fcee053ff0c4f5 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Date: Tue, 4 Aug 2020 08:39:32 +0530 Subject: [PATCH 2409/3170] Merge pull request #8672 from edmorley/fix-userguide-typos --- docs/html/user_guide.rst | 2 +- news/AE707F60-0ABE-4DBA-98AA-59CE8F989386.trivial | 0 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 news/AE707F60-0ABE-4DBA-98AA-59CE8F989386.trivial diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 702a97d0ca1..811386ebb01 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -945,7 +945,7 @@ Unfortunately, **the pip team cannot provide support for individual dependency conflict errors**. Please *only* open a ticket on the `pip issue tracker`_ if you believe that your problem has exposed a bug in pip. -.. _dependency hell: https://en.wikipedia.org/wiki/Dependency_hell> +.. _dependency hell: https://en.wikipedia.org/wiki/Dependency_hell .. _Awesome Python: https://python.libhunt.com/ .. _Python user Discourse: https://discuss.python.org/c/users/7 .. _Python user forums: https://www.python.org/community/forums/ diff --git a/news/AE707F60-0ABE-4DBA-98AA-59CE8F989386.trivial b/news/AE707F60-0ABE-4DBA-98AA-59CE8F989386.trivial new file mode 100644 index 00000000000..e69de29bb2d From ce1e7d80adfcf77612d178e9118e4f00f29d1c9d Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Date: Tue, 4 Aug 2020 07:38:48 +0530 Subject: [PATCH 2410/3170] Merge pull request #8681 from McSinyx/faster-deps [fast-deps] Make range requests closer to chunk size --- news/b7b40802-1aae-4295-99f4-a0dd48c96e69.trivial | 0 src/pip/_internal/network/lazy_wheel.py | 4 +++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 news/b7b40802-1aae-4295-99f4-a0dd48c96e69.trivial diff --git a/news/b7b40802-1aae-4295-99f4-a0dd48c96e69.trivial b/news/b7b40802-1aae-4295-99f4-a0dd48c96e69.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/network/lazy_wheel.py b/src/pip/_internal/network/lazy_wheel.py index c2371bf5cd3..16be0d2972a 100644 --- a/src/pip/_internal/network/lazy_wheel.py +++ b/src/pip/_internal/network/lazy_wheel.py @@ -109,8 +109,10 @@ def read(self, size=-1): all bytes until EOF are returned. Fewer than size bytes may be returned if EOF is reached. """ + download_size = max(size, self._chunk_size) start, length = self.tell(), self._length - stop = start + size if 0 <= size <= length-start else length + stop = length if size < 0 else min(start+download_size, length) + start = max(0, stop-download_size) self._download(start, stop-1) return self._file.read(size) From 527b3e27fb5fd969831d36ddba5d41cfc8542e3f Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Date: Tue, 4 Aug 2020 06:50:38 +0530 Subject: [PATCH 2411/3170] Merge pull request #8684 from uranusjr/zipfile-unicode-path-python2 --- news/8684.bugfix | 2 ++ src/pip/_internal/operations/install/wheel.py | 15 +++++++++++++-- 2 files changed, 15 insertions(+), 2 deletions(-) create mode 100644 news/8684.bugfix diff --git a/news/8684.bugfix b/news/8684.bugfix new file mode 100644 index 00000000000..528291d736a --- /dev/null +++ b/news/8684.bugfix @@ -0,0 +1,2 @@ +Use UTF-8 to handle ZIP archive entries on Python 2 according to PEP 427, so +non-ASCII paths can be resolved as expected. diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 8f73a88b074..4eba8141597 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -78,6 +78,7 @@ Union, cast, ) + from zipfile import ZipInfo from pip._vendor.pkg_resources import Distribution @@ -420,6 +421,15 @@ def __init__(self, src_record_path, dest_path, zip_file): self._zip_file = zip_file self.changed = False + def _getinfo(self): + # type: () -> ZipInfo + if not PY2: + return self._zip_file.getinfo(self.src_record_path) + # Python 2 does not expose a way to detect a ZIP's encoding, but the + # wheel specification (PEP 427) explicitly mandates that paths should + # use UTF-8, so we assume it is true. + return self._zip_file.getinfo(self.src_record_path.encode("utf-8")) + def save(self): # type: () -> None # directory creation is lazy and after file filtering @@ -439,11 +449,12 @@ def save(self): if os.path.exists(self.dest_path): os.unlink(self.dest_path) - with self._zip_file.open(self.src_record_path) as f: + zipinfo = self._getinfo() + + with self._zip_file.open(zipinfo) as f: with open(self.dest_path, "wb") as dest: shutil.copyfileobj(f, dest) - zipinfo = self._zip_file.getinfo(self.src_record_path) if zip_item_is_executable(zipinfo): set_extracted_file_to_default_mode_plus_executable(self.dest_path) From b29dd4edc5466492bb821ff470e04d59c97404ef Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Date: Tue, 4 Aug 2020 06:49:10 +0530 Subject: [PATCH 2412/3170] Merge pull request #8679 from pradyunsg/tweak-resolutionimpossible-message --- src/pip/_internal/resolution/resolvelib/factory.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index bd7e3efd9d3..dab23aa09d1 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -453,7 +453,7 @@ def describe_trigger(parent): logger.info(msg) return DistributionNotFound( - "ResolutionImpossible For help visit: " - "https://pip.pypa.io/en/stable/user_guide/" + "ResolutionImpossible: for help visit " + "https://pip.pypa.io/en/latest/user_guide/" "#fixing-conflicting-dependencies" ) From 4a39344e941f7fd2403a3c26e55b7f99ca98fefb Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Date: Tue, 4 Aug 2020 06:48:49 +0530 Subject: [PATCH 2413/3170] Merge pull request #8688 from brainwane/deprecation-opt-in-resolver Docs: Add details on old resolver deprecation and removal --- docs/html/development/release-process.rst | 3 +++ docs/html/user_guide.rst | 30 +++++++++++++++++++---- news/8371.doc | 1 + 3 files changed, 29 insertions(+), 5 deletions(-) create mode 100644 news/8371.doc diff --git a/docs/html/development/release-process.rst b/docs/html/development/release-process.rst index cbfbce4adf9..44197955e8e 100644 --- a/docs/html/development/release-process.rst +++ b/docs/html/development/release-process.rst @@ -2,6 +2,7 @@ Release process =============== +.. _`Release Cadence`: Release Cadence =============== @@ -72,6 +73,8 @@ only bugs will be considered, and merged (subject to normal review processes). Note that there may be delays due to the lack of developer resources for reviewing such pull requests. +.. _`Feature Flags`: + Feature Flags ============= diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 811386ebb01..e9c0020e8d0 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -1223,18 +1223,37 @@ Specific things we'd love to get feedback on: Please let us know through the `resolver testing survey`_. +Deprecation timeline +-------------------- + +We plan for the resolver changeover to proceed as follows, using +:ref:`Feature Flags` and following our :ref:`Release Cadence`: + +* pip 20.2: a beta of the new resolver is available, opt-in, using + the flag ``--use-feature=2020-resolver``. pip defaults to + legacy behavior. + +* pip 20.3: pip defaults to the new resolver, but a user can opt-out + and choose the old resolver behavior, using the flag + ``--use-deprecated=legacy-resolver``. + +* pip 21.0: pip uses new resolver, and the old resolver is no longer + available. + +Since this work will not change user-visible behavior described in the +pip documentation, this change is not covered by the :ref:`Deprecation +Policy`. + Context and followup -------------------- As discussed in `our announcement on the PSF blog`_, the pip team are in the process of developing a new "dependency resolver" (the part of -pip that works out what to install based on your requirements). Since -this work will not change user-visible behavior described in the pip -documentation, this change is not covered by the :ref:`Deprecation -Policy`. +pip that works out what to install based on your requirements). We're tracking our rollout in :issue:`6536` and you can watch for -announcements on the `low-traffic packaging announcements list`_. +announcements on the `low-traffic packaging announcements list`_ and +`the official Python blog`_. .. _freeze: https://pip.pypa.io/en/latest/reference/pip_freeze/ .. _resolver testing survey: https://tools.simplysecure.org/survey/index.php?r=survey/index&sid=989272&lang=en @@ -1242,3 +1261,4 @@ announcements on the `low-traffic packaging announcements list`_. .. _tensorflow: https://pypi.org/project/tensorflow/ .. _low-traffic packaging announcements list: https://mail.python.org/mailman3/lists/pypi-announce.python.org/ .. _our survey on upgrades that create conflicts: https://docs.google.com/forms/d/e/1FAIpQLSeBkbhuIlSofXqCyhi3kGkLmtrpPOEBwr6iJA6SzHdxWKfqdA/viewform +.. _the official Python blog: https://blog.python.org/ diff --git a/news/8371.doc b/news/8371.doc new file mode 100644 index 00000000000..ffd9919507d --- /dev/null +++ b/news/8371.doc @@ -0,0 +1 @@ +Add details on old resolver deprecation and removal to migration documentation. From 552b8376c0657485533c115faf69c208f26143f5 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Date: Tue, 4 Aug 2020 06:48:11 +0530 Subject: [PATCH 2414/3170] Merge pull request #8678 from uranusjr/new-resolver-no-deps-extras-install-self --- news/8677.bugfix | 2 ++ .../_internal/resolution/resolvelib/base.py | 4 +-- .../resolution/resolvelib/candidates.py | 30 +++++++++++-------- .../resolution/resolvelib/provider.py | 9 ++++-- tests/yaml/extras.yml | 7 +++++ 5 files changed, 35 insertions(+), 17 deletions(-) create mode 100644 news/8677.bugfix diff --git a/news/8677.bugfix b/news/8677.bugfix new file mode 100644 index 00000000000..e9efd827977 --- /dev/null +++ b/news/8677.bugfix @@ -0,0 +1,2 @@ +New resolver: Correctly include the base package when specified with extras +in ``--no-deps`` mode. diff --git a/src/pip/_internal/resolution/resolvelib/base.py b/src/pip/_internal/resolution/resolvelib/base.py index a155a1101ad..9245747bf2b 100644 --- a/src/pip/_internal/resolution/resolvelib/base.py +++ b/src/pip/_internal/resolution/resolvelib/base.py @@ -69,8 +69,8 @@ def source_link(self): # type: () -> Optional[Link] raise NotImplementedError("Override in subclass") - def iter_dependencies(self): - # type: () -> Iterable[Optional[Requirement]] + def iter_dependencies(self, with_requires): + # type: (bool) -> Iterable[Optional[Requirement]] raise NotImplementedError("Override in subclass") def get_install_requirement(self): diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index c289bb5839c..46cc7e7a236 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -275,8 +275,10 @@ def _get_requires_python_specifier(self): return None return spec - def iter_dependencies(self): - # type: () -> Iterable[Optional[Requirement]] + def iter_dependencies(self, with_requires): + # type: (bool) -> Iterable[Optional[Requirement]] + if not with_requires: + return for r in self.dist.requires(): yield self._factory.make_requirement_from_spec(str(r), self._ireq) python_dep = self._factory.make_requires_python_requirement( @@ -420,8 +422,10 @@ def format_for_error(self): # type: () -> str return "{} {} (Installed)".format(self.name, self.version) - def iter_dependencies(self): - # type: () -> Iterable[Optional[Requirement]] + def iter_dependencies(self, with_requires): + # type: (bool) -> Iterable[Optional[Requirement]] + if not with_requires: + return for r in self.dist.requires(): yield self._factory.make_requirement_from_spec(str(r), self._ireq) @@ -519,10 +523,16 @@ def source_link(self): # type: () -> Optional[Link] return self.base.source_link - def iter_dependencies(self): - # type: () -> Iterable[Optional[Requirement]] + def iter_dependencies(self, with_requires): + # type: (bool) -> Iterable[Optional[Requirement]] factory = self.base._factory + # Add a dependency on the exact base + # (See note 2b in the class docstring) + yield factory.make_requirement_from_candidate(self.base) + if not with_requires: + return + # The user may have specified extras that the candidate doesn't # support. We ignore any unsupported extras here. valid_extras = self.extras.intersection(self.base.dist.extras) @@ -535,10 +545,6 @@ def iter_dependencies(self): extra ) - # Add a dependency on the exact base - # (See note 2b in the class docstring) - yield factory.make_requirement_from_candidate(self.base) - for r in self.base.dist.requires(valid_extras): requirement = factory.make_requirement_from_spec( str(r), self.base._ireq, valid_extras, @@ -585,8 +591,8 @@ def format_for_error(self): # type: () -> str return "Python {}".format(self.version) - def iter_dependencies(self): - # type: () -> Iterable[Optional[Requirement]] + def iter_dependencies(self, with_requires): + # type: (bool) -> Iterable[Optional[Requirement]] return () def get_install_requirement(self): diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index 72f16205981..b2eb9d06ea5 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -145,6 +145,9 @@ def is_satisfied_by(self, requirement, candidate): def get_dependencies(self, candidate): # type: (Candidate) -> Sequence[Requirement] - if self._ignore_dependencies: - return [] - return [r for r in candidate.iter_dependencies() if r is not None] + with_requires = not self._ignore_dependencies + return [ + r + for r in candidate.iter_dependencies(with_requires) + if r is not None + ] diff --git a/tests/yaml/extras.yml b/tests/yaml/extras.yml index 6e2a1b17e7b..ac68fae4979 100644 --- a/tests/yaml/extras.yml +++ b/tests/yaml/extras.yml @@ -40,3 +40,10 @@ cases: - E 1.0.0 - F 1.0.0 skip: old +- + request: + - install: D[extra_1] + options: --no-deps + response: + - state: + - D 1.0.0 From ed205bdfa628693773b7ab84689e921ead633fdb Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Date: Sun, 2 Aug 2020 19:55:40 +0530 Subject: [PATCH 2415/3170] Merge pull request #8656 from chrahunt/gracefully-handle-bad-data-paths Trace a better error message on installation failure due to invalid .data files in wheels --- news/8654.bugfix | 2 ++ src/pip/_internal/operations/install/wheel.py | 24 ++++++++++++-- tests/functional/test_install_wheel.py | 33 +++++++++++++++++++ 3 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 news/8654.bugfix diff --git a/news/8654.bugfix b/news/8654.bugfix new file mode 100644 index 00000000000..ec0df7a903e --- /dev/null +++ b/news/8654.bugfix @@ -0,0 +1,2 @@ +Trace a better error message on installation failure due to invalid ``.data`` +files in wheels. diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 4eba8141597..e91b1b8d558 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -594,8 +594,28 @@ def data_scheme_file_maker(zip_file, scheme): def make_data_scheme_file(record_path): # type: (RecordPath) -> File normed_path = os.path.normpath(record_path) - _, scheme_key, dest_subpath = normed_path.split(os.path.sep, 2) - scheme_path = scheme_paths[scheme_key] + try: + _, scheme_key, dest_subpath = normed_path.split(os.path.sep, 2) + except ValueError: + message = ( + "Unexpected file in {}: {!r}. .data directory contents" + " should be named like: '<scheme key>/<path>'." + ).format(wheel_path, record_path) + raise InstallationError(message) + + try: + scheme_path = scheme_paths[scheme_key] + except KeyError: + valid_scheme_keys = ", ".join(sorted(scheme_paths)) + message = ( + "Unknown scheme key used in {}: {} (for file {!r}). .data" + " directory contents should be in subdirectories named" + " with a valid scheme key ({})" + ).format( + wheel_path, scheme_key, record_path, valid_scheme_keys + ) + raise InstallationError(message) + dest_path = os.path.join(scheme_path, dest_subpath) assert_no_path_traversal(scheme_path, dest_path) return ZipBackedFile(record_path, dest_path, zip_file) diff --git a/tests/functional/test_install_wheel.py b/tests/functional/test_install_wheel.py index c53f13ca415..ad4e749676f 100644 --- a/tests/functional/test_install_wheel.py +++ b/tests/functional/test_install_wheel.py @@ -681,3 +681,36 @@ def test_correct_package_name_while_creating_wheel_bug(script, package_name): package = create_basic_wheel_for_package(script, package_name, '1.0') wheel_name = os.path.basename(package) assert wheel_name == 'simple_package-1.0-py2.py3-none-any.whl' + + +@pytest.mark.parametrize("name", ["purelib", "abc"]) +def test_wheel_with_file_in_data_dir_has_reasonable_error( + script, tmpdir, name +): + """Normally we expect entities in the .data directory to be in a + subdirectory, but if they are not then we should show a reasonable error + message that includes the path. + """ + wheel_path = make_wheel( + "simple", "0.1.0", extra_data_files={name: "hello world"} + ).save_to_dir(tmpdir) + + result = script.pip( + "install", "--no-index", str(wheel_path), expect_error=True + ) + assert "simple-0.1.0.data/{}".format(name) in result.stderr + + +def test_wheel_with_unknown_subdir_in_data_dir_has_reasonable_error( + script, tmpdir +): + wheel_path = make_wheel( + "simple", + "0.1.0", + extra_data_files={"unknown/hello.txt": "hello world"} + ).save_to_dir(tmpdir) + + result = script.pip( + "install", "--no-index", str(wheel_path), expect_error=True + ) + assert "simple-0.1.0.data/unknown/hello.txt" in result.stderr From 864e2eee09522640b5e527f04b277d0e6978a42a Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Date: Sun, 2 Aug 2020 07:44:24 +0530 Subject: [PATCH 2416/3170] Merge pull request #8659 from uranusjr/fix-get-distribution-dot-in-name Canonicalize name in check_if_exists --- news/8645.bugfix | 2 ++ src/pip/_internal/req/req_install.py | 28 +++++++------------- tests/functional/test_install_upgrade.py | 33 ++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 19 deletions(-) create mode 100644 news/8645.bugfix diff --git a/news/8645.bugfix b/news/8645.bugfix new file mode 100644 index 00000000000..a388d24e4ad --- /dev/null +++ b/news/8645.bugfix @@ -0,0 +1,2 @@ +Correctly find already-installed distributions with dot (``.``) in the name +and uninstall them when needed. diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 644930a1528..4759f4af6f0 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -429,25 +429,13 @@ def check_if_exists(self, use_user_site): """ if self.req is None: return - # get_distribution() will resolve the entire list of requirements - # anyway, and we've already determined that we need the requirement - # in question, so strip the marker so that we don't try to - # evaluate it. - no_marker = Requirement(str(self.req)) - no_marker.marker = None - - # pkg_resources uses the canonical name to look up packages, but - # the name passed passed to get_distribution is not canonicalized - # so we have to explicitly convert it to a canonical name - no_marker.name = canonicalize_name(no_marker.name) - try: - self.satisfied_by = pkg_resources.get_distribution(str(no_marker)) - except pkg_resources.DistributionNotFound: + existing_dist = get_distribution(self.req.name) + if not existing_dist: return - except pkg_resources.VersionConflict: - existing_dist = get_distribution( - self.req.name - ) + + existing_version = existing_dist.parsed_version + if not self.req.specifier.contains(existing_version, prereleases=True): + self.satisfied_by = None if use_user_site: if dist_in_usersite(existing_dist): self.should_reinstall = True @@ -461,11 +449,13 @@ def check_if_exists(self, use_user_site): else: self.should_reinstall = True else: - if self.editable and self.satisfied_by: + if self.editable: self.should_reinstall = True # when installing editables, nothing pre-existing should ever # satisfy self.satisfied_by = None + else: + self.satisfied_by = existing_dist # Things valid for wheels @property diff --git a/tests/functional/test_install_upgrade.py b/tests/functional/test_install_upgrade.py index e45bf31483e..02e221101c5 100644 --- a/tests/functional/test_install_upgrade.py +++ b/tests/functional/test_install_upgrade.py @@ -1,3 +1,4 @@ +import itertools import os import sys import textwrap @@ -7,6 +8,7 @@ from tests.lib import pyversion # noqa: F401 from tests.lib import assert_all_changes from tests.lib.local_repos import local_checkout +from tests.lib.wheel import make_wheel @pytest.mark.network @@ -439,3 +441,34 @@ def prep_ve(self, script, version, pip_src, distribute=False): cwd=pip_src, expect_stderr=True, ) + + +@pytest.mark.parametrize("req1, req2", list(itertools.product( + ["foo.bar", "foo_bar", "foo-bar"], ["foo.bar", "foo_bar", "foo-bar"], +))) +def test_install_find_existing_package_canonicalize(script, req1, req2): + """Ensure an already-installed dist is found no matter how the dist name + was normalized on installation. (pypa/pip#8645) + """ + # Create and install a package that's not available in the later stage. + req_container = script.scratch_path.joinpath("foo-bar") + req_container.mkdir() + req_path = make_wheel("foo_bar", "1.0").save_to_dir(req_container) + script.pip("install", "--no-index", req_path) + + # Depend on the previously installed, but now unavailable package. + pkg_container = script.scratch_path.joinpath("pkg") + pkg_container.mkdir() + make_wheel( + "pkg", + "1.0", + metadata_updates={"Requires-Dist": req2}, + ).save_to_dir(pkg_container) + + # Ensure the previously installed package can be correctly used to match + # the dependency. + result = script.pip( + "install", "--no-index", "--find-links", pkg_container, "pkg", + ) + satisfied_message = "Requirement already satisfied: {}".format(req2) + assert satisfied_message in result.stdout, str(result) From 22d67dc261461db99242b72bc56a3a9318aa2e26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@acsone.eu> Date: Sat, 1 Aug 2020 14:13:54 +0200 Subject: [PATCH 2417/3170] Merge pull request #8665 from uranusjr/svn-version-more-robust Improve SVN version parser --- news/8665.bugfix | 1 + src/pip/_internal/vcs/subversion.py | 4 +++- tests/unit/test_vcs.py | 3 +++ 3 files changed, 7 insertions(+), 1 deletion(-) create mode 100644 news/8665.bugfix diff --git a/news/8665.bugfix b/news/8665.bugfix new file mode 100644 index 00000000000..0ce45846360 --- /dev/null +++ b/news/8665.bugfix @@ -0,0 +1 @@ +Fix SVN version detection for alternative SVN distributions. diff --git a/src/pip/_internal/vcs/subversion.py b/src/pip/_internal/vcs/subversion.py index 14825f791a4..ab134970b05 100644 --- a/src/pip/_internal/vcs/subversion.py +++ b/src/pip/_internal/vcs/subversion.py @@ -213,6 +213,8 @@ def call_vcs_version(self): # compiled Feb 25 2019, 14:20:39 on x86_64-apple-darwin17.0.0 # svn, version 1.7.14 (r1542130) # compiled Mar 28 2018, 08:49:13 on x86_64-pc-linux-gnu + # svn, version 1.12.0-SlikSvn (SlikSvn/1.12.0) + # compiled May 28 2019, 13:44:56 on x86_64-microsoft-windows6.2 version_prefix = 'svn, version ' version = self.run_command(['--version']) @@ -220,7 +222,7 @@ def call_vcs_version(self): return () version = version[len(version_prefix):].split()[0] - version_list = version.split('.') + version_list = version.partition('-')[0].split('.') try: parsed_version = tuple(map(int, version_list)) except ValueError: diff --git a/tests/unit/test_vcs.py b/tests/unit/test_vcs.py index 590cb5c0b75..93598c36739 100644 --- a/tests/unit/test_vcs.py +++ b/tests/unit/test_vcs.py @@ -443,6 +443,9 @@ def test_subversion__call_vcs_version(): ('svn, version 1.10.3 (r1842928)\n' ' compiled Feb 25 2019, 14:20:39 on x86_64-apple-darwin17.0.0', (1, 10, 3)), + ('svn, version 1.12.0-SlikSvn (SlikSvn/1.12.0)\n' + ' compiled May 28 2019, 13:44:56 on x86_64-microsoft-windows6.2', + (1, 12, 0)), ('svn, version 1.9.7 (r1800392)', (1, 9, 7)), ('svn, version 1.9.7a1 (r1800392)', ()), ('svn, version 1.9 (r1800392)', (1, 9)), From 6eea0d0a18d8da5a813d2d0a6cf73c8a7700c287 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Date: Fri, 31 Jul 2020 01:17:00 +0530 Subject: [PATCH 2418/3170] Merge pull request #8660 from brainwane/update-flag-in-docs Update documentation to reflect updated resolver feature flag --- .github/ISSUE_TEMPLATE/bug-report.md | 2 +- docs/html/user_guide.rst | 16 ++++++++-------- news/8660.doc | 1 + 3 files changed, 10 insertions(+), 9 deletions(-) create mode 100644 news/8660.doc diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md index fdefbe1a431..157be28b678 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ b/.github/ISSUE_TEMPLATE/bug-report.md @@ -4,7 +4,7 @@ about: Create a report to help us improve --- <!-- -If you're reporting an issue for `--unstable-feature=resolver`, use the "Dependency resolver failures / errors" template instead. +If you're reporting an issue for `--use-feature=2020-resolver`, use the "Dependency resolver failures / errors" template instead. --> **Environment** diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index e9c0020e8d0..a03ec164c44 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -783,7 +783,7 @@ specified packages due to conflicting dependencies (a ``ResolutionImpossible`` error). This documentation is specific to the new resolver, which you can use -with the flag ``--unstable-feature=resolver``. +with the flag ``--use-feature=2020-resolver``. Understanding your error message -------------------------------- @@ -1029,13 +1029,13 @@ Changes to the pip dependency resolver in 20.2 (2020) ===================================================== pip 20.1 included an alpha version of the new resolver (hidden behind -an optional ``--unstable-feature=resolver`` flag). pip 20.2 includes a -robust beta of the new resolver (hidden behind an optional -``--use-feature=2020-resolver`` flag) that we encourage you to -test. We will continue to improve the pip dependency resolver in -response to testers' feedback. Please give us feedback through the -`resolver testing survey`_. This will help us prepare to release pip -20.3, with the new resolver on by default, in October. +an optional ``--unstable-feature=resolver`` flag). pip 20.2 removes +that flag, and includes a robust beta of the new resolver (hidden +behind an optional ``--use-feature=2020-resolver`` flag) that we +encourage you to test. We will continue to improve the pip dependency +resolver in response to testers' feedback. Please give us feedback +through the `resolver testing survey`_. This will help us prepare to +release pip 20.3, with the new resolver on by default, in October. Watch out for ------------- diff --git a/news/8660.doc b/news/8660.doc new file mode 100644 index 00000000000..45b71cc26a4 --- /dev/null +++ b/news/8660.doc @@ -0,0 +1 @@ +Fix feature flag name in docs. From 02ad77d944ea48aa41fce34de209d3f6027d0486 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Date: Wed, 29 Jul 2020 09:28:58 +0530 Subject: [PATCH 2419/3170] Merge pull request #8603 from tekumara/pip-list-ignore-require-venv --- news/8603.feature | 1 + src/pip/_internal/commands/list.py | 1 + 2 files changed, 2 insertions(+) create mode 100644 news/8603.feature diff --git a/news/8603.feature b/news/8603.feature new file mode 100644 index 00000000000..1f8480baaa2 --- /dev/null +++ b/news/8603.feature @@ -0,0 +1 @@ +Ignore require-virtualenv in ``pip list`` diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index a67d0f8d4ab..20e9bff2b71 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -39,6 +39,7 @@ class ListCommand(IndexGroupCommand): Packages are listed in a case-insensitive sorted order. """ + ignore_require_venv = True usage = """ %prog [options]""" From bc86c7c33bd68351622af7ddb5a714a5aa0e09fc Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Tue, 4 Aug 2020 10:46:12 +0530 Subject: [PATCH 2420/3170] Bump for development --- src/pip/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/__init__.py b/src/pip/__init__.py index 6a730519c26..5a2f3c31745 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -4,7 +4,7 @@ from typing import List, Optional -__version__ = "20.2.1" +__version__ = "20.3.dev0" def main(args=None): From fa2714fcd6cc641bfba45dcd93ea206aa4752b95 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Tue, 4 Aug 2020 10:46:12 +0530 Subject: [PATCH 2421/3170] Bump for release --- NEWS.rst | 28 +++++++++++++++++++ news/8371.doc | 1 - news/8603.feature | 1 - news/8645.bugfix | 2 -- news/8654.bugfix | 2 -- news/8660.doc | 1 - news/8665.bugfix | 1 - news/8677.bugfix | 2 -- news/8684.bugfix | 2 -- ...707F60-0ABE-4DBA-98AA-59CE8F989386.trivial | 0 ...b40802-1aae-4295-99f4-a0dd48c96e69.trivial | 0 src/pip/__init__.py | 2 +- 12 files changed, 29 insertions(+), 13 deletions(-) delete mode 100644 news/8371.doc delete mode 100644 news/8603.feature delete mode 100644 news/8645.bugfix delete mode 100644 news/8654.bugfix delete mode 100644 news/8660.doc delete mode 100644 news/8665.bugfix delete mode 100644 news/8677.bugfix delete mode 100644 news/8684.bugfix delete mode 100644 news/AE707F60-0ABE-4DBA-98AA-59CE8F989386.trivial delete mode 100644 news/b7b40802-1aae-4295-99f4-a0dd48c96e69.trivial diff --git a/NEWS.rst b/NEWS.rst index 56ea38f15f5..aaacfffb4ef 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -9,6 +9,34 @@ .. towncrier release notes start +20.2.1 (2020-08-04) +=================== + +Features +-------- + +- Ignore require-virtualenv in ``pip list`` (`#8603 <https://github.com/pypa/pip/issues/8603>`_) + +Bug Fixes +--------- + +- Correctly find already-installed distributions with dot (``.``) in the name + and uninstall them when needed. (`#8645 <https://github.com/pypa/pip/issues/8645>`_) +- Trace a better error message on installation failure due to invalid ``.data`` + files in wheels. (`#8654 <https://github.com/pypa/pip/issues/8654>`_) +- Fix SVN version detection for alternative SVN distributions. (`#8665 <https://github.com/pypa/pip/issues/8665>`_) +- New resolver: Correctly include the base package when specified with extras + in ``--no-deps`` mode. (`#8677 <https://github.com/pypa/pip/issues/8677>`_) +- Use UTF-8 to handle ZIP archive entries on Python 2 according to PEP 427, so + non-ASCII paths can be resolved as expected. (`#8684 <https://github.com/pypa/pip/issues/8684>`_) + +Improved Documentation +---------------------- + +- Add details on old resolver deprecation and removal to migration documentation. (`#8371 <https://github.com/pypa/pip/issues/8371>`_) +- Fix feature flag name in docs. (`#8660 <https://github.com/pypa/pip/issues/8660>`_) + + 20.2 (2020-07-29) ================= diff --git a/news/8371.doc b/news/8371.doc deleted file mode 100644 index ffd9919507d..00000000000 --- a/news/8371.doc +++ /dev/null @@ -1 +0,0 @@ -Add details on old resolver deprecation and removal to migration documentation. diff --git a/news/8603.feature b/news/8603.feature deleted file mode 100644 index 1f8480baaa2..00000000000 --- a/news/8603.feature +++ /dev/null @@ -1 +0,0 @@ -Ignore require-virtualenv in ``pip list`` diff --git a/news/8645.bugfix b/news/8645.bugfix deleted file mode 100644 index a388d24e4ad..00000000000 --- a/news/8645.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -Correctly find already-installed distributions with dot (``.``) in the name -and uninstall them when needed. diff --git a/news/8654.bugfix b/news/8654.bugfix deleted file mode 100644 index ec0df7a903e..00000000000 --- a/news/8654.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -Trace a better error message on installation failure due to invalid ``.data`` -files in wheels. diff --git a/news/8660.doc b/news/8660.doc deleted file mode 100644 index 45b71cc26a4..00000000000 --- a/news/8660.doc +++ /dev/null @@ -1 +0,0 @@ -Fix feature flag name in docs. diff --git a/news/8665.bugfix b/news/8665.bugfix deleted file mode 100644 index 0ce45846360..00000000000 --- a/news/8665.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix SVN version detection for alternative SVN distributions. diff --git a/news/8677.bugfix b/news/8677.bugfix deleted file mode 100644 index e9efd827977..00000000000 --- a/news/8677.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -New resolver: Correctly include the base package when specified with extras -in ``--no-deps`` mode. diff --git a/news/8684.bugfix b/news/8684.bugfix deleted file mode 100644 index 528291d736a..00000000000 --- a/news/8684.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -Use UTF-8 to handle ZIP archive entries on Python 2 according to PEP 427, so -non-ASCII paths can be resolved as expected. diff --git a/news/AE707F60-0ABE-4DBA-98AA-59CE8F989386.trivial b/news/AE707F60-0ABE-4DBA-98AA-59CE8F989386.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/b7b40802-1aae-4295-99f4-a0dd48c96e69.trivial b/news/b7b40802-1aae-4295-99f4-a0dd48c96e69.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/pip/__init__.py b/src/pip/__init__.py index b67e61d063e..6a730519c26 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -4,7 +4,7 @@ from typing import List, Optional -__version__ = "20.2" +__version__ = "20.2.1" def main(args=None): From ae10b82f8cd086fb14ed8b99cbba212294f517c2 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Tue, 4 Aug 2020 16:45:12 +0530 Subject: [PATCH 2422/3170] Remove news/ files for 20.2.1 --- news/094ae133-3a6d-4a6c-a4c3-fe8d78223498.trivial | 0 news/2581396b-ebe4-46a9-b38f-e6b0da97d53a.trivial | 0 news/8371.doc | 1 - news/8603.feature | 1 - news/8645.bugfix | 2 -- news/8654.bugfix | 2 -- news/8660.doc | 1 - news/8665.bugfix | 1 - news/8677.bugfix | 2 -- news/8684.bugfix | 2 -- news/AE707F60-0ABE-4DBA-98AA-59CE8F989386.trivial | 0 news/b7b40802-1aae-4295-99f4-a0dd48c96e69.trivial | 0 12 files changed, 12 deletions(-) delete mode 100644 news/094ae133-3a6d-4a6c-a4c3-fe8d78223498.trivial delete mode 100644 news/2581396b-ebe4-46a9-b38f-e6b0da97d53a.trivial delete mode 100644 news/8371.doc delete mode 100644 news/8603.feature delete mode 100644 news/8645.bugfix delete mode 100644 news/8654.bugfix delete mode 100644 news/8660.doc delete mode 100644 news/8665.bugfix delete mode 100644 news/8677.bugfix delete mode 100644 news/8684.bugfix delete mode 100644 news/AE707F60-0ABE-4DBA-98AA-59CE8F989386.trivial delete mode 100644 news/b7b40802-1aae-4295-99f4-a0dd48c96e69.trivial diff --git a/news/094ae133-3a6d-4a6c-a4c3-fe8d78223498.trivial b/news/094ae133-3a6d-4a6c-a4c3-fe8d78223498.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/2581396b-ebe4-46a9-b38f-e6b0da97d53a.trivial b/news/2581396b-ebe4-46a9-b38f-e6b0da97d53a.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/8371.doc b/news/8371.doc deleted file mode 100644 index ffd9919507d..00000000000 --- a/news/8371.doc +++ /dev/null @@ -1 +0,0 @@ -Add details on old resolver deprecation and removal to migration documentation. diff --git a/news/8603.feature b/news/8603.feature deleted file mode 100644 index 1f8480baaa2..00000000000 --- a/news/8603.feature +++ /dev/null @@ -1 +0,0 @@ -Ignore require-virtualenv in ``pip list`` diff --git a/news/8645.bugfix b/news/8645.bugfix deleted file mode 100644 index a388d24e4ad..00000000000 --- a/news/8645.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -Correctly find already-installed distributions with dot (``.``) in the name -and uninstall them when needed. diff --git a/news/8654.bugfix b/news/8654.bugfix deleted file mode 100644 index ec0df7a903e..00000000000 --- a/news/8654.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -Trace a better error message on installation failure due to invalid ``.data`` -files in wheels. diff --git a/news/8660.doc b/news/8660.doc deleted file mode 100644 index 45b71cc26a4..00000000000 --- a/news/8660.doc +++ /dev/null @@ -1 +0,0 @@ -Fix feature flag name in docs. diff --git a/news/8665.bugfix b/news/8665.bugfix deleted file mode 100644 index 0ce45846360..00000000000 --- a/news/8665.bugfix +++ /dev/null @@ -1 +0,0 @@ -Fix SVN version detection for alternative SVN distributions. diff --git a/news/8677.bugfix b/news/8677.bugfix deleted file mode 100644 index e9efd827977..00000000000 --- a/news/8677.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -New resolver: Correctly include the base package when specified with extras -in ``--no-deps`` mode. diff --git a/news/8684.bugfix b/news/8684.bugfix deleted file mode 100644 index 528291d736a..00000000000 --- a/news/8684.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -Use UTF-8 to handle ZIP archive entries on Python 2 according to PEP 427, so -non-ASCII paths can be resolved as expected. diff --git a/news/AE707F60-0ABE-4DBA-98AA-59CE8F989386.trivial b/news/AE707F60-0ABE-4DBA-98AA-59CE8F989386.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/b7b40802-1aae-4295-99f4-a0dd48c96e69.trivial b/news/b7b40802-1aae-4295-99f4-a0dd48c96e69.trivial deleted file mode 100644 index e69de29bb2d..00000000000 From ff494f524e2d59865cebe71f21acaa311a3a55ab Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 5 Aug 2020 03:06:31 +0800 Subject: [PATCH 2423/3170] News --- news/8695.bugfix | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 news/8695.bugfix diff --git a/news/8695.bugfix b/news/8695.bugfix new file mode 100644 index 00000000000..668e4672e11 --- /dev/null +++ b/news/8695.bugfix @@ -0,0 +1,3 @@ +Fix regression that distributions in system site-packages are not correctly +found when a virtual environment is configured with ``system-site-packages`` +on. From e459763d814e0dbb72ad4a4f77835b8c8a615d68 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 5 Aug 2020 02:43:40 +0800 Subject: [PATCH 2424/3170] Also look for non-local when searching for dists This matches the behavior of pkg_resources.get_distribution(), which this function intends to replace. --- src/pip/_internal/utils/misc.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 24a7455628d..31167c10c61 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -484,17 +484,19 @@ def user_test(d): def search_distribution(req_name): + # type: (str) -> Optional[Distribution] # Canonicalize the name before searching in the list of # installed distributions and also while creating the package # dictionary to get the Distribution object req_name = canonicalize_name(req_name) - packages = get_installed_distributions(skip=()) + packages = get_installed_distributions(local_only=False, skip=()) pkg_dict = {canonicalize_name(p.key): p for p in packages} return pkg_dict.get(req_name) def get_distribution(req_name): + # type: (str) -> Optional[Distribution] """Given a requirement name, return the installed Distribution object""" # Search the distribution by looking through the working set From 8491ce77233a49b71c878a2818600a159217aa57 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 5 Aug 2020 02:45:14 +0800 Subject: [PATCH 2425/3170] Refactor and clarify get_distribution() behavior The call to get_installed_distributions() now passes all flags excplicitly so they are more obvious and less likely to be misunderstood in the future. The behavior also documented in the function docstring. The search_distribution() helper function is renamed with a leading underscore to make it clear that it is intended as a helper function to get_distribution(). --- src/pip/_internal/utils/misc.py | 25 ++++++++++++++++++++----- 1 file changed, 20 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 31167c10c61..5629c60c1c2 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -483,24 +483,39 @@ def user_test(d): ] -def search_distribution(req_name): +def _search_distribution(req_name): # type: (str) -> Optional[Distribution] + """Find a distribution matching the ``req_name`` in the environment. + This searches from *all* distributions available in the environment, to + match the behavior of ``pkg_resources.get_distribution()``. + """ # Canonicalize the name before searching in the list of # installed distributions and also while creating the package # dictionary to get the Distribution object req_name = canonicalize_name(req_name) - packages = get_installed_distributions(local_only=False, skip=()) + packages = get_installed_distributions( + local_only=False, + skip=(), + include_editables=True, + editables_only=False, + user_only=False, + paths=None, + ) pkg_dict = {canonicalize_name(p.key): p for p in packages} return pkg_dict.get(req_name) def get_distribution(req_name): # type: (str) -> Optional[Distribution] - """Given a requirement name, return the installed Distribution object""" + """Given a requirement name, return the installed Distribution object. + + This searches from *all* distributions available in the environment, to + match the behavior of ``pkg_resources.get_distribution()``. + """ # Search the distribution by looking through the working set - dist = search_distribution(req_name) + dist = _search_distribution(req_name) # If distribution could not be found, call working_set.require # to update the working set, and try to find the distribution @@ -516,7 +531,7 @@ def get_distribution(req_name): pkg_resources.working_set.require(req_name) except pkg_resources.DistributionNotFound: return None - return search_distribution(req_name) + return _search_distribution(req_name) def egg_link_path(dist): From c04182893ac6de24c9dda028cb2fd72111ca66a9 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 5 Aug 2020 04:55:56 +0800 Subject: [PATCH 2426/3170] Work around lax semantics in commands.search --- src/pip/_internal/commands/search.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pip/_internal/commands/search.py b/src/pip/_internal/commands/search.py index e906ce7667f..ff09472021e 100644 --- a/src/pip/_internal/commands/search.py +++ b/src/pip/_internal/commands/search.py @@ -140,6 +140,7 @@ def print_results(hits, name_column_width=None, terminal_width=None): write_output(line) if name in installed_packages: dist = get_distribution(name) + assert dist is not None with indent_log(): if dist.version == latest: write_output('INSTALLED: %s (latest)', dist.version) From ffd6a38646a32811cd150790e9f762aca545438b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Wed, 5 Aug 2020 22:19:49 +0700 Subject: [PATCH 2427/3170] Disable caching for range requests --- news/50cf024d-0a74-44c8-b3e9-483dd826fff2.trivial | 0 src/pip/_internal/network/lazy_wheel.py | 6 ++++-- 2 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 news/50cf024d-0a74-44c8-b3e9-483dd826fff2.trivial diff --git a/news/50cf024d-0a74-44c8-b3e9-483dd826fff2.trivial b/news/50cf024d-0a74-44c8-b3e9-483dd826fff2.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/network/lazy_wheel.py b/src/pip/_internal/network/lazy_wheel.py index 16be0d2972a..a0f9e151dd7 100644 --- a/src/pip/_internal/network/lazy_wheel.py +++ b/src/pip/_internal/network/lazy_wheel.py @@ -194,8 +194,10 @@ def _check_zip(self): def _stream_response(self, start, end, base_headers=HEADERS): # type: (int, int, Dict[str, str]) -> Response """Return HTTP response to a range request from start to end.""" - headers = {'Range': 'bytes={}-{}'.format(start, end)} - headers.update(base_headers) + headers = base_headers.copy() + headers['Range'] = 'bytes={}-{}'.format(start, end) + # TODO: Get range requests to be correctly cached + headers['Cache-Control'] = 'no-cache' return self._session.get(self._url, headers=headers, stream=True) def _merge(self, start, end, left, right): From 4fce2ea88c85a483e4250cef1c04dfdb013942c9 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 6 Aug 2020 10:59:07 +0800 Subject: [PATCH 2428/3170] Add test to ensure get_distribution() behavior --- tests/unit/test_utils.py | 74 ++++++++++++++++++++++++++++++++-------- 1 file changed, 60 insertions(+), 14 deletions(-) diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index ebabd29e260..0a1c47cd7ae 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -5,6 +5,7 @@ """ import codecs +import itertools import os import shutil import stat @@ -34,6 +35,7 @@ build_url_from_netloc, egg_link_path, format_size, + get_distribution, get_installed_distributions, get_prog, hide_url, @@ -192,26 +194,30 @@ def test_noegglink_in_sitepkgs_venv_global(self): @patch('pip._internal.utils.misc.dist_in_usersite') @patch('pip._internal.utils.misc.dist_is_local') @patch('pip._internal.utils.misc.dist_is_editable') -class Tests_get_installed_distributions: - """test util.get_installed_distributions""" - - workingset = [ - Mock(test_name="global"), - Mock(test_name="editable"), - Mock(test_name="normal"), - Mock(test_name="user"), - ] - - workingset_stdlib = [ +class TestsGetDistributions(object): + """Test get_installed_distributions() and get_distribution(). + """ + class MockWorkingSet(list): + def require(self, name): + pass + + workingset = MockWorkingSet(( + Mock(test_name="global", key="global"), + Mock(test_name="editable", key="editable"), + Mock(test_name="normal", key="normal"), + Mock(test_name="user", key="user"), + )) + + workingset_stdlib = MockWorkingSet(( Mock(test_name='normal', key='argparse'), Mock(test_name='normal', key='wsgiref') - ] + )) - workingset_freeze = [ + workingset_freeze = MockWorkingSet(( Mock(test_name='normal', key='pip'), Mock(test_name='normal', key='setuptools'), Mock(test_name='normal', key='distribute') - ] + )) def dist_is_editable(self, dist): return dist.test_name == "editable" @@ -287,6 +293,46 @@ def test_freeze_excludes(self, mock_dist_is_editable, skip=('setuptools', 'pip', 'distribute')) assert len(dists) == 0 + @pytest.mark.parametrize( + "working_set, req_name", + itertools.chain( + itertools.product([workingset], (d.key for d in workingset)), + itertools.product( + [workingset_stdlib], (d.key for d in workingset_stdlib), + ), + ), + ) + def test_get_distribution( + self, + mock_dist_is_editable, + mock_dist_is_local, + mock_dist_in_usersite, + working_set, + req_name, + ): + """Ensure get_distribution() finds all kinds of distributions. + """ + mock_dist_is_editable.side_effect = self.dist_is_editable + mock_dist_is_local.side_effect = self.dist_is_local + mock_dist_in_usersite.side_effect = self.dist_in_usersite + with patch("pip._vendor.pkg_resources.working_set", working_set): + dist = get_distribution(req_name) + assert dist is not None + assert dist.key == req_name + + @patch('pip._vendor.pkg_resources.working_set', workingset) + def test_get_distribution_nonexist( + self, + mock_dist_is_editable, + mock_dist_is_local, + mock_dist_in_usersite, + ): + mock_dist_is_editable.side_effect = self.dist_is_editable + mock_dist_is_local.side_effect = self.dist_is_local + mock_dist_in_usersite.side_effect = self.dist_in_usersite + dist = get_distribution("non-exist") + assert dist is None + def test_rmtree_errorhandler_nonexistent_directory(tmpdir): """ From 810385b971bc101182ded21e83457d83790e413a Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 6 Aug 2020 11:45:29 +0800 Subject: [PATCH 2429/3170] Always use UTF-8 to read pyvenv.cfg --- news/8717.bugfix | 1 + src/pip/_internal/utils/virtualenv.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 news/8717.bugfix diff --git a/news/8717.bugfix b/news/8717.bugfix new file mode 100644 index 00000000000..e8c8533c492 --- /dev/null +++ b/news/8717.bugfix @@ -0,0 +1 @@ +Always use UTF-8 to read ``pyvenv.cfg`` to match the built-in ``venv``. diff --git a/src/pip/_internal/utils/virtualenv.py b/src/pip/_internal/utils/virtualenv.py index 596a69a7dad..4a7812873b3 100644 --- a/src/pip/_internal/utils/virtualenv.py +++ b/src/pip/_internal/utils/virtualenv.py @@ -1,5 +1,6 @@ from __future__ import absolute_import +import io import logging import os import re @@ -51,7 +52,9 @@ def _get_pyvenv_cfg_lines(): """ pyvenv_cfg_file = os.path.join(sys.prefix, 'pyvenv.cfg') try: - with open(pyvenv_cfg_file) as f: + # Although PEP 405 does not specify, the built-in venv module always + # writes with UTF-8. (pypa/pip#8717) + with io.open(pyvenv_cfg_file, encoding='utf-8') as f: return f.read().splitlines() # avoids trailing newlines except IOError: return None From 4f210f36089398fccce91c1a5769a8b5e7258cc7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Wed, 5 Aug 2020 15:04:07 +0700 Subject: [PATCH 2430/3170] [2020-resolver] List downloaded distributions before exiting This unifies the behavior of pip download for both legacy and new resolvers. InstallRequirement.successfully_download is no longer needed for this task and is thus retired. --- news/8696.bugfix | 3 +++ src/pip/_internal/commands/download.py | 10 ++++++---- src/pip/_internal/req/req_install.py | 9 --------- src/pip/_internal/resolution/legacy/resolver.py | 6 ------ 4 files changed, 9 insertions(+), 19 deletions(-) create mode 100644 news/8696.bugfix diff --git a/news/8696.bugfix b/news/8696.bugfix new file mode 100644 index 00000000000..989d2d029a3 --- /dev/null +++ b/news/8696.bugfix @@ -0,0 +1,3 @@ +List downloaded distributions before exiting ``pip download`` +when using the new resolver to make the behavior the same as +that on the legacy resolver. diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index 46e8371261e..0861d9e67b2 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -134,10 +134,12 @@ def run(self, options, args): reqs, check_supported_wheels=True ) - downloaded = ' '.join([req.name # type: ignore - for req in requirement_set.requirements.values() - if req.successfully_downloaded]) + downloaded = [] # type: List[str] + for req in requirement_set.requirements.values(): + if not req.editable and req.satisfied_by is None: + assert req.name is not None + downloaded.append(req.name) if downloaded: - write_output('Successfully downloaded %s', downloaded) + write_output('Successfully downloaded %s', ' '.join(downloaded)) return SUCCESS diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 816969f8e62..907c4024903 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -179,15 +179,6 @@ def __init__( # e.g. dependencies, extras or constraints. self.user_supplied = user_supplied - # Set by the legacy resolver when the requirement has been downloaded - # TODO: This introduces a strong coupling between the resolver and the - # requirement (the coupling was previously between the resolver - # and the requirement set). This should be refactored to allow - # the requirement to decide for itself when it has been - # successfully downloaded - but that is more tricky to get right, - # se we are making the change in stages. - self.successfully_downloaded = False - self.isolated = isolated self.build_env = NoOpBuildEnvironment() # type: BuildEnvironment diff --git a/src/pip/_internal/resolution/legacy/resolver.py b/src/pip/_internal/resolution/legacy/resolver.py index a743b569694..d2dafa77f2b 100644 --- a/src/pip/_internal/resolution/legacy/resolver.py +++ b/src/pip/_internal/resolution/legacy/resolver.py @@ -444,12 +444,6 @@ def add_req(subreq, extras_requested): for subreq in dist.requires(available_requested): add_req(subreq, extras_requested=available_requested) - 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. - req_to_install.successfully_downloaded = True - return more_reqs def get_installation_order(self, req_set): From 709ad37b2f064e6e48604e8a8c40c04c88f0c338 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Thu, 6 Aug 2020 17:10:13 +0700 Subject: [PATCH 2431/3170] Make assertion failure give better message (#8692) --- news/9f8da1d9-dd18-47e9-b334-5eb862054409.trivial | 0 tests/functional/test_install_check.py | 3 +-- 2 files changed, 1 insertion(+), 2 deletions(-) create mode 100644 news/9f8da1d9-dd18-47e9-b334-5eb862054409.trivial diff --git a/news/9f8da1d9-dd18-47e9-b334-5eb862054409.trivial b/news/9f8da1d9-dd18-47e9-b334-5eb862054409.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/functional/test_install_check.py b/tests/functional/test_install_check.py index 96ed6b6e18a..a173cb5504f 100644 --- a/tests/functional/test_install_check.py +++ b/tests/functional/test_install_check.py @@ -2,9 +2,8 @@ def assert_contains_expected_lines(string, expected_lines): - lines = string.splitlines() for expected_line in expected_lines: - assert any(line.endswith(expected_line) for line in lines) + assert (expected_line + '\n') in string def test_check_install_canonicalization(script): From 487d00295ce0409ab59fd162e328654c6f93afc3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Wed, 5 Aug 2020 15:39:21 +0700 Subject: [PATCH 2432/3170] Define RequirementPreparer._session --- ...182139-edb4-4bf6-bc3f-2d37cb5759ad.trivial | 0 src/pip/_internal/cli/req_command.py | 1 + src/pip/_internal/operations/prepare.py | 45 ++++++++++++------- tests/unit/test_req.py | 4 +- 4 files changed, 33 insertions(+), 17 deletions(-) create mode 100644 news/c6182139-edb4-4bf6-bc3f-2d37cb5759ad.trivial diff --git a/news/c6182139-edb4-4bf6-bc3f-2d37cb5759ad.trivial b/news/c6182139-edb4-4bf6-bc3f-2d37cb5759ad.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 76d83896c2f..6562fe91874 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -238,6 +238,7 @@ def make_requirement_preparer( wheel_download_dir=wheel_download_dir, build_isolation=options.build_isolation, req_tracker=req_tracker, + session=session, downloader=downloader, finder=finder, require_hashes=options.require_hashes, diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index ddfa42989c2..209a6069b38 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -56,6 +56,7 @@ from pip._internal.index.package_finder import PackageFinder from pip._internal.models.link import Link from pip._internal.network.download import Downloader + from pip._internal.network.session import PipSession from pip._internal.req.req_install import InstallRequirement from pip._internal.req.req_tracker import RequirementTracker from pip._internal.utils.hashes import Hashes @@ -332,6 +333,7 @@ def __init__( wheel_download_dir, # type: Optional[str] build_isolation, # type: bool req_tracker, # type: RequirementTracker + session, # type: PipSession downloader, # type: Downloader finder, # type: PackageFinder require_hashes, # type: bool @@ -344,6 +346,7 @@ def __init__( self.src_dir = src_dir self.build_dir = build_dir self.req_tracker = req_tracker + self._session = session self.downloader = downloader self.finder = finder @@ -461,22 +464,32 @@ def _get_linked_req_hashes(self, req): def _fetch_metadata(preparer, link): # type: (Link) -> Optional[Distribution] - """Fetch metadata, using lazy wheel if possible.""" - use_lazy_wheel = preparer.use_lazy_wheel - remote_wheel = link.is_wheel and not link.is_file - if use_lazy_wheel and remote_wheel and not preparer.require_hashes: - wheel = Wheel(link.filename) - name = canonicalize_name(wheel.name) - # If HTTPRangeRequestUnsupported is raised, fallback silently. - with indent_log(), suppress(HTTPRangeRequestUnsupported): - logger.info( - 'Obtaining dependency information from %s %s', - name, wheel.version, - ) - url = link.url.split('#', 1)[0] - session = preparer.downloader._session - return dist_from_wheel_url(name, url, session) - return None + """Fetch metadata using lazy wheel, if possible.""" + if not self.use_lazy_wheel: + return None + if self.require_hashes: + logger.debug('Lazy wheel is not used as hash checking is required') + return None + if link.is_file or not link.is_wheel: + logger.debug( + 'Lazy wheel is not used as ' + '%r does not points to a remote wheel', + link, + ) + return None + + wheel = Wheel(link.filename) + name = canonicalize_name(wheel.name) + logger.info( + 'Obtaining dependency information from %s %s', + name, wheel.version, + ) + url = link.url.split('#', 1)[0] + try: + return dist_from_wheel_url(name, url, self._session) + except HTTPRangeRequestUnsupported: + logger.debug('%s does not support range requests', url) + return None def prepare_linked_requirement(self, req, parallel_builds=False): # type: (InstallRequirement, bool) -> Distribution diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index b8da863996c..ff1b51ae4ed 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -76,6 +76,7 @@ def _basic_resolver(self, finder, require_hashes=False): isolated=False, use_pep517=None, ) + session = PipSession() with get_requirement_tracker() as tracker: preparer = RequirementPreparer( @@ -85,7 +86,8 @@ def _basic_resolver(self, finder, require_hashes=False): wheel_download_dir=None, build_isolation=True, req_tracker=tracker, - downloader=Downloader(PipSession(), progress_bar="on"), + session=session, + downloader=Downloader(session, progress_bar="on"), finder=finder, require_hashes=require_hashes, use_user_site=False, From 11f7994a6690fcce5a1fdaddc9e3c779255cb334 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Thu, 6 Aug 2020 17:31:10 +0700 Subject: [PATCH 2433/3170] Revise method fetching metadata using lazy wheels * Rename it to fit the fact that it no longer handle fetching _not_ using lazy wheels * Use self as the first parameter * Unnest the checks with additional logs showing reason when lazy wheel is not used --- src/pip/_internal/operations/prepare.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 209a6069b38..e9e534e32f9 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -9,7 +9,6 @@ import os import shutil -from pip._vendor.contextlib2 import suppress from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.six import PY2 @@ -462,7 +461,7 @@ def _get_linked_req_hashes(self, req): # showing the user what the hash should be. return req.hashes(trust_internet=False) or MissingHashes() - def _fetch_metadata(preparer, link): + def _fetch_metadata_using_lazy_wheel(self, link): # type: (Link) -> Optional[Distribution] """Fetch metadata using lazy wheel, if possible.""" if not self.use_lazy_wheel: @@ -497,7 +496,8 @@ def prepare_linked_requirement(self, req, parallel_builds=False): assert req.link link = req.link self._log_preparing_link(req) - wheel_dist = self._fetch_metadata(link) + with indent_log(): + wheel_dist = self._fetch_metadata_using_lazy_wheel(link) if wheel_dist is not None: req.needs_more_preparation = True return wheel_dist From aae63795b2bf5bcde9e846215fd62fa1addd60a4 Mon Sep 17 00:00:00 2001 From: Noah Gorny <noah@gittabags.com> Date: Thu, 2 Jul 2020 21:59:43 +0300 Subject: [PATCH 2434/3170] reqfile: Update extra-index-url/index-url in session from requirements file Also update the relevant tests --- src/pip/_internal/network/session.py | 8 ++++++++ src/pip/_internal/req/req_file.py | 4 ++++ tests/unit/test_req_file.py | 13 +++++++++---- 3 files changed, 21 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py index 39a4a546edc..68ef68db535 100644 --- a/src/pip/_internal/network/session.py +++ b/src/pip/_internal/network/session.py @@ -305,6 +305,14 @@ def __init__(self, *args, **kwargs): for host in trusted_hosts: self.add_trusted_host(host, suppress_logging=True) + def update_index_urls(self, new_index_urls): + # type: (List[str]) -> None + """ + :param new_index_urls: New index urls to update the authentication + handler with. + """ + self.auth.index_urls = new_index_urls + def add_trusted_host(self, host, source=None, suppress_logging=False): # type: (str, Optional[str], bool) -> None """ diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index 1050582289a..72a568bdfc3 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -256,6 +256,10 @@ def handle_option_line( value = relative_to_reqs_file find_links.append(value) + if session: + # We need to update the auth urls in session + session.update_index_urls(index_urls) + search_scope = SearchScope( find_links=find_links, index_urls=index_urls, diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py index 879f088a41d..69d93a0cdc2 100644 --- a/tests/unit/test_req_file.py +++ b/tests/unit/test_req_file.py @@ -341,17 +341,22 @@ def test_set_finder_no_index(self, line_processor, finder): line_processor("--no-index", "file", 1, finder=finder) assert finder.index_urls == [] - def test_set_finder_index_url(self, line_processor, finder): - line_processor("--index-url=url", "file", 1, finder=finder) + def test_set_finder_index_url(self, line_processor, finder, session): + line_processor( + "--index-url=url", "file", 1, finder=finder, session=session) assert finder.index_urls == ['url'] + assert session.auth.index_urls == ['url'] def test_set_finder_find_links(self, line_processor, finder): line_processor("--find-links=url", "file", 1, finder=finder) assert finder.find_links == ['url'] - def test_set_finder_extra_index_urls(self, line_processor, finder): - line_processor("--extra-index-url=url", "file", 1, finder=finder) + def test_set_finder_extra_index_urls( + self, line_processor, finder, session): + line_processor( + "--extra-index-url=url", "file", 1, finder=finder, session=session) assert finder.index_urls == ['url'] + assert session.auth.index_urls == ['url'] def test_set_finder_trusted_host( self, line_processor, caplog, session, finder From 3e70cbe5714010effd6c3c4867d41cec07c3faff Mon Sep 17 00:00:00 2001 From: Noah Gorny <noah@gittabags.com> Date: Thu, 2 Jul 2020 22:11:29 +0300 Subject: [PATCH 2435/3170] news: Add --extra-index-url req file bugfix news --- news/8103.bugfix | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 news/8103.bugfix diff --git a/news/8103.bugfix b/news/8103.bugfix new file mode 100644 index 00000000000..76f17e136cf --- /dev/null +++ b/news/8103.bugfix @@ -0,0 +1,2 @@ +Propagate ``--extra-index-url`` from requirements file properly to session auth, +in order that keyrings and other auths will work as expected. From 312d1d0473e573a46fd857f0094adb6409a4ff06 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 7 Aug 2020 13:43:49 +0800 Subject: [PATCH 2436/3170] Add failing test for constraints with markers --- tests/functional/test_new_resolver.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 9329180334c..46e32ddd3d7 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -1,6 +1,7 @@ import json import os import sys +import textwrap import pytest from pip._vendor.packaging.utils import canonicalize_name @@ -729,6 +730,30 @@ def test_new_resolver_constraint_on_path(script): assert msg in result.stderr, str(result) +def test_new_resolver_constraint_only_marker_match(script): + create_basic_wheel_for_package(script, "pkg", "1.0") + create_basic_wheel_for_package(script, "pkg", "2.0") + create_basic_wheel_for_package(script, "pkg", "3.0") + + constrants_content = textwrap.dedent( + """ + pkg==1.0; python_version == "{ver[0]}.{ver[1]}" # Always satisfies. + pkg==2.0; python_version < "0" # Never satisfies. + """ + ).format(ver=sys.version_info) + constraints_txt = script.scratch_path / "constraints.txt" + constraints_txt.write_text(constrants_content) + + script.pip( + "install", "--use-feature=2020-resolver", + "--no-cache-dir", "--no-index", + "-c", constraints_txt, + "--find-links", script.scratch_path, + "pkg", + ) + assert_installed(script, pkg="1.0") + + def test_new_resolver_upgrade_needs_option(script): # Install pkg 1.0.0 create_basic_wheel_for_package(script, "pkg", "1.0.0") From 4683ad02e3133cca4fa677fa4efcb52c9ac51f20 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 7 Aug 2020 13:51:43 +0800 Subject: [PATCH 2437/3170] Allow filtering constraints with markers --- news/8724.bugfix | 2 ++ src/pip/_internal/resolution/resolvelib/resolver.py | 3 ++- 2 files changed, 4 insertions(+), 1 deletion(-) create mode 100644 news/8724.bugfix diff --git a/news/8724.bugfix b/news/8724.bugfix new file mode 100644 index 00000000000..8641098dde6 --- /dev/null +++ b/news/8724.bugfix @@ -0,0 +1,2 @@ +2020 Resolver: Correctly handle marker evaluation in constraints and exclude +them if their markers do not match the current environment. diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 6a38ffa5c11..fde86413d42 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -80,7 +80,8 @@ def resolve(self, root_reqs, check_supported_wheels): problem = check_invalid_constraint_type(req) if problem: raise InstallationError(problem) - + if not req.match_markers(): + continue name = canonicalize_name(req.name) if name in constraints: constraints[name] = constraints[name] & req.specifier From 8dc0d9c8d916b0371fa61d67916ffc5155a90e92 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Sat, 8 Aug 2020 00:14:27 +0700 Subject: [PATCH 2438/3170] Add news for disabling range response caching --- news/8701.bugfix | 2 ++ news/8716.bugfix | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 news/8701.bugfix create mode 100644 news/8716.bugfix diff --git a/news/8701.bugfix b/news/8701.bugfix new file mode 100644 index 00000000000..086a8f2ebf7 --- /dev/null +++ b/news/8701.bugfix @@ -0,0 +1,2 @@ +Disable caching for range requests, which causes corrupted wheels +when pip tries to obtain metadata using the feature ``fast-deps``. diff --git a/news/8716.bugfix b/news/8716.bugfix new file mode 100644 index 00000000000..086a8f2ebf7 --- /dev/null +++ b/news/8716.bugfix @@ -0,0 +1,2 @@ +Disable caching for range requests, which causes corrupted wheels +when pip tries to obtain metadata using the feature ``fast-deps``. From c7b477777a0fc5f2f8b71c2e6b6eb542a7c65126 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Date: Tue, 11 Aug 2020 15:41:35 +0530 Subject: [PATCH 2439/3170] Merge pull request #8730 from McSinyx/news-8701-8716 Add news for disabling range response caching --- news/8701.bugfix | 2 ++ news/8716.bugfix | 2 ++ 2 files changed, 4 insertions(+) create mode 100644 news/8701.bugfix create mode 100644 news/8716.bugfix diff --git a/news/8701.bugfix b/news/8701.bugfix new file mode 100644 index 00000000000..086a8f2ebf7 --- /dev/null +++ b/news/8701.bugfix @@ -0,0 +1,2 @@ +Disable caching for range requests, which causes corrupted wheels +when pip tries to obtain metadata using the feature ``fast-deps``. diff --git a/news/8716.bugfix b/news/8716.bugfix new file mode 100644 index 00000000000..086a8f2ebf7 --- /dev/null +++ b/news/8716.bugfix @@ -0,0 +1,2 @@ +Disable caching for range requests, which causes corrupted wheels +when pip tries to obtain metadata using the feature ``fast-deps``. From 626d6316829e45ee2e9fafc67b8ae6d4708a905d Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Date: Tue, 11 Aug 2020 15:41:02 +0530 Subject: [PATCH 2440/3170] Merge pull request #8744 from hroncok/keyring_global_nope When one keyring attempt fails, don't bother with more --- news/8090.bugfix | 3 +++ src/pip/_internal/network/auth.py | 2 ++ tests/unit/test_network_auth.py | 26 ++++++++++++++++++++++++++ 3 files changed, 31 insertions(+) create mode 100644 news/8090.bugfix diff --git a/news/8090.bugfix b/news/8090.bugfix new file mode 100644 index 00000000000..e9f2b7cbb9e --- /dev/null +++ b/news/8090.bugfix @@ -0,0 +1,3 @@ +Only attempt to use the keyring once and if it fails, don't try again. +This prevents spamming users with several keyring unlock prompts when they +cannot unlock or don't want to do so. diff --git a/src/pip/_internal/network/auth.py b/src/pip/_internal/network/auth.py index ca729fcdf5e..c49deaaf1b7 100644 --- a/src/pip/_internal/network/auth.py +++ b/src/pip/_internal/network/auth.py @@ -44,6 +44,7 @@ def get_keyring_auth(url, username): # type: (str, str) -> Optional[AuthInfo] """Return the tuple auth for a given url from keyring.""" + global keyring if not url or not keyring: return None @@ -69,6 +70,7 @@ def get_keyring_auth(url, username): logger.warning( "Keyring is skipped due to an exception: %s", str(exc), ) + keyring = None return None diff --git a/tests/unit/test_network_auth.py b/tests/unit/test_network_auth.py index 08320cfa143..8116b627f79 100644 --- a/tests/unit/test_network_auth.py +++ b/tests/unit/test_network_auth.py @@ -242,3 +242,29 @@ def test_keyring_get_credential(monkeypatch, url, expect): assert auth._get_new_credentials( url, allow_netrc=False, allow_keyring=True ) == expect + + +class KeyringModuleBroken(object): + """Represents the current supported API of keyring, but broken""" + + def __init__(self): + self._call_count = 0 + + def get_credential(self, system, username): + self._call_count += 1 + raise Exception("This keyring is broken!") + + +def test_broken_keyring_disables_keyring(monkeypatch): + keyring_broken = KeyringModuleBroken() + monkeypatch.setattr(pip._internal.network.auth, 'keyring', keyring_broken) + + auth = MultiDomainBasicAuth(index_urls=["http://example.com/"]) + + assert keyring_broken._call_count == 0 + for i in range(5): + url = "http://example.com/path" + str(i) + assert auth._get_new_credentials( + url, allow_netrc=False, allow_keyring=True + ) == (None, None) + assert keyring_broken._call_count == 1 From e04cd89f6fcc37f7fab00829dcb349838c8837a8 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Date: Mon, 10 Aug 2020 20:30:55 +0530 Subject: [PATCH 2441/3170] Merge pull request #8702 from uranusjr/get-distribution-looks-for-all --- news/8695.bugfix | 3 ++ src/pip/_internal/commands/search.py | 1 + src/pip/_internal/utils/misc.py | 27 ++++++++-- tests/unit/test_utils.py | 74 ++++++++++++++++++++++------ 4 files changed, 86 insertions(+), 19 deletions(-) create mode 100644 news/8695.bugfix diff --git a/news/8695.bugfix b/news/8695.bugfix new file mode 100644 index 00000000000..668e4672e11 --- /dev/null +++ b/news/8695.bugfix @@ -0,0 +1,3 @@ +Fix regression that distributions in system site-packages are not correctly +found when a virtual environment is configured with ``system-site-packages`` +on. diff --git a/src/pip/_internal/commands/search.py b/src/pip/_internal/commands/search.py index e906ce7667f..ff09472021e 100644 --- a/src/pip/_internal/commands/search.py +++ b/src/pip/_internal/commands/search.py @@ -140,6 +140,7 @@ def print_results(hits, name_column_width=None, terminal_width=None): write_output(line) if name in installed_packages: dist = get_distribution(name) + assert dist is not None with indent_log(): if dist.version == latest: write_output('INSTALLED: %s (latest)', dist.version) diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 24a7455628d..5629c60c1c2 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -483,22 +483,39 @@ def user_test(d): ] -def search_distribution(req_name): +def _search_distribution(req_name): + # type: (str) -> Optional[Distribution] + """Find a distribution matching the ``req_name`` in the environment. + This searches from *all* distributions available in the environment, to + match the behavior of ``pkg_resources.get_distribution()``. + """ # Canonicalize the name before searching in the list of # installed distributions and also while creating the package # dictionary to get the Distribution object req_name = canonicalize_name(req_name) - packages = get_installed_distributions(skip=()) + packages = get_installed_distributions( + local_only=False, + skip=(), + include_editables=True, + editables_only=False, + user_only=False, + paths=None, + ) pkg_dict = {canonicalize_name(p.key): p for p in packages} return pkg_dict.get(req_name) def get_distribution(req_name): - """Given a requirement name, return the installed Distribution object""" + # type: (str) -> Optional[Distribution] + """Given a requirement name, return the installed Distribution object. + + This searches from *all* distributions available in the environment, to + match the behavior of ``pkg_resources.get_distribution()``. + """ # Search the distribution by looking through the working set - dist = search_distribution(req_name) + dist = _search_distribution(req_name) # If distribution could not be found, call working_set.require # to update the working set, and try to find the distribution @@ -514,7 +531,7 @@ def get_distribution(req_name): pkg_resources.working_set.require(req_name) except pkg_resources.DistributionNotFound: return None - return search_distribution(req_name) + return _search_distribution(req_name) def egg_link_path(dist): diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index ebabd29e260..0a1c47cd7ae 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -5,6 +5,7 @@ """ import codecs +import itertools import os import shutil import stat @@ -34,6 +35,7 @@ build_url_from_netloc, egg_link_path, format_size, + get_distribution, get_installed_distributions, get_prog, hide_url, @@ -192,26 +194,30 @@ def test_noegglink_in_sitepkgs_venv_global(self): @patch('pip._internal.utils.misc.dist_in_usersite') @patch('pip._internal.utils.misc.dist_is_local') @patch('pip._internal.utils.misc.dist_is_editable') -class Tests_get_installed_distributions: - """test util.get_installed_distributions""" - - workingset = [ - Mock(test_name="global"), - Mock(test_name="editable"), - Mock(test_name="normal"), - Mock(test_name="user"), - ] - - workingset_stdlib = [ +class TestsGetDistributions(object): + """Test get_installed_distributions() and get_distribution(). + """ + class MockWorkingSet(list): + def require(self, name): + pass + + workingset = MockWorkingSet(( + Mock(test_name="global", key="global"), + Mock(test_name="editable", key="editable"), + Mock(test_name="normal", key="normal"), + Mock(test_name="user", key="user"), + )) + + workingset_stdlib = MockWorkingSet(( Mock(test_name='normal', key='argparse'), Mock(test_name='normal', key='wsgiref') - ] + )) - workingset_freeze = [ + workingset_freeze = MockWorkingSet(( Mock(test_name='normal', key='pip'), Mock(test_name='normal', key='setuptools'), Mock(test_name='normal', key='distribute') - ] + )) def dist_is_editable(self, dist): return dist.test_name == "editable" @@ -287,6 +293,46 @@ def test_freeze_excludes(self, mock_dist_is_editable, skip=('setuptools', 'pip', 'distribute')) assert len(dists) == 0 + @pytest.mark.parametrize( + "working_set, req_name", + itertools.chain( + itertools.product([workingset], (d.key for d in workingset)), + itertools.product( + [workingset_stdlib], (d.key for d in workingset_stdlib), + ), + ), + ) + def test_get_distribution( + self, + mock_dist_is_editable, + mock_dist_is_local, + mock_dist_in_usersite, + working_set, + req_name, + ): + """Ensure get_distribution() finds all kinds of distributions. + """ + mock_dist_is_editable.side_effect = self.dist_is_editable + mock_dist_is_local.side_effect = self.dist_is_local + mock_dist_in_usersite.side_effect = self.dist_in_usersite + with patch("pip._vendor.pkg_resources.working_set", working_set): + dist = get_distribution(req_name) + assert dist is not None + assert dist.key == req_name + + @patch('pip._vendor.pkg_resources.working_set', workingset) + def test_get_distribution_nonexist( + self, + mock_dist_is_editable, + mock_dist_is_local, + mock_dist_in_usersite, + ): + mock_dist_is_editable.side_effect = self.dist_is_editable + mock_dist_is_local.side_effect = self.dist_is_local + mock_dist_in_usersite.side_effect = self.dist_in_usersite + dist = get_distribution("non-exist") + assert dist is None + def test_rmtree_errorhandler_nonexistent_directory(tmpdir): """ From 516c7431bcab438721931c8eeba0b4c33740288a Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Date: Fri, 7 Aug 2020 15:11:32 +0530 Subject: [PATCH 2442/3170] Merge pull request #8718 from uranusjr/pyvenv-cfg-encoding Always use UTF-8 to read pyvenv.cfg --- news/8717.bugfix | 1 + src/pip/_internal/utils/virtualenv.py | 5 ++++- 2 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 news/8717.bugfix diff --git a/news/8717.bugfix b/news/8717.bugfix new file mode 100644 index 00000000000..e8c8533c492 --- /dev/null +++ b/news/8717.bugfix @@ -0,0 +1 @@ +Always use UTF-8 to read ``pyvenv.cfg`` to match the built-in ``venv``. diff --git a/src/pip/_internal/utils/virtualenv.py b/src/pip/_internal/utils/virtualenv.py index 596a69a7dad..4a7812873b3 100644 --- a/src/pip/_internal/utils/virtualenv.py +++ b/src/pip/_internal/utils/virtualenv.py @@ -1,5 +1,6 @@ from __future__ import absolute_import +import io import logging import os import re @@ -51,7 +52,9 @@ def _get_pyvenv_cfg_lines(): """ pyvenv_cfg_file = os.path.join(sys.prefix, 'pyvenv.cfg') try: - with open(pyvenv_cfg_file) as f: + # Although PEP 405 does not specify, the built-in venv module always + # writes with UTF-8. (pypa/pip#8717) + with io.open(pyvenv_cfg_file, encoding='utf-8') as f: return f.read().splitlines() # avoids trailing newlines except IOError: return None From 0ebe453140ccf2a8c3c537b4800fd2d7b97715e5 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Date: Fri, 7 Aug 2020 14:41:46 +0530 Subject: [PATCH 2443/3170] Merge pull request #8727 from uranusjr/new-resolver-constraint-markers --- news/8724.bugfix | 2 ++ .../resolution/resolvelib/resolver.py | 3 ++- tests/functional/test_new_resolver.py | 25 +++++++++++++++++++ 3 files changed, 29 insertions(+), 1 deletion(-) create mode 100644 news/8724.bugfix diff --git a/news/8724.bugfix b/news/8724.bugfix new file mode 100644 index 00000000000..8641098dde6 --- /dev/null +++ b/news/8724.bugfix @@ -0,0 +1,2 @@ +2020 Resolver: Correctly handle marker evaluation in constraints and exclude +them if their markers do not match the current environment. diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 43ea248632d..aecddb1138c 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -90,7 +90,8 @@ def resolve(self, root_reqs, check_supported_wheels): problem = check_invalid_constraint_type(req) if problem: raise InstallationError(problem) - + if not req.match_markers(): + continue name = canonicalize_name(req.name) if name in constraints: constraints[name] = constraints[name] & req.specifier diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 9329180334c..46e32ddd3d7 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -1,6 +1,7 @@ import json import os import sys +import textwrap import pytest from pip._vendor.packaging.utils import canonicalize_name @@ -729,6 +730,30 @@ def test_new_resolver_constraint_on_path(script): assert msg in result.stderr, str(result) +def test_new_resolver_constraint_only_marker_match(script): + create_basic_wheel_for_package(script, "pkg", "1.0") + create_basic_wheel_for_package(script, "pkg", "2.0") + create_basic_wheel_for_package(script, "pkg", "3.0") + + constrants_content = textwrap.dedent( + """ + pkg==1.0; python_version == "{ver[0]}.{ver[1]}" # Always satisfies. + pkg==2.0; python_version < "0" # Never satisfies. + """ + ).format(ver=sys.version_info) + constraints_txt = script.scratch_path / "constraints.txt" + constraints_txt.write_text(constrants_content) + + script.pip( + "install", "--use-feature=2020-resolver", + "--no-cache-dir", "--no-index", + "-c", constraints_txt, + "--find-links", script.scratch_path, + "pkg", + ) + assert_installed(script, pkg="1.0") + + def test_new_resolver_upgrade_needs_option(script): # Install pkg 1.0.0 create_basic_wheel_for_package(script, "pkg", "1.0.0") From b9e403b1730e8555018794f758e7a9fc871e3833 Mon Sep 17 00:00:00 2001 From: Chris Hunt <chrahunt@gmail.com> Date: Wed, 5 Aug 2020 19:58:22 -0400 Subject: [PATCH 2444/3170] Merge pull request #8716 from McSinyx/fix-range-request-cache Disable caching for range requests --- news/50cf024d-0a74-44c8-b3e9-483dd826fff2.trivial | 0 src/pip/_internal/network/lazy_wheel.py | 6 ++++-- 2 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 news/50cf024d-0a74-44c8-b3e9-483dd826fff2.trivial diff --git a/news/50cf024d-0a74-44c8-b3e9-483dd826fff2.trivial b/news/50cf024d-0a74-44c8-b3e9-483dd826fff2.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/network/lazy_wheel.py b/src/pip/_internal/network/lazy_wheel.py index 16be0d2972a..a0f9e151dd7 100644 --- a/src/pip/_internal/network/lazy_wheel.py +++ b/src/pip/_internal/network/lazy_wheel.py @@ -194,8 +194,10 @@ def _check_zip(self): def _stream_response(self, start, end, base_headers=HEADERS): # type: (int, int, Dict[str, str]) -> Response """Return HTTP response to a range request from start to end.""" - headers = {'Range': 'bytes={}-{}'.format(start, end)} - headers.update(base_headers) + headers = base_headers.copy() + headers['Range'] = 'bytes={}-{}'.format(start, end) + # TODO: Get range requests to be correctly cached + headers['Cache-Control'] = 'no-cache' return self._session.get(self._url, headers=headers, stream=True) def _merge(self, start, end, left, right): From e16ebf1b7fd77ad96eeb5ec07ae5326bea02268a Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Tue, 11 Aug 2020 16:56:41 +0530 Subject: [PATCH 2445/3170] Bump for release --- NEWS.rst | 19 +++++++++++++++++++ ...cf024d-0a74-44c8-b3e9-483dd826fff2.trivial | 0 news/8090.bugfix | 3 --- news/8695.bugfix | 3 --- news/8701.bugfix | 2 -- news/8716.bugfix | 2 -- news/8717.bugfix | 1 - news/8724.bugfix | 2 -- src/pip/__init__.py | 2 +- 9 files changed, 20 insertions(+), 14 deletions(-) delete mode 100644 news/50cf024d-0a74-44c8-b3e9-483dd826fff2.trivial delete mode 100644 news/8090.bugfix delete mode 100644 news/8695.bugfix delete mode 100644 news/8701.bugfix delete mode 100644 news/8716.bugfix delete mode 100644 news/8717.bugfix delete mode 100644 news/8724.bugfix diff --git a/NEWS.rst b/NEWS.rst index aaacfffb4ef..a0376a06f07 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -9,6 +9,25 @@ .. towncrier release notes start +20.2.2 (2020-08-11) +=================== + +Bug Fixes +--------- + +- Only attempt to use the keyring once and if it fails, don't try again. + This prevents spamming users with several keyring unlock prompts when they + cannot unlock or don't want to do so. (`#8090 <https://github.com/pypa/pip/issues/8090>`_) +- Fix regression that distributions in system site-packages are not correctly + found when a virtual environment is configured with ``system-site-packages`` + on. (`#8695 <https://github.com/pypa/pip/issues/8695>`_) +- Disable caching for range requests, which causes corrupted wheels + when pip tries to obtain metadata using the feature ``fast-deps``. (`#8701 <https://github.com/pypa/pip/issues/8701>`_, `#8716 <https://github.com/pypa/pip/issues/8716>`_) +- Always use UTF-8 to read ``pyvenv.cfg`` to match the built-in ``venv``. (`#8717 <https://github.com/pypa/pip/issues/8717>`_) +- 2020 Resolver: Correctly handle marker evaluation in constraints and exclude + them if their markers do not match the current environment. (`#8724 <https://github.com/pypa/pip/issues/8724>`_) + + 20.2.1 (2020-08-04) =================== diff --git a/news/50cf024d-0a74-44c8-b3e9-483dd826fff2.trivial b/news/50cf024d-0a74-44c8-b3e9-483dd826fff2.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/8090.bugfix b/news/8090.bugfix deleted file mode 100644 index e9f2b7cbb9e..00000000000 --- a/news/8090.bugfix +++ /dev/null @@ -1,3 +0,0 @@ -Only attempt to use the keyring once and if it fails, don't try again. -This prevents spamming users with several keyring unlock prompts when they -cannot unlock or don't want to do so. diff --git a/news/8695.bugfix b/news/8695.bugfix deleted file mode 100644 index 668e4672e11..00000000000 --- a/news/8695.bugfix +++ /dev/null @@ -1,3 +0,0 @@ -Fix regression that distributions in system site-packages are not correctly -found when a virtual environment is configured with ``system-site-packages`` -on. diff --git a/news/8701.bugfix b/news/8701.bugfix deleted file mode 100644 index 086a8f2ebf7..00000000000 --- a/news/8701.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -Disable caching for range requests, which causes corrupted wheels -when pip tries to obtain metadata using the feature ``fast-deps``. diff --git a/news/8716.bugfix b/news/8716.bugfix deleted file mode 100644 index 086a8f2ebf7..00000000000 --- a/news/8716.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -Disable caching for range requests, which causes corrupted wheels -when pip tries to obtain metadata using the feature ``fast-deps``. diff --git a/news/8717.bugfix b/news/8717.bugfix deleted file mode 100644 index e8c8533c492..00000000000 --- a/news/8717.bugfix +++ /dev/null @@ -1 +0,0 @@ -Always use UTF-8 to read ``pyvenv.cfg`` to match the built-in ``venv``. diff --git a/news/8724.bugfix b/news/8724.bugfix deleted file mode 100644 index 8641098dde6..00000000000 --- a/news/8724.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -2020 Resolver: Correctly handle marker evaluation in constraints and exclude -them if their markers do not match the current environment. diff --git a/src/pip/__init__.py b/src/pip/__init__.py index 6a730519c26..611753fedec 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -4,7 +4,7 @@ from typing import List, Optional -__version__ = "20.2.1" +__version__ = "20.2.2" def main(args=None): From cef8abd268d7516dc98a6dc5fe3e61b3fbd6944f Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Tue, 11 Aug 2020 16:56:42 +0530 Subject: [PATCH 2446/3170] Bump for development --- src/pip/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/__init__.py b/src/pip/__init__.py index 611753fedec..5a2f3c31745 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -4,7 +4,7 @@ from typing import List, Optional -__version__ = "20.2.2" +__version__ = "20.3.dev0" def main(args=None): From e62f16e96938ee24e7a57168b829942526be56e2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Sun, 9 Aug 2020 15:55:33 +0700 Subject: [PATCH 2447/3170] Make Downloader perform the download --- src/pip/_internal/network/download.py | 30 +++++++----------------- src/pip/_internal/operations/prepare.py | 31 ++++--------------------- tests/unit/test_operations_prepare.py | 13 ++--------- 3 files changed, 15 insertions(+), 59 deletions(-) diff --git a/src/pip/_internal/network/download.py b/src/pip/_internal/network/download.py index 44f9985a32b..a4d4bd2a565 100644 --- a/src/pip/_internal/network/download.py +++ b/src/pip/_internal/network/download.py @@ -24,7 +24,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Iterable, Optional + from typing import Iterable, Optional, Tuple from pip._vendor.requests.models import Response @@ -141,19 +141,6 @@ def _http_get_download(session, link): return resp -class Download(object): - def __init__( - self, - response, # type: Response - filename, # type: str - chunks, # type: Iterable[bytes] - ): - # type: (...) -> None - self.response = response - self.filename = filename - self.chunks = chunks - - class Downloader(object): def __init__( self, @@ -164,8 +151,8 @@ def __init__( self._session = session self._progress_bar = progress_bar - def __call__(self, link): - # type: (Link) -> Download + def __call__(self, link, location): + # type: (Link, str) -> Tuple[str, str] try: resp = _http_get_download(self._session, link) except NetworkConnectionError as e: @@ -175,8 +162,9 @@ def __call__(self, link): ) raise - return Download( - resp, - _get_http_response_filename(resp, link), - _prepare_download(resp, link, self._progress_bar), - ) + filename = _get_http_response_filename(resp, link) + chunks = _prepare_download(resp, link, self._progress_bar) + with open(os.path.join(location, filename), 'wb') as content_file: + for chunk in chunks: + content_file.write(chunk) + return content_file.name, resp.headers.get('Content-Type', '') diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index e9e534e32f9..e4de1be4abf 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -45,9 +45,7 @@ from pip._internal.vcs import vcs if MYPY_CHECK_RUNNING: - from typing import ( - Callable, List, Optional, Tuple, - ) + from typing import Callable, List, Optional from mypy_extensions import TypedDict from pip._vendor.pkg_resources import Distribution @@ -132,9 +130,9 @@ def get_http_url( content_type = mimetypes.guess_type(from_path)[0] else: # let's download to a tmp dir - from_path, content_type = _download_http_url( - link, downloader, temp_dir.path, hashes - ) + from_path, content_type = downloader(link, temp_dir.path) + if hashes: + hashes.check_against_path(from_path) return File(from_path, content_type) @@ -273,27 +271,6 @@ def unpack_url( return file -def _download_http_url( - link, # type: Link - downloader, # type: Downloader - temp_dir, # type: str - hashes, # type: Optional[Hashes] -): - # type: (...) -> Tuple[str, str] - """Download link url into temp_dir using provided session""" - download = downloader(link) - - file_path = os.path.join(temp_dir, download.filename) - with open(file_path, 'wb') as content_file: - for chunk in download.chunks: - content_file.write(chunk) - - if hashes: - hashes.check_against_path(file_path) - - return file_path, download.response.headers.get('content-type', '') - - def _check_download_dir(link, download_dir, hashes): # type: (Link, str, Optional[Hashes]) -> Optional[str] """ Check download_dir for previously downloaded file with correct hash diff --git a/tests/unit/test_operations_prepare.py b/tests/unit/test_operations_prepare.py index 41d8be260d3..d2e4d609107 100644 --- a/tests/unit/test_operations_prepare.py +++ b/tests/unit/test_operations_prepare.py @@ -10,11 +10,7 @@ from pip._internal.models.link import Link from pip._internal.network.download import Downloader from pip._internal.network.session import PipSession -from pip._internal.operations.prepare import ( - _copy_source_tree, - _download_http_url, - unpack_url, -) +from pip._internal.operations.prepare import _copy_source_tree, unpack_url from pip._internal.utils.hashes import Hashes from pip._internal.utils.urls import path_to_url from tests.lib.filesystem import ( @@ -83,12 +79,7 @@ def test_download_http_url__no_directory_traversal(mock_raise_for_status, download_dir = tmpdir.joinpath('download') os.mkdir(download_dir) - file_path, content_type = _download_http_url( - link, - downloader, - download_dir, - hashes=None, - ) + file_path, content_type = downloader(link, download_dir) # The file should be downloaded to download_dir. actual = os.listdir(download_dir) assert actual == ['out_dir_file'] From 078e0effb72b1078bab3d268aa5b4e374505e18a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Sun, 9 Aug 2020 22:44:20 +0700 Subject: [PATCH 2448/3170] Add memoization mechanism for file download This is intentionally dependent from caching, which relies on cache dir. --- ...a2b1b7-744e-4533-b3ff-6e7a1843d573.trivial | 0 src/pip/_internal/network/download.py | 9 +++- src/pip/_internal/operations/prepare.py | 43 +++++++++++-------- .../resolution/resolvelib/resolver.py | 6 ++- tests/unit/test_operations_prepare.py | 2 +- 5 files changed, 39 insertions(+), 21 deletions(-) create mode 100644 news/a3a2b1b7-744e-4533-b3ff-6e7a1843d573.trivial diff --git a/news/a3a2b1b7-744e-4533-b3ff-6e7a1843d573.trivial b/news/a3a2b1b7-744e-4533-b3ff-6e7a1843d573.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/network/download.py b/src/pip/_internal/network/download.py index a4d4bd2a565..0eb4fd9ce1b 100644 --- a/src/pip/_internal/network/download.py +++ b/src/pip/_internal/network/download.py @@ -151,8 +151,9 @@ def __init__( self._session = session self._progress_bar = progress_bar - def __call__(self, link, location): + def download_one(self, link, location): # type: (Link, str) -> Tuple[str, str] + """Download the file given by link into location.""" try: resp = _http_get_download(self._session, link) except NetworkConnectionError as e: @@ -168,3 +169,9 @@ def __call__(self, link, location): for chunk in chunks: content_file.write(chunk) return content_file.name, resp.headers.get('Content-Type', '') + + def download_many(self, links, location): + # type: (Iterable[Link], str) -> Iterable[Tuple[str, Tuple[str, str]]] + """Download the files given by links into location.""" + for link in links: + yield link.url, self.download_one(link, location) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index e4de1be4abf..5fdbd674b58 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -45,7 +45,7 @@ from pip._internal.vcs import vcs if MYPY_CHECK_RUNNING: - from typing import Callable, List, Optional + from typing import Callable, Dict, List, Optional, Tuple from mypy_extensions import TypedDict from pip._vendor.pkg_resources import Distribution @@ -130,7 +130,7 @@ def get_http_url( content_type = mimetypes.guess_type(from_path)[0] else: # let's download to a tmp dir - from_path, content_type = downloader(link, temp_dir.path) + from_path, content_type = downloader.download_one(link, temp_dir.path) if hashes: hashes.check_against_path(from_path) @@ -352,6 +352,9 @@ def __init__( # Should wheels be downloaded lazily? self.use_lazy_wheel = lazy_wheel + # Memoized downloaded files, as mapping of url: (path, mime type) + self._downloaded = {} # type: Dict[str, Tuple[str, str]] + @property def _download_should_save(self): # type: () -> bool @@ -480,12 +483,15 @@ def prepare_linked_requirement(self, req, parallel_builds=False): return wheel_dist return self._prepare_linked_requirement(req, parallel_builds) - def prepare_linked_requirement_more(self, req, parallel_builds=False): - # type: (InstallRequirement, bool) -> None + def prepare_linked_requirements_more(self, reqs, parallel_builds=False): + # type: (List[InstallRequirement], bool) -> None """Prepare a linked requirement more, if needed.""" - if not req.needs_more_preparation: - return - self._prepare_linked_requirement(req, parallel_builds) + # Let's download to a temporary directory. + tmpdir = TempDirectory(kind="unpack", globally_managed=True).path + links = (req.link for req in reqs) + self._downloaded.update(self.downloader.download_many(links, tmpdir)) + for req in reqs: + self._prepare_linked_requirement(req, parallel_builds) def _prepare_linked_requirement(self, req, parallel_builds): # type: (InstallRequirement, bool) -> Distribution @@ -499,16 +505,19 @@ def _prepare_linked_requirement(self, req, parallel_builds): with indent_log(): self._ensure_link_req_src_dir(req, download_dir, parallel_builds) - try: - local_file = unpack_url( - link, req.source_dir, self.downloader, download_dir, - hashes=self._get_linked_req_hashes(req) - ) - except NetworkConnectionError as exc: - raise InstallationError( - 'Could not install requirement {} because of HTTP ' - 'error {} for URL {}'.format(req, exc, link) - ) + if link.url in self._downloaded: + local_file = File(*self._downloaded[link.url]) + else: + try: + local_file = unpack_url( + link, req.source_dir, self.downloader, download_dir, + hashes=self._get_linked_req_hashes(req) + ) + except NetworkConnectionError as exc: + raise InstallationError( + 'Could not install requirement {} because of HTTP ' + 'error {} for URL {}'.format(req, exc, link) + ) # For use in later processing, preserve the file path on the # requirement. diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index fde86413d42..1cabe236d3f 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -160,8 +160,10 @@ def resolve(self, root_reqs, check_supported_wheels): req_set.add_named_requirement(ireq) - for actual_req in req_set.all_requirements: - self.factory.preparer.prepare_linked_requirement_more(actual_req) + self.factory.preparer.prepare_linked_requirements_more([ + req for req in req_set.all_requirements + if req.needs_more_preparation + ]) return req_set diff --git a/tests/unit/test_operations_prepare.py b/tests/unit/test_operations_prepare.py index d2e4d609107..e90eab8d7d0 100644 --- a/tests/unit/test_operations_prepare.py +++ b/tests/unit/test_operations_prepare.py @@ -79,7 +79,7 @@ def test_download_http_url__no_directory_traversal(mock_raise_for_status, download_dir = tmpdir.joinpath('download') os.mkdir(download_dir) - file_path, content_type = downloader(link, download_dir) + file_path, content_type = downloader.download_one(link, download_dir) # The file should be downloaded to download_dir. actual = os.listdir(download_dir) assert actual == ['out_dir_file'] From 39d296eeb8cb8b0f8e09493bc8f1cb6eb5940a46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Mon, 10 Aug 2020 22:24:00 +0700 Subject: [PATCH 2449/3170] Clean up code style and internal interface Co-Authored-By: Pradyun Gedam <pradyunsg@gmail.com> Co-Authored-By: Chris Hunt <chrahunt@gmail.com> --- src/pip/_internal/network/download.py | 7 +++++-- src/pip/_internal/operations/prepare.py | 12 +++++++----- src/pip/_internal/resolution/resolvelib/resolver.py | 7 ++----- 3 files changed, 14 insertions(+), 12 deletions(-) diff --git a/src/pip/_internal/network/download.py b/src/pip/_internal/network/download.py index 0eb4fd9ce1b..5fd277330b9 100644 --- a/src/pip/_internal/network/download.py +++ b/src/pip/_internal/network/download.py @@ -164,11 +164,14 @@ def download_one(self, link, location): raise filename = _get_http_response_filename(resp, link) + filepath = os.path.join(location, filename) + chunks = _prepare_download(resp, link, self._progress_bar) - with open(os.path.join(location, filename), 'wb') as content_file: + with open(filepath, 'wb') as content_file: for chunk in chunks: content_file.write(chunk) - return content_file.name, resp.headers.get('Content-Type', '') + content_type = resp.headers.get('Content-Type', '') + return filepath, content_type def download_many(self, links, location): # type: (Iterable[Link], str) -> Iterable[Tuple[str, Tuple[str, str]]] diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 5fdbd674b58..5f2a0c74f4c 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -45,7 +45,7 @@ from pip._internal.vcs import vcs if MYPY_CHECK_RUNNING: - from typing import Callable, Dict, List, Optional, Tuple + from typing import Callable, Dict, Iterable, List, Optional, Tuple from mypy_extensions import TypedDict from pip._vendor.pkg_resources import Distribution @@ -484,8 +484,10 @@ def prepare_linked_requirement(self, req, parallel_builds=False): return self._prepare_linked_requirement(req, parallel_builds) def prepare_linked_requirements_more(self, reqs, parallel_builds=False): - # type: (List[InstallRequirement], bool) -> None + # type: (Iterable[InstallRequirement], bool) -> None """Prepare a linked requirement more, if needed.""" + reqs = [req for req in reqs if req.needs_more_preparation] + # Let's download to a temporary directory. tmpdir = TempDirectory(kind="unpack", globally_managed=True).path links = (req.link for req in reqs) @@ -505,9 +507,7 @@ def _prepare_linked_requirement(self, req, parallel_builds): with indent_log(): self._ensure_link_req_src_dir(req, download_dir, parallel_builds) - if link.url in self._downloaded: - local_file = File(*self._downloaded[link.url]) - else: + if link.url not in self._downloaded: try: local_file = unpack_url( link, req.source_dir, self.downloader, download_dir, @@ -518,6 +518,8 @@ def _prepare_linked_requirement(self, req, parallel_builds): 'Could not install requirement {} because of HTTP ' 'error {} for URL {}'.format(req, exc, link) ) + else: + local_file = File(*self._downloaded[link.url]) # For use in later processing, preserve the file path on the # requirement. diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 1cabe236d3f..031d2f107d6 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -160,11 +160,8 @@ def resolve(self, root_reqs, check_supported_wheels): req_set.add_named_requirement(ireq) - self.factory.preparer.prepare_linked_requirements_more([ - req for req in req_set.all_requirements - if req.needs_more_preparation - ]) - + reqs = req_set.all_requirements + self.factory.preparer.prepare_linked_requirements_more(reqs) return req_set def get_installation_order(self, req_set): From 18c803a41363612569a57b26376693f8b7d72eea Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Mon, 10 Aug 2020 22:54:09 +0700 Subject: [PATCH 2450/3170] Check hashes of memoized downloads --- src/pip/_internal/operations/prepare.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 5f2a0c74f4c..82362a2cfec 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -507,11 +507,12 @@ def _prepare_linked_requirement(self, req, parallel_builds): with indent_log(): self._ensure_link_req_src_dir(req, download_dir, parallel_builds) + hashes = self._get_linked_req_hashes(req) if link.url not in self._downloaded: try: local_file = unpack_url( - link, req.source_dir, self.downloader, download_dir, - hashes=self._get_linked_req_hashes(req) + link, req.source_dir, self.downloader, + download_dir, hashes, ) except NetworkConnectionError as exc: raise InstallationError( @@ -519,7 +520,10 @@ def _prepare_linked_requirement(self, req, parallel_builds): 'error {} for URL {}'.format(req, exc, link) ) else: - local_file = File(*self._downloaded[link.url]) + file_path, content_type = self._downloaded[link.url] + if hashes: + hashes.check_against_path(file_path) + local_file = File(file_path, content_type) # For use in later processing, preserve the file path on the # requirement. From a1aeb4ce01cf8753bcca89ea3f83eb30abf51ae3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Tue, 11 Aug 2020 17:43:13 +0700 Subject: [PATCH 2451/3170] Check download folder for files to be downloaded in batch --- src/pip/_internal/operations/prepare.py | 39 ++++++++++++++++--------- 1 file changed, 26 insertions(+), 13 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 82362a2cfec..13a7df65a69 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -104,10 +104,14 @@ def unpack_vcs_link(link, location): class File(object): + def __init__(self, path, content_type): - # type: (str, str) -> None + # type: (str, Optional[str]) -> None self.path = path - self.content_type = content_type + if content_type is None: + self.content_type = mimetypes.guess_type(path)[0] + else: + self.content_type = content_type def get_http_url( @@ -127,7 +131,7 @@ def get_http_url( if already_downloaded_path: from_path = already_downloaded_path - content_type = mimetypes.guess_type(from_path)[0] + content_type = None else: # let's download to a tmp dir from_path, content_type = downloader.download_one(link, temp_dir.path) @@ -217,10 +221,7 @@ def get_file_url( # one; no internet-sourced hash will be in `hashes`. if hashes: hashes.check_against_path(from_path) - - content_type = mimetypes.guess_type(from_path)[0] - - return File(from_path, content_type) + return File(from_path, None) def unpack_url( @@ -378,6 +379,13 @@ def _log_preparing_link(self, req): else: logger.info('Collecting %s', req.req or req) + def _get_download_dir(self, link): + # type: (Link) -> Optional[str] + if link.is_wheel and self.wheel_download_dir: + # Download wheels to a dedicated dir when doing `pip wheel`. + return self.wheel_download_dir + return self.download_dir + def _ensure_link_req_src_dir(self, req, download_dir, parallel_builds): # type: (InstallRequirement, Optional[str], bool) -> None """Ensure source_dir of a linked InstallRequirement.""" @@ -487,10 +495,19 @@ def prepare_linked_requirements_more(self, reqs, parallel_builds=False): # type: (Iterable[InstallRequirement], bool) -> None """Prepare a linked requirement more, if needed.""" reqs = [req for req in reqs if req.needs_more_preparation] + links = [] # type: List[Link] + for req in reqs: + download_dir = self._get_download_dir(req.link) + if download_dir is not None: + hashes = self._get_linked_req_hashes(req) + file_path = _check_download_dir(req.link, download_dir, hashes) + if download_dir is None or file_path is None: + links.append(req.link) + else: + self._downloaded[req.link.url] = file_path, None # Let's download to a temporary directory. tmpdir = TempDirectory(kind="unpack", globally_managed=True).path - links = (req.link for req in reqs) self._downloaded.update(self.downloader.download_many(links, tmpdir)) for req in reqs: self._prepare_linked_requirement(req, parallel_builds) @@ -499,11 +516,7 @@ def _prepare_linked_requirement(self, req, parallel_builds): # type: (InstallRequirement, bool) -> Distribution assert req.link link = req.link - if link.is_wheel and self.wheel_download_dir: - # Download wheels to a dedicated dir when doing `pip wheel`. - download_dir = self.wheel_download_dir - else: - download_dir = self.download_dir + download_dir = self._get_download_dir(link) with indent_log(): self._ensure_link_req_src_dir(req, download_dir, parallel_builds) From b46576d9336ba2c081b486adb49ec773847c16ee Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Tue, 11 Aug 2020 22:56:37 +0700 Subject: [PATCH 2452/3170] Give batch downloader a separate class --- src/pip/_internal/cli/req_command.py | 5 +--- src/pip/_internal/network/download.py | 36 ++++++++++++++++++++++--- src/pip/_internal/operations/prepare.py | 19 ++++++------- tests/unit/test_operations_prepare.py | 16 +++++------ tests/unit/test_req.py | 3 +-- 5 files changed, 53 insertions(+), 26 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 6562fe91874..76abce5acdf 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -16,7 +16,6 @@ from pip._internal.index.collector import LinkCollector from pip._internal.index.package_finder import PackageFinder from pip._internal.models.selection_prefs import SelectionPreferences -from pip._internal.network.download import Downloader from pip._internal.network.session import PipSession from pip._internal.operations.prepare import RequirementPreparer from pip._internal.req.constructors import ( @@ -213,8 +212,6 @@ def make_requirement_preparer( """ Create a RequirementPreparer instance for the given parameters. """ - downloader = Downloader(session, progress_bar=options.progress_bar) - temp_build_dir_path = temp_build_dir.path assert temp_build_dir_path is not None @@ -239,7 +236,7 @@ def make_requirement_preparer( build_isolation=options.build_isolation, req_tracker=req_tracker, session=session, - downloader=downloader, + progress_bar=options.progress_bar, finder=finder, require_hashes=options.require_hashes, use_user_site=use_user_site, diff --git a/src/pip/_internal/network/download.py b/src/pip/_internal/network/download.py index 5fd277330b9..56feaabac10 100644 --- a/src/pip/_internal/network/download.py +++ b/src/pip/_internal/network/download.py @@ -151,7 +151,7 @@ def __init__( self._session = session self._progress_bar = progress_bar - def download_one(self, link, location): + def __call__(self, link, location): # type: (Link, str) -> Tuple[str, str] """Download the file given by link into location.""" try: @@ -173,8 +173,38 @@ def download_one(self, link, location): content_type = resp.headers.get('Content-Type', '') return filepath, content_type - def download_many(self, links, location): + +class BatchDownloader(object): + + def __init__( + self, + session, # type: PipSession + progress_bar, # type: str + ): + # type: (...) -> None + self._session = session + self._progress_bar = progress_bar + + def __call__(self, links, location): # type: (Iterable[Link], str) -> Iterable[Tuple[str, Tuple[str, str]]] """Download the files given by links into location.""" for link in links: - yield link.url, self.download_one(link, location) + try: + resp = _http_get_download(self._session, link) + except NetworkConnectionError as e: + assert e.response is not None + logger.critical( + "HTTP error %s while getting %s", + e.response.status_code, link, + ) + raise + + filename = _get_http_response_filename(resp, link) + filepath = os.path.join(location, filename) + + chunks = _prepare_download(resp, link, self._progress_bar) + with open(filepath, 'wb') as content_file: + for chunk in chunks: + content_file.write(chunk) + content_type = resp.headers.get('Content-Type', '') + yield link.url, (filepath, content_type) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 13a7df65a69..5eb71ce073f 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -26,6 +26,7 @@ VcsHashUnsupported, ) from pip._internal.models.wheel import Wheel +from pip._internal.network.download import BatchDownloader, Downloader from pip._internal.network.lazy_wheel import ( HTTPRangeRequestUnsupported, dist_from_wheel_url, @@ -52,7 +53,6 @@ from pip._internal.index.package_finder import PackageFinder from pip._internal.models.link import Link - from pip._internal.network.download import Downloader from pip._internal.network.session import PipSession from pip._internal.req.req_install import InstallRequirement from pip._internal.req.req_tracker import RequirementTracker @@ -116,7 +116,7 @@ def __init__(self, path, content_type): def get_http_url( link, # type: Link - downloader, # type: Downloader + download, # type: Downloader download_dir=None, # type: Optional[str] hashes=None, # type: Optional[Hashes] ): @@ -134,7 +134,7 @@ def get_http_url( content_type = None else: # let's download to a tmp dir - from_path, content_type = downloader.download_one(link, temp_dir.path) + from_path, content_type = download(link, temp_dir.path) if hashes: hashes.check_against_path(from_path) @@ -227,7 +227,7 @@ def get_file_url( def unpack_url( link, # type: Link location, # type: str - downloader, # type: Downloader + download, # type: Downloader download_dir=None, # type: Optional[str] hashes=None, # type: Optional[Hashes] ): @@ -259,7 +259,7 @@ def unpack_url( else: file = get_http_url( link, - downloader, + download, download_dir, hashes=hashes, ) @@ -311,7 +311,7 @@ def __init__( build_isolation, # type: bool req_tracker, # type: RequirementTracker session, # type: PipSession - downloader, # type: Downloader + progress_bar, # type: str finder, # type: PackageFinder require_hashes, # type: bool use_user_site, # type: bool @@ -324,7 +324,8 @@ def __init__( self.build_dir = build_dir self.req_tracker = req_tracker self._session = session - self.downloader = downloader + self._download = Downloader(session, progress_bar) + self._batch_download = BatchDownloader(session, progress_bar) self.finder = finder # Where still-packed archives should be written to. If None, they are @@ -508,7 +509,7 @@ def prepare_linked_requirements_more(self, reqs, parallel_builds=False): # Let's download to a temporary directory. tmpdir = TempDirectory(kind="unpack", globally_managed=True).path - self._downloaded.update(self.downloader.download_many(links, tmpdir)) + self._downloaded.update(self._batch_download(links, tmpdir)) for req in reqs: self._prepare_linked_requirement(req, parallel_builds) @@ -524,7 +525,7 @@ def _prepare_linked_requirement(self, req, parallel_builds): if link.url not in self._downloaded: try: local_file = unpack_url( - link, req.source_dir, self.downloader, + link, req.source_dir, self._download, download_dir, hashes, ) except NetworkConnectionError as exc: diff --git a/tests/unit/test_operations_prepare.py b/tests/unit/test_operations_prepare.py index e90eab8d7d0..ab6aaf6aa93 100644 --- a/tests/unit/test_operations_prepare.py +++ b/tests/unit/test_operations_prepare.py @@ -35,7 +35,7 @@ def _fake_session_get(*args, **kwargs): session = Mock() session.get = _fake_session_get - downloader = Downloader(session, progress_bar="on") + download = Downloader(session, progress_bar="on") uri = path_to_url(data.packages.joinpath("simple-1.0.tar.gz")) link = Link(uri) @@ -44,7 +44,7 @@ def _fake_session_get(*args, **kwargs): unpack_url( link, temp_dir, - downloader=downloader, + download=download, download_dir=None, ) assert set(os.listdir(temp_dir)) == { @@ -75,11 +75,11 @@ def test_download_http_url__no_directory_traversal(mock_raise_for_status, 'content-disposition': 'attachment;filename="../out_dir_file"' } session.get.return_value = resp - downloader = Downloader(session, progress_bar="on") + download = Downloader(session, progress_bar="on") download_dir = tmpdir.joinpath('download') os.mkdir(download_dir) - file_path, content_type = downloader.download_one(link, download_dir) + file_path, content_type = download(link, download_dir) # The file should be downloaded to download_dir. actual = os.listdir(download_dir) assert actual == ['out_dir_file'] @@ -178,11 +178,11 @@ def prep(self, tmpdir, data): self.dist_path2 = data.packages.joinpath(self.dist_file2) self.dist_url = Link(path_to_url(self.dist_path)) self.dist_url2 = Link(path_to_url(self.dist_path2)) - self.no_downloader = Mock(side_effect=AssertionError) + self.no_download = Mock(side_effect=AssertionError) def test_unpack_url_no_download(self, tmpdir, data): self.prep(tmpdir, data) - unpack_url(self.dist_url, self.build_dir, self.no_downloader) + unpack_url(self.dist_url, self.build_dir, self.no_download) assert os.path.isdir(os.path.join(self.build_dir, 'simple')) assert not os.path.isfile( os.path.join(self.download_dir, self.dist_file)) @@ -198,7 +198,7 @@ def test_unpack_url_bad_hash(self, tmpdir, data, with pytest.raises(HashMismatch): unpack_url(dist_url, self.build_dir, - downloader=self.no_downloader, + download=self.no_download, hashes=Hashes({'md5': ['bogus']})) def test_unpack_url_thats_a_dir(self, tmpdir, data): @@ -206,7 +206,7 @@ def test_unpack_url_thats_a_dir(self, tmpdir, data): dist_path = data.packages.joinpath("FSPkg") dist_url = Link(path_to_url(dist_path)) unpack_url(dist_url, self.build_dir, - downloader=self.no_downloader, + download=self.no_download, download_dir=self.download_dir) assert os.path.isdir(os.path.join(self.build_dir, 'fspkg')) diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index ff1b51ae4ed..a5a9d4baeef 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -18,7 +18,6 @@ InvalidWheelFilename, PreviousBuildDirError, ) -from pip._internal.network.download import Downloader from pip._internal.network.session import PipSession from pip._internal.operations.prepare import RequirementPreparer from pip._internal.req import InstallRequirement, RequirementSet @@ -87,7 +86,7 @@ def _basic_resolver(self, finder, require_hashes=False): build_isolation=True, req_tracker=tracker, session=session, - downloader=Downloader(session, progress_bar="on"), + progress_bar='on', finder=finder, require_hashes=require_hashes, use_user_site=False, From 9c4a88b0a067ccf551279c889c90eec1adfc6144 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Wed, 12 Aug 2020 11:07:36 +0200 Subject: [PATCH 2453/3170] Improve deprecation message for issue 8368 --- src/pip/_internal/commands/install.py | 21 +++------------------ src/pip/_internal/req/req_install.py | 13 +++++++++++++ 2 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 77ec210d6af..e41660070a0 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -21,7 +21,6 @@ from pip._internal.operations.check import check_install_conflicts from pip._internal.req import install_given_reqs from pip._internal.req.req_tracker import get_requirement_tracker -from pip._internal.utils.deprecation import deprecated from pip._internal.utils.distutils_args import parse_distutils_args from pip._internal.utils.filesystem import test_writable_dir from pip._internal.utils.misc import ( @@ -371,23 +370,9 @@ def run(self, options, args): # For now, we just warn about failures building legacy # requirements, as we'll fall through to a direct # install for those. - legacy_build_failure_names = [ - r.name # type: ignore - for r in build_failures if not r.use_pep517 - ] # type: List[str] - if legacy_build_failure_names: - deprecated( - reason=( - "Could not build wheels for {} which do not use " - "PEP 517. pip will fall back to legacy 'setup.py " - "install' for these.".format( - ", ".join(legacy_build_failure_names) - ) - ), - replacement="to fix the wheel build issue reported above", - gone_in="21.0", - issue=8368, - ) + for r in build_failures: + if not r.use_pep517: + r.legacy_install_reason = 8368 to_install = resolver.get_installation_order( requirement_set diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 816969f8e62..8f1b661248c 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -121,6 +121,7 @@ def __init__( self.comes_from = comes_from self.constraint = constraint self.editable = editable + self.legacy_install_reason = None # type: Optional[int] # source_dir is the local directory where the linked requirement is # located, or unpacked. In case unpacking is needed, creating and @@ -859,6 +860,18 @@ def install( except Exception: self.install_succeeded = True raise + else: + if self.legacy_install_reason == 8368: + deprecated( + reason=( + "{} was installed using the legacy 'setup.py install' " + "method, because a wheel could not be built for it.". + format(self.name) + ), + replacement="to fix the wheel build issue reported above", + gone_in="21.0", + issue=8368, + ) self.install_succeeded = success From 03d49da397a40d6652493340dde6f6a1661f512b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Thu, 13 Aug 2020 09:31:20 +0200 Subject: [PATCH 2454/3170] Add news --- news/8752.feature | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 news/8752.feature diff --git a/news/8752.feature b/news/8752.feature new file mode 100644 index 00000000000..d2560da1803 --- /dev/null +++ b/news/8752.feature @@ -0,0 +1,3 @@ +Make the ``setup.py install`` deprecation warning less noisy. We warn only +when ``setup.py install`` succeeded and ``setup.py bdist_wheel`` failed, as +situations where both fails are most probably irrelevant to this deprecation. From 4c348cf3a08f05f498e5a3ad19cc268752de3b49 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Thu, 13 Aug 2020 09:38:56 +0200 Subject: [PATCH 2455/3170] Consider success flag instead of absence of exception --- src/pip/_internal/req/req_install.py | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 8f1b661248c..4e306be0146 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -860,21 +860,21 @@ def install( except Exception: self.install_succeeded = True raise - else: - if self.legacy_install_reason == 8368: - deprecated( - reason=( - "{} was installed using the legacy 'setup.py install' " - "method, because a wheel could not be built for it.". - format(self.name) - ), - replacement="to fix the wheel build issue reported above", - gone_in="21.0", - issue=8368, - ) self.install_succeeded = success + if success and self.legacy_install_reason == 8368: + deprecated( + reason=( + "{} was installed using the legacy 'setup.py install' " + "method, because a wheel could not be built for it.". + format(self.name) + ), + replacement="to fix the wheel build issue reported above", + gone_in="21.0", + issue=8368, + ) + def check_invalid_constraint_type(req): # type: (InstallRequirement) -> str From 24d5fe86c04a8e6f728c34eab8aabd13e1a8cc8a Mon Sep 17 00:00:00 2001 From: Noah <noah.bar.ilan@gmail.com> Date: Thu, 13 Aug 2020 15:34:00 +0300 Subject: [PATCH 2456/3170] Update news/8103.bugfix Co-authored-by: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> --- news/8103.bugfix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/8103.bugfix b/news/8103.bugfix index 76f17e136cf..55e4d6571aa 100644 --- a/news/8103.bugfix +++ b/news/8103.bugfix @@ -1,2 +1,2 @@ Propagate ``--extra-index-url`` from requirements file properly to session auth, -in order that keyrings and other auths will work as expected. +so that keyring auth will work as expected. From 46b938349abc8b57a8f132b9859c24b9620cb380 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Fri, 14 Aug 2020 17:38:44 +0700 Subject: [PATCH 2457/3170] Allow py2 deprecation warning from setuptools --- news/d90a40c1-15b7-46b9-9162-335bb346b53f.trivial | 0 tests/functional/test_download.py | 2 ++ tests/functional/test_install.py | 5 ++++- 3 files changed, 6 insertions(+), 1 deletion(-) create mode 100644 news/d90a40c1-15b7-46b9-9162-335bb346b53f.trivial diff --git a/news/d90a40c1-15b7-46b9-9162-335bb346b53f.trivial b/news/d90a40c1-15b7-46b9-9162-335bb346b53f.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/functional/test_download.py b/tests/functional/test_download.py index 3e80fa5c538..3291d580d23 100644 --- a/tests/functional/test_download.py +++ b/tests/functional/test_download.py @@ -4,6 +4,7 @@ from hashlib import sha256 import pytest +from pip._vendor.six import PY2 from pip._internal.cli.status_codes import ERROR from pip._internal.utils.urls import path_to_url @@ -474,6 +475,7 @@ def make_wheel_with_python_requires(script, package_name, python_requires): package_dir.joinpath('setup.py').write_text(text) script.run( 'python', 'setup.py', 'bdist_wheel', '--universal', cwd=package_dir, + allow_stderr_warning=PY2, ) file_name = '{}-1.0-py2.py3-none-any.whl'.format(package_name) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 2185251d290..abf5a8d0744 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -9,6 +9,7 @@ from os.path import curdir, join, pardir import pytest +from pip._vendor.six import PY2 from pip._internal.cli.status_codes import ERROR, SUCCESS from pip._internal.models.index import PyPI, TestPyPI @@ -1565,7 +1566,9 @@ def test_install_incompatible_python_requires_wheel(script, with_wheel): version='0.1') """)) script.run( - 'python', 'setup.py', 'bdist_wheel', '--universal', cwd=pkga_path) + 'python', 'setup.py', 'bdist_wheel', '--universal', + cwd=pkga_path, allow_stderr_warning=PY2, + ) result = script.pip('install', './pkga/dist/pkga-0.1-py2.py3-none-any.whl', expect_error=True) assert _get_expected_error_text() in result.stderr, str(result) From 530463879ed5d988038fcb481a043f5092a30ead Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Sat, 15 Aug 2020 21:22:13 +0700 Subject: [PATCH 2458/3170] Use --durations instead of --duration for pytest Newer pytest no longer accepts --duration as an alias for --durations. --- .azure-pipelines/steps/run-tests-windows.yml | 2 +- .azure-pipelines/steps/run-tests.yml | 4 ++-- news/946beace-6164-4d1a-a05d-e9bebf43ccd0.trivial | 0 tools/travis/run.sh | 6 +++--- 4 files changed, 6 insertions(+), 6 deletions(-) create mode 100644 news/946beace-6164-4d1a-a05d-e9bebf43ccd0.trivial diff --git a/.azure-pipelines/steps/run-tests-windows.yml b/.azure-pipelines/steps/run-tests-windows.yml index a65136289ab..39282a3cc80 100644 --- a/.azure-pipelines/steps/run-tests-windows.yml +++ b/.azure-pipelines/steps/run-tests-windows.yml @@ -43,7 +43,7 @@ steps: # https://bugs.python.org/issue18199 $env:TEMP = "R:\Temp" - tox -e py -- -m integration -n auto --duration=5 --junit-xml=junit/integration-test.xml + tox -e py -- -m integration -n auto --durations=5 --junit-xml=junit/integration-test.xml displayName: Tox run integration tests - task: PublishTestResults@2 diff --git a/.azure-pipelines/steps/run-tests.yml b/.azure-pipelines/steps/run-tests.yml index 11ea2272728..5b9a9c50c89 100644 --- a/.azure-pipelines/steps/run-tests.yml +++ b/.azure-pipelines/steps/run-tests.yml @@ -11,10 +11,10 @@ steps: displayName: Tox run unit tests # Run integration tests in two groups so we will fail faster if there is a failure in the first group -- script: tox -e py -- -m integration -n auto --duration=5 -k "not test_install" --junit-xml=junit/integration-test-group0.xml +- script: tox -e py -- -m integration -n auto --durations=5 -k "not test_install" --junit-xml=junit/integration-test-group0.xml displayName: Tox run Group 0 integration tests -- script: tox -e py -- -m integration -n auto --duration=5 -k "test_install" --junit-xml=junit/integration-test-group1.xml +- script: tox -e py -- -m integration -n auto --durations=5 -k "test_install" --junit-xml=junit/integration-test-group1.xml displayName: Tox run Group 1 integration tests - task: PublishTestResults@2 diff --git a/news/946beace-6164-4d1a-a05d-e9bebf43ccd0.trivial b/news/946beace-6164-4d1a-a05d-e9bebf43ccd0.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tools/travis/run.sh b/tools/travis/run.sh index a531cbb56fd..df8f03e7a57 100755 --- a/tools/travis/run.sh +++ b/tools/travis/run.sh @@ -49,15 +49,15 @@ if [[ "$GROUP" == "1" ]]; then # Unit tests tox -- --use-venv -m unit -n auto # Integration tests (not the ones for 'pip install') - tox -- -m integration -n auto --duration=5 -k "not test_install" \ + tox -- -m integration -n auto --durations=5 -k "not test_install" \ --use-venv $RESOLVER_SWITCH elif [[ "$GROUP" == "2" ]]; then # Separate Job for running integration tests for 'pip install' - tox -- -m integration -n auto --duration=5 -k "test_install" \ + tox -- -m integration -n auto --durations=5 -k "test_install" \ --use-venv $RESOLVER_SWITCH elif [[ "$GROUP" == "3" ]]; then # Separate Job for tests that fail with the new resolver - tox -- -m fails_on_new_resolver -n auto --duration=5 \ + tox -- -m fails_on_new_resolver -n auto --durations=5 \ --use-venv $RESOLVER_SWITCH --new-resolver-runtests else # Non-Testing Jobs should run once From 0c0223765ae7ba6eda5d6317d83155c227e2002e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Sun, 16 Aug 2020 14:24:41 +0700 Subject: [PATCH 2459/3170] Unpin pytest and its plugins This works around the incompatibility of pytest-xdist 2.0.0 with older pytest: https://github.com/pytest-dev/pytest-xdist/issues/580 --- tools/requirements/tests.txt | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/tools/requirements/tests.txt b/tools/requirements/tests.txt index 0c84f20aa47..d24c973de25 100644 --- a/tools/requirements/tests.txt +++ b/tools/requirements/tests.txt @@ -4,16 +4,14 @@ enum34; python_version < '3.4' freezegun mock pretend -# pytest 5.x only supports python 3.5+ -pytest<5.0.0 +pytest pytest-cov -# Prevent installing 9.0 which has install_requires "pytest >= 5.0". -pytest-rerunfailures<9.0 +pytest-rerunfailures pytest-timeout pytest-xdist pyyaml -setuptools>=39.2.0 # Needed for `setuptools.wheel.Wheel` support. scripttest +setuptools>=39.2.0 # Needed for `setuptools.wheel.Wheel` support. https://github.com/pypa/virtualenv/archive/legacy.zip#egg=virtualenv werkzeug==0.16.0 wheel From 15e5680d8a66a96ded6a578c554292cc654f0b11 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Sun, 16 Aug 2020 14:25:32 +0700 Subject: [PATCH 2460/3170] Use the new resolver for test requirements --- tools/requirements/tests.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/requirements/tests.txt b/tools/requirements/tests.txt index d24c973de25..ef87225d6c4 100644 --- a/tools/requirements/tests.txt +++ b/tools/requirements/tests.txt @@ -1,3 +1,4 @@ +--use-feature=2020-resolver cryptography==2.8 csv23 enum34; python_version < '3.4' From 14397418d178bdc20c8084d39c1219d08425dbd3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Mon, 10 Aug 2020 22:08:48 +0700 Subject: [PATCH 2461/3170] Test hash checking for fast-deps --- ...494986-202e-4275-b7ec-d6f046c0aa05.trivial | 0 tests/functional/test_fast_deps.py | 29 +++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 news/0e494986-202e-4275-b7ec-d6f046c0aa05.trivial diff --git a/news/0e494986-202e-4275-b7ec-d6f046c0aa05.trivial b/news/0e494986-202e-4275-b7ec-d6f046c0aa05.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/functional/test_fast_deps.py b/tests/functional/test_fast_deps.py index b41055c5606..655440b881b 100644 --- a/tests/functional/test_fast_deps.py +++ b/tests/functional/test_fast_deps.py @@ -48,3 +48,32 @@ def test_build_wheel_with_deps(data, script): assert fnmatch.filter(created, 'requiresPaste-3.1.4-*.whl') assert fnmatch.filter(created, 'Paste-3.4.2-*.whl') assert fnmatch.filter(created, 'six-*.whl') + + +@mark.network +def test_require_hash(script, tmp_path): + reqs = tmp_path / 'requirements.txt' + reqs.write_text( + u'idna==2.10' + ' --hash=sha256:' + 'b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0' + ' --hash=sha256:' + 'b307872f855b18632ce0c21c5e45be78c0ea7ae4c15c828c20788b26921eb3f6' + ) + result = script.pip( + 'download', '--use-feature=fast-deps', '-r', str(reqs), + allow_stderr_warning=True, + ) + created = list(map(basename, result.files_created)) + assert fnmatch.filter(created, 'idna-2.10*') + + +@mark.network +def test_hash_mismatch(script, tmp_path): + reqs = tmp_path / 'requirements.txt' + reqs.write_text(u'idna==2.10 --hash=sha256:irna') + result = script.pip( + 'download', '--use-feature=fast-deps', '-r', str(reqs), + expect_error=True, + ) + assert 'DO NOT MATCH THE HASHES' in result.stderr From e93257c080221da6d607a958573c4ab0e007b34c Mon Sep 17 00:00:00 2001 From: Hugo <hugovk@users.noreply.github.com> Date: Tue, 18 Aug 2020 15:22:16 +0300 Subject: [PATCH 2462/3170] Warn Python 3.5 support is deprecated and will be removed in pip 21.0, Jan 2021 --- news/8181.removal | 1 + src/pip/_internal/cli/base_command.py | 14 +++++++++++++- tests/conftest.py | 4 ++-- 3 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 news/8181.removal diff --git a/news/8181.removal b/news/8181.removal new file mode 100644 index 00000000000..ae6bbe9f88a --- /dev/null +++ b/news/8181.removal @@ -0,0 +1 @@ +Deprecate support for Python 3.5 diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index c3b6a856be4..197400a72c5 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -158,7 +158,19 @@ def _main(self, args): "1st, 2020. Please upgrade your Python as Python 2.7 " "is no longer maintained. " ) + message - deprecated(message, replacement=None, gone_in=None) + deprecated(message, replacement=None, gone_in="21.0") + + if ( + sys.version_info[:2] == (3, 5) and + not options.no_python_version_warning + ): + message = ( + "Python 3.5 reached the end of its life on September " + "13th, 2020. Please upgrade your Python as Python 3.5 " + "is no longer maintained. pip 21.0 will drop support " + "for Python 3.5 in January 2021." + ) + deprecated(message, replacement=None, gone_in="21.0") # TODO: Try to get these passing down from the command? # without resorting to os.environ to hold these. diff --git a/tests/conftest.py b/tests/conftest.py index 2aab50207be..32b6e692610 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -470,8 +470,8 @@ def in_memory_pip(): @pytest.fixture(scope="session") def deprecated_python(): - """Used to indicate whether pip deprecated this python version""" - return sys.version_info[:2] in [(2, 7)] + """Used to indicate whether pip deprecated this Python version""" + return sys.version_info[:2] in [(2, 7), (3, 5)] @pytest.fixture(scope="session") From 1f0ace9a2e0ba1ea1551f5d0c217482368b33ca8 Mon Sep 17 00:00:00 2001 From: wim glenn <wim.glenn@gmail.com> Date: Tue, 18 Aug 2020 20:57:08 -0500 Subject: [PATCH 2463/3170] restore a broken slug anchor in user guide --- docs/html/user_guide.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 73a2643223b..d672be02e20 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -788,8 +788,6 @@ archives are built with identical packages. to use such a package, see :ref:`Controlling setup_requires<controlling-setup-requires>`. -.. _`Using pip from your program`: - Fixing conflicting dependencies =============================== @@ -971,6 +969,8 @@ issue tracker`_ if you believe that your problem has exposed a bug in pip. .. _"How do I ask a good question?": https://stackoverflow.com/help/how-to-ask .. _pip issue tracker: https://github.com/pypa/pip/issues +.. _`Using pip from your program`: + Using pip from your program =========================== From 4c1dcbba9cc77eba7c6596317a1ebedbe328afef Mon Sep 17 00:00:00 2001 From: wim glenn <wim.glenn@gmail.com> Date: Wed, 19 Aug 2020 12:21:16 -0500 Subject: [PATCH 2464/3170] Create 8781.doc --- news/8781.doc | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/8781.doc diff --git a/news/8781.doc b/news/8781.doc new file mode 100644 index 00000000000..108cf1a5622 --- /dev/null +++ b/news/8781.doc @@ -0,0 +1 @@ +Fixed a broken slug anchor in user guide (https://github.com/pypa/pip/pull/8781) From ce85775155df227dec3bb3992db64a5f239861dc Mon Sep 17 00:00:00 2001 From: Srinivas Nyayapati <shireenrao@gmail.com> Date: Thu, 20 Aug 2020 07:47:36 -0400 Subject: [PATCH 2465/3170] Bump Sphinx to v3.1.2 and fix rst issues --- docs/html/development/architecture/configuration-files.rst | 1 + news/629892ca-55da-4ca9-9cff-c15373e97ad1.trivial | 0 tools/requirements/docs.txt | 2 +- 3 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 news/629892ca-55da-4ca9-9cff-c15373e97ad1.trivial diff --git a/docs/html/development/architecture/configuration-files.rst b/docs/html/development/architecture/configuration-files.rst index ce0ef40ee27..2f96ea5ca50 100644 --- a/docs/html/development/architecture/configuration-files.rst +++ b/docs/html/development/architecture/configuration-files.rst @@ -109,6 +109,7 @@ manipulated. In addition to the methods discussed in the previous section, the methods used would be: .. py:class:: Configuration + :noindex: .. py:method:: get_file_to_edit() diff --git a/news/629892ca-55da-4ca9-9cff-c15373e97ad1.trivial b/news/629892ca-55da-4ca9-9cff-c15373e97ad1.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tools/requirements/docs.txt b/tools/requirements/docs.txt index acbd3390631..e787ceca6bc 100644 --- a/tools/requirements/docs.txt +++ b/tools/requirements/docs.txt @@ -1,4 +1,4 @@ -sphinx == 2.4.3 +sphinx == 3.1.2 git+https://github.com/python/python-docs-theme.git#egg=python-docs-theme git+https://github.com/pypa/pypa-docs-theme.git#egg=pypa-docs-theme From e51de6ecb40203f733734ec6957505abcf7fac34 Mon Sep 17 00:00:00 2001 From: Srinivas Nyayapati <shireenrao@gmail.com> Date: Thu, 20 Aug 2020 07:54:34 -0400 Subject: [PATCH 2466/3170] Bump to latest version 3.2.1 --- tools/requirements/docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/requirements/docs.txt b/tools/requirements/docs.txt index e787ceca6bc..267d5b1ddfb 100644 --- a/tools/requirements/docs.txt +++ b/tools/requirements/docs.txt @@ -1,4 +1,4 @@ -sphinx == 3.1.2 +sphinx == 3.2.1 git+https://github.com/python/python-docs-theme.git#egg=python-docs-theme git+https://github.com/pypa/pypa-docs-theme.git#egg=pypa-docs-theme From 4e34f6530a97ca16ee524f5ac97b90fa46475e26 Mon Sep 17 00:00:00 2001 From: Srinivas Nyayapati <shireenrao@gmail.com> Date: Thu, 20 Aug 2020 12:24:58 -0400 Subject: [PATCH 2467/3170] add theme config to set background color and text color for code-blocks --- docs/html/conf.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/docs/html/conf.py b/docs/html/conf.py index bd44b9bff9d..2ef2647ce72 100644 --- a/docs/html/conf.py +++ b/docs/html/conf.py @@ -143,7 +143,9 @@ 'collapsiblesidebar': True, 'externalrefs': True, 'navigation_depth': 3, - 'issues_url': 'https://github.com/pypa/pip/issues' + 'issues_url': 'https://github.com/pypa/pip/issues', + 'codebgcolor': '#eeffcc', + 'codetextcolor': '#333333', } # Add any paths that contain custom themes here, relative to this directory. From c84ef7a67c2bb3b38a4e0616a433b143e0ddc87c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Thu, 20 Aug 2020 17:10:59 +0700 Subject: [PATCH 2468/3170] Mark tests using remote svn and hg as xfail The source repositories for testing is no longer available. --- news/559bb022-21ae-498c-a2ce-2c354d880f5e.trivial | 0 tests/functional/test_install.py | 1 + tests/functional/test_install_reqs.py | 1 + tests/functional/test_install_user.py | 1 + tests/functional/test_uninstall.py | 2 ++ 5 files changed, 5 insertions(+) create mode 100644 news/559bb022-21ae-498c-a2ce-2c354d880f5e.trivial diff --git a/news/559bb022-21ae-498c-a2ce-2c354d880f5e.trivial b/news/559bb022-21ae-498c-a2ce-2c354d880f5e.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index abf5a8d0744..17a72bca82e 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -758,6 +758,7 @@ def test_install_using_install_option_and_editable(script, tmpdir): result.did_create(script_file) +@pytest.mark.xfail @pytest.mark.network @need_mercurial @windows_workaround_7667 diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index d12e19b211a..c879b6903dd 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -159,6 +159,7 @@ def test_relative_requirements_file( result.did_create(egg_link_file) +@pytest.mark.xfail @pytest.mark.network @need_svn def test_multiple_requirements_files(script, tmpdir, with_wheel): diff --git a/tests/functional/test_install_user.py b/tests/functional/test_install_user.py index 24169470a70..c5d7acced80 100644 --- a/tests/functional/test_install_user.py +++ b/tests/functional/test_install_user.py @@ -45,6 +45,7 @@ def test_reset_env_system_site_packages_usersite(self, script): project_name = result.stdout.strip() assert 'INITools' == project_name, project_name + @pytest.mark.xfail @pytest.mark.network @need_svn @pytest.mark.incompatible_with_test_venv diff --git a/tests/functional/test_uninstall.py b/tests/functional/test_uninstall.py index 1f2fe69125b..6e4aec0e584 100644 --- a/tests/functional/test_uninstall.py +++ b/tests/functional/test_uninstall.py @@ -307,6 +307,7 @@ def test_uninstall_easy_installed_console_scripts(script): ) +@pytest.mark.xfail @pytest.mark.network @need_svn def test_uninstall_editable_from_svn(script, tmpdir): @@ -372,6 +373,7 @@ def _test_uninstall_editable_with_source_outside_venv( ) +@pytest.mark.xfail @pytest.mark.network @need_svn def test_uninstall_from_reqs_file(script, tmpdir): From f060669e05931e6d69c315b4e718ea38f91bf84d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Mon, 24 Aug 2020 15:42:31 +0700 Subject: [PATCH 2469/3170] Fix indentation of lists and literal blocks --- docs/html/user_guide.rst | 38 +++++++++++++++----------------------- 1 file changed, 15 insertions(+), 23 deletions(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 73a2643223b..bce2d5e65e7 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -36,9 +36,7 @@ directly from distribution files. The most common scenario is to install from `PyPI`_ using :ref:`Requirement -Specifiers` - - :: +Specifiers` :: $ pip install SomePackage # latest version $ pip install SomePackage==1.0.4 # specific version @@ -119,9 +117,7 @@ Requirements Files ================== "Requirements files" are files containing a list of items to be -installed using :ref:`pip install` like so: - - :: +installed using :ref:`pip install` like so:: pip install -r requirements.txt @@ -207,9 +203,7 @@ contents is nearly identical to :ref:`Requirements Files`. There is one key difference: Including a package in a constraints file does not trigger installation of the package. -Use a constraints file like so: - - :: +Use a constraints file like so:: pip install -c constraints.txt @@ -807,16 +801,14 @@ Understanding your error message When you get a ``ResolutionImpossible`` error, you might see something like this: -:: - - pip install package_coffee==0.44.1 package_tea==4.3.0 - -:: +.. code-block:: console - Due to conflicting dependencies pip cannot install package_coffee and - package_tea: - - package_coffee depends on package_water<3.0.0,>=2.4.2 - - package_tea depends on package_water==2.3.1 + $ pip install package_coffee==0.44.1 package_tea==4.3.0 + ... + Due to conflicting dependencies pip cannot install + package_coffee and package_tea: + - package_coffee depends on package_water<3.0.0,>=2.4.2 + - package_tea depends on package_water==2.3.1 In this example, pip cannot install the packages you have requested, because they each depend on different versions of the same package @@ -1138,11 +1130,11 @@ How to test - If you use pip to install your software, try out the new resolver and let us know if it works for you with ``pip install``. Try: - - installing several packages simultaneously - - re-creating an environment using a ``requirements.txt`` file - - using ``pip install --force-reinstall`` to check whether - it does what you think it should - - using constraints files + - installing several packages simultaneously + - re-creating an environment using a ``requirements.txt`` file + - using ``pip install --force-reinstall`` to check whether + it does what you think it should + - using constraints files - If you have a build pipeline that depends on pip installing your dependencies for you, check that the new resolver does what you From fd48624ea95eea4978caf13166b31badb2fb6a4e Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Date: Mon, 24 Aug 2020 14:19:02 +0530 Subject: [PATCH 2470/3170] Update and rename 8781.doc to 8781.trivial --- news/8781.doc | 1 - news/8781.trivial | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) delete mode 100644 news/8781.doc create mode 100644 news/8781.trivial diff --git a/news/8781.doc b/news/8781.doc deleted file mode 100644 index 108cf1a5622..00000000000 --- a/news/8781.doc +++ /dev/null @@ -1 +0,0 @@ -Fixed a broken slug anchor in user guide (https://github.com/pypa/pip/pull/8781) diff --git a/news/8781.trivial b/news/8781.trivial new file mode 100644 index 00000000000..e6044f52f7d --- /dev/null +++ b/news/8781.trivial @@ -0,0 +1 @@ +Fix a broken slug anchor in user guide. From 984fa3c66419c69cadc9b45daae54522666cc5ed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Mon, 24 Aug 2020 15:48:39 +0700 Subject: [PATCH 2471/3170] Make version specifier explanation easier to read in reST --- docs/html/user_guide.rst | 52 +++++++++++-------- ...3f7456-cc25-4df9-9518-4732b1e07fe5.trivial | 0 2 files changed, 31 insertions(+), 21 deletions(-) create mode 100644 news/093f7456-cc25-4df9-9518-4732b1e07fe5.trivial diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index bce2d5e65e7..4699f95a1d4 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -827,27 +827,37 @@ commonly understood comparison operators to specify the required version However, Python packaging also supports some more complex ways for specifying package versions (e.g. ``~=`` or ``*``): -.. csv-table:: - :header: "Operator", "Description", "Example" - - ``>``, "Any version greater than the specified version", "``>3.1``: any - version greater than 3.1" - ``<``, "Any version less than the specified version", "``<3.1``: any version - less than ``3.1``" - ``<=``, "Any version less than or equal to the specified version", "``<=3.1``: - any version less than or equal to ``3.1``" - ``>=``, "Any version greater than or equal to the specified - version", "``>=3.1``: version ``3.1`` and greater" - ``==``, "Exactly the specified version", ``==3.1``: only version ``3.1`` - ``!=``, "Any version not equal to the specified version", "``!=3.1``: any - version other than ``3.1``" - ``~=``, "Any compatible release. Compatible releases are releases that are - within the same major or minor version, assuming the package author is using - semantic versioning.", "``~=3.1``: version ``3.1`` or later, but not version - ``4.0`` or later. ``~=3.1.2``: version ``3.1.2`` or later, but not - version ``3.2.0`` or later." - ``*``,Can be used at the end of a version number to represent "all", "``== 3. - 1.*``: any version that starts with ``3.1``. Equivalent to ``~=3.1.0``." ++----------+---------------------------------+--------------------------------+ +| Operator | Description | Example | ++==========+=================================+================================+ +| ``>`` | Any version greater than | ``>3.1``: any version | +| | the specified version. | greater than ``3.1``. | ++----------+---------------------------------+--------------------------------+ +| ``<`` | Any version less than | ``<3.1``: any version | +| | the specified version. | less than ``3.1``. | ++----------+---------------------------------+--------------------------------+ +| ``<=`` | Any version less than or | ``<=3.1``: any version | +| | equal to the specified version. | less than or equal to ``3.1``. | ++----------+---------------------------------+--------------------------------+ +| ``>=`` | Any version greater than or | ``>=3.1``: | +| | equal to the specified version. | version ``3.1`` and greater. | ++----------+---------------------------------+--------------------------------+ +| ``==`` | Exactly the specified version. | ``==3.1``: only ``3.1``. | ++----------+---------------------------------+--------------------------------+ +| ``!=`` | Any version not equal | ``!=3.1``: any version | +| | to the specified version. | other than ``3.1``. | ++----------+---------------------------------+--------------------------------+ +| ``~=`` | Any compatible release. | ``~=3.1``: version ``3.1`` | +| | Compatible releases are | or later, but not | +| | releases that are within the | version ``4.0`` or later. | +| | same major or minor version, | ``~=3.1.2``: version ``3.1.2`` | +| | assuming the package author | or later, but not | +| | is using semantic versioning. | version ``3.2.0`` or later. | ++----------+---------------------------------+--------------------------------+ +| ``*`` | Can be used at the end of | ``==3.1.*``: any version | +| | a version number to represent | that starts with ``3.1``. | +| | *all*, | Equivalent to ``~=3.1.0``. | ++----------+---------------------------------+--------------------------------+ The detailed specification of supported comparison operators can be found in :pep:`440`. diff --git a/news/093f7456-cc25-4df9-9518-4732b1e07fe5.trivial b/news/093f7456-cc25-4df9-9518-4732b1e07fe5.trivial new file mode 100644 index 00000000000..e69de29bb2d From 8b2b92485c497accbb3f0169142629ab83fd16e5 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 14 Aug 2020 08:41:42 +0800 Subject: [PATCH 2472/3170] Include Requires-Python dep even with --no-deps --- .../resolution/resolvelib/candidates.py | 24 ++++++-------- tests/functional/test_new_resolver.py | 32 +++++++++++++++++++ 2 files changed, 41 insertions(+), 15 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index ea2a8686b53..ff2b336d9e0 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -230,31 +230,25 @@ def dist(self): self._prepare() return self._dist - def _get_requires_python_specifier(self): - # type: () -> Optional[SpecifierSet] + def _get_requires_python_dependency(self): + # type: () -> Optional[Requirement] requires_python = get_requires_python(self.dist) if requires_python is None: return None try: spec = SpecifierSet(requires_python) except InvalidSpecifier as e: - logger.warning( - "Package %r has an invalid Requires-Python: %s", self.name, e, - ) + message = "Package %r has an invalid Requires-Python: %s" + logger.warning(message, self.name, e) return None - return spec + return self._factory.make_requires_python_requirement(spec) def iter_dependencies(self, with_requires): # type: (bool) -> Iterable[Optional[Requirement]] - if not with_requires: - return - for r in self.dist.requires(): + requires = self.dist.requires() if with_requires else () + for r in requires: yield self._factory.make_requirement_from_spec(str(r), self._ireq) - python_dep = self._factory.make_requires_python_requirement( - self._get_requires_python_specifier(), - ) - if python_dep: - yield python_dep + yield self._get_requires_python_dependency() def get_install_requirement(self): # type: () -> Optional[InstallRequirement] @@ -285,7 +279,7 @@ def __init__( wheel = Wheel(ireq.link.filename) wheel_name = canonicalize_name(wheel.name) assert name == wheel_name, ( - "{!r} != {!r} for wheel".format(name, wheel_name) + "{!r} != {!r} for wheel".format(name, wheel_name) ) # Version may not be present for PEP 508 direct URLs if version is not None: diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 46e32ddd3d7..1dab8d47091 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -988,3 +988,35 @@ def test_new_resolver_local_and_req(script): source_dir, "pkg!=0.1.0", expect_error=True, ) + + +def test_new_resolver_no_deps_checks_requires_python(script): + create_basic_wheel_for_package( + script, + "base", + "0.1.0", + depends=["dep"], + requires_python="<2", # Something that always fails. + ) + create_basic_wheel_for_package( + script, + "dep", + "0.2.0", + ) + + result = script.pip( + "install", + "--use-feature=2020-resolver", + "--no-cache-dir", + "--no-index", + "--no-deps", + "--find-links", script.scratch_path, + "base", + expect_error=True, + ) + + message = ( + "Package 'base' requires a different Python: " + "{}.{}.{} not in '<2'".format(*sys.version_info[:3]) + ) + assert message in result.stderr From 5401cc6e1003eeef68421b284697f3082b9085e6 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 14 Aug 2020 08:45:26 +0800 Subject: [PATCH 2473/3170] News --- news/8758.bugfix | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 news/8758.bugfix diff --git a/news/8758.bugfix b/news/8758.bugfix new file mode 100644 index 00000000000..9f44b7e47a4 --- /dev/null +++ b/news/8758.bugfix @@ -0,0 +1,2 @@ +New resolver: Correctly respect ``Requires-Python`` metadata to reject +incompatible packages in ``--no-deps`` mode. From 46bdaa1ece0f727b35fe646e238211f70d16bf8b Mon Sep 17 00:00:00 2001 From: Nicole Harris <n.harris@kabucreative.com> Date: Wed, 26 Aug 2020 10:42:22 +0100 Subject: [PATCH 2474/3170] Add ux docs to pip documentation --- docs/html/index.rst | 1 + docs/html/ux_research_design.rst | 67 ++++++++++++++++++++++++++++++++ 2 files changed, 68 insertions(+) create mode 100644 docs/html/ux_research_design.rst diff --git a/docs/html/index.rst b/docs/html/index.rst index 1ec91558519..860abcac9ee 100644 --- a/docs/html/index.rst +++ b/docs/html/index.rst @@ -14,6 +14,7 @@ Please take a look at our documentation for how to install and use pip: user_guide reference/index development/index + ux_research_design news In 2020, we're working on improvements to the heart of pip. Please `learn more and take our survey`_ to help us do it right. diff --git a/docs/html/ux_research_design.rst b/docs/html/ux_research_design.rst new file mode 100644 index 00000000000..302091917ea --- /dev/null +++ b/docs/html/ux_research_design.rst @@ -0,0 +1,67 @@ +==================== +UX Research & Design +==================== + +Over the course of 2020, the pip team has been working on improving pip's user +experience. + +Currently, our focus is on: + +1. `Understanding who uses pip`_ +2. `Understanding how pip compares to other package managers, and how pip supports other Python packaging tools`_ +3. `Understanding how pip's functionality is used, and how it could be improved`_ +4. `Understanding how pip's documentation is used, and how it could be improved`_ + +You can read the `overall plan`_ and the `mid-year update`_ to learn more about +our work. + +How to contribute +----------------- + +It is important that we hear from pip users so that we can: + +- Understand how pip is currently used by the Python community +- Understand how pip users would *like* pip to behave +- Understand pip's strengths and shortcomings +- Make useful design recommendations for improving pip + +If you are interested in participating in pip user research, please +`join pip's user panel`_. +You can `read more information about the user panel here`_. + +We are also looking for users to: + +- `Give us feedback about pip's new resolver`_ +- `Tell us how pip should handle conflicts with already installed packages when updating other packages`_ + + +Other ways to contribute +======================== + +You can also help by: + +- Reporting UX issues (or suggesting ideas for improvement) on the `pip issue tracker`_ +- `Working on UX issues`_ +- Testing new features. Currently, we are looking for users to `test pip's new dependency resolver`_. + +Next steps +---------- + +In the coming months we will extend this documentation to include: + +1. Summaries of our user research, including recommendations for how to improve pip +2. Tools for the pip team to continue to practice user centered design (e.g. user personas, etc.) + +.. _Understanding who uses pip: https://github.com/pypa/pip/issues/8518 +.. _Understanding how pip compares to other package managers, and how pip supports other Python packaging tools: https://github.com/pypa/pip/issues/8515 +.. _Understanding how pip's functionality is used, and how it could be improved: https://github.com/pypa/pip/issues/8516 +.. _Understanding how pip's documentation is used, and how it could be improved: https://github.com/pypa/pip/issues/8517 +.. _overall plan: https://wiki.python.org/psf/Pip2020DonorFundedRoadmap +.. _mid-year update: http://pyfound.blogspot.com/2020/07/pip-team-midyear-report.html +.. _join pip's user panel: https://tools.simplysecure.org/survey/index.php?r=survey/index&sid=827389&lang=en +.. _read more information about the user panel here: https://bit.ly/pip-ux-studies +.. _Give us feedback about pip's new resolver: https://tools.simplysecure.org/survey/index.php?r=survey/index&sid=989272&lang=en +.. _Tell us how pip should handle conflicts with already installed packages when updating other packages: https://docs.google.com/forms/d/1KtejgZnK-6NPTmAJ-7aWox4iktcezQauW-Mh3gbnydQ/edit +.. _pip issue tracker: https://github.com/pypa/pip/issues/new +.. _Working on UX issues: https://github.com/pypa/pip/issues?q=is%3Aissue+is%3Aopen+label%3A%22K%3A+UX%22 +.. _test pip's new dependency resolver: https://pip.pypa.io/en/latest/user_guide/#changes-to-the-pip-dependency-resolver-in-20-2-2020 From 3f40b10fe004eb01208b853a427fd674b4097cce Mon Sep 17 00:00:00 2001 From: Nicole Harris <n.harris@kabucreative.com> Date: Wed, 26 Aug 2020 10:44:20 +0100 Subject: [PATCH 2475/3170] Add news entry --- news/8807.doc | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/8807.doc diff --git a/news/8807.doc b/news/8807.doc new file mode 100644 index 00000000000..6ef1a123adb --- /dev/null +++ b/news/8807.doc @@ -0,0 +1 @@ +Add ux documentation From 1c61b4679fe875fdd6b3e17339d006041ae8438c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Fri, 28 Aug 2020 11:47:29 +0200 Subject: [PATCH 2476/3170] Add failing test fetching a git commit in refs --- tests/functional/test_vcs_git.py | 27 +++++++++++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/tests/functional/test_vcs_git.py b/tests/functional/test_vcs_git.py index 37c35c4b52a..8dd0ffcd99f 100644 --- a/tests/functional/test_vcs_git.py +++ b/tests/functional/test_vcs_git.py @@ -250,3 +250,30 @@ def test_get_repository_root(script): root2 = Git.get_repository_root(version_pkg_path.joinpath("tests")) assert os.path.normcase(root2) == os.path.normcase(version_pkg_path) + + +def test_resolve_commit_not_on_branch(script, tmp_path): + repo_path = tmp_path / "repo" + repo_file = repo_path / "file.txt" + clone_path = repo_path / "clone" + repo_path.mkdir() + script.run("git", "init", cwd=str(repo_path)) + repo_file.write_text(u".") + script.run("git", "add", "file.txt", cwd=str(repo_path)) + script.run("git", "commit", "-m", "initial commit", cwd=str(repo_path)) + script.run("git", "checkout", "-b", "abranch", cwd=str(repo_path)) + # create a commit + repo_file.write_text(u"..") + script.run("git", "commit", "-a", "-m", "commit 1", cwd=str(repo_path)) + commit = script.run( + "git", "rev-parse", "HEAD", cwd=str(repo_path) + ).stdout.strip() + # make sure our commit is not on a branch + script.run("git", "checkout", "master", cwd=str(repo_path)) + script.run("git", "branch", "-D", "abranch", cwd=str(repo_path)) + # create a ref that points to our commit + (repo_path / ".git" / "refs" / "myrefs").mkdir(parents=True) + (repo_path / ".git" / "refs" / "myrefs" / "myref").write_text(commit) + # check we can fetch our commit + rev_options = Git.make_rev_options(commit) + Git().fetch_new(str(clone_path), repo_path.as_uri(), rev_options) From 3aa0c2ed914b26ab1bc4379caf98ab7e68a3000e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sun, 30 Aug 2020 11:46:05 +0200 Subject: [PATCH 2477/3170] Git fetch more aggressively Before we were fetching only revisions starting with refs/. Now we also fetch revisions that look like commit that we don't have locally. --- news/8815.feature | 2 ++ src/pip/_internal/vcs/git.py | 41 ++++++++++++++++++++++++++++++++++-- 2 files changed, 41 insertions(+), 2 deletions(-) create mode 100644 news/8815.feature diff --git a/news/8815.feature b/news/8815.feature new file mode 100644 index 00000000000..7d9149d69c3 --- /dev/null +++ b/news/8815.feature @@ -0,0 +1,2 @@ +When installing a git URL that refers to a commit that is not available locally +after git clone, attempt to fetch it from the remote. diff --git a/src/pip/_internal/vcs/git.py b/src/pip/_internal/vcs/git.py index a9c7fb66e33..308a87dfa48 100644 --- a/src/pip/_internal/vcs/git.py +++ b/src/pip/_internal/vcs/git.py @@ -163,6 +163,29 @@ def get_revision_sha(cls, dest, rev): return (sha, False) + @classmethod + def _should_fetch(cls, dest, rev): + """ + Return true if rev is a ref or is a commit that we don't have locally. + + Branches and tags are not considered in this method because they are + assumed to be always available locally (which is a normal outcome of + ``git clone`` and ``git fetch --tags``). + """ + if rev.startswith("refs/"): + # Always fetch remote refs. + return True + + if not looks_like_hash(rev): + # Git fetch would fail with abbreviated commits. + return False + + if cls.has_commit(dest, rev): + # Don't fetch if we have the commit locally. + return False + + return True + @classmethod def resolve_revision(cls, dest, url, rev_options): # type: (str, HiddenText, RevOptions) -> RevOptions @@ -194,10 +217,10 @@ def resolve_revision(cls, dest, url, rev_options): rev, ) - if not rev.startswith('refs/'): + if not cls._should_fetch(dest, rev): return rev_options - # If it looks like a ref, we have to fetch it explicitly. + # fetch the requested revision cls.run_command( make_command('fetch', '-q', url, rev_options.to_args()), cwd=dest, @@ -306,6 +329,20 @@ def get_remote_url(cls, location): url = found_remote.split(' ')[1] return url.strip() + @classmethod + def has_commit(cls, location, rev): + """ + Check if rev is a commit that is available in the local repository. + """ + try: + cls.run_command( + ['rev-parse', '-q', '--verify', "sha^" + rev], cwd=location + ) + except SubProcessError: + return False + else: + return True + @classmethod def get_revision(cls, location, rev=None): if rev is None: From 5797a080b60962113ed743d5085e76b226faceae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Fri, 28 Aug 2020 11:53:14 +0200 Subject: [PATCH 2478/3170] Fix test that now requires a working git repo --- tests/unit/test_vcs.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/tests/unit/test_vcs.py b/tests/unit/test_vcs.py index 93598c36739..b6ed86b6296 100644 --- a/tests/unit/test_vcs.py +++ b/tests/unit/test_vcs.py @@ -173,8 +173,9 @@ def test_git_resolve_revision_not_found_warning(get_sha_mock, caplog): sha = 40 * 'a' rev_options = Git.make_rev_options(sha) - new_options = Git.resolve_revision('.', url, rev_options) - assert new_options.rev == sha + # resolve_revision with a full sha would fail here because + # it attempts a git fetch. This case is now covered by + # test_resolve_commit_not_on_branch. rev_options = Git.make_rev_options(sha[:6]) new_options = Git.resolve_revision('.', url, rev_options) From eff32df28abe05c5f25515faf587bf98e3791978 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@acsone.eu> Date: Sun, 30 Aug 2020 18:15:35 +0200 Subject: [PATCH 2479/3170] Add spacing in test Co-authored-by: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> --- tests/functional/test_vcs_git.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/tests/functional/test_vcs_git.py b/tests/functional/test_vcs_git.py index 8dd0ffcd99f..8b07ae6673b 100644 --- a/tests/functional/test_vcs_git.py +++ b/tests/functional/test_vcs_git.py @@ -258,22 +258,27 @@ def test_resolve_commit_not_on_branch(script, tmp_path): clone_path = repo_path / "clone" repo_path.mkdir() script.run("git", "init", cwd=str(repo_path)) + repo_file.write_text(u".") script.run("git", "add", "file.txt", cwd=str(repo_path)) script.run("git", "commit", "-m", "initial commit", cwd=str(repo_path)) script.run("git", "checkout", "-b", "abranch", cwd=str(repo_path)) + # create a commit repo_file.write_text(u"..") script.run("git", "commit", "-a", "-m", "commit 1", cwd=str(repo_path)) commit = script.run( "git", "rev-parse", "HEAD", cwd=str(repo_path) ).stdout.strip() + # make sure our commit is not on a branch script.run("git", "checkout", "master", cwd=str(repo_path)) script.run("git", "branch", "-D", "abranch", cwd=str(repo_path)) + # create a ref that points to our commit (repo_path / ".git" / "refs" / "myrefs").mkdir(parents=True) (repo_path / ".git" / "refs" / "myrefs" / "myref").write_text(commit) + # check we can fetch our commit rev_options = Git.make_rev_options(commit) Git().fetch_new(str(clone_path), repo_path.as_uri(), rev_options) From 87d129a801c126f340273256dd355f6b5cd32763 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 1 Sep 2020 15:37:10 +0800 Subject: [PATCH 2480/3170] Replace custom URL parsing with url_to_path() --- src/pip/_internal/req/req_file.py | 16 ++-------------- 1 file changed, 2 insertions(+), 14 deletions(-) diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index 72a568bdfc3..c8d7a0a5ae2 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -21,7 +21,7 @@ from pip._internal.network.utils import raise_for_status from pip._internal.utils.encoding import auto_decode from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from pip._internal.utils.urls import get_url_scheme +from pip._internal.utils.urls import get_url_scheme, url_to_path if MYPY_CHECK_RUNNING: from optparse import Values @@ -572,16 +572,7 @@ def get_file_content(url, session, comes_from=None): 'Requirements file {} references URL {}, ' 'which is local'.format(comes_from, url) ) - - path = url.split(':', 1)[1] - path = path.replace('\\', '/') - match = _url_slash_drive_re.match(path) - if match: - path = match.group(1) + ':' + path.split('|', 1)[1] - path = urllib_parse.unquote(path) - if path.startswith('/'): - path = '/' + path.lstrip('/') - url = path + url = url_to_path(url) try: with open(url, 'rb') as f: @@ -591,6 +582,3 @@ def get_file_content(url, session, comes_from=None): 'Could not open requirements file: {}'.format(exc) ) return url, content - - -_url_slash_drive_re = re.compile(r'/*([a-z])\|', re.I) From d570e504026b84570b7c707f68da7fc5b6b8283a Mon Sep 17 00:00:00 2001 From: Nicole Harris <n.harris@kabucreative.com> Date: Tue, 1 Sep 2020 18:43:11 +0100 Subject: [PATCH 2481/3170] Update docs based on feedback from UX team --- docs/html/ux_research_design.rst | 30 ++++++++++++++++++++++-------- 1 file changed, 22 insertions(+), 8 deletions(-) diff --git a/docs/html/ux_research_design.rst b/docs/html/ux_research_design.rst index 302091917ea..165b6949670 100644 --- a/docs/html/ux_research_design.rst +++ b/docs/html/ux_research_design.rst @@ -18,10 +18,14 @@ our work. How to contribute ----------------- +Participate in UX research +========================== + It is important that we hear from pip users so that we can: - Understand how pip is currently used by the Python community -- Understand how pip users would *like* pip to behave +- Understand how pip users *need* pip to behave +- Understand how pip users *would like* pip to behave - Understand pip's strengths and shortcomings - Make useful design recommendations for improving pip @@ -34,15 +38,25 @@ We are also looking for users to: - `Give us feedback about pip's new resolver`_ - `Tell us how pip should handle conflicts with already installed packages when updating other packages`_ +Report UX issues +================ + +If you believe that you have found a user experience bug in pip, or you have +ideas for how pip could be made better for all users, you please file an issue +on the `pip issue tracker`_. + +Work on UX issues +================= -Other ways to contribute -======================== +You can help improve pip's user experience by `working on UX issues`_. +Issues that are ideal for new contributors are marked with "good first issue". -You can also help by: +Test new features +================= -- Reporting UX issues (or suggesting ideas for improvement) on the `pip issue tracker`_ -- `Working on UX issues`_ -- Testing new features. Currently, we are looking for users to `test pip's new dependency resolver`_. +You can help the team by testing new features as they are released to the +community. Currently, we are looking for users to +`test pip's new dependency resolver`_. Next steps ---------- @@ -63,5 +77,5 @@ In the coming months we will extend this documentation to include: .. _Give us feedback about pip's new resolver: https://tools.simplysecure.org/survey/index.php?r=survey/index&sid=989272&lang=en .. _Tell us how pip should handle conflicts with already installed packages when updating other packages: https://docs.google.com/forms/d/1KtejgZnK-6NPTmAJ-7aWox4iktcezQauW-Mh3gbnydQ/edit .. _pip issue tracker: https://github.com/pypa/pip/issues/new -.. _Working on UX issues: https://github.com/pypa/pip/issues?q=is%3Aissue+is%3Aopen+label%3A%22K%3A+UX%22 +.. _working on UX issues: https://github.com/pypa/pip/issues?q=is%3Aissue+is%3Aopen+label%3A%22K%3A+UX%22 .. _test pip's new dependency resolver: https://pip.pypa.io/en/latest/user_guide/#changes-to-the-pip-dependency-resolver-in-20-2-2020 From 700eb7734fc10174eff3d198adad0db326047032 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 3 Sep 2020 16:33:52 +0800 Subject: [PATCH 2482/3170] Hashes from lines should intersect, not union --- news/8839.bugfix | 3 + .../resolution/resolvelib/factory.py | 2 +- src/pip/_internal/utils/hashes.py | 20 +++-- tests/functional/test_new_resolver_hashes.py | 87 +++++++++++++++++++ 4 files changed, 105 insertions(+), 7 deletions(-) create mode 100644 news/8839.bugfix create mode 100644 tests/functional/test_new_resolver_hashes.py diff --git a/news/8839.bugfix b/news/8839.bugfix new file mode 100644 index 00000000000..987b801e932 --- /dev/null +++ b/news/8839.bugfix @@ -0,0 +1,3 @@ +New resolver: If a package appears multiple times in user specification with +different ``--hash`` options, only hashes that present in all specifications +should be allowed. diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index e5630494956..7209f8c94eb 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -168,7 +168,7 @@ def _iter_found_candidates( extras = frozenset() # type: FrozenSet[str] for ireq in ireqs: specifier &= ireq.req.specifier - hashes |= ireq.hashes(trust_internet=False) + hashes &= ireq.hashes(trust_internet=False) extras |= frozenset(ireq.extras) # We use this to ensure that we only yield a single candidate for diff --git a/src/pip/_internal/utils/hashes.py b/src/pip/_internal/utils/hashes.py index d1b062fedf6..d9f74a64083 100644 --- a/src/pip/_internal/utils/hashes.py +++ b/src/pip/_internal/utils/hashes.py @@ -46,16 +46,24 @@ def __init__(self, hashes=None): """ self._allowed = {} if hashes is None else hashes - def __or__(self, other): + def __and__(self, other): # type: (Hashes) -> Hashes if not isinstance(other, Hashes): return NotImplemented - new = self._allowed.copy() + + # If either of the Hashes object is entirely empty (i.e. no hash + # specified at all), all hashes from the other object are allowed. + if not other: + return self + if not self: + return other + + # Otherwise only hashes that present in both objects are allowed. + new = {} for alg, values in iteritems(other._allowed): - try: - new[alg] += values - except KeyError: - new[alg] = values + if alg not in self._allowed: + continue + new[alg] = [v for v in values if v in self._allowed[alg]] return Hashes(new) @property diff --git a/tests/functional/test_new_resolver_hashes.py b/tests/functional/test_new_resolver_hashes.py new file mode 100644 index 00000000000..130f86b622b --- /dev/null +++ b/tests/functional/test_new_resolver_hashes.py @@ -0,0 +1,87 @@ +import collections +import hashlib + +import pytest + +from pip._internal.utils.urls import path_to_url +from tests.lib import ( + create_basic_sdist_for_package, + create_basic_wheel_for_package, +) + +_FindLinks = collections.namedtuple( + "_FindLinks", "index_html sdist_hash wheel_hash", +) + + +def _create_find_links(script): + sdist_path = create_basic_sdist_for_package(script, "base", "0.1.0") + wheel_path = create_basic_wheel_for_package(script, "base", "0.1.0") + + sdist_hash = hashlib.sha256(sdist_path.read_bytes()).hexdigest() + wheel_hash = hashlib.sha256(wheel_path.read_bytes()).hexdigest() + + index_html = script.scratch_path / "index.html" + index_html.write_text( + """ + <a href="{sdist_url}#sha256={sdist_hash}">{sdist_path.stem}</a> + <a href="{wheel_url}#sha256={wheel_hash}">{wheel_path.stem}</a> + """.format( + sdist_url=path_to_url(sdist_path), + sdist_hash=sdist_hash, + sdist_path=sdist_path, + wheel_url=path_to_url(wheel_path), + wheel_hash=wheel_hash, + wheel_path=wheel_path, + ) + ) + + return _FindLinks(index_html, sdist_hash, wheel_hash) + + +@pytest.mark.parametrize( + "requirements_template, message", + [ + ( + """ + base==0.1.0 --hash=sha256:{sdist_hash} --hash=sha256:{wheel_hash} + base==0.1.0 --hash=sha256:{sdist_hash} --hash=sha256:{wheel_hash} + """, + "Checked 2 links for project 'base' against 2 hashes " + "(2 matches, 0 no digest): discarding no candidates", + ), + ( + # Different hash lists are intersected. + """ + base==0.1.0 --hash=sha256:{sdist_hash} --hash=sha256:{wheel_hash} + base==0.1.0 --hash=sha256:{sdist_hash} + """, + "Checked 2 links for project 'base' against 1 hashes " + "(1 matches, 0 no digest): discarding 1 non-matches", + ), + ], + ids=["identical", "intersect"], +) +def test_new_resolver_hash_intersect(script, requirements_template, message): + find_links = _create_find_links(script) + + requirements_txt = script.scratch_path / "requirements.txt" + requirements_txt.write_text( + requirements_template.format( + sdist_hash=find_links.sdist_hash, + wheel_hash=find_links.wheel_hash, + ), + ) + + result = script.pip( + "install", + "--use-feature=2020-resolver", + "--no-cache-dir", + "--no-deps", + "--no-index", + "--find-links", find_links.index_html, + "--verbose", + "--requirement", requirements_txt, + ) + + assert message in result.stdout, str(result) From dfaa1110047165f4baf65428bf332e6912994252 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 3 Sep 2020 17:07:51 +0800 Subject: [PATCH 2483/3170] Pull in hashes from constraint files --- news/8792.bugfix | 2 + .../_internal/resolution/resolvelib/base.py | 37 +++++++++++++++++- .../resolution/resolvelib/factory.py | 11 ++++-- .../resolution/resolvelib/provider.py | 7 ++-- .../resolution/resolvelib/resolver.py | 8 ++-- tests/functional/test_new_resolver_hashes.py | 38 +++++++++++++++++++ .../resolution_resolvelib/test_requirement.py | 7 ++-- 7 files changed, 95 insertions(+), 15 deletions(-) create mode 100644 news/8792.bugfix diff --git a/news/8792.bugfix b/news/8792.bugfix new file mode 100644 index 00000000000..e83bdb09cfe --- /dev/null +++ b/news/8792.bugfix @@ -0,0 +1,2 @@ +New resolver: Pick up hash declarations in constraints files and use them to +filter available distributions. diff --git a/src/pip/_internal/resolution/resolvelib/base.py b/src/pip/_internal/resolution/resolvelib/base.py index 9245747bf2b..7c09cd70b8d 100644 --- a/src/pip/_internal/resolution/resolvelib/base.py +++ b/src/pip/_internal/resolution/resolvelib/base.py @@ -1,5 +1,8 @@ +from pip._vendor.packaging.specifiers import SpecifierSet from pip._vendor.packaging.utils import canonicalize_name +from pip._internal.req.req_install import InstallRequirement +from pip._internal.utils.hashes import Hashes from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: @@ -8,7 +11,6 @@ from pip._vendor.packaging.version import _BaseVersion from pip._internal.models.link import Link - from pip._internal.req.req_install import InstallRequirement CandidateLookup = Tuple[ Optional["Candidate"], @@ -24,6 +26,39 @@ def format_name(project, extras): return "{}[{}]".format(project, ",".join(canonical_extras)) +class Constraint(object): + def __init__(self, specifier, hashes): + # type: (SpecifierSet, Hashes) -> None + self.specifier = specifier + self.hashes = hashes + + @classmethod + def empty(cls): + # type: () -> Constraint + return Constraint(SpecifierSet(), Hashes()) + + @classmethod + def from_ireq(cls, ireq): + # type: (InstallRequirement) -> Constraint + return Constraint(ireq.specifier, ireq.hashes(trust_internet=False)) + + def __nonzero__(self): + # type: () -> bool + return bool(self.specifier) or bool(self.hashes) + + def __bool__(self): + # type: () -> bool + return self.__nonzero__() + + def __and__(self, other): + # type: (InstallRequirement) -> Constraint + if not isinstance(other, InstallRequirement): + return NotImplemented + specifier = self.specifier & other.specifier + hashes = self.hashes & other.hashes(trust_internet=False) + return Constraint(specifier, hashes) + + class Requirement(object): @property def name(self): diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 7209f8c94eb..ed310dd8c7c 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -22,6 +22,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.virtualenv import running_under_virtualenv +from .base import Constraint from .candidates import ( AlreadyInstalledCandidate, EditableCandidate, @@ -152,6 +153,7 @@ def _iter_found_candidates( self, ireqs, # type: Sequence[InstallRequirement] specifier, # type: SpecifierSet + hashes, # type: Hashes ): # type: (...) -> Iterable[Candidate] if not ireqs: @@ -164,7 +166,6 @@ def _iter_found_candidates( template = ireqs[0] name = canonicalize_name(template.req.name) - hashes = Hashes() extras = frozenset() # type: FrozenSet[str] for ireq in ireqs: specifier &= ireq.req.specifier @@ -218,7 +219,7 @@ def _iter_found_candidates( return six.itervalues(candidates) def find_candidates(self, requirements, constraint): - # type: (Sequence[Requirement], SpecifierSet) -> Iterable[Candidate] + # type: (Sequence[Requirement], Constraint) -> Iterable[Candidate] explicit_candidates = set() # type: Set[Candidate] ireqs = [] # type: List[InstallRequirement] for req in requirements: @@ -231,7 +232,11 @@ def find_candidates(self, requirements, constraint): # If none of the requirements want an explicit candidate, we can ask # the finder for candidates. if not explicit_candidates: - return self._iter_found_candidates(ireqs, constraint) + return self._iter_found_candidates( + ireqs, + constraint.specifier, + constraint.hashes, + ) if constraint: name = explicit_candidates.pop().name diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index b2eb9d06ea5..80577a61c58 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -1,8 +1,9 @@ -from pip._vendor.packaging.specifiers import SpecifierSet from pip._vendor.resolvelib.providers import AbstractProvider from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from .base import Constraint + if MYPY_CHECK_RUNNING: from typing import ( Any, @@ -41,7 +42,7 @@ class PipProvider(AbstractProvider): def __init__( self, factory, # type: Factory - constraints, # type: Dict[str, SpecifierSet] + constraints, # type: Dict[str, Constraint] ignore_dependencies, # type: bool upgrade_strategy, # type: str user_requested, # type: Set[str] @@ -134,7 +135,7 @@ def find_matches(self, requirements): if not requirements: return [] constraint = self._constraints.get( - requirements[0].name, SpecifierSet(), + requirements[0].name, Constraint.empty(), ) candidates = self._factory.find_candidates(requirements, constraint) return reversed(self._sort_matches(candidates)) diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 031d2f107d6..cb7d1ae8a59 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -14,12 +14,12 @@ from pip._internal.utils.misc import dist_is_editable from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from .base import Constraint from .factory import Factory if MYPY_CHECK_RUNNING: from typing import Dict, List, Optional, Set, Tuple - from pip._vendor.packaging.specifiers import SpecifierSet from pip._vendor.resolvelib.resolvers import Result from pip._vendor.resolvelib.structs import Graph @@ -71,7 +71,7 @@ def __init__( def resolve(self, root_reqs, check_supported_wheels): # type: (List[InstallRequirement], bool) -> RequirementSet - constraints = {} # type: Dict[str, SpecifierSet] + constraints = {} # type: Dict[str, Constraint] user_requested = set() # type: Set[str] requirements = [] for req in root_reqs: @@ -84,9 +84,9 @@ def resolve(self, root_reqs, check_supported_wheels): continue name = canonicalize_name(req.name) if name in constraints: - constraints[name] = constraints[name] & req.specifier + constraints[name] &= req else: - constraints[name] = req.specifier + constraints[name] = Constraint.from_ireq(req) else: if req.user_supplied and req.name: user_requested.add(canonicalize_name(req.name)) diff --git a/tests/functional/test_new_resolver_hashes.py b/tests/functional/test_new_resolver_hashes.py index 130f86b622b..703d4b40069 100644 --- a/tests/functional/test_new_resolver_hashes.py +++ b/tests/functional/test_new_resolver_hashes.py @@ -85,3 +85,41 @@ def test_new_resolver_hash_intersect(script, requirements_template, message): ) assert message in result.stdout, str(result) + + +def test_new_resolver_hash_intersect_from_constraint(script): + find_links = _create_find_links(script) + + constraints_txt = script.scratch_path / "constraints.txt" + constraints_txt.write_text( + "base==0.1.0 --hash=sha256:{sdist_hash}".format( + sdist_hash=find_links.sdist_hash, + ), + ) + requirements_txt = script.scratch_path / "requirements.txt" + requirements_txt.write_text( + """ + base==0.1.0 --hash=sha256:{sdist_hash} --hash=sha256:{wheel_hash} + """.format( + sdist_hash=find_links.sdist_hash, + wheel_hash=find_links.wheel_hash, + ), + ) + + result = script.pip( + "install", + "--use-feature=2020-resolver", + "--no-cache-dir", + "--no-deps", + "--no-index", + "--find-links", find_links.index_html, + "--verbose", + "--constraint", constraints_txt, + "--requirement", requirements_txt, + ) + + message = ( + "Checked 2 links for project 'base' against 1 hashes " + "(1 matches, 0 no digest): discarding 1 non-matches" + ) + assert message in result.stdout, str(result) diff --git a/tests/unit/resolution_resolvelib/test_requirement.py b/tests/unit/resolution_resolvelib/test_requirement.py index 21de3df4a4f..a03edb6f7c2 100644 --- a/tests/unit/resolution_resolvelib/test_requirement.py +++ b/tests/unit/resolution_resolvelib/test_requirement.py @@ -1,8 +1,7 @@ import pytest -from pip._vendor.packaging.specifiers import SpecifierSet from pip._vendor.resolvelib import BaseReporter, Resolver -from pip._internal.resolution.resolvelib.base import Candidate +from pip._internal.resolution.resolvelib.base import Candidate, Constraint from pip._internal.utils.urls import path_to_url # NOTE: All tests are prefixed `test_rlr` (for "test resolvelib resolver"). @@ -59,7 +58,7 @@ def test_new_resolver_correct_number_of_matches(test_cases, factory): """Requirements should return the correct number of candidates""" for spec, _, match_count in test_cases: req = factory.make_requirement_from_spec(spec, comes_from=None) - matches = factory.find_candidates([req], SpecifierSet()) + matches = factory.find_candidates([req], Constraint.empty()) assert len(list(matches)) == match_count @@ -68,7 +67,7 @@ def test_new_resolver_candidates_match_requirement(test_cases, factory): """ for spec, _, _ in test_cases: req = factory.make_requirement_from_spec(spec, comes_from=None) - for c in factory.find_candidates([req], SpecifierSet()): + for c in factory.find_candidates([req], Constraint.empty()): assert isinstance(c, Candidate) assert req.is_satisfied_by(c) From 753ad4d23008c77dada3da91bb0a9486841c9cea Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 3 Sep 2020 17:35:22 +0800 Subject: [PATCH 2484/3170] F Python 2 --- tests/functional/test_new_resolver_hashes.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/functional/test_new_resolver_hashes.py b/tests/functional/test_new_resolver_hashes.py index 703d4b40069..20a700ac8fa 100644 --- a/tests/functional/test_new_resolver_hashes.py +++ b/tests/functional/test_new_resolver_hashes.py @@ -47,7 +47,7 @@ def _create_find_links(script): base==0.1.0 --hash=sha256:{sdist_hash} --hash=sha256:{wheel_hash} base==0.1.0 --hash=sha256:{sdist_hash} --hash=sha256:{wheel_hash} """, - "Checked 2 links for project 'base' against 2 hashes " + "Checked 2 links for project {name!r} against 2 hashes " "(2 matches, 0 no digest): discarding no candidates", ), ( @@ -56,7 +56,7 @@ def _create_find_links(script): base==0.1.0 --hash=sha256:{sdist_hash} --hash=sha256:{wheel_hash} base==0.1.0 --hash=sha256:{sdist_hash} """, - "Checked 2 links for project 'base' against 1 hashes " + "Checked 2 links for project {name!r} against 1 hashes " "(1 matches, 0 no digest): discarding 1 non-matches", ), ], @@ -84,7 +84,7 @@ def test_new_resolver_hash_intersect(script, requirements_template, message): "--requirement", requirements_txt, ) - assert message in result.stdout, str(result) + assert message.format(name=u"base") in result.stdout, str(result) def test_new_resolver_hash_intersect_from_constraint(script): @@ -119,7 +119,7 @@ def test_new_resolver_hash_intersect_from_constraint(script): ) message = ( - "Checked 2 links for project 'base' against 1 hashes " + "Checked 2 links for project {name!r} against 1 hashes " "(1 matches, 0 no digest): discarding 1 non-matches" - ) + ).format(name=u"base") assert message in result.stdout, str(result) From cd549eb7f1f8de518929381de698bfa32c33ba0d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Thu, 23 Jul 2020 16:32:26 +0700 Subject: [PATCH 2485/3170] Use mock to patch for TZ env var --- tests/conftest.py | 11 ++++ tests/unit/test_base_command.py | 92 +++++++++++++-------------------- tests/unit/test_logging.py | 31 +++-------- 3 files changed, 52 insertions(+), 82 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 1ca4d45d00e..bdfe2f9ff18 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -6,6 +6,7 @@ import shutil import subprocess import sys +import time from contextlib import contextmanager import pytest @@ -556,3 +557,13 @@ def mock_server(): test_server = MockServer(server) with test_server.context: yield test_server + + +@pytest.fixture +def utc(): + # time.tzset() is not implemented on some platforms, e.g. Windows. + tzset = getattr(time, 'tzset', lambda: None) + with patch.dict(os.environ, {'TZ': 'UTC'}): + tzset() + yield + tzset() diff --git a/tests/unit/test_base_command.py b/tests/unit/test_base_command.py index 27147e76927..8ba3d9e2526 100644 --- a/tests/unit/test_base_command.py +++ b/tests/unit/test_base_command.py @@ -1,6 +1,5 @@ import logging import os -import time import pytest from mock import Mock, patch @@ -12,6 +11,12 @@ from pip._internal.utils.temp_dir import TempDirectory +@pytest.fixture +def fixed_time(utc): + with patch('time.time', lambda: 1547704837.040001): + yield + + class FakeCommand(Command): _name = 'fake' @@ -91,67 +96,40 @@ def test_handle_pip_version_check_called(mock_handle_version_check): mock_handle_version_check.assert_called_once() -class Test_base_command_logging(object): - """ - Test `pip.base_command.Command` setting up logging consumers based on - options - """ +def test_log_command_success(fixed_time, tmpdir): + """Test the --log option logs when command succeeds.""" + cmd = FakeCommand() + log_path = tmpdir.joinpath('log') + cmd.main(['fake', '--log', log_path]) + with open(log_path) as f: + assert f.read().rstrip() == '2019-01-17T06:00:37,040 fake' - def setup(self): - self.old_time = time.time - time.time = lambda: 1547704837.040001 - self.old_tz = os.environ.get('TZ') - os.environ['TZ'] = 'UTC' - # time.tzset() is not implemented on some platforms (notably, Windows). - if hasattr(time, 'tzset'): - time.tzset() - - def teardown(self): - if self.old_tz: - os.environ['TZ'] = self.old_tz - else: - del os.environ['TZ'] - if 'tzset' in dir(time): - time.tzset() - time.time = self.old_time - def test_log_command_success(self, tmpdir): - """ - Test the --log option logs when command succeeds - """ - cmd = FakeCommand() - log_path = tmpdir.joinpath('log') - cmd.main(['fake', '--log', log_path]) - with open(log_path) as f: - assert f.read().rstrip() == '2019-01-17T06:00:37,040 fake' +def test_log_command_error(fixed_time, tmpdir): + """Test the --log option logs when command fails.""" + cmd = FakeCommand(error=True) + log_path = tmpdir.joinpath('log') + cmd.main(['fake', '--log', log_path]) + with open(log_path) as f: + assert f.read().startswith('2019-01-17T06:00:37,040 fake') - def test_log_command_error(self, tmpdir): - """ - Test the --log option logs when command fails - """ - cmd = FakeCommand(error=True) - log_path = tmpdir.joinpath('log') - cmd.main(['fake', '--log', log_path]) - with open(log_path) as f: - assert f.read().startswith('2019-01-17T06:00:37,040 fake') - def test_log_file_command_error(self, tmpdir): - """ - Test the --log-file option logs (when there's an error). - """ - cmd = FakeCommand(error=True) - log_file_path = tmpdir.joinpath('log_file') - cmd.main(['fake', '--log-file', log_file_path]) - with open(log_file_path) as f: - assert f.read().startswith('2019-01-17T06:00:37,040 fake') +def test_log_file_command_error(fixed_time, tmpdir): + """Test the --log-file option logs (when there's an error).""" + cmd = FakeCommand(error=True) + log_file_path = tmpdir.joinpath('log_file') + cmd.main(['fake', '--log-file', log_file_path]) + with open(log_file_path) as f: + assert f.read().startswith('2019-01-17T06:00:37,040 fake') - def test_unicode_messages(self, tmpdir): - """ - Tests that logging bytestrings and unicode objects don't break logging - """ - cmd = FakeCommandWithUnicode() - log_path = tmpdir.joinpath('log') - cmd.main(['fake_unicode', '--log', log_path]) + +def test_log_unicode_messages(fixed_time, tmpdir): + """Tests that logging bytestrings and unicode objects + don't break logging. + """ + cmd = FakeCommandWithUnicode() + log_path = tmpdir.joinpath('log') + cmd.main(['fake_unicode', '--log', log_path]) @pytest.mark.no_auto_tempdir_manager diff --git a/tests/unit/test_logging.py b/tests/unit/test_logging.py index a62c18c770f..10d47eb6143 100644 --- a/tests/unit/test_logging.py +++ b/tests/unit/test_logging.py @@ -1,7 +1,5 @@ import errno import logging -import os -import time from threading import Thread import pytest @@ -33,24 +31,7 @@ def _make_broken_pipe_error(): class TestIndentingFormatter(object): - """ - Test `pip._internal.utils.logging.IndentingFormatter`. - """ - - def setup(self): - self.old_tz = os.environ.get('TZ') - os.environ['TZ'] = 'UTC' - # time.tzset() is not implemented on some platforms (notably, Windows). - if hasattr(time, 'tzset'): - time.tzset() - - def teardown(self): - if self.old_tz: - os.environ['TZ'] = self.old_tz - else: - del os.environ['TZ'] - if 'tzset' in dir(time): - time.tzset() + """Test ``pip._internal.utils.logging.IndentingFormatter``.""" def make_record(self, msg, level_name): level_number = getattr(logging, level_name) @@ -72,7 +53,7 @@ def make_record(self, msg, level_name): ('ERROR', 'ERROR: hello\nworld'), ('CRITICAL', 'ERROR: hello\nworld'), ]) - def test_format(self, level_name, expected): + def test_format(self, level_name, expected, utc): """ Args: level_name: a logging level name (e.g. "WARNING"). @@ -89,7 +70,7 @@ def test_format(self, level_name, expected): '2019-01-17T06:00:37,040 WARNING: hello\n' '2019-01-17T06:00:37,040 world'), ]) - def test_format_with_timestamp(self, level_name, expected): + def test_format_with_timestamp(self, level_name, expected, utc): record = self.make_record('hello\nworld', level_name=level_name) f = IndentingFormatter(fmt="%(message)s", add_timestamp=True) assert f.format(record) == expected @@ -99,7 +80,7 @@ def test_format_with_timestamp(self, level_name, expected): ('ERROR', 'DEPRECATION: hello\nworld'), ('CRITICAL', 'DEPRECATION: hello\nworld'), ]) - def test_format_deprecated(self, level_name, expected): + def test_format_deprecated(self, level_name, expected, utc): """ Test that logged deprecation warnings coming from deprecated() don't get another prefix. @@ -110,7 +91,7 @@ def test_format_deprecated(self, level_name, expected): f = IndentingFormatter(fmt="%(message)s") assert f.format(record) == expected - def test_thread_safety_base(self): + def test_thread_safety_base(self, utc): record = self.make_record( 'DEPRECATION: hello\nworld', level_name='WARNING', ) @@ -126,7 +107,7 @@ def thread_function(): thread.join() assert results[0] == results[1] - def test_thread_safety_indent_log(self): + def test_thread_safety_indent_log(self, utc): record = self.make_record( 'DEPRECATION: hello\nworld', level_name='WARNING', ) From 4a2e03c4ff293e6295b854819ab574873778f94d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Thu, 23 Jul 2020 16:32:56 +0700 Subject: [PATCH 2486/3170] Use monkeypatch for env var in wheel unit tests --- tests/conftest.py | 2 +- tests/unit/test_wheel.py | 12 +++++------- 2 files changed, 6 insertions(+), 8 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index bdfe2f9ff18..81bd553536b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -111,7 +111,7 @@ def use_new_resolver(request): features.add("2020-resolver") else: features.discard("2020-resolver") - with patch.dict(os.environ, {"PIP_USE_FEATURE", " ".join(features)}): + with patch.dict(os.environ, {"PIP_USE_FEATURE": " ".join(features)}): yield new_resolver diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 15dff94ca16..6c9fe02b32e 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -600,16 +600,14 @@ def test_trailing_ossep_removal(self): ) assert retval is None - def test_missing_PATH_env_treated_as_empty_PATH_env(self): + def test_missing_PATH_env_treated_as_empty_PATH_env(self, monkeypatch): scripts = ['a/b/foo'] - env = os.environ.copy() - del env['PATH'] - with patch.dict('os.environ', env, clear=True): - retval_missing = wheel.message_about_scripts_not_on_PATH(scripts) + monkeypatch.delenv('PATH') + retval_missing = wheel.message_about_scripts_not_on_PATH(scripts) - with patch.dict('os.environ', {'PATH': ''}): - retval_empty = wheel.message_about_scripts_not_on_PATH(scripts) + monkeypatch.setenv('PATH', '') + retval_empty = wheel.message_about_scripts_not_on_PATH(scripts) assert retval_missing == retval_empty From cca500f05352b1e599f44f7fef955d8b465337f5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Fri, 4 Sep 2020 17:37:43 +0700 Subject: [PATCH 2487/3170] Remove unused definitions --- news/6f8eff8d-9886-4e00-b431-5c809500e6bf.trivial | 0 src/pip/_internal/commands/list.py | 1 - src/pip/_internal/resolution/resolvelib/factory.py | 4 ---- src/pip/_internal/vcs/git.py | 1 - 4 files changed, 6 deletions(-) create mode 100644 news/6f8eff8d-9886-4e00-b431-5c809500e6bf.trivial diff --git a/news/6f8eff8d-9886-4e00-b431-5c809500e6bf.trivial b/news/6f8eff8d-9886-4e00-b431-5c809500e6bf.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index 20e9bff2b71..7c18a7d9f62 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -201,7 +201,6 @@ def iter_packages_latest_infos(self, packages, options): def latest_info(dist): # type: (Distribution) -> Distribution - typ = 'unknown' all_candidates = finder.find_all_candidates(dist.key) if not options.pre: # Remove prereleases diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index e5630494956..a0ec3d433f7 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -400,10 +400,6 @@ def text_join(parts): return ", ".join(parts[:-1]) + " and " + parts[-1] - def readable_form(cand): - # type: (Candidate) -> str - return "{} {}".format(cand.name, cand.version) - def describe_trigger(parent): # type: (Candidate) -> str ireq = parent.get_install_requirement() diff --git a/src/pip/_internal/vcs/git.py b/src/pip/_internal/vcs/git.py index 308a87dfa48..db8c7234984 100644 --- a/src/pip/_internal/vcs/git.py +++ b/src/pip/_internal/vcs/git.py @@ -386,7 +386,6 @@ def get_url_rev_and_auth(cls, url): urllib_request.url2pathname(path) .replace('\\', '/').lstrip('/') ) - url = urlunsplit((scheme, netloc, newpath, query, fragment)) after_plus = scheme.find('+') + 1 url = scheme[:after_plus] + urlunsplit( (scheme[after_plus:], netloc, newpath, query, fragment), From 1b0517e68b872a6ba5171261198c0e9d9bed58c4 Mon Sep 17 00:00:00 2001 From: Srinivas Nyayapati <shireenrao@gmail.com> Date: Mon, 31 Aug 2020 11:27:34 -0400 Subject: [PATCH 2488/3170] Add documentation for netrc support --- docs/html/user_guide.rst | 27 +++++++++++++++++++++++++++ news/7231.doc | 1 + 2 files changed, 28 insertions(+) create mode 100644 news/7231.doc diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index b92e8b0a34d..61dfaf0d53e 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -79,6 +79,31 @@ as the "username" and do not provide a password, for example - ``https://0123456789abcdef@pypi.company.com`` +netrc Support +------------- + +If no credentials are part of the URL, pip will attempt to get authentication credentials +for the URL’s hostname from the user’s .netrc file. This behaviour comes from the underlying +use of `requests`_ which in turn delegates it to the `Python standard library`_. + +The .netrc file contains login and initialization information used by the auto-login process. +It resides in the user's home directory. The .netrc file format is simple. You specify lines +with a machine name and follow that with lines for the login and password that are +associated with that machine. Machine name is the hostname in your URL. + +An example .netrc for the host example.com with a user named 'daniel', using the password +'qwerty' would look like: + +.. code-block:: shell + + machine example.com + login daniel + password qwerty + +As mentioned in the `standard library docs <https://docs.python.org/3/library/netrc.html>`_, +whitespace and non-printable characters are not allowed in passwords. + + Keyring Support --------------- @@ -1296,3 +1321,5 @@ announcements on the `low-traffic packaging announcements list`_ and .. _low-traffic packaging announcements list: https://mail.python.org/mailman3/lists/pypi-announce.python.org/ .. _our survey on upgrades that create conflicts: https://docs.google.com/forms/d/e/1FAIpQLSeBkbhuIlSofXqCyhi3kGkLmtrpPOEBwr6iJA6SzHdxWKfqdA/viewform .. _the official Python blog: https://blog.python.org/ +.. _requests: https://requests.readthedocs.io/en/master/user/authentication/#netrc-authentication +.. _Python standard library: https://docs.python.org/3/library/netrc.html diff --git a/news/7231.doc b/news/7231.doc new file mode 100644 index 00000000000..bef9bf3e6eb --- /dev/null +++ b/news/7231.doc @@ -0,0 +1 @@ +Add documentation for '.netrc' support. From 8e622390d06cafb4d179152ed19d23d81aed45f4 Mon Sep 17 00:00:00 2001 From: Srinivas Nyayapati <shireenrao@gmail.com> Date: Mon, 31 Aug 2020 11:43:26 -0400 Subject: [PATCH 2489/3170] mention using only ASCII for password --- docs/html/user_guide.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 61dfaf0d53e..3ef99e01b0e 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -101,7 +101,7 @@ An example .netrc for the host example.com with a user named 'daniel', using the password qwerty As mentioned in the `standard library docs <https://docs.python.org/3/library/netrc.html>`_, -whitespace and non-printable characters are not allowed in passwords. +only ASCII characters are allowed. Whitespace and non-printable characters are not allowed in passwords. Keyring Support From 7d9be739fe05c7dee15e2c14f2809fd0b5c0eae5 Mon Sep 17 00:00:00 2001 From: Daan De Meyer <daan.j.demeyer@gmail.com> Date: Sat, 8 Aug 2020 16:24:24 +0100 Subject: [PATCH 2490/3170] Fix uninstallation of user scripts User scripts are installed to ~/.local/bin. pip was looking for scripts to uninstall in ~/.local/lib/python3.8/site-packages/bin. This commit makes it look in ~/.local/bin instead. --- news/8733.bugfix | 1 + src/pip/_internal/locations.py | 17 ++++++++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) create mode 100644 news/8733.bugfix diff --git a/news/8733.bugfix b/news/8733.bugfix new file mode 100644 index 00000000000..b07ed95698b --- /dev/null +++ b/news/8733.bugfix @@ -0,0 +1 @@ +Correctly uninstall scripts installed with --user diff --git a/src/pip/_internal/locations.py b/src/pip/_internal/locations.py index 0c1235488d6..21a9af60df2 100644 --- a/src/pip/_internal/locations.py +++ b/src/pip/_internal/locations.py @@ -75,16 +75,27 @@ def get_src_prefix(): except AttributeError: user_site = site.USER_SITE + +def _get_bin_user(): + # type: (...) -> str + scheme = "{}_user".format(os.name) + if scheme not in sysconfig.get_scheme_names(): + scheme = "posix_user" # Default to POSIX for unknown platforms. + path = sysconfig.get_path("scripts", scheme=scheme) + assert path is not None + return path + + +bin_user = _get_bin_user() + if WINDOWS: bin_py = os.path.join(sys.prefix, 'Scripts') - bin_user = os.path.join(user_site, 'Scripts') # buildout uses 'bin' on Windows too? if not os.path.exists(bin_py): bin_py = os.path.join(sys.prefix, 'bin') - bin_user = os.path.join(user_site, 'bin') + bin_user = os.path.join(os.path.dirname(bin_user), 'bin') else: bin_py = os.path.join(sys.prefix, 'bin') - bin_user = os.path.join(user_site, 'bin') # Forcing to use /usr/local/bin for standard macOS framework installs # Also log to ~/Library/Logs/ for use with the Console.app log viewer From 65bddac52f99afd2c1f4c0964394f7c91b88756f Mon Sep 17 00:00:00 2001 From: Emmanuel Arias <eamanu@yaerobi.com> Date: Tue, 18 Aug 2020 17:14:23 -0300 Subject: [PATCH 2491/3170] Update Caching documentation Any headers from serve are ignored. So, this PR remove the paragraph that mention that. See #8009 and #5670 Close #8009 --- docs/html/reference/pip_install.rst | 5 ----- 1 file changed, 5 deletions(-) diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index 7be7bcd256b..fd962cd3658 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -550,11 +550,6 @@ response telling pip to simply use the cached item (and refresh the expiration timer) or it will return a whole new response which pip can then store in the cache. -When storing items in the cache, pip will respect the ``CacheControl`` header -if it exists, or it will fall back to the ``Expires`` header if that exists. -This allows pip to function as a browser would, and allows the index server -to communicate to pip how long it is reasonable to cache any particular item. - While this cache attempts to minimize network activity, it does not prevent network access altogether. If you want a local install solution that circumvents accessing PyPI, see :ref:`Installing from local packages`. From e046092068018c186195485092ee4e8911554d27 Mon Sep 17 00:00:00 2001 From: Emmanuel Arias <eamanu@yaerobi.com> Date: Tue, 18 Aug 2020 18:08:07 -0300 Subject: [PATCH 2492/3170] Add trivial news --- news/5e8c60c2-d540-4a25-af03-100d848acbc0.trivial | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 news/5e8c60c2-d540-4a25-af03-100d848acbc0.trivial diff --git a/news/5e8c60c2-d540-4a25-af03-100d848acbc0.trivial b/news/5e8c60c2-d540-4a25-af03-100d848acbc0.trivial new file mode 100644 index 00000000000..e69de29bb2d From b98dd2d07d0ff3188dfed1ebfec6fe13605e3170 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Date: Tue, 25 Aug 2020 20:40:01 +0530 Subject: [PATCH 2493/3170] Merge pull request #8778 from hugovk/deprecate-3.5 Deprecate support for Python 3.5 --- news/8181.removal | 1 + src/pip/_internal/cli/base_command.py | 14 +++++++++++++- tests/conftest.py | 4 ++-- 3 files changed, 16 insertions(+), 3 deletions(-) create mode 100644 news/8181.removal diff --git a/news/8181.removal b/news/8181.removal new file mode 100644 index 00000000000..ae6bbe9f88a --- /dev/null +++ b/news/8181.removal @@ -0,0 +1 @@ +Deprecate support for Python 3.5 diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index c3b6a856be4..197400a72c5 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -158,7 +158,19 @@ def _main(self, args): "1st, 2020. Please upgrade your Python as Python 2.7 " "is no longer maintained. " ) + message - deprecated(message, replacement=None, gone_in=None) + deprecated(message, replacement=None, gone_in="21.0") + + if ( + sys.version_info[:2] == (3, 5) and + not options.no_python_version_warning + ): + message = ( + "Python 3.5 reached the end of its life on September " + "13th, 2020. Please upgrade your Python as Python 3.5 " + "is no longer maintained. pip 21.0 will drop support " + "for Python 3.5 in January 2021." + ) + deprecated(message, replacement=None, gone_in="21.0") # TODO: Try to get these passing down from the command? # without resorting to os.environ to hold these. diff --git a/tests/conftest.py b/tests/conftest.py index 2aab50207be..32b6e692610 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -470,8 +470,8 @@ def in_memory_pip(): @pytest.fixture(scope="session") def deprecated_python(): - """Used to indicate whether pip deprecated this python version""" - return sys.version_info[:2] in [(2, 7)] + """Used to indicate whether pip deprecated this Python version""" + return sys.version_info[:2] in [(2, 7), (3, 5)] @pytest.fixture(scope="session") From d8f0a7b6936ec04b549e9ca5ad797a84f1a87146 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Date: Tue, 25 Aug 2020 20:40:27 +0530 Subject: [PATCH 2494/3170] Merge pull request #8752 from sbidoul/imp-8369-deprecation-sbi --- news/8752.feature | 3 +++ src/pip/_internal/commands/install.py | 21 +++------------------ src/pip/_internal/req/req_install.py | 13 +++++++++++++ 3 files changed, 19 insertions(+), 18 deletions(-) create mode 100644 news/8752.feature diff --git a/news/8752.feature b/news/8752.feature new file mode 100644 index 00000000000..d2560da1803 --- /dev/null +++ b/news/8752.feature @@ -0,0 +1,3 @@ +Make the ``setup.py install`` deprecation warning less noisy. We warn only +when ``setup.py install`` succeeded and ``setup.py bdist_wheel`` failed, as +situations where both fails are most probably irrelevant to this deprecation. diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 8c2c32fd43f..704e2d65666 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -22,7 +22,6 @@ from pip._internal.req import install_given_reqs from pip._internal.req.req_tracker import get_requirement_tracker from pip._internal.utils.datetime import today_is_later_than -from pip._internal.utils.deprecation import deprecated from pip._internal.utils.distutils_args import parse_distutils_args from pip._internal.utils.filesystem import test_writable_dir from pip._internal.utils.misc import ( @@ -372,23 +371,9 @@ def run(self, options, args): # For now, we just warn about failures building legacy # requirements, as we'll fall through to a direct # install for those. - legacy_build_failure_names = [ - r.name # type: ignore - for r in build_failures if not r.use_pep517 - ] # type: List[str] - if legacy_build_failure_names: - deprecated( - reason=( - "Could not build wheels for {} which do not use " - "PEP 517. pip will fall back to legacy 'setup.py " - "install' for these.".format( - ", ".join(legacy_build_failure_names) - ) - ), - replacement="to fix the wheel build issue reported above", - gone_in="21.0", - issue=8368, - ) + for r in build_failures: + if not r.use_pep517: + r.legacy_install_reason = 8368 to_install = resolver.get_installation_order( requirement_set diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 4759f4af6f0..f25cec96ae2 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -121,6 +121,7 @@ def __init__( self.comes_from = comes_from self.constraint = constraint self.editable = editable + self.legacy_install_reason = None # type: Optional[int] # source_dir is the local directory where the linked requirement is # located, or unpacked. In case unpacking is needed, creating and @@ -859,6 +860,18 @@ def install( self.install_succeeded = success + if success and self.legacy_install_reason == 8368: + deprecated( + reason=( + "{} was installed using the legacy 'setup.py install' " + "method, because a wheel could not be built for it.". + format(self.name) + ), + replacement="to fix the wheel build issue reported above", + gone_in="21.0", + issue=8368, + ) + def check_invalid_constraint_type(req): # type: (InstallRequirement) -> str From 8c2102a310372dea70f12e9581b334783441bcea Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Tue, 8 Sep 2020 16:43:34 +0530 Subject: [PATCH 2495/3170] Bump for release --- NEWS.rst | 16 ++++++++++++++++ news/8181.removal | 1 - news/8752.feature | 3 --- src/pip/__init__.py | 2 +- 4 files changed, 17 insertions(+), 5 deletions(-) delete mode 100644 news/8181.removal delete mode 100644 news/8752.feature diff --git a/NEWS.rst b/NEWS.rst index a0376a06f07..6f7c2cd232f 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -9,6 +9,22 @@ .. towncrier release notes start +20.2.3 (2020-09-08) +=================== + +Deprecations and Removals +------------------------- + +- Deprecate support for Python 3.5 (`#8181 <https://github.com/pypa/pip/issues/8181>`_) + +Features +-------- + +- Make the ``setup.py install`` deprecation warning less noisy. We warn only + when ``setup.py install`` succeeded and ``setup.py bdist_wheel`` failed, as + situations where both fails are most probably irrelevant to this deprecation. (`#8752 <https://github.com/pypa/pip/issues/8752>`_) + + 20.2.2 (2020-08-11) =================== diff --git a/news/8181.removal b/news/8181.removal deleted file mode 100644 index ae6bbe9f88a..00000000000 --- a/news/8181.removal +++ /dev/null @@ -1 +0,0 @@ -Deprecate support for Python 3.5 diff --git a/news/8752.feature b/news/8752.feature deleted file mode 100644 index d2560da1803..00000000000 --- a/news/8752.feature +++ /dev/null @@ -1,3 +0,0 @@ -Make the ``setup.py install`` deprecation warning less noisy. We warn only -when ``setup.py install`` succeeded and ``setup.py bdist_wheel`` failed, as -situations where both fails are most probably irrelevant to this deprecation. diff --git a/src/pip/__init__.py b/src/pip/__init__.py index 611753fedec..9fb68d40354 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -4,7 +4,7 @@ from typing import List, Optional -__version__ = "20.2.2" +__version__ = "20.2.3" def main(args=None): From e3015eb3a51b84a63d946784e8a0b03283106df5 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Tue, 8 Sep 2020 16:43:35 +0530 Subject: [PATCH 2496/3170] Bump for development --- src/pip/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/__init__.py b/src/pip/__init__.py index 9fb68d40354..5a2f3c31745 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -4,7 +4,7 @@ from typing import List, Optional -__version__ = "20.2.3" +__version__ = "20.3.dev0" def main(args=None): From c07ef581e729ea26110afeed5f084d3162a87f1f Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Thu, 10 Sep 2020 17:15:32 +0530 Subject: [PATCH 2497/3170] Factor out logger.into into a single call This makes it easier to conditionally print this information. --- src/pip/_internal/operations/prepare.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 5eb71ce073f..95ffec562dc 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -373,12 +373,15 @@ def _download_should_save(self): def _log_preparing_link(self, req): # type: (InstallRequirement) -> None - """Log the way the link prepared.""" + """Provide context for the requirement being prepared.""" if req.link.is_file: - path = req.link.file_path - logger.info('Processing %s', display_path(path)) + message = "Processing %s" + information = str(display_path(req.link.file_path)) else: - logger.info('Collecting %s', req.req or req) + message = "Collecting %s" + information = str(req.req or req) + + logger.info(message, information) def _get_download_dir(self, link): # type: (Link) -> Optional[str] From 963e390abe3c7ac00c90a189b3557981e92f5dde Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Thu, 10 Sep 2020 17:21:04 +0530 Subject: [PATCH 2498/3170] Improve how cached wheels are presented This is specifically for the case of look ups done in the new resolver. --- src/pip/_internal/operations/prepare.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 95ffec562dc..756adfa43e5 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -374,7 +374,7 @@ def _download_should_save(self): def _log_preparing_link(self, req): # type: (InstallRequirement) -> None """Provide context for the requirement being prepared.""" - if req.link.is_file: + if req.link.is_file and not req.original_link_is_in_wheel_cache: message = "Processing %s" information = str(display_path(req.link.file_path)) else: @@ -383,6 +383,10 @@ def _log_preparing_link(self, req): logger.info(message, information) + if req.original_link_is_in_wheel_cache: + with indent_log(): + logger.info("Using cached %s", req.link.filename) + def _get_download_dir(self, link): # type: (Link) -> Optional[str] if link.is_wheel and self.wheel_download_dir: From 3d32960c80ffed7946d84c636318153619fa5bf6 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Thu, 10 Sep 2020 17:21:42 +0530 Subject: [PATCH 2499/3170] Only Print "Collecting ..." when the requirement changes --- src/pip/_internal/operations/prepare.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 756adfa43e5..6f899e6ff12 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -357,6 +357,9 @@ def __init__( # Memoized downloaded files, as mapping of url: (path, mime type) self._downloaded = {} # type: Dict[str, Tuple[str, str]] + # Previous "header" printed for a link-based InstallRequirement + self._previous_requirement_header = None + @property def _download_should_save(self): # type: () -> bool @@ -381,7 +384,9 @@ def _log_preparing_link(self, req): message = "Collecting %s" information = str(req.req or req) - logger.info(message, information) + if (message, information) != self._previous_requirement_header: + self._previous_requirement_header = (message, information) + logger.info(message, information) if req.original_link_is_in_wheel_cache: with indent_log(): From cc472fd54ae4cc860b014fcb23378ba73a538a3d Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Thu, 10 Sep 2020 17:32:58 +0530 Subject: [PATCH 2500/3170] Use a symmetric type and make mypy happy --- src/pip/_internal/operations/prepare.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 6f899e6ff12..bf89091aa94 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -358,7 +358,7 @@ def __init__( self._downloaded = {} # type: Dict[str, Tuple[str, str]] # Previous "header" printed for a link-based InstallRequirement - self._previous_requirement_header = None + self._previous_requirement_header = ("", "") @property def _download_should_save(self): From 0ecbce24161aa26bdfaa9e7d4e3998fa66173424 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Date: Thu, 10 Sep 2020 17:53:48 +0530 Subject: [PATCH 2501/3170] Simplify type annotation Co-authored-by: Tzu-ping Chung <uranusjr@gmail.com> --- src/pip/_internal/locations.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/locations.py b/src/pip/_internal/locations.py index 21a9af60df2..a5426c36163 100644 --- a/src/pip/_internal/locations.py +++ b/src/pip/_internal/locations.py @@ -77,7 +77,7 @@ def get_src_prefix(): def _get_bin_user(): - # type: (...) -> str + # type: () -> str scheme = "{}_user".format(os.name) if scheme not in sysconfig.get_scheme_names(): scheme = "posix_user" # Default to POSIX for unknown platforms. From 0b3ba87bbfb46cd12778b2ca5b9f6e74be0b770a Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Fri, 3 Jul 2020 19:55:35 +0530 Subject: [PATCH 2502/3170] Add option to output full path of cache enty --- src/pip/_internal/commands/cache.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index 747277f6eaa..31e196ee3b0 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -37,11 +37,21 @@ class CacheCommand(Command): usage = """ %prog dir %prog info - %prog list [<pattern>] + %prog list [<pattern>] [--abspath] %prog remove <pattern> %prog purge """ + def add_options(self): + # type: () -> None + self.cmd_opts.add_option( + '--abspath', + dest='abspath', + action='store_true', + help='List the absolute path of wheels') + + self.parser.insert_option_group(0, self.cmd_opts) + def run(self, options, args): # type: (Values, List[Any]) -> int handlers = { @@ -118,15 +128,20 @@ def list_cache_items(self, options, args): files = self._find_wheels(options, pattern) if not files: - logger.info('Nothing cached.') + if not options.abspath: + logger.info('Nothing cached.') return results = [] for filename in files: wheel = os.path.basename(filename) size = filesystem.format_file_size(filename) - results.append(' - {} ({})'.format(wheel, size)) - logger.info('Cache contents:\n') + if options.abspath: + results.append(filename) + else: + results.append(' - {} ({})'.format(wheel, size)) + if not options.abspath: + logger.info('Cache contents:\n') logger.info('\n'.join(sorted(results))) def remove_cache_items(self, options, args): From d22becc073c4717a6f57ec56b9acd5f46e168cd3 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sat, 18 Jul 2020 14:06:44 +0530 Subject: [PATCH 2503/3170] Add unit tests for --abspath flag --- tests/functional/test_cache.py | 57 ++++++++++++++++++++++++++++++++++ 1 file changed, 57 insertions(+) diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index e30b2c07987..be165661ae5 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -70,6 +70,19 @@ def list_matches_wheel(wheel_name, result): return any(map(lambda l: l.startswith(expected), lines)) +def list_matches_wheel_abspath(wheel_name, result): + """Returns True if any line in `result`, which should be the output of + a `pip cache list --abspath` call, is a valid path and belongs to + `wheel_name`. + + E.g., If wheel_name is `foo-1.2.3` it searches for a line starting with + `foo-1.2.3-py3-none-any.whl`.""" + lines = result.stdout.splitlines() + expected = '{}-py3-none-any.whl'.format(wheel_name) + return any(map(lambda l: os.path.basename(l).startswith(expected) + and os.path.exists(l), lines)) + + @pytest.fixture def remove_matches_wheel(wheel_cache_dir): """Returns True if any line in `result`, which should be the output of @@ -132,6 +145,18 @@ def test_cache_list(script): assert list_matches_wheel('zzz-7.8.9', result) +@pytest.mark.usefixtures("populate_wheel_cache") +def test_cache_list_abspath(script): + """Running `pip cache list --abspath` should return full + paths of exactly what the populate_wheel_cache fixture adds.""" + result = script.pip('cache', 'list', '--abspath') + + assert list_matches_wheel_abspath('yyy-1.2.3', result) + assert list_matches_wheel_abspath('zzz-4.5.6', result) + assert list_matches_wheel_abspath('zzz-4.5.7', result) + assert list_matches_wheel_abspath('zzz-7.8.9', result) + + @pytest.mark.usefixtures("empty_wheel_cache") def test_cache_list_with_empty_cache(script): """Running `pip cache list` with an empty cache should print @@ -140,6 +165,14 @@ def test_cache_list_with_empty_cache(script): assert result.stdout == "Nothing cached.\n" +@pytest.mark.usefixtures("empty_wheel_cache") +def test_cache_list_with_empty_cache_abspath(script): + """Running `pip cache list --abspath` with an empty cache should not + print anything and exit.""" + result = script.pip('cache', 'list', '--abspath') + assert result.stdout.strip() == "" + + def test_cache_list_too_many_args(script): """Passing `pip cache list` too many arguments should cause an error.""" script.pip('cache', 'list', 'aaa', 'bbb', @@ -158,6 +191,18 @@ def test_cache_list_name_match(script): assert list_matches_wheel('zzz-7.8.9', result) +@pytest.mark.usefixtures("populate_wheel_cache") +def test_cache_list_name_match_abspath(script): + """Running `pip cache list zzz --abspath` should list paths of + zzz-4.5.6, zzz-4.5.7, zzz-7.8.9, but nothing else.""" + result = script.pip('cache', 'list', 'zzz', '--abspath', '--verbose') + + assert not list_matches_wheel_abspath('yyy-1.2.3', result) + assert list_matches_wheel_abspath('zzz-4.5.6', result) + assert list_matches_wheel_abspath('zzz-4.5.7', result) + assert list_matches_wheel_abspath('zzz-7.8.9', result) + + @pytest.mark.usefixtures("populate_wheel_cache") def test_cache_list_name_and_version_match(script): """Running `pip cache list zzz-4.5.6` should list zzz-4.5.6, but @@ -170,6 +215,18 @@ def test_cache_list_name_and_version_match(script): assert not list_matches_wheel('zzz-7.8.9', result) +@pytest.mark.usefixtures("populate_wheel_cache") +def test_cache_list_name_and_version_match_abspath(script): + """Running `pip cache list zzz-4.5.6 --abspath` should list path of + zzz-4.5.6, but nothing else.""" + result = script.pip('cache', 'list', 'zzz-4.5.6', '--abspath', '--verbose') + + assert not list_matches_wheel_abspath('yyy-1.2.3', result) + assert list_matches_wheel_abspath('zzz-4.5.6', result) + assert not list_matches_wheel_abspath('zzz-4.5.7', result) + assert not list_matches_wheel_abspath('zzz-7.8.9', result) + + @pytest.mark.usefixtures("populate_wheel_cache") def test_cache_remove_no_arguments(script): """Running `pip cache remove` with no arguments should cause an error.""" From b76a4c0153c3f5d49a1324a97429404853da9fe8 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Sat, 18 Jul 2020 14:10:06 +0530 Subject: [PATCH 2504/3170] Add feature news entry --- news/8355.feature | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/8355.feature diff --git a/news/8355.feature b/news/8355.feature new file mode 100644 index 00000000000..3a7fb537509 --- /dev/null +++ b/news/8355.feature @@ -0,0 +1 @@ +Add option ``--format`` to subcommand ``list`` of ``pip cache``, with ``abspath`` choice to output the full path of a wheel file. From 9450f8837af334b7daa798c40623a388db9b96e6 Mon Sep 17 00:00:00 2001 From: Devesh Kumar Singh <deveshkusingh@gmail.com> Date: Fri, 31 Jul 2020 02:54:58 +0530 Subject: [PATCH 2505/3170] Use format options for abspath --- src/pip/_internal/commands/cache.py | 42 ++++++++++++++++++++--------- tests/functional/test_cache.py | 20 +++++++------- 2 files changed, 40 insertions(+), 22 deletions(-) diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index 31e196ee3b0..b9d3ed410bc 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -37,18 +37,22 @@ class CacheCommand(Command): usage = """ %prog dir %prog info - %prog list [<pattern>] [--abspath] + %prog list [<pattern>] [--format=[human, abspath]] %prog remove <pattern> %prog purge """ def add_options(self): # type: () -> None + self.cmd_opts.add_option( - '--abspath', - dest='abspath', - action='store_true', - help='List the absolute path of wheels') + '--format', + action='store', + dest='list_format', + default="human", + choices=('human', 'abspath'), + help="Select the output format among: human (default) or abspath" + ) self.parser.insert_option_group(0, self.cmd_opts) @@ -126,22 +130,34 @@ def list_cache_items(self, options, args): pattern = '*' files = self._find_wheels(options, pattern) + if options.list_format == 'human': + self.format_for_human(files) + else: + self.format_for_abspath(files) + def format_for_human(self, files): + # type: (List[str]) -> None if not files: - if not options.abspath: - logger.info('Nothing cached.') + logger.info('Nothing cached.') return results = [] for filename in files: wheel = os.path.basename(filename) size = filesystem.format_file_size(filename) - if options.abspath: - results.append(filename) - else: - results.append(' - {} ({})'.format(wheel, size)) - if not options.abspath: - logger.info('Cache contents:\n') + results.append(' - {} ({})'.format(wheel, size)) + logger.info('Cache contents:\n') + logger.info('\n'.join(sorted(results))) + + def format_for_abspath(self, files): + # type: (List[str]) -> None + if not files: + return + + results = [] + for filename in files: + results.append(filename) + logger.info('\n'.join(sorted(results))) def remove_cache_items(self, options, args): diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index be165661ae5..603e11b5b0e 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -72,7 +72,7 @@ def list_matches_wheel(wheel_name, result): def list_matches_wheel_abspath(wheel_name, result): """Returns True if any line in `result`, which should be the output of - a `pip cache list --abspath` call, is a valid path and belongs to + a `pip cache list --format=abspath` call, is a valid path and belongs to `wheel_name`. E.g., If wheel_name is `foo-1.2.3` it searches for a line starting with @@ -147,9 +147,9 @@ def test_cache_list(script): @pytest.mark.usefixtures("populate_wheel_cache") def test_cache_list_abspath(script): - """Running `pip cache list --abspath` should return full + """Running `pip cache list --format=abspath` should return full paths of exactly what the populate_wheel_cache fixture adds.""" - result = script.pip('cache', 'list', '--abspath') + result = script.pip('cache', 'list', '--format=abspath') assert list_matches_wheel_abspath('yyy-1.2.3', result) assert list_matches_wheel_abspath('zzz-4.5.6', result) @@ -167,9 +167,9 @@ def test_cache_list_with_empty_cache(script): @pytest.mark.usefixtures("empty_wheel_cache") def test_cache_list_with_empty_cache_abspath(script): - """Running `pip cache list --abspath` with an empty cache should not + """Running `pip cache list --format=abspath` with an empty cache should not print anything and exit.""" - result = script.pip('cache', 'list', '--abspath') + result = script.pip('cache', 'list', '--format=abspath') assert result.stdout.strip() == "" @@ -193,9 +193,10 @@ def test_cache_list_name_match(script): @pytest.mark.usefixtures("populate_wheel_cache") def test_cache_list_name_match_abspath(script): - """Running `pip cache list zzz --abspath` should list paths of + """Running `pip cache list zzz --format=abspath` should list paths of zzz-4.5.6, zzz-4.5.7, zzz-7.8.9, but nothing else.""" - result = script.pip('cache', 'list', 'zzz', '--abspath', '--verbose') + result = script.pip('cache', 'list', 'zzz', '--format=abspath', + '--verbose') assert not list_matches_wheel_abspath('yyy-1.2.3', result) assert list_matches_wheel_abspath('zzz-4.5.6', result) @@ -217,9 +218,10 @@ def test_cache_list_name_and_version_match(script): @pytest.mark.usefixtures("populate_wheel_cache") def test_cache_list_name_and_version_match_abspath(script): - """Running `pip cache list zzz-4.5.6 --abspath` should list path of + """Running `pip cache list zzz-4.5.6 --format=abspath` should list path of zzz-4.5.6, but nothing else.""" - result = script.pip('cache', 'list', 'zzz-4.5.6', '--abspath', '--verbose') + result = script.pip('cache', 'list', 'zzz-4.5.6', '--format=abspath', + '--verbose') assert not list_matches_wheel_abspath('yyy-1.2.3', result) assert list_matches_wheel_abspath('zzz-4.5.6', result) From 179ccbe6f5a28a02361cf5d1eef10de7c943d5bb Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 11 Sep 2020 15:01:50 +0800 Subject: [PATCH 2506/3170] Add tests for empty hash intersection --- tests/functional/test_new_resolver_hashes.py | 88 ++++++++++++++++++++ 1 file changed, 88 insertions(+) diff --git a/tests/functional/test_new_resolver_hashes.py b/tests/functional/test_new_resolver_hashes.py index 20a700ac8fa..4b13ebc307d 100644 --- a/tests/functional/test_new_resolver_hashes.py +++ b/tests/functional/test_new_resolver_hashes.py @@ -123,3 +123,91 @@ def test_new_resolver_hash_intersect_from_constraint(script): "(1 matches, 0 no digest): discarding 1 non-matches" ).format(name=u"base") assert message in result.stdout, str(result) + + +@pytest.mark.parametrize( + "requirements_template, constraints_template", + [ + ( + """ + base==0.1.0 --hash=sha256:{sdist_hash} + base==0.1.0 --hash=sha256:{wheel_hash} + """, + "", + ), + ( + "base==0.1.0 --hash=sha256:{sdist_hash}", + "base==0.1.0 --hash=sha256:{wheel_hash}", + ), + ], + ids=["both-requirements", "one-each"], +) +def test_new_resolver_hash_intersect_empty( + script, requirements_template, constraints_template, +): + find_links = _create_find_links(script) + + constraints_txt = script.scratch_path / "constraints.txt" + constraints_txt.write_text( + constraints_template.format( + sdist_hash=find_links.sdist_hash, + wheel_hash=find_links.wheel_hash, + ), + ) + + requirements_txt = script.scratch_path / "requirements.txt" + requirements_txt.write_text( + requirements_template.format( + sdist_hash=find_links.sdist_hash, + wheel_hash=find_links.wheel_hash, + ), + ) + + result = script.pip( + "install", + "--use-feature=2020-resolver", + "--no-cache-dir", + "--no-deps", + "--no-index", + "--find-links", find_links.index_html, + "--constraint", constraints_txt, + "--requirement", requirements_txt, + expect_error=True, + ) + + assert ( + "THESE PACKAGES DO NOT MATCH THE HASHES FROM THE REQUIREMENTS FILE." + ) in result.stderr, str(result) + + +def test_new_resolver_hash_intersect_empty_from_constraint(script): + find_links = _create_find_links(script) + + constraints_txt = script.scratch_path / "constraints.txt" + constraints_txt.write_text( + """ + base==0.1.0 --hash=sha256:{sdist_hash} + base==0.1.0 --hash=sha256:{wheel_hash} + """.format( + sdist_hash=find_links.sdist_hash, + wheel_hash=find_links.wheel_hash, + ), + ) + + result = script.pip( + "install", + "--use-feature=2020-resolver", + "--no-cache-dir", + "--no-deps", + "--no-index", + "--find-links", find_links.index_html, + "--constraint", constraints_txt, + "base==0.1.0", + expect_error=True, + ) + + message = ( + "Hashes are required in --require-hashes mode, but they are missing " + "from some requirements." + ) + assert message in result.stderr, str(result) From 8385cb5250e72129210b9b3f4f78665325741003 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <hugovk@users.noreply.github.com> Date: Sun, 13 Sep 2020 10:14:03 +0300 Subject: [PATCH 2507/3170] Document fast-track deprecation policy --- docs/html/development/release-process.rst | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/html/development/release-process.rst b/docs/html/development/release-process.rst index 44197955e8e..82358b3ebc1 100644 --- a/docs/html/development/release-process.rst +++ b/docs/html/development/release-process.rst @@ -38,11 +38,17 @@ Deprecation Policy Any change to pip that removes or significantly alters user-visible behavior that is described in the pip documentation will be deprecated for a minimum of -6 months before the change occurs. Deprecation will take the form of a warning +6 months before the change occurs. + +Certain changes may be fast tracked and have a deprecation period of 3 months. +This requires at least two members of the pip team to be in favor of doing so, +and no pip maintainers opposing. + +Deprecation will take the form of a warning being issued by pip when the feature is used. Longer deprecation periods, or deprecation warnings for behavior changes that would not normally be covered by this policy, are also possible depending on circumstances, but this is at the -discretion of the pip developers. +discretion of the pip maintainers. Note that the documentation is the sole reference for what counts as agreed behavior. If something isn't explicitly mentioned in the documentation, it can From b6fb8b80e247c096d0f96b5980c1f57f3e53902e Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <hugovk@users.noreply.github.com> Date: Sun, 13 Sep 2020 10:15:34 +0300 Subject: [PATCH 2508/3170] Reflow paragraph --- docs/html/development/release-process.rst | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/docs/html/development/release-process.rst b/docs/html/development/release-process.rst index 82358b3ebc1..66ff3ca7c51 100644 --- a/docs/html/development/release-process.rst +++ b/docs/html/development/release-process.rst @@ -44,11 +44,11 @@ Certain changes may be fast tracked and have a deprecation period of 3 months. This requires at least two members of the pip team to be in favor of doing so, and no pip maintainers opposing. -Deprecation will take the form of a warning -being issued by pip when the feature is used. Longer deprecation periods, or -deprecation warnings for behavior changes that would not normally be covered by -this policy, are also possible depending on circumstances, but this is at the -discretion of the pip maintainers. +Deprecation will take the form of a warning being issued by pip when the +feature is used. Longer deprecation periods, or deprecation warnings for +behavior changes that would not normally be covered by this policy, are also +possible depending on circumstances, but this is at the discretion of the pip +maintainers. Note that the documentation is the sole reference for what counts as agreed behavior. If something isn't explicitly mentioned in the documentation, it can From 8f8a1d65b2d6b68dcfad6946ee9e24f76d867fe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Thu, 6 Aug 2020 21:56:34 +0700 Subject: [PATCH 2509/3170] Dedent late download logs --- ...82ffb4-cde3-4dd5-8f37-6f4ef53e028b.trivial | 0 src/pip/_internal/operations/prepare.py | 93 +++++++++---------- 2 files changed, 45 insertions(+), 48 deletions(-) create mode 100644 news/4c82ffb4-cde3-4dd5-8f37-6f4ef53e028b.trivial diff --git a/news/4c82ffb4-cde3-4dd5-8f37-6f4ef53e028b.trivial b/news/4c82ffb4-cde3-4dd5-8f37-6f4ef53e028b.trivial new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 5eb71ce073f..0a254052654 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -487,10 +487,10 @@ def prepare_linked_requirement(self, req, parallel_builds=False): self._log_preparing_link(req) with indent_log(): wheel_dist = self._fetch_metadata_using_lazy_wheel(link) - if wheel_dist is not None: - req.needs_more_preparation = True - return wheel_dist - return self._prepare_linked_requirement(req, parallel_builds) + if wheel_dist is not None: + req.needs_more_preparation = True + return wheel_dist + return self._prepare_linked_requirement(req, parallel_builds) def prepare_linked_requirements_more(self, reqs, parallel_builds=False): # type: (Iterable[InstallRequirement], bool) -> None @@ -519,51 +519,48 @@ def _prepare_linked_requirement(self, req, parallel_builds): link = req.link download_dir = self._get_download_dir(link) - with indent_log(): - self._ensure_link_req_src_dir(req, download_dir, parallel_builds) - hashes = self._get_linked_req_hashes(req) - if link.url not in self._downloaded: - try: - local_file = unpack_url( - link, req.source_dir, self._download, - download_dir, hashes, - ) - except NetworkConnectionError as exc: - raise InstallationError( - 'Could not install requirement {} because of HTTP ' - 'error {} for URL {}'.format(req, exc, link) - ) - else: - file_path, content_type = self._downloaded[link.url] - if hashes: - hashes.check_against_path(file_path) - local_file = File(file_path, content_type) - - # For use in later processing, preserve the file path on the - # requirement. - if local_file: - req.local_file_path = local_file.path - - dist = _get_prepared_distribution( - req, self.req_tracker, self.finder, self.build_isolation, - ) - - if download_dir: - if link.is_existing_dir(): - logger.info('Link is a directory, ignoring download_dir') - elif local_file: - download_location = os.path.join( - download_dir, link.filename - ) - if not os.path.exists(download_location): - shutil.copy(local_file.path, download_location) - download_path = display_path(download_location) - logger.info('Saved %s', download_path) + self._ensure_link_req_src_dir(req, download_dir, parallel_builds) + hashes = self._get_linked_req_hashes(req) + if link.url not in self._downloaded: + try: + local_file = unpack_url( + link, req.source_dir, self._download, + download_dir, hashes, + ) + except NetworkConnectionError as exc: + raise InstallationError( + 'Could not install requirement {} because of HTTP ' + 'error {} for URL {}'.format(req, exc, link) + ) + else: + file_path, content_type = self._downloaded[link.url] + if hashes: + hashes.check_against_path(file_path) + local_file = File(file_path, content_type) + + # For use in later processing, + # preserve the file path on the requirement. + if local_file: + req.local_file_path = local_file.path + + dist = _get_prepared_distribution( + req, self.req_tracker, self.finder, self.build_isolation, + ) - if self._download_should_save: - # Make a .zip of the source_dir we already created. - if link.is_vcs: - req.archive(self.download_dir) + if download_dir: + if link.is_existing_dir(): + logger.info('Link is a directory, ignoring download_dir') + elif local_file: + download_location = os.path.join(download_dir, link.filename) + if not os.path.exists(download_location): + shutil.copy(local_file.path, download_location) + download_path = display_path(download_location) + logger.info('Saved %s', download_path) + + if self._download_should_save: + # Make a .zip of the source_dir we already created. + if link.is_vcs: + req.archive(self.download_dir) return dist def prepare_editable_requirement( From 2f0fa53e95edceb5b45d7cd17cb508cc0aaed49f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <hugovk@users.noreply.github.com> Date: Sun, 13 Sep 2020 21:03:16 +0300 Subject: [PATCH 2510/3170] Add news fragment --- news/8417.removal | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/8417.removal diff --git a/news/8417.removal b/news/8417.removal new file mode 100644 index 00000000000..8f280b535c3 --- /dev/null +++ b/news/8417.removal @@ -0,0 +1 @@ +Document that certain removals can be fast tracked. From 12fe2872ccdb023dbe1fb1aaa2aa03eb8102bacc Mon Sep 17 00:00:00 2001 From: Srinivas Nyayapati <shireenrao@gmail.com> Date: Thu, 16 Jul 2020 08:53:18 -0400 Subject: [PATCH 2511/3170] first set of changes to use python -m to run pip in the docs --- docs/html/installing.rst | 9 +-------- docs/html/quickstart.rst | 12 ++++++------ news/7311.doc | 1 + 3 files changed, 8 insertions(+), 14 deletions(-) create mode 100644 news/7311.doc diff --git a/docs/html/installing.rst b/docs/html/installing.rst index 0a263ac4137..0f83bdd1151 100644 --- a/docs/html/installing.rst +++ b/docs/html/installing.rst @@ -97,12 +97,7 @@ the `Python Packaging User Guide Upgrading pip ============= -On Linux or macOS:: - - pip install -U pip - - -On Windows [4]_:: +On Windows, Linux or macOS:: python -m pip install -U pip @@ -134,5 +129,3 @@ pip works on Unix/Linux, macOS, and Windows. ``--user`` installs for pip itself, should not be considered to be fully tested or endorsed. For discussion, see `Issue 1668 <https://github.com/pypa/pip/issues/1668>`_. - -.. [4] https://github.com/pypa/pip/issues/1299 diff --git a/docs/html/quickstart.rst b/docs/html/quickstart.rst index c2250399c6a..88d207ebc5f 100644 --- a/docs/html/quickstart.rst +++ b/docs/html/quickstart.rst @@ -8,7 +8,7 @@ Install a package from `PyPI`_: :: - $ pip install SomePackage + $ python -m pip install SomePackage [...] Successfully installed SomePackage @@ -18,7 +18,7 @@ network connection: :: - $ pip install SomePackage-1.0-py2.py3-none-any.whl + $ python -m pip install SomePackage-1.0-py2.py3-none-any.whl [...] Successfully installed SomePackage @@ -26,7 +26,7 @@ Show what files were installed: :: - $ pip show --files SomePackage + $ python -m pip show --files SomePackage Name: SomePackage Version: 1.0 Location: /my/env/lib/pythonx.x/site-packages @@ -38,14 +38,14 @@ List what packages are outdated: :: - $ pip list --outdated + $ python -m pip list --outdated SomePackage (Current: 1.0 Latest: 2.0) Upgrade a package: :: - $ pip install --upgrade SomePackage + $ python -m pip install --upgrade SomePackage [...] Found existing installation: SomePackage 1.0 Uninstalling SomePackage: @@ -57,7 +57,7 @@ Uninstall a package: :: - $ pip uninstall SomePackage + $ python -m pip uninstall SomePackage Uninstalling SomePackage: /my/env/lib/pythonx.x/site-packages/somepackage Proceed (y/n)? y diff --git a/news/7311.doc b/news/7311.doc new file mode 100644 index 00000000000..f08d30a06e5 --- /dev/null +++ b/news/7311.doc @@ -0,0 +1 @@ +Use ``python -m`` to run pip in the docs From 2329d796897867cacf6b7b35b44ca16f89ae79bb Mon Sep 17 00:00:00 2001 From: Srinivas Nyayapati <shireenrao@gmail.com> Date: Thu, 16 Jul 2020 09:49:21 -0400 Subject: [PATCH 2512/3170] use shell instead of mentioning specific platform in installing doc --- docs/html/installing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/html/installing.rst b/docs/html/installing.rst index 0f83bdd1151..4c2c2eca3db 100644 --- a/docs/html/installing.rst +++ b/docs/html/installing.rst @@ -97,7 +97,7 @@ the `Python Packaging User Guide Upgrading pip ============= -On Windows, Linux or macOS:: +In a shell:: python -m pip install -U pip From 119d8666b4b3e59c531afcf970cd0ae2d0f3cdf0 Mon Sep 17 00:00:00 2001 From: Srinivas Nyayapati <shireenrao@gmail.com> Date: Sun, 19 Jul 2020 23:56:47 -0400 Subject: [PATCH 2513/3170] implement sphinx-tabs on installing page of docs --- docs/html/conf.py | 2 +- docs/html/installing.rst | 10 ++++++++-- tools/requirements/docs.txt | 1 + 3 files changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/html/conf.py b/docs/html/conf.py index 2ef2647ce72..c5c2e02e692 100644 --- a/docs/html/conf.py +++ b/docs/html/conf.py @@ -30,7 +30,7 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. # extensions = ['sphinx.ext.autodoc'] -extensions = ['sphinx.ext.extlinks', 'pip_sphinxext', 'sphinx.ext.intersphinx'] +extensions = ['sphinx.ext.extlinks', 'pip_sphinxext', 'sphinx.ext.intersphinx', 'sphinx_tabs.tabs'] # intersphinx intersphinx_cache_limit = 0 diff --git a/docs/html/installing.rst b/docs/html/installing.rst index 4c2c2eca3db..9653331a4f7 100644 --- a/docs/html/installing.rst +++ b/docs/html/installing.rst @@ -97,9 +97,15 @@ the `Python Packaging User Guide Upgrading pip ============= -In a shell:: +.. tabs:: - python -m pip install -U pip + .. tab:: Linux/MacOS + + python -m pip install -U pip + + .. tab:: Windows + + py -m pip install -U pip .. _compatibility-requirements: diff --git a/tools/requirements/docs.txt b/tools/requirements/docs.txt index 267d5b1ddfb..10a9b0c6014 100644 --- a/tools/requirements/docs.txt +++ b/tools/requirements/docs.txt @@ -1,6 +1,7 @@ sphinx == 3.2.1 git+https://github.com/python/python-docs-theme.git#egg=python-docs-theme git+https://github.com/pypa/pypa-docs-theme.git#egg=pypa-docs-theme +https://github.com/djungelorm/sphinx-tabs/releases/download/v1.1.13/sphinx-tabs-1.1.13.tar.gz # `docs.pipext` uses pip's internals to generate documentation. So, we install # the current directory to make it work. From dccf555813a744c6aa5807bbfadaa00448ea3d30 Mon Sep 17 00:00:00 2001 From: Srinivas Nyayapati <shireenrao@gmail.com> Date: Mon, 20 Jul 2020 09:18:26 -0400 Subject: [PATCH 2514/3170] update docs requirements --- tools/requirements/docs.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/requirements/docs.txt b/tools/requirements/docs.txt index 10a9b0c6014..dc93a60ff3d 100644 --- a/tools/requirements/docs.txt +++ b/tools/requirements/docs.txt @@ -1,7 +1,7 @@ sphinx == 3.2.1 git+https://github.com/python/python-docs-theme.git#egg=python-docs-theme git+https://github.com/pypa/pypa-docs-theme.git#egg=pypa-docs-theme -https://github.com/djungelorm/sphinx-tabs/releases/download/v1.1.13/sphinx-tabs-1.1.13.tar.gz +sphinx-tabs == 1.1.13 # `docs.pipext` uses pip's internals to generate documentation. So, we install # the current directory to make it work. From 28f3f2cd2ce21ed6e0f354a3ca924d9e9aac5ce7 Mon Sep 17 00:00:00 2001 From: Srinivas Nyayapati <shireenrao@gmail.com> Date: Tue, 21 Jul 2020 09:58:00 -0400 Subject: [PATCH 2515/3170] ignore sphinx-tab extension for man builds --- docs/html/conf.py | 7 ++++++- tox.ini | 2 +- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/docs/html/conf.py b/docs/html/conf.py index c5c2e02e692..f0e880f5e4c 100644 --- a/docs/html/conf.py +++ b/docs/html/conf.py @@ -30,7 +30,12 @@ # Add any Sphinx extension module names here, as strings. They can be # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. # extensions = ['sphinx.ext.autodoc'] -extensions = ['sphinx.ext.extlinks', 'pip_sphinxext', 'sphinx.ext.intersphinx', 'sphinx_tabs.tabs'] +extensions = [ + 'sphinx.ext.extlinks', + 'pip_sphinxext', + 'sphinx.ext.intersphinx', + 'sphinx_tabs.tabs' +] # intersphinx intersphinx_cache_limit = 0 diff --git a/tox.ini b/tox.ini index 82e9abc68d7..a673b0f598f 100644 --- a/tox.ini +++ b/tox.ini @@ -56,7 +56,7 @@ commands = # can not use a different configuration directory vs source directory on RTD # currently -- https://github.com/rtfd/readthedocs.org/issues/1543. # That is why we have a "-c docs/html" in the next line. - sphinx-build -W -d {envtmpdir}/doctrees/man -b man docs/man docs/build/man -c docs/html + sphinx-build -W -d {envtmpdir}/doctrees/man -b man docs/man docs/build/man -c docs/html -D extensions=sphinx.ext.extlinks,pip_sphinxext,sphinx.ext.intersphinx [testenv:lint] skip_install = True From ad8cc27b8c53bb93115e50974ced130875e05d2e Mon Sep 17 00:00:00 2001 From: Srinivas Nyayapati <shireenrao@gmail.com> Date: Tue, 28 Jul 2020 12:44:17 -0400 Subject: [PATCH 2516/3170] update quickstart.rst to use tabs --- docs/html/quickstart.rst | 125 ++++++++++++++++++++++++++++----------- 1 file changed, 92 insertions(+), 33 deletions(-) diff --git a/docs/html/quickstart.rst b/docs/html/quickstart.rst index 88d207ebc5f..41a9275e9b4 100644 --- a/docs/html/quickstart.rst +++ b/docs/html/quickstart.rst @@ -6,62 +6,121 @@ First, :doc:`install pip <installing>`. Install a package from `PyPI`_: -:: +.. tabs:: + + .. code-tab:: Bash Unix + + $ python -m pip install SomePackage + [...] + Successfully installed SomePackage + + .. code-tab:: Bash Windows + + $ py -m pip install SomePackage + [...] + Successfully installed SomePackage - $ python -m pip install SomePackage - [...] - Successfully installed SomePackage Install a package that's already been downloaded from `PyPI`_ or obtained from elsewhere. This is useful if the target machine does not have a network connection: -:: +.. tabs:: + + .. code-tab:: Bash Unix + + $ python -m pip install SomePackage-1.0-py2.py3-none-any.whl + [...] + Successfully installed SomePackage + + .. code-tab:: Bash Windows + + $ py -m pip install SomePackage-1.0-py2.py3-none-any.whl + [...] + Successfully installed SomePackage - $ python -m pip install SomePackage-1.0-py2.py3-none-any.whl - [...] - Successfully installed SomePackage Show what files were installed: -:: +.. tabs:: + + .. code-tab:: Bash Unix + + $ python -m pip show --files SomePackage + Name: SomePackage + Version: 1.0 + Location: /my/env/lib/pythonx.x/site-packages + Files: + ../somepackage/__init__.py + [...] - $ python -m pip show --files SomePackage - Name: SomePackage - Version: 1.0 - Location: /my/env/lib/pythonx.x/site-packages - Files: - ../somepackage/__init__.py - [...] + .. code-tab:: Bash Windows + + $ py -m pip show --files SomePackage + Name: SomePackage + Version: 1.0 + Location: /my/env/lib/pythonx.x/site-packages + Files: + ../somepackage/__init__.py + [...] List what packages are outdated: -:: +.. tabs:: + + .. code-tab:: Bash Unix + + $ python -m pip list --outdated + SomePackage (Current: 1.0 Latest: 2.0) - $ python -m pip list --outdated - SomePackage (Current: 1.0 Latest: 2.0) + .. code-tab:: Bash Windows + + $ py -m pip list --outdated + SomePackage (Current: 1.0 Latest: 2.0) Upgrade a package: -:: +.. tabs:: + + .. code-tab:: Bash Unix - $ python -m pip install --upgrade SomePackage - [...] - Found existing installation: SomePackage 1.0 - Uninstalling SomePackage: - Successfully uninstalled SomePackage - Running setup.py install for SomePackage - Successfully installed SomePackage + $ python -m pip install --upgrade SomePackage + [...] + Found existing installation: SomePackage 1.0 + Uninstalling SomePackage: + Successfully uninstalled SomePackage + Running setup.py install for SomePackage + Successfully installed SomePackage + + .. code-tab:: Bash Windows + + $ py -m pip install --upgrade SomePackage + [...] + Found existing installation: SomePackage 1.0 + Uninstalling SomePackage: + Successfully uninstalled SomePackage + Running setup.py install for SomePackage + Successfully installed SomePackage Uninstall a package: -:: +.. tabs:: + + .. code-tab:: Bash Unix + + $ python -m pip uninstall SomePackage + Uninstalling SomePackage: + /my/env/lib/pythonx.x/site-packages/somepackage + Proceed (y/n)? y + Successfully uninstalled SomePackage + + .. code-tab:: Bash Windows - $ python -m pip uninstall SomePackage - Uninstalling SomePackage: - /my/env/lib/pythonx.x/site-packages/somepackage - Proceed (y/n)? y - Successfully uninstalled SomePackage + $ py -m pip uninstall SomePackage + Uninstalling SomePackage: + /my/env/lib/pythonx.x/site-packages/somepackage + Proceed (y/n)? y + Successfully uninstalled SomePackage .. _PyPI: https://pypi.org/ From f1abd651e3b3c0ca393efc396dd6cace874b15ab Mon Sep 17 00:00:00 2001 From: Srinivas Nyayapati <shireenrao@gmail.com> Date: Wed, 29 Jul 2020 07:07:32 -0400 Subject: [PATCH 2517/3170] WIP change user guide + make consistent changes across --- docs/html/installing.rst | 8 +- docs/html/quickstart.rst | 38 ++-- docs/html/user_guide.rst | 469 +++++++++++++++++++++++++++++---------- news/7311.doc | 2 +- 4 files changed, 377 insertions(+), 140 deletions(-) diff --git a/docs/html/installing.rst b/docs/html/installing.rst index 9653331a4f7..511e4d02ec6 100644 --- a/docs/html/installing.rst +++ b/docs/html/installing.rst @@ -99,13 +99,13 @@ Upgrading pip .. tabs:: - .. tab:: Linux/MacOS + .. code-tab:: shell Unix/macOS - python -m pip install -U pip + $ python -m pip install -U pip - .. tab:: Windows + .. code-tab:: shell Windows - py -m pip install -U pip + C:\> py -m pip install -U pip .. _compatibility-requirements: diff --git a/docs/html/quickstart.rst b/docs/html/quickstart.rst index 41a9275e9b4..e8754d661ae 100644 --- a/docs/html/quickstart.rst +++ b/docs/html/quickstart.rst @@ -8,34 +8,34 @@ Install a package from `PyPI`_: .. tabs:: - .. code-tab:: Bash Unix + .. code-tab:: shell Unix/macOS $ python -m pip install SomePackage [...] Successfully installed SomePackage - .. code-tab:: Bash Windows + .. code-tab:: shell Windows - $ py -m pip install SomePackage + C:\> py -m pip install SomePackage [...] Successfully installed SomePackage Install a package that's already been downloaded from `PyPI`_ or -obtained from elsewhere. This is useful if the target machine does not have a +obtained from elsewhere. This is useful if the target macOShine does not have a network connection: .. tabs:: - .. code-tab:: Bash Unix + .. code-tab:: shell Unix/macOS $ python -m pip install SomePackage-1.0-py2.py3-none-any.whl [...] Successfully installed SomePackage - .. code-tab:: Bash Windows + .. code-tab:: shell Windows - $ py -m pip install SomePackage-1.0-py2.py3-none-any.whl + C:\> py -m pip install SomePackage-1.0-py2.py3-none-any.whl [...] Successfully installed SomePackage @@ -44,7 +44,7 @@ Show what files were installed: .. tabs:: - .. code-tab:: Bash Unix + .. code-tab:: shell Unix/macOS $ python -m pip show --files SomePackage Name: SomePackage @@ -54,9 +54,9 @@ Show what files were installed: ../somepackage/__init__.py [...] - .. code-tab:: Bash Windows + .. code-tab:: shell Windows - $ py -m pip show --files SomePackage + C:\> py -m pip show --files SomePackage Name: SomePackage Version: 1.0 Location: /my/env/lib/pythonx.x/site-packages @@ -68,21 +68,21 @@ List what packages are outdated: .. tabs:: - .. code-tab:: Bash Unix + .. code-tab:: shell Unix/macOS $ python -m pip list --outdated SomePackage (Current: 1.0 Latest: 2.0) - .. code-tab:: Bash Windows + .. code-tab:: shell Windows - $ py -m pip list --outdated + C:\> py -m pip list --outdated SomePackage (Current: 1.0 Latest: 2.0) Upgrade a package: .. tabs:: - .. code-tab:: Bash Unix + .. code-tab:: shell Unix/macOS $ python -m pip install --upgrade SomePackage [...] @@ -92,9 +92,9 @@ Upgrade a package: Running setup.py install for SomePackage Successfully installed SomePackage - .. code-tab:: Bash Windows + .. code-tab:: shell Windows - $ py -m pip install --upgrade SomePackage + C:\> py -m pip install --upgrade SomePackage [...] Found existing installation: SomePackage 1.0 Uninstalling SomePackage: @@ -106,7 +106,7 @@ Uninstall a package: .. tabs:: - .. code-tab:: Bash Unix + .. code-tab:: shell Unix/macOS $ python -m pip uninstall SomePackage Uninstalling SomePackage: @@ -114,9 +114,9 @@ Uninstall a package: Proceed (y/n)? y Successfully uninstalled SomePackage - .. code-tab:: Bash Windows + .. code-tab:: shell Windows - $ py -m pip uninstall SomePackage + C:\> py -m pip uninstall SomePackage Uninstalling SomePackage: /my/env/lib/pythonx.x/site-packages/somepackage Proceed (y/n)? y diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index ce013c2a362..99162ccf272 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -9,23 +9,24 @@ Running pip =========== pip is a command line program. When you install pip, a ``pip`` command is added -to your system, which can be run from the command prompt as follows:: +to your system, which can be run from the command prompt as follows: - $ pip <pip arguments> +.. tabs:: -If you cannot run the ``pip`` command directly (possibly because the location -where it was installed isn't on your operating system's ``PATH``) then you can -run pip via the Python interpreter:: + .. code-tab:: shell Unix/macOS - $ python -m pip <pip arguments> + $ python -m pip <pip arguments> -On Windows, the ``py`` launcher can be used:: + # python -m pip executes pip using the Python interpreter you + # specified as python. So '/usr/bin/python3.7 -m pip' means + # you are executing pip for your interpreter located at /usr/bin/python3.7. - $ py -m pip <pip arguments> + .. code-tab:: shell Windows -Even though pip is available from your Python installation as an importable -module, via ``import pip``, it is *not supported* to use pip in this way. For -more details, see :ref:`Using pip from your program`. + C:\> py -m pip <pip arguments> + + # py -m pip executes pip using the latest Python interpreter you + # have installed. For more details, see https://docs.python.org/3/using/windows.html#launcher. Installing Packages @@ -36,12 +37,21 @@ directly from distribution files. The most common scenario is to install from `PyPI`_ using :ref:`Requirement -Specifiers` :: +Specifiers` + +.. tabs:: + + .. code-tab:: shell Unix/macOS + + $ python -m pip install SomePackage # latest version + $ python -m pip install SomePackage==1.0.4 # specific version + $ python -m pip install 'SomePackage>=1.0.4' # minimum version - $ pip install SomePackage # latest version - $ pip install SomePackage==1.0.4 # specific version - $ pip install 'SomePackage>=1.0.4' # minimum version + .. code-tab:: shell Windows + C:\> py -m pip install SomePackage # latest version + C:\> py -m pip install SomePackage==1.0.4 # specific version + C:\> py -m pip install 'SomePackage>=1.0.4' # minimum version For more information and examples, see the :ref:`pip install` reference. @@ -142,10 +152,17 @@ Requirements Files ================== "Requirements files" are files containing a list of items to be -installed using :ref:`pip install` like so:: +installed using :ref:`pip install` like so: + +.. tabs:: + + .. code-tab:: shell Unix/macOS + + $ python -m pip install -r requirements.txt - pip install -r requirements.txt + .. code-tab:: shell Windows + C:\> py -m pip install -r requirements.txt Details on the format of the files are here: :ref:`Requirements File Format`. @@ -160,10 +177,17 @@ In practice, there are 4 common uses of Requirements files: this case, your requirement file contains a pinned version of everything that was installed when ``pip freeze`` was run. - :: +.. tabs:: - pip freeze > requirements.txt - pip install -r requirements.txt + .. code-tab:: shell Unix/macOS + + $ python -m pip freeze > requirements.txt + $ python -m pip install -r requirements.txt + + .. code-tab:: shell Windows + + C:\> py -m pip freeze > requirements.txt + C:\> py -m pip install -r requirements.txt 2. Requirements files are used to force pip to properly resolve dependencies. As it is now, pip `doesn't have true dependency resolution @@ -228,9 +252,17 @@ contents is nearly identical to :ref:`Requirements Files`. There is one key difference: Including a package in a constraints file does not trigger installation of the package. -Use a constraints file like so:: +Use a constraints file like so: + +.. tabs:: + + .. code-tab:: shell Unix/macOS + + $ python -m pip install -c constraints.txt + + .. code-tab:: shell Windows - pip install -c constraints.txt + C:\> py -m pip install -c constraints.txt Constraints files are used for exactly the same reason as requirements files when you don't know exactly what things you want to install. For instance, say @@ -268,9 +300,15 @@ archives. To install directly from a wheel archive: -:: +.. tabs:: + + .. code-tab:: shell Unix/macOS + + $ python -m pip install SomePackage-1.0-py2.py3-none-any.whl + + .. code-tab:: shell Windows - pip install SomePackage-1.0-py2.py3-none-any.whl + C:\> py -m pip install SomePackage-1.0-py2.py3-none-any.whl For the cases where wheels are not available, pip offers :ref:`pip wheel` as a @@ -283,17 +321,30 @@ convenience, to build wheels for all your requirements and dependencies. To build wheels for your requirements and all their dependencies to a local directory: -:: +.. tabs:: + + .. code-tab:: shell Unix/macOS + + $ python -m pip install wheel + $ python -m pip wheel --wheel-dir=/local/wheels -r requirements.txt - pip install wheel - pip wheel --wheel-dir=/local/wheels -r requirements.txt + .. code-tab:: shell Windows + + C:\> py -m pip install wheel + C:\> py -m pip wheel --wheel-dir=/local/wheels -r requirements.txt And *then* to install those requirements just using your local directory of wheels (and not from PyPI): -:: +.. tabs:: + + .. code-tab:: shell Unix/macOS - pip install --no-index --find-links=/local/wheels -r requirements.txt + $ python -m pip install --no-index --find-links=/local/wheels -r requirements.txt + + .. code-tab:: shell Windows + + C:\> py -m pip install --no-index --find-links=/local/wheels -r requirements.txt Uninstalling Packages @@ -301,9 +352,16 @@ Uninstalling Packages pip is able to uninstall most packages like so: -:: +.. tabs:: + + .. code-tab:: shell Unix/macOS + + $ python -m pip uninstall SomePackage + + .. code-tab:: shell Windows + + C:\> py -m pip uninstall SomePackage - $ pip uninstall SomePackage pip also performs an automatic uninstall of an old version of a package before upgrading to a newer version. @@ -316,33 +374,62 @@ Listing Packages To list installed packages: -:: +.. tabs:: + + .. code-tab:: shell Unix/macOS + + $ python -m pip list + docutils (0.9.1) + Jinja2 (2.6) + Pygments (1.5) + Sphinx (1.1.2) + + .. code-tab:: shell Windows + + C:\> py -m pip list + docutils (0.9.1) + Jinja2 (2.6) + Pygments (1.5) + Sphinx (1.1.2) - $ pip list - docutils (0.9.1) - Jinja2 (2.6) - Pygments (1.5) - Sphinx (1.1.2) To list outdated packages, and show the latest version available: -:: +.. tabs:: + + .. code-tab:: shell Unix/macOS - $ pip list --outdated - docutils (Current: 0.9.1 Latest: 0.10) - Sphinx (Current: 1.1.2 Latest: 1.1.3) + $ python -m pip list --outdated + docutils (Current: 0.9.1 Latest: 0.10) + Sphinx (Current: 1.1.2 Latest: 1.1.3) + .. code-tab:: shell Windows + + C:\> py -m pip list --outdated + docutils (Current: 0.9.1 Latest: 0.10) + Sphinx (Current: 1.1.2 Latest: 1.1.3) To show details about an installed package: -:: +.. tabs:: + + .. code-tab:: shell Unix/macOS - $ pip show sphinx - --- - Name: Sphinx - Version: 1.1.3 - Location: /my/env/lib/pythonx.x/site-packages - Requires: Pygments, Jinja2, docutils + $ python -m pip show sphinx + --- + Name: Sphinx + Version: 1.1.3 + Location: /my/env/lib/pythonx.x/site-packages + Requires: Pygments, Jinja2, docutils + + .. code-tab:: shell Windows + + C:\> py -m pip show sphinx + --- + Name: Sphinx + Version: 1.1.3 + Location: /my/env/lib/pythonx.x/site-packages + Requires: Pygments, Jinja2, docutils For more information and examples, see the :ref:`pip list` and :ref:`pip show` @@ -353,9 +440,17 @@ Searching for Packages ====================== pip can search `PyPI`_ for packages using the ``pip search`` -command:: +command: + +.. tabs:: + + .. code-tab:: shell Unix/macOS - $ pip search "query" + $ python -m pip search "query" + + .. code-tab:: shell Windows + + C:\> py -m pip search "query" The query will be used to search the names and summaries of all packages. @@ -384,7 +479,7 @@ all users) configuration: * On Unix the default configuration file is: :file:`$HOME/.config/pip/pip.conf` which respects the ``XDG_CONFIG_HOME`` environment variable. -* On macOS the configuration file is +* On macOSOS the configuration file is :file:`$HOME/Library/Application Support/pip/pip.conf` if directory ``$HOME/Library/Application Support/pip`` exists else :file:`$HOME/.config/pip/pip.conf`. @@ -393,7 +488,7 @@ all users) configuration: There are also a legacy per-user configuration file which is also respected, these are located at: -* On Unix and macOS the configuration file is: :file:`$HOME/.pip/pip.conf` +* On Unix and macOSOS the configuration file is: :file:`$HOME/.pip/pip.conf` * On Windows the configuration file is: :file:`%HOME%\\pip\\pip.ini` You can set a custom path location for this config file using the environment @@ -401,7 +496,7 @@ variable ``PIP_CONFIG_FILE``. **Inside a virtualenv**: -* On Unix and macOS the file is :file:`$VIRTUAL_ENV/pip.conf` +* On Unix and macOSOS the file is :file:`$VIRTUAL_ENV/pip.conf` * On Windows the file is: :file:`%VIRTUAL_ENV%\\pip.ini` **Global**: @@ -410,7 +505,7 @@ variable ``PIP_CONFIG_FILE``. it may be in a "pip" subdirectory of any of the paths set in the environment variable ``XDG_CONFIG_DIRS`` (if it exists), for example :file:`/etc/xdg/pip/pip.conf`. -* On macOS the file is: :file:`/Library/Application Support/pip/pip.conf` +* On macOSOS the file is: :file:`/Library/Application Support/pip/pip.conf` * On Windows XP the file is: :file:`C:\\Documents and Settings\\All Users\\Application Data\\pip\\pip.ini` * On Windows 7 and later the file is hidden, but writeable at @@ -515,22 +610,56 @@ pip's command line options can be set with environment variables using the format ``PIP_<UPPER_LONG_NAME>`` . Dashes (``-``) have to be replaced with underscores (``_``). -For example, to set the default timeout:: +For example, to set the default timeout: + +.. tabs:: + + .. code-tab:: shell Unix/macOS + + $ export PIP_DEFAULT_TIMEOUT=60 + + .. code-tab:: shell Windows + + C:\> set PIP_DEFAULT_TIMEOUT=60 + +This is the same as passing the option to pip directly: + +.. tabs:: - export PIP_DEFAULT_TIMEOUT=60 + .. code-tab:: shell Unix/macOS -This is the same as passing the option to pip directly:: + $ python -m pip --default-timeout=60 [...] - pip --default-timeout=60 [...] + .. code-tab:: shell Windows + + C:\> py -m pip --default-timeout=60 [...] For command line options which can be repeated, use a space to separate -multiple values. For example:: +multiple values. For example: - export PIP_FIND_LINKS="http://mirror1.example.com http://mirror2.example.com" +.. tabs:: -is the same as calling:: + .. code-tab:: shell Unix/macOS + + $ export PIP_FIND_LINKS="http://mirror1.example.com http://mirror2.example.com" + + .. code-tab:: shell Windows + + C:\> set PIP_FIND_LINKS="http://mirror1.example.com http://mirror2.example.com" + + +is the same as calling: + +.. tabs:: + + .. code-tab:: shell Unix/macOS + + $ python -m pip install --find-links=http://mirror1.example.com --find-links=http://mirror2.example.com + + .. code-tab:: shell Windows + + C:\> py -m pip install --find-links=http://mirror1.example.com --find-links=http://mirror2.example.com - pip install --find-links=http://mirror1.example.com --find-links=http://mirror2.example.com Options that do not take a value, but can be repeated (such as ``--verbose``) can be specified using the number of repetitions, so:: @@ -573,15 +702,15 @@ pip comes with support for command line completion in bash, zsh and fish. To setup for bash:: - $ pip completion --bash >> ~/.profile + $ python -m pip completion --bash >> ~/.profile To setup for zsh:: - $ pip completion --zsh >> ~/.zprofile + $ python -m pip completion --zsh >> ~/.zprofile To setup for fish:: -$ pip completion --fish > ~/.config/fish/completions/pip.fish + $ python -m pip completion --fish > ~/.config/fish/completions/pip.fish Alternatively, you can use the result of the ``completion`` command directly with the eval function of your shell, e.g. by adding the following to your @@ -600,24 +729,47 @@ Installing from local packages In some cases, you may want to install from local packages only, with no traffic to PyPI. -First, download the archives that fulfill your requirements:: +First, download the archives that fulfill your requirements: + +.. tabs:: + + .. code-tab:: shell Unix/macOS -$ pip download --destination-directory DIR -r requirements.txt + $ python -m pip download --destination-directory DIR -r requirements.txt + + .. code-tab:: shell Windows + + C:\> py -m pip download --destination-directory DIR -r requirements.txt Note that ``pip download`` will look in your wheel cache first, before trying to download from PyPI. If you've never installed your requirements before, you won't have a wheel cache for those items. In that case, if some of your requirements don't come as wheels from PyPI, and you want wheels, then run -this instead:: +this instead: + +.. tabs:: + + .. code-tab:: shell Unix/macOS -$ pip wheel --wheel-dir DIR -r requirements.txt + $ python -m pip wheel --wheel-dir DIR -r requirements.txt + .. code-tab:: shell Windows + + C:\> py -m pip wheel --wheel-dir DIR -r requirements.txt Then, to install from local only, you'll be using :ref:`--find-links -<install_--find-links>` and :ref:`--no-index <install_--no-index>` like so:: +<install_--find-links>` and :ref:`--no-index <install_--no-index>` like so: + +.. tabs:: + + .. code-tab:: shell Unix/macOS -$ pip install --no-index --find-links=DIR -r requirements.txt + $ python -m pip install --no-index --find-links=DIR -r requirements.txt + + .. code-tab:: shell Windows + + C:\> -m pip install --no-index --find-links=DIR -r requirements.txt "Only if needed" Recursive Upgrade @@ -636,10 +788,20 @@ The default strategy is ``only-if-needed``. This was changed in pip 10.0 due to the breaking nature of ``eager`` when upgrading conflicting dependencies. As an historic note, an earlier "fix" for getting the ``only-if-needed`` -behaviour was:: +behaviour was: + +.. tabs:: + + .. code-tab:: shell Unix/macOS + + $ python -m pip install --upgrade --no-deps SomePackage + $ python -m pip install SomePackage + + .. code-tab:: shell Windows + + C:\> py -m pip install --upgrade --no-deps SomePackage + C:\> py -m pip install SomePackage - pip install --upgrade --no-deps SomePackage - pip install SomePackage A proposal for an ``upgrade-all`` command is being considered as a safer alternative to the behaviour of eager upgrading. @@ -662,11 +824,19 @@ Moreover, the "user scheme" can be customized by setting the ``site.USER_BASE``. To install "SomePackage" into an environment with site.USER_BASE customized to -'/myappenv', do the following:: +'/myappenv', do the following: + +.. tabs:: + + .. code-tab:: shell Unix/macOS + + $ export PYTHONUSERBASE=/myappenv + $ python -m pip install --user SomePackage - export PYTHONUSERBASE=/myappenv - pip install --user SomePackage + .. code-tab:: shell Windows + C:\> set PYTHONUSERBASE=c:/myappenv + C:\> py -m pip install --user SomePackage ``pip install --user`` follows four rules: @@ -689,54 +859,105 @@ To install "SomePackage" into an environment with site.USER_BASE customized to To make the rules clearer, here are some examples: -From within a ``--no-site-packages`` virtualenv (i.e. the default kind):: +From within a ``--no-site-packages`` virtualenv (i.e. the default kind): + +.. tabs:: + + .. code-tab:: shell Unix/macOS + + $ python -m pip install --user SomePackage + Can not perform a '--user' install. User site-packages are not visible in this virtualenv. - $ pip install --user SomePackage - Can not perform a '--user' install. User site-packages are not visible in this virtualenv. + .. code-tab:: shell Windows + + C:\> py -m pip install --user SomePackage + Can not perform a '--user' install. User site-packages are not visible in this virtualenv. From within a ``--system-site-packages`` virtualenv where ``SomePackage==0.3`` -is already installed in the virtualenv:: +is already installed in the virtualenv: - $ pip install --user SomePackage==0.4 - Will not install to the user site because it will lack sys.path precedence +.. tabs:: + .. code-tab:: shell Unix/macOS -From within a real python, where ``SomePackage`` is *not* installed globally:: + $ python -m pip install --user SomePackage==0.4 + Will not install to the user site because it will lack sys.path precedence - $ pip install --user SomePackage - [...] - Successfully installed SomePackage + .. code-tab:: shell Windows + C:\> py -m pip install --user SomePackage==0.4 + Will not install to the user site because it will lack sys.path precedence -From within a real python, where ``SomePackage`` *is* installed globally, but -is *not* the latest version:: +From within a real python, where ``SomePackage`` is *not* installed globally: - $ pip install --user SomePackage - [...] - Requirement already satisfied (use --upgrade to upgrade) +.. tabs:: - $ pip install --user --upgrade SomePackage - [...] - Successfully installed SomePackage + .. code-tab:: shell Unix/macOS + $ python -m pip install --user SomePackage + [...] + Successfully installed SomePackage -From within a real python, where ``SomePackage`` *is* installed globally, and -is the latest version:: + .. code-tab:: shell Windows + + C:\> py -m pip install --user SomePackage + [...] + Successfully installed SomePackage + +From within a real python, where ``SomePackage`` *is* installed globally, but +is *not* the latest version: - $ pip install --user SomePackage - [...] - Requirement already satisfied (use --upgrade to upgrade) +.. tabs:: - $ pip install --user --upgrade SomePackage - [...] - Requirement already up-to-date: SomePackage + .. code-tab:: shell Unix/macOS - # force the install - $ pip install --user --ignore-installed SomePackage - [...] - Successfully installed SomePackage + $ python -m pip install --user SomePackage + [...] + Requirement already satisfied (use --upgrade to upgrade) + $ python -m pip install --user --upgrade SomePackage + [...] + Successfully installed SomePackage + .. code-tab:: shell Windows + + C:\> py -m pip install --user SomePackage + [...] + Requirement already satisfied (use --upgrade to upgrade) + C:\> py -m pip install --user --upgrade SomePackage + [...] + Successfully installed SomePackage + +From within a real python, where ``SomePackage`` *is* installed globally, and +is the latest version: + +.. tabs:: + + .. code-tab:: shell Unix/macOS + + $ python -m pip install --user SomePackage + [...] + Requirement already satisfied (use --upgrade to upgrade) + $ python -m pip install --user --upgrade SomePackage + [...] + Requirement already up-to-date: SomePackage + # force the install + $ python -m pip install --user --ignore-installed SomePackage + [...] + Successfully installed SomePackage + + .. code-tab:: shell Windows + + C:\> py -m pip install --user SomePackage + [...] + Requirement already satisfied (use --upgrade to upgrade) + C:\> py -m pip install --user --upgrade SomePackage + [...] + Requirement already up-to-date: SomePackage + # force the install + C:\> py -m pip install --user --ignore-installed SomePackage + [...] + Successfully installed SomePackage .. _`Repeatability`: @@ -801,7 +1022,7 @@ index servers are unavailable and avoids time-consuming recompilation. Create an archive like this:: $ tempdir=$(mktemp -d /tmp/wheelhouse-XXXXX) - $ pip wheel -r requirements.txt --wheel-dir=$tempdir + $ python -m pip wheel -r requirements.txt --wheel-dir=$tempdir $ cwd=`pwd` $ (cd "$tempdir"; tar -cjvf "$cwd/bundled.tar.bz2" *) @@ -809,10 +1030,10 @@ You can then install from the archive like this:: $ tempdir=$(mktemp -d /tmp/wheelhouse-XXXXX) $ (cd $tempdir; tar -xvf /path/to/bundled.tar.bz2) - $ pip install --force-reinstall --ignore-installed --upgrade --no-index --no-deps $tempdir/* + $ python -m pip install --force-reinstall --ignore-installed --upgrade --no-index --no-deps $tempdir/* Note that compiled packages are typically OS- and architecture-specific, so -these archives are not necessarily portable across machines. +these archives are not necessarily portable across macOShines. Hash-checking mode can be used along with this method to ensure that future archives are built with identical packages. @@ -842,10 +1063,18 @@ Understanding your error message When you get a ``ResolutionImpossible`` error, you might see something like this: -.. code-block:: console +.. tabs:: + + .. code-tab:: shell Unix/macOS + + $ python -m pip install package_coffee==0.44.1 package_tea==4.3.0 + + .. code-tab:: shell Windows + + C:\> py -m pip install package_coffee==0.44.1 package_tea==4.3.0 + +:: - $ pip install package_coffee==0.44.1 package_tea==4.3.0 - ... Due to conflicting dependencies pip cannot install package_coffee and package_tea: - package_coffee depends on package_water<3.0.0,>=2.4.2 @@ -936,7 +1165,7 @@ the same version of ``package_water``, you might consider: (e.g. ``pip install "package_coffee>0.44.*" "package_tea>4.0.0"``) - Asking pip to install *any* version of ``package_coffee`` and ``package_tea`` by removing the version specifiers altogether (e.g. - ``pip install package_coffee package_tea``) + ``python -m pip install package_coffee package_tea``) In the second case, pip will automatically find a version of both ``package_coffee`` and ``package_tea`` that depend on the same version of @@ -946,9 +1175,17 @@ In the second case, pip will automatically find a version of both - ``package_tea 4.3.0`` which *also* depends on ``package_water 2.6.1`` If you want to prioritize one package over another, you can add version -specifiers to *only* the more important package:: +specifiers to *only* the more important package: + +.. tabs:: + + .. code-tab:: shell Unix/macOS + + $ python -m pip install package_coffee==0.44.1b0 package_tea + + .. code-tab:: shell Windows - pip install package_coffee==0.44.1b0 package_tea + C:\> py -m pip install package_coffee==0.44.1b0 package_tea This will result in: diff --git a/news/7311.doc b/news/7311.doc index f08d30a06e5..6ed2c420489 100644 --- a/news/7311.doc +++ b/news/7311.doc @@ -1 +1 @@ -Use ``python -m`` to run pip in the docs +Add OS tabs for OS-specific commands. From 55c06d181b0844e3efcd9c21339cf05f8875083c Mon Sep 17 00:00:00 2001 From: Srinivas Nyayapati <shireenrao@gmail.com> Date: Thu, 30 Jul 2020 03:03:16 -0400 Subject: [PATCH 2518/3170] WIP - use code-block within group-tab for consistent look --- docs/html/installing.rst | 12 +- docs/html/quickstart.rst | 156 +++++++----- docs/html/user_guide.rst | 505 ++++++++++++++++++++++++--------------- 3 files changed, 409 insertions(+), 264 deletions(-) diff --git a/docs/html/installing.rst b/docs/html/installing.rst index 511e4d02ec6..919f4760619 100644 --- a/docs/html/installing.rst +++ b/docs/html/installing.rst @@ -99,13 +99,17 @@ Upgrading pip .. tabs:: - .. code-tab:: shell Unix/macOS + .. group-tab:: Unix/macOS - $ python -m pip install -U pip + .. code-block:: shell - .. code-tab:: shell Windows + $ python -m pip install -U pip - C:\> py -m pip install -U pip + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py -m pip install -U pip .. _compatibility-requirements: diff --git a/docs/html/quickstart.rst b/docs/html/quickstart.rst index e8754d661ae..bae9e2286ad 100644 --- a/docs/html/quickstart.rst +++ b/docs/html/quickstart.rst @@ -8,17 +8,21 @@ Install a package from `PyPI`_: .. tabs:: - .. code-tab:: shell Unix/macOS + .. group-tab:: Unix/macOS - $ python -m pip install SomePackage - [...] - Successfully installed SomePackage + .. code-block:: shell - .. code-tab:: shell Windows + $ python -m pip install SomePackage + [...] + Successfully installed SomePackage - C:\> py -m pip install SomePackage - [...] - Successfully installed SomePackage + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py -m pip install SomePackage + [...] + Successfully installed SomePackage Install a package that's already been downloaded from `PyPI`_ or @@ -27,100 +31,120 @@ network connection: .. tabs:: - .. code-tab:: shell Unix/macOS + .. group-tab:: Unix/macOS + + .. code-block:: shell - $ python -m pip install SomePackage-1.0-py2.py3-none-any.whl - [...] - Successfully installed SomePackage + $ python -m pip install SomePackage-1.0-py2.py3-none-any.whl + [...] + Successfully installed SomePackage - .. code-tab:: shell Windows + .. group-tab:: Windows - C:\> py -m pip install SomePackage-1.0-py2.py3-none-any.whl - [...] - Successfully installed SomePackage + .. code-block:: shell + + C:\> py -m pip install SomePackage-1.0-py2.py3-none-any.whl + [...] + Successfully installed SomePackage Show what files were installed: .. tabs:: - .. code-tab:: shell Unix/macOS + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip show --files SomePackage + Name: SomePackage + Version: 1.0 + Location: /my/env/lib/pythonx.x/site-packages + Files: + ../somepackage/__init__.py + [...] - $ python -m pip show --files SomePackage - Name: SomePackage - Version: 1.0 - Location: /my/env/lib/pythonx.x/site-packages - Files: - ../somepackage/__init__.py - [...] + .. group-tab:: Windows - .. code-tab:: shell Windows + .. code-block:: shell - C:\> py -m pip show --files SomePackage - Name: SomePackage - Version: 1.0 - Location: /my/env/lib/pythonx.x/site-packages - Files: - ../somepackage/__init__.py - [...] + C:\> py -m pip show --files SomePackage + Name: SomePackage + Version: 1.0 + Location: /my/env/lib/pythonx.x/site-packages + Files: + ../somepackage/__init__.py + [...] List what packages are outdated: .. tabs:: - .. code-tab:: shell Unix/macOS + .. group-tab:: Unix/macOS - $ python -m pip list --outdated - SomePackage (Current: 1.0 Latest: 2.0) + .. code-block:: shell - .. code-tab:: shell Windows + $ python -m pip list --outdated + SomePackage (Current: 1.0 Latest: 2.0) - C:\> py -m pip list --outdated - SomePackage (Current: 1.0 Latest: 2.0) + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py -m pip list --outdated + SomePackage (Current: 1.0 Latest: 2.0) Upgrade a package: .. tabs:: - .. code-tab:: shell Unix/macOS + .. group-tab:: Unix/macOS + + .. code-block:: shell - $ python -m pip install --upgrade SomePackage - [...] - Found existing installation: SomePackage 1.0 - Uninstalling SomePackage: - Successfully uninstalled SomePackage - Running setup.py install for SomePackage - Successfully installed SomePackage + $ python -m pip install --upgrade SomePackage + [...] + Found existing installation: SomePackage 1.0 + Uninstalling SomePackage: + Successfully uninstalled SomePackage + Running setup.py install for SomePackage + Successfully installed SomePackage - .. code-tab:: shell Windows + .. group-tab:: Windows - C:\> py -m pip install --upgrade SomePackage - [...] - Found existing installation: SomePackage 1.0 - Uninstalling SomePackage: - Successfully uninstalled SomePackage - Running setup.py install for SomePackage - Successfully installed SomePackage + .. code-block:: shell + + C:\> py -m pip install --upgrade SomePackage + [...] + Found existing installation: SomePackage 1.0 + Uninstalling SomePackage: + Successfully uninstalled SomePackage + Running setup.py install for SomePackage + Successfully installed SomePackage Uninstall a package: .. tabs:: - .. code-tab:: shell Unix/macOS + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip uninstall SomePackage + Uninstalling SomePackage: + /my/env/lib/pythonx.x/site-packages/somepackage + Proceed (y/n)? y + Successfully uninstalled SomePackage - $ python -m pip uninstall SomePackage - Uninstalling SomePackage: - /my/env/lib/pythonx.x/site-packages/somepackage - Proceed (y/n)? y - Successfully uninstalled SomePackage + .. group-tab:: Windows - .. code-tab:: shell Windows + .. code-block:: shell - C:\> py -m pip uninstall SomePackage - Uninstalling SomePackage: - /my/env/lib/pythonx.x/site-packages/somepackage - Proceed (y/n)? y - Successfully uninstalled SomePackage + C:\> py -m pip uninstall SomePackage + Uninstalling SomePackage: + /my/env/lib/pythonx.x/site-packages/somepackage + Proceed (y/n)? y + Successfully uninstalled SomePackage .. _PyPI: https://pypi.org/ diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 99162ccf272..177baec3c21 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -13,20 +13,24 @@ to your system, which can be run from the command prompt as follows: .. tabs:: - .. code-tab:: shell Unix/macOS + .. group-tab:: Unix/macOS - $ python -m pip <pip arguments> + .. code-block:: shell - # python -m pip executes pip using the Python interpreter you - # specified as python. So '/usr/bin/python3.7 -m pip' means - # you are executing pip for your interpreter located at /usr/bin/python3.7. + $ python -m pip <pip arguments> - .. code-tab:: shell Windows + ``python -m pip`` executes pip using the Python interpreter you + specified as python. So ``/usr/bin/python3.7 -m pip`` means + you are executing pip for your interpreter located at /usr/bin/python3.7. - C:\> py -m pip <pip arguments> + .. group-tab:: Windows - # py -m pip executes pip using the latest Python interpreter you - # have installed. For more details, see https://docs.python.org/3/using/windows.html#launcher. + .. code-block:: shell + + C:\> py -m pip <pip arguments> + + ``py -m pip`` executes pip using the latest Python interpreter you + have installed. For more details, read the `Python Windows launcher`_ docs. Installing Packages @@ -41,17 +45,21 @@ Specifiers` .. tabs:: - .. code-tab:: shell Unix/macOS + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip install SomePackage # latest version + $ python -m pip install SomePackage==1.0.4 # specific version + $ python -m pip install 'SomePackage>=1.0.4' # minimum version - $ python -m pip install SomePackage # latest version - $ python -m pip install SomePackage==1.0.4 # specific version - $ python -m pip install 'SomePackage>=1.0.4' # minimum version + .. group-tab:: Windows - .. code-tab:: shell Windows + .. code-block:: shell - C:\> py -m pip install SomePackage # latest version - C:\> py -m pip install SomePackage==1.0.4 # specific version - C:\> py -m pip install 'SomePackage>=1.0.4' # minimum version + C:\> py -m pip install SomePackage # latest version + C:\> py -m pip install SomePackage==1.0.4 # specific version + C:\> py -m pip install 'SomePackage>=1.0.4' # minimum version For more information and examples, see the :ref:`pip install` reference. @@ -156,13 +164,17 @@ installed using :ref:`pip install` like so: .. tabs:: - .. code-tab:: shell Unix/macOS + .. group-tab:: Unix/macOS - $ python -m pip install -r requirements.txt + .. code-block:: shell - .. code-tab:: shell Windows + $ python -m pip install -r requirements.txt - C:\> py -m pip install -r requirements.txt + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py -m pip install -r requirements.txt Details on the format of the files are here: :ref:`Requirements File Format`. @@ -179,15 +191,19 @@ In practice, there are 4 common uses of Requirements files: .. tabs:: - .. code-tab:: shell Unix/macOS + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip freeze > requirements.txt + $ python -m pip install -r requirements.txt - $ python -m pip freeze > requirements.txt - $ python -m pip install -r requirements.txt + .. group-tab:: Windows - .. code-tab:: shell Windows + .. code-block:: shell - C:\> py -m pip freeze > requirements.txt - C:\> py -m pip install -r requirements.txt + C:\> py -m pip freeze > requirements.txt + C:\> py -m pip install -r requirements.txt 2. Requirements files are used to force pip to properly resolve dependencies. As it is now, pip `doesn't have true dependency resolution @@ -256,13 +272,17 @@ Use a constraints file like so: .. tabs:: - .. code-tab:: shell Unix/macOS + .. group-tab:: Unix/macOS - $ python -m pip install -c constraints.txt + .. code-block:: shell - .. code-tab:: shell Windows + $ python -m pip install -c constraints.txt - C:\> py -m pip install -c constraints.txt + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py -m pip install -c constraints.txt Constraints files are used for exactly the same reason as requirements files when you don't know exactly what things you want to install. For instance, say @@ -302,13 +322,17 @@ To install directly from a wheel archive: .. tabs:: - .. code-tab:: shell Unix/macOS + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip install SomePackage-1.0-py2.py3-none-any.whl - $ python -m pip install SomePackage-1.0-py2.py3-none-any.whl + .. group-tab:: Windows - .. code-tab:: shell Windows + .. code-block:: shell - C:\> py -m pip install SomePackage-1.0-py2.py3-none-any.whl + C:\> py -m pip install SomePackage-1.0-py2.py3-none-any.whl For the cases where wheels are not available, pip offers :ref:`pip wheel` as a @@ -323,28 +347,36 @@ directory: .. tabs:: - .. code-tab:: shell Unix/macOS + .. group-tab:: Unix/macOS - $ python -m pip install wheel - $ python -m pip wheel --wheel-dir=/local/wheels -r requirements.txt + .. code-block:: shell - .. code-tab:: shell Windows + $ python -m pip install wheel + $ python -m pip wheel --wheel-dir=/local/wheels -r requirements.txt - C:\> py -m pip install wheel - C:\> py -m pip wheel --wheel-dir=/local/wheels -r requirements.txt + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py -m pip install wheel + C:\> py -m pip wheel --wheel-dir=/local/wheels -r requirements.txt And *then* to install those requirements just using your local directory of wheels (and not from PyPI): .. tabs:: - .. code-tab:: shell Unix/macOS + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip install --no-index --find-links=/local/wheels -r requirements.txt - $ python -m pip install --no-index --find-links=/local/wheels -r requirements.txt + .. group-tab:: Windows - .. code-tab:: shell Windows + .. code-block:: shell - C:\> py -m pip install --no-index --find-links=/local/wheels -r requirements.txt + C:\> py -m pip install --no-index --find-links=/local/wheels -r requirements.txt Uninstalling Packages @@ -354,13 +386,17 @@ pip is able to uninstall most packages like so: .. tabs:: - .. code-tab:: shell Unix/macOS + .. group-tab:: Unix/macOS - $ python -m pip uninstall SomePackage + .. code-block:: shell - .. code-tab:: shell Windows + $ python -m pip uninstall SomePackage - C:\> py -m pip uninstall SomePackage + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py -m pip uninstall SomePackage pip also performs an automatic uninstall of an old version of a package @@ -376,60 +412,72 @@ To list installed packages: .. tabs:: - .. code-tab:: shell Unix/macOS + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip list + docutils (0.9.1) + Jinja2 (2.6) + Pygments (1.5) + Sphinx (1.1.2) - $ python -m pip list - docutils (0.9.1) - Jinja2 (2.6) - Pygments (1.5) - Sphinx (1.1.2) + .. group-tab:: Windows - .. code-tab:: shell Windows + .. code-block:: shell - C:\> py -m pip list - docutils (0.9.1) - Jinja2 (2.6) - Pygments (1.5) - Sphinx (1.1.2) + C:\> py -m pip list + docutils (0.9.1) + Jinja2 (2.6) + Pygments (1.5) + Sphinx (1.1.2) To list outdated packages, and show the latest version available: .. tabs:: - .. code-tab:: shell Unix/macOS + .. group-tab:: Unix/macOS - $ python -m pip list --outdated - docutils (Current: 0.9.1 Latest: 0.10) - Sphinx (Current: 1.1.2 Latest: 1.1.3) + .. code-block:: shell - .. code-tab:: shell Windows + $ python -m pip list --outdated + docutils (Current: 0.9.1 Latest: 0.10) + Sphinx (Current: 1.1.2 Latest: 1.1.3) - C:\> py -m pip list --outdated - docutils (Current: 0.9.1 Latest: 0.10) - Sphinx (Current: 1.1.2 Latest: 1.1.3) + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py -m pip list --outdated + docutils (Current: 0.9.1 Latest: 0.10) + Sphinx (Current: 1.1.2 Latest: 1.1.3) To show details about an installed package: .. tabs:: - .. code-tab:: shell Unix/macOS + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip show sphinx + --- + Name: Sphinx + Version: 1.1.3 + Location: /my/env/lib/pythonx.x/site-packages + Requires: Pygments, Jinja2, docutils - $ python -m pip show sphinx - --- - Name: Sphinx - Version: 1.1.3 - Location: /my/env/lib/pythonx.x/site-packages - Requires: Pygments, Jinja2, docutils + .. group-tab:: Windows - .. code-tab:: shell Windows + .. code-block:: shell - C:\> py -m pip show sphinx - --- - Name: Sphinx - Version: 1.1.3 - Location: /my/env/lib/pythonx.x/site-packages - Requires: Pygments, Jinja2, docutils + C:\> py -m pip show sphinx + --- + Name: Sphinx + Version: 1.1.3 + Location: /my/env/lib/pythonx.x/site-packages + Requires: Pygments, Jinja2, docutils For more information and examples, see the :ref:`pip list` and :ref:`pip show` @@ -444,13 +492,17 @@ command: .. tabs:: - .. code-tab:: shell Unix/macOS + .. group-tab:: Unix/macOS - $ python -m pip search "query" + .. code-block:: shell - .. code-tab:: shell Windows + $ python -m pip search "query" - C:\> py -m pip search "query" + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py -m pip search "query" The query will be used to search the names and summaries of all packages. @@ -614,51 +666,67 @@ For example, to set the default timeout: .. tabs:: - .. code-tab:: shell Unix/macOS + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ export PIP_DEFAULT_TIMEOUT=60 - $ export PIP_DEFAULT_TIMEOUT=60 + .. group-tab:: Windows - .. code-tab:: shell Windows + .. code-block:: shell - C:\> set PIP_DEFAULT_TIMEOUT=60 + C:\> set PIP_DEFAULT_TIMEOUT=60 This is the same as passing the option to pip directly: .. tabs:: - .. code-tab:: shell Unix/macOS + .. group-tab:: Unix/macOS - $ python -m pip --default-timeout=60 [...] + .. code-block:: shell - .. code-tab:: shell Windows + $ python -m pip --default-timeout=60 [...] - C:\> py -m pip --default-timeout=60 [...] + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py -m pip --default-timeout=60 [...] For command line options which can be repeated, use a space to separate multiple values. For example: .. tabs:: - .. code-tab:: shell Unix/macOS + .. group-tab:: Unix/macOS + + .. code-block:: shell - $ export PIP_FIND_LINKS="http://mirror1.example.com http://mirror2.example.com" + $ export PIP_FIND_LINKS="http://mirror1.example.com http://mirror2.example.com" - .. code-tab:: shell Windows + .. group-tab:: Windows - C:\> set PIP_FIND_LINKS="http://mirror1.example.com http://mirror2.example.com" + .. code-block:: shell + + C:\> set PIP_FIND_LINKS="http://mirror1.example.com http://mirror2.example.com" is the same as calling: .. tabs:: - .. code-tab:: shell Unix/macOS + .. group-tab:: Unix/macOS + + .. code-block:: shell - $ python -m pip install --find-links=http://mirror1.example.com --find-links=http://mirror2.example.com + $ python -m pip install --find-links=http://mirror1.example.com --find-links=http://mirror2.example.com - .. code-tab:: shell Windows + .. group-tab:: Windows - C:\> py -m pip install --find-links=http://mirror1.example.com --find-links=http://mirror2.example.com + .. code-block:: shell + + C:\> py -m pip install --find-links=http://mirror1.example.com --find-links=http://mirror2.example.com Options that do not take a value, but can be repeated (such as ``--verbose``) @@ -733,13 +801,17 @@ First, download the archives that fulfill your requirements: .. tabs:: - .. code-tab:: shell Unix/macOS + .. group-tab:: Unix/macOS + + .. code-block:: shell - $ python -m pip download --destination-directory DIR -r requirements.txt + $ python -m pip download --destination-directory DIR -r requirements.txt - .. code-tab:: shell Windows + .. group-tab:: Windows - C:\> py -m pip download --destination-directory DIR -r requirements.txt + .. code-block:: shell + + C:\> py -m pip download --destination-directory DIR -r requirements.txt Note that ``pip download`` will look in your wheel cache first, before @@ -750,26 +822,34 @@ this instead: .. tabs:: - .. code-tab:: shell Unix/macOS + .. group-tab:: Unix/macOS + + .. code-block:: shell - $ python -m pip wheel --wheel-dir DIR -r requirements.txt + $ python -m pip wheel --wheel-dir DIR -r requirements.txt - .. code-tab:: shell Windows + .. group-tab:: Windows - C:\> py -m pip wheel --wheel-dir DIR -r requirements.txt + .. code-block:: shell + + C:\> py -m pip wheel --wheel-dir DIR -r requirements.txt Then, to install from local only, you'll be using :ref:`--find-links <install_--find-links>` and :ref:`--no-index <install_--no-index>` like so: .. tabs:: - .. code-tab:: shell Unix/macOS + .. group-tab:: Unix/macOS + + .. code-block:: shell - $ python -m pip install --no-index --find-links=DIR -r requirements.txt + $ python -m pip install --no-index --find-links=DIR -r requirements.txt - .. code-tab:: shell Windows + .. group-tab:: Windows - C:\> -m pip install --no-index --find-links=DIR -r requirements.txt + .. code-block:: shell + + C:\> -m pip install --no-index --find-links=DIR -r requirements.txt "Only if needed" Recursive Upgrade @@ -792,15 +872,19 @@ behaviour was: .. tabs:: - .. code-tab:: shell Unix/macOS + .. group-tab:: Unix/macOS + + .. code-block:: shell - $ python -m pip install --upgrade --no-deps SomePackage - $ python -m pip install SomePackage + $ python -m pip install --upgrade --no-deps SomePackage + $ python -m pip install SomePackage - .. code-tab:: shell Windows + .. group-tab:: Windows - C:\> py -m pip install --upgrade --no-deps SomePackage - C:\> py -m pip install SomePackage + .. code-block:: shell + + C:\> py -m pip install --upgrade --no-deps SomePackage + C:\> py -m pip install SomePackage A proposal for an ``upgrade-all`` command is being considered as a safer @@ -828,15 +912,19 @@ To install "SomePackage" into an environment with site.USER_BASE customized to .. tabs:: - .. code-tab:: shell Unix/macOS + .. group-tab:: Unix/macOS + + .. code-block:: shell - $ export PYTHONUSERBASE=/myappenv - $ python -m pip install --user SomePackage + $ export PYTHONUSERBASE=/myappenv + $ python -m pip install --user SomePackage - .. code-tab:: shell Windows + .. group-tab:: Windows - C:\> set PYTHONUSERBASE=c:/myappenv - C:\> py -m pip install --user SomePackage + .. code-block:: shell + + C:\> set PYTHONUSERBASE=c:/myappenv + C:\> py -m pip install --user SomePackage ``pip install --user`` follows four rules: @@ -863,15 +951,19 @@ From within a ``--no-site-packages`` virtualenv (i.e. the default kind): .. tabs:: - .. code-tab:: shell Unix/macOS + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip install --user SomePackage + Can not perform a '--user' install. User site-packages are not visible in this virtualenv. - $ python -m pip install --user SomePackage - Can not perform a '--user' install. User site-packages are not visible in this virtualenv. + .. group-tab:: Windows - .. code-tab:: shell Windows + .. code-block:: shell - C:\> py -m pip install --user SomePackage - Can not perform a '--user' install. User site-packages are not visible in this virtualenv. + C:\> py -m pip install --user SomePackage + Can not perform a '--user' install. User site-packages are not visible in this virtualenv. From within a ``--system-site-packages`` virtualenv where ``SomePackage==0.3`` @@ -879,85 +971,101 @@ is already installed in the virtualenv: .. tabs:: - .. code-tab:: shell Unix/macOS + .. group-tab:: Unix/macOS - $ python -m pip install --user SomePackage==0.4 - Will not install to the user site because it will lack sys.path precedence + .. code-block:: shell - .. code-tab:: shell Windows + $ python -m pip install --user SomePackage==0.4 + Will not install to the user site because it will lack sys.path precedence - C:\> py -m pip install --user SomePackage==0.4 - Will not install to the user site because it will lack sys.path precedence + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py -m pip install --user SomePackage==0.4 + Will not install to the user site because it will lack sys.path precedence From within a real python, where ``SomePackage`` is *not* installed globally: .. tabs:: - .. code-tab:: shell Unix/macOS + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip install --user SomePackage + [...] + Successfully installed SomePackage - $ python -m pip install --user SomePackage - [...] - Successfully installed SomePackage + .. group-tab:: Windows - .. code-tab:: shell Windows + .. code-block:: shell - C:\> py -m pip install --user SomePackage - [...] - Successfully installed SomePackage + C:\> py -m pip install --user SomePackage + [...] + Successfully installed SomePackage From within a real python, where ``SomePackage`` *is* installed globally, but is *not* the latest version: .. tabs:: - .. code-tab:: shell Unix/macOS + .. group-tab:: Unix/macOS - $ python -m pip install --user SomePackage - [...] - Requirement already satisfied (use --upgrade to upgrade) - $ python -m pip install --user --upgrade SomePackage - [...] - Successfully installed SomePackage + .. code-block:: shell - .. code-tab:: shell Windows + $ python -m pip install --user SomePackage + [...] + Requirement already satisfied (use --upgrade to upgrade) + $ python -m pip install --user --upgrade SomePackage + [...] + Successfully installed SomePackage - C:\> py -m pip install --user SomePackage - [...] - Requirement already satisfied (use --upgrade to upgrade) - C:\> py -m pip install --user --upgrade SomePackage - [...] - Successfully installed SomePackage + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py -m pip install --user SomePackage + [...] + Requirement already satisfied (use --upgrade to upgrade) + C:\> py -m pip install --user --upgrade SomePackage + [...] + Successfully installed SomePackage From within a real python, where ``SomePackage`` *is* installed globally, and is the latest version: .. tabs:: - .. code-tab:: shell Unix/macOS - - $ python -m pip install --user SomePackage - [...] - Requirement already satisfied (use --upgrade to upgrade) - $ python -m pip install --user --upgrade SomePackage - [...] - Requirement already up-to-date: SomePackage - # force the install - $ python -m pip install --user --ignore-installed SomePackage - [...] - Successfully installed SomePackage - - .. code-tab:: shell Windows - - C:\> py -m pip install --user SomePackage - [...] - Requirement already satisfied (use --upgrade to upgrade) - C:\> py -m pip install --user --upgrade SomePackage - [...] - Requirement already up-to-date: SomePackage - # force the install - C:\> py -m pip install --user --ignore-installed SomePackage - [...] - Successfully installed SomePackage + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip install --user SomePackage + [...] + Requirement already satisfied (use --upgrade to upgrade) + $ python -m pip install --user --upgrade SomePackage + [...] + Requirement already up-to-date: SomePackage + # force the install + $ python -m pip install --user --ignore-installed SomePackage + [...] + Successfully installed SomePackage + + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py -m pip install --user SomePackage + [...] + Requirement already satisfied (use --upgrade to upgrade) + C:\> py -m pip install --user --upgrade SomePackage + [...] + Requirement already up-to-date: SomePackage + # force the install + C:\> py -m pip install --user --ignore-installed SomePackage + [...] + Successfully installed SomePackage .. _`Repeatability`: @@ -1065,13 +1173,17 @@ like this: .. tabs:: - .. code-tab:: shell Unix/macOS + .. group-tab:: Unix/macOS + + .. code-block:: shell - $ python -m pip install package_coffee==0.44.1 package_tea==4.3.0 + $ python -m pip install package_coffee==0.44.1 package_tea==4.3.0 - .. code-tab:: shell Windows + .. group-tab:: Windows - C:\> py -m pip install package_coffee==0.44.1 package_tea==4.3.0 + .. code-block:: shell + + C:\> py -m pip install package_coffee==0.44.1 package_tea==4.3.0 :: @@ -1179,13 +1291,17 @@ specifiers to *only* the more important package: .. tabs:: - .. code-tab:: shell Unix/macOS + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip install package_coffee==0.44.1b0 package_tea - $ python -m pip install package_coffee==0.44.1b0 package_tea + .. group-tab:: Windows - .. code-tab:: shell Windows + .. code-block:: shell - C:\> py -m pip install package_coffee==0.44.1b0 package_tea + C:\> py -m pip install package_coffee==0.44.1b0 package_tea This will result in: @@ -1562,3 +1678,4 @@ announcements on the `low-traffic packaging announcements list`_ and .. _the official Python blog: https://blog.python.org/ .. _requests: https://requests.readthedocs.io/en/master/user/authentication/#netrc-authentication .. _Python standard library: https://docs.python.org/3/library/netrc.html +.. _Python Windows launcher: https://docs.python.org/3/using/windows.html#launcher From bcd0450158455049cc644b8ad439a1d296b02edc Mon Sep 17 00:00:00 2001 From: Srinivas Nyayapati <shireenrao@gmail.com> Date: Mon, 3 Aug 2020 23:30:43 -0400 Subject: [PATCH 2519/3170] WIP - update getting-started and pip ref docs --- docs/html/development/getting-started.rst | 22 +++++++++++++++++----- docs/html/reference/pip.rst | 13 +++++++++++-- docs/html/user_guide.rst | 8 ++++---- 3 files changed, 32 insertions(+), 11 deletions(-) diff --git a/docs/html/development/getting-started.rst b/docs/html/development/getting-started.rst index 326543202f6..9810bfaa4e9 100644 --- a/docs/html/development/getting-started.rst +++ b/docs/html/development/getting-started.rst @@ -38,13 +38,25 @@ To run the pip executable from your source tree during development, install pip locally using editable installation (inside a virtualenv). You can then invoke your local source tree pip normally. -.. code-block:: console +.. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ virtualenv venv # You can also use "python -m venv venv" from python3.3+ + $ source venv/bin/activate + $ python -m pip install -e . + $ python -m pip --version + + .. group-tab:: Windows - $ virtualenv venv # You can also use "python -m venv venv" from python3.3+ - $ source venv/bin/activate - $ python -m pip install -e . - $ python -m pip --version + .. code-block:: shell + C:\> virtualenv venv # You can also use "py -m venv venv" from python3.3+ + C:\> source venv/bin/activate + C:\> py -m pip install -e . + C:\> py -m pip --version Running Tests ============= diff --git a/docs/html/reference/pip.rst b/docs/html/reference/pip.rst index 9c218f3557d..9ca06e2c725 100644 --- a/docs/html/reference/pip.rst +++ b/docs/html/reference/pip.rst @@ -7,10 +7,19 @@ pip Usage ***** -:: +.. tabs:: - pip <command> [options] + .. group-tab:: Unix/macOS + .. code-block:: shell + + $ python -m pip <command> [options] + + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py -m pip <command> [options] Description *********** diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 177baec3c21..41cbb640407 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -531,7 +531,7 @@ all users) configuration: * On Unix the default configuration file is: :file:`$HOME/.config/pip/pip.conf` which respects the ``XDG_CONFIG_HOME`` environment variable. -* On macOSOS the configuration file is +* On macOS the configuration file is :file:`$HOME/Library/Application Support/pip/pip.conf` if directory ``$HOME/Library/Application Support/pip`` exists else :file:`$HOME/.config/pip/pip.conf`. @@ -540,7 +540,7 @@ all users) configuration: There are also a legacy per-user configuration file which is also respected, these are located at: -* On Unix and macOSOS the configuration file is: :file:`$HOME/.pip/pip.conf` +* On Unix and macOS the configuration file is: :file:`$HOME/.pip/pip.conf` * On Windows the configuration file is: :file:`%HOME%\\pip\\pip.ini` You can set a custom path location for this config file using the environment @@ -548,7 +548,7 @@ variable ``PIP_CONFIG_FILE``. **Inside a virtualenv**: -* On Unix and macOSOS the file is :file:`$VIRTUAL_ENV/pip.conf` +* On Unix and macOS the file is :file:`$VIRTUAL_ENV/pip.conf` * On Windows the file is: :file:`%VIRTUAL_ENV%\\pip.ini` **Global**: @@ -557,7 +557,7 @@ variable ``PIP_CONFIG_FILE``. it may be in a "pip" subdirectory of any of the paths set in the environment variable ``XDG_CONFIG_DIRS`` (if it exists), for example :file:`/etc/xdg/pip/pip.conf`. -* On macOSOS the file is: :file:`/Library/Application Support/pip/pip.conf` +* On macOS the file is: :file:`/Library/Application Support/pip/pip.conf` * On Windows XP the file is: :file:`C:\\Documents and Settings\\All Users\\Application Data\\pip\\pip.ini` * On Windows 7 and later the file is hidden, but writeable at From da3b7e05786efda511c275ce7e6f0bae1c91c005 Mon Sep 17 00:00:00 2001 From: Srinivas Nyayapati <shireenrao@gmail.com> Date: Tue, 4 Aug 2020 12:59:10 -0400 Subject: [PATCH 2520/3170] WIP - update pip-command-usage to take optional arguments and update pip install docs --- docs/html/reference/pip_install.rst | 440 +++++++++++++++++++++++----- docs/pip_sphinxext.py | 6 +- 2 files changed, 370 insertions(+), 76 deletions(-) diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index fd962cd3658..69ea7627538 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -10,7 +10,16 @@ pip install Usage ===== -.. pip-command-usage:: install +.. tabs:: + + .. group-tab:: Unix/macOS + + .. pip-command-usage:: install $ python -m pip + + .. group-tab:: Windows + + .. pip-command-usage:: install C:\> py -m pip + Description @@ -89,15 +98,33 @@ implementation (which might possibly change later) has it such that the first encountered member of the cycle is installed last. For instance, if quux depends on foo which depends on bar which depends on baz, -which depends on foo:: +which depends on foo: + +.. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip install quux + ... + Installing collected packages baz, bar, foo, quux - pip install quux - ... - Installing collected packages baz, bar, foo, quux + $ python -m pip install bar + ... + Installing collected packages foo, baz, bar - pip install bar - ... - Installing collected packages foo, baz, bar + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py -m pip install quux + ... + Installing collected packages baz, bar, foo, quux + + C:\> py -m pip install bar + ... + Installing collected packages foo, baz, bar Prior to v6.1.0, pip made no commitments about install order. @@ -387,9 +414,21 @@ If your repository layout is:: └── some_file some_other_file -Then, to install from this repository, the syntax would be:: +Then, to install from this repository, the syntax would be: + +.. tabs:: + + .. group-tab:: Unix/macOS - $ pip install -e "vcs+protocol://repo_url/#egg=pkg&subdirectory=pkg_dir" + .. code-block:: shell + + $ python -m pip install -e "vcs+protocol://repo_url/#egg=pkg&subdirectory=pkg_dir" + + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py -m pip install -e "vcs+protocol://repo_url/#egg=pkg&subdirectory=pkg_dir" Git @@ -636,17 +675,38 @@ against any requirement not only checks that hash but also activates a global .. _`--require-hashes`: Hash-checking mode can be forced on with the ``--require-hashes`` command-line -option:: +option: + +.. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip install --require-hashes -r requirements.txt + ... + Hashes are required in --require-hashes mode (implicitly on when a hash is + specified for any package). These requirements were missing hashes, + leaving them open to tampering. These are the hashes the downloaded + archives actually had. You can add lines like these to your requirements + files to prevent tampering. + pyelasticsearch==1.0 --hash=sha256:44ddfb1225054d7d6b1d02e9338e7d4809be94edbe9929a2ec0807d38df993fa + more-itertools==2.2 --hash=sha256:93e62e05c7ad3da1a233def6731e8285156701e3419a5fe279017c429ec67ce0 + + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py -m pip install --require-hashes -r requirements.txt + ... + Hashes are required in --require-hashes mode (implicitly on when a hash is + specified for any package). These requirements were missing hashes, + leaving them open to tampering. These are the hashes the downloaded + archives actually had. You can add lines like these to your requirements + files to prevent tampering. + pyelasticsearch==1.0 --hash=sha256:44ddfb1225054d7d6b1d02e9338e7d4809be94edbe9929a2ec0807d38df993fa + more-itertools==2.2 --hash=sha256:93e62e05c7ad3da1a233def6731e8285156701e3419a5fe279017c429ec67ce0 - $ pip install --require-hashes -r requirements.txt - ... - Hashes are required in --require-hashes mode (implicitly on when a hash is - specified for any package). These requirements were missing hashes, - leaving them open to tampering. These are the hashes the downloaded - archives actually had. You can add lines like these to your requirements - files to prevent tampering. - pyelasticsearch==1.0 --hash=sha256:44ddfb1225054d7d6b1d02e9338e7d4809be94edbe9929a2ec0807d38df993fa - more-itertools==2.2 --hash=sha256:93e62e05c7ad3da1a233def6731e8285156701e3419a5fe279017c429ec67ce0 This can be useful in deploy scripts, to ensure that the author of the requirements file provided hashes. It is also a convenient way to bootstrap @@ -692,14 +752,38 @@ Hash-checking mode also works with :ref:`pip download` and :ref:`pip wheel`. A as your project evolves. To be safe, install your project using pip and :ref:`--no-deps <install_--no-deps>`. - Instead of ``python setup.py develop``, use... :: + Instead of ``python setup.py develop``, use... + + .. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip install --no-deps -e . + + .. group-tab:: Windows - pip install --no-deps -e . + .. code-block:: shell - Instead of ``python setup.py install``, use... :: + C:\> py -m pip install --no-deps -e . - pip install --no-deps . + Instead of ``python setup.py install``, use... + + .. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip install --no-deps . + + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py -m pip install --no-deps . Hashes from PyPI ^^^^^^^^^^^^^^^^ @@ -717,9 +801,22 @@ Local project installs ---------------------- pip supports installing local project in both regular mode and editable mode. -You can install local projects by specifying the project path to pip:: +You can install local projects by specifying the project path to pip: + +.. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip install path/to/SomeProject + + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py -m pip install path/to/SomeProject -$ pip install path/to/SomeProject During regular installation, pip will copy the entire project directory to a temporary location and install from there. The exception is that pip will @@ -736,10 +833,24 @@ being copied. <https://setuptools.readthedocs.io/en/latest/setuptools.html#development-mode>`_ installs. -You can install local projects or VCS projects in "editable" mode:: +You can install local projects or VCS projects in "editable" mode: + +.. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip install -e path/to/SomeProject + $ python -m pip install -e git+http://repo/my_project.git#egg=SomeProject + + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py -m pip install -e path/to/SomeProject + C:\> py -m pip install -e git+http://repo/my_project.git#egg=SomeProject -$ pip install -e path/to/SomeProject -$ pip install -e git+http://repo/my_project.git#egg=SomeProject (See the :ref:`VCS Support` section above for more information on VCS-related syntax.) @@ -846,113 +957,292 @@ Examples #. Install ``SomePackage`` and its dependencies from `PyPI`_ using :ref:`Requirement Specifiers` - :: + .. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell - $ pip install SomePackage # latest version - $ pip install SomePackage==1.0.4 # specific version - $ pip install 'SomePackage>=1.0.4' # minimum version + $ python -m pip install SomePackage # latest version + $ python -m pip install SomePackage==1.0.4 # specific version + $ python -m pip install 'SomePackage>=1.0.4' # minimum version + + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py -m pip install SomePackage # latest version + C:\> py -m pip install SomePackage==1.0.4 # specific version + C:\> py -m pip install 'SomePackage>=1.0.4' # minimum version #. Install a list of requirements specified in a file. See the :ref:`Requirements files <Requirements Files>`. - :: + .. tabs:: + + .. group-tab:: Unix/macOS - $ pip install -r requirements.txt + .. code-block:: shell + + $ python -m pip install -r requirements.txt + + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py -m pip install -r requirements.txt #. Upgrade an already installed ``SomePackage`` to the latest from PyPI. - :: + .. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip install --upgrade SomePackage + + .. group-tab:: Windows - $ pip install --upgrade SomePackage + .. code-block:: shell + + C:\> py -m pip install --upgrade SomePackage #. Install a local project in "editable" mode. See the section on :ref:`Editable Installs <editable-installs>`. - :: + .. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip install -e . # project in current directory + $ python -m pip install -e path/to/project # project in another directory + + .. group-tab:: Windows - $ pip install -e . # project in current directory - $ pip install -e path/to/project # project in another directory + .. code-block:: shell + + C:\> py -m pip install -e . # project in current directory + C:\> py -m pip install -e path/to/project # project in another directory #. Install a project from VCS - :: + .. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip install SomeProject@git+https://git.repo/some_pkg.git@1.3.1 - $ pip install SomeProject@git+https://git.repo/some_pkg.git@1.3.1 + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py -m pip install SomeProject@git+https://git.repo/some_pkg.git@1.3.1 #. Install a project from VCS in "editable" mode. See the sections on :ref:`VCS Support <VCS Support>` and :ref:`Editable Installs <editable-installs>`. - :: + .. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip install -e git+https://git.repo/some_pkg.git#egg=SomePackage # from git + $ python -m pip install -e hg+https://hg.repo/some_pkg.git#egg=SomePackage # from mercurial + $ python -m python -m pip install -e svn+svn://svn.repo/some_pkg/trunk/#egg=SomePackage # from svn + $ python -m pip install -e git+https://git.repo/some_pkg.git@feature#egg=SomePackage # from 'feature' branch + $ python -m pip install -e "git+https://git.repo/some_repo.git#egg=subdir&subdirectory=subdir_path" # install a python package from a repo subdirectory + + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py -m pip install -e git+https://git.repo/some_pkg.git#egg=SomePackage # from git + C:\> py -m pip install -e hg+https://hg.repo/some_pkg.git#egg=SomePackage # from mercurial + C:\> py -m pip install -e svn+svn://svn.repo/some_pkg/trunk/#egg=SomePackage # from svn + C:\> py -m pip install -e git+https://git.repo/some_pkg.git@feature#egg=SomePackage # from 'feature' branch + C:\> py -m pip install -e "git+https://git.repo/some_repo.git#egg=subdir&subdirectory=subdir_path" # install a python package from a repo subdirectory - $ pip install -e git+https://git.repo/some_pkg.git#egg=SomePackage # from git - $ pip install -e hg+https://hg.repo/some_pkg.git#egg=SomePackage # from mercurial - $ pip install -e svn+svn://svn.repo/some_pkg/trunk/#egg=SomePackage # from svn - $ pip install -e git+https://git.repo/some_pkg.git@feature#egg=SomePackage # from 'feature' branch - $ pip install -e "git+https://git.repo/some_repo.git#egg=subdir&subdirectory=subdir_path" # install a python package from a repo subdirectory #. Install a package with `setuptools extras`_. - :: + .. tabs:: + + .. group-tab:: Unix/macOS - $ pip install SomePackage[PDF] - $ pip install "SomePackage[PDF] @ git+https://git.repo/SomePackage@master#subdirectory=subdir_path" - $ pip install .[PDF] # project in current directory - $ pip install SomePackage[PDF]==3.0 - $ pip install SomePackage[PDF,EPUB] # multiple extras + .. code-block:: shell + + $ python -m pip install SomePackage[PDF] + $ python -m pip install "SomePackage[PDF] @ git+https://git.repo/SomePackage@master#subdirectory=subdir_path" + $ python -m pip install .[PDF] # project in current directory + $ python -m pip install SomePackage[PDF]==3.0 + $ python -m pip install SomePackage[PDF,EPUB] # multiple extras + + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py -m pip install SomePackage[PDF] + C:\> py -m pip install "SomePackage[PDF] @ git+https://git.repo/SomePackage@master#subdirectory=subdir_path" + C:\> py -m pip install .[PDF] # project in current directory + C:\> py -m pip install SomePackage[PDF]==3.0 + C:\> py -m pip install SomePackage[PDF,EPUB] # multiple extras #. Install a particular source archive file. - :: + .. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip install ./downloads/SomePackage-1.0.4.tar.gz + $ python -m pip install http://my.package.repo/SomePackage-1.0.4.zip + + .. group-tab:: Windows + + .. code-block:: shell - $ pip install ./downloads/SomePackage-1.0.4.tar.gz - $ pip install http://my.package.repo/SomePackage-1.0.4.zip + C:\> py -m pip install ./downloads/SomePackage-1.0.4.tar.gz + C:\> py -m pip install http://my.package.repo/SomePackage-1.0.4.zip #. Install a particular source archive file following :pep:`440` direct references. - :: + .. tabs:: - $ pip install SomeProject@http://my.package.repo/SomeProject-1.2.3-py33-none-any.whl - $ pip install "SomeProject @ http://my.package.repo/SomeProject-1.2.3-py33-none-any.whl" - $ pip install SomeProject@http://my.package.repo/1.2.3.tar.gz + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip install SomeProject@http://my.package.repo/SomeProject-1.2.3-py33-none-any.whl + $ python -m pip install "SomeProject @ http://my.package.repo/SomeProject-1.2.3-py33-none-any.whl" + $ python -m pip install SomeProject@http://my.package.repo/1.2.3.tar.gz + + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py -m pip install SomeProject@http://my.package.repo/SomeProject-1.2.3-py33-none-any.whl + C:\> py -m pip install "SomeProject @ http://my.package.repo/SomeProject-1.2.3-py33-none-any.whl" + C:\> py -m pip install SomeProject@http://my.package.repo/1.2.3.tar.gz #. Install from alternative package repositories. - Install from a different index, and not `PyPI`_ :: + Install from a different index, and not `PyPI`_ + + .. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell - $ pip install --index-url http://my.package.repo/simple/ SomePackage + $ python -m pip install --index-url http://my.package.repo/simple/ SomePackage - Search an additional index during install, in addition to `PyPI`_ :: + .. group-tab:: Windows - $ pip install --extra-index-url http://my.package.repo/simple SomePackage + .. code-block:: shell - Install from a local flat directory containing archives (and don't scan indexes):: + C:\> py -m pip install --index-url http://my.package.repo/simple/ SomePackage - $ pip install --no-index --find-links=file:///local/dir/ SomePackage - $ pip install --no-index --find-links=/local/dir/ SomePackage - $ pip install --no-index --find-links=relative/dir/ SomePackage + + Search an additional index during install, in addition to `PyPI`_ + + .. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip install --extra-index-url http://my.package.repo/simple SomePackage + + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py -m pip install --extra-index-url http://my.package.repo/simple SomePackage + + + Install from a local flat directory containing archives (and don't scan indexes): + + .. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip install --no-index --find-links=file:///local/dir/ SomePackage + $ python -m pip install --no-index --find-links=/local/dir/ SomePackage + $ python -m pip install --no-index --find-links=relative/dir/ SomePackage + + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py -m pip install --no-index --find-links=file:///local/dir/ SomePackage + C:\> py -m pip install --no-index --find-links=/local/dir/ SomePackage + C:\> py -m pip install --no-index --find-links=relative/dir/ SomePackage #. Find pre-release and development versions, in addition to stable versions. By default, pip only finds stable versions. - :: + .. tabs:: - $ pip install --pre SomePackage + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip install --pre SomePackage + + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py -m pip install --pre SomePackage #. Install packages from source. - Do not use any binary packages:: + Do not use any binary packages + + .. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip install SomePackage1 SomePackage2 --no-binary :all: + + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py -m pip install SomePackage1 SomePackage2 --no-binary :all: + + Specify ``SomePackage1`` to be installed from source: + + .. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip install SomePackage1 SomePackage2 --no-binary SomePackage1 - $ pip install SomePackage1 SomePackage2 --no-binary :all: + .. group-tab:: Windows - Specify ``SomePackage1`` to be installed from source:: + .. code-block:: shell - $ pip install SomePackage1 SomePackage2 --no-binary SomePackage1 + C:\> py -m pip install SomePackage1 SomePackage2 --no-binary SomePackage1 ---- diff --git a/docs/pip_sphinxext.py b/docs/pip_sphinxext.py index 6cc7a2c82ec..0e7571eeae4 100644 --- a/docs/pip_sphinxext.py +++ b/docs/pip_sphinxext.py @@ -15,11 +15,15 @@ class PipCommandUsage(rst.Directive): required_arguments = 1 + optional_arguments = 4 def run(self): cmd = create_command(self.arguments[0]) + pip_cmd = '$ python -m pip' + if len(self.arguments) > 1: + pip_cmd = " ".join(self.arguments[1:]) usage = dedent( - cmd.usage.replace('%prog', 'pip {}'.format(cmd.name)) + cmd.usage.replace('%prog', '{} {}'.format(pip_cmd, cmd.name)) ).strip() node = nodes.literal_block(usage, usage) return [node] From 423ccfd4f13e43fb0bdee16ee0220e95813ff45f Mon Sep 17 00:00:00 2001 From: Srinivas Nyayapati <shireenrao@gmail.com> Date: Wed, 5 Aug 2020 23:18:37 -0400 Subject: [PATCH 2521/3170] WIP upd directive to use cmd prefix + upd ref docs --- docs/html/reference/pip_download.rst | 166 ++++++++++++++++++++------ docs/html/reference/pip_freeze.rst | 56 +++++++-- docs/html/reference/pip_install.rst | 4 +- docs/html/reference/pip_uninstall.rst | 39 ++++-- docs/pip_sphinxext.py | 10 +- 5 files changed, 211 insertions(+), 64 deletions(-) diff --git a/docs/html/reference/pip_download.rst b/docs/html/reference/pip_download.rst index b74b1d24038..d3c217b9480 100644 --- a/docs/html/reference/pip_download.rst +++ b/docs/html/reference/pip_download.rst @@ -11,7 +11,15 @@ pip download Usage ===== -.. pip-command-usage:: download +.. tabs:: + + .. group-tab:: Unix/macOS + + .. pip-command-usage:: download "python -m pip" + + .. group-tab:: Windows + + .. pip-command-usage:: download "py -m pip" Description @@ -56,11 +64,24 @@ Examples #. Download a package and all of its dependencies - :: + .. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip download SomePackage + $ python -m pip download -d . SomePackage # equivalent to above + $ python -m pip download --no-index --find-links=/tmp/wheelhouse -d /tmp/otherwheelhouse SomePackage + + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py -m pip download SomePackage + C:\> py -m pip download -d . SomePackage # equivalent to above + C:\> py -m pip download --no-index --find-links=/tmp/wheelhouse -d /tmp/otherwheelhouse SomePackage - $ pip download SomePackage - $ pip download -d . SomePackage # equivalent to above - $ pip download --no-index --find-links=/tmp/wheelhouse -d /tmp/otherwheelhouse SomePackage #. Download a package and all of its dependencies with OSX specific interpreter constraints. This forces OSX 10.10 or lower compatibility. Since OSX deps are forward compatible, @@ -69,51 +90,118 @@ Examples It will also match deps with platform ``any``. Also force the interpreter version to ``27`` (or more generic, i.e. ``2``) and implementation to ``cp`` (or more generic, i.e. ``py``). - :: + .. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip download \ + --only-binary=:all: \ + --platform macosx-10_10_x86_64 \ + --python-version 27 \ + --implementation cp \ + SomePackage - $ pip download \ - --only-binary=:all: \ - --platform macosx-10_10_x86_64 \ - --python-version 27 \ - --implementation cp \ - SomePackage + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py -m pip download ^ + --only-binary=:all: ^ + --platform macosx-10_10_x86_64 ^ + --python-version 27 ^ + --implementation cp ^ + SomePackage #. Download a package and its dependencies with linux specific constraints. Force the interpreter to be any minor version of py3k, and only accept ``cp34m`` or ``none`` as the abi. - :: + .. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell - $ pip download \ - --only-binary=:all: \ - --platform linux_x86_64 \ - --python-version 3 \ - --implementation cp \ - --abi cp34m \ - SomePackage + $ python -m pip download \ + --only-binary=:all: \ + --platform linux_x86_64 \ + --python-version 3 \ + --implementation cp \ + --abi cp34m \ + SomePackage + + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py -m pip download ^ + --only-binary=:all: ^ + --platform linux_x86_64 ^ + --python-version 3 ^ + --implementation cp ^ + --abi cp34m ^ + SomePackage #. Force platform, implementation, and abi agnostic deps. - :: + .. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip download \ + --only-binary=:all: \ + --platform any \ + --python-version 3 \ + --implementation py \ + --abi none \ + SomePackage + + .. group-tab:: Windows + + .. code-block:: shell - $ pip download \ - --only-binary=:all: \ - --platform any \ - --python-version 3 \ - --implementation py \ - --abi none \ - SomePackage + C:\> py -m pip download ^ + --only-binary=:all: ^ + --platform any ^ + --python-version 3 ^ + --implementation py ^ + --abi none ^ + SomePackage #. Even when overconstrained, this will still correctly fetch the pip universal wheel. - :: - - $ pip download \ - --only-binary=:all: \ - --platform linux_x86_64 \ - --python-version 33 \ - --implementation cp \ - --abi cp34m \ - pip>=8 - $ ls pip-8.1.1-py2.py3-none-any.whl - pip-8.1.1-py2.py3-none-any.whl + .. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip download \ + --only-binary=:all: \ + --platform linux_x86_64 \ + --python-version 33 \ + --implementation cp \ + --abi cp34m \ + pip>=8 + + $ ls pip-8.1.1-py2.py3-none-any.whl + pip-8.1.1-py2.py3-none-any.whl + + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py -m pip download ^ + --only-binary=:all: ^ + --platform linux_x86_64 ^ + --python-version 33 ^ + --implementation cp ^ + --abi cp34m ^ + pip>=8 + + C:\> dir pip-8.1.1-py2.py3-none-any.whl + pip-8.1.1-py2.py3-none-any.whl diff --git a/docs/html/reference/pip_freeze.rst b/docs/html/reference/pip_freeze.rst index 31efd571b5f..cd180e09552 100644 --- a/docs/html/reference/pip_freeze.rst +++ b/docs/html/reference/pip_freeze.rst @@ -11,7 +11,15 @@ pip freeze Usage ===== -.. pip-command-usage:: freeze +.. tabs:: + + .. group-tab:: Unix/macOS + + .. pip-command-usage:: freeze "python -m pip" + + .. group-tab:: Windows + + .. pip-command-usage:: freeze "py -m pip" Description @@ -31,19 +39,45 @@ Examples #. Generate output suitable for a requirements file. - :: + .. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip freeze + docutils==0.11 + Jinja2==2.7.2 + MarkupSafe==0.19 + Pygments==1.6 + Sphinx==1.2.2 - $ pip freeze - docutils==0.11 - Jinja2==2.7.2 - MarkupSafe==0.19 - Pygments==1.6 - Sphinx==1.2.2 + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py -m pip freeze + docutils==0.11 + Jinja2==2.7.2 + MarkupSafe==0.19 + Pygments==1.6 + Sphinx==1.2.2 #. Generate a requirements file and then install from it in another environment. - :: + .. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ env1/bin/python -m pip freeze > requirements.txt + $ env2/bin/python -m pip install -r requirements.txt + + .. group-tab:: Windows + + .. code-block:: shell - $ env1/bin/pip freeze > requirements.txt - $ env2/bin/pip install -r requirements.txt + C:\> env1\bin\python -m pip freeze > requirements.txt + C:\> env2\bin\python -m pip install -r requirements.txt diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index 69ea7627538..67cfd03c61b 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -14,11 +14,11 @@ Usage .. group-tab:: Unix/macOS - .. pip-command-usage:: install $ python -m pip + .. pip-command-usage:: install "python -m pip" .. group-tab:: Windows - .. pip-command-usage:: install C:\> py -m pip + .. pip-command-usage:: install "py -m pip" diff --git a/docs/html/reference/pip_uninstall.rst b/docs/html/reference/pip_uninstall.rst index 67d752d6b97..165ac4cd5b8 100644 --- a/docs/html/reference/pip_uninstall.rst +++ b/docs/html/reference/pip_uninstall.rst @@ -10,7 +10,15 @@ pip uninstall Usage ===== -.. pip-command-usage:: uninstall +.. tabs:: + + .. group-tab:: Unix/macOS + + .. pip-command-usage:: uninstall "python -m pip" + + .. group-tab:: Windows + + .. pip-command-usage:: uninstall "py -m pip" Description @@ -30,11 +38,26 @@ Examples #. Uninstall a package. - :: + .. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip uninstall simplejson + Uninstalling simplejson: + /home/me/env/lib/python2.7/site-packages/simplejson + /home/me/env/lib/python2.7/site-packages/simplejson-2.2.1-py2.7.egg-info + Proceed (y/n)? y + Successfully uninstalled simplejson + + .. group-tab:: Windows + + .. code-block:: shell - $ pip uninstall simplejson - Uninstalling simplejson: - /home/me/env/lib/python2.7/site-packages/simplejson - /home/me/env/lib/python2.7/site-packages/simplejson-2.2.1-py2.7.egg-info - Proceed (y/n)? y - Successfully uninstalled simplejson + C:\> py -m pip uninstall simplejson + Uninstalling simplejson: + /home/me/env/lib/python2.7/site-packages/simplejson + /home/me/env/lib/python2.7/site-packages/simplejson-2.2.1-py2.7.egg-info + Proceed (y/n)? y + Successfully uninstalled simplejson diff --git a/docs/pip_sphinxext.py b/docs/pip_sphinxext.py index 0e7571eeae4..9386d71e796 100644 --- a/docs/pip_sphinxext.py +++ b/docs/pip_sphinxext.py @@ -15,15 +15,17 @@ class PipCommandUsage(rst.Directive): required_arguments = 1 - optional_arguments = 4 + optional_arguments = 3 def run(self): cmd = create_command(self.arguments[0]) - pip_cmd = '$ python -m pip' + cmd_prefix = 'python -m pip' if len(self.arguments) > 1: - pip_cmd = " ".join(self.arguments[1:]) + cmd_prefix = " ".join(self.arguments[1:]) + cmd_prefix = cmd_prefix.strip('"') + cmd_prefix = cmd_prefix.strip("'") usage = dedent( - cmd.usage.replace('%prog', '{} {}'.format(pip_cmd, cmd.name)) + cmd.usage.replace('%prog', '{} {}'.format(cmd_prefix, cmd.name)) ).strip() node = nodes.literal_block(usage, usage) return [node] From 4a6276bfc98d17d523c69e9739e82eeb900a440b Mon Sep 17 00:00:00 2001 From: Srinivas Nyayapati <shireenrao@gmail.com> Date: Thu, 6 Aug 2020 16:06:10 -0400 Subject: [PATCH 2522/3170] Add tabs to ref docs --- docs/html/reference/pip_cache.rst | 10 +- docs/html/reference/pip_check.rst | 79 +++++++++--- docs/html/reference/pip_config.rst | 10 +- docs/html/reference/pip_debug.rst | 10 +- docs/html/reference/pip_hash.rst | 47 +++++-- docs/html/reference/pip_list.rst | 201 +++++++++++++++++++++++------ docs/html/reference/pip_search.rst | 30 ++++- docs/html/reference/pip_show.rst | 178 +++++++++++++++++-------- docs/html/reference/pip_wheel.rst | 75 +++++++++-- 9 files changed, 501 insertions(+), 139 deletions(-) diff --git a/docs/html/reference/pip_cache.rst b/docs/html/reference/pip_cache.rst index 8ad99f65cba..35e0dfcadac 100644 --- a/docs/html/reference/pip_cache.rst +++ b/docs/html/reference/pip_cache.rst @@ -9,7 +9,15 @@ pip cache Usage ***** -.. pip-command-usage:: cache +.. tabs:: + + .. group-tab:: Unix/macOS + + .. pip-command-usage:: cache "python -m pip" + + .. group-tab:: Windows + + .. pip-command-usage:: cache "py -m pip" Description *********** diff --git a/docs/html/reference/pip_check.rst b/docs/html/reference/pip_check.rst index a12d5b3ec78..07d7004753e 100644 --- a/docs/html/reference/pip_check.rst +++ b/docs/html/reference/pip_check.rst @@ -10,7 +10,15 @@ pip check Usage ===== -.. pip-command-usage:: check +.. tabs:: + + .. group-tab:: Unix/macOS + + .. pip-command-usage:: check "python -m pip" + + .. group-tab:: Windows + + .. pip-command-usage:: check "py -m pip" Description @@ -24,27 +32,66 @@ Examples #. If all dependencies are compatible: - :: + .. tabs:: + + .. group-tab:: Unix/macOS - $ pip check - No broken requirements found. - $ echo $? - 0 + .. code-block:: shell + + $ python -m pip check + No broken requirements found. + $ echo $? + 0 + + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py -m pip check + No broken requirements found. + C:\> echo %errorlevel% + 0 #. If a package is missing: - :: + .. tabs:: + + .. group-tab:: Unix/macOS - $ pip check - pyramid 1.5.2 requires WebOb, which is not installed. - $ echo $? - 1 + .. code-block:: shell + + $ python -m pip check + pyramid 1.5.2 requires WebOb, which is not installed. + $ echo $? + 1 + + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py -m pip check + pyramid 1.5.2 requires WebOb, which is not installed. + C:\> echo %errorlevel% + 1 #. If a package has the wrong version: - :: + .. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip check + pyramid 1.5.2 has requirement WebOb>=1.3.1, but you have WebOb 0.8. + $ echo $? + 1 + + .. group-tab:: Windows + + .. code-block:: shell - $ pip check - pyramid 1.5.2 has requirement WebOb>=1.3.1, but you have WebOb 0.8. - $ echo $? - 1 + C:\> py -m pip check + pyramid 1.5.2 has requirement WebOb>=1.3.1, but you have WebOb 0.8. + C:\> echo %errorlevel% + 1 diff --git a/docs/html/reference/pip_config.rst b/docs/html/reference/pip_config.rst index 70d9406c562..d9bf0afc8f5 100644 --- a/docs/html/reference/pip_config.rst +++ b/docs/html/reference/pip_config.rst @@ -11,7 +11,15 @@ pip config Usage ===== -.. pip-command-usage:: config +.. tabs:: + + .. group-tab:: Unix/macOS + + .. pip-command-usage:: config "python -m pip" + + .. group-tab:: Windows + + .. pip-command-usage:: config "py -m pip" Description diff --git a/docs/html/reference/pip_debug.rst b/docs/html/reference/pip_debug.rst index da147bcf2fa..2ef98228aa8 100644 --- a/docs/html/reference/pip_debug.rst +++ b/docs/html/reference/pip_debug.rst @@ -10,7 +10,15 @@ pip debug Usage ===== -.. pip-command-usage:: debug +.. tabs:: + + .. group-tab:: Unix/macOS + + .. pip-command-usage:: debug "python -m pip" + + .. group-tab:: Windows + + .. pip-command-usage:: debug "py -m pip" .. warning:: diff --git a/docs/html/reference/pip_hash.rst b/docs/html/reference/pip_hash.rst index dbf1f3e94f8..7ed39280c7a 100644 --- a/docs/html/reference/pip_hash.rst +++ b/docs/html/reference/pip_hash.rst @@ -10,7 +10,15 @@ pip hash Usage ===== -.. pip-command-usage:: hash +.. tabs:: + + .. group-tab:: Unix/macOS + + .. pip-command-usage:: hash "python -m pip" + + .. group-tab:: Windows + + .. pip-command-usage:: hash "py -m pip" Description @@ -39,13 +47,32 @@ Options Example ======= -Compute the hash of a downloaded archive:: +Compute the hash of a downloaded archive: + +.. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip download SomePackage + Collecting SomePackage + Downloading SomePackage-2.2.tar.gz + Saved ./pip_downloads/SomePackage-2.2.tar.gz + Successfully downloaded SomePackage + $ python -m pip hash ./pip_downloads/SomePackage-2.2.tar.gz + ./pip_downloads/SomePackage-2.2.tar.gz: + --hash=sha256:93e62e05c7ad3da1a233def6731e8285156701e3419a5fe279017c429ec67ce0 + + .. group-tab:: Windows + + .. code-block:: shell - $ pip download SomePackage - Collecting SomePackage - Downloading SomePackage-2.2.tar.gz - Saved ./pip_downloads/SomePackage-2.2.tar.gz - Successfully downloaded SomePackage - $ pip hash ./pip_downloads/SomePackage-2.2.tar.gz - ./pip_downloads/SomePackage-2.2.tar.gz: - --hash=sha256:93e62e05c7ad3da1a233def6731e8285156701e3419a5fe279017c429ec67ce0 + C:\> py -m pip download SomePackage + Collecting SomePackage + Downloading SomePackage-2.2.tar.gz + Saved ./pip_downloads/SomePackage-2.2.tar.gz + Successfully downloaded SomePackage + C:\> py -m pip hash ./pip_downloads/SomePackage-2.2.tar.gz + ./pip_downloads/SomePackage-2.2.tar.gz: + --hash=sha256:93e62e05c7ad3da1a233def6731e8285156701e3419a5fe279017c429ec67ce0 diff --git a/docs/html/reference/pip_list.rst b/docs/html/reference/pip_list.rst index 15d0920a7f2..bfdef9ebe1c 100644 --- a/docs/html/reference/pip_list.rst +++ b/docs/html/reference/pip_list.rst @@ -10,7 +10,15 @@ pip list Usage ===== -.. pip-command-usage:: list +.. tabs:: + + .. group-tab:: Unix/macOS + + .. pip-command-usage:: list "python -m pip" + + .. group-tab:: Windows + + .. pip-command-usage:: list "py -m pip" Description @@ -32,75 +40,182 @@ Examples #. List installed packages. - :: + .. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell - $ pip list - docutils (0.10) - Jinja2 (2.7.2) - MarkupSafe (0.18) - Pygments (1.6) - Sphinx (1.2.1) + $ python -m pip list + docutils (0.10) + Jinja2 (2.7.2) + MarkupSafe (0.18) + Pygments (1.6) + Sphinx (1.2.1) + + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py -m pip list + docutils (0.10) + Jinja2 (2.7.2) + MarkupSafe (0.18) + Pygments (1.6) + Sphinx (1.2.1) #. List outdated packages (excluding editables), and the latest version available. - :: + .. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip list --outdated + docutils (Current: 0.10 Latest: 0.11) + Sphinx (Current: 1.2.1 Latest: 1.2.2) + + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py -m pip list --outdated + docutils (Current: 0.10 Latest: 0.11) + Sphinx (Current: 1.2.1 Latest: 1.2.2) - $ pip list --outdated - docutils (Current: 0.10 Latest: 0.11) - Sphinx (Current: 1.2.1 Latest: 1.2.2) #. List installed packages with column formatting. - :: + .. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip list --format columns + Package Version + ------- ------- + docopt 0.6.2 + idlex 1.13 + jedi 0.9.0 - $ pip list --format columns - Package Version - ------- ------- - docopt 0.6.2 - idlex 1.13 - jedi 0.9.0 + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py -m pip list --format columns + Package Version + ------- ------- + docopt 0.6.2 + idlex 1.13 + jedi 0.9.0 #. List outdated packages with column formatting. - :: + .. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip list -o --format columns + Package Version Latest Type + ---------- ------- ------ ----- + retry 0.8.1 0.9.1 wheel + setuptools 20.6.7 21.0.0 wheel - $ pip list -o --format columns - Package Version Latest Type - ---------- ------- ------ ----- - retry 0.8.1 0.9.1 wheel - setuptools 20.6.7 21.0.0 wheel + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py -m pip list -o --format columns + Package Version Latest Type + ---------- ------- ------ ----- + retry 0.8.1 0.9.1 wheel + setuptools 20.6.7 21.0.0 wheel #. List packages that are not dependencies of other packages. Can be combined with other options. - :: + .. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip list --outdated --not-required + docutils (Current: 0.10 Latest: 0.11) + + .. group-tab:: Windows - $ pip list --outdated --not-required - docutils (Current: 0.10 Latest: 0.11) + .. code-block:: shell + + C:\> py -m pip list --outdated --not-required + docutils (Current: 0.10 Latest: 0.11) #. Use legacy formatting - :: + .. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip list --format=legacy + colorama (0.3.7) + docopt (0.6.2) + idlex (1.13) + jedi (0.9.0) + + .. group-tab:: Windows - $ pip list --format=legacy - colorama (0.3.7) - docopt (0.6.2) - idlex (1.13) - jedi (0.9.0) + .. code-block:: shell + + C:\> py -m pip list --format=legacy + colorama (0.3.7) + docopt (0.6.2) + idlex (1.13) + jedi (0.9.0) #. Use json formatting - :: + .. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip list --format=json + [{'name': 'colorama', 'version': '0.3.7'}, {'name': 'docopt', 'version': '0.6.2'}, ... + + .. group-tab:: Windows + + .. code-block:: shell - $ pip list --format=json - [{'name': 'colorama', 'version': '0.3.7'}, {'name': 'docopt', 'version': '0.6.2'}, ... + C:\> py -m pip list --format=json + [{'name': 'colorama', 'version': '0.3.7'}, {'name': 'docopt', 'version': '0.6.2'}, ... #. Use freeze formatting - :: + .. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip list --format=freeze + colorama==0.3.7 + docopt==0.6.2 + idlex==1.13 + jedi==0.9.0 + + .. group-tab:: Windows + + .. code-block:: shell - $ pip list --format=freeze - colorama==0.3.7 - docopt==0.6.2 - idlex==1.13 - jedi==0.9.0 + C:\> py -m pip list --format=freeze + colorama==0.3.7 + docopt==0.6.2 + idlex==1.13 + jedi==0.9.0 diff --git a/docs/html/reference/pip_search.rst b/docs/html/reference/pip_search.rst index db1bd2be806..e0fc3e25bcb 100644 --- a/docs/html/reference/pip_search.rst +++ b/docs/html/reference/pip_search.rst @@ -10,7 +10,15 @@ pip search Usage ===== -.. pip-command-usage:: search +.. tabs:: + + .. group-tab:: Unix/macOS + + .. pip-command-usage:: search "python -m pip" + + .. group-tab:: Windows + + .. pip-command-usage:: search "py -m pip" Description @@ -30,8 +38,20 @@ Examples #. Search for "peppercorn" - :: + .. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip search peppercorn + pepperedform - Helpers for using peppercorn with formprocess. + peppercorn - A library for converting a token stream into [...] + + .. group-tab:: Windows + + .. code-block:: shell - $ pip search peppercorn - pepperedform - Helpers for using peppercorn with formprocess. - peppercorn - A library for converting a token stream into [...] + C:\> py -m pip search peppercorn + pepperedform - Helpers for using peppercorn with formprocess. + peppercorn - A library for converting a token stream into [...] diff --git a/docs/html/reference/pip_show.rst b/docs/html/reference/pip_show.rst index e9568b6b098..ae9182f547f 100644 --- a/docs/html/reference/pip_show.rst +++ b/docs/html/reference/pip_show.rst @@ -10,7 +10,15 @@ pip show Usage ===== -.. pip-command-usage:: show +.. tabs:: + + .. group-tab:: Unix/macOS + + .. pip-command-usage:: show "python -m pip" + + .. group-tab:: Windows + + .. pip-command-usage:: show "py -m pip" Description @@ -30,58 +38,124 @@ Examples #. Show information about a package: - :: + .. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip show sphinx + Name: Sphinx + Version: 1.4.5 + Summary: Python documentation generator + Home-page: http://sphinx-doc.org/ + Author: Georg Brandl + Author-email: georg@python.org + License: BSD + Location: /my/env/lib/python2.7/site-packages + Requires: docutils, snowballstemmer, alabaster, Pygments, imagesize, Jinja2, babel, six + + .. group-tab:: Windows + + .. code-block:: shell - $ pip show sphinx - Name: Sphinx - Version: 1.4.5 - Summary: Python documentation generator - Home-page: http://sphinx-doc.org/ - Author: Georg Brandl - Author-email: georg@python.org - License: BSD - Location: /my/env/lib/python2.7/site-packages - Requires: docutils, snowballstemmer, alabaster, Pygments, imagesize, Jinja2, babel, six + C:\> py -m pip show sphinx + Name: Sphinx + Version: 1.4.5 + Summary: Python documentation generator + Home-page: http://sphinx-doc.org/ + Author: Georg Brandl + Author-email: georg@python.org + License: BSD + Location: /my/env/lib/python2.7/site-packages + Requires: docutils, snowballstemmer, alabaster, Pygments, imagesize, Jinja2, babel, six #. Show all information about a package - :: - - $ pip show --verbose sphinx - Name: Sphinx - Version: 1.4.5 - Summary: Python documentation generator - Home-page: http://sphinx-doc.org/ - Author: Georg Brandl - Author-email: georg@python.org - License: BSD - Location: /my/env/lib/python2.7/site-packages - Requires: docutils, snowballstemmer, alabaster, Pygments, imagesize, Jinja2, babel, six - Metadata-Version: 2.0 - Installer: - Classifiers: - Development Status :: 5 - Production/Stable - Environment :: Console - Environment :: Web Environment - Intended Audience :: Developers - Intended Audience :: Education - License :: OSI Approved :: BSD License - Operating System :: OS Independent - Programming Language :: Python - Programming Language :: Python :: 2 - Programming Language :: Python :: 3 - Framework :: Sphinx - Framework :: Sphinx :: Extension - Framework :: Sphinx :: Theme - Topic :: Documentation - Topic :: Documentation :: Sphinx - Topic :: Text Processing - Topic :: Utilities - Entry-points: - [console_scripts] - sphinx-apidoc = sphinx.apidoc:main - sphinx-autogen = sphinx.ext.autosummary.generate:main - sphinx-build = sphinx:main - sphinx-quickstart = sphinx.quickstart:main - [distutils.commands] - build_sphinx = sphinx.setup_command:BuildDoc + .. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip show --verbose sphinx + Name: Sphinx + Version: 1.4.5 + Summary: Python documentation generator + Home-page: http://sphinx-doc.org/ + Author: Georg Brandl + Author-email: georg@python.org + License: BSD + Location: /my/env/lib/python2.7/site-packages + Requires: docutils, snowballstemmer, alabaster, Pygments, imagesize, Jinja2, babel, six + Metadata-Version: 2.0 + Installer: + Classifiers: + Development Status :: 5 - Production/Stable + Environment :: Console + Environment :: Web Environment + Intended Audience :: Developers + Intended Audience :: Education + License :: OSI Approved :: BSD License + Operating System :: OS Independent + Programming Language :: Python + Programming Language :: Python :: 2 + Programming Language :: Python :: 3 + Framework :: Sphinx + Framework :: Sphinx :: Extension + Framework :: Sphinx :: Theme + Topic :: Documentation + Topic :: Documentation :: Sphinx + Topic :: Text Processing + Topic :: Utilities + Entry-points: + [console_scripts] + sphinx-apidoc = sphinx.apidoc:main + sphinx-autogen = sphinx.ext.autosummary.generate:main + sphinx-build = sphinx:main + sphinx-quickstart = sphinx.quickstart:main + [distutils.commands] + build_sphinx = sphinx.setup_command:BuildDoc + + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py -m pip show --verbose sphinx + Name: Sphinx + Version: 1.4.5 + Summary: Python documentation generator + Home-page: http://sphinx-doc.org/ + Author: Georg Brandl + Author-email: georg@python.org + License: BSD + Location: /my/env/lib/python2.7/site-packages + Requires: docutils, snowballstemmer, alabaster, Pygments, imagesize, Jinja2, babel, six + Metadata-Version: 2.0 + Installer: + Classifiers: + Development Status :: 5 - Production/Stable + Environment :: Console + Environment :: Web Environment + Intended Audience :: Developers + Intended Audience :: Education + License :: OSI Approved :: BSD License + Operating System :: OS Independent + Programming Language :: Python + Programming Language :: Python :: 2 + Programming Language :: Python :: 3 + Framework :: Sphinx + Framework :: Sphinx :: Extension + Framework :: Sphinx :: Theme + Topic :: Documentation + Topic :: Documentation :: Sphinx + Topic :: Text Processing + Topic :: Utilities + Entry-points: + [console_scripts] + sphinx-apidoc = sphinx.apidoc:main + sphinx-autogen = sphinx.ext.autosummary.generate:main + sphinx-build = sphinx:main + sphinx-quickstart = sphinx.quickstart:main + [distutils.commands] + build_sphinx = sphinx.setup_command:BuildDoc diff --git a/docs/html/reference/pip_wheel.rst b/docs/html/reference/pip_wheel.rst index dc32dda463c..0a11c6e254c 100644 --- a/docs/html/reference/pip_wheel.rst +++ b/docs/html/reference/pip_wheel.rst @@ -11,7 +11,15 @@ pip wheel Usage ===== -.. pip-command-usage:: wheel +.. tabs:: + + .. group-tab:: Unix/macOS + + .. pip-command-usage:: wheel "python -m pip" + + .. group-tab:: Windows + + .. pip-command-usage:: wheel "py -m pip" Description @@ -24,9 +32,22 @@ Build System Interface ---------------------- In order for pip to build a wheel, ``setup.py`` must implement the -``bdist_wheel`` command with the following syntax:: +``bdist_wheel`` command with the following syntax: + +.. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python setup.py bdist_wheel -d TARGET + + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py setup.py bdist_wheel -d TARGET - python setup.py bdist_wheel -d TARGET This command must create a wheel compatible with the invoking Python interpreter, and save that wheel in the directory TARGET. @@ -39,9 +60,22 @@ Customising the build It is possible using ``--global-option`` to include additional build commands with their arguments in the ``setup.py`` command. This is currently the only way to influence the building of C extensions from the command line. For -example:: +example: + +.. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip wheel --global-option bdist_ext --global-option -DFOO wheel + + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py -m pip wheel --global-option bdist_ext --global-option -DFOO wheel - pip wheel --global-option bdist_ext --global-option -DFOO wheel will result in a build command of @@ -69,13 +103,34 @@ Examples #. Build wheels for a requirement (and all its dependencies), and then install - :: + .. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip wheel --wheel-dir=/tmp/wheelhouse SomePackage + $ python -m pip install --no-index --find-links=/tmp/wheelhouse SomePackage - $ pip wheel --wheel-dir=/tmp/wheelhouse SomePackage - $ pip install --no-index --find-links=/tmp/wheelhouse SomePackage + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py -m pip wheel --wheel-dir=/tmp/wheelhouse SomePackage + C:\> py -m pip install --no-index --find-links=/tmp/wheelhouse SomePackage #. Build a wheel for a package from source - :: + .. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python -m pip wheel --no-binary SomePackage SomePackage + + .. group-tab:: Windows + + .. code-block:: shell - $ pip wheel --no-binary SomePackage SomePackage + C:\> py -m pip wheel --no-binary SomePackage SomePackage From 7673712d3e0fd4718367c9a183b89a56605f474d Mon Sep 17 00:00:00 2001 From: Srinivas Nyayapati <shireenrao@gmail.com> Date: Fri, 7 Aug 2020 14:14:23 -0400 Subject: [PATCH 2523/3170] Add tabs to get-pip.py install page --- docs/html/installing.rst | 79 ++++++++++++++++++++++++++++++++----- docs/html/reference/pip.rst | 16 +++++++- 2 files changed, 83 insertions(+), 12 deletions(-) diff --git a/docs/html/installing.rst b/docs/html/installing.rst index 919f4760619..14d5171603a 100644 --- a/docs/html/installing.rst +++ b/docs/html/installing.rst @@ -26,9 +26,21 @@ this link: `get-pip.py curl https://bootstrap.pypa.io/get-pip.py -o get-pip.py Then run the following command in the folder where you -have downloaded ``get-pip.py``:: +have downloaded ``get-pip.py``: - python get-pip.py +.. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python get-pip.py + + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py get-pip.py .. warning:: @@ -67,23 +79,70 @@ get-pip.py options install Options>` and the :ref:`general options <General Options>`. Below are some examples: -Install from local copies of pip and setuptools:: +Install from local copies of pip and setuptools: + +.. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python get-pip.py --no-index --find-links=/local/copies + + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py get-pip.py --no-index --find-links=/local/copies + +Install to the user site [3]_: + +.. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python get-pip.py --user + + .. group-tab:: Windows - python get-pip.py --no-index --find-links=/local/copies + .. code-block:: shell + + C:\> py get-pip.py --user + +Install behind a proxy: + +.. tabs:: -Install to the user site [3]_:: + .. group-tab:: Unix/macOS - python get-pip.py --user + .. code-block:: shell -Install behind a proxy:: + $ python get-pip.py --proxy="http://[user:passwd@]proxy.server:port" - python get-pip.py --proxy="http://[user:passwd@]proxy.server:port" + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py get-pip.py --proxy="http://[user:passwd@]proxy.server:port" ``get-pip.py`` can also be used to install a specified combination of ``pip``, -``setuptools``, and ``wheel`` using the same requirements syntax as pip:: +``setuptools``, and ``wheel`` using the same requirements syntax as pip: + +.. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python get-pip.py pip==9.0.2 wheel==0.30.0 setuptools==28.8.0 + + .. group-tab:: Windows - python get-pip.py pip==9.0.2 wheel==0.30.0 setuptools==28.8.0 + .. code-block:: shell + C:\> py get-pip.py pip==9.0.2 wheel==0.30.0 setuptools==28.8.0 Using Linux Package Managers ============================ diff --git a/docs/html/reference/pip.rst b/docs/html/reference/pip.rst index 9ca06e2c725..3694ca7f59e 100644 --- a/docs/html/reference/pip.rst +++ b/docs/html/reference/pip.rst @@ -230,9 +230,21 @@ Build Options The ``--global-option`` and ``--build-option`` arguments to the ``pip install`` and ``pip wheel`` inject additional arguments into the ``setup.py`` command (``--build-option`` is only available in ``pip wheel``). These arguments are -included in the command as follows:: +included in the command as follows: - python setup.py <global_options> BUILD COMMAND <build_options> +.. tabs:: + + .. group-tab:: Unix/macOS + + .. code-block:: shell + + $ python setup.py <global_options> BUILD COMMAND <build_options> + + .. group-tab:: Windows + + .. code-block:: shell + + C:\> py setup.py <global_options> BUILD COMMAND <build_options> The options are passed unmodified, and presently offer direct access to the distutils command line. Use of ``--global-option`` and ``--build-option`` From c36bd748f3194d885511d58170b24e57f8330b69 Mon Sep 17 00:00:00 2001 From: Srinivas Nyayapati <shireenrao@gmail.com> Date: Tue, 11 Aug 2020 08:50:14 -0400 Subject: [PATCH 2524/3170] fix tox.ini and conf.py to use sphinx_tabs_nowarn --- docs/html/conf.py | 3 +++ tox.ini | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/docs/html/conf.py b/docs/html/conf.py index f0e880f5e4c..9d0b7a2fa79 100644 --- a/docs/html/conf.py +++ b/docs/html/conf.py @@ -135,6 +135,9 @@ 'pypi': ('https://pypi.org/project/%s/', ''), } +# Turn off sphinx build warnings because of sphinx tabs during man pages build +sphinx_tabs_nowarn = True + # -- Options for HTML output -------------------------------------------------- # The theme to use for HTML and HTML Help pages. Major themes that come with diff --git a/tox.ini b/tox.ini index a673b0f598f..82e9abc68d7 100644 --- a/tox.ini +++ b/tox.ini @@ -56,7 +56,7 @@ commands = # can not use a different configuration directory vs source directory on RTD # currently -- https://github.com/rtfd/readthedocs.org/issues/1543. # That is why we have a "-c docs/html" in the next line. - sphinx-build -W -d {envtmpdir}/doctrees/man -b man docs/man docs/build/man -c docs/html -D extensions=sphinx.ext.extlinks,pip_sphinxext,sphinx.ext.intersphinx + sphinx-build -W -d {envtmpdir}/doctrees/man -b man docs/man docs/build/man -c docs/html [testenv:lint] skip_install = True From 28a00633d8614b8042fa29e50fce6d7c6ba278d7 Mon Sep 17 00:00:00 2001 From: Srinivas Nyayapati <shireenrao@gmail.com> Date: Sun, 16 Aug 2020 14:07:09 -0400 Subject: [PATCH 2525/3170] Remove platform prompt when not output is displayed --- docs/html/development/getting-started.rst | 16 +-- docs/html/installing.rst | 24 ++-- docs/html/reference/pip.rst | 4 +- docs/html/reference/pip_download.rst | 92 +++++++------- docs/html/reference/pip_freeze.rst | 8 +- docs/html/reference/pip_install.rst | 148 +++++++++++----------- docs/html/reference/pip_wheel.rst | 20 +-- docs/html/user_guide.rst | 114 ++++++++--------- 8 files changed, 213 insertions(+), 213 deletions(-) diff --git a/docs/html/development/getting-started.rst b/docs/html/development/getting-started.rst index 9810bfaa4e9..1bc4a551621 100644 --- a/docs/html/development/getting-started.rst +++ b/docs/html/development/getting-started.rst @@ -44,19 +44,19 @@ You can then invoke your local source tree pip normally. .. code-block:: shell - $ virtualenv venv # You can also use "python -m venv venv" from python3.3+ - $ source venv/bin/activate - $ python -m pip install -e . - $ python -m pip --version + virtualenv venv # You can also use "python -m venv venv" from python3.3+ + source venv/bin/activate + python -m pip install -e . + python -m pip --version .. group-tab:: Windows .. code-block:: shell - C:\> virtualenv venv # You can also use "py -m venv venv" from python3.3+ - C:\> source venv/bin/activate - C:\> py -m pip install -e . - C:\> py -m pip --version + virtualenv venv # You can also use "py -m venv venv" from python3.3+ + venv\Scripts\activate + py -m pip install -e . + py -m pip --version Running Tests ============= diff --git a/docs/html/installing.rst b/docs/html/installing.rst index 14d5171603a..e6717881ac1 100644 --- a/docs/html/installing.rst +++ b/docs/html/installing.rst @@ -34,13 +34,13 @@ have downloaded ``get-pip.py``: .. code-block:: shell - $ python get-pip.py + python get-pip.py .. group-tab:: Windows .. code-block:: shell - C:\> py get-pip.py + py get-pip.py .. warning:: @@ -87,13 +87,13 @@ Install from local copies of pip and setuptools: .. code-block:: shell - $ python get-pip.py --no-index --find-links=/local/copies + python get-pip.py --no-index --find-links=/local/copies .. group-tab:: Windows .. code-block:: shell - C:\> py get-pip.py --no-index --find-links=/local/copies + py get-pip.py --no-index --find-links=/local/copies Install to the user site [3]_: @@ -103,13 +103,13 @@ Install to the user site [3]_: .. code-block:: shell - $ python get-pip.py --user + python get-pip.py --user .. group-tab:: Windows .. code-block:: shell - C:\> py get-pip.py --user + py get-pip.py --user Install behind a proxy: @@ -119,13 +119,13 @@ Install behind a proxy: .. code-block:: shell - $ python get-pip.py --proxy="http://[user:passwd@]proxy.server:port" + python get-pip.py --proxy="http://[user:passwd@]proxy.server:port" .. group-tab:: Windows .. code-block:: shell - C:\> py get-pip.py --proxy="http://[user:passwd@]proxy.server:port" + py get-pip.py --proxy="http://[user:passwd@]proxy.server:port" ``get-pip.py`` can also be used to install a specified combination of ``pip``, ``setuptools``, and ``wheel`` using the same requirements syntax as pip: @@ -136,13 +136,13 @@ Install behind a proxy: .. code-block:: shell - $ python get-pip.py pip==9.0.2 wheel==0.30.0 setuptools==28.8.0 + python get-pip.py pip==9.0.2 wheel==0.30.0 setuptools==28.8.0 .. group-tab:: Windows .. code-block:: shell - C:\> py get-pip.py pip==9.0.2 wheel==0.30.0 setuptools==28.8.0 + py get-pip.py pip==9.0.2 wheel==0.30.0 setuptools==28.8.0 Using Linux Package Managers ============================ @@ -162,13 +162,13 @@ Upgrading pip .. code-block:: shell - $ python -m pip install -U pip + python -m pip install -U pip .. group-tab:: Windows .. code-block:: shell - C:\> py -m pip install -U pip + py -m pip install -U pip .. _compatibility-requirements: diff --git a/docs/html/reference/pip.rst b/docs/html/reference/pip.rst index 3694ca7f59e..a0c1148bf1e 100644 --- a/docs/html/reference/pip.rst +++ b/docs/html/reference/pip.rst @@ -238,13 +238,13 @@ included in the command as follows: .. code-block:: shell - $ python setup.py <global_options> BUILD COMMAND <build_options> + python setup.py <global_options> BUILD COMMAND <build_options> .. group-tab:: Windows .. code-block:: shell - C:\> py setup.py <global_options> BUILD COMMAND <build_options> + py setup.py <global_options> BUILD COMMAND <build_options> The options are passed unmodified, and presently offer direct access to the distutils command line. Use of ``--global-option`` and ``--build-option`` diff --git a/docs/html/reference/pip_download.rst b/docs/html/reference/pip_download.rst index d3c217b9480..141b6e1fa84 100644 --- a/docs/html/reference/pip_download.rst +++ b/docs/html/reference/pip_download.rst @@ -70,17 +70,17 @@ Examples .. code-block:: shell - $ python -m pip download SomePackage - $ python -m pip download -d . SomePackage # equivalent to above - $ python -m pip download --no-index --find-links=/tmp/wheelhouse -d /tmp/otherwheelhouse SomePackage + python -m pip download SomePackage + python -m pip download -d . SomePackage # equivalent to above + python -m pip download --no-index --find-links=/tmp/wheelhouse -d /tmp/otherwheelhouse SomePackage .. group-tab:: Windows .. code-block:: shell - C:\> py -m pip download SomePackage - C:\> py -m pip download -d . SomePackage # equivalent to above - C:\> py -m pip download --no-index --find-links=/tmp/wheelhouse -d /tmp/otherwheelhouse SomePackage + py -m pip download SomePackage + py -m pip download -d . SomePackage # equivalent to above + py -m pip download --no-index --find-links=/tmp/wheelhouse -d /tmp/otherwheelhouse SomePackage #. Download a package and all of its dependencies with OSX specific interpreter constraints. @@ -96,23 +96,23 @@ Examples .. code-block:: shell - $ python -m pip download \ - --only-binary=:all: \ - --platform macosx-10_10_x86_64 \ - --python-version 27 \ - --implementation cp \ - SomePackage + python -m pip download \ + --only-binary=:all: \ + --platform macosx-10_10_x86_64 \ + --python-version 27 \ + --implementation cp \ + SomePackage .. group-tab:: Windows .. code-block:: shell - C:\> py -m pip download ^ - --only-binary=:all: ^ - --platform macosx-10_10_x86_64 ^ - --python-version 27 ^ - --implementation cp ^ - SomePackage + py -m pip download ^ + --only-binary=:all: ^ + --platform macosx-10_10_x86_64 ^ + --python-version 27 ^ + --implementation cp ^ + SomePackage #. Download a package and its dependencies with linux specific constraints. Force the interpreter to be any minor version of py3k, and only accept @@ -124,25 +124,25 @@ Examples .. code-block:: shell - $ python -m pip download \ - --only-binary=:all: \ - --platform linux_x86_64 \ - --python-version 3 \ - --implementation cp \ - --abi cp34m \ - SomePackage + python -m pip download \ + --only-binary=:all: \ + --platform linux_x86_64 \ + --python-version 3 \ + --implementation cp \ + --abi cp34m \ + SomePackage .. group-tab:: Windows .. code-block:: shell - C:\> py -m pip download ^ - --only-binary=:all: ^ - --platform linux_x86_64 ^ - --python-version 3 ^ - --implementation cp ^ - --abi cp34m ^ - SomePackage + py -m pip download ^ + --only-binary=:all: ^ + --platform linux_x86_64 ^ + --python-version 3 ^ + --implementation cp ^ + --abi cp34m ^ + SomePackage #. Force platform, implementation, and abi agnostic deps. @@ -152,25 +152,25 @@ Examples .. code-block:: shell - $ python -m pip download \ - --only-binary=:all: \ - --platform any \ - --python-version 3 \ - --implementation py \ - --abi none \ - SomePackage + python -m pip download \ + --only-binary=:all: \ + --platform any \ + --python-version 3 \ + --implementation py \ + --abi none \ + SomePackage .. group-tab:: Windows .. code-block:: shell - C:\> py -m pip download ^ - --only-binary=:all: ^ - --platform any ^ - --python-version 3 ^ - --implementation py ^ - --abi none ^ - SomePackage + py -m pip download ^ + --only-binary=:all: ^ + --platform any ^ + --python-version 3 ^ + --implementation py ^ + --abi none ^ + SomePackage #. Even when overconstrained, this will still correctly fetch the pip universal wheel. diff --git a/docs/html/reference/pip_freeze.rst b/docs/html/reference/pip_freeze.rst index cd180e09552..91c80979ac8 100644 --- a/docs/html/reference/pip_freeze.rst +++ b/docs/html/reference/pip_freeze.rst @@ -72,12 +72,12 @@ Examples .. code-block:: shell - $ env1/bin/python -m pip freeze > requirements.txt - $ env2/bin/python -m pip install -r requirements.txt + env1/bin/python -m pip freeze > requirements.txt + env2/bin/python -m pip install -r requirements.txt .. group-tab:: Windows .. code-block:: shell - C:\> env1\bin\python -m pip freeze > requirements.txt - C:\> env2\bin\python -m pip install -r requirements.txt + env1\bin\python -m pip freeze > requirements.txt + env2\bin\python -m pip install -r requirements.txt diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index 67cfd03c61b..b2da2624060 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -422,13 +422,13 @@ Then, to install from this repository, the syntax would be: .. code-block:: shell - $ python -m pip install -e "vcs+protocol://repo_url/#egg=pkg&subdirectory=pkg_dir" + python -m pip install -e "vcs+protocol://repo_url/#egg=pkg&subdirectory=pkg_dir" .. group-tab:: Windows .. code-block:: shell - C:\> py -m pip install -e "vcs+protocol://repo_url/#egg=pkg&subdirectory=pkg_dir" + py -m pip install -e "vcs+protocol://repo_url/#egg=pkg&subdirectory=pkg_dir" Git @@ -760,13 +760,13 @@ Hash-checking mode also works with :ref:`pip download` and :ref:`pip wheel`. A .. code-block:: shell - $ python -m pip install --no-deps -e . + python -m pip install --no-deps -e . .. group-tab:: Windows .. code-block:: shell - C:\> py -m pip install --no-deps -e . + py -m pip install --no-deps -e . Instead of ``python setup.py install``, use... @@ -777,13 +777,13 @@ Hash-checking mode also works with :ref:`pip download` and :ref:`pip wheel`. A .. code-block:: shell - $ python -m pip install --no-deps . + python -m pip install --no-deps . .. group-tab:: Windows .. code-block:: shell - C:\> py -m pip install --no-deps . + py -m pip install --no-deps . Hashes from PyPI ^^^^^^^^^^^^^^^^ @@ -809,13 +809,13 @@ You can install local projects by specifying the project path to pip: .. code-block:: shell - $ python -m pip install path/to/SomeProject + python -m pip install path/to/SomeProject .. group-tab:: Windows .. code-block:: shell - C:\> py -m pip install path/to/SomeProject + py -m pip install path/to/SomeProject During regular installation, pip will copy the entire project directory to a @@ -841,15 +841,15 @@ You can install local projects or VCS projects in "editable" mode: .. code-block:: shell - $ python -m pip install -e path/to/SomeProject - $ python -m pip install -e git+http://repo/my_project.git#egg=SomeProject + python -m pip install -e path/to/SomeProject + python -m pip install -e git+http://repo/my_project.git#egg=SomeProject .. group-tab:: Windows .. code-block:: shell - C:\> py -m pip install -e path/to/SomeProject - C:\> py -m pip install -e git+http://repo/my_project.git#egg=SomeProject + py -m pip install -e path/to/SomeProject + py -m pip install -e git+http://repo/my_project.git#egg=SomeProject (See the :ref:`VCS Support` section above for more information on VCS-related syntax.) @@ -963,17 +963,17 @@ Examples .. code-block:: shell - $ python -m pip install SomePackage # latest version - $ python -m pip install SomePackage==1.0.4 # specific version - $ python -m pip install 'SomePackage>=1.0.4' # minimum version + python -m pip install SomePackage # latest version + python -m pip install SomePackage==1.0.4 # specific version + python -m pip install 'SomePackage>=1.0.4' # minimum version .. group-tab:: Windows .. code-block:: shell - C:\> py -m pip install SomePackage # latest version - C:\> py -m pip install SomePackage==1.0.4 # specific version - C:\> py -m pip install 'SomePackage>=1.0.4' # minimum version + py -m pip install SomePackage # latest version + py -m pip install SomePackage==1.0.4 # specific version + py -m pip install 'SomePackage>=1.0.4' # minimum version #. Install a list of requirements specified in a file. See the :ref:`Requirements files <Requirements Files>`. @@ -984,13 +984,13 @@ Examples .. code-block:: shell - $ python -m pip install -r requirements.txt + python -m pip install -r requirements.txt .. group-tab:: Windows .. code-block:: shell - C:\> py -m pip install -r requirements.txt + py -m pip install -r requirements.txt #. Upgrade an already installed ``SomePackage`` to the latest from PyPI. @@ -1001,13 +1001,13 @@ Examples .. code-block:: shell - $ python -m pip install --upgrade SomePackage + python -m pip install --upgrade SomePackage .. group-tab:: Windows .. code-block:: shell - C:\> py -m pip install --upgrade SomePackage + py -m pip install --upgrade SomePackage #. Install a local project in "editable" mode. See the section on :ref:`Editable Installs <editable-installs>`. @@ -1018,15 +1018,15 @@ Examples .. code-block:: shell - $ python -m pip install -e . # project in current directory - $ python -m pip install -e path/to/project # project in another directory + python -m pip install -e . # project in current directory + python -m pip install -e path/to/project # project in another directory .. group-tab:: Windows .. code-block:: shell - C:\> py -m pip install -e . # project in current directory - C:\> py -m pip install -e path/to/project # project in another directory + py -m pip install -e . # project in current directory + py -m pip install -e path/to/project # project in another directory #. Install a project from VCS @@ -1037,13 +1037,13 @@ Examples .. code-block:: shell - $ python -m pip install SomeProject@git+https://git.repo/some_pkg.git@1.3.1 + python -m pip install SomeProject@git+https://git.repo/some_pkg.git@1.3.1 .. group-tab:: Windows .. code-block:: shell - C:\> py -m pip install SomeProject@git+https://git.repo/some_pkg.git@1.3.1 + py -m pip install SomeProject@git+https://git.repo/some_pkg.git@1.3.1 #. Install a project from VCS in "editable" mode. See the sections on :ref:`VCS Support <VCS Support>` and :ref:`Editable Installs <editable-installs>`. @@ -1054,21 +1054,21 @@ Examples .. code-block:: shell - $ python -m pip install -e git+https://git.repo/some_pkg.git#egg=SomePackage # from git - $ python -m pip install -e hg+https://hg.repo/some_pkg.git#egg=SomePackage # from mercurial - $ python -m python -m pip install -e svn+svn://svn.repo/some_pkg/trunk/#egg=SomePackage # from svn - $ python -m pip install -e git+https://git.repo/some_pkg.git@feature#egg=SomePackage # from 'feature' branch - $ python -m pip install -e "git+https://git.repo/some_repo.git#egg=subdir&subdirectory=subdir_path" # install a python package from a repo subdirectory + python -m pip install -e git+https://git.repo/some_pkg.git#egg=SomePackage # from git + python -m pip install -e hg+https://hg.repo/some_pkg.git#egg=SomePackage # from mercurial + python -m python -m pip install -e svn+svn://svn.repo/some_pkg/trunk/#egg=SomePackage # from svn + python -m pip install -e git+https://git.repo/some_pkg.git@feature#egg=SomePackage # from 'feature' branch + python -m pip install -e "git+https://git.repo/some_repo.git#egg=subdir&subdirectory=subdir_path" # install a python package from a repo subdirectory .. group-tab:: Windows .. code-block:: shell - C:\> py -m pip install -e git+https://git.repo/some_pkg.git#egg=SomePackage # from git - C:\> py -m pip install -e hg+https://hg.repo/some_pkg.git#egg=SomePackage # from mercurial - C:\> py -m pip install -e svn+svn://svn.repo/some_pkg/trunk/#egg=SomePackage # from svn - C:\> py -m pip install -e git+https://git.repo/some_pkg.git@feature#egg=SomePackage # from 'feature' branch - C:\> py -m pip install -e "git+https://git.repo/some_repo.git#egg=subdir&subdirectory=subdir_path" # install a python package from a repo subdirectory + py -m pip install -e git+https://git.repo/some_pkg.git#egg=SomePackage # from git + py -m pip install -e hg+https://hg.repo/some_pkg.git#egg=SomePackage # from mercurial + py -m pip install -e svn+svn://svn.repo/some_pkg/trunk/#egg=SomePackage # from svn + py -m pip install -e git+https://git.repo/some_pkg.git@feature#egg=SomePackage # from 'feature' branch + py -m pip install -e "git+https://git.repo/some_repo.git#egg=subdir&subdirectory=subdir_path" # install a python package from a repo subdirectory #. Install a package with `setuptools extras`_. @@ -1079,21 +1079,21 @@ Examples .. code-block:: shell - $ python -m pip install SomePackage[PDF] - $ python -m pip install "SomePackage[PDF] @ git+https://git.repo/SomePackage@master#subdirectory=subdir_path" - $ python -m pip install .[PDF] # project in current directory - $ python -m pip install SomePackage[PDF]==3.0 - $ python -m pip install SomePackage[PDF,EPUB] # multiple extras + python -m pip install SomePackage[PDF] + python -m pip install "SomePackage[PDF] @ git+https://git.repo/SomePackage@master#subdirectory=subdir_path" + python -m pip install .[PDF] # project in current directory + python -m pip install SomePackage[PDF]==3.0 + python -m pip install SomePackage[PDF,EPUB] # multiple extras .. group-tab:: Windows .. code-block:: shell - C:\> py -m pip install SomePackage[PDF] - C:\> py -m pip install "SomePackage[PDF] @ git+https://git.repo/SomePackage@master#subdirectory=subdir_path" - C:\> py -m pip install .[PDF] # project in current directory - C:\> py -m pip install SomePackage[PDF]==3.0 - C:\> py -m pip install SomePackage[PDF,EPUB] # multiple extras + py -m pip install SomePackage[PDF] + py -m pip install "SomePackage[PDF] @ git+https://git.repo/SomePackage@master#subdirectory=subdir_path" + py -m pip install .[PDF] # project in current directory + py -m pip install SomePackage[PDF]==3.0 + py -m pip install SomePackage[PDF,EPUB] # multiple extras #. Install a particular source archive file. @@ -1104,15 +1104,15 @@ Examples .. code-block:: shell - $ python -m pip install ./downloads/SomePackage-1.0.4.tar.gz - $ python -m pip install http://my.package.repo/SomePackage-1.0.4.zip + python -m pip install ./downloads/SomePackage-1.0.4.tar.gz + python -m pip install http://my.package.repo/SomePackage-1.0.4.zip .. group-tab:: Windows .. code-block:: shell - C:\> py -m pip install ./downloads/SomePackage-1.0.4.tar.gz - C:\> py -m pip install http://my.package.repo/SomePackage-1.0.4.zip + py -m pip install ./downloads/SomePackage-1.0.4.tar.gz + py -m pip install http://my.package.repo/SomePackage-1.0.4.zip #. Install a particular source archive file following :pep:`440` direct references. @@ -1123,17 +1123,17 @@ Examples .. code-block:: shell - $ python -m pip install SomeProject@http://my.package.repo/SomeProject-1.2.3-py33-none-any.whl - $ python -m pip install "SomeProject @ http://my.package.repo/SomeProject-1.2.3-py33-none-any.whl" - $ python -m pip install SomeProject@http://my.package.repo/1.2.3.tar.gz + python -m pip install SomeProject@http://my.package.repo/SomeProject-1.2.3-py33-none-any.whl + python -m pip install "SomeProject @ http://my.package.repo/SomeProject-1.2.3-py33-none-any.whl" + python -m pip install SomeProject@http://my.package.repo/1.2.3.tar.gz .. group-tab:: Windows .. code-block:: shell - C:\> py -m pip install SomeProject@http://my.package.repo/SomeProject-1.2.3-py33-none-any.whl - C:\> py -m pip install "SomeProject @ http://my.package.repo/SomeProject-1.2.3-py33-none-any.whl" - C:\> py -m pip install SomeProject@http://my.package.repo/1.2.3.tar.gz + py -m pip install SomeProject@http://my.package.repo/SomeProject-1.2.3-py33-none-any.whl + py -m pip install "SomeProject @ http://my.package.repo/SomeProject-1.2.3-py33-none-any.whl" + py -m pip install SomeProject@http://my.package.repo/1.2.3.tar.gz #. Install from alternative package repositories. @@ -1146,13 +1146,13 @@ Examples .. code-block:: shell - $ python -m pip install --index-url http://my.package.repo/simple/ SomePackage + python -m pip install --index-url http://my.package.repo/simple/ SomePackage .. group-tab:: Windows .. code-block:: shell - C:\> py -m pip install --index-url http://my.package.repo/simple/ SomePackage + py -m pip install --index-url http://my.package.repo/simple/ SomePackage Search an additional index during install, in addition to `PyPI`_ @@ -1163,13 +1163,13 @@ Examples .. code-block:: shell - $ python -m pip install --extra-index-url http://my.package.repo/simple SomePackage + python -m pip install --extra-index-url http://my.package.repo/simple SomePackage .. group-tab:: Windows .. code-block:: shell - C:\> py -m pip install --extra-index-url http://my.package.repo/simple SomePackage + py -m pip install --extra-index-url http://my.package.repo/simple SomePackage Install from a local flat directory containing archives (and don't scan indexes): @@ -1180,17 +1180,17 @@ Examples .. code-block:: shell - $ python -m pip install --no-index --find-links=file:///local/dir/ SomePackage - $ python -m pip install --no-index --find-links=/local/dir/ SomePackage - $ python -m pip install --no-index --find-links=relative/dir/ SomePackage + python -m pip install --no-index --find-links=file:///local/dir/ SomePackage + python -m pip install --no-index --find-links=/local/dir/ SomePackage + python -m pip install --no-index --find-links=relative/dir/ SomePackage .. group-tab:: Windows .. code-block:: shell - C:\> py -m pip install --no-index --find-links=file:///local/dir/ SomePackage - C:\> py -m pip install --no-index --find-links=/local/dir/ SomePackage - C:\> py -m pip install --no-index --find-links=relative/dir/ SomePackage + py -m pip install --no-index --find-links=file:///local/dir/ SomePackage + py -m pip install --no-index --find-links=/local/dir/ SomePackage + py -m pip install --no-index --find-links=relative/dir/ SomePackage #. Find pre-release and development versions, in addition to stable versions. By default, pip only finds stable versions. @@ -1201,13 +1201,13 @@ Examples .. code-block:: shell - $ python -m pip install --pre SomePackage + python -m pip install --pre SomePackage .. group-tab:: Windows .. code-block:: shell - C:\> py -m pip install --pre SomePackage + py -m pip install --pre SomePackage #. Install packages from source. @@ -1220,13 +1220,13 @@ Examples .. code-block:: shell - $ python -m pip install SomePackage1 SomePackage2 --no-binary :all: + python -m pip install SomePackage1 SomePackage2 --no-binary :all: .. group-tab:: Windows .. code-block:: shell - C:\> py -m pip install SomePackage1 SomePackage2 --no-binary :all: + py -m pip install SomePackage1 SomePackage2 --no-binary :all: Specify ``SomePackage1`` to be installed from source: @@ -1236,13 +1236,13 @@ Examples .. code-block:: shell - $ python -m pip install SomePackage1 SomePackage2 --no-binary SomePackage1 + python -m pip install SomePackage1 SomePackage2 --no-binary SomePackage1 .. group-tab:: Windows .. code-block:: shell - C:\> py -m pip install SomePackage1 SomePackage2 --no-binary SomePackage1 + py -m pip install SomePackage1 SomePackage2 --no-binary SomePackage1 ---- diff --git a/docs/html/reference/pip_wheel.rst b/docs/html/reference/pip_wheel.rst index 0a11c6e254c..5dd4b032887 100644 --- a/docs/html/reference/pip_wheel.rst +++ b/docs/html/reference/pip_wheel.rst @@ -40,13 +40,13 @@ In order for pip to build a wheel, ``setup.py`` must implement the .. code-block:: shell - $ python setup.py bdist_wheel -d TARGET + python setup.py bdist_wheel -d TARGET .. group-tab:: Windows .. code-block:: shell - C:\> py setup.py bdist_wheel -d TARGET + py setup.py bdist_wheel -d TARGET This command must create a wheel compatible with the invoking Python @@ -68,13 +68,13 @@ example: .. code-block:: shell - $ python -m pip wheel --global-option bdist_ext --global-option -DFOO wheel + python -m pip wheel --global-option bdist_ext --global-option -DFOO wheel .. group-tab:: Windows .. code-block:: shell - C:\> py -m pip wheel --global-option bdist_ext --global-option -DFOO wheel + py -m pip wheel --global-option bdist_ext --global-option -DFOO wheel will result in a build command of @@ -109,15 +109,15 @@ Examples .. code-block:: shell - $ python -m pip wheel --wheel-dir=/tmp/wheelhouse SomePackage - $ python -m pip install --no-index --find-links=/tmp/wheelhouse SomePackage + python -m pip wheel --wheel-dir=/tmp/wheelhouse SomePackage + python -m pip install --no-index --find-links=/tmp/wheelhouse SomePackage .. group-tab:: Windows .. code-block:: shell - C:\> py -m pip wheel --wheel-dir=/tmp/wheelhouse SomePackage - C:\> py -m pip install --no-index --find-links=/tmp/wheelhouse SomePackage + py -m pip wheel --wheel-dir=/tmp/wheelhouse SomePackage + py -m pip install --no-index --find-links=/tmp/wheelhouse SomePackage #. Build a wheel for a package from source @@ -127,10 +127,10 @@ Examples .. code-block:: shell - $ python -m pip wheel --no-binary SomePackage SomePackage + python -m pip wheel --no-binary SomePackage SomePackage .. group-tab:: Windows .. code-block:: shell - C:\> py -m pip wheel --no-binary SomePackage SomePackage + py -m pip wheel --no-binary SomePackage SomePackage diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 41cbb640407..aa18a6f6685 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -17,7 +17,7 @@ to your system, which can be run from the command prompt as follows: .. code-block:: shell - $ python -m pip <pip arguments> + python -m pip <pip arguments> ``python -m pip`` executes pip using the Python interpreter you specified as python. So ``/usr/bin/python3.7 -m pip`` means @@ -27,7 +27,7 @@ to your system, which can be run from the command prompt as follows: .. code-block:: shell - C:\> py -m pip <pip arguments> + py -m pip <pip arguments> ``py -m pip`` executes pip using the latest Python interpreter you have installed. For more details, read the `Python Windows launcher`_ docs. @@ -49,17 +49,17 @@ Specifiers` .. code-block:: shell - $ python -m pip install SomePackage # latest version - $ python -m pip install SomePackage==1.0.4 # specific version - $ python -m pip install 'SomePackage>=1.0.4' # minimum version + python -m pip install SomePackage # latest version + python -m pip install SomePackage==1.0.4 # specific version + python -m pip install 'SomePackage>=1.0.4' # minimum version .. group-tab:: Windows .. code-block:: shell - C:\> py -m pip install SomePackage # latest version - C:\> py -m pip install SomePackage==1.0.4 # specific version - C:\> py -m pip install 'SomePackage>=1.0.4' # minimum version + py -m pip install SomePackage # latest version + py -m pip install SomePackage==1.0.4 # specific version + py -m pip install 'SomePackage>=1.0.4' # minimum version For more information and examples, see the :ref:`pip install` reference. @@ -168,13 +168,13 @@ installed using :ref:`pip install` like so: .. code-block:: shell - $ python -m pip install -r requirements.txt + python -m pip install -r requirements.txt .. group-tab:: Windows .. code-block:: shell - C:\> py -m pip install -r requirements.txt + py -m pip install -r requirements.txt Details on the format of the files are here: :ref:`Requirements File Format`. @@ -195,15 +195,15 @@ In practice, there are 4 common uses of Requirements files: .. code-block:: shell - $ python -m pip freeze > requirements.txt - $ python -m pip install -r requirements.txt + python -m pip freeze > requirements.txt + python -m pip install -r requirements.txt .. group-tab:: Windows .. code-block:: shell - C:\> py -m pip freeze > requirements.txt - C:\> py -m pip install -r requirements.txt + py -m pip freeze > requirements.txt + py -m pip install -r requirements.txt 2. Requirements files are used to force pip to properly resolve dependencies. As it is now, pip `doesn't have true dependency resolution @@ -276,13 +276,13 @@ Use a constraints file like so: .. code-block:: shell - $ python -m pip install -c constraints.txt + python -m pip install -c constraints.txt .. group-tab:: Windows .. code-block:: shell - C:\> py -m pip install -c constraints.txt + py -m pip install -c constraints.txt Constraints files are used for exactly the same reason as requirements files when you don't know exactly what things you want to install. For instance, say @@ -326,13 +326,13 @@ To install directly from a wheel archive: .. code-block:: shell - $ python -m pip install SomePackage-1.0-py2.py3-none-any.whl + python -m pip install SomePackage-1.0-py2.py3-none-any.whl .. group-tab:: Windows .. code-block:: shell - C:\> py -m pip install SomePackage-1.0-py2.py3-none-any.whl + py -m pip install SomePackage-1.0-py2.py3-none-any.whl For the cases where wheels are not available, pip offers :ref:`pip wheel` as a @@ -351,15 +351,15 @@ directory: .. code-block:: shell - $ python -m pip install wheel - $ python -m pip wheel --wheel-dir=/local/wheels -r requirements.txt + python -m pip install wheel + python -m pip wheel --wheel-dir=/local/wheels -r requirements.txt .. group-tab:: Windows .. code-block:: shell - C:\> py -m pip install wheel - C:\> py -m pip wheel --wheel-dir=/local/wheels -r requirements.txt + py -m pip install wheel + py -m pip wheel --wheel-dir=/local/wheels -r requirements.txt And *then* to install those requirements just using your local directory of wheels (and not from PyPI): @@ -370,13 +370,13 @@ wheels (and not from PyPI): .. code-block:: shell - $ python -m pip install --no-index --find-links=/local/wheels -r requirements.txt + python -m pip install --no-index --find-links=/local/wheels -r requirements.txt .. group-tab:: Windows .. code-block:: shell - C:\> py -m pip install --no-index --find-links=/local/wheels -r requirements.txt + py -m pip install --no-index --find-links=/local/wheels -r requirements.txt Uninstalling Packages @@ -390,13 +390,13 @@ pip is able to uninstall most packages like so: .. code-block:: shell - $ python -m pip uninstall SomePackage + python -m pip uninstall SomePackage .. group-tab:: Windows .. code-block:: shell - C:\> py -m pip uninstall SomePackage + py -m pip uninstall SomePackage pip also performs an automatic uninstall of an old version of a package @@ -496,13 +496,13 @@ command: .. code-block:: shell - $ python -m pip search "query" + python -m pip search "query" .. group-tab:: Windows .. code-block:: shell - C:\> py -m pip search "query" + py -m pip search "query" The query will be used to search the names and summaries of all packages. @@ -670,13 +670,13 @@ For example, to set the default timeout: .. code-block:: shell - $ export PIP_DEFAULT_TIMEOUT=60 + export PIP_DEFAULT_TIMEOUT=60 .. group-tab:: Windows .. code-block:: shell - C:\> set PIP_DEFAULT_TIMEOUT=60 + set PIP_DEFAULT_TIMEOUT=60 This is the same as passing the option to pip directly: @@ -686,13 +686,13 @@ This is the same as passing the option to pip directly: .. code-block:: shell - $ python -m pip --default-timeout=60 [...] + python -m pip --default-timeout=60 [...] .. group-tab:: Windows .. code-block:: shell - C:\> py -m pip --default-timeout=60 [...] + py -m pip --default-timeout=60 [...] For command line options which can be repeated, use a space to separate multiple values. For example: @@ -703,13 +703,13 @@ multiple values. For example: .. code-block:: shell - $ export PIP_FIND_LINKS="http://mirror1.example.com http://mirror2.example.com" + export PIP_FIND_LINKS="http://mirror1.example.com http://mirror2.example.com" .. group-tab:: Windows .. code-block:: shell - C:\> set PIP_FIND_LINKS="http://mirror1.example.com http://mirror2.example.com" + set PIP_FIND_LINKS="http://mirror1.example.com http://mirror2.example.com" is the same as calling: @@ -720,13 +720,13 @@ is the same as calling: .. code-block:: shell - $ python -m pip install --find-links=http://mirror1.example.com --find-links=http://mirror2.example.com + python -m pip install --find-links=http://mirror1.example.com --find-links=http://mirror2.example.com .. group-tab:: Windows .. code-block:: shell - C:\> py -m pip install --find-links=http://mirror1.example.com --find-links=http://mirror2.example.com + py -m pip install --find-links=http://mirror1.example.com --find-links=http://mirror2.example.com Options that do not take a value, but can be repeated (such as ``--verbose``) @@ -770,15 +770,15 @@ pip comes with support for command line completion in bash, zsh and fish. To setup for bash:: - $ python -m pip completion --bash >> ~/.profile + python -m pip completion --bash >> ~/.profile To setup for zsh:: - $ python -m pip completion --zsh >> ~/.zprofile + python -m pip completion --zsh >> ~/.zprofile To setup for fish:: - $ python -m pip completion --fish > ~/.config/fish/completions/pip.fish + python -m pip completion --fish > ~/.config/fish/completions/pip.fish Alternatively, you can use the result of the ``completion`` command directly with the eval function of your shell, e.g. by adding the following to your @@ -805,13 +805,13 @@ First, download the archives that fulfill your requirements: .. code-block:: shell - $ python -m pip download --destination-directory DIR -r requirements.txt + python -m pip download --destination-directory DIR -r requirements.txt .. group-tab:: Windows .. code-block:: shell - C:\> py -m pip download --destination-directory DIR -r requirements.txt + py -m pip download --destination-directory DIR -r requirements.txt Note that ``pip download`` will look in your wheel cache first, before @@ -826,13 +826,13 @@ this instead: .. code-block:: shell - $ python -m pip wheel --wheel-dir DIR -r requirements.txt + python -m pip wheel --wheel-dir DIR -r requirements.txt .. group-tab:: Windows .. code-block:: shell - C:\> py -m pip wheel --wheel-dir DIR -r requirements.txt + py -m pip wheel --wheel-dir DIR -r requirements.txt Then, to install from local only, you'll be using :ref:`--find-links <install_--find-links>` and :ref:`--no-index <install_--no-index>` like so: @@ -843,13 +843,13 @@ Then, to install from local only, you'll be using :ref:`--find-links .. code-block:: shell - $ python -m pip install --no-index --find-links=DIR -r requirements.txt + python -m pip install --no-index --find-links=DIR -r requirements.txt .. group-tab:: Windows .. code-block:: shell - C:\> -m pip install --no-index --find-links=DIR -r requirements.txt + py -m pip install --no-index --find-links=DIR -r requirements.txt "Only if needed" Recursive Upgrade @@ -876,15 +876,15 @@ behaviour was: .. code-block:: shell - $ python -m pip install --upgrade --no-deps SomePackage - $ python -m pip install SomePackage + python -m pip install --upgrade --no-deps SomePackage + python -m pip install SomePackage .. group-tab:: Windows .. code-block:: shell - C:\> py -m pip install --upgrade --no-deps SomePackage - C:\> py -m pip install SomePackage + py -m pip install --upgrade --no-deps SomePackage + py -m pip install SomePackage A proposal for an ``upgrade-all`` command is being considered as a safer @@ -916,15 +916,15 @@ To install "SomePackage" into an environment with site.USER_BASE customized to .. code-block:: shell - $ export PYTHONUSERBASE=/myappenv - $ python -m pip install --user SomePackage + export PYTHONUSERBASE=/myappenv + python -m pip install --user SomePackage .. group-tab:: Windows .. code-block:: shell - C:\> set PYTHONUSERBASE=c:/myappenv - C:\> py -m pip install --user SomePackage + set PYTHONUSERBASE=c:/myappenv + py -m pip install --user SomePackage ``pip install --user`` follows four rules: @@ -1177,13 +1177,13 @@ like this: .. code-block:: shell - $ python -m pip install package_coffee==0.44.1 package_tea==4.3.0 + python -m pip install package_coffee==0.44.1 package_tea==4.3.0 .. group-tab:: Windows .. code-block:: shell - C:\> py -m pip install package_coffee==0.44.1 package_tea==4.3.0 + py -m pip install package_coffee==0.44.1 package_tea==4.3.0 :: @@ -1295,13 +1295,13 @@ specifiers to *only* the more important package: .. code-block:: shell - $ python -m pip install package_coffee==0.44.1b0 package_tea + python -m pip install package_coffee==0.44.1b0 package_tea .. group-tab:: Windows .. code-block:: shell - C:\> py -m pip install package_coffee==0.44.1b0 package_tea + py -m pip install package_coffee==0.44.1b0 package_tea This will result in: From a2e2f5d052db75b47420044c807bf24456cde3ea Mon Sep 17 00:00:00 2001 From: Srinivas Nyayapati <shireenrao@gmail.com> Date: Sun, 16 Aug 2020 23:21:47 -0400 Subject: [PATCH 2526/3170] update all tabs with command output be console code-blocks --- docs/html/quickstart.rst | 26 +++++++++++----------- docs/html/reference/pip.rst | 4 ++-- docs/html/reference/pip_check.rst | 12 +++++----- docs/html/reference/pip_download.rst | 4 ++-- docs/html/reference/pip_freeze.rst | 4 ++-- docs/html/reference/pip_hash.rst | 4 ++-- docs/html/reference/pip_install.rst | 8 +++---- docs/html/reference/pip_list.rst | 32 +++++++++++++-------------- docs/html/reference/pip_search.rst | 4 ++-- docs/html/reference/pip_show.rst | 8 +++---- docs/html/reference/pip_uninstall.rst | 4 ++-- docs/html/user_guide.rst | 32 +++++++++++++-------------- 12 files changed, 71 insertions(+), 71 deletions(-) diff --git a/docs/html/quickstart.rst b/docs/html/quickstart.rst index bae9e2286ad..de0fd711bf3 100644 --- a/docs/html/quickstart.rst +++ b/docs/html/quickstart.rst @@ -10,7 +10,7 @@ Install a package from `PyPI`_: .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: console $ python -m pip install SomePackage [...] @@ -18,7 +18,7 @@ Install a package from `PyPI`_: .. group-tab:: Windows - .. code-block:: shell + .. code-block:: console C:\> py -m pip install SomePackage [...] @@ -26,14 +26,14 @@ Install a package from `PyPI`_: Install a package that's already been downloaded from `PyPI`_ or -obtained from elsewhere. This is useful if the target macOShine does not have a +obtained from elsewhere. This is useful if the target machine does not have a network connection: .. tabs:: .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: console $ python -m pip install SomePackage-1.0-py2.py3-none-any.whl [...] @@ -41,7 +41,7 @@ network connection: .. group-tab:: Windows - .. code-block:: shell + .. code-block:: console C:\> py -m pip install SomePackage-1.0-py2.py3-none-any.whl [...] @@ -54,7 +54,7 @@ Show what files were installed: .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: console $ python -m pip show --files SomePackage Name: SomePackage @@ -66,7 +66,7 @@ Show what files were installed: .. group-tab:: Windows - .. code-block:: shell + .. code-block:: console C:\> py -m pip show --files SomePackage Name: SomePackage @@ -82,14 +82,14 @@ List what packages are outdated: .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: console $ python -m pip list --outdated SomePackage (Current: 1.0 Latest: 2.0) .. group-tab:: Windows - .. code-block:: shell + .. code-block:: console C:\> py -m pip list --outdated SomePackage (Current: 1.0 Latest: 2.0) @@ -100,7 +100,7 @@ Upgrade a package: .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: console $ python -m pip install --upgrade SomePackage [...] @@ -112,7 +112,7 @@ Upgrade a package: .. group-tab:: Windows - .. code-block:: shell + .. code-block:: console C:\> py -m pip install --upgrade SomePackage [...] @@ -128,7 +128,7 @@ Uninstall a package: .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: console $ python -m pip uninstall SomePackage Uninstalling SomePackage: @@ -138,7 +138,7 @@ Uninstall a package: .. group-tab:: Windows - .. code-block:: shell + .. code-block:: console C:\> py -m pip uninstall SomePackage Uninstalling SomePackage: diff --git a/docs/html/reference/pip.rst b/docs/html/reference/pip.rst index a0c1148bf1e..ffd6df3d699 100644 --- a/docs/html/reference/pip.rst +++ b/docs/html/reference/pip.rst @@ -13,13 +13,13 @@ Usage .. code-block:: shell - $ python -m pip <command> [options] + python -m pip <command> [options] .. group-tab:: Windows .. code-block:: shell - C:\> py -m pip <command> [options] + py -m pip <command> [options] Description *********** diff --git a/docs/html/reference/pip_check.rst b/docs/html/reference/pip_check.rst index 07d7004753e..719528f20bb 100644 --- a/docs/html/reference/pip_check.rst +++ b/docs/html/reference/pip_check.rst @@ -36,7 +36,7 @@ Examples .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: console $ python -m pip check No broken requirements found. @@ -45,7 +45,7 @@ Examples .. group-tab:: Windows - .. code-block:: shell + .. code-block:: console C:\> py -m pip check No broken requirements found. @@ -58,7 +58,7 @@ Examples .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: console $ python -m pip check pyramid 1.5.2 requires WebOb, which is not installed. @@ -67,7 +67,7 @@ Examples .. group-tab:: Windows - .. code-block:: shell + .. code-block:: console C:\> py -m pip check pyramid 1.5.2 requires WebOb, which is not installed. @@ -80,7 +80,7 @@ Examples .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: console $ python -m pip check pyramid 1.5.2 has requirement WebOb>=1.3.1, but you have WebOb 0.8. @@ -89,7 +89,7 @@ Examples .. group-tab:: Windows - .. code-block:: shell + .. code-block:: console C:\> py -m pip check pyramid 1.5.2 has requirement WebOb>=1.3.1, but you have WebOb 0.8. diff --git a/docs/html/reference/pip_download.rst b/docs/html/reference/pip_download.rst index 141b6e1fa84..b4321be138b 100644 --- a/docs/html/reference/pip_download.rst +++ b/docs/html/reference/pip_download.rst @@ -178,7 +178,7 @@ Examples .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: console $ python -m pip download \ --only-binary=:all: \ @@ -193,7 +193,7 @@ Examples .. group-tab:: Windows - .. code-block:: shell + .. code-block:: console C:\> py -m pip download ^ --only-binary=:all: ^ diff --git a/docs/html/reference/pip_freeze.rst b/docs/html/reference/pip_freeze.rst index 91c80979ac8..e7cc14eadf4 100644 --- a/docs/html/reference/pip_freeze.rst +++ b/docs/html/reference/pip_freeze.rst @@ -43,7 +43,7 @@ Examples .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: console $ python -m pip freeze docutils==0.11 @@ -54,7 +54,7 @@ Examples .. group-tab:: Windows - .. code-block:: shell + .. code-block:: console C:\> py -m pip freeze docutils==0.11 diff --git a/docs/html/reference/pip_hash.rst b/docs/html/reference/pip_hash.rst index 7ed39280c7a..2313cae828b 100644 --- a/docs/html/reference/pip_hash.rst +++ b/docs/html/reference/pip_hash.rst @@ -53,7 +53,7 @@ Compute the hash of a downloaded archive: .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: console $ python -m pip download SomePackage Collecting SomePackage @@ -66,7 +66,7 @@ Compute the hash of a downloaded archive: .. group-tab:: Windows - .. code-block:: shell + .. code-block:: console C:\> py -m pip download SomePackage Collecting SomePackage diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index b2da2624060..68728581ff1 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -104,7 +104,7 @@ which depends on foo: .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: console $ python -m pip install quux ... @@ -116,7 +116,7 @@ which depends on foo: .. group-tab:: Windows - .. code-block:: shell + .. code-block:: console C:\> py -m pip install quux ... @@ -681,7 +681,7 @@ option: .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: console $ python -m pip install --require-hashes -r requirements.txt ... @@ -695,7 +695,7 @@ option: .. group-tab:: Windows - .. code-block:: shell + .. code-block:: console C:\> py -m pip install --require-hashes -r requirements.txt ... diff --git a/docs/html/reference/pip_list.rst b/docs/html/reference/pip_list.rst index bfdef9ebe1c..cb6064cc061 100644 --- a/docs/html/reference/pip_list.rst +++ b/docs/html/reference/pip_list.rst @@ -44,7 +44,7 @@ Examples .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: console $ python -m pip list docutils (0.10) @@ -55,7 +55,7 @@ Examples .. group-tab:: Windows - .. code-block:: shell + .. code-block:: console C:\> py -m pip list docutils (0.10) @@ -70,7 +70,7 @@ Examples .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: console $ python -m pip list --outdated docutils (Current: 0.10 Latest: 0.11) @@ -78,7 +78,7 @@ Examples .. group-tab:: Windows - .. code-block:: shell + .. code-block:: console C:\> py -m pip list --outdated docutils (Current: 0.10 Latest: 0.11) @@ -91,7 +91,7 @@ Examples .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: console $ python -m pip list --format columns Package Version @@ -102,7 +102,7 @@ Examples .. group-tab:: Windows - .. code-block:: shell + .. code-block:: console C:\> py -m pip list --format columns Package Version @@ -117,7 +117,7 @@ Examples .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: console $ python -m pip list -o --format columns Package Version Latest Type @@ -127,7 +127,7 @@ Examples .. group-tab:: Windows - .. code-block:: shell + .. code-block:: console C:\> py -m pip list -o --format columns Package Version Latest Type @@ -142,14 +142,14 @@ Examples .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: console $ python -m pip list --outdated --not-required docutils (Current: 0.10 Latest: 0.11) .. group-tab:: Windows - .. code-block:: shell + .. code-block:: console C:\> py -m pip list --outdated --not-required docutils (Current: 0.10 Latest: 0.11) @@ -160,7 +160,7 @@ Examples .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: console $ python -m pip list --format=legacy colorama (0.3.7) @@ -170,7 +170,7 @@ Examples .. group-tab:: Windows - .. code-block:: shell + .. code-block:: console C:\> py -m pip list --format=legacy colorama (0.3.7) @@ -184,14 +184,14 @@ Examples .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: console $ python -m pip list --format=json [{'name': 'colorama', 'version': '0.3.7'}, {'name': 'docopt', 'version': '0.6.2'}, ... .. group-tab:: Windows - .. code-block:: shell + .. code-block:: console C:\> py -m pip list --format=json [{'name': 'colorama', 'version': '0.3.7'}, {'name': 'docopt', 'version': '0.6.2'}, ... @@ -202,7 +202,7 @@ Examples .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: console $ python -m pip list --format=freeze colorama==0.3.7 @@ -212,7 +212,7 @@ Examples .. group-tab:: Windows - .. code-block:: shell + .. code-block:: console C:\> py -m pip list --format=freeze colorama==0.3.7 diff --git a/docs/html/reference/pip_search.rst b/docs/html/reference/pip_search.rst index e0fc3e25bcb..c0737aa7688 100644 --- a/docs/html/reference/pip_search.rst +++ b/docs/html/reference/pip_search.rst @@ -42,7 +42,7 @@ Examples .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: console $ python -m pip search peppercorn pepperedform - Helpers for using peppercorn with formprocess. @@ -50,7 +50,7 @@ Examples .. group-tab:: Windows - .. code-block:: shell + .. code-block:: console C:\> py -m pip search peppercorn pepperedform - Helpers for using peppercorn with formprocess. diff --git a/docs/html/reference/pip_show.rst b/docs/html/reference/pip_show.rst index ae9182f547f..71aa27e78db 100644 --- a/docs/html/reference/pip_show.rst +++ b/docs/html/reference/pip_show.rst @@ -42,7 +42,7 @@ Examples .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: console $ python -m pip show sphinx Name: Sphinx @@ -57,7 +57,7 @@ Examples .. group-tab:: Windows - .. code-block:: shell + .. code-block:: console C:\> py -m pip show sphinx Name: Sphinx @@ -76,7 +76,7 @@ Examples .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: console $ python -m pip show --verbose sphinx Name: Sphinx @@ -119,7 +119,7 @@ Examples .. group-tab:: Windows - .. code-block:: shell + .. code-block:: console C:\> py -m pip show --verbose sphinx Name: Sphinx diff --git a/docs/html/reference/pip_uninstall.rst b/docs/html/reference/pip_uninstall.rst index 165ac4cd5b8..ecc3b9210b2 100644 --- a/docs/html/reference/pip_uninstall.rst +++ b/docs/html/reference/pip_uninstall.rst @@ -42,7 +42,7 @@ Examples .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: console $ python -m pip uninstall simplejson Uninstalling simplejson: @@ -53,7 +53,7 @@ Examples .. group-tab:: Windows - .. code-block:: shell + .. code-block:: console C:\> py -m pip uninstall simplejson Uninstalling simplejson: diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index aa18a6f6685..64edc0b1887 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -414,7 +414,7 @@ To list installed packages: .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: console $ python -m pip list docutils (0.9.1) @@ -424,7 +424,7 @@ To list installed packages: .. group-tab:: Windows - .. code-block:: shell + .. code-block:: console C:\> py -m pip list docutils (0.9.1) @@ -439,7 +439,7 @@ To list outdated packages, and show the latest version available: .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: console $ python -m pip list --outdated docutils (Current: 0.9.1 Latest: 0.10) @@ -447,7 +447,7 @@ To list outdated packages, and show the latest version available: .. group-tab:: Windows - .. code-block:: shell + .. code-block:: console C:\> py -m pip list --outdated docutils (Current: 0.9.1 Latest: 0.10) @@ -459,7 +459,7 @@ To show details about an installed package: .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: console $ python -m pip show sphinx --- @@ -470,7 +470,7 @@ To show details about an installed package: .. group-tab:: Windows - .. code-block:: shell + .. code-block:: console C:\> py -m pip show sphinx --- @@ -953,14 +953,14 @@ From within a ``--no-site-packages`` virtualenv (i.e. the default kind): .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: console $ python -m pip install --user SomePackage Can not perform a '--user' install. User site-packages are not visible in this virtualenv. .. group-tab:: Windows - .. code-block:: shell + .. code-block:: console C:\> py -m pip install --user SomePackage Can not perform a '--user' install. User site-packages are not visible in this virtualenv. @@ -973,14 +973,14 @@ is already installed in the virtualenv: .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: console $ python -m pip install --user SomePackage==0.4 Will not install to the user site because it will lack sys.path precedence .. group-tab:: Windows - .. code-block:: shell + .. code-block:: console C:\> py -m pip install --user SomePackage==0.4 Will not install to the user site because it will lack sys.path precedence @@ -991,7 +991,7 @@ From within a real python, where ``SomePackage`` is *not* installed globally: .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: console $ python -m pip install --user SomePackage [...] @@ -999,7 +999,7 @@ From within a real python, where ``SomePackage`` is *not* installed globally: .. group-tab:: Windows - .. code-block:: shell + .. code-block:: console C:\> py -m pip install --user SomePackage [...] @@ -1012,7 +1012,7 @@ is *not* the latest version: .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: console $ python -m pip install --user SomePackage [...] @@ -1023,7 +1023,7 @@ is *not* the latest version: .. group-tab:: Windows - .. code-block:: shell + .. code-block:: console C:\> py -m pip install --user SomePackage [...] @@ -1039,7 +1039,7 @@ is the latest version: .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: console $ python -m pip install --user SomePackage [...] @@ -1054,7 +1054,7 @@ is the latest version: .. group-tab:: Windows - .. code-block:: shell + .. code-block:: console C:\> py -m pip install --user SomePackage [...] From 8f3151324fe971afbdb6fa8fe638c9108492aec0 Mon Sep 17 00:00:00 2001 From: shireenrao <shireenrao@gmail.com> Date: Tue, 25 Aug 2020 07:48:04 -0400 Subject: [PATCH 2527/3170] fix inconsistent indentation of tab blocks --- docs/html/conf.py | 2 +- docs/html/installing.rst | 64 +++--- docs/html/quickstart.rst | 2 +- docs/html/reference/pip.rst | 12 +- docs/html/reference/pip_check.rst | 18 +- docs/html/reference/pip_download.rst | 176 ++++++++------- docs/html/reference/pip_freeze.rst | 12 +- docs/html/reference/pip_hash.rst | 50 ++--- docs/html/reference/pip_install.rst | 308 +++++++++++++------------- docs/html/reference/pip_list.rst | 48 ++-- docs/html/reference/pip_search.rst | 6 +- docs/html/reference/pip_show.rst | 208 ++++++++--------- docs/html/reference/pip_uninstall.rst | 30 +-- docs/html/reference/pip_wheel.rst | 36 +-- 14 files changed, 488 insertions(+), 484 deletions(-) diff --git a/docs/html/conf.py b/docs/html/conf.py index 9d0b7a2fa79..a88ac33e2e4 100644 --- a/docs/html/conf.py +++ b/docs/html/conf.py @@ -34,7 +34,7 @@ 'sphinx.ext.extlinks', 'pip_sphinxext', 'sphinx.ext.intersphinx', - 'sphinx_tabs.tabs' + 'sphinx_tabs.tabs', ] # intersphinx diff --git a/docs/html/installing.rst b/docs/html/installing.rst index e6717881ac1..5379c1da0e0 100644 --- a/docs/html/installing.rst +++ b/docs/html/installing.rst @@ -30,17 +30,17 @@ have downloaded ``get-pip.py``: .. tabs:: - .. group-tab:: Unix/macOS + .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python get-pip.py + python get-pip.py - .. group-tab:: Windows + .. group-tab:: Windows - .. code-block:: shell + .. code-block:: shell - py get-pip.py + py get-pip.py .. warning:: @@ -83,66 +83,66 @@ Install from local copies of pip and setuptools: .. tabs:: - .. group-tab:: Unix/macOS + .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python get-pip.py --no-index --find-links=/local/copies + python get-pip.py --no-index --find-links=/local/copies - .. group-tab:: Windows + .. group-tab:: Windows - .. code-block:: shell + .. code-block:: shell - py get-pip.py --no-index --find-links=/local/copies + py get-pip.py --no-index --find-links=/local/copies Install to the user site [3]_: .. tabs:: - .. group-tab:: Unix/macOS + .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python get-pip.py --user + python get-pip.py --user - .. group-tab:: Windows + .. group-tab:: Windows - .. code-block:: shell + .. code-block:: shell - py get-pip.py --user + py get-pip.py --user Install behind a proxy: .. tabs:: - .. group-tab:: Unix/macOS + .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python get-pip.py --proxy="http://[user:passwd@]proxy.server:port" + python get-pip.py --proxy="http://[user:passwd@]proxy.server:port" - .. group-tab:: Windows + .. group-tab:: Windows - .. code-block:: shell + .. code-block:: shell - py get-pip.py --proxy="http://[user:passwd@]proxy.server:port" + py get-pip.py --proxy="http://[user:passwd@]proxy.server:port" ``get-pip.py`` can also be used to install a specified combination of ``pip``, ``setuptools``, and ``wheel`` using the same requirements syntax as pip: .. tabs:: - .. group-tab:: Unix/macOS + .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python get-pip.py pip==9.0.2 wheel==0.30.0 setuptools==28.8.0 + python get-pip.py pip==9.0.2 wheel==0.30.0 setuptools==28.8.0 - .. group-tab:: Windows + .. group-tab:: Windows - .. code-block:: shell + .. code-block:: shell - py get-pip.py pip==9.0.2 wheel==0.30.0 setuptools==28.8.0 + py get-pip.py pip==9.0.2 wheel==0.30.0 setuptools==28.8.0 Using Linux Package Managers ============================ @@ -162,13 +162,13 @@ Upgrading pip .. code-block:: shell - python -m pip install -U pip + python -m pip install -U pip .. group-tab:: Windows .. code-block:: shell - py -m pip install -U pip + py -m pip install -U pip .. _compatibility-requirements: diff --git a/docs/html/quickstart.rst b/docs/html/quickstart.rst index de0fd711bf3..9591e1127f5 100644 --- a/docs/html/quickstart.rst +++ b/docs/html/quickstart.rst @@ -142,7 +142,7 @@ Uninstall a package: C:\> py -m pip uninstall SomePackage Uninstalling SomePackage: - /my/env/lib/pythonx.x/site-packages/somepackage + /my/env/lib/pythonx.x/site-packages/somepackage Proceed (y/n)? y Successfully uninstalled SomePackage diff --git a/docs/html/reference/pip.rst b/docs/html/reference/pip.rst index ffd6df3d699..9fd42c676a9 100644 --- a/docs/html/reference/pip.rst +++ b/docs/html/reference/pip.rst @@ -234,17 +234,17 @@ included in the command as follows: .. tabs:: - .. group-tab:: Unix/macOS + .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: console - python setup.py <global_options> BUILD COMMAND <build_options> + python setup.py <global_options> BUILD COMMAND <build_options> - .. group-tab:: Windows + .. group-tab:: Windows - .. code-block:: shell + .. code-block:: shell - py setup.py <global_options> BUILD COMMAND <build_options> + py setup.py <global_options> BUILD COMMAND <build_options> The options are passed unmodified, and presently offer direct access to the distutils command line. Use of ``--global-option`` and ``--build-option`` diff --git a/docs/html/reference/pip_check.rst b/docs/html/reference/pip_check.rst index 719528f20bb..d3bb457e12c 100644 --- a/docs/html/reference/pip_check.rst +++ b/docs/html/reference/pip_check.rst @@ -32,11 +32,11 @@ Examples #. If all dependencies are compatible: - .. tabs:: + .. tabs:: .. group-tab:: Unix/macOS - .. code-block:: console + .. code-block:: console $ python -m pip check No broken requirements found. @@ -45,7 +45,7 @@ Examples .. group-tab:: Windows - .. code-block:: console + .. code-block:: console C:\> py -m pip check No broken requirements found. @@ -54,11 +54,11 @@ Examples #. If a package is missing: - .. tabs:: + .. tabs:: .. group-tab:: Unix/macOS - .. code-block:: console + .. code-block:: console $ python -m pip check pyramid 1.5.2 requires WebOb, which is not installed. @@ -67,7 +67,7 @@ Examples .. group-tab:: Windows - .. code-block:: console + .. code-block:: console C:\> py -m pip check pyramid 1.5.2 requires WebOb, which is not installed. @@ -76,11 +76,11 @@ Examples #. If a package has the wrong version: - .. tabs:: + .. tabs:: .. group-tab:: Unix/macOS - .. code-block:: console + .. code-block:: console $ python -m pip check pyramid 1.5.2 has requirement WebOb>=1.3.1, but you have WebOb 0.8. @@ -89,7 +89,7 @@ Examples .. group-tab:: Windows - .. code-block:: console + .. code-block:: console C:\> py -m pip check pyramid 1.5.2 has requirement WebOb>=1.3.1, but you have WebOb 0.8. diff --git a/docs/html/reference/pip_download.rst b/docs/html/reference/pip_download.rst index b4321be138b..7983bb95b04 100644 --- a/docs/html/reference/pip_download.rst +++ b/docs/html/reference/pip_download.rst @@ -64,144 +64,148 @@ Examples #. Download a package and all of its dependencies - .. tabs:: + .. tabs:: .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python -m pip download SomePackage - python -m pip download -d . SomePackage # equivalent to above - python -m pip download --no-index --find-links=/tmp/wheelhouse -d /tmp/otherwheelhouse SomePackage + python -m pip download SomePackage + python -m pip download -d . SomePackage # equivalent to above + python -m pip download --no-index --find-links=/tmp/wheelhouse -d /tmp/otherwheelhouse SomePackage .. group-tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip download SomePackage - py -m pip download -d . SomePackage # equivalent to above - py -m pip download --no-index --find-links=/tmp/wheelhouse -d /tmp/otherwheelhouse SomePackage + py -m pip download SomePackage + py -m pip download -d . SomePackage # equivalent to above + py -m pip download --no-index --find-links=/tmp/wheelhouse -d /tmp/otherwheelhouse SomePackage #. Download a package and all of its dependencies with OSX specific interpreter constraints. - This forces OSX 10.10 or lower compatibility. Since OSX deps are forward compatible, - this will also match ``macosx-10_9_x86_64``, ``macosx-10_8_x86_64``, ``macosx-10_8_intel``, - etc. - It will also match deps with platform ``any``. Also force the interpreter version to ``27`` - (or more generic, i.e. ``2``) and implementation to ``cp`` (or more generic, i.e. ``py``). + This forces OSX 10.10 or lower compatibility. Since OSX deps are forward compatible, + this will also match ``macosx-10_9_x86_64``, ``macosx-10_8_x86_64``, ``macosx-10_8_intel``, + etc. + It will also match deps with platform ``any``. Also force the interpreter version to ``27`` + (or more generic, i.e. ``2``) and implementation to ``cp`` (or more generic, i.e. ``py``). - .. tabs:: + .. tabs:: .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python -m pip download \ - --only-binary=:all: \ - --platform macosx-10_10_x86_64 \ - --python-version 27 \ - --implementation cp \ - SomePackage + python -m pip download \ + --only-binary=:all: \ + --platform macosx-10_10_x86_64 \ + --python-version 27 \ + --implementation cp \ + SomePackage .. group-tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip download ^ - --only-binary=:all: ^ - --platform macosx-10_10_x86_64 ^ - --python-version 27 ^ - --implementation cp ^ - SomePackage + py -m pip download ^ + --only-binary=:all: ^ + --platform macosx-10_10_x86_64 ^ + --python-version 27 ^ + --implementation cp ^ + SomePackage #. Download a package and its dependencies with linux specific constraints. - Force the interpreter to be any minor version of py3k, and only accept - ``cp34m`` or ``none`` as the abi. + Force the interpreter to be any minor version of py3k, and only accept + ``cp34m`` or ``none`` as the abi. - .. tabs:: + .. tabs:: .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python -m pip download \ - --only-binary=:all: \ - --platform linux_x86_64 \ - --python-version 3 \ - --implementation cp \ - --abi cp34m \ - SomePackage + python -m pip download \ + --only-binary=:all: \ + --platform linux_x86_64 \ + --python-version 3 \ + --implementation cp \ + --abi cp34m \ + SomePackage .. group-tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip download ^ - --only-binary=:all: ^ - --platform linux_x86_64 ^ - --python-version 3 ^ - --implementation cp ^ - --abi cp34m ^ - SomePackage + py -m pip download ^ + --only-binary=:all: ^ + --platform linux_x86_64 ^ + --python-version 3 ^ + --implementation cp ^ + --abi cp34m ^ + SomePackage #. Force platform, implementation, and abi agnostic deps. - .. tabs:: + .. tabs:: .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python -m pip download \ - --only-binary=:all: \ - --platform any \ - --python-version 3 \ - --implementation py \ - --abi none \ - SomePackage + python -m pip download \ + --only-binary=:all: \ + --platform any \ + --python-version 3 \ + --implementation py \ + --abi none \ + SomePackage .. group-tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip download ^ - --only-binary=:all: ^ - --platform any ^ - --python-version 3 ^ - --implementation py ^ - --abi none ^ - SomePackage + py -m pip download ^ + --only-binary=:all: ^ + --platform any ^ + --python-version 3 ^ + --implementation py ^ + --abi none ^ + SomePackage #. Even when overconstrained, this will still correctly fetch the pip universal wheel. - .. tabs:: + .. tabs:: .. group-tab:: Unix/macOS - .. code-block:: console + .. code-block:: console - $ python -m pip download \ - --only-binary=:all: \ - --platform linux_x86_64 \ - --python-version 33 \ - --implementation cp \ - --abi cp34m \ - pip>=8 + $ python -m pip download \ + --only-binary=:all: \ + --platform linux_x86_64 \ + --python-version 33 \ + --implementation cp \ + --abi cp34m \ + pip>=8 - $ ls pip-8.1.1-py2.py3-none-any.whl - pip-8.1.1-py2.py3-none-any.whl + .. code-block:: console + + $ ls pip-8.1.1-py2.py3-none-any.whl + pip-8.1.1-py2.py3-none-any.whl .. group-tab:: Windows - .. code-block:: console + .. code-block:: console + + C:\> py -m pip download ^ + --only-binary=:all: ^ + --platform linux_x86_64 ^ + --python-version 33 ^ + --implementation cp ^ + --abi cp34m ^ + pip>=8 - C:\> py -m pip download ^ - --only-binary=:all: ^ - --platform linux_x86_64 ^ - --python-version 33 ^ - --implementation cp ^ - --abi cp34m ^ - pip>=8 + .. code-block:: console - C:\> dir pip-8.1.1-py2.py3-none-any.whl - pip-8.1.1-py2.py3-none-any.whl + C:\> dir pip-8.1.1-py2.py3-none-any.whl + pip-8.1.1-py2.py3-none-any.whl diff --git a/docs/html/reference/pip_freeze.rst b/docs/html/reference/pip_freeze.rst index e7cc14eadf4..d4ed00bfb3e 100644 --- a/docs/html/reference/pip_freeze.rst +++ b/docs/html/reference/pip_freeze.rst @@ -39,11 +39,11 @@ Examples #. Generate output suitable for a requirements file. - .. tabs:: + .. tabs:: .. group-tab:: Unix/macOS - .. code-block:: console + .. code-block:: console $ python -m pip freeze docutils==0.11 @@ -54,7 +54,7 @@ Examples .. group-tab:: Windows - .. code-block:: console + .. code-block:: console C:\> py -m pip freeze docutils==0.11 @@ -66,18 +66,18 @@ Examples #. Generate a requirements file and then install from it in another environment. - .. tabs:: + .. tabs:: .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell env1/bin/python -m pip freeze > requirements.txt env2/bin/python -m pip install -r requirements.txt .. group-tab:: Windows - .. code-block:: shell + .. code-block:: shell env1\bin\python -m pip freeze > requirements.txt env2\bin\python -m pip install -r requirements.txt diff --git a/docs/html/reference/pip_hash.rst b/docs/html/reference/pip_hash.rst index 2313cae828b..71e1cf4be6c 100644 --- a/docs/html/reference/pip_hash.rst +++ b/docs/html/reference/pip_hash.rst @@ -51,28 +51,28 @@ Compute the hash of a downloaded archive: .. tabs:: - .. group-tab:: Unix/macOS - - .. code-block:: console - - $ python -m pip download SomePackage - Collecting SomePackage - Downloading SomePackage-2.2.tar.gz - Saved ./pip_downloads/SomePackage-2.2.tar.gz - Successfully downloaded SomePackage - $ python -m pip hash ./pip_downloads/SomePackage-2.2.tar.gz - ./pip_downloads/SomePackage-2.2.tar.gz: - --hash=sha256:93e62e05c7ad3da1a233def6731e8285156701e3419a5fe279017c429ec67ce0 - - .. group-tab:: Windows - - .. code-block:: console - - C:\> py -m pip download SomePackage - Collecting SomePackage - Downloading SomePackage-2.2.tar.gz - Saved ./pip_downloads/SomePackage-2.2.tar.gz - Successfully downloaded SomePackage - C:\> py -m pip hash ./pip_downloads/SomePackage-2.2.tar.gz - ./pip_downloads/SomePackage-2.2.tar.gz: - --hash=sha256:93e62e05c7ad3da1a233def6731e8285156701e3419a5fe279017c429ec67ce0 + .. group-tab:: Unix/macOS + + .. code-block:: console + + $ python -m pip download SomePackage + Collecting SomePackage + Downloading SomePackage-2.2.tar.gz + Saved ./pip_downloads/SomePackage-2.2.tar.gz + Successfully downloaded SomePackage + $ python -m pip hash ./pip_downloads/SomePackage-2.2.tar.gz + ./pip_downloads/SomePackage-2.2.tar.gz: + --hash=sha256:93e62e05c7ad3da1a233def6731e8285156701e3419a5fe279017c429ec67ce0 + + .. group-tab:: Windows + + .. code-block:: console + + C:\> py -m pip download SomePackage + Collecting SomePackage + Downloading SomePackage-2.2.tar.gz + Saved ./pip_downloads/SomePackage-2.2.tar.gz + Successfully downloaded SomePackage + C:\> py -m pip hash ./pip_downloads/SomePackage-2.2.tar.gz + ./pip_downloads/SomePackage-2.2.tar.gz: + --hash=sha256:93e62e05c7ad3da1a233def6731e8285156701e3419a5fe279017c429ec67ce0 diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index 68728581ff1..cb97c8ee075 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -106,25 +106,25 @@ which depends on foo: .. code-block:: console - $ python -m pip install quux - ... - Installing collected packages baz, bar, foo, quux + $ python -m pip install quux + ... + Installing collected packages baz, bar, foo, quux - $ python -m pip install bar - ... - Installing collected packages foo, baz, bar + $ python -m pip install bar + ... + Installing collected packages foo, baz, bar .. group-tab:: Windows .. code-block:: console - C:\> py -m pip install quux - ... - Installing collected packages baz, bar, foo, quux + C:\> py -m pip install quux + ... + Installing collected packages baz, bar, foo, quux - C:\> py -m pip install bar - ... - Installing collected packages foo, baz, bar + C:\> py -m pip install bar + ... + Installing collected packages foo, baz, bar Prior to v6.1.0, pip made no commitments about install order. @@ -422,13 +422,13 @@ Then, to install from this repository, the syntax would be: .. code-block:: shell - python -m pip install -e "vcs+protocol://repo_url/#egg=pkg&subdirectory=pkg_dir" + python -m pip install -e "vcs+protocol://repo_url/#egg=pkg&subdirectory=pkg_dir" .. group-tab:: Windows .. code-block:: shell - py -m pip install -e "vcs+protocol://repo_url/#egg=pkg&subdirectory=pkg_dir" + py -m pip install -e "vcs+protocol://repo_url/#egg=pkg&subdirectory=pkg_dir" Git @@ -683,29 +683,29 @@ option: .. code-block:: console - $ python -m pip install --require-hashes -r requirements.txt - ... - Hashes are required in --require-hashes mode (implicitly on when a hash is - specified for any package). These requirements were missing hashes, - leaving them open to tampering. These are the hashes the downloaded - archives actually had. You can add lines like these to your requirements - files to prevent tampering. - pyelasticsearch==1.0 --hash=sha256:44ddfb1225054d7d6b1d02e9338e7d4809be94edbe9929a2ec0807d38df993fa - more-itertools==2.2 --hash=sha256:93e62e05c7ad3da1a233def6731e8285156701e3419a5fe279017c429ec67ce0 + $ python -m pip install --require-hashes -r requirements.txt + ... + Hashes are required in --require-hashes mode (implicitly on when a hash is + specified for any package). These requirements were missing hashes, + leaving them open to tampering. These are the hashes the downloaded + archives actually had. You can add lines like these to your requirements + files to prevent tampering. + pyelasticsearch==1.0 --hash=sha256:44ddfb1225054d7d6b1d02e9338e7d4809be94edbe9929a2ec0807d38df993fa + more-itertools==2.2 --hash=sha256:93e62e05c7ad3da1a233def6731e8285156701e3419a5fe279017c429ec67ce0 .. group-tab:: Windows .. code-block:: console - C:\> py -m pip install --require-hashes -r requirements.txt - ... - Hashes are required in --require-hashes mode (implicitly on when a hash is - specified for any package). These requirements were missing hashes, - leaving them open to tampering. These are the hashes the downloaded - archives actually had. You can add lines like these to your requirements - files to prevent tampering. - pyelasticsearch==1.0 --hash=sha256:44ddfb1225054d7d6b1d02e9338e7d4809be94edbe9929a2ec0807d38df993fa - more-itertools==2.2 --hash=sha256:93e62e05c7ad3da1a233def6731e8285156701e3419a5fe279017c429ec67ce0 + C:\> py -m pip install --require-hashes -r requirements.txt + ... + Hashes are required in --require-hashes mode (implicitly on when a hash is + specified for any package). These requirements were missing hashes, + leaving them open to tampering. These are the hashes the downloaded + archives actually had. You can add lines like these to your requirements + files to prevent tampering. + pyelasticsearch==1.0 --hash=sha256:44ddfb1225054d7d6b1d02e9338e7d4809be94edbe9929a2ec0807d38df993fa + more-itertools==2.2 --hash=sha256:93e62e05c7ad3da1a233def6731e8285156701e3419a5fe279017c429ec67ce0 This can be useful in deploy scripts, to ensure that the author of the @@ -756,34 +756,34 @@ Hash-checking mode also works with :ref:`pip download` and :ref:`pip wheel`. A .. tabs:: - .. group-tab:: Unix/macOS + .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python -m pip install --no-deps -e . + python -m pip install --no-deps -e . - .. group-tab:: Windows + .. group-tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip install --no-deps -e . + py -m pip install --no-deps -e . Instead of ``python setup.py install``, use... - .. tabs:: + .. tabs:: .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python -m pip install --no-deps . + python -m pip install --no-deps . .. group-tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip install --no-deps . + py -m pip install --no-deps . Hashes from PyPI ^^^^^^^^^^^^^^^^ @@ -841,15 +841,15 @@ You can install local projects or VCS projects in "editable" mode: .. code-block:: shell - python -m pip install -e path/to/SomeProject - python -m pip install -e git+http://repo/my_project.git#egg=SomeProject + python -m pip install -e path/to/SomeProject + python -m pip install -e git+http://repo/my_project.git#egg=SomeProject .. group-tab:: Windows .. code-block:: shell - py -m pip install -e path/to/SomeProject - py -m pip install -e git+http://repo/my_project.git#egg=SomeProject + py -m pip install -e path/to/SomeProject + py -m pip install -e git+http://repo/my_project.git#egg=SomeProject (See the :ref:`VCS Support` section above for more information on VCS-related syntax.) @@ -957,292 +957,292 @@ Examples #. Install ``SomePackage`` and its dependencies from `PyPI`_ using :ref:`Requirement Specifiers` - .. tabs:: + .. tabs:: .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python -m pip install SomePackage # latest version - python -m pip install SomePackage==1.0.4 # specific version - python -m pip install 'SomePackage>=1.0.4' # minimum version + python -m pip install SomePackage # latest version + python -m pip install SomePackage==1.0.4 # specific version + python -m pip install 'SomePackage>=1.0.4' # minimum version .. group-tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip install SomePackage # latest version - py -m pip install SomePackage==1.0.4 # specific version - py -m pip install 'SomePackage>=1.0.4' # minimum version + py -m pip install SomePackage # latest version + py -m pip install SomePackage==1.0.4 # specific version + py -m pip install 'SomePackage>=1.0.4' # minimum version #. Install a list of requirements specified in a file. See the :ref:`Requirements files <Requirements Files>`. - .. tabs:: + .. tabs:: .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python -m pip install -r requirements.txt + python -m pip install -r requirements.txt .. group-tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip install -r requirements.txt + py -m pip install -r requirements.txt #. Upgrade an already installed ``SomePackage`` to the latest from PyPI. - .. tabs:: + .. tabs:: .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python -m pip install --upgrade SomePackage + python -m pip install --upgrade SomePackage .. group-tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip install --upgrade SomePackage + py -m pip install --upgrade SomePackage #. Install a local project in "editable" mode. See the section on :ref:`Editable Installs <editable-installs>`. - .. tabs:: + .. tabs:: .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python -m pip install -e . # project in current directory - python -m pip install -e path/to/project # project in another directory + python -m pip install -e . # project in current directory + python -m pip install -e path/to/project # project in another directory .. group-tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip install -e . # project in current directory - py -m pip install -e path/to/project # project in another directory + py -m pip install -e . # project in current directory + py -m pip install -e path/to/project # project in another directory #. Install a project from VCS - .. tabs:: + .. tabs:: .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python -m pip install SomeProject@git+https://git.repo/some_pkg.git@1.3.1 + python -m pip install SomeProject@git+https://git.repo/some_pkg.git@1.3.1 .. group-tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip install SomeProject@git+https://git.repo/some_pkg.git@1.3.1 + py -m pip install SomeProject@git+https://git.repo/some_pkg.git@1.3.1 #. Install a project from VCS in "editable" mode. See the sections on :ref:`VCS Support <VCS Support>` and :ref:`Editable Installs <editable-installs>`. - .. tabs:: + .. tabs:: .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python -m pip install -e git+https://git.repo/some_pkg.git#egg=SomePackage # from git - python -m pip install -e hg+https://hg.repo/some_pkg.git#egg=SomePackage # from mercurial - python -m python -m pip install -e svn+svn://svn.repo/some_pkg/trunk/#egg=SomePackage # from svn - python -m pip install -e git+https://git.repo/some_pkg.git@feature#egg=SomePackage # from 'feature' branch - python -m pip install -e "git+https://git.repo/some_repo.git#egg=subdir&subdirectory=subdir_path" # install a python package from a repo subdirectory + python -m pip install -e git+https://git.repo/some_pkg.git#egg=SomePackage # from git + python -m pip install -e hg+https://hg.repo/some_pkg.git#egg=SomePackage # from mercurial + python -m python -m pip install -e svn+svn://svn.repo/some_pkg/trunk/#egg=SomePackage # from svn + python -m pip install -e git+https://git.repo/some_pkg.git@feature#egg=SomePackage # from 'feature' branch + python -m pip install -e "git+https://git.repo/some_repo.git#egg=subdir&subdirectory=subdir_path" # install a python package from a repo subdirectory .. group-tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip install -e git+https://git.repo/some_pkg.git#egg=SomePackage # from git - py -m pip install -e hg+https://hg.repo/some_pkg.git#egg=SomePackage # from mercurial - py -m pip install -e svn+svn://svn.repo/some_pkg/trunk/#egg=SomePackage # from svn - py -m pip install -e git+https://git.repo/some_pkg.git@feature#egg=SomePackage # from 'feature' branch - py -m pip install -e "git+https://git.repo/some_repo.git#egg=subdir&subdirectory=subdir_path" # install a python package from a repo subdirectory + py -m pip install -e git+https://git.repo/some_pkg.git#egg=SomePackage # from git + py -m pip install -e hg+https://hg.repo/some_pkg.git#egg=SomePackage # from mercurial + py -m pip install -e svn+svn://svn.repo/some_pkg/trunk/#egg=SomePackage # from svn + py -m pip install -e git+https://git.repo/some_pkg.git@feature#egg=SomePackage # from 'feature' branch + py -m pip install -e "git+https://git.repo/some_repo.git#egg=subdir&subdirectory=subdir_path" # install a python package from a repo subdirectory #. Install a package with `setuptools extras`_. - .. tabs:: + .. tabs:: .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python -m pip install SomePackage[PDF] - python -m pip install "SomePackage[PDF] @ git+https://git.repo/SomePackage@master#subdirectory=subdir_path" - python -m pip install .[PDF] # project in current directory - python -m pip install SomePackage[PDF]==3.0 - python -m pip install SomePackage[PDF,EPUB] # multiple extras + python -m pip install SomePackage[PDF] + python -m pip install "SomePackage[PDF] @ git+https://git.repo/SomePackage@master#subdirectory=subdir_path" + python -m pip install .[PDF] # project in current directory + python -m pip install SomePackage[PDF]==3.0 + python -m pip install SomePackage[PDF,EPUB] # multiple extras .. group-tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip install SomePackage[PDF] - py -m pip install "SomePackage[PDF] @ git+https://git.repo/SomePackage@master#subdirectory=subdir_path" - py -m pip install .[PDF] # project in current directory - py -m pip install SomePackage[PDF]==3.0 - py -m pip install SomePackage[PDF,EPUB] # multiple extras + py -m pip install SomePackage[PDF] + py -m pip install "SomePackage[PDF] @ git+https://git.repo/SomePackage@master#subdirectory=subdir_path" + py -m pip install .[PDF] # project in current directory + py -m pip install SomePackage[PDF]==3.0 + py -m pip install SomePackage[PDF,EPUB] # multiple extras #. Install a particular source archive file. - .. tabs:: + .. tabs:: .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python -m pip install ./downloads/SomePackage-1.0.4.tar.gz - python -m pip install http://my.package.repo/SomePackage-1.0.4.zip + python -m pip install ./downloads/SomePackage-1.0.4.tar.gz + python -m pip install http://my.package.repo/SomePackage-1.0.4.zip .. group-tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip install ./downloads/SomePackage-1.0.4.tar.gz - py -m pip install http://my.package.repo/SomePackage-1.0.4.zip + py -m pip install ./downloads/SomePackage-1.0.4.tar.gz + py -m pip install http://my.package.repo/SomePackage-1.0.4.zip #. Install a particular source archive file following :pep:`440` direct references. - .. tabs:: + .. tabs:: .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python -m pip install SomeProject@http://my.package.repo/SomeProject-1.2.3-py33-none-any.whl - python -m pip install "SomeProject @ http://my.package.repo/SomeProject-1.2.3-py33-none-any.whl" - python -m pip install SomeProject@http://my.package.repo/1.2.3.tar.gz + python -m pip install SomeProject@http://my.package.repo/SomeProject-1.2.3-py33-none-any.whl + python -m pip install "SomeProject @ http://my.package.repo/SomeProject-1.2.3-py33-none-any.whl" + python -m pip install SomeProject@http://my.package.repo/1.2.3.tar.gz .. group-tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip install SomeProject@http://my.package.repo/SomeProject-1.2.3-py33-none-any.whl - py -m pip install "SomeProject @ http://my.package.repo/SomeProject-1.2.3-py33-none-any.whl" - py -m pip install SomeProject@http://my.package.repo/1.2.3.tar.gz + py -m pip install SomeProject@http://my.package.repo/SomeProject-1.2.3-py33-none-any.whl + py -m pip install "SomeProject @ http://my.package.repo/SomeProject-1.2.3-py33-none-any.whl" + py -m pip install SomeProject@http://my.package.repo/1.2.3.tar.gz #. Install from alternative package repositories. Install from a different index, and not `PyPI`_ - .. tabs:: + .. tabs:: .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python -m pip install --index-url http://my.package.repo/simple/ SomePackage + python -m pip install --index-url http://my.package.repo/simple/ SomePackage .. group-tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip install --index-url http://my.package.repo/simple/ SomePackage + py -m pip install --index-url http://my.package.repo/simple/ SomePackage Search an additional index during install, in addition to `PyPI`_ - .. tabs:: + .. tabs:: .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python -m pip install --extra-index-url http://my.package.repo/simple SomePackage + python -m pip install --extra-index-url http://my.package.repo/simple SomePackage .. group-tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip install --extra-index-url http://my.package.repo/simple SomePackage + py -m pip install --extra-index-url http://my.package.repo/simple SomePackage Install from a local flat directory containing archives (and don't scan indexes): - .. tabs:: + .. tabs:: .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python -m pip install --no-index --find-links=file:///local/dir/ SomePackage - python -m pip install --no-index --find-links=/local/dir/ SomePackage - python -m pip install --no-index --find-links=relative/dir/ SomePackage + python -m pip install --no-index --find-links=file:///local/dir/ SomePackage + python -m pip install --no-index --find-links=/local/dir/ SomePackage + python -m pip install --no-index --find-links=relative/dir/ SomePackage .. group-tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip install --no-index --find-links=file:///local/dir/ SomePackage - py -m pip install --no-index --find-links=/local/dir/ SomePackage - py -m pip install --no-index --find-links=relative/dir/ SomePackage + py -m pip install --no-index --find-links=file:///local/dir/ SomePackage + py -m pip install --no-index --find-links=/local/dir/ SomePackage + py -m pip install --no-index --find-links=relative/dir/ SomePackage #. Find pre-release and development versions, in addition to stable versions. By default, pip only finds stable versions. - .. tabs:: + .. tabs:: .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python -m pip install --pre SomePackage + python -m pip install --pre SomePackage .. group-tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip install --pre SomePackage + py -m pip install --pre SomePackage #. Install packages from source. Do not use any binary packages - .. tabs:: + .. tabs:: .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python -m pip install SomePackage1 SomePackage2 --no-binary :all: + python -m pip install SomePackage1 SomePackage2 --no-binary :all: .. group-tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip install SomePackage1 SomePackage2 --no-binary :all: + py -m pip install SomePackage1 SomePackage2 --no-binary :all: Specify ``SomePackage1`` to be installed from source: - .. tabs:: + .. tabs:: .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python -m pip install SomePackage1 SomePackage2 --no-binary SomePackage1 + python -m pip install SomePackage1 SomePackage2 --no-binary SomePackage1 .. group-tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip install SomePackage1 SomePackage2 --no-binary SomePackage1 + py -m pip install SomePackage1 SomePackage2 --no-binary SomePackage1 ---- diff --git a/docs/html/reference/pip_list.rst b/docs/html/reference/pip_list.rst index cb6064cc061..1489ed751a1 100644 --- a/docs/html/reference/pip_list.rst +++ b/docs/html/reference/pip_list.rst @@ -40,11 +40,11 @@ Examples #. List installed packages. - .. tabs:: + .. tabs:: .. group-tab:: Unix/macOS - .. code-block:: console + .. code-block:: console $ python -m pip list docutils (0.10) @@ -55,7 +55,7 @@ Examples .. group-tab:: Windows - .. code-block:: console + .. code-block:: console C:\> py -m pip list docutils (0.10) @@ -66,11 +66,11 @@ Examples #. List outdated packages (excluding editables), and the latest version available. - .. tabs:: + .. tabs:: .. group-tab:: Unix/macOS - .. code-block:: console + .. code-block:: console $ python -m pip list --outdated docutils (Current: 0.10 Latest: 0.11) @@ -78,7 +78,7 @@ Examples .. group-tab:: Windows - .. code-block:: console + .. code-block:: console C:\> py -m pip list --outdated docutils (Current: 0.10 Latest: 0.11) @@ -87,11 +87,11 @@ Examples #. List installed packages with column formatting. - .. tabs:: + .. tabs:: .. group-tab:: Unix/macOS - .. code-block:: console + .. code-block:: console $ python -m pip list --format columns Package Version @@ -102,7 +102,7 @@ Examples .. group-tab:: Windows - .. code-block:: console + .. code-block:: console C:\> py -m pip list --format columns Package Version @@ -113,11 +113,11 @@ Examples #. List outdated packages with column formatting. - .. tabs:: + .. tabs:: .. group-tab:: Unix/macOS - .. code-block:: console + .. code-block:: console $ python -m pip list -o --format columns Package Version Latest Type @@ -127,7 +127,7 @@ Examples .. group-tab:: Windows - .. code-block:: console + .. code-block:: console C:\> py -m pip list -o --format columns Package Version Latest Type @@ -138,29 +138,29 @@ Examples #. List packages that are not dependencies of other packages. Can be combined with other options. - .. tabs:: + .. tabs:: .. group-tab:: Unix/macOS - .. code-block:: console + .. code-block:: console $ python -m pip list --outdated --not-required docutils (Current: 0.10 Latest: 0.11) .. group-tab:: Windows - .. code-block:: console + .. code-block:: console C:\> py -m pip list --outdated --not-required docutils (Current: 0.10 Latest: 0.11) #. Use legacy formatting - .. tabs:: + .. tabs:: .. group-tab:: Unix/macOS - .. code-block:: console + .. code-block:: console $ python -m pip list --format=legacy colorama (0.3.7) @@ -170,7 +170,7 @@ Examples .. group-tab:: Windows - .. code-block:: console + .. code-block:: console C:\> py -m pip list --format=legacy colorama (0.3.7) @@ -180,29 +180,29 @@ Examples #. Use json formatting - .. tabs:: + .. tabs:: .. group-tab:: Unix/macOS - .. code-block:: console + .. code-block:: console $ python -m pip list --format=json [{'name': 'colorama', 'version': '0.3.7'}, {'name': 'docopt', 'version': '0.6.2'}, ... .. group-tab:: Windows - .. code-block:: console + .. code-block:: console C:\> py -m pip list --format=json [{'name': 'colorama', 'version': '0.3.7'}, {'name': 'docopt', 'version': '0.6.2'}, ... #. Use freeze formatting - .. tabs:: + .. tabs:: .. group-tab:: Unix/macOS - .. code-block:: console + .. code-block:: console $ python -m pip list --format=freeze colorama==0.3.7 @@ -212,7 +212,7 @@ Examples .. group-tab:: Windows - .. code-block:: console + .. code-block:: console C:\> py -m pip list --format=freeze colorama==0.3.7 diff --git a/docs/html/reference/pip_search.rst b/docs/html/reference/pip_search.rst index c0737aa7688..fba62959316 100644 --- a/docs/html/reference/pip_search.rst +++ b/docs/html/reference/pip_search.rst @@ -38,11 +38,11 @@ Examples #. Search for "peppercorn" - .. tabs:: + .. tabs:: .. group-tab:: Unix/macOS - .. code-block:: console + .. code-block:: console $ python -m pip search peppercorn pepperedform - Helpers for using peppercorn with formprocess. @@ -50,7 +50,7 @@ Examples .. group-tab:: Windows - .. code-block:: console + .. code-block:: console C:\> py -m pip search peppercorn pepperedform - Helpers for using peppercorn with formprocess. diff --git a/docs/html/reference/pip_show.rst b/docs/html/reference/pip_show.rst index 71aa27e78db..6bd3718b95d 100644 --- a/docs/html/reference/pip_show.rst +++ b/docs/html/reference/pip_show.rst @@ -38,124 +38,124 @@ Examples #. Show information about a package: - .. tabs:: + .. tabs:: .. group-tab:: Unix/macOS - .. code-block:: console + .. code-block:: console - $ python -m pip show sphinx - Name: Sphinx - Version: 1.4.5 - Summary: Python documentation generator - Home-page: http://sphinx-doc.org/ - Author: Georg Brandl - Author-email: georg@python.org - License: BSD - Location: /my/env/lib/python2.7/site-packages - Requires: docutils, snowballstemmer, alabaster, Pygments, imagesize, Jinja2, babel, six + $ python -m pip show sphinx + Name: Sphinx + Version: 1.4.5 + Summary: Python documentation generator + Home-page: http://sphinx-doc.org/ + Author: Georg Brandl + Author-email: georg@python.org + License: BSD + Location: /my/env/lib/python2.7/site-packages + Requires: docutils, snowballstemmer, alabaster, Pygments, imagesize, Jinja2, babel, six .. group-tab:: Windows - .. code-block:: console + .. code-block:: console - C:\> py -m pip show sphinx - Name: Sphinx - Version: 1.4.5 - Summary: Python documentation generator - Home-page: http://sphinx-doc.org/ - Author: Georg Brandl - Author-email: georg@python.org - License: BSD - Location: /my/env/lib/python2.7/site-packages - Requires: docutils, snowballstemmer, alabaster, Pygments, imagesize, Jinja2, babel, six + C:\> py -m pip show sphinx + Name: Sphinx + Version: 1.4.5 + Summary: Python documentation generator + Home-page: http://sphinx-doc.org/ + Author: Georg Brandl + Author-email: georg@python.org + License: BSD + Location: /my/env/lib/python2.7/site-packages + Requires: docutils, snowballstemmer, alabaster, Pygments, imagesize, Jinja2, babel, six #. Show all information about a package - .. tabs:: + .. tabs:: .. group-tab:: Unix/macOS - .. code-block:: console - - $ python -m pip show --verbose sphinx - Name: Sphinx - Version: 1.4.5 - Summary: Python documentation generator - Home-page: http://sphinx-doc.org/ - Author: Georg Brandl - Author-email: georg@python.org - License: BSD - Location: /my/env/lib/python2.7/site-packages - Requires: docutils, snowballstemmer, alabaster, Pygments, imagesize, Jinja2, babel, six - Metadata-Version: 2.0 - Installer: - Classifiers: - Development Status :: 5 - Production/Stable - Environment :: Console - Environment :: Web Environment - Intended Audience :: Developers - Intended Audience :: Education - License :: OSI Approved :: BSD License - Operating System :: OS Independent - Programming Language :: Python - Programming Language :: Python :: 2 - Programming Language :: Python :: 3 - Framework :: Sphinx - Framework :: Sphinx :: Extension - Framework :: Sphinx :: Theme - Topic :: Documentation - Topic :: Documentation :: Sphinx - Topic :: Text Processing - Topic :: Utilities - Entry-points: - [console_scripts] - sphinx-apidoc = sphinx.apidoc:main - sphinx-autogen = sphinx.ext.autosummary.generate:main - sphinx-build = sphinx:main - sphinx-quickstart = sphinx.quickstart:main - [distutils.commands] - build_sphinx = sphinx.setup_command:BuildDoc + .. code-block:: console + + $ python -m pip show --verbose sphinx + Name: Sphinx + Version: 1.4.5 + Summary: Python documentation generator + Home-page: http://sphinx-doc.org/ + Author: Georg Brandl + Author-email: georg@python.org + License: BSD + Location: /my/env/lib/python2.7/site-packages + Requires: docutils, snowballstemmer, alabaster, Pygments, imagesize, Jinja2, babel, six + Metadata-Version: 2.0 + Installer: + Classifiers: + Development Status :: 5 - Production/Stable + Environment :: Console + Environment :: Web Environment + Intended Audience :: Developers + Intended Audience :: Education + License :: OSI Approved :: BSD License + Operating System :: OS Independent + Programming Language :: Python + Programming Language :: Python :: 2 + Programming Language :: Python :: 3 + Framework :: Sphinx + Framework :: Sphinx :: Extension + Framework :: Sphinx :: Theme + Topic :: Documentation + Topic :: Documentation :: Sphinx + Topic :: Text Processing + Topic :: Utilities + Entry-points: + [console_scripts] + sphinx-apidoc = sphinx.apidoc:main + sphinx-autogen = sphinx.ext.autosummary.generate:main + sphinx-build = sphinx:main + sphinx-quickstart = sphinx.quickstart:main + [distutils.commands] + build_sphinx = sphinx.setup_command:BuildDoc .. group-tab:: Windows - .. code-block:: console - - C:\> py -m pip show --verbose sphinx - Name: Sphinx - Version: 1.4.5 - Summary: Python documentation generator - Home-page: http://sphinx-doc.org/ - Author: Georg Brandl - Author-email: georg@python.org - License: BSD - Location: /my/env/lib/python2.7/site-packages - Requires: docutils, snowballstemmer, alabaster, Pygments, imagesize, Jinja2, babel, six - Metadata-Version: 2.0 - Installer: - Classifiers: - Development Status :: 5 - Production/Stable - Environment :: Console - Environment :: Web Environment - Intended Audience :: Developers - Intended Audience :: Education - License :: OSI Approved :: BSD License - Operating System :: OS Independent - Programming Language :: Python - Programming Language :: Python :: 2 - Programming Language :: Python :: 3 - Framework :: Sphinx - Framework :: Sphinx :: Extension - Framework :: Sphinx :: Theme - Topic :: Documentation - Topic :: Documentation :: Sphinx - Topic :: Text Processing - Topic :: Utilities - Entry-points: - [console_scripts] - sphinx-apidoc = sphinx.apidoc:main - sphinx-autogen = sphinx.ext.autosummary.generate:main - sphinx-build = sphinx:main - sphinx-quickstart = sphinx.quickstart:main - [distutils.commands] - build_sphinx = sphinx.setup_command:BuildDoc + .. code-block:: console + + C:\> py -m pip show --verbose sphinx + Name: Sphinx + Version: 1.4.5 + Summary: Python documentation generator + Home-page: http://sphinx-doc.org/ + Author: Georg Brandl + Author-email: georg@python.org + License: BSD + Location: /my/env/lib/python2.7/site-packages + Requires: docutils, snowballstemmer, alabaster, Pygments, imagesize, Jinja2, babel, six + Metadata-Version: 2.0 + Installer: + Classifiers: + Development Status :: 5 - Production/Stable + Environment :: Console + Environment :: Web Environment + Intended Audience :: Developers + Intended Audience :: Education + License :: OSI Approved :: BSD License + Operating System :: OS Independent + Programming Language :: Python + Programming Language :: Python :: 2 + Programming Language :: Python :: 3 + Framework :: Sphinx + Framework :: Sphinx :: Extension + Framework :: Sphinx :: Theme + Topic :: Documentation + Topic :: Documentation :: Sphinx + Topic :: Text Processing + Topic :: Utilities + Entry-points: + [console_scripts] + sphinx-apidoc = sphinx.apidoc:main + sphinx-autogen = sphinx.ext.autosummary.generate:main + sphinx-build = sphinx:main + sphinx-quickstart = sphinx.quickstart:main + [distutils.commands] + build_sphinx = sphinx.setup_command:BuildDoc diff --git a/docs/html/reference/pip_uninstall.rst b/docs/html/reference/pip_uninstall.rst index ecc3b9210b2..8b31c5673c1 100644 --- a/docs/html/reference/pip_uninstall.rst +++ b/docs/html/reference/pip_uninstall.rst @@ -38,26 +38,26 @@ Examples #. Uninstall a package. - .. tabs:: + .. tabs:: .. group-tab:: Unix/macOS - .. code-block:: console + .. code-block:: console - $ python -m pip uninstall simplejson - Uninstalling simplejson: - /home/me/env/lib/python2.7/site-packages/simplejson - /home/me/env/lib/python2.7/site-packages/simplejson-2.2.1-py2.7.egg-info - Proceed (y/n)? y - Successfully uninstalled simplejson + $ python -m pip uninstall simplejson + Uninstalling simplejson: + /home/me/env/lib/python2.7/site-packages/simplejson + /home/me/env/lib/python2.7/site-packages/simplejson-2.2.1-py2.7.egg-info + Proceed (y/n)? y + Successfully uninstalled simplejson .. group-tab:: Windows - .. code-block:: console + .. code-block:: console - C:\> py -m pip uninstall simplejson - Uninstalling simplejson: - /home/me/env/lib/python2.7/site-packages/simplejson - /home/me/env/lib/python2.7/site-packages/simplejson-2.2.1-py2.7.egg-info - Proceed (y/n)? y - Successfully uninstalled simplejson + C:\> py -m pip uninstall simplejson + Uninstalling simplejson: + /home/me/env/lib/python2.7/site-packages/simplejson + /home/me/env/lib/python2.7/site-packages/simplejson-2.2.1-py2.7.egg-info + Proceed (y/n)? y + Successfully uninstalled simplejson diff --git a/docs/html/reference/pip_wheel.rst b/docs/html/reference/pip_wheel.rst index 5dd4b032887..c1bdf37f852 100644 --- a/docs/html/reference/pip_wheel.rst +++ b/docs/html/reference/pip_wheel.rst @@ -36,17 +36,17 @@ In order for pip to build a wheel, ``setup.py`` must implement the .. tabs:: - .. group-tab:: Unix/macOS + .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python setup.py bdist_wheel -d TARGET + python setup.py bdist_wheel -d TARGET - .. group-tab:: Windows + .. group-tab:: Windows - .. code-block:: shell + .. code-block:: shell - py setup.py bdist_wheel -d TARGET + py setup.py bdist_wheel -d TARGET This command must create a wheel compatible with the invoking Python @@ -64,17 +64,17 @@ example: .. tabs:: - .. group-tab:: Unix/macOS + .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python -m pip wheel --global-option bdist_ext --global-option -DFOO wheel + python -m pip wheel --global-option bdist_ext --global-option -DFOO wheel - .. group-tab:: Windows + .. group-tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip wheel --global-option bdist_ext --global-option -DFOO wheel + py -m pip wheel --global-option bdist_ext --global-option -DFOO wheel will result in a build command of @@ -103,34 +103,34 @@ Examples #. Build wheels for a requirement (and all its dependencies), and then install - .. tabs:: + .. tabs:: .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell python -m pip wheel --wheel-dir=/tmp/wheelhouse SomePackage python -m pip install --no-index --find-links=/tmp/wheelhouse SomePackage .. group-tab:: Windows - .. code-block:: shell + .. code-block:: shell py -m pip wheel --wheel-dir=/tmp/wheelhouse SomePackage py -m pip install --no-index --find-links=/tmp/wheelhouse SomePackage #. Build a wheel for a package from source - .. tabs:: + .. tabs:: .. group-tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell python -m pip wheel --no-binary SomePackage SomePackage .. group-tab:: Windows - .. code-block:: shell + .. code-block:: shell py -m pip wheel --no-binary SomePackage SomePackage From 294995bc47961b0d948e50521fc0a5ebfebab71b Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <hugovk@users.noreply.github.com> Date: Mon, 14 Sep 2020 23:00:48 +0300 Subject: [PATCH 2528/3170] Fix typo --- docs/html/development/release-process.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/html/development/release-process.rst b/docs/html/development/release-process.rst index 66ff3ca7c51..ebba1325864 100644 --- a/docs/html/development/release-process.rst +++ b/docs/html/development/release-process.rst @@ -16,7 +16,7 @@ with version numbers. Our release months are January, April, July, October. The release date within that month will be up to the release manager for that release. If there are no changes, then that release month is skipped and the next release will be -3 month later. +3 months later. The release manager may, at their discretion, choose whether or not there will be a pre-release period for a release, and if there is may extend that From 0fc1044ff44a83b3c4f5ed340217c940def6ad37 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Wed, 16 Sep 2020 17:49:15 +0530 Subject: [PATCH 2529/3170] :newspaper: --- news/8861.bugfix | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/8861.bugfix diff --git a/news/8861.bugfix b/news/8861.bugfix new file mode 100644 index 00000000000..d623419fae5 --- /dev/null +++ b/news/8861.bugfix @@ -0,0 +1 @@ +Tweak the output during dependency resolution in the new resolver. From c947d00882ef6f12cbae1bfb71bc2677c4d2a82a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Tue, 25 Aug 2020 22:06:44 +0700 Subject: [PATCH 2530/3170] [fast-deps] Check download directory before making requests --- news/8804.feature | 3 +++ src/pip/_internal/operations/prepare.py | 29 +++++++++++++------------ 2 files changed, 18 insertions(+), 14 deletions(-) create mode 100644 news/8804.feature diff --git a/news/8804.feature b/news/8804.feature new file mode 100644 index 00000000000..a29333342e5 --- /dev/null +++ b/news/8804.feature @@ -0,0 +1,3 @@ +Check the download directory for existing wheels to possibly avoid +fetching metadata when the ``fast-deps`` feature is used with +``pip wheel`` and ``pip download``. diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 0a254052654..09540edce74 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -486,26 +486,27 @@ def prepare_linked_requirement(self, req, parallel_builds=False): link = req.link self._log_preparing_link(req) with indent_log(): - wheel_dist = self._fetch_metadata_using_lazy_wheel(link) - if wheel_dist is not None: - req.needs_more_preparation = True - return wheel_dist + download_dir = self._get_download_dir(req.link) + if download_dir is not None and link.is_wheel: + hashes = self._get_linked_req_hashes(req) + file_path = _check_download_dir(req.link, download_dir, hashes) + if file_path is not None: + self._downloaded[req.link.url] = file_path, None + else: + file_path = None + + if file_path is None: + wheel_dist = self._fetch_metadata_using_lazy_wheel(link) + if wheel_dist is not None: + req.needs_more_preparation = True + return wheel_dist return self._prepare_linked_requirement(req, parallel_builds) def prepare_linked_requirements_more(self, reqs, parallel_builds=False): # type: (Iterable[InstallRequirement], bool) -> None """Prepare a linked requirement more, if needed.""" reqs = [req for req in reqs if req.needs_more_preparation] - links = [] # type: List[Link] - for req in reqs: - download_dir = self._get_download_dir(req.link) - if download_dir is not None: - hashes = self._get_linked_req_hashes(req) - file_path = _check_download_dir(req.link, download_dir, hashes) - if download_dir is None or file_path is None: - links.append(req.link) - else: - self._downloaded[req.link.url] = file_path, None + links = [req.link for req in reqs] # Let's download to a temporary directory. tmpdir = TempDirectory(kind="unpack", globally_managed=True).path From 2b07c5d28888c431437e770adff3d036b4b1e1f1 Mon Sep 17 00:00:00 2001 From: Vipul Kumar <finn02@disroot.org> Date: Wed, 23 Sep 2020 04:27:53 +0000 Subject: [PATCH 2531/3170] End no-color's description with period punctuation It would be nice, if like description of other options, "--no-color"'s description also ends with a period. --- src/pip/_internal/cli/cmdoptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index ed42c5f5ae7..f94ddfabb7f 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -187,7 +187,7 @@ class PipOption(Option): dest='no_color', action='store_true', default=False, - help="Suppress colored output", + help="Suppress colored output.", ) # type: Callable[..., Option] version = partial( From 4c534e65d21d4b17a76e29794496400f72e40f6f Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Wed, 23 Sep 2020 18:27:39 +0530 Subject: [PATCH 2532/3170] Tweak message when -r is not passed on a requirements.txt --- src/pip/_internal/req/constructors.py | 2 +- tests/unit/test_req.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index 7a4641ef5a1..24fff26eced 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -159,7 +159,7 @@ def deduce_helpful_msg(req): """ msg = "" if os.path.exists(req): - msg = " It does exist." + msg = " The path does exist. " # Try to parse and check if it is a requirements file. try: with open(req, 'r') as fp: diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index a5a9d4baeef..730a26a88d5 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -572,7 +572,7 @@ def test_requirement_file(self): install_req_from_line(req_file_path) err_msg = e.value.args[0] assert "Invalid requirement" in err_msg - assert "It looks like a path. It does exist." in err_msg + assert "It looks like a path. The path does exist." in err_msg assert "appears to be a requirements file." in err_msg assert "If that is the case, use the '-r' flag to install" in err_msg From e93c0ba6a326d39b3a9c52853707052f3c8e3e78 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Wed, 23 Sep 2020 19:50:53 +0530 Subject: [PATCH 2533/3170] Update linters None of these require any changes to the codebase. --- .pre-commit-config.yaml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 04c72d8e3c1..f0e8adea490 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ exclude: 'src/pip/_vendor/' repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v2.5.0 + rev: v3.2.0 hooks: - id: check-builtin-literals - id: check-added-large-files @@ -17,7 +17,7 @@ repos: exclude: .patch - repo: https://gitlab.com/pycqa/flake8 - rev: 3.8.1 + rev: 3.8.3 hooks: - id: flake8 additional_dependencies: [ @@ -56,6 +56,6 @@ repos: exclude: NEWS.rst - repo: https://github.com/mgedmin/check-manifest - rev: '0.42' + rev: '0.43' hooks: - id: check-manifest From 89dad17b87281a7cf8652d476edcc15c16f214a6 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Wed, 23 Sep 2020 19:51:42 +0530 Subject: [PATCH 2534/3170] Update linter: pygrep-hooks --- .pre-commit-config.yaml | 2 +- docs/html/user_guide.rst | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f0e8adea490..6feaa17541b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -44,7 +44,7 @@ repos: args: ["--pretty", "-2"] - repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.5.1 + rev: v1.6.0 hooks: - id: python-no-log-warn - id: python-no-eval diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index ce013c2a362..8e8b25259ae 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -1105,7 +1105,7 @@ The big change in this release is to the pip dependency resolver within pip. Computers need to know the right order to install pieces of software -("to install `x`, you need to install `y` first"). So, when Python +("to install ``x``, you need to install ``y`` first"). So, when Python programmers share software as packages, they have to precisely describe those installation prerequisites, and pip needs to navigate tricky situations where it's getting conflicting instructions. This new From 25ab172b55bba6213e4bfb68b5c6e1bc4bec857e Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Wed, 23 Sep 2020 18:38:01 +0530 Subject: [PATCH 2535/3170] Update linter: isort --- .pre-commit-config.yaml | 2 +- src/pip/_internal/__init__.py | 2 +- src/pip/_internal/build_env.py | 3 ++- src/pip/_internal/cache.py | 2 +- src/pip/_internal/cli/base_command.py | 5 ++--- src/pip/_internal/cli/cmdoptions.py | 3 ++- src/pip/_internal/cli/command_context.py | 2 +- src/pip/_internal/cli/main_parser.py | 2 +- src/pip/_internal/cli/spinners.py | 2 +- src/pip/_internal/commands/__init__.py | 1 + src/pip/_internal/commands/check.py | 2 +- src/pip/_internal/commands/completion.py | 2 +- src/pip/_internal/commands/configuration.py | 2 +- src/pip/_internal/commands/debug.py | 5 +++-- src/pip/_internal/commands/help.py | 6 ++++-- src/pip/_internal/commands/list.py | 5 +++-- src/pip/_internal/commands/search.py | 3 ++- src/pip/_internal/commands/show.py | 2 +- src/pip/_internal/configuration.py | 4 +--- src/pip/_internal/distributions/base.py | 3 ++- src/pip/_internal/distributions/installed.py | 1 + src/pip/_internal/distributions/sdist.py | 1 + src/pip/_internal/distributions/wheel.py | 1 + src/pip/_internal/exceptions.py | 4 ++-- src/pip/_internal/index/collector.py | 14 +++++++++++--- src/pip/_internal/index/package_finder.py | 9 ++++++++- src/pip/_internal/locations.py | 3 +-- src/pip/_internal/main.py | 2 +- src/pip/_internal/models/candidate.py | 1 + src/pip/_internal/models/direct_url.py | 4 +--- src/pip/_internal/models/format_control.py | 2 +- src/pip/_internal/models/link.py | 1 + src/pip/_internal/models/selection_prefs.py | 1 + src/pip/_internal/network/auth.py | 6 +++--- src/pip/_internal/network/cache.py | 2 +- src/pip/_internal/network/session.py | 4 +--- src/pip/_internal/network/xmlrpc.py | 1 + src/pip/_internal/operations/build/metadata.py | 3 ++- src/pip/_internal/operations/build/wheel.py | 1 + src/pip/_internal/operations/check.py | 5 ++--- src/pip/_internal/operations/freeze.py | 18 +++++++++++++----- src/pip/_internal/operations/install/wheel.py | 2 +- src/pip/_internal/pyproject.py | 2 +- src/pip/_internal/req/constructors.py | 5 ++--- src/pip/_internal/req/req_file.py | 10 +++++++++- src/pip/_internal/req/req_install.py | 12 ++++++------ src/pip/_internal/req/req_set.py | 1 + src/pip/_internal/req/req_tracker.py | 3 ++- src/pip/_internal/req/req_uninstall.py | 11 ++++++++++- src/pip/_internal/resolution/base.py | 1 + .../_internal/resolution/legacy/resolver.py | 1 + .../_internal/resolution/resolvelib/factory.py | 2 +- .../resolution/resolvelib/provider.py | 2 +- src/pip/_internal/utils/compat.py | 2 +- src/pip/_internal/utils/direct_url_helpers.py | 4 ++-- src/pip/_internal/utils/encoding.py | 2 +- src/pip/_internal/utils/entrypoints.py | 2 +- src/pip/_internal/utils/hashes.py | 5 ++--- src/pip/_internal/utils/misc.py | 15 +++++++++++++-- src/pip/_internal/utils/packaging.py | 3 ++- src/pip/_internal/utils/parallel.py | 2 +- src/pip/_internal/utils/subprocess.py | 9 ++++++++- src/pip/_internal/vcs/bazaar.py | 1 + src/pip/_internal/vcs/git.py | 1 + src/pip/_internal/vcs/subversion.py | 3 ++- src/pip/_internal/vcs/versioncontrol.py | 14 ++++++++++++-- src/pip/_internal/wheel_builder.py | 4 +--- tests/conftest.py | 3 ++- tests/functional/test_uninstall.py | 3 ++- tests/lib/__init__.py | 1 + tests/lib/server.py | 11 ++++++++++- tests/lib/wheel.py | 12 ++++++++++-- tests/unit/test_locations.py | 2 ++ 73 files changed, 198 insertions(+), 95 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 6feaa17541b..f1afdd473cf 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -27,7 +27,7 @@ repos: exclude: tests/data - repo: https://github.com/timothycrosley/isort - rev: 4.3.21 + rev: 5.5.3 hooks: - id: isort files: \.py$ diff --git a/src/pip/_internal/__init__.py b/src/pip/_internal/__init__.py index 264c2cab88d..a778e99488e 100755 --- a/src/pip/_internal/__init__.py +++ b/src/pip/_internal/__init__.py @@ -2,7 +2,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Optional, List + from typing import List, Optional def main(args=None): diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py index 28d1ad689a1..a08e63cd051 100644 --- a/src/pip/_internal/build_env.py +++ b/src/pip/_internal/build_env.py @@ -19,7 +19,8 @@ if MYPY_CHECK_RUNNING: from types import TracebackType - from typing import Tuple, Set, Iterable, Optional, List, Type + from typing import Iterable, List, Optional, Set, Tuple, Type + from pip._internal.index.package_finder import PackageFinder logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/cache.py b/src/pip/_internal/cache.py index 07db948b9bf..def8dd64a18 100644 --- a/src/pip/_internal/cache.py +++ b/src/pip/_internal/cache.py @@ -17,7 +17,7 @@ from pip._internal.utils.urls import path_to_url if MYPY_CHECK_RUNNING: - from typing import Optional, Set, List, Any, Dict + from typing import Any, Dict, List, Optional, Set from pip._vendor.packaging.tags import Tag diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index 197400a72c5..8f14aa6b7a1 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -43,12 +43,11 @@ from pip._internal.utils.virtualenv import running_under_virtualenv if MYPY_CHECK_RUNNING: - from typing import List, Optional, Tuple, Any from optparse import Values + from typing import Any, List, Optional, Tuple - from pip._internal.utils.temp_dir import ( + from pip._internal.utils.temp_dir import \ TempDirectoryTypeRegistry as TempDirRegistry - ) __all__ = ['Command'] diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index f94ddfabb7f..2f640b2cbb2 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -30,8 +30,9 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Any, Callable, Dict, Optional, Tuple from optparse import OptionParser, Values + from typing import Any, Callable, Dict, Optional, Tuple + from pip._internal.cli.parser import ConfigOptionParser diff --git a/src/pip/_internal/cli/command_context.py b/src/pip/_internal/cli/command_context.py index d1a64a77606..669c777749d 100644 --- a/src/pip/_internal/cli/command_context.py +++ b/src/pip/_internal/cli/command_context.py @@ -5,7 +5,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Iterator, ContextManager, TypeVar + from typing import ContextManager, Iterator, TypeVar _T = TypeVar('_T', covariant=True) diff --git a/src/pip/_internal/cli/main_parser.py b/src/pip/_internal/cli/main_parser.py index 08c82c1f711..6356d831df1 100644 --- a/src/pip/_internal/cli/main_parser.py +++ b/src/pip/_internal/cli/main_parser.py @@ -15,7 +15,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Tuple, List + from typing import List, Tuple __all__ = ["create_main_parser", "parse_command"] diff --git a/src/pip/_internal/cli/spinners.py b/src/pip/_internal/cli/spinners.py index c6c4c5cd1b1..65c3c23d742 100644 --- a/src/pip/_internal/cli/spinners.py +++ b/src/pip/_internal/cli/spinners.py @@ -13,7 +13,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Iterator, IO + from typing import IO, Iterator logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/commands/__init__.py b/src/pip/_internal/commands/__init__.py index 6825fa6e2d4..4f0c4ba3ab9 100644 --- a/src/pip/_internal/commands/__init__.py +++ b/src/pip/_internal/commands/__init__.py @@ -18,6 +18,7 @@ if MYPY_CHECK_RUNNING: from typing import Any + from pip._internal.cli.base_command import Command diff --git a/src/pip/_internal/commands/check.py b/src/pip/_internal/commands/check.py index b557ca64113..e066bb63c74 100644 --- a/src/pip/_internal/commands/check.py +++ b/src/pip/_internal/commands/check.py @@ -12,8 +12,8 @@ logger = logging.getLogger(__name__) if MYPY_CHECK_RUNNING: - from typing import List, Any from optparse import Values + from typing import Any, List class CheckCommand(Command): diff --git a/src/pip/_internal/commands/completion.py b/src/pip/_internal/commands/completion.py index 9b99f51f006..b19f1ed1a56 100644 --- a/src/pip/_internal/commands/completion.py +++ b/src/pip/_internal/commands/completion.py @@ -9,8 +9,8 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import List from optparse import Values + from typing import List BASE_COMPLETION = """ # pip {shell} completion start{script}# pip {shell} completion end diff --git a/src/pip/_internal/commands/configuration.py b/src/pip/_internal/commands/configuration.py index f9b3ab79d0b..2a6311acd74 100644 --- a/src/pip/_internal/commands/configuration.py +++ b/src/pip/_internal/commands/configuration.py @@ -15,8 +15,8 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import List, Any, Optional from optparse import Values + from typing import Any, List, Optional from pip._internal.configuration import Kind diff --git a/src/pip/_internal/commands/debug.py b/src/pip/_internal/commands/debug.py index ff369d7d967..1b65c43065b 100644 --- a/src/pip/_internal/commands/debug.py +++ b/src/pip/_internal/commands/debug.py @@ -19,9 +19,10 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from types import ModuleType - from typing import List, Optional, Dict from optparse import Values + from types import ModuleType + from typing import Dict, List, Optional + from pip._internal.configuration import Configuration logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/commands/help.py b/src/pip/_internal/commands/help.py index a2edc29897f..2ab2b6d8f25 100644 --- a/src/pip/_internal/commands/help.py +++ b/src/pip/_internal/commands/help.py @@ -6,8 +6,8 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import List from optparse import Values + from typing import List class HelpCommand(Command): @@ -20,7 +20,9 @@ class HelpCommand(Command): def run(self, options, args): # type: (Values, List[str]) -> int from pip._internal.commands import ( - commands_dict, create_command, get_similar_commands, + commands_dict, + create_command, + get_similar_commands, ) try: diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index 7c18a7d9f62..a6dfa5fd578 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -24,11 +24,12 @@ if MYPY_CHECK_RUNNING: from optparse import Values - from typing import List, Set, Tuple, Iterator + from typing import Iterator, List, Set, Tuple - from pip._internal.network.session import PipSession from pip._vendor.pkg_resources import Distribution + from pip._internal.network.session import PipSession + logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/commands/search.py b/src/pip/_internal/commands/search.py index ff09472021e..ea4f47db5b5 100644 --- a/src/pip/_internal/commands/search.py +++ b/src/pip/_internal/commands/search.py @@ -24,7 +24,8 @@ if MYPY_CHECK_RUNNING: from optparse import Values - from typing import List, Dict, Optional + from typing import Dict, List, Optional + from typing_extensions import TypedDict TransformedHit = TypedDict( 'TransformedHit', diff --git a/src/pip/_internal/commands/show.py b/src/pip/_internal/commands/show.py index 3892c5959ee..b0b3f3abdcc 100644 --- a/src/pip/_internal/commands/show.py +++ b/src/pip/_internal/commands/show.py @@ -14,7 +14,7 @@ if MYPY_CHECK_RUNNING: from optparse import Values - from typing import List, Dict, Iterator + from typing import Dict, Iterator, List logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/configuration.py b/src/pip/_internal/configuration.py index 13cab923070..23614fd2bbe 100644 --- a/src/pip/_internal/configuration.py +++ b/src/pip/_internal/configuration.py @@ -28,9 +28,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import ( - Any, Dict, Iterable, List, NewType, Optional, Tuple - ) + from typing import Any, Dict, Iterable, List, NewType, Optional, Tuple RawConfigParser = configparser.RawConfigParser # Shorthand Kind = NewType("Kind", str) diff --git a/src/pip/_internal/distributions/base.py b/src/pip/_internal/distributions/base.py index b836b98d162..3a789f80433 100644 --- a/src/pip/_internal/distributions/base.py +++ b/src/pip/_internal/distributions/base.py @@ -8,8 +8,9 @@ from typing import Optional from pip._vendor.pkg_resources import Distribution - from pip._internal.req import InstallRequirement + from pip._internal.index.package_finder import PackageFinder + from pip._internal.req import InstallRequirement @add_metaclass(abc.ABCMeta) diff --git a/src/pip/_internal/distributions/installed.py b/src/pip/_internal/distributions/installed.py index 0d15bf42405..a813b211fe6 100644 --- a/src/pip/_internal/distributions/installed.py +++ b/src/pip/_internal/distributions/installed.py @@ -5,6 +5,7 @@ from typing import Optional from pip._vendor.pkg_resources import Distribution + from pip._internal.index.package_finder import PackageFinder diff --git a/src/pip/_internal/distributions/sdist.py b/src/pip/_internal/distributions/sdist.py index be3d7d97a1c..06b9df09cbe 100644 --- a/src/pip/_internal/distributions/sdist.py +++ b/src/pip/_internal/distributions/sdist.py @@ -10,6 +10,7 @@ from typing import Set, Tuple from pip._vendor.pkg_resources import Distribution + from pip._internal.index.package_finder import PackageFinder diff --git a/src/pip/_internal/distributions/wheel.py b/src/pip/_internal/distributions/wheel.py index bf3482b151f..2adc2286271 100644 --- a/src/pip/_internal/distributions/wheel.py +++ b/src/pip/_internal/distributions/wheel.py @@ -6,6 +6,7 @@ if MYPY_CHECK_RUNNING: from pip._vendor.pkg_resources import Distribution + from pip._internal.index.package_finder import PackageFinder diff --git a/src/pip/_internal/exceptions.py b/src/pip/_internal/exceptions.py index 3f26215d657..62bde1eeda4 100644 --- a/src/pip/_internal/exceptions.py +++ b/src/pip/_internal/exceptions.py @@ -9,10 +9,10 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Any, Optional, List, Dict, Text + from typing import Any, Dict, List, Optional, Text from pip._vendor.pkg_resources import Distribution - from pip._vendor.requests.models import Response, Request + from pip._vendor.requests.models import Request, Response from pip._vendor.six import PY3 from pip._vendor.six.moves import configparser diff --git a/src/pip/_internal/index/collector.py b/src/pip/_internal/index/collector.py index 6c35fc66076..e6230c76734 100644 --- a/src/pip/_internal/index/collector.py +++ b/src/pip/_internal/index/collector.py @@ -28,12 +28,20 @@ from pip._internal.vcs import is_url, vcs if MYPY_CHECK_RUNNING: + import xml.etree.ElementTree from optparse import Values from typing import ( - Callable, Iterable, List, MutableMapping, Optional, - Protocol, Sequence, Tuple, TypeVar, Union, + Callable, + Iterable, + List, + MutableMapping, + Optional, + Protocol, + Sequence, + Tuple, + TypeVar, + Union, ) - import xml.etree.ElementTree from pip._vendor.requests import Response diff --git a/src/pip/_internal/index/package_finder.py b/src/pip/_internal/index/package_finder.py index 84115783ab8..19a9c2c1370 100644 --- a/src/pip/_internal/index/package_finder.py +++ b/src/pip/_internal/index/package_finder.py @@ -35,7 +35,14 @@ if MYPY_CHECK_RUNNING: from typing import ( - FrozenSet, Iterable, List, Optional, Set, Text, Tuple, Union, + FrozenSet, + Iterable, + List, + Optional, + Set, + Text, + Tuple, + Union, ) from pip._vendor.packaging.tags import Tag diff --git a/src/pip/_internal/locations.py b/src/pip/_internal/locations.py index 0c1235488d6..35a4512b4b1 100644 --- a/src/pip/_internal/locations.py +++ b/src/pip/_internal/locations.py @@ -22,9 +22,8 @@ from pip._internal.utils.virtualenv import running_under_virtualenv if MYPY_CHECK_RUNNING: - from typing import Dict, List, Optional, Union - from distutils.cmd import Command as DistutilsCommand + from typing import Dict, List, Optional, Union # Application Directories diff --git a/src/pip/_internal/main.py b/src/pip/_internal/main.py index 3208d5b8820..1c99c49a1f1 100644 --- a/src/pip/_internal/main.py +++ b/src/pip/_internal/main.py @@ -1,7 +1,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Optional, List + from typing import List, Optional def main(args=None): diff --git a/src/pip/_internal/models/candidate.py b/src/pip/_internal/models/candidate.py index 9149e0fc69c..0d89a8c07da 100644 --- a/src/pip/_internal/models/candidate.py +++ b/src/pip/_internal/models/candidate.py @@ -5,6 +5,7 @@ if MYPY_CHECK_RUNNING: from pip._vendor.packaging.version import _BaseVersion + from pip._internal.models.link import Link diff --git a/src/pip/_internal/models/direct_url.py b/src/pip/_internal/models/direct_url.py index 87bd9fe4b8f..99aa68d121b 100644 --- a/src/pip/_internal/models/direct_url.py +++ b/src/pip/_internal/models/direct_url.py @@ -8,9 +8,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import ( - Any, Dict, Iterable, Optional, Type, TypeVar, Union - ) + from typing import Any, Dict, Iterable, Optional, Type, TypeVar, Union T = TypeVar("T") diff --git a/src/pip/_internal/models/format_control.py b/src/pip/_internal/models/format_control.py index c6275e721b3..adcf61e2854 100644 --- a/src/pip/_internal/models/format_control.py +++ b/src/pip/_internal/models/format_control.py @@ -4,7 +4,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Optional, Set, FrozenSet + from typing import FrozenSet, Optional, Set class FormatControl(object): diff --git a/src/pip/_internal/models/link.py b/src/pip/_internal/models/link.py index c0d278adee9..29ef402beef 100644 --- a/src/pip/_internal/models/link.py +++ b/src/pip/_internal/models/link.py @@ -16,6 +16,7 @@ if MYPY_CHECK_RUNNING: from typing import Optional, Text, Tuple, Union + from pip._internal.index.collector import HTMLPage from pip._internal.utils.hashes import Hashes diff --git a/src/pip/_internal/models/selection_prefs.py b/src/pip/_internal/models/selection_prefs.py index 5db3ca91ca6..83110dd8f90 100644 --- a/src/pip/_internal/models/selection_prefs.py +++ b/src/pip/_internal/models/selection_prefs.py @@ -2,6 +2,7 @@ if MYPY_CHECK_RUNNING: from typing import Optional + from pip._internal.models.format_control import FormatControl diff --git a/src/pip/_internal/network/auth.py b/src/pip/_internal/network/auth.py index c49deaaf1b7..357811a16f1 100644 --- a/src/pip/_internal/network/auth.py +++ b/src/pip/_internal/network/auth.py @@ -20,11 +20,11 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Dict, Optional, Tuple, List, Any + from typing import Any, Dict, List, Optional, Tuple - from pip._internal.vcs.versioncontrol import AuthInfo + from pip._vendor.requests.models import Request, Response - from pip._vendor.requests.models import Response, Request + from pip._internal.vcs.versioncontrol import AuthInfo Credentials = Tuple[str, str, str] diff --git a/src/pip/_internal/network/cache.py b/src/pip/_internal/network/cache.py index a0d55b5e992..d2a1b7313f7 100644 --- a/src/pip/_internal/network/cache.py +++ b/src/pip/_internal/network/cache.py @@ -13,7 +13,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Optional, Iterator + from typing import Iterator, Optional def is_from_cache(response): diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py index 68ef68db535..176f0fb682c 100644 --- a/src/pip/_internal/network/session.py +++ b/src/pip/_internal/network/session.py @@ -37,9 +37,7 @@ from pip._internal.utils.urls import url_to_path if MYPY_CHECK_RUNNING: - from typing import ( - Iterator, List, Optional, Tuple, Union, - ) + from typing import Iterator, List, Optional, Tuple, Union from pip._internal.models.link import Link diff --git a/src/pip/_internal/network/xmlrpc.py b/src/pip/_internal/network/xmlrpc.py index e61126241e8..504018f28fe 100644 --- a/src/pip/_internal/network/xmlrpc.py +++ b/src/pip/_internal/network/xmlrpc.py @@ -14,6 +14,7 @@ if MYPY_CHECK_RUNNING: from typing import Dict + from pip._internal.network.session import PipSession diff --git a/src/pip/_internal/operations/build/metadata.py b/src/pip/_internal/operations/build/metadata.py index cf52f8d8f63..5709962b09e 100644 --- a/src/pip/_internal/operations/build/metadata.py +++ b/src/pip/_internal/operations/build/metadata.py @@ -8,9 +8,10 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from pip._internal.build_env import BuildEnvironment from pip._vendor.pep517.wrappers import Pep517HookCaller + from pip._internal.build_env import BuildEnvironment + def generate_metadata(build_env, backend): # type: (BuildEnvironment, Pep517HookCaller) -> str diff --git a/src/pip/_internal/operations/build/wheel.py b/src/pip/_internal/operations/build/wheel.py index 0c28c4989dc..d16ee0966e1 100644 --- a/src/pip/_internal/operations/build/wheel.py +++ b/src/pip/_internal/operations/build/wheel.py @@ -6,6 +6,7 @@ if MYPY_CHECK_RUNNING: from typing import List, Optional + from pip._vendor.pep517.wrappers import Pep517HookCaller logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/operations/check.py b/src/pip/_internal/operations/check.py index 5714915bcb2..52e8116e199 100644 --- a/src/pip/_internal/operations/check.py +++ b/src/pip/_internal/operations/check.py @@ -16,10 +16,9 @@ logger = logging.getLogger(__name__) if MYPY_CHECK_RUNNING: + from typing import Any, Callable, Dict, List, Optional, Set, Tuple + from pip._internal.req.req_install import InstallRequirement - from typing import ( - Any, Callable, Dict, Optional, Set, Tuple, List - ) # Shorthands PackageSet = Dict[str, 'PackageDetails'] diff --git a/src/pip/_internal/operations/freeze.py b/src/pip/_internal/operations/freeze.py index ddb9cb232ce..b98b8cd79b5 100644 --- a/src/pip/_internal/operations/freeze.py +++ b/src/pip/_internal/operations/freeze.py @@ -26,12 +26,20 @@ if MYPY_CHECK_RUNNING: from typing import ( - Iterator, Optional, List, Container, Set, Dict, Tuple, Iterable, Union + Container, + Dict, + Iterable, + Iterator, + List, + Optional, + Set, + Tuple, + Union, ) + + from pip._vendor.pkg_resources import Distribution, Requirement + from pip._internal.cache import WheelCache - from pip._vendor.pkg_resources import ( - Distribution, Requirement - ) RequirementInfo = Tuple[Optional[Union[str, Requirement]], bool, List[str]] @@ -183,7 +191,7 @@ def get_requirement_info(dist): location = os.path.normcase(os.path.abspath(dist.location)) - from pip._internal.vcs import vcs, RemoteNotFoundError + from pip._internal.vcs import RemoteNotFoundError, vcs vcs_backend = vcs.get_backend_for_dir(location) if vcs_backend is None: diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index e91b1b8d558..af6b39052d9 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -62,10 +62,10 @@ else: from email.message import Message from typing import ( + IO, Any, Callable, Dict, - IO, Iterable, Iterator, List, diff --git a/src/pip/_internal/pyproject.py b/src/pip/_internal/pyproject.py index 6b4faf7a752..4144a9ed60b 100644 --- a/src/pip/_internal/pyproject.py +++ b/src/pip/_internal/pyproject.py @@ -12,7 +12,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Any, Optional, List + from typing import Any, List, Optional def _is_list_of_str(obj): diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index 7a4641ef5a1..b089c502dfe 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -31,9 +31,8 @@ from pip._internal.vcs import is_url, vcs if MYPY_CHECK_RUNNING: - from typing import ( - Any, Dict, Optional, Set, Tuple, Union, - ) + from typing import Any, Dict, Optional, Set, Tuple, Union + from pip._internal.req.req_file import ParsedRequirement diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index c8d7a0a5ae2..b070dc640da 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -26,7 +26,15 @@ if MYPY_CHECK_RUNNING: from optparse import Values from typing import ( - Any, Callable, Dict, Iterator, List, NoReturn, Optional, Text, Tuple, + Any, + Callable, + Dict, + Iterator, + List, + NoReturn, + Optional, + Text, + Tuple, ) from pip._internal.index.package_finder import PackageFinder diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 72f792342dc..9a6763074a3 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -53,13 +53,13 @@ from pip._internal.vcs import vcs if MYPY_CHECK_RUNNING: - from typing import ( - Any, Dict, Iterable, List, Optional, Sequence, Union, - ) - from pip._internal.build_env import BuildEnvironment - from pip._vendor.pkg_resources import Distribution - from pip._vendor.packaging.specifiers import SpecifierSet + from typing import Any, Dict, Iterable, List, Optional, Sequence, Union + from pip._vendor.packaging.markers import Marker + from pip._vendor.packaging.specifiers import SpecifierSet + from pip._vendor.pkg_resources import Distribution + + from pip._internal.build_env import BuildEnvironment logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/req/req_set.py b/src/pip/_internal/req/req_set.py index ab4b6f849b4..c9ea3be5ddd 100644 --- a/src/pip/_internal/req/req_set.py +++ b/src/pip/_internal/req/req_set.py @@ -12,6 +12,7 @@ if MYPY_CHECK_RUNNING: from typing import Dict, Iterable, List, Optional, Tuple + from pip._internal.req.req_install import InstallRequirement diff --git a/src/pip/_internal/req/req_tracker.py b/src/pip/_internal/req/req_tracker.py index 13fb24563fe..7379c307b31 100644 --- a/src/pip/_internal/req/req_tracker.py +++ b/src/pip/_internal/req/req_tracker.py @@ -14,8 +14,9 @@ if MYPY_CHECK_RUNNING: from types import TracebackType from typing import Dict, Iterator, Optional, Set, Type, Union - from pip._internal.req.req_install import InstallRequirement + from pip._internal.models.link import Link + from pip._internal.req.req_install import InstallRequirement logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/req/req_uninstall.py b/src/pip/_internal/req/req_uninstall.py index 69719d338e6..2e7dfcc7369 100644 --- a/src/pip/_internal/req/req_uninstall.py +++ b/src/pip/_internal/req/req_uninstall.py @@ -29,8 +29,17 @@ if MYPY_CHECK_RUNNING: from typing import ( - Any, Callable, Dict, Iterable, Iterator, List, Optional, Set, Tuple, + Any, + Callable, + Dict, + Iterable, + Iterator, + List, + Optional, + Set, + Tuple, ) + from pip._vendor.pkg_resources import Distribution logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/resolution/base.py b/src/pip/_internal/resolution/base.py index 2fa118bd894..6d50555e531 100644 --- a/src/pip/_internal/resolution/base.py +++ b/src/pip/_internal/resolution/base.py @@ -2,6 +2,7 @@ if MYPY_CHECK_RUNNING: from typing import Callable, List + from pip._internal.req.req_install import InstallRequirement from pip._internal.req.req_set import RequirementSet diff --git a/src/pip/_internal/resolution/legacy/resolver.py b/src/pip/_internal/resolution/legacy/resolver.py index d2dafa77f2b..6ef00bba19d 100644 --- a/src/pip/_internal/resolution/legacy/resolver.py +++ b/src/pip/_internal/resolution/legacy/resolver.py @@ -42,6 +42,7 @@ if MYPY_CHECK_RUNNING: from typing import DefaultDict, List, Optional, Set, Tuple + from pip._vendor.pkg_resources import Distribution from pip._internal.cache import WheelCache diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 97b0b273070..96e2d53314d 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -38,8 +38,8 @@ if MYPY_CHECK_RUNNING: from typing import ( - FrozenSet, Dict, + FrozenSet, Iterable, List, Optional, diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index 80577a61c58..99f99bfc212 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -16,7 +16,7 @@ Union, ) - from .base import Requirement, Candidate + from .base import Candidate, Requirement from .factory import Factory # Notes on the relationship between the provider, the factory, and the diff --git a/src/pip/_internal/utils/compat.py b/src/pip/_internal/utils/compat.py index 89c5169af4e..cc63536783c 100644 --- a/src/pip/_internal/utils/compat.py +++ b/src/pip/_internal/utils/compat.py @@ -246,8 +246,8 @@ def get_terminal_size(): def ioctl_GWINSZ(fd): try: import fcntl - import termios import struct + import termios cr = struct.unpack_from( 'hh', fcntl.ioctl(fd, termios.TIOCGWINSZ, '12345678') diff --git a/src/pip/_internal/utils/direct_url_helpers.py b/src/pip/_internal/utils/direct_url_helpers.py index f1fe209e911..a355a6c5ee4 100644 --- a/src/pip/_internal/utils/direct_url_helpers.py +++ b/src/pip/_internal/utils/direct_url_helpers.py @@ -20,10 +20,10 @@ if MYPY_CHECK_RUNNING: from typing import Optional - from pip._internal.models.link import Link - from pip._vendor.pkg_resources import Distribution + from pip._internal.models.link import Link + logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/utils/encoding.py b/src/pip/_internal/utils/encoding.py index 5b83d61bb13..42a57535af8 100644 --- a/src/pip/_internal/utils/encoding.py +++ b/src/pip/_internal/utils/encoding.py @@ -6,7 +6,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import List, Tuple, Text + from typing import List, Text, Tuple BOMS = [ (codecs.BOM_UTF8, 'utf-8'), diff --git a/src/pip/_internal/utils/entrypoints.py b/src/pip/_internal/utils/entrypoints.py index befd01c8901..64d1cb2bd0b 100644 --- a/src/pip/_internal/utils/entrypoints.py +++ b/src/pip/_internal/utils/entrypoints.py @@ -4,7 +4,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Optional, List + from typing import List, Optional def _wrapper(args=None): diff --git a/src/pip/_internal/utils/hashes.py b/src/pip/_internal/utils/hashes.py index d9f74a64083..4d4e26b59f3 100644 --- a/src/pip/_internal/utils/hashes.py +++ b/src/pip/_internal/utils/hashes.py @@ -13,9 +13,8 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import ( - Dict, List, BinaryIO, NoReturn, Iterator - ) + from typing import BinaryIO, Dict, Iterator, List, NoReturn + from pip._vendor.six import PY3 if PY3: from hashlib import _Hash diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 5629c60c1c2..289db0e39fd 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -54,9 +54,20 @@ if MYPY_CHECK_RUNNING: from typing import ( - Any, AnyStr, Callable, Container, Iterable, Iterator, List, Optional, - Text, Tuple, TypeVar, Union, + Any, + AnyStr, + Callable, + Container, + Iterable, + Iterator, + List, + Optional, + Text, + Tuple, + TypeVar, + Union, ) + from pip._vendor.pkg_resources import Distribution VersionInfo = Tuple[int, int, int] diff --git a/src/pip/_internal/utils/packaging.py b/src/pip/_internal/utils/packaging.py index 68aa86edbf0..27fd204234f 100644 --- a/src/pip/_internal/utils/packaging.py +++ b/src/pip/_internal/utils/packaging.py @@ -11,8 +11,9 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Optional, Tuple from email.message import Message + from typing import Optional, Tuple + from pip._vendor.pkg_resources import Distribution diff --git a/src/pip/_internal/utils/parallel.py b/src/pip/_internal/utils/parallel.py index 9fe1fe8b9e4..d4113bdc285 100644 --- a/src/pip/_internal/utils/parallel.py +++ b/src/pip/_internal/utils/parallel.py @@ -29,8 +29,8 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Callable, Iterable, Iterator, Union, TypeVar from multiprocessing import pool + from typing import Callable, Iterable, Iterator, TypeVar, Union Pool = Union[pool.Pool, pool.ThreadPool] S = TypeVar('S') diff --git a/src/pip/_internal/utils/subprocess.py b/src/pip/_internal/utils/subprocess.py index d398e68da53..1dfe02fa0be 100644 --- a/src/pip/_internal/utils/subprocess.py +++ b/src/pip/_internal/utils/subprocess.py @@ -15,7 +15,14 @@ if MYPY_CHECK_RUNNING: from typing import ( - Any, Callable, Iterable, List, Mapping, Optional, Text, Union, + Any, + Callable, + Iterable, + List, + Mapping, + Optional, + Text, + Union, ) CommandArgs = List[Union[str, HiddenText]] diff --git a/src/pip/_internal/vcs/bazaar.py b/src/pip/_internal/vcs/bazaar.py index 94408c52fa9..3180713f7db 100644 --- a/src/pip/_internal/vcs/bazaar.py +++ b/src/pip/_internal/vcs/bazaar.py @@ -16,6 +16,7 @@ if MYPY_CHECK_RUNNING: from typing import Optional, Tuple + from pip._internal.utils.misc import HiddenText from pip._internal.vcs.versioncontrol import AuthInfo, RevOptions diff --git a/src/pip/_internal/vcs/git.py b/src/pip/_internal/vcs/git.py index db8c7234984..1831aede58a 100644 --- a/src/pip/_internal/vcs/git.py +++ b/src/pip/_internal/vcs/git.py @@ -25,6 +25,7 @@ if MYPY_CHECK_RUNNING: from typing import Optional, Tuple + from pip._internal.utils.misc import HiddenText from pip._internal.vcs.versioncontrol import AuthInfo, RevOptions diff --git a/src/pip/_internal/vcs/subversion.py b/src/pip/_internal/vcs/subversion.py index ab134970b05..eae09c19610 100644 --- a/src/pip/_internal/vcs/subversion.py +++ b/src/pip/_internal/vcs/subversion.py @@ -26,8 +26,9 @@ if MYPY_CHECK_RUNNING: from typing import Optional, Tuple - from pip._internal.utils.subprocess import CommandArgs + from pip._internal.utils.misc import HiddenText + from pip._internal.utils.subprocess import CommandArgs from pip._internal.vcs.versioncontrol import AuthInfo, RevOptions diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index 96f830f9918..e4f61a99c12 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -38,9 +38,19 @@ if MYPY_CHECK_RUNNING: from typing import ( - Dict, Iterable, Iterator, List, Optional, Text, Tuple, - Type, Union, Mapping, Any + Any, + Dict, + Iterable, + Iterator, + List, + Mapping, + Optional, + Text, + Tuple, + Type, + Union, ) + from pip._internal.utils.misc import HiddenText from pip._internal.utils.subprocess import CommandArgs diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index fa08016bdfb..27fce66c264 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -19,9 +19,7 @@ from pip._internal.vcs import vcs if MYPY_CHECK_RUNNING: - from typing import ( - Any, Callable, Iterable, List, Optional, Tuple, - ) + from typing import Any, Callable, Iterable, List, Optional, Tuple from pip._internal.cache import WheelCache from pip._internal.req.req_install import InstallRequirement diff --git a/tests/conftest.py b/tests/conftest.py index 97a25f60e9f..ffd9f42158b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -27,7 +27,8 @@ if MYPY_CHECK_RUNNING: from typing import Dict, Iterable - from tests.lib.server import MockServer as _MockServer, Responder + from tests.lib.server import MockServer as _MockServer + from tests.lib.server import Responder def pytest_addoption(parser): diff --git a/tests/functional/test_uninstall.py b/tests/functional/test_uninstall.py index 6e4aec0e584..5def6587174 100644 --- a/tests/functional/test_uninstall.py +++ b/tests/functional/test_uninstall.py @@ -422,9 +422,10 @@ def test_uninstallpathset_no_paths(caplog): Test UninstallPathSet logs notification when there are no paths to uninstall """ - from pip._internal.req.req_uninstall import UninstallPathSet from pkg_resources import get_distribution + from pip._internal.req.req_uninstall import UninstallPathSet + caplog.set_level(logging.INFO) test_dist = get_distribution('pip') diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index ee243d0e6f1..07569d814f4 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -31,6 +31,7 @@ if MYPY_CHECK_RUNNING: from typing import List, Optional + from pip._internal.models.target_python import TargetPython diff --git a/tests/lib/server.py b/tests/lib/server.py index 6a8862ac5a9..ebbf120d3d7 100644 --- a/tests/lib/server.py +++ b/tests/lib/server.py @@ -15,7 +15,16 @@ if MYPY_CHECK_RUNNING: from types import TracebackType from typing import ( - Any, Callable, Dict, Iterable, List, Optional, Text, Tuple, Type, Union + Any, + Callable, + Dict, + Iterable, + List, + Optional, + Text, + Tuple, + Type, + Union, ) from werkzeug.serving import BaseWSGIServer diff --git a/tests/lib/wheel.py b/tests/lib/wheel.py index 8ea458658e3..d89a680a190 100644 --- a/tests/lib/wheel.py +++ b/tests/lib/wheel.py @@ -20,8 +20,16 @@ if MYPY_CHECK_RUNNING: from typing import ( - AnyStr, Callable, Dict, List, Iterable, Optional, Tuple, Sequence, - TypeVar, Union, + AnyStr, + Callable, + Dict, + Iterable, + List, + Optional, + Sequence, + Tuple, + TypeVar, + Union, ) # path, digest, size diff --git a/tests/unit/test_locations.py b/tests/unit/test_locations.py index c9bbe794326..2ede236f91f 100644 --- a/tests/unit/test_locations.py +++ b/tests/unit/test_locations.py @@ -100,6 +100,7 @@ def test_distutils_config_file_read(self, tmpdir, monkeypatch): f.parent.mkdir() f.write_text("[install]\ninstall-scripts=" + install_scripts) from distutils.dist import Distribution + # patch the function that returns what config files are present monkeypatch.setattr( Distribution, @@ -121,6 +122,7 @@ def test_install_lib_takes_precedence(self, tmpdir, monkeypatch): f.parent.mkdir() f.write_text("[install]\ninstall-lib=" + install_lib) from distutils.dist import Distribution + # patch the function that returns what config files are present monkeypatch.setattr( Distribution, From 3a8e8578a87458719ca0dbb9b31989fffba230a9 Mon Sep 17 00:00:00 2001 From: Elisha Hollander <just4now666666@gmail.com> Date: Wed, 23 Sep 2020 17:42:29 +0300 Subject: [PATCH 2536/3170] Update LICENSE.txt --- LICENSE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE.txt b/LICENSE.txt index 737fec5c535..75eb0fd80b0 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2008-2019 The pip developers (see AUTHORS.txt file) +Copyright (c) 2008-2020 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 From a00f85d22bd86af2b8ecfff53efc732e95c20e7e Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Wed, 23 Sep 2020 19:59:10 +0530 Subject: [PATCH 2537/3170] Prepare flake8 for black --- setup.cfg | 3 +++ 1 file changed, 3 insertions(+) diff --git a/setup.cfg b/setup.cfg index 45fd58a3e7a..6b034f0eebe 100644 --- a/setup.cfg +++ b/setup.cfg @@ -16,6 +16,7 @@ default_section = THIRDPARTY include_trailing_comma = true [flake8] +max-line-length = 88 exclude = ./build, .nox, @@ -28,6 +29,8 @@ ignore = G200, G202, # pycodestyle checks ignored in the default configuration E121, E123, E126, E133, E226, E241, E242, E704, W503, W504, W505, + # black adds spaces around ':' + E203, per-file-ignores = # G: The plugin logging-format treats every .log and .error as logging. noxfile.py: G From 58c594c06b46842e830faa7e1ce7a5df5819daef Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Wed, 23 Sep 2020 19:57:09 +0530 Subject: [PATCH 2538/3170] Prepare isort for black --- setup.cfg | 3 +-- src/pip/_internal/cli/base_command.py | 15 +++++-------- src/pip/_internal/cli/main_parser.py | 5 +---- src/pip/_internal/cli/req_command.py | 5 +---- src/pip/_internal/commands/configuration.py | 6 +----- src/pip/_internal/commands/search.py | 1 + src/pip/_internal/index/package_finder.py | 11 +--------- src/pip/_internal/models/target_python.py | 5 +---- src/pip/_internal/network/download.py | 12 ++--------- src/pip/_internal/network/lazy_wheel.py | 6 +----- src/pip/_internal/network/session.py | 1 + .../operations/build/wheel_legacy.py | 4 +--- src/pip/_internal/operations/check.py | 4 +--- src/pip/_internal/operations/freeze.py | 5 +---- src/pip/_internal/operations/install/wheel.py | 21 +++---------------- src/pip/_internal/operations/prepare.py | 11 ++-------- src/pip/_internal/req/req_file.py | 5 +---- src/pip/_internal/req/req_install.py | 10 +++++---- .../_internal/resolution/legacy/resolver.py | 5 +---- .../resolution/resolvelib/provider.py | 11 +--------- src/pip/_internal/self_outdated_check.py | 12 ++--------- src/pip/_internal/utils/hashes.py | 6 +----- src/pip/_internal/utils/misc.py | 14 +++---------- src/pip/_internal/utils/subprocess.py | 11 +--------- src/pip/_internal/vcs/versioncontrol.py | 6 +----- tests/functional/test_configuration.py | 5 +---- tests/functional/test_new_resolver_hashes.py | 5 +---- tests/functional/test_search.py | 6 +----- tests/functional/test_uninstall.py | 6 +----- tests/unit/resolution_resolvelib/conftest.py | 1 + tests/unit/test_finder.py | 5 +---- tests/unit/test_operations_prepare.py | 6 +----- tests/unit/test_req_file.py | 5 +---- tests/unit/test_resolution_legacy_resolver.py | 5 +---- tests/unit/test_utils.py | 6 +----- tests/unit/test_utils_unpacking.py | 6 +----- tests/unit/test_wheel.py | 4 +--- 37 files changed, 53 insertions(+), 202 deletions(-) diff --git a/setup.cfg b/setup.cfg index 6b034f0eebe..450f953e4a5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -1,4 +1,5 @@ [isort] +profile = black skip = ./build, .nox, @@ -6,14 +7,12 @@ skip = .scratch, _vendor, data -multi_line_output = 3 known_third_party = pip._vendor known_first_party = pip tests default_section = THIRDPARTY -include_trailing_comma = true [flake8] max-line-length = 88 diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index 8f14aa6b7a1..e4b07e0ce8c 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -12,10 +12,7 @@ from pip._internal.cli import cmdoptions from pip._internal.cli.command_context import CommandContextMixIn -from pip._internal.cli.parser import ( - ConfigOptionParser, - UpdatingDefaultsHelpFormatter, -) +from pip._internal.cli.parser import ConfigOptionParser, UpdatingDefaultsHelpFormatter from pip._internal.cli.status_codes import ( ERROR, PREVIOUS_BUILD_DIR_ERROR, @@ -35,10 +32,7 @@ from pip._internal.utils.filesystem import check_path_owner from pip._internal.utils.logging import BrokenStdoutLoggingError, setup_logging from pip._internal.utils.misc import get_prog, normalize_path -from pip._internal.utils.temp_dir import ( - global_tempdir_manager, - tempdir_registry, -) +from pip._internal.utils.temp_dir import global_tempdir_manager, tempdir_registry from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.virtualenv import running_under_virtualenv @@ -46,8 +40,9 @@ from optparse import Values from typing import Any, List, Optional, Tuple - from pip._internal.utils.temp_dir import \ - TempDirectoryTypeRegistry as TempDirRegistry + from pip._internal.utils.temp_dir import ( + TempDirectoryTypeRegistry as TempDirRegistry, + ) __all__ = ['Command'] diff --git a/src/pip/_internal/cli/main_parser.py b/src/pip/_internal/cli/main_parser.py index 6356d831df1..ba3cf68aafb 100644 --- a/src/pip/_internal/cli/main_parser.py +++ b/src/pip/_internal/cli/main_parser.py @@ -5,10 +5,7 @@ import sys from pip._internal.cli import cmdoptions -from pip._internal.cli.parser import ( - ConfigOptionParser, - UpdatingDefaultsHelpFormatter, -) +from pip._internal.cli.parser import ConfigOptionParser, UpdatingDefaultsHelpFormatter from pip._internal.commands import commands_dict, get_similar_commands from pip._internal.exceptions import CommandError from pip._internal.utils.misc import get_pip_version, get_prog diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 76abce5acdf..0757e34f66e 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -38,10 +38,7 @@ from pip._internal.req.req_install import InstallRequirement from pip._internal.req.req_tracker import RequirementTracker from pip._internal.resolution.base import BaseResolver - from pip._internal.utils.temp_dir import ( - TempDirectory, - TempDirectoryTypeRegistry, - ) + from pip._internal.utils.temp_dir import TempDirectory, TempDirectoryTypeRegistry logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/commands/configuration.py b/src/pip/_internal/commands/configuration.py index 2a6311acd74..1ab90b47b43 100644 --- a/src/pip/_internal/commands/configuration.py +++ b/src/pip/_internal/commands/configuration.py @@ -4,11 +4,7 @@ from pip._internal.cli.base_command import Command from pip._internal.cli.status_codes import ERROR, SUCCESS -from pip._internal.configuration import ( - Configuration, - get_configuration_files, - kinds, -) +from pip._internal.configuration import Configuration, get_configuration_files, kinds from pip._internal.exceptions import PipError from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import get_prog, write_output diff --git a/src/pip/_internal/commands/search.py b/src/pip/_internal/commands/search.py index ea4f47db5b5..146d653e55f 100644 --- a/src/pip/_internal/commands/search.py +++ b/src/pip/_internal/commands/search.py @@ -7,6 +7,7 @@ from pip._vendor import pkg_resources from pip._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 pip._vendor.six.moves import xmlrpc_client # type: ignore diff --git a/src/pip/_internal/index/package_finder.py b/src/pip/_internal/index/package_finder.py index 19a9c2c1370..5162a8191d3 100644 --- a/src/pip/_internal/index/package_finder.py +++ b/src/pip/_internal/index/package_finder.py @@ -34,16 +34,7 @@ from pip._internal.utils.urls import url_to_path if MYPY_CHECK_RUNNING: - from typing import ( - FrozenSet, - Iterable, - List, - Optional, - Set, - Text, - Tuple, - Union, - ) + from typing import FrozenSet, Iterable, List, Optional, Set, Text, Tuple, Union from pip._vendor.packaging.tags import Tag from pip._vendor.packaging.version import _BaseVersion diff --git a/src/pip/_internal/models/target_python.py b/src/pip/_internal/models/target_python.py index 6d1ca79645f..ad7e506a6a9 100644 --- a/src/pip/_internal/models/target_python.py +++ b/src/pip/_internal/models/target_python.py @@ -1,9 +1,6 @@ import sys -from pip._internal.utils.compatibility_tags import ( - get_supported, - version_info_to_nodot, -) +from pip._internal.utils.compatibility_tags import get_supported, version_info_to_nodot from pip._internal.utils.misc import normalize_version_info from pip._internal.utils.typing import MYPY_CHECK_RUNNING diff --git a/src/pip/_internal/network/download.py b/src/pip/_internal/network/download.py index 56feaabac10..76896e89970 100644 --- a/src/pip/_internal/network/download.py +++ b/src/pip/_internal/network/download.py @@ -11,16 +11,8 @@ from pip._internal.exceptions import NetworkConnectionError from pip._internal.models.index import PyPI from pip._internal.network.cache import is_from_cache -from pip._internal.network.utils import ( - HEADERS, - raise_for_status, - response_chunks, -) -from pip._internal.utils.misc import ( - format_size, - redact_auth_from_url, - splitext, -) +from pip._internal.network.utils import HEADERS, raise_for_status, response_chunks +from pip._internal.utils.misc import format_size, redact_auth_from_url, splitext from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: diff --git a/src/pip/_internal/network/lazy_wheel.py b/src/pip/_internal/network/lazy_wheel.py index a0f9e151dd7..608475abab3 100644 --- a/src/pip/_internal/network/lazy_wheel.py +++ b/src/pip/_internal/network/lazy_wheel.py @@ -10,11 +10,7 @@ from pip._vendor.requests.models import CONTENT_CHUNK_SIZE from pip._vendor.six.moves import range -from pip._internal.network.utils import ( - HEADERS, - raise_for_status, - response_chunks, -) +from pip._internal.network.utils import HEADERS, raise_for_status, response_chunks from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.wheel import pkg_resources_distribution_for_wheel diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py index 176f0fb682c..454945d9aed 100644 --- a/src/pip/_internal/network/session.py +++ b/src/pip/_internal/network/session.py @@ -25,6 +25,7 @@ from pip import __version__ from pip._internal.network.auth import MultiDomainBasicAuth from pip._internal.network.cache import SafeFileCache + # Import ssl from compat so the initial import occurs in only one place. from pip._internal.utils.compat import has_tls, ipaddress from pip._internal.utils.glibc import libc_ver diff --git a/src/pip/_internal/operations/build/wheel_legacy.py b/src/pip/_internal/operations/build/wheel_legacy.py index 37dc876acbd..9da365e4ddd 100644 --- a/src/pip/_internal/operations/build/wheel_legacy.py +++ b/src/pip/_internal/operations/build/wheel_legacy.py @@ -2,9 +2,7 @@ import os.path from pip._internal.cli.spinners import open_spinner -from pip._internal.utils.setuptools_build import ( - make_setuptools_bdist_wheel_args, -) +from pip._internal.utils.setuptools_build import make_setuptools_bdist_wheel_args from pip._internal.utils.subprocess import ( LOG_DIVIDER, call_subprocess, diff --git a/src/pip/_internal/operations/check.py b/src/pip/_internal/operations/check.py index 52e8116e199..bc44d4357f6 100644 --- a/src/pip/_internal/operations/check.py +++ b/src/pip/_internal/operations/check.py @@ -7,9 +7,7 @@ from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.pkg_resources import RequirementParseError -from pip._internal.distributions import ( - make_distribution_for_install_requirement, -) +from pip._internal.distributions import make_distribution_for_install_requirement from pip._internal.utils.misc import get_installed_distributions from pip._internal.utils.typing import MYPY_CHECK_RUNNING diff --git a/src/pip/_internal/operations/freeze.py b/src/pip/_internal/operations/freeze.py index b98b8cd79b5..d4f790cd447 100644 --- a/src/pip/_internal/operations/freeze.py +++ b/src/pip/_internal/operations/freeze.py @@ -18,10 +18,7 @@ direct_url_as_pep440_direct_reference, dist_get_direct_url, ) -from pip._internal.utils.misc import ( - dist_is_editable, - get_installed_distributions, -) +from pip._internal.utils.misc import dist_is_editable, get_installed_distributions from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index af6b39052d9..8b67ebb9431 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -21,14 +21,7 @@ from pip._vendor import pkg_resources from pip._vendor.distlib.scripts import ScriptMaker from pip._vendor.distlib.util import get_export_entry -from pip._vendor.six import ( - PY2, - ensure_str, - ensure_text, - itervalues, - reraise, - text_type, -) +from pip._vendor.six import PY2, ensure_str, ensure_text, itervalues, reraise, text_type from pip._vendor.six.moves import filterfalse, map from pip._internal.exceptions import InstallationError @@ -36,12 +29,7 @@ from pip._internal.models.direct_url import DIRECT_URL_METADATA_NAME, DirectUrl from pip._internal.models.scheme import SCHEME_KEYS from pip._internal.utils.filesystem import adjacent_tmp_file, replace -from pip._internal.utils.misc import ( - captured_stdout, - ensure_dir, - hash_file, - partition, -) +from pip._internal.utils.misc import captured_stdout, ensure_dir, hash_file, partition from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.unpacking import ( current_umask, @@ -49,10 +37,7 @@ set_extracted_file_to_default_mode_plus_executable, zip_item_is_executable, ) -from pip._internal.utils.wheel import ( - parse_wheel, - pkg_resources_distribution_for_wheel, -) +from pip._internal.utils.wheel import parse_wheel, pkg_resources_distribution_for_wheel # Use the custom cast function at runtime to make cast work, # and import typing.cast when performing pre-commit and type diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index acff7ffa1f7..37f84f47e97 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -12,9 +12,7 @@ from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.six import PY2 -from pip._internal.distributions import ( - make_distribution_for_install_requirement, -) +from pip._internal.distributions import make_distribution_for_install_requirement from pip._internal.distributions.installed import InstalledDistribution from pip._internal.exceptions import ( DirectoryUrlHashUnsupported, @@ -34,12 +32,7 @@ from pip._internal.utils.filesystem import copy2_fixed from pip._internal.utils.hashes import MissingHashes from pip._internal.utils.logging import indent_log -from pip._internal.utils.misc import ( - display_path, - hide_url, - path_to_display, - rmtree, -) +from pip._internal.utils.misc import display_path, hide_url, path_to_display, rmtree from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.unpacking import unpack_file diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index b070dc640da..c8c9165d339 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -13,10 +13,7 @@ from pip._vendor.six.moves.urllib import parse as urllib_parse from pip._internal.cli import cmdoptions -from pip._internal.exceptions import ( - InstallationError, - RequirementsFileParseError, -) +from pip._internal.exceptions import InstallationError, RequirementsFileParseError from pip._internal.models.search_scope import SearchScope from pip._internal.network.utils import raise_for_status from pip._internal.utils.encoding import auto_decode diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 9a6763074a3..42999a59f77 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -22,10 +22,12 @@ from pip._internal.locations import get_scheme from pip._internal.models.link import Link from pip._internal.operations.build.metadata import generate_metadata -from pip._internal.operations.build.metadata_legacy import \ - generate_metadata as generate_metadata_legacy -from pip._internal.operations.install.editable_legacy import \ - install_editable as install_editable_legacy +from pip._internal.operations.build.metadata_legacy import ( + generate_metadata as generate_metadata_legacy, +) +from pip._internal.operations.install.editable_legacy import ( + install_editable as install_editable_legacy, +) from pip._internal.operations.install.legacy import LegacyInstallFailure from pip._internal.operations.install.legacy import install as install_legacy from pip._internal.operations.install.wheel import install_wheel diff --git a/src/pip/_internal/resolution/legacy/resolver.py b/src/pip/_internal/resolution/legacy/resolver.py index 6ef00bba19d..d0fc1a7b316 100644 --- a/src/pip/_internal/resolution/legacy/resolver.py +++ b/src/pip/_internal/resolution/legacy/resolver.py @@ -34,10 +34,7 @@ from pip._internal.utils.compatibility_tags import get_supported from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import dist_in_usersite, normalize_version_info -from pip._internal.utils.packaging import ( - check_requires_python, - get_requires_python, -) +from pip._internal.utils.packaging import check_requires_python, get_requires_python from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index 99f99bfc212..a1bab05a484 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -5,16 +5,7 @@ from .base import Constraint if MYPY_CHECK_RUNNING: - from typing import ( - Any, - Dict, - Iterable, - Optional, - Sequence, - Set, - Tuple, - Union, - ) + from typing import Any, Dict, Iterable, Optional, Sequence, Set, Tuple, Union from .base import Candidate, Requirement from .factory import Factory diff --git a/src/pip/_internal/self_outdated_check.py b/src/pip/_internal/self_outdated_check.py index fbd9dfd48b7..c2d166b1844 100644 --- a/src/pip/_internal/self_outdated_check.py +++ b/src/pip/_internal/self_outdated_check.py @@ -13,16 +13,8 @@ from pip._internal.index.collector import LinkCollector from pip._internal.index.package_finder import PackageFinder from pip._internal.models.selection_prefs import SelectionPreferences -from pip._internal.utils.filesystem import ( - adjacent_tmp_file, - check_path_owner, - replace, -) -from pip._internal.utils.misc import ( - ensure_dir, - get_distribution, - get_installed_version, -) +from pip._internal.utils.filesystem import adjacent_tmp_file, check_path_owner, replace +from pip._internal.utils.misc import ensure_dir, get_distribution, get_installed_version from pip._internal.utils.packaging import get_installer from pip._internal.utils.typing import MYPY_CHECK_RUNNING diff --git a/src/pip/_internal/utils/hashes.py b/src/pip/_internal/utils/hashes.py index 4d4e26b59f3..b306dafe7d7 100644 --- a/src/pip/_internal/utils/hashes.py +++ b/src/pip/_internal/utils/hashes.py @@ -4,11 +4,7 @@ from pip._vendor.six import iteritems, iterkeys, itervalues -from pip._internal.exceptions import ( - HashMismatch, - HashMissing, - InstallationError, -) +from pip._internal.exceptions import HashMismatch, HashMissing, InstallationError from pip._internal.utils.misc import read_chunks from pip._internal.utils.typing import MYPY_CHECK_RUNNING diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 289db0e39fd..c122beb32bb 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -20,6 +20,7 @@ from pip._vendor import pkg_resources from pip._vendor.packaging.utils import canonicalize_name + # 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 @@ -30,17 +31,8 @@ from pip import __version__ from pip._internal.exceptions import CommandError -from pip._internal.locations import ( - get_major_minor_version, - site_packages, - user_site, -) -from pip._internal.utils.compat import ( - WINDOWS, - expanduser, - stdlib_pkgs, - str_to_display, -) +from pip._internal.locations import get_major_minor_version, site_packages, user_site +from pip._internal.utils.compat import WINDOWS, expanduser, stdlib_pkgs, str_to_display from pip._internal.utils.typing import MYPY_CHECK_RUNNING, cast from pip._internal.utils.virtualenv import ( running_under_virtualenv, diff --git a/src/pip/_internal/utils/subprocess.py b/src/pip/_internal/utils/subprocess.py index 1dfe02fa0be..605e711e603 100644 --- a/src/pip/_internal/utils/subprocess.py +++ b/src/pip/_internal/utils/subprocess.py @@ -14,16 +14,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import ( - Any, - Callable, - Iterable, - List, - Mapping, - Optional, - Text, - Union, - ) + from typing import Any, Callable, Iterable, List, Mapping, Optional, Text, Union CommandArgs = List[Union[str, HiddenText]] diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index e4f61a99c12..219f7967319 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -12,11 +12,7 @@ from pip._vendor import pkg_resources from pip._vendor.six.moves.urllib import parse as urllib_parse -from pip._internal.exceptions import ( - BadCommand, - InstallationError, - SubProcessError, -) +from pip._internal.exceptions import BadCommand, InstallationError, SubProcessError from pip._internal.utils.compat import console_to_str, samefile from pip._internal.utils.logging import subprocess_logger from pip._internal.utils.misc import ( diff --git a/tests/functional/test_configuration.py b/tests/functional/test_configuration.py index 63b243f788e..f820bdc19a5 100644 --- a/tests/functional/test_configuration.py +++ b/tests/functional/test_configuration.py @@ -7,10 +7,7 @@ import pytest from pip._internal.cli.status_codes import ERROR -from pip._internal.configuration import ( - CONFIG_BASENAME, - get_configuration_files, -) +from pip._internal.configuration import CONFIG_BASENAME, get_configuration_files from tests.lib.configuration_helpers import ConfigurationMixin, kinds diff --git a/tests/functional/test_new_resolver_hashes.py b/tests/functional/test_new_resolver_hashes.py index 4b13ebc307d..ad5e2f3d051 100644 --- a/tests/functional/test_new_resolver_hashes.py +++ b/tests/functional/test_new_resolver_hashes.py @@ -4,10 +4,7 @@ import pytest from pip._internal.utils.urls import path_to_url -from tests.lib import ( - create_basic_sdist_for_package, - create_basic_wheel_for_package, -) +from tests.lib import create_basic_sdist_for_package, create_basic_wheel_for_package _FindLinks = collections.namedtuple( "_FindLinks", "index_html sdist_hash wheel_hash", diff --git a/tests/functional/test_search.py b/tests/functional/test_search.py index 5918b4f64f9..1892e26b5b5 100644 --- a/tests/functional/test_search.py +++ b/tests/functional/test_search.py @@ -5,11 +5,7 @@ from pip._internal.cli.status_codes import NO_MATCHES_FOUND, SUCCESS from pip._internal.commands import create_command -from pip._internal.commands.search import ( - highest_version, - print_results, - transform_hits, -) +from pip._internal.commands.search import highest_version, print_results, transform_hits from tests.lib import pyversion if pyversion >= '3': diff --git a/tests/functional/test_uninstall.py b/tests/functional/test_uninstall.py index 5def6587174..6c687ada5b3 100644 --- a/tests/functional/test_uninstall.py +++ b/tests/functional/test_uninstall.py @@ -13,11 +13,7 @@ from pip._internal.req.constructors import install_req_from_line from pip._internal.utils.misc import rmtree -from tests.lib import ( - assert_all_changes, - create_test_package_with_setup, - need_svn, -) +from tests.lib import assert_all_changes, create_test_package_with_setup, need_svn from tests.lib.local_repos import local_checkout, local_repo diff --git a/tests/unit/resolution_resolvelib/conftest.py b/tests/unit/resolution_resolvelib/conftest.py index 87f5d129cbd..9c1c9e5c4b3 100644 --- a/tests/unit/resolution_resolvelib/conftest.py +++ b/tests/unit/resolution_resolvelib/conftest.py @@ -4,6 +4,7 @@ from pip._internal.commands.install import InstallCommand from pip._internal.index.collector import LinkCollector from pip._internal.index.package_finder import PackageFinder + # from pip._internal.models.index import PyPI from pip._internal.models.search_scope import SearchScope from pip._internal.models.selection_prefs import SelectionPreferences diff --git a/tests/unit/test_finder.py b/tests/unit/test_finder.py index 853af723b5a..55fdab3b888 100644 --- a/tests/unit/test_finder.py +++ b/tests/unit/test_finder.py @@ -8,10 +8,7 @@ from pkg_resources import parse_version import pip._internal.utils.compatibility_tags -from pip._internal.exceptions import ( - BestVersionAlreadyInstalled, - DistributionNotFound, -) +from pip._internal.exceptions import BestVersionAlreadyInstalled, DistributionNotFound from pip._internal.index.package_finder import ( CandidateEvaluator, InstallationCandidate, diff --git a/tests/unit/test_operations_prepare.py b/tests/unit/test_operations_prepare.py index ab6aaf6aa93..af3ce72a1e0 100644 --- a/tests/unit/test_operations_prepare.py +++ b/tests/unit/test_operations_prepare.py @@ -13,11 +13,7 @@ from pip._internal.operations.prepare import _copy_source_tree, unpack_url from pip._internal.utils.hashes import Hashes from pip._internal.utils.urls import path_to_url -from tests.lib.filesystem import ( - get_filelist, - make_socket_file, - make_unreadable_file, -) +from tests.lib.filesystem import get_filelist, make_socket_file, make_unreadable_file from tests.lib.path import Path from tests.lib.requests_mocks import MockResponse diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py index 69d93a0cdc2..f995d05a674 100644 --- a/tests/unit/test_req_file.py +++ b/tests/unit/test_req_file.py @@ -10,10 +10,7 @@ from pretend import stub import pip._internal.req.req_file # this will be monkeypatched -from pip._internal.exceptions import ( - InstallationError, - RequirementsFileParseError, -) +from pip._internal.exceptions import InstallationError, RequirementsFileParseError from pip._internal.models.format_control import FormatControl from pip._internal.network.session import PipSession from pip._internal.req.constructors import ( diff --git a/tests/unit/test_resolution_legacy_resolver.py b/tests/unit/test_resolution_legacy_resolver.py index 561313c002b..0388b42be5f 100644 --- a/tests/unit/test_resolution_legacy_resolver.py +++ b/tests/unit/test_resolution_legacy_resolver.py @@ -4,10 +4,7 @@ import pytest from pip._vendor import pkg_resources -from pip._internal.exceptions import ( - NoneMetadataError, - UnsupportedPythonVersion, -) +from pip._internal.exceptions import NoneMetadataError, UnsupportedPythonVersion from pip._internal.req.constructors import install_req_from_line from pip._internal.resolution.legacy.resolver import ( Resolver, diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 0a1c47cd7ae..14b4d74820f 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -16,11 +16,7 @@ import pytest from mock import Mock, patch -from pip._internal.exceptions import ( - HashMismatch, - HashMissing, - InstallationError, -) +from pip._internal.exceptions import HashMismatch, HashMissing, InstallationError from pip._internal.utils.deprecation import PipDeprecationWarning, deprecated from pip._internal.utils.encoding import BOMS, auto_decode from pip._internal.utils.glibc import ( diff --git a/tests/unit/test_utils_unpacking.py b/tests/unit/test_utils_unpacking.py index d01ffb9cd0b..5c2be24d429 100644 --- a/tests/unit/test_utils_unpacking.py +++ b/tests/unit/test_utils_unpacking.py @@ -10,11 +10,7 @@ import pytest from pip._internal.exceptions import InstallationError -from pip._internal.utils.unpacking import ( - is_within_directory, - untar_file, - unzip_file, -) +from pip._internal.utils.unpacking import is_within_directory, untar_file, unzip_file class TestUnpackArchives(object): diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 6c9fe02b32e..35916058a76 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -19,9 +19,7 @@ DirectUrl, ) from pip._internal.models.scheme import Scheme -from pip._internal.operations.build.wheel_legacy import ( - get_legacy_build_wheel_path, -) +from pip._internal.operations.build.wheel_legacy import get_legacy_build_wheel_path from pip._internal.operations.install import wheel from pip._internal.utils.compat import WINDOWS from pip._internal.utils.misc import hash_file From f8fe3531b9177e89e680484bbf741c751e2e8172 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <hugovk@users.noreply.github.com> Date: Wed, 23 Sep 2020 22:00:32 +0300 Subject: [PATCH 2539/3170] Remove unnecessary config for isort 5 --- setup.cfg | 4 ---- 1 file changed, 4 deletions(-) diff --git a/setup.cfg b/setup.cfg index 45fd58a3e7a..58342df492b 100644 --- a/setup.cfg +++ b/setup.cfg @@ -9,10 +9,6 @@ skip = multi_line_output = 3 known_third_party = pip._vendor -known_first_party = - pip - tests -default_section = THIRDPARTY include_trailing_comma = true [flake8] From 342939ffb945d0009a25cf5d4d63cf83e1d23a4b Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Thu, 24 Sep 2020 02:11:32 +0530 Subject: [PATCH 2540/3170] Remove news entries for released changes --- news/8090.bugfix | 3 --- news/8695.bugfix | 3 --- news/8701.bugfix | 2 -- news/8716.bugfix | 2 -- news/8717.bugfix | 1 - news/8724.bugfix | 2 -- 6 files changed, 13 deletions(-) delete mode 100644 news/8090.bugfix delete mode 100644 news/8695.bugfix delete mode 100644 news/8701.bugfix delete mode 100644 news/8716.bugfix delete mode 100644 news/8717.bugfix delete mode 100644 news/8724.bugfix diff --git a/news/8090.bugfix b/news/8090.bugfix deleted file mode 100644 index e9f2b7cbb9e..00000000000 --- a/news/8090.bugfix +++ /dev/null @@ -1,3 +0,0 @@ -Only attempt to use the keyring once and if it fails, don't try again. -This prevents spamming users with several keyring unlock prompts when they -cannot unlock or don't want to do so. diff --git a/news/8695.bugfix b/news/8695.bugfix deleted file mode 100644 index 668e4672e11..00000000000 --- a/news/8695.bugfix +++ /dev/null @@ -1,3 +0,0 @@ -Fix regression that distributions in system site-packages are not correctly -found when a virtual environment is configured with ``system-site-packages`` -on. diff --git a/news/8701.bugfix b/news/8701.bugfix deleted file mode 100644 index 086a8f2ebf7..00000000000 --- a/news/8701.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -Disable caching for range requests, which causes corrupted wheels -when pip tries to obtain metadata using the feature ``fast-deps``. diff --git a/news/8716.bugfix b/news/8716.bugfix deleted file mode 100644 index 086a8f2ebf7..00000000000 --- a/news/8716.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -Disable caching for range requests, which causes corrupted wheels -when pip tries to obtain metadata using the feature ``fast-deps``. diff --git a/news/8717.bugfix b/news/8717.bugfix deleted file mode 100644 index e8c8533c492..00000000000 --- a/news/8717.bugfix +++ /dev/null @@ -1 +0,0 @@ -Always use UTF-8 to read ``pyvenv.cfg`` to match the built-in ``venv``. diff --git a/news/8724.bugfix b/news/8724.bugfix deleted file mode 100644 index 8641098dde6..00000000000 --- a/news/8724.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -2020 Resolver: Correctly handle marker evaluation in constraints and exclude -them if their markers do not match the current environment. From d45ba65c37108499d17a7ffd2cbcb07fdade2ae4 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <hugovk@users.noreply.github.com> Date: Thu, 24 Sep 2020 01:33:56 +0300 Subject: [PATCH 2541/3170] Include http directory in 'pip cache info' and 'pip cache purge' --- news/8892.feature | 1 + src/pip/_internal/commands/cache.py | 41 +++++++++---- tests/functional/test_cache.py | 90 +++++++++++++++++++++++++---- 3 files changed, 112 insertions(+), 20 deletions(-) create mode 100644 news/8892.feature diff --git a/news/8892.feature b/news/8892.feature new file mode 100644 index 00000000000..96c99bf8cbc --- /dev/null +++ b/news/8892.feature @@ -0,0 +1 @@ +Include http subdirectory in ``pip cache info`` and ``pip cache purge`` commands. diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index b9d3ed410bc..efba24f1ab4 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -102,19 +102,30 @@ def get_cache_info(self, options, args): if args: raise CommandError('Too many arguments') + num_http_files = len(self._find_http_files(options)) num_packages = len(self._find_wheels(options, '*')) - cache_location = self._wheels_cache_dir(options) - cache_size = filesystem.format_directory_size(cache_location) + http_cache_location = self._cache_dir(options, 'http') + wheels_cache_location = self._cache_dir(options, 'wheels') + http_cache_size = filesystem.format_directory_size(http_cache_location) + wheels_cache_size = filesystem.format_directory_size( + wheels_cache_location + ) message = textwrap.dedent(""" - Location: {location} - Size: {size} + HTTP files location: {http_cache_location} + HTTP files size: {http_cache_size} + Number of HTTP files: {num_http_files} + Wheels location: {wheels_cache_location} + Wheels size: {wheels_cache_size} Number of wheels: {package_count} """).format( - location=cache_location, + http_cache_location=http_cache_location, + http_cache_size=http_cache_size, + num_http_files=num_http_files, + wheels_cache_location=wheels_cache_location, package_count=num_packages, - size=cache_size, + wheels_cache_size=wheels_cache_size, ).strip() logger.info(message) @@ -169,6 +180,11 @@ def remove_cache_items(self, options, args): raise CommandError('Please provide a pattern') files = self._find_wheels(options, args[0]) + + # Only fetch http files if no specific pattern given + if args[0] == '*': + files += self._find_http_files(options) + if not files: raise CommandError('No matching packages') @@ -184,13 +200,18 @@ def purge_cache(self, options, args): return self.remove_cache_items(options, ['*']) - def _wheels_cache_dir(self, options): - # type: (Values) -> str - return os.path.join(options.cache_dir, 'wheels') + def _cache_dir(self, options, subdir): + # type: (Values, str) -> str + return os.path.join(options.cache_dir, subdir) + + def _find_http_files(self, options): + # type: (Values) -> List[str] + http_dir = self._cache_dir(options, 'http') + return filesystem.find_files(http_dir, '*') def _find_wheels(self, options, pattern): # type: (Values, str) -> List[str] - wheel_dir = self._wheels_cache_dir(options) + wheel_dir = self._cache_dir(options, 'wheels') # The wheel filename format, as specified in PEP 427, is: # {distribution}-{version}(-{build})?-{python}-{abi}-{platform}.whl diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index 603e11b5b0e..a1ffd90902d 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -15,11 +15,30 @@ def cache_dir(script): return result.stdout.strip() +@pytest.fixture +def http_cache_dir(cache_dir): + return os.path.normcase(os.path.join(cache_dir, 'http')) + + @pytest.fixture def wheel_cache_dir(cache_dir): return os.path.normcase(os.path.join(cache_dir, 'wheels')) +@pytest.fixture +def http_cache_files(http_cache_dir): + destination = os.path.join(http_cache_dir, 'arbitrary', 'pathname') + + if not os.path.exists(destination): + return [] + + filenames = glob(os.path.join(destination, '*')) + files = [] + for filename in filenames: + files.append(os.path.join(destination, filename)) + return files + + @pytest.fixture def wheel_cache_files(wheel_cache_dir): destination = os.path.join(wheel_cache_dir, 'arbitrary', 'pathname') @@ -34,6 +53,24 @@ def wheel_cache_files(wheel_cache_dir): return files +@pytest.fixture +def populate_http_cache(http_cache_dir): + destination = os.path.join(http_cache_dir, 'arbitrary', 'pathname') + os.makedirs(destination) + + files = [ + ('aaaaaaaaa', os.path.join(destination, 'aaaaaaaaa')), + ('bbbbbbbbb', os.path.join(destination, 'bbbbbbbbb')), + ('ccccccccc', os.path.join(destination, 'ccccccccc')), + ] + + for _name, filename in files: + with open(filename, 'w'): + pass + + return files + + @pytest.fixture def populate_wheel_cache(wheel_cache_dir): destination = os.path.join(wheel_cache_dir, 'arbitrary', 'pathname') @@ -83,6 +120,29 @@ def list_matches_wheel_abspath(wheel_name, result): and os.path.exists(l), lines)) +@pytest.fixture +def remove_matches_http(http_cache_dir): + """Returns True if any line in `result`, which should be the output of + a `pip cache purge` call, matches `http_filename`. + + E.g., If http_filename is `aaaaaaaaa`, it searches for a line equal to + `Removed <http files cache dir>/arbitrary/pathname/aaaaaaaaa`. + """ + + def _remove_matches_http(http_filename, result): + lines = result.stdout.splitlines() + + # The "/arbitrary/pathname/" bit is an implementation detail of how + # the `populate_http_cache` fixture is implemented. + path = os.path.join( + http_cache_dir, 'arbitrary', 'pathname', http_filename, + ) + expected = 'Removed {}'.format(path) + return expected in lines + + return _remove_matches_http + + @pytest.fixture def remove_matches_wheel(wheel_cache_dir): """Returns True if any line in `result`, which should be the output of @@ -124,11 +184,14 @@ def test_cache_dir_too_many_args(script, cache_dir): assert 'ERROR: Too many arguments' in result.stderr.splitlines() -@pytest.mark.usefixtures("populate_wheel_cache") -def test_cache_info(script, wheel_cache_dir, wheel_cache_files): +@pytest.mark.usefixtures("populate_http_cache", "populate_wheel_cache") +def test_cache_info( + script, http_cache_dir, wheel_cache_dir, wheel_cache_files +): result = script.pip('cache', 'info') - assert 'Location: {}'.format(wheel_cache_dir) in result.stdout + assert 'HTTP files location: {}'.format(http_cache_dir) in result.stdout + assert 'Wheels location: {}'.format(wheel_cache_dir) in result.stdout num_wheels = len(wheel_cache_files) assert 'Number of wheels: {}'.format(num_wheels) in result.stdout @@ -265,21 +328,28 @@ def test_cache_remove_name_and_version_match(script, remove_matches_wheel): assert not remove_matches_wheel('zzz-7.8.9', result) -@pytest.mark.usefixtures("populate_wheel_cache") -def test_cache_purge(script, remove_matches_wheel): - """Running `pip cache purge` should remove all cached wheels.""" +@pytest.mark.usefixtures("populate_http_cache", "populate_wheel_cache") +def test_cache_purge(script, remove_matches_http, remove_matches_wheel): + """Running `pip cache purge` should remove all cached http files and + wheels.""" result = script.pip('cache', 'purge', '--verbose') + assert remove_matches_http('aaaaaaaaa', result) + assert remove_matches_http('bbbbbbbbb', result) + assert remove_matches_http('ccccccccc', result) + assert remove_matches_wheel('yyy-1.2.3', result) assert remove_matches_wheel('zzz-4.5.6', result) assert remove_matches_wheel('zzz-4.5.7', result) assert remove_matches_wheel('zzz-7.8.9', result) -@pytest.mark.usefixtures("populate_wheel_cache") -def test_cache_purge_too_many_args(script, wheel_cache_files): +@pytest.mark.usefixtures("populate_http_cache", "populate_wheel_cache") +def test_cache_purge_too_many_args( + script, http_cache_files, wheel_cache_files +): """Running `pip cache purge aaa` should raise an error and remove no - cached wheels.""" + cached http files or wheels.""" result = script.pip('cache', 'purge', 'aaa', '--verbose', expect_error=True) assert result.stdout == '' @@ -289,7 +359,7 @@ def test_cache_purge_too_many_args(script, wheel_cache_files): assert 'ERROR: Too many arguments' in result.stderr.splitlines() # Make sure nothing was deleted. - for filename in wheel_cache_files: + for filename in http_cache_files + wheel_cache_files: assert os.path.exists(filename) From e5deff909c8f4eed0f6e60e769570e4e2c08c6b7 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Thu, 24 Sep 2020 00:36:02 +0530 Subject: [PATCH 2542/3170] Add black to pre-commit, ignoring all files Each of the lines in this "exclude" pattern will get a dedicated PR to remove it and would blacken that part of the codebase. --- .pre-commit-config.yaml | 36 ++++++++++++++++++++++++++++++++++++ 1 file changed, 36 insertions(+) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index f1afdd473cf..8a62a90f64c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -16,6 +16,42 @@ repos: - id: trailing-whitespace exclude: .patch +- repo: https://github.com/psf/black + rev: 20.8b1 + hooks: + - id: black + exclude: | + (?x) + ^docs/| + ^src/pip/_internal/cli| + ^src/pip/_internal/commands| + ^src/pip/_internal/distributions| + ^src/pip/_internal/index| + ^src/pip/_internal/models| + ^src/pip/_internal/network| + ^src/pip/_internal/operations| + ^src/pip/_internal/req| + ^src/pip/_internal/resolution| + ^src/pip/_internal/utils| + ^src/pip/_internal/vcs| + ^src/pip/_internal/\w+\.py$| + ^src/pip/__main__.py$| + ^tools/| + # Tests + ^tests/conftest.py| + ^tests/yaml| + ^tests/lib| + ^tests/data| + ^tests/unit| + ^tests/functional/(?!test_install)| + ^tests/functional/test_install| + # Files in the root of the repository + ^setup.py| + ^noxfile.py| + # A blank ignore, to avoid merge conflicts later. + ^$ + + - repo: https://gitlab.com/pycqa/flake8 rev: 3.8.3 hooks: From 8fc985eccba7f9cc0c6f53ff8ea08d6897895776 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <hugovk@users.noreply.github.com> Date: Thu, 24 Sep 2020 14:35:53 +0300 Subject: [PATCH 2543/3170] Flake8: use extend-ignore to avoid needing to redefine defaults --- setup.cfg | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index 450f953e4a5..59716a080f1 100644 --- a/setup.cfg +++ b/setup.cfg @@ -24,10 +24,8 @@ exclude = _vendor, data enable-extensions = G -ignore = +extend-ignore = G200, G202, - # pycodestyle checks ignored in the default configuration - E121, E123, E126, E133, E226, E241, E242, E704, W503, W504, W505, # black adds spaces around ':' E203, per-file-ignores = From 67bff6199ff7c8b080ce839d9d0e748b8d39b161 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko <wk@sydorenko.org.ua> Date: Sat, 5 Sep 2020 23:55:06 +0200 Subject: [PATCH 2544/3170] Add a change fragment about issue #8783 / PR #8848 --- news/8783.doc | 1 + news/8848.doc | 1 + 2 files changed, 2 insertions(+) create mode 100644 news/8783.doc create mode 120000 news/8848.doc diff --git a/news/8783.doc b/news/8783.doc new file mode 100644 index 00000000000..6d2bb8762d4 --- /dev/null +++ b/news/8783.doc @@ -0,0 +1 @@ +Added initial UX feedback widgets to docs. diff --git a/news/8848.doc b/news/8848.doc new file mode 120000 index 00000000000..a318abd149b --- /dev/null +++ b/news/8848.doc @@ -0,0 +1 @@ +8783.doc \ No newline at end of file From 3301284810f905bdb25c67233952892f7852df2d Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko <wk@sydorenko.org.ua> Date: Sat, 5 Sep 2020 21:01:37 +0200 Subject: [PATCH 2545/3170] =?UTF-8?q?=F0=9F=93=9D=20Add=20initial=20sphinx?= =?UTF-8?q?=20ext=20for=20per-doc=20feedbacks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Resolves #8783 --- docs/docs_feedback_sphinxext.py | 165 ++++++++++++++++++++++++++++++++ docs/html/conf.py | 22 ++++- 2 files changed, 186 insertions(+), 1 deletion(-) create mode 100644 docs/docs_feedback_sphinxext.py diff --git a/docs/docs_feedback_sphinxext.py b/docs/docs_feedback_sphinxext.py new file mode 100644 index 00000000000..90f2ddd7498 --- /dev/null +++ b/docs/docs_feedback_sphinxext.py @@ -0,0 +1,165 @@ +"""A sphinx extension for collecting per doc feedback.""" + +from __future__ import annotations + +from itertools import chain +from typing import TYPE_CHECKING + +if TYPE_CHECKING: + from typing import Dict, List, Union + + from sphinx.application import Sphinx + + +DEFAULT_DOC_LINES_THRESHOLD = 250 +RST_INDENT = 4 +EMAIL_INDENT = 6 + + +def _modify_rst_document_source_on_read( + app: Sphinx, + docname: str, + source: List[str], +) -> None: + """Add info block to top and bottom of each document source. + + This function modifies RST source in-place by adding an admonition + block at the top and the bottom of each document right after it's + been read from disk preserving :orphan: at top, if present. + """ + admonition_type = app.config.docs_feedback_admonition_type + big_doc_lines = app.config.docs_feedback_big_doc_lines + escaped_email = app.config.docs_feedback_email.replace(' ', r'\ ') + excluded_documents = set(app.config.docs_feedback_excluded_documents) + questions_list = app.config.docs_feedback_questions_list + + valid_admonitions = { + 'attention', 'caution', 'danger', 'error', 'hint', + 'important', 'note', 'tip', 'warning', 'admonition', + } + + if admonition_type not in valid_admonitions: + raise ValueError( + 'Expected `docs_feedback_admonition_type` to be one of ' + f'{valid_admonitions} but got {admonition_type}.' + ) + + if not questions_list: + raise ValueError( + 'Expected `docs_feedback_questions_list` to list questions ' + 'but got none.' + ) + + if docname in excluded_documents: + # NOTE: Completely ignore any document + # NOTE: listed in 'docs_feedback_excluded_documents'. + return + + is_doc_big = source[0].count('\n') >= big_doc_lines + + questions_list_rst = '\n'.join( + f'{" " * RST_INDENT}{number!s}. {question}' + for number, question in enumerate(questions_list, 1) + ) + questions_list_urlencoded = ( + '\n'.join( + f'\n{" " * RST_INDENT}{number!s}. {question} ' + for number, question in enumerate( + chain( + (f'Document: {docname}. Page URL: https://', ), + questions_list, + ), + ) + ). + rstrip('\r\n\t '). + replace('\r', '%0D'). + replace('\n', '%0A'). + replace(' ', '%20') + ) + + admonition_msg = rf""" + **Did this article help?** + + We are currently doing research to improve pip's documentation + and would love your feedback. + Please `email us`_ and let us know{{let_us_know_ending}} + +{{questions_list_rst}} + + .. _email us: + mailto:{escaped_email}\ + ?subject=[Doc:\ {docname}]\ Pip\ docs\ feedback\ \ + (URL\:\ https\://)\ + &body={questions_list_urlencoded} + """ + let_us_know_ending = ':' + + info_block_bottom = ( + f'.. {admonition_type}::\n\t\t{admonition_msg.format_map(locals())}\n' + ) + + questions_list_rst = '' + let_us_know_ending = ( + ' why you came to this page and what on it helped ' + 'you and what did not. ' + '(:issue:`Read more about this research <8517>`)' + ) + info_block_top = '' if is_doc_big else ( + f'.. {admonition_type}::\n\t\t{admonition_msg.format_map(locals())}\n' + ) + + orphan_mark = ':orphan:' + is_orphan = orphan_mark in source[0] + if is_orphan: + source[0].replace(orphan_mark, '') + else: + orphan_mark = '' + + source[0] = '\n\n'.join(( + orphan_mark, info_block_top, source[0], info_block_bottom, + )) + + +def setup(app: Sphinx) -> Dict[str, Union[bool, str]]: + """Initialize the Sphinx extension. + + This function adds a callback for modifying the document sources + in-place on read. + + It also declares the extension settings changable via :file:`conf.py`. + """ + rebuild_trigger = 'html' # rebuild full html on settings change + app.add_config_value( + 'docs_feedback_admonition_type', + default='important', + rebuild=rebuild_trigger, + ) + app.add_config_value( + 'docs_feedback_big_doc_lines', + default=DEFAULT_DOC_LINES_THRESHOLD, + rebuild=rebuild_trigger, + ) + app.add_config_value( + 'docs_feedback_email', + default='Docs UX Team <docs-feedback+ux/pip.pypa.io@pypa.io>', + rebuild=rebuild_trigger, + ) + app.add_config_value( + 'docs_feedback_excluded_documents', + default=set(), + rebuild=rebuild_trigger, + ) + app.add_config_value( + 'docs_feedback_questions_list', + default=(), + rebuild=rebuild_trigger, + ) + + app.add_css_file('important-admonition.css') + app.connect('source-read', _modify_rst_document_source_on_read) + + return { + 'parallel_read_safe': True, + 'parallel_write_safe': True, + 'version': 'builtin', + } diff --git a/docs/html/conf.py b/docs/html/conf.py index a88ac33e2e4..b859e990202 100644 --- a/docs/html/conf.py +++ b/docs/html/conf.py @@ -31,10 +31,14 @@ # extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. # extensions = ['sphinx.ext.autodoc'] extensions = [ + # native: 'sphinx.ext.extlinks', - 'pip_sphinxext', 'sphinx.ext.intersphinx', + # third-party: 'sphinx_tabs.tabs', + # in-tree: + 'docs_feedback_sphinxext', + 'pip_sphinxext', ] # intersphinx @@ -308,3 +312,19 @@ def to_document_name(path, base_dir): ) man_pages.append((fname_base, outname, description, u'pip developers', 1)) + +# -- Options for docs_feedback_sphinxext -------------------------------------- + +# NOTE: Must be one of 'attention', 'caution', 'danger', 'error', 'hint', +# NOTE: 'important', 'note', 'tip', 'warning' or 'admonition'. +docs_feedback_admonition_type = 'important' +docs_feedback_big_doc_lines = 50 # bigger docs will have a banner on top +docs_feedback_email = 'Docs UX Team <docs-feedback+ux/pip.pypa.io@pypa.io>' +docs_feedback_excluded_documents = { # these won't have any banners + 'news', +} +docs_feedback_questions_list = ( + 'What problem were you trying to solve when you came to this page?', + 'What content was useful?', + 'What content was not useful?', +) From 13962cd6adcf233c8e9b5a2875e706a19019cd24 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko <wk@sydorenko.org.ua> Date: Sat, 5 Sep 2020 23:51:11 +0200 Subject: [PATCH 2546/3170] =?UTF-8?q?=F0=9F=8E=A8=20Colorize=20the=20"impo?= =?UTF-8?q?rtant"=20admonition=20blocks?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- MANIFEST.in | 2 +- docs/html/_static/important-admonition.css | 8 ++++++++ docs/html/conf.py | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) create mode 100644 docs/html/_static/important-admonition.css diff --git a/MANIFEST.in b/MANIFEST.in index aa6a1d0e71f..24d4553785b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -22,7 +22,7 @@ exclude noxfile.py recursive-include src/pip/_vendor *.pem recursive-include src/pip/_vendor py.typed -recursive-include docs Makefile *.rst *.py *.bat +recursive-include docs *.css *.rst *.py exclude src/pip/_vendor/six exclude src/pip/_vendor/six/moves diff --git a/docs/html/_static/important-admonition.css b/docs/html/_static/important-admonition.css new file mode 100644 index 00000000000..a73ae2e4d4c --- /dev/null +++ b/docs/html/_static/important-admonition.css @@ -0,0 +1,8 @@ +.admonition.important { + background-color: rgb(219, 250, 244); + border: 1px solid rgb(26, 188, 156); +} + +.admonition.important>.admonition-title { + color: rgb(26, 188, 156); +} diff --git a/docs/html/conf.py b/docs/html/conf.py index b859e990202..444d15a819a 100644 --- a/docs/html/conf.py +++ b/docs/html/conf.py @@ -181,7 +181,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = [] +html_static_path = ['_static'] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. From 1135ac041d253122cf9791a0978332737f449195 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 24 Sep 2020 15:51:44 +0800 Subject: [PATCH 2547/3170] Move lru_cache to utils for reuse --- src/pip/_internal/index/collector.py | 26 ++------------------------ src/pip/_internal/utils/compat.py | 24 +++++++++++++++++++++++- 2 files changed, 25 insertions(+), 25 deletions(-) diff --git a/src/pip/_internal/index/collector.py b/src/pip/_internal/index/collector.py index e6230c76734..7b9abbf69e2 100644 --- a/src/pip/_internal/index/collector.py +++ b/src/pip/_internal/index/collector.py @@ -21,6 +21,7 @@ from pip._internal.models.link import Link from pip._internal.models.search_scope import SearchScope from pip._internal.network.utils import raise_for_status +from pip._internal.utils.compat import lru_cache from pip._internal.utils.filetypes import ARCHIVE_EXTENSIONS from pip._internal.utils.misc import pairwise, redact_auth_from_url from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -36,10 +37,8 @@ List, MutableMapping, Optional, - Protocol, Sequence, Tuple, - TypeVar, Union, ) @@ -50,31 +49,10 @@ HTMLElement = xml.etree.ElementTree.Element ResponseHeaders = MutableMapping[str, str] - # Used in the @lru_cache polyfill. - F = TypeVar('F') - - class LruCache(Protocol): - def __call__(self, maxsize=None): - # type: (Optional[int]) -> Callable[[F], F] - raise NotImplementedError - logger = logging.getLogger(__name__) -# Fallback to noop_lru_cache in Python 2 -# TODO: this can be removed when python 2 support is dropped! -def noop_lru_cache(maxsize=None): - # type: (Optional[int]) -> Callable[[F], F] - def _wrapper(f): - # type: (F) -> F - return f - return _wrapper - - -_lru_cache = getattr(functools, "lru_cache", noop_lru_cache) # type: LruCache - - def _match_vcs_scheme(url): # type: (str) -> Optional[str] """Look for VCS schemes in the URL. @@ -344,7 +322,7 @@ def with_cached_html_pages( `page` has `page.cache_link_parsing == False`. """ - @_lru_cache(maxsize=None) + @lru_cache(maxsize=None) def wrapper(cacheable_page): # type: (CacheablePageContent) -> List[Link] return list(fn(cacheable_page.page)) diff --git a/src/pip/_internal/utils/compat.py b/src/pip/_internal/utils/compat.py index cc63536783c..2196e6e0aea 100644 --- a/src/pip/_internal/utils/compat.py +++ b/src/pip/_internal/utils/compat.py @@ -7,6 +7,7 @@ from __future__ import absolute_import, division import codecs +import functools import locale import logging import os @@ -18,7 +19,15 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Optional, Text, Tuple, Union + from typing import Callable, Optional, Protocol, Text, Tuple, TypeVar, Union + + # Used in the @lru_cache polyfill. + F = TypeVar('F') + + class LruCache(Protocol): + def __call__(self, maxsize=None): + # type: (Optional[int]) -> Callable[[F], F] + raise NotImplementedError try: import ipaddress @@ -269,3 +278,16 @@ def ioctl_GWINSZ(fd): if not cr: cr = (os.environ.get('LINES', 25), os.environ.get('COLUMNS', 80)) return int(cr[1]), int(cr[0]) + + +# Fallback to noop_lru_cache in Python 2 +# TODO: this can be removed when python 2 support is dropped! +def noop_lru_cache(maxsize=None): + # type: (Optional[int]) -> Callable[[F], F] + def _wrapper(f): + # type: (F) -> F + return f + return _wrapper + + +lru_cache = getattr(functools, "lru_cache", noop_lru_cache) # type: LruCache From 1dd6d562789b06dc1508212505e32f36bd71d310 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 24 Sep 2020 15:52:52 +0800 Subject: [PATCH 2548/3170] Cache PackageFinder.find_all_candidates() --- src/pip/_internal/index/package_finder.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pip/_internal/index/package_finder.py b/src/pip/_internal/index/package_finder.py index 5162a8191d3..b361e194d75 100644 --- a/src/pip/_internal/index/package_finder.py +++ b/src/pip/_internal/index/package_finder.py @@ -25,6 +25,7 @@ from pip._internal.models.selection_prefs import SelectionPreferences from pip._internal.models.target_python import TargetPython from pip._internal.models.wheel import Wheel +from pip._internal.utils.compat import lru_cache from pip._internal.utils.filetypes import WHEEL_EXTENSION from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import build_netloc @@ -799,6 +800,7 @@ def process_project_url(self, project_url, link_evaluator): return package_links + @lru_cache(maxsize=None) def find_all_candidates(self, project_name): # type: (str) -> List[InstallationCandidate] """Find all available InstallationCandidate for project_name From 22eeff47707605b2537c0ee7a7ad23662649fb0a Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sat, 26 Sep 2020 15:08:41 +0800 Subject: [PATCH 2549/3170] News --- news/8905.feature.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 news/8905.feature.rst diff --git a/news/8905.feature.rst b/news/8905.feature.rst new file mode 100644 index 00000000000..5d27d40c2be --- /dev/null +++ b/news/8905.feature.rst @@ -0,0 +1,3 @@ +Cache package listings on index packages so they are guarenteed to stay stable +during a pip command session. This also improves performance when a index page +is accessed multiple times during the command session. From dedecf0735c34e03906e225a95231bf10d6dde79 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sat, 26 Sep 2020 19:55:00 +0800 Subject: [PATCH 2550/3170] Remove the .rst suffix from news fragment --- news/{8905.feature.rst => 8905.feature} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename news/{8905.feature.rst => 8905.feature} (100%) diff --git a/news/8905.feature.rst b/news/8905.feature similarity index 100% rename from news/8905.feature.rst rename to news/8905.feature From b215120b5ab1315c963ee409b720753ac690c7f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Filipe=20La=C3=ADns?= <lains@archlinux.org> Date: Sat, 26 Sep 2020 14:41:42 +0100 Subject: [PATCH 2551/3170] Revert "Merge pull request #8391 from VikramJayanthi17/error-swallow-fix" MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This reverts commit 7a60395dbd646401192252c4c2834607ff05fa96, reversing changes made to d3ce025e8dac297fe550e2acfa730288d715b6a7. It fixes devendored pip. See #8916. Signed-off-by: Filipe Laíns <lains@archlinux.org> --- src/pip/_vendor/__init__.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/pip/_vendor/__init__.py b/src/pip/_vendor/__init__.py index 581db54c8d8..c3db83ff6aa 100644 --- a/src/pip/_vendor/__init__.py +++ b/src/pip/_vendor/__init__.py @@ -32,11 +32,15 @@ def vendored(modulename): try: __import__(modulename, globals(), locals(), level=0) except ImportError: - # This error used to be silenced in earlier variants of this file, to instead - # raise the error when pip actually tries to use the missing module. - # Based on inputs in #5354, this was changed to explicitly raise the error. - # Re-raising the exception without modifying it is an intentional choice. - raise + # We can just silently allow import failures to pass here. If we + # got to this point it means that ``import pip._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* + # tries to import one of these modules to use it, which actually + # gives us a better error message than we would have otherwise + # gotten. + pass else: sys.modules[vendored_name] = sys.modules[modulename] base, head = vendored_name.rsplit(".", 1) From d4686b57935baf28fa709195af57975a88238a9d Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <hugovk@users.noreply.github.com> Date: Sun, 27 Sep 2020 21:11:35 +0300 Subject: [PATCH 2552/3170] Document Python support policy --- docs/html/development/release-process.rst | 8 +++++++- news/8927.removal | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 news/8927.removal diff --git a/docs/html/development/release-process.rst b/docs/html/development/release-process.rst index ebba1325864..1c6889ef01c 100644 --- a/docs/html/development/release-process.rst +++ b/docs/html/development/release-process.rst @@ -65,7 +65,7 @@ their merits. ``pip._internal.utils.deprecation.deprecated``. The function is not a part of pip's public API. -Python 2 support +Python 2 Support ---------------- pip will continue to ensure that it runs on Python 2.7 after the CPython 2.7 @@ -79,6 +79,12 @@ only bugs will be considered, and merged (subject to normal review processes). Note that there may be delays due to the lack of developer resources for reviewing such pull requests. +Python Support Policy +--------------------- + +In general, a given Python version is supported until its usage on PyPI falls below 5%. +This is at the maintainers' discretion, in case extraordinary circumstances arise. + .. _`Feature Flags`: Feature Flags diff --git a/news/8927.removal b/news/8927.removal new file mode 100644 index 00000000000..0032fa5f29d --- /dev/null +++ b/news/8927.removal @@ -0,0 +1 @@ +Document that Python versions are generally supported until PyPI usage falls below 5%. From 806c112ed098426f9aaf5e4812d52c8f6ac10ecc Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Mon, 28 Sep 2020 00:05:08 +0800 Subject: [PATCH 2553/3170] Don't crash on 'check' when METADATA is missing --- news/8676.feature | 2 ++ src/pip/_internal/operations/check.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 news/8676.feature diff --git a/news/8676.feature b/news/8676.feature new file mode 100644 index 00000000000..f8da963f6ed --- /dev/null +++ b/news/8676.feature @@ -0,0 +1,2 @@ +Improve error message friendliness when an environment has packages with +corrupted metadata. diff --git a/src/pip/_internal/operations/check.py b/src/pip/_internal/operations/check.py index bc44d4357f6..5dee6bcb400 100644 --- a/src/pip/_internal/operations/check.py +++ b/src/pip/_internal/operations/check.py @@ -45,8 +45,8 @@ def create_package_set_from_installed(**kwargs): name = canonicalize_name(dist.project_name) try: package_set[name] = PackageDetails(dist.version, dist.requires()) - except RequirementParseError as e: - # Don't crash on broken metadata + except (OSError, RequirementParseError) as e: + # Don't crash on unreadable or broken metadata logger.warning("Error parsing requirements for %s: %s", name, e) problems = True return package_set, problems From fbf710ea5dc6e8aa3a84a80691df1daded5a424f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <hugovk@users.noreply.github.com> Date: Mon, 28 Sep 2020 09:52:15 +0300 Subject: [PATCH 2554/3170] Pin pytest whilst pytest-rerunfailures is incompatible with pytest 6.1 --- tools/requirements/tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/requirements/tests.txt b/tools/requirements/tests.txt index ef87225d6c4..6ae8c5d4319 100644 --- a/tools/requirements/tests.txt +++ b/tools/requirements/tests.txt @@ -5,7 +5,7 @@ enum34; python_version < '3.4' freezegun mock pretend -pytest +pytest<6.1.0 # Until https://github.com/pytest-dev/pytest-rerunfailures/issues/128 is fixed pytest-cov pytest-rerunfailures pytest-timeout From ebc13756128b8f85ce1ea76cd5f507e7d0c928d4 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sat, 26 Sep 2020 16:06:44 +0800 Subject: [PATCH 2555/3170] Make private attribute looks private --- src/pip/_internal/resolution/resolvelib/provider.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index a1bab05a484..8ad1c65d352 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -43,7 +43,7 @@ def __init__( self._constraints = constraints self._ignore_dependencies = ignore_dependencies self._upgrade_strategy = upgrade_strategy - self.user_requested = user_requested + self._user_requested = user_requested def _sort_matches(self, matches): # type: (Iterable[Candidate]) -> Sequence[Candidate] @@ -84,7 +84,7 @@ def _eligible_for_upgrade(name): if self._upgrade_strategy == "eager": return True elif self._upgrade_strategy == "only-if-needed": - return (name in self.user_requested) + return (name in self._user_requested) return False def sort_key(c): From c2de8974d422596909c2700636dff07dd3bf1225 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sun, 27 Sep 2020 23:16:25 +0800 Subject: [PATCH 2556/3170] Resolve user-requested requirements first --- src/pip/_internal/resolution/resolvelib/provider.py | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index 8ad1c65d352..8264b471c90 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -115,11 +115,18 @@ def get_preference( self, resolution, # type: Optional[Candidate] candidates, # type: Sequence[Candidate] - information # type: Sequence[Tuple[Requirement, Candidate]] + information # type: Sequence[Tuple[Requirement, Optional[Candidate]]] ): # type: (...) -> Any - # Use the "usual" value for now - return len(candidates) + """Return a sort key to determine what dependency to look next. + + A smaller value makes a dependency higher priority. We put direct + (user-requested) dependencies first since they may contain useful + user-specified version ranges. Users tend to expect us to catch + problems in them early as well. + """ + transitive = all(parent is not None for _, parent in information) + return (transitive, len(candidates)) def find_matches(self, requirements): # type: (Sequence[Requirement]) -> Iterable[Candidate] From df6d3c701a02e7bcea71ae932f2f02917894664a Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Mon, 28 Sep 2020 22:22:58 +0800 Subject: [PATCH 2557/3170] News fragment --- news/8924.feature | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 news/8924.feature diff --git a/news/8924.feature b/news/8924.feature new file mode 100644 index 00000000000..c607aa0d06b --- /dev/null +++ b/news/8924.feature @@ -0,0 +1,2 @@ +New resolver: Tweak resolution logic to improve user experience when +user-supplied requirements conflict. From 6138184e2c84842472ac24d17777618f79c0aea1 Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara <sh@changeset.nyc> Date: Mon, 28 Sep 2020 15:19:17 -0400 Subject: [PATCH 2558/3170] Update docs index page about resolver and UX work --- docs/html/index.rst | 3 ++- news/a2fa2e68-01bf-11eb-a0b1-4fe8cb1f9dcf.trivial | 0 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 news/a2fa2e68-01bf-11eb-a0b1-4fe8cb1f9dcf.trivial diff --git a/docs/html/index.rst b/docs/html/index.rst index 860abcac9ee..64fc34c9d42 100644 --- a/docs/html/index.rst +++ b/docs/html/index.rst @@ -17,7 +17,7 @@ Please take a look at our documentation for how to install and use pip: ux_research_design news -In 2020, we're working on improvements to the heart of pip. Please `learn more and take our survey`_ to help us do it right. +In 2020, we're working on improvements to the heart of pip: :ref:`Resolver changes 2020`. Please `learn more and take our survey`_ to help us do it right, and `join our user experience surveys pool`_. If you find bugs, need help, or want to talk to the developers, please use our mailing lists or chat rooms: @@ -40,6 +40,7 @@ rooms, and mailing lists is expected to follow the `PSF Code of Conduct`_. .. _package installer: https://packaging.python.org/guides/tool-recommendations/ .. _Python Package Index: https://pypi.org +.. _join our user experience surveys pool: ux_research_design .. _learn more and take our survey: https://pyfound.blogspot.com/2020/03/new-pip-resolver-to-roll-out-this-year.html .. _Installation: https://pip.pypa.io/en/stable/installing.html .. _Documentation: https://pip.pypa.io/en/stable/ diff --git a/news/a2fa2e68-01bf-11eb-a0b1-4fe8cb1f9dcf.trivial b/news/a2fa2e68-01bf-11eb-a0b1-4fe8cb1f9dcf.trivial new file mode 100644 index 00000000000..e69de29bb2d From 4fc4333abf9270b33acaf305eb1e1c5d38db177f Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <hugovk@users.noreply.github.com> Date: Tue, 29 Sep 2020 09:57:46 +0300 Subject: [PATCH 2559/3170] Revert "Pin pytest whilst pytest-rerunfailures is incompatible with pytest 6.1" This reverts commit fbf710ea5dc6e8aa3a84a80691df1daded5a424f. --- tools/requirements/tests.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tools/requirements/tests.txt b/tools/requirements/tests.txt index 6ae8c5d4319..ef87225d6c4 100644 --- a/tools/requirements/tests.txt +++ b/tools/requirements/tests.txt @@ -5,7 +5,7 @@ enum34; python_version < '3.4' freezegun mock pretend -pytest<6.1.0 # Until https://github.com/pytest-dev/pytest-rerunfailures/issues/128 is fixed +pytest pytest-cov pytest-rerunfailures pytest-timeout From 3b44d70b5e85e0b71d9f1e4a888497352d7412e5 Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara <sh@changeset.nyc> Date: Wed, 30 Sep 2020 07:10:09 -0400 Subject: [PATCH 2560/3170] docs: Add how to default to new resolver Fixes #8661. --- docs/html/user_guide.rst | 7 +++++++ news/bc7f9ea0-030d-11eb-92cb-6b2b625d02fc.trivial | 0 2 files changed, 7 insertions(+) create mode 100644 news/bc7f9ea0-030d-11eb-92cb-6b2b625d02fc.trivial diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 06f85cba8cd..a2d13c43358 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -1545,6 +1545,12 @@ How to test - If you have a build pipeline that depends on pip installing your dependencies for you, check that the new resolver does what you need. + + - If you'd like pip to default to using the new resolver, run ``pip + config set global.use-feature 2020-resolver`` (for more on that + and the alternate ``PIP_USE_FEATURE`` environment variable + option, see `issue 8661`_). + - Run your project’s CI (test suite, build process, etc.) using the new resolver, and let us know of any issues. - If you have encountered resolver issues with pip in the past, @@ -1671,6 +1677,7 @@ announcements on the `low-traffic packaging announcements list`_ and .. _freeze: https://pip.pypa.io/en/latest/reference/pip_freeze/ .. _resolver testing survey: https://tools.simplysecure.org/survey/index.php?r=survey/index&sid=989272&lang=en +.. _issue 8661: https://github.com/pypa/pip/issues/8661 .. _our announcement on the PSF blog: http://pyfound.blogspot.com/2020/03/new-pip-resolver-to-roll-out-this-year.html .. _tensorflow: https://pypi.org/project/tensorflow/ .. _low-traffic packaging announcements list: https://mail.python.org/mailman3/lists/pypi-announce.python.org/ diff --git a/news/bc7f9ea0-030d-11eb-92cb-6b2b625d02fc.trivial b/news/bc7f9ea0-030d-11eb-92cb-6b2b625d02fc.trivial new file mode 100644 index 00000000000..e69de29bb2d From 2ef8040495711c7e7e695f80a35e208f7404f879 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Thu, 1 Oct 2020 20:56:01 +0700 Subject: [PATCH 2561/3170] Comment and rework conditionals in download dir check Co-authored-by: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> --- src/pip/_internal/operations/prepare.py | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 09540edce74..fd5ff781786 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -486,20 +486,25 @@ def prepare_linked_requirement(self, req, parallel_builds=False): link = req.link self._log_preparing_link(req) with indent_log(): + # Check if the relevant file is already available + # in the download directory + file_path = None download_dir = self._get_download_dir(req.link) if download_dir is not None and link.is_wheel: hashes = self._get_linked_req_hashes(req) file_path = _check_download_dir(req.link, download_dir, hashes) - if file_path is not None: - self._downloaded[req.link.url] = file_path, None - else: - file_path = None - if file_path is None: + if file_path is not None: + # The file is already available, so mark it as downloaded + self._downloaded[req.link.url] = file_path, None + else: + # The file is not available, attempt to fetch only metadata wheel_dist = self._fetch_metadata_using_lazy_wheel(link) if wheel_dist is not None: req.needs_more_preparation = True return wheel_dist + + # None of the optimizations worked, fully prepare the requirement return self._prepare_linked_requirement(req, parallel_builds) def prepare_linked_requirements_more(self, reqs, parallel_builds=False): From 0652a2f016b959a4adca67806b3aa30991196ab7 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <hugovk@users.noreply.github.com> Date: Fri, 2 Oct 2020 09:59:38 +0300 Subject: [PATCH 2562/3170] Rename to 'Package index page cache location' --- src/pip/_internal/commands/cache.py | 4 ++-- tests/functional/test_cache.py | 5 ++++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index efba24f1ab4..ec21be68fb5 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -113,8 +113,8 @@ def get_cache_info(self, options, args): ) message = textwrap.dedent(""" - HTTP files location: {http_cache_location} - HTTP files size: {http_cache_size} + Package index page cache location: {http_cache_location} + Package index page cache size: {http_cache_size} Number of HTTP files: {num_http_files} Wheels location: {wheels_cache_location} Wheels size: {wheels_cache_size} diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index a1ffd90902d..872f55982ba 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -190,7 +190,10 @@ def test_cache_info( ): result = script.pip('cache', 'info') - assert 'HTTP files location: {}'.format(http_cache_dir) in result.stdout + assert ( + 'Package index page cache location: {}'.format(http_cache_dir) + in result.stdout + ) assert 'Wheels location: {}'.format(wheel_cache_dir) in result.stdout num_wheels = len(wheel_cache_files) assert 'Number of wheels: {}'.format(num_wheels) in result.stdout From c5744d5643a2a4fbbaaaa5352af77ad6dce19e16 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@gmail.com> Date: Sun, 26 Apr 2020 20:40:25 +0530 Subject: [PATCH 2563/3170] Enforce news/*.{correct-kind}.rst naming --- .pre-commit-config.yaml | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8a62a90f64c..13b3abc62bd 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -85,11 +85,18 @@ repos: - id: python-no-log-warn - id: python-no-eval - id: rst-backticks - # Validate existing ReST files and NEWS fragments. - files: .*\.rst$|^news/.* + files: .*\.rst$ types: [file] - # The errors flagged in NEWS.rst are old. - exclude: NEWS.rst + exclude: NEWS.rst # The errors flagged in NEWS.rst are old. + +- repo: local + hooks: + - id: news-fragment-filenames + name: NEWS fragment + language: fail + entry: NEWS fragment files must be named *.(process|removal|feature|bugfix|vendor|doc|trivial).rst + exclude: ^news/(.gitignore|.*\.(process|removal|feature|bugfix|vendor|doc|trivial).rst) + files: ^news/ - repo: https://github.com/mgedmin/check-manifest rev: '0.43' From 603b2fa4ca886358f776c63596fc5682817213bc Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Thu, 24 Sep 2020 02:06:05 +0530 Subject: [PATCH 2564/3170] Rework the NEWS entries section --- docs/html/development/contributing.rst | 20 +++++++++++--------- 1 file changed, 11 insertions(+), 9 deletions(-) diff --git a/docs/html/development/contributing.rst b/docs/html/development/contributing.rst index 15690dae456..3d2d5ead027 100644 --- a/docs/html/development/contributing.rst +++ b/docs/html/development/contributing.rst @@ -70,15 +70,17 @@ such, but it is preferred to have a dedicated issue (for example, in case the PR ends up rejected due to code quality reasons). Once you have an issue or pull request, you take the number and you create a -file inside of the ``news/`` directory named after that issue number with an -extension of ``removal``, ``feature``, ``bugfix``, or ``doc``. Thus if your -issue or PR number is ``1234`` and this change is fixing a bug, then you would -create a file ``news/1234.bugfix``. PRs can span multiple categories by creating -multiple files (for instance, if you added a feature and deprecated/removed the -old feature at the same time, you would create ``news/NNNN.feature`` and -``news/NNNN.removal``). Likewise if a PR touches multiple issues/PRs you may -create a file for each of them with the exact same contents and Towncrier will -deduplicate them. +file inside of the ``news/`` directory, named after that issue number with a +"type" of ``removal``, ``feature``, ``bugfix``, or ``doc`` associated with it. + +If your issue or PR number is ``1234`` and this change is fixing a bug, +then you would create a file ``news/1234.bugfix.rst``. PRs can span multiple +categories by creating multiple files (for instance, if you added a feature and +deprecated/removed the old feature at the same time, you would create +``news/NNNN.feature.rst`` and ``news/NNNN.removal.rst``). + +If a PR touches multiple issues/PRs, you may create a file for each of them +with the exact same contents and Towncrier will deduplicate them. Contents of a NEWS entry ------------------------ From 8e8b6a6c7d024c76262e16e58719e479a0feec7d Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sat, 3 Oct 2020 16:45:39 +0530 Subject: [PATCH 2565/3170] Rename the NEWS fragments --- ...5.trivial => 093f7456-cc25-4df9-9518-4732b1e07fe5.trivial.rst} | 0 ...5.trivial => 0e494986-202e-4275-b7ec-d6f046c0aa05.trivial.rst} | 0 ...1.trivial => 2a1e2773-eae6-43a4-b075-55f49e713fb1.trivial.rst} | 0 ...b.trivial => 4c82ffb4-cde3-4dd5-8f37-6f4ef53e028b.trivial.rst} | 0 ...2.trivial => 50cf024d-0a74-44c8-b3e9-483dd826fff2.trivial.rst} | 0 ...e.trivial => 559bb022-21ae-498c-a2ce-2c354d880f5e.trivial.rst} | 0 ...0.trivial => 5e8c60c2-d540-4a25-af03-100d848acbc0.trivial.rst} | 0 ...1.trivial => 629892ca-55da-4ca9-9cff-c15373e97ad1.trivial.rst} | 0 ...f.trivial => 6f8eff8d-9886-4e00-b431-5c809500e6bf.trivial.rst} | 0 news/{7231.doc => 7231.doc.rst} | 0 news/{7311.doc => 7311.doc.rst} | 0 news/{8103.bugfix => 8103.bugfix.rst} | 0 news/{8181.removal => 8181.removal.rst} | 0 news/{8355.feature => 8355.feature.rst} | 0 news/{8417.removal => 8417.removal.rst} | 0 news/{8578.bugfix => 8578.bugfix.rst} | 0 news/{8636.doc => 8636.doc.rst} | 0 news/{8676.feature => 8676.feature.rst} | 0 news/{8696.bugfix => 8696.bugfix.rst} | 0 news/{8752.feature => 8752.feature.rst} | 0 news/{8758.bugfix => 8758.bugfix.rst} | 0 news/{8781.trivial => 8781.trivial.rst} | 0 news/{8783.doc => 8783.doc.rst} | 0 news/{8792.bugfix => 8792.bugfix.rst} | 0 news/{8804.feature => 8804.feature.rst} | 0 news/{8807.doc => 8807.doc.rst} | 0 news/{8815.feature => 8815.feature.rst} | 0 news/{8839.bugfix => 8839.bugfix.rst} | 0 news/{8848.doc => 8848.doc.rst} | 0 news/{8861.bugfix => 8861.bugfix.rst} | 0 news/{8892.feature => 8892.feature.rst} | 0 news/{8905.feature => 8905.feature.rst} | 0 news/{8924.feature => 8924.feature.rst} | 0 news/{8927.removal => 8927.removal.rst} | 0 ...0.trivial => 946beace-6164-4d1a-a05d-e9bebf43ccd0.trivial.rst} | 0 ...9.trivial => 9f8da1d9-dd18-47e9-b334-5eb862054409.trivial.rst} | 0 ...f.trivial => a2fa2e68-01bf-11eb-a0b1-4fe8cb1f9dcf.trivial.rst} | 0 ...3.trivial => a3a2b1b7-744e-4533-b3ff-6e7a1843d573.trivial.rst} | 0 ...d.trivial => c6182139-edb4-4bf6-bc3f-2d37cb5759ad.trivial.rst} | 0 ...5.trivial => d53425e4-767e-4d73-bce5-88644b781855.trivial.rst} | 0 ...f.trivial => d90a40c1-15b7-46b9-9162-335bb346b53f.trivial.rst} | 0 41 files changed, 0 insertions(+), 0 deletions(-) rename news/{093f7456-cc25-4df9-9518-4732b1e07fe5.trivial => 093f7456-cc25-4df9-9518-4732b1e07fe5.trivial.rst} (100%) rename news/{0e494986-202e-4275-b7ec-d6f046c0aa05.trivial => 0e494986-202e-4275-b7ec-d6f046c0aa05.trivial.rst} (100%) rename news/{2a1e2773-eae6-43a4-b075-55f49e713fb1.trivial => 2a1e2773-eae6-43a4-b075-55f49e713fb1.trivial.rst} (100%) rename news/{4c82ffb4-cde3-4dd5-8f37-6f4ef53e028b.trivial => 4c82ffb4-cde3-4dd5-8f37-6f4ef53e028b.trivial.rst} (100%) rename news/{50cf024d-0a74-44c8-b3e9-483dd826fff2.trivial => 50cf024d-0a74-44c8-b3e9-483dd826fff2.trivial.rst} (100%) rename news/{559bb022-21ae-498c-a2ce-2c354d880f5e.trivial => 559bb022-21ae-498c-a2ce-2c354d880f5e.trivial.rst} (100%) rename news/{5e8c60c2-d540-4a25-af03-100d848acbc0.trivial => 5e8c60c2-d540-4a25-af03-100d848acbc0.trivial.rst} (100%) rename news/{629892ca-55da-4ca9-9cff-c15373e97ad1.trivial => 629892ca-55da-4ca9-9cff-c15373e97ad1.trivial.rst} (100%) rename news/{6f8eff8d-9886-4e00-b431-5c809500e6bf.trivial => 6f8eff8d-9886-4e00-b431-5c809500e6bf.trivial.rst} (100%) rename news/{7231.doc => 7231.doc.rst} (100%) rename news/{7311.doc => 7311.doc.rst} (100%) rename news/{8103.bugfix => 8103.bugfix.rst} (100%) rename news/{8181.removal => 8181.removal.rst} (100%) rename news/{8355.feature => 8355.feature.rst} (100%) rename news/{8417.removal => 8417.removal.rst} (100%) rename news/{8578.bugfix => 8578.bugfix.rst} (100%) rename news/{8636.doc => 8636.doc.rst} (100%) rename news/{8676.feature => 8676.feature.rst} (100%) rename news/{8696.bugfix => 8696.bugfix.rst} (100%) rename news/{8752.feature => 8752.feature.rst} (100%) rename news/{8758.bugfix => 8758.bugfix.rst} (100%) rename news/{8781.trivial => 8781.trivial.rst} (100%) rename news/{8783.doc => 8783.doc.rst} (100%) rename news/{8792.bugfix => 8792.bugfix.rst} (100%) rename news/{8804.feature => 8804.feature.rst} (100%) rename news/{8807.doc => 8807.doc.rst} (100%) rename news/{8815.feature => 8815.feature.rst} (100%) rename news/{8839.bugfix => 8839.bugfix.rst} (100%) rename news/{8848.doc => 8848.doc.rst} (100%) rename news/{8861.bugfix => 8861.bugfix.rst} (100%) rename news/{8892.feature => 8892.feature.rst} (100%) rename news/{8905.feature => 8905.feature.rst} (100%) rename news/{8924.feature => 8924.feature.rst} (100%) rename news/{8927.removal => 8927.removal.rst} (100%) rename news/{946beace-6164-4d1a-a05d-e9bebf43ccd0.trivial => 946beace-6164-4d1a-a05d-e9bebf43ccd0.trivial.rst} (100%) rename news/{9f8da1d9-dd18-47e9-b334-5eb862054409.trivial => 9f8da1d9-dd18-47e9-b334-5eb862054409.trivial.rst} (100%) rename news/{a2fa2e68-01bf-11eb-a0b1-4fe8cb1f9dcf.trivial => a2fa2e68-01bf-11eb-a0b1-4fe8cb1f9dcf.trivial.rst} (100%) rename news/{a3a2b1b7-744e-4533-b3ff-6e7a1843d573.trivial => a3a2b1b7-744e-4533-b3ff-6e7a1843d573.trivial.rst} (100%) rename news/{c6182139-edb4-4bf6-bc3f-2d37cb5759ad.trivial => c6182139-edb4-4bf6-bc3f-2d37cb5759ad.trivial.rst} (100%) rename news/{d53425e4-767e-4d73-bce5-88644b781855.trivial => d53425e4-767e-4d73-bce5-88644b781855.trivial.rst} (100%) rename news/{d90a40c1-15b7-46b9-9162-335bb346b53f.trivial => d90a40c1-15b7-46b9-9162-335bb346b53f.trivial.rst} (100%) diff --git a/news/093f7456-cc25-4df9-9518-4732b1e07fe5.trivial b/news/093f7456-cc25-4df9-9518-4732b1e07fe5.trivial.rst similarity index 100% rename from news/093f7456-cc25-4df9-9518-4732b1e07fe5.trivial rename to news/093f7456-cc25-4df9-9518-4732b1e07fe5.trivial.rst diff --git a/news/0e494986-202e-4275-b7ec-d6f046c0aa05.trivial b/news/0e494986-202e-4275-b7ec-d6f046c0aa05.trivial.rst similarity index 100% rename from news/0e494986-202e-4275-b7ec-d6f046c0aa05.trivial rename to news/0e494986-202e-4275-b7ec-d6f046c0aa05.trivial.rst diff --git a/news/2a1e2773-eae6-43a4-b075-55f49e713fb1.trivial b/news/2a1e2773-eae6-43a4-b075-55f49e713fb1.trivial.rst similarity index 100% rename from news/2a1e2773-eae6-43a4-b075-55f49e713fb1.trivial rename to news/2a1e2773-eae6-43a4-b075-55f49e713fb1.trivial.rst diff --git a/news/4c82ffb4-cde3-4dd5-8f37-6f4ef53e028b.trivial b/news/4c82ffb4-cde3-4dd5-8f37-6f4ef53e028b.trivial.rst similarity index 100% rename from news/4c82ffb4-cde3-4dd5-8f37-6f4ef53e028b.trivial rename to news/4c82ffb4-cde3-4dd5-8f37-6f4ef53e028b.trivial.rst diff --git a/news/50cf024d-0a74-44c8-b3e9-483dd826fff2.trivial b/news/50cf024d-0a74-44c8-b3e9-483dd826fff2.trivial.rst similarity index 100% rename from news/50cf024d-0a74-44c8-b3e9-483dd826fff2.trivial rename to news/50cf024d-0a74-44c8-b3e9-483dd826fff2.trivial.rst diff --git a/news/559bb022-21ae-498c-a2ce-2c354d880f5e.trivial b/news/559bb022-21ae-498c-a2ce-2c354d880f5e.trivial.rst similarity index 100% rename from news/559bb022-21ae-498c-a2ce-2c354d880f5e.trivial rename to news/559bb022-21ae-498c-a2ce-2c354d880f5e.trivial.rst diff --git a/news/5e8c60c2-d540-4a25-af03-100d848acbc0.trivial b/news/5e8c60c2-d540-4a25-af03-100d848acbc0.trivial.rst similarity index 100% rename from news/5e8c60c2-d540-4a25-af03-100d848acbc0.trivial rename to news/5e8c60c2-d540-4a25-af03-100d848acbc0.trivial.rst diff --git a/news/629892ca-55da-4ca9-9cff-c15373e97ad1.trivial b/news/629892ca-55da-4ca9-9cff-c15373e97ad1.trivial.rst similarity index 100% rename from news/629892ca-55da-4ca9-9cff-c15373e97ad1.trivial rename to news/629892ca-55da-4ca9-9cff-c15373e97ad1.trivial.rst diff --git a/news/6f8eff8d-9886-4e00-b431-5c809500e6bf.trivial b/news/6f8eff8d-9886-4e00-b431-5c809500e6bf.trivial.rst similarity index 100% rename from news/6f8eff8d-9886-4e00-b431-5c809500e6bf.trivial rename to news/6f8eff8d-9886-4e00-b431-5c809500e6bf.trivial.rst diff --git a/news/7231.doc b/news/7231.doc.rst similarity index 100% rename from news/7231.doc rename to news/7231.doc.rst diff --git a/news/7311.doc b/news/7311.doc.rst similarity index 100% rename from news/7311.doc rename to news/7311.doc.rst diff --git a/news/8103.bugfix b/news/8103.bugfix.rst similarity index 100% rename from news/8103.bugfix rename to news/8103.bugfix.rst diff --git a/news/8181.removal b/news/8181.removal.rst similarity index 100% rename from news/8181.removal rename to news/8181.removal.rst diff --git a/news/8355.feature b/news/8355.feature.rst similarity index 100% rename from news/8355.feature rename to news/8355.feature.rst diff --git a/news/8417.removal b/news/8417.removal.rst similarity index 100% rename from news/8417.removal rename to news/8417.removal.rst diff --git a/news/8578.bugfix b/news/8578.bugfix.rst similarity index 100% rename from news/8578.bugfix rename to news/8578.bugfix.rst diff --git a/news/8636.doc b/news/8636.doc.rst similarity index 100% rename from news/8636.doc rename to news/8636.doc.rst diff --git a/news/8676.feature b/news/8676.feature.rst similarity index 100% rename from news/8676.feature rename to news/8676.feature.rst diff --git a/news/8696.bugfix b/news/8696.bugfix.rst similarity index 100% rename from news/8696.bugfix rename to news/8696.bugfix.rst diff --git a/news/8752.feature b/news/8752.feature.rst similarity index 100% rename from news/8752.feature rename to news/8752.feature.rst diff --git a/news/8758.bugfix b/news/8758.bugfix.rst similarity index 100% rename from news/8758.bugfix rename to news/8758.bugfix.rst diff --git a/news/8781.trivial b/news/8781.trivial.rst similarity index 100% rename from news/8781.trivial rename to news/8781.trivial.rst diff --git a/news/8783.doc b/news/8783.doc.rst similarity index 100% rename from news/8783.doc rename to news/8783.doc.rst diff --git a/news/8792.bugfix b/news/8792.bugfix.rst similarity index 100% rename from news/8792.bugfix rename to news/8792.bugfix.rst diff --git a/news/8804.feature b/news/8804.feature.rst similarity index 100% rename from news/8804.feature rename to news/8804.feature.rst diff --git a/news/8807.doc b/news/8807.doc.rst similarity index 100% rename from news/8807.doc rename to news/8807.doc.rst diff --git a/news/8815.feature b/news/8815.feature.rst similarity index 100% rename from news/8815.feature rename to news/8815.feature.rst diff --git a/news/8839.bugfix b/news/8839.bugfix.rst similarity index 100% rename from news/8839.bugfix rename to news/8839.bugfix.rst diff --git a/news/8848.doc b/news/8848.doc.rst similarity index 100% rename from news/8848.doc rename to news/8848.doc.rst diff --git a/news/8861.bugfix b/news/8861.bugfix.rst similarity index 100% rename from news/8861.bugfix rename to news/8861.bugfix.rst diff --git a/news/8892.feature b/news/8892.feature.rst similarity index 100% rename from news/8892.feature rename to news/8892.feature.rst diff --git a/news/8905.feature b/news/8905.feature.rst similarity index 100% rename from news/8905.feature rename to news/8905.feature.rst diff --git a/news/8924.feature b/news/8924.feature.rst similarity index 100% rename from news/8924.feature rename to news/8924.feature.rst diff --git a/news/8927.removal b/news/8927.removal.rst similarity index 100% rename from news/8927.removal rename to news/8927.removal.rst diff --git a/news/946beace-6164-4d1a-a05d-e9bebf43ccd0.trivial b/news/946beace-6164-4d1a-a05d-e9bebf43ccd0.trivial.rst similarity index 100% rename from news/946beace-6164-4d1a-a05d-e9bebf43ccd0.trivial rename to news/946beace-6164-4d1a-a05d-e9bebf43ccd0.trivial.rst diff --git a/news/9f8da1d9-dd18-47e9-b334-5eb862054409.trivial b/news/9f8da1d9-dd18-47e9-b334-5eb862054409.trivial.rst similarity index 100% rename from news/9f8da1d9-dd18-47e9-b334-5eb862054409.trivial rename to news/9f8da1d9-dd18-47e9-b334-5eb862054409.trivial.rst diff --git a/news/a2fa2e68-01bf-11eb-a0b1-4fe8cb1f9dcf.trivial b/news/a2fa2e68-01bf-11eb-a0b1-4fe8cb1f9dcf.trivial.rst similarity index 100% rename from news/a2fa2e68-01bf-11eb-a0b1-4fe8cb1f9dcf.trivial rename to news/a2fa2e68-01bf-11eb-a0b1-4fe8cb1f9dcf.trivial.rst diff --git a/news/a3a2b1b7-744e-4533-b3ff-6e7a1843d573.trivial b/news/a3a2b1b7-744e-4533-b3ff-6e7a1843d573.trivial.rst similarity index 100% rename from news/a3a2b1b7-744e-4533-b3ff-6e7a1843d573.trivial rename to news/a3a2b1b7-744e-4533-b3ff-6e7a1843d573.trivial.rst diff --git a/news/c6182139-edb4-4bf6-bc3f-2d37cb5759ad.trivial b/news/c6182139-edb4-4bf6-bc3f-2d37cb5759ad.trivial.rst similarity index 100% rename from news/c6182139-edb4-4bf6-bc3f-2d37cb5759ad.trivial rename to news/c6182139-edb4-4bf6-bc3f-2d37cb5759ad.trivial.rst diff --git a/news/d53425e4-767e-4d73-bce5-88644b781855.trivial b/news/d53425e4-767e-4d73-bce5-88644b781855.trivial.rst similarity index 100% rename from news/d53425e4-767e-4d73-bce5-88644b781855.trivial rename to news/d53425e4-767e-4d73-bce5-88644b781855.trivial.rst diff --git a/news/d90a40c1-15b7-46b9-9162-335bb346b53f.trivial b/news/d90a40c1-15b7-46b9-9162-335bb346b53f.trivial.rst similarity index 100% rename from news/d90a40c1-15b7-46b9-9162-335bb346b53f.trivial rename to news/d90a40c1-15b7-46b9-9162-335bb346b53f.trivial.rst From 717f54a931d96936cc001b9afe49ddbb510b7e11 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sun, 4 Oct 2020 23:14:46 +0530 Subject: [PATCH 2566/3170] Change the symlink to be a proper file instead. --- news/8848.doc.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) mode change 120000 => 100644 news/8848.doc.rst diff --git a/news/8848.doc.rst b/news/8848.doc.rst deleted file mode 120000 index a318abd149b..00000000000 --- a/news/8848.doc.rst +++ /dev/null @@ -1 +0,0 @@ -8783.doc \ No newline at end of file diff --git a/news/8848.doc.rst b/news/8848.doc.rst new file mode 100644 index 00000000000..6d2bb8762d4 --- /dev/null +++ b/news/8848.doc.rst @@ -0,0 +1 @@ +Added initial UX feedback widgets to docs. From 234bcf6c1176b80f4d44dba6d669acdb4673e3b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Tue, 6 Oct 2020 21:33:35 +0700 Subject: [PATCH 2567/3170] Enforce news/*.rst --- ...c.trivial => 24715eb2-c118-473b-9d99-4f6ce8bbfa83.trivial.rst} | 0 news/bc7f9ea0-030d-11eb-92cb-6b2b625d02fc.trivial.rst | 0 2 files changed, 0 insertions(+), 0 deletions(-) rename news/{bc7f9ea0-030d-11eb-92cb-6b2b625d02fc.trivial => 24715eb2-c118-473b-9d99-4f6ce8bbfa83.trivial.rst} (100%) create mode 100644 news/bc7f9ea0-030d-11eb-92cb-6b2b625d02fc.trivial.rst diff --git a/news/bc7f9ea0-030d-11eb-92cb-6b2b625d02fc.trivial b/news/24715eb2-c118-473b-9d99-4f6ce8bbfa83.trivial.rst similarity index 100% rename from news/bc7f9ea0-030d-11eb-92cb-6b2b625d02fc.trivial rename to news/24715eb2-c118-473b-9d99-4f6ce8bbfa83.trivial.rst diff --git a/news/bc7f9ea0-030d-11eb-92cb-6b2b625d02fc.trivial.rst b/news/bc7f9ea0-030d-11eb-92cb-6b2b625d02fc.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d From 78b294e7461c0df200b894a54362681c7e994815 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Fri, 4 Sep 2020 16:20:13 +0700 Subject: [PATCH 2568/3170] Remove download_dir exist check Both pip download and wheel call endure_dir on the directory. --- src/pip/_internal/commands/download.py | 1 - src/pip/_internal/operations/prepare.py | 18 ++---------------- 2 files changed, 2 insertions(+), 17 deletions(-) diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index 0861d9e67b2..31eebd9628c 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -87,7 +87,6 @@ def run(self, options, args): cmdoptions.check_dist_restriction(options) options.download_dir = normalize_path(options.download_dir) - ensure_dir(options.download_dir) session = self.get_default_session(options) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 3060bafa19c..c48bcca6354 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -353,20 +353,6 @@ def __init__( # Previous "header" printed for a link-based InstallRequirement self._previous_requirement_header = ("", "") - @property - def _download_should_save(self): - # type: () -> bool - if not self.download_dir: - return False - - if os.path.exists(self.download_dir): - return True - - logger.critical('Could not find download directory') - raise InstallationError( - "Could not find or access download directory '{}'" - .format(self.download_dir)) - def _log_preparing_link(self, req): # type: (InstallRequirement) -> None """Provide context for the requirement being prepared.""" @@ -568,9 +554,9 @@ def _prepare_linked_requirement(self, req, parallel_builds): download_path = display_path(download_location) logger.info('Saved %s', download_path) - if self._download_should_save: + if link.is_vcs: # Make a .zip of the source_dir we already created. - if link.is_vcs: + if self.download_dir is not None: req.archive(self.download_dir) return dist From 6887b0795b3567ac2a85578e5509dd8cfcc2b284 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Sat, 12 Sep 2020 20:57:12 +0700 Subject: [PATCH 2569/3170] Merge usage of download_dir and wheel_download_dir In every cases, at least one of them is None. By doing this, it is also possible to simplify wrapper codes around download_dir. --- src/pip/_internal/cli/req_command.py | 2 -- src/pip/_internal/commands/wheel.py | 2 +- src/pip/_internal/operations/prepare.py | 46 +++++++------------------ src/pip/_internal/req/req_install.py | 4 ++- tests/unit/test_req.py | 1 - 5 files changed, 17 insertions(+), 38 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 0757e34f66e..03cc52f6966 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -203,7 +203,6 @@ def make_requirement_preparer( finder, # type: PackageFinder use_user_site, # type: bool download_dir=None, # type: str - wheel_download_dir=None, # type: str ): # type: (...) -> RequirementPreparer """ @@ -229,7 +228,6 @@ def make_requirement_preparer( build_dir=temp_build_dir_path, src_dir=options.src_dir, download_dir=download_dir, - wheel_download_dir=wheel_download_dir, build_isolation=options.build_isolation, req_tracker=req_tracker, session=session, diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index 0f718566bd0..38a9b197fed 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -137,7 +137,7 @@ def run(self, options, args): req_tracker=req_tracker, session=session, finder=finder, - wheel_download_dir=options.wheel_dir, + download_dir=options.wheel_dir, use_user_site=False, ) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index c48bcca6354..8767115b8e0 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -300,7 +300,6 @@ def __init__( build_dir, # type: str download_dir, # type: Optional[str] src_dir, # type: str - wheel_download_dir, # type: Optional[str] build_isolation, # type: bool req_tracker, # type: RequirementTracker session, # type: PipSession @@ -325,16 +324,6 @@ def __init__( # 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. - 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'. - # Is build isolation allowed? self.build_isolation = build_isolation @@ -371,15 +360,8 @@ def _log_preparing_link(self, req): with indent_log(): logger.info("Using cached %s", req.link.filename) - def _get_download_dir(self, link): - # type: (Link) -> Optional[str] - if link.is_wheel and self.wheel_download_dir: - # Download wheels to a dedicated dir when doing `pip wheel`. - return self.wheel_download_dir - return self.download_dir - - def _ensure_link_req_src_dir(self, req, download_dir, parallel_builds): - # type: (InstallRequirement, Optional[str], bool) -> None + def _ensure_link_req_src_dir(self, req, parallel_builds): + # type: (InstallRequirement, bool) -> None """Ensure source_dir of a linked InstallRequirement.""" # Since source_dir is only set for editable requirements. if req.link.is_wheel: @@ -480,10 +462,9 @@ def prepare_linked_requirement(self, req, parallel_builds=False): # Check if the relevant file is already available # in the download directory file_path = None - download_dir = self._get_download_dir(req.link) - if download_dir is not None and link.is_wheel: + if self.download_dir is not None and link.is_wheel: hashes = self._get_linked_req_hashes(req) - file_path = _check_download_dir(req.link, download_dir, hashes) + file_path = _check_download_dir(req.link, self.download_dir, hashes) if file_path is not None: # The file is already available, so mark it as downloaded @@ -514,15 +495,14 @@ def _prepare_linked_requirement(self, req, parallel_builds): # type: (InstallRequirement, bool) -> Distribution assert req.link link = req.link - download_dir = self._get_download_dir(link) - self._ensure_link_req_src_dir(req, download_dir, parallel_builds) + self._ensure_link_req_src_dir(req, parallel_builds) hashes = self._get_linked_req_hashes(req) if link.url not in self._downloaded: try: local_file = unpack_url( link, req.source_dir, self._download, - download_dir, hashes, + self.download_dir, hashes, ) except NetworkConnectionError as exc: raise InstallationError( @@ -544,11 +524,13 @@ def _prepare_linked_requirement(self, req, parallel_builds): req, self.req_tracker, self.finder, self.build_isolation, ) - if download_dir: + if self.download_dir is not None: if link.is_existing_dir(): logger.info('Link is a directory, ignoring download_dir') elif local_file: - download_location = os.path.join(download_dir, link.filename) + download_location = os.path.join( + self.download_dir, link.filename + ) if not os.path.exists(download_location): shutil.copy(local_file.path, download_location) download_path = display_path(download_location) @@ -556,8 +538,7 @@ def _prepare_linked_requirement(self, req, parallel_builds): if link.is_vcs: # Make a .zip of the source_dir we already created. - if self.download_dir is not None: - req.archive(self.download_dir) + req.archive(self.download_dir) return dist def prepare_editable_requirement( @@ -579,14 +560,13 @@ def prepare_editable_requirement( 'hash.'.format(req) ) req.ensure_has_source_dir(self.src_dir) - req.update_editable(not self._download_should_save) + req.update_editable(self.download_dir is None) dist = _get_prepared_distribution( req, self.req_tracker, self.finder, self.build_isolation, ) - if self._download_should_save: - req.archive(self.download_dir) + req.archive(self.download_dir) req.check_if_exists(self.use_user_site) return dist diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 42999a59f77..8ce299503b8 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -700,12 +700,14 @@ def _clean_zip_name(name, prefix): return self.name + '/' + name def archive(self, build_dir): - # type: (str) -> None + # type: (Optional[str]) -> None """Saves archive to provided build_dir. Used for saving downloaded VCS requirements as part of `pip download`. """ assert self.source_dir + if build_dir is None: + return create_archive = True archive_name = '{}-{}.zip'.format(self.name, self.metadata["version"]) diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index 730a26a88d5..083d2c2c60d 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -82,7 +82,6 @@ def _basic_resolver(self, finder, require_hashes=False): build_dir=os.path.join(self.tempdir, 'build'), src_dir=os.path.join(self.tempdir, 'src'), download_dir=None, - wheel_download_dir=None, build_isolation=True, req_tracker=tracker, session=session, From b28e2c4928cc62d90b738a4613886fb1e2ad6a81 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Sat, 12 Sep 2020 21:08:49 +0700 Subject: [PATCH 2570/3170] New resolver: Avoid polluting dest dir Previously, during dependency resolution for `pip download -d <dir>` or `pip wheel -w <dir>`, distributions downloaded are always saved to <dir>, even for those are only used in backtracking and are not part of the returned requirement set. --- news/8827.bugfix.rst | 2 ++ src/pip/_internal/commands/download.py | 1 + src/pip/_internal/commands/wheel.py | 11 +++++--- src/pip/_internal/operations/prepare.py | 36 ++++++++++++++++--------- 4 files changed, 33 insertions(+), 17 deletions(-) create mode 100644 news/8827.bugfix.rst diff --git a/news/8827.bugfix.rst b/news/8827.bugfix.rst new file mode 100644 index 00000000000..608cd3d5c61 --- /dev/null +++ b/news/8827.bugfix.rst @@ -0,0 +1,2 @@ +Avoid polluting the destination directory by resolution artifacts +when the new resolver is used for ``pip download`` or ``pip wheel``. diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index 31eebd9628c..2f151e049cf 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -137,6 +137,7 @@ def run(self, options, args): for req in requirement_set.requirements.values(): if not req.editable and req.satisfied_by is None: assert req.name is not None + preparer.save_linked_requirement(req) downloaded.append(req.name) if downloaded: write_output('Successfully downloaded %s', ' '.join(downloaded)) diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index 38a9b197fed..8f5783c353f 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -21,6 +21,7 @@ from optparse import Values from typing import List + from pip._internal.req.req_install import InstallRequirement logger = logging.getLogger(__name__) @@ -156,10 +157,12 @@ def run(self, options, args): reqs, check_supported_wheels=True ) - reqs_to_build = [ - r for r in requirement_set.requirements.values() - if should_build_for_wheel_command(r) - ] + reqs_to_build = [] # type: List[InstallRequirement] + for req in requirement_set.requirements.values(): + if req.is_wheel: + preparer.save_linked_requirement(req) + elif should_build_for_wheel_command(req): + reqs_to_build.append(req) # build wheels build_successes, build_failures = build( diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 8767115b8e0..de017504abe 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -523,23 +523,33 @@ def _prepare_linked_requirement(self, req, parallel_builds): dist = _get_prepared_distribution( req, self.req_tracker, self.finder, self.build_isolation, ) + return dist - if self.download_dir is not None: - if link.is_existing_dir(): - logger.info('Link is a directory, ignoring download_dir') - elif local_file: - download_location = os.path.join( - self.download_dir, link.filename - ) - if not os.path.exists(download_location): - shutil.copy(local_file.path, download_location) - download_path = display_path(download_location) - logger.info('Saved %s', download_path) - + def save_linked_requirement(self, req): + # type: (InstallRequirement) -> None + assert self.download_dir is not None + assert req.link is not None + link = req.link if link.is_vcs: # Make a .zip of the source_dir we already created. req.archive(self.download_dir) - return dist + return + + if link.is_existing_dir(): + logger.debug( + 'Not copying link to destination directory ' + 'since it is a directory: %s', link, + ) + return + if req.local_file_path is None: + # No distribution was downloaded for this requirement. + return + + download_location = os.path.join(self.download_dir, link.filename) + if not os.path.exists(download_location): + shutil.copy(req.local_file_path, download_location) + download_path = display_path(download_location) + logger.info('Saved %s', download_path) def prepare_editable_requirement( self, From e48234e66908e0babe167f66eb910e3c65880796 Mon Sep 17 00:00:00 2001 From: David Poggi <drpoggi@users.noreply.github.com> Date: Thu, 8 Oct 2020 11:43:08 -0400 Subject: [PATCH 2571/3170] Fix docs for trusted-host in config file --- docs/html/user_guide.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index a2d13c43358..bfeaa94ae42 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -649,8 +649,8 @@ which can be written on multiple lines: http://mirror2.example.com trusted-host = - http://mirror1.example.com - http://mirror2.example.com + mirror1.example.com + mirror2.example.com This enables users to add additional values in the order of entry for such command line arguments. From 22406d48fce732655fc2aa510cb76783774776d1 Mon Sep 17 00:00:00 2001 From: Danny McClanahan <1305167+cosmicexplorer@users.noreply.github.com> Date: Tue, 22 Sep 2020 01:26:43 -0700 Subject: [PATCH 2572/3170] download requirements in the download command, outside of the resolver create PartialRequirementDownloadCompleter, and use in wheel, install, and download add NEWS entry rename NEWS entry rename NEWS entry respond to review comments move the partial requirement download completion to the bottom of the prepare_more method --- news/8896.trivial | 1 + src/pip/_internal/network/download.py | 4 +- src/pip/_internal/operations/prepare.py | 64 ++++++++++++++++++++++--- 3 files changed, 60 insertions(+), 9 deletions(-) create mode 100644 news/8896.trivial diff --git a/news/8896.trivial b/news/8896.trivial new file mode 100644 index 00000000000..3488b8e057a --- /dev/null +++ b/news/8896.trivial @@ -0,0 +1 @@ +Separate the batched *download* of lazily-fetched wheel files from the preparation of regularly-downloaded requirements in ``RequirementPreparer.prepare_linked_requirements_more()``. diff --git a/src/pip/_internal/network/download.py b/src/pip/_internal/network/download.py index 76896e89970..bad9e961085 100644 --- a/src/pip/_internal/network/download.py +++ b/src/pip/_internal/network/download.py @@ -178,7 +178,7 @@ def __init__( self._progress_bar = progress_bar def __call__(self, links, location): - # type: (Iterable[Link], str) -> Iterable[Tuple[str, Tuple[str, str]]] + # type: (Iterable[Link], str) -> Iterable[Tuple[Link, Tuple[str, str]]] """Download the files given by links into location.""" for link in links: try: @@ -199,4 +199,4 @@ def __call__(self, links, location): for chunk in chunks: content_file.write(chunk) content_type = resp.headers.get('Content-Type', '') - yield link.url, (filepath, content_type) + yield link, (filepath, content_type) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index de017504abe..45d7341eaf3 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -452,6 +452,39 @@ def _fetch_metadata_using_lazy_wheel(self, link): logger.debug('%s does not support range requests', url) return None + def _complete_partial_requirements( + self, + partially_downloaded_reqs, # type: Iterable[InstallRequirement] + parallel_builds=False, # type: bool + ): + # type: (...) -> None + """Download any requirements which were only fetched by metadata.""" + # Download to a temporary directory. These will be copied over as + # needed for downstream 'download', 'wheel', and 'install' commands. + temp_dir = TempDirectory(kind="unpack", globally_managed=True).path + + # Map each link to the requirement that owns it. This allows us to set + # `req.local_file_path` on the appropriate requirement after passing + # all the links at once into BatchDownloader. + links_to_fully_download = {} # type: Dict[Link, InstallRequirement] + for req in partially_downloaded_reqs: + assert req.link + links_to_fully_download[req.link] = req + + batch_download = self._batch_download( + links_to_fully_download.keys(), + temp_dir, + ) + for link, (filepath, _) in batch_download: + logger.debug("Downloading link %s to %s", link, filepath) + req = links_to_fully_download[link] + req.local_file_path = filepath + + # This step is necessary to ensure all lazy wheels are processed + # successfully by the 'download', 'wheel', and 'install' commands. + for req in partially_downloaded_reqs: + self._prepare_linked_requirement(req, parallel_builds) + def prepare_linked_requirement(self, req, parallel_builds=False): # type: (InstallRequirement, bool) -> Distribution """Prepare a requirement to be obtained from req.link.""" @@ -481,15 +514,32 @@ def prepare_linked_requirement(self, req, parallel_builds=False): def prepare_linked_requirements_more(self, reqs, parallel_builds=False): # type: (Iterable[InstallRequirement], bool) -> None - """Prepare a linked requirement more, if needed.""" + """Prepare linked requirements more, if needed.""" reqs = [req for req in reqs if req.needs_more_preparation] - links = [req.link for req in reqs] - - # Let's download to a temporary directory. - tmpdir = TempDirectory(kind="unpack", globally_managed=True).path - self._downloaded.update(self._batch_download(links, tmpdir)) for req in reqs: - self._prepare_linked_requirement(req, parallel_builds) + # Determine if any of these requirements were already downloaded. + download_dir = self._get_download_dir(req.link) + if download_dir is not None: + hashes = self._get_linked_req_hashes(req) + file_path = _check_download_dir(req.link, download_dir, hashes) + if file_path is not None: + self._downloaded[req.link.url] = file_path, None + req.needs_more_preparation = False + + # Prepare requirements we found were already downloaded for some + # reason. The other downloads will be completed separately. + partially_downloaded_reqs = [] # type: List[InstallRequirement] + for req in reqs: + if req.needs_more_preparation: + partially_downloaded_reqs.append(req) + else: + self._prepare_linked_requirement(req, parallel_builds) + + # TODO: separate this part out from RequirementPreparer when the v1 + # resolver can be removed! + self._complete_partial_requirements( + partially_downloaded_reqs, parallel_builds=parallel_builds, + ) def _prepare_linked_requirement(self, req, parallel_builds): # type: (InstallRequirement, bool) -> Distribution From 24ad324aefab90d396cba725c6076731ee707c6b Mon Sep 17 00:00:00 2001 From: Danny McClanahan <1305167+cosmicexplorer@users.noreply.github.com> Date: Fri, 9 Oct 2020 00:44:44 -0700 Subject: [PATCH 2573/3170] fix lint --- news/{8896.trivial => 8896.trivial.rst} | 0 src/pip/_internal/operations/prepare.py | 5 ++--- 2 files changed, 2 insertions(+), 3 deletions(-) rename news/{8896.trivial => 8896.trivial.rst} (100%) diff --git a/news/8896.trivial b/news/8896.trivial.rst similarity index 100% rename from news/8896.trivial rename to news/8896.trivial.rst diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 45d7341eaf3..7e822f863f9 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -518,10 +518,9 @@ def prepare_linked_requirements_more(self, reqs, parallel_builds=False): reqs = [req for req in reqs if req.needs_more_preparation] for req in reqs: # Determine if any of these requirements were already downloaded. - download_dir = self._get_download_dir(req.link) - if download_dir is not None: + if self.download_dir is not None and req.link.is_wheel: hashes = self._get_linked_req_hashes(req) - file_path = _check_download_dir(req.link, download_dir, hashes) + file_path = _check_download_dir(req.link, self.download_dir, hashes) if file_path is not None: self._downloaded[req.link.url] = file_path, None req.needs_more_preparation = False From 960dca9949cf742b2766f9fc20277321f44cf09c Mon Sep 17 00:00:00 2001 From: Noah Gorny <noah@gittabags.com> Date: Fri, 9 Oct 2020 16:32:09 +0300 Subject: [PATCH 2574/3170] resolvelib: factory: Get installed distributions correctly --- src/pip/_internal/resolution/resolvelib/factory.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 96e2d53314d..3300cc8c591 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -98,9 +98,15 @@ def __init__( self._editable_candidate_cache = {} # type: Cache[EditableCandidate] if not ignore_installed: + packages = get_installed_distributions( + local_only=False, + include_editables=True, + editables_only=False, + user_only=False, + paths=None, + ) self._installed_dists = { - canonicalize_name(dist.project_name): dist - for dist in get_installed_distributions() + canonicalize_name(p.key): p for p in packages } else: self._installed_dists = {} From 7e02958a1e03fffe0bb3b79c9d8a4f8f78d9ac03 Mon Sep 17 00:00:00 2001 From: Noah Gorny <noah@gittabags.com> Date: Fri, 9 Oct 2020 16:44:09 +0300 Subject: [PATCH 2575/3170] Add news fragment for 8963 --- news/8963.bugfix.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 news/8963.bugfix.rst diff --git a/news/8963.bugfix.rst b/news/8963.bugfix.rst new file mode 100644 index 00000000000..62c01b464ec --- /dev/null +++ b/news/8963.bugfix.rst @@ -0,0 +1,2 @@ +Correctly search for installed distributions in new resolver logic in order +to not miss packages (virtualenv packages from system-wide-packages for example) From da7569a4404342b03e96b0e7ac31391ae58aecdd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Thu, 8 Oct 2020 22:31:41 +0700 Subject: [PATCH 2576/3170] Document and start testing Python 3.9 support At first use it for GitHub Action for linting. --- .github/workflows/linting.yml | 6 +++--- news/8971.feature.rst | 1 + noxfile.py | 2 +- setup.py | 1 + tox.ini | 8 ++++---- 5 files changed, 10 insertions(+), 8 deletions(-) create mode 100644 news/8971.feature.rst diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 512969b0dff..0ff16be6445 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -23,10 +23,10 @@ jobs: steps: - uses: actions/checkout@v2 - - name: Set up Python 3.8 - uses: actions/setup-python@v1 + - name: Set up Python 3.9 + uses: actions/setup-python@v2 with: - python-version: 3.8 + python-version: 3.9 # Setup Caching - name: pip cache diff --git a/news/8971.feature.rst b/news/8971.feature.rst new file mode 100644 index 00000000000..e0b7b19cf69 --- /dev/null +++ b/news/8971.feature.rst @@ -0,0 +1 @@ +Support Python 3.9. diff --git a/noxfile.py b/noxfile.py index 1746bb69915..93a3b24d86d 100644 --- a/noxfile.py +++ b/noxfile.py @@ -69,7 +69,7 @@ def should_update_common_wheels(): # completely to nox for all our automation. Contributors should prefer using # `tox -e ...` until this note is removed. # ----------------------------------------------------------------------------- -@nox.session(python=["2.7", "3.5", "3.6", "3.7", "3.8", "pypy", "pypy3"]) +@nox.session(python=["2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "pypy", "pypy3"]) def test(session): # Get the common wheels. if should_update_common_wheels(): diff --git a/setup.py b/setup.py index 0557690deca..2601d8bd912 100644 --- a/setup.py +++ b/setup.py @@ -47,6 +47,7 @@ def get_version(rel_path): "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", + "Programming Language :: Python :: 3.9", "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ], diff --git a/tox.ini b/tox.ini index 82e9abc68d7..30a908cd36b 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,7 @@ minversion = 3.4.0 envlist = docs, packaging, lint, vendoring, - py27, py35, py36, py37, py38, pypy, pypy3 + py27, py35, py36, py37, py38, py39, pypy, pypy3 [helpers] # Wrapper for calls to pip that make sure the version being used is the @@ -49,7 +49,7 @@ setenv = [testenv:docs] # Don't skip install here since pip_sphinxext uses pip's internals. deps = -r{toxinidir}/tools/requirements/docs.txt -basepython = python3.8 +basepython = python3 commands = sphinx-build -W -d {envtmpdir}/doctrees/html -b html docs/html docs/build/html # Having the conf.py in the docs/html is weird but needed because we @@ -66,11 +66,11 @@ commands = pre-commit run [] --all-files --show-diff-on-failure [testenv:vendoring] -basepython = python3.8 +basepython = python3 skip_install = True commands_pre = deps = - vendoring==0.2.2 + vendoring>=0.3.3 # Required, otherwise we interpret --no-binary :all: as # "do not build wheels", which fails for PEP 517 requirements pip>=19.3.1 From cf6ecab62729cd361e5913bfa0132d104ec11ccf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Mon, 12 Oct 2020 17:36:51 +0700 Subject: [PATCH 2577/3170] Bump mypy to 0.790 for Python 3.9 compat HashError.order is now annotated as an int to allow HashErrors.errors.sort(key=lambda e: e.order). Alternatively we can define a function which assert e is not None but I prefer the more concise version, since we never raise HashError directly anyway. --- .pre-commit-config.yaml | 2 +- src/pip/_internal/exceptions.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 13b3abc62bd..838b1f24ebe 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -69,7 +69,7 @@ repos: files: \.py$ - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.770 + rev: v0.790 hooks: - id: mypy exclude: docs|tests diff --git a/src/pip/_internal/exceptions.py b/src/pip/_internal/exceptions.py index 62bde1eeda4..56482caf77b 100644 --- a/src/pip/_internal/exceptions.py +++ b/src/pip/_internal/exceptions.py @@ -200,7 +200,7 @@ class HashError(InstallationError): """ req = None # type: Optional[InstallRequirement] head = '' - order = None # type: Optional[int] + order = -1 # type: int def body(self): # type: () -> str From 336e979894b4e851cc1508450128977871e6b0e7 Mon Sep 17 00:00:00 2001 From: Anthony Sottile <asottile@umich.edu> Date: Mon, 12 Oct 2020 16:04:24 -0700 Subject: [PATCH 2578/3170] delete some dead test code (VERBOSE_FALSE) was just browsing through some files and noticed this --- tests/functional/test_search.py | 6 ------ tests/unit/test_vcs.py | 7 +------ 2 files changed, 1 insertion(+), 12 deletions(-) diff --git a/tests/functional/test_search.py b/tests/functional/test_search.py index 1892e26b5b5..875e2e5d6df 100644 --- a/tests/functional/test_search.py +++ b/tests/functional/test_search.py @@ -6,12 +6,6 @@ from pip._internal.cli.status_codes import NO_MATCHES_FOUND, SUCCESS from pip._internal.commands import create_command from pip._internal.commands.search import highest_version, print_results, transform_hits -from tests.lib import pyversion - -if pyversion >= '3': - VERBOSE_FALSE = False -else: - VERBOSE_FALSE = 0 def test_version_compare(): diff --git a/tests/unit/test_vcs.py b/tests/unit/test_vcs.py index b6ed86b6296..d36f9f01deb 100644 --- a/tests/unit/test_vcs.py +++ b/tests/unit/test_vcs.py @@ -13,12 +13,7 @@ from pip._internal.vcs.mercurial import Mercurial from pip._internal.vcs.subversion import Subversion from pip._internal.vcs.versioncontrol import RevOptions, VersionControl -from tests.lib import is_svn_installed, need_svn, pyversion - -if pyversion >= '3': - VERBOSE_FALSE = False -else: - VERBOSE_FALSE = 0 +from tests.lib import is_svn_installed, need_svn @pytest.mark.skipif( From 8326148149e1993928bd63cbd1e3a39d79867ce4 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Mon, 28 Sep 2020 16:36:04 +0800 Subject: [PATCH 2579/3170] Implement "lazy sequence" to avoid Internet find_matches() is modified to return a special type that implements the sequence protocol (instead of a plain list). This special sequence type tries to use the installed candidate as the first element if possible, and only access indexes when the installed candidate is considered unsatisfactory. --- .../resolution/resolvelib/factory.py | 67 +++++----- .../resolution/resolvelib/found_candidates.py | 122 ++++++++++++++++++ .../resolution/resolvelib/provider.py | 97 ++++---------- .../resolution_resolvelib/test_requirement.py | 9 +- 4 files changed, 182 insertions(+), 113 deletions(-) create mode 100644 src/pip/_internal/resolution/resolvelib/found_candidates.py diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 3300cc8c591..0ac8d1af9d4 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -1,7 +1,5 @@ -import collections import logging -from pip._vendor import six from pip._vendor.packaging.utils import canonicalize_name from pip._internal.exceptions import ( @@ -30,6 +28,7 @@ LinkCandidate, RequiresPythonCandidate, ) +from .found_candidates import FoundCandidates from .requirements import ( ExplicitRequirement, RequiresPythonRequirement, @@ -41,6 +40,7 @@ Dict, FrozenSet, Iterable, + Iterator, List, Optional, Sequence, @@ -98,15 +98,9 @@ def __init__( self._editable_candidate_cache = {} # type: Cache[EditableCandidate] if not ignore_installed: - packages = get_installed_distributions( - local_only=False, - include_editables=True, - editables_only=False, - user_only=False, - paths=None, - ) self._installed_dists = { - canonicalize_name(p.key): p for p in packages + canonicalize_name(dist.project_name): dist + for dist in get_installed_distributions(local_only=False) } else: self._installed_dists = {} @@ -160,6 +154,7 @@ def _iter_found_candidates( ireqs, # type: Sequence[InstallRequirement] specifier, # type: SpecifierSet hashes, # type: Hashes + prefers_installed, # type: bool ): # type: (...) -> Iterable[Candidate] if not ireqs: @@ -178,54 +173,49 @@ def _iter_found_candidates( hashes &= ireq.hashes(trust_internet=False) extras |= frozenset(ireq.extras) - # We use this to ensure that we only yield a single candidate for - # each version (the finder's preferred one for that version). The - # requirement needs to return only one candidate per version, so we - # implement that logic here so that requirements using this helper - # don't all have to do the same thing later. - candidates = collections.OrderedDict() # type: VersionCandidates - # Get the installed version, if it matches, unless the user # specified `--force-reinstall`, when we want the version from # the index instead. - installed_version = None installed_candidate = None if not self._force_reinstall and name in self._installed_dists: installed_dist = self._installed_dists[name] - installed_version = installed_dist.parsed_version - if specifier.contains(installed_version, prereleases=True): + if specifier.contains(installed_dist.version, prereleases=True): installed_candidate = self._make_candidate_from_dist( dist=installed_dist, extras=extras, template=template, ) - found = self._finder.find_best_candidate( - project_name=name, - specifier=specifier, - hashes=hashes, - ) - for ican in found.iter_applicable(): - if ican.version == installed_version and installed_candidate: - candidate = installed_candidate - else: - candidate = self._make_candidate_from_link( + def iter_index_candidates(): + # type: () -> Iterator[Candidate] + result = self._finder.find_best_candidate( + project_name=name, + specifier=specifier, + hashes=hashes, + ) + # PackageFinder returns earlier versions first, so we reverse. + for ican in reversed(list(result.iter_applicable())): + yield self._make_candidate_from_link( link=ican.link, extras=extras, template=template, name=name, version=ican.version, ) - candidates[ican.version] = candidate - # Yield the installed version even if it is not found on the index. - if installed_version and installed_candidate: - candidates[installed_version] = installed_candidate - - return six.itervalues(candidates) + return FoundCandidates( + iter_index_candidates, + installed_candidate, + prefers_installed, + ) - def find_candidates(self, requirements, constraint): - # type: (Sequence[Requirement], Constraint) -> Iterable[Candidate] + def find_candidates( + self, + requirements, # type: Sequence[Requirement] + constraint, # type: Constraint + prefers_installed, # type: bool + ): + # type: (...) -> Iterable[Candidate] explicit_candidates = set() # type: Set[Candidate] ireqs = [] # type: List[InstallRequirement] for req in requirements: @@ -242,6 +232,7 @@ def find_candidates(self, requirements, constraint): ireqs, constraint.specifier, constraint.hashes, + prefers_installed, ) if constraint: diff --git a/src/pip/_internal/resolution/resolvelib/found_candidates.py b/src/pip/_internal/resolution/resolvelib/found_candidates.py new file mode 100644 index 00000000000..0ddcb1fcc8d --- /dev/null +++ b/src/pip/_internal/resolution/resolvelib/found_candidates.py @@ -0,0 +1,122 @@ +from pip._vendor.six.moves import collections_abc + +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import Callable, Iterator, Optional, Set + + from pip._vendor.packaging.version import _BaseVersion + + from .base import Candidate + + +class _InstalledFirstCandidatesIterator(collections_abc.Iterator): + """Iterator for ``FoundCandidates``. + + This iterator is used when the resolver prefers to keep the version of an + already-installed package. The already-installed candidate is always + returned first. Candidates from index are accessed only when the resolver + wants them, and the already-installed version is excluded from them. + """ + def __init__( + self, + get_others, # type: Callable[[], Iterator[Candidate]] + installed, # type: Optional[Candidate] + ): + self._installed = installed + self._get_others = get_others + self._others = None # type: Optional[Iterator[Candidate]] + self._returned = set() # type: Set[_BaseVersion] + + def __next__(self): + # type: () -> Candidate + if self._installed and self._installed.version not in self._returned: + self._returned.add(self._installed.version) + return self._installed + if self._others is None: + self._others = self._get_others() + cand = next(self._others) + while cand.version in self._returned: + cand = next(self._others) + self._returned.add(cand.version) + return cand + + next = __next__ # XXX: Python 2. + + +class _InstalledReplacesCandidatesIterator(collections_abc.Iterator): + """Iterator for ``FoundCandidates``. + + This iterator is used when the resolver prefers to upgrade an + already-installed package. Candidates from index are returned in their + normal ordering, except replaced when the version is already installed. + """ + def __init__( + self, + get_others, # type: Callable[[], Iterator[Candidate]] + installed, # type: Optional[Candidate] + ): + self._installed = installed + self._get_others = get_others + self._others = None # type: Optional[Iterator[Candidate]] + self._returned = set() # type: Set[_BaseVersion] + + def __next__(self): + # type: () -> Candidate + if self._others is None: + self._others = self._get_others() + cand = next(self._others) + while cand.version in self._returned: + cand = next(self._others) + if self._installed and cand.version == self._installed.version: + cand = self._installed + self._returned.add(cand.version) + return cand + + next = __next__ # XXX: Python 2. + + +class FoundCandidates(collections_abc.Sequence): + """A lazy sequence to provide candidates to the resolver. + + The intended usage is to return this from `find_matches()` so the resolver + can iterate through the sequence multiple times, but only access the index + page when remote packages are actually needed. This improve performances + when suitable candidates are already installed on disk. + """ + def __init__( + self, + get_others, # type: Callable[[], Iterator[Candidate]] + installed, # type: Optional[Candidate] + prefers_installed, # type: bool + ): + self._get_others = get_others + self._installed = installed + self._prefers_installed = prefers_installed + + def __getitem__(self, index): + # type: (int) -> Candidate + for i, value in enumerate(self): + if index == i: + return value + raise IndexError(index) + + def __iter__(self): + # type: () -> Iterator[Candidate] + if self._prefers_installed: + klass = _InstalledFirstCandidatesIterator + else: + klass = _InstalledReplacesCandidatesIterator + return klass(self._get_others, self._installed) + + def __len__(self): + # type: () -> int + return sum(1 for _ in self) + + def __bool__(self): + # type: () -> bool + if self._prefers_installed and self._installed: + return True + return any(self) + + __nonzero__ = __bool__ # XXX: Python 2. diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index 8264b471c90..7f7d0e1540b 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -45,30 +45,26 @@ def __init__( self._upgrade_strategy = upgrade_strategy self._user_requested = user_requested - def _sort_matches(self, matches): - # type: (Iterable[Candidate]) -> Sequence[Candidate] - - # The requirement is responsible for returning a sequence of potential - # candidates, one per version. The provider handles the logic of - # deciding the order in which these candidates should be passed to - # the resolver. - - # The `matches` argument is a sequence of candidates, one per version, - # which are potential options to be installed. The requirement will - # have already sorted out whether to give us an already-installed - # candidate or a version from PyPI (i.e., it will deal with options - # like --force-reinstall and --ignore-installed). - - # We now work out the correct order. - # - # 1. If no other considerations apply, later versions take priority. - # 2. An already installed distribution is preferred over any other, - # unless the user has requested an upgrade. - # Upgrades are allowed when: - # * The --upgrade flag is set, and - # - The project was specified on the command line, or - # - The project is a dependency and the "eager" upgrade strategy - # was requested. + def identify(self, dependency): + # type: (Union[Requirement, Candidate]) -> str + return dependency.name + + def get_preference( + self, + resolution, # type: Optional[Candidate] + candidates, # type: Sequence[Candidate] + information # type: Sequence[Tuple[Requirement, Candidate]] + ): + # type: (...) -> Any + transitive = all(parent is not None for _, parent in information) + return (transitive, bool(candidates)) + + def find_matches(self, requirements): + # type: (Sequence[Requirement]) -> Iterable[Candidate] + if not requirements: + return [] + name = requirements[0].name + def _eligible_for_upgrade(name): # type: (str) -> bool """Are upgrades allowed for this project? @@ -87,56 +83,11 @@ def _eligible_for_upgrade(name): return (name in self._user_requested) return False - def sort_key(c): - # type: (Candidate) -> int - """Return a sort key for the matches. - - The highest priority should be given to installed candidates that - are not eligible for upgrade. We use the integer value in the first - part of the key to sort these before other candidates. - - We only pull the installed candidate to the bottom (i.e. most - preferred), but otherwise keep the ordering returned by the - requirement. The requirement is responsible for returning a list - otherwise sorted for the resolver, taking account for versions - and binary preferences as specified by the user. - """ - if c.is_installed and not _eligible_for_upgrade(c.name): - return 1 - return 0 - - return sorted(matches, key=sort_key) - - def identify(self, dependency): - # type: (Union[Requirement, Candidate]) -> str - return dependency.name - - def get_preference( - self, - resolution, # type: Optional[Candidate] - candidates, # type: Sequence[Candidate] - information # type: Sequence[Tuple[Requirement, Optional[Candidate]]] - ): - # type: (...) -> Any - """Return a sort key to determine what dependency to look next. - - A smaller value makes a dependency higher priority. We put direct - (user-requested) dependencies first since they may contain useful - user-specified version ranges. Users tend to expect us to catch - problems in them early as well. - """ - transitive = all(parent is not None for _, parent in information) - return (transitive, len(candidates)) - - def find_matches(self, requirements): - # type: (Sequence[Requirement]) -> Iterable[Candidate] - if not requirements: - return [] - constraint = self._constraints.get( - requirements[0].name, Constraint.empty(), + return self._factory.find_candidates( + requirements, + constraint=self._constraints.get(name, Constraint.empty()), + prefers_installed=(not _eligible_for_upgrade(name)), ) - candidates = self._factory.find_candidates(requirements, constraint) - return reversed(self._sort_matches(candidates)) def is_satisfied_by(self, requirement, candidate): # type: (Requirement, Candidate) -> bool diff --git a/tests/unit/resolution_resolvelib/test_requirement.py b/tests/unit/resolution_resolvelib/test_requirement.py index a03edb6f7c2..48c5f734704 100644 --- a/tests/unit/resolution_resolvelib/test_requirement.py +++ b/tests/unit/resolution_resolvelib/test_requirement.py @@ -58,7 +58,9 @@ def test_new_resolver_correct_number_of_matches(test_cases, factory): """Requirements should return the correct number of candidates""" for spec, _, match_count in test_cases: req = factory.make_requirement_from_spec(spec, comes_from=None) - matches = factory.find_candidates([req], Constraint.empty()) + matches = factory.find_candidates( + [req], Constraint.empty(), prefers_installed=False, + ) assert len(list(matches)) == match_count @@ -67,7 +69,10 @@ def test_new_resolver_candidates_match_requirement(test_cases, factory): """ for spec, _, _ in test_cases: req = factory.make_requirement_from_spec(spec, comes_from=None) - for c in factory.find_candidates([req], Constraint.empty()): + candidates = factory.find_candidates( + [req], Constraint.empty(), prefers_installed=False, + ) + for c in candidates: assert isinstance(c, Candidate) assert req.is_satisfied_by(c) From a270ca561695164e817eebe11c8129bebaf11f3e Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 29 Sep 2020 22:24:39 +0800 Subject: [PATCH 2580/3170] Mypy is wrong --- src/pip/_internal/resolution/resolvelib/found_candidates.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/resolution/resolvelib/found_candidates.py b/src/pip/_internal/resolution/resolvelib/found_candidates.py index 0ddcb1fcc8d..be50f52d1dd 100644 --- a/src/pip/_internal/resolution/resolvelib/found_candidates.py +++ b/src/pip/_internal/resolution/resolvelib/found_candidates.py @@ -1,4 +1,4 @@ -from pip._vendor.six.moves import collections_abc +from pip._vendor.six.moves import collections_abc # type: ignore from pip._internal.utils.typing import MYPY_CHECK_RUNNING From 01c9b6cf25cab66422c8182e66d0749cf5d7ac64 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 30 Sep 2020 15:09:59 +0800 Subject: [PATCH 2581/3170] Cache results and remove unused implementation --- .../resolution/resolvelib/found_candidates.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/found_candidates.py b/src/pip/_internal/resolution/resolvelib/found_candidates.py index be50f52d1dd..43591967f83 100644 --- a/src/pip/_internal/resolution/resolvelib/found_candidates.py +++ b/src/pip/_internal/resolution/resolvelib/found_candidates.py @@ -1,5 +1,6 @@ from pip._vendor.six.moves import collections_abc # type: ignore +from pip._internal.utils.compat import lru_cache from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: @@ -96,10 +97,10 @@ def __init__( def __getitem__(self, index): # type: (int) -> Candidate - for i, value in enumerate(self): - if index == i: - return value - raise IndexError(index) + # Implemented to satisfy the ABC check, This is not needed by the + # resolver, and should not be used by the provider either (for + # performance reasons). + raise NotImplementedError("don't do this") def __iter__(self): # type: () -> Iterator[Candidate] @@ -109,10 +110,12 @@ def __iter__(self): klass = _InstalledReplacesCandidatesIterator return klass(self._get_others, self._installed) + @lru_cache(maxsize=1) def __len__(self): # type: () -> int return sum(1 for _ in self) + @lru_cache(maxsize=1) def __bool__(self): # type: () -> bool if self._prefers_installed and self._installed: From 270e183718ec17598e345bd56027fa691dd13b5a Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 30 Sep 2020 15:55:49 +0800 Subject: [PATCH 2582/3170] News --- news/8023.feature.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 news/8023.feature.rst diff --git a/news/8023.feature.rst b/news/8023.feature.rst new file mode 100644 index 00000000000..c886e9a66c1 --- /dev/null +++ b/news/8023.feature.rst @@ -0,0 +1,2 @@ +New resolver: Avoid accessing indexes when the installed candidate is preferred +and considered good enough. From 6e3d56897b159212813136b0126f82737105b924 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 30 Sep 2020 21:36:15 +0800 Subject: [PATCH 2583/3170] Always return the installed version --- .../resolution/resolvelib/found_candidates.py | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/found_candidates.py b/src/pip/_internal/resolution/resolvelib/found_candidates.py index 43591967f83..c9b21727a54 100644 --- a/src/pip/_internal/resolution/resolvelib/found_candidates.py +++ b/src/pip/_internal/resolution/resolvelib/found_candidates.py @@ -66,10 +66,19 @@ def __next__(self): # type: () -> Candidate if self._others is None: self._others = self._get_others() - cand = next(self._others) - while cand.version in self._returned: + try: cand = next(self._others) - if self._installed and cand.version == self._installed.version: + while cand.version in self._returned: + cand = next(self._others) + if self._installed and cand.version == self._installed.version: + cand = self._installed + except StopIteration: + # Return the already-installed candidate as the last item if its + # version does not exist on the index. + if not self._installed: + raise + if self._installed.version in self._returned: + raise cand = self._installed self._returned.add(cand.version) return cand From d22775819bb70f6f321e839b4e3f3677c72f41fb Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 2 Oct 2020 00:12:28 +0800 Subject: [PATCH 2584/3170] Test for candidate ordering --- tests/functional/test_new_resolver.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 1dab8d47091..1718ab8a8b8 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -1020,3 +1020,29 @@ def test_new_resolver_no_deps_checks_requires_python(script): "{}.{}.{} not in '<2'".format(*sys.version_info[:3]) ) assert message in result.stderr + + +def test_new_resolver_prefers_installed_in_upgrade_if_latest(script): + create_basic_wheel_for_package(script, "pkg", "1") + local_pkg = create_test_package_with_setup(script, name="pkg", version="2") + + # Install the version that's not on the index. + script.pip( + "install", + "--use-feature=2020-resolver", + "--no-cache-dir", + "--no-index", + local_pkg, + ) + + # Now --upgrade should still pick the local version because it's "better". + script.pip( + "install", + "--use-feature=2020-resolver", + "--no-cache-dir", + "--no-index", + "--find-links", script.scratch_path, + "--upgrade", + "pkg", + ) + assert_installed(script, pkg="2") From 17d0086ea2b1dbff45ae8e2be88225fd366d67dd Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 1 Oct 2020 23:41:03 +0800 Subject: [PATCH 2585/3170] Do this all over again --- .../resolution/resolvelib/found_candidates.py | 111 ++++++------------ 1 file changed, 39 insertions(+), 72 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/found_candidates.py b/src/pip/_internal/resolution/resolvelib/found_candidates.py index c9b21727a54..49129046635 100644 --- a/src/pip/_internal/resolution/resolvelib/found_candidates.py +++ b/src/pip/_internal/resolution/resolvelib/found_candidates.py @@ -1,89 +1,51 @@ +import functools +import itertools + from pip._vendor.six.moves import collections_abc # type: ignore from pip._internal.utils.compat import lru_cache from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Callable, Iterator, Optional, Set + from typing import Any, Callable, Iterator, Optional, Set from pip._vendor.packaging.version import _BaseVersion from .base import Candidate -class _InstalledFirstCandidatesIterator(collections_abc.Iterator): - """Iterator for ``FoundCandidates``. +def _deduplicated_by_version(candidates): + # type: (Iterator[Candidate]) -> Iterator[Candidate] + returned = set() # type: Set[_BaseVersion] + for candidate in candidates: + if candidate.version in returned: + continue + returned.add(candidate.version) + yield candidate - This iterator is used when the resolver prefers to keep the version of an - already-installed package. The already-installed candidate is always - returned first. Candidates from index are accessed only when the resolver - wants them, and the already-installed version is excluded from them. - """ - def __init__( - self, - get_others, # type: Callable[[], Iterator[Candidate]] - installed, # type: Optional[Candidate] - ): - self._installed = installed - self._get_others = get_others - self._others = None # type: Optional[Iterator[Candidate]] - self._returned = set() # type: Set[_BaseVersion] - - def __next__(self): - # type: () -> Candidate - if self._installed and self._installed.version not in self._returned: - self._returned.add(self._installed.version) - return self._installed - if self._others is None: - self._others = self._get_others() - cand = next(self._others) - while cand.version in self._returned: - cand = next(self._others) - self._returned.add(cand.version) - return cand - - next = __next__ # XXX: Python 2. - - -class _InstalledReplacesCandidatesIterator(collections_abc.Iterator): + +def _replaces_sort_key(installed, candidate): + # type: (Candidate, Candidate) -> Any + return (candidate.version, candidate is installed) + + +def _insert_installed(installed, others): + # type: (Candidate, Iterator[Candidate]) -> Iterator[Candidate] """Iterator for ``FoundCandidates``. This iterator is used when the resolver prefers to upgrade an already-installed package. Candidates from index are returned in their normal ordering, except replaced when the version is already installed. + + The sort key prefers the installed candidate over candidates of the same + version from the index, so it is chosen on de-duplication. """ - def __init__( - self, - get_others, # type: Callable[[], Iterator[Candidate]] - installed, # type: Optional[Candidate] - ): - self._installed = installed - self._get_others = get_others - self._others = None # type: Optional[Iterator[Candidate]] - self._returned = set() # type: Set[_BaseVersion] - - def __next__(self): - # type: () -> Candidate - if self._others is None: - self._others = self._get_others() - try: - cand = next(self._others) - while cand.version in self._returned: - cand = next(self._others) - if self._installed and cand.version == self._installed.version: - cand = self._installed - except StopIteration: - # Return the already-installed candidate as the last item if its - # version does not exist on the index. - if not self._installed: - raise - if self._installed.version in self._returned: - raise - cand = self._installed - self._returned.add(cand.version) - return cand - - next = __next__ # XXX: Python 2. + candidates = sorted( + itertools.chain(others, [installed]), + key=functools.partial(_replaces_sort_key, installed), + reverse=True, + ) + return iter(candidates) class FoundCandidates(collections_abc.Sequence): @@ -106,22 +68,27 @@ def __init__( def __getitem__(self, index): # type: (int) -> Candidate - # Implemented to satisfy the ABC check, This is not needed by the + # Implemented to satisfy the ABC check. This is not needed by the # resolver, and should not be used by the provider either (for # performance reasons). raise NotImplementedError("don't do this") def __iter__(self): # type: () -> Iterator[Candidate] - if self._prefers_installed: - klass = _InstalledFirstCandidatesIterator + if not self._installed: + candidates = self._get_others() + elif self._prefers_installed: + candidates = itertools.chain([self._installed], self._get_others()) else: - klass = _InstalledReplacesCandidatesIterator - return klass(self._get_others, self._installed) + candidates = _insert_installed(self._installed, self._get_others()) + return _deduplicated_by_version(candidates) @lru_cache(maxsize=1) def __len__(self): # type: () -> int + # Implement to satisfy the ABC check and used in tests. This is not + # needed by the resolver, and should not be used by the provider either + # (for performance reasons). return sum(1 for _ in self) @lru_cache(maxsize=1) From 761433cee8ad0146bf38b2736a22c91d1dd63615 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 2 Oct 2020 19:55:05 +0800 Subject: [PATCH 2586/3170] Eliminate len() usage in tests --- .../_internal/resolution/resolvelib/found_candidates.py | 9 ++++----- tests/unit/resolution_resolvelib/test_requirement.py | 2 +- 2 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/found_candidates.py b/src/pip/_internal/resolution/resolvelib/found_candidates.py index 49129046635..db823224027 100644 --- a/src/pip/_internal/resolution/resolvelib/found_candidates.py +++ b/src/pip/_internal/resolution/resolvelib/found_candidates.py @@ -83,13 +83,12 @@ def __iter__(self): candidates = _insert_installed(self._installed, self._get_others()) return _deduplicated_by_version(candidates) - @lru_cache(maxsize=1) def __len__(self): # type: () -> int - # Implement to satisfy the ABC check and used in tests. This is not - # needed by the resolver, and should not be used by the provider either - # (for performance reasons). - return sum(1 for _ in self) + # Implemented to satisfy the ABC check. This is not needed by the + # resolver, and should not be used by the provider either (for + # performance reasons). + raise NotImplementedError("don't do this") @lru_cache(maxsize=1) def __bool__(self): diff --git a/tests/unit/resolution_resolvelib/test_requirement.py b/tests/unit/resolution_resolvelib/test_requirement.py index 48c5f734704..6149fd1aece 100644 --- a/tests/unit/resolution_resolvelib/test_requirement.py +++ b/tests/unit/resolution_resolvelib/test_requirement.py @@ -61,7 +61,7 @@ def test_new_resolver_correct_number_of_matches(test_cases, factory): matches = factory.find_candidates( [req], Constraint.empty(), prefers_installed=False, ) - assert len(list(matches)) == match_count + assert sum(1 for _ in matches) == match_count def test_new_resolver_candidates_match_requirement(test_cases, factory): From b921db84bdace474139477089ef1865a0217825f Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 2 Oct 2020 19:55:14 +0800 Subject: [PATCH 2587/3170] Improve sorting logic --- .../resolution/resolvelib/found_candidates.py | 20 +++++++++---------- 1 file changed, 9 insertions(+), 11 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/found_candidates.py b/src/pip/_internal/resolution/resolvelib/found_candidates.py index db823224027..a669e893670 100644 --- a/src/pip/_internal/resolution/resolvelib/found_candidates.py +++ b/src/pip/_internal/resolution/resolvelib/found_candidates.py @@ -1,5 +1,5 @@ -import functools import itertools +import operator from pip._vendor.six.moves import collections_abc # type: ignore @@ -7,7 +7,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Any, Callable, Iterator, Optional, Set + from typing import Callable, Iterator, Optional, Set from pip._vendor.packaging.version import _BaseVersion @@ -24,11 +24,6 @@ def _deduplicated_by_version(candidates): yield candidate -def _replaces_sort_key(installed, candidate): - # type: (Candidate, Candidate) -> Any - return (candidate.version, candidate is installed) - - def _insert_installed(installed, others): # type: (Candidate, Iterator[Candidate]) -> Iterator[Candidate] """Iterator for ``FoundCandidates``. @@ -37,12 +32,15 @@ def _insert_installed(installed, others): already-installed package. Candidates from index are returned in their normal ordering, except replaced when the version is already installed. - The sort key prefers the installed candidate over candidates of the same - version from the index, so it is chosen on de-duplication. + Since candidates from index are already sorted by reverse version order, + `sorted()` here would keep the ordering mostly intact, only shuffling the + already-installed candidate into the correct position. We put the already- + installed candidate in front of those from the index, so it's put in front + after sorting due to Python sorting's stableness guarentee. """ candidates = sorted( - itertools.chain(others, [installed]), - key=functools.partial(_replaces_sort_key, installed), + itertools.chain([installed], others), + key=operator.attrgetter("version"), reverse=True, ) return iter(candidates) From 6407f7ed857453c0117eec1d0934323511313623 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Date: Thu, 24 Sep 2020 02:00:24 +0530 Subject: [PATCH 2588/3170] Merge pull request #7859 from pradyunsg/docs/deep-dive-cli Architecture Documentation: CLI deep dive --- .../architecture/command-line-interface.rst | 57 +++++++++++++++++++ docs/html/development/architecture/index.rst | 1 + 2 files changed, 58 insertions(+) create mode 100644 docs/html/development/architecture/command-line-interface.rst diff --git a/docs/html/development/architecture/command-line-interface.rst b/docs/html/development/architecture/command-line-interface.rst new file mode 100644 index 00000000000..9bfa9119258 --- /dev/null +++ b/docs/html/development/architecture/command-line-interface.rst @@ -0,0 +1,57 @@ +====================== +Command Line Interface +====================== + +The ``pip._internal.cli`` package is responsible for processing and providing +pip's command line interface. This package handles: + +* CLI option definition and parsing +* autocompletion +* dispatching to the various commands +* utilities like progress bars and spinners + +.. note:: + + This section of the documentation is currently being written. pip + developers welcome your help to complete this documentation. If you're + interested in helping out, please let us know in the + `tracking issue <https://github.com/pypa/pip/issues/6831>`_. + + +.. _cli-overview: + +Overview +======== + +A ``ConfigOptionParser`` instance is used as the "main parser", +for parsing top level args. + +``Command`` then uses another ``ConfigOptionParser`` instance, to parse command-specific args. + +* TODO: How & where options are defined + (cmdoptions, command-specific files). + +* TODO: How & where arguments are processed. + (main_parser, command-specific parser) + +* TODO: How processed arguments are accessed. + (attributes on argument to ``Command.run()``) + +* TODO: How configuration and CLI "blend". + (implemented in ``ConfigOptionParser``) + +* TODO: progress bars and spinners + +* TODO: quirks / standard practices / broad ideas. + (avoiding lists in option def'n, special cased option value types, + ) + + +Future Refactoring Ideas +======================== + +* Change option definition to be a more declarative, consistent, static + data-structure, replacing the current ``partial(Option, ...)`` form +* Move progress bar and spinner to a ``cli.ui`` subpackage +* Move all ``Command`` classes into a ``cli.commands`` subpackage + (including base classes) diff --git a/docs/html/development/architecture/index.rst b/docs/html/development/architecture/index.rst index 417edf4e9e9..050c2fc45eb 100644 --- a/docs/html/development/architecture/index.rst +++ b/docs/html/development/architecture/index.rst @@ -26,6 +26,7 @@ Architecture of pip's internals anatomy configuration-files package-finding + command-line-interface upgrade-options From df554a9337c1e2d65fd096a4048bf34a4e868470 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Date: Mon, 14 Sep 2020 00:49:33 +0530 Subject: [PATCH 2589/3170] Merge pull request #8780 from eamanu/fix-8009 --- docs/html/reference/pip_install.rst | 5 ----- news/5e8c60c2-d540-4a25-af03-100d848acbc0.trivial | 0 2 files changed, 5 deletions(-) create mode 100644 news/5e8c60c2-d540-4a25-af03-100d848acbc0.trivial diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index 7be7bcd256b..fd962cd3658 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -550,11 +550,6 @@ response telling pip to simply use the cached item (and refresh the expiration timer) or it will return a whole new response which pip can then store in the cache. -When storing items in the cache, pip will respect the ``CacheControl`` header -if it exists, or it will fall back to the ``Expires`` header if that exists. -This allows pip to function as a browser would, and allows the index server -to communicate to pip how long it is reasonable to cache any particular item. - While this cache attempts to minimize network activity, it does not prevent network access altogether. If you want a local install solution that circumvents accessing PyPI, see :ref:`Installing from local packages`. diff --git a/news/5e8c60c2-d540-4a25-af03-100d848acbc0.trivial b/news/5e8c60c2-d540-4a25-af03-100d848acbc0.trivial new file mode 100644 index 00000000000..e69de29bb2d From f9a1761e9497a360b1edc67c0e16122954213c80 Mon Sep 17 00:00:00 2001 From: Xavier Fernandez <xav.fernandez@gmail.com> Date: Sun, 13 Sep 2020 15:08:09 +0200 Subject: [PATCH 2590/3170] Merge pull request #8795 from McSinyx/user-guide-fmt Reformat a few spots in user guide --- docs/html/user_guide.rst | 90 ++++++++++--------- ...3f7456-cc25-4df9-9518-4732b1e07fe5.trivial | 0 2 files changed, 46 insertions(+), 44 deletions(-) create mode 100644 news/093f7456-cc25-4df9-9518-4732b1e07fe5.trivial diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index a03ec164c44..2759bc61e5b 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -36,9 +36,7 @@ directly from distribution files. The most common scenario is to install from `PyPI`_ using :ref:`Requirement -Specifiers` - - :: +Specifiers` :: $ pip install SomePackage # latest version $ pip install SomePackage==1.0.4 # specific version @@ -103,9 +101,7 @@ Requirements Files ================== "Requirements files" are files containing a list of items to be -installed using :ref:`pip install` like so: - - :: +installed using :ref:`pip install` like so:: pip install -r requirements.txt @@ -191,9 +187,7 @@ contents is nearly identical to :ref:`Requirements Files`. There is one key difference: Including a package in a constraints file does not trigger installation of the package. -Use a constraints file like so: - - :: +Use a constraints file like so:: pip install -c constraints.txt @@ -791,16 +785,14 @@ Understanding your error message When you get a ``ResolutionImpossible`` error, you might see something like this: -:: - - pip install package_coffee==0.44.1 package_tea==4.3.0 - -:: +.. code-block:: console - Due to conflicting dependencies pip cannot install package_coffee and - package_tea: - - package_coffee depends on package_water<3.0.0,>=2.4.2 - - package_tea depends on package_water==2.3.1 + $ pip install package_coffee==0.44.1 package_tea==4.3.0 + ... + Due to conflicting dependencies pip cannot install + package_coffee and package_tea: + - package_coffee depends on package_water<3.0.0,>=2.4.2 + - package_tea depends on package_water==2.3.1 In this example, pip cannot install the packages you have requested, because they each depend on different versions of the same package @@ -819,27 +811,37 @@ commonly understood comparison operators to specify the required version However, Python packaging also supports some more complex ways for specifying package versions (e.g. ``~=`` or ``*``): -.. csv-table:: - :header: "Operator", "Description", "Example" - - ``>``, "Any version greater than the specified version", "``>3.1``: any - version greater than 3.1" - ``<``, "Any version less than the specified version", "``<3.1``: any version - less than ``3.1``" - ``<=``, "Any version less than or equal to the specified version", "``<=3.1``: - any version less than or equal to ``3.1``" - ``>=``, "Any version greater than or equal to the specified - version", "``>=3.1``: version ``3.1`` and greater" - ``==``, "Exactly the specified version", ``==3.1``: only version ``3.1`` - ``!=``, "Any version not equal to the specified version", "``!=3.1``: any - version other than ``3.1``" - ``~=``, "Any compatible release. Compatible releases are releases that are - within the same major or minor version, assuming the package author is using - semantic versioning.", "``~=3.1``: version ``3.1`` or later, but not version - ``4.0`` or later. ``~=3.1.2``: version ``3.1.2`` or later, but not - version ``3.2.0`` or later." - ``*``,Can be used at the end of a version number to represent "all", "``== 3. - 1.*``: any version that starts with ``3.1``. Equivalent to ``~=3.1.0``." ++----------+---------------------------------+--------------------------------+ +| Operator | Description | Example | ++==========+=================================+================================+ +| ``>`` | Any version greater than | ``>3.1``: any version | +| | the specified version. | greater than ``3.1``. | ++----------+---------------------------------+--------------------------------+ +| ``<`` | Any version less than | ``<3.1``: any version | +| | the specified version. | less than ``3.1``. | ++----------+---------------------------------+--------------------------------+ +| ``<=`` | Any version less than or | ``<=3.1``: any version | +| | equal to the specified version. | less than or equal to ``3.1``. | ++----------+---------------------------------+--------------------------------+ +| ``>=`` | Any version greater than or | ``>=3.1``: | +| | equal to the specified version. | version ``3.1`` and greater. | ++----------+---------------------------------+--------------------------------+ +| ``==`` | Exactly the specified version. | ``==3.1``: only ``3.1``. | ++----------+---------------------------------+--------------------------------+ +| ``!=`` | Any version not equal | ``!=3.1``: any version | +| | to the specified version. | other than ``3.1``. | ++----------+---------------------------------+--------------------------------+ +| ``~=`` | Any compatible release. | ``~=3.1``: version ``3.1`` | +| | Compatible releases are | or later, but not | +| | releases that are within the | version ``4.0`` or later. | +| | same major or minor version, | ``~=3.1.2``: version ``3.1.2`` | +| | assuming the package author | or later, but not | +| | is using semantic versioning. | version ``3.2.0`` or later. | ++----------+---------------------------------+--------------------------------+ +| ``*`` | Can be used at the end of | ``==3.1.*``: any version | +| | a version number to represent | that starts with ``3.1``. | +| | *all*, | Equivalent to ``~=3.1.0``. | ++----------+---------------------------------+--------------------------------+ The detailed specification of supported comparison operators can be found in :pep:`440`. @@ -1122,11 +1124,11 @@ How to test - If you use pip to install your software, try out the new resolver and let us know if it works for you with ``pip install``. Try: - - installing several packages simultaneously - - re-creating an environment using a ``requirements.txt`` file - - using ``pip install --force-reinstall`` to check whether - it does what you think it should - - using constraints files + - installing several packages simultaneously + - re-creating an environment using a ``requirements.txt`` file + - using ``pip install --force-reinstall`` to check whether + it does what you think it should + - using constraints files - If you have a build pipeline that depends on pip installing your dependencies for you, check that the new resolver does what you diff --git a/news/093f7456-cc25-4df9-9518-4732b1e07fe5.trivial b/news/093f7456-cc25-4df9-9518-4732b1e07fe5.trivial new file mode 100644 index 00000000000..e69de29bb2d From 71703136020b3e55cd024d70e7d6db40d108c7e0 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Date: Wed, 2 Sep 2020 19:34:31 +0530 Subject: [PATCH 2591/3170] Merge pull request #8807 from pypa/add-ux-docs Add ux docs to pip documentation --- docs/html/index.rst | 1 + docs/html/ux_research_design.rst | 81 ++++++++++++++++++++++++++++++++ news/8807.doc | 1 + 3 files changed, 83 insertions(+) create mode 100644 docs/html/ux_research_design.rst create mode 100644 news/8807.doc diff --git a/docs/html/index.rst b/docs/html/index.rst index ff41fc42b52..1418e7178a2 100644 --- a/docs/html/index.rst +++ b/docs/html/index.rst @@ -14,6 +14,7 @@ Please take a look at our documentation for how to install and use pip: user_guide reference/index development/index + ux_research_design news In 2020, we're working on improvements to the heart of pip. Please `learn more and take our survey`_ to help us do it right. diff --git a/docs/html/ux_research_design.rst b/docs/html/ux_research_design.rst new file mode 100644 index 00000000000..165b6949670 --- /dev/null +++ b/docs/html/ux_research_design.rst @@ -0,0 +1,81 @@ +==================== +UX Research & Design +==================== + +Over the course of 2020, the pip team has been working on improving pip's user +experience. + +Currently, our focus is on: + +1. `Understanding who uses pip`_ +2. `Understanding how pip compares to other package managers, and how pip supports other Python packaging tools`_ +3. `Understanding how pip's functionality is used, and how it could be improved`_ +4. `Understanding how pip's documentation is used, and how it could be improved`_ + +You can read the `overall plan`_ and the `mid-year update`_ to learn more about +our work. + +How to contribute +----------------- + +Participate in UX research +========================== + +It is important that we hear from pip users so that we can: + +- Understand how pip is currently used by the Python community +- Understand how pip users *need* pip to behave +- Understand how pip users *would like* pip to behave +- Understand pip's strengths and shortcomings +- Make useful design recommendations for improving pip + +If you are interested in participating in pip user research, please +`join pip's user panel`_. +You can `read more information about the user panel here`_. + +We are also looking for users to: + +- `Give us feedback about pip's new resolver`_ +- `Tell us how pip should handle conflicts with already installed packages when updating other packages`_ + +Report UX issues +================ + +If you believe that you have found a user experience bug in pip, or you have +ideas for how pip could be made better for all users, you please file an issue +on the `pip issue tracker`_. + +Work on UX issues +================= + +You can help improve pip's user experience by `working on UX issues`_. +Issues that are ideal for new contributors are marked with "good first issue". + +Test new features +================= + +You can help the team by testing new features as they are released to the +community. Currently, we are looking for users to +`test pip's new dependency resolver`_. + +Next steps +---------- + +In the coming months we will extend this documentation to include: + +1. Summaries of our user research, including recommendations for how to improve pip +2. Tools for the pip team to continue to practice user centered design (e.g. user personas, etc.) + +.. _Understanding who uses pip: https://github.com/pypa/pip/issues/8518 +.. _Understanding how pip compares to other package managers, and how pip supports other Python packaging tools: https://github.com/pypa/pip/issues/8515 +.. _Understanding how pip's functionality is used, and how it could be improved: https://github.com/pypa/pip/issues/8516 +.. _Understanding how pip's documentation is used, and how it could be improved: https://github.com/pypa/pip/issues/8517 +.. _overall plan: https://wiki.python.org/psf/Pip2020DonorFundedRoadmap +.. _mid-year update: http://pyfound.blogspot.com/2020/07/pip-team-midyear-report.html +.. _join pip's user panel: https://tools.simplysecure.org/survey/index.php?r=survey/index&sid=827389&lang=en +.. _read more information about the user panel here: https://bit.ly/pip-ux-studies +.. _Give us feedback about pip's new resolver: https://tools.simplysecure.org/survey/index.php?r=survey/index&sid=989272&lang=en +.. _Tell us how pip should handle conflicts with already installed packages when updating other packages: https://docs.google.com/forms/d/1KtejgZnK-6NPTmAJ-7aWox4iktcezQauW-Mh3gbnydQ/edit +.. _pip issue tracker: https://github.com/pypa/pip/issues/new +.. _working on UX issues: https://github.com/pypa/pip/issues?q=is%3Aissue+is%3Aopen+label%3A%22K%3A+UX%22 +.. _test pip's new dependency resolver: https://pip.pypa.io/en/latest/user_guide/#changes-to-the-pip-dependency-resolver-in-20-2-2020 diff --git a/news/8807.doc b/news/8807.doc new file mode 100644 index 00000000000..6ef1a123adb --- /dev/null +++ b/news/8807.doc @@ -0,0 +1 @@ +Add ux documentation From e832878f3f6d0cc7b272bd026cde56c76d750835 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Date: Thu, 17 Sep 2020 10:58:44 +0530 Subject: [PATCH 2592/3170] Merge pull request #8839 from uranusjr/new-resolver-hash-intersect --- news/8792.bugfix | 2 + news/8839.bugfix | 3 + .../_internal/resolution/resolvelib/base.py | 37 ++- .../resolution/resolvelib/factory.py | 13 +- .../resolution/resolvelib/provider.py | 7 +- .../resolution/resolvelib/resolver.py | 8 +- src/pip/_internal/utils/hashes.py | 20 +- tests/functional/test_new_resolver_hashes.py | 213 ++++++++++++++++++ .../resolution_resolvelib/test_requirement.py | 7 +- 9 files changed, 288 insertions(+), 22 deletions(-) create mode 100644 news/8792.bugfix create mode 100644 news/8839.bugfix create mode 100644 tests/functional/test_new_resolver_hashes.py diff --git a/news/8792.bugfix b/news/8792.bugfix new file mode 100644 index 00000000000..e83bdb09cfe --- /dev/null +++ b/news/8792.bugfix @@ -0,0 +1,2 @@ +New resolver: Pick up hash declarations in constraints files and use them to +filter available distributions. diff --git a/news/8839.bugfix b/news/8839.bugfix new file mode 100644 index 00000000000..987b801e932 --- /dev/null +++ b/news/8839.bugfix @@ -0,0 +1,3 @@ +New resolver: If a package appears multiple times in user specification with +different ``--hash`` options, only hashes that present in all specifications +should be allowed. diff --git a/src/pip/_internal/resolution/resolvelib/base.py b/src/pip/_internal/resolution/resolvelib/base.py index 9245747bf2b..7c09cd70b8d 100644 --- a/src/pip/_internal/resolution/resolvelib/base.py +++ b/src/pip/_internal/resolution/resolvelib/base.py @@ -1,5 +1,8 @@ +from pip._vendor.packaging.specifiers import SpecifierSet from pip._vendor.packaging.utils import canonicalize_name +from pip._internal.req.req_install import InstallRequirement +from pip._internal.utils.hashes import Hashes from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: @@ -8,7 +11,6 @@ from pip._vendor.packaging.version import _BaseVersion from pip._internal.models.link import Link - from pip._internal.req.req_install import InstallRequirement CandidateLookup = Tuple[ Optional["Candidate"], @@ -24,6 +26,39 @@ def format_name(project, extras): return "{}[{}]".format(project, ",".join(canonical_extras)) +class Constraint(object): + def __init__(self, specifier, hashes): + # type: (SpecifierSet, Hashes) -> None + self.specifier = specifier + self.hashes = hashes + + @classmethod + def empty(cls): + # type: () -> Constraint + return Constraint(SpecifierSet(), Hashes()) + + @classmethod + def from_ireq(cls, ireq): + # type: (InstallRequirement) -> Constraint + return Constraint(ireq.specifier, ireq.hashes(trust_internet=False)) + + def __nonzero__(self): + # type: () -> bool + return bool(self.specifier) or bool(self.hashes) + + def __bool__(self): + # type: () -> bool + return self.__nonzero__() + + def __and__(self, other): + # type: (InstallRequirement) -> Constraint + if not isinstance(other, InstallRequirement): + return NotImplemented + specifier = self.specifier & other.specifier + hashes = self.hashes & other.hashes(trust_internet=False) + return Constraint(specifier, hashes) + + class Requirement(object): @property def name(self): diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index dab23aa09d1..172f054fa72 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -22,6 +22,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.virtualenv import running_under_virtualenv +from .base import Constraint from .candidates import ( AlreadyInstalledCandidate, EditableCandidate, @@ -154,6 +155,7 @@ def _iter_found_candidates( self, ireqs, # type: Sequence[InstallRequirement] specifier, # type: SpecifierSet + hashes, # type: Hashes ): # type: (...) -> Iterable[Candidate] if not ireqs: @@ -166,11 +168,10 @@ def _iter_found_candidates( template = ireqs[0] name = canonicalize_name(template.req.name) - hashes = Hashes() extras = frozenset() # type: FrozenSet[str] for ireq in ireqs: specifier &= ireq.req.specifier - hashes |= ireq.hashes(trust_internet=False) + hashes &= ireq.hashes(trust_internet=False) extras |= frozenset(ireq.extras) # We use this to ensure that we only yield a single candidate for @@ -220,7 +221,7 @@ def _iter_found_candidates( return six.itervalues(candidates) def find_candidates(self, requirements, constraint): - # type: (Sequence[Requirement], SpecifierSet) -> Iterable[Candidate] + # type: (Sequence[Requirement], Constraint) -> Iterable[Candidate] explicit_candidates = set() # type: Set[Candidate] ireqs = [] # type: List[InstallRequirement] for req in requirements: @@ -233,7 +234,11 @@ def find_candidates(self, requirements, constraint): # If none of the requirements want an explicit candidate, we can ask # the finder for candidates. if not explicit_candidates: - return self._iter_found_candidates(ireqs, constraint) + return self._iter_found_candidates( + ireqs, + constraint.specifier, + constraint.hashes, + ) if constraint: name = explicit_candidates.pop().name diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index b2eb9d06ea5..80577a61c58 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -1,8 +1,9 @@ -from pip._vendor.packaging.specifiers import SpecifierSet from pip._vendor.resolvelib.providers import AbstractProvider from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from .base import Constraint + if MYPY_CHECK_RUNNING: from typing import ( Any, @@ -41,7 +42,7 @@ class PipProvider(AbstractProvider): def __init__( self, factory, # type: Factory - constraints, # type: Dict[str, SpecifierSet] + constraints, # type: Dict[str, Constraint] ignore_dependencies, # type: bool upgrade_strategy, # type: str user_requested, # type: Set[str] @@ -134,7 +135,7 @@ def find_matches(self, requirements): if not requirements: return [] constraint = self._constraints.get( - requirements[0].name, SpecifierSet(), + requirements[0].name, Constraint.empty(), ) candidates = self._factory.find_candidates(requirements, constraint) return reversed(self._sort_matches(candidates)) diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index aecddb1138c..449cfea287a 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -14,12 +14,12 @@ from pip._internal.utils.misc import dist_is_editable from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from .base import Constraint from .factory import Factory if MYPY_CHECK_RUNNING: from typing import Dict, List, Optional, Set, Tuple - from pip._vendor.packaging.specifiers import SpecifierSet from pip._vendor.resolvelib.resolvers import Result from pip._vendor.resolvelib.structs import Graph @@ -81,7 +81,7 @@ def __init__( def resolve(self, root_reqs, check_supported_wheels): # type: (List[InstallRequirement], bool) -> RequirementSet - constraints = {} # type: Dict[str, SpecifierSet] + constraints = {} # type: Dict[str, Constraint] user_requested = set() # type: Set[str] requirements = [] for req in root_reqs: @@ -94,9 +94,9 @@ def resolve(self, root_reqs, check_supported_wheels): continue name = canonicalize_name(req.name) if name in constraints: - constraints[name] = constraints[name] & req.specifier + constraints[name] &= req else: - constraints[name] = req.specifier + constraints[name] = Constraint.from_ireq(req) else: if req.user_supplied and req.name: user_requested.add(canonicalize_name(req.name)) diff --git a/src/pip/_internal/utils/hashes.py b/src/pip/_internal/utils/hashes.py index d1b062fedf6..d9f74a64083 100644 --- a/src/pip/_internal/utils/hashes.py +++ b/src/pip/_internal/utils/hashes.py @@ -46,16 +46,24 @@ def __init__(self, hashes=None): """ self._allowed = {} if hashes is None else hashes - def __or__(self, other): + def __and__(self, other): # type: (Hashes) -> Hashes if not isinstance(other, Hashes): return NotImplemented - new = self._allowed.copy() + + # If either of the Hashes object is entirely empty (i.e. no hash + # specified at all), all hashes from the other object are allowed. + if not other: + return self + if not self: + return other + + # Otherwise only hashes that present in both objects are allowed. + new = {} for alg, values in iteritems(other._allowed): - try: - new[alg] += values - except KeyError: - new[alg] = values + if alg not in self._allowed: + continue + new[alg] = [v for v in values if v in self._allowed[alg]] return Hashes(new) @property diff --git a/tests/functional/test_new_resolver_hashes.py b/tests/functional/test_new_resolver_hashes.py new file mode 100644 index 00000000000..4b13ebc307d --- /dev/null +++ b/tests/functional/test_new_resolver_hashes.py @@ -0,0 +1,213 @@ +import collections +import hashlib + +import pytest + +from pip._internal.utils.urls import path_to_url +from tests.lib import ( + create_basic_sdist_for_package, + create_basic_wheel_for_package, +) + +_FindLinks = collections.namedtuple( + "_FindLinks", "index_html sdist_hash wheel_hash", +) + + +def _create_find_links(script): + sdist_path = create_basic_sdist_for_package(script, "base", "0.1.0") + wheel_path = create_basic_wheel_for_package(script, "base", "0.1.0") + + sdist_hash = hashlib.sha256(sdist_path.read_bytes()).hexdigest() + wheel_hash = hashlib.sha256(wheel_path.read_bytes()).hexdigest() + + index_html = script.scratch_path / "index.html" + index_html.write_text( + """ + <a href="{sdist_url}#sha256={sdist_hash}">{sdist_path.stem}</a> + <a href="{wheel_url}#sha256={wheel_hash}">{wheel_path.stem}</a> + """.format( + sdist_url=path_to_url(sdist_path), + sdist_hash=sdist_hash, + sdist_path=sdist_path, + wheel_url=path_to_url(wheel_path), + wheel_hash=wheel_hash, + wheel_path=wheel_path, + ) + ) + + return _FindLinks(index_html, sdist_hash, wheel_hash) + + +@pytest.mark.parametrize( + "requirements_template, message", + [ + ( + """ + base==0.1.0 --hash=sha256:{sdist_hash} --hash=sha256:{wheel_hash} + base==0.1.0 --hash=sha256:{sdist_hash} --hash=sha256:{wheel_hash} + """, + "Checked 2 links for project {name!r} against 2 hashes " + "(2 matches, 0 no digest): discarding no candidates", + ), + ( + # Different hash lists are intersected. + """ + base==0.1.0 --hash=sha256:{sdist_hash} --hash=sha256:{wheel_hash} + base==0.1.0 --hash=sha256:{sdist_hash} + """, + "Checked 2 links for project {name!r} against 1 hashes " + "(1 matches, 0 no digest): discarding 1 non-matches", + ), + ], + ids=["identical", "intersect"], +) +def test_new_resolver_hash_intersect(script, requirements_template, message): + find_links = _create_find_links(script) + + requirements_txt = script.scratch_path / "requirements.txt" + requirements_txt.write_text( + requirements_template.format( + sdist_hash=find_links.sdist_hash, + wheel_hash=find_links.wheel_hash, + ), + ) + + result = script.pip( + "install", + "--use-feature=2020-resolver", + "--no-cache-dir", + "--no-deps", + "--no-index", + "--find-links", find_links.index_html, + "--verbose", + "--requirement", requirements_txt, + ) + + assert message.format(name=u"base") in result.stdout, str(result) + + +def test_new_resolver_hash_intersect_from_constraint(script): + find_links = _create_find_links(script) + + constraints_txt = script.scratch_path / "constraints.txt" + constraints_txt.write_text( + "base==0.1.0 --hash=sha256:{sdist_hash}".format( + sdist_hash=find_links.sdist_hash, + ), + ) + requirements_txt = script.scratch_path / "requirements.txt" + requirements_txt.write_text( + """ + base==0.1.0 --hash=sha256:{sdist_hash} --hash=sha256:{wheel_hash} + """.format( + sdist_hash=find_links.sdist_hash, + wheel_hash=find_links.wheel_hash, + ), + ) + + result = script.pip( + "install", + "--use-feature=2020-resolver", + "--no-cache-dir", + "--no-deps", + "--no-index", + "--find-links", find_links.index_html, + "--verbose", + "--constraint", constraints_txt, + "--requirement", requirements_txt, + ) + + message = ( + "Checked 2 links for project {name!r} against 1 hashes " + "(1 matches, 0 no digest): discarding 1 non-matches" + ).format(name=u"base") + assert message in result.stdout, str(result) + + +@pytest.mark.parametrize( + "requirements_template, constraints_template", + [ + ( + """ + base==0.1.0 --hash=sha256:{sdist_hash} + base==0.1.0 --hash=sha256:{wheel_hash} + """, + "", + ), + ( + "base==0.1.0 --hash=sha256:{sdist_hash}", + "base==0.1.0 --hash=sha256:{wheel_hash}", + ), + ], + ids=["both-requirements", "one-each"], +) +def test_new_resolver_hash_intersect_empty( + script, requirements_template, constraints_template, +): + find_links = _create_find_links(script) + + constraints_txt = script.scratch_path / "constraints.txt" + constraints_txt.write_text( + constraints_template.format( + sdist_hash=find_links.sdist_hash, + wheel_hash=find_links.wheel_hash, + ), + ) + + requirements_txt = script.scratch_path / "requirements.txt" + requirements_txt.write_text( + requirements_template.format( + sdist_hash=find_links.sdist_hash, + wheel_hash=find_links.wheel_hash, + ), + ) + + result = script.pip( + "install", + "--use-feature=2020-resolver", + "--no-cache-dir", + "--no-deps", + "--no-index", + "--find-links", find_links.index_html, + "--constraint", constraints_txt, + "--requirement", requirements_txt, + expect_error=True, + ) + + assert ( + "THESE PACKAGES DO NOT MATCH THE HASHES FROM THE REQUIREMENTS FILE." + ) in result.stderr, str(result) + + +def test_new_resolver_hash_intersect_empty_from_constraint(script): + find_links = _create_find_links(script) + + constraints_txt = script.scratch_path / "constraints.txt" + constraints_txt.write_text( + """ + base==0.1.0 --hash=sha256:{sdist_hash} + base==0.1.0 --hash=sha256:{wheel_hash} + """.format( + sdist_hash=find_links.sdist_hash, + wheel_hash=find_links.wheel_hash, + ), + ) + + result = script.pip( + "install", + "--use-feature=2020-resolver", + "--no-cache-dir", + "--no-deps", + "--no-index", + "--find-links", find_links.index_html, + "--constraint", constraints_txt, + "base==0.1.0", + expect_error=True, + ) + + message = ( + "Hashes are required in --require-hashes mode, but they are missing " + "from some requirements." + ) + assert message in result.stderr, str(result) diff --git a/tests/unit/resolution_resolvelib/test_requirement.py b/tests/unit/resolution_resolvelib/test_requirement.py index 21de3df4a4f..a03edb6f7c2 100644 --- a/tests/unit/resolution_resolvelib/test_requirement.py +++ b/tests/unit/resolution_resolvelib/test_requirement.py @@ -1,8 +1,7 @@ import pytest -from pip._vendor.packaging.specifiers import SpecifierSet from pip._vendor.resolvelib import BaseReporter, Resolver -from pip._internal.resolution.resolvelib.base import Candidate +from pip._internal.resolution.resolvelib.base import Candidate, Constraint from pip._internal.utils.urls import path_to_url # NOTE: All tests are prefixed `test_rlr` (for "test resolvelib resolver"). @@ -59,7 +58,7 @@ def test_new_resolver_correct_number_of_matches(test_cases, factory): """Requirements should return the correct number of candidates""" for spec, _, match_count in test_cases: req = factory.make_requirement_from_spec(spec, comes_from=None) - matches = factory.find_candidates([req], SpecifierSet()) + matches = factory.find_candidates([req], Constraint.empty()) assert len(list(matches)) == match_count @@ -68,7 +67,7 @@ def test_new_resolver_candidates_match_requirement(test_cases, factory): """ for spec, _, _ in test_cases: req = factory.make_requirement_from_spec(spec, comes_from=None) - for c in factory.find_candidates([req], SpecifierSet()): + for c in factory.find_candidates([req], Constraint.empty()): assert isinstance(c, Candidate) assert req.is_satisfied_by(c) From e7e62d8f753e4254d8650c34f8ead0dc9df053d9 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Date: Mon, 21 Sep 2020 02:26:17 +0530 Subject: [PATCH 2593/3170] Merge pull request #8873 from hugovk/docs-3-months-deprecation --- docs/html/development/release-process.rst | 18 ++++++++++++------ news/8417.removal | 1 + 2 files changed, 13 insertions(+), 6 deletions(-) create mode 100644 news/8417.removal diff --git a/docs/html/development/release-process.rst b/docs/html/development/release-process.rst index 44197955e8e..ebba1325864 100644 --- a/docs/html/development/release-process.rst +++ b/docs/html/development/release-process.rst @@ -16,7 +16,7 @@ with version numbers. Our release months are January, April, July, October. The release date within that month will be up to the release manager for that release. If there are no changes, then that release month is skipped and the next release will be -3 month later. +3 months later. The release manager may, at their discretion, choose whether or not there will be a pre-release period for a release, and if there is may extend that @@ -38,11 +38,17 @@ Deprecation Policy Any change to pip that removes or significantly alters user-visible behavior that is described in the pip documentation will be deprecated for a minimum of -6 months before the change occurs. Deprecation will take the form of a warning -being issued by pip when the feature is used. Longer deprecation periods, or -deprecation warnings for behavior changes that would not normally be covered by -this policy, are also possible depending on circumstances, but this is at the -discretion of the pip developers. +6 months before the change occurs. + +Certain changes may be fast tracked and have a deprecation period of 3 months. +This requires at least two members of the pip team to be in favor of doing so, +and no pip maintainers opposing. + +Deprecation will take the form of a warning being issued by pip when the +feature is used. Longer deprecation periods, or deprecation warnings for +behavior changes that would not normally be covered by this policy, are also +possible depending on circumstances, but this is at the discretion of the pip +maintainers. Note that the documentation is the sole reference for what counts as agreed behavior. If something isn't explicitly mentioned in the documentation, it can diff --git a/news/8417.removal b/news/8417.removal new file mode 100644 index 00000000000..8f280b535c3 --- /dev/null +++ b/news/8417.removal @@ -0,0 +1 @@ +Document that certain removals can be fast tracked. From 314de5a3b4181fb7c367450b8e7da98043ccb0a2 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Date: Sun, 27 Sep 2020 19:39:41 +0530 Subject: [PATCH 2594/3170] Merge pull request #8912 from uranusjr/cache-found-candidates --- news/8905.feature | 3 ++ src/pip/_internal/index/collector.py | 34 +++++++---------------- src/pip/_internal/index/package_finder.py | 2 ++ src/pip/_internal/utils/compat.py | 24 +++++++++++++++- 4 files changed, 38 insertions(+), 25 deletions(-) create mode 100644 news/8905.feature diff --git a/news/8905.feature b/news/8905.feature new file mode 100644 index 00000000000..5d27d40c2be --- /dev/null +++ b/news/8905.feature @@ -0,0 +1,3 @@ +Cache package listings on index packages so they are guarenteed to stay stable +during a pip command session. This also improves performance when a index page +is accessed multiple times during the command session. diff --git a/src/pip/_internal/index/collector.py b/src/pip/_internal/index/collector.py index 6c35fc66076..ef2100f8954 100644 --- a/src/pip/_internal/index/collector.py +++ b/src/pip/_internal/index/collector.py @@ -21,6 +21,7 @@ from pip._internal.models.link import Link from pip._internal.models.search_scope import SearchScope from pip._internal.network.utils import raise_for_status +from pip._internal.utils.compat import lru_cache from pip._internal.utils.filetypes import ARCHIVE_EXTENSIONS from pip._internal.utils.misc import pairwise, redact_auth_from_url from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -30,8 +31,14 @@ if MYPY_CHECK_RUNNING: from optparse import Values from typing import ( - Callable, Iterable, List, MutableMapping, Optional, - Protocol, Sequence, Tuple, TypeVar, Union, + Callable, + Iterable, + List, + MutableMapping, + Optional, + Sequence, + Tuple, + Union, ) import xml.etree.ElementTree @@ -42,31 +49,10 @@ HTMLElement = xml.etree.ElementTree.Element ResponseHeaders = MutableMapping[str, str] - # Used in the @lru_cache polyfill. - F = TypeVar('F') - - class LruCache(Protocol): - def __call__(self, maxsize=None): - # type: (Optional[int]) -> Callable[[F], F] - raise NotImplementedError - logger = logging.getLogger(__name__) -# Fallback to noop_lru_cache in Python 2 -# TODO: this can be removed when python 2 support is dropped! -def noop_lru_cache(maxsize=None): - # type: (Optional[int]) -> Callable[[F], F] - def _wrapper(f): - # type: (F) -> F - return f - return _wrapper - - -_lru_cache = getattr(functools, "lru_cache", noop_lru_cache) # type: LruCache - - def _match_vcs_scheme(url): # type: (str) -> Optional[str] """Look for VCS schemes in the URL. @@ -336,7 +322,7 @@ def with_cached_html_pages( `page` has `page.cache_link_parsing == False`. """ - @_lru_cache(maxsize=None) + @lru_cache(maxsize=None) def wrapper(cacheable_page): # type: (CacheablePageContent) -> List[Link] return list(fn(cacheable_page.page)) diff --git a/src/pip/_internal/index/package_finder.py b/src/pip/_internal/index/package_finder.py index 84115783ab8..8ceccee6c92 100644 --- a/src/pip/_internal/index/package_finder.py +++ b/src/pip/_internal/index/package_finder.py @@ -25,6 +25,7 @@ from pip._internal.models.selection_prefs import SelectionPreferences from pip._internal.models.target_python import TargetPython from pip._internal.models.wheel import Wheel +from pip._internal.utils.compat import lru_cache from pip._internal.utils.filetypes import WHEEL_EXTENSION from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import build_netloc @@ -801,6 +802,7 @@ def process_project_url(self, project_url, link_evaluator): return package_links + @lru_cache(maxsize=None) def find_all_candidates(self, project_name): # type: (str) -> List[InstallationCandidate] """Find all available InstallationCandidate for project_name diff --git a/src/pip/_internal/utils/compat.py b/src/pip/_internal/utils/compat.py index 89c5169af4e..9a2bb7800f9 100644 --- a/src/pip/_internal/utils/compat.py +++ b/src/pip/_internal/utils/compat.py @@ -7,6 +7,7 @@ from __future__ import absolute_import, division import codecs +import functools import locale import logging import os @@ -18,7 +19,15 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Optional, Text, Tuple, Union + from typing import Callable, Optional, Protocol, Text, Tuple, TypeVar, Union + + # Used in the @lru_cache polyfill. + F = TypeVar('F') + + class LruCache(Protocol): + def __call__(self, maxsize=None): + # type: (Optional[int]) -> Callable[[F], F] + raise NotImplementedError try: import ipaddress @@ -269,3 +278,16 @@ def ioctl_GWINSZ(fd): if not cr: cr = (os.environ.get('LINES', 25), os.environ.get('COLUMNS', 80)) return int(cr[1]), int(cr[0]) + + +# Fallback to noop_lru_cache in Python 2 +# TODO: this can be removed when python 2 support is dropped! +def noop_lru_cache(maxsize=None): + # type: (Optional[int]) -> Callable[[F], F] + def _wrapper(f): + # type: (F) -> F + return f + return _wrapper + + +lru_cache = getattr(functools, "lru_cache", noop_lru_cache) # type: LruCache From ae4d27179f564f184480ea88bd397f147b3dcb8a Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Date: Tue, 29 Sep 2020 15:04:51 +0530 Subject: [PATCH 2595/3170] Merge pull request #8924 from uranusjr/new-resolver-try-user-requested-combinations-first --- news/8924.feature | 2 ++ .../_internal/resolution/resolvelib/provider.py | 17 ++++++++++++----- 2 files changed, 14 insertions(+), 5 deletions(-) create mode 100644 news/8924.feature diff --git a/news/8924.feature b/news/8924.feature new file mode 100644 index 00000000000..c607aa0d06b --- /dev/null +++ b/news/8924.feature @@ -0,0 +1,2 @@ +New resolver: Tweak resolution logic to improve user experience when +user-supplied requirements conflict. diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index 80577a61c58..19c4d543f52 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -52,7 +52,7 @@ def __init__( self._constraints = constraints self._ignore_dependencies = ignore_dependencies self._upgrade_strategy = upgrade_strategy - self.user_requested = user_requested + self._user_requested = user_requested def _sort_matches(self, matches): # type: (Iterable[Candidate]) -> Sequence[Candidate] @@ -93,7 +93,7 @@ def _eligible_for_upgrade(name): if self._upgrade_strategy == "eager": return True elif self._upgrade_strategy == "only-if-needed": - return (name in self.user_requested) + return (name in self._user_requested) return False def sort_key(c): @@ -124,11 +124,18 @@ def get_preference( self, resolution, # type: Optional[Candidate] candidates, # type: Sequence[Candidate] - information # type: Sequence[Tuple[Requirement, Candidate]] + information # type: Sequence[Tuple[Requirement, Optional[Candidate]]] ): # type: (...) -> Any - # Use the "usual" value for now - return len(candidates) + """Return a sort key to determine what dependency to look next. + + A smaller value makes a dependency higher priority. We put direct + (user-requested) dependencies first since they may contain useful + user-specified version ranges. Users tend to expect us to catch + problems in them early as well. + """ + transitive = all(parent is not None for _, parent in information) + return (transitive, len(candidates)) def find_matches(self, requirements): # type: (Sequence[Requirement]) -> Iterable[Candidate] From ff5a9b5f6fbc7f9473bbb42ab14125e95bb090a8 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Date: Mon, 28 Sep 2020 14:38:25 +0530 Subject: [PATCH 2596/3170] Merge pull request #8926 from uranusjr/dont-crash-on-check Don't crash on 'check' when METADATA is missing --- news/8676.feature | 2 ++ src/pip/_internal/operations/check.py | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) create mode 100644 news/8676.feature diff --git a/news/8676.feature b/news/8676.feature new file mode 100644 index 00000000000..f8da963f6ed --- /dev/null +++ b/news/8676.feature @@ -0,0 +1,2 @@ +Improve error message friendliness when an environment has packages with +corrupted metadata. diff --git a/src/pip/_internal/operations/check.py b/src/pip/_internal/operations/check.py index 5714915bcb2..0d5963295d2 100644 --- a/src/pip/_internal/operations/check.py +++ b/src/pip/_internal/operations/check.py @@ -48,8 +48,8 @@ def create_package_set_from_installed(**kwargs): name = canonicalize_name(dist.project_name) try: package_set[name] = PackageDetails(dist.version, dist.requires()) - except RequirementParseError as e: - # Don't crash on broken metadata + except (OSError, RequirementParseError) as e: + # Don't crash on unreadable or broken metadata logger.warning("Error parsing requirements for %s: %s", name, e) problems = True return package_set, problems From 74f48cf8fc8ac5810e8c386f6b618e780405c031 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Date: Mon, 28 Sep 2020 14:47:48 +0530 Subject: [PATCH 2597/3170] Merge pull request #8927 from hugovk/document-python-support-policy Document Python support policy --- docs/html/development/release-process.rst | 8 +++++++- news/8927.removal | 1 + 2 files changed, 8 insertions(+), 1 deletion(-) create mode 100644 news/8927.removal diff --git a/docs/html/development/release-process.rst b/docs/html/development/release-process.rst index ebba1325864..1c6889ef01c 100644 --- a/docs/html/development/release-process.rst +++ b/docs/html/development/release-process.rst @@ -65,7 +65,7 @@ their merits. ``pip._internal.utils.deprecation.deprecated``. The function is not a part of pip's public API. -Python 2 support +Python 2 Support ---------------- pip will continue to ensure that it runs on Python 2.7 after the CPython 2.7 @@ -79,6 +79,12 @@ only bugs will be considered, and merged (subject to normal review processes). Note that there may be delays due to the lack of developer resources for reviewing such pull requests. +Python Support Policy +--------------------- + +In general, a given Python version is supported until its usage on PyPI falls below 5%. +This is at the maintainers' discretion, in case extraordinary circumstances arise. + .. _`Feature Flags`: Feature Flags diff --git a/news/8927.removal b/news/8927.removal new file mode 100644 index 00000000000..0032fa5f29d --- /dev/null +++ b/news/8927.removal @@ -0,0 +1 @@ +Document that Python versions are generally supported until PyPI usage falls below 5%. From c8533f0f60e5c3d722fa414b350c1a6d452da880 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Date: Tue, 29 Sep 2020 15:05:23 +0530 Subject: [PATCH 2598/3170] Merge pull request #8933 from brainwane/put-key-links-on-front-page --- docs/html/index.rst | 3 ++- news/a2fa2e68-01bf-11eb-a0b1-4fe8cb1f9dcf.trivial | 0 2 files changed, 2 insertions(+), 1 deletion(-) create mode 100644 news/a2fa2e68-01bf-11eb-a0b1-4fe8cb1f9dcf.trivial diff --git a/docs/html/index.rst b/docs/html/index.rst index 1418e7178a2..0f4f4b7dc19 100644 --- a/docs/html/index.rst +++ b/docs/html/index.rst @@ -17,7 +17,7 @@ Please take a look at our documentation for how to install and use pip: ux_research_design news -In 2020, we're working on improvements to the heart of pip. Please `learn more and take our survey`_ to help us do it right. +In 2020, we're working on improvements to the heart of pip: :ref:`Resolver changes 2020`. Please `learn more and take our survey`_ to help us do it right, and `join our user experience surveys pool`_. If you find bugs, need help, or want to talk to the developers, please use our mailing lists or chat rooms: @@ -40,6 +40,7 @@ rooms, and mailing lists is expected to follow the `PyPA Code of Conduct`_. .. _package installer: https://packaging.python.org/guides/tool-recommendations/ .. _Python Package Index: https://pypi.org +.. _join our user experience surveys pool: ux_research_design .. _learn more and take our survey: https://pyfound.blogspot.com/2020/03/new-pip-resolver-to-roll-out-this-year.html .. _Installation: https://pip.pypa.io/en/stable/installing.html .. _Documentation: https://pip.pypa.io/en/stable/ diff --git a/news/a2fa2e68-01bf-11eb-a0b1-4fe8cb1f9dcf.trivial b/news/a2fa2e68-01bf-11eb-a0b1-4fe8cb1f9dcf.trivial new file mode 100644 index 00000000000..e69de29bb2d From 0a637fa89ac2bfafadc0b924adb63925822c569b Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Date: Tue, 6 Oct 2020 18:56:56 +0530 Subject: [PATCH 2599/3170] Merge pull request #8942 from brainwane/docs-8661 docs: Add how to default to new resolver --- docs/html/user_guide.rst | 7 +++++++ news/bc7f9ea0-030d-11eb-92cb-6b2b625d02fc.trivial | 0 2 files changed, 7 insertions(+) create mode 100644 news/bc7f9ea0-030d-11eb-92cb-6b2b625d02fc.trivial diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 2759bc61e5b..77ffcecdd98 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -1133,6 +1133,12 @@ How to test - If you have a build pipeline that depends on pip installing your dependencies for you, check that the new resolver does what you need. + + - If you'd like pip to default to using the new resolver, run ``pip + config set global.use-feature 2020-resolver`` (for more on that + and the alternate ``PIP_USE_FEATURE`` environment variable + option, see `issue 8661`_). + - Run your project’s CI (test suite, build process, etc.) using the new resolver, and let us know of any issues. - If you have encountered resolver issues with pip in the past, @@ -1259,6 +1265,7 @@ announcements on the `low-traffic packaging announcements list`_ and .. _freeze: https://pip.pypa.io/en/latest/reference/pip_freeze/ .. _resolver testing survey: https://tools.simplysecure.org/survey/index.php?r=survey/index&sid=989272&lang=en +.. _issue 8661: https://github.com/pypa/pip/issues/8661 .. _our announcement on the PSF blog: http://pyfound.blogspot.com/2020/03/new-pip-resolver-to-roll-out-this-year.html .. _tensorflow: https://pypi.org/project/tensorflow/ .. _low-traffic packaging announcements list: https://mail.python.org/mailman3/lists/pypi-announce.python.org/ diff --git a/news/bc7f9ea0-030d-11eb-92cb-6b2b625d02fc.trivial b/news/bc7f9ea0-030d-11eb-92cb-6b2b625d02fc.trivial new file mode 100644 index 00000000000..e69de29bb2d From 4aec7e81c933e8d077804e10f89be135e02cd4c6 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Date: Wed, 2 Sep 2020 20:09:32 +0530 Subject: [PATCH 2600/3170] Merge pull request #8758 from uranusjr/new-resolver-requires-python-when-no-deps --- news/8758.bugfix | 2 ++ .../resolution/resolvelib/candidates.py | 22 +++++-------- tests/functional/test_new_resolver.py | 32 +++++++++++++++++++ 3 files changed, 42 insertions(+), 14 deletions(-) create mode 100644 news/8758.bugfix diff --git a/news/8758.bugfix b/news/8758.bugfix new file mode 100644 index 00000000000..9f44b7e47a4 --- /dev/null +++ b/news/8758.bugfix @@ -0,0 +1,2 @@ +New resolver: Correctly respect ``Requires-Python`` metadata to reject +incompatible packages in ``--no-deps`` mode. diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 46cc7e7a236..8b39d2dcbb8 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -261,31 +261,25 @@ def dist(self): self._fetch_metadata() return self._dist - def _get_requires_python_specifier(self): - # type: () -> Optional[SpecifierSet] + def _get_requires_python_dependency(self): + # type: () -> Optional[Requirement] requires_python = get_requires_python(self.dist) if requires_python is None: return None try: spec = SpecifierSet(requires_python) except InvalidSpecifier as e: - logger.warning( - "Package %r has an invalid Requires-Python: %s", self.name, e, - ) + message = "Package %r has an invalid Requires-Python: %s" + logger.warning(message, self.name, e) return None - return spec + return self._factory.make_requires_python_requirement(spec) def iter_dependencies(self, with_requires): # type: (bool) -> Iterable[Optional[Requirement]] - if not with_requires: - return - for r in self.dist.requires(): + requires = self.dist.requires() if with_requires else () + for r in requires: yield self._factory.make_requirement_from_spec(str(r), self._ireq) - python_dep = self._factory.make_requires_python_requirement( - self._get_requires_python_specifier(), - ) - if python_dep: - yield python_dep + yield self._get_requires_python_dependency() def get_install_requirement(self): # type: () -> Optional[InstallRequirement] diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 46e32ddd3d7..1dab8d47091 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -988,3 +988,35 @@ def test_new_resolver_local_and_req(script): source_dir, "pkg!=0.1.0", expect_error=True, ) + + +def test_new_resolver_no_deps_checks_requires_python(script): + create_basic_wheel_for_package( + script, + "base", + "0.1.0", + depends=["dep"], + requires_python="<2", # Something that always fails. + ) + create_basic_wheel_for_package( + script, + "dep", + "0.2.0", + ) + + result = script.pip( + "install", + "--use-feature=2020-resolver", + "--no-cache-dir", + "--no-index", + "--no-deps", + "--find-links", script.scratch_path, + "base", + expect_error=True, + ) + + message = ( + "Package 'base' requires a different Python: " + "{}.{}.{} not in '<2'".format(*sys.version_info[:3]) + ) + assert message in result.stderr From 6a8956d7a876508d50851f77ea13a08c96aa17eb Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Date: Tue, 13 Oct 2020 19:23:53 +0530 Subject: [PATCH 2601/3170] Merge pull request #8932 from uranusjr/new-resolver-lazy-sequence --- news/8023.feature.rst | 2 + .../resolution/resolvelib/factory.py | 59 ++++++----- .../resolution/resolvelib/found_candidates.py | 98 +++++++++++++++++++ .../resolution/resolvelib/provider.py | 97 +++++------------- tests/functional/test_new_resolver.py | 26 +++++ .../resolution_resolvelib/test_requirement.py | 11 ++- 6 files changed, 186 insertions(+), 107 deletions(-) create mode 100644 news/8023.feature.rst create mode 100644 src/pip/_internal/resolution/resolvelib/found_candidates.py diff --git a/news/8023.feature.rst b/news/8023.feature.rst new file mode 100644 index 00000000000..c886e9a66c1 --- /dev/null +++ b/news/8023.feature.rst @@ -0,0 +1,2 @@ +New resolver: Avoid accessing indexes when the installed candidate is preferred +and considered good enough. diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 172f054fa72..8813ab03890 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -1,7 +1,5 @@ -import collections import logging -from pip._vendor import six from pip._vendor.packaging.utils import canonicalize_name from pip._internal.exceptions import ( @@ -30,6 +28,7 @@ LinkCandidate, RequiresPythonCandidate, ) +from .found_candidates import FoundCandidates from .requirements import ( ExplicitRequirement, RequiresPythonRequirement, @@ -41,6 +40,7 @@ FrozenSet, Dict, Iterable, + Iterator, List, Optional, Sequence, @@ -102,7 +102,7 @@ def __init__( if not ignore_installed: self._installed_dists = { canonicalize_name(dist.project_name): dist - for dist in get_installed_distributions() + for dist in get_installed_distributions(local_only=False) } else: self._installed_dists = {} @@ -156,6 +156,7 @@ def _iter_found_candidates( ireqs, # type: Sequence[InstallRequirement] specifier, # type: SpecifierSet hashes, # type: Hashes + prefers_installed, # type: bool ): # type: (...) -> Iterable[Candidate] if not ireqs: @@ -174,54 +175,49 @@ def _iter_found_candidates( hashes &= ireq.hashes(trust_internet=False) extras |= frozenset(ireq.extras) - # We use this to ensure that we only yield a single candidate for - # each version (the finder's preferred one for that version). The - # requirement needs to return only one candidate per version, so we - # implement that logic here so that requirements using this helper - # don't all have to do the same thing later. - candidates = collections.OrderedDict() # type: VersionCandidates - # Get the installed version, if it matches, unless the user # specified `--force-reinstall`, when we want the version from # the index instead. - installed_version = None installed_candidate = None if not self._force_reinstall and name in self._installed_dists: installed_dist = self._installed_dists[name] - installed_version = installed_dist.parsed_version - if specifier.contains(installed_version, prereleases=True): + if specifier.contains(installed_dist.version, prereleases=True): installed_candidate = self._make_candidate_from_dist( dist=installed_dist, extras=extras, template=template, ) - found = self._finder.find_best_candidate( - project_name=name, - specifier=specifier, - hashes=hashes, - ) - for ican in found.iter_applicable(): - if ican.version == installed_version and installed_candidate: - candidate = installed_candidate - else: - candidate = self._make_candidate_from_link( + def iter_index_candidates(): + # type: () -> Iterator[Candidate] + result = self._finder.find_best_candidate( + project_name=name, + specifier=specifier, + hashes=hashes, + ) + # PackageFinder returns earlier versions first, so we reverse. + for ican in reversed(list(result.iter_applicable())): + yield self._make_candidate_from_link( link=ican.link, extras=extras, template=template, name=name, version=ican.version, ) - candidates[ican.version] = candidate - - # Yield the installed version even if it is not found on the index. - if installed_version and installed_candidate: - candidates[installed_version] = installed_candidate - return six.itervalues(candidates) + return FoundCandidates( + iter_index_candidates, + installed_candidate, + prefers_installed, + ) - def find_candidates(self, requirements, constraint): - # type: (Sequence[Requirement], Constraint) -> Iterable[Candidate] + def find_candidates( + self, + requirements, # type: Sequence[Requirement] + constraint, # type: Constraint + prefers_installed, # type: bool + ): + # type: (...) -> Iterable[Candidate] explicit_candidates = set() # type: Set[Candidate] ireqs = [] # type: List[InstallRequirement] for req in requirements: @@ -238,6 +234,7 @@ def find_candidates(self, requirements, constraint): ireqs, constraint.specifier, constraint.hashes, + prefers_installed, ) if constraint: diff --git a/src/pip/_internal/resolution/resolvelib/found_candidates.py b/src/pip/_internal/resolution/resolvelib/found_candidates.py new file mode 100644 index 00000000000..a669e893670 --- /dev/null +++ b/src/pip/_internal/resolution/resolvelib/found_candidates.py @@ -0,0 +1,98 @@ +import itertools +import operator + +from pip._vendor.six.moves import collections_abc # type: ignore + +from pip._internal.utils.compat import lru_cache +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import Callable, Iterator, Optional, Set + + from pip._vendor.packaging.version import _BaseVersion + + from .base import Candidate + + +def _deduplicated_by_version(candidates): + # type: (Iterator[Candidate]) -> Iterator[Candidate] + returned = set() # type: Set[_BaseVersion] + for candidate in candidates: + if candidate.version in returned: + continue + returned.add(candidate.version) + yield candidate + + +def _insert_installed(installed, others): + # type: (Candidate, Iterator[Candidate]) -> Iterator[Candidate] + """Iterator for ``FoundCandidates``. + + This iterator is used when the resolver prefers to upgrade an + already-installed package. Candidates from index are returned in their + normal ordering, except replaced when the version is already installed. + + Since candidates from index are already sorted by reverse version order, + `sorted()` here would keep the ordering mostly intact, only shuffling the + already-installed candidate into the correct position. We put the already- + installed candidate in front of those from the index, so it's put in front + after sorting due to Python sorting's stableness guarentee. + """ + candidates = sorted( + itertools.chain([installed], others), + key=operator.attrgetter("version"), + reverse=True, + ) + return iter(candidates) + + +class FoundCandidates(collections_abc.Sequence): + """A lazy sequence to provide candidates to the resolver. + + The intended usage is to return this from `find_matches()` so the resolver + can iterate through the sequence multiple times, but only access the index + page when remote packages are actually needed. This improve performances + when suitable candidates are already installed on disk. + """ + def __init__( + self, + get_others, # type: Callable[[], Iterator[Candidate]] + installed, # type: Optional[Candidate] + prefers_installed, # type: bool + ): + self._get_others = get_others + self._installed = installed + self._prefers_installed = prefers_installed + + def __getitem__(self, index): + # type: (int) -> Candidate + # Implemented to satisfy the ABC check. This is not needed by the + # resolver, and should not be used by the provider either (for + # performance reasons). + raise NotImplementedError("don't do this") + + def __iter__(self): + # type: () -> Iterator[Candidate] + if not self._installed: + candidates = self._get_others() + elif self._prefers_installed: + candidates = itertools.chain([self._installed], self._get_others()) + else: + candidates = _insert_installed(self._installed, self._get_others()) + return _deduplicated_by_version(candidates) + + def __len__(self): + # type: () -> int + # Implemented to satisfy the ABC check. This is not needed by the + # resolver, and should not be used by the provider either (for + # performance reasons). + raise NotImplementedError("don't do this") + + @lru_cache(maxsize=1) + def __bool__(self): + # type: () -> bool + if self._prefers_installed and self._installed: + return True + return any(self) + + __nonzero__ = __bool__ # XXX: Python 2. diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index 19c4d543f52..7d679e5fe46 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -54,30 +54,26 @@ def __init__( self._upgrade_strategy = upgrade_strategy self._user_requested = user_requested - def _sort_matches(self, matches): - # type: (Iterable[Candidate]) -> Sequence[Candidate] - - # The requirement is responsible for returning a sequence of potential - # candidates, one per version. The provider handles the logic of - # deciding the order in which these candidates should be passed to - # the resolver. - - # The `matches` argument is a sequence of candidates, one per version, - # which are potential options to be installed. The requirement will - # have already sorted out whether to give us an already-installed - # candidate or a version from PyPI (i.e., it will deal with options - # like --force-reinstall and --ignore-installed). - - # We now work out the correct order. - # - # 1. If no other considerations apply, later versions take priority. - # 2. An already installed distribution is preferred over any other, - # unless the user has requested an upgrade. - # Upgrades are allowed when: - # * The --upgrade flag is set, and - # - The project was specified on the command line, or - # - The project is a dependency and the "eager" upgrade strategy - # was requested. + def identify(self, dependency): + # type: (Union[Requirement, Candidate]) -> str + return dependency.name + + def get_preference( + self, + resolution, # type: Optional[Candidate] + candidates, # type: Sequence[Candidate] + information # type: Sequence[Tuple[Requirement, Candidate]] + ): + # type: (...) -> Any + transitive = all(parent is not None for _, parent in information) + return (transitive, bool(candidates)) + + def find_matches(self, requirements): + # type: (Sequence[Requirement]) -> Iterable[Candidate] + if not requirements: + return [] + name = requirements[0].name + def _eligible_for_upgrade(name): # type: (str) -> bool """Are upgrades allowed for this project? @@ -96,56 +92,11 @@ def _eligible_for_upgrade(name): return (name in self._user_requested) return False - def sort_key(c): - # type: (Candidate) -> int - """Return a sort key for the matches. - - The highest priority should be given to installed candidates that - are not eligible for upgrade. We use the integer value in the first - part of the key to sort these before other candidates. - - We only pull the installed candidate to the bottom (i.e. most - preferred), but otherwise keep the ordering returned by the - requirement. The requirement is responsible for returning a list - otherwise sorted for the resolver, taking account for versions - and binary preferences as specified by the user. - """ - if c.is_installed and not _eligible_for_upgrade(c.name): - return 1 - return 0 - - return sorted(matches, key=sort_key) - - def identify(self, dependency): - # type: (Union[Requirement, Candidate]) -> str - return dependency.name - - def get_preference( - self, - resolution, # type: Optional[Candidate] - candidates, # type: Sequence[Candidate] - information # type: Sequence[Tuple[Requirement, Optional[Candidate]]] - ): - # type: (...) -> Any - """Return a sort key to determine what dependency to look next. - - A smaller value makes a dependency higher priority. We put direct - (user-requested) dependencies first since they may contain useful - user-specified version ranges. Users tend to expect us to catch - problems in them early as well. - """ - transitive = all(parent is not None for _, parent in information) - return (transitive, len(candidates)) - - def find_matches(self, requirements): - # type: (Sequence[Requirement]) -> Iterable[Candidate] - if not requirements: - return [] - constraint = self._constraints.get( - requirements[0].name, Constraint.empty(), + return self._factory.find_candidates( + requirements, + constraint=self._constraints.get(name, Constraint.empty()), + prefers_installed=(not _eligible_for_upgrade(name)), ) - candidates = self._factory.find_candidates(requirements, constraint) - return reversed(self._sort_matches(candidates)) def is_satisfied_by(self, requirement, candidate): # type: (Requirement, Candidate) -> bool diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 1dab8d47091..1718ab8a8b8 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -1020,3 +1020,29 @@ def test_new_resolver_no_deps_checks_requires_python(script): "{}.{}.{} not in '<2'".format(*sys.version_info[:3]) ) assert message in result.stderr + + +def test_new_resolver_prefers_installed_in_upgrade_if_latest(script): + create_basic_wheel_for_package(script, "pkg", "1") + local_pkg = create_test_package_with_setup(script, name="pkg", version="2") + + # Install the version that's not on the index. + script.pip( + "install", + "--use-feature=2020-resolver", + "--no-cache-dir", + "--no-index", + local_pkg, + ) + + # Now --upgrade should still pick the local version because it's "better". + script.pip( + "install", + "--use-feature=2020-resolver", + "--no-cache-dir", + "--no-index", + "--find-links", script.scratch_path, + "--upgrade", + "pkg", + ) + assert_installed(script, pkg="2") diff --git a/tests/unit/resolution_resolvelib/test_requirement.py b/tests/unit/resolution_resolvelib/test_requirement.py index a03edb6f7c2..6149fd1aece 100644 --- a/tests/unit/resolution_resolvelib/test_requirement.py +++ b/tests/unit/resolution_resolvelib/test_requirement.py @@ -58,8 +58,10 @@ def test_new_resolver_correct_number_of_matches(test_cases, factory): """Requirements should return the correct number of candidates""" for spec, _, match_count in test_cases: req = factory.make_requirement_from_spec(spec, comes_from=None) - matches = factory.find_candidates([req], Constraint.empty()) - assert len(list(matches)) == match_count + matches = factory.find_candidates( + [req], Constraint.empty(), prefers_installed=False, + ) + assert sum(1 for _ in matches) == match_count def test_new_resolver_candidates_match_requirement(test_cases, factory): @@ -67,7 +69,10 @@ def test_new_resolver_candidates_match_requirement(test_cases, factory): """ for spec, _, _ in test_cases: req = factory.make_requirement_from_spec(spec, comes_from=None) - for c in factory.find_candidates([req], Constraint.empty()): + candidates = factory.find_candidates( + [req], Constraint.empty(), prefers_installed=False, + ) + for c in candidates: assert isinstance(c, Candidate) assert req.is_satisfied_by(c) From 57dd580f582f33310f1c53cca2af2255985425d2 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Fri, 16 Oct 2020 18:12:10 +0530 Subject: [PATCH 2602/3170] Bump for release --- NEWS.rst | 39 +++++++++++++++++++ ...3f7456-cc25-4df9-9518-4732b1e07fe5.trivial | 0 ...8c60c2-d540-4a25-af03-100d848acbc0.trivial | 0 news/8023.feature.rst | 2 - news/8417.removal | 1 - news/8676.feature | 2 - news/8758.bugfix | 2 - news/8792.bugfix | 2 - news/8807.doc | 1 - news/8839.bugfix | 3 -- news/8905.feature | 3 -- news/8924.feature | 2 - news/8927.removal | 1 - ...fa2e68-01bf-11eb-a0b1-4fe8cb1f9dcf.trivial | 0 ...7f9ea0-030d-11eb-92cb-6b2b625d02fc.trivial | 0 src/pip/__init__.py | 2 +- 16 files changed, 40 insertions(+), 20 deletions(-) delete mode 100644 news/093f7456-cc25-4df9-9518-4732b1e07fe5.trivial delete mode 100644 news/5e8c60c2-d540-4a25-af03-100d848acbc0.trivial delete mode 100644 news/8023.feature.rst delete mode 100644 news/8417.removal delete mode 100644 news/8676.feature delete mode 100644 news/8758.bugfix delete mode 100644 news/8792.bugfix delete mode 100644 news/8807.doc delete mode 100644 news/8839.bugfix delete mode 100644 news/8905.feature delete mode 100644 news/8924.feature delete mode 100644 news/8927.removal delete mode 100644 news/a2fa2e68-01bf-11eb-a0b1-4fe8cb1f9dcf.trivial delete mode 100644 news/bc7f9ea0-030d-11eb-92cb-6b2b625d02fc.trivial diff --git a/NEWS.rst b/NEWS.rst index 6f7c2cd232f..6c6db479980 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -9,6 +9,45 @@ .. towncrier release notes start +20.2.4 (2020-10-16) +=================== + +Deprecations and Removals +------------------------- + +- Document that certain removals can be fast tracked. (`#8417 <https://github.com/pypa/pip/issues/8417>`_) +- Document that Python versions are generally supported until PyPI usage falls below 5%. (`#8927 <https://github.com/pypa/pip/issues/8927>`_) + +Features +-------- + +- New resolver: Avoid accessing indexes when the installed candidate is preferred + and considered good enough. (`#8023 <https://github.com/pypa/pip/issues/8023>`_) +- Improve error message friendliness when an environment has packages with + corrupted metadata. (`#8676 <https://github.com/pypa/pip/issues/8676>`_) +- Cache package listings on index packages so they are guarenteed to stay stable + during a pip command session. This also improves performance when a index page + is accessed multiple times during the command session. (`#8905 <https://github.com/pypa/pip/issues/8905>`_) +- New resolver: Tweak resolution logic to improve user experience when + user-supplied requirements conflict. (`#8924 <https://github.com/pypa/pip/issues/8924>`_) + +Bug Fixes +--------- + +- New resolver: Correctly respect ``Requires-Python`` metadata to reject + incompatible packages in ``--no-deps`` mode. (`#8758 <https://github.com/pypa/pip/issues/8758>`_) +- New resolver: Pick up hash declarations in constraints files and use them to + filter available distributions. (`#8792 <https://github.com/pypa/pip/issues/8792>`_) +- New resolver: If a package appears multiple times in user specification with + different ``--hash`` options, only hashes that present in all specifications + should be allowed. (`#8839 <https://github.com/pypa/pip/issues/8839>`_) + +Improved Documentation +---------------------- + +- Add ux documentation (`#8807 <https://github.com/pypa/pip/issues/8807>`_) + + 20.2.3 (2020-09-08) =================== diff --git a/news/093f7456-cc25-4df9-9518-4732b1e07fe5.trivial b/news/093f7456-cc25-4df9-9518-4732b1e07fe5.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/5e8c60c2-d540-4a25-af03-100d848acbc0.trivial b/news/5e8c60c2-d540-4a25-af03-100d848acbc0.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/8023.feature.rst b/news/8023.feature.rst deleted file mode 100644 index c886e9a66c1..00000000000 --- a/news/8023.feature.rst +++ /dev/null @@ -1,2 +0,0 @@ -New resolver: Avoid accessing indexes when the installed candidate is preferred -and considered good enough. diff --git a/news/8417.removal b/news/8417.removal deleted file mode 100644 index 8f280b535c3..00000000000 --- a/news/8417.removal +++ /dev/null @@ -1 +0,0 @@ -Document that certain removals can be fast tracked. diff --git a/news/8676.feature b/news/8676.feature deleted file mode 100644 index f8da963f6ed..00000000000 --- a/news/8676.feature +++ /dev/null @@ -1,2 +0,0 @@ -Improve error message friendliness when an environment has packages with -corrupted metadata. diff --git a/news/8758.bugfix b/news/8758.bugfix deleted file mode 100644 index 9f44b7e47a4..00000000000 --- a/news/8758.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -New resolver: Correctly respect ``Requires-Python`` metadata to reject -incompatible packages in ``--no-deps`` mode. diff --git a/news/8792.bugfix b/news/8792.bugfix deleted file mode 100644 index e83bdb09cfe..00000000000 --- a/news/8792.bugfix +++ /dev/null @@ -1,2 +0,0 @@ -New resolver: Pick up hash declarations in constraints files and use them to -filter available distributions. diff --git a/news/8807.doc b/news/8807.doc deleted file mode 100644 index 6ef1a123adb..00000000000 --- a/news/8807.doc +++ /dev/null @@ -1 +0,0 @@ -Add ux documentation diff --git a/news/8839.bugfix b/news/8839.bugfix deleted file mode 100644 index 987b801e932..00000000000 --- a/news/8839.bugfix +++ /dev/null @@ -1,3 +0,0 @@ -New resolver: If a package appears multiple times in user specification with -different ``--hash`` options, only hashes that present in all specifications -should be allowed. diff --git a/news/8905.feature b/news/8905.feature deleted file mode 100644 index 5d27d40c2be..00000000000 --- a/news/8905.feature +++ /dev/null @@ -1,3 +0,0 @@ -Cache package listings on index packages so they are guarenteed to stay stable -during a pip command session. This also improves performance when a index page -is accessed multiple times during the command session. diff --git a/news/8924.feature b/news/8924.feature deleted file mode 100644 index c607aa0d06b..00000000000 --- a/news/8924.feature +++ /dev/null @@ -1,2 +0,0 @@ -New resolver: Tweak resolution logic to improve user experience when -user-supplied requirements conflict. diff --git a/news/8927.removal b/news/8927.removal deleted file mode 100644 index 0032fa5f29d..00000000000 --- a/news/8927.removal +++ /dev/null @@ -1 +0,0 @@ -Document that Python versions are generally supported until PyPI usage falls below 5%. diff --git a/news/a2fa2e68-01bf-11eb-a0b1-4fe8cb1f9dcf.trivial b/news/a2fa2e68-01bf-11eb-a0b1-4fe8cb1f9dcf.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/bc7f9ea0-030d-11eb-92cb-6b2b625d02fc.trivial b/news/bc7f9ea0-030d-11eb-92cb-6b2b625d02fc.trivial deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/pip/__init__.py b/src/pip/__init__.py index 9fb68d40354..b2e05142fbd 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -4,7 +4,7 @@ from typing import List, Optional -__version__ = "20.2.3" +__version__ = "20.2.4" def main(args=None): From c73f06f447283f7805c9539a07a78afa5670195d Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Fri, 16 Oct 2020 18:12:10 +0530 Subject: [PATCH 2603/3170] Bump for development --- src/pip/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/__init__.py b/src/pip/__init__.py index b2e05142fbd..5a2f3c31745 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -4,7 +4,7 @@ from typing import List, Optional -__version__ = "20.2.4" +__version__ = "20.3.dev0" def main(args=None): From 47bea8aa27e7c6d9823143675bf2789e878061ee Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Fri, 16 Oct 2020 18:25:19 +0530 Subject: [PATCH 2604/3170] Delete news fragments from pip 20.2.4 --- news/8023.feature.rst | 2 -- news/8758.bugfix.rst | 2 -- 2 files changed, 4 deletions(-) delete mode 100644 news/8023.feature.rst delete mode 100644 news/8758.bugfix.rst diff --git a/news/8023.feature.rst b/news/8023.feature.rst deleted file mode 100644 index c886e9a66c1..00000000000 --- a/news/8023.feature.rst +++ /dev/null @@ -1,2 +0,0 @@ -New resolver: Avoid accessing indexes when the installed candidate is preferred -and considered good enough. diff --git a/news/8758.bugfix.rst b/news/8758.bugfix.rst deleted file mode 100644 index 9f44b7e47a4..00000000000 --- a/news/8758.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -New resolver: Correctly respect ``Requires-Python`` metadata to reject -incompatible packages in ``--no-deps`` mode. From c69644257ec690787779ea5d96cbf6e016519de9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Sat, 17 Oct 2020 19:47:55 +0700 Subject: [PATCH 2605/3170] Pin vendoring to ~=0.3.3 in test suite Co-authored-by: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> --- tox.ini | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tox.ini b/tox.ini index 30a908cd36b..9c20759af3a 100644 --- a/tox.ini +++ b/tox.ini @@ -70,7 +70,7 @@ basepython = python3 skip_install = True commands_pre = deps = - vendoring>=0.3.3 + vendoring~=0.3.3 # Required, otherwise we interpret --no-binary :all: as # "do not build wheels", which fails for PEP 517 requirements pip>=19.3.1 From fb03b6aedeb2eb5e68d3dd539c13f2f0eb57b01d Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sun, 18 Oct 2020 22:42:24 +0530 Subject: [PATCH 2606/3170] Switch to sphinx-inline-tabs for tabs --- docs/html/conf.py | 2 +- docs/html/development/getting-started.rst | 26 +- docs/html/installing.rst | 84 ++-- docs/html/quickstart.rst | 170 +++---- docs/html/reference/pip.rst | 28 +- docs/html/reference/pip_cache.rst | 10 +- docs/html/reference/pip_check.rst | 88 ++-- docs/html/reference/pip_config.rst | 10 +- docs/html/reference/pip_debug.rst | 10 +- docs/html/reference/pip_download.rst | 192 ++++---- docs/html/reference/pip_freeze.rst | 63 ++- docs/html/reference/pip_hash.rst | 52 +- docs/html/reference/pip_install.rst | 473 +++++++++---------- docs/html/reference/pip_list.rst | 227 ++++----- docs/html/reference/pip_search.rst | 32 +- docs/html/reference/pip_show.rst | 232 +++++---- docs/html/reference/pip_uninstall.rst | 44 +- docs/html/reference/pip_wheel.rst | 70 ++- docs/html/user_guide.rst | 549 ++++++++++------------ tools/requirements/docs.txt | 2 +- 20 files changed, 1068 insertions(+), 1296 deletions(-) diff --git a/docs/html/conf.py b/docs/html/conf.py index 444d15a819a..adee6fd3c49 100644 --- a/docs/html/conf.py +++ b/docs/html/conf.py @@ -35,7 +35,7 @@ 'sphinx.ext.extlinks', 'sphinx.ext.intersphinx', # third-party: - 'sphinx_tabs.tabs', + 'sphinx_inline_tabs', # in-tree: 'docs_feedback_sphinxext', 'pip_sphinxext', diff --git a/docs/html/development/getting-started.rst b/docs/html/development/getting-started.rst index 1bc4a551621..436ed241baf 100644 --- a/docs/html/development/getting-started.rst +++ b/docs/html/development/getting-started.rst @@ -38,25 +38,23 @@ To run the pip executable from your source tree during development, install pip locally using editable installation (inside a virtualenv). You can then invoke your local source tree pip normally. -.. tabs:: +.. tab:: Unix/macOS - .. group-tab:: Unix/macOS + .. code-block:: shell - .. code-block:: shell + virtualenv venv # You can also use "python -m venv venv" from python3.3+ + source venv/bin/activate + python -m pip install -e . + python -m pip --version - virtualenv venv # You can also use "python -m venv venv" from python3.3+ - source venv/bin/activate - python -m pip install -e . - python -m pip --version +.. tab:: Windows - .. group-tab:: Windows + .. code-block:: shell - .. code-block:: shell - - virtualenv venv # You can also use "py -m venv venv" from python3.3+ - venv\Scripts\activate - py -m pip install -e . - py -m pip --version + virtualenv venv # You can also use "py -m venv venv" from python3.3+ + venv\Scripts\activate + py -m pip install -e . + py -m pip --version Running Tests ============= diff --git a/docs/html/installing.rst b/docs/html/installing.rst index 5379c1da0e0..00c74a70abf 100644 --- a/docs/html/installing.rst +++ b/docs/html/installing.rst @@ -28,19 +28,17 @@ this link: `get-pip.py Then run the following command in the folder where you have downloaded ``get-pip.py``: -.. tabs:: +.. tab:: Unix/macOS - .. group-tab:: Unix/macOS + .. code-block:: shell - .. code-block:: shell + python get-pip.py - python get-pip.py +.. tab:: Windows - .. group-tab:: Windows + .. code-block:: shell - .. code-block:: shell - - py get-pip.py + py get-pip.py .. warning:: @@ -81,68 +79,60 @@ some examples: Install from local copies of pip and setuptools: -.. tabs:: - - .. group-tab:: Unix/macOS +.. tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python get-pip.py --no-index --find-links=/local/copies + python get-pip.py --no-index --find-links=/local/copies - .. group-tab:: Windows +.. tab:: Windows - .. code-block:: shell + .. code-block:: shell - py get-pip.py --no-index --find-links=/local/copies + py get-pip.py --no-index --find-links=/local/copies Install to the user site [3]_: -.. tabs:: - - .. group-tab:: Unix/macOS +.. tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python get-pip.py --user + python get-pip.py --user - .. group-tab:: Windows +.. tab:: Windows - .. code-block:: shell + .. code-block:: shell - py get-pip.py --user + py get-pip.py --user Install behind a proxy: -.. tabs:: +.. tab:: Unix/macOS - .. group-tab:: Unix/macOS + .. code-block:: shell - .. code-block:: shell + python get-pip.py --proxy="http://[user:passwd@]proxy.server:port" - python get-pip.py --proxy="http://[user:passwd@]proxy.server:port" +.. tab:: Windows - .. group-tab:: Windows + .. code-block:: shell - .. code-block:: shell - - py get-pip.py --proxy="http://[user:passwd@]proxy.server:port" + py get-pip.py --proxy="http://[user:passwd@]proxy.server:port" ``get-pip.py`` can also be used to install a specified combination of ``pip``, ``setuptools``, and ``wheel`` using the same requirements syntax as pip: -.. tabs:: - - .. group-tab:: Unix/macOS +.. tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python get-pip.py pip==9.0.2 wheel==0.30.0 setuptools==28.8.0 + python get-pip.py pip==9.0.2 wheel==0.30.0 setuptools==28.8.0 - .. group-tab:: Windows +.. tab:: Windows - .. code-block:: shell + .. code-block:: shell - py get-pip.py pip==9.0.2 wheel==0.30.0 setuptools==28.8.0 + py get-pip.py pip==9.0.2 wheel==0.30.0 setuptools==28.8.0 Using Linux Package Managers ============================ @@ -156,19 +146,17 @@ the `Python Packaging User Guide Upgrading pip ============= -.. tabs:: - - .. group-tab:: Unix/macOS +.. tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python -m pip install -U pip + python -m pip install -U pip - .. group-tab:: Windows +.. tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip install -U pip + py -m pip install -U pip .. _compatibility-requirements: diff --git a/docs/html/quickstart.rst b/docs/html/quickstart.rst index 9591e1127f5..96602a7b316 100644 --- a/docs/html/quickstart.rst +++ b/docs/html/quickstart.rst @@ -6,145 +6,131 @@ First, :doc:`install pip <installing>`. Install a package from `PyPI`_: -.. tabs:: +.. tab:: Unix/macOS - .. group-tab:: Unix/macOS + .. code-block:: console - .. code-block:: console + $ python -m pip install SomePackage + [...] + Successfully installed SomePackage - $ python -m pip install SomePackage - [...] - Successfully installed SomePackage +.. tab:: Windows - .. group-tab:: Windows + .. code-block:: console - .. code-block:: console - - C:\> py -m pip install SomePackage - [...] - Successfully installed SomePackage + C:\> py -m pip install SomePackage + [...] + Successfully installed SomePackage Install a package that's already been downloaded from `PyPI`_ or obtained from elsewhere. This is useful if the target machine does not have a network connection: -.. tabs:: - - .. group-tab:: Unix/macOS - - .. code-block:: console +.. tab:: Unix/macOS - $ python -m pip install SomePackage-1.0-py2.py3-none-any.whl - [...] - Successfully installed SomePackage + .. code-block:: console - .. group-tab:: Windows + $ python -m pip install SomePackage-1.0-py2.py3-none-any.whl + [...] + Successfully installed SomePackage - .. code-block:: console +.. tab:: Windows - C:\> py -m pip install SomePackage-1.0-py2.py3-none-any.whl - [...] - Successfully installed SomePackage + .. code-block:: console + C:\> py -m pip install SomePackage-1.0-py2.py3-none-any.whl + [...] + Successfully installed SomePackage Show what files were installed: -.. tabs:: +.. tab:: Unix/macOS - .. group-tab:: Unix/macOS + .. code-block:: console - .. code-block:: console + $ python -m pip show --files SomePackage + Name: SomePackage + Version: 1.0 + Location: /my/env/lib/pythonx.x/site-packages + Files: + ../somepackage/__init__.py + [...] - $ python -m pip show --files SomePackage - Name: SomePackage - Version: 1.0 - Location: /my/env/lib/pythonx.x/site-packages - Files: - ../somepackage/__init__.py - [...] +.. tab:: Windows - .. group-tab:: Windows + .. code-block:: console - .. code-block:: console - - C:\> py -m pip show --files SomePackage - Name: SomePackage - Version: 1.0 - Location: /my/env/lib/pythonx.x/site-packages - Files: - ../somepackage/__init__.py - [...] + C:\> py -m pip show --files SomePackage + Name: SomePackage + Version: 1.0 + Location: /my/env/lib/pythonx.x/site-packages + Files: + ../somepackage/__init__.py + [...] List what packages are outdated: -.. tabs:: - - .. group-tab:: Unix/macOS +.. tab:: Unix/macOS - .. code-block:: console + .. code-block:: console - $ python -m pip list --outdated - SomePackage (Current: 1.0 Latest: 2.0) + $ python -m pip list --outdated + SomePackage (Current: 1.0 Latest: 2.0) - .. group-tab:: Windows +.. tab:: Windows - .. code-block:: console + .. code-block:: console - C:\> py -m pip list --outdated - SomePackage (Current: 1.0 Latest: 2.0) + C:\> py -m pip list --outdated + SomePackage (Current: 1.0 Latest: 2.0) Upgrade a package: -.. tabs:: +.. tab:: Unix/macOS - .. group-tab:: Unix/macOS + .. code-block:: console - .. code-block:: console + $ python -m pip install --upgrade SomePackage + [...] + Found existing installation: SomePackage 1.0 + Uninstalling SomePackage: + Successfully uninstalled SomePackage + Running setup.py install for SomePackage + Successfully installed SomePackage - $ python -m pip install --upgrade SomePackage - [...] - Found existing installation: SomePackage 1.0 - Uninstalling SomePackage: - Successfully uninstalled SomePackage - Running setup.py install for SomePackage - Successfully installed SomePackage +.. tab:: Windows - .. group-tab:: Windows + .. code-block:: console - .. code-block:: console - - C:\> py -m pip install --upgrade SomePackage - [...] - Found existing installation: SomePackage 1.0 - Uninstalling SomePackage: - Successfully uninstalled SomePackage - Running setup.py install for SomePackage - Successfully installed SomePackage + C:\> py -m pip install --upgrade SomePackage + [...] + Found existing installation: SomePackage 1.0 + Uninstalling SomePackage: + Successfully uninstalled SomePackage + Running setup.py install for SomePackage + Successfully installed SomePackage Uninstall a package: -.. tabs:: - - .. group-tab:: Unix/macOS +.. tab:: Unix/macOS - .. code-block:: console + .. code-block:: console - $ python -m pip uninstall SomePackage - Uninstalling SomePackage: - /my/env/lib/pythonx.x/site-packages/somepackage - Proceed (y/n)? y - Successfully uninstalled SomePackage + $ python -m pip uninstall SomePackage + Uninstalling SomePackage: + /my/env/lib/pythonx.x/site-packages/somepackage + Proceed (y/n)? y + Successfully uninstalled SomePackage - .. group-tab:: Windows +.. tab:: Windows - .. code-block:: console - - C:\> py -m pip uninstall SomePackage - Uninstalling SomePackage: - /my/env/lib/pythonx.x/site-packages/somepackage - Proceed (y/n)? y - Successfully uninstalled SomePackage + .. code-block:: console + C:\> py -m pip uninstall SomePackage + Uninstalling SomePackage: + /my/env/lib/pythonx.x/site-packages/somepackage + Proceed (y/n)? y + Successfully uninstalled SomePackage .. _PyPI: https://pypi.org/ diff --git a/docs/html/reference/pip.rst b/docs/html/reference/pip.rst index 9fd42c676a9..298a1101d0e 100644 --- a/docs/html/reference/pip.rst +++ b/docs/html/reference/pip.rst @@ -7,19 +7,17 @@ pip Usage ***** -.. tabs:: +.. tab:: Unix/macOS - .. group-tab:: Unix/macOS + .. code-block:: shell - .. code-block:: shell + python -m pip <command> [options] - python -m pip <command> [options] +.. tab:: Windows - .. group-tab:: Windows + .. code-block:: shell - .. code-block:: shell - - py -m pip <command> [options] + py -m pip <command> [options] Description *********** @@ -232,19 +230,17 @@ and ``pip wheel`` inject additional arguments into the ``setup.py`` command (``--build-option`` is only available in ``pip wheel``). These arguments are included in the command as follows: -.. tabs:: - - .. group-tab:: Unix/macOS +.. tab:: Unix/macOS - .. code-block:: console + .. code-block:: console - python setup.py <global_options> BUILD COMMAND <build_options> + python setup.py <global_options> BUILD COMMAND <build_options> - .. group-tab:: Windows +.. tab:: Windows - .. code-block:: shell + .. code-block:: shell - py setup.py <global_options> BUILD COMMAND <build_options> + py setup.py <global_options> BUILD COMMAND <build_options> The options are passed unmodified, and presently offer direct access to the distutils command line. Use of ``--global-option`` and ``--build-option`` diff --git a/docs/html/reference/pip_cache.rst b/docs/html/reference/pip_cache.rst index 35e0dfcadac..c443a6f3a75 100644 --- a/docs/html/reference/pip_cache.rst +++ b/docs/html/reference/pip_cache.rst @@ -9,15 +9,13 @@ pip cache Usage ***** -.. tabs:: +.. tab:: Unix/macOS - .. group-tab:: Unix/macOS + .. pip-command-usage:: cache "python -m pip" - .. pip-command-usage:: cache "python -m pip" +.. tab:: Windows - .. group-tab:: Windows - - .. pip-command-usage:: cache "py -m pip" + .. pip-command-usage:: cache "py -m pip" Description *********** diff --git a/docs/html/reference/pip_check.rst b/docs/html/reference/pip_check.rst index d3bb457e12c..3b2ecb511b1 100644 --- a/docs/html/reference/pip_check.rst +++ b/docs/html/reference/pip_check.rst @@ -10,15 +10,13 @@ pip check Usage ===== -.. tabs:: +.. tab:: Unix/macOS - .. group-tab:: Unix/macOS + .. pip-command-usage:: check "python -m pip" - .. pip-command-usage:: check "python -m pip" +.. tab:: Windows - .. group-tab:: Windows - - .. pip-command-usage:: check "py -m pip" + .. pip-command-usage:: check "py -m pip" Description @@ -32,66 +30,60 @@ Examples #. If all dependencies are compatible: - .. tabs:: - - .. group-tab:: Unix/macOS + .. tab:: Unix/macOS - .. code-block:: console + .. code-block:: console - $ python -m pip check - No broken requirements found. - $ echo $? - 0 + $ python -m pip check + No broken requirements found. + $ echo $? + 0 - .. group-tab:: Windows + .. tab:: Windows - .. code-block:: console + .. code-block:: console - C:\> py -m pip check - No broken requirements found. - C:\> echo %errorlevel% - 0 + C:\> py -m pip check + No broken requirements found. + C:\> echo %errorlevel% + 0 #. If a package is missing: - .. tabs:: + .. tab:: Unix/macOS - .. group-tab:: Unix/macOS + .. code-block:: console - .. code-block:: console + $ python -m pip check + pyramid 1.5.2 requires WebOb, which is not installed. + $ echo $? + 1 - $ python -m pip check - pyramid 1.5.2 requires WebOb, which is not installed. - $ echo $? - 1 + .. tab:: Windows - .. group-tab:: Windows + .. code-block:: console - .. code-block:: console - - C:\> py -m pip check - pyramid 1.5.2 requires WebOb, which is not installed. - C:\> echo %errorlevel% - 1 + C:\> py -m pip check + pyramid 1.5.2 requires WebOb, which is not installed. + C:\> echo %errorlevel% + 1 #. If a package has the wrong version: - .. tabs:: - - .. group-tab:: Unix/macOS + .. tab:: Unix/macOS - .. code-block:: console + .. code-block:: console - $ python -m pip check - pyramid 1.5.2 has requirement WebOb>=1.3.1, but you have WebOb 0.8. - $ echo $? - 1 + $ python -m pip check + pyramid 1.5.2 has requirement WebOb>=1.3.1, but you have WebOb 0.8. + $ echo $? + 1 - .. group-tab:: Windows + .. tab:: Windows - .. code-block:: console + .. code-block:: console - C:\> py -m pip check - pyramid 1.5.2 has requirement WebOb>=1.3.1, but you have WebOb 0.8. - C:\> echo %errorlevel% - 1 + C:\> py -m pip check + pyramid 1.5.2 has requirement WebOb>=1.3.1, but you have WebOb 0.8. + C:\> echo %errorlevel% + 1 diff --git a/docs/html/reference/pip_config.rst b/docs/html/reference/pip_config.rst index d9bf0afc8f5..14b1ac775db 100644 --- a/docs/html/reference/pip_config.rst +++ b/docs/html/reference/pip_config.rst @@ -11,15 +11,13 @@ pip config Usage ===== -.. tabs:: +.. tab:: Unix/macOS - .. group-tab:: Unix/macOS + .. pip-command-usage:: config "python -m pip" - .. pip-command-usage:: config "python -m pip" +.. tab:: Windows - .. group-tab:: Windows - - .. pip-command-usage:: config "py -m pip" + .. pip-command-usage:: config "py -m pip" Description diff --git a/docs/html/reference/pip_debug.rst b/docs/html/reference/pip_debug.rst index 2ef98228aa8..a077a169b65 100644 --- a/docs/html/reference/pip_debug.rst +++ b/docs/html/reference/pip_debug.rst @@ -10,15 +10,13 @@ pip debug Usage ===== -.. tabs:: +.. tab:: Unix/macOS - .. group-tab:: Unix/macOS + .. pip-command-usage:: debug "python -m pip" - .. pip-command-usage:: debug "python -m pip" +.. tab:: Windows - .. group-tab:: Windows - - .. pip-command-usage:: debug "py -m pip" + .. pip-command-usage:: debug "py -m pip" .. warning:: diff --git a/docs/html/reference/pip_download.rst b/docs/html/reference/pip_download.rst index 7983bb95b04..80acc1942fd 100644 --- a/docs/html/reference/pip_download.rst +++ b/docs/html/reference/pip_download.rst @@ -11,15 +11,13 @@ pip download Usage ===== -.. tabs:: +.. tab:: Unix/macOS - .. group-tab:: Unix/macOS + .. pip-command-usage:: download "python -m pip" - .. pip-command-usage:: download "python -m pip" +.. tab:: Windows - .. group-tab:: Windows - - .. pip-command-usage:: download "py -m pip" + .. pip-command-usage:: download "py -m pip" Description @@ -64,23 +62,21 @@ Examples #. Download a package and all of its dependencies - .. tabs:: - - .. group-tab:: Unix/macOS + .. tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python -m pip download SomePackage - python -m pip download -d . SomePackage # equivalent to above - python -m pip download --no-index --find-links=/tmp/wheelhouse -d /tmp/otherwheelhouse SomePackage + python -m pip download SomePackage + python -m pip download -d . SomePackage # equivalent to above + python -m pip download --no-index --find-links=/tmp/wheelhouse -d /tmp/otherwheelhouse SomePackage - .. group-tab:: Windows + .. tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip download SomePackage - py -m pip download -d . SomePackage # equivalent to above - py -m pip download --no-index --find-links=/tmp/wheelhouse -d /tmp/otherwheelhouse SomePackage + py -m pip download SomePackage + py -m pip download -d . SomePackage # equivalent to above + py -m pip download --no-index --find-links=/tmp/wheelhouse -d /tmp/otherwheelhouse SomePackage #. Download a package and all of its dependencies with OSX specific interpreter constraints. @@ -90,122 +86,114 @@ Examples It will also match deps with platform ``any``. Also force the interpreter version to ``27`` (or more generic, i.e. ``2``) and implementation to ``cp`` (or more generic, i.e. ``py``). - .. tabs:: - - .. group-tab:: Unix/macOS + .. tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python -m pip download \ - --only-binary=:all: \ - --platform macosx-10_10_x86_64 \ - --python-version 27 \ - --implementation cp \ - SomePackage + python -m pip download \ + --only-binary=:all: \ + --platform macosx-10_10_x86_64 \ + --python-version 27 \ + --implementation cp \ + SomePackage - .. group-tab:: Windows + .. tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip download ^ - --only-binary=:all: ^ - --platform macosx-10_10_x86_64 ^ - --python-version 27 ^ - --implementation cp ^ - SomePackage + py -m pip download ^ + --only-binary=:all: ^ + --platform macosx-10_10_x86_64 ^ + --python-version 27 ^ + --implementation cp ^ + SomePackage #. Download a package and its dependencies with linux specific constraints. Force the interpreter to be any minor version of py3k, and only accept ``cp34m`` or ``none`` as the abi. - .. tabs:: + .. tab:: Unix/macOS - .. group-tab:: Unix/macOS + .. code-block:: shell - .. code-block:: shell + python -m pip download \ + --only-binary=:all: \ + --platform linux_x86_64 \ + --python-version 3 \ + --implementation cp \ + --abi cp34m \ + SomePackage - python -m pip download \ - --only-binary=:all: \ - --platform linux_x86_64 \ - --python-version 3 \ - --implementation cp \ - --abi cp34m \ - SomePackage + .. tab:: Windows - .. group-tab:: Windows + .. code-block:: shell - .. code-block:: shell - - py -m pip download ^ - --only-binary=:all: ^ - --platform linux_x86_64 ^ - --python-version 3 ^ - --implementation cp ^ - --abi cp34m ^ - SomePackage + py -m pip download ^ + --only-binary=:all: ^ + --platform linux_x86_64 ^ + --python-version 3 ^ + --implementation cp ^ + --abi cp34m ^ + SomePackage #. Force platform, implementation, and abi agnostic deps. - .. tabs:: - - .. group-tab:: Unix/macOS + .. tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python -m pip download \ - --only-binary=:all: \ - --platform any \ - --python-version 3 \ - --implementation py \ - --abi none \ - SomePackage + python -m pip download \ + --only-binary=:all: \ + --platform any \ + --python-version 3 \ + --implementation py \ + --abi none \ + SomePackage - .. group-tab:: Windows + .. tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip download ^ - --only-binary=:all: ^ - --platform any ^ - --python-version 3 ^ - --implementation py ^ - --abi none ^ - SomePackage + py -m pip download ^ + --only-binary=:all: ^ + --platform any ^ + --python-version 3 ^ + --implementation py ^ + --abi none ^ + SomePackage #. Even when overconstrained, this will still correctly fetch the pip universal wheel. - .. tabs:: - - .. group-tab:: Unix/macOS + .. tab:: Unix/macOS - .. code-block:: console + .. code-block:: console - $ python -m pip download \ - --only-binary=:all: \ - --platform linux_x86_64 \ - --python-version 33 \ - --implementation cp \ - --abi cp34m \ - pip>=8 + $ python -m pip download \ + --only-binary=:all: \ + --platform linux_x86_64 \ + --python-version 33 \ + --implementation cp \ + --abi cp34m \ + pip>=8 - .. code-block:: console + .. code-block:: console - $ ls pip-8.1.1-py2.py3-none-any.whl - pip-8.1.1-py2.py3-none-any.whl + $ ls pip-8.1.1-py2.py3-none-any.whl + pip-8.1.1-py2.py3-none-any.whl - .. group-tab:: Windows + .. tab:: Windows - .. code-block:: console + .. code-block:: console - C:\> py -m pip download ^ - --only-binary=:all: ^ - --platform linux_x86_64 ^ - --python-version 33 ^ - --implementation cp ^ - --abi cp34m ^ - pip>=8 + C:\> py -m pip download ^ + --only-binary=:all: ^ + --platform linux_x86_64 ^ + --python-version 33 ^ + --implementation cp ^ + --abi cp34m ^ + pip>=8 - .. code-block:: console + .. code-block:: console - C:\> dir pip-8.1.1-py2.py3-none-any.whl - pip-8.1.1-py2.py3-none-any.whl + C:\> dir pip-8.1.1-py2.py3-none-any.whl + pip-8.1.1-py2.py3-none-any.whl diff --git a/docs/html/reference/pip_freeze.rst b/docs/html/reference/pip_freeze.rst index d4ed00bfb3e..152823a080b 100644 --- a/docs/html/reference/pip_freeze.rst +++ b/docs/html/reference/pip_freeze.rst @@ -11,15 +11,13 @@ pip freeze Usage ===== -.. tabs:: +.. tab:: Unix/macOS - .. group-tab:: Unix/macOS + .. pip-command-usage:: freeze "python -m pip" - .. pip-command-usage:: freeze "python -m pip" +.. tab:: Windows - .. group-tab:: Windows - - .. pip-command-usage:: freeze "py -m pip" + .. pip-command-usage:: freeze "py -m pip" Description @@ -39,45 +37,40 @@ Examples #. Generate output suitable for a requirements file. - .. tabs:: - - .. group-tab:: Unix/macOS + .. tab:: Unix/macOS - .. code-block:: console + .. code-block:: console - $ python -m pip freeze - docutils==0.11 - Jinja2==2.7.2 - MarkupSafe==0.19 - Pygments==1.6 - Sphinx==1.2.2 + $ python -m pip freeze + docutils==0.11 + Jinja2==2.7.2 + MarkupSafe==0.19 + Pygments==1.6 + Sphinx==1.2.2 - .. group-tab:: Windows + .. tab:: Windows - .. code-block:: console - - C:\> py -m pip freeze - docutils==0.11 - Jinja2==2.7.2 - MarkupSafe==0.19 - Pygments==1.6 - Sphinx==1.2.2 + .. code-block:: console + C:\> py -m pip freeze + docutils==0.11 + Jinja2==2.7.2 + MarkupSafe==0.19 + Pygments==1.6 + Sphinx==1.2.2 #. Generate a requirements file and then install from it in another environment. - .. tabs:: - - .. group-tab:: Unix/macOS + .. tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - env1/bin/python -m pip freeze > requirements.txt - env2/bin/python -m pip install -r requirements.txt + env1/bin/python -m pip freeze > requirements.txt + env2/bin/python -m pip install -r requirements.txt - .. group-tab:: Windows + .. tab:: Windows - .. code-block:: shell + .. code-block:: shell - env1\bin\python -m pip freeze > requirements.txt - env2\bin\python -m pip install -r requirements.txt + env1\bin\python -m pip freeze > requirements.txt + env2\bin\python -m pip install -r requirements.txt diff --git a/docs/html/reference/pip_hash.rst b/docs/html/reference/pip_hash.rst index 71e1cf4be6c..e9f5964dddd 100644 --- a/docs/html/reference/pip_hash.rst +++ b/docs/html/reference/pip_hash.rst @@ -10,15 +10,13 @@ pip hash Usage ===== -.. tabs:: +.. tab:: Unix/macOS - .. group-tab:: Unix/macOS + .. pip-command-usage:: hash "python -m pip" - .. pip-command-usage:: hash "python -m pip" +.. tab:: Windows - .. group-tab:: Windows - - .. pip-command-usage:: hash "py -m pip" + .. pip-command-usage:: hash "py -m pip" Description @@ -49,30 +47,28 @@ Example Compute the hash of a downloaded archive: -.. tabs:: - - .. group-tab:: Unix/macOS +.. tab:: Unix/macOS - .. code-block:: console + .. code-block:: console - $ python -m pip download SomePackage - Collecting SomePackage - Downloading SomePackage-2.2.tar.gz - Saved ./pip_downloads/SomePackage-2.2.tar.gz - Successfully downloaded SomePackage - $ python -m pip hash ./pip_downloads/SomePackage-2.2.tar.gz - ./pip_downloads/SomePackage-2.2.tar.gz: - --hash=sha256:93e62e05c7ad3da1a233def6731e8285156701e3419a5fe279017c429ec67ce0 + $ python -m pip download SomePackage + Collecting SomePackage + Downloading SomePackage-2.2.tar.gz + Saved ./pip_downloads/SomePackage-2.2.tar.gz + Successfully downloaded SomePackage + $ python -m pip hash ./pip_downloads/SomePackage-2.2.tar.gz + ./pip_downloads/SomePackage-2.2.tar.gz: + --hash=sha256:93e62e05c7ad3da1a233def6731e8285156701e3419a5fe279017c429ec67ce0 - .. group-tab:: Windows +.. tab:: Windows - .. code-block:: console + .. code-block:: console - C:\> py -m pip download SomePackage - Collecting SomePackage - Downloading SomePackage-2.2.tar.gz - Saved ./pip_downloads/SomePackage-2.2.tar.gz - Successfully downloaded SomePackage - C:\> py -m pip hash ./pip_downloads/SomePackage-2.2.tar.gz - ./pip_downloads/SomePackage-2.2.tar.gz: - --hash=sha256:93e62e05c7ad3da1a233def6731e8285156701e3419a5fe279017c429ec67ce0 + C:\> py -m pip download SomePackage + Collecting SomePackage + Downloading SomePackage-2.2.tar.gz + Saved ./pip_downloads/SomePackage-2.2.tar.gz + Successfully downloaded SomePackage + C:\> py -m pip hash ./pip_downloads/SomePackage-2.2.tar.gz + ./pip_downloads/SomePackage-2.2.tar.gz: + --hash=sha256:93e62e05c7ad3da1a233def6731e8285156701e3419a5fe279017c429ec67ce0 diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index cb97c8ee075..7d6b5471a2b 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -10,15 +10,13 @@ pip install Usage ===== -.. tabs:: +.. tab:: Unix/macOS - .. group-tab:: Unix/macOS + .. pip-command-usage:: install "python -m pip" - .. pip-command-usage:: install "python -m pip" +.. tab:: Windows - .. group-tab:: Windows - - .. pip-command-usage:: install "py -m pip" + .. pip-command-usage:: install "py -m pip" @@ -100,31 +98,29 @@ encountered member of the cycle is installed last. For instance, if quux depends on foo which depends on bar which depends on baz, which depends on foo: -.. tabs:: - - .. group-tab:: Unix/macOS +.. tab:: Unix/macOS - .. code-block:: console + .. code-block:: console - $ python -m pip install quux - ... - Installing collected packages baz, bar, foo, quux + $ python -m pip install quux + ... + Installing collected packages baz, bar, foo, quux - $ python -m pip install bar - ... - Installing collected packages foo, baz, bar + $ python -m pip install bar + ... + Installing collected packages foo, baz, bar - .. group-tab:: Windows +.. tab:: Windows - .. code-block:: console + .. code-block:: console - C:\> py -m pip install quux - ... - Installing collected packages baz, bar, foo, quux + C:\> py -m pip install quux + ... + Installing collected packages baz, bar, foo, quux - C:\> py -m pip install bar - ... - Installing collected packages foo, baz, bar + C:\> py -m pip install bar + ... + Installing collected packages foo, baz, bar Prior to v6.1.0, pip made no commitments about install order. @@ -416,19 +412,17 @@ If your repository layout is:: Then, to install from this repository, the syntax would be: -.. tabs:: +.. tab:: Unix/macOS - .. group-tab:: Unix/macOS + .. code-block:: shell - .. code-block:: shell + python -m pip install -e "vcs+protocol://repo_url/#egg=pkg&subdirectory=pkg_dir" - python -m pip install -e "vcs+protocol://repo_url/#egg=pkg&subdirectory=pkg_dir" +.. tab:: Windows - .. group-tab:: Windows - - .. code-block:: shell + .. code-block:: shell - py -m pip install -e "vcs+protocol://repo_url/#egg=pkg&subdirectory=pkg_dir" + py -m pip install -e "vcs+protocol://repo_url/#egg=pkg&subdirectory=pkg_dir" Git @@ -677,35 +671,33 @@ against any requirement not only checks that hash but also activates a global Hash-checking mode can be forced on with the ``--require-hashes`` command-line option: -.. tabs:: +.. tab:: Unix/macOS - .. group-tab:: Unix/macOS + .. code-block:: console - .. code-block:: console + $ python -m pip install --require-hashes -r requirements.txt + ... + Hashes are required in --require-hashes mode (implicitly on when a hash is + specified for any package). These requirements were missing hashes, + leaving them open to tampering. These are the hashes the downloaded + archives actually had. You can add lines like these to your requirements + files to prevent tampering. + pyelasticsearch==1.0 --hash=sha256:44ddfb1225054d7d6b1d02e9338e7d4809be94edbe9929a2ec0807d38df993fa + more-itertools==2.2 --hash=sha256:93e62e05c7ad3da1a233def6731e8285156701e3419a5fe279017c429ec67ce0 - $ python -m pip install --require-hashes -r requirements.txt - ... - Hashes are required in --require-hashes mode (implicitly on when a hash is - specified for any package). These requirements were missing hashes, - leaving them open to tampering. These are the hashes the downloaded - archives actually had. You can add lines like these to your requirements - files to prevent tampering. - pyelasticsearch==1.0 --hash=sha256:44ddfb1225054d7d6b1d02e9338e7d4809be94edbe9929a2ec0807d38df993fa - more-itertools==2.2 --hash=sha256:93e62e05c7ad3da1a233def6731e8285156701e3419a5fe279017c429ec67ce0 +.. tab:: Windows - .. group-tab:: Windows + .. code-block:: console - .. code-block:: console - - C:\> py -m pip install --require-hashes -r requirements.txt - ... - Hashes are required in --require-hashes mode (implicitly on when a hash is - specified for any package). These requirements were missing hashes, - leaving them open to tampering. These are the hashes the downloaded - archives actually had. You can add lines like these to your requirements - files to prevent tampering. - pyelasticsearch==1.0 --hash=sha256:44ddfb1225054d7d6b1d02e9338e7d4809be94edbe9929a2ec0807d38df993fa - more-itertools==2.2 --hash=sha256:93e62e05c7ad3da1a233def6731e8285156701e3419a5fe279017c429ec67ce0 + C:\> py -m pip install --require-hashes -r requirements.txt + ... + Hashes are required in --require-hashes mode (implicitly on when a hash is + specified for any package). These requirements were missing hashes, + leaving them open to tampering. These are the hashes the downloaded + archives actually had. You can add lines like these to your requirements + files to prevent tampering. + pyelasticsearch==1.0 --hash=sha256:44ddfb1225054d7d6b1d02e9338e7d4809be94edbe9929a2ec0807d38df993fa + more-itertools==2.2 --hash=sha256:93e62e05c7ad3da1a233def6731e8285156701e3419a5fe279017c429ec67ce0 This can be useful in deploy scripts, to ensure that the author of the @@ -736,54 +728,50 @@ Hash-checking mode also works with :ref:`pip download` and :ref:`pip wheel`. A .. warning:: - Beware of the ``setup_requires`` keyword arg in :file:`setup.py`. The - (rare) packages that use it will cause those dependencies to be downloaded - by setuptools directly, skipping pip's hash-checking. If you need to use - such a package, see :ref:`Controlling - setup_requires<controlling-setup-requires>`. + Beware of the ``setup_requires`` keyword arg in :file:`setup.py`. The + (rare) packages that use it will cause those dependencies to be downloaded + by setuptools directly, skipping pip's hash-checking. If you need to use + such a package, see :ref:`Controlling + setup_requires<controlling-setup-requires>`. .. warning:: - Be careful not to nullify all your security work when you install your - actual project by using setuptools directly: for example, by calling - ``python setup.py install``, ``python setup.py develop``, or - ``easy_install``. Setuptools will happily go out and download, unchecked, - anything you missed in your requirements file—and it’s easy to miss things - as your project evolves. To be safe, install your project using pip and - :ref:`--no-deps <install_--no-deps>`. - - Instead of ``python setup.py develop``, use... + Be careful not to nullify all your security work when you install your + actual project by using setuptools directly: for example, by calling + ``python setup.py install``, ``python setup.py develop``, or + ``easy_install``. Setuptools will happily go out and download, unchecked, + anything you missed in your requirements file—and it’s easy to miss things + as your project evolves. To be safe, install your project using pip and + :ref:`--no-deps <install_--no-deps>`. - .. tabs:: + Instead of ``python setup.py develop``, use... - .. group-tab:: Unix/macOS + .. tab:: Unix/macOS - .. code-block:: shell - - python -m pip install --no-deps -e . + .. code-block:: shell - .. group-tab:: Windows + python -m pip install --no-deps -e . - .. code-block:: shell + .. tab:: Windows - py -m pip install --no-deps -e . + .. code-block:: shell + py -m pip install --no-deps -e . - Instead of ``python setup.py install``, use... - .. tabs:: + Instead of ``python setup.py install``, use... - .. group-tab:: Unix/macOS + .. tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python -m pip install --no-deps . + python -m pip install --no-deps . - .. group-tab:: Windows + .. tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip install --no-deps . + py -m pip install --no-deps . Hashes from PyPI ^^^^^^^^^^^^^^^^ @@ -803,20 +791,17 @@ Local project installs pip supports installing local project in both regular mode and editable mode. You can install local projects by specifying the project path to pip: -.. tabs:: +.. tab:: Unix/macOS - .. group-tab:: Unix/macOS + .. code-block:: shell - .. code-block:: shell + python -m pip install path/to/SomeProject - python -m pip install path/to/SomeProject +.. tab:: Windows - .. group-tab:: Windows - - .. code-block:: shell - - py -m pip install path/to/SomeProject + .. code-block:: shell + py -m pip install path/to/SomeProject During regular installation, pip will copy the entire project directory to a temporary location and install from there. The exception is that pip will @@ -835,21 +820,19 @@ installs. You can install local projects or VCS projects in "editable" mode: -.. tabs:: +.. tab:: Unix/macOS - .. group-tab:: Unix/macOS + .. code-block:: shell - .. code-block:: shell + python -m pip install -e path/to/SomeProject + python -m pip install -e git+http://repo/my_project.git#egg=SomeProject - python -m pip install -e path/to/SomeProject - python -m pip install -e git+http://repo/my_project.git#egg=SomeProject +.. tab:: Windows - .. group-tab:: Windows + .. code-block:: shell - .. code-block:: shell - - py -m pip install -e path/to/SomeProject - py -m pip install -e git+http://repo/my_project.git#egg=SomeProject + py -m pip install -e path/to/SomeProject + py -m pip install -e git+http://repo/my_project.git#egg=SomeProject (See the :ref:`VCS Support` section above for more information on VCS-related syntax.) @@ -957,292 +940,256 @@ Examples #. Install ``SomePackage`` and its dependencies from `PyPI`_ using :ref:`Requirement Specifiers` - .. tabs:: - - .. group-tab:: Unix/macOS + .. tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python -m pip install SomePackage # latest version - python -m pip install SomePackage==1.0.4 # specific version - python -m pip install 'SomePackage>=1.0.4' # minimum version + python -m pip install SomePackage # latest version + python -m pip install SomePackage==1.0.4 # specific version + python -m pip install 'SomePackage>=1.0.4' # minimum version - .. group-tab:: Windows + .. tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip install SomePackage # latest version - py -m pip install SomePackage==1.0.4 # specific version - py -m pip install 'SomePackage>=1.0.4' # minimum version + py -m pip install SomePackage # latest version + py -m pip install SomePackage==1.0.4 # specific version + py -m pip install 'SomePackage>=1.0.4' # minimum version #. Install a list of requirements specified in a file. See the :ref:`Requirements files <Requirements Files>`. - .. tabs:: - - .. group-tab:: Unix/macOS + .. tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python -m pip install -r requirements.txt + python -m pip install -r requirements.txt - .. group-tab:: Windows + .. tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip install -r requirements.txt + py -m pip install -r requirements.txt #. Upgrade an already installed ``SomePackage`` to the latest from PyPI. - .. tabs:: + .. tab:: Unix/macOS - .. group-tab:: Unix/macOS - - .. code-block:: shell + .. code-block:: shell - python -m pip install --upgrade SomePackage + python -m pip install --upgrade SomePackage - .. group-tab:: Windows + .. tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip install --upgrade SomePackage + py -m pip install --upgrade SomePackage #. Install a local project in "editable" mode. See the section on :ref:`Editable Installs <editable-installs>`. - .. tabs:: - - .. group-tab:: Unix/macOS + .. tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python -m pip install -e . # project in current directory - python -m pip install -e path/to/project # project in another directory + python -m pip install -e . # project in current directory + python -m pip install -e path/to/project # project in another directory - .. group-tab:: Windows + .. tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip install -e . # project in current directory - py -m pip install -e path/to/project # project in another directory + py -m pip install -e . # project in current directory + py -m pip install -e path/to/project # project in another directory #. Install a project from VCS - .. tabs:: + .. tab:: Unix/macOS - .. group-tab:: Unix/macOS - - .. code-block:: shell + .. code-block:: shell - python -m pip install SomeProject@git+https://git.repo/some_pkg.git@1.3.1 + python -m pip install SomeProject@git+https://git.repo/some_pkg.git@1.3.1 - .. group-tab:: Windows + .. tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip install SomeProject@git+https://git.repo/some_pkg.git@1.3.1 + py -m pip install SomeProject@git+https://git.repo/some_pkg.git@1.3.1 #. Install a project from VCS in "editable" mode. See the sections on :ref:`VCS Support <VCS Support>` and :ref:`Editable Installs <editable-installs>`. - .. tabs:: - - .. group-tab:: Unix/macOS + .. tab:: Unix/macOS - .. code-block:: shell - - python -m pip install -e git+https://git.repo/some_pkg.git#egg=SomePackage # from git - python -m pip install -e hg+https://hg.repo/some_pkg.git#egg=SomePackage # from mercurial - python -m python -m pip install -e svn+svn://svn.repo/some_pkg/trunk/#egg=SomePackage # from svn - python -m pip install -e git+https://git.repo/some_pkg.git@feature#egg=SomePackage # from 'feature' branch - python -m pip install -e "git+https://git.repo/some_repo.git#egg=subdir&subdirectory=subdir_path" # install a python package from a repo subdirectory + .. code-block:: shell - .. group-tab:: Windows + python -m pip install -e git+https://git.repo/some_pkg.git#egg=SomePackage # from git + python -m pip install -e hg+https://hg.repo/some_pkg.git#egg=SomePackage # from mercurial + python -m python -m pip install -e svn+svn://svn.repo/some_pkg/trunk/#egg=SomePackage # from svn + python -m pip install -e git+https://git.repo/some_pkg.git@feature#egg=SomePackage # from 'feature' branch + python -m pip install -e "git+https://git.repo/some_repo.git#egg=subdir&subdirectory=subdir_path" # install a python package from a repo subdirectory - .. code-block:: shell + .. tab:: Windows - py -m pip install -e git+https://git.repo/some_pkg.git#egg=SomePackage # from git - py -m pip install -e hg+https://hg.repo/some_pkg.git#egg=SomePackage # from mercurial - py -m pip install -e svn+svn://svn.repo/some_pkg/trunk/#egg=SomePackage # from svn - py -m pip install -e git+https://git.repo/some_pkg.git@feature#egg=SomePackage # from 'feature' branch - py -m pip install -e "git+https://git.repo/some_repo.git#egg=subdir&subdirectory=subdir_path" # install a python package from a repo subdirectory + .. code-block:: shell + py -m pip install -e git+https://git.repo/some_pkg.git#egg=SomePackage # from git + py -m pip install -e hg+https://hg.repo/some_pkg.git#egg=SomePackage # from mercurial + py -m pip install -e svn+svn://svn.repo/some_pkg/trunk/#egg=SomePackage # from svn + py -m pip install -e git+https://git.repo/some_pkg.git@feature#egg=SomePackage # from 'feature' branch + py -m pip install -e "git+https://git.repo/some_repo.git#egg=subdir&subdirectory=subdir_path" # install a python package from a repo subdirectory #. Install a package with `setuptools extras`_. - .. tabs:: - - .. group-tab:: Unix/macOS - - .. code-block:: shell + .. tab:: Unix/macOS - python -m pip install SomePackage[PDF] - python -m pip install "SomePackage[PDF] @ git+https://git.repo/SomePackage@master#subdirectory=subdir_path" - python -m pip install .[PDF] # project in current directory - python -m pip install SomePackage[PDF]==3.0 - python -m pip install SomePackage[PDF,EPUB] # multiple extras + .. code-block:: shell - .. group-tab:: Windows + python -m pip install SomePackage[PDF] + python -m pip install "SomePackage[PDF] @ git+https://git.repo/SomePackage@master#subdirectory=subdir_path" + python -m pip install .[PDF] # project in current directory + python -m pip install SomePackage[PDF]==3.0 + python -m pip install SomePackage[PDF,EPUB] # multiple extras - .. code-block:: shell + .. tab:: Windows - py -m pip install SomePackage[PDF] - py -m pip install "SomePackage[PDF] @ git+https://git.repo/SomePackage@master#subdirectory=subdir_path" - py -m pip install .[PDF] # project in current directory - py -m pip install SomePackage[PDF]==3.0 - py -m pip install SomePackage[PDF,EPUB] # multiple extras + .. code-block:: shell + py -m pip install SomePackage[PDF] + py -m pip install "SomePackage[PDF] @ git+https://git.repo/SomePackage@master#subdirectory=subdir_path" + py -m pip install .[PDF] # project in current directory + py -m pip install SomePackage[PDF]==3.0 + py -m pip install SomePackage[PDF,EPUB] # multiple extras #. Install a particular source archive file. - .. tabs:: - - .. group-tab:: Unix/macOS - - .. code-block:: shell + .. tab:: Unix/macOS - python -m pip install ./downloads/SomePackage-1.0.4.tar.gz - python -m pip install http://my.package.repo/SomePackage-1.0.4.zip + .. code-block:: shell - .. group-tab:: Windows + python -m pip install ./downloads/SomePackage-1.0.4.tar.gz + python -m pip install http://my.package.repo/SomePackage-1.0.4.zip - .. code-block:: shell + .. tab:: Windows - py -m pip install ./downloads/SomePackage-1.0.4.tar.gz - py -m pip install http://my.package.repo/SomePackage-1.0.4.zip + .. code-block:: shell + py -m pip install ./downloads/SomePackage-1.0.4.tar.gz + py -m pip install http://my.package.repo/SomePackage-1.0.4.zip #. Install a particular source archive file following :pep:`440` direct references. - .. tabs:: - - .. group-tab:: Unix/macOS + .. tab:: Unix/macOS - .. code-block:: shell - - python -m pip install SomeProject@http://my.package.repo/SomeProject-1.2.3-py33-none-any.whl - python -m pip install "SomeProject @ http://my.package.repo/SomeProject-1.2.3-py33-none-any.whl" - python -m pip install SomeProject@http://my.package.repo/1.2.3.tar.gz + .. code-block:: shell - .. group-tab:: Windows + python -m pip install SomeProject@http://my.package.repo/SomeProject-1.2.3-py33-none-any.whl + python -m pip install "SomeProject @ http://my.package.repo/SomeProject-1.2.3-py33-none-any.whl" + python -m pip install SomeProject@http://my.package.repo/1.2.3.tar.gz - .. code-block:: shell + .. tab:: Windows - py -m pip install SomeProject@http://my.package.repo/SomeProject-1.2.3-py33-none-any.whl - py -m pip install "SomeProject @ http://my.package.repo/SomeProject-1.2.3-py33-none-any.whl" - py -m pip install SomeProject@http://my.package.repo/1.2.3.tar.gz + .. code-block:: shell + py -m pip install SomeProject@http://my.package.repo/SomeProject-1.2.3-py33-none-any.whl + py -m pip install "SomeProject @ http://my.package.repo/SomeProject-1.2.3-py33-none-any.whl" + py -m pip install SomeProject@http://my.package.repo/1.2.3.tar.gz #. Install from alternative package repositories. Install from a different index, and not `PyPI`_ - .. tabs:: - - .. group-tab:: Unix/macOS + .. tab:: Unix/macOS - .. code-block:: shell - - python -m pip install --index-url http://my.package.repo/simple/ SomePackage + .. code-block:: shell - .. group-tab:: Windows + python -m pip install --index-url http://my.package.repo/simple/ SomePackage - .. code-block:: shell + .. tab:: Windows - py -m pip install --index-url http://my.package.repo/simple/ SomePackage + .. code-block:: shell + py -m pip install --index-url http://my.package.repo/simple/ SomePackage Search an additional index during install, in addition to `PyPI`_ - .. tabs:: - - .. group-tab:: Unix/macOS - - .. code-block:: shell + .. tab:: Unix/macOS - python -m pip install --extra-index-url http://my.package.repo/simple SomePackage + .. code-block:: shell - .. group-tab:: Windows + python -m pip install --extra-index-url http://my.package.repo/simple SomePackage - .. code-block:: shell + .. tab:: Windows - py -m pip install --extra-index-url http://my.package.repo/simple SomePackage + .. code-block:: shell + py -m pip install --extra-index-url http://my.package.repo/simple SomePackage Install from a local flat directory containing archives (and don't scan indexes): - .. tabs:: + .. tab:: Unix/macOS - .. group-tab:: Unix/macOS - - .. code-block:: shell + .. code-block:: shell - python -m pip install --no-index --find-links=file:///local/dir/ SomePackage - python -m pip install --no-index --find-links=/local/dir/ SomePackage - python -m pip install --no-index --find-links=relative/dir/ SomePackage + python -m pip install --no-index --find-links=file:///local/dir/ SomePackage + python -m pip install --no-index --find-links=/local/dir/ SomePackage + python -m pip install --no-index --find-links=relative/dir/ SomePackage - .. group-tab:: Windows + .. tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip install --no-index --find-links=file:///local/dir/ SomePackage - py -m pip install --no-index --find-links=/local/dir/ SomePackage - py -m pip install --no-index --find-links=relative/dir/ SomePackage + py -m pip install --no-index --find-links=file:///local/dir/ SomePackage + py -m pip install --no-index --find-links=/local/dir/ SomePackage + py -m pip install --no-index --find-links=relative/dir/ SomePackage #. Find pre-release and development versions, in addition to stable versions. By default, pip only finds stable versions. - .. tabs:: - - .. group-tab:: Unix/macOS + .. tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python -m pip install --pre SomePackage + python -m pip install --pre SomePackage - .. group-tab:: Windows + .. tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip install --pre SomePackage + py -m pip install --pre SomePackage #. Install packages from source. Do not use any binary packages - .. tabs:: + .. tab:: Unix/macOS - .. group-tab:: Unix/macOS - - .. code-block:: shell + .. code-block:: shell - python -m pip install SomePackage1 SomePackage2 --no-binary :all: + python -m pip install SomePackage1 SomePackage2 --no-binary :all: - .. group-tab:: Windows + .. tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip install SomePackage1 SomePackage2 --no-binary :all: + py -m pip install SomePackage1 SomePackage2 --no-binary :all: Specify ``SomePackage1`` to be installed from source: - .. tabs:: - - .. group-tab:: Unix/macOS + .. tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python -m pip install SomePackage1 SomePackage2 --no-binary SomePackage1 + python -m pip install SomePackage1 SomePackage2 --no-binary SomePackage1 - .. group-tab:: Windows + .. tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip install SomePackage1 SomePackage2 --no-binary SomePackage1 + py -m pip install SomePackage1 SomePackage2 --no-binary SomePackage1 ---- diff --git a/docs/html/reference/pip_list.rst b/docs/html/reference/pip_list.rst index 1489ed751a1..bda322a8631 100644 --- a/docs/html/reference/pip_list.rst +++ b/docs/html/reference/pip_list.rst @@ -10,15 +10,13 @@ pip list Usage ===== -.. tabs:: +.. tab:: Unix/macOS - .. group-tab:: Unix/macOS + .. pip-command-usage:: list "python -m pip" - .. pip-command-usage:: list "python -m pip" +.. tab:: Windows - .. group-tab:: Windows - - .. pip-command-usage:: list "py -m pip" + .. pip-command-usage:: list "py -m pip" Description @@ -40,182 +38,165 @@ Examples #. List installed packages. - .. tabs:: - - .. group-tab:: Unix/macOS + .. tab:: Unix/macOS - .. code-block:: console + .. code-block:: console - $ python -m pip list - docutils (0.10) - Jinja2 (2.7.2) - MarkupSafe (0.18) - Pygments (1.6) - Sphinx (1.2.1) + $ python -m pip list + docutils (0.10) + Jinja2 (2.7.2) + MarkupSafe (0.18) + Pygments (1.6) + Sphinx (1.2.1) - .. group-tab:: Windows + .. tab:: Windows - .. code-block:: console + .. code-block:: console - C:\> py -m pip list - docutils (0.10) - Jinja2 (2.7.2) - MarkupSafe (0.18) - Pygments (1.6) - Sphinx (1.2.1) + C:\> py -m pip list + docutils (0.10) + Jinja2 (2.7.2) + MarkupSafe (0.18) + Pygments (1.6) + Sphinx (1.2.1) #. List outdated packages (excluding editables), and the latest version available. - .. tabs:: - - .. group-tab:: Unix/macOS + .. tab:: Unix/macOS - .. code-block:: console + .. code-block:: console - $ python -m pip list --outdated - docutils (Current: 0.10 Latest: 0.11) - Sphinx (Current: 1.2.1 Latest: 1.2.2) + $ python -m pip list --outdated + docutils (Current: 0.10 Latest: 0.11) + Sphinx (Current: 1.2.1 Latest: 1.2.2) - .. group-tab:: Windows + .. tab:: Windows - .. code-block:: console - - C:\> py -m pip list --outdated - docutils (Current: 0.10 Latest: 0.11) - Sphinx (Current: 1.2.1 Latest: 1.2.2) + .. code-block:: console + C:\> py -m pip list --outdated + docutils (Current: 0.10 Latest: 0.11) + Sphinx (Current: 1.2.1 Latest: 1.2.2) #. List installed packages with column formatting. - .. tabs:: - - .. group-tab:: Unix/macOS + .. tab:: Unix/macOS - .. code-block:: console + .. code-block:: console - $ python -m pip list --format columns - Package Version - ------- ------- - docopt 0.6.2 - idlex 1.13 - jedi 0.9.0 + $ python -m pip list --format columns + Package Version + ------- ------- + docopt 0.6.2 + idlex 1.13 + jedi 0.9.0 - .. group-tab:: Windows + .. tab:: Windows - .. code-block:: console + .. code-block:: console - C:\> py -m pip list --format columns - Package Version - ------- ------- - docopt 0.6.2 - idlex 1.13 - jedi 0.9.0 + C:\> py -m pip list --format columns + Package Version + ------- ------- + docopt 0.6.2 + idlex 1.13 + jedi 0.9.0 #. List outdated packages with column formatting. - .. tabs:: + .. tab:: Unix/macOS - .. group-tab:: Unix/macOS + .. code-block:: console - .. code-block:: console + $ python -m pip list -o --format columns + Package Version Latest Type + ---------- ------- ------ ----- + retry 0.8.1 0.9.1 wheel + setuptools 20.6.7 21.0.0 wheel - $ python -m pip list -o --format columns - Package Version Latest Type - ---------- ------- ------ ----- - retry 0.8.1 0.9.1 wheel - setuptools 20.6.7 21.0.0 wheel + .. tab:: Windows - .. group-tab:: Windows + .. code-block:: console - .. code-block:: console - - C:\> py -m pip list -o --format columns - Package Version Latest Type - ---------- ------- ------ ----- - retry 0.8.1 0.9.1 wheel - setuptools 20.6.7 21.0.0 wheel + C:\> py -m pip list -o --format columns + Package Version Latest Type + ---------- ------- ------ ----- + retry 0.8.1 0.9.1 wheel + setuptools 20.6.7 21.0.0 wheel #. List packages that are not dependencies of other packages. Can be combined with other options. - .. tabs:: - - .. group-tab:: Unix/macOS + .. tab:: Unix/macOS - .. code-block:: console + .. code-block:: console - $ python -m pip list --outdated --not-required - docutils (Current: 0.10 Latest: 0.11) + $ python -m pip list --outdated --not-required + docutils (Current: 0.10 Latest: 0.11) - .. group-tab:: Windows + .. tab:: Windows - .. code-block:: console + .. code-block:: console - C:\> py -m pip list --outdated --not-required - docutils (Current: 0.10 Latest: 0.11) + C:\> py -m pip list --outdated --not-required + docutils (Current: 0.10 Latest: 0.11) #. Use legacy formatting - .. tabs:: - - .. group-tab:: Unix/macOS + .. tab:: Unix/macOS - .. code-block:: console + .. code-block:: console - $ python -m pip list --format=legacy - colorama (0.3.7) - docopt (0.6.2) - idlex (1.13) - jedi (0.9.0) + $ python -m pip list --format=legacy + colorama (0.3.7) + docopt (0.6.2) + idlex (1.13) + jedi (0.9.0) - .. group-tab:: Windows + .. tab:: Windows - .. code-block:: console + .. code-block:: console - C:\> py -m pip list --format=legacy - colorama (0.3.7) - docopt (0.6.2) - idlex (1.13) - jedi (0.9.0) + C:\> py -m pip list --format=legacy + colorama (0.3.7) + docopt (0.6.2) + idlex (1.13) + jedi (0.9.0) #. Use json formatting - .. tabs:: + .. tab:: Unix/macOS - .. group-tab:: Unix/macOS + .. code-block:: console - .. code-block:: console + $ python -m pip list --format=json + [{'name': 'colorama', 'version': '0.3.7'}, {'name': 'docopt', 'version': '0.6.2'}, ... - $ python -m pip list --format=json - [{'name': 'colorama', 'version': '0.3.7'}, {'name': 'docopt', 'version': '0.6.2'}, ... + .. tab:: Windows - .. group-tab:: Windows + .. code-block:: console - .. code-block:: console - - C:\> py -m pip list --format=json - [{'name': 'colorama', 'version': '0.3.7'}, {'name': 'docopt', 'version': '0.6.2'}, ... + C:\> py -m pip list --format=json + [{'name': 'colorama', 'version': '0.3.7'}, {'name': 'docopt', 'version': '0.6.2'}, ... #. Use freeze formatting - .. tabs:: - - .. group-tab:: Unix/macOS + .. tab:: Unix/macOS - .. code-block:: console + .. code-block:: console - $ python -m pip list --format=freeze - colorama==0.3.7 - docopt==0.6.2 - idlex==1.13 - jedi==0.9.0 + $ python -m pip list --format=freeze + colorama==0.3.7 + docopt==0.6.2 + idlex==1.13 + jedi==0.9.0 - .. group-tab:: Windows + .. tab:: Windows - .. code-block:: console + .. code-block:: console - C:\> py -m pip list --format=freeze - colorama==0.3.7 - docopt==0.6.2 - idlex==1.13 - jedi==0.9.0 + C:\> py -m pip list --format=freeze + colorama==0.3.7 + docopt==0.6.2 + idlex==1.13 + jedi==0.9.0 diff --git a/docs/html/reference/pip_search.rst b/docs/html/reference/pip_search.rst index fba62959316..2d1a2aa69ed 100644 --- a/docs/html/reference/pip_search.rst +++ b/docs/html/reference/pip_search.rst @@ -10,15 +10,13 @@ pip search Usage ===== -.. tabs:: +.. tab:: Unix/macOS - .. group-tab:: Unix/macOS + .. pip-command-usage:: search "python -m pip" - .. pip-command-usage:: search "python -m pip" +.. tab:: Windows - .. group-tab:: Windows - - .. pip-command-usage:: search "py -m pip" + .. pip-command-usage:: search "py -m pip" Description @@ -38,20 +36,18 @@ Examples #. Search for "peppercorn" - .. tabs:: - - .. group-tab:: Unix/macOS + .. tab:: Unix/macOS - .. code-block:: console + .. code-block:: console - $ python -m pip search peppercorn - pepperedform - Helpers for using peppercorn with formprocess. - peppercorn - A library for converting a token stream into [...] + $ python -m pip search peppercorn + pepperedform - Helpers for using peppercorn with formprocess. + peppercorn - A library for converting a token stream into [...] - .. group-tab:: Windows + .. tab:: Windows - .. code-block:: console + .. code-block:: console - C:\> py -m pip search peppercorn - pepperedform - Helpers for using peppercorn with formprocess. - peppercorn - A library for converting a token stream into [...] + C:\> py -m pip search peppercorn + pepperedform - Helpers for using peppercorn with formprocess. + peppercorn - A library for converting a token stream into [...] diff --git a/docs/html/reference/pip_show.rst b/docs/html/reference/pip_show.rst index 6bd3718b95d..bcbe4e82067 100644 --- a/docs/html/reference/pip_show.rst +++ b/docs/html/reference/pip_show.rst @@ -10,15 +10,13 @@ pip show Usage ===== -.. tabs:: +.. tab:: Unix/macOS - .. group-tab:: Unix/macOS + .. pip-command-usage:: show "python -m pip" - .. pip-command-usage:: show "python -m pip" +.. tab:: Windows - .. group-tab:: Windows - - .. pip-command-usage:: show "py -m pip" + .. pip-command-usage:: show "py -m pip" Description @@ -38,124 +36,120 @@ Examples #. Show information about a package: - .. tabs:: - - .. group-tab:: Unix/macOS + .. tab:: Unix/macOS - .. code-block:: console + .. code-block:: console - $ python -m pip show sphinx - Name: Sphinx - Version: 1.4.5 - Summary: Python documentation generator - Home-page: http://sphinx-doc.org/ - Author: Georg Brandl - Author-email: georg@python.org - License: BSD - Location: /my/env/lib/python2.7/site-packages - Requires: docutils, snowballstemmer, alabaster, Pygments, imagesize, Jinja2, babel, six + $ python -m pip show sphinx + Name: Sphinx + Version: 1.4.5 + Summary: Python documentation generator + Home-page: http://sphinx-doc.org/ + Author: Georg Brandl + Author-email: georg@python.org + License: BSD + Location: /my/env/lib/python2.7/site-packages + Requires: docutils, snowballstemmer, alabaster, Pygments, imagesize, Jinja2, babel, six - .. group-tab:: Windows + .. tab:: Windows - .. code-block:: console + .. code-block:: console - C:\> py -m pip show sphinx - Name: Sphinx - Version: 1.4.5 - Summary: Python documentation generator - Home-page: http://sphinx-doc.org/ - Author: Georg Brandl - Author-email: georg@python.org - License: BSD - Location: /my/env/lib/python2.7/site-packages - Requires: docutils, snowballstemmer, alabaster, Pygments, imagesize, Jinja2, babel, six + C:\> py -m pip show sphinx + Name: Sphinx + Version: 1.4.5 + Summary: Python documentation generator + Home-page: http://sphinx-doc.org/ + Author: Georg Brandl + Author-email: georg@python.org + License: BSD + Location: /my/env/lib/python2.7/site-packages + Requires: docutils, snowballstemmer, alabaster, Pygments, imagesize, Jinja2, babel, six #. Show all information about a package - .. tabs:: - - .. group-tab:: Unix/macOS - - .. code-block:: console - - $ python -m pip show --verbose sphinx - Name: Sphinx - Version: 1.4.5 - Summary: Python documentation generator - Home-page: http://sphinx-doc.org/ - Author: Georg Brandl - Author-email: georg@python.org - License: BSD - Location: /my/env/lib/python2.7/site-packages - Requires: docutils, snowballstemmer, alabaster, Pygments, imagesize, Jinja2, babel, six - Metadata-Version: 2.0 - Installer: - Classifiers: - Development Status :: 5 - Production/Stable - Environment :: Console - Environment :: Web Environment - Intended Audience :: Developers - Intended Audience :: Education - License :: OSI Approved :: BSD License - Operating System :: OS Independent - Programming Language :: Python - Programming Language :: Python :: 2 - Programming Language :: Python :: 3 - Framework :: Sphinx - Framework :: Sphinx :: Extension - Framework :: Sphinx :: Theme - Topic :: Documentation - Topic :: Documentation :: Sphinx - Topic :: Text Processing - Topic :: Utilities - Entry-points: - [console_scripts] - sphinx-apidoc = sphinx.apidoc:main - sphinx-autogen = sphinx.ext.autosummary.generate:main - sphinx-build = sphinx:main - sphinx-quickstart = sphinx.quickstart:main - [distutils.commands] - build_sphinx = sphinx.setup_command:BuildDoc - - .. group-tab:: Windows - - .. code-block:: console - - C:\> py -m pip show --verbose sphinx - Name: Sphinx - Version: 1.4.5 - Summary: Python documentation generator - Home-page: http://sphinx-doc.org/ - Author: Georg Brandl - Author-email: georg@python.org - License: BSD - Location: /my/env/lib/python2.7/site-packages - Requires: docutils, snowballstemmer, alabaster, Pygments, imagesize, Jinja2, babel, six - Metadata-Version: 2.0 - Installer: - Classifiers: - Development Status :: 5 - Production/Stable - Environment :: Console - Environment :: Web Environment - Intended Audience :: Developers - Intended Audience :: Education - License :: OSI Approved :: BSD License - Operating System :: OS Independent - Programming Language :: Python - Programming Language :: Python :: 2 - Programming Language :: Python :: 3 - Framework :: Sphinx - Framework :: Sphinx :: Extension - Framework :: Sphinx :: Theme - Topic :: Documentation - Topic :: Documentation :: Sphinx - Topic :: Text Processing - Topic :: Utilities - Entry-points: - [console_scripts] - sphinx-apidoc = sphinx.apidoc:main - sphinx-autogen = sphinx.ext.autosummary.generate:main - sphinx-build = sphinx:main - sphinx-quickstart = sphinx.quickstart:main - [distutils.commands] - build_sphinx = sphinx.setup_command:BuildDoc + .. tab:: Unix/macOS + + .. code-block:: console + + $ python -m pip show --verbose sphinx + Name: Sphinx + Version: 1.4.5 + Summary: Python documentation generator + Home-page: http://sphinx-doc.org/ + Author: Georg Brandl + Author-email: georg@python.org + License: BSD + Location: /my/env/lib/python2.7/site-packages + Requires: docutils, snowballstemmer, alabaster, Pygments, imagesize, Jinja2, babel, six + Metadata-Version: 2.0 + Installer: + Classifiers: + Development Status :: 5 - Production/Stable + Environment :: Console + Environment :: Web Environment + Intended Audience :: Developers + Intended Audience :: Education + License :: OSI Approved :: BSD License + Operating System :: OS Independent + Programming Language :: Python + Programming Language :: Python :: 2 + Programming Language :: Python :: 3 + Framework :: Sphinx + Framework :: Sphinx :: Extension + Framework :: Sphinx :: Theme + Topic :: Documentation + Topic :: Documentation :: Sphinx + Topic :: Text Processing + Topic :: Utilities + Entry-points: + [console_scripts] + sphinx-apidoc = sphinx.apidoc:main + sphinx-autogen = sphinx.ext.autosummary.generate:main + sphinx-build = sphinx:main + sphinx-quickstart = sphinx.quickstart:main + [distutils.commands] + build_sphinx = sphinx.setup_command:BuildDoc + + .. tab:: Windows + + .. code-block:: console + + C:\> py -m pip show --verbose sphinx + Name: Sphinx + Version: 1.4.5 + Summary: Python documentation generator + Home-page: http://sphinx-doc.org/ + Author: Georg Brandl + Author-email: georg@python.org + License: BSD + Location: /my/env/lib/python2.7/site-packages + Requires: docutils, snowballstemmer, alabaster, Pygments, imagesize, Jinja2, babel, six + Metadata-Version: 2.0 + Installer: + Classifiers: + Development Status :: 5 - Production/Stable + Environment :: Console + Environment :: Web Environment + Intended Audience :: Developers + Intended Audience :: Education + License :: OSI Approved :: BSD License + Operating System :: OS Independent + Programming Language :: Python + Programming Language :: Python :: 2 + Programming Language :: Python :: 3 + Framework :: Sphinx + Framework :: Sphinx :: Extension + Framework :: Sphinx :: Theme + Topic :: Documentation + Topic :: Documentation :: Sphinx + Topic :: Text Processing + Topic :: Utilities + Entry-points: + [console_scripts] + sphinx-apidoc = sphinx.apidoc:main + sphinx-autogen = sphinx.ext.autosummary.generate:main + sphinx-build = sphinx:main + sphinx-quickstart = sphinx.quickstart:main + [distutils.commands] + build_sphinx = sphinx.setup_command:BuildDoc diff --git a/docs/html/reference/pip_uninstall.rst b/docs/html/reference/pip_uninstall.rst index 8b31c5673c1..fbbeddd45c8 100644 --- a/docs/html/reference/pip_uninstall.rst +++ b/docs/html/reference/pip_uninstall.rst @@ -10,15 +10,13 @@ pip uninstall Usage ===== -.. tabs:: +.. tab:: Unix/macOS - .. group-tab:: Unix/macOS + .. pip-command-usage:: uninstall "python -m pip" - .. pip-command-usage:: uninstall "python -m pip" +.. tab:: Windows - .. group-tab:: Windows - - .. pip-command-usage:: uninstall "py -m pip" + .. pip-command-usage:: uninstall "py -m pip" Description @@ -38,26 +36,24 @@ Examples #. Uninstall a package. - .. tabs:: - - .. group-tab:: Unix/macOS + .. tab:: Unix/macOS - .. code-block:: console + .. code-block:: console - $ python -m pip uninstall simplejson - Uninstalling simplejson: - /home/me/env/lib/python2.7/site-packages/simplejson - /home/me/env/lib/python2.7/site-packages/simplejson-2.2.1-py2.7.egg-info - Proceed (y/n)? y - Successfully uninstalled simplejson + $ python -m pip uninstall simplejson + Uninstalling simplejson: + /home/me/env/lib/python2.7/site-packages/simplejson + /home/me/env/lib/python2.7/site-packages/simplejson-2.2.1-py2.7.egg-info + Proceed (y/n)? y + Successfully uninstalled simplejson - .. group-tab:: Windows + .. tab:: Windows - .. code-block:: console + .. code-block:: console - C:\> py -m pip uninstall simplejson - Uninstalling simplejson: - /home/me/env/lib/python2.7/site-packages/simplejson - /home/me/env/lib/python2.7/site-packages/simplejson-2.2.1-py2.7.egg-info - Proceed (y/n)? y - Successfully uninstalled simplejson + C:\> py -m pip uninstall simplejson + Uninstalling simplejson: + /home/me/env/lib/python2.7/site-packages/simplejson + /home/me/env/lib/python2.7/site-packages/simplejson-2.2.1-py2.7.egg-info + Proceed (y/n)? y + Successfully uninstalled simplejson diff --git a/docs/html/reference/pip_wheel.rst b/docs/html/reference/pip_wheel.rst index c1bdf37f852..f6430bfed52 100644 --- a/docs/html/reference/pip_wheel.rst +++ b/docs/html/reference/pip_wheel.rst @@ -11,15 +11,13 @@ pip wheel Usage ===== -.. tabs:: +.. tab:: Unix/macOS - .. group-tab:: Unix/macOS + .. pip-command-usage:: wheel "python -m pip" - .. pip-command-usage:: wheel "python -m pip" +.. tab:: Windows - .. group-tab:: Windows - - .. pip-command-usage:: wheel "py -m pip" + .. pip-command-usage:: wheel "py -m pip" Description @@ -34,19 +32,17 @@ Build System Interface In order for pip to build a wheel, ``setup.py`` must implement the ``bdist_wheel`` command with the following syntax: -.. tabs:: - - .. group-tab:: Unix/macOS +.. tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python setup.py bdist_wheel -d TARGET + python setup.py bdist_wheel -d TARGET - .. group-tab:: Windows +.. tab:: Windows - .. code-block:: shell + .. code-block:: shell - py setup.py bdist_wheel -d TARGET + py setup.py bdist_wheel -d TARGET This command must create a wheel compatible with the invoking Python @@ -62,19 +58,17 @@ with their arguments in the ``setup.py`` command. This is currently the only way to influence the building of C extensions from the command line. For example: -.. tabs:: +.. tab:: Unix/macOS - .. group-tab:: Unix/macOS + .. code-block:: shell - .. code-block:: shell - - python -m pip wheel --global-option bdist_ext --global-option -DFOO wheel + python -m pip wheel --global-option bdist_ext --global-option -DFOO wheel - .. group-tab:: Windows +.. tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip wheel --global-option bdist_ext --global-option -DFOO wheel + py -m pip wheel --global-option bdist_ext --global-option -DFOO wheel will result in a build command of @@ -103,34 +97,30 @@ Examples #. Build wheels for a requirement (and all its dependencies), and then install - .. tabs:: + .. tab:: Unix/macOS - .. group-tab:: Unix/macOS - - .. code-block:: shell + .. code-block:: shell - python -m pip wheel --wheel-dir=/tmp/wheelhouse SomePackage - python -m pip install --no-index --find-links=/tmp/wheelhouse SomePackage + python -m pip wheel --wheel-dir=/tmp/wheelhouse SomePackage + python -m pip install --no-index --find-links=/tmp/wheelhouse SomePackage - .. group-tab:: Windows + .. tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip wheel --wheel-dir=/tmp/wheelhouse SomePackage - py -m pip install --no-index --find-links=/tmp/wheelhouse SomePackage + py -m pip wheel --wheel-dir=/tmp/wheelhouse SomePackage + py -m pip install --no-index --find-links=/tmp/wheelhouse SomePackage #. Build a wheel for a package from source - .. tabs:: + .. tab:: Unix/macOS - .. group-tab:: Unix/macOS - - .. code-block:: shell + .. code-block:: shell - python -m pip wheel --no-binary SomePackage SomePackage + python -m pip wheel --no-binary SomePackage SomePackage - .. group-tab:: Windows + .. tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip wheel --no-binary SomePackage SomePackage + py -m pip wheel --no-binary SomePackage SomePackage diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index a2d13c43358..3c9cfec6319 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -11,26 +11,24 @@ Running pip pip is a command line program. When you install pip, a ``pip`` command is added to your system, which can be run from the command prompt as follows: -.. tabs:: +.. tab:: Unix/macOS - .. group-tab:: Unix/macOS + .. code-block:: shell - .. code-block:: shell - - python -m pip <pip arguments> + python -m pip <pip arguments> - ``python -m pip`` executes pip using the Python interpreter you - specified as python. So ``/usr/bin/python3.7 -m pip`` means - you are executing pip for your interpreter located at /usr/bin/python3.7. + ``python -m pip`` executes pip using the Python interpreter you + specified as python. So ``/usr/bin/python3.7 -m pip`` means + you are executing pip for your interpreter located at /usr/bin/python3.7. - .. group-tab:: Windows +.. tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip <pip arguments> + py -m pip <pip arguments> - ``py -m pip`` executes pip using the latest Python interpreter you - have installed. For more details, read the `Python Windows launcher`_ docs. + ``py -m pip`` executes pip using the latest Python interpreter you + have installed. For more details, read the `Python Windows launcher`_ docs. Installing Packages @@ -43,23 +41,21 @@ directly from distribution files. The most common scenario is to install from `PyPI`_ using :ref:`Requirement Specifiers` -.. tabs:: +.. tab:: Unix/macOS - .. group-tab:: Unix/macOS - - .. code-block:: shell + .. code-block:: shell - python -m pip install SomePackage # latest version - python -m pip install SomePackage==1.0.4 # specific version - python -m pip install 'SomePackage>=1.0.4' # minimum version + python -m pip install SomePackage # latest version + python -m pip install SomePackage==1.0.4 # specific version + python -m pip install 'SomePackage>=1.0.4' # minimum version - .. group-tab:: Windows +.. tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip install SomePackage # latest version - py -m pip install SomePackage==1.0.4 # specific version - py -m pip install 'SomePackage>=1.0.4' # minimum version + py -m pip install SomePackage # latest version + py -m pip install SomePackage==1.0.4 # specific version + py -m pip install 'SomePackage>=1.0.4' # minimum version For more information and examples, see the :ref:`pip install` reference. @@ -162,19 +158,17 @@ Requirements Files "Requirements files" are files containing a list of items to be installed using :ref:`pip install` like so: -.. tabs:: +.. tab:: Unix/macOS - .. group-tab:: Unix/macOS + .. code-block:: shell - .. code-block:: shell + python -m pip install -r requirements.txt - python -m pip install -r requirements.txt +.. tab:: Windows - .. group-tab:: Windows + .. code-block:: shell - .. code-block:: shell - - py -m pip install -r requirements.txt + py -m pip install -r requirements.txt Details on the format of the files are here: :ref:`Requirements File Format`. @@ -189,16 +183,14 @@ In practice, there are 4 common uses of Requirements files: this case, your requirement file contains a pinned version of everything that was installed when ``pip freeze`` was run. -.. tabs:: - - .. group-tab:: Unix/macOS + .. tab:: Unix/macOS .. code-block:: shell python -m pip freeze > requirements.txt python -m pip install -r requirements.txt - .. group-tab:: Windows + .. tab:: Windows .. code-block:: shell @@ -270,19 +262,17 @@ installation of the package. Use a constraints file like so: -.. tabs:: +.. tab:: Unix/macOS - .. group-tab:: Unix/macOS + .. code-block:: shell - .. code-block:: shell + python -m pip install -c constraints.txt - python -m pip install -c constraints.txt +.. tab:: Windows - .. group-tab:: Windows - - .. code-block:: shell + .. code-block:: shell - py -m pip install -c constraints.txt + py -m pip install -c constraints.txt Constraints files are used for exactly the same reason as requirements files when you don't know exactly what things you want to install. For instance, say @@ -320,19 +310,17 @@ archives. To install directly from a wheel archive: -.. tabs:: +.. tab:: Unix/macOS - .. group-tab:: Unix/macOS + .. code-block:: shell - .. code-block:: shell - - python -m pip install SomePackage-1.0-py2.py3-none-any.whl + python -m pip install SomePackage-1.0-py2.py3-none-any.whl - .. group-tab:: Windows +.. tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip install SomePackage-1.0-py2.py3-none-any.whl + py -m pip install SomePackage-1.0-py2.py3-none-any.whl For the cases where wheels are not available, pip offers :ref:`pip wheel` as a @@ -345,38 +333,34 @@ convenience, to build wheels for all your requirements and dependencies. To build wheels for your requirements and all their dependencies to a local directory: -.. tabs:: - - .. group-tab:: Unix/macOS +.. tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python -m pip install wheel - python -m pip wheel --wheel-dir=/local/wheels -r requirements.txt + python -m pip install wheel + python -m pip wheel --wheel-dir=/local/wheels -r requirements.txt - .. group-tab:: Windows +.. tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip install wheel - py -m pip wheel --wheel-dir=/local/wheels -r requirements.txt + py -m pip install wheel + py -m pip wheel --wheel-dir=/local/wheels -r requirements.txt And *then* to install those requirements just using your local directory of wheels (and not from PyPI): -.. tabs:: +.. tab:: Unix/macOS - .. group-tab:: Unix/macOS + .. code-block:: shell - .. code-block:: shell + python -m pip install --no-index --find-links=/local/wheels -r requirements.txt - python -m pip install --no-index --find-links=/local/wheels -r requirements.txt +.. tab:: Windows - .. group-tab:: Windows - - .. code-block:: shell + .. code-block:: shell - py -m pip install --no-index --find-links=/local/wheels -r requirements.txt + py -m pip install --no-index --find-links=/local/wheels -r requirements.txt Uninstalling Packages @@ -384,19 +368,17 @@ Uninstalling Packages pip is able to uninstall most packages like so: -.. tabs:: +.. tab:: Unix/macOS - .. group-tab:: Unix/macOS + .. code-block:: shell - .. code-block:: shell - - python -m pip uninstall SomePackage + python -m pip uninstall SomePackage - .. group-tab:: Windows +.. tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip uninstall SomePackage + py -m pip uninstall SomePackage pip also performs an automatic uninstall of an old version of a package @@ -410,75 +392,68 @@ Listing Packages To list installed packages: -.. tabs:: +.. tab:: Unix/macOS - .. group-tab:: Unix/macOS + .. code-block:: console - .. code-block:: console + $ python -m pip list + docutils (0.9.1) + Jinja2 (2.6) + Pygments (1.5) + Sphinx (1.1.2) - $ python -m pip list - docutils (0.9.1) - Jinja2 (2.6) - Pygments (1.5) - Sphinx (1.1.2) +.. tab:: Windows - .. group-tab:: Windows + .. code-block:: console - .. code-block:: console - - C:\> py -m pip list - docutils (0.9.1) - Jinja2 (2.6) - Pygments (1.5) - Sphinx (1.1.2) + C:\> py -m pip list + docutils (0.9.1) + Jinja2 (2.6) + Pygments (1.5) + Sphinx (1.1.2) To list outdated packages, and show the latest version available: -.. tabs:: - - .. group-tab:: Unix/macOS +.. tab:: Unix/macOS - .. code-block:: console + .. code-block:: console - $ python -m pip list --outdated - docutils (Current: 0.9.1 Latest: 0.10) - Sphinx (Current: 1.1.2 Latest: 1.1.3) + $ python -m pip list --outdated + docutils (Current: 0.9.1 Latest: 0.10) + Sphinx (Current: 1.1.2 Latest: 1.1.3) - .. group-tab:: Windows +.. tab:: Windows - .. code-block:: console + .. code-block:: console - C:\> py -m pip list --outdated - docutils (Current: 0.9.1 Latest: 0.10) - Sphinx (Current: 1.1.2 Latest: 1.1.3) + C:\> py -m pip list --outdated + docutils (Current: 0.9.1 Latest: 0.10) + Sphinx (Current: 1.1.2 Latest: 1.1.3) To show details about an installed package: -.. tabs:: +.. tab:: Unix/macOS - .. group-tab:: Unix/macOS + .. code-block:: console - .. code-block:: console + $ python -m pip show sphinx + --- + Name: Sphinx + Version: 1.1.3 + Location: /my/env/lib/pythonx.x/site-packages + Requires: Pygments, Jinja2, docutils - $ python -m pip show sphinx - --- - Name: Sphinx - Version: 1.1.3 - Location: /my/env/lib/pythonx.x/site-packages - Requires: Pygments, Jinja2, docutils +.. tab:: Windows - .. group-tab:: Windows - - .. code-block:: console - - C:\> py -m pip show sphinx - --- - Name: Sphinx - Version: 1.1.3 - Location: /my/env/lib/pythonx.x/site-packages - Requires: Pygments, Jinja2, docutils + .. code-block:: console + C:\> py -m pip show sphinx + --- + Name: Sphinx + Version: 1.1.3 + Location: /my/env/lib/pythonx.x/site-packages + Requires: Pygments, Jinja2, docutils For more information and examples, see the :ref:`pip list` and :ref:`pip show` reference pages. @@ -490,19 +465,17 @@ Searching for Packages pip can search `PyPI`_ for packages using the ``pip search`` command: -.. tabs:: +.. tab:: Unix/macOS - .. group-tab:: Unix/macOS + .. code-block:: shell - .. code-block:: shell + python -m pip search "query" - python -m pip search "query" +.. tab:: Windows - .. group-tab:: Windows - - .. code-block:: shell + .. code-block:: shell - py -m pip search "query" + py -m pip search "query" The query will be used to search the names and summaries of all packages. @@ -664,70 +637,60 @@ underscores (``_``). For example, to set the default timeout: -.. tabs:: +.. tab:: Unix/macOS - .. group-tab:: Unix/macOS + .. code-block:: shell - .. code-block:: shell - - export PIP_DEFAULT_TIMEOUT=60 + export PIP_DEFAULT_TIMEOUT=60 - .. group-tab:: Windows +.. tab:: Windows - .. code-block:: shell + .. code-block:: shell - set PIP_DEFAULT_TIMEOUT=60 + set PIP_DEFAULT_TIMEOUT=60 This is the same as passing the option to pip directly: -.. tabs:: - - .. group-tab:: Unix/macOS +.. tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python -m pip --default-timeout=60 [...] + python -m pip --default-timeout=60 [...] - .. group-tab:: Windows +.. tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip --default-timeout=60 [...] + py -m pip --default-timeout=60 [...] For command line options which can be repeated, use a space to separate multiple values. For example: -.. tabs:: - - .. group-tab:: Unix/macOS - - .. code-block:: shell +.. tab:: Unix/macOS - export PIP_FIND_LINKS="http://mirror1.example.com http://mirror2.example.com" + .. code-block:: shell - .. group-tab:: Windows + export PIP_FIND_LINKS="http://mirror1.example.com http://mirror2.example.com" - .. code-block:: shell +.. tab:: Windows - set PIP_FIND_LINKS="http://mirror1.example.com http://mirror2.example.com" + .. code-block:: shell + set PIP_FIND_LINKS="http://mirror1.example.com http://mirror2.example.com" is the same as calling: -.. tabs:: - - .. group-tab:: Unix/macOS +.. tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python -m pip install --find-links=http://mirror1.example.com --find-links=http://mirror2.example.com + python -m pip install --find-links=http://mirror1.example.com --find-links=http://mirror2.example.com - .. group-tab:: Windows - - .. code-block:: shell +.. tab:: Windows - py -m pip install --find-links=http://mirror1.example.com --find-links=http://mirror2.example.com + .. code-block:: shell + py -m pip install --find-links=http://mirror1.example.com --find-links=http://mirror2.example.com Options that do not take a value, but can be repeated (such as ``--verbose``) can be specified using the number of repetitions, so:: @@ -799,20 +762,17 @@ to PyPI. First, download the archives that fulfill your requirements: -.. tabs:: +.. tab:: Unix/macOS - .. group-tab:: Unix/macOS + .. code-block:: shell - .. code-block:: shell - - python -m pip download --destination-directory DIR -r requirements.txt + python -m pip download --destination-directory DIR -r requirements.txt - .. group-tab:: Windows +.. tab:: Windows - .. code-block:: shell - - py -m pip download --destination-directory DIR -r requirements.txt + .. code-block:: shell + py -m pip download --destination-directory DIR -r requirements.txt Note that ``pip download`` will look in your wheel cache first, before trying to download from PyPI. If you've never installed your requirements @@ -820,36 +780,32 @@ before, you won't have a wheel cache for those items. In that case, if some of your requirements don't come as wheels from PyPI, and you want wheels, then run this instead: -.. tabs:: +.. tab:: Unix/macOS - .. group-tab:: Unix/macOS + .. code-block:: shell - .. code-block:: shell + python -m pip wheel --wheel-dir DIR -r requirements.txt - python -m pip wheel --wheel-dir DIR -r requirements.txt +.. tab:: Windows - .. group-tab:: Windows + .. code-block:: shell - .. code-block:: shell - - py -m pip wheel --wheel-dir DIR -r requirements.txt + py -m pip wheel --wheel-dir DIR -r requirements.txt Then, to install from local only, you'll be using :ref:`--find-links <install_--find-links>` and :ref:`--no-index <install_--no-index>` like so: -.. tabs:: +.. tab:: Unix/macOS - .. group-tab:: Unix/macOS - - .. code-block:: shell + .. code-block:: shell - python -m pip install --no-index --find-links=DIR -r requirements.txt + python -m pip install --no-index --find-links=DIR -r requirements.txt - .. group-tab:: Windows +.. tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip install --no-index --find-links=DIR -r requirements.txt + py -m pip install --no-index --find-links=DIR -r requirements.txt "Only if needed" Recursive Upgrade @@ -870,21 +826,19 @@ the breaking nature of ``eager`` when upgrading conflicting dependencies. As an historic note, an earlier "fix" for getting the ``only-if-needed`` behaviour was: -.. tabs:: +.. tab:: Unix/macOS - .. group-tab:: Unix/macOS + .. code-block:: shell - .. code-block:: shell + python -m pip install --upgrade --no-deps SomePackage + python -m pip install SomePackage - python -m pip install --upgrade --no-deps SomePackage - python -m pip install SomePackage +.. tab:: Windows - .. group-tab:: Windows + .. code-block:: shell - .. code-block:: shell - - py -m pip install --upgrade --no-deps SomePackage - py -m pip install SomePackage + py -m pip install --upgrade --no-deps SomePackage + py -m pip install SomePackage A proposal for an ``upgrade-all`` command is being considered as a safer @@ -910,21 +864,19 @@ Moreover, the "user scheme" can be customized by setting the To install "SomePackage" into an environment with site.USER_BASE customized to '/myappenv', do the following: -.. tabs:: - - .. group-tab:: Unix/macOS +.. tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - export PYTHONUSERBASE=/myappenv - python -m pip install --user SomePackage + export PYTHONUSERBASE=/myappenv + python -m pip install --user SomePackage - .. group-tab:: Windows +.. tab:: Windows - .. code-block:: shell + .. code-block:: shell - set PYTHONUSERBASE=c:/myappenv - py -m pip install --user SomePackage + set PYTHONUSERBASE=c:/myappenv + py -m pip install --user SomePackage ``pip install --user`` follows four rules: @@ -946,126 +898,115 @@ To install "SomePackage" into an environment with site.USER_BASE customized to To make the rules clearer, here are some examples: - From within a ``--no-site-packages`` virtualenv (i.e. the default kind): -.. tabs:: +.. tab:: Unix/macOS - .. group-tab:: Unix/macOS + .. code-block:: console - .. code-block:: console + $ python -m pip install --user SomePackage + Can not perform a '--user' install. User site-packages are not visible in this virtualenv. - $ python -m pip install --user SomePackage - Can not perform a '--user' install. User site-packages are not visible in this virtualenv. +.. tab:: Windows - .. group-tab:: Windows + .. code-block:: console - .. code-block:: console - - C:\> py -m pip install --user SomePackage - Can not perform a '--user' install. User site-packages are not visible in this virtualenv. + C:\> py -m pip install --user SomePackage + Can not perform a '--user' install. User site-packages are not visible in this virtualenv. From within a ``--system-site-packages`` virtualenv where ``SomePackage==0.3`` is already installed in the virtualenv: -.. tabs:: - - .. group-tab:: Unix/macOS +.. tab:: Unix/macOS - .. code-block:: console + .. code-block:: console - $ python -m pip install --user SomePackage==0.4 - Will not install to the user site because it will lack sys.path precedence + $ python -m pip install --user SomePackage==0.4 + Will not install to the user site because it will lack sys.path precedence - .. group-tab:: Windows +.. tab:: Windows - .. code-block:: console + .. code-block:: console - C:\> py -m pip install --user SomePackage==0.4 - Will not install to the user site because it will lack sys.path precedence + C:\> py -m pip install --user SomePackage==0.4 + Will not install to the user site because it will lack sys.path precedence From within a real python, where ``SomePackage`` is *not* installed globally: -.. tabs:: +.. tab:: Unix/macOS - .. group-tab:: Unix/macOS + .. code-block:: console - .. code-block:: console + $ python -m pip install --user SomePackage + [...] + Successfully installed SomePackage - $ python -m pip install --user SomePackage - [...] - Successfully installed SomePackage +.. tab:: Windows - .. group-tab:: Windows + .. code-block:: console - .. code-block:: console - - C:\> py -m pip install --user SomePackage - [...] - Successfully installed SomePackage + C:\> py -m pip install --user SomePackage + [...] + Successfully installed SomePackage From within a real python, where ``SomePackage`` *is* installed globally, but is *not* the latest version: -.. tabs:: - - .. group-tab:: Unix/macOS +.. tab:: Unix/macOS - .. code-block:: console + .. code-block:: console - $ python -m pip install --user SomePackage - [...] - Requirement already satisfied (use --upgrade to upgrade) - $ python -m pip install --user --upgrade SomePackage - [...] - Successfully installed SomePackage + $ python -m pip install --user SomePackage + [...] + Requirement already satisfied (use --upgrade to upgrade) + $ python -m pip install --user --upgrade SomePackage + [...] + Successfully installed SomePackage - .. group-tab:: Windows +.. tab:: Windows - .. code-block:: console + .. code-block:: console - C:\> py -m pip install --user SomePackage - [...] - Requirement already satisfied (use --upgrade to upgrade) - C:\> py -m pip install --user --upgrade SomePackage - [...] - Successfully installed SomePackage + C:\> py -m pip install --user SomePackage + [...] + Requirement already satisfied (use --upgrade to upgrade) + C:\> py -m pip install --user --upgrade SomePackage + [...] + Successfully installed SomePackage From within a real python, where ``SomePackage`` *is* installed globally, and is the latest version: -.. tabs:: - - .. group-tab:: Unix/macOS +.. tab:: Unix/macOS - .. code-block:: console + .. code-block:: console - $ python -m pip install --user SomePackage - [...] - Requirement already satisfied (use --upgrade to upgrade) - $ python -m pip install --user --upgrade SomePackage - [...] - Requirement already up-to-date: SomePackage - # force the install - $ python -m pip install --user --ignore-installed SomePackage - [...] - Successfully installed SomePackage + $ python -m pip install --user SomePackage + [...] + Requirement already satisfied (use --upgrade to upgrade) + $ python -m pip install --user --upgrade SomePackage + [...] + Requirement already up-to-date: SomePackage + # force the install + $ python -m pip install --user --ignore-installed SomePackage + [...] + Successfully installed SomePackage - .. group-tab:: Windows +.. tab:: Windows - .. code-block:: console + .. code-block:: console - C:\> py -m pip install --user SomePackage - [...] - Requirement already satisfied (use --upgrade to upgrade) - C:\> py -m pip install --user --upgrade SomePackage - [...] - Requirement already up-to-date: SomePackage - # force the install - C:\> py -m pip install --user --ignore-installed SomePackage - [...] - Successfully installed SomePackage + C:\> py -m pip install --user SomePackage + [...] + Requirement already satisfied (use --upgrade to upgrade) + C:\> py -m pip install --user --upgrade SomePackage + [...] + Requirement already up-to-date: SomePackage + # force the install + C:\> py -m pip install --user --ignore-installed SomePackage + [...] + Successfully installed SomePackage .. _`Repeatability`: @@ -1171,19 +1112,17 @@ Understanding your error message When you get a ``ResolutionImpossible`` error, you might see something like this: -.. tabs:: +.. tab:: Unix/macOS - .. group-tab:: Unix/macOS + .. code-block:: shell - .. code-block:: shell + python -m pip install package_coffee==0.44.1 package_tea==4.3.0 - python -m pip install package_coffee==0.44.1 package_tea==4.3.0 +.. tab:: Windows - .. group-tab:: Windows + .. code-block:: shell - .. code-block:: shell - - py -m pip install package_coffee==0.44.1 package_tea==4.3.0 + py -m pip install package_coffee==0.44.1 package_tea==4.3.0 :: @@ -1289,19 +1228,17 @@ In the second case, pip will automatically find a version of both If you want to prioritize one package over another, you can add version specifiers to *only* the more important package: -.. tabs:: - - .. group-tab:: Unix/macOS +.. tab:: Unix/macOS - .. code-block:: shell + .. code-block:: shell - python -m pip install package_coffee==0.44.1b0 package_tea + python -m pip install package_coffee==0.44.1b0 package_tea - .. group-tab:: Windows +.. tab:: Windows - .. code-block:: shell + .. code-block:: shell - py -m pip install package_coffee==0.44.1b0 package_tea + py -m pip install package_coffee==0.44.1b0 package_tea This will result in: diff --git a/tools/requirements/docs.txt b/tools/requirements/docs.txt index dc93a60ff3d..77a940c08a8 100644 --- a/tools/requirements/docs.txt +++ b/tools/requirements/docs.txt @@ -1,7 +1,7 @@ sphinx == 3.2.1 git+https://github.com/python/python-docs-theme.git#egg=python-docs-theme git+https://github.com/pypa/pypa-docs-theme.git#egg=pypa-docs-theme -sphinx-tabs == 1.1.13 +sphinx-inline-tabs # `docs.pipext` uses pip's internals to generate documentation. So, we install # the current directory to make it work. From 279e735969f609d927cce7e41576e46149c758c0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Mon, 19 Oct 2020 22:34:17 +0700 Subject: [PATCH 2607/3170] Update docs for less common news extension Namely trivial, vendor and process. These were missed during the trasition to *.rst news fragment. --- docs/html/development/contributing.rst | 16 ++++++++-------- ...31861-84a4-4f9b-9987-762e127cb42b.trivial.rst | 0 2 files changed, 8 insertions(+), 8 deletions(-) create mode 100644 news/e1831861-84a4-4f9b-9987-762e127cb42b.trivial.rst diff --git a/docs/html/development/contributing.rst b/docs/html/development/contributing.rst index 3d2d5ead027..3cdbb309d44 100644 --- a/docs/html/development/contributing.rst +++ b/docs/html/development/contributing.rst @@ -109,22 +109,22 @@ A trivial change is anything that does not warrant an entry in the news file. Some examples are: Code refactors that don't change anything as far as the public is concerned, typo fixes, white space modification, etc. To mark a PR as trivial a contributor simply needs to add a randomly named, empty file to -the ``news/`` directory with the extension of ``.trivial``. If you are on a +the ``news/`` directory with the extension of ``.trivial.rst``. If you are on a POSIX like operating system, one can be added by running -``touch news/$(uuidgen).trivial``. On Windows, the same result can be achieved -in Powershell using ``New-Item "news/$([guid]::NewGuid()).trivial"``. Core -committers may also add a "trivial" label to the PR which will accomplish the -same thing. +``touch news/$(uuidgen).trivial.rst``. On Windows, the same result can be +achieved in Powershell using ``New-Item "news/$([guid]::NewGuid()).trivial"``. +Core committers may also add a "trivial" label to the PR which will accomplish +the same thing. Upgrading, removing, or adding a new vendored library gets a special mention -using a ``news/<library>.vendor`` file. This is in addition to any features, +using a ``news/<library>.vendor.rst`` file. This is in addition to any features, bugfixes, or other kinds of news that pulling in this library may have. This uses the library name as the key so that updating the same library twice doesn't produce two news file entries. Changes to the processes, policies, or other non code related changed that are -otherwise notable can be done using a ``news/<name>.process`` file. This is not -typically used, but can be used for things like changing version schemes, +otherwise notable can be done using a ``news/<name>.process.rst`` file. This is +not typically used, but can be used for things like changing version schemes, updating deprecation policy, etc. diff --git a/news/e1831861-84a4-4f9b-9987-762e127cb42b.trivial.rst b/news/e1831861-84a4-4f9b-9987-762e127cb42b.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d From b8cd76339d3a95e40c47a985edee2dc8031feaf0 Mon Sep 17 00:00:00 2001 From: Bernard <bernard@ei8fdb.org> Date: Wed, 21 Oct 2020 17:50:04 +0200 Subject: [PATCH 2608/3170] WIP updating user guide documentation with explanation and solutions to pip backtracking --- docs/html/user_guide.rst | 132 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 132 insertions(+) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 3c9cfec6319..faceeb4fdbf 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -1306,6 +1306,138 @@ issue tracker`_ if you believe that your problem has exposed a bug in pip. .. _`Using pip from your program`: +Dependency resolution backtracking +================================== + +Or more commonly known as *"Why does pip download multiple versions of +the same package over and over again?"*. + +The purpose of this section is to provide explanation and practical +suggestions to pip users who encounter dependency resolution +backtracking during a ``pip install`` command. + +Why does backtracking occur? +---------------------------- + +With the release of the new resolver (resolver 2020), pip is now more +strict in the package versions it installs, when a users does a +``pip install`` command. This new behaviour means that pip works harder +to find out which version of a package is a good candidate to install. + +What is backtracking? +--------------------- + +During a pip install (e.g. ``pip install tea``), pip needs to work out +the packages dependencies (e.g. ``spoon``, ``hot-water``, ``cup`` etc). +For each of these dependent packages, it needs to work out which version +of a package is a good candidate to install. When it does, it has a set +of compatible packages which is installs. + +If pip tries one version, finds out it isn’t compatible it needs to “go +back” (backtrack) and download an older version. It then tries that, if +it is successful it will continue onto the next package - if not it will +continue to backtrack until it finds a compatible version. + +This backtrack behaviour can end in 2 ways - either 1) it will +successfully find a set packages it can install (good news!), or 2) it +will eventually display `resolution impossible`_ error message (not so +good). + +If pip starts backtracking during dependency resolution, it does not +know how long it will backtrack, and how much computation would be +needed. + +.. _resolution impossible: https://pip.pypa.io/en/latest/user_guide/#id35 + +What does this behaviour look like? +----------------------------------- + +Right now backtracking looks like this: +:: + + $ pip install tea==1.9.8 + Collecting tea==1.9.8 + Downloading tea-1.9.8-py2.py3-none-any.whl (346 kB) + |████████████████████████████████| 346 kB 10.4 MB/s + Collecting spoon==2.27.0 + Downloading spoon-2.27.0-py2.py3-none-any.whl (312 kB) + |████████████████████████████████| 312 kB 19.2 MB/s + Collecting hot-water>=0.1.9 + Downloading hot-water-0.1.13-py3-none-any.whl (9.3 kB) + Collecting cup>=1.6.0 + Downloading cup-3.22.0-py2.py3-none-any.whl (397 kB) + |████████████████████████████████| 397 kB 28.2 MB/s + INFO: pip is looking at multiple versions of this package to determine + which version is compatible with other requirements. + This could take a while. + Downloading cup-3.21.0-py2.py3-none-any.whl (395 kB) + |████████████████████████████████| 395 kB 27.0 MB/s + Downloading cup-3.20.0-py2.py3-none-any.whl (394 kB) + |████████████████████████████████| 394 kB 24.4 MB/s + Downloading cup-3.19.1-py2.py3-none-any.whl (394 kB) + |████████████████████████████████| 394 kB 21.3 MB/s + Downloading cup-3.19.0-py2.py3-none-any.whl (394 kB) + |████████████████████████████████| 394 kB 26.2 MB/s + Downloading cup-3.18.0-py2.py3-none-any.whl (393 kB) + |████████████████████████████████| 393 kB 22.1 MB/s + Downloading cup-3.17.0-py2.py3-none-any.whl (382 kB) + |████████████████████████████████| 382 kB 23.8 MB/s + Downloading cup-3.16.0-py2.py3-none-any.whl (376 kB) + |████████████████████████████████| 376 kB 27.5 MB/s + Downloading cup-3.15.1-py2.py3-none-any.whl (385 kB) + |████████████████████████████████| 385 kB 30.4 MB/s + INFO: pip is looking at multiple versions of this package to determine + which version is compatible with other requirements. + This could take a while. + Downloading cup-3.15.0-py2.py3-none-any.whl (378 kB) + |████████████████████████████████| 378 kB 21.4 MB/s + Downloading cup-3.14.0-py2.py3-none-any.whl (372 kB) + |████████████████████████████████| 372 kB 21.1 MB/s + Downloading cup-3.13.1-py2.py3-none-any.whl (381 kB) + |████████████████████████████████| 381 kB 21.8 MB/s + This is taking longer than usual. You might need to provide the + dependency resolver with stricter constraints to reduce runtime. + If you want to abort this run, you can press Ctrl + C to do so. + Downloading cup-3.13.0-py2.py3-none-any.whl (374 kB) + +In the above sample output, pip must downloaded multiple versions of +package cup - cup-3.22.0 to cup-3.13.0 - to find a version that will be +compatible with the other packages - ``spoon``, ``hot-water``, ``cup`` +etc. + +## Possible solutions + +====UPDATE FROM HERE====== +:::info +Unsure what possible solutions are. AFAIK there are no solutions - apart 1) waiting til it's finished, or 2) [dealing with potential resolution impossible](https://pip.pypa.io/en/latest/user_guide/#id37). + +Pradyun mentions "stricter constraints to reduce runtime". + +Can we help the user with these questions: +- What can I do to reduce the chances of backtracking occuring? +- Backtracking keeps happening with my installs, what can I do? +- Can I delete these unnecessary packages? +::: + +Possible solutions +------------------ + +:::info Unsure what possible solutions are. AFAIK there are no solutions +- apart 1) waiting til it's finished, or 2) `dealing with potential +resolution impossible`_. + +Pradyun mentions "stricter constraints to reduce runtime". + +Can we help the user with these questions: + +- What can I do to reduce the chances of backtracking occuring? +- Backtracking keeps happening with my installs, what can I do? +- Can I delete these unnecessary packages? ::: + +.. _dealing with potential resolution impossible: https://pip.pypa.io/en/latest/user_guide/#id37 +=====UPDATE UNTIL HERE====== + + Using pip from your program =========================== From cf89b0b6c2cf2449560d53b868cdd9e0d52f6abd Mon Sep 17 00:00:00 2001 From: Bernard <bernard@ei8fdb.org> Date: Fri, 23 Oct 2020 15:14:19 +0200 Subject: [PATCH 2609/3170] updating user guide with backtracking information --- docs/html/user_guide.rst | 188 ++++++++++++++++++++++++++++++--------- 1 file changed, 144 insertions(+), 44 deletions(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index faceeb4fdbf..aecb427f3e4 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -1306,53 +1306,93 @@ issue tracker`_ if you believe that your problem has exposed a bug in pip. .. _`Using pip from your program`: +Backtracking user guide information +=================================== + +Related to `GH issue 8713 <https://github.com/pypa/pip/issues/8713>`__. + +Objective +--------- + +The purpose of this content is to provide documentation of 1) why this +occurs, and 2) how the user can possible solve it. + +Location +-------- + +The end location of this content will be on\ `the pip user +guide <https://pip.pypa.io/en/latest/user_guide/#id35>`__, somewhere +near the Resolution Impossible section. + +-------------- + Dependency resolution backtracking ================================== Or more commonly known as *"Why does pip download multiple versions of -the same package over and over again?"*. +the same package over and over again during an install?"*. -The purpose of this section is to provide explanation and practical -suggestions to pip users who encounter dependency resolution -backtracking during a ``pip install`` command. - -Why does backtracking occur? ----------------------------- - -With the release of the new resolver (resolver 2020), pip is now more -strict in the package versions it installs, when a users does a -``pip install`` command. This new behaviour means that pip works harder -to find out which version of a package is a good candidate to install. +The purpose of this section is to provide explanation of why +backtracking happens, and practical suggestions to pip users who +encounter dependency resolution backtracking during a ``pip install`` +command. What is backtracking? --------------------- +Backtracking is not a bug, or an unexpected behaviour. It is part of the +way pip's dependency resolution process works. + During a pip install (e.g. ``pip install tea``), pip needs to work out -the packages dependencies (e.g. ``spoon``, ``hot-water``, ``cup`` etc). +the packages dependencies (e.g. ``spoon``, ``hot-water``, ``cup`` etc), +and the versions of each of these packages it needs to install. + For each of these dependent packages, it needs to work out which version -of a package is a good candidate to install. When it does, it has a set -of compatible packages which is installs. +of a package is a good candidate to install. + +If everythig goes well, this will result in pip computing a set of +compatible versions of all these packages. + +In the case where a package has a lot of versions, arriving at a good +candidate can take a lot of time. (The amount of time depends on the +package size, the number of versions pip must try, among other things) + +How does backtracking work? +~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +pip does this by trying one version, if it finds out it isn’t compatible +it needs to “go back” (backtrack) and download an older version. -If pip tries one version, finds out it isn’t compatible it needs to “go -back” (backtrack) and download an older version. It then tries that, if -it is successful it will continue onto the next package - if not it will -continue to backtrack until it finds a compatible version. +It then tries that version, if it is successful it will continue onto +the next package - if not it will continue to backtrack until it finds a +compatible version. + +At the end of this process, pip will have the set of compatible +packages, which it then installs. This backtrack behaviour can end in 2 ways - either 1) it will successfully find a set packages it can install (good news!), or 2) it -will eventually display `resolution impossible`_ error message (not so -good). +will eventually display `resolution +impossible <https://pip.pypa.io/en/latest/user_guide/#id35>`__ error +message (not so good). If pip starts backtracking during dependency resolution, it does not know how long it will backtrack, and how much computation would be needed. -.. _resolution impossible: https://pip.pypa.io/en/latest/user_guide/#id35 +Why does backtracking occur? +---------------------------- + +With the release of the new resolver (resolver 2020), pip is now more +strict in the package versions it installs, when a users does a +``pip install`` command. This new behaviour means that pip works harder +to find out which version of a package is a good candidate to install. What does this behaviour look like? ----------------------------------- Right now backtracking looks like this: + :: $ pip install tea==1.9.8 @@ -1405,37 +1445,97 @@ package cup - cup-3.22.0 to cup-3.13.0 - to find a version that will be compatible with the other packages - ``spoon``, ``hot-water``, ``cup`` etc. -## Possible solutions +These multiple ``Downloading cup-version`` lines shows pip backtracking. -====UPDATE FROM HERE====== -:::info -Unsure what possible solutions are. AFAIK there are no solutions - apart 1) waiting til it's finished, or 2) [dealing with potential resolution impossible](https://pip.pypa.io/en/latest/user_guide/#id37). +Possible ways to reduce backtracking occuring +--------------------------------------------- -Pradyun mentions "stricter constraints to reduce runtime". +It's important to mention backtracking behaviour is expected during a +``pip install`` process. What pip is trying to do is complicated - it is +working through potentially millions of package versions to identify the +comptible versions. -Can we help the user with these questions: -- What can I do to reduce the chances of backtracking occuring? -- Backtracking keeps happening with my installs, what can I do? -- Can I delete these unnecessary packages? -::: +There is no guaranteed solution to backtracking but you can reduce it - +here are a number of ways. -Possible solutions ------------------- +.. _1-allow-pip-to-complete-its-backtracking: + +1. Allow pip to complete its backtracking +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +In most cases, pip will complete the backtracking process successfully. +It is possible this could take a very long time to complete - this may +not be the preferred option. + +However there is a possibility pip will not be able to find a set of +compatible versions. + +If you'd prefer not to wait, you can interrupt pip (ctrl and c) and use +constraints to reduce the number of package versions it tries. + +.. _2-reduce-the-versions-of-the-backtracking-package: + +2. Reduce the versions of the backtracking package +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +If pip is backtracking more than you'd like, the next option is to +constrain the number of package versions it tries. + +A first good candidate for this constraining is the package(s) it is +backtracking on (e.g. in the above example - ``cup``). + +You coud try: + +==\ ``pip install tea cup > 3.13`` please check this syntax== -:::info Unsure what possible solutions are. AFAIK there are no solutions -- apart 1) waiting til it's finished, or 2) `dealing with potential -resolution impossible`_. +This will reduce the number of versions of ``cup`` it tries, and +possibly reduce the time pip takes to install. -Pradyun mentions "stricter constraints to reduce runtime". +There is a possibility that if you're wrong (in this case a newer +version would have worked) then you missed the chance to use it. This +can be trial and error. -Can we help the user with these questions: +.. _3-use-constraint-files-or-lockfiles: -- What can I do to reduce the chances of backtracking occuring? -- Backtracking keeps happening with my installs, what can I do? -- Can I delete these unnecessary packages? ::: +3. Use constraint files or lockfiles +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ -.. _dealing with potential resolution impossible: https://pip.pypa.io/en/latest/user_guide/#id37 -=====UPDATE UNTIL HERE====== +This option is a progression of 2 above. It requires users to know how +to inspect: + +- the packages they're are trying to install +- the package release frequency and compatibility policies +- their release notes and changelogs from past versions + +During deployment creating a lockfile stating the exact package and +version number for for each dependency of that package. You can do this +with `pip-tools <https://github.com/jazzband/pip-tools/>`__. + +This means the "work" is done once during development process, and so +will save users this work during deployment. + +The pip team is not available to provide support in helping you create a +suitable constraints file. + +.. _4-be-more-strict-on-package-dependencies-during-development: + +4. Be more strict on package dependencies during development +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ + +For package maintainers during the development, give pip some help by +creating constraint files for the dependency tree. This will reduce the +number of versions it will try. + +If you (as the user installing a package) see the package has specific +version dependencies, you could contact the maintainer and ask them to +be more strict with the packages requirements. + +Getting help +------------ + +If none of the suggestions above work for you, we recommend that you ask +for help and you've got `a number of +options <https://pip.pypa.io/en/latest/user_guide/#getting-help>`__. Using pip from your program From b49a213b56563831e7981a9a95c09e644e483d69 Mon Sep 17 00:00:00 2001 From: Bernard <bernard@ei8fdb.org> Date: Fri, 23 Oct 2020 15:16:39 +0200 Subject: [PATCH 2610/3170] fix syntax --- docs/html/user_guide.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index aecb427f3e4..332ba723d11 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -1484,9 +1484,9 @@ constrain the number of package versions it tries. A first good candidate for this constraining is the package(s) it is backtracking on (e.g. in the above example - ``cup``). -You coud try: +You could try: -==\ ``pip install tea cup > 3.13`` please check this syntax== +``pip install tea cup > 3.13`` This will reduce the number of versions of ``cup`` it tries, and possibly reduce the time pip takes to install. From e28f30d0704e56bd99b7937bca9012312cad29ab Mon Sep 17 00:00:00 2001 From: Bernard <bernard@ei8fdb.org> Date: Fri, 23 Oct 2020 15:26:41 +0200 Subject: [PATCH 2611/3170] Adding news file for #9309 --- news/9039.doc.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/9039.doc.rst diff --git a/news/9039.doc.rst b/news/9039.doc.rst new file mode 100644 index 00000000000..c87972d70d0 --- /dev/null +++ b/news/9039.doc.rst @@ -0,0 +1 @@ +adding a section to pip user guide to cover pip backtracking. From d185aea3e1a57c4bb81e909e44d807adf3d3f219 Mon Sep 17 00:00:00 2001 From: Bernard <bernard@ei8fdb.org> Date: Fri, 23 Oct 2020 15:27:20 +0200 Subject: [PATCH 2612/3170] updating --- news/9039.doc.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/9039.doc.rst b/news/9039.doc.rst index c87972d70d0..8f6dde8cba2 100644 --- a/news/9039.doc.rst +++ b/news/9039.doc.rst @@ -1 +1 @@ -adding a section to pip user guide to cover pip backtracking. +adding a section to pip user guide to cover pip backtracking, connected to #8975. From d5a2007e8ff578551a4d6e45ac190faaba6f9832 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sun, 25 Oct 2020 15:16:11 +0100 Subject: [PATCH 2613/3170] Avoid AssertionError in pip freeze with editable direct URLs --- news/8996.bugfix.rst | 3 +++ src/pip/_internal/utils/direct_url_helpers.py | 4 ---- tests/unit/test_direct_url_helpers.py | 15 +++++++++++++++ 3 files changed, 18 insertions(+), 4 deletions(-) create mode 100644 news/8996.bugfix.rst diff --git a/news/8996.bugfix.rst b/news/8996.bugfix.rst new file mode 100644 index 00000000000..e366a6a6694 --- /dev/null +++ b/news/8996.bugfix.rst @@ -0,0 +1,3 @@ +Do not fail in pip freeze when encountering a ``direct_url.jso`` metadata file +with editable=True. Render it as a non-editable ``file://`` URL until modern +editable installs are standardized and supported. diff --git a/src/pip/_internal/utils/direct_url_helpers.py b/src/pip/_internal/utils/direct_url_helpers.py index a355a6c5ee4..87bd61fa01f 100644 --- a/src/pip/_internal/utils/direct_url_helpers.py +++ b/src/pip/_internal/utils/direct_url_helpers.py @@ -43,10 +43,6 @@ def direct_url_as_pep440_direct_reference(direct_url, name): fragments.append(direct_url.info.hash) else: assert isinstance(direct_url.info, DirInfo) - # pip should never reach this point for editables, since - # pip freeze inspects the editable project location to produce - # the requirement string - assert not direct_url.info.editable requirement += direct_url.url if direct_url.subdirectory: fragments.append("subdirectory=" + direct_url.subdirectory) diff --git a/tests/unit/test_direct_url_helpers.py b/tests/unit/test_direct_url_helpers.py index 55cd5855b93..b0cb50c6eb9 100644 --- a/tests/unit/test_direct_url_helpers.py +++ b/tests/unit/test_direct_url_helpers.py @@ -55,6 +55,21 @@ def test_as_pep440_requirement_dir(): ) +def test_as_pep440_requirement_editable_dir(): + # direct_url_as_pep440_direct_reference behaves the same + # irrespective of the editable flag. It's the responsibility of + # callers to render it as editable + direct_url = DirectUrl( + url="file:///home/user/project", + info=DirInfo(editable=True), + ) + direct_url.validate() + assert ( + direct_url_as_pep440_direct_reference(direct_url, "pkg") == + "pkg @ file:///home/user/project" + ) + + def test_as_pep440_requirement_vcs(): direct_url = DirectUrl( url="https:///g.c/u/p.git", From 9faf431fbbd2c1fe94ee14aea096480123b0a6c6 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sun, 25 Oct 2020 23:02:59 +0530 Subject: [PATCH 2614/3170] Breakup conditional for readability --- tests/conftest.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index ffd9f42158b..81e13668885 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -63,10 +63,10 @@ def pytest_collection_modifyitems(config, items): if not hasattr(item, 'module'): # e.g.: DoctestTextfile continue - # Mark network tests as flaky - if (item.get_closest_marker('network') is not None and - "CI" in os.environ): - item.add_marker(pytest.mark.flaky(reruns=3)) + if "CI" in os.environ: + # Mark network tests as flaky + if item.get_closest_marker('network') is not None: + item.add_marker(pytest.mark.flaky(reruns=3)) if (item.get_closest_marker('fails_on_new_resolver') and config.getoption("--new-resolver") and From 66bb8a88c49ff58f5c7202daf75d9a84d96d77f2 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sun, 25 Oct 2020 23:03:10 +0530 Subject: [PATCH 2615/3170] Add a small delay between re-runs --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index 81e13668885..bf071c8de7a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -66,7 +66,7 @@ def pytest_collection_modifyitems(config, items): if "CI" in os.environ: # Mark network tests as flaky if item.get_closest_marker('network') is not None: - item.add_marker(pytest.mark.flaky(reruns=3)) + item.add_marker(pytest.mark.flaky(reruns=3, reruns_delay=2)) if (item.get_closest_marker('fails_on_new_resolver') and config.getoption("--new-resolver") and From bd3d5f8f29a63f34ab0b04dfe27d026e10da60b9 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sun, 25 Oct 2020 23:18:48 +0530 Subject: [PATCH 2616/3170] Set the CI variable on Azure Pipelines --- .azure-pipelines/linux.yml | 3 +++ .azure-pipelines/macos.yml | 3 +++ .azure-pipelines/windows.yml | 3 +++ 3 files changed, 9 insertions(+) diff --git a/.azure-pipelines/linux.yml b/.azure-pipelines/linux.yml index 6965a15fc6d..e5598074344 100644 --- a/.azure-pipelines/linux.yml +++ b/.azure-pipelines/linux.yml @@ -1,3 +1,6 @@ +variables: + CI: true + jobs: - template: jobs/test.yml parameters: diff --git a/.azure-pipelines/macos.yml b/.azure-pipelines/macos.yml index 85c2a0246af..9ad9edae862 100644 --- a/.azure-pipelines/macos.yml +++ b/.azure-pipelines/macos.yml @@ -1,3 +1,6 @@ +variables: + CI: true + jobs: - template: jobs/test.yml parameters: diff --git a/.azure-pipelines/windows.yml b/.azure-pipelines/windows.yml index 9d1bf5385d0..f56b8f50486 100644 --- a/.azure-pipelines/windows.yml +++ b/.azure-pipelines/windows.yml @@ -1,3 +1,6 @@ +variables: + CI: true + jobs: - template: jobs/test-windows.yml parameters: From abb3d0fae3f14a86bc2c4df6cd6ef54311e0ee34 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sun, 25 Oct 2020 23:32:26 +0530 Subject: [PATCH 2617/3170] Move build_env tests to functional These tests use the script fixture which as a 30s setup time on my machine. This KILLS productivity when trying to run unit tests as part of a feedback loop during development. --- tests/{unit => functional}/test_build_env.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{unit => functional}/test_build_env.py (100%) diff --git a/tests/unit/test_build_env.py b/tests/functional/test_build_env.py similarity index 100% rename from tests/unit/test_build_env.py rename to tests/functional/test_build_env.py From ecda23819baceb5f0986a0bd16ad612998b34553 Mon Sep 17 00:00:00 2001 From: Bernard <bernard@ei8fdb.org> Date: Mon, 26 Oct 2020 10:14:34 +0100 Subject: [PATCH 2618/3170] Updating file with PR review edits. --- docs/html/user_guide.rst | 95 +++++++++++++++++++--------------------- 1 file changed, 46 insertions(+), 49 deletions(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 332ba723d11..0efb64b89e2 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -1306,26 +1306,6 @@ issue tracker`_ if you believe that your problem has exposed a bug in pip. .. _`Using pip from your program`: -Backtracking user guide information -=================================== - -Related to `GH issue 8713 <https://github.com/pypa/pip/issues/8713>`__. - -Objective ---------- - -The purpose of this content is to provide documentation of 1) why this -occurs, and 2) how the user can possible solve it. - -Location --------- - -The end location of this content will be on\ `the pip user -guide <https://pip.pypa.io/en/latest/user_guide/#id35>`__, somewhere -near the Resolution Impossible section. - --------------- - Dependency resolution backtracking ================================== @@ -1344,49 +1324,70 @@ Backtracking is not a bug, or an unexpected behaviour. It is part of the way pip's dependency resolution process works. During a pip install (e.g. ``pip install tea``), pip needs to work out -the packages dependencies (e.g. ``spoon``, ``hot-water``, ``cup`` etc), +the package's dependencies (e.g. ``spoon``, ``hot-water``, ``cup`` etc), and the versions of each of these packages it needs to install. For each of these dependent packages, it needs to work out which version of a package is a good candidate to install. -If everythig goes well, this will result in pip computing a set of +A good candidate is a version of each package that is compatible with all the +other packages versions being installing in the same command, and works with +other requirements or constraints the user has specified. + +If everything goes well, this will result in pip computing a set of compatible versions of all these packages. In the case where a package has a lot of versions, arriving at a good candidate can take a lot of time. (The amount of time depends on the -package size, the number of versions pip must try, among other things) +package size, the number of versions pip must try, and other concerns.) How does backtracking work? ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -pip does this by trying one version, if it finds out it isn’t compatible -it needs to “go back” (backtrack) and download an older version. +When doing a pip install, it needs to start by making assumptions about the +packages it needs to install. During this install process it needs check this +assumptions as it goes along. -It then tries that version, if it is successful it will continue onto -the next package - if not it will continue to backtrack until it finds a -compatible version. +When it finds that an assumption is incorrect, it has to try another approach +(backtrack), which means discarding some of the work that has already been done, +and going back to choose another path. + +For example; The user requests `pip install tea`. +`tea` has dependencies of `cup`, `hot-water`, amongst others. -At the end of this process, pip will have the set of compatible -packages, which it then installs. +pip starts by installing a version of cup. If it finds out it isn’t compatible +(with the other package versions) it needs to “go back” (backtrack) and +download an older version. + +It then tries to install that version. If it is successful, it will continue +onto the next package. If not it will continue to backtrack until it finds a +compatible version. This backtrack behaviour can end in 2 ways - either 1) it will -successfully find a set packages it can install (good news!), or 2) it -will eventually display `resolution -impossible <https://pip.pypa.io/en/latest/user_guide/#id35>`__ error +successfully find a set packages it can install (good news!), or 2) it will +eventually display `resolution impossible <https://pip.pypa.io/en/latest/user_guide/#id35>`__ error message (not so good). If pip starts backtracking during dependency resolution, it does not know how long it will backtrack, and how much computation would be -needed. +needed, which for the user, means it can possibly take a long time to complete. Why does backtracking occur? ---------------------------- -With the release of the new resolver (resolver 2020), pip is now more -strict in the package versions it installs, when a users does a -``pip install`` command. This new behaviour means that pip works harder -to find out which version of a package is a good candidate to install. +With the release of the new resolver (:ref:`Resolver changes 2020`), pip is now +more strict in the package versions it installs when a users runs a +``pip install`` command. + +This new behaviour means that pip works harder to find out which version of a +package is a good candidate to install. It reduces the risk that installing a +new package will accidentally break an existing installed package, and so +reducing the risk of your environment gets messed up. + +Pip needs to backtrack because initially, it doesn't have all the information it +needs to work out the correct set of packages. This is because package indexes +don't provide full package dependency information before you have downloaded +the package. What does this behaviour look like? ----------------------------------- @@ -1440,20 +1441,20 @@ Right now backtracking looks like this: If you want to abort this run, you can press Ctrl + C to do so. Downloading cup-3.13.0-py2.py3-none-any.whl (374 kB) -In the above sample output, pip must downloaded multiple versions of +In the above sample output, pip had to download multiple versions of package cup - cup-3.22.0 to cup-3.13.0 - to find a version that will be compatible with the other packages - ``spoon``, ``hot-water``, ``cup`` etc. These multiple ``Downloading cup-version`` lines shows pip backtracking. -Possible ways to reduce backtracking occuring +Possible ways to reduce backtracking occurring --------------------------------------------- It's important to mention backtracking behaviour is expected during a ``pip install`` process. What pip is trying to do is complicated - it is working through potentially millions of package versions to identify the -comptible versions. +compatible versions. There is no guaranteed solution to backtracking but you can reduce it - here are a number of ways. @@ -1486,12 +1487,12 @@ backtracking on (e.g. in the above example - ``cup``). You could try: -``pip install tea cup > 3.13`` +``pip install tea "cup > 3.13"`` This will reduce the number of versions of ``cup`` it tries, and possibly reduce the time pip takes to install. -There is a possibility that if you're wrong (in this case a newer +There is a possibility that if you're wrong (in this case an older version would have worked) then you missed the chance to use it. This can be trial and error. @@ -1507,8 +1508,8 @@ to inspect: - the package release frequency and compatibility policies - their release notes and changelogs from past versions -During deployment creating a lockfile stating the exact package and -version number for for each dependency of that package. You can do this +During deployment, you can create a lockfile stating the exact package and +version number for for each dependency of that package. You can create this with `pip-tools <https://github.com/jazzband/pip-tools/>`__. This means the "work" is done once during development process, and so @@ -1526,10 +1527,6 @@ For package maintainers during the development, give pip some help by creating constraint files for the dependency tree. This will reduce the number of versions it will try. -If you (as the user installing a package) see the package has specific -version dependencies, you could contact the maintainer and ask them to -be more strict with the packages requirements. - Getting help ------------ From 89b765db9606b35e948a2db8e9e834982d92a068 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@acsone.eu> Date: Mon, 26 Oct 2020 12:16:37 +0100 Subject: [PATCH 2619/3170] Update news/8996.bugfix.rst Co-authored-by: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> --- news/8996.bugfix.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/8996.bugfix.rst b/news/8996.bugfix.rst index e366a6a6694..fa3528e7c47 100644 --- a/news/8996.bugfix.rst +++ b/news/8996.bugfix.rst @@ -1,3 +1,3 @@ -Do not fail in pip freeze when encountering a ``direct_url.jso`` metadata file +Do not fail in pip freeze when encountering a ``direct_url.json`` metadata file with editable=True. Render it as a non-editable ``file://`` URL until modern editable installs are standardized and supported. From 95e78ab284f0f102cff6c3e58194b46875128647 Mon Sep 17 00:00:00 2001 From: Bernard <bernard@ei8fdb.org> Date: Mon, 26 Oct 2020 13:23:07 +0100 Subject: [PATCH 2620/3170] Editing to make shorten content (reduce/remove duplication), add anchor. --- docs/html/user_guide.rst | 48 ++++++++++++++++++---------------------- 1 file changed, 21 insertions(+), 27 deletions(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 0efb64b89e2..bf2e6359b70 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -1304,7 +1304,7 @@ issue tracker`_ if you believe that your problem has exposed a bug in pip. .. _"How do I ask a good question?": https://stackoverflow.com/help/how-to-ask .. _pip issue tracker: https://github.com/pypa/pip/issues -.. _`Using pip from your program`: +.. _`Dependency resolution backtracking`: Dependency resolution backtracking ================================== @@ -1314,8 +1314,7 @@ the same package over and over again during an install?"*. The purpose of this section is to provide explanation of why backtracking happens, and practical suggestions to pip users who -encounter dependency resolution backtracking during a ``pip install`` -command. +encounter it during a ``pip install``. What is backtracking? --------------------- @@ -1324,18 +1323,12 @@ Backtracking is not a bug, or an unexpected behaviour. It is part of the way pip's dependency resolution process works. During a pip install (e.g. ``pip install tea``), pip needs to work out -the package's dependencies (e.g. ``spoon``, ``hot-water``, ``cup`` etc), -and the versions of each of these packages it needs to install. - -For each of these dependent packages, it needs to work out which version -of a package is a good candidate to install. - -A good candidate is a version of each package that is compatible with all the -other packages versions being installing in the same command, and works with -other requirements or constraints the user has specified. +the package's dependencies (e.g. ``spoon``, ``hot-water``, ``cup`` etc), the +versions of each of these packages it needs to install. For each of these +it needs to decide which version is a good candidate to install. -If everything goes well, this will result in pip computing a set of -compatible versions of all these packages. +A "good candidate" means a version of each package that is compatible with all +the other package versions being installed at the same time. In the case where a package has a lot of versions, arriving at a good candidate can take a lot of time. (The amount of time depends on the @@ -1345,19 +1338,19 @@ How does backtracking work? ~~~~~~~~~~~~~~~~~~~~~~~~~~~ When doing a pip install, it needs to start by making assumptions about the -packages it needs to install. During this install process it needs check this +packages it needs to install. During the install process it needs check this assumptions as it goes along. When it finds that an assumption is incorrect, it has to try another approach (backtrack), which means discarding some of the work that has already been done, and going back to choose another path. -For example; The user requests `pip install tea`. -`tea` has dependencies of `cup`, `hot-water`, amongst others. +For example; The user requests ``pip install tea``. ```tea`` has dependencies of +``cup``, ``hot-water``, ``spoon`` amongst others. -pip starts by installing a version of cup. If it finds out it isn’t compatible -(with the other package versions) it needs to “go back” (backtrack) and -download an older version. +pip starts by installing a version of ``cup``. If it finds out it isn’t +compatible (with the other package versions) it needs to “go back” +(backtrack) and download an older version. It then tries to install that version. If it is successful, it will continue onto the next package. If not it will continue to backtrack until it finds a @@ -1370,7 +1363,7 @@ message (not so good). If pip starts backtracking during dependency resolution, it does not know how long it will backtrack, and how much computation would be -needed, which for the user, means it can possibly take a long time to complete. +needed. For the user this means it can take a long time to complete. Why does backtracking occur? ---------------------------- @@ -1379,20 +1372,20 @@ With the release of the new resolver (:ref:`Resolver changes 2020`), pip is now more strict in the package versions it installs when a users runs a ``pip install`` command. -This new behaviour means that pip works harder to find out which version of a -package is a good candidate to install. It reduces the risk that installing a -new package will accidentally break an existing installed package, and so -reducing the risk of your environment gets messed up. - Pip needs to backtrack because initially, it doesn't have all the information it needs to work out the correct set of packages. This is because package indexes don't provide full package dependency information before you have downloaded the package. +This new resolver behaviour means that pip works harder to find out which +version of a package is a good candidate to install. It reduces the risk that +installing a new package will accidentally break an existing installed package, +and so reducing the risk of your environment gets messed up. + What does this behaviour look like? ----------------------------------- -Right now backtracking looks like this: +Right now backtracking behaviour looks like this: :: @@ -1534,6 +1527,7 @@ If none of the suggestions above work for you, we recommend that you ask for help and you've got `a number of options <https://pip.pypa.io/en/latest/user_guide/#getting-help>`__. +.. _`Using pip from your program`: Using pip from your program =========================== From ca63189b606fb137c66a40758e643f1592304924 Mon Sep 17 00:00:00 2001 From: Bernard Tyers <ei8fdb@users.noreply.github.com> Date: Mon, 26 Oct 2020 14:52:58 +0100 Subject: [PATCH 2621/3170] Update news/9039.doc.rst Co-authored-by: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> --- news/9039.doc.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/9039.doc.rst b/news/9039.doc.rst index 8f6dde8cba2..37241250dac 100644 --- a/news/9039.doc.rst +++ b/news/9039.doc.rst @@ -1 +1 @@ -adding a section to pip user guide to cover pip backtracking, connected to #8975. +Add a section to the User Guide to cover backtracking during dependency resolution. From e74116a436f9c50ff513ec8084202d33a8b9e04b Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Tue, 27 Oct 2020 00:53:41 +0530 Subject: [PATCH 2622/3170] Change assertion in topological sorting --- src/pip/_internal/resolution/resolvelib/resolver.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index cb7d1ae8a59..718619b513d 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -180,7 +180,10 @@ def get_installation_order(self, req_set): assert self._result is not None, "must call resolve() first" graph = self._result.graph - weights = get_topological_weights(graph) + weights = get_topological_weights( + graph, + expected_node_count=len(self._result.mapping) + 1, + ) sorted_items = sorted( req_set.requirements.items(), @@ -190,7 +193,7 @@ def get_installation_order(self, req_set): return [ireq for _, ireq in sorted_items] -def get_topological_weights(graph): +def get_topological_weights(graph, expected_node_count): # type: (Graph) -> Dict[Optional[str], int] """Assign weights to each node based on how "deep" they are. @@ -231,7 +234,7 @@ def visit(node): # Sanity checks assert weights[None] == 0 - assert len(weights) == len(graph) + assert len(weights) == expected_node_count return weights From 1bb7bf6a4cd1e37209d5bb1b1cad32353de818f5 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Tue, 27 Oct 2020 01:00:30 +0530 Subject: [PATCH 2623/3170] Update type annotation --- src/pip/_internal/resolution/resolvelib/resolver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 718619b513d..52f228bc604 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -194,7 +194,7 @@ def get_installation_order(self, req_set): def get_topological_weights(graph, expected_node_count): - # type: (Graph) -> Dict[Optional[str], int] + # type: (Graph, int) -> Dict[Optional[str], int] """Assign weights to each node based on how "deep" they are. This implementation may change at any point in the future without prior From 45d3a3e8595e1820ea26f8a5a56557f67b32a1d5 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Tue, 27 Oct 2020 01:05:29 +0530 Subject: [PATCH 2624/3170] Update tests for new signature --- tests/unit/resolution_resolvelib/test_resolver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/resolution_resolvelib/test_resolver.py b/tests/unit/resolution_resolvelib/test_resolver.py index 0f240ec6ce4..4a62cefb603 100644 --- a/tests/unit/resolution_resolvelib/test_resolver.py +++ b/tests/unit/resolution_resolvelib/test_resolver.py @@ -232,5 +232,5 @@ def test_new_resolver_get_installation_order(resolver, edges, ordered_reqs): def test_new_resolver_topological_weights(name, edges, expected_weights): graph = _make_graph(edges) - weights = get_topological_weights(graph) + weights = get_topological_weights(graph, len(expected_weights)) assert weights == expected_weights From bb38dd568ba4895795f97132d8981b2252dbab76 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Tue, 27 Oct 2020 01:32:49 +0530 Subject: [PATCH 2625/3170] Add GitHub Action based testing for MacOS --- .github/workflows/linting.yml | 1 - .github/workflows/macos.yml | 138 ++++++++++++++++++++++++++++++++++ 2 files changed, 138 insertions(+), 1 deletion(-) create mode 100644 .github/workflows/macos.yml diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index 0ff16be6445..ebe98fec7dd 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -19,7 +19,6 @@ jobs: os: - Ubuntu - Windows - - MacOS steps: - uses: actions/checkout@v2 diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml new file mode 100644 index 00000000000..75c2118ac12 --- /dev/null +++ b/.github/workflows/macos.yml @@ -0,0 +1,138 @@ +name: CI + +on: + push: + branches: + - master + pull_request: + schedule: + # Run every Friday at 18:02 UTC + - cron: 2 18 * * 5 + +jobs: + dev-tools: + name: Quality Check / ${{ matrix.os }} + runs-on: ${{ matrix.os }}-latest + + strategy: + matrix: + os: [MacOS] + + steps: + # Caches + - name: pip cache + uses: actions/cache@v1 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('tools/requirements/tests.txt') }}-${{ hashFiles('tools/requirements/docs.txt') }}-${{ hashFiles('tox.ini') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + - name: Set PY (for pre-commit cache) + run: echo "::set-env name=PY::$(python -c 'import hashlib, sys;print(hashlib.sha256(sys.version.encode()+sys.executable.encode()).hexdigest())')" + - name: pre-commit cache + uses: actions/cache@v1 + with: + path: ~/.cache/pre-commit + key: pre-commit|2020-02-14|${{ env.PY }}|${{ hashFiles('.pre-commit-config.yaml') }} + + # Setup + - uses: actions/checkout@v2 + - name: Set up Python 3.8 + uses: actions/setup-python@v1 + with: + python-version: 3.8 + + - name: Install tox + run: python -m pip install tox + + # Main check + - run: python -m tox -e "lint,docs" + + packaging: + name: Packaging / ${{ matrix.os }} + runs-on: ${{ matrix.os }}-latest + + strategy: + matrix: + os: [MacOS] + + steps: + # Caches + - name: pip cache + uses: actions/cache@v1 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('tools/requirements/tests.txt') }}-${{ hashFiles('tools/requirements/docs.txt') }}-${{ hashFiles('tox.ini') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + # Setup + - name: Set up git credentials + run: | + git config --global user.email "pypa-dev@googlegroups.com" + git config --global user.name "pip" + - uses: actions/checkout@v2 + - name: Set up Python 3.8 + uses: actions/setup-python@v1 + with: + python-version: 3.8 + - name: Install tox and nox + run: python -m pip install tox nox + + # Main check + - name: Check vendored packages + run: python -m tox -e "vendoring" + + - name: Prepare dummy release + run: nox -s prepare-release -- 99.9 + + - name: Generate distributions for the dummy release + run: nox -s build-release -- 99.9 + + tests: + name: Tests / ${{ matrix.python }} / ${{ matrix.os }} + runs-on: ${{ matrix.os }}-latest + + needs: dev-tools + + strategy: + fail-fast: false + matrix: + os: [MacOS] + python: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9] + + steps: + # Caches + - name: pip cache + uses: actions/cache@v1 + with: + path: ~/.cache/pip + key: ${{ runner.os }}-pip-${{ hashFiles('tools/requirements/tests.txt') }}-${{ hashFiles('tools/requirements/docs.txt') }}-${{ hashFiles('tox.ini') }} + restore-keys: | + ${{ runner.os }}-pip- + ${{ runner.os }}- + # Setup + - uses: actions/checkout@v2 + - uses: actions/setup-python@v1 + with: + python-version: ${{ matrix.python }} + + - name: Install tox + run: python -m pip install tox 'virtualenv<20' + + # Main check + - name: Run unit tests + run: >- + python -m tox -e py -- + -m unit + --verbose + --numprocesses auto + + - name: Run integration tests + run: >- + python -m tox -e py -- + -m integration + --verbose + --numprocesses auto + --duration=5 From 3ccf8614fa76c6a7699c7304b663660e835a6b39 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Tue, 27 Oct 2020 01:33:03 +0530 Subject: [PATCH 2626/3170] Drop MacOS test runs for Azure Pipelines --- .azure-pipelines/macos.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.azure-pipelines/macos.yml b/.azure-pipelines/macos.yml index 9ad9edae862..537ac3efdd4 100644 --- a/.azure-pipelines/macos.yml +++ b/.azure-pipelines/macos.yml @@ -2,10 +2,6 @@ variables: CI: true jobs: -- template: jobs/test.yml - parameters: - vmImage: macos-10.14 - - template: jobs/package.yml parameters: vmImage: macos-10.14 From d4cde6cb107390502977c2fd51481e7c464243d3 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Tue, 27 Oct 2020 01:49:00 +0530 Subject: [PATCH 2627/3170] Fix typo --- .github/workflows/macos.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 75c2118ac12..20ae2f77e8d 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -135,4 +135,4 @@ jobs: -m integration --verbose --numprocesses auto - --duration=5 + --durations=5 From 0e135407c759ee4e0e5e881f1686f14328a835f0 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Tue, 27 Oct 2020 02:10:36 +0530 Subject: [PATCH 2628/3170] Simplify YAML + better names --- .github/workflows/macos.yml | 29 ++++++++++------------------- 1 file changed, 10 insertions(+), 19 deletions(-) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 20ae2f77e8d..bdf3f671bd2 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -1,4 +1,4 @@ -name: CI +name: MacOS on: push: @@ -11,12 +11,8 @@ on: jobs: dev-tools: - name: Quality Check / ${{ matrix.os }} - runs-on: ${{ matrix.os }}-latest - - strategy: - matrix: - os: [MacOS] + name: Quality Check + runs-on: macos-latest steps: # Caches @@ -39,7 +35,7 @@ jobs: # Setup - uses: actions/checkout@v2 - name: Set up Python 3.8 - uses: actions/setup-python@v1 + uses: actions/setup-python@v2 with: python-version: 3.8 @@ -50,12 +46,8 @@ jobs: - run: python -m tox -e "lint,docs" packaging: - name: Packaging / ${{ matrix.os }} - runs-on: ${{ matrix.os }}-latest - - strategy: - matrix: - os: [MacOS] + name: Packaging + runs-on: macos-latest steps: # Caches @@ -74,7 +66,7 @@ jobs: git config --global user.name "pip" - uses: actions/checkout@v2 - name: Set up Python 3.8 - uses: actions/setup-python@v1 + uses: actions/setup-python@v2 with: python-version: 3.8 - name: Install tox and nox @@ -91,15 +83,14 @@ jobs: run: nox -s build-release -- 99.9 tests: - name: Tests / ${{ matrix.python }} / ${{ matrix.os }} - runs-on: ${{ matrix.os }}-latest + name: Tests / ${{ matrix.python }} + runs-on: macos-latest needs: dev-tools strategy: fail-fast: false matrix: - os: [MacOS] python: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9] steps: @@ -114,7 +105,7 @@ jobs: ${{ runner.os }}- # Setup - uses: actions/checkout@v2 - - uses: actions/setup-python@v1 + - uses: actions/setup-python@v2 with: python-version: ${{ matrix.python }} From 0608d2c5713b5ace57f013b99754942e04a6680c Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Tue, 27 Oct 2020 18:10:07 +0530 Subject: [PATCH 2629/3170] Remove the AP MacOS job --- .azure-pipelines/macos.yml | 7 ------- 1 file changed, 7 deletions(-) delete mode 100644 .azure-pipelines/macos.yml diff --git a/.azure-pipelines/macos.yml b/.azure-pipelines/macos.yml deleted file mode 100644 index 537ac3efdd4..00000000000 --- a/.azure-pipelines/macos.yml +++ /dev/null @@ -1,7 +0,0 @@ -variables: - CI: true - -jobs: -- template: jobs/package.yml - parameters: - vmImage: macos-10.14 From 94fbb6cf78c267bf7cdf83eeeb2536ad56cfe639 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 31 Jul 2020 07:58:28 +0800 Subject: [PATCH 2630/3170] Only do the ensure_text() dance on Windows POSIX is problematic when the environment is not configured properly. --- news/8658.bugfix.rst | 2 ++ src/pip/_internal/utils/misc.py | 2 +- src/pip/_internal/utils/temp_dir.py | 14 +++++++++++--- 3 files changed, 14 insertions(+), 4 deletions(-) create mode 100644 news/8658.bugfix.rst diff --git a/news/8658.bugfix.rst b/news/8658.bugfix.rst new file mode 100644 index 00000000000..6e43c8b3c0e --- /dev/null +++ b/news/8658.bugfix.rst @@ -0,0 +1,2 @@ +Only converts Windows path to unicode on Python 2 to avoid regressions when a +POSIX environment does not configure the file system encoding correctly. diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index c122beb32bb..4fb64d2672a 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -137,7 +137,7 @@ def get_prog(): # Retry every half second for up to 3 seconds @retry(stop_max_delay=3000, wait_fixed=500) def rmtree(dir, ignore_errors=False): - # type: (Text, bool) -> None + # type: (AnyStr, bool) -> None shutil.rmtree(dir, ignore_errors=ignore_errors, onerror=rmtree_errorhandler) diff --git a/src/pip/_internal/utils/temp_dir.py b/src/pip/_internal/utils/temp_dir.py index 03aa8286670..487f40db51f 100644 --- a/src/pip/_internal/utils/temp_dir.py +++ b/src/pip/_internal/utils/temp_dir.py @@ -10,6 +10,7 @@ from pip._vendor.contextlib2 import ExitStack from pip._vendor.six import ensure_text +from pip._internal.utils.compat import WINDOWS from pip._internal.utils.misc import enum, rmtree from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -193,10 +194,17 @@ def cleanup(self): """Remove the temporary directory created and reset state """ self._deleted = True - if os.path.exists(self._path): - # Make sure to pass unicode on Python 2 to make the contents also - # use unicode, ensuring non-ASCII names and can be represented. + if not os.path.exists(self._path): + return + # Make sure to pass unicode on Python 2 to make the contents also + # use unicode, ensuring non-ASCII names and can be represented. + # This is only done on Windows because POSIX platforms use bytes + # natively for paths, and the bytes-text conversion omission avoids + # errors caused by the environment configuring encodings incorrectly. + if WINDOWS: rmtree(ensure_text(self._path)) + else: + rmtree(self._path) class AdjacentTempDirectory(TempDirectory): From 95171c881fa8bc98b9dcd2f5c0c34cd7fce95904 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Tue, 20 Oct 2020 07:00:15 +0530 Subject: [PATCH 2631/3170] Display messages when backtracking on a package --- .../resolution/resolvelib/reporter.py | 35 +++++++++++++++ .../resolution/resolvelib/resolver.py | 5 ++- tests/functional/test_new_resolver.py | 45 +++++++++++++++++++ 3 files changed, 83 insertions(+), 2 deletions(-) create mode 100644 src/pip/_internal/resolution/resolvelib/reporter.py diff --git a/src/pip/_internal/resolution/resolvelib/reporter.py b/src/pip/_internal/resolution/resolvelib/reporter.py new file mode 100644 index 00000000000..56e80597511 --- /dev/null +++ b/src/pip/_internal/resolution/resolvelib/reporter.py @@ -0,0 +1,35 @@ +from collections import defaultdict +from logging import getLogger + +from pip._vendor.resolvelib.reporters import BaseReporter + +logger = getLogger(__name__) + + +class PipReporter(BaseReporter): + + def __init__(self): + self.backtracks_by_package = defaultdict(int) + + self._messages_at_backtrack = { + 8: ( + "pip is looking at multiple versions of this package to determine " + "which version is compatible with other requirements. " + "This could take a while." + ), + 13: ( + "This is taking longer than usual. You might need to provide the " + "dependency resolver with stricter constraints to reduce runtime." + "If you want to abort this run, you can press Ctrl + C to do so." + ) + } + + def backtracking(self, candidate): + self.backtracks_by_package[candidate.name] += 1 + + count = self.backtracks_by_package[candidate.name] + if count not in self._messages_at_backtrack: + return + + message = self._messages_at_backtrack[count] + logger.info(message) diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index cb7d1ae8a59..acb7cfeda7a 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -3,7 +3,7 @@ from pip._vendor import six from pip._vendor.packaging.utils import canonicalize_name -from pip._vendor.resolvelib import BaseReporter, ResolutionImpossible +from pip._vendor.resolvelib import ResolutionImpossible from pip._vendor.resolvelib import Resolver as RLResolver from pip._internal.exceptions import InstallationError @@ -11,6 +11,7 @@ from pip._internal.req.req_set import RequirementSet from pip._internal.resolution.base import BaseResolver from pip._internal.resolution.resolvelib.provider import PipProvider +from pip._internal.resolution.resolvelib.reporter import PipReporter from pip._internal.utils.misc import dist_is_editable from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -103,7 +104,7 @@ def resolve(self, root_reqs, check_supported_wheels): upgrade_strategy=self.upgrade_strategy, user_requested=user_requested, ) - reporter = BaseReporter() + reporter = PipReporter() resolver = RLResolver(provider, reporter) try: diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 1718ab8a8b8..aa1744cc8dd 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -1046,3 +1046,48 @@ def test_new_resolver_prefers_installed_in_upgrade_if_latest(script): "pkg", ) assert_installed(script, pkg="2") + + +@pytest.mark.parametrize("N", [10, 20]) +def test_new_resolver_presents_messages_when_backtracking_a_lot(script, N): + # Generate a set of wheels that will definitely cause backtracking. + for index in range(1, N+1): + A_version = "{index}.0.0".format(index=index) + B_version = "{index}.0.0".format(index=index) + C_version = "{index_minus_one}.0.0".format(index_minus_one=index - 1) + + depends = ["B == " + B_version] + if index != 1: + depends.append("C == " + C_version) + + print("A", A_version, "B", B_version, "C", C_version) + create_basic_wheel_for_package(script, "A", A_version, depends=depends) + + for index in range(1, N+1): + B_version = "{index}.0.0".format(index=index) + C_version = "{index}.0.0".format(index=index) + depends = ["C == " + C_version] + + print("B", B_version, "C", C_version) + create_basic_wheel_for_package(script, "B", B_version, depends=depends) + + for index in range(1, N+1): + C_version = "{index}.0.0".format(index=index) + print("C", C_version) + create_basic_wheel_for_package(script, "C", C_version) + + # Install A + result = script.pip( + "install", + "--use-feature=2020-resolver", + "--no-cache-dir", + "--no-index", + "--find-links", script.scratch_path, + "A" + ) + + assert_installed(script, A="1.0.0", B="1.0.0", C="1.0.0") + if N >= 8: # this number is hard-coded in the code too. + assert "This could take a while." in result.stdout + if N >= 13: # this number is hard-coded in the code too. + assert "press Ctrl + C" in result.stdout From daa003bd9bcfeeb15181af40d5e73ebb4cfa52ad Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Tue, 20 Oct 2020 07:07:14 +0530 Subject: [PATCH 2632/3170] Make mypy happy --- src/pip/_internal/resolution/resolvelib/reporter.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/resolution/resolvelib/reporter.py b/src/pip/_internal/resolution/resolvelib/reporter.py index 56e80597511..b0da83c7ee2 100644 --- a/src/pip/_internal/resolution/resolvelib/reporter.py +++ b/src/pip/_internal/resolution/resolvelib/reporter.py @@ -3,13 +3,22 @@ from pip._vendor.resolvelib.reporters import BaseReporter +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import DefaultDict + + from .base import Candidate + + logger = getLogger(__name__) class PipReporter(BaseReporter): def __init__(self): - self.backtracks_by_package = defaultdict(int) + # type: () -> None + self.backtracks_by_package = defaultdict(int) # type: DefaultDict[str, int] self._messages_at_backtrack = { 8: ( @@ -25,6 +34,7 @@ def __init__(self): } def backtracking(self, candidate): + # type: (Candidate) -> None self.backtracks_by_package[candidate.name] += 1 count = self.backtracks_by_package[candidate.name] From 9a1f790951332dea5d6811d5dd243e6f97571922 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Tue, 20 Oct 2020 15:40:17 +0530 Subject: [PATCH 2633/3170] :newspaper: --- news/8975.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/8975.feature.rst diff --git a/news/8975.feature.rst b/news/8975.feature.rst new file mode 100644 index 00000000000..082612505be --- /dev/null +++ b/news/8975.feature.rst @@ -0,0 +1 @@ +Log an informational message when backtracking takes multiple rounds on a specific package. From f3307a5103b603d582da673d3a3b21c108ecf443 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Mon, 26 Oct 2020 16:37:10 +0530 Subject: [PATCH 2634/3170] Present a message upon first backtrack --- src/pip/_internal/resolution/resolvelib/reporter.py | 5 +++++ tests/functional/test_new_resolver.py | 9 ++++++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/reporter.py b/src/pip/_internal/resolution/resolvelib/reporter.py index b0da83c7ee2..1d0dd9798fc 100644 --- a/src/pip/_internal/resolution/resolvelib/reporter.py +++ b/src/pip/_internal/resolution/resolvelib/reporter.py @@ -21,6 +21,11 @@ def __init__(self): self.backtracks_by_package = defaultdict(int) # type: DefaultDict[str, int] self._messages_at_backtrack = { + 1: ( + "pip is looking at multiple versions of this package to determine " + "which version is compatible with other requirements. " + "This could take a while." + ), 8: ( "pip is looking at multiple versions of this package to determine " "which version is compatible with other requirements. " diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index aa1744cc8dd..91befb7f53c 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -1048,7 +1048,7 @@ def test_new_resolver_prefers_installed_in_upgrade_if_latest(script): assert_installed(script, pkg="2") -@pytest.mark.parametrize("N", [10, 20]) +@pytest.mark.parametrize("N", [2, 10, 20]) def test_new_resolver_presents_messages_when_backtracking_a_lot(script, N): # Generate a set of wheels that will definitely cause backtracking. for index in range(1, N+1): @@ -1087,7 +1087,10 @@ def test_new_resolver_presents_messages_when_backtracking_a_lot(script, N): ) assert_installed(script, A="1.0.0", B="1.0.0", C="1.0.0") - if N >= 8: # this number is hard-coded in the code too. + # These numbers are hard-coded in the code. + if N >= 1: assert "This could take a while." in result.stdout - if N >= 13: # this number is hard-coded in the code too. + if N >= 8: + assert result.stdout.count("This could take a while.") >= 2 + if N >= 13: assert "press Ctrl + C" in result.stdout From 1acca46aa326cd8e5e594b416bf98e7d1a1d1f8c Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Mon, 26 Oct 2020 16:41:59 +0530 Subject: [PATCH 2635/3170] Prefix backtracking message with "INFO: " --- src/pip/_internal/resolution/resolvelib/reporter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/resolution/resolvelib/reporter.py b/src/pip/_internal/resolution/resolvelib/reporter.py index 1d0dd9798fc..dc7bc5ad49a 100644 --- a/src/pip/_internal/resolution/resolvelib/reporter.py +++ b/src/pip/_internal/resolution/resolvelib/reporter.py @@ -47,4 +47,4 @@ def backtracking(self, candidate): return message = self._messages_at_backtrack[count] - logger.info(message) + logger.info("INFO: %s", message) From 55e316a45256d054d19425015ef13868a84c5ff1 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Tue, 27 Oct 2020 01:22:13 +0530 Subject: [PATCH 2636/3170] Add the last line to the info message --- src/pip/_internal/resolution/resolvelib/reporter.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pip/_internal/resolution/resolvelib/reporter.py b/src/pip/_internal/resolution/resolvelib/reporter.py index dc7bc5ad49a..e150c494867 100644 --- a/src/pip/_internal/resolution/resolvelib/reporter.py +++ b/src/pip/_internal/resolution/resolvelib/reporter.py @@ -35,6 +35,8 @@ def __init__(self): "This is taking longer than usual. You might need to provide the " "dependency resolver with stricter constraints to reduce runtime." "If you want to abort this run, you can press Ctrl + C to do so." + "To improve how pip performs, tell us that this happened here: " + "https://pip.pypa.io/surveys/backtracking" ) } From 40904e3a058e5aca33f7103bd7769b98242c18fa Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Sun, 25 Oct 2020 10:35:38 +0000 Subject: [PATCH 2637/3170] Remove --build-dir option, as per deprecation --- src/pip/_internal/cli/base_command.py | 14 -------------- src/pip/_internal/cli/cmdoptions.py | 23 ----------------------- src/pip/_internal/commands/download.py | 5 +---- src/pip/_internal/commands/install.py | 6 +----- src/pip/_internal/commands/wheel.py | 5 +---- src/pip/_internal/req/req_install.py | 4 ++++ src/pip/_internal/utils/temp_dir.py | 2 ++ 7 files changed, 9 insertions(+), 50 deletions(-) diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index e4b07e0ce8c..f4f86e0e0d0 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -197,20 +197,6 @@ def _main(self, args): ) options.cache_dir = None - if getattr(options, "build_dir", None): - deprecated( - reason=( - "The -b/--build/--build-dir/--build-directory " - "option is deprecated." - ), - replacement=( - "use the TMPDIR/TEMP/TMP environment variable, " - "possibly combined with --no-clean" - ), - gone_in="20.3", - issue=8333, - ) - if 'resolver' in options.unstable_features: logger.critical( "--unstable-feature=resolver is no longer supported, and " diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index 2f640b2cbb2..e96eac586db 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -685,29 +685,6 @@ def _handle_no_cache_dir(option, opt, value, parser): ) # type: Callable[..., Option] -def _handle_build_dir(option, opt, value, parser): - # type: (Option, str, str, OptionParser) -> None - if value: - value = os.path.abspath(value) - setattr(parser.values, option.dest, value) - - -build_dir = partial( - PipOption, - '-b', '--build', '--build-dir', '--build-directory', - dest='build_dir', - type='path', - metavar='dir', - action='callback', - callback=_handle_build_dir, - help='(DEPRECATED) ' - '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: Callable[..., Option] - ignore_requires_python = partial( Option, '--ignore-requires-python', diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index 2f151e049cf..9535ef3cbe4 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -43,7 +43,6 @@ def add_options(self): # type: () -> None self.cmd_opts.add_option(cmdoptions.constraints()) self.cmd_opts.add_option(cmdoptions.requirements()) - self.cmd_opts.add_option(cmdoptions.build_dir()) self.cmd_opts.add_option(cmdoptions.no_deps()) self.cmd_opts.add_option(cmdoptions.global_options()) self.cmd_opts.add_option(cmdoptions.no_binary()) @@ -97,13 +96,11 @@ def run(self, options, args): session=session, target_python=target_python, ) - build_delete = (not (options.no_clean or options.build_dir)) req_tracker = self.enter_context(get_requirement_tracker()) directory = TempDirectory( - options.build_dir, - delete=build_delete, + delete=not options.no_clean, kind="download", globally_managed=True, ) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index e41660070a0..a4001553bfa 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -129,8 +129,6 @@ def add_options(self): help="Installation prefix where lib, bin and other top-level " "folders are placed") - self.cmd_opts.add_option(cmdoptions.build_dir()) - self.cmd_opts.add_option(cmdoptions.src()) self.cmd_opts.add_option( @@ -277,14 +275,12 @@ def run(self, options, args): target_python=target_python, ignore_requires_python=options.ignore_requires_python, ) - build_delete = (not (options.no_clean or options.build_dir)) wheel_cache = WheelCache(options.cache_dir, options.format_control) req_tracker = self.enter_context(get_requirement_tracker()) directory = TempDirectory( - options.build_dir, - delete=build_delete, + delete=not options.no_clean, kind="install", globally_managed=True, ) diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index 8f5783c353f..2d654338d7a 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -78,7 +78,6 @@ def add_options(self): self.cmd_opts.add_option(cmdoptions.src()) self.cmd_opts.add_option(cmdoptions.ignore_requires_python()) self.cmd_opts.add_option(cmdoptions.no_deps()) - self.cmd_opts.add_option(cmdoptions.build_dir()) self.cmd_opts.add_option(cmdoptions.progress_bar()) self.cmd_opts.add_option( @@ -115,7 +114,6 @@ def run(self, options, args): session = self.get_default_session(options) finder = self._build_package_finder(options, session) - build_delete = (not (options.no_clean or options.build_dir)) wheel_cache = WheelCache(options.cache_dir, options.format_control) options.wheel_dir = normalize_path(options.wheel_dir) @@ -124,8 +122,7 @@ def run(self, options, args): req_tracker = self.enter_context(get_requirement_tracker()) directory = TempDirectory( - options.build_dir, - delete=build_delete, + delete=not options.no_clean, kind="wheel", globally_managed=True, ) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 8ce299503b8..866d18fcb6e 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -358,6 +358,10 @@ def ensure_build_location(self, build_dir, autodelete, parallel_builds): return self._temp_build_dir.path + # This is the only remaining place where we manually determine the path + # for the temporary directory. It is only needed for editables where + # it is the value of the --src option. + # When parallel builds are enabled, add a UUID to the build directory # name so multiple builds do not interfere with each other. dir_name = canonicalize_name(self.name) diff --git a/src/pip/_internal/utils/temp_dir.py b/src/pip/_internal/utils/temp_dir.py index 03aa8286670..220a093e2fe 100644 --- a/src/pip/_internal/utils/temp_dir.py +++ b/src/pip/_internal/utils/temp_dir.py @@ -134,6 +134,8 @@ def __init__( # tempdir_registry says. delete = None + # The only time we specify path is in for editables where it + # is the value of the --src option. if path is None: path = self._create(kind) From 97e2c7a345c9d626eae0f37f7611775ae974ed5b Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Sun, 25 Oct 2020 10:38:18 +0000 Subject: [PATCH 2638/3170] Add news entry --- news/9049.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/9049.feature.rst diff --git a/news/9049.feature.rst b/news/9049.feature.rst new file mode 100644 index 00000000000..1cf0916f9a2 --- /dev/null +++ b/news/9049.feature.rst @@ -0,0 +1 @@ +Remove the ``--build-dir`` option, as per the deprecation. From 7c4c5b83300a44c506ab4f93a12080a34d4b2705 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Sun, 25 Oct 2020 14:26:28 +0000 Subject: [PATCH 2639/3170] Remove test_pip_wheel_fail_cause_of_previous_build_dir as it's no longer valid --- tests/functional/test_wheel.py | 38 +--------------------------------- 1 file changed, 1 insertion(+), 37 deletions(-) diff --git a/tests/functional/test_wheel.py b/tests/functional/test_wheel.py index 0b58c923785..73c741f8d2d 100644 --- a/tests/functional/test_wheel.py +++ b/tests/functional/test_wheel.py @@ -6,7 +6,7 @@ import pytest -from pip._internal.cli.status_codes import ERROR, PREVIOUS_BUILD_DIR_ERROR +from pip._internal.cli.status_codes import ERROR from tests.lib import pyversion # noqa: F401 @@ -229,42 +229,6 @@ def test_pip_wheel_source_deps(script, data): assert "Successfully built source" in result.stdout, result.stdout -def test_pip_wheel_fail_cause_of_previous_build_dir( - script, - data, - use_new_resolver, -): - """ - Test when 'pip wheel' tries to install a package that has a previous build - directory - """ - - # Given that I have a previous build dir of the `simple` package - build = script.venv_path / 'build' / 'simple' - os.makedirs(build) - build.joinpath('setup.py').write_text('#') - - # When I call pip trying to install things again - result = script.pip( - 'wheel', '--no-index', - '--find-links={data.find_links}'.format(**locals()), - '--build', script.venv_path / 'build', - 'simple==3.0', - expect_error=(not use_new_resolver), - expect_temp=(not use_new_resolver), - expect_stderr=True, - ) - - assert ( - "The -b/--build/--build-dir/--build-directory " - "option is deprecated." - ) in result.stderr - - # Then I see that the error code is the right one - if not use_new_resolver: - assert result.returncode == PREVIOUS_BUILD_DIR_ERROR, result - - def test_wheel_package_with_latin1_setup(script, data): """Create a wheel from a package with latin-1 encoded setup.py.""" From 0c2a2bc803b677e9b5a4b0cc571b7ed070da4e78 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Sun, 25 Oct 2020 19:06:14 +0000 Subject: [PATCH 2640/3170] Mark a couple of tests that still use --build as xfail --- tests/functional/test_install_cleanup.py | 3 +++ tests/functional/test_wheel.py | 3 +++ 2 files changed, 6 insertions(+) diff --git a/tests/functional/test_install_cleanup.py b/tests/functional/test_install_cleanup.py index c01c47c3e3e..30102fd2096 100644 --- a/tests/functional/test_install_cleanup.py +++ b/tests/functional/test_install_cleanup.py @@ -7,6 +7,9 @@ @pytest.mark.network +@pytest.mark.xfail( + reason="The --build option was removed" +) def test_no_clean_option_blocks_cleaning_after_install(script, data): """ Test --no-clean option blocks cleaning after install diff --git a/tests/functional/test_wheel.py b/tests/functional/test_wheel.py index 73c741f8d2d..5e91fea8ab8 100644 --- a/tests/functional/test_wheel.py +++ b/tests/functional/test_wheel.py @@ -187,6 +187,9 @@ def test_pip_wheel_fail(script, data): assert result.returncode != 0 +@pytest.mark.xfail( + reason="The --build option was removed" +) def test_no_clean_option_blocks_cleaning_after_wheel( script, data, From 52e1c9a39c363d7443f001c375e7eb2705f5605a Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Tue, 27 Oct 2020 15:44:17 +0000 Subject: [PATCH 2641/3170] Not clear why this didn't fail before... --- tests/functional/test_install_cleanup.py | 35 ------------------------ 1 file changed, 35 deletions(-) diff --git a/tests/functional/test_install_cleanup.py b/tests/functional/test_install_cleanup.py index 30102fd2096..10e49124960 100644 --- a/tests/functional/test_install_cleanup.py +++ b/tests/functional/test_install_cleanup.py @@ -1,10 +1,7 @@ -import os from os.path import exists import pytest -from pip._internal.cli.status_codes import PREVIOUS_BUILD_DIR_ERROR - @pytest.mark.network @pytest.mark.xfail( @@ -26,38 +23,6 @@ def test_no_clean_option_blocks_cleaning_after_install(script, data): assert exists(build) -@pytest.mark.network -def test_cleanup_prevented_upon_build_dir_exception( - script, - data, - use_new_resolver, -): - """ - Test no cleanup occurs after a PreviousBuildDirError - """ - build = script.venv_path / 'build' - build_simple = build / 'simple' - os.makedirs(build_simple) - build_simple.joinpath("setup.py").write_text("#") - result = script.pip( - 'install', '-f', data.find_links, '--no-index', 'simple', - '--build', build, - expect_error=(not use_new_resolver), - expect_temp=(not use_new_resolver), - expect_stderr=True, - ) - - assert ( - "The -b/--build/--build-dir/--build-directory " - "option is deprecated." - ) in result.stderr - - if not use_new_resolver: - assert result.returncode == PREVIOUS_BUILD_DIR_ERROR, str(result) - assert "pip can't proceed" in result.stderr, str(result) - assert exists(build_simple), str(result) - - @pytest.mark.network def test_pep517_no_legacy_cleanup(script, data, with_wheel): """Test a PEP 517 failed build does not attempt a legacy cleanup""" From cea9f32daef08a2172bc7b44cce9952db1cb7ca1 Mon Sep 17 00:00:00 2001 From: Daniel Katz <katzdm@gmail.com> Date: Fri, 28 Aug 2020 12:55:17 -0400 Subject: [PATCH 2642/3170] Support multiple `abi` and `platform` values for `pip download`. --- docs/html/reference/pip_download.rst | 29 ++++++++++++++++++ news/6121.feature | 1 + src/pip/_internal/cli/cmdoptions.py | 19 +++++++++--- src/pip/_internal/models/target_python.py | 30 +++++++++---------- src/pip/_internal/utils/compatibility_tags.py | 19 +++++------- tests/unit/test_models_wheel.py | 20 ++++++------- tests/unit/test_target_python.py | 8 ++--- tests/unit/test_utils_compatibility_tags.py | 4 +-- 8 files changed, 84 insertions(+), 46 deletions(-) create mode 100644 news/6121.feature diff --git a/docs/html/reference/pip_download.rst b/docs/html/reference/pip_download.rst index 80acc1942fd..b600d15e560 100644 --- a/docs/html/reference/pip_download.rst +++ b/docs/html/reference/pip_download.rst @@ -197,3 +197,32 @@ Examples C:\> dir pip-8.1.1-py2.py3-none-any.whl pip-8.1.1-py2.py3-none-any.whl + +#. Download a package supporting one of several ABIs and platforms. + This is useful when fetching wheels for a well-defined interpreter, whose + supported ABIs and platforms are known and fixed, different than the one pip is + running under. + + .. tab:: Unix/macOS + + .. code-block:: console + + $ python -m pip download \ + --only-binary=:all: \ + --platform manylinux1_x86_64 --platform linux_x86_64 --platform any \ + --python-version 36 \ + --implementation cp \ + --abi cp36m --abi cp36 --abi abi3 --abi none \ + SomePackage + + .. tab:: Windows + + .. code-block:: console + + C:> py -m pip download ^ + --only-binary=:all: ^ + --platform manylinux1_x86_64 --platform linux_x86_64 --platform any ^ + --python-version 36 ^ + --implementation cp ^ + --abi cp36m --abi cp36 --abi abi3 --abi none ^ + SomePackage diff --git a/news/6121.feature b/news/6121.feature new file mode 100644 index 00000000000..016426e885d --- /dev/null +++ b/news/6121.feature @@ -0,0 +1 @@ +Allow comma-separated values for --abi and --platform. diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index e96eac586db..fe2fae3563f 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -31,7 +31,7 @@ if MYPY_CHECK_RUNNING: from optparse import OptionParser, Values - from typing import Any, Callable, Dict, Optional, Tuple + from typing import Any, Callable, Dict, List, Optional, Tuple from pip._internal.cli.parser import ConfigOptionParser @@ -588,7 +588,7 @@ def _handle_python_version(option, opt_str, value, parser): metavar='abi', default=None, help=("Only use wheels compatible with Python " - "abi <abi>, e.g. 'pypy_41'. If not specified, then the " + "abi <abi>, e.g. 'pypy_41,none'. If not specified, then the " "current interpreter abi tag is used. Generally " "you will need to specify --implementation, " "--platform, and --python-version when using " @@ -606,10 +606,21 @@ def add_target_python_options(cmd_opts): def make_target_python(options): # type: (Values) -> TargetPython + + # abi can be a comma-separated list of values. + abis = options.abi # type: Optional[List[str]] + if options.abi: + abis = options.abi.split(',') + + # platform can also be a comma-separated list of values. + platforms = options.platform # type: Optional[List[str]] + if options.platform: + platforms = options.platform.split(',') + target_python = TargetPython( - platform=options.platform, + platforms=platforms, py_version_info=options.python_version, - abi=options.abi, + abis=abis, implementation=options.implementation, ) diff --git a/src/pip/_internal/models/target_python.py b/src/pip/_internal/models/target_python.py index ad7e506a6a9..4593dc854f8 100644 --- a/src/pip/_internal/models/target_python.py +++ b/src/pip/_internal/models/target_python.py @@ -19,9 +19,9 @@ class TargetPython(object): __slots__ = [ "_given_py_version_info", - "abi", + "abis", "implementation", - "platform", + "platforms", "py_version", "py_version_info", "_valid_tags", @@ -29,23 +29,23 @@ class TargetPython(object): def __init__( self, - platform=None, # type: Optional[str] + platforms=None, # type: Optional[List[str]] py_version_info=None, # type: Optional[Tuple[int, ...]] - abi=None, # type: Optional[str] + abis=None, # type: Optional[List[str]] implementation=None, # type: Optional[str] ): # type: (...) -> None """ - :param platform: A string or None. If None, searches for packages - that are supported by the current system. Otherwise, will find - packages that can be built on the platform passed in. These + :param platforms: A list of strings or None. If None, searches for + packages that are supported by the current system. Otherwise, will + find packages that can be built on the platforms passed in. These packages will only be downloaded for distribution: they will not be built locally. :param py_version_info: An optional tuple of ints representing the Python version information to use (e.g. `sys.version_info[:3]`). This can have length 1, 2, or 3 when provided. - :param abi: A string or None. This is passed to compatibility_tags.py's - get_supported() function as is. + :param abis: A list of strings or None. This is passed to + compatibility_tags.py's get_supported() function as is. :param implementation: A string or None. This is passed to compatibility_tags.py's get_supported() function as is. """ @@ -59,9 +59,9 @@ def __init__( py_version = '.'.join(map(str, py_version_info[:2])) - self.abi = abi + self.abis = abis self.implementation = implementation - self.platform = platform + self.platforms = platforms self.py_version = py_version self.py_version_info = py_version_info @@ -80,9 +80,9 @@ def format_given(self): ) key_values = [ - ('platform', self.platform), + ('platforms', self.platforms), ('version_info', display_version), - ('abi', self.abi), + ('abis', self.abis), ('implementation', self.implementation), ] return ' '.join( @@ -108,8 +108,8 @@ def get_tags(self): tags = get_supported( version=version, - platform=self.platform, - abi=self.abi, + platforms=self.platforms, + abis=self.abis, impl=self.implementation, ) self._valid_tags = tags diff --git a/src/pip/_internal/utils/compatibility_tags.py b/src/pip/_internal/utils/compatibility_tags.py index 4f21874ec6b..eb1727e3d95 100644 --- a/src/pip/_internal/utils/compatibility_tags.py +++ b/src/pip/_internal/utils/compatibility_tags.py @@ -105,9 +105,9 @@ def _get_custom_interpreter(implementation=None, version=None): def get_supported( version=None, # type: Optional[str] - platform=None, # type: Optional[str] + platforms=None, # type: Optional[List[str]] impl=None, # type: Optional[str] - abi=None # type: Optional[str] + abis=None # type: Optional[List[str]] ): # type: (...) -> List[Tag] """Return a list of supported tags for each version specified in @@ -115,11 +115,11 @@ def get_supported( :param version: a string version, of the form "33" or "32", or None. The version will be assumed to support our ABI. - :param platform: specify the exact platform you want valid + :param platform: specify a list of platforms you want valid tags for, or None. If None, use the local system platform. :param impl: specify the exact implementation you want valid tags for, or None. If None, use the local interpreter impl. - :param abi: specify the exact abi you want valid + :param abis: specify a list of abis you want valid tags for, or None. If None, use the local interpreter abi. """ supported = [] # type: List[Tag] @@ -130,13 +130,10 @@ def get_supported( interpreter = _get_custom_interpreter(impl, version) - abis = None # type: Optional[List[str]] - if abi is not None: - abis = [abi] - - platforms = None # type: Optional[List[str]] - if platform is not None: - platforms = _get_custom_platforms(platform) + if platforms and len(platforms) == 1: + # Only expand list of platforms if a single platform was provided. + # Otherwise, assume that the list provided is comprehensive. + platforms = _get_custom_platforms(platforms[0]) is_cpython = (impl or interpreter_name()) == "cp" if is_cpython: diff --git a/tests/unit/test_models_wheel.py b/tests/unit/test_models_wheel.py index f1fef6f09e8..05ee74262dd 100644 --- a/tests/unit/test_models_wheel.py +++ b/tests/unit/test_models_wheel.py @@ -76,7 +76,7 @@ def test_supported_osx_version(self): Wheels built for macOS 10.6 are supported on 10.9 """ tags = compatibility_tags.get_supported( - '27', platform='macosx_10_9_intel', impl='cp' + '27', platforms=['macosx_10_9_intel'], impl='cp' ) w = Wheel('simple-0.1-cp27-none-macosx_10_6_intel.whl') assert w.supported(tags=tags) @@ -88,7 +88,7 @@ def test_not_supported_osx_version(self): Wheels built for macOS 10.9 are not supported on 10.6 """ tags = compatibility_tags.get_supported( - '27', platform='macosx_10_6_intel', impl='cp' + '27', platforms=['macosx_10_6_intel'], impl='cp' ) w = Wheel('simple-0.1-cp27-none-macosx_10_9_intel.whl') assert not w.supported(tags=tags) @@ -98,22 +98,22 @@ def test_supported_multiarch_darwin(self): Multi-arch wheels (intel) are supported on components (i386, x86_64) """ universal = compatibility_tags.get_supported( - '27', platform='macosx_10_5_universal', impl='cp' + '27', platforms=['macosx_10_5_universal'], impl='cp' ) intel = compatibility_tags.get_supported( - '27', platform='macosx_10_5_intel', impl='cp' + '27', platforms=['macosx_10_5_intel'], impl='cp' ) x64 = compatibility_tags.get_supported( - '27', platform='macosx_10_5_x86_64', impl='cp' + '27', platforms=['macosx_10_5_x86_64'], impl='cp' ) i386 = compatibility_tags.get_supported( - '27', platform='macosx_10_5_i386', impl='cp' + '27', platforms=['macosx_10_5_i386'], impl='cp' ) ppc = compatibility_tags.get_supported( - '27', platform='macosx_10_5_ppc', impl='cp' + '27', platforms=['macosx_10_5_ppc'], impl='cp' ) ppc64 = compatibility_tags.get_supported( - '27', platform='macosx_10_5_ppc64', impl='cp' + '27', platforms=['macosx_10_5_ppc64'], impl='cp' ) w = Wheel('simple-0.1-cp27-none-macosx_10_5_intel.whl') @@ -136,10 +136,10 @@ def test_not_supported_multiarch_darwin(self): Single-arch wheels (x86_64) are not supported on multi-arch (intel) """ universal = compatibility_tags.get_supported( - '27', platform='macosx_10_5_universal', impl='cp' + '27', platforms=['macosx_10_5_universal'], impl='cp' ) intel = compatibility_tags.get_supported( - '27', platform='macosx_10_5_intel', impl='cp' + '27', platforms=['macosx_10_5_intel'], impl='cp' ) w = Wheel('simple-0.1-cp27-none-macosx_10_5_i386.whl') diff --git a/tests/unit/test_target_python.py b/tests/unit/test_target_python.py index 0dc2af22bd0..a314988ebc0 100644 --- a/tests/unit/test_target_python.py +++ b/tests/unit/test_target_python.py @@ -45,16 +45,16 @@ def test_init__py_version_info_none(self): ({}, ''), (dict(py_version_info=(3, 6)), "version_info='3.6'"), ( - dict(platform='darwin', py_version_info=(3, 6)), - "platform='darwin' version_info='3.6'", + dict(platforms=['darwin'], py_version_info=(3, 6)), + "platforms=['darwin'] version_info='3.6'", ), ( dict( - platform='darwin', py_version_info=(3, 6), abi='cp36m', + platforms=['darwin'], py_version_info=(3, 6), abis=['cp36m'], implementation='cp' ), ( - "platform='darwin' version_info='3.6' abi='cp36m' " + "platforms=['darwin'] version_info='3.6' abis=['cp36m'] " "implementation='cp'" ), ), diff --git a/tests/unit/test_utils_compatibility_tags.py b/tests/unit/test_utils_compatibility_tags.py index 12c8da453d9..64f59a2f98d 100644 --- a/tests/unit/test_utils_compatibility_tags.py +++ b/tests/unit/test_utils_compatibility_tags.py @@ -63,7 +63,7 @@ def test_manylinux2010_implies_manylinux1(self, manylinux2010, manylinux1): Specifying manylinux2010 implies manylinux1. """ groups = {} - supported = compatibility_tags.get_supported(platform=manylinux2010) + supported = compatibility_tags.get_supported(platforms=[manylinux2010]) for tag in supported: groups.setdefault( (tag.interpreter, tag.abi), [] @@ -87,7 +87,7 @@ def test_manylinuxA_implies_manylinuxB(self, manylinuxA, manylinuxB): Specifying manylinux2014 implies manylinux2010/manylinux1. """ groups = {} - supported = compatibility_tags.get_supported(platform=manylinuxA) + supported = compatibility_tags.get_supported(platforms=[manylinuxA]) for tag in supported: groups.setdefault( (tag.interpreter, tag.abi), [] From abf987bde3d3fa019189f252152aa018b3b0a30d Mon Sep 17 00:00:00 2001 From: Daniel Katz <katzdm@gmail.com> Date: Sat, 29 Aug 2020 18:55:49 -0400 Subject: [PATCH 2643/3170] Use 'append'-style CLI arguments, rather than ','-separated values. --- news/6121.feature | 2 +- src/pip/_internal/cli/cmdoptions.py | 34 +++++++++++------------------ 2 files changed, 14 insertions(+), 22 deletions(-) diff --git a/news/6121.feature b/news/6121.feature index 016426e885d..16b272a69f7 100644 --- a/news/6121.feature +++ b/news/6121.feature @@ -1 +1 @@ -Allow comma-separated values for --abi and --platform. +Allow multiple values for --abi and --platform. diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index fe2fae3563f..e65ce84c849 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -495,9 +495,11 @@ def only_binary(): '--platform', dest='platform', metavar='platform', + action='append', default=None, - help=("Only use wheels compatible with <platform>. " - "Defaults to the platform of the running system."), + help=("Only use wheels compatible with <platform>. Defaults to the " + "platform of the running system. Use multiple options to specify " + "multiple platforms supported by the target interpreter."), ) # type: Callable[..., Option] @@ -586,13 +588,14 @@ def _handle_python_version(option, opt_str, value, parser): '--abi', dest='abi', metavar='abi', + action='append', default=None, - help=("Only use wheels compatible with Python " - "abi <abi>, e.g. 'pypy_41,none'. If not specified, then the " - "current interpreter abi tag is used. Generally " - "you will need to specify --implementation, " - "--platform, and --python-version when using " - "this option."), + help=("Only use wheels compatible with Python abi <abi>, e.g. 'pypy_41'. " + "If not specified, then the current interpreter abi tag is used. " + "Use multiple options to specify multiple abis supported by the " + "target interpreter. Generally you will need to specify " + "--implementation, --platform, and --python-version when using this " + "option."), ) # type: Callable[..., Option] @@ -606,21 +609,10 @@ def add_target_python_options(cmd_opts): def make_target_python(options): # type: (Values) -> TargetPython - - # abi can be a comma-separated list of values. - abis = options.abi # type: Optional[List[str]] - if options.abi: - abis = options.abi.split(',') - - # platform can also be a comma-separated list of values. - platforms = options.platform # type: Optional[List[str]] - if options.platform: - platforms = options.platform.split(',') - target_python = TargetPython( - platforms=platforms, + platforms=options.platform, py_version_info=options.python_version, - abis=abis, + abis=options.abi, implementation=options.implementation, ) From 7237bd3397dd22a44ea65bb869d15d7b5e6a4310 Mon Sep 17 00:00:00 2001 From: Daniel Katz <katzdm@gmail.com> Date: Wed, 9 Sep 2020 09:58:58 -0400 Subject: [PATCH 2644/3170] Respond to feedback, and add functional tests. --- src/pip/_internal/cli/cmdoptions.py | 10 +++++----- tests/functional/test_download.py | 31 +++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index e65ce84c849..d7b272ef3a9 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -31,7 +31,7 @@ if MYPY_CHECK_RUNNING: from optparse import OptionParser, Values - from typing import Any, Callable, Dict, List, Optional, Tuple + from typing import Any, Callable, Dict, Optional, Tuple from pip._internal.cli.parser import ConfigOptionParser @@ -498,8 +498,8 @@ def only_binary(): action='append', default=None, help=("Only use wheels compatible with <platform>. Defaults to the " - "platform of the running system. Use multiple options to specify " - "multiple platforms supported by the target interpreter."), + "platform of the running system. Use this option multiple times to " + "specify multiple platforms supported by the target interpreter."), ) # type: Callable[..., Option] @@ -592,8 +592,8 @@ def _handle_python_version(option, opt_str, value, parser): default=None, help=("Only use wheels compatible with Python abi <abi>, e.g. 'pypy_41'. " "If not specified, then the current interpreter abi tag is used. " - "Use multiple options to specify multiple abis supported by the " - "target interpreter. Generally you will need to specify " + "Use this option multiple times to specify multiple abis supported " + "by the target interpreter. Generally you will need to specify " "--implementation, --platform, and --python-version when using this " "option."), ) # type: Callable[..., Option] diff --git a/tests/functional/test_download.py b/tests/functional/test_download.py index 3291d580d23..2eee51b086a 100644 --- a/tests/functional/test_download.py +++ b/tests/functional/test_download.py @@ -309,6 +309,21 @@ def test_download_specify_platform(script, data): Path('scratch') / 'fake-2.0-py2.py3-none-linux_x86_64.whl' ) + # Test with multiple supported platforms specified. + data.reset() + fake_wheel(data, 'fake-3.0-py2.py3-none-linux_x86_64.whl') + result = script.pip( + 'download', '--no-index', '--find-links', data.find_links, + '--only-binary=:all:', + '--dest', '.', + '--platform', 'manylinux1_x86_64', '--platform', 'linux_x86_64', + '--platform', 'any', + 'fake==3' + ) + result.did_create( + Path('scratch') / 'fake-3.0-py2.py3-none-linux_x86_64.whl' + ) + class TestDownloadPlatformManylinuxes(object): """ @@ -575,6 +590,22 @@ def test_download_specify_abi(script, data): expect_error=True, ) + data.reset() + fake_wheel(data, 'fake-1.0-fk2-otherabi-fake_platform.whl') + result = script.pip( + 'download', '--no-index', '--find-links', data.find_links, + '--only-binary=:all:', + '--dest', '.', + '--python-version', '2', + '--implementation', 'fk', + '--platform', 'fake_platform', + '--abi', 'fakeabi', '--abi', 'otherabi', '--abi', 'none', + 'fake' + ) + result.did_create( + Path('scratch') / 'fake-1.0-fk2-otherabi-fake_platform.whl' + ) + def test_download_specify_implementation(script, data): """ From e08ec3593d1c6d88995b56d8062d818827f065f0 Mon Sep 17 00:00:00 2001 From: Daniel Katz <katzdm@gmail.com> Date: Fri, 16 Oct 2020 10:05:05 -0400 Subject: [PATCH 2645/3170] Expand platform-tags unconditionally. --- src/pip/_internal/utils/compatibility_tags.py | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/utils/compatibility_tags.py b/src/pip/_internal/utils/compatibility_tags.py index eb1727e3d95..d5e6ab552b8 100644 --- a/src/pip/_internal/utils/compatibility_tags.py +++ b/src/pip/_internal/utils/compatibility_tags.py @@ -86,6 +86,20 @@ def _get_custom_platforms(arch): return arches +def _expand_allowed_platforms(platforms): + seen = set() + result = [] + + for p in platforms: + if p in seen: + continue + additions = [c for c in _get_custom_platforms(p) if c not in seen] + seen.update(additions) + result.extend(additions) + + return result + + def _get_python_version(version): # type: (str) -> PythonVersion if len(version) > 1: @@ -130,10 +144,7 @@ def get_supported( interpreter = _get_custom_interpreter(impl, version) - if platforms and len(platforms) == 1: - # Only expand list of platforms if a single platform was provided. - # Otherwise, assume that the list provided is comprehensive. - platforms = _get_custom_platforms(platforms[0]) + platforms = _expand_allowed_platforms(platforms) is_cpython = (impl or interpreter_name()) == "cp" if is_cpython: From 10372270344689e5b77dd868f24be6615a1a8a28 Mon Sep 17 00:00:00 2001 From: Daniel Katz <katzdm@gmail.com> Date: Fri, 16 Oct 2020 10:08:22 -0400 Subject: [PATCH 2646/3170] Rename news-file to use new *.rst convention. --- news/{6121.feature => 6121.feature.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename news/{6121.feature => 6121.feature.rst} (100%) diff --git a/news/6121.feature b/news/6121.feature.rst similarity index 100% rename from news/6121.feature rename to news/6121.feature.rst From 01a512c7fc29add601a63fc228c48d28a544478d Mon Sep 17 00:00:00 2001 From: Daniel Katz <katzdm@gmail.com> Date: Fri, 16 Oct 2020 10:12:51 -0400 Subject: [PATCH 2647/3170] Fix linter error: Add missing function type declaration. --- src/pip/_internal/utils/compatibility_tags.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/pip/_internal/utils/compatibility_tags.py b/src/pip/_internal/utils/compatibility_tags.py index d5e6ab552b8..6780f9d9d64 100644 --- a/src/pip/_internal/utils/compatibility_tags.py +++ b/src/pip/_internal/utils/compatibility_tags.py @@ -87,6 +87,10 @@ def _get_custom_platforms(arch): def _expand_allowed_platforms(platforms): + # type: (Optional[List[str]]) -> Optional[List[str]] + if not platforms: + return None + seen = set() result = [] From 7632c7a22b54d89cacb10cbc2eb33d4a0dab8c81 Mon Sep 17 00:00:00 2001 From: Daniel Katz <katzdm@gmail.com> Date: Mon, 26 Oct 2020 21:08:39 -0400 Subject: [PATCH 2648/3170] Spell `abis` and `platforms` as plural words. --- src/pip/_internal/cli/cmdoptions.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index d7b272ef3a9..86bc740f83c 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -97,8 +97,8 @@ def check_dist_restriction(options, check_target=False): """ dist_restriction_set = any([ options.python_version, - options.platform, - options.abi, + options.platforms, + options.abis, options.implementation, ]) @@ -490,10 +490,10 @@ def only_binary(): ) -platform = partial( +platforms = partial( Option, '--platform', - dest='platform', + dest='platforms', metavar='platform', action='append', default=None, @@ -583,10 +583,10 @@ def _handle_python_version(option, opt_str, value, parser): ) # type: Callable[..., Option] -abi = partial( +abis = partial( Option, '--abi', - dest='abi', + dest='abis', metavar='abi', action='append', default=None, @@ -601,18 +601,18 @@ def _handle_python_version(option, opt_str, value, parser): def add_target_python_options(cmd_opts): # type: (OptionGroup) -> None - cmd_opts.add_option(platform()) + cmd_opts.add_option(platforms()) cmd_opts.add_option(python_version()) cmd_opts.add_option(implementation()) - cmd_opts.add_option(abi()) + cmd_opts.add_option(abis()) def make_target_python(options): # type: (Values) -> TargetPython target_python = TargetPython( - platforms=options.platform, + platforms=options.platforms, py_version_info=options.python_version, - abis=options.abi, + abis=options.abis, implementation=options.implementation, ) From e3675c5a936c7dc1eb301a348a874e6fe39b7852 Mon Sep 17 00:00:00 2001 From: Bernard <bernard@ei8fdb.org> Date: Wed, 28 Oct 2020 12:56:45 +0100 Subject: [PATCH 2649/3170] Committing edits. --- docs/html/user_guide.rst | 72 +++++++++++++++++++++------------------- 1 file changed, 37 insertions(+), 35 deletions(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index bf2e6359b70..387a8bf61ac 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -1276,6 +1276,8 @@ In this situation, you could consider: - Refactoring your project to reduce the number of dependencies (for example, by breaking up a monolithic code base into smaller pieces) +.. _`Getting help`: + Getting help ------------ @@ -1338,7 +1340,7 @@ How does backtracking work? ~~~~~~~~~~~~~~~~~~~~~~~~~~~ When doing a pip install, it needs to start by making assumptions about the -packages it needs to install. During the install process it needs check this +packages it needs to install. During the install process it needs to check these assumptions as it goes along. When it finds that an assumption is incorrect, it has to try another approach @@ -1357,7 +1359,7 @@ onto the next package. If not it will continue to backtrack until it finds a compatible version. This backtrack behaviour can end in 2 ways - either 1) it will -successfully find a set packages it can install (good news!), or 2) it will +successfully find a set of packages it can install (good news!), or 2) it will eventually display `resolution impossible <https://pip.pypa.io/en/latest/user_guide/#id35>`__ error message (not so good). @@ -1369,7 +1371,7 @@ Why does backtracking occur? ---------------------------- With the release of the new resolver (:ref:`Resolver changes 2020`), pip is now -more strict in the package versions it installs when a users runs a +more strict in the package versions it installs when a user runs a ``pip install`` command. Pip needs to backtrack because initially, it doesn't have all the information it @@ -1380,7 +1382,7 @@ the package. This new resolver behaviour means that pip works harder to find out which version of a package is a good candidate to install. It reduces the risk that installing a new package will accidentally break an existing installed package, -and so reducing the risk of your environment gets messed up. +and so reducing the risk that your environment gets messed up. What does this behaviour look like? ----------------------------------- @@ -1391,55 +1393,55 @@ Right now backtracking behaviour looks like this: $ pip install tea==1.9.8 Collecting tea==1.9.8 - Downloading tea-1.9.8-py2.py3-none-any.whl (346 kB) - |████████████████████████████████| 346 kB 10.4 MB/s + Downloading tea-1.9.8-py2.py3-none-any.whl (346 kB) + |████████████████████████████████| 346 kB 10.4 MB/s Collecting spoon==2.27.0 - Downloading spoon-2.27.0-py2.py3-none-any.whl (312 kB) - |████████████████████████████████| 312 kB 19.2 MB/s + Downloading spoon-2.27.0-py2.py3-none-any.whl (312 kB) + |████████████████████████████████| 312 kB 19.2 MB/s Collecting hot-water>=0.1.9 Downloading hot-water-0.1.13-py3-none-any.whl (9.3 kB) Collecting cup>=1.6.0 - Downloading cup-3.22.0-py2.py3-none-any.whl (397 kB) - |████████████████████████████████| 397 kB 28.2 MB/s + Downloading cup-3.22.0-py2.py3-none-any.whl (397 kB) + |████████████████████████████████| 397 kB 28.2 MB/s INFO: pip is looking at multiple versions of this package to determine which version is compatible with other requirements. This could take a while. - Downloading cup-3.21.0-py2.py3-none-any.whl (395 kB) - |████████████████████████████████| 395 kB 27.0 MB/s - Downloading cup-3.20.0-py2.py3-none-any.whl (394 kB) - |████████████████████████████████| 394 kB 24.4 MB/s - Downloading cup-3.19.1-py2.py3-none-any.whl (394 kB) - |████████████████████████████████| 394 kB 21.3 MB/s - Downloading cup-3.19.0-py2.py3-none-any.whl (394 kB) - |████████████████████████████████| 394 kB 26.2 MB/s - Downloading cup-3.18.0-py2.py3-none-any.whl (393 kB) - |████████████████████████████████| 393 kB 22.1 MB/s - Downloading cup-3.17.0-py2.py3-none-any.whl (382 kB) - |████████████████████████████████| 382 kB 23.8 MB/s - Downloading cup-3.16.0-py2.py3-none-any.whl (376 kB) - |████████████████████████████████| 376 kB 27.5 MB/s - Downloading cup-3.15.1-py2.py3-none-any.whl (385 kB) - |████████████████████████████████| 385 kB 30.4 MB/s + Downloading cup-3.21.0-py2.py3-none-any.whl (395 kB) + |████████████████████████████████| 395 kB 27.0 MB/s + Downloading cup-3.20.0-py2.py3-none-any.whl (394 kB) + |████████████████████████████████| 394 kB 24.4 MB/s + Downloading cup-3.19.1-py2.py3-none-any.whl (394 kB) + |████████████████████████████████| 394 kB 21.3 MB/s + Downloading cup-3.19.0-py2.py3-none-any.whl (394 kB) + |████████████████████████████████| 394 kB 26.2 MB/s + Downloading cup-3.18.0-py2.py3-none-any.whl (393 kB) + |████████████████████████████████| 393 kB 22.1 MB/s + Downloading cup-3.17.0-py2.py3-none-any.whl (382 kB) + |████████████████████████████████| 382 kB 23.8 MB/s + Downloading cup-3.16.0-py2.py3-none-any.whl (376 kB) + |████████████████████████████████| 376 kB 27.5 MB/s + Downloading cup-3.15.1-py2.py3-none-any.whl (385 kB) + |████████████████████████████████| 385 kB 30.4 MB/s INFO: pip is looking at multiple versions of this package to determine which version is compatible with other requirements. This could take a while. - Downloading cup-3.15.0-py2.py3-none-any.whl (378 kB) - |████████████████████████████████| 378 kB 21.4 MB/s - Downloading cup-3.14.0-py2.py3-none-any.whl (372 kB) - |████████████████████████████████| 372 kB 21.1 MB/s - Downloading cup-3.13.1-py2.py3-none-any.whl (381 kB) - |████████████████████████████████| 381 kB 21.8 MB/s + Downloading cup-3.15.0-py2.py3-none-any.whl (378 kB) + |████████████████████████████████| 378 kB 21.4 MB/s + Downloading cup-3.14.0-py2.py3-none-any.whl (372 kB) + |████████████████████████████████| 372 kB 21.1 MB/s + Downloading cup-3.13.1-py2.py3-none-any.whl (381 kB) + |████████████████████████████████| 381 kB 21.8 MB/s This is taking longer than usual. You might need to provide the dependency resolver with stricter constraints to reduce runtime. If you want to abort this run, you can press Ctrl + C to do so. - Downloading cup-3.13.0-py2.py3-none-any.whl (374 kB) + Downloading cup-3.13.0-py2.py3-none-any.whl (374 kB) In the above sample output, pip had to download multiple versions of package cup - cup-3.22.0 to cup-3.13.0 - to find a version that will be compatible with the other packages - ``spoon``, ``hot-water``, ``cup`` etc. -These multiple ``Downloading cup-version`` lines shows pip backtracking. +These multiple ``Downloading cup-version`` lines show pip backtracking. Possible ways to reduce backtracking occurring --------------------------------------------- @@ -1525,7 +1527,7 @@ Getting help If none of the suggestions above work for you, we recommend that you ask for help and you've got `a number of -options <https://pip.pypa.io/en/latest/user_guide/#getting-help>`__. +options :ref:`Getting help`. .. _`Using pip from your program`: From 741b80ac085c810e79008743ff03090b6dc41aa1 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Date: Wed, 28 Oct 2020 18:25:12 +0530 Subject: [PATCH 2650/3170] Update wording in informational message --- src/pip/_internal/resolution/resolvelib/reporter.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/resolution/resolvelib/reporter.py b/src/pip/_internal/resolution/resolvelib/reporter.py index e150c494867..65c4373731f 100644 --- a/src/pip/_internal/resolution/resolvelib/reporter.py +++ b/src/pip/_internal/resolution/resolvelib/reporter.py @@ -35,7 +35,7 @@ def __init__(self): "This is taking longer than usual. You might need to provide the " "dependency resolver with stricter constraints to reduce runtime." "If you want to abort this run, you can press Ctrl + C to do so." - "To improve how pip performs, tell us that this happened here: " + "To improve how pip performs, tell us what happened here: " "https://pip.pypa.io/surveys/backtracking" ) } From 0cf00929816acc6303df368d5f657f584d610cad Mon Sep 17 00:00:00 2001 From: Bernard Tyers <ei8fdb@users.noreply.github.com> Date: Wed, 28 Oct 2020 14:30:04 +0100 Subject: [PATCH 2651/3170] Apply suggestions from code review Co-authored-by: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Co-authored-by: Sumana Harihareswara <sh@changeset.nyc> --- docs/html/user_guide.rst | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 387a8bf61ac..ca3a58cb67b 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -1326,8 +1326,8 @@ way pip's dependency resolution process works. During a pip install (e.g. ``pip install tea``), pip needs to work out the package's dependencies (e.g. ``spoon``, ``hot-water``, ``cup`` etc), the -versions of each of these packages it needs to install. For each of these -it needs to decide which version is a good candidate to install. +versions of each of these packages it needs to install. For each package +pip needs to decide which version is a good candidate to install. A "good candidate" means a version of each package that is compatible with all the other package versions being installed at the same time. @@ -1339,11 +1339,11 @@ package size, the number of versions pip must try, and other concerns.) How does backtracking work? ~~~~~~~~~~~~~~~~~~~~~~~~~~~ -When doing a pip install, it needs to start by making assumptions about the +When doing a pip install, pip starts by making assumptions about the packages it needs to install. During the install process it needs to check these assumptions as it goes along. -When it finds that an assumption is incorrect, it has to try another approach +When pip finds that an assumption is incorrect, it has to try another approach (backtrack), which means discarding some of the work that has already been done, and going back to choose another path. @@ -1360,7 +1360,7 @@ compatible version. This backtrack behaviour can end in 2 ways - either 1) it will successfully find a set of packages it can install (good news!), or 2) it will -eventually display `resolution impossible <https://pip.pypa.io/en/latest/user_guide/#id35>`__ error +eventually display a `resolution impossible <https://pip.pypa.io/en/latest/user_guide/#id35>`__ error message (not so good). If pip starts backtracking during dependency resolution, it does not @@ -1382,7 +1382,9 @@ the package. This new resolver behaviour means that pip works harder to find out which version of a package is a good candidate to install. It reduces the risk that installing a new package will accidentally break an existing installed package, -and so reducing the risk that your environment gets messed up. +and so reduces the risk that your environment gets messed up. + +Please address this. What does this behaviour look like? ----------------------------------- @@ -1499,7 +1501,7 @@ can be trial and error. This option is a progression of 2 above. It requires users to know how to inspect: -- the packages they're are trying to install +- the packages they're trying to install - the package release frequency and compatibility policies - their release notes and changelogs from past versions @@ -1526,8 +1528,7 @@ Getting help ------------ If none of the suggestions above work for you, we recommend that you ask -for help and you've got `a number of -options :ref:`Getting help`. +for help and you've got `a number of options :ref:`Getting help`. .. _`Using pip from your program`: From 60ae3e8759a2fe38293f41d36eba6216aa3650d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Nguy=E1=BB=85n=20Gia=20Phong?= <mcsinyx@disroot.org> Date: Mon, 19 Oct 2020 22:39:01 +0700 Subject: [PATCH 2652/3170] Test against Python 3.9 on Travis CI --- .travis.yml | 2 +- news/5661d979-1abd-4c9a-a7bf-45701b33dd6c.trivial.rst | 0 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 news/5661d979-1abd-4c9a-a7bf-45701b33dd6c.trivial.rst diff --git a/.travis.yml b/.travis.yml index 7c41f5fbccb..c0c7e703931 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,7 @@ language: python cache: pip dist: xenial -python: 3.8 +python: 3.9 addons: apt: packages: diff --git a/news/5661d979-1abd-4c9a-a7bf-45701b33dd6c.trivial.rst b/news/5661d979-1abd-4c9a-a7bf-45701b33dd6c.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d From 9f318de7b6fc471f473069268f64dae45f7a5628 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Thu, 29 Oct 2020 02:14:27 +0530 Subject: [PATCH 2653/3170] Add a debugging reporter for pip's resolver --- .../resolution/resolvelib/reporter.py | 36 +++++++++++++++++-- .../resolution/resolvelib/resolver.py | 11 ++++-- 2 files changed, 43 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/reporter.py b/src/pip/_internal/resolution/resolvelib/reporter.py index 65c4373731f..07ce399acc6 100644 --- a/src/pip/_internal/resolution/resolvelib/reporter.py +++ b/src/pip/_internal/resolution/resolvelib/reporter.py @@ -6,9 +6,9 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import DefaultDict + from typing import Any, DefaultDict - from .base import Candidate + from .base import Candidate, Requirement logger = getLogger(__name__) @@ -50,3 +50,35 @@ def backtracking(self, candidate): message = self._messages_at_backtrack[count] logger.info("INFO: %s", message) + + +class PipDebuggingReporter(BaseReporter): + """A reporter that does an info log for every event it sees.""" + + def starting(self): + # type: () -> None + logger.info("Reporter.starting()") + + def starting_round(self, index): + # type: (int) -> None + logger.info("Reporter.starting_round(%r)", index) + + def ending_round(self, index, state): + # type: (int, Any) -> None + logger.info("Reporter.ending_round(%r, state)", index) + + def ending(self, state): + # type: (Any) -> None + logger.info("Reporter.ending(%r)", state) + + def adding_requirement(self, requirement, parent): + # type: (Requirement, Candidate) -> None + logger.info("Reporter.adding_requirement(%r, %r)", requirement, parent) + + def backtracking(self, candidate): + # type: (Candidate) -> None + logger.info("Reporter.backtracking(%r)", candidate) + + def pinning(self, candidate): + # type: (Candidate) -> None + logger.info("Reporter.pinning(%r)", candidate) diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 65d0874121a..e2e164d12c1 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -1,5 +1,6 @@ import functools import logging +import os from pip._vendor import six from pip._vendor.packaging.utils import canonicalize_name @@ -11,7 +12,10 @@ from pip._internal.req.req_set import RequirementSet from pip._internal.resolution.base import BaseResolver from pip._internal.resolution.resolvelib.provider import PipProvider -from pip._internal.resolution.resolvelib.reporter import PipReporter +from pip._internal.resolution.resolvelib.reporter import ( + PipDebuggingReporter, + PipReporter, +) from pip._internal.utils.misc import dist_is_editable from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -104,7 +108,10 @@ def resolve(self, root_reqs, check_supported_wheels): upgrade_strategy=self.upgrade_strategy, user_requested=user_requested, ) - reporter = PipReporter() + if "PIP_RESOLVER_DEBUG" in os.environ: + reporter = PipDebuggingReporter() + else: + reporter = PipReporter() resolver = RLResolver(provider, reporter) try: From 1cd89f824a9e4afe76824e1c8d5ada2686d1c974 Mon Sep 17 00:00:00 2001 From: Xavier Fernandez <xav.fernandez@gmail.com> Date: Wed, 28 Oct 2020 23:55:13 +0100 Subject: [PATCH 2654/3170] Fix trivial news file generation on windows --- docs/html/development/contributing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/html/development/contributing.rst b/docs/html/development/contributing.rst index 3cdbb309d44..63eb4c33ee7 100644 --- a/docs/html/development/contributing.rst +++ b/docs/html/development/contributing.rst @@ -112,7 +112,7 @@ as trivial a contributor simply needs to add a randomly named, empty file to the ``news/`` directory with the extension of ``.trivial.rst``. If you are on a POSIX like operating system, one can be added by running ``touch news/$(uuidgen).trivial.rst``. On Windows, the same result can be -achieved in Powershell using ``New-Item "news/$([guid]::NewGuid()).trivial"``. +achieved in Powershell using ``New-Item "news/$([guid]::NewGuid()).trivial.rst"``. Core committers may also add a "trivial" label to the PR which will accomplish the same thing. From 3a20399d938b55b580b169f7fae6037646e8d8e6 Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara <sh@changeset.nyc> Date: Fri, 23 Oct 2020 17:58:53 -0400 Subject: [PATCH 2655/3170] Update user guide to change resolver default In pip 20.3, the new resolver will be the default. Therefore, this commit updates user docs to change instructions for testing, migration, etc. Towards #8937. --- README.rst | 5 +- .../html/development/architecture/anatomy.rst | 3 +- .../architecture/upgrade-options.rst | 4 + docs/html/development/release-process.rst | 2 + docs/html/index.rst | 2 +- docs/html/user_guide.rst | 135 +++++++++--------- news/9044.doc.rst | 1 + 7 files changed, 83 insertions(+), 69 deletions(-) create mode 100644 news/9044.doc.rst diff --git a/README.rst b/README.rst index 395b642d60d..3d5bd0f2955 100644 --- a/README.rst +++ b/README.rst @@ -19,7 +19,7 @@ We release updates regularly, with a new version every 3 months. Find more detai * `Release notes`_ * `Release process`_ -In 2020, we're working on improvements to the heart of pip. Please `learn more and take our survey`_ to help us do it right. +In pip 20.3, we're making a big improvement to the heart of pip. Please `learn more`_ and `take our survey`_ to help us do it right. If you find bugs, need help, or want to talk to the developers, please use our mailing lists or chat rooms: @@ -48,7 +48,8 @@ rooms, and mailing lists is expected to follow the `PSF Code of Conduct`_. .. _Release process: https://pip.pypa.io/en/latest/development/release-process/ .. _GitHub page: https://github.com/pypa/pip .. _Development documentation: https://pip.pypa.io/en/latest/development -.. _learn more and take our survey: https://pyfound.blogspot.com/2020/03/new-pip-resolver-to-roll-out-this-year.html +.. _learn more: https://pip.pypa.io/en/latest/user_guide/#changes-to-the-pip-dependency-resolver-in-20-3-2020 +.. _take our survey: https://pyfound.blogspot.com/2020/03/new-pip-resolver-to-roll-out-this-year.html .. _Issue tracking: https://github.com/pypa/pip/issues .. _Discourse channel: https://discuss.python.org/c/packaging .. _Development mailing list: https://mail.python.org/mailman3/lists/distutils-sig.python.org/ diff --git a/docs/html/development/architecture/anatomy.rst b/docs/html/development/architecture/anatomy.rst index 4b117bafe42..46bba448944 100644 --- a/docs/html/development/architecture/anatomy.rst +++ b/docs/html/development/architecture/anatomy.rst @@ -97,8 +97,7 @@ Within ``src/``: * ``pep425tags.py`` -- getting refactored into packaging.tags (a library on PyPI) which is external to pip (but vendored by pip). :pep:`425` tags: turns out lots of people want this! Compatibility tags for built distributions -> e.g., platform, Python version, etc. * ``pyproject.py`` -- ``pyproject.toml`` is a new standard (:pep:`518` and :pep:`517`). This file reads pyproject.toml and passes that info elsewhere. The rest of the processing happens in a different file. All the handling for 517 and 518 is in a different file. - * ``req/`` *[*\ **A DIRECTORY THAT NEEDS REFACTORING. A LOT**\ *\ …… Remember Step 3? Dependency resolution etc.? This is that step! Each file represents … have the entire flow of installing & uninstalling, getting info about packages…. Some files here are more than 1,000 lines long! (used to be longer?!) Refactor will deeply improve developer experience.]* - * ``resolve.py`` -- This is where the current dependency resolution algorithm sits. Pradyun is `improving the pip dependency resolver`_. Pradyun will get rid of this file and replace it with a directory called “resolution”. (this work is in git master…. There is further work that is going to be in a branch soon) + * ``req/`` *[*\ **A DIRECTORY THAT NEEDS REFACTORING. A LOT**\ *\ …… Remember Step 3? Dependency resolution etc.? This is that step! Each file represents … have the entire flow of installing & uninstalling, getting info about packages…. Some files here are more than 1,000 lines long! (used to be longer?!) Refactor will deeply improve developer experience. Also, we're `improving the pip dependency resolver`_ in 2020 so a bunch of this is changing.]* * ``utils/`` *[everything that is not “operationally” pip ….. Misc functions and files get dumped. There’s some organization here. There’s a models.py here which needs refactoring. Deprecation.py is useful, as are other things, but some things do not belong here. There ought to be some GitHub issues for refactoring some things here. Maybe a few issues with checkbox lists.]* * ``vcs/`` *[stands for Version Control System. Where pip handles all version control stuff -- one of the ``pip install`` arguments you can use is a version control link. Are any of these commands vendored? No, via subprocesses. For performance, it makes sense (we think) to do this instead of pygitlib2 or similar -- and has to be pure Python, can’t include C libraries, because you can’t include compiled C stuff, because you might not have it for the platform you are running on.]* diff --git a/docs/html/development/architecture/upgrade-options.rst b/docs/html/development/architecture/upgrade-options.rst index c87e6c97676..36e34c71ee9 100644 --- a/docs/html/development/architecture/upgrade-options.rst +++ b/docs/html/development/architecture/upgrade-options.rst @@ -6,6 +6,10 @@ When installing packages, pip chooses a distribution file, and installs it in the user's environment. There are many choices involved in deciding which file to install, and these are controlled by a variety of options. +.. note:: + + This section of the documentation needs to be updated per + :ref:`Resolver changes 2020`. Controlling what gets installed =============================== diff --git a/docs/html/development/release-process.rst b/docs/html/development/release-process.rst index 1c6889ef01c..e8c4f579553 100644 --- a/docs/html/development/release-process.rst +++ b/docs/html/development/release-process.rst @@ -65,6 +65,8 @@ their merits. ``pip._internal.utils.deprecation.deprecated``. The function is not a part of pip's public API. +.. _`Python 2 Support`: + Python 2 Support ---------------- diff --git a/docs/html/index.rst b/docs/html/index.rst index 64fc34c9d42..62217b368ff 100644 --- a/docs/html/index.rst +++ b/docs/html/index.rst @@ -17,7 +17,7 @@ Please take a look at our documentation for how to install and use pip: ux_research_design news -In 2020, we're working on improvements to the heart of pip: :ref:`Resolver changes 2020`. Please `learn more and take our survey`_ to help us do it right, and `join our user experience surveys pool`_. +In pip 20.3, we're making a big improvement to the heart of pip: :ref:`Resolver changes 2020`. Please `learn more and take our survey`_ to help us do it right, and `join our user experience surveys pool`_. If you find bugs, need help, or want to talk to the developers, please use our mailing lists or chat rooms: diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 0e0f6cd3e36..c6ad72f7e35 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -197,16 +197,18 @@ In practice, there are 4 common uses of Requirements files: py -m pip freeze > requirements.txt py -m pip install -r requirements.txt -2. Requirements files are used to force pip to properly resolve dependencies. - As it is now, pip `doesn't have true dependency resolution - <https://github.com/pypa/pip/issues/988>`_, but instead simply uses the first - specification it finds for a project. E.g. if ``pkg1`` requires - ``pkg3>=1.0`` and ``pkg2`` requires ``pkg3>=1.0,<=2.0``, and if ``pkg1`` is - resolved first, pip will only use ``pkg3>=1.0``, and could easily end up - installing a version of ``pkg3`` that conflicts with the needs of ``pkg2``. - To solve this problem, you can place ``pkg3>=1.0,<=2.0`` (i.e. the correct - specification) into your requirements file directly along with the other top - level requirements. Like so:: +2. Requirements files are used to force pip to properly resolve + dependencies. In versions of pip prior to 20.3, pip `doesn't have + true dependency resolution + <https://github.com/pypa/pip/issues/988>`_, but instead simply uses + the first specification it finds for a project. E.g. if ``pkg1`` + requires ``pkg3>=1.0`` and ``pkg2`` requires ``pkg3>=1.0,<=2.0``, + and if ``pkg1`` is resolved first, pip will only use ``pkg3>=1.0``, + and could easily end up installing a version of ``pkg3`` that + conflicts with the needs of ``pkg2``. To solve this problem for + pip 20.2 and previous, you can place ``pkg3>=1.0,<=2.0`` (i.e. the + correct specification) into your requirements file directly along + with the other top level requirements. Like so:: pkg1 pkg2 @@ -1095,6 +1097,8 @@ archives are built with identical packages. to use such a package, see :ref:`Controlling setup_requires<controlling-setup-requires>`. +.. _`Fixing conflicting dependencies`: + Fixing conflicting dependencies =============================== @@ -1103,8 +1107,10 @@ pip users who encounter an error where pip cannot install their specified packages due to conflicting dependencies (a ``ResolutionImpossible`` error). -This documentation is specific to the new resolver, which you can use -with the flag ``--use-feature=2020-resolver``. +This documentation is specific to the new resolver, which is the +default behavior in pip 20.3 and later. If you are using pip 20.2, you +can invoke the new resolver by using the flag +``--use-feature=2020-resolver``. Understanding your error message -------------------------------- @@ -1376,17 +1382,20 @@ of ability. Some examples that you could consider include: .. _`Resolver changes 2020`: -Changes to the pip dependency resolver in 20.2 (2020) +Changes to the pip dependency resolver in 20.3 (2020) ===================================================== -pip 20.1 included an alpha version of the new resolver (hidden behind -an optional ``--unstable-feature=resolver`` flag). pip 20.2 removes -that flag, and includes a robust beta of the new resolver (hidden -behind an optional ``--use-feature=2020-resolver`` flag) that we -encourage you to test. We will continue to improve the pip dependency -resolver in response to testers' feedback. Please give us feedback -through the `resolver testing survey`_. This will help us prepare to -release pip 20.3, with the new resolver on by default, in October. +pip 20.3 has a new dependency resolver, on by default. (pip 20.1 and +20.2 included pre-release versions of the new dependency resolver, +hidden behind optional user flags.) Read below for a migration guide, +how to invoke the legacy resolver, and the deprecation timeline. We +also made a `two-minute video explanation`_ you can watch. + +We will continue to improve the pip dependency resolver in response to +testers' feedback. Please give us feedback through the `resolver +testing survey`_. + +.. _`Migration guide for 2020 resolver changes`: Watch out for ------------- @@ -1442,28 +1451,20 @@ We are also changing our support for :ref:`Constraints Files`: * Constraints cannot have extras (see :issue:`6628`) -How to test ------------ +How to upgrade and migrate +-------------------------- -1. **Install pip 20.2** with ``python -m pip install --upgrade pip``. +1. **Install pip 20.3** with ``python -m pip install --upgrade pip``. 2. **Validate your current environment** by running ``pip check``. This will report if you have any inconsistencies in your set of installed packages. Having a clean installation will make it much less likely - that you will hit issues when the new resolver is released (and may + that you will hit issues with the new resolver (and may address hidden problems in your current environment!). If you run ``pip check`` and run into stuff you can’t figure out, please `ask for help in our issue tracker or chat <https://pip.pypa.io/>`__. -3. **Test the new version of pip** (see below). To test the new - resolver, use the ``--use-feature=2020-resolver`` flag, as in: - - ``pip install example --use-feature=2020-resolver`` - - The more feedback we can get, the more we can make sure that the - final release is solid. (Only try the new resolver **in a - non-production environment**, though - it isn't ready for you to - rely on in production!) +3. **Test the new version of pip**. While we have tried to make sure that pip’s test suite covers as many cases as we can, we are very aware that there are people using @@ -1478,45 +1479,45 @@ How to test - using ``pip install --force-reinstall`` to check whether it does what you think it should - using constraints files + - the "Setups to test with special attention" and "Examples to try" below - If you have a build pipeline that depends on pip installing your dependencies for you, check that the new resolver does what you need. - - If you'd like pip to default to using the new resolver, run ``pip - config set global.use-feature 2020-resolver`` (for more on that - and the alternate ``PIP_USE_FEATURE`` environment variable - option, see `issue 8661`_). - - Run your project’s CI (test suite, build process, etc.) using the new resolver, and let us know of any issues. - If you have encountered resolver issues with pip in the past, - check whether the new resolver fixes them. Also, let us know if - the new resolver has issues with any workarounds you put in to - address the current resolver’s limitations. We’ll need to ensure - that people can transition off such workarounds smoothly. + check whether the new resolver fixes them, and read :ref:`Fixing + conflicting dependencies`. Also, let us know if the new resolver + has issues with any workarounds you put in to address the + current resolver’s limitations. We’ll need to ensure that people + can transition off such workarounds smoothly. - If you develop or support a tool that wraps pip or uses it to deliver part of your functionality, please test your integration - with pip 20.2. + with pip 20.3. -4. **Please report bugs** through the `resolver testing survey`_. +4. **Temporarily use the old resolver when necessary.** If you run + into resolution errors and need a workaround while you're fixing + their root causes, you can choose the old resolver behavior + using the flag ``--use-deprecated=legacy-resolver``. -Setups we might need more testing on ------------------------------------- + Per our :ref:`Python 2 Support` policy, pip 20.3 users who are + using Python 2 and who have trouble with the new resolver can + choose to switch to the old resolver behavior using the flag + ``--use-deprecated=legacy-resolver``. Python 2 users should + upgrade to Python 3 as soon as possible, since in pip 21.0 in + January 2021, pip will drop support for Python 2 altogether. -* Windows, including Windows Subsystem for Linux (WSL) +5. **Please report bugs** through the `resolver testing survey`_. -* Macintosh -* Debian, Fedora, Red Hat, CentOS, Mint, Arch, Raspbian, Gentoo - -* non-Latin localized filesystems and OSes, such as Japanese, Chinese, and Korean, and right-to-left such as Hebrew, Urdu, and Arabic - -* Multi-user installations +Setups to test with special attention +------------------------------------- * Requirements files with 100+ packages -* An installation workflow that involves multiple requirements files +* Installation workflows that involve multiple requirements files * Requirements files that include hashes (:ref:`hash-checking mode`) or pinned dependencies (perhaps as output from ``pip-compile`` within @@ -1530,12 +1531,6 @@ Setups we might need more testing on * Installing from source code held in local directories -* Using the most recent versions of Python 3.6, 3.7, 3.8, and 3.9 - -* PyPy - -* Customized terminals (where you have modified how error messages and standard output display) - Examples to try ^^^^^^^^^^^^^^^ @@ -1580,15 +1575,26 @@ Specific things we'd love to get feedback on: Please let us know through the `resolver testing survey`_. +.. _`Deprecation timeline for 2020 resolver changes`: + Deprecation timeline -------------------- We plan for the resolver changeover to proceed as follows, using :ref:`Feature Flags` and following our :ref:`Release Cadence`: -* pip 20.2: a beta of the new resolver is available, opt-in, using - the flag ``--use-feature=2020-resolver``. pip defaults to - legacy behavior. +* pip 20.1: an alpha version of the new resolver was available, + opt-in, using the optional flag + ``--unstable-feature=resolver``. pip defaulted to legacy + behavior. + +* pip 20.2: a beta of the new resolver was available, opt-in, using + the flag ``--use-feature=2020-resolver``. pip defaulted to legacy + behavior. Users of pip 20.2 who want pip to default to using the + new resolver can run ``pip config set global.use-feature + 2020-resolver`` (for more on that and the alternate + ``PIP_USE_FEATURE`` environment variable option, see `issue + 8661`_). * pip 20.3: pip defaults to the new resolver, but a user can opt-out and choose the old resolver behavior, using the flag @@ -1616,6 +1622,7 @@ announcements on the `low-traffic packaging announcements list`_ and .. _resolver testing survey: https://tools.simplysecure.org/survey/index.php?r=survey/index&sid=989272&lang=en .. _issue 8661: https://github.com/pypa/pip/issues/8661 .. _our announcement on the PSF blog: http://pyfound.blogspot.com/2020/03/new-pip-resolver-to-roll-out-this-year.html +.. _two-minute video explanation: https://www.youtube.com/watch?v=B4GQCBBsuNU .. _tensorflow: https://pypi.org/project/tensorflow/ .. _low-traffic packaging announcements list: https://mail.python.org/mailman3/lists/pypi-announce.python.org/ .. _our survey on upgrades that create conflicts: https://docs.google.com/forms/d/e/1FAIpQLSeBkbhuIlSofXqCyhi3kGkLmtrpPOEBwr6iJA6SzHdxWKfqdA/viewform diff --git a/news/9044.doc.rst b/news/9044.doc.rst new file mode 100644 index 00000000000..58c7f39d20f --- /dev/null +++ b/news/9044.doc.rst @@ -0,0 +1 @@ +Update user docs to reflect new resolver as default in 20.3. From f4e96e95c95cbb84a250f90d8cedafe2bd534dc3 Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara <sh@changeset.nyc> Date: Mon, 26 Oct 2020 15:47:02 -0400 Subject: [PATCH 2656/3170] Add redirect for heading in user guide --- docs/html/user_guide.rst | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index c6ad72f7e35..f6c4ee5dd65 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -1380,6 +1380,8 @@ of ability. Some examples that you could consider include: * ``distlib`` - Packaging and distribution utilities (including functions for interacting with PyPI). +.. _changes-to-the-pip-dependency-resolver-in-20-2-2020: + .. _`Resolver changes 2020`: Changes to the pip dependency resolver in 20.3 (2020) From 4ce9565aa1751d117e5190362c2000cf96c2e3c8 Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara <sh@changeset.nyc> Date: Mon, 26 Oct 2020 16:01:24 -0400 Subject: [PATCH 2657/3170] Update docs about Python 2 and resolver Per #9019. --- docs/html/user_guide.rst | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index f6c4ee5dd65..2e8bfeb026c 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -1452,6 +1452,11 @@ We are also changing our support for :ref:`Constraints Files`: * Links are not allowed as constraints (see :issue:`8253`) * Constraints cannot have extras (see :issue:`6628`) +Per our :ref:`Python 2 Support` policy, pip 20.3 users who are using +Python 2 will use the legacy resolver by default. Python 2 users +should upgrade to Python 3 as soon as possible, since in pip 21.0 in +January 2021, pip will drop support for Python 2 altogether. + How to upgrade and migrate -------------------------- @@ -1504,13 +1509,6 @@ How to upgrade and migrate their root causes, you can choose the old resolver behavior using the flag ``--use-deprecated=legacy-resolver``. - Per our :ref:`Python 2 Support` policy, pip 20.3 users who are - using Python 2 and who have trouble with the new resolver can - choose to switch to the old resolver behavior using the flag - ``--use-deprecated=legacy-resolver``. Python 2 users should - upgrade to Python 3 as soon as possible, since in pip 21.0 in - January 2021, pip will drop support for Python 2 altogether. - 5. **Please report bugs** through the `resolver testing survey`_. From 7657c44409049043b8b7470baf0535849fb00800 Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara <sh@changeset.nyc> Date: Wed, 28 Oct 2020 23:29:09 -0400 Subject: [PATCH 2658/3170] docs: Clarify links for resolver work and studies Co-Authored-By: Bernard Tyers <bernard+work@ei8fdb.org> --- README.rst | 5 +++-- docs/html/index.rst | 6 +++--- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/README.rst b/README.rst index 3d5bd0f2955..d8d12c850da 100644 --- a/README.rst +++ b/README.rst @@ -19,7 +19,7 @@ We release updates regularly, with a new version every 3 months. Find more detai * `Release notes`_ * `Release process`_ -In pip 20.3, we're making a big improvement to the heart of pip. Please `learn more`_ and `take our survey`_ to help us do it right. +In pip 20.3, we're `making a big improvement to the heart of pip`_; `learn more`_. We want your input, so `sign up for our user experience research studies`_ to help us do it right. If you find bugs, need help, or want to talk to the developers, please use our mailing lists or chat rooms: @@ -48,8 +48,9 @@ rooms, and mailing lists is expected to follow the `PSF Code of Conduct`_. .. _Release process: https://pip.pypa.io/en/latest/development/release-process/ .. _GitHub page: https://github.com/pypa/pip .. _Development documentation: https://pip.pypa.io/en/latest/development +.. _making a big improvement to the heart of pip: https://pyfound.blogspot.com/2020/03/new-pip-resolver-to-roll-out-this-year.html .. _learn more: https://pip.pypa.io/en/latest/user_guide/#changes-to-the-pip-dependency-resolver-in-20-3-2020 -.. _take our survey: https://pyfound.blogspot.com/2020/03/new-pip-resolver-to-roll-out-this-year.html +.. _sign up for our user experience research studies: https://pyfound.blogspot.com/2020/03/new-pip-resolver-to-roll-out-this-year.html .. _Issue tracking: https://github.com/pypa/pip/issues .. _Discourse channel: https://discuss.python.org/c/packaging .. _Development mailing list: https://mail.python.org/mailman3/lists/distutils-sig.python.org/ diff --git a/docs/html/index.rst b/docs/html/index.rst index 62217b368ff..ce40b49fa6c 100644 --- a/docs/html/index.rst +++ b/docs/html/index.rst @@ -17,7 +17,7 @@ Please take a look at our documentation for how to install and use pip: ux_research_design news -In pip 20.3, we're making a big improvement to the heart of pip: :ref:`Resolver changes 2020`. Please `learn more and take our survey`_ to help us do it right, and `join our user experience surveys pool`_. +In pip 20.3, we're `making a big improvement to the heart of pip`_; :ref:`Resolver changes 2020`. We want your input, so `sign up for our user experience research studies`_ to help us do it right. If you find bugs, need help, or want to talk to the developers, please use our mailing lists or chat rooms: @@ -40,8 +40,8 @@ rooms, and mailing lists is expected to follow the `PSF Code of Conduct`_. .. _package installer: https://packaging.python.org/guides/tool-recommendations/ .. _Python Package Index: https://pypi.org -.. _join our user experience surveys pool: ux_research_design -.. _learn more and take our survey: https://pyfound.blogspot.com/2020/03/new-pip-resolver-to-roll-out-this-year.html +.. _making a big improvement to the heart of pip: https://pyfound.blogspot.com/2020/03/new-pip-resolver-to-roll-out-this-year.html +.. _sign up for our user experience research studies: https://pyfound.blogspot.com/2020/03/new-pip-resolver-to-roll-out-this-year.html .. _Installation: https://pip.pypa.io/en/stable/installing.html .. _Documentation: https://pip.pypa.io/en/stable/ .. _Changelog: https://pip.pypa.io/en/stable/news.html From b7c3503a342ba52b3e782a4a3373f8b31e855e4a Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara <sh@changeset.nyc> Date: Mon, 26 Oct 2020 15:44:15 -0400 Subject: [PATCH 2659/3170] Add note about install/upgrade behavior to docs Towards #8115. --- docs/html/development/architecture/upgrade-options.rst | 10 +++++++--- docs/html/reference/pip_install.rst | 3 +++ 2 files changed, 10 insertions(+), 3 deletions(-) diff --git a/docs/html/development/architecture/upgrade-options.rst b/docs/html/development/architecture/upgrade-options.rst index 36e34c71ee9..0527101f8c6 100644 --- a/docs/html/development/architecture/upgrade-options.rst +++ b/docs/html/development/architecture/upgrade-options.rst @@ -2,9 +2,10 @@ Options that control the installation process ============================================= -When installing packages, pip chooses a distribution file, and installs it in -the user's environment. There are many choices involved in deciding which file -to install, and these are controlled by a variety of options. +When installing packages, pip chooses a distribution file, and +installs it in the user's environment. There are many choices (which +are `still evolving`_) involved in deciding which file to install, and +these are controlled by a variety of options. .. note:: @@ -122,3 +123,6 @@ necessarily resolution or what gets installed. ``--constraint`` ``--editable <LOCATION>`` + + +.. _still evolving: https://github.com/pypa/pip/issues/8115 diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index 7d6b5471a2b..be974dacf61 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -36,6 +36,9 @@ pip install has several stages: 3. Build wheels. All the dependencies that can be are built into wheels. 4. Install the packages (and uninstall anything being upgraded/replaced). +Note that ``pip install`` prefers to leave the installed version as-is +unless ``--upgrade`` is specified. + Argument Handling ----------------- From 539303202590abcbe620951c27a44782e3a001b6 Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara <sh@changeset.nyc> Date: Mon, 26 Oct 2020 20:36:36 -0400 Subject: [PATCH 2660/3170] Improve docs on resolver, constraints, and hash-checking Related to #9020. --- docs/html/user_guide.rst | 10 +++++++++- news/9056.doc.rst | 1 + 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 news/9056.doc.rst diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 2e8bfeb026c..391297bb927 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -1446,7 +1446,15 @@ install x y`` in a single command. We would like your thoughts on what pip's behavior should be; please answer `our survey on upgrades that create conflicts`_. -We are also changing our support for :ref:`Constraints Files`: +We are also changing our support for :ref:`Constraints Files` and related functionality: + +* Constraints don't override the existing requirements; they simply + constrain what versions are visible as input to the resolver (see + :issue:`9020`) + +* Hash-checking mode requires that all requirements are specified as a +`==` match on a version and may not work well in combination with +constraints (see :issue:`9020`) * Unnamed requirements are not allowed as constraints (see :issue:`6628` and :issue:`8210`) * Links are not allowed as constraints (see :issue:`8253`) diff --git a/news/9056.doc.rst b/news/9056.doc.rst new file mode 100644 index 00000000000..7317dc9cd0d --- /dev/null +++ b/news/9056.doc.rst @@ -0,0 +1 @@ +Improve migration guide to reflect changes in new resolver behavior. From 27b100e8d1086442cd87c274542a465e0b718603 Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara <sh@changeset.nyc> Date: Mon, 26 Oct 2020 21:01:18 -0400 Subject: [PATCH 2661/3170] Explain resolver changes affecting constraints files Related to #8792, #8076, #9020. --- docs/html/user_guide.rst | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 391297bb927..a1491923e95 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -291,7 +291,10 @@ organisation and use that everywhere. If the thing being installed requires "helloworld" to be installed, your fixed version specified in your constraints file will be used. -Constraints file support was added in pip 7.1. +Constraints file support was added in pip 7.1. In :ref:`Resolver +changes 2020` we did a fairly comprehensive overhaul and stripped +constraints files down to being purely a way to specify global +(version) limits for packages. .. _`Installing from Wheels`: @@ -1446,15 +1449,21 @@ install x y`` in a single command. We would like your thoughts on what pip's behavior should be; please answer `our survey on upgrades that create conflicts`_. -We are also changing our support for :ref:`Constraints Files` and related functionality: +We are also changing our support for :ref:`Constraints Files` and +related functionality. We did a fairly comprehensive overhaul and +stripped constraints files down to being purely a way to specify +global (version) limits for packages. Specifically: * Constraints don't override the existing requirements; they simply constrain what versions are visible as input to the resolver (see :issue:`9020`) +* Providing an editable requirement (``-e .``) does not cause pip to + ignore version specifiers or constraints (see :issue:`8076`) + * Hash-checking mode requires that all requirements are specified as a -`==` match on a version and may not work well in combination with -constraints (see :issue:`9020`) + `==` match on a version and may not work well in combination with + constraints (see :issue:`9020` and :issue:`8792`) * Unnamed requirements are not allowed as constraints (see :issue:`6628` and :issue:`8210`) * Links are not allowed as constraints (see :issue:`8253`) From 395e1ae8e83e49cd24d8e05b7567f907dbdee8f9 Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara <sh@changeset.nyc> Date: Mon, 26 Oct 2020 21:14:16 -0400 Subject: [PATCH 2662/3170] Add detail to docs on constraints changes with new resolver Related to #8307, #8115. --- docs/html/user_guide.rst | 23 ++++++++++++++--------- 1 file changed, 14 insertions(+), 9 deletions(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index a1491923e95..a8439d8c538 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -1449,22 +1449,27 @@ install x y`` in a single command. We would like your thoughts on what pip's behavior should be; please answer `our survey on upgrades that create conflicts`_. -We are also changing our support for :ref:`Constraints Files` and -related functionality. We did a fairly comprehensive overhaul and -stripped constraints files down to being purely a way to specify -global (version) limits for packages. Specifically: +We are also changing our support for :ref:`Constraints Files`, +editable installs, and related functionality. We did a fairly +comprehensive overhaul and stripped constraints files down to being +purely a way to specify global (version) limits for packages, and so +some combinations that used to be allowed will now cause +errors. Specifically: * Constraints don't override the existing requirements; they simply constrain what versions are visible as input to the resolver (see :issue:`9020`) - * Providing an editable requirement (``-e .``) does not cause pip to - ignore version specifiers or constraints (see :issue:`8076`) - + ignore version specifiers or constraints (see :issue:`8076`), and if + you have a conflict between a pinned requirement and a local + directory then pip will indicate that it cannot find a version + satisfying both (see :issue:`8307`) * Hash-checking mode requires that all requirements are specified as a - `==` match on a version and may not work well in combination with + ``==`` match on a version and may not work well in combination with constraints (see :issue:`9020` and :issue:`8792`) - +* If necessary to satisfy constraints, pip will happily reinstall + packages, upgrading or downgrading, without needing any additional + command-line options (see :issue:`8115` and :doc:`development/architecture/upgrade-options`) * Unnamed requirements are not allowed as constraints (see :issue:`6628` and :issue:`8210`) * Links are not allowed as constraints (see :issue:`8253`) * Constraints cannot have extras (see :issue:`6628`) From fbcdeca91ae5e781ce9c3b2cd31aac82cbe6825f Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara <sh@changeset.nyc> Date: Tue, 27 Oct 2020 19:32:40 -0400 Subject: [PATCH 2663/3170] Clarify constraints overhaul in user guide Co-authored-by: Paul Moore <p.f.moore@gmail.com> --- docs/html/user_guide.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index a8439d8c538..d7107db70a3 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -292,9 +292,10 @@ organisation and use that everywhere. If the thing being installed requires file will be used. Constraints file support was added in pip 7.1. In :ref:`Resolver -changes 2020` we did a fairly comprehensive overhaul and stripped -constraints files down to being purely a way to specify global -(version) limits for packages. +changes 2020` we did a fairly comprehensive overhaul, removing several +undocumented and unsupported quirks from the previous implementation, +and stripped constraints files down to being purely a way to specify +global (version) limits for packages. .. _`Installing from Wheels`: From 3f4e15aef0d406a93e7a9b66280ea054938f7837 Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara <sh@changeset.nyc> Date: Wed, 28 Oct 2020 23:21:11 -0400 Subject: [PATCH 2664/3170] docs: Emphasize that pip may break existing packages Related to #7744 . --- docs/html/user_guide.rst | 29 ++++++++++++++--------------- 1 file changed, 14 insertions(+), 15 deletions(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index d7107db70a3..677a8a063e7 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -197,18 +197,16 @@ In practice, there are 4 common uses of Requirements files: py -m pip freeze > requirements.txt py -m pip install -r requirements.txt -2. Requirements files are used to force pip to properly resolve - dependencies. In versions of pip prior to 20.3, pip `doesn't have - true dependency resolution - <https://github.com/pypa/pip/issues/988>`_, but instead simply uses - the first specification it finds for a project. E.g. if ``pkg1`` - requires ``pkg3>=1.0`` and ``pkg2`` requires ``pkg3>=1.0,<=2.0``, - and if ``pkg1`` is resolved first, pip will only use ``pkg3>=1.0``, - and could easily end up installing a version of ``pkg3`` that - conflicts with the needs of ``pkg2``. To solve this problem for - pip 20.2 and previous, you can place ``pkg3>=1.0,<=2.0`` (i.e. the - correct specification) into your requirements file directly along - with the other top level requirements. Like so:: +2. Requirements files are used to force pip to properly resolve dependencies. + pip 20.2 and earlier `doesn't have true dependency resolution + <https://github.com/pypa/pip/issues/988>`_, but instead simply uses the first + specification it finds for a project. E.g. if ``pkg1`` requires + ``pkg3>=1.0`` and ``pkg2`` requires ``pkg3>=1.0,<=2.0``, and if ``pkg1`` is + resolved first, pip will only use ``pkg3>=1.0``, and could easily end up + installing a version of ``pkg3`` that conflicts with the needs of ``pkg2``. + To solve this problem, you can place ``pkg3>=1.0,<=2.0`` (i.e. the correct + specification) into your requirements file directly along with the other top + level requirements. Like so:: pkg1 pkg2 @@ -1441,12 +1439,13 @@ time to fix the underlying problem in the packages, because pip will be stricter from here on out. This also means that, when you run a ``pip install`` command, pip only -considers the packages you are installing in that command, and may -break already-installed packages. It will not guarantee that your +considers the packages you are installing in that command, and **may +break already-installed packages**. It will not guarantee that your environment will be consistent all the time. If you ``pip install x`` and then ``pip install y``, it's possible that the version of ``y`` you get will be different than it would be if you had run ``pip -install x y`` in a single command. We would like your thoughts on what +install x y`` in a single command. We are considering changing this +behavior (per :issue:`7744`) and would like your thoughts on what pip's behavior should be; please answer `our survey on upgrades that create conflicts`_. From 385077a9447355bddb01a76b444f273a7254431c Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Fri, 30 Oct 2020 05:26:11 +0530 Subject: [PATCH 2665/3170] Factor out logic for determining resolver to use --- src/pip/_internal/cli/req_command.py | 22 ++++++++++++++++++---- src/pip/_internal/commands/install.py | 8 ++++---- 2 files changed, 22 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 03cc52f6966..bab4fbb7ae9 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -195,7 +195,18 @@ def __init__(self, *args, **kw): self.cmd_opts.add_option(cmdoptions.no_clean()) @staticmethod + def determine_resolver_variant(options): + # type: (Values) -> str + """Determines which resolver should be used, based on the given options.""" + if '2020-resolver' in options.features_enabled: + resolver_variant = "2020-resolver" + else: + resolver_variant = "legacy" + return resolver_variant + + @classmethod def make_requirement_preparer( + cls, temp_build_dir, # type: TempDirectory options, # type: Values req_tracker, # type: RequirementTracker @@ -211,7 +222,8 @@ def make_requirement_preparer( temp_build_dir_path = temp_build_dir.path assert temp_build_dir_path is not None - if '2020-resolver' in options.features_enabled: + resolver_variant = cls.determine_resolver_variant(options) + if resolver_variant == "2020-resolver": lazy_wheel = 'fast-deps' in options.features_enabled if lazy_wheel: logger.warning( @@ -238,8 +250,9 @@ def make_requirement_preparer( lazy_wheel=lazy_wheel, ) - @staticmethod + @classmethod def make_resolver( + cls, preparer, # type: RequirementPreparer finder, # type: PackageFinder options, # type: Values @@ -250,7 +263,7 @@ def make_resolver( force_reinstall=False, # type: bool upgrade_strategy="to-satisfy-only", # type: str use_pep517=None, # type: Optional[bool] - py_version_info=None # type: Optional[Tuple[int, ...]] + py_version_info=None, # type: Optional[Tuple[int, ...]] ): # type: (...) -> BaseResolver """ @@ -261,10 +274,11 @@ def make_resolver( isolated=options.isolated_mode, use_pep517=use_pep517, ) + resolver_variant = cls.determine_resolver_variant(options) # The long import name and duplicated invocation is needed to convince # Mypy into correctly typechecking. Otherwise it would complain the # "Resolver" class being redefined. - if '2020-resolver' in options.features_enabled: + if resolver_variant == "2020-resolver": import pip._internal.resolution.resolvelib.resolver return pip._internal.resolution.resolvelib.resolver.Resolver( diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index a4001553bfa..4c16b9caedd 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -427,7 +427,7 @@ def run(self, options, args): if conflicts is not None: self._warn_about_conflicts( conflicts, - new_resolver='2020-resolver' in options.features_enabled, + resolver_variant=self.determine_resolver_variant(options), ) installed_desc = ' '.join(items) @@ -520,14 +520,14 @@ def _determine_conflicts(self, to_install): ) return None - def _warn_about_conflicts(self, conflict_details, new_resolver): - # type: (ConflictDetails, bool) -> None + def _warn_about_conflicts(self, conflict_details, resolver_variant): + # type: (ConflictDetails, str) -> None package_set, (missing, conflicting) = conflict_details if not missing and not conflicting: return parts = [] # type: List[str] - if not new_resolver: + if resolver_variant == "legacy": parts.append( "After October 2020 you may experience errors when installing " "or updating packages. This is because pip will change the " From d012c7d4af326079c6a4f2db5f58b53580a69452 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Fri, 30 Oct 2020 07:12:36 +0530 Subject: [PATCH 2666/3170] Add a warning to fast-deps with legacy resolver --- src/pip/_internal/cli/req_command.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index bab4fbb7ae9..f92e2b509f7 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -235,6 +235,10 @@ def make_requirement_preparer( ) else: lazy_wheel = False + if lazy_wheel: + logger.warning( + 'fast-deps has no effect when used with the legacy resolver.' + ) return RequirementPreparer( build_dir=temp_build_dir_path, From 6028e6a0fb64873ae49485d858bd85611c2d25f1 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Fri, 30 Oct 2020 05:32:12 +0530 Subject: [PATCH 2667/3170] Change how we skip a failing test --- tests/functional/test_install.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 17a72bca82e..6a4ad369567 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -538,7 +538,7 @@ def assert_re_match(pattern, text): @pytest.mark.network -@pytest.mark.fails_on_new_resolver +@pytest.mark.skip("Fails on new resolver") def test_hashed_install_failure_later_flag(script, tmpdir): with requirements_file( "blessings==1.0\n" From 5cba61e118a4289f68e59f173ca1bc7770238d4b Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Fri, 30 Oct 2020 05:58:02 +0530 Subject: [PATCH 2668/3170] Switch to resolver variants in the test suite --- tests/conftest.py | 9 +++-- tests/functional/test_install.py | 22 +++++------ tests/functional/test_install_reqs.py | 48 ++++++++++++------------ tests/functional/test_install_upgrade.py | 8 ++-- tests/functional/test_wheel.py | 4 +- 5 files changed, 47 insertions(+), 44 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index bf071c8de7a..629227ed80d 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -103,17 +103,20 @@ def pytest_collection_modifyitems(config, items): @pytest.fixture(scope="session", autouse=True) -def use_new_resolver(request): - """Set environment variable to make pip default to the new resolver. +def resolver_variant(request): + """Set environment variable to make pip default to the correct resolver. """ new_resolver = request.config.getoption("--new-resolver") features = set(os.environ.get("PIP_USE_FEATURE", "").split()) if new_resolver: + retval = "2020-resolver" features.add("2020-resolver") else: + retval = "legacy" features.discard("2020-resolver") + with patch.dict(os.environ, {"PIP_USE_FEATURE": " ".join(features)}): - yield new_resolver + yield retval @pytest.fixture(scope='session') diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 6a4ad369567..0d2ad0f2b22 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -941,7 +941,7 @@ def test_install_nonlocal_compatible_wheel(script, data): def test_install_nonlocal_compatible_wheel_path( script, data, - use_new_resolver + resolver_variant, ): target_dir = script.scratch_path / 'target' @@ -952,9 +952,9 @@ def test_install_nonlocal_compatible_wheel_path( '--no-index', '--only-binary=:all:', Path(data.packages) / 'simplewheel-2.0-py3-fakeabi-fakeplat.whl', - expect_error=use_new_resolver + expect_error=(resolver_variant == "2020-resolver"), ) - if use_new_resolver: + if resolver_variant == "2020-resolver": assert result.returncode == ERROR else: assert result.returncode == SUCCESS @@ -1456,7 +1456,7 @@ def test_install_no_binary_disables_cached_wheels(script, data, with_wheel): assert "Running setup.py install for upper" in str(res), str(res) -def test_install_editable_with_wrong_egg_name(script, use_new_resolver): +def test_install_editable_with_wrong_egg_name(script, resolver_variant): script.scratch_path.joinpath("pkga").mkdir() pkga_path = script.scratch_path / 'pkga' pkga_path.joinpath("setup.py").write_text(textwrap.dedent(""" @@ -1467,12 +1467,12 @@ def test_install_editable_with_wrong_egg_name(script, use_new_resolver): result = script.pip( 'install', '--editable', 'file://{pkga_path}#egg=pkgb'.format(**locals()), - expect_error=use_new_resolver, + expect_error=(resolver_variant == "2020-resolver"), ) assert ("Generating metadata for package pkgb produced metadata " "for project name pkga. Fix your #egg=pkgb " "fragments.") in result.stderr - if use_new_resolver: + if resolver_variant == "2020-resolver": assert "has different name in metadata" in result.stderr, str(result) else: assert "Successfully installed pkga" in str(result), str(result) @@ -1505,7 +1505,7 @@ def test_double_install(script): assert msg not in result.stderr -def test_double_install_fail(script, use_new_resolver): +def test_double_install_fail(script, resolver_variant): """ Test double install failing with two different version requirements """ @@ -1514,9 +1514,9 @@ def test_double_install_fail(script, use_new_resolver): 'pip==7.*', 'pip==7.1.2', # The new resolver is perfectly capable of handling this - expect_error=(not use_new_resolver) + expect_error=(resolver_variant == "legacy"), ) - if not use_new_resolver: + if resolver_variant == "legacy": msg = ("Double requirement given: pip==7.1.2 (already in pip==7.*, " "name='pip')") assert msg in result.stderr @@ -1770,11 +1770,11 @@ def test_user_config_accepted(script): ) @pytest.mark.parametrize("use_module", [True, False]) def test_install_pip_does_not_modify_pip_when_satisfied( - script, install_args, expected_message, use_module, use_new_resolver): + script, install_args, expected_message, use_module, resolver_variant): """ Test it doesn't upgrade the pip if it already satisfies the requirement. """ - variation = "satisfied" if use_new_resolver else "up-to-date" + variation = "satisfied" if resolver_variant else "up-to-date" expected_message = expected_message.format(variation) result = script.pip_install_local( 'pip', *install_args, use_module=use_module diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index c879b6903dd..0405c4d2445 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -376,7 +376,7 @@ def test_constraints_local_install_causes_error(script, data): def test_constraints_constrain_to_local_editable( script, data, - use_new_resolver + resolver_variant, ): to_install = data.src.joinpath("singlemodule") script.scratch_path.joinpath("constraints.txt").write_text( @@ -386,15 +386,15 @@ def test_constraints_constrain_to_local_editable( 'install', '--no-index', '-f', data.find_links, '-c', script.scratch_path / 'constraints.txt', 'singlemodule', allow_stderr_warning=True, - expect_error=use_new_resolver + expect_error=(resolver_variant == "2020-resolver"), ) - if use_new_resolver: + if resolver_variant == "2020-resolver": assert 'Links are not allowed as constraints' in result.stderr else: assert 'Running setup.py develop for singlemodule' in result.stdout -def test_constraints_constrain_to_local(script, data, use_new_resolver): +def test_constraints_constrain_to_local(script, data, resolver_variant): to_install = data.src.joinpath("singlemodule") script.scratch_path.joinpath("constraints.txt").write_text( "{url}#egg=singlemodule".format(url=path_to_url(to_install)) @@ -403,15 +403,15 @@ def test_constraints_constrain_to_local(script, data, use_new_resolver): 'install', '--no-index', '-f', data.find_links, '-c', script.scratch_path / 'constraints.txt', 'singlemodule', allow_stderr_warning=True, - expect_error=use_new_resolver + expect_error=(resolver_variant == "2020-resolver"), ) - if use_new_resolver: + if resolver_variant == "2020-resolver": assert 'Links are not allowed as constraints' in result.stderr else: assert 'Running setup.py install for singlemodule' in result.stdout -def test_constrained_to_url_install_same_url(script, data, use_new_resolver): +def test_constrained_to_url_install_same_url(script, data, resolver_variant): to_install = data.src.joinpath("singlemodule") constraints = path_to_url(to_install) + "#egg=singlemodule" script.scratch_path.joinpath("constraints.txt").write_text(constraints) @@ -419,9 +419,9 @@ def test_constrained_to_url_install_same_url(script, data, use_new_resolver): 'install', '--no-index', '-f', data.find_links, '-c', script.scratch_path / 'constraints.txt', to_install, allow_stderr_warning=True, - expect_error=use_new_resolver + expect_error=(resolver_variant == "2020-resolver"), ) - if use_new_resolver: + if resolver_variant == "2020-resolver": assert 'Links are not allowed as constraints' in result.stderr else: assert ('Running setup.py install for singlemodule' @@ -462,7 +462,7 @@ def test_double_install_spurious_hash_mismatch( assert 'Successfully installed simple-1.0' in str(result) -def test_install_with_extras_from_constraints(script, data, use_new_resolver): +def test_install_with_extras_from_constraints(script, data, resolver_variant): to_install = data.packages.joinpath("LocalExtras") script.scratch_path.joinpath("constraints.txt").write_text( "{url}#egg=LocalExtras[bar]".format(url=path_to_url(to_install)) @@ -470,9 +470,9 @@ def test_install_with_extras_from_constraints(script, data, use_new_resolver): result = script.pip_install_local( '-c', script.scratch_path / 'constraints.txt', 'LocalExtras', allow_stderr_warning=True, - expect_error=use_new_resolver + expect_error=(resolver_variant == "2020-resolver"), ) - if use_new_resolver: + if resolver_variant == "2020-resolver": assert 'Links are not allowed as constraints' in result.stderr else: result.did_create(script.site_packages / 'simple') @@ -494,7 +494,7 @@ def test_install_with_extras_from_install(script): result.did_create(script.site_packages / 'singlemodule.py') -def test_install_with_extras_joined(script, data, use_new_resolver): +def test_install_with_extras_joined(script, data, resolver_variant): to_install = data.packages.joinpath("LocalExtras") script.scratch_path.joinpath("constraints.txt").write_text( "{url}#egg=LocalExtras[bar]".format(url=path_to_url(to_install)) @@ -502,16 +502,16 @@ def test_install_with_extras_joined(script, data, use_new_resolver): result = script.pip_install_local( '-c', script.scratch_path / 'constraints.txt', 'LocalExtras[baz]', allow_stderr_warning=True, - expect_error=use_new_resolver + expect_error=(resolver_variant == "2020-resolver"), ) - if use_new_resolver: + if resolver_variant == "2020-resolver": assert 'Links are not allowed as constraints' in result.stderr else: result.did_create(script.site_packages / 'simple') result.did_create(script.site_packages / 'singlemodule.py') -def test_install_with_extras_editable_joined(script, data, use_new_resolver): +def test_install_with_extras_editable_joined(script, data, resolver_variant): to_install = data.packages.joinpath("LocalExtras") script.scratch_path.joinpath("constraints.txt").write_text( "-e {url}#egg=LocalExtras[bar]".format(url=path_to_url(to_install)) @@ -519,9 +519,9 @@ def test_install_with_extras_editable_joined(script, data, use_new_resolver): result = script.pip_install_local( '-c', script.scratch_path / 'constraints.txt', 'LocalExtras[baz]', allow_stderr_warning=True, - expect_error=use_new_resolver + expect_error=(resolver_variant == "2020-resolver"), ) - if use_new_resolver: + if resolver_variant == "2020-resolver": assert 'Links are not allowed as constraints' in result.stderr else: result.did_create(script.site_packages / 'simple') @@ -550,7 +550,7 @@ def test_install_distribution_duplicate_extras(script, data): def test_install_distribution_union_with_constraints( script, data, - use_new_resolver + resolver_variant, ): to_install = data.packages.joinpath("LocalExtras") script.scratch_path.joinpath("constraints.txt").write_text( @@ -558,9 +558,9 @@ def test_install_distribution_union_with_constraints( result = script.pip_install_local( '-c', script.scratch_path / 'constraints.txt', to_install + '[baz]', allow_stderr_warning=True, - expect_error=use_new_resolver + expect_error=(resolver_variant == "2020-resolver"), ) - if use_new_resolver: + if resolver_variant == "2020-resolver": msg = 'Unnamed requirements are not allowed as constraints' assert msg in result.stderr else: @@ -571,16 +571,16 @@ def test_install_distribution_union_with_constraints( def test_install_distribution_union_with_versions( script, data, - use_new_resolver, + resolver_variant, ): to_install_001 = data.packages.joinpath("LocalExtras") to_install_002 = data.packages.joinpath("LocalExtras-0.0.2") result = script.pip_install_local( to_install_001 + "[bar]", to_install_002 + "[baz]", - expect_error=use_new_resolver, + expect_error=(resolver_variant == "2020-resolver"), ) - if use_new_resolver: + if resolver_variant == "2020-resolver": assert ( "Cannot install localextras[bar] 0.0.1 and localextras[baz] 0.0.2 " "because these package versions have conflicting dependencies." diff --git a/tests/functional/test_install_upgrade.py b/tests/functional/test_install_upgrade.py index 02e221101c5..923a594c623 100644 --- a/tests/functional/test_install_upgrade.py +++ b/tests/functional/test_install_upgrade.py @@ -40,7 +40,7 @@ def test_invalid_upgrade_strategy_causes_error(script): def test_only_if_needed_does_not_upgrade_deps_when_satisfied( script, - use_new_resolver, + resolver_variant, with_wheel ): """ @@ -62,7 +62,7 @@ def test_only_if_needed_does_not_upgrade_deps_when_satisfied( ), "should not have uninstalled simple==2.0" msg = "Requirement already satisfied" - if not use_new_resolver: + if resolver_variant == "legacy": msg = msg + ", skipping upgrade: simple" assert ( msg in result.stdout @@ -184,7 +184,7 @@ def test_upgrade_if_requested(script, with_wheel): ) -def test_upgrade_with_newest_already_installed(script, data, use_new_resolver): +def test_upgrade_with_newest_already_installed(script, data, resolver_variant): """ If the newest version of a package is already installed, the package should not be reinstalled and the user should be informed. @@ -194,7 +194,7 @@ def test_upgrade_with_newest_already_installed(script, data, use_new_resolver): 'install', '--upgrade', '-f', data.find_links, '--no-index', 'simple' ) assert not result.files_created, 'simple upgraded when it should not have' - if use_new_resolver: + if resolver_variant == "2020-resolver": msg = "Requirement already satisfied" else: msg = "already up-to-date" diff --git a/tests/functional/test_wheel.py b/tests/functional/test_wheel.py index 5e91fea8ab8..f75b009beb7 100644 --- a/tests/functional/test_wheel.py +++ b/tests/functional/test_wheel.py @@ -193,7 +193,7 @@ def test_pip_wheel_fail(script, data): def test_no_clean_option_blocks_cleaning_after_wheel( script, data, - use_new_resolver, + resolver_variant, ): """ Test --no-clean option blocks cleaning after wheel build @@ -209,7 +209,7 @@ def test_no_clean_option_blocks_cleaning_after_wheel( allow_stderr_warning=True, ) - if not use_new_resolver: + if resolver_variant == "legacy": build = build / 'simple' message = "build/simple should still exist {}".format(result) assert exists(build), message From 2d91950cad26906657c6dedc7f7f4a5be9fa6e54 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Fri, 30 Oct 2020 05:59:14 +0530 Subject: [PATCH 2669/3170] Allow passing legacy-resolver from CLI --- src/pip/_internal/cli/cmdoptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index 86bc740f83c..ec4df3fcc77 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -905,7 +905,7 @@ def check_list_path_option(options): metavar='feature', action='append', default=[], - choices=[], + choices=['legacy-resolver'], help=( 'Enable deprecated functionality, that will be removed in the future.' ), From 07ec3013f0c746f610291dd86fa607a8752fa74a Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Fri, 30 Oct 2020 06:02:41 +0530 Subject: [PATCH 2670/3170] Drop custom logic for new_resolver tests --- setup.cfg | 1 - tests/conftest.py | 6 ------ 2 files changed, 7 deletions(-) diff --git a/setup.cfg b/setup.cfg index 5f4fd4036ac..8d9edbcc069 100644 --- a/setup.cfg +++ b/setup.cfg @@ -61,7 +61,6 @@ markers = mercurial: VCS: Mercurial git: VCS: git yaml: yaml based tests - fails_on_new_resolver: Does not yet work on the new resolver [coverage:run] branch = True diff --git a/tests/conftest.py b/tests/conftest.py index 629227ed80d..53eb90391b3 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -68,12 +68,6 @@ def pytest_collection_modifyitems(config, items): if item.get_closest_marker('network') is not None: item.add_marker(pytest.mark.flaky(reruns=3, reruns_delay=2)) - if (item.get_closest_marker('fails_on_new_resolver') and - config.getoption("--new-resolver") and - not config.getoption("--new-resolver-runtests")): - item.add_marker(pytest.mark.skip( - 'This test does not work with the new resolver')) - if six.PY3: if (item.get_closest_marker('incompatible_with_test_venv') and config.getoption("--use-venv")): From 6859de08d96801e4b17641c3b6f19ef303350c19 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Fri, 30 Oct 2020 06:06:18 +0530 Subject: [PATCH 2671/3170] Get the resolver name directly from test CLI --- tests/conftest.py | 23 +++++++++-------------- 1 file changed, 9 insertions(+), 14 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 53eb90391b3..be00094d3c6 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -39,16 +39,11 @@ def pytest_addoption(parser): help="keep temporary test directories", ) parser.addoption( - "--new-resolver", - action="store_true", - default=False, - help="use new resolver in tests", - ) - parser.addoption( - "--new-resolver-runtests", - action="store_true", - default=False, - help="run the skipped tests for the new resolver", + "--resolver", + action="store", + default="2020-resolver", + choices=["2020-resolver", "legacy"], + help="use given resolver in tests", ) parser.addoption( "--use-venv", @@ -100,17 +95,17 @@ def pytest_collection_modifyitems(config, items): def resolver_variant(request): """Set environment variable to make pip default to the correct resolver. """ - new_resolver = request.config.getoption("--new-resolver") + resolver = request.config.getoption("--resolver") + + # Handle the environment variables for this test. features = set(os.environ.get("PIP_USE_FEATURE", "").split()) if new_resolver: - retval = "2020-resolver" features.add("2020-resolver") else: - retval = "legacy" features.discard("2020-resolver") with patch.dict(os.environ, {"PIP_USE_FEATURE": " ".join(features)}): - yield retval + yield resolver @pytest.fixture(scope='session') From 29d1f4506e98814510cc4e67a5a1792d521360f4 Mon Sep 17 00:00:00 2001 From: Bernard <bernard@ei8fdb.org> Date: Fri, 30 Oct 2020 12:31:01 +0100 Subject: [PATCH 2672/3170] Adding link to Constraints Files --- docs/html/user_guide.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index ca3a58cb67b..925a0273b50 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -1469,7 +1469,7 @@ However there is a possibility pip will not be able to find a set of compatible versions. If you'd prefer not to wait, you can interrupt pip (ctrl and c) and use -constraints to reduce the number of package versions it tries. +:ref:`Constraints Files`: to reduce the number of package versions it tries. .. _2-reduce-the-versions-of-the-backtracking-package: From e7237c6e848eecba20f21423c39a7d6dc8b93941 Mon Sep 17 00:00:00 2001 From: Noah Gorny <noah@gittabags.com> Date: Fri, 30 Oct 2020 13:34:11 +0200 Subject: [PATCH 2673/3170] Update gitignore to ignore sublime text config files --- .gitignore | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.gitignore b/.gitignore index 946385eef5e..dc6244855fe 100644 --- a/.gitignore +++ b/.gitignore @@ -39,6 +39,10 @@ tests/data/common_wheels/ # For Visual Studio Code .vscode/ +# For Sublime Text +*.sublime-workspace +*.sublime-project + # Scratch Pad for experiments .scratch/ From 00f3d57131b953c01b3c94a6a1e4ae7961f3948c Mon Sep 17 00:00:00 2001 From: Bernard <bernard@ei8fdb.org> Date: Fri, 30 Oct 2020 12:35:52 +0100 Subject: [PATCH 2674/3170] Final heading edit. --- docs/html/user_guide.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 925a0273b50..bd6e2baced1 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -1473,8 +1473,8 @@ If you'd prefer not to wait, you can interrupt pip (ctrl and c) and use .. _2-reduce-the-versions-of-the-backtracking-package: -2. Reduce the versions of the backtracking package -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +2. Reduce the number of versions pip will try to backtrack through +~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ If pip is backtracking more than you'd like, the next option is to constrain the number of package versions it tries. From c33cf49381ff9212bffcaed7517ce38171ad634c Mon Sep 17 00:00:00 2001 From: Xavier Fernandez <xavier.fernandez@polyconseil.fr> Date: Fri, 30 Oct 2020 10:06:18 +0100 Subject: [PATCH 2675/3170] freeze: deprecate option --find-links --- news/9069.removal.rst | 1 + src/pip/_internal/commands/freeze.py | 9 +++++++++ tests/functional/test_freeze.py | 3 ++- 3 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 news/9069.removal.rst diff --git a/news/9069.removal.rst b/news/9069.removal.rst new file mode 100644 index 00000000000..a7fae08da4b --- /dev/null +++ b/news/9069.removal.rst @@ -0,0 +1 @@ +Deprecate ``--find-links`` option in ``pip freeze`` diff --git a/src/pip/_internal/commands/freeze.py b/src/pip/_internal/commands/freeze.py index 2071fbabd61..20084a4981a 100644 --- a/src/pip/_internal/commands/freeze.py +++ b/src/pip/_internal/commands/freeze.py @@ -9,6 +9,7 @@ from pip._internal.models.format_control import FormatControl from pip._internal.operations.freeze import freeze from pip._internal.utils.compat import stdlib_pkgs +from pip._internal.utils.deprecation import deprecated from pip._internal.utils.typing import MYPY_CHECK_RUNNING DEV_PKGS = {'pip', 'setuptools', 'distribute', 'wheel'} @@ -86,6 +87,14 @@ def run(self, options, args): cmdoptions.check_list_path_option(options) + if options.find_links: + deprecated( + "--find-links option in pip freeze is deprecated.", + replacement=None, + gone_in="21.2", + issue=9069, + ) + freeze_kwargs = dict( requirement=options.requirements, find_links=options.find_links, diff --git a/tests/functional/test_freeze.py b/tests/functional/test_freeze.py index ef22169869b..74d676aeda2 100644 --- a/tests/functional/test_freeze.py +++ b/tests/functional/test_freeze.py @@ -358,7 +358,8 @@ def test_freeze_mercurial_clone_srcdir(script, tmpdir): _check_output(result.stdout, expected) result = script.pip( - 'freeze', '-f', '{repo_dir}#egg=pip_test_package'.format(**locals()) + 'freeze', '-f', '{repo_dir}#egg=pip_test_package'.format(**locals()), + expect_stderr=True, ) expected = textwrap.dedent( """ From 0d8acc9000471becf97b91b4465777b37bf5e011 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Fri, 30 Oct 2020 06:10:07 +0530 Subject: [PATCH 2676/3170] Flip the switch in the new resolver - Python 2 doesn't get the new shiny thing. - Passing --use-deprecated=legacy-resolver uses the deprecated legacy resolver. - Passing --use-feature=2020-resolver is now a no-op, that prints a warning that it's going to be removed. - Using fast-deps without the new resolver will cause a warning to be printed. --- src/pip/_internal/cli/req_command.py | 27 ++++++++++++++++++++++----- 1 file changed, 22 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index f92e2b509f7..f2d1c605b51 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -9,6 +9,8 @@ import os from functools import partial +from pip._vendor.six import PY2 + from pip._internal.cli import cmdoptions from pip._internal.cli.base_command import Command from pip._internal.cli.command_context import CommandContextMixIn @@ -198,11 +200,26 @@ def __init__(self, *args, **kw): def determine_resolver_variant(options): # type: (Values) -> str """Determines which resolver should be used, based on the given options.""" + # We didn't want to change things for Python 2, since it's nearly done with + # and we're using performance improvements that only work on Python 3. + if PY2: + if '2020-resolver' in options.features_enabled: + return "2020-resolver" + else: + return "legacy" + + # Warn about the options that are gonna be removed. if '2020-resolver' in options.features_enabled: - resolver_variant = "2020-resolver" - else: - resolver_variant = "legacy" - return resolver_variant + logger.warning( + "--use-feature=2020-resolver no longer has any effect, " + "since it is now the default dependency resolver in pip. " + "This will become an error in pip 21.0." + ) + + if "legacy-resolver" in options.deprecated_features_enabled: + return "legacy" + + return "2020-resolver" @classmethod def make_requirement_preparer( @@ -235,7 +252,7 @@ def make_requirement_preparer( ) else: lazy_wheel = False - if lazy_wheel: + if 'fast-deps' in options.features_enabled: logger.warning( 'fast-deps has no effect when used with the legacy resolver.' ) From 6f26fb9feeb795e2c956d97a129f494a4104c457 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Fri, 30 Oct 2020 06:57:08 +0530 Subject: [PATCH 2677/3170] Update tests for resolver changes --- tests/conftest.py | 20 ++++- tests/functional/test_install.py | 2 +- tests/functional/test_new_resolver.py | 95 ++++++++++---------- tests/functional/test_new_resolver_errors.py | 2 +- tests/functional/test_new_resolver_hashes.py | 4 - tests/functional/test_new_resolver_target.py | 2 +- tests/functional/test_new_resolver_user.py | 28 +++--- 7 files changed, 78 insertions(+), 75 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index be00094d3c6..6f7bd1a6cfd 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -99,12 +99,24 @@ def resolver_variant(request): # Handle the environment variables for this test. features = set(os.environ.get("PIP_USE_FEATURE", "").split()) - if new_resolver: - features.add("2020-resolver") + deprecated_features = set(os.environ.get("PIP_USE_DEPRECATED", "").split()) + + if six.PY3: + if resolver == "legacy": + deprecated_features.add("legacy-resolver") + else: + deprecated_features.discard("legacy-resolver") else: - features.discard("2020-resolver") + if resolver == "2020-resolver": + features.add("2020-resolver") + else: + features.discard("2020-resolver") - with patch.dict(os.environ, {"PIP_USE_FEATURE": " ".join(features)}): + env = { + "PIP_USE_FEATURE": " ".join(features), + "PIP_USE_DEPRECATED": " ".join(deprecated_features), + } + with patch.dict(os.environ, env): yield resolver diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 0d2ad0f2b22..b20e09f4062 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -407,7 +407,7 @@ def test_vcs_url_urlquote_normalization(script, tmpdir): ) -@pytest.mark.parametrize("resolver", ["", "--use-feature=2020-resolver"]) +@pytest.mark.parametrize("resolver", ["", "--use-deprecated=legacy-resolver"]) def test_basic_install_from_local_directory( script, data, resolver, with_wheel ): diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 91befb7f53c..0465975eecc 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -53,7 +53,7 @@ def test_new_resolver_can_install(script): "0.1.0", ) script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "simple" @@ -68,7 +68,7 @@ def test_new_resolver_can_install_with_version(script): "0.1.0", ) script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "simple==0.1.0" @@ -88,7 +88,7 @@ def test_new_resolver_picks_latest_version(script): "0.2.0", ) script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "simple" @@ -108,7 +108,7 @@ def test_new_resolver_picks_installed_version(script): "0.2.0", ) script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "simple==0.1.0" @@ -116,7 +116,7 @@ def test_new_resolver_picks_installed_version(script): assert_installed(script, simple="0.1.0") result = script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "simple" @@ -137,7 +137,7 @@ def test_new_resolver_picks_installed_version_if_no_match_found(script): "0.2.0", ) script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "simple==0.1.0" @@ -145,7 +145,7 @@ def test_new_resolver_picks_installed_version_if_no_match_found(script): assert_installed(script, simple="0.1.0") result = script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "simple" ) @@ -166,7 +166,7 @@ def test_new_resolver_installs_dependencies(script): "0.1.0", ) script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "base" @@ -187,7 +187,7 @@ def test_new_resolver_ignore_dependencies(script): "0.1.0", ) script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--no-deps", "--find-links", script.scratch_path, "base" @@ -219,7 +219,7 @@ def test_new_resolver_installs_extras(tmpdir, script, root_dep): "0.1.0", ) script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "-r", req_file, @@ -243,7 +243,7 @@ def test_new_resolver_installs_extras_deprecated(tmpdir, script): "0.1.0", ) result = script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "-r", req_file, @@ -266,7 +266,7 @@ def test_new_resolver_installs_extras_warn_missing(script): "0.1.0", ) result = script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "base[add,missing]", @@ -280,7 +280,7 @@ def test_new_resolver_installs_extras_warn_missing(script): def test_new_resolver_installed_message(script): create_basic_wheel_for_package(script, "A", "1.0") result = script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "A", @@ -292,7 +292,7 @@ def test_new_resolver_installed_message(script): def test_new_resolver_no_dist_message(script): create_basic_wheel_for_package(script, "A", "1.0") result = script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "B", @@ -323,7 +323,7 @@ def test_new_resolver_installs_editable(script): version="0.1.0", ) script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "base", @@ -371,7 +371,6 @@ def test_new_resolver_requires_python( args = [ "install", - "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, @@ -393,7 +392,7 @@ def test_new_resolver_requires_python_error(script): requires_python="<2", ) result = script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "base", @@ -421,7 +420,7 @@ def test_new_resolver_installed(script): ) result = script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "base", @@ -429,7 +428,7 @@ def test_new_resolver_installed(script): assert "Requirement already satisfied" not in result.stdout, str(result) result = script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "base~=0.1.0", @@ -451,7 +450,7 @@ def test_new_resolver_ignore_installed(script): satisfied_output = "Requirement already satisfied" result = script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "base", @@ -459,7 +458,7 @@ def test_new_resolver_ignore_installed(script): assert satisfied_output not in result.stdout, str(result) result = script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--ignore-installed", "--find-links", script.scratch_path, "base", @@ -492,7 +491,7 @@ def test_new_resolver_only_builds_sdists_when_needed(script): ) # We only ever need to check dep 0.2.0 as it's the latest version script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "base" @@ -501,7 +500,7 @@ def test_new_resolver_only_builds_sdists_when_needed(script): # We merge criteria here, as we have two "dep" requirements script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "base", "dep" @@ -514,7 +513,7 @@ def test_new_resolver_install_different_version(script): create_basic_wheel_for_package(script, "base", "0.2.0") script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "base==0.1.0", @@ -522,7 +521,7 @@ def test_new_resolver_install_different_version(script): # This should trigger an uninstallation of base. result = script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "base==0.2.0", @@ -541,7 +540,7 @@ def test_new_resolver_force_reinstall(script): create_basic_wheel_for_package(script, "base", "0.1.0") script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "base==0.1.0", @@ -550,7 +549,7 @@ def test_new_resolver_force_reinstall(script): # This should trigger an uninstallation of base due to --force-reinstall, # even though the installed version matches. result = script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "--force-reinstall", @@ -589,7 +588,7 @@ def test_new_resolver_handles_prerelease( for version in available_versions: create_basic_wheel_for_package(script, "pkg", version) script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, *pip_args @@ -611,7 +610,7 @@ def test_new_reolver_skips_marker(script, pkg_deps, root_deps): create_basic_wheel_for_package(script, "dep", "1.0") script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, *root_deps @@ -636,7 +635,7 @@ def test_new_resolver_constraints(script, constraints): constraints_file = script.scratch_path / "constraints.txt" constraints_file.write_text("\n".join(constraints)) script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "-c", constraints_file, @@ -652,7 +651,7 @@ def test_new_resolver_constraint_no_specifier(script): constraints_file = script.scratch_path / "constraints.txt" constraints_file.write_text("pkg") script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "-c", constraints_file, @@ -683,7 +682,7 @@ def test_new_resolver_constraint_reject_invalid(script, constraint, error): constraints_file = script.scratch_path / "constraints.txt" constraints_file.write_text(constraint) result = script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "-c", constraints_file, @@ -702,7 +701,7 @@ def test_new_resolver_constraint_on_dependency(script): constraints_file = script.scratch_path / "constraints.txt" constraints_file.write_text("dep==2.0") script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "-c", constraints_file, @@ -719,7 +718,7 @@ def test_new_resolver_constraint_on_path(script): constraints_txt = script.scratch_path / "constraints.txt" constraints_txt.write_text("foo==1.0") result = script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "-c", constraints_txt, str(script.scratch_path), @@ -745,7 +744,7 @@ def test_new_resolver_constraint_only_marker_match(script): constraints_txt.write_text(constrants_content) script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "-c", constraints_txt, "--find-links", script.scratch_path, @@ -758,7 +757,7 @@ def test_new_resolver_upgrade_needs_option(script): # Install pkg 1.0.0 create_basic_wheel_for_package(script, "pkg", "1.0.0") script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "pkg", @@ -769,7 +768,7 @@ def test_new_resolver_upgrade_needs_option(script): # This should not upgrade because we don't specify --upgrade result = script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "pkg", @@ -780,7 +779,7 @@ def test_new_resolver_upgrade_needs_option(script): # This should upgrade result = script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "--upgrade", @@ -800,7 +799,7 @@ def test_new_resolver_upgrade_strategy(script): create_basic_wheel_for_package(script, "base", "1.0.0", depends=["dep"]) create_basic_wheel_for_package(script, "dep", "1.0.0") script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "base", @@ -814,7 +813,7 @@ def test_new_resolver_upgrade_strategy(script): create_basic_wheel_for_package(script, "dep", "2.0.0") script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "--upgrade", @@ -828,7 +827,7 @@ def test_new_resolver_upgrade_strategy(script): create_basic_wheel_for_package(script, "base", "3.0.0", depends=["dep"]) script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "--upgrade", "--upgrade-strategy=eager", @@ -907,7 +906,7 @@ def test_new_resolver_extra_merge_in_package( ) script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, requirement + "[dev]", @@ -946,7 +945,7 @@ def test_new_resolver_build_directory_error_zazo_19(script): create_basic_sdist_for_package(script, "pkg_b", "1.0.0") script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "pkg-a", "pkg-b", @@ -959,7 +958,7 @@ def test_new_resolver_upgrade_same_version(script): create_basic_wheel_for_package(script, "pkg", "1") script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "pkg", @@ -967,7 +966,7 @@ def test_new_resolver_upgrade_same_version(script): assert_installed(script, pkg="2") script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "--upgrade", @@ -983,7 +982,7 @@ def test_new_resolver_local_and_req(script): version="0.1.0", ) script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", source_dir, "pkg!=0.1.0", expect_error=True, @@ -1006,7 +1005,6 @@ def test_new_resolver_no_deps_checks_requires_python(script): result = script.pip( "install", - "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--no-deps", @@ -1029,7 +1027,6 @@ def test_new_resolver_prefers_installed_in_upgrade_if_latest(script): # Install the version that's not on the index. script.pip( "install", - "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", local_pkg, @@ -1038,7 +1035,6 @@ def test_new_resolver_prefers_installed_in_upgrade_if_latest(script): # Now --upgrade should still pick the local version because it's "better". script.pip( "install", - "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, @@ -1079,7 +1075,6 @@ def test_new_resolver_presents_messages_when_backtracking_a_lot(script, N): # Install A result = script.pip( "install", - "--use-feature=2020-resolver", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, diff --git a/tests/functional/test_new_resolver_errors.py b/tests/functional/test_new_resolver_errors.py index 267f2130710..830acc764e9 100644 --- a/tests/functional/test_new_resolver_errors.py +++ b/tests/functional/test_new_resolver_errors.py @@ -15,7 +15,7 @@ def test_new_resolver_conflict_requirements_file(tmpdir, script): req_file.write_text("pkga\npkgb") result = script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "-r", req_file, diff --git a/tests/functional/test_new_resolver_hashes.py b/tests/functional/test_new_resolver_hashes.py index ad5e2f3d051..6fa642f8b8f 100644 --- a/tests/functional/test_new_resolver_hashes.py +++ b/tests/functional/test_new_resolver_hashes.py @@ -72,7 +72,6 @@ def test_new_resolver_hash_intersect(script, requirements_template, message): result = script.pip( "install", - "--use-feature=2020-resolver", "--no-cache-dir", "--no-deps", "--no-index", @@ -105,7 +104,6 @@ def test_new_resolver_hash_intersect_from_constraint(script): result = script.pip( "install", - "--use-feature=2020-resolver", "--no-cache-dir", "--no-deps", "--no-index", @@ -162,7 +160,6 @@ def test_new_resolver_hash_intersect_empty( result = script.pip( "install", - "--use-feature=2020-resolver", "--no-cache-dir", "--no-deps", "--no-index", @@ -193,7 +190,6 @@ def test_new_resolver_hash_intersect_empty_from_constraint(script): result = script.pip( "install", - "--use-feature=2020-resolver", "--no-cache-dir", "--no-deps", "--no-index", diff --git a/tests/functional/test_new_resolver_target.py b/tests/functional/test_new_resolver_target.py index 6189e1cb5bc..037244a2c4b 100644 --- a/tests/functional/test_new_resolver_target.py +++ b/tests/functional/test_new_resolver_target.py @@ -37,7 +37,7 @@ def test_new_resolver_target_checks_compatibility_failure( ): fake_wheel_tag = "fakepy1-fakeabi-fakeplat" args = [ - "install", "--use-feature=2020-resolver", + "install", "--only-binary=:all:", "--no-cache-dir", "--no-index", "--target", str(script.scratch_path.joinpath("target")), diff --git a/tests/functional/test_new_resolver_user.py b/tests/functional/test_new_resolver_user.py index 2aae3eb16cb..dd617318cef 100644 --- a/tests/functional/test_new_resolver_user.py +++ b/tests/functional/test_new_resolver_user.py @@ -10,7 +10,7 @@ def test_new_resolver_install_user(script): create_basic_wheel_for_package(script, "base", "0.1.0") result = script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "--user", @@ -28,13 +28,13 @@ def test_new_resolver_install_user_satisfied_by_global_site(script): create_basic_wheel_for_package(script, "base", "1.0.0") script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "base==1.0.0", ) result = script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "--user", @@ -54,7 +54,7 @@ def test_new_resolver_install_user_conflict_in_user_site(script): create_basic_wheel_for_package(script, "base", "2.0.0") script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "--user", @@ -62,7 +62,7 @@ def test_new_resolver_install_user_conflict_in_user_site(script): ) result = script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "--user", @@ -82,13 +82,13 @@ def test_new_resolver_install_user_in_virtualenv_with_conflict_fails(script): create_basic_wheel_for_package(script, "base", "2.0.0") script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "base==2.0.0", ) result = script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "--user", @@ -128,13 +128,13 @@ def test_new_resolver_install_user_reinstall_global_site(script): create_basic_wheel_for_package(script, "base", "1.0.0") script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "base==1.0.0", ) result = script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "--user", @@ -159,14 +159,14 @@ def test_new_resolver_install_user_conflict_in_global_site(script): create_basic_wheel_for_package(script, "base", "2.0.0") script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "base==1.0.0", ) result = script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "--user", @@ -191,13 +191,13 @@ def test_new_resolver_install_user_conflict_in_global_and_user_sites(script): create_basic_wheel_for_package(script, "base", "2.0.0") script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "base==2.0.0", ) script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "--user", @@ -206,7 +206,7 @@ def test_new_resolver_install_user_conflict_in_global_and_user_sites(script): ) result = script.pip( - "install", "--use-feature=2020-resolver", + "install", "--no-cache-dir", "--no-index", "--find-links", script.scratch_path, "--user", From ec9bb109228aa3d8ad2952d72d1374b43157be53 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Fri, 30 Oct 2020 06:57:15 +0530 Subject: [PATCH 2678/3170] Update YAML tests for resolver changes --- tests/functional/test_yaml.py | 12 ++++++------ tests/yaml/backtrack.yml | 2 +- tests/yaml/conflict_1.yml | 10 +++++----- tests/yaml/conflict_2.yml | 2 +- tests/yaml/conflict_3.yml | 2 +- tests/yaml/conflicting_diamond.yml | 2 +- tests/yaml/conflicting_triangle.yml | 2 +- tests/yaml/extras.yml | 2 +- tests/yaml/fallback.yml | 2 +- tests/yaml/large.yml | 4 ++-- tests/yaml/overlap1.yml | 2 +- tests/yaml/pip988.yml | 2 +- tests/yaml/poetry2298.yml | 2 +- 13 files changed, 23 insertions(+), 23 deletions(-) diff --git a/tests/functional/test_yaml.py b/tests/functional/test_yaml.py index e22a01ace91..4b5f38f97ec 100644 --- a/tests/functional/test_yaml.py +++ b/tests/functional/test_yaml.py @@ -27,7 +27,7 @@ def generate_yaml_tests(directory): base = data.get("base", {}) cases = data["cases"] - for resolver in 'old', 'new': + for resolver in 'legacy', '2020-resolver': for i, case_template in enumerate(cases): case = base.copy() case.update(case_template) @@ -39,7 +39,7 @@ def generate_yaml_tests(directory): case[":resolver:"] = resolver skip = case.pop("skip", False) - assert skip in [False, True, 'old', 'new'] + assert skip in [False, True, 'legacy', '2020-resolver'] if skip is True or skip == resolver: case = pytest.param(case, marks=pytest.mark.xfail) @@ -84,11 +84,11 @@ def stripping_split(my_str, splitwith, count=None): return retval -def handle_request(script, action, requirement, options, new_resolver=False): +def handle_request(script, action, requirement, options, resolver_variant): if action == 'install': args = ['install'] - if new_resolver: - args.append("--use-feature=2020-resolver") + if resolver_variant == "legacy": + args.append("--use-deprecated=legacy-resolver") args.extend(["--no-index", "--find-links", path_to_url(script.scratch_path)]) elif action == 'uninstall': @@ -183,7 +183,7 @@ def test_yaml_based(script, case): effect = handle_request(script, action, request[action], request.get('options', '').split(), - case[':resolver:'] == 'new') + resolver_variant=case[':resolver:']) result = effect['result'] if 0: # for analyzing output easier diff --git a/tests/yaml/backtrack.yml b/tests/yaml/backtrack.yml index 0fe843875d6..ffcb722b88c 100644 --- a/tests/yaml/backtrack.yml +++ b/tests/yaml/backtrack.yml @@ -37,4 +37,4 @@ cases: - A 1.0.0 - B 1.0.0 - C 1.0.0 - skip: old + skip: legacy diff --git a/tests/yaml/conflict_1.yml b/tests/yaml/conflict_1.yml index c0a56199541..dc18be32a1f 100644 --- a/tests/yaml/conflict_1.yml +++ b/tests/yaml/conflict_1.yml @@ -12,7 +12,7 @@ cases: - error: code: 0 stderr: ['incompatible'] - skip: old + skip: legacy # -- a good error message would be: # A 1.0.0 has incompatible requirements B==1.0.0, B==2.0.0 @@ -22,7 +22,7 @@ cases: response: - state: - B 1.0.0 - skip: old + skip: legacy # -- old error: # Double requirement given: B (already in B==1.0.0, name='B') @@ -36,7 +36,7 @@ cases: stderr: >- Cannot install B==1.0.0 and B==2.0.0 because these package versions have conflicting dependencies. - skip: old + skip: legacy # -- currently the (new resolver) error message is: # Could not find a version that satisfies the requirement B==1.0.0 # Could not find a version that satisfies the requirement B==2.0.0 @@ -55,7 +55,7 @@ cases: error: code: 1 stderr: 'no\s+matching\s+distribution' - skip: old + skip: legacy # -- currently (new resolver) error message is: # Could not find a version that satisfies the requirement B==1.5.0 # No matching distribution found for b @@ -71,7 +71,7 @@ cases: error: code: 1 stderr: 'no\s+matching\s+distribution' - skip: old + skip: legacy # -- currently the error message is: # Could not find a version that satisfies the requirement A==2.0 # No matching distribution found for a diff --git a/tests/yaml/conflict_2.yml b/tests/yaml/conflict_2.yml index 8a51ad57f95..7ec5848ed8f 100644 --- a/tests/yaml/conflict_2.yml +++ b/tests/yaml/conflict_2.yml @@ -25,4 +25,4 @@ cases: stderr: >- Cannot install six<1.12 and virtualenv 20.0.2 because these package versions have conflicting dependencies. - skip: old + skip: legacy diff --git a/tests/yaml/conflict_3.yml b/tests/yaml/conflict_3.yml index d261b3158d2..53f2b4a981f 100644 --- a/tests/yaml/conflict_3.yml +++ b/tests/yaml/conflict_3.yml @@ -11,7 +11,7 @@ cases: - install: A response: - state: null - skip: old + skip: legacy # -- currently the error message is: # Could not find a version that satisfies the requirement C==2.0.0 (from a) # Could not find a version that satisfies the requirement C==1.0.0 (from b) diff --git a/tests/yaml/conflicting_diamond.yml b/tests/yaml/conflicting_diamond.yml index 0ea5f9ca8d3..c28b667ac6b 100644 --- a/tests/yaml/conflicting_diamond.yml +++ b/tests/yaml/conflicting_diamond.yml @@ -16,4 +16,4 @@ cases: versions have conflicting dependencies. # TODO: Tweak this error message to make sense. # https://github.com/pypa/pip/issues/8495 - skip: old + skip: legacy diff --git a/tests/yaml/conflicting_triangle.yml b/tests/yaml/conflicting_triangle.yml index e8e88b34730..02b348ca2f2 100644 --- a/tests/yaml/conflicting_triangle.yml +++ b/tests/yaml/conflicting_triangle.yml @@ -15,4 +15,4 @@ cases: - error: code: 0 stderr: ['c==1\.0\.0', 'incompatible'] - skip: old + skip: legacy diff --git a/tests/yaml/extras.yml b/tests/yaml/extras.yml index ac68fae4979..b0f4e992c9c 100644 --- a/tests/yaml/extras.yml +++ b/tests/yaml/extras.yml @@ -39,7 +39,7 @@ cases: - D 1.0.0 - E 1.0.0 - F 1.0.0 - skip: old + skip: legacy - request: - install: D[extra_1] diff --git a/tests/yaml/fallback.yml b/tests/yaml/fallback.yml index 6902ad57991..86925398a56 100644 --- a/tests/yaml/fallback.yml +++ b/tests/yaml/fallback.yml @@ -17,4 +17,4 @@ cases: # the old resolver tries to install A 1.0.0 (which fails), but the new # resolver realises that A 1.0.0 cannot be installed and falls back to # installing the older version A 0.8.0 instead. - skip: old + skip: legacy diff --git a/tests/yaml/large.yml b/tests/yaml/large.yml index 0d5f6f3ef86..fbb1c737eca 100644 --- a/tests/yaml/large.yml +++ b/tests/yaml/large.yml @@ -251,7 +251,7 @@ cases: - idna 2.7 - pycparser 2.18 - six 1.11.0 - skip: old + skip: legacy - request: - install: cachecontrol @@ -292,4 +292,4 @@ cases: html5lib 0.999999999 because these package versions have conflicting dependencies. - skip: old + skip: legacy diff --git a/tests/yaml/overlap1.yml b/tests/yaml/overlap1.yml index 9cad2650b03..9afbb04c379 100644 --- a/tests/yaml/overlap1.yml +++ b/tests/yaml/overlap1.yml @@ -27,7 +27,7 @@ cases: - fussy 3.8.0 - myapp 0.2.4 - requests 1.3.0 - skip: old + skip: legacy - request: - install: fussy diff --git a/tests/yaml/pip988.yml b/tests/yaml/pip988.yml index 29441f3a0a7..1190d2a4e07 100644 --- a/tests/yaml/pip988.yml +++ b/tests/yaml/pip988.yml @@ -34,4 +34,4 @@ cases: # - B 2.0.0 # - C 1.0.0 # but because B 2.0.0 depends on C >=2.0.0 this is wrong - skip: old + skip: legacy diff --git a/tests/yaml/poetry2298.yml b/tests/yaml/poetry2298.yml index 992f3a02ef0..8b0670896ae 100644 --- a/tests/yaml/poetry2298.yml +++ b/tests/yaml/poetry2298.yml @@ -21,4 +21,4 @@ cases: - poetry 1.0.5 - sphinx 3.0.1 - zappa 0.51.0 - skip: old + skip: legacy From 40267390960d6fced106518918fdb83f89dfdff7 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Fri, 30 Oct 2020 07:16:50 +0530 Subject: [PATCH 2679/3170] Bye bye experimental tests --- .travis.yml | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/.travis.yml b/.travis.yml index 7c41f5fbccb..703cde0e560 100644 --- a/.travis.yml +++ b/.travis.yml @@ -10,7 +10,6 @@ addons: stages: - primary - secondary -- experimental jobs: include: @@ -32,25 +31,6 @@ jobs: - env: GROUP=2 python: pypy2.7-7.1.1 - # Test experimental stuff that are not part of the standard pip usage. - # Helpful for developers working on them to see how they're doing. - - stage: experimental - env: - - GROUP=1 - - NEW_RESOLVER=1 - - env: - - GROUP=2 - - NEW_RESOLVER=1 - - env: - - GROUP=3 - - NEW_RESOLVER=1 - - fast_finish: true - allow_failures: - - env: - - GROUP=3 - - NEW_RESOLVER=1 - before_install: tools/travis/setup.sh install: travis_retry tools/travis/install.sh script: tools/travis/run.sh From 47c8d38bd9f4deff0a8798673e56837ac9f2b331 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Fri, 30 Oct 2020 07:30:12 +0530 Subject: [PATCH 2680/3170] :newspaper: --- news/9019.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/9019.feature.rst diff --git a/news/9019.feature.rst b/news/9019.feature.rst new file mode 100644 index 00000000000..5f81e95fd5c --- /dev/null +++ b/news/9019.feature.rst @@ -0,0 +1 @@ +Switch to the new dependency resolver by default. From 53db14b18865604cd3dfbdd3514c5e09f80a0cf3 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Fri, 30 Oct 2020 14:41:57 +0530 Subject: [PATCH 2681/3170] Mark test about install order as an xfail --- tests/functional/test_install_reqs.py | 1 + 1 file changed, 1 insertion(+) diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index 0405c4d2445..99a317932f5 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -211,6 +211,7 @@ def test_multiple_constraints_files(script, data): assert 'installed Upper-1.0' in result.stdout +@pytest.mark.xfail(reason="Unclear what this guarantee is for.") def test_respect_order_in_requirements_file(script, data): script.scratch_path.joinpath("frameworks-req.txt").write_text(textwrap.dedent("""\ parent From 16c3205184d1efcdaba1fe7110e180bffee2afdf Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Fri, 30 Oct 2020 18:00:13 +0530 Subject: [PATCH 2682/3170] Add a better error message for no-more-responses --- tests/lib/server.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/lib/server.py b/tests/lib/server.py index ebbf120d3d7..bc8ffb7eb5e 100644 --- a/tests/lib/server.py +++ b/tests/lib/server.py @@ -97,7 +97,10 @@ def _mock_wsgi_adapter(mock): """ def adapter(environ, start_response): # type: (Environ, StartResponse) -> Body - responder = mock(environ, start_response) + try: + responder = mock(environ, start_response) + except StopIteration: + raise RuntimeError('Ran out of mocked responses.') return responder(environ, start_response) return adapter From a56fefc764f8e9503b2fc0d73b280e2cda807d67 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Fri, 30 Oct 2020 14:42:22 +0530 Subject: [PATCH 2683/3170] Add debugging information to CI --- tests/functional/test_install_reqs.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index 99a317932f5..6b57303eac7 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -585,18 +585,18 @@ def test_install_distribution_union_with_versions( assert ( "Cannot install localextras[bar] 0.0.1 and localextras[baz] 0.0.2 " "because these package versions have conflicting dependencies." - ) in result.stderr + ) in result.stderr, str(result) assert ( "localextras[bar] 0.0.1 depends on localextras 0.0.1" - ) in result.stdout + ) in result.stdout, str(result) assert ( "localextras[baz] 0.0.2 depends on localextras 0.0.2" - ) in result.stdout + ) in result.stdout, str(result) else: assert ( "Successfully installed LocalExtras-0.0.1 simple-3.0 " "singlemodule-0.0.1" - ) in result.stdout + ) in result.stdout, str(result) @pytest.mark.xfail From 8a5656f7b1524863a04e79e782fae7354b606110 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Fri, 30 Oct 2020 22:57:27 +0530 Subject: [PATCH 2684/3170] Fix a flaky test --- tests/functional/test_install_reqs.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index 6b57303eac7..5302e68d1f6 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -582,21 +582,20 @@ def test_install_distribution_union_with_versions( expect_error=(resolver_variant == "2020-resolver"), ) if resolver_variant == "2020-resolver": - assert ( - "Cannot install localextras[bar] 0.0.1 and localextras[baz] 0.0.2 " - "because these package versions have conflicting dependencies." - ) in result.stderr, str(result) + packages = ["localextras[bar] 0.0.1", "localextras[baz] 0.0.2"] + + assert "Cannot install localextras[bar]" in result.stderr assert ( "localextras[bar] 0.0.1 depends on localextras 0.0.1" - ) in result.stdout, str(result) + ) in result.stdout assert ( "localextras[baz] 0.0.2 depends on localextras 0.0.2" - ) in result.stdout, str(result) + ) in result.stdout else: assert ( "Successfully installed LocalExtras-0.0.1 simple-3.0 " "singlemodule-0.0.1" - ) in result.stdout, str(result) + ) in result.stdout @pytest.mark.xfail From 64ff484c76f7d13eda77ac6a49cbc1eb58b6bf90 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Fri, 30 Oct 2020 22:59:50 +0530 Subject: [PATCH 2685/3170] Skip tests that fail on Python 2 --- tests/functional/test_install.py | 1 + tests/functional/test_install_config.py | 3 +++ tests/functional/test_install_reqs.py | 2 -- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index b20e09f4062..f9a807bca79 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1837,6 +1837,7 @@ def test_install_yanked_file_and_print_warning(script, data): assert 'Successfully installed simple-3.0\n' in result.stdout, str(result) +@skip_if_python2 @pytest.mark.parametrize("install_args", [ (), ("--trusted-host", "localhost"), diff --git a/tests/functional/test_install_config.py b/tests/functional/test_install_config.py index dcc9c66d5a4..783f6ac7e97 100644 --- a/tests/functional/test_install_config.py +++ b/tests/functional/test_install_config.py @@ -5,6 +5,7 @@ import pytest +from tests.lib import skip_if_python2 from tests.lib.server import ( authorization_response, file_response, @@ -129,6 +130,7 @@ def test_command_line_appends_correctly(script, data): ), 'stdout: {}'.format(result.stdout) +@skip_if_python2 def test_config_file_override_stack( script, virtualenv, mock_server, shared_data ): @@ -247,6 +249,7 @@ def test_prompt_for_authentication(script, data, cert_factory): result.stdout, str(result) +@skip_if_python2 def test_do_not_prompt_for_authentication(script, data, cert_factory): """Test behaviour if --no-input option is given while installing from a index url requiring authentication diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index 5302e68d1f6..575adbe157d 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -582,8 +582,6 @@ def test_install_distribution_union_with_versions( expect_error=(resolver_variant == "2020-resolver"), ) if resolver_variant == "2020-resolver": - packages = ["localextras[bar] 0.0.1", "localextras[baz] 0.0.2"] - assert "Cannot install localextras[bar]" in result.stderr assert ( "localextras[bar] 0.0.1 depends on localextras 0.0.1" From 9725229888f21b63b90803413db1545311ee3b19 Mon Sep 17 00:00:00 2001 From: Xavier Fernandez <xavier.fernandez@polyconseil.fr> Date: Fri, 30 Oct 2020 21:50:59 +0100 Subject: [PATCH 2686/3170] Add --exclude option to pip freeze and pip list commands --- news/4256.feature.rst | 1 + news/4256.removal.rst | 2 ++ src/pip/_internal/cli/cmdoptions.py | 21 ++++++++++++++++++++- src/pip/_internal/commands/freeze.py | 4 ++++ src/pip/_internal/commands/list.py | 7 +++++++ tests/functional/test_freeze.py | 20 ++++++++++++++++++++ tests/functional/test_list.py | 15 ++++++++++++++- 7 files changed, 68 insertions(+), 2 deletions(-) create mode 100644 news/4256.feature.rst create mode 100644 news/4256.removal.rst diff --git a/news/4256.feature.rst b/news/4256.feature.rst new file mode 100644 index 00000000000..03d7c95d779 --- /dev/null +++ b/news/4256.feature.rst @@ -0,0 +1 @@ +Add ``--exclude`` option to ``pip freeze`` and ``pip list`` commands to explicitly exclude packages from the output. diff --git a/news/4256.removal.rst b/news/4256.removal.rst new file mode 100644 index 00000000000..6d560b7bba6 --- /dev/null +++ b/news/4256.removal.rst @@ -0,0 +1,2 @@ +``pip freeze`` will stop filtering the ``pip``, ``setuptools``, ``distribute`` and ``wheel`` packages from ``pip freeze`` output in a future version. +To keep the previous behavior, users should use the new ``--exclude`` option. diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index ec4df3fcc77..6a6634fb8b8 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -20,6 +20,8 @@ from optparse import SUPPRESS_HELP, Option, OptionGroup from textwrap import dedent +from pip._vendor.packaging.utils import canonicalize_name + from pip._internal.cli.progress_bars import BAR_TYPES from pip._internal.exceptions import CommandError from pip._internal.locations import USER_CACHE_DIR, get_src_prefix @@ -133,9 +135,15 @@ def _path_option_check(option, opt, value): return os.path.expanduser(value) +def _package_name_option_check(option, opt, value): + # type: (Option, str, str) -> str + return canonicalize_name(value) + + class PipOption(Option): - TYPES = Option.TYPES + ("path",) + TYPES = Option.TYPES + ("path", "package_name") TYPE_CHECKER = Option.TYPE_CHECKER.copy() + TYPE_CHECKER["package_name"] = _package_name_option_check TYPE_CHECKER["path"] = _path_option_check @@ -866,6 +874,17 @@ def check_list_path_option(options): ) +list_exclude = partial( + PipOption, + '--exclude', + dest='excludes', + action='append', + metavar='package', + type='package_name', + help="Exclude specified package from the output", +) # type: Callable[..., Option] + + no_python_version_warning = partial( Option, '--no-python-version-warning', diff --git a/src/pip/_internal/commands/freeze.py b/src/pip/_internal/commands/freeze.py index 20084a4981a..4d1ce69a124 100644 --- a/src/pip/_internal/commands/freeze.py +++ b/src/pip/_internal/commands/freeze.py @@ -74,6 +74,7 @@ def add_options(self): dest='exclude_editable', action='store_true', help='Exclude editable package from output.') + self.cmd_opts.add_option(cmdoptions.list_exclude()) self.parser.insert_option_group(0, self.cmd_opts) @@ -85,6 +86,9 @@ def run(self, options, args): if not options.freeze_all: skip.update(DEV_PKGS) + if options.excludes: + skip.update(options.excludes) + cmdoptions.check_list_path_option(options) if options.find_links: diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index a6dfa5fd578..27b15d70a52 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -12,6 +12,7 @@ from pip._internal.index.collector import LinkCollector from pip._internal.index.package_finder import PackageFinder from pip._internal.models.selection_prefs import SelectionPreferences +from pip._internal.utils.compat import stdlib_pkgs from pip._internal.utils.misc import ( dist_is_editable, get_installed_distributions, @@ -114,6 +115,7 @@ def add_options(self): help='Include editable package from output.', default=True, ) + self.cmd_opts.add_option(cmdoptions.list_exclude()) index_opts = cmdoptions.make_option_group( cmdoptions.index_group, self.parser ) @@ -147,12 +149,17 @@ def run(self, options, args): cmdoptions.check_list_path_option(options) + skip = set(stdlib_pkgs) + if options.excludes: + skip.update(options.excludes) + packages = get_installed_distributions( local_only=options.local, user_only=options.user, editables_only=options.editable, include_editables=options.include_editable, paths=options.path, + skip=skip, ) # get_not_required must be called firstly in order to find and diff --git a/tests/functional/test_freeze.py b/tests/functional/test_freeze.py index 74d676aeda2..f0a2265f3a0 100644 --- a/tests/functional/test_freeze.py +++ b/tests/functional/test_freeze.py @@ -16,6 +16,7 @@ need_mercurial, need_svn, path_to_url, + wheel, ) distribute_re = re.compile('^distribute==[0-9.]+\n', re.MULTILINE) @@ -80,6 +81,25 @@ def test_freeze_with_pip(script): assert 'pip==' in result.stdout +def test_exclude_and_normalization(script, tmpdir): + req_path = wheel.make_wheel( + name="Normalizable_Name", version="1.0").save_to_dir(tmpdir) + script.pip("install", "--no-index", req_path) + result = script.pip("freeze") + assert "Normalizable-Name" in result.stdout + result = script.pip("freeze", "--exclude", "normalizablE-namE") + assert "Normalizable-Name" not in result.stdout + + +def test_freeze_multiple_exclude_with_all(script, with_wheel): + result = script.pip('freeze', '--all') + assert 'pip==' in result.stdout + assert 'wheel==' in result.stdout + result = script.pip('freeze', '--all', '--exclude', 'pip', '--exclude', 'wheel') + assert 'pip==' not in result.stdout + assert 'wheel==' not in result.stdout + + def test_freeze_with_invalid_names(script): """ Test that invalid names produce warnings and are passed over gracefully. diff --git a/tests/functional/test_list.py b/tests/functional/test_list.py index 37787246bd0..40dfbdea30d 100644 --- a/tests/functional/test_list.py +++ b/tests/functional/test_list.py @@ -3,7 +3,7 @@ import pytest -from tests.lib import create_test_package_with_setup +from tests.lib import create_test_package_with_setup, wheel from tests.lib.path import Path @@ -94,6 +94,19 @@ def test_local_columns_flag(simple_script): assert 'simple 1.0' in result.stdout, str(result) +def test_multiple_exclude_and_normalization(script, tmpdir): + req_path = wheel.make_wheel( + name="Normalizable_Name", version="1.0").save_to_dir(tmpdir) + script.pip("install", "--no-index", req_path) + result = script.pip("list") + print(result.stdout) + assert "Normalizable-Name" in result.stdout + assert "pip" in result.stdout + result = script.pip("list", "--exclude", "normalizablE-namE", "--exclude", "pIp") + assert "Normalizable-Name" not in result.stdout + assert "pip" not in result.stdout + + @pytest.mark.network @pytest.mark.incompatible_with_test_venv def test_user_flag(script, data): From 4ba4e21419b011e28f7343af2f002dd5ee3ec3b1 Mon Sep 17 00:00:00 2001 From: Xavier Fernandez <xavier.fernandez@polyconseil.fr> Date: Fri, 30 Oct 2020 22:29:25 +0100 Subject: [PATCH 2687/3170] Explicitly state that pip/_vendor/vendor.txt should be available --- news/8327.vendor.rst | 2 ++ src/pip/_vendor/README.rst | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 news/8327.vendor.rst diff --git a/news/8327.vendor.rst b/news/8327.vendor.rst new file mode 100644 index 00000000000..7b4536ae2fa --- /dev/null +++ b/news/8327.vendor.rst @@ -0,0 +1,2 @@ +Fix devendoring instructions to explicitly state that ``vendor.txt`` should not be removed. +It is mandatory for ``pip debug`` command. diff --git a/src/pip/_vendor/README.rst b/src/pip/_vendor/README.rst index 1fd0d439440..6699d72c2f3 100644 --- a/src/pip/_vendor/README.rst +++ b/src/pip/_vendor/README.rst @@ -132,7 +132,7 @@ semi-supported method (that we don't test in our CI) and requires a bit of extra work on your end in order to solve the problems described above. 1. Delete everything in ``pip/_vendor/`` **except** for - ``pip/_vendor/__init__.py``. + ``pip/_vendor/__init__.py`` and ``pip/_vendor/vendor.txt``. 2. Generate wheels for each of pip's dependencies (and any of their dependencies) using your patched copies of these libraries. These must be placed somewhere on the filesystem that pip can access (``pip/_vendor`` is From aae52d79b935477c71df954b50053c28c8282036 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sat, 31 Oct 2020 03:27:09 +0530 Subject: [PATCH 2688/3170] Change where the 2020 resolver warning is logged --- src/pip/_internal/cli/base_command.py | 9 +++++++++ src/pip/_internal/cli/req_command.py | 8 -------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index f4f86e0e0d0..86f1733a57c 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -10,6 +10,8 @@ import sys import traceback +from pip._vendor.six import PY2 + from pip._internal.cli import cmdoptions from pip._internal.cli.command_context import CommandContextMixIn from pip._internal.cli.parser import ConfigOptionParser, UpdatingDefaultsHelpFormatter @@ -204,6 +206,13 @@ def _main(self, args): ) sys.exit(ERROR) + if '2020-resolver' in options.features_enabled and not PY2: + logger.warning( + "--use-feature=2020-resolver no longer has any effect, " + "since it is now the default dependency resolver in pip. " + "This will become an error in pip 21.0." + ) + try: status = self.run(options, args) assert isinstance(status, int) diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index f2d1c605b51..008066ab1c4 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -208,14 +208,6 @@ def determine_resolver_variant(options): else: return "legacy" - # Warn about the options that are gonna be removed. - if '2020-resolver' in options.features_enabled: - logger.warning( - "--use-feature=2020-resolver no longer has any effect, " - "since it is now the default dependency resolver in pip. " - "This will become an error in pip 21.0." - ) - if "legacy-resolver" in options.deprecated_features_enabled: return "legacy" From fad456a1653a9c0a9466f32f8c424b620202032e Mon Sep 17 00:00:00 2001 From: Xavier Fernandez <xavier.fernandez@polyconseil.fr> Date: Sat, 31 Oct 2020 00:00:32 +0100 Subject: [PATCH 2689/3170] resolver: stabilize output for tests (& users) Otherwise the test_install_distribution_union_with_versions test can end up with either: Cannot install localextras[bar] 0.0.2 and localextras[baz] 0.0.1 because these package versions have conflicting dependencies. or Cannot install localextras[baz] 0.0.2 and localextras[bar] 0.0.1 because these package versions have conflicting dependencies. --- src/pip/_internal/resolution/resolvelib/factory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 0ac8d1af9d4..8e1a68d168d 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -421,7 +421,7 @@ def describe_trigger(parent): triggers.append(trigger) if triggers: - info = text_join(triggers) + info = text_join(sorted(triggers)) else: info = "the requested packages" From 4dc48da9dbdea09364a67c98f4f3d3b4499d4b7d Mon Sep 17 00:00:00 2001 From: Xavier Fernandez <xavier.fernandez@polyconseil.fr> Date: Sat, 31 Oct 2020 15:54:54 +0100 Subject: [PATCH 2690/3170] utils: make Hashes object hashable --- src/pip/_internal/utils/hashes.py | 23 ++++++++++++++++++++++- tests/unit/test_utils.py | 10 ++++++++++ 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/utils/hashes.py b/src/pip/_internal/utils/hashes.py index b306dafe7d7..4d90f5bfda4 100644 --- a/src/pip/_internal/utils/hashes.py +++ b/src/pip/_internal/utils/hashes.py @@ -39,7 +39,12 @@ def __init__(self, hashes=None): :param hashes: A dict of algorithm names pointing to lists of allowed hex digests """ - self._allowed = {} if hashes is None else hashes + allowed = {} + if hashes is not None: + for alg, keys in hashes.items(): + # Make sure values are always sorted (to ease equality checks) + allowed[alg] = sorted(keys) + self._allowed = allowed def __and__(self, other): # type: (Hashes) -> Hashes @@ -128,6 +133,22 @@ def __bool__(self): # type: () -> bool return self.__nonzero__() + def __eq__(self, other): + # type: (object) -> bool + if not isinstance(other, Hashes): + return NotImplemented + return self._allowed == other._allowed + + def __hash__(self): + # type: () -> int + return hash( + ",".join(sorted( + ":".join((alg, digest)) + for alg, digest_list in self._allowed.items() + for digest in digest_list + )) + ) + class MissingHashes(Hashes): """A workalike for Hashes used when we're missing a hash for a requirement diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 14b4d74820f..1996b35cb37 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -541,6 +541,16 @@ def test_non_zero(self): assert not Hashes() assert not Hashes({}) + def test_equality(self): + assert Hashes() == Hashes() + assert Hashes({'sha256': ['abcd']}) == Hashes({'sha256': ['abcd']}) + assert Hashes({'sha256': ['ab', 'cd']}) == Hashes({'sha256': ['cd', 'ab']}) + + def test_hash(self): + cache = {} + cache[Hashes({'sha256': ['ab', 'cd']})] = 42 + assert cache[Hashes({'sha256': ['ab', 'cd']})] == 42 + class TestEncoding(object): """Tests for pip._internal.utils.encoding""" From 5ec275fca2b8a1944c2b0a19ae4c334febd4929f Mon Sep 17 00:00:00 2001 From: Xavier Fernandez <xavier.fernandez@polyconseil.fr> Date: Sat, 31 Oct 2020 16:00:11 +0100 Subject: [PATCH 2691/3170] Cache find_best_candidate results This is possible because self.make_candidate_evaluator only depends on: - the function arguments which are keys to the cache - self._target_python which never changes during a pip resolution - self._candidate_prefs which never changes during a pip resolution On a fresh install, pip install <a package with ~ 100 dependencies> runs on my machine in: master (a0e34e9cf707c5) ======================= 0m33.058s 0m34.105s 0m32.426s This commit =========== 0m15.860s 0m16.254s 0m15.910s pip 20.2.4 - legacy resolver ============================ 0m15.145s 0m15.040s 0m15.152s --- src/pip/_internal/index/package_finder.py | 1 + 1 file changed, 1 insertion(+) diff --git a/src/pip/_internal/index/package_finder.py b/src/pip/_internal/index/package_finder.py index b361e194d75..9f39631dde2 100644 --- a/src/pip/_internal/index/package_finder.py +++ b/src/pip/_internal/index/package_finder.py @@ -863,6 +863,7 @@ def make_candidate_evaluator( hashes=hashes, ) + @lru_cache(maxsize=None) def find_best_candidate( self, project_name, # type: str From 0d767477ccc809583be6fdcc6f9c784b94ff620d Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sat, 31 Oct 2020 23:33:49 +0530 Subject: [PATCH 2692/3170] Update AUTHORS.txt --- AUTHORS.txt | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/AUTHORS.txt b/AUTHORS.txt index 5304f51fb2d..f6fe855d7af 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -146,6 +146,7 @@ Daniel Collins Daniel Hahler Daniel Holth Daniel Jost +Daniel Katz Daniel Shaulov Daniele Esposti Daniele Procida @@ -160,6 +161,7 @@ David Bordeynik David Caro David Evans David Linke +David Poggi David Pursehouse David Tucker David Wales @@ -183,6 +185,7 @@ Eitan Adler ekristina elainechan Eli Schwartz +Elisha Hollander Ellen Marie Dash Emil Burzo Emil Styrke @@ -203,6 +206,7 @@ everdimension Felix Yan fiber-space Filip Kokosiński +Filipe Laíns Florian Briand Florian Rathgeber Francesco @@ -318,6 +322,7 @@ Kyle Persohn lakshmanaram Laszlo Kiss-Kollar Laurent Bristiel +Laurie O Laurie Opperman Leon Sasson Lev Givon @@ -398,6 +403,7 @@ nvdv Ofekmeister ofrinevo Oliver Jeeves +Oliver Mannion Oliver Tonnhofer Olivier Girardot Olivier Grisel @@ -489,6 +495,7 @@ Segev Finer SeongSoo Cho Sergey Vasilyev Seth Woodworth +shireenrao Shlomi Fish Shovan Maity Simeon Visser @@ -553,6 +560,7 @@ Ville Skyttä Vinay Sajip Vincent Philippon Vinicyus Macedo +Vipul Kumar Vitaly Babiy Vladimir Rutsky W. Trevor King From 4a4b79afb4be441d56e2fe460602e7b350de6add Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sat, 31 Oct 2020 23:33:50 +0530 Subject: [PATCH 2693/3170] Bump for release --- NEWS.rst | 86 +++++++++++++++++++ ...56-cc25-4df9-9518-4732b1e07fe5.trivial.rst | 0 ...86-202e-4275-b7ec-d6f046c0aa05.trivial.rst | 0 ...b2-c118-473b-9d99-4f6ce8bbfa83.trivial.rst | 0 ...73-eae6-43a4-b075-55f49e713fb1.trivial.rst | 0 news/4256.feature.rst | 1 - news/4256.removal.rst | 2 - ...b4-cde3-4dd5-8f37-6f4ef53e028b.trivial.rst | 0 ...4d-0a74-44c8-b3e9-483dd826fff2.trivial.rst | 0 ...22-21ae-498c-a2ce-2c354d880f5e.trivial.rst | 0 ...79-1abd-4c9a-a7bf-45701b33dd6c.trivial.rst | 0 ...c2-d540-4a25-af03-100d848acbc0.trivial.rst | 0 news/6121.feature.rst | 1 - ...ca-55da-4ca9-9cff-c15373e97ad1.trivial.rst | 0 ...8d-9886-4e00-b431-5c809500e6bf.trivial.rst | 0 news/7231.doc.rst | 1 - news/7311.doc.rst | 1 - news/8103.bugfix.rst | 2 - news/8181.removal.rst | 1 - news/8327.vendor.rst | 2 - news/8355.feature.rst | 1 - news/8417.removal.rst | 1 - news/8578.bugfix.rst | 4 - news/8636.doc.rst | 1 - news/8658.bugfix.rst | 2 - news/8676.feature.rst | 2 - news/8696.bugfix.rst | 3 - news/8752.feature.rst | 3 - news/8781.trivial.rst | 1 - news/8783.doc.rst | 1 - news/8792.bugfix.rst | 2 - news/8804.feature.rst | 3 - news/8807.doc.rst | 1 - news/8815.feature.rst | 2 - news/8827.bugfix.rst | 2 - news/8839.bugfix.rst | 3 - news/8848.doc.rst | 1 - news/8861.bugfix.rst | 1 - news/8892.feature.rst | 1 - news/8905.feature.rst | 3 - news/8924.feature.rst | 2 - news/8927.removal.rst | 1 - news/8963.bugfix.rst | 2 - news/8971.feature.rst | 1 - news/8975.feature.rst | 1 - news/8996.bugfix.rst | 3 - news/9019.feature.rst | 1 - news/9044.doc.rst | 1 - news/9049.feature.rst | 1 - news/9056.doc.rst | 1 - news/9069.removal.rst | 1 - ...ce-6164-4d1a-a05d-e9bebf43ccd0.trivial.rst | 0 ...d9-dd18-47e9-b334-5eb862054409.trivial.rst | 0 ...68-01bf-11eb-a0b1-4fe8cb1f9dcf.trivial.rst | 0 ...b7-744e-4533-b3ff-6e7a1843d573.trivial.rst | 0 ...a0-030d-11eb-92cb-6b2b625d02fc.trivial.rst | 0 ...39-edb4-4bf6-bc3f-2d37cb5759ad.trivial.rst | 0 ...e4-767e-4d73-bce5-88644b781855.trivial.rst | 0 ...c1-15b7-46b9-9162-335bb346b53f.trivial.rst | 0 ...61-84a4-4f9b-9987-762e127cb42b.trivial.rst | 0 src/pip/__init__.py | 2 +- 61 files changed, 87 insertions(+), 65 deletions(-) delete mode 100644 news/093f7456-cc25-4df9-9518-4732b1e07fe5.trivial.rst delete mode 100644 news/0e494986-202e-4275-b7ec-d6f046c0aa05.trivial.rst delete mode 100644 news/24715eb2-c118-473b-9d99-4f6ce8bbfa83.trivial.rst delete mode 100644 news/2a1e2773-eae6-43a4-b075-55f49e713fb1.trivial.rst delete mode 100644 news/4256.feature.rst delete mode 100644 news/4256.removal.rst delete mode 100644 news/4c82ffb4-cde3-4dd5-8f37-6f4ef53e028b.trivial.rst delete mode 100644 news/50cf024d-0a74-44c8-b3e9-483dd826fff2.trivial.rst delete mode 100644 news/559bb022-21ae-498c-a2ce-2c354d880f5e.trivial.rst delete mode 100644 news/5661d979-1abd-4c9a-a7bf-45701b33dd6c.trivial.rst delete mode 100644 news/5e8c60c2-d540-4a25-af03-100d848acbc0.trivial.rst delete mode 100644 news/6121.feature.rst delete mode 100644 news/629892ca-55da-4ca9-9cff-c15373e97ad1.trivial.rst delete mode 100644 news/6f8eff8d-9886-4e00-b431-5c809500e6bf.trivial.rst delete mode 100644 news/7231.doc.rst delete mode 100644 news/7311.doc.rst delete mode 100644 news/8103.bugfix.rst delete mode 100644 news/8181.removal.rst delete mode 100644 news/8327.vendor.rst delete mode 100644 news/8355.feature.rst delete mode 100644 news/8417.removal.rst delete mode 100644 news/8578.bugfix.rst delete mode 100644 news/8636.doc.rst delete mode 100644 news/8658.bugfix.rst delete mode 100644 news/8676.feature.rst delete mode 100644 news/8696.bugfix.rst delete mode 100644 news/8752.feature.rst delete mode 100644 news/8781.trivial.rst delete mode 100644 news/8783.doc.rst delete mode 100644 news/8792.bugfix.rst delete mode 100644 news/8804.feature.rst delete mode 100644 news/8807.doc.rst delete mode 100644 news/8815.feature.rst delete mode 100644 news/8827.bugfix.rst delete mode 100644 news/8839.bugfix.rst delete mode 100644 news/8848.doc.rst delete mode 100644 news/8861.bugfix.rst delete mode 100644 news/8892.feature.rst delete mode 100644 news/8905.feature.rst delete mode 100644 news/8924.feature.rst delete mode 100644 news/8927.removal.rst delete mode 100644 news/8963.bugfix.rst delete mode 100644 news/8971.feature.rst delete mode 100644 news/8975.feature.rst delete mode 100644 news/8996.bugfix.rst delete mode 100644 news/9019.feature.rst delete mode 100644 news/9044.doc.rst delete mode 100644 news/9049.feature.rst delete mode 100644 news/9056.doc.rst delete mode 100644 news/9069.removal.rst delete mode 100644 news/946beace-6164-4d1a-a05d-e9bebf43ccd0.trivial.rst delete mode 100644 news/9f8da1d9-dd18-47e9-b334-5eb862054409.trivial.rst delete mode 100644 news/a2fa2e68-01bf-11eb-a0b1-4fe8cb1f9dcf.trivial.rst delete mode 100644 news/a3a2b1b7-744e-4533-b3ff-6e7a1843d573.trivial.rst delete mode 100644 news/bc7f9ea0-030d-11eb-92cb-6b2b625d02fc.trivial.rst delete mode 100644 news/c6182139-edb4-4bf6-bc3f-2d37cb5759ad.trivial.rst delete mode 100644 news/d53425e4-767e-4d73-bce5-88644b781855.trivial.rst delete mode 100644 news/d90a40c1-15b7-46b9-9162-335bb346b53f.trivial.rst delete mode 100644 news/e1831861-84a4-4f9b-9987-762e127cb42b.trivial.rst diff --git a/NEWS.rst b/NEWS.rst index 6c6db479980..fffba7cf31f 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -9,6 +9,92 @@ .. towncrier release notes start +20.3b1 (2020-10-31) +=================== + +Deprecations and Removals +------------------------- + +- ``pip freeze`` will stop filtering the ``pip``, ``setuptools``, ``distribute`` and ``wheel`` packages from ``pip freeze`` output in a future version. + To keep the previous behavior, users should use the new ``--exclude`` option. (`#4256 <https://github.com/pypa/pip/issues/4256>`_) +- Deprecate support for Python 3.5 (`#8181 <https://github.com/pypa/pip/issues/8181>`_) +- Document that certain removals can be fast tracked. (`#8417 <https://github.com/pypa/pip/issues/8417>`_) +- Document that Python versions are generally supported until PyPI usage falls below 5%. (`#8927 <https://github.com/pypa/pip/issues/8927>`_) +- Deprecate ``--find-links`` option in ``pip freeze`` (`#9069 <https://github.com/pypa/pip/issues/9069>`_) + +Features +-------- + +- Add ``--exclude`` option to ``pip freeze`` and ``pip list`` commands to explicitly exclude packages from the output. (`#4256 <https://github.com/pypa/pip/issues/4256>`_) +- Allow multiple values for --abi and --platform. (`#6121 <https://github.com/pypa/pip/issues/6121>`_) +- Add option ``--format`` to subcommand ``list`` of ``pip cache``, with ``abspath`` choice to output the full path of a wheel file. (`#8355 <https://github.com/pypa/pip/issues/8355>`_) +- Improve error message friendliness when an environment has packages with + corrupted metadata. (`#8676 <https://github.com/pypa/pip/issues/8676>`_) +- Make the ``setup.py install`` deprecation warning less noisy. We warn only + when ``setup.py install`` succeeded and ``setup.py bdist_wheel`` failed, as + situations where both fails are most probably irrelevant to this deprecation. (`#8752 <https://github.com/pypa/pip/issues/8752>`_) +- Check the download directory for existing wheels to possibly avoid + fetching metadata when the ``fast-deps`` feature is used with + ``pip wheel`` and ``pip download``. (`#8804 <https://github.com/pypa/pip/issues/8804>`_) +- When installing a git URL that refers to a commit that is not available locally + after git clone, attempt to fetch it from the remote. (`#8815 <https://github.com/pypa/pip/issues/8815>`_) +- Include http subdirectory in ``pip cache info`` and ``pip cache purge`` commands. (`#8892 <https://github.com/pypa/pip/issues/8892>`_) +- Cache package listings on index packages so they are guarenteed to stay stable + during a pip command session. This also improves performance when a index page + is accessed multiple times during the command session. (`#8905 <https://github.com/pypa/pip/issues/8905>`_) +- New resolver: Tweak resolution logic to improve user experience when + user-supplied requirements conflict. (`#8924 <https://github.com/pypa/pip/issues/8924>`_) +- Support Python 3.9. (`#8971 <https://github.com/pypa/pip/issues/8971>`_) +- Log an informational message when backtracking takes multiple rounds on a specific package. (`#8975 <https://github.com/pypa/pip/issues/8975>`_) +- Switch to the new dependency resolver by default. (`#9019 <https://github.com/pypa/pip/issues/9019>`_) +- Remove the ``--build-dir`` option, as per the deprecation. (`#9049 <https://github.com/pypa/pip/issues/9049>`_) + +Bug Fixes +--------- + +- Propagate ``--extra-index-url`` from requirements file properly to session auth, + so that keyring auth will work as expected. (`#8103 <https://github.com/pypa/pip/issues/8103>`_) +- Allow specifying verbosity and quiet level via configuration files + and environment variables. Previously these options were treated as + boolean values when read from there while through CLI the level can be + specified. (`#8578 <https://github.com/pypa/pip/issues/8578>`_) +- Only converts Windows path to unicode on Python 2 to avoid regressions when a + POSIX environment does not configure the file system encoding correctly. (`#8658 <https://github.com/pypa/pip/issues/8658>`_) +- List downloaded distributions before exiting ``pip download`` + when using the new resolver to make the behavior the same as + that on the legacy resolver. (`#8696 <https://github.com/pypa/pip/issues/8696>`_) +- New resolver: Pick up hash declarations in constraints files and use them to + filter available distributions. (`#8792 <https://github.com/pypa/pip/issues/8792>`_) +- Avoid polluting the destination directory by resolution artifacts + when the new resolver is used for ``pip download`` or ``pip wheel``. (`#8827 <https://github.com/pypa/pip/issues/8827>`_) +- New resolver: If a package appears multiple times in user specification with + different ``--hash`` options, only hashes that present in all specifications + should be allowed. (`#8839 <https://github.com/pypa/pip/issues/8839>`_) +- Tweak the output during dependency resolution in the new resolver. (`#8861 <https://github.com/pypa/pip/issues/8861>`_) +- Correctly search for installed distributions in new resolver logic in order + to not miss packages (virtualenv packages from system-wide-packages for example) (`#8963 <https://github.com/pypa/pip/issues/8963>`_) +- Do not fail in pip freeze when encountering a ``direct_url.json`` metadata file + with editable=True. Render it as a non-editable ``file://`` URL until modern + editable installs are standardized and supported. (`#8996 <https://github.com/pypa/pip/issues/8996>`_) + +Vendored Libraries +------------------ + +- Fix devendoring instructions to explicitly state that ``vendor.txt`` should not be removed. + It is mandatory for ``pip debug`` command. + +Improved Documentation +---------------------- + +- Add documentation for '.netrc' support. (`#7231 <https://github.com/pypa/pip/issues/7231>`_) +- Add OS tabs for OS-specific commands. (`#7311 <https://github.com/pypa/pip/issues/7311>`_) +- Add note and example on keyring support for index basic-auth (`#8636 <https://github.com/pypa/pip/issues/8636>`_) +- Added initial UX feedback widgets to docs. (`#8783 <https://github.com/pypa/pip/issues/8783>`_, `#8848 <https://github.com/pypa/pip/issues/8848>`_) +- Add ux documentation (`#8807 <https://github.com/pypa/pip/issues/8807>`_) +- Update user docs to reflect new resolver as default in 20.3. (`#9044 <https://github.com/pypa/pip/issues/9044>`_) +- Improve migration guide to reflect changes in new resolver behavior. (`#9056 <https://github.com/pypa/pip/issues/9056>`_) + + 20.2.4 (2020-10-16) =================== diff --git a/news/093f7456-cc25-4df9-9518-4732b1e07fe5.trivial.rst b/news/093f7456-cc25-4df9-9518-4732b1e07fe5.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/0e494986-202e-4275-b7ec-d6f046c0aa05.trivial.rst b/news/0e494986-202e-4275-b7ec-d6f046c0aa05.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/24715eb2-c118-473b-9d99-4f6ce8bbfa83.trivial.rst b/news/24715eb2-c118-473b-9d99-4f6ce8bbfa83.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/2a1e2773-eae6-43a4-b075-55f49e713fb1.trivial.rst b/news/2a1e2773-eae6-43a4-b075-55f49e713fb1.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/4256.feature.rst b/news/4256.feature.rst deleted file mode 100644 index 03d7c95d779..00000000000 --- a/news/4256.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Add ``--exclude`` option to ``pip freeze`` and ``pip list`` commands to explicitly exclude packages from the output. diff --git a/news/4256.removal.rst b/news/4256.removal.rst deleted file mode 100644 index 6d560b7bba6..00000000000 --- a/news/4256.removal.rst +++ /dev/null @@ -1,2 +0,0 @@ -``pip freeze`` will stop filtering the ``pip``, ``setuptools``, ``distribute`` and ``wheel`` packages from ``pip freeze`` output in a future version. -To keep the previous behavior, users should use the new ``--exclude`` option. diff --git a/news/4c82ffb4-cde3-4dd5-8f37-6f4ef53e028b.trivial.rst b/news/4c82ffb4-cde3-4dd5-8f37-6f4ef53e028b.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/50cf024d-0a74-44c8-b3e9-483dd826fff2.trivial.rst b/news/50cf024d-0a74-44c8-b3e9-483dd826fff2.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/559bb022-21ae-498c-a2ce-2c354d880f5e.trivial.rst b/news/559bb022-21ae-498c-a2ce-2c354d880f5e.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/5661d979-1abd-4c9a-a7bf-45701b33dd6c.trivial.rst b/news/5661d979-1abd-4c9a-a7bf-45701b33dd6c.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/5e8c60c2-d540-4a25-af03-100d848acbc0.trivial.rst b/news/5e8c60c2-d540-4a25-af03-100d848acbc0.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/6121.feature.rst b/news/6121.feature.rst deleted file mode 100644 index 16b272a69f7..00000000000 --- a/news/6121.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Allow multiple values for --abi and --platform. diff --git a/news/629892ca-55da-4ca9-9cff-c15373e97ad1.trivial.rst b/news/629892ca-55da-4ca9-9cff-c15373e97ad1.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/6f8eff8d-9886-4e00-b431-5c809500e6bf.trivial.rst b/news/6f8eff8d-9886-4e00-b431-5c809500e6bf.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/7231.doc.rst b/news/7231.doc.rst deleted file mode 100644 index bef9bf3e6eb..00000000000 --- a/news/7231.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Add documentation for '.netrc' support. diff --git a/news/7311.doc.rst b/news/7311.doc.rst deleted file mode 100644 index 6ed2c420489..00000000000 --- a/news/7311.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Add OS tabs for OS-specific commands. diff --git a/news/8103.bugfix.rst b/news/8103.bugfix.rst deleted file mode 100644 index 55e4d6571aa..00000000000 --- a/news/8103.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Propagate ``--extra-index-url`` from requirements file properly to session auth, -so that keyring auth will work as expected. diff --git a/news/8181.removal.rst b/news/8181.removal.rst deleted file mode 100644 index ae6bbe9f88a..00000000000 --- a/news/8181.removal.rst +++ /dev/null @@ -1 +0,0 @@ -Deprecate support for Python 3.5 diff --git a/news/8327.vendor.rst b/news/8327.vendor.rst deleted file mode 100644 index 7b4536ae2fa..00000000000 --- a/news/8327.vendor.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fix devendoring instructions to explicitly state that ``vendor.txt`` should not be removed. -It is mandatory for ``pip debug`` command. diff --git a/news/8355.feature.rst b/news/8355.feature.rst deleted file mode 100644 index 3a7fb537509..00000000000 --- a/news/8355.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Add option ``--format`` to subcommand ``list`` of ``pip cache``, with ``abspath`` choice to output the full path of a wheel file. diff --git a/news/8417.removal.rst b/news/8417.removal.rst deleted file mode 100644 index 8f280b535c3..00000000000 --- a/news/8417.removal.rst +++ /dev/null @@ -1 +0,0 @@ -Document that certain removals can be fast tracked. diff --git a/news/8578.bugfix.rst b/news/8578.bugfix.rst deleted file mode 100644 index 3df7ed078cf..00000000000 --- a/news/8578.bugfix.rst +++ /dev/null @@ -1,4 +0,0 @@ -Allow specifying verbosity and quiet level via configuration files -and environment variables. Previously these options were treated as -boolean values when read from there while through CLI the level can be -specified. diff --git a/news/8636.doc.rst b/news/8636.doc.rst deleted file mode 100644 index 081cf1c7eb0..00000000000 --- a/news/8636.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Add note and example on keyring support for index basic-auth diff --git a/news/8658.bugfix.rst b/news/8658.bugfix.rst deleted file mode 100644 index 6e43c8b3c0e..00000000000 --- a/news/8658.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Only converts Windows path to unicode on Python 2 to avoid regressions when a -POSIX environment does not configure the file system encoding correctly. diff --git a/news/8676.feature.rst b/news/8676.feature.rst deleted file mode 100644 index f8da963f6ed..00000000000 --- a/news/8676.feature.rst +++ /dev/null @@ -1,2 +0,0 @@ -Improve error message friendliness when an environment has packages with -corrupted metadata. diff --git a/news/8696.bugfix.rst b/news/8696.bugfix.rst deleted file mode 100644 index 989d2d029a3..00000000000 --- a/news/8696.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -List downloaded distributions before exiting ``pip download`` -when using the new resolver to make the behavior the same as -that on the legacy resolver. diff --git a/news/8752.feature.rst b/news/8752.feature.rst deleted file mode 100644 index d2560da1803..00000000000 --- a/news/8752.feature.rst +++ /dev/null @@ -1,3 +0,0 @@ -Make the ``setup.py install`` deprecation warning less noisy. We warn only -when ``setup.py install`` succeeded and ``setup.py bdist_wheel`` failed, as -situations where both fails are most probably irrelevant to this deprecation. diff --git a/news/8781.trivial.rst b/news/8781.trivial.rst deleted file mode 100644 index e6044f52f7d..00000000000 --- a/news/8781.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -Fix a broken slug anchor in user guide. diff --git a/news/8783.doc.rst b/news/8783.doc.rst deleted file mode 100644 index 6d2bb8762d4..00000000000 --- a/news/8783.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Added initial UX feedback widgets to docs. diff --git a/news/8792.bugfix.rst b/news/8792.bugfix.rst deleted file mode 100644 index e83bdb09cfe..00000000000 --- a/news/8792.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -New resolver: Pick up hash declarations in constraints files and use them to -filter available distributions. diff --git a/news/8804.feature.rst b/news/8804.feature.rst deleted file mode 100644 index a29333342e5..00000000000 --- a/news/8804.feature.rst +++ /dev/null @@ -1,3 +0,0 @@ -Check the download directory for existing wheels to possibly avoid -fetching metadata when the ``fast-deps`` feature is used with -``pip wheel`` and ``pip download``. diff --git a/news/8807.doc.rst b/news/8807.doc.rst deleted file mode 100644 index 6ef1a123adb..00000000000 --- a/news/8807.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Add ux documentation diff --git a/news/8815.feature.rst b/news/8815.feature.rst deleted file mode 100644 index 7d9149d69c3..00000000000 --- a/news/8815.feature.rst +++ /dev/null @@ -1,2 +0,0 @@ -When installing a git URL that refers to a commit that is not available locally -after git clone, attempt to fetch it from the remote. diff --git a/news/8827.bugfix.rst b/news/8827.bugfix.rst deleted file mode 100644 index 608cd3d5c61..00000000000 --- a/news/8827.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Avoid polluting the destination directory by resolution artifacts -when the new resolver is used for ``pip download`` or ``pip wheel``. diff --git a/news/8839.bugfix.rst b/news/8839.bugfix.rst deleted file mode 100644 index 987b801e932..00000000000 --- a/news/8839.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -New resolver: If a package appears multiple times in user specification with -different ``--hash`` options, only hashes that present in all specifications -should be allowed. diff --git a/news/8848.doc.rst b/news/8848.doc.rst deleted file mode 100644 index 6d2bb8762d4..00000000000 --- a/news/8848.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Added initial UX feedback widgets to docs. diff --git a/news/8861.bugfix.rst b/news/8861.bugfix.rst deleted file mode 100644 index d623419fae5..00000000000 --- a/news/8861.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Tweak the output during dependency resolution in the new resolver. diff --git a/news/8892.feature.rst b/news/8892.feature.rst deleted file mode 100644 index 96c99bf8cbc..00000000000 --- a/news/8892.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Include http subdirectory in ``pip cache info`` and ``pip cache purge`` commands. diff --git a/news/8905.feature.rst b/news/8905.feature.rst deleted file mode 100644 index 5d27d40c2be..00000000000 --- a/news/8905.feature.rst +++ /dev/null @@ -1,3 +0,0 @@ -Cache package listings on index packages so they are guarenteed to stay stable -during a pip command session. This also improves performance when a index page -is accessed multiple times during the command session. diff --git a/news/8924.feature.rst b/news/8924.feature.rst deleted file mode 100644 index c607aa0d06b..00000000000 --- a/news/8924.feature.rst +++ /dev/null @@ -1,2 +0,0 @@ -New resolver: Tweak resolution logic to improve user experience when -user-supplied requirements conflict. diff --git a/news/8927.removal.rst b/news/8927.removal.rst deleted file mode 100644 index 0032fa5f29d..00000000000 --- a/news/8927.removal.rst +++ /dev/null @@ -1 +0,0 @@ -Document that Python versions are generally supported until PyPI usage falls below 5%. diff --git a/news/8963.bugfix.rst b/news/8963.bugfix.rst deleted file mode 100644 index 62c01b464ec..00000000000 --- a/news/8963.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Correctly search for installed distributions in new resolver logic in order -to not miss packages (virtualenv packages from system-wide-packages for example) diff --git a/news/8971.feature.rst b/news/8971.feature.rst deleted file mode 100644 index e0b7b19cf69..00000000000 --- a/news/8971.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Support Python 3.9. diff --git a/news/8975.feature.rst b/news/8975.feature.rst deleted file mode 100644 index 082612505be..00000000000 --- a/news/8975.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Log an informational message when backtracking takes multiple rounds on a specific package. diff --git a/news/8996.bugfix.rst b/news/8996.bugfix.rst deleted file mode 100644 index fa3528e7c47..00000000000 --- a/news/8996.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -Do not fail in pip freeze when encountering a ``direct_url.json`` metadata file -with editable=True. Render it as a non-editable ``file://`` URL until modern -editable installs are standardized and supported. diff --git a/news/9019.feature.rst b/news/9019.feature.rst deleted file mode 100644 index 5f81e95fd5c..00000000000 --- a/news/9019.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Switch to the new dependency resolver by default. diff --git a/news/9044.doc.rst b/news/9044.doc.rst deleted file mode 100644 index 58c7f39d20f..00000000000 --- a/news/9044.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Update user docs to reflect new resolver as default in 20.3. diff --git a/news/9049.feature.rst b/news/9049.feature.rst deleted file mode 100644 index 1cf0916f9a2..00000000000 --- a/news/9049.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Remove the ``--build-dir`` option, as per the deprecation. diff --git a/news/9056.doc.rst b/news/9056.doc.rst deleted file mode 100644 index 7317dc9cd0d..00000000000 --- a/news/9056.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Improve migration guide to reflect changes in new resolver behavior. diff --git a/news/9069.removal.rst b/news/9069.removal.rst deleted file mode 100644 index a7fae08da4b..00000000000 --- a/news/9069.removal.rst +++ /dev/null @@ -1 +0,0 @@ -Deprecate ``--find-links`` option in ``pip freeze`` diff --git a/news/946beace-6164-4d1a-a05d-e9bebf43ccd0.trivial.rst b/news/946beace-6164-4d1a-a05d-e9bebf43ccd0.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/9f8da1d9-dd18-47e9-b334-5eb862054409.trivial.rst b/news/9f8da1d9-dd18-47e9-b334-5eb862054409.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/a2fa2e68-01bf-11eb-a0b1-4fe8cb1f9dcf.trivial.rst b/news/a2fa2e68-01bf-11eb-a0b1-4fe8cb1f9dcf.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/a3a2b1b7-744e-4533-b3ff-6e7a1843d573.trivial.rst b/news/a3a2b1b7-744e-4533-b3ff-6e7a1843d573.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/bc7f9ea0-030d-11eb-92cb-6b2b625d02fc.trivial.rst b/news/bc7f9ea0-030d-11eb-92cb-6b2b625d02fc.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/c6182139-edb4-4bf6-bc3f-2d37cb5759ad.trivial.rst b/news/c6182139-edb4-4bf6-bc3f-2d37cb5759ad.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/d53425e4-767e-4d73-bce5-88644b781855.trivial.rst b/news/d53425e4-767e-4d73-bce5-88644b781855.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/d90a40c1-15b7-46b9-9162-335bb346b53f.trivial.rst b/news/d90a40c1-15b7-46b9-9162-335bb346b53f.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/e1831861-84a4-4f9b-9987-762e127cb42b.trivial.rst b/news/e1831861-84a4-4f9b-9987-762e127cb42b.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/src/pip/__init__.py b/src/pip/__init__.py index 5a2f3c31745..81b0dabd83e 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -4,7 +4,7 @@ from typing import List, Optional -__version__ = "20.3.dev0" +__version__ = "20.3b1" def main(args=None): From bfbf89d574528914c8b241941dcd24fbefb90f3f Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sat, 31 Oct 2020 23:33:50 +0530 Subject: [PATCH 2694/3170] Bump for development --- src/pip/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/__init__.py b/src/pip/__init__.py index 81b0dabd83e..cd092971361 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -4,7 +4,7 @@ from typing import List, Optional -__version__ = "20.3b1" +__version__ = "20.3.dev1" def main(args=None): From d08b4d99e217af5e19dfc6ecb54317afeefae7d8 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Mon, 2 Nov 2020 01:16:58 +0800 Subject: [PATCH 2695/3170] Use packaging.version to check version equality --- news/9083.bugfix.rst | 3 ++ .../resolution/resolvelib/candidates.py | 5 ++- tests/functional/test_new_resolver.py | 43 +++++++++++++++++++ 3 files changed, 49 insertions(+), 2 deletions(-) create mode 100644 news/9083.bugfix.rst diff --git a/news/9083.bugfix.rst b/news/9083.bugfix.rst new file mode 100644 index 00000000000..97fc552b6f3 --- /dev/null +++ b/news/9083.bugfix.rst @@ -0,0 +1,3 @@ +New resolver: Check version equality with ``packaging.version`` to avoid edge +cases if a wheel used different version normalization logic in its filename +and metadata. diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index ff2b336d9e0..65585fd36a6 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -283,9 +283,10 @@ def __init__( ) # Version may not be present for PEP 508 direct URLs if version is not None: - assert str(version) == wheel.version, ( + wheel_version = Version(wheel.version) + assert version == wheel_version, ( "{!r} != {!r} for wheel {}".format( - version, wheel.version, name + version, wheel_version, name ) ) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 0465975eecc..374d37aeea3 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -11,6 +11,7 @@ create_basic_wheel_for_package, create_test_package_with_setup, ) +from tests.lib.wheel import make_wheel def assert_installed(script, **kwargs): @@ -1089,3 +1090,45 @@ def test_new_resolver_presents_messages_when_backtracking_a_lot(script, N): assert result.stdout.count("This could take a while.") >= 2 if N >= 13: assert "press Ctrl + C" in result.stdout + + +@pytest.mark.parametrize( + "metadata_version", + [ + "0.1.0+local.1", # Normalized form. + "0.1.0+local_1", # Non-normalized form containing an underscore. + + # Non-normalized form containing a dash. This is allowed, installation + # works correctly, but assert_installed() fails because pkg_resources + # cannot handle it correctly. Nobody is complaining about it right now, + # we're probably dropping it for importlib.metadata soon(tm), so let's + # ignore it for the time being. + pytest.param("0.1.0+local-1", marks=pytest.mark.xfail), + ], + ids=["meta_dot", "meta_underscore", "meta_dash"], +) +@pytest.mark.parametrize( + "filename_version", + [ + ("0.1.0+local.1"), # Tools are encouraged to use this. + ("0.1.0+local_1"), # But this is allowed (version not normalized). + ], + ids=["file_dot", "file_underscore"], +) +def test_new_resolver_check_wheel_version_normalized( + script, + metadata_version, + filename_version, +): + filename = "simple-{}-py2.py3-none-any.whl".format(filename_version) + + wheel_builder = make_wheel(name="simple", version=metadata_version) + wheel_builder.save_to(script.scratch_path / filename) + + script.pip( + "install", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "simple" + ) + assert_installed(script, simple="0.1.0+local.1") From 69a95cf39180ce1186e17144251abf20e3b3ec9c Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 4 Nov 2020 21:18:51 +0800 Subject: [PATCH 2696/3170] Tie-break requirements with package name This makes the ordering deterministic to improve debugging and user experience. --- news/9100.feature.rst | 1 + src/pip/_internal/resolution/resolvelib/provider.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 news/9100.feature.rst diff --git a/news/9100.feature.rst b/news/9100.feature.rst new file mode 100644 index 00000000000..eb6c7283948 --- /dev/null +++ b/news/9100.feature.rst @@ -0,0 +1 @@ +The new resolver now resolves packages in a deterministic order. diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index 7f7d0e1540b..c0e6b60d90a 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -57,7 +57,8 @@ def get_preference( ): # type: (...) -> Any transitive = all(parent is not None for _, parent in information) - return (transitive, bool(candidates)) + key = next(iter(candidates)).name if candidates else "" + return (transitive, key) def find_matches(self, requirements): # type: (Sequence[Requirement]) -> Iterable[Candidate] From bf55229fc6348308989f963feeea8c90d25a1af8 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 4 Nov 2020 21:27:16 +0800 Subject: [PATCH 2697/3170] Use set to dedup package list in error message --- news/9101.bugfix.rst | 1 + src/pip/_internal/resolution/resolvelib/factory.py | 6 +++--- 2 files changed, 4 insertions(+), 3 deletions(-) create mode 100644 news/9101.bugfix.rst diff --git a/news/9101.bugfix.rst b/news/9101.bugfix.rst new file mode 100644 index 00000000000..441f2a93136 --- /dev/null +++ b/news/9101.bugfix.rst @@ -0,0 +1 @@ +New resolver: Show each requirement in the conflict error message only once to reduce cluttering. diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 8e1a68d168d..c65cb7f76f8 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -406,19 +406,19 @@ def describe_trigger(parent): # type: (Candidate) -> str ireq = parent.get_install_requirement() if not ireq or not ireq.comes_from: - return "{} {}".format(parent.name, parent.version) + return "{}=={}".format(parent.name, parent.version) if isinstance(ireq.comes_from, InstallRequirement): return str(ireq.comes_from.name) return str(ireq.comes_from) - triggers = [] + triggers = set() for req, parent in e.causes: if parent is None: # This is a root requirement, so we can report it directly trigger = req.format_for_error() else: trigger = describe_trigger(parent) - triggers.append(trigger) + triggers.add(trigger) if triggers: info = text_join(sorted(triggers)) From 3defc24e31f7abca425733ddf6d748dc574b795f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Tue, 7 Jul 2020 00:26:50 +0200 Subject: [PATCH 2698/3170] Removed unused comes_from argument of parse_requirements --- src/pip/_internal/req/req_file.py | 23 ++++------------------- tests/unit/test_req.py | 1 - 2 files changed, 4 insertions(+), 20 deletions(-) diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index c8c9165d339..0af60fa0569 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -105,7 +105,6 @@ def __init__( self, filename, # type: str lineno, # type: int - comes_from, # type: Optional[str] args, # type: str opts, # type: Values constraint, # type: bool @@ -113,7 +112,6 @@ def __init__( # type: (...) -> None self.filename = filename self.lineno = lineno - self.comes_from = comes_from self.opts = opts self.constraint = constraint @@ -134,7 +132,6 @@ def parse_requirements( filename, # type: str session, # type: PipSession finder=None, # type: Optional[PackageFinder] - comes_from=None, # type: Optional[str] options=None, # type: Optional[optparse.Values] constraint=False, # type: bool ): @@ -144,13 +141,12 @@ def parse_requirements( :param filename: Path or url of requirements file. :param session: PipSession instance. :param finder: Instance of pip.index.PackageFinder. - :param comes_from: Origin description of requirements. :param options: cli options. :param constraint: If true, parsing a constraint file rather than requirements file. """ line_parser = get_line_parser(finder) - parser = RequirementsFileParser(session, line_parser, comes_from) + parser = RequirementsFileParser(session, line_parser) for parsed_line in parser.parse(filename, constraint): parsed_req = handle_line( @@ -333,12 +329,10 @@ def __init__( self, session, # type: PipSession line_parser, # type: LineParser - comes_from, # type: Optional[str] ): # type: (...) -> None self._session = session self._line_parser = line_parser - self._comes_from = comes_from def parse(self, filename, constraint): # type: (str, bool) -> Iterator[ParsedLine] @@ -382,9 +376,7 @@ def _parse_and_recurse(self, filename, constraint): def _parse_file(self, filename, constraint): # type: (str, bool) -> Iterator[ParsedLine] - _, content = get_file_content( - filename, self._session, comes_from=self._comes_from - ) + _, content = get_file_content(filename, self._session) lines_enum = preprocess(content) @@ -399,7 +391,6 @@ def _parse_file(self, filename, constraint): yield ParsedLine( filename, line_number, - self._comes_from, args_str, opts, constraint, @@ -553,15 +544,14 @@ def expand_env_variables(lines_enum): yield line_number, line -def get_file_content(url, session, comes_from=None): - # type: (str, PipSession, Optional[str]) -> Tuple[str, Text] +def get_file_content(url, session): + # type: (str, PipSession) -> Tuple[str, Text] """Gets the content of a file; it may be a filename, file: URL, or http: URL. Returns (location, content). Content is unicode. Respects # -*- coding: declarations on the retrieved files. :param url: File path or url. :param session: PipSession instance. - :param comes_from: Origin description of requirements. """ scheme = get_url_scheme(url) @@ -572,11 +562,6 @@ def get_file_content(url, session, comes_from=None): return resp.url, resp.text elif scheme == 'file': - if comes_from and comes_from.startswith('http'): - raise InstallationError( - 'Requirements file {} references URL {}, ' - 'which is local'.format(comes_from, url) - ) url = url_to_path(url) try: diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index 083d2c2c60d..e168a3cc164 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -47,7 +47,6 @@ def get_processed_req_from_line(line, fname='file', lineno=1): parsed_line = ParsedLine( fname, lineno, - fname, args_str, opts, False, From 5ef05cfb1fb64e9da61be2950ce091bc5c43f0f8 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 10 Nov 2020 14:58:38 +0800 Subject: [PATCH 2699/3170] Make sure periods are followed by a space The strings are *delibrately* reformatted so line continuations occur in the middle of sentences. This helps me ensure all spaces are added properly. --- .../resolution/resolvelib/reporter.py | 22 +++++++++---------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/reporter.py b/src/pip/_internal/resolution/resolvelib/reporter.py index 07ce399acc6..376265c2d03 100644 --- a/src/pip/_internal/resolution/resolvelib/reporter.py +++ b/src/pip/_internal/resolution/resolvelib/reporter.py @@ -22,21 +22,21 @@ def __init__(self): self._messages_at_backtrack = { 1: ( - "pip is looking at multiple versions of this package to determine " - "which version is compatible with other requirements. " - "This could take a while." + "pip is looking at multiple versions of this package to " + "determine which version is compatible with other " + "requirements. This could take a while." ), 8: ( - "pip is looking at multiple versions of this package to determine " - "which version is compatible with other requirements. " - "This could take a while." + "pip is looking at multiple versions of this package to " + "determine which version is compatible with other " + "requirements. This could take a while." ), 13: ( - "This is taking longer than usual. You might need to provide the " - "dependency resolver with stricter constraints to reduce runtime." - "If you want to abort this run, you can press Ctrl + C to do so." - "To improve how pip performs, tell us what happened here: " - "https://pip.pypa.io/surveys/backtracking" + "This is taking longer than usual. You might need to provide " + "the dependency resolver with stricter constraints to reduce " + "runtime. If you want to abort this run, you can press " + "Ctrl + C to do so. To improve how pip performs, tell us what " + "happened here: https://pip.pypa.io/surveys/backtracking" ) } From 8383249442660cfc9609b69acbc2db2351cf3a45 Mon Sep 17 00:00:00 2001 From: Bernard <bernard@ei8fdb.org> Date: Wed, 11 Nov 2020 11:20:29 +0100 Subject: [PATCH 2700/3170] Incorporating commit: e6acca646abcaaac7515da9b964d5b5291264142 --- docs/html/user_guide.rst | 19 ++++++++----------- 1 file changed, 8 insertions(+), 11 deletions(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index bd6e2baced1..7401720ba64 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -1337,7 +1337,7 @@ candidate can take a lot of time. (The amount of time depends on the package size, the number of versions pip must try, and other concerns.) How does backtracking work? -~~~~~~~~~~~~~~~~~~~~~~~~~~~ +^^^^^^^^^^^^^^^^^^^^^^^^^^^ When doing a pip install, pip starts by making assumptions about the packages it needs to install. During the install process it needs to check these @@ -1384,8 +1384,6 @@ version of a package is a good candidate to install. It reduces the risk that installing a new package will accidentally break an existing installed package, and so reduces the risk that your environment gets messed up. -Please address this. - What does this behaviour look like? ----------------------------------- @@ -1439,14 +1437,13 @@ Right now backtracking behaviour looks like this: Downloading cup-3.13.0-py2.py3-none-any.whl (374 kB) In the above sample output, pip had to download multiple versions of -package cup - cup-3.22.0 to cup-3.13.0 - to find a version that will be -compatible with the other packages - ``spoon``, ``hot-water``, ``cup`` -etc. +package ``cup`` - cup-3.22.0 to cup-3.13.0 - to find a version that will be +compatible with the other packages - ``spoon``, ``hot-water``, etc. These multiple ``Downloading cup-version`` lines show pip backtracking. Possible ways to reduce backtracking occurring ---------------------------------------------- +---------------------------------------------- It's important to mention backtracking behaviour is expected during a ``pip install`` process. What pip is trying to do is complicated - it is @@ -1459,7 +1456,7 @@ here are a number of ways. .. _1-allow-pip-to-complete-its-backtracking: 1. Allow pip to complete its backtracking -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ In most cases, pip will complete the backtracking process successfully. It is possible this could take a very long time to complete - this may @@ -1474,7 +1471,7 @@ If you'd prefer not to wait, you can interrupt pip (ctrl and c) and use .. _2-reduce-the-versions-of-the-backtracking-package: 2. Reduce the number of versions pip will try to backtrack through -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ If pip is backtracking more than you'd like, the next option is to constrain the number of package versions it tries. @@ -1496,7 +1493,7 @@ can be trial and error. .. _3-use-constraint-files-or-lockfiles: 3. Use constraint files or lockfiles -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ This option is a progression of 2 above. It requires users to know how to inspect: @@ -1518,7 +1515,7 @@ suitable constraints file. .. _4-be-more-strict-on-package-dependencies-during-development: 4. Be more strict on package dependencies during development -~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ For package maintainers during the development, give pip some help by creating constraint files for the dependency tree. This will reduce the From 90925a71574707749e8743a5001d658fd0237e23 Mon Sep 17 00:00:00 2001 From: Bernard <bernard@ei8fdb.org> Date: Wed, 11 Nov 2020 12:59:37 +0100 Subject: [PATCH 2701/3170] =?UTF-8?q?Fixed=20linting=20failures.=20?= =?UTF-8?q?=F0=9F=A4=9E?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- docs/html/user_guide.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 7401720ba64..a6ea8f31460 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -1525,7 +1525,7 @@ Getting help ------------ If none of the suggestions above work for you, we recommend that you ask -for help and you've got `a number of options :ref:`Getting help`. +for help. :ref:`Getting help`. .. _`Using pip from your program`: From 7be91574d9c65d7aaaf1395a98706744e7936dda Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Thu, 12 Nov 2020 06:53:29 +0530 Subject: [PATCH 2702/3170] Update message displayed on conflicts post-resolution --- src/pip/_internal/commands/install.py | 17 ++++++++++------- tests/functional/test_install_check.py | 2 +- 2 files changed, 11 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 4c16b9caedd..38f9f063dea 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -529,14 +529,16 @@ def _warn_about_conflicts(self, conflict_details, resolver_variant): parts = [] # type: List[str] if resolver_variant == "legacy": parts.append( - "After October 2020 you may experience errors when installing " - "or updating packages. This is because pip will change the " - "way that it resolves dependency conflicts.\n" + "pip's legacy dependency resolver does not consider dependency " + "conflicts when selecting packages. This behaviour is the " + "source of the following dependency conflicts." ) + else: + assert resolver_variant == "2020-resolver" parts.append( - "We recommend you use --use-feature=2020-resolver to test " - "your packages with the new resolver before it becomes the " - "default.\n" + "pip's dependency resolver does not currently take into account " + "all the packages that are installed. This behaviour is the " + "source of the following dependency conflicts." ) # NOTE: There is some duplication here, with commands/check.py @@ -557,7 +559,7 @@ def _warn_about_conflicts(self, conflict_details, resolver_variant): version = package_set[project_name][0] for dep_name, dep_version, req in conflicting[project_name]: message = ( - "{name} {version} requires {requirement}, but you'll have " + "{name} {version} requires {requirement}, but {you} have " "{dep_name} {dep_version} which is incompatible." ).format( name=project_name, @@ -565,6 +567,7 @@ def _warn_about_conflicts(self, conflict_details, resolver_variant): requirement=req, dep_name=dep_name, dep_version=dep_version, + you=("you" if resolver_variant == "2020-resolver" else "you'll") ) parts.append(message) diff --git a/tests/functional/test_install_check.py b/tests/functional/test_install_check.py index a173cb5504f..56ac7daf65a 100644 --- a/tests/functional/test_install_check.py +++ b/tests/functional/test_install_check.py @@ -89,7 +89,7 @@ def test_check_install_does_not_warn_for_out_of_graph_issues(script): assert_contains_expected_lines(result.stderr, [ "broken 1.0 requires missing, which is not installed.", "broken 1.0 requires conflict<1.0, " - "but you'll have conflict 1.0 which is incompatible." + "but you have conflict 1.0 which is incompatible." ]) # Install unrelated package From 5bfc0259f9a4c3d5748f7ca514f3b9c60e34246e Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Mon, 19 Oct 2020 19:47:22 +0530 Subject: [PATCH 2703/3170] Clarify which version of pip we're seeing --- docs/html/conf.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/docs/html/conf.py b/docs/html/conf.py index adee6fd3c49..68951b2c404 100644 --- a/docs/html/conf.py +++ b/docs/html/conf.py @@ -94,7 +94,8 @@ # We have this here because readthedocs plays tricks sometimes and there seems # to be a heisenbug, related to the version of pip discovered. This is here to # help debug that if someone decides to do that in the future. -print(version) +print("pip version:", version) +print("pip release:", release) # The language for content autogenerated by Sphinx. Refer to documentation # for a list of supported languages. From 19717d170163c8f564d0945beb30776a114e31b9 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Mon, 19 Oct 2020 20:06:04 +0530 Subject: [PATCH 2704/3170] Switch documentation theme to Furo --- docs/html/conf.py | 25 ++++++------------------- tools/requirements/docs.txt | 3 +-- 2 files changed, 7 insertions(+), 21 deletions(-) diff --git a/docs/html/conf.py b/docs/html/conf.py index 68951b2c404..7983187e9a3 100644 --- a/docs/html/conf.py +++ b/docs/html/conf.py @@ -128,9 +128,6 @@ # output. They are ignored by default. # show_authors = False -# The name of the Pygments (syntax highlighting) style to use. -pygments_style = 'sphinx' - # A list of ignored prefixes for module index sorting. # modindex_common_prefix = [] @@ -147,25 +144,18 @@ # The theme to use for HTML and HTML Help pages. Major themes that come with # Sphinx are currently 'default' and 'sphinxdoc'. -html_theme = "pypa_theme" +html_theme = "furo" # Theme options are theme-specific and customize the look and feel of a theme # further. For a list of options available for each theme, see the # documentation. -html_theme_options = { - 'collapsiblesidebar': True, - 'externalrefs': True, - 'navigation_depth': 3, - 'issues_url': 'https://github.com/pypa/pip/issues', - 'codebgcolor': '#eeffcc', - 'codetextcolor': '#333333', -} +html_theme_options = {} # Add any paths that contain custom themes here, relative to this directory. # The name for this set of Sphinx documents. If None, it defaults to # "<project> v<release> documentation". -# html_title = None +html_title = f"{project} documentation v{release}" # A shorter title for the navigation bar. Default is the same as html_title. # html_short_title = None @@ -182,7 +172,7 @@ # Add any paths that contain custom static files (such as style sheets) here, # relative to this directory. They are copied after the builtin static files, # so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = ['_static'] +html_static_path = [] # If not '', a 'Last updated on:' timestamp is inserted at every page bottom, # using the given strftime format. @@ -205,10 +195,7 @@ smartquotes_action = "qe" # Custom sidebar templates, maps document names to template names. -html_sidebars = { - '**': ['localtoc.html', 'relations.html'], - 'index': ['localtoc.html'] -} +html_sidebars = {} # Additional templates that should be rendered to pages, maps page names to # template names. @@ -322,7 +309,7 @@ def to_document_name(path, base_dir): docs_feedback_big_doc_lines = 50 # bigger docs will have a banner on top docs_feedback_email = 'Docs UX Team <docs-feedback+ux/pip.pypa.io@pypa.io>' docs_feedback_excluded_documents = { # these won't have any banners - 'news', + 'news', 'reference/index', } docs_feedback_questions_list = ( 'What problem were you trying to solve when you came to this page?', diff --git a/tools/requirements/docs.txt b/tools/requirements/docs.txt index 77a940c08a8..0c5103d0a2a 100644 --- a/tools/requirements/docs.txt +++ b/tools/requirements/docs.txt @@ -1,6 +1,5 @@ sphinx == 3.2.1 -git+https://github.com/python/python-docs-theme.git#egg=python-docs-theme -git+https://github.com/pypa/pypa-docs-theme.git#egg=pypa-docs-theme +furo sphinx-inline-tabs # `docs.pipext` uses pip's internals to generate documentation. So, we install From f3ee03d6d0a8551bc65d4a0436dc31b23472085d Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Mon, 19 Oct 2020 20:06:17 +0530 Subject: [PATCH 2705/3170] Drop custom styling for admonitions --- docs/docs_feedback_sphinxext.py | 1 - docs/html/_static/important-admonition.css | 8 -------- 2 files changed, 9 deletions(-) delete mode 100644 docs/html/_static/important-admonition.css diff --git a/docs/docs_feedback_sphinxext.py b/docs/docs_feedback_sphinxext.py index 90f2ddd7498..86eb3d61a7c 100644 --- a/docs/docs_feedback_sphinxext.py +++ b/docs/docs_feedback_sphinxext.py @@ -155,7 +155,6 @@ def setup(app: Sphinx) -> Dict[str, Union[bool, str]]: rebuild=rebuild_trigger, ) - app.add_css_file('important-admonition.css') app.connect('source-read', _modify_rst_document_source_on_read) return { diff --git a/docs/html/_static/important-admonition.css b/docs/html/_static/important-admonition.css deleted file mode 100644 index a73ae2e4d4c..00000000000 --- a/docs/html/_static/important-admonition.css +++ /dev/null @@ -1,8 +0,0 @@ -.admonition.important { - background-color: rgb(219, 250, 244); - border: 1px solid rgb(26, 188, 156); -} - -.admonition.important>.admonition-title { - color: rgb(26, 188, 156); -} From 6962284236bc6e2ffc3256f5f70876b70d4d353b Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Mon, 19 Oct 2020 20:07:12 +0530 Subject: [PATCH 2706/3170] Remove inline table of contents Furo provides a fully-fleshed-out right sidebar that provides an in-page table of contents, that's scrollable and does not interrupt content. --- docs/html/development/architecture/overview.rst | 1 - docs/html/reference/pip.rst | 1 - docs/html/reference/pip_cache.rst | 1 - docs/html/reference/pip_check.rst | 2 -- docs/html/reference/pip_config.rst | 2 -- docs/html/reference/pip_debug.rst | 2 -- docs/html/reference/pip_download.rst | 2 -- docs/html/reference/pip_freeze.rst | 2 -- docs/html/reference/pip_hash.rst | 2 -- docs/html/reference/pip_install.rst | 1 - docs/html/reference/pip_list.rst | 1 - docs/html/reference/pip_search.rst | 1 - docs/html/reference/pip_show.rst | 1 - docs/html/reference/pip_uninstall.rst | 1 - docs/html/reference/pip_wheel.rst | 1 - docs/html/user_guide.rst | 2 -- 16 files changed, 23 deletions(-) diff --git a/docs/html/development/architecture/overview.rst b/docs/html/development/architecture/overview.rst index 637a22f2f31..f9bcfb8731e 100644 --- a/docs/html/development/architecture/overview.rst +++ b/docs/html/development/architecture/overview.rst @@ -4,7 +4,6 @@ developers welcome your help to complete this documentation. If you're interested in helping out, please let us know in the `tracking issue`_. -.. contents:: **************************** Broad functionality overview diff --git a/docs/html/reference/pip.rst b/docs/html/reference/pip.rst index 298a1101d0e..1f52630f69f 100644 --- a/docs/html/reference/pip.rst +++ b/docs/html/reference/pip.rst @@ -2,7 +2,6 @@ pip === -.. contents:: Usage ***** diff --git a/docs/html/reference/pip_cache.rst b/docs/html/reference/pip_cache.rst index c443a6f3a75..0a23c510d6f 100644 --- a/docs/html/reference/pip_cache.rst +++ b/docs/html/reference/pip_cache.rst @@ -4,7 +4,6 @@ pip cache --------- -.. contents:: Usage ***** diff --git a/docs/html/reference/pip_check.rst b/docs/html/reference/pip_check.rst index 3b2ecb511b1..268cf9a143c 100644 --- a/docs/html/reference/pip_check.rst +++ b/docs/html/reference/pip_check.rst @@ -4,8 +4,6 @@ pip check ========= -.. contents:: - Usage ===== diff --git a/docs/html/reference/pip_config.rst b/docs/html/reference/pip_config.rst index 14b1ac775db..8b2f846304f 100644 --- a/docs/html/reference/pip_config.rst +++ b/docs/html/reference/pip_config.rst @@ -5,8 +5,6 @@ pip config ========== -.. contents:: - Usage ===== diff --git a/docs/html/reference/pip_debug.rst b/docs/html/reference/pip_debug.rst index a077a169b65..4023533c905 100644 --- a/docs/html/reference/pip_debug.rst +++ b/docs/html/reference/pip_debug.rst @@ -4,8 +4,6 @@ pip debug ========= -.. contents:: - Usage ===== diff --git a/docs/html/reference/pip_download.rst b/docs/html/reference/pip_download.rst index b600d15e560..4f15314d765 100644 --- a/docs/html/reference/pip_download.rst +++ b/docs/html/reference/pip_download.rst @@ -5,8 +5,6 @@ pip download ============ -.. contents:: - Usage ===== diff --git a/docs/html/reference/pip_freeze.rst b/docs/html/reference/pip_freeze.rst index 152823a080b..352f7d32168 100644 --- a/docs/html/reference/pip_freeze.rst +++ b/docs/html/reference/pip_freeze.rst @@ -5,8 +5,6 @@ pip freeze ========== -.. contents:: - Usage ===== diff --git a/docs/html/reference/pip_hash.rst b/docs/html/reference/pip_hash.rst index e9f5964dddd..7df0d5a4f13 100644 --- a/docs/html/reference/pip_hash.rst +++ b/docs/html/reference/pip_hash.rst @@ -4,8 +4,6 @@ pip hash ======== -.. contents:: - Usage ===== diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index be974dacf61..1a5507fdc0d 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -4,7 +4,6 @@ pip install =========== -.. contents:: Usage diff --git a/docs/html/reference/pip_list.rst b/docs/html/reference/pip_list.rst index bda322a8631..5119a804c0d 100644 --- a/docs/html/reference/pip_list.rst +++ b/docs/html/reference/pip_list.rst @@ -4,7 +4,6 @@ pip list ======== -.. contents:: Usage diff --git a/docs/html/reference/pip_search.rst b/docs/html/reference/pip_search.rst index 2d1a2aa69ed..9905a1bafac 100644 --- a/docs/html/reference/pip_search.rst +++ b/docs/html/reference/pip_search.rst @@ -4,7 +4,6 @@ pip search ========== -.. contents:: Usage diff --git a/docs/html/reference/pip_show.rst b/docs/html/reference/pip_show.rst index bcbe4e82067..b603f786fd9 100644 --- a/docs/html/reference/pip_show.rst +++ b/docs/html/reference/pip_show.rst @@ -4,7 +4,6 @@ pip show ======== -.. contents:: Usage diff --git a/docs/html/reference/pip_uninstall.rst b/docs/html/reference/pip_uninstall.rst index fbbeddd45c8..f1c69d09c3a 100644 --- a/docs/html/reference/pip_uninstall.rst +++ b/docs/html/reference/pip_uninstall.rst @@ -4,7 +4,6 @@ pip uninstall ============= -.. contents:: Usage diff --git a/docs/html/reference/pip_wheel.rst b/docs/html/reference/pip_wheel.rst index f6430bfed52..c2a9543fc99 100644 --- a/docs/html/reference/pip_wheel.rst +++ b/docs/html/reference/pip_wheel.rst @@ -5,7 +5,6 @@ pip wheel ========= -.. contents:: Usage diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 677a8a063e7..55e0c926834 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -2,8 +2,6 @@ User Guide ========== -.. contents:: - Running pip =========== From 321163fb98e2733195a8e44c86e9466b989f5008 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Thu, 12 Nov 2020 07:29:03 +0530 Subject: [PATCH 2707/3170] Fix misplaced blockquotes Signed-off-by: Pradyun Gedam <pradyunsg@users.noreply.github.com> --- .../architecture/upgrade-options.rst | 24 +++++++-------- docs/html/development/ci.rst | 30 +++++++++---------- docs/html/development/getting-started.rst | 6 ++-- docs/html/development/issue-triage.rst | 26 ++++++++-------- docs/pip_sphinxext.py | 2 +- 5 files changed, 44 insertions(+), 44 deletions(-) diff --git a/docs/html/development/architecture/upgrade-options.rst b/docs/html/development/architecture/upgrade-options.rst index 0527101f8c6..6196413ef93 100644 --- a/docs/html/development/architecture/upgrade-options.rst +++ b/docs/html/development/architecture/upgrade-options.rst @@ -34,18 +34,18 @@ relevant if ``--upgrade`` is specified. The base behaviour is to allow packages specified on pip's command line to be upgraded. This option controls what *other* packages can be upgraded: - * ``eager`` - all packages will be upgraded to the latest possible version. - It should be noted here that pip's current resolution algorithm isn't even - aware of packages other than those specified on the command line, and - those identified as dependencies. This may or may not be true of the new - resolver. - * ``only-if-needed`` - packages are only upgraded if they are named in the - pip command or a requirement file (i.e, they are direct requirements), or - an upgraded parent needs a later version of the dependency than is - currently installed. - * ``to-satisfy-only`` (**undocumented**) - packages are not upgraded (not - even direct requirements) unless the currently installed version fails to - satisfy a requirement (either explicitly specified or a dependency). +* ``eager`` - all packages will be upgraded to the latest possible version. + It should be noted here that pip's current resolution algorithm isn't even + aware of packages other than those specified on the command line, and + those identified as dependencies. This may or may not be true of the new + resolver. +* ``only-if-needed`` - packages are only upgraded if they are named in the + pip command or a requirement file (i.e, they are direct requirements), or + an upgraded parent needs a later version of the dependency than is + currently installed. +* ``to-satisfy-only`` (**undocumented**) - packages are not upgraded (not + even direct requirements) unless the currently installed version fails to + satisfy a requirement (either explicitly specified or a dependency). ``--force-reinstall`` diff --git a/docs/html/development/ci.rst b/docs/html/development/ci.rst index 7f214a3b84c..5befb316a4d 100644 --- a/docs/html/development/ci.rst +++ b/docs/html/development/ci.rst @@ -17,24 +17,24 @@ Supported interpreters pip support a variety of Python interpreters: - - CPython 2.7 - - CPython 3.5 - - CPython 3.6 - - CPython 3.7 - - CPython 3.8 - - Latest PyPy - - Latest PyPy3 +- CPython 2.7 +- CPython 3.5 +- CPython 3.6 +- CPython 3.7 +- CPython 3.8 +- Latest PyPy +- Latest PyPy3 on different operating systems: - - Linux - - Windows - - MacOS +- Linux +- Windows +- MacOS and on different architectures: - - x64 - - x86 +- x64 +- x86 so 42 hypothetical interpreters. @@ -66,9 +66,9 @@ Services pip test suite and checks are distributed on three different platforms that provides free executors for open source packages: - - `GitHub Actions`_ (Used for code quality and development tasks) - - `Azure DevOps CI`_ (Used for tests) - - `Travis CI`_ (Used for PyPy tests) +- `GitHub Actions`_ (Used for code quality and development tasks) +- `Azure DevOps CI`_ (Used for tests) +- `Travis CI`_ (Used for PyPy tests) .. _`Travis CI`: https://travis-ci.org/ .. _`Azure DevOps CI`: https://azure.microsoft.com/en-us/services/devops/ diff --git a/docs/html/development/getting-started.rst b/docs/html/development/getting-started.rst index 436ed241baf..94ff37fc32c 100644 --- a/docs/html/development/getting-started.rst +++ b/docs/html/development/getting-started.rst @@ -157,9 +157,9 @@ in order to start contributing. * A guide on `triaging issues`_ for issue tracker * Getting started with Git - - `Hello World for Git`_ - - `Understanding the GitHub flow`_ - - `Start using Git on the command line`_ + - `Hello World for Git`_ + - `Understanding the GitHub flow`_ + - `Start using Git on the command line`_ .. _`open an issue`: https://github.com/pypa/pip/issues/new?title=Trouble+with+pip+development+environment diff --git a/docs/html/development/issue-triage.rst b/docs/html/development/issue-triage.rst index a887bda62a8..9b5e5cc1c3e 100644 --- a/docs/html/development/issue-triage.rst +++ b/docs/html/development/issue-triage.rst @@ -276,16 +276,16 @@ An issue may be considered resolved and closed when: - for each possible improvement or problem represented in the issue discussion: - - Consensus has been reached on a specific action and the actions - appear to be external to the project, with no follow up needed - in the project afterwards. + - Consensus has been reached on a specific action and the actions + appear to be external to the project, with no follow up needed + in the project afterwards. - - PEP updates (with a corresponding issue in - `python/peps <https://github.com/python/peps>`__) - - already tracked by another issue + - PEP updates (with a corresponding issue in + `python/peps <https://github.com/python/peps>`__) + - already tracked by another issue - - A project-specific issue has been identified and the issue no - longer occurs as of the latest commit on the master branch. + - A project-specific issue has been identified and the issue no + longer occurs as of the latest commit on the master branch. - An enhancement or feature request no longer has a proponent and the maintainers don't think it's worth keeping open. @@ -305,8 +305,8 @@ Common issues manager-managed pip/python installation (specifically with Debian/Ubuntu). These typically present themselves as: - #. Not being able to find installed packages - #. basic libraries not able to be found, fundamental OS components missing - #. In these situations you will want to make sure that we know how they got - their Python and pip. Knowing the relevant package manager commands can - help, e.g. ``dpkg -S``. + #. Not being able to find installed packages + #. basic libraries not able to be found, fundamental OS components missing + #. In these situations you will want to make sure that we know how they got + their Python and pip. Knowing the relevant package manager commands can + help, e.g. ``dpkg -S``. diff --git a/docs/pip_sphinxext.py b/docs/pip_sphinxext.py index 9386d71e796..2486d5c33b9 100644 --- a/docs/pip_sphinxext.py +++ b/docs/pip_sphinxext.py @@ -143,7 +143,7 @@ def process_options(self): prefix = '{}_'.format(self.determine_opt_prefix(opt_name)) self.view_list.append( - ' * :ref:`{short}{long}<{prefix}{opt_name}>`'.format( + '* :ref:`{short}{long}<{prefix}{opt_name}>`'.format( short=short_opt_name, long=opt_name, prefix=prefix, From 9613c887f1dc6cb80080bc2cd6a8334dab417846 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Thu, 12 Nov 2020 12:59:42 +0100 Subject: [PATCH 2708/3170] Test download editable --- tests/functional/test_download.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/functional/test_download.py b/tests/functional/test_download.py index 2eee51b086a..6ee02d81744 100644 --- a/tests/functional/test_download.py +++ b/tests/functional/test_download.py @@ -849,3 +849,19 @@ def test_download_http_url_bad_hash( assert len(requests) == 1 assert requests[0]['PATH_INFO'] == '/simple-1.0.tar.gz' assert requests[0]['HTTP_ACCEPT_ENCODING'] == 'identity' + + +def test_download_editable(script, data, tmpdir): + """ + Test 'pip download' of editables in requirement file. + """ + editable_path = os.path.join(data.src, 'simplewheel-1.0') + requirements_path = tmpdir / "requirements.txt" + requirements_path.write_text("-e " + str(editable_path.resolve()) + "\n") + download_dir = tmpdir / "download_dir" + script.pip( + 'download', '--no-deps', '-r', str(requirements_path), '-d', str(download_dir) + ) + downloads = os.listdir(download_dir) + assert len(downloads) == 1 + assert downloads[0].endswith(".zip") From 11d07016d98502710075338d992a6e59ed553a13 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Thu, 12 Nov 2020 12:34:37 +0100 Subject: [PATCH 2709/3170] Add failing test for issue 9122 --- tests/functional/test_wheel.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/tests/functional/test_wheel.py b/tests/functional/test_wheel.py index f75b009beb7..286d694356e 100644 --- a/tests/functional/test_wheel.py +++ b/tests/functional/test_wheel.py @@ -169,6 +169,22 @@ def test_pip_wheel_builds_editable(script, data): result.did_create(wheel_file_path) +def test_pip_wheel_builds_editable_does_not_create_zip(script, data, tmpdir): + """ + Test 'pip wheel' of editables does not create zip files + (regression test for issue #9122) + """ + wheel_dir = tmpdir / "wheel_dir" + wheel_dir.mkdir() + editable_path = os.path.join(data.src, 'simplewheel-1.0') + script.pip( + 'wheel', '--no-deps', '-e', editable_path, '-w', wheel_dir + ) + wheels = os.listdir(wheel_dir) + assert len(wheels) == 1 + assert wheels[0].endswith(".whl") + + def test_pip_wheel_fail(script, data): """ Test 'pip wheel' failure. From 657d91f6725336718a42b6661d728f54afaa1498 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sat, 14 Nov 2020 03:56:01 +0800 Subject: [PATCH 2710/3170] Reorder and revise installation docs An additional section is added to instruct the user to self-check whether pip is installed before proceeding. The Linux package manager section is moved to the front to encourage users to try that first before learning about get-pip.py, which tends to provide seemingly working but subtly broken results there. A paragraph is added to the section to instruct users to report issues to package managers instead. A new section on enturepip is added as an alternative to get-pip.py, which is almost guarenteed to be available at this point. --- docs/html/installing.rst | 76 +++++++++++++++++++++++++++++++--------- news/9131.doc.rst | 1 + 2 files changed, 60 insertions(+), 17 deletions(-) create mode 100644 news/9131.doc.rst diff --git a/docs/html/installing.rst b/docs/html/installing.rst index 00c74a70abf..a49aebea4a3 100644 --- a/docs/html/installing.rst +++ b/docs/html/installing.rst @@ -10,8 +10,58 @@ Do I need to install pip? pip is already installed if you are using Python 2 >=2.7.9 or Python 3 >=3.4 downloaded from `python.org <https://www.python.org>`_ or if you are working in a :ref:`Virtual Environment <pypug:Creating and using Virtual Environments>` -created by :ref:`pypug:virtualenv` or :ref:`pyvenv <pypug:venv>`. -Just make sure to :ref:`upgrade pip <Upgrading pip>`. +created by :ref:`pypug:virtualenv` or :ref:`venv <pypug:venv>`. Just make sure +to :ref:`upgrade pip <Upgrading pip>`. + +Use the following command to check whether pip is installed: + +.. tab:: Unix/macOS + + .. code-block:: console + + $ python -m pip --version + pip X.Y.Z from .../site-packages/pip (python X.Y) + +.. tab:: Windows + + .. code-block:: console + + C:\> py -m pip --version + pip X.Y.Z from ...\site-packages\pip (python X.Y) + +Using Linux Package Managers +============================ + +.. warning:: + + If you installed Python from a package manager on Linux, you should always + install pip for that Python installation using the same source. + +See `pypug:Installing pip/setuptools/wheel with Linux Package Managers <https://packaging.python.org/guides/installing-using-linux-tools/>`_ +in the Python Packaging User Guide. + +Here are ways to contact a few Linux package maintainers if you run into +problems: + +* `Deadsnakes PPA <https://github.com/deadsnakes/issues>`_ +* `Debian Python Team <https://wiki.debian.org/Teams/PythonTeam>`_ (for general + issues related to ``apt``) +* `Red Hat Bugzilla <https://bugzilla.redhat.com/>`_ + +pip developers do not have control over how Linux distributions handle pip +installations, and are unable to provide solutions to related issues in +general. + +Using ensurepip +=============== + +Python >=3.4 can self-bootstrap pip with the built-in +:ref:`ensurepip <pypug:ensurepip>` module. Refer to the standard library +documentation for more details. Make sure to :ref:`upgrade pip <Upgrading pip>` +after ``ensurepip`` installs pip. + +See the `Using Linux Package Managers`_ section if your Python reports +``No module named ensurepip`` on Debian and derived systems (e.g. Ubuntu). .. _`get-pip`: @@ -19,7 +69,13 @@ Just make sure to :ref:`upgrade pip <Upgrading pip>`. Installing with get-pip.py ========================== -To install pip, securely [1]_ download ``get-pip.py`` by following +.. warning:: + + Be cautious if you are using a Python install that is managed by your operating + system or another package manager. ``get-pip.py`` does not coordinate with + those tools, and may leave your system in an inconsistent state. + +To manually install pip, securely [1]_ download ``get-pip.py`` by following this link: `get-pip.py <https://bootstrap.pypa.io/get-pip.py>`_. Alternatively, use ``curl``:: @@ -40,13 +96,6 @@ have downloaded ``get-pip.py``: py get-pip.py - -.. warning:: - - Be cautious if you are using a Python install that is managed by your operating - system or another package manager. ``get-pip.py`` does not coordinate with - those tools, and may leave your system in an inconsistent state. - ``get-pip.py`` also installs :ref:`pypug:setuptools` [2]_ and :ref:`pypug:wheel` if they are not already. :ref:`pypug:setuptools` is required to install :term:`source distributions <pypug:Source Distribution (or "sdist")>`. Both are @@ -134,13 +183,6 @@ Install behind a proxy: py get-pip.py pip==9.0.2 wheel==0.30.0 setuptools==28.8.0 -Using Linux Package Managers -============================ - -See :ref:`pypug:Installing pip/setuptools/wheel with Linux Package Managers` in -the `Python Packaging User Guide -<https://packaging.python.org/guides/tool-recommendations/>`_. - .. _`Upgrading pip`: Upgrading pip diff --git a/news/9131.doc.rst b/news/9131.doc.rst new file mode 100644 index 00000000000..18862aa554a --- /dev/null +++ b/news/9131.doc.rst @@ -0,0 +1 @@ +Reorder and revise installation instructions to make them easier to follow. From a29dda6d2b1d0b1c5566438a2f0377ca11ee9442 Mon Sep 17 00:00:00 2001 From: Noah Gorny <noah@gittabags.com> Date: Sat, 14 Nov 2020 12:03:53 +0200 Subject: [PATCH 2711/3170] cli: Drop --unstable-feature flag --- src/pip/_internal/cli/base_command.py | 7 ------- src/pip/_internal/cli/cmdoptions.py | 12 ------------ 2 files changed, 19 deletions(-) diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index 86f1733a57c..c1522d6391b 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -199,13 +199,6 @@ def _main(self, args): ) options.cache_dir = None - if 'resolver' in options.unstable_features: - logger.critical( - "--unstable-feature=resolver is no longer supported, and " - "has been replaced with --use-feature=2020-resolver instead." - ) - sys.exit(ERROR) - if '2020-resolver' in options.features_enabled and not PY2: logger.warning( "--use-feature=2020-resolver no longer has any effect, " diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index 6a6634fb8b8..07d612a6f54 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -895,17 +895,6 @@ def check_list_path_option(options): ) # type: Callable[..., Option] -unstable_feature = partial( - Option, - '--unstable-feature', - dest='unstable_features', - metavar='feature', - action='append', - default=[], - choices=['resolver'], - help=SUPPRESS_HELP, # TODO: drop this in pip 20.3 -) # type: Callable[..., Option] - use_new_feature = partial( Option, '--use-feature', @@ -958,7 +947,6 @@ def check_list_path_option(options): disable_pip_version_check, no_color, no_python_version_warning, - unstable_feature, use_new_feature, use_deprecated_feature, ] From 62868dca5faad557bd6f822b7c11faf3bc989140 Mon Sep 17 00:00:00 2001 From: Noah Gorny <noah@gittabags.com> Date: Sat, 14 Nov 2020 12:08:17 +0200 Subject: [PATCH 2712/3170] news: Add --unstable-feature removal news fragment --- news/9133.removal.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/9133.removal.rst diff --git a/news/9133.removal.rst b/news/9133.removal.rst new file mode 100644 index 00000000000..876e89b13eb --- /dev/null +++ b/news/9133.removal.rst @@ -0,0 +1 @@ +Remove --unstable-feature flag as it has been deprecated. From 962a0169b2e947c5c7f879ec7c62191e74a3634a Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Thu, 19 Nov 2020 13:36:18 +0000 Subject: [PATCH 2713/3170] Use the new environment files Signed-off-by: Pradyun Gedam <pradyunsg@users.noreply.github.com> --- .github/workflows/linting.yml | 2 +- .github/workflows/macos.yml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml index ebe98fec7dd..71459d660e8 100644 --- a/.github/workflows/linting.yml +++ b/.github/workflows/linting.yml @@ -38,7 +38,7 @@ jobs: ${{ runner.os }}- - name: Set PY (for pre-commit cache) - run: echo "::set-env name=PY::$(python -c 'import hashlib, sys;print(hashlib.sha256(sys.version.encode()+sys.executable.encode()).hexdigest())')" + run: echo "PY=$(python -c 'import hashlib, sys;print(hashlib.sha256(sys.version.encode()+sys.executable.encode()).hexdigest())')" >> $GITHUB_ENV - name: pre-commit cache uses: actions/cache@v1 with: diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index bdf3f671bd2..5d7b9acab87 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -25,7 +25,7 @@ jobs: ${{ runner.os }}-pip- ${{ runner.os }}- - name: Set PY (for pre-commit cache) - run: echo "::set-env name=PY::$(python -c 'import hashlib, sys;print(hashlib.sha256(sys.version.encode()+sys.executable.encode()).hexdigest())')" + run: echo "PY=$(python -c 'import hashlib, sys;print(hashlib.sha256(sys.version.encode()+sys.executable.encode()).hexdigest())')" >> $GITHUB_ENV - name: pre-commit cache uses: actions/cache@v1 with: From a3977d18350105884aac94eb1858919d21ac4396 Mon Sep 17 00:00:00 2001 From: Dustin Ingram <di@users.noreply.github.com> Date: Thu, 19 Nov 2020 12:25:57 -0600 Subject: [PATCH 2714/3170] Don't notify pypa-dev IRC on build status --- .travis.yml | 11 ----------- 1 file changed, 11 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9e7e5d68c25..165d2ef1540 100644 --- a/.travis.yml +++ b/.travis.yml @@ -34,14 +34,3 @@ jobs: before_install: tools/travis/setup.sh install: travis_retry tools/travis/install.sh script: tools/travis/run.sh - -notifications: - irc: - channels: - # This is set to a secure variable to prevent forks from notifying the - # IRC channel whenever they fail a build. This can be removed when travis - # implements https://github.com/travis-ci/travis-ci/issues/1094. - # The actual value here is: irc.freenode.org#pypa-dev - - secure: zAlwcmrDThlRsZz7CPDGpj4ABTzf7bc/zQXYtvIuqmSj0yJMAwsO5Vx/+qdTGYBvmW/oHw2s/uUgtkZzntSQiVQToKMag2fs0d3wV5bLJQUE2Si2jnH2JOQo3JZWSo9HOqL6WYmlKGI8lH9FVTdVLgpeJmIpLy1bN4zx4/TiJjc= - skip_join: true - use_notice: true From 7c3028fa4d73c5d97ccb2a9a292a5e2c6cc258ce Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Thu, 19 Nov 2020 13:56:28 +0000 Subject: [PATCH 2715/3170] Add automation for upgrading dependencies Signed-off-by: Pradyun Gedam <pradyunsg@users.noreply.github.com> --- noxfile.py | 48 +++++++++++++++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/noxfile.py b/noxfile.py index 93a3b24d86d..c21abc2a386 100644 --- a/noxfile.py +++ b/noxfile.py @@ -8,6 +8,7 @@ import os import shutil import sys +from pathlib import Path import nox @@ -152,9 +153,50 @@ def lint(session): @nox.session def vendoring(session): - session.install("vendoring") - - session.run("vendoring", "sync", ".", "-v") + session.install("vendoring>=0.3.0") + + if "--upgrade" not in session.posargs: + session.run("vendoring", "sync", ".", "-v") + return + + def pinned_requirements(path): + for line in path.read_text().splitlines(): + one, two = line.split("==", 1) + name = one.strip() + version = two.split("#")[0].strip() + yield name, version + + vendor_txt = Path("src/pip/_vendor/vendor.txt") + for name, old_version in pinned_requirements(vendor_txt): + # update requirements.txt + session.run("vendoring", "update", ".", name) + + # get the updated version + new_version = old_version + for inner_name, inner_version in pinned_requirements(vendor_txt): + if inner_name == name: + # this is a dedicated assignment, to make flake8 happy + new_version = inner_version + break + else: + session.error(f"Could not find {name} in {vendor_txt}") + + # check if the version changed. + if new_version == old_version: + continue # no change, nothing more to do here. + + # synchronize the contents + session.run("vendoring", "sync", ".") + + # Determine the correct message + message = f"Upgrade {name} to {new_version}" + + # Write our news fragment + news_file = Path("news") / (name + ".vendor.rst") + news_file.write_text(message + "\n") # "\n" appeases end-of-line-fixer + + # Commit the changes + release.commit_file(session, ".", message=message) # ----------------------------------------------------------------------------- From ca6095bfb397610f24e5ad07962c42be7d2db128 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Thu, 19 Nov 2020 14:19:18 +0000 Subject: [PATCH 2716/3170] Upgrade colorama to 0.4.4 --- news/colorama.vendor.rst | 1 + src/pip/_vendor/colorama/__init__.py | 2 +- src/pip/_vendor/colorama/ansi.py | 2 +- src/pip/_vendor/colorama/ansitowin32.py | 19 +-- src/pip/_vendor/requests/LICENSE | 182 ++++++++++++++++++++++-- src/pip/_vendor/urllib3/LICENSE.txt | 2 +- src/pip/_vendor/vendor.txt | 2 +- 7 files changed, 187 insertions(+), 23 deletions(-) create mode 100644 news/colorama.vendor.rst diff --git a/news/colorama.vendor.rst b/news/colorama.vendor.rst new file mode 100644 index 00000000000..30c755eb04a --- /dev/null +++ b/news/colorama.vendor.rst @@ -0,0 +1 @@ +Upgrade colorama to 0.4.4 diff --git a/src/pip/_vendor/colorama/__init__.py b/src/pip/_vendor/colorama/__init__.py index 34c263cc8bb..b149ed79b0a 100644 --- a/src/pip/_vendor/colorama/__init__.py +++ b/src/pip/_vendor/colorama/__init__.py @@ -3,4 +3,4 @@ from .ansi import Fore, Back, Style, Cursor from .ansitowin32 import AnsiToWin32 -__version__ = '0.4.3' +__version__ = '0.4.4' diff --git a/src/pip/_vendor/colorama/ansi.py b/src/pip/_vendor/colorama/ansi.py index 78776588db9..11ec695ff79 100644 --- a/src/pip/_vendor/colorama/ansi.py +++ b/src/pip/_vendor/colorama/ansi.py @@ -6,7 +6,7 @@ CSI = '\033[' OSC = '\033]' -BEL = '\007' +BEL = '\a' def code_to_chars(code): diff --git a/src/pip/_vendor/colorama/ansitowin32.py b/src/pip/_vendor/colorama/ansitowin32.py index 359c92be50e..6039a054320 100644 --- a/src/pip/_vendor/colorama/ansitowin32.py +++ b/src/pip/_vendor/colorama/ansitowin32.py @@ -3,7 +3,7 @@ import sys import os -from .ansi import AnsiFore, AnsiBack, AnsiStyle, Style +from .ansi import AnsiFore, AnsiBack, AnsiStyle, Style, BEL from .winterm import WinTerm, WinColor, WinStyle from .win32 import windll, winapi_test @@ -68,7 +68,7 @@ class AnsiToWin32(object): 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_OSC_RE = re.compile('\001?\033\\]([^\a]*)(\a)\002?') # Operating System Command def __init__(self, wrapped, convert=None, strip=None, autoreset=False): # The wrapped stream (normally sys.stdout or sys.stderr) @@ -247,11 +247,12 @@ def convert_osc(self, text): start, end = match.span() text = text[:start] + text[end:] paramstring, command = match.groups() - if command in '\x07': # \x07 = BEL - params = paramstring.split(";") - # 0 - change title and icon (we will only change title) - # 1 - change icon (we don't support this) - # 2 - change title - if params[0] in '02': - winterm.set_title(params[1]) + if command == BEL: + if paramstring.count(";") == 1: + params = paramstring.split(";") + # 0 - change title and icon (we will only change title) + # 1 - change icon (we don't support this) + # 2 - change title + if params[0] in '02': + winterm.set_title(params[1]) return text diff --git a/src/pip/_vendor/requests/LICENSE b/src/pip/_vendor/requests/LICENSE index 13d91ddc7a8..67db8588217 100644 --- a/src/pip/_vendor/requests/LICENSE +++ b/src/pip/_vendor/requests/LICENSE @@ -1,13 +1,175 @@ -Copyright 2019 Kenneth Reitz - Licensed under the Apache License, Version 2.0 (the "License"); - you may not use this file except in compliance with the License. - You may obtain a copy of the License at + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ - https://www.apache.org/licenses/LICENSE-2.0 + TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION - Unless required by applicable law or agreed to in writing, software - distributed under the License is distributed on an "AS IS" BASIS, - WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. - See the License for the specific language governing permissions and - limitations under the License. + 1. Definitions. + + "License" shall mean the terms and conditions for use, reproduction, + and distribution as defined by Sections 1 through 9 of this document. + + "Licensor" shall mean the copyright owner or entity authorized by + the copyright owner that is granting the License. + + "Legal Entity" shall mean the union of the acting entity and all + other entities that control, are controlled by, or are under common + control with that entity. For the purposes of this definition, + "control" means (i) the power, direct or indirect, to cause the + direction or management of such entity, whether by contract or + otherwise, or (ii) ownership of fifty percent (50%) or more of the + outstanding shares, or (iii) beneficial ownership of such entity. + + "You" (or "Your") shall mean an individual or Legal Entity + exercising permissions granted by this License. + + "Source" form shall mean the preferred form for making modifications, + including but not limited to software source code, documentation + source, and configuration files. + + "Object" form shall mean any form resulting from mechanical + transformation or translation of a Source form, including but + not limited to compiled object code, generated documentation, + and conversions to other media types. + + "Work" shall mean the work of authorship, whether in Source or + Object form, made available under the License, as indicated by a + copyright notice that is included in or attached to the work + (an example is provided in the Appendix below). + + "Derivative Works" shall mean any work, whether in Source or Object + form, that is based on (or derived from) the Work and for which the + editorial revisions, annotations, elaborations, or other modifications + represent, as a whole, an original work of authorship. For the purposes + of this License, Derivative Works shall not include works that remain + separable from, or merely link (or bind by name) to the interfaces of, + the Work and Derivative Works thereof. + + "Contribution" shall mean any work of authorship, including + the original version of the Work and any modifications or additions + to that Work or Derivative Works thereof, that is intentionally + submitted to Licensor for inclusion in the Work by the copyright owner + or by an individual or Legal Entity authorized to submit on behalf of + the copyright owner. For the purposes of this definition, "submitted" + means any form of electronic, verbal, or written communication sent + to the Licensor or its representatives, including but not limited to + communication on electronic mailing lists, source code control systems, + and issue tracking systems that are managed by, or on behalf of, the + Licensor for the purpose of discussing and improving the Work, but + excluding communication that is conspicuously marked or otherwise + designated in writing by the copyright owner as "Not a Contribution." + + "Contributor" shall mean Licensor and any individual or Legal Entity + on behalf of whom a Contribution has been received by Licensor and + subsequently incorporated within the Work. + + 2. Grant of Copyright License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + copyright license to reproduce, prepare Derivative Works of, + publicly display, publicly perform, sublicense, and distribute the + Work and such Derivative Works in Source or Object form. + + 3. Grant of Patent License. Subject to the terms and conditions of + this License, each Contributor hereby grants to You a perpetual, + worldwide, non-exclusive, no-charge, royalty-free, irrevocable + (except as stated in this section) patent license to make, have made, + use, offer to sell, sell, import, and otherwise transfer the Work, + where such license applies only to those patent claims licensable + by such Contributor that are necessarily infringed by their + Contribution(s) alone or by combination of their Contribution(s) + with the Work to which such Contribution(s) was submitted. If You + institute patent litigation against any entity (including a + cross-claim or counterclaim in a lawsuit) alleging that the Work + or a Contribution incorporated within the Work constitutes direct + or contributory patent infringement, then any patent licenses + granted to You under this License for that Work shall terminate + as of the date such litigation is filed. + + 4. Redistribution. You may reproduce and distribute copies of the + Work or Derivative Works thereof in any medium, with or without + modifications, and in Source or Object form, provided that You + meet the following conditions: + + (a) You must give any other recipients of the Work or + Derivative Works a copy of this License; and + + (b) You must cause any modified files to carry prominent notices + stating that You changed the files; and + + (c) You must retain, in the Source form of any Derivative Works + that You distribute, all copyright, patent, trademark, and + attribution notices from the Source form of the Work, + excluding those notices that do not pertain to any part of + the Derivative Works; and + + (d) If the Work includes a "NOTICE" text file as part of its + distribution, then any Derivative Works that You distribute must + include a readable copy of the attribution notices contained + within such NOTICE file, excluding those notices that do not + pertain to any part of the Derivative Works, in at least one + of the following places: within a NOTICE text file distributed + as part of the Derivative Works; within the Source form or + documentation, if provided along with the Derivative Works; or, + within a display generated by the Derivative Works, if and + wherever such third-party notices normally appear. The contents + of the NOTICE file are for informational purposes only and + do not modify the License. You may add Your own attribution + notices within Derivative Works that You distribute, alongside + or as an addendum to the NOTICE text from the Work, provided + that such additional attribution notices cannot be construed + as modifying the License. + + You may add Your own copyright statement to Your modifications and + may provide additional or different license terms and conditions + for use, reproduction, or distribution of Your modifications, or + for any such Derivative Works as a whole, provided Your use, + reproduction, and distribution of the Work otherwise complies with + the conditions stated in this License. + + 5. Submission of Contributions. Unless You explicitly state otherwise, + any Contribution intentionally submitted for inclusion in the Work + by You to the Licensor shall be under the terms and conditions of + this License, without any additional terms or conditions. + Notwithstanding the above, nothing herein shall supersede or modify + the terms of any separate license agreement you may have executed + with Licensor regarding such Contributions. + + 6. Trademarks. This License does not grant permission to use the trade + names, trademarks, service marks, or product names of the Licensor, + except as required for reasonable and customary use in describing the + origin of the Work and reproducing the content of the NOTICE file. + + 7. Disclaimer of Warranty. Unless required by applicable law or + agreed to in writing, Licensor provides the Work (and each + Contributor provides its Contributions) on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or + implied, including, without limitation, any warranties or conditions + of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A + PARTICULAR PURPOSE. You are solely responsible for determining the + appropriateness of using or redistributing the Work and assume any + risks associated with Your exercise of permissions under this License. + + 8. Limitation of Liability. In no event and under no legal theory, + whether in tort (including negligence), contract, or otherwise, + unless required by applicable law (such as deliberate and grossly + negligent acts) or agreed to in writing, shall any Contributor be + liable to You for damages, including any direct, indirect, special, + incidental, or consequential damages of any character arising as a + result of this License or out of the use or inability to use the + Work (including but not limited to damages for loss of goodwill, + work stoppage, computer failure or malfunction, or any and all + other commercial damages or losses), even if such Contributor + has been advised of the possibility of such damages. + + 9. Accepting Warranty or Additional Liability. While redistributing + the Work or Derivative Works thereof, You may choose to offer, + and charge a fee for, acceptance of support, warranty, indemnity, + or other liability obligations and/or rights consistent with this + License. However, in accepting such obligations, You may act only + on Your own behalf and on Your sole responsibility, not on behalf + of any other Contributor, and only if You agree to indemnify, + defend, and hold each Contributor harmless for any liability + incurred by, or claims asserted against, such Contributor by reason + of your accepting any such warranty or additional liability. diff --git a/src/pip/_vendor/urllib3/LICENSE.txt b/src/pip/_vendor/urllib3/LICENSE.txt index c89cf27b859..429a1767e44 100644 --- a/src/pip/_vendor/urllib3/LICENSE.txt +++ b/src/pip/_vendor/urllib3/LICENSE.txt @@ -1,6 +1,6 @@ MIT License -Copyright (c) 2008-2019 Andrey Petrov and contributors (see CONTRIBUTORS.txt) +Copyright (c) 2008-2020 Andrey Petrov and contributors (see CONTRIBUTORS.txt) Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 06fa1358f00..1dea203f206 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -1,6 +1,6 @@ appdirs==1.4.4 CacheControl==0.12.6 -colorama==0.4.3 +colorama==0.4.4 contextlib2==0.6.0.post1 distlib==0.3.1 distro==1.5.0 From 1c6a11952905962ce89c8bfbf1949b76f838bb15 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Thu, 19 Nov 2020 14:19:42 +0000 Subject: [PATCH 2717/3170] Upgrade pep517 to 0.9.1 --- news/pep517.vendor.rst | 1 + src/pip/_vendor/pep517/__init__.py | 4 ++- src/pip/_vendor/pep517/wrappers.py | 47 +++++++++++++++++++++--------- src/pip/_vendor/vendor.txt | 2 +- 4 files changed, 38 insertions(+), 16 deletions(-) create mode 100644 news/pep517.vendor.rst diff --git a/news/pep517.vendor.rst b/news/pep517.vendor.rst new file mode 100644 index 00000000000..945f1e74686 --- /dev/null +++ b/news/pep517.vendor.rst @@ -0,0 +1 @@ +Upgrade pep517 to 0.9.1 diff --git a/src/pip/_vendor/pep517/__init__.py b/src/pip/_vendor/pep517/__init__.py index 7355b68a240..10687486e2b 100644 --- a/src/pip/_vendor/pep517/__init__.py +++ b/src/pip/_vendor/pep517/__init__.py @@ -1,4 +1,6 @@ """Wrappers to build Python packages using PEP 517 hooks """ -__version__ = '0.8.2' +__version__ = '0.9.1' + +from .wrappers import * # noqa: F401, F403 diff --git a/src/pip/_vendor/pep517/wrappers.py b/src/pip/_vendor/pep517/wrappers.py index 00a3d1a789f..d6338ea5201 100644 --- a/src/pip/_vendor/pep517/wrappers.py +++ b/src/pip/_vendor/pep517/wrappers.py @@ -9,6 +9,15 @@ from . import compat +__all__ = [ + 'BackendUnavailable', + 'BackendInvalid', + 'HookMissing', + 'UnsupportedOperation', + 'default_subprocess_runner', + 'quiet_subprocess_runner', + 'Pep517HookCaller', +] try: import importlib.resources as resources @@ -102,19 +111,22 @@ def norm_and_check(source_tree, requested): class Pep517HookCaller(object): """A wrapper around a source directory to be built with a PEP 517 backend. - source_dir : The path to the source directory, containing pyproject.toml. - build_backend : The build backend spec, as per PEP 517, from + :param source_dir: The path to the source directory, containing pyproject.toml. - backend_path : The backend path, as per PEP 517, from pyproject.toml. - runner : A callable that invokes the wrapper subprocess. + :param build_backend: The build backend spec, as per PEP 517, from + pyproject.toml. + :param backend_path: The backend path, as per PEP 517, from pyproject.toml. + :param runner: A callable that invokes the wrapper subprocess. + :param python_executable: The Python executable used to invoke the backend The 'runner', if provided, must expect the following: - cmd : a list of strings representing the command and arguments to - execute, as would be passed to e.g. 'subprocess.check_call'. - cwd : a string representing the working directory that must be - used for the subprocess. Corresponds to the provided source_dir. - extra_environ : a dict mapping environment variable names to values - which must be set for the subprocess execution. + + - cmd: a list of strings representing the command and arguments to + execute, as would be passed to e.g. 'subprocess.check_call'. + - cwd: a string representing the working directory that must be + used for the subprocess. Corresponds to the provided source_dir. + - extra_environ: a dict mapping environment variable names to values + which must be set for the subprocess execution. """ def __init__( self, @@ -122,6 +134,7 @@ def __init__( build_backend, backend_path=None, runner=None, + python_executable=None, ): if runner is None: runner = default_subprocess_runner @@ -134,6 +147,9 @@ def __init__( ] self.backend_path = backend_path self._subprocess_runner = runner + if not python_executable: + python_executable = sys.executable + self.python_executable = python_executable @contextmanager def subprocess_runner(self, runner): @@ -150,7 +166,8 @@ def subprocess_runner(self, runner): def get_requires_for_build_wheel(self, config_settings=None): """Identify packages required for building a wheel - Returns a list of dependency specifications, e.g.: + Returns a list of dependency specifications, e.g.:: + ["wheel >= 0.25", "setuptools"] This does not include requirements specified in pyproject.toml. @@ -164,7 +181,7 @@ def get_requires_for_build_wheel(self, config_settings=None): def prepare_metadata_for_build_wheel( self, metadata_directory, config_settings=None, _allow_fallback=True): - """Prepare a *.dist-info folder with metadata for this project. + """Prepare a ``*.dist-info`` folder with metadata for this project. Returns the name of the newly created folder. @@ -202,7 +219,8 @@ def build_wheel( def get_requires_for_build_sdist(self, config_settings=None): """Identify packages required for building a wheel - Returns a list of dependency specifications, e.g.: + Returns a list of dependency specifications, e.g.:: + ["setuptools >= 26"] This does not include requirements specified in pyproject.toml. @@ -252,8 +270,9 @@ def _call_hook(self, hook_name, kwargs): # Run the hook in a subprocess with _in_proc_script_path() as script: + python = self.python_executable self._subprocess_runner( - [sys.executable, str(script), hook_name, td], + [python, abspath(str(script)), hook_name, td], cwd=self.source_dir, extra_environ=extra_environ ) diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 1dea203f206..601daaed4ab 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -8,7 +8,7 @@ html5lib==1.1 ipaddress==1.0.23 # Only needed on 2.6 and 2.7 msgpack==1.0.0 packaging==20.4 -pep517==0.8.2 +pep517==0.9.1 progress==1.5 pyparsing==2.4.7 requests==2.24.0 From 1604ac473753739b202f6f1f7acc1c66b11ad5e2 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Thu, 19 Nov 2020 14:20:05 +0000 Subject: [PATCH 2718/3170] Upgrade requests to 2.25.0 --- news/requests.vendor.rst | 1 + src/pip/_vendor/requests/__init__.py | 4 ++-- src/pip/_vendor/requests/__version__.py | 4 ++-- src/pip/_vendor/requests/models.py | 4 +++- src/pip/_vendor/requests/sessions.py | 14 +++++++++++++- src/pip/_vendor/requests/utils.py | 12 +++++++++--- src/pip/_vendor/vendor.txt | 2 +- 7 files changed, 31 insertions(+), 10 deletions(-) create mode 100644 news/requests.vendor.rst diff --git a/news/requests.vendor.rst b/news/requests.vendor.rst new file mode 100644 index 00000000000..515ca25a94a --- /dev/null +++ b/news/requests.vendor.rst @@ -0,0 +1 @@ +Upgrade requests to 2.25.0 diff --git a/src/pip/_vendor/requests/__init__.py b/src/pip/_vendor/requests/__init__.py index 517458b5a25..4bea577a36f 100644 --- a/src/pip/_vendor/requests/__init__.py +++ b/src/pip/_vendor/requests/__init__.py @@ -57,10 +57,10 @@ def check_compatibility(urllib3_version, chardet_version): # Check urllib3 for compatibility. major, minor, patch = urllib3_version # noqa: F811 major, minor, patch = int(major), int(minor), int(patch) - # urllib3 >= 1.21.1, <= 1.25 + # urllib3 >= 1.21.1, <= 1.26 assert major == 1 assert minor >= 21 - assert minor <= 25 + assert minor <= 26 # Check chardet for compatibility. major, minor, patch = chardet_version.split('.')[:3] diff --git a/src/pip/_vendor/requests/__version__.py b/src/pip/_vendor/requests/__version__.py index 531e26ceb24..71085207750 100644 --- a/src/pip/_vendor/requests/__version__.py +++ b/src/pip/_vendor/requests/__version__.py @@ -5,8 +5,8 @@ __title__ = 'requests' __description__ = 'Python HTTP for Humans.' __url__ = 'https://requests.readthedocs.io' -__version__ = '2.24.0' -__build__ = 0x022400 +__version__ = '2.25.0' +__build__ = 0x022500 __author__ = 'Kenneth Reitz' __author_email__ = 'me@kennethreitz.org' __license__ = 'Apache 2.0' diff --git a/src/pip/_vendor/requests/models.py b/src/pip/_vendor/requests/models.py index 015e715dad3..b0ce2950f25 100644 --- a/src/pip/_vendor/requests/models.py +++ b/src/pip/_vendor/requests/models.py @@ -273,7 +273,9 @@ class PreparedRequest(RequestEncodingMixin, RequestHooksMixin): """The fully mutable :class:`PreparedRequest <PreparedRequest>` object, containing the exact bytes that will be sent to the server. - Generated from either a :class:`Request <Request>` object or manually. + Instances are generated from a :class:`Request <Request>` object, and + should not be instantiated manually; doing so may produce undesirable + effects. Usage:: diff --git a/src/pip/_vendor/requests/sessions.py b/src/pip/_vendor/requests/sessions.py index e8e2d609a78..fdf7e9fe35d 100644 --- a/src/pip/_vendor/requests/sessions.py +++ b/src/pip/_vendor/requests/sessions.py @@ -387,6 +387,13 @@ def __init__(self): self.stream = False #: SSL Verification default. + #: Defaults to `True`, requiring requests to verify the TLS certificate at the + #: remote end. + #: If verify is set to `False`, requests will accept any TLS certificate + #: presented by the server, and will ignore hostname mismatches and/or + #: expired certificates, which will make your application vulnerable to + #: man-in-the-middle (MitM) attacks. + #: Only set this to `False` for testing. self.verify = True #: SSL client certificate default, if String, path to ssl client @@ -495,7 +502,12 @@ def request(self, method, url, content. Defaults to ``False``. :param verify: (optional) Either a boolean, in which case it controls whether we verify the server's TLS certificate, or a string, in which case it must be a path - to a CA bundle to use. Defaults to ``True``. + to a CA bundle to use. Defaults to ``True``. When set to + ``False``, requests will accept any TLS certificate presented by + the server, and will ignore hostname mismatches and/or expired + certificates, which will make your application vulnerable to + man-in-the-middle (MitM) attacks. Setting verify to ``False`` + may be useful during local development or testing. :param cert: (optional) if String, path to ssl client cert file (.pem). If Tuple, ('cert', 'key') pair. :rtype: requests.Response diff --git a/src/pip/_vendor/requests/utils.py b/src/pip/_vendor/requests/utils.py index c1700d7fe85..16d5776201d 100644 --- a/src/pip/_vendor/requests/utils.py +++ b/src/pip/_vendor/requests/utils.py @@ -169,14 +169,20 @@ def super_len(o): def get_netrc_auth(url, raise_errors=False): """Returns the Requests tuple auth for a given url from netrc.""" + netrc_file = os.environ.get('NETRC') + if netrc_file is not None: + netrc_locations = (netrc_file,) + else: + netrc_locations = ('~/{}'.format(f) for f in NETRC_FILES) + try: from netrc import netrc, NetrcParseError netrc_path = None - for f in NETRC_FILES: + for f in netrc_locations: try: - loc = os.path.expanduser('~/{}'.format(f)) + loc = os.path.expanduser(f) except KeyError: # os.path.expanduser can fail when $HOME is undefined and # getpwuid fails. See https://bugs.python.org/issue20164 & @@ -212,7 +218,7 @@ def get_netrc_auth(url, raise_errors=False): if raise_errors: raise - # AppEngine hackiness. + # App Engine hackiness. except (ImportError, AttributeError): pass diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 601daaed4ab..c3d93e275bd 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -11,7 +11,7 @@ packaging==20.4 pep517==0.9.1 progress==1.5 pyparsing==2.4.7 -requests==2.24.0 +requests==2.25.0 certifi==2020.06.20 chardet==3.0.4 idna==2.10 From b08c4d5f3f2d6f9e4ff1ebfa2bb7efbd86d2adff Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Thu, 19 Nov 2020 14:20:28 +0000 Subject: [PATCH 2719/3170] Upgrade certifi to 2020.11.8 --- news/certifi.vendor.rst | 1 + src/pip/_vendor/certifi/__init__.py | 2 +- src/pip/_vendor/certifi/cacert.pem | 194 +++++++++++++--------------- src/pip/_vendor/vendor.txt | 2 +- 4 files changed, 93 insertions(+), 106 deletions(-) create mode 100644 news/certifi.vendor.rst diff --git a/news/certifi.vendor.rst b/news/certifi.vendor.rst new file mode 100644 index 00000000000..b181bc30c48 --- /dev/null +++ b/news/certifi.vendor.rst @@ -0,0 +1 @@ +Upgrade certifi to 2020.11.8 diff --git a/src/pip/_vendor/certifi/__init__.py b/src/pip/_vendor/certifi/__init__.py index 5d52a62e7f4..4e5133b261d 100644 --- a/src/pip/_vendor/certifi/__init__.py +++ b/src/pip/_vendor/certifi/__init__.py @@ -1,3 +1,3 @@ from .core import contents, where -__version__ = "2020.06.20" +__version__ = "2020.11.08" diff --git a/src/pip/_vendor/certifi/cacert.pem b/src/pip/_vendor/certifi/cacert.pem index 0fd855f4646..a1072085ce5 100644 --- a/src/pip/_vendor/certifi/cacert.pem +++ b/src/pip/_vendor/certifi/cacert.pem @@ -575,46 +575,6 @@ VSJYACPq4xJDKVtHCN2MQWplBqjlIapBtJUhlbl90TSrE9atvNziPTnNvT51cKEY WQPJIrSPnNVeKtelttQKbfi3QBFGmh95DmK/D5fs4C8fF5Q= -----END CERTIFICATE----- -# Issuer: O=Government Root Certification Authority -# Subject: O=Government Root Certification Authority -# Label: "Taiwan GRCA" -# Serial: 42023070807708724159991140556527066870 -# MD5 Fingerprint: 37:85:44:53:32:45:1f:20:f0:f3:95:e1:25:c4:43:4e -# SHA1 Fingerprint: f4:8b:11:bf:de:ab:be:94:54:20:71:e6:41:de:6b:be:88:2b:40:b9 -# SHA256 Fingerprint: 76:00:29:5e:ef:e8:5b:9e:1f:d6:24:db:76:06:2a:aa:ae:59:81:8a:54:d2:77:4c:d4:c0:b2:c0:11:31:e1:b3 ------BEGIN CERTIFICATE----- -MIIFcjCCA1qgAwIBAgIQH51ZWtcvwgZEpYAIaeNe9jANBgkqhkiG9w0BAQUFADA/ -MQswCQYDVQQGEwJUVzEwMC4GA1UECgwnR292ZXJubWVudCBSb290IENlcnRpZmlj -YXRpb24gQXV0aG9yaXR5MB4XDTAyMTIwNTEzMjMzM1oXDTMyMTIwNTEzMjMzM1ow -PzELMAkGA1UEBhMCVFcxMDAuBgNVBAoMJ0dvdmVybm1lbnQgUm9vdCBDZXJ0aWZp -Y2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB -AJoluOzMonWoe/fOW1mKydGGEghU7Jzy50b2iPN86aXfTEc2pBsBHH8eV4qNw8XR -IePaJD9IK/ufLqGU5ywck9G/GwGHU5nOp/UKIXZ3/6m3xnOUT0b3EEk3+qhZSV1q -gQdW8or5BtD3cCJNtLdBuTK4sfCxw5w/cP1T3YGq2GN49thTbqGsaoQkclSGxtKy -yhwOeYHWtXBiCAEuTk8O1RGvqa/lmr/czIdtJuTJV6L7lvnM4T9TjGxMfptTCAts -F/tnyMKtsc2AtJfcdgEWFelq16TheEfOhtX7MfP6Mb40qij7cEwdScevLJ1tZqa2 -jWR+tSBqnTuBto9AAGdLiYa4zGX+FVPpBMHWXx1E1wovJ5pGfaENda1UhhXcSTvx -ls4Pm6Dso3pdvtUqdULle96ltqqvKKyskKw4t9VoNSZ63Pc78/1Fm9G7Q3hub/FC -VGqY8A2tl+lSXunVanLeavcbYBT0peS2cWeqH+riTcFCQP5nRhc4L0c/cZyu5SHK -YS1tB6iEfC3uUSXxY5Ce/eFXiGvviiNtsea9P63RPZYLhY3Naye7twWb7LuRqQoH -EgKXTiCQ8P8NHuJBO9NAOueNXdpm5AKwB1KYXA6OM5zCppX7VRluTI6uSw+9wThN -Xo+EHWbNxWCWtFJaBYmOlXqYwZE8lSOyDvR5tMl8wUohAgMBAAGjajBoMB0GA1Ud -DgQWBBTMzO/MKWCkO7GStjz6MmKPrCUVOzAMBgNVHRMEBTADAQH/MDkGBGcqBwAE -MTAvMC0CAQAwCQYFKw4DAhoFADAHBgVnKgMAAAQUA5vwIhP/lSg209yewDL7MTqK -UWUwDQYJKoZIhvcNAQEFBQADggIBAECASvomyc5eMN1PhnR2WPWus4MzeKR6dBcZ -TulStbngCnRiqmjKeKBMmo4sIy7VahIkv9Ro04rQ2JyftB8M3jh+Vzj8jeJPXgyf -qzvS/3WXy6TjZwj/5cAWtUgBfen5Cv8b5Wppv3ghqMKnI6mGq3ZW6A4M9hPdKmaK -ZEk9GhiHkASfQlK3T8v+R0F2Ne//AHY2RTKbxkaFXeIksB7jSJaYV0eUVXoPQbFE -JPPB/hprv4j9wabak2BegUqZIJxIZhm1AHlUD7gsL0u8qV1bYH+Mh6XgUmMqvtg7 -hUAV/h62ZT/FS9p+tXo1KaMuephgIqP0fSdOLeq0dDzpD6QzDxARvBMB1uUO07+1 -EqLhRSPAzAhuYbeJq4PjJB7mXQfnHyA+z2fI56wwbSdLaG5LKlwCCDTb+HbkZ6Mm -nD+iMsJKxYEYMRBWqoTvLQr/uB930r+lWKBi5NdLkXWNiYCYfm3LU05er/ayl4WX -udpVBrkk7tfGOB5jGxI7leFYrPLfhNVfmS8NVVvmONsuP3LpSIXLuykTjx44Vbnz -ssQwmSNOXfJIoRIM3BKQCZBUkQM8R+XVyWXgt0t97EfTsws+rZ7QdAAO671RrcDe -LMDDav7v3Aun+kbfYNucpllQdSNpc5Oy+fwC00fmcc4QAu4njIT/rEUNE1yDMuAl -pYYsfPQS ------END CERTIFICATE----- - # Issuer: CN=DigiCert Assured ID Root CA O=DigiCert Inc OU=www.digicert.com # Subject: CN=DigiCert Assured ID Root CA O=DigiCert Inc OU=www.digicert.com # Label: "DigiCert Assured ID Root CA" @@ -1062,38 +1022,6 @@ fQjGGoe9GKhzvSbKYAydzpmfz1wPMOG+FDHqAjAU9JM8SaczepBGR7NjfRObTrdv GDeAU/7dIOA1mjbRxwG55tzd8/8dLDoWV9mSOdY= -----END CERTIFICATE----- -# Issuer: CN=OISTE WISeKey Global Root GA CA O=WISeKey OU=Copyright (c) 2005/OISTE Foundation Endorsed -# Subject: CN=OISTE WISeKey Global Root GA CA O=WISeKey OU=Copyright (c) 2005/OISTE Foundation Endorsed -# Label: "OISTE WISeKey Global Root GA CA" -# Serial: 86718877871133159090080555911823548314 -# MD5 Fingerprint: bc:6c:51:33:a7:e9:d3:66:63:54:15:72:1b:21:92:93 -# SHA1 Fingerprint: 59:22:a1:e1:5a:ea:16:35:21:f8:98:39:6a:46:46:b0:44:1b:0f:a9 -# SHA256 Fingerprint: 41:c9:23:86:6a:b4:ca:d6:b7:ad:57:80:81:58:2e:02:07:97:a6:cb:df:4f:ff:78:ce:83:96:b3:89:37:d7:f5 ------BEGIN CERTIFICATE----- -MIID8TCCAtmgAwIBAgIQQT1yx/RrH4FDffHSKFTfmjANBgkqhkiG9w0BAQUFADCB -ijELMAkGA1UEBhMCQ0gxEDAOBgNVBAoTB1dJU2VLZXkxGzAZBgNVBAsTEkNvcHly -aWdodCAoYykgMjAwNTEiMCAGA1UECxMZT0lTVEUgRm91bmRhdGlvbiBFbmRvcnNl -ZDEoMCYGA1UEAxMfT0lTVEUgV0lTZUtleSBHbG9iYWwgUm9vdCBHQSBDQTAeFw0w -NTEyMTExNjAzNDRaFw0zNzEyMTExNjA5NTFaMIGKMQswCQYDVQQGEwJDSDEQMA4G -A1UEChMHV0lTZUtleTEbMBkGA1UECxMSQ29weXJpZ2h0IChjKSAyMDA1MSIwIAYD -VQQLExlPSVNURSBGb3VuZGF0aW9uIEVuZG9yc2VkMSgwJgYDVQQDEx9PSVNURSBX -SVNlS2V5IEdsb2JhbCBSb290IEdBIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8A -MIIBCgKCAQEAy0+zAJs9Nt350UlqaxBJH+zYK7LG+DKBKUOVTJoZIyEVRd7jyBxR -VVuuk+g3/ytr6dTqvirdqFEr12bDYVxgAsj1znJ7O7jyTmUIms2kahnBAbtzptf2 -w93NvKSLtZlhuAGio9RN1AU9ka34tAhxZK9w8RxrfvbDd50kc3vkDIzh2TbhmYsF -mQvtRTEJysIA2/dyoJaqlYfQjse2YXMNdmaM3Bu0Y6Kff5MTMPGhJ9vZ/yxViJGg -4E8HsChWjBgbl0SOid3gF27nKu+POQoxhILYQBRJLnpB5Kf+42TMwVlxSywhp1t9 -4B3RLoGbw9ho972WG6xwsRYUC9tguSYBBQIDAQABo1EwTzALBgNVHQ8EBAMCAYYw -DwYDVR0TAQH/BAUwAwEB/zAdBgNVHQ4EFgQUswN+rja8sHnR3JQmthG+IbJphpQw -EAYJKwYBBAGCNxUBBAMCAQAwDQYJKoZIhvcNAQEFBQADggEBAEuh/wuHbrP5wUOx -SPMowB0uyQlB+pQAHKSkq0lPjz0e701vvbyk9vImMMkQyh2I+3QZH4VFvbBsUfk2 -ftv1TDI6QU9bR8/oCy22xBmddMVHxjtqD6wU2zz0c5ypBd8A3HR4+vg1YFkCExh8 -vPtNsCBtQ7tgMHpnM1zFmdH4LTlSc/uMqpclXHLZCB6rTjzjgTGfA6b7wP4piFXa -hNVQA7bihKOmNqoROgHhGEvWRGizPflTdISzRpFGlgC3gCy24eMQ4tui5yiPAZZi -Fj4A4xylNoEYokxSdsARo27mHbrjWr42U8U+dY+GaSlYU7Wcu2+fXMUY7N0v4ZjJ -/L7fCg0= ------END CERTIFICATE----- - # Issuer: CN=Certigna O=Dhimyotis # Subject: CN=Certigna O=Dhimyotis # Label: "Certigna" @@ -2285,38 +2213,6 @@ e9eiPZaGzPImNC1qkp2aGtAw4l1OBLBfiyB+d8E9lYLRRpo7PHi4b6HQDWSieB4p TpPDpFQUWw== -----END CERTIFICATE----- -# Issuer: CN=EE Certification Centre Root CA O=AS Sertifitseerimiskeskus -# Subject: CN=EE Certification Centre Root CA O=AS Sertifitseerimiskeskus -# Label: "EE Certification Centre Root CA" -# Serial: 112324828676200291871926431888494945866 -# MD5 Fingerprint: 43:5e:88:d4:7d:1a:4a:7e:fd:84:2e:52:eb:01:d4:6f -# SHA1 Fingerprint: c9:a8:b9:e7:55:80:5e:58:e3:53:77:a7:25:eb:af:c3:7b:27:cc:d7 -# SHA256 Fingerprint: 3e:84:ba:43:42:90:85:16:e7:75:73:c0:99:2f:09:79:ca:08:4e:46:85:68:1f:f1:95:cc:ba:8a:22:9b:8a:76 ------BEGIN CERTIFICATE----- -MIIEAzCCAuugAwIBAgIQVID5oHPtPwBMyonY43HmSjANBgkqhkiG9w0BAQUFADB1 -MQswCQYDVQQGEwJFRTEiMCAGA1UECgwZQVMgU2VydGlmaXRzZWVyaW1pc2tlc2t1 -czEoMCYGA1UEAwwfRUUgQ2VydGlmaWNhdGlvbiBDZW50cmUgUm9vdCBDQTEYMBYG -CSqGSIb3DQEJARYJcGtpQHNrLmVlMCIYDzIwMTAxMDMwMTAxMDMwWhgPMjAzMDEy -MTcyMzU5NTlaMHUxCzAJBgNVBAYTAkVFMSIwIAYDVQQKDBlBUyBTZXJ0aWZpdHNl -ZXJpbWlza2Vza3VzMSgwJgYDVQQDDB9FRSBDZXJ0aWZpY2F0aW9uIENlbnRyZSBS -b290IENBMRgwFgYJKoZIhvcNAQkBFglwa2lAc2suZWUwggEiMA0GCSqGSIb3DQEB -AQUAA4IBDwAwggEKAoIBAQDIIMDs4MVLqwd4lfNE7vsLDP90jmG7sWLqI9iroWUy -euuOF0+W2Ap7kaJjbMeMTC55v6kF/GlclY1i+blw7cNRfdCT5mzrMEvhvH2/UpvO -bntl8jixwKIy72KyaOBhU8E2lf/slLo2rpwcpzIP5Xy0xm90/XsY6KxX7QYgSzIw -WFv9zajmofxwvI6Sc9uXp3whrj3B9UiHbCe9nyV0gVWw93X2PaRka9ZP585ArQ/d -MtO8ihJTmMmJ+xAdTX7Nfh9WDSFwhfYggx/2uh8Ej+p3iDXE/+pOoYtNP2MbRMNE -1CV2yreN1x5KZmTNXMWcg+HCCIia7E6j8T4cLNlsHaFLAgMBAAGjgYowgYcwDwYD -VR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYwHQYDVR0OBBYEFBLyWj7qVhy/ -zQas8fElyalL1BSZMEUGA1UdJQQ+MDwGCCsGAQUFBwMCBggrBgEFBQcDAQYIKwYB -BQUHAwMGCCsGAQUFBwMEBggrBgEFBQcDCAYIKwYBBQUHAwkwDQYJKoZIhvcNAQEF -BQADggEBAHv25MANqhlHt01Xo/6tu7Fq1Q+e2+RjxY6hUFaTlrg4wCQiZrxTFGGV -v9DHKpY5P30osxBAIWrEr7BSdxjhlthWXePdNl4dp1BUoMUq5KqMlIpPnTX/dqQG -E5Gion0ARD9V04I8GtVbvFZMIi5GQ4okQC3zErg7cBqklrkar4dBGmoYDQZPxz5u -uSlNDUmJEYcyW+ZLBMjkXOZ0c5RdFpgTlf7727FE5TpwrDdr5rMzcijJs1eg9gIW -iAYLtqZLICjU3j2LrTcFU3T+bsy8QxdxXvnFzBqpYe73dgzzcvRyrc9yAjYHR8/v -GVCJYMzpJJUPwssd8m92kMfMdcGWxZ0= ------END CERTIFICATE----- - # Issuer: CN=D-TRUST Root Class 3 CA 2 2009 O=D-Trust GmbH # Subject: CN=D-TRUST Root Class 3 CA 2 2009 O=D-Trust GmbH # Label: "D-TRUST Root Class 3 CA 2 2009" @@ -4618,3 +4514,93 @@ PuXaTH4MNMn5X7azKFGnpyuqSfqNZSlO42sTp5SjLVFteAxEy9/eCG/Oo2Sr05WE 1LlSVHJ7liXMvGnjSG4N0MedJ5qq+BOS3R7fY581qRY27Iy4g/Q9iY/NtBde17MX QRBdJ3NghVdJIgc= -----END CERTIFICATE----- + +# Issuer: CN=Trustwave Global Certification Authority O=Trustwave Holdings, Inc. +# Subject: CN=Trustwave Global Certification Authority O=Trustwave Holdings, Inc. +# Label: "Trustwave Global Certification Authority" +# Serial: 1846098327275375458322922162 +# MD5 Fingerprint: f8:1c:18:2d:2f:ba:5f:6d:a1:6c:bc:c7:ab:91:c7:0e +# SHA1 Fingerprint: 2f:8f:36:4f:e1:58:97:44:21:59:87:a5:2a:9a:d0:69:95:26:7f:b5 +# SHA256 Fingerprint: 97:55:20:15:f5:dd:fc:3c:87:88:c0:06:94:45:55:40:88:94:45:00:84:f1:00:86:70:86:bc:1a:2b:b5:8d:c8 +-----BEGIN CERTIFICATE----- +MIIF2jCCA8KgAwIBAgIMBfcOhtpJ80Y1LrqyMA0GCSqGSIb3DQEBCwUAMIGIMQsw +CQYDVQQGEwJVUzERMA8GA1UECAwISWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28x +ITAfBgNVBAoMGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1 +c3R3YXZlIEdsb2JhbCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0xNzA4MjMx +OTM0MTJaFw00MjA4MjMxOTM0MTJaMIGIMQswCQYDVQQGEwJVUzERMA8GA1UECAwI +SWxsaW5vaXMxEDAOBgNVBAcMB0NoaWNhZ28xITAfBgNVBAoMGFRydXN0d2F2ZSBI +b2xkaW5ncywgSW5jLjExMC8GA1UEAwwoVHJ1c3R3YXZlIEdsb2JhbCBDZXJ0aWZp +Y2F0aW9uIEF1dGhvcml0eTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIB +ALldUShLPDeS0YLOvR29zd24q88KPuFd5dyqCblXAj7mY2Hf8g+CY66j96xz0Xzn +swuvCAAJWX/NKSqIk4cXGIDtiLK0thAfLdZfVaITXdHG6wZWiYj+rDKd/VzDBcdu +7oaJuogDnXIhhpCujwOl3J+IKMujkkkP7NAP4m1ET4BqstTnoApTAbqOl5F2brz8 +1Ws25kCI1nsvXwXoLG0R8+eyvpJETNKXpP7ScoFDB5zpET71ixpZfR9oWN0EACyW +80OzfpgZdNmcc9kYvkHHNHnZ9GLCQ7mzJ7Aiy/k9UscwR7PJPrhq4ufogXBeQotP +JqX+OsIgbrv4Fo7NDKm0G2x2EOFYeUY+VM6AqFcJNykbmROPDMjWLBz7BegIlT1l +RtzuzWniTY+HKE40Cz7PFNm73bZQmq131BnW2hqIyE4bJ3XYsgjxroMwuREOzYfw +hI0Vcnyh78zyiGG69Gm7DIwLdVcEuE4qFC49DxweMqZiNu5m4iK4BUBjECLzMx10 +coos9TkpoNPnG4CELcU9402x/RpvumUHO1jsQkUm+9jaJXLE9gCxInm943xZYkqc +BW89zubWR2OZxiRvchLIrH+QtAuRcOi35hYQcRfO3gZPSEF9NUqjifLJS3tBEW1n +twiYTOURGa5CgNz7kAXU+FDKvuStx8KU1xad5hePrzb7AgMBAAGjQjBAMA8GA1Ud +EwEB/wQFMAMBAf8wHQYDVR0OBBYEFJngGWcNYtt2s9o9uFvo/ULSMQ6HMA4GA1Ud +DwEB/wQEAwIBBjANBgkqhkiG9w0BAQsFAAOCAgEAmHNw4rDT7TnsTGDZqRKGFx6W +0OhUKDtkLSGm+J1WE2pIPU/HPinbbViDVD2HfSMF1OQc3Og4ZYbFdada2zUFvXfe +uyk3QAUHw5RSn8pk3fEbK9xGChACMf1KaA0HZJDmHvUqoai7PF35owgLEQzxPy0Q +lG/+4jSHg9bP5Rs1bdID4bANqKCqRieCNqcVtgimQlRXtpla4gt5kNdXElE1GYhB +aCXUNxeEFfsBctyV3lImIJgm4nb1J2/6ADtKYdkNy1GTKv0WBpanI5ojSP5RvbbE +sLFUzt5sQa0WZ37b/TjNuThOssFgy50X31ieemKyJo90lZvkWx3SD92YHJtZuSPT +MaCm/zjdzyBP6VhWOmfD0faZmZ26NraAL4hHT4a/RDqA5Dccprrql5gR0IRiR2Qe +qu5AvzSxnI9O4fKSTx+O856X3vOmeWqJcU9LJxdI/uz0UA9PSX3MReO9ekDFQdxh +VicGaeVyQYHTtgGJoC86cnn+OjC/QezHYj6RS8fZMXZC+fc8Y+wmjHMMfRod6qh8 +h6jCJ3zhM0EPz8/8AKAigJ5Kp28AsEFFtyLKaEjFQqKu3R3y4G5OBVixwJAWKqQ9 +EEC+j2Jjg6mcgn0tAumDMHzLJ8n9HmYAsC7TIS+OMxZsmO0QqAfWzJPP29FpHOTK +yeC2nOnOcXHebD8WpHk= +-----END CERTIFICATE----- + +# Issuer: CN=Trustwave Global ECC P256 Certification Authority O=Trustwave Holdings, Inc. +# Subject: CN=Trustwave Global ECC P256 Certification Authority O=Trustwave Holdings, Inc. +# Label: "Trustwave Global ECC P256 Certification Authority" +# Serial: 4151900041497450638097112925 +# MD5 Fingerprint: 5b:44:e3:8d:5d:36:86:26:e8:0d:05:d2:59:a7:83:54 +# SHA1 Fingerprint: b4:90:82:dd:45:0c:be:8b:5b:b1:66:d3:e2:a4:08:26:cd:ed:42:cf +# SHA256 Fingerprint: 94:5b:bc:82:5e:a5:54:f4:89:d1:fd:51:a7:3d:df:2e:a6:24:ac:70:19:a0:52:05:22:5c:22:a7:8c:cf:a8:b4 +-----BEGIN CERTIFICATE----- +MIICYDCCAgegAwIBAgIMDWpfCD8oXD5Rld9dMAoGCCqGSM49BAMCMIGRMQswCQYD +VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAf +BgNVBAoTGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3 +YXZlIEdsb2JhbCBFQ0MgUDI1NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0x +NzA4MjMxOTM1MTBaFw00MjA4MjMxOTM1MTBaMIGRMQswCQYDVQQGEwJVUzERMA8G +A1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0 +d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBF +Q0MgUDI1NiBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTBZMBMGByqGSM49AgEGCCqG +SM49AwEHA0IABH77bOYj43MyCMpg5lOcunSNGLB4kFKA3TjASh3RqMyTpJcGOMoN +FWLGjgEqZZ2q3zSRLoHB5DOSMcT9CTqmP62jQzBBMA8GA1UdEwEB/wQFMAMBAf8w +DwYDVR0PAQH/BAUDAwcGADAdBgNVHQ4EFgQUo0EGrJBt0UrrdaVKEJmzsaGLSvcw +CgYIKoZIzj0EAwIDRwAwRAIgB+ZU2g6gWrKuEZ+Hxbb/ad4lvvigtwjzRM4q3wgh +DDcCIC0mA6AFvWvR9lz4ZcyGbbOcNEhjhAnFjXca4syc4XR7 +-----END CERTIFICATE----- + +# Issuer: CN=Trustwave Global ECC P384 Certification Authority O=Trustwave Holdings, Inc. +# Subject: CN=Trustwave Global ECC P384 Certification Authority O=Trustwave Holdings, Inc. +# Label: "Trustwave Global ECC P384 Certification Authority" +# Serial: 2704997926503831671788816187 +# MD5 Fingerprint: ea:cf:60:c4:3b:b9:15:29:40:a1:97:ed:78:27:93:d6 +# SHA1 Fingerprint: e7:f3:a3:c8:cf:6f:c3:04:2e:6d:0e:67:32:c5:9e:68:95:0d:5e:d2 +# SHA256 Fingerprint: 55:90:38:59:c8:c0:c3:eb:b8:75:9e:ce:4e:25:57:22:5f:f5:75:8b:bd:38:eb:d4:82:76:60:1e:1b:d5:80:97 +-----BEGIN CERTIFICATE----- +MIICnTCCAiSgAwIBAgIMCL2Fl2yZJ6SAaEc7MAoGCCqGSM49BAMDMIGRMQswCQYD +VQQGEwJVUzERMA8GA1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAf +BgNVBAoTGFRydXN0d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3 +YXZlIEdsb2JhbCBFQ0MgUDM4NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0x +NzA4MjMxOTM2NDNaFw00MjA4MjMxOTM2NDNaMIGRMQswCQYDVQQGEwJVUzERMA8G +A1UECBMISWxsaW5vaXMxEDAOBgNVBAcTB0NoaWNhZ28xITAfBgNVBAoTGFRydXN0 +d2F2ZSBIb2xkaW5ncywgSW5jLjE6MDgGA1UEAxMxVHJ1c3R3YXZlIEdsb2JhbCBF +Q0MgUDM4NCBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTB2MBAGByqGSM49AgEGBSuB +BAAiA2IABGvaDXU1CDFHBa5FmVXxERMuSvgQMSOjfoPTfygIOiYaOs+Xgh+AtycJ +j9GOMMQKmw6sWASr9zZ9lCOkmwqKi6vr/TklZvFe/oyujUF5nQlgziip04pt89ZF +1PKYhDhloKNDMEEwDwYDVR0TAQH/BAUwAwEB/zAPBgNVHQ8BAf8EBQMDBwYAMB0G +A1UdDgQWBBRVqYSJ0sEyvRjLbKYHTsjnnb6CkDAKBggqhkjOPQQDAwNnADBkAjA3 +AZKXRRJ+oPM+rRk6ct30UJMDEr5E0k9BpIycnR+j9sKS50gU/k6bpZFXrsY3crsC +MGclCrEMXu6pY5Jv5ZAL/mYiykf9ijH3g/56vxC+GCsej/YpHpRZ744hN8tRmKVu +Sw== +-----END CERTIFICATE----- diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index c3d93e275bd..f08348b9fc8 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -12,7 +12,7 @@ pep517==0.9.1 progress==1.5 pyparsing==2.4.7 requests==2.25.0 - certifi==2020.06.20 + certifi==2020.11.8 chardet==3.0.4 idna==2.10 urllib3==1.25.9 From e76b1ddeaa3694ae87df577c751ba7d9d36b4771 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Thu, 19 Nov 2020 14:20:52 +0000 Subject: [PATCH 2720/3170] Upgrade urllib3 to 1.26.2 --- news/urllib3.vendor.rst | 1 + src/pip/_vendor/urllib3/__init__.py | 21 +- src/pip/_vendor/urllib3/_collections.py | 9 +- src/pip/_vendor/urllib3/_version.py | 2 + src/pip/_vendor/urllib3/connection.py | 198 ++++++++++++---- src/pip/_vendor/urllib3/connectionpool.py | 152 +++++++----- .../contrib/_securetransport/bindings.py | 62 +++-- .../contrib/_securetransport/low_level.py | 74 +++++- src/pip/_vendor/urllib3/contrib/appengine.py | 10 +- src/pip/_vendor/urllib3/contrib/ntlmpool.py | 2 +- src/pip/_vendor/urllib3/contrib/pyopenssl.py | 34 +-- .../urllib3/contrib/securetransport.py | 78 ++++++- src/pip/_vendor/urllib3/contrib/socks.py | 20 +- src/pip/_vendor/urllib3/exceptions.py | 141 +++++++---- src/pip/_vendor/urllib3/fields.py | 5 +- src/pip/_vendor/urllib3/filepost.py | 4 +- .../urllib3/packages/backports/makefile.py | 1 - .../packages/ssl_match_hostname/__init__.py | 7 +- src/pip/_vendor/urllib3/poolmanager.py | 112 ++++++--- src/pip/_vendor/urllib3/request.py | 9 +- src/pip/_vendor/urllib3/response.py | 70 +++--- src/pip/_vendor/urllib3/util/__init__.py | 17 +- src/pip/_vendor/urllib3/util/connection.py | 18 +- src/pip/_vendor/urllib3/util/proxy.py | 56 +++++ src/pip/_vendor/urllib3/util/queue.py | 1 + src/pip/_vendor/urllib3/util/request.py | 10 +- src/pip/_vendor/urllib3/util/response.py | 31 ++- src/pip/_vendor/urllib3/util/retry.py | 212 ++++++++++++++--- src/pip/_vendor/urllib3/util/ssl_.py | 114 ++++++--- src/pip/_vendor/urllib3/util/ssltransport.py | 221 ++++++++++++++++++ src/pip/_vendor/urllib3/util/timeout.py | 49 ++-- src/pip/_vendor/urllib3/util/url.py | 2 +- src/pip/_vendor/urllib3/util/wait.py | 6 +- src/pip/_vendor/vendor.txt | 2 +- 34 files changed, 1344 insertions(+), 407 deletions(-) create mode 100644 news/urllib3.vendor.rst create mode 100644 src/pip/_vendor/urllib3/_version.py create mode 100644 src/pip/_vendor/urllib3/util/proxy.py create mode 100644 src/pip/_vendor/urllib3/util/ssltransport.py diff --git a/news/urllib3.vendor.rst b/news/urllib3.vendor.rst new file mode 100644 index 00000000000..10e1e7b45f5 --- /dev/null +++ b/news/urllib3.vendor.rst @@ -0,0 +1 @@ +Upgrade urllib3 to 1.26.2 diff --git a/src/pip/_vendor/urllib3/__init__.py b/src/pip/_vendor/urllib3/__init__.py index 667e9bce9e3..fe86b59d782 100644 --- a/src/pip/_vendor/urllib3/__init__.py +++ b/src/pip/_vendor/urllib3/__init__.py @@ -1,28 +1,27 @@ """ -urllib3 - Thread-safe connection pooling and re-using. +Python HTTP library with thread-safe connection pooling, file post support, user friendly, and more """ from __future__ import absolute_import -import warnings -from .connectionpool import HTTPConnectionPool, HTTPSConnectionPool, connection_from_url +# Set default logging handler to avoid "No handler found" warnings. +import logging +import warnings +from logging import NullHandler from . import exceptions +from ._version import __version__ +from .connectionpool import HTTPConnectionPool, HTTPSConnectionPool, connection_from_url from .filepost import encode_multipart_formdata from .poolmanager import PoolManager, ProxyManager, proxy_from_url from .response import HTTPResponse from .util.request import make_headers -from .util.url import get_host -from .util.timeout import Timeout from .util.retry import Retry - - -# Set default logging handler to avoid "No handler found" warnings. -import logging -from logging import NullHandler +from .util.timeout import Timeout +from .util.url import get_host __author__ = "Andrey Petrov (andrey.petrov@shazow.net)" __license__ = "MIT" -__version__ = "1.25.9" +__version__ = __version__ __all__ = ( "HTTPConnectionPool", diff --git a/src/pip/_vendor/urllib3/_collections.py b/src/pip/_vendor/urllib3/_collections.py index 019d1511d56..da9857e986d 100644 --- a/src/pip/_vendor/urllib3/_collections.py +++ b/src/pip/_vendor/urllib3/_collections.py @@ -17,9 +17,10 @@ def __exit__(self, exc_type, exc_value, traceback): from collections import OrderedDict -from .exceptions import InvalidHeader -from .packages.six import iterkeys, itervalues, PY3 +from .exceptions import InvalidHeader +from .packages import six +from .packages.six import iterkeys, itervalues __all__ = ["RecentlyUsedContainer", "HTTPHeaderDict"] @@ -174,7 +175,7 @@ def __eq__(self, other): def __ne__(self, other): return not self.__eq__(other) - if not PY3: # Python 2 + if six.PY2: # Python 2 iterkeys = MutableMapping.iterkeys itervalues = MutableMapping.itervalues @@ -190,7 +191,7 @@ def __iter__(self): def pop(self, key, default=__marker): """D.pop(k[,d]) -> v, remove specified key and return the corresponding value. - If key is not found, d is returned if given, otherwise KeyError is raised. + If key is not found, d is returned if given, otherwise KeyError is raised. """ # Using the MutableMapping function directly fails due to the private marker. # Using ordinary dict.pop would expose the internal structures. diff --git a/src/pip/_vendor/urllib3/_version.py b/src/pip/_vendor/urllib3/_version.py new file mode 100644 index 00000000000..2dba29e3fbe --- /dev/null +++ b/src/pip/_vendor/urllib3/_version.py @@ -0,0 +1,2 @@ +# This file is protected via CODEOWNERS +__version__ = "1.26.2" diff --git a/src/pip/_vendor/urllib3/connection.py b/src/pip/_vendor/urllib3/connection.py index 6da1cf4b6dc..660d679c361 100644 --- a/src/pip/_vendor/urllib3/connection.py +++ b/src/pip/_vendor/urllib3/connection.py @@ -1,14 +1,18 @@ from __future__ import absolute_import -import re + import datetime import logging import os +import re import socket -from socket import error as SocketError, timeout as SocketTimeout import warnings +from socket import error as SocketError +from socket import timeout as SocketTimeout + from .packages import six from .packages.six.moves.http_client import HTTPConnection as _HTTPConnection from .packages.six.moves.http_client import HTTPException # noqa: F401 +from .util.proxy import create_proxy_ssl_context try: # Compiled with SSL? import ssl @@ -30,27 +34,33 @@ class ConnectionError(Exception): pass +try: # Python 3: + # Not a no-op, we're adding this to the namespace so it can be imported. + BrokenPipeError = BrokenPipeError +except NameError: # Python 2: + + class BrokenPipeError(Exception): + pass + + +from ._collections import HTTPHeaderDict # noqa (historical, removed in v2) +from ._version import __version__ from .exceptions import ( - NewConnectionError, ConnectTimeoutError, + NewConnectionError, SubjectAltNameWarning, SystemTimeWarning, ) -from .packages.ssl_match_hostname import match_hostname, CertificateError - +from .packages.ssl_match_hostname import CertificateError, match_hostname +from .util import SKIP_HEADER, SKIPPABLE_HEADERS, connection from .util.ssl_ import ( - resolve_cert_reqs, - resolve_ssl_version, assert_fingerprint, create_urllib3_context, + resolve_cert_reqs, + resolve_ssl_version, ssl_wrap_socket, ) - -from .util import connection - -from ._collections import HTTPHeaderDict - log = logging.getLogger(__name__) port_by_scheme = {"http": 80, "https": 443} @@ -62,34 +72,30 @@ class ConnectionError(Exception): _CONTAINS_CONTROL_CHAR_RE = re.compile(r"[^-!#$%&'*+.^_`|~0-9a-zA-Z]") -class DummyConnection(object): - """Used to detect a failed ConnectionCls import.""" - - pass - - class HTTPConnection(_HTTPConnection, object): """ - Based on httplib.HTTPConnection but provides an extra constructor + Based on :class:`http.client.HTTPConnection` but provides an extra constructor backwards-compatibility layer between older and newer Pythons. Additional keyword parameters are used to configure attributes of the connection. Accepted parameters include: - - ``strict``: See the documentation on :class:`urllib3.connectionpool.HTTPConnectionPool` - - ``source_address``: Set the source address for the current connection. - - ``socket_options``: Set specific options on the underlying socket. If not specified, then - defaults are loaded from ``HTTPConnection.default_socket_options`` which includes disabling - Nagle's algorithm (sets TCP_NODELAY to 1) unless the connection is behind a proxy. + - ``strict``: See the documentation on :class:`urllib3.connectionpool.HTTPConnectionPool` + - ``source_address``: Set the source address for the current connection. + - ``socket_options``: Set specific options on the underlying socket. If not specified, then + defaults are loaded from ``HTTPConnection.default_socket_options`` which includes disabling + Nagle's algorithm (sets TCP_NODELAY to 1) unless the connection is behind a proxy. - For example, if you wish to enable TCP Keep Alive in addition to the defaults, - you might pass:: + For example, if you wish to enable TCP Keep Alive in addition to the defaults, + you might pass: - HTTPConnection.default_socket_options + [ - (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), - ] + .. code-block:: python - Or you may want to disable the defaults by passing an empty list (e.g., ``[]``). + HTTPConnection.default_socket_options + [ + (socket.SOL_SOCKET, socket.SO_KEEPALIVE, 1), + ] + + Or you may want to disable the defaults by passing an empty list (e.g., ``[]``). """ default_port = port_by_scheme["http"] @@ -112,6 +118,10 @@ def __init__(self, *args, **kw): #: provided, we use the default options. self.socket_options = kw.pop("socket_options", self.default_socket_options) + # Proxy options provided by the user. + self.proxy = kw.pop("proxy", None) + self.proxy_config = kw.pop("proxy_config", None) + _HTTPConnection.__init__(self, *args, **kw) @property @@ -144,7 +154,7 @@ def host(self, value): self._dns_host = value def _new_conn(self): - """ Establish a socket connection and set nodelay settings on it. + """Establish a socket connection and set nodelay settings on it. :return: New socket connection. """ @@ -174,10 +184,13 @@ def _new_conn(self): return conn + def _is_using_tunnel(self): + # Google App Engine's httplib does not define _tunnel_host + return getattr(self, "_tunnel_host", None) + def _prepare_conn(self, conn): self.sock = conn - # Google App Engine's httplib does not define _tunnel_host - if getattr(self, "_tunnel_host", None): + if self._is_using_tunnel(): # TODO: Fix tunnel so it doesn't depend on self.sock state. self._tunnel() # Mark this connection as not reusable @@ -188,7 +201,9 @@ def connect(self): self._prepare_conn(conn) def putrequest(self, method, url, *args, **kwargs): - """Send a request to the server""" + """""" + # Empty docstring because the indentation of CPython's implementation + # is broken but we don't want this method in our documentation. match = _CONTAINS_CONTROL_CHAR_RE.search(method) if match: raise ValueError( @@ -198,17 +213,40 @@ def putrequest(self, method, url, *args, **kwargs): return _HTTPConnection.putrequest(self, method, url, *args, **kwargs) + def putheader(self, header, *values): + """""" + if SKIP_HEADER not in values: + _HTTPConnection.putheader(self, header, *values) + elif six.ensure_str(header.lower()) not in SKIPPABLE_HEADERS: + raise ValueError( + "urllib3.util.SKIP_HEADER only supports '%s'" + % ("', '".join(map(str.title, sorted(SKIPPABLE_HEADERS))),) + ) + + def request(self, method, url, body=None, headers=None): + if headers is None: + headers = {} + else: + # Avoid modifying the headers passed into .request() + headers = headers.copy() + if "user-agent" not in (six.ensure_str(k.lower()) for k in headers): + headers["User-Agent"] = _get_default_user_agent() + super(HTTPConnection, self).request(method, url, body=body, headers=headers) + def request_chunked(self, method, url, body=None, headers=None): """ Alternative to the common request method, which sends the body with chunked encoding and not as one block """ - headers = HTTPHeaderDict(headers if headers is not None else {}) - skip_accept_encoding = "accept-encoding" in headers - skip_host = "host" in headers + headers = headers or {} + header_keys = set([six.ensure_str(k.lower()) for k in headers]) + skip_accept_encoding = "accept-encoding" in header_keys + skip_host = "host" in header_keys self.putrequest( method, url, skip_accept_encoding=skip_accept_encoding, skip_host=skip_host ) + if "user-agent" not in header_keys: + self.putheader("User-Agent", _get_default_user_agent()) for header, value in headers.items(): self.putheader(header, value) if "transfer-encoding" not in headers: @@ -225,16 +263,22 @@ def request_chunked(self, method, url, body=None, headers=None): if not isinstance(chunk, bytes): chunk = chunk.encode("utf8") len_str = hex(len(chunk))[2:] - self.send(len_str.encode("utf-8")) - self.send(b"\r\n") - self.send(chunk) - self.send(b"\r\n") + to_send = bytearray(len_str.encode()) + to_send += b"\r\n" + to_send += chunk + to_send += b"\r\n" + self.send(to_send) # After the if clause, to always have a closed body self.send(b"0\r\n\r\n") class HTTPSConnection(HTTPConnection): + """ + Many of the parameters to this constructor are passed to the underlying SSL + socket by means of :py:func:`urllib3.util.ssl_wrap_socket`. + """ + default_port = port_by_scheme["https"] cert_reqs = None @@ -243,6 +287,7 @@ class HTTPSConnection(HTTPConnection): ca_cert_data = None ssl_version = None assert_fingerprint = None + tls_in_tls_required = False def __init__( self, @@ -307,10 +352,15 @@ def connect(self): # Add certificate verification conn = self._new_conn() hostname = self.host + tls_in_tls = False + + if self._is_using_tunnel(): + if self.tls_in_tls_required: + conn = self._connect_tls_proxy(hostname, conn) + tls_in_tls = True - # Google App Engine's httplib does not define _tunnel_host - if getattr(self, "_tunnel_host", None): self.sock = conn + # Calls self._set_hostport(), so self.host is # self._tunnel_host below. self._tunnel() @@ -368,8 +418,26 @@ def connect(self): ca_cert_data=self.ca_cert_data, server_hostname=server_hostname, ssl_context=context, + tls_in_tls=tls_in_tls, ) + # If we're using all defaults and the connection + # is TLSv1 or TLSv1.1 we throw a DeprecationWarning + # for the host. + if ( + default_ssl_context + and self.ssl_version is None + and hasattr(self.sock, "version") + and self.sock.version() in {"TLSv1", "TLSv1.1"} + ): + warnings.warn( + "Negotiating TLSv1/TLSv1.1 by default is deprecated " + "and will be disabled in urllib3 v2.0.0. Connecting to " + "'%s' with '%s' can be enabled by explicitly opting-in " + "with 'ssl_version'" % (self.host, self.sock.version()), + DeprecationWarning, + ) + if self.assert_fingerprint: assert_fingerprint( self.sock.getpeercert(binary_form=True), self.assert_fingerprint @@ -400,6 +468,40 @@ def connect(self): or self.assert_fingerprint is not None ) + def _connect_tls_proxy(self, hostname, conn): + """ + Establish a TLS connection to the proxy using the provided SSL context. + """ + proxy_config = self.proxy_config + ssl_context = proxy_config.ssl_context + if ssl_context: + # If the user provided a proxy context, we assume CA and client + # certificates have already been set + return ssl_wrap_socket( + sock=conn, + server_hostname=hostname, + ssl_context=ssl_context, + ) + + ssl_context = create_proxy_ssl_context( + self.ssl_version, + self.cert_reqs, + self.ca_certs, + self.ca_cert_dir, + self.ca_cert_data, + ) + + # If no cert was provided, use only the default options for server + # certificate validation + return ssl_wrap_socket( + sock=conn, + ca_certs=self.ca_certs, + ca_cert_dir=self.ca_cert_dir, + ca_cert_data=self.ca_cert_data, + server_hostname=hostname, + ssl_context=ssl_context, + ) + def _match_hostname(cert, asserted_hostname): try: @@ -416,6 +518,16 @@ def _match_hostname(cert, asserted_hostname): raise +def _get_default_user_agent(): + return "python-urllib3/%s" % __version__ + + +class DummyConnection(object): + """Used to detect a failed ConnectionCls import.""" + + pass + + if not ssl: HTTPSConnection = DummyConnection # noqa: F811 diff --git a/src/pip/_vendor/urllib3/connectionpool.py b/src/pip/_vendor/urllib3/connectionpool.py index 5f044dbd90f..4708c5bfc78 100644 --- a/src/pip/_vendor/urllib3/connectionpool.py +++ b/src/pip/_vendor/urllib3/connectionpool.py @@ -1,57 +1,53 @@ from __future__ import absolute_import + import errno import logging +import socket import sys import warnings +from socket import error as SocketError +from socket import timeout as SocketTimeout -from socket import error as SocketError, timeout as SocketTimeout -import socket - - +from .connection import ( + BaseSSLError, + BrokenPipeError, + DummyConnection, + HTTPConnection, + HTTPException, + HTTPSConnection, + VerifiedHTTPSConnection, + port_by_scheme, +) from .exceptions import ( ClosedPoolError, - ProtocolError, EmptyPoolError, HeaderParsingError, HostChangedError, + InsecureRequestWarning, LocationValueError, MaxRetryError, + NewConnectionError, + ProtocolError, ProxyError, ReadTimeoutError, SSLError, TimeoutError, - InsecureRequestWarning, - NewConnectionError, ) -from .packages.ssl_match_hostname import CertificateError from .packages import six from .packages.six.moves import queue -from .connection import ( - port_by_scheme, - DummyConnection, - HTTPConnection, - HTTPSConnection, - VerifiedHTTPSConnection, - HTTPException, - BaseSSLError, -) +from .packages.ssl_match_hostname import CertificateError from .request import RequestMethods from .response import HTTPResponse - from .util.connection import is_connection_dropped +from .util.proxy import connection_requires_http_tunnel +from .util.queue import LifoQueue from .util.request import set_file_position from .util.response import assert_header_parsing from .util.retry import Retry from .util.timeout import Timeout -from .util.url import ( - get_host, - parse_url, - Url, - _normalize_host as normalize_host, - _encode_target, -) -from .util.queue import LifoQueue - +from .util.url import Url, _encode_target +from .util.url import _normalize_host as normalize_host +from .util.url import get_host, parse_url xrange = six.moves.xrange @@ -111,16 +107,16 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods): :param host: Host used for this HTTP Connection (e.g. "localhost"), passed into - :class:`httplib.HTTPConnection`. + :class:`http.client.HTTPConnection`. :param port: Port used for this HTTP Connection (None is equivalent to 80), passed - into :class:`httplib.HTTPConnection`. + into :class:`http.client.HTTPConnection`. :param strict: Causes BadStatusLine to be raised if the status line can't be parsed as a valid HTTP/1.0 or 1.1 status line, passed into - :class:`httplib.HTTPConnection`. + :class:`http.client.HTTPConnection`. .. note:: Only works in Python 2. This parameter is ignored in Python 3. @@ -154,11 +150,11 @@ class HTTPConnectionPool(ConnectionPool, RequestMethods): :param _proxy: Parsed proxy URL, should not be used directly, instead, see - :class:`urllib3.connectionpool.ProxyManager`" + :class:`urllib3.ProxyManager` :param _proxy_headers: A dictionary with proxy headers, should not be used directly, - instead, see :class:`urllib3.connectionpool.ProxyManager`" + instead, see :class:`urllib3.ProxyManager` :param \\**conn_kw: Additional parameters are used to create fresh :class:`urllib3.connection.HTTPConnection`, @@ -181,6 +177,7 @@ def __init__( retries=None, _proxy=None, _proxy_headers=None, + _proxy_config=None, **conn_kw ): ConnectionPool.__init__(self, host, port) @@ -202,6 +199,7 @@ def __init__( self.proxy = _proxy self.proxy_headers = _proxy_headers or {} + self.proxy_config = _proxy_config # Fill the queue up so that doing get() on it will block properly for _ in xrange(maxsize): @@ -218,6 +216,9 @@ def __init__( # list. self.conn_kw.setdefault("socket_options", []) + self.conn_kw["proxy"] = self.proxy + self.conn_kw["proxy_config"] = self.proxy_config + def _new_conn(self): """ Return a fresh :class:`HTTPConnection`. @@ -272,7 +273,7 @@ def _get_conn(self, timeout=None): conn.close() if getattr(conn, "auto_open", 1) == 0: # This is a proxied connection that has been mutated by - # httplib._tunnel() and cannot be reused (since it would + # http.client._tunnel() and cannot be reused (since it would # attempt to bypass the proxy) conn = None @@ -384,12 +385,30 @@ def _make_request( self._raise_timeout(err=e, url=url, timeout_value=conn.timeout) raise - # conn.request() calls httplib.*.request, not the method in + # conn.request() calls http.client.*.request, not the method in # urllib3.request. It also calls makefile (recv) on the socket. - if chunked: - conn.request_chunked(method, url, **httplib_request_kw) - else: - conn.request(method, url, **httplib_request_kw) + try: + if chunked: + conn.request_chunked(method, url, **httplib_request_kw) + else: + conn.request(method, url, **httplib_request_kw) + + # We are swallowing BrokenPipeError (errno.EPIPE) since the server is + # legitimately able to close the connection after sending a valid response. + # With this behaviour, the received response is still readable. + except BrokenPipeError: + # Python 3 + pass + except IOError as e: + # Python 2 and macOS/Linux + # EPIPE and ESHUTDOWN are BrokenPipeError on Python 2, and EPROTOTYPE is needed on macOS + # https://erickt.github.io/blog/2014/11/19/adventures-in-debugging-a-potential-osx-kernel-bug/ + if e.errno not in { + errno.EPIPE, + errno.ESHUTDOWN, + errno.EPROTOTYPE, + }: + raise # Reset the timeout for the recv() on the socket read_timeout = timeout_obj.read_timeout @@ -532,10 +551,12 @@ def urlopen( :param method: HTTP request method (such as GET, POST, PUT, etc.) + :param url: + The URL to perform the request on. + :param body: - Data to send in the request body (useful for creating - POST requests, see HTTPConnectionPool.post_url for - more convenience). + Data to send in the request body, either :class:`str`, :class:`bytes`, + an iterable of :class:`str`/:class:`bytes`, or a file-like object. :param headers: Dictionary of custom headers to send, such as User-Agent, @@ -565,7 +586,7 @@ def urlopen( :param assert_same_host: If ``True``, will make sure that the host of the pool requests is - consistent else will raise HostChangedError. When False, you can + consistent else will raise HostChangedError. When ``False``, you can use the pool on an HTTP proxy and request foreign hosts. :param timeout: @@ -602,6 +623,10 @@ def urlopen( Additional parameters are passed to :meth:`urllib3.response.HTTPResponse.from_httplib` """ + + parsed_url = parse_url(url) + destination_scheme = parsed_url.scheme + if headers is None: headers = self.headers @@ -619,7 +644,7 @@ def urlopen( if url.startswith("/"): url = six.ensure_str(_encode_target(url)) else: - url = six.ensure_str(parse_url(url).url) + url = six.ensure_str(parsed_url.url) conn = None @@ -634,10 +659,14 @@ def urlopen( # [1] <https://github.com/urllib3/urllib3/issues/651> release_this_conn = release_conn - # Merge the proxy headers. Only do this in HTTP. We have to copy the - # headers dict so we can safely change it without those changes being - # reflected in anyone else's copy. - if self.scheme == "http": + http_tunnel_required = connection_requires_http_tunnel( + self.proxy, self.proxy_config, destination_scheme + ) + + # Merge the proxy headers. Only done when not using HTTP CONNECT. We + # have to copy the headers dict so we can safely change it without those + # changes being reflected in anyone else's copy. + if not http_tunnel_required: headers = headers.copy() headers.update(self.proxy_headers) @@ -663,7 +692,7 @@ def urlopen( is_new_proxy_conn = self.proxy is not None and not getattr( conn, "sock", None ) - if is_new_proxy_conn: + if is_new_proxy_conn and http_tunnel_required: self._prepare_proxy(conn) # Make the request on the httplib connection object. @@ -698,9 +727,11 @@ def urlopen( # Everything went great! clean_exit = True - except queue.Empty: - # Timed out by queue. - raise EmptyPoolError(self, "No pool connections are available.") + except EmptyPoolError: + # Didn't get a connection from the pool, no need to clean up + clean_exit = True + release_this_conn = False + raise except ( TimeoutError, @@ -835,11 +866,7 @@ class HTTPSConnectionPool(HTTPConnectionPool): """ Same as :class:`.HTTPConnectionPool`, but HTTPS. - When Python is compiled with the :mod:`ssl` module, then - :class:`.VerifiedHTTPSConnection` is used, which *can* verify certificates, - instead of :class:`.HTTPSConnection`. - - :class:`.VerifiedHTTPSConnection` uses one of ``assert_fingerprint``, + :class:`.HTTPSConnection` uses one of ``assert_fingerprint``, ``assert_hostname`` and ``host`` in this order to verify connections. If ``assert_hostname`` is False, no verification is done. @@ -923,15 +950,22 @@ def _prepare_conn(self, conn): def _prepare_proxy(self, conn): """ - Establish tunnel connection early, because otherwise httplib - would improperly set Host: header to proxy's IP:port. + Establishes a tunnel connection through HTTP CONNECT. + + Tunnel connection is established early because otherwise httplib would + improperly set Host: header to proxy's IP:port. """ + conn.set_tunnel(self._proxy_host, self.port, self.proxy_headers) + + if self.proxy.scheme == "https": + conn.tls_in_tls_required = True + conn.connect() def _new_conn(self): """ - Return a fresh :class:`httplib.HTTPSConnection`. + Return a fresh :class:`http.client.HTTPSConnection`. """ self.num_connections += 1 log.debug( diff --git a/src/pip/_vendor/urllib3/contrib/_securetransport/bindings.py b/src/pip/_vendor/urllib3/contrib/_securetransport/bindings.py index d9b67333188..42526be7f55 100644 --- a/src/pip/_vendor/urllib3/contrib/_securetransport/bindings.py +++ b/src/pip/_vendor/urllib3/contrib/_securetransport/bindings.py @@ -32,30 +32,26 @@ from __future__ import absolute_import import platform -from ctypes.util import find_library from ctypes import ( - c_void_p, - c_int32, + CDLL, + CFUNCTYPE, + POINTER, + c_bool, + c_byte, c_char_p, + c_int32, + c_long, c_size_t, - c_byte, c_uint32, c_ulong, - c_long, - c_bool, + c_void_p, ) -from ctypes import CDLL, POINTER, CFUNCTYPE - - -security_path = find_library("Security") -if not security_path: - raise ImportError("The library Security could not be found") - +from ctypes.util import find_library -core_foundation_path = find_library("CoreFoundation") -if not core_foundation_path: - raise ImportError("The library CoreFoundation could not be found") +from pip._vendor.urllib3.packages.six import raise_from +if platform.system() != "Darwin": + raise ImportError("Only macOS is supported") version = platform.mac_ver()[0] version_info = tuple(map(int, version.split("."))) @@ -65,8 +61,31 @@ % (version_info[0], version_info[1]) ) -Security = CDLL(security_path, use_errno=True) -CoreFoundation = CDLL(core_foundation_path, use_errno=True) + +def load_cdll(name, macos10_16_path): + """Loads a CDLL by name, falling back to known path on 10.16+""" + try: + # Big Sur is technically 11 but we use 10.16 due to the Big Sur + # beta being labeled as 10.16. + if version_info >= (10, 16): + path = macos10_16_path + else: + path = find_library(name) + if not path: + raise OSError # Caught and reraised as 'ImportError' + return CDLL(path, use_errno=True) + except OSError: + raise_from(ImportError("The library %s failed to load" % name), None) + + +Security = load_cdll( + "Security", "/System/Library/Frameworks/Security.framework/Security" +) +CoreFoundation = load_cdll( + "CoreFoundation", + "/System/Library/Frameworks/CoreFoundation.framework/CoreFoundation", +) + Boolean = c_bool CFIndex = c_long @@ -276,6 +295,13 @@ Security.SSLSetProtocolVersionMax.argtypes = [SSLContextRef, SSLProtocol] Security.SSLSetProtocolVersionMax.restype = OSStatus + try: + Security.SSLSetALPNProtocols.argtypes = [SSLContextRef, CFArrayRef] + Security.SSLSetALPNProtocols.restype = OSStatus + except AttributeError: + # Supported only in 10.12+ + pass + Security.SecCopyErrorMessageString.argtypes = [OSStatus, c_void_p] Security.SecCopyErrorMessageString.restype = CFStringRef diff --git a/src/pip/_vendor/urllib3/contrib/_securetransport/low_level.py b/src/pip/_vendor/urllib3/contrib/_securetransport/low_level.py index e60168cac14..ed8120190c0 100644 --- a/src/pip/_vendor/urllib3/contrib/_securetransport/low_level.py +++ b/src/pip/_vendor/urllib3/contrib/_securetransport/low_level.py @@ -10,13 +10,13 @@ import base64 import ctypes import itertools -import re import os +import re import ssl +import struct import tempfile -from .bindings import Security, CoreFoundation, CFConst - +from .bindings import CFConst, CoreFoundation, Security # This regular expression is used to grab PEM data out of a PEM bundle. _PEM_CERTS_RE = re.compile( @@ -56,6 +56,51 @@ def _cf_dictionary_from_tuples(tuples): ) +def _cfstr(py_bstr): + """ + Given a Python binary data, create a CFString. + The string must be CFReleased by the caller. + """ + c_str = ctypes.c_char_p(py_bstr) + cf_str = CoreFoundation.CFStringCreateWithCString( + CoreFoundation.kCFAllocatorDefault, + c_str, + CFConst.kCFStringEncodingUTF8, + ) + return cf_str + + +def _create_cfstring_array(lst): + """ + Given a list of Python binary data, create an associated CFMutableArray. + The array must be CFReleased by the caller. + + Raises an ssl.SSLError on failure. + """ + cf_arr = None + try: + cf_arr = CoreFoundation.CFArrayCreateMutable( + CoreFoundation.kCFAllocatorDefault, + 0, + ctypes.byref(CoreFoundation.kCFTypeArrayCallBacks), + ) + if not cf_arr: + raise MemoryError("Unable to allocate memory!") + for item in lst: + cf_str = _cfstr(item) + if not cf_str: + raise MemoryError("Unable to allocate memory!") + try: + CoreFoundation.CFArrayAppendValue(cf_arr, cf_str) + finally: + CoreFoundation.CFRelease(cf_str) + except BaseException as e: + if cf_arr: + CoreFoundation.CFRelease(cf_arr) + raise ssl.SSLError("Unable to allocate array: %s" % (e,)) + return cf_arr + + def _cf_string_to_unicode(value): """ Creates a Unicode string from a CFString object. Used entirely for error @@ -326,3 +371,26 @@ def _load_client_cert_chain(keychain, *paths): finally: for obj in itertools.chain(identities, certificates): CoreFoundation.CFRelease(obj) + + +TLS_PROTOCOL_VERSIONS = { + "SSLv2": (0, 2), + "SSLv3": (3, 0), + "TLSv1": (3, 1), + "TLSv1.1": (3, 2), + "TLSv1.2": (3, 3), +} + + +def _build_tls_unknown_ca_alert(version): + """ + Builds a TLS alert record for an unknown CA. + """ + ver_maj, ver_min = TLS_PROTOCOL_VERSIONS[version] + severity_fatal = 0x02 + description_unknown_ca = 0x30 + msg = struct.pack(">BB", severity_fatal, description_unknown_ca) + msg_len = len(msg) + record_type_alert = 0x15 + record = struct.pack(">BBBH", record_type_alert, ver_maj, ver_min, msg_len) + msg + return record diff --git a/src/pip/_vendor/urllib3/contrib/appengine.py b/src/pip/_vendor/urllib3/contrib/appengine.py index d09d2be645a..b9d2a6907c4 100644 --- a/src/pip/_vendor/urllib3/contrib/appengine.py +++ b/src/pip/_vendor/urllib3/contrib/appengine.py @@ -39,24 +39,24 @@ """ from __future__ import absolute_import + import io import logging import warnings -from ..packages.six.moves.urllib.parse import urljoin from ..exceptions import ( HTTPError, HTTPWarning, MaxRetryError, ProtocolError, - TimeoutError, SSLError, + TimeoutError, ) - +from ..packages.six.moves.urllib.parse import urljoin from ..request import RequestMethods from ..response import HTTPResponse -from ..util.timeout import Timeout from ..util.retry import Retry +from ..util.timeout import Timeout from . import _appengine_environ try: @@ -90,7 +90,7 @@ class AppEngineManager(RequestMethods): * If you attempt to use this on App Engine Flexible, as full socket support is available. * If a request size is more than 10 megabytes. - * If a response size is more than 32 megabtyes. + * If a response size is more than 32 megabytes. * If you use an unsupported request method such as OPTIONS. Beyond those cases, it will raise normal urllib3 errors. diff --git a/src/pip/_vendor/urllib3/contrib/ntlmpool.py b/src/pip/_vendor/urllib3/contrib/ntlmpool.py index 1fd242a6e0d..b2df45dcf60 100644 --- a/src/pip/_vendor/urllib3/contrib/ntlmpool.py +++ b/src/pip/_vendor/urllib3/contrib/ntlmpool.py @@ -6,12 +6,12 @@ from __future__ import absolute_import from logging import getLogger + from ntlm import ntlm from .. import HTTPSConnectionPool from ..packages.six.moves.http_client import HTTPSConnection - log = getLogger(__name__) diff --git a/src/pip/_vendor/urllib3/contrib/pyopenssl.py b/src/pip/_vendor/urllib3/contrib/pyopenssl.py index d8fe0629c42..bc5c114fa7e 100644 --- a/src/pip/_vendor/urllib3/contrib/pyopenssl.py +++ b/src/pip/_vendor/urllib3/contrib/pyopenssl.py @@ -1,27 +1,31 @@ """ -SSL with SNI_-support for Python 2. Follow these instructions if you would -like to verify SSL certificates in Python 2. Note, the default libraries do +TLS with SNI_-support for Python 2. Follow these instructions if you would +like to verify TLS certificates in Python 2. Note, the default libraries do *not* do certificate checking; you need to do additional work to validate certificates yourself. This needs the following packages installed: -* pyOpenSSL (tested with 16.0.0) -* cryptography (minimum 1.3.4, from pyopenssl) -* idna (minimum 2.0, from cryptography) +* `pyOpenSSL`_ (tested with 16.0.0) +* `cryptography`_ (minimum 1.3.4, from pyopenssl) +* `idna`_ (minimum 2.0, from cryptography) However, pyopenssl depends on cryptography, which depends on idna, so while we use all three directly here we end up having relatively few packages required. You can install them with the following command: - pip install pyopenssl cryptography idna +.. code-block:: bash + + $ python -m pip install pyopenssl cryptography idna To activate certificate checking, call :func:`~urllib3.contrib.pyopenssl.inject_into_urllib3` from your Python code before you begin making HTTP requests. This can be done in a ``sitecustomize`` module, or at any other time before your application begins using ``urllib3``, -like this:: +like this: + +.. code-block:: python try: import urllib3.contrib.pyopenssl @@ -35,11 +39,11 @@ Activating this module also has the positive side effect of disabling SSL/TLS compression in Python 2 (see `CRIME attack`_). -If you want to configure the default list of supported cipher suites, you can -set the ``urllib3.contrib.pyopenssl.DEFAULT_SSL_CIPHER_LIST`` variable. - .. _sni: https://en.wikipedia.org/wiki/Server_Name_Indication .. _crime attack: https://en.wikipedia.org/wiki/CRIME_(security_exploit) +.. _pyopenssl: https://www.pyopenssl.org +.. _cryptography: https://cryptography.io +.. _idna: https://github.com/kjd/idna """ from __future__ import absolute_import @@ -56,8 +60,9 @@ class UnsupportedExtension(Exception): pass -from socket import timeout, error as SocketError from io import BytesIO +from socket import error as SocketError +from socket import timeout try: # Platform-specific: Python 2 from socket import _fileobject @@ -67,11 +72,10 @@ class UnsupportedExtension(Exception): import logging import ssl -from ..packages import six import sys from .. import util - +from ..packages import six __all__ = ["inject_into_urllib3", "extract_from_urllib3"] @@ -465,6 +469,10 @@ def load_cert_chain(self, certfile, keyfile=None, password=None): self._ctx.set_passwd_cb(lambda *_: password) self._ctx.use_privatekey_file(keyfile or certfile) + def set_alpn_protocols(self, protocols): + protocols = [six.ensure_binary(p) for p in protocols] + return self._ctx.set_alpn_protos(protocols) + def wrap_socket( self, sock, diff --git a/src/pip/_vendor/urllib3/contrib/securetransport.py b/src/pip/_vendor/urllib3/contrib/securetransport.py index a6b7e94ade5..8f058f5070b 100644 --- a/src/pip/_vendor/urllib3/contrib/securetransport.py +++ b/src/pip/_vendor/urllib3/contrib/securetransport.py @@ -29,6 +29,8 @@ that reason, this code should be considered to be covered both by urllib3's license and by oscrypto's: +.. code-block:: + Copyright (c) 2015-2016 Will Bond <will@wbond.net> Permission is hereby granted, free of charge, to any person obtaining a @@ -58,16 +60,21 @@ import shutil import socket import ssl +import struct import threading import weakref +from pip._vendor import six + from .. import util -from ._securetransport.bindings import Security, SecurityConst, CoreFoundation +from ._securetransport.bindings import CoreFoundation, Security, SecurityConst from ._securetransport.low_level import ( _assert_no_error, + _build_tls_unknown_ca_alert, _cert_array_from_pem, - _temporary_keychain, + _create_cfstring_array, _load_client_cert_chain, + _temporary_keychain, ) try: # Platform-specific: Python 2 @@ -374,16 +381,55 @@ def _set_ciphers(self): ) _assert_no_error(result) + def _set_alpn_protocols(self, protocols): + """ + Sets up the ALPN protocols on the context. + """ + if not protocols: + return + protocols_arr = _create_cfstring_array(protocols) + try: + result = Security.SSLSetALPNProtocols(self.context, protocols_arr) + _assert_no_error(result) + finally: + CoreFoundation.CFRelease(protocols_arr) + def _custom_validate(self, verify, trust_bundle): """ Called when we have set custom validation. We do this in two cases: first, when cert validation is entirely disabled; and second, when using a custom trust DB. + Raises an SSLError if the connection is not trusted. """ # If we disabled cert validation, just say: cool. if not verify: return + successes = ( + SecurityConst.kSecTrustResultUnspecified, + SecurityConst.kSecTrustResultProceed, + ) + try: + trust_result = self._evaluate_trust(trust_bundle) + if trust_result in successes: + return + reason = "error code: %d" % (trust_result,) + except Exception as e: + # Do not trust on error + reason = "exception: %r" % (e,) + + # SecureTransport does not send an alert nor shuts down the connection. + rec = _build_tls_unknown_ca_alert(self.version()) + self.socket.sendall(rec) + # close the connection immediately + # l_onoff = 1, activate linger + # l_linger = 0, linger for 0 seoncds + opts = struct.pack("ii", 1, 0) + self.socket.setsockopt(socket.SOL_SOCKET, socket.SO_LINGER, opts) + self.close() + raise ssl.SSLError("certificate verify failed, %s" % reason) + + def _evaluate_trust(self, trust_bundle): # We want data in memory, so load it up. if os.path.isfile(trust_bundle): with open(trust_bundle, "rb") as f: @@ -421,15 +467,7 @@ def _custom_validate(self, verify, trust_bundle): if cert_array is not None: CoreFoundation.CFRelease(cert_array) - # Ok, now we can look at what the result was. - successes = ( - SecurityConst.kSecTrustResultUnspecified, - SecurityConst.kSecTrustResultProceed, - ) - if trust_result.value not in successes: - raise ssl.SSLError( - "certificate verify failed, error code: %d" % trust_result.value - ) + return trust_result.value def handshake( self, @@ -441,6 +479,7 @@ def handshake( client_cert, client_key, client_key_passphrase, + alpn_protocols, ): """ Actually performs the TLS handshake. This is run automatically by @@ -481,6 +520,9 @@ def handshake( # Setup the ciphers. self._set_ciphers() + # Setup the ALPN protocols. + self._set_alpn_protocols(alpn_protocols) + # Set the minimum and maximum TLS versions. result = Security.SSLSetProtocolVersionMin(self.context, min_version) _assert_no_error(result) @@ -754,6 +796,7 @@ def __init__(self, protocol): self._client_cert = None self._client_key = None self._client_key_passphrase = None + self._alpn_protocols = None @property def check_hostname(self): @@ -831,6 +874,18 @@ def load_cert_chain(self, certfile, keyfile=None, password=None): self._client_key = keyfile self._client_cert_passphrase = password + def set_alpn_protocols(self, protocols): + """ + Sets the ALPN protocols that will later be set on the context. + + Raises a NotImplementedError if ALPN is not supported. + """ + if not hasattr(Security, "SSLSetALPNProtocols"): + raise NotImplementedError( + "SecureTransport supports ALPN only in macOS 10.12+" + ) + self._alpn_protocols = [six.ensure_binary(p) for p in protocols] + def wrap_socket( self, sock, @@ -860,5 +915,6 @@ def wrap_socket( self._client_cert, self._client_key, self._client_key_passphrase, + self._alpn_protocols, ) return wrapped_socket diff --git a/src/pip/_vendor/urllib3/contrib/socks.py b/src/pip/_vendor/urllib3/contrib/socks.py index 9e97f7aa98f..93df8325d59 100644 --- a/src/pip/_vendor/urllib3/contrib/socks.py +++ b/src/pip/_vendor/urllib3/contrib/socks.py @@ -14,22 +14,26 @@ - SOCKS5 with local DNS (``proxy_url='socks5://...``) - Usernames and passwords for the SOCKS proxy - .. note:: - It is recommended to use ``socks5h://`` or ``socks4a://`` schemes in - your ``proxy_url`` to ensure that DNS resolution is done from the remote - server instead of client-side when connecting to a domain name. +.. note:: + It is recommended to use ``socks5h://`` or ``socks4a://`` schemes in + your ``proxy_url`` to ensure that DNS resolution is done from the remote + server instead of client-side when connecting to a domain name. SOCKS4 supports IPv4 and domain names with the SOCKS4A extension. SOCKS5 supports IPv4, IPv6, and domain names. When connecting to a SOCKS4 proxy the ``username`` portion of the ``proxy_url`` -will be sent as the ``userid`` section of the SOCKS request:: +will be sent as the ``userid`` section of the SOCKS request: + +.. code-block:: python proxy_url="socks4a://<userid>@proxy-host" When connecting to a SOCKS5 proxy the ``username`` and ``password`` portion of the ``proxy_url`` will be sent as the username/password to authenticate -with the proxy:: +with the proxy: + +.. code-block:: python proxy_url="socks5h://<username>:<password>@proxy-host" @@ -40,6 +44,7 @@ import socks except ImportError: import warnings + from ..exceptions import DependencyWarning warnings.warn( @@ -52,7 +57,8 @@ ) raise -from socket import error as SocketError, timeout as SocketTimeout +from socket import error as SocketError +from socket import timeout as SocketTimeout from ..connection import HTTPConnection, HTTPSConnection from ..connectionpool import HTTPConnectionPool, HTTPSConnectionPool diff --git a/src/pip/_vendor/urllib3/exceptions.py b/src/pip/_vendor/urllib3/exceptions.py index 5cc4d8a4f17..d69958d5dfc 100644 --- a/src/pip/_vendor/urllib3/exceptions.py +++ b/src/pip/_vendor/urllib3/exceptions.py @@ -1,21 +1,24 @@ from __future__ import absolute_import + from .packages.six.moves.http_client import IncompleteRead as httplib_IncompleteRead # Base Exceptions class HTTPError(Exception): - "Base exception used by this module." + """Base exception used by this module.""" + pass class HTTPWarning(Warning): - "Base warning used by this module." + """Base warning used by this module.""" + pass class PoolError(HTTPError): - "Base exception for errors caused within a pool." + """Base exception for errors caused within a pool.""" def __init__(self, pool, message): self.pool = pool @@ -27,7 +30,7 @@ def __reduce__(self): class RequestError(PoolError): - "Base exception for PoolErrors that have associated URLs." + """Base exception for PoolErrors that have associated URLs.""" def __init__(self, pool, url, message): self.url = url @@ -39,12 +42,13 @@ def __reduce__(self): class SSLError(HTTPError): - "Raised when SSL certificate fails in an HTTPS connection." + """Raised when SSL certificate fails in an HTTPS connection.""" + pass class ProxyError(HTTPError): - "Raised when the connection to a proxy fails." + """Raised when the connection to a proxy fails.""" def __init__(self, message, error, *args): super(ProxyError, self).__init__(message, error, *args) @@ -52,12 +56,14 @@ def __init__(self, message, error, *args): class DecodeError(HTTPError): - "Raised when automatic decoding based on Content-Type fails." + """Raised when automatic decoding based on Content-Type fails.""" + pass class ProtocolError(HTTPError): - "Raised when something unexpected happens mid-request/response." + """Raised when something unexpected happens mid-request/response.""" + pass @@ -87,7 +93,7 @@ def __init__(self, pool, url, reason=None): class HostChangedError(RequestError): - "Raised when an existing pool gets a request for a foreign host." + """Raised when an existing pool gets a request for a foreign host.""" def __init__(self, pool, url, retries=3): message = "Tried to open a foreign host with url: %s" % url @@ -96,13 +102,13 @@ def __init__(self, pool, url, retries=3): class TimeoutStateError(HTTPError): - """ Raised when passing an invalid state to a timeout """ + """Raised when passing an invalid state to a timeout""" pass class TimeoutError(HTTPError): - """ Raised when a socket timeout error occurs. + """Raised when a socket timeout error occurs. Catching this error will catch both :exc:`ReadTimeoutErrors <ReadTimeoutError>` and :exc:`ConnectTimeoutErrors <ConnectTimeoutError>`. @@ -112,39 +118,45 @@ class TimeoutError(HTTPError): class ReadTimeoutError(TimeoutError, RequestError): - "Raised when a socket timeout occurs while receiving data from a server" + """Raised when a socket timeout occurs while receiving data from a server""" + pass # This timeout error does not have a URL attached and needs to inherit from the # base HTTPError class ConnectTimeoutError(TimeoutError): - "Raised when a socket timeout occurs while connecting to a server" + """Raised when a socket timeout occurs while connecting to a server""" + pass class NewConnectionError(ConnectTimeoutError, PoolError): - "Raised when we fail to establish a new connection. Usually ECONNREFUSED." + """Raised when we fail to establish a new connection. Usually ECONNREFUSED.""" + pass class EmptyPoolError(PoolError): - "Raised when a pool runs out of connections and no more are allowed." + """Raised when a pool runs out of connections and no more are allowed.""" + pass class ClosedPoolError(PoolError): - "Raised when a request enters a pool after the pool has been closed." + """Raised when a request enters a pool after the pool has been closed.""" + pass class LocationValueError(ValueError, HTTPError): - "Raised when there is something wrong with a given URL input." + """Raised when there is something wrong with a given URL input.""" + pass class LocationParseError(LocationValueError): - "Raised when get_host or similar fails to parse the URL input." + """Raised when get_host or similar fails to parse the URL input.""" def __init__(self, location): message = "Failed to parse: %s" % location @@ -153,39 +165,56 @@ def __init__(self, location): self.location = location +class URLSchemeUnknown(LocationValueError): + """Raised when a URL input has an unsupported scheme.""" + + def __init__(self, scheme): + message = "Not supported URL scheme %s" % scheme + super(URLSchemeUnknown, self).__init__(message) + + self.scheme = scheme + + class ResponseError(HTTPError): - "Used as a container for an error reason supplied in a MaxRetryError." + """Used as a container for an error reason supplied in a MaxRetryError.""" + GENERIC_ERROR = "too many error responses" SPECIFIC_ERROR = "too many {status_code} error responses" class SecurityWarning(HTTPWarning): - "Warned when performing security reducing actions" + """Warned when performing security reducing actions""" + pass class SubjectAltNameWarning(SecurityWarning): - "Warned when connecting to a host with a certificate missing a SAN." + """Warned when connecting to a host with a certificate missing a SAN.""" + pass class InsecureRequestWarning(SecurityWarning): - "Warned when making an unverified HTTPS request." + """Warned when making an unverified HTTPS request.""" + pass class SystemTimeWarning(SecurityWarning): - "Warned when system time is suspected to be wrong" + """Warned when system time is suspected to be wrong""" + pass class InsecurePlatformWarning(SecurityWarning): - "Warned when certain SSL configuration is not available on a platform." + """Warned when certain TLS/SSL configuration is not available on a platform.""" + pass class SNIMissingWarning(HTTPWarning): - "Warned when making a HTTPS request without SNI available." + """Warned when making a HTTPS request without SNI available.""" + pass @@ -198,29 +227,16 @@ class DependencyWarning(HTTPWarning): pass -class InvalidProxyConfigurationWarning(HTTPWarning): - """ - Warned when using an HTTPS proxy and an HTTPS URL. Currently - urllib3 doesn't support HTTPS proxies and the proxy will be - contacted via HTTP instead. This warning can be fixed by - changing your HTTPS proxy URL into an HTTP proxy URL. - - If you encounter this warning read this: - https://github.com/urllib3/urllib3/issues/1850 - """ - - pass - - class ResponseNotChunked(ProtocolError, ValueError): - "Response needs to be chunked in order to read it as chunks." + """Response needs to be chunked in order to read it as chunks.""" + pass class BodyNotHttplibCompatible(HTTPError): """ - Body should be httplib.HTTPResponse like (have an fp attribute which - returns raw chunks) for read_chunked(). + Body should be :class:`http.client.HTTPResponse` like + (have an fp attribute which returns raw chunks) for read_chunked(). """ pass @@ -230,9 +246,8 @@ class IncompleteRead(HTTPError, httplib_IncompleteRead): """ Response length doesn't match expected Content-Length - Subclass of http_client.IncompleteRead to allow int value - for `partial` to avoid creating large objects on streamed - reads. + Subclass of :class:`http.client.IncompleteRead` to allow int value + for ``partial`` to avoid creating large objects on streamed reads. """ def __init__(self, partial, expected): @@ -245,13 +260,32 @@ def __repr__(self): ) +class InvalidChunkLength(HTTPError, httplib_IncompleteRead): + """Invalid chunk length in a chunked response.""" + + def __init__(self, response, length): + super(InvalidChunkLength, self).__init__( + response.tell(), response.length_remaining + ) + self.response = response + self.length = length + + def __repr__(self): + return "InvalidChunkLength(got length %r, %i bytes read)" % ( + self.length, + self.partial, + ) + + class InvalidHeader(HTTPError): - "The header provided was somehow invalid." + """The header provided was somehow invalid.""" + pass -class ProxySchemeUnknown(AssertionError, ValueError): - "ProxyManager does not support the supplied scheme" +class ProxySchemeUnknown(AssertionError, URLSchemeUnknown): + """ProxyManager does not support the supplied scheme""" + # TODO(t-8ch): Stop inheriting from AssertionError in v2.0. def __init__(self, scheme): @@ -259,8 +293,14 @@ def __init__(self, scheme): super(ProxySchemeUnknown, self).__init__(message) +class ProxySchemeUnsupported(ValueError): + """Fetching HTTPS resources through HTTPS proxies is unsupported""" + + pass + + class HeaderParsingError(HTTPError): - "Raised by assert_header_parsing, but we convert it to a log.warning statement." + """Raised by assert_header_parsing, but we convert it to a log.warning statement.""" def __init__(self, defects, unparsed_data): message = "%s, unparsed data: %r" % (defects or "Unknown", unparsed_data) @@ -268,5 +308,6 @@ def __init__(self, defects, unparsed_data): class UnrewindableBodyError(HTTPError): - "urllib3 encountered an error when trying to rewind a body" + """urllib3 encountered an error when trying to rewind a body""" + pass diff --git a/src/pip/_vendor/urllib3/fields.py b/src/pip/_vendor/urllib3/fields.py index 8715b2202b0..9d630f491d9 100644 --- a/src/pip/_vendor/urllib3/fields.py +++ b/src/pip/_vendor/urllib3/fields.py @@ -1,4 +1,5 @@ from __future__ import absolute_import + import email.utils import mimetypes import re @@ -26,7 +27,8 @@ def format_header_param_rfc2231(name, value): strategy defined in RFC 2231. Particularly useful for header parameters which might contain - non-ASCII values, like file names. This follows RFC 2388 Section 4.4. + non-ASCII values, like file names. This follows + `RFC 2388 Section 4.4 <https://tools.ietf.org/html/rfc2388#section-4.4>`_. :param name: The name of the parameter, a string expected to be ASCII only. @@ -65,7 +67,6 @@ def format_header_param_rfc2231(name, value): u"\u0022": u"%22", # Replace "\" with "\\". u"\u005C": u"\u005C\u005C", - u"\u005C": u"\u005C\u005C", } # All control characters from 0x00 to 0x1F *except* 0x1B. diff --git a/src/pip/_vendor/urllib3/filepost.py b/src/pip/_vendor/urllib3/filepost.py index b7b00992c65..36c9252c647 100644 --- a/src/pip/_vendor/urllib3/filepost.py +++ b/src/pip/_vendor/urllib3/filepost.py @@ -1,13 +1,13 @@ from __future__ import absolute_import + import binascii import codecs import os - from io import BytesIO +from .fields import RequestField from .packages import six from .packages.six import b -from .fields import RequestField writer = codecs.lookup("utf-8")[3] diff --git a/src/pip/_vendor/urllib3/packages/backports/makefile.py b/src/pip/_vendor/urllib3/packages/backports/makefile.py index a3156a69c08..b8fb2154b6d 100644 --- a/src/pip/_vendor/urllib3/packages/backports/makefile.py +++ b/src/pip/_vendor/urllib3/packages/backports/makefile.py @@ -7,7 +7,6 @@ wants to create a "fake" socket object. """ import io - from socket import SocketIO diff --git a/src/pip/_vendor/urllib3/packages/ssl_match_hostname/__init__.py b/src/pip/_vendor/urllib3/packages/ssl_match_hostname/__init__.py index 75b6bb1cf0d..6b12fd90aad 100644 --- a/src/pip/_vendor/urllib3/packages/ssl_match_hostname/__init__.py +++ b/src/pip/_vendor/urllib3/packages/ssl_match_hostname/__init__.py @@ -10,10 +10,13 @@ except ImportError: try: # Backport of the function from a pypi module - from backports.ssl_match_hostname import CertificateError, match_hostname + from backports.ssl_match_hostname import ( # type: ignore + CertificateError, + match_hostname, + ) except ImportError: # Our vendored copy - from ._implementation import CertificateError, match_hostname + from ._implementation import CertificateError, match_hostname # type: ignore # Not needed, but documenting what we provide. __all__ = ("CertificateError", "match_hostname") diff --git a/src/pip/_vendor/urllib3/poolmanager.py b/src/pip/_vendor/urllib3/poolmanager.py index e2bd3bd8dba..3a31a285bf6 100644 --- a/src/pip/_vendor/urllib3/poolmanager.py +++ b/src/pip/_vendor/urllib3/poolmanager.py @@ -1,24 +1,24 @@ from __future__ import absolute_import + import collections import functools import logging -import warnings from ._collections import RecentlyUsedContainer -from .connectionpool import HTTPConnectionPool, HTTPSConnectionPool -from .connectionpool import port_by_scheme +from .connectionpool import HTTPConnectionPool, HTTPSConnectionPool, port_by_scheme from .exceptions import ( LocationValueError, MaxRetryError, ProxySchemeUnknown, - InvalidProxyConfigurationWarning, + ProxySchemeUnsupported, + URLSchemeUnknown, ) from .packages import six from .packages.six.moves.urllib.parse import urljoin from .request import RequestMethods -from .util.url import parse_url +from .util.proxy import connection_requires_http_tunnel from .util.retry import Retry - +from .util.url import parse_url __all__ = ["PoolManager", "ProxyManager", "proxy_from_url"] @@ -59,6 +59,7 @@ "key_headers", # dict "key__proxy", # parsed proxy url "key__proxy_headers", # dict + "key__proxy_config", # class "key_socket_options", # list of (level (int), optname (int), value (int or str)) tuples "key__socks_options", # dict "key_assert_hostname", # bool or string @@ -70,6 +71,9 @@ #: All custom key schemes should include the fields in this key at a minimum. PoolKey = collections.namedtuple("PoolKey", _key_fields) +_proxy_config_fields = ("ssl_context", "use_forwarding_for_https") +ProxyConfig = collections.namedtuple("ProxyConfig", _proxy_config_fields) + def _default_key_normalizer(key_class, request_context): """ @@ -161,6 +165,7 @@ class PoolManager(RequestMethods): """ proxy = None + proxy_config = None def __init__(self, num_pools=10, headers=None, **connection_pool_kw): RequestMethods.__init__(self, headers) @@ -182,7 +187,7 @@ def __exit__(self, exc_type, exc_val, exc_tb): def _new_pool(self, scheme, host, port, request_context=None): """ - Create a new :class:`ConnectionPool` based on host, port, scheme, and + Create a new :class:`urllib3.connectionpool.ConnectionPool` based on host, port, scheme, and any additional pool keyword arguments. If ``request_context`` is provided, it is provided as keyword arguments @@ -218,7 +223,7 @@ def clear(self): def connection_from_host(self, host, port=None, scheme="http", pool_kwargs=None): """ - Get a :class:`ConnectionPool` based on the host, port, and scheme. + Get a :class:`urllib3.connectionpool.ConnectionPool` based on the host, port, and scheme. If ``port`` isn't given, it will be derived from the ``scheme`` using ``urllib3.connectionpool.port_by_scheme``. If ``pool_kwargs`` is @@ -241,20 +246,22 @@ def connection_from_host(self, host, port=None, scheme="http", pool_kwargs=None) def connection_from_context(self, request_context): """ - Get a :class:`ConnectionPool` based on the request context. + Get a :class:`urllib3.connectionpool.ConnectionPool` based on the request context. ``request_context`` must at least contain the ``scheme`` key and its value must be a key in ``key_fn_by_scheme`` instance variable. """ scheme = request_context["scheme"].lower() - pool_key_constructor = self.key_fn_by_scheme[scheme] + pool_key_constructor = self.key_fn_by_scheme.get(scheme) + if not pool_key_constructor: + raise URLSchemeUnknown(scheme) pool_key = pool_key_constructor(request_context) return self.connection_from_pool_key(pool_key, request_context=request_context) def connection_from_pool_key(self, pool_key, request_context=None): """ - Get a :class:`ConnectionPool` based on the provided pool key. + Get a :class:`urllib3.connectionpool.ConnectionPool` based on the provided pool key. ``pool_key`` should be a namedtuple that only contains immutable objects. At a minimum it must have the ``scheme``, ``host``, and @@ -312,9 +319,39 @@ def _merge_pool_kwargs(self, override): base_pool_kwargs[key] = value return base_pool_kwargs + def _proxy_requires_url_absolute_form(self, parsed_url): + """ + Indicates if the proxy requires the complete destination URL in the + request. Normally this is only needed when not using an HTTP CONNECT + tunnel. + """ + if self.proxy is None: + return False + + return not connection_requires_http_tunnel( + self.proxy, self.proxy_config, parsed_url.scheme + ) + + def _validate_proxy_scheme_url_selection(self, url_scheme): + """ + Validates that were not attempting to do TLS in TLS connections on + Python2 or with unsupported SSL implementations. + """ + if self.proxy is None or url_scheme != "https": + return + + if self.proxy.scheme != "https": + return + + if six.PY2 and not self.proxy_config.use_forwarding_for_https: + raise ProxySchemeUnsupported( + "Contacting HTTPS destinations through HTTPS proxies " + "'via CONNECT tunnels' is not supported in Python 2" + ) + def urlopen(self, method, url, redirect=True, **kw): """ - Same as :meth:`urllib3.connectionpool.HTTPConnectionPool.urlopen` + Same as :meth:`urllib3.HTTPConnectionPool.urlopen` with custom cross-host redirect logic and only sends the request-uri portion of the ``url``. @@ -322,6 +359,8 @@ def urlopen(self, method, url, redirect=True, **kw): :class:`urllib3.connectionpool.ConnectionPool` can be chosen for it. """ u = parse_url(url) + self._validate_proxy_scheme_url_selection(u.scheme) + conn = self.connection_from_host(u.host, port=u.port, scheme=u.scheme) kw["assert_same_host"] = False @@ -330,7 +369,7 @@ def urlopen(self, method, url, redirect=True, **kw): if "headers" not in kw: kw["headers"] = self.headers.copy() - if self.proxy is not None and u.scheme == "http": + if self._proxy_requires_url_absolute_form(u): response = conn.urlopen(method, url, **kw) else: response = conn.urlopen(method, u.request_uri, **kw) @@ -392,6 +431,19 @@ class ProxyManager(PoolManager): HTTPS/CONNECT case they are sent only once. Could be used for proxy authentication. + :param proxy_ssl_context: + The proxy SSL context is used to establish the TLS connection to the + proxy when using HTTPS proxies. + + :param use_forwarding_for_https: + (Defaults to False) If set to True will forward requests to the HTTPS + proxy to be made on behalf of the client instead of creating a TLS + tunnel via the CONNECT method. **Enabling this flag means that request + and response headers and content will be visible from the HTTPS proxy** + whereas tunneling keeps request and response headers and content + private. IP address, target hostname, SNI, and port are always visible + to an HTTPS proxy even when this flag is disabled. + Example: >>> proxy = urllib3.ProxyManager('http://localhost:3128/') >>> r1 = proxy.request('GET', 'http://google.com/') @@ -411,6 +463,8 @@ def __init__( num_pools=10, headers=None, proxy_headers=None, + proxy_ssl_context=None, + use_forwarding_for_https=False, **connection_pool_kw ): @@ -421,18 +475,22 @@ def __init__( proxy_url.port, ) proxy = parse_url(proxy_url) - if not proxy.port: - port = port_by_scheme.get(proxy.scheme, 80) - proxy = proxy._replace(port=port) if proxy.scheme not in ("http", "https"): raise ProxySchemeUnknown(proxy.scheme) + if not proxy.port: + port = port_by_scheme.get(proxy.scheme, 80) + proxy = proxy._replace(port=port) + self.proxy = proxy self.proxy_headers = proxy_headers or {} + self.proxy_ssl_context = proxy_ssl_context + self.proxy_config = ProxyConfig(proxy_ssl_context, use_forwarding_for_https) connection_pool_kw["_proxy"] = self.proxy connection_pool_kw["_proxy_headers"] = self.proxy_headers + connection_pool_kw["_proxy_config"] = self.proxy_config super(ProxyManager, self).__init__(num_pools, headers, **connection_pool_kw) @@ -461,27 +519,13 @@ def _set_proxy_headers(self, url, headers=None): headers_.update(headers) return headers_ - def _validate_proxy_scheme_url_selection(self, url_scheme): - if url_scheme == "https" and self.proxy.scheme == "https": - warnings.warn( - "Your proxy configuration specified an HTTPS scheme for the proxy. " - "Are you sure you want to use HTTPS to contact the proxy? " - "This most likely indicates an error in your configuration. " - "Read this issue for more info: " - "https://github.com/urllib3/urllib3/issues/1850", - InvalidProxyConfigurationWarning, - stacklevel=3, - ) - def urlopen(self, method, url, redirect=True, **kw): "Same as HTTP(S)ConnectionPool.urlopen, ``url`` must be absolute." u = parse_url(url) - self._validate_proxy_scheme_url_selection(u.scheme) - - if u.scheme == "http": - # For proxied HTTPS requests, httplib sets the necessary headers - # on the CONNECT to the proxy. For HTTP, we'll definitely - # need to set 'Host' at the very least. + if not connection_requires_http_tunnel(self.proxy, self.proxy_config, u.scheme): + # For connections using HTTP CONNECT, httplib sets the necessary + # headers on the CONNECT to the proxy. If we're not using CONNECT, + # we'll definitely need to set 'Host' at the very least. headers = kw.get("headers", self.headers) kw["headers"] = self._set_proxy_headers(url, headers) diff --git a/src/pip/_vendor/urllib3/request.py b/src/pip/_vendor/urllib3/request.py index 55f160bbf10..398386a5b9f 100644 --- a/src/pip/_vendor/urllib3/request.py +++ b/src/pip/_vendor/urllib3/request.py @@ -3,15 +3,14 @@ from .filepost import encode_multipart_formdata from .packages.six.moves.urllib.parse import urlencode - __all__ = ["RequestMethods"] class RequestMethods(object): """ Convenience mixin for classes who implement a :meth:`urlopen` method, such - as :class:`~urllib3.connectionpool.HTTPConnectionPool` and - :class:`~urllib3.poolmanager.PoolManager`. + as :class:`urllib3.HTTPConnectionPool` and + :class:`urllib3.PoolManager`. Provides behavior for making common types of HTTP request methods and decides which type of request field encoding to use. @@ -111,9 +110,9 @@ def request_encode_body( the body. This is useful for request methods like POST, PUT, PATCH, etc. When ``encode_multipart=True`` (default), then - :meth:`urllib3.filepost.encode_multipart_formdata` is used to encode + :func:`urllib3.encode_multipart_formdata` is used to encode the payload with the appropriate content type. Otherwise - :meth:`urllib.urlencode` is used with the + :func:`urllib.parse.urlencode` is used with the 'application/x-www-form-urlencoded' content type. Multipart encoding must be used when posting files, and it's reasonably diff --git a/src/pip/_vendor/urllib3/response.py b/src/pip/_vendor/urllib3/response.py index 7dc9b93caef..38693f4fc6e 100644 --- a/src/pip/_vendor/urllib3/response.py +++ b/src/pip/_vendor/urllib3/response.py @@ -1,10 +1,11 @@ from __future__ import absolute_import -from contextlib import contextmanager -import zlib + import io import logging -from socket import timeout as SocketTimeout +import zlib +from contextlib import contextmanager from socket import error as SocketError +from socket import timeout as SocketTimeout try: import brotli @@ -12,19 +13,20 @@ brotli = None from ._collections import HTTPHeaderDict +from .connection import BaseSSLError, HTTPException from .exceptions import ( BodyNotHttplibCompatible, - ProtocolError, DecodeError, - ReadTimeoutError, - ResponseNotChunked, + HTTPError, IncompleteRead, + InvalidChunkLength, InvalidHeader, - HTTPError, + ProtocolError, + ReadTimeoutError, + ResponseNotChunked, + SSLError, ) -from .packages.six import string_types as basestring, PY3 -from .packages.six.moves import http_client as httplib -from .connection import HTTPException, BaseSSLError +from .packages import six from .util.response import is_fp_closed, is_response_to_head log = logging.getLogger(__name__) @@ -107,11 +109,10 @@ class BrotliDecoder(object): # are for 'brotlipy' and bottom branches for 'Brotli' def __init__(self): self._obj = brotli.Decompressor() - - def decompress(self, data): if hasattr(self._obj, "decompress"): - return self._obj.decompress(data) - return self._obj.process(data) + self.decompress = self._obj.decompress + else: + self.decompress = self._obj.process def flush(self): if hasattr(self._obj, "flush"): @@ -157,13 +158,13 @@ class HTTPResponse(io.IOBase): """ HTTP Response container. - Backwards-compatible to httplib's HTTPResponse but the response ``body`` is + Backwards-compatible with :class:`http.client.HTTPResponse` but the response ``body`` is loaded and decoded on-demand when the ``data`` property is accessed. This class is also compatible with the Python standard library's :mod:`io` module, and can hence be treated as a readable object in the context of that framework. - Extra parameters for behaviour not present in httplib.HTTPResponse: + Extra parameters for behaviour not present in :class:`http.client.HTTPResponse`: :param preload_content: If True, the response's body will be preloaded during construction. @@ -173,7 +174,7 @@ class is also compatible with the Python standard library's :mod:`io` 'content-encoding' header. :param original_response: - When this HTTPResponse wrapper is generated from an httplib.HTTPResponse + When this HTTPResponse wrapper is generated from an :class:`http.client.HTTPResponse` object, it's convenient to include the original for debug purposes. It's otherwise unused. @@ -233,7 +234,7 @@ def __init__( self.msg = msg self._request_url = request_url - if body and isinstance(body, (basestring, bytes)): + if body and isinstance(body, (six.string_types, bytes)): self._body = body self._pool = pool @@ -291,7 +292,7 @@ def drain_conn(self): @property def data(self): - # For backwords-compat with earlier urllib3 0.4 and earlier. + # For backwards-compat with earlier urllib3 0.4 and earlier. if self._body: return self._body @@ -308,8 +309,8 @@ def isclosed(self): def tell(self): """ Obtain the number of bytes pulled over the wire so far. May differ from - the amount of content returned by :meth:``HTTPResponse.read`` if bytes - are encoded on the wire (e.g, compressed). + the amount of content returned by :meth:``urllib3.response.HTTPResponse.read`` + if bytes are encoded on the wire (e.g, compressed). """ return self._fp_bytes_read @@ -443,10 +444,9 @@ def _error_catcher(self): except BaseSSLError as e: # FIXME: Is there a better way to differentiate between SSLErrors? - if "read operation timed out" not in str(e): # Defensive: - # This shouldn't happen but just in case we're missing an edge - # case, let's avoid swallowing SSL errors. - raise + if "read operation timed out" not in str(e): + # SSL errors related to framing/MAC get wrapped and reraised here + raise SSLError(e) raise ReadTimeoutError(self._pool, None, "Read timed out.") @@ -480,7 +480,7 @@ def _error_catcher(self): def read(self, amt=None, decode_content=None, cache_content=False): """ - Similar to :meth:`httplib.HTTPResponse.read`, but with two additional + Similar to :meth:`http.client.HTTPResponse.read`, but with two additional parameters: ``decode_content`` and ``cache_content``. :param amt: @@ -581,7 +581,7 @@ def stream(self, amt=2 ** 16, decode_content=None): @classmethod def from_httplib(ResponseCls, r, **response_kw): """ - Given an :class:`httplib.HTTPResponse` instance ``r``, return a + Given an :class:`http.client.HTTPResponse` instance ``r``, return a corresponding :class:`urllib3.response.HTTPResponse` object. Remaining parameters are passed to the HTTPResponse constructor, along @@ -590,11 +590,11 @@ def from_httplib(ResponseCls, r, **response_kw): headers = r.msg if not isinstance(headers, HTTPHeaderDict): - if PY3: - headers = HTTPHeaderDict(headers.items()) - else: + if six.PY2: # Python 2.7 headers = HTTPHeaderDict.from_httplib(headers) + else: + headers = HTTPHeaderDict(headers.items()) # HTTPResponse objects in Python 3 don't have a .strict attribute strict = getattr(r, "strict", 0) @@ -610,7 +610,7 @@ def from_httplib(ResponseCls, r, **response_kw): ) return resp - # Backwards-compatibility methods for httplib.HTTPResponse + # Backwards-compatibility methods for http.client.HTTPResponse def getheaders(self): return self.headers @@ -680,8 +680,8 @@ def readinto(self, b): def supports_chunked_reads(self): """ Checks if the underlying file-like object looks like a - httplib.HTTPResponse object. We do this by testing for the fp - attribute. If it is present we assume it returns raw chunks as + :class:`http.client.HTTPResponse` object. We do this by testing for + the fp attribute. If it is present we assume it returns raw chunks as processed by read_chunked(). """ return hasattr(self._fp, "fp") @@ -698,7 +698,7 @@ def _update_chunk_length(self): except ValueError: # Invalid chunked protocol response, abort. self.close() - raise httplib.IncompleteRead(line) + raise InvalidChunkLength(self, line) def _handle_chunk(self, amt): returned_chunk = None @@ -745,7 +745,7 @@ def read_chunked(self, amt=None, decode_content=None): ) if not self.supports_chunked_reads(): raise BodyNotHttplibCompatible( - "Body should be httplib.HTTPResponse like. " + "Body should be http.client.HTTPResponse like. " "It should have have an fp attribute which returns raw chunks." ) diff --git a/src/pip/_vendor/urllib3/util/__init__.py b/src/pip/_vendor/urllib3/util/__init__.py index a96c73a9d85..4547fc522b6 100644 --- a/src/pip/_vendor/urllib3/util/__init__.py +++ b/src/pip/_vendor/urllib3/util/__init__.py @@ -2,23 +2,23 @@ # For backwards compatibility, provide imports that used to be here. from .connection import is_connection_dropped -from .request import make_headers +from .request import SKIP_HEADER, SKIPPABLE_HEADERS, make_headers from .response import is_fp_closed +from .retry import Retry from .ssl_ import ( - SSLContext, + ALPN_PROTOCOLS, HAS_SNI, IS_PYOPENSSL, IS_SECURETRANSPORT, + PROTOCOL_TLS, + SSLContext, assert_fingerprint, resolve_cert_reqs, resolve_ssl_version, ssl_wrap_socket, - PROTOCOL_TLS, ) -from .timeout import current_time, Timeout - -from .retry import Retry -from .url import get_host, parse_url, split_first, Url +from .timeout import Timeout, current_time +from .url import Url, get_host, parse_url, split_first from .wait import wait_for_read, wait_for_write __all__ = ( @@ -27,6 +27,7 @@ "IS_SECURETRANSPORT", "SSLContext", "PROTOCOL_TLS", + "ALPN_PROTOCOLS", "Retry", "Timeout", "Url", @@ -43,4 +44,6 @@ "ssl_wrap_socket", "wait_for_read", "wait_for_write", + "SKIP_HEADER", + "SKIPPABLE_HEADERS", ) diff --git a/src/pip/_vendor/urllib3/util/connection.py b/src/pip/_vendor/urllib3/util/connection.py index 86f0a3b00ed..f1e5d37f88f 100644 --- a/src/pip/_vendor/urllib3/util/connection.py +++ b/src/pip/_vendor/urllib3/util/connection.py @@ -1,7 +1,12 @@ from __future__ import absolute_import + import socket -from .wait import NoWayToWaitForSocketError, wait_for_read + +from pip._vendor.urllib3.exceptions import LocationParseError + from ..contrib import _appengine_environ +from ..packages import six +from .wait import NoWayToWaitForSocketError, wait_for_read def is_connection_dropped(conn): # Platform-specific @@ -9,7 +14,7 @@ def is_connection_dropped(conn): # Platform-specific Returns True if the connection is dropped and should be closed. :param conn: - :class:`httplib.HTTPConnection` object. + :class:`http.client.HTTPConnection` object. Note: For platforms like AppEngine, this will always return ``False`` to let the platform handle connection recycling transparently for us. @@ -42,7 +47,7 @@ def create_connection( port)``) and return the socket object. Passing the optional *timeout* parameter will set the timeout on the socket instance before attempting to connect. If no *timeout* is supplied, the - global default timeout setting returned by :func:`getdefaulttimeout` + global default timeout setting returned by :func:`socket.getdefaulttimeout` is used. If *source_address* is set it must be a tuple of (host, port) for the socket to bind as a source address before making the connection. An host of '' or port 0 tells the OS to use the default. @@ -58,6 +63,13 @@ def create_connection( # The original create_connection function always returns all records. family = allowed_gai_family() + try: + host.encode("idna") + except UnicodeError: + return six.raise_from( + LocationParseError(u"'%s', label empty or too long" % host), None + ) + for res in socket.getaddrinfo(host, port, family, socket.SOCK_STREAM): af, socktype, proto, canonname, sa = res sock = None diff --git a/src/pip/_vendor/urllib3/util/proxy.py b/src/pip/_vendor/urllib3/util/proxy.py new file mode 100644 index 00000000000..34f884d5b31 --- /dev/null +++ b/src/pip/_vendor/urllib3/util/proxy.py @@ -0,0 +1,56 @@ +from .ssl_ import create_urllib3_context, resolve_cert_reqs, resolve_ssl_version + + +def connection_requires_http_tunnel( + proxy_url=None, proxy_config=None, destination_scheme=None +): + """ + Returns True if the connection requires an HTTP CONNECT through the proxy. + + :param URL proxy_url: + URL of the proxy. + :param ProxyConfig proxy_config: + Proxy configuration from poolmanager.py + :param str destination_scheme: + The scheme of the destination. (i.e https, http, etc) + """ + # If we're not using a proxy, no way to use a tunnel. + if proxy_url is None: + return False + + # HTTP destinations never require tunneling, we always forward. + if destination_scheme == "http": + return False + + # Support for forwarding with HTTPS proxies and HTTPS destinations. + if ( + proxy_url.scheme == "https" + and proxy_config + and proxy_config.use_forwarding_for_https + ): + return False + + # Otherwise always use a tunnel. + return True + + +def create_proxy_ssl_context( + ssl_version, cert_reqs, ca_certs=None, ca_cert_dir=None, ca_cert_data=None +): + """ + Generates a default proxy ssl context if one hasn't been provided by the + user. + """ + ssl_context = create_urllib3_context( + ssl_version=resolve_ssl_version(ssl_version), + cert_reqs=resolve_cert_reqs(cert_reqs), + ) + if ( + not ca_certs + and not ca_cert_dir + and not ca_cert_data + and hasattr(ssl_context, "load_default_certs") + ): + ssl_context.load_default_certs() + + return ssl_context diff --git a/src/pip/_vendor/urllib3/util/queue.py b/src/pip/_vendor/urllib3/util/queue.py index d3d379a1999..41784104ee4 100644 --- a/src/pip/_vendor/urllib3/util/queue.py +++ b/src/pip/_vendor/urllib3/util/queue.py @@ -1,4 +1,5 @@ import collections + from ..packages import six from ..packages.six.moves import queue diff --git a/src/pip/_vendor/urllib3/util/request.py b/src/pip/_vendor/urllib3/util/request.py index 3b7bb54dafb..25103383ec7 100644 --- a/src/pip/_vendor/urllib3/util/request.py +++ b/src/pip/_vendor/urllib3/util/request.py @@ -1,8 +1,16 @@ from __future__ import absolute_import + from base64 import b64encode -from ..packages.six import b, integer_types from ..exceptions import UnrewindableBodyError +from ..packages.six import b, integer_types + +# Pass as a value within ``headers`` to skip +# emitting some HTTP headers that are added automatically. +# The only headers that are supported are ``Accept-Encoding``, +# ``Host``, and ``User-Agent``. +SKIP_HEADER = "@@@SKIP_HEADER@@@" +SKIPPABLE_HEADERS = frozenset(["accept-encoding", "host", "user-agent"]) ACCEPT_ENCODING = "gzip,deflate" try: diff --git a/src/pip/_vendor/urllib3/util/response.py b/src/pip/_vendor/urllib3/util/response.py index 715868dd100..5ea609ccedf 100644 --- a/src/pip/_vendor/urllib3/util/response.py +++ b/src/pip/_vendor/urllib3/util/response.py @@ -1,7 +1,9 @@ from __future__ import absolute_import -from ..packages.six.moves import http_client as httplib + +from email.errors import MultipartInvariantViolationDefect, StartBoundaryNotFoundDefect from ..exceptions import HeaderParsingError +from ..packages.six.moves import http_client as httplib def is_fp_closed(obj): @@ -42,8 +44,7 @@ def assert_header_parsing(headers): Only works on Python 3. - :param headers: Headers to verify. - :type headers: `httplib.HTTPMessage`. + :param http.client.HTTPMessage headers: Headers to verify. :raises urllib3.exceptions.HeaderParsingError: If parsing errors are found. @@ -66,6 +67,25 @@ def assert_header_parsing(headers): if isinstance(payload, (bytes, str)): unparsed_data = payload + if defects: + # httplib is assuming a response body is available + # when parsing headers even when httplib only sends + # header data to parse_headers() This results in + # defects on multipart responses in particular. + # See: https://github.com/urllib3/urllib3/issues/800 + + # So we ignore the following defects: + # - StartBoundaryNotFoundDefect: + # The claimed start boundary was never found. + # - MultipartInvariantViolationDefect: + # A message claimed to be a multipart but no subparts were found. + defects = [ + defect + for defect in defects + if not isinstance( + defect, (StartBoundaryNotFoundDefect, MultipartInvariantViolationDefect) + ) + ] if defects or unparsed_data: raise HeaderParsingError(defects=defects, unparsed_data=unparsed_data) @@ -76,8 +96,9 @@ def is_response_to_head(response): Checks whether the request of a response has been a HEAD-request. Handles the quirks of AppEngine. - :param conn: - :type conn: :class:`httplib.HTTPResponse` + :param http.client.HTTPResponse response: + Response to check if the originating request + used 'HEAD' as a method. """ # FIXME: Can we do this somehow without accessing private httplib _method? method = response._method diff --git a/src/pip/_vendor/urllib3/util/retry.py b/src/pip/_vendor/urllib3/util/retry.py index ee30c91b147..ee51f922f84 100644 --- a/src/pip/_vendor/urllib3/util/retry.py +++ b/src/pip/_vendor/urllib3/util/retry.py @@ -1,23 +1,24 @@ from __future__ import absolute_import -import time + +import email import logging +import re +import time +import warnings from collections import namedtuple from itertools import takewhile -import email -import re from ..exceptions import ( ConnectTimeoutError, + InvalidHeader, MaxRetryError, ProtocolError, + ProxyError, ReadTimeoutError, ResponseError, - InvalidHeader, - ProxyError, ) from ..packages import six - log = logging.getLogger(__name__) @@ -27,8 +28,51 @@ ) +# TODO: In v2 we can remove this sentinel and metaclass with deprecated options. +_Default = object() + + +class _RetryMeta(type): + @property + def DEFAULT_METHOD_WHITELIST(cls): + warnings.warn( + "Using 'Retry.DEFAULT_METHOD_WHITELIST' is deprecated and " + "will be removed in v2.0. Use 'Retry.DEFAULT_METHODS_ALLOWED' instead", + DeprecationWarning, + ) + return cls.DEFAULT_ALLOWED_METHODS + + @DEFAULT_METHOD_WHITELIST.setter + def DEFAULT_METHOD_WHITELIST(cls, value): + warnings.warn( + "Using 'Retry.DEFAULT_METHOD_WHITELIST' is deprecated and " + "will be removed in v2.0. Use 'Retry.DEFAULT_ALLOWED_METHODS' instead", + DeprecationWarning, + ) + cls.DEFAULT_ALLOWED_METHODS = value + + @property + def DEFAULT_REDIRECT_HEADERS_BLACKLIST(cls): + warnings.warn( + "Using 'Retry.DEFAULT_REDIRECT_HEADERS_BLACKLIST' is deprecated and " + "will be removed in v2.0. Use 'Retry.DEFAULT_REMOVE_HEADERS_ON_REDIRECT' instead", + DeprecationWarning, + ) + return cls.DEFAULT_REMOVE_HEADERS_ON_REDIRECT + + @DEFAULT_REDIRECT_HEADERS_BLACKLIST.setter + def DEFAULT_REDIRECT_HEADERS_BLACKLIST(cls, value): + warnings.warn( + "Using 'Retry.DEFAULT_REDIRECT_HEADERS_BLACKLIST' is deprecated and " + "will be removed in v2.0. Use 'Retry.DEFAULT_REMOVE_HEADERS_ON_REDIRECT' instead", + DeprecationWarning, + ) + cls.DEFAULT_REMOVE_HEADERS_ON_REDIRECT = value + + +@six.add_metaclass(_RetryMeta) class Retry(object): - """ Retry configuration. + """Retry configuration. Each retry attempt will create a new Retry object with updated values, so they can be safely reused. @@ -54,8 +98,7 @@ class Retry(object): Total number of retries to allow. Takes precedence over other counts. Set to ``None`` to remove this constraint and fall back on other - counts. It's a good idea to set this to some sensibly-high value to - account for unexpected edge cases and avoid infinite retry loops. + counts. Set to ``0`` to fail on the first retry. @@ -96,18 +139,35 @@ class Retry(object): Set to ``0`` to fail on the first retry of this type. - :param iterable method_whitelist: + :param int other: + How many times to retry on other errors. + + Other errors are errors that are not connect, read, redirect or status errors. + These errors might be raised after the request was sent to the server, so the + request might have side-effects. + + Set to ``0`` to fail on the first retry of this type. + + If ``total`` is not set, it's a good idea to set this to 0 to account + for unexpected edge cases and avoid infinite retry loops. + + :param iterable allowed_methods: Set of uppercased HTTP method verbs that we should retry on. By default, we only retry on methods which are considered to be idempotent (multiple requests with the same parameters end with the - same state). See :attr:`Retry.DEFAULT_METHOD_WHITELIST`. + same state). See :attr:`Retry.DEFAULT_ALLOWED_METHODS`. Set to a ``False`` value to retry on any verb. + .. warning:: + + Previously this parameter was named ``method_whitelist``, that + usage is deprecated in v1.26.0 and will be removed in v2.0. + :param iterable status_forcelist: A set of integer HTTP status codes that we should force a retry on. - A retry is initiated if the request method is in ``method_whitelist`` + A retry is initiated if the request method is in ``allowed_methods`` and the response status code is in ``status_forcelist``. By default, this is disabled with ``None``. @@ -148,13 +208,16 @@ class Retry(object): request. """ - DEFAULT_METHOD_WHITELIST = frozenset( + #: Default methods to be used for ``allowed_methods`` + DEFAULT_ALLOWED_METHODS = frozenset( ["HEAD", "GET", "PUT", "DELETE", "OPTIONS", "TRACE"] ) + #: Default status codes to be used for ``status_forcelist`` RETRY_AFTER_STATUS_CODES = frozenset([413, 429, 503]) - DEFAULT_REDIRECT_HEADERS_BLACKLIST = frozenset(["Authorization"]) + #: Default headers to be used for ``remove_headers_on_redirect`` + DEFAULT_REMOVE_HEADERS_ON_REDIRECT = frozenset(["Authorization"]) #: Maximum backoff time. BACKOFF_MAX = 120 @@ -166,20 +229,42 @@ def __init__( read=None, redirect=None, status=None, - method_whitelist=DEFAULT_METHOD_WHITELIST, + other=None, + allowed_methods=_Default, status_forcelist=None, backoff_factor=0, raise_on_redirect=True, raise_on_status=True, history=None, respect_retry_after_header=True, - remove_headers_on_redirect=DEFAULT_REDIRECT_HEADERS_BLACKLIST, + remove_headers_on_redirect=_Default, + # TODO: Deprecated, remove in v2.0 + method_whitelist=_Default, ): + if method_whitelist is not _Default: + if allowed_methods is not _Default: + raise ValueError( + "Using both 'allowed_methods' and " + "'method_whitelist' together is not allowed. " + "Instead only use 'allowed_methods'" + ) + warnings.warn( + "Using 'method_whitelist' with Retry is deprecated and " + "will be removed in v2.0. Use 'allowed_methods' instead", + DeprecationWarning, + ) + allowed_methods = method_whitelist + if allowed_methods is _Default: + allowed_methods = self.DEFAULT_ALLOWED_METHODS + if remove_headers_on_redirect is _Default: + remove_headers_on_redirect = self.DEFAULT_REMOVE_HEADERS_ON_REDIRECT + self.total = total self.connect = connect self.read = read self.status = status + self.other = other if redirect is False or total is False: redirect = 0 @@ -187,7 +272,7 @@ def __init__( self.redirect = redirect self.status_forcelist = status_forcelist or set() - self.method_whitelist = method_whitelist + self.allowed_methods = allowed_methods self.backoff_factor = backoff_factor self.raise_on_redirect = raise_on_redirect self.raise_on_status = raise_on_status @@ -204,7 +289,7 @@ def new(self, **kw): read=self.read, redirect=self.redirect, status=self.status, - method_whitelist=self.method_whitelist, + other=self.other, status_forcelist=self.status_forcelist, backoff_factor=self.backoff_factor, raise_on_redirect=self.raise_on_redirect, @@ -213,6 +298,23 @@ def new(self, **kw): remove_headers_on_redirect=self.remove_headers_on_redirect, respect_retry_after_header=self.respect_retry_after_header, ) + + # TODO: If already given in **kw we use what's given to us + # If not given we need to figure out what to pass. We decide + # based on whether our class has the 'method_whitelist' property + # and if so we pass the deprecated 'method_whitelist' otherwise + # we use 'allowed_methods'. Remove in v2.0 + if "method_whitelist" not in kw and "allowed_methods" not in kw: + if "method_whitelist" in self.__dict__: + warnings.warn( + "Using 'method_whitelist' with Retry is deprecated and " + "will be removed in v2.0. Use 'allowed_methods' instead", + DeprecationWarning, + ) + params["method_whitelist"] = self.allowed_methods + else: + params["allowed_methods"] = self.allowed_methods + params.update(kw) return type(self)(**params) @@ -231,7 +333,7 @@ def from_int(cls, retries, redirect=True, default=None): return new_retries def get_backoff_time(self): - """ Formula for computing the current backoff + """Formula for computing the current backoff :rtype: float """ @@ -252,10 +354,17 @@ def parse_retry_after(self, retry_after): if re.match(r"^\s*[0-9]+\s*$", retry_after): seconds = int(retry_after) else: - retry_date_tuple = email.utils.parsedate(retry_after) + retry_date_tuple = email.utils.parsedate_tz(retry_after) if retry_date_tuple is None: raise InvalidHeader("Invalid Retry-After header: %s" % retry_after) - retry_date = time.mktime(retry_date_tuple) + if retry_date_tuple[9] is None: # Python 2 + # Assume UTC if no timezone was specified + # On Python2.7, parsedate_tz returns None for a timezone offset + # instead of 0 if no timezone is given, where mktime_tz treats + # a None timezone offset as local time. + retry_date_tuple = retry_date_tuple[:9] + (0,) + retry_date_tuple[10:] + + retry_date = email.utils.mktime_tz(retry_date_tuple) seconds = retry_date - time.time() if seconds < 0: @@ -288,7 +397,7 @@ def _sleep_backoff(self): time.sleep(backoff) def sleep(self, response=None): - """ Sleep between retry attempts. + """Sleep between retry attempts. This method will respect a server's ``Retry-After`` response header and sleep the duration of the time requested. If that is not present, it @@ -304,7 +413,7 @@ def sleep(self, response=None): self._sleep_backoff() def _is_connection_error(self, err): - """ Errors when we're fairly sure that the server did not receive the + """Errors when we're fairly sure that the server did not receive the request, so it should be safe to retry. """ if isinstance(err, ProxyError): @@ -312,22 +421,33 @@ def _is_connection_error(self, err): return isinstance(err, ConnectTimeoutError) def _is_read_error(self, err): - """ Errors that occur after the request has been started, so we should + """Errors that occur after the request has been started, so we should assume that the server began processing it. """ return isinstance(err, (ReadTimeoutError, ProtocolError)) def _is_method_retryable(self, method): - """ Checks if a given HTTP method should be retried upon, depending if - it is included on the method whitelist. + """Checks if a given HTTP method should be retried upon, depending if + it is included in the allowed_methods """ - if self.method_whitelist and method.upper() not in self.method_whitelist: - return False + # TODO: For now favor if the Retry implementation sets its own method_whitelist + # property outside of our constructor to avoid breaking custom implementations. + if "method_whitelist" in self.__dict__: + warnings.warn( + "Using 'method_whitelist' with Retry is deprecated and " + "will be removed in v2.0. Use 'allowed_methods' instead", + DeprecationWarning, + ) + allowed_methods = self.method_whitelist + else: + allowed_methods = self.allowed_methods + if allowed_methods and method.upper() not in allowed_methods: + return False return True def is_retry(self, method, status_code, has_retry_after=False): - """ Is this method/status code retryable? (Based on whitelists and control + """Is this method/status code retryable? (Based on allowlists and control variables such as the number of total retries to allow, whether to respect the Retry-After header, whether this header is present, and whether the returned status code is on the list of status codes to @@ -348,7 +468,14 @@ def is_retry(self, method, status_code, has_retry_after=False): def is_exhausted(self): """ Are we out of retries? """ - retry_counts = (self.total, self.connect, self.read, self.redirect, self.status) + retry_counts = ( + self.total, + self.connect, + self.read, + self.redirect, + self.status, + self.other, + ) retry_counts = list(filter(None, retry_counts)) if not retry_counts: return False @@ -364,7 +491,7 @@ def increment( _pool=None, _stacktrace=None, ): - """ Return a new Retry object with incremented retry counters. + """Return a new Retry object with incremented retry counters. :param response: A response object, or None, if the server did not return a response. @@ -386,6 +513,7 @@ def increment( read = self.read redirect = self.redirect status_count = self.status + other = self.other cause = "unknown" status = None redirect_location = None @@ -404,6 +532,11 @@ def increment( elif read is not None: read -= 1 + elif error: + # Other retry? + if other is not None: + other -= 1 + elif response and response.get_redirect_location(): # Redirect retry? if redirect is not None: @@ -414,7 +547,7 @@ def increment( else: # Incrementing because of a server error like a 500 in - # status_forcelist and a the given method is in the whitelist + # status_forcelist and the given method is in the allowed_methods cause = ResponseError.GENERIC_ERROR if response and response.status: if status_count is not None: @@ -432,6 +565,7 @@ def increment( read=read, redirect=redirect, status=status_count, + other=other, history=history, ) @@ -448,6 +582,20 @@ def __repr__(self): "read={self.read}, redirect={self.redirect}, status={self.status})" ).format(cls=type(self), self=self) + def __getattr__(self, item): + if item == "method_whitelist": + # TODO: Remove this deprecated alias in v2.0 + warnings.warn( + "Using 'method_whitelist' with Retry is deprecated and " + "will be removed in v2.0. Use 'allowed_methods' instead", + DeprecationWarning, + ) + return self.allowed_methods + try: + return getattr(super(Retry, self), item) + except AttributeError: + return getattr(Retry, item) + # For backwards compatibility (equivalent to pre-v1.9): Retry.DEFAULT = Retry(3) diff --git a/src/pip/_vendor/urllib3/util/ssl_.py b/src/pip/_vendor/urllib3/util/ssl_.py index d3b463d49f5..763da82bb66 100644 --- a/src/pip/_vendor/urllib3/util/ssl_.py +++ b/src/pip/_vendor/urllib3/util/ssl_.py @@ -1,21 +1,27 @@ from __future__ import absolute_import -import errno -import warnings + import hmac +import os import sys - +import warnings from binascii import hexlify, unhexlify from hashlib import md5, sha1, sha256 -from .url import IPV4_RE, BRACELESS_IPV6_ADDRZ_RE -from ..exceptions import SSLError, InsecurePlatformWarning, SNIMissingWarning +from ..exceptions import ( + InsecurePlatformWarning, + ProxySchemeUnsupported, + SNIMissingWarning, + SSLError, +) from ..packages import six - +from .url import BRACELESS_IPV6_ADDRZ_RE, IPV4_RE SSLContext = None +SSLTransport = None HAS_SNI = False IS_PYOPENSSL = False IS_SECURETRANSPORT = False +ALPN_PROTOCOLS = ["http/1.1"] # Maps the length of a digest to a possible hash function producing this digest HASHFUNC_MAP = {32: md5, 40: sha1, 64: sha256} @@ -29,8 +35,8 @@ def _const_compare_digest_backport(a, b): Returns True if the digests match, and False otherwise. """ result = abs(len(a) - len(b)) - for l, r in zip(bytearray(a), bytearray(b)): - result |= l ^ r + for left, right in zip(bytearray(a), bytearray(b)): + result |= left ^ right return result == 0 @@ -38,11 +44,21 @@ def _const_compare_digest_backport(a, b): try: # Test for SSL features import ssl - from ssl import wrap_socket, CERT_REQUIRED + from ssl import CERT_REQUIRED, wrap_socket +except ImportError: + pass + +try: from ssl import HAS_SNI # Has SNI? except ImportError: pass +try: + from .ssltransport import SSLTransport +except ImportError: + pass + + try: # Platform-specific: Python 3.6 from ssl import PROTOCOL_TLS @@ -57,12 +73,18 @@ def _const_compare_digest_backport(a, b): try: - from ssl import OP_NO_SSLv2, OP_NO_SSLv3, OP_NO_COMPRESSION + from ssl import OP_NO_COMPRESSION, OP_NO_SSLv2, OP_NO_SSLv3 except ImportError: OP_NO_SSLv2, OP_NO_SSLv3 = 0x1000000, 0x2000000 OP_NO_COMPRESSION = 0x20000 +try: # OP_NO_TICKET was added in Python 3.6 + from ssl import OP_NO_TICKET +except ImportError: + OP_NO_TICKET = 0x4000 + + # A secure default. # Sources for more information on TLS ciphers: # @@ -249,7 +271,7 @@ def create_urllib3_context( ``ssl.CERT_REQUIRED``. :param options: Specific OpenSSL options. These default to ``ssl.OP_NO_SSLv2``, - ``ssl.OP_NO_SSLv3``, ``ssl.OP_NO_COMPRESSION``. + ``ssl.OP_NO_SSLv3``, ``ssl.OP_NO_COMPRESSION``, and ``ssl.OP_NO_TICKET``. :param ciphers: Which cipher suites to allow the server to select. :returns: @@ -272,6 +294,11 @@ def create_urllib3_context( # Disable compression to prevent CRIME attacks for OpenSSL 1.0+ # (issue #309) options |= OP_NO_COMPRESSION + # TLSv1.2 only. Unless set explicitly, do not request tickets. + # This may save some bandwidth on wire, and although the ticket is encrypted, + # there is a risk associated with it being on wire, + # if the server is not rotating its ticketing keys properly. + options |= OP_NO_TICKET context.options |= options @@ -293,6 +320,14 @@ def create_urllib3_context( # We do our own verification, including fingerprints and alternative # hostnames. So disable it here context.check_hostname = False + + # Enable logging of TLS session keys via defacto standard environment variable + # 'SSLKEYLOGFILE', if the feature is available (Python 3.8+). Skip empty values. + if hasattr(context, "keylog_filename"): + sslkeylogfile = os.environ.get("SSLKEYLOGFILE") + if sslkeylogfile: + context.keylog_filename = sslkeylogfile + return context @@ -309,6 +344,7 @@ def ssl_wrap_socket( ca_cert_dir=None, key_password=None, ca_cert_data=None, + tls_in_tls=False, ): """ All arguments except for server_hostname, ssl_context, and ca_cert_dir have @@ -330,6 +366,8 @@ def ssl_wrap_socket( :param ca_cert_data: Optional string containing CA certificates in PEM format suitable for passing as the cadata parameter to SSLContext.load_verify_locations() + :param tls_in_tls: + Use SSLTransport to wrap the existing socket. """ context = ssl_context if context is None: @@ -341,14 +379,8 @@ def ssl_wrap_socket( if ca_certs or ca_cert_dir or ca_cert_data: try: context.load_verify_locations(ca_certs, ca_cert_dir, ca_cert_data) - except IOError as e: # Platform-specific: Python 2.7 + except (IOError, OSError) as e: raise SSLError(e) - # Py33 raises FileNotFoundError which subclasses OSError - # These are not equivalent unless we check the errno attribute - except OSError as e: # Platform-specific: Python 3.3 and beyond - if e.errno == errno.ENOENT: - raise SSLError(e) - raise elif ssl_context is None and hasattr(context, "load_default_certs"): # try to load OS default certs; works well on Windows (require Python3.4+) @@ -366,16 +398,21 @@ def ssl_wrap_socket( else: context.load_cert_chain(certfile, keyfile, key_password) + try: + if hasattr(context, "set_alpn_protocols"): + context.set_alpn_protocols(ALPN_PROTOCOLS) + except NotImplementedError: + pass + # If we detect server_hostname is an IP address then the SNI # extension should not be used according to RFC3546 Section 3.1 - # We shouldn't warn the user if SNI isn't available but we would - # not be using SNI anyways due to IP address for server_hostname. - if ( - server_hostname is not None and not is_ipaddress(server_hostname) - ) or IS_SECURETRANSPORT: - if HAS_SNI and server_hostname is not None: - return context.wrap_socket(sock, server_hostname=server_hostname) - + use_sni_hostname = server_hostname and not is_ipaddress(server_hostname) + # SecureTransport uses server_hostname in certificate verification. + send_sni = (use_sni_hostname and HAS_SNI) or ( + IS_SECURETRANSPORT and server_hostname + ) + # Do not warn the user if server_hostname is an invalid SNI hostname. + if not HAS_SNI and use_sni_hostname: warnings.warn( "An HTTPS request has been made, but the SNI (Server Name " "Indication) extension to TLS is not available on this platform. " @@ -387,7 +424,13 @@ def ssl_wrap_socket( SNIMissingWarning, ) - return context.wrap_socket(sock) + if send_sni: + ssl_sock = _ssl_wrap_socket_impl( + sock, context, tls_in_tls, server_hostname=server_hostname + ) + else: + ssl_sock = _ssl_wrap_socket_impl(sock, context, tls_in_tls) + return ssl_sock def is_ipaddress(hostname): @@ -412,3 +455,20 @@ def _is_key_file_encrypted(key_file): return True return False + + +def _ssl_wrap_socket_impl(sock, ssl_context, tls_in_tls, server_hostname=None): + if tls_in_tls: + if not SSLTransport: + # Import error, ssl is not available. + raise ProxySchemeUnsupported( + "TLS in TLS requires support for the 'ssl' module" + ) + + SSLTransport._validate_ssl_context_for_tls_in_tls(ssl_context) + return SSLTransport(sock, ssl_context, server_hostname) + + if server_hostname: + return ssl_context.wrap_socket(sock, server_hostname=server_hostname) + else: + return ssl_context.wrap_socket(sock) diff --git a/src/pip/_vendor/urllib3/util/ssltransport.py b/src/pip/_vendor/urllib3/util/ssltransport.py new file mode 100644 index 00000000000..ca00233c931 --- /dev/null +++ b/src/pip/_vendor/urllib3/util/ssltransport.py @@ -0,0 +1,221 @@ +import io +import socket +import ssl + +from pip._vendor.urllib3.exceptions import ProxySchemeUnsupported +from pip._vendor.urllib3.packages import six + +SSL_BLOCKSIZE = 16384 + + +class SSLTransport: + """ + The SSLTransport wraps an existing socket and establishes an SSL connection. + + Contrary to Python's implementation of SSLSocket, it allows you to chain + multiple TLS connections together. It's particularly useful if you need to + implement TLS within TLS. + + The class supports most of the socket API operations. + """ + + @staticmethod + def _validate_ssl_context_for_tls_in_tls(ssl_context): + """ + Raises a ProxySchemeUnsupported if the provided ssl_context can't be used + for TLS in TLS. + + The only requirement is that the ssl_context provides the 'wrap_bio' + methods. + """ + + if not hasattr(ssl_context, "wrap_bio"): + if six.PY2: + raise ProxySchemeUnsupported( + "TLS in TLS requires SSLContext.wrap_bio() which isn't " + "supported on Python 2" + ) + else: + raise ProxySchemeUnsupported( + "TLS in TLS requires SSLContext.wrap_bio() which isn't " + "available on non-native SSLContext" + ) + + def __init__( + self, socket, ssl_context, server_hostname=None, suppress_ragged_eofs=True + ): + """ + Create an SSLTransport around socket using the provided ssl_context. + """ + self.incoming = ssl.MemoryBIO() + self.outgoing = ssl.MemoryBIO() + + self.suppress_ragged_eofs = suppress_ragged_eofs + self.socket = socket + + self.sslobj = ssl_context.wrap_bio( + self.incoming, self.outgoing, server_hostname=server_hostname + ) + + # Perform initial handshake. + self._ssl_io_loop(self.sslobj.do_handshake) + + def __enter__(self): + return self + + def __exit__(self, *_): + self.close() + + def fileno(self): + return self.socket.fileno() + + def read(self, len=1024, buffer=None): + return self._wrap_ssl_read(len, buffer) + + def recv(self, len=1024, flags=0): + if flags != 0: + raise ValueError("non-zero flags not allowed in calls to recv") + return self._wrap_ssl_read(len) + + def recv_into(self, buffer, nbytes=None, flags=0): + if flags != 0: + raise ValueError("non-zero flags not allowed in calls to recv_into") + if buffer and (nbytes is None): + nbytes = len(buffer) + elif nbytes is None: + nbytes = 1024 + return self.read(nbytes, buffer) + + def sendall(self, data, flags=0): + if flags != 0: + raise ValueError("non-zero flags not allowed in calls to sendall") + count = 0 + with memoryview(data) as view, view.cast("B") as byte_view: + amount = len(byte_view) + while count < amount: + v = self.send(byte_view[count:]) + count += v + + def send(self, data, flags=0): + if flags != 0: + raise ValueError("non-zero flags not allowed in calls to send") + response = self._ssl_io_loop(self.sslobj.write, data) + return response + + def makefile( + self, mode="r", buffering=None, encoding=None, errors=None, newline=None + ): + """ + Python's httpclient uses makefile and buffered io when reading HTTP + messages and we need to support it. + + This is unfortunately a copy and paste of socket.py makefile with small + changes to point to the socket directly. + """ + if not set(mode) <= {"r", "w", "b"}: + raise ValueError("invalid mode %r (only r, w, b allowed)" % (mode,)) + + writing = "w" in mode + reading = "r" in mode or not writing + assert reading or writing + binary = "b" in mode + rawmode = "" + if reading: + rawmode += "r" + if writing: + rawmode += "w" + raw = socket.SocketIO(self, rawmode) + self.socket._io_refs += 1 + if buffering is None: + buffering = -1 + if buffering < 0: + buffering = io.DEFAULT_BUFFER_SIZE + if buffering == 0: + if not binary: + raise ValueError("unbuffered streams must be binary") + return raw + if reading and writing: + buffer = io.BufferedRWPair(raw, raw, buffering) + elif reading: + buffer = io.BufferedReader(raw, buffering) + else: + assert writing + buffer = io.BufferedWriter(raw, buffering) + if binary: + return buffer + text = io.TextIOWrapper(buffer, encoding, errors, newline) + text.mode = mode + return text + + def unwrap(self): + self._ssl_io_loop(self.sslobj.unwrap) + + def close(self): + self.socket.close() + + def getpeercert(self, binary_form=False): + return self.sslobj.getpeercert(binary_form) + + def version(self): + return self.sslobj.version() + + def cipher(self): + return self.sslobj.cipher() + + def selected_alpn_protocol(self): + return self.sslobj.selected_alpn_protocol() + + def selected_npn_protocol(self): + return self.sslobj.selected_npn_protocol() + + def shared_ciphers(self): + return self.sslobj.shared_ciphers() + + def compression(self): + return self.sslobj.compression() + + def settimeout(self, value): + self.socket.settimeout(value) + + def gettimeout(self): + return self.socket.gettimeout() + + def _decref_socketios(self): + self.socket._decref_socketios() + + def _wrap_ssl_read(self, len, buffer=None): + try: + return self._ssl_io_loop(self.sslobj.read, len, buffer) + except ssl.SSLError as e: + if e.errno == ssl.SSL_ERROR_EOF and self.suppress_ragged_eofs: + return 0 # eof, return 0. + else: + raise + + def _ssl_io_loop(self, func, *args): + """ Performs an I/O loop between incoming/outgoing and the socket.""" + should_loop = True + ret = None + + while should_loop: + errno = None + try: + ret = func(*args) + except ssl.SSLError as e: + if e.errno not in (ssl.SSL_ERROR_WANT_READ, ssl.SSL_ERROR_WANT_WRITE): + # WANT_READ, and WANT_WRITE are expected, others are not. + raise e + errno = e.errno + + buf = self.outgoing.read() + self.socket.sendall(buf) + + if errno is None: + should_loop = False + elif errno == ssl.SSL_ERROR_WANT_READ: + buf = self.socket.recv(SSL_BLOCKSIZE) + if buf: + self.incoming.write(buf) + else: + self.incoming.write_eof() + return ret diff --git a/src/pip/_vendor/urllib3/util/timeout.py b/src/pip/_vendor/urllib3/util/timeout.py index b61fea75c50..ff69593b05b 100644 --- a/src/pip/_vendor/urllib3/util/timeout.py +++ b/src/pip/_vendor/urllib3/util/timeout.py @@ -1,9 +1,10 @@ from __future__ import absolute_import +import time + # The default socket timeout, used by httplib to indicate that no timeout was # specified by the user from socket import _GLOBAL_DEFAULT_TIMEOUT -import time from ..exceptions import TimeoutStateError @@ -17,22 +18,28 @@ class Timeout(object): - """ Timeout configuration. + """Timeout configuration. + + Timeouts can be defined as a default for a pool: + + .. code-block:: python + + timeout = Timeout(connect=2.0, read=7.0) + http = PoolManager(timeout=timeout) + response = http.request('GET', 'http://example.com/') - Timeouts can be defined as a default for a pool:: + Or per-request (which overrides the default for the pool): - timeout = Timeout(connect=2.0, read=7.0) - http = PoolManager(timeout=timeout) - response = http.request('GET', 'http://example.com/') + .. code-block:: python - Or per-request (which overrides the default for the pool):: + response = http.request('GET', 'http://example.com/', timeout=Timeout(10)) - response = http.request('GET', 'http://example.com/', timeout=Timeout(10)) + Timeouts can be disabled by setting all the parameters to ``None``: - Timeouts can be disabled by setting all the parameters to ``None``:: + .. code-block:: python - no_timeout = Timeout(connect=None, read=None) - response = http.request('GET', 'http://example.com/, timeout=no_timeout) + no_timeout = Timeout(connect=None, read=None) + response = http.request('GET', 'http://example.com/, timeout=no_timeout) :param total: @@ -43,7 +50,7 @@ class Timeout(object): Defaults to None. - :type total: integer, float, or None + :type total: int, float, or None :param connect: The maximum amount of time (in seconds) to wait for a connection @@ -53,7 +60,7 @@ class Timeout(object): <http://hg.python.org/cpython/file/603b4d593758/Lib/socket.py#l535>`_. None will set an infinite timeout for connection attempts. - :type connect: integer, float, or None + :type connect: int, float, or None :param read: The maximum amount of time (in seconds) to wait between consecutive @@ -63,7 +70,7 @@ class Timeout(object): <http://hg.python.org/cpython/file/603b4d593758/Lib/socket.py#l535>`_. None will set an infinite timeout. - :type read: integer, float, or None + :type read: int, float, or None .. note:: @@ -111,7 +118,7 @@ def __repr__(self): @classmethod def _validate_timeout(cls, value, name): - """ Check that a timeout attribute is valid. + """Check that a timeout attribute is valid. :param value: The timeout value to validate :param name: The name of the timeout attribute to validate. This is @@ -157,7 +164,7 @@ def _validate_timeout(cls, value, name): @classmethod def from_float(cls, timeout): - """ Create a new Timeout from a legacy timeout value. + """Create a new Timeout from a legacy timeout value. The timeout value used by httplib.py sets the same timeout on the connect(), and recv() socket requests. This creates a :class:`Timeout` @@ -172,7 +179,7 @@ def from_float(cls, timeout): return Timeout(read=timeout, connect=timeout) def clone(self): - """ Create a copy of the timeout object + """Create a copy of the timeout object Timeout properties are stored per-pool but each request needs a fresh Timeout object to ensure each one has its own start/stop configured. @@ -186,7 +193,7 @@ def clone(self): return Timeout(connect=self._connect, read=self._read, total=self.total) def start_connect(self): - """ Start the timeout clock, used during a connect() attempt + """Start the timeout clock, used during a connect() attempt :raises urllib3.exceptions.TimeoutStateError: if you attempt to start a timer that has been started already. @@ -197,7 +204,7 @@ def start_connect(self): return self._start_connect def get_connect_duration(self): - """ Gets the time elapsed since the call to :meth:`start_connect`. + """Gets the time elapsed since the call to :meth:`start_connect`. :return: Elapsed time in seconds. :rtype: float @@ -212,7 +219,7 @@ def get_connect_duration(self): @property def connect_timeout(self): - """ Get the value to use when setting a connection timeout. + """Get the value to use when setting a connection timeout. This will be a positive float or integer, the value None (never timeout), or the default system timeout. @@ -230,7 +237,7 @@ def connect_timeout(self): @property def read_timeout(self): - """ Get the value for the read timeout. + """Get the value for the read timeout. This assumes some time has elapsed in the connection timeout and computes the read timeout appropriately. diff --git a/src/pip/_vendor/urllib3/util/url.py b/src/pip/_vendor/urllib3/util/url.py index 0eb0b6a8cc5..66c8795b11e 100644 --- a/src/pip/_vendor/urllib3/util/url.py +++ b/src/pip/_vendor/urllib3/util/url.py @@ -1,11 +1,11 @@ from __future__ import absolute_import + import re from collections import namedtuple from ..exceptions import LocationParseError from ..packages import six - url_attrs = ["scheme", "auth", "host", "port", "path", "query", "fragment"] # We only want to normalize urls with an HTTP(S) scheme. diff --git a/src/pip/_vendor/urllib3/util/wait.py b/src/pip/_vendor/urllib3/util/wait.py index d71d2fd722b..c280646c7be 100644 --- a/src/pip/_vendor/urllib3/util/wait.py +++ b/src/pip/_vendor/urllib3/util/wait.py @@ -1,7 +1,7 @@ import errno -from functools import partial import select import sys +from functools import partial try: from time import monotonic @@ -140,14 +140,14 @@ def wait_for_socket(*args, **kwargs): def wait_for_read(sock, timeout=None): - """ Waits for reading to be available on a given socket. + """Waits for reading to be available on a given socket. Returns True if the socket is readable, or False if the timeout expired. """ return wait_for_socket(sock, read=True, timeout=timeout) def wait_for_write(sock, timeout=None): - """ Waits for writing to be available on a given socket. + """Waits for writing to be available on a given socket. Returns True if the socket is readable, or False if the timeout expired. """ return wait_for_socket(sock, write=True, timeout=timeout) diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index f08348b9fc8..5943f971def 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -15,7 +15,7 @@ requests==2.25.0 certifi==2020.11.8 chardet==3.0.4 idna==2.10 - urllib3==1.25.9 + urllib3==1.26.2 resolvelib==0.4.0 retrying==1.3.3 setuptools==44.0.0 From 910b304a9750286506ba234c8390ae0fa4256d01 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Thu, 19 Nov 2020 14:21:14 +0000 Subject: [PATCH 2721/3170] Upgrade resolvelib to 0.5.2 --- news/resolvelib.vendor.rst | 1 + src/pip/_vendor/resolvelib/__init__.py | 2 +- src/pip/_vendor/resolvelib/providers.py | 46 ++++++++----- src/pip/_vendor/resolvelib/reporters.py | 15 ++-- src/pip/_vendor/resolvelib/resolvers.py | 53 ++++++-------- src/pip/_vendor/resolvelib/structs.py | 91 ++++++++++++++++++++++--- src/pip/_vendor/vendor.txt | 2 +- 7 files changed, 139 insertions(+), 71 deletions(-) create mode 100644 news/resolvelib.vendor.rst diff --git a/news/resolvelib.vendor.rst b/news/resolvelib.vendor.rst new file mode 100644 index 00000000000..97e4f4a8e58 --- /dev/null +++ b/news/resolvelib.vendor.rst @@ -0,0 +1 @@ +Upgrade resolvelib to 0.5.2 diff --git a/src/pip/_vendor/resolvelib/__init__.py b/src/pip/_vendor/resolvelib/__init__.py index 3b444545de0..78ede4fd1a8 100644 --- a/src/pip/_vendor/resolvelib/__init__.py +++ b/src/pip/_vendor/resolvelib/__init__.py @@ -11,7 +11,7 @@ "ResolutionTooDeep", ] -__version__ = "0.4.0" +__version__ = "0.5.2" from .providers import AbstractProvider, AbstractResolver diff --git a/src/pip/_vendor/resolvelib/providers.py b/src/pip/_vendor/resolvelib/providers.py index 68b7290dfa0..965cf9c138f 100644 --- a/src/pip/_vendor/resolvelib/providers.py +++ b/src/pip/_vendor/resolvelib/providers.py @@ -1,32 +1,36 @@ class AbstractProvider(object): - """Delegate class to provide requirement interface for the resolver. - """ + """Delegate class to provide requirement interface for the resolver.""" - def identify(self, dependency): - """Given a dependency, return an identifier for it. + def identify(self, requirement_or_candidate): + """Given a requirement or candidate, return an identifier for it. - This is used in many places to identify the dependency, e.g. whether - two requirements should have their specifier parts merged, whether - two specifications would conflict with each other (because they the - same name but different versions). + This is used in many places to identify a requirement or candidate, + e.g. whether two requirements should have their specifier parts merged, + whether two candidates would conflict with each other (because they + have same name but different versions). """ raise NotImplementedError def get_preference(self, resolution, candidates, information): - """Produce a sort key for given specification based on preference. + """Produce a sort key for given requirement based on preference. The preference is defined as "I think this requirement should be resolved first". The lower the return value is, the more preferred this group of arguments is. :param resolution: Currently pinned candidate, or `None`. - :param candidates: A list of possible candidates. + :param candidates: An iterable of possible candidates. :param information: A list of requirement information. - Each information instance is a named tuple with two entries: + The `candidates` iterable's exact type depends on the return type of + `find_matches()`. A sequence is passed-in as-is if possible. If it + returns a callble, the iterator returned by that callable is passed + in here. + + Each element in `information` is a named tuple with two entries: * `requirement` specifies a requirement contributing to the current - candidate list + candidate list. * `parent` specifies the candidate that provides (dependend on) the requirement, or `None` to indicate a root requirement. @@ -43,7 +47,7 @@ def get_preference(self, resolution, candidates, information): A sortable value should be returned (this will be used as the `key` parameter of the built-in sorting function). The smaller the value is, - the more preferred this specification is (i.e. the sorting function + the more preferred this requirement is (i.e. the sorting function is called with `reverse=False`). """ raise NotImplementedError @@ -56,11 +60,18 @@ def find_matches(self, requirements): returned, and for a "named" requirement, the index(es) should be consulted to find concrete candidates for this requirement. - :param requirements: A collection of requirements which all of the the + The return value should produce candidates ordered by preference; the + most preferred candidate should come first. The return type may be one + of the following: + + * A callable that returns an iterator that yields candidates. + * An collection of candidates. + * An iterable of candidates. This will be consumed immediately into a + list of candidates. + + :param requirements: A collection of requirements which all of the returned candidates must match. All requirements are guaranteed to have the same identifier. The collection is never empty. - :returns: An iterable that orders candidates by preference, e.g. the - most preferred candidate should come first. """ raise NotImplementedError @@ -85,8 +96,7 @@ def get_dependencies(self, candidate): class AbstractResolver(object): - """The thing that performs the actual resolution work. - """ + """The thing that performs the actual resolution work.""" base_exception = Exception diff --git a/src/pip/_vendor/resolvelib/reporters.py b/src/pip/_vendor/resolvelib/reporters.py index a0a2a458844..563489e133b 100644 --- a/src/pip/_vendor/resolvelib/reporters.py +++ b/src/pip/_vendor/resolvelib/reporters.py @@ -1,10 +1,8 @@ class BaseReporter(object): - """Delegate class to provider progress reporting for the resolver. - """ + """Delegate class to provider progress reporting for the resolver.""" def starting(self): - """Called before the resolution actually starts. - """ + """Called before the resolution actually starts.""" def starting_round(self, index): """Called before each round of resolution starts. @@ -20,8 +18,7 @@ def ending_round(self, index, state): """ def ending(self, state): - """Called before the resolution ends successfully. - """ + """Called before the resolution ends successfully.""" def adding_requirement(self, requirement, parent): """Called when adding a new requirement into the resolve criteria. @@ -34,9 +31,7 @@ def adding_requirement(self, requirement, parent): """ def backtracking(self, candidate): - """Called when rejecting a candidate during backtracking. - """ + """Called when rejecting a candidate during backtracking.""" def pinning(self, candidate): - """Called when adding a candidate to the potential solution. - """ + """Called when adding a candidate to the potential solution.""" diff --git a/src/pip/_vendor/resolvelib/resolvers.py b/src/pip/_vendor/resolvelib/resolvers.py index 4497f976a86..976608b1775 100644 --- a/src/pip/_vendor/resolvelib/resolvers.py +++ b/src/pip/_vendor/resolvelib/resolvers.py @@ -1,8 +1,7 @@ import collections -from .compat import collections_abc from .providers import AbstractResolver -from .structs import DirectedGraph +from .structs import DirectedGraph, build_iter_view RequirementInformation = collections.namedtuple( @@ -76,17 +75,11 @@ def __repr__(self): @classmethod def from_requirement(cls, provider, requirement, parent): - """Build an instance from a requirement. - """ - candidates = provider.find_matches([requirement]) - if not isinstance(candidates, collections_abc.Sequence): - candidates = list(candidates) - criterion = cls( - candidates=candidates, - information=[RequirementInformation(requirement, parent)], - incompatibilities=[], - ) - if not candidates: + """Build an instance from a requirement.""" + cands = build_iter_view(provider.find_matches([requirement])) + infos = [RequirementInformation(requirement, parent)] + criterion = cls(cands, infos, incompatibilities=[]) + if not cands: raise RequirementsConflicted(criterion) return criterion @@ -97,15 +90,12 @@ def iter_parent(self): return (i.parent for i in self.information) def merged_with(self, provider, requirement, parent): - """Build a new instance from this and a new requirement. - """ + """Build a new instance from this and a new requirement.""" infos = list(self.information) infos.append(RequirementInformation(requirement, parent)) - candidates = provider.find_matches([r for r, _ in infos]) - if not isinstance(candidates, collections_abc.Sequence): - candidates = list(candidates) - criterion = type(self)(candidates, infos, list(self.incompatibilities)) - if not candidates: + cands = build_iter_view(provider.find_matches([r for r, _ in infos])) + criterion = type(self)(cands, infos, list(self.incompatibilities)) + if not cands: raise RequirementsConflicted(criterion) return criterion @@ -114,13 +104,12 @@ def excluded_of(self, candidate): Returns the new instance, or None if we still have no valid candidates. """ + cands = self.candidates.excluding(candidate) + if not cands: + return None incompats = list(self.incompatibilities) incompats.append(candidate) - candidates = [c for c in self.candidates if c != candidate] - if not candidates: - return None - criterion = type(self)(candidates, list(self.information), incompats) - return criterion + return type(self)(cands, list(self.information), incompats) class ResolutionError(ResolverException): @@ -175,7 +164,8 @@ def _push_new_state(self): state = State(mapping=collections.OrderedDict(), criteria={}) else: state = State( - mapping=base.mapping.copy(), criteria=base.criteria.copy(), + mapping=base.mapping.copy(), + criteria=base.criteria.copy(), ) self._states.append(state) @@ -192,12 +182,10 @@ def _merge_into_criterion(self, requirement, parent): def _get_criterion_item_preference(self, item): name, criterion = item - try: - pinned = self.state.mapping[name] - except KeyError: - pinned = None return self._p.get_preference( - pinned, criterion.candidates, criterion.information, + self.state.mapping.get(name), + criterion.candidates.for_preference(), + criterion.information, ) def _is_current_pin_satisfying(self, name, criterion): @@ -390,8 +378,7 @@ def _build_result(state): class Resolver(AbstractResolver): - """The thing that performs the actual resolution work. - """ + """The thing that performs the actual resolution work.""" base_exception = ResolverException diff --git a/src/pip/_vendor/resolvelib/structs.py b/src/pip/_vendor/resolvelib/structs.py index 1eee08b383a..479aad5dc17 100644 --- a/src/pip/_vendor/resolvelib/structs.py +++ b/src/pip/_vendor/resolvelib/structs.py @@ -1,6 +1,8 @@ +from .compat import collections_abc + + class DirectedGraph(object): - """A graph structure with directed edges. - """ + """A graph structure with directed edges.""" def __init__(self): self._vertices = set() @@ -17,8 +19,7 @@ def __contains__(self, key): return key in self._vertices def copy(self): - """Return a shallow copy of this graph. - """ + """Return a shallow copy of this graph.""" other = DirectedGraph() other._vertices = set(self._vertices) other._forwards = {k: set(v) for k, v in self._forwards.items()} @@ -26,8 +27,7 @@ def copy(self): return other def add(self, key): - """Add a new vertex to the graph. - """ + """Add a new vertex to the graph.""" if key in self._vertices: raise ValueError("vertex exists") self._vertices.add(key) @@ -35,8 +35,7 @@ def add(self, key): self._backwards[key] = set() def remove(self, key): - """Remove a vertex from the graph, disconnecting all edges from/to it. - """ + """Remove a vertex from the graph, disconnecting all edges from/to it.""" self._vertices.remove(key) for f in self._forwards.pop(key): self._backwards[f].remove(key) @@ -66,3 +65,79 @@ def iter_children(self, key): def iter_parents(self, key): return iter(self._backwards[key]) + + +class _FactoryIterableView(object): + """Wrap an iterator factory returned by `find_matches()`. + + Calling `iter()` on this class would invoke the underlying iterator + factory, making it a "collection with ordering" that can be iterated + through multiple times, but lacks random access methods presented in + built-in Python sequence types. + """ + + def __init__(self, factory): + self._factory = factory + + def __bool__(self): + try: + next(self._factory()) + except StopIteration: + return False + return True + + __nonzero__ = __bool__ # XXX: Python 2. + + def __iter__(self): + return self._factory() + + def for_preference(self): + """Provide an candidate iterable for `get_preference()`""" + return self._factory() + + def excluding(self, candidate): + """Create a new `Candidates` instance excluding `candidate`.""" + + def factory(): + return (c for c in self._factory() if c != candidate) + + return type(self)(factory) + + +class _SequenceIterableView(object): + """Wrap an iterable returned by find_matches(). + + This is essentially just a proxy to the underlying sequence that provides + the same interface as `_FactoryIterableView`. + """ + + def __init__(self, sequence): + self._sequence = sequence + + def __bool__(self): + return bool(self._sequence) + + __nonzero__ = __bool__ # XXX: Python 2. + + def __iter__(self): + return iter(self._sequence) + + def __len__(self): + return len(self._sequence) + + def for_preference(self): + """Provide an candidate iterable for `get_preference()`""" + return self._sequence + + def excluding(self, candidate): + """Create a new instance excluding `candidate`.""" + return type(self)([c for c in self._sequence if c != candidate]) + + +def build_iter_view(matches): + """Build an iterable view from the value returned by `find_matches()`.""" + if callable(matches): + return _FactoryIterableView(matches) + if not isinstance(matches, collections_abc.Sequence): + matches = list(matches) + return _SequenceIterableView(matches) diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 5943f971def..468107bb648 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -16,7 +16,7 @@ requests==2.25.0 chardet==3.0.4 idna==2.10 urllib3==1.26.2 -resolvelib==0.4.0 +resolvelib==0.5.2 retrying==1.3.3 setuptools==44.0.0 six==1.15.0 From 0f43e77b7fb975a4dc205bbc55e2130ee9cf3c1a Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Thu, 19 Nov 2020 16:14:42 +0000 Subject: [PATCH 2722/3170] Include package name in backtracking messaging Signed-off-by: Pradyun Gedam <pradyunsg@users.noreply.github.com> --- src/pip/_internal/resolution/resolvelib/reporter.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/reporter.py b/src/pip/_internal/resolution/resolvelib/reporter.py index 376265c2d03..d0ef3fadc67 100644 --- a/src/pip/_internal/resolution/resolvelib/reporter.py +++ b/src/pip/_internal/resolution/resolvelib/reporter.py @@ -22,12 +22,12 @@ def __init__(self): self._messages_at_backtrack = { 1: ( - "pip is looking at multiple versions of this package to " + "pip is looking at multiple versions of {package_name} to " "determine which version is compatible with other " "requirements. This could take a while." ), 8: ( - "pip is looking at multiple versions of this package to " + "pip is looking at multiple versions of {package_name} to " "determine which version is compatible with other " "requirements. This could take a while." ), @@ -49,7 +49,7 @@ def backtracking(self, candidate): return message = self._messages_at_backtrack[count] - logger.info("INFO: %s", message) + logger.info("INFO: %s", message.format(package_name=candidate.name)) class PipDebuggingReporter(BaseReporter): From 74b1db4f642c20f61001336b514d1c866d8a5446 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Thu, 19 Nov 2020 14:22:01 +0000 Subject: [PATCH 2723/3170] Upgrade toml to 0.10.2 --- news/toml.vendor.rst | 1 + src/pip/_vendor/toml/__init__.py | 2 +- src/pip/_vendor/toml/common.py | 6 ------ src/pip/_vendor/toml/decoder.py | 7 ++++++- src/pip/_vendor/toml/encoder.py | 2 +- src/pip/_vendor/toml/tz.py | 3 +++ src/pip/_vendor/vendor.txt | 2 +- 7 files changed, 13 insertions(+), 10 deletions(-) create mode 100644 news/toml.vendor.rst delete mode 100644 src/pip/_vendor/toml/common.py diff --git a/news/toml.vendor.rst b/news/toml.vendor.rst new file mode 100644 index 00000000000..566c104f80c --- /dev/null +++ b/news/toml.vendor.rst @@ -0,0 +1 @@ +Upgrade toml to 0.10.2 diff --git a/src/pip/_vendor/toml/__init__.py b/src/pip/_vendor/toml/__init__.py index 7a08fe72540..34a5eabb6ea 100644 --- a/src/pip/_vendor/toml/__init__.py +++ b/src/pip/_vendor/toml/__init__.py @@ -6,7 +6,7 @@ from pip._vendor.toml import encoder from pip._vendor.toml import decoder -__version__ = "0.10.1" +__version__ = "0.10.2" _spec_ = "0.5.0" load = decoder.load diff --git a/src/pip/_vendor/toml/common.py b/src/pip/_vendor/toml/common.py deleted file mode 100644 index a5d673dac5f..00000000000 --- a/src/pip/_vendor/toml/common.py +++ /dev/null @@ -1,6 +0,0 @@ -# content after the \ -escapes = ['0', 'b', 'f', 'n', 'r', 't', '"'] -# What it should be replaced by -escapedchars = ['\0', '\b', '\f', '\n', '\r', '\t', '\"'] -# Used for substitution -escape_to_escapedchars = dict(zip(_escapes, _escapedchars)) diff --git a/src/pip/_vendor/toml/decoder.py b/src/pip/_vendor/toml/decoder.py index e4887770c3b..e071100de0f 100644 --- a/src/pip/_vendor/toml/decoder.py +++ b/src/pip/_vendor/toml/decoder.py @@ -440,7 +440,8 @@ def loads(s, _dict=dict, decoder=None): groups[i][0] == "'"): groupstr = groups[i] j = i + 1 - while not groupstr[0] == groupstr[-1]: + while ((not groupstr[0] == groupstr[-1]) or + len(groupstr) == 1): j += 1 if j > len(groups) + 2: raise TomlDecodeError("Invalid group name '" + @@ -811,8 +812,12 @@ def load_value(self, v, strictly_valid=True): raise ValueError("Empty value is invalid") if v == 'true': return (True, "bool") + elif v.lower() == 'true': + raise ValueError("Only all lowercase booleans allowed") elif v == 'false': return (False, "bool") + elif v.lower() == 'false': + raise ValueError("Only all lowercase booleans allowed") elif v[0] == '"' or v[0] == "'": quotechar = v[0] testv = v[1:].split(quotechar) diff --git a/src/pip/_vendor/toml/encoder.py b/src/pip/_vendor/toml/encoder.py index a8b03c7bea8..7fb94da98ac 100644 --- a/src/pip/_vendor/toml/encoder.py +++ b/src/pip/_vendor/toml/encoder.py @@ -61,7 +61,7 @@ def dumps(o, encoder=None): retval += addtoretval outer_objs = [id(o)] while sections: - section_ids = [id(section) for section in sections] + section_ids = [id(section) for section in sections.values()] for outer_obj in outer_objs: if outer_obj in section_ids: raise ValueError("Circular reference detected") diff --git a/src/pip/_vendor/toml/tz.py b/src/pip/_vendor/toml/tz.py index 93c3c8ad262..bf20593a264 100644 --- a/src/pip/_vendor/toml/tz.py +++ b/src/pip/_vendor/toml/tz.py @@ -11,6 +11,9 @@ def __init__(self, toml_offset): self._hours = int(self._raw_offset[1:3]) self._minutes = int(self._raw_offset[4:6]) + def __deepcopy__(self, memo): + return self.__class__(self._raw_offset) + def tzname(self, dt): return "UTC" + self._raw_offset diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 468107bb648..21614a5e61c 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -20,5 +20,5 @@ resolvelib==0.5.2 retrying==1.3.3 setuptools==44.0.0 six==1.15.0 -toml==0.10.1 +toml==0.10.2 webencodings==0.5.1 From 3697f45ee6ab946a6f73cb7857246051cbf80ef8 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Thu, 19 Nov 2020 19:06:11 +0000 Subject: [PATCH 2724/3170] Fix the certifi version in vendor.txt Signed-off-by: Pradyun Gedam <pradyunsg@users.noreply.github.com> --- src/pip/_vendor/vendor.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 21614a5e61c..cc8157f1697 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -12,7 +12,7 @@ pep517==0.9.1 progress==1.5 pyparsing==2.4.7 requests==2.25.0 - certifi==2020.11.8 + certifi==2020.11.08 chardet==3.0.4 idna==2.10 urllib3==1.26.2 From 9b3cd280fdafcfa4a3d00a0a26594566609de535 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 4 Nov 2020 00:05:26 +0800 Subject: [PATCH 2725/3170] Add failing test --- tests/functional/test_new_resolver.py | 43 +++++++++++++++++++++++++++ 1 file changed, 43 insertions(+) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 374d37aeea3..5e36ec4986c 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -1132,3 +1132,46 @@ def test_new_resolver_check_wheel_version_normalized( "simple" ) assert_installed(script, simple="0.1.0+local.1") + + +def test_new_resolver_contraint_on_dep_with_extra(script): + create_basic_wheel_for_package( + script, + name="simple", + version="1", + depends=["dep[x]"], + ) + create_basic_wheel_for_package( + script, + name="dep", + version="1", + extras={"x": ["depx==1"]}, + ) + create_basic_wheel_for_package( + script, + name="dep", + version="2", + extras={"x": ["depx==2"]}, + ) + create_basic_wheel_for_package( + script, + name="depx", + version="1", + ) + create_basic_wheel_for_package( + script, + name="depx", + version="2", + ) + + constraints_txt = script.scratch_path / "constraints.txt" + constraints_txt.write_text("dep==1") + + script.pip( + "install", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "--constraint", constraints_txt, + "simple", + ) + assert_installed(script, simple="1", dep="1", depx="1") From d589795834bd36d057f51b2b65643f8c44726e1f Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 4 Nov 2020 00:29:19 +0800 Subject: [PATCH 2726/3170] Allow constraining an explicit requirement --- src/pip/_internal/resolution/resolvelib/base.py | 7 +++++++ src/pip/_internal/resolution/resolvelib/factory.py | 10 ++-------- 2 files changed, 9 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/base.py b/src/pip/_internal/resolution/resolvelib/base.py index 7c09cd70b8d..e2edbe9f42c 100644 --- a/src/pip/_internal/resolution/resolvelib/base.py +++ b/src/pip/_internal/resolution/resolvelib/base.py @@ -58,6 +58,13 @@ def __and__(self, other): hashes = self.hashes & other.hashes(trust_internet=False) return Constraint(specifier, hashes) + def is_satisfied_by(self, candidate): + # type: (Candidate) -> bool + # We can safely always allow prereleases here since PackageFinder + # already implements the prerelease logic, and would have filtered out + # prerelease candidates if the user does not expect them. + return self.specifier.contains(candidate.version, prereleases=True) + class Requirement(object): @property diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index c65cb7f76f8..f4177d981f6 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -235,16 +235,10 @@ def find_candidates( prefers_installed, ) - if constraint: - name = explicit_candidates.pop().name - raise InstallationError( - "Could not satisfy constraints for {!r}: installation from " - "path or url cannot be constrained to a version".format(name) - ) - return ( c for c in explicit_candidates - if all(req.is_satisfied_by(c) for req in requirements) + if constraint.is_satisfied_by(c) + and all(req.is_satisfied_by(c) for req in requirements) ) def make_requirement_from_install_req(self, ireq, requested_extras): From 0f6750c98cd5391c00517f5c88cbc1c5f322dfc6 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 4 Nov 2020 01:30:12 +0800 Subject: [PATCH 2727/3170] Modify old tests to accomodate restriction removal --- tests/functional/test_install_reqs.py | 24 ++++++++++++++++++++---- 1 file changed, 20 insertions(+), 4 deletions(-) diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index 575adbe157d..c5985243b60 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -341,7 +341,11 @@ def test_constraints_only_causes_error(script, data): assert 'installed requiresupper' not in result.stdout -def test_constraints_local_editable_install_causes_error(script, data): +def test_constraints_local_editable_install_causes_error( + script, + data, + resolver_variant, +): script.scratch_path.joinpath("constraints.txt").write_text( "singlemodule==0.0.0" ) @@ -350,7 +354,11 @@ def test_constraints_local_editable_install_causes_error(script, data): 'install', '--no-index', '-f', data.find_links, '-c', script.scratch_path / 'constraints.txt', '-e', to_install, expect_error=True) - assert 'Could not satisfy constraints for' in result.stderr + if resolver_variant == "legacy-resolver": + assert 'Could not satisfy constraints' in result.stderr, str(result) + else: + # Because singlemodule only has 0.0.1 available. + assert 'No matching distribution found' in result.stderr, str(result) @pytest.mark.network @@ -362,7 +370,11 @@ def test_constraints_local_editable_install_pep518(script, data): 'install', '--no-index', '-f', data.find_links, '-e', to_install) -def test_constraints_local_install_causes_error(script, data): +def test_constraints_local_install_causes_error( + script, + data, + resolver_variant, +): script.scratch_path.joinpath("constraints.txt").write_text( "singlemodule==0.0.0" ) @@ -371,7 +383,11 @@ def test_constraints_local_install_causes_error(script, data): 'install', '--no-index', '-f', data.find_links, '-c', script.scratch_path / 'constraints.txt', to_install, expect_error=True) - assert 'Could not satisfy constraints for' in result.stderr + if resolver_variant == "legacy-resolver": + assert 'Could not satisfy constraints' in result.stderr, str(result) + else: + # Because singlemodule only has 0.0.1 available. + assert 'No matching distribution found' in result.stderr, str(result) def test_constraints_constrain_to_local_editable( From 9efafb186fd907b308be5e06223213476a6b7dce Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sat, 21 Nov 2020 21:53:37 +0800 Subject: [PATCH 2728/3170] Implement __str__ for debuggability --- .../resolution/resolvelib/candidates.py | 17 +++++++++++++++++ .../resolution/resolvelib/requirements.py | 10 +++++++++- 2 files changed, 26 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 65585fd36a6..1fc2ff479a9 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -143,6 +143,10 @@ def __init__( self._version = version self._dist = None # type: Optional[Distribution] + def __str__(self): + # type: () -> str + return "{} {}".format(self.name, self.version) + def __repr__(self): # type: () -> str return "{class_name}({link!r})".format( @@ -359,6 +363,10 @@ def __init__( skip_reason = "already satisfied" factory.preparer.prepare_installed_requirement(self._ireq, skip_reason) + def __str__(self): + # type: () -> str + return str(self.dist) + def __repr__(self): # type: () -> str return "{class_name}({distribution!r})".format( @@ -445,6 +453,11 @@ def __init__( self.base = base self.extras = extras + def __str__(self): + # type: () -> str + name, rest = str(self.base).split(" ", 1) + return "{}[{}] {}".format(name, ",".join(self.extras), rest) + def __repr__(self): # type: () -> str return "{class_name}(base={base!r}, extras={extras!r})".format( @@ -554,6 +567,10 @@ def __init__(self, py_version_info): # only one RequiresPythonCandidate in a resolution, i.e. the host Python. # The built-in object.__eq__() and object.__ne__() do exactly what we want. + def __str__(self): + # type: () -> str + return "Python {}".format(self._version) + @property def name(self): # type: () -> str diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py index bc1061f4303..25cddceaf62 100644 --- a/src/pip/_internal/resolution/resolvelib/requirements.py +++ b/src/pip/_internal/resolution/resolvelib/requirements.py @@ -17,6 +17,10 @@ def __init__(self, candidate): # type: (Candidate) -> None self.candidate = candidate + def __str__(self): + # type: () -> str + return str(self.candidate) + def __repr__(self): # type: () -> str return "{class_name}({candidate!r})".format( @@ -106,6 +110,10 @@ def __init__(self, specifier, match): self.specifier = specifier self._candidate = match + def __str__(self): + # type: () -> str + return "Python {}".format(self.specifier) + def __repr__(self): # type: () -> str return "{class_name}({specifier!r})".format( @@ -120,7 +128,7 @@ def name(self): def format_for_error(self): # type: () -> str - return "Python " + str(self.specifier) + return str(self) def get_candidate_lookup(self): # type: () -> CandidateLookup From 8662248774fae518b35f1ec5fdc437c0ae68b4bc Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Sat, 21 Nov 2020 07:20:19 -0800 Subject: [PATCH 2729/3170] Always close stderr after subprocess completion in call_subprocess() When running Python with warnings enabled, fixes warnings of the form: .../site-packages/pip/_internal/vcs/versioncontrol.py:773: ResourceWarning: unclosed file <_io.BufferedReader name=12> return call_subprocess(cmd, cwd, --- news/9156.bugfix.rst | 1 + src/pip/_internal/vcs/versioncontrol.py | 2 ++ 2 files changed, 3 insertions(+) create mode 100644 news/9156.bugfix.rst diff --git a/news/9156.bugfix.rst b/news/9156.bugfix.rst new file mode 100644 index 00000000000..9b433fae2fc --- /dev/null +++ b/news/9156.bugfix.rst @@ -0,0 +1 @@ +Fix ResourceWarning in VCS subprocesses diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index 219f7967319..6724dcc697d 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -150,6 +150,8 @@ def call_subprocess( finally: if proc.stdout: proc.stdout.close() + if proc.stderr: + proc.stderr.close() proc_had_error = ( proc.returncode and proc.returncode not in extra_ok_returncodes From a24d198c15d03e8e30b1efcd3e32d303238355f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Wed, 11 Nov 2020 23:56:34 +0100 Subject: [PATCH 2730/3170] Do not download editables while preparing requirements Downloading is done at the end of the download command just like any other requirement. This is necessary to avoid archiving editable requirements to a zip file when running pip wheel. --- news/9122.bugfix.rst | 2 ++ src/pip/_internal/commands/download.py | 2 +- src/pip/_internal/operations/prepare.py | 3 +-- tests/functional/test_download.py | 4 ++-- 4 files changed, 6 insertions(+), 5 deletions(-) create mode 100644 news/9122.bugfix.rst diff --git a/news/9122.bugfix.rst b/news/9122.bugfix.rst new file mode 100644 index 00000000000..da2ae5e5f91 --- /dev/null +++ b/news/9122.bugfix.rst @@ -0,0 +1,2 @@ +Fix a regression that made ``pip wheel`` generate zip files of editable +requirements in the wheel directory. diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index 9535ef3cbe4..a2d3bf7d9b6 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -132,7 +132,7 @@ def run(self, options, args): downloaded = [] # type: List[str] for req in requirement_set.requirements.values(): - if not req.editable and req.satisfied_by is None: + if req.satisfied_by is None: assert req.name is not None preparer.save_linked_requirement(req) downloaded.append(req.name) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index de017504abe..13b2c0beee1 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -530,7 +530,7 @@ def save_linked_requirement(self, req): assert self.download_dir is not None assert req.link is not None link = req.link - if link.is_vcs: + if link.is_vcs or (link.is_existing_dir() and req.editable): # Make a .zip of the source_dir we already created. req.archive(self.download_dir) return @@ -576,7 +576,6 @@ def prepare_editable_requirement( req, self.req_tracker, self.finder, self.build_isolation, ) - req.archive(self.download_dir) req.check_if_exists(self.use_user_site) return dist diff --git a/tests/functional/test_download.py b/tests/functional/test_download.py index 6ee02d81744..8a816b63b44 100644 --- a/tests/functional/test_download.py +++ b/tests/functional/test_download.py @@ -855,9 +855,9 @@ def test_download_editable(script, data, tmpdir): """ Test 'pip download' of editables in requirement file. """ - editable_path = os.path.join(data.src, 'simplewheel-1.0') + editable_path = str(data.src / 'simplewheel-1.0').replace(os.path.sep, "/") requirements_path = tmpdir / "requirements.txt" - requirements_path.write_text("-e " + str(editable_path.resolve()) + "\n") + requirements_path.write_text("-e " + editable_path + "\n") download_dir = tmpdir / "download_dir" script.pip( 'download', '--no-deps', '-r', str(requirements_path), '-d', str(download_dir) From c3670b36cbaea0a756bcb0b1f96e56faedacb3b2 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sat, 21 Nov 2020 22:13:18 +0800 Subject: [PATCH 2731/3170] 2020 resolver can constrain path/URL to versions --- tests/functional/test_new_resolver.py | 28 ++++++++++++++++++++++----- 1 file changed, 23 insertions(+), 5 deletions(-) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 5e36ec4986c..45e1a034700 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -712,22 +712,40 @@ def test_new_resolver_constraint_on_dependency(script): assert_installed(script, dep="2.0") -def test_new_resolver_constraint_on_path(script): +@pytest.mark.parametrize( + "constraint_version, expect_error, message", + [ + ("1.0", True, "ERROR: No matching distribution found for foo 2.0"), + ("2.0", False, "Successfully installed foo-2.0"), + ], +) +def test_new_resolver_constraint_on_path_empty( + script, + constraint_version, + expect_error, + message, +): + """A path requirement can be filtered by a constraint. + """ setup_py = script.scratch_path / "setup.py" text = "from setuptools import setup\nsetup(name='foo', version='2.0')" setup_py.write_text(text) + constraints_txt = script.scratch_path / "constraints.txt" - constraints_txt.write_text("foo==1.0") + constraints_txt.write_text("foo=={}".format(constraint_version)) + result = script.pip( "install", "--no-cache-dir", "--no-index", "-c", constraints_txt, str(script.scratch_path), - expect_error=True, + expect_error=expect_error, ) - msg = "installation from path or url cannot be constrained to a version" - assert msg in result.stderr, str(result) + if expect_error: + assert message in result.stderr, str(result) + else: + assert message in result.stdout, str(result) def test_new_resolver_constraint_only_marker_match(script): From ce46a5e36d0ed3499c38df3bedf4ac4eb1ed9bc2 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Thu, 19 Nov 2020 15:39:00 +0000 Subject: [PATCH 2732/3170] Re-install local candidates unconditionally Signed-off-by: Pradyun Gedam <pradyunsg@users.noreply.github.com> --- .../resolution/resolvelib/resolver.py | 8 +++- tests/functional/test_new_resolver.py | 40 +++++++++++++++++++ 2 files changed, 46 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index e2e164d12c1..6290d2fe31f 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -132,15 +132,19 @@ def resolve(self, root_reqs, check_supported_wheels): # Check if there is already an installation under the same name, # and set a flag for later stages to uninstall it, if needed. - # * There isn't, good -- no uninstalltion needed. + # + # * There is no existing installation. Nothing to uninstall. + # * The candidate is a local path/file. Always reinstall. # * The --force-reinstall flag is set. Always reinstall. # * The installation is different in version or editable-ness, so # we need to uninstall it to install the new distribution. # * The installed version is the same as the pending distribution. - # Skip this distrubiton altogether to save work. + # Skip this distribution altogether to save work. installed_dist = self.factory.get_dist_to_uninstall(candidate) if installed_dist is None: ireq.should_reinstall = False + elif candidate.source_link.is_file: + ireq.should_reinstall = True elif self.factory.force_reinstall: ireq.should_reinstall = True elif installed_dist.parsed_version != candidate.version: diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 45e1a034700..2c28ff20a29 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -1193,3 +1193,43 @@ def test_new_resolver_contraint_on_dep_with_extra(script): "simple", ) assert_installed(script, simple="1", dep="1", depx="1") + + +def test_new_resolver_does_reinstall_local_wheels(script): + archive_path = create_basic_wheel_for_package( + script, + "pkg", + "1.0", + ) + script.pip( + "install", "--no-cache-dir", "--no-index", + archive_path, + ) + assert_installed(script, pkg="1.0") + + result = script.pip( + "install", "--no-cache-dir", "--no-index", + archive_path, + ) + assert "Installing collected packages: pkg" in result.stdout, str(result) + assert_installed(script, pkg="1.0") + + +def test_new_resolver_does_reinstall_local_paths(script): + pkg = create_test_package_with_setup( + script, + name="pkg", + version="1.0" + ) + script.pip( + "install", "--no-cache-dir", "--no-index", + pkg, + ) + assert_installed(script, pkg="1.0") + + result = script.pip( + "install", "--no-cache-dir", "--no-index", + pkg, + ) + assert "Installing collected packages: pkg" in result.stdout, str(result) + assert_installed(script, pkg="1.0") From bb7fce7209fd678f593faadf34270a6c09e60a5f Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Thu, 19 Nov 2020 15:46:37 +0000 Subject: [PATCH 2733/3170] Ensure we're not mishandling local indexes Signed-off-by: Pradyun Gedam <pradyunsg@users.noreply.github.com> --- tests/functional/test_new_resolver.py | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 2c28ff20a29..211f0c58b70 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -1233,3 +1233,29 @@ def test_new_resolver_does_reinstall_local_paths(script): ) assert "Installing collected packages: pkg" in result.stdout, str(result) assert_installed(script, pkg="1.0") + + +def test_new_resolver_does_not_reinstall_when_from_a_local_index(script): + create_basic_wheel_for_package( + script, + "simple", + "0.1.0", + ) + script.pip( + "install", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "simple" + ) + assert_installed(script, simple="0.1.0") + + result = script.pip( + "install", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "simple" + ) + # Should not reinstall! + assert "Installing collected packages: simple" not in result.stdout, str(result) + assert "Requirement already satisfied: simple" in result.stdout, str(result) + assert_installed(script, simple="0.1.0") From d6e3643fd981890205cd941c510da3c987687666 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Fri, 20 Nov 2020 13:47:49 +0000 Subject: [PATCH 2734/3170] Print a message and don't reinstall wheels Also, adds a test for source distributions being reinstalled. Signed-off-by: Pradyun Gedam <pradyunsg@users.noreply.github.com> --- .../resolution/resolvelib/resolver.py | 18 ++++-- tests/functional/test_new_resolver.py | 55 ++++++------------- 2 files changed, 29 insertions(+), 44 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 6290d2fe31f..f29b9692bd2 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -134,23 +134,31 @@ def resolve(self, root_reqs, check_supported_wheels): # and set a flag for later stages to uninstall it, if needed. # # * There is no existing installation. Nothing to uninstall. - # * The candidate is a local path/file. Always reinstall. # * The --force-reinstall flag is set. Always reinstall. # * The installation is different in version or editable-ness, so # we need to uninstall it to install the new distribution. - # * The installed version is the same as the pending distribution. - # Skip this distribution altogether to save work. + # * The installed version is different from the pending distribution. + # * The candidate is a local wheel. Do nothing. + # * The candidate is a local path. Always reinstall. installed_dist = self.factory.get_dist_to_uninstall(candidate) if installed_dist is None: ireq.should_reinstall = False - elif candidate.source_link.is_file: - ireq.should_reinstall = True elif self.factory.force_reinstall: ireq.should_reinstall = True elif installed_dist.parsed_version != candidate.version: ireq.should_reinstall = True elif dist_is_editable(installed_dist) != candidate.is_editable: ireq.should_reinstall = True + elif candidate.source_link.is_file: + if candidate.source_link.is_wheel: + logger.info( + "%s is already installed with the same version as the " + "provided wheel. Use --force-reinstall to force an " + "installation of the wheel.", + ireq.name, + ) + continue + ireq.should_reinstall = True else: continue diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 211f0c58b70..a26786c89aa 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -1152,51 +1152,28 @@ def test_new_resolver_check_wheel_version_normalized( assert_installed(script, simple="0.1.0+local.1") -def test_new_resolver_contraint_on_dep_with_extra(script): - create_basic_wheel_for_package( - script, - name="simple", - version="1", - depends=["dep[x]"], - ) - create_basic_wheel_for_package( - script, - name="dep", - version="1", - extras={"x": ["depx==1"]}, - ) - create_basic_wheel_for_package( - script, - name="dep", - version="2", - extras={"x": ["depx==2"]}, - ) - create_basic_wheel_for_package( +def test_new_resolver_does_not_reinstall_local_wheels(script): + archive_path = create_basic_wheel_for_package( script, - name="depx", - version="1", + "pkg", + "1.0", ) - create_basic_wheel_for_package( - script, - name="depx", - version="2", + script.pip( + "install", "--no-cache-dir", "--no-index", + archive_path, ) + assert_installed(script, pkg="1.0") - constraints_txt = script.scratch_path / "constraints.txt" - constraints_txt.write_text("dep==1") - - script.pip( - "install", - "--no-cache-dir", "--no-index", - "--find-links", script.scratch_path, - "--constraint", constraints_txt, - "simple", + result = script.pip( + "install", "--no-cache-dir", "--no-index", + archive_path, ) - assert_installed(script, simple="1", dep="1", depx="1") + assert "Installing collected packages: pkg" not in result.stdout, str(result) + assert_installed(script, pkg="1.0") -def test_new_resolver_does_reinstall_local_wheels(script): - archive_path = create_basic_wheel_for_package( +def test_new_resolver_does_reinstall_local_sdists(script): + archive_path = create_basic_sdist_for_package( script, "pkg", "1.0", @@ -1236,7 +1213,7 @@ def test_new_resolver_does_reinstall_local_paths(script): def test_new_resolver_does_not_reinstall_when_from_a_local_index(script): - create_basic_wheel_for_package( + create_basic_sdist_for_package( script, "simple", "0.1.0", From cd15a8514e7231c476076d4dbe5b5cc885e223ae Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Fri, 20 Nov 2020 12:45:18 +0000 Subject: [PATCH 2735/3170] Factor out is_archive_file Signed-off-by: Pradyun Gedam <pradyunsg@users.noreply.github.com> --- src/pip/_internal/index/collector.py | 15 ++------------- src/pip/_internal/req/constructors.py | 13 ++----------- src/pip/_internal/utils/filetypes.py | 10 ++++++++++ 3 files changed, 14 insertions(+), 24 deletions(-) diff --git a/src/pip/_internal/index/collector.py b/src/pip/_internal/index/collector.py index 7b9abbf69e2..b850b8cbed6 100644 --- a/src/pip/_internal/index/collector.py +++ b/src/pip/_internal/index/collector.py @@ -22,7 +22,7 @@ from pip._internal.models.search_scope import SearchScope from pip._internal.network.utils import raise_for_status from pip._internal.utils.compat import lru_cache -from pip._internal.utils.filetypes import ARCHIVE_EXTENSIONS +from pip._internal.utils.filetypes import is_archive_file from pip._internal.utils.misc import pairwise, redact_auth_from_url from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.urls import path_to_url, url_to_path @@ -65,17 +65,6 @@ def _match_vcs_scheme(url): return None -def _is_url_like_archive(url): - # type: (str) -> bool - """Return whether the URL looks like an archive. - """ - filename = Link(url).filename - for bad_ext in ARCHIVE_EXTENSIONS: - if filename.endswith(bad_ext): - return True - return False - - class _NotHTML(Exception): def __init__(self, content_type, request_desc): # type: (str, str) -> None @@ -130,7 +119,7 @@ def _get_html_response(url, session): 3. Check the Content-Type header to make sure we got HTML, and raise `_NotHTML` otherwise. """ - if _is_url_like_archive(url): + if is_archive_file(Link(url).filename): _ensure_html_response(url, session=session) logger.debug('Getting page %s', redact_auth_from_url(url)) diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index 97420af6c25..2245cb826ff 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -24,8 +24,8 @@ from pip._internal.pyproject import make_pyproject_path from pip._internal.req.req_install import InstallRequirement from pip._internal.utils.deprecation import deprecated -from pip._internal.utils.filetypes import ARCHIVE_EXTENSIONS -from pip._internal.utils.misc import is_installable_dir, splitext +from pip._internal.utils.filetypes import is_archive_file +from pip._internal.utils.misc import is_installable_dir from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.urls import path_to_url from pip._internal.vcs import is_url, vcs @@ -45,15 +45,6 @@ operators = Specifier._operators.keys() -def is_archive_file(name): - # type: (str) -> bool - """Return True if `name` is a considered as an archive file.""" - ext = splitext(name)[1].lower() - if ext in ARCHIVE_EXTENSIONS: - return True - return False - - def _strip_extras(path): # type: (str) -> Tuple[str, Optional[str]] m = re.match(r'^(.+)(\[[^\]]+\])$', path) diff --git a/src/pip/_internal/utils/filetypes.py b/src/pip/_internal/utils/filetypes.py index daa0ca771b7..201c6ebbed8 100644 --- a/src/pip/_internal/utils/filetypes.py +++ b/src/pip/_internal/utils/filetypes.py @@ -1,5 +1,6 @@ """Filetype information. """ +from pip._internal.utils.misc import splitext from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: @@ -14,3 +15,12 @@ ARCHIVE_EXTENSIONS = ( ZIP_EXTENSIONS + BZ2_EXTENSIONS + TAR_EXTENSIONS + XZ_EXTENSIONS ) + + +def is_archive_file(name): + # type: (str) -> bool + """Return True if `name` is a considered as an archive file.""" + ext = splitext(name)[1].lower() + if ext in ARCHIVE_EXTENSIONS: + return True + return False From 9add1c1f6196ea6b21bf7f6e49e63dd19e13e313 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Fri, 20 Nov 2020 14:36:03 +0000 Subject: [PATCH 2736/3170] Add deprecation warning when reinstalling sdists Signed-off-by: Pradyun Gedam <pradyunsg@users.noreply.github.com> --- .../_internal/resolution/resolvelib/resolver.py | 16 ++++++++++++++++ tests/functional/test_new_resolver.py | 2 ++ 2 files changed, 18 insertions(+) diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index f29b9692bd2..2ba14202536 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -16,6 +16,8 @@ PipDebuggingReporter, PipReporter, ) +from pip._internal.utils.deprecation import deprecated +from pip._internal.utils.filetypes import is_archive_file from pip._internal.utils.misc import dist_is_editable from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -139,6 +141,7 @@ def resolve(self, root_reqs, check_supported_wheels): # we need to uninstall it to install the new distribution. # * The installed version is different from the pending distribution. # * The candidate is a local wheel. Do nothing. + # * The candidate is a local sdist. Print a deprecation warning. # * The candidate is a local path. Always reinstall. installed_dist = self.factory.get_dist_to_uninstall(candidate) if installed_dist is None: @@ -158,6 +161,19 @@ def resolve(self, root_reqs, check_supported_wheels): ireq.name, ) continue + if is_archive_file(candidate.source_link.file_path): + reason = ( + "Source distribution is being reinstalled despite an " + "installed package having the same name and version as " + "the installed package." + ) + replacement = "use --force-reinstall" + deprecated( + reason=reason, + replacement=replacement, + gone_in="21.1", + issue=8711, + ) ireq.should_reinstall = True else: continue diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index a26786c89aa..9a4c98f8a22 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -1187,8 +1187,10 @@ def test_new_resolver_does_reinstall_local_sdists(script): result = script.pip( "install", "--no-cache-dir", "--no-index", archive_path, + expect_stderr=True, ) assert "Installing collected packages: pkg" in result.stdout, str(result) + assert "DEPRECATION" in result.stderr, str(result) assert_installed(script, pkg="1.0") From 5753884715cb518da966d3ce6cde9001e7429cd1 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Fri, 20 Nov 2020 18:34:03 +0000 Subject: [PATCH 2737/3170] Don't deprecate reinstalling from .zip files Signed-off-by: Pradyun Gedam <pradyunsg@users.noreply.github.com> --- src/pip/_internal/resolution/resolvelib/resolver.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 2ba14202536..69c96de1593 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -161,7 +161,12 @@ def resolve(self, root_reqs, check_supported_wheels): ireq.name, ) continue - if is_archive_file(candidate.source_link.file_path): + + looks_like_sdist = ( + is_archive_file(candidate.source_link.file_path) + and candidate.source_link.ext != ".zip" + ) + if looks_like_sdist: reason = ( "Source distribution is being reinstalled despite an " "installed package having the same name and version as " From 258242f1779cd83bc56d032a9586890bb4ada5e4 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Fri, 20 Nov 2020 18:35:38 +0000 Subject: [PATCH 2738/3170] Remove test for not reinstalling wheels, as requested Signed-off-by: Pradyun Gedam <pradyunsg@users.noreply.github.com> --- tests/functional/test_new_resolver.py | 20 -------------------- 1 file changed, 20 deletions(-) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 9a4c98f8a22..b730b3cbdf9 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -1152,26 +1152,6 @@ def test_new_resolver_check_wheel_version_normalized( assert_installed(script, simple="0.1.0+local.1") -def test_new_resolver_does_not_reinstall_local_wheels(script): - archive_path = create_basic_wheel_for_package( - script, - "pkg", - "1.0", - ) - script.pip( - "install", "--no-cache-dir", "--no-index", - archive_path, - ) - assert_installed(script, pkg="1.0") - - result = script.pip( - "install", "--no-cache-dir", "--no-index", - archive_path, - ) - assert "Installing collected packages: pkg" not in result.stdout, str(result) - assert_installed(script, pkg="1.0") - - def test_new_resolver_does_reinstall_local_sdists(script): archive_path = create_basic_sdist_for_package( script, From b2785d8ab97f4e95e663bc29826660bd2afe25e8 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Fri, 20 Nov 2020 18:39:04 +0000 Subject: [PATCH 2739/3170] Drop incorrect line from comment This was a copy-paste error that I didn't catch earlier. Signed-off-by: Pradyun Gedam <pradyunsg@users.noreply.github.com> --- src/pip/_internal/resolution/resolvelib/resolver.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 69c96de1593..9053c871d8e 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -139,7 +139,6 @@ def resolve(self, root_reqs, check_supported_wheels): # * The --force-reinstall flag is set. Always reinstall. # * The installation is different in version or editable-ness, so # we need to uninstall it to install the new distribution. - # * The installed version is different from the pending distribution. # * The candidate is a local wheel. Do nothing. # * The candidate is a local sdist. Print a deprecation warning. # * The candidate is a local path. Always reinstall. From 8f821866d66b49f1acb3dfdbb18d21576f024eab Mon Sep 17 00:00:00 2001 From: Noah Gorny <noah@gittabags.com> Date: Wed, 25 Nov 2020 21:16:41 +0200 Subject: [PATCH 2740/3170] Redact auth from URL in UpdatingDefaultsHelpFormatter --- news/9160.bugfix.rst | 1 + src/pip/_internal/cli/parser.py | 13 ++++++++++++- 2 files changed, 13 insertions(+), 1 deletion(-) create mode 100644 news/9160.bugfix.rst diff --git a/news/9160.bugfix.rst b/news/9160.bugfix.rst new file mode 100644 index 00000000000..fad6dc1f0d2 --- /dev/null +++ b/news/9160.bugfix.rst @@ -0,0 +1 @@ +Redact auth from URL in help message. diff --git a/src/pip/_internal/cli/parser.py b/src/pip/_internal/cli/parser.py index b6b78318a7a..ea3b383e2f7 100644 --- a/src/pip/_internal/cli/parser.py +++ b/src/pip/_internal/cli/parser.py @@ -17,6 +17,7 @@ from pip._internal.cli.status_codes import UNKNOWN_ERROR from pip._internal.configuration import Configuration, ConfigurationError from pip._internal.utils.compat import get_terminal_size +from pip._internal.utils.misc import redact_auth_from_url logger = logging.getLogger(__name__) @@ -106,12 +107,22 @@ class UpdatingDefaultsHelpFormatter(PrettyHelpFormatter): This is updates the defaults before expanding them, allowing them to show up correctly in the help listing. + + Also redact auth from url type options """ def expand_default(self, option): + default_value = None if self.parser is not None: self.parser._update_defaults(self.parser.defaults) - return optparse.IndentedHelpFormatter.expand_default(self, option) + default_value = self.parser.defaults.get(option.dest) + help_text = optparse.IndentedHelpFormatter.expand_default(self, option) + + if default_value and option.metavar == 'URL': + help_text = help_text.replace( + default_value, redact_auth_from_url(default_value)) + + return help_text class CustomOptionParser(optparse.OptionParser): From 4f8dfcf29d2527e3087c401b405b6c59b401cbad Mon Sep 17 00:00:00 2001 From: Noah Gorny <noah@gittabags.com> Date: Thu, 26 Nov 2020 15:15:21 +0200 Subject: [PATCH 2741/3170] tests: help: Test that auth is redacted from url in help menu --- tests/functional/test_help.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/functional/test_help.py b/tests/functional/test_help.py index 00a395006b7..9c2508abb51 100644 --- a/tests/functional/test_help.py +++ b/tests/functional/test_help.py @@ -64,6 +64,16 @@ def test_help_command_should_exit_status_error_when_cmd_does_not_exist(script): assert result.returncode == ERROR +def test_help_command_redact_auth_from_url(script): + """ + Test `help` on various subcommands redact auth from url + """ + script.environ['PIP_INDEX_URL'] = 'https://user:secret@example.com' + result = script.pip('install', '--help') + assert result.returncode == SUCCESS + assert 'secret' not in result.stdout + + def test_help_commands_equally_functional(in_memory_pip): """ Test if `pip help` and 'pip --help' behave the same way. From 9f4c5409d08c66e846819049c950fdf3d9804ccb Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sat, 28 Nov 2020 13:54:39 +0000 Subject: [PATCH 2742/3170] :art: Breakup and move comment Signed-off-by: Pradyun Gedam <pradyunsg@users.noreply.github.com> --- .../_internal/resolution/resolvelib/resolver.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 9053c871d8e..6b752bd654c 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -134,25 +134,23 @@ def resolve(self, root_reqs, check_supported_wheels): # Check if there is already an installation under the same name, # and set a flag for later stages to uninstall it, if needed. - # - # * There is no existing installation. Nothing to uninstall. - # * The --force-reinstall flag is set. Always reinstall. - # * The installation is different in version or editable-ness, so - # we need to uninstall it to install the new distribution. - # * The candidate is a local wheel. Do nothing. - # * The candidate is a local sdist. Print a deprecation warning. - # * The candidate is a local path. Always reinstall. installed_dist = self.factory.get_dist_to_uninstall(candidate) if installed_dist is None: + # There is no existing installation -- nothing to uninstall. ireq.should_reinstall = False elif self.factory.force_reinstall: + # The --force-reinstall flag is set -- reinstall. ireq.should_reinstall = True elif installed_dist.parsed_version != candidate.version: + # The installation is different in version -- reinstall. ireq.should_reinstall = True elif dist_is_editable(installed_dist) != candidate.is_editable: + # The installation is different in editable-ness -- reinstall. ireq.should_reinstall = True elif candidate.source_link.is_file: + # The incoming distribution is under file:// if candidate.source_link.is_wheel: + # is a local wheel -- do nothing. logger.info( "%s is already installed with the same version as the " "provided wheel. Use --force-reinstall to force an " @@ -166,6 +164,7 @@ def resolve(self, root_reqs, check_supported_wheels): and candidate.source_link.ext != ".zip" ) if looks_like_sdist: + # is a local sdist -- show a deprecation warning! reason = ( "Source distribution is being reinstalled despite an " "installed package having the same name and version as " @@ -178,6 +177,8 @@ def resolve(self, root_reqs, check_supported_wheels): gone_in="21.1", issue=8711, ) + + # is a local sdist or path -- reinstall ireq.should_reinstall = True else: continue From 1133342f300f5dcc3ac404560e1577a16545f9e5 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sat, 28 Nov 2020 13:56:40 +0000 Subject: [PATCH 2743/3170] Always reinstall editables Signed-off-by: Pradyun Gedam <pradyunsg@users.noreply.github.com> --- src/pip/_internal/resolution/resolvelib/resolver.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 6b752bd654c..30b860f6c48 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -144,8 +144,9 @@ def resolve(self, root_reqs, check_supported_wheels): elif installed_dist.parsed_version != candidate.version: # The installation is different in version -- reinstall. ireq.should_reinstall = True - elif dist_is_editable(installed_dist) != candidate.is_editable: - # The installation is different in editable-ness -- reinstall. + elif candidate.is_editable or dist_is_editable(installed_dist): + # The incoming distribution is editable, or different in + # editable-ness to installation -- reinstall. ireq.should_reinstall = True elif candidate.source_link.is_file: # The incoming distribution is under file:// From 2f9067408eec1f47abf146f12715602644dd1bae Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sat, 28 Nov 2020 14:01:32 +0000 Subject: [PATCH 2744/3170] :newspaper: Signed-off-by: Pradyun Gedam <pradyunsg@users.noreply.github.com> --- news/9169.bugfix.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 news/9169.bugfix.rst diff --git a/news/9169.bugfix.rst b/news/9169.bugfix.rst new file mode 100644 index 00000000000..299ec273366 --- /dev/null +++ b/news/9169.bugfix.rst @@ -0,0 +1,2 @@ +New Resolver: editable installations are done, regardless of whether +the already-installed distribution is editable. From 1466e7c49ac90cf11f4990a5cb0793aa1a42a3a7 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sat, 28 Nov 2020 14:48:16 +0000 Subject: [PATCH 2745/3170] Don't upgrade setuptools when vendoring Signed-off-by: Pradyun Gedam <pradyunsg@users.noreply.github.com> --- noxfile.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/noxfile.py b/noxfile.py index c21abc2a386..29e3959e463 100644 --- a/noxfile.py +++ b/noxfile.py @@ -168,6 +168,9 @@ def pinned_requirements(path): vendor_txt = Path("src/pip/_vendor/vendor.txt") for name, old_version in pinned_requirements(vendor_txt): + if name == "setuptools": + continue + # update requirements.txt session.run("vendoring", "update", ".", name) From d2671af23df254cfcaa24dcec2fd6ea951553f52 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sat, 28 Nov 2020 14:48:58 +0000 Subject: [PATCH 2746/3170] Upgrade packaging to 20.7 --- news/packaging.vendor.rst | 1 + src/pip/_vendor/packaging/__about__.py | 27 --- src/pip/_vendor/packaging/__init__.py | 25 +-- src/pip/_vendor/packaging/requirements.py | 8 +- src/pip/_vendor/packaging/specifiers.py | 27 +-- src/pip/_vendor/packaging/tags.py | 219 ++++++++++++++++------ src/pip/_vendor/packaging/utils.py | 18 +- src/pip/_vendor/packaging/version.py | 41 +++- src/pip/_vendor/vendor.txt | 2 +- 9 files changed, 228 insertions(+), 140 deletions(-) create mode 100644 news/packaging.vendor.rst delete mode 100644 src/pip/_vendor/packaging/__about__.py diff --git a/news/packaging.vendor.rst b/news/packaging.vendor.rst new file mode 100644 index 00000000000..4e49a4639b0 --- /dev/null +++ b/news/packaging.vendor.rst @@ -0,0 +1 @@ +Upgrade packaging to 20.7 diff --git a/src/pip/_vendor/packaging/__about__.py b/src/pip/_vendor/packaging/__about__.py deleted file mode 100644 index 4d998578d7b..00000000000 --- a/src/pip/_vendor/packaging/__about__.py +++ /dev/null @@ -1,27 +0,0 @@ -# This file is dual licensed under the terms of the Apache License, Version -# 2.0, and the BSD License. See the LICENSE file in the root of this repository -# for complete details. -from __future__ import absolute_import, division, print_function - -__all__ = [ - "__title__", - "__summary__", - "__uri__", - "__version__", - "__author__", - "__email__", - "__license__", - "__copyright__", -] - -__title__ = "packaging" -__summary__ = "Core utilities for Python packages" -__uri__ = "https://github.com/pypa/packaging" - -__version__ = "20.4" - -__author__ = "Donald Stufft and individual contributors" -__email__ = "donald@stufft.io" - -__license__ = "BSD-2-Clause or Apache-2.0" -__copyright__ = "Copyright 2014-2019 %s" % __author__ diff --git a/src/pip/_vendor/packaging/__init__.py b/src/pip/_vendor/packaging/__init__.py index a0cf67df524..18fecb867a8 100644 --- a/src/pip/_vendor/packaging/__init__.py +++ b/src/pip/_vendor/packaging/__init__.py @@ -1,26 +1,5 @@ # This file is dual licensed under the terms of the Apache License, Version # 2.0, and the BSD License. See the LICENSE file in the root of this repository # for complete details. -from __future__ import absolute_import, division, print_function - -from .__about__ import ( - __author__, - __copyright__, - __email__, - __license__, - __summary__, - __title__, - __uri__, - __version__, -) - -__all__ = [ - "__title__", - "__summary__", - "__uri__", - "__version__", - "__author__", - "__email__", - "__license__", - "__copyright__", -] +"""Core utilities for Python packages""" +__version__ = "20.7" diff --git a/src/pip/_vendor/packaging/requirements.py b/src/pip/_vendor/packaging/requirements.py index 5e64101c43d..f9d1c65991a 100644 --- a/src/pip/_vendor/packaging/requirements.py +++ b/src/pip/_vendor/packaging/requirements.py @@ -5,16 +5,22 @@ import string import re +import sys from pip._vendor.pyparsing import stringStart, stringEnd, originalTextFor, ParseException from pip._vendor.pyparsing import ZeroOrMore, Word, Optional, Regex, Combine from pip._vendor.pyparsing import Literal as L # noqa -from pip._vendor.six.moves.urllib import parse as urlparse from ._typing import TYPE_CHECKING from .markers import MARKER_EXPR, Marker from .specifiers import LegacySpecifier, Specifier, SpecifierSet +if sys.version_info[0] >= 3: + from urllib import parse as urlparse # pragma: no cover +else: # pragma: no cover + import urlparse + + if TYPE_CHECKING: # pragma: no cover from typing import List diff --git a/src/pip/_vendor/packaging/specifiers.py b/src/pip/_vendor/packaging/specifiers.py index fe09bb1dbb2..a42cbfef332 100644 --- a/src/pip/_vendor/packaging/specifiers.py +++ b/src/pip/_vendor/packaging/specifiers.py @@ -7,6 +7,7 @@ import functools import itertools import re +import warnings from ._compat import string_types, with_metaclass from ._typing import TYPE_CHECKING @@ -14,17 +15,7 @@ from .version import Version, LegacyVersion, parse if TYPE_CHECKING: # pragma: no cover - from typing import ( - List, - Dict, - Union, - Iterable, - Iterator, - Optional, - Callable, - Tuple, - FrozenSet, - ) + from typing import List, Dict, Union, Iterable, Iterator, Optional, Callable, Tuple ParsedVersion = Union[Version, LegacyVersion] UnparsedVersion = Union[Version, LegacyVersion, str] @@ -285,6 +276,16 @@ class LegacySpecifier(_IndividualSpecifier): ">": "greater_than", } + def __init__(self, spec="", prereleases=None): + # type: (str, Optional[bool]) -> None + super(LegacySpecifier, self).__init__(spec, prereleases) + + warnings.warn( + "Creating a LegacyVersion has been deprecated and will be " + "removed in the next major release", + DeprecationWarning, + ) + def _coerce_version(self, version): # type: (Union[ParsedVersion, str]) -> LegacyVersion if not isinstance(version, LegacyVersion): @@ -317,7 +318,7 @@ def _compare_greater_than(self, prospective, spec): def _require_version_compare( - fn # type: (Callable[[Specifier, ParsedVersion, str], bool]) + fn, # type: (Callable[[Specifier, ParsedVersion, str], bool]) ): # type: (...) -> Callable[[Specifier, ParsedVersion, str], bool] @functools.wraps(fn) @@ -750,7 +751,7 @@ def __len__(self): return len(self._specs) def __iter__(self): - # type: () -> Iterator[FrozenSet[_IndividualSpecifier]] + # type: () -> Iterator[_IndividualSpecifier] return iter(self._specs) @property diff --git a/src/pip/_vendor/packaging/tags.py b/src/pip/_vendor/packaging/tags.py index 9064910b8ba..842447d863b 100644 --- a/src/pip/_vendor/packaging/tags.py +++ b/src/pip/_vendor/packaging/tags.py @@ -13,6 +13,7 @@ EXTENSION_SUFFIXES = [x[0] for x in imp.get_suffixes()] del imp +import collections import logging import os import platform @@ -57,6 +58,24 @@ _32_BIT_INTERPRETER = sys.maxsize <= 2 ** 32 +_LEGACY_MANYLINUX_MAP = { + # CentOS 7 w/ glibc 2.17 (PEP 599) + (2, 17): "manylinux2014", + # CentOS 6 w/ glibc 2.12 (PEP 571) + (2, 12): "manylinux2010", + # CentOS 5 w/ glibc 2.5 (PEP 513) + (2, 5): "manylinux1", +} + +# If glibc ever changes its major version, we need to know what the last +# minor version was, so we can build the complete list of all versions. +# For now, guess what the highest minor version might be, assume it will +# be 50 for testing. Once this actually happens, update the dictionary +# with the actual value. +_LAST_GLIBC_MINOR = collections.defaultdict(lambda: 50) # type: Dict[int, int] +glibcVersion = collections.namedtuple("Version", ["major", "minor"]) + + class Tag(object): """ A representation of the tag triple for a wheel. @@ -65,13 +84,19 @@ class Tag(object): is also supported. """ - __slots__ = ["_interpreter", "_abi", "_platform"] + __slots__ = ["_interpreter", "_abi", "_platform", "_hash"] def __init__(self, interpreter, abi, platform): # type: (str, str, str) -> None self._interpreter = interpreter.lower() self._abi = abi.lower() self._platform = platform.lower() + # The __hash__ of every single element in a Set[Tag] will be evaluated each time + # that a set calls its `.disjoint()` method, which may be called hundreds of + # times when scanning a page of links for packages with tags matching that + # Set[Tag]. Pre-computing the value here produces significant speedups for + # downstream consumers. + self._hash = hash((self._interpreter, self._abi, self._platform)) @property def interpreter(self): @@ -101,7 +126,7 @@ def __eq__(self, other): def __hash__(self): # type: () -> int - return hash((self._interpreter, self._abi, self._platform)) + return self._hash def __str__(self): # type: () -> str @@ -382,7 +407,12 @@ def _mac_binary_formats(version, cpu_arch): return [] formats.extend(["fat32", "fat"]) - formats.append("universal") + if cpu_arch in {"arm64", "x86_64"}: + formats.append("universal2") + + if cpu_arch in {"x86_64", "i386", "ppc64", "ppc"}: + formats.append("universal") + return formats @@ -405,30 +435,73 @@ def mac_platforms(version=None, arch=None): arch = _mac_arch(cpu_arch) else: arch = arch - for minor_version in range(version[1], -1, -1): - compat_version = version[0], minor_version - binary_formats = _mac_binary_formats(compat_version, arch) - for binary_format in binary_formats: - yield "macosx_{major}_{minor}_{binary_format}".format( - major=compat_version[0], - minor=compat_version[1], - binary_format=binary_format, - ) + if (10, 0) <= version and version < (11, 0): + # Prior to Mac OS 11, each yearly release of Mac OS bumped the + # "minor" version number. The major version was always 10. + for minor_version in range(version[1], -1, -1): + compat_version = 10, minor_version + binary_formats = _mac_binary_formats(compat_version, arch) + for binary_format in binary_formats: + yield "macosx_{major}_{minor}_{binary_format}".format( + major=10, minor=minor_version, binary_format=binary_format + ) + + if version >= (11, 0): + # Starting with Mac OS 11, each yearly release bumps the major version + # number. The minor versions are now the midyear updates. + for major_version in range(version[0], 10, -1): + compat_version = major_version, 0 + binary_formats = _mac_binary_formats(compat_version, arch) + for binary_format in binary_formats: + yield "macosx_{major}_{minor}_{binary_format}".format( + major=major_version, minor=0, binary_format=binary_format + ) -# From PEP 513. -def _is_manylinux_compatible(name, glibc_version): - # type: (str, GlibcVersion) -> bool + if version >= (11, 0) and arch == "x86_64": + # Mac OS 11 on x86_64 is compatible with binaries from previous releases. + # Arm64 support was introduced in 11.0, so no Arm binaries from previous + # releases exist. + for minor_version in range(16, 3, -1): + compat_version = 10, minor_version + binary_formats = _mac_binary_formats(compat_version, arch) + for binary_format in binary_formats: + yield "macosx_{major}_{minor}_{binary_format}".format( + major=compat_version[0], + minor=compat_version[1], + binary_format=binary_format, + ) + + +# From PEP 513, PEP 600 +def _is_manylinux_compatible(name, arch, glibc_version): + # type: (str, str, GlibcVersion) -> bool + sys_glibc = _get_glibc_version() + if sys_glibc < glibc_version: + return False # Check for presence of _manylinux module. try: import _manylinux # noqa - - return bool(getattr(_manylinux, name + "_compatible")) - except (ImportError, AttributeError): - # Fall through to heuristic check below. + except ImportError: pass - - return _have_compatible_glibc(*glibc_version) + else: + if hasattr(_manylinux, "manylinux_compatible"): + result = _manylinux.manylinux_compatible( + glibc_version[0], glibc_version[1], arch + ) + if result is not None: + return bool(result) + else: + if glibc_version == (2, 5): + if hasattr(_manylinux, "manylinux1_compatible"): + return bool(_manylinux.manylinux1_compatible) + if glibc_version == (2, 12): + if hasattr(_manylinux, "manylinux2010_compatible"): + return bool(_manylinux.manylinux2010_compatible) + if glibc_version == (2, 17): + if hasattr(_manylinux, "manylinux2014_compatible"): + return bool(_manylinux.manylinux2014_compatible) + return True def _glibc_version_string(): @@ -474,8 +547,20 @@ def _glibc_version_string_ctypes(): # main program". This way we can let the linker do the work to figure out # which libc our process is actually using. # - # Note: typeshed is wrong here so we are ignoring this line. - process_namespace = ctypes.CDLL(None) # type: ignore + # We must also handle the special case where the executable is not a + # dynamically linked executable. This can occur when using musl libc, + # for example. In this situation, dlopen() will error, leading to an + # OSError. Interestingly, at least in the case of musl, there is no + # errno set on the OSError. The single string argument used to construct + # OSError comes from libc itself and is therefore not portable to + # hard code here. In any case, failure to call dlopen() means we + # can proceed, so we bail on our attempt. + try: + # Note: typeshed is wrong here so we are ignoring this line. + process_namespace = ctypes.CDLL(None) # type: ignore + except OSError: + return None + try: gnu_get_libc_version = process_namespace.gnu_get_libc_version except AttributeError: @@ -493,10 +578,9 @@ def _glibc_version_string_ctypes(): return version_str -# Separated out from have_compatible_glibc for easier unit testing. -def _check_glibc_version(version_str, required_major, minimum_minor): - # type: (str, int, int) -> bool - # Parse string and check against requested version. +def _parse_glibc_version(version_str): + # type: (str) -> Tuple[int, int] + # Parse glibc version. # # We use a regexp instead of str.split because we want to discard any # random junk that might come after the minor version -- this might happen @@ -509,19 +593,23 @@ def _check_glibc_version(version_str, required_major, minimum_minor): " got: %s" % version_str, RuntimeWarning, ) - return False - return ( - int(m.group("major")) == required_major - and int(m.group("minor")) >= minimum_minor - ) + return -1, -1 + return (int(m.group("major")), int(m.group("minor"))) -def _have_compatible_glibc(required_major, minimum_minor): - # type: (int, int) -> bool +_glibc_version = [] # type: List[Tuple[int, int]] + + +def _get_glibc_version(): + # type: () -> Tuple[int, int] + if _glibc_version: + return _glibc_version[0] version_str = _glibc_version_string() if version_str is None: - return False - return _check_glibc_version(version_str, required_major, minimum_minor) + _glibc_version.append((-1, -1)) + else: + _glibc_version.append(_parse_glibc_version(version_str)) + return _glibc_version[0] # Python does not provide platform information at sufficient granularity to @@ -639,7 +727,42 @@ def _have_compatible_manylinux_abi(arch): return _is_linux_armhf() if arch == "i686": return _is_linux_i686() - return True + return arch in {"x86_64", "aarch64", "ppc64", "ppc64le", "s390x"} + + +def _manylinux_tags(linux, arch): + # type: (str, str) -> Iterator[str] + # Oldest glibc to be supported regardless of architecture is (2, 17). + too_old_glibc2 = glibcVersion(2, 16) + if arch in {"x86_64", "i686"}: + # On x86/i686 also oldest glibc to be supported is (2, 5). + too_old_glibc2 = glibcVersion(2, 4) + current_glibc = glibcVersion(*_get_glibc_version()) + glibc_max_list = [current_glibc] + # We can assume compatibility across glibc major versions. + # https://sourceware.org/bugzilla/show_bug.cgi?id=24636 + # + # Build a list of maximum glibc versions so that we can + # output the canonical list of all glibc from current_glibc + # down to too_old_glibc2, including all intermediary versions. + for glibc_major in range(current_glibc.major - 1, 1, -1): + glibc_max_list.append(glibcVersion(glibc_major, _LAST_GLIBC_MINOR[glibc_major])) + for glibc_max in glibc_max_list: + if glibc_max.major == too_old_glibc2.major: + min_minor = too_old_glibc2.minor + else: + # For other glibc major versions oldest supported is (x, 0). + min_minor = -1 + for glibc_minor in range(glibc_max.minor, min_minor, -1): + glibc_version = (glibc_max.major, glibc_minor) + tag = "manylinux_{}_{}".format(*glibc_version) + if _is_manylinux_compatible(tag, arch, glibc_version): + yield linux.replace("linux", tag) + # Handle the legacy manylinux1, manylinux2010, manylinux2014 tags. + if glibc_version in _LEGACY_MANYLINUX_MAP: + legacy_tag = _LEGACY_MANYLINUX_MAP[glibc_version] + if _is_manylinux_compatible(legacy_tag, arch, glibc_version): + yield linux.replace("linux", legacy_tag) def _linux_platforms(is_32bit=_32_BIT_INTERPRETER): @@ -650,28 +773,10 @@ def _linux_platforms(is_32bit=_32_BIT_INTERPRETER): linux = "linux_i686" elif linux == "linux_aarch64": linux = "linux_armv7l" - manylinux_support = [] _, arch = linux.split("_", 1) if _have_compatible_manylinux_abi(arch): - if arch in {"x86_64", "i686", "aarch64", "armv7l", "ppc64", "ppc64le", "s390x"}: - manylinux_support.append( - ("manylinux2014", (2, 17)) - ) # CentOS 7 w/ glibc 2.17 (PEP 599) - if arch in {"x86_64", "i686"}: - manylinux_support.append( - ("manylinux2010", (2, 12)) - ) # CentOS 6 w/ glibc 2.12 (PEP 571) - manylinux_support.append( - ("manylinux1", (2, 5)) - ) # CentOS 5 w/ glibc 2.5 (PEP 513) - manylinux_support_iter = iter(manylinux_support) - for name, glibc_version in manylinux_support_iter: - if _is_manylinux_compatible(name, glibc_version): - yield linux.replace("linux", name) - break - # Support for a later manylinux implies support for an earlier version. - for name, _ in manylinux_support_iter: - yield linux.replace("linux", name) + for tag in _manylinux_tags(linux, arch): + yield tag yield linux diff --git a/src/pip/_vendor/packaging/utils.py b/src/pip/_vendor/packaging/utils.py index 19579c1a0fa..92c7b00b778 100644 --- a/src/pip/_vendor/packaging/utils.py +++ b/src/pip/_vendor/packaging/utils.py @@ -12,6 +12,8 @@ from typing import NewType, Union NormalizedName = NewType("NormalizedName", str) +else: + NormalizedName = str _canonicalize_regex = re.compile(r"[-_.]+") @@ -23,18 +25,18 @@ def canonicalize_name(name): return cast("NormalizedName", value) -def canonicalize_version(_version): - # type: (str) -> Union[Version, str] +def canonicalize_version(version): + # type: (Union[Version, str]) -> Union[Version, str] """ This is very similar to Version.__str__, but has one subtle difference with the way it handles the release segment. """ - - try: - version = Version(_version) - except InvalidVersion: - # Legacy versions cannot be normalized - return _version + if not isinstance(version, Version): + try: + version = Version(version) + except InvalidVersion: + # Legacy versions cannot be normalized + return version parts = [] diff --git a/src/pip/_vendor/packaging/version.py b/src/pip/_vendor/packaging/version.py index 00371e86a87..517d91f2485 100644 --- a/src/pip/_vendor/packaging/version.py +++ b/src/pip/_vendor/packaging/version.py @@ -6,6 +6,7 @@ import collections import itertools import re +import warnings from ._structures import Infinity, NegativeInfinity from ._typing import TYPE_CHECKING @@ -71,36 +72,50 @@ def __hash__(self): # type: () -> int return hash(self._key) + # Please keep the duplicated `isinstance` check + # in the six comparisons hereunder + # unless you find a way to avoid adding overhead function calls. def __lt__(self, other): # type: (_BaseVersion) -> bool - return self._compare(other, lambda s, o: s < o) + if not isinstance(other, _BaseVersion): + return NotImplemented + + return self._key < other._key def __le__(self, other): # type: (_BaseVersion) -> bool - return self._compare(other, lambda s, o: s <= o) + if not isinstance(other, _BaseVersion): + return NotImplemented + + return self._key <= other._key def __eq__(self, other): # type: (object) -> bool - return self._compare(other, lambda s, o: s == o) + if not isinstance(other, _BaseVersion): + return NotImplemented + + return self._key == other._key def __ge__(self, other): # type: (_BaseVersion) -> bool - return self._compare(other, lambda s, o: s >= o) + if not isinstance(other, _BaseVersion): + return NotImplemented + + return self._key >= other._key def __gt__(self, other): # type: (_BaseVersion) -> bool - return self._compare(other, lambda s, o: s > o) + if not isinstance(other, _BaseVersion): + return NotImplemented + + return self._key > other._key def __ne__(self, other): # type: (object) -> bool - return self._compare(other, lambda s, o: s != o) - - def _compare(self, other, method): - # type: (object, VersionComparisonMethod) -> Union[bool, NotImplemented] if not isinstance(other, _BaseVersion): return NotImplemented - return method(self._key, other._key) + return self._key != other._key class LegacyVersion(_BaseVersion): @@ -109,6 +124,12 @@ def __init__(self, version): self._version = str(version) self._key = _legacy_cmpkey(self._version) + warnings.warn( + "Creating a LegacyVersion has been deprecated and will be " + "removed in the next major release", + DeprecationWarning, + ) + def __str__(self): # type: () -> str return self._version diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index cc8157f1697..89eae55b690 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -7,7 +7,7 @@ distro==1.5.0 html5lib==1.1 ipaddress==1.0.23 # Only needed on 2.6 and 2.7 msgpack==1.0.0 -packaging==20.4 +packaging==20.7 pep517==0.9.1 progress==1.5 pyparsing==2.4.7 From b23591f4dacaebc08de37afec762843beb914ee6 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sat, 28 Nov 2020 14:49:47 +0000 Subject: [PATCH 2747/3170] Upgrade resolvelib to 0.5.3 --- news/resolvelib.vendor.rst | 2 +- src/pip/_vendor/resolvelib/__init__.py | 2 +- src/pip/_vendor/resolvelib/resolvers.py | 135 +++++++++++++++--------- src/pip/_vendor/resolvelib/structs.py | 18 ++-- src/pip/_vendor/vendor.txt | 2 +- 5 files changed, 98 insertions(+), 61 deletions(-) diff --git a/news/resolvelib.vendor.rst b/news/resolvelib.vendor.rst index 97e4f4a8e58..52d32af8bea 100644 --- a/news/resolvelib.vendor.rst +++ b/news/resolvelib.vendor.rst @@ -1 +1 @@ -Upgrade resolvelib to 0.5.2 +Upgrade resolvelib to 0.5.3 diff --git a/src/pip/_vendor/resolvelib/__init__.py b/src/pip/_vendor/resolvelib/__init__.py index 78ede4fd1a8..5a400f23ed1 100644 --- a/src/pip/_vendor/resolvelib/__init__.py +++ b/src/pip/_vendor/resolvelib/__init__.py @@ -11,7 +11,7 @@ "ResolutionTooDeep", ] -__version__ = "0.5.2" +__version__ = "0.5.3" from .providers import AbstractProvider, AbstractResolver diff --git a/src/pip/_vendor/resolvelib/resolvers.py b/src/pip/_vendor/resolvelib/resolvers.py index 976608b1775..acf0f8a6b43 100644 --- a/src/pip/_vendor/resolvelib/resolvers.py +++ b/src/pip/_vendor/resolvelib/resolvers.py @@ -99,16 +99,15 @@ def merged_with(self, provider, requirement, parent): raise RequirementsConflicted(criterion) return criterion - def excluded_of(self, candidate): - """Build a new instance from this, but excluding specified candidate. + def excluded_of(self, candidates): + """Build a new instance from this, but excluding specified candidates. Returns the new instance, or None if we still have no valid candidates. """ - cands = self.candidates.excluding(candidate) + cands = self.candidates.excluding(candidates) if not cands: return None - incompats = list(self.incompatibilities) - incompats.append(candidate) + incompats = self.incompatibilities + candidates return type(self)(cands, list(self.information), incompats) @@ -158,15 +157,11 @@ def _push_new_state(self): This new state will be used to hold resolution results of the next coming round. """ - try: - base = self._states[-1] - except IndexError: - state = State(mapping=collections.OrderedDict(), criteria={}) - else: - state = State( - mapping=base.mapping.copy(), - criteria=base.criteria.copy(), - ) + base = self._states[-1] + state = State( + mapping=base.mapping.copy(), + criteria=base.criteria.copy(), + ) self._states.append(state) def _merge_into_criterion(self, requirement, parent): @@ -239,44 +234,77 @@ def _attempt_to_pin_criterion(self, name, criterion): return causes def _backtrack(self): - # Drop the current state, it's known not to work. - del self._states[-1] - - # We need at least 2 states here: - # (a) One to backtrack to. - # (b) One to restore state (a) to its state prior to candidate-pinning, - # so we can pin another one instead. + """Perform backtracking. + + When we enter here, the stack is like this:: + + [ state Z ] + [ state Y ] + [ state X ] + .... earlier states are irrelevant. + + 1. No pins worked for Z, so it does not have a pin. + 2. We want to reset state Y to unpinned, and pin another candidate. + 3. State X holds what state Y was before the pin, but does not + have the incompatibility information gathered in state Y. + + Each iteration of the loop will: + + 1. Discard Z. + 2. Discard Y but remember its incompatibility information gathered + previously, and the failure we're dealing with right now. + 3. Push a new state Y' based on X, and apply the incompatibility + information from Y to Y'. + 4a. If this causes Y' to conflict, we need to backtrack again. Make Y' + the new Z and go back to step 2. + 4b. If the incompatibilites apply cleanly, end backtracking. + """ + while len(self._states) >= 3: + # Remove the state that triggered backtracking. + del self._states[-1] + + # Retrieve the last candidate pin and known incompatibilities. + broken_state = self._states.pop() + name, candidate = broken_state.mapping.popitem() + incompatibilities_from_broken = [ + (k, v.incompatibilities) + for k, v in broken_state.criteria.items() + ] - while len(self._states) >= 2: - # Retract the last candidate pin. - prev_state = self._states.pop() - try: - name, candidate = prev_state.mapping.popitem() - except KeyError: - continue self._r.backtracking(candidate) - # Create a new state to work on, with the newly known not-working - # candidate excluded. + # Create a new state from the last known-to-work one, and apply + # the previously gathered incompatibility information. self._push_new_state() + for k, incompatibilities in incompatibilities_from_broken: + try: + crit = self.state.criteria[k] + except KeyError: + continue + self.state.criteria[k] = crit.excluded_of(incompatibilities) - # Mark the retracted candidate as incompatible. - criterion = self.state.criteria[name].excluded_of(candidate) - if criterion is None: - # This state still does not work. Try the still previous state. - del self._states[-1] - continue - self.state.criteria[name] = criterion + # Mark the newly known incompatibility. + criterion = self.state.criteria[name].excluded_of([candidate]) - return True + # It works! Let's work on this new state. + if criterion: + self.state.criteria[name] = criterion + return True + + # State does not work after adding the new incompatibility + # information. Try the still previous state. + # No way to backtrack anymore. return False def resolve(self, requirements, max_rounds): if self._states: raise RuntimeError("already resolved") - self._push_new_state() + self._r.starting() + + # Initialize the root state. + self._states = [State(mapping=collections.OrderedDict(), criteria={})] for r in requirements: try: name, crit = self._merge_into_criterion(r, parent=None) @@ -284,14 +312,14 @@ def resolve(self, requirements, max_rounds): raise ResolutionImpossible(e.criterion.information) self.state.criteria[name] = crit - self._r.starting() + # The root state is saved as a sentinel so the first ever pin can have + # something to backtrack to if it fails. The root state is basically + # pinning the virtual "root" package in the graph. + self._push_new_state() for round_index in range(max_rounds): self._r.starting_round(round_index) - self._push_new_state() - curr = self.state - unsatisfied_criterion_items = [ item for item in self.state.criteria.items() @@ -300,8 +328,7 @@ def resolve(self, requirements, max_rounds): # All criteria are accounted for. Nothing more to pin, we are done! if not unsatisfied_criterion_items: - del self._states[-1] - self._r.ending(curr) + self._r.ending(self.state) return self.state # Choose the most preferred unpinned criterion to try. @@ -311,16 +338,20 @@ def resolve(self, requirements, max_rounds): ) failure_causes = self._attempt_to_pin_criterion(name, criterion) - # Backtrack if pinning fails. if failure_causes: - result = self._backtrack() - if not result: - causes = [ - i for crit in failure_causes for i in crit.information - ] + # Backtrack if pinning fails. The backtrack process puts us in + # an unpinned state, so we can work on it in the next round. + success = self._backtrack() + + # Dead ends everywhere. Give up. + if not success: + causes = [i for c in failure_causes for i in c.information] raise ResolutionImpossible(causes) + else: + # Pinning was successful. Push a new state to do another pin. + self._push_new_state() - self._r.ending_round(round_index, curr) + self._r.ending_round(round_index, self.state) raise ResolutionTooDeep(max_rounds) diff --git a/src/pip/_vendor/resolvelib/structs.py b/src/pip/_vendor/resolvelib/structs.py index 479aad5dc17..c4542f08a06 100644 --- a/src/pip/_vendor/resolvelib/structs.py +++ b/src/pip/_vendor/resolvelib/structs.py @@ -79,6 +79,9 @@ class _FactoryIterableView(object): def __init__(self, factory): self._factory = factory + def __repr__(self): + return "{}({})".format(type(self).__name__, list(self._factory())) + def __bool__(self): try: next(self._factory()) @@ -95,11 +98,11 @@ def for_preference(self): """Provide an candidate iterable for `get_preference()`""" return self._factory() - def excluding(self, candidate): - """Create a new `Candidates` instance excluding `candidate`.""" + def excluding(self, candidates): + """Create a new instance excluding specified candidates.""" def factory(): - return (c for c in self._factory() if c != candidate) + return (c for c in self._factory() if c not in candidates) return type(self)(factory) @@ -114,6 +117,9 @@ class _SequenceIterableView(object): def __init__(self, sequence): self._sequence = sequence + def __repr__(self): + return "{}({})".format(type(self).__name__, self._sequence) + def __bool__(self): return bool(self._sequence) @@ -129,9 +135,9 @@ def for_preference(self): """Provide an candidate iterable for `get_preference()`""" return self._sequence - def excluding(self, candidate): - """Create a new instance excluding `candidate`.""" - return type(self)([c for c in self._sequence if c != candidate]) + def excluding(self, candidates): + """Create a new instance excluding specified candidates.""" + return type(self)([c for c in self._sequence if c not in candidates]) def build_iter_view(matches): diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 89eae55b690..c7bc37c16c4 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -16,7 +16,7 @@ requests==2.25.0 chardet==3.0.4 idna==2.10 urllib3==1.26.2 -resolvelib==0.5.2 +resolvelib==0.5.3 retrying==1.3.3 setuptools==44.0.0 six==1.15.0 From 14149b03f44739b39b389717bb77e16aa061040f Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sat, 28 Nov 2020 14:59:25 +0000 Subject: [PATCH 2748/3170] Add news entries related to resolvelib Signed-off-by: Pradyun Gedam <pradyunsg@users.noreply.github.com> --- news/9011.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/9011.bugfix.rst diff --git a/news/9011.bugfix.rst b/news/9011.bugfix.rst new file mode 100644 index 00000000000..4e299dd9b6d --- /dev/null +++ b/news/9011.bugfix.rst @@ -0,0 +1 @@ +New Resolver: Rework backtracking and state management, to avoid getting stuck in an infinite loop. From 5f6cd8e39b2f3aaa047c488730cf0248020d3ec4 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sat, 28 Nov 2020 15:06:26 +0000 Subject: [PATCH 2749/3170] Add news entries related to packaging Signed-off-by: Pradyun Gedam <pradyunsg@users.noreply.github.com> --- news/9077.feature.rst | 1 + news/9138.feature.rst | 1 + 2 files changed, 2 insertions(+) create mode 100644 news/9077.feature.rst create mode 100644 news/9138.feature.rst diff --git a/news/9077.feature.rst b/news/9077.feature.rst new file mode 100644 index 00000000000..f77dacd0654 --- /dev/null +++ b/news/9077.feature.rst @@ -0,0 +1 @@ +Add support for :pep:`600`: Future 'manylinux' Platform Tags for Portable Linux Built Distributions. diff --git a/news/9138.feature.rst b/news/9138.feature.rst new file mode 100644 index 00000000000..98009cdd1a2 --- /dev/null +++ b/news/9138.feature.rst @@ -0,0 +1 @@ +Add support for MacOS Big Sur compatibility tags. From 5b71640447414200dc39e0437d00fc6592f9c9fd Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko <wk@sydorenko.org.ua> Date: Sat, 28 Nov 2020 23:52:52 +0100 Subject: [PATCH 2750/3170] Fix handling orphan documents in the feedback ext Before this change, the cut-off step was present but the new string wasn't assigned back and this resulted in `:orphan:` not only re- injected at the beginning of the document but also left over after the admonition. --- docs/docs_feedback_sphinxext.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/docs_feedback_sphinxext.py b/docs/docs_feedback_sphinxext.py index 86eb3d61a7c..15da4177766 100644 --- a/docs/docs_feedback_sphinxext.py +++ b/docs/docs_feedback_sphinxext.py @@ -111,7 +111,7 @@ def _modify_rst_document_source_on_read( orphan_mark = ':orphan:' is_orphan = orphan_mark in source[0] if is_orphan: - source[0].replace(orphan_mark, '') + source[0] = source[0].replace(orphan_mark, '') else: orphan_mark = '' From 765b30699a92ea32ee3d9210ed9b8fdc371d6ffb Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko <wk@sydorenko.org.ua> Date: Sun, 29 Nov 2020 00:04:04 +0100 Subject: [PATCH 2751/3170] Add a change note about the PR #9171 --- news/9171.doc.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 news/9171.doc.rst diff --git a/news/9171.doc.rst b/news/9171.doc.rst new file mode 100644 index 00000000000..ca02166a2e2 --- /dev/null +++ b/news/9171.doc.rst @@ -0,0 +1,3 @@ +Fixed moving the ``:orphan:`` to top of documents in the Sphinx +extension for collecting the UX feedback from docs (initially +introduced in PR #8848). From 8cfdb4518fffdb6b7ac177c1a29d95e346033fdf Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Date: Sun, 29 Nov 2020 11:38:41 +0000 Subject: [PATCH 2752/3170] Rename 9171.doc.rst to 9171.trivial.rst --- news/{9171.doc.rst => 9171.trivial.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename news/{9171.doc.rst => 9171.trivial.rst} (100%) diff --git a/news/9171.doc.rst b/news/9171.trivial.rst similarity index 100% rename from news/9171.doc.rst rename to news/9171.trivial.rst From 68713c0a26395e68240445fae62940091cb45519 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sun, 29 Nov 2020 17:33:59 +0000 Subject: [PATCH 2753/3170] Mark the failing test as xfail --- tests/unit/test_models_wheel.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/unit/test_models_wheel.py b/tests/unit/test_models_wheel.py index 05ee74262dd..a4f954a2c7d 100644 --- a/tests/unit/test_models_wheel.py +++ b/tests/unit/test_models_wheel.py @@ -93,6 +93,14 @@ def test_not_supported_osx_version(self): w = Wheel('simple-0.1-cp27-none-macosx_10_9_intel.whl') assert not w.supported(tags=tags) + @pytest.mark.xfail( + reason=( + "packaging.tags changed behaviour in this area, and @pradyunsg " + "decided as the release manager that this behaviour change is less " + "critical than Big Sur support for pip 20.3. See " + "https://github.com/pypa/packaging/pull/361 for further discussion." + ) + ) def test_supported_multiarch_darwin(self): """ Multi-arch wheels (intel) are supported on components (i386, x86_64) From 0579cacd42dd33c475f93cee0ed8f12c19fe6c22 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sun, 29 Nov 2020 20:20:44 +0000 Subject: [PATCH 2754/3170] Describe changelog quirk around beta release This helps guide users who are reading changelogs better understand what changes are a part of a "main" release. --- docs/html/news.rst | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/html/news.rst b/docs/html/news.rst index c2b835a0ff9..137cddf36ab 100644 --- a/docs/html/news.rst +++ b/docs/html/news.rst @@ -2,4 +2,9 @@ Changelog ========= +.. attention:: + + Major and minor releases of pip also include changes listed within + prior beta releases. + .. include:: ../../NEWS.rst From 9c8dfdd31383a4dd1528d6062eb13154108488d3 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sun, 29 Nov 2020 21:10:01 +0000 Subject: [PATCH 2755/3170] Expand on troubleshooting steps in resolver migration guide Co-authored-by: Sumana Harihareswara <sh@changeset.nyc> --- docs/html/user_guide.rst | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 9c30dca5ae0..e3aa5dc33d2 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -1747,10 +1747,21 @@ How to upgrade and migrate deliver part of your functionality, please test your integration with pip 20.3. -4. **Temporarily use the old resolver when necessary.** If you run - into resolution errors and need a workaround while you're fixing - their root causes, you can choose the old resolver behavior - using the flag ``--use-deprecated=legacy-resolver``. +4. **Troubleshoot and try these workarounds if necessary.** + + - If pip is taking longer to install packages, read + :ref:`Dependency resolution backtracking` for ways to reduce the + time pip spends backtracking due to dependency conflicts. + - If you don't want pip to actually resolve dependencies, use the + ``--no-deps`` option. This is useful when you have a set of package + versions that work together in reality, even though their metadata says + that they conflict. For guidance on a long-term fix, read + :ref:`Fixing conflicting dependencies`. + - If you run into resolution errors and need a workaround while you're + fixing their root causes, you can choose the old resolver behavior using + the flag ``--use-deprecated=legacy-resolver``. This will work until we + release pip 21.0 (see + :ref:`Deprecation timeline for 2020 resolver changes`). 5. **Please report bugs** through the `resolver testing survey`_. From d87cc2d216b33c3ef9c6c0e60d65d69d87239d26 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Mon, 30 Nov 2020 11:58:31 +0000 Subject: [PATCH 2756/3170] Update AUTHORS.txt --- AUTHORS.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AUTHORS.txt b/AUTHORS.txt index f6fe855d7af..7ab324ce423 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -79,6 +79,8 @@ Benjamin Peterson Benjamin VanEvery Benoit Pierre Berker Peksag +Bernard +Bernard Tyers Bernardo B. Marques Bernhard M. Wiedemann Bertil Hatt From c31c148a5b1d87591862c715adc7a7e5f3242fba Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Mon, 30 Nov 2020 11:58:32 +0000 Subject: [PATCH 2757/3170] Bump for release --- NEWS.rst | 49 ++++++++++++++++++++++++++++++++++++++ news/9011.bugfix.rst | 1 - news/9039.doc.rst | 1 - news/9077.feature.rst | 1 - news/9083.bugfix.rst | 3 --- news/9100.feature.rst | 1 - news/9101.bugfix.rst | 1 - news/9122.bugfix.rst | 2 -- news/9131.doc.rst | 1 - news/9133.removal.rst | 1 - news/9138.feature.rst | 1 - news/9156.bugfix.rst | 1 - news/9160.bugfix.rst | 1 - news/9169.bugfix.rst | 2 -- news/9171.trivial.rst | 3 --- news/certifi.vendor.rst | 1 - news/colorama.vendor.rst | 1 - news/packaging.vendor.rst | 1 - news/pep517.vendor.rst | 1 - news/requests.vendor.rst | 1 - news/resolvelib.vendor.rst | 1 - news/toml.vendor.rst | 1 - news/urllib3.vendor.rst | 1 - src/pip/__init__.py | 2 +- 24 files changed, 50 insertions(+), 29 deletions(-) delete mode 100644 news/9011.bugfix.rst delete mode 100644 news/9039.doc.rst delete mode 100644 news/9077.feature.rst delete mode 100644 news/9083.bugfix.rst delete mode 100644 news/9100.feature.rst delete mode 100644 news/9101.bugfix.rst delete mode 100644 news/9122.bugfix.rst delete mode 100644 news/9131.doc.rst delete mode 100644 news/9133.removal.rst delete mode 100644 news/9138.feature.rst delete mode 100644 news/9156.bugfix.rst delete mode 100644 news/9160.bugfix.rst delete mode 100644 news/9169.bugfix.rst delete mode 100644 news/9171.trivial.rst delete mode 100644 news/certifi.vendor.rst delete mode 100644 news/colorama.vendor.rst delete mode 100644 news/packaging.vendor.rst delete mode 100644 news/pep517.vendor.rst delete mode 100644 news/requests.vendor.rst delete mode 100644 news/resolvelib.vendor.rst delete mode 100644 news/toml.vendor.rst delete mode 100644 news/urllib3.vendor.rst diff --git a/NEWS.rst b/NEWS.rst index fffba7cf31f..feff1591909 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -9,6 +9,55 @@ .. towncrier release notes start +20.3 (2020-11-30) +================= + +Deprecations and Removals +------------------------- + +- Remove --unstable-feature flag as it has been deprecated. (`#9133 <https://github.com/pypa/pip/issues/9133>`_) + +Features +-------- + +- Add support for :pep:`600`: Future 'manylinux' Platform Tags for Portable Linux Built Distributions. (`#9077 <https://github.com/pypa/pip/issues/9077>`_) +- The new resolver now resolves packages in a deterministic order. (`#9100 <https://github.com/pypa/pip/issues/9100>`_) +- Add support for MacOS Big Sur compatibility tags. (`#9138 <https://github.com/pypa/pip/issues/9138>`_) + +Bug Fixes +--------- + +- New Resolver: Rework backtracking and state management, to avoid getting stuck in an infinite loop. (`#9011 <https://github.com/pypa/pip/issues/9011>`_) +- New resolver: Check version equality with ``packaging.version`` to avoid edge + cases if a wheel used different version normalization logic in its filename + and metadata. (`#9083 <https://github.com/pypa/pip/issues/9083>`_) +- New resolver: Show each requirement in the conflict error message only once to reduce cluttering. (`#9101 <https://github.com/pypa/pip/issues/9101>`_) +- Fix a regression that made ``pip wheel`` generate zip files of editable + requirements in the wheel directory. (`#9122 <https://github.com/pypa/pip/issues/9122>`_) +- Fix ResourceWarning in VCS subprocesses (`#9156 <https://github.com/pypa/pip/issues/9156>`_) +- Redact auth from URL in help message. (`#9160 <https://github.com/pypa/pip/issues/9160>`_) +- New Resolver: editable installations are done, regardless of whether + the already-installed distribution is editable. (`#9169 <https://github.com/pypa/pip/issues/9169>`_) + +Vendored Libraries +------------------ + +- Upgrade certifi to 2020.11.8 +- Upgrade colorama to 0.4.4 +- Upgrade packaging to 20.7 +- Upgrade pep517 to 0.9.1 +- Upgrade requests to 2.25.0 +- Upgrade resolvelib to 0.5.3 +- Upgrade toml to 0.10.2 +- Upgrade urllib3 to 1.26.2 + +Improved Documentation +---------------------- + +- Add a section to the User Guide to cover backtracking during dependency resolution. (`#9039 <https://github.com/pypa/pip/issues/9039>`_) +- Reorder and revise installation instructions to make them easier to follow. (`#9131 <https://github.com/pypa/pip/issues/9131>`_) + + 20.3b1 (2020-10-31) =================== diff --git a/news/9011.bugfix.rst b/news/9011.bugfix.rst deleted file mode 100644 index 4e299dd9b6d..00000000000 --- a/news/9011.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -New Resolver: Rework backtracking and state management, to avoid getting stuck in an infinite loop. diff --git a/news/9039.doc.rst b/news/9039.doc.rst deleted file mode 100644 index 37241250dac..00000000000 --- a/news/9039.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Add a section to the User Guide to cover backtracking during dependency resolution. diff --git a/news/9077.feature.rst b/news/9077.feature.rst deleted file mode 100644 index f77dacd0654..00000000000 --- a/news/9077.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Add support for :pep:`600`: Future 'manylinux' Platform Tags for Portable Linux Built Distributions. diff --git a/news/9083.bugfix.rst b/news/9083.bugfix.rst deleted file mode 100644 index 97fc552b6f3..00000000000 --- a/news/9083.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -New resolver: Check version equality with ``packaging.version`` to avoid edge -cases if a wheel used different version normalization logic in its filename -and metadata. diff --git a/news/9100.feature.rst b/news/9100.feature.rst deleted file mode 100644 index eb6c7283948..00000000000 --- a/news/9100.feature.rst +++ /dev/null @@ -1 +0,0 @@ -The new resolver now resolves packages in a deterministic order. diff --git a/news/9101.bugfix.rst b/news/9101.bugfix.rst deleted file mode 100644 index 441f2a93136..00000000000 --- a/news/9101.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -New resolver: Show each requirement in the conflict error message only once to reduce cluttering. diff --git a/news/9122.bugfix.rst b/news/9122.bugfix.rst deleted file mode 100644 index da2ae5e5f91..00000000000 --- a/news/9122.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fix a regression that made ``pip wheel`` generate zip files of editable -requirements in the wheel directory. diff --git a/news/9131.doc.rst b/news/9131.doc.rst deleted file mode 100644 index 18862aa554a..00000000000 --- a/news/9131.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Reorder and revise installation instructions to make them easier to follow. diff --git a/news/9133.removal.rst b/news/9133.removal.rst deleted file mode 100644 index 876e89b13eb..00000000000 --- a/news/9133.removal.rst +++ /dev/null @@ -1 +0,0 @@ -Remove --unstable-feature flag as it has been deprecated. diff --git a/news/9138.feature.rst b/news/9138.feature.rst deleted file mode 100644 index 98009cdd1a2..00000000000 --- a/news/9138.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Add support for MacOS Big Sur compatibility tags. diff --git a/news/9156.bugfix.rst b/news/9156.bugfix.rst deleted file mode 100644 index 9b433fae2fc..00000000000 --- a/news/9156.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix ResourceWarning in VCS subprocesses diff --git a/news/9160.bugfix.rst b/news/9160.bugfix.rst deleted file mode 100644 index fad6dc1f0d2..00000000000 --- a/news/9160.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Redact auth from URL in help message. diff --git a/news/9169.bugfix.rst b/news/9169.bugfix.rst deleted file mode 100644 index 299ec273366..00000000000 --- a/news/9169.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -New Resolver: editable installations are done, regardless of whether -the already-installed distribution is editable. diff --git a/news/9171.trivial.rst b/news/9171.trivial.rst deleted file mode 100644 index ca02166a2e2..00000000000 --- a/news/9171.trivial.rst +++ /dev/null @@ -1,3 +0,0 @@ -Fixed moving the ``:orphan:`` to top of documents in the Sphinx -extension for collecting the UX feedback from docs (initially -introduced in PR #8848). diff --git a/news/certifi.vendor.rst b/news/certifi.vendor.rst deleted file mode 100644 index b181bc30c48..00000000000 --- a/news/certifi.vendor.rst +++ /dev/null @@ -1 +0,0 @@ -Upgrade certifi to 2020.11.8 diff --git a/news/colorama.vendor.rst b/news/colorama.vendor.rst deleted file mode 100644 index 30c755eb04a..00000000000 --- a/news/colorama.vendor.rst +++ /dev/null @@ -1 +0,0 @@ -Upgrade colorama to 0.4.4 diff --git a/news/packaging.vendor.rst b/news/packaging.vendor.rst deleted file mode 100644 index 4e49a4639b0..00000000000 --- a/news/packaging.vendor.rst +++ /dev/null @@ -1 +0,0 @@ -Upgrade packaging to 20.7 diff --git a/news/pep517.vendor.rst b/news/pep517.vendor.rst deleted file mode 100644 index 945f1e74686..00000000000 --- a/news/pep517.vendor.rst +++ /dev/null @@ -1 +0,0 @@ -Upgrade pep517 to 0.9.1 diff --git a/news/requests.vendor.rst b/news/requests.vendor.rst deleted file mode 100644 index 515ca25a94a..00000000000 --- a/news/requests.vendor.rst +++ /dev/null @@ -1 +0,0 @@ -Upgrade requests to 2.25.0 diff --git a/news/resolvelib.vendor.rst b/news/resolvelib.vendor.rst deleted file mode 100644 index 52d32af8bea..00000000000 --- a/news/resolvelib.vendor.rst +++ /dev/null @@ -1 +0,0 @@ -Upgrade resolvelib to 0.5.3 diff --git a/news/toml.vendor.rst b/news/toml.vendor.rst deleted file mode 100644 index 566c104f80c..00000000000 --- a/news/toml.vendor.rst +++ /dev/null @@ -1 +0,0 @@ -Upgrade toml to 0.10.2 diff --git a/news/urllib3.vendor.rst b/news/urllib3.vendor.rst deleted file mode 100644 index 10e1e7b45f5..00000000000 --- a/news/urllib3.vendor.rst +++ /dev/null @@ -1 +0,0 @@ -Upgrade urllib3 to 1.26.2 diff --git a/src/pip/__init__.py b/src/pip/__init__.py index cd092971361..cc35d260833 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -4,7 +4,7 @@ from typing import List, Optional -__version__ = "20.3.dev1" +__version__ = "20.3" def main(args=None): From 4ba207020e216cdecbc306b93dcafd79f474b61b Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Mon, 30 Nov 2020 11:58:32 +0000 Subject: [PATCH 2758/3170] Bump for development --- src/pip/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/__init__.py b/src/pip/__init__.py index cc35d260833..ae0fe9a9f24 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -4,7 +4,7 @@ from typing import List, Optional -__version__ = "20.3" +__version__ = "21.0.dev0" def main(args=None): From 31a2e1a586d2ccfee1f30b5cebfa5acfc6197662 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Tue, 1 Dec 2020 23:12:51 +0100 Subject: [PATCH 2759/3170] Restore --build-dir --- news/9193.removal.rst | 2 ++ src/pip/_internal/cli/base_command.py | 14 ++++++++++++++ src/pip/_internal/cli/cmdoptions.py | 8 ++++++++ src/pip/_internal/commands/download.py | 1 + src/pip/_internal/commands/install.py | 2 ++ src/pip/_internal/commands/wheel.py | 1 + 6 files changed, 28 insertions(+) create mode 100644 news/9193.removal.rst diff --git a/news/9193.removal.rst b/news/9193.removal.rst new file mode 100644 index 00000000000..5957a55836f --- /dev/null +++ b/news/9193.removal.rst @@ -0,0 +1,2 @@ +The --build-dir option has been restored as a no-op, to soften the transition +for tools that still used it. diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index c1522d6391b..7f05efb85db 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -199,6 +199,20 @@ def _main(self, args): ) options.cache_dir = None + if getattr(options, "build_dir", None): + deprecated( + reason=( + "The -b/--build/--build-dir/--build-directory " + "option is deprecated and has no effect anymore." + ), + replacement=( + "use the TMPDIR/TEMP/TMP environment variable, " + "possibly combined with --no-clean" + ), + gone_in="21.1", + issue=8333, + ) + if '2020-resolver' in options.features_enabled and not PY2: logger.warning( "--use-feature=2020-resolver no longer has any effect, " diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index 07d612a6f54..3543ed48bb3 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -695,6 +695,14 @@ def _handle_no_cache_dir(option, opt, value, parser): help="Don't install package dependencies.", ) # type: Callable[..., Option] +build_dir = partial( + PipOption, + '-b', '--build', '--build-dir', '--build-directory', + dest='build_dir', + type='path', + metavar='dir', + help=SUPPRESS_HELP, +) # type: Callable[..., Option] ignore_requires_python = partial( Option, diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index a2d3bf7d9b6..7405870aefc 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -43,6 +43,7 @@ def add_options(self): # type: () -> None self.cmd_opts.add_option(cmdoptions.constraints()) self.cmd_opts.add_option(cmdoptions.requirements()) + self.cmd_opts.add_option(cmdoptions.build_dir()) self.cmd_opts.add_option(cmdoptions.no_deps()) self.cmd_opts.add_option(cmdoptions.global_options()) self.cmd_opts.add_option(cmdoptions.no_binary()) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 38f9f063dea..a4e10f260a2 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -129,6 +129,8 @@ def add_options(self): help="Installation prefix where lib, bin and other top-level " "folders are placed") + self.cmd_opts.add_option(cmdoptions.build_dir()) + self.cmd_opts.add_option(cmdoptions.src()) self.cmd_opts.add_option( diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index 2d654338d7a..39fd2bf8128 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -78,6 +78,7 @@ def add_options(self): self.cmd_opts.add_option(cmdoptions.src()) self.cmd_opts.add_option(cmdoptions.ignore_requires_python()) self.cmd_opts.add_option(cmdoptions.no_deps()) + self.cmd_opts.add_option(cmdoptions.build_dir()) self.cmd_opts.add_option(cmdoptions.progress_bar()) self.cmd_opts.add_option( From 8bc9c33b3661fa8f14d8f5b21c0c381953975a1a Mon Sep 17 00:00:00 2001 From: Noah Gorny <noah@gittabags.com> Date: Wed, 2 Dec 2020 22:53:43 +0200 Subject: [PATCH 2760/3170] tests: Add extra_index_url test case to help redact url --- tests/functional/test_help.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tests/functional/test_help.py b/tests/functional/test_help.py index 9c2508abb51..a660cdf520d 100644 --- a/tests/functional/test_help.py +++ b/tests/functional/test_help.py @@ -74,6 +74,17 @@ def test_help_command_redact_auth_from_url(script): assert 'secret' not in result.stdout +def test_help_command_redact_auth_from_url_with_extra_index_url(script): + """ + Test `help` on various subcommands redact auth from url with extra index url + """ + script.environ['PIP_INDEX_URL'] = 'https://user:secret@example.com' + script.environ['PIP_EXTRA_INDEX_URL'] = 'https://user:secret@example2.com' + result = script.pip('install', '--help') + assert result.returncode == SUCCESS + assert 'secret' not in result.stdout + + def test_help_commands_equally_functional(in_memory_pip): """ Test if `pip help` and 'pip --help' behave the same way. From fbba5be5ca91e1cd98f0996474824640946c6475 Mon Sep 17 00:00:00 2001 From: toxinu <toxinu@gmail.com> Date: Thu, 3 Dec 2020 17:44:29 +0900 Subject: [PATCH 2761/3170] Use nickname instead of real name --- .mailmap | 1 - AUTHORS.txt | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.mailmap b/.mailmap index 63d292348d5..29f9ec039b6 100644 --- a/.mailmap +++ b/.mailmap @@ -20,7 +20,6 @@ Dustin Ingram <di@di.codes> <di@users.noreply.gi Endoh Takanao <djmchl@gmail.com> Erik M. Bray <embray@stsci.edu> Gabriel de Perthuis <g2p.code@gmail.com> -Geoffrey Lehée <geoffrey@lehee.name> Hsiaoming Yang <lepture@me.com> Igor Kuzmitshov <kuzmiigo@gmail.com> <igor@qubit.com> Ilya Baryshev <baryshev@gmail.com> diff --git a/AUTHORS.txt b/AUTHORS.txt index 7ab324ce423..5b53db6c2b7 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -218,7 +218,6 @@ Gabriel Curio Gabriel de Perthuis Garry Polley gdanielson -Geoffrey Lehée Geoffrey Sneddon George Song Georgi Valkov @@ -551,6 +550,7 @@ Tony Zhaocheng Tan TonyBeswick toonarmycaptain Toshio Kuratomi +toxinu Travis Swicegood Tzu-ping Chung Valentin Haenel From f94a429e17b450ac2d3432f46492416ac2cf58ad Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Thu, 3 Dec 2020 09:10:45 +0000 Subject: [PATCH 2762/3170] Bump for release --- NEWS.rst | 10 ++++++++++ news/9193.removal.rst | 2 -- src/pip/__init__.py | 2 +- 3 files changed, 11 insertions(+), 3 deletions(-) delete mode 100644 news/9193.removal.rst diff --git a/NEWS.rst b/NEWS.rst index feff1591909..71071a4c084 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -9,6 +9,16 @@ .. towncrier release notes start +20.3.1 (2020-12-03) +=================== + +Deprecations and Removals +------------------------- + +- The --build-dir option has been restored as a no-op, to soften the transition + for tools that still used it. (`#9193 <https://github.com/pypa/pip/issues/9193>`_) + + 20.3 (2020-11-30) ================= diff --git a/news/9193.removal.rst b/news/9193.removal.rst deleted file mode 100644 index 5957a55836f..00000000000 --- a/news/9193.removal.rst +++ /dev/null @@ -1,2 +0,0 @@ -The --build-dir option has been restored as a no-op, to soften the transition -for tools that still used it. diff --git a/src/pip/__init__.py b/src/pip/__init__.py index ae0fe9a9f24..08679517f7d 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -4,7 +4,7 @@ from typing import List, Optional -__version__ = "21.0.dev0" +__version__ = "20.3.1" def main(args=None): From 3d8d3b304477c65fa7d56a35086e0586c25bce90 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Thu, 3 Dec 2020 09:10:46 +0000 Subject: [PATCH 2763/3170] Bump for development --- src/pip/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/__init__.py b/src/pip/__init__.py index 08679517f7d..ae0fe9a9f24 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -4,7 +4,7 @@ from typing import List, Optional -__version__ = "20.3.1" +__version__ = "21.0.dev0" def main(args=None): From 4ad924a66f87002195e6815a3697c3316afc02d1 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 3 Dec 2020 16:39:11 +0800 Subject: [PATCH 2764/3170] Resolve direct and pinned requirements first --- news/9185.feature.rst | 2 + .../resolution/resolvelib/provider.py | 46 ++++++++++++++++++- 2 files changed, 47 insertions(+), 1 deletion(-) create mode 100644 news/9185.feature.rst diff --git a/news/9185.feature.rst b/news/9185.feature.rst new file mode 100644 index 00000000000..a9d9ae7187c --- /dev/null +++ b/news/9185.feature.rst @@ -0,0 +1,2 @@ +New resolver: Resolve direct and pinned (``==`` or ``===``) requirements first +to improve resolver performance. diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index c0e6b60d90a..b1df3ca52b8 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -56,9 +56,53 @@ def get_preference( information # type: Sequence[Tuple[Requirement, Candidate]] ): # type: (...) -> Any + """Produce a sort key for given requirement based on preference. + + The lower the return value is, the more preferred this group of + arguments is. + + Currently pip considers the followings in order: + + * Prefer if any of the known requirements points to an explicit URL. + * If equal, prefer if any requirements contain `===` and `==`. + * If equal, prefer user-specified (non-transitive) requirements. + * If equal, order alphabetically for consistency (helps debuggability). + """ + + def _get_restrictive_rating(requirements): + # type: (Iterable[Requirement]) -> int + """Rate how restrictive a set of requirements are. + + ``Requirement.get_candidate_lookup()`` returns a 2-tuple for + lookup. The first element is ``Optional[Candidate]`` and the + second ``Optional[InstallRequirement]``. + + * If the requirement is an explicit one, the explicitly-required + candidate is returned as the first element. + * If the requirement is based on a PEP 508 specifier, the backing + ``InstallRequirement`` is returned as the second element. + + We use the first element to check whether there is an explicit + requirement, and the second for equality operator. + """ + lookups = (r.get_candidate_lookup() for r in requirements) + cands, ireqs = zip(*lookups) + if any(cand is not None for cand in cands): + return 0 + spec_sets = (ireq.specifier for ireq in ireqs if ireq) + operators = ( + specifier.operator + for spec_set in spec_sets + for specifier in spec_set + ) + if any(op in ("==", "===") for op in operators): + return 1 + return 2 + + restrictive = _get_restrictive_rating(req for req, _ in information) transitive = all(parent is not None for _, parent in information) key = next(iter(candidates)).name if candidates else "" - return (transitive, key) + return (restrictive, transitive, key) def find_matches(self, requirements): # type: (Sequence[Requirement]) -> Iterable[Candidate] From 82fe333c09c11b508389d9c925fec92f0c7e1ed0 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 3 Dec 2020 18:25:17 +0800 Subject: [PATCH 2765/3170] Also prefer requirements with non-empty specifiers --- src/pip/_internal/resolution/resolvelib/provider.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index b1df3ca52b8..c91f252f7b4 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -64,7 +64,9 @@ def get_preference( Currently pip considers the followings in order: * Prefer if any of the known requirements points to an explicit URL. - * If equal, prefer if any requirements contain `===` and `==`. + * If equal, prefer if any requirements contain ``===`` and ``==``. + * If equal, prefer if requirements include version constraints, e.g. + ``>=`` and ``<``. * If equal, prefer user-specified (non-transitive) requirements. * If equal, order alphabetically for consistency (helps debuggability). """ @@ -90,14 +92,17 @@ def _get_restrictive_rating(requirements): if any(cand is not None for cand in cands): return 0 spec_sets = (ireq.specifier for ireq in ireqs if ireq) - operators = ( + operators = [ specifier.operator for spec_set in spec_sets for specifier in spec_set - ) + ] if any(op in ("==", "===") for op in operators): return 1 - return 2 + if operators: + return 2 + # A "bare" requirement without any version requirements. + return 3 restrictive = _get_restrictive_rating(req for req, _ in information) transitive = all(parent is not None for _, parent in information) From 5cfd8a7c3ec791c26616005a688db226fd767549 Mon Sep 17 00:00:00 2001 From: Noah Gorny <noah@gittabags.com> Date: Wed, 2 Dec 2020 22:56:03 +0200 Subject: [PATCH 2766/3170] Handle case of list default values in UpdatingDefaultsHelpFormatter Happens because we pass a list from --extra-index-url --- news/9191.bugfix.rst | 2 ++ src/pip/_internal/cli/parser.py | 18 +++++++++++++----- 2 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 news/9191.bugfix.rst diff --git a/news/9191.bugfix.rst b/news/9191.bugfix.rst new file mode 100644 index 00000000000..436f2f2dd29 --- /dev/null +++ b/news/9191.bugfix.rst @@ -0,0 +1,2 @@ +Handle case of list default values in UpdatingDefaultsHelpFormatter +Happens because we pass a list from --extra-index-url diff --git a/src/pip/_internal/cli/parser.py b/src/pip/_internal/cli/parser.py index ea3b383e2f7..7170bfd3841 100644 --- a/src/pip/_internal/cli/parser.py +++ b/src/pip/_internal/cli/parser.py @@ -112,15 +112,23 @@ class UpdatingDefaultsHelpFormatter(PrettyHelpFormatter): """ def expand_default(self, option): - default_value = None + default_values = None if self.parser is not None: self.parser._update_defaults(self.parser.defaults) - default_value = self.parser.defaults.get(option.dest) + default_values = self.parser.defaults.get(option.dest) help_text = optparse.IndentedHelpFormatter.expand_default(self, option) - if default_value and option.metavar == 'URL': - help_text = help_text.replace( - default_value, redact_auth_from_url(default_value)) + if default_values and option.metavar == 'URL': + if isinstance(default_values, string_types): + default_values = [default_values] + + # If its not a list, we should abort and just return the help text + if not isinstance(default_values, list): + default_values = [] + + for val in default_values: + help_text = help_text.replace( + val, redact_auth_from_url(val)) return help_text From ffb3d1bc761d74583b5c59275bfda997afeebc03 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 4 Dec 2020 23:39:32 +0800 Subject: [PATCH 2767/3170] Correctly implement yanking logic Do not return yanked versions from an index, unless the version range can only be satisfied by yanked candidates. --- news/9203.bugfix.rst | 2 ++ src/pip/_internal/resolution/resolvelib/factory.py | 11 ++++++++++- 2 files changed, 12 insertions(+), 1 deletion(-) create mode 100644 news/9203.bugfix.rst diff --git a/news/9203.bugfix.rst b/news/9203.bugfix.rst new file mode 100644 index 00000000000..29b39d66c3e --- /dev/null +++ b/news/9203.bugfix.rst @@ -0,0 +1,2 @@ +New resolver: Correctly implement PEP 592. Do not return yanked versions from +an index, unless the version range can only be satisfied by yanked candidates. diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index f4177d981f6..c723d343bf9 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -193,8 +193,17 @@ def iter_index_candidates(): specifier=specifier, hashes=hashes, ) + icans = list(result.iter_applicable()) + + # PEP 592: Yanked releases must be ignored unless only yanked + # releases can satisfy the version range. So if this is false, + # all yanked icans need to be skipped. + all_yanked = all(ican.link.is_yanked for ican in icans) + # PackageFinder returns earlier versions first, so we reverse. - for ican in reversed(list(result.iter_applicable())): + for ican in reversed(icans): + if not all_yanked and ican.link.is_yanked: + continue yield self._make_candidate_from_link( link=ican.link, extras=extras, From e2fb3e12ab458f7347fb9149327ad28dabaac0d6 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 8 Dec 2020 01:03:57 +0800 Subject: [PATCH 2768/3170] Use the project name to look up constraints User-specified constraints only contain the project name without extras, but a constraint a project A would also apply to A's extra variants. This introduces a new project_name property on Requirement and Candidate classes for this lookup. --- news/9232.bugfix.rst | 2 ++ .../_internal/resolution/resolvelib/base.py | 32 +++++++++++++++++++ .../resolution/resolvelib/candidates.py | 28 +++++++++++++--- .../resolution/resolvelib/provider.py | 12 ++++++- .../resolution/resolvelib/requirements.py | 19 +++++++++-- 5 files changed, 86 insertions(+), 7 deletions(-) create mode 100644 news/9232.bugfix.rst diff --git a/news/9232.bugfix.rst b/news/9232.bugfix.rst new file mode 100644 index 00000000000..e881c53846c --- /dev/null +++ b/news/9232.bugfix.rst @@ -0,0 +1,2 @@ +New resolver: Make constraints also apply to package variants with extras, so +the resolver correctly avoids baktracking on them. diff --git a/src/pip/_internal/resolution/resolvelib/base.py b/src/pip/_internal/resolution/resolvelib/base.py index e2edbe9f42c..7eb8a178eb9 100644 --- a/src/pip/_internal/resolution/resolvelib/base.py +++ b/src/pip/_internal/resolution/resolvelib/base.py @@ -67,9 +67,25 @@ def is_satisfied_by(self, candidate): class Requirement(object): + @property + def project_name(self): + # type: () -> str + """The "project name" of a requirement. + + This is different from ``name`` if this requirement contains extras, + in which case ``name`` would contain the ``[...]`` part, while this + refers to the name of the project. + """ + raise NotImplementedError("Subclass should override") + @property def name(self): # type: () -> str + """The name identifying this requirement in the resolver. + + This is different from ``project_name`` if this requirement contains + extras, where ``project_name`` would not contain the ``[...]`` part. + """ raise NotImplementedError("Subclass should override") def is_satisfied_by(self, candidate): @@ -86,9 +102,25 @@ def format_for_error(self): class Candidate(object): + @property + def project_name(self): + # type: () -> str + """The "project name" of the candidate. + + This is different from ``name`` if this candidate contains extras, + in which case ``name`` would contain the ``[...]`` part, while this + refers to the name of the project. + """ + raise NotImplementedError("Override in subclass") + @property def name(self): # type: () -> str + """The name identifying this candidate in the resolver. + + This is different from ``project_name`` if this candidate contains + extras, where ``project_name`` would not contain the ``[...]`` part. + """ raise NotImplementedError("Override in subclass") @property diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 1fc2ff479a9..cd1f188706f 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -175,13 +175,18 @@ def source_link(self): return self._source_link @property - def name(self): + def project_name(self): # type: () -> str """The normalised name of the project the candidate refers to""" if self._name is None: self._name = canonicalize_name(self.dist.project_name) return self._name + @property + def name(self): + # type: () -> str + return self.project_name + @property def version(self): # type: () -> _BaseVersion @@ -390,10 +395,15 @@ def __ne__(self, other): return not self.__eq__(other) @property - def name(self): + def project_name(self): # type: () -> str return canonicalize_name(self.dist.project_name) + @property + def name(self): + # type: () -> str + return self.project_name + @property def version(self): # type: () -> _BaseVersion @@ -481,11 +491,16 @@ def __ne__(self, other): # type: (Any) -> bool return not self.__eq__(other) + @property + def project_name(self): + # type: () -> str + return self.base.project_name + @property def name(self): # type: () -> str """The normalised name of the project the candidate refers to""" - return format_name(self.base.name, self.extras) + return format_name(self.base.project_name, self.extras) @property def version(self): @@ -572,11 +587,16 @@ def __str__(self): return "Python {}".format(self._version) @property - def name(self): + def project_name(self): # type: () -> str # Avoid conflicting with the PyPI package "Python". return "<Python from Requires-Python>" + @property + def name(self): + # type: () -> str + return self.project_name + @property def version(self): # type: () -> _BaseVersion diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index c91f252f7b4..3883135f12b 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -30,6 +30,16 @@ class PipProvider(AbstractProvider): + """Pip's provider implementation for resolvelib. + + :params constraints: A mapping of constraints specified by the user. Keys + are canonicalized project names. + :params ignore_dependencies: Whether the user specified ``--no-deps``. + :params upgrade_strategy: The user-specified upgrade strategy. + :params user_requested: A set of canonicalized package names that the user + supplied for pip to install/upgrade. + """ + def __init__( self, factory, # type: Factory @@ -113,7 +123,7 @@ def find_matches(self, requirements): # type: (Sequence[Requirement]) -> Iterable[Candidate] if not requirements: return [] - name = requirements[0].name + name = requirements[0].project_name def _eligible_for_upgrade(name): # type: (str) -> bool diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py index 25cddceaf62..d926d0a0656 100644 --- a/src/pip/_internal/resolution/resolvelib/requirements.py +++ b/src/pip/_internal/resolution/resolvelib/requirements.py @@ -28,6 +28,12 @@ def __repr__(self): candidate=self.candidate, ) + @property + def project_name(self): + # type: () -> str + # No need to canonicalise - the candidate did this + return self.candidate.project_name + @property def name(self): # type: () -> str @@ -65,11 +71,15 @@ def __repr__(self): requirement=str(self._ireq.req), ) + @property + def project_name(self): + # type: () -> str + return canonicalize_name(self._ireq.req.name) + @property def name(self): # type: () -> str - canonical_name = canonicalize_name(self._ireq.req.name) - return format_name(canonical_name, self._extras) + return format_name(self.project_name, self._extras) def format_for_error(self): # type: () -> str @@ -121,6 +131,11 @@ def __repr__(self): specifier=str(self.specifier), ) + @property + def project_name(self): + # type: () -> str + return self._candidate.project_name + @property def name(self): # type: () -> str From 66b4a3d56ef83409bbf3cc480e3eff0ae070367e Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Date: Tue, 8 Dec 2020 12:51:24 +0000 Subject: [PATCH 2769/3170] Update news/9232.bugfix.rst --- news/9232.bugfix.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/9232.bugfix.rst b/news/9232.bugfix.rst index e881c53846c..2d50d1ce41d 100644 --- a/news/9232.bugfix.rst +++ b/news/9232.bugfix.rst @@ -1,2 +1,2 @@ New resolver: Make constraints also apply to package variants with extras, so -the resolver correctly avoids baktracking on them. +the resolver correctly avoids backtracking on them. From 8de94b5740ac0fb83f3dc899c9d4f23d85ed82c0 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Date: Tue, 8 Dec 2020 12:55:52 +0000 Subject: [PATCH 2770/3170] Update news/9191.bugfix.rst --- news/9191.bugfix.rst | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/news/9191.bugfix.rst b/news/9191.bugfix.rst index 436f2f2dd29..e1c6d633de9 100644 --- a/news/9191.bugfix.rst +++ b/news/9191.bugfix.rst @@ -1,2 +1,2 @@ -Handle case of list default values in UpdatingDefaultsHelpFormatter -Happens because we pass a list from --extra-index-url +Fix crash when logic for redacting authentication information from URLs +in ``--help`` is given a list of strings, instead of a single string. From 8bf159f4c777e22fc3e07ae89af73503cad64107 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 9 Dec 2020 11:27:51 +0800 Subject: [PATCH 2771/3170] Intentionally delay resolving setuptools --- src/pip/_internal/resolution/resolvelib/provider.py | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index 3883135f12b..40a641a2a4d 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -117,7 +117,18 @@ def _get_restrictive_rating(requirements): restrictive = _get_restrictive_rating(req for req, _ in information) transitive = all(parent is not None for _, parent in information) key = next(iter(candidates)).name if candidates else "" - return (restrictive, transitive, key) + + # HACK: Setuptools have a very long and solid backward compatibility + # track record, and extremely few projects would request a narrow, + # non-recent version range of it since that would break a lot things. + # (Most projects specify it only to request for an installer feature, + # which does not work, but that's another topic.) Intentionally + # delaying Setuptools helps reduce branches the resolver has to check. + # This serves as a temporary fix for issues like "apache-airlfow[all]" + # while we work on "proper" branch pruning techniques. + delay_this = (key == "setuptools") + + return (delay_this, restrictive, transitive, key) def find_matches(self, requirements): # type: (Sequence[Requirement]) -> Iterable[Candidate] From 52523832481d33237acb9f03a2154500d2b63ad5 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 9 Dec 2020 18:42:17 +0800 Subject: [PATCH 2772/3170] News entry for setuptools priority hack --- news/9249.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/9249.feature.rst diff --git a/news/9249.feature.rst b/news/9249.feature.rst new file mode 100644 index 00000000000..1c56b39ef5d --- /dev/null +++ b/news/9249.feature.rst @@ -0,0 +1 @@ +Add a mechanism to delay resolving certain packages, and use it for setuptools. From 0ddfe0882862717c24a32695ed5af5b69094226b Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko <wk@sydorenko.org.ua> Date: Sun, 29 Nov 2020 13:18:33 +0100 Subject: [PATCH 2773/3170] Add a preview for the "next release" change notes This change integrates `sphinxcontrib-towncrier` into the docs build. It uses Towncrier and its configs to render a draft changelog as RST and injects that into the `news.rst` document right above the older changelog entries using `towncrier-draft-entries` directive. Co-Authored-By: Pradyun Gedam <pradyunsg@gmail.com> --- docs/html/conf.py | 9 +++++++++ docs/html/news.rst | 2 ++ news/9172.doc.rst | 1 + tools/requirements/docs.txt | 1 + 4 files changed, 13 insertions(+) create mode 100644 news/9172.doc.rst diff --git a/docs/html/conf.py b/docs/html/conf.py index 7983187e9a3..b61c08bd57e 100644 --- a/docs/html/conf.py +++ b/docs/html/conf.py @@ -13,6 +13,7 @@ import glob import os +import pathlib import re import sys @@ -36,6 +37,7 @@ 'sphinx.ext.intersphinx', # third-party: 'sphinx_inline_tabs', + 'sphinxcontrib.towncrier', # in-tree: 'docs_feedback_sphinxext', 'pip_sphinxext', @@ -316,3 +318,10 @@ def to_document_name(path, base_dir): 'What content was useful?', 'What content was not useful?', ) + +# -- Options for towncrier_draft extension ----------------------------------- + +towncrier_draft_autoversion_mode = 'draft' # or: 'sphinx-release', 'sphinx-version' +towncrier_draft_include_empty = True +towncrier_draft_working_directory = pathlib.Path(docs_dir).parent +# Not yet supported: towncrier_draft_config_path = 'pyproject.toml' # relative to cwd diff --git a/docs/html/news.rst b/docs/html/news.rst index 137cddf36ab..8b54a02e637 100644 --- a/docs/html/news.rst +++ b/docs/html/news.rst @@ -7,4 +7,6 @@ Changelog Major and minor releases of pip also include changes listed within prior beta releases. +.. towncrier-draft-entries:: |release|, unreleased as on + .. include:: ../../NEWS.rst diff --git a/news/9172.doc.rst b/news/9172.doc.rst new file mode 100644 index 00000000000..fc0063766b2 --- /dev/null +++ b/news/9172.doc.rst @@ -0,0 +1 @@ +Render the unreleased pip version change notes on the news page in docs. diff --git a/tools/requirements/docs.txt b/tools/requirements/docs.txt index 0c5103d0a2a..a5aae67c106 100644 --- a/tools/requirements/docs.txt +++ b/tools/requirements/docs.txt @@ -1,6 +1,7 @@ sphinx == 3.2.1 furo sphinx-inline-tabs +sphinxcontrib-towncrier # `docs.pipext` uses pip's internals to generate documentation. So, we install # the current directory to make it work. From d45541c8f3b3d87ae55a08d7021e8e879293285c Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sat, 12 Dec 2020 01:02:59 +0800 Subject: [PATCH 2774/3170] Skip candidate not providing valid metadata This is done by catching InstallationError from the underlying distribution preparation logic. There are three cases to catch: 1. Candidates from indexes. These are simply ignored since we can potentially satisfy the requirement with other candidates. 2. Candidates from URLs with a dist name (PEP 508 or #egg=). A new UnsatisfiableRequirement class is introduced to represent this; it is like an ExplicitRequirement without an underlying candidate. As the name suggests, an instance of this can never be satisfied, and will cause eventual backtracking. 3. Candidates from URLs without a dist name. This is only possible for top-level user requirements, and no recourse is possible for them. So we error out eagerly. The InstallationError raised during distribution preparation is cached in the factory, like successfully prepared candidates, since we don't want to repeatedly try to build a candidate if we already know it'd fail. Plus pip's preparation logic also does not allow packages to be built multiple times anyway. --- news/9246.bugfix.rst | 2 + src/pip/_internal/exceptions.py | 15 +++++++ .../resolution/resolvelib/candidates.py | 20 ++------- .../resolution/resolvelib/factory.py | 41 +++++++++++++++---- .../resolution/resolvelib/requirements.py | 41 +++++++++++++++++++ src/pip/_internal/utils/subprocess.py | 8 +--- tests/functional/test_new_resolver.py | 19 +++++++++ tests/unit/test_utils_subprocess.py | 8 ++-- 8 files changed, 119 insertions(+), 35 deletions(-) create mode 100644 news/9246.bugfix.rst diff --git a/news/9246.bugfix.rst b/news/9246.bugfix.rst new file mode 100644 index 00000000000..e7ebd398f3e --- /dev/null +++ b/news/9246.bugfix.rst @@ -0,0 +1,2 @@ +New resolver: Discard a candidate if it fails to provide metadata from source, +or if the provided metadata is inconsistent, instead of quitting outright. diff --git a/src/pip/_internal/exceptions.py b/src/pip/_internal/exceptions.py index 56482caf77b..3f2659e8792 100644 --- a/src/pip/_internal/exceptions.py +++ b/src/pip/_internal/exceptions.py @@ -151,6 +151,21 @@ def __str__(self): ) +class InstallationSubprocessError(InstallationError): + """A subprocess call failed during installation.""" + def __init__(self, returncode, description): + # type: (int, str) -> None + self.returncode = returncode + self.description = description + + def __str__(self): + # type: () -> str + return ( + "Command errored out with exit status {}: {} " + "Check the logs for full command output." + ).format(self.returncode, self.description) + + class HashErrors(InstallationError): """Multiple HashError instances rolled into one for reporting""" diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index cd1f188706f..5d838d1d405 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -141,7 +141,7 @@ def __init__( self._ireq = ireq self._name = name self._version = version - self._dist = None # type: Optional[Distribution] + self.dist = self._prepare() def __str__(self): # type: () -> str @@ -209,8 +209,6 @@ def _prepare_distribution(self): def _check_metadata_consistency(self, dist): # type: (Distribution) -> None """Check for consistency of project name and version of dist.""" - # TODO: (Longer term) Rather than abort, reject this candidate - # and backtrack. This would need resolvelib support. name = canonicalize_name(dist.project_name) if self._name is not None and self._name != name: raise MetadataInconsistent(self._ireq, "name", dist.project_name) @@ -219,25 +217,14 @@ def _check_metadata_consistency(self, dist): raise MetadataInconsistent(self._ireq, "version", dist.version) def _prepare(self): - # type: () -> None - if self._dist is not None: - return + # type: () -> Distribution try: dist = self._prepare_distribution() except HashError as e: e.req = self._ireq raise - - assert dist is not None, "Distribution already installed" self._check_metadata_consistency(dist) - self._dist = dist - - @property - def dist(self): - # type: () -> Distribution - if self._dist is None: - self._prepare() - return self._dist + return dist def _get_requires_python_dependency(self): # type: () -> Optional[Requirement] @@ -261,7 +248,6 @@ def iter_dependencies(self, with_requires): def get_install_requirement(self): # type: () -> Optional[InstallRequirement] - self._prepare() return self._ireq diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index c723d343bf9..5cd333bad14 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -5,6 +5,8 @@ from pip._internal.exceptions import ( DistributionNotFound, InstallationError, + InstallationSubprocessError, + MetadataInconsistent, UnsupportedPythonVersion, UnsupportedWheel, ) @@ -33,6 +35,7 @@ ExplicitRequirement, RequiresPythonRequirement, SpecifierRequirement, + UnsatisfiableRequirement, ) if MYPY_CHECK_RUNNING: @@ -96,6 +99,7 @@ def __init__( self._link_candidate_cache = {} # type: Cache[LinkCandidate] self._editable_candidate_cache = {} # type: Cache[EditableCandidate] + self._build_failures = {} # type: Cache[InstallationError] if not ignore_installed: self._installed_dists = { @@ -130,20 +134,34 @@ def _make_candidate_from_link( name, # type: Optional[str] version, # type: Optional[_BaseVersion] ): - # type: (...) -> Candidate + # type: (...) -> Optional[Candidate] # TODO: Check already installed candidate, and use it if the link and # editable flag match. + if link in self._build_failures: + return None if template.editable: if link not in self._editable_candidate_cache: - self._editable_candidate_cache[link] = EditableCandidate( - link, template, factory=self, name=name, version=version, - ) + try: + self._editable_candidate_cache[link] = EditableCandidate( + link, template, factory=self, + name=name, version=version, + ) + except (InstallationSubprocessError, MetadataInconsistent) as e: + logger.warning("Discarding %s. %s", link, e) + self._build_failures[link] = e + return None base = self._editable_candidate_cache[link] # type: BaseCandidate else: if link not in self._link_candidate_cache: - self._link_candidate_cache[link] = LinkCandidate( - link, template, factory=self, name=name, version=version, - ) + try: + self._link_candidate_cache[link] = LinkCandidate( + link, template, factory=self, + name=name, version=version, + ) + except (InstallationSubprocessError, MetadataInconsistent) as e: + logger.warning("Discarding %s. %s", link, e) + self._build_failures[link] = e + return None base = self._link_candidate_cache[link] if extras: return ExtrasCandidate(base, extras) @@ -204,13 +222,16 @@ def iter_index_candidates(): for ican in reversed(icans): if not all_yanked and ican.link.is_yanked: continue - yield self._make_candidate_from_link( + candidate = self._make_candidate_from_link( link=ican.link, extras=extras, template=template, name=name, version=ican.version, ) + if candidate is None: + continue + yield candidate return FoundCandidates( iter_index_candidates, @@ -274,6 +295,10 @@ def make_requirement_from_install_req(self, ireq, requested_extras): name=canonicalize_name(ireq.name) if ireq.name else None, version=None, ) + if cand is None: + if not ireq.name: + raise self._build_failures[ireq.link] + return UnsatisfiableRequirement(canonicalize_name(ireq.name)) return self.make_requirement_from_candidate(cand) def make_requirement_from_candidate(self, candidate): diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py index d926d0a0656..1229f353750 100644 --- a/src/pip/_internal/resolution/resolvelib/requirements.py +++ b/src/pip/_internal/resolution/resolvelib/requirements.py @@ -158,3 +158,44 @@ def is_satisfied_by(self, candidate): # already implements the prerelease logic, and would have filtered out # prerelease candidates if the user does not expect them. return self.specifier.contains(candidate.version, prereleases=True) + + +class UnsatisfiableRequirement(Requirement): + """A requirement that cannot be satisfied. + """ + def __init__(self, name): + # type: (str) -> None + self._name = name + + def __str__(self): + # type: () -> str + return "{} (unavailable)".format(self._name) + + def __repr__(self): + # type: () -> str + return "{class_name}({name!r})".format( + class_name=self.__class__.__name__, + name=str(self._name), + ) + + @property + def project_name(self): + # type: () -> str + return self._name + + @property + def name(self): + # type: () -> str + return self._name + + def format_for_error(self): + # type: () -> str + return str(self) + + def get_candidate_lookup(self): + # type: () -> CandidateLookup + return None, None + + def is_satisfied_by(self, candidate): + # type: (Candidate) -> bool + return False diff --git a/src/pip/_internal/utils/subprocess.py b/src/pip/_internal/utils/subprocess.py index 605e711e603..325897c8739 100644 --- a/src/pip/_internal/utils/subprocess.py +++ b/src/pip/_internal/utils/subprocess.py @@ -7,7 +7,7 @@ from pip._vendor.six.moves import shlex_quote from pip._internal.cli.spinners import SpinnerInterface, open_spinner -from pip._internal.exceptions import InstallationError +from pip._internal.exceptions import InstallationSubprocessError from pip._internal.utils.compat import console_to_str, str_to_display from pip._internal.utils.logging import subprocess_logger from pip._internal.utils.misc import HiddenText, path_to_display @@ -233,11 +233,7 @@ def call_subprocess( exit_status=proc.returncode, ) subprocess_logger.error(msg) - exc_msg = ( - 'Command errored out with exit status {}: {} ' - 'Check the logs for full command output.' - ).format(proc.returncode, command_desc) - raise InstallationError(exc_msg) + raise InstallationSubprocessError(proc.returncode, command_desc) elif on_returncode == 'warn': subprocess_logger.warning( 'Command "%s" had error code %s in %s', diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index b730b3cbdf9..00d82fb95ee 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -1218,3 +1218,22 @@ def test_new_resolver_does_not_reinstall_when_from_a_local_index(script): assert "Installing collected packages: simple" not in result.stdout, str(result) assert "Requirement already satisfied: simple" in result.stdout, str(result) assert_installed(script, simple="0.1.0") + + +def test_new_resolver_skip_inconsistent_metadata(script): + create_basic_wheel_for_package(script, "A", "1") + + a_2 = create_basic_wheel_for_package(script, "A", "2") + a_2.rename(a_2.parent.joinpath("a-3-py2.py3-none-any.whl")) + + result = script.pip( + "install", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "--verbose", + "A", + allow_stderr_warning=True, + ) + + assert " different version in metadata: '2'" in result.stderr, str(result) + assert_installed(script, a="1") diff --git a/tests/unit/test_utils_subprocess.py b/tests/unit/test_utils_subprocess.py index b0de2bf578d..1a031065114 100644 --- a/tests/unit/test_utils_subprocess.py +++ b/tests/unit/test_utils_subprocess.py @@ -7,7 +7,7 @@ import pytest from pip._internal.cli.spinners import SpinnerInterface -from pip._internal.exceptions import InstallationError +from pip._internal.exceptions import InstallationSubprocessError from pip._internal.utils.misc import hide_value from pip._internal.utils.subprocess import ( call_subprocess, @@ -276,7 +276,7 @@ def test_info_logging__subprocess_error(self, capfd, caplog): command = 'print("Hello"); print("world"); exit("fail")' args, spinner = self.prepare_call(caplog, log_level, command=command) - with pytest.raises(InstallationError) as exc: + with pytest.raises(InstallationSubprocessError) as exc: call_subprocess(args, spinner=spinner) result = None exc_message = str(exc.value) @@ -360,7 +360,7 @@ def test_info_logging_with_show_stdout_true(self, capfd, caplog): # log level is only WARNING. (0, True, None, WARNING, (None, 'done', 2)), # Test a non-zero exit status. - (3, False, None, INFO, (InstallationError, 'error', 2)), + (3, False, None, INFO, (InstallationSubprocessError, 'error', 2)), # Test a non-zero exit status also in extra_ok_returncodes. (3, False, (3, ), INFO, (None, 'done', 2)), ]) @@ -396,7 +396,7 @@ def test_spinner_finish( assert spinner.spin_count == expected_spin_count def test_closes_stdin(self): - with pytest.raises(InstallationError): + with pytest.raises(InstallationSubprocessError): call_subprocess( [sys.executable, '-c', 'input()'], show_stdout=True, From 120105d3e8566f79449bea968515944dcb68dbfc Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sat, 12 Dec 2020 02:19:51 +0800 Subject: [PATCH 2775/3170] Cache AlreadyInstalledCandidate Since the "Requirement already satisfied" message is printed during candidate preparation, instantiating the candidate multiple times result in excessive logging during intensive backtracking. By caching the already-installed candidates, each package is only prepared, and thus only logged once. --- news/9117.bugfix.rst | 2 ++ src/pip/_internal/resolution/resolvelib/factory.py | 8 +++++++- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 news/9117.bugfix.rst diff --git a/news/9117.bugfix.rst b/news/9117.bugfix.rst new file mode 100644 index 00000000000..7991dac7d01 --- /dev/null +++ b/news/9117.bugfix.rst @@ -0,0 +1,2 @@ +New resolver: The "Requirement already satisfied" log is not printed only once +for each package during resolution. diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index c723d343bf9..b4c7bf11351 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -96,6 +96,8 @@ def __init__( self._link_candidate_cache = {} # type: Cache[LinkCandidate] self._editable_candidate_cache = {} # type: Cache[EditableCandidate] + self._installed_candidate_cache = { + } # type: Dict[str, AlreadyInstalledCandidate] if not ignore_installed: self._installed_dists = { @@ -117,7 +119,11 @@ def _make_candidate_from_dist( template, # type: InstallRequirement ): # type: (...) -> Candidate - base = AlreadyInstalledCandidate(dist, template, factory=self) + try: + base = self._installed_candidate_cache[dist.key] + except KeyError: + base = AlreadyInstalledCandidate(dist, template, factory=self) + self._installed_candidate_cache[dist.key] = base if extras: return ExtrasCandidate(base, extras) return base From b2c04877fa145894579af423f3088b300f74877c Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sat, 12 Dec 2020 18:47:20 +0800 Subject: [PATCH 2776/3170] Add comments explaining InstallationError handling --- src/pip/_internal/resolution/resolvelib/candidates.py | 3 +++ src/pip/_internal/resolution/resolvelib/factory.py | 11 +++++++++++ 2 files changed, 14 insertions(+) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 5d838d1d405..83b6c98ab6a 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -221,6 +221,9 @@ def _prepare(self): try: dist = self._prepare_distribution() except HashError as e: + # Provide HashError the underlying ireq that caused it. This + # provides context for the resulting error message to show the + # offending line to the user. e.req = self._ireq raise self._check_metadata_consistency(dist) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 5cd333bad14..c7fb4f3f03e 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -137,8 +137,12 @@ def _make_candidate_from_link( # type: (...) -> Optional[Candidate] # TODO: Check already installed candidate, and use it if the link and # editable flag match. + if link in self._build_failures: + # We already tried this candidate before, and it does not build. + # Don't bother trying again. return None + if template.editable: if link not in self._editable_candidate_cache: try: @@ -163,6 +167,7 @@ def _make_candidate_from_link( self._build_failures[link] = e return None base = self._link_candidate_cache[link] + if extras: return ExtrasCandidate(base, extras) return base @@ -296,6 +301,12 @@ def make_requirement_from_install_req(self, ireq, requested_extras): version=None, ) if cand is None: + # There's no way we can satisfy a URL requirement if the underlying + # candidate fails to build. An unnamed URL must be user-supplied, so + # we fail eagerly. If the URL is named, an unsatisfiable requirement + # can make the resolver do the right thing, either backtrack (and + # maybe find some other requirement that's buildable) or raise a + # ResolutionImpossible eventually. if not ireq.name: raise self._build_failures[ireq.link] return UnsatisfiableRequirement(canonicalize_name(ireq.name)) From c7591bf973bc4977ab645cda7d67841e20096174 Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara <sh@changeset.nyc> Date: Sun, 13 Dec 2020 17:25:59 -0500 Subject: [PATCH 2777/3170] docs: Clarify that old resolver is default with Python 2 Partially addresses #9194. Signed-off-by: Sumana Harihareswara <sh@changeset.nyc> --- README.rst | 13 ++++++++++-- docs/html/development/release-process.rst | 5 +++-- docs/html/index.rst | 13 ++++++++++-- docs/html/user_guide.rst | 24 +++++++++++++---------- 4 files changed, 39 insertions(+), 16 deletions(-) diff --git a/README.rst b/README.rst index d8d12c850da..ef0dd0ecadd 100644 --- a/README.rst +++ b/README.rst @@ -19,7 +19,16 @@ We release updates regularly, with a new version every 3 months. Find more detai * `Release notes`_ * `Release process`_ -In pip 20.3, we're `making a big improvement to the heart of pip`_; `learn more`_. We want your input, so `sign up for our user experience research studies`_ to help us do it right. +.. warning:: + + In pip 20.3, we've `made a big improvement to the heart of pip`_; + :ref:`Resolver changes 2020`. We want your input, so `sign up for + our user experience research studies`_ to help us do it right. + +.. warning:: + + pip 21.0, in January 2021, will remove Python 2 support, per pip's + :ref:`Python 2 Support` policy. Please migrate to Python 3. If you find bugs, need help, or want to talk to the developers, please use our mailing lists or chat rooms: @@ -48,7 +57,7 @@ rooms, and mailing lists is expected to follow the `PSF Code of Conduct`_. .. _Release process: https://pip.pypa.io/en/latest/development/release-process/ .. _GitHub page: https://github.com/pypa/pip .. _Development documentation: https://pip.pypa.io/en/latest/development -.. _making a big improvement to the heart of pip: https://pyfound.blogspot.com/2020/03/new-pip-resolver-to-roll-out-this-year.html +.. _made a big improvement to the heart of pip: https://pyfound.blogspot.com/2020/11/pip-20-3-new-resolver.html .. _learn more: https://pip.pypa.io/en/latest/user_guide/#changes-to-the-pip-dependency-resolver-in-20-3-2020 .. _sign up for our user experience research studies: https://pyfound.blogspot.com/2020/03/new-pip-resolver-to-roll-out-this-year.html .. _Issue tracking: https://github.com/pypa/pip/issues diff --git a/docs/html/development/release-process.rst b/docs/html/development/release-process.rst index e8c4f579553..91ce3e23379 100644 --- a/docs/html/development/release-process.rst +++ b/docs/html/development/release-process.rst @@ -70,8 +70,8 @@ their merits. Python 2 Support ---------------- -pip will continue to ensure that it runs on Python 2.7 after the CPython 2.7 -EOL date. Support for Python 2.7 will be dropped, if bugs in Python 2.7 itself +pip will continue to ensure that it runs on Python 2.7 after the `CPython 2.7 +EOL date`_. Support for Python 2.7 will be dropped, if bugs in Python 2.7 itself make this necessary (which is unlikely) or in pip 21.0 (Jan 2021), whichever is earlier. @@ -180,3 +180,4 @@ order to create one of these the changes should already be merged into the .. _`get-pip repository`: https://github.com/pypa/get-pip .. _`psf-salt repository`: https://github.com/python/psf-salt .. _`CPython`: https://github.com/python/cpython +.. _`CPython 2.7 EOL date`: https://www.python.org/doc/sunset-python-2/ diff --git a/docs/html/index.rst b/docs/html/index.rst index ce40b49fa6c..1ac460bd9d9 100644 --- a/docs/html/index.rst +++ b/docs/html/index.rst @@ -17,7 +17,16 @@ Please take a look at our documentation for how to install and use pip: ux_research_design news -In pip 20.3, we're `making a big improvement to the heart of pip`_; :ref:`Resolver changes 2020`. We want your input, so `sign up for our user experience research studies`_ to help us do it right. +.. warning:: + + In pip 20.3, we've `made a big improvement to the heart of pip`_; + :ref:`Resolver changes 2020`. We want your input, so `sign up for + our user experience research studies`_ to help us do it right. + +.. warning:: + + pip 21.0, in January 2021, will remove Python 2 support, per pip's + :ref:`Python 2 Support` policy. Please migrate to Python 3. If you find bugs, need help, or want to talk to the developers, please use our mailing lists or chat rooms: @@ -40,7 +49,7 @@ rooms, and mailing lists is expected to follow the `PSF Code of Conduct`_. .. _package installer: https://packaging.python.org/guides/tool-recommendations/ .. _Python Package Index: https://pypi.org -.. _making a big improvement to the heart of pip: https://pyfound.blogspot.com/2020/03/new-pip-resolver-to-roll-out-this-year.html +.. _made a big improvement to the heart of pip: https://pyfound.blogspot.com/2020/11/pip-20-3-new-resolver.html .. _sign up for our user experience research studies: https://pyfound.blogspot.com/2020/03/new-pip-resolver-to-roll-out-this-year.html .. _Installation: https://pip.pypa.io/en/stable/installing.html .. _Documentation: https://pip.pypa.io/en/stable/ diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index e3aa5dc33d2..873d413107f 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -1610,11 +1610,12 @@ of ability. Some examples that you could consider include: Changes to the pip dependency resolver in 20.3 (2020) ===================================================== -pip 20.3 has a new dependency resolver, on by default. (pip 20.1 and -20.2 included pre-release versions of the new dependency resolver, -hidden behind optional user flags.) Read below for a migration guide, -how to invoke the legacy resolver, and the deprecation timeline. We -also made a `two-minute video explanation`_ you can watch. +pip 20.3 has a new dependency resolver, on by default for Python 3 +users. (pip 20.1 and 20.2 included pre-release versions of the new +dependency resolver, hidden behind optional user flags.) Read below +for a migration guide, how to invoke the legacy resolver, and the +deprecation timeline. We also made a `two-minute video explanation`_ +you can watch. We will continue to improve the pip dependency resolver in response to testers' feedback. Please give us feedback through the `resolver @@ -1815,7 +1816,7 @@ Specific things we'd love to get feedback on: * Cases where the new resolver produces the wrong result, obviously. We hope there won't be too many of these, but we'd like - to trap such bugs now. + to trap such bugs before we remove the legacy resolver. * Cases where the resolver produced an error when you believe it should have been able to work out what to do. @@ -1850,12 +1851,15 @@ We plan for the resolver changeover to proceed as follows, using ``PIP_USE_FEATURE`` environment variable option, see `issue 8661`_). -* pip 20.3: pip defaults to the new resolver, but a user can opt-out - and choose the old resolver behavior, using the flag - ``--use-deprecated=legacy-resolver``. +* pip 20.3: pip defaults to the new resolver in Python 3 environments, + but a user can opt-out and choose the old resolver behavior, + using the flag ``--use-deprecated=legacy-resolver``. In Python 2 + environments, pip defaults to the old resolver, and the new one ia + available using the flag ``--use-feature=2020-resolver``. * pip 21.0: pip uses new resolver, and the old resolver is no longer - available. + available. Python 2 support is removed per our :ref:`Python 2 + Support` policy. Since this work will not change user-visible behavior described in the pip documentation, this change is not covered by the :ref:`Deprecation From df7a97f3bc9a7bb0b1bcbcd15419468d05beb442 Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara <sh@changeset.nyc> Date: Sun, 13 Dec 2020 17:27:14 -0500 Subject: [PATCH 2778/3170] docs: Fix small style issues. Signed-off-by: Sumana Harihareswara <sh@changeset.nyc> --- docs/html/user_guide.rst | 14 +++++++------- news/9269.doc.rst | 1 + 2 files changed, 8 insertions(+), 7 deletions(-) create mode 100644 news/9269.doc.rst diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 873d413107f..e8218fb785d 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -512,8 +512,8 @@ all users) configuration: else :file:`$HOME/.config/pip/pip.conf`. * On Windows the configuration file is :file:`%APPDATA%\\pip\\pip.ini`. -There are also a legacy per-user configuration file which is also respected, -these are located at: +There is also a legacy per-user configuration file which is also respected. +To find its location: * On Unix and macOS the configuration file is: :file:`$HOME/.pip/pip.conf` * On Windows the configuration file is: :file:`%HOME%\\pip\\pip.ini` @@ -1084,7 +1084,7 @@ You can then install from the archive like this:: $ python -m pip install --force-reinstall --ignore-installed --upgrade --no-index --no-deps $tempdir/* Note that compiled packages are typically OS- and architecture-specific, so -these archives are not necessarily portable across macOShines. +these archives are not necessarily portable across machines. Hash-checking mode can be used along with this method to ensure that future archives are built with identical packages. @@ -1331,7 +1331,7 @@ Backtracking is not a bug, or an unexpected behaviour. It is part of the way pip's dependency resolution process works. During a pip install (e.g. ``pip install tea``), pip needs to work out -the package's dependencies (e.g. ``spoon``, ``hot-water``, ``cup`` etc), the +the package's dependencies (e.g. ``spoon``, ``hot-water``, ``cup`` etc.), the versions of each of these packages it needs to install. For each package pip needs to decide which version is a good candidate to install. @@ -1466,9 +1466,9 @@ here are a number of ways. In most cases, pip will complete the backtracking process successfully. It is possible this could take a very long time to complete - this may -not be the preferred option. +not be your preferred option. -However there is a possibility pip will not be able to find a set of +However, there is a possibility pip will not be able to find a set of compatible versions. If you'd prefer not to wait, you can interrupt pip (ctrl and c) and use @@ -1523,7 +1523,7 @@ suitable constraints file. 4. Be more strict on package dependencies during development ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ -For package maintainers during the development, give pip some help by +For package maintainers during software development, give pip some help by creating constraint files for the dependency tree. This will reduce the number of versions it will try. diff --git a/news/9269.doc.rst b/news/9269.doc.rst new file mode 100644 index 00000000000..f8ff14d9428 --- /dev/null +++ b/news/9269.doc.rst @@ -0,0 +1 @@ +Update documentation to reflect that pip still uses legacy resolver by default in Python 2 environments. From 8acf6d4a3cd150f54d46f45ef26bb178cfda602c Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara <sh@changeset.nyc> Date: Sun, 13 Dec 2020 17:50:45 -0500 Subject: [PATCH 2779/3170] docs: Fix README for PyPI rendering Signed-off-by: Sumana Harihareswara <sh@changeset.nyc> --- README.rst | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/README.rst b/README.rst index ef0dd0ecadd..a15de466b7f 100644 --- a/README.rst +++ b/README.rst @@ -19,16 +19,9 @@ We release updates regularly, with a new version every 3 months. Find more detai * `Release notes`_ * `Release process`_ -.. warning:: +In pip 20.3, we've `made a big improvement to the heart of pip`_; `learn more`_. We want your input, so `sign up for our user experience research studies`_ to help us do it right. - In pip 20.3, we've `made a big improvement to the heart of pip`_; - :ref:`Resolver changes 2020`. We want your input, so `sign up for - our user experience research studies`_ to help us do it right. - -.. warning:: - - pip 21.0, in January 2021, will remove Python 2 support, per pip's - :ref:`Python 2 Support` policy. Please migrate to Python 3. +**Note**: pip 21.0, in January 2021, will remove Python 2 support, per pip's `Python 2 support policy`_. Please migrate to Python 3. If you find bugs, need help, or want to talk to the developers, please use our mailing lists or chat rooms: @@ -60,6 +53,7 @@ rooms, and mailing lists is expected to follow the `PSF Code of Conduct`_. .. _made a big improvement to the heart of pip: https://pyfound.blogspot.com/2020/11/pip-20-3-new-resolver.html .. _learn more: https://pip.pypa.io/en/latest/user_guide/#changes-to-the-pip-dependency-resolver-in-20-3-2020 .. _sign up for our user experience research studies: https://pyfound.blogspot.com/2020/03/new-pip-resolver-to-roll-out-this-year.html +.. _Python 2 support policy: https://pip.pypa.io/en/latest/development/release-process/#python-2-support .. _Issue tracking: https://github.com/pypa/pip/issues .. _Discourse channel: https://discuss.python.org/c/packaging .. _Development mailing list: https://mail.python.org/mailman3/lists/distutils-sig.python.org/ From acc0cc9fe331b7f3f81322c212b169c0d014c3ab Mon Sep 17 00:00:00 2001 From: Sumana Harihareswara <sh@changeset.nyc> Date: Mon, 14 Dec 2020 06:46:14 -0500 Subject: [PATCH 2780/3170] docs: Fix typo Co-authored-by: Xavier Fernandez <xav.fernandez@gmail.com> --- docs/html/user_guide.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index e8218fb785d..415c9b1e71c 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -1854,7 +1854,7 @@ We plan for the resolver changeover to proceed as follows, using * pip 20.3: pip defaults to the new resolver in Python 3 environments, but a user can opt-out and choose the old resolver behavior, using the flag ``--use-deprecated=legacy-resolver``. In Python 2 - environments, pip defaults to the old resolver, and the new one ia + environments, pip defaults to the old resolver, and the new one is available using the flag ``--use-feature=2020-resolver``. * pip 21.0: pip uses new resolver, and the old resolver is no longer From 6b2ccdd10d8720cd8de5b7428277a2b0afe41bc1 Mon Sep 17 00:00:00 2001 From: gpiks <gaurav.pikale@gmail.com> Date: Mon, 14 Dec 2020 14:12:47 -0500 Subject: [PATCH 2781/3170] Update packaging to version 20.8 To account for Python 3.10, update the packaging version to 20.8. --- news/packaging.vendor.rst | 1 + src/pip/_vendor/packaging/__about__.py | 27 +++++++++++++++++++++++ src/pip/_vendor/packaging/__init__.py | 25 +++++++++++++++++++-- src/pip/_vendor/packaging/requirements.py | 12 +++++----- src/pip/_vendor/packaging/tags.py | 8 ++----- src/pip/_vendor/vendor.txt | 2 +- 6 files changed, 60 insertions(+), 15 deletions(-) create mode 100644 news/packaging.vendor.rst create mode 100644 src/pip/_vendor/packaging/__about__.py diff --git a/news/packaging.vendor.rst b/news/packaging.vendor.rst new file mode 100644 index 00000000000..24d7440e055 --- /dev/null +++ b/news/packaging.vendor.rst @@ -0,0 +1 @@ +Update vendoring to 20.8 diff --git a/src/pip/_vendor/packaging/__about__.py b/src/pip/_vendor/packaging/__about__.py new file mode 100644 index 00000000000..2d39193b051 --- /dev/null +++ b/src/pip/_vendor/packaging/__about__.py @@ -0,0 +1,27 @@ +# This file is dual licensed under the terms of the Apache License, Version +# 2.0, and the BSD License. See the LICENSE file in the root of this repository +# for complete details. +from __future__ import absolute_import, division, print_function + +__all__ = [ + "__title__", + "__summary__", + "__uri__", + "__version__", + "__author__", + "__email__", + "__license__", + "__copyright__", +] + +__title__ = "packaging" +__summary__ = "Core utilities for Python packages" +__uri__ = "https://github.com/pypa/packaging" + +__version__ = "20.8" + +__author__ = "Donald Stufft and individual contributors" +__email__ = "donald@stufft.io" + +__license__ = "BSD-2-Clause or Apache-2.0" +__copyright__ = "2014-2019 %s" % __author__ diff --git a/src/pip/_vendor/packaging/__init__.py b/src/pip/_vendor/packaging/__init__.py index 18fecb867a8..a0cf67df524 100644 --- a/src/pip/_vendor/packaging/__init__.py +++ b/src/pip/_vendor/packaging/__init__.py @@ -1,5 +1,26 @@ # This file is dual licensed under the terms of the Apache License, Version # 2.0, and the BSD License. See the LICENSE file in the root of this repository # for complete details. -"""Core utilities for Python packages""" -__version__ = "20.7" +from __future__ import absolute_import, division, print_function + +from .__about__ import ( + __author__, + __copyright__, + __email__, + __license__, + __summary__, + __title__, + __uri__, + __version__, +) + +__all__ = [ + "__title__", + "__summary__", + "__uri__", + "__version__", + "__author__", + "__email__", + "__license__", + "__copyright__", +] diff --git a/src/pip/_vendor/packaging/requirements.py b/src/pip/_vendor/packaging/requirements.py index f9d1c65991a..df7f41d2c08 100644 --- a/src/pip/_vendor/packaging/requirements.py +++ b/src/pip/_vendor/packaging/requirements.py @@ -22,7 +22,7 @@ if TYPE_CHECKING: # pragma: no cover - from typing import List + from typing import List, Optional as TOptional, Set class InvalidRequirement(ValueError): @@ -109,7 +109,7 @@ def __init__(self, requirement_string): ) ) - self.name = req.name + self.name = req.name # type: str if req.url: parsed_url = urlparse.urlparse(req.url) if parsed_url.scheme == "file": @@ -119,12 +119,12 @@ def __init__(self, requirement_string): not parsed_url.scheme and not parsed_url.netloc ): raise InvalidRequirement("Invalid URL: {0}".format(req.url)) - self.url = req.url + self.url = req.url # type: TOptional[str] else: self.url = None - self.extras = set(req.extras.asList() if req.extras else []) - self.specifier = SpecifierSet(req.specifier) - self.marker = req.marker if req.marker else None + self.extras = set(req.extras.asList() if req.extras else []) # type: Set[str] + self.specifier = SpecifierSet(req.specifier) # type: SpecifierSet + self.marker = req.marker if req.marker else None # type: TOptional[Marker] def __str__(self): # type: () -> str diff --git a/src/pip/_vendor/packaging/tags.py b/src/pip/_vendor/packaging/tags.py index 842447d863b..13798e38bce 100644 --- a/src/pip/_vendor/packaging/tags.py +++ b/src/pip/_vendor/packaging/tags.py @@ -410,7 +410,7 @@ def _mac_binary_formats(version, cpu_arch): if cpu_arch in {"arm64", "x86_64"}: formats.append("universal2") - if cpu_arch in {"x86_64", "i386", "ppc64", "ppc"}: + if cpu_arch in {"x86_64", "i386", "ppc64", "ppc", "intel"}: formats.append("universal") return formats @@ -827,11 +827,7 @@ def interpreter_version(**kwargs): def _version_nodot(version): # type: (PythonVersion) -> str - if any(v >= 10 for v in version): - sep = "_" - else: - sep = "" - return sep.join(map(str, version)) + return "".join(map(str, version)) def sys_tags(**kwargs): diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index c7bc37c16c4..15c000339ae 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -7,7 +7,7 @@ distro==1.5.0 html5lib==1.1 ipaddress==1.0.23 # Only needed on 2.6 and 2.7 msgpack==1.0.0 -packaging==20.7 +packaging==20.8 pep517==0.9.1 progress==1.5 pyparsing==2.4.7 From 08816b30b5c9b8e67bbd339997443fc3af05378f Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Tue, 15 Dec 2020 01:45:05 +0000 Subject: [PATCH 2782/3170] Update AUTHORS.txt --- AUTHORS.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.txt b/AUTHORS.txt index 5b53db6c2b7..0360f988f2b 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -504,6 +504,7 @@ Simon Cross Simon Pichugin sinoroc sinscary +socketubs Sorin Sbarnea Srinivas Nyayapati Stavros Korokithakis From e1fded5780ac24bbfb311538a75f66384ade7226 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Tue, 15 Dec 2020 01:45:06 +0000 Subject: [PATCH 2783/3170] Bump for release --- NEWS.rst | 35 +++++++++++++++++++++++++++++++++++ news/9117.bugfix.rst | 2 -- news/9185.feature.rst | 2 -- news/9191.bugfix.rst | 2 -- news/9203.bugfix.rst | 2 -- news/9232.bugfix.rst | 2 -- news/9246.bugfix.rst | 2 -- news/9249.feature.rst | 1 - news/9269.doc.rst | 1 - news/packaging.vendor.rst | 1 - src/pip/__init__.py | 2 +- 11 files changed, 36 insertions(+), 16 deletions(-) delete mode 100644 news/9117.bugfix.rst delete mode 100644 news/9185.feature.rst delete mode 100644 news/9191.bugfix.rst delete mode 100644 news/9203.bugfix.rst delete mode 100644 news/9232.bugfix.rst delete mode 100644 news/9246.bugfix.rst delete mode 100644 news/9249.feature.rst delete mode 100644 news/9269.doc.rst delete mode 100644 news/packaging.vendor.rst diff --git a/NEWS.rst b/NEWS.rst index 71071a4c084..80ad25ada43 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -9,6 +9,41 @@ .. towncrier release notes start +20.3.2 (2020-12-15) +=================== + +Features +-------- + +- New resolver: Resolve direct and pinned (``==`` or ``===``) requirements first + to improve resolver performance. (`#9185 <https://github.com/pypa/pip/issues/9185>`_) +- Add a mechanism to delay resolving certain packages, and use it for setuptools. (`#9249 <https://github.com/pypa/pip/issues/9249>`_) + +Bug Fixes +--------- + +- New resolver: The "Requirement already satisfied" log is not printed only once + for each package during resolution. (`#9117 <https://github.com/pypa/pip/issues/9117>`_) +- Fix crash when logic for redacting authentication information from URLs + in ``--help`` is given a list of strings, instead of a single string. (`#9191 <https://github.com/pypa/pip/issues/9191>`_) +- New resolver: Correctly implement PEP 592. Do not return yanked versions from + an index, unless the version range can only be satisfied by yanked candidates. (`#9203 <https://github.com/pypa/pip/issues/9203>`_) +- New resolver: Make constraints also apply to package variants with extras, so + the resolver correctly avoids backtracking on them. (`#9232 <https://github.com/pypa/pip/issues/9232>`_) +- New resolver: Discard a candidate if it fails to provide metadata from source, + or if the provided metadata is inconsistent, instead of quitting outright. (`#9246 <https://github.com/pypa/pip/issues/9246>`_) + +Vendored Libraries +------------------ + +- Update vendoring to 20.8 + +Improved Documentation +---------------------- + +- Update documentation to reflect that pip still uses legacy resolver by default in Python 2 environments. (`#9269 <https://github.com/pypa/pip/issues/9269>`_) + + 20.3.1 (2020-12-03) =================== diff --git a/news/9117.bugfix.rst b/news/9117.bugfix.rst deleted file mode 100644 index 7991dac7d01..00000000000 --- a/news/9117.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -New resolver: The "Requirement already satisfied" log is not printed only once -for each package during resolution. diff --git a/news/9185.feature.rst b/news/9185.feature.rst deleted file mode 100644 index a9d9ae7187c..00000000000 --- a/news/9185.feature.rst +++ /dev/null @@ -1,2 +0,0 @@ -New resolver: Resolve direct and pinned (``==`` or ``===``) requirements first -to improve resolver performance. diff --git a/news/9191.bugfix.rst b/news/9191.bugfix.rst deleted file mode 100644 index e1c6d633de9..00000000000 --- a/news/9191.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fix crash when logic for redacting authentication information from URLs -in ``--help`` is given a list of strings, instead of a single string. diff --git a/news/9203.bugfix.rst b/news/9203.bugfix.rst deleted file mode 100644 index 29b39d66c3e..00000000000 --- a/news/9203.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -New resolver: Correctly implement PEP 592. Do not return yanked versions from -an index, unless the version range can only be satisfied by yanked candidates. diff --git a/news/9232.bugfix.rst b/news/9232.bugfix.rst deleted file mode 100644 index 2d50d1ce41d..00000000000 --- a/news/9232.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -New resolver: Make constraints also apply to package variants with extras, so -the resolver correctly avoids backtracking on them. diff --git a/news/9246.bugfix.rst b/news/9246.bugfix.rst deleted file mode 100644 index e7ebd398f3e..00000000000 --- a/news/9246.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -New resolver: Discard a candidate if it fails to provide metadata from source, -or if the provided metadata is inconsistent, instead of quitting outright. diff --git a/news/9249.feature.rst b/news/9249.feature.rst deleted file mode 100644 index 1c56b39ef5d..00000000000 --- a/news/9249.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Add a mechanism to delay resolving certain packages, and use it for setuptools. diff --git a/news/9269.doc.rst b/news/9269.doc.rst deleted file mode 100644 index f8ff14d9428..00000000000 --- a/news/9269.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Update documentation to reflect that pip still uses legacy resolver by default in Python 2 environments. diff --git a/news/packaging.vendor.rst b/news/packaging.vendor.rst deleted file mode 100644 index 24d7440e055..00000000000 --- a/news/packaging.vendor.rst +++ /dev/null @@ -1 +0,0 @@ -Update vendoring to 20.8 diff --git a/src/pip/__init__.py b/src/pip/__init__.py index ae0fe9a9f24..bea377a9899 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -4,7 +4,7 @@ from typing import List, Optional -__version__ = "21.0.dev0" +__version__ = "20.3.2" def main(args=None): From e647c61b5eab241404feb4ca1ed20a0dff9c4564 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Tue, 15 Dec 2020 01:45:06 +0000 Subject: [PATCH 2784/3170] Bump for development --- src/pip/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/__init__.py b/src/pip/__init__.py index bea377a9899..ae0fe9a9f24 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -4,7 +4,7 @@ from typing import List, Optional -__version__ = "20.3.2" +__version__ = "21.0.dev0" def main(args=None): From 145be2eaf2c2a3433c761a5c8a41e2de453e81b1 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 15 Dec 2020 17:18:12 +0800 Subject: [PATCH 2785/3170] Skip pip search tests unless explicitly requested --- setup.cfg | 1 + tests/conftest.py | 10 ++++++++++ tests/functional/test_search.py | 9 +++++++++ 3 files changed, 20 insertions(+) diff --git a/setup.cfg b/setup.cfg index 8d9edbcc069..84a959a32d4 100644 --- a/setup.cfg +++ b/setup.cfg @@ -61,6 +61,7 @@ markers = mercurial: VCS: Mercurial git: VCS: git yaml: yaml based tests + search: tests for 'pip search' [coverage:run] branch = True diff --git a/tests/conftest.py b/tests/conftest.py index 6f7bd1a6cfd..dc1fdb5140f 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -51,6 +51,12 @@ def pytest_addoption(parser): default=False, help="use venv for virtual environment creation", ) + parser.addoption( + "--run-search", + action="store_true", + default=False, + help="run 'pip search' tests", + ) def pytest_collection_modifyitems(config, items): @@ -58,6 +64,10 @@ def pytest_collection_modifyitems(config, items): if not hasattr(item, 'module'): # e.g.: DoctestTextfile continue + if (item.get_closest_marker('search') and + not config.getoption('--run-search')): + item.add_marker(pytest.mark.skip('pip search test skipped')) + if "CI" in os.environ: # Mark network tests as flaky if item.get_closest_marker('network') is not None: diff --git a/tests/functional/test_search.py b/tests/functional/test_search.py index 875e2e5d6df..b8bc6d51e2f 100644 --- a/tests/functional/test_search.py +++ b/tests/functional/test_search.py @@ -56,6 +56,7 @@ def test_pypi_xml_transformation(): @pytest.mark.network +@pytest.mark.search def test_basic_search(script): """ End to end test of search command. @@ -75,6 +76,7 @@ def test_basic_search(script): "https://github.com/pypa/warehouse/issues/3717 for more " "information."), ) +@pytest.mark.search def test_multiple_search(script): """ Test searching for multiple packages at once. @@ -88,6 +90,7 @@ def test_multiple_search(script): assert 'Tools for parsing and using INI-style files' in output.stdout +@pytest.mark.search def test_search_missing_argument(script): """ Test missing required argument for search @@ -97,6 +100,7 @@ def test_search_missing_argument(script): @pytest.mark.network +@pytest.mark.search def test_run_method_should_return_success_when_find_packages(): """ Test SearchCommand.run for found package @@ -110,6 +114,7 @@ def test_run_method_should_return_success_when_find_packages(): @pytest.mark.network +@pytest.mark.search def test_run_method_should_return_no_matches_found_when_does_not_find_pkgs(): """ Test SearchCommand.run for no matches @@ -123,6 +128,7 @@ def test_run_method_should_return_no_matches_found_when_does_not_find_pkgs(): @pytest.mark.network +@pytest.mark.search def test_search_should_exit_status_code_zero_when_find_packages(script): """ Test search exit status code for package found @@ -132,6 +138,7 @@ def test_search_should_exit_status_code_zero_when_find_packages(script): @pytest.mark.network +@pytest.mark.search def test_search_exit_status_code_when_finds_no_package(script): """ Test search exit status code for no matches @@ -140,6 +147,7 @@ def test_search_exit_status_code_when_finds_no_package(script): assert result.returncode == NO_MATCHES_FOUND, result.returncode +@pytest.mark.search def test_latest_prerelease_install_message(caplog, monkeypatch): """ Test documentation for installing pre-release packages is displayed @@ -168,6 +176,7 @@ def test_latest_prerelease_install_message(caplog, monkeypatch): assert get_dist.calls == [pretend.call('ni')] +@pytest.mark.search def test_search_print_results_should_contain_latest_versions(caplog): """ Test that printed search results contain the latest package versions From 7165ab8cb9a0d6297b5a0dcd7d9a84350d0a0b3b Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Date: Tue, 15 Dec 2020 09:50:35 +0000 Subject: [PATCH 2786/3170] Revert "Skip candidate not providing valid metadata" --- src/pip/_internal/exceptions.py | 15 ------ .../resolution/resolvelib/candidates.py | 23 +++++--- .../resolution/resolvelib/factory.py | 52 +++---------------- .../resolution/resolvelib/requirements.py | 41 --------------- src/pip/_internal/utils/subprocess.py | 8 ++- tests/functional/test_new_resolver.py | 19 ------- tests/unit/test_utils_subprocess.py | 8 +-- 7 files changed, 35 insertions(+), 131 deletions(-) diff --git a/src/pip/_internal/exceptions.py b/src/pip/_internal/exceptions.py index 3f2659e8792..56482caf77b 100644 --- a/src/pip/_internal/exceptions.py +++ b/src/pip/_internal/exceptions.py @@ -151,21 +151,6 @@ def __str__(self): ) -class InstallationSubprocessError(InstallationError): - """A subprocess call failed during installation.""" - def __init__(self, returncode, description): - # type: (int, str) -> None - self.returncode = returncode - self.description = description - - def __str__(self): - # type: () -> str - return ( - "Command errored out with exit status {}: {} " - "Check the logs for full command output." - ).format(self.returncode, self.description) - - class HashErrors(InstallationError): """Multiple HashError instances rolled into one for reporting""" diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 83b6c98ab6a..cd1f188706f 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -141,7 +141,7 @@ def __init__( self._ireq = ireq self._name = name self._version = version - self.dist = self._prepare() + self._dist = None # type: Optional[Distribution] def __str__(self): # type: () -> str @@ -209,6 +209,8 @@ def _prepare_distribution(self): def _check_metadata_consistency(self, dist): # type: (Distribution) -> None """Check for consistency of project name and version of dist.""" + # TODO: (Longer term) Rather than abort, reject this candidate + # and backtrack. This would need resolvelib support. name = canonicalize_name(dist.project_name) if self._name is not None and self._name != name: raise MetadataInconsistent(self._ireq, "name", dist.project_name) @@ -217,17 +219,25 @@ def _check_metadata_consistency(self, dist): raise MetadataInconsistent(self._ireq, "version", dist.version) def _prepare(self): - # type: () -> Distribution + # type: () -> None + if self._dist is not None: + return try: dist = self._prepare_distribution() except HashError as e: - # Provide HashError the underlying ireq that caused it. This - # provides context for the resulting error message to show the - # offending line to the user. e.req = self._ireq raise + + assert dist is not None, "Distribution already installed" self._check_metadata_consistency(dist) - return dist + self._dist = dist + + @property + def dist(self): + # type: () -> Distribution + if self._dist is None: + self._prepare() + return self._dist def _get_requires_python_dependency(self): # type: () -> Optional[Requirement] @@ -251,6 +261,7 @@ def iter_dependencies(self, with_requires): def get_install_requirement(self): # type: () -> Optional[InstallRequirement] + self._prepare() return self._ireq diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 35345c5f0a1..b4c7bf11351 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -5,8 +5,6 @@ from pip._internal.exceptions import ( DistributionNotFound, InstallationError, - InstallationSubprocessError, - MetadataInconsistent, UnsupportedPythonVersion, UnsupportedWheel, ) @@ -35,7 +33,6 @@ ExplicitRequirement, RequiresPythonRequirement, SpecifierRequirement, - UnsatisfiableRequirement, ) if MYPY_CHECK_RUNNING: @@ -97,7 +94,6 @@ def __init__( self._force_reinstall = force_reinstall self._ignore_requires_python = ignore_requires_python - self._build_failures = {} # type: Cache[InstallationError] self._link_candidate_cache = {} # type: Cache[LinkCandidate] self._editable_candidate_cache = {} # type: Cache[EditableCandidate] self._installed_candidate_cache = { @@ -140,40 +136,21 @@ def _make_candidate_from_link( name, # type: Optional[str] version, # type: Optional[_BaseVersion] ): - # type: (...) -> Optional[Candidate] + # type: (...) -> Candidate # TODO: Check already installed candidate, and use it if the link and # editable flag match. - - if link in self._build_failures: - # We already tried this candidate before, and it does not build. - # Don't bother trying again. - return None - if template.editable: if link not in self._editable_candidate_cache: - try: - self._editable_candidate_cache[link] = EditableCandidate( - link, template, factory=self, - name=name, version=version, - ) - except (InstallationSubprocessError, MetadataInconsistent) as e: - logger.warning("Discarding %s. %s", link, e) - self._build_failures[link] = e - return None + self._editable_candidate_cache[link] = EditableCandidate( + link, template, factory=self, name=name, version=version, + ) base = self._editable_candidate_cache[link] # type: BaseCandidate else: if link not in self._link_candidate_cache: - try: - self._link_candidate_cache[link] = LinkCandidate( - link, template, factory=self, - name=name, version=version, - ) - except (InstallationSubprocessError, MetadataInconsistent) as e: - logger.warning("Discarding %s. %s", link, e) - self._build_failures[link] = e - return None + self._link_candidate_cache[link] = LinkCandidate( + link, template, factory=self, name=name, version=version, + ) base = self._link_candidate_cache[link] - if extras: return ExtrasCandidate(base, extras) return base @@ -233,16 +210,13 @@ def iter_index_candidates(): for ican in reversed(icans): if not all_yanked and ican.link.is_yanked: continue - candidate = self._make_candidate_from_link( + yield self._make_candidate_from_link( link=ican.link, extras=extras, template=template, name=name, version=ican.version, ) - if candidate is None: - continue - yield candidate return FoundCandidates( iter_index_candidates, @@ -306,16 +280,6 @@ def make_requirement_from_install_req(self, ireq, requested_extras): name=canonicalize_name(ireq.name) if ireq.name else None, version=None, ) - if cand is None: - # There's no way we can satisfy a URL requirement if the underlying - # candidate fails to build. An unnamed URL must be user-supplied, so - # we fail eagerly. If the URL is named, an unsatisfiable requirement - # can make the resolver do the right thing, either backtrack (and - # maybe find some other requirement that's buildable) or raise a - # ResolutionImpossible eventually. - if not ireq.name: - raise self._build_failures[ireq.link] - return UnsatisfiableRequirement(canonicalize_name(ireq.name)) return self.make_requirement_from_candidate(cand) def make_requirement_from_candidate(self, candidate): diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py index 1229f353750..d926d0a0656 100644 --- a/src/pip/_internal/resolution/resolvelib/requirements.py +++ b/src/pip/_internal/resolution/resolvelib/requirements.py @@ -158,44 +158,3 @@ def is_satisfied_by(self, candidate): # already implements the prerelease logic, and would have filtered out # prerelease candidates if the user does not expect them. return self.specifier.contains(candidate.version, prereleases=True) - - -class UnsatisfiableRequirement(Requirement): - """A requirement that cannot be satisfied. - """ - def __init__(self, name): - # type: (str) -> None - self._name = name - - def __str__(self): - # type: () -> str - return "{} (unavailable)".format(self._name) - - def __repr__(self): - # type: () -> str - return "{class_name}({name!r})".format( - class_name=self.__class__.__name__, - name=str(self._name), - ) - - @property - def project_name(self): - # type: () -> str - return self._name - - @property - def name(self): - # type: () -> str - return self._name - - def format_for_error(self): - # type: () -> str - return str(self) - - def get_candidate_lookup(self): - # type: () -> CandidateLookup - return None, None - - def is_satisfied_by(self, candidate): - # type: (Candidate) -> bool - return False diff --git a/src/pip/_internal/utils/subprocess.py b/src/pip/_internal/utils/subprocess.py index 325897c8739..605e711e603 100644 --- a/src/pip/_internal/utils/subprocess.py +++ b/src/pip/_internal/utils/subprocess.py @@ -7,7 +7,7 @@ from pip._vendor.six.moves import shlex_quote from pip._internal.cli.spinners import SpinnerInterface, open_spinner -from pip._internal.exceptions import InstallationSubprocessError +from pip._internal.exceptions import InstallationError from pip._internal.utils.compat import console_to_str, str_to_display from pip._internal.utils.logging import subprocess_logger from pip._internal.utils.misc import HiddenText, path_to_display @@ -233,7 +233,11 @@ def call_subprocess( exit_status=proc.returncode, ) subprocess_logger.error(msg) - raise InstallationSubprocessError(proc.returncode, command_desc) + exc_msg = ( + 'Command errored out with exit status {}: {} ' + 'Check the logs for full command output.' + ).format(proc.returncode, command_desc) + raise InstallationError(exc_msg) elif on_returncode == 'warn': subprocess_logger.warning( 'Command "%s" had error code %s in %s', diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 00d82fb95ee..b730b3cbdf9 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -1218,22 +1218,3 @@ def test_new_resolver_does_not_reinstall_when_from_a_local_index(script): assert "Installing collected packages: simple" not in result.stdout, str(result) assert "Requirement already satisfied: simple" in result.stdout, str(result) assert_installed(script, simple="0.1.0") - - -def test_new_resolver_skip_inconsistent_metadata(script): - create_basic_wheel_for_package(script, "A", "1") - - a_2 = create_basic_wheel_for_package(script, "A", "2") - a_2.rename(a_2.parent.joinpath("a-3-py2.py3-none-any.whl")) - - result = script.pip( - "install", - "--no-cache-dir", "--no-index", - "--find-links", script.scratch_path, - "--verbose", - "A", - allow_stderr_warning=True, - ) - - assert " different version in metadata: '2'" in result.stderr, str(result) - assert_installed(script, a="1") diff --git a/tests/unit/test_utils_subprocess.py b/tests/unit/test_utils_subprocess.py index 1a031065114..b0de2bf578d 100644 --- a/tests/unit/test_utils_subprocess.py +++ b/tests/unit/test_utils_subprocess.py @@ -7,7 +7,7 @@ import pytest from pip._internal.cli.spinners import SpinnerInterface -from pip._internal.exceptions import InstallationSubprocessError +from pip._internal.exceptions import InstallationError from pip._internal.utils.misc import hide_value from pip._internal.utils.subprocess import ( call_subprocess, @@ -276,7 +276,7 @@ def test_info_logging__subprocess_error(self, capfd, caplog): command = 'print("Hello"); print("world"); exit("fail")' args, spinner = self.prepare_call(caplog, log_level, command=command) - with pytest.raises(InstallationSubprocessError) as exc: + with pytest.raises(InstallationError) as exc: call_subprocess(args, spinner=spinner) result = None exc_message = str(exc.value) @@ -360,7 +360,7 @@ def test_info_logging_with_show_stdout_true(self, capfd, caplog): # log level is only WARNING. (0, True, None, WARNING, (None, 'done', 2)), # Test a non-zero exit status. - (3, False, None, INFO, (InstallationSubprocessError, 'error', 2)), + (3, False, None, INFO, (InstallationError, 'error', 2)), # Test a non-zero exit status also in extra_ok_returncodes. (3, False, (3, ), INFO, (None, 'done', 2)), ]) @@ -396,7 +396,7 @@ def test_spinner_finish( assert spinner.spin_count == expected_spin_count def test_closes_stdin(self): - with pytest.raises(InstallationSubprocessError): + with pytest.raises(InstallationError): call_subprocess( [sys.executable, '-c', 'input()'], show_stdout=True, From 95c3ae32a3e7b15ef76d1b94b84d3c64715e96ef Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Tue, 15 Dec 2020 14:15:00 +0000 Subject: [PATCH 2787/3170] :newspaper: --- news/9264.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/9264.bugfix.rst diff --git a/news/9264.bugfix.rst b/news/9264.bugfix.rst new file mode 100644 index 00000000000..0178ab197f0 --- /dev/null +++ b/news/9264.bugfix.rst @@ -0,0 +1 @@ +Revert "Skip candidate not providing valid metadata", as that caused pip to be overeager about downloading from the package index. From fe1fd0a0be46e22d4528cf298d6db8cb75fcc078 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Tue, 15 Dec 2020 15:21:16 +0000 Subject: [PATCH 2788/3170] Bump for development --- src/pip/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/__init__.py b/src/pip/__init__.py index 41b291e61a0..ae0fe9a9f24 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -4,7 +4,7 @@ from typing import List, Optional -__version__ = "20.3.3" +__version__ = "21.0.dev0" def main(args=None): From a387de10d81ca0688c7cd267be6054efeca89e94 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Tue, 15 Dec 2020 15:21:16 +0000 Subject: [PATCH 2789/3170] Bump for release --- NEWS.rst | 9 +++++++++ news/9264.bugfix.rst | 1 - src/pip/__init__.py | 2 +- 3 files changed, 10 insertions(+), 2 deletions(-) delete mode 100644 news/9264.bugfix.rst diff --git a/NEWS.rst b/NEWS.rst index 80ad25ada43..a082cddf314 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -9,6 +9,15 @@ .. towncrier release notes start +20.3.3 (2020-12-15) +=================== + +Bug Fixes +--------- + +- Revert "Skip candidate not providing valid metadata", as that caused pip to be overeager about downloading from the package index. (`#9264 <https://github.com/pypa/pip/issues/9264>`_) + + 20.3.2 (2020-12-15) =================== diff --git a/news/9264.bugfix.rst b/news/9264.bugfix.rst deleted file mode 100644 index 0178ab197f0..00000000000 --- a/news/9264.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Revert "Skip candidate not providing valid metadata", as that caused pip to be overeager about downloading from the package index. diff --git a/src/pip/__init__.py b/src/pip/__init__.py index ae0fe9a9f24..41b291e61a0 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -4,7 +4,7 @@ from typing import List, Optional -__version__ = "21.0.dev0" +__version__ = "20.3.3" def main(args=None): From 09e8fa96c7bf821916e9a21135359d1507fa5b68 Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Mon, 14 Dec 2020 14:24:42 -0800 Subject: [PATCH 2790/3170] Distribute and install py.typed to provide type information Complies with PEP 561: https://www.python.org/dev/peps/pep-0561/#packaging-type-information By distributing and installing the py.typed file, mypy will use pip's type information when imported into other projects. For example, the pip-tools project can use pip's types and mypy to help verify correctness. mypy docs: https://mypy.readthedocs.io/en/stable/installed_packages.html#making-pep-561-compatible-packages --- setup.py | 1 + src/pip/py.typed | 4 ++++ 2 files changed, 5 insertions(+) create mode 100644 src/pip/py.typed diff --git a/setup.py b/setup.py index 2601d8bd912..3c0d08c3c63 100644 --- a/setup.py +++ b/setup.py @@ -68,6 +68,7 @@ def get_version(rel_path): exclude=["contrib", "docs", "tests*", "tasks"], ), package_data={ + "pip": ["py.typed"], "pip._vendor": ["vendor.txt"], "pip._vendor.certifi": ["*.pem"], "pip._vendor.requests": ["*.pem"], diff --git a/src/pip/py.typed b/src/pip/py.typed new file mode 100644 index 00000000000..0b44fd9b5c7 --- /dev/null +++ b/src/pip/py.typed @@ -0,0 +1,4 @@ +pip is a command line program. While it is implemented in Python, and so is +available for import, you must not use pip's internal APIs in this way. Typing +information is provided as a convenience only and is not a gaurantee. Expect +unannounced changes to the API and types in releases. From f8b03eefe20b3102e23b8934bbfda87903109883 Mon Sep 17 00:00:00 2001 From: Nikita Chepanov <nchepanov@bloomberg.net> Date: Tue, 15 Dec 2020 16:57:55 -0500 Subject: [PATCH 2791/3170] Add `--ignore-requires-python` support to pip download --- news/1884.feature.rst | 1 + src/pip/_internal/commands/download.py | 3 +++ tests/functional/test_download.py | 28 ++++++++++++++++++++++++++ 3 files changed, 32 insertions(+) create mode 100644 news/1884.feature.rst diff --git a/news/1884.feature.rst b/news/1884.feature.rst new file mode 100644 index 00000000000..4b0b4180c35 --- /dev/null +++ b/news/1884.feature.rst @@ -0,0 +1 @@ +Add ``--ignore-requires-python`` support to pip download. diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index 7405870aefc..7509397240b 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -56,6 +56,7 @@ def add_options(self): self.cmd_opts.add_option(cmdoptions.no_build_isolation()) self.cmd_opts.add_option(cmdoptions.use_pep517()) self.cmd_opts.add_option(cmdoptions.no_use_pep517()) + self.cmd_opts.add_option(cmdoptions.ignore_requires_python()) self.cmd_opts.add_option( '-d', '--dest', '--destination-dir', '--destination-directory', @@ -96,6 +97,7 @@ def run(self, options, args): options=options, session=session, target_python=target_python, + ignore_requires_python=options.ignore_requires_python, ) req_tracker = self.enter_context(get_requirement_tracker()) @@ -122,6 +124,7 @@ def run(self, options, args): preparer=preparer, finder=finder, options=options, + ignore_requires_python=options.ignore_requires_python, py_version_info=options.python_version, ) diff --git a/tests/functional/test_download.py b/tests/functional/test_download.py index 8a816b63b44..24bc4ddbcc9 100644 --- a/tests/functional/test_download.py +++ b/tests/functional/test_download.py @@ -530,6 +530,34 @@ def make_args(python_version): script.pip(*args) # no exception +def test_download_ignore_requires_python_dont_fail_with_wrong_python( + script, + with_wheel, +): + """ + Test that --ignore-requires-python ignores Requires-Python check. + """ + wheel_path = make_wheel_with_python_requires( + script, + "mypackage", + python_requires="==999", + ) + wheel_dir = os.path.dirname(wheel_path) + + result = script.pip( + "download", + "--ignore-requires-python", + "--no-index", + "--find-links", + wheel_dir, + "--only-binary=:all:", + "--dest", + ".", + "mypackage==1.0", + ) + result.did_create(Path('scratch') / 'mypackage-1.0-py2.py3-none-any.whl') + + def test_download_specify_abi(script, data): """ Test using "pip download --abi" to download a .whl archive From 77fa5dfb9ec2c48e7d6396210a77dd7efaa96856 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Fri, 18 Dec 2020 17:48:38 +0000 Subject: [PATCH 2792/3170] Present a nicer error in pip search --- src/pip/_internal/commands/search.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/commands/search.py b/src/pip/_internal/commands/search.py index 146d653e55f..185495e7688 100644 --- a/src/pip/_internal/commands/search.py +++ b/src/pip/_internal/commands/search.py @@ -79,7 +79,14 @@ def search(self, query, options): transport = PipXmlrpcTransport(index_url, session) pypi = xmlrpc_client.ServerProxy(index_url, transport) - hits = pypi.search({'name': query, 'summary': query}, 'or') + try: + hits = pypi.search({'name': query, 'summary': query}, 'or') + except xmlrpc_client.Fault as fault: + message = "XMLRPC request failed [code: {code}]\n{string}".format( + code=fault.faultCode, + string=fault.faultString, + ) + raise CommandError(message) return hits From 7c775c86df34bffd3b56df58847ca87f8bd5778f Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Mon, 30 Nov 2020 17:55:13 +0000 Subject: [PATCH 2793/3170] Update python_requires in setup.py I am really happy to make this nice and short. --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index 2601d8bd912..fe751c28194 100644 --- a/setup.py +++ b/setup.py @@ -85,5 +85,5 @@ def get_version(rel_path): }, zip_safe=False, - python_requires='>=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*', + python_requires='>=3.6', ) From aaae3c5c3291f0feb73825685ac4e2005ec2d5e2 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Mon, 30 Nov 2020 19:36:17 +0000 Subject: [PATCH 2794/3170] Update classifiers to drop Python 2.5,3.5 --- setup.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/setup.py b/setup.py index fe751c28194..0c2808e9218 100644 --- a/setup.py +++ b/setup.py @@ -40,10 +40,7 @@ def get_version(rel_path): "License :: OSI Approved :: MIT License", "Topic :: Software Development :: Build Tools", "Programming Language :: Python", - "Programming Language :: Python :: 2", - "Programming Language :: Python :: 2.7", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3.5", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", From 872fd658c4d8f4676d491d7986ba1a78a201f701 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sat, 19 Dec 2020 16:09:55 +0000 Subject: [PATCH 2795/3170] Add `Python 3 :: Only` classifier --- setup.py | 1 + 1 file changed, 1 insertion(+) diff --git a/setup.py b/setup.py index 0c2808e9218..b7d0e8f51d7 100644 --- a/setup.py +++ b/setup.py @@ -41,6 +41,7 @@ def get_version(rel_path): "Topic :: Software Development :: Build Tools", "Programming Language :: Python", "Programming Language :: Python :: 3", + "Programming Language :: Python :: 3 :: Only" "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", From 527550d5a7f1e967498f6eeca83a13b52cbcd136 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Mon, 30 Nov 2020 20:55:54 +0000 Subject: [PATCH 2796/3170] Drop Python 2.7 and 3.5 from CI --- .azure-pipelines/jobs/test-windows.yml | 16 ++-------------- .azure-pipelines/jobs/test.yml | 10 ++-------- .github/workflows/macos.yml | 2 +- .travis.yml | 4 ---- noxfile.py | 2 +- 5 files changed, 6 insertions(+), 28 deletions(-) diff --git a/.azure-pipelines/jobs/test-windows.yml b/.azure-pipelines/jobs/test-windows.yml index 6053b0eb005..99cd8a836bd 100644 --- a/.azure-pipelines/jobs/test-windows.yml +++ b/.azure-pipelines/jobs/test-windows.yml @@ -9,14 +9,8 @@ jobs: vmImage: ${{ parameters.vmImage }} strategy: matrix: - "2.7-x86": - python.version: '2.7' - python.architecture: x86 - "2.7": # because Python 2! - python.version: '2.7' - python.architecture: x64 - "3.5": # lowest Py3 version - python.version: '3.5' + "3.6": # lowest Python version + python.version: '3.6' python.architecture: x64 "3.8": # current python.version: '3.8' @@ -38,16 +32,10 @@ jobs: vmImage: ${{ parameters.vmImage }} strategy: matrix: - "3.6": - python.version: '3.6' - python.architecture: x64 "3.7": python.version: '3.7' python.architecture: x64 # This is for Windows, so test x86 builds - "3.5-x86": - python.version: '3.5' - python.architecture: x86 "3.6-x86": python.version: '3.6' python.architecture: x86 diff --git a/.azure-pipelines/jobs/test.yml b/.azure-pipelines/jobs/test.yml index 274e075a69b..a3a0ef80b6d 100644 --- a/.azure-pipelines/jobs/test.yml +++ b/.azure-pipelines/jobs/test.yml @@ -9,8 +9,8 @@ jobs: vmImage: ${{ parameters.vmImage }} strategy: matrix: - "2.7": - python.version: '2.7' + "3.6": # lowest Python version + python.version: '3.6' python.architecture: x64 "3.8": python.version: '3.8' @@ -29,12 +29,6 @@ jobs: vmImage: ${{ parameters.vmImage }} strategy: matrix: - "3.5": - python.version: '3.5' - python.architecture: x64 - "3.6": - python.version: '3.6' - python.architecture: x64 "3.7": python.version: '3.7' python.architecture: x64 diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index 5d7b9acab87..c7afa8d35ea 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -91,7 +91,7 @@ jobs: strategy: fail-fast: false matrix: - python: [2.7, 3.5, 3.6, 3.7, 3.8, 3.9] + python: [3.6, 3.7, 3.8, 3.9] steps: # Caches diff --git a/.travis.yml b/.travis.yml index 165d2ef1540..9e685d1ecec 100644 --- a/.travis.yml +++ b/.travis.yml @@ -26,10 +26,6 @@ jobs: python: pypy3.5-7.0.0 - env: GROUP=2 python: pypy3.5-7.0.0 - - env: GROUP=1 - python: pypy2.7-7.1.1 - - env: GROUP=2 - python: pypy2.7-7.1.1 before_install: tools/travis/setup.sh install: travis_retry tools/travis/install.sh diff --git a/noxfile.py b/noxfile.py index 29e3959e463..b94cfe86fe7 100644 --- a/noxfile.py +++ b/noxfile.py @@ -70,7 +70,7 @@ def should_update_common_wheels(): # completely to nox for all our automation. Contributors should prefer using # `tox -e ...` until this note is removed. # ----------------------------------------------------------------------------- -@nox.session(python=["2.7", "3.5", "3.6", "3.7", "3.8", "3.9", "pypy", "pypy3"]) +@nox.session(python=["3.6", "3.7", "3.8", "3.9", "pypy3"]) def test(session): # Get the common wheels. if should_update_common_wheels(): From b2bcb2611ddbc3f51580cbb4be87b3275926e127 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sat, 19 Dec 2020 15:57:49 +0000 Subject: [PATCH 2797/3170] Bump to PyPy 3.6 --- .travis.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.travis.yml b/.travis.yml index 9e685d1ecec..6610b6eb019 100644 --- a/.travis.yml +++ b/.travis.yml @@ -23,9 +23,9 @@ jobs: # PyPy - stage: secondary env: GROUP=1 - python: pypy3.5-7.0.0 + python: pypy3.6-7.3.1 - env: GROUP=2 - python: pypy3.5-7.0.0 + python: pypy3.6-7.3.1 before_install: tools/travis/setup.sh install: travis_retry tools/travis/install.sh From 0befae128a5da8f6d400b83d6df1c05533f0f31e Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Mon, 30 Nov 2020 20:56:41 +0000 Subject: [PATCH 2798/3170] Drop CI info about Python 2.7/3.5 --- docs/html/development/ci.rst | 38 ------------------------------------ docs/html/installing.rst | 2 +- 2 files changed, 1 insertion(+), 39 deletions(-) diff --git a/docs/html/development/ci.rst b/docs/html/development/ci.rst index 5befb316a4d..5c33231b1b2 100644 --- a/docs/html/development/ci.rst +++ b/docs/html/development/ci.rst @@ -17,12 +17,9 @@ Supported interpreters pip support a variety of Python interpreters: -- CPython 2.7 -- CPython 3.5 - CPython 3.6 - CPython 3.7 - CPython 3.8 -- Latest PyPy - Latest PyPy3 on different operating systems: @@ -95,9 +92,6 @@ Actual testing +------------------------------+---------------+-----------------+ | **interpreter** | **unit** | **integration** | +-----------+----------+-------+---------------+-----------------+ -| | | CP2.7 | Azure | Azure | -| | +-------+---------------+-----------------+ -| | | CP3.5 | Azure | | | | +-------+---------------+-----------------+ | | | CP3.6 | Azure | | | | +-------+---------------+-----------------+ @@ -105,77 +99,45 @@ Actual testing | | +-------+---------------+-----------------+ | | | CP3.8 | Azure | | | | +-------+---------------+-----------------+ -| | | PyPy | | | -| | +-------+---------------+-----------------+ | | | PyPy3 | | | | Windows +----------+-------+---------------+-----------------+ -| | | CP2.7 | Azure | Azure | -| | +-------+---------------+-----------------+ -| | | CP3.5 | Azure | Azure | -| | +-------+---------------+-----------------+ | | | CP3.6 | Azure | | | | +-------+---------------+-----------------+ | | x64 | CP3.7 | Azure | | | | +-------+---------------+-----------------+ | | | CP3.8 | Azure | Azure | | | +-------+---------------+-----------------+ -| | | PyPy | | | -| | +-------+---------------+-----------------+ | | | PyPy3 | | | +-----------+----------+-------+---------------+-----------------+ -| | | CP2.7 | | | -| | +-------+---------------+-----------------+ -| | | CP3.5 | | | -| | +-------+---------------+-----------------+ | | | CP3.6 | | | | | +-------+---------------+-----------------+ | | x86 | CP3.7 | | | | | +-------+---------------+-----------------+ | | | CP3.8 | | | | | +-------+---------------+-----------------+ -| | | PyPy | | | -| | +-------+---------------+-----------------+ | | | PyPy3 | | | | Linux +----------+-------+---------------+-----------------+ -| | | CP2.7 | Azure | Azure | -| | +-------+---------------+-----------------+ -| | | CP3.5 | Azure | Azure | -| | +-------+---------------+-----------------+ | | | CP3.6 | Azure | Azure | | | +-------+---------------+-----------------+ | | x64 | CP3.7 | Azure | Azure | | | +-------+---------------+-----------------+ | | | CP3.8 | Azure | Azure | | | +-------+---------------+-----------------+ -| | | PyPy | Travis | Travis | -| | +-------+---------------+-----------------+ | | | PyPy3 | Travis | Travis | +-----------+----------+-------+---------------+-----------------+ -| | | CP2.7 | | | -| | +-------+---------------+-----------------+ -| | | CP3.5 | | | -| | +-------+---------------+-----------------+ | | | CP3.6 | | | | | +-------+---------------+-----------------+ | | x86 | CP3.7 | | | | | +-------+---------------+-----------------+ | | | CP3.8 | | | | | +-------+---------------+-----------------+ -| | | PyPy | | | -| | +-------+---------------+-----------------+ | | | PyPy3 | | | | MacOS +----------+-------+---------------+-----------------+ -| | | CP2.7 | Azure | Azure | -| | +-------+---------------+-----------------+ -| | | CP3.5 | Azure | Azure | -| | +-------+---------------+-----------------+ | | | CP3.6 | Azure | Azure | | | +-------+---------------+-----------------+ | | x64 | CP3.7 | Azure | Azure | | | +-------+---------------+-----------------+ | | | CP3.8 | Azure | Azure | | | +-------+---------------+-----------------+ -| | | PyPy | | | -| | +-------+---------------+-----------------+ | | | PyPy3 | | | +-----------+----------+-------+---------------+-----------------+ diff --git a/docs/html/installing.rst b/docs/html/installing.rst index a49aebea4a3..9e2c7051ef3 100644 --- a/docs/html/installing.rst +++ b/docs/html/installing.rst @@ -206,7 +206,7 @@ Upgrading pip Python and OS Compatibility =========================== -pip works with CPython versions 2.7, 3.5, 3.6, 3.7, 3.8 and also PyPy. +pip works with CPython versions 3.6, 3.7, 3.8 and also PyPy. This means pip works on the latest patch version of each of these minor versions. Previous patch versions are supported on a best effort approach. From 7281ed9d9867d7a94d3000c0156189cf844c5eed Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Mon, 30 Nov 2020 20:56:56 +0000 Subject: [PATCH 2799/3170] No longer print a deprecation warning on 2.7/3.5 These versions are no longer supported. --- src/pip/_internal/cli/base_command.py | 30 --------------------------- 1 file changed, 30 deletions(-) diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index 7f05efb85db..ce93552b3c1 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -6,7 +6,6 @@ import logging.config import optparse import os -import platform import sys import traceback @@ -139,35 +138,6 @@ def _main(self, args): user_log_file=options.log, ) - if ( - sys.version_info[:2] == (2, 7) and - not options.no_python_version_warning - ): - message = ( - "pip 21.0 will drop support for Python 2.7 in January 2021. " - "More details about Python 2 support in pip can be found at " - "https://pip.pypa.io/en/latest/development/release-process/#python-2-support" # noqa - ) - if platform.python_implementation() == "CPython": - message = ( - "Python 2.7 reached the end of its life on January " - "1st, 2020. Please upgrade your Python as Python 2.7 " - "is no longer maintained. " - ) + message - deprecated(message, replacement=None, gone_in="21.0") - - if ( - sys.version_info[:2] == (3, 5) and - not options.no_python_version_warning - ): - message = ( - "Python 3.5 reached the end of its life on September " - "13th, 2020. Please upgrade your Python as Python 3.5 " - "is no longer maintained. pip 21.0 will drop support " - "for Python 3.5 in January 2021." - ) - deprecated(message, replacement=None, gone_in="21.0") - # TODO: Try to get these passing down from the command? # without resorting to os.environ to hold these. # This also affects isolated builds and it should. From 79f1b593357ca9bf4219ddee4785b4aade5b4fcf Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Mon, 30 Nov 2020 20:57:40 +0000 Subject: [PATCH 2800/3170] We have no deprecated versions of Python now --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index dc1fdb5140f..78be52788a1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -496,7 +496,7 @@ def in_memory_pip(): @pytest.fixture(scope="session") def deprecated_python(): """Used to indicate whether pip deprecated this Python version""" - return sys.version_info[:2] in [(2, 7), (3, 5)] + return sys.version_info[:2] in [] @pytest.fixture(scope="session") From c0a7444de60c72725f5e92da3be88bcb1344c587 Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Sat, 19 Dec 2020 15:28:02 -0800 Subject: [PATCH 2801/3170] =?UTF-8?q?Update=20isort=20URL:=20timothycrosle?= =?UTF-8?q?y/isort=20=E2=86=92=20PyCQA/isort?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .pre-commit-config.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 838b1f24ebe..7cf771c3bc4 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -62,7 +62,7 @@ repos: ] exclude: tests/data -- repo: https://github.com/timothycrosley/isort +- repo: https://github.com/PyCQA/isort rev: 5.5.3 hooks: - id: isort From 4f4cf38c4642b03492db39b52a7631695f305391 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sun, 20 Dec 2020 12:03:00 +0000 Subject: [PATCH 2802/3170] Update passage on Python 2 support --- docs/html/development/release-process.rst | 13 +++---------- 1 file changed, 3 insertions(+), 10 deletions(-) diff --git a/docs/html/development/release-process.rst b/docs/html/development/release-process.rst index 91ce3e23379..17d11e7d1cf 100644 --- a/docs/html/development/release-process.rst +++ b/docs/html/development/release-process.rst @@ -70,16 +70,9 @@ their merits. Python 2 Support ---------------- -pip will continue to ensure that it runs on Python 2.7 after the `CPython 2.7 -EOL date`_. Support for Python 2.7 will be dropped, if bugs in Python 2.7 itself -make this necessary (which is unlikely) or in pip 21.0 (Jan 2021), whichever is -earlier. - -However, bugs reported with pip which only occur on Python 2.7 would likely not -be addressed directly by pip's maintainers. Pull Requests to fix Python 2.7 -only bugs will be considered, and merged (subject to normal review processes). -Note that there may be delays due to the lack of developer resources for -reviewing such pull requests. +pip 20.3 was the last version of pip that supported Python 2. Bugs reported +with pip which only occur on Python 2.7 will likely be closed as "won't fix" +issues by pip's maintainers. Python Support Policy --------------------- From c7a72060bac55df3be76f2e15bc5d3497de57dae Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sun, 20 Dec 2020 12:03:14 +0000 Subject: [PATCH 2803/3170] :newspaper: --- news/6148.removal.rst | 1 + news/9189.removal.rst | 1 + 2 files changed, 2 insertions(+) create mode 100644 news/6148.removal.rst create mode 100644 news/9189.removal.rst diff --git a/news/6148.removal.rst b/news/6148.removal.rst new file mode 100644 index 00000000000..cf6d85e70ba --- /dev/null +++ b/news/6148.removal.rst @@ -0,0 +1 @@ +Drop support for Python 2. diff --git a/news/9189.removal.rst b/news/9189.removal.rst new file mode 100644 index 00000000000..79928cbb15a --- /dev/null +++ b/news/9189.removal.rst @@ -0,0 +1 @@ +Drop support for Python 3.5. From e6d2bb234158f5c37d18999870ddda18b64e0a64 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Mon, 21 Dec 2020 19:10:19 +0000 Subject: [PATCH 2804/3170] :newspaper: --- news/9315.feature.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/9315.feature.rst diff --git a/news/9315.feature.rst b/news/9315.feature.rst new file mode 100644 index 00000000000..64d1f25338b --- /dev/null +++ b/news/9315.feature.rst @@ -0,0 +1 @@ +Improve presentation of XMLRPC errors in pip search. From ccbf085095addb7c55e341391cc899181040fc37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Mon, 21 Dec 2020 17:59:28 +0100 Subject: [PATCH 2805/3170] Improve local pre-commit experience --- .pre-commit-config-slow.yaml | 7 +++++++ .pre-commit-config.yaml | 5 ----- MANIFEST.in | 1 + noxfile.py | 3 +++ tox.ini | 1 + 5 files changed, 12 insertions(+), 5 deletions(-) create mode 100644 .pre-commit-config-slow.yaml diff --git a/.pre-commit-config-slow.yaml b/.pre-commit-config-slow.yaml new file mode 100644 index 00000000000..2179c665769 --- /dev/null +++ b/.pre-commit-config-slow.yaml @@ -0,0 +1,7 @@ +# Slow pre-commit checks we don't want to run locally with each commit. + +repos: +- repo: https://github.com/mgedmin/check-manifest + rev: '0.43' + hooks: + - id: check-manifest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 838b1f24ebe..4eadea08c47 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -97,8 +97,3 @@ repos: entry: NEWS fragment files must be named *.(process|removal|feature|bugfix|vendor|doc|trivial).rst exclude: ^news/(.gitignore|.*\.(process|removal|feature|bugfix|vendor|doc|trivial).rst) files: ^news/ - -- repo: https://github.com/mgedmin/check-manifest - rev: '0.43' - hooks: - - id: check-manifest diff --git a/MANIFEST.in b/MANIFEST.in index 24d4553785b..2cf636ce3f7 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -17,6 +17,7 @@ exclude .appveyor.yml exclude .travis.yml exclude .readthedocs.yml exclude .pre-commit-config.yaml +exclude .pre-commit-config-slow.yaml exclude tox.ini exclude noxfile.py diff --git a/noxfile.py b/noxfile.py index 29e3959e463..372defef513 100644 --- a/noxfile.py +++ b/noxfile.py @@ -149,6 +149,9 @@ def lint(session): args = ["--all-files", "--show-diff-on-failure"] session.run("pre-commit", "run", *args) + session.run( + "pre-commit", "run", "-c", ".pre-commit-config-slow.yaml", *args + ) @nox.session diff --git a/tox.ini b/tox.ini index 9c20759af3a..e458e374b50 100644 --- a/tox.ini +++ b/tox.ini @@ -64,6 +64,7 @@ commands_pre = deps = pre-commit commands = pre-commit run [] --all-files --show-diff-on-failure + pre-commit run [] -c .pre-commit-config-slow.yaml --all-files --show-diff-on-failure [testenv:vendoring] basepython = python3 From 817ee230516a0e2e11ceeabb736e59378129169b Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <hugovk@users.noreply.github.com> Date: Sun, 20 Dec 2020 21:58:50 +0200 Subject: [PATCH 2806/3170] Remove redundant Python 2.7 code --- .pre-commit-config.yaml | 4 -- docs/html/development/getting-started.rst | 2 +- docs/html/development/release-process.rst | 1 - docs/html/reference/pip_install.rst | 2 +- docs/html/reference/pip_uninstall.rst | 8 +-- news/8802.removal.rst | 1 + src/pip/_internal/cli/base_command.py | 4 +- src/pip/_internal/cli/parser.py | 5 +- src/pip/_internal/cli/progress_bars.py | 7 +-- src/pip/_internal/cli/req_command.py | 10 ---- src/pip/_internal/commands/list.py | 6 +- src/pip/_internal/configuration.py | 3 +- src/pip/_internal/distributions/base.py | 6 +- src/pip/_internal/exceptions.py | 13 +---- src/pip/_internal/index/collector.py | 4 +- src/pip/_internal/models/direct_url.py | 6 +- src/pip/_internal/models/index.py | 2 +- src/pip/_internal/models/link.py | 3 +- src/pip/_internal/models/search_scope.py | 2 +- src/pip/_internal/network/auth.py | 2 +- src/pip/_internal/network/lazy_wheel.py | 1 - src/pip/_internal/network/session.py | 2 +- src/pip/_internal/network/xmlrpc.py | 2 +- src/pip/_internal/operations/freeze.py | 3 +- src/pip/_internal/operations/install/wheel.py | 51 ++++++----------- src/pip/_internal/operations/prepare.py | 36 ++++-------- src/pip/_internal/pyproject.py | 5 -- src/pip/_internal/req/req_file.py | 8 +-- src/pip/_internal/req/req_install.py | 4 -- src/pip/_internal/req/req_uninstall.py | 7 ++- src/pip/_internal/utils/compat.py | 55 ++----------------- src/pip/_internal/utils/filesystem.py | 14 +---- src/pip/_internal/utils/hashes.py | 17 ++---- src/pip/_internal/utils/logging.py | 26 ++------- src/pip/_internal/utils/misc.py | 41 +++----------- src/pip/_internal/utils/parallel.py | 4 +- src/pip/_internal/utils/subprocess.py | 7 +-- src/pip/_internal/utils/urls.py | 5 +- src/pip/_internal/utils/wheel.py | 9 +-- src/pip/_internal/vcs/bazaar.py | 3 +- src/pip/_internal/vcs/git.py | 4 +- src/pip/_internal/vcs/mercurial.py | 3 +- src/pip/_internal/vcs/versioncontrol.py | 10 ++-- tests/conftest.py | 41 +++++--------- tests/functional/test_download.py | 3 +- tests/functional/test_install.py | 21 +------ tests/functional/test_install_config.py | 3 - tests/functional/test_install_index.py | 3 +- tests/functional/test_install_wheel.py | 7 +-- tests/functional/test_warning.py | 25 --------- tests/lib/__init__.py | 8 +-- tests/lib/local_repos.py | 3 +- tests/lib/path.py | 21 +++---- tests/lib/test_wheel.py | 6 +- tests/lib/wheel.py | 12 ++-- tests/unit/test_collector.py | 5 +- tests/unit/test_logging.py | 31 ++--------- tests/unit/test_req_file.py | 6 +- tests/unit/test_urls.py | 2 +- tests/unit/test_utils_parallel.py | 7 +-- tests/unit/test_utils_pkg_resources.py | 3 - tests/unit/test_utils_wheel.py | 2 - tests/unit/test_vcs_mercurial.py | 3 +- tests/unit/test_wheel.py | 4 +- tox.ini | 2 +- 65 files changed, 160 insertions(+), 466 deletions(-) create mode 100644 news/8802.removal.rst diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7cf771c3bc4..6c8a3c85932 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -74,10 +74,6 @@ repos: - id: mypy exclude: docs|tests args: ["--pretty"] - - id: mypy - name: mypy, for Python 2 - exclude: noxfile.py|tools/automation/release|docs|tests - args: ["--pretty", "-2"] - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.6.0 diff --git a/docs/html/development/getting-started.rst b/docs/html/development/getting-started.rst index 94ff37fc32c..1d625613b73 100644 --- a/docs/html/development/getting-started.rst +++ b/docs/html/development/getting-started.rst @@ -79,7 +79,7 @@ To run tests without parallelization, run: $ tox -e py36 The example above runs tests against Python 3.6. You can also use other -versions like ``py27`` and ``pypy3``. +versions like ``py39`` and ``pypy3``. ``tox`` has been configured to forward any additional arguments it is given to ``pytest``. This enables the use of pytest's `rich CLI`_. As an example, you diff --git a/docs/html/development/release-process.rst b/docs/html/development/release-process.rst index 17d11e7d1cf..a133e57f20c 100644 --- a/docs/html/development/release-process.rst +++ b/docs/html/development/release-process.rst @@ -173,4 +173,3 @@ order to create one of these the changes should already be merged into the .. _`get-pip repository`: https://github.com/pypa/get-pip .. _`psf-salt repository`: https://github.com/python/psf-salt .. _`CPython`: https://github.com/python/cpython -.. _`CPython 2.7 EOL date`: https://www.python.org/doc/sunset-python-2/ diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index 1a5507fdc0d..1b53513266d 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -291,7 +291,7 @@ Since version 6.0, pip also supports specifiers containing `environment markers :: - SomeProject ==5.4 ; python_version < '2.7' + SomeProject ==5.4 ; python_version < '3.8' SomeProject; sys_platform == 'win32' Since version 19.1, pip also supports `direct references diff --git a/docs/html/reference/pip_uninstall.rst b/docs/html/reference/pip_uninstall.rst index f1c69d09c3a..e6eeb5ebf6a 100644 --- a/docs/html/reference/pip_uninstall.rst +++ b/docs/html/reference/pip_uninstall.rst @@ -41,8 +41,8 @@ Examples $ python -m pip uninstall simplejson Uninstalling simplejson: - /home/me/env/lib/python2.7/site-packages/simplejson - /home/me/env/lib/python2.7/site-packages/simplejson-2.2.1-py2.7.egg-info + /home/me/env/lib/python3.9/site-packages/simplejson + /home/me/env/lib/python3.9/site-packages/simplejson-2.2.1-py3.9.egg-info Proceed (y/n)? y Successfully uninstalled simplejson @@ -52,7 +52,7 @@ Examples C:\> py -m pip uninstall simplejson Uninstalling simplejson: - /home/me/env/lib/python2.7/site-packages/simplejson - /home/me/env/lib/python2.7/site-packages/simplejson-2.2.1-py2.7.egg-info + /home/me/env/lib/python3.9/site-packages/simplejson + /home/me/env/lib/python3.9/site-packages/simplejson-2.2.1-py3.9.egg-info Proceed (y/n)? y Successfully uninstalled simplejson diff --git a/news/8802.removal.rst b/news/8802.removal.rst new file mode 100644 index 00000000000..79d8e508166 --- /dev/null +++ b/news/8802.removal.rst @@ -0,0 +1 @@ +Modernise the codebase after Python 2. diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index ce93552b3c1..190c4d86e6c 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -9,8 +9,6 @@ import sys import traceback -from pip._vendor.six import PY2 - from pip._internal.cli import cmdoptions from pip._internal.cli.command_context import CommandContextMixIn from pip._internal.cli.parser import ConfigOptionParser, UpdatingDefaultsHelpFormatter @@ -183,7 +181,7 @@ def _main(self, args): issue=8333, ) - if '2020-resolver' in options.features_enabled and not PY2: + if '2020-resolver' in options.features_enabled: logger.warning( "--use-feature=2020-resolver no longer has any effect, " "since it is now the default dependency resolver in pip. " diff --git a/src/pip/_internal/cli/parser.py b/src/pip/_internal/cli/parser.py index 7170bfd3841..5bacd47a1d3 100644 --- a/src/pip/_internal/cli/parser.py +++ b/src/pip/_internal/cli/parser.py @@ -12,7 +12,6 @@ from distutils.util import strtobool from pip._vendor.contextlib2 import suppress -from pip._vendor.six import string_types from pip._internal.cli.status_codes import UNKNOWN_ERROR from pip._internal.configuration import Configuration, ConfigurationError @@ -119,7 +118,7 @@ def expand_default(self, option): help_text = optparse.IndentedHelpFormatter.expand_default(self, option) if default_values and option.metavar == 'URL': - if isinstance(default_values, string_types): + if isinstance(default_values, str): default_values = [default_values] # If its not a list, we should abort and just return the help text @@ -275,7 +274,7 @@ def get_default_values(self): defaults = self._update_defaults(self.defaults.copy()) # ours for option in self._get_all_options(): default = defaults.get(option.dest) - if isinstance(default, string_types): + if isinstance(default, str): opt_str = option.get_opt_string() defaults[option.dest] = option.check_value(opt_str, default) return optparse.Values(defaults) diff --git a/src/pip/_internal/cli/progress_bars.py b/src/pip/_internal/cli/progress_bars.py index 69338552f13..4a2e5936159 100644 --- a/src/pip/_internal/cli/progress_bars.py +++ b/src/pip/_internal/cli/progress_bars.py @@ -4,7 +4,6 @@ import sys from signal import SIGINT, default_int_handler, signal -from pip._vendor import six from pip._vendor.progress.bar import Bar, FillingCirclesBar, IncrementalBar from pip._vendor.progress.spinner import Spinner @@ -36,8 +35,8 @@ def _select_progress_class(preferred, fallback): # Collect all of the possible characters we want to use with the preferred # bar. characters = [ - getattr(preferred, "empty_fill", six.text_type()), - getattr(preferred, "fill", six.text_type()), + getattr(preferred, "empty_fill", str()), + getattr(preferred, "fill", str()), ] characters += list(getattr(preferred, "phases", [])) @@ -45,7 +44,7 @@ def _select_progress_class(preferred, fallback): # of the given file, if this works then we'll assume that we can use the # fancier bar and if not we'll fall back to the plaintext bar. try: - six.text_type().join(characters).encode(encoding) + str().join(characters).encode(encoding) except UnicodeEncodeError: return fallback else: diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 008066ab1c4..2adbb74119d 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -9,8 +9,6 @@ import os from functools import partial -from pip._vendor.six import PY2 - from pip._internal.cli import cmdoptions from pip._internal.cli.base_command import Command from pip._internal.cli.command_context import CommandContextMixIn @@ -200,14 +198,6 @@ def __init__(self, *args, **kw): def determine_resolver_variant(options): # type: (Values) -> str """Determines which resolver should be used, based on the given options.""" - # We didn't want to change things for Python 2, since it's nearly done with - # and we're using performance improvements that only work on Python 3. - if PY2: - if '2020-resolver' in options.features_enabled: - return "2020-resolver" - else: - return "legacy" - if "legacy-resolver" in options.deprecated_features_enabled: return "legacy" diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index 27b15d70a52..3f29e48b4fe 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -3,8 +3,6 @@ import json import logging -from pip._vendor import six - from pip._internal.cli import cmdoptions from pip._internal.cli.req_command import IndexGroupCommand from pip._internal.cli.status_codes import SUCCESS @@ -315,13 +313,13 @@ def format_for_json(packages, options): for dist in packages: info = { 'name': dist.project_name, - 'version': six.text_type(dist.version), + 'version': str(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_version'] = str(dist.latest_version) info['latest_filetype'] = dist.latest_filetype data.append(info) return json.dumps(data) diff --git a/src/pip/_internal/configuration.py b/src/pip/_internal/configuration.py index 23614fd2bbe..9e835785562 100644 --- a/src/pip/_internal/configuration.py +++ b/src/pip/_internal/configuration.py @@ -11,13 +11,12 @@ A single word describing where the configuration key-value pair came from """ +import configparser import locale import logging import os import sys -from pip._vendor.six.moves import configparser - from pip._internal.exceptions import ( ConfigurationError, ConfigurationFileCouldNotBeLoaded, diff --git a/src/pip/_internal/distributions/base.py b/src/pip/_internal/distributions/base.py index 3a789f80433..dc7ae96aa04 100644 --- a/src/pip/_internal/distributions/base.py +++ b/src/pip/_internal/distributions/base.py @@ -1,7 +1,5 @@ import abc -from pip._vendor.six import add_metaclass - from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: @@ -13,8 +11,7 @@ from pip._internal.req import InstallRequirement -@add_metaclass(abc.ABCMeta) -class AbstractDistribution(object): +class AbstractDistribution(object, metaclass=abc.ABCMeta): """A base class for handling installable artifacts. The requirements for anything installable are as follows: @@ -29,7 +26,6 @@ class AbstractDistribution(object): - we must be able to create a Distribution object exposing the above metadata. """ - def __init__(self, req): # type: (InstallRequirement) -> None super(AbstractDistribution, self).__init__() diff --git a/src/pip/_internal/exceptions.py b/src/pip/_internal/exceptions.py index 56482caf77b..870205584a2 100644 --- a/src/pip/_internal/exceptions.py +++ b/src/pip/_internal/exceptions.py @@ -4,25 +4,18 @@ from itertools import chain, groupby, repeat -from pip._vendor.six import iteritems - from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: + import configparser + from hashlib import _Hash from typing import Any, Dict, List, Optional, Text from pip._vendor.pkg_resources import Distribution from pip._vendor.requests.models import Request, Response - from pip._vendor.six import PY3 - from pip._vendor.six.moves import configparser from pip._internal.req.req_install import InstallRequirement - if PY3: - from hashlib import _Hash - else: - from hashlib import _hash as _Hash - class PipError(Exception): """Base pip exception""" @@ -346,7 +339,7 @@ def hash_then_or(hash_name): return chain([hash_name], repeat(' or')) lines = [] # type: List[str] - for hash_name, expecteds in iteritems(self.allowed): + for hash_name, expecteds in self.allowed.items(): prefix = hash_then_or(hash_name) lines.extend((' Expected {} {}'.format(next(prefix), e)) for e in expecteds) diff --git a/src/pip/_internal/index/collector.py b/src/pip/_internal/index/collector.py index b850b8cbed6..b852645fd86 100644 --- a/src/pip/_internal/index/collector.py +++ b/src/pip/_internal/index/collector.py @@ -10,12 +10,12 @@ import os import re from collections import OrderedDict +from urllib import parse as urllib_parse +from urllib import request as urllib_request from pip._vendor import html5lib, requests from pip._vendor.distlib.compat import unescape from pip._vendor.requests.exceptions import RetryError, SSLError -from pip._vendor.six.moves.urllib import parse as urllib_parse -from pip._vendor.six.moves.urllib import request as urllib_request from pip._internal.exceptions import NetworkConnectionError from pip._internal.models.link import Link diff --git a/src/pip/_internal/models/direct_url.py b/src/pip/_internal/models/direct_url.py index 99aa68d121b..8f544caf603 100644 --- a/src/pip/_internal/models/direct_url.py +++ b/src/pip/_internal/models/direct_url.py @@ -1,9 +1,7 @@ """ PEP 610 """ import json import re - -from pip._vendor import six -from pip._vendor.six.moves.urllib import parse as urllib_parse +from urllib import parse as urllib_parse from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -35,8 +33,6 @@ def _get(d, expected_type, key, default=None): if key not in d: return default value = d[key] - if six.PY2 and expected_type is str: - expected_type = six.string_types # type: ignore if not isinstance(value, expected_type): raise DirectUrlValidationError( "{!r} has unexpected type for {} (expected {})".format( diff --git a/src/pip/_internal/models/index.py b/src/pip/_internal/models/index.py index 5b4a1fe2274..0374d7f55e8 100644 --- a/src/pip/_internal/models/index.py +++ b/src/pip/_internal/models/index.py @@ -1,4 +1,4 @@ -from pip._vendor.six.moves.urllib import parse as urllib_parse +from urllib import parse as urllib_parse class PackageIndex(object): diff --git a/src/pip/_internal/models/link.py b/src/pip/_internal/models/link.py index 29ef402beef..4ad4f7bde9a 100644 --- a/src/pip/_internal/models/link.py +++ b/src/pip/_internal/models/link.py @@ -1,8 +1,7 @@ import os import posixpath import re - -from pip._vendor.six.moves.urllib import parse as urllib_parse +from urllib import parse as urllib_parse from pip._internal.utils.filetypes import WHEEL_EXTENSION from pip._internal.utils.misc import ( diff --git a/src/pip/_internal/models/search_scope.py b/src/pip/_internal/models/search_scope.py index d732504e6f5..ab6f9148693 100644 --- a/src/pip/_internal/models/search_scope.py +++ b/src/pip/_internal/models/search_scope.py @@ -2,9 +2,9 @@ import logging import os import posixpath +from urllib import parse as urllib_parse from pip._vendor.packaging.utils import canonicalize_name -from pip._vendor.six.moves.urllib import parse as urllib_parse from pip._internal.models.index import PyPI from pip._internal.utils.compat import has_tls diff --git a/src/pip/_internal/network/auth.py b/src/pip/_internal/network/auth.py index 357811a16f1..3de21518e96 100644 --- a/src/pip/_internal/network/auth.py +++ b/src/pip/_internal/network/auth.py @@ -5,10 +5,10 @@ """ import logging +from urllib import parse as urllib_parse from pip._vendor.requests.auth import AuthBase, HTTPBasicAuth from pip._vendor.requests.utils import get_netrc_auth -from pip._vendor.six.moves.urllib import parse as urllib_parse from pip._internal.utils.misc import ( ask, diff --git a/src/pip/_internal/network/lazy_wheel.py b/src/pip/_internal/network/lazy_wheel.py index 608475abab3..83704f6f190 100644 --- a/src/pip/_internal/network/lazy_wheel.py +++ b/src/pip/_internal/network/lazy_wheel.py @@ -8,7 +8,6 @@ from zipfile import BadZipfile, ZipFile from pip._vendor.requests.models import CONTENT_CHUNK_SIZE -from pip._vendor.six.moves import range from pip._internal.network.utils import HEADERS, raise_for_status, response_chunks from pip._internal.utils.typing import MYPY_CHECK_RUNNING diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py index 454945d9aed..5839c4d28df 100644 --- a/src/pip/_internal/network/session.py +++ b/src/pip/_internal/network/session.py @@ -13,13 +13,13 @@ import platform import sys import warnings +from urllib import parse as urllib_parse from pip._vendor import requests, six, urllib3 from pip._vendor.cachecontrol import CacheControlAdapter from pip._vendor.requests.adapters import BaseAdapter, HTTPAdapter from pip._vendor.requests.models import Response from pip._vendor.requests.structures import CaseInsensitiveDict -from pip._vendor.six.moves.urllib import parse as urllib_parse from pip._vendor.urllib3.exceptions import InsecureRequestWarning from pip import __version__ diff --git a/src/pip/_internal/network/xmlrpc.py b/src/pip/_internal/network/xmlrpc.py index 504018f28fe..d025a145a35 100644 --- a/src/pip/_internal/network/xmlrpc.py +++ b/src/pip/_internal/network/xmlrpc.py @@ -2,11 +2,11 @@ """ import logging +from urllib import parse as urllib_parse # 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._internal.exceptions import NetworkConnectionError from pip._internal.network.utils import raise_for_status diff --git a/src/pip/_internal/operations/freeze.py b/src/pip/_internal/operations/freeze.py index d4f790cd447..ba885afd65e 100644 --- a/src/pip/_internal/operations/freeze.py +++ b/src/pip/_internal/operations/freeze.py @@ -4,7 +4,6 @@ import logging import os -from pip._vendor import six from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.pkg_resources import RequirementParseError @@ -162,7 +161,7 @@ def freeze( # 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): + for name, files in req_files.items(): if len(files) > 1: logger.warning("Requirement %s included multiple times [%s]", name, ', '.join(sorted(set(files)))) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 8b67ebb9431..7b7d48661ce 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -15,14 +15,13 @@ import sys import warnings from base64 import urlsafe_b64encode -from itertools import chain, starmap +from itertools import chain, filterfalse, starmap from zipfile import ZipFile from pip._vendor import pkg_resources from pip._vendor.distlib.scripts import ScriptMaker from pip._vendor.distlib.util import get_export_entry -from pip._vendor.six import PY2, ensure_str, ensure_text, itervalues, reraise, text_type -from pip._vendor.six.moves import filterfalse, map +from pip._vendor.six import ensure_str, ensure_text, reraise from pip._internal.exceptions import InstallationError from pip._internal.locations import get_major_minor_version @@ -70,12 +69,12 @@ from pip._internal.models.scheme import Scheme from pip._internal.utils.filesystem import NamedTemporaryFileResult - RecordPath = NewType('RecordPath', text_type) + RecordPath = NewType('RecordPath', str) InstalledCSVRow = Tuple[RecordPath, str, Union[int, str]] class File(Protocol): src_record_path = None # type: RecordPath - dest_path = None # type: text_type + dest_path = None # type: str changed = None # type: bool def save(self): @@ -87,7 +86,7 @@ def save(self): def rehash(path, blocksize=1 << 20): - # type: (text_type, int) -> Tuple[str, str] + # type: (str, int) -> Tuple[str, str] """Return (encoded_digest, length) for path using hashlib.sha256()""" h, length = hash_file(path, blocksize) digest = 'sha256=' + urlsafe_b64encode( @@ -102,14 +101,11 @@ def csv_io_kwargs(mode): """Return keyword arguments to properly open a CSV file in the given mode. """ - if PY2: - return {'mode': '{}b'.format(mode)} - else: - return {'mode': mode, 'newline': '', 'encoding': 'utf-8'} + return {'mode': mode, 'newline': '', 'encoding': 'utf-8'} def fix_script(path): - # type: (text_type) -> bool + # type: (str) -> bool """Replace #!python with #!/path/to/python Return True if file was changed. """ @@ -257,12 +253,12 @@ def _normalized_outrows(outrows): def _record_to_fs_path(record_path): - # type: (RecordPath) -> text_type + # type: (RecordPath) -> str return record_path def _fs_to_record_path(path, relative_to=None): - # type: (text_type, Optional[text_type]) -> RecordPath + # type: (str, Optional[str]) -> RecordPath if relative_to is not None: # On Windows, do not handle relative paths if they belong to different # logical disks @@ -307,7 +303,7 @@ def get_csv_rows_for_installed( path = _fs_to_record_path(f, lib_dir) digest, length = rehash(f) installed_rows.append((path, digest, length)) - for installed_record_path in itervalues(installed): + for installed_record_path in installed.values(): installed_rows.append((installed_record_path, '', '')) return installed_rows @@ -400,7 +396,7 @@ def get_console_script_specs(console): class ZipBackedFile(object): def __init__(self, src_record_path, dest_path, zip_file): - # type: (RecordPath, text_type, ZipFile) -> None + # type: (RecordPath, str, ZipFile) -> None self.src_record_path = src_record_path self.dest_path = dest_path self._zip_file = zip_file @@ -408,12 +404,7 @@ def __init__(self, src_record_path, dest_path, zip_file): def _getinfo(self): # type: () -> ZipInfo - if not PY2: - return self._zip_file.getinfo(self.src_record_path) - # Python 2 does not expose a way to detect a ZIP's encoding, but the - # wheel specification (PEP 427) explicitly mandates that paths should - # use UTF-8, so we assume it is true. - return self._zip_file.getinfo(self.src_record_path.encode("utf-8")) + return self._zip_file.getinfo(self.src_record_path) def save(self): # type: () -> None @@ -525,7 +516,7 @@ def _install_wheel( generated = [] # type: List[str] def record_installed(srcfile, destfile, modified=False): - # type: (RecordPath, text_type, bool) -> None + # type: (RecordPath, str, bool) -> None """Map archive RECORD paths to installation RECORD paths.""" newpath = _fs_to_record_path(destfile, lib_dir) installed[srcfile] = newpath @@ -546,7 +537,7 @@ def is_dir_path(path): return path.endswith("/") def assert_no_path_traversal(dest_dir_path, target_path): - # type: (text_type, text_type) -> None + # type: (str, str) -> None if not is_within_directory(dest_dir_path, target_path): message = ( "The wheel {!r} has a file {!r} trying to install" @@ -557,7 +548,7 @@ def assert_no_path_traversal(dest_dir_path, target_path): ) def root_scheme_file_maker(zip_file, dest): - # type: (ZipFile, text_type) -> Callable[[RecordPath], File] + # type: (ZipFile, str) -> Callable[[RecordPath], File] def make_root_scheme_file(record_path): # type: (RecordPath) -> File normed_path = os.path.normpath(record_path) @@ -675,7 +666,7 @@ def is_entrypoint_wrapper(file): record_installed(file.src_record_path, file.dest_path, file.changed) def pyc_source_file_paths(): - # type: () -> Iterator[text_type] + # type: () -> Iterator[str] # We de-duplicate installation paths, since there can be overlap (e.g. # file in .data maps to same location as file in wheel root). # Sorting installation paths makes it easier to reproduce and debug @@ -689,16 +680,10 @@ def pyc_source_file_paths(): yield full_installed_path def pyc_output_path(path): - # type: (text_type) -> text_type + # type: (str) -> str """Return the path the pyc file would have been written to. """ - if PY2: - if sys.flags.optimize: - return path + 'o' - else: - return path + 'c' - else: - return importlib.util.cache_from_source(path) + return importlib.util.cache_from_source(path) # Compile all of the pyc files for the installed files if pycompile: diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 13b2c0beee1..cc9c129b28a 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -10,7 +10,6 @@ import shutil from pip._vendor.packaging.utils import canonicalize_name -from pip._vendor.six import PY2 from pip._internal.distributions import make_distribution_for_install_requirement from pip._internal.distributions.installed import InstalledDistribution @@ -51,26 +50,16 @@ from pip._internal.req.req_tracker import RequirementTracker from pip._internal.utils.hashes import Hashes - if PY2: - CopytreeKwargs = TypedDict( - 'CopytreeKwargs', - { - 'ignore': Callable[[str, List[str]], List[str]], - 'symlinks': bool, - }, - total=False, - ) - else: - CopytreeKwargs = TypedDict( - 'CopytreeKwargs', - { - 'copy_function': Callable[[str, str], None], - 'ignore': Callable[[str, List[str]], List[str]], - 'ignore_dangling_symlinks': bool, - 'symlinks': bool, - }, - total=False, - ) + CopytreeKwargs = TypedDict( + 'CopytreeKwargs', + { + 'copy_function': Callable[[str, str], None], + 'ignore': Callable[[str, List[str]], List[str]], + 'ignore_dangling_symlinks': bool, + 'symlinks': bool, + }, + total=False, + ) logger = logging.getLogger(__name__) @@ -179,10 +168,7 @@ def ignore(d, names): kwargs = dict(ignore=ignore, symlinks=True) # type: CopytreeKwargs - if not PY2: - # Python 2 does not support copy_function, so we only ignore - # errors on special file copy in Python 3. - kwargs['copy_function'] = _copy2_ignoring_special_files + kwargs['copy_function'] = _copy2_ignoring_special_files shutil.copytree(source, target, **kwargs) diff --git a/src/pip/_internal/pyproject.py b/src/pip/_internal/pyproject.py index 4144a9ed60b..b7ca902e3a5 100644 --- a/src/pip/_internal/pyproject.py +++ b/src/pip/_internal/pyproject.py @@ -2,7 +2,6 @@ import io import os -import sys from collections import namedtuple from pip._vendor import six, toml @@ -27,10 +26,6 @@ def make_pyproject_path(unpacked_source_directory): # type: (str) -> str path = os.path.join(unpacked_source_directory, 'pyproject.toml') - # Python2 __file__ should not be unicode - if six.PY2 and isinstance(path, six.text_type): - path = path.encode(sys.getfilesystemencoding()) - return path diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index 0af60fa0569..ae891ce7aab 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -8,9 +8,7 @@ import os import re import shlex -import sys - -from pip._vendor.six.moves.urllib import parse as urllib_parse +from urllib import parse as urllib_parse from pip._internal.cli import cmdoptions from pip._internal.exceptions import InstallationError, RequirementsFileParseError @@ -410,10 +408,6 @@ def parse_line(line): defaults.format_control = finder.format_control args_str, options_str = break_args_options(line) - # Prior to 2.7.3, shlex cannot deal with unicode entries - if sys.version_info < (2, 7, 3): - # https://github.com/python/mypy/issues/1174 - options_str = options_str.encode('utf8') # type: ignore # https://github.com/python/mypy/issues/1174 opts, _ = parser.parse_args( diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 866d18fcb6e..e66fda226ea 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -480,10 +480,6 @@ def setup_py_path(self): assert self.source_dir, "No source dir for {}".format(self) setup_py = os.path.join(self.unpacked_source_directory, 'setup.py') - # Python2 __file__ should not be unicode - if six.PY2 and isinstance(setup_py, six.text_type): - setup_py = setup_py.encode(sys.getfilesystemencoding()) - return setup_py @property diff --git a/src/pip/_internal/req/req_uninstall.py b/src/pip/_internal/req/req_uninstall.py index 2e7dfcc7369..b70d5e7f43e 100644 --- a/src/pip/_internal/req/req_uninstall.py +++ b/src/pip/_internal/req/req_uninstall.py @@ -6,12 +6,13 @@ import os import sys import sysconfig +from importlib.util import cache_from_source from pip._vendor import pkg_resources from pip._internal.exceptions import UninstallationError from pip._internal.locations import bin_py, bin_user -from pip._internal.utils.compat import WINDOWS, cache_from_source, uses_pycache +from pip._internal.utils.compat import WINDOWS from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import ( FakeFile, @@ -363,7 +364,7 @@ def add(self, 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: + if os.path.splitext(path)[1] == '.py': self.add(cache_from_source(path)) def add_pth(self, pth_file, entry): @@ -609,7 +610,7 @@ def add(self, entry): # treats non-absolute paths with drive letter markings like c:foo\bar # as absolute paths. It also does not recognize UNC paths if they don't # have more than "\\sever\share". Valid examples: "\\server\share\" or - # "\\server\share\folder". Python 2.7.8+ support UNC in splitdrive. + # "\\server\share\folder". if WINDOWS and not os.path.splitdrive(entry)[0]: entry = entry.replace('\\', '/') self.entries.add(entry) diff --git a/src/pip/_internal/utils/compat.py b/src/pip/_internal/utils/compat.py index 2196e6e0aea..b0ff63f51c7 100644 --- a/src/pip/_internal/utils/compat.py +++ b/src/pip/_internal/utils/compat.py @@ -14,8 +14,6 @@ import shutil import sys -from pip._vendor.six import PY2, text_type - from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: @@ -41,47 +39,13 @@ def __call__(self, maxsize=None): __all__ = [ - "ipaddress", "uses_pycache", "console_to_str", - "get_path_uid", "stdlib_pkgs", "WINDOWS", "samefile", "get_terminal_size", + "ipaddress", "console_to_str", + "get_path_uid", "stdlib_pkgs", "WINDOWS", "get_terminal_size", ] logger = logging.getLogger(__name__) -if PY2: - 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 -else: - uses_pycache = True - from importlib.util import cache_from_source - - -if PY2: - # In Python 2.7, 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)) - # Python 2 gave us characters - convert to numeric bytes - raw_bytes = (ord(b) for b in raw_bytes) - return u"".join(map(u"\\x{:x}".format, raw_bytes)), err.end - codecs.register_error( - "backslashreplace_decode", - backslashreplace_decode_fn, - ) - backslashreplace_decode = "backslashreplace_decode" -else: - backslashreplace_decode = "backslashreplace" - def has_tls(): # type: () -> bool @@ -114,7 +78,7 @@ def str_to_display(data, desc=None): We also ensure that the output can be safely written to standard output without encoding errors. """ - if isinstance(data, text_type): + if isinstance(data, str): return data # Otherwise, data is a bytes object (str in Python 2). @@ -135,7 +99,7 @@ def str_to_display(data, desc=None): desc or 'Bytes object', encoding, ) - decoded_data = data.decode(encoding, errors=backslashreplace_decode) + decoded_data = data.decode(encoding, errors="backslashreplace") # Make sure we can print the output, by encoding it to the output # encoding with replacement of unencodable characters, and then @@ -226,17 +190,6 @@ def expanduser(path): (sys.platform == 'cli' and os.name == 'nt')) -def samefile(file1, file2): - # type: (str, str) -> bool - """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(): # type: () -> Tuple[int, int] diff --git a/src/pip/_internal/utils/filesystem.py b/src/pip/_internal/utils/filesystem.py index 303243fd22f..1b0c083cfda 100644 --- a/src/pip/_internal/utils/filesystem.py +++ b/src/pip/_internal/utils/filesystem.py @@ -12,7 +12,6 @@ # 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._internal.utils.compat import get_path_uid from pip._internal.utils.misc import format_size @@ -114,18 +113,7 @@ def adjacent_tmp_file(path, **kwargs): _replace_retry = retry(stop_max_delay=1000, wait_fixed=250) -if PY2: - @_replace_retry - def replace(src, dest): - # type: (str, str) -> None - try: - os.rename(src, dest) - except OSError: - os.remove(dest) - os.rename(src, dest) - -else: - replace = _replace_retry(os.replace) +replace = _replace_retry(os.replace) # test_writable_dir and _test_writable_dir_win are copied from Flit, diff --git a/src/pip/_internal/utils/hashes.py b/src/pip/_internal/utils/hashes.py index 4d90f5bfda4..53da6636e6d 100644 --- a/src/pip/_internal/utils/hashes.py +++ b/src/pip/_internal/utils/hashes.py @@ -2,21 +2,14 @@ import hashlib -from pip._vendor.six import iteritems, iterkeys, itervalues - from pip._internal.exceptions import HashMismatch, HashMissing, InstallationError from pip._internal.utils.misc import read_chunks from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: + from hashlib import _Hash from typing import BinaryIO, Dict, Iterator, List, NoReturn - from pip._vendor.six import PY3 - if PY3: - from hashlib import _Hash - else: - from hashlib import _hash as _Hash - # The recommended hash algo of the moment. Change this whenever the state of # the art changes; it won't hurt backward compatibility. @@ -60,7 +53,7 @@ def __and__(self, other): # Otherwise only hashes that present in both objects are allowed. new = {} - for alg, values in iteritems(other._allowed): + for alg, values in other._allowed.items(): if alg not in self._allowed: continue new[alg] = [v for v in values if v in self._allowed[alg]] @@ -89,7 +82,7 @@ def check_against_chunks(self, chunks): """ gots = {} - for hash_name in iterkeys(self._allowed): + for hash_name in self._allowed.keys(): try: gots[hash_name] = hashlib.new(hash_name) except (ValueError, TypeError): @@ -98,10 +91,10 @@ def check_against_chunks(self, chunks): ) for chunk in chunks: - for hash in itervalues(gots): + for hash in gots.values(): hash.update(chunk) - for hash_name, got in iteritems(gots): + for hash_name, got in gots.items(): if got.hexdigest() in self._allowed[hash_name]: return self._raise(gots) diff --git a/src/pip/_internal/utils/logging.py b/src/pip/_internal/utils/logging.py index 9a017cf7e33..44c9beabd2d 100644 --- a/src/pip/_internal/utils/logging.py +++ b/src/pip/_internal/utils/logging.py @@ -11,8 +11,6 @@ import sys from logging import Filter, getLogger -from pip._vendor.six import PY2 - from pip._internal.utils.compat import WINDOWS from pip._internal.utils.deprecation import DEPRECATION_MSG_PREFIX from pip._internal.utils.misc import ensure_dir @@ -62,30 +60,18 @@ class BrokenStdoutLoggingError(Exception): pass -# BrokenPipeError does not exist in Python 2 and, in addition, manifests -# differently in Windows and non-Windows. +# BrokenPipeError manifests differently in Windows and non-Windows. if WINDOWS: # In Windows, a broken pipe can show up as EINVAL rather than EPIPE: # https://bugs.python.org/issue19612 # https://bugs.python.org/issue30418 - if PY2: - def _is_broken_pipe_error(exc_class, exc): - """See the docstring for non-Windows Python 3 below.""" - return (exc_class is IOError and - exc.errno in (errno.EINVAL, errno.EPIPE)) - else: - # In Windows, a broken pipe IOError became OSError in Python 3. - def _is_broken_pipe_error(exc_class, exc): - """See the docstring for non-Windows Python 3 below.""" - return ((exc_class is BrokenPipeError) or # noqa: F821 - (exc_class is OSError and - exc.errno in (errno.EINVAL, errno.EPIPE))) -elif PY2: def _is_broken_pipe_error(exc_class, exc): - """See the docstring for non-Windows Python 3 below.""" - return (exc_class is IOError and exc.errno == errno.EPIPE) + """See the docstring for non-Windows below.""" + return ((exc_class is BrokenPipeError) or # noqa: F821 + (exc_class is OSError and + exc.errno in (errno.EINVAL, errno.EPIPE))) else: - # Then we are in the non-Windows Python 3 case. + # Then we are in the non-Windows case. def _is_broken_pipe_error(exc_class, exc): """ Return whether an exception is a broken pipe error. diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 4fb64d2672a..aa428da0564 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -16,7 +16,10 @@ import stat import sys from collections import deque -from itertools import tee +from io import StringIO +from itertools import filterfalse, tee, zip_longest +from urllib import parse as urllib_parse +from urllib.parse import unquote as urllib_unquote from pip._vendor import pkg_resources from pip._vendor.packaging.utils import canonicalize_name @@ -24,26 +27,17 @@ # 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, text_type -from pip._vendor.six.moves import filter, filterfalse, input, map, zip_longest -from pip._vendor.six.moves.urllib import parse as urllib_parse -from pip._vendor.six.moves.urllib.parse import unquote as urllib_unquote from pip import __version__ from pip._internal.exceptions import CommandError from pip._internal.locations import get_major_minor_version, site_packages, user_site -from pip._internal.utils.compat import WINDOWS, expanduser, stdlib_pkgs, str_to_display +from pip._internal.utils.compat import WINDOWS, expanduser, stdlib_pkgs from pip._internal.utils.typing import MYPY_CHECK_RUNNING, cast from pip._internal.utils.virtualenv import ( running_under_virtualenv, virtualenv_no_global, ) -if PY2: - from io import BytesIO as StringIO -else: - from io import StringIO - if MYPY_CHECK_RUNNING: from typing import ( Any, @@ -173,7 +167,7 @@ def path_to_display(path): """ if path is None: return None - if isinstance(path, text_type): + if isinstance(path, str): return path # Otherwise, path is a bytes object (str in Python 2). try: @@ -181,17 +175,9 @@ def path_to_display(path): except UnicodeDecodeError: # Include the full bytes to make troubleshooting easier, even though # it may not be very human readable. - if PY2: - # Convert the bytes to a readable str representation using - # repr(), and then convert the str to unicode. - # Also, we add the prefix "b" to the repr() return value both - # to make the Python 2 output look like the Python 3 output, and - # to signal to the user that this is a bytes representation. - display_path = str_to_display('b{!r}'.format(path)) - else: - # Silence the "F821 undefined name 'ascii'" flake8 error since - # in Python 3 ascii() is a built-in. - display_path = ascii(path) # noqa: F821 + # Silence the "F821 undefined name 'ascii'" flake8 error since + # ascii() is a built-in. + display_path = ascii(path) # noqa: F821 return display_path @@ -201,9 +187,6 @@ def display_path(path): """Gives the display value for a given path, making it relative to cwd if possible.""" path = os.path.normcase(os.path.abspath(path)) - if sys.version_info[0] == 2: - path = path.decode(sys.getfilesystemencoding(), 'replace') - path = path.encode(sys.getdefaultencoding(), 'replace') if path.startswith(os.getcwd() + os.path.sep): path = '.' + path[len(os.getcwd()):] return path @@ -854,12 +837,6 @@ def __eq__(self, other): # just the raw, original string. return (self.secret == other.secret) - # We need to provide an explicit __ne__ implementation for Python 2. - # TODO: remove this when we drop PY2 support. - def __ne__(self, other): - # type: (Any) -> bool - return not self == other - def hide_value(value): # type: (str) -> HiddenText diff --git a/src/pip/_internal/utils/parallel.py b/src/pip/_internal/utils/parallel.py index d4113bdc285..57082367e18 100644 --- a/src/pip/_internal/utils/parallel.py +++ b/src/pip/_internal/utils/parallel.py @@ -23,8 +23,6 @@ from multiprocessing.dummy import Pool as ThreadPool from pip._vendor.requests.adapters import DEFAULT_POOLSIZE -from pip._vendor.six import PY2 -from pip._vendor.six.moves import map from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -100,7 +98,7 @@ def _map_multithread(func, iterable, chunksize=1): return pool.imap_unordered(func, iterable, chunksize) -if LACK_SEM_OPEN or PY2: +if LACK_SEM_OPEN: map_multiprocess = map_multithread = _map_fallback else: map_multiprocess = _map_multiprocess diff --git a/src/pip/_internal/utils/subprocess.py b/src/pip/_internal/utils/subprocess.py index 605e711e603..85b92c47923 100644 --- a/src/pip/_internal/utils/subprocess.py +++ b/src/pip/_internal/utils/subprocess.py @@ -2,10 +2,9 @@ import logging import os +import shlex import subprocess -from pip._vendor.six.moves import shlex_quote - from pip._internal.cli.spinners import SpinnerInterface, open_spinner from pip._internal.exceptions import InstallationError from pip._internal.utils.compat import console_to_str, str_to_display @@ -51,8 +50,8 @@ def format_command_args(args): # has type unicode and includes a non-ascii character. (The type # checker doesn't ensure the annotations are correct in all cases.) return ' '.join( - shlex_quote(str(arg)) if isinstance(arg, HiddenText) - else shlex_quote(arg) for arg in args + shlex.quote(str(arg)) if isinstance(arg, HiddenText) + else shlex.quote(arg) for arg in args ) diff --git a/src/pip/_internal/utils/urls.py b/src/pip/_internal/utils/urls.py index f37bc8f90b2..91df4c30d82 100644 --- a/src/pip/_internal/utils/urls.py +++ b/src/pip/_internal/utils/urls.py @@ -1,8 +1,7 @@ import os import sys - -from pip._vendor.six.moves.urllib import parse as urllib_parse -from pip._vendor.six.moves.urllib import request as urllib_request +from urllib import parse as urllib_parse +from urllib import request as urllib_request from pip._internal.utils.typing import MYPY_CHECK_RUNNING diff --git a/src/pip/_internal/utils/wheel.py b/src/pip/_internal/utils/wheel.py index 9ce371c76eb..6be61371e55 100644 --- a/src/pip/_internal/utils/wheel.py +++ b/src/pip/_internal/utils/wheel.py @@ -5,11 +5,11 @@ import logging from email.parser import Parser -from zipfile import ZipFile +from zipfile import BadZipFile, ZipFile from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.pkg_resources import DistInfoDistribution -from pip._vendor.six import PY2, ensure_str +from pip._vendor.six import ensure_str from pip._internal.exceptions import UnsupportedWheel from pip._internal.utils.pkg_resources import DictMetadata @@ -21,11 +21,6 @@ from pip._vendor.pkg_resources import Distribution -if PY2: - from zipfile import BadZipfile as BadZipFile -else: - from zipfile import BadZipFile - VERSION_COMPATIBLE = (1, 0) diff --git a/src/pip/_internal/vcs/bazaar.py b/src/pip/_internal/vcs/bazaar.py index 3180713f7db..3a269a64774 100644 --- a/src/pip/_internal/vcs/bazaar.py +++ b/src/pip/_internal/vcs/bazaar.py @@ -5,8 +5,7 @@ import logging import os - -from pip._vendor.six.moves.urllib import parse as urllib_parse +from urllib import parse as urllib_parse from pip._internal.utils.misc import display_path, rmtree from pip._internal.utils.subprocess import make_command diff --git a/src/pip/_internal/vcs/git.py b/src/pip/_internal/vcs/git.py index 1831aede58a..98dc3046e50 100644 --- a/src/pip/_internal/vcs/git.py +++ b/src/pip/_internal/vcs/git.py @@ -6,10 +6,10 @@ import logging import os.path import re +from urllib import parse as urllib_parse +from urllib import request as urllib_request from pip._vendor.packaging.version import parse as parse_version -from pip._vendor.six.moves.urllib import parse as urllib_parse -from pip._vendor.six.moves.urllib import request as urllib_request from pip._internal.exceptions import BadCommand, SubProcessError from pip._internal.utils.misc import display_path, hide_url diff --git a/src/pip/_internal/vcs/mercurial.py b/src/pip/_internal/vcs/mercurial.py index 69763feaea4..34a045f4c60 100644 --- a/src/pip/_internal/vcs/mercurial.py +++ b/src/pip/_internal/vcs/mercurial.py @@ -3,11 +3,10 @@ from __future__ import absolute_import +import configparser import logging import os -from pip._vendor.six.moves import configparser - from pip._internal.exceptions import BadCommand, SubProcessError from pip._internal.utils.misc import display_path from pip._internal.utils.subprocess import make_command diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index 6724dcc697d..61bf8ce34c6 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -8,12 +8,12 @@ import shutil import subprocess import sys +from urllib import parse as urllib_parse from pip._vendor import pkg_resources -from pip._vendor.six.moves.urllib import parse as urllib_parse from pip._internal.exceptions import BadCommand, InstallationError, SubProcessError -from pip._internal.utils.compat import console_to_str, samefile +from pip._internal.utils.compat import console_to_str from pip._internal.utils.logging import subprocess_logger from pip._internal.utils.misc import ( ask_path_exists, @@ -197,7 +197,7 @@ def find_path_to_setup_from_repo_root(location, repo_root): ) return None - if samefile(repo_root, location): + if os.path.samefile(repo_root, location): return None return os.path.relpath(location, repo_root) @@ -289,9 +289,7 @@ def __init__(self): # Register more schemes with urlparse for various version control # systems urllib_parse.uses_netloc.extend(self.schemes) - # Python >= 2.7.4, 3.3 doesn't have uses_fragment - if getattr(urllib_parse, 'uses_fragment', None): - urllib_parse.uses_fragment.extend(self.schemes) + urllib_parse.uses_fragment.extend(self.schemes) super(VcsSupport, self).__init__() def __iter__(self): diff --git a/tests/conftest.py b/tests/conftest.py index 78be52788a1..499c121bca9 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,7 +10,6 @@ from contextlib import contextmanager import pytest -import six from mock import patch from pip._vendor.contextlib2 import ExitStack, nullcontext from setuptools.wheel import Wheel @@ -73,15 +72,14 @@ def pytest_collection_modifyitems(config, items): if item.get_closest_marker('network') is not None: item.add_marker(pytest.mark.flaky(reruns=3, reruns_delay=2)) - if six.PY3: - if (item.get_closest_marker('incompatible_with_test_venv') and - config.getoption("--use-venv")): - item.add_marker(pytest.mark.skip( - 'Incompatible with test venv')) - if (item.get_closest_marker('incompatible_with_venv') and - sys.prefix != sys.base_prefix): - item.add_marker(pytest.mark.skip( - 'Incompatible with venv')) + if (item.get_closest_marker('incompatible_with_test_venv') and + config.getoption("--use-venv")): + item.add_marker(pytest.mark.skip( + 'Incompatible with test venv')) + if (item.get_closest_marker('incompatible_with_venv') and + sys.prefix != sys.base_prefix): + item.add_marker(pytest.mark.skip( + 'Incompatible with venv')) module_path = os.path.relpath( item.module.__file__, @@ -111,16 +109,10 @@ def resolver_variant(request): features = set(os.environ.get("PIP_USE_FEATURE", "").split()) deprecated_features = set(os.environ.get("PIP_USE_DEPRECATED", "").split()) - if six.PY3: - if resolver == "legacy": - deprecated_features.add("legacy-resolver") - else: - deprecated_features.discard("legacy-resolver") + if resolver == "legacy": + deprecated_features.add("legacy-resolver") else: - if resolver == "2020-resolver": - features.add("2020-resolver") - else: - features.discard("2020-resolver") + deprecated_features.discard("legacy-resolver") env = { "PIP_USE_FEATURE": " ".join(features), @@ -141,7 +133,7 @@ def tmpdir_factory(request, tmpdir_factory): # handle non-ASCII file names. This works around the problem by # passing a unicode object to rmtree(). shutil.rmtree( - six.text_type(tmpdir_factory.getbasetemp()), + str(tmpdir_factory.getbasetemp()), ignore_errors=True, ) @@ -166,7 +158,7 @@ def tmpdir(request, tmpdir): # py.path.remove() uses str paths on Python 2 and cannot # handle non-ASCII file names. This works around the problem by # passing a unicode object to rmtree(). - shutil.rmtree(six.text_type(tmpdir), ignore_errors=True) + shutil.rmtree(str(tmpdir), ignore_errors=True) @pytest.fixture(autouse=True) @@ -337,7 +329,7 @@ def install_egg_link(venv, project_name, egg_info_dir): def virtualenv_template(request, tmpdir_factory, pip_src, setuptools_install, coverage_install): - if six.PY3 and request.config.getoption('--use-venv'): + if request.config.getoption('--use-venv'): venv_type = 'venv' else: venv_type = 'virtualenv' @@ -474,10 +466,7 @@ def __init__(self, returncode, stdout): class InMemoryPip(object): def pip(self, *args): orig_stdout = sys.stdout - if six.PY3: - stdout = io.StringIO() - else: - stdout = io.BytesIO() + stdout = io.StringIO() sys.stdout = stdout try: returncode = pip_entry_point(list(args)) diff --git a/tests/functional/test_download.py b/tests/functional/test_download.py index 24bc4ddbcc9..5cd91f5421d 100644 --- a/tests/functional/test_download.py +++ b/tests/functional/test_download.py @@ -4,7 +4,6 @@ from hashlib import sha256 import pytest -from pip._vendor.six import PY2 from pip._internal.cli.status_codes import ERROR from pip._internal.utils.urls import path_to_url @@ -490,7 +489,7 @@ def make_wheel_with_python_requires(script, package_name, python_requires): package_dir.joinpath('setup.py').write_text(text) script.run( 'python', 'setup.py', 'bdist_wheel', '--universal', cwd=package_dir, - allow_stderr_warning=PY2, + allow_stderr_warning=False, ) file_name = '{}-1.0-py2.py3-none-any.whl'.format(package_name) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index f9a807bca79..3bd0f63afe3 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -9,7 +9,6 @@ from os.path import curdir, join, pardir import pytest -from pip._vendor.six import PY2 from pip._internal.cli.status_codes import ERROR, SUCCESS from pip._internal.models.index import PyPI, TestPyPI @@ -26,8 +25,6 @@ pyversion, pyversion_tuple, requirements_file, - skip_if_not_python2, - skip_if_python2, windows_workaround_7667, ) from tests.lib.filesystem import make_socket_file @@ -658,22 +655,7 @@ def test_editable_install__local_dir_no_setup_py_with_pyproject( assert 'A "pyproject.toml" file was found' in msg -@skip_if_not_python2 -@pytest.mark.xfail -def test_install_argparse_shadowed(script): - # When argparse is in the stdlib, we support installing it - # even though that's pretty useless because older packages did need to - # depend on it, and not having its metadata will cause pkg_resources - # requirements checks to fail // trigger easy-install, both of which are - # bad. - # XXX: Note, this test hits the outside-environment check, not the - # in-stdlib check, because our tests run in virtualenvs... - result = script.pip('install', 'argparse>=1.4') - assert "Not uninstalling argparse" in result.stdout - - @pytest.mark.network -@skip_if_python2 def test_upgrade_argparse_shadowed(script): # If argparse is installed - even if shadowed for imported - we support # upgrading it and properly remove the older versions files. @@ -1568,7 +1550,7 @@ def test_install_incompatible_python_requires_wheel(script, with_wheel): """)) script.run( 'python', 'setup.py', 'bdist_wheel', '--universal', - cwd=pkga_path, allow_stderr_warning=PY2, + cwd=pkga_path, allow_stderr_warning=False, ) result = script.pip('install', './pkga/dist/pkga-0.1-py2.py3-none-any.whl', expect_error=True) @@ -1837,7 +1819,6 @@ def test_install_yanked_file_and_print_warning(script, data): assert 'Successfully installed simple-3.0\n' in result.stdout, str(result) -@skip_if_python2 @pytest.mark.parametrize("install_args", [ (), ("--trusted-host", "localhost"), diff --git a/tests/functional/test_install_config.py b/tests/functional/test_install_config.py index 783f6ac7e97..dcc9c66d5a4 100644 --- a/tests/functional/test_install_config.py +++ b/tests/functional/test_install_config.py @@ -5,7 +5,6 @@ import pytest -from tests.lib import skip_if_python2 from tests.lib.server import ( authorization_response, file_response, @@ -130,7 +129,6 @@ def test_command_line_appends_correctly(script, data): ), 'stdout: {}'.format(result.stdout) -@skip_if_python2 def test_config_file_override_stack( script, virtualenv, mock_server, shared_data ): @@ -249,7 +247,6 @@ def test_prompt_for_authentication(script, data, cert_factory): result.stdout, str(result) -@skip_if_python2 def test_do_not_prompt_for_authentication(script, data, cert_factory): """Test behaviour if --no-input option is given while installing from a index url requiring authentication diff --git a/tests/functional/test_install_index.py b/tests/functional/test_install_index.py index e887595b937..8e432b95409 100644 --- a/tests/functional/test_install_index.py +++ b/tests/functional/test_install_index.py @@ -1,7 +1,6 @@ import os import textwrap - -from pip._vendor.six.moves.urllib import parse as urllib_parse +from urllib import parse as urllib_parse def test_find_links_relative_path(script, data, with_wheel): diff --git a/tests/functional/test_install_wheel.py b/tests/functional/test_install_wheel.py index ad4e749676f..177e86db320 100644 --- a/tests/functional/test_install_wheel.py +++ b/tests/functional/test_install_wheel.py @@ -8,7 +8,7 @@ import pytest -from tests.lib import create_basic_wheel_for_package, skip_if_python2 +from tests.lib import create_basic_wheel_for_package from tests.lib.path import Path from tests.lib.wheel import make_wheel @@ -118,7 +118,6 @@ def test_basic_install_from_wheel_file(script, data): # Installation seems to work, but scripttest fails to check. # I really don't care now since we're desupporting it soon anyway. -@skip_if_python2 def test_basic_install_from_unicode_wheel(script, data): """ Test installing from a wheel (that has a script) @@ -394,8 +393,6 @@ def test_install_from_wheel_gen_uppercase_entrypoint( assert bool(os.access(script.base_path / wrapper_file, os.X_OK)) -# pkg_resources.EntryPoint() does not parse unicode correctly on Python 2. -@skip_if_python2 def test_install_from_wheel_gen_unicode_entrypoint(script): make_wheel( "script_wheel_unicode", @@ -651,8 +648,6 @@ def test_wheel_installs_ok_with_badly_encoded_irrelevant_dist_info_file( ) -# Metadata is not decoded on Python 2. -@skip_if_python2 def test_wheel_install_fails_with_badly_encoded_metadata(script): package = create_basic_wheel_for_package( script, diff --git a/tests/functional/test_warning.py b/tests/functional/test_warning.py index ff228421e66..3558704bcef 100644 --- a/tests/functional/test_warning.py +++ b/tests/functional/test_warning.py @@ -1,10 +1,7 @@ -import platform import textwrap import pytest -from tests.lib import skip_if_not_python2, skip_if_python2 - @pytest.fixture def warnings_demo(tmpdir): @@ -37,33 +34,11 @@ def test_deprecation_warnings_can_be_silenced(script, warnings_demo): CPYTHON_DEPRECATION_TEXT = "January 1st, 2020" -@skip_if_python2 def test_version_warning_is_not_shown_if_python_version_is_not_2(script): result = script.pip("debug", allow_stderr_warning=True) assert DEPRECATION_TEXT not in result.stderr, str(result) assert CPYTHON_DEPRECATION_TEXT not in result.stderr, str(result) -@skip_if_python2 def test_flag_does_nothing_if_python_version_is_not_2(script): script.pip("list", "--no-python-version-warning") - - -@skip_if_not_python2 -def test_version_warning_is_shown_if_python_version_is_2(script): - result = script.pip("debug", allow_stderr_warning=True) - assert DEPRECATION_TEXT in result.stderr, str(result) - if platform.python_implementation() == 'CPython': - assert CPYTHON_DEPRECATION_TEXT in result.stderr, str(result) - else: - assert CPYTHON_DEPRECATION_TEXT not in result.stderr, str(result) - - -@skip_if_not_python2 -def test_version_warning_is_not_shown_when_flag_is_passed(script): - result = script.pip( - "debug", "--no-python-version-warning", allow_stderr_warning=True - ) - assert DEPRECATION_TEXT not in result.stderr, str(result) - assert CPYTHON_DEPRECATION_TEXT not in result.stderr, str(result) - assert "--no-python-version-warning" not in result.stderr diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index 07569d814f4..2e632e30152 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -15,7 +15,7 @@ from zipfile import ZipFile import pytest -from pip._vendor.six import PY2, ensure_binary, text_type +from pip._vendor.six import ensure_binary from scripttest import FoundDir, TestFileEnvironment from pip._internal.index.collector import LinkCollector @@ -1107,7 +1107,7 @@ def create_basic_sdist_for_package( retval, 'gztar', root_dir=script.temp_path, - base_dir=text_type(os.curdir), + base_dir=str(os.curdir), ) shutil.move(generated, retval) @@ -1164,10 +1164,6 @@ def need_mercurial(fn): )(fn)) -skip_if_python2 = pytest.mark.skipif(PY2, reason="Non-Python 2 only") -skip_if_not_python2 = pytest.mark.skipif(not PY2, reason="Python 2 only") - - # Workaround for test failures after new wheel release. windows_workaround_7667 = pytest.mark.skipif( "sys.platform == 'win32' and sys.version_info < (3,)", diff --git a/tests/lib/local_repos.py b/tests/lib/local_repos.py index 2a41595f9f2..6899677eeda 100644 --- a/tests/lib/local_repos.py +++ b/tests/lib/local_repos.py @@ -2,8 +2,7 @@ import os import subprocess - -from pip._vendor.six.moves.urllib import request as urllib_request +from urllib import request as urllib_request from pip._internal.utils.misc import hide_url from pip._internal.utils.typing import MYPY_CHECK_RUNNING diff --git a/tests/lib/path.py b/tests/lib/path.py index d1ea6bc5e8d..ec4f1e37c5b 100644 --- a/tests/lib/path.py +++ b/tests/lib/path.py @@ -5,9 +5,6 @@ import glob import os -import shutil - -from pip._vendor import six try: from os import supports_fd @@ -15,11 +12,7 @@ supports_fd = set() - -_base = six.text_type if os.path.supports_unicode_filenames else str - - -class Path(_base): +class Path(str): """ Models a path in an object oriented way. """ @@ -32,8 +25,8 @@ class Path(_base): def __new__(cls, *paths): if len(paths): - return _base.__new__(cls, os.path.join(*paths)) - return _base.__new__(cls) + return str.__new__(cls, os.path.join(*paths)) + return str.__new__(cls) def __div__(self, path): """ @@ -71,20 +64,20 @@ def __add__(self, path): >>> Path('/home/a') + 'bc.d' '/home/abc.d' """ - return Path(_base(self) + path) + return Path(str(self) + path) def __radd__(self, path): """ >>> '/home/a' + Path('bc.d') '/home/abc.d' """ - return Path(path + _base(self)) + return Path(path + str(self)) def __repr__(self): - return u"Path({inner})".format(inner=_base.__repr__(self)) + return u"Path({inner})".format(inner=str.__repr__(self)) def __hash__(self): - return _base.__hash__(self) + return str.__hash__(self) @property def name(self): diff --git a/tests/lib/test_wheel.py b/tests/lib/test_wheel.py index a6f46cd899c..15e5a75fe1e 100644 --- a/tests/lib/test_wheel.py +++ b/tests/lib/test_wheel.py @@ -5,7 +5,7 @@ from functools import partial from zipfile import ZipFile -from pip._vendor.six import ensure_text, iteritems +from pip._vendor.six import ensure_text from pip._internal.utils.typing import MYPY_CHECK_RUNNING from tests.lib.wheel import ( @@ -182,7 +182,7 @@ def test_make_wheel_default_record(): ], "simple-0.1.0.dist-info/RECORD": ["", ""], } - for name, values in iteritems(expected): + for name, values in expected.items(): assert records[name] == values, name # WHEEL and METADATA aren't constructed in a stable way, so just spot @@ -191,7 +191,7 @@ def test_make_wheel_default_record(): "simple-0.1.0.dist-info/METADATA": "51", "simple-0.1.0.dist-info/WHEEL": "104", } - for name, length in iteritems(expected_variable): + for name, length in expected_variable.items(): assert records[name][0].startswith("sha256="), name assert records[name][1] == length, name diff --git a/tests/lib/wheel.py b/tests/lib/wheel.py index d89a680a190..b5e222fda43 100644 --- a/tests/lib/wheel.py +++ b/tests/lib/wheel.py @@ -13,7 +13,7 @@ import csv23 from pip._vendor.requests.structures import CaseInsensitiveDict -from pip._vendor.six import ensure_binary, ensure_text, iteritems +from pip._vendor.six import ensure_binary, ensure_text from pip._internal.utils.typing import MYPY_CHECK_RUNNING from tests.lib.path import Path @@ -68,7 +68,7 @@ def message_from_dict(headers): List values are converted into repeated headers in the result. """ message = Message() - for name, value in iteritems(headers): + for name, value in headers.items(): if isinstance(value, list): for v in value: message[name] = v @@ -161,7 +161,7 @@ def make_entry_points_file( entry_points_data["console_scripts"] = console_scripts lines = [] - for section, values in iteritems(entry_points_data): + for section, values in entry_points_data.items(): lines.append("[{}]".format(section)) lines.extend(values) @@ -175,7 +175,7 @@ def make_files(files): # type: (Dict[str, AnyStr]) -> List[File] return [ File(name, ensure_binary(contents)) - for name, contents in iteritems(files) + for name, contents in files.items() ] @@ -184,7 +184,7 @@ def make_metadata_files(name, version, files): get_path = partial(dist_info_path, name, version) return [ File(get_path(name), ensure_binary(contents)) - for name, contents in iteritems(files) + for name, contents in files.items() ] @@ -193,7 +193,7 @@ def make_data_files(name, version, files): data_dir = "{}-{}.data".format(name, version) return [ File("{}/{}".format(data_dir, name), ensure_binary(contents)) - for name, contents in iteritems(files) + for name, contents in files.items() ] diff --git a/tests/unit/test_collector.py b/tests/unit/test_collector.py index fa1057b640e..4384812fc67 100644 --- a/tests/unit/test_collector.py +++ b/tests/unit/test_collector.py @@ -3,13 +3,13 @@ import re import uuid from textwrap import dedent +from urllib import request as urllib_request import mock import pretend import pytest from mock import Mock, patch from pip._vendor import html5lib, requests -from pip._vendor.six.moves.urllib import request as urllib_request from pip._internal.exceptions import NetworkConnectionError from pip._internal.index.collector import ( @@ -30,7 +30,7 @@ from pip._internal.models.index import PyPI from pip._internal.models.link import Link from pip._internal.network.session import PipSession -from tests.lib import make_test_link_collector, skip_if_python2 +from tests.lib import make_test_link_collector @pytest.mark.parametrize( @@ -406,7 +406,6 @@ def test_parse_links__yanked_reason(anchor_html, expected): assert actual == expected -@skip_if_python2 def test_parse_links_caches_same_page_by_url(): html = ( '<html><head><meta charset="utf-8"><head>' diff --git a/tests/unit/test_logging.py b/tests/unit/test_logging.py index 10d47eb6143..54bce7052ac 100644 --- a/tests/unit/test_logging.py +++ b/tests/unit/test_logging.py @@ -1,10 +1,8 @@ -import errno import logging from threading import Thread import pytest from mock import patch -from pip._vendor.six import PY2 from pip._internal.utils.logging import ( BrokenStdoutLoggingError, @@ -17,19 +15,6 @@ logger = logging.getLogger(__name__) -# This is a Python 2/3 compatibility helper. -def _make_broken_pipe_error(): - """ - Return an exception object representing a broken pipe. - """ - if PY2: - # This is one way a broken pipe error can show up in Python 2 - # (a non-Windows example in this case). - return IOError(errno.EPIPE, 'Broken pipe') - - return BrokenPipeError() # noqa: F821 - - class TestIndentingFormatter(object): """Test ``pip._internal.utils.logging.IndentingFormatter``.""" @@ -146,7 +131,7 @@ def test_broken_pipe_in_stderr_flush(self): with captured_stderr() as stderr: handler = ColorizedStreamHandler(stream=stderr) with patch('sys.stderr.flush') as mock_flush: - mock_flush.side_effect = _make_broken_pipe_error() + mock_flush.side_effect = BrokenPipeError() # The emit() call raises no exception. handler.emit(record) @@ -154,13 +139,9 @@ def test_broken_pipe_in_stderr_flush(self): assert err_text.startswith('my error') # Check that the logging framework tried to log the exception. - if PY2: - assert 'IOError: [Errno 32] Broken pipe' in err_text - assert 'Logged from file' in err_text - else: - assert 'Logging error' in err_text - assert 'BrokenPipeError' in err_text - assert "Message: 'my error'" in err_text + assert 'Logging error' in err_text + assert 'BrokenPipeError' in err_text + assert "Message: 'my error'" in err_text def test_broken_pipe_in_stdout_write(self): """ @@ -173,7 +154,7 @@ def test_broken_pipe_in_stdout_write(self): with captured_stdout() as stdout: handler = ColorizedStreamHandler(stream=stdout) with patch('sys.stdout.write') as mock_write: - mock_write.side_effect = _make_broken_pipe_error() + mock_write.side_effect = BrokenPipeError() with pytest.raises(BrokenStdoutLoggingError): handler.emit(record) @@ -188,7 +169,7 @@ def test_broken_pipe_in_stdout_flush(self): with captured_stdout() as stdout: handler = ColorizedStreamHandler(stream=stdout) with patch('sys.stdout.flush') as mock_flush: - mock_flush.side_effect = _make_broken_pipe_error() + mock_flush.side_effect = BrokenPipeError() with pytest.raises(BrokenStdoutLoggingError): handler.emit(record) diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py index f995d05a674..4812637ee5a 100644 --- a/tests/unit/test_req_file.py +++ b/tests/unit/test_req_file.py @@ -6,7 +6,6 @@ import pytest from mock import patch -from pip._vendor.six import PY2 from pretend import stub import pip._internal.req.req_file # this will be monkeypatched @@ -220,12 +219,11 @@ def test_error_message(self, line_processor): line_number=3 ) - package_name = "u'my-package=1.0'" if PY2 else "'my-package=1.0'" expected = ( - "Invalid requirement: {} " + "Invalid requirement: 'my-package=1.0' " '(from line 3 of path/requirements.txt)\n' 'Hint: = is not a valid operator. Did you mean == ?' - ).format(package_name) + ) assert str(exc.value) == expected def test_yield_line_requirement(self, line_processor): diff --git a/tests/unit/test_urls.py b/tests/unit/test_urls.py index 7428cef9ebc..9c6f75a8021 100644 --- a/tests/unit/test_urls.py +++ b/tests/unit/test_urls.py @@ -1,8 +1,8 @@ import os import sys +from urllib import request as urllib_request import pytest -from pip._vendor.six.moves.urllib import request as urllib_request from pip._internal.utils.urls import get_url_scheme, path_to_url, url_to_path diff --git a/tests/unit/test_utils_parallel.py b/tests/unit/test_utils_parallel.py index 6086dcaa08b..d5449988e3a 100644 --- a/tests/unit/test_utils_parallel.py +++ b/tests/unit/test_utils_parallel.py @@ -5,11 +5,9 @@ from math import factorial from sys import modules -from pip._vendor.six import PY2 -from pip._vendor.six.moves import map from pytest import mark -DUNDER_IMPORT = '__builtin__.__import__' if PY2 else 'builtins.__import__' +DUNDER_IMPORT = 'builtins.__import__' FUNC, ITERABLE = factorial, range(42) MAPS = 'map_multiprocess', 'map_multithread' _import = __import__ @@ -63,9 +61,8 @@ def test_lack_sem_open(name, monkeypatch): def test_have_sem_open(name, monkeypatch): """Test fallback when sem_open is available.""" monkeypatch.setattr(DUNDER_IMPORT, have_sem_open) - impl = '_map_fallback' if PY2 else '_{}'.format(name) with tmp_import_parallel() as parallel: - assert getattr(parallel, name) is getattr(parallel, impl) + assert getattr(parallel, name) is getattr(parallel, '_{}'.format(name)) @mark.parametrize('name', MAPS) diff --git a/tests/unit/test_utils_pkg_resources.py b/tests/unit/test_utils_pkg_resources.py index d113d6df124..ae7357ba1cc 100644 --- a/tests/unit/test_utils_pkg_resources.py +++ b/tests/unit/test_utils_pkg_resources.py @@ -6,7 +6,6 @@ from pip._internal.utils.packaging import get_metadata, get_requires_python from pip._internal.utils.pkg_resources import DictMetadata -from tests.lib import skip_if_python2 def test_dict_metadata_works(): @@ -45,8 +44,6 @@ def test_dict_metadata_works(): assert requires_python == get_requires_python(dist) -# Metadata is not decoded on Python 2, so no chance for error. -@skip_if_python2 def test_dict_metadata_throws_on_bad_unicode(): metadata = DictMetadata({ "METADATA": b"\xff" diff --git a/tests/unit/test_utils_wheel.py b/tests/unit/test_utils_wheel.py index cf8bd6dc3ed..abd30114800 100644 --- a/tests/unit/test_utils_wheel.py +++ b/tests/unit/test_utils_wheel.py @@ -9,7 +9,6 @@ from pip._internal.exceptions import UnsupportedWheel from pip._internal.utils import wheel from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from tests.lib import skip_if_python2 if MYPY_CHECK_RUNNING: from tests.lib.path import Path @@ -88,7 +87,6 @@ def test_wheel_metadata_fails_missing_wheel(tmpdir, zip_dir): assert "could not read" in str(e.value) -@skip_if_python2 def test_wheel_metadata_fails_on_bad_encoding(tmpdir, zip_dir): dist_info_dir = tmpdir / "simple-0.1.0.dist-info" dist_info_dir.mkdir() diff --git a/tests/unit/test_vcs_mercurial.py b/tests/unit/test_vcs_mercurial.py index 630619b8236..07224c0a4d6 100644 --- a/tests/unit/test_vcs_mercurial.py +++ b/tests/unit/test_vcs_mercurial.py @@ -2,10 +2,9 @@ Contains functional tests of the Mercurial class. """ +import configparser import os -from pip._vendor.six.moves import configparser - from pip._internal.utils.misc import hide_url from pip._internal.vcs.mercurial import Mercurial from tests.lib import need_mercurial diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 35916058a76..a97ec89da44 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -25,7 +25,7 @@ from pip._internal.utils.misc import hash_file from pip._internal.utils.unpacking import unpack_file from pip._internal.utils.wheel import pkg_resources_distribution_for_wheel -from tests.lib import DATA_DIR, assert_paths_equal, skip_if_python2 +from tests.lib import DATA_DIR, assert_paths_equal from tests.lib.wheel import make_wheel @@ -83,7 +83,7 @@ def test_get_legacy_build_wheel_path__multiple_names(caplog): [ u"pip = pip._internal.main:pip", u"pip:pip = pip._internal.main:pip", - pytest.param(u"進入點 = 套件.模組:函式", marks=skip_if_python2), + u"進入點 = 套件.模組:函式", ], ) def test_get_entrypoints(console_scripts): diff --git a/tox.ini b/tox.ini index 9c20759af3a..46f67920e1d 100644 --- a/tox.ini +++ b/tox.ini @@ -2,7 +2,7 @@ minversion = 3.4.0 envlist = docs, packaging, lint, vendoring, - py27, py35, py36, py37, py38, py39, pypy, pypy3 + py36, py37, py38, py39, pypy3 [helpers] # Wrapper for calls to pip that make sure the version being used is the From 33f48e2b72dec07f3bfdf30eff72da6b3f2f5f96 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <hugovk@users.noreply.github.com> Date: Tue, 22 Dec 2020 10:22:20 +0200 Subject: [PATCH 2807/3170] Replace more six --- src/pip/_internal/pyproject.py | 4 ++-- src/pip/_internal/req/req_install.py | 4 ++-- tests/lib/venv.py | 8 ++------ 3 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/pip/_internal/pyproject.py b/src/pip/_internal/pyproject.py index b7ca902e3a5..97fdff7669f 100644 --- a/src/pip/_internal/pyproject.py +++ b/src/pip/_internal/pyproject.py @@ -4,7 +4,7 @@ import os from collections import namedtuple -from pip._vendor import six, toml +from pip._vendor import toml from pip._vendor.packaging.requirements import InvalidRequirement, Requirement from pip._internal.exceptions import InstallationError @@ -18,7 +18,7 @@ def _is_list_of_str(obj): # type: (Any) -> bool return ( isinstance(obj, list) and - all(isinstance(item, six.string_types) for item in obj) + all(isinstance(item, str) for item in obj) ) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index e66fda226ea..067affd53ac 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -223,7 +223,7 @@ def __str__(self): if self.satisfied_by is not None: s += ' in {}'.format(display_path(self.satisfied_by.location)) if self.comes_from: - if isinstance(self.comes_from, six.string_types): + if isinstance(self.comes_from, str): comes_from = self.comes_from # type: Optional[str] else: comes_from = self.comes_from.from_path() @@ -334,7 +334,7 @@ def from_path(self): return None s = str(self.req) if self.comes_from: - if isinstance(self.comes_from, six.string_types): + if isinstance(self.comes_from, str): comes_from = self.comes_from else: comes_from = self.comes_from.from_path() diff --git a/tests/lib/venv.py b/tests/lib/venv.py index cc94e29f254..045dd78a76f 100644 --- a/tests/lib/venv.py +++ b/tests/lib/venv.py @@ -4,15 +4,12 @@ import shutil import sys import textwrap +import venv as _venv -import six import virtualenv as _virtualenv from .path import Path -if six.PY3: - import venv as _venv - class VirtualEnvironment(object): """ @@ -37,8 +34,7 @@ def _update_paths(self): self.site = Path(lib) / 'site-packages' # Workaround for https://github.com/pypa/virtualenv/issues/306 if hasattr(sys, "pypy_version_info"): - version_fmt = '{0}' if six.PY3 else '{0}.{1}' - version_dir = version_fmt.format(*sys.version_info) + version_dir = '{0}'.format(*sys.version_info) self.lib = Path(home, 'lib-python', version_dir) else: self.lib = Path(lib) From d509a27ad4e462181719f25c32ba64c2c34b68de Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <hugovk@users.noreply.github.com> Date: Tue, 22 Dec 2020 15:21:17 +0200 Subject: [PATCH 2808/3170] Review updates --- src/pip/_internal/operations/prepare.py | 12 ++++--- src/pip/_internal/pyproject.py | 4 +-- src/pip/_internal/utils/compat.py | 48 ++++--------------------- 3 files changed, 15 insertions(+), 49 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index cc9c129b28a..f1321709b86 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -166,11 +166,13 @@ def ignore(d, names): skipped += [target_basename] return skipped - kwargs = dict(ignore=ignore, symlinks=True) # type: CopytreeKwargs - - kwargs['copy_function'] = _copy2_ignoring_special_files - - shutil.copytree(source, target, **kwargs) + shutil.copytree( + source, + target, + ignore=ignore, + symlinks=True, + copy_function=_copy2_ignoring_special_files, + ) def get_file_url( diff --git a/src/pip/_internal/pyproject.py b/src/pip/_internal/pyproject.py index 97fdff7669f..ee90de12e12 100644 --- a/src/pip/_internal/pyproject.py +++ b/src/pip/_internal/pyproject.py @@ -24,9 +24,7 @@ def _is_list_of_str(obj): def make_pyproject_path(unpacked_source_directory): # type: (str) -> str - path = os.path.join(unpacked_source_directory, 'pyproject.toml') - - return path + return os.path.join(unpacked_source_directory, 'pyproject.toml') BuildSystemDetails = namedtuple('BuildSystemDetails', [ diff --git a/src/pip/_internal/utils/compat.py b/src/pip/_internal/utils/compat.py index b0ff63f51c7..0115c307db8 100644 --- a/src/pip/_internal/utils/compat.py +++ b/src/pip/_internal/utils/compat.py @@ -190,47 +190,13 @@ def expanduser(path): (sys.platform == 'cli' and os.name == 'nt')) -if hasattr(shutil, 'get_terminal_size'): - def get_terminal_size(): - # type: () -> Tuple[int, int] - """ - 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()) # type: ignore -else: - def get_terminal_size(): - # type: () -> Tuple[int, int] - """ - 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 struct - import termios - cr = struct.unpack_from( - 'hh', - fcntl.ioctl(fd, termios.TIOCGWINSZ, '12345678') - ) - except Exception: - 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: - if sys.platform != "win32": - try: - fd = os.open(os.ctermid(), os.O_RDONLY) - cr = ioctl_GWINSZ(fd) - os.close(fd) - except Exception: - pass - if not cr: - cr = (os.environ.get('LINES', 25), os.environ.get('COLUMNS', 80)) - return int(cr[1]), int(cr[0]) +def get_terminal_size(): + # type: () -> Tuple[int, int] + """ + 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()) # type: ignore # Fallback to noop_lru_cache in Python 2 From 2426744203c87de55033c94d043f79f2683909bd Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <hugovk@users.noreply.github.com> Date: Tue, 22 Dec 2020 16:38:25 +0200 Subject: [PATCH 2809/3170] "" is clearer than str() --- src/pip/_internal/cli/progress_bars.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/cli/progress_bars.py b/src/pip/_internal/cli/progress_bars.py index 4a2e5936159..968a8c7cf35 100644 --- a/src/pip/_internal/cli/progress_bars.py +++ b/src/pip/_internal/cli/progress_bars.py @@ -35,8 +35,8 @@ def _select_progress_class(preferred, fallback): # Collect all of the possible characters we want to use with the preferred # bar. characters = [ - getattr(preferred, "empty_fill", str()), - getattr(preferred, "fill", str()), + getattr(preferred, "empty_fill", ""), + getattr(preferred, "fill", ""), ] characters += list(getattr(preferred, "phases", [])) @@ -44,7 +44,7 @@ def _select_progress_class(preferred, fallback): # of the given file, if this works then we'll assume that we can use the # fancier bar and if not we'll fall back to the plaintext bar. try: - str().join(characters).encode(encoding) + "".join(characters).encode(encoding) except UnicodeEncodeError: return fallback else: From 209ca8de8f9710d7cb260a95a0791d9d39967fb9 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <hugovk@users.noreply.github.com> Date: Tue, 22 Dec 2020 16:40:01 +0200 Subject: [PATCH 2810/3170] Remove default allow_stderr_warning=False --- tests/functional/test_download.py | 1 - tests/functional/test_install.py | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/tests/functional/test_download.py b/tests/functional/test_download.py index 5cd91f5421d..72b55fda92f 100644 --- a/tests/functional/test_download.py +++ b/tests/functional/test_download.py @@ -489,7 +489,6 @@ def make_wheel_with_python_requires(script, package_name, python_requires): package_dir.joinpath('setup.py').write_text(text) script.run( 'python', 'setup.py', 'bdist_wheel', '--universal', cwd=package_dir, - allow_stderr_warning=False, ) file_name = '{}-1.0-py2.py3-none-any.whl'.format(package_name) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 3bd0f63afe3..aedd691a4e3 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1550,7 +1550,7 @@ def test_install_incompatible_python_requires_wheel(script, with_wheel): """)) script.run( 'python', 'setup.py', 'bdist_wheel', '--universal', - cwd=pkga_path, allow_stderr_warning=False, + cwd=pkga_path, ) result = script.pip('install', './pkga/dist/pkga-0.1-py2.py3-none-any.whl', expect_error=True) From 9db97546b3a027a1f57d46b3a61f85da3b7659e5 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <hugovk@users.noreply.github.com> Date: Tue, 22 Dec 2020 22:28:23 +0200 Subject: [PATCH 2811/3170] os.curdir is already a str Co-authored-by: Jon Dufresne <jon.dufresne@gmail.com> --- tests/lib/__init__.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index 2e632e30152..b7c63029a4c 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -1099,15 +1099,12 @@ def create_basic_sdist_for_package( path.parent.mkdir(exist_ok=True, parents=True) path.write_bytes(ensure_binary(files[fname])) - # The base_dir cast is required to make `shutil.make_archive()` use - # Unicode paths on Python 2, making it able to properly archive - # files with non-ASCII names. retval = script.scratch_path / archive_name generated = shutil.make_archive( retval, 'gztar', root_dir=script.temp_path, - base_dir=str(os.curdir), + base_dir=os.curdir, ) shutil.move(generated, retval) From add5cfa514740baebf9b3a58572e829c4b006b38 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <hugovk@users.noreply.github.com> Date: Tue, 22 Dec 2020 22:20:46 +0200 Subject: [PATCH 2812/3170] Replace compat shim with shutil.get_terminal_size() --- src/pip/_internal/cli/parser.py | 4 ++-- src/pip/_internal/commands/search.py | 4 ++-- src/pip/_internal/utils/compat.py | 14 ++------------ 3 files changed, 6 insertions(+), 16 deletions(-) diff --git a/src/pip/_internal/cli/parser.py b/src/pip/_internal/cli/parser.py index 5bacd47a1d3..b64e967806c 100644 --- a/src/pip/_internal/cli/parser.py +++ b/src/pip/_internal/cli/parser.py @@ -7,6 +7,7 @@ import logging import optparse +import shutil import sys import textwrap from distutils.util import strtobool @@ -15,7 +16,6 @@ from pip._internal.cli.status_codes import UNKNOWN_ERROR from pip._internal.configuration import Configuration, ConfigurationError -from pip._internal.utils.compat import get_terminal_size from pip._internal.utils.misc import redact_auth_from_url logger = logging.getLogger(__name__) @@ -28,7 +28,7 @@ def __init__(self, *args, **kwargs): # help position must be aligned with __init__.parseopts.description kwargs['max_help_position'] = 30 kwargs['indent_increment'] = 1 - kwargs['width'] = get_terminal_size()[0] - 2 + kwargs['width'] = shutil.get_terminal_size()[0] - 2 optparse.IndentedHelpFormatter.__init__(self, *args, **kwargs) def format_option_strings(self, option): diff --git a/src/pip/_internal/commands/search.py b/src/pip/_internal/commands/search.py index 146d653e55f..f382ddc6516 100644 --- a/src/pip/_internal/commands/search.py +++ b/src/pip/_internal/commands/search.py @@ -1,6 +1,7 @@ from __future__ import absolute_import import logging +import shutil import sys import textwrap from collections import OrderedDict @@ -18,7 +19,6 @@ from pip._internal.exceptions import CommandError from pip._internal.models.index import PyPI from pip._internal.network.xmlrpc import PipXmlrpcTransport -from pip._internal.utils.compat import get_terminal_size from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import get_distribution, write_output from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -64,7 +64,7 @@ def run(self, options, args): terminal_width = None if sys.stdout.isatty(): - terminal_width = get_terminal_size()[0] + terminal_width = shutil.get_terminal_size()[0] print_results(hits, terminal_width=terminal_width) if pypi_hits: diff --git a/src/pip/_internal/utils/compat.py b/src/pip/_internal/utils/compat.py index 0115c307db8..e6ddbcb5f8e 100644 --- a/src/pip/_internal/utils/compat.py +++ b/src/pip/_internal/utils/compat.py @@ -11,13 +11,12 @@ import locale import logging import os -import shutil import sys from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Callable, Optional, Protocol, Text, Tuple, TypeVar, Union + from typing import Callable, Optional, Protocol, Text, TypeVar, Union # Used in the @lru_cache polyfill. F = TypeVar('F') @@ -40,7 +39,7 @@ def __call__(self, maxsize=None): __all__ = [ "ipaddress", "console_to_str", - "get_path_uid", "stdlib_pkgs", "WINDOWS", "get_terminal_size", + "get_path_uid", "stdlib_pkgs", "WINDOWS", ] @@ -190,15 +189,6 @@ def expanduser(path): (sys.platform == 'cli' and os.name == 'nt')) -def get_terminal_size(): - # type: () -> Tuple[int, int] - """ - 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()) # type: ignore - - # Fallback to noop_lru_cache in Python 2 # TODO: this can be removed when python 2 support is dropped! def noop_lru_cache(maxsize=None): From 1f3b0300df586cd5c97444e13f61f20541e3f5c3 Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Wed, 23 Dec 2020 08:41:48 -0800 Subject: [PATCH 2813/3170] Remove "universal" from "bdist_wheel" configuration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Since dropping Python 2 support, the wheel is no longer universal. See the wheel docs: https://wheel.readthedocs.io/en/stable/user_guide.html#building-wheels > If your project … is expected to work on both Python 2 and 3, you will > want to tell wheel to produce universal wheels by adding this to your > setup.cfg file: --- setup.cfg | 3 --- 1 file changed, 3 deletions(-) diff --git a/setup.cfg b/setup.cfg index 84a959a32d4..10210196de5 100644 --- a/setup.cfg +++ b/setup.cfg @@ -101,8 +101,5 @@ exclude_lines = # Can be set to exclude e.g. `if PY2:` on Python 3 ${PIP_CI_COVERAGE_EXCLUDES} -[bdist_wheel] -universal = 1 - [metadata] license_file = LICENSE.txt From 53234e578fcf08deb6518dcaed48d97e1e2b82b9 Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Wed, 23 Dec 2020 15:42:48 -0800 Subject: [PATCH 2814/3170] Remove obsolete "# type: ignore" comments Obsolete since dropping Python 2 support. Add the mypy setting "warn_unused_ignores = True" to catch these earlier. --- setup.cfg | 1 + src/pip/_internal/cli/main_parser.py | 2 +- src/pip/_internal/commands/debug.py | 15 ++------------- src/pip/_internal/operations/install/wheel.py | 3 +-- src/pip/_internal/req/req_file.py | 6 ++---- src/pip/_internal/utils/unpacking.py | 6 ++---- 6 files changed, 9 insertions(+), 24 deletions(-) diff --git a/setup.cfg b/setup.cfg index 84a959a32d4..c28a167bd59 100644 --- a/setup.cfg +++ b/setup.cfg @@ -42,6 +42,7 @@ follow_imports = silent ignore_missing_imports = True disallow_untyped_defs = True disallow_any_generics = True +warn_unused_ignores = True [mypy-pip/_vendor/*] follow_imports = skip diff --git a/src/pip/_internal/cli/main_parser.py b/src/pip/_internal/cli/main_parser.py index ba3cf68aafb..6d69e82f0cb 100644 --- a/src/pip/_internal/cli/main_parser.py +++ b/src/pip/_internal/cli/main_parser.py @@ -68,7 +68,7 @@ def parse_command(args): # --version if general_options.version: - sys.stdout.write(parser.version) # type: ignore + sys.stdout.write(parser.version) sys.stdout.write(os.linesep) sys.exit() diff --git a/src/pip/_internal/commands/debug.py b/src/pip/_internal/commands/debug.py index 1b65c43065b..9b9808e62fb 100644 --- a/src/pip/_internal/commands/debug.py +++ b/src/pip/_internal/commands/debug.py @@ -36,12 +36,7 @@ def show_value(name, value): def show_sys_implementation(): # type: () -> None logger.info('sys.implementation:') - if hasattr(sys, 'implementation'): - implementation = sys.implementation # type: ignore - implementation_name = implementation.name - else: - implementation_name = '' - + implementation_name = sys.implementation.name with indent_log(): show_value('name', implementation_name) @@ -88,13 +83,7 @@ def get_vendor_version_from_module(module_name): if not version: # Try to find version in debundled module info - # The type for module.__file__ is Optional[str] in - # Python 2, and str in Python 3. The type: ignore is - # added to account for Python 2, instead of a cast - # and should be removed once we drop Python 2 support - pkg_set = pkg_resources.WorkingSet( - [os.path.dirname(module.__file__)] # type: ignore - ) + pkg_set = pkg_resources.WorkingSet([os.path.dirname(module.__file__)]) package = pkg_set.find(pkg_resources.Requirement.parse(module_name)) version = getattr(package, 'version', None) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 7b7d48661ce..a439dffa9ff 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -92,8 +92,7 @@ def rehash(path, blocksize=1 << 20): digest = 'sha256=' + urlsafe_b64encode( h.digest() ).decode('latin1').rstrip('=') - # unicode/str python2 issues - return (digest, str(length)) # type: ignore + return (digest, str(length)) def csv_io_kwargs(mode): diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index ae891ce7aab..4b86eac801e 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -409,9 +409,7 @@ def parse_line(line): args_str, options_str = break_args_options(line) - # https://github.com/python/mypy/issues/1174 - opts, _ = parser.parse_args( - shlex.split(options_str), defaults) # type: ignore + opts, _ = parser.parse_args(shlex.split(options_str), defaults) return args_str, opts @@ -433,7 +431,7 @@ def break_args_options(line): else: args.append(token) options.pop(0) - return ' '.join(args), ' '.join(options) # type: ignore + return ' '.join(args), ' '.join(options) class OptionParsingError(Exception): diff --git a/src/pip/_internal/utils/unpacking.py b/src/pip/_internal/utils/unpacking.py index 620f31ebb74..b1c2bc3a471 100644 --- a/src/pip/_internal/utils/unpacking.py +++ b/src/pip/_internal/utils/unpacking.py @@ -192,8 +192,7 @@ def untar_file(filename, location): for member in tar.getmembers(): fn = member.name if leading: - # https://github.com/python/mypy/issues/1174 - fn = split_leading_dir(fn)[1] # type: ignore + fn = split_leading_dir(fn)[1] path = os.path.join(location, fn) if not is_within_directory(location, path): message = ( @@ -234,8 +233,7 @@ def untar_file(filename, location): shutil.copyfileobj(fp, destfp) fp.close() # Update the timestamp (useful for cython compiled files) - # https://github.com/python/typeshed/issues/2673 - tar.utime(member, path) # type: ignore + tar.utime(member, path) # member have any execute permissions for user/group/world? if member.mode & 0o111: set_extracted_file_to_default_mode_plus_executable(path) From f32adaf09b68ab4cf149dd871729d83b7d5083af Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Thu, 24 Dec 2020 08:31:35 -0800 Subject: [PATCH 2815/3170] Remove __future__ imports Unnecessary since dropping Python 2. --- news/3ba38d39-9189-4bc3-bc87-bf598f1c1064.trivial.rst | 0 src/pip/__main__.py | 2 -- src/pip/_internal/cli/base_command.py | 2 -- src/pip/_internal/cli/cmdoptions.py | 2 -- src/pip/_internal/cli/main.py | 2 -- src/pip/_internal/cli/parser.py | 2 -- src/pip/_internal/cli/progress_bars.py | 2 -- src/pip/_internal/cli/spinners.py | 2 -- src/pip/_internal/cli/status_codes.py | 2 -- src/pip/_internal/commands/__init__.py | 2 -- src/pip/_internal/commands/cache.py | 2 -- src/pip/_internal/commands/completion.py | 2 -- src/pip/_internal/commands/debug.py | 2 -- src/pip/_internal/commands/download.py | 2 -- src/pip/_internal/commands/freeze.py | 2 -- src/pip/_internal/commands/hash.py | 2 -- src/pip/_internal/commands/help.py | 2 -- src/pip/_internal/commands/install.py | 2 -- src/pip/_internal/commands/list.py | 2 -- src/pip/_internal/commands/search.py | 2 -- src/pip/_internal/commands/show.py | 2 -- src/pip/_internal/commands/uninstall.py | 2 -- src/pip/_internal/commands/wheel.py | 2 -- src/pip/_internal/exceptions.py | 2 -- src/pip/_internal/index/package_finder.py | 2 -- src/pip/_internal/locations.py | 2 -- src/pip/_internal/operations/freeze.py | 2 -- src/pip/_internal/operations/install/wheel.py | 2 -- src/pip/_internal/pyproject.py | 2 -- src/pip/_internal/req/__init__.py | 2 -- src/pip/_internal/req/req_file.py | 2 -- src/pip/_internal/req/req_install.py | 2 -- src/pip/_internal/req/req_set.py | 2 -- src/pip/_internal/req/req_tracker.py | 2 -- src/pip/_internal/req/req_uninstall.py | 2 -- src/pip/_internal/self_outdated_check.py | 2 -- src/pip/_internal/utils/appdirs.py | 2 -- src/pip/_internal/utils/compat.py | 2 -- src/pip/_internal/utils/compatibility_tags.py | 2 -- src/pip/_internal/utils/datetime.py | 2 -- src/pip/_internal/utils/deprecation.py | 2 -- src/pip/_internal/utils/glibc.py | 2 -- src/pip/_internal/utils/hashes.py | 2 -- src/pip/_internal/utils/logging.py | 2 -- src/pip/_internal/utils/misc.py | 2 -- src/pip/_internal/utils/packaging.py | 2 -- src/pip/_internal/utils/subprocess.py | 2 -- src/pip/_internal/utils/temp_dir.py | 2 -- src/pip/_internal/utils/unpacking.py | 2 -- src/pip/_internal/utils/virtualenv.py | 2 -- src/pip/_internal/utils/wheel.py | 2 -- src/pip/_internal/vcs/bazaar.py | 2 -- src/pip/_internal/vcs/git.py | 2 -- src/pip/_internal/vcs/mercurial.py | 2 -- src/pip/_internal/vcs/subversion.py | 2 -- src/pip/_internal/vcs/versioncontrol.py | 2 -- tests/lib/__init__.py | 2 -- tests/lib/git_submodule_helpers.py | 2 -- tests/lib/local_repos.py | 2 -- tests/lib/path.py | 2 -- tests/lib/test_lib.py | 2 -- tests/lib/venv.py | 2 -- 62 files changed, 122 deletions(-) create mode 100644 news/3ba38d39-9189-4bc3-bc87-bf598f1c1064.trivial.rst diff --git a/news/3ba38d39-9189-4bc3-bc87-bf598f1c1064.trivial.rst b/news/3ba38d39-9189-4bc3-bc87-bf598f1c1064.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/__main__.py b/src/pip/__main__.py index 7c2505fa5bd..ea738bfc24f 100644 --- a/src/pip/__main__.py +++ b/src/pip/__main__.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - import os import sys diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index 190c4d86e6c..adfaab3ff70 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -1,7 +1,5 @@ """Base Command class, and related routines""" -from __future__ import absolute_import, print_function - import logging import logging.config import optparse diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index 3543ed48bb3..8f427d6b587 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -10,8 +10,6 @@ # The following comment should be removed at some point in the future. # mypy: strict-optional=False -from __future__ import absolute_import - import os import textwrap import warnings diff --git a/src/pip/_internal/cli/main.py b/src/pip/_internal/cli/main.py index 172f30dd5bf..9a5fbb1f1ec 100644 --- a/src/pip/_internal/cli/main.py +++ b/src/pip/_internal/cli/main.py @@ -1,7 +1,5 @@ """Primary application entrypoint. """ -from __future__ import absolute_import - import locale import logging import os diff --git a/src/pip/_internal/cli/parser.py b/src/pip/_internal/cli/parser.py index b64e967806c..e56193c3d2e 100644 --- a/src/pip/_internal/cli/parser.py +++ b/src/pip/_internal/cli/parser.py @@ -3,8 +3,6 @@ # The following comment should be removed at some point in the future. # mypy: disallow-untyped-defs=False -from __future__ import absolute_import - import logging import optparse import shutil diff --git a/src/pip/_internal/cli/progress_bars.py b/src/pip/_internal/cli/progress_bars.py index 968a8c7cf35..1e40d6d9e2d 100644 --- a/src/pip/_internal/cli/progress_bars.py +++ b/src/pip/_internal/cli/progress_bars.py @@ -1,5 +1,3 @@ -from __future__ import division - import itertools import sys from signal import SIGINT, default_int_handler, signal diff --git a/src/pip/_internal/cli/spinners.py b/src/pip/_internal/cli/spinners.py index 65c3c23d742..171d3a02d2f 100644 --- a/src/pip/_internal/cli/spinners.py +++ b/src/pip/_internal/cli/spinners.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import, division - import contextlib import itertools import logging diff --git a/src/pip/_internal/cli/status_codes.py b/src/pip/_internal/cli/status_codes.py index 275360a3175..5e29502cddf 100644 --- a/src/pip/_internal/cli/status_codes.py +++ b/src/pip/_internal/cli/status_codes.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - SUCCESS = 0 ERROR = 1 UNKNOWN_ERROR = 2 diff --git a/src/pip/_internal/commands/__init__.py b/src/pip/_internal/commands/__init__.py index 4f0c4ba3ab9..f0554b655dc 100644 --- a/src/pip/_internal/commands/__init__.py +++ b/src/pip/_internal/commands/__init__.py @@ -9,8 +9,6 @@ # return type of difflib.get_close_matches to be reported # as List[Sequence[str]] whereas it should have been List[str] -from __future__ import absolute_import - import importlib from collections import OrderedDict, namedtuple diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index ec21be68fb5..80e668faea9 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - import logging import os import textwrap diff --git a/src/pip/_internal/commands/completion.py b/src/pip/_internal/commands/completion.py index b19f1ed1a56..2c19d5686d2 100644 --- a/src/pip/_internal/commands/completion.py +++ b/src/pip/_internal/commands/completion.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - import sys import textwrap diff --git a/src/pip/_internal/commands/debug.py b/src/pip/_internal/commands/debug.py index 9b9808e62fb..747a1c1758b 100644 --- a/src/pip/_internal/commands/debug.py +++ b/src/pip/_internal/commands/debug.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - import locale import logging import os diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index 7509397240b..0f09fcc0eed 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - import logging import os diff --git a/src/pip/_internal/commands/freeze.py b/src/pip/_internal/commands/freeze.py index 4d1ce69a124..d9caa33516e 100644 --- a/src/pip/_internal/commands/freeze.py +++ b/src/pip/_internal/commands/freeze.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - import sys from pip._internal.cache import WheelCache diff --git a/src/pip/_internal/commands/hash.py b/src/pip/_internal/commands/hash.py index 37831c39522..db68f6ce7bc 100644 --- a/src/pip/_internal/commands/hash.py +++ b/src/pip/_internal/commands/hash.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - import hashlib import logging import sys diff --git a/src/pip/_internal/commands/help.py b/src/pip/_internal/commands/help.py index 2ab2b6d8f25..a6c25478e0f 100644 --- a/src/pip/_internal/commands/help.py +++ b/src/pip/_internal/commands/help.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from pip._internal.cli.base_command import Command from pip._internal.cli.status_codes import SUCCESS from pip._internal.exceptions import CommandError diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index a4e10f260a2..a9a44787273 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - import errno import logging import operator diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index 3f29e48b4fe..89cfb625e59 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - import json import logging diff --git a/src/pip/_internal/commands/search.py b/src/pip/_internal/commands/search.py index f382ddc6516..a03b2a633e2 100644 --- a/src/pip/_internal/commands/search.py +++ b/src/pip/_internal/commands/search.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - import logging import shutil import sys diff --git a/src/pip/_internal/commands/show.py b/src/pip/_internal/commands/show.py index b0b3f3abdcc..a6363cfd0a2 100644 --- a/src/pip/_internal/commands/show.py +++ b/src/pip/_internal/commands/show.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - import logging import os from email.parser import FeedParser diff --git a/src/pip/_internal/commands/uninstall.py b/src/pip/_internal/commands/uninstall.py index 3371fe47ff1..6dc96c3d630 100644 --- a/src/pip/_internal/commands/uninstall.py +++ b/src/pip/_internal/commands/uninstall.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - from pip._vendor.packaging.utils import canonicalize_name from pip._internal.cli.base_command import Command diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index 39fd2bf8128..f9be310960f 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -1,7 +1,5 @@ # -*- coding: utf-8 -*- -from __future__ import absolute_import - import logging import os import shutil diff --git a/src/pip/_internal/exceptions.py b/src/pip/_internal/exceptions.py index 870205584a2..b60bbbd21a2 100644 --- a/src/pip/_internal/exceptions.py +++ b/src/pip/_internal/exceptions.py @@ -1,7 +1,5 @@ """Exceptions used throughout package""" -from __future__ import absolute_import - from itertools import chain, groupby, repeat from pip._internal.utils.typing import MYPY_CHECK_RUNNING diff --git a/src/pip/_internal/index/package_finder.py b/src/pip/_internal/index/package_finder.py index 9f39631dde2..4fdecd226ec 100644 --- a/src/pip/_internal/index/package_finder.py +++ b/src/pip/_internal/index/package_finder.py @@ -3,8 +3,6 @@ # The following comment should be removed at some point in the future. # mypy: strict-optional=False -from __future__ import absolute_import - import logging import re diff --git a/src/pip/_internal/locations.py b/src/pip/_internal/locations.py index 35a4512b4b1..7d549dcef1e 100644 --- a/src/pip/_internal/locations.py +++ b/src/pip/_internal/locations.py @@ -3,8 +3,6 @@ # The following comment should be removed at some point in the future. # mypy: strict-optional=False -from __future__ import absolute_import - import os import os.path import platform diff --git a/src/pip/_internal/operations/freeze.py b/src/pip/_internal/operations/freeze.py index ba885afd65e..3529c55edc2 100644 --- a/src/pip/_internal/operations/freeze.py +++ b/src/pip/_internal/operations/freeze.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - import collections import logging import os diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index a439dffa9ff..6dc68089e91 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -1,8 +1,6 @@ """Support for installing and building the "wheel" binary package format. """ -from __future__ import absolute_import - import collections import compileall import contextlib diff --git a/src/pip/_internal/pyproject.py b/src/pip/_internal/pyproject.py index ee90de12e12..38192b8507f 100644 --- a/src/pip/_internal/pyproject.py +++ b/src/pip/_internal/pyproject.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - import io import os from collections import namedtuple diff --git a/src/pip/_internal/req/__init__.py b/src/pip/_internal/req/__init__.py index 8568d3f8b6e..8bdec4fc8c5 100644 --- a/src/pip/_internal/req/__init__.py +++ b/src/pip/_internal/req/__init__.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - import collections import logging diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index 4b86eac801e..8c34bc149cd 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -2,8 +2,6 @@ Requirements file parsing """ -from __future__ import absolute_import - import optparse import os import re diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 067affd53ac..92a77f87bc8 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -1,8 +1,6 @@ # The following comment should be removed at some point in the future. # mypy: strict-optional=False -from __future__ import absolute_import - import logging import os import shutil diff --git a/src/pip/_internal/req/req_set.py b/src/pip/_internal/req/req_set.py index c9ea3be5ddd..42c76820d21 100644 --- a/src/pip/_internal/req/req_set.py +++ b/src/pip/_internal/req/req_set.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - import logging from collections import OrderedDict diff --git a/src/pip/_internal/req/req_tracker.py b/src/pip/_internal/req/req_tracker.py index 7379c307b31..cfbfbb10f43 100644 --- a/src/pip/_internal/req/req_tracker.py +++ b/src/pip/_internal/req/req_tracker.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - import contextlib import errno import hashlib diff --git a/src/pip/_internal/req/req_uninstall.py b/src/pip/_internal/req/req_uninstall.py index b70d5e7f43e..5e62b832897 100644 --- a/src/pip/_internal/req/req_uninstall.py +++ b/src/pip/_internal/req/req_uninstall.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - import csv import functools import logging diff --git a/src/pip/_internal/self_outdated_check.py b/src/pip/_internal/self_outdated_check.py index c2d166b1844..a44c00cda68 100644 --- a/src/pip/_internal/self_outdated_check.py +++ b/src/pip/_internal/self_outdated_check.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - import datetime import hashlib import json diff --git a/src/pip/_internal/utils/appdirs.py b/src/pip/_internal/utils/appdirs.py index 3989ed31c3a..a0a37be8723 100644 --- a/src/pip/_internal/utils/appdirs.py +++ b/src/pip/_internal/utils/appdirs.py @@ -6,8 +6,6 @@ and eventually drop this after all usages are changed. """ -from __future__ import absolute_import - import os from pip._vendor import appdirs as _appdirs diff --git a/src/pip/_internal/utils/compat.py b/src/pip/_internal/utils/compat.py index e6ddbcb5f8e..31afee716fa 100644 --- a/src/pip/_internal/utils/compat.py +++ b/src/pip/_internal/utils/compat.py @@ -4,8 +4,6 @@ # The following comment should be removed at some point in the future. # mypy: disallow-untyped-defs=False -from __future__ import absolute_import, division - import codecs import functools import locale diff --git a/src/pip/_internal/utils/compatibility_tags.py b/src/pip/_internal/utils/compatibility_tags.py index 6780f9d9d64..4bf5aaa9369 100644 --- a/src/pip/_internal/utils/compatibility_tags.py +++ b/src/pip/_internal/utils/compatibility_tags.py @@ -1,8 +1,6 @@ """Generate and work with PEP 425 Compatibility Tags. """ -from __future__ import absolute_import - import re from pip._vendor.packaging.tags import ( diff --git a/src/pip/_internal/utils/datetime.py b/src/pip/_internal/utils/datetime.py index 4d0503c2f33..b638646c8bb 100644 --- a/src/pip/_internal/utils/datetime.py +++ b/src/pip/_internal/utils/datetime.py @@ -1,8 +1,6 @@ """For when pip wants to check the date or time. """ -from __future__ import absolute_import - import datetime diff --git a/src/pip/_internal/utils/deprecation.py b/src/pip/_internal/utils/deprecation.py index 2f20cfd49d3..534d3fde86c 100644 --- a/src/pip/_internal/utils/deprecation.py +++ b/src/pip/_internal/utils/deprecation.py @@ -5,8 +5,6 @@ # The following comment should be removed at some point in the future. # mypy: disallow-untyped-defs=False -from __future__ import absolute_import - import logging import warnings diff --git a/src/pip/_internal/utils/glibc.py b/src/pip/_internal/utils/glibc.py index 36104244138..819979d8001 100644 --- a/src/pip/_internal/utils/glibc.py +++ b/src/pip/_internal/utils/glibc.py @@ -1,8 +1,6 @@ # The following comment should be removed at some point in the future. # mypy: strict-optional=False -from __future__ import absolute_import - import os import sys diff --git a/src/pip/_internal/utils/hashes.py b/src/pip/_internal/utils/hashes.py index 53da6636e6d..30d0498f39c 100644 --- a/src/pip/_internal/utils/hashes.py +++ b/src/pip/_internal/utils/hashes.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - import hashlib from pip._internal.exceptions import HashMismatch, HashMissing, InstallationError diff --git a/src/pip/_internal/utils/logging.py b/src/pip/_internal/utils/logging.py index 44c9beabd2d..fd9ff6561f4 100644 --- a/src/pip/_internal/utils/logging.py +++ b/src/pip/_internal/utils/logging.py @@ -1,8 +1,6 @@ # The following comment should be removed at some point in the future. # mypy: disallow-untyped-defs=False -from __future__ import absolute_import - import contextlib import errno import logging diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index aa428da0564..604494e0c96 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -2,8 +2,6 @@ # mypy: strict-optional=False # mypy: disallow-untyped-defs=False -from __future__ import absolute_import - import contextlib import errno import getpass diff --git a/src/pip/_internal/utils/packaging.py b/src/pip/_internal/utils/packaging.py index 27fd204234f..fae06070c87 100644 --- a/src/pip/_internal/utils/packaging.py +++ b/src/pip/_internal/utils/packaging.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - import logging from email.parser import FeedParser diff --git a/src/pip/_internal/utils/subprocess.py b/src/pip/_internal/utils/subprocess.py index 85b92c47923..4a5cce469de 100644 --- a/src/pip/_internal/utils/subprocess.py +++ b/src/pip/_internal/utils/subprocess.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - import logging import os import shlex diff --git a/src/pip/_internal/utils/temp_dir.py b/src/pip/_internal/utils/temp_dir.py index 371958c9311..dc369fb6852 100644 --- a/src/pip/_internal/utils/temp_dir.py +++ b/src/pip/_internal/utils/temp_dir.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - import errno import itertools import logging diff --git a/src/pip/_internal/utils/unpacking.py b/src/pip/_internal/utils/unpacking.py index b1c2bc3a471..fbfa8f50672 100644 --- a/src/pip/_internal/utils/unpacking.py +++ b/src/pip/_internal/utils/unpacking.py @@ -1,8 +1,6 @@ """Utilities related archives. """ -from __future__ import absolute_import - import logging import os import shutil diff --git a/src/pip/_internal/utils/virtualenv.py b/src/pip/_internal/utils/virtualenv.py index 4a7812873b3..b387ec0b082 100644 --- a/src/pip/_internal/utils/virtualenv.py +++ b/src/pip/_internal/utils/virtualenv.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - import io import logging import os diff --git a/src/pip/_internal/utils/wheel.py b/src/pip/_internal/utils/wheel.py index 6be61371e55..4371e64b4a9 100644 --- a/src/pip/_internal/utils/wheel.py +++ b/src/pip/_internal/utils/wheel.py @@ -1,8 +1,6 @@ """Support functions for working with wheel files. """ -from __future__ import absolute_import - import logging from email.parser import Parser from zipfile import BadZipFile, ZipFile diff --git a/src/pip/_internal/vcs/bazaar.py b/src/pip/_internal/vcs/bazaar.py index 3a269a64774..e1649cc7261 100644 --- a/src/pip/_internal/vcs/bazaar.py +++ b/src/pip/_internal/vcs/bazaar.py @@ -1,8 +1,6 @@ # The following comment should be removed at some point in the future. # mypy: disallow-untyped-defs=False -from __future__ import absolute_import - import logging import os from urllib import parse as urllib_parse diff --git a/src/pip/_internal/vcs/git.py b/src/pip/_internal/vcs/git.py index 98dc3046e50..772a160be97 100644 --- a/src/pip/_internal/vcs/git.py +++ b/src/pip/_internal/vcs/git.py @@ -1,8 +1,6 @@ # The following comment should be removed at some point in the future. # mypy: disallow-untyped-defs=False -from __future__ import absolute_import - import logging import os.path import re diff --git a/src/pip/_internal/vcs/mercurial.py b/src/pip/_internal/vcs/mercurial.py index 34a045f4c60..1dded808893 100644 --- a/src/pip/_internal/vcs/mercurial.py +++ b/src/pip/_internal/vcs/mercurial.py @@ -1,8 +1,6 @@ # The following comment should be removed at some point in the future. # mypy: disallow-untyped-defs=False -from __future__ import absolute_import - import configparser import logging import os diff --git a/src/pip/_internal/vcs/subversion.py b/src/pip/_internal/vcs/subversion.py index eae09c19610..dcdbbcdd54b 100644 --- a/src/pip/_internal/vcs/subversion.py +++ b/src/pip/_internal/vcs/subversion.py @@ -1,8 +1,6 @@ # The following comment should be removed at some point in the future. # mypy: disallow-untyped-defs=False -from __future__ import absolute_import - import logging import os import re diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index 61bf8ce34c6..48465ec8dff 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -1,7 +1,5 @@ """Handles all VCS (version control) support""" -from __future__ import absolute_import - import errno import logging import os diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index b7c63029a4c..c8a885aea7e 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - import os import re import shutil diff --git a/tests/lib/git_submodule_helpers.py b/tests/lib/git_submodule_helpers.py index 34295a05dc9..494d329cac1 100644 --- a/tests/lib/git_submodule_helpers.py +++ b/tests/lib/git_submodule_helpers.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - import textwrap from tests.lib import _create_main_file, _git_commit diff --git a/tests/lib/local_repos.py b/tests/lib/local_repos.py index 6899677eeda..c93d1670e90 100644 --- a/tests/lib/local_repos.py +++ b/tests/lib/local_repos.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - import os import subprocess from urllib import request as urllib_request diff --git a/tests/lib/path.py b/tests/lib/path.py index ec4f1e37c5b..3bd5d319f35 100644 --- a/tests/lib/path.py +++ b/tests/lib/path.py @@ -1,8 +1,6 @@ # flake8: noqa # -*- coding: utf-8 -*- # Author: Aziz Köksal -from __future__ import absolute_import - import glob import os diff --git a/tests/lib/test_lib.py b/tests/lib/test_lib.py index 9c00e9d1f0c..655e0bdeea9 100644 --- a/tests/lib/test_lib.py +++ b/tests/lib/test_lib.py @@ -1,6 +1,4 @@ """Test the test support.""" -from __future__ import absolute_import - import filecmp import re import sys diff --git a/tests/lib/venv.py b/tests/lib/venv.py index 045dd78a76f..c5652fecf42 100644 --- a/tests/lib/venv.py +++ b/tests/lib/venv.py @@ -1,5 +1,3 @@ -from __future__ import absolute_import - import compileall import shutil import sys From a61586cdcf1d9c5427e856ee86c74493fd952fae Mon Sep 17 00:00:00 2001 From: Richard Si <63936253+ichard26@users.noreply.github.com> Date: Thu, 24 Dec 2020 17:48:12 -0500 Subject: [PATCH 2816/3170] Fix broken email link in docs feedback banners 'docs-feedback+ux/pip.pypa.io@pypa.io' isn't a valid email address, so I replaced it with the right email address according to this GitHub comment: https://github.com/pypa/pip/issues/8783#issuecomment-685823974 --- docs/docs_feedback_sphinxext.py | 2 +- docs/html/conf.py | 2 +- news/9343.doc.rst | 1 + 3 files changed, 3 insertions(+), 2 deletions(-) create mode 100644 news/9343.doc.rst diff --git a/docs/docs_feedback_sphinxext.py b/docs/docs_feedback_sphinxext.py index 15da4177766..a8ab94e5cbd 100644 --- a/docs/docs_feedback_sphinxext.py +++ b/docs/docs_feedback_sphinxext.py @@ -141,7 +141,7 @@ def setup(app: Sphinx) -> Dict[str, Union[bool, str]]: ) app.add_config_value( 'docs_feedback_email', - default='Docs UX Team <docs-feedback+ux/pip.pypa.io@pypa.io>', + default='Docs UX Team <docs-feedback@pypa.io>', rebuild=rebuild_trigger, ) app.add_config_value( diff --git a/docs/html/conf.py b/docs/html/conf.py index 7983187e9a3..b0063fce9b2 100644 --- a/docs/html/conf.py +++ b/docs/html/conf.py @@ -307,7 +307,7 @@ def to_document_name(path, base_dir): # NOTE: 'important', 'note', 'tip', 'warning' or 'admonition'. docs_feedback_admonition_type = 'important' docs_feedback_big_doc_lines = 50 # bigger docs will have a banner on top -docs_feedback_email = 'Docs UX Team <docs-feedback+ux/pip.pypa.io@pypa.io>' +docs_feedback_email = 'Docs UX Team <docs-feedback@pypa.io>' docs_feedback_excluded_documents = { # these won't have any banners 'news', 'reference/index', } diff --git a/news/9343.doc.rst b/news/9343.doc.rst new file mode 100644 index 00000000000..1e4f91aec4c --- /dev/null +++ b/news/9343.doc.rst @@ -0,0 +1 @@ +Fix broken email link in docs feedback banners. From c148bcc1aa4025a9dae5bcdabe1a1e01e6df28e5 Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Thu, 24 Dec 2020 15:00:05 -0800 Subject: [PATCH 2817/3170] Use short Python3 super() syntax --- ...ec6e91c-91d6-475c-959a-83485cafa4b2.trivial.rst | 0 src/pip/_internal/cache.py | 14 ++++---------- src/pip/_internal/cli/base_command.py | 2 +- src/pip/_internal/cli/command_context.py | 2 +- src/pip/_internal/cli/parser.py | 6 +++--- src/pip/_internal/cli/progress_bars.py | 14 ++++---------- src/pip/_internal/cli/req_command.py | 4 ++-- src/pip/_internal/configuration.py | 2 +- src/pip/_internal/distributions/base.py | 2 +- src/pip/_internal/exceptions.py | 5 ++--- src/pip/_internal/index/collector.py | 2 +- src/pip/_internal/models/candidate.py | 2 +- src/pip/_internal/models/index.py | 2 +- src/pip/_internal/models/link.py | 2 +- src/pip/_internal/network/cache.py | 2 +- src/pip/_internal/network/session.py | 12 ++++-------- src/pip/_internal/network/xmlrpc.py | 2 +- src/pip/_internal/operations/install/wheel.py | 4 ++-- src/pip/_internal/operations/prepare.py | 2 +- src/pip/_internal/resolution/legacy/resolver.py | 2 +- .../_internal/resolution/resolvelib/candidates.py | 4 ++-- .../_internal/resolution/resolvelib/resolver.py | 2 +- src/pip/_internal/utils/hashes.py | 2 +- src/pip/_internal/utils/logging.py | 10 +++++----- src/pip/_internal/utils/temp_dir.py | 4 ++-- src/pip/_internal/utils/wheel.py | 4 ++-- src/pip/_internal/vcs/bazaar.py | 4 ++-- src/pip/_internal/vcs/git.py | 6 +++--- src/pip/_internal/vcs/mercurial.py | 2 +- src/pip/_internal/vcs/subversion.py | 6 +++--- src/pip/_internal/vcs/versioncontrol.py | 2 +- tests/lib/__init__.py | 8 ++++---- tests/lib/server.py | 2 +- tests/unit/test_base_command.py | 4 ++-- tests/unit/test_format_control.py | 2 +- 35 files changed, 64 insertions(+), 81 deletions(-) create mode 100644 news/4ec6e91c-91d6-475c-959a-83485cafa4b2.trivial.rst diff --git a/news/4ec6e91c-91d6-475c-959a-83485cafa4b2.trivial.rst b/news/4ec6e91c-91d6-475c-959a-83485cafa4b2.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/cache.py b/src/pip/_internal/cache.py index def8dd64a18..b02c82a3c30 100644 --- a/src/pip/_internal/cache.py +++ b/src/pip/_internal/cache.py @@ -46,7 +46,7 @@ class Cache(object): def __init__(self, cache_dir, format_control, allowed_formats): # type: (str, FormatControl, Set[str]) -> None - super(Cache, self).__init__() + super().__init__() assert not cache_dir or os.path.isabs(cache_dir) self.cache_dir = cache_dir or None self.format_control = format_control @@ -175,9 +175,7 @@ class SimpleWheelCache(Cache): def __init__(self, cache_dir, format_control): # type: (str, FormatControl) -> None - super(SimpleWheelCache, self).__init__( - cache_dir, format_control, {"binary"} - ) + super().__init__(cache_dir, format_control, {"binary"}) def get_path_for_link_legacy(self, link): # type: (Link) -> str @@ -262,9 +260,7 @@ def __init__(self, format_control): globally_managed=True, ) - super(EphemWheelCache, self).__init__( - self._temp_dir.path, format_control - ) + super().__init__(self._temp_dir.path, format_control) class CacheEntry(object): @@ -286,9 +282,7 @@ class WheelCache(Cache): def __init__(self, cache_dir, format_control): # type: (str, FormatControl) -> None - super(WheelCache, self).__init__( - cache_dir, format_control, {'binary'} - ) + super().__init__(cache_dir, format_control, {'binary'}) self._wheel_cache = SimpleWheelCache(cache_dir, format_control) self._ephem_cache = EphemWheelCache(format_control) diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index adfaab3ff70..6a3f3838263 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -52,7 +52,7 @@ class Command(CommandContextMixIn): def __init__(self, name, summary, isolated=False): # type: (str, str, bool) -> None - super(Command, self).__init__() + super().__init__() parser_kw = { 'usage': self.usage, 'prog': '{} {}'.format(get_prog(), name), diff --git a/src/pip/_internal/cli/command_context.py b/src/pip/_internal/cli/command_context.py index 669c777749d..7ee2d24e324 100644 --- a/src/pip/_internal/cli/command_context.py +++ b/src/pip/_internal/cli/command_context.py @@ -13,7 +13,7 @@ class CommandContextMixIn(object): def __init__(self): # type: () -> None - super(CommandContextMixIn, self).__init__() + super().__init__() self._in_main_context = False self._main_context = ExitStack() diff --git a/src/pip/_internal/cli/parser.py b/src/pip/_internal/cli/parser.py index e56193c3d2e..ba647f3a169 100644 --- a/src/pip/_internal/cli/parser.py +++ b/src/pip/_internal/cli/parser.py @@ -27,7 +27,7 @@ def __init__(self, *args, **kwargs): kwargs['max_help_position'] = 30 kwargs['indent_increment'] = 1 kwargs['width'] = shutil.get_terminal_size()[0] - 2 - optparse.IndentedHelpFormatter.__init__(self, *args, **kwargs) + super().__init__(*args, **kwargs) def format_option_strings(self, option): return self._format_option_strings(option) @@ -113,7 +113,7 @@ def expand_default(self, option): if self.parser is not None: self.parser._update_defaults(self.parser.defaults) default_values = self.parser.defaults.get(option.dest) - help_text = optparse.IndentedHelpFormatter.expand_default(self, option) + help_text = super().expand_default(option) if default_values and option.metavar == 'URL': if isinstance(default_values, str): @@ -162,7 +162,7 @@ def __init__(self, *args, **kwargs): self.config = Configuration(isolated) assert self.name - optparse.OptionParser.__init__(self, *args, **kwargs) + super().__init__(*args, **kwargs) def check_default(self, option, key, val): try: diff --git a/src/pip/_internal/cli/progress_bars.py b/src/pip/_internal/cli/progress_bars.py index 1e40d6d9e2d..9a0334f3aaf 100644 --- a/src/pip/_internal/cli/progress_bars.py +++ b/src/pip/_internal/cli/progress_bars.py @@ -76,10 +76,7 @@ def __init__(self, *args, **kwargs): Save the original SIGINT handler for later. """ # https://github.com/python/mypy/issues/5887 - super(InterruptibleMixin, self).__init__( # type: ignore - *args, - **kwargs - ) + super().__init__(*args, **kwargs) # type: ignore self.original_handler = signal(SIGINT, self.handle_sigint) @@ -99,7 +96,7 @@ def finish(self): This should happen regardless of whether the progress display finishes normally, or gets interrupted. """ - super(InterruptibleMixin, self).finish() # type: ignore + super().finish() # type: ignore signal(SIGINT, self.original_handler) def handle_sigint(self, signum, frame): # type: ignore @@ -133,10 +130,7 @@ class DownloadProgressMixin(object): def __init__(self, *args, **kwargs): # type: (List[Any], Dict[Any, Any]) -> None # https://github.com/python/mypy/issues/5887 - super(DownloadProgressMixin, self).__init__( # type: ignore - *args, - **kwargs - ) + super().__init__(*args, **kwargs) # type: ignore self.message = (" " * ( get_indentation() + 2 )) + self.message # type: str @@ -185,7 +179,7 @@ def __init__(self, *args, **kwargs): self.hide_cursor = False # https://github.com/python/mypy/issues/5887 - super(WindowsMixin, self).__init__(*args, **kwargs) # type: ignore + super().__init__(*args, **kwargs) # type: ignore # Check if we are running on Windows and we have the colorama module, # if we do then wrap our file with it. diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 2adbb74119d..468b3cceab5 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -51,7 +51,7 @@ class SessionCommandMixin(CommandContextMixIn): """ def __init__(self): # type: () -> None - super(SessionCommandMixin, self).__init__() + super().__init__() self._session = None # Optional[PipSession] @classmethod @@ -190,7 +190,7 @@ class RequirementCommand(IndexGroupCommand): def __init__(self, *args, **kw): # type: (Any, Any) -> None - super(RequirementCommand, self).__init__(*args, **kw) + super().__init__(*args, **kw) self.cmd_opts.add_option(cmdoptions.no_clean()) diff --git a/src/pip/_internal/configuration.py b/src/pip/_internal/configuration.py index 9e835785562..a55882b4634 100644 --- a/src/pip/_internal/configuration.py +++ b/src/pip/_internal/configuration.py @@ -110,7 +110,7 @@ class Configuration(object): def __init__(self, isolated, load_only=None): # type: (bool, Optional[Kind]) -> None - super(Configuration, self).__init__() + super().__init__() if load_only is not None and load_only not in VALID_LOAD_ONLY: raise ConfigurationError( diff --git a/src/pip/_internal/distributions/base.py b/src/pip/_internal/distributions/base.py index dc7ae96aa04..6c68a86a27b 100644 --- a/src/pip/_internal/distributions/base.py +++ b/src/pip/_internal/distributions/base.py @@ -28,7 +28,7 @@ class AbstractDistribution(object, metaclass=abc.ABCMeta): """ def __init__(self, req): # type: (InstallRequirement) -> None - super(AbstractDistribution, self).__init__() + super().__init__() self.req = req @abc.abstractmethod diff --git a/src/pip/_internal/exceptions.py b/src/pip/_internal/exceptions.py index b60bbbd21a2..1d59eff8bf1 100644 --- a/src/pip/_internal/exceptions.py +++ b/src/pip/_internal/exceptions.py @@ -106,8 +106,7 @@ def __init__(self, error_msg, response=None, request=None): if (self.response is not None and not self.request and hasattr(response, 'request')): self.request = self.response.request - super(NetworkConnectionError, self).__init__( - error_msg, response, request) + super().__init__(error_msg, response, request) def __str__(self): # type: () -> str @@ -357,7 +356,7 @@ class ConfigurationFileCouldNotBeLoaded(ConfigurationError): def __init__(self, reason="could not be loaded", fname=None, error=None): # type: (str, Optional[str], Optional[configparser.Error]) -> None - super(ConfigurationFileCouldNotBeLoaded, self).__init__(error) + super().__init__(error) self.reason = reason self.fname = fname self.error = error diff --git a/src/pip/_internal/index/collector.py b/src/pip/_internal/index/collector.py index b852645fd86..2715fcf92a0 100644 --- a/src/pip/_internal/index/collector.py +++ b/src/pip/_internal/index/collector.py @@ -68,7 +68,7 @@ def _match_vcs_scheme(url): class _NotHTML(Exception): def __init__(self, content_type, request_desc): # type: (str, str) -> None - super(_NotHTML, self).__init__(content_type, request_desc) + super().__init__(content_type, request_desc) self.content_type = content_type self.request_desc = request_desc diff --git a/src/pip/_internal/models/candidate.py b/src/pip/_internal/models/candidate.py index 0d89a8c07da..d8a8d42ebcf 100644 --- a/src/pip/_internal/models/candidate.py +++ b/src/pip/_internal/models/candidate.py @@ -21,7 +21,7 @@ def __init__(self, name, version, link): self.version = parse_version(version) # type: _BaseVersion self.link = link - super(InstallationCandidate, self).__init__( + super().__init__( key=(self.name, self.version, self.link), defining_class=InstallationCandidate ) diff --git a/src/pip/_internal/models/index.py b/src/pip/_internal/models/index.py index 0374d7f55e8..7f3285692be 100644 --- a/src/pip/_internal/models/index.py +++ b/src/pip/_internal/models/index.py @@ -10,7 +10,7 @@ class PackageIndex(object): def __init__(self, url, file_storage_domain): # type: (str, str) -> None - super(PackageIndex, self).__init__() + super().__init__() self.url = url self.netloc = urllib_parse.urlsplit(url).netloc self.simple_url = self._url_for_path('simple') diff --git a/src/pip/_internal/models/link.py b/src/pip/_internal/models/link.py index 4ad4f7bde9a..265194e9663 100644 --- a/src/pip/_internal/models/link.py +++ b/src/pip/_internal/models/link.py @@ -76,7 +76,7 @@ def __init__( self.requires_python = requires_python if requires_python else None self.yanked_reason = yanked_reason - super(Link, self).__init__(key=url, defining_class=Link) + super().__init__(key=url, defining_class=Link) self.cache_link_parsing = cache_link_parsing diff --git a/src/pip/_internal/network/cache.py b/src/pip/_internal/network/cache.py index d2a1b7313f7..582a7d72dcd 100644 --- a/src/pip/_internal/network/cache.py +++ b/src/pip/_internal/network/cache.py @@ -42,7 +42,7 @@ class SafeFileCache(BaseCache): def __init__(self, directory): # type: (str) -> None assert directory is not None, "Cache directory must not be None." - super(SafeFileCache, self).__init__() + super().__init__() self.directory = directory def _get_cache_path(self, name): diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py index 5839c4d28df..43ab1e18cd8 100644 --- a/src/pip/_internal/network/session.py +++ b/src/pip/_internal/network/session.py @@ -211,17 +211,13 @@ def close(self): class InsecureHTTPAdapter(HTTPAdapter): def cert_verify(self, conn, url, verify, cert): - super(InsecureHTTPAdapter, self).cert_verify( - conn=conn, url=url, verify=False, cert=cert - ) + super().cert_verify(conn=conn, url=url, verify=False, cert=cert) class InsecureCacheControlAdapter(CacheControlAdapter): def cert_verify(self, conn, url, verify, cert): - super(InsecureCacheControlAdapter, self).cert_verify( - conn=conn, url=url, verify=False, cert=cert - ) + super().cert_verify(conn=conn, url=url, verify=False, cert=cert) class PipSession(requests.Session): @@ -238,7 +234,7 @@ def __init__(self, *args, **kwargs): trusted_hosts = kwargs.pop("trusted_hosts", []) # type: List[str] index_urls = kwargs.pop("index_urls", None) - super(PipSession, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) # Namespace the attribute with "pip_" just in case to prevent # possible conflicts with the base class. @@ -425,4 +421,4 @@ def request(self, method, url, *args, **kwargs): kwargs.setdefault("timeout", self.timeout) # Dispatch the actual request - return super(PipSession, self).request(method, url, *args, **kwargs) + return super().request(method, url, *args, **kwargs) diff --git a/src/pip/_internal/network/xmlrpc.py b/src/pip/_internal/network/xmlrpc.py index d025a145a35..6dd03eb0f9d 100644 --- a/src/pip/_internal/network/xmlrpc.py +++ b/src/pip/_internal/network/xmlrpc.py @@ -28,7 +28,7 @@ class PipXmlrpcTransport(xmlrpc_client.Transport): def __init__(self, index_url, session, use_datetime=False): # type: (str, PipSession, bool) -> None - xmlrpc_client.Transport.__init__(self, use_datetime) + super().__init__(use_datetime) index_parts = urllib_parse.urlparse(index_url) self._scheme = index_parts.scheme self._session = session diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 6dc68089e91..e822a7f8adc 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -449,7 +449,7 @@ def save(self): class MissingCallableSuffix(InstallationError): def __init__(self, entry_point): # type: (str) -> None - super(MissingCallableSuffix, self).__init__( + super().__init__( "Invalid script entry point: {} - A callable " "suffix is required. Cf https://packaging.python.org/" "specifications/entry-points/#use-for-scripts for more " @@ -468,7 +468,7 @@ class PipScriptMaker(ScriptMaker): def make(self, specification, options=None): # type: (str, Dict[str, Any]) -> List[str] _raise_for_invalid_entrypoint(specification) - return super(PipScriptMaker, self).make(specification, options) + return super().make(specification, options) def _install_wheel( diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index f1321709b86..1550a60d1de 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -298,7 +298,7 @@ def __init__( lazy_wheel, # type: bool ): # type: (...) -> None - super(RequirementPreparer, self).__init__() + super().__init__() self.src_dir = src_dir self.build_dir = build_dir diff --git a/src/pip/_internal/resolution/legacy/resolver.py b/src/pip/_internal/resolution/legacy/resolver.py index d0fc1a7b316..abb3300e933 100644 --- a/src/pip/_internal/resolution/legacy/resolver.py +++ b/src/pip/_internal/resolution/legacy/resolver.py @@ -124,7 +124,7 @@ def __init__( py_version_info=None, # type: Optional[Tuple[int, ...]] ): # type: (...) -> None - super(Resolver, self).__init__() + super().__init__() assert upgrade_strategy in self._allowed_strategies if py_version_info is None: diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index cd1f188706f..275cb5105a4 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -304,7 +304,7 @@ def __init__( template.link is template.original_link): ireq.original_link_is_in_wheel_cache = True - super(LinkCandidate, self).__init__( + super().__init__( link=link, source_link=source_link, ireq=ireq, @@ -332,7 +332,7 @@ def __init__( version=None, # type: Optional[_BaseVersion] ): # type: (...) -> None - super(EditableCandidate, self).__init__( + super().__init__( link=link, source_link=link, ireq=make_install_req_from_editable(link, template), diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 30b860f6c48..695c6be7a7e 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -57,7 +57,7 @@ def __init__( upgrade_strategy, # type: str py_version_info=None, # type: Optional[Tuple[int, ...]] ): - super(Resolver, self).__init__() + super().__init__() assert upgrade_strategy in self._allowed_strategies self.factory = Factory( diff --git a/src/pip/_internal/utils/hashes.py b/src/pip/_internal/utils/hashes.py index 30d0498f39c..30a7f4a69f6 100644 --- a/src/pip/_internal/utils/hashes.py +++ b/src/pip/_internal/utils/hashes.py @@ -153,7 +153,7 @@ def __init__(self): """Don't offer the ``hashes`` kwarg.""" # Pass our favorite hash in to generate a "gotten hash". With the # empty list, it will never match, so an error will always raise. - super(MissingHashes, self).__init__(hashes={FAVORITE_HASH: []}) + super().__init__(hashes={FAVORITE_HASH: []}) def _raise(self, gots): # type: (Dict[str, _Hash]) -> NoReturn diff --git a/src/pip/_internal/utils/logging.py b/src/pip/_internal/utils/logging.py index fd9ff6561f4..9fd1d42c702 100644 --- a/src/pip/_internal/utils/logging.py +++ b/src/pip/_internal/utils/logging.py @@ -110,7 +110,7 @@ def __init__(self, *args, **kwargs): with their record's timestamp. """ self.add_timestamp = kwargs.pop("add_timestamp", False) - super(IndentingFormatter, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) def get_message_start(self, formatted, levelno): """ @@ -133,7 +133,7 @@ def format(self, record): Calls the standard formatter, but will indent all of the log message lines by our current indentation level. """ - formatted = super(IndentingFormatter, self).format(record) + formatted = super().format(record) message_start = self.get_message_start(formatted, record.levelno) formatted = message_start + formatted @@ -169,7 +169,7 @@ class ColorizedStreamHandler(logging.StreamHandler): COLORS = [] def __init__(self, stream=None, no_color=None): - logging.StreamHandler.__init__(self, stream) + super().__init__(stream) self._no_color = no_color if WINDOWS and colorama: @@ -228,7 +228,7 @@ def handleError(self, record): _is_broken_pipe_error(exc_class, exc)): raise BrokenStdoutLoggingError() - return super(ColorizedStreamHandler, self).handleError(record) + return super().handleError(record) class BetterRotatingFileHandler(logging.handlers.RotatingFileHandler): @@ -256,7 +256,7 @@ class ExcludeLoggerFilter(Filter): def filter(self, record): # The base Filter class allows only records from a logger (or its # children). - return not super(ExcludeLoggerFilter, self).filter(record) + return not super().filter(record) def setup_logging(verbosity, no_color, user_log_file): diff --git a/src/pip/_internal/utils/temp_dir.py b/src/pip/_internal/utils/temp_dir.py index dc369fb6852..c97edc76d6b 100644 --- a/src/pip/_internal/utils/temp_dir.py +++ b/src/pip/_internal/utils/temp_dir.py @@ -121,7 +121,7 @@ def __init__( kind="temp", # type: str globally_managed=False, # type: bool ): - super(TempDirectory, self).__init__() + super().__init__() if delete is _default: if path is not None: @@ -231,7 +231,7 @@ class AdjacentTempDirectory(TempDirectory): def __init__(self, original, delete=None): # type: (str, Optional[bool]) -> None self.original = original.rstrip('/\\') - super(AdjacentTempDirectory, self).__init__(delete=delete) + super().__init__(delete=delete) @classmethod def _generate_names(cls, name): diff --git a/src/pip/_internal/utils/wheel.py b/src/pip/_internal/utils/wheel.py index 4371e64b4a9..c6dc4ccb0d9 100644 --- a/src/pip/_internal/utils/wheel.py +++ b/src/pip/_internal/utils/wheel.py @@ -32,13 +32,13 @@ class WheelMetadata(DictMetadata): """ def __init__(self, metadata, wheel_name): # type: (Dict[str, bytes], str) -> None - super(WheelMetadata, self).__init__(metadata) + super().__init__(metadata) self._wheel_name = wheel_name def get_metadata(self, name): # type: (str) -> str try: - return super(WheelMetadata, self).get_metadata(name) + return super().get_metadata(name) except UnicodeDecodeError as e: # Augment the default error with the origin of the file. raise UnsupportedWheel( diff --git a/src/pip/_internal/vcs/bazaar.py b/src/pip/_internal/vcs/bazaar.py index e1649cc7261..22969c726de 100644 --- a/src/pip/_internal/vcs/bazaar.py +++ b/src/pip/_internal/vcs/bazaar.py @@ -31,7 +31,7 @@ class Bazaar(VersionControl): ) def __init__(self, *args, **kwargs): - super(Bazaar, self).__init__(*args, **kwargs) + super().__init__(*args, **kwargs) # 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): @@ -82,7 +82,7 @@ def update(self, dest, url, rev_options): def get_url_rev_and_auth(cls, url): # type: (str) -> Tuple[str, Optional[str], AuthInfo] # hotfix the URL scheme after removing bzr+ from bzr+ssh:// readd it - url, rev, user_pass = super(Bazaar, cls).get_url_rev_and_auth(url) + url, rev, user_pass = super().get_url_rev_and_auth(url) if url.startswith('ssh://'): url = 'bzr+' + url return url, rev, user_pass diff --git a/src/pip/_internal/vcs/git.py b/src/pip/_internal/vcs/git.py index 772a160be97..46f15fc8b41 100644 --- a/src/pip/_internal/vcs/git.py +++ b/src/pip/_internal/vcs/git.py @@ -393,10 +393,10 @@ def get_url_rev_and_auth(cls, url): if '://' not in url: assert 'file:' not in url url = url.replace('git+', 'git+ssh://') - url, rev, user_pass = super(Git, cls).get_url_rev_and_auth(url) + url, rev, user_pass = super().get_url_rev_and_auth(url) url = url.replace('ssh://', '') else: - url, rev, user_pass = super(Git, cls).get_url_rev_and_auth(url) + url, rev, user_pass = super().get_url_rev_and_auth(url) return url, rev, user_pass @@ -411,7 +411,7 @@ def update_submodules(cls, location): @classmethod def get_repository_root(cls, location): - loc = super(Git, cls).get_repository_root(location) + loc = super().get_repository_root(location) if loc: return loc try: diff --git a/src/pip/_internal/vcs/mercurial.py b/src/pip/_internal/vcs/mercurial.py index 1dded808893..1c84266742f 100644 --- a/src/pip/_internal/vcs/mercurial.py +++ b/src/pip/_internal/vcs/mercurial.py @@ -134,7 +134,7 @@ def get_subdirectory(cls, location): @classmethod def get_repository_root(cls, location): - loc = super(Mercurial, cls).get_repository_root(location) + loc = super().get_repository_root(location) if loc: return loc try: diff --git a/src/pip/_internal/vcs/subversion.py b/src/pip/_internal/vcs/subversion.py index dcdbbcdd54b..3bb7ea0f85e 100644 --- a/src/pip/_internal/vcs/subversion.py +++ b/src/pip/_internal/vcs/subversion.py @@ -84,7 +84,7 @@ def get_netloc_and_auth(cls, netloc, scheme): if scheme == 'ssh': # The --username and --password options can't be used for # svn+ssh URLs, so keep the auth information in the URL. - return super(Subversion, cls).get_netloc_and_auth(netloc, scheme) + return super().get_netloc_and_auth(netloc, scheme) return split_auth_from_netloc(netloc) @@ -92,7 +92,7 @@ def get_netloc_and_auth(cls, netloc, scheme): def get_url_rev_and_auth(cls, url): # type: (str) -> Tuple[str, Optional[str], AuthInfo] # hotfix the URL scheme after removing svn+ from svn+ssh:// readd it - url, rev, user_pass = super(Subversion, cls).get_url_rev_and_auth(url) + url, rev, user_pass = super().get_url_rev_and_auth(url) if url.startswith('ssh://'): url = 'svn+' + url return url, rev, user_pass @@ -197,7 +197,7 @@ def __init__(self, use_interactive=None): # Empty tuple: Could not parse version. self._vcs_version = None # type: Optional[Tuple[int, ...]] - super(Subversion, self).__init__() + super().__init__() def call_vcs_version(self): # type: () -> Tuple[int, ...] diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index 48465ec8dff..3bbc3fb285c 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -288,7 +288,7 @@ def __init__(self): # systems urllib_parse.uses_netloc.extend(self.schemes) urllib_parse.uses_fragment.extend(self.schemes) - super(VcsSupport, self).__init__() + super().__init__() def __iter__(self): # type: () -> Iterator[str] diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index c8a885aea7e..d233f897176 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -506,7 +506,7 @@ def __init__(self, base_path, *args, **kwargs): self.pip_expect_warning = kwargs.pop('pip_expect_warning', None) # Call the TestFileEnvironment __init__ - super(PipTestEnvironment, self).__init__(base_path, *args, **kwargs) + super().__init__(base_path, *args, **kwargs) # Expand our absolute path directories into relative for name in ["base", "venv", "bin", "lib", "site_packages", @@ -531,7 +531,7 @@ def _ignore_file(self, fn): if fn.endswith('__pycache__') or fn.endswith(".pyc"): result = True else: - result = super(PipTestEnvironment, self)._ignore_file(fn) + result = super()._ignore_file(fn) return result def _find_traverse(self, path, result): @@ -542,7 +542,7 @@ def _find_traverse(self, path, result): if not self.temp_path or path != 'tmp': result[path] = FoundDir(self.base_path, path) else: - super(PipTestEnvironment, self)._find_traverse(path, result) + super()._find_traverse(path, result) def run(self, *args, **kw): """ @@ -620,7 +620,7 @@ def run(self, *args, **kw): # Pass expect_stderr=True to allow any stderr. We do this because # we do our checking of stderr further on in check_stderr(). kw['expect_stderr'] = True - result = super(PipTestEnvironment, self).run(cwd=cwd, *args, **kw) + result = super().run(cwd=cwd, *args, **kw) if expect_error and not allow_error: if result.returncode == 0: diff --git a/tests/lib/server.py b/tests/lib/server.py index bc8ffb7eb5e..5902ae0c96a 100644 --- a/tests/lib/server.py +++ b/tests/lib/server.py @@ -67,7 +67,7 @@ def blocked_signals(): class _RequestHandler(WSGIRequestHandler): def make_environ(self): - environ = super(_RequestHandler, self).make_environ() + environ = super().make_environ() # From pallets/werkzeug#1469, will probably be in release after # 0.16.0. diff --git a/tests/unit/test_base_command.py b/tests/unit/test_base_command.py index 8ba3d9e2526..6d60ca09420 100644 --- a/tests/unit/test_base_command.py +++ b/tests/unit/test_base_command.py @@ -27,11 +27,11 @@ def run_func(): raise SystemExit(1) self.run_func = run_func - super(FakeCommand, self).__init__(self._name, self._name) + super().__init__(self._name, self._name) def main(self, args): args.append("--disable-pip-version-check") - return super(FakeCommand, self).main(args) + return super().main(args) def run(self, options, args): logging.getLogger("pip.tests").info("fake") diff --git a/tests/unit/test_format_control.py b/tests/unit/test_format_control.py index 0e152798184..f8498e8e5d9 100644 --- a/tests/unit/test_format_control.py +++ b/tests/unit/test_format_control.py @@ -8,7 +8,7 @@ class SimpleCommand(Command): def __init__(self): - super(SimpleCommand, self).__init__('fake', 'fake summary') + super().__init__('fake', 'fake summary') def add_options(self): self.cmd_opts.add_option(cmdoptions.no_binary()) From 5e11687cbdbe193416181b46074260440469ba00 Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Thu, 24 Dec 2020 15:28:50 -0800 Subject: [PATCH 2818/3170] Replace typing.Text with str Using typing.Text is unnecessary since dropping Python 2 support. In Python 3, typing.Text is a simple alias of str. It exists as a backward compatibility shim for Python 2. --- ...bb86bc866fdc4257a445e0df09dd7e64.trivial.rst | 0 src/pip/_internal/exceptions.py | 4 ++-- src/pip/_internal/index/package_finder.py | 10 ++++------ src/pip/_internal/models/link.py | 4 ++-- .../_internal/operations/build/wheel_legacy.py | 6 +++--- src/pip/_internal/req/req_file.py | 10 +++++----- src/pip/_internal/self_outdated_check.py | 4 ++-- src/pip/_internal/utils/compat.py | 6 +++--- src/pip/_internal/utils/encoding.py | 6 +++--- src/pip/_internal/utils/misc.py | 8 +++----- src/pip/_internal/utils/subprocess.py | 8 ++++---- src/pip/_internal/utils/unpacking.py | 10 +++++----- src/pip/_internal/utils/urls.py | 6 +++--- src/pip/_internal/vcs/versioncontrol.py | 7 +++---- tests/lib/certs.py | 4 ++-- tests/lib/server.py | 17 +++-------------- 16 files changed, 47 insertions(+), 63 deletions(-) create mode 100644 news/bb86bc866fdc4257a445e0df09dd7e64.trivial.rst diff --git a/news/bb86bc866fdc4257a445e0df09dd7e64.trivial.rst b/news/bb86bc866fdc4257a445e0df09dd7e64.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/exceptions.py b/src/pip/_internal/exceptions.py index b60bbbd21a2..d8a074f5b6e 100644 --- a/src/pip/_internal/exceptions.py +++ b/src/pip/_internal/exceptions.py @@ -7,7 +7,7 @@ if MYPY_CHECK_RUNNING: import configparser from hashlib import _Hash - from typing import Any, Dict, List, Optional, Text + from typing import Any, Dict, List, Optional from pip._vendor.pkg_resources import Distribution from pip._vendor.requests.models import Request, Response @@ -95,7 +95,7 @@ class NetworkConnectionError(PipError): """HTTP connection error""" def __init__(self, error_msg, response=None, request=None): - # type: (Text, Response, Request) -> None + # type: (str, Response, Request) -> None """ Initialize NetworkConnectionError with `request` and `response` objects. diff --git a/src/pip/_internal/index/package_finder.py b/src/pip/_internal/index/package_finder.py index 4fdecd226ec..860eb1255ce 100644 --- a/src/pip/_internal/index/package_finder.py +++ b/src/pip/_internal/index/package_finder.py @@ -33,7 +33,7 @@ from pip._internal.utils.urls import url_to_path if MYPY_CHECK_RUNNING: - from typing import FrozenSet, Iterable, List, Optional, Set, Text, Tuple, Union + from typing import FrozenSet, Iterable, List, Optional, Set, Tuple, Union from pip._vendor.packaging.tags import Tag from pip._vendor.packaging.version import _BaseVersion @@ -149,7 +149,7 @@ def __init__( self.project_name = project_name def evaluate_link(self, link): - # type: (Link) -> Tuple[bool, Optional[Text]] + # type: (Link) -> Tuple[bool, Optional[str]] """ Determine whether a link is a candidate for installation. @@ -736,7 +736,7 @@ def _sort_links(self, links): return no_eggs + eggs def _log_skipped_link(self, link, reason): - # type: (Link, Text) -> None + # type: (Link, str) -> None if link not in self._logged_links: # Mark this as a unicode string to prevent "UnicodeEncodeError: # 'ascii' codec can't encode character" in Python 2 when @@ -761,9 +761,7 @@ def get_install_candidate(self, link_evaluator, link): return InstallationCandidate( name=link_evaluator.project_name, link=link, - # Convert the Text result to str since InstallationCandidate - # accepts str. - version=str(result), + version=result, ) def evaluate_links(self, link_evaluator, links): diff --git a/src/pip/_internal/models/link.py b/src/pip/_internal/models/link.py index 4ad4f7bde9a..194d68f84df 100644 --- a/src/pip/_internal/models/link.py +++ b/src/pip/_internal/models/link.py @@ -14,7 +14,7 @@ from pip._internal.utils.urls import path_to_url, url_to_path if MYPY_CHECK_RUNNING: - from typing import Optional, Text, Tuple, Union + from typing import Optional, Tuple, Union from pip._internal.index.collector import HTMLPage from pip._internal.utils.hashes import Hashes @@ -38,7 +38,7 @@ def __init__( url, # type: str comes_from=None, # type: Optional[Union[str, HTMLPage]] requires_python=None, # type: Optional[str] - yanked_reason=None, # type: Optional[Text] + yanked_reason=None, # type: Optional[str] cache_link_parsing=True, # type: bool ): # type: (...) -> None diff --git a/src/pip/_internal/operations/build/wheel_legacy.py b/src/pip/_internal/operations/build/wheel_legacy.py index 9da365e4ddd..73401cd78c4 100644 --- a/src/pip/_internal/operations/build/wheel_legacy.py +++ b/src/pip/_internal/operations/build/wheel_legacy.py @@ -11,14 +11,14 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import List, Optional, Text + from typing import List, Optional logger = logging.getLogger(__name__) def format_command_result( command_args, # type: List[str] - command_output, # type: Text + command_output, # type: str ): # type: (...) -> str """Format command information for logging.""" @@ -42,7 +42,7 @@ def get_legacy_build_wheel_path( temp_dir, # type: str name, # type: str command_args, # type: List[str] - command_output, # type: Text + command_output, # type: str ): # type: (...) -> Optional[str] """Return the path to the wheel in the temporary build directory.""" diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index 8c34bc149cd..26055b4d6cb 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -156,7 +156,7 @@ def parse_requirements( def preprocess(content): - # type: (Text) -> ReqFileLines + # type: (str) -> ReqFileLines """Split, filter, and join lines, and return a line iterator :param content: the content of the requirements file @@ -396,7 +396,7 @@ def _parse_file(self, filename, constraint): def get_line_parser(finder): # type: (Optional[PackageFinder]) -> LineParser def parse_line(line): - # type: (Text) -> Tuple[str, Values] + # type: (str) -> Tuple[str, Values] # Build new parser for each line since it accumulates appendable # options. parser = build_parser() @@ -415,7 +415,7 @@ def parse_line(line): def break_args_options(line): - # type: (Text) -> Tuple[str, Text] + # type: (str) -> Tuple[str, str] """Break up the line into an args and options string. We only want to shlex (and then optparse) the options, not the args. args can contain markers which are corrupted by shlex. @@ -468,7 +468,7 @@ def join_lines(lines_enum): comments). The joined line takes on the index of the first line. """ primary_line_number = None - new_line = [] # type: List[Text] + new_line = [] # type: List[str] for line_number, line in lines_enum: if not line.endswith('\\') or COMMENT_RE.match(line): if COMMENT_RE.match(line): @@ -535,7 +535,7 @@ def expand_env_variables(lines_enum): def get_file_content(url, session): - # type: (str, PipSession) -> Tuple[str, Text] + # type: (str, PipSession) -> Tuple[str, str] """Gets the content of a file; it may be a filename, file: URL, or http: URL. Returns (location, content). Content is unicode. Respects # -*- coding: declarations on the retrieved files. diff --git a/src/pip/_internal/self_outdated_check.py b/src/pip/_internal/self_outdated_check.py index a44c00cda68..81594120079 100644 --- a/src/pip/_internal/self_outdated_check.py +++ b/src/pip/_internal/self_outdated_check.py @@ -18,7 +18,7 @@ if MYPY_CHECK_RUNNING: import optparse - from typing import Any, Dict, Text, Union + from typing import Any, Dict from pip._internal.network.session import PipSession @@ -30,7 +30,7 @@ def _get_statefile_name(key): - # type: (Union[str, Text]) -> str + # type: (str) -> str key_bytes = ensure_binary(key) name = hashlib.sha224(key_bytes).hexdigest() return name diff --git a/src/pip/_internal/utils/compat.py b/src/pip/_internal/utils/compat.py index 31afee716fa..cc8ecd0f682 100644 --- a/src/pip/_internal/utils/compat.py +++ b/src/pip/_internal/utils/compat.py @@ -14,7 +14,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Callable, Optional, Protocol, Text, TypeVar, Union + from typing import Callable, Optional, Protocol, TypeVar, Union # Used in the @lru_cache polyfill. F = TypeVar('F') @@ -57,7 +57,7 @@ def has_tls(): def str_to_display(data, desc=None): - # type: (Union[bytes, Text], Optional[str]) -> Text + # type: (Union[bytes, str], Optional[str]) -> str """ For display or logging purposes, convert a bytes object (or text) to text (e.g. unicode in Python 2) safe for output. @@ -124,7 +124,7 @@ def str_to_display(data, desc=None): def console_to_str(data): - # type: (bytes) -> Text + # type: (bytes) -> str """Return a string, safe for output, of subprocess output. """ return str_to_display(data, desc='Subprocess output') diff --git a/src/pip/_internal/utils/encoding.py b/src/pip/_internal/utils/encoding.py index 42a57535af8..7df67987842 100644 --- a/src/pip/_internal/utils/encoding.py +++ b/src/pip/_internal/utils/encoding.py @@ -6,7 +6,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import List, Text, Tuple + from typing import List, Tuple BOMS = [ (codecs.BOM_UTF8, 'utf-8'), @@ -16,13 +16,13 @@ (codecs.BOM_UTF32, 'utf-32'), (codecs.BOM_UTF32_BE, 'utf-32-be'), (codecs.BOM_UTF32_LE, 'utf-32-le'), -] # type: List[Tuple[bytes, Text]] +] # type: List[Tuple[bytes, str]] ENCODING_RE = re.compile(br'coding[:=]\s*([-\w.]+)') def auto_decode(data): - # type: (bytes) -> Text + # type: (bytes) -> str """Check a bytes string for a BOM to correctly detect the encoding Fallback to locale.getpreferredencoding(False) like open() on Python3""" diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 604494e0c96..c3d969eb22f 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -46,10 +46,8 @@ Iterator, List, Optional, - Text, Tuple, TypeVar, - Union, ) from pip._vendor.pkg_resources import Distribution @@ -155,7 +153,7 @@ def rmtree_errorhandler(func, path, exc_info): def path_to_display(path): - # type: (Optional[Union[str, Text]]) -> Optional[Text] + # type: (Optional[str]) -> Optional[str] """ Convert a bytes (or text) path to text (unicode in Python 2) for display and logging purposes. @@ -181,7 +179,7 @@ def path_to_display(path): def display_path(path): - # type: (Union[str, Text]) -> str + # type: (str) -> str """Gives the display value for a given path, making it relative to cwd if possible.""" path = os.path.normcase(os.path.abspath(path)) @@ -885,7 +883,7 @@ def is_console_interactive(): def hash_file(path, blocksize=1 << 20): - # type: (Text, int) -> Tuple[Any, int] + # type: (str, int) -> Tuple[Any, int] """Return (hash, length) for path using hashlib.sha256() """ diff --git a/src/pip/_internal/utils/subprocess.py b/src/pip/_internal/utils/subprocess.py index 4a5cce469de..25c9020699d 100644 --- a/src/pip/_internal/utils/subprocess.py +++ b/src/pip/_internal/utils/subprocess.py @@ -11,7 +11,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Any, Callable, Iterable, List, Mapping, Optional, Text, Union + from typing import Any, Callable, Iterable, List, Mapping, Optional, Union CommandArgs = List[Union[str, HiddenText]] @@ -66,10 +66,10 @@ def reveal_command_args(args): def make_subprocess_output_error( cmd_args, # type: Union[List[str], CommandArgs] cwd, # type: Optional[str] - lines, # type: List[Text] + lines, # type: List[str] exit_status, # type: int ): - # type: (...) -> Text + # type: (...) -> str """ Create and return the error message to use to log a subprocess error with command output. @@ -117,7 +117,7 @@ def call_subprocess( spinner=None, # type: Optional[SpinnerInterface] log_failed_cmd=True # type: Optional[bool] ): - # type: (...) -> Text + # type: (...) -> str """ Args: show_stdout: if true, use INFO to log the subprocess's stderr and diff --git a/src/pip/_internal/utils/unpacking.py b/src/pip/_internal/utils/unpacking.py index fbfa8f50672..5cfba87f81a 100644 --- a/src/pip/_internal/utils/unpacking.py +++ b/src/pip/_internal/utils/unpacking.py @@ -19,7 +19,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Iterable, List, Optional, Text, Union + from typing import Iterable, List, Optional from zipfile import ZipInfo @@ -51,7 +51,7 @@ def current_umask(): def split_leading_dir(path): - # type: (Union[str, Text]) -> List[Union[str, Text]] + # type: (str) -> List[str] path = path.lstrip('/').lstrip('\\') if ( '/' in path and ( @@ -67,7 +67,7 @@ def split_leading_dir(path): def has_leading_dir(paths): - # type: (Iterable[Union[str, Text]]) -> bool + # type: (Iterable[str]) -> bool """Returns true if all the paths have the same leading path name (i.e., everything is in one subdirectory in an archive)""" common_prefix = None @@ -83,7 +83,7 @@ def has_leading_dir(paths): def is_within_directory(directory, target): - # type: ((Union[str, Text]), (Union[str, Text])) -> bool + # type: (str, str) -> bool """ Return true if the absolute path of target is within the directory """ @@ -95,7 +95,7 @@ def is_within_directory(directory, target): def set_extracted_file_to_default_mode_plus_executable(path): - # type: (Union[str, Text]) -> None + # type: (str) -> None """ Make file present at path have execute for user/group/world (chmod +x) is no-op on windows per python docs diff --git a/src/pip/_internal/utils/urls.py b/src/pip/_internal/utils/urls.py index 91df4c30d82..5754153f216 100644 --- a/src/pip/_internal/utils/urls.py +++ b/src/pip/_internal/utils/urls.py @@ -6,18 +6,18 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Optional, Text, Union + from typing import Optional def get_url_scheme(url): - # type: (Union[str, Text]) -> Optional[Text] + # type: (str) -> Optional[str] if ':' not in url: return None return url.split(':', 1)[0].lower() def path_to_url(path): - # type: (Union[str, Text]) -> str + # type: (str) -> str """ Convert a path to a file: URL. The path will be made absolute and have quoted path parts. diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index 48465ec8dff..3500d1690a5 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -39,7 +39,6 @@ List, Mapping, Optional, - Text, Tuple, Type, Union, @@ -58,7 +57,7 @@ def is_url(name): - # type: (Union[str, Text]) -> bool + # type: (str) -> bool """ Return true if the name looks like a URL. """ @@ -92,7 +91,7 @@ def call_subprocess( extra_ok_returncodes=None, # type: Optional[Iterable[int]] log_failed_cmd=True # type: Optional[bool] ): - # type: (...) -> Text + # type: (...) -> str """ Args: extra_ok_returncodes: an iterable of integer return codes that are @@ -764,7 +763,7 @@ def run_command( extra_ok_returncodes=None, # type: Optional[Iterable[int]] log_failed_cmd=True # type: bool ): - # type: (...) -> Text + # type: (...) -> str """ Run a VCS subcommand This is simply a wrapper around call_subprocess that adds the VCS diff --git a/tests/lib/certs.py b/tests/lib/certs.py index 7d86ee4c04e..1f51f2174cf 100644 --- a/tests/lib/certs.py +++ b/tests/lib/certs.py @@ -9,11 +9,11 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Text, Tuple + from typing import Tuple def make_tls_cert(hostname): - # type: (Text) -> Tuple[x509.Certificate, rsa.RSAPrivateKey] + # type: (str) -> Tuple[x509.Certificate, rsa.RSAPrivateKey] key = rsa.generate_private_key( public_exponent=65537, key_size=2048, diff --git a/tests/lib/server.py b/tests/lib/server.py index bc8ffb7eb5e..add3fd5621d 100644 --- a/tests/lib/server.py +++ b/tests/lib/server.py @@ -14,18 +14,7 @@ if MYPY_CHECK_RUNNING: from types import TracebackType - from typing import ( - Any, - Callable, - Dict, - Iterable, - List, - Optional, - Text, - Tuple, - Type, - Union, - ) + from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Type from werkzeug.serving import BaseWSGIServer @@ -166,7 +155,7 @@ def server_running(server): def text_html_response(text): - # type: (Text) -> Responder + # type: (str) -> Responder def responder(environ, start_response): # type: (Environ, StartResponse) -> Body start_response("200 OK", [ @@ -178,7 +167,7 @@ def responder(environ, start_response): def html5_page(text): - # type: (Union[Text, str]) -> Text + # type: (str) -> str return dedent(u""" <!DOCTYPE html> <html> From 2e380249913683311737c1566073f36e485cde70 Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Thu, 24 Dec 2020 08:53:38 -0800 Subject: [PATCH 2819/3170] Drop u prefix from str literals Unnecessary since dropping Python 2 support. This makes one test case from test_str_to_display a duplicate and so has been removed. --- docs/html/conf.py | 12 +++++----- ...d5-38b6-48cc-8136-0c32d3ace838.trivial.rst | 0 src/pip/_internal/cli/progress_bars.py | 2 +- src/pip/_internal/index/package_finder.py | 14 ++++-------- src/pip/_internal/network/utils.py | 6 ++--- .../_internal/resolution/legacy/resolver.py | 2 +- .../resolution/resolvelib/resolver.py | 8 +++---- src/pip/_internal/utils/subprocess.py | 2 +- tests/conftest.py | 2 +- tests/functional/test_fast_deps.py | 4 ++-- tests/functional/test_new_resolver_hashes.py | 4 ++-- tests/functional/test_vcs_git.py | 4 ++-- tests/lib/__init__.py | 2 +- tests/lib/path.py | 2 +- tests/lib/server.py | 2 +- tests/lib/wheel.py | 2 +- tests/unit/test_appdirs.py | 2 +- tests/unit/test_cache.py | 2 +- tests/unit/test_collector.py | 8 +++---- tests/unit/test_compat.py | 16 ++++++-------- tests/unit/test_index.py | 6 ++--- tests/unit/test_resolution_legacy_resolver.py | 2 +- tests/unit/test_utils.py | 18 +++++++-------- tests/unit/test_utils_subprocess.py | 18 +++++++-------- tests/unit/test_wheel.py | 22 +++++++++---------- tools/automation/release/__init__.py | 4 ++-- 26 files changed, 79 insertions(+), 87 deletions(-) create mode 100644 news/b5e475d5-38b6-48cc-8136-0c32d3ace838.trivial.rst diff --git a/docs/html/conf.py b/docs/html/conf.py index 7983187e9a3..1f3131ce3c5 100644 --- a/docs/html/conf.py +++ b/docs/html/conf.py @@ -239,8 +239,8 @@ ( 'index', 'pip.tex', - u'pip Documentation', - u'pip developers', + 'pip Documentation', + 'pip developers', 'manual', ), ] @@ -269,8 +269,8 @@ ( 'index', 'pip', - u'package manager for Python packages', - u'pip developers', + 'package manager for Python packages', + 'pip developers', 1 ) ] @@ -295,11 +295,11 @@ def to_document_name(path, base_dir): for fname in raw_subcommands: fname_base = to_document_name(fname, man_dir) outname = 'pip-' + fname_base.split('/')[1] - description = u'description of {} command'.format( + description = 'description of {} command'.format( outname.replace('-', ' ') ) - man_pages.append((fname_base, outname, description, u'pip developers', 1)) + man_pages.append((fname_base, outname, description, 'pip developers', 1)) # -- Options for docs_feedback_sphinxext -------------------------------------- diff --git a/news/b5e475d5-38b6-48cc-8136-0c32d3ace838.trivial.rst b/news/b5e475d5-38b6-48cc-8136-0c32d3ace838.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/cli/progress_bars.py b/src/pip/_internal/cli/progress_bars.py index 1e40d6d9e2d..c8d6567d34b 100644 --- a/src/pip/_internal/cli/progress_bars.py +++ b/src/pip/_internal/cli/progress_bars.py @@ -125,7 +125,7 @@ class BlueEmojiBar(IncrementalBar): suffix = "%(percent)d%%" bar_prefix = " " bar_suffix = " " - phases = (u"\U0001F539", u"\U0001F537", u"\U0001F535") # type: Any + phases = ("\U0001F539", "\U0001F537", "\U0001F535") # type: Any class DownloadProgressMixin(object): diff --git a/src/pip/_internal/index/package_finder.py b/src/pip/_internal/index/package_finder.py index 860eb1255ce..cd768704819 100644 --- a/src/pip/_internal/index/package_finder.py +++ b/src/pip/_internal/index/package_finder.py @@ -161,10 +161,7 @@ def evaluate_link(self, link): version = None if link.is_yanked and not self._allow_yanked: reason = link.yanked_reason or '<none given>' - # Mark this as a unicode string to prevent "UnicodeEncodeError: - # 'ascii' codec can't encode character" in Python 2 when - # the reason contains non-ascii characters. - return (False, u'yanked for reason: {}'.format(reason)) + return (False, 'yanked for reason: {}'.format(reason)) if link.egg_fragment: egg_info = link.egg_fragment @@ -738,12 +735,9 @@ def _sort_links(self, links): def _log_skipped_link(self, link, reason): # type: (Link, str) -> None if link not in self._logged_links: - # Mark this as a unicode string to prevent "UnicodeEncodeError: - # 'ascii' codec can't encode character" in Python 2 when - # the reason contains non-ascii characters. - # Also, put the link at the end so the reason is more visible - # and because the link string is usually very long. - logger.debug(u'Skipping link: %s: %s', reason, link) + # Put the link at the end so the reason is more visible and because + # the link string is usually very long. + logger.debug('Skipping link: %s: %s', reason, link) self._logged_links.add(link) def get_install_candidate(self, link_evaluator, link): diff --git a/src/pip/_internal/network/utils.py b/src/pip/_internal/network/utils.py index 907b3fed49a..f4ff95010fc 100644 --- a/src/pip/_internal/network/utils.py +++ b/src/pip/_internal/network/utils.py @@ -30,7 +30,7 @@ def raise_for_status(resp): # type: (Response) -> None - http_error_msg = u'' + http_error_msg = '' if isinstance(resp.reason, bytes): # We attempt to decode utf-8 first because some servers # choose to localize their reason strings. If the string @@ -44,11 +44,11 @@ def raise_for_status(resp): reason = resp.reason if 400 <= resp.status_code < 500: - http_error_msg = u'%s Client Error: %s for url: %s' % ( + http_error_msg = '%s Client Error: %s for url: %s' % ( resp.status_code, reason, resp.url) elif 500 <= resp.status_code < 600: - http_error_msg = u'%s Server Error: %s for url: %s' % ( + http_error_msg = '%s Server Error: %s for url: %s' % ( resp.status_code, reason, resp.url) if http_error_msg: diff --git a/src/pip/_internal/resolution/legacy/resolver.py b/src/pip/_internal/resolution/legacy/resolver.py index d0fc1a7b316..b71e294ba79 100644 --- a/src/pip/_internal/resolution/legacy/resolver.py +++ b/src/pip/_internal/resolution/legacy/resolver.py @@ -276,7 +276,7 @@ def _find_requirement_link(self, req): # Mark this as a unicode string to prevent # "UnicodeEncodeError: 'ascii' codec can't encode character" # in Python 2 when the reason contains non-ascii characters. - u'The candidate selected for download or install is a ' + 'The candidate selected for download or install is a ' 'yanked version: {candidate}\n' 'Reason for being yanked: {reason}' ).format(candidate=best_candidate, reason=reason) diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 30b860f6c48..751606f512e 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -189,14 +189,14 @@ def resolve(self, root_reqs, check_supported_wheels): # The reason can contain non-ASCII characters, Unicode # is required for Python 2. msg = ( - u'The candidate selected for download or install is a ' - u'yanked version: {name!r} candidate (version {version} ' - u'at {link})\nReason for being yanked: {reason}' + 'The candidate selected for download or install is a ' + 'yanked version: {name!r} candidate (version {version} ' + 'at {link})\nReason for being yanked: {reason}' ).format( name=candidate.name, version=candidate.version, link=link, - reason=link.yanked_reason or u'<none given>', + reason=link.yanked_reason or '<none given>', ) logger.warning(msg) diff --git a/src/pip/_internal/utils/subprocess.py b/src/pip/_internal/utils/subprocess.py index 25c9020699d..7de45aed010 100644 --- a/src/pip/_internal/utils/subprocess.py +++ b/src/pip/_internal/utils/subprocess.py @@ -90,7 +90,7 @@ def make_subprocess_output_error( # Use a unicode string to avoid "UnicodeEncodeError: 'ascii' # codec can't encode character ..." in Python 2 when a format # argument (e.g. `output`) has a non-ascii character. - u'Command errored out with exit status {exit_status}:\n' + 'Command errored out with exit status {exit_status}:\n' ' command: {command_display}\n' ' cwd: {cwd_display}\n' 'Complete output ({line_count} lines):\n{output}{divider}' diff --git a/tests/conftest.py b/tests/conftest.py index 499c121bca9..7e67a0b2aa8 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -496,7 +496,7 @@ def factory(): """ output_path = Path(str(tmpdir_factory.mktemp("certs"))) / "cert.pem" # Must be Text on PY2. - cert, key = make_tls_cert(u"localhost") + cert, key = make_tls_cert("localhost") with open(str(output_path), "wb") as f: f.write(serialize_cert(cert)) f.write(serialize_key(key)) diff --git a/tests/functional/test_fast_deps.py b/tests/functional/test_fast_deps.py index 655440b881b..e82641986ac 100644 --- a/tests/functional/test_fast_deps.py +++ b/tests/functional/test_fast_deps.py @@ -54,7 +54,7 @@ def test_build_wheel_with_deps(data, script): def test_require_hash(script, tmp_path): reqs = tmp_path / 'requirements.txt' reqs.write_text( - u'idna==2.10' + 'idna==2.10' ' --hash=sha256:' 'b97d804b1e9b523befed77c48dacec60e6dcb0b5391d57af6a65a312a90648c0' ' --hash=sha256:' @@ -71,7 +71,7 @@ def test_require_hash(script, tmp_path): @mark.network def test_hash_mismatch(script, tmp_path): reqs = tmp_path / 'requirements.txt' - reqs.write_text(u'idna==2.10 --hash=sha256:irna') + reqs.write_text('idna==2.10 --hash=sha256:irna') result = script.pip( 'download', '--use-feature=fast-deps', '-r', str(reqs), expect_error=True, diff --git a/tests/functional/test_new_resolver_hashes.py b/tests/functional/test_new_resolver_hashes.py index 6fa642f8b8f..854b66418ae 100644 --- a/tests/functional/test_new_resolver_hashes.py +++ b/tests/functional/test_new_resolver_hashes.py @@ -80,7 +80,7 @@ def test_new_resolver_hash_intersect(script, requirements_template, message): "--requirement", requirements_txt, ) - assert message.format(name=u"base") in result.stdout, str(result) + assert message.format(name="base") in result.stdout, str(result) def test_new_resolver_hash_intersect_from_constraint(script): @@ -116,7 +116,7 @@ def test_new_resolver_hash_intersect_from_constraint(script): message = ( "Checked 2 links for project {name!r} against 1 hashes " "(1 matches, 0 no digest): discarding 1 non-matches" - ).format(name=u"base") + ).format(name="base") assert message in result.stdout, str(result) diff --git a/tests/functional/test_vcs_git.py b/tests/functional/test_vcs_git.py index 8b07ae6673b..d5de1a2fd77 100644 --- a/tests/functional/test_vcs_git.py +++ b/tests/functional/test_vcs_git.py @@ -259,13 +259,13 @@ def test_resolve_commit_not_on_branch(script, tmp_path): repo_path.mkdir() script.run("git", "init", cwd=str(repo_path)) - repo_file.write_text(u".") + repo_file.write_text(".") script.run("git", "add", "file.txt", cwd=str(repo_path)) script.run("git", "commit", "-m", "initial commit", cwd=str(repo_path)) script.run("git", "checkout", "-b", "abranch", cwd=str(repo_path)) # create a commit - repo_file.write_text(u"..") + repo_file.write_text("..") script.run("git", "commit", "-a", "-m", "commit 1", cwd=str(repo_path)) commit = script.run( "git", "rev-parse", "HEAD", cwd=str(repo_path) diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index c8a885aea7e..bdf50c6f70e 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -306,7 +306,7 @@ def assert_installed(self, pkg_name, editable=True, with_files=None, if not (egg_link_contents.endswith('\n.') and egg_link_contents[:-2].endswith(pkg_dir)): raise TestFailure(textwrap.dedent( - u'''\ + '''\ Incorrect egg_link file {egg_link_file!r} Expected ending: {expected_ending!r} ------- Actual contents ------- diff --git a/tests/lib/path.py b/tests/lib/path.py index 3bd5d319f35..2a9e29d8cd2 100644 --- a/tests/lib/path.py +++ b/tests/lib/path.py @@ -72,7 +72,7 @@ def __radd__(self, path): return Path(path + str(self)) def __repr__(self): - return u"Path({inner})".format(inner=str.__repr__(self)) + return "Path({inner})".format(inner=str.__repr__(self)) def __hash__(self): return str.__hash__(self) diff --git a/tests/lib/server.py b/tests/lib/server.py index add3fd5621d..b0b802689d5 100644 --- a/tests/lib/server.py +++ b/tests/lib/server.py @@ -168,7 +168,7 @@ def responder(environ, start_response): def html5_page(text): # type: (str) -> str - return dedent(u""" + return dedent(""" <!DOCTYPE html> <html> <body> diff --git a/tests/lib/wheel.py b/tests/lib/wheel.py index b5e222fda43..2121a175caf 100644 --- a/tests/lib/wheel.py +++ b/tests/lib/wheel.py @@ -240,7 +240,7 @@ def record_file_maker_wrapper( if record_callback is not _default: records = record_callback(records) - with StringIO(newline=u"") as buf: + with StringIO(newline="") as buf: writer = csv23.writer(buf) for record in records: writer.writerow(map(ensure_text, record)) diff --git a/tests/unit/test_appdirs.py b/tests/unit/test_appdirs.py index e129c0c0b83..623486b2894 100644 --- a/tests/unit/test_appdirs.py +++ b/tests/unit/test_appdirs.py @@ -70,7 +70,7 @@ def test_user_cache_dir_unicode(self, monkeypatch): return def my_get_win_folder(csidl_name): - return u"\u00DF\u00E4\u03B1\u20AC" + return "\u00DF\u00E4\u03B1\u20AC" monkeypatch.setattr(_appdirs, "_get_win_folder", my_get_win_folder) diff --git a/tests/unit/test_cache.py b/tests/unit/test_cache.py index a289fb59890..89cbb079b72 100644 --- a/tests/unit/test_cache.py +++ b/tests/unit/test_cache.py @@ -47,7 +47,7 @@ def test_cache_hash(): assert h == "72aa79d3315c181d2cc23239d7109a782de663b6f89982624d8c1e86" h = _hash_dict({"url": "https://g.c/o/r", "subdirectory": "sd"}) assert h == "8b13391b6791bf7f3edeabb41ea4698d21bcbdbba7f9c7dc9339750d" - h = _hash_dict({"subdirectory": u"/\xe9e"}) + h = _hash_dict({"subdirectory": "/\xe9e"}) assert h == "f83b32dfa27a426dec08c21bf006065dd003d0aac78e7fc493d9014d" diff --git a/tests/unit/test_collector.py b/tests/unit/test_collector.py index 4384812fc67..294ea721a3a 100644 --- a/tests/unit/test_collector.py +++ b/tests/unit/test_collector.py @@ -382,14 +382,14 @@ def test_clean_link(url, clean_url): ('<a href="/pkg4-1.0.tar.gz" data-yanked="version < 1"></a>', 'version < 1'), # Test a yanked reason with a non-ascii character. - (u'<a href="/pkg-1.0.tar.gz" data-yanked="curlyquote \u2018"></a>', - u'curlyquote \u2018'), + ('<a href="/pkg-1.0.tar.gz" data-yanked="curlyquote \u2018"></a>', + 'curlyquote \u2018'), ]) def test_parse_links__yanked_reason(anchor_html, expected): html = ( # Mark this as a unicode string for Python 2 since anchor_html # can contain non-ascii. - u'<html><head><meta charset="utf-8"><head>' + '<html><head><meta charset="utf-8"><head>' '<body>{}</body></html>' ).format(anchor_html) html_bytes = html.encode('utf-8') @@ -552,7 +552,7 @@ def make_fake_html_response(url): """ Create a fake requests.Response object. """ - html = dedent(u"""\ + html = dedent("""\ <html><head><meta name="api-version" value="2" /></head> <body> <a href="/abc-1.0.tar.gz#md5=000000000">abc-1.0.tar.gz</a> diff --git a/tests/unit/test_compat.py b/tests/unit/test_compat.py index 1b7a482a2a3..9411bad0a89 100644 --- a/tests/unit/test_compat.py +++ b/tests/unit/test_compat.py @@ -54,11 +54,9 @@ def test_get_path_uid_symlink_without_NOFOLLOW(tmpdir, monkeypatch): @pytest.mark.parametrize('data, expected', [ - ('abc', u'abc'), - # Test text (unicode in Python 2) input. - (u'abc', u'abc'), + ('abc', 'abc'), # Test text input with non-ascii characters. - (u'déf', u'déf'), + ('déf', 'déf'), ]) def test_str_to_display(data, expected): actual = str_to_display(data) @@ -70,13 +68,13 @@ def test_str_to_display(data, expected): @pytest.mark.parametrize('data, encoding, expected', [ # Test str input with non-ascii characters. - ('déf', 'utf-8', u'déf'), + ('déf', 'utf-8', 'déf'), # Test bytes input with non-ascii characters: - (u'déf'.encode('utf-8'), 'utf-8', u'déf'), + ('déf'.encode('utf-8'), 'utf-8', 'déf'), # Test a Windows encoding. - (u'déf'.encode('cp1252'), 'cp1252', u'déf'), + ('déf'.encode('cp1252'), 'cp1252', 'déf'), # Test a Windows encoding with incompatibly encoded text. - (u'déf'.encode('utf-8'), 'cp1252', u'déf'), + ('déf'.encode('utf-8'), 'cp1252', 'déf'), ]) def test_str_to_display__encoding(monkeypatch, data, encoding, expected): monkeypatch.setattr(locale, 'getpreferredencoding', lambda: encoding) @@ -90,7 +88,7 @@ def test_str_to_display__encoding(monkeypatch, data, encoding, expected): def test_str_to_display__decode_error(monkeypatch, caplog): monkeypatch.setattr(locale, 'getpreferredencoding', lambda: 'utf-8') # Encode with an incompatible encoding. - data = u'ab'.encode('utf-16') + data = 'ab'.encode('utf-16') actual = str_to_display(data) # Keep the expected value endian safe if sys.byteorder == "little": diff --git a/tests/unit/test_index.py b/tests/unit/test_index.py index 2ae847ae0b7..e719707ab78 100644 --- a/tests/unit/test_index.py +++ b/tests/unit/test_index.py @@ -131,9 +131,9 @@ def test_evaluate_link( ('bad metadata', False, (False, 'yanked for reason: bad metadata')), # Test a unicode string with a non-ascii character. - (u'curly quote: \u2018', True, (True, '1.12')), - (u'curly quote: \u2018', False, - (False, u'yanked for reason: curly quote: \u2018')), + ('curly quote: \u2018', True, (True, '1.12')), + ('curly quote: \u2018', False, + (False, 'yanked for reason: curly quote: \u2018')), ]) def test_evaluate_link__allow_yanked( self, yanked_reason, allow_yanked, expected, diff --git a/tests/unit/test_resolution_legacy_resolver.py b/tests/unit/test_resolution_legacy_resolver.py index 0388b42be5f..c4ff6492917 100644 --- a/tests/unit/test_resolution_legacy_resolver.py +++ b/tests/unit/test_resolution_legacy_resolver.py @@ -245,7 +245,7 @@ def test_sort_best_candidate__all_yanked(self, caplog, monkeypatch): # Test no reason given. ('', '<none given>'), # Test a unicode string with a non-ascii character. - (u'curly quote: \u2018', u'curly quote: \u2018'), + ('curly quote: \u2018', 'curly quote: \u2018'), ]) def test_sort_best_candidate__yanked_reason( self, caplog, monkeypatch, yanked_reason, expected_reason, diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 1996b35cb37..6ed479ae813 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -419,12 +419,12 @@ def test_rmtree_retries_for_3sec(tmpdir, monkeypatch): if sys.byteorder == "little": expected_byte_string = ( - u"b'\\xff\\xfe/\\x00p\\x00a\\x00t\\x00h\\x00/" + "b'\\xff\\xfe/\\x00p\\x00a\\x00t\\x00h\\x00/" "\\x00d\\x00\\xe9\\x00f\\x00'" ) elif sys.byteorder == "big": expected_byte_string = ( - u"b'\\xfe\\xff\\x00/\\x00p\\x00a\\x00t\\x00h\\" + "b'\\xfe\\xff\\x00/\\x00p\\x00a\\x00t\\x00h\\" "x00/\\x00d\\x00\\xe9\\x00f'" ) @@ -432,12 +432,12 @@ def test_rmtree_retries_for_3sec(tmpdir, monkeypatch): @pytest.mark.parametrize('path, fs_encoding, expected', [ (None, None, None), # Test passing a text (unicode) string. - (u'/path/déf', None, u'/path/déf'), + ('/path/déf', None, '/path/déf'), # Test a bytes object with a non-ascii character. - (u'/path/déf'.encode('utf-8'), 'utf-8', u'/path/déf'), + ('/path/déf'.encode('utf-8'), 'utf-8', '/path/déf'), # Test a bytes object with a character that can't be decoded. - (u'/path/déf'.encode('utf-8'), 'ascii', u"b'/path/d\\xc3\\xa9f'"), - (u'/path/déf'.encode('utf-16'), 'utf-8', expected_byte_string), + ('/path/déf'.encode('utf-8'), 'ascii', "b'/path/d\\xc3\\xa9f'"), + ('/path/déf'.encode('utf-16'), 'utf-8', expected_byte_string), ]) def test_path_to_display(monkeypatch, path, fs_encoding, expected): monkeypatch.setattr(sys, 'getfilesystemencoding', lambda: fs_encoding) @@ -572,17 +572,17 @@ def test_auto_decode_utf_16_be(self): assert auto_decode(data) == "Django==1.4.2" def test_auto_decode_no_bom(self): - assert auto_decode(b'foobar') == u'foobar' + assert auto_decode(b'foobar') == 'foobar' def test_auto_decode_pep263_headers(self): - latin1_req = u'# coding=latin1\n# Pas trop de café' + latin1_req = '# coding=latin1\n# Pas trop de café' assert auto_decode(latin1_req.encode('latin1')) == latin1_req def test_auto_decode_no_preferred_encoding(self): om, em = Mock(), Mock() om.return_value = 'ascii' em.return_value = None - data = u'data' + data = 'data' with patch('sys.getdefaultencoding', om): with patch('locale.getpreferredencoding', em): ret = auto_decode(data.encode(sys.getdefaultencoding())) diff --git a/tests/unit/test_utils_subprocess.py b/tests/unit/test_utils_subprocess.py index b0de2bf578d..c8ac7b81767 100644 --- a/tests/unit/test_utils_subprocess.py +++ b/tests/unit/test_utils_subprocess.py @@ -61,7 +61,7 @@ def test_make_subprocess_output_error__non_ascii_command_arg(monkeypatch): # Check in Python 2 that the str (bytes object) with the non-ascii # character has the encoding we expect. (This comes from the source # code encoding at the top of the file.) - assert cmd_args[1].decode('utf-8') == u'déf' + assert cmd_args[1].decode('utf-8') == 'déf' # We need to monkeypatch so the encoding will be correct on Windows. monkeypatch.setattr(locale, 'getpreferredencoding', lambda: 'utf-8') @@ -71,13 +71,13 @@ def test_make_subprocess_output_error__non_ascii_command_arg(monkeypatch): lines=[], exit_status=1, ) - expected = dedent(u"""\ + expected = dedent("""\ Command errored out with exit status 1: command: foo 'déf' cwd: /path/to/cwd Complete output (0 lines): ----------------------------------------""") - assert actual == expected, u'actual: {}'.format(actual) + assert actual == expected, 'actual: {}'.format(actual) @pytest.mark.skipif("sys.version_info < (3,)") @@ -115,7 +115,7 @@ def test_make_subprocess_output_error__non_ascii_cwd_python_2( Test a str (bytes object) cwd with a non-ascii character in Python 2. """ cmd_args = ['test'] - cwd = u'/path/to/cwd/déf'.encode(encoding) + cwd = '/path/to/cwd/déf'.encode(encoding) monkeypatch.setattr(sys, 'getfilesystemencoding', lambda: encoding) actual = make_subprocess_output_error( cmd_args=cmd_args, @@ -123,13 +123,13 @@ def test_make_subprocess_output_error__non_ascii_cwd_python_2( lines=[], exit_status=1, ) - expected = dedent(u"""\ + expected = dedent("""\ Command errored out with exit status 1: command: test cwd: /path/to/cwd/déf Complete output (0 lines): ----------------------------------------""") - assert actual == expected, u'actual: {}'.format(actual) + assert actual == expected, 'actual: {}'.format(actual) # This test is mainly important for checking unicode in Python 2. @@ -137,21 +137,21 @@ def test_make_subprocess_output_error__non_ascii_line(): """ Test a line with a non-ascii character. """ - lines = [u'curly-quote: \u2018\n'] + lines = ['curly-quote: \u2018\n'] actual = make_subprocess_output_error( cmd_args=['test'], cwd='/path/to/cwd', lines=lines, exit_status=1, ) - expected = dedent(u"""\ + expected = dedent("""\ Command errored out with exit status 1: command: test cwd: /path/to/cwd Complete output (1 lines): curly-quote: \u2018 ----------------------------------------""") - assert actual == expected, u'actual: {}'.format(actual) + assert actual == expected, 'actual: {}'.format(actual) class FakeSpinner(SpinnerInterface): diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index a97ec89da44..6e7adc2a54d 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -81,13 +81,13 @@ def test_get_legacy_build_wheel_path__multiple_names(caplog): @pytest.mark.parametrize( "console_scripts", [ - u"pip = pip._internal.main:pip", - u"pip:pip = pip._internal.main:pip", - u"進入點 = 套件.模組:函式", + "pip = pip._internal.main:pip", + "pip:pip = pip._internal.main:pip", + "進入點 = 套件.模組:函式", ], ) def test_get_entrypoints(console_scripts): - entry_points_text = u""" + entry_points_text = """ [console_scripts] {} [section] @@ -125,8 +125,8 @@ def test_get_entrypoints_no_entrypoints(): @pytest.mark.parametrize("outrows, expected", [ ([ - (u'', '', 'a'), - (u'', '', ''), + ('', '', 'a'), + ('', '', ''), ], [ ('', '', ''), ('', '', 'a'), @@ -134,16 +134,16 @@ def test_get_entrypoints_no_entrypoints(): ([ # Include an int to check avoiding the following error: # > TypeError: '<' not supported between instances of 'str' and 'int' - (u'', '', 1), - (u'', '', ''), + ('', '', 1), + ('', '', ''), ], [ ('', '', ''), ('', '', '1'), ]), ([ # Test the normalization correctly encode everything for csv.writer(). - (u'😉', '', 1), - (u'', '', ''), + ('😉', '', 1), + ('', '', ''), ], [ ('', '', ''), ('😉', '', '1'), @@ -160,7 +160,7 @@ def call_get_csv_rows_for_installed(tmpdir, text): # Test that an installed file appearing in RECORD has its filename # updated in the new RECORD file. - installed = {u'a': 'z'} + installed = {'a': 'z'} changed = set() generated = [] lib_dir = '/lib/dir' diff --git a/tools/automation/release/__init__.py b/tools/automation/release/__init__.py index 042723100a8..c1364cfc461 100644 --- a/tools/automation/release/__init__.py +++ b/tools/automation/release/__init__.py @@ -76,8 +76,8 @@ def generate_authors(filename: str) -> None: # Write our authors to the AUTHORS file with io.open(filename, "w", encoding="utf-8") as fp: - fp.write(u"\n".join(authors)) - fp.write(u"\n") + fp.write("\n".join(authors)) + fp.write("\n") def commit_file(session: Session, filename: str, *, message: str) -> None: From ba40f58ecc36206608f92b58111a1fd2b2bc85f6 Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Thu, 24 Dec 2020 09:35:33 -0800 Subject: [PATCH 2820/3170] Remove encoding cookie from Python source files Unnecessary since dropping Python 2. Python now decodes files as utf-8 by default. --- docs/html/conf.py | 2 -- src/pip/_internal/commands/wheel.py | 2 -- tests/functional/test_install_wheel.py | 2 -- tests/lib/path.py | 1 - tests/unit/test_compat.py | 2 -- tests/unit/test_utils.py | 2 -- tests/unit/test_utils_subprocess.py | 1 - tests/unit/test_wheel.py | 2 -- 8 files changed, 14 deletions(-) diff --git a/docs/html/conf.py b/docs/html/conf.py index 1f3131ce3c5..9e65cbe7a10 100644 --- a/docs/html/conf.py +++ b/docs/html/conf.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- -# # pip documentation build configuration file, created by # sphinx-quickstart on Tue Apr 22 22:08:49 2008 # diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index f9be310960f..13dad544c72 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import logging import os import shutil diff --git a/tests/functional/test_install_wheel.py b/tests/functional/test_install_wheel.py index 177e86db320..9bf965625ff 100644 --- a/tests/functional/test_install_wheel.py +++ b/tests/functional/test_install_wheel.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import csv import distutils import glob diff --git a/tests/lib/path.py b/tests/lib/path.py index 2a9e29d8cd2..a9dc29ad7a5 100644 --- a/tests/lib/path.py +++ b/tests/lib/path.py @@ -1,5 +1,4 @@ # flake8: noqa -# -*- coding: utf-8 -*- # Author: Aziz Köksal import glob import os diff --git a/tests/unit/test_compat.py b/tests/unit/test_compat.py index 9411bad0a89..cc024b570a7 100644 --- a/tests/unit/test_compat.py +++ b/tests/unit/test_compat.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - import locale import os import sys diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 6ed479ae813..57434669e54 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """ util tests diff --git a/tests/unit/test_utils_subprocess.py b/tests/unit/test_utils_subprocess.py index c8ac7b81767..fd73878c1a7 100644 --- a/tests/unit/test_utils_subprocess.py +++ b/tests/unit/test_utils_subprocess.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- import locale import sys from logging import DEBUG, ERROR, INFO, WARNING diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 6e7adc2a54d..0f7a3c0747b 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - """Tests for wheel binary packages and .dist-info.""" import csv import logging From 653f12b5e794e4a6ca26fbb540d15850dd7c4e1e Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Fri, 25 Dec 2020 08:24:11 -0800 Subject: [PATCH 2821/3170] Replace vendored ipaddress with stdlib The vendored copy is unnecessary since dropping Python 2 support. The module has been available in the stdlib since Python 3.3. --- ...b1-2151-45c3-baa0-b87e50d7e56d.trivial.rst | 0 src/pip/_internal/network/session.py | 3 +- src/pip/_internal/utils/compat.py | 15 +- src/pip/_vendor/ipaddress.LICENSE | 50 - src/pip/_vendor/ipaddress.py | 2420 ----------------- src/pip/_vendor/ipaddress.pyi | 1 - .../ssl_match_hostname/_implementation.py | 2 +- src/pip/_vendor/vendor.txt | 1 - 8 files changed, 4 insertions(+), 2488 deletions(-) create mode 100644 news/54754cb1-2151-45c3-baa0-b87e50d7e56d.trivial.rst delete mode 100644 src/pip/_vendor/ipaddress.LICENSE delete mode 100644 src/pip/_vendor/ipaddress.py delete mode 100644 src/pip/_vendor/ipaddress.pyi diff --git a/news/54754cb1-2151-45c3-baa0-b87e50d7e56d.trivial.rst b/news/54754cb1-2151-45c3-baa0-b87e50d7e56d.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py index 5839c4d28df..9cae32556b4 100644 --- a/src/pip/_internal/network/session.py +++ b/src/pip/_internal/network/session.py @@ -6,6 +6,7 @@ # mypy: disallow-untyped-defs=False import email.utils +import ipaddress import json import logging import mimetypes @@ -27,7 +28,7 @@ from pip._internal.network.cache import SafeFileCache # Import ssl from compat so the initial import occurs in only one place. -from pip._internal.utils.compat import has_tls, ipaddress +from pip._internal.utils.compat import has_tls from pip._internal.utils.glibc import libc_ver from pip._internal.utils.misc import ( build_url_from_netloc, diff --git a/src/pip/_internal/utils/compat.py b/src/pip/_internal/utils/compat.py index cc8ecd0f682..b71ddb4ba2e 100644 --- a/src/pip/_internal/utils/compat.py +++ b/src/pip/_internal/utils/compat.py @@ -24,21 +24,8 @@ def __call__(self, maxsize=None): # type: (Optional[int]) -> Callable[[F], F] raise NotImplementedError -try: - import ipaddress -except ImportError: - try: - from pip._vendor import ipaddress # type: ignore - except ImportError: - import ipaddr as ipaddress # type: ignore - ipaddress.ip_address = ipaddress.IPAddress # type: ignore - ipaddress.ip_network = ipaddress.IPNetwork # type: ignore - -__all__ = [ - "ipaddress", "console_to_str", - "get_path_uid", "stdlib_pkgs", "WINDOWS", -] +__all__ = ["console_to_str", "get_path_uid", "stdlib_pkgs", "WINDOWS"] logger = logging.getLogger(__name__) diff --git a/src/pip/_vendor/ipaddress.LICENSE b/src/pip/_vendor/ipaddress.LICENSE deleted file mode 100644 index 41bd16ba6c4..00000000000 --- a/src/pip/_vendor/ipaddress.LICENSE +++ /dev/null @@ -1,50 +0,0 @@ -This package is a modified version of cpython's ipaddress module. -It is therefore distributed under the PSF license, as follows: - -PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 --------------------------------------------- - -1. This LICENSE AGREEMENT is between the Python Software Foundation -("PSF"), and the Individual or Organization ("Licensee") accessing and -otherwise using this software ("Python") in source or binary form and -its associated documentation. - -2. Subject to the terms and conditions of this License Agreement, PSF hereby -grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, -analyze, test, perform and/or display publicly, prepare derivative works, -distribute, and otherwise use Python alone or in any derivative version, -provided, however, that PSF's License Agreement and PSF's notice of copyright, -i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, -2011, 2012, 2013, 2014 Python Software Foundation; All Rights Reserved" are -retained in Python alone or in any derivative version prepared by Licensee. - -3. In the event Licensee prepares a derivative work that is based on -or incorporates Python or any part thereof, and wants to make -the derivative work available to others as provided herein, then -Licensee hereby agrees to include in any such work a brief summary of -the changes made to Python. - -4. PSF is making Python available to Licensee on an "AS IS" -basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR -IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND -DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS -FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT -INFRINGE ANY THIRD PARTY RIGHTS. - -5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON -FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS -A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, -OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. - -6. This License Agreement will automatically terminate upon a material -breach of its terms and conditions. - -7. Nothing in this License Agreement shall be deemed to create any -relationship of agency, partnership, or joint venture between PSF and -Licensee. This License Agreement does not grant permission to use PSF -trademarks or trade name in a trademark sense to endorse or promote -products or services of Licensee, or any third party. - -8. By copying, installing or otherwise using Python, Licensee -agrees to be bound by the terms and conditions of this License -Agreement. diff --git a/src/pip/_vendor/ipaddress.py b/src/pip/_vendor/ipaddress.py deleted file mode 100644 index 3e6f9e499c3..00000000000 --- a/src/pip/_vendor/ipaddress.py +++ /dev/null @@ -1,2420 +0,0 @@ -# Copyright 2007 Google Inc. -# Licensed to PSF under a Contributor Agreement. - -"""A fast, lightweight IPv4/IPv6 manipulation library in Python. - -This library is used to create/poke/manipulate IPv4 and IPv6 addresses -and networks. - -""" - -from __future__ import unicode_literals - - -import itertools -import struct - -__version__ = '1.0.23' - -# Compatibility functions -_compat_int_types = (int,) -try: - _compat_int_types = (int, long) -except NameError: - pass -try: - _compat_str = unicode -except NameError: - _compat_str = str - assert bytes != str -if b'\0'[0] == 0: # Python 3 semantics - def _compat_bytes_to_byte_vals(byt): - return byt -else: - def _compat_bytes_to_byte_vals(byt): - return [struct.unpack(b'!B', b)[0] for b in byt] -try: - _compat_int_from_byte_vals = int.from_bytes -except AttributeError: - def _compat_int_from_byte_vals(bytvals, endianess): - assert endianess == 'big' - res = 0 - for bv in bytvals: - assert isinstance(bv, _compat_int_types) - res = (res << 8) + bv - return res - - -def _compat_to_bytes(intval, length, endianess): - assert isinstance(intval, _compat_int_types) - assert endianess == 'big' - if length == 4: - if intval < 0 or intval >= 2 ** 32: - raise struct.error("integer out of range for 'I' format code") - return struct.pack(b'!I', intval) - elif length == 16: - if intval < 0 or intval >= 2 ** 128: - raise struct.error("integer out of range for 'QQ' format code") - return struct.pack(b'!QQ', intval >> 64, intval & 0xffffffffffffffff) - else: - raise NotImplementedError() - - -if hasattr(int, 'bit_length'): - # Not int.bit_length , since that won't work in 2.7 where long exists - def _compat_bit_length(i): - return i.bit_length() -else: - def _compat_bit_length(i): - for res in itertools.count(): - if i >> res == 0: - return res - - -def _compat_range(start, end, step=1): - assert step > 0 - i = start - while i < end: - yield i - i += step - - -class _TotalOrderingMixin(object): - __slots__ = () - - # Helper that derives the other comparison operations from - # __lt__ and __eq__ - # We avoid functools.total_ordering because it doesn't handle - # NotImplemented correctly yet (http://bugs.python.org/issue10042) - def __eq__(self, other): - raise NotImplementedError - - def __ne__(self, other): - equal = self.__eq__(other) - if equal is NotImplemented: - return NotImplemented - return not equal - - def __lt__(self, other): - raise NotImplementedError - - def __le__(self, other): - less = self.__lt__(other) - if less is NotImplemented or not less: - return self.__eq__(other) - return less - - def __gt__(self, other): - less = self.__lt__(other) - if less is NotImplemented: - return NotImplemented - equal = self.__eq__(other) - if equal is NotImplemented: - return NotImplemented - return not (less or equal) - - def __ge__(self, other): - less = self.__lt__(other) - if less is NotImplemented: - return NotImplemented - return not less - - -IPV4LENGTH = 32 -IPV6LENGTH = 128 - - -class AddressValueError(ValueError): - """A Value Error related to the address.""" - - -class NetmaskValueError(ValueError): - """A Value Error related to the netmask.""" - - -def ip_address(address): - """Take an IP string/int and return an object of the correct type. - - Args: - address: A string or integer, the IP address. Either IPv4 or - IPv6 addresses may be supplied; integers less than 2**32 will - be considered to be IPv4 by default. - - Returns: - An IPv4Address or IPv6Address object. - - Raises: - ValueError: if the *address* passed isn't either a v4 or a v6 - address - - """ - try: - return IPv4Address(address) - except (AddressValueError, NetmaskValueError): - pass - - try: - return IPv6Address(address) - except (AddressValueError, NetmaskValueError): - pass - - if isinstance(address, bytes): - raise AddressValueError( - '%r does not appear to be an IPv4 or IPv6 address. ' - 'Did you pass in a bytes (str in Python 2) instead of' - ' a unicode object?' % address) - - raise ValueError('%r does not appear to be an IPv4 or IPv6 address' % - address) - - -def ip_network(address, strict=True): - """Take an IP string/int and return an object of the correct type. - - Args: - address: A string or integer, the IP network. Either IPv4 or - IPv6 networks may be supplied; integers less than 2**32 will - be considered to be IPv4 by default. - - Returns: - An IPv4Network or IPv6Network object. - - Raises: - ValueError: if the string passed isn't either a v4 or a v6 - address. Or if the network has host bits set. - - """ - try: - return IPv4Network(address, strict) - except (AddressValueError, NetmaskValueError): - pass - - try: - return IPv6Network(address, strict) - except (AddressValueError, NetmaskValueError): - pass - - if isinstance(address, bytes): - raise AddressValueError( - '%r does not appear to be an IPv4 or IPv6 network. ' - 'Did you pass in a bytes (str in Python 2) instead of' - ' a unicode object?' % address) - - raise ValueError('%r does not appear to be an IPv4 or IPv6 network' % - address) - - -def ip_interface(address): - """Take an IP string/int and return an object of the correct type. - - Args: - address: A string or integer, the IP address. Either IPv4 or - IPv6 addresses may be supplied; integers less than 2**32 will - be considered to be IPv4 by default. - - Returns: - An IPv4Interface or IPv6Interface object. - - Raises: - ValueError: if the string passed isn't either a v4 or a v6 - address. - - Notes: - The IPv?Interface classes describe an Address on a particular - Network, so they're basically a combination of both the Address - and Network classes. - - """ - try: - return IPv4Interface(address) - except (AddressValueError, NetmaskValueError): - pass - - try: - return IPv6Interface(address) - except (AddressValueError, NetmaskValueError): - pass - - raise ValueError('%r does not appear to be an IPv4 or IPv6 interface' % - address) - - -def v4_int_to_packed(address): - """Represent an address as 4 packed bytes in network (big-endian) order. - - Args: - address: An integer representation of an IPv4 IP address. - - Returns: - The integer address packed as 4 bytes in network (big-endian) order. - - Raises: - ValueError: If the integer is negative or too large to be an - IPv4 IP address. - - """ - try: - return _compat_to_bytes(address, 4, 'big') - except (struct.error, OverflowError): - raise ValueError("Address negative or too large for IPv4") - - -def v6_int_to_packed(address): - """Represent an address as 16 packed bytes in network (big-endian) order. - - Args: - address: An integer representation of an IPv6 IP address. - - Returns: - The integer address packed as 16 bytes in network (big-endian) order. - - """ - try: - return _compat_to_bytes(address, 16, 'big') - except (struct.error, OverflowError): - raise ValueError("Address negative or too large for IPv6") - - -def _split_optional_netmask(address): - """Helper to split the netmask and raise AddressValueError if needed""" - addr = _compat_str(address).split('/') - if len(addr) > 2: - raise AddressValueError("Only one '/' permitted in %r" % address) - return addr - - -def _find_address_range(addresses): - """Find a sequence of sorted deduplicated IPv#Address. - - Args: - addresses: a list of IPv#Address objects. - - Yields: - A tuple containing the first and last IP addresses in the sequence. - - """ - it = iter(addresses) - first = last = next(it) - for ip in it: - if ip._ip != last._ip + 1: - yield first, last - first = ip - last = ip - yield first, last - - -def _count_righthand_zero_bits(number, bits): - """Count the number of zero bits on the right hand side. - - Args: - number: an integer. - bits: maximum number of bits to count. - - Returns: - The number of zero bits on the right hand side of the number. - - """ - if number == 0: - return bits - return min(bits, _compat_bit_length(~number & (number - 1))) - - -def summarize_address_range(first, last): - """Summarize a network range given the first and last IP addresses. - - Example: - >>> list(summarize_address_range(IPv4Address('192.0.2.0'), - ... IPv4Address('192.0.2.130'))) - ... #doctest: +NORMALIZE_WHITESPACE - [IPv4Network('192.0.2.0/25'), IPv4Network('192.0.2.128/31'), - IPv4Network('192.0.2.130/32')] - - Args: - first: the first IPv4Address or IPv6Address in the range. - last: the last IPv4Address or IPv6Address in the range. - - Returns: - An iterator of the summarized IPv(4|6) network objects. - - Raise: - TypeError: - If the first and last objects are not IP addresses. - If the first and last objects are not the same version. - ValueError: - If the last object is not greater than the first. - If the version of the first address is not 4 or 6. - - """ - if (not (isinstance(first, _BaseAddress) and - isinstance(last, _BaseAddress))): - raise TypeError('first and last must be IP addresses, not networks') - if first.version != last.version: - raise TypeError("%s and %s are not of the same version" % ( - first, last)) - if first > last: - raise ValueError('last IP address must be greater than first') - - if first.version == 4: - ip = IPv4Network - elif first.version == 6: - ip = IPv6Network - else: - raise ValueError('unknown IP version') - - ip_bits = first._max_prefixlen - first_int = first._ip - last_int = last._ip - while first_int <= last_int: - nbits = min(_count_righthand_zero_bits(first_int, ip_bits), - _compat_bit_length(last_int - first_int + 1) - 1) - net = ip((first_int, ip_bits - nbits)) - yield net - first_int += 1 << nbits - if first_int - 1 == ip._ALL_ONES: - break - - -def _collapse_addresses_internal(addresses): - """Loops through the addresses, collapsing concurrent netblocks. - - Example: - - ip1 = IPv4Network('192.0.2.0/26') - ip2 = IPv4Network('192.0.2.64/26') - ip3 = IPv4Network('192.0.2.128/26') - ip4 = IPv4Network('192.0.2.192/26') - - _collapse_addresses_internal([ip1, ip2, ip3, ip4]) -> - [IPv4Network('192.0.2.0/24')] - - This shouldn't be called directly; it is called via - collapse_addresses([]). - - Args: - addresses: A list of IPv4Network's or IPv6Network's - - Returns: - A list of IPv4Network's or IPv6Network's depending on what we were - passed. - - """ - # First merge - to_merge = list(addresses) - subnets = {} - while to_merge: - net = to_merge.pop() - supernet = net.supernet() - existing = subnets.get(supernet) - if existing is None: - subnets[supernet] = net - elif existing != net: - # Merge consecutive subnets - del subnets[supernet] - to_merge.append(supernet) - # Then iterate over resulting networks, skipping subsumed subnets - last = None - for net in sorted(subnets.values()): - if last is not None: - # Since they are sorted, - # last.network_address <= net.network_address is a given. - if last.broadcast_address >= net.broadcast_address: - continue - yield net - last = net - - -def collapse_addresses(addresses): - """Collapse a list of IP objects. - - Example: - collapse_addresses([IPv4Network('192.0.2.0/25'), - IPv4Network('192.0.2.128/25')]) -> - [IPv4Network('192.0.2.0/24')] - - Args: - addresses: An iterator of IPv4Network or IPv6Network objects. - - Returns: - An iterator of the collapsed IPv(4|6)Network objects. - - Raises: - TypeError: If passed a list of mixed version objects. - - """ - addrs = [] - ips = [] - nets = [] - - # split IP addresses and networks - for ip in addresses: - if isinstance(ip, _BaseAddress): - if ips and ips[-1]._version != ip._version: - raise TypeError("%s and %s are not of the same version" % ( - ip, ips[-1])) - ips.append(ip) - elif ip._prefixlen == ip._max_prefixlen: - if ips and ips[-1]._version != ip._version: - raise TypeError("%s and %s are not of the same version" % ( - ip, ips[-1])) - try: - ips.append(ip.ip) - except AttributeError: - ips.append(ip.network_address) - else: - if nets and nets[-1]._version != ip._version: - raise TypeError("%s and %s are not of the same version" % ( - ip, nets[-1])) - nets.append(ip) - - # sort and dedup - ips = sorted(set(ips)) - - # find consecutive address ranges in the sorted sequence and summarize them - if ips: - for first, last in _find_address_range(ips): - addrs.extend(summarize_address_range(first, last)) - - return _collapse_addresses_internal(addrs + nets) - - -def get_mixed_type_key(obj): - """Return a key suitable for sorting between networks and addresses. - - Address and Network objects are not sortable by default; they're - fundamentally different so the expression - - IPv4Address('192.0.2.0') <= IPv4Network('192.0.2.0/24') - - doesn't make any sense. There are some times however, where you may wish - to have ipaddress sort these for you anyway. If you need to do this, you - can use this function as the key= argument to sorted(). - - Args: - obj: either a Network or Address object. - Returns: - appropriate key. - - """ - if isinstance(obj, _BaseNetwork): - return obj._get_networks_key() - elif isinstance(obj, _BaseAddress): - return obj._get_address_key() - return NotImplemented - - -class _IPAddressBase(_TotalOrderingMixin): - - """The mother class.""" - - __slots__ = () - - @property - def exploded(self): - """Return the longhand version of the IP address as a string.""" - return self._explode_shorthand_ip_string() - - @property - def compressed(self): - """Return the shorthand version of the IP address as a string.""" - return _compat_str(self) - - @property - def reverse_pointer(self): - """The name of the reverse DNS pointer for the IP address, e.g.: - >>> ipaddress.ip_address("127.0.0.1").reverse_pointer - '1.0.0.127.in-addr.arpa' - >>> ipaddress.ip_address("2001:db8::1").reverse_pointer - '1.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.0.8.b.d.0.1.0.0.2.ip6.arpa' - - """ - return self._reverse_pointer() - - @property - def version(self): - msg = '%200s has no version specified' % (type(self),) - raise NotImplementedError(msg) - - def _check_int_address(self, address): - if address < 0: - msg = "%d (< 0) is not permitted as an IPv%d address" - raise AddressValueError(msg % (address, self._version)) - if address > self._ALL_ONES: - msg = "%d (>= 2**%d) is not permitted as an IPv%d address" - raise AddressValueError(msg % (address, self._max_prefixlen, - self._version)) - - def _check_packed_address(self, address, expected_len): - address_len = len(address) - if address_len != expected_len: - msg = ( - '%r (len %d != %d) is not permitted as an IPv%d address. ' - 'Did you pass in a bytes (str in Python 2) instead of' - ' a unicode object?') - raise AddressValueError(msg % (address, address_len, - expected_len, self._version)) - - @classmethod - def _ip_int_from_prefix(cls, prefixlen): - """Turn the prefix length into a bitwise netmask - - Args: - prefixlen: An integer, the prefix length. - - Returns: - An integer. - - """ - return cls._ALL_ONES ^ (cls._ALL_ONES >> prefixlen) - - @classmethod - def _prefix_from_ip_int(cls, ip_int): - """Return prefix length from the bitwise netmask. - - Args: - ip_int: An integer, the netmask in expanded bitwise format - - Returns: - An integer, the prefix length. - - Raises: - ValueError: If the input intermingles zeroes & ones - """ - trailing_zeroes = _count_righthand_zero_bits(ip_int, - cls._max_prefixlen) - prefixlen = cls._max_prefixlen - trailing_zeroes - leading_ones = ip_int >> trailing_zeroes - all_ones = (1 << prefixlen) - 1 - if leading_ones != all_ones: - byteslen = cls._max_prefixlen // 8 - details = _compat_to_bytes(ip_int, byteslen, 'big') - msg = 'Netmask pattern %r mixes zeroes & ones' - raise ValueError(msg % details) - return prefixlen - - @classmethod - def _report_invalid_netmask(cls, netmask_str): - msg = '%r is not a valid netmask' % netmask_str - raise NetmaskValueError(msg) - - @classmethod - def _prefix_from_prefix_string(cls, prefixlen_str): - """Return prefix length from a numeric string - - Args: - prefixlen_str: The string to be converted - - Returns: - An integer, the prefix length. - - Raises: - NetmaskValueError: If the input is not a valid netmask - """ - # int allows a leading +/- as well as surrounding whitespace, - # so we ensure that isn't the case - if not _BaseV4._DECIMAL_DIGITS.issuperset(prefixlen_str): - cls._report_invalid_netmask(prefixlen_str) - try: - prefixlen = int(prefixlen_str) - except ValueError: - cls._report_invalid_netmask(prefixlen_str) - if not (0 <= prefixlen <= cls._max_prefixlen): - cls._report_invalid_netmask(prefixlen_str) - return prefixlen - - @classmethod - def _prefix_from_ip_string(cls, ip_str): - """Turn a netmask/hostmask string into a prefix length - - Args: - ip_str: The netmask/hostmask to be converted - - Returns: - An integer, the prefix length. - - Raises: - NetmaskValueError: If the input is not a valid netmask/hostmask - """ - # Parse the netmask/hostmask like an IP address. - try: - ip_int = cls._ip_int_from_string(ip_str) - except AddressValueError: - cls._report_invalid_netmask(ip_str) - - # Try matching a netmask (this would be /1*0*/ as a bitwise regexp). - # Note that the two ambiguous cases (all-ones and all-zeroes) are - # treated as netmasks. - try: - return cls._prefix_from_ip_int(ip_int) - except ValueError: - pass - - # Invert the bits, and try matching a /0+1+/ hostmask instead. - ip_int ^= cls._ALL_ONES - try: - return cls._prefix_from_ip_int(ip_int) - except ValueError: - cls._report_invalid_netmask(ip_str) - - def __reduce__(self): - return self.__class__, (_compat_str(self),) - - -class _BaseAddress(_IPAddressBase): - - """A generic IP object. - - This IP class contains the version independent methods which are - used by single IP addresses. - """ - - __slots__ = () - - def __int__(self): - return self._ip - - def __eq__(self, other): - try: - return (self._ip == other._ip and - self._version == other._version) - except AttributeError: - return NotImplemented - - def __lt__(self, other): - if not isinstance(other, _IPAddressBase): - return NotImplemented - if not isinstance(other, _BaseAddress): - raise TypeError('%s and %s are not of the same type' % ( - self, other)) - if self._version != other._version: - raise TypeError('%s and %s are not of the same version' % ( - self, other)) - if self._ip != other._ip: - return self._ip < other._ip - return False - - # Shorthand for Integer addition and subtraction. This is not - # meant to ever support addition/subtraction of addresses. - def __add__(self, other): - if not isinstance(other, _compat_int_types): - return NotImplemented - return self.__class__(int(self) + other) - - def __sub__(self, other): - if not isinstance(other, _compat_int_types): - return NotImplemented - return self.__class__(int(self) - other) - - def __repr__(self): - return '%s(%r)' % (self.__class__.__name__, _compat_str(self)) - - def __str__(self): - return _compat_str(self._string_from_ip_int(self._ip)) - - def __hash__(self): - return hash(hex(int(self._ip))) - - def _get_address_key(self): - return (self._version, self) - - def __reduce__(self): - return self.__class__, (self._ip,) - - -class _BaseNetwork(_IPAddressBase): - - """A generic IP network object. - - This IP class contains the version independent methods which are - used by networks. - - """ - def __init__(self, address): - self._cache = {} - - def __repr__(self): - return '%s(%r)' % (self.__class__.__name__, _compat_str(self)) - - def __str__(self): - return '%s/%d' % (self.network_address, self.prefixlen) - - def hosts(self): - """Generate Iterator over usable hosts in a network. - - This is like __iter__ except it doesn't return the network - or broadcast addresses. - - """ - network = int(self.network_address) - broadcast = int(self.broadcast_address) - for x in _compat_range(network + 1, broadcast): - yield self._address_class(x) - - def __iter__(self): - network = int(self.network_address) - broadcast = int(self.broadcast_address) - for x in _compat_range(network, broadcast + 1): - yield self._address_class(x) - - def __getitem__(self, n): - network = int(self.network_address) - broadcast = int(self.broadcast_address) - if n >= 0: - if network + n > broadcast: - raise IndexError('address out of range') - return self._address_class(network + n) - else: - n += 1 - if broadcast + n < network: - raise IndexError('address out of range') - return self._address_class(broadcast + n) - - def __lt__(self, other): - if not isinstance(other, _IPAddressBase): - return NotImplemented - if not isinstance(other, _BaseNetwork): - raise TypeError('%s and %s are not of the same type' % ( - self, other)) - if self._version != other._version: - raise TypeError('%s and %s are not of the same version' % ( - self, other)) - if self.network_address != other.network_address: - return self.network_address < other.network_address - if self.netmask != other.netmask: - return self.netmask < other.netmask - return False - - def __eq__(self, other): - try: - return (self._version == other._version and - self.network_address == other.network_address and - int(self.netmask) == int(other.netmask)) - except AttributeError: - return NotImplemented - - def __hash__(self): - return hash(int(self.network_address) ^ int(self.netmask)) - - def __contains__(self, other): - # always false if one is v4 and the other is v6. - if self._version != other._version: - return False - # dealing with another network. - if isinstance(other, _BaseNetwork): - return False - # dealing with another address - else: - # address - return (int(self.network_address) <= int(other._ip) <= - int(self.broadcast_address)) - - def overlaps(self, other): - """Tell if self is partly contained in other.""" - return self.network_address in other or ( - self.broadcast_address in other or ( - other.network_address in self or ( - other.broadcast_address in self))) - - @property - def broadcast_address(self): - x = self._cache.get('broadcast_address') - if x is None: - x = self._address_class(int(self.network_address) | - int(self.hostmask)) - self._cache['broadcast_address'] = x - return x - - @property - def hostmask(self): - x = self._cache.get('hostmask') - if x is None: - x = self._address_class(int(self.netmask) ^ self._ALL_ONES) - self._cache['hostmask'] = x - return x - - @property - def with_prefixlen(self): - return '%s/%d' % (self.network_address, self._prefixlen) - - @property - def with_netmask(self): - return '%s/%s' % (self.network_address, self.netmask) - - @property - def with_hostmask(self): - return '%s/%s' % (self.network_address, self.hostmask) - - @property - def num_addresses(self): - """Number of hosts in the current subnet.""" - return int(self.broadcast_address) - int(self.network_address) + 1 - - @property - def _address_class(self): - # Returning bare address objects (rather than interfaces) allows for - # more consistent behaviour across the network address, broadcast - # address and individual host addresses. - msg = '%200s has no associated address class' % (type(self),) - raise NotImplementedError(msg) - - @property - def prefixlen(self): - return self._prefixlen - - def address_exclude(self, other): - """Remove an address from a larger block. - - For example: - - addr1 = ip_network('192.0.2.0/28') - addr2 = ip_network('192.0.2.1/32') - list(addr1.address_exclude(addr2)) = - [IPv4Network('192.0.2.0/32'), IPv4Network('192.0.2.2/31'), - IPv4Network('192.0.2.4/30'), IPv4Network('192.0.2.8/29')] - - or IPv6: - - addr1 = ip_network('2001:db8::1/32') - addr2 = ip_network('2001:db8::1/128') - list(addr1.address_exclude(addr2)) = - [ip_network('2001:db8::1/128'), - ip_network('2001:db8::2/127'), - ip_network('2001:db8::4/126'), - ip_network('2001:db8::8/125'), - ... - ip_network('2001:db8:8000::/33')] - - Args: - other: An IPv4Network or IPv6Network object of the same type. - - Returns: - An iterator of the IPv(4|6)Network objects which is self - minus other. - - Raises: - TypeError: If self and other are of differing address - versions, or if other is not a network object. - ValueError: If other is not completely contained by self. - - """ - if not self._version == other._version: - raise TypeError("%s and %s are not of the same version" % ( - self, other)) - - if not isinstance(other, _BaseNetwork): - raise TypeError("%s is not a network object" % other) - - if not other.subnet_of(self): - raise ValueError('%s not contained in %s' % (other, self)) - if other == self: - return - - # Make sure we're comparing the network of other. - other = other.__class__('%s/%s' % (other.network_address, - other.prefixlen)) - - s1, s2 = self.subnets() - while s1 != other and s2 != other: - if other.subnet_of(s1): - yield s2 - s1, s2 = s1.subnets() - elif other.subnet_of(s2): - yield s1 - s1, s2 = s2.subnets() - else: - # If we got here, there's a bug somewhere. - raise AssertionError('Error performing exclusion: ' - 's1: %s s2: %s other: %s' % - (s1, s2, other)) - if s1 == other: - yield s2 - elif s2 == other: - yield s1 - else: - # If we got here, there's a bug somewhere. - raise AssertionError('Error performing exclusion: ' - 's1: %s s2: %s other: %s' % - (s1, s2, other)) - - def compare_networks(self, other): - """Compare two IP objects. - - This is only concerned about the comparison of the integer - representation of the network addresses. This means that the - host bits aren't considered at all in this method. If you want - to compare host bits, you can easily enough do a - 'HostA._ip < HostB._ip' - - Args: - other: An IP object. - - Returns: - If the IP versions of self and other are the same, returns: - - -1 if self < other: - eg: IPv4Network('192.0.2.0/25') < IPv4Network('192.0.2.128/25') - IPv6Network('2001:db8::1000/124') < - IPv6Network('2001:db8::2000/124') - 0 if self == other - eg: IPv4Network('192.0.2.0/24') == IPv4Network('192.0.2.0/24') - IPv6Network('2001:db8::1000/124') == - IPv6Network('2001:db8::1000/124') - 1 if self > other - eg: IPv4Network('192.0.2.128/25') > IPv4Network('192.0.2.0/25') - IPv6Network('2001:db8::2000/124') > - IPv6Network('2001:db8::1000/124') - - Raises: - TypeError if the IP versions are different. - - """ - # does this need to raise a ValueError? - if self._version != other._version: - raise TypeError('%s and %s are not of the same type' % ( - self, other)) - # self._version == other._version below here: - if self.network_address < other.network_address: - return -1 - if self.network_address > other.network_address: - return 1 - # self.network_address == other.network_address below here: - if self.netmask < other.netmask: - return -1 - if self.netmask > other.netmask: - return 1 - return 0 - - def _get_networks_key(self): - """Network-only key function. - - Returns an object that identifies this address' network and - netmask. This function is a suitable "key" argument for sorted() - and list.sort(). - - """ - return (self._version, self.network_address, self.netmask) - - def subnets(self, prefixlen_diff=1, new_prefix=None): - """The subnets which join to make the current subnet. - - In the case that self contains only one IP - (self._prefixlen == 32 for IPv4 or self._prefixlen == 128 - for IPv6), yield an iterator with just ourself. - - Args: - prefixlen_diff: An integer, the amount the prefix length - should be increased by. This should not be set if - new_prefix is also set. - new_prefix: The desired new prefix length. This must be a - larger number (smaller prefix) than the existing prefix. - This should not be set if prefixlen_diff is also set. - - Returns: - An iterator of IPv(4|6) objects. - - Raises: - ValueError: The prefixlen_diff is too small or too large. - OR - prefixlen_diff and new_prefix are both set or new_prefix - is a smaller number than the current prefix (smaller - number means a larger network) - - """ - if self._prefixlen == self._max_prefixlen: - yield self - return - - if new_prefix is not None: - if new_prefix < self._prefixlen: - raise ValueError('new prefix must be longer') - if prefixlen_diff != 1: - raise ValueError('cannot set prefixlen_diff and new_prefix') - prefixlen_diff = new_prefix - self._prefixlen - - if prefixlen_diff < 0: - raise ValueError('prefix length diff must be > 0') - new_prefixlen = self._prefixlen + prefixlen_diff - - if new_prefixlen > self._max_prefixlen: - raise ValueError( - 'prefix length diff %d is invalid for netblock %s' % ( - new_prefixlen, self)) - - start = int(self.network_address) - end = int(self.broadcast_address) + 1 - step = (int(self.hostmask) + 1) >> prefixlen_diff - for new_addr in _compat_range(start, end, step): - current = self.__class__((new_addr, new_prefixlen)) - yield current - - def supernet(self, prefixlen_diff=1, new_prefix=None): - """The supernet containing the current network. - - Args: - prefixlen_diff: An integer, the amount the prefix length of - the network should be decreased by. For example, given a - /24 network and a prefixlen_diff of 3, a supernet with a - /21 netmask is returned. - - Returns: - An IPv4 network object. - - Raises: - ValueError: If self.prefixlen - prefixlen_diff < 0. I.e., you have - a negative prefix length. - OR - If prefixlen_diff and new_prefix are both set or new_prefix is a - larger number than the current prefix (larger number means a - smaller network) - - """ - if self._prefixlen == 0: - return self - - if new_prefix is not None: - if new_prefix > self._prefixlen: - raise ValueError('new prefix must be shorter') - if prefixlen_diff != 1: - raise ValueError('cannot set prefixlen_diff and new_prefix') - prefixlen_diff = self._prefixlen - new_prefix - - new_prefixlen = self.prefixlen - prefixlen_diff - if new_prefixlen < 0: - raise ValueError( - 'current prefixlen is %d, cannot have a prefixlen_diff of %d' % - (self.prefixlen, prefixlen_diff)) - return self.__class__(( - int(self.network_address) & (int(self.netmask) << prefixlen_diff), - new_prefixlen)) - - @property - def is_multicast(self): - """Test if the address is reserved for multicast use. - - Returns: - A boolean, True if the address is a multicast address. - See RFC 2373 2.7 for details. - - """ - return (self.network_address.is_multicast and - self.broadcast_address.is_multicast) - - @staticmethod - def _is_subnet_of(a, b): - try: - # Always false if one is v4 and the other is v6. - if a._version != b._version: - raise TypeError( - "%s and %s are not of the same version" % (a, b)) - return (b.network_address <= a.network_address and - b.broadcast_address >= a.broadcast_address) - except AttributeError: - raise TypeError("Unable to test subnet containment " - "between %s and %s" % (a, b)) - - def subnet_of(self, other): - """Return True if this network is a subnet of other.""" - return self._is_subnet_of(self, other) - - def supernet_of(self, other): - """Return True if this network is a supernet of other.""" - return self._is_subnet_of(other, self) - - @property - def is_reserved(self): - """Test if the address is otherwise IETF reserved. - - Returns: - A boolean, True if the address is within one of the - reserved IPv6 Network ranges. - - """ - return (self.network_address.is_reserved and - self.broadcast_address.is_reserved) - - @property - def is_link_local(self): - """Test if the address is reserved for link-local. - - Returns: - A boolean, True if the address is reserved per RFC 4291. - - """ - return (self.network_address.is_link_local and - self.broadcast_address.is_link_local) - - @property - def is_private(self): - """Test if this address is allocated for private networks. - - Returns: - A boolean, True if the address is reserved per - iana-ipv4-special-registry or iana-ipv6-special-registry. - - """ - return (self.network_address.is_private and - self.broadcast_address.is_private) - - @property - def is_global(self): - """Test if this address is allocated for public networks. - - Returns: - A boolean, True if the address is not reserved per - iana-ipv4-special-registry or iana-ipv6-special-registry. - - """ - return not self.is_private - - @property - def is_unspecified(self): - """Test if the address is unspecified. - - Returns: - A boolean, True if this is the unspecified address as defined in - RFC 2373 2.5.2. - - """ - return (self.network_address.is_unspecified and - self.broadcast_address.is_unspecified) - - @property - def is_loopback(self): - """Test if the address is a loopback address. - - Returns: - A boolean, True if the address is a loopback address as defined in - RFC 2373 2.5.3. - - """ - return (self.network_address.is_loopback and - self.broadcast_address.is_loopback) - - -class _BaseV4(object): - - """Base IPv4 object. - - The following methods are used by IPv4 objects in both single IP - addresses and networks. - - """ - - __slots__ = () - _version = 4 - # Equivalent to 255.255.255.255 or 32 bits of 1's. - _ALL_ONES = (2 ** IPV4LENGTH) - 1 - _DECIMAL_DIGITS = frozenset('0123456789') - - # the valid octets for host and netmasks. only useful for IPv4. - _valid_mask_octets = frozenset([255, 254, 252, 248, 240, 224, 192, 128, 0]) - - _max_prefixlen = IPV4LENGTH - # There are only a handful of valid v4 netmasks, so we cache them all - # when constructed (see _make_netmask()). - _netmask_cache = {} - - def _explode_shorthand_ip_string(self): - return _compat_str(self) - - @classmethod - def _make_netmask(cls, arg): - """Make a (netmask, prefix_len) tuple from the given argument. - - Argument can be: - - an integer (the prefix length) - - a string representing the prefix length (e.g. "24") - - a string representing the prefix netmask (e.g. "255.255.255.0") - """ - if arg not in cls._netmask_cache: - if isinstance(arg, _compat_int_types): - prefixlen = arg - else: - try: - # Check for a netmask in prefix length form - prefixlen = cls._prefix_from_prefix_string(arg) - except NetmaskValueError: - # Check for a netmask or hostmask in dotted-quad form. - # This may raise NetmaskValueError. - prefixlen = cls._prefix_from_ip_string(arg) - netmask = IPv4Address(cls._ip_int_from_prefix(prefixlen)) - cls._netmask_cache[arg] = netmask, prefixlen - return cls._netmask_cache[arg] - - @classmethod - def _ip_int_from_string(cls, ip_str): - """Turn the given IP string into an integer for comparison. - - Args: - ip_str: A string, the IP ip_str. - - Returns: - The IP ip_str as an integer. - - Raises: - AddressValueError: if ip_str isn't a valid IPv4 Address. - - """ - if not ip_str: - raise AddressValueError('Address cannot be empty') - - octets = ip_str.split('.') - if len(octets) != 4: - raise AddressValueError("Expected 4 octets in %r" % ip_str) - - try: - return _compat_int_from_byte_vals( - map(cls._parse_octet, octets), 'big') - except ValueError as exc: - raise AddressValueError("%s in %r" % (exc, ip_str)) - - @classmethod - def _parse_octet(cls, octet_str): - """Convert a decimal octet into an integer. - - Args: - octet_str: A string, the number to parse. - - Returns: - The octet as an integer. - - Raises: - ValueError: if the octet isn't strictly a decimal from [0..255]. - - """ - if not octet_str: - raise ValueError("Empty octet not permitted") - # Whitelist the characters, since int() allows a lot of bizarre stuff. - if not cls._DECIMAL_DIGITS.issuperset(octet_str): - msg = "Only decimal digits permitted in %r" - raise ValueError(msg % octet_str) - # We do the length check second, since the invalid character error - # is likely to be more informative for the user - if len(octet_str) > 3: - msg = "At most 3 characters permitted in %r" - raise ValueError(msg % octet_str) - # Convert to integer (we know digits are legal) - octet_int = int(octet_str, 10) - # Any octets that look like they *might* be written in octal, - # and which don't look exactly the same in both octal and - # decimal are rejected as ambiguous - if octet_int > 7 and octet_str[0] == '0': - msg = "Ambiguous (octal/decimal) value in %r not permitted" - raise ValueError(msg % octet_str) - if octet_int > 255: - raise ValueError("Octet %d (> 255) not permitted" % octet_int) - return octet_int - - @classmethod - def _string_from_ip_int(cls, ip_int): - """Turns a 32-bit integer into dotted decimal notation. - - Args: - ip_int: An integer, the IP address. - - Returns: - The IP address as a string in dotted decimal notation. - - """ - return '.'.join(_compat_str(struct.unpack(b'!B', b)[0] - if isinstance(b, bytes) - else b) - for b in _compat_to_bytes(ip_int, 4, 'big')) - - def _is_hostmask(self, ip_str): - """Test if the IP string is a hostmask (rather than a netmask). - - Args: - ip_str: A string, the potential hostmask. - - Returns: - A boolean, True if the IP string is a hostmask. - - """ - bits = ip_str.split('.') - try: - parts = [x for x in map(int, bits) if x in self._valid_mask_octets] - except ValueError: - return False - if len(parts) != len(bits): - return False - if parts[0] < parts[-1]: - return True - return False - - def _reverse_pointer(self): - """Return the reverse DNS pointer name for the IPv4 address. - - This implements the method described in RFC1035 3.5. - - """ - reverse_octets = _compat_str(self).split('.')[::-1] - return '.'.join(reverse_octets) + '.in-addr.arpa' - - @property - def max_prefixlen(self): - return self._max_prefixlen - - @property - def version(self): - return self._version - - -class IPv4Address(_BaseV4, _BaseAddress): - - """Represent and manipulate single IPv4 Addresses.""" - - __slots__ = ('_ip', '__weakref__') - - def __init__(self, address): - - """ - Args: - address: A string or integer representing the IP - - Additionally, an integer can be passed, so - IPv4Address('192.0.2.1') == IPv4Address(3221225985). - or, more generally - IPv4Address(int(IPv4Address('192.0.2.1'))) == - IPv4Address('192.0.2.1') - - Raises: - AddressValueError: If ipaddress isn't a valid IPv4 address. - - """ - # Efficient constructor from integer. - if isinstance(address, _compat_int_types): - self._check_int_address(address) - self._ip = address - return - - # Constructing from a packed address - if isinstance(address, bytes): - self._check_packed_address(address, 4) - bvs = _compat_bytes_to_byte_vals(address) - self._ip = _compat_int_from_byte_vals(bvs, 'big') - return - - # Assume input argument to be string or any object representation - # which converts into a formatted IP string. - addr_str = _compat_str(address) - if '/' in addr_str: - raise AddressValueError("Unexpected '/' in %r" % address) - self._ip = self._ip_int_from_string(addr_str) - - @property - def packed(self): - """The binary representation of this address.""" - return v4_int_to_packed(self._ip) - - @property - def is_reserved(self): - """Test if the address is otherwise IETF reserved. - - Returns: - A boolean, True if the address is within the - reserved IPv4 Network range. - - """ - return self in self._constants._reserved_network - - @property - def is_private(self): - """Test if this address is allocated for private networks. - - Returns: - A boolean, True if the address is reserved per - iana-ipv4-special-registry. - - """ - return any(self in net for net in self._constants._private_networks) - - @property - def is_global(self): - return ( - self not in self._constants._public_network and - not self.is_private) - - @property - def is_multicast(self): - """Test if the address is reserved for multicast use. - - Returns: - A boolean, True if the address is multicast. - See RFC 3171 for details. - - """ - return self in self._constants._multicast_network - - @property - def is_unspecified(self): - """Test if the address is unspecified. - - Returns: - A boolean, True if this is the unspecified address as defined in - RFC 5735 3. - - """ - return self == self._constants._unspecified_address - - @property - def is_loopback(self): - """Test if the address is a loopback address. - - Returns: - A boolean, True if the address is a loopback per RFC 3330. - - """ - return self in self._constants._loopback_network - - @property - def is_link_local(self): - """Test if the address is reserved for link-local. - - Returns: - A boolean, True if the address is link-local per RFC 3927. - - """ - return self in self._constants._linklocal_network - - -class IPv4Interface(IPv4Address): - - def __init__(self, address): - if isinstance(address, (bytes, _compat_int_types)): - IPv4Address.__init__(self, address) - self.network = IPv4Network(self._ip) - self._prefixlen = self._max_prefixlen - return - - if isinstance(address, tuple): - IPv4Address.__init__(self, address[0]) - if len(address) > 1: - self._prefixlen = int(address[1]) - else: - self._prefixlen = self._max_prefixlen - - self.network = IPv4Network(address, strict=False) - self.netmask = self.network.netmask - self.hostmask = self.network.hostmask - return - - addr = _split_optional_netmask(address) - IPv4Address.__init__(self, addr[0]) - - self.network = IPv4Network(address, strict=False) - self._prefixlen = self.network._prefixlen - - self.netmask = self.network.netmask - self.hostmask = self.network.hostmask - - def __str__(self): - return '%s/%d' % (self._string_from_ip_int(self._ip), - self.network.prefixlen) - - def __eq__(self, other): - address_equal = IPv4Address.__eq__(self, other) - if not address_equal or address_equal is NotImplemented: - return address_equal - try: - return self.network == other.network - except AttributeError: - # An interface with an associated network is NOT the - # same as an unassociated address. That's why the hash - # takes the extra info into account. - return False - - def __lt__(self, other): - address_less = IPv4Address.__lt__(self, other) - if address_less is NotImplemented: - return NotImplemented - try: - return (self.network < other.network or - self.network == other.network and address_less) - except AttributeError: - # We *do* allow addresses and interfaces to be sorted. The - # unassociated address is considered less than all interfaces. - return False - - def __hash__(self): - return self._ip ^ self._prefixlen ^ int(self.network.network_address) - - __reduce__ = _IPAddressBase.__reduce__ - - @property - def ip(self): - return IPv4Address(self._ip) - - @property - def with_prefixlen(self): - return '%s/%s' % (self._string_from_ip_int(self._ip), - self._prefixlen) - - @property - def with_netmask(self): - return '%s/%s' % (self._string_from_ip_int(self._ip), - self.netmask) - - @property - def with_hostmask(self): - return '%s/%s' % (self._string_from_ip_int(self._ip), - self.hostmask) - - -class IPv4Network(_BaseV4, _BaseNetwork): - - """This class represents and manipulates 32-bit IPv4 network + addresses.. - - Attributes: [examples for IPv4Network('192.0.2.0/27')] - .network_address: IPv4Address('192.0.2.0') - .hostmask: IPv4Address('0.0.0.31') - .broadcast_address: IPv4Address('192.0.2.32') - .netmask: IPv4Address('255.255.255.224') - .prefixlen: 27 - - """ - # Class to use when creating address objects - _address_class = IPv4Address - - def __init__(self, address, strict=True): - - """Instantiate a new IPv4 network object. - - Args: - address: A string or integer representing the IP [& network]. - '192.0.2.0/24' - '192.0.2.0/255.255.255.0' - '192.0.0.2/0.0.0.255' - are all functionally the same in IPv4. Similarly, - '192.0.2.1' - '192.0.2.1/255.255.255.255' - '192.0.2.1/32' - are also functionally equivalent. That is to say, failing to - provide a subnetmask will create an object with a mask of /32. - - If the mask (portion after the / in the argument) is given in - dotted quad form, it is treated as a netmask if it starts with a - non-zero field (e.g. /255.0.0.0 == /8) and as a hostmask if it - starts with a zero field (e.g. 0.255.255.255 == /8), with the - single exception of an all-zero mask which is treated as a - netmask == /0. If no mask is given, a default of /32 is used. - - Additionally, an integer can be passed, so - IPv4Network('192.0.2.1') == IPv4Network(3221225985) - or, more generally - IPv4Interface(int(IPv4Interface('192.0.2.1'))) == - IPv4Interface('192.0.2.1') - - Raises: - AddressValueError: If ipaddress isn't a valid IPv4 address. - NetmaskValueError: If the netmask isn't valid for - an IPv4 address. - ValueError: If strict is True and a network address is not - supplied. - - """ - _BaseNetwork.__init__(self, address) - - # Constructing from a packed address or integer - if isinstance(address, (_compat_int_types, bytes)): - self.network_address = IPv4Address(address) - self.netmask, self._prefixlen = self._make_netmask( - self._max_prefixlen) - # fixme: address/network test here. - return - - if isinstance(address, tuple): - if len(address) > 1: - arg = address[1] - else: - # We weren't given an address[1] - arg = self._max_prefixlen - self.network_address = IPv4Address(address[0]) - self.netmask, self._prefixlen = self._make_netmask(arg) - packed = int(self.network_address) - if packed & int(self.netmask) != packed: - if strict: - raise ValueError('%s has host bits set' % self) - else: - self.network_address = IPv4Address(packed & - int(self.netmask)) - return - - # Assume input argument to be string or any object representation - # which converts into a formatted IP prefix string. - addr = _split_optional_netmask(address) - self.network_address = IPv4Address(self._ip_int_from_string(addr[0])) - - if len(addr) == 2: - arg = addr[1] - else: - arg = self._max_prefixlen - self.netmask, self._prefixlen = self._make_netmask(arg) - - if strict: - if (IPv4Address(int(self.network_address) & int(self.netmask)) != - self.network_address): - raise ValueError('%s has host bits set' % self) - self.network_address = IPv4Address(int(self.network_address) & - int(self.netmask)) - - if self._prefixlen == (self._max_prefixlen - 1): - self.hosts = self.__iter__ - - @property - def is_global(self): - """Test if this address is allocated for public networks. - - Returns: - A boolean, True if the address is not reserved per - iana-ipv4-special-registry. - - """ - return (not (self.network_address in IPv4Network('100.64.0.0/10') and - self.broadcast_address in IPv4Network('100.64.0.0/10')) and - not self.is_private) - - -class _IPv4Constants(object): - - _linklocal_network = IPv4Network('169.254.0.0/16') - - _loopback_network = IPv4Network('127.0.0.0/8') - - _multicast_network = IPv4Network('224.0.0.0/4') - - _public_network = IPv4Network('100.64.0.0/10') - - _private_networks = [ - IPv4Network('0.0.0.0/8'), - IPv4Network('10.0.0.0/8'), - IPv4Network('127.0.0.0/8'), - IPv4Network('169.254.0.0/16'), - IPv4Network('172.16.0.0/12'), - IPv4Network('192.0.0.0/29'), - IPv4Network('192.0.0.170/31'), - IPv4Network('192.0.2.0/24'), - IPv4Network('192.168.0.0/16'), - IPv4Network('198.18.0.0/15'), - IPv4Network('198.51.100.0/24'), - IPv4Network('203.0.113.0/24'), - IPv4Network('240.0.0.0/4'), - IPv4Network('255.255.255.255/32'), - ] - - _reserved_network = IPv4Network('240.0.0.0/4') - - _unspecified_address = IPv4Address('0.0.0.0') - - -IPv4Address._constants = _IPv4Constants - - -class _BaseV6(object): - - """Base IPv6 object. - - The following methods are used by IPv6 objects in both single IP - addresses and networks. - - """ - - __slots__ = () - _version = 6 - _ALL_ONES = (2 ** IPV6LENGTH) - 1 - _HEXTET_COUNT = 8 - _HEX_DIGITS = frozenset('0123456789ABCDEFabcdef') - _max_prefixlen = IPV6LENGTH - - # There are only a bunch of valid v6 netmasks, so we cache them all - # when constructed (see _make_netmask()). - _netmask_cache = {} - - @classmethod - def _make_netmask(cls, arg): - """Make a (netmask, prefix_len) tuple from the given argument. - - Argument can be: - - an integer (the prefix length) - - a string representing the prefix length (e.g. "24") - - a string representing the prefix netmask (e.g. "255.255.255.0") - """ - if arg not in cls._netmask_cache: - if isinstance(arg, _compat_int_types): - prefixlen = arg - else: - prefixlen = cls._prefix_from_prefix_string(arg) - netmask = IPv6Address(cls._ip_int_from_prefix(prefixlen)) - cls._netmask_cache[arg] = netmask, prefixlen - return cls._netmask_cache[arg] - - @classmethod - def _ip_int_from_string(cls, ip_str): - """Turn an IPv6 ip_str into an integer. - - Args: - ip_str: A string, the IPv6 ip_str. - - Returns: - An int, the IPv6 address - - Raises: - AddressValueError: if ip_str isn't a valid IPv6 Address. - - """ - if not ip_str: - raise AddressValueError('Address cannot be empty') - - parts = ip_str.split(':') - - # An IPv6 address needs at least 2 colons (3 parts). - _min_parts = 3 - if len(parts) < _min_parts: - msg = "At least %d parts expected in %r" % (_min_parts, ip_str) - raise AddressValueError(msg) - - # If the address has an IPv4-style suffix, convert it to hexadecimal. - if '.' in parts[-1]: - try: - ipv4_int = IPv4Address(parts.pop())._ip - except AddressValueError as exc: - raise AddressValueError("%s in %r" % (exc, ip_str)) - parts.append('%x' % ((ipv4_int >> 16) & 0xFFFF)) - parts.append('%x' % (ipv4_int & 0xFFFF)) - - # An IPv6 address can't have more than 8 colons (9 parts). - # The extra colon comes from using the "::" notation for a single - # leading or trailing zero part. - _max_parts = cls._HEXTET_COUNT + 1 - if len(parts) > _max_parts: - msg = "At most %d colons permitted in %r" % ( - _max_parts - 1, ip_str) - raise AddressValueError(msg) - - # Disregarding the endpoints, find '::' with nothing in between. - # This indicates that a run of zeroes has been skipped. - skip_index = None - for i in _compat_range(1, len(parts) - 1): - if not parts[i]: - if skip_index is not None: - # Can't have more than one '::' - msg = "At most one '::' permitted in %r" % ip_str - raise AddressValueError(msg) - skip_index = i - - # parts_hi is the number of parts to copy from above/before the '::' - # parts_lo is the number of parts to copy from below/after the '::' - if skip_index is not None: - # If we found a '::', then check if it also covers the endpoints. - parts_hi = skip_index - parts_lo = len(parts) - skip_index - 1 - if not parts[0]: - parts_hi -= 1 - if parts_hi: - msg = "Leading ':' only permitted as part of '::' in %r" - raise AddressValueError(msg % ip_str) # ^: requires ^:: - if not parts[-1]: - parts_lo -= 1 - if parts_lo: - msg = "Trailing ':' only permitted as part of '::' in %r" - raise AddressValueError(msg % ip_str) # :$ requires ::$ - parts_skipped = cls._HEXTET_COUNT - (parts_hi + parts_lo) - if parts_skipped < 1: - msg = "Expected at most %d other parts with '::' in %r" - raise AddressValueError(msg % (cls._HEXTET_COUNT - 1, ip_str)) - else: - # Otherwise, allocate the entire address to parts_hi. The - # endpoints could still be empty, but _parse_hextet() will check - # for that. - if len(parts) != cls._HEXTET_COUNT: - msg = "Exactly %d parts expected without '::' in %r" - raise AddressValueError(msg % (cls._HEXTET_COUNT, ip_str)) - if not parts[0]: - msg = "Leading ':' only permitted as part of '::' in %r" - raise AddressValueError(msg % ip_str) # ^: requires ^:: - if not parts[-1]: - msg = "Trailing ':' only permitted as part of '::' in %r" - raise AddressValueError(msg % ip_str) # :$ requires ::$ - parts_hi = len(parts) - parts_lo = 0 - parts_skipped = 0 - - try: - # Now, parse the hextets into a 128-bit integer. - ip_int = 0 - for i in range(parts_hi): - ip_int <<= 16 - ip_int |= cls._parse_hextet(parts[i]) - ip_int <<= 16 * parts_skipped - for i in range(-parts_lo, 0): - ip_int <<= 16 - ip_int |= cls._parse_hextet(parts[i]) - return ip_int - except ValueError as exc: - raise AddressValueError("%s in %r" % (exc, ip_str)) - - @classmethod - def _parse_hextet(cls, hextet_str): - """Convert an IPv6 hextet string into an integer. - - Args: - hextet_str: A string, the number to parse. - - Returns: - The hextet as an integer. - - Raises: - ValueError: if the input isn't strictly a hex number from - [0..FFFF]. - - """ - # Whitelist the characters, since int() allows a lot of bizarre stuff. - if not cls._HEX_DIGITS.issuperset(hextet_str): - raise ValueError("Only hex digits permitted in %r" % hextet_str) - # We do the length check second, since the invalid character error - # is likely to be more informative for the user - if len(hextet_str) > 4: - msg = "At most 4 characters permitted in %r" - raise ValueError(msg % hextet_str) - # Length check means we can skip checking the integer value - return int(hextet_str, 16) - - @classmethod - def _compress_hextets(cls, hextets): - """Compresses a list of hextets. - - Compresses a list of strings, replacing the longest continuous - sequence of "0" in the list with "" and adding empty strings at - the beginning or at the end of the string such that subsequently - calling ":".join(hextets) will produce the compressed version of - the IPv6 address. - - Args: - hextets: A list of strings, the hextets to compress. - - Returns: - A list of strings. - - """ - best_doublecolon_start = -1 - best_doublecolon_len = 0 - doublecolon_start = -1 - doublecolon_len = 0 - for index, hextet in enumerate(hextets): - if hextet == '0': - doublecolon_len += 1 - if doublecolon_start == -1: - # Start of a sequence of zeros. - doublecolon_start = index - if doublecolon_len > best_doublecolon_len: - # This is the longest sequence of zeros so far. - best_doublecolon_len = doublecolon_len - best_doublecolon_start = doublecolon_start - else: - doublecolon_len = 0 - doublecolon_start = -1 - - if best_doublecolon_len > 1: - best_doublecolon_end = (best_doublecolon_start + - best_doublecolon_len) - # For zeros at the end of the address. - if best_doublecolon_end == len(hextets): - hextets += [''] - hextets[best_doublecolon_start:best_doublecolon_end] = [''] - # For zeros at the beginning of the address. - if best_doublecolon_start == 0: - hextets = [''] + hextets - - return hextets - - @classmethod - def _string_from_ip_int(cls, ip_int=None): - """Turns a 128-bit integer into hexadecimal notation. - - Args: - ip_int: An integer, the IP address. - - Returns: - A string, the hexadecimal representation of the address. - - Raises: - ValueError: The address is bigger than 128 bits of all ones. - - """ - if ip_int is None: - ip_int = int(cls._ip) - - if ip_int > cls._ALL_ONES: - raise ValueError('IPv6 address is too large') - - hex_str = '%032x' % ip_int - hextets = ['%x' % int(hex_str[x:x + 4], 16) for x in range(0, 32, 4)] - - hextets = cls._compress_hextets(hextets) - return ':'.join(hextets) - - def _explode_shorthand_ip_string(self): - """Expand a shortened IPv6 address. - - Args: - ip_str: A string, the IPv6 address. - - Returns: - A string, the expanded IPv6 address. - - """ - if isinstance(self, IPv6Network): - ip_str = _compat_str(self.network_address) - elif isinstance(self, IPv6Interface): - ip_str = _compat_str(self.ip) - else: - ip_str = _compat_str(self) - - ip_int = self._ip_int_from_string(ip_str) - hex_str = '%032x' % ip_int - parts = [hex_str[x:x + 4] for x in range(0, 32, 4)] - if isinstance(self, (_BaseNetwork, IPv6Interface)): - return '%s/%d' % (':'.join(parts), self._prefixlen) - return ':'.join(parts) - - def _reverse_pointer(self): - """Return the reverse DNS pointer name for the IPv6 address. - - This implements the method described in RFC3596 2.5. - - """ - reverse_chars = self.exploded[::-1].replace(':', '') - return '.'.join(reverse_chars) + '.ip6.arpa' - - @property - def max_prefixlen(self): - return self._max_prefixlen - - @property - def version(self): - return self._version - - -class IPv6Address(_BaseV6, _BaseAddress): - - """Represent and manipulate single IPv6 Addresses.""" - - __slots__ = ('_ip', '__weakref__') - - def __init__(self, address): - """Instantiate a new IPv6 address object. - - Args: - address: A string or integer representing the IP - - Additionally, an integer can be passed, so - IPv6Address('2001:db8::') == - IPv6Address(42540766411282592856903984951653826560) - or, more generally - IPv6Address(int(IPv6Address('2001:db8::'))) == - IPv6Address('2001:db8::') - - Raises: - AddressValueError: If address isn't a valid IPv6 address. - - """ - # Efficient constructor from integer. - if isinstance(address, _compat_int_types): - self._check_int_address(address) - self._ip = address - return - - # Constructing from a packed address - if isinstance(address, bytes): - self._check_packed_address(address, 16) - bvs = _compat_bytes_to_byte_vals(address) - self._ip = _compat_int_from_byte_vals(bvs, 'big') - return - - # Assume input argument to be string or any object representation - # which converts into a formatted IP string. - addr_str = _compat_str(address) - if '/' in addr_str: - raise AddressValueError("Unexpected '/' in %r" % address) - self._ip = self._ip_int_from_string(addr_str) - - @property - def packed(self): - """The binary representation of this address.""" - return v6_int_to_packed(self._ip) - - @property - def is_multicast(self): - """Test if the address is reserved for multicast use. - - Returns: - A boolean, True if the address is a multicast address. - See RFC 2373 2.7 for details. - - """ - return self in self._constants._multicast_network - - @property - def is_reserved(self): - """Test if the address is otherwise IETF reserved. - - Returns: - A boolean, True if the address is within one of the - reserved IPv6 Network ranges. - - """ - return any(self in x for x in self._constants._reserved_networks) - - @property - def is_link_local(self): - """Test if the address is reserved for link-local. - - Returns: - A boolean, True if the address is reserved per RFC 4291. - - """ - return self in self._constants._linklocal_network - - @property - def is_site_local(self): - """Test if the address is reserved for site-local. - - Note that the site-local address space has been deprecated by RFC 3879. - Use is_private to test if this address is in the space of unique local - addresses as defined by RFC 4193. - - Returns: - A boolean, True if the address is reserved per RFC 3513 2.5.6. - - """ - return self in self._constants._sitelocal_network - - @property - def is_private(self): - """Test if this address is allocated for private networks. - - Returns: - A boolean, True if the address is reserved per - iana-ipv6-special-registry. - - """ - return any(self in net for net in self._constants._private_networks) - - @property - def is_global(self): - """Test if this address is allocated for public networks. - - Returns: - A boolean, true if the address is not reserved per - iana-ipv6-special-registry. - - """ - return not self.is_private - - @property - def is_unspecified(self): - """Test if the address is unspecified. - - Returns: - A boolean, True if this is the unspecified address as defined in - RFC 2373 2.5.2. - - """ - return self._ip == 0 - - @property - def is_loopback(self): - """Test if the address is a loopback address. - - Returns: - A boolean, True if the address is a loopback address as defined in - RFC 2373 2.5.3. - - """ - return self._ip == 1 - - @property - def ipv4_mapped(self): - """Return the IPv4 mapped address. - - Returns: - If the IPv6 address is a v4 mapped address, return the - IPv4 mapped address. Return None otherwise. - - """ - if (self._ip >> 32) != 0xFFFF: - return None - return IPv4Address(self._ip & 0xFFFFFFFF) - - @property - def teredo(self): - """Tuple of embedded teredo IPs. - - Returns: - Tuple of the (server, client) IPs or None if the address - doesn't appear to be a teredo address (doesn't start with - 2001::/32) - - """ - if (self._ip >> 96) != 0x20010000: - return None - return (IPv4Address((self._ip >> 64) & 0xFFFFFFFF), - IPv4Address(~self._ip & 0xFFFFFFFF)) - - @property - def sixtofour(self): - """Return the IPv4 6to4 embedded address. - - Returns: - The IPv4 6to4-embedded address if present or None if the - address doesn't appear to contain a 6to4 embedded address. - - """ - if (self._ip >> 112) != 0x2002: - return None - return IPv4Address((self._ip >> 80) & 0xFFFFFFFF) - - -class IPv6Interface(IPv6Address): - - def __init__(self, address): - if isinstance(address, (bytes, _compat_int_types)): - IPv6Address.__init__(self, address) - self.network = IPv6Network(self._ip) - self._prefixlen = self._max_prefixlen - return - if isinstance(address, tuple): - IPv6Address.__init__(self, address[0]) - if len(address) > 1: - self._prefixlen = int(address[1]) - else: - self._prefixlen = self._max_prefixlen - self.network = IPv6Network(address, strict=False) - self.netmask = self.network.netmask - self.hostmask = self.network.hostmask - return - - addr = _split_optional_netmask(address) - IPv6Address.__init__(self, addr[0]) - self.network = IPv6Network(address, strict=False) - self.netmask = self.network.netmask - self._prefixlen = self.network._prefixlen - self.hostmask = self.network.hostmask - - def __str__(self): - return '%s/%d' % (self._string_from_ip_int(self._ip), - self.network.prefixlen) - - def __eq__(self, other): - address_equal = IPv6Address.__eq__(self, other) - if not address_equal or address_equal is NotImplemented: - return address_equal - try: - return self.network == other.network - except AttributeError: - # An interface with an associated network is NOT the - # same as an unassociated address. That's why the hash - # takes the extra info into account. - return False - - def __lt__(self, other): - address_less = IPv6Address.__lt__(self, other) - if address_less is NotImplemented: - return NotImplemented - try: - return (self.network < other.network or - self.network == other.network and address_less) - except AttributeError: - # We *do* allow addresses and interfaces to be sorted. The - # unassociated address is considered less than all interfaces. - return False - - def __hash__(self): - return self._ip ^ self._prefixlen ^ int(self.network.network_address) - - __reduce__ = _IPAddressBase.__reduce__ - - @property - def ip(self): - return IPv6Address(self._ip) - - @property - def with_prefixlen(self): - return '%s/%s' % (self._string_from_ip_int(self._ip), - self._prefixlen) - - @property - def with_netmask(self): - return '%s/%s' % (self._string_from_ip_int(self._ip), - self.netmask) - - @property - def with_hostmask(self): - return '%s/%s' % (self._string_from_ip_int(self._ip), - self.hostmask) - - @property - def is_unspecified(self): - return self._ip == 0 and self.network.is_unspecified - - @property - def is_loopback(self): - return self._ip == 1 and self.network.is_loopback - - -class IPv6Network(_BaseV6, _BaseNetwork): - - """This class represents and manipulates 128-bit IPv6 networks. - - Attributes: [examples for IPv6('2001:db8::1000/124')] - .network_address: IPv6Address('2001:db8::1000') - .hostmask: IPv6Address('::f') - .broadcast_address: IPv6Address('2001:db8::100f') - .netmask: IPv6Address('ffff:ffff:ffff:ffff:ffff:ffff:ffff:fff0') - .prefixlen: 124 - - """ - - # Class to use when creating address objects - _address_class = IPv6Address - - def __init__(self, address, strict=True): - """Instantiate a new IPv6 Network object. - - Args: - address: A string or integer representing the IPv6 network or the - IP and prefix/netmask. - '2001:db8::/128' - '2001:db8:0000:0000:0000:0000:0000:0000/128' - '2001:db8::' - are all functionally the same in IPv6. That is to say, - failing to provide a subnetmask will create an object with - a mask of /128. - - Additionally, an integer can be passed, so - IPv6Network('2001:db8::') == - IPv6Network(42540766411282592856903984951653826560) - or, more generally - IPv6Network(int(IPv6Network('2001:db8::'))) == - IPv6Network('2001:db8::') - - strict: A boolean. If true, ensure that we have been passed - A true network address, eg, 2001:db8::1000/124 and not an - IP address on a network, eg, 2001:db8::1/124. - - Raises: - AddressValueError: If address isn't a valid IPv6 address. - NetmaskValueError: If the netmask isn't valid for - an IPv6 address. - ValueError: If strict was True and a network address was not - supplied. - - """ - _BaseNetwork.__init__(self, address) - - # Efficient constructor from integer or packed address - if isinstance(address, (bytes, _compat_int_types)): - self.network_address = IPv6Address(address) - self.netmask, self._prefixlen = self._make_netmask( - self._max_prefixlen) - return - - if isinstance(address, tuple): - if len(address) > 1: - arg = address[1] - else: - arg = self._max_prefixlen - self.netmask, self._prefixlen = self._make_netmask(arg) - self.network_address = IPv6Address(address[0]) - packed = int(self.network_address) - if packed & int(self.netmask) != packed: - if strict: - raise ValueError('%s has host bits set' % self) - else: - self.network_address = IPv6Address(packed & - int(self.netmask)) - return - - # Assume input argument to be string or any object representation - # which converts into a formatted IP prefix string. - addr = _split_optional_netmask(address) - - self.network_address = IPv6Address(self._ip_int_from_string(addr[0])) - - if len(addr) == 2: - arg = addr[1] - else: - arg = self._max_prefixlen - self.netmask, self._prefixlen = self._make_netmask(arg) - - if strict: - if (IPv6Address(int(self.network_address) & int(self.netmask)) != - self.network_address): - raise ValueError('%s has host bits set' % self) - self.network_address = IPv6Address(int(self.network_address) & - int(self.netmask)) - - if self._prefixlen == (self._max_prefixlen - 1): - self.hosts = self.__iter__ - - def hosts(self): - """Generate Iterator over usable hosts in a network. - - This is like __iter__ except it doesn't return the - Subnet-Router anycast address. - - """ - network = int(self.network_address) - broadcast = int(self.broadcast_address) - for x in _compat_range(network + 1, broadcast + 1): - yield self._address_class(x) - - @property - def is_site_local(self): - """Test if the address is reserved for site-local. - - Note that the site-local address space has been deprecated by RFC 3879. - Use is_private to test if this address is in the space of unique local - addresses as defined by RFC 4193. - - Returns: - A boolean, True if the address is reserved per RFC 3513 2.5.6. - - """ - return (self.network_address.is_site_local and - self.broadcast_address.is_site_local) - - -class _IPv6Constants(object): - - _linklocal_network = IPv6Network('fe80::/10') - - _multicast_network = IPv6Network('ff00::/8') - - _private_networks = [ - IPv6Network('::1/128'), - IPv6Network('::/128'), - IPv6Network('::ffff:0:0/96'), - IPv6Network('100::/64'), - IPv6Network('2001::/23'), - IPv6Network('2001:2::/48'), - IPv6Network('2001:db8::/32'), - IPv6Network('2001:10::/28'), - IPv6Network('fc00::/7'), - IPv6Network('fe80::/10'), - ] - - _reserved_networks = [ - IPv6Network('::/8'), IPv6Network('100::/8'), - IPv6Network('200::/7'), IPv6Network('400::/6'), - IPv6Network('800::/5'), IPv6Network('1000::/4'), - IPv6Network('4000::/3'), IPv6Network('6000::/3'), - IPv6Network('8000::/3'), IPv6Network('A000::/3'), - IPv6Network('C000::/3'), IPv6Network('E000::/4'), - IPv6Network('F000::/5'), IPv6Network('F800::/6'), - IPv6Network('FE00::/9'), - ] - - _sitelocal_network = IPv6Network('fec0::/10') - - -IPv6Address._constants = _IPv6Constants diff --git a/src/pip/_vendor/ipaddress.pyi b/src/pip/_vendor/ipaddress.pyi deleted file mode 100644 index eef994d9457..00000000000 --- a/src/pip/_vendor/ipaddress.pyi +++ /dev/null @@ -1 +0,0 @@ -from ipaddress import * \ No newline at end of file diff --git a/src/pip/_vendor/urllib3/packages/ssl_match_hostname/_implementation.py b/src/pip/_vendor/urllib3/packages/ssl_match_hostname/_implementation.py index 5831c2e01d6..689208d3c63 100644 --- a/src/pip/_vendor/urllib3/packages/ssl_match_hostname/_implementation.py +++ b/src/pip/_vendor/urllib3/packages/ssl_match_hostname/_implementation.py @@ -11,7 +11,7 @@ # python-3.5) otherwise only do DNS matching. This allows # backports.ssl_match_hostname to continue to be used in Python 2.7. try: - from pip._vendor import ipaddress + import ipaddress except ImportError: ipaddress = None diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 15c000339ae..4f7042cc127 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -5,7 +5,6 @@ contextlib2==0.6.0.post1 distlib==0.3.1 distro==1.5.0 html5lib==1.1 -ipaddress==1.0.23 # Only needed on 2.6 and 2.7 msgpack==1.0.0 packaging==20.8 pep517==0.9.1 From 17d72b748f0a4397a125b2d73130152ba79c1311 Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Fri, 25 Dec 2020 08:16:15 -0800 Subject: [PATCH 2822/3170] Replace utils.compat.lru_cache with stdlib functools.lru_cache The stdlib version has been available since Python 3.2. --- ...e8-ebb1-4eaf-aee0-e5582a8c5d58.trivial.rst | 0 src/pip/_internal/index/collector.py | 3 +-- src/pip/_internal/index/package_finder.py | 6 ++--- .../resolution/resolvelib/found_candidates.py | 4 ++-- src/pip/_internal/utils/compat.py | 24 +------------------ 5 files changed, 7 insertions(+), 30 deletions(-) create mode 100644 news/275aa0e8-ebb1-4eaf-aee0-e5582a8c5d58.trivial.rst diff --git a/news/275aa0e8-ebb1-4eaf-aee0-e5582a8c5d58.trivial.rst b/news/275aa0e8-ebb1-4eaf-aee0-e5582a8c5d58.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/index/collector.py b/src/pip/_internal/index/collector.py index b852645fd86..ad931c06fe7 100644 --- a/src/pip/_internal/index/collector.py +++ b/src/pip/_internal/index/collector.py @@ -21,7 +21,6 @@ from pip._internal.models.link import Link from pip._internal.models.search_scope import SearchScope from pip._internal.network.utils import raise_for_status -from pip._internal.utils.compat import lru_cache from pip._internal.utils.filetypes import is_archive_file from pip._internal.utils.misc import pairwise, redact_auth_from_url from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -311,7 +310,7 @@ def with_cached_html_pages( `page` has `page.cache_link_parsing == False`. """ - @lru_cache(maxsize=None) + @functools.lru_cache(maxsize=None) def wrapper(cacheable_page): # type: (CacheablePageContent) -> List[Link] return list(fn(cacheable_page.page)) diff --git a/src/pip/_internal/index/package_finder.py b/src/pip/_internal/index/package_finder.py index 860eb1255ce..745958b2e5e 100644 --- a/src/pip/_internal/index/package_finder.py +++ b/src/pip/_internal/index/package_finder.py @@ -3,6 +3,7 @@ # The following comment should be removed at some point in the future. # mypy: strict-optional=False +import functools import logging import re @@ -23,7 +24,6 @@ from pip._internal.models.selection_prefs import SelectionPreferences from pip._internal.models.target_python import TargetPython from pip._internal.models.wheel import Wheel -from pip._internal.utils.compat import lru_cache from pip._internal.utils.filetypes import WHEEL_EXTENSION from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import build_netloc @@ -796,7 +796,7 @@ def process_project_url(self, project_url, link_evaluator): return package_links - @lru_cache(maxsize=None) + @functools.lru_cache(maxsize=None) def find_all_candidates(self, project_name): # type: (str) -> List[InstallationCandidate] """Find all available InstallationCandidate for project_name @@ -859,7 +859,7 @@ def make_candidate_evaluator( hashes=hashes, ) - @lru_cache(maxsize=None) + @functools.lru_cache(maxsize=None) def find_best_candidate( self, project_name, # type: str diff --git a/src/pip/_internal/resolution/resolvelib/found_candidates.py b/src/pip/_internal/resolution/resolvelib/found_candidates.py index a669e893670..439259818fa 100644 --- a/src/pip/_internal/resolution/resolvelib/found_candidates.py +++ b/src/pip/_internal/resolution/resolvelib/found_candidates.py @@ -1,9 +1,9 @@ +import functools import itertools import operator from pip._vendor.six.moves import collections_abc # type: ignore -from pip._internal.utils.compat import lru_cache from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: @@ -88,7 +88,7 @@ def __len__(self): # performance reasons). raise NotImplementedError("don't do this") - @lru_cache(maxsize=1) + @functools.lru_cache(maxsize=1) def __bool__(self): # type: () -> bool if self._prefers_installed and self._installed: diff --git a/src/pip/_internal/utils/compat.py b/src/pip/_internal/utils/compat.py index cc8ecd0f682..6eeb712ad67 100644 --- a/src/pip/_internal/utils/compat.py +++ b/src/pip/_internal/utils/compat.py @@ -5,7 +5,6 @@ # mypy: disallow-untyped-defs=False import codecs -import functools import locale import logging import os @@ -14,15 +13,7 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Callable, Optional, Protocol, TypeVar, Union - - # Used in the @lru_cache polyfill. - F = TypeVar('F') - - class LruCache(Protocol): - def __call__(self, maxsize=None): - # type: (Optional[int]) -> Callable[[F], F] - raise NotImplementedError + from typing import Optional, Union try: import ipaddress @@ -185,16 +176,3 @@ def expanduser(path): # windows detection, covers cpython and ironpython WINDOWS = (sys.platform.startswith("win") or (sys.platform == 'cli' and os.name == 'nt')) - - -# Fallback to noop_lru_cache in Python 2 -# TODO: this can be removed when python 2 support is dropped! -def noop_lru_cache(maxsize=None): - # type: (Optional[int]) -> Callable[[F], F] - def _wrapper(f): - # type: (F) -> F - return f - return _wrapper - - -lru_cache = getattr(functools, "lru_cache", noop_lru_cache) # type: LruCache From e28badf979be78ad601cf1d388d1e78f448a3547 Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Fri, 25 Dec 2020 08:54:58 -0800 Subject: [PATCH 2823/3170] Remove unnecessary type annotation in BlueEmojiBar --- src/pip/_internal/cli/progress_bars.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/cli/progress_bars.py b/src/pip/_internal/cli/progress_bars.py index c8d6567d34b..4b1bb65a465 100644 --- a/src/pip/_internal/cli/progress_bars.py +++ b/src/pip/_internal/cli/progress_bars.py @@ -125,7 +125,7 @@ class BlueEmojiBar(IncrementalBar): suffix = "%(percent)d%%" bar_prefix = " " bar_suffix = " " - phases = ("\U0001F539", "\U0001F537", "\U0001F535") # type: Any + phases = ("\U0001F539", "\U0001F537", "\U0001F535") class DownloadProgressMixin(object): From 7e3fe0c84e7a4203a74df8ae60d45ea2f1291f33 Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Fri, 25 Dec 2020 10:22:20 -0800 Subject: [PATCH 2824/3170] Remove Python 2 compatibility shim for urlparse.uses_fragment This attribute (now urllib.parse.uses_fragment) is unused by the stdlib and remains only for backwards compatibility. See the comment: https://github.com/python/cpython/blob/v3.6.0/Lib/urllib/parse.py#L55-L63 --- news/b034ad46-e6b0-48b1-8b26-1145d611d082.trivial.rst | 0 src/pip/_internal/vcs/bazaar.py | 8 -------- src/pip/_internal/vcs/versioncontrol.py | 1 - 3 files changed, 9 deletions(-) create mode 100644 news/b034ad46-e6b0-48b1-8b26-1145d611d082.trivial.rst diff --git a/news/b034ad46-e6b0-48b1-8b26-1145d611d082.trivial.rst b/news/b034ad46-e6b0-48b1-8b26-1145d611d082.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/vcs/bazaar.py b/src/pip/_internal/vcs/bazaar.py index 22969c726de..0ead8870402 100644 --- a/src/pip/_internal/vcs/bazaar.py +++ b/src/pip/_internal/vcs/bazaar.py @@ -3,7 +3,6 @@ import logging import os -from urllib import parse as urllib_parse from pip._internal.utils.misc import display_path, rmtree from pip._internal.utils.subprocess import make_command @@ -30,13 +29,6 @@ class Bazaar(VersionControl): 'bzr+lp', ) - def __init__(self, *args, **kwargs): - super().__init__(*args, **kwargs) - # 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']) - @staticmethod def get_base_rev_args(rev): return ['-r', rev] diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index 35caf19157a..3ef17172aba 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -286,7 +286,6 @@ def __init__(self): # Register more schemes with urlparse for various version control # systems urllib_parse.uses_netloc.extend(self.schemes) - urllib_parse.uses_fragment.extend(self.schemes) super().__init__() def __iter__(self): From 0b761a164c6429db4a6b0e0e173c3f7dba2546a0 Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Fri, 25 Dec 2020 11:24:38 -0800 Subject: [PATCH 2825/3170] Harmonize type signature of VersionControl.get_remote_url() subclasses In the base class, the signature is defined as: type: (str) -> str Further, the docstring says: Raises RemoteNotFoundError if the repository does not have a remote url configured. However, some subclasses were returning None instead of raising RemoteNotFoundError. This violated the type signature and forced calling code to handle multiple error paradigms. Now, all subclasses implement the base's signature. This allowed simplifying some call sites as they can assume None will not be returned. This mismatch was noticed while trying to remove "mypy: disallow-untyped-defs=False" comments. --- ...71b0-98f9-4e1f-a541-af95fb990af9.trivial.rst | 0 src/pip/_internal/operations/freeze.py | 3 +-- src/pip/_internal/vcs/bazaar.py | 5 +++-- src/pip/_internal/vcs/git.py | 1 + src/pip/_internal/vcs/mercurial.py | 1 + src/pip/_internal/vcs/subversion.py | 5 +++-- src/pip/_internal/vcs/versioncontrol.py | 4 +--- tests/functional/test_vcs_bazaar.py | 13 +++++++++++++ tests/functional/test_vcs_subversion.py | 17 +++++++++++++++++ 9 files changed, 40 insertions(+), 9 deletions(-) create mode 100644 news/738a71b0-98f9-4e1f-a541-af95fb990af9.trivial.rst create mode 100644 tests/functional/test_vcs_subversion.py diff --git a/news/738a71b0-98f9-4e1f-a541-af95fb990af9.trivial.rst b/news/738a71b0-98f9-4e1f-a541-af95fb990af9.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/operations/freeze.py b/src/pip/_internal/operations/freeze.py index 3529c55edc2..0c26f9c7e9d 100644 --- a/src/pip/_internal/operations/freeze.py +++ b/src/pip/_internal/operations/freeze.py @@ -225,8 +225,7 @@ def get_requirement_info(dist): "falling back to uneditable format", exc ) else: - if req is not None: - return (req, True, []) + return (req, True, []) logger.warning( 'Could not determine repository location of %s', location diff --git a/src/pip/_internal/vcs/bazaar.py b/src/pip/_internal/vcs/bazaar.py index 22969c726de..d55399b7a16 100644 --- a/src/pip/_internal/vcs/bazaar.py +++ b/src/pip/_internal/vcs/bazaar.py @@ -9,7 +9,7 @@ from pip._internal.utils.subprocess import make_command from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.urls import path_to_url -from pip._internal.vcs.versioncontrol import VersionControl, vcs +from pip._internal.vcs.versioncontrol import RemoteNotFoundError, VersionControl, vcs if MYPY_CHECK_RUNNING: from typing import Optional, Tuple @@ -89,6 +89,7 @@ def get_url_rev_and_auth(cls, url): @classmethod def get_remote_url(cls, location): + # type: (str) -> str urls = cls.run_command(['info'], cwd=location) for line in urls.splitlines(): line = line.strip() @@ -99,7 +100,7 @@ def get_remote_url(cls, location): if cls._is_local_repository(repo): return path_to_url(repo) return repo - return None + raise RemoteNotFoundError @classmethod def get_revision(cls, location): diff --git a/src/pip/_internal/vcs/git.py b/src/pip/_internal/vcs/git.py index 46f15fc8b41..688f132a49d 100644 --- a/src/pip/_internal/vcs/git.py +++ b/src/pip/_internal/vcs/git.py @@ -303,6 +303,7 @@ def update(self, dest, url, rev_options): @classmethod def get_remote_url(cls, location): + # type: (str) -> str """ Return URL of the first remote encountered. diff --git a/src/pip/_internal/vcs/mercurial.py b/src/pip/_internal/vcs/mercurial.py index 1c84266742f..e7988d1ac2d 100644 --- a/src/pip/_internal/vcs/mercurial.py +++ b/src/pip/_internal/vcs/mercurial.py @@ -87,6 +87,7 @@ def update(self, dest, url, rev_options): @classmethod def get_remote_url(cls, location): + # type: (str) -> str url = cls.run_command( ['showconfig', 'paths.default'], cwd=location).strip() diff --git a/src/pip/_internal/vcs/subversion.py b/src/pip/_internal/vcs/subversion.py index 3bb7ea0f85e..85ce2aa9169 100644 --- a/src/pip/_internal/vcs/subversion.py +++ b/src/pip/_internal/vcs/subversion.py @@ -14,7 +14,7 @@ ) from pip._internal.utils.subprocess import make_command from pip._internal.utils.typing import MYPY_CHECK_RUNNING -from pip._internal.vcs.versioncontrol import VersionControl, vcs +from pip._internal.vcs.versioncontrol import RemoteNotFoundError, VersionControl, vcs _svn_xml_url_re = re.compile('url="([^"]+)"') _svn_rev_re = re.compile(r'committed-rev="(\d+)"') @@ -110,6 +110,7 @@ def make_rev_args(username, password): @classmethod def get_remote_url(cls, location): + # type: (str) -> str # In cases where the source is in a subdirectory, not alongside # setup.py we have to look up in the location until we find a real # setup.py @@ -125,7 +126,7 @@ def get_remote_url(cls, location): "parent directories)", orig_location, ) - return None + raise RemoteNotFoundError return cls._get_svn_url_rev(location)[0] diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index 35caf19157a..b78cbabf3fa 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -410,7 +410,7 @@ def get_requirement_revision(cls, repo_dir): @classmethod def get_src_requirement(cls, repo_dir, project_name): - # type: (str, str) -> Optional[str] + # type: (str, str) -> str """ Return the requirement string to use to redownload the files currently at the given repository directory. @@ -423,8 +423,6 @@ def get_src_requirement(cls, repo_dir, project_name): {repository_url}@{revision}#egg={project_name} """ repo_url = cls.get_remote_url(repo_dir) - if repo_url is None: - return None if cls.should_add_vcs_url_prefix(repo_url): repo_url = '{}+{}'.format(cls.name, repo_url) diff --git a/tests/functional/test_vcs_bazaar.py b/tests/functional/test_vcs_bazaar.py index d928da8b364..ad24d73d5ba 100644 --- a/tests/functional/test_vcs_bazaar.py +++ b/tests/functional/test_vcs_bazaar.py @@ -8,6 +8,7 @@ from pip._internal.utils.misc import hide_url from pip._internal.vcs.bazaar import Bazaar +from pip._internal.vcs.versioncontrol import RemoteNotFoundError from tests.lib import ( _test_path_to_file_url, _vcs_add, @@ -65,3 +66,15 @@ def test_export_rev(script, tmpdir): with open(export_dir / 'test_file', 'r') as f: assert f.read() == 'something initial' + + +@need_bzr +def test_get_remote_url__no_remote(script, tmpdir): + repo_dir = tmpdir / 'temp-repo' + repo_dir.mkdir() + repo_dir = str(repo_dir) + + script.run('bzr', 'init', repo_dir) + + with pytest.raises(RemoteNotFoundError): + Bazaar().get_remote_url(repo_dir) diff --git a/tests/functional/test_vcs_subversion.py b/tests/functional/test_vcs_subversion.py new file mode 100644 index 00000000000..c71c793f895 --- /dev/null +++ b/tests/functional/test_vcs_subversion.py @@ -0,0 +1,17 @@ +import pytest + +from pip._internal.vcs.subversion import Subversion +from pip._internal.vcs.versioncontrol import RemoteNotFoundError +from tests.lib import _create_svn_repo, need_svn + + +@need_svn +def test_get_remote_url__no_remote(script, tmpdir): + repo_dir = tmpdir / 'temp-repo' + repo_dir.mkdir() + repo_dir = str(repo_dir) + + _create_svn_repo(script, repo_dir) + + with pytest.raises(RemoteNotFoundError): + Subversion().get_remote_url(repo_dir) From 09b3d3a50be67d118229e39be86f849c1e59bdd9 Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Thu, 24 Dec 2020 13:23:07 -0800 Subject: [PATCH 2826/3170] Remove object from class definitions Unnecessary since dropping Python 2 support. In Python 3, all classes are new style classes. --- ...170af15-1373-4226-a1ec-efe54b7ad480.trivial.rst | 0 src/pip/_internal/build_env.py | 2 +- src/pip/_internal/cache.py | 4 ++-- src/pip/_internal/cli/command_context.py | 2 +- src/pip/_internal/cli/progress_bars.py | 6 +++--- src/pip/_internal/cli/spinners.py | 4 ++-- src/pip/_internal/configuration.py | 2 +- src/pip/_internal/distributions/base.py | 2 +- src/pip/_internal/index/collector.py | 8 ++++---- src/pip/_internal/index/package_finder.py | 10 +++++----- src/pip/_internal/models/direct_url.py | 8 ++++---- src/pip/_internal/models/format_control.py | 2 +- src/pip/_internal/models/index.py | 2 +- src/pip/_internal/models/scheme.py | 2 +- src/pip/_internal/models/search_scope.py | 2 +- src/pip/_internal/models/selection_prefs.py | 2 +- src/pip/_internal/models/target_python.py | 2 +- src/pip/_internal/models/wheel.py | 2 +- src/pip/_internal/network/download.py | 4 ++-- src/pip/_internal/network/lazy_wheel.py | 2 +- src/pip/_internal/operations/freeze.py | 2 +- src/pip/_internal/operations/install/wheel.py | 4 ++-- src/pip/_internal/operations/prepare.py | 4 ++-- src/pip/_internal/req/__init__.py | 2 +- src/pip/_internal/req/constructors.py | 2 +- src/pip/_internal/req/req_file.py | 6 +++--- src/pip/_internal/req/req_install.py | 2 +- src/pip/_internal/req/req_set.py | 2 +- src/pip/_internal/req/req_tracker.py | 2 +- src/pip/_internal/req/req_uninstall.py | 6 +++--- src/pip/_internal/resolution/base.py | 2 +- src/pip/_internal/resolution/resolvelib/base.py | 6 +++--- src/pip/_internal/resolution/resolvelib/factory.py | 2 +- src/pip/_internal/self_outdated_check.py | 2 +- src/pip/_internal/utils/hashes.py | 2 +- src/pip/_internal/utils/misc.py | 4 ++-- src/pip/_internal/utils/models.py | 2 +- src/pip/_internal/utils/pkg_resources.py | 2 +- src/pip/_internal/utils/temp_dir.py | 6 +++--- src/pip/_internal/vcs/versioncontrol.py | 6 +++--- tests/conftest.py | 6 +++--- tests/functional/test_download.py | 2 +- tests/functional/test_install_reqs.py | 2 +- tests/functional/test_install_upgrade.py | 2 +- tests/functional/test_new_resolver.py | 2 +- tests/lib/__init__.py | 4 ++-- tests/lib/configuration_helpers.py | 2 +- tests/lib/options_helpers.py | 2 +- tests/lib/requests_mocks.py | 8 ++++---- tests/lib/venv.py | 2 +- tests/lib/wheel.py | 2 +- tests/unit/test_base_command.py | 4 ++-- tests/unit/test_check.py | 2 +- tests/unit/test_collector.py | 2 +- tests/unit/test_finder.py | 2 +- tests/unit/test_logging.py | 4 ++-- tests/unit/test_models.py | 4 ++-- tests/unit/test_models_wheel.py | 2 +- tests/unit/test_network_auth.py | 8 ++++---- tests/unit/test_network_session.py | 2 +- tests/unit/test_operations_prepare.py | 2 +- tests/unit/test_options.py | 4 ++-- tests/unit/test_req.py | 4 ++-- tests/unit/test_req_file.py | 14 +++++++------- tests/unit/test_req_install.py | 4 ++-- tests/unit/test_req_uninstall.py | 6 +++--- tests/unit/test_resolution_legacy_resolver.py | 4 ++-- tests/unit/test_self_check_outdated.py | 6 +++--- tests/unit/test_utils.py | 12 ++++++------ tests/unit/test_utils_compatibility_tags.py | 6 +++--- tests/unit/test_utils_subprocess.py | 2 +- tests/unit/test_utils_unpacking.py | 2 +- tests/unit/test_wheel.py | 8 ++++---- 73 files changed, 136 insertions(+), 136 deletions(-) create mode 100644 news/1170af15-1373-4226-a1ec-efe54b7ad480.trivial.rst diff --git a/news/1170af15-1373-4226-a1ec-efe54b7ad480.trivial.rst b/news/1170af15-1373-4226-a1ec-efe54b7ad480.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py index a08e63cd051..a587d9f7c8f 100644 --- a/src/pip/_internal/build_env.py +++ b/src/pip/_internal/build_env.py @@ -46,7 +46,7 @@ def __init__(self, path): self.lib_dirs = [purelib, platlib] -class BuildEnvironment(object): +class BuildEnvironment: """Creates and manages an isolated environment to install build deps """ diff --git a/src/pip/_internal/cache.py b/src/pip/_internal/cache.py index b02c82a3c30..8724d909352 100644 --- a/src/pip/_internal/cache.py +++ b/src/pip/_internal/cache.py @@ -33,7 +33,7 @@ def _hash_dict(d): return hashlib.sha224(s.encode("ascii")).hexdigest() -class Cache(object): +class Cache: """An abstract class - provides cache directories for data from links @@ -263,7 +263,7 @@ def __init__(self, format_control): super().__init__(self._temp_dir.path, format_control) -class CacheEntry(object): +class CacheEntry: def __init__( self, link, # type: Link diff --git a/src/pip/_internal/cli/command_context.py b/src/pip/_internal/cli/command_context.py index 7ee2d24e324..ade14f2f677 100644 --- a/src/pip/_internal/cli/command_context.py +++ b/src/pip/_internal/cli/command_context.py @@ -10,7 +10,7 @@ _T = TypeVar('_T', covariant=True) -class CommandContextMixIn(object): +class CommandContextMixIn: def __init__(self): # type: () -> None super().__init__() diff --git a/src/pip/_internal/cli/progress_bars.py b/src/pip/_internal/cli/progress_bars.py index e248a1a5fa6..2c856a51fab 100644 --- a/src/pip/_internal/cli/progress_bars.py +++ b/src/pip/_internal/cli/progress_bars.py @@ -52,7 +52,7 @@ def _select_progress_class(preferred, fallback): _BaseBar = _select_progress_class(IncrementalBar, Bar) # type: Any -class InterruptibleMixin(object): +class InterruptibleMixin: """ Helper to ensure that self.finish() gets called on keyboard interrupt. @@ -125,7 +125,7 @@ class BlueEmojiBar(IncrementalBar): phases = ("\U0001F539", "\U0001F537", "\U0001F535") -class DownloadProgressMixin(object): +class DownloadProgressMixin: def __init__(self, *args, **kwargs): # type: (List[Any], Dict[Any, Any]) -> None @@ -164,7 +164,7 @@ def iter(self, it): # type: ignore self.finish() -class WindowsMixin(object): +class WindowsMixin: def __init__(self, *args, **kwargs): # type: (List[Any], Dict[Any, Any]) -> None diff --git a/src/pip/_internal/cli/spinners.py b/src/pip/_internal/cli/spinners.py index 171d3a02d2f..05ec2dcc765 100644 --- a/src/pip/_internal/cli/spinners.py +++ b/src/pip/_internal/cli/spinners.py @@ -16,7 +16,7 @@ logger = logging.getLogger(__name__) -class SpinnerInterface(object): +class SpinnerInterface: def spin(self): # type: () -> None raise NotImplementedError() @@ -109,7 +109,7 @@ def finish(self, final_status): self._finished = True -class RateLimiter(object): +class RateLimiter: def __init__(self, min_update_interval_seconds): # type: (float) -> None self._min_update_interval_seconds = min_update_interval_seconds diff --git a/src/pip/_internal/configuration.py b/src/pip/_internal/configuration.py index a55882b4634..5ca07c8ee88 100644 --- a/src/pip/_internal/configuration.py +++ b/src/pip/_internal/configuration.py @@ -94,7 +94,7 @@ def get_configuration_files(): } -class Configuration(object): +class Configuration: """Handles management of configuration. Provides an interface to accessing and managing configuration files. diff --git a/src/pip/_internal/distributions/base.py b/src/pip/_internal/distributions/base.py index 6c68a86a27b..37db810b351 100644 --- a/src/pip/_internal/distributions/base.py +++ b/src/pip/_internal/distributions/base.py @@ -11,7 +11,7 @@ from pip._internal.req import InstallRequirement -class AbstractDistribution(object, metaclass=abc.ABCMeta): +class AbstractDistribution(metaclass=abc.ABCMeta): """A base class for handling installable artifacts. The requirements for anything installable are as follows: diff --git a/src/pip/_internal/index/collector.py b/src/pip/_internal/index/collector.py index 2715fcf92a0..cadde36e5c3 100644 --- a/src/pip/_internal/index/collector.py +++ b/src/pip/_internal/index/collector.py @@ -285,7 +285,7 @@ def _create_link_from_element( return link -class CacheablePageContent(object): +class CacheablePageContent: def __init__(self, page): # type: (HTMLPage) -> None assert page.cache_link_parsing @@ -351,7 +351,7 @@ def parse_links(page): yield link -class HTMLPage(object): +class HTMLPage: """Represents one page, along with its URL""" def __init__( @@ -525,7 +525,7 @@ def sort_path(path): return files, urls -class CollectedLinks(object): +class CollectedLinks: """ Encapsulates the return value of a call to LinkCollector.collect_links(). @@ -560,7 +560,7 @@ def __init__( self.project_urls = project_urls -class LinkCollector(object): +class LinkCollector: """ Responsible for collecting Link objects from all configured locations, diff --git a/src/pip/_internal/index/package_finder.py b/src/pip/_internal/index/package_finder.py index cd768704819..abfe1afde61 100644 --- a/src/pip/_internal/index/package_finder.py +++ b/src/pip/_internal/index/package_finder.py @@ -98,7 +98,7 @@ def _check_link_requires_python( return True -class LinkEvaluator(object): +class LinkEvaluator: """ Responsible for evaluating links for a particular project. @@ -311,7 +311,7 @@ def filter_unallowed_hashes( return filtered -class CandidatePreferences(object): +class CandidatePreferences: """ Encapsulates some of the preferences for filtering and sorting @@ -331,7 +331,7 @@ def __init__( self.prefer_binary = prefer_binary -class BestCandidateResult(object): +class BestCandidateResult: """A collection of candidates, returned by `PackageFinder.find_best_candidate`. This class is only intended to be instantiated by CandidateEvaluator's @@ -376,7 +376,7 @@ def iter_applicable(self): return iter(self._applicable_candidates) -class CandidateEvaluator(object): +class CandidateEvaluator: """ Responsible for filtering and sorting candidates for installation based @@ -572,7 +572,7 @@ def compute_best_candidate( ) -class PackageFinder(object): +class PackageFinder: """This finds packages. This is meant to match easy_install's technique for looking for diff --git a/src/pip/_internal/models/direct_url.py b/src/pip/_internal/models/direct_url.py index 8f544caf603..1d97b38eaa6 100644 --- a/src/pip/_internal/models/direct_url.py +++ b/src/pip/_internal/models/direct_url.py @@ -71,7 +71,7 @@ def _filter_none(**kwargs): return {k: v for k, v in kwargs.items() if v is not None} -class VcsInfo(object): +class VcsInfo: name = "vcs_info" def __init__( @@ -112,7 +112,7 @@ def _to_dict(self): ) -class ArchiveInfo(object): +class ArchiveInfo: name = "archive_info" def __init__( @@ -133,7 +133,7 @@ def _to_dict(self): return _filter_none(hash=self.hash) -class DirInfo(object): +class DirInfo: name = "dir_info" def __init__( @@ -160,7 +160,7 @@ def _to_dict(self): InfoType = Union[ArchiveInfo, DirInfo, VcsInfo] -class DirectUrl(object): +class DirectUrl: def __init__( self, diff --git a/src/pip/_internal/models/format_control.py b/src/pip/_internal/models/format_control.py index adcf61e2854..fc2747c950f 100644 --- a/src/pip/_internal/models/format_control.py +++ b/src/pip/_internal/models/format_control.py @@ -7,7 +7,7 @@ from typing import FrozenSet, Optional, Set -class FormatControl(object): +class FormatControl: """Helper for managing formats from which a package can be installed. """ diff --git a/src/pip/_internal/models/index.py b/src/pip/_internal/models/index.py index 7f3285692be..ec328190a2d 100644 --- a/src/pip/_internal/models/index.py +++ b/src/pip/_internal/models/index.py @@ -1,7 +1,7 @@ from urllib import parse as urllib_parse -class PackageIndex(object): +class PackageIndex: """Represents a Package Index and provides easier access to endpoints """ diff --git a/src/pip/_internal/models/scheme.py b/src/pip/_internal/models/scheme.py index 5040551eb0e..697cd19b478 100644 --- a/src/pip/_internal/models/scheme.py +++ b/src/pip/_internal/models/scheme.py @@ -9,7 +9,7 @@ SCHEME_KEYS = ['platlib', 'purelib', 'headers', 'scripts', 'data'] -class Scheme(object): +class Scheme: """A Scheme holds paths which are used as the base directories for artifacts associated with a Python package. """ diff --git a/src/pip/_internal/models/search_scope.py b/src/pip/_internal/models/search_scope.py index ab6f9148693..abfb8bed412 100644 --- a/src/pip/_internal/models/search_scope.py +++ b/src/pip/_internal/models/search_scope.py @@ -18,7 +18,7 @@ logger = logging.getLogger(__name__) -class SearchScope(object): +class SearchScope: """ Encapsulates the locations that pip is configured to search. diff --git a/src/pip/_internal/models/selection_prefs.py b/src/pip/_internal/models/selection_prefs.py index 83110dd8f90..4d5822268b7 100644 --- a/src/pip/_internal/models/selection_prefs.py +++ b/src/pip/_internal/models/selection_prefs.py @@ -6,7 +6,7 @@ from pip._internal.models.format_control import FormatControl -class SelectionPreferences(object): +class SelectionPreferences: """ Encapsulates the candidate selection preferences for downloading and installing files. diff --git a/src/pip/_internal/models/target_python.py b/src/pip/_internal/models/target_python.py index 4593dc854f8..2f2a74242de 100644 --- a/src/pip/_internal/models/target_python.py +++ b/src/pip/_internal/models/target_python.py @@ -10,7 +10,7 @@ from pip._vendor.packaging.tags import Tag -class TargetPython(object): +class TargetPython: """ Encapsulates the properties of a Python interpreter one is targeting diff --git a/src/pip/_internal/models/wheel.py b/src/pip/_internal/models/wheel.py index 4d4068f3b73..49aae147033 100644 --- a/src/pip/_internal/models/wheel.py +++ b/src/pip/_internal/models/wheel.py @@ -12,7 +12,7 @@ from typing import List -class Wheel(object): +class Wheel: """A wheel file""" wheel_file_re = re.compile( diff --git a/src/pip/_internal/network/download.py b/src/pip/_internal/network/download.py index 76896e89970..32396573cae 100644 --- a/src/pip/_internal/network/download.py +++ b/src/pip/_internal/network/download.py @@ -133,7 +133,7 @@ def _http_get_download(session, link): return resp -class Downloader(object): +class Downloader: def __init__( self, session, # type: PipSession @@ -166,7 +166,7 @@ def __call__(self, link, location): return filepath, content_type -class BatchDownloader(object): +class BatchDownloader: def __init__( self, diff --git a/src/pip/_internal/network/lazy_wheel.py b/src/pip/_internal/network/lazy_wheel.py index 83704f6f190..c68a35b9e1b 100644 --- a/src/pip/_internal/network/lazy_wheel.py +++ b/src/pip/_internal/network/lazy_wheel.py @@ -44,7 +44,7 @@ def dist_from_wheel_url(name, url, session): return pkg_resources_distribution_for_wheel(zip_file, name, wheel.name) -class LazyZipOverHTTP(object): +class LazyZipOverHTTP: """File-like object mapped to a ZIP file over HTTP. This uses HTTP range requests to lazily fetch the file's content, diff --git a/src/pip/_internal/operations/freeze.py b/src/pip/_internal/operations/freeze.py index 3529c55edc2..74a6cd3d9c6 100644 --- a/src/pip/_internal/operations/freeze.py +++ b/src/pip/_internal/operations/freeze.py @@ -236,7 +236,7 @@ def get_requirement_info(dist): return (None, False, comments) -class FrozenRequirement(object): +class FrozenRequirement: def __init__(self, name, req, editable, comments=()): # type: (str, Union[str, Requirement], bool, Iterable[str]) -> None self.name = name diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index e822a7f8adc..49e93c510c1 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -391,7 +391,7 @@ def get_console_script_specs(console): return scripts_to_generate -class ZipBackedFile(object): +class ZipBackedFile: def __init__(self, src_record_path, dest_path, zip_file): # type: (RecordPath, str, ZipFile) -> None self.src_record_path = src_record_path @@ -432,7 +432,7 @@ def save(self): set_extracted_file_to_default_mode_plus_executable(self.dest_path) -class ScriptFile(object): +class ScriptFile: def __init__(self, file): # type: (File) -> None self._file = file diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 1550a60d1de..853e2e7fbd4 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -85,7 +85,7 @@ def unpack_vcs_link(link, location): vcs_backend.unpack(location, url=hide_url(link.url)) -class File(object): +class File: def __init__(self, path, content_type): # type: (str, Optional[str]) -> None @@ -279,7 +279,7 @@ def _check_download_dir(link, download_dir, hashes): return download_path -class RequirementPreparer(object): +class RequirementPreparer: """Prepares a Requirement """ diff --git a/src/pip/_internal/req/__init__.py b/src/pip/_internal/req/__init__.py index 8bdec4fc8c5..9f9bc501223 100644 --- a/src/pip/_internal/req/__init__.py +++ b/src/pip/_internal/req/__init__.py @@ -19,7 +19,7 @@ logger = logging.getLogger(__name__) -class InstallationResult(object): +class InstallationResult: def __init__(self, name): # type: (str) -> None self.name = name diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index 2245cb826ff..3564ab399e7 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -171,7 +171,7 @@ def deduce_helpful_msg(req): return msg -class RequirementParts(object): +class RequirementParts: def __init__( self, requirement, # type: Optional[Requirement] diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index 26055b4d6cb..bc5d6dfb2ef 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -77,7 +77,7 @@ SUPPORTED_OPTIONS_REQ_DEST = [str(o().dest) for o in SUPPORTED_OPTIONS_REQ] -class ParsedRequirement(object): +class ParsedRequirement: def __init__( self, requirement, # type:str @@ -96,7 +96,7 @@ def __init__( self.line_source = line_source -class ParsedLine(object): +class ParsedLine: def __init__( self, filename, # type: str @@ -320,7 +320,7 @@ def handle_line( return None -class RequirementsFileParser(object): +class RequirementsFileParser: def __init__( self, session, # type: PipSession diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 92a77f87bc8..f391b47f6d2 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -92,7 +92,7 @@ def _get_dist(metadata_directory): ) -class InstallRequirement(object): +class InstallRequirement: """ Represents something that may be installed later on, may have information about where to fetch the relevant requirement and also contains logic for diff --git a/src/pip/_internal/req/req_set.py b/src/pip/_internal/req/req_set.py index 42c76820d21..96bb4013ebe 100644 --- a/src/pip/_internal/req/req_set.py +++ b/src/pip/_internal/req/req_set.py @@ -17,7 +17,7 @@ logger = logging.getLogger(__name__) -class RequirementSet(object): +class RequirementSet: def __init__(self, check_supported_wheels=True): # type: (bool) -> None diff --git a/src/pip/_internal/req/req_tracker.py b/src/pip/_internal/req/req_tracker.py index cfbfbb10f43..84edbbfae66 100644 --- a/src/pip/_internal/req/req_tracker.py +++ b/src/pip/_internal/req/req_tracker.py @@ -62,7 +62,7 @@ def get_requirement_tracker(): yield tracker -class RequirementTracker(object): +class RequirementTracker: def __init__(self, root): # type: (str) -> None diff --git a/src/pip/_internal/req/req_uninstall.py b/src/pip/_internal/req/req_uninstall.py index 5e62b832897..43d42c3b41d 100644 --- a/src/pip/_internal/req/req_uninstall.py +++ b/src/pip/_internal/req/req_uninstall.py @@ -214,7 +214,7 @@ def compress_for_output_listing(paths): return will_remove, will_skip -class StashedUninstallPathSet(object): +class StashedUninstallPathSet: """A set of file rename operations to stash files while tentatively uninstalling them.""" def __init__(self): @@ -325,7 +325,7 @@ def can_rollback(self): return bool(self._moves) -class UninstallPathSet(object): +class UninstallPathSet: """A set of file paths to be removed in the uninstallation of a requirement.""" def __init__(self, dist): @@ -590,7 +590,7 @@ def from_dist(cls, dist): return paths_to_remove -class UninstallPthEntries(object): +class UninstallPthEntries: def __init__(self, pth_file): # type: (str) -> None self.file = pth_file diff --git a/src/pip/_internal/resolution/base.py b/src/pip/_internal/resolution/base.py index 6d50555e531..f2816ab71c2 100644 --- a/src/pip/_internal/resolution/base.py +++ b/src/pip/_internal/resolution/base.py @@ -11,7 +11,7 @@ ] -class BaseResolver(object): +class BaseResolver: def resolve(self, root_reqs, check_supported_wheels): # type: (List[InstallRequirement], bool) -> RequirementSet raise NotImplementedError() diff --git a/src/pip/_internal/resolution/resolvelib/base.py b/src/pip/_internal/resolution/resolvelib/base.py index 7eb8a178eb9..82c5ec7c72d 100644 --- a/src/pip/_internal/resolution/resolvelib/base.py +++ b/src/pip/_internal/resolution/resolvelib/base.py @@ -26,7 +26,7 @@ def format_name(project, extras): return "{}[{}]".format(project, ",".join(canonical_extras)) -class Constraint(object): +class Constraint: def __init__(self, specifier, hashes): # type: (SpecifierSet, Hashes) -> None self.specifier = specifier @@ -66,7 +66,7 @@ def is_satisfied_by(self, candidate): return self.specifier.contains(candidate.version, prereleases=True) -class Requirement(object): +class Requirement: @property def project_name(self): # type: () -> str @@ -101,7 +101,7 @@ def format_for_error(self): raise NotImplementedError("Subclass should override") -class Candidate(object): +class Candidate: @property def project_name(self): # type: () -> str diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index b4c7bf11351..03d0faadeeb 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -71,7 +71,7 @@ logger = logging.getLogger(__name__) -class Factory(object): +class Factory: def __init__( self, finder, # type: PackageFinder diff --git a/src/pip/_internal/self_outdated_check.py b/src/pip/_internal/self_outdated_check.py index 81594120079..01ed8787b5a 100644 --- a/src/pip/_internal/self_outdated_check.py +++ b/src/pip/_internal/self_outdated_check.py @@ -36,7 +36,7 @@ def _get_statefile_name(key): return name -class SelfCheckState(object): +class SelfCheckState: def __init__(self, cache_dir): # type: (str) -> None self.state = {} # type: Dict[str, Any] diff --git a/src/pip/_internal/utils/hashes.py b/src/pip/_internal/utils/hashes.py index 30a7f4a69f6..35dae235821 100644 --- a/src/pip/_internal/utils/hashes.py +++ b/src/pip/_internal/utils/hashes.py @@ -19,7 +19,7 @@ STRONG_HASHES = ['sha256', 'sha384', 'sha512'] -class Hashes(object): +class Hashes: """A wrapper that builds multiple hashes at once and checks them against known-good values diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index c3d969eb22f..72db88c7f41 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -573,7 +573,7 @@ def write_output(msg, *args): logger.info(msg, *args) -class FakeFile(object): +class FakeFile: """Wrap a list of lines in an object with readline() to make ConfigParser happy.""" def __init__(self, lines): @@ -805,7 +805,7 @@ def redact_auth_from_url(url): return _transform_url(url, _redact_netloc)[0] -class HiddenText(object): +class HiddenText: def __init__( self, secret, # type: str diff --git a/src/pip/_internal/utils/models.py b/src/pip/_internal/utils/models.py index d1c2f226796..e7db67a933f 100644 --- a/src/pip/_internal/utils/models.py +++ b/src/pip/_internal/utils/models.py @@ -6,7 +6,7 @@ import operator -class KeyBasedCompareMixin(object): +class KeyBasedCompareMixin: """Provides comparison capabilities that is based on a key """ diff --git a/src/pip/_internal/utils/pkg_resources.py b/src/pip/_internal/utils/pkg_resources.py index 0bc129acc6a..0f42cc381af 100644 --- a/src/pip/_internal/utils/pkg_resources.py +++ b/src/pip/_internal/utils/pkg_resources.py @@ -7,7 +7,7 @@ from typing import Dict, Iterable, List -class DictMetadata(object): +class DictMetadata: """IMetadataProvider that reads metadata files from a dictionary. """ def __init__(self, metadata): diff --git a/src/pip/_internal/utils/temp_dir.py b/src/pip/_internal/utils/temp_dir.py index c97edc76d6b..f224a041880 100644 --- a/src/pip/_internal/utils/temp_dir.py +++ b/src/pip/_internal/utils/temp_dir.py @@ -45,7 +45,7 @@ def global_tempdir_manager(): _tempdir_manager = old_tempdir_manager -class TempDirectoryTypeRegistry(object): +class TempDirectoryTypeRegistry: """Manages temp directory behavior """ @@ -86,14 +86,14 @@ def tempdir_registry(): _tempdir_registry = old_tempdir_registry -class _Default(object): +class _Default: pass _default = _Default() -class TempDirectory(object): +class TempDirectory: """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 diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index 35caf19157a..2e4713e8a47 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -204,7 +204,7 @@ class RemoteNotFoundError(Exception): pass -class RevOptions(object): +class RevOptions: """ Encapsulates a VCS-specific revision to install, along with any VCS @@ -277,7 +277,7 @@ def make_new(self, rev): return self.vc_class.make_rev_options(rev, extra_args=self.extra_args) -class VcsSupport(object): +class VcsSupport: _registry = {} # type: Dict[str, VersionControl] schemes = ['ssh', 'git', 'hg', 'bzr', 'sftp', 'svn'] @@ -372,7 +372,7 @@ def get_backend(self, name): vcs = VcsSupport() -class VersionControl(object): +class VersionControl: name = '' dirname = '' repo_name = '' diff --git a/tests/conftest.py b/tests/conftest.py index 7e67a0b2aa8..048258f96fe 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -457,13 +457,13 @@ def data(tmpdir): return TestData.copy(tmpdir.joinpath("data")) -class InMemoryPipResult(object): +class InMemoryPipResult: def __init__(self, returncode, stdout): self.returncode = returncode self.stdout = stdout -class InMemoryPip(object): +class InMemoryPip: def pip(self, *args): orig_stdout = sys.stdout stdout = io.StringIO() @@ -506,7 +506,7 @@ def factory(): return factory -class MockServer(object): +class MockServer: def __init__(self, server): # type: (_MockServer) -> None self._server = server diff --git a/tests/functional/test_download.py b/tests/functional/test_download.py index 72b55fda92f..90981395a79 100644 --- a/tests/functional/test_download.py +++ b/tests/functional/test_download.py @@ -324,7 +324,7 @@ def test_download_specify_platform(script, data): ) -class TestDownloadPlatformManylinuxes(object): +class TestDownloadPlatformManylinuxes: """ "pip download --platform" downloads a .whl archive supported for manylinux platforms. diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index c5985243b60..83fe9c94511 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -16,7 +16,7 @@ from tests.lib.path import Path -class ArgRecordingSdist(object): +class ArgRecordingSdist: def __init__(self, sdist_path, args_path): self.sdist_path = sdist_path self._args_path = args_path diff --git a/tests/functional/test_install_upgrade.py b/tests/functional/test_install_upgrade.py index 923a594c623..0dd4f9f8b50 100644 --- a/tests/functional/test_install_upgrade.py +++ b/tests/functional/test_install_upgrade.py @@ -396,7 +396,7 @@ def test_upgrade_vcs_req_with_dist_found(script): assert "pypi.org" not in result.stdout, result.stdout -class TestUpgradeDistributeToSetuptools(object): +class TestUpgradeDistributeToSetuptools: """ From pip1.4 to pip6, pip supported a set of "hacks" (see Issue #1122) to allow distribute to conflict with setuptools, so that the following would diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index b730b3cbdf9..4b40ca23bfe 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -858,7 +858,7 @@ def test_new_resolver_upgrade_strategy(script): assert_installed(script, dep="2.0.0") -class TestExtraMerge(object): +class TestExtraMerge: """ Test installing a package that depends the same package with different extras, one listed as required and the other as in extra. diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index 954145b0a06..26cb4ff94f8 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -146,7 +146,7 @@ def make_test_finder( ) -class TestData(object): +class TestData: """ Represents a bundle of pre-created test data. @@ -230,7 +230,7 @@ class TestFailure(AssertionError): pass -class TestPipResult(object): +class TestPipResult: def __init__(self, impl, verbose=False): self._impl = impl diff --git a/tests/lib/configuration_helpers.py b/tests/lib/configuration_helpers.py index 3e3692696a8..384a424e2d0 100644 --- a/tests/lib/configuration_helpers.py +++ b/tests/lib/configuration_helpers.py @@ -14,7 +14,7 @@ kinds = pip._internal.configuration.kinds -class ConfigurationMixin(object): +class ConfigurationMixin: def setup(self): self.configuration = pip._internal.configuration.Configuration( diff --git a/tests/lib/options_helpers.py b/tests/lib/options_helpers.py index 2354a818df8..8cc5e306d52 100644 --- a/tests/lib/options_helpers.py +++ b/tests/lib/options_helpers.py @@ -17,7 +17,7 @@ def main(self, args): return self.parse_args(args) -class AddFakeCommandMixin(object): +class AddFakeCommandMixin: def setup(self): commands_dict['fake'] = CommandInfo( diff --git a/tests/lib/requests_mocks.py b/tests/lib/requests_mocks.py index e8e3e9c886e..b8ae2d232d2 100644 --- a/tests/lib/requests_mocks.py +++ b/tests/lib/requests_mocks.py @@ -4,7 +4,7 @@ from io import BytesIO -class FakeStream(object): +class FakeStream: def __init__(self, contents): self._io = BytesIO(contents) @@ -19,7 +19,7 @@ def release_conn(self): pass -class MockResponse(object): +class MockResponse: def __init__(self, contents): self.raw = FakeStream(contents) @@ -33,7 +33,7 @@ def __init__(self, contents): self.history = [] -class MockConnection(object): +class MockConnection: def _send(self, req, **kwargs): raise NotImplementedError("_send must be overridden for tests") @@ -45,7 +45,7 @@ def send(self, req, **kwargs): return resp -class MockRequest(object): +class MockRequest: def __init__(self, url): self.url = url diff --git a/tests/lib/venv.py b/tests/lib/venv.py index c5652fecf42..e3ed3450650 100644 --- a/tests/lib/venv.py +++ b/tests/lib/venv.py @@ -9,7 +9,7 @@ from .path import Path -class VirtualEnvironment(object): +class VirtualEnvironment: """ An abstraction around virtual environments, currently it only uses virtualenv but in the future it could use pyvenv. diff --git a/tests/lib/wheel.py b/tests/lib/wheel.py index 2121a175caf..d460a126df9 100644 --- a/tests/lib/wheel.py +++ b/tests/lib/wheel.py @@ -261,7 +261,7 @@ def wheel_name(name, version, pythons, abis, platforms): return "{}.whl".format(stem) -class WheelBuilder(object): +class WheelBuilder: """A wheel that can be saved or converted to several formats. """ diff --git a/tests/unit/test_base_command.py b/tests/unit/test_base_command.py index 6d60ca09420..857d4f4f300 100644 --- a/tests/unit/test_base_command.py +++ b/tests/unit/test_base_command.py @@ -52,7 +52,7 @@ def run(self, options, args): ) -class TestCommand(object): +class TestCommand: def call_main(self, capsys, args): """ @@ -159,7 +159,7 @@ def test_base_command_global_tempdir_cleanup(kind, exists): assert temp_dir._tempdir_manager is None assert temp_dir._tempdir_registry is None - class Holder(object): + class Holder: value = None def create_temp_dirs(options, args): diff --git a/tests/unit/test_check.py b/tests/unit/test_check.py index 1d1921484ea..c53830aa099 100644 --- a/tests/unit/test_check.py +++ b/tests/unit/test_check.py @@ -6,7 +6,7 @@ from pip._internal.operations import check -class TestInstalledDistributionsCall(object): +class TestInstalledDistributionsCall: def test_passes_correct_default_kwargs(self, monkeypatch): my_mock = mock.MagicMock(return_value=[]) diff --git a/tests/unit/test_collector.py b/tests/unit/test_collector.py index 294ea721a3a..ac765c5602b 100644 --- a/tests/unit/test_collector.py +++ b/tests/unit/test_collector.py @@ -640,7 +640,7 @@ def check_links_include(links, names): ) -class TestLinkCollector(object): +class TestLinkCollector: @patch('pip._internal.index.collector._get_html_response') def test_fetch_page(self, mock_get_html_response): diff --git a/tests/unit/test_finder.py b/tests/unit/test_finder.py index 55fdab3b888..d716e582539 100644 --- a/tests/unit/test_finder.py +++ b/tests/unit/test_finder.py @@ -432,7 +432,7 @@ def test_finder_installs_pre_releases_with_version_spec(): assert found.link.url == "https://foo/bar-2.0b1.tar.gz" -class TestLinkEvaluator(object): +class TestLinkEvaluator: def make_test_link_evaluator(self, formats): target_python = TargetPython() diff --git a/tests/unit/test_logging.py b/tests/unit/test_logging.py index 54bce7052ac..b3da43cb857 100644 --- a/tests/unit/test_logging.py +++ b/tests/unit/test_logging.py @@ -15,7 +15,7 @@ logger = logging.getLogger(__name__) -class TestIndentingFormatter(object): +class TestIndentingFormatter: """Test ``pip._internal.utils.logging.IndentingFormatter``.""" def make_record(self, msg, level_name): @@ -110,7 +110,7 @@ def thread_function(): assert results[0] == results[1] -class TestColorizedStreamHandler(object): +class TestColorizedStreamHandler: def _make_log_record(self): attrs = { diff --git a/tests/unit/test_models.py b/tests/unit/test_models.py index f6363367755..8e2975bd7e2 100644 --- a/tests/unit/test_models.py +++ b/tests/unit/test_models.py @@ -6,7 +6,7 @@ from pip._internal.models import candidate, index -class TestPackageIndex(object): +class TestPackageIndex: """Tests for pip._internal.models.index.PackageIndex """ @@ -41,7 +41,7 @@ def test_TestPyPI_urls_are_correct(self): assert pack_index.file_storage_domain == "test-files.pythonhosted.org" -class TestInstallationCandidate(object): +class TestInstallationCandidate: def test_sets_correct_variables(self): obj = candidate.InstallationCandidate( diff --git a/tests/unit/test_models_wheel.py b/tests/unit/test_models_wheel.py index a4f954a2c7d..e0d45f5c840 100644 --- a/tests/unit/test_models_wheel.py +++ b/tests/unit/test_models_wheel.py @@ -6,7 +6,7 @@ from pip._internal.utils import compatibility_tags -class TestWheelFile(object): +class TestWheelFile: def test_std_wheel_pattern(self): w = Wheel('simple-1.1.1-py2-none-any.whl') diff --git a/tests/unit/test_network_auth.py b/tests/unit/test_network_auth.py index 8116b627f79..44c739d864f 100644 --- a/tests/unit/test_network_auth.py +++ b/tests/unit/test_network_auth.py @@ -71,7 +71,7 @@ def test_get_index_url_credentials(): assert get("http://example.com/path3/path2") == (None, None) -class KeyringModuleV1(object): +class KeyringModuleV1: """Represents the supported API of keyring before get_credential was added. """ @@ -209,10 +209,10 @@ def _send(sent_req, **kwargs): assert keyring.saved_passwords == [] -class KeyringModuleV2(object): +class KeyringModuleV2: """Represents the current supported API of keyring""" - class Credential(object): + class Credential: def __init__(self, username, password): self.username = username self.password = password @@ -244,7 +244,7 @@ def test_keyring_get_credential(monkeypatch, url, expect): ) == expect -class KeyringModuleBroken(object): +class KeyringModuleBroken: """Represents the current supported API of keyring, but broken""" def __init__(self): diff --git a/tests/unit/test_network_session.py b/tests/unit/test_network_session.py index a0d1463b2cf..e9b575a96bc 100644 --- a/tests/unit/test_network_session.py +++ b/tests/unit/test_network_session.py @@ -199,7 +199,7 @@ def test_iter_secure_origins__trusted_hosts_empty(self): ], ) def test_is_secure_origin(self, caplog, location, trusted, expected): - class MockLogger(object): + class MockLogger: def __init__(self): self.called = False diff --git a/tests/unit/test_operations_prepare.py b/tests/unit/test_operations_prepare.py index af3ce72a1e0..9bdecc8e0f1 100644 --- a/tests/unit/test_operations_prepare.py +++ b/tests/unit/test_operations_prepare.py @@ -161,7 +161,7 @@ def test_copy_source_tree_with_unreadable_dir_fails(clean_project, tmpdir): assert expected_files == copied_files -class Test_unpack_url(object): +class Test_unpack_url: def prep(self, tmpdir, data): self.build_dir = tmpdir.joinpath('build') diff --git a/tests/unit/test_options.py b/tests/unit/test_options.py index 533a4b8db3d..b02658af109 100644 --- a/tests/unit/test_options.py +++ b/tests/unit/test_options.py @@ -160,7 +160,7 @@ def test_cache_dir__PIP_NO_CACHE_DIR_invalid__with_no_cache_dir( main(['--no-cache-dir', 'fake']) -class TestUsePEP517Options(object): +class TestUsePEP517Options: """ Test options related to using --use-pep517. @@ -431,7 +431,7 @@ def test_client_cert(self): assert options1.client_cert == options2.client_cert == 'path' -class TestOptionsConfigFiles(object): +class TestOptionsConfigFiles: def test_venv_config_file_found(self, monkeypatch): # strict limit on the global config files list diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index e168a3cc164..0c0b1ce4a8c 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -58,7 +58,7 @@ def get_processed_req_from_line(line, fname='file', lineno=1): return req -class TestRequirementSet(object): +class TestRequirementSet: """RequirementSet tests""" def setup(self): @@ -317,7 +317,7 @@ def test_hashed_deps_on_require_hashes(self): )) -class TestInstallRequirement(object): +class TestInstallRequirement: def setup(self): self.tempdir = tempfile.mkdtemp() diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py index 4812637ee5a..0f188d7b0ac 100644 --- a/tests/unit/test_req_file.py +++ b/tests/unit/test_req_file.py @@ -67,7 +67,7 @@ def parse_reqfile( ) -class TestPreprocess(object): +class TestPreprocess: """tests for `preprocess`""" def test_comments_and_joins_case1(self): @@ -97,7 +97,7 @@ def test_comments_and_joins_case3(self): assert list(result) == [(1, 'req1'), (3, 'req2')] -class TestIgnoreComments(object): +class TestIgnoreComments: """tests for `ignore_comment`""" def test_ignore_line(self): @@ -116,7 +116,7 @@ def test_strip_comment(self): assert list(result) == [(1, 'req1'), (2, 'req'), (3, 'req2')] -class TestJoinLines(object): +class TestJoinLines: """tests for `join_lines`""" def test_join_lines(self): @@ -183,7 +183,7 @@ def process_line( return process_line -class TestProcessLine(object): +class TestProcessLine: """tests for `process_line`""" def test_parser_error(self, line_processor): @@ -513,7 +513,7 @@ def get_file_content(filename, *args, **kwargs): assert not result[0].constraint -class TestBreakOptionsArgs(object): +class TestBreakOptionsArgs: def test_no_args(self): assert ('', '--option') == break_args_options('--option') @@ -530,7 +530,7 @@ def test_args_long_options(self): assert ('arg arg', '--long') == result -class TestOptionVariants(object): +class TestOptionVariants: # this suite is really just testing optparse, but added it anyway @@ -555,7 +555,7 @@ def test_variant5(self, line_processor, finder): assert finder.index_urls == ['url'] -class TestParseRequirements(object): +class TestParseRequirements: """tests for `parse_reqfile`""" @pytest.mark.network diff --git a/tests/unit/test_req_install.py b/tests/unit/test_req_install.py index d0d80035299..d8eee8d13d4 100644 --- a/tests/unit/test_req_install.py +++ b/tests/unit/test_req_install.py @@ -12,7 +12,7 @@ from pip._internal.req.req_install import InstallRequirement -class TestInstallRequirementBuildDirectory(object): +class TestInstallRequirementBuildDirectory: # no need to test symlinks on Windows @pytest.mark.skipif("sys.platform == 'win32'") def test_tmp_build_directory(self): @@ -51,7 +51,7 @@ def test_forward_slash_results_in_a_link(self, tmpdir): assert requirement.link is not None -class TestInstallRequirementFrom(object): +class TestInstallRequirementFrom: def test_install_req_from_string_invalid_requirement(self): """ diff --git a/tests/unit/test_req_uninstall.py b/tests/unit/test_req_uninstall.py index d4d707e6042..90bf0d50fbc 100644 --- a/tests/unit/test_req_uninstall.py +++ b/tests/unit/test_req_uninstall.py @@ -24,7 +24,7 @@ def mock_is_local(path): def test_uninstallation_paths(): - class dist(object): + class dist: def get_metadata_lines(self, record): return ['file.py,,', 'file.pyc,,', @@ -116,7 +116,7 @@ def in_tmpdir(paths): assert sorted(expected_rename) == sorted(compact(will_rename)) -class TestUninstallPathSet(object): +class TestUninstallPathSet: def test_add(self, tmpdir, monkeypatch): monkeypatch.setattr(pip._internal.req.req_uninstall, 'is_local', mock_is_local) @@ -215,7 +215,7 @@ def test_detect_symlink_dirs(self, monkeypatch, tmpdir): assert ups.paths == {path1} -class TestStashedUninstallPathSet(object): +class TestStashedUninstallPathSet: WALK_RESULT = [ ("A", ["B", "C"], ["a.py"]), ("A/B", ["D"], ["b.py"]), diff --git a/tests/unit/test_resolution_legacy_resolver.py b/tests/unit/test_resolution_legacy_resolver.py index c4ff6492917..f56ecd96e72 100644 --- a/tests/unit/test_resolution_legacy_resolver.py +++ b/tests/unit/test_resolution_legacy_resolver.py @@ -52,7 +52,7 @@ def make_fake_dist(requires_python=None, metadata_name=None): return FakeDist(metadata, metadata_name=metadata_name) -class TestCheckDistRequiresPython(object): +class TestCheckDistRequiresPython: """ Test _check_dist_requires_python(). @@ -173,7 +173,7 @@ def test_empty_metadata_error(self, caplog, metadata_name): ) -class TestYankedWarning(object): +class TestYankedWarning: """ Test _populate_link() emits warning if one or more candidates are yanked. """ diff --git a/tests/unit/test_self_check_outdated.py b/tests/unit/test_self_check_outdated.py index c5e60d92fc4..42c4c452726 100644 --- a/tests/unit/test_self_check_outdated.py +++ b/tests/unit/test_self_check_outdated.py @@ -17,12 +17,12 @@ from tests.lib.path import Path -class MockBestCandidateResult(object): +class MockBestCandidateResult: def __init__(self, best): self.best_candidate = best -class MockPackageFinder(object): +class MockPackageFinder: BASE_URL = 'https://pypi.org/simple/pip-{0}.tar.gz' PIP_PROJECT_NAME = 'pip' @@ -43,7 +43,7 @@ def find_best_candidate(self, project_name): return MockBestCandidateResult(self.INSTALLATION_CANDIDATES[0]) -class MockDistribution(object): +class MockDistribution: def __init__(self, installer): self.installer = installer diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 57434669e54..41501d38b0f 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -188,7 +188,7 @@ def test_noegglink_in_sitepkgs_venv_global(self): @patch('pip._internal.utils.misc.dist_in_usersite') @patch('pip._internal.utils.misc.dist_is_local') @patch('pip._internal.utils.misc.dist_is_editable') -class TestsGetDistributions(object): +class TestsGetDistributions: """Test get_installed_distributions() and get_distribution(). """ class MockWorkingSet(list): @@ -443,7 +443,7 @@ def test_path_to_display(monkeypatch, path, fs_encoding, expected): assert actual == expected, 'actual: {!r}'.format(actual) -class Test_normalize_path(object): +class Test_normalize_path: # Technically, symlinks are possible on Windows, but you need a special # permission bit to create them, and Python 2 doesn't support it anyway, so # it's easiest just to skip this test on Windows altogether. @@ -480,7 +480,7 @@ def test_resolve_symlinks(self, tmpdir): os.chdir(orig_working_dir) -class TestHashes(object): +class TestHashes: """Tests for pip._internal.utils.hashes""" @pytest.mark.parametrize('hash_name, hex_digest, expected', [ @@ -550,7 +550,7 @@ def test_hash(self): assert cache[Hashes({'sha256': ['ab', 'cd']})] == 42 -class TestEncoding(object): +class TestEncoding: """Tests for pip._internal.utils.encoding""" def test_auto_decode_utf_16_le(self): @@ -596,7 +596,7 @@ def raises(error): raise error -class TestGlibc(object): +class TestGlibc: @pytest.mark.skipif("sys.platform == 'win32'") def test_glibc_version_string(self, monkeypatch): monkeypatch.setattr( @@ -641,7 +641,7 @@ def test_normalize_version_info(version_info, expected): assert actual == expected -class TestGetProg(object): +class TestGetProg: @pytest.mark.parametrize( ("argv", "executable", "expected"), diff --git a/tests/unit/test_utils_compatibility_tags.py b/tests/unit/test_utils_compatibility_tags.py index 64f59a2f98d..735f024c122 100644 --- a/tests/unit/test_utils_compatibility_tags.py +++ b/tests/unit/test_utils_compatibility_tags.py @@ -21,7 +21,7 @@ def test_version_info_to_nodot(version_info, expected): assert actual == expected -class Testcompatibility_tags(object): +class Testcompatibility_tags: def mock_get_config_var(self, **kwd): """ @@ -52,7 +52,7 @@ def test_no_hyphen_tag(self): assert '-' not in tag.platform -class TestManylinux2010Tags(object): +class TestManylinux2010Tags: @pytest.mark.parametrize("manylinux2010,manylinux1", [ ("manylinux2010_x86_64", "manylinux1_x86_64"), @@ -75,7 +75,7 @@ def test_manylinux2010_implies_manylinux1(self, manylinux2010, manylinux1): assert arches[:2] == [manylinux2010, manylinux1] -class TestManylinux2014Tags(object): +class TestManylinux2014Tags: @pytest.mark.parametrize("manylinuxA,manylinuxB", [ ("manylinux2014_x86_64", ["manylinux2010_x86_64", diff --git a/tests/unit/test_utils_subprocess.py b/tests/unit/test_utils_subprocess.py index fd73878c1a7..8a67d7d8da4 100644 --- a/tests/unit/test_utils_subprocess.py +++ b/tests/unit/test_utils_subprocess.py @@ -166,7 +166,7 @@ def finish(self, final_status): self.final_status = final_status -class TestCallSubprocess(object): +class TestCallSubprocess: """ Test call_subprocess(). diff --git a/tests/unit/test_utils_unpacking.py b/tests/unit/test_utils_unpacking.py index 5c2be24d429..94121acff19 100644 --- a/tests/unit/test_utils_unpacking.py +++ b/tests/unit/test_utils_unpacking.py @@ -13,7 +13,7 @@ from pip._internal.utils.unpacking import is_within_directory, untar_file, unzip_file -class TestUnpackArchives(object): +class TestUnpackArchives: """ test_tar.tgz/test_tar.zip have content as follows engineered to confirm 3 things: diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 0f7a3c0747b..52a5fe0436a 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -223,7 +223,7 @@ def test_wheel_root_is_purelib(text, expected): assert wheel.wheel_root_is_purelib(message_from_string(text)) == expected -class TestWheelFile(object): +class TestWheelFile: def test_unpack_wheel_no_flatten(self, tmpdir): filepath = os.path.join(DATA_DIR, 'packages', @@ -232,7 +232,7 @@ def test_unpack_wheel_no_flatten(self, tmpdir): assert os.path.isdir(os.path.join(tmpdir, 'meta-1.0.dist-info')) -class TestInstallUnpackedWheel(object): +class TestInstallUnpackedWheel: """ Tests for moving files from wheel src to scheme paths """ @@ -487,7 +487,7 @@ def test_invalid_entrypoints_fail( assert entrypoint in exc_text -class TestMessageAboutScriptsNotOnPATH(object): +class TestMessageAboutScriptsNotOnPATH: tilde_warning_msg = ( "NOTE: The current PATH contains path(s) starting with `~`, " @@ -644,7 +644,7 @@ def test_multi_script_all_tilde_not_at_start__multi_dir_not_on_PATH(self): assert self.tilde_warning_msg not in retval -class TestWheelHashCalculators(object): +class TestWheelHashCalculators: def prep(self, tmpdir): self.test_file = tmpdir.joinpath("hash.file") From a1fff4a080d1148599c30b074a8dcbb37475701f Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Thu, 24 Dec 2020 14:13:52 -0800 Subject: [PATCH 2827/3170] Replace io.open() and codecs.open() with builtin open() In Python 3, these are functionally equivalent and share the same feature set. --- news/dc9e5ecc-9fc9-4762-914e-34014e8d09bf.trivial.rst | 0 setup.py | 3 +-- src/pip/_internal/pyproject.py | 3 +-- src/pip/_internal/utils/virtualenv.py | 3 +-- tools/automation/release/__init__.py | 3 +-- 5 files changed, 4 insertions(+), 8 deletions(-) create mode 100644 news/dc9e5ecc-9fc9-4762-914e-34014e8d09bf.trivial.rst diff --git a/news/dc9e5ecc-9fc9-4762-914e-34014e8d09bf.trivial.rst b/news/dc9e5ecc-9fc9-4762-914e-34014e8d09bf.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/setup.py b/setup.py index b7d0e8f51d7..b3aaa361396 100644 --- a/setup.py +++ b/setup.py @@ -1,7 +1,6 @@ # The following comment should be removed at some point in the future. # mypy: disallow-untyped-defs=False -import codecs import os import sys @@ -12,7 +11,7 @@ def read(rel_path): here = os.path.abspath(os.path.dirname(__file__)) # intentionally *not* adding an encoding option to open, See: # https://github.com/pypa/virtualenv/issues/201#issuecomment-3145690 - with codecs.open(os.path.join(here, rel_path), 'r') as fp: + with open(os.path.join(here, rel_path), 'r') as fp: return fp.read() diff --git a/src/pip/_internal/pyproject.py b/src/pip/_internal/pyproject.py index 38192b8507f..68ca53bf0bf 100644 --- a/src/pip/_internal/pyproject.py +++ b/src/pip/_internal/pyproject.py @@ -1,4 +1,3 @@ -import io import os from collections import namedtuple @@ -62,7 +61,7 @@ def load_pyproject_toml( has_setup = os.path.isfile(setup_py) if has_pyproject: - with io.open(pyproject_toml, encoding="utf-8") as f: + with open(pyproject_toml, encoding="utf-8") as f: pp_toml = toml.load(f) build_system = pp_toml.get("build-system") else: diff --git a/src/pip/_internal/utils/virtualenv.py b/src/pip/_internal/utils/virtualenv.py index b387ec0b082..acaceee281b 100644 --- a/src/pip/_internal/utils/virtualenv.py +++ b/src/pip/_internal/utils/virtualenv.py @@ -1,4 +1,3 @@ -import io import logging import os import re @@ -52,7 +51,7 @@ def _get_pyvenv_cfg_lines(): try: # Although PEP 405 does not specify, the built-in venv module always # writes with UTF-8. (pypa/pip#8717) - with io.open(pyvenv_cfg_file, encoding='utf-8') as f: + with open(pyvenv_cfg_file, encoding='utf-8') as f: return f.read().splitlines() # avoids trailing newlines except IOError: return None diff --git a/tools/automation/release/__init__.py b/tools/automation/release/__init__.py index c1364cfc461..92e8c4110c4 100644 --- a/tools/automation/release/__init__.py +++ b/tools/automation/release/__init__.py @@ -4,7 +4,6 @@ """ import contextlib -import io import os import pathlib import subprocess @@ -75,7 +74,7 @@ def generate_authors(filename: str) -> None: authors = get_author_list() # Write our authors to the AUTHORS file - with io.open(filename, "w", encoding="utf-8") as fp: + with open(filename, "w", encoding="utf-8") as fp: fp.write("\n".join(authors)) fp.write("\n") From cdcf74fb8eee47ac13f24dc4692c29bc144ea66c Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Wed, 23 Dec 2020 11:25:12 -0800 Subject: [PATCH 2828/3170] Use f-strings for simple string formatting Use pyupgrade to convert simple string formatting to use f-string syntax. pyupgrade is intentionally timid and will not create an f-string if it would make the expression longer or if the substitution parameters are anything but simple names or dotted names. --- docs/pip_sphinxext.py | 6 ++--- ...60-675c-4104-9825-39d1ee0a20b7.trivial.rst | 0 src/pip/_internal/cli/base_command.py | 4 ++-- src/pip/_internal/cli/cmdoptions.py | 2 +- src/pip/_internal/cli/main.py | 2 +- src/pip/_internal/cli/main_parser.py | 4 ++-- src/pip/_internal/cli/parser.py | 6 ++--- src/pip/_internal/cli/progress_bars.py | 2 +- src/pip/_internal/commands/cache.py | 2 +- src/pip/_internal/commands/configuration.py | 2 +- src/pip/_internal/commands/debug.py | 4 ++-- src/pip/_internal/commands/help.py | 4 ++-- src/pip/_internal/configuration.py | 6 ++--- src/pip/_internal/distributions/sdist.py | 2 +- src/pip/_internal/exceptions.py | 10 ++++---- src/pip/_internal/index/collector.py | 4 ++-- src/pip/_internal/index/package_finder.py | 10 ++++---- src/pip/_internal/locations.py | 6 ++--- src/pip/_internal/models/direct_url.py | 2 +- src/pip/_internal/models/link.py | 4 ++-- src/pip/_internal/models/target_python.py | 2 +- src/pip/_internal/models/wheel.py | 2 +- src/pip/_internal/network/auth.py | 4 ++-- src/pip/_internal/network/lazy_wheel.py | 2 +- src/pip/_internal/network/session.py | 4 ++-- .../operations/build/metadata_legacy.py | 2 +- src/pip/_internal/operations/build/wheel.py | 2 +- .../operations/build/wheel_legacy.py | 6 ++--- src/pip/_internal/operations/freeze.py | 6 ++--- .../_internal/operations/install/legacy.py | 2 +- src/pip/_internal/operations/install/wheel.py | 2 +- src/pip/_internal/req/__init__.py | 4 ++-- src/pip/_internal/req/constructors.py | 18 +++++++------- src/pip/_internal/req/req_file.py | 8 +++---- src/pip/_internal/req/req_install.py | 10 ++++---- .../resolution/resolvelib/candidates.py | 14 +++++------ .../resolution/resolvelib/factory.py | 6 ++--- .../resolution/resolvelib/requirements.py | 2 +- src/pip/_internal/self_outdated_check.py | 2 +- src/pip/_internal/utils/compatibility_tags.py | 2 +- src/pip/_internal/utils/direct_url_helpers.py | 2 +- src/pip/_internal/utils/hashes.py | 2 +- src/pip/_internal/utils/misc.py | 10 ++++---- src/pip/_internal/utils/pkg_resources.py | 2 +- src/pip/_internal/utils/temp_dir.py | 8 +++---- src/pip/_internal/utils/unpacking.py | 2 +- src/pip/_internal/utils/wheel.py | 10 ++++---- src/pip/_internal/vcs/git.py | 8 +++---- src/pip/_internal/vcs/versioncontrol.py | 12 +++++----- tests/conftest.py | 2 +- tests/functional/test_cache.py | 16 ++++++------- tests/functional/test_configuration.py | 8 +++---- tests/functional/test_debug.py | 2 +- tests/functional/test_download.py | 10 ++++---- tests/functional/test_freeze.py | 4 ++-- tests/functional/test_install.py | 10 ++++---- tests/functional/test_install_cleanup.py | 2 +- tests/functional/test_install_config.py | 18 +++++++------- tests/functional/test_install_direct_url.py | 2 +- tests/functional/test_install_extras.py | 3 +-- .../test_install_force_reinstall.py | 2 +- tests/functional/test_install_reqs.py | 2 +- tests/functional/test_install_upgrade.py | 2 +- tests/functional/test_install_vcs_git.py | 24 +++++++++---------- tests/functional/test_install_wheel.py | 4 ++-- tests/functional/test_new_resolver.py | 24 +++++++++---------- tests/functional/test_new_resolver_target.py | 2 +- tests/functional/test_show.py | 2 +- tests/functional/test_wheel.py | 2 +- tests/functional/test_yaml.py | 8 +++---- tests/lib/__init__.py | 20 ++++++++-------- tests/lib/index.py | 4 ++-- tests/lib/test_lib.py | 6 ++--- tests/lib/venv.py | 2 +- tests/lib/wheel.py | 10 ++++---- tests/unit/test_cmdoptions.py | 2 +- tests/unit/test_collector.py | 6 ++--- tests/unit/test_commands.py | 2 +- tests/unit/test_compat.py | 6 ++--- tests/unit/test_finder.py | 2 +- tests/unit/test_index.py | 2 +- tests/unit/test_network_lazy_wheel.py | 2 +- tests/unit/test_network_session.py | 4 ++-- tests/unit/test_operations_prepare.py | 2 +- tests/unit/test_options.py | 4 ++-- tests/unit/test_req.py | 6 ++--- tests/unit/test_req_file.py | 10 ++++---- tests/unit/test_resolution_legacy_resolver.py | 4 ++-- tests/unit/test_utils.py | 6 ++--- tests/unit/test_utils_distutils_args.py | 2 +- tests/unit/test_utils_parallel.py | 2 +- tests/unit/test_utils_subprocess.py | 16 ++++++------- tests/unit/test_utils_unpacking.py | 4 ++-- tests/unit/test_utils_wheel.py | 2 +- tools/automation/release/__init__.py | 4 ++-- 95 files changed, 257 insertions(+), 260 deletions(-) create mode 100644 news/ea24fc60-675c-4104-9825-39d1ee0a20b7.trivial.rst diff --git a/docs/pip_sphinxext.py b/docs/pip_sphinxext.py index 2486d5c33b9..df4390d8103 100644 --- a/docs/pip_sphinxext.py +++ b/docs/pip_sphinxext.py @@ -25,7 +25,7 @@ def run(self): cmd_prefix = cmd_prefix.strip('"') cmd_prefix = cmd_prefix.strip("'") usage = dedent( - cmd.usage.replace('%prog', '{} {}'.format(cmd_prefix, cmd.name)) + cmd.usage.replace('%prog', f'{cmd_prefix} {cmd.name}') ).strip() node = nodes.literal_block(usage, usage) return [node] @@ -63,7 +63,7 @@ def _format_option(self, option, cmd_name=None): line += option._long_opts[0] if option.takes_value(): metavar = option.metavar or option.dest.lower() - line += " <{}>".format(metavar.lower()) + line += f" <{metavar.lower()}>" # fix defaults opt_help = option.help.replace('%default', str(option.default)) # fix paths with sys.prefix @@ -123,7 +123,7 @@ def determine_opt_prefix(self, opt_name): if cmd.cmd_opts.has_option(opt_name): return command - raise KeyError('Could not identify prefix of opt {}'.format(opt_name)) + raise KeyError(f'Could not identify prefix of opt {opt_name}') def process_options(self): for option in SUPPORTED_OPTIONS: diff --git a/news/ea24fc60-675c-4104-9825-39d1ee0a20b7.trivial.rst b/news/ea24fc60-675c-4104-9825-39d1ee0a20b7.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index 6a3f3838263..a94a7a47ee7 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -55,7 +55,7 @@ def __init__(self, name, summary, isolated=False): super().__init__() parser_kw = { 'usage': self.usage, - 'prog': '{} {}'.format(get_prog(), name), + 'prog': f'{get_prog()} {name}', 'formatter': UpdatingDefaultsHelpFormatter(), 'add_help_option': False, 'name': name, @@ -70,7 +70,7 @@ def __init__(self, name, summary, isolated=False): self.tempdir_registry = None # type: Optional[TempDirRegistry] # Commands should add options to this option group - optgroup_name = '{} Options'.format(self.name.capitalize()) + optgroup_name = f'{self.name.capitalize()} Options' self.cmd_opts = optparse.OptionGroup(self.parser, optgroup_name) # Add the general options diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index 8f427d6b587..e16f42de610 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -46,7 +46,7 @@ def raise_option_error(parser, option, msg): option: an Option instance. msg: the error text. """ - msg = '{} error: {}'.format(option, msg) + msg = f'{option} error: {msg}' msg = textwrap.fill(' '.join(msg.split())) parser.error(msg) diff --git a/src/pip/_internal/cli/main.py b/src/pip/_internal/cli/main.py index 9a5fbb1f1ec..ed59073072f 100644 --- a/src/pip/_internal/cli/main.py +++ b/src/pip/_internal/cli/main.py @@ -57,7 +57,7 @@ def main(args=None): try: cmd_name, cmd_args = parse_command(args) except PipError as exc: - sys.stderr.write("ERROR: {}".format(exc)) + sys.stderr.write(f"ERROR: {exc}") sys.stderr.write(os.linesep) sys.exit(1) diff --git a/src/pip/_internal/cli/main_parser.py b/src/pip/_internal/cli/main_parser.py index 6d69e82f0cb..fcee6a2c234 100644 --- a/src/pip/_internal/cli/main_parser.py +++ b/src/pip/_internal/cli/main_parser.py @@ -83,9 +83,9 @@ def parse_command(args): if cmd_name not in commands_dict: guess = get_similar_commands(cmd_name) - msg = ['unknown command "{}"'.format(cmd_name)] + msg = [f'unknown command "{cmd_name}"'] if guess: - msg.append('maybe you meant "{}"'.format(guess)) + msg.append(f'maybe you meant "{guess}"') raise CommandError(' - '.join(msg)) diff --git a/src/pip/_internal/cli/parser.py b/src/pip/_internal/cli/parser.py index ba647f3a169..3bc86d9a38a 100644 --- a/src/pip/_internal/cli/parser.py +++ b/src/pip/_internal/cli/parser.py @@ -82,7 +82,7 @@ def format_description(self, description): description = description.rstrip() # dedent, then reindent description = self.indent_lines(textwrap.dedent(description), " ") - description = '{}:\n{}\n'.format(label, description) + description = f'{label}:\n{description}\n' return description else: return '' @@ -168,7 +168,7 @@ def check_default(self, option, key, val): try: return option.check_value(key, val) except optparse.OptionValueError as exc: - print("An error occurred during configuration: {}".format(exc)) + print(f"An error occurred during configuration: {exc}") sys.exit(3) def _get_ordered_configuration_items(self): @@ -279,4 +279,4 @@ def get_default_values(self): def error(self, msg): self.print_usage(sys.stderr) - self.exit(UNKNOWN_ERROR, "{}\n".format(msg)) + self.exit(UNKNOWN_ERROR, f"{msg}\n") diff --git a/src/pip/_internal/cli/progress_bars.py b/src/pip/_internal/cli/progress_bars.py index e248a1a5fa6..15b4f66d2e0 100644 --- a/src/pip/_internal/cli/progress_bars.py +++ b/src/pip/_internal/cli/progress_bars.py @@ -152,7 +152,7 @@ def download_speed(self): def pretty_eta(self): # type: () -> str if self.eta: # type: ignore - return "eta {}".format(self.eta_td) # type: ignore + return f"eta {self.eta_td}" # type: ignore return "" def iter(self, it): # type: ignore diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index 80e668faea9..d5ac45ad738 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -154,7 +154,7 @@ def format_for_human(self, files): for filename in files: wheel = os.path.basename(filename) size = filesystem.format_file_size(filename) - results.append(' - {} ({})'.format(wheel, size)) + results.append(f' - {wheel} ({size})') logger.info('Cache contents:\n') logger.info('\n'.join(sorted(results))) diff --git a/src/pip/_internal/commands/configuration.py b/src/pip/_internal/commands/configuration.py index 1ab90b47b43..a440a2b1774 100644 --- a/src/pip/_internal/commands/configuration.py +++ b/src/pip/_internal/commands/configuration.py @@ -221,7 +221,7 @@ def print_env_var_values(self): write_output("%s:", 'env_var') with indent_log(): for key, value in sorted(self.configuration.get_environ_vars()): - env_var = 'PIP_{}'.format(key.upper()) + env_var = f'PIP_{key.upper()}' write_output("%s=%r", env_var, value) def open_in_editor(self, options, args): diff --git a/src/pip/_internal/commands/debug.py b/src/pip/_internal/commands/debug.py index 747a1c1758b..61df18e20cd 100644 --- a/src/pip/_internal/commands/debug.py +++ b/src/pip/_internal/commands/debug.py @@ -66,7 +66,7 @@ def get_module_from_module_name(module_name): module_name = 'pkg_resources' __import__( - 'pip._vendor.{}'.format(module_name), + f'pip._vendor.{module_name}', globals(), locals(), level=0 @@ -126,7 +126,7 @@ def show_tags(options): formatted_target = target_python.format_given() suffix = '' if formatted_target: - suffix = ' (target: {})'.format(formatted_target) + suffix = f' (target: {formatted_target})' msg = 'Compatible tags: {}{}'.format(len(tags), suffix) logger.info(msg) diff --git a/src/pip/_internal/commands/help.py b/src/pip/_internal/commands/help.py index a6c25478e0f..8372ac615dc 100644 --- a/src/pip/_internal/commands/help.py +++ b/src/pip/_internal/commands/help.py @@ -32,9 +32,9 @@ def run(self, options, args): if cmd_name not in commands_dict: guess = get_similar_commands(cmd_name) - msg = ['unknown command "{}"'.format(cmd_name)] + msg = [f'unknown command "{cmd_name}"'] if guess: - msg.append('maybe you meant "{}"'.format(guess)) + msg.append(f'maybe you meant "{guess}"') raise CommandError(' - '.join(msg)) diff --git a/src/pip/_internal/configuration.py b/src/pip/_internal/configuration.py index a55882b4634..7d85c8dcf07 100644 --- a/src/pip/_internal/configuration.py +++ b/src/pip/_internal/configuration.py @@ -164,7 +164,7 @@ def get_value(self, key): try: return self._dictionary[key] except KeyError: - raise ConfigurationError("No such key - {}".format(key)) + raise ConfigurationError(f"No such key - {key}") def set_value(self, key, value): # type: (str, Any) -> None @@ -193,7 +193,7 @@ def unset_value(self, key): assert self.load_only if key not in self._config[self.load_only]: - raise ConfigurationError("No such key - {}".format(key)) + raise ConfigurationError(f"No such key - {key}") fname, parser = self._get_parser_to_modify() @@ -403,4 +403,4 @@ def _mark_as_modified(self, fname, parser): def __repr__(self): # type: () -> str - return "{}({!r})".format(self.__class__.__name__, self._dictionary) + return f"{self.__class__.__name__}({self._dictionary!r})" diff --git a/src/pip/_internal/distributions/sdist.py b/src/pip/_internal/distributions/sdist.py index 06b9df09cbe..9b708fdd83c 100644 --- a/src/pip/_internal/distributions/sdist.py +++ b/src/pip/_internal/distributions/sdist.py @@ -52,7 +52,7 @@ def _raise_conflicts(conflicting_with, conflicting_reqs): requirement=self.req, conflicting_with=conflicting_with, description=', '.join( - '{} is incompatible with {}'.format(installed, wanted) + f'{installed} is incompatible with {wanted}' for installed, wanted in sorted(conflicting) ) ) diff --git a/src/pip/_internal/exceptions.py b/src/pip/_internal/exceptions.py index 7eef1414c4c..b4e7e68ed90 100644 --- a/src/pip/_internal/exceptions.py +++ b/src/pip/_internal/exceptions.py @@ -203,11 +203,11 @@ def body(self): its link already populated by the resolver's _populate_link(). """ - return ' {}'.format(self._requirement_name()) + return f' {self._requirement_name()}' def __str__(self): # type: () -> str - return '{}\n{}'.format(self.head, self.body()) + return f'{self.head}\n{self.body()}' def _requirement_name(self): # type: () -> str @@ -364,8 +364,8 @@ def __init__(self, reason="could not be loaded", fname=None, error=None): def __str__(self): # type: () -> str if self.fname is not None: - message_part = " in {}.".format(self.fname) + message_part = f" in {self.fname}." else: assert self.error is not None - message_part = ".\n{}\n".format(self.error) - return "Configuration file {}{}".format(self.reason, message_part) + message_part = f".\n{self.error}\n" + return f"Configuration file {self.reason}{message_part}" diff --git a/src/pip/_internal/index/collector.py b/src/pip/_internal/index/collector.py index 2715fcf92a0..a2cfd941b61 100644 --- a/src/pip/_internal/index/collector.py +++ b/src/pip/_internal/index/collector.py @@ -448,7 +448,7 @@ def _get_html_page(link, session=None): reason += str(exc) _handle_get_page_fail(link, reason, meth=logger.info) except requests.ConnectionError as exc: - _handle_get_page_fail(link, "connection error: {}".format(exc)) + _handle_get_page_fail(link, f"connection error: {exc}") except requests.Timeout: _handle_get_page_fail(link, "timed out") else: @@ -657,7 +657,7 @@ def collect_links(self, project_name): ), ] for link in url_locations: - lines.append('* {}'.format(link)) + lines.append(f'* {link}') logger.debug('\n'.join(lines)) return CollectedLinks( diff --git a/src/pip/_internal/index/package_finder.py b/src/pip/_internal/index/package_finder.py index cd768704819..d18790a480e 100644 --- a/src/pip/_internal/index/package_finder.py +++ b/src/pip/_internal/index/package_finder.py @@ -161,7 +161,7 @@ def evaluate_link(self, link): version = None if link.is_yanked and not self._allow_yanked: reason = link.yanked_reason or '<none given>' - return (False, 'yanked for reason: {}'.format(reason)) + return (False, f'yanked for reason: {reason}') if link.egg_fragment: egg_info = link.egg_fragment @@ -171,7 +171,7 @@ def evaluate_link(self, link): if not ext: return (False, 'not a file') if ext not in SUPPORTED_EXTENSIONS: - return (False, 'unsupported archive format: {}'.format(ext)) + return (False, f'unsupported archive format: {ext}') if "binary" not in self._formats and ext == WHEEL_EXTENSION: reason = 'No binaries permitted for {}'.format( self.project_name) @@ -204,7 +204,7 @@ def evaluate_link(self, link): # This should be up by the self.ok_binary check, but see issue 2700. if "source" not in self._formats and ext != WHEEL_EXTENSION: - reason = 'No sources permitted for {}'.format(self.project_name) + reason = f'No sources permitted for {self.project_name}' return (False, reason) if not version: @@ -212,7 +212,7 @@ def evaluate_link(self, link): egg_info, self._canonical_name, ) if not version: - reason = 'Missing project version for {}'.format(self.project_name) + reason = f'Missing project version for {self.project_name}' return (False, reason) match = self._py_version_re.search(version) @@ -983,7 +983,7 @@ def _find_name_version_sep(fragment, canonical_name): continue if canonicalize_name(fragment[:i]) == canonical_name: return i - raise ValueError("{} does not match {}".format(fragment, canonical_name)) + raise ValueError(f"{fragment} does not match {canonical_name}") def _extract_version_from_fragment(fragment, canonical_name): diff --git a/src/pip/_internal/locations.py b/src/pip/_internal/locations.py index 7d549dcef1e..dc5d2e0b2d8 100644 --- a/src/pip/_internal/locations.py +++ b/src/pip/_internal/locations.py @@ -111,8 +111,8 @@ def distutils_scheme( # 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={} prefix={}".format(user, prefix) - assert not (home and prefix), "home={} prefix={}".format(home, prefix) + assert not (user and prefix), f"user={user} prefix={prefix}" + assert not (home and prefix), f"home={home} prefix={prefix}" i.user = user or i.user if user or home: i.prefix = "" @@ -138,7 +138,7 @@ def distutils_scheme( i.prefix, 'include', 'site', - 'python{}'.format(get_major_minor_version()), + f'python{get_major_minor_version()}', dist_name, ) diff --git a/src/pip/_internal/models/direct_url.py b/src/pip/_internal/models/direct_url.py index 8f544caf603..4db5d000ed3 100644 --- a/src/pip/_internal/models/direct_url.py +++ b/src/pip/_internal/models/direct_url.py @@ -46,7 +46,7 @@ def _get_required(d, expected_type, key, default=None): # type: (Dict[str, Any], Type[T], str, Optional[T]) -> T value = _get(d, expected_type, key, default) if value is None: - raise DirectUrlValidationError("{} must have a value".format(key)) + raise DirectUrlValidationError(f"{key} must have a value") return value diff --git a/src/pip/_internal/models/link.py b/src/pip/_internal/models/link.py index 6a1920f84bd..07f9c565c12 100644 --- a/src/pip/_internal/models/link.py +++ b/src/pip/_internal/models/link.py @@ -83,7 +83,7 @@ def __init__( def __str__(self): # type: () -> str if self.requires_python: - rp = ' (requires-python:{})'.format(self.requires_python) + rp = f' (requires-python:{self.requires_python})' else: rp = '' if self.comes_from: @@ -94,7 +94,7 @@ def __str__(self): def __repr__(self): # type: () -> str - return '<Link {}>'.format(self) + return f'<Link {self}>' @property def url(self): diff --git a/src/pip/_internal/models/target_python.py b/src/pip/_internal/models/target_python.py index 4593dc854f8..0843dc775f4 100644 --- a/src/pip/_internal/models/target_python.py +++ b/src/pip/_internal/models/target_python.py @@ -86,7 +86,7 @@ def format_given(self): ('implementation', self.implementation), ] return ' '.join( - '{}={!r}'.format(key, value) for key, value in key_values + f'{key}={value!r}' for key, value in key_values if value is not None ) diff --git a/src/pip/_internal/models/wheel.py b/src/pip/_internal/models/wheel.py index 4d4068f3b73..debcc26a343 100644 --- a/src/pip/_internal/models/wheel.py +++ b/src/pip/_internal/models/wheel.py @@ -30,7 +30,7 @@ def __init__(self, filename): wheel_info = self.wheel_file_re.match(filename) if not wheel_info: raise InvalidWheelFilename( - "{} is not a valid wheel filename.".format(filename) + f"{filename} is not a valid wheel filename." ) self.filename = filename self.name = wheel_info.group('name').replace('_', '-') diff --git a/src/pip/_internal/network/auth.py b/src/pip/_internal/network/auth.py index 3de21518e96..1a613854061 100644 --- a/src/pip/_internal/network/auth.py +++ b/src/pip/_internal/network/auth.py @@ -199,7 +199,7 @@ def _get_url_and_credentials(self, original_url): (username is not None and password is not None) or # Credentials were not found (username is None and password is None) - ), "Could not load credentials from url: {}".format(original_url) + ), f"Could not load credentials from url: {original_url}" return url, username, password @@ -223,7 +223,7 @@ def __call__(self, req): # Factored out to allow for easy patching in tests def _prompt_for_password(self, netloc): # type: (str) -> Tuple[Optional[str], Optional[str], bool] - username = ask_input("User for {}: ".format(netloc)) + username = ask_input(f"User for {netloc}: ") if not username: return None, None, False auth = get_keyring_auth(netloc, username) diff --git a/src/pip/_internal/network/lazy_wheel.py b/src/pip/_internal/network/lazy_wheel.py index 83704f6f190..ea375d9a8cd 100644 --- a/src/pip/_internal/network/lazy_wheel.py +++ b/src/pip/_internal/network/lazy_wheel.py @@ -190,7 +190,7 @@ def _stream_response(self, start, end, base_headers=HEADERS): # type: (int, int, Dict[str, str]) -> Response """Return HTTP response to a range request from start to end.""" headers = base_headers.copy() - headers['Range'] = 'bytes={}-{}'.format(start, end) + headers['Range'] = f'bytes={start}-{end}' # TODO: Get range requests to be correctly cached headers['Cache-Control'] = 'no-cache' return self._session.get(self._url, headers=headers, stream=True) diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py index 43ab1e18cd8..bf6eeb9b4ea 100644 --- a/src/pip/_internal/network/session.py +++ b/src/pip/_internal/network/session.py @@ -317,9 +317,9 @@ def add_trusted_host(self, host, source=None, suppress_logging=False): string came from. """ if not suppress_logging: - msg = 'adding trusted host: {!r}'.format(host) + msg = f'adding trusted host: {host!r}' if source is not None: - msg += ' (from {})'.format(source) + msg += f' (from {source})' logger.info(msg) host_port = parse_netloc(host) diff --git a/src/pip/_internal/operations/build/metadata_legacy.py b/src/pip/_internal/operations/build/metadata_legacy.py index 14762aef3c0..d44589666f4 100644 --- a/src/pip/_internal/operations/build/metadata_legacy.py +++ b/src/pip/_internal/operations/build/metadata_legacy.py @@ -26,7 +26,7 @@ def _find_egg_info(directory): if not filenames: raise InstallationError( - "No .egg-info directory found in {}".format(directory) + f"No .egg-info directory found in {directory}" ) if len(filenames) > 1: diff --git a/src/pip/_internal/operations/build/wheel.py b/src/pip/_internal/operations/build/wheel.py index d16ee0966e1..d25f9c42f62 100644 --- a/src/pip/_internal/operations/build/wheel.py +++ b/src/pip/_internal/operations/build/wheel.py @@ -34,7 +34,7 @@ def build_wheel_pep517( logger.debug('Destination directory: %s', tempd) runner = runner_with_spinner_message( - 'Building wheel for {} (PEP 517)'.format(name) + f'Building wheel for {name} (PEP 517)' ) with backend.subprocess_runner(runner): wheel_name = backend.build_wheel( diff --git a/src/pip/_internal/operations/build/wheel_legacy.py b/src/pip/_internal/operations/build/wheel_legacy.py index 73401cd78c4..82fa44406e6 100644 --- a/src/pip/_internal/operations/build/wheel_legacy.py +++ b/src/pip/_internal/operations/build/wheel_legacy.py @@ -23,7 +23,7 @@ def format_command_result( # type: (...) -> str """Format command information for logging.""" command_desc = format_command_args(command_args) - text = 'Command arguments: {}\n'.format(command_desc) + text = f'Command arguments: {command_desc}\n' if not command_output: text += 'Command output: None' @@ -32,7 +32,7 @@ def format_command_result( else: if not command_output.endswith('\n'): command_output += '\n' - text += 'Command output:\n{}{}'.format(command_output, LOG_DIVIDER) + text += f'Command output:\n{command_output}{LOG_DIVIDER}' return text @@ -87,7 +87,7 @@ def build_wheel_legacy( destination_dir=tempd, ) - spin_message = 'Building wheel for {} (setup.py)'.format(name) + spin_message = f'Building wheel for {name} (setup.py)' with open_spinner(spin_message) as spinner: logger.debug('Destination directory: %s', tempd) diff --git a/src/pip/_internal/operations/freeze.py b/src/pip/_internal/operations/freeze.py index 3529c55edc2..18298aece6d 100644 --- a/src/pip/_internal/operations/freeze.py +++ b/src/pip/_internal/operations/freeze.py @@ -56,7 +56,7 @@ def freeze( find_links = find_links or [] for link in find_links: - yield '-f {}'.format(link) + yield f'-f {link}' installations = {} # type: Dict[str, FrozenRequirement] for dist in get_installed_distributions( @@ -195,7 +195,7 @@ def get_requirement_info(dist): location, ) comments = [ - '# Editable install with no version control ({})'.format(req) + f'# Editable install with no version control ({req})' ] return (location, True, comments) @@ -270,5 +270,5 @@ def __str__(self): # type: () -> str req = self.req if self.editable: - req = '-e {}'.format(req) + req = f'-e {req}' return '\n'.join(list(self.comments) + [str(req)]) + '\n' diff --git a/src/pip/_internal/operations/install/legacy.py b/src/pip/_internal/operations/install/legacy.py index 87227d5fed6..63a693a91ed 100644 --- a/src/pip/_internal/operations/install/legacy.py +++ b/src/pip/_internal/operations/install/legacy.py @@ -68,7 +68,7 @@ def install( ) runner = runner_with_spinner_message( - "Running setup.py install for {}".format(req_name) + f"Running setup.py install for {req_name}" ) with indent_log(), build_env: runner( diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index e822a7f8adc..fd59dbd7a13 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -360,7 +360,7 @@ def get_console_script_specs(console): ) scripts_to_generate.append( - 'pip{} = {}'.format(get_major_minor_version(), pip_script) + f'pip{get_major_minor_version()} = {pip_script}' ) # Delete any other versioned pip entry points pip_ep = [k for k in console if re.match(r'pip(\d(\.\d)?)?$', k)] diff --git a/src/pip/_internal/req/__init__.py b/src/pip/_internal/req/__init__.py index 8bdec4fc8c5..92622933529 100644 --- a/src/pip/_internal/req/__init__.py +++ b/src/pip/_internal/req/__init__.py @@ -26,7 +26,7 @@ def __init__(self, name): def __repr__(self): # type: () -> str - return "InstallationResult(name={!r})".format(self.name) + return f"InstallationResult(name={self.name!r})" def _validate_requirements( @@ -34,7 +34,7 @@ def _validate_requirements( ): # type: (...) -> Iterator[Tuple[str, InstallRequirement]] for req in requirements: - assert req.name, "invalid to-be-installed requirement: {}".format(req) + assert req.name, f"invalid to-be-installed requirement: {req}" yield req.name, req diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index 2245cb826ff..382eac96cef 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -111,8 +111,8 @@ def parse_editable(editable_req): return package_name, url_no_extras, set() for version_control in vcs: - if url.lower().startswith('{}:'.format(version_control)): - url = '{}+{}'.format(version_control, url) + if url.lower().startswith(f'{version_control}:'): + url = f'{version_control}+{url}' break if '+' not in url: @@ -167,7 +167,7 @@ def deduce_helpful_msg(req): "Cannot parse '%s' as requirements file", req, exc_info=True ) else: - msg += " File '{}' does not exist.".format(req) + msg += f" File '{req}' does not exist." return msg @@ -193,7 +193,7 @@ def parse_req_from_editable(editable_req): try: req = Requirement(name) except InvalidRequirement: - raise InstallationError("Invalid requirement: '{}'".format(name)) + raise InstallationError(f"Invalid requirement: '{name}'") else: req = None @@ -342,7 +342,7 @@ def with_source(text): # type: (str) -> str if not line_source: return text - return '{} (from {})'.format(text, line_source) + return f'{text} (from {line_source})' if req_as_string is not None: try: @@ -357,10 +357,10 @@ def with_source(text): else: add_msg = '' msg = with_source( - 'Invalid requirement: {!r}'.format(req_as_string) + f'Invalid requirement: {req_as_string!r}' ) if add_msg: - msg += '\nHint: {}'.format(add_msg) + msg += f'\nHint: {add_msg}' raise InstallationError(msg) else: # Deprecate extras after specifiers: "name>=1.0[extras]" @@ -370,7 +370,7 @@ def with_source(text): for spec in req.specifier: spec_str = str(spec) if spec_str.endswith(']'): - msg = "Extras after version '{}'.".format(spec_str) + msg = f"Extras after version '{spec_str}'." replace = "moving the extras before version specifiers" deprecated(msg, replacement=replace, gone_in="21.0") else: @@ -421,7 +421,7 @@ def install_req_from_req_string( try: req = Requirement(req_string) except InvalidRequirement: - raise InstallationError("Invalid requirement: '{}'".format(req_string)) + raise InstallationError(f"Invalid requirement: '{req_string}'") domains_not_allowed = [ PyPI.file_storage_domain, diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index 26055b4d6cb..88154324dc1 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -201,7 +201,7 @@ def handle_requirement_line( if dest in line.opts.__dict__ and line.opts.__dict__[dest]: req_options[dest] = line.opts.__dict__[dest] - line_source = 'line {} of {}'.format(line.lineno, line.filename) + line_source = f'line {line.lineno} of {line.filename}' return ParsedRequirement( requirement=line.requirement, is_editable=line.is_editable, @@ -271,7 +271,7 @@ def handle_option_line( if session: for host in opts.trusted_hosts or []: - source = 'line {} of {}'.format(lineno, filename) + source = f'line {lineno} of {filename}' session.add_trusted_host(host, source=source) @@ -381,7 +381,7 @@ def _parse_file(self, filename, constraint): args_str, opts = self._line_parser(line) except OptionParsingError as e: # add offending line - msg = 'Invalid requirement: {}\n{}'.format(line, e.msg) + msg = f'Invalid requirement: {line}\n{e.msg}' raise RequirementsFileParseError(msg) yield ParsedLine( @@ -559,6 +559,6 @@ def get_file_content(url, session): content = auto_decode(f.read()) except IOError as exc: raise InstallationError( - 'Could not open requirements file: {}'.format(exc) + f'Could not open requirements file: {exc}' ) return url, content diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 92a77f87bc8..1bddf1f2551 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -226,7 +226,7 @@ def __str__(self): else: comes_from = self.comes_from.from_path() if comes_from: - s += ' (from {})'.format(comes_from) + s += f' (from {comes_from})' return s def __repr__(self): @@ -364,7 +364,7 @@ def ensure_build_location(self, build_dir, autodelete, parallel_builds): # name so multiple builds do not interfere with each other. dir_name = canonicalize_name(self.name) if parallel_builds: - dir_name = "{}_{}".format(dir_name, uuid.uuid4().hex) + dir_name = f"{dir_name}_{uuid.uuid4().hex}" # FIXME: Is there a better place to create the build_dir? (hg and bzr # need this) @@ -475,7 +475,7 @@ def unpacked_source_directory(self): @property def setup_py_path(self): # type: () -> str - assert self.source_dir, "No source dir for {}".format(self) + assert self.source_dir, f"No source dir for {self}" setup_py = os.path.join(self.unpacked_source_directory, 'setup.py') return setup_py @@ -483,7 +483,7 @@ def setup_py_path(self): @property def pyproject_toml_path(self): # type: () -> str - assert self.source_dir, "No source dir for {}".format(self) + assert self.source_dir, f"No source dir for {self}" return make_pyproject_path(self.unpacked_source_directory) def load_pyproject_toml(self): @@ -526,7 +526,7 @@ def _generate_metadata(self): setup_py_path=self.setup_py_path, source_dir=self.unpacked_source_directory, isolated=self.isolated, - details=self.name or "from {}".format(self.link) + details=self.name or f"from {self.link}" ) assert self.pep517_backend is not None diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 275cb5105a4..39358711acb 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -88,9 +88,9 @@ def make_install_req_from_dist(dist, template): if template.req: line = str(template.req) elif template.link: - line = "{} @ {}".format(project_name, template.link.url) + line = f"{project_name} @ {template.link.url}" else: - line = "{}=={}".format(project_name, dist.parsed_version) + line = f"{project_name}=={dist.parsed_version}" ireq = install_req_from_line( line, user_supplied=template.user_supplied, @@ -145,7 +145,7 @@ def __init__( def __str__(self): # type: () -> str - return "{} {}".format(self.name, self.version) + return f"{self.name} {self.version}" def __repr__(self): # type: () -> str @@ -288,7 +288,7 @@ def __init__( wheel = Wheel(ireq.link.filename) wheel_name = canonicalize_name(wheel.name) assert name == wheel_name, ( - "{!r} != {!r} for wheel".format(name, wheel_name) + f"{name!r} != {wheel_name!r} for wheel" ) # Version may not be present for PEP 508 direct URLs if version is not None: @@ -416,7 +416,7 @@ def is_editable(self): def format_for_error(self): # type: () -> str - return "{} {} (Installed)".format(self.name, self.version) + return f"{self.name} {self.version} (Installed)" def iter_dependencies(self, with_requires): # type: (bool) -> Iterable[Optional[Requirement]] @@ -584,7 +584,7 @@ def __init__(self, py_version_info): def __str__(self): # type: () -> str - return "Python {}".format(self._version) + return f"Python {self._version}" @property def project_name(self): @@ -604,7 +604,7 @@ def version(self): def format_for_error(self): # type: () -> str - return "Python {}".format(self.version) + return f"Python {self.version}" def iter_dependencies(self, with_requires): # type: (bool) -> Iterable[Optional[Requirement]] diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index b4c7bf11351..aaafba072eb 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -391,13 +391,13 @@ def get_installation_error(self, e): if parent is None: req_disp = str(req) else: - req_disp = '{} (from {})'.format(req, parent.name) + req_disp = f'{req} (from {parent.name})' logger.critical( "Could not find a version that satisfies the requirement %s", req_disp, ) return DistributionNotFound( - 'No matching distribution found for {}'.format(req) + f'No matching distribution found for {req}' ) # OK, we now have a list of requirements that can't all be @@ -415,7 +415,7 @@ def describe_trigger(parent): # type: (Candidate) -> str ireq = parent.get_install_requirement() if not ireq or not ireq.comes_from: - return "{}=={}".format(parent.name, parent.version) + return f"{parent.name}=={parent.version}" if isinstance(ireq.comes_from, InstallRequirement): return str(ireq.comes_from.name) return str(ireq.comes_from) diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py index d926d0a0656..85343d5980e 100644 --- a/src/pip/_internal/resolution/resolvelib/requirements.py +++ b/src/pip/_internal/resolution/resolvelib/requirements.py @@ -122,7 +122,7 @@ def __init__(self, specifier, match): def __str__(self): # type: () -> str - return "Python {}".format(self.specifier) + return f"Python {self.specifier}" def __repr__(self): # type: () -> str diff --git a/src/pip/_internal/self_outdated_check.py b/src/pip/_internal/self_outdated_check.py index 81594120079..99325efc84d 100644 --- a/src/pip/_internal/self_outdated_check.py +++ b/src/pip/_internal/self_outdated_check.py @@ -181,7 +181,7 @@ def pip_self_version_check(session, options): # command context, so be pragmatic here and suggest the command # that's always available. This does not accommodate spaces in # `sys.executable`. - pip_cmd = "{} -m pip".format(sys.executable) + pip_cmd = f"{sys.executable} -m pip" logger.warning( "You are using pip version %s; however, version %s is " "available.\nYou should consider upgrading via the " diff --git a/src/pip/_internal/utils/compatibility_tags.py b/src/pip/_internal/utils/compatibility_tags.py index 4bf5aaa9369..ac37c3a17ba 100644 --- a/src/pip/_internal/utils/compatibility_tags.py +++ b/src/pip/_internal/utils/compatibility_tags.py @@ -116,7 +116,7 @@ def _get_custom_interpreter(implementation=None, version=None): implementation = interpreter_name() if version is None: version = interpreter_version() - return "{}{}".format(implementation, version) + return f"{implementation}{version}" def get_supported( diff --git a/src/pip/_internal/utils/direct_url_helpers.py b/src/pip/_internal/utils/direct_url_helpers.py index 87bd61fa01f..376212ca113 100644 --- a/src/pip/_internal/utils/direct_url_helpers.py +++ b/src/pip/_internal/utils/direct_url_helpers.py @@ -93,7 +93,7 @@ def direct_url_from_link(link, source_dir=None, link_is_in_wheel_cache=False): hash = None hash_name = link.hash_name if hash_name: - hash = "{}={}".format(hash_name, link.hash) + hash = f"{hash_name}={link.hash}" return DirectUrl( url=link.url_without_fragment, info=ArchiveInfo(hash=hash), diff --git a/src/pip/_internal/utils/hashes.py b/src/pip/_internal/utils/hashes.py index 30a7f4a69f6..e0c9c303b8e 100644 --- a/src/pip/_internal/utils/hashes.py +++ b/src/pip/_internal/utils/hashes.py @@ -85,7 +85,7 @@ def check_against_chunks(self, chunks): gots[hash_name] = hashlib.new(hash_name) except (ValueError, TypeError): raise InstallationError( - 'Unknown hash name: {}'.format(hash_name) + f'Unknown hash name: {hash_name}' ) for chunk in chunks: diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index c3d969eb22f..2448185798e 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -116,7 +116,7 @@ def get_prog(): try: prog = os.path.basename(sys.argv[0]) if prog in ('__main__.py', '-c'): - return "{} -m pip".format(sys.executable) + return f"{sys.executable} -m pip" else: return prog except (AttributeError, TypeError, IndexError): @@ -676,8 +676,8 @@ def build_netloc(host, port): return host if ':' in host: # Only wrap host with square brackets when it is IPv6 - host = '[{}]'.format(host) - return '{}:{}'.format(host, port) + host = f'[{host}]' + return f'{host}:{port}' def build_url_from_netloc(netloc, scheme='https'): @@ -687,8 +687,8 @@ def build_url_from_netloc(netloc, scheme='https'): """ if netloc.count(':') >= 2 and '@' not in netloc and '[' not in netloc: # It must be a bare IPv6 address, so wrap it with brackets. - netloc = '[{}]'.format(netloc) - return '{}://{}'.format(scheme, netloc) + netloc = f'[{netloc}]' + return f'{scheme}://{netloc}' def parse_netloc(netloc): diff --git a/src/pip/_internal/utils/pkg_resources.py b/src/pip/_internal/utils/pkg_resources.py index 0bc129acc6a..73eeecb28c8 100644 --- a/src/pip/_internal/utils/pkg_resources.py +++ b/src/pip/_internal/utils/pkg_resources.py @@ -24,7 +24,7 @@ def get_metadata(self, name): return ensure_str(self._metadata[name]) except UnicodeDecodeError as e: # Mirrors handling done in pkg_resources.NullProvider. - e.reason += " in {} file".format(name) + e.reason += f" in {name} file" raise def get_metadata_lines(self, name): diff --git a/src/pip/_internal/utils/temp_dir.py b/src/pip/_internal/utils/temp_dir.py index c97edc76d6b..6d86f6ffd6f 100644 --- a/src/pip/_internal/utils/temp_dir.py +++ b/src/pip/_internal/utils/temp_dir.py @@ -151,13 +151,13 @@ def __init__( def path(self): # type: () -> str assert not self._deleted, ( - "Attempted to access deleted path: {}".format(self._path) + f"Attempted to access deleted path: {self._path}" ) return self._path def __repr__(self): # type: () -> str - return "<{} {!r}>".format(self.__class__.__name__, self.path) + return f"<{self.__class__.__name__} {self.path!r}>" def __enter__(self): # type: (_T) -> _T @@ -184,7 +184,7 @@ def _create(self, kind): # scripts, so we canonicalize the path by traversing potential # symlinks here. path = os.path.realpath( - tempfile.mkdtemp(prefix="pip-{}-".format(kind)) + tempfile.mkdtemp(prefix=f"pip-{kind}-") ) logger.debug("Created temporary directory: %s", path) return path @@ -275,7 +275,7 @@ def _create(self, kind): else: # Final fallback on the default behavior. path = os.path.realpath( - tempfile.mkdtemp(prefix="pip-{}-".format(kind)) + tempfile.mkdtemp(prefix=f"pip-{kind}-") ) logger.debug("Created temporary directory: %s", path) diff --git a/src/pip/_internal/utils/unpacking.py b/src/pip/_internal/utils/unpacking.py index 5cfba87f81a..a24d7e55735 100644 --- a/src/pip/_internal/utils/unpacking.py +++ b/src/pip/_internal/utils/unpacking.py @@ -273,5 +273,5 @@ def unpack_file( filename, location, content_type, ) raise InstallationError( - 'Cannot determine archive format of {}'.format(location) + f'Cannot determine archive format of {location}' ) diff --git a/src/pip/_internal/utils/wheel.py b/src/pip/_internal/utils/wheel.py index c6dc4ccb0d9..2c01cf9927b 100644 --- a/src/pip/_internal/utils/wheel.py +++ b/src/pip/_internal/utils/wheel.py @@ -57,7 +57,7 @@ def pkg_resources_distribution_for_wheel(wheel_zip, name, location): info_dir, _ = parse_wheel(wheel_zip, name) metadata_files = [ - p for p in wheel_zip.namelist() if p.startswith("{}/".format(info_dir)) + p for p in wheel_zip.namelist() if p.startswith(f"{info_dir}/") ] metadata_text = {} # type: Dict[str, bytes] @@ -152,7 +152,7 @@ def read_wheel_metadata_file(source, path): # and RuntimeError for password-protected files except (BadZipFile, KeyError, RuntimeError) as e: raise UnsupportedWheel( - "could not read {!r} file: {!r}".format(path, e) + f"could not read {path!r} file: {e!r}" ) @@ -161,14 +161,14 @@ def wheel_metadata(source, dist_info_dir): """Return the WHEEL metadata of an extracted wheel, if possible. Otherwise, raise UnsupportedWheel. """ - path = "{}/WHEEL".format(dist_info_dir) + path = f"{dist_info_dir}/WHEEL" # Zip file path separators must be / wheel_contents = read_wheel_metadata_file(source, path) try: wheel_text = ensure_str(wheel_contents) except UnicodeDecodeError as e: - raise UnsupportedWheel("error decoding {!r}: {!r}".format(path, e)) + raise UnsupportedWheel(f"error decoding {path!r}: {e!r}") # FeedParser (used by Parser) does not raise any exceptions. The returned # message may have .defects populated, but for backwards-compatibility we @@ -190,7 +190,7 @@ def wheel_version(wheel_data): try: return tuple(map(int, version.split('.'))) except ValueError: - raise UnsupportedWheel("invalid Wheel-Version: {!r}".format(version)) + raise UnsupportedWheel(f"invalid Wheel-Version: {version!r}") def check_compatibility(version, name): diff --git a/src/pip/_internal/vcs/git.py b/src/pip/_internal/vcs/git.py index 46f15fc8b41..d55992ec450 100644 --- a/src/pip/_internal/vcs/git.py +++ b/src/pip/_internal/vcs/git.py @@ -147,12 +147,12 @@ def get_revision_sha(cls, dest, rev): except ValueError: # Include the offending line to simplify troubleshooting if # this error ever occurs. - raise ValueError('unexpected show-ref line: {!r}'.format(line)) + raise ValueError(f'unexpected show-ref line: {line!r}') refs[ref] = sha - branch_ref = 'refs/remotes/origin/{}'.format(rev) - tag_ref = 'refs/tags/{}'.format(rev) + branch_ref = f'refs/remotes/origin/{rev}' + tag_ref = f'refs/tags/{rev}' sha = refs.get(branch_ref) if sha is not None: @@ -266,7 +266,7 @@ def fetch_new(self, dest, url, rev_options): elif self.get_current_branch(dest) != branch_name: # Then a specific branch was requested, and that branch # is not yet checked out. - track_branch = 'origin/{}'.format(branch_name) + track_branch = f'origin/{branch_name}' cmd_args = [ 'checkout', '-b', branch_name, '--track', track_branch, ] diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index 35caf19157a..2784f07ee75 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -77,9 +77,9 @@ def make_vcs_requirement_url(repo_url, rev, project_name, subdir=None): project_name: the (unescaped) project name. """ egg_project_name = pkg_resources.to_filename(project_name) - req = '{}@{}#egg={}'.format(repo_url, rev, egg_project_name) + req = f'{repo_url}@{rev}#egg={egg_project_name}' if subdir: - req += '&subdirectory={}'.format(subdir) + req += f'&subdirectory={subdir}' return req @@ -236,7 +236,7 @@ def __init__( def __repr__(self): # type: () -> str - return '<RevOptions {}: rev={!r}>'.format(self.vc_class.name, self.rev) + return f'<RevOptions {self.vc_class.name}: rev={self.rev!r}>' @property def arg_rev(self): @@ -264,7 +264,7 @@ def to_display(self): if not self.rev: return '' - return ' (to revision {})'.format(self.rev) + return f' (to revision {self.rev})' def make_new(self, rev): # type: (str) -> RevOptions @@ -389,7 +389,7 @@ def should_add_vcs_url_prefix(cls, remote_url): Return whether the vcs prefix (e.g. "git+") should be added to a repository's remote url when used in a requirement. """ - return not remote_url.lower().startswith('{}:'.format(cls.name)) + return not remote_url.lower().startswith(f'{cls.name}:') @classmethod def get_subdirectory(cls, location): @@ -427,7 +427,7 @@ def get_src_requirement(cls, repo_dir, project_name): return None if cls.should_add_vcs_url_prefix(repo_url): - repo_url = '{}+{}'.format(cls.name, repo_url) + repo_url = f'{cls.name}+{repo_url}' revision = cls.get_requirement_revision(repo_dir) subdir = cls.get_subdirectory(repo_dir) diff --git a/tests/conftest.py b/tests/conftest.py index 7e67a0b2aa8..e12e4ae67ab 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -95,7 +95,7 @@ def pytest_collection_modifyitems(config, items): item.add_marker(pytest.mark.unit) else: raise RuntimeError( - "Unknown test type (filename = {})".format(module_path) + f"Unknown test type (filename = {module_path})" ) diff --git a/tests/functional/test_cache.py b/tests/functional/test_cache.py index 872f55982ba..0dc79108182 100644 --- a/tests/functional/test_cache.py +++ b/tests/functional/test_cache.py @@ -103,7 +103,7 @@ def list_matches_wheel(wheel_name, result): E.g., If wheel_name is `foo-1.2.3` it searches for a line starting with `- foo-1.2.3-py3-none-any.whl `.""" lines = result.stdout.splitlines() - expected = ' - {}-py3-none-any.whl '.format(wheel_name) + expected = f' - {wheel_name}-py3-none-any.whl ' return any(map(lambda l: l.startswith(expected), lines)) @@ -115,7 +115,7 @@ def list_matches_wheel_abspath(wheel_name, result): E.g., If wheel_name is `foo-1.2.3` it searches for a line starting with `foo-1.2.3-py3-none-any.whl`.""" lines = result.stdout.splitlines() - expected = '{}-py3-none-any.whl'.format(wheel_name) + expected = f'{wheel_name}-py3-none-any.whl' return any(map(lambda l: os.path.basename(l).startswith(expected) and os.path.exists(l), lines)) @@ -137,7 +137,7 @@ def _remove_matches_http(http_filename, result): path = os.path.join( http_cache_dir, 'arbitrary', 'pathname', http_filename, ) - expected = 'Removed {}'.format(path) + expected = f'Removed {path}' return expected in lines return _remove_matches_http @@ -155,14 +155,14 @@ def remove_matches_wheel(wheel_cache_dir): def _remove_matches_wheel(wheel_name, result): lines = result.stdout.splitlines() - wheel_filename = '{}-py3-none-any.whl'.format(wheel_name) + wheel_filename = f'{wheel_name}-py3-none-any.whl' # The "/arbitrary/pathname/" bit is an implementation detail of how # the `populate_wheel_cache` fixture is implemented. path = os.path.join( wheel_cache_dir, 'arbitrary', 'pathname', wheel_filename, ) - expected = 'Removed {}'.format(path) + expected = f'Removed {path}' return expected in lines return _remove_matches_wheel @@ -191,12 +191,12 @@ def test_cache_info( result = script.pip('cache', 'info') assert ( - 'Package index page cache location: {}'.format(http_cache_dir) + f'Package index page cache location: {http_cache_dir}' in result.stdout ) - assert 'Wheels location: {}'.format(wheel_cache_dir) in result.stdout + assert f'Wheels location: {wheel_cache_dir}' in result.stdout num_wheels = len(wheel_cache_files) - assert 'Number of wheels: {}'.format(num_wheels) in result.stdout + assert f'Number of wheels: {num_wheels}' in result.stdout @pytest.mark.usefixtures("populate_wheel_cache") diff --git a/tests/functional/test_configuration.py b/tests/functional/test_configuration.py index f820bdc19a5..72c09bd3632 100644 --- a/tests/functional/test_configuration.py +++ b/tests/functional/test_configuration.py @@ -98,7 +98,7 @@ def test_env_values(self, script): """)) result = script.pip("config", "debug") - assert "{}, exists: True".format(config_file) in result.stdout + assert f"{config_file}, exists: True" in result.stdout assert "global.timeout: 60" in result.stdout assert "freeze.timeout: 10" in result.stdout assert re.search(r"env:\n( .+\n)+", result.stdout) @@ -117,7 +117,7 @@ def test_user_values(self, script,): script.pip("config", "--user", "set", "freeze.timeout", "10") result = script.pip("config", "debug") - assert "{}, exists: True".format(new_config_file) in result.stdout + assert f"{new_config_file}, exists: True" in result.stdout assert "global.timeout: 60" in result.stdout assert "freeze.timeout: 10" in result.stdout assert re.search(r"user:\n( .+\n)+", result.stdout) @@ -134,7 +134,7 @@ def test_site_values(self, script, virtualenv): script.pip("config", "--site", "set", "freeze.timeout", "10") result = script.pip("config", "debug") - assert "{}, exists: True".format(site_config_file) in result.stdout + assert f"{site_config_file}, exists: True" in result.stdout assert "global.timeout: 60" in result.stdout assert "freeze.timeout: 10" in result.stdout assert re.search(r"site:\n( .+\n)+", result.stdout) @@ -149,4 +149,4 @@ def test_global_config_file(self, script): # So we just check if the file can be identified global_config_file = get_configuration_files()[kinds.GLOBAL][0] result = script.pip("config", "debug") - assert "{}, exists:".format(global_config_file) in result.stdout + assert f"{global_config_file}, exists:" in result.stdout diff --git a/tests/functional/test_debug.py b/tests/functional/test_debug.py index f309604df58..0e2261e1ae0 100644 --- a/tests/functional/test_debug.py +++ b/tests/functional/test_debug.py @@ -40,7 +40,7 @@ def test_debug__library_versions(script): vendored_versions = create_vendor_txt_map() for name, value in vendored_versions.items(): - assert '{}=={}'.format(name, value) in result.stdout + assert f'{name}=={value}' in result.stdout @pytest.mark.parametrize( diff --git a/tests/functional/test_download.py b/tests/functional/test_download.py index 72b55fda92f..bae8a56aacf 100644 --- a/tests/functional/test_download.py +++ b/tests/functional/test_download.py @@ -364,7 +364,7 @@ def test_download_compatible_manylinuxes( """ Earlier manylinuxes are compatible with later manylinuxes. """ - wheel = 'fake-1.0-py2.py3-none-{}.whl'.format(wheel_abi) + wheel = f'fake-1.0-py2.py3-none-{wheel_abi}.whl' fake_wheel(data, wheel) result = script.pip( 'download', '--no-index', '--find-links', data.find_links, @@ -491,7 +491,7 @@ def make_wheel_with_python_requires(script, package_name, python_requires): 'python', 'setup.py', 'bdist_wheel', '--universal', cwd=package_dir, ) - file_name = '{}-1.0-py2.py3-none-any.whl'.format(package_name) + file_name = f'{package_name}-1.0-py2.py3-none-any.whl' return package_dir / 'dist' / file_name @@ -521,7 +521,7 @@ def make_args(python_version): "ERROR: Package 'mypackage' requires a different Python: " "3.3.0 not in '==3.2'" ) - assert expected_err in result.stderr, 'stderr: {}'.format(result.stderr) + assert expected_err in result.stderr, f'stderr: {result.stderr}' # Now try with a --python-version that satisfies the Requires-Python. args = make_args('32') @@ -863,8 +863,8 @@ def test_download_http_url_bad_hash( file_response(simple_pkg) ]) mock_server.start() - base_address = 'http://{}:{}'.format(mock_server.host, mock_server.port) - url = "{}/simple-1.0.tar.gz#sha256={}".format(base_address, digest) + base_address = f'http://{mock_server.host}:{mock_server.port}' + url = f"{base_address}/simple-1.0.tar.gz#sha256={digest}" shared_script.pip('download', '-d', str(download_dir), url) diff --git a/tests/functional/test_freeze.py b/tests/functional/test_freeze.py index f0a2265f3a0..5d3d4968619 100644 --- a/tests/functional/test_freeze.py +++ b/tests/functional/test_freeze.py @@ -495,7 +495,7 @@ def test_freeze_bazaar_clone(script, tmpdir): try: checkout_path = _create_test_package(script, vcs='bazaar') except OSError as e: - pytest.fail('Invoking `bzr` failed: {e}'.format(e=e)) + pytest.fail(f'Invoking `bzr` failed: {e}') result = script.run( 'bzr', 'checkout', checkout_path, 'bzr-package' @@ -552,7 +552,7 @@ def test_freeze_nested_vcs(script, outer_vcs, inner_vcs): result = script.pip("freeze", expect_stderr=True) _check_output( result.stdout, - "...-e {}+...#egg=version_pkg\n...".format(inner_vcs), + f"...-e {inner_vcs}+...#egg=version_pkg\n...", ) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index aedd691a4e3..9c36fef0ecb 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -530,7 +530,7 @@ def test_hashed_install_failure(script, tmpdir): def assert_re_match(pattern, text): assert re.search(pattern, text), ( - "Could not find {!r} in {!r}".format(pattern, text) + f"Could not find {pattern!r} in {text!r}" ) @@ -1023,7 +1023,7 @@ def test_install_package_with_prefix(script, data): install_path = ( distutils.sysconfig.get_python_lib(prefix=rel_prefix_path) / # we still test for egg-info because no-binary implies setup.py install - 'simple-1.0-py{}.egg-info'.format(pyversion) + f'simple-1.0-py{pyversion}.egg-info' ) result.did_create(install_path) @@ -1040,7 +1040,7 @@ def test_install_editable_with_prefix(script): if hasattr(sys, "pypy_version_info"): site_packages = os.path.join( - 'prefix', 'lib', 'python{}'.format(pyversion), 'site-packages') + 'prefix', 'lib', f'python{pyversion}', 'site-packages') else: site_packages = distutils.sysconfig.get_python_lib(prefix='prefix') @@ -1086,7 +1086,7 @@ def test_install_package_that_emits_unicode(script, data): ) assert ( 'FakeError: this package designed to fail on install' in result.stderr - ), 'stderr: {}'.format(result.stderr) + ), f'stderr: {result.stderr}' assert 'UnicodeDecodeError' not in result.stderr assert 'UnicodeDecodeError' not in result.stdout @@ -1838,7 +1838,7 @@ def test_install_sends_client_cert(install_args, script, cert_factory, data): file_response(str(data.packages / "simple-3.0.tar.gz")), ] - url = "https://{}:{}/simple".format(server.host, server.port) + url = f"https://{server.host}:{server.port}/simple" args = ["install", "-vvv", "--cert", cert_path, "--client-cert", cert_path] args.extend(["--index-url", url]) diff --git a/tests/functional/test_install_cleanup.py b/tests/functional/test_install_cleanup.py index 10e49124960..7b64ed4b5e9 100644 --- a/tests/functional/test_install_cleanup.py +++ b/tests/functional/test_install_cleanup.py @@ -14,7 +14,7 @@ def test_no_clean_option_blocks_cleaning_after_install(script, data): build = script.base_path / 'pip-build' script.pip( 'install', '--no-clean', '--no-index', '--build', build, - '--find-links={}'.format(data.find_links), 'simple', + f'--find-links={data.find_links}', 'simple', expect_temp=True, # TODO: allow_stderr_warning is used for the --build deprecation, # remove it when removing support for --build diff --git a/tests/functional/test_install_config.py b/tests/functional/test_install_config.py index dcc9c66d5a4..41be6fbbbb6 100644 --- a/tests/functional/test_install_config.py +++ b/tests/functional/test_install_config.py @@ -100,9 +100,9 @@ def test_command_line_append_flags(script, virtualenv, data): in result.stdout ) assert ( - 'Skipping link: not a file: {}'.format(data.find_links) in + f'Skipping link: not a file: {data.find_links}' in result.stdout - ), 'stdout: {}'.format(result.stdout) + ), f'stdout: {result.stdout}' @pytest.mark.network @@ -124,9 +124,9 @@ def test_command_line_appends_correctly(script, data): in result.stdout ), result.stdout assert ( - 'Skipping link: not a file: {}'.format(data.find_links) in + f'Skipping link: not a file: {data.find_links}' in result.stdout - ), 'stdout: {}'.format(result.stdout) + ), f'stdout: {result.stdout}' def test_config_file_override_stack( @@ -143,7 +143,7 @@ def test_config_file_override_stack( file_response(shared_data.packages.joinpath("INITools-0.2.tar.gz")), ]) mock_server.start() - base_address = "http://{}:{}".format(mock_server.host, mock_server.port) + base_address = f"http://{mock_server.host}:{mock_server.port}" config_file = script.scratch_path / "test-pip.cfg" @@ -166,7 +166,7 @@ def test_config_file_override_stack( ) script.pip('install', '-vvv', 'INITools', expect_error=True) script.pip( - 'install', '-vvv', '--index-url', "{}/simple3".format(base_address), + 'install', '-vvv', '--index-url', f"{base_address}/simple3", 'INITools', ) @@ -236,14 +236,14 @@ def test_prompt_for_authentication(script, data, cert_factory): authorization_response(str(data.packages / "simple-3.0.tar.gz")), ] - url = "https://{}:{}/simple".format(server.host, server.port) + url = f"https://{server.host}:{server.port}/simple" with server_running(server): result = script.pip('install', "--index-url", url, "--cert", cert_path, "--client-cert", cert_path, 'simple', expect_error=True) - assert 'User for {}:{}'.format(server.host, server.port) in \ + assert f'User for {server.host}:{server.port}' in \ result.stdout, str(result) @@ -266,7 +266,7 @@ def test_do_not_prompt_for_authentication(script, data, cert_factory): authorization_response(str(data.packages / "simple-3.0.tar.gz")), ] - url = "https://{}:{}/simple".format(server.host, server.port) + url = f"https://{server.host}:{server.port}/simple" with server_running(server): result = script.pip('install', "--index-url", url, diff --git a/tests/functional/test_install_direct_url.py b/tests/functional/test_install_direct_url.py index ec1e927ebf8..23273774d16 100644 --- a/tests/functional/test_install_direct_url.py +++ b/tests/functional/test_install_direct_url.py @@ -33,7 +33,7 @@ def test_install_vcs_editable_no_direct_url(script, with_wheel): def test_install_vcs_non_editable_direct_url(script, with_wheel): pkg_path = _create_test_package(script, name="testpkg") url = path_to_url(pkg_path) - args = ["install", "git+{}#egg=testpkg".format(url)] + args = ["install", f"git+{url}#egg=testpkg"] result = script.pip(*args) direct_url = _get_created_direct_url(result, "testpkg") assert direct_url diff --git a/tests/functional/test_install_extras.py b/tests/functional/test_install_extras.py index d70067b6bca..0ec42940630 100644 --- a/tests/functional/test_install_extras.py +++ b/tests/functional/test_install_extras.py @@ -171,5 +171,4 @@ def test_install_extra_merging(script, data, extra_to_install, simple_version): '{pkga_path}{extra_to_install}'.format(**locals()), ) - assert ('Successfully installed pkga-0.1 simple-{}'.format(simple_version) - ) in result.stdout + assert f'Successfully installed pkga-0.1 simple-{simple_version}' in result.stdout diff --git a/tests/functional/test_install_force_reinstall.py b/tests/functional/test_install_force_reinstall.py index 0fbdeb276c4..265c52b20db 100644 --- a/tests/functional/test_install_force_reinstall.py +++ b/tests/functional/test_install_force_reinstall.py @@ -11,7 +11,7 @@ def check_installed_version(script, package, expected): if line.startswith('Version: '): version = line.split()[-1] break - assert version == expected, 'version {} != {}'.format(version, expected) + assert version == expected, f'version {version} != {expected}' def check_force_reinstall(script, specifier, expected): diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index c5985243b60..c76b71e27c9 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -55,7 +55,7 @@ def _arg_recording_sdist_maker(name): sdist_path = create_basic_sdist_for_package( script, name, "0.1.0", extra_files ) - args_path = output_dir / "{}.json".format(name) + args_path = output_dir / f"{name}.json" return ArgRecordingSdist(sdist_path, args_path) return _arg_recording_sdist_maker diff --git a/tests/functional/test_install_upgrade.py b/tests/functional/test_install_upgrade.py index 923a594c623..1b0ecfc078f 100644 --- a/tests/functional/test_install_upgrade.py +++ b/tests/functional/test_install_upgrade.py @@ -470,5 +470,5 @@ def test_install_find_existing_package_canonicalize(script, req1, req2): result = script.pip( "install", "--no-index", "--find-links", pkg_container, "pkg", ) - satisfied_message = "Requirement already satisfied: {}".format(req2) + satisfied_message = f"Requirement already satisfied: {req2}" assert satisfied_message in result.stdout, str(result) diff --git a/tests/functional/test_install_vcs_git.py b/tests/functional/test_install_vcs_git.py index 59393d34747..4c26d8e88ac 100644 --- a/tests/functional/test_install_vcs_git.py +++ b/tests/functional/test_install_vcs_git.py @@ -38,7 +38,7 @@ def _get_branch_remote(script, package_name, branch): """ repo_dir = _get_editable_repo_dir(script, package_name) result = script.run( - 'git', 'config', 'branch.{}.remote'.format(branch), cwd=repo_dir + 'git', 'config', f'branch.{branch}.remote', cwd=repo_dir ) return result.stdout.strip() @@ -57,12 +57,12 @@ def _github_checkout(url_path, temp_dir, rev=None, egg=None, scheme=None): """ if scheme is None: scheme = 'https' - url = 'git+{}://github.com/{}'.format(scheme, url_path) + url = f'git+{scheme}://github.com/{url_path}' local_url = local_checkout(url, temp_dir) if rev is not None: - local_url += '@{}'.format(rev) + local_url += f'@{rev}' if egg is not None: - local_url += '#egg={}'.format(egg) + local_url += f'#egg={egg}' return local_url @@ -77,8 +77,8 @@ def _make_version_pkg_url(path, rev=None, name="version_pkg"): rev: an optional revision to install like a branch name, tag, or SHA. """ file_url = _test_path_to_file_url(path) - url_rev = '' if rev is None else '@{}'.format(rev) - url = 'git+{}{}#egg={}'.format(file_url, url_rev, name) + url_rev = '' if rev is None else f'@{rev}' + url = f'git+{file_url}{url_rev}#egg={name}' return url @@ -278,11 +278,11 @@ def test_git_with_tag_name_and_update(script, tmpdir): url_path = 'pypa/pip-test-package.git' base_local_url = _github_checkout(url_path, tmpdir) - local_url = '{}#egg=pip-test-package'.format(base_local_url) + local_url = f'{base_local_url}#egg=pip-test-package' result = script.pip('install', '-e', local_url) result.assert_installed('pip-test-package', with_files=['.git']) - new_local_url = '{}@0.1.2#egg=pip-test-package'.format(base_local_url) + new_local_url = f'{base_local_url}@0.1.2#egg=pip-test-package' result = script.pip( 'install', '--global-option=--version', '-e', new_local_url, ) @@ -484,12 +484,12 @@ def test_install_git_branch_not_cached(script, with_wheel): repo_dir = _create_test_package(script, name=PKG) url = _make_version_pkg_url(repo_dir, rev="master", name=PKG) result = script.pip("install", url, "--only-binary=:all:") - assert "Successfully built {}".format(PKG) in result.stdout, result.stdout + assert f"Successfully built {PKG}" in result.stdout, result.stdout script.pip("uninstall", "-y", PKG) # build occurs on the second install too because it is not cached result = script.pip("install", url) assert ( - "Successfully built {}".format(PKG) in result.stdout + f"Successfully built {PKG}" in result.stdout ), result.stdout @@ -504,10 +504,10 @@ def test_install_git_sha_cached(script, with_wheel): ).stdout.strip() url = _make_version_pkg_url(repo_dir, rev=commit, name=PKG) result = script.pip("install", url) - assert "Successfully built {}".format(PKG) in result.stdout, result.stdout + assert f"Successfully built {PKG}" in result.stdout, result.stdout script.pip("uninstall", "-y", PKG) # build does not occur on the second install because it is cached result = script.pip("install", url) assert ( - "Successfully built {}".format(PKG) not in result.stdout + f"Successfully built {PKG}" not in result.stdout ), result.stdout diff --git a/tests/functional/test_install_wheel.py b/tests/functional/test_install_wheel.py index 9bf965625ff..8df208bb7da 100644 --- a/tests/functional/test_install_wheel.py +++ b/tests/functional/test_install_wheel.py @@ -14,7 +14,7 @@ # assert_installed expects a package subdirectory, so give it to them def make_wheel_with_file(name, version, **kwargs): extra_files = kwargs.setdefault("extra_files", {}) - extra_files["{}/__init__.py".format(name)] = "# example" + extra_files[f"{name}/__init__.py"] = "# example" return make_wheel(name=name, version=version, **kwargs) @@ -691,7 +691,7 @@ def test_wheel_with_file_in_data_dir_has_reasonable_error( result = script.pip( "install", "--no-index", str(wheel_path), expect_error=True ) - assert "simple-0.1.0.data/{}".format(name) in result.stderr + assert f"simple-0.1.0.data/{name}" in result.stderr def test_wheel_with_unknown_subdir_in_data_dir_has_reasonable_error( diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index b730b3cbdf9..5de2f56e8d6 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -21,8 +21,7 @@ def assert_installed(script, **kwargs): for val in json.loads(ret.stdout) ) expected = set((canonicalize_name(k), v) for k, v in kwargs.items()) - assert expected <= installed, \ - "{!r} not all in {!r}".format(expected, installed) + assert expected <= installed, f"{expected!r} not all in {installed!r}" def assert_not_installed(script, *args): @@ -34,17 +33,16 @@ def assert_not_installed(script, *args): # None of the given names should be listed as installed, i.e. their # intersection should be empty. expected = set(canonicalize_name(k) for k in args) - assert not (expected & installed), \ - "{!r} contained in {!r}".format(expected, installed) + assert not (expected & installed), f"{expected!r} contained in {installed!r}" def assert_editable(script, *args): # This simply checks whether all of the listed packages have a # corresponding .egg-link file installed. # TODO: Implement a more rigorous way to test for editable installations. - egg_links = set("{}.egg-link".format(arg) for arg in args) + egg_links = set(f"{arg}.egg-link" for arg in args) assert egg_links <= set(os.listdir(script.site_packages_path)), \ - "{!r} not all found in {!r}".format(args, script.site_packages_path) + f"{args!r} not all found in {script.site_packages_path!r}" def test_new_resolver_can_install(script): @@ -732,7 +730,7 @@ def test_new_resolver_constraint_on_path_empty( setup_py.write_text(text) constraints_txt = script.scratch_path / "constraints.txt" - constraints_txt.write_text("foo=={}".format(constraint_version)) + constraints_txt.write_text(f"foo=={constraint_version}") result = script.pip( "install", @@ -1067,8 +1065,8 @@ def test_new_resolver_prefers_installed_in_upgrade_if_latest(script): def test_new_resolver_presents_messages_when_backtracking_a_lot(script, N): # Generate a set of wheels that will definitely cause backtracking. for index in range(1, N+1): - A_version = "{index}.0.0".format(index=index) - B_version = "{index}.0.0".format(index=index) + A_version = f"{index}.0.0" + B_version = f"{index}.0.0" C_version = "{index_minus_one}.0.0".format(index_minus_one=index - 1) depends = ["B == " + B_version] @@ -1079,15 +1077,15 @@ def test_new_resolver_presents_messages_when_backtracking_a_lot(script, N): create_basic_wheel_for_package(script, "A", A_version, depends=depends) for index in range(1, N+1): - B_version = "{index}.0.0".format(index=index) - C_version = "{index}.0.0".format(index=index) + B_version = f"{index}.0.0" + C_version = f"{index}.0.0" depends = ["C == " + C_version] print("B", B_version, "C", C_version) create_basic_wheel_for_package(script, "B", B_version, depends=depends) for index in range(1, N+1): - C_version = "{index}.0.0".format(index=index) + C_version = f"{index}.0.0" print("C", C_version) create_basic_wheel_for_package(script, "C", C_version) @@ -1138,7 +1136,7 @@ def test_new_resolver_check_wheel_version_normalized( metadata_version, filename_version, ): - filename = "simple-{}-py2.py3-none-any.whl".format(filename_version) + filename = f"simple-{filename_version}-py2.py3-none-any.whl" wheel_builder = make_wheel(name="simple", version=metadata_version) wheel_builder.save_to(script.scratch_path / filename) diff --git a/tests/functional/test_new_resolver_target.py b/tests/functional/test_new_resolver_target.py index 037244a2c4b..f5ec6ac7a09 100644 --- a/tests/functional/test_new_resolver_target.py +++ b/tests/functional/test_new_resolver_target.py @@ -16,7 +16,7 @@ def _make_fake_wheel(wheel_tag): version="1.0", wheel_metadata_updates={"Tag": []}, ) - wheel_path = wheel_house.joinpath("fake-1.0-{}.whl".format(wheel_tag)) + wheel_path = wheel_house.joinpath(f"fake-1.0-{wheel_tag}.whl") wheel_builder.save_to(wheel_path) return wheel_path diff --git a/tests/functional/test_show.py b/tests/functional/test_show.py index c19228b566c..7047aa63aa8 100644 --- a/tests/functional/test_show.py +++ b/tests/functional/test_show.py @@ -16,7 +16,7 @@ def test_basic_show(script): lines = result.stdout.splitlines() assert len(lines) == 10 assert 'Name: pip' in lines - assert 'Version: {}'.format(__version__) in lines + assert f'Version: {__version__}' in lines assert any(line.startswith('Location: ') for line in lines) assert 'Requires: ' in lines diff --git a/tests/functional/test_wheel.py b/tests/functional/test_wheel.py index 286d694356e..14bc351a05f 100644 --- a/tests/functional/test_wheel.py +++ b/tests/functional/test_wheel.py @@ -227,7 +227,7 @@ def test_no_clean_option_blocks_cleaning_after_wheel( if resolver_variant == "legacy": build = build / 'simple' - message = "build/simple should still exist {}".format(result) + message = f"build/simple should still exist {result}" assert exists(build), message diff --git a/tests/functional/test_yaml.py b/tests/functional/test_yaml.py index 4b5f38f97ec..ba7b17531ef 100644 --- a/tests/functional/test_yaml.py +++ b/tests/functional/test_yaml.py @@ -77,7 +77,7 @@ def stripping_split(my_str, splitwith, count=None): for part in parts[1:]: verb, args_str = stripping_split(part, " ", 1) - assert verb in ["depends"], "Unknown verb {!r}".format(verb) + assert verb in ["depends"], f"Unknown verb {verb!r}" retval[verb] = stripping_split(args_str, ",") @@ -94,14 +94,14 @@ def handle_request(script, action, requirement, options, resolver_variant): elif action == 'uninstall': args = ['uninstall', '--yes'] else: - raise "Did not excpet action: {!r}".format(action) + raise f"Did not excpet action: {action!r}" if isinstance(requirement, str): args.append(requirement) elif isinstance(requirement, list): args.extend(requirement) else: - raise "requirement neither str nor list {!r}".format(requirement) + raise f"requirement neither str nor list {requirement!r}" args.extend(options) args.append("--verbose") @@ -177,7 +177,7 @@ def test_yaml_based(script, case): if action in request: break else: - raise "Unsupported request {!r}".format(request) + raise f"Unsupported request {request!r}" # Perform the requested action effect = handle_request(script, action, diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index 954145b0a06..411701de5f5 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -373,7 +373,7 @@ def _one_or_both(a, b): if not a: return str(b) - return "{a}\n{b}".format(a=a, b=b) + return f"{a}\n{b}" def make_check_stderr_message(stderr, line, reason): @@ -748,7 +748,7 @@ def _create_main_file(dir_path, name=None, output=None): def main(): print({!r}) """.format(output)) - filename = '{}.py'.format(name) + filename = f'{name}.py' dir_path.joinpath(filename).write_text(text) @@ -983,14 +983,14 @@ def add_file(path, text): z.writestr(path, contents) records.append((path, digest(contents), str(len(contents)))) - dist_info = "{}-{}.dist-info".format(name, version) - record_path = "{}/RECORD".format(dist_info) + dist_info = f"{name}-{version}.dist-info" + record_path = f"{dist_info}/RECORD" records = [(record_path, "", "")] buf = BytesIO() with ZipFile(buf, "w") as z: - add_file("{}/WHEEL".format(dist_info), "Wheel-Version: 1.0") + add_file(f"{dist_info}/WHEEL", "Wheel-Version: 1.0") add_file( - "{}/METADATA".format(dist_info), + f"{dist_info}/METADATA", dedent( """\ Metadata-Version: 2.1 @@ -1023,10 +1023,10 @@ def create_basic_wheel_for_package( # Fix wheel distribution name by replacing runs of non-alphanumeric # characters with an underscore _ as per PEP 491 name = re.sub(r"[^\w\d.]+", "_", name, re.UNICODE) - archive_name = "{}-{}-py2.py3-none-any.whl".format(name, version) + archive_name = f"{name}-{version}-py2.py3-none-any.whl" archive_path = script.scratch_path / archive_name - package_init_py = "{name}/__init__.py".format(name=name) + package_init_py = f"{name}/__init__.py" assert package_init_py not in extra_files extra_files[package_init_py] = textwrap.dedent( """ @@ -1037,7 +1037,7 @@ def hello(): ).format(version=version, name=name) requires_dist = depends + [ - '{package}; extra == "{extra}"'.format(package=package, extra=extra) + f'{package}; extra == "{extra}"' for extra, packages in extras.items() for package in packages ] @@ -1118,7 +1118,7 @@ def wrapper(fn): subprocess.check_output(check_cmd) except (OSError, subprocess.CalledProcessError): return pytest.mark.skip( - reason='{name} is not available'.format(name=name))(fn) + reason=f'{name} is not available')(fn) return fn return wrapper diff --git a/tests/lib/index.py b/tests/lib/index.py index 0f507a0e7ff..e6dc2a58bea 100644 --- a/tests/lib/index.py +++ b/tests/lib/index.py @@ -3,10 +3,10 @@ def make_mock_candidate(version, yanked_reason=None, hex_digest=None): - url = 'https://example.com/pkg-{}.tar.gz'.format(version) + url = f'https://example.com/pkg-{version}.tar.gz' if hex_digest is not None: assert len(hex_digest) == 64 - url += '#sha256={}'.format(hex_digest) + url += f'#sha256={hex_digest}' link = Link(url, yanked_reason=yanked_reason) candidate = InstallationCandidate('mypackage', version, link) diff --git a/tests/lib/test_lib.py b/tests/lib/test_lib.py index 655e0bdeea9..47b97724f23 100644 --- a/tests/lib/test_lib.py +++ b/tests/lib/test_lib.py @@ -19,7 +19,7 @@ def assert_error_startswith(exc_type, expected_start): yield assert str(err.value).startswith(expected_start), ( - 'full message: {}'.format(err.value) + f'full message: {err.value}' ) @@ -82,8 +82,8 @@ def run_stderr_with_prefix(self, script, prefix, **kwargs): """ Call run() that prints stderr with the given prefix. """ - text = '{}: hello, world\\n'.format(prefix) - command = 'import sys; sys.stderr.write("{}")'.format(text) + text = f'{prefix}: hello, world\\n' + command = f'import sys; sys.stderr.write("{text}")' args = [sys.executable, '-c', command] script.run(*args, **kwargs) diff --git a/tests/lib/venv.py b/tests/lib/venv.py index c5652fecf42..40edfa2209e 100644 --- a/tests/lib/venv.py +++ b/tests/lib/venv.py @@ -38,7 +38,7 @@ def _update_paths(self): self.lib = Path(lib) def __repr__(self): - return "<VirtualEnvironment {}>".format(self.location) + return f"<VirtualEnvironment {self.location}>" def _create(self, clear=False): if clear: diff --git a/tests/lib/wheel.py b/tests/lib/wheel.py index 2121a175caf..34fa27d18d3 100644 --- a/tests/lib/wheel.py +++ b/tests/lib/wheel.py @@ -79,7 +79,7 @@ def message_from_dict(headers): def dist_info_path(name, version, path): # type: (str, str, str) -> str - return "{}-{}.dist-info/{}".format(name, version, path) + return f"{name}-{version}.dist-info/{path}" def make_metadata_file( @@ -162,7 +162,7 @@ def make_entry_points_file( lines = [] for section, values in entry_points_data.items(): - lines.append("[{}]".format(section)) + lines.append(f"[{section}]") lines.extend(values) return File( @@ -190,9 +190,9 @@ def make_metadata_files(name, version, files): def make_data_files(name, version, files): # type: (str, str, Dict[str, AnyStr]) -> List[File] - data_dir = "{}-{}.data".format(name, version) + data_dir = f"{name}-{version}.data" return [ - File("{}/{}".format(data_dir, name), ensure_binary(contents)) + File(f"{data_dir}/{name}", ensure_binary(contents)) for name, contents in files.items() ] @@ -258,7 +258,7 @@ def wheel_name(name, version, pythons, abis, platforms): ".".join(abis), ".".join(platforms), ]) - return "{}.whl".format(stem) + return f"{stem}.whl" class WheelBuilder(object): diff --git a/tests/unit/test_cmdoptions.py b/tests/unit/test_cmdoptions.py index 150570e716e..bac33ce77b1 100644 --- a/tests/unit/test_cmdoptions.py +++ b/tests/unit/test_cmdoptions.py @@ -21,4 +21,4 @@ ]) def test_convert_python_version(value, expected): actual = _convert_python_version(value) - assert actual == expected, 'actual: {!r}'.format(actual) + assert actual == expected, f'actual: {actual!r}' diff --git a/tests/unit/test_collector.py b/tests/unit/test_collector.py index 294ea721a3a..cca99d000ae 100644 --- a/tests/unit/test_collector.py +++ b/tests/unit/test_collector.py @@ -398,7 +398,7 @@ def test_parse_links__yanked_reason(anchor_html, expected): encoding=None, # parse_links() is cached by url, so we inject a random uuid to ensure # the page content isn't cached. - url='https://example.com/simple-{}/'.format(uuid.uuid4()), + url=f'https://example.com/simple-{uuid.uuid4()}/', ) links = list(parse_links(page)) link, = links @@ -580,7 +580,7 @@ def test_get_html_page_directory_append_index(tmpdir): actual = _get_html_page(Link(dir_url), session=session) assert mock_func.mock_calls == [ mock.call(expected_url, session=session), - ], 'actual calls: {}'.format(mock_func.mock_calls) + ], f'actual calls: {mock_func.mock_calls}' assert actual.content == fake_response.content assert actual.encoding is None @@ -636,7 +636,7 @@ def check_links_include(links, names): """ for name in names: assert any(link.url.endswith(name) for link in links), ( - 'name {!r} not among links: {}'.format(name, links) + f'name {name!r} not among links: {links}' ) diff --git a/tests/unit/test_commands.py b/tests/unit/test_commands.py index 7fae427c697..59cbf930ffb 100644 --- a/tests/unit/test_commands.py +++ b/tests/unit/test_commands.py @@ -19,7 +19,7 @@ def check_commands(pred, expected): """ commands = [create_command(name) for name in sorted(commands_dict)] actual = [command.name for command in commands if pred(command)] - assert actual == expected, 'actual: {}'.format(actual) + assert actual == expected, f'actual: {actual}' def test_commands_dict__order(): diff --git a/tests/unit/test_compat.py b/tests/unit/test_compat.py index cc024b570a7..c1dee67d29f 100644 --- a/tests/unit/test_compat.py +++ b/tests/unit/test_compat.py @@ -60,7 +60,7 @@ def test_str_to_display(data, expected): actual = str_to_display(data) assert actual == expected, ( # Show the encoding for easier troubleshooting. - 'encoding: {!r}'.format(locale.getpreferredencoding()) + f'encoding: {locale.getpreferredencoding()!r}' ) @@ -79,7 +79,7 @@ def test_str_to_display__encoding(monkeypatch, data, encoding, expected): actual = str_to_display(data) assert actual == expected, ( # Show the encoding for easier troubleshooting. - 'encoding: {!r}'.format(locale.getpreferredencoding()) + f'encoding: {locale.getpreferredencoding()!r}' ) @@ -96,7 +96,7 @@ def test_str_to_display__decode_error(monkeypatch, caplog): assert actual == expected, ( # Show the encoding for easier troubleshooting. - 'encoding: {!r}'.format(locale.getpreferredencoding()) + f'encoding: {locale.getpreferredencoding()!r}' ) assert len(caplog.records) == 1 record = caplog.records[0] diff --git a/tests/unit/test_finder.py b/tests/unit/test_finder.py index 55fdab3b888..594ffc2c93c 100644 --- a/tests/unit/test_finder.py +++ b/tests/unit/test_finder.py @@ -474,7 +474,7 @@ def test_evaluate_link__substring_fails(self, url, expected_msg): def test_process_project_url(data): project_name = 'simple' index_url = data.index_url('simple') - project_url = Link('{}/{}'.format(index_url, project_name)) + project_url = Link(f'{index_url}/{project_name}') finder = make_test_finder(index_urls=[index_url]) link_evaluator = finder.make_link_evaluator(project_name) actual = finder.process_project_url( diff --git a/tests/unit/test_index.py b/tests/unit/test_index.py index e719707ab78..b6f7f632cc0 100644 --- a/tests/unit/test_index.py +++ b/tests/unit/test_index.py @@ -763,7 +763,7 @@ def test_find_name_version_sep(fragment, canonical_name, expected): def test_find_name_version_sep_failure(fragment, canonical_name): with pytest.raises(ValueError) as ctx: _find_name_version_sep(fragment, canonical_name) - message = "{} does not match {}".format(fragment, canonical_name) + message = f"{fragment} does not match {canonical_name}" assert str(ctx.value) == message diff --git a/tests/unit/test_network_lazy_wheel.py b/tests/unit/test_network_lazy_wheel.py index cf0e6213d3f..e6747a18e6a 100644 --- a/tests/unit/test_network_lazy_wheel.py +++ b/tests/unit/test_network_lazy_wheel.py @@ -33,7 +33,7 @@ def mypy_whl_no_range(mock_server, shared_data): mypy_whl = shared_data.packages / 'mypy-0.782-py3-none-any.whl' mock_server.set_responses([file_response(mypy_whl)]) mock_server.start() - base_address = 'http://{}:{}'.format(mock_server.host, mock_server.port) + base_address = f'http://{mock_server.host}:{mock_server.port}' yield "{}/{}".format(base_address, 'mypy-0.782-py3-none-any.whl') mock_server.stop() diff --git a/tests/unit/test_network_session.py b/tests/unit/test_network_session.py index a0d1463b2cf..3cf6a0ee9f8 100644 --- a/tests/unit/test_network_session.py +++ b/tests/unit/test_network_session.py @@ -13,7 +13,7 @@ def get_user_agent(): def test_user_agent(): user_agent = get_user_agent() - assert user_agent.startswith("pip/{}".format(__version__)) + assert user_agent.startswith(f"pip/{__version__}") @pytest.mark.parametrize('name, expected_like_ci', [ @@ -115,7 +115,7 @@ def test_add_trusted_host(self): session.add_trusted_host('host3') assert session.pip_trusted_origins == [ ('host1', None), ('host3', None), ('host2', None) - ], 'actual: {}'.format(session.pip_trusted_origins) + ], f'actual: {session.pip_trusted_origins}' session.add_trusted_host('host4:8080') prefix4 = 'https://host4:8080/' diff --git a/tests/unit/test_operations_prepare.py b/tests/unit/test_operations_prepare.py index af3ce72a1e0..c31d45b58cc 100644 --- a/tests/unit/test_operations_prepare.py +++ b/tests/unit/test_operations_prepare.py @@ -189,7 +189,7 @@ def test_unpack_url_bad_hash(self, tmpdir, data, Test when the file url hash fragment is wrong """ self.prep(tmpdir, data) - url = '{}#md5=bogus'.format(self.dist_url.url) + url = f'{self.dist_url.url}#md5=bogus' dist_url = Link(url) with pytest.raises(HashMismatch): unpack_url(dist_url, diff --git a/tests/unit/test_options.py b/tests/unit/test_options.py index 533a4b8db3d..5ec1236bd2f 100644 --- a/tests/unit/test_options.py +++ b/tests/unit/test_options.py @@ -262,7 +262,7 @@ def test_subcommand_option_before_subcommand_fails(self): @contextmanager def tmpconfig(option, value, section='global'): with NamedTemporaryFile(mode='w', delete=False) as f: - f.write('[{}]\n{}={}\n'.format(section, option, value)) + f.write(f'[{section}]\n{option}={value}\n') name = f.name try: yield name @@ -275,7 +275,7 @@ class TestCountOptions(AddFakeCommandMixin): @pytest.mark.parametrize('option', ('verbose', 'quiet')) @pytest.mark.parametrize('value', range(4)) def test_cli_long(self, option, value): - flags = ['--{}'.format(option)] * value + flags = [f'--{option}'] * value opt1, args1 = main(flags+['fake']) opt2, args2 = main(['fake']+flags) assert getattr(opt1, option) == getattr(opt2, option) == value diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index e168a3cc164..143b1b54468 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -454,14 +454,14 @@ def test_markers_semicolon(self): def test_markers_url(self): # test "URL; markers" syntax url = 'http://foo.com/?p=bar.git;a=snapshot;h=v0.1;sf=tgz' - line = '{}; python_version >= "3"'.format(url) + line = f'{url}; python_version >= "3"' req = install_req_from_line(line) assert req.link.url == url, req.url assert str(req.markers) == 'python_version >= "3"' # without space, markers are part of the URL url = 'http://foo.com/?p=bar.git;a=snapshot;h=v0.1;sf=tgz' - line = '{};python_version >= "3"'.format(url) + line = f'{url};python_version >= "3"' req = install_req_from_line(line) assert req.link.url == line, req.url assert req.markers is None @@ -560,7 +560,7 @@ def test_unidentifiable_name(self): with pytest.raises(InstallationError) as e: install_req_from_line(test_name) err_msg = e.value.args[0] - assert "Invalid requirement: '{}'".format(test_name) == err_msg + assert f"Invalid requirement: '{test_name}'" == err_msg def test_requirement_file(self): req_file_path = os.path.join(self.tempdir, 'test.txt') diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py index 4812637ee5a..1b19bf2e00f 100644 --- a/tests/unit/test_req_file.py +++ b/tests/unit/test_req_file.py @@ -270,7 +270,7 @@ def test_yield_editable_requirement(self, line_processor): def test_yield_editable_constraint(self, line_processor): url = 'git+https://url#egg=SomeProject' - line = '-e {}'.format(url) + line = f'-e {url}' filename = 'filename' comes_from = '-c {} (line {})'.format(filename, 1) req = install_req_from_editable( @@ -432,7 +432,7 @@ def get_file_content(filename, *args, **kwargs): return None, '-r reqs.txt' elif filename == 'http://me.com/me/reqs.txt': return None, req_name - assert False, 'Unexpected file requested {}'.format(filename) + assert False, f'Unexpected file requested {filename}' monkeypatch.setattr( pip._internal.req.req_file, 'get_file_content', get_file_content @@ -478,7 +478,7 @@ def test_absolute_local_nested_req_files( # POSIX-ify the path, since Windows backslashes aren't supported. other_req_file_str = str(other_req_file).replace('\\', '/') - req_file.write_text('-r {}'.format(other_req_file_str)) + req_file.write_text(f'-r {other_req_file_str}') other_req_file.write_text(req_name) reqs = list(parse_reqfile(str(req_file), session=session)) @@ -498,10 +498,10 @@ def test_absolute_http_nested_req_file_in_local( def get_file_content(filename, *args, **kwargs): if filename == str(req_file): - return None, '-r {}'.format(nested_req_file) + return None, f'-r {nested_req_file}' elif filename == nested_req_file: return None, req_name - assert False, 'Unexpected file requested {}'.format(filename) + assert False, f'Unexpected file requested {filename}' monkeypatch.setattr( pip._internal.req.req_file, 'get_file_content', get_file_content diff --git a/tests/unit/test_resolution_legacy_resolver.py b/tests/unit/test_resolution_legacy_resolver.py index c4ff6492917..333a2ae1657 100644 --- a/tests/unit/test_resolution_legacy_resolver.py +++ b/tests/unit/test_resolution_legacy_resolver.py @@ -34,7 +34,7 @@ def __init__(self, metadata, metadata_name=None): self.metadata = metadata def __str__(self): - return '<distribution {!r}>'.format(self.project_name) + return f'<distribution {self.project_name!r}>' def has_metadata(self, name): return (name == self.metadata_name) @@ -47,7 +47,7 @@ def get_metadata(self, name): def make_fake_dist(requires_python=None, metadata_name=None): metadata = 'Name: test\n' if requires_python is not None: - metadata += 'Requires-Python:{}'.format(requires_python) + metadata += f'Requires-Python:{requires_python}' return FakeDist(metadata, metadata_name=metadata_name) diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 57434669e54..84adffd7beb 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -63,11 +63,11 @@ def setup(self): self.user_site = 'USER_SITE' self.user_site_egglink = os.path.join( self.user_site, - '{}.egg-link'.format(project) + f'{project}.egg-link' ) self.site_packages_egglink = os.path.join( self.site_packages, - '{}.egg-link'.format(project), + f'{project}.egg-link', ) # patches @@ -440,7 +440,7 @@ def test_rmtree_retries_for_3sec(tmpdir, monkeypatch): def test_path_to_display(monkeypatch, path, fs_encoding, expected): monkeypatch.setattr(sys, 'getfilesystemencoding', lambda: fs_encoding) actual = path_to_display(path) - assert actual == expected, 'actual: {!r}'.format(actual) + assert actual == expected, f'actual: {actual!r}' class Test_normalize_path(object): diff --git a/tests/unit/test_utils_distutils_args.py b/tests/unit/test_utils_distutils_args.py index 5bca65018ec..96cdb180424 100644 --- a/tests/unit/test_utils_distutils_args.py +++ b/tests/unit/test_utils_distutils_args.py @@ -50,7 +50,7 @@ def test_multiple_invocations_do_not_keep_options(): ("root", "11"), ]) def test_all_value_options_work(name, value): - result = parse_distutils_args(["--{}={}".format(name, value)]) + result = parse_distutils_args([f"--{name}={value}"]) key_name = name.replace("-", "_") assert result[key_name] == value diff --git a/tests/unit/test_utils_parallel.py b/tests/unit/test_utils_parallel.py index d5449988e3a..5a23f7d655f 100644 --- a/tests/unit/test_utils_parallel.py +++ b/tests/unit/test_utils_parallel.py @@ -62,7 +62,7 @@ def test_have_sem_open(name, monkeypatch): """Test fallback when sem_open is available.""" monkeypatch.setattr(DUNDER_IMPORT, have_sem_open) with tmp_import_parallel() as parallel: - assert getattr(parallel, name) is getattr(parallel, '_{}'.format(name)) + assert getattr(parallel, name) is getattr(parallel, f'_{name}') @mark.parametrize('name', MAPS) diff --git a/tests/unit/test_utils_subprocess.py b/tests/unit/test_utils_subprocess.py index fd73878c1a7..3f0b2195793 100644 --- a/tests/unit/test_utils_subprocess.py +++ b/tests/unit/test_utils_subprocess.py @@ -48,7 +48,7 @@ def test_make_subprocess_output_error(): line2 line3 ----------------------------------------""") - assert actual == expected, 'actual: {}'.format(actual) + assert actual == expected, f'actual: {actual}' def test_make_subprocess_output_error__non_ascii_command_arg(monkeypatch): @@ -76,7 +76,7 @@ def test_make_subprocess_output_error__non_ascii_command_arg(monkeypatch): cwd: /path/to/cwd Complete output (0 lines): ----------------------------------------""") - assert actual == expected, 'actual: {}'.format(actual) + assert actual == expected, f'actual: {actual}' @pytest.mark.skipif("sys.version_info < (3,)") @@ -98,7 +98,7 @@ def test_make_subprocess_output_error__non_ascii_cwd_python_3(monkeypatch): cwd: /path/to/cwd/déf Complete output (0 lines): ----------------------------------------""") - assert actual == expected, 'actual: {}'.format(actual) + assert actual == expected, f'actual: {actual}' @pytest.mark.parametrize('encoding', [ @@ -128,7 +128,7 @@ def test_make_subprocess_output_error__non_ascii_cwd_python_2( cwd: /path/to/cwd/déf Complete output (0 lines): ----------------------------------------""") - assert actual == expected, 'actual: {}'.format(actual) + assert actual == expected, f'actual: {actual}' # This test is mainly important for checking unicode in Python 2. @@ -150,7 +150,7 @@ def test_make_subprocess_output_error__non_ascii_line(): Complete output (1 lines): curly-quote: \u2018 ----------------------------------------""") - assert actual == expected, 'actual: {}'.format(actual) + assert actual == expected, f'actual: {actual}' class FakeSpinner(SpinnerInterface): @@ -205,7 +205,7 @@ def check_result( records = caplog.record_tuples if len(records) != len(expected_records): - raise RuntimeError('{} != {}'.format(records, expected_records)) + raise RuntimeError(f'{records} != {expected_records}') for record, expected_record in zip(records, expected_records): # Check the logger_name and log level parts exactly. @@ -316,7 +316,7 @@ def test_info_logging__subprocess_error(self, capfd, caplog): 'Hello', 'fail', 'world', - ], 'lines: {}'.format(actual) # Show the full output on failure. + ], f'lines: {actual}' # Show the full output on failure. assert command_line.startswith(' command: ') assert command_line.endswith('print("world"); exit("fail")\'') @@ -375,7 +375,7 @@ def test_spinner_finish( expected_spin_count = expected[2] command = ( - 'print("Hello"); print("world"); exit({})'.format(exit_status) + f'print("Hello"); print("world"); exit({exit_status})' ) args, spinner = self.prepare_call(caplog, log_level, command=command) try: diff --git a/tests/unit/test_utils_unpacking.py b/tests/unit/test_utils_unpacking.py index 5c2be24d429..5a30df27c4f 100644 --- a/tests/unit/test_utils_unpacking.py +++ b/tests/unit/test_utils_unpacking.py @@ -66,14 +66,14 @@ def confirm_files(self): if expected_contents is not None: with open(path, mode='rb') as f: contents = f.read() - assert contents == expected_contents, 'fname: {}'.format(fname) + assert contents == expected_contents, f'fname: {fname}' if sys.platform == 'win32': # the permissions tests below don't apply in windows # due to os.chmod being a noop continue mode = self.mode(path) assert mode == expected_mode, ( - "mode: {}, expected mode: {}".format(mode, expected_mode) + f"mode: {mode}, expected mode: {expected_mode}" ) def make_zip_file(self, filename, file_list): diff --git a/tests/unit/test_utils_wheel.py b/tests/unit/test_utils_wheel.py index abd30114800..a73ecd6c3d6 100644 --- a/tests/unit/test_utils_wheel.py +++ b/tests/unit/test_utils_wheel.py @@ -112,7 +112,7 @@ def test_wheel_version_fails_on_no_wheel_version(): def test_wheel_version_fails_on_bad_wheel_version(version): with pytest.raises(UnsupportedWheel) as e: wheel.wheel_version( - message_from_string("Wheel-Version: {}".format(version)) + message_from_string(f"Wheel-Version: {version}") ) assert "invalid Wheel-Version" in str(e.value) diff --git a/tools/automation/release/__init__.py b/tools/automation/release/__init__.py index c1364cfc461..738214491c7 100644 --- a/tools/automation/release/__init__.py +++ b/tools/automation/release/__init__.py @@ -98,13 +98,13 @@ def update_version_file(version: str, filepath: str) -> None: with open(filepath, "w", encoding="utf-8") as f: for line in content: if line.startswith("__version__ ="): - f.write('__version__ = "{}"\n'.format(version)) + f.write(f'__version__ = "{version}"\n') file_modified = True else: f.write(line) assert file_modified, \ - "Version file {} did not get modified".format(filepath) + f"Version file {filepath} did not get modified" def create_git_tag(session: Session, tag_name: str, *, message: str) -> None: From 07ddf66f65cbfb8352f81f179ab39943d56cb937 Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Fri, 25 Dec 2020 16:28:53 -0800 Subject: [PATCH 2829/3170] Remove Python 2 compatibility shim for json.JSONDecodeError JSONDecodeError has been available since Python 3.5. https://docs.python.org/3/library/json.html#json.JSONDecodeError --- news/7ced09a1-9af6-4190-8249-05a6328e379e.trivial.rst | 0 src/pip/_internal/utils/direct_url_helpers.py | 9 ++------- 2 files changed, 2 insertions(+), 7 deletions(-) create mode 100644 news/7ced09a1-9af6-4190-8249-05a6328e379e.trivial.rst diff --git a/news/7ced09a1-9af6-4190-8249-05a6328e379e.trivial.rst b/news/7ced09a1-9af6-4190-8249-05a6328e379e.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/utils/direct_url_helpers.py b/src/pip/_internal/utils/direct_url_helpers.py index 87bd61fa01f..c6cd976caed 100644 --- a/src/pip/_internal/utils/direct_url_helpers.py +++ b/src/pip/_internal/utils/direct_url_helpers.py @@ -1,3 +1,4 @@ +import json import logging from pip._internal.models.direct_url import ( @@ -11,12 +12,6 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.vcs import vcs -try: - from json import JSONDecodeError -except ImportError: - # PY2 - JSONDecodeError = ValueError # type: ignore - if MYPY_CHECK_RUNNING: from typing import Optional @@ -114,7 +109,7 @@ def dist_get_direct_url(dist): return DirectUrl.from_json(dist.get_metadata(DIRECT_URL_METADATA_NAME)) except ( DirectUrlValidationError, - JSONDecodeError, + json.JSONDecodeError, UnicodeDecodeError ) as e: logger.warning( From 1f9d6e4aeae5f5f54a8264085e2d8dad302b9ede Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Fri, 25 Dec 2020 16:48:47 -0800 Subject: [PATCH 2830/3170] Remove unnecessary str() call around str literal Unnecessary since dropping Python 2. --- news/857785f2-1d4e-4067-9b4b-acc6ae741aef.trivial.rst | 0 tests/functional/test_wheel.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 news/857785f2-1d4e-4067-9b4b-acc6ae741aef.trivial.rst diff --git a/news/857785f2-1d4e-4067-9b4b-acc6ae741aef.trivial.rst b/news/857785f2-1d4e-4067-9b4b-acc6ae741aef.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/functional/test_wheel.py b/tests/functional/test_wheel.py index 286d694356e..b2ea414ac92 100644 --- a/tests/functional/test_wheel.py +++ b/tests/functional/test_wheel.py @@ -301,7 +301,7 @@ def test_pip_wheel_ext_module_with_tmpdir_inside(script, data, common_wheels): # To avoid a test dependency on a C compiler, we set the env vars to "noop" # The .c source is empty anyway - script.environ['CC'] = script.environ['LDSHARED'] = str('true') + script.environ['CC'] = script.environ['LDSHARED'] = 'true' result = script.pip( 'wheel', data.src / 'extension', From 14ebb03997dd7f615e6651f845f1c63cdd883f3d Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Wed, 23 Dec 2020 11:25:12 -0800 Subject: [PATCH 2831/3170] Use "yield from" syntax Available since Python 3.3. https://docs.python.org/3/whatsnew/3.3.html#pep-380 --- news/d96bbdcd-059a-4ea4-b02e-343f8b51aad5.trivial.rst | 0 src/pip/_internal/network/session.py | 3 +-- src/pip/_internal/req/req_file.py | 8 ++------ 3 files changed, 3 insertions(+), 8 deletions(-) create mode 100644 news/d96bbdcd-059a-4ea4-b02e-343f8b51aad5.trivial.rst diff --git a/news/d96bbdcd-059a-4ea4-b02e-343f8b51aad5.trivial.rst b/news/d96bbdcd-059a-4ea4-b02e-343f8b51aad5.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py index c4ac889e28d..2b780154000 100644 --- a/src/pip/_internal/network/session.py +++ b/src/pip/_internal/network/session.py @@ -340,8 +340,7 @@ def add_trusted_host(self, host, source=None, suppress_logging=False): def iter_secure_origins(self): # type: () -> Iterator[SecureOrigin] - for secure_origin in SECURE_ORIGINS: - yield secure_origin + yield from SECURE_ORIGINS for host, port in self.pip_trusted_origins: yield ('*', host, '*' if port is None else port) diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index 087129613d0..5e1f71324d7 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -334,8 +334,7 @@ def parse(self, filename, constraint): # type: (str, bool) -> Iterator[ParsedLine] """Parse a given file, yielding parsed lines. """ - for line in self._parse_and_recurse(filename, constraint): - yield line + yield from self._parse_and_recurse(filename, constraint) def _parse_and_recurse(self, filename, constraint): # type: (str, bool) -> Iterator[ParsedLine] @@ -363,10 +362,7 @@ def _parse_and_recurse(self, filename, constraint): os.path.dirname(filename), req_path, ) - for inner_line in self._parse_and_recurse( - req_path, nested_constraint, - ): - yield inner_line + yield from self._parse_and_recurse(req_path, nested_constraint) else: yield line From d282fb94a3b7f4992aad35513d27deb1d44db37f Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Sat, 26 Dec 2020 05:49:31 -0800 Subject: [PATCH 2832/3170] Use unified OSError and its subclasses Since Python 3.3, the following classes have merged into OSError. They remain as aliases for backward compatibility. - EnvironmentError - IOError - WindowsError https://docs.python.org/3/library/exceptions.html#OSError Python 3 also has subclasses of OSError to help identify more specific errors. For example, FileNotFoundError. This allows simplifying some except blocks. --- ...2f-aef7-4323-8332-819f0be13d79.trivial.rst | 0 setup.cfg | 7 ---- src/pip/_internal/commands/install.py | 12 +++--- src/pip/_internal/network/cache.py | 2 +- src/pip/_internal/req/req_file.py | 2 +- src/pip/_internal/req/req_tracker.py | 7 +--- src/pip/_internal/self_outdated_check.py | 2 +- src/pip/_internal/utils/filesystem.py | 28 ++++++-------- src/pip/_internal/utils/misc.py | 2 +- src/pip/_internal/utils/virtualenv.py | 2 +- src/pip/_internal/vcs/versioncontrol.py | 14 +++---- tests/unit/test_command_install.py | 38 +++++++++---------- 12 files changed, 48 insertions(+), 68 deletions(-) create mode 100644 news/f0af302f-aef7-4323-8332-819f0be13d79.trivial.rst diff --git a/news/f0af302f-aef7-4323-8332-819f0be13d79.trivial.rst b/news/f0af302f-aef7-4323-8332-819f0be13d79.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/setup.cfg b/setup.cfg index d2bf9fb69db..25850c4cefa 100644 --- a/setup.cfg +++ b/setup.cfg @@ -29,13 +29,6 @@ per-file-ignores = noxfile.py: G # B011: Do not call assert False since python -O removes these calls tests/*: B011 - # TODO: Remove IOError from except (OSError, IOError) blocks in - # these files when Python 2 is removed. - # In Python 3, IOError have been merged into OSError - # https://github.com/PyCQA/flake8-bugbear/issues/110 - src/pip/_internal/utils/filesystem.py: B014 - src/pip/_internal/network/cache.py: B014 - src/pip/_internal/utils/misc.py: B014 [mypy] follow_imports = silent diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index a9a44787273..cd97ecb6042 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -435,10 +435,10 @@ def run(self, options, args): write_output( 'Successfully installed %s', installed_desc, ) - except EnvironmentError as error: + except OSError as error: show_traceback = (self.verbosity >= 1) - message = create_env_error_message( + message = create_os_error_message( error, show_traceback, options.use_user_site, ) logger.error(message, exc_info=show_traceback) # noqa @@ -697,16 +697,16 @@ def format_options(option_names): ) -def create_env_error_message(error, show_traceback, using_user_site): - # type: (EnvironmentError, bool, bool) -> str - """Format an error message for an EnvironmentError +def create_os_error_message(error, show_traceback, using_user_site): + # type: (OSError, bool, bool) -> str + """Format an error message for an OSError 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") + parts.append("Could not install packages due to an OSError") if not show_traceback: parts.append(": ") parts.append(str(error)) diff --git a/src/pip/_internal/network/cache.py b/src/pip/_internal/network/cache.py index 582a7d72dcd..9253b204769 100644 --- a/src/pip/_internal/network/cache.py +++ b/src/pip/_internal/network/cache.py @@ -29,7 +29,7 @@ def suppressed_cache_errors(): """ try: yield - except (OSError, IOError): + except OSError: pass diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index 5e1f71324d7..dfa4650904f 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -553,7 +553,7 @@ def get_file_content(url, session): try: with open(url, 'rb') as f: content = auto_decode(f.read()) - except IOError as exc: + except OSError as exc: raise InstallationError( f'Could not open requirements file: {exc}' ) diff --git a/src/pip/_internal/req/req_tracker.py b/src/pip/_internal/req/req_tracker.py index 84edbbfae66..daa5b44ca25 100644 --- a/src/pip/_internal/req/req_tracker.py +++ b/src/pip/_internal/req/req_tracker.py @@ -1,5 +1,4 @@ import contextlib -import errno import hashlib import logging import os @@ -103,10 +102,8 @@ def add(self, req): try: with open(entry_path) as fp: contents = fp.read() - except IOError as e: - # if the error is anything other than "file does not exist", raise. - if e.errno != errno.ENOENT: - raise + except FileNotFoundError: + pass else: message = '{} is already being built: {}'.format( req.link, contents) diff --git a/src/pip/_internal/self_outdated_check.py b/src/pip/_internal/self_outdated_check.py index 56d03a86687..c22f06afe87 100644 --- a/src/pip/_internal/self_outdated_check.py +++ b/src/pip/_internal/self_outdated_check.py @@ -50,7 +50,7 @@ def __init__(self, cache_dir): try: with open(self.statefile_path) as statefile: self.state = json.load(statefile) - except (IOError, ValueError, KeyError): + except (OSError, ValueError, KeyError): # Explicitly suppressing exceptions, since we don't want to # error out if the cache file is invalid. pass diff --git a/src/pip/_internal/utils/filesystem.py b/src/pip/_internal/utils/filesystem.py index 1b0c083cfda..dfa2802f71a 100644 --- a/src/pip/_internal/utils/filesystem.py +++ b/src/pip/_internal/utils/filesystem.py @@ -1,4 +1,3 @@ -import errno import fnmatch import os import os.path @@ -64,7 +63,7 @@ def copy2_fixed(src, dest): """ try: shutil.copy2(src, dest) - except (OSError, IOError): + except OSError: for f in [src, dest]: try: is_socket_file = is_socket(f) @@ -148,27 +147,22 @@ def _test_writable_dir_win(path): file = os.path.join(path, name) try: fd = os.open(file, os.O_RDWR | os.O_CREAT | os.O_EXCL) - # Python 2 doesn't support FileExistsError and PermissionError. - except OSError as e: - # exception FileExistsError - if e.errno == errno.EEXIST: - continue - # exception PermissionError - if e.errno == errno.EPERM or e.errno == errno.EACCES: - # This could be because there's a directory with the same name. - # But it's highly unlikely there's a directory called that, - # so we'll assume it's because the parent dir is not writable. - # This could as well be because the parent dir is not readable, - # due to non-privileged user access. - return False - raise + except FileExistsError: + pass + except PermissionError: + # This could be because there's a directory with the same name. + # But it's highly unlikely there's a directory called that, + # so we'll assume it's because the parent dir is not writable. + # This could as well be because the parent dir is not readable, + # due to non-privileged user access. + return False else: os.close(fd) os.unlink(file) return True # This should never be reached - raise EnvironmentError( + raise OSError( 'Unexpected condition testing for writable directory' ) diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index e83259eccb0..8e475017e30 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -138,7 +138,7 @@ def rmtree_errorhandler(func, path, exc_info): read-only attribute, and hopefully continue without problems.""" try: has_attr_readonly = not (os.stat(path).st_mode & stat.S_IWRITE) - except (IOError, OSError): + except OSError: # it's equivalent to os.path.exists return diff --git a/src/pip/_internal/utils/virtualenv.py b/src/pip/_internal/utils/virtualenv.py index acaceee281b..3086bf2fc8d 100644 --- a/src/pip/_internal/utils/virtualenv.py +++ b/src/pip/_internal/utils/virtualenv.py @@ -53,7 +53,7 @@ def _get_pyvenv_cfg_lines(): # writes with UTF-8. (pypa/pip#8717) with open(pyvenv_cfg_file, encoding='utf-8') as f: return f.read().splitlines() # avoids trailing newlines - except IOError: + except OSError: return None diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index a175e6d8468..f40a1005948 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -1,6 +1,5 @@ """Handles all VCS (version control) support""" -import errno import logging import os import shutil @@ -772,16 +771,13 @@ def run_command( extra_environ=extra_environ, extra_ok_returncodes=extra_ok_returncodes, log_failed_cmd=log_failed_cmd) - except OSError as e: + except FileNotFoundError: # 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 {cls.name!r} - do you have ' - '{cls.name!r} installed and in your ' - 'PATH?'.format(**locals())) - else: - raise # re-raise exception if a different error occurred + raise BadCommand( + 'Cannot find command {cls.name!r} - do you have ' + '{cls.name!r} installed and in your ' + 'PATH?'.format(**locals())) @classmethod def is_repository_directory(cls, path): diff --git a/tests/unit/test_command_install.py b/tests/unit/test_command_install.py index 7b6b38de0fa..66eb8ef3881 100644 --- a/tests/unit/test_command_install.py +++ b/tests/unit/test_command_install.py @@ -5,7 +5,7 @@ from pip._vendor.packaging.requirements import Requirement from pip._internal.commands.install import ( - create_env_error_message, + create_os_error_message, decide_user_install, reject_location_related_install_options, ) @@ -81,35 +81,35 @@ def test_rejection_for_location_requirement_options(): @pytest.mark.parametrize('error, show_traceback, using_user_site, expected', [ # show_traceback = True, using_user_site = True - (EnvironmentError("Illegal byte sequence"), True, True, 'Could not install' - ' packages due to an EnvironmentError.\n'), - (EnvironmentError(errno.EACCES, "No file permission"), True, True, 'Could' - ' not install packages due to an EnvironmentError.\nCheck the' + (OSError("Illegal byte sequence"), True, True, 'Could not install' + ' packages due to an OSError.\n'), + (OSError(errno.EACCES, "No file permission"), True, True, 'Could' + ' not install packages due to an OSError.\nCheck the' ' permissions.\n'), # show_traceback = True, using_user_site = False - (EnvironmentError("Illegal byte sequence"), True, False, 'Could not' - ' install packages due to an EnvironmentError.\n'), - (EnvironmentError(errno.EACCES, "No file permission"), True, False, 'Could' - ' not install packages due to an EnvironmentError.\nConsider using the' + (OSError("Illegal byte sequence"), True, False, 'Could not' + ' install packages due to an OSError.\n'), + (OSError(errno.EACCES, "No file permission"), True, False, 'Could' + ' not install packages due to an OSError.\nConsider using the' ' `--user` option or check the permissions.\n'), # show_traceback = False, using_user_site = True - (EnvironmentError("Illegal byte sequence"), False, True, 'Could not' - ' install packages due to an EnvironmentError: Illegal byte' + (OSError("Illegal byte sequence"), False, True, 'Could not' + ' install packages due to an OSError: Illegal byte' ' sequence\n'), - (EnvironmentError(errno.EACCES, "No file permission"), False, True, 'Could' - ' not install packages due to an EnvironmentError: [Errno 13] No file' + (OSError(errno.EACCES, "No file permission"), False, True, 'Could' + ' not install packages due to an OSError: [Errno 13] No file' ' permission\nCheck the permissions.\n'), # show_traceback = False, using_user_site = False - (EnvironmentError("Illegal byte sequence"), False, False, 'Could not' - ' install packages due to an EnvironmentError: Illegal byte sequence' + (OSError("Illegal byte sequence"), False, False, 'Could not' + ' install packages due to an OSError: Illegal byte sequence' '\n'), - (EnvironmentError(errno.EACCES, "No file permission"), False, False, - 'Could not install packages due to an EnvironmentError: [Errno 13] No' + (OSError(errno.EACCES, "No file permission"), False, False, + 'Could not install packages due to an OSError: [Errno 13] No' ' file permission\nConsider using the `--user` option or check the' ' permissions.\n'), ]) -def test_create_env_error_message( +def test_create_os_error_message( error, show_traceback, using_user_site, expected ): - msg = create_env_error_message(error, show_traceback, using_user_site) + msg = create_os_error_message(error, show_traceback, using_user_site) assert msg == expected From 41a30089de329090196012709e436c0a4a41049b Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 15 Dec 2020 22:23:07 +0800 Subject: [PATCH 2833/3170] Re-apply invalid metadata skip Skip candidate not providing valid metadata This reverts commit 7165ab8cb9a0d6297b5a0dcd7d9a84350d0a0b3b. --- src/pip/_internal/exceptions.py | 15 ++++++ .../resolution/resolvelib/candidates.py | 23 +++----- .../resolution/resolvelib/factory.py | 52 ++++++++++++++++--- .../resolution/resolvelib/requirements.py | 41 +++++++++++++++ src/pip/_internal/utils/subprocess.py | 8 +-- tests/functional/test_new_resolver.py | 19 +++++++ tests/unit/test_utils_subprocess.py | 8 +-- 7 files changed, 131 insertions(+), 35 deletions(-) diff --git a/src/pip/_internal/exceptions.py b/src/pip/_internal/exceptions.py index b4e7e68ed90..8335450e4e6 100644 --- a/src/pip/_internal/exceptions.py +++ b/src/pip/_internal/exceptions.py @@ -141,6 +141,21 @@ def __str__(self): ) +class InstallationSubprocessError(InstallationError): + """A subprocess call failed during installation.""" + def __init__(self, returncode, description): + # type: (int, str) -> None + self.returncode = returncode + self.description = description + + def __str__(self): + # type: () -> str + return ( + "Command errored out with exit status {}: {} " + "Check the logs for full command output." + ).format(self.returncode, self.description) + + class HashErrors(InstallationError): """Multiple HashError instances rolled into one for reporting""" diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 39358711acb..2453b65ac62 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -141,7 +141,7 @@ def __init__( self._ireq = ireq self._name = name self._version = version - self._dist = None # type: Optional[Distribution] + self.dist = self._prepare() def __str__(self): # type: () -> str @@ -209,8 +209,6 @@ def _prepare_distribution(self): def _check_metadata_consistency(self, dist): # type: (Distribution) -> None """Check for consistency of project name and version of dist.""" - # TODO: (Longer term) Rather than abort, reject this candidate - # and backtrack. This would need resolvelib support. name = canonicalize_name(dist.project_name) if self._name is not None and self._name != name: raise MetadataInconsistent(self._ireq, "name", dist.project_name) @@ -219,25 +217,17 @@ def _check_metadata_consistency(self, dist): raise MetadataInconsistent(self._ireq, "version", dist.version) def _prepare(self): - # type: () -> None - if self._dist is not None: - return + # type: () -> Distribution try: dist = self._prepare_distribution() except HashError as e: + # Provide HashError the underlying ireq that caused it. This + # provides context for the resulting error message to show the + # offending line to the user. e.req = self._ireq raise - - assert dist is not None, "Distribution already installed" self._check_metadata_consistency(dist) - self._dist = dist - - @property - def dist(self): - # type: () -> Distribution - if self._dist is None: - self._prepare() - return self._dist + return dist def _get_requires_python_dependency(self): # type: () -> Optional[Requirement] @@ -261,7 +251,6 @@ def iter_dependencies(self, with_requires): def get_install_requirement(self): # type: () -> Optional[InstallRequirement] - self._prepare() return self._ireq diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 70484f47017..f780831188d 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -5,6 +5,8 @@ from pip._internal.exceptions import ( DistributionNotFound, InstallationError, + InstallationSubprocessError, + MetadataInconsistent, UnsupportedPythonVersion, UnsupportedWheel, ) @@ -33,6 +35,7 @@ ExplicitRequirement, RequiresPythonRequirement, SpecifierRequirement, + UnsatisfiableRequirement, ) if MYPY_CHECK_RUNNING: @@ -94,6 +97,7 @@ def __init__( self._force_reinstall = force_reinstall self._ignore_requires_python = ignore_requires_python + self._build_failures = {} # type: Cache[InstallationError] self._link_candidate_cache = {} # type: Cache[LinkCandidate] self._editable_candidate_cache = {} # type: Cache[EditableCandidate] self._installed_candidate_cache = { @@ -136,21 +140,40 @@ def _make_candidate_from_link( name, # type: Optional[str] version, # type: Optional[_BaseVersion] ): - # type: (...) -> Candidate + # type: (...) -> Optional[Candidate] # TODO: Check already installed candidate, and use it if the link and # editable flag match. + + if link in self._build_failures: + # We already tried this candidate before, and it does not build. + # Don't bother trying again. + return None + if template.editable: if link not in self._editable_candidate_cache: - self._editable_candidate_cache[link] = EditableCandidate( - link, template, factory=self, name=name, version=version, - ) + try: + self._editable_candidate_cache[link] = EditableCandidate( + link, template, factory=self, + name=name, version=version, + ) + except (InstallationSubprocessError, MetadataInconsistent) as e: + logger.warning("Discarding %s. %s", link, e) + self._build_failures[link] = e + return None base = self._editable_candidate_cache[link] # type: BaseCandidate else: if link not in self._link_candidate_cache: - self._link_candidate_cache[link] = LinkCandidate( - link, template, factory=self, name=name, version=version, - ) + try: + self._link_candidate_cache[link] = LinkCandidate( + link, template, factory=self, + name=name, version=version, + ) + except (InstallationSubprocessError, MetadataInconsistent) as e: + logger.warning("Discarding %s. %s", link, e) + self._build_failures[link] = e + return None base = self._link_candidate_cache[link] + if extras: return ExtrasCandidate(base, extras) return base @@ -210,13 +233,16 @@ def iter_index_candidates(): for ican in reversed(icans): if not all_yanked and ican.link.is_yanked: continue - yield self._make_candidate_from_link( + candidate = self._make_candidate_from_link( link=ican.link, extras=extras, template=template, name=name, version=ican.version, ) + if candidate is None: + continue + yield candidate return FoundCandidates( iter_index_candidates, @@ -280,6 +306,16 @@ def make_requirement_from_install_req(self, ireq, requested_extras): name=canonicalize_name(ireq.name) if ireq.name else None, version=None, ) + if cand is None: + # There's no way we can satisfy a URL requirement if the underlying + # candidate fails to build. An unnamed URL must be user-supplied, so + # we fail eagerly. If the URL is named, an unsatisfiable requirement + # can make the resolver do the right thing, either backtrack (and + # maybe find some other requirement that's buildable) or raise a + # ResolutionImpossible eventually. + if not ireq.name: + raise self._build_failures[ireq.link] + return UnsatisfiableRequirement(canonicalize_name(ireq.name)) return self.make_requirement_from_candidate(cand) def make_requirement_from_candidate(self, candidate): diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py index 85343d5980e..61c81e00eee 100644 --- a/src/pip/_internal/resolution/resolvelib/requirements.py +++ b/src/pip/_internal/resolution/resolvelib/requirements.py @@ -158,3 +158,44 @@ def is_satisfied_by(self, candidate): # already implements the prerelease logic, and would have filtered out # prerelease candidates if the user does not expect them. return self.specifier.contains(candidate.version, prereleases=True) + + +class UnsatisfiableRequirement(Requirement): + """A requirement that cannot be satisfied. + """ + def __init__(self, name): + # type: (str) -> None + self._name = name + + def __str__(self): + # type: () -> str + return "{} (unavailable)".format(self._name) + + def __repr__(self): + # type: () -> str + return "{class_name}({name!r})".format( + class_name=self.__class__.__name__, + name=str(self._name), + ) + + @property + def project_name(self): + # type: () -> str + return self._name + + @property + def name(self): + # type: () -> str + return self._name + + def format_for_error(self): + # type: () -> str + return str(self) + + def get_candidate_lookup(self): + # type: () -> CandidateLookup + return None, None + + def is_satisfied_by(self, candidate): + # type: (Candidate) -> bool + return False diff --git a/src/pip/_internal/utils/subprocess.py b/src/pip/_internal/utils/subprocess.py index 7de45aed010..b7becfb63e3 100644 --- a/src/pip/_internal/utils/subprocess.py +++ b/src/pip/_internal/utils/subprocess.py @@ -4,7 +4,7 @@ import subprocess from pip._internal.cli.spinners import SpinnerInterface, open_spinner -from pip._internal.exceptions import InstallationError +from pip._internal.exceptions import InstallationSubprocessError from pip._internal.utils.compat import console_to_str, str_to_display from pip._internal.utils.logging import subprocess_logger from pip._internal.utils.misc import HiddenText, path_to_display @@ -230,11 +230,7 @@ def call_subprocess( exit_status=proc.returncode, ) subprocess_logger.error(msg) - exc_msg = ( - 'Command errored out with exit status {}: {} ' - 'Check the logs for full command output.' - ).format(proc.returncode, command_desc) - raise InstallationError(exc_msg) + raise InstallationSubprocessError(proc.returncode, command_desc) elif on_returncode == 'warn': subprocess_logger.warning( 'Command "%s" had error code %s in %s', diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index efe9b1ec707..66493a777c2 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -1216,3 +1216,22 @@ def test_new_resolver_does_not_reinstall_when_from_a_local_index(script): assert "Installing collected packages: simple" not in result.stdout, str(result) assert "Requirement already satisfied: simple" in result.stdout, str(result) assert_installed(script, simple="0.1.0") + + +def test_new_resolver_skip_inconsistent_metadata(script): + create_basic_wheel_for_package(script, "A", "1") + + a_2 = create_basic_wheel_for_package(script, "A", "2") + a_2.rename(a_2.parent.joinpath("a-3-py2.py3-none-any.whl")) + + result = script.pip( + "install", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "--verbose", + "A", + allow_stderr_warning=True, + ) + + assert " different version in metadata: '2'" in result.stderr, str(result) + assert_installed(script, a="1") diff --git a/tests/unit/test_utils_subprocess.py b/tests/unit/test_utils_subprocess.py index 7b9900cd62f..a9a52dc4190 100644 --- a/tests/unit/test_utils_subprocess.py +++ b/tests/unit/test_utils_subprocess.py @@ -6,7 +6,7 @@ import pytest from pip._internal.cli.spinners import SpinnerInterface -from pip._internal.exceptions import InstallationError +from pip._internal.exceptions import InstallationSubprocessError from pip._internal.utils.misc import hide_value from pip._internal.utils.subprocess import ( call_subprocess, @@ -275,7 +275,7 @@ def test_info_logging__subprocess_error(self, capfd, caplog): command = 'print("Hello"); print("world"); exit("fail")' args, spinner = self.prepare_call(caplog, log_level, command=command) - with pytest.raises(InstallationError) as exc: + with pytest.raises(InstallationSubprocessError) as exc: call_subprocess(args, spinner=spinner) result = None exc_message = str(exc.value) @@ -359,7 +359,7 @@ def test_info_logging_with_show_stdout_true(self, capfd, caplog): # log level is only WARNING. (0, True, None, WARNING, (None, 'done', 2)), # Test a non-zero exit status. - (3, False, None, INFO, (InstallationError, 'error', 2)), + (3, False, None, INFO, (InstallationSubprocessError, 'error', 2)), # Test a non-zero exit status also in extra_ok_returncodes. (3, False, (3, ), INFO, (None, 'done', 2)), ]) @@ -395,7 +395,7 @@ def test_spinner_finish( assert spinner.spin_count == expected_spin_count def test_closes_stdin(self): - with pytest.raises(InstallationError): + with pytest.raises(InstallationSubprocessError): call_subprocess( [sys.executable, '-c', 'input()'], show_stdout=True, From 92ad71761266b1164b8733415fd6f93c58b68822 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 15 Dec 2020 16:07:07 +0800 Subject: [PATCH 2834/3170] New resolver incorrectly tries unneeded candidates When the new resolver needs to upgrade a package, it puts the already-installed package in the middle of the candidate list obtained from indexes. But when doing it, the candidate list is eagerly consumed, causing pip to download all candidates. --- tests/functional/test_new_resolver.py | 42 +++++++++++++++++++++++++++ 1 file changed, 42 insertions(+) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 66493a777c2..f3850c208c6 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -1235,3 +1235,45 @@ def test_new_resolver_skip_inconsistent_metadata(script): assert " different version in metadata: '2'" in result.stderr, str(result) assert_installed(script, a="1") + + +@pytest.mark.parametrize( + "upgrade", + [True, False], + ids=["upgrade", "no-upgrade"], +) +def test_new_resolver_lazy_fetch_candidates(script, upgrade): + create_basic_wheel_for_package(script, "myuberpkg", "1") + create_basic_wheel_for_package(script, "myuberpkg", "2") + create_basic_wheel_for_package(script, "myuberpkg", "3") + + # Install an old version first. + script.pip( + "install", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "myuberpkg==1", + ) + + # Now install the same package again, maybe with the upgrade flag. + if upgrade: + pip_upgrade_args = ["--upgrade"] + else: + pip_upgrade_args = [] + result = script.pip( + "install", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "myuberpkg", + *pip_upgrade_args # Trailing comma fails on Python 2. + ) + + # pip should install the version preferred by the strategy... + if upgrade: + assert_installed(script, myuberpkg="3") + else: + assert_installed(script, myuberpkg="1") + + # But should reach there in the best route possible, without trying + # candidates it does not need to. + assert "myuberpkg-2" not in result.stdout, str(result) From 8e55757a2f2d0fffe93ef495ba8a110503a7ef6f Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 15 Dec 2020 17:08:35 +0800 Subject: [PATCH 2835/3170] Lazy-evaluate candidates with installed inserted --- news/9203.bugfix.rst | 4 +++ news/9246.bugfix.rst | 4 +++ .../resolution/resolvelib/found_candidates.py | 26 ++++++++++--------- 3 files changed, 22 insertions(+), 12 deletions(-) create mode 100644 news/9203.bugfix.rst create mode 100644 news/9246.bugfix.rst diff --git a/news/9203.bugfix.rst b/news/9203.bugfix.rst new file mode 100644 index 00000000000..38320218fbb --- /dev/null +++ b/news/9203.bugfix.rst @@ -0,0 +1,4 @@ +New resolver: Discard a faulty distribution, instead of quitting outright. +This implementation is taken from 20.2.2, with a fix that always makes the +resolver iterate through candidates from indexes lazily, to avoid downloading +candidates we do not need. diff --git a/news/9246.bugfix.rst b/news/9246.bugfix.rst new file mode 100644 index 00000000000..93f7f18f9f5 --- /dev/null +++ b/news/9246.bugfix.rst @@ -0,0 +1,4 @@ +New resolver: Discard a source distribution if it fails to generate metadata, +instead of quitting outright. This implementation is taken from 20.2.2, with a +fix that always makes the resolver iterate through candidates from indexes +lazily, to avoid downloading candidates we do not need. diff --git a/src/pip/_internal/resolution/resolvelib/found_candidates.py b/src/pip/_internal/resolution/resolvelib/found_candidates.py index 439259818fa..0eae7cb93db 100644 --- a/src/pip/_internal/resolution/resolvelib/found_candidates.py +++ b/src/pip/_internal/resolution/resolvelib/found_candidates.py @@ -1,6 +1,5 @@ import functools import itertools -import operator from pip._vendor.six.moves import collections_abc # type: ignore @@ -32,18 +31,21 @@ def _insert_installed(installed, others): already-installed package. Candidates from index are returned in their normal ordering, except replaced when the version is already installed. - Since candidates from index are already sorted by reverse version order, - `sorted()` here would keep the ordering mostly intact, only shuffling the - already-installed candidate into the correct position. We put the already- - installed candidate in front of those from the index, so it's put in front - after sorting due to Python sorting's stableness guarentee. + The implementation iterates through and yields other candidates, inserting + the installed candidate exactly once before we start yielding older or + equivalent candidates, or after all other candidates if they are all newer. """ - candidates = sorted( - itertools.chain([installed], others), - key=operator.attrgetter("version"), - reverse=True, - ) - return iter(candidates) + installed_yielded = False + for candidate in others: + # If the installed candidate is better, yield it first. + if not installed_yielded and installed.version >= candidate.version: + yield installed + installed_yielded = True + yield candidate + + # If the installed candidate is older than all other candidates. + if not installed_yielded: + yield installed class FoundCandidates(collections_abc.Sequence): From 2a254525423ee39c6c942bffa074a8d5ed37dc5d Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sat, 19 Dec 2020 23:18:00 +0800 Subject: [PATCH 2836/3170] Add docstring to emphasise laziness --- .../resolution/resolvelib/found_candidates.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/src/pip/_internal/resolution/resolvelib/found_candidates.py b/src/pip/_internal/resolution/resolvelib/found_candidates.py index 0eae7cb93db..be7811dc6ac 100644 --- a/src/pip/_internal/resolution/resolvelib/found_candidates.py +++ b/src/pip/_internal/resolution/resolvelib/found_candidates.py @@ -1,3 +1,13 @@ +"""Utilities to lazily create and visit candidates found. + +Creating and visiting a candidate is a *very* costly operation. It involves +fetching, extracting, potentially building modules from source, and verifying +distribution metadata. It is therefore crucial for performance to keep +everything here lazy all the way down, so we only touch candidates that we +absolutely need, and not "download the world" when we only need one version of +something. +""" + import functools import itertools From b92c0c1112483b9edc48e9cb1ffb121eba887e75 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <hugovk@users.noreply.github.com> Date: Sun, 27 Dec 2020 13:29:40 +0200 Subject: [PATCH 2837/3170] Use Formatter.default_time_format after dropping PY2 --- news/80B9D718-3054-41C7-B920-78348DFD01A6.trivial.rst | 0 src/pip/_internal/utils/logging.py | 5 ++--- 2 files changed, 2 insertions(+), 3 deletions(-) create mode 100644 news/80B9D718-3054-41C7-B920-78348DFD01A6.trivial.rst diff --git a/news/80B9D718-3054-41C7-B920-78348DFD01A6.trivial.rst b/news/80B9D718-3054-41C7-B920-78348DFD01A6.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/utils/logging.py b/src/pip/_internal/utils/logging.py index 9fd1d42c702..dabc98180ef 100644 --- a/src/pip/_internal/utils/logging.py +++ b/src/pip/_internal/utils/logging.py @@ -101,6 +101,7 @@ def get_indentation(): class IndentingFormatter(logging.Formatter): + default_time_format = "%Y-%m-%dT%H:%M:%S" def __init__(self, *args, **kwargs): """ @@ -139,9 +140,7 @@ def format(self, record): prefix = '' if self.add_timestamp: - # TODO: Use Formatter.default_time_format after dropping PY2. - t = self.formatTime(record, "%Y-%m-%dT%H:%M:%S") - prefix = '{t},{record.msecs:03.0f} '.format(**locals()) + prefix = f"{self.formatTime(record)} " prefix += " " * get_indentation() formatted = "".join([ prefix + line From 6902269d4ccbace7afda0dbcba8dca86354cf300 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sat, 19 Dec 2020 12:08:33 +0800 Subject: [PATCH 2838/3170] Verify built wheel contains valid metadata --- news/9206.feature.rst | 3 ++ src/pip/_internal/commands/install.py | 1 + src/pip/_internal/commands/wheel.py | 9 ++++ src/pip/_internal/wheel_builder.py | 60 ++++++++++++++++++++++++++- 4 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 news/9206.feature.rst diff --git a/news/9206.feature.rst b/news/9206.feature.rst new file mode 100644 index 00000000000..90cd2cf99fb --- /dev/null +++ b/news/9206.feature.rst @@ -0,0 +1,3 @@ +``pip wheel`` now verifies the built wheel contains valid metadata, and can be +installed by a subsequent ``pip install``. This can be disabled with +``--no-verify``. diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index a4e10f260a2..0f6c384e5cd 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -347,6 +347,7 @@ def run(self, options, args): _, build_failures = build( reqs_to_build, wheel_cache=wheel_cache, + verify=True, build_options=[], global_options=[], ) diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index 39fd2bf8128..a9f66258a14 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -81,6 +81,14 @@ def add_options(self): self.cmd_opts.add_option(cmdoptions.build_dir()) self.cmd_opts.add_option(cmdoptions.progress_bar()) + self.cmd_opts.add_option( + '--no-verify', + dest='no_verify', + action='store_true', + default=False, + help="Don't verify if built wheel is valid.", + ) + self.cmd_opts.add_option( '--global-option', dest='global_options', @@ -166,6 +174,7 @@ def run(self, options, args): build_successes, build_failures = build( reqs_to_build, wheel_cache=wheel_cache, + verify=(not options.no_verify), build_options=options.build_options or [], global_options=options.global_options or [], ) diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index 27fce66c264..dbc34d0952b 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -5,8 +5,15 @@ import os.path import re import shutil +import zipfile +from pip._vendor.packaging.utils import canonicalize_name, canonicalize_version +from pip._vendor.packaging.version import InvalidVersion, Version +from pip._vendor.pkg_resources import Distribution + +from pip._internal.exceptions import InvalidWheelFilename, UnsupportedWheel from pip._internal.models.link import Link +from pip._internal.models.wheel import Wheel from pip._internal.operations.build.wheel import build_wheel_pep517 from pip._internal.operations.build.wheel_legacy import build_wheel_legacy from pip._internal.utils.logging import indent_log @@ -16,6 +23,7 @@ from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.urls import path_to_url +from pip._internal.utils.wheel import pkg_resources_distribution_for_wheel from pip._internal.vcs import vcs if MYPY_CHECK_RUNNING: @@ -160,9 +168,49 @@ def _always_true(_): return True +def _get_metadata_version(dist): + # type: (Distribution) -> Optional[Version] + for line in dist.get_metadata_lines(dist.PKG_INFO): + if line.lower().startswith("metadata-version:"): + value = line.split(":", 1)[-1].strip() + try: + return Version(value) + except InvalidVersion: + msg = "Invalid Metadata-Version: {}".format(value) + raise UnsupportedWheel(msg) + raise UnsupportedWheel("Missing Metadata-Version") + + +def _verify_one(req, wheel_path): + # type: (InstallRequirement, str) -> None + canonical_name = canonicalize_name(req.name) + w = Wheel(os.path.basename(wheel_path)) + if canonicalize_name(w.name) != canonical_name: + raise InvalidWheelFilename( + "Wheel has unexpected file name: expected {!r}, " + "got {!r}".format(canonical_name, w.name), + ) + with zipfile.ZipFile(wheel_path, allowZip64=True) as zf: + dist = pkg_resources_distribution_for_wheel( + zf, canonical_name, wheel_path, + ) + if canonicalize_version(dist.version) != canonicalize_version(w.version): + raise InvalidWheelFilename( + "Wheel has unexpected file name: expected {!r}, " + "got {!r}".format(dist.version, w.version), + ) + if (_get_metadata_version(dist) >= Version("1.2") + and not isinstance(dist.parsed_version, Version)): + raise UnsupportedWheel( + "Metadata 1.2 mandates PEP 440 version, " + "but {!r} is not".format(dist.version) + ) + + def _build_one( req, # type: InstallRequirement output_dir, # type: str + verify, # type: bool build_options, # type: List[str] global_options, # type: List[str] ): @@ -182,9 +230,16 @@ def _build_one( # Install build deps into temporary directory (PEP 518) with req.build_env: - return _build_one_inside_env( + wheel_path = _build_one_inside_env( req, output_dir, build_options, global_options ) + if wheel_path and verify: + try: + _verify_one(req, wheel_path) + except (InvalidWheelFilename, UnsupportedWheel) as e: + logger.warning("Built wheel for %s is invalid: %s", req.name, e) + return None + return wheel_path def _build_one_inside_env( @@ -257,6 +312,7 @@ def _clean_one_legacy(req, global_options): def build( requirements, # type: Iterable[InstallRequirement] wheel_cache, # type: WheelCache + verify, # type: bool build_options, # type: List[str] global_options, # type: List[str] ): @@ -280,7 +336,7 @@ def build( for req in requirements: cache_dir = _get_cache_dir(req, wheel_cache) wheel_file = _build_one( - req, cache_dir, build_options, global_options + req, cache_dir, verify, build_options, global_options ) if wheel_file: # Update the link for this. From 6a438bdc9384f6e23e9f33f06039edda64411f72 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sun, 27 Dec 2020 20:20:49 +0800 Subject: [PATCH 2839/3170] Upgrade vendored resolvelib to 0.5.4 --- news/9180.vendor.rst | 2 ++ src/pip/_vendor/resolvelib/__init__.py | 2 +- src/pip/_vendor/resolvelib/resolvers.py | 36 +++++++++++++++---------- src/pip/_vendor/vendor.txt | 2 +- 4 files changed, 26 insertions(+), 16 deletions(-) create mode 100644 news/9180.vendor.rst diff --git a/news/9180.vendor.rst b/news/9180.vendor.rst new file mode 100644 index 00000000000..61c433fe631 --- /dev/null +++ b/news/9180.vendor.rst @@ -0,0 +1,2 @@ +Upgrade resolvelib to 0.5.4 to fix error when an existing incompatibility is +unable to be applied to a backtracked state. diff --git a/src/pip/_vendor/resolvelib/__init__.py b/src/pip/_vendor/resolvelib/__init__.py index 5a400f23ed1..f023ad63154 100644 --- a/src/pip/_vendor/resolvelib/__init__.py +++ b/src/pip/_vendor/resolvelib/__init__.py @@ -11,7 +11,7 @@ "ResolutionTooDeep", ] -__version__ = "0.5.3" +__version__ = "0.5.4" from .providers import AbstractProvider, AbstractResolver diff --git a/src/pip/_vendor/resolvelib/resolvers.py b/src/pip/_vendor/resolvelib/resolvers.py index acf0f8a6b43..bb88d8c2c75 100644 --- a/src/pip/_vendor/resolvelib/resolvers.py +++ b/src/pip/_vendor/resolvelib/resolvers.py @@ -257,7 +257,7 @@ def _backtrack(self): information from Y to Y'. 4a. If this causes Y' to conflict, we need to backtrack again. Make Y' the new Z and go back to step 2. - 4b. If the incompatibilites apply cleanly, end backtracking. + 4b. If the incompatibilities apply cleanly, end backtracking. """ while len(self._states) >= 3: # Remove the state that triggered backtracking. @@ -271,28 +271,36 @@ def _backtrack(self): for k, v in broken_state.criteria.items() ] + # Also mark the newly known incompatibility. + incompatibilities_from_broken.append((name, [candidate])) + self._r.backtracking(candidate) # Create a new state from the last known-to-work one, and apply # the previously gathered incompatibility information. - self._push_new_state() - for k, incompatibilities in incompatibilities_from_broken: - try: - crit = self.state.criteria[k] - except KeyError: - continue - self.state.criteria[k] = crit.excluded_of(incompatibilities) + def _patch_criteria(): + for k, incompatibilities in incompatibilities_from_broken: + if not incompatibilities: + continue + try: + criterion = self.state.criteria[k] + except KeyError: + continue + criterion = criterion.excluded_of(incompatibilities) + if criterion is None: + return False + self.state.criteria[k] = criterion + return True - # Mark the newly known incompatibility. - criterion = self.state.criteria[name].excluded_of([candidate]) + self._push_new_state() + success = _patch_criteria() # It works! Let's work on this new state. - if criterion: - self.state.criteria[name] = criterion + if success: return True - # State does not work after adding the new incompatibility - # information. Try the still previous state. + # State does not work after applying known incompatibilities. + # Try the still previous state. # No way to backtrack anymore. return False diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 4f7042cc127..1db32f396ae 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -15,7 +15,7 @@ requests==2.25.0 chardet==3.0.4 idna==2.10 urllib3==1.26.2 -resolvelib==0.5.3 +resolvelib==0.5.4 retrying==1.3.3 setuptools==44.0.0 six==1.15.0 From a3e246f7d00a1d8e464152e739c328ade3e04ab4 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <hugovk@users.noreply.github.com> Date: Sun, 27 Dec 2020 16:01:06 +0200 Subject: [PATCH 2840/3170] Cleanup: replace 'from urllib import xyz as urllib_xyz' with import urllib.xyz --- ...42-2198-4760-B5A7-B5A6BB98ECA2.trivial.rst | 0 src/pip/_internal/index/collector.py | 22 +++++++++---------- src/pip/_internal/models/direct_url.py | 6 ++--- src/pip/_internal/models/index.py | 6 ++--- src/pip/_internal/models/link.py | 10 ++++----- src/pip/_internal/models/search_scope.py | 8 +++---- src/pip/_internal/network/auth.py | 4 ++-- src/pip/_internal/network/session.py | 4 ++-- src/pip/_internal/network/xmlrpc.py | 6 ++--- src/pip/_internal/req/req_file.py | 4 ++-- src/pip/_internal/utils/misc.py | 13 +++++------ src/pip/_internal/utils/urls.py | 10 ++++----- src/pip/_internal/vcs/git.py | 10 ++++----- src/pip/_internal/vcs/versioncontrol.py | 10 ++++----- tests/functional/test_install_index.py | 4 ++-- tests/lib/local_repos.py | 4 ++-- tests/unit/test_collector.py | 4 ++-- tests/unit/test_urls.py | 6 ++--- 18 files changed, 65 insertions(+), 66 deletions(-) create mode 100644 news/9DE59242-2198-4760-B5A7-B5A6BB98ECA2.trivial.rst diff --git a/news/9DE59242-2198-4760-B5A7-B5A6BB98ECA2.trivial.rst b/news/9DE59242-2198-4760-B5A7-B5A6BB98ECA2.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/index/collector.py b/src/pip/_internal/index/collector.py index 53b97c6b553..ee4eb719992 100644 --- a/src/pip/_internal/index/collector.py +++ b/src/pip/_internal/index/collector.py @@ -9,9 +9,9 @@ import mimetypes import os import re +import urllib.parse +import urllib.request from collections import OrderedDict -from urllib import parse as urllib_parse -from urllib import request as urllib_request from pip._vendor import html5lib, requests from pip._vendor.distlib.compat import unescape @@ -94,7 +94,7 @@ def _ensure_html_response(url, session): Raises `_NotHTTP` if the URL is not available for a HEAD request, or `_NotHTML` if the content type is not text/html. """ - scheme, netloc, path, query, fragment = urllib_parse.urlsplit(url) + scheme, netloc, path, query, fragment = urllib.parse.urlsplit(url) if scheme not in {'http', 'https'}: raise _NotHTTP() @@ -192,7 +192,7 @@ def _clean_url_path_part(part): Clean a "part" of a URL path (i.e. after splitting on "@" characters). """ # We unquote prior to quoting to make sure nothing is double quoted. - return urllib_parse.quote(urllib_parse.unquote(part)) + return urllib.parse.quote(urllib.parse.unquote(part)) def _clean_file_url_path(part): @@ -206,7 +206,7 @@ def _clean_file_url_path(part): # should not be quoted. On Linux where drive letters do not # exist, the colon should be quoted. We rely on urllib.request # to do the right thing here. - return urllib_request.pathname2url(urllib_request.url2pathname(part)) + return urllib.request.pathname2url(urllib.request.url2pathname(part)) # percent-encoded: / @@ -245,11 +245,11 @@ def _clean_link(url): """ # Split the URL into parts according to the general structure # `scheme://netloc/path;parameters?query#fragment`. - result = urllib_parse.urlparse(url) + result = urllib.parse.urlparse(url) # If the netloc is empty, then the URL refers to a local filesystem path. is_local_path = not result.netloc path = _clean_url_path(result.path, is_local_path=is_local_path) - return urllib_parse.urlunparse(result._replace(path=path)) + return urllib.parse.urlunparse(result._replace(path=path)) def _create_link_from_element( @@ -265,7 +265,7 @@ def _create_link_from_element( if not href: return None - url = _clean_link(urllib_parse.urljoin(base_url, href)) + url = _clean_link(urllib.parse.urljoin(base_url, href)) pyrequire = anchor.get('data-requires-python') pyrequire = unescape(pyrequire) if pyrequire else None @@ -416,13 +416,13 @@ def _get_html_page(link, session=None): return None # Tack index.html onto file:// URLs that point to directories - scheme, _, path, _, _, _ = urllib_parse.urlparse(url) - if (scheme == 'file' and os.path.isdir(urllib_request.url2pathname(path))): + scheme, _, path, _, _, _ = urllib.parse.urlparse(url) + if (scheme == 'file' and os.path.isdir(urllib.request.url2pathname(path))): # add trailing slash if not present so urljoin doesn't trim # final segment if not url.endswith('/'): url += '/' - url = urllib_parse.urljoin(url, 'index.html') + url = urllib.parse.urljoin(url, 'index.html') logger.debug(' file: URL is directory, getting %s', url) try: diff --git a/src/pip/_internal/models/direct_url.py b/src/pip/_internal/models/direct_url.py index 39112ea4e37..a8869bd0442 100644 --- a/src/pip/_internal/models/direct_url.py +++ b/src/pip/_internal/models/direct_url.py @@ -1,7 +1,7 @@ """ PEP 610 """ import json import re -from urllib import parse as urllib_parse +import urllib.parse from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -194,9 +194,9 @@ def redacted_url(self): environment variables as specified in PEP 610, or it is ``git`` in the case of a git URL. """ - purl = urllib_parse.urlsplit(self.url) + purl = urllib.parse.urlsplit(self.url) netloc = self._remove_auth_from_netloc(purl.netloc) - surl = urllib_parse.urlunsplit( + surl = urllib.parse.urlunsplit( (purl.scheme, netloc, purl.path, purl.query, purl.fragment) ) return surl diff --git a/src/pip/_internal/models/index.py b/src/pip/_internal/models/index.py index ec328190a2d..b148abb4250 100644 --- a/src/pip/_internal/models/index.py +++ b/src/pip/_internal/models/index.py @@ -1,4 +1,4 @@ -from urllib import parse as urllib_parse +import urllib.parse class PackageIndex: @@ -12,7 +12,7 @@ def __init__(self, url, file_storage_domain): # type: (str, str) -> None super().__init__() self.url = url - self.netloc = urllib_parse.urlsplit(url).netloc + self.netloc = urllib.parse.urlsplit(url).netloc self.simple_url = self._url_for_path('simple') self.pypi_url = self._url_for_path('pypi') @@ -23,7 +23,7 @@ def __init__(self, url, file_storage_domain): def _url_for_path(self, path): # type: (str) -> str - return urllib_parse.urljoin(self.url, path) + return urllib.parse.urljoin(self.url, path) PyPI = PackageIndex( diff --git a/src/pip/_internal/models/link.py b/src/pip/_internal/models/link.py index 07f9c565c12..06a7ceb3fce 100644 --- a/src/pip/_internal/models/link.py +++ b/src/pip/_internal/models/link.py @@ -1,7 +1,7 @@ import os import posixpath import re -from urllib import parse as urllib_parse +import urllib.parse from pip._internal.utils.filetypes import WHEEL_EXTENSION from pip._internal.utils.misc import ( @@ -67,7 +67,7 @@ def __init__( if url.startswith('\\\\'): url = path_to_url(url) - self._parsed_url = urllib_parse.urlsplit(url) + self._parsed_url = urllib.parse.urlsplit(url) # Store the url as a private attribute to prevent accidentally # trying to set a new value. self._url = url @@ -112,7 +112,7 @@ def filename(self): netloc, user_pass = split_auth_from_netloc(self.netloc) return netloc - name = urllib_parse.unquote(name) + name = urllib.parse.unquote(name) assert name, ( 'URL {self._url!r} produced no filename'.format(**locals())) return name @@ -138,7 +138,7 @@ def netloc(self): @property def path(self): # type: () -> str - return urllib_parse.unquote(self._parsed_url.path) + return urllib.parse.unquote(self._parsed_url.path) def splitext(self): # type: () -> Tuple[str, str] @@ -153,7 +153,7 @@ def ext(self): def url_without_fragment(self): # type: () -> str scheme, netloc, path, query, fragment = self._parsed_url - return urllib_parse.urlunsplit((scheme, netloc, path, query, None)) + return urllib.parse.urlunsplit((scheme, netloc, path, query, None)) _egg_fragment_re = re.compile(r'[#&]egg=([^&]*)') diff --git a/src/pip/_internal/models/search_scope.py b/src/pip/_internal/models/search_scope.py index abfb8bed412..c972f1d1704 100644 --- a/src/pip/_internal/models/search_scope.py +++ b/src/pip/_internal/models/search_scope.py @@ -2,7 +2,7 @@ import logging import os import posixpath -from urllib import parse as urllib_parse +import urllib.parse from pip._vendor.packaging.utils import canonicalize_name @@ -53,7 +53,7 @@ def create( # relies on TLS. if not has_tls(): for link in itertools.chain(index_urls, built_find_links): - parsed = urllib_parse.urlparse(link) + parsed = urllib.parse.urlparse(link) if parsed.scheme == 'https': logger.warning( 'pip is configured with locations that require ' @@ -86,7 +86,7 @@ def get_formatted_locations(self): redacted_index_url = redact_auth_from_url(url) # Parse the URL - purl = urllib_parse.urlsplit(redacted_index_url) + purl = urllib.parse.urlsplit(redacted_index_url) # URL is generally invalid if scheme and netloc is missing # there are issues with Python and URL parsing, so this test @@ -122,7 +122,7 @@ def mkurl_pypi_url(url): # type: (str) -> str loc = posixpath.join( url, - urllib_parse.quote(canonicalize_name(project_name))) + urllib.parse.quote(canonicalize_name(project_name))) # For maximum compatibility with easy_install, ensure the path # ends in a trailing slash. Although this isn't in the spec # (and PyPI can handle it without the slash) some other index diff --git a/src/pip/_internal/network/auth.py b/src/pip/_internal/network/auth.py index 1a613854061..a63a2965c67 100644 --- a/src/pip/_internal/network/auth.py +++ b/src/pip/_internal/network/auth.py @@ -5,7 +5,7 @@ """ import logging -from urllib import parse as urllib_parse +import urllib.parse from pip._vendor.requests.auth import AuthBase, HTTPBasicAuth from pip._vendor.requests.utils import get_netrc_auth @@ -250,7 +250,7 @@ def handle_401(self, resp, **kwargs): if not self.prompting: return resp - parsed = urllib_parse.urlparse(resp.url) + parsed = urllib.parse.urlparse(resp.url) # Prompt the user for a new username and password username, password, save = self._prompt_for_password(parsed.netloc) diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py index 2b780154000..5021b8eefaa 100644 --- a/src/pip/_internal/network/session.py +++ b/src/pip/_internal/network/session.py @@ -13,8 +13,8 @@ import os import platform import sys +import urllib.parse import warnings -from urllib import parse as urllib_parse from pip._vendor import requests, six, urllib3 from pip._vendor.cachecontrol import CacheControlAdapter @@ -347,7 +347,7 @@ def iter_secure_origins(self): def is_secure_origin(self, location): # type: (Link) -> bool # Determine if this url used a secure transport mechanism - parsed = urllib_parse.urlparse(str(location)) + parsed = urllib.parse.urlparse(str(location)) origin_protocol, origin_host, origin_port = ( parsed.scheme, parsed.hostname, parsed.port, ) diff --git a/src/pip/_internal/network/xmlrpc.py b/src/pip/_internal/network/xmlrpc.py index 6dd03eb0f9d..87490453259 100644 --- a/src/pip/_internal/network/xmlrpc.py +++ b/src/pip/_internal/network/xmlrpc.py @@ -2,7 +2,7 @@ """ import logging -from urllib import parse as urllib_parse +import urllib.parse # NOTE: XMLRPC Client is not annotated in typeshed as on 2017-07-17, which is # why we ignore the type on this import @@ -29,14 +29,14 @@ class PipXmlrpcTransport(xmlrpc_client.Transport): def __init__(self, index_url, session, use_datetime=False): # type: (str, PipSession, bool) -> None super().__init__(use_datetime) - index_parts = urllib_parse.urlparse(index_url) + index_parts = urllib.parse.urlparse(index_url) self._scheme = index_parts.scheme self._session = session def request(self, host, handler, request_body, verbose=False): # type: (str, str, Dict[str, str], bool) -> None parts = (self._scheme, host, handler, None, None, None) - url = urllib_parse.urlunparse(parts) + url = urllib.parse.urlunparse(parts) try: headers = {'Content-Type': 'text/xml'} response = self._session.post(url, data=request_body, diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index dfa4650904f..716005dc560 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -6,7 +6,7 @@ import os import re import shlex -from urllib import parse as urllib_parse +import urllib.parse from pip._internal.cli import cmdoptions from pip._internal.exceptions import InstallationError, RequirementsFileParseError @@ -354,7 +354,7 @@ def _parse_and_recurse(self, filename, constraint): # original file is over http if SCHEME_RE.search(filename): # do a url join so relative paths work - req_path = urllib_parse.urljoin(filename, req_path) + req_path = urllib.parse.urljoin(filename, req_path) # original file and nested file are paths elif not SCHEME_RE.search(req_path): # do a join so relative paths work diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 8e475017e30..dcd23ec65f4 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -13,11 +13,10 @@ import shutil import stat import sys +import urllib.parse from collections import deque from io import StringIO from itertools import filterfalse, tee, zip_longest -from urllib import parse as urllib_parse -from urllib.parse import unquote as urllib_unquote from pip._vendor import pkg_resources from pip._vendor.packaging.utils import canonicalize_name @@ -697,7 +696,7 @@ def parse_netloc(netloc): Return the host-port pair from a netloc. """ url = build_url_from_netloc(netloc) - parsed = urllib_parse.urlparse(url) + parsed = urllib.parse.urlparse(url) return parsed.hostname, parsed.port @@ -723,7 +722,7 @@ def split_auth_from_netloc(netloc): user_pass = auth, None user_pass = tuple( - None if x is None else urllib_unquote(x) for x in user_pass + None if x is None else urllib.parse.unquote(x) for x in user_pass ) return netloc, user_pass @@ -745,7 +744,7 @@ def redact_netloc(netloc): user = '****' password = '' else: - user = urllib_parse.quote(user) + user = urllib.parse.quote(user) password = ':****' return '{user}{password}@{netloc}'.format(user=user, password=password, @@ -762,13 +761,13 @@ def _transform_url(url, transform_netloc): Returns a tuple containing the transformed url as item 0 and the original tuple returned by transform_netloc as item 1. """ - purl = urllib_parse.urlsplit(url) + purl = urllib.parse.urlsplit(url) netloc_tuple = transform_netloc(purl.netloc) # stripped url url_pieces = ( purl.scheme, netloc_tuple[0], purl.path, purl.query, purl.fragment ) - surl = urllib_parse.urlunsplit(url_pieces) + surl = urllib.parse.urlunsplit(url_pieces) return surl, netloc_tuple diff --git a/src/pip/_internal/utils/urls.py b/src/pip/_internal/utils/urls.py index 5754153f216..0ef063c2198 100644 --- a/src/pip/_internal/utils/urls.py +++ b/src/pip/_internal/utils/urls.py @@ -1,7 +1,7 @@ import os import sys -from urllib import parse as urllib_parse -from urllib import request as urllib_request +import urllib.parse +import urllib.request from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -23,7 +23,7 @@ def path_to_url(path): quoted path parts. """ path = os.path.normpath(os.path.abspath(path)) - url = urllib_parse.urljoin('file:', urllib_request.pathname2url(path)) + url = urllib.parse.urljoin('file:', urllib.request.pathname2url(path)) return url @@ -36,7 +36,7 @@ def url_to_path(url): "You can only turn file: urls into filenames (not {url!r})" .format(**locals())) - _, netloc, path, _, _ = urllib_parse.urlsplit(url) + _, netloc, path, _, _ = urllib.parse.urlsplit(url) if not netloc or netloc == 'localhost': # According to RFC 8089, same as empty authority. @@ -50,5 +50,5 @@ def url_to_path(url): .format(**locals()) ) - path = urllib_request.url2pathname(netloc + path) + path = urllib.request.url2pathname(netloc + path) return path diff --git a/src/pip/_internal/vcs/git.py b/src/pip/_internal/vcs/git.py index 71cb5331229..e540e02dd86 100644 --- a/src/pip/_internal/vcs/git.py +++ b/src/pip/_internal/vcs/git.py @@ -4,8 +4,8 @@ import logging import os.path import re -from urllib import parse as urllib_parse -from urllib import request as urllib_request +import urllib.parse +import urllib.request from pip._vendor.packaging.version import parse as parse_version @@ -28,8 +28,8 @@ from pip._internal.vcs.versioncontrol import AuthInfo, RevOptions -urlsplit = urllib_parse.urlsplit -urlunsplit = urllib_parse.urlunsplit +urlsplit = urllib.parse.urlsplit +urlunsplit = urllib.parse.urlunsplit logger = logging.getLogger(__name__) @@ -383,7 +383,7 @@ def get_url_rev_and_auth(cls, url): initial_slashes = path[:-len(path.lstrip('/'))] newpath = ( initial_slashes + - urllib_request.url2pathname(path) + urllib.request.url2pathname(path) .replace('\\', '/').lstrip('/') ) after_plus = scheme.find('+') + 1 diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index f40a1005948..56caad09d89 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -5,7 +5,7 @@ import shutil import subprocess import sys -from urllib import parse as urllib_parse +import urllib.parse from pip._vendor import pkg_resources @@ -284,7 +284,7 @@ def __init__(self): # type: () -> None # Register more schemes with urlparse for various version control # systems - urllib_parse.uses_netloc.extend(self.schemes) + urllib.parse.uses_netloc.extend(self.schemes) super().__init__() def __iter__(self): @@ -518,7 +518,7 @@ def get_url_rev_and_auth(cls, url): Returns: (url, rev, (username, password)). """ - scheme, netloc, path, query, frag = urllib_parse.urlsplit(url) + scheme, netloc, path, query, frag = urllib.parse.urlsplit(url) if '+' not in scheme: raise ValueError( "Sorry, {!r} is a malformed VCS url. " @@ -537,7 +537,7 @@ def get_url_rev_and_auth(cls, url): "which is not supported. Include a revision after @ " "or remove @ from the URL.".format(url) ) - url = urllib_parse.urlunsplit((scheme, netloc, path, query, '')) + url = urllib.parse.urlunsplit((scheme, netloc, path, query, '')) return url, rev, user_pass @staticmethod @@ -571,7 +571,7 @@ def normalize_url(url): Normalize a URL for comparison by unquoting it and removing any trailing slash. """ - return urllib_parse.unquote(url).rstrip('/') + return urllib.parse.unquote(url).rstrip('/') @classmethod def compare_urls(cls, url1, url2): diff --git a/tests/functional/test_install_index.py b/tests/functional/test_install_index.py index 8e432b95409..42d2f7a675e 100644 --- a/tests/functional/test_install_index.py +++ b/tests/functional/test_install_index.py @@ -1,6 +1,6 @@ import os import textwrap -from urllib import parse as urllib_parse +import urllib.parse def test_find_links_relative_path(script, data, with_wheel): @@ -58,7 +58,7 @@ def test_file_index_url_quoting(script, data, with_wheel): """ Test url quoting of file index url with a space """ - index_url = data.index_url(urllib_parse.quote("in dex")) + index_url = data.index_url(urllib.parse.quote("in dex")) result = script.pip( 'install', '-vvv', '--index-url', index_url, 'simple' ) diff --git a/tests/lib/local_repos.py b/tests/lib/local_repos.py index c93d1670e90..222f132207f 100644 --- a/tests/lib/local_repos.py +++ b/tests/lib/local_repos.py @@ -1,6 +1,6 @@ import os import subprocess -from urllib import request as urllib_request +import urllib.request from pip._internal.utils.misc import hide_url from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -18,7 +18,7 @@ def _create_svn_initools_repo(initools_dir): directory = os.path.dirname(initools_dir) subprocess.check_call('svnadmin create INITools'.split(), cwd=directory) - filename, _ = urllib_request.urlretrieve( + filename, _ = urllib.request.urlretrieve( 'http://bitbucket.org/hltbra/pip-initools-dump/raw/8b55c908a320/' 'INITools_modified.dump' ) diff --git a/tests/unit/test_collector.py b/tests/unit/test_collector.py index 22a85b6f27f..90d8f8d0fe5 100644 --- a/tests/unit/test_collector.py +++ b/tests/unit/test_collector.py @@ -1,9 +1,9 @@ import logging import os.path import re +import urllib.request import uuid from textwrap import dedent -from urllib import request as urllib_request import mock import pretend @@ -568,7 +568,7 @@ def test_get_html_page_directory_append_index(tmpdir): dirpath = tmpdir / "something" dirpath.mkdir() dir_url = "file:///{}".format( - urllib_request.pathname2url(dirpath).lstrip("/"), + urllib.request.pathname2url(dirpath).lstrip("/"), ) expected_url = "{}/index.html".format(dir_url.rstrip("/")) diff --git a/tests/unit/test_urls.py b/tests/unit/test_urls.py index 9c6f75a8021..607023fd28e 100644 --- a/tests/unit/test_urls.py +++ b/tests/unit/test_urls.py @@ -1,6 +1,6 @@ import os import sys -from urllib import request as urllib_request +import urllib.request import pytest @@ -21,7 +21,7 @@ def test_get_url_scheme(url, expected): def test_path_to_url_unix(): assert path_to_url('/tmp/file') == 'file:///tmp/file' path = os.path.join(os.getcwd(), 'file') - assert path_to_url('file') == 'file://' + urllib_request.pathname2url(path) + assert path_to_url('file') == 'file://' + urllib.request.pathname2url(path) @pytest.mark.skipif("sys.platform != 'win32'") @@ -30,7 +30,7 @@ def test_path_to_url_win(): assert path_to_url('c:\\tmp\\file') == 'file:///C:/tmp/file' assert path_to_url(r'\\unc\as\path') == 'file://unc/as/path' path = os.path.join(os.getcwd(), 'file') - assert path_to_url('file') == 'file:' + urllib_request.pathname2url(path) + assert path_to_url('file') == 'file:' + urllib.request.pathname2url(path) @pytest.mark.parametrize("url,win_expected,non_win_expected", [ From 92b90ea7e967a9e68530abb8c0e3b1e8a039fd8b Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sun, 27 Dec 2020 22:25:34 +0800 Subject: [PATCH 2841/3170] Split news into two sections --- news/9180.bugfix.rst | 1 + news/9180.vendor.rst | 2 -- news/resolvelib.vendor.rst | 1 + 3 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 news/9180.bugfix.rst delete mode 100644 news/9180.vendor.rst create mode 100644 news/resolvelib.vendor.rst diff --git a/news/9180.bugfix.rst b/news/9180.bugfix.rst new file mode 100644 index 00000000000..e597c1ad90a --- /dev/null +++ b/news/9180.bugfix.rst @@ -0,0 +1 @@ +Fix error when an existing incompatibility is unable to be applied to a backtracked state. diff --git a/news/9180.vendor.rst b/news/9180.vendor.rst deleted file mode 100644 index 61c433fe631..00000000000 --- a/news/9180.vendor.rst +++ /dev/null @@ -1,2 +0,0 @@ -Upgrade resolvelib to 0.5.4 to fix error when an existing incompatibility is -unable to be applied to a backtracked state. diff --git a/news/resolvelib.vendor.rst b/news/resolvelib.vendor.rst new file mode 100644 index 00000000000..680da3be1e7 --- /dev/null +++ b/news/resolvelib.vendor.rst @@ -0,0 +1 @@ +Upgrade resolvelib to 0.5.4. From 1ff092486bd167b7356559f0a3b4af59f141346d Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <hugovk@users.noreply.github.com> Date: Sun, 27 Dec 2020 16:28:22 +0200 Subject: [PATCH 2842/3170] Replace Python 2 tmpdir removal workarounds --- tests/conftest.py | 10 ++-------- 1 file changed, 2 insertions(+), 8 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 0bb69dae6d7..edf0ee05099 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -129,11 +129,8 @@ def tmpdir_factory(request, tmpdir_factory): """ yield tmpdir_factory if not request.config.getoption("--keep-tmpdir"): - # py.path.remove() uses str paths on Python 2 and cannot - # handle non-ASCII file names. This works around the problem by - # passing a unicode object to rmtree(). shutil.rmtree( - str(tmpdir_factory.getbasetemp()), + tmpdir_factory.getbasetemp(), ignore_errors=True, ) @@ -155,10 +152,7 @@ def tmpdir(request, tmpdir): # This should prevent us from needing a multiple gigabyte temporary # directory while running the tests. if not request.config.getoption("--keep-tmpdir"): - # py.path.remove() uses str paths on Python 2 and cannot - # handle non-ASCII file names. This works around the problem by - # passing a unicode object to rmtree(). - shutil.rmtree(str(tmpdir), ignore_errors=True) + tmpdir.remove(ignore_errors=True) @pytest.fixture(autouse=True) From 6480bed441a03d6346b9439112ee4b9aeee7cbc4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Mon, 21 Dec 2020 12:38:41 +0100 Subject: [PATCH 2843/3170] Revert "Remove on_returncode parameter from call_subprocess" This reverts commit ab3ee7191ca47294f8827916180969e23f5e0381. --- src/pip/_internal/vcs/git.py | 10 ++---- src/pip/_internal/vcs/mercurial.py | 1 + src/pip/_internal/vcs/versioncontrol.py | 42 ++++++++++++++++--------- 3 files changed, 32 insertions(+), 21 deletions(-) diff --git a/src/pip/_internal/vcs/git.py b/src/pip/_internal/vcs/git.py index 1831aede58a..141263803b9 100644 --- a/src/pip/_internal/vcs/git.py +++ b/src/pip/_internal/vcs/git.py @@ -135,13 +135,8 @@ def get_revision_sha(cls, dest, rev): rev: the revision name. """ # Pass rev to pre-filter the list. - - output = '' - try: - output = cls.run_command(['show-ref', rev], cwd=dest) - except SubProcessError: - pass - + output = cls.run_command(['show-ref', rev], cwd=dest, + on_returncode='ignore') refs = {} for line in output.strip().splitlines(): try: @@ -420,6 +415,7 @@ def get_repository_root(cls, location): r = cls.run_command( ['rev-parse', '--show-toplevel'], cwd=location, + on_returncode='raise', log_failed_cmd=False, ) except BadCommand: diff --git a/src/pip/_internal/vcs/mercurial.py b/src/pip/_internal/vcs/mercurial.py index 69763feaea4..b7f8073fd38 100644 --- a/src/pip/_internal/vcs/mercurial.py +++ b/src/pip/_internal/vcs/mercurial.py @@ -144,6 +144,7 @@ def get_repository_root(cls, location): r = cls.run_command( ['root'], cwd=location, + on_returncode='raise', log_failed_cmd=False, ) except BadCommand: diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index 6724dcc697d..5b93b66091f 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -90,6 +90,7 @@ def make_vcs_requirement_url(repo_url, rev, project_name, subdir=None): def call_subprocess( cmd, # type: Union[List[str], CommandArgs] cwd=None, # type: Optional[str] + on_returncode='raise', # type: str extra_environ=None, # type: Optional[Mapping[str, Any]] extra_ok_returncodes=None, # type: Optional[Iterable[int]] log_failed_cmd=True # type: Optional[bool] @@ -157,21 +158,32 @@ def call_subprocess( proc.returncode and proc.returncode not in extra_ok_returncodes ) if proc_had_error: - if not showing_subprocess and log_failed_cmd: - # Then the subprocess streams haven't been logged to the - # console yet. - msg = make_subprocess_output_error( - cmd_args=cmd, - cwd=cwd, - lines=all_output, - exit_status=proc.returncode, + if on_returncode == 'raise': + if not showing_subprocess and log_failed_cmd: + # Then the subprocess streams haven't been logged to the + # console yet. + msg = make_subprocess_output_error( + cmd_args=cmd, + cwd=cwd, + lines=all_output, + exit_status=proc.returncode, + ) + subprocess_logger.error(msg) + exc_msg = ( + 'Command errored out with exit status {}: {} ' + 'Check the logs for full command output.' + ).format(proc.returncode, command_desc) + raise SubProcessError(exc_msg) + elif on_returncode == 'warn': + subprocess_logger.warning( + 'Command "{}" had error code {} in {}'.format( + command_desc, proc.returncode, cwd) ) - subprocess_logger.error(msg) - exc_msg = ( - 'Command errored out with exit status {}: {} ' - 'Check the logs for full command output.' - ).format(proc.returncode, command_desc) - raise SubProcessError(exc_msg) + elif on_returncode == 'ignore': + pass + else: + raise ValueError('Invalid value: on_returncode={!r}'.format( + on_returncode)) return ''.join(all_output) @@ -764,6 +776,7 @@ def run_command( cls, cmd, # type: Union[List[str], CommandArgs] cwd=None, # type: Optional[str] + on_returncode='raise', # type: str extra_environ=None, # type: Optional[Mapping[str, Any]] extra_ok_returncodes=None, # type: Optional[Iterable[int]] log_failed_cmd=True # type: bool @@ -777,6 +790,7 @@ def run_command( cmd = make_command(cls.name, *cmd) try: return call_subprocess(cmd, cwd, + on_returncode=on_returncode, extra_environ=extra_environ, extra_ok_returncodes=extra_ok_returncodes, log_failed_cmd=log_failed_cmd) From 6693a71e0a472533714351b46aed688e19a17874 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Mon, 21 Dec 2020 12:44:23 +0100 Subject: [PATCH 2844/3170] Revert "Remove show_stdout from run_command args" This reverts commit 94882fd1ed9171ea5a2f4b8904dbd8763f05ba68. --- src/pip/_internal/vcs/bazaar.py | 7 ++-- src/pip/_internal/vcs/git.py | 19 +++++----- src/pip/_internal/vcs/mercurial.py | 16 ++++---- src/pip/_internal/vcs/subversion.py | 9 +++-- src/pip/_internal/vcs/versioncontrol.py | 50 +++++++++++++++++-------- 5 files changed, 62 insertions(+), 39 deletions(-) diff --git a/src/pip/_internal/vcs/bazaar.py b/src/pip/_internal/vcs/bazaar.py index 3180713f7db..efe524492af 100644 --- a/src/pip/_internal/vcs/bazaar.py +++ b/src/pip/_internal/vcs/bazaar.py @@ -55,7 +55,8 @@ def export(self, location, url): url, rev_options = self.get_url_rev_options(url) self.run_command( - make_command('export', location, url, rev_options.to_args()) + make_command('export', location, url, rev_options.to_args()), + show_stdout=False, ) def fetch_new(self, dest, url, rev_options): @@ -92,7 +93,7 @@ def get_url_rev_and_auth(cls, url): @classmethod def get_remote_url(cls, location): - urls = cls.run_command(['info'], cwd=location) + urls = cls.run_command(['info'], show_stdout=False, cwd=location) for line in urls.splitlines(): line = line.strip() for x in ('checkout of branch: ', @@ -107,7 +108,7 @@ def get_remote_url(cls, location): @classmethod def get_revision(cls, location): revision = cls.run_command( - ['revno'], cwd=location, + ['revno'], show_stdout=False, cwd=location, ) return revision.splitlines()[-1] diff --git a/src/pip/_internal/vcs/git.py b/src/pip/_internal/vcs/git.py index 141263803b9..a6d1396fc77 100644 --- a/src/pip/_internal/vcs/git.py +++ b/src/pip/_internal/vcs/git.py @@ -11,7 +11,7 @@ from pip._vendor.six.moves.urllib import parse as urllib_parse from pip._vendor.six.moves.urllib import request as urllib_request -from pip._internal.exceptions import BadCommand, SubProcessError +from pip._internal.exceptions import BadCommand, InstallationError, SubProcessError from pip._internal.utils.misc import display_path, hide_url from pip._internal.utils.subprocess import make_command from pip._internal.utils.temp_dir import TempDirectory @@ -79,7 +79,7 @@ def is_immutable_rev_checkout(self, url, dest): def get_git_version(self): VERSION_PFX = 'git version ' - version = self.run_command(['version']) + version = self.run_command(['version'], show_stdout=False) if version.startswith(VERSION_PFX): version = version[len(VERSION_PFX):].split()[0] else: @@ -102,7 +102,7 @@ def get_current_branch(cls, location): # and to suppress the message to stderr. args = ['symbolic-ref', '-q', 'HEAD'] output = cls.run_command( - args, extra_ok_returncodes=(1, ), cwd=location, + args, extra_ok_returncodes=(1, ), show_stdout=False, cwd=location, ) ref = output.strip() @@ -121,7 +121,7 @@ def export(self, location, url): self.unpack(temp_dir.path, url=url) self.run_command( ['checkout-index', '-a', '-f', '--prefix', location], - cwd=temp_dir.path + show_stdout=False, cwd=temp_dir.path ) @classmethod @@ -136,7 +136,7 @@ def get_revision_sha(cls, dest, rev): """ # Pass rev to pre-filter the list. output = cls.run_command(['show-ref', rev], cwd=dest, - on_returncode='ignore') + show_stdout=False, on_returncode='ignore') refs = {} for line in output.strip().splitlines(): try: @@ -310,7 +310,7 @@ def get_remote_url(cls, location): # exits with return code 1 if there are no matching lines. stdout = cls.run_command( ['config', '--get-regexp', r'remote\..*\.url'], - extra_ok_returncodes=(1, ), cwd=location, + extra_ok_returncodes=(1, ), show_stdout=False, cwd=location, ) remotes = stdout.splitlines() try: @@ -344,7 +344,7 @@ def get_revision(cls, location, rev=None): if rev is None: rev = 'HEAD' current_rev = cls.run_command( - ['rev-parse', rev], cwd=location, + ['rev-parse', rev], show_stdout=False, cwd=location, ) return current_rev.strip() @@ -357,7 +357,7 @@ def get_subdirectory(cls, location): # find the repo root git_dir = cls.run_command( ['rev-parse', '--git-dir'], - cwd=location).strip() + show_stdout=False, cwd=location).strip() if not os.path.isabs(git_dir): git_dir = os.path.join(location, git_dir) repo_root = os.path.abspath(os.path.join(git_dir, '..')) @@ -415,6 +415,7 @@ def get_repository_root(cls, location): r = cls.run_command( ['rev-parse', '--show-toplevel'], cwd=location, + show_stdout=False, on_returncode='raise', log_failed_cmd=False, ) @@ -422,7 +423,7 @@ def get_repository_root(cls, location): logger.debug("could not determine if %s is under git control " "because git is not available", location) return None - except SubProcessError: + except InstallationError: return None return os.path.normpath(r.rstrip('\r\n')) diff --git a/src/pip/_internal/vcs/mercurial.py b/src/pip/_internal/vcs/mercurial.py index b7f8073fd38..75e903cc8a6 100644 --- a/src/pip/_internal/vcs/mercurial.py +++ b/src/pip/_internal/vcs/mercurial.py @@ -8,7 +8,7 @@ from pip._vendor.six.moves import configparser -from pip._internal.exceptions import BadCommand, SubProcessError +from pip._internal.exceptions import BadCommand, InstallationError from pip._internal.utils.misc import display_path from pip._internal.utils.subprocess import make_command from pip._internal.utils.temp_dir import TempDirectory @@ -47,7 +47,7 @@ def export(self, location, url): self.unpack(temp_dir.path, url=url) self.run_command( - ['archive', location], cwd=temp_dir.path + ['archive', location], show_stdout=False, cwd=temp_dir.path ) def fetch_new(self, dest, url, rev_options): @@ -92,7 +92,7 @@ def update(self, dest, url, rev_options): def get_remote_url(cls, location): url = cls.run_command( ['showconfig', 'paths.default'], - cwd=location).strip() + show_stdout=False, cwd=location).strip() if cls._is_local_repository(url): url = path_to_url(url) return url.strip() @@ -103,7 +103,8 @@ def get_revision(cls, location): Return the repository-local changeset revision number, as an integer. """ current_revision = cls.run_command( - ['parents', '--template={rev}'], cwd=location).strip() + ['parents', '--template={rev}'], + show_stdout=False, cwd=location).strip() return current_revision @classmethod @@ -114,7 +115,7 @@ def get_requirement_revision(cls, location): """ current_rev_hash = cls.run_command( ['parents', '--template={node}'], - cwd=location).strip() + show_stdout=False, cwd=location).strip() return current_rev_hash @classmethod @@ -130,7 +131,7 @@ def get_subdirectory(cls, location): """ # find the repo root repo_root = cls.run_command( - ['root'], cwd=location).strip() + ['root'], show_stdout=False, cwd=location).strip() if not os.path.isabs(repo_root): repo_root = os.path.abspath(os.path.join(location, repo_root)) return find_path_to_setup_from_repo_root(location, repo_root) @@ -144,6 +145,7 @@ def get_repository_root(cls, location): r = cls.run_command( ['root'], cwd=location, + show_stdout=False, on_returncode='raise', log_failed_cmd=False, ) @@ -151,7 +153,7 @@ def get_repository_root(cls, location): logger.debug("could not determine if %s is under hg control " "because hg is not available", location) return None - except SubProcessError: + except InstallationError: return None return os.path.normpath(r.rstrip('\r\n')) diff --git a/src/pip/_internal/vcs/subversion.py b/src/pip/_internal/vcs/subversion.py index eae09c19610..a57e19751fa 100644 --- a/src/pip/_internal/vcs/subversion.py +++ b/src/pip/_internal/vcs/subversion.py @@ -133,7 +133,7 @@ def get_remote_url(cls, location): @classmethod def _get_svn_url_rev(cls, location): - from pip._internal.exceptions import SubProcessError + from pip._internal.exceptions import InstallationError entries_path = os.path.join(location, cls.dirname, 'entries') if os.path.exists(entries_path): @@ -166,12 +166,13 @@ def _get_svn_url_rev(cls, location): # are only potentially needed for remote server requests. xml = cls.run_command( ['info', '--xml', location], + show_stdout=False, ) url = _svn_info_xml_url_re.search(xml).group(1) revs = [ int(m.group(1)) for m in _svn_info_xml_rev_re.finditer(xml) ] - except SubProcessError: + except InstallationError: url, revs = None, [] if revs: @@ -217,7 +218,7 @@ def call_vcs_version(self): # svn, version 1.12.0-SlikSvn (SlikSvn/1.12.0) # compiled May 28 2019, 13:44:56 on x86_64-microsoft-windows6.2 version_prefix = 'svn, version ' - version = self.run_command(['--version']) + version = self.run_command(['--version'], show_stdout=True) if not version.startswith(version_prefix): return () @@ -300,7 +301,7 @@ def export(self, location, url): 'export', self.get_remote_call_options(), rev_options.to_args(), url, location, ) - self.run_command(cmd_args) + self.run_command(cmd_args, show_stdout=False) def fetch_new(self, dest, url, rev_options): # type: (str, HiddenText, RevOptions) -> None diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index 5b93b66091f..a4995c88f78 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -34,12 +34,10 @@ if MYPY_CHECK_RUNNING: from typing import ( - Any, Dict, Iterable, Iterator, List, - Mapping, Optional, Text, Tuple, @@ -89,15 +87,17 @@ def make_vcs_requirement_url(repo_url, rev, project_name, subdir=None): def call_subprocess( cmd, # type: Union[List[str], CommandArgs] + show_stdout=False, # type: bool cwd=None, # type: Optional[str] on_returncode='raise', # type: str - extra_environ=None, # type: Optional[Mapping[str, Any]] extra_ok_returncodes=None, # type: Optional[Iterable[int]] log_failed_cmd=True # type: Optional[bool] ): # type: (...) -> Text """ Args: + show_stdout: if true, use INFO to log the subprocess's stderr and + stdout streams. Otherwise, use DEBUG. Defaults to False. extra_ok_returncodes: an iterable of integer return codes that are acceptable, in addition to 0. Defaults to None, which means []. log_failed_cmd: if false, failed commands are not logged, @@ -105,16 +105,33 @@ def call_subprocess( """ if extra_ok_returncodes is None: extra_ok_returncodes = [] - - # log the subprocess output at DEBUG level. - log_subprocess = subprocess_logger.debug - - env = os.environ.copy() - if extra_environ: - env.update(extra_environ) + # Most places in pip use show_stdout=False. + # What this means is-- + # + # - We log this output of stdout and stderr at DEBUG level + # as it is received. + # - If DEBUG logging isn't enabled (e.g. if --verbose logging wasn't + # requested), then we show a spinner so the user can still see the + # subprocess is in progress. + # - If the subprocess exits with an error, we log the output to stderr + # at ERROR level if it hasn't already been displayed to the console + # (e.g. if --verbose logging wasn't enabled). This way we don't log + # the output to the console twice. + # + # If show_stdout=True, then the above is still done, but with DEBUG + # replaced by INFO. + if show_stdout: + # Then log the subprocess output at INFO level. + log_subprocess = subprocess_logger.info + used_level = logging.INFO + else: + # Then log the subprocess output using DEBUG. This also ensures + # it will be logged to the log file (aka user_log), if enabled. + log_subprocess = subprocess_logger.debug + used_level = logging.DEBUG # Whether the subprocess will be visible in the console. - showing_subprocess = True + showing_subprocess = subprocess_logger.getEffectiveLevel() <= used_level command_desc = format_command_args(cmd) try: @@ -176,8 +193,10 @@ def call_subprocess( raise SubProcessError(exc_msg) elif on_returncode == 'warn': subprocess_logger.warning( - 'Command "{}" had error code {} in {}'.format( - command_desc, proc.returncode, cwd) + 'Command "%s" had error code %s in %s', + command_desc, + proc.returncode, + cwd, ) elif on_returncode == 'ignore': pass @@ -775,9 +794,9 @@ def get_revision(cls, location): def run_command( cls, cmd, # type: Union[List[str], CommandArgs] + show_stdout=True, # type: bool cwd=None, # type: Optional[str] on_returncode='raise', # type: str - extra_environ=None, # type: Optional[Mapping[str, Any]] extra_ok_returncodes=None, # type: Optional[Iterable[int]] log_failed_cmd=True # type: bool ): @@ -789,9 +808,8 @@ def run_command( """ cmd = make_command(cls.name, *cmd) try: - return call_subprocess(cmd, cwd, + return call_subprocess(cmd, show_stdout, cwd, on_returncode=on_returncode, - extra_environ=extra_environ, extra_ok_returncodes=extra_ok_returncodes, log_failed_cmd=log_failed_cmd) except OSError as e: From eda67075c1669b298d82a9a0e4c5484d02ad41b8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Mon, 21 Dec 2020 13:03:11 +0100 Subject: [PATCH 2845/3170] Revert "Create call_subprocess just for vcs commands" This reverts commit 8adbc216a647b6b349f1b7f1eaa9e71cd3108955. --- src/pip/_internal/exceptions.py | 5 - src/pip/_internal/vcs/subversion.py | 14 ++- src/pip/_internal/vcs/versioncontrol.py | 144 +++--------------------- 3 files changed, 25 insertions(+), 138 deletions(-) diff --git a/src/pip/_internal/exceptions.py b/src/pip/_internal/exceptions.py index 56482caf77b..5a2d012a500 100644 --- a/src/pip/_internal/exceptions.py +++ b/src/pip/_internal/exceptions.py @@ -91,11 +91,6 @@ class CommandError(PipError): """Raised when there is an error in command-line arguments""" -class SubProcessError(PipError): - """Raised when there is an error raised while executing a - command in subprocess""" - - class PreviousBuildDirError(PipError): """Raised when there's a previous conflicting build directory""" diff --git a/src/pip/_internal/vcs/subversion.py b/src/pip/_internal/vcs/subversion.py index a57e19751fa..2575872a7b3 100644 --- a/src/pip/_internal/vcs/subversion.py +++ b/src/pip/_internal/vcs/subversion.py @@ -25,7 +25,7 @@ if MYPY_CHECK_RUNNING: - from typing import Optional, Tuple + from typing import Optional, Text, Tuple from pip._internal.utils.misc import HiddenText from pip._internal.utils.subprocess import CommandArgs @@ -218,7 +218,17 @@ def call_vcs_version(self): # svn, version 1.12.0-SlikSvn (SlikSvn/1.12.0) # compiled May 28 2019, 13:44:56 on x86_64-microsoft-windows6.2 version_prefix = 'svn, version ' - version = self.run_command(['--version'], show_stdout=True) + cmd_output = self.run_command(['--version'], show_stdout=False) + + # Split the output by newline, and find the first line where + # version_prefix is present + output_lines = cmd_output.split('\n') + version = '' # type: Text + + for line in output_lines: + if version_prefix in line: + version = line + break if not version.startswith(version_prefix): return () diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index a4995c88f78..1d9cb08feb3 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -6,15 +6,13 @@ import logging import os import shutil -import subprocess import sys from pip._vendor import pkg_resources from pip._vendor.six.moves.urllib import parse as urllib_parse -from pip._internal.exceptions import BadCommand, InstallationError, SubProcessError -from pip._internal.utils.compat import console_to_str, samefile -from pip._internal.utils.logging import subprocess_logger +from pip._internal.exceptions import BadCommand, InstallationError +from pip._internal.utils.compat import samefile from pip._internal.utils.misc import ( ask_path_exists, backup_dir, @@ -23,21 +21,18 @@ hide_value, rmtree, ) -from pip._internal.utils.subprocess import ( - format_command_args, - make_command, - make_subprocess_output_error, - reveal_command_args, -) +from pip._internal.utils.subprocess import call_subprocess, make_command from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.urls import get_url_scheme if MYPY_CHECK_RUNNING: from typing import ( + Any, Dict, Iterable, Iterator, List, + Mapping, Optional, Text, Tuple, @@ -45,6 +40,7 @@ Union, ) + from pip._internal.cli.spinners import SpinnerInterface from pip._internal.utils.misc import HiddenText from pip._internal.utils.subprocess import CommandArgs @@ -85,127 +81,6 @@ def make_vcs_requirement_url(repo_url, rev, project_name, subdir=None): return req -def call_subprocess( - cmd, # type: Union[List[str], CommandArgs] - show_stdout=False, # type: bool - cwd=None, # type: Optional[str] - on_returncode='raise', # type: str - extra_ok_returncodes=None, # type: Optional[Iterable[int]] - log_failed_cmd=True # type: Optional[bool] -): - # type: (...) -> Text - """ - Args: - show_stdout: if true, use INFO to log the subprocess's stderr and - stdout streams. Otherwise, use DEBUG. Defaults to False. - extra_ok_returncodes: an iterable of integer return codes that are - acceptable, in addition to 0. Defaults to None, which means []. - log_failed_cmd: if false, failed commands are not logged, - only raised. - """ - if extra_ok_returncodes is None: - extra_ok_returncodes = [] - # Most places in pip use show_stdout=False. - # What this means is-- - # - # - We log this output of stdout and stderr at DEBUG level - # as it is received. - # - If DEBUG logging isn't enabled (e.g. if --verbose logging wasn't - # requested), then we show a spinner so the user can still see the - # subprocess is in progress. - # - If the subprocess exits with an error, we log the output to stderr - # at ERROR level if it hasn't already been displayed to the console - # (e.g. if --verbose logging wasn't enabled). This way we don't log - # the output to the console twice. - # - # If show_stdout=True, then the above is still done, but with DEBUG - # replaced by INFO. - if show_stdout: - # Then log the subprocess output at INFO level. - log_subprocess = subprocess_logger.info - used_level = logging.INFO - else: - # Then log the subprocess output using DEBUG. This also ensures - # it will be logged to the log file (aka user_log), if enabled. - log_subprocess = subprocess_logger.debug - used_level = logging.DEBUG - - # Whether the subprocess will be visible in the console. - showing_subprocess = subprocess_logger.getEffectiveLevel() <= used_level - - command_desc = format_command_args(cmd) - try: - proc = subprocess.Popen( - # Convert HiddenText objects to the underlying str. - reveal_command_args(cmd), - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - cwd=cwd - ) - if proc.stdin: - proc.stdin.close() - except Exception as exc: - if log_failed_cmd: - subprocess_logger.critical( - "Error %s while executing command %s", exc, command_desc, - ) - raise - all_output = [] - while True: - # The "line" value is a unicode string in Python 2. - line = None - if proc.stdout: - line = console_to_str(proc.stdout.readline()) - if not line: - break - line = line.rstrip() - all_output.append(line + '\n') - - # Show the line immediately. - log_subprocess(line) - try: - proc.wait() - finally: - if proc.stdout: - proc.stdout.close() - if proc.stderr: - proc.stderr.close() - - proc_had_error = ( - proc.returncode and proc.returncode not in extra_ok_returncodes - ) - if proc_had_error: - if on_returncode == 'raise': - if not showing_subprocess and log_failed_cmd: - # Then the subprocess streams haven't been logged to the - # console yet. - msg = make_subprocess_output_error( - cmd_args=cmd, - cwd=cwd, - lines=all_output, - exit_status=proc.returncode, - ) - subprocess_logger.error(msg) - exc_msg = ( - 'Command errored out with exit status {}: {} ' - 'Check the logs for full command output.' - ).format(proc.returncode, command_desc) - raise SubProcessError(exc_msg) - elif on_returncode == 'warn': - subprocess_logger.warning( - 'Command "%s" had error code %s in %s', - command_desc, - proc.returncode, - cwd, - ) - elif on_returncode == 'ignore': - pass - else: - raise ValueError('Invalid value: on_returncode={!r}'.format( - on_returncode)) - return ''.join(all_output) - - def find_path_to_setup_from_repo_root(location, repo_root): # type: (str, str) -> Optional[str] """ @@ -798,6 +673,9 @@ def run_command( cwd=None, # type: Optional[str] on_returncode='raise', # type: str extra_ok_returncodes=None, # type: Optional[Iterable[int]] + command_desc=None, # type: Optional[str] + extra_environ=None, # type: Optional[Mapping[str, Any]] + spinner=None, # type: Optional[SpinnerInterface] log_failed_cmd=True # type: bool ): # type: (...) -> Text @@ -811,6 +689,10 @@ def run_command( return call_subprocess(cmd, show_stdout, cwd, on_returncode=on_returncode, extra_ok_returncodes=extra_ok_returncodes, + command_desc=command_desc, + extra_environ=extra_environ, + unset_environ=cls.unset_environ, + spinner=spinner, log_failed_cmd=log_failed_cmd) except OSError as e: # errno.ENOENT = no such file or directory From 8665a3e1e2c6d95802ff351eb66fa59e0939e820 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Mon, 21 Dec 2020 13:05:16 +0100 Subject: [PATCH 2846/3170] Revert "Improve check for svn version string" This reverts commit 1471897b84b43c467c753b5edebe636f835afc6a. --- src/pip/_internal/vcs/subversion.py | 15 ++------------- 1 file changed, 2 insertions(+), 13 deletions(-) diff --git a/src/pip/_internal/vcs/subversion.py b/src/pip/_internal/vcs/subversion.py index 2575872a7b3..b84108efc52 100644 --- a/src/pip/_internal/vcs/subversion.py +++ b/src/pip/_internal/vcs/subversion.py @@ -25,7 +25,7 @@ if MYPY_CHECK_RUNNING: - from typing import Optional, Text, Tuple + from typing import Optional, Tuple from pip._internal.utils.misc import HiddenText from pip._internal.utils.subprocess import CommandArgs @@ -218,18 +218,7 @@ def call_vcs_version(self): # svn, version 1.12.0-SlikSvn (SlikSvn/1.12.0) # compiled May 28 2019, 13:44:56 on x86_64-microsoft-windows6.2 version_prefix = 'svn, version ' - cmd_output = self.run_command(['--version'], show_stdout=False) - - # Split the output by newline, and find the first line where - # version_prefix is present - output_lines = cmd_output.split('\n') - version = '' # type: Text - - for line in output_lines: - if version_prefix in line: - version = line - break - + version = self.run_command(['--version'], show_stdout=False) if not version.startswith(version_prefix): return () From 19039c27e99349ef627aef55280158baef557e0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Mon, 21 Dec 2020 15:07:48 +0100 Subject: [PATCH 2847/3170] Revert "Bubble up SubProcessError to basecommand._main" This reverts commit e9f738a3daec91b131ae985e16809d47b1cfdaff. --- src/pip/_internal/cli/base_command.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index 7f05efb85db..41e7dcf101b 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -27,7 +27,6 @@ InstallationError, NetworkConnectionError, PreviousBuildDirError, - SubProcessError, UninstallationError, ) from pip._internal.utils.deprecation import deprecated @@ -230,7 +229,7 @@ def _main(self, args): return PREVIOUS_BUILD_DIR_ERROR except (InstallationError, UninstallationError, BadCommand, - SubProcessError, NetworkConnectionError) as exc: + NetworkConnectionError) as exc: logger.critical(str(exc)) logger.debug('Exception information:', exc_info=True) From bcc412c44aa59b5dd217dcd5b741bf2efec0962c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Mon, 21 Dec 2020 13:33:46 +0100 Subject: [PATCH 2848/3170] Additional revert of 7969 Revert additional changes that were made after 7969 and depended on it. --- src/pip/_internal/vcs/git.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/vcs/git.py b/src/pip/_internal/vcs/git.py index a6d1396fc77..de9c7f51981 100644 --- a/src/pip/_internal/vcs/git.py +++ b/src/pip/_internal/vcs/git.py @@ -11,7 +11,7 @@ from pip._vendor.six.moves.urllib import parse as urllib_parse from pip._vendor.six.moves.urllib import request as urllib_request -from pip._internal.exceptions import BadCommand, InstallationError, SubProcessError +from pip._internal.exceptions import BadCommand, InstallationError from pip._internal.utils.misc import display_path, hide_url from pip._internal.utils.subprocess import make_command from pip._internal.utils.temp_dir import TempDirectory @@ -332,9 +332,11 @@ def has_commit(cls, location, rev): """ try: cls.run_command( - ['rev-parse', '-q', '--verify', "sha^" + rev], cwd=location + ['rev-parse', '-q', '--verify', "sha^" + rev], + cwd=location, + log_failed_cmd=False, ) - except SubProcessError: + except InstallationError: return False else: return True From 139293b5e2541df764f7760f790f42cdb0d48621 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Mon, 21 Dec 2020 14:39:50 +0100 Subject: [PATCH 2849/3170] add stdout_only to call_subprocess --- src/pip/_internal/utils/subprocess.py | 69 ++++++++++++++++++--------- 1 file changed, 47 insertions(+), 22 deletions(-) diff --git a/src/pip/_internal/utils/subprocess.py b/src/pip/_internal/utils/subprocess.py index 605e711e603..3c79c7289b8 100644 --- a/src/pip/_internal/utils/subprocess.py +++ b/src/pip/_internal/utils/subprocess.py @@ -118,7 +118,8 @@ def call_subprocess( extra_environ=None, # type: Optional[Mapping[str, Any]] unset_environ=None, # type: Optional[Iterable[str]] spinner=None, # type: Optional[SpinnerInterface] - log_failed_cmd=True # type: Optional[bool] + log_failed_cmd=True, # type: Optional[bool] + stdout_only=False, # type: Optional[bool] ): # type: (...) -> Text """ @@ -130,6 +131,9 @@ def call_subprocess( unset_environ: an iterable of environment variable names to unset prior to calling subprocess.Popen(). log_failed_cmd: if false, failed commands are not logged, only raised. + stdout_only: if true, return only stdout, else return both. When true, + logging of both stdout and stderr occurs when the subprocess has + terminated, else logging occurs as subprocess output is produced. """ if extra_ok_returncodes is None: extra_ok_returncodes = [] @@ -180,8 +184,11 @@ def call_subprocess( proc = subprocess.Popen( # Convert HiddenText objects to the underlying str. reveal_command_args(cmd), - stderr=subprocess.STDOUT, stdin=subprocess.PIPE, - stdout=subprocess.PIPE, cwd=cwd, env=env, + stdin=subprocess.PIPE, + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT if not stdout_only else subprocess.PIPE, + cwd=cwd, + env=env, ) assert proc.stdin assert proc.stdout @@ -193,25 +200,43 @@ def call_subprocess( ) raise all_output = [] - while True: - # The "line" value is a unicode string in Python 2. - line = console_to_str(proc.stdout.readline()) - if not line: - break - line = line.rstrip() - all_output.append(line + '\n') + if not stdout_only: + # In this mode, stdout and stderr are in the same pip. + while True: + # The "line" value is a unicode string in Python 2. + line = console_to_str(proc.stdout.readline()) + if not line: + break + line = line.rstrip() + all_output.append(line + '\n') + + # Show the line immediately. + log_subprocess(line) + # Update the spinner. + if use_spinner: + assert spinner + spinner.spin() + try: + proc.wait() + finally: + if proc.stdout: + proc.stdout.close() + output = ''.join(all_output) + else: + # In this mode, stdout and stderr are in different pipes. + # We must use the communicate which is the only safe way to read both. + out_bytes, err_bytes = proc.communicate() + # log line by line to preserve pip log indenting + out = console_to_str(out_bytes) + for out_line in out.splitlines(): + log_subprocess(out_line) + all_output.append(out) + err = console_to_str(err_bytes) + for err_line in err.splitlines(): + log_subprocess(err_line) + all_output.append(err) + output = out - # Show the line immediately. - log_subprocess(line) - # Update the spinner. - if use_spinner: - assert spinner - spinner.spin() - try: - proc.wait() - finally: - if proc.stdout: - proc.stdout.close() proc_had_error = ( proc.returncode and proc.returncode not in extra_ok_returncodes ) @@ -250,7 +275,7 @@ def call_subprocess( else: raise ValueError('Invalid value: on_returncode={!r}'.format( on_returncode)) - return ''.join(all_output) + return output def runner_with_spinner_message(message): From 74369e860e5b6d1ea6fed4144cb9b506287990ec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Mon, 21 Dec 2020 14:46:21 +0100 Subject: [PATCH 2850/3170] vcs: capture subprocess stdout only --- news/8876.bugfix.rst | 3 +++ src/pip/_internal/utils/subprocess.py | 10 +++---- src/pip/_internal/vcs/bazaar.py | 6 +++-- src/pip/_internal/vcs/git.py | 35 ++++++++++++++++++++----- src/pip/_internal/vcs/mercurial.py | 19 +++++++++++--- src/pip/_internal/vcs/subversion.py | 5 +++- src/pip/_internal/vcs/versioncontrol.py | 6 +++-- 7 files changed, 63 insertions(+), 21 deletions(-) create mode 100644 news/8876.bugfix.rst diff --git a/news/8876.bugfix.rst b/news/8876.bugfix.rst new file mode 100644 index 00000000000..98250dc9745 --- /dev/null +++ b/news/8876.bugfix.rst @@ -0,0 +1,3 @@ +Fixed hanging VCS subprocess calls when the VCS outputs a large amount of data +on stderr. Restored logging of VCS errors that was inadvertently removed in pip +20.2. diff --git a/src/pip/_internal/utils/subprocess.py b/src/pip/_internal/utils/subprocess.py index 3c79c7289b8..a75bc68754b 100644 --- a/src/pip/_internal/utils/subprocess.py +++ b/src/pip/_internal/utils/subprocess.py @@ -190,9 +190,6 @@ def call_subprocess( cwd=cwd, env=env, ) - assert proc.stdin - assert proc.stdout - proc.stdin.close() except Exception as exc: if log_failed_cmd: subprocess_logger.critical( @@ -201,7 +198,10 @@ def call_subprocess( raise all_output = [] if not stdout_only: - # In this mode, stdout and stderr are in the same pip. + assert proc.stdout + assert proc.stdin + proc.stdin.close() + # In this mode, stdout and stderr are in the same pipe. while True: # The "line" value is a unicode string in Python 2. line = console_to_str(proc.stdout.readline()) @@ -224,7 +224,7 @@ def call_subprocess( output = ''.join(all_output) else: # In this mode, stdout and stderr are in different pipes. - # We must use the communicate which is the only safe way to read both. + # We must use communicate() which is the only safe way to read both. out_bytes, err_bytes = proc.communicate() # log line by line to preserve pip log indenting out = console_to_str(out_bytes) diff --git a/src/pip/_internal/vcs/bazaar.py b/src/pip/_internal/vcs/bazaar.py index efe524492af..4a63d6faa5c 100644 --- a/src/pip/_internal/vcs/bazaar.py +++ b/src/pip/_internal/vcs/bazaar.py @@ -93,7 +93,9 @@ def get_url_rev_and_auth(cls, url): @classmethod def get_remote_url(cls, location): - urls = cls.run_command(['info'], show_stdout=False, cwd=location) + urls = cls.run_command( + ['info'], show_stdout=False, stdout_only=True, cwd=location + ) for line in urls.splitlines(): line = line.strip() for x in ('checkout of branch: ', @@ -108,7 +110,7 @@ def get_remote_url(cls, location): @classmethod def get_revision(cls, location): revision = cls.run_command( - ['revno'], show_stdout=False, cwd=location, + ['revno'], show_stdout=False, stdout_only=True, cwd=location, ) return revision.splitlines()[-1] diff --git a/src/pip/_internal/vcs/git.py b/src/pip/_internal/vcs/git.py index de9c7f51981..565961a0631 100644 --- a/src/pip/_internal/vcs/git.py +++ b/src/pip/_internal/vcs/git.py @@ -79,7 +79,9 @@ def is_immutable_rev_checkout(self, url, dest): def get_git_version(self): VERSION_PFX = 'git version ' - version = self.run_command(['version'], show_stdout=False) + version = self.run_command( + ['version'], show_stdout=False, stdout_only=True + ) if version.startswith(VERSION_PFX): version = version[len(VERSION_PFX):].split()[0] else: @@ -102,7 +104,11 @@ def get_current_branch(cls, location): # and to suppress the message to stderr. args = ['symbolic-ref', '-q', 'HEAD'] output = cls.run_command( - args, extra_ok_returncodes=(1, ), show_stdout=False, cwd=location, + args, + extra_ok_returncodes=(1, ), + show_stdout=False, + stdout_only=True, + cwd=location, ) ref = output.strip() @@ -135,8 +141,13 @@ def get_revision_sha(cls, dest, rev): rev: the revision name. """ # Pass rev to pre-filter the list. - output = cls.run_command(['show-ref', rev], cwd=dest, - show_stdout=False, on_returncode='ignore') + output = cls.run_command( + ['show-ref', rev], + cwd=dest, + show_stdout=False, + stdout_only=True, + on_returncode='ignore', + ) refs = {} for line in output.strip().splitlines(): try: @@ -310,7 +321,10 @@ def get_remote_url(cls, location): # exits with return code 1 if there are no matching lines. stdout = cls.run_command( ['config', '--get-regexp', r'remote\..*\.url'], - extra_ok_returncodes=(1, ), show_stdout=False, cwd=location, + extra_ok_returncodes=(1, ), + show_stdout=False, + stdout_only=True, + cwd=location, ) remotes = stdout.splitlines() try: @@ -346,7 +360,10 @@ def get_revision(cls, location, rev=None): if rev is None: rev = 'HEAD' current_rev = cls.run_command( - ['rev-parse', rev], show_stdout=False, cwd=location, + ['rev-parse', rev], + show_stdout=False, + stdout_only=True, + cwd=location, ) return current_rev.strip() @@ -359,7 +376,10 @@ def get_subdirectory(cls, location): # find the repo root git_dir = cls.run_command( ['rev-parse', '--git-dir'], - show_stdout=False, cwd=location).strip() + show_stdout=False, + stdout_only=True, + cwd=location, + ).strip() if not os.path.isabs(git_dir): git_dir = os.path.join(location, git_dir) repo_root = os.path.abspath(os.path.join(git_dir, '..')) @@ -418,6 +438,7 @@ def get_repository_root(cls, location): ['rev-parse', '--show-toplevel'], cwd=location, show_stdout=False, + stdout_only=True, on_returncode='raise', log_failed_cmd=False, ) diff --git a/src/pip/_internal/vcs/mercurial.py b/src/pip/_internal/vcs/mercurial.py index 75e903cc8a6..d2d145f623f 100644 --- a/src/pip/_internal/vcs/mercurial.py +++ b/src/pip/_internal/vcs/mercurial.py @@ -92,7 +92,10 @@ def update(self, dest, url, rev_options): def get_remote_url(cls, location): url = cls.run_command( ['showconfig', 'paths.default'], - show_stdout=False, cwd=location).strip() + show_stdout=False, + stdout_only=True, + cwd=location, + ).strip() if cls._is_local_repository(url): url = path_to_url(url) return url.strip() @@ -104,7 +107,10 @@ def get_revision(cls, location): """ current_revision = cls.run_command( ['parents', '--template={rev}'], - show_stdout=False, cwd=location).strip() + show_stdout=False, + stdout_only=True, + cwd=location, + ).strip() return current_revision @classmethod @@ -115,7 +121,10 @@ def get_requirement_revision(cls, location): """ current_rev_hash = cls.run_command( ['parents', '--template={node}'], - show_stdout=False, cwd=location).strip() + show_stdout=False, + stdout_only=True, + cwd=location, + ).strip() return current_rev_hash @classmethod @@ -131,7 +140,8 @@ def get_subdirectory(cls, location): """ # find the repo root repo_root = cls.run_command( - ['root'], show_stdout=False, cwd=location).strip() + ['root'], show_stdout=False, stdout_only=True, cwd=location + ).strip() if not os.path.isabs(repo_root): repo_root = os.path.abspath(os.path.join(location, repo_root)) return find_path_to_setup_from_repo_root(location, repo_root) @@ -146,6 +156,7 @@ def get_repository_root(cls, location): ['root'], cwd=location, show_stdout=False, + stdout_only=True, on_returncode='raise', log_failed_cmd=False, ) diff --git a/src/pip/_internal/vcs/subversion.py b/src/pip/_internal/vcs/subversion.py index b84108efc52..701f41db4b2 100644 --- a/src/pip/_internal/vcs/subversion.py +++ b/src/pip/_internal/vcs/subversion.py @@ -167,6 +167,7 @@ def _get_svn_url_rev(cls, location): xml = cls.run_command( ['info', '--xml', location], show_stdout=False, + stdout_only=True, ) url = _svn_info_xml_url_re.search(xml).group(1) revs = [ @@ -218,7 +219,9 @@ def call_vcs_version(self): # svn, version 1.12.0-SlikSvn (SlikSvn/1.12.0) # compiled May 28 2019, 13:44:56 on x86_64-microsoft-windows6.2 version_prefix = 'svn, version ' - version = self.run_command(['--version'], show_stdout=False) + version = self.run_command( + ['--version'], show_stdout=False, stdout_only=True + ) if not version.startswith(version_prefix): return () diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index 1d9cb08feb3..0e807a2fb06 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -676,7 +676,8 @@ def run_command( command_desc=None, # type: Optional[str] extra_environ=None, # type: Optional[Mapping[str, Any]] spinner=None, # type: Optional[SpinnerInterface] - log_failed_cmd=True # type: bool + log_failed_cmd=True, # type: bool + stdout_only=False, # type: bool ): # type: (...) -> Text """ @@ -693,7 +694,8 @@ def run_command( extra_environ=extra_environ, unset_environ=cls.unset_environ, spinner=spinner, - log_failed_cmd=log_failed_cmd) + log_failed_cmd=log_failed_cmd, + stdout_only=stdout_only) except OSError as e: # errno.ENOENT = no such file or directory # In other words, the VCS executable isn't available From 570a45ae711f7b86a99e3023207c4d263c57f908 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Tue, 22 Dec 2020 00:16:42 +0100 Subject: [PATCH 2851/3170] Add test for call_subprocess stdout_only --- tests/unit/test_utils_subprocess.py | 30 +++++++++++++++++++++++++++++ 1 file changed, 30 insertions(+) diff --git a/tests/unit/test_utils_subprocess.py b/tests/unit/test_utils_subprocess.py index b0de2bf578d..47c890050b0 100644 --- a/tests/unit/test_utils_subprocess.py +++ b/tests/unit/test_utils_subprocess.py @@ -14,6 +14,7 @@ format_command_args, make_command, make_subprocess_output_error, + subprocess_logger, ) @@ -154,6 +155,35 @@ def test_make_subprocess_output_error__non_ascii_line(): assert actual == expected, u'actual: {}'.format(actual) +@pytest.mark.parametrize( + ('stdout_only', 'expected'), + [ + (True, ("out\n", "out\r\n")), + (False, ("out\nerr\n", "out\r\nerr\r\n", "err\nout\n", "err\r\nout\r\n")), + ], +) +def test_call_subprocess_stdout_only(capfd, monkeypatch, stdout_only, expected): + log = [] + monkeypatch.setattr(subprocess_logger, "debug", lambda *args: log.append(args[0])) + out = call_subprocess( + [ + sys.executable, + "-c", + "import sys; " + "sys.stdout.write('out\\n'); " + "sys.stderr.write('err\\n')" + ], + stdout_only=stdout_only, + ) + assert out in expected + captured = capfd.readouterr() + assert captured.err == "" + assert ( + log == ["Running command %s", "out", "err"] + or log == ["Running command %s", "err", "out"] + ) + + class FakeSpinner(SpinnerInterface): def __init__(self): From c115cdc81a3b94397be69534d196019263e6762a Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Sat, 26 Dec 2020 08:31:00 -0800 Subject: [PATCH 2852/3170] Remove unnecessary uses of six.ensure_(binary|str|text) Now that Python 2 is not supported, the bytes/str boundaries are more clear and explicit. Using six.ensure_* methods for backwards compatibility is no longer necessary as the types are known and verified using mypy. One exception is tests/lib/wheel.py which allows tests to pass test setup data as either bytes or str. The module operations.install.wheel also remains untouched as it is especially delicate to bytes/str mixups and the current version is working. --- ...35a1-abe3-4532-8add-bf7491b0eea5.bugfix.rst | 0 src/pip/_internal/network/session.py | 12 +++--------- src/pip/_internal/req/req_install.py | 2 +- src/pip/_internal/self_outdated_check.py | 5 ++--- src/pip/_internal/utils/pkg_resources.py | 3 +-- src/pip/_internal/utils/temp_dir.py | 12 +----------- src/pip/_internal/utils/wheel.py | 18 ++++-------------- tests/lib/__init__.py | 3 +-- tests/lib/test_wheel.py | 4 +--- tests/lib/wheel.py | 16 +++++++++++----- tests/unit/test_utils_pkg_resources.py | 5 +---- 11 files changed, 26 insertions(+), 54 deletions(-) create mode 100644 news/bca635a1-abe3-4532-8add-bf7491b0eea5.bugfix.rst diff --git a/news/bca635a1-abe3-4532-8add-bf7491b0eea5.bugfix.rst b/news/bca635a1-abe3-4532-8add-bf7491b0eea5.bugfix.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py index 5021b8eefaa..46f300f911f 100644 --- a/src/pip/_internal/network/session.py +++ b/src/pip/_internal/network/session.py @@ -16,7 +16,7 @@ import urllib.parse import warnings -from pip._vendor import requests, six, urllib3 +from pip._vendor import requests, urllib3 from pip._vendor.cachecontrol import CacheControlAdapter from pip._vendor.requests.adapters import BaseAdapter, HTTPAdapter from pip._vendor.requests.models import Response @@ -367,14 +367,8 @@ def is_secure_origin(self, location): continue try: - addr = ipaddress.ip_address( - None - if origin_host is None - else six.ensure_text(origin_host) - ) - network = ipaddress.ip_network( - six.ensure_text(secure_host) - ) + addr = ipaddress.ip_address(origin_host) + network = ipaddress.ip_network(secure_host) except ValueError: # We don't have both a valid address or a valid network, so # we'll check this origin against hostnames. diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 0bf00132036..b453cb73ba3 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -255,7 +255,7 @@ def name(self): # type: () -> Optional[str] if self.req is None: return None - return six.ensure_str(pkg_resources.safe_name(self.req.name)) + return pkg_resources.safe_name(self.req.name) @property def specifier(self): diff --git a/src/pip/_internal/self_outdated_check.py b/src/pip/_internal/self_outdated_check.py index c22f06afe87..f1ad241688e 100644 --- a/src/pip/_internal/self_outdated_check.py +++ b/src/pip/_internal/self_outdated_check.py @@ -6,7 +6,6 @@ import sys from pip._vendor.packaging import version as packaging_version -from pip._vendor.six import ensure_binary from pip._internal.index.collector import LinkCollector from pip._internal.index.package_finder import PackageFinder @@ -31,7 +30,7 @@ def _get_statefile_name(key): # type: (str) -> str - key_bytes = ensure_binary(key) + key_bytes = key.encode() name = hashlib.sha224(key_bytes).hexdigest() return name @@ -85,7 +84,7 @@ def save(self, pypi_version, current_time): text = json.dumps(state, sort_keys=True, separators=(",", ":")) with adjacent_tmp_file(self.statefile_path) as f: - f.write(ensure_binary(text)) + f.write(text.encode()) try: # Since we have a prefix-specific state file, we can just diff --git a/src/pip/_internal/utils/pkg_resources.py b/src/pip/_internal/utils/pkg_resources.py index d5b26f53895..816ac122369 100644 --- a/src/pip/_internal/utils/pkg_resources.py +++ b/src/pip/_internal/utils/pkg_resources.py @@ -1,5 +1,4 @@ from pip._vendor.pkg_resources import yield_lines -from pip._vendor.six import ensure_str from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -21,7 +20,7 @@ def has_metadata(self, name): def get_metadata(self, name): # type: (str) -> str try: - return ensure_str(self._metadata[name]) + return self._metadata[name].decode() except UnicodeDecodeError as e: # Mirrors handling done in pkg_resources.NullProvider. e.reason += f" in {name} file" diff --git a/src/pip/_internal/utils/temp_dir.py b/src/pip/_internal/utils/temp_dir.py index 91b277df6ec..c7fca502b50 100644 --- a/src/pip/_internal/utils/temp_dir.py +++ b/src/pip/_internal/utils/temp_dir.py @@ -6,9 +6,7 @@ from contextlib import contextmanager from pip._vendor.contextlib2 import ExitStack -from pip._vendor.six import ensure_text -from pip._internal.utils.compat import WINDOWS from pip._internal.utils.misc import enum, rmtree from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -196,15 +194,7 @@ def cleanup(self): self._deleted = True if not os.path.exists(self._path): return - # Make sure to pass unicode on Python 2 to make the contents also - # use unicode, ensuring non-ASCII names and can be represented. - # This is only done on Windows because POSIX platforms use bytes - # natively for paths, and the bytes-text conversion omission avoids - # errors caused by the environment configuring encodings incorrectly. - if WINDOWS: - rmtree(ensure_text(self._path)) - else: - rmtree(self._path) + rmtree(self._path) class AdjacentTempDirectory(TempDirectory): diff --git a/src/pip/_internal/utils/wheel.py b/src/pip/_internal/utils/wheel.py index 2c01cf9927b..a02fbee1813 100644 --- a/src/pip/_internal/utils/wheel.py +++ b/src/pip/_internal/utils/wheel.py @@ -7,7 +7,6 @@ from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.pkg_resources import DistInfoDistribution -from pip._vendor.six import ensure_str from pip._internal.exceptions import UnsupportedWheel from pip._internal.utils.pkg_resources import DictMetadata @@ -62,17 +61,10 @@ def pkg_resources_distribution_for_wheel(wheel_zip, name, location): metadata_text = {} # type: Dict[str, bytes] for path in metadata_files: - # If a flag is set, namelist entries may be unicode in Python 2. - # We coerce them to native str type to match the types used in the rest - # of the code. This cannot fail because unicode can always be encoded - # with UTF-8. - full_path = ensure_str(path) - _, metadata_name = full_path.split("/", 1) + _, metadata_name = path.split("/", 1) try: - metadata_text[metadata_name] = read_wheel_metadata_file( - wheel_zip, full_path - ) + metadata_text[metadata_name] = read_wheel_metadata_file(wheel_zip, path) except UnsupportedWheel as e: raise UnsupportedWheel( "{} has an invalid wheel, {}".format(name, str(e)) @@ -139,9 +131,7 @@ def wheel_dist_info_dir(source, name): ) ) - # Zip file paths can be unicode or str depending on the zip entry flags, - # so normalize it. - return ensure_str(info_dir) + return info_dir def read_wheel_metadata_file(source, path): @@ -166,7 +156,7 @@ def wheel_metadata(source, dist_info_dir): wheel_contents = read_wheel_metadata_file(source, path) try: - wheel_text = ensure_str(wheel_contents) + wheel_text = wheel_contents.decode() except UnicodeDecodeError as e: raise UnsupportedWheel(f"error decoding {path!r}: {e!r}") diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index 6a98d4acf78..520a0ca2e88 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -13,7 +13,6 @@ from zipfile import ZipFile import pytest -from pip._vendor.six import ensure_binary from scripttest import FoundDir, TestFileEnvironment from pip._internal.index.collector import LinkCollector @@ -1095,7 +1094,7 @@ def create_basic_sdist_for_package( for fname in files: path = script.temp_path / fname path.parent.mkdir(exist_ok=True, parents=True) - path.write_bytes(ensure_binary(files[fname])) + path.write_bytes(files[fname].encode("utf-8")) retval = script.scratch_path / archive_name generated = shutil.make_archive( diff --git a/tests/lib/test_wheel.py b/tests/lib/test_wheel.py index 15e5a75fe1e..31d918abd73 100644 --- a/tests/lib/test_wheel.py +++ b/tests/lib/test_wheel.py @@ -5,8 +5,6 @@ from functools import partial from zipfile import ZipFile -from pip._vendor.six import ensure_text - from pip._internal.utils.typing import MYPY_CHECK_RUNNING from tests.lib.wheel import ( _default, @@ -164,7 +162,7 @@ def test_make_wheel_default_record(): extra_data_files={"purelib/info.txt": "c"}, ).as_zipfile() as z: record_bytes = z.read("simple-0.1.0.dist-info/RECORD") - record_text = ensure_text(record_bytes) + record_text = record_bytes.decode() record_rows = list(csv.reader(record_text.splitlines())) records = { row[0]: row[1:] for row in record_rows diff --git a/tests/lib/wheel.py b/tests/lib/wheel.py index f96f5d06eb7..5872912b258 100644 --- a/tests/lib/wheel.py +++ b/tests/lib/wheel.py @@ -13,7 +13,6 @@ import csv23 from pip._vendor.requests.structures import CaseInsensitiveDict -from pip._vendor.six import ensure_binary, ensure_text from pip._internal.utils.typing import MYPY_CHECK_RUNNING from tests.lib.path import Path @@ -61,6 +60,13 @@ class Defaulted(Union[Default, T]): pass +def ensure_binary(value): + # type: (AnyStr) -> bytes + if isinstance(value, bytes): + return value + return value.encode() + + def message_from_dict(headers): # type: (Dict[str, HeaderValue]) -> Message """Plain key-value pairs are set in the returned message. @@ -110,7 +116,7 @@ def make_metadata_file( if body is not _default: message.set_payload(body) - return File(path, ensure_binary(message_from_dict(metadata).as_string())) + return File(path, message_from_dict(metadata).as_bytes()) def make_wheel_metadata_file( @@ -139,7 +145,7 @@ def make_wheel_metadata_file( if updates is not _default: metadata.update(updates) - return File(path, ensure_binary(message_from_dict(metadata).as_string())) + return File(path, message_from_dict(metadata).as_bytes()) def make_entry_points_file( @@ -167,7 +173,7 @@ def make_entry_points_file( return File( dist_info_path(name, version, "entry_points.txt"), - ensure_binary("\n".join(lines)), + "\n".join(lines).encode(), ) @@ -243,7 +249,7 @@ def record_file_maker_wrapper( with StringIO(newline="") as buf: writer = csv23.writer(buf) for record in records: - writer.writerow(map(ensure_text, record)) + writer.writerow(record) contents = buf.getvalue().encode("utf-8") yield File(record_path, contents) diff --git a/tests/unit/test_utils_pkg_resources.py b/tests/unit/test_utils_pkg_resources.py index ae7357ba1cc..7cc95b91b4f 100644 --- a/tests/unit/test_utils_pkg_resources.py +++ b/tests/unit/test_utils_pkg_resources.py @@ -2,7 +2,6 @@ import pytest from pip._vendor.pkg_resources import DistInfoDistribution, Requirement -from pip._vendor.six import ensure_binary from pip._internal.utils.packaging import get_metadata, get_requires_python from pip._internal.utils.pkg_resources import DictMetadata @@ -26,9 +25,7 @@ def test_dict_metadata_works(): metadata["Provides-Extra"] = extra metadata["Requires-Python"] = requires_python - inner_metadata = DictMetadata({ - "METADATA": ensure_binary(metadata.as_string()) - }) + inner_metadata = DictMetadata({"METADATA": metadata.as_bytes()}) dist = DistInfoDistribution( location="<in-memory>", metadata=inner_metadata, project_name=name ) From 14380668b5fa0bc5daf49f19efd45928865a7f3f Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Sun, 27 Dec 2020 08:08:33 -0800 Subject: [PATCH 2853/3170] Drop csv23 in favor of stdlib csv Using csv23 has been unnecessary since dropping Python 2 support. It previously remained as a compatibility shim. --- news/43602ba6-8a59-425c-9a97-9c8e87e28ddb.trivial.rst | 0 tests/lib/wheel.py | 4 ++-- tools/requirements/tests.txt | 1 - 3 files changed, 2 insertions(+), 3 deletions(-) create mode 100644 news/43602ba6-8a59-425c-9a97-9c8e87e28ddb.trivial.rst diff --git a/news/43602ba6-8a59-425c-9a97-9c8e87e28ddb.trivial.rst b/news/43602ba6-8a59-425c-9a97-9c8e87e28ddb.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/lib/wheel.py b/tests/lib/wheel.py index f96f5d06eb7..a7403bd7f58 100644 --- a/tests/lib/wheel.py +++ b/tests/lib/wheel.py @@ -1,5 +1,6 @@ """Helper for building wheels as would be in test cases. """ +import csv import itertools from base64 import urlsafe_b64encode from collections import namedtuple @@ -11,7 +12,6 @@ from io import BytesIO, StringIO from zipfile import ZipFile -import csv23 from pip._vendor.requests.structures import CaseInsensitiveDict from pip._vendor.six import ensure_binary, ensure_text @@ -241,7 +241,7 @@ def record_file_maker_wrapper( records = record_callback(records) with StringIO(newline="") as buf: - writer = csv23.writer(buf) + writer = csv.writer(buf) for record in records: writer.writerow(map(ensure_text, record)) contents = buf.getvalue().encode("utf-8") diff --git a/tools/requirements/tests.txt b/tools/requirements/tests.txt index ef87225d6c4..28022b36769 100644 --- a/tools/requirements/tests.txt +++ b/tools/requirements/tests.txt @@ -1,6 +1,5 @@ --use-feature=2020-resolver cryptography==2.8 -csv23 enum34; python_version < '3.4' freezegun mock From 3cfb0ca1e81c68c1eb2ebc25f568513d0611a47b Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Sun, 27 Dec 2020 08:10:20 -0800 Subject: [PATCH 2854/3170] Drop enum34 in favor of stdlib enum Using enum34 has been unnecessary since dropping Python 2 support. It previously remained as a compatibility shim. --- tools/requirements/tests.txt | 1 - 1 file changed, 1 deletion(-) diff --git a/tools/requirements/tests.txt b/tools/requirements/tests.txt index 28022b36769..9b4e9849054 100644 --- a/tools/requirements/tests.txt +++ b/tools/requirements/tests.txt @@ -1,6 +1,5 @@ --use-feature=2020-resolver cryptography==2.8 -enum34; python_version < '3.4' freezegun mock pretend From e47734ed033d1ddc41fbc6d9dd71d55c2af52a54 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Mon, 14 Dec 2020 14:56:07 +0100 Subject: [PATCH 2855/3170] Add failing test that pip wheel -e keeps git clone in src --- tests/functional/test_wheel.py | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/functional/test_wheel.py b/tests/functional/test_wheel.py index 286d694356e..f780130c99d 100644 --- a/tests/functional/test_wheel.py +++ b/tests/functional/test_wheel.py @@ -169,6 +169,25 @@ def test_pip_wheel_builds_editable(script, data): result.did_create(wheel_file_path) +@pytest.mark.network +def test_pip_wheel_git_editable_keeps_clone(script, tmpdir): + """ + Test that `pip wheel -e giturl` preserves a git clone in src. + """ + script.pip( + 'wheel', + '--no-deps', + '-e', + 'git+https://github.com/pypa/pip-test-package#egg=pip-test-package', + '--src', + tmpdir / 'src', + '--wheel-dir', + tmpdir, + ) + assert (tmpdir / 'src' / 'pip-test-package').exists() + assert (tmpdir / 'src' / 'pip-test-package' / '.git').exists() + + def test_pip_wheel_builds_editable_does_not_create_zip(script, data, tmpdir): """ Test 'pip wheel' of editables does not create zip files From 4ea00f2f1b988272132f06d0987ba27dc10395de Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Wed, 23 Dec 2020 10:42:48 +0100 Subject: [PATCH 2856/3170] The preparer always clones VCS requirements Previously, in download mode, it did a vcs export, which did not include vcs information, leading to issues when the build backend required it. --- src/pip/_internal/operations/prepare.py | 2 +- src/pip/_internal/req/req_install.py | 9 +++------ 2 files changed, 4 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 13b2c0beee1..da2fea088f7 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -570,7 +570,7 @@ def prepare_editable_requirement( 'hash.'.format(req) ) req.ensure_has_source_dir(self.src_dir) - req.update_editable(self.download_dir is None) + req.update_editable() dist = _get_prepared_distribution( req, self.req_tracker, self.finder, self.build_isolation, diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 866d18fcb6e..357dbceac13 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -617,8 +617,8 @@ def ensure_has_source_dir( ) # For editable installations - def update_editable(self, obtain=True): - # type: (bool) -> None + def update_editable(self): + # type: () -> None if not self.link: logger.debug( "Cannot update repository at %s; repository location is " @@ -651,10 +651,7 @@ def update_editable(self, obtain=True): ) deprecated(reason, replacement, gone_in="21.0", issue=7554) hidden_url = hide_url(self.link.url) - if obtain: - vcs_backend.obtain(self.source_dir, url=hidden_url) - else: - vcs_backend.export(self.source_dir, url=hidden_url) + vcs_backend.obtain(self.source_dir, url=hidden_url) else: assert 0, ( 'Unexpected version control type (in {}): {}'.format( From 50517b2171d3080f0d32b4a0360f3fadaf8da6e4 Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Sat, 26 Dec 2020 07:17:39 -0800 Subject: [PATCH 2857/3170] Prefer subprocess.DEVNULL over open(os.devnull, 'w') Available since Python 3.3. https://docs.python.org/3/library/subprocess.html#subprocess.DEVNULL Use a context manager for the other opened file, dump. --- ...e2240e-e268-4519-bee7-6f79bc4cf489.trivial.rst | 0 tests/lib/local_repos.py | 15 ++++++--------- 2 files changed, 6 insertions(+), 9 deletions(-) create mode 100644 news/30e2240e-e268-4519-bee7-6f79bc4cf489.trivial.rst diff --git a/news/30e2240e-e268-4519-bee7-6f79bc4cf489.trivial.rst b/news/30e2240e-e268-4519-bee7-6f79bc4cf489.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/lib/local_repos.py b/tests/lib/local_repos.py index 222f132207f..63ec50ccb1d 100644 --- a/tests/lib/local_repos.py +++ b/tests/lib/local_repos.py @@ -22,15 +22,12 @@ def _create_svn_initools_repo(initools_dir): 'http://bitbucket.org/hltbra/pip-initools-dump/raw/8b55c908a320/' 'INITools_modified.dump' ) - devnull = open(os.devnull, 'w') - dump = open(filename) - subprocess.check_call( - ['svnadmin', 'load', initools_dir], - stdin=dump, - stdout=devnull, - ) - dump.close() - devnull.close() + with open(filename) as dump: + subprocess.check_call( + ['svnadmin', 'load', initools_dir], + stdin=dump, + stdout=subprocess.DEVNULL, + ) os.remove(filename) From 156886c144616e7f00bb33c0c64e3952622ff750 Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Sun, 27 Dec 2020 07:51:02 -0800 Subject: [PATCH 2858/3170] Use dict and set comprehension where available --- news/24193261-eaf9-4117-a1a9-d5bb7f93b447.trivial.rst | 0 src/pip/_internal/req/req_uninstall.py | 5 ++--- src/pip/_internal/utils/wheel.py | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) create mode 100644 news/24193261-eaf9-4117-a1a9-d5bb7f93b447.trivial.rst diff --git a/news/24193261-eaf9-4117-a1a9-d5bb7f93b447.trivial.rst b/news/24193261-eaf9-4117-a1a9-d5bb7f93b447.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/req/req_uninstall.py b/src/pip/_internal/req/req_uninstall.py index 43d42c3b41d..64157169e06 100644 --- a/src/pip/_internal/req/req_uninstall.py +++ b/src/pip/_internal/req/req_uninstall.py @@ -130,10 +130,9 @@ def compress_for_rename(paths): This set may include directories when the original sequence of paths included every file on disk. """ - case_map = dict((os.path.normcase(p), p) for p in paths) + case_map = {os.path.normcase(p): p for p in paths} remaining = set(case_map) - unchecked = sorted(set(os.path.split(p)[0] - for p in case_map.values()), key=len) + unchecked = sorted({os.path.split(p)[0] for p in case_map.values()}, key=len) wildcards = set() # type: Set[str] def norm_join(*a): diff --git a/src/pip/_internal/utils/wheel.py b/src/pip/_internal/utils/wheel.py index 2c01cf9927b..e8d9c4bc154 100644 --- a/src/pip/_internal/utils/wheel.py +++ b/src/pip/_internal/utils/wheel.py @@ -114,7 +114,7 @@ def wheel_dist_info_dir(source, name): it doesn't match the provided name. """ # Zip file path separators must be / - subdirs = set(p.split("/", 1)[0] for p in source.namelist()) + subdirs = {p.split("/", 1)[0] for p in source.namelist()} info_dirs = [s for s in subdirs if s.endswith('.dist-info')] From 8a9fea743426235f6835d13ca85390ff0ffb9f15 Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Sun, 27 Dec 2020 09:11:35 -0800 Subject: [PATCH 2859/3170] Remove unnecessary class FakeFile MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The class was being used in a single place, passed to csv.reader(). However, per the docs, csv.reader() can handle a list of str objects. https://docs.python.org/3/library/csv.html#csv.reader > csvfile can be any object which supports the iterator protocol and > returns a string each time its __next__() method is called — file > objects and list objects are both suitable. --- ...01f0a-f673-4c1b-9959-3196b6c000e9.trivial.rst | 0 src/pip/_internal/req/req_uninstall.py | 3 +-- src/pip/_internal/utils/misc.py | 16 ---------------- 3 files changed, 1 insertion(+), 18 deletions(-) create mode 100644 news/f0b01f0a-f673-4c1b-9959-3196b6c000e9.trivial.rst diff --git a/news/f0b01f0a-f673-4c1b-9959-3196b6c000e9.trivial.rst b/news/f0b01f0a-f673-4c1b-9959-3196b6c000e9.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/req/req_uninstall.py b/src/pip/_internal/req/req_uninstall.py index 43d42c3b41d..eea2508db86 100644 --- a/src/pip/_internal/req/req_uninstall.py +++ b/src/pip/_internal/req/req_uninstall.py @@ -13,7 +13,6 @@ from pip._internal.utils.compat import WINDOWS from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import ( - FakeFile, ask, dist_in_usersite, dist_is_local, @@ -90,7 +89,7 @@ def uninstallation_paths(dist): UninstallPathSet.add() takes care of the __pycache__ .py[co]. """ - r = csv.reader(FakeFile(dist.get_metadata_lines('RECORD'))) + r = csv.reader(dist.get_metadata_lines('RECORD')) for row in r: path = os.path.join(dist.location, row[0]) yield path diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index dcd23ec65f4..bd205ada5db 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -572,22 +572,6 @@ def write_output(msg, *args): logger.info(msg, *args) -class FakeFile: - """Wrap a list of lines in an object with readline() to make - ConfigParser happy.""" - def __init__(self, lines): - self._gen = iter(lines) - - def readline(self): - try: - return next(self._gen) - except StopIteration: - return '' - - def __iter__(self): - return self._gen - - class StreamWrapper(StringIO): @classmethod From bd0c1f5d4ca0beba75d7a22bd57c6bbd3342ca79 Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Sun, 27 Dec 2020 12:06:28 -0800 Subject: [PATCH 2860/3170] Remove outdated PyPy workaround for site_packages location Upstream issue has been resolved and released on Python versions supported by pip: https://foss.heptapod.net/pypy/pypy/-/issues/2506 --- news/ae7bdce7-d6f3-4f30-9192-6a8e69027d6a.trivial.rst | 0 src/pip/_internal/locations.py | 7 ------- 2 files changed, 7 deletions(-) create mode 100644 news/ae7bdce7-d6f3-4f30-9192-6a8e69027d6a.trivial.rst diff --git a/news/ae7bdce7-d6f3-4f30-9192-6a8e69027d6a.trivial.rst b/news/ae7bdce7-d6f3-4f30-9192-6a8e69027d6a.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/locations.py b/src/pip/_internal/locations.py index dc5d2e0b2d8..2a903297733 100644 --- a/src/pip/_internal/locations.py +++ b/src/pip/_internal/locations.py @@ -5,11 +5,9 @@ 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 # type: ignore from distutils.command.install import install as distutils_install_command @@ -60,11 +58,6 @@ def get_src_prefix(): site_packages = sysconfig.get_path("purelib") # type: Optional[str] -# 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. From 9109e1ccde2b91ed0d4f39851c4b7982abfced8d Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Sun, 27 Dec 2020 13:08:23 -0800 Subject: [PATCH 2861/3170] Remove outdated mypy workaround for colorama import --- ...89-8b92-4ec1-a3a1-a6657cf6fd5b.trivial.rst | 0 src/pip/_internal/utils/logging.py | 26 +++---------------- 2 files changed, 3 insertions(+), 23 deletions(-) create mode 100644 news/a9950589-8b92-4ec1-a3a1-a6657cf6fd5b.trivial.rst diff --git a/news/a9950589-8b92-4ec1-a3a1-a6657cf6fd5b.trivial.rst b/news/a9950589-8b92-4ec1-a3a1-a6657cf6fd5b.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/utils/logging.py b/src/pip/_internal/utils/logging.py index 9fd1d42c702..6e1aff12386 100644 --- a/src/pip/_internal/utils/logging.py +++ b/src/pip/_internal/utils/logging.py @@ -20,31 +20,11 @@ try: - # Use "import as" and set colorama in the else clause to avoid mypy - # errors and get the following correct revealed type for colorama: - # `Union[_importlib_modulespec.ModuleType, None]` - # Otherwise, we get an error like the following in the except block: - # > Incompatible types in assignment (expression has type "None", - # variable has type Module) - # TODO: eliminate the need to use "import as" once mypy addresses some - # of its issues with conditional imports. Here is an umbrella issue: - # https://github.com/python/mypy/issues/1297 - from pip._vendor import colorama as _colorama + from pip._vendor import colorama # Lots of different errors can come from this, including SystemError and # ImportError. except Exception: colorama = None -else: - # Import Fore explicitly rather than accessing below as colorama.Fore - # to avoid the following error running mypy: - # > Module has no attribute "Fore" - # TODO: eliminate the need to import Fore once mypy addresses some of its - # issues with conditional imports. This particular case could be an - # instance of the following issue (but also see the umbrella issue above): - # https://github.com/python/mypy/issues/3500 - from pip._vendor.colorama import Fore - - colorama = _colorama _log_state = threading.local() @@ -162,8 +142,8 @@ class ColorizedStreamHandler(logging.StreamHandler): if colorama: COLORS = [ # This needs to be in order from highest logging level to lowest. - (logging.ERROR, _color_wrap(Fore.RED)), - (logging.WARNING, _color_wrap(Fore.YELLOW)), + (logging.ERROR, _color_wrap(colorama.Fore.RED)), + (logging.WARNING, _color_wrap(colorama.Fore.YELLOW)), ] else: COLORS = [] From 089bbcb939fe6e92515a6693a68107a2ae0127ce Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Sun, 27 Dec 2020 17:15:49 -0800 Subject: [PATCH 2862/3170] Remove unnecessary type override NamedTemporaryFileResult The object returned by NamedTemporaryFile delegates all functions calls to the underlying file so can avoid the type override by relying on IO methods. --- ...05edb4f-502c-4213-b8b8-c9173718e8ab.trivial.rst | 0 src/pip/_internal/operations/install/wheel.py | 4 ++-- src/pip/_internal/utils/filesystem.py | 14 ++++---------- 3 files changed, 6 insertions(+), 12 deletions(-) create mode 100644 news/205edb4f-502c-4213-b8b8-c9173718e8ab.trivial.rst diff --git a/news/205edb4f-502c-4213-b8b8-c9173718e8ab.trivial.rst b/news/205edb4f-502c-4213-b8b8-c9173718e8ab.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index b6da06f55a5..37dcb618c65 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -46,6 +46,7 @@ from typing import ( IO, Any, + BinaryIO, Callable, Dict, Iterable, @@ -65,7 +66,6 @@ from pip._vendor.pkg_resources import Distribution from pip._internal.models.scheme import Scheme - from pip._internal.utils.filesystem import NamedTemporaryFileResult RecordPath = NewType('RecordPath', str) InstalledCSVRow = Tuple[RecordPath, str, Union[int, str]] @@ -742,7 +742,7 @@ def pyc_output_path(path): @contextlib.contextmanager def _generate_file(path, **kwargs): - # type: (str, **Any) -> Iterator[NamedTemporaryFileResult] + # type: (str, **Any) -> Iterator[BinaryIO] with adjacent_tmp_file(path, **kwargs) as f: yield f os.chmod(f.name, generated_file_mode) diff --git a/src/pip/_internal/utils/filesystem.py b/src/pip/_internal/utils/filesystem.py index dfa2802f71a..1af8c10eaaa 100644 --- a/src/pip/_internal/utils/filesystem.py +++ b/src/pip/_internal/utils/filesystem.py @@ -19,12 +19,6 @@ if MYPY_CHECK_RUNNING: from typing import Any, BinaryIO, Iterator, List, Union - class NamedTemporaryFileResult(BinaryIO): - @property - def file(self): - # type: () -> BinaryIO - pass - def check_path_owner(path): # type: (str) -> bool @@ -86,7 +80,7 @@ def is_socket(path): @contextmanager def adjacent_tmp_file(path, **kwargs): - # type: (str, **Any) -> Iterator[NamedTemporaryFileResult] + # type: (str, **Any) -> Iterator[BinaryIO] """Return a file-like object pointing to a tmp file next to path. The file is created securely and is ensured to be written to disk @@ -102,12 +96,12 @@ def adjacent_tmp_file(path, **kwargs): suffix='.tmp', **kwargs ) as f: - result = cast('NamedTemporaryFileResult', f) + result = cast('BinaryIO', f) try: yield result finally: - result.file.flush() - os.fsync(result.file.fileno()) + result.flush() + os.fsync(result.fileno()) _replace_retry = retry(stop_max_delay=1000, wait_fixed=250) From 1cabd3a60975f280a217963b2b278cd9262737da Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <hugovk@users.noreply.github.com> Date: Mon, 28 Dec 2020 18:34:06 +0200 Subject: [PATCH 2863/3170] The future is now --- tests/functional/test_build_env.py | 4 ---- tests/functional/test_uninstall.py | 2 -- 2 files changed, 6 deletions(-) diff --git a/tests/functional/test_build_env.py b/tests/functional/test_build_env.py index 1f3b88b4d87..7a392f42646 100644 --- a/tests/functional/test_build_env.py +++ b/tests/functional/test_build_env.py @@ -17,7 +17,6 @@ def run_with_build_env(script, setup_script_contents, build_env_script.write_text( dedent( ''' - from __future__ import print_function import subprocess import sys @@ -161,8 +160,6 @@ def test_build_env_overlay_prefix_has_priority(script): 'installing pkg==4.3 in normal') ''', ''' - from __future__ import print_function - print(__import__('pkg').__version__) ''') assert result.stdout.strip() == '2.0', str(result) @@ -195,7 +192,6 @@ def test_build_env_isolation(script): run_with_build_env( script, '', r''' - from __future__ import print_function from distutils.sysconfig import get_python_lib import sys diff --git a/tests/functional/test_uninstall.py b/tests/functional/test_uninstall.py index 6c687ada5b3..d3c6a392ec4 100644 --- a/tests/functional/test_uninstall.py +++ b/tests/functional/test_uninstall.py @@ -1,5 +1,3 @@ -from __future__ import with_statement - import json import logging import os From cc00f8ac71e6588e414b9d1e0bee3c88a3ade5eb Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <hugovk@users.noreply.github.com> Date: Sat, 26 Dec 2020 21:38:06 +0200 Subject: [PATCH 2864/3170] Allow testing of feature branches --- .github/workflows/macos.yml | 2 -- 1 file changed, 2 deletions(-) diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index c7afa8d35ea..de226389d26 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -2,8 +2,6 @@ name: MacOS on: push: - branches: - - master pull_request: schedule: # Run every Friday at 18:02 UTC From 4bfe1037ee4a9e7152f1bcc4eb98dbcc80f62b33 Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Mon, 28 Dec 2020 17:06:44 -0800 Subject: [PATCH 2865/3170] Remove unused class CopytreeKwargs Unused since d509a27ad4e462181719f25c32ba64c2c34b68de. --- ...86c319cb-0539-41a0-871b-4ffe72765f6f.trivial.rst | 0 src/pip/_internal/operations/prepare.py | 13 +------------ 2 files changed, 1 insertion(+), 12 deletions(-) create mode 100644 news/86c319cb-0539-41a0-871b-4ffe72765f6f.trivial.rst diff --git a/news/86c319cb-0539-41a0-871b-4ffe72765f6f.trivial.rst b/news/86c319cb-0539-41a0-871b-4ffe72765f6f.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 853e2e7fbd4..977d83a1b4e 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -38,9 +38,8 @@ from pip._internal.vcs import vcs if MYPY_CHECK_RUNNING: - from typing import Callable, Dict, Iterable, List, Optional, Tuple + from typing import Dict, Iterable, List, Optional, Tuple - from mypy_extensions import TypedDict from pip._vendor.pkg_resources import Distribution from pip._internal.index.package_finder import PackageFinder @@ -50,16 +49,6 @@ from pip._internal.req.req_tracker import RequirementTracker from pip._internal.utils.hashes import Hashes - CopytreeKwargs = TypedDict( - 'CopytreeKwargs', - { - 'copy_function': Callable[[str, str], None], - 'ignore': Callable[[str, List[str]], List[str]], - 'ignore_dangling_symlinks': bool, - 'symlinks': bool, - }, - total=False, - ) logger = logging.getLogger(__name__) From b78f71216f6067e16bf3eb25132bc4da8b41c5e7 Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Mon, 28 Dec 2020 18:17:24 -0800 Subject: [PATCH 2866/3170] Remove unnecessary "noqa" comments --- news/8a225e3d-998e-4924-92e6-2ab2173159f8.trivial.rst | 0 src/pip/__main__.py | 2 +- src/pip/_internal/network/auth.py | 2 +- src/pip/_internal/req/req_set.py | 4 ++-- src/pip/_internal/utils/logging.py | 4 ++-- src/pip/_internal/utils/misc.py | 4 +--- 6 files changed, 7 insertions(+), 9 deletions(-) create mode 100644 news/8a225e3d-998e-4924-92e6-2ab2173159f8.trivial.rst diff --git a/news/8a225e3d-998e-4924-92e6-2ab2173159f8.trivial.rst b/news/8a225e3d-998e-4924-92e6-2ab2173159f8.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/__main__.py b/src/pip/__main__.py index ea738bfc24f..1005489f32f 100644 --- a/src/pip/__main__.py +++ b/src/pip/__main__.py @@ -18,7 +18,7 @@ path = os.path.dirname(os.path.dirname(__file__)) sys.path.insert(0, path) -from pip._internal.cli.main import main as _main # isort:skip # noqa +from pip._internal.cli.main import main as _main if __name__ == '__main__': sys.exit(_main()) diff --git a/src/pip/_internal/network/auth.py b/src/pip/_internal/network/auth.py index a63a2965c67..174eb834875 100644 --- a/src/pip/_internal/network/auth.py +++ b/src/pip/_internal/network/auth.py @@ -31,7 +31,7 @@ logger = logging.getLogger(__name__) try: - import keyring # noqa + import keyring except ImportError: keyring = None except Exception as exc: diff --git a/src/pip/_internal/req/req_set.py b/src/pip/_internal/req/req_set.py index 96bb4013ebe..fa58be66341 100644 --- a/src/pip/_internal/req/req_set.py +++ b/src/pip/_internal/req/req_set.py @@ -24,7 +24,7 @@ def __init__(self, check_supported_wheels=True): """Create a RequirementSet. """ - self.requirements = OrderedDict() # type: Dict[str, InstallRequirement] # noqa: E501 + self.requirements = OrderedDict() # type: Dict[str, InstallRequirement] self.check_supported_wheels = check_supported_wheels self.unnamed_requirements = [] # type: List[InstallRequirement] @@ -69,7 +69,7 @@ def add_requirement( parent_req_name=None, # type: Optional[str] extras_requested=None # type: Optional[Iterable[str]] ): - # type: (...) -> Tuple[List[InstallRequirement], Optional[InstallRequirement]] # noqa: E501 + # type: (...) -> Tuple[List[InstallRequirement], Optional[InstallRequirement]] """Add install_req as a requirement to install. :param parent_req_name: The name of the requirement that needed this diff --git a/src/pip/_internal/utils/logging.py b/src/pip/_internal/utils/logging.py index 6e1aff12386..687bfe3b2f6 100644 --- a/src/pip/_internal/utils/logging.py +++ b/src/pip/_internal/utils/logging.py @@ -45,7 +45,7 @@ class BrokenStdoutLoggingError(Exception): # https://bugs.python.org/issue30418 def _is_broken_pipe_error(exc_class, exc): """See the docstring for non-Windows below.""" - return ((exc_class is BrokenPipeError) or # noqa: F821 + return ((exc_class is BrokenPipeError) or (exc_class is OSError and exc.errno in (errno.EINVAL, errno.EPIPE))) else: @@ -58,7 +58,7 @@ def _is_broken_pipe_error(exc_class, exc): exc_class: an exception class. exc: an exception instance. """ - return (exc_class is BrokenPipeError) # noqa: F821 + return (exc_class is BrokenPipeError) @contextlib.contextmanager diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index bd205ada5db..85e26a02145 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -170,9 +170,7 @@ def path_to_display(path): except UnicodeDecodeError: # Include the full bytes to make troubleshooting easier, even though # it may not be very human readable. - # Silence the "F821 undefined name 'ascii'" flake8 error since - # ascii() is a built-in. - display_path = ascii(path) # noqa: F821 + display_path = ascii(path) return display_path From 133e146a8843ac84c9fe1d02c28fffc985135137 Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Mon, 28 Dec 2020 19:15:44 -0800 Subject: [PATCH 2867/3170] Remove unused type VersionCandidate Unused since 6a8956d7a876508d50851f77ea13a08c96aa17eb. --- news/a13640e3-bfd3-46ae-b4b6-bcb9784303b4.trivial.rst | 0 src/pip/_internal/resolution/resolvelib/factory.py | 2 -- 2 files changed, 2 deletions(-) create mode 100644 news/a13640e3-bfd3-46ae-b4b6-bcb9784303b4.trivial.rst diff --git a/news/a13640e3-bfd3-46ae-b4b6-bcb9784303b4.trivial.rst b/news/a13640e3-bfd3-46ae-b4b6-bcb9784303b4.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index f780831188d..39dc3d15f16 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -68,8 +68,6 @@ C = TypeVar("C") Cache = Dict[Link, C] - VersionCandidates = Dict[_BaseVersion, Candidate] - logger = logging.getLogger(__name__) From 5150129f6bad206cdf24f9315fb7405506ecb5c3 Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Mon, 28 Dec 2020 21:46:53 -0800 Subject: [PATCH 2868/3170] Replace compat.expanduser() with os.path.expanduser() The upstream bug has been fixed and released in all supported Python version: https://bugs.python.org/issue14768 --- ...fc-938e-457b-ae6e-0905e7443b2f.trivial.rst | 0 src/pip/_internal/configuration.py | 4 ++-- src/pip/_internal/utils/compat.py | 13 ------------ src/pip/_internal/utils/misc.py | 4 ++-- tests/unit/test_collector.py | 2 +- tests/unit/test_compat.py | 20 +------------------ 6 files changed, 6 insertions(+), 37 deletions(-) create mode 100644 news/7edb0afc-938e-457b-ae6e-0905e7443b2f.trivial.rst diff --git a/news/7edb0afc-938e-457b-ae6e-0905e7443b2f.trivial.rst b/news/7edb0afc-938e-457b-ae6e-0905e7443b2f.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/configuration.py b/src/pip/_internal/configuration.py index 559c198c777..9d9a36e8077 100644 --- a/src/pip/_internal/configuration.py +++ b/src/pip/_internal/configuration.py @@ -22,7 +22,7 @@ ConfigurationFileCouldNotBeLoaded, ) from pip._internal.utils import appdirs -from pip._internal.utils.compat import WINDOWS, expanduser +from pip._internal.utils.compat import WINDOWS from pip._internal.utils.misc import ensure_dir, enum from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -80,7 +80,7 @@ def get_configuration_files(): site_config_file = os.path.join(sys.prefix, CONFIG_BASENAME) legacy_config_file = os.path.join( - expanduser('~'), + os.path.expanduser('~'), 'pip' if WINDOWS else '.pip', CONFIG_BASENAME, ) diff --git a/src/pip/_internal/utils/compat.py b/src/pip/_internal/utils/compat.py index dc351b804ba..0ae0483c813 100644 --- a/src/pip/_internal/utils/compat.py +++ b/src/pip/_internal/utils/compat.py @@ -139,19 +139,6 @@ def get_path_uid(path): return file_uid -def expanduser(path): - # type: (str) -> str - """ - Expand ~ and ~user constructions. - - Includes a workaround for https://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']`, diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 85e26a02145..65a8c13bd8d 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -28,7 +28,7 @@ from pip import __version__ from pip._internal.exceptions import CommandError from pip._internal.locations import get_major_minor_version, site_packages, user_site -from pip._internal.utils.compat import WINDOWS, expanduser, stdlib_pkgs +from pip._internal.utils.compat import WINDOWS, stdlib_pkgs from pip._internal.utils.typing import MYPY_CHECK_RUNNING, cast from pip._internal.utils.virtualenv import ( running_under_virtualenv, @@ -302,7 +302,7 @@ def normalize_path(path, resolve_symlinks=True): Convert a path to its canonical, case-normalized, absolute version. """ - path = expanduser(path) + path = os.path.expanduser(path) if resolve_symlinks: path = os.path.realpath(path) else: diff --git a/tests/unit/test_collector.py b/tests/unit/test_collector.py index 90d8f8d0fe5..9fb920b32fe 100644 --- a/tests/unit/test_collector.py +++ b/tests/unit/test_collector.py @@ -733,7 +733,7 @@ def test_link_collector_create( assert search_scope.index_urls == expected_index_urls -@patch('pip._internal.utils.misc.expanduser') +@patch('os.path.expanduser') def test_link_collector_create_find_links_expansion( mock_expanduser, tmpdir, ): diff --git a/tests/unit/test_compat.py b/tests/unit/test_compat.py index c1dee67d29f..655e45ab75e 100644 --- a/tests/unit/test_compat.py +++ b/tests/unit/test_compat.py @@ -5,12 +5,7 @@ import pytest import pip._internal.utils.compat as pip_compat -from pip._internal.utils.compat import ( - console_to_str, - expanduser, - get_path_uid, - str_to_display, -) +from pip._internal.utils.compat import console_to_str, get_path_uid, str_to_display def test_get_path_uid(): @@ -127,16 +122,3 @@ def check_warning(msg, *args, **kwargs): monkeypatch.setattr(locale, 'getpreferredencoding', lambda: 'utf-8') monkeypatch.setattr(pip_compat.logger, 'warning', check_warning) console_to_str(some_bytes) - - -@pytest.mark.parametrize("home,path,expanded", [ - ("/Users/test", "~", "/Users/test"), - ("/Users/test", "~/.cache", "/Users/test/.cache"), - # Verify that we are not affected by https://bugs.python.org/issue14768 - ("/", "~", "/"), - ("/", "~/.cache", "/.cache"), -]) -def test_expanduser(home, path, expanded, monkeypatch): - monkeypatch.setenv("HOME", home) - monkeypatch.setenv("USERPROFILE", home) - assert expanduser(path) == expanded From a49f1732e96125d9175bee7b828d9129105cbbf5 Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Wed, 30 Dec 2020 08:07:24 -0800 Subject: [PATCH 2869/3170] Remove unnecessary __ne__ definitions Unnecessary since dropping Python 2 support. In Python 3, `__ne__` defaults to the opposite of of `__eq__`. https://docs.python.org/3/reference/datamodel.html#object.__ne__ > For `__ne__()`, by default it delegates to `__eq__()` and inverts the > result unless it is `NotImplemented`. --- ...85b5f1-5f9a-4f6b-8960-3334570ae591.trivial.rst | 0 src/pip/_internal/models/format_control.py | 4 ---- .../_internal/resolution/resolvelib/candidates.py | 15 --------------- src/pip/_internal/utils/models.py | 3 --- tests/unit/test_utils.py | 3 +-- 5 files changed, 1 insertion(+), 24 deletions(-) create mode 100644 news/4a85b5f1-5f9a-4f6b-8960-3334570ae591.trivial.rst diff --git a/news/4a85b5f1-5f9a-4f6b-8960-3334570ae591.trivial.rst b/news/4a85b5f1-5f9a-4f6b-8960-3334570ae591.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/models/format_control.py b/src/pip/_internal/models/format_control.py index fc2747c950f..eb46f25359a 100644 --- a/src/pip/_internal/models/format_control.py +++ b/src/pip/_internal/models/format_control.py @@ -36,10 +36,6 @@ def __eq__(self, other): for k in self.__slots__ ) - def __ne__(self, other): - # type: (object) -> bool - return not self.__eq__(other) - def __repr__(self): # type: () -> str return "{}({}, {})".format( diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 2453b65ac62..6725684a515 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -164,11 +164,6 @@ def __eq__(self, other): return self._link == other._link return False - # Needed for Python 2, which does not implement this by default - def __ne__(self, other): - # type: (Any) -> bool - return not self.__eq__(other) - @property def source_link(self): # type: () -> Optional[Link] @@ -378,11 +373,6 @@ def __eq__(self, other): return self.name == other.name and self.version == other.version return False - # Needed for Python 2, which does not implement this by default - def __ne__(self, other): - # type: (Any) -> bool - return not self.__eq__(other) - @property def project_name(self): # type: () -> str @@ -475,11 +465,6 @@ def __eq__(self, other): return self.base == other.base and self.extras == other.extras return False - # Needed for Python 2, which does not implement this by default - def __ne__(self, other): - # type: (Any) -> bool - return not self.__eq__(other) - @property def project_name(self): # type: () -> str diff --git a/src/pip/_internal/utils/models.py b/src/pip/_internal/utils/models.py index e7db67a933f..c14e9ff926e 100644 --- a/src/pip/_internal/utils/models.py +++ b/src/pip/_internal/utils/models.py @@ -34,9 +34,6 @@ def __ge__(self, other): def __eq__(self, other): return self._compare(other, operator.__eq__) - def __ne__(self, other): - return self._compare(other, operator.__ne__) - def _compare(self, other, method): if not isinstance(other, self._defining_class): return NotImplemented diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 7b24c8983b3..9c43d553143 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -847,8 +847,7 @@ def test_equality_same_secret(self): hidden2 = HiddenText('secret', redacted='####') assert hidden1 == hidden2 - # Also test __ne__. This assertion fails in Python 2 without - # defining HiddenText.__ne__. + # Also test __ne__. assert not hidden1 != hidden2 def test_equality_different_secret(self): From c513c5e8909173ee999ad50f0738d6129d8ae91d Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Wed, 30 Dec 2020 10:08:12 -0800 Subject: [PATCH 2870/3170] Harmonize return type of VersionControl.get_revision in subclasses Previously, the Subversion subclass violated the parent's type signature by returning an int, but it is now coerced to a str to match the expected signature. --- news/2905cccb-2fe8-4b0d-8734-303510a7e4ce.trivial.rst | 0 src/pip/_internal/vcs/bazaar.py | 1 + src/pip/_internal/vcs/git.py | 1 + src/pip/_internal/vcs/mercurial.py | 1 + src/pip/_internal/vcs/subversion.py | 3 ++- 5 files changed, 5 insertions(+), 1 deletion(-) create mode 100644 news/2905cccb-2fe8-4b0d-8734-303510a7e4ce.trivial.rst diff --git a/news/2905cccb-2fe8-4b0d-8734-303510a7e4ce.trivial.rst b/news/2905cccb-2fe8-4b0d-8734-303510a7e4ce.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/vcs/bazaar.py b/src/pip/_internal/vcs/bazaar.py index ee78b5d27cd..6af3e35ff84 100644 --- a/src/pip/_internal/vcs/bazaar.py +++ b/src/pip/_internal/vcs/bazaar.py @@ -96,6 +96,7 @@ def get_remote_url(cls, location): @classmethod def get_revision(cls, location): + # type: (str) -> str revision = cls.run_command( ['revno'], cwd=location, ) diff --git a/src/pip/_internal/vcs/git.py b/src/pip/_internal/vcs/git.py index e540e02dd86..88a56d6adda 100644 --- a/src/pip/_internal/vcs/git.py +++ b/src/pip/_internal/vcs/git.py @@ -345,6 +345,7 @@ def has_commit(cls, location, rev): @classmethod def get_revision(cls, location, rev=None): + # type: (str, Optional[str]) -> str if rev is None: rev = 'HEAD' current_rev = cls.run_command( diff --git a/src/pip/_internal/vcs/mercurial.py b/src/pip/_internal/vcs/mercurial.py index e7988d1ac2d..98c288864f1 100644 --- a/src/pip/_internal/vcs/mercurial.py +++ b/src/pip/_internal/vcs/mercurial.py @@ -97,6 +97,7 @@ def get_remote_url(cls, location): @classmethod def get_revision(cls, location): + # type: (str) -> str """ Return the repository-local changeset revision number, as an integer. """ diff --git a/src/pip/_internal/vcs/subversion.py b/src/pip/_internal/vcs/subversion.py index 85ce2aa9169..166b673ff83 100644 --- a/src/pip/_internal/vcs/subversion.py +++ b/src/pip/_internal/vcs/subversion.py @@ -49,6 +49,7 @@ def get_base_rev_args(rev): @classmethod def get_revision(cls, location): + # type: (str) -> str """ Return the maximum revision for all files under a given location """ @@ -73,7 +74,7 @@ def get_revision(cls, location): dirs[:] = [] continue # not part of the same svn tree, skip it revision = max(revision, localrev) - return revision + return str(revision) @classmethod def get_netloc_and_auth(cls, netloc, scheme): From 06f1eff0243953a13650704b5a59a7dc1fc7b316 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Thu, 31 Dec 2020 12:33:14 +0100 Subject: [PATCH 2871/3170] Simplify check_binary_allowed check_binary_allowed is only used to check if a wheel needs to be built in 'pip install' mode. It mixed format control and pep517 mode check. We change it so it checks allowed formats only, which leads to better readability of _should_build(). --- src/pip/_internal/commands/install.py | 2 -- src/pip/_internal/wheel_builder.py | 5 +++- tests/unit/test_wheel_builder.py | 35 ++++++++++++++++++++++++--- 3 files changed, 35 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index a9a44787273..da3ee229995 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -50,8 +50,6 @@ def get_check_binary_allowed(format_control): # type: (FormatControl) -> BinaryAllowedPredicate def check_binary_allowed(req): # type: (InstallRequirement) -> bool - if req.use_pep517: - return True canonical_name = canonicalize_name(req.name) allowed_formats = format_control.get_allowed_formats(canonical_name) return "binary" in allowed_formats diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index 27fce66c264..e315fe1110c 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -68,6 +68,9 @@ def _should_build( if req.editable or not req.source_dir: return False + if req.use_pep517: + return True + if not check_binary_allowed(req): logger.info( "Skipping wheel build for %s, due to binaries " @@ -75,7 +78,7 @@ def _should_build( ) return False - if not req.use_pep517 and not is_wheel_installed(): + if not is_wheel_installed(): # we don't build legacy requirements if wheel is not installed logger.info( "Using legacy 'setup.py install' for %s, " diff --git a/tests/unit/test_wheel_builder.py b/tests/unit/test_wheel_builder.py index dcaa1e793ad..bd42f305956 100644 --- a/tests/unit/test_wheel_builder.py +++ b/tests/unit/test_wheel_builder.py @@ -53,17 +53,44 @@ def __init__( @pytest.mark.parametrize( "req, disallow_binaries, expected", [ - (ReqMock(), False, True), - (ReqMock(), True, False), + # When binaries are allowed, we build. + (ReqMock(use_pep517=True), False, True), + (ReqMock(use_pep517=False), False, True), + # When binaries are disallowed, we don't build, unless pep517 is + # enabled. + (ReqMock(use_pep517=True), True, True), + (ReqMock(use_pep517=False), True, False), + # We don't build constraints. (ReqMock(constraint=True), False, False), + # We don't build reqs that are already wheels. (ReqMock(is_wheel=True), False, False), + # We don't build editables. (ReqMock(editable=True), False, False), (ReqMock(source_dir=None), False, False), # By default (i.e. when binaries are allowed), VCS requirements # should be built in install mode. - (ReqMock(link=Link("git+https://g.c/org/repo")), False, True), + ( + ReqMock(link=Link("git+https://g.c/org/repo"), use_pep517=True), + False, + True, + ), + ( + ReqMock(link=Link("git+https://g.c/org/repo"), use_pep517=False), + False, + True, + ), # Disallowing binaries, however, should cause them not to be built. - (ReqMock(link=Link("git+https://g.c/org/repo")), True, False), + # unless pep517 is enabled. + ( + ReqMock(link=Link("git+https://g.c/org/repo"), use_pep517=True), + True, + True, + ), + ( + ReqMock(link=Link("git+https://g.c/org/repo"), use_pep517=False), + True, + False, + ), ], ) def test_should_build_for_install_command(req, disallow_binaries, expected): From ca053fbe8885274b6721b51b36182748c96a0387 Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Thu, 31 Dec 2020 14:30:25 -0800 Subject: [PATCH 2872/3170] Handle ._get_svn_url_rev() returning None in .get_remote_url() The method Subversion._get_svn_url_rev() will sometimes return None for a remote URL. The calling code should handle this. If it is None, raise a RemoteNotFoundError as prescribed by the parent class docstring. Followup to 0b761a164c6429db4a6b0e0e173c3f7dba2546a0. --- ...dd6ac6d-b127-4551-a404-404b0ee8dcd3.trivial.rst | 0 src/pip/_internal/vcs/subversion.py | 6 +++++- tests/functional/test_vcs_subversion.py | 14 ++++++++++++++ 3 files changed, 19 insertions(+), 1 deletion(-) create mode 100644 news/0dd6ac6d-b127-4551-a404-404b0ee8dcd3.trivial.rst diff --git a/news/0dd6ac6d-b127-4551-a404-404b0ee8dcd3.trivial.rst b/news/0dd6ac6d-b127-4551-a404-404b0ee8dcd3.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/vcs/subversion.py b/src/pip/_internal/vcs/subversion.py index 85ce2aa9169..b6b7010bfc0 100644 --- a/src/pip/_internal/vcs/subversion.py +++ b/src/pip/_internal/vcs/subversion.py @@ -128,7 +128,11 @@ def get_remote_url(cls, location): ) raise RemoteNotFoundError - return cls._get_svn_url_rev(location)[0] + url, _rev = cls._get_svn_url_rev(location) + if url is None: + raise RemoteNotFoundError + + return url @classmethod def _get_svn_url_rev(cls, location): diff --git a/tests/functional/test_vcs_subversion.py b/tests/functional/test_vcs_subversion.py index c71c793f895..194019da955 100644 --- a/tests/functional/test_vcs_subversion.py +++ b/tests/functional/test_vcs_subversion.py @@ -15,3 +15,17 @@ def test_get_remote_url__no_remote(script, tmpdir): with pytest.raises(RemoteNotFoundError): Subversion().get_remote_url(repo_dir) + + +@need_svn +def test_get_remote_url__no_remote_with_setup(script, tmpdir): + repo_dir = tmpdir / 'temp-repo' + repo_dir.mkdir() + setup = repo_dir / "setup.py" + setup.touch() + repo_dir = str(repo_dir) + + _create_svn_repo(script, repo_dir) + + with pytest.raises(RemoteNotFoundError): + Subversion().get_remote_url(repo_dir) From 014ccec8caecefdb45bd1829104d69c74ec8b1a2 Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Fri, 1 Jan 2021 09:48:05 -0800 Subject: [PATCH 2873/3170] Make get_similar_command() return None for no value Follows the more conventional pattern of using None, instead of False, when no value can be returned. This simplifies typing a bit by using Optional instead of Union[bool, ...]. --- .../2b5d1433-ec03-4b33-8cf1-ff76baa3690e.trivial.rst | 0 src/pip/_internal/commands/__init__.py | 12 +++--------- 2 files changed, 3 insertions(+), 9 deletions(-) create mode 100644 news/2b5d1433-ec03-4b33-8cf1-ff76baa3690e.trivial.rst diff --git a/news/2b5d1433-ec03-4b33-8cf1-ff76baa3690e.trivial.rst b/news/2b5d1433-ec03-4b33-8cf1-ff76baa3690e.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/commands/__init__.py b/src/pip/_internal/commands/__init__.py index f0554b655dc..f2411201c47 100644 --- a/src/pip/_internal/commands/__init__.py +++ b/src/pip/_internal/commands/__init__.py @@ -2,20 +2,13 @@ Package containing all pip commands """ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False -# There is currently a bug in python/typeshed mentioned at -# https://github.com/python/typeshed/issues/3906 which causes the -# return type of difflib.get_close_matches to be reported -# as List[Sequence[str]] whereas it should have been List[str] - import importlib from collections import OrderedDict, namedtuple from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Any + from typing import Any, Optional from pip._internal.cli.base_command import Command @@ -108,6 +101,7 @@ def create_command(name, **kwargs): def get_similar_commands(name): + # type: (str) -> Optional[str] """Command name auto-correct.""" from difflib import get_close_matches @@ -118,4 +112,4 @@ def get_similar_commands(name): if close_commands: return close_commands[0] else: - return False + return None From 004b2cf9a878170c59cfb8ed5360c82429a6e6c3 Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Fri, 1 Jan 2021 16:04:44 -0800 Subject: [PATCH 2874/3170] Remove unused utility function consume() Unused since e706af20fe9e73cab3482b1f34b5510e88909506. --- news/49254991-9583-470e-a263-b196acf4072b.trivial.rst | 0 src/pip/_internal/utils/misc.py | 6 ------ 2 files changed, 6 deletions(-) create mode 100644 news/49254991-9583-470e-a263-b196acf4072b.trivial.rst diff --git a/news/49254991-9583-470e-a263-b196acf4072b.trivial.rst b/news/49254991-9583-470e-a263-b196acf4072b.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 65a8c13bd8d..a3bd49b9139 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -14,7 +14,6 @@ import stat import sys import urllib.parse -from collections import deque from io import StringIO from itertools import filterfalse, tee, zip_longest @@ -635,11 +634,6 @@ def get_installed_version(dist_name, working_set=None): return dist.version if dist else 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) From 93a51f1de38594179d9c43fdf556c1cddaa4ce8e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sat, 2 Jan 2021 13:43:56 +0100 Subject: [PATCH 2875/3170] The freeze operation does not need a wheel_cache --- src/pip/_internal/commands/freeze.py | 5 ----- src/pip/_internal/operations/freeze.py | 3 --- 2 files changed, 8 deletions(-) diff --git a/src/pip/_internal/commands/freeze.py b/src/pip/_internal/commands/freeze.py index d9caa33516e..8a0b07a0692 100644 --- a/src/pip/_internal/commands/freeze.py +++ b/src/pip/_internal/commands/freeze.py @@ -1,10 +1,8 @@ import sys -from pip._internal.cache import WheelCache from pip._internal.cli import cmdoptions from pip._internal.cli.base_command import Command from pip._internal.cli.status_codes import SUCCESS -from pip._internal.models.format_control import FormatControl from pip._internal.operations.freeze import freeze from pip._internal.utils.compat import stdlib_pkgs from pip._internal.utils.deprecation import deprecated @@ -78,8 +76,6 @@ def add_options(self): def run(self, options, args): # type: (Values, List[str]) -> int - format_control = FormatControl(set(), set()) - wheel_cache = WheelCache(options.cache_dir, format_control) skip = set(stdlib_pkgs) if not options.freeze_all: skip.update(DEV_PKGS) @@ -104,7 +100,6 @@ def run(self, options, args): user_only=options.user, paths=options.path, isolated=options.isolated_mode, - wheel_cache=wheel_cache, skip=skip, exclude_editable=options.exclude_editable, ) diff --git a/src/pip/_internal/operations/freeze.py b/src/pip/_internal/operations/freeze.py index 47f5ab50703..5d63c12fa1a 100644 --- a/src/pip/_internal/operations/freeze.py +++ b/src/pip/_internal/operations/freeze.py @@ -33,8 +33,6 @@ from pip._vendor.pkg_resources import Distribution, Requirement - from pip._internal.cache import WheelCache - RequirementInfo = Tuple[Optional[Union[str, Requirement]], bool, List[str]] @@ -48,7 +46,6 @@ def freeze( user_only=False, # type: bool paths=None, # type: Optional[List[str]] isolated=False, # type: bool - wheel_cache=None, # type: Optional[WheelCache] exclude_editable=False, # type: bool skip=() # type: Container[str] ): From e76eadc9d6128c10f0105ae624342de428a452f3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sat, 2 Jan 2021 13:46:55 +0100 Subject: [PATCH 2876/3170] Pass freeze() kwargs directly --- src/pip/_internal/commands/freeze.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/commands/freeze.py b/src/pip/_internal/commands/freeze.py index 8a0b07a0692..bf20db6c370 100644 --- a/src/pip/_internal/commands/freeze.py +++ b/src/pip/_internal/commands/freeze.py @@ -93,7 +93,7 @@ def run(self, options, args): issue=9069, ) - freeze_kwargs = dict( + for line in freeze( requirement=options.requirements, find_links=options.find_links, local_only=options.local, @@ -102,8 +102,6 @@ def run(self, options, args): isolated=options.isolated_mode, skip=skip, exclude_editable=options.exclude_editable, - ) - - for line in freeze(**freeze_kwargs): + ): sys.stdout.write(line + '\n') return SUCCESS From b43062d4282397750ebfc3071281bfdeb0714498 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sun, 3 Jan 2021 15:34:05 +0100 Subject: [PATCH 2877/3170] Remove deprecation deadline for 8368 --- src/pip/_internal/req/req_install.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 0bf00132036..6ec3c9c6975 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -864,7 +864,7 @@ def install( format(self.name) ), replacement="to fix the wheel build issue reported above", - gone_in="21.0", + gone_in=None, issue=8368, ) From 0866bfa7eec7f2a440f600cab700eec4509fd454 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Mon, 4 Jan 2021 13:26:41 +0800 Subject: [PATCH 2878/3170] Order resolution preference by command ordering --- src/pip/_internal/resolution/resolvelib/provider.py | 11 ++++++----- src/pip/_internal/resolution/resolvelib/resolver.py | 8 +++++--- tests/unit/resolution_resolvelib/conftest.py | 2 +- 3 files changed, 12 insertions(+), 9 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index 40a641a2a4d..2ac3933b404 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -5,7 +5,7 @@ from .base import Constraint if MYPY_CHECK_RUNNING: - from typing import Any, Dict, Iterable, Optional, Sequence, Set, Tuple, Union + from typing import Any, Dict, Iterable, Optional, Sequence, Tuple, Union from .base import Candidate, Requirement from .factory import Factory @@ -46,7 +46,7 @@ def __init__( constraints, # type: Dict[str, Constraint] ignore_dependencies, # type: bool upgrade_strategy, # type: str - user_requested, # type: Set[str] + user_requested, # type: Dict[str, int] ): # type: (...) -> None self._factory = factory @@ -77,7 +77,8 @@ def get_preference( * If equal, prefer if any requirements contain ``===`` and ``==``. * If equal, prefer if requirements include version constraints, e.g. ``>=`` and ``<``. - * If equal, prefer user-specified (non-transitive) requirements. + * If equal, prefer user-specified (non-transitive) requirements, and + order user-specified requirements by the order they are specified. * If equal, order alphabetically for consistency (helps debuggability). """ @@ -115,8 +116,8 @@ def _get_restrictive_rating(requirements): return 3 restrictive = _get_restrictive_rating(req for req, _ in information) - transitive = all(parent is not None for _, parent in information) key = next(iter(candidates)).name if candidates else "" + order = self._user_requested.get(key, float("inf")) # HACK: Setuptools have a very long and solid backward compatibility # track record, and extremely few projects would request a narrow, @@ -128,7 +129,7 @@ def _get_restrictive_rating(requirements): # while we work on "proper" branch pruning techniques. delay_this = (key == "setuptools") - return (delay_this, restrictive, transitive, key) + return (delay_this, restrictive, order, key) def find_matches(self, requirements): # type: (Sequence[Requirement]) -> Iterable[Candidate] diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index d30d696fc46..d02a49c7daa 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -79,9 +79,9 @@ def resolve(self, root_reqs, check_supported_wheels): # type: (List[InstallRequirement], bool) -> RequirementSet constraints = {} # type: Dict[str, Constraint] - user_requested = set() # type: Set[str] + user_requested = {} # type: Dict[str, int] requirements = [] - for req in root_reqs: + for i, req in enumerate(root_reqs): if req.constraint: # Ensure we only accept valid constraints problem = check_invalid_constraint_type(req) @@ -96,7 +96,9 @@ def resolve(self, root_reqs, check_supported_wheels): constraints[name] = Constraint.from_ireq(req) else: if req.user_supplied and req.name: - user_requested.add(canonicalize_name(req.name)) + canonical_name = canonicalize_name(req.name) + if canonical_name not in user_requested: + user_requested[canonical_name] = i r = self.factory.make_requirement_from_install_req( req, requested_extras=(), ) diff --git a/tests/unit/resolution_resolvelib/conftest.py b/tests/unit/resolution_resolvelib/conftest.py index 9c1c9e5c4b3..f77b98ee193 100644 --- a/tests/unit/resolution_resolvelib/conftest.py +++ b/tests/unit/resolution_resolvelib/conftest.py @@ -69,5 +69,5 @@ def provider(factory): constraints={}, ignore_dependencies=False, upgrade_strategy="to-satisfy-only", - user_requested=set(), + user_requested={}, ) From 886275de2d7f92823c812fd0b89a402560c357df Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= <miro@hroncok.cz> Date: Wed, 22 Jul 2020 18:30:59 +0200 Subject: [PATCH 2879/3170] Only query the keyring for URLs that actually trigger error 401 Fixes https://github.com/pypa/pip/issues/8090 --- news/8090.bugfix.rst | 3 ++ src/pip/_internal/network/auth.py | 11 +++++- tests/functional/test_install_config.py | 49 +++++++++++++++++++++++++ tests/lib/server.py | 23 +++++++++--- 4 files changed, 79 insertions(+), 7 deletions(-) create mode 100644 news/8090.bugfix.rst diff --git a/news/8090.bugfix.rst b/news/8090.bugfix.rst new file mode 100644 index 00000000000..ff8b80f3c8e --- /dev/null +++ b/news/8090.bugfix.rst @@ -0,0 +1,3 @@ +Only query the keyring for URLs that actually trigger error 401. +This prevents an unnecessary keyring unlock prompt on every pip install +invocation (even with default index URL which is not password protected). diff --git a/src/pip/_internal/network/auth.py b/src/pip/_internal/network/auth.py index 174eb834875..99fa2bd6762 100644 --- a/src/pip/_internal/network/auth.py +++ b/src/pip/_internal/network/auth.py @@ -112,7 +112,7 @@ def _get_index_url(self, url): return None def _get_new_credentials(self, original_url, allow_netrc=True, - allow_keyring=True): + allow_keyring=False): # type: (str, bool, bool) -> AuthInfo """Find and return credentials for the specified URL.""" # Split the credentials and netloc from the url. @@ -252,8 +252,15 @@ def handle_401(self, resp, **kwargs): parsed = urllib.parse.urlparse(resp.url) + # Query the keyring for credentials: + username, password = self._get_new_credentials(resp.url, + allow_netrc=False, + allow_keyring=True) + # Prompt the user for a new username and password - username, password, save = self._prompt_for_password(parsed.netloc) + save = False + if not username and not password: + username, password, save = self._prompt_for_password(parsed.netloc) # Store the new username and password to use for future requests self._credentials_to_save = None diff --git a/tests/functional/test_install_config.py b/tests/functional/test_install_config.py index 41be6fbbbb6..27e4f0b0cc4 100644 --- a/tests/functional/test_install_config.py +++ b/tests/functional/test_install_config.py @@ -274,3 +274,52 @@ def test_do_not_prompt_for_authentication(script, data, cert_factory): '--no-input', 'simple', expect_error=True) assert "ERROR: HTTP error 401" in result.stderr + + +@pytest.mark.parametrize("auth_needed", (True, False)) +def test_prompt_for_keyring_if_needed(script, data, cert_factory, auth_needed): + """Test behaviour while installing from a index url + requiring authentication and keyring is possible. + """ + cert_path = cert_factory() + ctx = ssl.SSLContext(ssl.PROTOCOL_SSLv23) + ctx.load_cert_chain(cert_path, cert_path) + ctx.load_verify_locations(cafile=cert_path) + ctx.verify_mode = ssl.CERT_REQUIRED + + response = authorization_response if auth_needed else file_response + + server = make_mock_server(ssl_context=ctx) + server.mock.side_effect = [ + package_page({ + "simple-3.0.tar.gz": "/files/simple-3.0.tar.gz", + }), + response(str(data.packages / "simple-3.0.tar.gz")), + response(str(data.packages / "simple-3.0.tar.gz")), + ] + + url = "https://{}:{}/simple".format(server.host, server.port) + + keyring_content = textwrap.dedent("""\ + import os + import sys + from collections import namedtuple + + Cred = namedtuple("Cred", ["username", "password"]) + + def get_credential(url, username): + sys.stderr.write("get_credential was called" + os.linesep) + return Cred("USERNAME", "PASSWORD") + """) + keyring_path = script.site_packages_path / 'keyring.py' + keyring_path.write_text(keyring_content) + + with server_running(server): + result = script.pip('install', "--index-url", url, + "--cert", cert_path, "--client-cert", cert_path, + 'simple') + + if auth_needed: + assert "get_credential was called" in result.stderr + else: + assert "get_credential was called" not in result.stderr diff --git a/tests/lib/server.py b/tests/lib/server.py index cd3c522bfec..dbeff934d6f 100644 --- a/tests/lib/server.py +++ b/tests/lib/server.py @@ -2,6 +2,7 @@ import signal import ssl import threading +from base64 import b64encode from contextlib import contextmanager from textwrap import dedent @@ -219,14 +220,26 @@ def responder(environ, start_response): def authorization_response(path): + # type: (str) -> Responder + correct_auth = "Basic " + b64encode(b"USERNAME:PASSWORD").decode("ascii") + def responder(environ, start_response): # type: (Environ, StartResponse) -> Body - start_response( - "401 Unauthorized", [ - ("WWW-Authenticate", "Basic"), - ], - ) + if environ.get('HTTP_AUTHORIZATION') == correct_auth: + size = os.stat(path).st_size + start_response( + "200 OK", [ + ("Content-Type", "application/octet-stream"), + ("Content-Length", str(size)), + ], + ) + else: + start_response( + "401 Unauthorized", [ + ("WWW-Authenticate", "Basic"), + ], + ) with open(path, 'rb') as f: return [f.read()] From 79cbe6b93f24de12b90a8fbba03b320c58cef31f Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 6 Jan 2021 05:50:27 +0800 Subject: [PATCH 2880/3170] Avoid downloading candidates of a same version Since 41a30089de, Candidate objects prepare their underlying distribution eagerly on creation, so the error can be caught deterministically to trigger backtracking. This however has a negative side-effect. Since dist preparation involves metadata validation, a remote distribution must be downloaded on Candidate creation. This means that an sdist will be downloaded, validated, and discarded (since its version is known to be incompatible) during backtracking. This commit moves version deduplication of candidates from indexes to before the Candidate object is created, to avoid unneeded preparation. Note that we still need another round of deduplication in FoundCandidates to remove duplicated candidates when a distribution is already installed. --- .../resolution/resolvelib/factory.py | 4 +++ .../resolution/resolvelib/found_candidates.py | 29 +++++++------------ 2 files changed, 14 insertions(+), 19 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 39dc3d15f16..bfaa0520ace 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -228,9 +228,12 @@ def iter_index_candidates(): all_yanked = all(ican.link.is_yanked for ican in icans) # PackageFinder returns earlier versions first, so we reverse. + versions_found = set() # type: Set[_BaseVersion] for ican in reversed(icans): if not all_yanked and ican.link.is_yanked: continue + if ican.version in versions_found: + continue candidate = self._make_candidate_from_link( link=ican.link, extras=extras, @@ -241,6 +244,7 @@ def iter_index_candidates(): if candidate is None: continue yield candidate + versions_found.add(ican.version) return FoundCandidates( iter_index_candidates, diff --git a/src/pip/_internal/resolution/resolvelib/found_candidates.py b/src/pip/_internal/resolution/resolvelib/found_candidates.py index be7811dc6ac..c3f95c1d41d 100644 --- a/src/pip/_internal/resolution/resolvelib/found_candidates.py +++ b/src/pip/_internal/resolution/resolvelib/found_candidates.py @@ -16,23 +16,11 @@ from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Callable, Iterator, Optional, Set - - from pip._vendor.packaging.version import _BaseVersion + from typing import Callable, Iterator, Optional from .base import Candidate -def _deduplicated_by_version(candidates): - # type: (Iterator[Candidate]) -> Iterator[Candidate] - returned = set() # type: Set[_BaseVersion] - for candidate in candidates: - if candidate.version in returned: - continue - returned.add(candidate.version) - yield candidate - - def _insert_installed(installed, others): # type: (Candidate, Iterator[Candidate]) -> Iterator[Candidate] """Iterator for ``FoundCandidates``. @@ -86,12 +74,15 @@ def __getitem__(self, index): def __iter__(self): # type: () -> Iterator[Candidate] if not self._installed: - candidates = self._get_others() - elif self._prefers_installed: - candidates = itertools.chain([self._installed], self._get_others()) - else: - candidates = _insert_installed(self._installed, self._get_others()) - return _deduplicated_by_version(candidates) + return self._get_others() + others = ( + candidate + for candidate in self._get_others() + if candidate.version != self._installed.version + ) + if self._prefers_installed: + return itertools.chain([self._installed], others) + return _insert_installed(self._installed, others) def __len__(self): # type: () -> int From 750b8a32fe77c6b4775e1ea2f4eba9e3af78ab15 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sun, 10 Jan 2021 11:50:13 +0100 Subject: [PATCH 2881/3170] Remove support for legacy cache entries --- news/7502.removal.rst | 2 ++ src/pip/_internal/cache.py | 47 -------------------------------------- tests/unit/test_cache.py | 41 --------------------------------- 3 files changed, 2 insertions(+), 88 deletions(-) create mode 100644 news/7502.removal.rst diff --git a/news/7502.removal.rst b/news/7502.removal.rst new file mode 100644 index 00000000000..9f03366ed8e --- /dev/null +++ b/news/7502.removal.rst @@ -0,0 +1,2 @@ +Remove support for legacy wheel cache entries that were created with pip +versions older than 20.0. diff --git a/src/pip/_internal/cache.py b/src/pip/_internal/cache.py index 8724d909352..5e7db9c1f62 100644 --- a/src/pip/_internal/cache.py +++ b/src/pip/_internal/cache.py @@ -55,34 +55,6 @@ def __init__(self, cache_dir, format_control, allowed_formats): _valid_formats = {"source", "binary"} assert self.allowed_formats.union(_valid_formats) == _valid_formats - def _get_cache_path_parts_legacy(self, link): - # type: (Link) -> List[str] - """Get parts of part that must be os.path.joined with cache_dir - - Legacy cache key (pip < 20) for compatibility with older caches. - """ - - # We want to generate an url to use as our cache key, we don't want to - # 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_cache_path_parts(self, link): # type: (Link) -> List[str] """Get parts of part that must be os.path.joined with cache_dir @@ -139,17 +111,8 @@ def _get_candidates(self, link, canonical_package_name): if os.path.isdir(path): for candidate in os.listdir(path): candidates.append((candidate, path)) - # TODO remove legacy path lookup in pip>=21 - legacy_path = self.get_path_for_link_legacy(link) - if os.path.isdir(legacy_path): - for candidate in os.listdir(legacy_path): - candidates.append((candidate, legacy_path)) return candidates - def get_path_for_link_legacy(self, link): - # type: (Link) -> str - raise NotImplementedError() - def get_path_for_link(self, link): # type: (Link) -> str """Return a directory to store cached items in for link. @@ -177,12 +140,6 @@ def __init__(self, cache_dir, format_control): # type: (str, FormatControl) -> None super().__init__(cache_dir, format_control, {"binary"}) - def get_path_for_link_legacy(self, link): - # type: (Link) -> str - parts = self._get_cache_path_parts_legacy(link) - assert self.cache_dir - return os.path.join(self.cache_dir, "wheels", *parts) - def get_path_for_link(self, link): # type: (Link) -> str """Return a directory to store cached wheels for link @@ -286,10 +243,6 @@ def __init__(self, cache_dir, format_control): self._wheel_cache = SimpleWheelCache(cache_dir, format_control) self._ephem_cache = EphemWheelCache(format_control) - def get_path_for_link_legacy(self, link): - # type: (Link) -> str - return self._wheel_cache.get_path_for_link_legacy(link) - def get_path_for_link(self, link): # type: (Link) -> str return self._wheel_cache.get_path_for_link(link) diff --git a/tests/unit/test_cache.py b/tests/unit/test_cache.py index 89cbb079b72..bab62d4e3a8 100644 --- a/tests/unit/test_cache.py +++ b/tests/unit/test_cache.py @@ -51,47 +51,6 @@ def test_cache_hash(): assert h == "f83b32dfa27a426dec08c21bf006065dd003d0aac78e7fc493d9014d" -def test_get_path_for_link_legacy(tmpdir): - """ - Test that an existing cache entry that was created with the legacy hashing - mechanism is returned by WheelCache._get_candidates(). - """ - wc = WheelCache(tmpdir, FormatControl()) - link = Link("https://g.c/o/r") - path = wc.get_path_for_link(link) - legacy_path = wc.get_path_for_link_legacy(link) - assert path != legacy_path - ensure_dir(path) - with open(os.path.join(path, "test-1.0.0-pyz-none-any.whl"), "w"): - pass - ensure_dir(legacy_path) - with open(os.path.join(legacy_path, "test-1.0.0-pyx-none-any.whl"), "w"): - pass - expected_candidates = { - "test-1.0.0-pyx-none-any.whl", "test-1.0.0-pyz-none-any.whl" - } - candidates = {c[0] for c in wc._get_candidates(link, "test")} - assert candidates == expected_candidates - - -def test_get_with_legacy_entry_only(tmpdir): - """ - Test that an existing cache entry that was created with the legacy hashing - mechanism is actually returned in WheelCache.get(). - """ - wc = WheelCache(tmpdir, FormatControl()) - link = Link("https://g.c/o/r") - legacy_path = wc.get_path_for_link_legacy(link) - ensure_dir(legacy_path) - with open(os.path.join(legacy_path, "test-1.0.0-py3-none-any.whl"), "w"): - pass - cached_link = wc.get(link, "test", [Tag("py3", "none", "any")]) - assert ( - os.path.normcase(os.path.dirname(cached_link.file_path)) == - os.path.normcase(legacy_path) - ) - - def test_get_cache_entry(tmpdir): wc = WheelCache(tmpdir, FormatControl()) persi_link = Link("https://g.c/o/r/persi") From c985454b424ff03e25645c5b9313a6cfe06777d2 Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Fri, 1 Jan 2021 12:06:53 -0800 Subject: [PATCH 2882/3170] Complete type annotations for the vcs package --- ...57-9ed5-4d22-a877-50f2d5adcae0.trivial.rst | 0 src/pip/_internal/vcs/bazaar.py | 7 +++--- src/pip/_internal/vcs/git.py | 22 ++++++++++++----- src/pip/_internal/vcs/mercurial.py | 10 +++++--- src/pip/_internal/vcs/subversion.py | 24 ++++++++++++------- 5 files changed, 41 insertions(+), 22 deletions(-) create mode 100644 news/f8f0d057-9ed5-4d22-a877-50f2d5adcae0.trivial.rst diff --git a/news/f8f0d057-9ed5-4d22-a877-50f2d5adcae0.trivial.rst b/news/f8f0d057-9ed5-4d22-a877-50f2d5adcae0.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/vcs/bazaar.py b/src/pip/_internal/vcs/bazaar.py index 3c9c8a47930..d5af489b31c 100644 --- a/src/pip/_internal/vcs/bazaar.py +++ b/src/pip/_internal/vcs/bazaar.py @@ -1,6 +1,3 @@ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - import logging import os @@ -11,7 +8,7 @@ from pip._internal.vcs.versioncontrol import RemoteNotFoundError, VersionControl, vcs if MYPY_CHECK_RUNNING: - from typing import Optional, Tuple + from typing import List, Optional, Tuple from pip._internal.utils.misc import HiddenText from pip._internal.vcs.versioncontrol import AuthInfo, RevOptions @@ -31,6 +28,7 @@ class Bazaar(VersionControl): @staticmethod def get_base_rev_args(rev): + # type: (str) -> List[str] return ['-r', rev] def export(self, location, url): @@ -107,6 +105,7 @@ def get_revision(cls, location): @classmethod def is_commit_id_equal(cls, dest, name): + # type: (str, Optional[str]) -> bool """Always assume the versions don't match""" return False diff --git a/src/pip/_internal/vcs/git.py b/src/pip/_internal/vcs/git.py index a2a1b5f0394..488c15342f8 100644 --- a/src/pip/_internal/vcs/git.py +++ b/src/pip/_internal/vcs/git.py @@ -1,6 +1,3 @@ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - import logging import os.path import re @@ -22,7 +19,9 @@ ) if MYPY_CHECK_RUNNING: - from typing import Optional, Tuple + from typing import List, Optional, Tuple + + from pip._vendor.packaging.version import _BaseVersion from pip._internal.utils.misc import HiddenText from pip._internal.vcs.versioncontrol import AuthInfo, RevOptions @@ -39,6 +38,7 @@ def looks_like_hash(sha): + # type: (str) -> bool return bool(HASH_REGEX.match(sha)) @@ -56,6 +56,7 @@ class Git(VersionControl): @staticmethod def get_base_rev_args(rev): + # type: (str) -> List[str] return [rev] def is_immutable_rev_checkout(self, url, dest): @@ -76,6 +77,7 @@ def is_immutable_rev_checkout(self, url, dest): return not is_tag_or_branch def get_git_version(self): + # type: () -> _BaseVersion VERSION_PFX = 'git version ' version = self.run_command( ['version'], show_stdout=False, stdout_only=True @@ -92,6 +94,7 @@ def get_git_version(self): @classmethod def get_current_branch(cls, location): + # type: (str) -> Optional[str] """ Return the current branch, or None if HEAD isn't at a branch (e.g. detached HEAD). @@ -130,6 +133,7 @@ def export(self, location, url): @classmethod def get_revision_sha(cls, dest, rev): + # type: (str, str) -> Tuple[Optional[str], bool] """ Return (sha_or_none, is_branch), where sha_or_none is a commit hash if the revision names a remote branch or tag, otherwise None. @@ -149,13 +153,13 @@ def get_revision_sha(cls, dest, rev): refs = {} for line in output.strip().splitlines(): try: - sha, ref = line.split() + ref_sha, ref_name = line.split() except ValueError: # Include the offending line to simplify troubleshooting if # this error ever occurs. raise ValueError(f'unexpected show-ref line: {line!r}') - refs[ref] = sha + refs[ref_name] = ref_sha branch_ref = f'refs/remotes/origin/{rev}' tag_ref = f'refs/tags/{rev}' @@ -170,6 +174,7 @@ def get_revision_sha(cls, dest, rev): @classmethod def _should_fetch(cls, dest, rev): + # type: (str, str) -> bool """ Return true if rev is a ref or is a commit that we don't have locally. @@ -238,6 +243,7 @@ def resolve_revision(cls, dest, url, rev_options): @classmethod def is_commit_id_equal(cls, dest, name): + # type: (str, Optional[str]) -> bool """ Return whether the current commit hash equals the given name. @@ -340,6 +346,7 @@ def get_remote_url(cls, location): @classmethod def has_commit(cls, location, rev): + # type: (str, str) -> bool """ Check if rev is a commit that is available in the local repository. """ @@ -369,6 +376,7 @@ def get_revision(cls, location, rev=None): @classmethod def get_subdirectory(cls, location): + # type: (str) -> Optional[str] """ Return the path to setup.py, relative to the repo root. Return None if setup.py is in the repo root. @@ -421,6 +429,7 @@ def get_url_rev_and_auth(cls, url): @classmethod def update_submodules(cls, location): + # type: (str) -> None if not os.path.exists(os.path.join(location, '.gitmodules')): return cls.run_command( @@ -430,6 +439,7 @@ def update_submodules(cls, location): @classmethod def get_repository_root(cls, location): + # type: (str) -> Optional[str] loc = super().get_repository_root(location) if loc: return loc diff --git a/src/pip/_internal/vcs/mercurial.py b/src/pip/_internal/vcs/mercurial.py index 640697550b1..08f15f58256 100644 --- a/src/pip/_internal/vcs/mercurial.py +++ b/src/pip/_internal/vcs/mercurial.py @@ -1,6 +1,3 @@ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - import configparser import logging import os @@ -18,6 +15,8 @@ ) if MYPY_CHECK_RUNNING: + from typing import List, Optional + from pip._internal.utils.misc import HiddenText from pip._internal.vcs.versioncontrol import RevOptions @@ -35,6 +34,7 @@ class Mercurial(VersionControl): @staticmethod def get_base_rev_args(rev): + # type: (str) -> List[str] return [rev] def export(self, location, url): @@ -114,6 +114,7 @@ def get_revision(cls, location): @classmethod def get_requirement_revision(cls, location): + # type: (str) -> str """ Return the changeset identification hash, as a 40-character hexadecimal string @@ -128,11 +129,13 @@ def get_requirement_revision(cls, location): @classmethod def is_commit_id_equal(cls, dest, name): + # type: (str, Optional[str]) -> bool """Always assume the versions don't match""" return False @classmethod def get_subdirectory(cls, location): + # type: (str) -> Optional[str] """ Return the path to setup.py, relative to the repo root. Return None if setup.py is in the repo root. @@ -147,6 +150,7 @@ def get_subdirectory(cls, location): @classmethod def get_repository_root(cls, location): + # type: (str) -> Optional[str] loc = super().get_repository_root(location) if loc: return loc diff --git a/src/pip/_internal/vcs/subversion.py b/src/pip/_internal/vcs/subversion.py index f397c427e67..5ffec921a38 100644 --- a/src/pip/_internal/vcs/subversion.py +++ b/src/pip/_internal/vcs/subversion.py @@ -1,6 +1,3 @@ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - import logging import os import re @@ -23,7 +20,7 @@ if MYPY_CHECK_RUNNING: - from typing import Optional, Tuple + from typing import List, Optional, Tuple from pip._internal.utils.misc import HiddenText from pip._internal.utils.subprocess import CommandArgs @@ -41,10 +38,12 @@ class Subversion(VersionControl): @classmethod def should_add_vcs_url_prefix(cls, remote_url): + # type: (str) -> bool return True @staticmethod def get_base_rev_args(rev): + # type: (str) -> List[str] return ['-r', rev] @classmethod @@ -69,6 +68,7 @@ def get_revision(cls, location): dirurl, localrev = cls._get_svn_url_rev(base) if base == location: + assert dirurl is not None base = dirurl + '/' # save the root url elif not dirurl or not dirurl.startswith(base): dirs[:] = [] @@ -78,6 +78,7 @@ def get_revision(cls, location): @classmethod def get_netloc_and_auth(cls, netloc, scheme): + # type: (str, str) -> Tuple[str, Tuple[Optional[str], Optional[str]]] """ This override allows the auth information to be passed to svn via the --username and --password options instead of via the URL. @@ -137,6 +138,7 @@ def get_remote_url(cls, location): @classmethod def _get_svn_url_rev(cls, location): + # type: (str) -> Tuple[Optional[str], int] from pip._internal.exceptions import InstallationError entries_path = os.path.join(location, cls.dirname, 'entries') @@ -146,13 +148,14 @@ def _get_svn_url_rev(cls, location): else: # subversion >= 1.7 does not have the 'entries' file data = '' + url = None if (data.startswith('8') or data.startswith('9') or data.startswith('10')): - data = list(map(str.splitlines, data.split('\n\x0c\n'))) - del data[0][0] # get rid of the '8' - url = data[0][3] - revs = [int(d[9]) for d in data if len(d) > 9 and d[9]] + [0] + entries = list(map(str.splitlines, data.split('\n\x0c\n'))) + del entries[0][0] # get rid of the '8' + url = entries[0][3] + revs = [int(d[9]) for d in entries if len(d) > 9 and d[9]] + [0] elif data.startswith('<?xml'): match = _svn_xml_url_re.search(data) if not match: @@ -173,7 +176,9 @@ def _get_svn_url_rev(cls, location): show_stdout=False, stdout_only=True, ) - url = _svn_info_xml_url_re.search(xml).group(1) + match = _svn_info_xml_url_re.search(xml) + assert match is not None + url = match.group(1) revs = [ int(m.group(1)) for m in _svn_info_xml_rev_re.finditer(xml) ] @@ -189,6 +194,7 @@ def _get_svn_url_rev(cls, location): @classmethod def is_commit_id_equal(cls, dest, name): + # type: (str, Optional[str]) -> bool """Always assume the versions don't match""" return False From d5b461d28fc638d7b859ec45ae5a8384c50ae545 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Sat, 16 Jan 2021 10:29:55 +0000 Subject: [PATCH 2883/3170] Vendoring changes for 21.0 --- src/pip/_vendor/vendor.txt | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 1db32f396ae..c2f81d7f00a 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -5,14 +5,14 @@ contextlib2==0.6.0.post1 distlib==0.3.1 distro==1.5.0 html5lib==1.1 -msgpack==1.0.0 +msgpack==1.0.2 packaging==20.8 pep517==0.9.1 progress==1.5 pyparsing==2.4.7 -requests==2.25.0 - certifi==2020.11.08 - chardet==3.0.4 +requests==2.25.1 + certifi==2020.12.5 + chardet==4.0.0 idna==2.10 urllib3==1.26.2 resolvelib==0.5.4 From 7deba59c3332f985cc0aac9e7de984c40fd79106 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Sat, 16 Jan 2021 10:36:11 +0000 Subject: [PATCH 2884/3170] Upgrade msgpack and requests --- news/msgpack.vendor.rst | 1 + news/requests.vendor.rst | 1 + src/pip/_vendor/certifi/__init__.py | 2 +- src/pip/_vendor/certifi/cacert.pem | 363 +- src/pip/_vendor/chardet/__init__.py | 48 +- src/pip/_vendor/chardet/charsetgroupprober.py | 1 + src/pip/_vendor/chardet/cli/chardetect.py | 7 +- src/pip/_vendor/chardet/compat.py | 6 +- src/pip/_vendor/chardet/langbulgarianmodel.py | 4860 +++++++++++++- src/pip/_vendor/chardet/langcyrillicmodel.py | 333 - src/pip/_vendor/chardet/langgreekmodel.py | 4605 ++++++++++++- src/pip/_vendor/chardet/langhebrewmodel.py | 4569 ++++++++++++- src/pip/_vendor/chardet/langhungarianmodel.py | 4857 +++++++++++++- src/pip/_vendor/chardet/langrussianmodel.py | 5718 +++++++++++++++++ src/pip/_vendor/chardet/langthaimodel.py | 4568 ++++++++++++- src/pip/_vendor/chardet/langturkishmodel.py | 4560 ++++++++++++- src/pip/_vendor/chardet/metadata/__init__.py | 0 src/pip/_vendor/chardet/metadata/languages.py | 310 + src/pip/_vendor/chardet/sbcharsetprober.py | 45 +- src/pip/_vendor/chardet/sbcsgroupprober.py | 76 +- src/pip/_vendor/chardet/universaldetector.py | 8 +- src/pip/_vendor/chardet/version.py | 2 +- src/pip/_vendor/msgpack/_version.py | 2 +- src/pip/_vendor/msgpack/ext.py | 4 +- src/pip/_vendor/msgpack/fallback.py | 42 +- src/pip/_vendor/requests/__init__.py | 6 +- src/pip/_vendor/requests/__version__.py | 4 +- src/pip/_vendor/requests/sessions.py | 4 +- src/pip/_vendor/requests/utils.py | 4 + 29 files changed, 33048 insertions(+), 1958 deletions(-) create mode 100644 news/msgpack.vendor.rst create mode 100644 news/requests.vendor.rst delete mode 100644 src/pip/_vendor/chardet/langcyrillicmodel.py create mode 100644 src/pip/_vendor/chardet/langrussianmodel.py create mode 100644 src/pip/_vendor/chardet/metadata/__init__.py create mode 100644 src/pip/_vendor/chardet/metadata/languages.py diff --git a/news/msgpack.vendor.rst b/news/msgpack.vendor.rst new file mode 100644 index 00000000000..14a06e1c6bc --- /dev/null +++ b/news/msgpack.vendor.rst @@ -0,0 +1 @@ +Upgrade msgpack to 1.0.2. diff --git a/news/requests.vendor.rst b/news/requests.vendor.rst new file mode 100644 index 00000000000..9c9dee7b4a4 --- /dev/null +++ b/news/requests.vendor.rst @@ -0,0 +1 @@ +Upgrade requests to 2.25.1. diff --git a/src/pip/_vendor/certifi/__init__.py b/src/pip/_vendor/certifi/__init__.py index 4e5133b261d..17aaf900bda 100644 --- a/src/pip/_vendor/certifi/__init__.py +++ b/src/pip/_vendor/certifi/__init__.py @@ -1,3 +1,3 @@ from .core import contents, where -__version__ = "2020.11.08" +__version__ = "2020.12.05" diff --git a/src/pip/_vendor/certifi/cacert.pem b/src/pip/_vendor/certifi/cacert.pem index a1072085ce5..c9459dc85d1 100644 --- a/src/pip/_vendor/certifi/cacert.pem +++ b/src/pip/_vendor/certifi/cacert.pem @@ -155,112 +155,6 @@ eu6FSqdQgPCnXEqULl8FmTxSQeDNtGPPAUO6nIPcj2A781q0tHuu2guQOHXvgR1m 0vdXcDazv/wor3ElhVsT/h5/WrQ8 -----END CERTIFICATE----- -# Issuer: CN=GeoTrust Global CA O=GeoTrust Inc. -# Subject: CN=GeoTrust Global CA O=GeoTrust Inc. -# Label: "GeoTrust Global CA" -# Serial: 144470 -# MD5 Fingerprint: f7:75:ab:29:fb:51:4e:b7:77:5e:ff:05:3c:99:8e:f5 -# SHA1 Fingerprint: de:28:f4:a4:ff:e5:b9:2f:a3:c5:03:d1:a3:49:a7:f9:96:2a:82:12 -# SHA256 Fingerprint: ff:85:6a:2d:25:1d:cd:88:d3:66:56:f4:50:12:67:98:cf:ab:aa:de:40:79:9c:72:2d:e4:d2:b5:db:36:a7:3a ------BEGIN CERTIFICATE----- -MIIDVDCCAjygAwIBAgIDAjRWMA0GCSqGSIb3DQEBBQUAMEIxCzAJBgNVBAYTAlVT -MRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMRswGQYDVQQDExJHZW9UcnVzdCBHbG9i -YWwgQ0EwHhcNMDIwNTIxMDQwMDAwWhcNMjIwNTIxMDQwMDAwWjBCMQswCQYDVQQG -EwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEbMBkGA1UEAxMSR2VvVHJ1c3Qg -R2xvYmFsIENBMIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA2swYYzD9 -9BcjGlZ+W988bDjkcbd4kdS8odhM+KhDtgPpTSEHCIjaWC9mOSm9BXiLnTjoBbdq -fnGk5sRgprDvgOSJKA+eJdbtg/OtppHHmMlCGDUUna2YRpIuT8rxh0PBFpVXLVDv -iS2Aelet8u5fa9IAjbkU+BQVNdnARqN7csiRv8lVK83Qlz6cJmTM386DGXHKTubU -1XupGc1V3sjs0l44U+VcT4wt/lAjNvxm5suOpDkZALeVAjmRCw7+OC7RHQWa9k0+ -bw8HHa8sHo9gOeL6NlMTOdReJivbPagUvTLrGAMoUgRx5aszPeE4uwc2hGKceeoW -MPRfwCvocWvk+QIDAQABo1MwUTAPBgNVHRMBAf8EBTADAQH/MB0GA1UdDgQWBBTA -ephojYn7qwVkDBF9qn1luMrMTjAfBgNVHSMEGDAWgBTAephojYn7qwVkDBF9qn1l -uMrMTjANBgkqhkiG9w0BAQUFAAOCAQEANeMpauUvXVSOKVCUn5kaFOSPeCpilKIn -Z57QzxpeR+nBsqTP3UEaBU6bS+5Kb1VSsyShNwrrZHYqLizz/Tt1kL/6cdjHPTfS -tQWVYrmm3ok9Nns4d0iXrKYgjy6myQzCsplFAMfOEVEiIuCl6rYVSAlk6l5PdPcF -PseKUgzbFbS9bZvlxrFUaKnjaZC2mqUPuLk/IH2uSrW4nOQdtqvmlKXBx4Ot2/Un -hw4EbNX/3aBd7YdStysVAq45pmp06drE57xNNB6pXE0zX5IJL4hmXXeXxx12E6nV -5fEWCRE11azbJHFwLJhWC9kXtNHjUStedejV0NxPNO3CBWaAocvmMw== ------END CERTIFICATE----- - -# Issuer: CN=GeoTrust Universal CA O=GeoTrust Inc. -# Subject: CN=GeoTrust Universal CA O=GeoTrust Inc. -# Label: "GeoTrust Universal CA" -# Serial: 1 -# MD5 Fingerprint: 92:65:58:8b:a2:1a:31:72:73:68:5c:b4:a5:7a:07:48 -# SHA1 Fingerprint: e6:21:f3:35:43:79:05:9a:4b:68:30:9d:8a:2f:74:22:15:87:ec:79 -# SHA256 Fingerprint: a0:45:9b:9f:63:b2:25:59:f5:fa:5d:4c:6d:b3:f9:f7:2f:f1:93:42:03:35:78:f0:73:bf:1d:1b:46:cb:b9:12 ------BEGIN CERTIFICATE----- -MIIFaDCCA1CgAwIBAgIBATANBgkqhkiG9w0BAQUFADBFMQswCQYDVQQGEwJVUzEW -MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEeMBwGA1UEAxMVR2VvVHJ1c3QgVW5pdmVy -c2FsIENBMB4XDTA0MDMwNDA1MDAwMFoXDTI5MDMwNDA1MDAwMFowRTELMAkGA1UE -BhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xHjAcBgNVBAMTFUdlb1RydXN0 -IFVuaXZlcnNhbCBDQTCCAiIwDQYJKoZIhvcNAQEBBQADggIPADCCAgoCggIBAKYV -VaCjxuAfjJ0hUNfBvitbtaSeodlyWL0AG0y/YckUHUWCq8YdgNY96xCcOq9tJPi8 -cQGeBvV8Xx7BDlXKg5pZMK4ZyzBIle0iN430SppyZj6tlcDgFgDgEB8rMQ7XlFTT -QjOgNB0eRXbdT8oYN+yFFXoZCPzVx5zw8qkuEKmS5j1YPakWaDwvdSEYfyh3peFh -F7em6fgemdtzbvQKoiFs7tqqhZJmr/Z6a4LauiIINQ/PQvE1+mrufislzDoR5G2v -c7J2Ha3QsnhnGqQ5HFELZ1aD/ThdDc7d8Lsrlh/eezJS/R27tQahsiFepdaVaH/w -mZ7cRQg+59IJDTWU3YBOU5fXtQlEIGQWFwMCTFMNaN7VqnJNk22CDtucvc+081xd -VHppCZbW2xHBjXWotM85yM48vCR85mLK4b19p71XZQvk/iXttmkQ3CgaRr0BHdCX -teGYO8A3ZNY9lO4L4fUorgtWv3GLIylBjobFS1J72HGrH4oVpjuDWtdYAVHGTEHZ -f9hBZ3KiKN9gg6meyHv8U3NyWfWTehd2Ds735VzZC1U0oqpbtWpU5xPKV+yXbfRe -Bi9Fi1jUIxaS5BZuKGNZMN9QAZxjiRqf2xeUgnA3wySemkfWWspOqGmJch+RbNt+ -nhutxx9z3SxPGWX9f5NAEC7S8O08ni4oPmkmM8V7AgMBAAGjYzBhMA8GA1UdEwEB -/wQFMAMBAf8wHQYDVR0OBBYEFNq7LqqwDLiIJlF0XG0D08DYj3rWMB8GA1UdIwQY -MBaAFNq7LqqwDLiIJlF0XG0D08DYj3rWMA4GA1UdDwEB/wQEAwIBhjANBgkqhkiG -9w0BAQUFAAOCAgEAMXjmx7XfuJRAyXHEqDXsRh3ChfMoWIawC/yOsjmPRFWrZIRc -aanQmjg8+uUfNeVE44B5lGiku8SfPeE0zTBGi1QrlaXv9z+ZhP015s8xxtxqv6fX -IwjhmF7DWgh2qaavdy+3YL1ERmrvl/9zlcGO6JP7/TG37FcREUWbMPEaiDnBTzyn -ANXH/KttgCJwpQzgXQQpAvvLoJHRfNbDflDVnVi+QTjruXU8FdmbyUqDWcDaU/0z -uzYYm4UPFd3uLax2k7nZAY1IEKj79TiG8dsKxr2EoyNB3tZ3b4XUhRxQ4K5RirqN -Pnbiucon8l+f725ZDQbYKxek0nxru18UGkiPGkzns0ccjkxFKyDuSN/n3QmOGKja -QI2SJhFTYXNd673nxE0pN2HrrDktZy4W1vUAg4WhzH92xH3kt0tm7wNFYGm2DFKW -koRepqO1pD4r2czYG0eq8kTaT/kD6PAUyz/zg97QwVTjt+gKN02LIFkDMBmhLMi9 -ER/frslKxfMnZmaGrGiR/9nmUxwPi1xpZQomyB40w11Re9epnAahNt3ViZS82eQt -DF4JbAiXfKM9fJP/P6EUp8+1Xevb2xzEdt+Iub1FBZUbrvxGakyvSOPOrg/Sfuvm -bJxPgWp6ZKy7PtXny3YuxadIwVyQD8vIP/rmMuGNG2+k5o7Y+SlIis5z/iw= ------END CERTIFICATE----- - -# Issuer: CN=GeoTrust Universal CA 2 O=GeoTrust Inc. -# Subject: CN=GeoTrust Universal CA 2 O=GeoTrust Inc. -# Label: "GeoTrust Universal CA 2" -# Serial: 1 -# MD5 Fingerprint: 34:fc:b8:d0:36:db:9e:14:b3:c2:f2:db:8f:e4:94:c7 -# SHA1 Fingerprint: 37:9a:19:7b:41:85:45:35:0c:a6:03:69:f3:3c:2e:af:47:4f:20:79 -# SHA256 Fingerprint: a0:23:4f:3b:c8:52:7c:a5:62:8e:ec:81:ad:5d:69:89:5d:a5:68:0d:c9:1d:1c:b8:47:7f:33:f8:78:b9:5b:0b ------BEGIN CERTIFICATE----- -MIIFbDCCA1SgAwIBAgIBATANBgkqhkiG9w0BAQUFADBHMQswCQYDVQQGEwJVUzEW -MBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1c3QgVW5pdmVy -c2FsIENBIDIwHhcNMDQwMzA0MDUwMDAwWhcNMjkwMzA0MDUwMDAwWjBHMQswCQYD -VQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjEgMB4GA1UEAxMXR2VvVHJ1 -c3QgVW5pdmVyc2FsIENBIDIwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoIC -AQCzVFLByT7y2dyxUxpZKeexw0Uo5dfR7cXFS6GqdHtXr0om/Nj1XqduGdt0DE81 -WzILAePb63p3NeqqWuDW6KFXlPCQo3RWlEQwAx5cTiuFJnSCegx2oG9NzkEtoBUG -FF+3Qs17j1hhNNwqCPkuwwGmIkQcTAeC5lvO0Ep8BNMZcyfwqph/Lq9O64ceJHdq -XbboW0W63MOhBW9Wjo8QJqVJwy7XQYci4E+GymC16qFjwAGXEHm9ADwSbSsVsaxL -se4YuU6W3Nx2/zu+z18DwPw76L5GG//aQMJS9/7jOvdqdzXQ2o3rXhhqMcceujwb -KNZrVMaqW9eiLBsZzKIC9ptZvTdrhrVtgrrY6slWvKk2WP0+GfPtDCapkzj4T8Fd -IgbQl+rhrcZV4IErKIM6+vR7IVEAvlI4zs1meaj0gVbi0IMJR1FbUGrP20gaXT73 -y/Zl92zxlfgCOzJWgjl6W70viRu/obTo/3+NjN8D8WBOWBFM66M/ECuDmgFz2ZRt -hAAnZqzwcEAJQpKtT5MNYQlRJNiS1QuUYbKHsu3/mjX/hVTK7URDrBs8FmtISgoc -QIgfksILAAX/8sgCSqSqqcyZlpwvWOB94b67B9xfBHJcMTTD7F8t4D1kkCLm0ey4 -Lt1ZrtmhN79UNdxzMk+MBB4zsslG8dhcyFVQyWi9qLo2CQIDAQABo2MwYTAPBgNV -HRMBAf8EBTADAQH/MB0GA1UdDgQWBBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAfBgNV -HSMEGDAWgBR281Xh+qQ2+/CfXGJx7Tz0RzgQKzAOBgNVHQ8BAf8EBAMCAYYwDQYJ -KoZIhvcNAQEFBQADggIBAGbBxiPz2eAubl/oz66wsCVNK/g7WJtAJDday6sWSf+z -dXkzoS9tcBc0kf5nfo/sm+VegqlVHy/c1FEHEv6sFj4sNcZj/NwQ6w2jqtB8zNHQ -L1EuxBRa3ugZ4T7GzKQp5y6EqgYweHZUcyiYWTjgAA1i00J9IZ+uPTqM1fp3DRgr -Fg5fNuH8KrUwJM/gYwx7WBr+mbpCErGR9Hxo4sjoryzqyX6uuyo9DRXcNJW2GHSo -ag/HtPQTxORb7QrSpJdMKu0vbBKJPfEncKpqA1Ihn0CoZ1Dy81of398j9tx4TuaY -T1U6U+Pv8vSfx3zYWK8pIpe44L2RLrB27FcRz+8pRPPphXpgY+RdM4kX2TGq2tbz -GDVyz4crL2MjhF2EjD9XoIj8mZEoJmmZ1I+XRL6O1UixpCgp8RW04eWe3fiPpm8m -1wk8OhwRDqZsN/etRIcsKMfYdIKz0G9KV7s1KSegi+ghp4dkNl3M2Basx7InQJJV -OCiNUW7dFGdTbHFcJoRNdVq2fmBWqU2t+5sel/MN2dKXVHfaPRK34B7vCAas+YWH -6aLcr34YEoP9VhdBLtUpgn2Z9DH2canPLAEnpQW5qrJITirvn5NSUZU8UnOOVkwX -QMAJKOSLakhT2+zNVVXxxvjpoixMptEmX36vWkzaH6byHCx+rgIW0lbQL1dTR+iS ------END CERTIFICATE----- - # Issuer: CN=AAA Certificate Services O=Comodo CA Limited # Subject: CN=AAA Certificate Services O=Comodo CA Limited # Label: "Comodo AAA Services root" @@ -776,104 +670,6 @@ hAhm0sQ2fac+EPyI4NSA5QC9qvNOBqN6avlicuMJT+ubDgEj8Z+7fNzcbBGXJbLy tGMU0gYqZ4yD9c7qB9iaah7s5Aq7KkzrCWA5zspi2C5u -----END CERTIFICATE----- -# Issuer: CN=GeoTrust Primary Certification Authority O=GeoTrust Inc. -# Subject: CN=GeoTrust Primary Certification Authority O=GeoTrust Inc. -# Label: "GeoTrust Primary Certification Authority" -# Serial: 32798226551256963324313806436981982369 -# MD5 Fingerprint: 02:26:c3:01:5e:08:30:37:43:a9:d0:7d:cf:37:e6:bf -# SHA1 Fingerprint: 32:3c:11:8e:1b:f7:b8:b6:52:54:e2:e2:10:0d:d6:02:90:37:f0:96 -# SHA256 Fingerprint: 37:d5:10:06:c5:12:ea:ab:62:64:21:f1:ec:8c:92:01:3f:c5:f8:2a:e9:8e:e5:33:eb:46:19:b8:de:b4:d0:6c ------BEGIN CERTIFICATE----- -MIIDfDCCAmSgAwIBAgIQGKy1av1pthU6Y2yv2vrEoTANBgkqhkiG9w0BAQUFADBY -MQswCQYDVQQGEwJVUzEWMBQGA1UEChMNR2VvVHJ1c3QgSW5jLjExMC8GA1UEAxMo -R2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhvcml0eTAeFw0wNjEx -MjcwMDAwMDBaFw0zNjA3MTYyMzU5NTlaMFgxCzAJBgNVBAYTAlVTMRYwFAYDVQQK -Ew1HZW9UcnVzdCBJbmMuMTEwLwYDVQQDEyhHZW9UcnVzdCBQcmltYXJ5IENlcnRp -ZmljYXRpb24gQXV0aG9yaXR5MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKC -AQEAvrgVe//UfH1nrYNke8hCUy3f9oQIIGHWAVlqnEQRr+92/ZV+zmEwu3qDXwK9 -AWbK7hWNb6EwnL2hhZ6UOvNWiAAxz9juapYC2e0DjPt1befquFUWBRaa9OBesYjA -ZIVcFU2Ix7e64HXprQU9nceJSOC7KMgD4TCTZF5SwFlwIjVXiIrxlQqD17wxcwE0 -7e9GceBrAqg1cmuXm2bgyxx5X9gaBGgeRwLmnWDiNpcB3841kt++Z8dtd1k7j53W -kBWUvEI0EME5+bEnPn7WinXFsq+W06Lem+SYvn3h6YGttm/81w7a4DSwDRp35+MI -mO9Y+pyEtzavwt+s0vQQBnBxNQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4G -A1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQULNVQQZcVi/CPNmFbSvtr2ZnJM5IwDQYJ -KoZIhvcNAQEFBQADggEBAFpwfyzdtzRP9YZRqSa+S7iq8XEN3GHHoOo0Hnp3DwQ1 -6CePbJC/kRYkRj5KTs4rFtULUh38H2eiAkUxT87z+gOneZ1TatnaYzr4gNfTmeGl -4b7UVXGYNTq+k+qurUKykG/g/CFNNWMziUnWm07Kx+dOCQD32sfvmWKZd7aVIl6K -oKv0uHiYyjgZmclynnjNS6yvGaBzEi38wkG6gZHaFloxt/m0cYASSJlyc1pZU8Fj -UjPtp8nSOQJw+uCxQmYpqptR7TBUIhRf2asdweSU8Pj1K/fqynhG1riR/aYNKxoU -AT6A8EKglQdebc3MS6RFjasS6LPeWuWgfOgPIh1a6Vk= ------END CERTIFICATE----- - -# Issuer: CN=thawte Primary Root CA O=thawte, Inc. OU=Certification Services Division/(c) 2006 thawte, Inc. - For authorized use only -# Subject: CN=thawte Primary Root CA O=thawte, Inc. OU=Certification Services Division/(c) 2006 thawte, Inc. - For authorized use only -# Label: "thawte Primary Root CA" -# Serial: 69529181992039203566298953787712940909 -# MD5 Fingerprint: 8c:ca:dc:0b:22:ce:f5:be:72:ac:41:1a:11:a8:d8:12 -# SHA1 Fingerprint: 91:c6:d6:ee:3e:8a:c8:63:84:e5:48:c2:99:29:5c:75:6c:81:7b:81 -# SHA256 Fingerprint: 8d:72:2f:81:a9:c1:13:c0:79:1d:f1:36:a2:96:6d:b2:6c:95:0a:97:1d:b4:6b:41:99:f4:ea:54:b7:8b:fb:9f ------BEGIN CERTIFICATE----- -MIIEIDCCAwigAwIBAgIQNE7VVyDV7exJ9C/ON9srbTANBgkqhkiG9w0BAQUFADCB -qTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMf -Q2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIw -MDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxHzAdBgNV -BAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwHhcNMDYxMTE3MDAwMDAwWhcNMzYw -NzE2MjM1OTU5WjCBqTELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5j -LjEoMCYGA1UECxMfQ2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYG -A1UECxMvKGMpIDIwMDYgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNl -IG9ubHkxHzAdBgNVBAMTFnRoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EwggEiMA0GCSqG -SIb3DQEBAQUAA4IBDwAwggEKAoIBAQCsoPD7gFnUnMekz52hWXMJEEUMDSxuaPFs -W0hoSVk3/AszGcJ3f8wQLZU0HObrTQmnHNK4yZc2AreJ1CRfBsDMRJSUjQJib+ta -3RGNKJpchJAQeg29dGYvajig4tVUROsdB58Hum/u6f1OCyn1PoSgAfGcq/gcfomk -6KHYcWUNo1F77rzSImANuVud37r8UVsLr5iy6S7pBOhih94ryNdOwUxkHt3Ph1i6 -Sk/KaAcdHJ1KxtUvkcx8cXIcxcBn6zL9yZJclNqFwJu/U30rCfSMnZEfl2pSy94J -NqR32HuHUETVPm4pafs5SSYeCaWAe0At6+gnhcn+Yf1+5nyXHdWdAgMBAAGjQjBA -MA8GA1UdEwEB/wQFMAMBAf8wDgYDVR0PAQH/BAQDAgEGMB0GA1UdDgQWBBR7W0XP -r87Lev0xkhpqtvNG61dIUDANBgkqhkiG9w0BAQUFAAOCAQEAeRHAS7ORtvzw6WfU -DW5FvlXok9LOAz/t2iWwHVfLHjp2oEzsUHboZHIMpKnxuIvW1oeEuzLlQRHAd9mz -YJ3rG9XRbkREqaYB7FViHXe4XI5ISXycO1cRrK1zN44veFyQaEfZYGDm/Ac9IiAX -xPcW6cTYcvnIc3zfFi8VqT79aie2oetaupgf1eNNZAqdE8hhuvU5HIe6uL17In/2 -/qxAeeWsEG89jxt5dovEN7MhGITlNgDrYyCZuen+MwS7QcjBAvlEYyCegc5C09Y/ -LHbTY5xZ3Y+m4Q6gLkH3LpVHz7z9M/P2C2F+fpErgUfCJzDupxBdN49cOSvkBPB7 -jVaMaA== ------END CERTIFICATE----- - -# Issuer: CN=VeriSign Class 3 Public Primary Certification Authority - G5 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2006 VeriSign, Inc. - For authorized use only -# Subject: CN=VeriSign Class 3 Public Primary Certification Authority - G5 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2006 VeriSign, Inc. - For authorized use only -# Label: "VeriSign Class 3 Public Primary Certification Authority - G5" -# Serial: 33037644167568058970164719475676101450 -# MD5 Fingerprint: cb:17:e4:31:67:3e:e2:09:fe:45:57:93:f3:0a:fa:1c -# SHA1 Fingerprint: 4e:b6:d5:78:49:9b:1c:cf:5f:58:1e:ad:56:be:3d:9b:67:44:a5:e5 -# SHA256 Fingerprint: 9a:cf:ab:7e:43:c8:d8:80:d0:6b:26:2a:94:de:ee:e4:b4:65:99:89:c3:d0:ca:f1:9b:af:64:05:e4:1a:b7:df ------BEGIN CERTIFICATE----- -MIIE0zCCA7ugAwIBAgIQGNrRniZ96LtKIVjNzGs7SjANBgkqhkiG9w0BAQUFADCB -yjELMAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQL -ExZWZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJp -U2lnbiwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxW -ZXJpU2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0 -aG9yaXR5IC0gRzUwHhcNMDYxMTA4MDAwMDAwWhcNMzYwNzE2MjM1OTU5WjCByjEL -MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW -ZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNiBWZXJpU2ln -biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJp -U2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9y -aXR5IC0gRzUwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQCvJAgIKXo1 -nmAMqudLO07cfLw8RRy7K+D+KQL5VwijZIUVJ/XxrcgxiV0i6CqqpkKzj/i5Vbex -t0uz/o9+B1fs70PbZmIVYc9gDaTY3vjgw2IIPVQT60nKWVSFJuUrjxuf6/WhkcIz -SdhDY2pSS9KP6HBRTdGJaXvHcPaz3BJ023tdS1bTlr8Vd6Gw9KIl8q8ckmcY5fQG -BO+QueQA5N06tRn/Arr0PO7gi+s3i+z016zy9vA9r911kTMZHRxAy3QkGSGT2RT+ -rCpSx4/VBEnkjWNHiDxpg8v+R70rfk/Fla4OndTRQ8Bnc+MUCH7lP59zuDMKz10/ -NIeWiu5T6CUVAgMBAAGjgbIwga8wDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8E -BAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJaW1hZ2UvZ2lmMCEwHzAH -BgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYjaHR0cDovL2xvZ28udmVy -aXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFH/TZafC3ey78DAJ80M5+gKv -MzEzMA0GCSqGSIb3DQEBBQUAA4IBAQCTJEowX2LP2BqYLz3q3JktvXf2pXkiOOzE -p6B4Eq1iDkVwZMXnl2YtmAl+X6/WzChl8gGqCBpH3vn5fJJaCGkgDdk+bW48DW7Y -5gaRQBi5+MHt39tBquCWIMnNZBU4gcmU7qKEKQsTb47bDN0lAtukixlE0kF6BWlK -WE9gyn6CagsCqiUXObXbf+eEZSqVir2G3l6BFoMtEMze/aiCKm0oHw0LxOXnGiYZ -4fQRbxC1lfznQgUy286dUV4otp6F01vvpX1FQHKOtw5rDgb7MzVIcbidJ4vEZV8N -hnacRHr2lVz2XTIIM6RUthg/aFzyQkqFOFSDX9HoLPKsEdao7WNq ------END CERTIFICATE----- - # Issuer: CN=SecureTrust CA O=SecureTrust Corporation # Subject: CN=SecureTrust CA O=SecureTrust Corporation # Label: "SecureTrust CA" @@ -1151,95 +947,6 @@ i/nDhDwTqn6Sm1dTk/pwwpEOMfmbZ13pljheX7NzTogVZ96edhBiIL5VaZVDADlN 9u6wWk5JRFRYX0KD -----END CERTIFICATE----- -# Issuer: CN=GeoTrust Primary Certification Authority - G3 O=GeoTrust Inc. OU=(c) 2008 GeoTrust Inc. - For authorized use only -# Subject: CN=GeoTrust Primary Certification Authority - G3 O=GeoTrust Inc. OU=(c) 2008 GeoTrust Inc. - For authorized use only -# Label: "GeoTrust Primary Certification Authority - G3" -# Serial: 28809105769928564313984085209975885599 -# MD5 Fingerprint: b5:e8:34:36:c9:10:44:58:48:70:6d:2e:83:d4:b8:05 -# SHA1 Fingerprint: 03:9e:ed:b8:0b:e7:a0:3c:69:53:89:3b:20:d2:d9:32:3a:4c:2a:fd -# SHA256 Fingerprint: b4:78:b8:12:25:0d:f8:78:63:5c:2a:a7:ec:7d:15:5e:aa:62:5e:e8:29:16:e2:cd:29:43:61:88:6c:d1:fb:d4 ------BEGIN CERTIFICATE----- -MIID/jCCAuagAwIBAgIQFaxulBmyeUtB9iepwxgPHzANBgkqhkiG9w0BAQsFADCB -mDELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUdlb1RydXN0IEluYy4xOTA3BgNVBAsT -MChjKSAyMDA4IEdlb1RydXN0IEluYy4gLSBGb3IgYXV0aG9yaXplZCB1c2Ugb25s -eTE2MDQGA1UEAxMtR2VvVHJ1c3QgUHJpbWFyeSBDZXJ0aWZpY2F0aW9uIEF1dGhv -cml0eSAtIEczMB4XDTA4MDQwMjAwMDAwMFoXDTM3MTIwMTIzNTk1OVowgZgxCzAJ -BgNVBAYTAlVTMRYwFAYDVQQKEw1HZW9UcnVzdCBJbmMuMTkwNwYDVQQLEzAoYykg -MjAwOCBHZW9UcnVzdCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxNjA0 -BgNVBAMTLUdlb1RydXN0IFByaW1hcnkgQ2VydGlmaWNhdGlvbiBBdXRob3JpdHkg -LSBHMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBANziXmJYHTNXOTIz -+uvLh4yn1ErdBojqZI4xmKU4kB6Yzy5jK/BGvESyiaHAKAxJcCGVn2TAppMSAmUm -hsalifD614SgcK9PGpc/BkTVyetyEH3kMSj7HGHmKAdEc5IiaacDiGydY8hS2pgn -5whMcD60yRLBxWeDXTPzAxHsatBT4tG6NmCUgLthY2xbF37fQJQeqw3CIShwiP/W -JmxsYAQlTlV+fe+/lEjetx3dcI0FX4ilm/LC7urRQEFtYjgdVgbFA0dRIBn8exAL -DmKudlW/X3e+PkkBUz2YJQN2JFodtNuJ6nnltrM7P7pMKEF/BqxqjsHQ9gUdfeZC -huOl1UcCAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNVHQ8BAf8EBAMCAQYw -HQYDVR0OBBYEFMR5yo6hTgMdHNxr2zFblD4/MH8tMA0GCSqGSIb3DQEBCwUAA4IB -AQAtxRPPVoB7eni9n64smefv2t+UXglpp+duaIy9cr5HqQ6XErhK8WTTOd8lNNTB -zU6B8A8ExCSzNJbGpqow32hhc9f5joWJ7w5elShKKiePEI4ufIbEAp7aDHdlDkQN -kv39sxY2+hENHYwOB4lqKVb3cvTdFZx3NWZXqxNT2I7BQMXXExZacse3aQHEerGD -AWh9jUGhlBjBJVz88P6DAod8DQ3PLghcSkANPuyBYeYk28rgDi0Hsj5W3I31QYUH -SJsMC8tJP33st/3LjWeJGqvtux6jAAgIFyqCXDFdRootD4abdNlF+9RAsXqqaC2G -spki4cErx5z481+oghLrGREt ------END CERTIFICATE----- - -# Issuer: CN=thawte Primary Root CA - G2 O=thawte, Inc. OU=(c) 2007 thawte, Inc. - For authorized use only -# Subject: CN=thawte Primary Root CA - G2 O=thawte, Inc. OU=(c) 2007 thawte, Inc. - For authorized use only -# Label: "thawte Primary Root CA - G2" -# Serial: 71758320672825410020661621085256472406 -# MD5 Fingerprint: 74:9d:ea:60:24:c4:fd:22:53:3e:cc:3a:72:d9:29:4f -# SHA1 Fingerprint: aa:db:bc:22:23:8f:c4:01:a1:27:bb:38:dd:f4:1d:db:08:9e:f0:12 -# SHA256 Fingerprint: a4:31:0d:50:af:18:a6:44:71:90:37:2a:86:af:af:8b:95:1f:fb:43:1d:83:7f:1e:56:88:b4:59:71:ed:15:57 ------BEGIN CERTIFICATE----- -MIICiDCCAg2gAwIBAgIQNfwmXNmET8k9Jj1Xm67XVjAKBggqhkjOPQQDAzCBhDEL -MAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjE4MDYGA1UECxMvKGMp -IDIwMDcgdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxJDAi -BgNVBAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EgLSBHMjAeFw0wNzExMDUwMDAw -MDBaFw0zODAxMTgyMzU5NTlaMIGEMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhh -d3RlLCBJbmMuMTgwNgYDVQQLEy8oYykgMjAwNyB0aGF3dGUsIEluYy4gLSBGb3Ig -YXV0aG9yaXplZCB1c2Ugb25seTEkMCIGA1UEAxMbdGhhd3RlIFByaW1hcnkgUm9v -dCBDQSAtIEcyMHYwEAYHKoZIzj0CAQYFK4EEACIDYgAEotWcgnuVnfFSeIf+iha/ -BebfowJPDQfGAFG6DAJSLSKkQjnE/o/qycG+1E3/n3qe4rF8mq2nhglzh9HnmuN6 -papu+7qzcMBniKI11KOasf2twu8x+qi58/sIxpHR+ymVo0IwQDAPBgNVHRMBAf8E -BTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNVHQ4EFgQUmtgAMADna3+FGO6Lts6K -DPgR4bswCgYIKoZIzj0EAwMDaQAwZgIxAN344FdHW6fmCsO99YCKlzUNG4k8VIZ3 -KMqh9HneteY4sPBlcIx/AlTCv//YoT7ZzwIxAMSNlPzcU9LcnXgWHxUzI1NS41ox -XZ3Krr0TKUQNJ1uo52icEvdYPy5yAlejj6EULg== ------END CERTIFICATE----- - -# Issuer: CN=thawte Primary Root CA - G3 O=thawte, Inc. OU=Certification Services Division/(c) 2008 thawte, Inc. - For authorized use only -# Subject: CN=thawte Primary Root CA - G3 O=thawte, Inc. OU=Certification Services Division/(c) 2008 thawte, Inc. - For authorized use only -# Label: "thawte Primary Root CA - G3" -# Serial: 127614157056681299805556476275995414779 -# MD5 Fingerprint: fb:1b:5d:43:8a:94:cd:44:c6:76:f2:43:4b:47:e7:31 -# SHA1 Fingerprint: f1:8b:53:8d:1b:e9:03:b6:a6:f0:56:43:5b:17:15:89:ca:f3:6b:f2 -# SHA256 Fingerprint: 4b:03:f4:58:07:ad:70:f2:1b:fc:2c:ae:71:c9:fd:e4:60:4c:06:4c:f5:ff:b6:86:ba:e5:db:aa:d7:fd:d3:4c ------BEGIN CERTIFICATE----- -MIIEKjCCAxKgAwIBAgIQYAGXt0an6rS0mtZLL/eQ+zANBgkqhkiG9w0BAQsFADCB -rjELMAkGA1UEBhMCVVMxFTATBgNVBAoTDHRoYXd0ZSwgSW5jLjEoMCYGA1UECxMf -Q2VydGlmaWNhdGlvbiBTZXJ2aWNlcyBEaXZpc2lvbjE4MDYGA1UECxMvKGMpIDIw -MDggdGhhd3RlLCBJbmMuIC0gRm9yIGF1dGhvcml6ZWQgdXNlIG9ubHkxJDAiBgNV -BAMTG3RoYXd0ZSBQcmltYXJ5IFJvb3QgQ0EgLSBHMzAeFw0wODA0MDIwMDAwMDBa -Fw0zNzEyMDEyMzU5NTlaMIGuMQswCQYDVQQGEwJVUzEVMBMGA1UEChMMdGhhd3Rl -LCBJbmMuMSgwJgYDVQQLEx9DZXJ0aWZpY2F0aW9uIFNlcnZpY2VzIERpdmlzaW9u -MTgwNgYDVQQLEy8oYykgMjAwOCB0aGF3dGUsIEluYy4gLSBGb3IgYXV0aG9yaXpl -ZCB1c2Ugb25seTEkMCIGA1UEAxMbdGhhd3RlIFByaW1hcnkgUm9vdCBDQSAtIEcz -MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAsr8nLPvb2FvdeHsbnndm -gcs+vHyu86YnmjSjaDFxODNi5PNxZnmxqWWjpYvVj2AtP0LMqmsywCPLLEHd5N/8 -YZzic7IilRFDGF/Eth9XbAoFWCLINkw6fKXRz4aviKdEAhN0cXMKQlkC+BsUa0Lf -b1+6a4KinVvnSr0eAXLbS3ToO39/fR8EtCab4LRarEc9VbjXsCZSKAExQGbY2SS9 -9irY7CFJXJv2eul/VTV+lmuNk5Mny5K76qxAwJ/C+IDPXfRa3M50hqY+bAtTyr2S -zhkGcuYMXDhpxwTWvGzOW/b3aJzcJRVIiKHpqfiYnODz1TEoYRFsZ5aNOZnLwkUk -OQIDAQABo0IwQDAPBgNVHRMBAf8EBTADAQH/MA4GA1UdDwEB/wQEAwIBBjAdBgNV -HQ4EFgQUrWyqlGCc7eT/+j4KdCtjA/e2Wb8wDQYJKoZIhvcNAQELBQADggEBABpA -2JVlrAmSicY59BDlqQ5mU1143vokkbvnRFHfxhY0Cu9qRFHqKweKA3rD6z8KLFIW -oCtDuSWQP3CpMyVtRRooOyfPqsMpQhvfO0zAMzRbQYi/aytlryjvsvXDqmbOe1bu -t8jLZ8HJnBoYuMTDSQPxYA5QzUbF83d597YV4Djbxy8ooAw/dyZ02SUS2jHaGh7c -KUGRIjxpp7sC8rZcJwOJ9Abqm+RyguOhCcHpABnTPtRwa7pxpqpYrvS76Wy274fM -m7v/OeZWYdMKp8RcTGB7BXcmer/YB1IsYvdwY9k5vG8cwnncdimvzsUsZAReiDZu -MdRAGmI0Nj81Aa6sY6A= ------END CERTIFICATE----- - # Issuer: CN=GeoTrust Primary Certification Authority - G2 O=GeoTrust Inc. OU=(c) 2007 GeoTrust Inc. - For authorized use only # Subject: CN=GeoTrust Primary Certification Authority - G2 O=GeoTrust Inc. OU=(c) 2007 GeoTrust Inc. - For authorized use only # Label: "GeoTrust Primary Certification Authority - G2" @@ -1301,35 +1008,6 @@ lRQOfc2VNNnSj3BzgXucfr2YYdhFh5iQxeuGMMY1v/D/w1WIg0vvBZIGcfK4mJO3 7M2CYfE45k+XmCpajQ== -----END CERTIFICATE----- -# Issuer: CN=VeriSign Class 3 Public Primary Certification Authority - G4 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2007 VeriSign, Inc. - For authorized use only -# Subject: CN=VeriSign Class 3 Public Primary Certification Authority - G4 O=VeriSign, Inc. OU=VeriSign Trust Network/(c) 2007 VeriSign, Inc. - For authorized use only -# Label: "VeriSign Class 3 Public Primary Certification Authority - G4" -# Serial: 63143484348153506665311985501458640051 -# MD5 Fingerprint: 3a:52:e1:e7:fd:6f:3a:e3:6f:f3:6f:99:1b:f9:22:41 -# SHA1 Fingerprint: 22:d5:d8:df:8f:02:31:d1:8d:f7:9d:b7:cf:8a:2d:64:c9:3f:6c:3a -# SHA256 Fingerprint: 69:dd:d7:ea:90:bb:57:c9:3e:13:5d:c8:5e:a6:fc:d5:48:0b:60:32:39:bd:c4:54:fc:75:8b:2a:26:cf:7f:79 ------BEGIN CERTIFICATE----- -MIIDhDCCAwqgAwIBAgIQL4D+I4wOIg9IZxIokYesszAKBggqhkjOPQQDAzCByjEL -MAkGA1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZW -ZXJpU2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2ln -biwgSW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJp -U2lnbiBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9y -aXR5IC0gRzQwHhcNMDcxMTA1MDAwMDAwWhcNMzgwMTE4MjM1OTU5WjCByjELMAkG -A1UEBhMCVVMxFzAVBgNVBAoTDlZlcmlTaWduLCBJbmMuMR8wHQYDVQQLExZWZXJp -U2lnbiBUcnVzdCBOZXR3b3JrMTowOAYDVQQLEzEoYykgMjAwNyBWZXJpU2lnbiwg -SW5jLiAtIEZvciBhdXRob3JpemVkIHVzZSBvbmx5MUUwQwYDVQQDEzxWZXJpU2ln -biBDbGFzcyAzIFB1YmxpYyBQcmltYXJ5IENlcnRpZmljYXRpb24gQXV0aG9yaXR5 -IC0gRzQwdjAQBgcqhkjOPQIBBgUrgQQAIgNiAASnVnp8Utpkmw4tXNherJI9/gHm -GUo9FANL+mAnINmDiWn6VMaaGF5VKmTeBvaNSjutEDxlPZCIBIngMGGzrl0Bp3ve -fLK+ymVhAIau2o970ImtTR1ZmkGxvEeA3J5iw/mjgbIwga8wDwYDVR0TAQH/BAUw -AwEB/zAOBgNVHQ8BAf8EBAMCAQYwbQYIKwYBBQUHAQwEYTBfoV2gWzBZMFcwVRYJ -aW1hZ2UvZ2lmMCEwHzAHBgUrDgMCGgQUj+XTGoasjY5rw8+AatRIGCx7GS4wJRYj -aHR0cDovL2xvZ28udmVyaXNpZ24uY29tL3ZzbG9nby5naWYwHQYDVR0OBBYEFLMW -kf3upm7ktS5Jj4d4gYDs5bG1MAoGCCqGSM49BAMDA2gAMGUCMGYhDBgmYFo4e1ZC -4Kf8NoRRkSAsdk1DPcQdhCPQrNZ8NQbOzWm9kA3bbEhCHQ6qQgIxAJw9SDkjOVga -FRJZap7v1VmyHVIsmXHNxynfGyphe3HR3vPA5Q06Sqotp9iGKt0uEA== ------END CERTIFICATE----- - # Issuer: CN=NetLock Arany (Class Gold) F\u0151tan\xfas\xedtv\xe1ny O=NetLock Kft. OU=Tan\xfas\xedtv\xe1nykiad\xf3k (Certification Services) # Subject: CN=NetLock Arany (Class Gold) F\u0151tan\xfas\xedtv\xe1ny O=NetLock Kft. OU=Tan\xfas\xedtv\xe1nykiad\xf3k (Certification Services) # Label: "NetLock Arany (Class Gold) F\u0151tan\xfas\xedtv\xe1ny" @@ -4604,3 +4282,44 @@ AZKXRRJ+oPM+rRk6ct30UJMDEr5E0k9BpIycnR+j9sKS50gU/k6bpZFXrsY3crsC MGclCrEMXu6pY5Jv5ZAL/mYiykf9ijH3g/56vxC+GCsej/YpHpRZ744hN8tRmKVu Sw== -----END CERTIFICATE----- + +# Issuer: CN=NAVER Global Root Certification Authority O=NAVER BUSINESS PLATFORM Corp. +# Subject: CN=NAVER Global Root Certification Authority O=NAVER BUSINESS PLATFORM Corp. +# Label: "NAVER Global Root Certification Authority" +# Serial: 9013692873798656336226253319739695165984492813 +# MD5 Fingerprint: c8:7e:41:f6:25:3b:f5:09:b3:17:e8:46:3d:bf:d0:9b +# SHA1 Fingerprint: 8f:6b:f2:a9:27:4a:da:14:a0:c4:f4:8e:61:27:f9:c0:1e:78:5d:d1 +# SHA256 Fingerprint: 88:f4:38:dc:f8:ff:d1:fa:8f:42:91:15:ff:e5:f8:2a:e1:e0:6e:0c:70:c3:75:fa:ad:71:7b:34:a4:9e:72:65 +-----BEGIN CERTIFICATE----- +MIIFojCCA4qgAwIBAgIUAZQwHqIL3fXFMyqxQ0Rx+NZQTQ0wDQYJKoZIhvcNAQEM +BQAwaTELMAkGA1UEBhMCS1IxJjAkBgNVBAoMHU5BVkVSIEJVU0lORVNTIFBMQVRG +T1JNIENvcnAuMTIwMAYDVQQDDClOQVZFUiBHbG9iYWwgUm9vdCBDZXJ0aWZpY2F0 +aW9uIEF1dGhvcml0eTAeFw0xNzA4MTgwODU4NDJaFw0zNzA4MTgyMzU5NTlaMGkx +CzAJBgNVBAYTAktSMSYwJAYDVQQKDB1OQVZFUiBCVVNJTkVTUyBQTEFURk9STSBD +b3JwLjEyMDAGA1UEAwwpTkFWRVIgR2xvYmFsIFJvb3QgQ2VydGlmaWNhdGlvbiBB +dXRob3JpdHkwggIiMA0GCSqGSIb3DQEBAQUAA4ICDwAwggIKAoICAQC21PGTXLVA +iQqrDZBbUGOukJR0F0Vy1ntlWilLp1agS7gvQnXp2XskWjFlqxcX0TM62RHcQDaH +38dq6SZeWYp34+hInDEW+j6RscrJo+KfziFTowI2MMtSAuXaMl3Dxeb57hHHi8lE +HoSTGEq0n+USZGnQJoViAbbJAh2+g1G7XNr4rRVqmfeSVPc0W+m/6imBEtRTkZaz +kVrd/pBzKPswRrXKCAfHcXLJZtM0l/aM9BhK4dA9WkW2aacp+yPOiNgSnABIqKYP +szuSjXEOdMWLyEz59JuOuDxp7W87UC9Y7cSw0BwbagzivESq2M0UXZR4Yb8Obtoq +vC8MC3GmsxY/nOb5zJ9TNeIDoKAYv7vxvvTWjIcNQvcGufFt7QSUqP620wbGQGHf +nZ3zVHbOUzoBppJB7ASjjw2i1QnK1sua8e9DXcCrpUHPXFNwcMmIpi3Ua2FzUCaG +YQ5fG8Ir4ozVu53BA0K6lNpfqbDKzE0K70dpAy8i+/Eozr9dUGWokG2zdLAIx6yo +0es+nPxdGoMuK8u180SdOqcXYZaicdNwlhVNt0xz7hlcxVs+Qf6sdWA7G2POAN3a +CJBitOUt7kinaxeZVL6HSuOpXgRM6xBtVNbv8ejyYhbLgGvtPe31HzClrkvJE+2K +AQHJuFFYwGY6sWZLxNUxAmLpdIQM201GLQIDAQABo0IwQDAdBgNVHQ4EFgQU0p+I +36HNLL3s9TsBAZMzJ7LrYEswDgYDVR0PAQH/BAQDAgEGMA8GA1UdEwEB/wQFMAMB +Af8wDQYJKoZIhvcNAQEMBQADggIBADLKgLOdPVQG3dLSLvCkASELZ0jKbY7gyKoN +qo0hV4/GPnrK21HUUrPUloSlWGB/5QuOH/XcChWB5Tu2tyIvCZwTFrFsDDUIbatj +cu3cvuzHV+YwIHHW1xDBE1UBjCpD5EHxzzp6U5LOogMFDTjfArsQLtk70pt6wKGm ++LUx5vR1yblTmXVHIloUFcd4G7ad6Qz4G3bxhYTeodoS76TiEJd6eN4MUZeoIUCL +hr0N8F5OSza7OyAfikJW4Qsav3vQIkMsRIz75Sq0bBwcupTgE34h5prCy8VCZLQe +lHsIJchxzIdFV4XTnyliIoNRlwAYl3dqmJLJfGBs32x9SuRwTMKeuB330DTHD8z7 +p/8Dvq1wkNoL3chtl1+afwkyQf3NosxabUzyqkn+Zvjp2DXrDige7kgvOtB5CTh8 +piKCk5XQA76+AqAF3SAi428diDRgxuYKuQl1C/AH6GmWNcf7I4GOODm4RStDeKLR +LBT/DShycpWbXgnbiUSYqqFJu3FS8r/2/yehNq+4tneI3TqkbZs0kNwUXTC/t+sX +5Ie3cdCh13cV1ELX8vMxmV2b3RZtP+oGI/hGoiLtk/bdmuYqh7GYVPEi92tF4+KO +dh2ajcQGjTa3FPOdVGm3jjzVpG2Tgbet9r1ke8LJaDmgkpzNNIaRkPpkUZ3+/uul +9XXeifdy +-----END CERTIFICATE----- diff --git a/src/pip/_vendor/chardet/__init__.py b/src/pip/_vendor/chardet/__init__.py index 0f9f820ef6e..80ad2546d79 100644 --- a/src/pip/_vendor/chardet/__init__.py +++ b/src/pip/_vendor/chardet/__init__.py @@ -16,11 +16,14 @@ ######################### END LICENSE BLOCK ######################### -from .compat import PY2, PY3 from .universaldetector import UniversalDetector +from .enums import InputState from .version import __version__, VERSION +__all__ = ['UniversalDetector', 'detect', 'detect_all', '__version__', 'VERSION'] + + def detect(byte_str): """ Detect the encoding of the given byte string. @@ -31,9 +34,50 @@ def detect(byte_str): if not isinstance(byte_str, bytearray): if not isinstance(byte_str, bytes): raise TypeError('Expected object of type bytes or bytearray, got: ' - '{0}'.format(type(byte_str))) + '{}'.format(type(byte_str))) else: byte_str = bytearray(byte_str) detector = UniversalDetector() detector.feed(byte_str) return detector.close() + + +def detect_all(byte_str): + """ + Detect all the possible encodings of the given byte string. + + :param byte_str: The byte sequence to examine. + :type byte_str: ``bytes`` or ``bytearray`` + """ + if not isinstance(byte_str, bytearray): + if not isinstance(byte_str, bytes): + raise TypeError('Expected object of type bytes or bytearray, got: ' + '{}'.format(type(byte_str))) + else: + byte_str = bytearray(byte_str) + + detector = UniversalDetector() + detector.feed(byte_str) + detector.close() + + if detector._input_state == InputState.HIGH_BYTE: + results = [] + for prober in detector._charset_probers: + if prober.get_confidence() > detector.MINIMUM_THRESHOLD: + charset_name = prober.charset_name + lower_charset_name = prober.charset_name.lower() + # Use Windows encoding name instead of ISO-8859 if we saw any + # extra Windows-specific bytes + if lower_charset_name.startswith('iso-8859'): + if detector._has_win_bytes: + charset_name = detector.ISO_WIN_MAP.get(lower_charset_name, + charset_name) + results.append({ + 'encoding': charset_name, + 'confidence': prober.get_confidence(), + 'language': prober.language, + }) + if len(results) > 0: + return sorted(results, key=lambda result: -result['confidence']) + + return [detector.result] diff --git a/src/pip/_vendor/chardet/charsetgroupprober.py b/src/pip/_vendor/chardet/charsetgroupprober.py index 8b3738efd8e..5812cef0b59 100644 --- a/src/pip/_vendor/chardet/charsetgroupprober.py +++ b/src/pip/_vendor/chardet/charsetgroupprober.py @@ -73,6 +73,7 @@ def feed(self, byte_str): continue if state == ProbingState.FOUND_IT: self._best_guess_prober = prober + self._state = ProbingState.FOUND_IT return self.state elif state == ProbingState.NOT_ME: prober.active = False diff --git a/src/pip/_vendor/chardet/cli/chardetect.py b/src/pip/_vendor/chardet/cli/chardetect.py index c61136b639e..6d6f93aabd4 100644 --- a/src/pip/_vendor/chardet/cli/chardetect.py +++ b/src/pip/_vendor/chardet/cli/chardetect.py @@ -1,4 +1,3 @@ -#!/usr/bin/env python """ Script which takes one or more file paths and reports on their detected encodings @@ -45,10 +44,10 @@ def description_of(lines, name='stdin'): if PY2: name = name.decode(sys.getfilesystemencoding(), 'ignore') if result['encoding']: - return '{0}: {1} with confidence {2}'.format(name, result['encoding'], + return '{}: {} with confidence {}'.format(name, result['encoding'], result['confidence']) else: - return '{0}: no result'.format(name) + return '{}: no result'.format(name) def main(argv=None): @@ -69,7 +68,7 @@ def main(argv=None): type=argparse.FileType('rb'), nargs='*', default=[sys.stdin if PY2 else sys.stdin.buffer]) parser.add_argument('--version', action='version', - version='%(prog)s {0}'.format(__version__)) + version='%(prog)s {}'.format(__version__)) args = parser.parse_args(argv) for f in args.input: diff --git a/src/pip/_vendor/chardet/compat.py b/src/pip/_vendor/chardet/compat.py index ddd74687c02..8941572b3e6 100644 --- a/src/pip/_vendor/chardet/compat.py +++ b/src/pip/_vendor/chardet/compat.py @@ -25,10 +25,12 @@ if sys.version_info < (3, 0): PY2 = True PY3 = False - base_str = (str, unicode) + string_types = (str, unicode) text_type = unicode + iteritems = dict.iteritems else: PY2 = False PY3 = True - base_str = (bytes, str) + string_types = (bytes, str) text_type = str + iteritems = dict.items diff --git a/src/pip/_vendor/chardet/langbulgarianmodel.py b/src/pip/_vendor/chardet/langbulgarianmodel.py index 2aa4fb2e22f..e963a50979a 100644 --- a/src/pip/_vendor/chardet/langbulgarianmodel.py +++ b/src/pip/_vendor/chardet/langbulgarianmodel.py @@ -1,228 +1,4650 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is Mozilla Communicator client code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 1998 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### +#!/usr/bin/env python +# -*- coding: utf-8 -*- -# 255: Control characters that usually does not exist in any text -# 254: Carriage/Return -# 253: symbol (punctuation) that does not belong to word -# 252: 0 - 9 +from pip._vendor.chardet.sbcharsetprober import SingleByteCharSetModel -# Character Mapping Table: -# this table is modified base on win1251BulgarianCharToOrderMap, so -# only number <64 is sure valid -Latin5_BulgarianCharToOrderMap = ( -255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 -253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 -252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 -253, 77, 90, 99,100, 72,109,107,101, 79,185, 81,102, 76, 94, 82, # 40 -110,186,108, 91, 74,119, 84, 96,111,187,115,253,253,253,253,253, # 50 -253, 65, 69, 70, 66, 63, 68,112,103, 92,194,104, 95, 86, 87, 71, # 60 -116,195, 85, 93, 97,113,196,197,198,199,200,253,253,253,253,253, # 70 -194,195,196,197,198,199,200,201,202,203,204,205,206,207,208,209, # 80 -210,211,212,213,214,215,216,217,218,219,220,221,222,223,224,225, # 90 - 81,226,227,228,229,230,105,231,232,233,234,235,236, 45,237,238, # a0 - 31, 32, 35, 43, 37, 44, 55, 47, 40, 59, 33, 46, 38, 36, 41, 30, # b0 - 39, 28, 34, 51, 48, 49, 53, 50, 54, 57, 61,239, 67,240, 60, 56, # c0 - 1, 18, 9, 20, 11, 3, 23, 15, 2, 26, 12, 10, 14, 6, 4, 13, # d0 - 7, 8, 5, 19, 29, 25, 22, 21, 27, 24, 17, 75, 52,241, 42, 16, # e0 - 62,242,243,244, 58,245, 98,246,247,248,249,250,251, 91,252,253, # f0 -) +# 3: Positive +# 2: Likely +# 1: Unlikely +# 0: Negative -win1251BulgarianCharToOrderMap = ( -255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 -253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 -252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 -253, 77, 90, 99,100, 72,109,107,101, 79,185, 81,102, 76, 94, 82, # 40 -110,186,108, 91, 74,119, 84, 96,111,187,115,253,253,253,253,253, # 50 -253, 65, 69, 70, 66, 63, 68,112,103, 92,194,104, 95, 86, 87, 71, # 60 -116,195, 85, 93, 97,113,196,197,198,199,200,253,253,253,253,253, # 70 -206,207,208,209,210,211,212,213,120,214,215,216,217,218,219,220, # 80 -221, 78, 64, 83,121, 98,117,105,222,223,224,225,226,227,228,229, # 90 - 88,230,231,232,233,122, 89,106,234,235,236,237,238, 45,239,240, # a0 - 73, 80,118,114,241,242,243,244,245, 62, 58,246,247,248,249,250, # b0 - 31, 32, 35, 43, 37, 44, 55, 47, 40, 59, 33, 46, 38, 36, 41, 30, # c0 - 39, 28, 34, 51, 48, 49, 53, 50, 54, 57, 61,251, 67,252, 60, 56, # d0 - 1, 18, 9, 20, 11, 3, 23, 15, 2, 26, 12, 10, 14, 6, 4, 13, # e0 - 7, 8, 5, 19, 29, 25, 22, 21, 27, 24, 17, 75, 52,253, 42, 16, # f0 -) +BULGARIAN_LANG_MODEL = { + 63: { # 'e' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 0, # 'а' + 18: 1, # 'б' + 9: 1, # 'в' + 20: 1, # 'г' + 11: 1, # 'д' + 3: 1, # 'е' + 23: 1, # 'ж' + 15: 1, # 'з' + 2: 0, # 'и' + 26: 1, # 'й' + 12: 1, # 'к' + 10: 1, # 'л' + 14: 1, # 'м' + 6: 1, # 'н' + 4: 1, # 'о' + 13: 1, # 'п' + 7: 1, # 'р' + 8: 1, # 'с' + 5: 1, # 'т' + 19: 0, # 'у' + 29: 1, # 'ф' + 25: 1, # 'х' + 22: 0, # 'ц' + 21: 1, # 'ч' + 27: 1, # 'ш' + 24: 1, # 'щ' + 17: 0, # 'ъ' + 52: 0, # 'ь' + 42: 0, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 45: { # '\xad' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 1, # 'Б' + 35: 1, # 'В' + 43: 0, # 'Г' + 37: 1, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 1, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 0, # 'Л' + 38: 1, # 'М' + 36: 0, # 'Н' + 41: 1, # 'О' + 30: 1, # 'П' + 39: 1, # 'Р' + 28: 1, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 1, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 0, # 'а' + 18: 0, # 'б' + 9: 0, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 0, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 0, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 0, # 'л' + 14: 0, # 'м' + 6: 0, # 'н' + 4: 0, # 'о' + 13: 0, # 'п' + 7: 0, # 'р' + 8: 0, # 'с' + 5: 0, # 'т' + 19: 0, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 0, # 'ъ' + 52: 0, # 'ь' + 42: 0, # 'ю' + 16: 0, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 31: { # 'А' + 63: 0, # 'e' + 45: 1, # '\xad' + 31: 1, # 'А' + 32: 1, # 'Б' + 35: 2, # 'В' + 43: 1, # 'Г' + 37: 2, # 'Д' + 44: 2, # 'Е' + 55: 1, # 'Ж' + 47: 2, # 'З' + 40: 1, # 'И' + 59: 1, # 'Й' + 33: 1, # 'К' + 46: 2, # 'Л' + 38: 1, # 'М' + 36: 2, # 'Н' + 41: 1, # 'О' + 30: 2, # 'П' + 39: 2, # 'Р' + 28: 2, # 'С' + 34: 2, # 'Т' + 51: 1, # 'У' + 48: 2, # 'Ф' + 49: 1, # 'Х' + 53: 1, # 'Ц' + 50: 1, # 'Ч' + 54: 1, # 'Ш' + 57: 2, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 1, # 'Я' + 1: 1, # 'а' + 18: 2, # 'б' + 9: 2, # 'в' + 20: 2, # 'г' + 11: 2, # 'д' + 3: 1, # 'е' + 23: 1, # 'ж' + 15: 2, # 'з' + 2: 0, # 'и' + 26: 2, # 'й' + 12: 2, # 'к' + 10: 3, # 'л' + 14: 2, # 'м' + 6: 3, # 'н' + 4: 0, # 'о' + 13: 2, # 'п' + 7: 2, # 'р' + 8: 2, # 'с' + 5: 2, # 'т' + 19: 1, # 'у' + 29: 2, # 'ф' + 25: 1, # 'х' + 22: 1, # 'ц' + 21: 1, # 'ч' + 27: 1, # 'ш' + 24: 0, # 'щ' + 17: 0, # 'ъ' + 52: 0, # 'ь' + 42: 0, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 32: { # 'Б' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 2, # 'А' + 32: 2, # 'Б' + 35: 1, # 'В' + 43: 1, # 'Г' + 37: 2, # 'Д' + 44: 1, # 'Е' + 55: 1, # 'Ж' + 47: 2, # 'З' + 40: 1, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 1, # 'Л' + 38: 1, # 'М' + 36: 2, # 'Н' + 41: 2, # 'О' + 30: 1, # 'П' + 39: 1, # 'Р' + 28: 2, # 'С' + 34: 2, # 'Т' + 51: 1, # 'У' + 48: 2, # 'Ф' + 49: 1, # 'Х' + 53: 1, # 'Ц' + 50: 1, # 'Ч' + 54: 0, # 'Ш' + 57: 1, # 'Щ' + 61: 2, # 'Ъ' + 60: 1, # 'Ю' + 56: 1, # 'Я' + 1: 3, # 'а' + 18: 0, # 'б' + 9: 0, # 'в' + 20: 0, # 'г' + 11: 1, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 2, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 2, # 'л' + 14: 0, # 'м' + 6: 0, # 'н' + 4: 3, # 'о' + 13: 0, # 'п' + 7: 2, # 'р' + 8: 1, # 'с' + 5: 0, # 'т' + 19: 2, # 'у' + 29: 0, # 'ф' + 25: 1, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 3, # 'ъ' + 52: 1, # 'ь' + 42: 1, # 'ю' + 16: 2, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 35: { # 'В' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 2, # 'А' + 32: 1, # 'Б' + 35: 1, # 'В' + 43: 0, # 'Г' + 37: 1, # 'Д' + 44: 2, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 2, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 1, # 'Л' + 38: 1, # 'М' + 36: 1, # 'Н' + 41: 1, # 'О' + 30: 1, # 'П' + 39: 2, # 'Р' + 28: 2, # 'С' + 34: 1, # 'Т' + 51: 1, # 'У' + 48: 2, # 'Ф' + 49: 0, # 'Х' + 53: 1, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 1, # 'Ъ' + 60: 1, # 'Ю' + 56: 2, # 'Я' + 1: 3, # 'а' + 18: 1, # 'б' + 9: 0, # 'в' + 20: 0, # 'г' + 11: 1, # 'д' + 3: 3, # 'е' + 23: 1, # 'ж' + 15: 2, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 1, # 'к' + 10: 2, # 'л' + 14: 1, # 'м' + 6: 2, # 'н' + 4: 2, # 'о' + 13: 1, # 'п' + 7: 2, # 'р' + 8: 2, # 'с' + 5: 2, # 'т' + 19: 1, # 'у' + 29: 0, # 'ф' + 25: 1, # 'х' + 22: 0, # 'ц' + 21: 2, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 2, # 'ъ' + 52: 1, # 'ь' + 42: 1, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 43: { # 'Г' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 2, # 'А' + 32: 1, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 1, # 'Д' + 44: 2, # 'Е' + 55: 0, # 'Ж' + 47: 1, # 'З' + 40: 1, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 1, # 'Л' + 38: 0, # 'М' + 36: 1, # 'Н' + 41: 1, # 'О' + 30: 0, # 'П' + 39: 1, # 'Р' + 28: 1, # 'С' + 34: 0, # 'Т' + 51: 1, # 'У' + 48: 1, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 1, # 'Щ' + 61: 1, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 2, # 'а' + 18: 1, # 'б' + 9: 1, # 'в' + 20: 0, # 'г' + 11: 1, # 'д' + 3: 3, # 'е' + 23: 1, # 'ж' + 15: 0, # 'з' + 2: 2, # 'и' + 26: 0, # 'й' + 12: 1, # 'к' + 10: 2, # 'л' + 14: 1, # 'м' + 6: 1, # 'н' + 4: 2, # 'о' + 13: 0, # 'п' + 7: 2, # 'р' + 8: 0, # 'с' + 5: 0, # 'т' + 19: 2, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 1, # 'щ' + 17: 2, # 'ъ' + 52: 1, # 'ь' + 42: 1, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 37: { # 'Д' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 2, # 'А' + 32: 1, # 'Б' + 35: 2, # 'В' + 43: 1, # 'Г' + 37: 2, # 'Д' + 44: 2, # 'Е' + 55: 2, # 'Ж' + 47: 1, # 'З' + 40: 2, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 1, # 'Л' + 38: 1, # 'М' + 36: 1, # 'Н' + 41: 2, # 'О' + 30: 2, # 'П' + 39: 1, # 'Р' + 28: 2, # 'С' + 34: 1, # 'Т' + 51: 1, # 'У' + 48: 1, # 'Ф' + 49: 0, # 'Х' + 53: 1, # 'Ц' + 50: 1, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 1, # 'Ъ' + 60: 1, # 'Ю' + 56: 1, # 'Я' + 1: 3, # 'а' + 18: 0, # 'б' + 9: 2, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 3, # 'е' + 23: 3, # 'ж' + 15: 1, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 1, # 'л' + 14: 1, # 'м' + 6: 2, # 'н' + 4: 3, # 'о' + 13: 0, # 'п' + 7: 2, # 'р' + 8: 0, # 'с' + 5: 0, # 'т' + 19: 2, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 2, # 'ъ' + 52: 1, # 'ь' + 42: 2, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 44: { # 'Е' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 1, # 'А' + 32: 1, # 'Б' + 35: 2, # 'В' + 43: 1, # 'Г' + 37: 1, # 'Д' + 44: 1, # 'Е' + 55: 1, # 'Ж' + 47: 1, # 'З' + 40: 1, # 'И' + 59: 1, # 'Й' + 33: 2, # 'К' + 46: 2, # 'Л' + 38: 1, # 'М' + 36: 2, # 'Н' + 41: 2, # 'О' + 30: 1, # 'П' + 39: 2, # 'Р' + 28: 2, # 'С' + 34: 2, # 'Т' + 51: 1, # 'У' + 48: 2, # 'Ф' + 49: 1, # 'Х' + 53: 2, # 'Ц' + 50: 1, # 'Ч' + 54: 1, # 'Ш' + 57: 1, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 1, # 'Я' + 1: 0, # 'а' + 18: 1, # 'б' + 9: 2, # 'в' + 20: 1, # 'г' + 11: 2, # 'д' + 3: 0, # 'е' + 23: 1, # 'ж' + 15: 1, # 'з' + 2: 0, # 'и' + 26: 1, # 'й' + 12: 2, # 'к' + 10: 2, # 'л' + 14: 2, # 'м' + 6: 2, # 'н' + 4: 0, # 'о' + 13: 1, # 'п' + 7: 2, # 'р' + 8: 2, # 'с' + 5: 1, # 'т' + 19: 1, # 'у' + 29: 1, # 'ф' + 25: 1, # 'х' + 22: 0, # 'ц' + 21: 1, # 'ч' + 27: 1, # 'ш' + 24: 1, # 'щ' + 17: 1, # 'ъ' + 52: 0, # 'ь' + 42: 1, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 55: { # 'Ж' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 1, # 'А' + 32: 0, # 'Б' + 35: 1, # 'В' + 43: 0, # 'Г' + 37: 1, # 'Д' + 44: 1, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 1, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 1, # 'Н' + 41: 1, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 1, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 2, # 'а' + 18: 0, # 'б' + 9: 0, # 'в' + 20: 0, # 'г' + 11: 1, # 'д' + 3: 2, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 2, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 0, # 'л' + 14: 0, # 'м' + 6: 0, # 'н' + 4: 2, # 'о' + 13: 1, # 'п' + 7: 1, # 'р' + 8: 0, # 'с' + 5: 0, # 'т' + 19: 1, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 1, # 'ъ' + 52: 1, # 'ь' + 42: 1, # 'ю' + 16: 0, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 47: { # 'З' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 2, # 'А' + 32: 1, # 'Б' + 35: 1, # 'В' + 43: 1, # 'Г' + 37: 1, # 'Д' + 44: 1, # 'Е' + 55: 0, # 'Ж' + 47: 1, # 'З' + 40: 1, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 1, # 'Л' + 38: 1, # 'М' + 36: 2, # 'Н' + 41: 1, # 'О' + 30: 1, # 'П' + 39: 1, # 'Р' + 28: 1, # 'С' + 34: 1, # 'Т' + 51: 1, # 'У' + 48: 0, # 'Ф' + 49: 1, # 'Х' + 53: 1, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 1, # 'Ъ' + 60: 0, # 'Ю' + 56: 1, # 'Я' + 1: 3, # 'а' + 18: 1, # 'б' + 9: 2, # 'в' + 20: 1, # 'г' + 11: 2, # 'д' + 3: 2, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 1, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 2, # 'л' + 14: 1, # 'м' + 6: 1, # 'н' + 4: 1, # 'о' + 13: 0, # 'п' + 7: 1, # 'р' + 8: 0, # 'с' + 5: 0, # 'т' + 19: 1, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 1, # 'ъ' + 52: 0, # 'ь' + 42: 1, # 'ю' + 16: 0, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 40: { # 'И' + 63: 0, # 'e' + 45: 1, # '\xad' + 31: 1, # 'А' + 32: 1, # 'Б' + 35: 1, # 'В' + 43: 1, # 'Г' + 37: 1, # 'Д' + 44: 2, # 'Е' + 55: 1, # 'Ж' + 47: 2, # 'З' + 40: 1, # 'И' + 59: 1, # 'Й' + 33: 2, # 'К' + 46: 2, # 'Л' + 38: 2, # 'М' + 36: 2, # 'Н' + 41: 1, # 'О' + 30: 1, # 'П' + 39: 2, # 'Р' + 28: 2, # 'С' + 34: 2, # 'Т' + 51: 0, # 'У' + 48: 1, # 'Ф' + 49: 1, # 'Х' + 53: 1, # 'Ц' + 50: 1, # 'Ч' + 54: 1, # 'Ш' + 57: 1, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 2, # 'Я' + 1: 1, # 'а' + 18: 1, # 'б' + 9: 3, # 'в' + 20: 2, # 'г' + 11: 1, # 'д' + 3: 1, # 'е' + 23: 0, # 'ж' + 15: 3, # 'з' + 2: 0, # 'и' + 26: 1, # 'й' + 12: 1, # 'к' + 10: 2, # 'л' + 14: 2, # 'м' + 6: 2, # 'н' + 4: 0, # 'о' + 13: 1, # 'п' + 7: 2, # 'р' + 8: 2, # 'с' + 5: 2, # 'т' + 19: 0, # 'у' + 29: 1, # 'ф' + 25: 1, # 'х' + 22: 1, # 'ц' + 21: 1, # 'ч' + 27: 1, # 'ш' + 24: 1, # 'щ' + 17: 0, # 'ъ' + 52: 0, # 'ь' + 42: 0, # 'ю' + 16: 0, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 59: { # 'Й' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 1, # 'Д' + 44: 1, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 1, # 'Л' + 38: 1, # 'М' + 36: 1, # 'Н' + 41: 1, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 1, # 'С' + 34: 1, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 1, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 1, # 'Я' + 1: 0, # 'а' + 18: 0, # 'б' + 9: 0, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 1, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 0, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 0, # 'л' + 14: 0, # 'м' + 6: 0, # 'н' + 4: 2, # 'о' + 13: 0, # 'п' + 7: 0, # 'р' + 8: 0, # 'с' + 5: 0, # 'т' + 19: 0, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 1, # 'ъ' + 52: 0, # 'ь' + 42: 0, # 'ю' + 16: 0, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 33: { # 'К' + 63: 0, # 'e' + 45: 1, # '\xad' + 31: 2, # 'А' + 32: 1, # 'Б' + 35: 1, # 'В' + 43: 1, # 'Г' + 37: 1, # 'Д' + 44: 1, # 'Е' + 55: 0, # 'Ж' + 47: 1, # 'З' + 40: 2, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 1, # 'Л' + 38: 0, # 'М' + 36: 2, # 'Н' + 41: 2, # 'О' + 30: 2, # 'П' + 39: 1, # 'Р' + 28: 2, # 'С' + 34: 1, # 'Т' + 51: 1, # 'У' + 48: 1, # 'Ф' + 49: 1, # 'Х' + 53: 1, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 1, # 'Ъ' + 60: 1, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 0, # 'б' + 9: 1, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 2, # 'е' + 23: 1, # 'ж' + 15: 0, # 'з' + 2: 2, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 2, # 'л' + 14: 1, # 'м' + 6: 2, # 'н' + 4: 3, # 'о' + 13: 0, # 'п' + 7: 3, # 'р' + 8: 1, # 'с' + 5: 0, # 'т' + 19: 2, # 'у' + 29: 0, # 'ф' + 25: 1, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 1, # 'ш' + 24: 0, # 'щ' + 17: 2, # 'ъ' + 52: 1, # 'ь' + 42: 2, # 'ю' + 16: 0, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 46: { # 'Л' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 2, # 'А' + 32: 1, # 'Б' + 35: 1, # 'В' + 43: 2, # 'Г' + 37: 1, # 'Д' + 44: 2, # 'Е' + 55: 0, # 'Ж' + 47: 1, # 'З' + 40: 2, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 1, # 'Л' + 38: 0, # 'М' + 36: 1, # 'Н' + 41: 2, # 'О' + 30: 1, # 'П' + 39: 0, # 'Р' + 28: 1, # 'С' + 34: 1, # 'Т' + 51: 1, # 'У' + 48: 0, # 'Ф' + 49: 1, # 'Х' + 53: 1, # 'Ц' + 50: 1, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 1, # 'Ъ' + 60: 1, # 'Ю' + 56: 1, # 'Я' + 1: 2, # 'а' + 18: 0, # 'б' + 9: 1, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 2, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 0, # 'л' + 14: 0, # 'м' + 6: 0, # 'н' + 4: 2, # 'о' + 13: 0, # 'п' + 7: 0, # 'р' + 8: 0, # 'с' + 5: 0, # 'т' + 19: 2, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 1, # 'ъ' + 52: 1, # 'ь' + 42: 2, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 38: { # 'М' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 2, # 'А' + 32: 1, # 'Б' + 35: 2, # 'В' + 43: 0, # 'Г' + 37: 1, # 'Д' + 44: 1, # 'Е' + 55: 0, # 'Ж' + 47: 1, # 'З' + 40: 2, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 1, # 'Л' + 38: 1, # 'М' + 36: 1, # 'Н' + 41: 2, # 'О' + 30: 1, # 'П' + 39: 1, # 'Р' + 28: 2, # 'С' + 34: 1, # 'Т' + 51: 1, # 'У' + 48: 1, # 'Ф' + 49: 0, # 'Х' + 53: 1, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 1, # 'Ъ' + 60: 0, # 'Ю' + 56: 1, # 'Я' + 1: 3, # 'а' + 18: 0, # 'б' + 9: 0, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 2, # 'л' + 14: 0, # 'м' + 6: 2, # 'н' + 4: 3, # 'о' + 13: 0, # 'п' + 7: 1, # 'р' + 8: 0, # 'с' + 5: 0, # 'т' + 19: 2, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 2, # 'ъ' + 52: 1, # 'ь' + 42: 2, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 36: { # 'Н' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 2, # 'А' + 32: 2, # 'Б' + 35: 1, # 'В' + 43: 1, # 'Г' + 37: 2, # 'Д' + 44: 2, # 'Е' + 55: 1, # 'Ж' + 47: 1, # 'З' + 40: 2, # 'И' + 59: 1, # 'Й' + 33: 2, # 'К' + 46: 1, # 'Л' + 38: 1, # 'М' + 36: 1, # 'Н' + 41: 2, # 'О' + 30: 1, # 'П' + 39: 1, # 'Р' + 28: 2, # 'С' + 34: 2, # 'Т' + 51: 1, # 'У' + 48: 1, # 'Ф' + 49: 1, # 'Х' + 53: 1, # 'Ц' + 50: 1, # 'Ч' + 54: 1, # 'Ш' + 57: 0, # 'Щ' + 61: 1, # 'Ъ' + 60: 1, # 'Ю' + 56: 1, # 'Я' + 1: 3, # 'а' + 18: 0, # 'б' + 9: 0, # 'в' + 20: 1, # 'г' + 11: 0, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 0, # 'л' + 14: 0, # 'м' + 6: 0, # 'н' + 4: 3, # 'о' + 13: 0, # 'п' + 7: 0, # 'р' + 8: 0, # 'с' + 5: 1, # 'т' + 19: 1, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 1, # 'ш' + 24: 0, # 'щ' + 17: 0, # 'ъ' + 52: 0, # 'ь' + 42: 2, # 'ю' + 16: 2, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 41: { # 'О' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 1, # 'А' + 32: 1, # 'Б' + 35: 2, # 'В' + 43: 1, # 'Г' + 37: 2, # 'Д' + 44: 1, # 'Е' + 55: 1, # 'Ж' + 47: 1, # 'З' + 40: 1, # 'И' + 59: 1, # 'Й' + 33: 2, # 'К' + 46: 2, # 'Л' + 38: 2, # 'М' + 36: 2, # 'Н' + 41: 2, # 'О' + 30: 1, # 'П' + 39: 2, # 'Р' + 28: 2, # 'С' + 34: 2, # 'Т' + 51: 1, # 'У' + 48: 1, # 'Ф' + 49: 1, # 'Х' + 53: 0, # 'Ц' + 50: 1, # 'Ч' + 54: 1, # 'Ш' + 57: 1, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 1, # 'Я' + 1: 1, # 'а' + 18: 2, # 'б' + 9: 2, # 'в' + 20: 2, # 'г' + 11: 1, # 'д' + 3: 1, # 'е' + 23: 1, # 'ж' + 15: 1, # 'з' + 2: 0, # 'и' + 26: 1, # 'й' + 12: 2, # 'к' + 10: 2, # 'л' + 14: 1, # 'м' + 6: 1, # 'н' + 4: 0, # 'о' + 13: 2, # 'п' + 7: 2, # 'р' + 8: 2, # 'с' + 5: 3, # 'т' + 19: 1, # 'у' + 29: 1, # 'ф' + 25: 1, # 'х' + 22: 1, # 'ц' + 21: 2, # 'ч' + 27: 0, # 'ш' + 24: 2, # 'щ' + 17: 0, # 'ъ' + 52: 0, # 'ь' + 42: 0, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 30: { # 'П' + 63: 0, # 'e' + 45: 1, # '\xad' + 31: 2, # 'А' + 32: 1, # 'Б' + 35: 1, # 'В' + 43: 1, # 'Г' + 37: 1, # 'Д' + 44: 1, # 'Е' + 55: 0, # 'Ж' + 47: 1, # 'З' + 40: 2, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 1, # 'Л' + 38: 1, # 'М' + 36: 1, # 'Н' + 41: 2, # 'О' + 30: 2, # 'П' + 39: 2, # 'Р' + 28: 2, # 'С' + 34: 1, # 'Т' + 51: 2, # 'У' + 48: 1, # 'Ф' + 49: 0, # 'Х' + 53: 1, # 'Ц' + 50: 1, # 'Ч' + 54: 1, # 'Ш' + 57: 0, # 'Щ' + 61: 1, # 'Ъ' + 60: 1, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 0, # 'б' + 9: 0, # 'в' + 20: 0, # 'г' + 11: 2, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 2, # 'и' + 26: 0, # 'й' + 12: 1, # 'к' + 10: 3, # 'л' + 14: 0, # 'м' + 6: 1, # 'н' + 4: 3, # 'о' + 13: 0, # 'п' + 7: 3, # 'р' + 8: 1, # 'с' + 5: 1, # 'т' + 19: 2, # 'у' + 29: 1, # 'ф' + 25: 1, # 'х' + 22: 0, # 'ц' + 21: 1, # 'ч' + 27: 1, # 'ш' + 24: 0, # 'щ' + 17: 2, # 'ъ' + 52: 1, # 'ь' + 42: 1, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 39: { # 'Р' + 63: 0, # 'e' + 45: 1, # '\xad' + 31: 2, # 'А' + 32: 1, # 'Б' + 35: 1, # 'В' + 43: 2, # 'Г' + 37: 2, # 'Д' + 44: 2, # 'Е' + 55: 0, # 'Ж' + 47: 1, # 'З' + 40: 2, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 0, # 'Л' + 38: 1, # 'М' + 36: 1, # 'Н' + 41: 2, # 'О' + 30: 2, # 'П' + 39: 1, # 'Р' + 28: 1, # 'С' + 34: 1, # 'Т' + 51: 1, # 'У' + 48: 1, # 'Ф' + 49: 1, # 'Х' + 53: 1, # 'Ц' + 50: 1, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 1, # 'Ъ' + 60: 1, # 'Ю' + 56: 1, # 'Я' + 1: 3, # 'а' + 18: 0, # 'б' + 9: 0, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 2, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 2, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 0, # 'л' + 14: 0, # 'м' + 6: 1, # 'н' + 4: 3, # 'о' + 13: 0, # 'п' + 7: 0, # 'р' + 8: 1, # 'с' + 5: 0, # 'т' + 19: 3, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 1, # 'ъ' + 52: 0, # 'ь' + 42: 1, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 28: { # 'С' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 3, # 'А' + 32: 2, # 'Б' + 35: 2, # 'В' + 43: 1, # 'Г' + 37: 2, # 'Д' + 44: 2, # 'Е' + 55: 1, # 'Ж' + 47: 1, # 'З' + 40: 2, # 'И' + 59: 0, # 'Й' + 33: 2, # 'К' + 46: 1, # 'Л' + 38: 1, # 'М' + 36: 1, # 'Н' + 41: 2, # 'О' + 30: 2, # 'П' + 39: 1, # 'Р' + 28: 2, # 'С' + 34: 2, # 'Т' + 51: 1, # 'У' + 48: 1, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 1, # 'Ъ' + 60: 1, # 'Ю' + 56: 1, # 'Я' + 1: 3, # 'а' + 18: 1, # 'б' + 9: 2, # 'в' + 20: 1, # 'г' + 11: 1, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 2, # 'к' + 10: 3, # 'л' + 14: 2, # 'м' + 6: 1, # 'н' + 4: 3, # 'о' + 13: 3, # 'п' + 7: 2, # 'р' + 8: 0, # 'с' + 5: 3, # 'т' + 19: 2, # 'у' + 29: 2, # 'ф' + 25: 1, # 'х' + 22: 1, # 'ц' + 21: 1, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 3, # 'ъ' + 52: 1, # 'ь' + 42: 1, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 34: { # 'Т' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 2, # 'А' + 32: 2, # 'Б' + 35: 1, # 'В' + 43: 0, # 'Г' + 37: 1, # 'Д' + 44: 2, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 2, # 'И' + 59: 0, # 'Й' + 33: 2, # 'К' + 46: 1, # 'Л' + 38: 1, # 'М' + 36: 1, # 'Н' + 41: 2, # 'О' + 30: 1, # 'П' + 39: 2, # 'Р' + 28: 2, # 'С' + 34: 1, # 'Т' + 51: 1, # 'У' + 48: 1, # 'Ф' + 49: 0, # 'Х' + 53: 1, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 1, # 'Ъ' + 60: 0, # 'Ю' + 56: 1, # 'Я' + 1: 3, # 'а' + 18: 1, # 'б' + 9: 1, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 2, # 'и' + 26: 0, # 'й' + 12: 1, # 'к' + 10: 1, # 'л' + 14: 0, # 'м' + 6: 0, # 'н' + 4: 3, # 'о' + 13: 0, # 'п' + 7: 3, # 'р' + 8: 0, # 'с' + 5: 0, # 'т' + 19: 2, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 2, # 'ъ' + 52: 0, # 'ь' + 42: 1, # 'ю' + 16: 2, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 51: { # 'У' + 63: 0, # 'e' + 45: 1, # '\xad' + 31: 1, # 'А' + 32: 1, # 'Б' + 35: 1, # 'В' + 43: 1, # 'Г' + 37: 1, # 'Д' + 44: 2, # 'Е' + 55: 1, # 'Ж' + 47: 1, # 'З' + 40: 1, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 1, # 'Л' + 38: 1, # 'М' + 36: 1, # 'Н' + 41: 0, # 'О' + 30: 1, # 'П' + 39: 1, # 'Р' + 28: 1, # 'С' + 34: 2, # 'Т' + 51: 0, # 'У' + 48: 1, # 'Ф' + 49: 1, # 'Х' + 53: 1, # 'Ц' + 50: 1, # 'Ч' + 54: 1, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 1, # 'а' + 18: 1, # 'б' + 9: 2, # 'в' + 20: 1, # 'г' + 11: 1, # 'д' + 3: 2, # 'е' + 23: 1, # 'ж' + 15: 1, # 'з' + 2: 2, # 'и' + 26: 1, # 'й' + 12: 2, # 'к' + 10: 1, # 'л' + 14: 1, # 'м' + 6: 2, # 'н' + 4: 2, # 'о' + 13: 1, # 'п' + 7: 1, # 'р' + 8: 2, # 'с' + 5: 1, # 'т' + 19: 1, # 'у' + 29: 0, # 'ф' + 25: 1, # 'х' + 22: 0, # 'ц' + 21: 2, # 'ч' + 27: 1, # 'ш' + 24: 0, # 'щ' + 17: 1, # 'ъ' + 52: 0, # 'ь' + 42: 0, # 'ю' + 16: 0, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 48: { # 'Ф' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 2, # 'А' + 32: 1, # 'Б' + 35: 1, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 1, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 2, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 1, # 'Л' + 38: 0, # 'М' + 36: 1, # 'Н' + 41: 1, # 'О' + 30: 2, # 'П' + 39: 1, # 'Р' + 28: 2, # 'С' + 34: 1, # 'Т' + 51: 1, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 2, # 'а' + 18: 0, # 'б' + 9: 0, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 2, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 2, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 2, # 'л' + 14: 0, # 'м' + 6: 0, # 'н' + 4: 2, # 'о' + 13: 0, # 'п' + 7: 2, # 'р' + 8: 0, # 'с' + 5: 0, # 'т' + 19: 1, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 1, # 'ъ' + 52: 1, # 'ь' + 42: 1, # 'ю' + 16: 0, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 49: { # 'Х' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 1, # 'А' + 32: 0, # 'Б' + 35: 1, # 'В' + 43: 1, # 'Г' + 37: 1, # 'Д' + 44: 1, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 1, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 1, # 'Л' + 38: 1, # 'М' + 36: 1, # 'Н' + 41: 1, # 'О' + 30: 1, # 'П' + 39: 1, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 1, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 2, # 'а' + 18: 0, # 'б' + 9: 1, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 2, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 2, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 1, # 'л' + 14: 1, # 'м' + 6: 0, # 'н' + 4: 2, # 'о' + 13: 0, # 'п' + 7: 2, # 'р' + 8: 0, # 'с' + 5: 0, # 'т' + 19: 2, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 2, # 'ъ' + 52: 1, # 'ь' + 42: 1, # 'ю' + 16: 0, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 53: { # 'Ц' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 1, # 'А' + 32: 0, # 'Б' + 35: 1, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 1, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 2, # 'И' + 59: 0, # 'Й' + 33: 2, # 'К' + 46: 1, # 'Л' + 38: 1, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 1, # 'Р' + 28: 2, # 'С' + 34: 0, # 'Т' + 51: 1, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 2, # 'а' + 18: 0, # 'б' + 9: 2, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 2, # 'е' + 23: 0, # 'ж' + 15: 1, # 'з' + 2: 2, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 0, # 'л' + 14: 0, # 'м' + 6: 0, # 'н' + 4: 1, # 'о' + 13: 0, # 'п' + 7: 1, # 'р' + 8: 0, # 'с' + 5: 0, # 'т' + 19: 1, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 1, # 'ъ' + 52: 0, # 'ь' + 42: 1, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 50: { # 'Ч' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 2, # 'А' + 32: 1, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 1, # 'Е' + 55: 0, # 'Ж' + 47: 1, # 'З' + 40: 1, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 1, # 'Л' + 38: 0, # 'М' + 36: 1, # 'Н' + 41: 1, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 1, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 2, # 'а' + 18: 0, # 'б' + 9: 0, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 3, # 'е' + 23: 1, # 'ж' + 15: 0, # 'з' + 2: 2, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 1, # 'л' + 14: 0, # 'м' + 6: 0, # 'н' + 4: 2, # 'о' + 13: 0, # 'п' + 7: 1, # 'р' + 8: 0, # 'с' + 5: 0, # 'т' + 19: 2, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 1, # 'ъ' + 52: 1, # 'ь' + 42: 0, # 'ю' + 16: 0, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 54: { # 'Ш' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 1, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 1, # 'Е' + 55: 0, # 'Ж' + 47: 1, # 'З' + 40: 1, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 1, # 'Н' + 41: 1, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 1, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 2, # 'а' + 18: 0, # 'б' + 9: 2, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 2, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 2, # 'и' + 26: 0, # 'й' + 12: 1, # 'к' + 10: 1, # 'л' + 14: 1, # 'м' + 6: 1, # 'н' + 4: 2, # 'о' + 13: 1, # 'п' + 7: 1, # 'р' + 8: 0, # 'с' + 5: 0, # 'т' + 19: 2, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 0, # 'ц' + 21: 1, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 1, # 'ъ' + 52: 1, # 'ь' + 42: 0, # 'ю' + 16: 0, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 57: { # 'Щ' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 1, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 1, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 1, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 1, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 2, # 'а' + 18: 0, # 'б' + 9: 0, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 2, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 1, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 0, # 'л' + 14: 0, # 'м' + 6: 0, # 'н' + 4: 1, # 'о' + 13: 0, # 'п' + 7: 1, # 'р' + 8: 0, # 'с' + 5: 0, # 'т' + 19: 1, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 1, # 'ъ' + 52: 0, # 'ь' + 42: 0, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 61: { # 'Ъ' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 1, # 'Б' + 35: 1, # 'В' + 43: 0, # 'Г' + 37: 1, # 'Д' + 44: 0, # 'Е' + 55: 1, # 'Ж' + 47: 1, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 2, # 'Л' + 38: 1, # 'М' + 36: 1, # 'Н' + 41: 0, # 'О' + 30: 1, # 'П' + 39: 2, # 'Р' + 28: 1, # 'С' + 34: 1, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 1, # 'Х' + 53: 1, # 'Ц' + 50: 1, # 'Ч' + 54: 1, # 'Ш' + 57: 1, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 0, # 'а' + 18: 0, # 'б' + 9: 0, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 0, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 0, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 1, # 'л' + 14: 0, # 'м' + 6: 1, # 'н' + 4: 0, # 'о' + 13: 0, # 'п' + 7: 1, # 'р' + 8: 0, # 'с' + 5: 0, # 'т' + 19: 0, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 0, # 'ъ' + 52: 0, # 'ь' + 42: 0, # 'ю' + 16: 0, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 60: { # 'Ю' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 1, # 'А' + 32: 1, # 'Б' + 35: 0, # 'В' + 43: 1, # 'Г' + 37: 1, # 'Д' + 44: 0, # 'Е' + 55: 1, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 1, # 'Л' + 38: 0, # 'М' + 36: 1, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 1, # 'Р' + 28: 1, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 0, # 'а' + 18: 1, # 'б' + 9: 1, # 'в' + 20: 2, # 'г' + 11: 1, # 'д' + 3: 0, # 'е' + 23: 2, # 'ж' + 15: 1, # 'з' + 2: 1, # 'и' + 26: 0, # 'й' + 12: 1, # 'к' + 10: 1, # 'л' + 14: 1, # 'м' + 6: 1, # 'н' + 4: 0, # 'о' + 13: 1, # 'п' + 7: 1, # 'р' + 8: 1, # 'с' + 5: 1, # 'т' + 19: 0, # 'у' + 29: 0, # 'ф' + 25: 1, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 0, # 'ъ' + 52: 0, # 'ь' + 42: 0, # 'ю' + 16: 0, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 56: { # 'Я' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 1, # 'Б' + 35: 1, # 'В' + 43: 1, # 'Г' + 37: 1, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 1, # 'Л' + 38: 1, # 'М' + 36: 1, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 1, # 'С' + 34: 2, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 0, # 'а' + 18: 1, # 'б' + 9: 1, # 'в' + 20: 1, # 'г' + 11: 1, # 'д' + 3: 0, # 'е' + 23: 0, # 'ж' + 15: 1, # 'з' + 2: 1, # 'и' + 26: 1, # 'й' + 12: 1, # 'к' + 10: 1, # 'л' + 14: 2, # 'м' + 6: 2, # 'н' + 4: 0, # 'о' + 13: 2, # 'п' + 7: 1, # 'р' + 8: 1, # 'с' + 5: 1, # 'т' + 19: 0, # 'у' + 29: 0, # 'ф' + 25: 1, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 1, # 'ш' + 24: 0, # 'щ' + 17: 0, # 'ъ' + 52: 0, # 'ь' + 42: 1, # 'ю' + 16: 0, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 1: { # 'а' + 63: 1, # 'e' + 45: 1, # '\xad' + 31: 1, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 1, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 1, # 'а' + 18: 3, # 'б' + 9: 3, # 'в' + 20: 3, # 'г' + 11: 3, # 'д' + 3: 3, # 'е' + 23: 3, # 'ж' + 15: 3, # 'з' + 2: 3, # 'и' + 26: 3, # 'й' + 12: 3, # 'к' + 10: 3, # 'л' + 14: 3, # 'м' + 6: 3, # 'н' + 4: 2, # 'о' + 13: 3, # 'п' + 7: 3, # 'р' + 8: 3, # 'с' + 5: 3, # 'т' + 19: 3, # 'у' + 29: 3, # 'ф' + 25: 3, # 'х' + 22: 3, # 'ц' + 21: 3, # 'ч' + 27: 3, # 'ш' + 24: 3, # 'щ' + 17: 0, # 'ъ' + 52: 0, # 'ь' + 42: 1, # 'ю' + 16: 3, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 18: { # 'б' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 0, # 'б' + 9: 3, # 'в' + 20: 1, # 'г' + 11: 2, # 'д' + 3: 3, # 'е' + 23: 1, # 'ж' + 15: 1, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 1, # 'к' + 10: 3, # 'л' + 14: 2, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 1, # 'п' + 7: 3, # 'р' + 8: 3, # 'с' + 5: 0, # 'т' + 19: 3, # 'у' + 29: 0, # 'ф' + 25: 2, # 'х' + 22: 1, # 'ц' + 21: 1, # 'ч' + 27: 1, # 'ш' + 24: 3, # 'щ' + 17: 3, # 'ъ' + 52: 1, # 'ь' + 42: 2, # 'ю' + 16: 3, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 9: { # 'в' + 63: 1, # 'e' + 45: 1, # '\xad' + 31: 0, # 'А' + 32: 1, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 1, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 1, # 'б' + 9: 0, # 'в' + 20: 2, # 'г' + 11: 3, # 'д' + 3: 3, # 'е' + 23: 1, # 'ж' + 15: 3, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 3, # 'к' + 10: 3, # 'л' + 14: 2, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 2, # 'п' + 7: 3, # 'р' + 8: 3, # 'с' + 5: 3, # 'т' + 19: 2, # 'у' + 29: 0, # 'ф' + 25: 2, # 'х' + 22: 2, # 'ц' + 21: 3, # 'ч' + 27: 2, # 'ш' + 24: 1, # 'щ' + 17: 3, # 'ъ' + 52: 1, # 'ь' + 42: 2, # 'ю' + 16: 3, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 20: { # 'г' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 1, # 'б' + 9: 2, # 'в' + 20: 1, # 'г' + 11: 2, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 1, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 1, # 'к' + 10: 3, # 'л' + 14: 1, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 1, # 'п' + 7: 3, # 'р' + 8: 2, # 'с' + 5: 2, # 'т' + 19: 3, # 'у' + 29: 1, # 'ф' + 25: 1, # 'х' + 22: 0, # 'ц' + 21: 1, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 3, # 'ъ' + 52: 1, # 'ь' + 42: 1, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 11: { # 'д' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 2, # 'б' + 9: 3, # 'в' + 20: 2, # 'г' + 11: 2, # 'д' + 3: 3, # 'е' + 23: 3, # 'ж' + 15: 2, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 3, # 'к' + 10: 3, # 'л' + 14: 3, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 3, # 'п' + 7: 3, # 'р' + 8: 3, # 'с' + 5: 1, # 'т' + 19: 3, # 'у' + 29: 1, # 'ф' + 25: 2, # 'х' + 22: 2, # 'ц' + 21: 2, # 'ч' + 27: 1, # 'ш' + 24: 1, # 'щ' + 17: 3, # 'ъ' + 52: 1, # 'ь' + 42: 1, # 'ю' + 16: 3, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 3: { # 'е' + 63: 0, # 'e' + 45: 1, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 2, # 'а' + 18: 3, # 'б' + 9: 3, # 'в' + 20: 3, # 'г' + 11: 3, # 'д' + 3: 2, # 'е' + 23: 3, # 'ж' + 15: 3, # 'з' + 2: 2, # 'и' + 26: 3, # 'й' + 12: 3, # 'к' + 10: 3, # 'л' + 14: 3, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 3, # 'п' + 7: 3, # 'р' + 8: 3, # 'с' + 5: 3, # 'т' + 19: 2, # 'у' + 29: 3, # 'ф' + 25: 3, # 'х' + 22: 3, # 'ц' + 21: 3, # 'ч' + 27: 3, # 'ш' + 24: 3, # 'щ' + 17: 1, # 'ъ' + 52: 0, # 'ь' + 42: 1, # 'ю' + 16: 3, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 23: { # 'ж' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 3, # 'б' + 9: 2, # 'в' + 20: 1, # 'г' + 11: 3, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 2, # 'к' + 10: 1, # 'л' + 14: 1, # 'м' + 6: 3, # 'н' + 4: 2, # 'о' + 13: 1, # 'п' + 7: 1, # 'р' + 8: 1, # 'с' + 5: 1, # 'т' + 19: 2, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 1, # 'ц' + 21: 1, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 2, # 'ъ' + 52: 0, # 'ь' + 42: 0, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 15: { # 'з' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 3, # 'б' + 9: 3, # 'в' + 20: 3, # 'г' + 11: 3, # 'д' + 3: 3, # 'е' + 23: 1, # 'ж' + 15: 1, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 3, # 'к' + 10: 3, # 'л' + 14: 3, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 3, # 'п' + 7: 3, # 'р' + 8: 3, # 'с' + 5: 3, # 'т' + 19: 3, # 'у' + 29: 1, # 'ф' + 25: 2, # 'х' + 22: 2, # 'ц' + 21: 2, # 'ч' + 27: 2, # 'ш' + 24: 1, # 'щ' + 17: 2, # 'ъ' + 52: 1, # 'ь' + 42: 1, # 'ю' + 16: 2, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 2: { # 'и' + 63: 1, # 'e' + 45: 1, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 1, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 1, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 1, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 1, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 3, # 'б' + 9: 3, # 'в' + 20: 3, # 'г' + 11: 3, # 'д' + 3: 3, # 'е' + 23: 3, # 'ж' + 15: 3, # 'з' + 2: 3, # 'и' + 26: 3, # 'й' + 12: 3, # 'к' + 10: 3, # 'л' + 14: 3, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 3, # 'п' + 7: 3, # 'р' + 8: 3, # 'с' + 5: 3, # 'т' + 19: 2, # 'у' + 29: 3, # 'ф' + 25: 3, # 'х' + 22: 3, # 'ц' + 21: 3, # 'ч' + 27: 3, # 'ш' + 24: 3, # 'щ' + 17: 2, # 'ъ' + 52: 0, # 'ь' + 42: 1, # 'ю' + 16: 3, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 26: { # 'й' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 1, # 'а' + 18: 2, # 'б' + 9: 2, # 'в' + 20: 1, # 'г' + 11: 2, # 'д' + 3: 2, # 'е' + 23: 0, # 'ж' + 15: 2, # 'з' + 2: 1, # 'и' + 26: 0, # 'й' + 12: 3, # 'к' + 10: 2, # 'л' + 14: 2, # 'м' + 6: 3, # 'н' + 4: 2, # 'о' + 13: 1, # 'п' + 7: 2, # 'р' + 8: 3, # 'с' + 5: 3, # 'т' + 19: 1, # 'у' + 29: 2, # 'ф' + 25: 1, # 'х' + 22: 2, # 'ц' + 21: 2, # 'ч' + 27: 1, # 'ш' + 24: 1, # 'щ' + 17: 1, # 'ъ' + 52: 0, # 'ь' + 42: 0, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 12: { # 'к' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 1, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 1, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 1, # 'б' + 9: 3, # 'в' + 20: 2, # 'г' + 11: 1, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 2, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 1, # 'к' + 10: 3, # 'л' + 14: 2, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 1, # 'п' + 7: 3, # 'р' + 8: 3, # 'с' + 5: 3, # 'т' + 19: 3, # 'у' + 29: 1, # 'ф' + 25: 1, # 'х' + 22: 3, # 'ц' + 21: 2, # 'ч' + 27: 1, # 'ш' + 24: 0, # 'щ' + 17: 3, # 'ъ' + 52: 1, # 'ь' + 42: 2, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 10: { # 'л' + 63: 1, # 'e' + 45: 1, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 1, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 3, # 'б' + 9: 3, # 'в' + 20: 3, # 'г' + 11: 2, # 'д' + 3: 3, # 'е' + 23: 3, # 'ж' + 15: 2, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 3, # 'к' + 10: 1, # 'л' + 14: 2, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 2, # 'п' + 7: 2, # 'р' + 8: 3, # 'с' + 5: 3, # 'т' + 19: 3, # 'у' + 29: 2, # 'ф' + 25: 2, # 'х' + 22: 2, # 'ц' + 21: 2, # 'ч' + 27: 2, # 'ш' + 24: 1, # 'щ' + 17: 3, # 'ъ' + 52: 2, # 'ь' + 42: 3, # 'ю' + 16: 3, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 14: { # 'м' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 1, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 3, # 'б' + 9: 3, # 'в' + 20: 1, # 'г' + 11: 1, # 'д' + 3: 3, # 'е' + 23: 1, # 'ж' + 15: 1, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 2, # 'к' + 10: 3, # 'л' + 14: 1, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 3, # 'п' + 7: 2, # 'р' + 8: 2, # 'с' + 5: 1, # 'т' + 19: 3, # 'у' + 29: 2, # 'ф' + 25: 1, # 'х' + 22: 2, # 'ц' + 21: 2, # 'ч' + 27: 2, # 'ш' + 24: 1, # 'щ' + 17: 3, # 'ъ' + 52: 1, # 'ь' + 42: 2, # 'ю' + 16: 3, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 6: { # 'н' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 1, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 2, # 'б' + 9: 2, # 'в' + 20: 3, # 'г' + 11: 3, # 'д' + 3: 3, # 'е' + 23: 2, # 'ж' + 15: 2, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 3, # 'к' + 10: 2, # 'л' + 14: 1, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 1, # 'п' + 7: 2, # 'р' + 8: 3, # 'с' + 5: 3, # 'т' + 19: 3, # 'у' + 29: 3, # 'ф' + 25: 2, # 'х' + 22: 3, # 'ц' + 21: 3, # 'ч' + 27: 2, # 'ш' + 24: 1, # 'щ' + 17: 3, # 'ъ' + 52: 2, # 'ь' + 42: 2, # 'ю' + 16: 3, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 4: { # 'о' + 63: 0, # 'e' + 45: 1, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 2, # 'а' + 18: 3, # 'б' + 9: 3, # 'в' + 20: 3, # 'г' + 11: 3, # 'д' + 3: 3, # 'е' + 23: 3, # 'ж' + 15: 3, # 'з' + 2: 3, # 'и' + 26: 3, # 'й' + 12: 3, # 'к' + 10: 3, # 'л' + 14: 3, # 'м' + 6: 3, # 'н' + 4: 2, # 'о' + 13: 3, # 'п' + 7: 3, # 'р' + 8: 3, # 'с' + 5: 3, # 'т' + 19: 2, # 'у' + 29: 3, # 'ф' + 25: 3, # 'х' + 22: 3, # 'ц' + 21: 3, # 'ч' + 27: 3, # 'ш' + 24: 3, # 'щ' + 17: 1, # 'ъ' + 52: 0, # 'ь' + 42: 1, # 'ю' + 16: 3, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 13: { # 'п' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 1, # 'б' + 9: 2, # 'в' + 20: 1, # 'г' + 11: 1, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 1, # 'з' + 2: 3, # 'и' + 26: 1, # 'й' + 12: 2, # 'к' + 10: 3, # 'л' + 14: 1, # 'м' + 6: 2, # 'н' + 4: 3, # 'о' + 13: 1, # 'п' + 7: 3, # 'р' + 8: 2, # 'с' + 5: 2, # 'т' + 19: 3, # 'у' + 29: 1, # 'ф' + 25: 1, # 'х' + 22: 2, # 'ц' + 21: 2, # 'ч' + 27: 1, # 'ш' + 24: 1, # 'щ' + 17: 3, # 'ъ' + 52: 1, # 'ь' + 42: 2, # 'ю' + 16: 2, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 7: { # 'р' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 3, # 'б' + 9: 3, # 'в' + 20: 3, # 'г' + 11: 3, # 'д' + 3: 3, # 'е' + 23: 3, # 'ж' + 15: 2, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 3, # 'к' + 10: 3, # 'л' + 14: 3, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 2, # 'п' + 7: 1, # 'р' + 8: 3, # 'с' + 5: 3, # 'т' + 19: 3, # 'у' + 29: 2, # 'ф' + 25: 3, # 'х' + 22: 3, # 'ц' + 21: 2, # 'ч' + 27: 3, # 'ш' + 24: 1, # 'щ' + 17: 3, # 'ъ' + 52: 1, # 'ь' + 42: 2, # 'ю' + 16: 3, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 8: { # 'с' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 2, # 'б' + 9: 3, # 'в' + 20: 2, # 'г' + 11: 2, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 1, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 3, # 'к' + 10: 3, # 'л' + 14: 3, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 3, # 'п' + 7: 3, # 'р' + 8: 1, # 'с' + 5: 3, # 'т' + 19: 3, # 'у' + 29: 2, # 'ф' + 25: 2, # 'х' + 22: 2, # 'ц' + 21: 2, # 'ч' + 27: 2, # 'ш' + 24: 0, # 'щ' + 17: 3, # 'ъ' + 52: 2, # 'ь' + 42: 2, # 'ю' + 16: 3, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 5: { # 'т' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 3, # 'б' + 9: 3, # 'в' + 20: 2, # 'г' + 11: 2, # 'д' + 3: 3, # 'е' + 23: 1, # 'ж' + 15: 1, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 3, # 'к' + 10: 3, # 'л' + 14: 2, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 2, # 'п' + 7: 3, # 'р' + 8: 3, # 'с' + 5: 3, # 'т' + 19: 3, # 'у' + 29: 1, # 'ф' + 25: 2, # 'х' + 22: 2, # 'ц' + 21: 2, # 'ч' + 27: 1, # 'ш' + 24: 1, # 'щ' + 17: 3, # 'ъ' + 52: 2, # 'ь' + 42: 2, # 'ю' + 16: 3, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 19: { # 'у' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 3, # 'б' + 9: 3, # 'в' + 20: 3, # 'г' + 11: 3, # 'д' + 3: 2, # 'е' + 23: 3, # 'ж' + 15: 3, # 'з' + 2: 2, # 'и' + 26: 2, # 'й' + 12: 3, # 'к' + 10: 3, # 'л' + 14: 3, # 'м' + 6: 3, # 'н' + 4: 2, # 'о' + 13: 3, # 'п' + 7: 3, # 'р' + 8: 3, # 'с' + 5: 3, # 'т' + 19: 1, # 'у' + 29: 2, # 'ф' + 25: 2, # 'х' + 22: 2, # 'ц' + 21: 3, # 'ч' + 27: 3, # 'ш' + 24: 2, # 'щ' + 17: 1, # 'ъ' + 52: 0, # 'ь' + 42: 1, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 29: { # 'ф' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 1, # 'б' + 9: 1, # 'в' + 20: 1, # 'г' + 11: 0, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 2, # 'к' + 10: 2, # 'л' + 14: 1, # 'м' + 6: 1, # 'н' + 4: 3, # 'о' + 13: 0, # 'п' + 7: 2, # 'р' + 8: 2, # 'с' + 5: 2, # 'т' + 19: 2, # 'у' + 29: 0, # 'ф' + 25: 1, # 'х' + 22: 0, # 'ц' + 21: 1, # 'ч' + 27: 1, # 'ш' + 24: 0, # 'щ' + 17: 2, # 'ъ' + 52: 2, # 'ь' + 42: 1, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 25: { # 'х' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 1, # 'б' + 9: 3, # 'в' + 20: 0, # 'г' + 11: 1, # 'д' + 3: 2, # 'е' + 23: 0, # 'ж' + 15: 1, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 1, # 'к' + 10: 2, # 'л' + 14: 2, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 1, # 'п' + 7: 3, # 'р' + 8: 1, # 'с' + 5: 2, # 'т' + 19: 3, # 'у' + 29: 0, # 'ф' + 25: 1, # 'х' + 22: 0, # 'ц' + 21: 1, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 2, # 'ъ' + 52: 0, # 'ь' + 42: 1, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 22: { # 'ц' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 1, # 'б' + 9: 2, # 'в' + 20: 1, # 'г' + 11: 1, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 1, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 2, # 'к' + 10: 1, # 'л' + 14: 1, # 'м' + 6: 1, # 'н' + 4: 2, # 'о' + 13: 1, # 'п' + 7: 1, # 'р' + 8: 1, # 'с' + 5: 1, # 'т' + 19: 2, # 'у' + 29: 1, # 'ф' + 25: 1, # 'х' + 22: 1, # 'ц' + 21: 1, # 'ч' + 27: 1, # 'ш' + 24: 1, # 'щ' + 17: 2, # 'ъ' + 52: 1, # 'ь' + 42: 0, # 'ю' + 16: 2, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 21: { # 'ч' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 1, # 'б' + 9: 3, # 'в' + 20: 1, # 'г' + 11: 0, # 'д' + 3: 3, # 'е' + 23: 1, # 'ж' + 15: 0, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 3, # 'к' + 10: 2, # 'л' + 14: 2, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 0, # 'п' + 7: 2, # 'р' + 8: 0, # 'с' + 5: 2, # 'т' + 19: 3, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 1, # 'ш' + 24: 0, # 'щ' + 17: 2, # 'ъ' + 52: 0, # 'ь' + 42: 1, # 'ю' + 16: 0, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 27: { # 'ш' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 0, # 'б' + 9: 2, # 'в' + 20: 0, # 'г' + 11: 1, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 3, # 'к' + 10: 2, # 'л' + 14: 1, # 'м' + 6: 3, # 'н' + 4: 2, # 'о' + 13: 2, # 'п' + 7: 1, # 'р' + 8: 0, # 'с' + 5: 1, # 'т' + 19: 2, # 'у' + 29: 1, # 'ф' + 25: 0, # 'х' + 22: 0, # 'ц' + 21: 1, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 2, # 'ъ' + 52: 1, # 'ь' + 42: 1, # 'ю' + 16: 0, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 24: { # 'щ' + 63: 1, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 3, # 'а' + 18: 0, # 'б' + 9: 1, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 3, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 3, # 'и' + 26: 0, # 'й' + 12: 1, # 'к' + 10: 0, # 'л' + 14: 0, # 'м' + 6: 2, # 'н' + 4: 3, # 'о' + 13: 0, # 'п' + 7: 1, # 'р' + 8: 0, # 'с' + 5: 2, # 'т' + 19: 3, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 1, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 1, # 'ъ' + 52: 0, # 'ь' + 42: 0, # 'ю' + 16: 2, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 17: { # 'ъ' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 1, # 'а' + 18: 3, # 'б' + 9: 3, # 'в' + 20: 3, # 'г' + 11: 3, # 'д' + 3: 2, # 'е' + 23: 3, # 'ж' + 15: 3, # 'з' + 2: 1, # 'и' + 26: 2, # 'й' + 12: 3, # 'к' + 10: 3, # 'л' + 14: 3, # 'м' + 6: 3, # 'н' + 4: 3, # 'о' + 13: 3, # 'п' + 7: 3, # 'р' + 8: 3, # 'с' + 5: 3, # 'т' + 19: 1, # 'у' + 29: 1, # 'ф' + 25: 2, # 'х' + 22: 2, # 'ц' + 21: 3, # 'ч' + 27: 2, # 'ш' + 24: 3, # 'щ' + 17: 0, # 'ъ' + 52: 0, # 'ь' + 42: 2, # 'ю' + 16: 0, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 52: { # 'ь' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 0, # 'а' + 18: 0, # 'б' + 9: 0, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 1, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 0, # 'и' + 26: 0, # 'й' + 12: 1, # 'к' + 10: 0, # 'л' + 14: 0, # 'м' + 6: 1, # 'н' + 4: 3, # 'о' + 13: 0, # 'п' + 7: 0, # 'р' + 8: 0, # 'с' + 5: 1, # 'т' + 19: 0, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 1, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 0, # 'ъ' + 52: 0, # 'ь' + 42: 1, # 'ю' + 16: 0, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 42: { # 'ю' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 1, # 'а' + 18: 2, # 'б' + 9: 1, # 'в' + 20: 2, # 'г' + 11: 2, # 'д' + 3: 1, # 'е' + 23: 2, # 'ж' + 15: 2, # 'з' + 2: 1, # 'и' + 26: 1, # 'й' + 12: 2, # 'к' + 10: 2, # 'л' + 14: 2, # 'м' + 6: 2, # 'н' + 4: 1, # 'о' + 13: 1, # 'п' + 7: 2, # 'р' + 8: 2, # 'с' + 5: 2, # 'т' + 19: 1, # 'у' + 29: 1, # 'ф' + 25: 1, # 'х' + 22: 2, # 'ц' + 21: 3, # 'ч' + 27: 1, # 'ш' + 24: 1, # 'щ' + 17: 1, # 'ъ' + 52: 0, # 'ь' + 42: 0, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 16: { # 'я' + 63: 0, # 'e' + 45: 1, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 0, # 'а' + 18: 3, # 'б' + 9: 3, # 'в' + 20: 2, # 'г' + 11: 3, # 'д' + 3: 2, # 'е' + 23: 1, # 'ж' + 15: 2, # 'з' + 2: 1, # 'и' + 26: 2, # 'й' + 12: 3, # 'к' + 10: 3, # 'л' + 14: 3, # 'м' + 6: 3, # 'н' + 4: 1, # 'о' + 13: 2, # 'п' + 7: 2, # 'р' + 8: 3, # 'с' + 5: 3, # 'т' + 19: 1, # 'у' + 29: 1, # 'ф' + 25: 3, # 'х' + 22: 2, # 'ц' + 21: 1, # 'ч' + 27: 1, # 'ш' + 24: 2, # 'щ' + 17: 0, # 'ъ' + 52: 0, # 'ь' + 42: 0, # 'ю' + 16: 1, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 58: { # 'є' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 0, # 'а' + 18: 0, # 'б' + 9: 0, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 0, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 0, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 0, # 'л' + 14: 0, # 'м' + 6: 0, # 'н' + 4: 0, # 'о' + 13: 0, # 'п' + 7: 0, # 'р' + 8: 0, # 'с' + 5: 0, # 'т' + 19: 0, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 0, # 'ъ' + 52: 0, # 'ь' + 42: 0, # 'ю' + 16: 0, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, + 62: { # '№' + 63: 0, # 'e' + 45: 0, # '\xad' + 31: 0, # 'А' + 32: 0, # 'Б' + 35: 0, # 'В' + 43: 0, # 'Г' + 37: 0, # 'Д' + 44: 0, # 'Е' + 55: 0, # 'Ж' + 47: 0, # 'З' + 40: 0, # 'И' + 59: 0, # 'Й' + 33: 0, # 'К' + 46: 0, # 'Л' + 38: 0, # 'М' + 36: 0, # 'Н' + 41: 0, # 'О' + 30: 0, # 'П' + 39: 0, # 'Р' + 28: 0, # 'С' + 34: 0, # 'Т' + 51: 0, # 'У' + 48: 0, # 'Ф' + 49: 0, # 'Х' + 53: 0, # 'Ц' + 50: 0, # 'Ч' + 54: 0, # 'Ш' + 57: 0, # 'Щ' + 61: 0, # 'Ъ' + 60: 0, # 'Ю' + 56: 0, # 'Я' + 1: 0, # 'а' + 18: 0, # 'б' + 9: 0, # 'в' + 20: 0, # 'г' + 11: 0, # 'д' + 3: 0, # 'е' + 23: 0, # 'ж' + 15: 0, # 'з' + 2: 0, # 'и' + 26: 0, # 'й' + 12: 0, # 'к' + 10: 0, # 'л' + 14: 0, # 'м' + 6: 0, # 'н' + 4: 0, # 'о' + 13: 0, # 'п' + 7: 0, # 'р' + 8: 0, # 'с' + 5: 0, # 'т' + 19: 0, # 'у' + 29: 0, # 'ф' + 25: 0, # 'х' + 22: 0, # 'ц' + 21: 0, # 'ч' + 27: 0, # 'ш' + 24: 0, # 'щ' + 17: 0, # 'ъ' + 52: 0, # 'ь' + 42: 0, # 'ю' + 16: 0, # 'я' + 58: 0, # 'є' + 62: 0, # '№' + }, +} -# Model Table: -# total sequences: 100% -# first 512 sequences: 96.9392% -# first 1024 sequences:3.0618% -# rest sequences: 0.2992% -# negative sequences: 0.0020% -BulgarianLangModel = ( -0,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,3,3,3,3,3,3,3,2,3,3,3,3,3, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,0,3,3,3,2,2,3,2,2,1,2,2, -3,1,3,3,2,3,3,3,3,3,3,3,3,3,3,3,3,0,3,3,3,3,3,3,3,3,3,3,0,3,0,1, -0,0,0,0,0,0,0,0,0,0,1,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,2,3,3,3,3,3,3,3,3,0,3,1,0, -0,1,0,0,0,0,0,0,0,0,1,1,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, -3,2,2,2,3,3,3,3,3,3,3,3,3,3,3,3,3,1,3,2,3,3,3,3,3,3,3,3,0,3,0,0, -0,0,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,2,3,3,2,3,3,3,3,3,3,3,3,3,3,3,3,1,3,2,3,3,3,3,3,3,3,3,0,3,0,0, -0,0,0,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,3,3,3,3,2,3,2,2,1,3,3,3,3,2,2,2,1,1,2,0,1,0,1,0,0, -0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,1, -3,3,3,3,3,3,3,2,3,2,2,3,3,1,1,2,3,3,2,3,3,3,3,2,1,2,0,2,0,3,0,0, -0,0,0,0,0,0,0,1,0,0,2,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,1, -3,3,3,3,3,3,3,1,3,3,3,3,3,2,3,2,3,3,3,3,3,2,3,3,1,3,0,3,0,2,0,0, -0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1, -3,3,3,3,3,3,3,3,1,3,3,2,3,3,3,1,3,3,2,3,2,2,2,0,0,2,0,2,0,2,0,0, -0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,1, -3,3,3,3,3,3,3,3,3,0,3,3,3,2,2,3,3,3,1,2,2,3,2,1,1,2,0,2,0,0,0,0, -1,0,0,0,0,0,0,0,0,0,2,0,0,1,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1, -3,3,3,3,3,3,3,2,3,3,1,2,3,2,2,2,3,3,3,3,3,2,2,3,1,2,0,2,1,2,0,0, -0,0,0,0,0,0,0,0,0,0,3,0,0,1,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,1, -3,3,3,3,3,1,3,3,3,3,3,2,3,3,3,2,3,3,2,3,2,2,2,3,1,2,0,1,0,1,0,0, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1, -3,3,3,3,3,3,3,3,3,3,3,1,1,1,2,2,1,3,1,3,2,2,3,0,0,1,0,1,0,1,0,0, -0,0,0,1,0,0,0,0,1,0,2,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1, -3,3,3,3,3,2,2,3,2,2,3,1,2,1,1,1,2,3,1,3,1,2,2,0,1,1,1,1,0,1,0,0, -0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1, -3,3,3,3,3,1,3,2,2,3,3,1,2,3,1,1,3,3,3,3,1,2,2,1,1,1,0,2,0,2,0,1, -0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,2,2,3,3,3,2,2,1,1,2,0,2,0,1,0,0, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1, -3,0,1,2,1,3,3,2,3,3,3,3,3,2,3,2,1,0,3,1,2,1,2,1,2,3,2,1,0,1,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,1,1,2,3,3,3,3,3,3,3,3,3,3,3,3,0,0,3,1,3,3,2,3,3,2,2,2,0,1,0,0, -0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,3,3,3,3,0,3,3,3,3,3,2,1,1,2,1,3,3,0,3,1,1,1,1,3,2,0,1,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1, -3,3,2,2,2,3,3,3,3,3,3,3,3,3,3,3,1,1,3,1,3,3,2,3,2,2,2,3,0,2,0,0, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,2,3,3,2,2,3,2,1,1,1,1,1,3,1,3,1,1,0,0,0,1,0,0,0,1,0,0, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,2,3,2,0,3,2,0,3,0,2,0,0,2,1,3,1,0,0,1,0,0,0,1,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, -3,3,3,3,2,1,1,1,1,2,1,1,2,1,1,1,2,2,1,2,1,1,1,0,1,1,0,1,0,1,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1, -3,3,3,3,2,1,3,1,1,2,1,3,2,1,1,0,1,2,3,2,1,1,1,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,3,3,3,3,2,2,1,0,1,0,0,1,0,0,0,2,1,0,3,0,0,1,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, -3,3,3,2,3,2,3,3,1,3,2,1,1,1,2,1,1,2,1,3,0,1,0,0,0,1,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,1,1,2,2,3,3,2,3,2,2,2,3,1,2,2,1,1,2,1,1,2,2,0,1,1,0,1,0,2,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,2,1,3,1,0,2,2,1,3,2,1,0,0,2,0,2,0,1,0,0,0,0,0,0,0,1,0,0, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1, -3,3,3,3,3,3,1,2,0,2,3,1,2,3,2,0,1,3,1,2,1,1,1,0,0,1,0,0,2,2,2,3, -2,2,2,2,1,2,1,1,2,2,1,1,2,0,1,1,1,0,0,1,1,0,0,1,1,0,0,0,1,1,0,1, -3,3,3,3,3,2,1,2,2,1,2,0,2,0,1,0,1,2,1,2,1,1,0,0,0,1,0,1,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,1, -3,3,2,3,3,1,1,3,1,0,3,2,1,0,0,0,1,2,0,2,0,1,0,0,0,1,0,1,2,1,2,2, -1,1,1,1,1,1,1,2,2,2,1,1,1,1,1,1,1,0,1,2,1,1,1,0,0,0,0,0,1,1,0,0, -3,1,0,1,0,2,3,2,2,2,3,2,2,2,2,2,1,0,2,1,2,1,1,1,0,1,2,1,2,2,2,1, -1,1,2,2,2,2,1,2,1,1,0,1,2,1,2,2,2,1,1,1,0,1,1,1,1,2,0,1,0,0,0,0, -2,3,2,3,3,0,0,2,1,0,2,1,0,0,0,0,2,3,0,2,0,0,0,0,0,1,0,0,2,0,1,2, -2,1,2,1,2,2,1,1,1,2,1,1,1,0,1,2,2,1,1,1,1,1,0,1,1,1,0,0,1,2,0,0, -3,3,2,2,3,0,2,3,1,1,2,0,0,0,1,0,0,2,0,2,0,0,0,1,0,1,0,1,2,0,2,2, -1,1,1,1,2,1,0,1,2,2,2,1,1,1,1,1,1,1,0,1,1,1,0,0,0,0,0,0,1,1,0,0, -2,3,2,3,3,0,0,3,0,1,1,0,1,0,0,0,2,2,1,2,0,0,0,0,0,0,0,0,2,0,1,2, -2,2,1,1,1,1,1,2,2,2,1,0,2,0,1,0,1,0,0,1,0,1,0,0,1,0,0,0,0,1,0,0, -3,3,3,3,2,2,2,2,2,0,2,1,1,1,1,2,1,2,1,1,0,2,0,1,0,1,0,0,2,0,1,2, -1,1,1,1,1,1,1,2,2,1,1,0,2,0,1,0,2,0,0,1,1,1,0,0,2,0,0,0,1,1,0,0, -2,3,3,3,3,1,0,0,0,0,0,0,0,0,0,0,2,0,0,1,1,0,0,0,0,0,0,1,2,0,1,2, -2,2,2,1,1,2,1,1,2,2,2,1,2,0,1,1,1,1,1,1,0,1,1,1,1,0,0,1,1,1,0,0, -2,3,3,3,3,0,2,2,0,2,1,0,0,0,1,1,1,2,0,2,0,0,0,3,0,0,0,0,2,0,2,2, -1,1,1,2,1,2,1,1,2,2,2,1,2,0,1,1,1,0,1,1,1,1,0,2,1,0,0,0,1,1,0,0, -2,3,3,3,3,0,2,1,0,0,2,0,0,0,0,0,1,2,0,2,0,0,0,0,0,0,0,0,2,0,1,2, -1,1,1,2,1,1,1,1,2,2,2,0,1,0,1,1,1,0,0,1,1,1,0,0,1,0,0,0,0,1,0,0, -3,3,2,2,3,0,1,0,1,0,0,0,0,0,0,0,1,1,0,3,0,0,0,0,0,0,0,0,1,0,2,2, -1,1,1,1,1,2,1,1,2,2,1,2,2,1,0,1,1,1,1,1,0,1,0,0,1,0,0,0,1,1,0,0, -3,1,0,1,0,2,2,2,2,3,2,1,1,1,2,3,0,0,1,0,2,1,1,0,1,1,1,1,2,1,1,1, -1,2,2,1,2,1,2,2,1,1,0,1,2,1,2,2,1,1,1,0,0,1,1,1,2,1,0,1,0,0,0,0, -2,1,0,1,0,3,1,2,2,2,2,1,2,2,1,1,1,0,2,1,2,2,1,1,2,1,1,0,2,1,1,1, -1,2,2,2,2,2,2,2,1,2,0,1,1,0,2,1,1,1,1,1,0,0,1,1,1,1,0,1,0,0,0,0, -2,1,1,1,1,2,2,2,2,1,2,2,2,1,2,2,1,1,2,1,2,3,2,2,1,1,1,1,0,1,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,2,2,3,2,0,1,2,0,1,2,1,1,0,1,0,1,2,1,2,0,0,0,1,1,0,0,0,1,0,0,2, -1,1,0,0,1,1,0,1,1,1,1,0,2,0,1,1,1,0,0,1,1,0,0,0,0,1,0,0,0,1,0,0, -2,0,0,0,0,1,2,2,2,2,2,2,2,1,2,1,1,1,1,1,1,1,0,1,1,1,1,1,2,1,1,1, -1,2,2,2,2,1,1,2,1,2,1,1,1,0,2,1,2,1,1,1,0,2,1,1,1,1,0,1,0,0,0,0, -3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0, -1,1,0,1,0,1,1,1,1,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,2,2,3,2,0,0,0,0,1,0,0,0,0,0,0,1,1,0,2,0,0,0,0,0,0,0,0,1,0,1,2, -1,1,1,1,1,1,0,0,2,2,2,2,2,0,1,1,0,1,1,1,1,1,0,0,1,0,0,0,1,1,0,1, -2,3,1,2,1,0,1,1,0,2,2,2,0,0,1,0,0,1,1,1,1,0,0,0,0,0,0,0,1,0,1,2, -1,1,1,1,2,1,1,1,1,1,1,1,1,0,1,1,0,1,0,1,0,1,0,0,1,0,0,0,0,1,0,0, -2,2,2,2,2,0,0,2,0,0,2,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,2,0,2,2, -1,1,1,1,1,0,0,1,2,1,1,0,1,0,1,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0, -1,2,2,2,2,0,0,2,0,1,1,0,0,0,1,0,0,2,0,2,0,0,0,0,0,0,0,0,0,0,1,1, -0,0,0,1,1,1,1,1,1,1,1,1,1,0,1,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0, -1,2,2,3,2,0,0,1,0,0,1,0,0,0,0,0,0,1,0,2,0,0,0,1,0,0,0,0,0,0,0,2, -1,1,0,0,1,0,0,0,1,1,0,0,1,0,1,1,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0, -2,1,2,2,2,1,2,1,2,2,1,1,2,1,1,1,0,1,1,1,1,2,0,1,0,1,1,1,1,0,1,1, -1,1,2,1,1,1,1,1,1,0,0,1,2,1,1,1,1,1,1,0,0,1,1,1,0,0,0,0,0,0,0,0, -1,0,0,1,3,1,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,2,2,2,1,0,0,1,0,2,0,0,0,0,0,1,1,1,0,1,0,0,0,0,0,0,0,0,2,0,0,1, -0,2,0,1,0,0,1,1,2,0,1,0,1,0,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0, -1,2,2,2,2,0,1,1,0,2,1,0,1,1,1,0,0,1,0,2,0,1,0,0,0,0,0,0,0,0,0,1, -0,1,0,0,1,0,0,0,1,1,0,0,1,0,0,1,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0, -2,2,2,2,2,0,0,1,0,0,0,1,0,1,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,1, -0,1,0,1,1,1,0,0,1,1,1,0,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0, -2,0,1,0,0,1,2,1,1,1,1,1,1,2,2,1,0,0,1,0,1,0,0,0,0,1,1,1,1,0,0,0, -1,1,2,1,1,1,1,0,0,0,1,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,2,1,2,1,0,0,1,0,0,0,0,0,0,0,0,1,1,0,1,0,0,0,0,0,0,0,0,0,0,0,1, -0,0,0,0,0,0,0,0,1,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,0,0,1,2,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1,0,0,0, -0,1,1,0,1,1,1,0,0,1,0,0,1,0,1,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0, -1,0,1,0,0,1,1,1,1,1,1,1,1,1,1,1,0,0,1,0,2,0,0,2,0,1,0,0,1,0,0,1, -1,1,0,0,1,1,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,1,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0, -1,1,1,1,1,1,1,2,0,0,0,0,0,0,2,1,0,1,1,0,0,1,1,1,0,1,0,0,0,0,0,0, -2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,0,0,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,1,1,0,1,1,1,1,1,0,1,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, -) +# 255: Undefined characters that did not exist in training text +# 254: Carriage/Return +# 253: symbol (punctuation) that does not belong to word +# 252: 0 - 9 +# 251: Control characters -Latin5BulgarianModel = { - 'char_to_order_map': Latin5_BulgarianCharToOrderMap, - 'precedence_matrix': BulgarianLangModel, - 'typical_positive_ratio': 0.969392, - 'keep_english_letter': False, - 'charset_name': "ISO-8859-5", - 'language': 'Bulgairan', +# Character Mapping Table(s): +ISO_8859_5_BULGARIAN_CHAR_TO_ORDER = { + 0: 255, # '\x00' + 1: 255, # '\x01' + 2: 255, # '\x02' + 3: 255, # '\x03' + 4: 255, # '\x04' + 5: 255, # '\x05' + 6: 255, # '\x06' + 7: 255, # '\x07' + 8: 255, # '\x08' + 9: 255, # '\t' + 10: 254, # '\n' + 11: 255, # '\x0b' + 12: 255, # '\x0c' + 13: 254, # '\r' + 14: 255, # '\x0e' + 15: 255, # '\x0f' + 16: 255, # '\x10' + 17: 255, # '\x11' + 18: 255, # '\x12' + 19: 255, # '\x13' + 20: 255, # '\x14' + 21: 255, # '\x15' + 22: 255, # '\x16' + 23: 255, # '\x17' + 24: 255, # '\x18' + 25: 255, # '\x19' + 26: 255, # '\x1a' + 27: 255, # '\x1b' + 28: 255, # '\x1c' + 29: 255, # '\x1d' + 30: 255, # '\x1e' + 31: 255, # '\x1f' + 32: 253, # ' ' + 33: 253, # '!' + 34: 253, # '"' + 35: 253, # '#' + 36: 253, # '$' + 37: 253, # '%' + 38: 253, # '&' + 39: 253, # "'" + 40: 253, # '(' + 41: 253, # ')' + 42: 253, # '*' + 43: 253, # '+' + 44: 253, # ',' + 45: 253, # '-' + 46: 253, # '.' + 47: 253, # '/' + 48: 252, # '0' + 49: 252, # '1' + 50: 252, # '2' + 51: 252, # '3' + 52: 252, # '4' + 53: 252, # '5' + 54: 252, # '6' + 55: 252, # '7' + 56: 252, # '8' + 57: 252, # '9' + 58: 253, # ':' + 59: 253, # ';' + 60: 253, # '<' + 61: 253, # '=' + 62: 253, # '>' + 63: 253, # '?' + 64: 253, # '@' + 65: 77, # 'A' + 66: 90, # 'B' + 67: 99, # 'C' + 68: 100, # 'D' + 69: 72, # 'E' + 70: 109, # 'F' + 71: 107, # 'G' + 72: 101, # 'H' + 73: 79, # 'I' + 74: 185, # 'J' + 75: 81, # 'K' + 76: 102, # 'L' + 77: 76, # 'M' + 78: 94, # 'N' + 79: 82, # 'O' + 80: 110, # 'P' + 81: 186, # 'Q' + 82: 108, # 'R' + 83: 91, # 'S' + 84: 74, # 'T' + 85: 119, # 'U' + 86: 84, # 'V' + 87: 96, # 'W' + 88: 111, # 'X' + 89: 187, # 'Y' + 90: 115, # 'Z' + 91: 253, # '[' + 92: 253, # '\\' + 93: 253, # ']' + 94: 253, # '^' + 95: 253, # '_' + 96: 253, # '`' + 97: 65, # 'a' + 98: 69, # 'b' + 99: 70, # 'c' + 100: 66, # 'd' + 101: 63, # 'e' + 102: 68, # 'f' + 103: 112, # 'g' + 104: 103, # 'h' + 105: 92, # 'i' + 106: 194, # 'j' + 107: 104, # 'k' + 108: 95, # 'l' + 109: 86, # 'm' + 110: 87, # 'n' + 111: 71, # 'o' + 112: 116, # 'p' + 113: 195, # 'q' + 114: 85, # 'r' + 115: 93, # 's' + 116: 97, # 't' + 117: 113, # 'u' + 118: 196, # 'v' + 119: 197, # 'w' + 120: 198, # 'x' + 121: 199, # 'y' + 122: 200, # 'z' + 123: 253, # '{' + 124: 253, # '|' + 125: 253, # '}' + 126: 253, # '~' + 127: 253, # '\x7f' + 128: 194, # '\x80' + 129: 195, # '\x81' + 130: 196, # '\x82' + 131: 197, # '\x83' + 132: 198, # '\x84' + 133: 199, # '\x85' + 134: 200, # '\x86' + 135: 201, # '\x87' + 136: 202, # '\x88' + 137: 203, # '\x89' + 138: 204, # '\x8a' + 139: 205, # '\x8b' + 140: 206, # '\x8c' + 141: 207, # '\x8d' + 142: 208, # '\x8e' + 143: 209, # '\x8f' + 144: 210, # '\x90' + 145: 211, # '\x91' + 146: 212, # '\x92' + 147: 213, # '\x93' + 148: 214, # '\x94' + 149: 215, # '\x95' + 150: 216, # '\x96' + 151: 217, # '\x97' + 152: 218, # '\x98' + 153: 219, # '\x99' + 154: 220, # '\x9a' + 155: 221, # '\x9b' + 156: 222, # '\x9c' + 157: 223, # '\x9d' + 158: 224, # '\x9e' + 159: 225, # '\x9f' + 160: 81, # '\xa0' + 161: 226, # 'Ё' + 162: 227, # 'Ђ' + 163: 228, # 'Ѓ' + 164: 229, # 'Є' + 165: 230, # 'Ѕ' + 166: 105, # 'І' + 167: 231, # 'Ї' + 168: 232, # 'Ј' + 169: 233, # 'Љ' + 170: 234, # 'Њ' + 171: 235, # 'Ћ' + 172: 236, # 'Ќ' + 173: 45, # '\xad' + 174: 237, # 'Ў' + 175: 238, # 'Џ' + 176: 31, # 'А' + 177: 32, # 'Б' + 178: 35, # 'В' + 179: 43, # 'Г' + 180: 37, # 'Д' + 181: 44, # 'Е' + 182: 55, # 'Ж' + 183: 47, # 'З' + 184: 40, # 'И' + 185: 59, # 'Й' + 186: 33, # 'К' + 187: 46, # 'Л' + 188: 38, # 'М' + 189: 36, # 'Н' + 190: 41, # 'О' + 191: 30, # 'П' + 192: 39, # 'Р' + 193: 28, # 'С' + 194: 34, # 'Т' + 195: 51, # 'У' + 196: 48, # 'Ф' + 197: 49, # 'Х' + 198: 53, # 'Ц' + 199: 50, # 'Ч' + 200: 54, # 'Ш' + 201: 57, # 'Щ' + 202: 61, # 'Ъ' + 203: 239, # 'Ы' + 204: 67, # 'Ь' + 205: 240, # 'Э' + 206: 60, # 'Ю' + 207: 56, # 'Я' + 208: 1, # 'а' + 209: 18, # 'б' + 210: 9, # 'в' + 211: 20, # 'г' + 212: 11, # 'д' + 213: 3, # 'е' + 214: 23, # 'ж' + 215: 15, # 'з' + 216: 2, # 'и' + 217: 26, # 'й' + 218: 12, # 'к' + 219: 10, # 'л' + 220: 14, # 'м' + 221: 6, # 'н' + 222: 4, # 'о' + 223: 13, # 'п' + 224: 7, # 'р' + 225: 8, # 'с' + 226: 5, # 'т' + 227: 19, # 'у' + 228: 29, # 'ф' + 229: 25, # 'х' + 230: 22, # 'ц' + 231: 21, # 'ч' + 232: 27, # 'ш' + 233: 24, # 'щ' + 234: 17, # 'ъ' + 235: 75, # 'ы' + 236: 52, # 'ь' + 237: 241, # 'э' + 238: 42, # 'ю' + 239: 16, # 'я' + 240: 62, # '№' + 241: 242, # 'ё' + 242: 243, # 'ђ' + 243: 244, # 'ѓ' + 244: 58, # 'є' + 245: 245, # 'ѕ' + 246: 98, # 'і' + 247: 246, # 'ї' + 248: 247, # 'ј' + 249: 248, # 'љ' + 250: 249, # 'њ' + 251: 250, # 'ћ' + 252: 251, # 'ќ' + 253: 91, # '§' + 254: 252, # 'ў' + 255: 253, # 'џ' } -Win1251BulgarianModel = { - 'char_to_order_map': win1251BulgarianCharToOrderMap, - 'precedence_matrix': BulgarianLangModel, - 'typical_positive_ratio': 0.969392, - 'keep_english_letter': False, - 'charset_name': "windows-1251", - 'language': 'Bulgarian', +ISO_8859_5_BULGARIAN_MODEL = SingleByteCharSetModel(charset_name='ISO-8859-5', + language='Bulgarian', + char_to_order_map=ISO_8859_5_BULGARIAN_CHAR_TO_ORDER, + language_model=BULGARIAN_LANG_MODEL, + typical_positive_ratio=0.969392, + keep_ascii_letters=False, + alphabet='АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЬЮЯабвгдежзийклмнопрстуфхцчшщъьюя') + +WINDOWS_1251_BULGARIAN_CHAR_TO_ORDER = { + 0: 255, # '\x00' + 1: 255, # '\x01' + 2: 255, # '\x02' + 3: 255, # '\x03' + 4: 255, # '\x04' + 5: 255, # '\x05' + 6: 255, # '\x06' + 7: 255, # '\x07' + 8: 255, # '\x08' + 9: 255, # '\t' + 10: 254, # '\n' + 11: 255, # '\x0b' + 12: 255, # '\x0c' + 13: 254, # '\r' + 14: 255, # '\x0e' + 15: 255, # '\x0f' + 16: 255, # '\x10' + 17: 255, # '\x11' + 18: 255, # '\x12' + 19: 255, # '\x13' + 20: 255, # '\x14' + 21: 255, # '\x15' + 22: 255, # '\x16' + 23: 255, # '\x17' + 24: 255, # '\x18' + 25: 255, # '\x19' + 26: 255, # '\x1a' + 27: 255, # '\x1b' + 28: 255, # '\x1c' + 29: 255, # '\x1d' + 30: 255, # '\x1e' + 31: 255, # '\x1f' + 32: 253, # ' ' + 33: 253, # '!' + 34: 253, # '"' + 35: 253, # '#' + 36: 253, # '$' + 37: 253, # '%' + 38: 253, # '&' + 39: 253, # "'" + 40: 253, # '(' + 41: 253, # ')' + 42: 253, # '*' + 43: 253, # '+' + 44: 253, # ',' + 45: 253, # '-' + 46: 253, # '.' + 47: 253, # '/' + 48: 252, # '0' + 49: 252, # '1' + 50: 252, # '2' + 51: 252, # '3' + 52: 252, # '4' + 53: 252, # '5' + 54: 252, # '6' + 55: 252, # '7' + 56: 252, # '8' + 57: 252, # '9' + 58: 253, # ':' + 59: 253, # ';' + 60: 253, # '<' + 61: 253, # '=' + 62: 253, # '>' + 63: 253, # '?' + 64: 253, # '@' + 65: 77, # 'A' + 66: 90, # 'B' + 67: 99, # 'C' + 68: 100, # 'D' + 69: 72, # 'E' + 70: 109, # 'F' + 71: 107, # 'G' + 72: 101, # 'H' + 73: 79, # 'I' + 74: 185, # 'J' + 75: 81, # 'K' + 76: 102, # 'L' + 77: 76, # 'M' + 78: 94, # 'N' + 79: 82, # 'O' + 80: 110, # 'P' + 81: 186, # 'Q' + 82: 108, # 'R' + 83: 91, # 'S' + 84: 74, # 'T' + 85: 119, # 'U' + 86: 84, # 'V' + 87: 96, # 'W' + 88: 111, # 'X' + 89: 187, # 'Y' + 90: 115, # 'Z' + 91: 253, # '[' + 92: 253, # '\\' + 93: 253, # ']' + 94: 253, # '^' + 95: 253, # '_' + 96: 253, # '`' + 97: 65, # 'a' + 98: 69, # 'b' + 99: 70, # 'c' + 100: 66, # 'd' + 101: 63, # 'e' + 102: 68, # 'f' + 103: 112, # 'g' + 104: 103, # 'h' + 105: 92, # 'i' + 106: 194, # 'j' + 107: 104, # 'k' + 108: 95, # 'l' + 109: 86, # 'm' + 110: 87, # 'n' + 111: 71, # 'o' + 112: 116, # 'p' + 113: 195, # 'q' + 114: 85, # 'r' + 115: 93, # 's' + 116: 97, # 't' + 117: 113, # 'u' + 118: 196, # 'v' + 119: 197, # 'w' + 120: 198, # 'x' + 121: 199, # 'y' + 122: 200, # 'z' + 123: 253, # '{' + 124: 253, # '|' + 125: 253, # '}' + 126: 253, # '~' + 127: 253, # '\x7f' + 128: 206, # 'Ђ' + 129: 207, # 'Ѓ' + 130: 208, # '‚' + 131: 209, # 'ѓ' + 132: 210, # '„' + 133: 211, # '…' + 134: 212, # '†' + 135: 213, # '‡' + 136: 120, # '€' + 137: 214, # '‰' + 138: 215, # 'Љ' + 139: 216, # '‹' + 140: 217, # 'Њ' + 141: 218, # 'Ќ' + 142: 219, # 'Ћ' + 143: 220, # 'Џ' + 144: 221, # 'ђ' + 145: 78, # '‘' + 146: 64, # '’' + 147: 83, # '“' + 148: 121, # '”' + 149: 98, # '•' + 150: 117, # '–' + 151: 105, # '—' + 152: 222, # None + 153: 223, # '™' + 154: 224, # 'љ' + 155: 225, # '›' + 156: 226, # 'њ' + 157: 227, # 'ќ' + 158: 228, # 'ћ' + 159: 229, # 'џ' + 160: 88, # '\xa0' + 161: 230, # 'Ў' + 162: 231, # 'ў' + 163: 232, # 'Ј' + 164: 233, # '¤' + 165: 122, # 'Ґ' + 166: 89, # '¦' + 167: 106, # '§' + 168: 234, # 'Ё' + 169: 235, # '©' + 170: 236, # 'Є' + 171: 237, # '«' + 172: 238, # '¬' + 173: 45, # '\xad' + 174: 239, # '®' + 175: 240, # 'Ї' + 176: 73, # '°' + 177: 80, # '±' + 178: 118, # 'І' + 179: 114, # 'і' + 180: 241, # 'ґ' + 181: 242, # 'µ' + 182: 243, # '¶' + 183: 244, # '·' + 184: 245, # 'ё' + 185: 62, # '№' + 186: 58, # 'є' + 187: 246, # '»' + 188: 247, # 'ј' + 189: 248, # 'Ѕ' + 190: 249, # 'ѕ' + 191: 250, # 'ї' + 192: 31, # 'А' + 193: 32, # 'Б' + 194: 35, # 'В' + 195: 43, # 'Г' + 196: 37, # 'Д' + 197: 44, # 'Е' + 198: 55, # 'Ж' + 199: 47, # 'З' + 200: 40, # 'И' + 201: 59, # 'Й' + 202: 33, # 'К' + 203: 46, # 'Л' + 204: 38, # 'М' + 205: 36, # 'Н' + 206: 41, # 'О' + 207: 30, # 'П' + 208: 39, # 'Р' + 209: 28, # 'С' + 210: 34, # 'Т' + 211: 51, # 'У' + 212: 48, # 'Ф' + 213: 49, # 'Х' + 214: 53, # 'Ц' + 215: 50, # 'Ч' + 216: 54, # 'Ш' + 217: 57, # 'Щ' + 218: 61, # 'Ъ' + 219: 251, # 'Ы' + 220: 67, # 'Ь' + 221: 252, # 'Э' + 222: 60, # 'Ю' + 223: 56, # 'Я' + 224: 1, # 'а' + 225: 18, # 'б' + 226: 9, # 'в' + 227: 20, # 'г' + 228: 11, # 'д' + 229: 3, # 'е' + 230: 23, # 'ж' + 231: 15, # 'з' + 232: 2, # 'и' + 233: 26, # 'й' + 234: 12, # 'к' + 235: 10, # 'л' + 236: 14, # 'м' + 237: 6, # 'н' + 238: 4, # 'о' + 239: 13, # 'п' + 240: 7, # 'р' + 241: 8, # 'с' + 242: 5, # 'т' + 243: 19, # 'у' + 244: 29, # 'ф' + 245: 25, # 'х' + 246: 22, # 'ц' + 247: 21, # 'ч' + 248: 27, # 'ш' + 249: 24, # 'щ' + 250: 17, # 'ъ' + 251: 75, # 'ы' + 252: 52, # 'ь' + 253: 253, # 'э' + 254: 42, # 'ю' + 255: 16, # 'я' } + +WINDOWS_1251_BULGARIAN_MODEL = SingleByteCharSetModel(charset_name='windows-1251', + language='Bulgarian', + char_to_order_map=WINDOWS_1251_BULGARIAN_CHAR_TO_ORDER, + language_model=BULGARIAN_LANG_MODEL, + typical_positive_ratio=0.969392, + keep_ascii_letters=False, + alphabet='АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЬЮЯабвгдежзийклмнопрстуфхцчшщъьюя') + diff --git a/src/pip/_vendor/chardet/langcyrillicmodel.py b/src/pip/_vendor/chardet/langcyrillicmodel.py deleted file mode 100644 index e5f9a1fd19c..00000000000 --- a/src/pip/_vendor/chardet/langcyrillicmodel.py +++ /dev/null @@ -1,333 +0,0 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is Mozilla Communicator client code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 1998 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### - -# KOI8-R language model -# Character Mapping Table: -KOI8R_char_to_order_map = ( -255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 -253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 -252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 -253,142,143,144,145,146,147,148,149,150,151,152, 74,153, 75,154, # 40 -155,156,157,158,159,160,161,162,163,164,165,253,253,253,253,253, # 50 -253, 71,172, 66,173, 65,174, 76,175, 64,176,177, 77, 72,178, 69, # 60 - 67,179, 78, 73,180,181, 79,182,183,184,185,253,253,253,253,253, # 70 -191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206, # 80 -207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222, # 90 -223,224,225, 68,226,227,228,229,230,231,232,233,234,235,236,237, # a0 -238,239,240,241,242,243,244,245,246,247,248,249,250,251,252,253, # b0 - 27, 3, 21, 28, 13, 2, 39, 19, 26, 4, 23, 11, 8, 12, 5, 1, # c0 - 15, 16, 9, 7, 6, 14, 24, 10, 17, 18, 20, 25, 30, 29, 22, 54, # d0 - 59, 37, 44, 58, 41, 48, 53, 46, 55, 42, 60, 36, 49, 38, 31, 34, # e0 - 35, 43, 45, 32, 40, 52, 56, 33, 61, 62, 51, 57, 47, 63, 50, 70, # f0 -) - -win1251_char_to_order_map = ( -255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 -253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 -252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 -253,142,143,144,145,146,147,148,149,150,151,152, 74,153, 75,154, # 40 -155,156,157,158,159,160,161,162,163,164,165,253,253,253,253,253, # 50 -253, 71,172, 66,173, 65,174, 76,175, 64,176,177, 77, 72,178, 69, # 60 - 67,179, 78, 73,180,181, 79,182,183,184,185,253,253,253,253,253, # 70 -191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206, -207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222, -223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238, -239,240,241,242,243,244,245,246, 68,247,248,249,250,251,252,253, - 37, 44, 33, 46, 41, 48, 56, 51, 42, 60, 36, 49, 38, 31, 34, 35, - 45, 32, 40, 52, 53, 55, 58, 50, 57, 63, 70, 62, 61, 47, 59, 43, - 3, 21, 10, 19, 13, 2, 24, 20, 4, 23, 11, 8, 12, 5, 1, 15, - 9, 7, 6, 14, 39, 26, 28, 22, 25, 29, 54, 18, 17, 30, 27, 16, -) - -latin5_char_to_order_map = ( -255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 -253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 -252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 -253,142,143,144,145,146,147,148,149,150,151,152, 74,153, 75,154, # 40 -155,156,157,158,159,160,161,162,163,164,165,253,253,253,253,253, # 50 -253, 71,172, 66,173, 65,174, 76,175, 64,176,177, 77, 72,178, 69, # 60 - 67,179, 78, 73,180,181, 79,182,183,184,185,253,253,253,253,253, # 70 -191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206, -207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222, -223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238, - 37, 44, 33, 46, 41, 48, 56, 51, 42, 60, 36, 49, 38, 31, 34, 35, - 45, 32, 40, 52, 53, 55, 58, 50, 57, 63, 70, 62, 61, 47, 59, 43, - 3, 21, 10, 19, 13, 2, 24, 20, 4, 23, 11, 8, 12, 5, 1, 15, - 9, 7, 6, 14, 39, 26, 28, 22, 25, 29, 54, 18, 17, 30, 27, 16, -239, 68,240,241,242,243,244,245,246,247,248,249,250,251,252,255, -) - -macCyrillic_char_to_order_map = ( -255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 -253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 -252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 -253,142,143,144,145,146,147,148,149,150,151,152, 74,153, 75,154, # 40 -155,156,157,158,159,160,161,162,163,164,165,253,253,253,253,253, # 50 -253, 71,172, 66,173, 65,174, 76,175, 64,176,177, 77, 72,178, 69, # 60 - 67,179, 78, 73,180,181, 79,182,183,184,185,253,253,253,253,253, # 70 - 37, 44, 33, 46, 41, 48, 56, 51, 42, 60, 36, 49, 38, 31, 34, 35, - 45, 32, 40, 52, 53, 55, 58, 50, 57, 63, 70, 62, 61, 47, 59, 43, -191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206, -207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222, -223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238, -239,240,241,242,243,244,245,246,247,248,249,250,251,252, 68, 16, - 3, 21, 10, 19, 13, 2, 24, 20, 4, 23, 11, 8, 12, 5, 1, 15, - 9, 7, 6, 14, 39, 26, 28, 22, 25, 29, 54, 18, 17, 30, 27,255, -) - -IBM855_char_to_order_map = ( -255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 -253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 -252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 -253,142,143,144,145,146,147,148,149,150,151,152, 74,153, 75,154, # 40 -155,156,157,158,159,160,161,162,163,164,165,253,253,253,253,253, # 50 -253, 71,172, 66,173, 65,174, 76,175, 64,176,177, 77, 72,178, 69, # 60 - 67,179, 78, 73,180,181, 79,182,183,184,185,253,253,253,253,253, # 70 -191,192,193,194, 68,195,196,197,198,199,200,201,202,203,204,205, -206,207,208,209,210,211,212,213,214,215,216,217, 27, 59, 54, 70, - 3, 37, 21, 44, 28, 58, 13, 41, 2, 48, 39, 53, 19, 46,218,219, -220,221,222,223,224, 26, 55, 4, 42,225,226,227,228, 23, 60,229, -230,231,232,233,234,235, 11, 36,236,237,238,239,240,241,242,243, - 8, 49, 12, 38, 5, 31, 1, 34, 15,244,245,246,247, 35, 16,248, - 43, 9, 45, 7, 32, 6, 40, 14, 52, 24, 56, 10, 33, 17, 61,249, -250, 18, 62, 20, 51, 25, 57, 30, 47, 29, 63, 22, 50,251,252,255, -) - -IBM866_char_to_order_map = ( -255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 -253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 -252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 -253,142,143,144,145,146,147,148,149,150,151,152, 74,153, 75,154, # 40 -155,156,157,158,159,160,161,162,163,164,165,253,253,253,253,253, # 50 -253, 71,172, 66,173, 65,174, 76,175, 64,176,177, 77, 72,178, 69, # 60 - 67,179, 78, 73,180,181, 79,182,183,184,185,253,253,253,253,253, # 70 - 37, 44, 33, 46, 41, 48, 56, 51, 42, 60, 36, 49, 38, 31, 34, 35, - 45, 32, 40, 52, 53, 55, 58, 50, 57, 63, 70, 62, 61, 47, 59, 43, - 3, 21, 10, 19, 13, 2, 24, 20, 4, 23, 11, 8, 12, 5, 1, 15, -191,192,193,194,195,196,197,198,199,200,201,202,203,204,205,206, -207,208,209,210,211,212,213,214,215,216,217,218,219,220,221,222, -223,224,225,226,227,228,229,230,231,232,233,234,235,236,237,238, - 9, 7, 6, 14, 39, 26, 28, 22, 25, 29, 54, 18, 17, 30, 27, 16, -239, 68,240,241,242,243,244,245,246,247,248,249,250,251,252,255, -) - -# Model Table: -# total sequences: 100% -# first 512 sequences: 97.6601% -# first 1024 sequences: 2.3389% -# rest sequences: 0.1237% -# negative sequences: 0.0009% -RussianLangModel = ( -0,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,1,3,3,3,3,1,3,3,3,2,3,2,3,3, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,0,3,2,2,2,2,2,0,0,2, -3,3,3,2,3,3,3,3,3,3,3,3,3,3,2,3,3,0,0,3,3,3,3,3,3,3,3,3,2,3,2,0, -0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,2,2,3,3,3,3,3,3,3,3,3,2,3,3,0,0,3,3,3,3,3,3,3,3,2,3,3,1,0, -0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,2,3,2,3,3,3,3,3,3,3,3,3,3,3,3,3,0,0,3,3,3,3,3,3,3,3,3,3,3,2,1, -0,0,0,0,0,0,0,2,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,3,0,0,3,3,3,3,3,3,3,3,3,3,3,2,1, -0,0,0,0,0,1,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,3,2,2,2,3,1,3,3,1,3,3,3,3,2,2,3,0,2,2,2,3,3,2,1,0, -0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,2,3,3,3,3,3,2,2,3,2,3,3,3,2,1,2,2,0,1,2,2,2,2,2,2,0, -0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,3,0,2,2,3,3,2,1,2,0, -0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,1,0,0,2,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,2,3,3,1,2,3,2,2,3,2,3,3,3,3,2,2,3,0,3,2,2,3,1,1,1,0, -0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,3,2,2,3,3,3,3,3,2,3,3,3,3,2,2,2,0,3,3,3,2,2,2,2,0, -0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,3,3,3,2,3,2,3,3,3,3,3,3,2,3,2,2,0,1,3,2,1,2,2,1,0, -0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,3,3,3,3,2,1,1,3,0,1,1,1,1,2,1,1,0,2,2,2,1,2,0,1,0, -0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,2,3,3,2,2,2,2,1,3,2,3,2,3,2,1,2,2,0,1,1,2,1,2,1,2,0, -0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,3,3,3,3,3,2,2,3,2,3,3,3,2,2,2,2,0,2,2,2,2,3,1,1,0, -0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0, -3,2,3,2,2,3,3,3,3,3,3,3,3,3,1,3,2,0,0,3,3,3,3,2,3,3,3,3,2,3,2,0, -0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,3,3,3,3,3,2,2,3,3,0,2,1,0,3,2,3,2,3,0,0,1,2,0,0,1,0,1,2,1,1,0, -0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,0,3,0,2,3,3,3,3,2,3,3,3,3,1,2,2,0,0,2,3,2,2,2,3,2,3,2,2,3,0,0, -0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,2,3,0,2,3,2,3,0,1,2,3,3,2,0,2,3,0,0,2,3,2,2,0,1,3,1,3,2,2,1,0, -0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,1,3,0,2,3,3,3,3,3,3,3,3,2,1,3,2,0,0,2,2,3,3,3,2,3,3,0,2,2,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,2,2,3,3,2,2,2,3,3,0,0,1,1,1,1,1,2,0,0,1,1,1,1,0,1,0, -0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,2,2,3,3,3,3,3,3,3,0,3,2,3,3,2,3,2,0,2,1,0,1,1,0,1,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,2,3,3,3,2,2,2,2,3,1,3,2,3,1,1,2,1,0,2,2,2,2,1,3,1,0, -0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0, -2,2,3,3,3,3,3,1,2,2,1,3,1,0,3,0,0,3,0,0,0,1,1,0,1,2,1,0,0,0,0,0, -0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,2,2,1,1,3,3,3,2,2,1,2,2,3,1,1,2,0,0,2,2,1,3,0,0,2,1,1,2,1,1,0, -0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,2,3,3,3,3,1,2,2,2,1,2,1,3,3,1,1,2,1,2,1,2,2,0,2,0,0,1,1,0,1,0, -0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,3,3,3,3,3,2,1,3,2,2,3,2,0,3,2,0,3,0,1,0,1,1,0,0,1,1,1,1,0,1,0, -0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,2,3,3,3,2,2,2,3,3,1,2,1,2,1,0,1,0,1,1,0,1,0,0,2,1,1,1,0,1,0, -0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0, -3,1,1,2,1,2,3,3,2,2,1,2,2,3,0,2,1,0,0,2,2,3,2,1,2,2,2,2,2,3,1,0, -0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,1,1,0,1,1,2,2,1,1,3,0,0,1,3,1,1,1,0,0,0,1,0,1,1,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,1,3,3,3,2,0,0,0,2,1,0,1,0,2,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,0,1,0,0,2,3,2,2,2,1,2,2,2,1,2,1,0,0,1,1,1,0,2,0,1,1,1,0,0,1,1, -1,0,0,0,0,0,1,2,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0, -2,3,3,3,3,0,0,0,0,1,0,0,0,0,3,0,1,2,1,0,0,0,0,0,0,0,1,1,0,0,1,1, -1,0,1,0,1,2,0,0,1,1,2,1,0,1,1,1,1,0,1,1,1,1,0,1,0,0,1,0,0,1,1,0, -2,2,3,2,2,2,3,1,2,2,2,2,2,2,2,2,1,1,1,1,1,1,1,0,1,0,1,1,1,0,2,1, -1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,1,0,1,0,1,1,0,1,1,1,0,1,1,0, -3,3,3,2,2,2,2,3,2,2,1,1,2,2,2,2,1,1,3,1,2,1,2,0,0,1,1,0,1,0,2,1, -1,1,1,1,1,2,1,0,1,1,1,1,0,1,0,0,1,1,0,0,1,0,1,0,0,1,0,0,0,1,1,0, -2,0,0,1,0,3,2,2,2,2,1,2,1,2,1,2,0,0,0,2,1,2,2,1,1,2,2,0,1,1,0,2, -1,1,1,1,1,0,1,1,1,2,1,1,1,2,1,0,1,2,1,1,1,1,0,1,1,1,0,0,1,0,0,1, -1,3,2,2,2,1,1,1,2,3,0,0,0,0,2,0,2,2,1,0,0,0,0,0,0,1,0,0,0,0,1,1, -1,0,1,1,0,1,0,1,1,0,1,1,0,2,0,0,1,1,0,0,1,0,0,0,0,0,0,0,0,1,1,0, -2,3,2,3,2,1,2,2,2,2,1,0,0,0,2,0,0,1,1,0,0,0,0,0,0,0,1,1,0,0,2,1, -1,1,2,1,0,2,0,0,1,0,1,0,0,1,0,0,1,1,0,1,1,0,0,0,0,0,1,0,0,0,0,0, -3,0,0,1,0,2,2,2,3,2,2,2,2,2,2,2,0,0,0,2,1,2,1,1,1,2,2,0,0,0,1,2, -1,1,1,1,1,0,1,2,1,1,1,1,1,1,1,0,1,1,1,1,1,1,0,1,1,1,1,1,1,0,0,1, -2,3,2,3,3,2,0,1,1,1,0,0,1,0,2,0,1,1,3,1,0,0,0,0,0,0,0,1,0,0,2,1, -1,1,1,1,1,1,1,0,1,0,1,1,1,1,0,1,1,1,0,0,1,1,0,1,0,0,0,0,0,0,1,0, -2,3,3,3,3,1,2,2,2,2,0,1,1,0,2,1,1,1,2,1,0,1,1,0,0,1,0,1,0,0,2,0, -0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,3,3,3,2,0,0,1,1,2,2,1,0,0,2,0,1,1,3,0,0,1,0,0,0,0,0,1,0,1,2,1, -1,1,2,0,1,1,1,0,1,0,1,1,0,1,0,1,1,1,1,0,1,0,0,0,0,0,0,1,0,1,1,0, -1,3,2,3,2,1,0,0,2,2,2,0,1,0,2,0,1,1,1,0,1,0,0,0,3,0,1,1,0,0,2,1, -1,1,1,0,1,1,0,0,0,0,1,1,0,1,0,0,2,1,1,0,1,0,0,0,1,0,1,0,0,1,1,0, -3,1,2,1,1,2,2,2,2,2,2,1,2,2,1,1,0,0,0,2,2,2,0,0,0,1,2,1,0,1,0,1, -2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,2,1,1,1,0,1,0,1,1,0,1,1,1,0,0,1, -3,0,0,0,0,2,0,1,1,1,1,1,1,1,0,1,0,0,0,1,1,1,0,1,0,1,1,0,0,1,0,1, -1,1,0,0,1,0,0,0,1,0,1,1,0,0,1,0,1,0,1,0,0,0,0,1,0,0,0,1,0,0,0,1, -1,3,3,2,2,0,0,0,2,2,0,0,0,1,2,0,1,1,2,0,0,0,0,0,0,0,0,1,0,0,2,1, -0,1,1,0,0,1,1,0,0,0,1,1,0,1,1,0,1,1,0,0,1,0,0,0,0,0,0,0,0,0,1,0, -2,3,2,3,2,0,0,0,0,1,1,0,0,0,2,0,2,0,2,0,0,0,0,0,1,0,0,1,0,0,1,1, -1,1,2,0,1,2,1,0,1,1,2,1,1,1,1,1,2,1,1,0,1,0,0,1,1,1,1,1,0,1,1,0, -1,3,2,2,2,1,0,0,2,2,1,0,1,2,2,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,1,1, -0,0,1,1,0,1,1,0,0,1,1,0,1,1,0,0,1,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0, -1,0,0,1,0,2,3,1,2,2,2,2,2,2,1,1,0,0,0,1,0,1,0,2,1,1,1,0,0,0,0,1, -1,1,0,1,1,0,1,1,1,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1,0,0,0, -2,0,2,0,0,1,0,3,2,1,2,1,2,2,0,1,0,0,0,2,1,0,0,2,1,1,1,1,0,2,0,2, -2,1,1,1,1,1,1,1,1,1,1,1,1,2,1,0,1,1,1,1,0,0,0,1,1,1,1,0,1,0,0,1, -1,2,2,2,2,1,0,0,1,0,0,0,0,0,2,0,1,1,1,1,0,0,0,0,1,0,1,2,0,0,2,0, -1,0,1,1,1,2,1,0,1,0,1,1,0,0,1,0,1,1,1,0,1,0,0,0,1,0,0,1,0,1,1,0, -2,1,2,2,2,0,3,0,1,1,0,0,0,0,2,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1, -0,0,0,1,1,1,0,0,1,0,1,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0, -1,2,2,3,2,2,0,0,1,1,2,0,1,2,1,0,1,0,1,0,0,1,0,0,0,0,0,0,0,0,0,1, -0,1,1,0,0,1,1,0,0,1,1,0,0,1,1,0,1,1,0,0,1,0,0,0,0,0,0,0,0,1,1,0, -2,2,1,1,2,1,2,2,2,2,2,1,2,2,0,1,0,0,0,1,2,2,2,1,2,1,1,1,1,1,2,1, -1,1,1,1,1,1,1,1,1,1,0,0,1,1,1,0,1,1,1,0,0,0,0,1,1,1,0,1,1,0,0,1, -1,2,2,2,2,0,1,0,2,2,0,0,0,0,2,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,2,0, -0,0,1,0,0,1,0,0,0,0,1,0,1,1,0,0,1,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0, -0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,2,2,2,2,0,0,0,2,2,2,0,1,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,1, -0,1,1,0,0,1,1,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,2,2,2,2,0,0,0,0,1,0,0,1,1,2,0,0,0,0,1,0,1,0,0,1,0,0,2,0,0,0,1, -0,0,1,0,0,1,0,0,0,1,1,0,0,0,0,0,1,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0, -1,2,2,2,1,1,2,0,2,1,1,1,1,0,2,2,0,0,0,0,0,0,0,0,0,1,1,0,0,0,1,1, -0,0,1,0,1,1,0,0,0,0,1,0,0,0,0,0,1,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0, -1,0,2,1,2,0,0,0,0,0,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,0,0, -0,0,1,0,1,1,0,0,0,0,1,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0, -1,0,0,0,0,2,0,1,2,1,0,1,1,1,0,1,0,0,0,1,0,1,0,0,1,0,1,0,0,0,0,1, -0,0,0,0,0,1,0,0,1,1,0,0,1,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1, -2,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, -1,0,0,0,1,0,0,0,1,1,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,1,0,0,0,0,0, -2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, -1,1,1,0,1,0,1,0,0,1,1,1,1,0,0,0,1,0,0,0,0,1,0,0,0,1,0,1,0,0,0,0, -1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, -1,1,0,1,1,0,1,0,1,0,0,0,0,1,1,0,1,1,0,0,0,0,0,1,0,1,1,0,1,0,0,0, -0,1,1,1,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0, -) - -Koi8rModel = { - 'char_to_order_map': KOI8R_char_to_order_map, - 'precedence_matrix': RussianLangModel, - 'typical_positive_ratio': 0.976601, - 'keep_english_letter': False, - 'charset_name': "KOI8-R", - 'language': 'Russian', -} - -Win1251CyrillicModel = { - 'char_to_order_map': win1251_char_to_order_map, - 'precedence_matrix': RussianLangModel, - 'typical_positive_ratio': 0.976601, - 'keep_english_letter': False, - 'charset_name': "windows-1251", - 'language': 'Russian', -} - -Latin5CyrillicModel = { - 'char_to_order_map': latin5_char_to_order_map, - 'precedence_matrix': RussianLangModel, - 'typical_positive_ratio': 0.976601, - 'keep_english_letter': False, - 'charset_name': "ISO-8859-5", - 'language': 'Russian', -} - -MacCyrillicModel = { - 'char_to_order_map': macCyrillic_char_to_order_map, - 'precedence_matrix': RussianLangModel, - 'typical_positive_ratio': 0.976601, - 'keep_english_letter': False, - 'charset_name': "MacCyrillic", - 'language': 'Russian', -} - -Ibm866Model = { - 'char_to_order_map': IBM866_char_to_order_map, - 'precedence_matrix': RussianLangModel, - 'typical_positive_ratio': 0.976601, - 'keep_english_letter': False, - 'charset_name': "IBM866", - 'language': 'Russian', -} - -Ibm855Model = { - 'char_to_order_map': IBM855_char_to_order_map, - 'precedence_matrix': RussianLangModel, - 'typical_positive_ratio': 0.976601, - 'keep_english_letter': False, - 'charset_name': "IBM855", - 'language': 'Russian', -} diff --git a/src/pip/_vendor/chardet/langgreekmodel.py b/src/pip/_vendor/chardet/langgreekmodel.py index 533222166cc..d99528ede73 100644 --- a/src/pip/_vendor/chardet/langgreekmodel.py +++ b/src/pip/_vendor/chardet/langgreekmodel.py @@ -1,225 +1,4398 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is Mozilla Communicator client code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 1998 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### +#!/usr/bin/env python +# -*- coding: utf-8 -*- -# 255: Control characters that usually does not exist in any text +from pip._vendor.chardet.sbcharsetprober import SingleByteCharSetModel + + +# 3: Positive +# 2: Likely +# 1: Unlikely +# 0: Negative + +GREEK_LANG_MODEL = { + 60: { # 'e' + 60: 2, # 'e' + 55: 1, # 'o' + 58: 2, # 't' + 36: 1, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 1, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 0, # 'ο' + 9: 0, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 0, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 55: { # 'o' + 60: 0, # 'e' + 55: 2, # 'o' + 58: 2, # 't' + 36: 1, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 1, # 'ν' + 30: 0, # 'ξ' + 4: 0, # 'ο' + 9: 0, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 1, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 58: { # 't' + 60: 2, # 'e' + 55: 1, # 'o' + 58: 1, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 2, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 1, # 'ο' + 9: 0, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 0, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 36: { # '·' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 0, # 'ο' + 9: 0, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 0, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 61: { # 'Ά' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 1, # 'γ' + 21: 2, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 2, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 0, # 'ο' + 9: 1, # 'π' + 8: 2, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 0, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 46: { # 'Έ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 2, # 'β' + 20: 2, # 'γ' + 21: 0, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 2, # 'κ' + 16: 2, # 'λ' + 10: 0, # 'μ' + 6: 3, # 'ν' + 30: 2, # 'ξ' + 4: 0, # 'ο' + 9: 2, # 'π' + 8: 2, # 'ρ' + 14: 0, # 'ς' + 7: 1, # 'σ' + 2: 2, # 'τ' + 12: 0, # 'υ' + 28: 2, # 'φ' + 23: 3, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 54: { # 'Ό' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 2, # 'λ' + 10: 2, # 'μ' + 6: 2, # 'ν' + 30: 0, # 'ξ' + 4: 0, # 'ο' + 9: 2, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 2, # 'σ' + 2: 3, # 'τ' + 12: 0, # 'υ' + 28: 0, # 'φ' + 23: 2, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 31: { # 'Α' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 2, # 'Β' + 43: 2, # 'Γ' + 41: 1, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 2, # 'Θ' + 47: 2, # 'Ι' + 44: 2, # 'Κ' + 53: 2, # 'Λ' + 38: 2, # 'Μ' + 49: 2, # 'Ν' + 59: 1, # 'Ξ' + 39: 0, # 'Ο' + 35: 2, # 'Π' + 48: 2, # 'Ρ' + 37: 2, # 'Σ' + 33: 2, # 'Τ' + 45: 2, # 'Υ' + 56: 2, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 2, # 'γ' + 21: 0, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 1, # 'θ' + 5: 0, # 'ι' + 11: 2, # 'κ' + 16: 3, # 'λ' + 10: 2, # 'μ' + 6: 3, # 'ν' + 30: 2, # 'ξ' + 4: 0, # 'ο' + 9: 3, # 'π' + 8: 3, # 'ρ' + 14: 2, # 'ς' + 7: 2, # 'σ' + 2: 0, # 'τ' + 12: 3, # 'υ' + 28: 2, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 2, # 'ύ' + 27: 0, # 'ώ' + }, + 51: { # 'Β' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 2, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 1, # 'Ε' + 40: 1, # 'Η' + 52: 0, # 'Θ' + 47: 1, # 'Ι' + 44: 0, # 'Κ' + 53: 1, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 2, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 2, # 'ά' + 18: 2, # 'έ' + 22: 2, # 'ή' + 15: 0, # 'ί' + 1: 2, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 2, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 2, # 'ι' + 11: 0, # 'κ' + 16: 2, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 2, # 'ο' + 9: 0, # 'π' + 8: 2, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 0, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 43: { # 'Γ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 1, # 'Α' + 51: 0, # 'Β' + 43: 2, # 'Γ' + 41: 0, # 'Δ' + 34: 2, # 'Ε' + 40: 1, # 'Η' + 52: 0, # 'Θ' + 47: 2, # 'Ι' + 44: 1, # 'Κ' + 53: 1, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 1, # 'Ο' + 35: 0, # 'Π' + 48: 2, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 2, # 'Υ' + 56: 0, # 'Φ' + 50: 1, # 'Χ' + 57: 2, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 2, # 'ί' + 1: 2, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 2, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 3, # 'ι' + 11: 0, # 'κ' + 16: 2, # 'λ' + 10: 0, # 'μ' + 6: 2, # 'ν' + 30: 0, # 'ξ' + 4: 0, # 'ο' + 9: 0, # 'π' + 8: 2, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 0, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 41: { # 'Δ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 2, # 'Ε' + 40: 2, # 'Η' + 52: 0, # 'Θ' + 47: 2, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 2, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 2, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 2, # 'ή' + 15: 2, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 2, # 'η' + 25: 0, # 'θ' + 5: 3, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 2, # 'ο' + 9: 0, # 'π' + 8: 2, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 2, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 2, # 'ω' + 19: 1, # 'ό' + 26: 2, # 'ύ' + 27: 2, # 'ώ' + }, + 34: { # 'Ε' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 2, # 'Α' + 51: 0, # 'Β' + 43: 2, # 'Γ' + 41: 2, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 2, # 'Ι' + 44: 2, # 'Κ' + 53: 2, # 'Λ' + 38: 2, # 'Μ' + 49: 2, # 'Ν' + 59: 1, # 'Ξ' + 39: 0, # 'Ο' + 35: 2, # 'Π' + 48: 2, # 'Ρ' + 37: 2, # 'Σ' + 33: 2, # 'Τ' + 45: 2, # 'Υ' + 56: 0, # 'Φ' + 50: 2, # 'Χ' + 57: 2, # 'Ω' + 17: 3, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 3, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 3, # 'γ' + 21: 2, # 'δ' + 3: 1, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 1, # 'θ' + 5: 2, # 'ι' + 11: 3, # 'κ' + 16: 3, # 'λ' + 10: 2, # 'μ' + 6: 3, # 'ν' + 30: 2, # 'ξ' + 4: 0, # 'ο' + 9: 3, # 'π' + 8: 2, # 'ρ' + 14: 0, # 'ς' + 7: 2, # 'σ' + 2: 2, # 'τ' + 12: 2, # 'υ' + 28: 2, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 1, # 'ύ' + 27: 0, # 'ώ' + }, + 40: { # 'Η' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 1, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 2, # 'Θ' + 47: 0, # 'Ι' + 44: 2, # 'Κ' + 53: 0, # 'Λ' + 38: 2, # 'Μ' + 49: 2, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 2, # 'Π' + 48: 2, # 'Ρ' + 37: 2, # 'Σ' + 33: 2, # 'Τ' + 45: 1, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 2, # 'λ' + 10: 0, # 'μ' + 6: 1, # 'ν' + 30: 0, # 'ξ' + 4: 0, # 'ο' + 9: 0, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 0, # 'υ' + 28: 0, # 'φ' + 23: 1, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 52: { # 'Θ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 2, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 2, # 'Ε' + 40: 2, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 2, # 'Ο' + 35: 0, # 'Π' + 48: 1, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 1, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 2, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 3, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 2, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 0, # 'ο' + 9: 0, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 2, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 2, # 'ύ' + 27: 0, # 'ώ' + }, + 47: { # 'Ι' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 2, # 'Α' + 51: 1, # 'Β' + 43: 1, # 'Γ' + 41: 2, # 'Δ' + 34: 2, # 'Ε' + 40: 2, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 2, # 'Κ' + 53: 2, # 'Λ' + 38: 2, # 'Μ' + 49: 2, # 'Ν' + 59: 0, # 'Ξ' + 39: 2, # 'Ο' + 35: 0, # 'Π' + 48: 2, # 'Ρ' + 37: 2, # 'Σ' + 33: 2, # 'Τ' + 45: 0, # 'Υ' + 56: 2, # 'Φ' + 50: 0, # 'Χ' + 57: 2, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 2, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 2, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 1, # 'ν' + 30: 0, # 'ξ' + 4: 2, # 'ο' + 9: 0, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 2, # 'σ' + 2: 1, # 'τ' + 12: 0, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 1, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 44: { # 'Κ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 2, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 1, # 'Δ' + 34: 2, # 'Ε' + 40: 2, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 1, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 2, # 'Ο' + 35: 0, # 'Π' + 48: 2, # 'Ρ' + 37: 0, # 'Σ' + 33: 1, # 'Τ' + 45: 2, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 1, # 'Ω' + 17: 3, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 3, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 2, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 2, # 'ι' + 11: 0, # 'κ' + 16: 2, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 2, # 'ο' + 9: 0, # 'π' + 8: 2, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 2, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 2, # 'ό' + 26: 2, # 'ύ' + 27: 2, # 'ώ' + }, + 53: { # 'Λ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 2, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 2, # 'Ε' + 40: 2, # 'Η' + 52: 0, # 'Θ' + 47: 2, # 'Ι' + 44: 0, # 'Κ' + 53: 2, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 2, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 2, # 'Σ' + 33: 0, # 'Τ' + 45: 2, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 2, # 'Ω' + 17: 2, # 'ά' + 18: 2, # 'έ' + 22: 0, # 'ή' + 15: 2, # 'ί' + 1: 2, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 2, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 1, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 2, # 'ο' + 9: 0, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 2, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 2, # 'ό' + 26: 2, # 'ύ' + 27: 0, # 'ώ' + }, + 38: { # 'Μ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 2, # 'Α' + 51: 2, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 2, # 'Ε' + 40: 2, # 'Η' + 52: 0, # 'Θ' + 47: 2, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 2, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 2, # 'Ο' + 35: 2, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 2, # 'ά' + 18: 2, # 'έ' + 22: 2, # 'ή' + 15: 2, # 'ί' + 1: 2, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 2, # 'η' + 25: 0, # 'θ' + 5: 3, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 2, # 'ο' + 9: 3, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 2, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 2, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 49: { # 'Ν' + 60: 2, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 2, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 2, # 'Ε' + 40: 2, # 'Η' + 52: 0, # 'Θ' + 47: 2, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 2, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 2, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 2, # 'Ω' + 17: 0, # 'ά' + 18: 2, # 'έ' + 22: 0, # 'ή' + 15: 2, # 'ί' + 1: 2, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 1, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 2, # 'ο' + 9: 0, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 0, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 1, # 'ω' + 19: 2, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 59: { # 'Ξ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 1, # 'Ε' + 40: 1, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 1, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 2, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 2, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 2, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 0, # 'ο' + 9: 0, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 0, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 39: { # 'Ο' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 1, # 'Β' + 43: 2, # 'Γ' + 41: 2, # 'Δ' + 34: 2, # 'Ε' + 40: 1, # 'Η' + 52: 2, # 'Θ' + 47: 2, # 'Ι' + 44: 2, # 'Κ' + 53: 2, # 'Λ' + 38: 2, # 'Μ' + 49: 2, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 2, # 'Π' + 48: 2, # 'Ρ' + 37: 2, # 'Σ' + 33: 2, # 'Τ' + 45: 2, # 'Υ' + 56: 2, # 'Φ' + 50: 2, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 2, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 3, # 'ι' + 11: 2, # 'κ' + 16: 2, # 'λ' + 10: 2, # 'μ' + 6: 2, # 'ν' + 30: 0, # 'ξ' + 4: 0, # 'ο' + 9: 2, # 'π' + 8: 2, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 2, # 'τ' + 12: 2, # 'υ' + 28: 1, # 'φ' + 23: 1, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 2, # 'ύ' + 27: 0, # 'ώ' + }, + 35: { # 'Π' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 2, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 2, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 2, # 'Ι' + 44: 0, # 'Κ' + 53: 2, # 'Λ' + 38: 1, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 2, # 'Ο' + 35: 0, # 'Π' + 48: 2, # 'Ρ' + 37: 0, # 'Σ' + 33: 1, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 1, # 'Χ' + 57: 2, # 'Ω' + 17: 2, # 'ά' + 18: 1, # 'έ' + 22: 1, # 'ή' + 15: 2, # 'ί' + 1: 3, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 2, # 'η' + 25: 0, # 'θ' + 5: 2, # 'ι' + 11: 0, # 'κ' + 16: 2, # 'λ' + 10: 0, # 'μ' + 6: 2, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 0, # 'π' + 8: 3, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 2, # 'υ' + 28: 0, # 'φ' + 23: 2, # 'χ' + 42: 0, # 'ψ' + 24: 2, # 'ω' + 19: 2, # 'ό' + 26: 0, # 'ύ' + 27: 3, # 'ώ' + }, + 48: { # 'Ρ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 2, # 'Α' + 51: 0, # 'Β' + 43: 1, # 'Γ' + 41: 1, # 'Δ' + 34: 2, # 'Ε' + 40: 2, # 'Η' + 52: 0, # 'Θ' + 47: 2, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 2, # 'Ν' + 59: 0, # 'Ξ' + 39: 2, # 'Ο' + 35: 0, # 'Π' + 48: 2, # 'Ρ' + 37: 0, # 'Σ' + 33: 1, # 'Τ' + 45: 1, # 'Υ' + 56: 0, # 'Φ' + 50: 1, # 'Χ' + 57: 1, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 2, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 1, # 'ο' + 9: 0, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 3, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 2, # 'ω' + 19: 0, # 'ό' + 26: 2, # 'ύ' + 27: 0, # 'ώ' + }, + 37: { # 'Σ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 2, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 1, # 'Δ' + 34: 2, # 'Ε' + 40: 2, # 'Η' + 52: 0, # 'Θ' + 47: 2, # 'Ι' + 44: 2, # 'Κ' + 53: 0, # 'Λ' + 38: 2, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 2, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 2, # 'Σ' + 33: 2, # 'Τ' + 45: 2, # 'Υ' + 56: 0, # 'Φ' + 50: 2, # 'Χ' + 57: 2, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 2, # 'ή' + 15: 2, # 'ί' + 1: 2, # 'α' + 29: 2, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 3, # 'η' + 25: 0, # 'θ' + 5: 2, # 'ι' + 11: 2, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 2, # 'ο' + 9: 2, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 3, # 'τ' + 12: 3, # 'υ' + 28: 0, # 'φ' + 23: 2, # 'χ' + 42: 0, # 'ψ' + 24: 2, # 'ω' + 19: 0, # 'ό' + 26: 2, # 'ύ' + 27: 2, # 'ώ' + }, + 33: { # 'Τ' + 60: 0, # 'e' + 55: 1, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 2, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 2, # 'Ε' + 40: 2, # 'Η' + 52: 0, # 'Θ' + 47: 2, # 'Ι' + 44: 2, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 2, # 'Ο' + 35: 0, # 'Π' + 48: 2, # 'Ρ' + 37: 0, # 'Σ' + 33: 1, # 'Τ' + 45: 1, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 2, # 'Ω' + 17: 2, # 'ά' + 18: 2, # 'έ' + 22: 0, # 'ή' + 15: 2, # 'ί' + 1: 3, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 2, # 'ε' + 32: 0, # 'ζ' + 13: 2, # 'η' + 25: 0, # 'θ' + 5: 2, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 2, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 0, # 'π' + 8: 2, # 'ρ' + 14: 0, # 'ς' + 7: 2, # 'σ' + 2: 0, # 'τ' + 12: 2, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 2, # 'ό' + 26: 2, # 'ύ' + 27: 3, # 'ώ' + }, + 45: { # 'Υ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 2, # 'Γ' + 41: 0, # 'Δ' + 34: 1, # 'Ε' + 40: 2, # 'Η' + 52: 2, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 1, # 'Λ' + 38: 2, # 'Μ' + 49: 2, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 2, # 'Π' + 48: 1, # 'Ρ' + 37: 2, # 'Σ' + 33: 2, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 1, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 2, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 0, # 'ο' + 9: 3, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 0, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 56: { # 'Φ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 1, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 1, # 'Η' + 52: 0, # 'Θ' + 47: 2, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 2, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 2, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 2, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 2, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 2, # 'ο' + 9: 0, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 2, # 'τ' + 12: 2, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 1, # 'ύ' + 27: 1, # 'ώ' + }, + 50: { # 'Χ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 1, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 2, # 'Ε' + 40: 2, # 'Η' + 52: 0, # 'Θ' + 47: 2, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 1, # 'Ν' + 59: 0, # 'Ξ' + 39: 1, # 'Ο' + 35: 0, # 'Π' + 48: 2, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 1, # 'Χ' + 57: 1, # 'Ω' + 17: 2, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 2, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 2, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 2, # 'ο' + 9: 0, # 'π' + 8: 3, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 2, # 'τ' + 12: 0, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 2, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 57: { # 'Ω' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 1, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 1, # 'Λ' + 38: 0, # 'Μ' + 49: 2, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 2, # 'Ρ' + 37: 2, # 'Σ' + 33: 2, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 0, # 'ο' + 9: 0, # 'π' + 8: 2, # 'ρ' + 14: 2, # 'ς' + 7: 2, # 'σ' + 2: 0, # 'τ' + 12: 0, # 'υ' + 28: 0, # 'φ' + 23: 1, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 17: { # 'ά' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 2, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 3, # 'β' + 20: 3, # 'γ' + 21: 3, # 'δ' + 3: 3, # 'ε' + 32: 3, # 'ζ' + 13: 0, # 'η' + 25: 3, # 'θ' + 5: 2, # 'ι' + 11: 3, # 'κ' + 16: 3, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 3, # 'ξ' + 4: 0, # 'ο' + 9: 3, # 'π' + 8: 3, # 'ρ' + 14: 3, # 'ς' + 7: 3, # 'σ' + 2: 3, # 'τ' + 12: 0, # 'υ' + 28: 3, # 'φ' + 23: 3, # 'χ' + 42: 3, # 'ψ' + 24: 2, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 18: { # 'έ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 3, # 'α' + 29: 2, # 'β' + 20: 3, # 'γ' + 21: 2, # 'δ' + 3: 3, # 'ε' + 32: 2, # 'ζ' + 13: 0, # 'η' + 25: 3, # 'θ' + 5: 0, # 'ι' + 11: 3, # 'κ' + 16: 3, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 3, # 'ξ' + 4: 3, # 'ο' + 9: 3, # 'π' + 8: 3, # 'ρ' + 14: 3, # 'ς' + 7: 3, # 'σ' + 2: 3, # 'τ' + 12: 0, # 'υ' + 28: 3, # 'φ' + 23: 3, # 'χ' + 42: 3, # 'ψ' + 24: 2, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 22: { # 'ή' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 1, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 3, # 'γ' + 21: 3, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 3, # 'θ' + 5: 0, # 'ι' + 11: 3, # 'κ' + 16: 2, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 2, # 'ξ' + 4: 0, # 'ο' + 9: 3, # 'π' + 8: 3, # 'ρ' + 14: 3, # 'ς' + 7: 3, # 'σ' + 2: 3, # 'τ' + 12: 0, # 'υ' + 28: 2, # 'φ' + 23: 3, # 'χ' + 42: 2, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 15: { # 'ί' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 3, # 'α' + 29: 2, # 'β' + 20: 3, # 'γ' + 21: 3, # 'δ' + 3: 3, # 'ε' + 32: 3, # 'ζ' + 13: 3, # 'η' + 25: 3, # 'θ' + 5: 0, # 'ι' + 11: 3, # 'κ' + 16: 3, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 3, # 'ξ' + 4: 3, # 'ο' + 9: 3, # 'π' + 8: 3, # 'ρ' + 14: 3, # 'ς' + 7: 3, # 'σ' + 2: 3, # 'τ' + 12: 0, # 'υ' + 28: 1, # 'φ' + 23: 3, # 'χ' + 42: 2, # 'ψ' + 24: 3, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 1: { # 'α' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 2, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 2, # 'έ' + 22: 0, # 'ή' + 15: 3, # 'ί' + 1: 0, # 'α' + 29: 3, # 'β' + 20: 3, # 'γ' + 21: 3, # 'δ' + 3: 2, # 'ε' + 32: 3, # 'ζ' + 13: 1, # 'η' + 25: 3, # 'θ' + 5: 3, # 'ι' + 11: 3, # 'κ' + 16: 3, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 3, # 'ξ' + 4: 2, # 'ο' + 9: 3, # 'π' + 8: 3, # 'ρ' + 14: 3, # 'ς' + 7: 3, # 'σ' + 2: 3, # 'τ' + 12: 3, # 'υ' + 28: 3, # 'φ' + 23: 3, # 'χ' + 42: 2, # 'ψ' + 24: 0, # 'ω' + 19: 2, # 'ό' + 26: 2, # 'ύ' + 27: 0, # 'ώ' + }, + 29: { # 'β' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 3, # 'ά' + 18: 2, # 'έ' + 22: 3, # 'ή' + 15: 2, # 'ί' + 1: 3, # 'α' + 29: 0, # 'β' + 20: 2, # 'γ' + 21: 2, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 2, # 'η' + 25: 0, # 'θ' + 5: 3, # 'ι' + 11: 0, # 'κ' + 16: 3, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 0, # 'π' + 8: 3, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 0, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 2, # 'ω' + 19: 2, # 'ό' + 26: 2, # 'ύ' + 27: 2, # 'ώ' + }, + 20: { # 'γ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 3, # 'ά' + 18: 3, # 'έ' + 22: 3, # 'ή' + 15: 3, # 'ί' + 1: 3, # 'α' + 29: 0, # 'β' + 20: 3, # 'γ' + 21: 0, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 3, # 'η' + 25: 0, # 'θ' + 5: 3, # 'ι' + 11: 3, # 'κ' + 16: 3, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 3, # 'ξ' + 4: 3, # 'ο' + 9: 0, # 'π' + 8: 3, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 2, # 'υ' + 28: 0, # 'φ' + 23: 3, # 'χ' + 42: 0, # 'ψ' + 24: 3, # 'ω' + 19: 3, # 'ό' + 26: 2, # 'ύ' + 27: 3, # 'ώ' + }, + 21: { # 'δ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 2, # 'ά' + 18: 3, # 'έ' + 22: 3, # 'ή' + 15: 3, # 'ί' + 1: 3, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 3, # 'η' + 25: 0, # 'θ' + 5: 3, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 0, # 'π' + 8: 3, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 3, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 3, # 'ω' + 19: 3, # 'ό' + 26: 3, # 'ύ' + 27: 3, # 'ώ' + }, + 3: { # 'ε' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 2, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 3, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 3, # 'ί' + 1: 2, # 'α' + 29: 3, # 'β' + 20: 3, # 'γ' + 21: 3, # 'δ' + 3: 2, # 'ε' + 32: 2, # 'ζ' + 13: 0, # 'η' + 25: 3, # 'θ' + 5: 3, # 'ι' + 11: 3, # 'κ' + 16: 3, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 3, # 'ξ' + 4: 2, # 'ο' + 9: 3, # 'π' + 8: 3, # 'ρ' + 14: 3, # 'ς' + 7: 3, # 'σ' + 2: 3, # 'τ' + 12: 3, # 'υ' + 28: 3, # 'φ' + 23: 3, # 'χ' + 42: 2, # 'ψ' + 24: 3, # 'ω' + 19: 2, # 'ό' + 26: 3, # 'ύ' + 27: 2, # 'ώ' + }, + 32: { # 'ζ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 2, # 'ά' + 18: 2, # 'έ' + 22: 2, # 'ή' + 15: 2, # 'ί' + 1: 2, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 3, # 'η' + 25: 0, # 'θ' + 5: 2, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 0, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 1, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 3, # 'ω' + 19: 2, # 'ό' + 26: 0, # 'ύ' + 27: 2, # 'ώ' + }, + 13: { # 'η' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 2, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 3, # 'γ' + 21: 2, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 3, # 'θ' + 5: 0, # 'ι' + 11: 3, # 'κ' + 16: 3, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 2, # 'ξ' + 4: 0, # 'ο' + 9: 2, # 'π' + 8: 3, # 'ρ' + 14: 3, # 'ς' + 7: 3, # 'σ' + 2: 3, # 'τ' + 12: 0, # 'υ' + 28: 2, # 'φ' + 23: 3, # 'χ' + 42: 2, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 25: { # 'θ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 2, # 'ά' + 18: 3, # 'έ' + 22: 3, # 'ή' + 15: 2, # 'ί' + 1: 3, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 3, # 'η' + 25: 0, # 'θ' + 5: 3, # 'ι' + 11: 0, # 'κ' + 16: 1, # 'λ' + 10: 3, # 'μ' + 6: 2, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 0, # 'π' + 8: 3, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 3, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 3, # 'ω' + 19: 3, # 'ό' + 26: 3, # 'ύ' + 27: 3, # 'ώ' + }, + 5: { # 'ι' + 60: 0, # 'e' + 55: 1, # 'o' + 58: 0, # 't' + 36: 2, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 1, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 3, # 'ά' + 18: 3, # 'έ' + 22: 3, # 'ή' + 15: 0, # 'ί' + 1: 3, # 'α' + 29: 3, # 'β' + 20: 3, # 'γ' + 21: 3, # 'δ' + 3: 3, # 'ε' + 32: 2, # 'ζ' + 13: 3, # 'η' + 25: 3, # 'θ' + 5: 0, # 'ι' + 11: 3, # 'κ' + 16: 3, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 3, # 'ξ' + 4: 3, # 'ο' + 9: 3, # 'π' + 8: 3, # 'ρ' + 14: 3, # 'ς' + 7: 3, # 'σ' + 2: 3, # 'τ' + 12: 0, # 'υ' + 28: 2, # 'φ' + 23: 3, # 'χ' + 42: 2, # 'ψ' + 24: 3, # 'ω' + 19: 3, # 'ό' + 26: 0, # 'ύ' + 27: 3, # 'ώ' + }, + 11: { # 'κ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 3, # 'ά' + 18: 3, # 'έ' + 22: 3, # 'ή' + 15: 3, # 'ί' + 1: 3, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 3, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 3, # 'η' + 25: 2, # 'θ' + 5: 3, # 'ι' + 11: 3, # 'κ' + 16: 3, # 'λ' + 10: 3, # 'μ' + 6: 2, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 2, # 'π' + 8: 3, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 3, # 'τ' + 12: 3, # 'υ' + 28: 2, # 'φ' + 23: 2, # 'χ' + 42: 0, # 'ψ' + 24: 3, # 'ω' + 19: 3, # 'ό' + 26: 3, # 'ύ' + 27: 3, # 'ώ' + }, + 16: { # 'λ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 3, # 'ά' + 18: 3, # 'έ' + 22: 3, # 'ή' + 15: 3, # 'ί' + 1: 3, # 'α' + 29: 1, # 'β' + 20: 2, # 'γ' + 21: 1, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 3, # 'η' + 25: 2, # 'θ' + 5: 3, # 'ι' + 11: 2, # 'κ' + 16: 3, # 'λ' + 10: 2, # 'μ' + 6: 2, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 3, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 3, # 'τ' + 12: 3, # 'υ' + 28: 2, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 3, # 'ω' + 19: 3, # 'ό' + 26: 3, # 'ύ' + 27: 3, # 'ώ' + }, + 10: { # 'μ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 1, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 3, # 'ά' + 18: 3, # 'έ' + 22: 3, # 'ή' + 15: 3, # 'ί' + 1: 3, # 'α' + 29: 3, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 3, # 'η' + 25: 0, # 'θ' + 5: 3, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 3, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 2, # 'υ' + 28: 3, # 'φ' + 23: 0, # 'χ' + 42: 2, # 'ψ' + 24: 3, # 'ω' + 19: 3, # 'ό' + 26: 2, # 'ύ' + 27: 2, # 'ώ' + }, + 6: { # 'ν' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 2, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 3, # 'ά' + 18: 3, # 'έ' + 22: 3, # 'ή' + 15: 3, # 'ί' + 1: 3, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 3, # 'δ' + 3: 3, # 'ε' + 32: 2, # 'ζ' + 13: 3, # 'η' + 25: 3, # 'θ' + 5: 3, # 'ι' + 11: 0, # 'κ' + 16: 1, # 'λ' + 10: 0, # 'μ' + 6: 2, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 0, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 3, # 'σ' + 2: 3, # 'τ' + 12: 3, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 3, # 'ω' + 19: 3, # 'ό' + 26: 3, # 'ύ' + 27: 3, # 'ώ' + }, + 30: { # 'ξ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 2, # 'ά' + 18: 3, # 'έ' + 22: 3, # 'ή' + 15: 2, # 'ί' + 1: 3, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 3, # 'η' + 25: 0, # 'θ' + 5: 2, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 0, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 3, # 'τ' + 12: 2, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 3, # 'ω' + 19: 2, # 'ό' + 26: 3, # 'ύ' + 27: 1, # 'ώ' + }, + 4: { # 'ο' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 2, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 2, # 'έ' + 22: 3, # 'ή' + 15: 3, # 'ί' + 1: 2, # 'α' + 29: 3, # 'β' + 20: 3, # 'γ' + 21: 3, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 3, # 'η' + 25: 3, # 'θ' + 5: 3, # 'ι' + 11: 3, # 'κ' + 16: 3, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 2, # 'ξ' + 4: 2, # 'ο' + 9: 3, # 'π' + 8: 3, # 'ρ' + 14: 3, # 'ς' + 7: 3, # 'σ' + 2: 3, # 'τ' + 12: 3, # 'υ' + 28: 3, # 'φ' + 23: 3, # 'χ' + 42: 2, # 'ψ' + 24: 2, # 'ω' + 19: 1, # 'ό' + 26: 3, # 'ύ' + 27: 2, # 'ώ' + }, + 9: { # 'π' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 3, # 'ά' + 18: 3, # 'έ' + 22: 3, # 'ή' + 15: 3, # 'ί' + 1: 3, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 3, # 'η' + 25: 0, # 'θ' + 5: 3, # 'ι' + 11: 0, # 'κ' + 16: 3, # 'λ' + 10: 0, # 'μ' + 6: 2, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 0, # 'π' + 8: 3, # 'ρ' + 14: 2, # 'ς' + 7: 0, # 'σ' + 2: 3, # 'τ' + 12: 3, # 'υ' + 28: 0, # 'φ' + 23: 2, # 'χ' + 42: 0, # 'ψ' + 24: 3, # 'ω' + 19: 3, # 'ό' + 26: 2, # 'ύ' + 27: 3, # 'ώ' + }, + 8: { # 'ρ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 3, # 'ά' + 18: 3, # 'έ' + 22: 3, # 'ή' + 15: 3, # 'ί' + 1: 3, # 'α' + 29: 2, # 'β' + 20: 3, # 'γ' + 21: 2, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 3, # 'η' + 25: 3, # 'θ' + 5: 3, # 'ι' + 11: 3, # 'κ' + 16: 1, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 2, # 'ξ' + 4: 3, # 'ο' + 9: 2, # 'π' + 8: 2, # 'ρ' + 14: 0, # 'ς' + 7: 2, # 'σ' + 2: 3, # 'τ' + 12: 3, # 'υ' + 28: 3, # 'φ' + 23: 3, # 'χ' + 42: 0, # 'ψ' + 24: 3, # 'ω' + 19: 3, # 'ό' + 26: 3, # 'ύ' + 27: 3, # 'ώ' + }, + 14: { # 'ς' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 2, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 0, # 'θ' + 5: 0, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 0, # 'ο' + 9: 0, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 0, # 'τ' + 12: 0, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 7: { # 'σ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 2, # 'ά' + 18: 2, # 'έ' + 22: 3, # 'ή' + 15: 3, # 'ί' + 1: 3, # 'α' + 29: 3, # 'β' + 20: 0, # 'γ' + 21: 2, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 3, # 'η' + 25: 3, # 'θ' + 5: 3, # 'ι' + 11: 3, # 'κ' + 16: 2, # 'λ' + 10: 3, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 3, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 3, # 'σ' + 2: 3, # 'τ' + 12: 3, # 'υ' + 28: 3, # 'φ' + 23: 3, # 'χ' + 42: 0, # 'ψ' + 24: 3, # 'ω' + 19: 3, # 'ό' + 26: 3, # 'ύ' + 27: 2, # 'ώ' + }, + 2: { # 'τ' + 60: 0, # 'e' + 55: 2, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 3, # 'ά' + 18: 3, # 'έ' + 22: 3, # 'ή' + 15: 3, # 'ί' + 1: 3, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 3, # 'ε' + 32: 2, # 'ζ' + 13: 3, # 'η' + 25: 0, # 'θ' + 5: 3, # 'ι' + 11: 2, # 'κ' + 16: 2, # 'λ' + 10: 3, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 0, # 'π' + 8: 3, # 'ρ' + 14: 0, # 'ς' + 7: 3, # 'σ' + 2: 3, # 'τ' + 12: 3, # 'υ' + 28: 2, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 3, # 'ω' + 19: 3, # 'ό' + 26: 3, # 'ύ' + 27: 3, # 'ώ' + }, + 12: { # 'υ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 2, # 'ά' + 18: 2, # 'έ' + 22: 3, # 'ή' + 15: 2, # 'ί' + 1: 3, # 'α' + 29: 2, # 'β' + 20: 3, # 'γ' + 21: 2, # 'δ' + 3: 2, # 'ε' + 32: 2, # 'ζ' + 13: 2, # 'η' + 25: 3, # 'θ' + 5: 2, # 'ι' + 11: 3, # 'κ' + 16: 3, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 3, # 'ξ' + 4: 3, # 'ο' + 9: 3, # 'π' + 8: 3, # 'ρ' + 14: 3, # 'ς' + 7: 3, # 'σ' + 2: 3, # 'τ' + 12: 0, # 'υ' + 28: 2, # 'φ' + 23: 3, # 'χ' + 42: 2, # 'ψ' + 24: 2, # 'ω' + 19: 2, # 'ό' + 26: 0, # 'ύ' + 27: 2, # 'ώ' + }, + 28: { # 'φ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 3, # 'ά' + 18: 3, # 'έ' + 22: 3, # 'ή' + 15: 3, # 'ί' + 1: 3, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 2, # 'η' + 25: 2, # 'θ' + 5: 3, # 'ι' + 11: 0, # 'κ' + 16: 2, # 'λ' + 10: 0, # 'μ' + 6: 1, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 0, # 'π' + 8: 3, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 3, # 'τ' + 12: 3, # 'υ' + 28: 1, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 3, # 'ω' + 19: 3, # 'ό' + 26: 2, # 'ύ' + 27: 2, # 'ώ' + }, + 23: { # 'χ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 3, # 'ά' + 18: 2, # 'έ' + 22: 3, # 'ή' + 15: 3, # 'ί' + 1: 3, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 2, # 'η' + 25: 2, # 'θ' + 5: 3, # 'ι' + 11: 0, # 'κ' + 16: 2, # 'λ' + 10: 2, # 'μ' + 6: 3, # 'ν' + 30: 0, # 'ξ' + 4: 3, # 'ο' + 9: 0, # 'π' + 8: 3, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 3, # 'τ' + 12: 3, # 'υ' + 28: 0, # 'φ' + 23: 2, # 'χ' + 42: 0, # 'ψ' + 24: 3, # 'ω' + 19: 3, # 'ό' + 26: 3, # 'ύ' + 27: 3, # 'ώ' + }, + 42: { # 'ψ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 2, # 'ά' + 18: 2, # 'έ' + 22: 1, # 'ή' + 15: 2, # 'ί' + 1: 2, # 'α' + 29: 0, # 'β' + 20: 0, # 'γ' + 21: 0, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 3, # 'η' + 25: 0, # 'θ' + 5: 2, # 'ι' + 11: 0, # 'κ' + 16: 0, # 'λ' + 10: 0, # 'μ' + 6: 0, # 'ν' + 30: 0, # 'ξ' + 4: 2, # 'ο' + 9: 0, # 'π' + 8: 0, # 'ρ' + 14: 0, # 'ς' + 7: 0, # 'σ' + 2: 2, # 'τ' + 12: 1, # 'υ' + 28: 0, # 'φ' + 23: 0, # 'χ' + 42: 0, # 'ψ' + 24: 2, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 24: { # 'ω' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 1, # 'ά' + 18: 0, # 'έ' + 22: 2, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 2, # 'β' + 20: 3, # 'γ' + 21: 2, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 0, # 'η' + 25: 3, # 'θ' + 5: 2, # 'ι' + 11: 0, # 'κ' + 16: 2, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 0, # 'ξ' + 4: 0, # 'ο' + 9: 3, # 'π' + 8: 3, # 'ρ' + 14: 3, # 'ς' + 7: 3, # 'σ' + 2: 3, # 'τ' + 12: 0, # 'υ' + 28: 2, # 'φ' + 23: 2, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 19: { # 'ό' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 3, # 'β' + 20: 3, # 'γ' + 21: 3, # 'δ' + 3: 1, # 'ε' + 32: 2, # 'ζ' + 13: 2, # 'η' + 25: 2, # 'θ' + 5: 2, # 'ι' + 11: 3, # 'κ' + 16: 3, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 1, # 'ξ' + 4: 2, # 'ο' + 9: 3, # 'π' + 8: 3, # 'ρ' + 14: 3, # 'ς' + 7: 3, # 'σ' + 2: 3, # 'τ' + 12: 0, # 'υ' + 28: 2, # 'φ' + 23: 3, # 'χ' + 42: 2, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 26: { # 'ύ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 2, # 'α' + 29: 2, # 'β' + 20: 2, # 'γ' + 21: 1, # 'δ' + 3: 3, # 'ε' + 32: 0, # 'ζ' + 13: 2, # 'η' + 25: 3, # 'θ' + 5: 0, # 'ι' + 11: 3, # 'κ' + 16: 3, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 2, # 'ξ' + 4: 3, # 'ο' + 9: 3, # 'π' + 8: 3, # 'ρ' + 14: 3, # 'ς' + 7: 3, # 'σ' + 2: 3, # 'τ' + 12: 0, # 'υ' + 28: 2, # 'φ' + 23: 2, # 'χ' + 42: 2, # 'ψ' + 24: 2, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, + 27: { # 'ώ' + 60: 0, # 'e' + 55: 0, # 'o' + 58: 0, # 't' + 36: 0, # '·' + 61: 0, # 'Ά' + 46: 0, # 'Έ' + 54: 0, # 'Ό' + 31: 0, # 'Α' + 51: 0, # 'Β' + 43: 0, # 'Γ' + 41: 0, # 'Δ' + 34: 0, # 'Ε' + 40: 0, # 'Η' + 52: 0, # 'Θ' + 47: 0, # 'Ι' + 44: 0, # 'Κ' + 53: 0, # 'Λ' + 38: 0, # 'Μ' + 49: 0, # 'Ν' + 59: 0, # 'Ξ' + 39: 0, # 'Ο' + 35: 0, # 'Π' + 48: 0, # 'Ρ' + 37: 0, # 'Σ' + 33: 0, # 'Τ' + 45: 0, # 'Υ' + 56: 0, # 'Φ' + 50: 0, # 'Χ' + 57: 0, # 'Ω' + 17: 0, # 'ά' + 18: 0, # 'έ' + 22: 0, # 'ή' + 15: 0, # 'ί' + 1: 0, # 'α' + 29: 1, # 'β' + 20: 0, # 'γ' + 21: 3, # 'δ' + 3: 0, # 'ε' + 32: 0, # 'ζ' + 13: 1, # 'η' + 25: 2, # 'θ' + 5: 2, # 'ι' + 11: 0, # 'κ' + 16: 2, # 'λ' + 10: 3, # 'μ' + 6: 3, # 'ν' + 30: 1, # 'ξ' + 4: 0, # 'ο' + 9: 2, # 'π' + 8: 3, # 'ρ' + 14: 3, # 'ς' + 7: 3, # 'σ' + 2: 3, # 'τ' + 12: 0, # 'υ' + 28: 1, # 'φ' + 23: 1, # 'χ' + 42: 0, # 'ψ' + 24: 0, # 'ω' + 19: 0, # 'ό' + 26: 0, # 'ύ' + 27: 0, # 'ώ' + }, +} + +# 255: Undefined characters that did not exist in training text # 254: Carriage/Return # 253: symbol (punctuation) that does not belong to word # 252: 0 - 9 +# 251: Control characters -# Character Mapping Table: -Latin7_char_to_order_map = ( -255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 -253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 -252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 -253, 82,100,104, 94, 98,101,116,102,111,187,117, 92, 88,113, 85, # 40 - 79,118,105, 83, 67,114,119, 95, 99,109,188,253,253,253,253,253, # 50 -253, 72, 70, 80, 81, 60, 96, 93, 89, 68,120, 97, 77, 86, 69, 55, # 60 - 78,115, 65, 66, 58, 76,106,103, 87,107,112,253,253,253,253,253, # 70 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 80 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 90 -253,233, 90,253,253,253,253,253,253,253,253,253,253, 74,253,253, # a0 -253,253,253,253,247,248, 61, 36, 46, 71, 73,253, 54,253,108,123, # b0 -110, 31, 51, 43, 41, 34, 91, 40, 52, 47, 44, 53, 38, 49, 59, 39, # c0 - 35, 48,250, 37, 33, 45, 56, 50, 84, 57,120,121, 17, 18, 22, 15, # d0 -124, 1, 29, 20, 21, 3, 32, 13, 25, 5, 11, 16, 10, 6, 30, 4, # e0 - 9, 8, 14, 7, 2, 12, 28, 23, 42, 24, 64, 75, 19, 26, 27,253, # f0 -) - -win1253_char_to_order_map = ( -255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 -253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 -252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 -253, 82,100,104, 94, 98,101,116,102,111,187,117, 92, 88,113, 85, # 40 - 79,118,105, 83, 67,114,119, 95, 99,109,188,253,253,253,253,253, # 50 -253, 72, 70, 80, 81, 60, 96, 93, 89, 68,120, 97, 77, 86, 69, 55, # 60 - 78,115, 65, 66, 58, 76,106,103, 87,107,112,253,253,253,253,253, # 70 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 80 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 90 -253,233, 61,253,253,253,253,253,253,253,253,253,253, 74,253,253, # a0 -253,253,253,253,247,253,253, 36, 46, 71, 73,253, 54,253,108,123, # b0 -110, 31, 51, 43, 41, 34, 91, 40, 52, 47, 44, 53, 38, 49, 59, 39, # c0 - 35, 48,250, 37, 33, 45, 56, 50, 84, 57,120,121, 17, 18, 22, 15, # d0 -124, 1, 29, 20, 21, 3, 32, 13, 25, 5, 11, 16, 10, 6, 30, 4, # e0 - 9, 8, 14, 7, 2, 12, 28, 23, 42, 24, 64, 75, 19, 26, 27,253, # f0 -) +# Character Mapping Table(s): +WINDOWS_1253_GREEK_CHAR_TO_ORDER = { + 0: 255, # '\x00' + 1: 255, # '\x01' + 2: 255, # '\x02' + 3: 255, # '\x03' + 4: 255, # '\x04' + 5: 255, # '\x05' + 6: 255, # '\x06' + 7: 255, # '\x07' + 8: 255, # '\x08' + 9: 255, # '\t' + 10: 254, # '\n' + 11: 255, # '\x0b' + 12: 255, # '\x0c' + 13: 254, # '\r' + 14: 255, # '\x0e' + 15: 255, # '\x0f' + 16: 255, # '\x10' + 17: 255, # '\x11' + 18: 255, # '\x12' + 19: 255, # '\x13' + 20: 255, # '\x14' + 21: 255, # '\x15' + 22: 255, # '\x16' + 23: 255, # '\x17' + 24: 255, # '\x18' + 25: 255, # '\x19' + 26: 255, # '\x1a' + 27: 255, # '\x1b' + 28: 255, # '\x1c' + 29: 255, # '\x1d' + 30: 255, # '\x1e' + 31: 255, # '\x1f' + 32: 253, # ' ' + 33: 253, # '!' + 34: 253, # '"' + 35: 253, # '#' + 36: 253, # '$' + 37: 253, # '%' + 38: 253, # '&' + 39: 253, # "'" + 40: 253, # '(' + 41: 253, # ')' + 42: 253, # '*' + 43: 253, # '+' + 44: 253, # ',' + 45: 253, # '-' + 46: 253, # '.' + 47: 253, # '/' + 48: 252, # '0' + 49: 252, # '1' + 50: 252, # '2' + 51: 252, # '3' + 52: 252, # '4' + 53: 252, # '5' + 54: 252, # '6' + 55: 252, # '7' + 56: 252, # '8' + 57: 252, # '9' + 58: 253, # ':' + 59: 253, # ';' + 60: 253, # '<' + 61: 253, # '=' + 62: 253, # '>' + 63: 253, # '?' + 64: 253, # '@' + 65: 82, # 'A' + 66: 100, # 'B' + 67: 104, # 'C' + 68: 94, # 'D' + 69: 98, # 'E' + 70: 101, # 'F' + 71: 116, # 'G' + 72: 102, # 'H' + 73: 111, # 'I' + 74: 187, # 'J' + 75: 117, # 'K' + 76: 92, # 'L' + 77: 88, # 'M' + 78: 113, # 'N' + 79: 85, # 'O' + 80: 79, # 'P' + 81: 118, # 'Q' + 82: 105, # 'R' + 83: 83, # 'S' + 84: 67, # 'T' + 85: 114, # 'U' + 86: 119, # 'V' + 87: 95, # 'W' + 88: 99, # 'X' + 89: 109, # 'Y' + 90: 188, # 'Z' + 91: 253, # '[' + 92: 253, # '\\' + 93: 253, # ']' + 94: 253, # '^' + 95: 253, # '_' + 96: 253, # '`' + 97: 72, # 'a' + 98: 70, # 'b' + 99: 80, # 'c' + 100: 81, # 'd' + 101: 60, # 'e' + 102: 96, # 'f' + 103: 93, # 'g' + 104: 89, # 'h' + 105: 68, # 'i' + 106: 120, # 'j' + 107: 97, # 'k' + 108: 77, # 'l' + 109: 86, # 'm' + 110: 69, # 'n' + 111: 55, # 'o' + 112: 78, # 'p' + 113: 115, # 'q' + 114: 65, # 'r' + 115: 66, # 's' + 116: 58, # 't' + 117: 76, # 'u' + 118: 106, # 'v' + 119: 103, # 'w' + 120: 87, # 'x' + 121: 107, # 'y' + 122: 112, # 'z' + 123: 253, # '{' + 124: 253, # '|' + 125: 253, # '}' + 126: 253, # '~' + 127: 253, # '\x7f' + 128: 255, # '€' + 129: 255, # None + 130: 255, # '‚' + 131: 255, # 'ƒ' + 132: 255, # '„' + 133: 255, # '…' + 134: 255, # '†' + 135: 255, # '‡' + 136: 255, # None + 137: 255, # '‰' + 138: 255, # None + 139: 255, # '‹' + 140: 255, # None + 141: 255, # None + 142: 255, # None + 143: 255, # None + 144: 255, # None + 145: 255, # '‘' + 146: 255, # '’' + 147: 255, # '“' + 148: 255, # '”' + 149: 255, # '•' + 150: 255, # '–' + 151: 255, # '—' + 152: 255, # None + 153: 255, # '™' + 154: 255, # None + 155: 255, # '›' + 156: 255, # None + 157: 255, # None + 158: 255, # None + 159: 255, # None + 160: 253, # '\xa0' + 161: 233, # '΅' + 162: 61, # 'Ά' + 163: 253, # '£' + 164: 253, # '¤' + 165: 253, # '¥' + 166: 253, # '¦' + 167: 253, # '§' + 168: 253, # '¨' + 169: 253, # '©' + 170: 253, # None + 171: 253, # '«' + 172: 253, # '¬' + 173: 74, # '\xad' + 174: 253, # '®' + 175: 253, # '―' + 176: 253, # '°' + 177: 253, # '±' + 178: 253, # '²' + 179: 253, # '³' + 180: 247, # '΄' + 181: 253, # 'µ' + 182: 253, # '¶' + 183: 36, # '·' + 184: 46, # 'Έ' + 185: 71, # 'Ή' + 186: 73, # 'Ί' + 187: 253, # '»' + 188: 54, # 'Ό' + 189: 253, # '½' + 190: 108, # 'Ύ' + 191: 123, # 'Ώ' + 192: 110, # 'ΐ' + 193: 31, # 'Α' + 194: 51, # 'Β' + 195: 43, # 'Γ' + 196: 41, # 'Δ' + 197: 34, # 'Ε' + 198: 91, # 'Ζ' + 199: 40, # 'Η' + 200: 52, # 'Θ' + 201: 47, # 'Ι' + 202: 44, # 'Κ' + 203: 53, # 'Λ' + 204: 38, # 'Μ' + 205: 49, # 'Ν' + 206: 59, # 'Ξ' + 207: 39, # 'Ο' + 208: 35, # 'Π' + 209: 48, # 'Ρ' + 210: 250, # None + 211: 37, # 'Σ' + 212: 33, # 'Τ' + 213: 45, # 'Υ' + 214: 56, # 'Φ' + 215: 50, # 'Χ' + 216: 84, # 'Ψ' + 217: 57, # 'Ω' + 218: 120, # 'Ϊ' + 219: 121, # 'Ϋ' + 220: 17, # 'ά' + 221: 18, # 'έ' + 222: 22, # 'ή' + 223: 15, # 'ί' + 224: 124, # 'ΰ' + 225: 1, # 'α' + 226: 29, # 'β' + 227: 20, # 'γ' + 228: 21, # 'δ' + 229: 3, # 'ε' + 230: 32, # 'ζ' + 231: 13, # 'η' + 232: 25, # 'θ' + 233: 5, # 'ι' + 234: 11, # 'κ' + 235: 16, # 'λ' + 236: 10, # 'μ' + 237: 6, # 'ν' + 238: 30, # 'ξ' + 239: 4, # 'ο' + 240: 9, # 'π' + 241: 8, # 'ρ' + 242: 14, # 'ς' + 243: 7, # 'σ' + 244: 2, # 'τ' + 245: 12, # 'υ' + 246: 28, # 'φ' + 247: 23, # 'χ' + 248: 42, # 'ψ' + 249: 24, # 'ω' + 250: 64, # 'ϊ' + 251: 75, # 'ϋ' + 252: 19, # 'ό' + 253: 26, # 'ύ' + 254: 27, # 'ώ' + 255: 253, # None +} -# Model Table: -# total sequences: 100% -# first 512 sequences: 98.2851% -# first 1024 sequences:1.7001% -# rest sequences: 0.0359% -# negative sequences: 0.0148% -GreekLangModel = ( -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,3,2,2,3,3,3,3,3,3,3,3,1,3,3,3,0,2,2,3,3,0,3,0,3,2,0,3,3,3,0, -3,0,0,0,2,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,3,3,3,3,0,3,3,0,3,2,3,3,0,3,2,3,3,3,0,0,3,0,3,0,3,3,2,0,0,0, -2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0, -0,2,3,2,2,3,3,3,3,3,3,3,3,0,3,3,3,3,0,2,3,3,0,3,3,3,3,2,3,3,3,0, -2,0,0,0,2,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,2,3,3,2,3,3,3,3,3,3,3,3,3,3,3,3,0,2,1,3,3,3,3,2,3,3,2,3,3,2,0, -0,0,0,0,2,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,3,3,3,0,3,3,3,3,3,3,0,3,3,0,3,3,3,3,3,3,3,3,3,3,0,3,2,3,3,0, -2,0,1,0,2,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0, -0,3,3,3,3,3,2,3,0,0,0,0,3,3,0,3,1,3,3,3,0,3,3,0,3,3,3,3,0,0,0,0, -2,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,3,3,3,3,0,3,0,3,3,3,3,3,0,3,2,2,2,3,0,2,3,3,3,3,3,2,3,3,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,3,3,3,3,3,2,2,2,3,3,3,3,0,3,1,3,3,3,3,2,3,3,3,3,3,3,3,2,2,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,3,3,3,3,2,0,3,0,0,0,3,3,2,3,3,3,3,3,0,0,3,2,3,0,2,3,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,0,3,3,3,3,0,0,3,3,0,2,3,0,3,0,3,3,3,0,0,3,0,3,0,2,2,3,3,0,0, -0,0,1,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,3,3,3,3,2,0,3,2,3,3,3,3,0,3,3,3,3,3,0,3,3,2,3,2,3,3,2,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,3,2,3,2,3,3,3,3,3,3,0,2,3,2,3,2,2,2,3,2,3,3,2,3,0,2,2,2,3,0, -2,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,3,0,0,0,3,3,3,2,3,3,0,0,3,0,3,0,0,0,3,2,0,3,0,3,0,0,2,0,2,0, -0,0,0,0,2,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,3,3,3,0,3,3,3,3,3,3,0,3,3,0,3,0,0,0,3,3,0,3,3,3,0,0,1,2,3,0, -3,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,3,3,3,3,2,0,0,3,2,2,3,3,0,3,3,3,3,3,2,1,3,0,3,2,3,3,2,1,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,3,3,0,2,3,3,3,3,3,3,0,0,3,0,3,0,0,0,3,3,0,3,2,3,0,0,3,3,3,0, -3,0,0,0,2,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,3,3,3,0,3,3,3,3,3,3,0,0,3,0,3,0,0,0,3,2,0,3,2,3,0,0,3,2,3,0, -2,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,3,1,2,2,3,3,3,3,3,3,0,2,3,0,3,0,0,0,3,3,0,3,0,2,0,0,2,3,1,0, -2,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,0,3,3,3,3,0,3,0,3,3,2,3,0,3,3,3,3,3,3,0,3,3,3,0,2,3,0,0,3,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,0,3,3,3,0,0,3,0,0,0,3,3,0,3,0,2,3,3,0,0,3,0,3,0,3,3,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,3,0,0,0,3,3,3,3,3,3,0,0,3,0,2,0,0,0,3,3,0,3,0,3,0,0,2,0,2,0, -0,0,0,0,1,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,3,3,3,3,3,0,3,0,2,0,3,2,0,3,2,3,2,3,0,0,3,2,3,2,3,3,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,3,0,0,2,3,3,3,3,3,0,0,0,3,0,2,1,0,0,3,2,2,2,0,3,0,0,2,2,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,0,3,3,3,2,0,3,0,3,0,3,3,0,2,1,2,3,3,0,0,3,0,3,0,3,3,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,2,3,3,3,0,3,3,3,3,3,3,0,2,3,0,3,0,0,0,2,1,0,2,2,3,0,0,2,2,2,0, -0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,3,0,0,2,3,3,3,2,3,0,0,1,3,0,2,0,0,0,0,3,0,1,0,2,0,0,1,1,1,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,3,3,3,3,1,0,3,0,0,0,3,2,0,3,2,3,3,3,0,0,3,0,3,2,2,2,1,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,0,3,3,3,0,0,3,0,0,0,0,2,0,2,3,3,2,2,2,2,3,0,2,0,2,2,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,3,3,3,2,0,0,0,0,0,0,2,3,0,2,0,2,3,2,0,0,3,0,3,0,3,1,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,3,2,3,3,2,2,3,0,2,0,3,0,0,0,2,0,0,0,0,1,2,0,2,0,2,0, -0,2,0,2,0,2,2,0,0,1,0,2,2,2,0,2,2,2,0,2,2,2,0,0,2,0,0,1,0,0,0,0, -0,2,0,3,3,2,0,0,0,0,0,0,1,3,0,2,0,2,2,2,0,0,2,0,3,0,0,2,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,0,2,3,2,0,2,2,0,2,0,2,2,0,2,0,2,2,2,0,0,0,0,0,0,2,3,0,0,0,2, -0,1,2,0,0,0,0,2,2,0,0,0,2,1,0,2,2,0,0,0,0,0,0,1,0,2,0,0,0,0,0,0, -0,0,2,1,0,2,3,2,2,3,2,3,2,0,0,3,3,3,0,0,3,2,0,0,0,1,1,0,2,0,2,2, -0,2,0,2,0,2,2,0,0,2,0,2,2,2,0,2,2,2,2,0,0,2,0,0,0,2,0,1,0,0,0,0, -0,3,0,3,3,2,2,0,3,0,0,0,2,2,0,2,2,2,1,2,0,0,1,2,2,0,0,3,0,0,0,2, -0,1,2,0,0,0,1,2,0,0,0,0,0,0,0,2,2,0,1,0,0,2,0,0,0,2,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,2,3,3,2,2,0,0,0,2,0,2,3,3,0,2,0,0,0,0,0,0,2,2,2,0,2,2,0,2,0,2, -0,2,2,0,0,2,2,2,2,1,0,0,2,2,0,2,0,0,2,0,0,0,0,0,0,2,0,0,0,0,0,0, -0,2,0,3,2,3,0,0,0,3,0,0,2,2,0,2,0,2,2,2,0,0,2,0,0,0,0,0,0,0,0,2, -0,0,2,2,0,0,2,2,2,0,0,0,0,0,0,2,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,2,0,0,3,2,0,2,2,2,2,2,0,0,0,2,0,0,0,0,2,0,1,0,0,2,0,1,0,0,0, -0,2,2,2,0,2,2,0,1,2,0,2,2,2,0,2,2,2,2,1,2,2,0,0,2,0,0,0,0,0,0,0, -0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0, -0,2,0,2,0,2,2,0,0,0,0,1,2,1,0,0,2,2,0,0,2,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,3,2,3,0,0,2,0,0,0,2,2,0,2,0,0,0,1,0,0,2,0,2,0,2,2,0,0,0,0, -0,0,2,0,0,0,0,2,2,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0, -0,2,2,3,2,2,0,0,0,0,0,0,1,3,0,2,0,2,2,0,0,0,1,0,2,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,2,0,2,0,3,2,0,2,0,0,0,0,0,0,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, -0,0,2,0,0,0,0,1,1,0,0,2,1,2,0,2,2,0,1,0,0,1,0,0,0,2,0,0,0,0,0,0, -0,3,0,2,2,2,0,0,2,0,0,0,2,0,0,0,2,3,0,2,0,0,0,0,0,0,2,2,0,0,0,2, -0,1,2,0,0,0,1,2,2,1,0,0,0,2,0,0,2,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,2,1,2,0,2,2,0,2,0,0,2,0,0,0,0,1,2,1,0,2,1,0,0,0,0,0,0,0,0,0,0, -0,0,2,0,0,0,3,1,2,2,0,2,0,0,0,0,2,0,0,0,2,0,0,3,0,0,0,0,2,2,2,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,2,1,0,2,0,1,2,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,1,0,0,0,0,0,0,2, -0,2,2,0,0,2,2,2,2,2,0,1,2,0,0,0,2,2,0,1,0,2,0,0,2,2,0,0,0,0,0,0, -0,0,0,0,1,0,0,0,0,0,0,0,3,0,0,2,0,0,0,0,0,0,0,0,2,0,2,0,0,0,0,2, -0,1,2,0,0,0,0,2,2,1,0,1,0,1,0,2,2,2,1,0,0,0,0,0,0,1,0,0,0,0,0,0, -0,2,0,1,2,0,0,0,0,0,0,0,0,0,0,2,0,0,2,2,0,0,0,0,1,0,0,0,0,0,0,2, -0,2,2,0,0,0,0,2,2,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,2,0,0,2,0,0,0, -0,2,2,2,2,0,0,0,3,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,2,0,0,0,0,0,0,1, -0,0,2,0,0,0,0,1,2,0,0,0,0,0,0,2,2,1,1,0,0,0,0,0,0,1,0,0,0,0,0,0, -0,2,0,2,2,2,0,0,2,0,0,0,0,0,0,0,2,2,2,0,0,0,2,0,0,0,0,0,0,0,0,2, -0,0,1,0,0,0,0,2,1,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0, -0,3,0,2,0,0,0,0,0,0,0,0,2,0,0,0,0,0,2,0,0,0,0,0,0,0,2,0,0,0,0,2, -0,0,2,0,0,0,0,2,2,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,2,0,2,2,1,0,0,0,0,0,0,2,0,0,2,0,2,2,2,0,0,0,0,0,0,2,0,0,0,0,2, -0,0,2,0,0,2,0,2,2,0,0,0,0,2,0,2,0,0,0,0,0,2,0,0,0,2,0,0,0,0,0,0, -0,0,3,0,0,0,2,2,0,2,2,0,0,0,0,0,2,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,2,0,0,0,0,0, -0,2,2,2,2,2,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,1, -0,0,0,0,0,0,0,2,1,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,2,2,0,0,0,0,0,2,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0, -0,2,0,0,0,2,0,0,0,0,0,1,0,0,0,0,2,2,0,0,0,1,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,0,2,0,0,0, -0,2,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,1,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,0,0,2,0,2,0,0,0, -0,0,0,0,0,0,0,0,2,1,0,0,0,0,0,0,2,0,0,0,1,2,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -) +WINDOWS_1253_GREEK_MODEL = SingleByteCharSetModel(charset_name='windows-1253', + language='Greek', + char_to_order_map=WINDOWS_1253_GREEK_CHAR_TO_ORDER, + language_model=GREEK_LANG_MODEL, + typical_positive_ratio=0.982851, + keep_ascii_letters=False, + alphabet='ΆΈΉΊΌΎΏΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩάέήίαβγδεζηθικλμνξοπρςστυφχψωόύώ') -Latin7GreekModel = { - 'char_to_order_map': Latin7_char_to_order_map, - 'precedence_matrix': GreekLangModel, - 'typical_positive_ratio': 0.982851, - 'keep_english_letter': False, - 'charset_name': "ISO-8859-7", - 'language': 'Greek', +ISO_8859_7_GREEK_CHAR_TO_ORDER = { + 0: 255, # '\x00' + 1: 255, # '\x01' + 2: 255, # '\x02' + 3: 255, # '\x03' + 4: 255, # '\x04' + 5: 255, # '\x05' + 6: 255, # '\x06' + 7: 255, # '\x07' + 8: 255, # '\x08' + 9: 255, # '\t' + 10: 254, # '\n' + 11: 255, # '\x0b' + 12: 255, # '\x0c' + 13: 254, # '\r' + 14: 255, # '\x0e' + 15: 255, # '\x0f' + 16: 255, # '\x10' + 17: 255, # '\x11' + 18: 255, # '\x12' + 19: 255, # '\x13' + 20: 255, # '\x14' + 21: 255, # '\x15' + 22: 255, # '\x16' + 23: 255, # '\x17' + 24: 255, # '\x18' + 25: 255, # '\x19' + 26: 255, # '\x1a' + 27: 255, # '\x1b' + 28: 255, # '\x1c' + 29: 255, # '\x1d' + 30: 255, # '\x1e' + 31: 255, # '\x1f' + 32: 253, # ' ' + 33: 253, # '!' + 34: 253, # '"' + 35: 253, # '#' + 36: 253, # '$' + 37: 253, # '%' + 38: 253, # '&' + 39: 253, # "'" + 40: 253, # '(' + 41: 253, # ')' + 42: 253, # '*' + 43: 253, # '+' + 44: 253, # ',' + 45: 253, # '-' + 46: 253, # '.' + 47: 253, # '/' + 48: 252, # '0' + 49: 252, # '1' + 50: 252, # '2' + 51: 252, # '3' + 52: 252, # '4' + 53: 252, # '5' + 54: 252, # '6' + 55: 252, # '7' + 56: 252, # '8' + 57: 252, # '9' + 58: 253, # ':' + 59: 253, # ';' + 60: 253, # '<' + 61: 253, # '=' + 62: 253, # '>' + 63: 253, # '?' + 64: 253, # '@' + 65: 82, # 'A' + 66: 100, # 'B' + 67: 104, # 'C' + 68: 94, # 'D' + 69: 98, # 'E' + 70: 101, # 'F' + 71: 116, # 'G' + 72: 102, # 'H' + 73: 111, # 'I' + 74: 187, # 'J' + 75: 117, # 'K' + 76: 92, # 'L' + 77: 88, # 'M' + 78: 113, # 'N' + 79: 85, # 'O' + 80: 79, # 'P' + 81: 118, # 'Q' + 82: 105, # 'R' + 83: 83, # 'S' + 84: 67, # 'T' + 85: 114, # 'U' + 86: 119, # 'V' + 87: 95, # 'W' + 88: 99, # 'X' + 89: 109, # 'Y' + 90: 188, # 'Z' + 91: 253, # '[' + 92: 253, # '\\' + 93: 253, # ']' + 94: 253, # '^' + 95: 253, # '_' + 96: 253, # '`' + 97: 72, # 'a' + 98: 70, # 'b' + 99: 80, # 'c' + 100: 81, # 'd' + 101: 60, # 'e' + 102: 96, # 'f' + 103: 93, # 'g' + 104: 89, # 'h' + 105: 68, # 'i' + 106: 120, # 'j' + 107: 97, # 'k' + 108: 77, # 'l' + 109: 86, # 'm' + 110: 69, # 'n' + 111: 55, # 'o' + 112: 78, # 'p' + 113: 115, # 'q' + 114: 65, # 'r' + 115: 66, # 's' + 116: 58, # 't' + 117: 76, # 'u' + 118: 106, # 'v' + 119: 103, # 'w' + 120: 87, # 'x' + 121: 107, # 'y' + 122: 112, # 'z' + 123: 253, # '{' + 124: 253, # '|' + 125: 253, # '}' + 126: 253, # '~' + 127: 253, # '\x7f' + 128: 255, # '\x80' + 129: 255, # '\x81' + 130: 255, # '\x82' + 131: 255, # '\x83' + 132: 255, # '\x84' + 133: 255, # '\x85' + 134: 255, # '\x86' + 135: 255, # '\x87' + 136: 255, # '\x88' + 137: 255, # '\x89' + 138: 255, # '\x8a' + 139: 255, # '\x8b' + 140: 255, # '\x8c' + 141: 255, # '\x8d' + 142: 255, # '\x8e' + 143: 255, # '\x8f' + 144: 255, # '\x90' + 145: 255, # '\x91' + 146: 255, # '\x92' + 147: 255, # '\x93' + 148: 255, # '\x94' + 149: 255, # '\x95' + 150: 255, # '\x96' + 151: 255, # '\x97' + 152: 255, # '\x98' + 153: 255, # '\x99' + 154: 255, # '\x9a' + 155: 255, # '\x9b' + 156: 255, # '\x9c' + 157: 255, # '\x9d' + 158: 255, # '\x9e' + 159: 255, # '\x9f' + 160: 253, # '\xa0' + 161: 233, # '‘' + 162: 90, # '’' + 163: 253, # '£' + 164: 253, # '€' + 165: 253, # '₯' + 166: 253, # '¦' + 167: 253, # '§' + 168: 253, # '¨' + 169: 253, # '©' + 170: 253, # 'ͺ' + 171: 253, # '«' + 172: 253, # '¬' + 173: 74, # '\xad' + 174: 253, # None + 175: 253, # '―' + 176: 253, # '°' + 177: 253, # '±' + 178: 253, # '²' + 179: 253, # '³' + 180: 247, # '΄' + 181: 248, # '΅' + 182: 61, # 'Ά' + 183: 36, # '·' + 184: 46, # 'Έ' + 185: 71, # 'Ή' + 186: 73, # 'Ί' + 187: 253, # '»' + 188: 54, # 'Ό' + 189: 253, # '½' + 190: 108, # 'Ύ' + 191: 123, # 'Ώ' + 192: 110, # 'ΐ' + 193: 31, # 'Α' + 194: 51, # 'Β' + 195: 43, # 'Γ' + 196: 41, # 'Δ' + 197: 34, # 'Ε' + 198: 91, # 'Ζ' + 199: 40, # 'Η' + 200: 52, # 'Θ' + 201: 47, # 'Ι' + 202: 44, # 'Κ' + 203: 53, # 'Λ' + 204: 38, # 'Μ' + 205: 49, # 'Ν' + 206: 59, # 'Ξ' + 207: 39, # 'Ο' + 208: 35, # 'Π' + 209: 48, # 'Ρ' + 210: 250, # None + 211: 37, # 'Σ' + 212: 33, # 'Τ' + 213: 45, # 'Υ' + 214: 56, # 'Φ' + 215: 50, # 'Χ' + 216: 84, # 'Ψ' + 217: 57, # 'Ω' + 218: 120, # 'Ϊ' + 219: 121, # 'Ϋ' + 220: 17, # 'ά' + 221: 18, # 'έ' + 222: 22, # 'ή' + 223: 15, # 'ί' + 224: 124, # 'ΰ' + 225: 1, # 'α' + 226: 29, # 'β' + 227: 20, # 'γ' + 228: 21, # 'δ' + 229: 3, # 'ε' + 230: 32, # 'ζ' + 231: 13, # 'η' + 232: 25, # 'θ' + 233: 5, # 'ι' + 234: 11, # 'κ' + 235: 16, # 'λ' + 236: 10, # 'μ' + 237: 6, # 'ν' + 238: 30, # 'ξ' + 239: 4, # 'ο' + 240: 9, # 'π' + 241: 8, # 'ρ' + 242: 14, # 'ς' + 243: 7, # 'σ' + 244: 2, # 'τ' + 245: 12, # 'υ' + 246: 28, # 'φ' + 247: 23, # 'χ' + 248: 42, # 'ψ' + 249: 24, # 'ω' + 250: 64, # 'ϊ' + 251: 75, # 'ϋ' + 252: 19, # 'ό' + 253: 26, # 'ύ' + 254: 27, # 'ώ' + 255: 253, # None } -Win1253GreekModel = { - 'char_to_order_map': win1253_char_to_order_map, - 'precedence_matrix': GreekLangModel, - 'typical_positive_ratio': 0.982851, - 'keep_english_letter': False, - 'charset_name': "windows-1253", - 'language': 'Greek', -} +ISO_8859_7_GREEK_MODEL = SingleByteCharSetModel(charset_name='ISO-8859-7', + language='Greek', + char_to_order_map=ISO_8859_7_GREEK_CHAR_TO_ORDER, + language_model=GREEK_LANG_MODEL, + typical_positive_ratio=0.982851, + keep_ascii_letters=False, + alphabet='ΆΈΉΊΌΎΏΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΤΥΦΧΨΩάέήίαβγδεζηθικλμνξοπρςστυφχψωόύώ') + diff --git a/src/pip/_vendor/chardet/langhebrewmodel.py b/src/pip/_vendor/chardet/langhebrewmodel.py index 58f4c875ec9..484c652a48e 100644 --- a/src/pip/_vendor/chardet/langhebrewmodel.py +++ b/src/pip/_vendor/chardet/langhebrewmodel.py @@ -1,200 +1,4383 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is Mozilla Universal charset detector code. -# -# The Initial Developer of the Original Code is -# Simon Montagu -# Portions created by the Initial Developer are Copyright (C) 2005 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# Shy Shalom - original C code -# Shoshannah Forbes - original C code (?) -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### +#!/usr/bin/env python +# -*- coding: utf-8 -*- -# 255: Control characters that usually does not exist in any text +from pip._vendor.chardet.sbcharsetprober import SingleByteCharSetModel + + +# 3: Positive +# 2: Likely +# 1: Unlikely +# 0: Negative + +HEBREW_LANG_MODEL = { + 50: { # 'a' + 50: 0, # 'a' + 60: 1, # 'c' + 61: 1, # 'd' + 42: 1, # 'e' + 53: 1, # 'i' + 56: 2, # 'l' + 54: 2, # 'n' + 49: 0, # 'o' + 51: 2, # 'r' + 43: 1, # 's' + 44: 2, # 't' + 63: 1, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 0, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 1, # 'ה' + 2: 0, # 'ו' + 24: 0, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 0, # 'י' + 25: 0, # 'ך' + 15: 0, # 'כ' + 4: 0, # 'ל' + 11: 0, # 'ם' + 6: 1, # 'מ' + 23: 0, # 'ן' + 12: 0, # 'נ' + 19: 0, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 0, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 1, # 'ק' + 7: 0, # 'ר' + 10: 1, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 1, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 60: { # 'c' + 50: 1, # 'a' + 60: 1, # 'c' + 61: 0, # 'd' + 42: 1, # 'e' + 53: 1, # 'i' + 56: 1, # 'l' + 54: 0, # 'n' + 49: 1, # 'o' + 51: 1, # 'r' + 43: 1, # 's' + 44: 2, # 't' + 63: 1, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 1, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 1, # 'ה' + 2: 0, # 'ו' + 24: 0, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 0, # 'י' + 25: 0, # 'ך' + 15: 0, # 'כ' + 4: 0, # 'ל' + 11: 0, # 'ם' + 6: 1, # 'מ' + 23: 0, # 'ן' + 12: 1, # 'נ' + 19: 0, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 0, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 61: { # 'd' + 50: 1, # 'a' + 60: 0, # 'c' + 61: 1, # 'd' + 42: 1, # 'e' + 53: 1, # 'i' + 56: 1, # 'l' + 54: 1, # 'n' + 49: 2, # 'o' + 51: 1, # 'r' + 43: 1, # 's' + 44: 0, # 't' + 63: 1, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 0, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 1, # 'ה' + 2: 0, # 'ו' + 24: 0, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 0, # 'י' + 25: 0, # 'ך' + 15: 0, # 'כ' + 4: 0, # 'ל' + 11: 0, # 'ם' + 6: 0, # 'מ' + 23: 0, # 'ן' + 12: 0, # 'נ' + 19: 0, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 0, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 1, # '–' + 52: 1, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 42: { # 'e' + 50: 1, # 'a' + 60: 1, # 'c' + 61: 2, # 'd' + 42: 1, # 'e' + 53: 1, # 'i' + 56: 2, # 'l' + 54: 2, # 'n' + 49: 1, # 'o' + 51: 2, # 'r' + 43: 2, # 's' + 44: 2, # 't' + 63: 1, # 'u' + 34: 1, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 0, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 0, # 'ה' + 2: 0, # 'ו' + 24: 0, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 0, # 'י' + 25: 0, # 'ך' + 15: 0, # 'כ' + 4: 0, # 'ל' + 11: 0, # 'ם' + 6: 0, # 'מ' + 23: 0, # 'ן' + 12: 0, # 'נ' + 19: 0, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 1, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 1, # '–' + 52: 2, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 53: { # 'i' + 50: 1, # 'a' + 60: 2, # 'c' + 61: 1, # 'd' + 42: 1, # 'e' + 53: 0, # 'i' + 56: 1, # 'l' + 54: 2, # 'n' + 49: 2, # 'o' + 51: 1, # 'r' + 43: 2, # 's' + 44: 2, # 't' + 63: 1, # 'u' + 34: 0, # '\xa0' + 55: 1, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 0, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 0, # 'ה' + 2: 0, # 'ו' + 24: 0, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 0, # 'י' + 25: 0, # 'ך' + 15: 0, # 'כ' + 4: 0, # 'ל' + 11: 0, # 'ם' + 6: 0, # 'מ' + 23: 0, # 'ן' + 12: 0, # 'נ' + 19: 0, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 0, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 1, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 56: { # 'l' + 50: 1, # 'a' + 60: 1, # 'c' + 61: 1, # 'd' + 42: 2, # 'e' + 53: 2, # 'i' + 56: 2, # 'l' + 54: 1, # 'n' + 49: 1, # 'o' + 51: 0, # 'r' + 43: 1, # 's' + 44: 1, # 't' + 63: 1, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 0, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 0, # 'ה' + 2: 0, # 'ו' + 24: 0, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 0, # 'י' + 25: 0, # 'ך' + 15: 0, # 'כ' + 4: 0, # 'ל' + 11: 0, # 'ם' + 6: 0, # 'מ' + 23: 0, # 'ן' + 12: 0, # 'נ' + 19: 0, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 0, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 1, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 54: { # 'n' + 50: 1, # 'a' + 60: 1, # 'c' + 61: 1, # 'd' + 42: 1, # 'e' + 53: 1, # 'i' + 56: 1, # 'l' + 54: 1, # 'n' + 49: 1, # 'o' + 51: 0, # 'r' + 43: 1, # 's' + 44: 2, # 't' + 63: 1, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 0, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 1, # 'ה' + 2: 0, # 'ו' + 24: 0, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 0, # 'י' + 25: 0, # 'ך' + 15: 0, # 'כ' + 4: 0, # 'ל' + 11: 0, # 'ם' + 6: 0, # 'מ' + 23: 0, # 'ן' + 12: 0, # 'נ' + 19: 0, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 0, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 2, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 49: { # 'o' + 50: 1, # 'a' + 60: 1, # 'c' + 61: 1, # 'd' + 42: 1, # 'e' + 53: 1, # 'i' + 56: 1, # 'l' + 54: 2, # 'n' + 49: 1, # 'o' + 51: 2, # 'r' + 43: 1, # 's' + 44: 1, # 't' + 63: 1, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 0, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 0, # 'ה' + 2: 0, # 'ו' + 24: 0, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 0, # 'י' + 25: 0, # 'ך' + 15: 0, # 'כ' + 4: 0, # 'ל' + 11: 0, # 'ם' + 6: 0, # 'מ' + 23: 0, # 'ן' + 12: 0, # 'נ' + 19: 0, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 0, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 1, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 51: { # 'r' + 50: 2, # 'a' + 60: 1, # 'c' + 61: 1, # 'd' + 42: 2, # 'e' + 53: 1, # 'i' + 56: 1, # 'l' + 54: 1, # 'n' + 49: 2, # 'o' + 51: 1, # 'r' + 43: 1, # 's' + 44: 1, # 't' + 63: 1, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 0, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 0, # 'ה' + 2: 0, # 'ו' + 24: 0, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 0, # 'י' + 25: 0, # 'ך' + 15: 0, # 'כ' + 4: 0, # 'ל' + 11: 0, # 'ם' + 6: 0, # 'מ' + 23: 0, # 'ן' + 12: 0, # 'נ' + 19: 0, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 0, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 2, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 43: { # 's' + 50: 1, # 'a' + 60: 1, # 'c' + 61: 0, # 'd' + 42: 2, # 'e' + 53: 1, # 'i' + 56: 1, # 'l' + 54: 1, # 'n' + 49: 1, # 'o' + 51: 1, # 'r' + 43: 1, # 's' + 44: 2, # 't' + 63: 1, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 0, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 0, # 'ה' + 2: 0, # 'ו' + 24: 0, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 0, # 'י' + 25: 0, # 'ך' + 15: 0, # 'כ' + 4: 0, # 'ל' + 11: 0, # 'ם' + 6: 0, # 'מ' + 23: 0, # 'ן' + 12: 0, # 'נ' + 19: 0, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 0, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 1, # '’' + 47: 0, # '“' + 46: 2, # '”' + 58: 0, # '†' + 40: 2, # '…' + }, + 44: { # 't' + 50: 1, # 'a' + 60: 1, # 'c' + 61: 0, # 'd' + 42: 2, # 'e' + 53: 2, # 'i' + 56: 1, # 'l' + 54: 0, # 'n' + 49: 1, # 'o' + 51: 1, # 'r' + 43: 1, # 's' + 44: 1, # 't' + 63: 1, # 'u' + 34: 1, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 0, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 0, # 'ה' + 2: 0, # 'ו' + 24: 0, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 0, # 'י' + 25: 0, # 'ך' + 15: 0, # 'כ' + 4: 0, # 'ל' + 11: 0, # 'ם' + 6: 0, # 'מ' + 23: 0, # 'ן' + 12: 0, # 'נ' + 19: 0, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 0, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 2, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 63: { # 'u' + 50: 1, # 'a' + 60: 1, # 'c' + 61: 1, # 'd' + 42: 1, # 'e' + 53: 1, # 'i' + 56: 1, # 'l' + 54: 1, # 'n' + 49: 0, # 'o' + 51: 1, # 'r' + 43: 2, # 's' + 44: 1, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 0, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 0, # 'ה' + 2: 0, # 'ו' + 24: 0, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 0, # 'י' + 25: 0, # 'ך' + 15: 0, # 'כ' + 4: 0, # 'ל' + 11: 0, # 'ם' + 6: 0, # 'מ' + 23: 0, # 'ן' + 12: 0, # 'נ' + 19: 0, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 0, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 1, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 34: { # '\xa0' + 50: 1, # 'a' + 60: 0, # 'c' + 61: 1, # 'd' + 42: 0, # 'e' + 53: 1, # 'i' + 56: 0, # 'l' + 54: 1, # 'n' + 49: 1, # 'o' + 51: 0, # 'r' + 43: 1, # 's' + 44: 1, # 't' + 63: 0, # 'u' + 34: 2, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 2, # 'א' + 8: 1, # 'ב' + 20: 1, # 'ג' + 16: 1, # 'ד' + 3: 1, # 'ה' + 2: 1, # 'ו' + 24: 1, # 'ז' + 14: 1, # 'ח' + 22: 1, # 'ט' + 1: 2, # 'י' + 25: 0, # 'ך' + 15: 1, # 'כ' + 4: 1, # 'ל' + 11: 0, # 'ם' + 6: 2, # 'מ' + 23: 0, # 'ן' + 12: 1, # 'נ' + 19: 1, # 'ס' + 13: 1, # 'ע' + 26: 0, # 'ף' + 18: 1, # 'פ' + 27: 0, # 'ץ' + 21: 1, # 'צ' + 17: 1, # 'ק' + 7: 1, # 'ר' + 10: 1, # 'ש' + 5: 1, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 55: { # '´' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 1, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 1, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 1, # 'ה' + 2: 1, # 'ו' + 24: 0, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 2, # 'י' + 25: 0, # 'ך' + 15: 0, # 'כ' + 4: 1, # 'ל' + 11: 0, # 'ם' + 6: 1, # 'מ' + 23: 1, # 'ן' + 12: 1, # 'נ' + 19: 1, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 0, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 1, # 'ר' + 10: 1, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 48: { # '¼' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 1, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 0, # 'ה' + 2: 1, # 'ו' + 24: 0, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 0, # 'י' + 25: 0, # 'ך' + 15: 1, # 'כ' + 4: 1, # 'ל' + 11: 0, # 'ם' + 6: 1, # 'מ' + 23: 0, # 'ן' + 12: 0, # 'נ' + 19: 0, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 0, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 39: { # '½' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 0, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 0, # 'ה' + 2: 0, # 'ו' + 24: 0, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 0, # 'י' + 25: 0, # 'ך' + 15: 1, # 'כ' + 4: 1, # 'ל' + 11: 0, # 'ם' + 6: 0, # 'מ' + 23: 0, # 'ן' + 12: 0, # 'נ' + 19: 0, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 0, # 'פ' + 27: 0, # 'ץ' + 21: 1, # 'צ' + 17: 1, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 57: { # '¾' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 0, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 0, # 'ה' + 2: 0, # 'ו' + 24: 0, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 0, # 'י' + 25: 0, # 'ך' + 15: 0, # 'כ' + 4: 0, # 'ל' + 11: 0, # 'ם' + 6: 0, # 'מ' + 23: 0, # 'ן' + 12: 0, # 'נ' + 19: 0, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 0, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 30: { # 'ְ' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 1, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 1, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 2, # 'א' + 8: 2, # 'ב' + 20: 2, # 'ג' + 16: 2, # 'ד' + 3: 2, # 'ה' + 2: 2, # 'ו' + 24: 2, # 'ז' + 14: 2, # 'ח' + 22: 2, # 'ט' + 1: 2, # 'י' + 25: 2, # 'ך' + 15: 2, # 'כ' + 4: 2, # 'ל' + 11: 1, # 'ם' + 6: 2, # 'מ' + 23: 0, # 'ן' + 12: 2, # 'נ' + 19: 2, # 'ס' + 13: 2, # 'ע' + 26: 0, # 'ף' + 18: 2, # 'פ' + 27: 0, # 'ץ' + 21: 2, # 'צ' + 17: 2, # 'ק' + 7: 2, # 'ר' + 10: 2, # 'ש' + 5: 2, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 59: { # 'ֱ' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 1, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 0, # 'א' + 8: 1, # 'ב' + 20: 1, # 'ג' + 16: 0, # 'ד' + 3: 0, # 'ה' + 2: 0, # 'ו' + 24: 1, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 1, # 'י' + 25: 0, # 'ך' + 15: 1, # 'כ' + 4: 2, # 'ל' + 11: 0, # 'ם' + 6: 2, # 'מ' + 23: 0, # 'ן' + 12: 1, # 'נ' + 19: 0, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 0, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 1, # 'ר' + 10: 1, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 41: { # 'ֲ' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 0, # 'א' + 8: 2, # 'ב' + 20: 1, # 'ג' + 16: 2, # 'ד' + 3: 1, # 'ה' + 2: 1, # 'ו' + 24: 1, # 'ז' + 14: 1, # 'ח' + 22: 1, # 'ט' + 1: 1, # 'י' + 25: 1, # 'ך' + 15: 1, # 'כ' + 4: 2, # 'ל' + 11: 0, # 'ם' + 6: 2, # 'מ' + 23: 0, # 'ן' + 12: 2, # 'נ' + 19: 1, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 1, # 'פ' + 27: 0, # 'ץ' + 21: 2, # 'צ' + 17: 1, # 'ק' + 7: 2, # 'ר' + 10: 2, # 'ש' + 5: 1, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 33: { # 'ִ' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 1, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 1, # 'ִ' + 37: 0, # 'ֵ' + 36: 1, # 'ֶ' + 31: 0, # 'ַ' + 29: 1, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 1, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 1, # 'א' + 8: 2, # 'ב' + 20: 2, # 'ג' + 16: 2, # 'ד' + 3: 1, # 'ה' + 2: 1, # 'ו' + 24: 2, # 'ז' + 14: 1, # 'ח' + 22: 1, # 'ט' + 1: 3, # 'י' + 25: 1, # 'ך' + 15: 2, # 'כ' + 4: 2, # 'ל' + 11: 2, # 'ם' + 6: 2, # 'מ' + 23: 2, # 'ן' + 12: 2, # 'נ' + 19: 2, # 'ס' + 13: 1, # 'ע' + 26: 0, # 'ף' + 18: 2, # 'פ' + 27: 1, # 'ץ' + 21: 2, # 'צ' + 17: 2, # 'ק' + 7: 2, # 'ר' + 10: 2, # 'ש' + 5: 2, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 37: { # 'ֵ' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 1, # 'ֶ' + 31: 1, # 'ַ' + 29: 1, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 2, # 'א' + 8: 2, # 'ב' + 20: 1, # 'ג' + 16: 2, # 'ד' + 3: 2, # 'ה' + 2: 1, # 'ו' + 24: 1, # 'ז' + 14: 2, # 'ח' + 22: 1, # 'ט' + 1: 3, # 'י' + 25: 2, # 'ך' + 15: 1, # 'כ' + 4: 2, # 'ל' + 11: 2, # 'ם' + 6: 1, # 'מ' + 23: 2, # 'ן' + 12: 2, # 'נ' + 19: 1, # 'ס' + 13: 2, # 'ע' + 26: 1, # 'ף' + 18: 1, # 'פ' + 27: 1, # 'ץ' + 21: 1, # 'צ' + 17: 1, # 'ק' + 7: 2, # 'ר' + 10: 2, # 'ש' + 5: 2, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 36: { # 'ֶ' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 1, # 'ֶ' + 31: 1, # 'ַ' + 29: 1, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 2, # 'א' + 8: 2, # 'ב' + 20: 1, # 'ג' + 16: 2, # 'ד' + 3: 2, # 'ה' + 2: 1, # 'ו' + 24: 1, # 'ז' + 14: 2, # 'ח' + 22: 1, # 'ט' + 1: 2, # 'י' + 25: 2, # 'ך' + 15: 1, # 'כ' + 4: 2, # 'ל' + 11: 2, # 'ם' + 6: 2, # 'מ' + 23: 2, # 'ן' + 12: 2, # 'נ' + 19: 2, # 'ס' + 13: 1, # 'ע' + 26: 1, # 'ף' + 18: 1, # 'פ' + 27: 2, # 'ץ' + 21: 1, # 'צ' + 17: 1, # 'ק' + 7: 2, # 'ר' + 10: 2, # 'ש' + 5: 2, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 31: { # 'ַ' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 1, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 1, # 'ֶ' + 31: 0, # 'ַ' + 29: 2, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 2, # 'א' + 8: 2, # 'ב' + 20: 2, # 'ג' + 16: 2, # 'ד' + 3: 2, # 'ה' + 2: 1, # 'ו' + 24: 2, # 'ז' + 14: 2, # 'ח' + 22: 2, # 'ט' + 1: 3, # 'י' + 25: 1, # 'ך' + 15: 2, # 'כ' + 4: 2, # 'ל' + 11: 2, # 'ם' + 6: 2, # 'מ' + 23: 2, # 'ן' + 12: 2, # 'נ' + 19: 2, # 'ס' + 13: 2, # 'ע' + 26: 2, # 'ף' + 18: 2, # 'פ' + 27: 1, # 'ץ' + 21: 2, # 'צ' + 17: 2, # 'ק' + 7: 2, # 'ר' + 10: 2, # 'ש' + 5: 2, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 29: { # 'ָ' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 1, # 'ַ' + 29: 2, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 1, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 2, # 'א' + 8: 2, # 'ב' + 20: 2, # 'ג' + 16: 2, # 'ד' + 3: 3, # 'ה' + 2: 2, # 'ו' + 24: 2, # 'ז' + 14: 2, # 'ח' + 22: 1, # 'ט' + 1: 2, # 'י' + 25: 2, # 'ך' + 15: 2, # 'כ' + 4: 2, # 'ל' + 11: 2, # 'ם' + 6: 2, # 'מ' + 23: 2, # 'ן' + 12: 2, # 'נ' + 19: 1, # 'ס' + 13: 2, # 'ע' + 26: 1, # 'ף' + 18: 2, # 'פ' + 27: 1, # 'ץ' + 21: 2, # 'צ' + 17: 2, # 'ק' + 7: 2, # 'ר' + 10: 2, # 'ש' + 5: 2, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 35: { # 'ֹ' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 1, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 2, # 'א' + 8: 2, # 'ב' + 20: 1, # 'ג' + 16: 2, # 'ד' + 3: 2, # 'ה' + 2: 1, # 'ו' + 24: 1, # 'ז' + 14: 1, # 'ח' + 22: 1, # 'ט' + 1: 1, # 'י' + 25: 1, # 'ך' + 15: 2, # 'כ' + 4: 2, # 'ל' + 11: 2, # 'ם' + 6: 2, # 'מ' + 23: 2, # 'ן' + 12: 2, # 'נ' + 19: 2, # 'ס' + 13: 2, # 'ע' + 26: 1, # 'ף' + 18: 2, # 'פ' + 27: 1, # 'ץ' + 21: 2, # 'צ' + 17: 2, # 'ק' + 7: 2, # 'ר' + 10: 2, # 'ש' + 5: 2, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 62: { # 'ֻ' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 0, # 'א' + 8: 1, # 'ב' + 20: 1, # 'ג' + 16: 1, # 'ד' + 3: 1, # 'ה' + 2: 1, # 'ו' + 24: 1, # 'ז' + 14: 1, # 'ח' + 22: 0, # 'ט' + 1: 1, # 'י' + 25: 0, # 'ך' + 15: 1, # 'כ' + 4: 2, # 'ל' + 11: 1, # 'ם' + 6: 1, # 'מ' + 23: 1, # 'ן' + 12: 1, # 'נ' + 19: 1, # 'ס' + 13: 1, # 'ע' + 26: 0, # 'ף' + 18: 1, # 'פ' + 27: 0, # 'ץ' + 21: 1, # 'צ' + 17: 1, # 'ק' + 7: 1, # 'ר' + 10: 1, # 'ש' + 5: 1, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 28: { # 'ּ' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 3, # 'ְ' + 59: 0, # 'ֱ' + 41: 1, # 'ֲ' + 33: 3, # 'ִ' + 37: 2, # 'ֵ' + 36: 2, # 'ֶ' + 31: 3, # 'ַ' + 29: 3, # 'ָ' + 35: 2, # 'ֹ' + 62: 1, # 'ֻ' + 28: 0, # 'ּ' + 38: 2, # 'ׁ' + 45: 1, # 'ׂ' + 9: 2, # 'א' + 8: 2, # 'ב' + 20: 1, # 'ג' + 16: 2, # 'ד' + 3: 1, # 'ה' + 2: 2, # 'ו' + 24: 1, # 'ז' + 14: 1, # 'ח' + 22: 1, # 'ט' + 1: 2, # 'י' + 25: 2, # 'ך' + 15: 2, # 'כ' + 4: 2, # 'ל' + 11: 1, # 'ם' + 6: 2, # 'מ' + 23: 1, # 'ן' + 12: 2, # 'נ' + 19: 1, # 'ס' + 13: 2, # 'ע' + 26: 1, # 'ף' + 18: 1, # 'פ' + 27: 1, # 'ץ' + 21: 1, # 'צ' + 17: 1, # 'ק' + 7: 2, # 'ר' + 10: 2, # 'ש' + 5: 2, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 38: { # 'ׁ' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 2, # 'ִ' + 37: 2, # 'ֵ' + 36: 2, # 'ֶ' + 31: 2, # 'ַ' + 29: 2, # 'ָ' + 35: 1, # 'ֹ' + 62: 1, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 0, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 0, # 'ה' + 2: 2, # 'ו' + 24: 0, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 1, # 'י' + 25: 0, # 'ך' + 15: 0, # 'כ' + 4: 0, # 'ל' + 11: 0, # 'ם' + 6: 0, # 'מ' + 23: 0, # 'ן' + 12: 0, # 'נ' + 19: 0, # 'ס' + 13: 1, # 'ע' + 26: 0, # 'ף' + 18: 0, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 45: { # 'ׂ' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 2, # 'ִ' + 37: 1, # 'ֵ' + 36: 2, # 'ֶ' + 31: 1, # 'ַ' + 29: 2, # 'ָ' + 35: 1, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 1, # 'א' + 8: 0, # 'ב' + 20: 1, # 'ג' + 16: 0, # 'ד' + 3: 1, # 'ה' + 2: 2, # 'ו' + 24: 0, # 'ז' + 14: 1, # 'ח' + 22: 0, # 'ט' + 1: 1, # 'י' + 25: 0, # 'ך' + 15: 0, # 'כ' + 4: 0, # 'ל' + 11: 1, # 'ם' + 6: 1, # 'מ' + 23: 0, # 'ן' + 12: 1, # 'נ' + 19: 0, # 'ס' + 13: 1, # 'ע' + 26: 0, # 'ף' + 18: 1, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 1, # 'ר' + 10: 0, # 'ש' + 5: 1, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 9: { # 'א' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 1, # '´' + 48: 1, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 2, # 'ֱ' + 41: 2, # 'ֲ' + 33: 2, # 'ִ' + 37: 2, # 'ֵ' + 36: 2, # 'ֶ' + 31: 2, # 'ַ' + 29: 2, # 'ָ' + 35: 2, # 'ֹ' + 62: 1, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 2, # 'א' + 8: 3, # 'ב' + 20: 3, # 'ג' + 16: 3, # 'ד' + 3: 3, # 'ה' + 2: 3, # 'ו' + 24: 3, # 'ז' + 14: 3, # 'ח' + 22: 3, # 'ט' + 1: 3, # 'י' + 25: 3, # 'ך' + 15: 3, # 'כ' + 4: 3, # 'ל' + 11: 3, # 'ם' + 6: 3, # 'מ' + 23: 3, # 'ן' + 12: 3, # 'נ' + 19: 3, # 'ס' + 13: 2, # 'ע' + 26: 3, # 'ף' + 18: 3, # 'פ' + 27: 1, # 'ץ' + 21: 3, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 8: { # 'ב' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 1, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 1, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 2, # 'ִ' + 37: 2, # 'ֵ' + 36: 2, # 'ֶ' + 31: 2, # 'ַ' + 29: 2, # 'ָ' + 35: 2, # 'ֹ' + 62: 1, # 'ֻ' + 28: 3, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 3, # 'א' + 8: 3, # 'ב' + 20: 3, # 'ג' + 16: 3, # 'ד' + 3: 3, # 'ה' + 2: 3, # 'ו' + 24: 3, # 'ז' + 14: 3, # 'ח' + 22: 3, # 'ט' + 1: 3, # 'י' + 25: 2, # 'ך' + 15: 3, # 'כ' + 4: 3, # 'ל' + 11: 2, # 'ם' + 6: 3, # 'מ' + 23: 3, # 'ן' + 12: 3, # 'נ' + 19: 3, # 'ס' + 13: 3, # 'ע' + 26: 1, # 'ף' + 18: 3, # 'פ' + 27: 2, # 'ץ' + 21: 3, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 1, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 20: { # 'ג' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 2, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 1, # 'ִ' + 37: 1, # 'ֵ' + 36: 1, # 'ֶ' + 31: 2, # 'ַ' + 29: 2, # 'ָ' + 35: 1, # 'ֹ' + 62: 0, # 'ֻ' + 28: 2, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 2, # 'א' + 8: 3, # 'ב' + 20: 2, # 'ג' + 16: 3, # 'ד' + 3: 3, # 'ה' + 2: 3, # 'ו' + 24: 3, # 'ז' + 14: 2, # 'ח' + 22: 2, # 'ט' + 1: 3, # 'י' + 25: 1, # 'ך' + 15: 1, # 'כ' + 4: 3, # 'ל' + 11: 3, # 'ם' + 6: 3, # 'מ' + 23: 3, # 'ן' + 12: 3, # 'נ' + 19: 2, # 'ס' + 13: 3, # 'ע' + 26: 2, # 'ף' + 18: 2, # 'פ' + 27: 1, # 'ץ' + 21: 1, # 'צ' + 17: 1, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 0, # '–' + 52: 1, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 16: { # 'ד' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 2, # 'ִ' + 37: 2, # 'ֵ' + 36: 2, # 'ֶ' + 31: 2, # 'ַ' + 29: 2, # 'ָ' + 35: 2, # 'ֹ' + 62: 1, # 'ֻ' + 28: 2, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 3, # 'א' + 8: 3, # 'ב' + 20: 3, # 'ג' + 16: 3, # 'ד' + 3: 3, # 'ה' + 2: 3, # 'ו' + 24: 1, # 'ז' + 14: 2, # 'ח' + 22: 2, # 'ט' + 1: 3, # 'י' + 25: 2, # 'ך' + 15: 2, # 'כ' + 4: 3, # 'ל' + 11: 3, # 'ם' + 6: 3, # 'מ' + 23: 2, # 'ן' + 12: 3, # 'נ' + 19: 2, # 'ס' + 13: 3, # 'ע' + 26: 2, # 'ף' + 18: 3, # 'פ' + 27: 0, # 'ץ' + 21: 2, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 3: { # 'ה' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 1, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 0, # '´' + 48: 1, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 1, # 'ְ' + 59: 1, # 'ֱ' + 41: 2, # 'ֲ' + 33: 2, # 'ִ' + 37: 2, # 'ֵ' + 36: 2, # 'ֶ' + 31: 3, # 'ַ' + 29: 2, # 'ָ' + 35: 1, # 'ֹ' + 62: 1, # 'ֻ' + 28: 2, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 3, # 'א' + 8: 3, # 'ב' + 20: 3, # 'ג' + 16: 3, # 'ד' + 3: 3, # 'ה' + 2: 3, # 'ו' + 24: 3, # 'ז' + 14: 3, # 'ח' + 22: 3, # 'ט' + 1: 3, # 'י' + 25: 1, # 'ך' + 15: 3, # 'כ' + 4: 3, # 'ל' + 11: 3, # 'ם' + 6: 3, # 'מ' + 23: 3, # 'ן' + 12: 3, # 'נ' + 19: 3, # 'ס' + 13: 3, # 'ע' + 26: 0, # 'ף' + 18: 3, # 'פ' + 27: 1, # 'ץ' + 21: 3, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 1, # '–' + 52: 1, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 2, # '…' + }, + 2: { # 'ו' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 1, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 1, # '´' + 48: 1, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 2, # 'ִ' + 37: 1, # 'ֵ' + 36: 1, # 'ֶ' + 31: 2, # 'ַ' + 29: 2, # 'ָ' + 35: 3, # 'ֹ' + 62: 0, # 'ֻ' + 28: 3, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 3, # 'א' + 8: 3, # 'ב' + 20: 3, # 'ג' + 16: 3, # 'ד' + 3: 3, # 'ה' + 2: 3, # 'ו' + 24: 3, # 'ז' + 14: 3, # 'ח' + 22: 3, # 'ט' + 1: 3, # 'י' + 25: 3, # 'ך' + 15: 3, # 'כ' + 4: 3, # 'ל' + 11: 3, # 'ם' + 6: 3, # 'מ' + 23: 3, # 'ן' + 12: 3, # 'נ' + 19: 3, # 'ס' + 13: 3, # 'ע' + 26: 3, # 'ף' + 18: 3, # 'פ' + 27: 3, # 'ץ' + 21: 3, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 1, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 2, # '…' + }, + 24: { # 'ז' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 1, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'ְ' + 59: 0, # 'ֱ' + 41: 1, # 'ֲ' + 33: 1, # 'ִ' + 37: 2, # 'ֵ' + 36: 2, # 'ֶ' + 31: 2, # 'ַ' + 29: 2, # 'ָ' + 35: 1, # 'ֹ' + 62: 1, # 'ֻ' + 28: 2, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 3, # 'א' + 8: 2, # 'ב' + 20: 2, # 'ג' + 16: 2, # 'ד' + 3: 3, # 'ה' + 2: 3, # 'ו' + 24: 2, # 'ז' + 14: 2, # 'ח' + 22: 1, # 'ט' + 1: 3, # 'י' + 25: 1, # 'ך' + 15: 3, # 'כ' + 4: 3, # 'ל' + 11: 2, # 'ם' + 6: 3, # 'מ' + 23: 2, # 'ן' + 12: 2, # 'נ' + 19: 1, # 'ס' + 13: 2, # 'ע' + 26: 1, # 'ף' + 18: 1, # 'פ' + 27: 0, # 'ץ' + 21: 2, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 1, # 'ש' + 5: 2, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 14: { # 'ח' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 1, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'ְ' + 59: 1, # 'ֱ' + 41: 2, # 'ֲ' + 33: 2, # 'ִ' + 37: 2, # 'ֵ' + 36: 2, # 'ֶ' + 31: 2, # 'ַ' + 29: 2, # 'ָ' + 35: 2, # 'ֹ' + 62: 1, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 2, # 'א' + 8: 3, # 'ב' + 20: 2, # 'ג' + 16: 3, # 'ד' + 3: 3, # 'ה' + 2: 3, # 'ו' + 24: 3, # 'ז' + 14: 2, # 'ח' + 22: 2, # 'ט' + 1: 3, # 'י' + 25: 1, # 'ך' + 15: 2, # 'כ' + 4: 3, # 'ל' + 11: 3, # 'ם' + 6: 3, # 'מ' + 23: 2, # 'ן' + 12: 3, # 'נ' + 19: 3, # 'ס' + 13: 1, # 'ע' + 26: 2, # 'ף' + 18: 2, # 'פ' + 27: 2, # 'ץ' + 21: 3, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 0, # '–' + 52: 1, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 22: { # 'ט' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 2, # 'ִ' + 37: 1, # 'ֵ' + 36: 1, # 'ֶ' + 31: 2, # 'ַ' + 29: 1, # 'ָ' + 35: 1, # 'ֹ' + 62: 1, # 'ֻ' + 28: 1, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 3, # 'א' + 8: 3, # 'ב' + 20: 3, # 'ג' + 16: 1, # 'ד' + 3: 3, # 'ה' + 2: 3, # 'ו' + 24: 2, # 'ז' + 14: 3, # 'ח' + 22: 2, # 'ט' + 1: 3, # 'י' + 25: 1, # 'ך' + 15: 2, # 'כ' + 4: 3, # 'ל' + 11: 2, # 'ם' + 6: 2, # 'מ' + 23: 2, # 'ן' + 12: 3, # 'נ' + 19: 2, # 'ס' + 13: 3, # 'ע' + 26: 2, # 'ף' + 18: 3, # 'פ' + 27: 1, # 'ץ' + 21: 2, # 'צ' + 17: 2, # 'ק' + 7: 3, # 'ר' + 10: 2, # 'ש' + 5: 3, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 1: { # 'י' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 1, # '´' + 48: 1, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 2, # 'ִ' + 37: 2, # 'ֵ' + 36: 1, # 'ֶ' + 31: 2, # 'ַ' + 29: 2, # 'ָ' + 35: 2, # 'ֹ' + 62: 1, # 'ֻ' + 28: 2, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 3, # 'א' + 8: 3, # 'ב' + 20: 3, # 'ג' + 16: 3, # 'ד' + 3: 3, # 'ה' + 2: 3, # 'ו' + 24: 3, # 'ז' + 14: 3, # 'ח' + 22: 3, # 'ט' + 1: 3, # 'י' + 25: 3, # 'ך' + 15: 3, # 'כ' + 4: 3, # 'ל' + 11: 3, # 'ם' + 6: 3, # 'מ' + 23: 3, # 'ן' + 12: 3, # 'נ' + 19: 3, # 'ס' + 13: 3, # 'ע' + 26: 3, # 'ף' + 18: 3, # 'פ' + 27: 3, # 'ץ' + 21: 3, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 1, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 2, # '…' + }, + 25: { # 'ך' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 2, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 1, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 1, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 1, # 'ה' + 2: 0, # 'ו' + 24: 0, # 'ז' + 14: 1, # 'ח' + 22: 0, # 'ט' + 1: 0, # 'י' + 25: 0, # 'ך' + 15: 0, # 'כ' + 4: 1, # 'ל' + 11: 0, # 'ם' + 6: 1, # 'מ' + 23: 0, # 'ן' + 12: 0, # 'נ' + 19: 0, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 0, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 1, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 15: { # 'כ' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 2, # 'ִ' + 37: 2, # 'ֵ' + 36: 2, # 'ֶ' + 31: 2, # 'ַ' + 29: 2, # 'ָ' + 35: 1, # 'ֹ' + 62: 1, # 'ֻ' + 28: 3, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 3, # 'א' + 8: 3, # 'ב' + 20: 2, # 'ג' + 16: 3, # 'ד' + 3: 3, # 'ה' + 2: 3, # 'ו' + 24: 3, # 'ז' + 14: 3, # 'ח' + 22: 2, # 'ט' + 1: 3, # 'י' + 25: 3, # 'ך' + 15: 3, # 'כ' + 4: 3, # 'ל' + 11: 3, # 'ם' + 6: 3, # 'מ' + 23: 3, # 'ן' + 12: 3, # 'נ' + 19: 3, # 'ס' + 13: 2, # 'ע' + 26: 3, # 'ף' + 18: 3, # 'פ' + 27: 1, # 'ץ' + 21: 2, # 'צ' + 17: 2, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 4: { # 'ל' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 1, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 3, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 2, # 'ִ' + 37: 2, # 'ֵ' + 36: 2, # 'ֶ' + 31: 2, # 'ַ' + 29: 2, # 'ָ' + 35: 2, # 'ֹ' + 62: 1, # 'ֻ' + 28: 2, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 3, # 'א' + 8: 3, # 'ב' + 20: 3, # 'ג' + 16: 3, # 'ד' + 3: 3, # 'ה' + 2: 3, # 'ו' + 24: 3, # 'ז' + 14: 3, # 'ח' + 22: 3, # 'ט' + 1: 3, # 'י' + 25: 3, # 'ך' + 15: 3, # 'כ' + 4: 3, # 'ל' + 11: 3, # 'ם' + 6: 3, # 'מ' + 23: 2, # 'ן' + 12: 3, # 'נ' + 19: 3, # 'ס' + 13: 3, # 'ע' + 26: 2, # 'ף' + 18: 3, # 'פ' + 27: 2, # 'ץ' + 21: 3, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 1, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 11: { # 'ם' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 1, # 'א' + 8: 1, # 'ב' + 20: 1, # 'ג' + 16: 0, # 'ד' + 3: 1, # 'ה' + 2: 1, # 'ו' + 24: 1, # 'ז' + 14: 1, # 'ח' + 22: 0, # 'ט' + 1: 1, # 'י' + 25: 0, # 'ך' + 15: 1, # 'כ' + 4: 1, # 'ל' + 11: 1, # 'ם' + 6: 1, # 'מ' + 23: 0, # 'ן' + 12: 1, # 'נ' + 19: 0, # 'ס' + 13: 1, # 'ע' + 26: 0, # 'ף' + 18: 1, # 'פ' + 27: 1, # 'ץ' + 21: 1, # 'צ' + 17: 1, # 'ק' + 7: 1, # 'ר' + 10: 1, # 'ש' + 5: 1, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 2, # '…' + }, + 6: { # 'מ' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 1, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 2, # 'ִ' + 37: 2, # 'ֵ' + 36: 2, # 'ֶ' + 31: 2, # 'ַ' + 29: 2, # 'ָ' + 35: 2, # 'ֹ' + 62: 1, # 'ֻ' + 28: 2, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 3, # 'א' + 8: 3, # 'ב' + 20: 3, # 'ג' + 16: 3, # 'ד' + 3: 3, # 'ה' + 2: 3, # 'ו' + 24: 3, # 'ז' + 14: 3, # 'ח' + 22: 3, # 'ט' + 1: 3, # 'י' + 25: 2, # 'ך' + 15: 3, # 'כ' + 4: 3, # 'ל' + 11: 3, # 'ם' + 6: 3, # 'מ' + 23: 3, # 'ן' + 12: 3, # 'נ' + 19: 3, # 'ס' + 13: 3, # 'ע' + 26: 0, # 'ף' + 18: 3, # 'פ' + 27: 2, # 'ץ' + 21: 3, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 23: { # 'ן' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 0, # '´' + 48: 1, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 1, # 'א' + 8: 1, # 'ב' + 20: 1, # 'ג' + 16: 1, # 'ד' + 3: 1, # 'ה' + 2: 1, # 'ו' + 24: 0, # 'ז' + 14: 1, # 'ח' + 22: 1, # 'ט' + 1: 1, # 'י' + 25: 0, # 'ך' + 15: 1, # 'כ' + 4: 1, # 'ל' + 11: 1, # 'ם' + 6: 1, # 'מ' + 23: 0, # 'ן' + 12: 1, # 'נ' + 19: 1, # 'ס' + 13: 1, # 'ע' + 26: 1, # 'ף' + 18: 1, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 1, # 'ק' + 7: 1, # 'ר' + 10: 1, # 'ש' + 5: 1, # 'ת' + 32: 1, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 2, # '…' + }, + 12: { # 'נ' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 2, # 'ִ' + 37: 2, # 'ֵ' + 36: 2, # 'ֶ' + 31: 2, # 'ַ' + 29: 2, # 'ָ' + 35: 1, # 'ֹ' + 62: 1, # 'ֻ' + 28: 2, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 3, # 'א' + 8: 3, # 'ב' + 20: 3, # 'ג' + 16: 3, # 'ד' + 3: 3, # 'ה' + 2: 3, # 'ו' + 24: 3, # 'ז' + 14: 3, # 'ח' + 22: 3, # 'ט' + 1: 3, # 'י' + 25: 2, # 'ך' + 15: 3, # 'כ' + 4: 3, # 'ל' + 11: 3, # 'ם' + 6: 3, # 'מ' + 23: 3, # 'ן' + 12: 3, # 'נ' + 19: 3, # 'ס' + 13: 3, # 'ע' + 26: 2, # 'ף' + 18: 3, # 'פ' + 27: 2, # 'ץ' + 21: 3, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 19: { # 'ס' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 1, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 2, # 'ִ' + 37: 1, # 'ֵ' + 36: 2, # 'ֶ' + 31: 2, # 'ַ' + 29: 1, # 'ָ' + 35: 1, # 'ֹ' + 62: 2, # 'ֻ' + 28: 2, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 2, # 'א' + 8: 3, # 'ב' + 20: 3, # 'ג' + 16: 3, # 'ד' + 3: 3, # 'ה' + 2: 3, # 'ו' + 24: 1, # 'ז' + 14: 3, # 'ח' + 22: 3, # 'ט' + 1: 3, # 'י' + 25: 2, # 'ך' + 15: 3, # 'כ' + 4: 3, # 'ל' + 11: 2, # 'ם' + 6: 3, # 'מ' + 23: 2, # 'ן' + 12: 3, # 'נ' + 19: 2, # 'ס' + 13: 3, # 'ע' + 26: 3, # 'ף' + 18: 3, # 'פ' + 27: 0, # 'ץ' + 21: 2, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 1, # 'ש' + 5: 3, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 13: { # 'ע' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 1, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 1, # 'ְ' + 59: 1, # 'ֱ' + 41: 2, # 'ֲ' + 33: 2, # 'ִ' + 37: 2, # 'ֵ' + 36: 2, # 'ֶ' + 31: 2, # 'ַ' + 29: 2, # 'ָ' + 35: 2, # 'ֹ' + 62: 1, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 2, # 'א' + 8: 3, # 'ב' + 20: 3, # 'ג' + 16: 3, # 'ד' + 3: 3, # 'ה' + 2: 3, # 'ו' + 24: 3, # 'ז' + 14: 1, # 'ח' + 22: 3, # 'ט' + 1: 3, # 'י' + 25: 2, # 'ך' + 15: 2, # 'כ' + 4: 3, # 'ל' + 11: 3, # 'ם' + 6: 3, # 'מ' + 23: 2, # 'ן' + 12: 3, # 'נ' + 19: 3, # 'ס' + 13: 2, # 'ע' + 26: 1, # 'ף' + 18: 2, # 'פ' + 27: 2, # 'ץ' + 21: 3, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 26: { # 'ף' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 1, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 0, # 'ה' + 2: 1, # 'ו' + 24: 0, # 'ז' + 14: 1, # 'ח' + 22: 0, # 'ט' + 1: 0, # 'י' + 25: 0, # 'ך' + 15: 1, # 'כ' + 4: 1, # 'ל' + 11: 0, # 'ם' + 6: 1, # 'מ' + 23: 0, # 'ן' + 12: 0, # 'נ' + 19: 1, # 'ס' + 13: 0, # 'ע' + 26: 1, # 'ף' + 18: 1, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 1, # 'ק' + 7: 1, # 'ר' + 10: 1, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 18: { # 'פ' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 1, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 2, # 'ִ' + 37: 1, # 'ֵ' + 36: 2, # 'ֶ' + 31: 1, # 'ַ' + 29: 2, # 'ָ' + 35: 1, # 'ֹ' + 62: 1, # 'ֻ' + 28: 2, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 3, # 'א' + 8: 2, # 'ב' + 20: 3, # 'ג' + 16: 2, # 'ד' + 3: 3, # 'ה' + 2: 3, # 'ו' + 24: 2, # 'ז' + 14: 3, # 'ח' + 22: 3, # 'ט' + 1: 3, # 'י' + 25: 2, # 'ך' + 15: 3, # 'כ' + 4: 3, # 'ל' + 11: 2, # 'ם' + 6: 2, # 'מ' + 23: 3, # 'ן' + 12: 3, # 'נ' + 19: 3, # 'ס' + 13: 3, # 'ע' + 26: 2, # 'ף' + 18: 2, # 'פ' + 27: 2, # 'ץ' + 21: 3, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 27: { # 'ץ' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 1, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 1, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 0, # 'ה' + 2: 0, # 'ו' + 24: 0, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 0, # 'י' + 25: 0, # 'ך' + 15: 0, # 'כ' + 4: 1, # 'ל' + 11: 0, # 'ם' + 6: 0, # 'מ' + 23: 0, # 'ן' + 12: 0, # 'נ' + 19: 1, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 0, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 1, # 'ר' + 10: 0, # 'ש' + 5: 1, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 21: { # 'צ' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 1, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 2, # 'ִ' + 37: 2, # 'ֵ' + 36: 1, # 'ֶ' + 31: 2, # 'ַ' + 29: 2, # 'ָ' + 35: 1, # 'ֹ' + 62: 1, # 'ֻ' + 28: 2, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 3, # 'א' + 8: 3, # 'ב' + 20: 2, # 'ג' + 16: 3, # 'ד' + 3: 3, # 'ה' + 2: 3, # 'ו' + 24: 1, # 'ז' + 14: 3, # 'ח' + 22: 2, # 'ט' + 1: 3, # 'י' + 25: 1, # 'ך' + 15: 1, # 'כ' + 4: 3, # 'ל' + 11: 2, # 'ם' + 6: 3, # 'מ' + 23: 2, # 'ן' + 12: 3, # 'נ' + 19: 1, # 'ס' + 13: 3, # 'ע' + 26: 2, # 'ף' + 18: 3, # 'פ' + 27: 2, # 'ץ' + 21: 2, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 0, # 'ש' + 5: 3, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 17: { # 'ק' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 1, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 2, # 'ִ' + 37: 2, # 'ֵ' + 36: 1, # 'ֶ' + 31: 2, # 'ַ' + 29: 2, # 'ָ' + 35: 2, # 'ֹ' + 62: 1, # 'ֻ' + 28: 2, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 3, # 'א' + 8: 3, # 'ב' + 20: 2, # 'ג' + 16: 3, # 'ד' + 3: 3, # 'ה' + 2: 3, # 'ו' + 24: 2, # 'ז' + 14: 3, # 'ח' + 22: 3, # 'ט' + 1: 3, # 'י' + 25: 1, # 'ך' + 15: 1, # 'כ' + 4: 3, # 'ל' + 11: 2, # 'ם' + 6: 3, # 'מ' + 23: 2, # 'ן' + 12: 3, # 'נ' + 19: 3, # 'ס' + 13: 3, # 'ע' + 26: 2, # 'ף' + 18: 3, # 'פ' + 27: 2, # 'ץ' + 21: 3, # 'צ' + 17: 2, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 0, # '–' + 52: 1, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 7: { # 'ר' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 2, # '´' + 48: 1, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 2, # 'ְ' + 59: 0, # 'ֱ' + 41: 1, # 'ֲ' + 33: 2, # 'ִ' + 37: 2, # 'ֵ' + 36: 2, # 'ֶ' + 31: 2, # 'ַ' + 29: 2, # 'ָ' + 35: 2, # 'ֹ' + 62: 1, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 3, # 'א' + 8: 3, # 'ב' + 20: 3, # 'ג' + 16: 3, # 'ד' + 3: 3, # 'ה' + 2: 3, # 'ו' + 24: 3, # 'ז' + 14: 3, # 'ח' + 22: 3, # 'ט' + 1: 3, # 'י' + 25: 3, # 'ך' + 15: 3, # 'כ' + 4: 3, # 'ל' + 11: 3, # 'ם' + 6: 3, # 'מ' + 23: 3, # 'ן' + 12: 3, # 'נ' + 19: 3, # 'ס' + 13: 3, # 'ע' + 26: 2, # 'ף' + 18: 3, # 'פ' + 27: 3, # 'ץ' + 21: 3, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 2, # '…' + }, + 10: { # 'ש' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 1, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 1, # 'ִ' + 37: 1, # 'ֵ' + 36: 1, # 'ֶ' + 31: 1, # 'ַ' + 29: 1, # 'ָ' + 35: 1, # 'ֹ' + 62: 1, # 'ֻ' + 28: 2, # 'ּ' + 38: 3, # 'ׁ' + 45: 2, # 'ׂ' + 9: 3, # 'א' + 8: 3, # 'ב' + 20: 3, # 'ג' + 16: 3, # 'ד' + 3: 3, # 'ה' + 2: 3, # 'ו' + 24: 2, # 'ז' + 14: 3, # 'ח' + 22: 3, # 'ט' + 1: 3, # 'י' + 25: 3, # 'ך' + 15: 3, # 'כ' + 4: 3, # 'ל' + 11: 3, # 'ם' + 6: 3, # 'מ' + 23: 2, # 'ן' + 12: 3, # 'נ' + 19: 2, # 'ס' + 13: 3, # 'ע' + 26: 2, # 'ף' + 18: 3, # 'פ' + 27: 1, # 'ץ' + 21: 2, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 1, # '…' + }, + 5: { # 'ת' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 1, # '\xa0' + 55: 0, # '´' + 48: 1, # '¼' + 39: 1, # '½' + 57: 0, # '¾' + 30: 2, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 2, # 'ִ' + 37: 2, # 'ֵ' + 36: 2, # 'ֶ' + 31: 2, # 'ַ' + 29: 2, # 'ָ' + 35: 1, # 'ֹ' + 62: 1, # 'ֻ' + 28: 2, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 3, # 'א' + 8: 3, # 'ב' + 20: 3, # 'ג' + 16: 2, # 'ד' + 3: 3, # 'ה' + 2: 3, # 'ו' + 24: 2, # 'ז' + 14: 3, # 'ח' + 22: 2, # 'ט' + 1: 3, # 'י' + 25: 2, # 'ך' + 15: 3, # 'כ' + 4: 3, # 'ל' + 11: 3, # 'ם' + 6: 3, # 'מ' + 23: 3, # 'ן' + 12: 3, # 'נ' + 19: 2, # 'ס' + 13: 3, # 'ע' + 26: 2, # 'ף' + 18: 3, # 'פ' + 27: 1, # 'ץ' + 21: 2, # 'צ' + 17: 3, # 'ק' + 7: 3, # 'ר' + 10: 3, # 'ש' + 5: 3, # 'ת' + 32: 1, # '–' + 52: 1, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 2, # '…' + }, + 32: { # '–' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 1, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 1, # 'א' + 8: 1, # 'ב' + 20: 1, # 'ג' + 16: 1, # 'ד' + 3: 1, # 'ה' + 2: 1, # 'ו' + 24: 0, # 'ז' + 14: 1, # 'ח' + 22: 0, # 'ט' + 1: 1, # 'י' + 25: 0, # 'ך' + 15: 1, # 'כ' + 4: 1, # 'ל' + 11: 0, # 'ם' + 6: 1, # 'מ' + 23: 0, # 'ן' + 12: 0, # 'נ' + 19: 1, # 'ס' + 13: 1, # 'ע' + 26: 0, # 'ף' + 18: 1, # 'פ' + 27: 0, # 'ץ' + 21: 1, # 'צ' + 17: 0, # 'ק' + 7: 1, # 'ר' + 10: 1, # 'ש' + 5: 1, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 52: { # '’' + 50: 1, # 'a' + 60: 0, # 'c' + 61: 1, # 'd' + 42: 1, # 'e' + 53: 1, # 'i' + 56: 1, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 1, # 'r' + 43: 2, # 's' + 44: 2, # 't' + 63: 1, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 0, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 0, # 'ה' + 2: 1, # 'ו' + 24: 0, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 0, # 'י' + 25: 0, # 'ך' + 15: 0, # 'כ' + 4: 0, # 'ל' + 11: 0, # 'ם' + 6: 1, # 'מ' + 23: 0, # 'ן' + 12: 0, # 'נ' + 19: 0, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 0, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 1, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 47: { # '“' + 50: 1, # 'a' + 60: 1, # 'c' + 61: 1, # 'd' + 42: 1, # 'e' + 53: 1, # 'i' + 56: 1, # 'l' + 54: 1, # 'n' + 49: 1, # 'o' + 51: 1, # 'r' + 43: 1, # 's' + 44: 1, # 't' + 63: 1, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 2, # 'א' + 8: 1, # 'ב' + 20: 1, # 'ג' + 16: 1, # 'ד' + 3: 1, # 'ה' + 2: 1, # 'ו' + 24: 1, # 'ז' + 14: 1, # 'ח' + 22: 1, # 'ט' + 1: 1, # 'י' + 25: 0, # 'ך' + 15: 1, # 'כ' + 4: 1, # 'ל' + 11: 0, # 'ם' + 6: 1, # 'מ' + 23: 0, # 'ן' + 12: 1, # 'נ' + 19: 1, # 'ס' + 13: 1, # 'ע' + 26: 0, # 'ף' + 18: 1, # 'פ' + 27: 0, # 'ץ' + 21: 1, # 'צ' + 17: 1, # 'ק' + 7: 1, # 'ר' + 10: 1, # 'ש' + 5: 1, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 46: { # '”' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 1, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 1, # 'א' + 8: 1, # 'ב' + 20: 1, # 'ג' + 16: 0, # 'ד' + 3: 0, # 'ה' + 2: 0, # 'ו' + 24: 0, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 1, # 'י' + 25: 0, # 'ך' + 15: 1, # 'כ' + 4: 1, # 'ל' + 11: 0, # 'ם' + 6: 1, # 'מ' + 23: 0, # 'ן' + 12: 0, # 'נ' + 19: 0, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 0, # 'פ' + 27: 0, # 'ץ' + 21: 1, # 'צ' + 17: 0, # 'ק' + 7: 1, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 0, # '†' + 40: 0, # '…' + }, + 58: { # '†' + 50: 0, # 'a' + 60: 0, # 'c' + 61: 0, # 'd' + 42: 0, # 'e' + 53: 0, # 'i' + 56: 0, # 'l' + 54: 0, # 'n' + 49: 0, # 'o' + 51: 0, # 'r' + 43: 0, # 's' + 44: 0, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 0, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 0, # 'ה' + 2: 0, # 'ו' + 24: 0, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 0, # 'י' + 25: 0, # 'ך' + 15: 0, # 'כ' + 4: 0, # 'ל' + 11: 0, # 'ם' + 6: 0, # 'מ' + 23: 0, # 'ן' + 12: 0, # 'נ' + 19: 0, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 0, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 0, # 'ר' + 10: 0, # 'ש' + 5: 0, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 0, # '”' + 58: 2, # '†' + 40: 0, # '…' + }, + 40: { # '…' + 50: 1, # 'a' + 60: 1, # 'c' + 61: 1, # 'd' + 42: 1, # 'e' + 53: 1, # 'i' + 56: 0, # 'l' + 54: 1, # 'n' + 49: 0, # 'o' + 51: 1, # 'r' + 43: 1, # 's' + 44: 1, # 't' + 63: 0, # 'u' + 34: 0, # '\xa0' + 55: 0, # '´' + 48: 0, # '¼' + 39: 0, # '½' + 57: 0, # '¾' + 30: 0, # 'ְ' + 59: 0, # 'ֱ' + 41: 0, # 'ֲ' + 33: 0, # 'ִ' + 37: 0, # 'ֵ' + 36: 0, # 'ֶ' + 31: 0, # 'ַ' + 29: 0, # 'ָ' + 35: 0, # 'ֹ' + 62: 0, # 'ֻ' + 28: 0, # 'ּ' + 38: 0, # 'ׁ' + 45: 0, # 'ׂ' + 9: 1, # 'א' + 8: 0, # 'ב' + 20: 0, # 'ג' + 16: 0, # 'ד' + 3: 1, # 'ה' + 2: 1, # 'ו' + 24: 1, # 'ז' + 14: 0, # 'ח' + 22: 0, # 'ט' + 1: 1, # 'י' + 25: 0, # 'ך' + 15: 1, # 'כ' + 4: 1, # 'ל' + 11: 0, # 'ם' + 6: 1, # 'מ' + 23: 0, # 'ן' + 12: 1, # 'נ' + 19: 0, # 'ס' + 13: 0, # 'ע' + 26: 0, # 'ף' + 18: 1, # 'פ' + 27: 0, # 'ץ' + 21: 0, # 'צ' + 17: 0, # 'ק' + 7: 1, # 'ר' + 10: 1, # 'ש' + 5: 1, # 'ת' + 32: 0, # '–' + 52: 0, # '’' + 47: 0, # '“' + 46: 1, # '”' + 58: 0, # '†' + 40: 2, # '…' + }, +} + +# 255: Undefined characters that did not exist in training text # 254: Carriage/Return # 253: symbol (punctuation) that does not belong to word # 252: 0 - 9 +# 251: Control characters -# Windows-1255 language model -# Character Mapping Table: -WIN1255_CHAR_TO_ORDER_MAP = ( -255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 -253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 -252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 -253, 69, 91, 79, 80, 92, 89, 97, 90, 68,111,112, 82, 73, 95, 85, # 40 - 78,121, 86, 71, 67,102,107, 84,114,103,115,253,253,253,253,253, # 50 -253, 50, 74, 60, 61, 42, 76, 70, 64, 53,105, 93, 56, 65, 54, 49, # 60 - 66,110, 51, 43, 44, 63, 81, 77, 98, 75,108,253,253,253,253,253, # 70 -124,202,203,204,205, 40, 58,206,207,208,209,210,211,212,213,214, -215, 83, 52, 47, 46, 72, 32, 94,216,113,217,109,218,219,220,221, - 34,116,222,118,100,223,224,117,119,104,125,225,226, 87, 99,227, -106,122,123,228, 55,229,230,101,231,232,120,233, 48, 39, 57,234, - 30, 59, 41, 88, 33, 37, 36, 31, 29, 35,235, 62, 28,236,126,237, -238, 38, 45,239,240,241,242,243,127,244,245,246,247,248,249,250, - 9, 8, 20, 16, 3, 2, 24, 14, 22, 1, 25, 15, 4, 11, 6, 23, - 12, 19, 13, 26, 18, 27, 21, 17, 7, 10, 5,251,252,128, 96,253, -) +# Character Mapping Table(s): +WINDOWS_1255_HEBREW_CHAR_TO_ORDER = { + 0: 255, # '\x00' + 1: 255, # '\x01' + 2: 255, # '\x02' + 3: 255, # '\x03' + 4: 255, # '\x04' + 5: 255, # '\x05' + 6: 255, # '\x06' + 7: 255, # '\x07' + 8: 255, # '\x08' + 9: 255, # '\t' + 10: 254, # '\n' + 11: 255, # '\x0b' + 12: 255, # '\x0c' + 13: 254, # '\r' + 14: 255, # '\x0e' + 15: 255, # '\x0f' + 16: 255, # '\x10' + 17: 255, # '\x11' + 18: 255, # '\x12' + 19: 255, # '\x13' + 20: 255, # '\x14' + 21: 255, # '\x15' + 22: 255, # '\x16' + 23: 255, # '\x17' + 24: 255, # '\x18' + 25: 255, # '\x19' + 26: 255, # '\x1a' + 27: 255, # '\x1b' + 28: 255, # '\x1c' + 29: 255, # '\x1d' + 30: 255, # '\x1e' + 31: 255, # '\x1f' + 32: 253, # ' ' + 33: 253, # '!' + 34: 253, # '"' + 35: 253, # '#' + 36: 253, # '$' + 37: 253, # '%' + 38: 253, # '&' + 39: 253, # "'" + 40: 253, # '(' + 41: 253, # ')' + 42: 253, # '*' + 43: 253, # '+' + 44: 253, # ',' + 45: 253, # '-' + 46: 253, # '.' + 47: 253, # '/' + 48: 252, # '0' + 49: 252, # '1' + 50: 252, # '2' + 51: 252, # '3' + 52: 252, # '4' + 53: 252, # '5' + 54: 252, # '6' + 55: 252, # '7' + 56: 252, # '8' + 57: 252, # '9' + 58: 253, # ':' + 59: 253, # ';' + 60: 253, # '<' + 61: 253, # '=' + 62: 253, # '>' + 63: 253, # '?' + 64: 253, # '@' + 65: 69, # 'A' + 66: 91, # 'B' + 67: 79, # 'C' + 68: 80, # 'D' + 69: 92, # 'E' + 70: 89, # 'F' + 71: 97, # 'G' + 72: 90, # 'H' + 73: 68, # 'I' + 74: 111, # 'J' + 75: 112, # 'K' + 76: 82, # 'L' + 77: 73, # 'M' + 78: 95, # 'N' + 79: 85, # 'O' + 80: 78, # 'P' + 81: 121, # 'Q' + 82: 86, # 'R' + 83: 71, # 'S' + 84: 67, # 'T' + 85: 102, # 'U' + 86: 107, # 'V' + 87: 84, # 'W' + 88: 114, # 'X' + 89: 103, # 'Y' + 90: 115, # 'Z' + 91: 253, # '[' + 92: 253, # '\\' + 93: 253, # ']' + 94: 253, # '^' + 95: 253, # '_' + 96: 253, # '`' + 97: 50, # 'a' + 98: 74, # 'b' + 99: 60, # 'c' + 100: 61, # 'd' + 101: 42, # 'e' + 102: 76, # 'f' + 103: 70, # 'g' + 104: 64, # 'h' + 105: 53, # 'i' + 106: 105, # 'j' + 107: 93, # 'k' + 108: 56, # 'l' + 109: 65, # 'm' + 110: 54, # 'n' + 111: 49, # 'o' + 112: 66, # 'p' + 113: 110, # 'q' + 114: 51, # 'r' + 115: 43, # 's' + 116: 44, # 't' + 117: 63, # 'u' + 118: 81, # 'v' + 119: 77, # 'w' + 120: 98, # 'x' + 121: 75, # 'y' + 122: 108, # 'z' + 123: 253, # '{' + 124: 253, # '|' + 125: 253, # '}' + 126: 253, # '~' + 127: 253, # '\x7f' + 128: 124, # '€' + 129: 202, # None + 130: 203, # '‚' + 131: 204, # 'ƒ' + 132: 205, # '„' + 133: 40, # '…' + 134: 58, # '†' + 135: 206, # '‡' + 136: 207, # 'ˆ' + 137: 208, # '‰' + 138: 209, # None + 139: 210, # '‹' + 140: 211, # None + 141: 212, # None + 142: 213, # None + 143: 214, # None + 144: 215, # None + 145: 83, # '‘' + 146: 52, # '’' + 147: 47, # '“' + 148: 46, # '”' + 149: 72, # '•' + 150: 32, # '–' + 151: 94, # '—' + 152: 216, # '˜' + 153: 113, # '™' + 154: 217, # None + 155: 109, # '›' + 156: 218, # None + 157: 219, # None + 158: 220, # None + 159: 221, # None + 160: 34, # '\xa0' + 161: 116, # '¡' + 162: 222, # '¢' + 163: 118, # '£' + 164: 100, # '₪' + 165: 223, # '¥' + 166: 224, # '¦' + 167: 117, # '§' + 168: 119, # '¨' + 169: 104, # '©' + 170: 125, # '×' + 171: 225, # '«' + 172: 226, # '¬' + 173: 87, # '\xad' + 174: 99, # '®' + 175: 227, # '¯' + 176: 106, # '°' + 177: 122, # '±' + 178: 123, # '²' + 179: 228, # '³' + 180: 55, # '´' + 181: 229, # 'µ' + 182: 230, # '¶' + 183: 101, # '·' + 184: 231, # '¸' + 185: 232, # '¹' + 186: 120, # '÷' + 187: 233, # '»' + 188: 48, # '¼' + 189: 39, # '½' + 190: 57, # '¾' + 191: 234, # '¿' + 192: 30, # 'ְ' + 193: 59, # 'ֱ' + 194: 41, # 'ֲ' + 195: 88, # 'ֳ' + 196: 33, # 'ִ' + 197: 37, # 'ֵ' + 198: 36, # 'ֶ' + 199: 31, # 'ַ' + 200: 29, # 'ָ' + 201: 35, # 'ֹ' + 202: 235, # None + 203: 62, # 'ֻ' + 204: 28, # 'ּ' + 205: 236, # 'ֽ' + 206: 126, # '־' + 207: 237, # 'ֿ' + 208: 238, # '׀' + 209: 38, # 'ׁ' + 210: 45, # 'ׂ' + 211: 239, # '׃' + 212: 240, # 'װ' + 213: 241, # 'ױ' + 214: 242, # 'ײ' + 215: 243, # '׳' + 216: 127, # '״' + 217: 244, # None + 218: 245, # None + 219: 246, # None + 220: 247, # None + 221: 248, # None + 222: 249, # None + 223: 250, # None + 224: 9, # 'א' + 225: 8, # 'ב' + 226: 20, # 'ג' + 227: 16, # 'ד' + 228: 3, # 'ה' + 229: 2, # 'ו' + 230: 24, # 'ז' + 231: 14, # 'ח' + 232: 22, # 'ט' + 233: 1, # 'י' + 234: 25, # 'ך' + 235: 15, # 'כ' + 236: 4, # 'ל' + 237: 11, # 'ם' + 238: 6, # 'מ' + 239: 23, # 'ן' + 240: 12, # 'נ' + 241: 19, # 'ס' + 242: 13, # 'ע' + 243: 26, # 'ף' + 244: 18, # 'פ' + 245: 27, # 'ץ' + 246: 21, # 'צ' + 247: 17, # 'ק' + 248: 7, # 'ר' + 249: 10, # 'ש' + 250: 5, # 'ת' + 251: 251, # None + 252: 252, # None + 253: 128, # '\u200e' + 254: 96, # '\u200f' + 255: 253, # None +} -# Model Table: -# total sequences: 100% -# first 512 sequences: 98.4004% -# first 1024 sequences: 1.5981% -# rest sequences: 0.087% -# negative sequences: 0.0015% -HEBREW_LANG_MODEL = ( -0,3,3,3,3,3,3,3,3,3,3,2,3,3,3,3,3,3,3,3,3,3,3,2,3,2,1,2,0,1,0,0, -3,0,3,1,0,0,1,3,2,0,1,1,2,0,2,2,2,1,1,1,1,2,1,1,1,2,0,0,2,2,0,1, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2, -1,2,1,2,1,2,0,0,2,0,0,0,0,0,1,0,1,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2, -1,2,1,3,1,1,0,0,2,0,0,0,1,0,1,0,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,0,1,2,2,1,3, -1,2,1,1,2,2,0,0,2,2,0,0,0,0,1,0,1,0,0,0,1,0,0,0,0,0,0,1,0,1,1,0, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,3,2,2,2,2,3,2, -1,2,1,2,2,2,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,3,2,3,2,2,3,2,2,2,1,2,2,2,2, -1,2,1,1,2,2,0,1,2,0,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,1,0, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,0,2,2,2,2,2, -0,2,0,2,2,2,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,0,2,2,2, -0,2,1,2,2,2,0,0,2,1,0,0,0,0,1,0,1,0,0,0,0,0,0,2,0,0,0,0,0,0,1,0, -3,3,3,3,3,3,3,3,3,3,3,2,3,3,3,3,3,3,3,3,3,3,3,3,3,2,1,2,3,2,2,2, -1,2,1,2,2,2,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,1,0, -3,3,3,3,3,3,3,3,3,2,3,3,3,2,3,3,3,3,3,3,3,3,3,3,3,3,3,1,0,2,0,2, -0,2,1,2,2,2,0,0,1,2,0,0,0,0,1,0,1,0,0,0,0,0,0,1,0,0,0,2,0,0,1,0, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,2,3,2,2,3,2,1,2,1,1,1, -0,1,1,1,1,1,3,0,1,0,0,0,0,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0, -3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,0,1,1,0,0,1,0,0,1,0,0,0,0, -0,0,1,0,0,0,0,0,2,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2, -0,2,0,1,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0, -3,3,3,3,3,3,3,3,3,2,3,3,3,2,1,2,3,3,2,3,3,3,3,2,3,2,1,2,0,2,1,2, -0,2,0,2,2,2,0,0,1,2,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,1,0,0,1,0, -3,3,3,3,3,3,3,3,3,2,3,3,3,1,2,2,3,3,2,3,2,3,2,2,3,1,2,2,0,2,2,2, -0,2,1,2,2,2,0,0,1,2,0,0,0,0,1,0,0,0,0,0,1,0,0,1,0,0,0,1,0,0,1,0, -3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,3,3,2,3,3,2,2,2,3,3,3,3,1,3,2,2,2, -0,2,0,1,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,2,3,3,3,2,3,2,2,2,1,2,2,0,2,2,2,2, -0,2,0,2,2,2,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0, -3,3,3,3,3,3,3,3,3,3,3,2,3,3,3,1,3,2,3,3,2,3,3,2,2,1,2,2,2,2,2,2, -0,2,1,2,1,2,0,0,1,0,0,0,0,0,1,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,1,0, -3,3,3,3,3,3,2,3,2,3,3,2,3,3,3,3,2,3,2,3,3,3,3,3,2,2,2,2,2,2,2,1, -0,2,0,1,2,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0, -3,3,3,3,3,3,3,3,3,2,1,2,3,3,3,3,3,3,3,2,3,2,3,2,1,2,3,0,2,1,2,2, -0,2,1,1,2,1,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,2,0, -3,3,3,3,3,3,3,3,3,2,3,3,3,3,2,1,3,1,2,2,2,1,2,3,3,1,2,1,2,2,2,2, -0,1,1,1,1,1,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0,0,2,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,3,3,3,0,2,3,3,3,1,3,3,3,1,2,2,2,2,1,1,2,2,2,2,2,2, -0,2,0,1,1,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0, -3,3,3,3,3,3,2,3,3,3,2,2,3,3,3,2,1,2,3,2,3,2,2,2,2,1,2,1,1,1,2,2, -0,2,1,1,1,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0, -3,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0,0,1,0,0,0,0,0, -1,0,1,0,0,0,0,0,2,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,2,3,3,2,3,1,2,2,2,2,3,2,3,1,1,2,2,1,2,2,1,1,0,2,2,2,2, -0,1,0,1,2,2,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0, -3,0,0,1,1,0,1,0,0,1,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,2,2,0, -0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,0,1,0,1,0,1,1,0,1,1,0,0,0,1,1,0,1,1,1,0,0,0,0,0,0,1,0,0,0,0,0, -0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,0,0,0,1,1,0,1,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0, -3,2,2,1,2,2,2,2,2,2,2,1,2,2,1,2,2,1,1,1,1,1,1,1,1,2,1,1,0,3,3,3, -0,3,0,2,2,2,2,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0, -2,2,2,3,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1,2,2,1,2,2,2,1,1,1,2,0,1, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,2,2,2,2,2,2,2,2,2,2,1,2,2,2,2,2,2,2,2,2,2,2,0,2,2,0,0,0,0,0,0, -0,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,3,1,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,2,1,2,1,0,2,1,0, -0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,1,1,1,1,1,1,1,1,1,1,0,0,1,1,1,1,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0, -0,3,1,1,2,2,2,2,2,1,2,2,2,1,1,2,2,2,2,2,2,2,1,2,2,1,0,1,1,1,1,0, -0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,2,1,1,1,1,2,1,1,2,1,0,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0,0,0,0,0,0, -0,0,2,0,0,0,0,0,0,0,0,1,1,0,0,0,0,1,1,0,0,1,1,0,0,0,0,0,0,1,0,0, -2,1,1,2,2,2,2,2,2,2,2,2,2,2,1,2,2,2,2,2,1,2,1,2,1,1,1,1,0,0,0,0, -0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,2,1,2,2,2,2,2,2,2,2,2,2,1,2,1,2,1,1,2,1,1,1,2,1,2,1,2,0,1,0,1, -0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,3,1,2,2,2,1,2,2,2,2,2,2,2,2,1,2,1,1,1,1,1,1,2,1,2,1,1,0,1,0,1, -0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,1,2,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,2,2, -0,2,0,1,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0, -3,0,0,0,1,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,1,1,1,1,1,1,1,0,1,1,0,1,0,0,1,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,2,0,1,1,1,0,1,0,0,0,1,1,0,1,1,0,0,0,0,0,1,1,0,0, -0,1,1,1,2,1,2,2,2,0,2,0,2,0,1,1,2,1,1,1,1,2,1,0,1,1,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,0,1,0,0,0,0,0,1,0,1,2,2,0,1,0,0,1,1,2,2,1,2,0,2,0,0,0,1,2,0,1, -2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,2,0,2,1,2,0,2,0,0,1,1,1,1,1,1,0,1,0,0,0,1,0,0,1, -2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,1,0,0,0,0,0,1,0,2,1,1,0,1,0,0,1,1,1,2,2,0,0,1,0,0,0,1,0,0,1, -1,1,2,1,0,1,1,1,0,1,0,1,1,1,1,0,0,0,1,0,1,0,0,0,0,0,0,0,0,2,2,1, -0,2,0,1,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,1,0,0,1,0,1,1,1,1,0,0,0,0,0,1,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,1,1,1,1,1,1,1,1,2,1,0,1,1,1,1,1,1,1,1,1,1,1,0,1,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,1,1,1,0,0,0,0,1,1,1,0,1,1,0,1,0,0,0,1,1,0,1, -2,0,1,0,1,0,1,0,0,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,1,0,1,1,1,0,1,0,0,1,1,2,1,1,2,0,1,0,0,0,1,1,0,1, -1,0,0,1,0,0,1,0,0,0,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,1,0,1,1,2,0,1,0,0,0,0,2,1,1,2,0,2,0,0,0,1,1,0,1, -1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,1,0,2,1,1,0,1,0,0,2,2,1,2,1,1,0,1,0,0,0,1,1,0,1, -2,0,1,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,1,2,2,0,0,0,0,0,1,1,0,1,0,0,1,0,0,0,0,1,0,1, -1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,1,2,2,0,0,0,0,2,1,1,1,0,2,1,1,0,0,0,2,1,0,1, -1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,1,0,1,1,2,0,1,0,0,1,1,0,2,1,1,0,1,0,0,0,1,1,0,1, -2,2,1,1,1,0,1,1,0,1,1,0,1,0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,1,0,2,1,1,0,1,0,0,1,1,0,1,2,1,0,2,0,0,0,1,1,0,1, -2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0, -0,1,0,0,2,0,2,1,1,0,1,0,1,0,0,1,0,0,0,0,1,0,0,0,1,0,0,0,0,0,1,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,0,0,1,0,0,1,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,1,0,1,1,2,0,1,0,0,1,1,1,0,1,0,0,1,0,0,0,1,0,0,1, -1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,0,0,0,0,0,0,0,1,0,1,1,0,0,1,0,0,2,1,1,1,1,1,0,1,0,0,0,0,1,0,1, -0,1,1,1,2,1,1,1,1,0,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,1,2,1,0,0,0,0,0,1,1,1,1,1,0,1,0,0,0,1,1,0,0, -) +WINDOWS_1255_HEBREW_MODEL = SingleByteCharSetModel(charset_name='windows-1255', + language='Hebrew', + char_to_order_map=WINDOWS_1255_HEBREW_CHAR_TO_ORDER, + language_model=HEBREW_LANG_MODEL, + typical_positive_ratio=0.984004, + keep_ascii_letters=False, + alphabet='אבגדהוזחטיךכלםמןנסעףפץצקרשתװױײ') -Win1255HebrewModel = { - 'char_to_order_map': WIN1255_CHAR_TO_ORDER_MAP, - 'precedence_matrix': HEBREW_LANG_MODEL, - 'typical_positive_ratio': 0.984004, - 'keep_english_letter': False, - 'charset_name': "windows-1255", - 'language': 'Hebrew', -} diff --git a/src/pip/_vendor/chardet/langhungarianmodel.py b/src/pip/_vendor/chardet/langhungarianmodel.py index bb7c095e1ea..bbc5cda6441 100644 --- a/src/pip/_vendor/chardet/langhungarianmodel.py +++ b/src/pip/_vendor/chardet/langhungarianmodel.py @@ -1,225 +1,4650 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is Mozilla Communicator client code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 1998 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### +#!/usr/bin/env python +# -*- coding: utf-8 -*- -# 255: Control characters that usually does not exist in any text +from pip._vendor.chardet.sbcharsetprober import SingleByteCharSetModel + + +# 3: Positive +# 2: Likely +# 1: Unlikely +# 0: Negative + +HUNGARIAN_LANG_MODEL = { + 28: { # 'A' + 28: 0, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 2, # 'D' + 32: 1, # 'E' + 50: 1, # 'F' + 49: 2, # 'G' + 38: 1, # 'H' + 39: 2, # 'I' + 53: 1, # 'J' + 36: 2, # 'K' + 41: 2, # 'L' + 34: 1, # 'M' + 35: 2, # 'N' + 47: 1, # 'O' + 46: 2, # 'P' + 43: 2, # 'R' + 33: 2, # 'S' + 37: 2, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 1, # 'Y' + 52: 2, # 'Z' + 2: 0, # 'a' + 18: 1, # 'b' + 26: 1, # 'c' + 17: 2, # 'd' + 1: 1, # 'e' + 27: 1, # 'f' + 12: 1, # 'g' + 20: 1, # 'h' + 9: 1, # 'i' + 22: 1, # 'j' + 7: 2, # 'k' + 6: 2, # 'l' + 13: 2, # 'm' + 4: 2, # 'n' + 8: 0, # 'o' + 23: 2, # 'p' + 10: 2, # 'r' + 5: 1, # 's' + 3: 1, # 't' + 21: 1, # 'u' + 19: 1, # 'v' + 62: 1, # 'x' + 16: 0, # 'y' + 11: 3, # 'z' + 51: 1, # 'Á' + 44: 0, # 'É' + 61: 1, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 40: { # 'B' + 28: 2, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 2, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 1, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 0, # 'M' + 35: 1, # 'N' + 47: 2, # 'O' + 46: 0, # 'P' + 43: 1, # 'R' + 33: 1, # 'S' + 37: 1, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 2, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 3, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 0, # 'h' + 9: 2, # 'i' + 22: 1, # 'j' + 7: 0, # 'k' + 6: 1, # 'l' + 13: 0, # 'm' + 4: 0, # 'n' + 8: 2, # 'o' + 23: 1, # 'p' + 10: 2, # 'r' + 5: 0, # 's' + 3: 0, # 't' + 21: 3, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 0, # 'z' + 51: 1, # 'Á' + 44: 1, # 'É' + 61: 1, # 'Í' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 1, # 'Ú' + 63: 1, # 'Ü' + 14: 2, # 'á' + 15: 2, # 'é' + 30: 1, # 'í' + 25: 1, # 'ó' + 24: 1, # 'ö' + 31: 1, # 'ú' + 29: 1, # 'ü' + 42: 1, # 'ő' + 56: 1, # 'ű' + }, + 54: { # 'C' + 28: 1, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 1, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 1, # 'H' + 39: 2, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 0, # 'N' + 47: 1, # 'O' + 46: 1, # 'P' + 43: 1, # 'R' + 33: 2, # 'S' + 37: 1, # 'T' + 57: 1, # 'U' + 48: 0, # 'V' + 55: 1, # 'Y' + 52: 1, # 'Z' + 2: 2, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 1, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 1, # 'h' + 9: 1, # 'i' + 22: 0, # 'j' + 7: 0, # 'k' + 6: 1, # 'l' + 13: 0, # 'm' + 4: 0, # 'n' + 8: 2, # 'o' + 23: 0, # 'p' + 10: 1, # 'r' + 5: 3, # 's' + 3: 0, # 't' + 21: 1, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 1, # 'z' + 51: 1, # 'Á' + 44: 1, # 'É' + 61: 1, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 1, # 'á' + 15: 1, # 'é' + 30: 1, # 'í' + 25: 1, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 45: { # 'D' + 28: 2, # 'A' + 40: 1, # 'B' + 54: 0, # 'C' + 45: 1, # 'D' + 32: 2, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 2, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 0, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 2, # 'O' + 46: 0, # 'P' + 43: 1, # 'R' + 33: 1, # 'S' + 37: 1, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 1, # 'Y' + 52: 1, # 'Z' + 2: 2, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 3, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 0, # 'h' + 9: 1, # 'i' + 22: 0, # 'j' + 7: 0, # 'k' + 6: 0, # 'l' + 13: 0, # 'm' + 4: 0, # 'n' + 8: 1, # 'o' + 23: 0, # 'p' + 10: 2, # 'r' + 5: 0, # 's' + 3: 0, # 't' + 21: 2, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 1, # 'z' + 51: 1, # 'Á' + 44: 1, # 'É' + 61: 1, # 'Í' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 1, # 'Ú' + 63: 1, # 'Ü' + 14: 1, # 'á' + 15: 1, # 'é' + 30: 1, # 'í' + 25: 1, # 'ó' + 24: 1, # 'ö' + 31: 1, # 'ú' + 29: 1, # 'ü' + 42: 1, # 'ő' + 56: 0, # 'ű' + }, + 32: { # 'E' + 28: 1, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 1, # 'E' + 50: 1, # 'F' + 49: 2, # 'G' + 38: 1, # 'H' + 39: 1, # 'I' + 53: 1, # 'J' + 36: 2, # 'K' + 41: 2, # 'L' + 34: 2, # 'M' + 35: 2, # 'N' + 47: 1, # 'O' + 46: 1, # 'P' + 43: 2, # 'R' + 33: 2, # 'S' + 37: 2, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 1, # 'Y' + 52: 1, # 'Z' + 2: 1, # 'a' + 18: 1, # 'b' + 26: 1, # 'c' + 17: 2, # 'd' + 1: 1, # 'e' + 27: 1, # 'f' + 12: 3, # 'g' + 20: 1, # 'h' + 9: 1, # 'i' + 22: 1, # 'j' + 7: 1, # 'k' + 6: 2, # 'l' + 13: 2, # 'm' + 4: 2, # 'n' + 8: 0, # 'o' + 23: 1, # 'p' + 10: 2, # 'r' + 5: 2, # 's' + 3: 1, # 't' + 21: 2, # 'u' + 19: 1, # 'v' + 62: 1, # 'x' + 16: 0, # 'y' + 11: 3, # 'z' + 51: 1, # 'Á' + 44: 1, # 'É' + 61: 0, # 'Í' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 0, # 'Ú' + 63: 1, # 'Ü' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 1, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 50: { # 'F' + 28: 1, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 1, # 'E' + 50: 1, # 'F' + 49: 0, # 'G' + 38: 1, # 'H' + 39: 1, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 1, # 'O' + 46: 0, # 'P' + 43: 1, # 'R' + 33: 0, # 'S' + 37: 1, # 'T' + 57: 1, # 'U' + 48: 0, # 'V' + 55: 1, # 'Y' + 52: 0, # 'Z' + 2: 2, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 2, # 'e' + 27: 1, # 'f' + 12: 0, # 'g' + 20: 0, # 'h' + 9: 2, # 'i' + 22: 1, # 'j' + 7: 0, # 'k' + 6: 1, # 'l' + 13: 0, # 'm' + 4: 0, # 'n' + 8: 2, # 'o' + 23: 0, # 'p' + 10: 2, # 'r' + 5: 0, # 's' + 3: 0, # 't' + 21: 1, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 0, # 'z' + 51: 1, # 'Á' + 44: 1, # 'É' + 61: 0, # 'Í' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 0, # 'Ú' + 63: 1, # 'Ü' + 14: 1, # 'á' + 15: 1, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 2, # 'ö' + 31: 1, # 'ú' + 29: 1, # 'ü' + 42: 1, # 'ő' + 56: 1, # 'ű' + }, + 49: { # 'G' + 28: 2, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 2, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 1, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 1, # 'O' + 46: 1, # 'P' + 43: 1, # 'R' + 33: 1, # 'S' + 37: 1, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 2, # 'Y' + 52: 1, # 'Z' + 2: 2, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 2, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 0, # 'h' + 9: 1, # 'i' + 22: 0, # 'j' + 7: 0, # 'k' + 6: 1, # 'l' + 13: 0, # 'm' + 4: 0, # 'n' + 8: 2, # 'o' + 23: 0, # 'p' + 10: 2, # 'r' + 5: 0, # 's' + 3: 0, # 't' + 21: 1, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 2, # 'y' + 11: 0, # 'z' + 51: 1, # 'Á' + 44: 1, # 'É' + 61: 1, # 'Í' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 1, # 'Ú' + 63: 1, # 'Ü' + 14: 1, # 'á' + 15: 1, # 'é' + 30: 0, # 'í' + 25: 1, # 'ó' + 24: 1, # 'ö' + 31: 1, # 'ú' + 29: 1, # 'ü' + 42: 1, # 'ő' + 56: 0, # 'ű' + }, + 38: { # 'H' + 28: 2, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 0, # 'D' + 32: 1, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 1, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 1, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 1, # 'O' + 46: 0, # 'P' + 43: 1, # 'R' + 33: 1, # 'S' + 37: 1, # 'T' + 57: 1, # 'U' + 48: 0, # 'V' + 55: 1, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 2, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 0, # 'h' + 9: 2, # 'i' + 22: 1, # 'j' + 7: 0, # 'k' + 6: 1, # 'l' + 13: 1, # 'm' + 4: 0, # 'n' + 8: 3, # 'o' + 23: 0, # 'p' + 10: 1, # 'r' + 5: 0, # 's' + 3: 0, # 't' + 21: 2, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 0, # 'z' + 51: 2, # 'Á' + 44: 2, # 'É' + 61: 1, # 'Í' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 1, # 'Ú' + 63: 1, # 'Ü' + 14: 2, # 'á' + 15: 1, # 'é' + 30: 2, # 'í' + 25: 1, # 'ó' + 24: 1, # 'ö' + 31: 1, # 'ú' + 29: 1, # 'ü' + 42: 1, # 'ő' + 56: 1, # 'ű' + }, + 39: { # 'I' + 28: 2, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 1, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 2, # 'I' + 53: 1, # 'J' + 36: 2, # 'K' + 41: 2, # 'L' + 34: 1, # 'M' + 35: 2, # 'N' + 47: 1, # 'O' + 46: 1, # 'P' + 43: 1, # 'R' + 33: 2, # 'S' + 37: 1, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 0, # 'Y' + 52: 2, # 'Z' + 2: 0, # 'a' + 18: 1, # 'b' + 26: 1, # 'c' + 17: 2, # 'd' + 1: 0, # 'e' + 27: 1, # 'f' + 12: 2, # 'g' + 20: 1, # 'h' + 9: 0, # 'i' + 22: 1, # 'j' + 7: 1, # 'k' + 6: 2, # 'l' + 13: 2, # 'm' + 4: 1, # 'n' + 8: 0, # 'o' + 23: 1, # 'p' + 10: 2, # 'r' + 5: 2, # 's' + 3: 2, # 't' + 21: 0, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 1, # 'z' + 51: 1, # 'Á' + 44: 1, # 'É' + 61: 0, # 'Í' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 1, # 'Ú' + 63: 1, # 'Ü' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 53: { # 'J' + 28: 2, # 'A' + 40: 0, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 2, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 1, # 'H' + 39: 1, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 1, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 1, # 'S' + 37: 1, # 'T' + 57: 1, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 1, # 'Z' + 2: 2, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 2, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 0, # 'h' + 9: 1, # 'i' + 22: 0, # 'j' + 7: 0, # 'k' + 6: 0, # 'l' + 13: 0, # 'm' + 4: 0, # 'n' + 8: 1, # 'o' + 23: 0, # 'p' + 10: 0, # 'r' + 5: 0, # 's' + 3: 0, # 't' + 21: 2, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 0, # 'z' + 51: 1, # 'Á' + 44: 1, # 'É' + 61: 0, # 'Í' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 1, # 'Ú' + 63: 1, # 'Ü' + 14: 2, # 'á' + 15: 1, # 'é' + 30: 0, # 'í' + 25: 2, # 'ó' + 24: 2, # 'ö' + 31: 1, # 'ú' + 29: 0, # 'ü' + 42: 1, # 'ő' + 56: 0, # 'ű' + }, + 36: { # 'K' + 28: 2, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 2, # 'E' + 50: 1, # 'F' + 49: 0, # 'G' + 38: 1, # 'H' + 39: 2, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 2, # 'O' + 46: 0, # 'P' + 43: 1, # 'R' + 33: 1, # 'S' + 37: 1, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 1, # 'Y' + 52: 0, # 'Z' + 2: 2, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 2, # 'e' + 27: 1, # 'f' + 12: 0, # 'g' + 20: 1, # 'h' + 9: 3, # 'i' + 22: 0, # 'j' + 7: 0, # 'k' + 6: 1, # 'l' + 13: 1, # 'm' + 4: 1, # 'n' + 8: 2, # 'o' + 23: 0, # 'p' + 10: 2, # 'r' + 5: 0, # 's' + 3: 0, # 't' + 21: 1, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 0, # 'z' + 51: 1, # 'Á' + 44: 1, # 'É' + 61: 1, # 'Í' + 58: 1, # 'Ó' + 59: 2, # 'Ö' + 60: 1, # 'Ú' + 63: 1, # 'Ü' + 14: 2, # 'á' + 15: 2, # 'é' + 30: 1, # 'í' + 25: 1, # 'ó' + 24: 2, # 'ö' + 31: 1, # 'ú' + 29: 2, # 'ü' + 42: 1, # 'ő' + 56: 0, # 'ű' + }, + 41: { # 'L' + 28: 2, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 2, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 2, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 2, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 2, # 'O' + 46: 0, # 'P' + 43: 1, # 'R' + 33: 1, # 'S' + 37: 2, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 1, # 'Y' + 52: 1, # 'Z' + 2: 2, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 3, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 0, # 'h' + 9: 2, # 'i' + 22: 1, # 'j' + 7: 0, # 'k' + 6: 1, # 'l' + 13: 0, # 'm' + 4: 0, # 'n' + 8: 2, # 'o' + 23: 0, # 'p' + 10: 0, # 'r' + 5: 0, # 's' + 3: 0, # 't' + 21: 2, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 0, # 'z' + 51: 2, # 'Á' + 44: 1, # 'É' + 61: 1, # 'Í' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 1, # 'Ú' + 63: 1, # 'Ü' + 14: 2, # 'á' + 15: 1, # 'é' + 30: 1, # 'í' + 25: 1, # 'ó' + 24: 1, # 'ö' + 31: 0, # 'ú' + 29: 1, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 34: { # 'M' + 28: 2, # 'A' + 40: 1, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 2, # 'E' + 50: 1, # 'F' + 49: 0, # 'G' + 38: 1, # 'H' + 39: 2, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 1, # 'O' + 46: 1, # 'P' + 43: 1, # 'R' + 33: 1, # 'S' + 37: 1, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 1, # 'Y' + 52: 1, # 'Z' + 2: 3, # 'a' + 18: 0, # 'b' + 26: 1, # 'c' + 17: 0, # 'd' + 1: 3, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 0, # 'h' + 9: 3, # 'i' + 22: 0, # 'j' + 7: 0, # 'k' + 6: 0, # 'l' + 13: 1, # 'm' + 4: 1, # 'n' + 8: 3, # 'o' + 23: 0, # 'p' + 10: 1, # 'r' + 5: 0, # 's' + 3: 0, # 't' + 21: 2, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 0, # 'z' + 51: 2, # 'Á' + 44: 1, # 'É' + 61: 1, # 'Í' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 1, # 'Ú' + 63: 1, # 'Ü' + 14: 2, # 'á' + 15: 2, # 'é' + 30: 1, # 'í' + 25: 1, # 'ó' + 24: 1, # 'ö' + 31: 1, # 'ú' + 29: 1, # 'ü' + 42: 0, # 'ő' + 56: 1, # 'ű' + }, + 35: { # 'N' + 28: 2, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 2, # 'D' + 32: 2, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 1, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 1, # 'O' + 46: 1, # 'P' + 43: 1, # 'R' + 33: 1, # 'S' + 37: 2, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 2, # 'Y' + 52: 1, # 'Z' + 2: 3, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 3, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 0, # 'h' + 9: 2, # 'i' + 22: 0, # 'j' + 7: 0, # 'k' + 6: 0, # 'l' + 13: 0, # 'm' + 4: 1, # 'n' + 8: 2, # 'o' + 23: 0, # 'p' + 10: 0, # 'r' + 5: 0, # 's' + 3: 0, # 't' + 21: 1, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 2, # 'y' + 11: 0, # 'z' + 51: 1, # 'Á' + 44: 1, # 'É' + 61: 1, # 'Í' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 1, # 'Ú' + 63: 1, # 'Ü' + 14: 1, # 'á' + 15: 2, # 'é' + 30: 1, # 'í' + 25: 1, # 'ó' + 24: 1, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 1, # 'ő' + 56: 0, # 'ű' + }, + 47: { # 'O' + 28: 1, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 1, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 1, # 'I' + 53: 1, # 'J' + 36: 2, # 'K' + 41: 2, # 'L' + 34: 2, # 'M' + 35: 2, # 'N' + 47: 1, # 'O' + 46: 1, # 'P' + 43: 2, # 'R' + 33: 2, # 'S' + 37: 2, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 1, # 'Y' + 52: 1, # 'Z' + 2: 0, # 'a' + 18: 1, # 'b' + 26: 1, # 'c' + 17: 1, # 'd' + 1: 1, # 'e' + 27: 1, # 'f' + 12: 1, # 'g' + 20: 1, # 'h' + 9: 1, # 'i' + 22: 1, # 'j' + 7: 2, # 'k' + 6: 2, # 'l' + 13: 1, # 'm' + 4: 1, # 'n' + 8: 1, # 'o' + 23: 1, # 'p' + 10: 2, # 'r' + 5: 1, # 's' + 3: 2, # 't' + 21: 1, # 'u' + 19: 0, # 'v' + 62: 1, # 'x' + 16: 0, # 'y' + 11: 1, # 'z' + 51: 1, # 'Á' + 44: 1, # 'É' + 61: 0, # 'Í' + 58: 1, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 46: { # 'P' + 28: 1, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 1, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 1, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 0, # 'M' + 35: 1, # 'N' + 47: 1, # 'O' + 46: 1, # 'P' + 43: 2, # 'R' + 33: 1, # 'S' + 37: 1, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 0, # 'Y' + 52: 1, # 'Z' + 2: 2, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 2, # 'e' + 27: 1, # 'f' + 12: 0, # 'g' + 20: 1, # 'h' + 9: 2, # 'i' + 22: 0, # 'j' + 7: 0, # 'k' + 6: 1, # 'l' + 13: 0, # 'm' + 4: 1, # 'n' + 8: 2, # 'o' + 23: 0, # 'p' + 10: 2, # 'r' + 5: 1, # 's' + 3: 0, # 't' + 21: 1, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 0, # 'z' + 51: 2, # 'Á' + 44: 1, # 'É' + 61: 1, # 'Í' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 0, # 'Ú' + 63: 1, # 'Ü' + 14: 3, # 'á' + 15: 2, # 'é' + 30: 0, # 'í' + 25: 1, # 'ó' + 24: 1, # 'ö' + 31: 0, # 'ú' + 29: 1, # 'ü' + 42: 1, # 'ő' + 56: 0, # 'ű' + }, + 43: { # 'R' + 28: 2, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 2, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 2, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 2, # 'O' + 46: 1, # 'P' + 43: 1, # 'R' + 33: 2, # 'S' + 37: 2, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 1, # 'Y' + 52: 1, # 'Z' + 2: 2, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 2, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 1, # 'h' + 9: 2, # 'i' + 22: 0, # 'j' + 7: 0, # 'k' + 6: 0, # 'l' + 13: 0, # 'm' + 4: 0, # 'n' + 8: 2, # 'o' + 23: 0, # 'p' + 10: 0, # 'r' + 5: 0, # 's' + 3: 0, # 't' + 21: 1, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 0, # 'z' + 51: 2, # 'Á' + 44: 1, # 'É' + 61: 1, # 'Í' + 58: 2, # 'Ó' + 59: 1, # 'Ö' + 60: 1, # 'Ú' + 63: 1, # 'Ü' + 14: 2, # 'á' + 15: 2, # 'é' + 30: 1, # 'í' + 25: 2, # 'ó' + 24: 1, # 'ö' + 31: 1, # 'ú' + 29: 1, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 33: { # 'S' + 28: 2, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 2, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 2, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 2, # 'O' + 46: 1, # 'P' + 43: 1, # 'R' + 33: 2, # 'S' + 37: 2, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 1, # 'Y' + 52: 3, # 'Z' + 2: 2, # 'a' + 18: 0, # 'b' + 26: 1, # 'c' + 17: 0, # 'd' + 1: 2, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 1, # 'h' + 9: 2, # 'i' + 22: 0, # 'j' + 7: 1, # 'k' + 6: 1, # 'l' + 13: 1, # 'm' + 4: 0, # 'n' + 8: 2, # 'o' + 23: 1, # 'p' + 10: 0, # 'r' + 5: 0, # 's' + 3: 1, # 't' + 21: 1, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 3, # 'z' + 51: 2, # 'Á' + 44: 1, # 'É' + 61: 1, # 'Í' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 1, # 'Ú' + 63: 1, # 'Ü' + 14: 2, # 'á' + 15: 1, # 'é' + 30: 1, # 'í' + 25: 1, # 'ó' + 24: 1, # 'ö' + 31: 1, # 'ú' + 29: 1, # 'ü' + 42: 1, # 'ő' + 56: 1, # 'ű' + }, + 37: { # 'T' + 28: 2, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 2, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 2, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 2, # 'O' + 46: 1, # 'P' + 43: 2, # 'R' + 33: 1, # 'S' + 37: 2, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 1, # 'Y' + 52: 1, # 'Z' + 2: 2, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 2, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 1, # 'h' + 9: 2, # 'i' + 22: 0, # 'j' + 7: 0, # 'k' + 6: 0, # 'l' + 13: 0, # 'm' + 4: 0, # 'n' + 8: 2, # 'o' + 23: 0, # 'p' + 10: 1, # 'r' + 5: 1, # 's' + 3: 0, # 't' + 21: 2, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 1, # 'z' + 51: 2, # 'Á' + 44: 2, # 'É' + 61: 1, # 'Í' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 1, # 'Ú' + 63: 1, # 'Ü' + 14: 2, # 'á' + 15: 1, # 'é' + 30: 1, # 'í' + 25: 1, # 'ó' + 24: 2, # 'ö' + 31: 1, # 'ú' + 29: 1, # 'ü' + 42: 1, # 'ő' + 56: 1, # 'ű' + }, + 57: { # 'U' + 28: 1, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 1, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 1, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 1, # 'O' + 46: 1, # 'P' + 43: 1, # 'R' + 33: 2, # 'S' + 37: 1, # 'T' + 57: 0, # 'U' + 48: 1, # 'V' + 55: 0, # 'Y' + 52: 1, # 'Z' + 2: 0, # 'a' + 18: 1, # 'b' + 26: 1, # 'c' + 17: 1, # 'd' + 1: 1, # 'e' + 27: 0, # 'f' + 12: 2, # 'g' + 20: 0, # 'h' + 9: 0, # 'i' + 22: 1, # 'j' + 7: 1, # 'k' + 6: 1, # 'l' + 13: 1, # 'm' + 4: 1, # 'n' + 8: 0, # 'o' + 23: 1, # 'p' + 10: 1, # 'r' + 5: 1, # 's' + 3: 1, # 't' + 21: 0, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 1, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 1, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 48: { # 'V' + 28: 2, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 1, # 'D' + 32: 2, # 'E' + 50: 1, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 2, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 0, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 1, # 'O' + 46: 1, # 'P' + 43: 1, # 'R' + 33: 1, # 'S' + 37: 1, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 1, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 2, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 0, # 'h' + 9: 2, # 'i' + 22: 0, # 'j' + 7: 0, # 'k' + 6: 1, # 'l' + 13: 0, # 'm' + 4: 0, # 'n' + 8: 2, # 'o' + 23: 0, # 'p' + 10: 0, # 'r' + 5: 0, # 's' + 3: 0, # 't' + 21: 1, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 0, # 'z' + 51: 2, # 'Á' + 44: 2, # 'É' + 61: 1, # 'Í' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 0, # 'Ú' + 63: 1, # 'Ü' + 14: 2, # 'á' + 15: 2, # 'é' + 30: 1, # 'í' + 25: 0, # 'ó' + 24: 1, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 55: { # 'Y' + 28: 2, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 2, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 1, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 1, # 'O' + 46: 1, # 'P' + 43: 1, # 'R' + 33: 1, # 'S' + 37: 1, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 0, # 'Y' + 52: 2, # 'Z' + 2: 1, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 1, # 'd' + 1: 1, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 0, # 'h' + 9: 0, # 'i' + 22: 0, # 'j' + 7: 0, # 'k' + 6: 0, # 'l' + 13: 0, # 'm' + 4: 0, # 'n' + 8: 1, # 'o' + 23: 1, # 'p' + 10: 0, # 'r' + 5: 0, # 's' + 3: 0, # 't' + 21: 0, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 0, # 'z' + 51: 1, # 'Á' + 44: 1, # 'É' + 61: 1, # 'Í' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 1, # 'Ú' + 63: 1, # 'Ü' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 52: { # 'Z' + 28: 2, # 'A' + 40: 1, # 'B' + 54: 0, # 'C' + 45: 1, # 'D' + 32: 2, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 2, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 2, # 'O' + 46: 1, # 'P' + 43: 1, # 'R' + 33: 2, # 'S' + 37: 1, # 'T' + 57: 1, # 'U' + 48: 1, # 'V' + 55: 1, # 'Y' + 52: 1, # 'Z' + 2: 1, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 1, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 0, # 'h' + 9: 1, # 'i' + 22: 0, # 'j' + 7: 0, # 'k' + 6: 0, # 'l' + 13: 0, # 'm' + 4: 1, # 'n' + 8: 1, # 'o' + 23: 0, # 'p' + 10: 1, # 'r' + 5: 2, # 's' + 3: 0, # 't' + 21: 1, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 0, # 'z' + 51: 2, # 'Á' + 44: 1, # 'É' + 61: 1, # 'Í' + 58: 1, # 'Ó' + 59: 1, # 'Ö' + 60: 1, # 'Ú' + 63: 1, # 'Ü' + 14: 1, # 'á' + 15: 1, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 1, # 'ö' + 31: 1, # 'ú' + 29: 1, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 2: { # 'a' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 1, # 'a' + 18: 3, # 'b' + 26: 3, # 'c' + 17: 3, # 'd' + 1: 2, # 'e' + 27: 2, # 'f' + 12: 3, # 'g' + 20: 3, # 'h' + 9: 3, # 'i' + 22: 3, # 'j' + 7: 3, # 'k' + 6: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 8: 2, # 'o' + 23: 3, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 3, # 'u' + 19: 3, # 'v' + 62: 1, # 'x' + 16: 2, # 'y' + 11: 3, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 1, # 'á' + 15: 1, # 'é' + 30: 1, # 'í' + 25: 1, # 'ó' + 24: 1, # 'ö' + 31: 1, # 'ú' + 29: 1, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 18: { # 'b' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 3, # 'b' + 26: 1, # 'c' + 17: 1, # 'd' + 1: 3, # 'e' + 27: 1, # 'f' + 12: 1, # 'g' + 20: 1, # 'h' + 9: 3, # 'i' + 22: 2, # 'j' + 7: 2, # 'k' + 6: 2, # 'l' + 13: 1, # 'm' + 4: 2, # 'n' + 8: 3, # 'o' + 23: 1, # 'p' + 10: 3, # 'r' + 5: 2, # 's' + 3: 1, # 't' + 21: 3, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 1, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 2, # 'í' + 25: 3, # 'ó' + 24: 2, # 'ö' + 31: 2, # 'ú' + 29: 2, # 'ü' + 42: 2, # 'ő' + 56: 1, # 'ű' + }, + 26: { # 'c' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 1, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 1, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 2, # 'a' + 18: 1, # 'b' + 26: 2, # 'c' + 17: 1, # 'd' + 1: 3, # 'e' + 27: 1, # 'f' + 12: 1, # 'g' + 20: 3, # 'h' + 9: 3, # 'i' + 22: 1, # 'j' + 7: 2, # 'k' + 6: 1, # 'l' + 13: 1, # 'm' + 4: 1, # 'n' + 8: 3, # 'o' + 23: 1, # 'p' + 10: 2, # 'r' + 5: 3, # 's' + 3: 2, # 't' + 21: 2, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 2, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 2, # 'á' + 15: 2, # 'é' + 30: 2, # 'í' + 25: 1, # 'ó' + 24: 1, # 'ö' + 31: 1, # 'ú' + 29: 1, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 17: { # 'd' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 2, # 'b' + 26: 1, # 'c' + 17: 2, # 'd' + 1: 3, # 'e' + 27: 1, # 'f' + 12: 1, # 'g' + 20: 2, # 'h' + 9: 3, # 'i' + 22: 3, # 'j' + 7: 2, # 'k' + 6: 1, # 'l' + 13: 2, # 'm' + 4: 3, # 'n' + 8: 3, # 'o' + 23: 1, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 3, # 'u' + 19: 3, # 'v' + 62: 0, # 'x' + 16: 2, # 'y' + 11: 2, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 3, # 'í' + 25: 3, # 'ó' + 24: 3, # 'ö' + 31: 2, # 'ú' + 29: 2, # 'ü' + 42: 2, # 'ő' + 56: 1, # 'ű' + }, + 1: { # 'e' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 2, # 'a' + 18: 3, # 'b' + 26: 3, # 'c' + 17: 3, # 'd' + 1: 2, # 'e' + 27: 3, # 'f' + 12: 3, # 'g' + 20: 3, # 'h' + 9: 3, # 'i' + 22: 3, # 'j' + 7: 3, # 'k' + 6: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 8: 2, # 'o' + 23: 3, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 2, # 'u' + 19: 3, # 'v' + 62: 2, # 'x' + 16: 2, # 'y' + 11: 3, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 3, # 'á' + 15: 1, # 'é' + 30: 1, # 'í' + 25: 1, # 'ó' + 24: 1, # 'ö' + 31: 1, # 'ú' + 29: 1, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 27: { # 'f' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 1, # 'b' + 26: 1, # 'c' + 17: 1, # 'd' + 1: 3, # 'e' + 27: 2, # 'f' + 12: 1, # 'g' + 20: 1, # 'h' + 9: 3, # 'i' + 22: 2, # 'j' + 7: 1, # 'k' + 6: 1, # 'l' + 13: 1, # 'm' + 4: 1, # 'n' + 8: 3, # 'o' + 23: 0, # 'p' + 10: 3, # 'r' + 5: 1, # 's' + 3: 1, # 't' + 21: 2, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 0, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 1, # 'í' + 25: 1, # 'ó' + 24: 3, # 'ö' + 31: 1, # 'ú' + 29: 2, # 'ü' + 42: 1, # 'ő' + 56: 1, # 'ű' + }, + 12: { # 'g' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 3, # 'b' + 26: 2, # 'c' + 17: 2, # 'd' + 1: 3, # 'e' + 27: 2, # 'f' + 12: 3, # 'g' + 20: 3, # 'h' + 9: 3, # 'i' + 22: 3, # 'j' + 7: 2, # 'k' + 6: 3, # 'l' + 13: 2, # 'm' + 4: 3, # 'n' + 8: 3, # 'o' + 23: 1, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 3, # 'u' + 19: 3, # 'v' + 62: 0, # 'x' + 16: 3, # 'y' + 11: 2, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 2, # 'í' + 25: 3, # 'ó' + 24: 2, # 'ö' + 31: 2, # 'ú' + 29: 2, # 'ü' + 42: 2, # 'ő' + 56: 1, # 'ű' + }, + 20: { # 'h' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 1, # 'b' + 26: 1, # 'c' + 17: 0, # 'd' + 1: 3, # 'e' + 27: 0, # 'f' + 12: 1, # 'g' + 20: 2, # 'h' + 9: 3, # 'i' + 22: 1, # 'j' + 7: 1, # 'k' + 6: 1, # 'l' + 13: 1, # 'm' + 4: 1, # 'n' + 8: 3, # 'o' + 23: 0, # 'p' + 10: 1, # 'r' + 5: 2, # 's' + 3: 1, # 't' + 21: 3, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 2, # 'y' + 11: 0, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 3, # 'í' + 25: 2, # 'ó' + 24: 2, # 'ö' + 31: 2, # 'ú' + 29: 1, # 'ü' + 42: 1, # 'ő' + 56: 1, # 'ű' + }, + 9: { # 'i' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 3, # 'b' + 26: 3, # 'c' + 17: 3, # 'd' + 1: 3, # 'e' + 27: 3, # 'f' + 12: 3, # 'g' + 20: 3, # 'h' + 9: 2, # 'i' + 22: 2, # 'j' + 7: 3, # 'k' + 6: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 8: 2, # 'o' + 23: 2, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 3, # 'u' + 19: 3, # 'v' + 62: 1, # 'x' + 16: 1, # 'y' + 11: 3, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 3, # 'á' + 15: 2, # 'é' + 30: 1, # 'í' + 25: 3, # 'ó' + 24: 1, # 'ö' + 31: 2, # 'ú' + 29: 1, # 'ü' + 42: 0, # 'ő' + 56: 1, # 'ű' + }, + 22: { # 'j' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 2, # 'b' + 26: 1, # 'c' + 17: 3, # 'd' + 1: 3, # 'e' + 27: 1, # 'f' + 12: 1, # 'g' + 20: 2, # 'h' + 9: 1, # 'i' + 22: 2, # 'j' + 7: 2, # 'k' + 6: 2, # 'l' + 13: 1, # 'm' + 4: 2, # 'n' + 8: 3, # 'o' + 23: 1, # 'p' + 10: 2, # 'r' + 5: 2, # 's' + 3: 3, # 't' + 21: 3, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 2, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 1, # 'í' + 25: 3, # 'ó' + 24: 3, # 'ö' + 31: 3, # 'ú' + 29: 2, # 'ü' + 42: 1, # 'ő' + 56: 1, # 'ű' + }, + 7: { # 'k' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 3, # 'b' + 26: 2, # 'c' + 17: 1, # 'd' + 1: 3, # 'e' + 27: 1, # 'f' + 12: 1, # 'g' + 20: 2, # 'h' + 9: 3, # 'i' + 22: 2, # 'j' + 7: 3, # 'k' + 6: 3, # 'l' + 13: 1, # 'm' + 4: 3, # 'n' + 8: 3, # 'o' + 23: 1, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 3, # 'u' + 19: 2, # 'v' + 62: 0, # 'x' + 16: 2, # 'y' + 11: 1, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 3, # 'í' + 25: 2, # 'ó' + 24: 3, # 'ö' + 31: 1, # 'ú' + 29: 3, # 'ü' + 42: 1, # 'ő' + 56: 1, # 'ű' + }, + 6: { # 'l' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 1, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 1, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 2, # 'b' + 26: 3, # 'c' + 17: 3, # 'd' + 1: 3, # 'e' + 27: 3, # 'f' + 12: 3, # 'g' + 20: 3, # 'h' + 9: 3, # 'i' + 22: 3, # 'j' + 7: 3, # 'k' + 6: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 8: 3, # 'o' + 23: 2, # 'p' + 10: 2, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 3, # 'u' + 19: 3, # 'v' + 62: 0, # 'x' + 16: 3, # 'y' + 11: 2, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 3, # 'í' + 25: 3, # 'ó' + 24: 3, # 'ö' + 31: 2, # 'ú' + 29: 2, # 'ü' + 42: 3, # 'ő' + 56: 1, # 'ű' + }, + 13: { # 'm' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 3, # 'b' + 26: 2, # 'c' + 17: 1, # 'd' + 1: 3, # 'e' + 27: 1, # 'f' + 12: 1, # 'g' + 20: 2, # 'h' + 9: 3, # 'i' + 22: 2, # 'j' + 7: 1, # 'k' + 6: 3, # 'l' + 13: 3, # 'm' + 4: 2, # 'n' + 8: 3, # 'o' + 23: 3, # 'p' + 10: 2, # 'r' + 5: 2, # 's' + 3: 2, # 't' + 21: 3, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 2, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 2, # 'í' + 25: 2, # 'ó' + 24: 2, # 'ö' + 31: 2, # 'ú' + 29: 2, # 'ü' + 42: 1, # 'ő' + 56: 2, # 'ű' + }, + 4: { # 'n' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 3, # 'b' + 26: 3, # 'c' + 17: 3, # 'd' + 1: 3, # 'e' + 27: 2, # 'f' + 12: 3, # 'g' + 20: 3, # 'h' + 9: 3, # 'i' + 22: 2, # 'j' + 7: 3, # 'k' + 6: 2, # 'l' + 13: 2, # 'm' + 4: 3, # 'n' + 8: 3, # 'o' + 23: 2, # 'p' + 10: 2, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 3, # 'u' + 19: 2, # 'v' + 62: 1, # 'x' + 16: 3, # 'y' + 11: 3, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 2, # 'í' + 25: 2, # 'ó' + 24: 3, # 'ö' + 31: 2, # 'ú' + 29: 3, # 'ü' + 42: 2, # 'ő' + 56: 1, # 'ű' + }, + 8: { # 'o' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 1, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 2, # 'a' + 18: 3, # 'b' + 26: 3, # 'c' + 17: 3, # 'd' + 1: 2, # 'e' + 27: 2, # 'f' + 12: 3, # 'g' + 20: 3, # 'h' + 9: 2, # 'i' + 22: 2, # 'j' + 7: 3, # 'k' + 6: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 8: 1, # 'o' + 23: 3, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 2, # 'u' + 19: 3, # 'v' + 62: 1, # 'x' + 16: 1, # 'y' + 11: 3, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 1, # 'á' + 15: 2, # 'é' + 30: 1, # 'í' + 25: 1, # 'ó' + 24: 1, # 'ö' + 31: 1, # 'ú' + 29: 1, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 23: { # 'p' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 1, # 'b' + 26: 2, # 'c' + 17: 1, # 'd' + 1: 3, # 'e' + 27: 1, # 'f' + 12: 1, # 'g' + 20: 2, # 'h' + 9: 3, # 'i' + 22: 2, # 'j' + 7: 2, # 'k' + 6: 3, # 'l' + 13: 1, # 'm' + 4: 2, # 'n' + 8: 3, # 'o' + 23: 3, # 'p' + 10: 3, # 'r' + 5: 2, # 's' + 3: 2, # 't' + 21: 3, # 'u' + 19: 2, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 2, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 2, # 'í' + 25: 2, # 'ó' + 24: 2, # 'ö' + 31: 1, # 'ú' + 29: 2, # 'ü' + 42: 1, # 'ő' + 56: 1, # 'ű' + }, + 10: { # 'r' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 3, # 'b' + 26: 3, # 'c' + 17: 3, # 'd' + 1: 3, # 'e' + 27: 2, # 'f' + 12: 3, # 'g' + 20: 2, # 'h' + 9: 3, # 'i' + 22: 3, # 'j' + 7: 3, # 'k' + 6: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 8: 3, # 'o' + 23: 2, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 3, # 'u' + 19: 3, # 'v' + 62: 1, # 'x' + 16: 2, # 'y' + 11: 3, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 2, # 'í' + 25: 3, # 'ó' + 24: 3, # 'ö' + 31: 3, # 'ú' + 29: 3, # 'ü' + 42: 2, # 'ő' + 56: 2, # 'ű' + }, + 5: { # 's' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 3, # 'b' + 26: 2, # 'c' + 17: 2, # 'd' + 1: 3, # 'e' + 27: 2, # 'f' + 12: 2, # 'g' + 20: 2, # 'h' + 9: 3, # 'i' + 22: 1, # 'j' + 7: 3, # 'k' + 6: 2, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 8: 3, # 'o' + 23: 2, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 3, # 'u' + 19: 2, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 3, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 3, # 'í' + 25: 3, # 'ó' + 24: 3, # 'ö' + 31: 3, # 'ú' + 29: 3, # 'ü' + 42: 2, # 'ő' + 56: 1, # 'ű' + }, + 3: { # 't' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 3, # 'b' + 26: 2, # 'c' + 17: 1, # 'd' + 1: 3, # 'e' + 27: 2, # 'f' + 12: 1, # 'g' + 20: 3, # 'h' + 9: 3, # 'i' + 22: 3, # 'j' + 7: 3, # 'k' + 6: 3, # 'l' + 13: 2, # 'm' + 4: 3, # 'n' + 8: 3, # 'o' + 23: 1, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 3, # 'u' + 19: 3, # 'v' + 62: 0, # 'x' + 16: 3, # 'y' + 11: 1, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 2, # 'í' + 25: 3, # 'ó' + 24: 3, # 'ö' + 31: 3, # 'ú' + 29: 3, # 'ü' + 42: 3, # 'ő' + 56: 2, # 'ű' + }, + 21: { # 'u' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 1, # 'a' + 18: 2, # 'b' + 26: 2, # 'c' + 17: 3, # 'd' + 1: 2, # 'e' + 27: 1, # 'f' + 12: 3, # 'g' + 20: 2, # 'h' + 9: 2, # 'i' + 22: 2, # 'j' + 7: 3, # 'k' + 6: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 8: 1, # 'o' + 23: 2, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 1, # 'u' + 19: 3, # 'v' + 62: 1, # 'x' + 16: 1, # 'y' + 11: 2, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 2, # 'á' + 15: 1, # 'é' + 30: 1, # 'í' + 25: 1, # 'ó' + 24: 0, # 'ö' + 31: 1, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 19: { # 'v' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 2, # 'b' + 26: 1, # 'c' + 17: 1, # 'd' + 1: 3, # 'e' + 27: 1, # 'f' + 12: 1, # 'g' + 20: 1, # 'h' + 9: 3, # 'i' + 22: 1, # 'j' + 7: 1, # 'k' + 6: 1, # 'l' + 13: 1, # 'm' + 4: 1, # 'n' + 8: 3, # 'o' + 23: 1, # 'p' + 10: 1, # 'r' + 5: 2, # 's' + 3: 2, # 't' + 21: 2, # 'u' + 19: 2, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 1, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 2, # 'í' + 25: 2, # 'ó' + 24: 2, # 'ö' + 31: 1, # 'ú' + 29: 2, # 'ü' + 42: 1, # 'ő' + 56: 1, # 'ű' + }, + 62: { # 'x' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 1, # 'a' + 18: 1, # 'b' + 26: 1, # 'c' + 17: 0, # 'd' + 1: 1, # 'e' + 27: 1, # 'f' + 12: 0, # 'g' + 20: 0, # 'h' + 9: 1, # 'i' + 22: 0, # 'j' + 7: 1, # 'k' + 6: 1, # 'l' + 13: 1, # 'm' + 4: 1, # 'n' + 8: 1, # 'o' + 23: 1, # 'p' + 10: 1, # 'r' + 5: 1, # 's' + 3: 1, # 't' + 21: 1, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 0, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 1, # 'á' + 15: 1, # 'é' + 30: 1, # 'í' + 25: 1, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 16: { # 'y' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 2, # 'b' + 26: 1, # 'c' + 17: 1, # 'd' + 1: 3, # 'e' + 27: 2, # 'f' + 12: 2, # 'g' + 20: 2, # 'h' + 9: 3, # 'i' + 22: 2, # 'j' + 7: 2, # 'k' + 6: 2, # 'l' + 13: 2, # 'm' + 4: 3, # 'n' + 8: 3, # 'o' + 23: 2, # 'p' + 10: 2, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 3, # 'u' + 19: 3, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 2, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 2, # 'í' + 25: 2, # 'ó' + 24: 3, # 'ö' + 31: 2, # 'ú' + 29: 2, # 'ü' + 42: 1, # 'ő' + 56: 2, # 'ű' + }, + 11: { # 'z' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 3, # 'a' + 18: 2, # 'b' + 26: 1, # 'c' + 17: 3, # 'd' + 1: 3, # 'e' + 27: 1, # 'f' + 12: 2, # 'g' + 20: 2, # 'h' + 9: 3, # 'i' + 22: 1, # 'j' + 7: 3, # 'k' + 6: 2, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 8: 3, # 'o' + 23: 1, # 'p' + 10: 2, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 3, # 'u' + 19: 2, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 3, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 3, # 'á' + 15: 3, # 'é' + 30: 3, # 'í' + 25: 3, # 'ó' + 24: 3, # 'ö' + 31: 2, # 'ú' + 29: 3, # 'ü' + 42: 2, # 'ő' + 56: 1, # 'ű' + }, + 51: { # 'Á' + 28: 0, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 0, # 'E' + 50: 1, # 'F' + 49: 2, # 'G' + 38: 1, # 'H' + 39: 1, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 2, # 'L' + 34: 1, # 'M' + 35: 2, # 'N' + 47: 0, # 'O' + 46: 1, # 'P' + 43: 2, # 'R' + 33: 2, # 'S' + 37: 1, # 'T' + 57: 0, # 'U' + 48: 1, # 'V' + 55: 0, # 'Y' + 52: 1, # 'Z' + 2: 0, # 'a' + 18: 1, # 'b' + 26: 1, # 'c' + 17: 1, # 'd' + 1: 0, # 'e' + 27: 0, # 'f' + 12: 1, # 'g' + 20: 1, # 'h' + 9: 0, # 'i' + 22: 1, # 'j' + 7: 1, # 'k' + 6: 2, # 'l' + 13: 2, # 'm' + 4: 0, # 'n' + 8: 0, # 'o' + 23: 1, # 'p' + 10: 1, # 'r' + 5: 1, # 's' + 3: 1, # 't' + 21: 0, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 1, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 1, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 44: { # 'É' + 28: 0, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 1, # 'E' + 50: 0, # 'F' + 49: 2, # 'G' + 38: 1, # 'H' + 39: 1, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 2, # 'L' + 34: 1, # 'M' + 35: 2, # 'N' + 47: 0, # 'O' + 46: 1, # 'P' + 43: 2, # 'R' + 33: 2, # 'S' + 37: 2, # 'T' + 57: 0, # 'U' + 48: 1, # 'V' + 55: 0, # 'Y' + 52: 1, # 'Z' + 2: 0, # 'a' + 18: 1, # 'b' + 26: 1, # 'c' + 17: 1, # 'd' + 1: 0, # 'e' + 27: 0, # 'f' + 12: 1, # 'g' + 20: 1, # 'h' + 9: 0, # 'i' + 22: 1, # 'j' + 7: 1, # 'k' + 6: 2, # 'l' + 13: 1, # 'm' + 4: 2, # 'n' + 8: 0, # 'o' + 23: 1, # 'p' + 10: 2, # 'r' + 5: 3, # 's' + 3: 1, # 't' + 21: 0, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 0, # 'z' + 51: 0, # 'Á' + 44: 1, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 61: { # 'Í' + 28: 0, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 0, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 1, # 'J' + 36: 0, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 0, # 'O' + 46: 1, # 'P' + 43: 1, # 'R' + 33: 1, # 'S' + 37: 1, # 'T' + 57: 0, # 'U' + 48: 1, # 'V' + 55: 0, # 'Y' + 52: 1, # 'Z' + 2: 0, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 0, # 'e' + 27: 0, # 'f' + 12: 2, # 'g' + 20: 0, # 'h' + 9: 0, # 'i' + 22: 0, # 'j' + 7: 0, # 'k' + 6: 0, # 'l' + 13: 1, # 'm' + 4: 0, # 'n' + 8: 0, # 'o' + 23: 0, # 'p' + 10: 1, # 'r' + 5: 0, # 's' + 3: 1, # 't' + 21: 0, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 1, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 58: { # 'Ó' + 28: 1, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 0, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 1, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 2, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 0, # 'O' + 46: 1, # 'P' + 43: 1, # 'R' + 33: 1, # 'S' + 37: 1, # 'T' + 57: 0, # 'U' + 48: 1, # 'V' + 55: 0, # 'Y' + 52: 1, # 'Z' + 2: 0, # 'a' + 18: 1, # 'b' + 26: 1, # 'c' + 17: 1, # 'd' + 1: 0, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 2, # 'h' + 9: 0, # 'i' + 22: 0, # 'j' + 7: 1, # 'k' + 6: 1, # 'l' + 13: 0, # 'm' + 4: 1, # 'n' + 8: 0, # 'o' + 23: 1, # 'p' + 10: 1, # 'r' + 5: 1, # 's' + 3: 0, # 't' + 21: 0, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 1, # 'z' + 51: 0, # 'Á' + 44: 1, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 59: { # 'Ö' + 28: 0, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 0, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 0, # 'O' + 46: 1, # 'P' + 43: 1, # 'R' + 33: 1, # 'S' + 37: 1, # 'T' + 57: 0, # 'U' + 48: 1, # 'V' + 55: 0, # 'Y' + 52: 1, # 'Z' + 2: 0, # 'a' + 18: 0, # 'b' + 26: 1, # 'c' + 17: 1, # 'd' + 1: 0, # 'e' + 27: 0, # 'f' + 12: 0, # 'g' + 20: 0, # 'h' + 9: 0, # 'i' + 22: 0, # 'j' + 7: 1, # 'k' + 6: 1, # 'l' + 13: 1, # 'm' + 4: 1, # 'n' + 8: 0, # 'o' + 23: 0, # 'p' + 10: 2, # 'r' + 5: 1, # 's' + 3: 1, # 't' + 21: 0, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 1, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 60: { # 'Ú' + 28: 0, # 'A' + 40: 1, # 'B' + 54: 1, # 'C' + 45: 1, # 'D' + 32: 0, # 'E' + 50: 1, # 'F' + 49: 1, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 1, # 'R' + 33: 1, # 'S' + 37: 1, # 'T' + 57: 0, # 'U' + 48: 1, # 'V' + 55: 0, # 'Y' + 52: 1, # 'Z' + 2: 0, # 'a' + 18: 0, # 'b' + 26: 0, # 'c' + 17: 0, # 'd' + 1: 0, # 'e' + 27: 0, # 'f' + 12: 2, # 'g' + 20: 0, # 'h' + 9: 0, # 'i' + 22: 2, # 'j' + 7: 0, # 'k' + 6: 0, # 'l' + 13: 0, # 'm' + 4: 1, # 'n' + 8: 0, # 'o' + 23: 0, # 'p' + 10: 1, # 'r' + 5: 1, # 's' + 3: 1, # 't' + 21: 0, # 'u' + 19: 0, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 0, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 63: { # 'Ü' + 28: 0, # 'A' + 40: 1, # 'B' + 54: 0, # 'C' + 45: 1, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 1, # 'G' + 38: 1, # 'H' + 39: 0, # 'I' + 53: 1, # 'J' + 36: 1, # 'K' + 41: 1, # 'L' + 34: 1, # 'M' + 35: 1, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 1, # 'R' + 33: 1, # 'S' + 37: 1, # 'T' + 57: 0, # 'U' + 48: 1, # 'V' + 55: 0, # 'Y' + 52: 1, # 'Z' + 2: 0, # 'a' + 18: 1, # 'b' + 26: 0, # 'c' + 17: 1, # 'd' + 1: 0, # 'e' + 27: 0, # 'f' + 12: 1, # 'g' + 20: 0, # 'h' + 9: 0, # 'i' + 22: 0, # 'j' + 7: 0, # 'k' + 6: 1, # 'l' + 13: 0, # 'm' + 4: 1, # 'n' + 8: 0, # 'o' + 23: 0, # 'p' + 10: 1, # 'r' + 5: 1, # 's' + 3: 1, # 't' + 21: 0, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 1, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 14: { # 'á' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 1, # 'a' + 18: 3, # 'b' + 26: 3, # 'c' + 17: 3, # 'd' + 1: 1, # 'e' + 27: 2, # 'f' + 12: 3, # 'g' + 20: 2, # 'h' + 9: 2, # 'i' + 22: 3, # 'j' + 7: 3, # 'k' + 6: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 8: 1, # 'o' + 23: 2, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 2, # 'u' + 19: 3, # 'v' + 62: 0, # 'x' + 16: 1, # 'y' + 11: 3, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 1, # 'á' + 15: 2, # 'é' + 30: 1, # 'í' + 25: 0, # 'ó' + 24: 1, # 'ö' + 31: 0, # 'ú' + 29: 1, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 15: { # 'é' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 1, # 'a' + 18: 3, # 'b' + 26: 2, # 'c' + 17: 3, # 'd' + 1: 1, # 'e' + 27: 1, # 'f' + 12: 3, # 'g' + 20: 3, # 'h' + 9: 2, # 'i' + 22: 2, # 'j' + 7: 3, # 'k' + 6: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 8: 1, # 'o' + 23: 3, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 0, # 'u' + 19: 3, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 3, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 1, # 'á' + 15: 1, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 1, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 30: { # 'í' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 0, # 'a' + 18: 1, # 'b' + 26: 2, # 'c' + 17: 1, # 'd' + 1: 0, # 'e' + 27: 1, # 'f' + 12: 3, # 'g' + 20: 0, # 'h' + 9: 0, # 'i' + 22: 1, # 'j' + 7: 1, # 'k' + 6: 2, # 'l' + 13: 2, # 'm' + 4: 3, # 'n' + 8: 0, # 'o' + 23: 1, # 'p' + 10: 3, # 'r' + 5: 2, # 's' + 3: 3, # 't' + 21: 0, # 'u' + 19: 3, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 2, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 25: { # 'ó' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 2, # 'a' + 18: 3, # 'b' + 26: 2, # 'c' + 17: 3, # 'd' + 1: 1, # 'e' + 27: 2, # 'f' + 12: 2, # 'g' + 20: 2, # 'h' + 9: 2, # 'i' + 22: 2, # 'j' + 7: 3, # 'k' + 6: 3, # 'l' + 13: 2, # 'm' + 4: 3, # 'n' + 8: 1, # 'o' + 23: 2, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 1, # 'u' + 19: 2, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 3, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 1, # 'á' + 15: 1, # 'é' + 30: 1, # 'í' + 25: 0, # 'ó' + 24: 1, # 'ö' + 31: 1, # 'ú' + 29: 1, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 24: { # 'ö' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 0, # 'a' + 18: 3, # 'b' + 26: 1, # 'c' + 17: 2, # 'd' + 1: 0, # 'e' + 27: 1, # 'f' + 12: 2, # 'g' + 20: 1, # 'h' + 9: 0, # 'i' + 22: 1, # 'j' + 7: 3, # 'k' + 6: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 8: 0, # 'o' + 23: 2, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 3, # 't' + 21: 0, # 'u' + 19: 3, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 3, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 31: { # 'ú' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 1, # 'a' + 18: 1, # 'b' + 26: 2, # 'c' + 17: 1, # 'd' + 1: 1, # 'e' + 27: 2, # 'f' + 12: 3, # 'g' + 20: 1, # 'h' + 9: 1, # 'i' + 22: 3, # 'j' + 7: 1, # 'k' + 6: 3, # 'l' + 13: 1, # 'm' + 4: 2, # 'n' + 8: 0, # 'o' + 23: 1, # 'p' + 10: 3, # 'r' + 5: 3, # 's' + 3: 2, # 't' + 21: 1, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 2, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 1, # 'á' + 15: 1, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 29: { # 'ü' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 1, # 'a' + 18: 1, # 'b' + 26: 1, # 'c' + 17: 2, # 'd' + 1: 1, # 'e' + 27: 1, # 'f' + 12: 3, # 'g' + 20: 2, # 'h' + 9: 1, # 'i' + 22: 1, # 'j' + 7: 3, # 'k' + 6: 3, # 'l' + 13: 1, # 'm' + 4: 3, # 'n' + 8: 0, # 'o' + 23: 1, # 'p' + 10: 2, # 'r' + 5: 2, # 's' + 3: 2, # 't' + 21: 0, # 'u' + 19: 2, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 2, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 0, # 'á' + 15: 1, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 42: { # 'ő' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 1, # 'a' + 18: 2, # 'b' + 26: 1, # 'c' + 17: 2, # 'd' + 1: 1, # 'e' + 27: 1, # 'f' + 12: 1, # 'g' + 20: 1, # 'h' + 9: 1, # 'i' + 22: 1, # 'j' + 7: 2, # 'k' + 6: 3, # 'l' + 13: 1, # 'm' + 4: 2, # 'n' + 8: 1, # 'o' + 23: 1, # 'p' + 10: 2, # 'r' + 5: 2, # 's' + 3: 2, # 't' + 21: 1, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 2, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 0, # 'á' + 15: 1, # 'é' + 30: 1, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 1, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, + 56: { # 'ű' + 28: 0, # 'A' + 40: 0, # 'B' + 54: 0, # 'C' + 45: 0, # 'D' + 32: 0, # 'E' + 50: 0, # 'F' + 49: 0, # 'G' + 38: 0, # 'H' + 39: 0, # 'I' + 53: 0, # 'J' + 36: 0, # 'K' + 41: 0, # 'L' + 34: 0, # 'M' + 35: 0, # 'N' + 47: 0, # 'O' + 46: 0, # 'P' + 43: 0, # 'R' + 33: 0, # 'S' + 37: 0, # 'T' + 57: 0, # 'U' + 48: 0, # 'V' + 55: 0, # 'Y' + 52: 0, # 'Z' + 2: 1, # 'a' + 18: 1, # 'b' + 26: 0, # 'c' + 17: 1, # 'd' + 1: 1, # 'e' + 27: 1, # 'f' + 12: 1, # 'g' + 20: 1, # 'h' + 9: 1, # 'i' + 22: 1, # 'j' + 7: 1, # 'k' + 6: 1, # 'l' + 13: 0, # 'm' + 4: 2, # 'n' + 8: 0, # 'o' + 23: 0, # 'p' + 10: 1, # 'r' + 5: 1, # 's' + 3: 1, # 't' + 21: 0, # 'u' + 19: 1, # 'v' + 62: 0, # 'x' + 16: 0, # 'y' + 11: 2, # 'z' + 51: 0, # 'Á' + 44: 0, # 'É' + 61: 0, # 'Í' + 58: 0, # 'Ó' + 59: 0, # 'Ö' + 60: 0, # 'Ú' + 63: 0, # 'Ü' + 14: 0, # 'á' + 15: 0, # 'é' + 30: 0, # 'í' + 25: 0, # 'ó' + 24: 0, # 'ö' + 31: 0, # 'ú' + 29: 0, # 'ü' + 42: 0, # 'ő' + 56: 0, # 'ű' + }, +} + +# 255: Undefined characters that did not exist in training text # 254: Carriage/Return # 253: symbol (punctuation) that does not belong to word # 252: 0 - 9 +# 251: Control characters -# Character Mapping Table: -Latin2_HungarianCharToOrderMap = ( -255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 -253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 -252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 -253, 28, 40, 54, 45, 32, 50, 49, 38, 39, 53, 36, 41, 34, 35, 47, - 46, 71, 43, 33, 37, 57, 48, 64, 68, 55, 52,253,253,253,253,253, -253, 2, 18, 26, 17, 1, 27, 12, 20, 9, 22, 7, 6, 13, 4, 8, - 23, 67, 10, 5, 3, 21, 19, 65, 62, 16, 11,253,253,253,253,253, -159,160,161,162,163,164,165,166,167,168,169,170,171,172,173,174, -175,176,177,178,179,180,181,182,183,184,185,186,187,188,189,190, -191,192,193,194,195,196,197, 75,198,199,200,201,202,203,204,205, - 79,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220, -221, 51, 81,222, 78,223,224,225,226, 44,227,228,229, 61,230,231, -232,233,234, 58,235, 66, 59,236,237,238, 60, 69, 63,239,240,241, - 82, 14, 74,242, 70, 80,243, 72,244, 15, 83, 77, 84, 30, 76, 85, -245,246,247, 25, 73, 42, 24,248,249,250, 31, 56, 29,251,252,253, -) - -win1250HungarianCharToOrderMap = ( -255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 -253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 -252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 -253, 28, 40, 54, 45, 32, 50, 49, 38, 39, 53, 36, 41, 34, 35, 47, - 46, 72, 43, 33, 37, 57, 48, 64, 68, 55, 52,253,253,253,253,253, -253, 2, 18, 26, 17, 1, 27, 12, 20, 9, 22, 7, 6, 13, 4, 8, - 23, 67, 10, 5, 3, 21, 19, 65, 62, 16, 11,253,253,253,253,253, -161,162,163,164,165,166,167,168,169,170,171,172,173,174,175,176, -177,178,179,180, 78,181, 69,182,183,184,185,186,187,188,189,190, -191,192,193,194,195,196,197, 76,198,199,200,201,202,203,204,205, - 81,206,207,208,209,210,211,212,213,214,215,216,217,218,219,220, -221, 51, 83,222, 80,223,224,225,226, 44,227,228,229, 61,230,231, -232,233,234, 58,235, 66, 59,236,237,238, 60, 70, 63,239,240,241, - 84, 14, 75,242, 71, 82,243, 73,244, 15, 85, 79, 86, 30, 77, 87, -245,246,247, 25, 74, 42, 24,248,249,250, 31, 56, 29,251,252,253, -) +# Character Mapping Table(s): +WINDOWS_1250_HUNGARIAN_CHAR_TO_ORDER = { + 0: 255, # '\x00' + 1: 255, # '\x01' + 2: 255, # '\x02' + 3: 255, # '\x03' + 4: 255, # '\x04' + 5: 255, # '\x05' + 6: 255, # '\x06' + 7: 255, # '\x07' + 8: 255, # '\x08' + 9: 255, # '\t' + 10: 254, # '\n' + 11: 255, # '\x0b' + 12: 255, # '\x0c' + 13: 254, # '\r' + 14: 255, # '\x0e' + 15: 255, # '\x0f' + 16: 255, # '\x10' + 17: 255, # '\x11' + 18: 255, # '\x12' + 19: 255, # '\x13' + 20: 255, # '\x14' + 21: 255, # '\x15' + 22: 255, # '\x16' + 23: 255, # '\x17' + 24: 255, # '\x18' + 25: 255, # '\x19' + 26: 255, # '\x1a' + 27: 255, # '\x1b' + 28: 255, # '\x1c' + 29: 255, # '\x1d' + 30: 255, # '\x1e' + 31: 255, # '\x1f' + 32: 253, # ' ' + 33: 253, # '!' + 34: 253, # '"' + 35: 253, # '#' + 36: 253, # '$' + 37: 253, # '%' + 38: 253, # '&' + 39: 253, # "'" + 40: 253, # '(' + 41: 253, # ')' + 42: 253, # '*' + 43: 253, # '+' + 44: 253, # ',' + 45: 253, # '-' + 46: 253, # '.' + 47: 253, # '/' + 48: 252, # '0' + 49: 252, # '1' + 50: 252, # '2' + 51: 252, # '3' + 52: 252, # '4' + 53: 252, # '5' + 54: 252, # '6' + 55: 252, # '7' + 56: 252, # '8' + 57: 252, # '9' + 58: 253, # ':' + 59: 253, # ';' + 60: 253, # '<' + 61: 253, # '=' + 62: 253, # '>' + 63: 253, # '?' + 64: 253, # '@' + 65: 28, # 'A' + 66: 40, # 'B' + 67: 54, # 'C' + 68: 45, # 'D' + 69: 32, # 'E' + 70: 50, # 'F' + 71: 49, # 'G' + 72: 38, # 'H' + 73: 39, # 'I' + 74: 53, # 'J' + 75: 36, # 'K' + 76: 41, # 'L' + 77: 34, # 'M' + 78: 35, # 'N' + 79: 47, # 'O' + 80: 46, # 'P' + 81: 72, # 'Q' + 82: 43, # 'R' + 83: 33, # 'S' + 84: 37, # 'T' + 85: 57, # 'U' + 86: 48, # 'V' + 87: 64, # 'W' + 88: 68, # 'X' + 89: 55, # 'Y' + 90: 52, # 'Z' + 91: 253, # '[' + 92: 253, # '\\' + 93: 253, # ']' + 94: 253, # '^' + 95: 253, # '_' + 96: 253, # '`' + 97: 2, # 'a' + 98: 18, # 'b' + 99: 26, # 'c' + 100: 17, # 'd' + 101: 1, # 'e' + 102: 27, # 'f' + 103: 12, # 'g' + 104: 20, # 'h' + 105: 9, # 'i' + 106: 22, # 'j' + 107: 7, # 'k' + 108: 6, # 'l' + 109: 13, # 'm' + 110: 4, # 'n' + 111: 8, # 'o' + 112: 23, # 'p' + 113: 67, # 'q' + 114: 10, # 'r' + 115: 5, # 's' + 116: 3, # 't' + 117: 21, # 'u' + 118: 19, # 'v' + 119: 65, # 'w' + 120: 62, # 'x' + 121: 16, # 'y' + 122: 11, # 'z' + 123: 253, # '{' + 124: 253, # '|' + 125: 253, # '}' + 126: 253, # '~' + 127: 253, # '\x7f' + 128: 161, # '€' + 129: 162, # None + 130: 163, # '‚' + 131: 164, # None + 132: 165, # '„' + 133: 166, # '…' + 134: 167, # '†' + 135: 168, # '‡' + 136: 169, # None + 137: 170, # '‰' + 138: 171, # 'Š' + 139: 172, # '‹' + 140: 173, # 'Ś' + 141: 174, # 'Ť' + 142: 175, # 'Ž' + 143: 176, # 'Ź' + 144: 177, # None + 145: 178, # '‘' + 146: 179, # '’' + 147: 180, # '“' + 148: 78, # '”' + 149: 181, # '•' + 150: 69, # '–' + 151: 182, # '—' + 152: 183, # None + 153: 184, # '™' + 154: 185, # 'š' + 155: 186, # '›' + 156: 187, # 'ś' + 157: 188, # 'ť' + 158: 189, # 'ž' + 159: 190, # 'ź' + 160: 191, # '\xa0' + 161: 192, # 'ˇ' + 162: 193, # '˘' + 163: 194, # 'Ł' + 164: 195, # '¤' + 165: 196, # 'Ą' + 166: 197, # '¦' + 167: 76, # '§' + 168: 198, # '¨' + 169: 199, # '©' + 170: 200, # 'Ş' + 171: 201, # '«' + 172: 202, # '¬' + 173: 203, # '\xad' + 174: 204, # '®' + 175: 205, # 'Ż' + 176: 81, # '°' + 177: 206, # '±' + 178: 207, # '˛' + 179: 208, # 'ł' + 180: 209, # '´' + 181: 210, # 'µ' + 182: 211, # '¶' + 183: 212, # '·' + 184: 213, # '¸' + 185: 214, # 'ą' + 186: 215, # 'ş' + 187: 216, # '»' + 188: 217, # 'Ľ' + 189: 218, # '˝' + 190: 219, # 'ľ' + 191: 220, # 'ż' + 192: 221, # 'Ŕ' + 193: 51, # 'Á' + 194: 83, # 'Â' + 195: 222, # 'Ă' + 196: 80, # 'Ä' + 197: 223, # 'Ĺ' + 198: 224, # 'Ć' + 199: 225, # 'Ç' + 200: 226, # 'Č' + 201: 44, # 'É' + 202: 227, # 'Ę' + 203: 228, # 'Ë' + 204: 229, # 'Ě' + 205: 61, # 'Í' + 206: 230, # 'Î' + 207: 231, # 'Ď' + 208: 232, # 'Đ' + 209: 233, # 'Ń' + 210: 234, # 'Ň' + 211: 58, # 'Ó' + 212: 235, # 'Ô' + 213: 66, # 'Ő' + 214: 59, # 'Ö' + 215: 236, # '×' + 216: 237, # 'Ř' + 217: 238, # 'Ů' + 218: 60, # 'Ú' + 219: 70, # 'Ű' + 220: 63, # 'Ü' + 221: 239, # 'Ý' + 222: 240, # 'Ţ' + 223: 241, # 'ß' + 224: 84, # 'ŕ' + 225: 14, # 'á' + 226: 75, # 'â' + 227: 242, # 'ă' + 228: 71, # 'ä' + 229: 82, # 'ĺ' + 230: 243, # 'ć' + 231: 73, # 'ç' + 232: 244, # 'č' + 233: 15, # 'é' + 234: 85, # 'ę' + 235: 79, # 'ë' + 236: 86, # 'ě' + 237: 30, # 'í' + 238: 77, # 'î' + 239: 87, # 'ď' + 240: 245, # 'đ' + 241: 246, # 'ń' + 242: 247, # 'ň' + 243: 25, # 'ó' + 244: 74, # 'ô' + 245: 42, # 'ő' + 246: 24, # 'ö' + 247: 248, # '÷' + 248: 249, # 'ř' + 249: 250, # 'ů' + 250: 31, # 'ú' + 251: 56, # 'ű' + 252: 29, # 'ü' + 253: 251, # 'ý' + 254: 252, # 'ţ' + 255: 253, # '˙' +} -# Model Table: -# total sequences: 100% -# first 512 sequences: 94.7368% -# first 1024 sequences:5.2623% -# rest sequences: 0.8894% -# negative sequences: 0.0009% -HungarianLangModel = ( -0,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,1,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3, -3,3,3,3,3,3,3,3,3,3,2,3,3,3,3,3,3,3,3,2,2,3,3,1,1,2,2,2,2,2,1,2, -3,2,2,3,3,3,3,3,2,3,3,3,3,3,3,1,2,3,3,3,3,2,3,3,1,1,3,3,0,1,1,1, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0, -3,2,1,3,3,3,3,3,2,3,3,3,3,3,1,1,2,3,3,3,3,3,3,3,1,1,3,2,0,1,1,1, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0, -3,3,3,3,3,3,3,3,3,3,3,1,1,2,3,3,3,1,3,3,3,3,3,1,3,3,2,2,0,3,2,3, -0,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0, -3,3,3,3,3,3,2,3,3,3,2,3,3,2,3,3,3,3,3,2,3,3,2,2,3,2,3,2,0,3,2,2, -0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0, -3,3,3,3,3,3,2,3,3,3,3,3,2,3,3,3,1,2,3,2,2,3,1,2,3,3,2,2,0,3,3,3, -0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,3,3,3,2,2,3,3,3,3,3,3,2,3,3,3,3,2,3,3,3,3,0,2,3,2, -0,0,0,1,1,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,3,3,3,3,1,1,1,3,3,2,1,3,2,2,3,2,1,3,2,2,1,0,3,3,1, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, -3,2,2,3,3,3,3,3,1,2,3,3,3,3,1,2,1,3,3,3,3,2,2,3,1,1,3,2,0,1,1,1, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0, -3,3,3,3,3,3,3,3,2,2,3,3,3,3,3,2,1,3,3,3,3,3,2,2,1,3,3,3,0,1,1,2, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,1,0, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,2,3,3,3,2,3,3,2,3,3,3,2,0,3,2,3, -0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,1,0, -3,3,3,3,3,3,2,3,3,3,2,3,2,3,3,3,1,3,2,2,2,3,1,1,3,3,1,1,0,3,3,2, -0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,2,3,3,3,2,3,2,3,3,3,2,3,3,3,3,3,1,2,3,2,2,0,2,2,2, -0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, -3,3,3,2,2,2,3,1,3,3,2,2,1,3,3,3,1,1,3,1,2,3,2,3,2,2,2,1,0,2,2,2, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0, -3,1,1,3,3,3,3,3,1,2,3,3,3,3,1,2,1,3,3,3,2,2,3,2,1,0,3,2,0,1,1,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,1,1,3,3,3,3,3,1,2,3,3,3,3,1,1,0,3,3,3,3,0,2,3,0,0,2,1,0,1,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,2,2,3,3,2,2,2,2,3,3,0,1,2,3,2,3,2,2,3,2,1,2,0,2,2,2, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0, -3,3,3,3,3,3,1,2,3,3,3,2,1,2,3,3,2,2,2,3,2,3,3,1,3,3,1,1,0,2,3,2, -0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, -3,3,3,1,2,2,2,2,3,3,3,1,1,1,3,3,1,1,3,1,1,3,2,1,2,3,1,1,0,2,2,2, -0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, -3,3,3,2,1,2,1,1,3,3,1,1,1,1,3,3,1,1,2,2,1,2,1,1,2,2,1,1,0,2,2,1, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, -3,3,3,1,1,2,1,1,3,3,1,0,1,1,3,3,2,0,1,1,2,3,1,0,2,2,1,0,0,1,3,2, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, -3,2,1,3,3,3,3,3,1,2,3,2,3,3,2,1,1,3,2,3,2,1,2,2,0,1,2,1,0,0,1,1, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0, -3,3,3,3,2,2,2,2,3,1,2,2,1,1,3,3,0,3,2,1,2,3,2,1,3,3,1,1,0,2,1,3, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, -3,3,3,2,2,2,3,2,3,3,3,2,1,1,3,3,1,1,1,2,2,3,2,3,2,2,2,1,0,2,2,1, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, -1,0,0,3,3,3,3,3,0,0,3,3,2,3,0,0,0,2,3,3,1,0,1,2,0,0,1,1,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,1,2,3,3,3,3,3,1,2,3,3,2,2,1,1,0,3,3,2,2,1,2,2,1,0,2,2,0,1,1,1, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,2,2,1,3,1,2,3,3,2,2,1,1,2,2,1,1,1,1,3,2,1,1,1,1,2,1,0,1,2,1, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0, -2,3,3,1,1,1,1,1,3,3,3,0,1,1,3,3,1,1,1,1,1,2,2,0,3,1,1,2,0,2,1,1, -0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0, -3,1,0,1,2,1,2,2,0,1,2,3,1,2,0,0,0,2,1,1,1,1,1,2,0,0,1,1,0,0,0,0, -1,2,1,2,2,2,1,2,1,2,0,2,0,2,2,1,1,2,1,1,2,1,1,1,0,1,0,0,0,1,1,0, -1,1,1,2,3,2,3,3,0,1,2,2,3,1,0,1,0,2,1,2,2,0,1,1,0,0,1,1,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,0,0,3,3,2,2,1,0,0,3,2,3,2,0,0,0,1,1,3,0,0,1,1,0,0,2,1,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,1,1,2,2,3,3,1,0,1,3,2,3,1,1,1,0,1,1,1,1,1,3,1,0,0,2,2,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,1,1,1,2,2,2,1,0,1,2,3,3,2,0,0,0,2,1,1,1,2,1,1,1,0,1,1,1,0,0,0, -1,2,2,2,2,2,1,1,1,2,0,2,1,1,1,1,1,2,1,1,1,1,1,1,0,1,1,1,0,0,1,1, -3,2,2,1,0,0,1,1,2,2,0,3,0,1,2,1,1,0,0,1,1,1,0,1,1,1,1,0,2,1,1,1, -2,2,1,1,1,2,1,2,1,1,1,1,1,1,1,2,1,1,1,2,3,1,1,1,1,1,1,1,1,1,0,1, -2,3,3,0,1,0,0,0,3,3,1,0,0,1,2,2,1,0,0,0,0,2,0,0,1,1,1,0,2,1,1,1, -2,1,1,1,1,1,1,2,1,1,0,1,1,0,1,1,1,0,1,2,1,1,0,1,1,1,1,1,1,1,0,1, -2,3,3,0,1,0,0,0,2,2,0,0,0,0,1,2,2,0,0,0,0,1,0,0,1,1,0,0,2,0,1,0, -2,1,1,1,1,2,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,1,1,2,0,1,1,1,1,1,0,1, -3,2,2,0,1,0,1,0,2,3,2,0,0,1,2,2,1,0,0,1,1,1,0,0,2,1,0,1,2,2,1,1, -2,1,1,1,1,1,1,2,1,1,1,1,1,1,0,2,1,0,1,1,0,1,1,1,0,1,1,2,1,1,0,1, -2,2,2,0,0,1,0,0,2,2,1,1,0,0,2,1,1,0,0,0,1,2,0,0,2,1,0,0,2,1,1,1, -2,1,1,1,1,2,1,2,1,1,1,2,2,1,1,2,1,1,1,2,1,1,1,1,1,1,1,1,1,1,0,1, -1,2,3,0,0,0,1,0,3,2,1,0,0,1,2,1,1,0,0,0,0,2,1,0,1,1,0,0,2,1,2,1, -1,1,0,0,0,1,0,1,1,1,1,1,2,0,0,1,0,0,0,2,0,0,1,1,1,1,1,1,1,1,0,1, -3,0,0,2,1,2,2,1,0,0,2,1,2,2,0,0,0,2,1,1,1,0,1,1,0,0,1,1,2,0,0,0, -1,2,1,2,2,1,1,2,1,2,0,1,1,1,1,1,1,1,1,1,2,1,1,0,0,1,1,1,1,0,0,1, -1,3,2,0,0,0,1,0,2,2,2,0,0,0,2,2,1,0,0,0,0,3,1,1,1,1,0,0,2,1,1,1, -2,1,0,1,1,1,0,1,1,1,1,1,1,1,0,2,1,0,0,1,0,1,1,0,1,1,1,1,1,1,0,1, -2,3,2,0,0,0,1,0,2,2,0,0,0,0,2,1,1,0,0,0,0,2,1,0,1,1,0,0,2,1,1,0, -2,1,1,1,1,2,1,2,1,2,0,1,1,1,0,2,1,1,1,2,1,1,1,1,0,1,1,1,1,1,0,1, -3,1,1,2,2,2,3,2,1,1,2,2,1,1,0,1,0,2,2,1,1,1,1,1,0,0,1,1,0,1,1,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,2,2,0,0,0,0,0,2,2,0,0,0,0,2,2,1,0,0,0,1,1,0,0,1,2,0,0,2,1,1,1, -2,2,1,1,1,2,1,2,1,1,0,1,1,1,1,2,1,1,1,2,1,1,1,1,0,1,2,1,1,1,0,1, -1,0,0,1,2,3,2,1,0,0,2,0,1,1,0,0,0,1,1,1,1,0,1,1,0,0,1,0,0,0,0,0, -1,2,1,2,1,2,1,1,1,2,0,2,1,1,1,0,1,2,0,0,1,1,1,0,0,0,0,0,0,0,0,0, -2,3,2,0,0,0,0,0,1,1,2,1,0,0,1,1,1,0,0,0,0,2,0,0,1,1,0,0,2,1,1,1, -2,1,1,1,1,1,1,2,1,0,1,1,1,1,0,2,1,1,1,1,1,1,0,1,0,1,1,1,1,1,0,1, -1,2,2,0,1,1,1,0,2,2,2,0,0,0,3,2,1,0,0,0,1,1,0,0,1,1,0,1,1,1,0,0, -1,1,0,1,1,1,1,1,1,1,1,2,1,1,1,1,1,1,1,2,1,1,1,0,0,1,1,1,0,1,0,1, -2,1,0,2,1,1,2,2,1,1,2,1,1,1,0,0,0,1,1,0,1,1,1,1,0,0,1,1,1,0,0,0, -1,2,2,2,2,2,1,1,1,2,0,2,1,1,1,1,1,1,1,1,1,1,1,1,0,1,1,0,0,0,1,0, -1,2,3,0,0,0,1,0,2,2,0,0,0,0,2,2,0,0,0,0,0,1,0,0,1,0,0,0,2,0,1,0, -2,1,1,1,1,1,0,2,0,0,0,1,2,1,1,1,1,0,1,2,0,1,0,1,0,1,1,1,0,1,0,1, -2,2,2,0,0,0,1,0,2,1,2,0,0,0,1,1,2,0,0,0,0,1,0,0,1,1,0,0,2,1,0,1, -2,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,1,2,0,1,1,1,1,1,0,1, -1,2,2,0,0,0,1,0,2,2,2,0,0,0,1,1,0,0,0,0,0,1,1,0,2,0,0,1,1,1,0,1, -1,0,1,1,1,1,1,1,0,1,1,1,1,0,0,1,0,0,1,1,0,1,0,1,1,1,1,1,0,0,0,1, -1,0,0,1,0,1,2,1,0,0,1,1,1,2,0,0,0,1,1,0,1,0,1,1,0,0,1,0,0,0,0,0, -0,2,1,2,1,1,1,1,1,2,0,2,0,1,1,0,1,2,1,0,1,1,1,0,0,0,0,0,0,1,0,0, -2,1,1,0,1,2,0,0,1,1,1,0,0,0,1,1,0,0,0,0,0,1,0,0,1,0,0,0,2,1,0,1, -2,2,1,1,1,1,1,2,1,1,0,1,1,1,1,2,1,1,1,2,1,1,0,1,0,1,1,1,1,1,0,1, -1,2,2,0,0,0,0,0,1,1,0,0,0,0,2,1,0,0,0,0,0,2,0,0,2,2,0,0,2,0,0,1, -2,1,1,1,1,1,1,1,0,1,1,0,1,1,0,1,0,0,0,1,1,1,1,0,0,1,1,1,1,0,0,1, -1,1,2,0,0,3,1,0,2,1,1,1,0,0,1,1,1,0,0,0,1,1,0,0,0,1,0,0,1,0,1,0, -1,2,1,0,1,1,1,2,1,1,0,1,1,1,1,1,0,0,0,1,1,1,1,1,0,1,0,0,0,1,0,0, -2,1,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,0,1,0,0,0,1,0,0,0,0,2,0,0,0, -2,1,1,1,1,1,1,1,1,1,0,1,1,1,1,1,1,1,1,1,2,1,1,0,0,1,1,1,1,1,0,1, -2,1,1,1,2,1,1,1,0,1,1,2,1,0,0,0,0,1,1,1,1,0,1,0,0,0,0,1,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,1,0,1,1,1,1,1,0,0,1,1,2,1,0,0,0,1,1,0,0,0,1,1,0,0,1,0,1,0,0,0, -1,2,1,1,1,1,1,1,1,1,0,1,0,1,1,1,1,1,1,0,1,1,1,0,0,0,0,0,0,1,0,0, -2,0,0,0,1,1,1,1,0,0,1,1,0,0,0,0,0,1,1,1,2,0,0,1,0,0,1,0,1,0,0,0, -0,1,1,1,1,1,1,1,1,2,0,1,1,1,1,0,1,1,1,0,1,1,1,0,0,0,0,0,0,0,0,0, -1,0,0,1,1,1,1,1,0,0,2,1,0,1,0,0,0,1,0,1,0,0,0,0,0,0,1,0,0,0,0,0, -0,1,1,1,1,1,1,0,1,1,0,1,0,1,1,0,1,1,0,0,1,1,1,0,0,0,0,0,0,0,0,0, -1,0,0,1,1,1,0,0,0,0,1,0,2,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0, -0,1,1,1,1,1,0,0,1,1,0,1,0,1,0,0,1,1,1,0,1,1,1,0,0,0,0,0,0,0,0,0, -0,0,0,1,0,0,0,0,0,0,1,1,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,1,1,1,0,1,0,0,1,1,0,1,0,1,1,0,1,1,1,0,1,1,1,0,0,0,0,0,0,0,0,0, -2,1,1,1,1,1,1,1,1,1,1,0,0,1,1,1,0,0,1,0,0,1,0,1,0,1,1,1,0,0,1,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,0,0,1,1,1,1,0,0,0,1,1,1,0,0,0,0,1,1,1,0,0,0,0,0,0,0,0,0,0,0,0, -0,1,1,1,1,1,1,0,1,1,0,1,0,1,0,0,1,1,0,0,1,1,0,0,0,0,0,0,0,0,0,0, -) +WINDOWS_1250_HUNGARIAN_MODEL = SingleByteCharSetModel(charset_name='windows-1250', + language='Hungarian', + char_to_order_map=WINDOWS_1250_HUNGARIAN_CHAR_TO_ORDER, + language_model=HUNGARIAN_LANG_MODEL, + typical_positive_ratio=0.947368, + keep_ascii_letters=True, + alphabet='ABCDEFGHIJKLMNOPRSTUVZabcdefghijklmnoprstuvzÁÉÍÓÖÚÜáéíóöúüŐőŰű') -Latin2HungarianModel = { - 'char_to_order_map': Latin2_HungarianCharToOrderMap, - 'precedence_matrix': HungarianLangModel, - 'typical_positive_ratio': 0.947368, - 'keep_english_letter': True, - 'charset_name': "ISO-8859-2", - 'language': 'Hungarian', +ISO_8859_2_HUNGARIAN_CHAR_TO_ORDER = { + 0: 255, # '\x00' + 1: 255, # '\x01' + 2: 255, # '\x02' + 3: 255, # '\x03' + 4: 255, # '\x04' + 5: 255, # '\x05' + 6: 255, # '\x06' + 7: 255, # '\x07' + 8: 255, # '\x08' + 9: 255, # '\t' + 10: 254, # '\n' + 11: 255, # '\x0b' + 12: 255, # '\x0c' + 13: 254, # '\r' + 14: 255, # '\x0e' + 15: 255, # '\x0f' + 16: 255, # '\x10' + 17: 255, # '\x11' + 18: 255, # '\x12' + 19: 255, # '\x13' + 20: 255, # '\x14' + 21: 255, # '\x15' + 22: 255, # '\x16' + 23: 255, # '\x17' + 24: 255, # '\x18' + 25: 255, # '\x19' + 26: 255, # '\x1a' + 27: 255, # '\x1b' + 28: 255, # '\x1c' + 29: 255, # '\x1d' + 30: 255, # '\x1e' + 31: 255, # '\x1f' + 32: 253, # ' ' + 33: 253, # '!' + 34: 253, # '"' + 35: 253, # '#' + 36: 253, # '$' + 37: 253, # '%' + 38: 253, # '&' + 39: 253, # "'" + 40: 253, # '(' + 41: 253, # ')' + 42: 253, # '*' + 43: 253, # '+' + 44: 253, # ',' + 45: 253, # '-' + 46: 253, # '.' + 47: 253, # '/' + 48: 252, # '0' + 49: 252, # '1' + 50: 252, # '2' + 51: 252, # '3' + 52: 252, # '4' + 53: 252, # '5' + 54: 252, # '6' + 55: 252, # '7' + 56: 252, # '8' + 57: 252, # '9' + 58: 253, # ':' + 59: 253, # ';' + 60: 253, # '<' + 61: 253, # '=' + 62: 253, # '>' + 63: 253, # '?' + 64: 253, # '@' + 65: 28, # 'A' + 66: 40, # 'B' + 67: 54, # 'C' + 68: 45, # 'D' + 69: 32, # 'E' + 70: 50, # 'F' + 71: 49, # 'G' + 72: 38, # 'H' + 73: 39, # 'I' + 74: 53, # 'J' + 75: 36, # 'K' + 76: 41, # 'L' + 77: 34, # 'M' + 78: 35, # 'N' + 79: 47, # 'O' + 80: 46, # 'P' + 81: 71, # 'Q' + 82: 43, # 'R' + 83: 33, # 'S' + 84: 37, # 'T' + 85: 57, # 'U' + 86: 48, # 'V' + 87: 64, # 'W' + 88: 68, # 'X' + 89: 55, # 'Y' + 90: 52, # 'Z' + 91: 253, # '[' + 92: 253, # '\\' + 93: 253, # ']' + 94: 253, # '^' + 95: 253, # '_' + 96: 253, # '`' + 97: 2, # 'a' + 98: 18, # 'b' + 99: 26, # 'c' + 100: 17, # 'd' + 101: 1, # 'e' + 102: 27, # 'f' + 103: 12, # 'g' + 104: 20, # 'h' + 105: 9, # 'i' + 106: 22, # 'j' + 107: 7, # 'k' + 108: 6, # 'l' + 109: 13, # 'm' + 110: 4, # 'n' + 111: 8, # 'o' + 112: 23, # 'p' + 113: 67, # 'q' + 114: 10, # 'r' + 115: 5, # 's' + 116: 3, # 't' + 117: 21, # 'u' + 118: 19, # 'v' + 119: 65, # 'w' + 120: 62, # 'x' + 121: 16, # 'y' + 122: 11, # 'z' + 123: 253, # '{' + 124: 253, # '|' + 125: 253, # '}' + 126: 253, # '~' + 127: 253, # '\x7f' + 128: 159, # '\x80' + 129: 160, # '\x81' + 130: 161, # '\x82' + 131: 162, # '\x83' + 132: 163, # '\x84' + 133: 164, # '\x85' + 134: 165, # '\x86' + 135: 166, # '\x87' + 136: 167, # '\x88' + 137: 168, # '\x89' + 138: 169, # '\x8a' + 139: 170, # '\x8b' + 140: 171, # '\x8c' + 141: 172, # '\x8d' + 142: 173, # '\x8e' + 143: 174, # '\x8f' + 144: 175, # '\x90' + 145: 176, # '\x91' + 146: 177, # '\x92' + 147: 178, # '\x93' + 148: 179, # '\x94' + 149: 180, # '\x95' + 150: 181, # '\x96' + 151: 182, # '\x97' + 152: 183, # '\x98' + 153: 184, # '\x99' + 154: 185, # '\x9a' + 155: 186, # '\x9b' + 156: 187, # '\x9c' + 157: 188, # '\x9d' + 158: 189, # '\x9e' + 159: 190, # '\x9f' + 160: 191, # '\xa0' + 161: 192, # 'Ą' + 162: 193, # '˘' + 163: 194, # 'Ł' + 164: 195, # '¤' + 165: 196, # 'Ľ' + 166: 197, # 'Ś' + 167: 75, # '§' + 168: 198, # '¨' + 169: 199, # 'Š' + 170: 200, # 'Ş' + 171: 201, # 'Ť' + 172: 202, # 'Ź' + 173: 203, # '\xad' + 174: 204, # 'Ž' + 175: 205, # 'Ż' + 176: 79, # '°' + 177: 206, # 'ą' + 178: 207, # '˛' + 179: 208, # 'ł' + 180: 209, # '´' + 181: 210, # 'ľ' + 182: 211, # 'ś' + 183: 212, # 'ˇ' + 184: 213, # '¸' + 185: 214, # 'š' + 186: 215, # 'ş' + 187: 216, # 'ť' + 188: 217, # 'ź' + 189: 218, # '˝' + 190: 219, # 'ž' + 191: 220, # 'ż' + 192: 221, # 'Ŕ' + 193: 51, # 'Á' + 194: 81, # 'Â' + 195: 222, # 'Ă' + 196: 78, # 'Ä' + 197: 223, # 'Ĺ' + 198: 224, # 'Ć' + 199: 225, # 'Ç' + 200: 226, # 'Č' + 201: 44, # 'É' + 202: 227, # 'Ę' + 203: 228, # 'Ë' + 204: 229, # 'Ě' + 205: 61, # 'Í' + 206: 230, # 'Î' + 207: 231, # 'Ď' + 208: 232, # 'Đ' + 209: 233, # 'Ń' + 210: 234, # 'Ň' + 211: 58, # 'Ó' + 212: 235, # 'Ô' + 213: 66, # 'Ő' + 214: 59, # 'Ö' + 215: 236, # '×' + 216: 237, # 'Ř' + 217: 238, # 'Ů' + 218: 60, # 'Ú' + 219: 69, # 'Ű' + 220: 63, # 'Ü' + 221: 239, # 'Ý' + 222: 240, # 'Ţ' + 223: 241, # 'ß' + 224: 82, # 'ŕ' + 225: 14, # 'á' + 226: 74, # 'â' + 227: 242, # 'ă' + 228: 70, # 'ä' + 229: 80, # 'ĺ' + 230: 243, # 'ć' + 231: 72, # 'ç' + 232: 244, # 'č' + 233: 15, # 'é' + 234: 83, # 'ę' + 235: 77, # 'ë' + 236: 84, # 'ě' + 237: 30, # 'í' + 238: 76, # 'î' + 239: 85, # 'ď' + 240: 245, # 'đ' + 241: 246, # 'ń' + 242: 247, # 'ň' + 243: 25, # 'ó' + 244: 73, # 'ô' + 245: 42, # 'ő' + 246: 24, # 'ö' + 247: 248, # '÷' + 248: 249, # 'ř' + 249: 250, # 'ů' + 250: 31, # 'ú' + 251: 56, # 'ű' + 252: 29, # 'ü' + 253: 251, # 'ý' + 254: 252, # 'ţ' + 255: 253, # '˙' } -Win1250HungarianModel = { - 'char_to_order_map': win1250HungarianCharToOrderMap, - 'precedence_matrix': HungarianLangModel, - 'typical_positive_ratio': 0.947368, - 'keep_english_letter': True, - 'charset_name': "windows-1250", - 'language': 'Hungarian', -} +ISO_8859_2_HUNGARIAN_MODEL = SingleByteCharSetModel(charset_name='ISO-8859-2', + language='Hungarian', + char_to_order_map=ISO_8859_2_HUNGARIAN_CHAR_TO_ORDER, + language_model=HUNGARIAN_LANG_MODEL, + typical_positive_ratio=0.947368, + keep_ascii_letters=True, + alphabet='ABCDEFGHIJKLMNOPRSTUVZabcdefghijklmnoprstuvzÁÉÍÓÖÚÜáéíóöúüŐőŰű') + diff --git a/src/pip/_vendor/chardet/langrussianmodel.py b/src/pip/_vendor/chardet/langrussianmodel.py new file mode 100644 index 00000000000..5594452b55b --- /dev/null +++ b/src/pip/_vendor/chardet/langrussianmodel.py @@ -0,0 +1,5718 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from pip._vendor.chardet.sbcharsetprober import SingleByteCharSetModel + + +# 3: Positive +# 2: Likely +# 1: Unlikely +# 0: Negative + +RUSSIAN_LANG_MODEL = { + 37: { # 'А' + 37: 0, # 'А' + 44: 1, # 'Б' + 33: 1, # 'В' + 46: 1, # 'Г' + 41: 1, # 'Д' + 48: 1, # 'Е' + 56: 1, # 'Ж' + 51: 1, # 'З' + 42: 1, # 'И' + 60: 1, # 'Й' + 36: 1, # 'К' + 49: 1, # 'Л' + 38: 1, # 'М' + 31: 2, # 'Н' + 34: 1, # 'О' + 35: 1, # 'П' + 45: 1, # 'Р' + 32: 1, # 'С' + 40: 1, # 'Т' + 52: 1, # 'У' + 53: 1, # 'Ф' + 55: 1, # 'Х' + 58: 1, # 'Ц' + 50: 1, # 'Ч' + 57: 1, # 'Ш' + 63: 1, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 1, # 'Ю' + 43: 1, # 'Я' + 3: 1, # 'а' + 21: 2, # 'б' + 10: 2, # 'в' + 19: 2, # 'г' + 13: 2, # 'д' + 2: 0, # 'е' + 24: 1, # 'ж' + 20: 1, # 'з' + 4: 0, # 'и' + 23: 1, # 'й' + 11: 2, # 'к' + 8: 3, # 'л' + 12: 2, # 'м' + 5: 2, # 'н' + 1: 0, # 'о' + 15: 2, # 'п' + 9: 2, # 'р' + 7: 2, # 'с' + 6: 2, # 'т' + 14: 2, # 'у' + 39: 2, # 'ф' + 26: 2, # 'х' + 28: 0, # 'ц' + 22: 1, # 'ч' + 25: 2, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 1, # 'э' + 27: 0, # 'ю' + 16: 0, # 'я' + }, + 44: { # 'Б' + 37: 1, # 'А' + 44: 0, # 'Б' + 33: 1, # 'В' + 46: 1, # 'Г' + 41: 0, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 1, # 'Л' + 38: 1, # 'М' + 31: 1, # 'Н' + 34: 1, # 'О' + 35: 0, # 'П' + 45: 1, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 1, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 1, # 'Я' + 3: 2, # 'а' + 21: 0, # 'б' + 10: 0, # 'в' + 19: 0, # 'г' + 13: 1, # 'д' + 2: 3, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 2, # 'л' + 12: 0, # 'м' + 5: 0, # 'н' + 1: 3, # 'о' + 15: 0, # 'п' + 9: 2, # 'р' + 7: 0, # 'с' + 6: 0, # 'т' + 14: 2, # 'у' + 39: 0, # 'ф' + 26: 0, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 2, # 'ы' + 17: 1, # 'ь' + 30: 2, # 'э' + 27: 1, # 'ю' + 16: 1, # 'я' + }, + 33: { # 'В' + 37: 2, # 'А' + 44: 0, # 'Б' + 33: 1, # 'В' + 46: 0, # 'Г' + 41: 1, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 1, # 'К' + 49: 1, # 'Л' + 38: 1, # 'М' + 31: 1, # 'Н' + 34: 1, # 'О' + 35: 1, # 'П' + 45: 1, # 'Р' + 32: 1, # 'С' + 40: 1, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 1, # 'Ш' + 63: 0, # 'Щ' + 62: 1, # 'Ы' + 61: 1, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 1, # 'Я' + 3: 2, # 'а' + 21: 1, # 'б' + 10: 1, # 'в' + 19: 1, # 'г' + 13: 2, # 'д' + 2: 3, # 'е' + 24: 0, # 'ж' + 20: 2, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 1, # 'к' + 8: 2, # 'л' + 12: 2, # 'м' + 5: 2, # 'н' + 1: 3, # 'о' + 15: 2, # 'п' + 9: 2, # 'р' + 7: 3, # 'с' + 6: 2, # 'т' + 14: 2, # 'у' + 39: 0, # 'ф' + 26: 1, # 'х' + 28: 1, # 'ц' + 22: 2, # 'ч' + 25: 1, # 'ш' + 29: 0, # 'щ' + 54: 1, # 'ъ' + 18: 3, # 'ы' + 17: 1, # 'ь' + 30: 2, # 'э' + 27: 0, # 'ю' + 16: 1, # 'я' + }, + 46: { # 'Г' + 37: 1, # 'А' + 44: 1, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 1, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 1, # 'Л' + 38: 1, # 'М' + 31: 1, # 'Н' + 34: 1, # 'О' + 35: 1, # 'П' + 45: 1, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 2, # 'а' + 21: 0, # 'б' + 10: 1, # 'в' + 19: 0, # 'г' + 13: 2, # 'д' + 2: 2, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 2, # 'л' + 12: 1, # 'м' + 5: 1, # 'н' + 1: 3, # 'о' + 15: 0, # 'п' + 9: 2, # 'р' + 7: 0, # 'с' + 6: 0, # 'т' + 14: 2, # 'у' + 39: 0, # 'ф' + 26: 0, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 1, # 'ь' + 30: 1, # 'э' + 27: 1, # 'ю' + 16: 0, # 'я' + }, + 41: { # 'Д' + 37: 1, # 'А' + 44: 0, # 'Б' + 33: 1, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 2, # 'Е' + 56: 1, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 1, # 'К' + 49: 1, # 'Л' + 38: 0, # 'М' + 31: 1, # 'Н' + 34: 1, # 'О' + 35: 0, # 'П' + 45: 1, # 'Р' + 32: 1, # 'С' + 40: 0, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 1, # 'Ц' + 50: 1, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 1, # 'Ы' + 61: 1, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 1, # 'Я' + 3: 3, # 'а' + 21: 0, # 'б' + 10: 2, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 2, # 'е' + 24: 3, # 'ж' + 20: 1, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 2, # 'л' + 12: 1, # 'м' + 5: 1, # 'н' + 1: 3, # 'о' + 15: 0, # 'п' + 9: 2, # 'р' + 7: 0, # 'с' + 6: 0, # 'т' + 14: 2, # 'у' + 39: 0, # 'ф' + 26: 1, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 1, # 'ы' + 17: 1, # 'ь' + 30: 2, # 'э' + 27: 1, # 'ю' + 16: 1, # 'я' + }, + 48: { # 'Е' + 37: 1, # 'А' + 44: 1, # 'Б' + 33: 1, # 'В' + 46: 1, # 'Г' + 41: 1, # 'Д' + 48: 1, # 'Е' + 56: 1, # 'Ж' + 51: 1, # 'З' + 42: 1, # 'И' + 60: 1, # 'Й' + 36: 1, # 'К' + 49: 1, # 'Л' + 38: 1, # 'М' + 31: 2, # 'Н' + 34: 1, # 'О' + 35: 1, # 'П' + 45: 2, # 'Р' + 32: 2, # 'С' + 40: 1, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 1, # 'Х' + 58: 1, # 'Ц' + 50: 1, # 'Ч' + 57: 1, # 'Ш' + 63: 1, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 1, # 'Я' + 3: 0, # 'а' + 21: 0, # 'б' + 10: 2, # 'в' + 19: 2, # 'г' + 13: 2, # 'д' + 2: 2, # 'е' + 24: 1, # 'ж' + 20: 1, # 'з' + 4: 0, # 'и' + 23: 2, # 'й' + 11: 1, # 'к' + 8: 2, # 'л' + 12: 2, # 'м' + 5: 1, # 'н' + 1: 0, # 'о' + 15: 1, # 'п' + 9: 1, # 'р' + 7: 3, # 'с' + 6: 0, # 'т' + 14: 0, # 'у' + 39: 1, # 'ф' + 26: 1, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 1, # 'ш' + 29: 2, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 0, # 'э' + 27: 1, # 'ю' + 16: 0, # 'я' + }, + 56: { # 'Ж' + 37: 1, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 1, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 1, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 1, # 'Н' + 34: 1, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 2, # 'а' + 21: 1, # 'б' + 10: 0, # 'в' + 19: 1, # 'г' + 13: 1, # 'д' + 2: 2, # 'е' + 24: 1, # 'ж' + 20: 0, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 0, # 'л' + 12: 1, # 'м' + 5: 0, # 'н' + 1: 2, # 'о' + 15: 0, # 'п' + 9: 1, # 'р' + 7: 0, # 'с' + 6: 0, # 'т' + 14: 2, # 'у' + 39: 0, # 'ф' + 26: 0, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 0, # 'э' + 27: 2, # 'ю' + 16: 0, # 'я' + }, + 51: { # 'З' + 37: 1, # 'А' + 44: 0, # 'Б' + 33: 1, # 'В' + 46: 1, # 'Г' + 41: 1, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 1, # 'Л' + 38: 1, # 'М' + 31: 1, # 'Н' + 34: 1, # 'О' + 35: 0, # 'П' + 45: 1, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 1, # 'Ы' + 61: 1, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 1, # 'б' + 10: 2, # 'в' + 19: 0, # 'г' + 13: 2, # 'д' + 2: 2, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 1, # 'л' + 12: 1, # 'м' + 5: 2, # 'н' + 1: 2, # 'о' + 15: 0, # 'п' + 9: 1, # 'р' + 7: 0, # 'с' + 6: 0, # 'т' + 14: 1, # 'у' + 39: 0, # 'ф' + 26: 0, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 1, # 'ы' + 17: 0, # 'ь' + 30: 0, # 'э' + 27: 0, # 'ю' + 16: 1, # 'я' + }, + 42: { # 'И' + 37: 1, # 'А' + 44: 1, # 'Б' + 33: 1, # 'В' + 46: 1, # 'Г' + 41: 1, # 'Д' + 48: 2, # 'Е' + 56: 1, # 'Ж' + 51: 1, # 'З' + 42: 1, # 'И' + 60: 1, # 'Й' + 36: 1, # 'К' + 49: 1, # 'Л' + 38: 1, # 'М' + 31: 1, # 'Н' + 34: 1, # 'О' + 35: 1, # 'П' + 45: 1, # 'Р' + 32: 2, # 'С' + 40: 1, # 'Т' + 52: 0, # 'У' + 53: 1, # 'Ф' + 55: 1, # 'Х' + 58: 1, # 'Ц' + 50: 1, # 'Ч' + 57: 0, # 'Ш' + 63: 1, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 1, # 'Ю' + 43: 1, # 'Я' + 3: 1, # 'а' + 21: 2, # 'б' + 10: 2, # 'в' + 19: 2, # 'г' + 13: 2, # 'д' + 2: 2, # 'е' + 24: 0, # 'ж' + 20: 2, # 'з' + 4: 1, # 'и' + 23: 0, # 'й' + 11: 1, # 'к' + 8: 2, # 'л' + 12: 2, # 'м' + 5: 2, # 'н' + 1: 1, # 'о' + 15: 1, # 'п' + 9: 2, # 'р' + 7: 2, # 'с' + 6: 2, # 'т' + 14: 1, # 'у' + 39: 1, # 'ф' + 26: 2, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 1, # 'ш' + 29: 1, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 0, # 'э' + 27: 1, # 'ю' + 16: 0, # 'я' + }, + 60: { # 'Й' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 1, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 1, # 'К' + 49: 1, # 'Л' + 38: 0, # 'М' + 31: 1, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 1, # 'С' + 40: 1, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 1, # 'Х' + 58: 1, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 0, # 'а' + 21: 0, # 'б' + 10: 0, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 1, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 0, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 0, # 'л' + 12: 0, # 'м' + 5: 0, # 'н' + 1: 2, # 'о' + 15: 0, # 'п' + 9: 0, # 'р' + 7: 0, # 'с' + 6: 0, # 'т' + 14: 0, # 'у' + 39: 0, # 'ф' + 26: 0, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 0, # 'э' + 27: 0, # 'ю' + 16: 0, # 'я' + }, + 36: { # 'К' + 37: 2, # 'А' + 44: 0, # 'Б' + 33: 1, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 1, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 1, # 'Л' + 38: 0, # 'М' + 31: 1, # 'Н' + 34: 2, # 'О' + 35: 1, # 'П' + 45: 1, # 'Р' + 32: 1, # 'С' + 40: 1, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 1, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 0, # 'б' + 10: 1, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 2, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 2, # 'л' + 12: 0, # 'м' + 5: 1, # 'н' + 1: 3, # 'о' + 15: 0, # 'п' + 9: 2, # 'р' + 7: 2, # 'с' + 6: 2, # 'т' + 14: 2, # 'у' + 39: 0, # 'ф' + 26: 1, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 1, # 'ы' + 17: 1, # 'ь' + 30: 2, # 'э' + 27: 1, # 'ю' + 16: 0, # 'я' + }, + 49: { # 'Л' + 37: 2, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 1, # 'Г' + 41: 0, # 'Д' + 48: 1, # 'Е' + 56: 1, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 1, # 'К' + 49: 1, # 'Л' + 38: 1, # 'М' + 31: 0, # 'Н' + 34: 1, # 'О' + 35: 1, # 'П' + 45: 0, # 'Р' + 32: 1, # 'С' + 40: 1, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 1, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 1, # 'Ы' + 61: 1, # 'Ь' + 47: 0, # 'Э' + 59: 1, # 'Ю' + 43: 1, # 'Я' + 3: 2, # 'а' + 21: 0, # 'б' + 10: 0, # 'в' + 19: 1, # 'г' + 13: 0, # 'д' + 2: 2, # 'е' + 24: 1, # 'ж' + 20: 0, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 1, # 'л' + 12: 0, # 'м' + 5: 1, # 'н' + 1: 2, # 'о' + 15: 0, # 'п' + 9: 0, # 'р' + 7: 0, # 'с' + 6: 0, # 'т' + 14: 2, # 'у' + 39: 0, # 'ф' + 26: 1, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 1, # 'ы' + 17: 1, # 'ь' + 30: 2, # 'э' + 27: 2, # 'ю' + 16: 1, # 'я' + }, + 38: { # 'М' + 37: 1, # 'А' + 44: 1, # 'Б' + 33: 1, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 1, # 'К' + 49: 1, # 'Л' + 38: 1, # 'М' + 31: 1, # 'Н' + 34: 1, # 'О' + 35: 1, # 'П' + 45: 1, # 'Р' + 32: 1, # 'С' + 40: 1, # 'Т' + 52: 1, # 'У' + 53: 1, # 'Ф' + 55: 1, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 1, # 'Ы' + 61: 0, # 'Ь' + 47: 1, # 'Э' + 59: 0, # 'Ю' + 43: 1, # 'Я' + 3: 3, # 'а' + 21: 0, # 'б' + 10: 0, # 'в' + 19: 1, # 'г' + 13: 0, # 'д' + 2: 2, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 1, # 'л' + 12: 1, # 'м' + 5: 2, # 'н' + 1: 3, # 'о' + 15: 0, # 'п' + 9: 1, # 'р' + 7: 1, # 'с' + 6: 0, # 'т' + 14: 2, # 'у' + 39: 0, # 'ф' + 26: 0, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 3, # 'ы' + 17: 1, # 'ь' + 30: 2, # 'э' + 27: 1, # 'ю' + 16: 1, # 'я' + }, + 31: { # 'Н' + 37: 2, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 1, # 'Г' + 41: 1, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 1, # 'З' + 42: 2, # 'И' + 60: 0, # 'Й' + 36: 1, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 1, # 'Н' + 34: 1, # 'О' + 35: 0, # 'П' + 45: 1, # 'Р' + 32: 1, # 'С' + 40: 1, # 'Т' + 52: 1, # 'У' + 53: 1, # 'Ф' + 55: 1, # 'Х' + 58: 1, # 'Ц' + 50: 1, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 1, # 'Ы' + 61: 1, # 'Ь' + 47: 1, # 'Э' + 59: 0, # 'Ю' + 43: 1, # 'Я' + 3: 3, # 'а' + 21: 0, # 'б' + 10: 0, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 3, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 0, # 'л' + 12: 0, # 'м' + 5: 0, # 'н' + 1: 3, # 'о' + 15: 0, # 'п' + 9: 1, # 'р' + 7: 0, # 'с' + 6: 0, # 'т' + 14: 3, # 'у' + 39: 0, # 'ф' + 26: 1, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 1, # 'ы' + 17: 2, # 'ь' + 30: 1, # 'э' + 27: 1, # 'ю' + 16: 1, # 'я' + }, + 34: { # 'О' + 37: 0, # 'А' + 44: 1, # 'Б' + 33: 1, # 'В' + 46: 1, # 'Г' + 41: 2, # 'Д' + 48: 1, # 'Е' + 56: 1, # 'Ж' + 51: 1, # 'З' + 42: 1, # 'И' + 60: 1, # 'Й' + 36: 1, # 'К' + 49: 2, # 'Л' + 38: 1, # 'М' + 31: 2, # 'Н' + 34: 1, # 'О' + 35: 1, # 'П' + 45: 2, # 'Р' + 32: 1, # 'С' + 40: 1, # 'Т' + 52: 1, # 'У' + 53: 1, # 'Ф' + 55: 1, # 'Х' + 58: 0, # 'Ц' + 50: 1, # 'Ч' + 57: 1, # 'Ш' + 63: 1, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 1, # 'Я' + 3: 1, # 'а' + 21: 2, # 'б' + 10: 1, # 'в' + 19: 2, # 'г' + 13: 2, # 'д' + 2: 0, # 'е' + 24: 1, # 'ж' + 20: 1, # 'з' + 4: 0, # 'и' + 23: 1, # 'й' + 11: 2, # 'к' + 8: 2, # 'л' + 12: 1, # 'м' + 5: 3, # 'н' + 1: 0, # 'о' + 15: 2, # 'п' + 9: 2, # 'р' + 7: 2, # 'с' + 6: 2, # 'т' + 14: 1, # 'у' + 39: 1, # 'ф' + 26: 2, # 'х' + 28: 1, # 'ц' + 22: 2, # 'ч' + 25: 2, # 'ш' + 29: 1, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 0, # 'э' + 27: 0, # 'ю' + 16: 0, # 'я' + }, + 35: { # 'П' + 37: 1, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 1, # 'Л' + 38: 0, # 'М' + 31: 1, # 'Н' + 34: 1, # 'О' + 35: 1, # 'П' + 45: 2, # 'Р' + 32: 1, # 'С' + 40: 1, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 1, # 'Ы' + 61: 1, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 1, # 'Я' + 3: 2, # 'а' + 21: 0, # 'б' + 10: 0, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 2, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 2, # 'л' + 12: 0, # 'м' + 5: 1, # 'н' + 1: 3, # 'о' + 15: 0, # 'п' + 9: 3, # 'р' + 7: 1, # 'с' + 6: 1, # 'т' + 14: 2, # 'у' + 39: 1, # 'ф' + 26: 0, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 1, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 1, # 'ы' + 17: 2, # 'ь' + 30: 1, # 'э' + 27: 0, # 'ю' + 16: 2, # 'я' + }, + 45: { # 'Р' + 37: 2, # 'А' + 44: 1, # 'Б' + 33: 1, # 'В' + 46: 1, # 'Г' + 41: 1, # 'Д' + 48: 2, # 'Е' + 56: 1, # 'Ж' + 51: 0, # 'З' + 42: 2, # 'И' + 60: 0, # 'Й' + 36: 1, # 'К' + 49: 1, # 'Л' + 38: 1, # 'М' + 31: 1, # 'Н' + 34: 2, # 'О' + 35: 0, # 'П' + 45: 1, # 'Р' + 32: 1, # 'С' + 40: 1, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 1, # 'Х' + 58: 1, # 'Ц' + 50: 1, # 'Ч' + 57: 1, # 'Ш' + 63: 0, # 'Щ' + 62: 1, # 'Ы' + 61: 1, # 'Ь' + 47: 1, # 'Э' + 59: 1, # 'Ю' + 43: 1, # 'Я' + 3: 3, # 'а' + 21: 0, # 'б' + 10: 1, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 2, # 'е' + 24: 1, # 'ж' + 20: 0, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 0, # 'л' + 12: 0, # 'м' + 5: 0, # 'н' + 1: 3, # 'о' + 15: 0, # 'п' + 9: 1, # 'р' + 7: 0, # 'с' + 6: 0, # 'т' + 14: 2, # 'у' + 39: 0, # 'ф' + 26: 0, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 2, # 'ы' + 17: 0, # 'ь' + 30: 1, # 'э' + 27: 1, # 'ю' + 16: 2, # 'я' + }, + 32: { # 'С' + 37: 1, # 'А' + 44: 1, # 'Б' + 33: 1, # 'В' + 46: 1, # 'Г' + 41: 1, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 1, # 'К' + 49: 1, # 'Л' + 38: 1, # 'М' + 31: 1, # 'Н' + 34: 1, # 'О' + 35: 1, # 'П' + 45: 1, # 'Р' + 32: 1, # 'С' + 40: 2, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 1, # 'Х' + 58: 1, # 'Ц' + 50: 1, # 'Ч' + 57: 1, # 'Ш' + 63: 0, # 'Щ' + 62: 1, # 'Ы' + 61: 1, # 'Ь' + 47: 1, # 'Э' + 59: 1, # 'Ю' + 43: 1, # 'Я' + 3: 2, # 'а' + 21: 1, # 'б' + 10: 2, # 'в' + 19: 1, # 'г' + 13: 2, # 'д' + 2: 3, # 'е' + 24: 1, # 'ж' + 20: 1, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 2, # 'к' + 8: 2, # 'л' + 12: 2, # 'м' + 5: 2, # 'н' + 1: 2, # 'о' + 15: 2, # 'п' + 9: 2, # 'р' + 7: 1, # 'с' + 6: 3, # 'т' + 14: 2, # 'у' + 39: 1, # 'ф' + 26: 1, # 'х' + 28: 1, # 'ц' + 22: 1, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 1, # 'ъ' + 18: 1, # 'ы' + 17: 1, # 'ь' + 30: 2, # 'э' + 27: 1, # 'ю' + 16: 1, # 'я' + }, + 40: { # 'Т' + 37: 1, # 'А' + 44: 0, # 'Б' + 33: 1, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 1, # 'К' + 49: 1, # 'Л' + 38: 1, # 'М' + 31: 1, # 'Н' + 34: 2, # 'О' + 35: 0, # 'П' + 45: 1, # 'Р' + 32: 1, # 'С' + 40: 1, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 1, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 1, # 'Ы' + 61: 1, # 'Ь' + 47: 1, # 'Э' + 59: 1, # 'Ю' + 43: 1, # 'Я' + 3: 3, # 'а' + 21: 1, # 'б' + 10: 2, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 3, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 1, # 'к' + 8: 1, # 'л' + 12: 0, # 'м' + 5: 0, # 'н' + 1: 3, # 'о' + 15: 0, # 'п' + 9: 2, # 'р' + 7: 1, # 'с' + 6: 0, # 'т' + 14: 2, # 'у' + 39: 0, # 'ф' + 26: 0, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 1, # 'щ' + 54: 0, # 'ъ' + 18: 3, # 'ы' + 17: 1, # 'ь' + 30: 2, # 'э' + 27: 1, # 'ю' + 16: 1, # 'я' + }, + 52: { # 'У' + 37: 1, # 'А' + 44: 1, # 'Б' + 33: 1, # 'В' + 46: 1, # 'Г' + 41: 1, # 'Д' + 48: 1, # 'Е' + 56: 1, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 1, # 'Й' + 36: 1, # 'К' + 49: 1, # 'Л' + 38: 1, # 'М' + 31: 1, # 'Н' + 34: 1, # 'О' + 35: 1, # 'П' + 45: 1, # 'Р' + 32: 1, # 'С' + 40: 1, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 1, # 'Х' + 58: 0, # 'Ц' + 50: 1, # 'Ч' + 57: 1, # 'Ш' + 63: 1, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 1, # 'Ю' + 43: 0, # 'Я' + 3: 1, # 'а' + 21: 2, # 'б' + 10: 2, # 'в' + 19: 1, # 'г' + 13: 2, # 'д' + 2: 1, # 'е' + 24: 2, # 'ж' + 20: 2, # 'з' + 4: 2, # 'и' + 23: 1, # 'й' + 11: 1, # 'к' + 8: 2, # 'л' + 12: 2, # 'м' + 5: 1, # 'н' + 1: 2, # 'о' + 15: 1, # 'п' + 9: 2, # 'р' + 7: 2, # 'с' + 6: 2, # 'т' + 14: 0, # 'у' + 39: 1, # 'ф' + 26: 1, # 'х' + 28: 1, # 'ц' + 22: 2, # 'ч' + 25: 1, # 'ш' + 29: 1, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 2, # 'э' + 27: 1, # 'ю' + 16: 0, # 'я' + }, + 53: { # 'Ф' + 37: 1, # 'А' + 44: 1, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 1, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 1, # 'О' + 35: 0, # 'П' + 45: 1, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 2, # 'а' + 21: 0, # 'б' + 10: 0, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 2, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 2, # 'л' + 12: 0, # 'м' + 5: 0, # 'н' + 1: 2, # 'о' + 15: 0, # 'п' + 9: 2, # 'р' + 7: 0, # 'с' + 6: 1, # 'т' + 14: 2, # 'у' + 39: 0, # 'ф' + 26: 0, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 1, # 'ь' + 30: 2, # 'э' + 27: 0, # 'ю' + 16: 0, # 'я' + }, + 55: { # 'Х' + 37: 1, # 'А' + 44: 0, # 'Б' + 33: 1, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 1, # 'Л' + 38: 1, # 'М' + 31: 1, # 'Н' + 34: 1, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 2, # 'а' + 21: 0, # 'б' + 10: 2, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 2, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 2, # 'л' + 12: 1, # 'м' + 5: 0, # 'н' + 1: 2, # 'о' + 15: 0, # 'п' + 9: 2, # 'р' + 7: 0, # 'с' + 6: 0, # 'т' + 14: 1, # 'у' + 39: 0, # 'ф' + 26: 0, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 1, # 'ь' + 30: 1, # 'э' + 27: 0, # 'ю' + 16: 0, # 'я' + }, + 58: { # 'Ц' + 37: 1, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 1, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 1, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 1, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 1, # 'а' + 21: 0, # 'б' + 10: 1, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 2, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 0, # 'л' + 12: 0, # 'м' + 5: 0, # 'н' + 1: 0, # 'о' + 15: 0, # 'п' + 9: 0, # 'р' + 7: 0, # 'с' + 6: 0, # 'т' + 14: 1, # 'у' + 39: 0, # 'ф' + 26: 0, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 1, # 'ы' + 17: 0, # 'ь' + 30: 0, # 'э' + 27: 1, # 'ю' + 16: 0, # 'я' + }, + 50: { # 'Ч' + 37: 1, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 1, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 1, # 'Н' + 34: 0, # 'О' + 35: 1, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 1, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 1, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 2, # 'а' + 21: 0, # 'б' + 10: 0, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 2, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 1, # 'л' + 12: 0, # 'м' + 5: 0, # 'н' + 1: 1, # 'о' + 15: 0, # 'п' + 9: 1, # 'р' + 7: 0, # 'с' + 6: 3, # 'т' + 14: 2, # 'у' + 39: 0, # 'ф' + 26: 0, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 1, # 'ь' + 30: 0, # 'э' + 27: 0, # 'ю' + 16: 0, # 'я' + }, + 57: { # 'Ш' + 37: 1, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 1, # 'К' + 49: 1, # 'Л' + 38: 0, # 'М' + 31: 1, # 'Н' + 34: 1, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 1, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 2, # 'а' + 21: 0, # 'б' + 10: 1, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 2, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 1, # 'и' + 23: 0, # 'й' + 11: 1, # 'к' + 8: 2, # 'л' + 12: 1, # 'м' + 5: 1, # 'н' + 1: 2, # 'о' + 15: 2, # 'п' + 9: 1, # 'р' + 7: 0, # 'с' + 6: 2, # 'т' + 14: 2, # 'у' + 39: 0, # 'ф' + 26: 1, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 1, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 1, # 'э' + 27: 0, # 'ю' + 16: 0, # 'я' + }, + 63: { # 'Щ' + 37: 1, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 1, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 1, # 'а' + 21: 0, # 'б' + 10: 0, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 1, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 1, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 0, # 'л' + 12: 0, # 'м' + 5: 0, # 'н' + 1: 1, # 'о' + 15: 0, # 'п' + 9: 0, # 'р' + 7: 0, # 'с' + 6: 0, # 'т' + 14: 1, # 'у' + 39: 0, # 'ф' + 26: 0, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 0, # 'э' + 27: 0, # 'ю' + 16: 0, # 'я' + }, + 62: { # 'Ы' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 1, # 'В' + 46: 1, # 'Г' + 41: 0, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 1, # 'Й' + 36: 1, # 'К' + 49: 1, # 'Л' + 38: 1, # 'М' + 31: 1, # 'Н' + 34: 0, # 'О' + 35: 1, # 'П' + 45: 1, # 'Р' + 32: 1, # 'С' + 40: 1, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 1, # 'Х' + 58: 1, # 'Ц' + 50: 0, # 'Ч' + 57: 1, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 0, # 'а' + 21: 0, # 'б' + 10: 0, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 0, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 0, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 0, # 'л' + 12: 0, # 'м' + 5: 0, # 'н' + 1: 0, # 'о' + 15: 0, # 'п' + 9: 0, # 'р' + 7: 0, # 'с' + 6: 0, # 'т' + 14: 0, # 'у' + 39: 0, # 'ф' + 26: 0, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 0, # 'э' + 27: 0, # 'ю' + 16: 0, # 'я' + }, + 61: { # 'Ь' + 37: 0, # 'А' + 44: 1, # 'Б' + 33: 1, # 'В' + 46: 0, # 'Г' + 41: 1, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 1, # 'К' + 49: 0, # 'Л' + 38: 1, # 'М' + 31: 1, # 'Н' + 34: 1, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 1, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 1, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 1, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 1, # 'Ю' + 43: 1, # 'Я' + 3: 0, # 'а' + 21: 0, # 'б' + 10: 0, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 0, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 0, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 0, # 'л' + 12: 0, # 'м' + 5: 0, # 'н' + 1: 0, # 'о' + 15: 0, # 'п' + 9: 0, # 'р' + 7: 0, # 'с' + 6: 0, # 'т' + 14: 0, # 'у' + 39: 0, # 'ф' + 26: 0, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 0, # 'э' + 27: 0, # 'ю' + 16: 0, # 'я' + }, + 47: { # 'Э' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 1, # 'В' + 46: 0, # 'Г' + 41: 1, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 1, # 'Й' + 36: 1, # 'К' + 49: 1, # 'Л' + 38: 1, # 'М' + 31: 1, # 'Н' + 34: 0, # 'О' + 35: 1, # 'П' + 45: 1, # 'Р' + 32: 1, # 'С' + 40: 1, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 1, # 'а' + 21: 1, # 'б' + 10: 2, # 'в' + 19: 1, # 'г' + 13: 2, # 'д' + 2: 0, # 'е' + 24: 1, # 'ж' + 20: 0, # 'з' + 4: 0, # 'и' + 23: 2, # 'й' + 11: 2, # 'к' + 8: 2, # 'л' + 12: 2, # 'м' + 5: 2, # 'н' + 1: 0, # 'о' + 15: 1, # 'п' + 9: 2, # 'р' + 7: 1, # 'с' + 6: 3, # 'т' + 14: 1, # 'у' + 39: 1, # 'ф' + 26: 1, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 1, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 0, # 'э' + 27: 0, # 'ю' + 16: 0, # 'я' + }, + 59: { # 'Ю' + 37: 1, # 'А' + 44: 1, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 1, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 1, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 1, # 'Р' + 32: 0, # 'С' + 40: 1, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 1, # 'Ч' + 57: 0, # 'Ш' + 63: 1, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 0, # 'а' + 21: 1, # 'б' + 10: 0, # 'в' + 19: 1, # 'г' + 13: 1, # 'д' + 2: 0, # 'е' + 24: 1, # 'ж' + 20: 0, # 'з' + 4: 0, # 'и' + 23: 0, # 'й' + 11: 1, # 'к' + 8: 2, # 'л' + 12: 1, # 'м' + 5: 2, # 'н' + 1: 0, # 'о' + 15: 1, # 'п' + 9: 1, # 'р' + 7: 1, # 'с' + 6: 0, # 'т' + 14: 0, # 'у' + 39: 0, # 'ф' + 26: 1, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 0, # 'э' + 27: 0, # 'ю' + 16: 0, # 'я' + }, + 43: { # 'Я' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 1, # 'В' + 46: 1, # 'Г' + 41: 0, # 'Д' + 48: 1, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 1, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 1, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 1, # 'С' + 40: 1, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 1, # 'Х' + 58: 0, # 'Ц' + 50: 1, # 'Ч' + 57: 0, # 'Ш' + 63: 1, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 1, # 'Ю' + 43: 1, # 'Я' + 3: 0, # 'а' + 21: 1, # 'б' + 10: 1, # 'в' + 19: 1, # 'г' + 13: 1, # 'д' + 2: 0, # 'е' + 24: 0, # 'ж' + 20: 1, # 'з' + 4: 0, # 'и' + 23: 1, # 'й' + 11: 1, # 'к' + 8: 1, # 'л' + 12: 1, # 'м' + 5: 2, # 'н' + 1: 0, # 'о' + 15: 1, # 'п' + 9: 1, # 'р' + 7: 1, # 'с' + 6: 0, # 'т' + 14: 0, # 'у' + 39: 0, # 'ф' + 26: 1, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 1, # 'ш' + 29: 1, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 0, # 'э' + 27: 0, # 'ю' + 16: 0, # 'я' + }, + 3: { # 'а' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 1, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 1, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 2, # 'а' + 21: 3, # 'б' + 10: 3, # 'в' + 19: 3, # 'г' + 13: 3, # 'д' + 2: 3, # 'е' + 24: 3, # 'ж' + 20: 3, # 'з' + 4: 3, # 'и' + 23: 3, # 'й' + 11: 3, # 'к' + 8: 3, # 'л' + 12: 3, # 'м' + 5: 3, # 'н' + 1: 2, # 'о' + 15: 3, # 'п' + 9: 3, # 'р' + 7: 3, # 'с' + 6: 3, # 'т' + 14: 3, # 'у' + 39: 2, # 'ф' + 26: 3, # 'х' + 28: 3, # 'ц' + 22: 3, # 'ч' + 25: 3, # 'ш' + 29: 3, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 2, # 'э' + 27: 3, # 'ю' + 16: 3, # 'я' + }, + 21: { # 'б' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 1, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 2, # 'б' + 10: 2, # 'в' + 19: 1, # 'г' + 13: 2, # 'д' + 2: 3, # 'е' + 24: 2, # 'ж' + 20: 1, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 2, # 'к' + 8: 3, # 'л' + 12: 2, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 1, # 'п' + 9: 3, # 'р' + 7: 3, # 'с' + 6: 2, # 'т' + 14: 3, # 'у' + 39: 0, # 'ф' + 26: 2, # 'х' + 28: 1, # 'ц' + 22: 1, # 'ч' + 25: 2, # 'ш' + 29: 3, # 'щ' + 54: 2, # 'ъ' + 18: 3, # 'ы' + 17: 2, # 'ь' + 30: 1, # 'э' + 27: 2, # 'ю' + 16: 3, # 'я' + }, + 10: { # 'в' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 2, # 'б' + 10: 2, # 'в' + 19: 2, # 'г' + 13: 3, # 'д' + 2: 3, # 'е' + 24: 1, # 'ж' + 20: 3, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 3, # 'к' + 8: 3, # 'л' + 12: 2, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 3, # 'п' + 9: 3, # 'р' + 7: 3, # 'с' + 6: 3, # 'т' + 14: 3, # 'у' + 39: 1, # 'ф' + 26: 2, # 'х' + 28: 2, # 'ц' + 22: 2, # 'ч' + 25: 3, # 'ш' + 29: 2, # 'щ' + 54: 2, # 'ъ' + 18: 3, # 'ы' + 17: 3, # 'ь' + 30: 1, # 'э' + 27: 1, # 'ю' + 16: 3, # 'я' + }, + 19: { # 'г' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 1, # 'б' + 10: 2, # 'в' + 19: 1, # 'г' + 13: 3, # 'д' + 2: 3, # 'е' + 24: 0, # 'ж' + 20: 1, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 2, # 'к' + 8: 3, # 'л' + 12: 2, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 0, # 'п' + 9: 3, # 'р' + 7: 2, # 'с' + 6: 2, # 'т' + 14: 3, # 'у' + 39: 1, # 'ф' + 26: 1, # 'х' + 28: 1, # 'ц' + 22: 2, # 'ч' + 25: 1, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 1, # 'ы' + 17: 1, # 'ь' + 30: 1, # 'э' + 27: 1, # 'ю' + 16: 0, # 'я' + }, + 13: { # 'д' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 2, # 'б' + 10: 3, # 'в' + 19: 2, # 'г' + 13: 2, # 'д' + 2: 3, # 'е' + 24: 2, # 'ж' + 20: 2, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 3, # 'к' + 8: 3, # 'л' + 12: 2, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 2, # 'п' + 9: 3, # 'р' + 7: 3, # 'с' + 6: 3, # 'т' + 14: 3, # 'у' + 39: 1, # 'ф' + 26: 2, # 'х' + 28: 3, # 'ц' + 22: 2, # 'ч' + 25: 2, # 'ш' + 29: 1, # 'щ' + 54: 2, # 'ъ' + 18: 3, # 'ы' + 17: 3, # 'ь' + 30: 1, # 'э' + 27: 2, # 'ю' + 16: 3, # 'я' + }, + 2: { # 'е' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 2, # 'а' + 21: 3, # 'б' + 10: 3, # 'в' + 19: 3, # 'г' + 13: 3, # 'д' + 2: 3, # 'е' + 24: 3, # 'ж' + 20: 3, # 'з' + 4: 2, # 'и' + 23: 3, # 'й' + 11: 3, # 'к' + 8: 3, # 'л' + 12: 3, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 3, # 'п' + 9: 3, # 'р' + 7: 3, # 'с' + 6: 3, # 'т' + 14: 2, # 'у' + 39: 2, # 'ф' + 26: 3, # 'х' + 28: 3, # 'ц' + 22: 3, # 'ч' + 25: 3, # 'ш' + 29: 3, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 1, # 'э' + 27: 2, # 'ю' + 16: 3, # 'я' + }, + 24: { # 'ж' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 2, # 'б' + 10: 1, # 'в' + 19: 2, # 'г' + 13: 3, # 'д' + 2: 3, # 'е' + 24: 2, # 'ж' + 20: 1, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 2, # 'к' + 8: 2, # 'л' + 12: 1, # 'м' + 5: 3, # 'н' + 1: 2, # 'о' + 15: 1, # 'п' + 9: 2, # 'р' + 7: 2, # 'с' + 6: 1, # 'т' + 14: 3, # 'у' + 39: 1, # 'ф' + 26: 0, # 'х' + 28: 1, # 'ц' + 22: 2, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 1, # 'ы' + 17: 2, # 'ь' + 30: 1, # 'э' + 27: 1, # 'ю' + 16: 1, # 'я' + }, + 20: { # 'з' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 3, # 'б' + 10: 3, # 'в' + 19: 3, # 'г' + 13: 3, # 'д' + 2: 3, # 'е' + 24: 2, # 'ж' + 20: 2, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 3, # 'к' + 8: 3, # 'л' + 12: 3, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 0, # 'п' + 9: 3, # 'р' + 7: 2, # 'с' + 6: 2, # 'т' + 14: 3, # 'у' + 39: 0, # 'ф' + 26: 0, # 'х' + 28: 1, # 'ц' + 22: 2, # 'ч' + 25: 1, # 'ш' + 29: 0, # 'щ' + 54: 2, # 'ъ' + 18: 3, # 'ы' + 17: 2, # 'ь' + 30: 1, # 'э' + 27: 1, # 'ю' + 16: 3, # 'я' + }, + 4: { # 'и' + 37: 1, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 1, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 3, # 'б' + 10: 3, # 'в' + 19: 3, # 'г' + 13: 3, # 'д' + 2: 3, # 'е' + 24: 3, # 'ж' + 20: 3, # 'з' + 4: 3, # 'и' + 23: 3, # 'й' + 11: 3, # 'к' + 8: 3, # 'л' + 12: 3, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 3, # 'п' + 9: 3, # 'р' + 7: 3, # 'с' + 6: 3, # 'т' + 14: 2, # 'у' + 39: 2, # 'ф' + 26: 3, # 'х' + 28: 3, # 'ц' + 22: 3, # 'ч' + 25: 3, # 'ш' + 29: 3, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 2, # 'э' + 27: 3, # 'ю' + 16: 3, # 'я' + }, + 23: { # 'й' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 1, # 'а' + 21: 1, # 'б' + 10: 1, # 'в' + 19: 2, # 'г' + 13: 3, # 'д' + 2: 2, # 'е' + 24: 0, # 'ж' + 20: 2, # 'з' + 4: 1, # 'и' + 23: 0, # 'й' + 11: 2, # 'к' + 8: 2, # 'л' + 12: 2, # 'м' + 5: 3, # 'н' + 1: 2, # 'о' + 15: 1, # 'п' + 9: 2, # 'р' + 7: 3, # 'с' + 6: 3, # 'т' + 14: 1, # 'у' + 39: 2, # 'ф' + 26: 1, # 'х' + 28: 2, # 'ц' + 22: 3, # 'ч' + 25: 2, # 'ш' + 29: 1, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 1, # 'э' + 27: 1, # 'ю' + 16: 2, # 'я' + }, + 11: { # 'к' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 1, # 'б' + 10: 3, # 'в' + 19: 1, # 'г' + 13: 1, # 'д' + 2: 3, # 'е' + 24: 2, # 'ж' + 20: 2, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 2, # 'к' + 8: 3, # 'л' + 12: 1, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 0, # 'п' + 9: 3, # 'р' + 7: 3, # 'с' + 6: 3, # 'т' + 14: 3, # 'у' + 39: 1, # 'ф' + 26: 2, # 'х' + 28: 2, # 'ц' + 22: 1, # 'ч' + 25: 2, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 1, # 'ы' + 17: 1, # 'ь' + 30: 1, # 'э' + 27: 1, # 'ю' + 16: 1, # 'я' + }, + 8: { # 'л' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 2, # 'б' + 10: 2, # 'в' + 19: 3, # 'г' + 13: 2, # 'д' + 2: 3, # 'е' + 24: 3, # 'ж' + 20: 2, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 3, # 'к' + 8: 3, # 'л' + 12: 2, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 2, # 'п' + 9: 1, # 'р' + 7: 3, # 'с' + 6: 2, # 'т' + 14: 3, # 'у' + 39: 2, # 'ф' + 26: 2, # 'х' + 28: 1, # 'ц' + 22: 3, # 'ч' + 25: 2, # 'ш' + 29: 1, # 'щ' + 54: 0, # 'ъ' + 18: 3, # 'ы' + 17: 3, # 'ь' + 30: 1, # 'э' + 27: 3, # 'ю' + 16: 3, # 'я' + }, + 12: { # 'м' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 2, # 'б' + 10: 2, # 'в' + 19: 2, # 'г' + 13: 1, # 'д' + 2: 3, # 'е' + 24: 1, # 'ж' + 20: 1, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 2, # 'к' + 8: 3, # 'л' + 12: 2, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 2, # 'п' + 9: 2, # 'р' + 7: 3, # 'с' + 6: 2, # 'т' + 14: 3, # 'у' + 39: 2, # 'ф' + 26: 2, # 'х' + 28: 2, # 'ц' + 22: 2, # 'ч' + 25: 1, # 'ш' + 29: 1, # 'щ' + 54: 0, # 'ъ' + 18: 3, # 'ы' + 17: 2, # 'ь' + 30: 2, # 'э' + 27: 1, # 'ю' + 16: 3, # 'я' + }, + 5: { # 'н' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 2, # 'б' + 10: 2, # 'в' + 19: 3, # 'г' + 13: 3, # 'д' + 2: 3, # 'е' + 24: 2, # 'ж' + 20: 2, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 3, # 'к' + 8: 2, # 'л' + 12: 1, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 1, # 'п' + 9: 2, # 'р' + 7: 3, # 'с' + 6: 3, # 'т' + 14: 3, # 'у' + 39: 2, # 'ф' + 26: 2, # 'х' + 28: 3, # 'ц' + 22: 3, # 'ч' + 25: 2, # 'ш' + 29: 2, # 'щ' + 54: 1, # 'ъ' + 18: 3, # 'ы' + 17: 3, # 'ь' + 30: 1, # 'э' + 27: 3, # 'ю' + 16: 3, # 'я' + }, + 1: { # 'о' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 2, # 'а' + 21: 3, # 'б' + 10: 3, # 'в' + 19: 3, # 'г' + 13: 3, # 'д' + 2: 3, # 'е' + 24: 3, # 'ж' + 20: 3, # 'з' + 4: 3, # 'и' + 23: 3, # 'й' + 11: 3, # 'к' + 8: 3, # 'л' + 12: 3, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 3, # 'п' + 9: 3, # 'р' + 7: 3, # 'с' + 6: 3, # 'т' + 14: 2, # 'у' + 39: 2, # 'ф' + 26: 3, # 'х' + 28: 2, # 'ц' + 22: 3, # 'ч' + 25: 3, # 'ш' + 29: 3, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 2, # 'э' + 27: 3, # 'ю' + 16: 3, # 'я' + }, + 15: { # 'п' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 1, # 'б' + 10: 0, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 3, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 2, # 'к' + 8: 3, # 'л' + 12: 1, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 2, # 'п' + 9: 3, # 'р' + 7: 2, # 'с' + 6: 2, # 'т' + 14: 3, # 'у' + 39: 1, # 'ф' + 26: 0, # 'х' + 28: 2, # 'ц' + 22: 2, # 'ч' + 25: 1, # 'ш' + 29: 1, # 'щ' + 54: 0, # 'ъ' + 18: 3, # 'ы' + 17: 2, # 'ь' + 30: 1, # 'э' + 27: 1, # 'ю' + 16: 3, # 'я' + }, + 9: { # 'р' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 2, # 'б' + 10: 3, # 'в' + 19: 3, # 'г' + 13: 3, # 'д' + 2: 3, # 'е' + 24: 3, # 'ж' + 20: 2, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 3, # 'к' + 8: 2, # 'л' + 12: 3, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 2, # 'п' + 9: 2, # 'р' + 7: 3, # 'с' + 6: 3, # 'т' + 14: 3, # 'у' + 39: 2, # 'ф' + 26: 3, # 'х' + 28: 2, # 'ц' + 22: 2, # 'ч' + 25: 3, # 'ш' + 29: 2, # 'щ' + 54: 0, # 'ъ' + 18: 3, # 'ы' + 17: 3, # 'ь' + 30: 2, # 'э' + 27: 2, # 'ю' + 16: 3, # 'я' + }, + 7: { # 'с' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 1, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 2, # 'б' + 10: 3, # 'в' + 19: 2, # 'г' + 13: 3, # 'д' + 2: 3, # 'е' + 24: 2, # 'ж' + 20: 2, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 3, # 'к' + 8: 3, # 'л' + 12: 3, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 3, # 'п' + 9: 3, # 'р' + 7: 3, # 'с' + 6: 3, # 'т' + 14: 3, # 'у' + 39: 2, # 'ф' + 26: 3, # 'х' + 28: 2, # 'ц' + 22: 3, # 'ч' + 25: 2, # 'ш' + 29: 1, # 'щ' + 54: 2, # 'ъ' + 18: 3, # 'ы' + 17: 3, # 'ь' + 30: 2, # 'э' + 27: 3, # 'ю' + 16: 3, # 'я' + }, + 6: { # 'т' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 2, # 'б' + 10: 3, # 'в' + 19: 2, # 'г' + 13: 2, # 'д' + 2: 3, # 'е' + 24: 1, # 'ж' + 20: 1, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 3, # 'к' + 8: 3, # 'л' + 12: 2, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 2, # 'п' + 9: 3, # 'р' + 7: 3, # 'с' + 6: 2, # 'т' + 14: 3, # 'у' + 39: 2, # 'ф' + 26: 2, # 'х' + 28: 2, # 'ц' + 22: 2, # 'ч' + 25: 2, # 'ш' + 29: 2, # 'щ' + 54: 2, # 'ъ' + 18: 3, # 'ы' + 17: 3, # 'ь' + 30: 2, # 'э' + 27: 2, # 'ю' + 16: 3, # 'я' + }, + 14: { # 'у' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 2, # 'а' + 21: 3, # 'б' + 10: 3, # 'в' + 19: 3, # 'г' + 13: 3, # 'д' + 2: 3, # 'е' + 24: 3, # 'ж' + 20: 3, # 'з' + 4: 2, # 'и' + 23: 2, # 'й' + 11: 3, # 'к' + 8: 3, # 'л' + 12: 3, # 'м' + 5: 3, # 'н' + 1: 2, # 'о' + 15: 3, # 'п' + 9: 3, # 'р' + 7: 3, # 'с' + 6: 3, # 'т' + 14: 1, # 'у' + 39: 2, # 'ф' + 26: 3, # 'х' + 28: 2, # 'ц' + 22: 3, # 'ч' + 25: 3, # 'ш' + 29: 3, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 2, # 'э' + 27: 3, # 'ю' + 16: 2, # 'я' + }, + 39: { # 'ф' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 1, # 'б' + 10: 0, # 'в' + 19: 1, # 'г' + 13: 0, # 'д' + 2: 3, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 1, # 'к' + 8: 2, # 'л' + 12: 1, # 'м' + 5: 1, # 'н' + 1: 3, # 'о' + 15: 1, # 'п' + 9: 2, # 'р' + 7: 2, # 'с' + 6: 2, # 'т' + 14: 2, # 'у' + 39: 2, # 'ф' + 26: 0, # 'х' + 28: 0, # 'ц' + 22: 1, # 'ч' + 25: 1, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 2, # 'ы' + 17: 1, # 'ь' + 30: 2, # 'э' + 27: 1, # 'ю' + 16: 1, # 'я' + }, + 26: { # 'х' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 0, # 'б' + 10: 3, # 'в' + 19: 1, # 'г' + 13: 1, # 'д' + 2: 2, # 'е' + 24: 0, # 'ж' + 20: 1, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 1, # 'к' + 8: 2, # 'л' + 12: 2, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 1, # 'п' + 9: 3, # 'р' + 7: 2, # 'с' + 6: 2, # 'т' + 14: 2, # 'у' + 39: 1, # 'ф' + 26: 1, # 'х' + 28: 1, # 'ц' + 22: 1, # 'ч' + 25: 2, # 'ш' + 29: 0, # 'щ' + 54: 1, # 'ъ' + 18: 0, # 'ы' + 17: 1, # 'ь' + 30: 1, # 'э' + 27: 1, # 'ю' + 16: 0, # 'я' + }, + 28: { # 'ц' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 1, # 'б' + 10: 2, # 'в' + 19: 1, # 'г' + 13: 1, # 'д' + 2: 3, # 'е' + 24: 0, # 'ж' + 20: 1, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 2, # 'к' + 8: 1, # 'л' + 12: 1, # 'м' + 5: 1, # 'н' + 1: 3, # 'о' + 15: 0, # 'п' + 9: 1, # 'р' + 7: 0, # 'с' + 6: 1, # 'т' + 14: 3, # 'у' + 39: 0, # 'ф' + 26: 0, # 'х' + 28: 1, # 'ц' + 22: 0, # 'ч' + 25: 1, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 3, # 'ы' + 17: 1, # 'ь' + 30: 0, # 'э' + 27: 1, # 'ю' + 16: 0, # 'я' + }, + 22: { # 'ч' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 1, # 'б' + 10: 1, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 3, # 'е' + 24: 1, # 'ж' + 20: 0, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 3, # 'к' + 8: 2, # 'л' + 12: 1, # 'м' + 5: 3, # 'н' + 1: 2, # 'о' + 15: 0, # 'п' + 9: 2, # 'р' + 7: 1, # 'с' + 6: 3, # 'т' + 14: 3, # 'у' + 39: 1, # 'ф' + 26: 1, # 'х' + 28: 0, # 'ц' + 22: 1, # 'ч' + 25: 2, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 3, # 'ь' + 30: 0, # 'э' + 27: 0, # 'ю' + 16: 0, # 'я' + }, + 25: { # 'ш' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 1, # 'б' + 10: 2, # 'в' + 19: 1, # 'г' + 13: 0, # 'д' + 2: 3, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 3, # 'к' + 8: 3, # 'л' + 12: 2, # 'м' + 5: 3, # 'н' + 1: 3, # 'о' + 15: 2, # 'п' + 9: 2, # 'р' + 7: 1, # 'с' + 6: 2, # 'т' + 14: 3, # 'у' + 39: 2, # 'ф' + 26: 1, # 'х' + 28: 1, # 'ц' + 22: 1, # 'ч' + 25: 1, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 3, # 'ь' + 30: 1, # 'э' + 27: 1, # 'ю' + 16: 0, # 'я' + }, + 29: { # 'щ' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 3, # 'а' + 21: 0, # 'б' + 10: 1, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 3, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 3, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 0, # 'л' + 12: 1, # 'м' + 5: 2, # 'н' + 1: 1, # 'о' + 15: 0, # 'п' + 9: 2, # 'р' + 7: 0, # 'с' + 6: 0, # 'т' + 14: 2, # 'у' + 39: 0, # 'ф' + 26: 0, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 2, # 'ь' + 30: 0, # 'э' + 27: 0, # 'ю' + 16: 0, # 'я' + }, + 54: { # 'ъ' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 0, # 'а' + 21: 0, # 'б' + 10: 0, # 'в' + 19: 0, # 'г' + 13: 0, # 'д' + 2: 2, # 'е' + 24: 0, # 'ж' + 20: 0, # 'з' + 4: 0, # 'и' + 23: 0, # 'й' + 11: 0, # 'к' + 8: 0, # 'л' + 12: 0, # 'м' + 5: 0, # 'н' + 1: 0, # 'о' + 15: 0, # 'п' + 9: 0, # 'р' + 7: 0, # 'с' + 6: 0, # 'т' + 14: 0, # 'у' + 39: 0, # 'ф' + 26: 0, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 0, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 0, # 'э' + 27: 1, # 'ю' + 16: 2, # 'я' + }, + 18: { # 'ы' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 0, # 'а' + 21: 3, # 'б' + 10: 3, # 'в' + 19: 2, # 'г' + 13: 2, # 'д' + 2: 3, # 'е' + 24: 2, # 'ж' + 20: 2, # 'з' + 4: 2, # 'и' + 23: 3, # 'й' + 11: 3, # 'к' + 8: 3, # 'л' + 12: 3, # 'м' + 5: 3, # 'н' + 1: 1, # 'о' + 15: 3, # 'п' + 9: 3, # 'р' + 7: 3, # 'с' + 6: 3, # 'т' + 14: 1, # 'у' + 39: 0, # 'ф' + 26: 3, # 'х' + 28: 2, # 'ц' + 22: 3, # 'ч' + 25: 3, # 'ш' + 29: 2, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 0, # 'э' + 27: 0, # 'ю' + 16: 2, # 'я' + }, + 17: { # 'ь' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 0, # 'а' + 21: 2, # 'б' + 10: 2, # 'в' + 19: 2, # 'г' + 13: 2, # 'д' + 2: 3, # 'е' + 24: 1, # 'ж' + 20: 3, # 'з' + 4: 2, # 'и' + 23: 0, # 'й' + 11: 3, # 'к' + 8: 0, # 'л' + 12: 3, # 'м' + 5: 3, # 'н' + 1: 2, # 'о' + 15: 2, # 'п' + 9: 1, # 'р' + 7: 3, # 'с' + 6: 2, # 'т' + 14: 0, # 'у' + 39: 2, # 'ф' + 26: 1, # 'х' + 28: 2, # 'ц' + 22: 2, # 'ч' + 25: 3, # 'ш' + 29: 2, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 1, # 'э' + 27: 3, # 'ю' + 16: 3, # 'я' + }, + 30: { # 'э' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 1, # 'М' + 31: 1, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 1, # 'Р' + 32: 1, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 1, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 0, # 'а' + 21: 1, # 'б' + 10: 1, # 'в' + 19: 1, # 'г' + 13: 2, # 'д' + 2: 1, # 'е' + 24: 0, # 'ж' + 20: 1, # 'з' + 4: 0, # 'и' + 23: 2, # 'й' + 11: 2, # 'к' + 8: 2, # 'л' + 12: 2, # 'м' + 5: 2, # 'н' + 1: 0, # 'о' + 15: 2, # 'п' + 9: 2, # 'р' + 7: 2, # 'с' + 6: 3, # 'т' + 14: 1, # 'у' + 39: 2, # 'ф' + 26: 1, # 'х' + 28: 0, # 'ц' + 22: 0, # 'ч' + 25: 1, # 'ш' + 29: 0, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 1, # 'э' + 27: 1, # 'ю' + 16: 1, # 'я' + }, + 27: { # 'ю' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 2, # 'а' + 21: 3, # 'б' + 10: 1, # 'в' + 19: 2, # 'г' + 13: 3, # 'д' + 2: 1, # 'е' + 24: 2, # 'ж' + 20: 2, # 'з' + 4: 1, # 'и' + 23: 1, # 'й' + 11: 2, # 'к' + 8: 2, # 'л' + 12: 2, # 'м' + 5: 2, # 'н' + 1: 1, # 'о' + 15: 2, # 'п' + 9: 2, # 'р' + 7: 3, # 'с' + 6: 3, # 'т' + 14: 0, # 'у' + 39: 1, # 'ф' + 26: 2, # 'х' + 28: 2, # 'ц' + 22: 2, # 'ч' + 25: 2, # 'ш' + 29: 3, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 1, # 'э' + 27: 2, # 'ю' + 16: 1, # 'я' + }, + 16: { # 'я' + 37: 0, # 'А' + 44: 0, # 'Б' + 33: 0, # 'В' + 46: 0, # 'Г' + 41: 0, # 'Д' + 48: 0, # 'Е' + 56: 0, # 'Ж' + 51: 0, # 'З' + 42: 0, # 'И' + 60: 0, # 'Й' + 36: 0, # 'К' + 49: 0, # 'Л' + 38: 0, # 'М' + 31: 0, # 'Н' + 34: 0, # 'О' + 35: 0, # 'П' + 45: 0, # 'Р' + 32: 0, # 'С' + 40: 0, # 'Т' + 52: 0, # 'У' + 53: 0, # 'Ф' + 55: 0, # 'Х' + 58: 0, # 'Ц' + 50: 0, # 'Ч' + 57: 0, # 'Ш' + 63: 0, # 'Щ' + 62: 0, # 'Ы' + 61: 0, # 'Ь' + 47: 0, # 'Э' + 59: 0, # 'Ю' + 43: 0, # 'Я' + 3: 0, # 'а' + 21: 2, # 'б' + 10: 3, # 'в' + 19: 2, # 'г' + 13: 3, # 'д' + 2: 3, # 'е' + 24: 3, # 'ж' + 20: 3, # 'з' + 4: 2, # 'и' + 23: 2, # 'й' + 11: 3, # 'к' + 8: 3, # 'л' + 12: 3, # 'м' + 5: 3, # 'н' + 1: 0, # 'о' + 15: 2, # 'п' + 9: 2, # 'р' + 7: 3, # 'с' + 6: 3, # 'т' + 14: 1, # 'у' + 39: 1, # 'ф' + 26: 3, # 'х' + 28: 2, # 'ц' + 22: 2, # 'ч' + 25: 2, # 'ш' + 29: 3, # 'щ' + 54: 0, # 'ъ' + 18: 0, # 'ы' + 17: 0, # 'ь' + 30: 0, # 'э' + 27: 2, # 'ю' + 16: 2, # 'я' + }, +} + +# 255: Undefined characters that did not exist in training text +# 254: Carriage/Return +# 253: symbol (punctuation) that does not belong to word +# 252: 0 - 9 +# 251: Control characters + +# Character Mapping Table(s): +IBM866_RUSSIAN_CHAR_TO_ORDER = { + 0: 255, # '\x00' + 1: 255, # '\x01' + 2: 255, # '\x02' + 3: 255, # '\x03' + 4: 255, # '\x04' + 5: 255, # '\x05' + 6: 255, # '\x06' + 7: 255, # '\x07' + 8: 255, # '\x08' + 9: 255, # '\t' + 10: 254, # '\n' + 11: 255, # '\x0b' + 12: 255, # '\x0c' + 13: 254, # '\r' + 14: 255, # '\x0e' + 15: 255, # '\x0f' + 16: 255, # '\x10' + 17: 255, # '\x11' + 18: 255, # '\x12' + 19: 255, # '\x13' + 20: 255, # '\x14' + 21: 255, # '\x15' + 22: 255, # '\x16' + 23: 255, # '\x17' + 24: 255, # '\x18' + 25: 255, # '\x19' + 26: 255, # '\x1a' + 27: 255, # '\x1b' + 28: 255, # '\x1c' + 29: 255, # '\x1d' + 30: 255, # '\x1e' + 31: 255, # '\x1f' + 32: 253, # ' ' + 33: 253, # '!' + 34: 253, # '"' + 35: 253, # '#' + 36: 253, # '$' + 37: 253, # '%' + 38: 253, # '&' + 39: 253, # "'" + 40: 253, # '(' + 41: 253, # ')' + 42: 253, # '*' + 43: 253, # '+' + 44: 253, # ',' + 45: 253, # '-' + 46: 253, # '.' + 47: 253, # '/' + 48: 252, # '0' + 49: 252, # '1' + 50: 252, # '2' + 51: 252, # '3' + 52: 252, # '4' + 53: 252, # '5' + 54: 252, # '6' + 55: 252, # '7' + 56: 252, # '8' + 57: 252, # '9' + 58: 253, # ':' + 59: 253, # ';' + 60: 253, # '<' + 61: 253, # '=' + 62: 253, # '>' + 63: 253, # '?' + 64: 253, # '@' + 65: 142, # 'A' + 66: 143, # 'B' + 67: 144, # 'C' + 68: 145, # 'D' + 69: 146, # 'E' + 70: 147, # 'F' + 71: 148, # 'G' + 72: 149, # 'H' + 73: 150, # 'I' + 74: 151, # 'J' + 75: 152, # 'K' + 76: 74, # 'L' + 77: 153, # 'M' + 78: 75, # 'N' + 79: 154, # 'O' + 80: 155, # 'P' + 81: 156, # 'Q' + 82: 157, # 'R' + 83: 158, # 'S' + 84: 159, # 'T' + 85: 160, # 'U' + 86: 161, # 'V' + 87: 162, # 'W' + 88: 163, # 'X' + 89: 164, # 'Y' + 90: 165, # 'Z' + 91: 253, # '[' + 92: 253, # '\\' + 93: 253, # ']' + 94: 253, # '^' + 95: 253, # '_' + 96: 253, # '`' + 97: 71, # 'a' + 98: 172, # 'b' + 99: 66, # 'c' + 100: 173, # 'd' + 101: 65, # 'e' + 102: 174, # 'f' + 103: 76, # 'g' + 104: 175, # 'h' + 105: 64, # 'i' + 106: 176, # 'j' + 107: 177, # 'k' + 108: 77, # 'l' + 109: 72, # 'm' + 110: 178, # 'n' + 111: 69, # 'o' + 112: 67, # 'p' + 113: 179, # 'q' + 114: 78, # 'r' + 115: 73, # 's' + 116: 180, # 't' + 117: 181, # 'u' + 118: 79, # 'v' + 119: 182, # 'w' + 120: 183, # 'x' + 121: 184, # 'y' + 122: 185, # 'z' + 123: 253, # '{' + 124: 253, # '|' + 125: 253, # '}' + 126: 253, # '~' + 127: 253, # '\x7f' + 128: 37, # 'А' + 129: 44, # 'Б' + 130: 33, # 'В' + 131: 46, # 'Г' + 132: 41, # 'Д' + 133: 48, # 'Е' + 134: 56, # 'Ж' + 135: 51, # 'З' + 136: 42, # 'И' + 137: 60, # 'Й' + 138: 36, # 'К' + 139: 49, # 'Л' + 140: 38, # 'М' + 141: 31, # 'Н' + 142: 34, # 'О' + 143: 35, # 'П' + 144: 45, # 'Р' + 145: 32, # 'С' + 146: 40, # 'Т' + 147: 52, # 'У' + 148: 53, # 'Ф' + 149: 55, # 'Х' + 150: 58, # 'Ц' + 151: 50, # 'Ч' + 152: 57, # 'Ш' + 153: 63, # 'Щ' + 154: 70, # 'Ъ' + 155: 62, # 'Ы' + 156: 61, # 'Ь' + 157: 47, # 'Э' + 158: 59, # 'Ю' + 159: 43, # 'Я' + 160: 3, # 'а' + 161: 21, # 'б' + 162: 10, # 'в' + 163: 19, # 'г' + 164: 13, # 'д' + 165: 2, # 'е' + 166: 24, # 'ж' + 167: 20, # 'з' + 168: 4, # 'и' + 169: 23, # 'й' + 170: 11, # 'к' + 171: 8, # 'л' + 172: 12, # 'м' + 173: 5, # 'н' + 174: 1, # 'о' + 175: 15, # 'п' + 176: 191, # '░' + 177: 192, # '▒' + 178: 193, # '▓' + 179: 194, # '│' + 180: 195, # '┤' + 181: 196, # '╡' + 182: 197, # '╢' + 183: 198, # '╖' + 184: 199, # '╕' + 185: 200, # '╣' + 186: 201, # '║' + 187: 202, # '╗' + 188: 203, # '╝' + 189: 204, # '╜' + 190: 205, # '╛' + 191: 206, # '┐' + 192: 207, # '└' + 193: 208, # '┴' + 194: 209, # '┬' + 195: 210, # '├' + 196: 211, # '─' + 197: 212, # '┼' + 198: 213, # '╞' + 199: 214, # '╟' + 200: 215, # '╚' + 201: 216, # '╔' + 202: 217, # '╩' + 203: 218, # '╦' + 204: 219, # '╠' + 205: 220, # '═' + 206: 221, # '╬' + 207: 222, # '╧' + 208: 223, # '╨' + 209: 224, # '╤' + 210: 225, # '╥' + 211: 226, # '╙' + 212: 227, # '╘' + 213: 228, # '╒' + 214: 229, # '╓' + 215: 230, # '╫' + 216: 231, # '╪' + 217: 232, # '┘' + 218: 233, # '┌' + 219: 234, # '█' + 220: 235, # '▄' + 221: 236, # '▌' + 222: 237, # '▐' + 223: 238, # '▀' + 224: 9, # 'р' + 225: 7, # 'с' + 226: 6, # 'т' + 227: 14, # 'у' + 228: 39, # 'ф' + 229: 26, # 'х' + 230: 28, # 'ц' + 231: 22, # 'ч' + 232: 25, # 'ш' + 233: 29, # 'щ' + 234: 54, # 'ъ' + 235: 18, # 'ы' + 236: 17, # 'ь' + 237: 30, # 'э' + 238: 27, # 'ю' + 239: 16, # 'я' + 240: 239, # 'Ё' + 241: 68, # 'ё' + 242: 240, # 'Є' + 243: 241, # 'є' + 244: 242, # 'Ї' + 245: 243, # 'ї' + 246: 244, # 'Ў' + 247: 245, # 'ў' + 248: 246, # '°' + 249: 247, # '∙' + 250: 248, # '·' + 251: 249, # '√' + 252: 250, # '№' + 253: 251, # '¤' + 254: 252, # '■' + 255: 255, # '\xa0' +} + +IBM866_RUSSIAN_MODEL = SingleByteCharSetModel(charset_name='IBM866', + language='Russian', + char_to_order_map=IBM866_RUSSIAN_CHAR_TO_ORDER, + language_model=RUSSIAN_LANG_MODEL, + typical_positive_ratio=0.976601, + keep_ascii_letters=False, + alphabet='ЁАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяё') + +WINDOWS_1251_RUSSIAN_CHAR_TO_ORDER = { + 0: 255, # '\x00' + 1: 255, # '\x01' + 2: 255, # '\x02' + 3: 255, # '\x03' + 4: 255, # '\x04' + 5: 255, # '\x05' + 6: 255, # '\x06' + 7: 255, # '\x07' + 8: 255, # '\x08' + 9: 255, # '\t' + 10: 254, # '\n' + 11: 255, # '\x0b' + 12: 255, # '\x0c' + 13: 254, # '\r' + 14: 255, # '\x0e' + 15: 255, # '\x0f' + 16: 255, # '\x10' + 17: 255, # '\x11' + 18: 255, # '\x12' + 19: 255, # '\x13' + 20: 255, # '\x14' + 21: 255, # '\x15' + 22: 255, # '\x16' + 23: 255, # '\x17' + 24: 255, # '\x18' + 25: 255, # '\x19' + 26: 255, # '\x1a' + 27: 255, # '\x1b' + 28: 255, # '\x1c' + 29: 255, # '\x1d' + 30: 255, # '\x1e' + 31: 255, # '\x1f' + 32: 253, # ' ' + 33: 253, # '!' + 34: 253, # '"' + 35: 253, # '#' + 36: 253, # '$' + 37: 253, # '%' + 38: 253, # '&' + 39: 253, # "'" + 40: 253, # '(' + 41: 253, # ')' + 42: 253, # '*' + 43: 253, # '+' + 44: 253, # ',' + 45: 253, # '-' + 46: 253, # '.' + 47: 253, # '/' + 48: 252, # '0' + 49: 252, # '1' + 50: 252, # '2' + 51: 252, # '3' + 52: 252, # '4' + 53: 252, # '5' + 54: 252, # '6' + 55: 252, # '7' + 56: 252, # '8' + 57: 252, # '9' + 58: 253, # ':' + 59: 253, # ';' + 60: 253, # '<' + 61: 253, # '=' + 62: 253, # '>' + 63: 253, # '?' + 64: 253, # '@' + 65: 142, # 'A' + 66: 143, # 'B' + 67: 144, # 'C' + 68: 145, # 'D' + 69: 146, # 'E' + 70: 147, # 'F' + 71: 148, # 'G' + 72: 149, # 'H' + 73: 150, # 'I' + 74: 151, # 'J' + 75: 152, # 'K' + 76: 74, # 'L' + 77: 153, # 'M' + 78: 75, # 'N' + 79: 154, # 'O' + 80: 155, # 'P' + 81: 156, # 'Q' + 82: 157, # 'R' + 83: 158, # 'S' + 84: 159, # 'T' + 85: 160, # 'U' + 86: 161, # 'V' + 87: 162, # 'W' + 88: 163, # 'X' + 89: 164, # 'Y' + 90: 165, # 'Z' + 91: 253, # '[' + 92: 253, # '\\' + 93: 253, # ']' + 94: 253, # '^' + 95: 253, # '_' + 96: 253, # '`' + 97: 71, # 'a' + 98: 172, # 'b' + 99: 66, # 'c' + 100: 173, # 'd' + 101: 65, # 'e' + 102: 174, # 'f' + 103: 76, # 'g' + 104: 175, # 'h' + 105: 64, # 'i' + 106: 176, # 'j' + 107: 177, # 'k' + 108: 77, # 'l' + 109: 72, # 'm' + 110: 178, # 'n' + 111: 69, # 'o' + 112: 67, # 'p' + 113: 179, # 'q' + 114: 78, # 'r' + 115: 73, # 's' + 116: 180, # 't' + 117: 181, # 'u' + 118: 79, # 'v' + 119: 182, # 'w' + 120: 183, # 'x' + 121: 184, # 'y' + 122: 185, # 'z' + 123: 253, # '{' + 124: 253, # '|' + 125: 253, # '}' + 126: 253, # '~' + 127: 253, # '\x7f' + 128: 191, # 'Ђ' + 129: 192, # 'Ѓ' + 130: 193, # '‚' + 131: 194, # 'ѓ' + 132: 195, # '„' + 133: 196, # '…' + 134: 197, # '†' + 135: 198, # '‡' + 136: 199, # '€' + 137: 200, # '‰' + 138: 201, # 'Љ' + 139: 202, # '‹' + 140: 203, # 'Њ' + 141: 204, # 'Ќ' + 142: 205, # 'Ћ' + 143: 206, # 'Џ' + 144: 207, # 'ђ' + 145: 208, # '‘' + 146: 209, # '’' + 147: 210, # '“' + 148: 211, # '”' + 149: 212, # '•' + 150: 213, # '–' + 151: 214, # '—' + 152: 215, # None + 153: 216, # '™' + 154: 217, # 'љ' + 155: 218, # '›' + 156: 219, # 'њ' + 157: 220, # 'ќ' + 158: 221, # 'ћ' + 159: 222, # 'џ' + 160: 223, # '\xa0' + 161: 224, # 'Ў' + 162: 225, # 'ў' + 163: 226, # 'Ј' + 164: 227, # '¤' + 165: 228, # 'Ґ' + 166: 229, # '¦' + 167: 230, # '§' + 168: 231, # 'Ё' + 169: 232, # '©' + 170: 233, # 'Є' + 171: 234, # '«' + 172: 235, # '¬' + 173: 236, # '\xad' + 174: 237, # '®' + 175: 238, # 'Ї' + 176: 239, # '°' + 177: 240, # '±' + 178: 241, # 'І' + 179: 242, # 'і' + 180: 243, # 'ґ' + 181: 244, # 'µ' + 182: 245, # '¶' + 183: 246, # '·' + 184: 68, # 'ё' + 185: 247, # '№' + 186: 248, # 'є' + 187: 249, # '»' + 188: 250, # 'ј' + 189: 251, # 'Ѕ' + 190: 252, # 'ѕ' + 191: 253, # 'ї' + 192: 37, # 'А' + 193: 44, # 'Б' + 194: 33, # 'В' + 195: 46, # 'Г' + 196: 41, # 'Д' + 197: 48, # 'Е' + 198: 56, # 'Ж' + 199: 51, # 'З' + 200: 42, # 'И' + 201: 60, # 'Й' + 202: 36, # 'К' + 203: 49, # 'Л' + 204: 38, # 'М' + 205: 31, # 'Н' + 206: 34, # 'О' + 207: 35, # 'П' + 208: 45, # 'Р' + 209: 32, # 'С' + 210: 40, # 'Т' + 211: 52, # 'У' + 212: 53, # 'Ф' + 213: 55, # 'Х' + 214: 58, # 'Ц' + 215: 50, # 'Ч' + 216: 57, # 'Ш' + 217: 63, # 'Щ' + 218: 70, # 'Ъ' + 219: 62, # 'Ы' + 220: 61, # 'Ь' + 221: 47, # 'Э' + 222: 59, # 'Ю' + 223: 43, # 'Я' + 224: 3, # 'а' + 225: 21, # 'б' + 226: 10, # 'в' + 227: 19, # 'г' + 228: 13, # 'д' + 229: 2, # 'е' + 230: 24, # 'ж' + 231: 20, # 'з' + 232: 4, # 'и' + 233: 23, # 'й' + 234: 11, # 'к' + 235: 8, # 'л' + 236: 12, # 'м' + 237: 5, # 'н' + 238: 1, # 'о' + 239: 15, # 'п' + 240: 9, # 'р' + 241: 7, # 'с' + 242: 6, # 'т' + 243: 14, # 'у' + 244: 39, # 'ф' + 245: 26, # 'х' + 246: 28, # 'ц' + 247: 22, # 'ч' + 248: 25, # 'ш' + 249: 29, # 'щ' + 250: 54, # 'ъ' + 251: 18, # 'ы' + 252: 17, # 'ь' + 253: 30, # 'э' + 254: 27, # 'ю' + 255: 16, # 'я' +} + +WINDOWS_1251_RUSSIAN_MODEL = SingleByteCharSetModel(charset_name='windows-1251', + language='Russian', + char_to_order_map=WINDOWS_1251_RUSSIAN_CHAR_TO_ORDER, + language_model=RUSSIAN_LANG_MODEL, + typical_positive_ratio=0.976601, + keep_ascii_letters=False, + alphabet='ЁАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяё') + +IBM855_RUSSIAN_CHAR_TO_ORDER = { + 0: 255, # '\x00' + 1: 255, # '\x01' + 2: 255, # '\x02' + 3: 255, # '\x03' + 4: 255, # '\x04' + 5: 255, # '\x05' + 6: 255, # '\x06' + 7: 255, # '\x07' + 8: 255, # '\x08' + 9: 255, # '\t' + 10: 254, # '\n' + 11: 255, # '\x0b' + 12: 255, # '\x0c' + 13: 254, # '\r' + 14: 255, # '\x0e' + 15: 255, # '\x0f' + 16: 255, # '\x10' + 17: 255, # '\x11' + 18: 255, # '\x12' + 19: 255, # '\x13' + 20: 255, # '\x14' + 21: 255, # '\x15' + 22: 255, # '\x16' + 23: 255, # '\x17' + 24: 255, # '\x18' + 25: 255, # '\x19' + 26: 255, # '\x1a' + 27: 255, # '\x1b' + 28: 255, # '\x1c' + 29: 255, # '\x1d' + 30: 255, # '\x1e' + 31: 255, # '\x1f' + 32: 253, # ' ' + 33: 253, # '!' + 34: 253, # '"' + 35: 253, # '#' + 36: 253, # '$' + 37: 253, # '%' + 38: 253, # '&' + 39: 253, # "'" + 40: 253, # '(' + 41: 253, # ')' + 42: 253, # '*' + 43: 253, # '+' + 44: 253, # ',' + 45: 253, # '-' + 46: 253, # '.' + 47: 253, # '/' + 48: 252, # '0' + 49: 252, # '1' + 50: 252, # '2' + 51: 252, # '3' + 52: 252, # '4' + 53: 252, # '5' + 54: 252, # '6' + 55: 252, # '7' + 56: 252, # '8' + 57: 252, # '9' + 58: 253, # ':' + 59: 253, # ';' + 60: 253, # '<' + 61: 253, # '=' + 62: 253, # '>' + 63: 253, # '?' + 64: 253, # '@' + 65: 142, # 'A' + 66: 143, # 'B' + 67: 144, # 'C' + 68: 145, # 'D' + 69: 146, # 'E' + 70: 147, # 'F' + 71: 148, # 'G' + 72: 149, # 'H' + 73: 150, # 'I' + 74: 151, # 'J' + 75: 152, # 'K' + 76: 74, # 'L' + 77: 153, # 'M' + 78: 75, # 'N' + 79: 154, # 'O' + 80: 155, # 'P' + 81: 156, # 'Q' + 82: 157, # 'R' + 83: 158, # 'S' + 84: 159, # 'T' + 85: 160, # 'U' + 86: 161, # 'V' + 87: 162, # 'W' + 88: 163, # 'X' + 89: 164, # 'Y' + 90: 165, # 'Z' + 91: 253, # '[' + 92: 253, # '\\' + 93: 253, # ']' + 94: 253, # '^' + 95: 253, # '_' + 96: 253, # '`' + 97: 71, # 'a' + 98: 172, # 'b' + 99: 66, # 'c' + 100: 173, # 'd' + 101: 65, # 'e' + 102: 174, # 'f' + 103: 76, # 'g' + 104: 175, # 'h' + 105: 64, # 'i' + 106: 176, # 'j' + 107: 177, # 'k' + 108: 77, # 'l' + 109: 72, # 'm' + 110: 178, # 'n' + 111: 69, # 'o' + 112: 67, # 'p' + 113: 179, # 'q' + 114: 78, # 'r' + 115: 73, # 's' + 116: 180, # 't' + 117: 181, # 'u' + 118: 79, # 'v' + 119: 182, # 'w' + 120: 183, # 'x' + 121: 184, # 'y' + 122: 185, # 'z' + 123: 253, # '{' + 124: 253, # '|' + 125: 253, # '}' + 126: 253, # '~' + 127: 253, # '\x7f' + 128: 191, # 'ђ' + 129: 192, # 'Ђ' + 130: 193, # 'ѓ' + 131: 194, # 'Ѓ' + 132: 68, # 'ё' + 133: 195, # 'Ё' + 134: 196, # 'є' + 135: 197, # 'Є' + 136: 198, # 'ѕ' + 137: 199, # 'Ѕ' + 138: 200, # 'і' + 139: 201, # 'І' + 140: 202, # 'ї' + 141: 203, # 'Ї' + 142: 204, # 'ј' + 143: 205, # 'Ј' + 144: 206, # 'љ' + 145: 207, # 'Љ' + 146: 208, # 'њ' + 147: 209, # 'Њ' + 148: 210, # 'ћ' + 149: 211, # 'Ћ' + 150: 212, # 'ќ' + 151: 213, # 'Ќ' + 152: 214, # 'ў' + 153: 215, # 'Ў' + 154: 216, # 'џ' + 155: 217, # 'Џ' + 156: 27, # 'ю' + 157: 59, # 'Ю' + 158: 54, # 'ъ' + 159: 70, # 'Ъ' + 160: 3, # 'а' + 161: 37, # 'А' + 162: 21, # 'б' + 163: 44, # 'Б' + 164: 28, # 'ц' + 165: 58, # 'Ц' + 166: 13, # 'д' + 167: 41, # 'Д' + 168: 2, # 'е' + 169: 48, # 'Е' + 170: 39, # 'ф' + 171: 53, # 'Ф' + 172: 19, # 'г' + 173: 46, # 'Г' + 174: 218, # '«' + 175: 219, # '»' + 176: 220, # '░' + 177: 221, # '▒' + 178: 222, # '▓' + 179: 223, # '│' + 180: 224, # '┤' + 181: 26, # 'х' + 182: 55, # 'Х' + 183: 4, # 'и' + 184: 42, # 'И' + 185: 225, # '╣' + 186: 226, # '║' + 187: 227, # '╗' + 188: 228, # '╝' + 189: 23, # 'й' + 190: 60, # 'Й' + 191: 229, # '┐' + 192: 230, # '└' + 193: 231, # '┴' + 194: 232, # '┬' + 195: 233, # '├' + 196: 234, # '─' + 197: 235, # '┼' + 198: 11, # 'к' + 199: 36, # 'К' + 200: 236, # '╚' + 201: 237, # '╔' + 202: 238, # '╩' + 203: 239, # '╦' + 204: 240, # '╠' + 205: 241, # '═' + 206: 242, # '╬' + 207: 243, # '¤' + 208: 8, # 'л' + 209: 49, # 'Л' + 210: 12, # 'м' + 211: 38, # 'М' + 212: 5, # 'н' + 213: 31, # 'Н' + 214: 1, # 'о' + 215: 34, # 'О' + 216: 15, # 'п' + 217: 244, # '┘' + 218: 245, # '┌' + 219: 246, # '█' + 220: 247, # '▄' + 221: 35, # 'П' + 222: 16, # 'я' + 223: 248, # '▀' + 224: 43, # 'Я' + 225: 9, # 'р' + 226: 45, # 'Р' + 227: 7, # 'с' + 228: 32, # 'С' + 229: 6, # 'т' + 230: 40, # 'Т' + 231: 14, # 'у' + 232: 52, # 'У' + 233: 24, # 'ж' + 234: 56, # 'Ж' + 235: 10, # 'в' + 236: 33, # 'В' + 237: 17, # 'ь' + 238: 61, # 'Ь' + 239: 249, # '№' + 240: 250, # '\xad' + 241: 18, # 'ы' + 242: 62, # 'Ы' + 243: 20, # 'з' + 244: 51, # 'З' + 245: 25, # 'ш' + 246: 57, # 'Ш' + 247: 30, # 'э' + 248: 47, # 'Э' + 249: 29, # 'щ' + 250: 63, # 'Щ' + 251: 22, # 'ч' + 252: 50, # 'Ч' + 253: 251, # '§' + 254: 252, # '■' + 255: 255, # '\xa0' +} + +IBM855_RUSSIAN_MODEL = SingleByteCharSetModel(charset_name='IBM855', + language='Russian', + char_to_order_map=IBM855_RUSSIAN_CHAR_TO_ORDER, + language_model=RUSSIAN_LANG_MODEL, + typical_positive_ratio=0.976601, + keep_ascii_letters=False, + alphabet='ЁАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяё') + +KOI8_R_RUSSIAN_CHAR_TO_ORDER = { + 0: 255, # '\x00' + 1: 255, # '\x01' + 2: 255, # '\x02' + 3: 255, # '\x03' + 4: 255, # '\x04' + 5: 255, # '\x05' + 6: 255, # '\x06' + 7: 255, # '\x07' + 8: 255, # '\x08' + 9: 255, # '\t' + 10: 254, # '\n' + 11: 255, # '\x0b' + 12: 255, # '\x0c' + 13: 254, # '\r' + 14: 255, # '\x0e' + 15: 255, # '\x0f' + 16: 255, # '\x10' + 17: 255, # '\x11' + 18: 255, # '\x12' + 19: 255, # '\x13' + 20: 255, # '\x14' + 21: 255, # '\x15' + 22: 255, # '\x16' + 23: 255, # '\x17' + 24: 255, # '\x18' + 25: 255, # '\x19' + 26: 255, # '\x1a' + 27: 255, # '\x1b' + 28: 255, # '\x1c' + 29: 255, # '\x1d' + 30: 255, # '\x1e' + 31: 255, # '\x1f' + 32: 253, # ' ' + 33: 253, # '!' + 34: 253, # '"' + 35: 253, # '#' + 36: 253, # '$' + 37: 253, # '%' + 38: 253, # '&' + 39: 253, # "'" + 40: 253, # '(' + 41: 253, # ')' + 42: 253, # '*' + 43: 253, # '+' + 44: 253, # ',' + 45: 253, # '-' + 46: 253, # '.' + 47: 253, # '/' + 48: 252, # '0' + 49: 252, # '1' + 50: 252, # '2' + 51: 252, # '3' + 52: 252, # '4' + 53: 252, # '5' + 54: 252, # '6' + 55: 252, # '7' + 56: 252, # '8' + 57: 252, # '9' + 58: 253, # ':' + 59: 253, # ';' + 60: 253, # '<' + 61: 253, # '=' + 62: 253, # '>' + 63: 253, # '?' + 64: 253, # '@' + 65: 142, # 'A' + 66: 143, # 'B' + 67: 144, # 'C' + 68: 145, # 'D' + 69: 146, # 'E' + 70: 147, # 'F' + 71: 148, # 'G' + 72: 149, # 'H' + 73: 150, # 'I' + 74: 151, # 'J' + 75: 152, # 'K' + 76: 74, # 'L' + 77: 153, # 'M' + 78: 75, # 'N' + 79: 154, # 'O' + 80: 155, # 'P' + 81: 156, # 'Q' + 82: 157, # 'R' + 83: 158, # 'S' + 84: 159, # 'T' + 85: 160, # 'U' + 86: 161, # 'V' + 87: 162, # 'W' + 88: 163, # 'X' + 89: 164, # 'Y' + 90: 165, # 'Z' + 91: 253, # '[' + 92: 253, # '\\' + 93: 253, # ']' + 94: 253, # '^' + 95: 253, # '_' + 96: 253, # '`' + 97: 71, # 'a' + 98: 172, # 'b' + 99: 66, # 'c' + 100: 173, # 'd' + 101: 65, # 'e' + 102: 174, # 'f' + 103: 76, # 'g' + 104: 175, # 'h' + 105: 64, # 'i' + 106: 176, # 'j' + 107: 177, # 'k' + 108: 77, # 'l' + 109: 72, # 'm' + 110: 178, # 'n' + 111: 69, # 'o' + 112: 67, # 'p' + 113: 179, # 'q' + 114: 78, # 'r' + 115: 73, # 's' + 116: 180, # 't' + 117: 181, # 'u' + 118: 79, # 'v' + 119: 182, # 'w' + 120: 183, # 'x' + 121: 184, # 'y' + 122: 185, # 'z' + 123: 253, # '{' + 124: 253, # '|' + 125: 253, # '}' + 126: 253, # '~' + 127: 253, # '\x7f' + 128: 191, # '─' + 129: 192, # '│' + 130: 193, # '┌' + 131: 194, # '┐' + 132: 195, # '└' + 133: 196, # '┘' + 134: 197, # '├' + 135: 198, # '┤' + 136: 199, # '┬' + 137: 200, # '┴' + 138: 201, # '┼' + 139: 202, # '▀' + 140: 203, # '▄' + 141: 204, # '█' + 142: 205, # '▌' + 143: 206, # '▐' + 144: 207, # '░' + 145: 208, # '▒' + 146: 209, # '▓' + 147: 210, # '⌠' + 148: 211, # '■' + 149: 212, # '∙' + 150: 213, # '√' + 151: 214, # '≈' + 152: 215, # '≤' + 153: 216, # '≥' + 154: 217, # '\xa0' + 155: 218, # '⌡' + 156: 219, # '°' + 157: 220, # '²' + 158: 221, # '·' + 159: 222, # '÷' + 160: 223, # '═' + 161: 224, # '║' + 162: 225, # '╒' + 163: 68, # 'ё' + 164: 226, # '╓' + 165: 227, # '╔' + 166: 228, # '╕' + 167: 229, # '╖' + 168: 230, # '╗' + 169: 231, # '╘' + 170: 232, # '╙' + 171: 233, # '╚' + 172: 234, # '╛' + 173: 235, # '╜' + 174: 236, # '╝' + 175: 237, # '╞' + 176: 238, # '╟' + 177: 239, # '╠' + 178: 240, # '╡' + 179: 241, # 'Ё' + 180: 242, # '╢' + 181: 243, # '╣' + 182: 244, # '╤' + 183: 245, # '╥' + 184: 246, # '╦' + 185: 247, # '╧' + 186: 248, # '╨' + 187: 249, # '╩' + 188: 250, # '╪' + 189: 251, # '╫' + 190: 252, # '╬' + 191: 253, # '©' + 192: 27, # 'ю' + 193: 3, # 'а' + 194: 21, # 'б' + 195: 28, # 'ц' + 196: 13, # 'д' + 197: 2, # 'е' + 198: 39, # 'ф' + 199: 19, # 'г' + 200: 26, # 'х' + 201: 4, # 'и' + 202: 23, # 'й' + 203: 11, # 'к' + 204: 8, # 'л' + 205: 12, # 'м' + 206: 5, # 'н' + 207: 1, # 'о' + 208: 15, # 'п' + 209: 16, # 'я' + 210: 9, # 'р' + 211: 7, # 'с' + 212: 6, # 'т' + 213: 14, # 'у' + 214: 24, # 'ж' + 215: 10, # 'в' + 216: 17, # 'ь' + 217: 18, # 'ы' + 218: 20, # 'з' + 219: 25, # 'ш' + 220: 30, # 'э' + 221: 29, # 'щ' + 222: 22, # 'ч' + 223: 54, # 'ъ' + 224: 59, # 'Ю' + 225: 37, # 'А' + 226: 44, # 'Б' + 227: 58, # 'Ц' + 228: 41, # 'Д' + 229: 48, # 'Е' + 230: 53, # 'Ф' + 231: 46, # 'Г' + 232: 55, # 'Х' + 233: 42, # 'И' + 234: 60, # 'Й' + 235: 36, # 'К' + 236: 49, # 'Л' + 237: 38, # 'М' + 238: 31, # 'Н' + 239: 34, # 'О' + 240: 35, # 'П' + 241: 43, # 'Я' + 242: 45, # 'Р' + 243: 32, # 'С' + 244: 40, # 'Т' + 245: 52, # 'У' + 246: 56, # 'Ж' + 247: 33, # 'В' + 248: 61, # 'Ь' + 249: 62, # 'Ы' + 250: 51, # 'З' + 251: 57, # 'Ш' + 252: 47, # 'Э' + 253: 63, # 'Щ' + 254: 50, # 'Ч' + 255: 70, # 'Ъ' +} + +KOI8_R_RUSSIAN_MODEL = SingleByteCharSetModel(charset_name='KOI8-R', + language='Russian', + char_to_order_map=KOI8_R_RUSSIAN_CHAR_TO_ORDER, + language_model=RUSSIAN_LANG_MODEL, + typical_positive_ratio=0.976601, + keep_ascii_letters=False, + alphabet='ЁАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяё') + +MACCYRILLIC_RUSSIAN_CHAR_TO_ORDER = { + 0: 255, # '\x00' + 1: 255, # '\x01' + 2: 255, # '\x02' + 3: 255, # '\x03' + 4: 255, # '\x04' + 5: 255, # '\x05' + 6: 255, # '\x06' + 7: 255, # '\x07' + 8: 255, # '\x08' + 9: 255, # '\t' + 10: 254, # '\n' + 11: 255, # '\x0b' + 12: 255, # '\x0c' + 13: 254, # '\r' + 14: 255, # '\x0e' + 15: 255, # '\x0f' + 16: 255, # '\x10' + 17: 255, # '\x11' + 18: 255, # '\x12' + 19: 255, # '\x13' + 20: 255, # '\x14' + 21: 255, # '\x15' + 22: 255, # '\x16' + 23: 255, # '\x17' + 24: 255, # '\x18' + 25: 255, # '\x19' + 26: 255, # '\x1a' + 27: 255, # '\x1b' + 28: 255, # '\x1c' + 29: 255, # '\x1d' + 30: 255, # '\x1e' + 31: 255, # '\x1f' + 32: 253, # ' ' + 33: 253, # '!' + 34: 253, # '"' + 35: 253, # '#' + 36: 253, # '$' + 37: 253, # '%' + 38: 253, # '&' + 39: 253, # "'" + 40: 253, # '(' + 41: 253, # ')' + 42: 253, # '*' + 43: 253, # '+' + 44: 253, # ',' + 45: 253, # '-' + 46: 253, # '.' + 47: 253, # '/' + 48: 252, # '0' + 49: 252, # '1' + 50: 252, # '2' + 51: 252, # '3' + 52: 252, # '4' + 53: 252, # '5' + 54: 252, # '6' + 55: 252, # '7' + 56: 252, # '8' + 57: 252, # '9' + 58: 253, # ':' + 59: 253, # ';' + 60: 253, # '<' + 61: 253, # '=' + 62: 253, # '>' + 63: 253, # '?' + 64: 253, # '@' + 65: 142, # 'A' + 66: 143, # 'B' + 67: 144, # 'C' + 68: 145, # 'D' + 69: 146, # 'E' + 70: 147, # 'F' + 71: 148, # 'G' + 72: 149, # 'H' + 73: 150, # 'I' + 74: 151, # 'J' + 75: 152, # 'K' + 76: 74, # 'L' + 77: 153, # 'M' + 78: 75, # 'N' + 79: 154, # 'O' + 80: 155, # 'P' + 81: 156, # 'Q' + 82: 157, # 'R' + 83: 158, # 'S' + 84: 159, # 'T' + 85: 160, # 'U' + 86: 161, # 'V' + 87: 162, # 'W' + 88: 163, # 'X' + 89: 164, # 'Y' + 90: 165, # 'Z' + 91: 253, # '[' + 92: 253, # '\\' + 93: 253, # ']' + 94: 253, # '^' + 95: 253, # '_' + 96: 253, # '`' + 97: 71, # 'a' + 98: 172, # 'b' + 99: 66, # 'c' + 100: 173, # 'd' + 101: 65, # 'e' + 102: 174, # 'f' + 103: 76, # 'g' + 104: 175, # 'h' + 105: 64, # 'i' + 106: 176, # 'j' + 107: 177, # 'k' + 108: 77, # 'l' + 109: 72, # 'm' + 110: 178, # 'n' + 111: 69, # 'o' + 112: 67, # 'p' + 113: 179, # 'q' + 114: 78, # 'r' + 115: 73, # 's' + 116: 180, # 't' + 117: 181, # 'u' + 118: 79, # 'v' + 119: 182, # 'w' + 120: 183, # 'x' + 121: 184, # 'y' + 122: 185, # 'z' + 123: 253, # '{' + 124: 253, # '|' + 125: 253, # '}' + 126: 253, # '~' + 127: 253, # '\x7f' + 128: 37, # 'А' + 129: 44, # 'Б' + 130: 33, # 'В' + 131: 46, # 'Г' + 132: 41, # 'Д' + 133: 48, # 'Е' + 134: 56, # 'Ж' + 135: 51, # 'З' + 136: 42, # 'И' + 137: 60, # 'Й' + 138: 36, # 'К' + 139: 49, # 'Л' + 140: 38, # 'М' + 141: 31, # 'Н' + 142: 34, # 'О' + 143: 35, # 'П' + 144: 45, # 'Р' + 145: 32, # 'С' + 146: 40, # 'Т' + 147: 52, # 'У' + 148: 53, # 'Ф' + 149: 55, # 'Х' + 150: 58, # 'Ц' + 151: 50, # 'Ч' + 152: 57, # 'Ш' + 153: 63, # 'Щ' + 154: 70, # 'Ъ' + 155: 62, # 'Ы' + 156: 61, # 'Ь' + 157: 47, # 'Э' + 158: 59, # 'Ю' + 159: 43, # 'Я' + 160: 191, # '†' + 161: 192, # '°' + 162: 193, # 'Ґ' + 163: 194, # '£' + 164: 195, # '§' + 165: 196, # '•' + 166: 197, # '¶' + 167: 198, # 'І' + 168: 199, # '®' + 169: 200, # '©' + 170: 201, # '™' + 171: 202, # 'Ђ' + 172: 203, # 'ђ' + 173: 204, # '≠' + 174: 205, # 'Ѓ' + 175: 206, # 'ѓ' + 176: 207, # '∞' + 177: 208, # '±' + 178: 209, # '≤' + 179: 210, # '≥' + 180: 211, # 'і' + 181: 212, # 'µ' + 182: 213, # 'ґ' + 183: 214, # 'Ј' + 184: 215, # 'Є' + 185: 216, # 'є' + 186: 217, # 'Ї' + 187: 218, # 'ї' + 188: 219, # 'Љ' + 189: 220, # 'љ' + 190: 221, # 'Њ' + 191: 222, # 'њ' + 192: 223, # 'ј' + 193: 224, # 'Ѕ' + 194: 225, # '¬' + 195: 226, # '√' + 196: 227, # 'ƒ' + 197: 228, # '≈' + 198: 229, # '∆' + 199: 230, # '«' + 200: 231, # '»' + 201: 232, # '…' + 202: 233, # '\xa0' + 203: 234, # 'Ћ' + 204: 235, # 'ћ' + 205: 236, # 'Ќ' + 206: 237, # 'ќ' + 207: 238, # 'ѕ' + 208: 239, # '–' + 209: 240, # '—' + 210: 241, # '“' + 211: 242, # '”' + 212: 243, # '‘' + 213: 244, # '’' + 214: 245, # '÷' + 215: 246, # '„' + 216: 247, # 'Ў' + 217: 248, # 'ў' + 218: 249, # 'Џ' + 219: 250, # 'џ' + 220: 251, # '№' + 221: 252, # 'Ё' + 222: 68, # 'ё' + 223: 16, # 'я' + 224: 3, # 'а' + 225: 21, # 'б' + 226: 10, # 'в' + 227: 19, # 'г' + 228: 13, # 'д' + 229: 2, # 'е' + 230: 24, # 'ж' + 231: 20, # 'з' + 232: 4, # 'и' + 233: 23, # 'й' + 234: 11, # 'к' + 235: 8, # 'л' + 236: 12, # 'м' + 237: 5, # 'н' + 238: 1, # 'о' + 239: 15, # 'п' + 240: 9, # 'р' + 241: 7, # 'с' + 242: 6, # 'т' + 243: 14, # 'у' + 244: 39, # 'ф' + 245: 26, # 'х' + 246: 28, # 'ц' + 247: 22, # 'ч' + 248: 25, # 'ш' + 249: 29, # 'щ' + 250: 54, # 'ъ' + 251: 18, # 'ы' + 252: 17, # 'ь' + 253: 30, # 'э' + 254: 27, # 'ю' + 255: 255, # '€' +} + +MACCYRILLIC_RUSSIAN_MODEL = SingleByteCharSetModel(charset_name='MacCyrillic', + language='Russian', + char_to_order_map=MACCYRILLIC_RUSSIAN_CHAR_TO_ORDER, + language_model=RUSSIAN_LANG_MODEL, + typical_positive_ratio=0.976601, + keep_ascii_letters=False, + alphabet='ЁАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяё') + +ISO_8859_5_RUSSIAN_CHAR_TO_ORDER = { + 0: 255, # '\x00' + 1: 255, # '\x01' + 2: 255, # '\x02' + 3: 255, # '\x03' + 4: 255, # '\x04' + 5: 255, # '\x05' + 6: 255, # '\x06' + 7: 255, # '\x07' + 8: 255, # '\x08' + 9: 255, # '\t' + 10: 254, # '\n' + 11: 255, # '\x0b' + 12: 255, # '\x0c' + 13: 254, # '\r' + 14: 255, # '\x0e' + 15: 255, # '\x0f' + 16: 255, # '\x10' + 17: 255, # '\x11' + 18: 255, # '\x12' + 19: 255, # '\x13' + 20: 255, # '\x14' + 21: 255, # '\x15' + 22: 255, # '\x16' + 23: 255, # '\x17' + 24: 255, # '\x18' + 25: 255, # '\x19' + 26: 255, # '\x1a' + 27: 255, # '\x1b' + 28: 255, # '\x1c' + 29: 255, # '\x1d' + 30: 255, # '\x1e' + 31: 255, # '\x1f' + 32: 253, # ' ' + 33: 253, # '!' + 34: 253, # '"' + 35: 253, # '#' + 36: 253, # '$' + 37: 253, # '%' + 38: 253, # '&' + 39: 253, # "'" + 40: 253, # '(' + 41: 253, # ')' + 42: 253, # '*' + 43: 253, # '+' + 44: 253, # ',' + 45: 253, # '-' + 46: 253, # '.' + 47: 253, # '/' + 48: 252, # '0' + 49: 252, # '1' + 50: 252, # '2' + 51: 252, # '3' + 52: 252, # '4' + 53: 252, # '5' + 54: 252, # '6' + 55: 252, # '7' + 56: 252, # '8' + 57: 252, # '9' + 58: 253, # ':' + 59: 253, # ';' + 60: 253, # '<' + 61: 253, # '=' + 62: 253, # '>' + 63: 253, # '?' + 64: 253, # '@' + 65: 142, # 'A' + 66: 143, # 'B' + 67: 144, # 'C' + 68: 145, # 'D' + 69: 146, # 'E' + 70: 147, # 'F' + 71: 148, # 'G' + 72: 149, # 'H' + 73: 150, # 'I' + 74: 151, # 'J' + 75: 152, # 'K' + 76: 74, # 'L' + 77: 153, # 'M' + 78: 75, # 'N' + 79: 154, # 'O' + 80: 155, # 'P' + 81: 156, # 'Q' + 82: 157, # 'R' + 83: 158, # 'S' + 84: 159, # 'T' + 85: 160, # 'U' + 86: 161, # 'V' + 87: 162, # 'W' + 88: 163, # 'X' + 89: 164, # 'Y' + 90: 165, # 'Z' + 91: 253, # '[' + 92: 253, # '\\' + 93: 253, # ']' + 94: 253, # '^' + 95: 253, # '_' + 96: 253, # '`' + 97: 71, # 'a' + 98: 172, # 'b' + 99: 66, # 'c' + 100: 173, # 'd' + 101: 65, # 'e' + 102: 174, # 'f' + 103: 76, # 'g' + 104: 175, # 'h' + 105: 64, # 'i' + 106: 176, # 'j' + 107: 177, # 'k' + 108: 77, # 'l' + 109: 72, # 'm' + 110: 178, # 'n' + 111: 69, # 'o' + 112: 67, # 'p' + 113: 179, # 'q' + 114: 78, # 'r' + 115: 73, # 's' + 116: 180, # 't' + 117: 181, # 'u' + 118: 79, # 'v' + 119: 182, # 'w' + 120: 183, # 'x' + 121: 184, # 'y' + 122: 185, # 'z' + 123: 253, # '{' + 124: 253, # '|' + 125: 253, # '}' + 126: 253, # '~' + 127: 253, # '\x7f' + 128: 191, # '\x80' + 129: 192, # '\x81' + 130: 193, # '\x82' + 131: 194, # '\x83' + 132: 195, # '\x84' + 133: 196, # '\x85' + 134: 197, # '\x86' + 135: 198, # '\x87' + 136: 199, # '\x88' + 137: 200, # '\x89' + 138: 201, # '\x8a' + 139: 202, # '\x8b' + 140: 203, # '\x8c' + 141: 204, # '\x8d' + 142: 205, # '\x8e' + 143: 206, # '\x8f' + 144: 207, # '\x90' + 145: 208, # '\x91' + 146: 209, # '\x92' + 147: 210, # '\x93' + 148: 211, # '\x94' + 149: 212, # '\x95' + 150: 213, # '\x96' + 151: 214, # '\x97' + 152: 215, # '\x98' + 153: 216, # '\x99' + 154: 217, # '\x9a' + 155: 218, # '\x9b' + 156: 219, # '\x9c' + 157: 220, # '\x9d' + 158: 221, # '\x9e' + 159: 222, # '\x9f' + 160: 223, # '\xa0' + 161: 224, # 'Ё' + 162: 225, # 'Ђ' + 163: 226, # 'Ѓ' + 164: 227, # 'Є' + 165: 228, # 'Ѕ' + 166: 229, # 'І' + 167: 230, # 'Ї' + 168: 231, # 'Ј' + 169: 232, # 'Љ' + 170: 233, # 'Њ' + 171: 234, # 'Ћ' + 172: 235, # 'Ќ' + 173: 236, # '\xad' + 174: 237, # 'Ў' + 175: 238, # 'Џ' + 176: 37, # 'А' + 177: 44, # 'Б' + 178: 33, # 'В' + 179: 46, # 'Г' + 180: 41, # 'Д' + 181: 48, # 'Е' + 182: 56, # 'Ж' + 183: 51, # 'З' + 184: 42, # 'И' + 185: 60, # 'Й' + 186: 36, # 'К' + 187: 49, # 'Л' + 188: 38, # 'М' + 189: 31, # 'Н' + 190: 34, # 'О' + 191: 35, # 'П' + 192: 45, # 'Р' + 193: 32, # 'С' + 194: 40, # 'Т' + 195: 52, # 'У' + 196: 53, # 'Ф' + 197: 55, # 'Х' + 198: 58, # 'Ц' + 199: 50, # 'Ч' + 200: 57, # 'Ш' + 201: 63, # 'Щ' + 202: 70, # 'Ъ' + 203: 62, # 'Ы' + 204: 61, # 'Ь' + 205: 47, # 'Э' + 206: 59, # 'Ю' + 207: 43, # 'Я' + 208: 3, # 'а' + 209: 21, # 'б' + 210: 10, # 'в' + 211: 19, # 'г' + 212: 13, # 'д' + 213: 2, # 'е' + 214: 24, # 'ж' + 215: 20, # 'з' + 216: 4, # 'и' + 217: 23, # 'й' + 218: 11, # 'к' + 219: 8, # 'л' + 220: 12, # 'м' + 221: 5, # 'н' + 222: 1, # 'о' + 223: 15, # 'п' + 224: 9, # 'р' + 225: 7, # 'с' + 226: 6, # 'т' + 227: 14, # 'у' + 228: 39, # 'ф' + 229: 26, # 'х' + 230: 28, # 'ц' + 231: 22, # 'ч' + 232: 25, # 'ш' + 233: 29, # 'щ' + 234: 54, # 'ъ' + 235: 18, # 'ы' + 236: 17, # 'ь' + 237: 30, # 'э' + 238: 27, # 'ю' + 239: 16, # 'я' + 240: 239, # '№' + 241: 68, # 'ё' + 242: 240, # 'ђ' + 243: 241, # 'ѓ' + 244: 242, # 'є' + 245: 243, # 'ѕ' + 246: 244, # 'і' + 247: 245, # 'ї' + 248: 246, # 'ј' + 249: 247, # 'љ' + 250: 248, # 'њ' + 251: 249, # 'ћ' + 252: 250, # 'ќ' + 253: 251, # '§' + 254: 252, # 'ў' + 255: 255, # 'џ' +} + +ISO_8859_5_RUSSIAN_MODEL = SingleByteCharSetModel(charset_name='ISO-8859-5', + language='Russian', + char_to_order_map=ISO_8859_5_RUSSIAN_CHAR_TO_ORDER, + language_model=RUSSIAN_LANG_MODEL, + typical_positive_ratio=0.976601, + keep_ascii_letters=False, + alphabet='ЁАБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯабвгдежзийклмнопрстуфхцчшщъыьэюяё') + diff --git a/src/pip/_vendor/chardet/langthaimodel.py b/src/pip/_vendor/chardet/langthaimodel.py index 15f94c2df02..9a37db57388 100644 --- a/src/pip/_vendor/chardet/langthaimodel.py +++ b/src/pip/_vendor/chardet/langthaimodel.py @@ -1,199 +1,4383 @@ -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is Mozilla Communicator client code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 1998 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### +#!/usr/bin/env python +# -*- coding: utf-8 -*- -# 255: Control characters that usually does not exist in any text +from pip._vendor.chardet.sbcharsetprober import SingleByteCharSetModel + + +# 3: Positive +# 2: Likely +# 1: Unlikely +# 0: Negative + +THAI_LANG_MODEL = { + 5: { # 'ก' + 5: 2, # 'ก' + 30: 2, # 'ข' + 24: 2, # 'ค' + 8: 2, # 'ง' + 26: 2, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 1, # 'ซ' + 47: 0, # 'ญ' + 58: 3, # 'ฎ' + 57: 2, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 2, # 'ณ' + 20: 2, # 'ด' + 19: 3, # 'ต' + 44: 0, # 'ถ' + 14: 2, # 'ท' + 48: 0, # 'ธ' + 3: 2, # 'น' + 17: 1, # 'บ' + 25: 2, # 'ป' + 39: 1, # 'ผ' + 62: 1, # 'ฝ' + 31: 1, # 'พ' + 54: 0, # 'ฟ' + 45: 1, # 'ภ' + 9: 2, # 'ม' + 16: 1, # 'ย' + 2: 3, # 'ร' + 61: 2, # 'ฤ' + 15: 3, # 'ล' + 12: 3, # 'ว' + 42: 2, # 'ศ' + 46: 3, # 'ษ' + 18: 2, # 'ส' + 21: 2, # 'ห' + 4: 3, # 'อ' + 63: 1, # 'ฯ' + 22: 2, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 3, # 'ำ' + 23: 3, # 'ิ' + 13: 3, # 'ี' + 40: 0, # 'ึ' + 27: 2, # 'ื' + 32: 2, # 'ุ' + 35: 1, # 'ู' + 11: 2, # 'เ' + 28: 2, # 'แ' + 41: 1, # 'โ' + 29: 1, # 'ใ' + 33: 2, # 'ไ' + 50: 1, # 'ๆ' + 37: 3, # '็' + 6: 3, # '่' + 7: 3, # '้' + 38: 2, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 30: { # 'ข' + 5: 1, # 'ก' + 30: 0, # 'ข' + 24: 1, # 'ค' + 8: 1, # 'ง' + 26: 1, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 2, # 'ณ' + 20: 0, # 'ด' + 19: 2, # 'ต' + 44: 0, # 'ถ' + 14: 1, # 'ท' + 48: 0, # 'ธ' + 3: 2, # 'น' + 17: 1, # 'บ' + 25: 1, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 2, # 'ย' + 2: 1, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 2, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 1, # 'ส' + 21: 1, # 'ห' + 4: 3, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 2, # 'ี' + 40: 3, # 'ึ' + 27: 1, # 'ื' + 32: 1, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 1, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 1, # '็' + 6: 2, # '่' + 7: 3, # '้' + 38: 1, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 24: { # 'ค' + 5: 0, # 'ก' + 30: 0, # 'ข' + 24: 2, # 'ค' + 8: 2, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 2, # 'ณ' + 20: 2, # 'ด' + 19: 2, # 'ต' + 44: 0, # 'ถ' + 14: 1, # 'ท' + 48: 0, # 'ธ' + 3: 3, # 'น' + 17: 0, # 'บ' + 25: 1, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 2, # 'ม' + 16: 2, # 'ย' + 2: 3, # 'ร' + 61: 0, # 'ฤ' + 15: 3, # 'ล' + 12: 3, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 1, # 'ส' + 21: 0, # 'ห' + 4: 2, # 'อ' + 63: 0, # 'ฯ' + 22: 2, # 'ะ' + 10: 3, # 'ั' + 1: 2, # 'า' + 36: 3, # 'ำ' + 23: 3, # 'ิ' + 13: 2, # 'ี' + 40: 0, # 'ึ' + 27: 3, # 'ื' + 32: 3, # 'ุ' + 35: 2, # 'ู' + 11: 1, # 'เ' + 28: 0, # 'แ' + 41: 3, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 1, # '็' + 6: 3, # '่' + 7: 3, # '้' + 38: 3, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 8: { # 'ง' + 5: 3, # 'ก' + 30: 2, # 'ข' + 24: 3, # 'ค' + 8: 2, # 'ง' + 26: 2, # 'จ' + 52: 1, # 'ฉ' + 34: 2, # 'ช' + 51: 1, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 2, # 'ด' + 19: 2, # 'ต' + 44: 1, # 'ถ' + 14: 3, # 'ท' + 48: 1, # 'ธ' + 3: 3, # 'น' + 17: 2, # 'บ' + 25: 2, # 'ป' + 39: 2, # 'ผ' + 62: 1, # 'ฝ' + 31: 2, # 'พ' + 54: 0, # 'ฟ' + 45: 1, # 'ภ' + 9: 2, # 'ม' + 16: 1, # 'ย' + 2: 2, # 'ร' + 61: 0, # 'ฤ' + 15: 2, # 'ล' + 12: 2, # 'ว' + 42: 2, # 'ศ' + 46: 1, # 'ษ' + 18: 3, # 'ส' + 21: 3, # 'ห' + 4: 2, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 1, # 'ั' + 1: 3, # 'า' + 36: 0, # 'ำ' + 23: 2, # 'ิ' + 13: 1, # 'ี' + 40: 0, # 'ึ' + 27: 1, # 'ื' + 32: 1, # 'ุ' + 35: 0, # 'ู' + 11: 3, # 'เ' + 28: 2, # 'แ' + 41: 1, # 'โ' + 29: 2, # 'ใ' + 33: 2, # 'ไ' + 50: 3, # 'ๆ' + 37: 0, # '็' + 6: 2, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 26: { # 'จ' + 5: 2, # 'ก' + 30: 1, # 'ข' + 24: 0, # 'ค' + 8: 2, # 'ง' + 26: 3, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 2, # 'ด' + 19: 1, # 'ต' + 44: 1, # 'ถ' + 14: 2, # 'ท' + 48: 0, # 'ธ' + 3: 3, # 'น' + 17: 1, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 1, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 1, # 'ม' + 16: 1, # 'ย' + 2: 3, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 1, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 2, # 'ส' + 21: 1, # 'ห' + 4: 2, # 'อ' + 63: 0, # 'ฯ' + 22: 3, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 3, # 'ำ' + 23: 2, # 'ิ' + 13: 1, # 'ี' + 40: 3, # 'ึ' + 27: 1, # 'ื' + 32: 3, # 'ุ' + 35: 2, # 'ู' + 11: 1, # 'เ' + 28: 1, # 'แ' + 41: 0, # 'โ' + 29: 1, # 'ใ' + 33: 1, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 2, # '่' + 7: 2, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 52: { # 'ฉ' + 5: 0, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 3, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 3, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 1, # 'ม' + 16: 1, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 2, # 'ล' + 12: 1, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 1, # 'ะ' + 10: 1, # 'ั' + 1: 1, # 'า' + 36: 0, # 'ำ' + 23: 1, # 'ิ' + 13: 1, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 1, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 34: { # 'ช' + 5: 1, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 1, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 1, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 1, # 'ท' + 48: 0, # 'ธ' + 3: 3, # 'น' + 17: 2, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 2, # 'ม' + 16: 1, # 'ย' + 2: 1, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 1, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 2, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 2, # 'ั' + 1: 3, # 'า' + 36: 1, # 'ำ' + 23: 3, # 'ิ' + 13: 2, # 'ี' + 40: 0, # 'ึ' + 27: 3, # 'ื' + 32: 3, # 'ุ' + 35: 1, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 1, # '็' + 6: 3, # '่' + 7: 3, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 51: { # 'ซ' + 5: 0, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 1, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 1, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 1, # 'ส' + 21: 0, # 'ห' + 4: 2, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 1, # 'ั' + 1: 1, # 'า' + 36: 0, # 'ำ' + 23: 1, # 'ิ' + 13: 2, # 'ี' + 40: 3, # 'ึ' + 27: 2, # 'ื' + 32: 1, # 'ุ' + 35: 1, # 'ู' + 11: 1, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 1, # '็' + 6: 1, # '่' + 7: 2, # '้' + 38: 1, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 47: { # 'ญ' + 5: 1, # 'ก' + 30: 1, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 0, # 'ซ' + 47: 3, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 1, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 1, # 'บ' + 25: 1, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 1, # 'ม' + 16: 0, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 1, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 1, # 'ส' + 21: 2, # 'ห' + 4: 1, # 'อ' + 63: 0, # 'ฯ' + 22: 1, # 'ะ' + 10: 2, # 'ั' + 1: 3, # 'า' + 36: 0, # 'ำ' + 23: 1, # 'ิ' + 13: 1, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 1, # 'เ' + 28: 1, # 'แ' + 41: 0, # 'โ' + 29: 1, # 'ใ' + 33: 0, # 'ไ' + 50: 1, # 'ๆ' + 37: 0, # '็' + 6: 2, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 58: { # 'ฎ' + 5: 2, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 1, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 1, # 'ิ' + 13: 2, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 57: { # 'ฏ' + 5: 0, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 3, # 'ิ' + 13: 1, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 49: { # 'ฐ' + 5: 1, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 2, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 2, # 'ม' + 16: 0, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 1, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 1, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 3, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 1, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 53: { # 'ฑ' + 5: 0, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 2, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 3, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 55: { # 'ฒ' + 5: 0, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 3, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 1, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 1, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 43: { # 'ณ' + 5: 1, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 3, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 3, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 1, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 1, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 1, # 'ส' + 21: 1, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 3, # 'ะ' + 10: 0, # 'ั' + 1: 3, # 'า' + 36: 0, # 'ำ' + 23: 1, # 'ิ' + 13: 2, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 1, # 'เ' + 28: 1, # 'แ' + 41: 0, # 'โ' + 29: 1, # 'ใ' + 33: 1, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 3, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 20: { # 'ด' + 5: 2, # 'ก' + 30: 2, # 'ข' + 24: 2, # 'ค' + 8: 3, # 'ง' + 26: 2, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 1, # 'ด' + 19: 2, # 'ต' + 44: 1, # 'ถ' + 14: 2, # 'ท' + 48: 0, # 'ธ' + 3: 1, # 'น' + 17: 1, # 'บ' + 25: 1, # 'ป' + 39: 1, # 'ผ' + 62: 0, # 'ฝ' + 31: 1, # 'พ' + 54: 0, # 'ฟ' + 45: 1, # 'ภ' + 9: 2, # 'ม' + 16: 3, # 'ย' + 2: 2, # 'ร' + 61: 0, # 'ฤ' + 15: 2, # 'ล' + 12: 2, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 2, # 'ส' + 21: 2, # 'ห' + 4: 1, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 3, # 'ั' + 1: 2, # 'า' + 36: 2, # 'ำ' + 23: 3, # 'ิ' + 13: 3, # 'ี' + 40: 1, # 'ึ' + 27: 2, # 'ื' + 32: 3, # 'ุ' + 35: 2, # 'ู' + 11: 2, # 'เ' + 28: 2, # 'แ' + 41: 1, # 'โ' + 29: 2, # 'ใ' + 33: 2, # 'ไ' + 50: 2, # 'ๆ' + 37: 2, # '็' + 6: 1, # '่' + 7: 3, # '้' + 38: 1, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 19: { # 'ต' + 5: 2, # 'ก' + 30: 1, # 'ข' + 24: 1, # 'ค' + 8: 0, # 'ง' + 26: 1, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 1, # 'ด' + 19: 1, # 'ต' + 44: 2, # 'ถ' + 14: 1, # 'ท' + 48: 0, # 'ธ' + 3: 2, # 'น' + 17: 1, # 'บ' + 25: 1, # 'ป' + 39: 1, # 'ผ' + 62: 0, # 'ฝ' + 31: 1, # 'พ' + 54: 0, # 'ฟ' + 45: 2, # 'ภ' + 9: 1, # 'ม' + 16: 1, # 'ย' + 2: 3, # 'ร' + 61: 0, # 'ฤ' + 15: 2, # 'ล' + 12: 1, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 3, # 'ส' + 21: 0, # 'ห' + 4: 3, # 'อ' + 63: 1, # 'ฯ' + 22: 2, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 2, # 'ำ' + 23: 3, # 'ิ' + 13: 2, # 'ี' + 40: 1, # 'ึ' + 27: 1, # 'ื' + 32: 3, # 'ุ' + 35: 2, # 'ู' + 11: 1, # 'เ' + 28: 1, # 'แ' + 41: 1, # 'โ' + 29: 1, # 'ใ' + 33: 1, # 'ไ' + 50: 0, # 'ๆ' + 37: 2, # '็' + 6: 3, # '่' + 7: 3, # '้' + 38: 2, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 44: { # 'ถ' + 5: 1, # 'ก' + 30: 0, # 'ข' + 24: 1, # 'ค' + 8: 0, # 'ง' + 26: 1, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 1, # 'ต' + 44: 0, # 'ถ' + 14: 1, # 'ท' + 48: 0, # 'ธ' + 3: 1, # 'น' + 17: 2, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 1, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 1, # 'ร' + 61: 0, # 'ฤ' + 15: 1, # 'ล' + 12: 1, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 1, # 'ส' + 21: 0, # 'ห' + 4: 1, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 2, # 'ั' + 1: 3, # 'า' + 36: 0, # 'ำ' + 23: 2, # 'ิ' + 13: 1, # 'ี' + 40: 3, # 'ึ' + 27: 2, # 'ื' + 32: 2, # 'ุ' + 35: 3, # 'ู' + 11: 1, # 'เ' + 28: 1, # 'แ' + 41: 0, # 'โ' + 29: 1, # 'ใ' + 33: 1, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 2, # '่' + 7: 3, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 14: { # 'ท' + 5: 1, # 'ก' + 30: 1, # 'ข' + 24: 3, # 'ค' + 8: 1, # 'ง' + 26: 1, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 2, # 'ด' + 19: 1, # 'ต' + 44: 0, # 'ถ' + 14: 1, # 'ท' + 48: 3, # 'ธ' + 3: 3, # 'น' + 17: 2, # 'บ' + 25: 2, # 'ป' + 39: 1, # 'ผ' + 62: 0, # 'ฝ' + 31: 2, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 1, # 'ม' + 16: 3, # 'ย' + 2: 3, # 'ร' + 61: 1, # 'ฤ' + 15: 1, # 'ล' + 12: 2, # 'ว' + 42: 3, # 'ศ' + 46: 1, # 'ษ' + 18: 1, # 'ส' + 21: 0, # 'ห' + 4: 2, # 'อ' + 63: 0, # 'ฯ' + 22: 2, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 3, # 'ำ' + 23: 2, # 'ิ' + 13: 3, # 'ี' + 40: 2, # 'ึ' + 27: 1, # 'ื' + 32: 3, # 'ุ' + 35: 1, # 'ู' + 11: 0, # 'เ' + 28: 1, # 'แ' + 41: 0, # 'โ' + 29: 1, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 1, # '็' + 6: 3, # '่' + 7: 3, # '้' + 38: 2, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 48: { # 'ธ' + 5: 0, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 1, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 1, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 2, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 2, # 'า' + 36: 0, # 'ำ' + 23: 3, # 'ิ' + 13: 3, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 2, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 3, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 3: { # 'น' + 5: 3, # 'ก' + 30: 2, # 'ข' + 24: 3, # 'ค' + 8: 1, # 'ง' + 26: 2, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 1, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 1, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 3, # 'ด' + 19: 3, # 'ต' + 44: 2, # 'ถ' + 14: 3, # 'ท' + 48: 3, # 'ธ' + 3: 2, # 'น' + 17: 2, # 'บ' + 25: 2, # 'ป' + 39: 2, # 'ผ' + 62: 0, # 'ฝ' + 31: 2, # 'พ' + 54: 1, # 'ฟ' + 45: 1, # 'ภ' + 9: 2, # 'ม' + 16: 2, # 'ย' + 2: 2, # 'ร' + 61: 1, # 'ฤ' + 15: 2, # 'ล' + 12: 3, # 'ว' + 42: 1, # 'ศ' + 46: 0, # 'ษ' + 18: 2, # 'ส' + 21: 2, # 'ห' + 4: 3, # 'อ' + 63: 1, # 'ฯ' + 22: 2, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 3, # 'ำ' + 23: 3, # 'ิ' + 13: 3, # 'ี' + 40: 3, # 'ึ' + 27: 3, # 'ื' + 32: 3, # 'ุ' + 35: 2, # 'ู' + 11: 3, # 'เ' + 28: 2, # 'แ' + 41: 3, # 'โ' + 29: 3, # 'ใ' + 33: 3, # 'ไ' + 50: 2, # 'ๆ' + 37: 1, # '็' + 6: 3, # '่' + 7: 3, # '้' + 38: 2, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 17: { # 'บ' + 5: 3, # 'ก' + 30: 2, # 'ข' + 24: 2, # 'ค' + 8: 1, # 'ง' + 26: 1, # 'จ' + 52: 1, # 'ฉ' + 34: 1, # 'ช' + 51: 1, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 1, # 'ด' + 19: 2, # 'ต' + 44: 1, # 'ถ' + 14: 3, # 'ท' + 48: 0, # 'ธ' + 3: 3, # 'น' + 17: 3, # 'บ' + 25: 2, # 'ป' + 39: 2, # 'ผ' + 62: 0, # 'ฝ' + 31: 1, # 'พ' + 54: 1, # 'ฟ' + 45: 1, # 'ภ' + 9: 1, # 'ม' + 16: 0, # 'ย' + 2: 3, # 'ร' + 61: 0, # 'ฤ' + 15: 2, # 'ล' + 12: 3, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 2, # 'ส' + 21: 2, # 'ห' + 4: 2, # 'อ' + 63: 1, # 'ฯ' + 22: 0, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 2, # 'ำ' + 23: 2, # 'ิ' + 13: 2, # 'ี' + 40: 0, # 'ึ' + 27: 2, # 'ื' + 32: 3, # 'ุ' + 35: 2, # 'ู' + 11: 2, # 'เ' + 28: 2, # 'แ' + 41: 1, # 'โ' + 29: 2, # 'ใ' + 33: 2, # 'ไ' + 50: 0, # 'ๆ' + 37: 1, # '็' + 6: 2, # '่' + 7: 2, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 25: { # 'ป' + 5: 2, # 'ก' + 30: 0, # 'ข' + 24: 1, # 'ค' + 8: 0, # 'ง' + 26: 1, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 1, # 'ซ' + 47: 0, # 'ญ' + 58: 1, # 'ฎ' + 57: 3, # 'ฏ' + 49: 1, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 1, # 'ด' + 19: 1, # 'ต' + 44: 1, # 'ถ' + 14: 1, # 'ท' + 48: 0, # 'ธ' + 3: 2, # 'น' + 17: 0, # 'บ' + 25: 1, # 'ป' + 39: 1, # 'ผ' + 62: 1, # 'ฝ' + 31: 1, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 1, # 'ม' + 16: 0, # 'ย' + 2: 3, # 'ร' + 61: 0, # 'ฤ' + 15: 3, # 'ล' + 12: 1, # 'ว' + 42: 0, # 'ศ' + 46: 1, # 'ษ' + 18: 2, # 'ส' + 21: 1, # 'ห' + 4: 2, # 'อ' + 63: 0, # 'ฯ' + 22: 1, # 'ะ' + 10: 3, # 'ั' + 1: 1, # 'า' + 36: 0, # 'ำ' + 23: 2, # 'ิ' + 13: 3, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 1, # 'ุ' + 35: 0, # 'ู' + 11: 1, # 'เ' + 28: 2, # 'แ' + 41: 0, # 'โ' + 29: 1, # 'ใ' + 33: 2, # 'ไ' + 50: 0, # 'ๆ' + 37: 3, # '็' + 6: 1, # '่' + 7: 2, # '้' + 38: 1, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 39: { # 'ผ' + 5: 1, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 1, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 2, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 1, # 'ม' + 16: 2, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 3, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 1, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 1, # 'ะ' + 10: 1, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 2, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 1, # 'ื' + 32: 0, # 'ุ' + 35: 3, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 3, # '่' + 7: 1, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 62: { # 'ฝ' + 5: 0, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 1, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 1, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 1, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 1, # 'ี' + 40: 2, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 2, # '่' + 7: 1, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 31: { # 'พ' + 5: 1, # 'ก' + 30: 1, # 'ข' + 24: 1, # 'ค' + 8: 1, # 'ง' + 26: 1, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 1, # 'ณ' + 20: 1, # 'ด' + 19: 1, # 'ต' + 44: 0, # 'ถ' + 14: 2, # 'ท' + 48: 1, # 'ธ' + 3: 3, # 'น' + 17: 2, # 'บ' + 25: 0, # 'ป' + 39: 1, # 'ผ' + 62: 0, # 'ฝ' + 31: 1, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 1, # 'ม' + 16: 2, # 'ย' + 2: 3, # 'ร' + 61: 2, # 'ฤ' + 15: 2, # 'ล' + 12: 2, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 1, # 'ส' + 21: 1, # 'ห' + 4: 2, # 'อ' + 63: 1, # 'ฯ' + 22: 0, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 0, # 'ำ' + 23: 3, # 'ิ' + 13: 2, # 'ี' + 40: 1, # 'ึ' + 27: 3, # 'ื' + 32: 1, # 'ุ' + 35: 2, # 'ู' + 11: 1, # 'เ' + 28: 1, # 'แ' + 41: 0, # 'โ' + 29: 1, # 'ใ' + 33: 1, # 'ไ' + 50: 0, # 'ๆ' + 37: 1, # '็' + 6: 0, # '่' + 7: 1, # '้' + 38: 3, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 54: { # 'ฟ' + 5: 0, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 1, # 'ต' + 44: 0, # 'ถ' + 14: 1, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 2, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 1, # 'ร' + 61: 0, # 'ฤ' + 15: 2, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 1, # 'ส' + 21: 0, # 'ห' + 4: 1, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 2, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 1, # 'ิ' + 13: 1, # 'ี' + 40: 0, # 'ึ' + 27: 1, # 'ื' + 32: 1, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 1, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 2, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 45: { # 'ภ' + 5: 0, # 'ก' + 30: 0, # 'ข' + 24: 1, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 3, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 1, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 1, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 0, # 'ำ' + 23: 1, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 2, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 1, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 9: { # 'ม' + 5: 2, # 'ก' + 30: 2, # 'ข' + 24: 2, # 'ค' + 8: 2, # 'ง' + 26: 2, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 1, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 1, # 'ณ' + 20: 2, # 'ด' + 19: 2, # 'ต' + 44: 1, # 'ถ' + 14: 2, # 'ท' + 48: 1, # 'ธ' + 3: 3, # 'น' + 17: 2, # 'บ' + 25: 2, # 'ป' + 39: 1, # 'ผ' + 62: 0, # 'ฝ' + 31: 3, # 'พ' + 54: 0, # 'ฟ' + 45: 1, # 'ภ' + 9: 2, # 'ม' + 16: 1, # 'ย' + 2: 2, # 'ร' + 61: 2, # 'ฤ' + 15: 2, # 'ล' + 12: 2, # 'ว' + 42: 1, # 'ศ' + 46: 1, # 'ษ' + 18: 3, # 'ส' + 21: 3, # 'ห' + 4: 3, # 'อ' + 63: 0, # 'ฯ' + 22: 1, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 0, # 'ำ' + 23: 3, # 'ิ' + 13: 3, # 'ี' + 40: 0, # 'ึ' + 27: 3, # 'ื' + 32: 3, # 'ุ' + 35: 3, # 'ู' + 11: 2, # 'เ' + 28: 2, # 'แ' + 41: 2, # 'โ' + 29: 2, # 'ใ' + 33: 2, # 'ไ' + 50: 1, # 'ๆ' + 37: 1, # '็' + 6: 3, # '่' + 7: 2, # '้' + 38: 1, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 16: { # 'ย' + 5: 3, # 'ก' + 30: 1, # 'ข' + 24: 2, # 'ค' + 8: 3, # 'ง' + 26: 2, # 'จ' + 52: 0, # 'ฉ' + 34: 2, # 'ช' + 51: 0, # 'ซ' + 47: 2, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 2, # 'ด' + 19: 2, # 'ต' + 44: 1, # 'ถ' + 14: 2, # 'ท' + 48: 1, # 'ธ' + 3: 3, # 'น' + 17: 3, # 'บ' + 25: 1, # 'ป' + 39: 1, # 'ผ' + 62: 0, # 'ฝ' + 31: 1, # 'พ' + 54: 0, # 'ฟ' + 45: 1, # 'ภ' + 9: 2, # 'ม' + 16: 0, # 'ย' + 2: 2, # 'ร' + 61: 0, # 'ฤ' + 15: 1, # 'ล' + 12: 3, # 'ว' + 42: 1, # 'ศ' + 46: 0, # 'ษ' + 18: 2, # 'ส' + 21: 1, # 'ห' + 4: 2, # 'อ' + 63: 0, # 'ฯ' + 22: 2, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 0, # 'ำ' + 23: 2, # 'ิ' + 13: 3, # 'ี' + 40: 1, # 'ึ' + 27: 2, # 'ื' + 32: 2, # 'ุ' + 35: 3, # 'ู' + 11: 2, # 'เ' + 28: 1, # 'แ' + 41: 1, # 'โ' + 29: 2, # 'ใ' + 33: 2, # 'ไ' + 50: 2, # 'ๆ' + 37: 1, # '็' + 6: 3, # '่' + 7: 2, # '้' + 38: 3, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 2: { # 'ร' + 5: 3, # 'ก' + 30: 2, # 'ข' + 24: 2, # 'ค' + 8: 3, # 'ง' + 26: 2, # 'จ' + 52: 0, # 'ฉ' + 34: 2, # 'ช' + 51: 1, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 3, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 3, # 'ณ' + 20: 2, # 'ด' + 19: 2, # 'ต' + 44: 3, # 'ถ' + 14: 3, # 'ท' + 48: 1, # 'ธ' + 3: 2, # 'น' + 17: 2, # 'บ' + 25: 3, # 'ป' + 39: 2, # 'ผ' + 62: 1, # 'ฝ' + 31: 2, # 'พ' + 54: 1, # 'ฟ' + 45: 1, # 'ภ' + 9: 3, # 'ม' + 16: 2, # 'ย' + 2: 3, # 'ร' + 61: 0, # 'ฤ' + 15: 2, # 'ล' + 12: 3, # 'ว' + 42: 2, # 'ศ' + 46: 2, # 'ษ' + 18: 2, # 'ส' + 21: 2, # 'ห' + 4: 3, # 'อ' + 63: 1, # 'ฯ' + 22: 3, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 0, # 'ำ' + 23: 3, # 'ิ' + 13: 3, # 'ี' + 40: 2, # 'ึ' + 27: 3, # 'ื' + 32: 3, # 'ุ' + 35: 3, # 'ู' + 11: 3, # 'เ' + 28: 3, # 'แ' + 41: 1, # 'โ' + 29: 2, # 'ใ' + 33: 1, # 'ไ' + 50: 0, # 'ๆ' + 37: 3, # '็' + 6: 3, # '่' + 7: 3, # '้' + 38: 3, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 61: { # 'ฤ' + 5: 0, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 2, # 'ต' + 44: 0, # 'ถ' + 14: 2, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 1, # 'ม' + 16: 0, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 2, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 15: { # 'ล' + 5: 2, # 'ก' + 30: 3, # 'ข' + 24: 1, # 'ค' + 8: 3, # 'ง' + 26: 1, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 2, # 'ด' + 19: 2, # 'ต' + 44: 1, # 'ถ' + 14: 2, # 'ท' + 48: 0, # 'ธ' + 3: 1, # 'น' + 17: 2, # 'บ' + 25: 2, # 'ป' + 39: 1, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 1, # 'ภ' + 9: 1, # 'ม' + 16: 3, # 'ย' + 2: 1, # 'ร' + 61: 0, # 'ฤ' + 15: 1, # 'ล' + 12: 1, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 2, # 'ส' + 21: 1, # 'ห' + 4: 3, # 'อ' + 63: 2, # 'ฯ' + 22: 3, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 2, # 'ำ' + 23: 3, # 'ิ' + 13: 3, # 'ี' + 40: 2, # 'ึ' + 27: 3, # 'ื' + 32: 2, # 'ุ' + 35: 3, # 'ู' + 11: 2, # 'เ' + 28: 1, # 'แ' + 41: 1, # 'โ' + 29: 2, # 'ใ' + 33: 1, # 'ไ' + 50: 0, # 'ๆ' + 37: 2, # '็' + 6: 3, # '่' + 7: 3, # '้' + 38: 2, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 12: { # 'ว' + 5: 3, # 'ก' + 30: 2, # 'ข' + 24: 1, # 'ค' + 8: 3, # 'ง' + 26: 2, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 1, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 1, # 'ณ' + 20: 2, # 'ด' + 19: 1, # 'ต' + 44: 1, # 'ถ' + 14: 1, # 'ท' + 48: 0, # 'ธ' + 3: 3, # 'น' + 17: 2, # 'บ' + 25: 1, # 'ป' + 39: 1, # 'ผ' + 62: 0, # 'ฝ' + 31: 1, # 'พ' + 54: 1, # 'ฟ' + 45: 0, # 'ภ' + 9: 3, # 'ม' + 16: 3, # 'ย' + 2: 3, # 'ร' + 61: 0, # 'ฤ' + 15: 3, # 'ล' + 12: 1, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 2, # 'ส' + 21: 2, # 'ห' + 4: 2, # 'อ' + 63: 0, # 'ฯ' + 22: 2, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 0, # 'ำ' + 23: 3, # 'ิ' + 13: 2, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 2, # 'ุ' + 35: 0, # 'ู' + 11: 3, # 'เ' + 28: 2, # 'แ' + 41: 1, # 'โ' + 29: 1, # 'ใ' + 33: 2, # 'ไ' + 50: 1, # 'ๆ' + 37: 0, # '็' + 6: 3, # '่' + 7: 3, # '้' + 38: 1, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 42: { # 'ศ' + 5: 1, # 'ก' + 30: 0, # 'ข' + 24: 1, # 'ค' + 8: 0, # 'ง' + 26: 1, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 1, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 1, # 'ต' + 44: 0, # 'ถ' + 14: 1, # 'ท' + 48: 0, # 'ธ' + 3: 2, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 2, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 2, # 'ว' + 42: 1, # 'ศ' + 46: 2, # 'ษ' + 18: 1, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 2, # 'ั' + 1: 3, # 'า' + 36: 0, # 'ำ' + 23: 2, # 'ิ' + 13: 0, # 'ี' + 40: 3, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 2, # 'ู' + 11: 0, # 'เ' + 28: 1, # 'แ' + 41: 0, # 'โ' + 29: 1, # 'ใ' + 33: 1, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 1, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 46: { # 'ษ' + 5: 0, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 2, # 'ฎ' + 57: 1, # 'ฏ' + 49: 2, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 3, # 'ณ' + 20: 0, # 'ด' + 19: 1, # 'ต' + 44: 0, # 'ถ' + 14: 1, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 1, # 'ภ' + 9: 1, # 'ม' + 16: 2, # 'ย' + 2: 2, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 1, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 2, # 'ะ' + 10: 2, # 'ั' + 1: 3, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 1, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 1, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 2, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 18: { # 'ส' + 5: 2, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 2, # 'ง' + 26: 1, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 3, # 'ด' + 19: 3, # 'ต' + 44: 3, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 3, # 'น' + 17: 2, # 'บ' + 25: 1, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 2, # 'ภ' + 9: 3, # 'ม' + 16: 1, # 'ย' + 2: 3, # 'ร' + 61: 0, # 'ฤ' + 15: 1, # 'ล' + 12: 2, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 2, # 'ห' + 4: 3, # 'อ' + 63: 0, # 'ฯ' + 22: 2, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 3, # 'ำ' + 23: 3, # 'ิ' + 13: 3, # 'ี' + 40: 2, # 'ึ' + 27: 3, # 'ื' + 32: 3, # 'ุ' + 35: 3, # 'ู' + 11: 2, # 'เ' + 28: 0, # 'แ' + 41: 1, # 'โ' + 29: 0, # 'ใ' + 33: 1, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 3, # '่' + 7: 1, # '้' + 38: 2, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 21: { # 'ห' + 5: 3, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 1, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 2, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 1, # 'ด' + 19: 3, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 3, # 'น' + 17: 0, # 'บ' + 25: 1, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 1, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 3, # 'ม' + 16: 2, # 'ย' + 2: 3, # 'ร' + 61: 0, # 'ฤ' + 15: 3, # 'ล' + 12: 2, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 3, # 'อ' + 63: 0, # 'ฯ' + 22: 1, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 0, # 'ำ' + 23: 1, # 'ิ' + 13: 1, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 1, # 'ุ' + 35: 1, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 3, # '็' + 6: 3, # '่' + 7: 3, # '้' + 38: 2, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 4: { # 'อ' + 5: 3, # 'ก' + 30: 1, # 'ข' + 24: 2, # 'ค' + 8: 3, # 'ง' + 26: 1, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 3, # 'ด' + 19: 2, # 'ต' + 44: 1, # 'ถ' + 14: 2, # 'ท' + 48: 1, # 'ธ' + 3: 3, # 'น' + 17: 3, # 'บ' + 25: 1, # 'ป' + 39: 1, # 'ผ' + 62: 0, # 'ฝ' + 31: 1, # 'พ' + 54: 1, # 'ฟ' + 45: 1, # 'ภ' + 9: 3, # 'ม' + 16: 3, # 'ย' + 2: 3, # 'ร' + 61: 0, # 'ฤ' + 15: 2, # 'ล' + 12: 2, # 'ว' + 42: 1, # 'ศ' + 46: 0, # 'ษ' + 18: 2, # 'ส' + 21: 2, # 'ห' + 4: 3, # 'อ' + 63: 0, # 'ฯ' + 22: 2, # 'ะ' + 10: 3, # 'ั' + 1: 3, # 'า' + 36: 2, # 'ำ' + 23: 2, # 'ิ' + 13: 3, # 'ี' + 40: 0, # 'ึ' + 27: 3, # 'ื' + 32: 3, # 'ุ' + 35: 0, # 'ู' + 11: 3, # 'เ' + 28: 1, # 'แ' + 41: 1, # 'โ' + 29: 2, # 'ใ' + 33: 2, # 'ไ' + 50: 1, # 'ๆ' + 37: 1, # '็' + 6: 2, # '่' + 7: 2, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 63: { # 'ฯ' + 5: 0, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 2, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 22: { # 'ะ' + 5: 3, # 'ก' + 30: 1, # 'ข' + 24: 2, # 'ค' + 8: 1, # 'ง' + 26: 2, # 'จ' + 52: 0, # 'ฉ' + 34: 3, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 3, # 'ด' + 19: 3, # 'ต' + 44: 1, # 'ถ' + 14: 3, # 'ท' + 48: 1, # 'ธ' + 3: 2, # 'น' + 17: 3, # 'บ' + 25: 2, # 'ป' + 39: 1, # 'ผ' + 62: 0, # 'ฝ' + 31: 2, # 'พ' + 54: 0, # 'ฟ' + 45: 1, # 'ภ' + 9: 3, # 'ม' + 16: 2, # 'ย' + 2: 2, # 'ร' + 61: 0, # 'ฤ' + 15: 2, # 'ล' + 12: 2, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 3, # 'ส' + 21: 3, # 'ห' + 4: 2, # 'อ' + 63: 1, # 'ฯ' + 22: 1, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 3, # 'เ' + 28: 2, # 'แ' + 41: 1, # 'โ' + 29: 2, # 'ใ' + 33: 2, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 10: { # 'ั' + 5: 3, # 'ก' + 30: 0, # 'ข' + 24: 1, # 'ค' + 8: 3, # 'ง' + 26: 3, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 0, # 'ซ' + 47: 3, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 2, # 'ฐ' + 53: 0, # 'ฑ' + 55: 3, # 'ฒ' + 43: 3, # 'ณ' + 20: 3, # 'ด' + 19: 3, # 'ต' + 44: 0, # 'ถ' + 14: 2, # 'ท' + 48: 0, # 'ธ' + 3: 3, # 'น' + 17: 3, # 'บ' + 25: 1, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 2, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 3, # 'ม' + 16: 3, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 2, # 'ล' + 12: 3, # 'ว' + 42: 2, # 'ศ' + 46: 0, # 'ษ' + 18: 3, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 3, # '่' + 7: 3, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 1: { # 'า' + 5: 3, # 'ก' + 30: 2, # 'ข' + 24: 3, # 'ค' + 8: 3, # 'ง' + 26: 3, # 'จ' + 52: 0, # 'ฉ' + 34: 3, # 'ช' + 51: 1, # 'ซ' + 47: 2, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 3, # 'ณ' + 20: 3, # 'ด' + 19: 3, # 'ต' + 44: 1, # 'ถ' + 14: 3, # 'ท' + 48: 2, # 'ธ' + 3: 3, # 'น' + 17: 3, # 'บ' + 25: 2, # 'ป' + 39: 1, # 'ผ' + 62: 1, # 'ฝ' + 31: 3, # 'พ' + 54: 1, # 'ฟ' + 45: 1, # 'ภ' + 9: 3, # 'ม' + 16: 3, # 'ย' + 2: 3, # 'ร' + 61: 0, # 'ฤ' + 15: 3, # 'ล' + 12: 3, # 'ว' + 42: 2, # 'ศ' + 46: 3, # 'ษ' + 18: 3, # 'ส' + 21: 3, # 'ห' + 4: 2, # 'อ' + 63: 1, # 'ฯ' + 22: 3, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 3, # 'เ' + 28: 2, # 'แ' + 41: 1, # 'โ' + 29: 2, # 'ใ' + 33: 2, # 'ไ' + 50: 1, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 36: { # 'ำ' + 5: 2, # 'ก' + 30: 1, # 'ข' + 24: 3, # 'ค' + 8: 2, # 'ง' + 26: 1, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 1, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 1, # 'ด' + 19: 1, # 'ต' + 44: 1, # 'ถ' + 14: 1, # 'ท' + 48: 0, # 'ธ' + 3: 3, # 'น' + 17: 1, # 'บ' + 25: 1, # 'ป' + 39: 1, # 'ผ' + 62: 0, # 'ฝ' + 31: 1, # 'พ' + 54: 0, # 'ฟ' + 45: 1, # 'ภ' + 9: 1, # 'ม' + 16: 0, # 'ย' + 2: 2, # 'ร' + 61: 0, # 'ฤ' + 15: 2, # 'ล' + 12: 1, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 1, # 'ส' + 21: 3, # 'ห' + 4: 1, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 3, # 'เ' + 28: 2, # 'แ' + 41: 1, # 'โ' + 29: 2, # 'ใ' + 33: 2, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 23: { # 'ิ' + 5: 3, # 'ก' + 30: 1, # 'ข' + 24: 2, # 'ค' + 8: 3, # 'ง' + 26: 3, # 'จ' + 52: 0, # 'ฉ' + 34: 3, # 'ช' + 51: 0, # 'ซ' + 47: 2, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 3, # 'ด' + 19: 3, # 'ต' + 44: 1, # 'ถ' + 14: 3, # 'ท' + 48: 3, # 'ธ' + 3: 3, # 'น' + 17: 3, # 'บ' + 25: 2, # 'ป' + 39: 2, # 'ผ' + 62: 0, # 'ฝ' + 31: 3, # 'พ' + 54: 1, # 'ฟ' + 45: 2, # 'ภ' + 9: 3, # 'ม' + 16: 2, # 'ย' + 2: 2, # 'ร' + 61: 0, # 'ฤ' + 15: 2, # 'ล' + 12: 3, # 'ว' + 42: 3, # 'ศ' + 46: 2, # 'ษ' + 18: 2, # 'ส' + 21: 3, # 'ห' + 4: 1, # 'อ' + 63: 1, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 3, # 'เ' + 28: 1, # 'แ' + 41: 1, # 'โ' + 29: 1, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 3, # '่' + 7: 2, # '้' + 38: 2, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 13: { # 'ี' + 5: 3, # 'ก' + 30: 2, # 'ข' + 24: 2, # 'ค' + 8: 0, # 'ง' + 26: 1, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 2, # 'ด' + 19: 1, # 'ต' + 44: 0, # 'ถ' + 14: 2, # 'ท' + 48: 0, # 'ธ' + 3: 1, # 'น' + 17: 2, # 'บ' + 25: 2, # 'ป' + 39: 1, # 'ผ' + 62: 0, # 'ฝ' + 31: 2, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 2, # 'ม' + 16: 3, # 'ย' + 2: 2, # 'ร' + 61: 0, # 'ฤ' + 15: 1, # 'ล' + 12: 2, # 'ว' + 42: 1, # 'ศ' + 46: 0, # 'ษ' + 18: 2, # 'ส' + 21: 1, # 'ห' + 4: 2, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 2, # 'เ' + 28: 2, # 'แ' + 41: 1, # 'โ' + 29: 1, # 'ใ' + 33: 1, # 'ไ' + 50: 1, # 'ๆ' + 37: 0, # '็' + 6: 3, # '่' + 7: 3, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 40: { # 'ึ' + 5: 3, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 3, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 1, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 1, # 'ม' + 16: 0, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 3, # '่' + 7: 3, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 27: { # 'ื' + 5: 0, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 1, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 2, # 'น' + 17: 3, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 2, # 'ม' + 16: 0, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 3, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 3, # '่' + 7: 3, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 32: { # 'ุ' + 5: 3, # 'ก' + 30: 2, # 'ข' + 24: 3, # 'ค' + 8: 3, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 2, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 1, # 'ฒ' + 43: 3, # 'ณ' + 20: 3, # 'ด' + 19: 3, # 'ต' + 44: 1, # 'ถ' + 14: 2, # 'ท' + 48: 1, # 'ธ' + 3: 2, # 'น' + 17: 2, # 'บ' + 25: 2, # 'ป' + 39: 2, # 'ผ' + 62: 0, # 'ฝ' + 31: 1, # 'พ' + 54: 0, # 'ฟ' + 45: 1, # 'ภ' + 9: 3, # 'ม' + 16: 1, # 'ย' + 2: 2, # 'ร' + 61: 0, # 'ฤ' + 15: 2, # 'ล' + 12: 1, # 'ว' + 42: 1, # 'ศ' + 46: 2, # 'ษ' + 18: 1, # 'ส' + 21: 1, # 'ห' + 4: 1, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 1, # 'เ' + 28: 0, # 'แ' + 41: 1, # 'โ' + 29: 0, # 'ใ' + 33: 1, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 3, # '่' + 7: 2, # '้' + 38: 1, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 35: { # 'ู' + 5: 3, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 2, # 'ง' + 26: 1, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 2, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 1, # 'ณ' + 20: 2, # 'ด' + 19: 2, # 'ต' + 44: 0, # 'ถ' + 14: 1, # 'ท' + 48: 0, # 'ธ' + 3: 2, # 'น' + 17: 0, # 'บ' + 25: 3, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 2, # 'ม' + 16: 0, # 'ย' + 2: 1, # 'ร' + 61: 0, # 'ฤ' + 15: 3, # 'ล' + 12: 1, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 1, # 'เ' + 28: 1, # 'แ' + 41: 1, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 3, # '่' + 7: 3, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 11: { # 'เ' + 5: 3, # 'ก' + 30: 3, # 'ข' + 24: 3, # 'ค' + 8: 2, # 'ง' + 26: 3, # 'จ' + 52: 3, # 'ฉ' + 34: 3, # 'ช' + 51: 2, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 1, # 'ณ' + 20: 3, # 'ด' + 19: 3, # 'ต' + 44: 1, # 'ถ' + 14: 3, # 'ท' + 48: 1, # 'ธ' + 3: 3, # 'น' + 17: 3, # 'บ' + 25: 3, # 'ป' + 39: 2, # 'ผ' + 62: 1, # 'ฝ' + 31: 3, # 'พ' + 54: 1, # 'ฟ' + 45: 3, # 'ภ' + 9: 3, # 'ม' + 16: 2, # 'ย' + 2: 3, # 'ร' + 61: 0, # 'ฤ' + 15: 3, # 'ล' + 12: 3, # 'ว' + 42: 2, # 'ศ' + 46: 0, # 'ษ' + 18: 3, # 'ส' + 21: 3, # 'ห' + 4: 3, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 28: { # 'แ' + 5: 3, # 'ก' + 30: 2, # 'ข' + 24: 2, # 'ค' + 8: 1, # 'ง' + 26: 2, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 2, # 'ด' + 19: 3, # 'ต' + 44: 2, # 'ถ' + 14: 3, # 'ท' + 48: 0, # 'ธ' + 3: 3, # 'น' + 17: 3, # 'บ' + 25: 2, # 'ป' + 39: 3, # 'ผ' + 62: 0, # 'ฝ' + 31: 2, # 'พ' + 54: 2, # 'ฟ' + 45: 0, # 'ภ' + 9: 2, # 'ม' + 16: 2, # 'ย' + 2: 2, # 'ร' + 61: 0, # 'ฤ' + 15: 3, # 'ล' + 12: 2, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 3, # 'ส' + 21: 3, # 'ห' + 4: 1, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 41: { # 'โ' + 5: 2, # 'ก' + 30: 1, # 'ข' + 24: 2, # 'ค' + 8: 0, # 'ง' + 26: 1, # 'จ' + 52: 1, # 'ฉ' + 34: 1, # 'ช' + 51: 1, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 3, # 'ด' + 19: 2, # 'ต' + 44: 0, # 'ถ' + 14: 2, # 'ท' + 48: 0, # 'ธ' + 3: 3, # 'น' + 17: 1, # 'บ' + 25: 3, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 1, # 'พ' + 54: 1, # 'ฟ' + 45: 1, # 'ภ' + 9: 1, # 'ม' + 16: 2, # 'ย' + 2: 2, # 'ร' + 61: 0, # 'ฤ' + 15: 3, # 'ล' + 12: 0, # 'ว' + 42: 1, # 'ศ' + 46: 0, # 'ษ' + 18: 2, # 'ส' + 21: 0, # 'ห' + 4: 2, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 29: { # 'ใ' + 5: 2, # 'ก' + 30: 0, # 'ข' + 24: 1, # 'ค' + 8: 0, # 'ง' + 26: 3, # 'จ' + 52: 0, # 'ฉ' + 34: 3, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 3, # 'ด' + 19: 1, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 3, # 'น' + 17: 2, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 1, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 3, # 'ส' + 21: 3, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 33: { # 'ไ' + 5: 1, # 'ก' + 30: 2, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 1, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 3, # 'ด' + 19: 1, # 'ต' + 44: 0, # 'ถ' + 14: 3, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 1, # 'บ' + 25: 3, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 2, # 'ฟ' + 45: 0, # 'ภ' + 9: 3, # 'ม' + 16: 0, # 'ย' + 2: 3, # 'ร' + 61: 0, # 'ฤ' + 15: 1, # 'ล' + 12: 3, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 1, # 'ส' + 21: 2, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 50: { # 'ๆ' + 5: 0, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 37: { # '็' + 5: 2, # 'ก' + 30: 1, # 'ข' + 24: 2, # 'ค' + 8: 2, # 'ง' + 26: 3, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 1, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 1, # 'ด' + 19: 2, # 'ต' + 44: 0, # 'ถ' + 14: 1, # 'ท' + 48: 0, # 'ธ' + 3: 3, # 'น' + 17: 3, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 2, # 'ม' + 16: 1, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 2, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 1, # 'ส' + 21: 0, # 'ห' + 4: 1, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 1, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 1, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 6: { # '่' + 5: 2, # 'ก' + 30: 1, # 'ข' + 24: 2, # 'ค' + 8: 3, # 'ง' + 26: 2, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 1, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 1, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 1, # 'ด' + 19: 2, # 'ต' + 44: 1, # 'ถ' + 14: 2, # 'ท' + 48: 1, # 'ธ' + 3: 3, # 'น' + 17: 1, # 'บ' + 25: 2, # 'ป' + 39: 2, # 'ผ' + 62: 1, # 'ฝ' + 31: 1, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 3, # 'ม' + 16: 3, # 'ย' + 2: 2, # 'ร' + 61: 0, # 'ฤ' + 15: 2, # 'ล' + 12: 3, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 2, # 'ส' + 21: 1, # 'ห' + 4: 3, # 'อ' + 63: 0, # 'ฯ' + 22: 1, # 'ะ' + 10: 0, # 'ั' + 1: 3, # 'า' + 36: 2, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 3, # 'เ' + 28: 2, # 'แ' + 41: 1, # 'โ' + 29: 2, # 'ใ' + 33: 2, # 'ไ' + 50: 1, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 7: { # '้' + 5: 2, # 'ก' + 30: 1, # 'ข' + 24: 2, # 'ค' + 8: 3, # 'ง' + 26: 2, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 1, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 1, # 'ด' + 19: 2, # 'ต' + 44: 1, # 'ถ' + 14: 2, # 'ท' + 48: 0, # 'ธ' + 3: 3, # 'น' + 17: 2, # 'บ' + 25: 2, # 'ป' + 39: 2, # 'ผ' + 62: 0, # 'ฝ' + 31: 1, # 'พ' + 54: 1, # 'ฟ' + 45: 0, # 'ภ' + 9: 3, # 'ม' + 16: 2, # 'ย' + 2: 2, # 'ร' + 61: 0, # 'ฤ' + 15: 1, # 'ล' + 12: 3, # 'ว' + 42: 1, # 'ศ' + 46: 0, # 'ษ' + 18: 2, # 'ส' + 21: 2, # 'ห' + 4: 3, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 3, # 'า' + 36: 2, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 2, # 'เ' + 28: 2, # 'แ' + 41: 1, # 'โ' + 29: 2, # 'ใ' + 33: 2, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 38: { # '์' + 5: 2, # 'ก' + 30: 1, # 'ข' + 24: 1, # 'ค' + 8: 0, # 'ง' + 26: 1, # 'จ' + 52: 0, # 'ฉ' + 34: 1, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 2, # 'ด' + 19: 1, # 'ต' + 44: 1, # 'ถ' + 14: 1, # 'ท' + 48: 0, # 'ธ' + 3: 1, # 'น' + 17: 1, # 'บ' + 25: 1, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 1, # 'พ' + 54: 1, # 'ฟ' + 45: 0, # 'ภ' + 9: 2, # 'ม' + 16: 0, # 'ย' + 2: 1, # 'ร' + 61: 1, # 'ฤ' + 15: 1, # 'ล' + 12: 1, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 1, # 'ส' + 21: 1, # 'ห' + 4: 2, # 'อ' + 63: 1, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 2, # 'เ' + 28: 2, # 'แ' + 41: 1, # 'โ' + 29: 1, # 'ใ' + 33: 1, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 0, # '๑' + 59: 0, # '๒' + 60: 0, # '๕' + }, + 56: { # '๑' + 5: 0, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 2, # '๑' + 59: 1, # '๒' + 60: 1, # '๕' + }, + 59: { # '๒' + 5: 0, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 1, # '๑' + 59: 1, # '๒' + 60: 3, # '๕' + }, + 60: { # '๕' + 5: 0, # 'ก' + 30: 0, # 'ข' + 24: 0, # 'ค' + 8: 0, # 'ง' + 26: 0, # 'จ' + 52: 0, # 'ฉ' + 34: 0, # 'ช' + 51: 0, # 'ซ' + 47: 0, # 'ญ' + 58: 0, # 'ฎ' + 57: 0, # 'ฏ' + 49: 0, # 'ฐ' + 53: 0, # 'ฑ' + 55: 0, # 'ฒ' + 43: 0, # 'ณ' + 20: 0, # 'ด' + 19: 0, # 'ต' + 44: 0, # 'ถ' + 14: 0, # 'ท' + 48: 0, # 'ธ' + 3: 0, # 'น' + 17: 0, # 'บ' + 25: 0, # 'ป' + 39: 0, # 'ผ' + 62: 0, # 'ฝ' + 31: 0, # 'พ' + 54: 0, # 'ฟ' + 45: 0, # 'ภ' + 9: 0, # 'ม' + 16: 0, # 'ย' + 2: 0, # 'ร' + 61: 0, # 'ฤ' + 15: 0, # 'ล' + 12: 0, # 'ว' + 42: 0, # 'ศ' + 46: 0, # 'ษ' + 18: 0, # 'ส' + 21: 0, # 'ห' + 4: 0, # 'อ' + 63: 0, # 'ฯ' + 22: 0, # 'ะ' + 10: 0, # 'ั' + 1: 0, # 'า' + 36: 0, # 'ำ' + 23: 0, # 'ิ' + 13: 0, # 'ี' + 40: 0, # 'ึ' + 27: 0, # 'ื' + 32: 0, # 'ุ' + 35: 0, # 'ู' + 11: 0, # 'เ' + 28: 0, # 'แ' + 41: 0, # 'โ' + 29: 0, # 'ใ' + 33: 0, # 'ไ' + 50: 0, # 'ๆ' + 37: 0, # '็' + 6: 0, # '่' + 7: 0, # '้' + 38: 0, # '์' + 56: 2, # '๑' + 59: 1, # '๒' + 60: 0, # '๕' + }, +} + +# 255: Undefined characters that did not exist in training text # 254: Carriage/Return # 253: symbol (punctuation) that does not belong to word # 252: 0 - 9 +# 251: Control characters -# The following result for thai was collected from a limited sample (1M). - -# Character Mapping Table: -TIS620CharToOrderMap = ( -255,255,255,255,255,255,255,255,255,255,254,255,255,254,255,255, # 00 -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, # 10 -253,253,253,253,253,253,253,253,253,253,253,253,253,253,253,253, # 20 -252,252,252,252,252,252,252,252,252,252,253,253,253,253,253,253, # 30 -253,182,106,107,100,183,184,185,101, 94,186,187,108,109,110,111, # 40 -188,189,190, 89, 95,112,113,191,192,193,194,253,253,253,253,253, # 50 -253, 64, 72, 73,114, 74,115,116,102, 81,201,117, 90,103, 78, 82, # 60 - 96,202, 91, 79, 84,104,105, 97, 98, 92,203,253,253,253,253,253, # 70 -209,210,211,212,213, 88,214,215,216,217,218,219,220,118,221,222, -223,224, 99, 85, 83,225,226,227,228,229,230,231,232,233,234,235, -236, 5, 30,237, 24,238, 75, 8, 26, 52, 34, 51,119, 47, 58, 57, - 49, 53, 55, 43, 20, 19, 44, 14, 48, 3, 17, 25, 39, 62, 31, 54, - 45, 9, 16, 2, 61, 15,239, 12, 42, 46, 18, 21, 76, 4, 66, 63, - 22, 10, 1, 36, 23, 13, 40, 27, 32, 35, 86,240,241,242,243,244, - 11, 28, 41, 29, 33,245, 50, 37, 6, 7, 67, 77, 38, 93,246,247, - 68, 56, 59, 65, 69, 60, 70, 80, 71, 87,248,249,250,251,252,253, -) +# Character Mapping Table(s): +TIS_620_THAI_CHAR_TO_ORDER = { + 0: 255, # '\x00' + 1: 255, # '\x01' + 2: 255, # '\x02' + 3: 255, # '\x03' + 4: 255, # '\x04' + 5: 255, # '\x05' + 6: 255, # '\x06' + 7: 255, # '\x07' + 8: 255, # '\x08' + 9: 255, # '\t' + 10: 254, # '\n' + 11: 255, # '\x0b' + 12: 255, # '\x0c' + 13: 254, # '\r' + 14: 255, # '\x0e' + 15: 255, # '\x0f' + 16: 255, # '\x10' + 17: 255, # '\x11' + 18: 255, # '\x12' + 19: 255, # '\x13' + 20: 255, # '\x14' + 21: 255, # '\x15' + 22: 255, # '\x16' + 23: 255, # '\x17' + 24: 255, # '\x18' + 25: 255, # '\x19' + 26: 255, # '\x1a' + 27: 255, # '\x1b' + 28: 255, # '\x1c' + 29: 255, # '\x1d' + 30: 255, # '\x1e' + 31: 255, # '\x1f' + 32: 253, # ' ' + 33: 253, # '!' + 34: 253, # '"' + 35: 253, # '#' + 36: 253, # '$' + 37: 253, # '%' + 38: 253, # '&' + 39: 253, # "'" + 40: 253, # '(' + 41: 253, # ')' + 42: 253, # '*' + 43: 253, # '+' + 44: 253, # ',' + 45: 253, # '-' + 46: 253, # '.' + 47: 253, # '/' + 48: 252, # '0' + 49: 252, # '1' + 50: 252, # '2' + 51: 252, # '3' + 52: 252, # '4' + 53: 252, # '5' + 54: 252, # '6' + 55: 252, # '7' + 56: 252, # '8' + 57: 252, # '9' + 58: 253, # ':' + 59: 253, # ';' + 60: 253, # '<' + 61: 253, # '=' + 62: 253, # '>' + 63: 253, # '?' + 64: 253, # '@' + 65: 182, # 'A' + 66: 106, # 'B' + 67: 107, # 'C' + 68: 100, # 'D' + 69: 183, # 'E' + 70: 184, # 'F' + 71: 185, # 'G' + 72: 101, # 'H' + 73: 94, # 'I' + 74: 186, # 'J' + 75: 187, # 'K' + 76: 108, # 'L' + 77: 109, # 'M' + 78: 110, # 'N' + 79: 111, # 'O' + 80: 188, # 'P' + 81: 189, # 'Q' + 82: 190, # 'R' + 83: 89, # 'S' + 84: 95, # 'T' + 85: 112, # 'U' + 86: 113, # 'V' + 87: 191, # 'W' + 88: 192, # 'X' + 89: 193, # 'Y' + 90: 194, # 'Z' + 91: 253, # '[' + 92: 253, # '\\' + 93: 253, # ']' + 94: 253, # '^' + 95: 253, # '_' + 96: 253, # '`' + 97: 64, # 'a' + 98: 72, # 'b' + 99: 73, # 'c' + 100: 114, # 'd' + 101: 74, # 'e' + 102: 115, # 'f' + 103: 116, # 'g' + 104: 102, # 'h' + 105: 81, # 'i' + 106: 201, # 'j' + 107: 117, # 'k' + 108: 90, # 'l' + 109: 103, # 'm' + 110: 78, # 'n' + 111: 82, # 'o' + 112: 96, # 'p' + 113: 202, # 'q' + 114: 91, # 'r' + 115: 79, # 's' + 116: 84, # 't' + 117: 104, # 'u' + 118: 105, # 'v' + 119: 97, # 'w' + 120: 98, # 'x' + 121: 92, # 'y' + 122: 203, # 'z' + 123: 253, # '{' + 124: 253, # '|' + 125: 253, # '}' + 126: 253, # '~' + 127: 253, # '\x7f' + 128: 209, # '\x80' + 129: 210, # '\x81' + 130: 211, # '\x82' + 131: 212, # '\x83' + 132: 213, # '\x84' + 133: 88, # '\x85' + 134: 214, # '\x86' + 135: 215, # '\x87' + 136: 216, # '\x88' + 137: 217, # '\x89' + 138: 218, # '\x8a' + 139: 219, # '\x8b' + 140: 220, # '\x8c' + 141: 118, # '\x8d' + 142: 221, # '\x8e' + 143: 222, # '\x8f' + 144: 223, # '\x90' + 145: 224, # '\x91' + 146: 99, # '\x92' + 147: 85, # '\x93' + 148: 83, # '\x94' + 149: 225, # '\x95' + 150: 226, # '\x96' + 151: 227, # '\x97' + 152: 228, # '\x98' + 153: 229, # '\x99' + 154: 230, # '\x9a' + 155: 231, # '\x9b' + 156: 232, # '\x9c' + 157: 233, # '\x9d' + 158: 234, # '\x9e' + 159: 235, # '\x9f' + 160: 236, # None + 161: 5, # 'ก' + 162: 30, # 'ข' + 163: 237, # 'ฃ' + 164: 24, # 'ค' + 165: 238, # 'ฅ' + 166: 75, # 'ฆ' + 167: 8, # 'ง' + 168: 26, # 'จ' + 169: 52, # 'ฉ' + 170: 34, # 'ช' + 171: 51, # 'ซ' + 172: 119, # 'ฌ' + 173: 47, # 'ญ' + 174: 58, # 'ฎ' + 175: 57, # 'ฏ' + 176: 49, # 'ฐ' + 177: 53, # 'ฑ' + 178: 55, # 'ฒ' + 179: 43, # 'ณ' + 180: 20, # 'ด' + 181: 19, # 'ต' + 182: 44, # 'ถ' + 183: 14, # 'ท' + 184: 48, # 'ธ' + 185: 3, # 'น' + 186: 17, # 'บ' + 187: 25, # 'ป' + 188: 39, # 'ผ' + 189: 62, # 'ฝ' + 190: 31, # 'พ' + 191: 54, # 'ฟ' + 192: 45, # 'ภ' + 193: 9, # 'ม' + 194: 16, # 'ย' + 195: 2, # 'ร' + 196: 61, # 'ฤ' + 197: 15, # 'ล' + 198: 239, # 'ฦ' + 199: 12, # 'ว' + 200: 42, # 'ศ' + 201: 46, # 'ษ' + 202: 18, # 'ส' + 203: 21, # 'ห' + 204: 76, # 'ฬ' + 205: 4, # 'อ' + 206: 66, # 'ฮ' + 207: 63, # 'ฯ' + 208: 22, # 'ะ' + 209: 10, # 'ั' + 210: 1, # 'า' + 211: 36, # 'ำ' + 212: 23, # 'ิ' + 213: 13, # 'ี' + 214: 40, # 'ึ' + 215: 27, # 'ื' + 216: 32, # 'ุ' + 217: 35, # 'ู' + 218: 86, # 'ฺ' + 219: 240, # None + 220: 241, # None + 221: 242, # None + 222: 243, # None + 223: 244, # '฿' + 224: 11, # 'เ' + 225: 28, # 'แ' + 226: 41, # 'โ' + 227: 29, # 'ใ' + 228: 33, # 'ไ' + 229: 245, # 'ๅ' + 230: 50, # 'ๆ' + 231: 37, # '็' + 232: 6, # '่' + 233: 7, # '้' + 234: 67, # '๊' + 235: 77, # '๋' + 236: 38, # '์' + 237: 93, # 'ํ' + 238: 246, # '๎' + 239: 247, # '๏' + 240: 68, # '๐' + 241: 56, # '๑' + 242: 59, # '๒' + 243: 65, # '๓' + 244: 69, # '๔' + 245: 60, # '๕' + 246: 70, # '๖' + 247: 80, # '๗' + 248: 71, # '๘' + 249: 87, # '๙' + 250: 248, # '๚' + 251: 249, # '๛' + 252: 250, # None + 253: 251, # None + 254: 252, # None + 255: 253, # None +} -# Model Table: -# total sequences: 100% -# first 512 sequences: 92.6386% -# first 1024 sequences:7.3177% -# rest sequences: 1.0230% -# negative sequences: 0.0436% -ThaiLangModel = ( -0,1,3,3,3,3,0,0,3,3,0,3,3,0,3,3,3,3,3,3,3,3,0,0,3,3,3,0,3,3,3,3, -0,3,3,0,0,0,1,3,0,3,3,2,3,3,0,1,2,3,3,3,3,0,2,0,2,0,0,3,2,1,2,2, -3,0,3,3,2,3,0,0,3,3,0,3,3,0,3,3,3,3,3,3,3,3,3,0,3,2,3,0,2,2,2,3, -0,2,3,0,0,0,0,1,0,1,2,3,1,1,3,2,2,0,1,1,0,0,1,0,0,0,0,0,0,0,1,1, -3,3,3,2,3,3,3,3,3,3,3,3,3,3,3,2,2,2,2,2,2,2,3,3,2,3,2,3,3,2,2,2, -3,1,2,3,0,3,3,2,2,1,2,3,3,1,2,0,1,3,0,1,0,0,1,0,0,0,0,0,0,0,1,1, -3,3,2,2,3,3,3,3,1,2,3,3,3,3,3,2,2,2,2,3,3,2,2,3,3,2,2,3,2,3,2,2, -3,3,1,2,3,1,2,2,3,3,1,0,2,1,0,0,3,1,2,1,0,0,1,0,0,0,0,0,0,1,0,1, -3,3,3,3,3,3,2,2,3,3,3,3,2,3,2,2,3,3,2,2,3,2,2,2,2,1,1,3,1,2,1,1, -3,2,1,0,2,1,0,1,0,1,1,0,1,1,0,0,1,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0, -3,3,3,2,3,2,3,3,2,2,3,2,3,3,2,3,1,1,2,3,2,2,2,3,2,2,2,2,2,1,2,1, -2,2,1,1,3,3,2,1,0,1,2,2,0,1,3,0,0,0,1,1,0,0,0,0,0,2,3,0,0,2,1,1, -3,3,2,3,3,2,0,0,3,3,0,3,3,0,2,2,3,1,2,2,1,1,1,0,2,2,2,0,2,2,1,1, -0,2,1,0,2,0,0,2,0,1,0,0,1,0,0,0,1,1,1,1,0,0,0,0,0,0,0,0,0,0,1,0, -3,3,2,3,3,2,0,0,3,3,0,2,3,0,2,1,2,2,2,2,1,2,0,0,2,2,2,0,2,2,1,1, -0,2,1,0,2,0,0,2,0,1,1,0,1,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0, -3,3,2,3,2,3,2,0,2,2,1,3,2,1,3,2,1,2,3,2,2,3,0,2,3,2,2,1,2,2,2,2, -1,2,2,0,0,0,0,2,0,1,2,0,1,1,1,0,1,0,3,1,1,0,0,0,0,0,0,0,0,0,1,0, -3,3,2,3,3,2,3,2,2,2,3,2,2,3,2,2,1,2,3,2,2,3,1,3,2,2,2,3,2,2,2,3, -3,2,1,3,0,1,1,1,0,2,1,1,1,1,1,0,1,0,1,1,0,0,0,0,0,0,0,0,0,2,0,0, -1,0,0,3,0,3,3,3,3,3,0,0,3,0,2,2,3,3,3,3,3,0,0,0,1,1,3,0,0,0,0,2, -0,0,1,0,0,0,0,0,0,0,2,3,0,0,0,3,0,2,0,0,0,0,0,3,0,0,0,0,0,0,0,0, -2,0,3,3,3,3,0,0,2,3,0,0,3,0,3,3,2,3,3,3,3,3,0,0,3,3,3,0,0,0,3,3, -0,0,3,0,0,0,0,2,0,0,2,1,1,3,0,0,1,0,0,2,3,0,1,0,0,0,0,0,0,0,1,0, -3,3,3,3,2,3,3,3,3,3,3,3,1,2,1,3,3,2,2,1,2,2,2,3,1,1,2,0,2,1,2,1, -2,2,1,0,0,0,1,1,0,1,0,1,1,0,0,0,0,0,1,1,0,0,1,0,0,0,0,0,0,0,0,0, -3,0,2,1,2,3,3,3,0,2,0,2,2,0,2,1,3,2,2,1,2,1,0,0,2,2,1,0,2,1,2,2, -0,1,1,0,0,0,0,1,0,1,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,2,1,3,3,1,1,3,0,2,3,1,1,3,2,1,1,2,0,2,2,3,2,1,1,1,1,1,2, -3,0,0,1,3,1,2,1,2,0,3,0,0,0,1,0,3,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0, -3,3,1,1,3,2,3,3,3,1,3,2,1,3,2,1,3,2,2,2,2,1,3,3,1,2,1,3,1,2,3,0, -2,1,1,3,2,2,2,1,2,1,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2, -3,3,2,3,2,3,3,2,3,2,3,2,3,3,2,1,0,3,2,2,2,1,2,2,2,1,2,2,1,2,1,1, -2,2,2,3,0,1,3,1,1,1,1,0,1,1,0,2,1,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,2,3,2,2,1,1,3,2,3,2,3,2,0,3,2,2,1,2,0,2,2,2,1,2,2,2,2,1, -3,2,1,2,2,1,0,2,0,1,0,0,1,1,0,0,0,0,0,1,1,0,1,0,0,0,0,0,0,0,0,1, -3,3,3,3,3,2,3,1,2,3,3,2,2,3,0,1,1,2,0,3,3,2,2,3,0,1,1,3,0,0,0,0, -3,1,0,3,3,0,2,0,2,1,0,0,3,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,2,3,2,3,3,0,1,3,1,1,2,1,2,1,1,3,1,1,0,2,3,1,1,1,1,1,1,1,1, -3,1,1,2,2,2,2,1,1,1,0,0,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, -3,2,2,1,1,2,1,3,3,2,3,2,2,3,2,2,3,1,2,2,1,2,0,3,2,1,2,2,2,2,2,1, -3,2,1,2,2,2,1,1,1,1,0,0,1,1,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,3,1,3,3,0,2,1,0,3,2,0,0,3,1,0,1,1,0,1,0,0,0,0,0,1, -1,0,0,1,0,3,2,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,0,2,2,2,3,0,0,1,3,0,3,2,0,3,2,2,3,3,3,3,3,1,0,2,2,2,0,2,2,1,2, -0,2,3,0,0,0,0,1,0,1,0,0,1,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, -3,0,2,3,1,3,3,2,3,3,0,3,3,0,3,2,2,3,2,3,3,3,0,0,2,2,3,0,1,1,1,3, -0,0,3,0,0,0,2,2,0,1,3,0,1,2,2,2,3,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1, -3,2,3,3,2,0,3,3,2,2,3,1,3,2,1,3,2,0,1,2,2,0,2,3,2,1,0,3,0,0,0,0, -3,0,0,2,3,1,3,0,0,3,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,1,3,2,2,2,1,2,0,1,3,1,1,3,1,3,0,0,2,1,1,1,1,2,1,1,1,0,2,1,0,1, -1,2,0,0,0,3,1,1,0,0,0,0,1,0,1,0,0,1,0,1,0,0,0,0,0,3,1,0,0,0,1,0, -3,3,3,3,2,2,2,2,2,1,3,1,1,1,2,0,1,1,2,1,2,1,3,2,0,0,3,1,1,1,1,1, -3,1,0,2,3,0,0,0,3,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,2,3,0,3,3,0,2,0,0,0,0,0,0,0,3,0,0,1,0,0,0,0,0,0,0,0,0,0,0, -0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,2,3,1,3,0,0,1,2,0,0,2,0,3,3,2,3,3,3,2,3,0,0,2,2,2,0,0,0,2,2, -0,0,1,0,0,0,0,3,0,0,0,0,2,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0, -0,0,0,3,0,2,0,0,0,0,0,0,0,0,0,0,1,2,3,1,3,3,0,0,1,0,3,0,0,0,0,0, -0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,1,2,3,1,2,3,1,0,3,0,2,2,1,0,2,1,1,2,0,1,0,0,1,1,1,1,0,1,0,0, -1,0,0,0,0,1,1,0,3,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,2,1,0,1,1,1,3,1,2,2,2,2,2,2,1,1,1,1,0,3,1,0,1,3,1,1,1,1, -1,1,0,2,0,1,3,1,1,0,0,1,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,2,0,1, -3,0,2,2,1,3,3,2,3,3,0,1,1,0,2,2,1,2,1,3,3,1,0,0,3,2,0,0,0,0,2,1, -0,1,0,0,0,0,1,2,0,1,1,3,1,1,2,2,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0, -0,0,3,0,0,1,0,0,0,3,0,0,3,0,3,1,0,1,1,1,3,2,0,0,0,3,0,0,0,0,2,0, -0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,2,0,0,0,0,0,0,0,0,0, -3,3,1,3,2,1,3,3,1,2,2,0,1,2,1,0,1,2,0,0,0,0,0,3,0,0,0,3,0,0,0,0, -3,0,0,1,1,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,0,1,2,0,3,3,3,2,2,0,1,1,0,1,3,0,0,0,2,2,0,0,0,0,3,1,0,1,0,0,0, -0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,0,2,3,1,2,0,0,2,1,0,3,1,0,1,2,0,1,1,1,1,3,0,0,3,1,1,0,2,2,1,1, -0,2,0,0,0,0,0,1,0,1,0,0,1,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,0,0,3,1,2,0,0,2,2,0,1,2,0,1,0,1,3,1,2,1,0,0,0,2,0,3,0,0,0,1,0, -0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,0,1,1,2,2,0,0,0,2,0,2,1,0,1,1,0,1,1,1,2,1,0,0,1,1,1,0,2,1,1,1, -0,1,1,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,1,0,1, -0,0,0,2,0,1,3,1,1,1,1,0,0,0,0,3,2,0,1,0,0,0,1,2,0,0,0,1,0,0,0,0, -0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,3,3,3,3,1,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,0,2,3,2,2,0,0,0,1,0,0,0,0,2,3,2,1,2,2,3,0,0,0,2,3,1,0,0,0,1,1, -0,0,1,0,0,0,0,0,0,0,1,0,0,1,0,0,0,0,0,1,1,0,1,0,0,0,0,0,0,0,0,0, -3,3,2,2,0,1,0,0,0,0,2,0,2,0,1,0,0,0,1,1,0,0,0,2,1,0,1,0,1,1,0,0, -0,1,0,2,0,0,1,0,3,0,1,0,0,0,2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,1,0,0,1,0,0,0,0,0,1,1,2,0,0,0,0,1,0,0,1,3,1,0,0,0,0,1,1,0,0, -0,1,0,0,0,0,3,0,0,0,0,0,0,3,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0, -3,3,1,1,1,1,2,3,0,0,2,1,1,1,1,1,0,2,1,1,0,0,0,2,1,0,1,2,1,1,0,1, -2,1,0,3,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,3,1,0,0,0,0,0,0,0,3,0,0,0,3,0,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,1, -0,0,0,2,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,2,0,0,0,0,0,0,1,2,1,0,1,1,0,2,0,0,1,0,0,2,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,2,0,0,0,1,3,0,1,0,0,0,2,0,0,0,0,0,0,0,1,2,0,0,0,0,0, -3,3,0,0,1,1,2,0,0,1,2,1,0,1,1,1,0,1,1,0,0,2,1,1,0,1,0,0,1,1,1,0, -0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,3,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,2,2,1,0,0,0,0,1,0,0,0,0,3,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0, -2,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,3,0,0,1,1,0,0,0,2,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -1,1,0,1,2,0,1,2,0,0,1,1,0,2,0,1,0,0,1,0,0,0,0,1,0,0,0,2,0,0,0,0, -1,0,0,1,0,1,1,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,1,0,0,0,0,0,0,0,1,1,0,1,1,0,2,1,3,0,0,0,0,1,1,0,0,0,0,0,0,0,3, -1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,0,1,0,1,0,0,2,0,0,2,0,0,1,1,2,0,0,1,1,0,0,0,1,0,0,0,1,1,0,0,0, -1,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0, -1,0,0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,1,1,0,0,0, -2,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,3,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,0,0,0,0,2,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,1,3,0,0,0, -2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,1,0,0,0,0, -1,0,0,0,0,0,0,0,0,1,0,0,0,0,2,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,1,1,0,0,2,1,0,0,1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -) +TIS_620_THAI_MODEL = SingleByteCharSetModel(charset_name='TIS-620', + language='Thai', + char_to_order_map=TIS_620_THAI_CHAR_TO_ORDER, + language_model=THAI_LANG_MODEL, + typical_positive_ratio=0.926386, + keep_ascii_letters=False, + alphabet='กขฃคฅฆงจฉชซฌญฎฏฐฑฒณดตถทธนบปผฝพฟภมยรฤลฦวศษสหฬอฮฯะัาำิีึืฺุู฿เแโใไๅๆ็่้๊๋์ํ๎๏๐๑๒๓๔๕๖๗๘๙๚๛') -TIS620ThaiModel = { - 'char_to_order_map': TIS620CharToOrderMap, - 'precedence_matrix': ThaiLangModel, - 'typical_positive_ratio': 0.926386, - 'keep_english_letter': False, - 'charset_name': "TIS-620", - 'language': 'Thai', -} diff --git a/src/pip/_vendor/chardet/langturkishmodel.py b/src/pip/_vendor/chardet/langturkishmodel.py index a427a457398..43f4230aead 100644 --- a/src/pip/_vendor/chardet/langturkishmodel.py +++ b/src/pip/_vendor/chardet/langturkishmodel.py @@ -1,193 +1,4383 @@ +#!/usr/bin/env python # -*- coding: utf-8 -*- -######################## BEGIN LICENSE BLOCK ######################## -# The Original Code is Mozilla Communicator client code. -# -# The Initial Developer of the Original Code is -# Netscape Communications Corporation. -# Portions created by the Initial Developer are Copyright (C) 1998 -# the Initial Developer. All Rights Reserved. -# -# Contributor(s): -# Mark Pilgrim - port to Python -# Özgür Baskın - Turkish Language Model -# -# This library is free software; you can redistribute it and/or -# modify it under the terms of the GNU Lesser General Public -# License as published by the Free Software Foundation; either -# version 2.1 of the License, or (at your option) any later version. -# -# This library is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU -# Lesser General Public License for more details. -# -# You should have received a copy of the GNU Lesser General Public -# License along with this library; if not, write to the Free Software -# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA -# 02110-1301 USA -######################### END LICENSE BLOCK ######################### -# 255: Control characters that usually does not exist in any text +from pip._vendor.chardet.sbcharsetprober import SingleByteCharSetModel + + +# 3: Positive +# 2: Likely +# 1: Unlikely +# 0: Negative + +TURKISH_LANG_MODEL = { + 23: { # 'A' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 0, # 'c' + 12: 2, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 1, # 'g' + 25: 1, # 'h' + 3: 1, # 'i' + 24: 0, # 'j' + 10: 2, # 'k' + 5: 1, # 'l' + 13: 1, # 'm' + 4: 1, # 'n' + 15: 0, # 'o' + 26: 0, # 'p' + 7: 1, # 'r' + 8: 1, # 's' + 9: 1, # 't' + 14: 1, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 3, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 1, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 0, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 37: { # 'B' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 2, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 2, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 1, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 1, # 'P' + 44: 0, # 'R' + 35: 1, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 1, # 'Y' + 56: 0, # 'Z' + 1: 2, # 'a' + 21: 0, # 'b' + 28: 2, # 'c' + 12: 0, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 0, # 'j' + 10: 0, # 'k' + 5: 0, # 'l' + 13: 1, # 'm' + 4: 1, # 'n' + 15: 0, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 2, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 1, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 1, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 0, # 'ı' + 40: 1, # 'Ş' + 19: 1, # 'ş' + }, + 47: { # 'C' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 1, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 1, # 'L' + 20: 0, # 'M' + 46: 1, # 'N' + 42: 0, # 'O' + 48: 1, # 'P' + 44: 1, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 1, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 2, # 'c' + 12: 0, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 2, # 'j' + 10: 1, # 'k' + 5: 2, # 'l' + 13: 2, # 'm' + 4: 2, # 'n' + 15: 1, # 'o' + 26: 0, # 'p' + 7: 2, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 3, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 2, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 1, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 1, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ğ' + 41: 1, # 'İ' + 6: 3, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 39: { # 'D' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 1, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 1, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 1, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 2, # 'a' + 21: 0, # 'b' + 28: 2, # 'c' + 12: 0, # 'd' + 2: 2, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 0, # 'j' + 10: 0, # 'k' + 5: 1, # 'l' + 13: 3, # 'm' + 4: 0, # 'n' + 15: 1, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 1, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 1, # 'z' + 63: 0, # '·' + 54: 1, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 1, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 0, # 'ü' + 30: 1, # 'ğ' + 41: 0, # 'İ' + 6: 1, # 'ı' + 40: 1, # 'Ş' + 19: 0, # 'ş' + }, + 29: { # 'E' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 1, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 1, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 0, # 'c' + 12: 2, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 1, # 'g' + 25: 0, # 'h' + 3: 1, # 'i' + 24: 1, # 'j' + 10: 0, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 1, # 's' + 9: 1, # 't' + 14: 1, # 'u' + 32: 1, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 2, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 3, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 52: { # 'F' + 23: 0, # 'A' + 37: 1, # 'B' + 47: 1, # 'C' + 39: 1, # 'D' + 29: 1, # 'E' + 52: 2, # 'F' + 36: 0, # 'G' + 45: 2, # 'H' + 53: 1, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 1, # 'M' + 46: 1, # 'N' + 42: 1, # 'O' + 48: 2, # 'P' + 44: 1, # 'R' + 35: 1, # 'S' + 31: 1, # 'T' + 51: 1, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 2, # 'Y' + 56: 0, # 'Z' + 1: 0, # 'a' + 21: 1, # 'b' + 28: 1, # 'c' + 12: 1, # 'd' + 2: 0, # 'e' + 18: 1, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 2, # 'i' + 24: 1, # 'j' + 10: 0, # 'k' + 5: 0, # 'l' + 13: 1, # 'm' + 4: 2, # 'n' + 15: 1, # 'o' + 26: 0, # 'p' + 7: 2, # 'r' + 8: 1, # 's' + 9: 1, # 't' + 14: 1, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 1, # 'y' + 22: 1, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 1, # 'Ö' + 55: 2, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 2, # 'ö' + 17: 0, # 'ü' + 30: 1, # 'ğ' + 41: 1, # 'İ' + 6: 2, # 'ı' + 40: 0, # 'Ş' + 19: 2, # 'ş' + }, + 36: { # 'G' + 23: 1, # 'A' + 37: 0, # 'B' + 47: 1, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 1, # 'F' + 36: 2, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 2, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 2, # 'N' + 42: 1, # 'O' + 48: 1, # 'P' + 44: 1, # 'R' + 35: 1, # 'S' + 31: 0, # 'T' + 51: 1, # 'U' + 38: 2, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 1, # 'c' + 12: 0, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 1, # 'j' + 10: 1, # 'k' + 5: 0, # 'l' + 13: 3, # 'm' + 4: 2, # 'n' + 15: 0, # 'o' + 26: 1, # 'p' + 7: 0, # 'r' + 8: 1, # 's' + 9: 1, # 't' + 14: 3, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 1, # 'x' + 11: 0, # 'y' + 22: 2, # 'z' + 63: 0, # '·' + 54: 1, # 'Ç' + 50: 2, # 'Ö' + 55: 0, # 'Ü' + 59: 1, # 'â' + 33: 2, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 0, # 'ü' + 30: 1, # 'ğ' + 41: 1, # 'İ' + 6: 2, # 'ı' + 40: 2, # 'Ş' + 19: 1, # 'ş' + }, + 45: { # 'H' + 23: 0, # 'A' + 37: 1, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 2, # 'F' + 36: 2, # 'G' + 45: 1, # 'H' + 53: 1, # 'I' + 60: 0, # 'J' + 16: 2, # 'K' + 49: 1, # 'L' + 20: 0, # 'M' + 46: 1, # 'N' + 42: 1, # 'O' + 48: 1, # 'P' + 44: 0, # 'R' + 35: 2, # 'S' + 31: 0, # 'T' + 51: 1, # 'U' + 38: 2, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 2, # 'c' + 12: 0, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 2, # 'i' + 24: 0, # 'j' + 10: 1, # 'k' + 5: 0, # 'l' + 13: 2, # 'm' + 4: 0, # 'n' + 15: 1, # 'o' + 26: 1, # 'p' + 7: 1, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 3, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 2, # 'z' + 63: 0, # '·' + 54: 1, # 'Ç' + 50: 1, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 1, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 0, # 'ü' + 30: 2, # 'ğ' + 41: 1, # 'İ' + 6: 0, # 'ı' + 40: 2, # 'Ş' + 19: 1, # 'ş' + }, + 53: { # 'I' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 1, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 2, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 1, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 2, # 'a' + 21: 0, # 'b' + 28: 2, # 'c' + 12: 0, # 'd' + 2: 2, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 0, # 'j' + 10: 0, # 'k' + 5: 2, # 'l' + 13: 2, # 'm' + 4: 0, # 'n' + 15: 0, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 2, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 2, # 'z' + 63: 0, # '·' + 54: 1, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 2, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 0, # 'ı' + 40: 1, # 'Ş' + 19: 1, # 'ş' + }, + 60: { # 'J' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 1, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 0, # 'a' + 21: 1, # 'b' + 28: 0, # 'c' + 12: 1, # 'd' + 2: 0, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 1, # 'i' + 24: 0, # 'j' + 10: 0, # 'k' + 5: 0, # 'l' + 13: 0, # 'm' + 4: 1, # 'n' + 15: 0, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 1, # 's' + 9: 0, # 't' + 14: 0, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 0, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 16: { # 'K' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 3, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 2, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 2, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 2, # 'a' + 21: 3, # 'b' + 28: 0, # 'c' + 12: 3, # 'd' + 2: 1, # 'e' + 18: 3, # 'f' + 27: 3, # 'g' + 25: 3, # 'h' + 3: 3, # 'i' + 24: 2, # 'j' + 10: 3, # 'k' + 5: 0, # 'l' + 13: 0, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 1, # 'p' + 7: 3, # 'r' + 8: 3, # 's' + 9: 3, # 't' + 14: 0, # 'u' + 32: 3, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 2, # 'y' + 22: 1, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 2, # 'ü' + 30: 0, # 'ğ' + 41: 1, # 'İ' + 6: 3, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 49: { # 'L' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 2, # 'E' + 52: 0, # 'F' + 36: 1, # 'G' + 45: 1, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 1, # 'M' + 46: 0, # 'N' + 42: 2, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 1, # 'Y' + 56: 0, # 'Z' + 1: 0, # 'a' + 21: 3, # 'b' + 28: 0, # 'c' + 12: 2, # 'd' + 2: 0, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 2, # 'i' + 24: 0, # 'j' + 10: 1, # 'k' + 5: 0, # 'l' + 13: 0, # 'm' + 4: 2, # 'n' + 15: 1, # 'o' + 26: 1, # 'p' + 7: 1, # 'r' + 8: 1, # 's' + 9: 1, # 't' + 14: 0, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 2, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 2, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 1, # 'ü' + 30: 1, # 'ğ' + 41: 0, # 'İ' + 6: 2, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 20: { # 'M' + 23: 1, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 1, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 2, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 1, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 2, # 'b' + 28: 0, # 'c' + 12: 3, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 1, # 'g' + 25: 1, # 'h' + 3: 2, # 'i' + 24: 2, # 'j' + 10: 2, # 'k' + 5: 2, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 1, # 'p' + 7: 3, # 'r' + 8: 0, # 's' + 9: 2, # 't' + 14: 3, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 2, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 3, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 3, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 46: { # 'N' + 23: 0, # 'A' + 37: 1, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 1, # 'F' + 36: 1, # 'G' + 45: 1, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 2, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 1, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 1, # 'R' + 35: 1, # 'S' + 31: 0, # 'T' + 51: 1, # 'U' + 38: 2, # 'V' + 62: 0, # 'W' + 43: 1, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 2, # 'c' + 12: 0, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 1, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 2, # 'j' + 10: 1, # 'k' + 5: 1, # 'l' + 13: 3, # 'm' + 4: 2, # 'n' + 15: 1, # 'o' + 26: 1, # 'p' + 7: 1, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 3, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 1, # 'x' + 11: 1, # 'y' + 22: 2, # 'z' + 63: 0, # '·' + 54: 1, # 'Ç' + 50: 1, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ğ' + 41: 1, # 'İ' + 6: 2, # 'ı' + 40: 1, # 'Ş' + 19: 1, # 'ş' + }, + 42: { # 'O' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 1, # 'F' + 36: 0, # 'G' + 45: 1, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 2, # 'K' + 49: 1, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 2, # 'P' + 44: 1, # 'R' + 35: 1, # 'S' + 31: 0, # 'T' + 51: 1, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 2, # 'c' + 12: 0, # 'd' + 2: 2, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 0, # 'j' + 10: 0, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 0, # 'n' + 15: 1, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 2, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 2, # 'z' + 63: 0, # '·' + 54: 2, # 'Ç' + 50: 1, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 2, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 0, # 'ü' + 30: 1, # 'ğ' + 41: 2, # 'İ' + 6: 1, # 'ı' + 40: 1, # 'Ş' + 19: 1, # 'ş' + }, + 48: { # 'P' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 2, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 2, # 'F' + 36: 1, # 'G' + 45: 1, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 2, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 1, # 'N' + 42: 1, # 'O' + 48: 1, # 'P' + 44: 0, # 'R' + 35: 1, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 2, # 'a' + 21: 0, # 'b' + 28: 2, # 'c' + 12: 0, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 0, # 'j' + 10: 1, # 'k' + 5: 0, # 'l' + 13: 2, # 'm' + 4: 0, # 'n' + 15: 2, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 2, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 2, # 'x' + 11: 0, # 'y' + 22: 2, # 'z' + 63: 0, # '·' + 54: 1, # 'Ç' + 50: 2, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 2, # 'ö' + 17: 0, # 'ü' + 30: 1, # 'ğ' + 41: 1, # 'İ' + 6: 0, # 'ı' + 40: 2, # 'Ş' + 19: 1, # 'ş' + }, + 44: { # 'R' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 1, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 1, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 1, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 1, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 1, # 'b' + 28: 1, # 'c' + 12: 0, # 'd' + 2: 2, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 0, # 'j' + 10: 1, # 'k' + 5: 2, # 'l' + 13: 2, # 'm' + 4: 0, # 'n' + 15: 1, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 2, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 1, # 'y' + 22: 2, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 1, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 1, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 1, # 'ü' + 30: 1, # 'ğ' + 41: 0, # 'İ' + 6: 2, # 'ı' + 40: 1, # 'Ş' + 19: 1, # 'ş' + }, + 35: { # 'S' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 1, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 1, # 'F' + 36: 1, # 'G' + 45: 1, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 3, # 'K' + 49: 1, # 'L' + 20: 1, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 1, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 1, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 1, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 2, # 'c' + 12: 0, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 0, # 'j' + 10: 1, # 'k' + 5: 1, # 'l' + 13: 2, # 'm' + 4: 1, # 'n' + 15: 0, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 0, # 's' + 9: 1, # 't' + 14: 2, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 1, # 'z' + 63: 0, # '·' + 54: 2, # 'Ç' + 50: 2, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 3, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 3, # 'ı' + 40: 2, # 'Ş' + 19: 1, # 'ş' + }, + 31: { # 'T' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 1, # 'J' + 16: 2, # 'K' + 49: 0, # 'L' + 20: 1, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 2, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 2, # 'b' + 28: 0, # 'c' + 12: 1, # 'd' + 2: 3, # 'e' + 18: 2, # 'f' + 27: 2, # 'g' + 25: 0, # 'h' + 3: 1, # 'i' + 24: 1, # 'j' + 10: 2, # 'k' + 5: 2, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 2, # 'p' + 7: 2, # 'r' + 8: 0, # 's' + 9: 2, # 't' + 14: 2, # 'u' + 32: 1, # 'v' + 57: 1, # 'w' + 58: 1, # 'x' + 11: 2, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 1, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 3, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 51: { # 'U' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 1, # 'F' + 36: 1, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 1, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 1, # 'N' + 42: 0, # 'O' + 48: 1, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 1, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 1, # 'c' + 12: 0, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 2, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 0, # 'j' + 10: 1, # 'k' + 5: 1, # 'l' + 13: 3, # 'm' + 4: 2, # 'n' + 15: 0, # 'o' + 26: 1, # 'p' + 7: 0, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 2, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 2, # 'z' + 63: 0, # '·' + 54: 1, # 'Ç' + 50: 1, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 0, # 'ü' + 30: 1, # 'ğ' + 41: 1, # 'İ' + 6: 2, # 'ı' + 40: 0, # 'Ş' + 19: 1, # 'ş' + }, + 38: { # 'V' + 23: 1, # 'A' + 37: 1, # 'B' + 47: 1, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 2, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 3, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 1, # 'P' + 44: 1, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 1, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 2, # 'c' + 12: 0, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 0, # 'j' + 10: 0, # 'k' + 5: 2, # 'l' + 13: 2, # 'm' + 4: 0, # 'n' + 15: 2, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 0, # 's' + 9: 1, # 't' + 14: 3, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 1, # 'y' + 22: 2, # 'z' + 63: 0, # '·' + 54: 1, # 'Ç' + 50: 1, # 'Ö' + 55: 0, # 'Ü' + 59: 1, # 'â' + 33: 2, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 0, # 'ü' + 30: 1, # 'ğ' + 41: 1, # 'İ' + 6: 3, # 'ı' + 40: 2, # 'Ş' + 19: 1, # 'ş' + }, + 62: { # 'W' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 0, # 'a' + 21: 0, # 'b' + 28: 0, # 'c' + 12: 0, # 'd' + 2: 0, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 0, # 'j' + 10: 0, # 'k' + 5: 0, # 'l' + 13: 0, # 'm' + 4: 0, # 'n' + 15: 0, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 0, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 0, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 43: { # 'Y' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 1, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 2, # 'F' + 36: 0, # 'G' + 45: 1, # 'H' + 53: 1, # 'I' + 60: 0, # 'J' + 16: 2, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 2, # 'N' + 42: 0, # 'O' + 48: 2, # 'P' + 44: 1, # 'R' + 35: 1, # 'S' + 31: 0, # 'T' + 51: 1, # 'U' + 38: 2, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 2, # 'c' + 12: 0, # 'd' + 2: 2, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 1, # 'j' + 10: 1, # 'k' + 5: 1, # 'l' + 13: 3, # 'm' + 4: 0, # 'n' + 15: 2, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 3, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 1, # 'x' + 11: 0, # 'y' + 22: 2, # 'z' + 63: 0, # '·' + 54: 1, # 'Ç' + 50: 2, # 'Ö' + 55: 1, # 'Ü' + 59: 1, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 0, # 'ü' + 30: 1, # 'ğ' + 41: 1, # 'İ' + 6: 0, # 'ı' + 40: 2, # 'Ş' + 19: 1, # 'ş' + }, + 56: { # 'Z' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 2, # 'Z' + 1: 2, # 'a' + 21: 1, # 'b' + 28: 0, # 'c' + 12: 0, # 'd' + 2: 2, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 2, # 'i' + 24: 1, # 'j' + 10: 0, # 'k' + 5: 0, # 'l' + 13: 1, # 'm' + 4: 1, # 'n' + 15: 0, # 'o' + 26: 0, # 'p' + 7: 1, # 'r' + 8: 1, # 's' + 9: 0, # 't' + 14: 2, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 1, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 1, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 1: { # 'a' + 23: 3, # 'A' + 37: 0, # 'B' + 47: 1, # 'C' + 39: 0, # 'D' + 29: 3, # 'E' + 52: 0, # 'F' + 36: 1, # 'G' + 45: 1, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 3, # 'M' + 46: 1, # 'N' + 42: 0, # 'O' + 48: 1, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 3, # 'T' + 51: 0, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 2, # 'Z' + 1: 2, # 'a' + 21: 3, # 'b' + 28: 0, # 'c' + 12: 3, # 'd' + 2: 2, # 'e' + 18: 3, # 'f' + 27: 3, # 'g' + 25: 3, # 'h' + 3: 3, # 'i' + 24: 3, # 'j' + 10: 3, # 'k' + 5: 0, # 'l' + 13: 2, # 'm' + 4: 3, # 'n' + 15: 1, # 'o' + 26: 3, # 'p' + 7: 3, # 'r' + 8: 3, # 's' + 9: 3, # 't' + 14: 3, # 'u' + 32: 3, # 'v' + 57: 2, # 'w' + 58: 0, # 'x' + 11: 3, # 'y' + 22: 0, # 'z' + 63: 1, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 1, # 'ç' + 61: 1, # 'î' + 34: 1, # 'ö' + 17: 3, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 3, # 'ı' + 40: 0, # 'Ş' + 19: 1, # 'ş' + }, + 21: { # 'b' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 1, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 1, # 'J' + 16: 2, # 'K' + 49: 0, # 'L' + 20: 2, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 1, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 1, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 2, # 'b' + 28: 0, # 'c' + 12: 3, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 3, # 'g' + 25: 1, # 'h' + 3: 3, # 'i' + 24: 2, # 'j' + 10: 3, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 3, # 'p' + 7: 1, # 'r' + 8: 2, # 's' + 9: 2, # 't' + 14: 2, # 'u' + 32: 1, # 'v' + 57: 0, # 'w' + 58: 1, # 'x' + 11: 3, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 1, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 0, # 'ü' + 30: 1, # 'ğ' + 41: 0, # 'İ' + 6: 2, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 28: { # 'c' + 23: 0, # 'A' + 37: 1, # 'B' + 47: 1, # 'C' + 39: 1, # 'D' + 29: 2, # 'E' + 52: 0, # 'F' + 36: 2, # 'G' + 45: 2, # 'H' + 53: 1, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 2, # 'M' + 46: 1, # 'N' + 42: 1, # 'O' + 48: 2, # 'P' + 44: 1, # 'R' + 35: 1, # 'S' + 31: 2, # 'T' + 51: 2, # 'U' + 38: 2, # 'V' + 62: 0, # 'W' + 43: 3, # 'Y' + 56: 0, # 'Z' + 1: 1, # 'a' + 21: 1, # 'b' + 28: 2, # 'c' + 12: 2, # 'd' + 2: 1, # 'e' + 18: 1, # 'f' + 27: 2, # 'g' + 25: 2, # 'h' + 3: 3, # 'i' + 24: 1, # 'j' + 10: 3, # 'k' + 5: 0, # 'l' + 13: 2, # 'm' + 4: 3, # 'n' + 15: 2, # 'o' + 26: 2, # 'p' + 7: 3, # 'r' + 8: 3, # 's' + 9: 3, # 't' + 14: 1, # 'u' + 32: 0, # 'v' + 57: 1, # 'w' + 58: 0, # 'x' + 11: 2, # 'y' + 22: 1, # 'z' + 63: 1, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 1, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 1, # 'î' + 34: 2, # 'ö' + 17: 2, # 'ü' + 30: 2, # 'ğ' + 41: 1, # 'İ' + 6: 3, # 'ı' + 40: 0, # 'Ş' + 19: 2, # 'ş' + }, + 12: { # 'd' + 23: 1, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 2, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 3, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 1, # 'S' + 31: 1, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 2, # 'b' + 28: 1, # 'c' + 12: 3, # 'd' + 2: 3, # 'e' + 18: 1, # 'f' + 27: 3, # 'g' + 25: 3, # 'h' + 3: 2, # 'i' + 24: 3, # 'j' + 10: 2, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 1, # 'o' + 26: 2, # 'p' + 7: 3, # 'r' + 8: 2, # 's' + 9: 2, # 't' + 14: 3, # 'u' + 32: 1, # 'v' + 57: 0, # 'w' + 58: 1, # 'x' + 11: 3, # 'y' + 22: 1, # 'z' + 63: 1, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 1, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 2, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 2: { # 'e' + 23: 2, # 'A' + 37: 0, # 'B' + 47: 2, # 'C' + 39: 0, # 'D' + 29: 3, # 'E' + 52: 1, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 1, # 'K' + 49: 0, # 'L' + 20: 3, # 'M' + 46: 1, # 'N' + 42: 0, # 'O' + 48: 1, # 'P' + 44: 1, # 'R' + 35: 0, # 'S' + 31: 3, # 'T' + 51: 0, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 1, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 3, # 'b' + 28: 0, # 'c' + 12: 3, # 'd' + 2: 2, # 'e' + 18: 3, # 'f' + 27: 3, # 'g' + 25: 3, # 'h' + 3: 3, # 'i' + 24: 3, # 'j' + 10: 3, # 'k' + 5: 0, # 'l' + 13: 2, # 'm' + 4: 3, # 'n' + 15: 1, # 'o' + 26: 3, # 'p' + 7: 3, # 'r' + 8: 3, # 's' + 9: 3, # 't' + 14: 3, # 'u' + 32: 3, # 'v' + 57: 2, # 'w' + 58: 0, # 'x' + 11: 3, # 'y' + 22: 1, # 'z' + 63: 1, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 1, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 3, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 3, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 18: { # 'f' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 2, # 'K' + 49: 0, # 'L' + 20: 2, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 2, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 1, # 'b' + 28: 0, # 'c' + 12: 3, # 'd' + 2: 3, # 'e' + 18: 2, # 'f' + 27: 1, # 'g' + 25: 1, # 'h' + 3: 1, # 'i' + 24: 1, # 'j' + 10: 1, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 2, # 'p' + 7: 1, # 'r' + 8: 3, # 's' + 9: 3, # 't' + 14: 1, # 'u' + 32: 2, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 1, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 1, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 1, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 1, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 27: { # 'g' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 1, # 'S' + 31: 1, # 'T' + 51: 0, # 'U' + 38: 2, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 1, # 'b' + 28: 0, # 'c' + 12: 1, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 2, # 'g' + 25: 1, # 'h' + 3: 2, # 'i' + 24: 3, # 'j' + 10: 2, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 2, # 'n' + 15: 0, # 'o' + 26: 1, # 'p' + 7: 2, # 'r' + 8: 2, # 's' + 9: 3, # 't' + 14: 3, # 'u' + 32: 1, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 1, # 'y' + 22: 0, # 'z' + 63: 1, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 2, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 25: { # 'h' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 2, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 0, # 'c' + 12: 2, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 1, # 'g' + 25: 2, # 'h' + 3: 2, # 'i' + 24: 3, # 'j' + 10: 3, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 1, # 'o' + 26: 1, # 'p' + 7: 3, # 'r' + 8: 3, # 's' + 9: 2, # 't' + 14: 3, # 'u' + 32: 2, # 'v' + 57: 1, # 'w' + 58: 0, # 'x' + 11: 1, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 3, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 3: { # 'i' + 23: 2, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 1, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 3, # 'M' + 46: 0, # 'N' + 42: 1, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 1, # 'S' + 31: 2, # 'T' + 51: 0, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 2, # 'b' + 28: 0, # 'c' + 12: 3, # 'd' + 2: 3, # 'e' + 18: 2, # 'f' + 27: 3, # 'g' + 25: 1, # 'h' + 3: 3, # 'i' + 24: 2, # 'j' + 10: 3, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 1, # 'o' + 26: 3, # 'p' + 7: 3, # 'r' + 8: 3, # 's' + 9: 3, # 't' + 14: 3, # 'u' + 32: 2, # 'v' + 57: 1, # 'w' + 58: 1, # 'x' + 11: 3, # 'y' + 22: 1, # 'z' + 63: 1, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 1, # 'Ü' + 59: 0, # 'â' + 33: 2, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 3, # 'ü' + 30: 0, # 'ğ' + 41: 1, # 'İ' + 6: 2, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 24: { # 'j' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 1, # 'J' + 16: 2, # 'K' + 49: 0, # 'L' + 20: 2, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 1, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 1, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 1, # 'Z' + 1: 3, # 'a' + 21: 1, # 'b' + 28: 1, # 'c' + 12: 3, # 'd' + 2: 3, # 'e' + 18: 2, # 'f' + 27: 1, # 'g' + 25: 1, # 'h' + 3: 2, # 'i' + 24: 1, # 'j' + 10: 2, # 'k' + 5: 2, # 'l' + 13: 3, # 'm' + 4: 2, # 'n' + 15: 0, # 'o' + 26: 1, # 'p' + 7: 2, # 'r' + 8: 3, # 's' + 9: 2, # 't' + 14: 3, # 'u' + 32: 2, # 'v' + 57: 0, # 'w' + 58: 2, # 'x' + 11: 1, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 1, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 1, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 3, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 10: { # 'k' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 2, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 3, # 'T' + 51: 0, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 1, # 'Z' + 1: 3, # 'a' + 21: 2, # 'b' + 28: 0, # 'c' + 12: 2, # 'd' + 2: 3, # 'e' + 18: 1, # 'f' + 27: 2, # 'g' + 25: 2, # 'h' + 3: 3, # 'i' + 24: 2, # 'j' + 10: 2, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 3, # 'p' + 7: 2, # 'r' + 8: 2, # 's' + 9: 2, # 't' + 14: 3, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 1, # 'x' + 11: 3, # 'y' + 22: 0, # 'z' + 63: 1, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 3, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 3, # 'ü' + 30: 1, # 'ğ' + 41: 0, # 'İ' + 6: 3, # 'ı' + 40: 0, # 'Ş' + 19: 1, # 'ş' + }, + 5: { # 'l' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 3, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 2, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 1, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 0, # 'a' + 21: 3, # 'b' + 28: 0, # 'c' + 12: 3, # 'd' + 2: 1, # 'e' + 18: 3, # 'f' + 27: 3, # 'g' + 25: 2, # 'h' + 3: 3, # 'i' + 24: 2, # 'j' + 10: 3, # 'k' + 5: 1, # 'l' + 13: 1, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 2, # 'p' + 7: 3, # 'r' + 8: 3, # 's' + 9: 3, # 't' + 14: 2, # 'u' + 32: 2, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 3, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 1, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 2, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 3, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 13: { # 'm' + 23: 1, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 3, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 3, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 3, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 1, # 'Y' + 56: 0, # 'Z' + 1: 2, # 'a' + 21: 3, # 'b' + 28: 0, # 'c' + 12: 3, # 'd' + 2: 2, # 'e' + 18: 3, # 'f' + 27: 3, # 'g' + 25: 3, # 'h' + 3: 3, # 'i' + 24: 3, # 'j' + 10: 3, # 'k' + 5: 0, # 'l' + 13: 2, # 'm' + 4: 3, # 'n' + 15: 1, # 'o' + 26: 2, # 'p' + 7: 3, # 'r' + 8: 3, # 's' + 9: 3, # 't' + 14: 2, # 'u' + 32: 2, # 'v' + 57: 1, # 'w' + 58: 0, # 'x' + 11: 3, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 3, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 3, # 'ı' + 40: 0, # 'Ş' + 19: 1, # 'ş' + }, + 4: { # 'n' + 23: 1, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 1, # 'H' + 53: 0, # 'I' + 60: 2, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 3, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 2, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 2, # 'b' + 28: 1, # 'c' + 12: 3, # 'd' + 2: 3, # 'e' + 18: 1, # 'f' + 27: 2, # 'g' + 25: 3, # 'h' + 3: 2, # 'i' + 24: 2, # 'j' + 10: 3, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 1, # 'o' + 26: 3, # 'p' + 7: 2, # 'r' + 8: 3, # 's' + 9: 3, # 't' + 14: 3, # 'u' + 32: 2, # 'v' + 57: 0, # 'w' + 58: 2, # 'x' + 11: 3, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 1, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 2, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 1, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 15: { # 'o' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 1, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 2, # 'F' + 36: 1, # 'G' + 45: 1, # 'H' + 53: 1, # 'I' + 60: 0, # 'J' + 16: 3, # 'K' + 49: 2, # 'L' + 20: 0, # 'M' + 46: 2, # 'N' + 42: 1, # 'O' + 48: 2, # 'P' + 44: 1, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 2, # 'c' + 12: 0, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 1, # 'i' + 24: 2, # 'j' + 10: 1, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 2, # 'n' + 15: 2, # 'o' + 26: 0, # 'p' + 7: 1, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 3, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 2, # 'x' + 11: 0, # 'y' + 22: 2, # 'z' + 63: 0, # '·' + 54: 1, # 'Ç' + 50: 2, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 3, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 0, # 'ü' + 30: 2, # 'ğ' + 41: 2, # 'İ' + 6: 3, # 'ı' + 40: 2, # 'Ş' + 19: 2, # 'ş' + }, + 26: { # 'p' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 1, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 1, # 'b' + 28: 0, # 'c' + 12: 1, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 1, # 'g' + 25: 1, # 'h' + 3: 2, # 'i' + 24: 3, # 'j' + 10: 1, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 2, # 'n' + 15: 0, # 'o' + 26: 2, # 'p' + 7: 2, # 'r' + 8: 1, # 's' + 9: 1, # 't' + 14: 3, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 1, # 'x' + 11: 1, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 3, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 1, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 3, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 7: { # 'r' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 1, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 2, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 2, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 2, # 'T' + 51: 1, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 1, # 'Z' + 1: 3, # 'a' + 21: 1, # 'b' + 28: 0, # 'c' + 12: 3, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 2, # 'g' + 25: 3, # 'h' + 3: 2, # 'i' + 24: 2, # 'j' + 10: 3, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 2, # 'p' + 7: 3, # 'r' + 8: 3, # 's' + 9: 3, # 't' + 14: 3, # 'u' + 32: 2, # 'v' + 57: 0, # 'w' + 58: 1, # 'x' + 11: 2, # 'y' + 22: 0, # 'z' + 63: 1, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 2, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 3, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 2, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 8: { # 's' + 23: 1, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 1, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 3, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 2, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 1, # 'Z' + 1: 3, # 'a' + 21: 2, # 'b' + 28: 1, # 'c' + 12: 3, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 2, # 'g' + 25: 2, # 'h' + 3: 2, # 'i' + 24: 3, # 'j' + 10: 3, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 3, # 'p' + 7: 3, # 'r' + 8: 3, # 's' + 9: 3, # 't' + 14: 3, # 'u' + 32: 2, # 'v' + 57: 0, # 'w' + 58: 1, # 'x' + 11: 2, # 'y' + 22: 1, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 2, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 2, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 3, # 'ı' + 40: 0, # 'Ş' + 19: 1, # 'ş' + }, + 9: { # 't' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 1, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 2, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 2, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 1, # 'Z' + 1: 3, # 'a' + 21: 3, # 'b' + 28: 0, # 'c' + 12: 3, # 'd' + 2: 3, # 'e' + 18: 2, # 'f' + 27: 2, # 'g' + 25: 2, # 'h' + 3: 2, # 'i' + 24: 2, # 'j' + 10: 3, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 2, # 'p' + 7: 3, # 'r' + 8: 3, # 's' + 9: 3, # 't' + 14: 3, # 'u' + 32: 3, # 'v' + 57: 0, # 'w' + 58: 2, # 'x' + 11: 2, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 3, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 2, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 3, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 14: { # 'u' + 23: 3, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 3, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 1, # 'H' + 53: 0, # 'I' + 60: 1, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 3, # 'M' + 46: 2, # 'N' + 42: 0, # 'O' + 48: 1, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 3, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 1, # 'Y' + 56: 2, # 'Z' + 1: 2, # 'a' + 21: 3, # 'b' + 28: 0, # 'c' + 12: 3, # 'd' + 2: 2, # 'e' + 18: 2, # 'f' + 27: 3, # 'g' + 25: 3, # 'h' + 3: 3, # 'i' + 24: 2, # 'j' + 10: 3, # 'k' + 5: 0, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 3, # 'p' + 7: 3, # 'r' + 8: 3, # 's' + 9: 3, # 't' + 14: 3, # 'u' + 32: 2, # 'v' + 57: 2, # 'w' + 58: 0, # 'x' + 11: 3, # 'y' + 22: 0, # 'z' + 63: 1, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 3, # 'ü' + 30: 1, # 'ğ' + 41: 0, # 'İ' + 6: 3, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 32: { # 'v' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 1, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 0, # 'c' + 12: 3, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 1, # 'j' + 10: 1, # 'k' + 5: 3, # 'l' + 13: 2, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 1, # 'p' + 7: 1, # 'r' + 8: 2, # 's' + 9: 3, # 't' + 14: 3, # 'u' + 32: 1, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 2, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 1, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 57: { # 'w' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 1, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 1, # 'a' + 21: 0, # 'b' + 28: 0, # 'c' + 12: 0, # 'd' + 2: 2, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 1, # 'h' + 3: 0, # 'i' + 24: 0, # 'j' + 10: 1, # 'k' + 5: 0, # 'l' + 13: 0, # 'm' + 4: 1, # 'n' + 15: 0, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 1, # 's' + 9: 0, # 't' + 14: 1, # 'u' + 32: 0, # 'v' + 57: 2, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 0, # 'z' + 63: 1, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 1, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 0, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 58: { # 'x' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 1, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 1, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 1, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 0, # 'a' + 21: 1, # 'b' + 28: 0, # 'c' + 12: 2, # 'd' + 2: 1, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 2, # 'i' + 24: 2, # 'j' + 10: 1, # 'k' + 5: 0, # 'l' + 13: 0, # 'm' + 4: 2, # 'n' + 15: 0, # 'o' + 26: 0, # 'p' + 7: 1, # 'r' + 8: 2, # 's' + 9: 1, # 't' + 14: 0, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 2, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 1, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 2, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 11: { # 'y' + 23: 1, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 1, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 1, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 1, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 1, # 'Y' + 56: 1, # 'Z' + 1: 3, # 'a' + 21: 1, # 'b' + 28: 0, # 'c' + 12: 2, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 2, # 'g' + 25: 2, # 'h' + 3: 2, # 'i' + 24: 1, # 'j' + 10: 2, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 1, # 'p' + 7: 2, # 'r' + 8: 1, # 's' + 9: 2, # 't' + 14: 3, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 1, # 'x' + 11: 3, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 3, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 2, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 3, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 22: { # 'z' + 23: 2, # 'A' + 37: 2, # 'B' + 47: 1, # 'C' + 39: 2, # 'D' + 29: 3, # 'E' + 52: 1, # 'F' + 36: 2, # 'G' + 45: 2, # 'H' + 53: 1, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 3, # 'M' + 46: 2, # 'N' + 42: 2, # 'O' + 48: 2, # 'P' + 44: 1, # 'R' + 35: 1, # 'S' + 31: 3, # 'T' + 51: 2, # 'U' + 38: 2, # 'V' + 62: 0, # 'W' + 43: 2, # 'Y' + 56: 1, # 'Z' + 1: 1, # 'a' + 21: 2, # 'b' + 28: 1, # 'c' + 12: 2, # 'd' + 2: 2, # 'e' + 18: 3, # 'f' + 27: 2, # 'g' + 25: 2, # 'h' + 3: 3, # 'i' + 24: 2, # 'j' + 10: 3, # 'k' + 5: 0, # 'l' + 13: 2, # 'm' + 4: 3, # 'n' + 15: 2, # 'o' + 26: 2, # 'p' + 7: 3, # 'r' + 8: 3, # 's' + 9: 3, # 't' + 14: 0, # 'u' + 32: 2, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 3, # 'y' + 22: 2, # 'z' + 63: 1, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 2, # 'Ü' + 59: 1, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 2, # 'ö' + 17: 2, # 'ü' + 30: 2, # 'ğ' + 41: 1, # 'İ' + 6: 3, # 'ı' + 40: 1, # 'Ş' + 19: 2, # 'ş' + }, + 63: { # '·' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 0, # 'a' + 21: 0, # 'b' + 28: 0, # 'c' + 12: 0, # 'd' + 2: 1, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 0, # 'j' + 10: 0, # 'k' + 5: 0, # 'l' + 13: 2, # 'm' + 4: 0, # 'n' + 15: 0, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 2, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 0, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 54: { # 'Ç' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 1, # 'C' + 39: 1, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 1, # 'G' + 45: 1, # 'H' + 53: 1, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 1, # 'O' + 48: 1, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 1, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 2, # 'Y' + 56: 0, # 'Z' + 1: 0, # 'a' + 21: 1, # 'b' + 28: 0, # 'c' + 12: 1, # 'd' + 2: 0, # 'e' + 18: 0, # 'f' + 27: 1, # 'g' + 25: 0, # 'h' + 3: 3, # 'i' + 24: 0, # 'j' + 10: 1, # 'k' + 5: 0, # 'l' + 13: 0, # 'm' + 4: 2, # 'n' + 15: 1, # 'o' + 26: 0, # 'p' + 7: 2, # 'r' + 8: 0, # 's' + 9: 1, # 't' + 14: 0, # 'u' + 32: 2, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 2, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 2, # 'ı' + 40: 0, # 'Ş' + 19: 1, # 'ş' + }, + 50: { # 'Ö' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 1, # 'C' + 39: 1, # 'D' + 29: 2, # 'E' + 52: 0, # 'F' + 36: 1, # 'G' + 45: 2, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 1, # 'M' + 46: 1, # 'N' + 42: 2, # 'O' + 48: 2, # 'P' + 44: 1, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 1, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 2, # 'Y' + 56: 0, # 'Z' + 1: 0, # 'a' + 21: 2, # 'b' + 28: 1, # 'c' + 12: 2, # 'd' + 2: 0, # 'e' + 18: 1, # 'f' + 27: 1, # 'g' + 25: 1, # 'h' + 3: 2, # 'i' + 24: 0, # 'j' + 10: 2, # 'k' + 5: 0, # 'l' + 13: 0, # 'm' + 4: 3, # 'n' + 15: 2, # 'o' + 26: 2, # 'p' + 7: 3, # 'r' + 8: 1, # 's' + 9: 2, # 't' + 14: 0, # 'u' + 32: 1, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 1, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 2, # 'ö' + 17: 2, # 'ü' + 30: 1, # 'ğ' + 41: 0, # 'İ' + 6: 2, # 'ı' + 40: 0, # 'Ş' + 19: 1, # 'ş' + }, + 55: { # 'Ü' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 2, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 1, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 1, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 2, # 'a' + 21: 0, # 'b' + 28: 2, # 'c' + 12: 0, # 'd' + 2: 2, # 'e' + 18: 0, # 'f' + 27: 1, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 0, # 'j' + 10: 0, # 'k' + 5: 1, # 'l' + 13: 1, # 'm' + 4: 1, # 'n' + 15: 0, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 0, # 's' + 9: 1, # 't' + 14: 2, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 1, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 1, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 0, # 'ü' + 30: 1, # 'ğ' + 41: 1, # 'İ' + 6: 0, # 'ı' + 40: 0, # 'Ş' + 19: 1, # 'ş' + }, + 59: { # 'â' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 1, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 1, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 2, # 'a' + 21: 0, # 'b' + 28: 0, # 'c' + 12: 0, # 'd' + 2: 2, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 0, # 'j' + 10: 0, # 'k' + 5: 0, # 'l' + 13: 2, # 'm' + 4: 0, # 'n' + 15: 1, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 2, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 1, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 1, # 'ı' + 40: 1, # 'Ş' + 19: 0, # 'ş' + }, + 33: { # 'ç' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 3, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 1, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 2, # 'T' + 51: 0, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 0, # 'Z' + 1: 0, # 'a' + 21: 3, # 'b' + 28: 0, # 'c' + 12: 2, # 'd' + 2: 0, # 'e' + 18: 2, # 'f' + 27: 1, # 'g' + 25: 3, # 'h' + 3: 3, # 'i' + 24: 0, # 'j' + 10: 3, # 'k' + 5: 0, # 'l' + 13: 0, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 1, # 'p' + 7: 3, # 'r' + 8: 2, # 's' + 9: 3, # 't' + 14: 0, # 'u' + 32: 2, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 2, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 1, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 3, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 61: { # 'î' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 0, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 0, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 1, # 'Z' + 1: 2, # 'a' + 21: 0, # 'b' + 28: 0, # 'c' + 12: 0, # 'd' + 2: 2, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 1, # 'j' + 10: 0, # 'k' + 5: 0, # 'l' + 13: 1, # 'm' + 4: 1, # 'n' + 15: 0, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 1, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 1, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 1, # 'î' + 34: 0, # 'ö' + 17: 0, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 1, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 34: { # 'ö' + 23: 0, # 'A' + 37: 1, # 'B' + 47: 1, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 2, # 'F' + 36: 1, # 'G' + 45: 1, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 3, # 'K' + 49: 1, # 'L' + 20: 0, # 'M' + 46: 1, # 'N' + 42: 1, # 'O' + 48: 2, # 'P' + 44: 1, # 'R' + 35: 1, # 'S' + 31: 1, # 'T' + 51: 1, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 1, # 'Z' + 1: 3, # 'a' + 21: 1, # 'b' + 28: 2, # 'c' + 12: 1, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 2, # 'g' + 25: 2, # 'h' + 3: 1, # 'i' + 24: 2, # 'j' + 10: 1, # 'k' + 5: 2, # 'l' + 13: 3, # 'm' + 4: 2, # 'n' + 15: 2, # 'o' + 26: 0, # 'p' + 7: 0, # 'r' + 8: 3, # 's' + 9: 1, # 't' + 14: 3, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 1, # 'y' + 22: 2, # 'z' + 63: 0, # '·' + 54: 1, # 'Ç' + 50: 2, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 2, # 'ç' + 61: 0, # 'î' + 34: 2, # 'ö' + 17: 0, # 'ü' + 30: 2, # 'ğ' + 41: 1, # 'İ' + 6: 1, # 'ı' + 40: 2, # 'Ş' + 19: 1, # 'ş' + }, + 17: { # 'ü' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 1, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 0, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 1, # 'J' + 16: 1, # 'K' + 49: 0, # 'L' + 20: 1, # 'M' + 46: 0, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 1, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 0, # 'Y' + 56: 1, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 0, # 'c' + 12: 1, # 'd' + 2: 3, # 'e' + 18: 1, # 'f' + 27: 2, # 'g' + 25: 0, # 'h' + 3: 1, # 'i' + 24: 1, # 'j' + 10: 2, # 'k' + 5: 3, # 'l' + 13: 2, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 2, # 'p' + 7: 2, # 'r' + 8: 3, # 's' + 9: 2, # 't' + 14: 3, # 'u' + 32: 1, # 'v' + 57: 1, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 1, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 2, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 2, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 30: { # 'ğ' + 23: 0, # 'A' + 37: 2, # 'B' + 47: 1, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 2, # 'F' + 36: 1, # 'G' + 45: 0, # 'H' + 53: 1, # 'I' + 60: 0, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 1, # 'M' + 46: 2, # 'N' + 42: 2, # 'O' + 48: 1, # 'P' + 44: 1, # 'R' + 35: 0, # 'S' + 31: 1, # 'T' + 51: 0, # 'U' + 38: 2, # 'V' + 62: 0, # 'W' + 43: 2, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 0, # 'b' + 28: 2, # 'c' + 12: 0, # 'd' + 2: 2, # 'e' + 18: 0, # 'f' + 27: 0, # 'g' + 25: 0, # 'h' + 3: 0, # 'i' + 24: 3, # 'j' + 10: 1, # 'k' + 5: 2, # 'l' + 13: 3, # 'm' + 4: 0, # 'n' + 15: 1, # 'o' + 26: 0, # 'p' + 7: 1, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 3, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 2, # 'z' + 63: 0, # '·' + 54: 2, # 'Ç' + 50: 2, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 1, # 'ç' + 61: 0, # 'î' + 34: 2, # 'ö' + 17: 0, # 'ü' + 30: 1, # 'ğ' + 41: 2, # 'İ' + 6: 2, # 'ı' + 40: 2, # 'Ş' + 19: 1, # 'ş' + }, + 41: { # 'İ' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 1, # 'C' + 39: 1, # 'D' + 29: 1, # 'E' + 52: 0, # 'F' + 36: 2, # 'G' + 45: 2, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 2, # 'M' + 46: 1, # 'N' + 42: 1, # 'O' + 48: 2, # 'P' + 44: 0, # 'R' + 35: 1, # 'S' + 31: 1, # 'T' + 51: 1, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 2, # 'Y' + 56: 0, # 'Z' + 1: 1, # 'a' + 21: 2, # 'b' + 28: 1, # 'c' + 12: 2, # 'd' + 2: 1, # 'e' + 18: 0, # 'f' + 27: 3, # 'g' + 25: 2, # 'h' + 3: 2, # 'i' + 24: 2, # 'j' + 10: 2, # 'k' + 5: 0, # 'l' + 13: 1, # 'm' + 4: 3, # 'n' + 15: 1, # 'o' + 26: 1, # 'p' + 7: 3, # 'r' + 8: 3, # 's' + 9: 2, # 't' + 14: 0, # 'u' + 32: 0, # 'v' + 57: 1, # 'w' + 58: 0, # 'x' + 11: 2, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 1, # 'Ü' + 59: 1, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 1, # 'ö' + 17: 1, # 'ü' + 30: 2, # 'ğ' + 41: 0, # 'İ' + 6: 3, # 'ı' + 40: 0, # 'Ş' + 19: 1, # 'ş' + }, + 6: { # 'ı' + 23: 2, # 'A' + 37: 0, # 'B' + 47: 0, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 0, # 'F' + 36: 1, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 2, # 'J' + 16: 3, # 'K' + 49: 0, # 'L' + 20: 3, # 'M' + 46: 1, # 'N' + 42: 0, # 'O' + 48: 0, # 'P' + 44: 0, # 'R' + 35: 0, # 'S' + 31: 2, # 'T' + 51: 0, # 'U' + 38: 0, # 'V' + 62: 0, # 'W' + 43: 2, # 'Y' + 56: 1, # 'Z' + 1: 3, # 'a' + 21: 2, # 'b' + 28: 1, # 'c' + 12: 3, # 'd' + 2: 3, # 'e' + 18: 3, # 'f' + 27: 3, # 'g' + 25: 2, # 'h' + 3: 3, # 'i' + 24: 3, # 'j' + 10: 3, # 'k' + 5: 3, # 'l' + 13: 3, # 'm' + 4: 3, # 'n' + 15: 0, # 'o' + 26: 3, # 'p' + 7: 3, # 'r' + 8: 3, # 's' + 9: 3, # 't' + 14: 3, # 'u' + 32: 3, # 'v' + 57: 1, # 'w' + 58: 1, # 'x' + 11: 3, # 'y' + 22: 0, # 'z' + 63: 1, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 2, # 'ç' + 61: 0, # 'î' + 34: 0, # 'ö' + 17: 3, # 'ü' + 30: 0, # 'ğ' + 41: 0, # 'İ' + 6: 3, # 'ı' + 40: 0, # 'Ş' + 19: 0, # 'ş' + }, + 40: { # 'Ş' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 1, # 'C' + 39: 1, # 'D' + 29: 1, # 'E' + 52: 0, # 'F' + 36: 1, # 'G' + 45: 2, # 'H' + 53: 1, # 'I' + 60: 0, # 'J' + 16: 0, # 'K' + 49: 0, # 'L' + 20: 2, # 'M' + 46: 1, # 'N' + 42: 1, # 'O' + 48: 2, # 'P' + 44: 2, # 'R' + 35: 1, # 'S' + 31: 1, # 'T' + 51: 0, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 2, # 'Y' + 56: 1, # 'Z' + 1: 0, # 'a' + 21: 2, # 'b' + 28: 0, # 'c' + 12: 2, # 'd' + 2: 0, # 'e' + 18: 3, # 'f' + 27: 0, # 'g' + 25: 2, # 'h' + 3: 3, # 'i' + 24: 2, # 'j' + 10: 1, # 'k' + 5: 0, # 'l' + 13: 1, # 'm' + 4: 3, # 'n' + 15: 2, # 'o' + 26: 0, # 'p' + 7: 3, # 'r' + 8: 2, # 's' + 9: 2, # 't' + 14: 1, # 'u' + 32: 3, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 2, # 'y' + 22: 0, # 'z' + 63: 0, # '·' + 54: 0, # 'Ç' + 50: 0, # 'Ö' + 55: 1, # 'Ü' + 59: 0, # 'â' + 33: 0, # 'ç' + 61: 0, # 'î' + 34: 2, # 'ö' + 17: 1, # 'ü' + 30: 2, # 'ğ' + 41: 0, # 'İ' + 6: 2, # 'ı' + 40: 1, # 'Ş' + 19: 2, # 'ş' + }, + 19: { # 'ş' + 23: 0, # 'A' + 37: 0, # 'B' + 47: 1, # 'C' + 39: 0, # 'D' + 29: 0, # 'E' + 52: 2, # 'F' + 36: 1, # 'G' + 45: 0, # 'H' + 53: 0, # 'I' + 60: 0, # 'J' + 16: 3, # 'K' + 49: 2, # 'L' + 20: 0, # 'M' + 46: 1, # 'N' + 42: 1, # 'O' + 48: 1, # 'P' + 44: 1, # 'R' + 35: 1, # 'S' + 31: 0, # 'T' + 51: 1, # 'U' + 38: 1, # 'V' + 62: 0, # 'W' + 43: 1, # 'Y' + 56: 0, # 'Z' + 1: 3, # 'a' + 21: 1, # 'b' + 28: 2, # 'c' + 12: 0, # 'd' + 2: 3, # 'e' + 18: 0, # 'f' + 27: 2, # 'g' + 25: 1, # 'h' + 3: 1, # 'i' + 24: 0, # 'j' + 10: 2, # 'k' + 5: 2, # 'l' + 13: 3, # 'm' + 4: 0, # 'n' + 15: 0, # 'o' + 26: 1, # 'p' + 7: 3, # 'r' + 8: 0, # 's' + 9: 0, # 't' + 14: 3, # 'u' + 32: 0, # 'v' + 57: 0, # 'w' + 58: 0, # 'x' + 11: 0, # 'y' + 22: 2, # 'z' + 63: 0, # '·' + 54: 1, # 'Ç' + 50: 2, # 'Ö' + 55: 0, # 'Ü' + 59: 0, # 'â' + 33: 1, # 'ç' + 61: 1, # 'î' + 34: 2, # 'ö' + 17: 0, # 'ü' + 30: 1, # 'ğ' + 41: 1, # 'İ' + 6: 1, # 'ı' + 40: 1, # 'Ş' + 19: 1, # 'ş' + }, +} + +# 255: Undefined characters that did not exist in training text # 254: Carriage/Return # 253: symbol (punctuation) that does not belong to word # 252: 0 - 9 +# 251: Control characters -# Character Mapping Table: -Latin5_TurkishCharToOrderMap = ( -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, -255,255,255,255,255,255,255,255,255,255,255,255,255,255,255,255, -255, 23, 37, 47, 39, 29, 52, 36, 45, 53, 60, 16, 49, 20, 46, 42, - 48, 69, 44, 35, 31, 51, 38, 62, 65, 43, 56,255,255,255,255,255, -255, 1, 21, 28, 12, 2, 18, 27, 25, 3, 24, 10, 5, 13, 4, 15, - 26, 64, 7, 8, 9, 14, 32, 57, 58, 11, 22,255,255,255,255,255, -180,179,178,177,176,175,174,173,172,171,170,169,168,167,166,165, -164,163,162,161,160,159,101,158,157,156,155,154,153,152,151,106, -150,149,148,147,146,145,144,100,143,142,141,140,139,138,137,136, - 94, 80, 93,135,105,134,133, 63,132,131,130,129,128,127,126,125, -124,104, 73, 99, 79, 85,123, 54,122, 98, 92,121,120, 91,103,119, - 68,118,117, 97,116,115, 50, 90,114,113,112,111, 55, 41, 40, 86, - 89, 70, 59, 78, 71, 82, 88, 33, 77, 66, 84, 83,110, 75, 61, 96, - 30, 67,109, 74, 87,102, 34, 95, 81,108, 76, 72, 17, 6, 19,107, -) +# Character Mapping Table(s): +ISO_8859_9_TURKISH_CHAR_TO_ORDER = { + 0: 255, # '\x00' + 1: 255, # '\x01' + 2: 255, # '\x02' + 3: 255, # '\x03' + 4: 255, # '\x04' + 5: 255, # '\x05' + 6: 255, # '\x06' + 7: 255, # '\x07' + 8: 255, # '\x08' + 9: 255, # '\t' + 10: 255, # '\n' + 11: 255, # '\x0b' + 12: 255, # '\x0c' + 13: 255, # '\r' + 14: 255, # '\x0e' + 15: 255, # '\x0f' + 16: 255, # '\x10' + 17: 255, # '\x11' + 18: 255, # '\x12' + 19: 255, # '\x13' + 20: 255, # '\x14' + 21: 255, # '\x15' + 22: 255, # '\x16' + 23: 255, # '\x17' + 24: 255, # '\x18' + 25: 255, # '\x19' + 26: 255, # '\x1a' + 27: 255, # '\x1b' + 28: 255, # '\x1c' + 29: 255, # '\x1d' + 30: 255, # '\x1e' + 31: 255, # '\x1f' + 32: 255, # ' ' + 33: 255, # '!' + 34: 255, # '"' + 35: 255, # '#' + 36: 255, # '$' + 37: 255, # '%' + 38: 255, # '&' + 39: 255, # "'" + 40: 255, # '(' + 41: 255, # ')' + 42: 255, # '*' + 43: 255, # '+' + 44: 255, # ',' + 45: 255, # '-' + 46: 255, # '.' + 47: 255, # '/' + 48: 255, # '0' + 49: 255, # '1' + 50: 255, # '2' + 51: 255, # '3' + 52: 255, # '4' + 53: 255, # '5' + 54: 255, # '6' + 55: 255, # '7' + 56: 255, # '8' + 57: 255, # '9' + 58: 255, # ':' + 59: 255, # ';' + 60: 255, # '<' + 61: 255, # '=' + 62: 255, # '>' + 63: 255, # '?' + 64: 255, # '@' + 65: 23, # 'A' + 66: 37, # 'B' + 67: 47, # 'C' + 68: 39, # 'D' + 69: 29, # 'E' + 70: 52, # 'F' + 71: 36, # 'G' + 72: 45, # 'H' + 73: 53, # 'I' + 74: 60, # 'J' + 75: 16, # 'K' + 76: 49, # 'L' + 77: 20, # 'M' + 78: 46, # 'N' + 79: 42, # 'O' + 80: 48, # 'P' + 81: 69, # 'Q' + 82: 44, # 'R' + 83: 35, # 'S' + 84: 31, # 'T' + 85: 51, # 'U' + 86: 38, # 'V' + 87: 62, # 'W' + 88: 65, # 'X' + 89: 43, # 'Y' + 90: 56, # 'Z' + 91: 255, # '[' + 92: 255, # '\\' + 93: 255, # ']' + 94: 255, # '^' + 95: 255, # '_' + 96: 255, # '`' + 97: 1, # 'a' + 98: 21, # 'b' + 99: 28, # 'c' + 100: 12, # 'd' + 101: 2, # 'e' + 102: 18, # 'f' + 103: 27, # 'g' + 104: 25, # 'h' + 105: 3, # 'i' + 106: 24, # 'j' + 107: 10, # 'k' + 108: 5, # 'l' + 109: 13, # 'm' + 110: 4, # 'n' + 111: 15, # 'o' + 112: 26, # 'p' + 113: 64, # 'q' + 114: 7, # 'r' + 115: 8, # 's' + 116: 9, # 't' + 117: 14, # 'u' + 118: 32, # 'v' + 119: 57, # 'w' + 120: 58, # 'x' + 121: 11, # 'y' + 122: 22, # 'z' + 123: 255, # '{' + 124: 255, # '|' + 125: 255, # '}' + 126: 255, # '~' + 127: 255, # '\x7f' + 128: 180, # '\x80' + 129: 179, # '\x81' + 130: 178, # '\x82' + 131: 177, # '\x83' + 132: 176, # '\x84' + 133: 175, # '\x85' + 134: 174, # '\x86' + 135: 173, # '\x87' + 136: 172, # '\x88' + 137: 171, # '\x89' + 138: 170, # '\x8a' + 139: 169, # '\x8b' + 140: 168, # '\x8c' + 141: 167, # '\x8d' + 142: 166, # '\x8e' + 143: 165, # '\x8f' + 144: 164, # '\x90' + 145: 163, # '\x91' + 146: 162, # '\x92' + 147: 161, # '\x93' + 148: 160, # '\x94' + 149: 159, # '\x95' + 150: 101, # '\x96' + 151: 158, # '\x97' + 152: 157, # '\x98' + 153: 156, # '\x99' + 154: 155, # '\x9a' + 155: 154, # '\x9b' + 156: 153, # '\x9c' + 157: 152, # '\x9d' + 158: 151, # '\x9e' + 159: 106, # '\x9f' + 160: 150, # '\xa0' + 161: 149, # '¡' + 162: 148, # '¢' + 163: 147, # '£' + 164: 146, # '¤' + 165: 145, # '¥' + 166: 144, # '¦' + 167: 100, # '§' + 168: 143, # '¨' + 169: 142, # '©' + 170: 141, # 'ª' + 171: 140, # '«' + 172: 139, # '¬' + 173: 138, # '\xad' + 174: 137, # '®' + 175: 136, # '¯' + 176: 94, # '°' + 177: 80, # '±' + 178: 93, # '²' + 179: 135, # '³' + 180: 105, # '´' + 181: 134, # 'µ' + 182: 133, # '¶' + 183: 63, # '·' + 184: 132, # '¸' + 185: 131, # '¹' + 186: 130, # 'º' + 187: 129, # '»' + 188: 128, # '¼' + 189: 127, # '½' + 190: 126, # '¾' + 191: 125, # '¿' + 192: 124, # 'À' + 193: 104, # 'Á' + 194: 73, # 'Â' + 195: 99, # 'Ã' + 196: 79, # 'Ä' + 197: 85, # 'Å' + 198: 123, # 'Æ' + 199: 54, # 'Ç' + 200: 122, # 'È' + 201: 98, # 'É' + 202: 92, # 'Ê' + 203: 121, # 'Ë' + 204: 120, # 'Ì' + 205: 91, # 'Í' + 206: 103, # 'Î' + 207: 119, # 'Ï' + 208: 68, # 'Ğ' + 209: 118, # 'Ñ' + 210: 117, # 'Ò' + 211: 97, # 'Ó' + 212: 116, # 'Ô' + 213: 115, # 'Õ' + 214: 50, # 'Ö' + 215: 90, # '×' + 216: 114, # 'Ø' + 217: 113, # 'Ù' + 218: 112, # 'Ú' + 219: 111, # 'Û' + 220: 55, # 'Ü' + 221: 41, # 'İ' + 222: 40, # 'Ş' + 223: 86, # 'ß' + 224: 89, # 'à' + 225: 70, # 'á' + 226: 59, # 'â' + 227: 78, # 'ã' + 228: 71, # 'ä' + 229: 82, # 'å' + 230: 88, # 'æ' + 231: 33, # 'ç' + 232: 77, # 'è' + 233: 66, # 'é' + 234: 84, # 'ê' + 235: 83, # 'ë' + 236: 110, # 'ì' + 237: 75, # 'í' + 238: 61, # 'î' + 239: 96, # 'ï' + 240: 30, # 'ğ' + 241: 67, # 'ñ' + 242: 109, # 'ò' + 243: 74, # 'ó' + 244: 87, # 'ô' + 245: 102, # 'õ' + 246: 34, # 'ö' + 247: 95, # '÷' + 248: 81, # 'ø' + 249: 108, # 'ù' + 250: 76, # 'ú' + 251: 72, # 'û' + 252: 17, # 'ü' + 253: 6, # 'ı' + 254: 19, # 'ş' + 255: 107, # 'ÿ' +} -TurkishLangModel = ( -3,2,3,3,3,1,3,3,3,3,3,3,3,3,2,1,1,3,3,1,3,3,0,3,3,3,3,3,0,3,1,3, -3,2,1,0,0,1,1,0,0,0,1,0,0,1,1,1,1,0,0,0,0,0,0,0,2,2,0,0,1,0,0,1, -3,2,2,3,3,0,3,3,3,3,3,3,3,2,3,1,0,3,3,1,3,3,0,3,3,3,3,3,0,3,0,3, -3,1,1,0,1,0,1,0,0,0,0,0,0,1,1,1,1,0,0,0,0,0,0,0,2,2,0,0,0,1,0,1, -3,3,2,3,3,0,3,3,3,3,3,3,3,2,3,1,1,3,3,0,3,3,1,2,3,3,3,3,0,3,0,3, -3,1,1,0,0,0,1,0,0,0,0,1,1,0,1,2,1,0,0,0,1,0,0,0,0,2,0,0,0,0,0,1, -3,3,3,3,3,3,2,3,3,3,3,3,3,3,3,1,3,3,2,0,3,2,1,2,2,1,3,3,0,0,0,2, -2,2,0,1,0,0,1,0,0,1,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,1,0,1,0,0,1, -3,3,3,2,3,3,1,2,3,3,3,3,3,3,3,1,3,2,1,0,3,2,0,1,2,3,3,2,1,0,0,2, -2,1,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,2,0,2,0,0,0, -1,0,1,3,3,1,3,3,3,3,3,3,3,1,2,0,0,2,3,0,2,3,0,0,2,2,2,3,0,3,0,1, -2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,3,3,3,3,3,3,3,3,3,3,3,3,0,3,3,3,0,3,2,0,2,3,2,3,3,1,0,0,2, -3,2,0,0,1,0,0,0,0,0,0,2,0,0,1,0,0,0,0,0,0,0,0,0,1,1,1,0,2,0,0,1, -3,3,3,2,3,3,2,3,3,3,3,2,3,3,3,0,3,3,0,0,2,1,0,0,2,3,2,2,0,0,0,2, -2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,0,0,1,0,1,0,2,0,0,1, -3,3,3,2,3,3,3,3,3,3,3,2,3,3,3,0,3,2,0,1,3,2,1,1,3,2,3,2,1,0,0,2, -2,2,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0, -3,3,3,2,3,3,3,3,3,3,3,2,3,3,3,0,3,2,2,0,2,3,0,0,2,2,2,2,0,0,0,2, -3,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,2,0,1,0,0,0, -3,3,3,3,3,3,3,2,2,2,2,3,2,3,3,0,3,3,1,1,2,2,0,0,2,2,3,2,0,0,1,3, -0,3,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0,1, -3,3,3,2,3,3,3,2,1,2,2,3,2,3,3,0,3,2,0,0,1,1,0,1,1,2,1,2,0,0,0,1, -0,3,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,1,0,0,0, -3,3,3,2,3,3,2,3,2,2,2,3,3,3,3,1,3,1,1,0,3,2,1,1,3,3,2,3,1,0,0,1, -1,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,2,0,0,1, -3,2,2,3,3,0,3,3,3,3,3,3,3,2,2,1,0,3,3,1,3,3,0,1,3,3,2,3,0,3,0,3, -2,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0, -2,2,2,3,3,0,3,3,3,3,3,3,3,3,3,0,0,3,2,0,3,3,0,3,2,3,3,3,0,3,1,3, -2,0,0,0,0,0,0,0,0,0,0,1,0,1,2,0,1,0,0,0,0,0,0,0,2,2,0,0,1,0,0,1, -3,3,3,1,2,3,3,1,0,0,1,0,0,3,3,2,3,0,0,2,0,0,2,0,2,0,0,0,2,0,2,0, -0,3,1,0,1,0,0,0,2,2,1,0,1,1,2,1,2,2,2,0,2,1,1,0,0,0,2,0,0,0,0,0, -1,2,1,3,3,0,3,3,3,3,3,2,3,0,0,0,0,2,3,0,2,3,1,0,2,3,1,3,0,3,0,2, -3,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,1,3,3,2,2,3,2,2,0,1,2,3,0,1,2,1,0,1,0,0,0,1,0,2,2,0,0,0,1, -1,1,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,1,1,0,0,1,0,0,0, -3,3,3,1,3,3,1,1,3,3,1,1,3,3,1,0,2,1,2,0,2,1,0,0,1,1,2,1,0,0,0,2, -2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,1,0,2,1,3,0,0,2,0,0,3,3,0,3,0,0,1,0,1,2,0,0,1,1,2,2,0,1,0, -0,1,2,1,1,0,1,0,1,1,1,1,1,0,1,1,1,2,2,1,2,0,1,0,0,0,0,0,0,1,0,0, -3,3,3,2,3,2,3,3,0,2,2,2,3,3,3,0,3,0,0,0,2,2,0,1,2,1,1,1,0,0,0,1, -0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0, -3,3,3,3,3,3,2,1,2,2,3,3,3,3,2,0,2,0,0,0,2,2,0,0,2,1,3,3,0,0,1,1, -1,1,0,0,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0, -1,1,2,3,3,0,3,3,3,3,3,3,2,2,0,2,0,2,3,2,3,2,2,2,2,2,2,2,1,3,2,3, -2,0,2,1,2,2,2,2,1,1,2,2,1,2,2,1,2,0,0,2,1,1,0,2,1,0,0,1,0,0,0,1, -2,3,3,1,1,1,0,1,1,1,2,3,2,1,1,0,0,0,0,0,0,0,0,0,0,1,0,1,0,0,0,0, -0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,2,2,2,3,2,3,2,2,1,3,3,3,0,2,1,2,0,2,1,0,0,1,1,1,1,1,0,0,1, -2,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,2,0,1,0,0,0, -3,3,3,2,3,3,3,3,3,2,3,1,2,3,3,1,2,0,0,0,0,0,0,0,3,2,1,1,0,0,0,0, -2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,0, -3,3,3,2,2,3,3,2,1,1,1,1,1,3,3,0,3,1,0,0,1,1,0,0,3,1,2,1,0,0,0,0, -0,3,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0, -3,3,3,2,2,3,2,2,2,3,2,1,1,3,3,0,3,0,0,0,0,1,0,0,3,1,1,2,0,0,0,1, -1,0,0,1,0,0,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1, -1,1,1,3,3,0,3,3,3,3,3,2,2,2,1,2,0,2,1,2,2,1,1,0,1,2,2,2,2,2,2,2, -0,0,2,1,2,1,2,1,0,1,1,3,1,2,1,1,2,0,0,2,0,1,0,1,0,1,0,0,0,1,0,1, -3,3,3,1,3,3,3,0,1,1,0,2,2,3,1,0,3,0,0,0,1,0,0,0,1,0,0,1,0,1,0,0, -1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,2,0,0,2,2,1,0,0,1,0,0,3,3,1,3,0,0,1,1,0,2,0,3,0,0,0,2,0,1,1, -0,1,2,0,1,2,2,0,2,2,2,2,1,0,2,1,1,0,2,0,2,1,2,0,0,0,0,0,0,0,0,0, -3,3,3,1,3,2,3,2,0,2,2,2,1,3,2,0,2,1,2,0,1,2,0,0,1,0,2,2,0,0,0,2, -1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,1,0,1,0,0,0, -3,3,3,0,3,3,1,1,2,3,1,0,3,2,3,0,3,0,0,0,1,0,0,0,1,0,1,0,0,0,0,0, -1,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,3,3,0,3,3,2,3,3,2,2,0,0,0,0,1,2,0,1,3,0,0,0,3,1,1,0,3,0,2, -2,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,1,2,2,1,0,3,1,1,1,1,3,3,2,3,0,0,1,0,1,2,0,2,2,0,2,2,0,2,1, -0,2,2,1,1,1,1,0,2,1,1,0,1,1,1,1,2,1,2,1,2,0,1,0,1,0,0,0,0,0,0,0, -3,3,3,0,1,1,3,0,0,1,1,0,0,2,2,0,3,0,0,1,1,0,1,0,0,0,0,0,2,0,0,0, -0,3,1,0,1,0,1,0,2,0,0,1,0,1,0,1,1,1,2,1,1,0,2,0,0,0,0,0,0,0,0,0, -3,3,3,0,2,0,2,0,1,1,1,0,0,3,3,0,2,0,0,1,0,0,2,1,1,0,1,0,1,0,1,0, -0,2,0,1,2,0,2,0,2,1,1,0,1,0,2,1,1,0,2,1,1,0,1,0,0,0,1,1,0,0,0,0, -3,2,3,0,1,0,0,0,0,0,0,0,0,1,2,0,1,0,0,1,0,0,1,0,0,0,0,0,2,0,0,0, -0,0,1,1,0,0,1,0,1,0,0,1,0,0,0,2,1,0,1,0,2,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,0,0,2,3,0,0,1,0,1,0,2,3,2,3,0,0,1,3,0,2,1,0,0,0,0,2,0,1,0, -0,2,1,0,0,1,1,0,2,1,0,0,1,0,0,1,1,0,1,1,2,0,1,0,0,0,0,1,0,0,0,0, -3,2,2,0,0,1,1,0,0,0,0,0,0,3,1,1,1,0,0,0,0,0,1,0,0,0,0,0,2,0,1,0, -0,1,0,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0, -0,0,0,3,3,0,2,3,2,2,1,2,2,1,1,2,0,1,3,2,2,2,0,0,2,2,0,0,0,1,2,1, -3,0,2,1,1,0,1,1,1,0,1,2,2,2,1,1,2,0,0,0,0,1,0,1,1,0,0,0,0,0,0,0, -0,1,1,2,3,0,3,3,3,2,2,2,2,1,0,1,0,1,0,1,2,2,0,0,2,2,1,3,1,1,2,1, -0,0,1,1,2,0,1,1,0,0,1,2,0,2,1,1,2,0,0,1,0,0,0,1,0,1,0,1,0,0,0,0, -3,3,2,0,0,3,1,0,0,0,0,0,0,3,2,1,2,0,0,1,0,0,2,0,0,0,0,0,2,0,1,0, -0,2,1,1,0,0,1,0,1,2,0,0,1,1,0,0,2,1,1,1,1,0,2,0,0,0,0,0,0,0,0,0, -3,3,2,0,0,1,0,0,0,0,1,0,0,3,3,2,2,0,0,1,0,0,2,0,1,0,0,0,2,0,1,0, -0,0,1,1,0,0,2,0,2,1,0,0,1,1,2,1,2,0,2,1,2,1,1,1,0,0,1,1,0,0,0,0, -3,3,2,0,0,2,2,0,0,0,1,1,0,2,2,1,3,1,0,1,0,1,2,0,0,0,0,0,1,0,1,0, -0,1,1,0,0,0,0,0,1,0,0,1,0,0,0,1,1,0,1,0,1,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,2,0,0,0,1,0,0,1,0,0,2,3,1,2,0,0,1,0,0,2,0,0,0,1,0,2,0,2,0, -0,1,1,2,2,1,2,0,2,1,1,0,0,1,1,0,1,1,1,1,2,1,1,0,0,0,0,0,0,0,0,0, -3,3,3,0,2,1,2,1,0,0,1,1,0,3,3,1,2,0,0,1,0,0,2,0,2,0,1,1,2,0,0,0, -0,0,1,1,1,1,2,0,1,1,0,1,1,1,1,0,0,0,1,1,1,0,1,0,0,0,1,0,0,0,0,0, -3,3,3,0,2,2,3,2,0,0,1,0,0,2,3,1,0,0,0,0,0,0,2,0,2,0,0,0,2,0,0,0, -0,1,1,0,0,0,1,0,0,1,0,1,1,0,1,0,1,1,1,0,1,0,0,0,0,0,0,0,0,0,0,0, -3,2,3,0,0,0,0,0,0,0,1,0,0,2,2,2,2,0,0,1,0,0,2,0,0,0,0,0,2,0,1,0, -0,0,2,1,1,0,1,0,2,1,1,0,0,1,1,2,1,0,2,0,2,0,1,0,0,0,2,0,0,0,0,0, -0,0,0,2,2,0,2,1,1,1,1,2,2,0,0,1,0,1,0,0,1,3,0,0,0,0,1,0,0,2,1,0, -0,0,1,0,1,0,0,0,0,0,2,1,0,1,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0,0, -2,0,0,2,3,0,2,3,1,2,2,0,2,0,0,2,0,2,1,1,1,2,1,0,0,1,2,1,1,2,1,0, -1,0,2,0,1,0,1,1,0,0,2,2,1,2,1,1,2,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0, -3,3,3,0,2,1,2,0,0,0,1,0,0,3,2,0,1,0,0,1,0,0,2,0,0,0,1,2,1,0,1,0, -0,0,0,0,1,0,1,0,0,1,0,0,0,0,1,0,1,0,1,1,1,0,1,0,0,0,0,0,0,0,0,0, -0,0,0,2,2,0,2,2,1,1,0,1,1,1,1,1,0,0,1,2,1,1,1,0,1,0,0,0,1,1,1,1, -0,0,2,1,0,1,1,1,0,1,1,2,1,2,1,1,2,0,1,1,2,1,0,2,0,0,0,0,0,0,0,0, -3,2,2,0,0,2,0,0,0,0,0,0,0,2,2,0,2,0,0,1,0,0,2,0,0,0,0,0,2,0,0,0, -0,2,1,0,0,0,0,0,1,0,0,0,0,0,0,0,1,0,0,0,1,0,1,0,0,0,0,0,0,0,0,0, -0,0,0,3,2,0,2,2,0,1,1,0,1,0,0,1,0,0,0,1,0,1,0,0,0,0,0,1,0,0,0,0, -2,0,1,0,1,0,1,1,0,0,1,2,0,1,0,1,1,0,0,1,0,1,0,2,0,0,0,0,0,0,0,0, -2,2,2,0,1,1,0,0,0,1,0,0,0,1,2,0,1,0,0,1,0,0,1,0,0,0,0,1,2,0,1,0, -0,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0,1,0,1,0,2,0,0,0,0,0,0,0,0,0,0,0, -2,2,2,2,1,0,1,1,1,0,0,0,0,1,2,0,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,2,0,0,0,0,0,0,0, -1,1,2,0,1,0,0,0,1,0,1,0,0,0,1,0,0,1,0,0,0,0,0,0,0,1,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,0,2,0,0,0,0,0,1, -0,0,1,2,2,0,2,1,2,1,1,2,2,0,0,0,0,1,0,0,1,1,0,0,2,0,0,0,0,1,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0, -2,2,2,0,0,0,1,0,0,0,0,0,0,2,2,1,1,0,0,0,0,0,1,0,0,0,0,0,0,0,0,0, -0,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,1,1,0,0,0,1,0,0,0,1,0,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -2,2,2,0,1,0,1,0,0,0,0,0,0,1,1,0,0,0,0,0,0,0,1,0,1,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,1,0,0,0,0,1,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,1,0,0,0,0,0,0,0,0,0,0,2,2,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0,0, -) +ISO_8859_9_TURKISH_MODEL = SingleByteCharSetModel(charset_name='ISO-8859-9', + language='Turkish', + char_to_order_map=ISO_8859_9_TURKISH_CHAR_TO_ORDER, + language_model=TURKISH_LANG_MODEL, + typical_positive_ratio=0.97029, + keep_ascii_letters=True, + alphabet='ABCDEFGHIJKLMNOPRSTUVYZabcdefghijklmnoprstuvyzÂÇÎÖÛÜâçîöûüĞğİıŞş') -Latin5TurkishModel = { - 'char_to_order_map': Latin5_TurkishCharToOrderMap, - 'precedence_matrix': TurkishLangModel, - 'typical_positive_ratio': 0.970290, - 'keep_english_letter': True, - 'charset_name': "ISO-8859-9", - 'language': 'Turkish', -} diff --git a/src/pip/_vendor/chardet/metadata/__init__.py b/src/pip/_vendor/chardet/metadata/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_vendor/chardet/metadata/languages.py b/src/pip/_vendor/chardet/metadata/languages.py new file mode 100644 index 00000000000..3237d5abf60 --- /dev/null +++ b/src/pip/_vendor/chardet/metadata/languages.py @@ -0,0 +1,310 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +""" +Metadata about languages used by our model training code for our +SingleByteCharSetProbers. Could be used for other things in the future. + +This code is based on the language metadata from the uchardet project. +""" +from __future__ import absolute_import, print_function + +from string import ascii_letters + + +# TODO: Add Ukranian (KOI8-U) + +class Language(object): + """Metadata about a language useful for training models + + :ivar name: The human name for the language, in English. + :type name: str + :ivar iso_code: 2-letter ISO 639-1 if possible, 3-letter ISO code otherwise, + or use another catalog as a last resort. + :type iso_code: str + :ivar use_ascii: Whether or not ASCII letters should be included in trained + models. + :type use_ascii: bool + :ivar charsets: The charsets we want to support and create data for. + :type charsets: list of str + :ivar alphabet: The characters in the language's alphabet. If `use_ascii` is + `True`, you only need to add those not in the ASCII set. + :type alphabet: str + :ivar wiki_start_pages: The Wikipedia pages to start from if we're crawling + Wikipedia for training data. + :type wiki_start_pages: list of str + """ + def __init__(self, name=None, iso_code=None, use_ascii=True, charsets=None, + alphabet=None, wiki_start_pages=None): + super(Language, self).__init__() + self.name = name + self.iso_code = iso_code + self.use_ascii = use_ascii + self.charsets = charsets + if self.use_ascii: + if alphabet: + alphabet += ascii_letters + else: + alphabet = ascii_letters + elif not alphabet: + raise ValueError('Must supply alphabet if use_ascii is False') + self.alphabet = ''.join(sorted(set(alphabet))) if alphabet else None + self.wiki_start_pages = wiki_start_pages + + def __repr__(self): + return '{}({})'.format(self.__class__.__name__, + ', '.join('{}={!r}'.format(k, v) + for k, v in self.__dict__.items() + if not k.startswith('_'))) + + +LANGUAGES = {'Arabic': Language(name='Arabic', + iso_code='ar', + use_ascii=False, + # We only support encodings that use isolated + # forms, because the current recommendation is + # that the rendering system handles presentation + # forms. This means we purposefully skip IBM864. + charsets=['ISO-8859-6', 'WINDOWS-1256', + 'CP720', 'CP864'], + alphabet=u'ءآأؤإئابةتثجحخدذرزسشصضطظعغػؼؽؾؿـفقكلمنهوىيًٌٍَُِّ', + wiki_start_pages=[u'الصفحة_الرئيسية']), + 'Belarusian': Language(name='Belarusian', + iso_code='be', + use_ascii=False, + charsets=['ISO-8859-5', 'WINDOWS-1251', + 'IBM866', 'MacCyrillic'], + alphabet=(u'АБВГДЕЁЖЗІЙКЛМНОПРСТУЎФХЦЧШЫЬЭЮЯ' + u'абвгдеёжзійклмнопрстуўфхцчшыьэюяʼ'), + wiki_start_pages=[u'Галоўная_старонка']), + 'Bulgarian': Language(name='Bulgarian', + iso_code='bg', + use_ascii=False, + charsets=['ISO-8859-5', 'WINDOWS-1251', + 'IBM855'], + alphabet=(u'АБВГДЕЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЬЮЯ' + u'абвгдежзийклмнопрстуфхцчшщъьюя'), + wiki_start_pages=[u'Начална_страница']), + 'Czech': Language(name='Czech', + iso_code='cz', + use_ascii=True, + charsets=['ISO-8859-2', 'WINDOWS-1250'], + alphabet=u'áčďéěíňóřšťúůýžÁČĎÉĚÍŇÓŘŠŤÚŮÝŽ', + wiki_start_pages=[u'Hlavní_strana']), + 'Danish': Language(name='Danish', + iso_code='da', + use_ascii=True, + charsets=['ISO-8859-1', 'ISO-8859-15', + 'WINDOWS-1252'], + alphabet=u'æøåÆØÅ', + wiki_start_pages=[u'Forside']), + 'German': Language(name='German', + iso_code='de', + use_ascii=True, + charsets=['ISO-8859-1', 'WINDOWS-1252'], + alphabet=u'äöüßÄÖÜ', + wiki_start_pages=[u'Wikipedia:Hauptseite']), + 'Greek': Language(name='Greek', + iso_code='el', + use_ascii=False, + charsets=['ISO-8859-7', 'WINDOWS-1253'], + alphabet=(u'αβγδεζηθικλμνξοπρσςτυφχψωάέήίόύώ' + u'ΑΒΓΔΕΖΗΘΙΚΛΜΝΞΟΠΡΣΣΤΥΦΧΨΩΆΈΉΊΌΎΏ'), + wiki_start_pages=[u'Πύλη:Κύρια']), + 'English': Language(name='English', + iso_code='en', + use_ascii=True, + charsets=['ISO-8859-1', 'WINDOWS-1252'], + wiki_start_pages=[u'Main_Page']), + 'Esperanto': Language(name='Esperanto', + iso_code='eo', + # Q, W, X, and Y not used at all + use_ascii=False, + charsets=['ISO-8859-3'], + alphabet=(u'abcĉdefgĝhĥijĵklmnoprsŝtuŭvz' + u'ABCĈDEFGĜHĤIJĴKLMNOPRSŜTUŬVZ'), + wiki_start_pages=[u'Vikipedio:Ĉefpaĝo']), + 'Spanish': Language(name='Spanish', + iso_code='es', + use_ascii=True, + charsets=['ISO-8859-1', 'ISO-8859-15', + 'WINDOWS-1252'], + alphabet=u'ñáéíóúüÑÁÉÍÓÚÜ', + wiki_start_pages=[u'Wikipedia:Portada']), + 'Estonian': Language(name='Estonian', + iso_code='et', + use_ascii=False, + charsets=['ISO-8859-4', 'ISO-8859-13', + 'WINDOWS-1257'], + # C, F, Š, Q, W, X, Y, Z, Ž are only for + # loanwords + alphabet=(u'ABDEGHIJKLMNOPRSTUVÕÄÖÜ' + u'abdeghijklmnoprstuvõäöü'), + wiki_start_pages=[u'Esileht']), + 'Finnish': Language(name='Finnish', + iso_code='fi', + use_ascii=True, + charsets=['ISO-8859-1', 'ISO-8859-15', + 'WINDOWS-1252'], + alphabet=u'ÅÄÖŠŽåäöšž', + wiki_start_pages=[u'Wikipedia:Etusivu']), + 'French': Language(name='French', + iso_code='fr', + use_ascii=True, + charsets=['ISO-8859-1', 'ISO-8859-15', + 'WINDOWS-1252'], + alphabet=u'œàâçèéîïùûêŒÀÂÇÈÉÎÏÙÛÊ', + wiki_start_pages=[u'Wikipédia:Accueil_principal', + u'Bœuf (animal)']), + 'Hebrew': Language(name='Hebrew', + iso_code='he', + use_ascii=False, + charsets=['ISO-8859-8', 'WINDOWS-1255'], + alphabet=u'אבגדהוזחטיךכלםמןנסעףפץצקרשתװױײ', + wiki_start_pages=[u'עמוד_ראשי']), + 'Croatian': Language(name='Croatian', + iso_code='hr', + # Q, W, X, Y are only used for foreign words. + use_ascii=False, + charsets=['ISO-8859-2', 'WINDOWS-1250'], + alphabet=(u'abcčćdđefghijklmnoprsštuvzž' + u'ABCČĆDĐEFGHIJKLMNOPRSŠTUVZŽ'), + wiki_start_pages=[u'Glavna_stranica']), + 'Hungarian': Language(name='Hungarian', + iso_code='hu', + # Q, W, X, Y are only used for foreign words. + use_ascii=False, + charsets=['ISO-8859-2', 'WINDOWS-1250'], + alphabet=(u'abcdefghijklmnoprstuvzáéíóöőúüű' + u'ABCDEFGHIJKLMNOPRSTUVZÁÉÍÓÖŐÚÜŰ'), + wiki_start_pages=[u'Kezdőlap']), + 'Italian': Language(name='Italian', + iso_code='it', + use_ascii=True, + charsets=['ISO-8859-1', 'ISO-8859-15', + 'WINDOWS-1252'], + alphabet=u'ÀÈÉÌÒÓÙàèéìòóù', + wiki_start_pages=[u'Pagina_principale']), + 'Lithuanian': Language(name='Lithuanian', + iso_code='lt', + use_ascii=False, + charsets=['ISO-8859-13', 'WINDOWS-1257', + 'ISO-8859-4'], + # Q, W, and X not used at all + alphabet=(u'AĄBCČDEĘĖFGHIĮYJKLMNOPRSŠTUŲŪVZŽ' + u'aąbcčdeęėfghiįyjklmnoprsštuųūvzž'), + wiki_start_pages=[u'Pagrindinis_puslapis']), + 'Latvian': Language(name='Latvian', + iso_code='lv', + use_ascii=False, + charsets=['ISO-8859-13', 'WINDOWS-1257', + 'ISO-8859-4'], + # Q, W, X, Y are only for loanwords + alphabet=(u'AĀBCČDEĒFGĢHIĪJKĶLĻMNŅOPRSŠTUŪVZŽ' + u'aābcčdeēfgģhiījkķlļmnņoprsštuūvzž'), + wiki_start_pages=[u'Sākumlapa']), + 'Macedonian': Language(name='Macedonian', + iso_code='mk', + use_ascii=False, + charsets=['ISO-8859-5', 'WINDOWS-1251', + 'MacCyrillic', 'IBM855'], + alphabet=(u'АБВГДЃЕЖЗЅИЈКЛЉМНЊОПРСТЌУФХЦЧЏШ' + u'абвгдѓежзѕијклљмнњопрстќуфхцчџш'), + wiki_start_pages=[u'Главна_страница']), + 'Dutch': Language(name='Dutch', + iso_code='nl', + use_ascii=True, + charsets=['ISO-8859-1', 'WINDOWS-1252'], + wiki_start_pages=[u'Hoofdpagina']), + 'Polish': Language(name='Polish', + iso_code='pl', + # Q and X are only used for foreign words. + use_ascii=False, + charsets=['ISO-8859-2', 'WINDOWS-1250'], + alphabet=(u'AĄBCĆDEĘFGHIJKLŁMNŃOÓPRSŚTUWYZŹŻ' + u'aąbcćdeęfghijklłmnńoóprsśtuwyzźż'), + wiki_start_pages=[u'Wikipedia:Strona_główna']), + 'Portuguese': Language(name='Portuguese', + iso_code='pt', + use_ascii=True, + charsets=['ISO-8859-1', 'ISO-8859-15', + 'WINDOWS-1252'], + alphabet=u'ÁÂÃÀÇÉÊÍÓÔÕÚáâãàçéêíóôõú', + wiki_start_pages=[u'Wikipédia:Página_principal']), + 'Romanian': Language(name='Romanian', + iso_code='ro', + use_ascii=True, + charsets=['ISO-8859-2', 'WINDOWS-1250'], + alphabet=u'ăâîșțĂÂÎȘȚ', + wiki_start_pages=[u'Pagina_principală']), + 'Russian': Language(name='Russian', + iso_code='ru', + use_ascii=False, + charsets=['ISO-8859-5', 'WINDOWS-1251', + 'KOI8-R', 'MacCyrillic', 'IBM866', + 'IBM855'], + alphabet=(u'абвгдеёжзийклмнопрстуфхцчшщъыьэюя' + u'АБВГДЕЁЖЗИЙКЛМНОПРСТУФХЦЧШЩЪЫЬЭЮЯ'), + wiki_start_pages=[u'Заглавная_страница']), + 'Slovak': Language(name='Slovak', + iso_code='sk', + use_ascii=True, + charsets=['ISO-8859-2', 'WINDOWS-1250'], + alphabet=u'áäčďéíĺľňóôŕšťúýžÁÄČĎÉÍĹĽŇÓÔŔŠŤÚÝŽ', + wiki_start_pages=[u'Hlavná_stránka']), + 'Slovene': Language(name='Slovene', + iso_code='sl', + # Q, W, X, Y are only used for foreign words. + use_ascii=False, + charsets=['ISO-8859-2', 'WINDOWS-1250'], + alphabet=(u'abcčdefghijklmnoprsštuvzž' + u'ABCČDEFGHIJKLMNOPRSŠTUVZŽ'), + wiki_start_pages=[u'Glavna_stran']), + # Serbian can be written in both Latin and Cyrillic, but there's no + # simple way to get the Latin alphabet pages from Wikipedia through + # the API, so for now we just support Cyrillic. + 'Serbian': Language(name='Serbian', + iso_code='sr', + alphabet=(u'АБВГДЂЕЖЗИЈКЛЉМНЊОПРСТЋУФХЦЧЏШ' + u'абвгдђежзијклљмнњопрстћуфхцчџш'), + charsets=['ISO-8859-5', 'WINDOWS-1251', + 'MacCyrillic', 'IBM855'], + wiki_start_pages=[u'Главна_страна']), + 'Thai': Language(name='Thai', + iso_code='th', + use_ascii=False, + charsets=['ISO-8859-11', 'TIS-620', 'CP874'], + alphabet=u'กขฃคฅฆงจฉชซฌญฎฏฐฑฒณดตถทธนบปผฝพฟภมยรฤลฦวศษสหฬอฮฯะัาำิีึืฺุู฿เแโใไๅๆ็่้๊๋์ํ๎๏๐๑๒๓๔๕๖๗๘๙๚๛', + wiki_start_pages=[u'หน้าหลัก']), + 'Turkish': Language(name='Turkish', + iso_code='tr', + # Q, W, and X are not used by Turkish + use_ascii=False, + charsets=['ISO-8859-3', 'ISO-8859-9', + 'WINDOWS-1254'], + alphabet=(u'abcçdefgğhıijklmnoöprsştuüvyzâîû' + u'ABCÇDEFGĞHIİJKLMNOÖPRSŞTUÜVYZÂÎÛ'), + wiki_start_pages=[u'Ana_Sayfa']), + 'Vietnamese': Language(name='Vietnamese', + iso_code='vi', + use_ascii=False, + # Windows-1258 is the only common 8-bit + # Vietnamese encoding supported by Python. + # From Wikipedia: + # For systems that lack support for Unicode, + # dozens of 8-bit Vietnamese code pages are + # available.[1] The most common are VISCII + # (TCVN 5712:1993), VPS, and Windows-1258.[3] + # Where ASCII is required, such as when + # ensuring readability in plain text e-mail, + # Vietnamese letters are often encoded + # according to Vietnamese Quoted-Readable + # (VIQR) or VSCII Mnemonic (VSCII-MNEM),[4] + # though usage of either variable-width + # scheme has declined dramatically following + # the adoption of Unicode on the World Wide + # Web. + charsets=['WINDOWS-1258'], + alphabet=(u'aăâbcdđeêghiklmnoôơpqrstuưvxy' + u'AĂÂBCDĐEÊGHIKLMNOÔƠPQRSTUƯVXY'), + wiki_start_pages=[u'Chữ_Quốc_ngữ']), + } diff --git a/src/pip/_vendor/chardet/sbcharsetprober.py b/src/pip/_vendor/chardet/sbcharsetprober.py index 0adb51de5a2..46ba835c66c 100644 --- a/src/pip/_vendor/chardet/sbcharsetprober.py +++ b/src/pip/_vendor/chardet/sbcharsetprober.py @@ -26,10 +26,22 @@ # 02110-1301 USA ######################### END LICENSE BLOCK ######################### +from collections import namedtuple + from .charsetprober import CharSetProber from .enums import CharacterCategory, ProbingState, SequenceLikelihood +SingleByteCharSetModel = namedtuple('SingleByteCharSetModel', + ['charset_name', + 'language', + 'char_to_order_map', + 'language_model', + 'typical_positive_ratio', + 'keep_ascii_letters', + 'alphabet']) + + class SingleByteCharSetProber(CharSetProber): SAMPLE_SIZE = 64 SB_ENOUGH_REL_THRESHOLD = 1024 # 0.25 * SAMPLE_SIZE^2 @@ -65,25 +77,25 @@ def charset_name(self): if self._name_prober: return self._name_prober.charset_name else: - return self._model['charset_name'] + return self._model.charset_name @property def language(self): if self._name_prober: return self._name_prober.language else: - return self._model.get('language') + return self._model.language def feed(self, byte_str): - if not self._model['keep_english_letter']: + # TODO: Make filter_international_words keep things in self.alphabet + if not self._model.keep_ascii_letters: byte_str = self.filter_international_words(byte_str) if not byte_str: return self.state - char_to_order_map = self._model['char_to_order_map'] - for i, c in enumerate(byte_str): - # XXX: Order is in range 1-64, so one would think we want 0-63 here, - # but that leads to 27 more test failures than before. - order = char_to_order_map[c] + char_to_order_map = self._model.char_to_order_map + language_model = self._model.language_model + for char in byte_str: + order = char_to_order_map.get(char, CharacterCategory.UNDEFINED) # XXX: This was SYMBOL_CAT_ORDER before, with a value of 250, but # CharacterCategory.SYMBOL is actually 253, so we use CONTROL # to make it closer to the original intent. The only difference @@ -91,20 +103,21 @@ def feed(self, byte_str): # _total_char purposes. if order < CharacterCategory.CONTROL: self._total_char += 1 + # TODO: Follow uchardet's lead and discount confidence for frequent + # control characters. + # See https://github.com/BYVoid/uchardet/commit/55b4f23971db61 if order < self.SAMPLE_SIZE: self._freq_char += 1 if self._last_order < self.SAMPLE_SIZE: self._total_seqs += 1 if not self._reversed: - i = (self._last_order * self.SAMPLE_SIZE) + order - model = self._model['precedence_matrix'][i] - else: # reverse the order of the letters in the lookup - i = (order * self.SAMPLE_SIZE) + self._last_order - model = self._model['precedence_matrix'][i] - self._seq_counters[model] += 1 + lm_cat = language_model[self._last_order][order] + else: + lm_cat = language_model[order][self._last_order] + self._seq_counters[lm_cat] += 1 self._last_order = order - charset_name = self._model['charset_name'] + charset_name = self._model.charset_name if self.state == ProbingState.DETECTING: if self._total_seqs > self.SB_ENOUGH_REL_THRESHOLD: confidence = self.get_confidence() @@ -125,7 +138,7 @@ def get_confidence(self): r = 0.01 if self._total_seqs > 0: r = ((1.0 * self._seq_counters[SequenceLikelihood.POSITIVE]) / - self._total_seqs / self._model['typical_positive_ratio']) + self._total_seqs / self._model.typical_positive_ratio) r = r * self._freq_char / self._total_char if r >= 1.0: r = 0.99 diff --git a/src/pip/_vendor/chardet/sbcsgroupprober.py b/src/pip/_vendor/chardet/sbcsgroupprober.py index 98e95dc1a3c..bdeef4e15b0 100644 --- a/src/pip/_vendor/chardet/sbcsgroupprober.py +++ b/src/pip/_vendor/chardet/sbcsgroupprober.py @@ -27,47 +27,57 @@ ######################### END LICENSE BLOCK ######################### from .charsetgroupprober import CharSetGroupProber -from .sbcharsetprober import SingleByteCharSetProber -from .langcyrillicmodel import (Win1251CyrillicModel, Koi8rModel, - Latin5CyrillicModel, MacCyrillicModel, - Ibm866Model, Ibm855Model) -from .langgreekmodel import Latin7GreekModel, Win1253GreekModel -from .langbulgarianmodel import Latin5BulgarianModel, Win1251BulgarianModel -# from .langhungarianmodel import Latin2HungarianModel, Win1250HungarianModel -from .langthaimodel import TIS620ThaiModel -from .langhebrewmodel import Win1255HebrewModel from .hebrewprober import HebrewProber -from .langturkishmodel import Latin5TurkishModel +from .langbulgarianmodel import (ISO_8859_5_BULGARIAN_MODEL, + WINDOWS_1251_BULGARIAN_MODEL) +from .langgreekmodel import ISO_8859_7_GREEK_MODEL, WINDOWS_1253_GREEK_MODEL +from .langhebrewmodel import WINDOWS_1255_HEBREW_MODEL +# from .langhungarianmodel import (ISO_8859_2_HUNGARIAN_MODEL, +# WINDOWS_1250_HUNGARIAN_MODEL) +from .langrussianmodel import (IBM855_RUSSIAN_MODEL, IBM866_RUSSIAN_MODEL, + ISO_8859_5_RUSSIAN_MODEL, KOI8_R_RUSSIAN_MODEL, + MACCYRILLIC_RUSSIAN_MODEL, + WINDOWS_1251_RUSSIAN_MODEL) +from .langthaimodel import TIS_620_THAI_MODEL +from .langturkishmodel import ISO_8859_9_TURKISH_MODEL +from .sbcharsetprober import SingleByteCharSetProber class SBCSGroupProber(CharSetGroupProber): def __init__(self): super(SBCSGroupProber, self).__init__() + hebrew_prober = HebrewProber() + logical_hebrew_prober = SingleByteCharSetProber(WINDOWS_1255_HEBREW_MODEL, + False, hebrew_prober) + # TODO: See if using ISO-8859-8 Hebrew model works better here, since + # it's actually the visual one + visual_hebrew_prober = SingleByteCharSetProber(WINDOWS_1255_HEBREW_MODEL, + True, hebrew_prober) + hebrew_prober.set_model_probers(logical_hebrew_prober, + visual_hebrew_prober) + # TODO: ORDER MATTERS HERE. I changed the order vs what was in master + # and several tests failed that did not before. Some thought + # should be put into the ordering, and we should consider making + # order not matter here, because that is very counter-intuitive. self.probers = [ - SingleByteCharSetProber(Win1251CyrillicModel), - SingleByteCharSetProber(Koi8rModel), - SingleByteCharSetProber(Latin5CyrillicModel), - SingleByteCharSetProber(MacCyrillicModel), - SingleByteCharSetProber(Ibm866Model), - SingleByteCharSetProber(Ibm855Model), - SingleByteCharSetProber(Latin7GreekModel), - SingleByteCharSetProber(Win1253GreekModel), - SingleByteCharSetProber(Latin5BulgarianModel), - SingleByteCharSetProber(Win1251BulgarianModel), + SingleByteCharSetProber(WINDOWS_1251_RUSSIAN_MODEL), + SingleByteCharSetProber(KOI8_R_RUSSIAN_MODEL), + SingleByteCharSetProber(ISO_8859_5_RUSSIAN_MODEL), + SingleByteCharSetProber(MACCYRILLIC_RUSSIAN_MODEL), + SingleByteCharSetProber(IBM866_RUSSIAN_MODEL), + SingleByteCharSetProber(IBM855_RUSSIAN_MODEL), + SingleByteCharSetProber(ISO_8859_7_GREEK_MODEL), + SingleByteCharSetProber(WINDOWS_1253_GREEK_MODEL), + SingleByteCharSetProber(ISO_8859_5_BULGARIAN_MODEL), + SingleByteCharSetProber(WINDOWS_1251_BULGARIAN_MODEL), # TODO: Restore Hungarian encodings (iso-8859-2 and windows-1250) # after we retrain model. - # SingleByteCharSetProber(Latin2HungarianModel), - # SingleByteCharSetProber(Win1250HungarianModel), - SingleByteCharSetProber(TIS620ThaiModel), - SingleByteCharSetProber(Latin5TurkishModel), + # SingleByteCharSetProber(ISO_8859_2_HUNGARIAN_MODEL), + # SingleByteCharSetProber(WINDOWS_1250_HUNGARIAN_MODEL), + SingleByteCharSetProber(TIS_620_THAI_MODEL), + SingleByteCharSetProber(ISO_8859_9_TURKISH_MODEL), + hebrew_prober, + logical_hebrew_prober, + visual_hebrew_prober, ] - hebrew_prober = HebrewProber() - logical_hebrew_prober = SingleByteCharSetProber(Win1255HebrewModel, - False, hebrew_prober) - visual_hebrew_prober = SingleByteCharSetProber(Win1255HebrewModel, True, - hebrew_prober) - hebrew_prober.set_model_probers(logical_hebrew_prober, visual_hebrew_prober) - self.probers.extend([hebrew_prober, logical_hebrew_prober, - visual_hebrew_prober]) - self.reset() diff --git a/src/pip/_vendor/chardet/universaldetector.py b/src/pip/_vendor/chardet/universaldetector.py index 7b4e92d6158..055a8ac1b1d 100644 --- a/src/pip/_vendor/chardet/universaldetector.py +++ b/src/pip/_vendor/chardet/universaldetector.py @@ -266,7 +266,7 @@ def close(self): 'language': max_prober.language} # Log all prober confidences if none met MINIMUM_THRESHOLD - if self.logger.getEffectiveLevel() == logging.DEBUG: + if self.logger.getEffectiveLevel() <= logging.DEBUG: if self.result['encoding'] is None: self.logger.debug('no probers hit minimum threshold') for group_prober in self._charset_probers: @@ -280,7 +280,7 @@ def close(self): prober.get_confidence()) else: self.logger.debug('%s %s confidence = %s', - prober.charset_name, - prober.language, - prober.get_confidence()) + group_prober.charset_name, + group_prober.language, + group_prober.get_confidence()) return self.result diff --git a/src/pip/_vendor/chardet/version.py b/src/pip/_vendor/chardet/version.py index bb2a34a70ea..70369b9d663 100644 --- a/src/pip/_vendor/chardet/version.py +++ b/src/pip/_vendor/chardet/version.py @@ -5,5 +5,5 @@ :author: Dan Blanchard (dan.blanchard@gmail.com) """ -__version__ = "3.0.4" +__version__ = "4.0.0" VERSION = __version__.split('.') diff --git a/src/pip/_vendor/msgpack/_version.py b/src/pip/_vendor/msgpack/_version.py index 9f55cf50dc6..1c83c8ed3d6 100644 --- a/src/pip/_vendor/msgpack/_version.py +++ b/src/pip/_vendor/msgpack/_version.py @@ -1 +1 @@ -version = (1, 0, 0) +version = (1, 0, 2) diff --git a/src/pip/_vendor/msgpack/ext.py b/src/pip/_vendor/msgpack/ext.py index 8341c68b8ab..4eb9dd65adc 100644 --- a/src/pip/_vendor/msgpack/ext.py +++ b/src/pip/_vendor/msgpack/ext.py @@ -178,7 +178,9 @@ def to_datetime(self): :rtype: datetime. """ - return datetime.datetime.fromtimestamp(self.to_unix(), _utc) + return datetime.datetime.fromtimestamp(0, _utc) + datetime.timedelta( + seconds=self.to_unix() + ) @staticmethod def from_datetime(dt): diff --git a/src/pip/_vendor/msgpack/fallback.py b/src/pip/_vendor/msgpack/fallback.py index 9f6665b3eb3..0bfa94eacb6 100644 --- a/src/pip/_vendor/msgpack/fallback.py +++ b/src/pip/_vendor/msgpack/fallback.py @@ -365,18 +365,19 @@ def _get_extradata(self): return self._buffer[self._buff_i :] def read_bytes(self, n): - ret = self._read(n) + ret = self._read(n, raise_outofdata=False) self._consume() return ret - def _read(self, n): + def _read(self, n, raise_outofdata=True): # (int) -> bytearray - self._reserve(n) + self._reserve(n, raise_outofdata=raise_outofdata) i = self._buff_i - self._buff_i = i + n - return self._buffer[i : i + n] + ret = self._buffer[i : i + n] + self._buff_i = i + len(ret) + return ret - def _reserve(self, n): + def _reserve(self, n, raise_outofdata=True): remain_bytes = len(self._buffer) - self._buff_i - n # Fast path: buffer has n bytes already @@ -404,7 +405,7 @@ def _reserve(self, n): self._buffer += read_data remain_bytes -= len(read_data) - if len(self._buffer) < n + self._buff_i: + if len(self._buffer) < n + self._buff_i and raise_outofdata: self._buff_i = 0 # rollback raise OutOfData @@ -743,7 +744,7 @@ class Packer(object): """ MessagePack Packer - Usage: + Usage:: packer = Packer() astream.write(packer.pack(a)) @@ -783,6 +784,29 @@ class Packer(object): :param str unicode_errors: The error handler for encoding unicode. (default: 'strict') DO NOT USE THIS!! This option is kept for very specific usage. + + Example of streaming deserialize from file-like object:: + + unpacker = Unpacker(file_like) + for o in unpacker: + process(o) + + Example of streaming deserialize from socket:: + + unpacker = Unpacker() + while True: + buf = sock.recv(1024**2) + if not buf: + break + unpacker.feed(buf) + for o in unpacker: + process(o) + + Raises ``ExtraData`` when *packed* contains extra bytes. + Raises ``OutOfData`` when *packed* is incomplete. + Raises ``FormatError`` when *packed* is not valid msgpack. + Raises ``StackError`` when *packed* contains too nested. + Other exceptions can be raised during unpacking. """ def __init__( @@ -920,7 +944,7 @@ def _pack( len(obj), dict_iteritems(obj), nest_limit - 1 ) - if self._datetime and check(obj, _DateTime): + if self._datetime and check(obj, _DateTime) and obj.tzinfo is not None: obj = Timestamp.from_datetime(obj) default_used = 1 continue diff --git a/src/pip/_vendor/requests/__init__.py b/src/pip/_vendor/requests/__init__.py index 4bea577a36f..18046c45138 100644 --- a/src/pip/_vendor/requests/__init__.py +++ b/src/pip/_vendor/requests/__init__.py @@ -65,10 +65,8 @@ def check_compatibility(urllib3_version, chardet_version): # Check chardet for compatibility. major, minor, patch = chardet_version.split('.')[:3] major, minor, patch = int(major), int(minor), int(patch) - # chardet >= 3.0.2, < 3.1.0 - assert major == 3 - assert minor < 1 - assert patch >= 2 + # chardet >= 3.0.2, < 5.0.0 + assert (3, 0, 2) <= (major, minor, patch) < (5, 0, 0) def _check_cryptography(cryptography_version): diff --git a/src/pip/_vendor/requests/__version__.py b/src/pip/_vendor/requests/__version__.py index 71085207750..1267488d28c 100644 --- a/src/pip/_vendor/requests/__version__.py +++ b/src/pip/_vendor/requests/__version__.py @@ -5,8 +5,8 @@ __title__ = 'requests' __description__ = 'Python HTTP for Humans.' __url__ = 'https://requests.readthedocs.io' -__version__ = '2.25.0' -__build__ = 0x022500 +__version__ = '2.25.1' +__build__ = 0x022501 __author__ = 'Kenneth Reitz' __author_email__ = 'me@kennethreitz.org' __license__ = 'Apache 2.0' diff --git a/src/pip/_vendor/requests/sessions.py b/src/pip/_vendor/requests/sessions.py index fdf7e9fe35d..45ab8a5d3f6 100644 --- a/src/pip/_vendor/requests/sessions.py +++ b/src/pip/_vendor/requests/sessions.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- """ -requests.session -~~~~~~~~~~~~~~~~ +requests.sessions +~~~~~~~~~~~~~~~~~ This module provides a Session object to manage and persist settings across requests (cookies, auth, proxies). diff --git a/src/pip/_vendor/requests/utils.py b/src/pip/_vendor/requests/utils.py index 16d5776201d..db67938e67e 100644 --- a/src/pip/_vendor/requests/utils.py +++ b/src/pip/_vendor/requests/utils.py @@ -503,6 +503,10 @@ def get_encoding_from_headers(headers): if 'text' in content_type: return 'ISO-8859-1' + if 'application/json' in content_type: + # Assume UTF-8 based on RFC 4627: https://www.ietf.org/rfc/rfc4627.txt since the charset was unset + return 'utf-8' + def stream_decode_response_unicode(iterator, r): """Stream decodes a iterator.""" From a79758c13f94e00326e1b6c8b1a83e75e6cfc4f0 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Sat, 16 Jan 2021 11:19:06 +0000 Subject: [PATCH 2885/3170] Add leading zero in certifi version --- src/pip/_vendor/vendor.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index c2f81d7f00a..1ec29804f14 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -11,7 +11,7 @@ pep517==0.9.1 progress==1.5 pyparsing==2.4.7 requests==2.25.1 - certifi==2020.12.5 + certifi==2020.12.05 chardet==4.0.0 idna==2.10 urllib3==1.26.2 From c09bc1432b4bc8ca16cb9f91400ec2260cdffec6 Mon Sep 17 00:00:00 2001 From: Noah Gorny <noah@gittabags.com> Date: Sat, 16 Jan 2021 17:59:18 +0200 Subject: [PATCH 2886/3170] commands: debug: Use packaging.version.parse to compare --- news/9461.bugfix.rst | 1 + src/pip/_internal/commands/debug.py | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) create mode 100644 news/9461.bugfix.rst diff --git a/news/9461.bugfix.rst b/news/9461.bugfix.rst new file mode 100644 index 00000000000..b1decff41fc --- /dev/null +++ b/news/9461.bugfix.rst @@ -0,0 +1 @@ +commands: debug: Use packaging.version.parse to compare between versions. diff --git a/src/pip/_internal/commands/debug.py b/src/pip/_internal/commands/debug.py index 61df18e20cd..be4f4704053 100644 --- a/src/pip/_internal/commands/debug.py +++ b/src/pip/_internal/commands/debug.py @@ -6,6 +6,7 @@ import pip._vendor from pip._vendor import pkg_resources from pip._vendor.certifi import where +from pip._vendor.packaging.version import parse as parse_version from pip import __file__ as pip_location from pip._internal.cli import cmdoptions @@ -100,7 +101,7 @@ def show_actual_vendor_versions(vendor_txt_versions): extra_message = ' (Unable to locate actual module version, using'\ ' vendor.txt specified version)' actual_version = expected_version - elif actual_version != expected_version: + elif parse_version(actual_version) != parse_version(expected_version): extra_message = ' (CONFLICT: vendor.txt suggests version should'\ ' be {})'.format(expected_version) logger.info('%s==%s%s', module_name, actual_version, extra_message) From 9361faa03ee75cbfad58d7546fb8a7196e85fa79 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Wed, 23 Dec 2020 12:26:22 +0100 Subject: [PATCH 2887/3170] Add newfragments --- news/9273.bugfix.rst | 3 +++ news/9337.bugfix.rst | 2 ++ news/9338.removal.rst | 2 ++ 3 files changed, 7 insertions(+) create mode 100644 news/9273.bugfix.rst create mode 100644 news/9337.bugfix.rst create mode 100644 news/9338.removal.rst diff --git a/news/9273.bugfix.rst b/news/9273.bugfix.rst new file mode 100644 index 00000000000..e729ea29446 --- /dev/null +++ b/news/9273.bugfix.rst @@ -0,0 +1,3 @@ +Fix a regression that made ``pip wheel`` do a VCS export instead of a VCS clone +for editable requirements. This broke VCS requirements that need the VCS +information to build correctly. diff --git a/news/9337.bugfix.rst b/news/9337.bugfix.rst new file mode 100644 index 00000000000..e9d08c3ad82 --- /dev/null +++ b/news/9337.bugfix.rst @@ -0,0 +1,2 @@ +Fix ``pip download`` of editable VCS requirements that need VCS information +to build correctly. diff --git a/news/9338.removal.rst b/news/9338.removal.rst new file mode 100644 index 00000000000..6d3b666e53f --- /dev/null +++ b/news/9338.removal.rst @@ -0,0 +1,2 @@ +Remove the VCS export feature that was used only with editable VCS +requirements and had correctness issues. From ab181811aeb8014017a0404d729d32fe1245f927 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Mon, 18 Jan 2021 05:54:04 +0800 Subject: [PATCH 2888/3170] Use str to pass versions to avoid debundling issue --- news/9348.bugfix.rst | 2 ++ src/pip/_internal/req/req_install.py | 12 ++++++++++-- 2 files changed, 12 insertions(+), 2 deletions(-) create mode 100644 news/9348.bugfix.rst diff --git a/news/9348.bugfix.rst b/news/9348.bugfix.rst new file mode 100644 index 00000000000..99e673954c9 --- /dev/null +++ b/news/9348.bugfix.rst @@ -0,0 +1,2 @@ +Avoid parsing version to make the version check more robust against lousily +debundled downstream distributions. diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 11d000df503..87b5b03f3b3 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -432,8 +432,16 @@ def check_if_exists(self, use_user_site): if not existing_dist: return - existing_version = existing_dist.parsed_version - if not self.req.specifier.contains(existing_version, prereleases=True): + # pkg_resouces may contain a different copy of packaging.version from + # pip in if the downstream distributor does a poor job debundling pip. + # We avoid existing_dist.parsed_version and let SpecifierSet.contains + # parses the version instead. + existing_version = existing_dist.version + version_compatible = ( + existing_version is not None and + self.req.specifier.contains(existing_version, prereleases=True) + ) + if not version_compatible: self.satisfied_by = None if use_user_site: if dist_in_usersite(existing_dist): From 5385d8a6cde85d28ea39ab55089cc391ea23743c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sat, 9 Jan 2021 15:48:19 +0100 Subject: [PATCH 2889/3170] Construct valid backends list for error message. --- src/pip/_internal/req/constructors.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index d02dc636b0e..5a8f0666ca2 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -116,10 +116,11 @@ def parse_editable(editable_req): break if '+' not in url: + backends = ", ".join([backend.name + '+' for backend in vcs.backends]) raise InstallationError( - '{} is not a valid editable requirement. ' - 'It should either be a path to a local project or a VCS URL ' - '(beginning with svn+, git+, hg+, or bzr+).'.format(editable_req) + f'{editable_req} is not a valid editable requirement. ' + f'It should either be a path to a local project or a VCS URL ' + f'(beginning with {backends}).' ) vc_type = url.split('+', 1)[0].lower() From b3761f6fabbd4f2aa36342c8f14ee0200dc55151 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sat, 9 Jan 2021 15:50:51 +0100 Subject: [PATCH 2890/3170] Remove support for git+git@ pseudo VCS URLs. Now that we don't need to support git@ pseudo-urls, we can simplify the test for valid VCS URLs based on link.is_vcs, which is turns is based on the URL scheme. This also means we fail earlier if a git@ pseudo URL is used. Since VCS requirements are not validated to be URLs in Requirement constructors, we can simplify update_editable. --- news/7554.removal.rst | 2 ++ src/pip/_internal/req/constructors.py | 15 +++--------- src/pip/_internal/req/req_install.py | 31 +++++------------------- src/pip/_internal/vcs/bazaar.py | 2 +- src/pip/_internal/vcs/subversion.py | 4 ++- tests/functional/test_install_vcs_git.py | 5 ++-- 6 files changed, 19 insertions(+), 40 deletions(-) create mode 100644 news/7554.removal.rst diff --git a/news/7554.removal.rst b/news/7554.removal.rst new file mode 100644 index 00000000000..d5037d5fdb9 --- /dev/null +++ b/news/7554.removal.rst @@ -0,0 +1,2 @@ +Remove support for VCS pseudo URLs editable requirements. It was emitting +deprecation warning since version 20.0. diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index 5a8f0666ca2..6c223cd7994 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -115,7 +115,9 @@ def parse_editable(editable_req): url = f'{version_control}+{url}' break - if '+' not in url: + link = Link(url) + + if not link.is_vcs: backends = ", ".join([backend.name + '+' for backend in vcs.backends]) raise InstallationError( f'{editable_req} is not a valid editable requirement. ' @@ -123,16 +125,7 @@ def parse_editable(editable_req): f'(beginning with {backends}).' ) - vc_type = url.split('+', 1)[0].lower() - - if not vcs.get_backend(vc_type): - backends = ", ".join([bends.name + '+URL' for bends in vcs.backends]) - error_message = "For --editable={}, " \ - "only {} are currently supported".format( - editable_req, backends) - raise InstallationError(error_message) - - package_name = Link(url).egg_fragment + package_name = link.egg_fragment if not package_name: raise InstallationError( "Could not detect requirement name for '{}', please specify one " diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 11d000df503..6d0aa30b418 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -625,31 +625,12 @@ def update_editable(self): if self.link.scheme == 'file': # Static paths don't get updated return - assert '+' in self.link.url, \ - "bad url: {self.link.url!r}".format(**locals()) - vc_type, url = self.link.url.split('+', 1) - vcs_backend = vcs.get_backend(vc_type) - if vcs_backend: - if not self.link.is_vcs: - reason = ( - "This form of VCS requirement is being deprecated: {}." - ).format( - self.link.url - ) - replacement = None - if self.link.url.startswith("git+git@"): - replacement = ( - "git+https://git@example.com/..., " - "git+ssh://git@example.com/..., " - "or the insecure git+git://git@example.com/..." - ) - deprecated(reason, replacement, gone_in="21.0", issue=7554) - hidden_url = hide_url(self.link.url) - vcs_backend.obtain(self.source_dir, url=hidden_url) - else: - assert 0, ( - 'Unexpected version control type (in {}): {}'.format( - self.link, vc_type)) + vcs_backend = vcs.get_backend_for_scheme(self.link.scheme) + # Editable requirements are validated in Requirement constructors. + # So here, if it's neither a path nor a valid VCS URL, it's a bug. + assert vcs_backend, f"Unsupported VCS URL {self.link.url}" + hidden_url = hide_url(self.link.url) + vcs_backend.obtain(self.source_dir, url=hidden_url) # Top-level Actions def uninstall(self, auto_confirm=False, verbose=False): diff --git a/src/pip/_internal/vcs/bazaar.py b/src/pip/_internal/vcs/bazaar.py index 3c9c8a47930..2e82134992e 100644 --- a/src/pip/_internal/vcs/bazaar.py +++ b/src/pip/_internal/vcs/bazaar.py @@ -26,7 +26,7 @@ class Bazaar(VersionControl): repo_name = 'branch' schemes = ( 'bzr', 'bzr+http', 'bzr+https', 'bzr+ssh', 'bzr+sftp', 'bzr+ftp', - 'bzr+lp', + 'bzr+lp', 'bzr+file' ) @staticmethod diff --git a/src/pip/_internal/vcs/subversion.py b/src/pip/_internal/vcs/subversion.py index f397c427e67..54d616485a6 100644 --- a/src/pip/_internal/vcs/subversion.py +++ b/src/pip/_internal/vcs/subversion.py @@ -37,7 +37,9 @@ class Subversion(VersionControl): name = 'svn' dirname = '.svn' repo_name = 'checkout' - schemes = ('svn', 'svn+ssh', 'svn+http', 'svn+https', 'svn+svn') + schemes = ( + 'svn', 'svn+ssh', 'svn+http', 'svn+https', 'svn+svn', 'svn+file' + ) @classmethod def should_add_vcs_url_prefix(cls, remote_url): diff --git a/tests/functional/test_install_vcs_git.py b/tests/functional/test_install_vcs_git.py index 4c26d8e88ac..afd6ffae055 100644 --- a/tests/functional/test_install_vcs_git.py +++ b/tests/functional/test_install_vcs_git.py @@ -1,5 +1,6 @@ import pytest +from pip._internal.utils.urls import path_to_url from tests.lib import pyversion # noqa: F401 from tests.lib import ( _change_test_package_version, @@ -454,7 +455,7 @@ def test_check_submodule_addition(script): ) install_result = script.pip( - 'install', '-e', 'git+' + module_path + '#egg=version_pkg' + 'install', '-e', 'git+' + path_to_url(module_path) + '#egg=version_pkg' ) install_result.did_create( script.venv / 'src/version-pkg/testpkg/static/testfile' @@ -467,7 +468,7 @@ def test_check_submodule_addition(script): # expect error because git may write to stderr update_result = script.pip( - 'install', '-e', 'git+' + module_path + '#egg=version_pkg', + 'install', '-e', 'git+' + path_to_url(module_path) + '#egg=version_pkg', '--upgrade', ) From 28bbf898ea6633ffc9e3c9a7d228406bb7b5d9f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@acsone.eu> Date: Wed, 13 Jan 2021 23:33:11 +0100 Subject: [PATCH 2891/3170] Improve syntax Co-authored-by: Xavier Fernandez <xav.fernandez@gmail.com> --- src/pip/_internal/req/constructors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index 6c223cd7994..86c50b176dd 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -118,7 +118,7 @@ def parse_editable(editable_req): link = Link(url) if not link.is_vcs: - backends = ", ".join([backend.name + '+' for backend in vcs.backends]) + backends = ", ".join(f"{backend.name}+" for backend in vcs.backends) raise InstallationError( f'{editable_req} is not a valid editable requirement. ' f'It should either be a path to a local project or a VCS URL ' From 78b891a6d1dae4e76bc106e8ef1217f07ef85dd9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sun, 17 Jan 2021 14:22:07 +0100 Subject: [PATCH 2892/3170] Remove unused VCS schemes These schemes without + are support in an ad-hoc fashion in parse_editable_requirement. --- src/pip/_internal/vcs/bazaar.py | 2 +- src/pip/_internal/vcs/git.py | 2 +- src/pip/_internal/vcs/mercurial.py | 2 +- src/pip/_internal/vcs/subversion.py | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/vcs/bazaar.py b/src/pip/_internal/vcs/bazaar.py index 2e82134992e..383f73a6d22 100644 --- a/src/pip/_internal/vcs/bazaar.py +++ b/src/pip/_internal/vcs/bazaar.py @@ -25,7 +25,7 @@ class Bazaar(VersionControl): dirname = '.bzr' repo_name = 'branch' schemes = ( - 'bzr', 'bzr+http', 'bzr+https', 'bzr+ssh', 'bzr+sftp', 'bzr+ftp', + 'bzr+http', 'bzr+https', 'bzr+ssh', 'bzr+sftp', 'bzr+ftp', 'bzr+lp', 'bzr+file' ) diff --git a/src/pip/_internal/vcs/git.py b/src/pip/_internal/vcs/git.py index a2a1b5f0394..cc22cd75685 100644 --- a/src/pip/_internal/vcs/git.py +++ b/src/pip/_internal/vcs/git.py @@ -47,7 +47,7 @@ class Git(VersionControl): dirname = '.git' repo_name = 'clone' schemes = ( - 'git', 'git+http', 'git+https', 'git+ssh', 'git+git', 'git+file', + '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 diff --git a/src/pip/_internal/vcs/mercurial.py b/src/pip/_internal/vcs/mercurial.py index 640697550b1..ae7dd7b7089 100644 --- a/src/pip/_internal/vcs/mercurial.py +++ b/src/pip/_internal/vcs/mercurial.py @@ -30,7 +30,7 @@ class Mercurial(VersionControl): dirname = '.hg' repo_name = 'clone' schemes = ( - 'hg', 'hg+file', 'hg+http', 'hg+https', 'hg+ssh', 'hg+static-http', + 'hg+file', 'hg+http', 'hg+https', 'hg+ssh', 'hg+static-http', ) @staticmethod diff --git a/src/pip/_internal/vcs/subversion.py b/src/pip/_internal/vcs/subversion.py index 54d616485a6..56e8d4b4844 100644 --- a/src/pip/_internal/vcs/subversion.py +++ b/src/pip/_internal/vcs/subversion.py @@ -38,7 +38,7 @@ class Subversion(VersionControl): dirname = '.svn' repo_name = 'checkout' schemes = ( - 'svn', 'svn+ssh', 'svn+http', 'svn+https', 'svn+svn', 'svn+file' + 'svn+ssh', 'svn+http', 'svn+https', 'svn+svn', 'svn+file' ) @classmethod From 7ee22ba9957d436ff0759273deb85e155df86e60 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sun, 17 Jan 2021 14:31:01 +0100 Subject: [PATCH 2893/3170] Show full list of supported VCS scheme in error message When an editable requirement is neither a local directory nor a URL with a supported VCS scheme, we show the full list of supported VCS schemes in the error message. --- src/pip/_internal/req/constructors.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index 86c50b176dd..172b4cb0335 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -118,7 +118,7 @@ def parse_editable(editable_req): link = Link(url) if not link.is_vcs: - backends = ", ".join(f"{backend.name}+" for backend in vcs.backends) + backends = ", ".join(vcs.all_schemes) raise InstallationError( f'{editable_req} is not a valid editable requirement. ' f'It should either be a path to a local project or a VCS URL ' From 6c0e484703a2c6991e7807841d586e64b526ef00 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Mon, 18 Jan 2021 15:54:43 +0800 Subject: [PATCH 2894/3170] Include both sources in inconsistency error --- news/9186.feature.rst | 3 +++ src/pip/_internal/exceptions.py | 15 +++++++------ .../resolution/resolvelib/candidates.py | 21 +++++++++++++------ tests/functional/test_install.py | 2 +- tests/functional/test_new_resolver.py | 4 +++- 5 files changed, 31 insertions(+), 14 deletions(-) create mode 100644 news/9186.feature.rst diff --git a/news/9186.feature.rst b/news/9186.feature.rst new file mode 100644 index 00000000000..175b5a883ac --- /dev/null +++ b/news/9186.feature.rst @@ -0,0 +1,3 @@ +New resolver: Error message shown when a wheel contains inconsistent metadata +is made more helpful by including both values from the file name and internal +metadata. diff --git a/src/pip/_internal/exceptions.py b/src/pip/_internal/exceptions.py index 43d083205a0..891f8c37654 100644 --- a/src/pip/_internal/exceptions.py +++ b/src/pip/_internal/exceptions.py @@ -7,7 +7,7 @@ if MYPY_CHECK_RUNNING: import configparser from hashlib import _Hash - from typing import Any, Dict, List, Optional + from typing import Dict, List, Optional from pip._vendor.pkg_resources import Distribution from pip._vendor.requests.models import Request, Response @@ -123,17 +123,20 @@ class MetadataInconsistent(InstallationError): that do not match the information previously obtained from sdist filename or user-supplied ``#egg=`` value. """ - def __init__(self, ireq, field, built): - # type: (InstallRequirement, str, Any) -> None + def __init__(self, ireq, field, f_val, m_val): + # type: (InstallRequirement, str, str, str) -> None self.ireq = ireq self.field = field - self.built = built + self.f_val = f_val + self.m_val = m_val def __str__(self): # type: () -> str - return "Requested {} has different {} in metadata: {!r}".format( - self.ireq, self.field, self.built, + template = ( + "Requested {} has inconsistent {}: " + "filename has {!r}, but metadata has {!r}" ) + return template.format(self.ireq, self.field, self.f_val, self.m_val) class InstallationSubprocessError(InstallationError): diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 6725684a515..91662b326b7 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -204,12 +204,21 @@ def _prepare_distribution(self): def _check_metadata_consistency(self, dist): # type: (Distribution) -> None """Check for consistency of project name and version of dist.""" - name = canonicalize_name(dist.project_name) - if self._name is not None and self._name != name: - raise MetadataInconsistent(self._ireq, "name", dist.project_name) - version = dist.parsed_version - if self._version is not None and self._version != version: - raise MetadataInconsistent(self._ireq, "version", dist.version) + canonical_name = canonicalize_name(dist.project_name) + if self._name is not None and self._name != canonical_name: + raise MetadataInconsistent( + self._ireq, + "name", + self._name, + dist.project_name, + ) + if self._version is not None and self._version != dist.parsed_version: + raise MetadataInconsistent( + self._ireq, + "version", + str(self._version), + dist.version, + ) def _prepare(self): # type: () -> Distribution diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 9c36fef0ecb..1c0650c6f5f 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1455,7 +1455,7 @@ def test_install_editable_with_wrong_egg_name(script, resolver_variant): "for project name pkga. Fix your #egg=pkgb " "fragments.") in result.stderr if resolver_variant == "2020-resolver": - assert "has different name in metadata" in result.stderr, str(result) + assert "has inconsistent" in result.stderr, str(result) else: assert "Successfully installed pkga" in str(result), str(result) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index f3850c208c6..95422d22627 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -1233,7 +1233,9 @@ def test_new_resolver_skip_inconsistent_metadata(script): allow_stderr_warning=True, ) - assert " different version in metadata: '2'" in result.stderr, str(result) + assert ( + " inconsistent version: filename has '3', but metadata has '2'" + ) in result.stderr, str(result) assert_installed(script, a="1") From a08c4be3f8018beea2bb34120e91e5c70cef22db Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 23 Apr 2020 12:01:05 +0800 Subject: [PATCH 2895/3170] Basic abstraction and one migration --- src/pip/_internal/metadata/__init__.py | 15 +++++++ src/pip/_internal/metadata/base.py | 29 +++++++++++++ src/pip/_internal/metadata/pkg_resources.py | 46 +++++++++++++++++++++ src/pip/_internal/self_outdated_check.py | 10 ++--- tests/unit/test_self_check_outdated.py | 12 +++++- 5 files changed, 104 insertions(+), 8 deletions(-) create mode 100644 src/pip/_internal/metadata/__init__.py create mode 100644 src/pip/_internal/metadata/base.py create mode 100644 src/pip/_internal/metadata/pkg_resources.py diff --git a/src/pip/_internal/metadata/__init__.py b/src/pip/_internal/metadata/__init__.py new file mode 100644 index 00000000000..84e91d6aebc --- /dev/null +++ b/src/pip/_internal/metadata/__init__.py @@ -0,0 +1,15 @@ +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import List, Optional + + from .base import BaseEnvironment + + +def get_environment(paths=None): + # type: (Optional[List[str]]) -> BaseEnvironment + from .pkg_resources import Environment + + if paths is None: + return Environment.default() + return Environment.from_paths(paths) diff --git a/src/pip/_internal/metadata/base.py b/src/pip/_internal/metadata/base.py new file mode 100644 index 00000000000..6d387220fab --- /dev/null +++ b/src/pip/_internal/metadata/base.py @@ -0,0 +1,29 @@ +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import List, Optional + + +class BaseDistribution: + @property + def installer(self): + # type: () -> str + raise NotImplementedError() + + +class BaseEnvironment: + """An environment containing distributions to introspect.""" + + @classmethod + def default(cls): + # type: () -> BaseEnvironment + raise NotImplementedError() + + @classmethod + def from_paths(cls, paths): + # type: (List[str]) -> BaseEnvironment + raise NotImplementedError() + + def get_distribution(self, name): + # type: (str) -> Optional[BaseDistribution] + raise NotImplementedError() diff --git a/src/pip/_internal/metadata/pkg_resources.py b/src/pip/_internal/metadata/pkg_resources.py new file mode 100644 index 00000000000..02c32876a0f --- /dev/null +++ b/src/pip/_internal/metadata/pkg_resources.py @@ -0,0 +1,46 @@ +from pip._vendor import pkg_resources + +from pip._internal.utils.packaging import get_installer +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +from .base import BaseDistribution, BaseEnvironment + +if MYPY_CHECK_RUNNING: + from typing import List, Optional + + +class Distribution(BaseDistribution): + def __init__(self, dist): + # type: (pkg_resources.Distribution) -> None + self._dist = dist + + @property + def installer(self): + # type: () -> str + # TODO: Move get_installer() implementation here. + return get_installer(self._dist) + + +class Environment(BaseEnvironment): + def __init__(self, ws): + # type: (pkg_resources.WorkingSet) -> None + self._ws = ws + + @classmethod + def default(cls): + # type: () -> BaseEnvironment + return cls(pkg_resources.working_set) + + @classmethod + def from_paths(cls, paths): + # type: (List[str]) -> BaseEnvironment + return cls(pkg_resources.WorkingSet(paths)) + + def get_distribution(self, name): + # type: (str) -> Optional[BaseDistribution] + req = pkg_resources.Requirement(name) + try: + dist = self._ws.find(req) + except pkg_resources.DistributionNotFound: + return None + return Distribution(dist) diff --git a/src/pip/_internal/self_outdated_check.py b/src/pip/_internal/self_outdated_check.py index c22f06afe87..1819886591c 100644 --- a/src/pip/_internal/self_outdated_check.py +++ b/src/pip/_internal/self_outdated_check.py @@ -10,10 +10,10 @@ from pip._internal.index.collector import LinkCollector from pip._internal.index.package_finder import PackageFinder +from pip._internal.metadata import get_environment from pip._internal.models.selection_prefs import SelectionPreferences from pip._internal.utils.filesystem import adjacent_tmp_file, check_path_owner, replace -from pip._internal.utils.misc import ensure_dir, get_distribution, get_installed_version -from pip._internal.utils.packaging import get_installer +from pip._internal.utils.misc import ensure_dir, get_installed_version from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: @@ -103,10 +103,8 @@ def was_installed_by_pip(pkg): This is used not to display the upgrade message when pip is in fact installed by system package manager, such as dnf on Fedora. """ - dist = get_distribution(pkg) - if not dist: - return False - return "pip" == get_installer(dist) + dist = get_environment().get_distribution(pkg) + return dist is not None and "pip" == dist.installer def pip_self_version_check(session, options): diff --git a/tests/unit/test_self_check_outdated.py b/tests/unit/test_self_check_outdated.py index 42c4c452726..35a94a36b0f 100644 --- a/tests/unit/test_self_check_outdated.py +++ b/tests/unit/test_self_check_outdated.py @@ -57,6 +57,14 @@ def get_metadata_lines(self, name): raise NotImplementedError('nope') +class MockEnvironment(object): + def __init__(self, installer): + self.installer = installer + + def get_distribution(self, name): + return MockDistribution(self.installer) + + def _options(): ''' Some default options that we pass to self_outdated_check.pip_self_version_check ''' @@ -97,8 +105,8 @@ def test_pip_self_version_check(monkeypatch, stored_time, installed_ver, pretend.call_recorder(lambda *a, **kw: None)) monkeypatch.setattr(logger, 'debug', pretend.call_recorder(lambda s, exc_info=None: None)) - monkeypatch.setattr(self_outdated_check, 'get_distribution', - lambda name: MockDistribution(installer)) + monkeypatch.setattr(self_outdated_check, 'get_environment', + lambda: MockEnvironment(installer)) fake_state = pretend.stub( state={"last_check": stored_time, 'pypi_version': installed_ver}, From 7e3610461d5966d5e676dd7dc6d9071af6c30e09 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 23 Apr 2020 12:40:31 +0800 Subject: [PATCH 2896/3170] Some more yet unused implementation --- src/pip/_internal/metadata/base.py | 47 ++++++++++++++++++++- src/pip/_internal/metadata/pkg_resources.py | 30 ++++++++++++- 2 files changed, 74 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/metadata/base.py b/src/pip/_internal/metadata/base.py index 6d387220fab..312a900b00a 100644 --- a/src/pip/_internal/metadata/base.py +++ b/src/pip/_internal/metadata/base.py @@ -1,15 +1,36 @@ +from pip._internal.utils.misc import stdlib_pkgs # TODO: Move definition here. from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import List, Optional + from typing import Container, Iterator, List, Optional class BaseDistribution: + @property + def canonical_name(self): + # type: () -> str + raise NotImplementedError() + @property def installer(self): # type: () -> str raise NotImplementedError() + @property + def editable(self): + # type: () -> bool + raise NotImplementedError() + + @property + def local(self): + # type: () -> bool + raise NotImplementedError() + + @property + def in_usersite(self): + # type: () -> bool + raise NotImplementedError() + class BaseEnvironment: """An environment containing distributions to introspect.""" @@ -27,3 +48,27 @@ def from_paths(cls, paths): def get_distribution(self, name): # type: (str) -> Optional[BaseDistribution] raise NotImplementedError() + + def iter_distributions(self): + # type: () -> Iterator[BaseDistribution] + raise NotImplementedError() + + def iter_installed_distributions( + self, + local_only=True, # type: bool + skip=stdlib_pkgs, # type: Container[str] + include_editables=True, # type: bool + editables_only=False, # type: bool + user_only=False, # type: bool + ): + # type: (...) -> Iterator[BaseDistribution] + it = self.iter_distributions() + if local_only: + it = (d for d in it if d.local) + if not include_editables: + it = (d for d in it if not d.editable) + if editables_only: + it = (d for d in it if d.editable) + if user_only: + it = (d for d in it if d.in_usersite) + return (d for d in it if d.canonical_name not in skip) diff --git a/src/pip/_internal/metadata/pkg_resources.py b/src/pip/_internal/metadata/pkg_resources.py index 02c32876a0f..5462fecde87 100644 --- a/src/pip/_internal/metadata/pkg_resources.py +++ b/src/pip/_internal/metadata/pkg_resources.py @@ -1,12 +1,14 @@ from pip._vendor import pkg_resources +from pip._vendor.packaging.utils import canonicalize_name +from pip._internal.utils.misc import dist_in_usersite, dist_is_editable, dist_is_local from pip._internal.utils.packaging import get_installer from pip._internal.utils.typing import MYPY_CHECK_RUNNING from .base import BaseDistribution, BaseEnvironment if MYPY_CHECK_RUNNING: - from typing import List, Optional + from typing import Iterator, List, Optional class Distribution(BaseDistribution): @@ -14,12 +16,31 @@ def __init__(self, dist): # type: (pkg_resources.Distribution) -> None self._dist = dist + @property + def canonical_name(self): + # type: () -> str + return canonicalize_name(self._dist.project_name) + @property def installer(self): # type: () -> str - # TODO: Move get_installer() implementation here. return get_installer(self._dist) + @property + def editable(self): + # type: () -> bool + return dist_is_editable(self._dist) + + @property + def local(self): + # type: () -> bool + return dist_is_local(self._dist) + + @property + def in_usersite(self): + # type: () -> bool + return dist_in_usersite(self._dist) + class Environment(BaseEnvironment): def __init__(self, ws): @@ -44,3 +65,8 @@ def get_distribution(self, name): except pkg_resources.DistributionNotFound: return None return Distribution(dist) + + def iter_distributions(self): + # type: () -> Iterator[BaseDistribution] + for dist in self._ws: + yield Distribution(dist) From 349bb730ded98f906875c928c7e3d9902fca7d09 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 19 Jan 2021 04:19:32 +0800 Subject: [PATCH 2897/3170] Move dist-related logic from misc into metadata --- src/pip/_internal/metadata/base.py | 13 +++ src/pip/_internal/metadata/pkg_resources.py | 42 +++++-- src/pip/_internal/utils/misc.py | 120 ++++---------------- tests/unit/test_utils.py | 28 +++-- 4 files changed, 85 insertions(+), 118 deletions(-) diff --git a/src/pip/_internal/metadata/base.py b/src/pip/_internal/metadata/base.py index 312a900b00a..0d4de00764d 100644 --- a/src/pip/_internal/metadata/base.py +++ b/src/pip/_internal/metadata/base.py @@ -47,10 +47,12 @@ def from_paths(cls, paths): def get_distribution(self, name): # type: (str) -> Optional[BaseDistribution] + """Given a requirement name, return the installed distributions.""" raise NotImplementedError() def iter_distributions(self): # type: () -> Iterator[BaseDistribution] + """Iterate through installed distributions.""" raise NotImplementedError() def iter_installed_distributions( @@ -62,6 +64,17 @@ def iter_installed_distributions( user_only=False, # type: bool ): # type: (...) -> Iterator[BaseDistribution] + """Return a list of installed distributions. + + :param local_only: If True (default), only return installations + local to the current virtualenv, if in a virtualenv. + :param skip: An iterable of canonicalized project names to ignore; + defaults to ``stdlib_pkgs``. + :param include_editables: If False, don't report editables. + :param editables_only: If True, only report editables. + :param user_only: If True, only report installations in the user + site directory. + """ it = self.iter_distributions() if local_only: it = (d for d in it if d.local) diff --git a/src/pip/_internal/metadata/pkg_resources.py b/src/pip/_internal/metadata/pkg_resources.py index 5462fecde87..1e73b79f40e 100644 --- a/src/pip/_internal/metadata/pkg_resources.py +++ b/src/pip/_internal/metadata/pkg_resources.py @@ -1,7 +1,7 @@ from pip._vendor import pkg_resources from pip._vendor.packaging.utils import canonicalize_name -from pip._internal.utils.misc import dist_in_usersite, dist_is_editable, dist_is_local +from pip._internal.utils import misc # TODO: Move definition here. from pip._internal.utils.packaging import get_installer from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -29,17 +29,17 @@ def installer(self): @property def editable(self): # type: () -> bool - return dist_is_editable(self._dist) + return misc.dist_is_editable(self._dist) @property def local(self): # type: () -> bool - return dist_is_local(self._dist) + return misc.dist_is_local(self._dist) @property def in_usersite(self): # type: () -> bool - return dist_in_usersite(self._dist) + return misc.dist_in_usersite(self._dist) class Environment(BaseEnvironment): @@ -57,14 +57,42 @@ def from_paths(cls, paths): # type: (List[str]) -> BaseEnvironment return cls(pkg_resources.WorkingSet(paths)) + def _search_distribution(self, name): + # type: (str) -> Optional[BaseDistribution] + """Find a distribution matching the ``name`` in the environment. + + This searches from *all* distributions available in the environment, to + match the behavior of ``pkg_resources.get_distribution()``. + """ + canonical_name = canonicalize_name(name) + for dist in self.iter_distributions(): + if dist.canonical_name == canonical_name: + return dist + return None + def get_distribution(self, name): # type: (str) -> Optional[BaseDistribution] - req = pkg_resources.Requirement(name) + + # Search the distribution by looking through the working set. + dist = self._search_distribution(name) + if dist: + return dist + + # If distribution could not be found, call working_set.require to + # update the working set, and try to find the distribution again. + # This might happen for e.g. when you install a package twice, once + # using setup.py develop and again using setup.py install. Now when + # running pip uninstall twice, the package gets removed from the + # working set in the first uninstall, so we have to populate the + # working set again so that pip knows about it and the packages gets + # picked up and is successfully uninstalled the second time too. try: - dist = self._ws.find(req) + # We didn't pass in any version specifiers, so this can never + # raise pkg_resources.VersionConflict. + self._ws.require(name) except pkg_resources.DistributionNotFound: return None - return Distribution(dist) + return self._search_distribution(name) def iter_distributions(self): # type: () -> Iterator[BaseDistribution] diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index a3bd49b9139..0cee9156574 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -18,7 +18,6 @@ from itertools import filterfalse, tee, zip_longest from pip._vendor import pkg_resources -from pip._vendor.packaging.utils import canonicalize_name # NOTE: retrying is not annotated in typeshed as on 2017-07-17, which is # why we ignore the type on this import. @@ -402,86 +401,20 @@ def get_installed_distributions( paths=None # type: Optional[List[str]] ): # type: (...) -> List[Distribution] - """ - Return a list of installed Distribution objects. - - If ``local_only`` is True (default), only return installations - local to the current virtualenv, if in a virtualenv. - - ``skip`` argument is an iterable of lower-case project names to - ignore; defaults to stdlib_pkgs - - If ``include_editables`` is False, don't report editables. - - If ``editables_only`` is True , only report editables. - - If ``user_only`` is True , only report installations in the user - site directory. - - If ``paths`` is set, only report the distributions present at the - specified list of locations. - """ - if paths: - working_set = pkg_resources.WorkingSet(paths) - else: - working_set = pkg_resources.working_set - - if local_only: - local_test = dist_is_local - else: - def local_test(d): - return True - - if include_editables: - def editable_test(d): - return True - else: - def editable_test(d): - return not dist_is_editable(d) - - if editables_only: - def editables_only_test(d): - return dist_is_editable(d) - else: - def editables_only_test(d): - return True - - if user_only: - user_test = dist_in_usersite - else: - def user_test(d): - return True - - return [d for d in working_set - if local_test(d) and - d.key not in skip and - editable_test(d) and - editables_only_test(d) and - user_test(d) - ] - - -def _search_distribution(req_name): - # type: (str) -> Optional[Distribution] - """Find a distribution matching the ``req_name`` in the environment. - - This searches from *all* distributions available in the environment, to - match the behavior of ``pkg_resources.get_distribution()``. - """ - # Canonicalize the name before searching in the list of - # installed distributions and also while creating the package - # dictionary to get the Distribution object - req_name = canonicalize_name(req_name) - packages = get_installed_distributions( - local_only=False, - skip=(), - include_editables=True, - editables_only=False, - user_only=False, - paths=None, + """Return a list of installed Distribution objects. + + Left for compatibility until direct pkg_resources uses are refactored out. + """ + from pip._internal.metadata import get_environment + from pip._internal.metadata.pkg_resources import Distribution as _Dist + dists = get_environment(paths).iter_installed_distributions( + local_only=local_only, + skip=skip, + include_editables=include_editables, + editables_only=editables_only, + user_only=user_only, ) - pkg_dict = {canonicalize_name(p.key): p for p in packages} - return pkg_dict.get(req_name) + return [cast(_Dist, dist)._dist for dist in dists] def get_distribution(req_name): @@ -490,26 +423,15 @@ def get_distribution(req_name): This searches from *all* distributions available in the environment, to match the behavior of ``pkg_resources.get_distribution()``. - """ - # Search the distribution by looking through the working set - dist = _search_distribution(req_name) - - # If distribution could not be found, call working_set.require - # to update the working set, and try to find the distribution - # again. - # This might happen for e.g. when you install a package - # twice, once using setup.py develop and again using setup.py install. - # Now when run pip uninstall twice, the package gets removed - # from the working set in the first uninstall, so we have to populate - # the working set again so that pip knows about it and the packages - # gets picked up and is successfully uninstalled the second time too. - if not dist: - try: - pkg_resources.working_set.require(req_name) - except pkg_resources.DistributionNotFound: - return None - return _search_distribution(req_name) + Left for compatibility until direct pkg_resources uses are refactored out. + """ + from pip._internal.metadata import get_environment + from pip._internal.metadata.pkg_resources import Distribution as _Dist + dist = get_environment().get_distribution(req_name) + if dist is None: + return None + return cast(_Dist, dist)._dist def egg_link_path(dist): diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 9c43d553143..4caf4cc754b 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -196,21 +196,21 @@ def require(self, name): pass workingset = MockWorkingSet(( - Mock(test_name="global", key="global"), - Mock(test_name="editable", key="editable"), - Mock(test_name="normal", key="normal"), - Mock(test_name="user", key="user"), + Mock(test_name="global", project_name="global"), + Mock(test_name="editable", project_name="editable"), + Mock(test_name="normal", project_name="normal"), + Mock(test_name="user", project_name="user"), )) workingset_stdlib = MockWorkingSet(( - Mock(test_name='normal', key='argparse'), - Mock(test_name='normal', key='wsgiref') + Mock(test_name='normal', project_name='argparse'), + Mock(test_name='normal', project_name='wsgiref') )) workingset_freeze = MockWorkingSet(( - Mock(test_name='normal', key='pip'), - Mock(test_name='normal', key='setuptools'), - Mock(test_name='normal', key='distribute') + Mock(test_name='normal', project_name='pip'), + Mock(test_name='normal', project_name='setuptools'), + Mock(test_name='normal', project_name='distribute') )) def dist_is_editable(self, dist): @@ -290,9 +290,13 @@ def test_freeze_excludes(self, mock_dist_is_editable, @pytest.mark.parametrize( "working_set, req_name", itertools.chain( - itertools.product([workingset], (d.key for d in workingset)), itertools.product( - [workingset_stdlib], (d.key for d in workingset_stdlib), + [workingset], + (d.project_name for d in workingset), + ), + itertools.product( + [workingset_stdlib], + (d.project_name for d in workingset_stdlib), ), ), ) @@ -312,7 +316,7 @@ def test_get_distribution( with patch("pip._vendor.pkg_resources.working_set", working_set): dist = get_distribution(req_name) assert dist is not None - assert dist.key == req_name + assert dist.project_name == req_name @patch('pip._vendor.pkg_resources.working_set', workingset) def test_get_distribution_nonexist( From 11e37aa6e1b3afd6a8235327b64122b8398232a1 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 19 Jan 2021 08:13:57 +0800 Subject: [PATCH 2898/3170] Separate default and ad-hoc environment APIs pkg_resources performs annoying caching that needs to be worked around in some parts of pip. This makes it easier to represent the difference between environments backend by WorkingSet() and working_set. --- src/pip/_internal/metadata/__init__.py | 23 ++++++++++++++++++--- src/pip/_internal/metadata/base.py | 2 +- src/pip/_internal/metadata/pkg_resources.py | 2 +- src/pip/_internal/self_outdated_check.py | 4 ++-- src/pip/_internal/utils/misc.py | 13 ++++++++---- tests/unit/test_self_check_outdated.py | 2 +- 6 files changed, 34 insertions(+), 12 deletions(-) diff --git a/src/pip/_internal/metadata/__init__.py b/src/pip/_internal/metadata/__init__.py index 84e91d6aebc..da2c4355dfc 100644 --- a/src/pip/_internal/metadata/__init__.py +++ b/src/pip/_internal/metadata/__init__.py @@ -6,10 +6,27 @@ from .base import BaseEnvironment -def get_environment(paths=None): +def get_default_environment(): + # type: () -> BaseEnvironment + """Get the default representation for the current environment. + + This returns an Environment instance from the chosen backend. The default + Environment instance should be built from ``sys.path`` and may use caching + to share instance state accorss calls. + """ + from .pkg_resources import Environment + + return Environment.default() + + +def get_environment(paths): # type: (Optional[List[str]]) -> BaseEnvironment + """Get a representation of the environment specified by ``paths``. + + This returns an Environment instance from the chosen backend based on the + given import paths. The backend must build a fresh instance representing + the state of installed distributions when this function is called. + """ from .pkg_resources import Environment - if paths is None: - return Environment.default() return Environment.from_paths(paths) diff --git a/src/pip/_internal/metadata/base.py b/src/pip/_internal/metadata/base.py index 0d4de00764d..955682545cc 100644 --- a/src/pip/_internal/metadata/base.py +++ b/src/pip/_internal/metadata/base.py @@ -42,7 +42,7 @@ def default(cls): @classmethod def from_paths(cls, paths): - # type: (List[str]) -> BaseEnvironment + # type: (Optional[List[str]]) -> BaseEnvironment raise NotImplementedError() def get_distribution(self, name): diff --git a/src/pip/_internal/metadata/pkg_resources.py b/src/pip/_internal/metadata/pkg_resources.py index 1e73b79f40e..d9db2955159 100644 --- a/src/pip/_internal/metadata/pkg_resources.py +++ b/src/pip/_internal/metadata/pkg_resources.py @@ -54,7 +54,7 @@ def default(cls): @classmethod def from_paths(cls, paths): - # type: (List[str]) -> BaseEnvironment + # type: (Optional[List[str]]) -> BaseEnvironment return cls(pkg_resources.WorkingSet(paths)) def _search_distribution(self, name): diff --git a/src/pip/_internal/self_outdated_check.py b/src/pip/_internal/self_outdated_check.py index 1819886591c..e8c8282cbf9 100644 --- a/src/pip/_internal/self_outdated_check.py +++ b/src/pip/_internal/self_outdated_check.py @@ -10,7 +10,7 @@ from pip._internal.index.collector import LinkCollector from pip._internal.index.package_finder import PackageFinder -from pip._internal.metadata import get_environment +from pip._internal.metadata import get_default_environment from pip._internal.models.selection_prefs import SelectionPreferences from pip._internal.utils.filesystem import adjacent_tmp_file, check_path_owner, replace from pip._internal.utils.misc import ensure_dir, get_installed_version @@ -103,7 +103,7 @@ def was_installed_by_pip(pkg): This is used not to display the upgrade message when pip is in fact installed by system package manager, such as dnf on Fedora. """ - dist = get_environment().get_distribution(pkg) + dist = get_default_environment().get_distribution(pkg) return dist is not None and "pip" == dist.installer diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 0cee9156574..5214e9dd79b 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -405,9 +405,14 @@ def get_installed_distributions( Left for compatibility until direct pkg_resources uses are refactored out. """ - from pip._internal.metadata import get_environment + from pip._internal.metadata import get_default_environment, get_environment from pip._internal.metadata.pkg_resources import Distribution as _Dist - dists = get_environment(paths).iter_installed_distributions( + + if paths is None: + env = get_default_environment() + else: + env = get_environment(paths) + dists = env.iter_installed_distributions( local_only=local_only, skip=skip, include_editables=include_editables, @@ -426,9 +431,9 @@ def get_distribution(req_name): Left for compatibility until direct pkg_resources uses are refactored out. """ - from pip._internal.metadata import get_environment + from pip._internal.metadata import get_default_environment from pip._internal.metadata.pkg_resources import Distribution as _Dist - dist = get_environment().get_distribution(req_name) + dist = get_default_environment().get_distribution(req_name) if dist is None: return None return cast(_Dist, dist)._dist diff --git a/tests/unit/test_self_check_outdated.py b/tests/unit/test_self_check_outdated.py index 35a94a36b0f..2e8663a9c01 100644 --- a/tests/unit/test_self_check_outdated.py +++ b/tests/unit/test_self_check_outdated.py @@ -105,7 +105,7 @@ def test_pip_self_version_check(monkeypatch, stored_time, installed_ver, pretend.call_recorder(lambda *a, **kw: None)) monkeypatch.setattr(logger, 'debug', pretend.call_recorder(lambda s, exc_info=None: None)) - monkeypatch.setattr(self_outdated_check, 'get_environment', + monkeypatch.setattr(self_outdated_check, 'get_default_environment', lambda: MockEnvironment(installer)) fake_state = pretend.stub( From e19c0951014a948a2fddd173549c06db7d875abd Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller <bbodenmiller@gmail.com> Date: Tue, 19 Jan 2021 16:33:30 -0800 Subject: [PATCH 2899/3170] Clarify that --cert replaces default CA bundle --- src/pip/_internal/cli/cmdoptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index e16f42de610..a6c444a58fc 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -302,7 +302,7 @@ def exists_action(): dest='cert', type='path', metavar='path', - help="Path to alternate CA bundle.", + help="Path to CA bundle. Replaces default CA bundle.", ) # type: Callable[..., Option] client_cert = partial( From f2cb6ad5c18b3cdbd4e93ea1adb9e26641431d8d Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller <bbodenmiller@gmail.com> Date: Tue, 19 Jan 2021 16:55:06 -0800 Subject: [PATCH 2900/3170] Improve --cert help text --- src/pip/_internal/cli/cmdoptions.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index a6c444a58fc..0a280bc0755 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -302,7 +302,7 @@ def exists_action(): dest='cert', type='path', metavar='path', - help="Path to CA bundle. Replaces default CA bundle.", + help="Path to PEM-encoded CA certificate bundle. If provided, overrides the default.", ) # type: Callable[..., Option] client_cert = partial( From 202bdfad64fc61353228c821c9deb5bbc697ab3c Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller <bbodenmiller@gmail.com> Date: Tue, 19 Jan 2021 17:52:24 -0800 Subject: [PATCH 2901/3170] Multiple lines for help text --- src/pip/_internal/cli/cmdoptions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index 0a280bc0755..3ad7f1d177b 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -302,7 +302,8 @@ def exists_action(): dest='cert', type='path', metavar='path', - help="Path to PEM-encoded CA certificate bundle. If provided, overrides the default.", + help="Path to PEM-encoded CA certificate bundle. " + "If provided, overrides the default.", ) # type: Callable[..., Option] client_cert = partial( From 98b3d19e5345c37f17ab67a714ec31abd8f5b668 Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller <bbodenmiller@gmail.com> Date: Tue, 19 Jan 2021 17:54:22 -0800 Subject: [PATCH 2902/3170] Improve SSL Certificate Verification details --- docs/html/reference/pip_install.rst | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index 1b53513266d..9eedafd78d1 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -561,8 +561,14 @@ See the :ref:`pip install Examples<pip install Examples>`. SSL Certificate Verification ---------------------------- -Starting with v1.3, pip provides SSL certificate verification over https, to -prevent man-in-the-middle attacks against PyPI downloads. +Starting with v1.3, pip provides SSL certificate verification over HTTP, to +prevent man-in-the-middle attacks against PyPI downloads. This is handled by +`requests <https://pypi.org/project/requests/>`_ which by default uses a +bundled CA certificate store provided by +`certifi <https://pypi.org/project/certifi/>`_ and does not use the system +certificate store. This may by overridden by using ``--cert`` option or by +using ``REQUESTS_CA_BUNDLE`` or ``CURL_CA_BUNDLE`` `environment variables <https://requests.readthedocs.io/en/latest/user/advanced/#verification>`_ +supported by ``requests``. .. _`Caching`: From 48e2c62cddd7ad4e41e9be81894d8eb76fa2de4d Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller <bbodenmiller@gmail.com> Date: Tue, 19 Jan 2021 18:09:28 -0800 Subject: [PATCH 2903/3170] Add details on PIP_CERT --- docs/html/reference/pip_install.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index 9eedafd78d1..df2eeba3e4c 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -567,8 +567,9 @@ prevent man-in-the-middle attacks against PyPI downloads. This is handled by bundled CA certificate store provided by `certifi <https://pypi.org/project/certifi/>`_ and does not use the system certificate store. This may by overridden by using ``--cert`` option or by -using ``REQUESTS_CA_BUNDLE`` or ``CURL_CA_BUNDLE`` `environment variables <https://requests.readthedocs.io/en/latest/user/advanced/#verification>`_ -supported by ``requests``. +using ``PIP_CERT`` environment variable; additional +`environment variables <https://requests.readthedocs.io/en/latest/user/advanced/#verification>`_ +are also supported by ``requests``. .. _`Caching`: From 61bda3dfc8144bd9fe42546e1e391712f1450189 Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller <bbodenmiller@gmail.com> Date: Tue, 19 Jan 2021 18:20:49 -0800 Subject: [PATCH 2904/3170] Simplify SSL doc - remove implementation details --- docs/html/reference/pip_install.rst | 13 +++++-------- 1 file changed, 5 insertions(+), 8 deletions(-) diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index df2eeba3e4c..7604d330bc3 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -562,14 +562,11 @@ SSL Certificate Verification ---------------------------- Starting with v1.3, pip provides SSL certificate verification over HTTP, to -prevent man-in-the-middle attacks against PyPI downloads. This is handled by -`requests <https://pypi.org/project/requests/>`_ which by default uses a -bundled CA certificate store provided by -`certifi <https://pypi.org/project/certifi/>`_ and does not use the system -certificate store. This may by overridden by using ``--cert`` option or by -using ``PIP_CERT`` environment variable; additional -`environment variables <https://requests.readthedocs.io/en/latest/user/advanced/#verification>`_ -are also supported by ``requests``. +prevent man-in-the-middle attacks against PyPI downloads. This does not use +the system certificate store but instead uses a bundled CA certificate +store. The default bundled CA certificate store certificate store may be +overridden by using ``--cert`` option or by using ``PIP_CERT``, +``REQUESTS_CA_BUNDLE``, or ``CURL_CA_BUNDLE`` environment variables. .. _`Caching`: From 6739f56351a88add7a9e09e4eec25f691f79ec79 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Wed, 20 Jan 2021 15:42:29 +0000 Subject: [PATCH 2905/3170] Use our own copy of strtobool, rather than the one from distutils --- src/pip/_internal/cli/cmdoptions.py | 2 +- src/pip/_internal/cli/parser.py | 3 +-- src/pip/_internal/utils/misc.py | 17 +++++++++++++++++ 3 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index e16f42de610..663143950ac 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -13,7 +13,6 @@ import os import textwrap import warnings -from distutils.util import strtobool from functools import partial from optparse import SUPPRESS_HELP, Option, OptionGroup from textwrap import dedent @@ -27,6 +26,7 @@ from pip._internal.models.index import PyPI from pip._internal.models.target_python import TargetPython from pip._internal.utils.hashes import STRONG_HASHES +from pip._internal.utils.misc import strtobool from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: diff --git a/src/pip/_internal/cli/parser.py b/src/pip/_internal/cli/parser.py index 3bc86d9a38a..60c61f30d3f 100644 --- a/src/pip/_internal/cli/parser.py +++ b/src/pip/_internal/cli/parser.py @@ -8,13 +8,12 @@ import shutil import sys import textwrap -from distutils.util import strtobool from pip._vendor.contextlib2 import suppress from pip._internal.cli.status_codes import UNKNOWN_ERROR from pip._internal.configuration import Configuration, ConfigurationError -from pip._internal.utils.misc import redact_auth_from_url +from pip._internal.utils.misc import redact_auth_from_url, strtobool logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index a3bd49b9139..6dd94e2fae7 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -244,6 +244,23 @@ def ask_password(message): return getpass.getpass(message) +def strtobool(val): + # type: (str) -> int + """Convert a string representation of truth to true (1) or false (0). + + True values are 'y', 'yes', 't', 'true', 'on', and '1'; false values + are 'n', 'no', 'f', 'false', 'off', and '0'. Raises ValueError if + 'val' is anything else. + """ + val = val.lower() + if val in ('y', 'yes', 't', 'true', 'on', '1'): + return 1 + elif val in ('n', 'no', 'f', 'false', 'off', '0'): + return 0 + else: + raise ValueError("invalid truth value %r" % (val,)) + + def format_size(bytes): # type: (float) -> str if bytes > 1000 * 1000: From 474c82bc3eed43273cdf05982778a7ea4afa5ebb Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 20 Jan 2021 08:03:45 +0800 Subject: [PATCH 2906/3170] Custom RST directive to generate stable version ID --- docs/html/news.rst | 2 +- docs/pip_sphinxext.py | 62 ++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 62 insertions(+), 2 deletions(-) diff --git a/docs/html/news.rst b/docs/html/news.rst index 8b54a02e637..829e6b74fd8 100644 --- a/docs/html/news.rst +++ b/docs/html/news.rst @@ -9,4 +9,4 @@ Changelog .. towncrier-draft-entries:: |release|, unreleased as on -.. include:: ../../NEWS.rst +.. pip-news-include:: ../../NEWS.rst diff --git a/docs/pip_sphinxext.py b/docs/pip_sphinxext.py index df4390d8103..d40b1564568 100644 --- a/docs/pip_sphinxext.py +++ b/docs/pip_sphinxext.py @@ -1,10 +1,12 @@ """pip sphinx extensions""" import optparse +import pathlib +import re import sys from textwrap import dedent -from docutils import nodes +from docutils import nodes, statemachine from docutils.parsers import rst from docutils.statemachine import ViewList @@ -13,6 +15,63 @@ from pip._internal.req.req_file import SUPPORTED_OPTIONS +class PipNewsInclude(rst.Directive): + required_arguments = 1 + + def _is_version_section_title_underline(self, prev, curr): + """Find a ==== line that marks the version section title.""" + if prev is None: + return False + if re.match(r"^=+$", curr) is None: + return False + if len(curr) < len(prev): + return False + return True + + def _iter_lines_with_refs(self, lines): + """Transform the input lines to add a ref before each section title. + + This is done by looking one line ahead and locate a title's underline, + and add a ref before the title text. + + Dots in the version is converted into dash, and a ``v`` is prefixed. + This makes Sphinx use them as HTML ``id`` verbatim without generating + auto numbering (which would make the the anchors unstable). + """ + prev = None + for line in lines: + # Transform the previous line to include an explicit ref. + if self._is_version_section_title_underline(prev, line): + vref = prev.split(None, 1)[0].replace(".", "-") + yield f".. _`v{vref}`:" + yield "" # Empty line between ref and the title. + if prev is not None: + yield prev + prev = line + if prev is not None: + yield prev + + def run(self): + source = self.state_machine.input_lines.source( + self.lineno - self.state_machine.input_offset - 1, + ) + path = ( + pathlib.Path(source) + .resolve() + .parent + .joinpath(self.arguments[0]) + .resolve() + ) + include_lines = statemachine.string2lines( + path.read_text(encoding="utf-8"), + self.state.document.settings.tab_width, + convert_whitespace=True, + ) + include_lines = list(self._iter_lines_with_refs(include_lines)) + self.state_machine.insert_input(include_lines, str(path)) + return [] + + class PipCommandUsage(rst.Directive): required_arguments = 1 optional_arguments = 3 @@ -162,3 +221,4 @@ def setup(app): app.add_directive( 'pip-requirements-file-options-ref-list', PipReqFileOptionsReference ) + app.add_directive('pip-news-include', PipNewsInclude) From f5a13dc6c595e1b6f45dddc994b6f22b5cf40b19 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Sat, 23 Jan 2021 10:38:13 +0000 Subject: [PATCH 2907/3170] Release process now (as of 21.0) produces py3-only wheels --- noxfile.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index 2c0a48ecbea..de4daf8a79c 100644 --- a/noxfile.py +++ b/noxfile.py @@ -324,7 +324,7 @@ def upload_release(session): # Sanity check: Make sure the files are correctly named. distfile_names = map(os.path.basename, distribution_files) expected_distribution_files = [ - f"pip-{version}-py2.py3-none-any.whl", + f"pip-{version}-py3-none-any.whl", f"pip-{version}.tar.gz", ] if sorted(distfile_names) != sorted(expected_distribution_files): From d6a21011b39f4d5afc71cfa5c9483f61787a1aff Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Sat, 23 Jan 2021 11:49:19 +0000 Subject: [PATCH 2908/3170] Remove support for extras after specifier in requirements --- src/pip/_internal/req/constructors.py | 4 +--- tests/functional/test_install_extras.py | 11 ++++------- tests/functional/test_new_resolver.py | 26 ------------------------- 3 files changed, 5 insertions(+), 36 deletions(-) diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index 172b4cb0335..cfb1951b6b8 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -23,7 +23,6 @@ from pip._internal.models.wheel import Wheel from pip._internal.pyproject import make_pyproject_path from pip._internal.req.req_install import InstallRequirement -from pip._internal.utils.deprecation import deprecated from pip._internal.utils.filetypes import is_archive_file from pip._internal.utils.misc import is_installable_dir from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -365,8 +364,7 @@ def with_source(text): spec_str = str(spec) if spec_str.endswith(']'): msg = f"Extras after version '{spec_str}'." - replace = "moving the extras before version specifiers" - deprecated(msg, replacement=replace, gone_in="21.0") + raise InstallationError(msg) else: req = None diff --git a/tests/functional/test_install_extras.py b/tests/functional/test_install_extras.py index 0ec42940630..e960100729f 100644 --- a/tests/functional/test_install_extras.py +++ b/tests/functional/test_install_extras.py @@ -104,9 +104,9 @@ def test_nonexistent_options_listed_in_order(script, data): assert matches == ['nonexistent', 'nope'] -def test_install_deprecated_extra(script, data): +def test_install_fails_if_extra_at_end(script, data): """ - Warn about deprecated order of specifiers and extras. + Fail if order of specifiers and extras is incorrect. Test uses a requirements file to avoid a testing issue where the specifier gets interpreted as shell redirect. @@ -114,15 +114,12 @@ def test_install_deprecated_extra(script, data): script.scratch_path.joinpath("requirements.txt").write_text( "requires_simple_extra>=0.1[extra]" ) - simple = script.site_packages / 'simple' result = script.pip( 'install', '--no-index', '--find-links=' + data.find_links, - '-r', script.scratch_path / 'requirements.txt', expect_stderr=True, + '-r', script.scratch_path / 'requirements.txt', expect_error=True, ) - - result.did_create(simple) - assert ("DEPRECATION: Extras after version" in result.stderr) + assert "Extras after version" in result.stderr def test_install_special_extra(script): diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 95422d22627..6943fd7de80 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -226,32 +226,6 @@ def test_new_resolver_installs_extras(tmpdir, script, root_dep): assert_installed(script, base="0.1.0", dep="0.1.0") -def test_new_resolver_installs_extras_deprecated(tmpdir, script): - req_file = tmpdir.joinpath("requirements.txt") - req_file.write_text("base >= 0.1.0[add]") - - create_basic_wheel_for_package( - script, - "base", - "0.1.0", - extras={"add": ["dep"]}, - ) - create_basic_wheel_for_package( - script, - "dep", - "0.1.0", - ) - result = script.pip( - "install", - "--no-cache-dir", "--no-index", - "--find-links", script.scratch_path, - "-r", req_file, - expect_stderr=True - ) - assert "DEPRECATION: Extras after version" in result.stderr - assert_installed(script, base="0.1.0", dep="0.1.0") - - def test_new_resolver_installs_extras_warn_missing(script): create_basic_wheel_for_package( script, From 8517a44f842ae3d1871b514c58c9ff17d6e858ea Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sat, 23 Jan 2021 11:51:53 +0000 Subject: [PATCH 2909/3170] Update changelog for 20.3.4 --- NEWS.rst | 33 +++++++++++++++++++++++++++++++++ news/8876.bugfix.rst | 3 --- news/9180.bugfix.rst | 1 - news/9203.bugfix.rst | 4 ---- news/9206.feature.rst | 3 --- news/9246.bugfix.rst | 4 ---- news/9315.feature.rst | 1 - news/resolvelib.vendor.rst | 1 - 8 files changed, 33 insertions(+), 17 deletions(-) delete mode 100644 news/8876.bugfix.rst delete mode 100644 news/9180.bugfix.rst delete mode 100644 news/9203.bugfix.rst delete mode 100644 news/9206.feature.rst delete mode 100644 news/9246.bugfix.rst delete mode 100644 news/9315.feature.rst delete mode 100644 news/resolvelib.vendor.rst diff --git a/NEWS.rst b/NEWS.rst index a082cddf314..18cc7b70427 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -9,6 +9,39 @@ .. towncrier release notes start +20.3.4 (2021-01-23) +=================== + +Features +-------- + +- ``pip wheel`` now verifies the built wheel contains valid metadata, and can be + installed by a subsequent ``pip install``. This can be disabled with + ``--no-verify``. (`#9206 <https://github.com/pypa/pip/issues/9206>`_) +- Improve presentation of XMLRPC errors in pip search. (`#9315 <https://github.com/pypa/pip/issues/9315>`_) + +Bug Fixes +--------- + +- Fixed hanging VCS subprocess calls when the VCS outputs a large amount of data + on stderr. Restored logging of VCS errors that was inadvertently removed in pip + 20.2. (`#8876 <https://github.com/pypa/pip/issues/8876>`_) +- Fix error when an existing incompatibility is unable to be applied to a backtracked state. (`#9180 <https://github.com/pypa/pip/issues/9180>`_) +- New resolver: Discard a faulty distribution, instead of quitting outright. + This implementation is taken from 20.2.2, with a fix that always makes the + resolver iterate through candidates from indexes lazily, to avoid downloading + candidates we do not need. (`#9203 <https://github.com/pypa/pip/issues/9203>`_) +- New resolver: Discard a source distribution if it fails to generate metadata, + instead of quitting outright. This implementation is taken from 20.2.2, with a + fix that always makes the resolver iterate through candidates from indexes + lazily, to avoid downloading candidates we do not need. (`#9246 <https://github.com/pypa/pip/issues/9246>`_) + +Vendored Libraries +------------------ + +- Upgrade resolvelib to 0.5.4. + + 20.3.3 (2020-12-15) =================== diff --git a/news/8876.bugfix.rst b/news/8876.bugfix.rst deleted file mode 100644 index 98250dc9745..00000000000 --- a/news/8876.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -Fixed hanging VCS subprocess calls when the VCS outputs a large amount of data -on stderr. Restored logging of VCS errors that was inadvertently removed in pip -20.2. diff --git a/news/9180.bugfix.rst b/news/9180.bugfix.rst deleted file mode 100644 index e597c1ad90a..00000000000 --- a/news/9180.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix error when an existing incompatibility is unable to be applied to a backtracked state. diff --git a/news/9203.bugfix.rst b/news/9203.bugfix.rst deleted file mode 100644 index 38320218fbb..00000000000 --- a/news/9203.bugfix.rst +++ /dev/null @@ -1,4 +0,0 @@ -New resolver: Discard a faulty distribution, instead of quitting outright. -This implementation is taken from 20.2.2, with a fix that always makes the -resolver iterate through candidates from indexes lazily, to avoid downloading -candidates we do not need. diff --git a/news/9206.feature.rst b/news/9206.feature.rst deleted file mode 100644 index 90cd2cf99fb..00000000000 --- a/news/9206.feature.rst +++ /dev/null @@ -1,3 +0,0 @@ -``pip wheel`` now verifies the built wheel contains valid metadata, and can be -installed by a subsequent ``pip install``. This can be disabled with -``--no-verify``. diff --git a/news/9246.bugfix.rst b/news/9246.bugfix.rst deleted file mode 100644 index 93f7f18f9f5..00000000000 --- a/news/9246.bugfix.rst +++ /dev/null @@ -1,4 +0,0 @@ -New resolver: Discard a source distribution if it fails to generate metadata, -instead of quitting outright. This implementation is taken from 20.2.2, with a -fix that always makes the resolver iterate through candidates from indexes -lazily, to avoid downloading candidates we do not need. diff --git a/news/9315.feature.rst b/news/9315.feature.rst deleted file mode 100644 index 64d1f25338b..00000000000 --- a/news/9315.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Improve presentation of XMLRPC errors in pip search. diff --git a/news/resolvelib.vendor.rst b/news/resolvelib.vendor.rst deleted file mode 100644 index 680da3be1e7..00000000000 --- a/news/resolvelib.vendor.rst +++ /dev/null @@ -1 +0,0 @@ -Upgrade resolvelib to 0.5.4. From 5575209bb297d341bf78c0db8e5a3f382c384abd Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Sat, 23 Jan 2021 13:14:05 +0000 Subject: [PATCH 2910/3170] Update AUTHORS.txt --- AUTHORS.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AUTHORS.txt b/AUTHORS.txt index 0360f988f2b..6bfbf36e29a 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -394,6 +394,7 @@ Nick Timkovich Nicolas Bock Nicole Harris Nikhil Benesch +Nikita Chepanov Nikolay Korolev Nitesh Sharma Noah @@ -466,6 +467,7 @@ Remi Rampin Rene Dudfield Riccardo Magliocchetti Richard Jones +Richard Si Ricky Ng-Adam RobberPhex Robert Collins From 91f43f1fd4e4daf5d8fbcd511bf2ba7ad6fd61d2 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Sat, 23 Jan 2021 13:14:09 +0000 Subject: [PATCH 2911/3170] Bump for release --- NEWS.rst | 46 +++++++++++++++++++ ...6d-b127-4551-a404-404b0ee8dcd3.trivial.rst | 0 ...15-1373-4226-a1ec-efe54b7ad480.trivial.rst | 0 news/1884.feature.rst | 1 - ...4f-502c-4213-b8b8-c9173718e8ab.trivial.rst | 0 ...61-eaf9-4117-a1a9-d5bb7f93b447.trivial.rst | 0 ...e8-ebb1-4eaf-aee0-e5582a8c5d58.trivial.rst | 0 ...cb-2fe8-4b0d-8734-303510a7e4ce.trivial.rst | 0 ...33-ec03-4b33-8cf1-ff76baa3690e.trivial.rst | 0 ...0e-e268-4519-bee7-6f79bc4cf489.trivial.rst | 0 ...39-9189-4bc3-bc87-bf598f1c1064.trivial.rst | 0 ...a6-8a59-425c-9a97-9c8e87e28ddb.trivial.rst | 0 ...91-9583-470e-a263-b196acf4072b.trivial.rst | 0 ...f1-5f9a-4f6b-8960-3334570ae591.trivial.rst | 0 ...1c-91d6-475c-959a-83485cafa4b2.trivial.rst | 0 ...b1-2151-45c3-baa0-b87e50d7e56d.trivial.rst | 0 news/6148.removal.rst | 1 - ...b0-98f9-4e1f-a541-af95fb990af9.trivial.rst | 0 news/7502.removal.rst | 2 - news/7554.removal.rst | 2 - ...a1-9af6-4190-8249-05a6328e379e.trivial.rst | 0 ...fc-938e-457b-ae6e-0905e7443b2f.trivial.rst | 0 ...18-3054-41C7-B920-78348DFD01A6.trivial.rst | 0 ...f2-1d4e-4067-9b4b-acc6ae741aef.trivial.rst | 0 ...cb-0539-41a0-871b-4ffe72765f6f.trivial.rst | 0 news/8802.removal.rst | 1 - ...3d-998e-4924-92e6-2ab2173159f8.trivial.rst | 0 news/9172.doc.rst | 1 - news/9186.feature.rst | 3 -- news/9189.removal.rst | 1 - news/9273.bugfix.rst | 3 -- news/9337.bugfix.rst | 2 - news/9338.removal.rst | 2 - news/9343.doc.rst | 1 - ...42-2198-4760-B5A7-B5A6BB98ECA2.trivial.rst | 0 ...e3-bfd3-46ae-b4b6-bcb9784303b4.trivial.rst | 0 ...89-8b92-4ec1-a3a1-a6657cf6fd5b.trivial.rst | 0 ...e7-d6f3-4f30-9192-6a8e69027d6a.trivial.rst | 0 ...46-e6b0-48b1-8b26-1145d611d082.trivial.rst | 0 ...d5-38b6-48cc-8136-0c32d3ace838.trivial.rst | 0 ...86bc866fdc4257a445e0df09dd7e64.trivial.rst | 0 ...cd-059a-4ea4-b02e-343f8b51aad5.trivial.rst | 0 ...cc-9fc9-4762-914e-34014e8d09bf.trivial.rst | 0 ...60-675c-4104-9825-39d1ee0a20b7.trivial.rst | 0 ...2f-aef7-4323-8332-819f0be13d79.trivial.rst | 0 ...0a-f673-4c1b-9959-3196b6c000e9.trivial.rst | 0 news/msgpack.vendor.rst | 1 - news/requests.vendor.rst | 1 - src/pip/__init__.py | 2 +- 49 files changed, 47 insertions(+), 23 deletions(-) delete mode 100644 news/0dd6ac6d-b127-4551-a404-404b0ee8dcd3.trivial.rst delete mode 100644 news/1170af15-1373-4226-a1ec-efe54b7ad480.trivial.rst delete mode 100644 news/1884.feature.rst delete mode 100644 news/205edb4f-502c-4213-b8b8-c9173718e8ab.trivial.rst delete mode 100644 news/24193261-eaf9-4117-a1a9-d5bb7f93b447.trivial.rst delete mode 100644 news/275aa0e8-ebb1-4eaf-aee0-e5582a8c5d58.trivial.rst delete mode 100644 news/2905cccb-2fe8-4b0d-8734-303510a7e4ce.trivial.rst delete mode 100644 news/2b5d1433-ec03-4b33-8cf1-ff76baa3690e.trivial.rst delete mode 100644 news/30e2240e-e268-4519-bee7-6f79bc4cf489.trivial.rst delete mode 100644 news/3ba38d39-9189-4bc3-bc87-bf598f1c1064.trivial.rst delete mode 100644 news/43602ba6-8a59-425c-9a97-9c8e87e28ddb.trivial.rst delete mode 100644 news/49254991-9583-470e-a263-b196acf4072b.trivial.rst delete mode 100644 news/4a85b5f1-5f9a-4f6b-8960-3334570ae591.trivial.rst delete mode 100644 news/4ec6e91c-91d6-475c-959a-83485cafa4b2.trivial.rst delete mode 100644 news/54754cb1-2151-45c3-baa0-b87e50d7e56d.trivial.rst delete mode 100644 news/6148.removal.rst delete mode 100644 news/738a71b0-98f9-4e1f-a541-af95fb990af9.trivial.rst delete mode 100644 news/7502.removal.rst delete mode 100644 news/7554.removal.rst delete mode 100644 news/7ced09a1-9af6-4190-8249-05a6328e379e.trivial.rst delete mode 100644 news/7edb0afc-938e-457b-ae6e-0905e7443b2f.trivial.rst delete mode 100644 news/80B9D718-3054-41C7-B920-78348DFD01A6.trivial.rst delete mode 100644 news/857785f2-1d4e-4067-9b4b-acc6ae741aef.trivial.rst delete mode 100644 news/86c319cb-0539-41a0-871b-4ffe72765f6f.trivial.rst delete mode 100644 news/8802.removal.rst delete mode 100644 news/8a225e3d-998e-4924-92e6-2ab2173159f8.trivial.rst delete mode 100644 news/9172.doc.rst delete mode 100644 news/9186.feature.rst delete mode 100644 news/9189.removal.rst delete mode 100644 news/9273.bugfix.rst delete mode 100644 news/9337.bugfix.rst delete mode 100644 news/9338.removal.rst delete mode 100644 news/9343.doc.rst delete mode 100644 news/9DE59242-2198-4760-B5A7-B5A6BB98ECA2.trivial.rst delete mode 100644 news/a13640e3-bfd3-46ae-b4b6-bcb9784303b4.trivial.rst delete mode 100644 news/a9950589-8b92-4ec1-a3a1-a6657cf6fd5b.trivial.rst delete mode 100644 news/ae7bdce7-d6f3-4f30-9192-6a8e69027d6a.trivial.rst delete mode 100644 news/b034ad46-e6b0-48b1-8b26-1145d611d082.trivial.rst delete mode 100644 news/b5e475d5-38b6-48cc-8136-0c32d3ace838.trivial.rst delete mode 100644 news/bb86bc866fdc4257a445e0df09dd7e64.trivial.rst delete mode 100644 news/d96bbdcd-059a-4ea4-b02e-343f8b51aad5.trivial.rst delete mode 100644 news/dc9e5ecc-9fc9-4762-914e-34014e8d09bf.trivial.rst delete mode 100644 news/ea24fc60-675c-4104-9825-39d1ee0a20b7.trivial.rst delete mode 100644 news/f0af302f-aef7-4323-8332-819f0be13d79.trivial.rst delete mode 100644 news/f0b01f0a-f673-4c1b-9959-3196b6c000e9.trivial.rst delete mode 100644 news/msgpack.vendor.rst delete mode 100644 news/requests.vendor.rst diff --git a/NEWS.rst b/NEWS.rst index 18cc7b70427..83608ac9c6c 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,49 @@ +21.0 (2021-01-23) +================= + +Deprecations and Removals +------------------------- + +- Drop support for Python 2. (`#6148 <https://github.com/pypa/pip/issues/6148>`_) +- Remove support for legacy wheel cache entries that were created with pip + versions older than 20.0. (`#7502 <https://github.com/pypa/pip/issues/7502>`_) +- Remove support for VCS pseudo URLs editable requirements. It was emitting + deprecation warning since version 20.0. (`#7554 <https://github.com/pypa/pip/issues/7554>`_) +- Modernise the codebase after Python 2. (`#8802 <https://github.com/pypa/pip/issues/8802>`_) +- Drop support for Python 3.5. (`#9189 <https://github.com/pypa/pip/issues/9189>`_) +- Remove the VCS export feature that was used only with editable VCS + requirements and had correctness issues. (`#9338 <https://github.com/pypa/pip/issues/9338>`_) + +Features +-------- + +- Add ``--ignore-requires-python`` support to pip download. (`#1884 <https://github.com/pypa/pip/issues/1884>`_) +- New resolver: Error message shown when a wheel contains inconsistent metadata + is made more helpful by including both values from the file name and internal + metadata. (`#9186 <https://github.com/pypa/pip/issues/9186>`_) + +Bug Fixes +--------- + +- Fix a regression that made ``pip wheel`` do a VCS export instead of a VCS clone + for editable requirements. This broke VCS requirements that need the VCS + information to build correctly. (`#9273 <https://github.com/pypa/pip/issues/9273>`_) +- Fix ``pip download`` of editable VCS requirements that need VCS information + to build correctly. (`#9337 <https://github.com/pypa/pip/issues/9337>`_) + +Vendored Libraries +------------------ + +- Upgrade msgpack to 1.0.2. +- Upgrade requests to 2.25.1. + +Improved Documentation +---------------------- + +- Render the unreleased pip version change notes on the news page in docs. (`#9172 <https://github.com/pypa/pip/issues/9172>`_) +- Fix broken email link in docs feedback banners. (`#9343 <https://github.com/pypa/pip/issues/9343>`_) + + .. note You should *NOT* be adding new change log entries to this file, this diff --git a/news/0dd6ac6d-b127-4551-a404-404b0ee8dcd3.trivial.rst b/news/0dd6ac6d-b127-4551-a404-404b0ee8dcd3.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/1170af15-1373-4226-a1ec-efe54b7ad480.trivial.rst b/news/1170af15-1373-4226-a1ec-efe54b7ad480.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/1884.feature.rst b/news/1884.feature.rst deleted file mode 100644 index 4b0b4180c35..00000000000 --- a/news/1884.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Add ``--ignore-requires-python`` support to pip download. diff --git a/news/205edb4f-502c-4213-b8b8-c9173718e8ab.trivial.rst b/news/205edb4f-502c-4213-b8b8-c9173718e8ab.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/24193261-eaf9-4117-a1a9-d5bb7f93b447.trivial.rst b/news/24193261-eaf9-4117-a1a9-d5bb7f93b447.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/275aa0e8-ebb1-4eaf-aee0-e5582a8c5d58.trivial.rst b/news/275aa0e8-ebb1-4eaf-aee0-e5582a8c5d58.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/2905cccb-2fe8-4b0d-8734-303510a7e4ce.trivial.rst b/news/2905cccb-2fe8-4b0d-8734-303510a7e4ce.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/2b5d1433-ec03-4b33-8cf1-ff76baa3690e.trivial.rst b/news/2b5d1433-ec03-4b33-8cf1-ff76baa3690e.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/30e2240e-e268-4519-bee7-6f79bc4cf489.trivial.rst b/news/30e2240e-e268-4519-bee7-6f79bc4cf489.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/3ba38d39-9189-4bc3-bc87-bf598f1c1064.trivial.rst b/news/3ba38d39-9189-4bc3-bc87-bf598f1c1064.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/43602ba6-8a59-425c-9a97-9c8e87e28ddb.trivial.rst b/news/43602ba6-8a59-425c-9a97-9c8e87e28ddb.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/49254991-9583-470e-a263-b196acf4072b.trivial.rst b/news/49254991-9583-470e-a263-b196acf4072b.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/4a85b5f1-5f9a-4f6b-8960-3334570ae591.trivial.rst b/news/4a85b5f1-5f9a-4f6b-8960-3334570ae591.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/4ec6e91c-91d6-475c-959a-83485cafa4b2.trivial.rst b/news/4ec6e91c-91d6-475c-959a-83485cafa4b2.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/54754cb1-2151-45c3-baa0-b87e50d7e56d.trivial.rst b/news/54754cb1-2151-45c3-baa0-b87e50d7e56d.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/6148.removal.rst b/news/6148.removal.rst deleted file mode 100644 index cf6d85e70ba..00000000000 --- a/news/6148.removal.rst +++ /dev/null @@ -1 +0,0 @@ -Drop support for Python 2. diff --git a/news/738a71b0-98f9-4e1f-a541-af95fb990af9.trivial.rst b/news/738a71b0-98f9-4e1f-a541-af95fb990af9.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/7502.removal.rst b/news/7502.removal.rst deleted file mode 100644 index 9f03366ed8e..00000000000 --- a/news/7502.removal.rst +++ /dev/null @@ -1,2 +0,0 @@ -Remove support for legacy wheel cache entries that were created with pip -versions older than 20.0. diff --git a/news/7554.removal.rst b/news/7554.removal.rst deleted file mode 100644 index d5037d5fdb9..00000000000 --- a/news/7554.removal.rst +++ /dev/null @@ -1,2 +0,0 @@ -Remove support for VCS pseudo URLs editable requirements. It was emitting -deprecation warning since version 20.0. diff --git a/news/7ced09a1-9af6-4190-8249-05a6328e379e.trivial.rst b/news/7ced09a1-9af6-4190-8249-05a6328e379e.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/7edb0afc-938e-457b-ae6e-0905e7443b2f.trivial.rst b/news/7edb0afc-938e-457b-ae6e-0905e7443b2f.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/80B9D718-3054-41C7-B920-78348DFD01A6.trivial.rst b/news/80B9D718-3054-41C7-B920-78348DFD01A6.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/857785f2-1d4e-4067-9b4b-acc6ae741aef.trivial.rst b/news/857785f2-1d4e-4067-9b4b-acc6ae741aef.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/86c319cb-0539-41a0-871b-4ffe72765f6f.trivial.rst b/news/86c319cb-0539-41a0-871b-4ffe72765f6f.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/8802.removal.rst b/news/8802.removal.rst deleted file mode 100644 index 79d8e508166..00000000000 --- a/news/8802.removal.rst +++ /dev/null @@ -1 +0,0 @@ -Modernise the codebase after Python 2. diff --git a/news/8a225e3d-998e-4924-92e6-2ab2173159f8.trivial.rst b/news/8a225e3d-998e-4924-92e6-2ab2173159f8.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/9172.doc.rst b/news/9172.doc.rst deleted file mode 100644 index fc0063766b2..00000000000 --- a/news/9172.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Render the unreleased pip version change notes on the news page in docs. diff --git a/news/9186.feature.rst b/news/9186.feature.rst deleted file mode 100644 index 175b5a883ac..00000000000 --- a/news/9186.feature.rst +++ /dev/null @@ -1,3 +0,0 @@ -New resolver: Error message shown when a wheel contains inconsistent metadata -is made more helpful by including both values from the file name and internal -metadata. diff --git a/news/9189.removal.rst b/news/9189.removal.rst deleted file mode 100644 index 79928cbb15a..00000000000 --- a/news/9189.removal.rst +++ /dev/null @@ -1 +0,0 @@ -Drop support for Python 3.5. diff --git a/news/9273.bugfix.rst b/news/9273.bugfix.rst deleted file mode 100644 index e729ea29446..00000000000 --- a/news/9273.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -Fix a regression that made ``pip wheel`` do a VCS export instead of a VCS clone -for editable requirements. This broke VCS requirements that need the VCS -information to build correctly. diff --git a/news/9337.bugfix.rst b/news/9337.bugfix.rst deleted file mode 100644 index e9d08c3ad82..00000000000 --- a/news/9337.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fix ``pip download`` of editable VCS requirements that need VCS information -to build correctly. diff --git a/news/9338.removal.rst b/news/9338.removal.rst deleted file mode 100644 index 6d3b666e53f..00000000000 --- a/news/9338.removal.rst +++ /dev/null @@ -1,2 +0,0 @@ -Remove the VCS export feature that was used only with editable VCS -requirements and had correctness issues. diff --git a/news/9343.doc.rst b/news/9343.doc.rst deleted file mode 100644 index 1e4f91aec4c..00000000000 --- a/news/9343.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Fix broken email link in docs feedback banners. diff --git a/news/9DE59242-2198-4760-B5A7-B5A6BB98ECA2.trivial.rst b/news/9DE59242-2198-4760-B5A7-B5A6BB98ECA2.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/a13640e3-bfd3-46ae-b4b6-bcb9784303b4.trivial.rst b/news/a13640e3-bfd3-46ae-b4b6-bcb9784303b4.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/a9950589-8b92-4ec1-a3a1-a6657cf6fd5b.trivial.rst b/news/a9950589-8b92-4ec1-a3a1-a6657cf6fd5b.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/ae7bdce7-d6f3-4f30-9192-6a8e69027d6a.trivial.rst b/news/ae7bdce7-d6f3-4f30-9192-6a8e69027d6a.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/b034ad46-e6b0-48b1-8b26-1145d611d082.trivial.rst b/news/b034ad46-e6b0-48b1-8b26-1145d611d082.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/b5e475d5-38b6-48cc-8136-0c32d3ace838.trivial.rst b/news/b5e475d5-38b6-48cc-8136-0c32d3ace838.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/bb86bc866fdc4257a445e0df09dd7e64.trivial.rst b/news/bb86bc866fdc4257a445e0df09dd7e64.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/d96bbdcd-059a-4ea4-b02e-343f8b51aad5.trivial.rst b/news/d96bbdcd-059a-4ea4-b02e-343f8b51aad5.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/dc9e5ecc-9fc9-4762-914e-34014e8d09bf.trivial.rst b/news/dc9e5ecc-9fc9-4762-914e-34014e8d09bf.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/ea24fc60-675c-4104-9825-39d1ee0a20b7.trivial.rst b/news/ea24fc60-675c-4104-9825-39d1ee0a20b7.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/f0af302f-aef7-4323-8332-819f0be13d79.trivial.rst b/news/f0af302f-aef7-4323-8332-819f0be13d79.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/f0b01f0a-f673-4c1b-9959-3196b6c000e9.trivial.rst b/news/f0b01f0a-f673-4c1b-9959-3196b6c000e9.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/msgpack.vendor.rst b/news/msgpack.vendor.rst deleted file mode 100644 index 14a06e1c6bc..00000000000 --- a/news/msgpack.vendor.rst +++ /dev/null @@ -1 +0,0 @@ -Upgrade msgpack to 1.0.2. diff --git a/news/requests.vendor.rst b/news/requests.vendor.rst deleted file mode 100644 index 9c9dee7b4a4..00000000000 --- a/news/requests.vendor.rst +++ /dev/null @@ -1 +0,0 @@ -Upgrade requests to 2.25.1. diff --git a/src/pip/__init__.py b/src/pip/__init__.py index ae0fe9a9f24..38ebba574c9 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -4,7 +4,7 @@ from typing import List, Optional -__version__ = "21.0.dev0" +__version__ = "21.0" def main(args=None): From c1382e915546ffb600907923685015c930ef0f26 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Sat, 23 Jan 2021 13:14:09 +0000 Subject: [PATCH 2912/3170] Bump for development --- src/pip/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/__init__.py b/src/pip/__init__.py index 38ebba574c9..9de6dc0f2a6 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -4,7 +4,7 @@ from typing import List, Optional -__version__ = "21.0" +__version__ = "21.1.dev0" def main(args=None): From e01c7d719a0360ddab9f69f34c612933f332f551 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Sat, 23 Jan 2021 14:37:51 +0000 Subject: [PATCH 2913/3170] Revert "Bump for development" This reverts commit c1382e915546ffb600907923685015c930ef0f26. --- src/pip/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/__init__.py b/src/pip/__init__.py index 9de6dc0f2a6..38ebba574c9 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -4,7 +4,7 @@ from typing import List, Optional -__version__ = "21.1.dev0" +__version__ = "21.0" def main(args=None): From f03051540ca60bc5653129048f5171aeb415db26 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Sat, 23 Jan 2021 14:39:29 +0000 Subject: [PATCH 2914/3170] Revert "Bump for release" This reverts commit 91f43f1fd4e4daf5d8fbcd511bf2ba7ad6fd61d2. --- NEWS.rst | 46 ------------------- ...6d-b127-4551-a404-404b0ee8dcd3.trivial.rst | 0 ...15-1373-4226-a1ec-efe54b7ad480.trivial.rst | 0 news/1884.feature.rst | 1 + ...4f-502c-4213-b8b8-c9173718e8ab.trivial.rst | 0 ...61-eaf9-4117-a1a9-d5bb7f93b447.trivial.rst | 0 ...e8-ebb1-4eaf-aee0-e5582a8c5d58.trivial.rst | 0 ...cb-2fe8-4b0d-8734-303510a7e4ce.trivial.rst | 0 ...33-ec03-4b33-8cf1-ff76baa3690e.trivial.rst | 0 ...0e-e268-4519-bee7-6f79bc4cf489.trivial.rst | 0 ...39-9189-4bc3-bc87-bf598f1c1064.trivial.rst | 0 ...a6-8a59-425c-9a97-9c8e87e28ddb.trivial.rst | 0 ...91-9583-470e-a263-b196acf4072b.trivial.rst | 0 ...f1-5f9a-4f6b-8960-3334570ae591.trivial.rst | 0 ...1c-91d6-475c-959a-83485cafa4b2.trivial.rst | 0 ...b1-2151-45c3-baa0-b87e50d7e56d.trivial.rst | 0 news/6148.removal.rst | 1 + ...b0-98f9-4e1f-a541-af95fb990af9.trivial.rst | 0 news/7502.removal.rst | 2 + news/7554.removal.rst | 2 + ...a1-9af6-4190-8249-05a6328e379e.trivial.rst | 0 ...fc-938e-457b-ae6e-0905e7443b2f.trivial.rst | 0 ...18-3054-41C7-B920-78348DFD01A6.trivial.rst | 0 ...f2-1d4e-4067-9b4b-acc6ae741aef.trivial.rst | 0 ...cb-0539-41a0-871b-4ffe72765f6f.trivial.rst | 0 news/8802.removal.rst | 1 + ...3d-998e-4924-92e6-2ab2173159f8.trivial.rst | 0 news/9172.doc.rst | 1 + news/9186.feature.rst | 3 ++ news/9189.removal.rst | 1 + news/9273.bugfix.rst | 3 ++ news/9337.bugfix.rst | 2 + news/9338.removal.rst | 2 + news/9343.doc.rst | 1 + ...42-2198-4760-B5A7-B5A6BB98ECA2.trivial.rst | 0 ...e3-bfd3-46ae-b4b6-bcb9784303b4.trivial.rst | 0 ...89-8b92-4ec1-a3a1-a6657cf6fd5b.trivial.rst | 0 ...e7-d6f3-4f30-9192-6a8e69027d6a.trivial.rst | 0 ...46-e6b0-48b1-8b26-1145d611d082.trivial.rst | 0 ...d5-38b6-48cc-8136-0c32d3ace838.trivial.rst | 0 ...86bc866fdc4257a445e0df09dd7e64.trivial.rst | 0 ...cd-059a-4ea4-b02e-343f8b51aad5.trivial.rst | 0 ...cc-9fc9-4762-914e-34014e8d09bf.trivial.rst | 0 ...60-675c-4104-9825-39d1ee0a20b7.trivial.rst | 0 ...2f-aef7-4323-8332-819f0be13d79.trivial.rst | 0 ...0a-f673-4c1b-9959-3196b6c000e9.trivial.rst | 0 news/msgpack.vendor.rst | 1 + news/requests.vendor.rst | 1 + src/pip/__init__.py | 2 +- 49 files changed, 23 insertions(+), 47 deletions(-) create mode 100644 news/0dd6ac6d-b127-4551-a404-404b0ee8dcd3.trivial.rst create mode 100644 news/1170af15-1373-4226-a1ec-efe54b7ad480.trivial.rst create mode 100644 news/1884.feature.rst create mode 100644 news/205edb4f-502c-4213-b8b8-c9173718e8ab.trivial.rst create mode 100644 news/24193261-eaf9-4117-a1a9-d5bb7f93b447.trivial.rst create mode 100644 news/275aa0e8-ebb1-4eaf-aee0-e5582a8c5d58.trivial.rst create mode 100644 news/2905cccb-2fe8-4b0d-8734-303510a7e4ce.trivial.rst create mode 100644 news/2b5d1433-ec03-4b33-8cf1-ff76baa3690e.trivial.rst create mode 100644 news/30e2240e-e268-4519-bee7-6f79bc4cf489.trivial.rst create mode 100644 news/3ba38d39-9189-4bc3-bc87-bf598f1c1064.trivial.rst create mode 100644 news/43602ba6-8a59-425c-9a97-9c8e87e28ddb.trivial.rst create mode 100644 news/49254991-9583-470e-a263-b196acf4072b.trivial.rst create mode 100644 news/4a85b5f1-5f9a-4f6b-8960-3334570ae591.trivial.rst create mode 100644 news/4ec6e91c-91d6-475c-959a-83485cafa4b2.trivial.rst create mode 100644 news/54754cb1-2151-45c3-baa0-b87e50d7e56d.trivial.rst create mode 100644 news/6148.removal.rst create mode 100644 news/738a71b0-98f9-4e1f-a541-af95fb990af9.trivial.rst create mode 100644 news/7502.removal.rst create mode 100644 news/7554.removal.rst create mode 100644 news/7ced09a1-9af6-4190-8249-05a6328e379e.trivial.rst create mode 100644 news/7edb0afc-938e-457b-ae6e-0905e7443b2f.trivial.rst create mode 100644 news/80B9D718-3054-41C7-B920-78348DFD01A6.trivial.rst create mode 100644 news/857785f2-1d4e-4067-9b4b-acc6ae741aef.trivial.rst create mode 100644 news/86c319cb-0539-41a0-871b-4ffe72765f6f.trivial.rst create mode 100644 news/8802.removal.rst create mode 100644 news/8a225e3d-998e-4924-92e6-2ab2173159f8.trivial.rst create mode 100644 news/9172.doc.rst create mode 100644 news/9186.feature.rst create mode 100644 news/9189.removal.rst create mode 100644 news/9273.bugfix.rst create mode 100644 news/9337.bugfix.rst create mode 100644 news/9338.removal.rst create mode 100644 news/9343.doc.rst create mode 100644 news/9DE59242-2198-4760-B5A7-B5A6BB98ECA2.trivial.rst create mode 100644 news/a13640e3-bfd3-46ae-b4b6-bcb9784303b4.trivial.rst create mode 100644 news/a9950589-8b92-4ec1-a3a1-a6657cf6fd5b.trivial.rst create mode 100644 news/ae7bdce7-d6f3-4f30-9192-6a8e69027d6a.trivial.rst create mode 100644 news/b034ad46-e6b0-48b1-8b26-1145d611d082.trivial.rst create mode 100644 news/b5e475d5-38b6-48cc-8136-0c32d3ace838.trivial.rst create mode 100644 news/bb86bc866fdc4257a445e0df09dd7e64.trivial.rst create mode 100644 news/d96bbdcd-059a-4ea4-b02e-343f8b51aad5.trivial.rst create mode 100644 news/dc9e5ecc-9fc9-4762-914e-34014e8d09bf.trivial.rst create mode 100644 news/ea24fc60-675c-4104-9825-39d1ee0a20b7.trivial.rst create mode 100644 news/f0af302f-aef7-4323-8332-819f0be13d79.trivial.rst create mode 100644 news/f0b01f0a-f673-4c1b-9959-3196b6c000e9.trivial.rst create mode 100644 news/msgpack.vendor.rst create mode 100644 news/requests.vendor.rst diff --git a/NEWS.rst b/NEWS.rst index 83608ac9c6c..18cc7b70427 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,49 +1,3 @@ -21.0 (2021-01-23) -================= - -Deprecations and Removals -------------------------- - -- Drop support for Python 2. (`#6148 <https://github.com/pypa/pip/issues/6148>`_) -- Remove support for legacy wheel cache entries that were created with pip - versions older than 20.0. (`#7502 <https://github.com/pypa/pip/issues/7502>`_) -- Remove support for VCS pseudo URLs editable requirements. It was emitting - deprecation warning since version 20.0. (`#7554 <https://github.com/pypa/pip/issues/7554>`_) -- Modernise the codebase after Python 2. (`#8802 <https://github.com/pypa/pip/issues/8802>`_) -- Drop support for Python 3.5. (`#9189 <https://github.com/pypa/pip/issues/9189>`_) -- Remove the VCS export feature that was used only with editable VCS - requirements and had correctness issues. (`#9338 <https://github.com/pypa/pip/issues/9338>`_) - -Features --------- - -- Add ``--ignore-requires-python`` support to pip download. (`#1884 <https://github.com/pypa/pip/issues/1884>`_) -- New resolver: Error message shown when a wheel contains inconsistent metadata - is made more helpful by including both values from the file name and internal - metadata. (`#9186 <https://github.com/pypa/pip/issues/9186>`_) - -Bug Fixes ---------- - -- Fix a regression that made ``pip wheel`` do a VCS export instead of a VCS clone - for editable requirements. This broke VCS requirements that need the VCS - information to build correctly. (`#9273 <https://github.com/pypa/pip/issues/9273>`_) -- Fix ``pip download`` of editable VCS requirements that need VCS information - to build correctly. (`#9337 <https://github.com/pypa/pip/issues/9337>`_) - -Vendored Libraries ------------------- - -- Upgrade msgpack to 1.0.2. -- Upgrade requests to 2.25.1. - -Improved Documentation ----------------------- - -- Render the unreleased pip version change notes on the news page in docs. (`#9172 <https://github.com/pypa/pip/issues/9172>`_) -- Fix broken email link in docs feedback banners. (`#9343 <https://github.com/pypa/pip/issues/9343>`_) - - .. note You should *NOT* be adding new change log entries to this file, this diff --git a/news/0dd6ac6d-b127-4551-a404-404b0ee8dcd3.trivial.rst b/news/0dd6ac6d-b127-4551-a404-404b0ee8dcd3.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/news/1170af15-1373-4226-a1ec-efe54b7ad480.trivial.rst b/news/1170af15-1373-4226-a1ec-efe54b7ad480.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/news/1884.feature.rst b/news/1884.feature.rst new file mode 100644 index 00000000000..4b0b4180c35 --- /dev/null +++ b/news/1884.feature.rst @@ -0,0 +1 @@ +Add ``--ignore-requires-python`` support to pip download. diff --git a/news/205edb4f-502c-4213-b8b8-c9173718e8ab.trivial.rst b/news/205edb4f-502c-4213-b8b8-c9173718e8ab.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/news/24193261-eaf9-4117-a1a9-d5bb7f93b447.trivial.rst b/news/24193261-eaf9-4117-a1a9-d5bb7f93b447.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/news/275aa0e8-ebb1-4eaf-aee0-e5582a8c5d58.trivial.rst b/news/275aa0e8-ebb1-4eaf-aee0-e5582a8c5d58.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/news/2905cccb-2fe8-4b0d-8734-303510a7e4ce.trivial.rst b/news/2905cccb-2fe8-4b0d-8734-303510a7e4ce.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/news/2b5d1433-ec03-4b33-8cf1-ff76baa3690e.trivial.rst b/news/2b5d1433-ec03-4b33-8cf1-ff76baa3690e.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/news/30e2240e-e268-4519-bee7-6f79bc4cf489.trivial.rst b/news/30e2240e-e268-4519-bee7-6f79bc4cf489.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/news/3ba38d39-9189-4bc3-bc87-bf598f1c1064.trivial.rst b/news/3ba38d39-9189-4bc3-bc87-bf598f1c1064.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/news/43602ba6-8a59-425c-9a97-9c8e87e28ddb.trivial.rst b/news/43602ba6-8a59-425c-9a97-9c8e87e28ddb.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/news/49254991-9583-470e-a263-b196acf4072b.trivial.rst b/news/49254991-9583-470e-a263-b196acf4072b.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/news/4a85b5f1-5f9a-4f6b-8960-3334570ae591.trivial.rst b/news/4a85b5f1-5f9a-4f6b-8960-3334570ae591.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/news/4ec6e91c-91d6-475c-959a-83485cafa4b2.trivial.rst b/news/4ec6e91c-91d6-475c-959a-83485cafa4b2.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/news/54754cb1-2151-45c3-baa0-b87e50d7e56d.trivial.rst b/news/54754cb1-2151-45c3-baa0-b87e50d7e56d.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/news/6148.removal.rst b/news/6148.removal.rst new file mode 100644 index 00000000000..cf6d85e70ba --- /dev/null +++ b/news/6148.removal.rst @@ -0,0 +1 @@ +Drop support for Python 2. diff --git a/news/738a71b0-98f9-4e1f-a541-af95fb990af9.trivial.rst b/news/738a71b0-98f9-4e1f-a541-af95fb990af9.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/news/7502.removal.rst b/news/7502.removal.rst new file mode 100644 index 00000000000..9f03366ed8e --- /dev/null +++ b/news/7502.removal.rst @@ -0,0 +1,2 @@ +Remove support for legacy wheel cache entries that were created with pip +versions older than 20.0. diff --git a/news/7554.removal.rst b/news/7554.removal.rst new file mode 100644 index 00000000000..d5037d5fdb9 --- /dev/null +++ b/news/7554.removal.rst @@ -0,0 +1,2 @@ +Remove support for VCS pseudo URLs editable requirements. It was emitting +deprecation warning since version 20.0. diff --git a/news/7ced09a1-9af6-4190-8249-05a6328e379e.trivial.rst b/news/7ced09a1-9af6-4190-8249-05a6328e379e.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/news/7edb0afc-938e-457b-ae6e-0905e7443b2f.trivial.rst b/news/7edb0afc-938e-457b-ae6e-0905e7443b2f.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/news/80B9D718-3054-41C7-B920-78348DFD01A6.trivial.rst b/news/80B9D718-3054-41C7-B920-78348DFD01A6.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/news/857785f2-1d4e-4067-9b4b-acc6ae741aef.trivial.rst b/news/857785f2-1d4e-4067-9b4b-acc6ae741aef.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/news/86c319cb-0539-41a0-871b-4ffe72765f6f.trivial.rst b/news/86c319cb-0539-41a0-871b-4ffe72765f6f.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/news/8802.removal.rst b/news/8802.removal.rst new file mode 100644 index 00000000000..79d8e508166 --- /dev/null +++ b/news/8802.removal.rst @@ -0,0 +1 @@ +Modernise the codebase after Python 2. diff --git a/news/8a225e3d-998e-4924-92e6-2ab2173159f8.trivial.rst b/news/8a225e3d-998e-4924-92e6-2ab2173159f8.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/news/9172.doc.rst b/news/9172.doc.rst new file mode 100644 index 00000000000..fc0063766b2 --- /dev/null +++ b/news/9172.doc.rst @@ -0,0 +1 @@ +Render the unreleased pip version change notes on the news page in docs. diff --git a/news/9186.feature.rst b/news/9186.feature.rst new file mode 100644 index 00000000000..175b5a883ac --- /dev/null +++ b/news/9186.feature.rst @@ -0,0 +1,3 @@ +New resolver: Error message shown when a wheel contains inconsistent metadata +is made more helpful by including both values from the file name and internal +metadata. diff --git a/news/9189.removal.rst b/news/9189.removal.rst new file mode 100644 index 00000000000..79928cbb15a --- /dev/null +++ b/news/9189.removal.rst @@ -0,0 +1 @@ +Drop support for Python 3.5. diff --git a/news/9273.bugfix.rst b/news/9273.bugfix.rst new file mode 100644 index 00000000000..e729ea29446 --- /dev/null +++ b/news/9273.bugfix.rst @@ -0,0 +1,3 @@ +Fix a regression that made ``pip wheel`` do a VCS export instead of a VCS clone +for editable requirements. This broke VCS requirements that need the VCS +information to build correctly. diff --git a/news/9337.bugfix.rst b/news/9337.bugfix.rst new file mode 100644 index 00000000000..e9d08c3ad82 --- /dev/null +++ b/news/9337.bugfix.rst @@ -0,0 +1,2 @@ +Fix ``pip download`` of editable VCS requirements that need VCS information +to build correctly. diff --git a/news/9338.removal.rst b/news/9338.removal.rst new file mode 100644 index 00000000000..6d3b666e53f --- /dev/null +++ b/news/9338.removal.rst @@ -0,0 +1,2 @@ +Remove the VCS export feature that was used only with editable VCS +requirements and had correctness issues. diff --git a/news/9343.doc.rst b/news/9343.doc.rst new file mode 100644 index 00000000000..1e4f91aec4c --- /dev/null +++ b/news/9343.doc.rst @@ -0,0 +1 @@ +Fix broken email link in docs feedback banners. diff --git a/news/9DE59242-2198-4760-B5A7-B5A6BB98ECA2.trivial.rst b/news/9DE59242-2198-4760-B5A7-B5A6BB98ECA2.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/news/a13640e3-bfd3-46ae-b4b6-bcb9784303b4.trivial.rst b/news/a13640e3-bfd3-46ae-b4b6-bcb9784303b4.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/news/a9950589-8b92-4ec1-a3a1-a6657cf6fd5b.trivial.rst b/news/a9950589-8b92-4ec1-a3a1-a6657cf6fd5b.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/news/ae7bdce7-d6f3-4f30-9192-6a8e69027d6a.trivial.rst b/news/ae7bdce7-d6f3-4f30-9192-6a8e69027d6a.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/news/b034ad46-e6b0-48b1-8b26-1145d611d082.trivial.rst b/news/b034ad46-e6b0-48b1-8b26-1145d611d082.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/news/b5e475d5-38b6-48cc-8136-0c32d3ace838.trivial.rst b/news/b5e475d5-38b6-48cc-8136-0c32d3ace838.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/news/bb86bc866fdc4257a445e0df09dd7e64.trivial.rst b/news/bb86bc866fdc4257a445e0df09dd7e64.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/news/d96bbdcd-059a-4ea4-b02e-343f8b51aad5.trivial.rst b/news/d96bbdcd-059a-4ea4-b02e-343f8b51aad5.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/news/dc9e5ecc-9fc9-4762-914e-34014e8d09bf.trivial.rst b/news/dc9e5ecc-9fc9-4762-914e-34014e8d09bf.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/news/ea24fc60-675c-4104-9825-39d1ee0a20b7.trivial.rst b/news/ea24fc60-675c-4104-9825-39d1ee0a20b7.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/news/f0af302f-aef7-4323-8332-819f0be13d79.trivial.rst b/news/f0af302f-aef7-4323-8332-819f0be13d79.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/news/f0b01f0a-f673-4c1b-9959-3196b6c000e9.trivial.rst b/news/f0b01f0a-f673-4c1b-9959-3196b6c000e9.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/news/msgpack.vendor.rst b/news/msgpack.vendor.rst new file mode 100644 index 00000000000..14a06e1c6bc --- /dev/null +++ b/news/msgpack.vendor.rst @@ -0,0 +1 @@ +Upgrade msgpack to 1.0.2. diff --git a/news/requests.vendor.rst b/news/requests.vendor.rst new file mode 100644 index 00000000000..9c9dee7b4a4 --- /dev/null +++ b/news/requests.vendor.rst @@ -0,0 +1 @@ +Upgrade requests to 2.25.1. diff --git a/src/pip/__init__.py b/src/pip/__init__.py index 38ebba574c9..ae0fe9a9f24 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -4,7 +4,7 @@ from typing import List, Optional -__version__ = "21.0" +__version__ = "21.0.dev0" def main(args=None): From 2a6f3c6c3a0a73b2eaa6ec32bea3ab725e473e3a Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Sat, 23 Jan 2021 14:40:44 +0000 Subject: [PATCH 2915/3170] Fix syntax of classifiers --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b3aaa361396..9f305eafde3 100644 --- a/setup.py +++ b/setup.py @@ -40,7 +40,7 @@ def get_version(rel_path): "Topic :: Software Development :: Build Tools", "Programming Language :: Python", "Programming Language :: Python :: 3", - "Programming Language :: Python :: 3 :: Only" + "Programming Language :: Python :: 3 :: Only", "Programming Language :: Python :: 3.6", "Programming Language :: Python :: 3.7", "Programming Language :: Python :: 3.8", From 6c09bdd7c0aff4bb48d9524b9e46d94d1830e9da Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Sat, 23 Jan 2021 14:42:04 +0000 Subject: [PATCH 2916/3170] Bump for release --- NEWS.rst | 46 +++++++++++++++++++ ...6d-b127-4551-a404-404b0ee8dcd3.trivial.rst | 0 ...15-1373-4226-a1ec-efe54b7ad480.trivial.rst | 0 news/1884.feature.rst | 1 - ...4f-502c-4213-b8b8-c9173718e8ab.trivial.rst | 0 ...61-eaf9-4117-a1a9-d5bb7f93b447.trivial.rst | 0 ...e8-ebb1-4eaf-aee0-e5582a8c5d58.trivial.rst | 0 ...cb-2fe8-4b0d-8734-303510a7e4ce.trivial.rst | 0 ...33-ec03-4b33-8cf1-ff76baa3690e.trivial.rst | 0 ...0e-e268-4519-bee7-6f79bc4cf489.trivial.rst | 0 ...39-9189-4bc3-bc87-bf598f1c1064.trivial.rst | 0 ...a6-8a59-425c-9a97-9c8e87e28ddb.trivial.rst | 0 ...91-9583-470e-a263-b196acf4072b.trivial.rst | 0 ...f1-5f9a-4f6b-8960-3334570ae591.trivial.rst | 0 ...1c-91d6-475c-959a-83485cafa4b2.trivial.rst | 0 ...b1-2151-45c3-baa0-b87e50d7e56d.trivial.rst | 0 news/6148.removal.rst | 1 - ...b0-98f9-4e1f-a541-af95fb990af9.trivial.rst | 0 news/7502.removal.rst | 2 - news/7554.removal.rst | 2 - ...a1-9af6-4190-8249-05a6328e379e.trivial.rst | 0 ...fc-938e-457b-ae6e-0905e7443b2f.trivial.rst | 0 ...18-3054-41C7-B920-78348DFD01A6.trivial.rst | 0 ...f2-1d4e-4067-9b4b-acc6ae741aef.trivial.rst | 0 ...cb-0539-41a0-871b-4ffe72765f6f.trivial.rst | 0 news/8802.removal.rst | 1 - ...3d-998e-4924-92e6-2ab2173159f8.trivial.rst | 0 news/9172.doc.rst | 1 - news/9186.feature.rst | 3 -- news/9189.removal.rst | 1 - news/9273.bugfix.rst | 3 -- news/9337.bugfix.rst | 2 - news/9338.removal.rst | 2 - news/9343.doc.rst | 1 - ...42-2198-4760-B5A7-B5A6BB98ECA2.trivial.rst | 0 ...e3-bfd3-46ae-b4b6-bcb9784303b4.trivial.rst | 0 ...89-8b92-4ec1-a3a1-a6657cf6fd5b.trivial.rst | 0 ...e7-d6f3-4f30-9192-6a8e69027d6a.trivial.rst | 0 ...46-e6b0-48b1-8b26-1145d611d082.trivial.rst | 0 ...d5-38b6-48cc-8136-0c32d3ace838.trivial.rst | 0 ...86bc866fdc4257a445e0df09dd7e64.trivial.rst | 0 ...cd-059a-4ea4-b02e-343f8b51aad5.trivial.rst | 0 ...cc-9fc9-4762-914e-34014e8d09bf.trivial.rst | 0 ...60-675c-4104-9825-39d1ee0a20b7.trivial.rst | 0 ...2f-aef7-4323-8332-819f0be13d79.trivial.rst | 0 ...0a-f673-4c1b-9959-3196b6c000e9.trivial.rst | 0 news/msgpack.vendor.rst | 1 - news/requests.vendor.rst | 1 - src/pip/__init__.py | 2 +- 49 files changed, 47 insertions(+), 23 deletions(-) delete mode 100644 news/0dd6ac6d-b127-4551-a404-404b0ee8dcd3.trivial.rst delete mode 100644 news/1170af15-1373-4226-a1ec-efe54b7ad480.trivial.rst delete mode 100644 news/1884.feature.rst delete mode 100644 news/205edb4f-502c-4213-b8b8-c9173718e8ab.trivial.rst delete mode 100644 news/24193261-eaf9-4117-a1a9-d5bb7f93b447.trivial.rst delete mode 100644 news/275aa0e8-ebb1-4eaf-aee0-e5582a8c5d58.trivial.rst delete mode 100644 news/2905cccb-2fe8-4b0d-8734-303510a7e4ce.trivial.rst delete mode 100644 news/2b5d1433-ec03-4b33-8cf1-ff76baa3690e.trivial.rst delete mode 100644 news/30e2240e-e268-4519-bee7-6f79bc4cf489.trivial.rst delete mode 100644 news/3ba38d39-9189-4bc3-bc87-bf598f1c1064.trivial.rst delete mode 100644 news/43602ba6-8a59-425c-9a97-9c8e87e28ddb.trivial.rst delete mode 100644 news/49254991-9583-470e-a263-b196acf4072b.trivial.rst delete mode 100644 news/4a85b5f1-5f9a-4f6b-8960-3334570ae591.trivial.rst delete mode 100644 news/4ec6e91c-91d6-475c-959a-83485cafa4b2.trivial.rst delete mode 100644 news/54754cb1-2151-45c3-baa0-b87e50d7e56d.trivial.rst delete mode 100644 news/6148.removal.rst delete mode 100644 news/738a71b0-98f9-4e1f-a541-af95fb990af9.trivial.rst delete mode 100644 news/7502.removal.rst delete mode 100644 news/7554.removal.rst delete mode 100644 news/7ced09a1-9af6-4190-8249-05a6328e379e.trivial.rst delete mode 100644 news/7edb0afc-938e-457b-ae6e-0905e7443b2f.trivial.rst delete mode 100644 news/80B9D718-3054-41C7-B920-78348DFD01A6.trivial.rst delete mode 100644 news/857785f2-1d4e-4067-9b4b-acc6ae741aef.trivial.rst delete mode 100644 news/86c319cb-0539-41a0-871b-4ffe72765f6f.trivial.rst delete mode 100644 news/8802.removal.rst delete mode 100644 news/8a225e3d-998e-4924-92e6-2ab2173159f8.trivial.rst delete mode 100644 news/9172.doc.rst delete mode 100644 news/9186.feature.rst delete mode 100644 news/9189.removal.rst delete mode 100644 news/9273.bugfix.rst delete mode 100644 news/9337.bugfix.rst delete mode 100644 news/9338.removal.rst delete mode 100644 news/9343.doc.rst delete mode 100644 news/9DE59242-2198-4760-B5A7-B5A6BB98ECA2.trivial.rst delete mode 100644 news/a13640e3-bfd3-46ae-b4b6-bcb9784303b4.trivial.rst delete mode 100644 news/a9950589-8b92-4ec1-a3a1-a6657cf6fd5b.trivial.rst delete mode 100644 news/ae7bdce7-d6f3-4f30-9192-6a8e69027d6a.trivial.rst delete mode 100644 news/b034ad46-e6b0-48b1-8b26-1145d611d082.trivial.rst delete mode 100644 news/b5e475d5-38b6-48cc-8136-0c32d3ace838.trivial.rst delete mode 100644 news/bb86bc866fdc4257a445e0df09dd7e64.trivial.rst delete mode 100644 news/d96bbdcd-059a-4ea4-b02e-343f8b51aad5.trivial.rst delete mode 100644 news/dc9e5ecc-9fc9-4762-914e-34014e8d09bf.trivial.rst delete mode 100644 news/ea24fc60-675c-4104-9825-39d1ee0a20b7.trivial.rst delete mode 100644 news/f0af302f-aef7-4323-8332-819f0be13d79.trivial.rst delete mode 100644 news/f0b01f0a-f673-4c1b-9959-3196b6c000e9.trivial.rst delete mode 100644 news/msgpack.vendor.rst delete mode 100644 news/requests.vendor.rst diff --git a/NEWS.rst b/NEWS.rst index 18cc7b70427..83608ac9c6c 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,49 @@ +21.0 (2021-01-23) +================= + +Deprecations and Removals +------------------------- + +- Drop support for Python 2. (`#6148 <https://github.com/pypa/pip/issues/6148>`_) +- Remove support for legacy wheel cache entries that were created with pip + versions older than 20.0. (`#7502 <https://github.com/pypa/pip/issues/7502>`_) +- Remove support for VCS pseudo URLs editable requirements. It was emitting + deprecation warning since version 20.0. (`#7554 <https://github.com/pypa/pip/issues/7554>`_) +- Modernise the codebase after Python 2. (`#8802 <https://github.com/pypa/pip/issues/8802>`_) +- Drop support for Python 3.5. (`#9189 <https://github.com/pypa/pip/issues/9189>`_) +- Remove the VCS export feature that was used only with editable VCS + requirements and had correctness issues. (`#9338 <https://github.com/pypa/pip/issues/9338>`_) + +Features +-------- + +- Add ``--ignore-requires-python`` support to pip download. (`#1884 <https://github.com/pypa/pip/issues/1884>`_) +- New resolver: Error message shown when a wheel contains inconsistent metadata + is made more helpful by including both values from the file name and internal + metadata. (`#9186 <https://github.com/pypa/pip/issues/9186>`_) + +Bug Fixes +--------- + +- Fix a regression that made ``pip wheel`` do a VCS export instead of a VCS clone + for editable requirements. This broke VCS requirements that need the VCS + information to build correctly. (`#9273 <https://github.com/pypa/pip/issues/9273>`_) +- Fix ``pip download`` of editable VCS requirements that need VCS information + to build correctly. (`#9337 <https://github.com/pypa/pip/issues/9337>`_) + +Vendored Libraries +------------------ + +- Upgrade msgpack to 1.0.2. +- Upgrade requests to 2.25.1. + +Improved Documentation +---------------------- + +- Render the unreleased pip version change notes on the news page in docs. (`#9172 <https://github.com/pypa/pip/issues/9172>`_) +- Fix broken email link in docs feedback banners. (`#9343 <https://github.com/pypa/pip/issues/9343>`_) + + .. note You should *NOT* be adding new change log entries to this file, this diff --git a/news/0dd6ac6d-b127-4551-a404-404b0ee8dcd3.trivial.rst b/news/0dd6ac6d-b127-4551-a404-404b0ee8dcd3.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/1170af15-1373-4226-a1ec-efe54b7ad480.trivial.rst b/news/1170af15-1373-4226-a1ec-efe54b7ad480.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/1884.feature.rst b/news/1884.feature.rst deleted file mode 100644 index 4b0b4180c35..00000000000 --- a/news/1884.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Add ``--ignore-requires-python`` support to pip download. diff --git a/news/205edb4f-502c-4213-b8b8-c9173718e8ab.trivial.rst b/news/205edb4f-502c-4213-b8b8-c9173718e8ab.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/24193261-eaf9-4117-a1a9-d5bb7f93b447.trivial.rst b/news/24193261-eaf9-4117-a1a9-d5bb7f93b447.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/275aa0e8-ebb1-4eaf-aee0-e5582a8c5d58.trivial.rst b/news/275aa0e8-ebb1-4eaf-aee0-e5582a8c5d58.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/2905cccb-2fe8-4b0d-8734-303510a7e4ce.trivial.rst b/news/2905cccb-2fe8-4b0d-8734-303510a7e4ce.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/2b5d1433-ec03-4b33-8cf1-ff76baa3690e.trivial.rst b/news/2b5d1433-ec03-4b33-8cf1-ff76baa3690e.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/30e2240e-e268-4519-bee7-6f79bc4cf489.trivial.rst b/news/30e2240e-e268-4519-bee7-6f79bc4cf489.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/3ba38d39-9189-4bc3-bc87-bf598f1c1064.trivial.rst b/news/3ba38d39-9189-4bc3-bc87-bf598f1c1064.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/43602ba6-8a59-425c-9a97-9c8e87e28ddb.trivial.rst b/news/43602ba6-8a59-425c-9a97-9c8e87e28ddb.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/49254991-9583-470e-a263-b196acf4072b.trivial.rst b/news/49254991-9583-470e-a263-b196acf4072b.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/4a85b5f1-5f9a-4f6b-8960-3334570ae591.trivial.rst b/news/4a85b5f1-5f9a-4f6b-8960-3334570ae591.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/4ec6e91c-91d6-475c-959a-83485cafa4b2.trivial.rst b/news/4ec6e91c-91d6-475c-959a-83485cafa4b2.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/54754cb1-2151-45c3-baa0-b87e50d7e56d.trivial.rst b/news/54754cb1-2151-45c3-baa0-b87e50d7e56d.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/6148.removal.rst b/news/6148.removal.rst deleted file mode 100644 index cf6d85e70ba..00000000000 --- a/news/6148.removal.rst +++ /dev/null @@ -1 +0,0 @@ -Drop support for Python 2. diff --git a/news/738a71b0-98f9-4e1f-a541-af95fb990af9.trivial.rst b/news/738a71b0-98f9-4e1f-a541-af95fb990af9.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/7502.removal.rst b/news/7502.removal.rst deleted file mode 100644 index 9f03366ed8e..00000000000 --- a/news/7502.removal.rst +++ /dev/null @@ -1,2 +0,0 @@ -Remove support for legacy wheel cache entries that were created with pip -versions older than 20.0. diff --git a/news/7554.removal.rst b/news/7554.removal.rst deleted file mode 100644 index d5037d5fdb9..00000000000 --- a/news/7554.removal.rst +++ /dev/null @@ -1,2 +0,0 @@ -Remove support for VCS pseudo URLs editable requirements. It was emitting -deprecation warning since version 20.0. diff --git a/news/7ced09a1-9af6-4190-8249-05a6328e379e.trivial.rst b/news/7ced09a1-9af6-4190-8249-05a6328e379e.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/7edb0afc-938e-457b-ae6e-0905e7443b2f.trivial.rst b/news/7edb0afc-938e-457b-ae6e-0905e7443b2f.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/80B9D718-3054-41C7-B920-78348DFD01A6.trivial.rst b/news/80B9D718-3054-41C7-B920-78348DFD01A6.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/857785f2-1d4e-4067-9b4b-acc6ae741aef.trivial.rst b/news/857785f2-1d4e-4067-9b4b-acc6ae741aef.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/86c319cb-0539-41a0-871b-4ffe72765f6f.trivial.rst b/news/86c319cb-0539-41a0-871b-4ffe72765f6f.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/8802.removal.rst b/news/8802.removal.rst deleted file mode 100644 index 79d8e508166..00000000000 --- a/news/8802.removal.rst +++ /dev/null @@ -1 +0,0 @@ -Modernise the codebase after Python 2. diff --git a/news/8a225e3d-998e-4924-92e6-2ab2173159f8.trivial.rst b/news/8a225e3d-998e-4924-92e6-2ab2173159f8.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/9172.doc.rst b/news/9172.doc.rst deleted file mode 100644 index fc0063766b2..00000000000 --- a/news/9172.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Render the unreleased pip version change notes on the news page in docs. diff --git a/news/9186.feature.rst b/news/9186.feature.rst deleted file mode 100644 index 175b5a883ac..00000000000 --- a/news/9186.feature.rst +++ /dev/null @@ -1,3 +0,0 @@ -New resolver: Error message shown when a wheel contains inconsistent metadata -is made more helpful by including both values from the file name and internal -metadata. diff --git a/news/9189.removal.rst b/news/9189.removal.rst deleted file mode 100644 index 79928cbb15a..00000000000 --- a/news/9189.removal.rst +++ /dev/null @@ -1 +0,0 @@ -Drop support for Python 3.5. diff --git a/news/9273.bugfix.rst b/news/9273.bugfix.rst deleted file mode 100644 index e729ea29446..00000000000 --- a/news/9273.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -Fix a regression that made ``pip wheel`` do a VCS export instead of a VCS clone -for editable requirements. This broke VCS requirements that need the VCS -information to build correctly. diff --git a/news/9337.bugfix.rst b/news/9337.bugfix.rst deleted file mode 100644 index e9d08c3ad82..00000000000 --- a/news/9337.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Fix ``pip download`` of editable VCS requirements that need VCS information -to build correctly. diff --git a/news/9338.removal.rst b/news/9338.removal.rst deleted file mode 100644 index 6d3b666e53f..00000000000 --- a/news/9338.removal.rst +++ /dev/null @@ -1,2 +0,0 @@ -Remove the VCS export feature that was used only with editable VCS -requirements and had correctness issues. diff --git a/news/9343.doc.rst b/news/9343.doc.rst deleted file mode 100644 index 1e4f91aec4c..00000000000 --- a/news/9343.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Fix broken email link in docs feedback banners. diff --git a/news/9DE59242-2198-4760-B5A7-B5A6BB98ECA2.trivial.rst b/news/9DE59242-2198-4760-B5A7-B5A6BB98ECA2.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/a13640e3-bfd3-46ae-b4b6-bcb9784303b4.trivial.rst b/news/a13640e3-bfd3-46ae-b4b6-bcb9784303b4.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/a9950589-8b92-4ec1-a3a1-a6657cf6fd5b.trivial.rst b/news/a9950589-8b92-4ec1-a3a1-a6657cf6fd5b.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/ae7bdce7-d6f3-4f30-9192-6a8e69027d6a.trivial.rst b/news/ae7bdce7-d6f3-4f30-9192-6a8e69027d6a.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/b034ad46-e6b0-48b1-8b26-1145d611d082.trivial.rst b/news/b034ad46-e6b0-48b1-8b26-1145d611d082.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/b5e475d5-38b6-48cc-8136-0c32d3ace838.trivial.rst b/news/b5e475d5-38b6-48cc-8136-0c32d3ace838.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/bb86bc866fdc4257a445e0df09dd7e64.trivial.rst b/news/bb86bc866fdc4257a445e0df09dd7e64.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/d96bbdcd-059a-4ea4-b02e-343f8b51aad5.trivial.rst b/news/d96bbdcd-059a-4ea4-b02e-343f8b51aad5.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/dc9e5ecc-9fc9-4762-914e-34014e8d09bf.trivial.rst b/news/dc9e5ecc-9fc9-4762-914e-34014e8d09bf.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/ea24fc60-675c-4104-9825-39d1ee0a20b7.trivial.rst b/news/ea24fc60-675c-4104-9825-39d1ee0a20b7.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/f0af302f-aef7-4323-8332-819f0be13d79.trivial.rst b/news/f0af302f-aef7-4323-8332-819f0be13d79.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/f0b01f0a-f673-4c1b-9959-3196b6c000e9.trivial.rst b/news/f0b01f0a-f673-4c1b-9959-3196b6c000e9.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/msgpack.vendor.rst b/news/msgpack.vendor.rst deleted file mode 100644 index 14a06e1c6bc..00000000000 --- a/news/msgpack.vendor.rst +++ /dev/null @@ -1 +0,0 @@ -Upgrade msgpack to 1.0.2. diff --git a/news/requests.vendor.rst b/news/requests.vendor.rst deleted file mode 100644 index 9c9dee7b4a4..00000000000 --- a/news/requests.vendor.rst +++ /dev/null @@ -1 +0,0 @@ -Upgrade requests to 2.25.1. diff --git a/src/pip/__init__.py b/src/pip/__init__.py index ae0fe9a9f24..38ebba574c9 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -4,7 +4,7 @@ from typing import List, Optional -__version__ = "21.0.dev0" +__version__ = "21.0" def main(args=None): From b944419af1a6b605b8e8441a36acb54427df10d7 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Sat, 23 Jan 2021 14:42:05 +0000 Subject: [PATCH 2917/3170] Bump for development --- src/pip/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/__init__.py b/src/pip/__init__.py index 38ebba574c9..9de6dc0f2a6 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -4,7 +4,7 @@ from typing import List, Optional -__version__ = "21.0" +__version__ = "21.1.dev0" def main(args=None): From 431d2576dd83aacb0b46cad14de511880d9e4701 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <hugovk@users.noreply.github.com> Date: Sat, 23 Jan 2021 17:49:14 +0200 Subject: [PATCH 2918/3170] Docs: update to past tense now pip 21.0 is out --- README.rst | 2 +- docs/html/index.rst | 2 +- docs/html/user_guide.rst | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/README.rst b/README.rst index a15de466b7f..222b055eb4c 100644 --- a/README.rst +++ b/README.rst @@ -21,7 +21,7 @@ We release updates regularly, with a new version every 3 months. Find more detai In pip 20.3, we've `made a big improvement to the heart of pip`_; `learn more`_. We want your input, so `sign up for our user experience research studies`_ to help us do it right. -**Note**: pip 21.0, in January 2021, will remove Python 2 support, per pip's `Python 2 support policy`_. Please migrate to Python 3. +**Note**: pip 21.0, in January 2021, removed Python 2 support, per pip's `Python 2 support policy`_. Please migrate to Python 3. If you find bugs, need help, or want to talk to the developers, please use our mailing lists or chat rooms: diff --git a/docs/html/index.rst b/docs/html/index.rst index 1ac460bd9d9..b92a23e020a 100644 --- a/docs/html/index.rst +++ b/docs/html/index.rst @@ -25,7 +25,7 @@ Please take a look at our documentation for how to install and use pip: .. warning:: - pip 21.0, in January 2021, will remove Python 2 support, per pip's + pip 21.0, in January 2021, removed Python 2 support, per pip's :ref:`Python 2 Support` policy. Please migrate to Python 3. If you find bugs, need help, or want to talk to the developers, please use our mailing lists or chat rooms: diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 415c9b1e71c..92887885baf 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -1699,7 +1699,7 @@ errors. Specifically: Per our :ref:`Python 2 Support` policy, pip 20.3 users who are using Python 2 will use the legacy resolver by default. Python 2 users should upgrade to Python 3 as soon as possible, since in pip 21.0 in -January 2021, pip will drop support for Python 2 altogether. +January 2021, pip dropped support for Python 2 altogether. How to upgrade and migrate From e25b1834594874745575f221a056a228042aaff0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sun, 24 Jan 2021 15:25:37 +0100 Subject: [PATCH 2919/3170] Update pre-commit hooks --- .pre-commit-config.yaml | 10 +++++----- src/pip/_internal/locations.py | 2 +- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 636fdfd3d4c..49676b5cf0f 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -2,7 +2,7 @@ exclude: 'src/pip/_vendor/' repos: - repo: https://github.com/pre-commit/pre-commit-hooks - rev: v3.2.0 + rev: v3.4.0 hooks: - id: check-builtin-literals - id: check-added-large-files @@ -53,7 +53,7 @@ repos: - repo: https://gitlab.com/pycqa/flake8 - rev: 3.8.3 + rev: 3.8.4 hooks: - id: flake8 additional_dependencies: [ @@ -63,20 +63,20 @@ repos: exclude: tests/data - repo: https://github.com/PyCQA/isort - rev: 5.5.3 + rev: 5.7.0 hooks: - id: isort files: \.py$ - repo: https://github.com/pre-commit/mirrors-mypy - rev: v0.790 + rev: v0.800 hooks: - id: mypy exclude: docs|tests args: ["--pretty"] - repo: https://github.com/pre-commit/pygrep-hooks - rev: v1.6.0 + rev: v1.7.0 hooks: - id: python-no-log-warn - id: python-no-eval diff --git a/src/pip/_internal/locations.py b/src/pip/_internal/locations.py index 2a903297733..88b9e43cd86 100644 --- a/src/pip/_internal/locations.py +++ b/src/pip/_internal/locations.py @@ -8,7 +8,7 @@ import site import sys import sysconfig -from distutils.command.install import SCHEME_KEYS # type: ignore +from distutils.command.install import SCHEME_KEYS from distutils.command.install import install as distutils_install_command from pip._internal.models.scheme import Scheme From fe3aec0f5e4a15c4110eb1974497371f26c4fec3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sun, 24 Jan 2021 15:39:51 +0100 Subject: [PATCH 2920/3170] Better way to run slow linters in CI only Taking advantage of pre-commit's manual stage. --- .pre-commit-config-slow.yaml | 7 ------- .pre-commit-config.yaml | 6 ++++++ MANIFEST.in | 1 - noxfile.py | 4 +--- tox.ini | 3 +-- 5 files changed, 8 insertions(+), 13 deletions(-) delete mode 100644 .pre-commit-config-slow.yaml diff --git a/.pre-commit-config-slow.yaml b/.pre-commit-config-slow.yaml deleted file mode 100644 index 2179c665769..00000000000 --- a/.pre-commit-config-slow.yaml +++ /dev/null @@ -1,7 +0,0 @@ -# Slow pre-commit checks we don't want to run locally with each commit. - -repos: -- repo: https://github.com/mgedmin/check-manifest - rev: '0.43' - hooks: - - id: check-manifest diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 49676b5cf0f..164de60b05d 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -93,3 +93,9 @@ repos: entry: NEWS fragment files must be named *.(process|removal|feature|bugfix|vendor|doc|trivial).rst exclude: ^news/(.gitignore|.*\.(process|removal|feature|bugfix|vendor|doc|trivial).rst) files: ^news/ + +- repo: https://github.com/mgedmin/check-manifest + rev: '0.46' + hooks: + - id: check-manifest + stages: [manual] diff --git a/MANIFEST.in b/MANIFEST.in index 2cf636ce3f7..24d4553785b 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -17,7 +17,6 @@ exclude .appveyor.yml exclude .travis.yml exclude .readthedocs.yml exclude .pre-commit-config.yaml -exclude .pre-commit-config-slow.yaml exclude tox.ini exclude noxfile.py diff --git a/noxfile.py b/noxfile.py index de4daf8a79c..e89e73a8d0f 100644 --- a/noxfile.py +++ b/noxfile.py @@ -147,11 +147,9 @@ def lint(session): args = session.posargs + ["--all-files"] else: args = ["--all-files", "--show-diff-on-failure"] + args.append("--hook-stage=manual") session.run("pre-commit", "run", *args) - session.run( - "pre-commit", "run", "-c", ".pre-commit-config-slow.yaml", *args - ) @nox.session diff --git a/tox.ini b/tox.ini index 7fcc2c664a2..79586eba8c3 100644 --- a/tox.ini +++ b/tox.ini @@ -63,8 +63,7 @@ skip_install = True commands_pre = deps = pre-commit commands = - pre-commit run [] --all-files --show-diff-on-failure - pre-commit run [] -c .pre-commit-config-slow.yaml --all-files --show-diff-on-failure + pre-commit run [] --all-files --show-diff-on-failure --hook-stage=manual [testenv:vendoring] basepython = python3 From 7b0865164c0311d999c354a5ac12210ec52b27f5 Mon Sep 17 00:00:00 2001 From: XAMES3 <44119552+xames3@users.noreply.github.com> Date: Mon, 25 Jan 2021 00:34:50 +0530 Subject: [PATCH 2921/3170] Update copyright year from 2020 to 2021 --- LICENSE.txt | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/LICENSE.txt b/LICENSE.txt index 75eb0fd80b0..00addc2725a 100644 --- a/LICENSE.txt +++ b/LICENSE.txt @@ -1,4 +1,4 @@ -Copyright (c) 2008-2020 The pip developers (see AUTHORS.txt file) +Copyright (c) 2008-2021 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 From c6b32ae24f764128263d212a32f48f38cf2888a2 Mon Sep 17 00:00:00 2001 From: Greg Roodt <greg@canva.com> Date: Mon, 25 Jan 2021 12:56:02 +1100 Subject: [PATCH 2922/3170] Exclude empty drafts. --- docs/html/conf.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/html/conf.py b/docs/html/conf.py index f81cb6b7dac..2efb7135892 100644 --- a/docs/html/conf.py +++ b/docs/html/conf.py @@ -320,6 +320,6 @@ def to_document_name(path, base_dir): # -- Options for towncrier_draft extension ----------------------------------- towncrier_draft_autoversion_mode = 'draft' # or: 'sphinx-release', 'sphinx-version' -towncrier_draft_include_empty = True +towncrier_draft_include_empty = False towncrier_draft_working_directory = pathlib.Path(docs_dir).parent # Not yet supported: towncrier_draft_config_path = 'pyproject.toml' # relative to cwd From c72631a5f107df75c835d49258cdd24cbd15c54a Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Mon, 25 Jan 2021 07:58:07 +0800 Subject: [PATCH 2923/3170] Pin setuptools under 52 for easy_install tests --- tests/functional/test_uninstall.py | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/tests/functional/test_uninstall.py b/tests/functional/test_uninstall.py index d3c6a392ec4..878e713ed9e 100644 --- a/tests/functional/test_uninstall.py +++ b/tests/functional/test_uninstall.py @@ -63,6 +63,9 @@ def test_basic_uninstall_with_scripts(script): Uninstall an easy_installed package with scripts. """ + # setuptools 52 removed easy_install. + script.pip("install", "setuptools==51.3.3", use_module=True) + result = script.easy_install('PyLogo', expect_stderr=True) easy_install_pth = script.site_packages / 'easy-install.pth' pylogo = sys.platform == 'win32' and 'pylogo' or 'PyLogo' @@ -81,6 +84,9 @@ def test_uninstall_easy_install_after_import(script): Uninstall an easy_installed package after it's been imported """ + # setuptools 52 removed easy_install. + script.pip("install", "setuptools==51.3.3", use_module=True) + result = script.easy_install('INITools==0.2', expect_stderr=True) # the import forces the generation of __pycache__ if the version of python # supports it @@ -104,6 +110,9 @@ def test_uninstall_trailing_newline(script): lacks a trailing newline """ + # setuptools 52 removed easy_install. + script.pip("install", "setuptools==51.3.3", use_module=True) + script.easy_install('INITools==0.2', expect_stderr=True) script.easy_install('PyLogo', expect_stderr=True) easy_install_pth = script.site_packages_path / 'easy-install.pth' @@ -285,8 +294,10 @@ def test_uninstall_easy_installed_console_scripts(script): """ Test uninstalling package with console_scripts that is easy_installed. """ - # setuptools >= 42.0.0 deprecates easy_install and prints a warning when - # used + # setuptools 52 removed easy_install and prints a warning after 42 when + # the command is used. + script.pip("install", "setuptools==51.3.3", use_module=True) + result = script.easy_install('discover', allow_stderr_warning=True) result.did_create(script.bin / 'discover' + script.exe) result2 = script.pip('uninstall', 'discover', '-y') From 68a86c53c7719cac5129561c72ed5b74f8c30263 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 27 Jan 2021 19:40:01 +0800 Subject: [PATCH 2924/3170] Failing test for repeated fetch --- tests/functional/test_new_resolver.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 6943fd7de80..e2688f05b12 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -1253,3 +1253,28 @@ def test_new_resolver_lazy_fetch_candidates(script, upgrade): # But should reach there in the best route possible, without trying # candidates it does not need to. assert "myuberpkg-2" not in result.stdout, str(result) + + +def test_new_resolver_no_fetch_no_satisfying(script): + create_basic_wheel_for_package(script, "myuberpkg", "1") + + # Install the package. This should emit a "Processing" message for + # fetching the distribution from the --find-links page. + result = script.pip( + "install", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "myuberpkg", + ) + assert "Processing ./myuberpkg-1-" in result.stdout, str(result) + + # Try to upgrade the package. This should NOT emit the "Processing" + # message because the currently installed version is latest. + result = script.pip( + "install", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "--upgrade", + "myuberpkg", + ) + assert "Processing ./myuberpkg-1-" not in result.stdout, str(result) From 2e70ec075126d4928e17c32d9b63b3839f8b0b74 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 27 Jan 2021 19:47:35 +0800 Subject: [PATCH 2925/3170] Create the candidate lazily to avoid download --- news/9516.bugfix.rst | 3 + .../resolution/resolvelib/factory.py | 19 ++--- .../resolution/resolvelib/found_candidates.py | 82 ++++++++++++++----- 3 files changed, 74 insertions(+), 30 deletions(-) create mode 100644 news/9516.bugfix.rst diff --git a/news/9516.bugfix.rst b/news/9516.bugfix.rst new file mode 100644 index 00000000000..9b9cc812614 --- /dev/null +++ b/news/9516.bugfix.rst @@ -0,0 +1,3 @@ +New resolver: Download and prepare a distribution only at the last possible +moment to avoid unnecessary network access when the same version is already +installed locally. diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index bfaa0520ace..be0729e3994 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -1,3 +1,4 @@ +import functools import logging from pip._vendor.packaging.utils import canonicalize_name @@ -65,6 +66,7 @@ from .base import Candidate, Requirement from .candidates import BaseCandidate + from .found_candidates import IndexCandidateInfo C = TypeVar("C") Cache = Dict[Link, C] @@ -213,8 +215,8 @@ def _iter_found_candidates( template=template, ) - def iter_index_candidates(): - # type: () -> Iterator[Candidate] + def iter_index_candidate_infos(): + # type: () -> Iterator[IndexCandidateInfo] result = self._finder.find_best_candidate( project_name=name, specifier=specifier, @@ -228,26 +230,21 @@ def iter_index_candidates(): all_yanked = all(ican.link.is_yanked for ican in icans) # PackageFinder returns earlier versions first, so we reverse. - versions_found = set() # type: Set[_BaseVersion] for ican in reversed(icans): if not all_yanked and ican.link.is_yanked: continue - if ican.version in versions_found: - continue - candidate = self._make_candidate_from_link( + func = functools.partial( + self._make_candidate_from_link, link=ican.link, extras=extras, template=template, name=name, version=ican.version, ) - if candidate is None: - continue - yield candidate - versions_found.add(ican.version) + yield ican.version, func return FoundCandidates( - iter_index_candidates, + iter_index_candidate_infos, installed_candidate, prefers_installed, ) diff --git a/src/pip/_internal/resolution/resolvelib/found_candidates.py b/src/pip/_internal/resolution/resolvelib/found_candidates.py index c3f95c1d41d..5d4b4654988 100644 --- a/src/pip/_internal/resolution/resolvelib/found_candidates.py +++ b/src/pip/_internal/resolution/resolvelib/found_candidates.py @@ -9,20 +9,62 @@ """ import functools -import itertools from pip._vendor.six.moves import collections_abc # type: ignore from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: - from typing import Callable, Iterator, Optional + from typing import Callable, Iterator, Optional, Set, Tuple + + from pip._vendor.packaging.version import _BaseVersion from .base import Candidate + IndexCandidateInfo = Tuple[_BaseVersion, Callable[[], Optional[Candidate]]] + + +def _iter_built(infos): + # type: (Iterator[IndexCandidateInfo]) -> Iterator[Candidate] + """Iterator for ``FoundCandidates``. + + This iterator is used the package is not already installed. Candidates + from index come later in their normal ordering. + """ + versions_found = set() # type: Set[_BaseVersion] + for version, func in infos: + if version in versions_found: + continue + candidate = func() + if candidate is None: + continue + yield candidate + versions_found.add(version) + + +def _iter_built_with_prepended(installed, infos): + # type: (Candidate, Iterator[IndexCandidateInfo]) -> Iterator[Candidate] + """Iterator for ``FoundCandidates``. + + This iterator is used when the resolver prefers the already-installed + candidate and NOT to upgrade. The installed candidate is therefore + always yielded first, and candidates from index come later in their + normal ordering, except skipped when the version is already installed. + """ + yield installed + versions_found = {installed.version} # type: Set[_BaseVersion] + for version, func in infos: + if version in versions_found: + continue + candidate = func() + if candidate is None: + continue + yield candidate + versions_found.add(version) + -def _insert_installed(installed, others): - # type: (Candidate, Iterator[Candidate]) -> Iterator[Candidate] +def _iter_built_with_inserted(installed, infos): + # type: (Candidate, Iterator[IndexCandidateInfo]) -> Iterator[Candidate] """Iterator for ``FoundCandidates``. This iterator is used when the resolver prefers to upgrade an @@ -33,16 +75,22 @@ def _insert_installed(installed, others): the installed candidate exactly once before we start yielding older or equivalent candidates, or after all other candidates if they are all newer. """ - installed_yielded = False - for candidate in others: + versions_found = set() # type: Set[_BaseVersion] + for version, func in infos: + if version in versions_found: + continue # If the installed candidate is better, yield it first. - if not installed_yielded and installed.version >= candidate.version: + if installed.version >= version: yield installed - installed_yielded = True + versions_found.add(installed.version) + candidate = func() + if candidate is None: + continue yield candidate + versions_found.add(version) # If the installed candidate is older than all other candidates. - if not installed_yielded: + if installed.version not in versions_found: yield installed @@ -56,11 +104,11 @@ class FoundCandidates(collections_abc.Sequence): """ def __init__( self, - get_others, # type: Callable[[], Iterator[Candidate]] + get_infos, # type: Callable[[], Iterator[IndexCandidateInfo]] installed, # type: Optional[Candidate] prefers_installed, # type: bool ): - self._get_others = get_others + self._get_infos = get_infos self._installed = installed self._prefers_installed = prefers_installed @@ -73,16 +121,12 @@ def __getitem__(self, index): def __iter__(self): # type: () -> Iterator[Candidate] + infos = self._get_infos() if not self._installed: - return self._get_others() - others = ( - candidate - for candidate in self._get_others() - if candidate.version != self._installed.version - ) + return _iter_built(infos) if self._prefers_installed: - return itertools.chain([self._installed], others) - return _insert_installed(self._installed, others) + return _iter_built_with_prepended(self._installed, infos) + return _iter_built_with_inserted(self._installed, infos) def __len__(self): # type: () -> int From 7d43ec5166088ed02c7339e1aa700adca44856b4 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 27 Jan 2021 21:55:46 +0800 Subject: [PATCH 2926/3170] More permissive output check --- tests/functional/test_new_resolver.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index e2688f05b12..4d2acbb23b4 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -1266,7 +1266,7 @@ def test_new_resolver_no_fetch_no_satisfying(script): "--find-links", script.scratch_path, "myuberpkg", ) - assert "Processing ./myuberpkg-1-" in result.stdout, str(result) + assert "Processing " in result.stdout, str(result) # Try to upgrade the package. This should NOT emit the "Processing" # message because the currently installed version is latest. @@ -1277,4 +1277,4 @@ def test_new_resolver_no_fetch_no_satisfying(script): "--upgrade", "myuberpkg", ) - assert "Processing ./myuberpkg-1-" not in result.stdout, str(result) + assert "Processing " not in result.stdout, str(result) From c7ace4de391f9970792c3bb83c648b7294b7b720 Mon Sep 17 00:00:00 2001 From: Henry Schreiner <henryschreineriii@gmail.com> Date: Fri, 29 Jan 2021 15:47:20 -0500 Subject: [PATCH 2927/3170] Upgrade packaging to 20.9 --- news/packaging.vendor.rst | 1 + src/pip/_vendor/packaging/__about__.py | 2 +- src/pip/_vendor/packaging/markers.py | 16 +++-- src/pip/_vendor/packaging/requirements.py | 17 +++-- src/pip/_vendor/packaging/specifiers.py | 4 +- src/pip/_vendor/packaging/tags.py | 26 ++++++-- src/pip/_vendor/packaging/utils.py | 75 ++++++++++++++++++++++- src/pip/_vendor/vendor.txt | 2 +- 8 files changed, 123 insertions(+), 20 deletions(-) create mode 100644 news/packaging.vendor.rst diff --git a/news/packaging.vendor.rst b/news/packaging.vendor.rst new file mode 100644 index 00000000000..ccaa7a9727f --- /dev/null +++ b/news/packaging.vendor.rst @@ -0,0 +1 @@ +Upgrade packaging to 20.9 diff --git a/src/pip/_vendor/packaging/__about__.py b/src/pip/_vendor/packaging/__about__.py index 2d39193b051..4c43a968c8a 100644 --- a/src/pip/_vendor/packaging/__about__.py +++ b/src/pip/_vendor/packaging/__about__.py @@ -18,7 +18,7 @@ __summary__ = "Core utilities for Python packages" __uri__ = "https://github.com/pypa/packaging" -__version__ = "20.8" +__version__ = "20.9" __author__ = "Donald Stufft and individual contributors" __email__ = "donald@stufft.io" diff --git a/src/pip/_vendor/packaging/markers.py b/src/pip/_vendor/packaging/markers.py index ed642b01fcc..69a60cf1e95 100644 --- a/src/pip/_vendor/packaging/markers.py +++ b/src/pip/_vendor/packaging/markers.py @@ -8,13 +8,21 @@ import platform import sys -from pip._vendor.pyparsing import ParseException, ParseResults, stringStart, stringEnd -from pip._vendor.pyparsing import ZeroOrMore, Group, Forward, QuotedString -from pip._vendor.pyparsing import Literal as L # noqa +from pip._vendor.pyparsing import ( # noqa: N817 + Forward, + Group, + Literal as L, + ParseException, + ParseResults, + QuotedString, + ZeroOrMore, + stringEnd, + stringStart, +) from ._compat import string_types from ._typing import TYPE_CHECKING -from .specifiers import Specifier, InvalidSpecifier +from .specifiers import InvalidSpecifier, Specifier if TYPE_CHECKING: # pragma: no cover from typing import Any, Callable, Dict, List, Optional, Tuple, Union diff --git a/src/pip/_vendor/packaging/requirements.py b/src/pip/_vendor/packaging/requirements.py index df7f41d2c08..c2a7fdacd59 100644 --- a/src/pip/_vendor/packaging/requirements.py +++ b/src/pip/_vendor/packaging/requirements.py @@ -3,13 +3,22 @@ # for complete details. from __future__ import absolute_import, division, print_function -import string import re +import string import sys -from pip._vendor.pyparsing import stringStart, stringEnd, originalTextFor, ParseException -from pip._vendor.pyparsing import ZeroOrMore, Word, Optional, Regex, Combine -from pip._vendor.pyparsing import Literal as L # noqa +from pip._vendor.pyparsing import ( # noqa: N817 + Combine, + Literal as L, + Optional, + ParseException, + Regex, + Word, + ZeroOrMore, + originalTextFor, + stringEnd, + stringStart, +) from ._typing import TYPE_CHECKING from .markers import MARKER_EXPR, Marker diff --git a/src/pip/_vendor/packaging/specifiers.py b/src/pip/_vendor/packaging/specifiers.py index a42cbfef332..a6a83c1fe93 100644 --- a/src/pip/_vendor/packaging/specifiers.py +++ b/src/pip/_vendor/packaging/specifiers.py @@ -12,10 +12,10 @@ from ._compat import string_types, with_metaclass from ._typing import TYPE_CHECKING from .utils import canonicalize_version -from .version import Version, LegacyVersion, parse +from .version import LegacyVersion, Version, parse if TYPE_CHECKING: # pragma: no cover - from typing import List, Dict, Union, Iterable, Iterator, Optional, Callable, Tuple + from typing import Callable, Dict, Iterable, Iterator, List, Optional, Tuple, Union ParsedVersion = Union[Version, LegacyVersion] UnparsedVersion = Union[Version, LegacyVersion, str] diff --git a/src/pip/_vendor/packaging/tags.py b/src/pip/_vendor/packaging/tags.py index 13798e38bce..d637f1b6993 100644 --- a/src/pip/_vendor/packaging/tags.py +++ b/src/pip/_vendor/packaging/tags.py @@ -27,9 +27,9 @@ if TYPE_CHECKING: # pragma: no cover from typing import ( + IO, Dict, FrozenSet, - IO, Iterable, Iterator, List, @@ -458,14 +458,28 @@ def mac_platforms(version=None, arch=None): major=major_version, minor=0, binary_format=binary_format ) - if version >= (11, 0) and arch == "x86_64": + if version >= (11, 0): # Mac OS 11 on x86_64 is compatible with binaries from previous releases. # Arm64 support was introduced in 11.0, so no Arm binaries from previous # releases exist. - for minor_version in range(16, 3, -1): - compat_version = 10, minor_version - binary_formats = _mac_binary_formats(compat_version, arch) - for binary_format in binary_formats: + # + # However, the "universal2" binary format can have a + # macOS version earlier than 11.0 when the x86_64 part of the binary supports + # that version of macOS. + if arch == "x86_64": + for minor_version in range(16, 3, -1): + compat_version = 10, minor_version + binary_formats = _mac_binary_formats(compat_version, arch) + for binary_format in binary_formats: + yield "macosx_{major}_{minor}_{binary_format}".format( + major=compat_version[0], + minor=compat_version[1], + binary_format=binary_format, + ) + else: + for minor_version in range(16, 3, -1): + compat_version = 10, minor_version + binary_format = "universal2" yield "macosx_{major}_{minor}_{binary_format}".format( major=compat_version[0], minor=compat_version[1], diff --git a/src/pip/_vendor/packaging/utils.py b/src/pip/_vendor/packaging/utils.py index 92c7b00b778..6e8c2a3e5bb 100644 --- a/src/pip/_vendor/packaging/utils.py +++ b/src/pip/_vendor/packaging/utils.py @@ -6,23 +6,41 @@ import re from ._typing import TYPE_CHECKING, cast +from .tags import Tag, parse_tag from .version import InvalidVersion, Version if TYPE_CHECKING: # pragma: no cover - from typing import NewType, Union + from typing import FrozenSet, NewType, Tuple, Union + BuildTag = Union[Tuple[()], Tuple[int, str]] NormalizedName = NewType("NormalizedName", str) else: + BuildTag = tuple NormalizedName = str + +class InvalidWheelFilename(ValueError): + """ + An invalid wheel filename was found, users should refer to PEP 427. + """ + + +class InvalidSdistFilename(ValueError): + """ + An invalid sdist filename was found, users should refer to the packaging user guide. + """ + + _canonicalize_regex = re.compile(r"[-_.]+") +# PEP 427: The build number must start with a digit. +_build_tag_regex = re.compile(r"(\d+)(.*)") def canonicalize_name(name): # type: (str) -> NormalizedName # This is taken from PEP 503. value = _canonicalize_regex.sub("-", name).lower() - return cast("NormalizedName", value) + return cast(NormalizedName, value) def canonicalize_version(version): @@ -65,3 +83,56 @@ def canonicalize_version(version): parts.append("+{0}".format(version.local)) return "".join(parts) + + +def parse_wheel_filename(filename): + # type: (str) -> Tuple[NormalizedName, Version, BuildTag, FrozenSet[Tag]] + if not filename.endswith(".whl"): + raise InvalidWheelFilename( + "Invalid wheel filename (extension must be '.whl'): {0}".format(filename) + ) + + filename = filename[:-4] + dashes = filename.count("-") + if dashes not in (4, 5): + raise InvalidWheelFilename( + "Invalid wheel filename (wrong number of parts): {0}".format(filename) + ) + + parts = filename.split("-", dashes - 2) + name_part = parts[0] + # See PEP 427 for the rules on escaping the project name + if "__" in name_part or re.match(r"^[\w\d._]*$", name_part, re.UNICODE) is None: + raise InvalidWheelFilename("Invalid project name: {0}".format(filename)) + name = canonicalize_name(name_part) + version = Version(parts[1]) + if dashes == 5: + build_part = parts[2] + build_match = _build_tag_regex.match(build_part) + if build_match is None: + raise InvalidWheelFilename( + "Invalid build number: {0} in '{1}'".format(build_part, filename) + ) + build = cast(BuildTag, (int(build_match.group(1)), build_match.group(2))) + else: + build = () + tags = parse_tag(parts[-1]) + return (name, version, build, tags) + + +def parse_sdist_filename(filename): + # type: (str) -> Tuple[NormalizedName, Version] + if not filename.endswith(".tar.gz"): + raise InvalidSdistFilename( + "Invalid sdist filename (extension must be '.tar.gz'): {0}".format(filename) + ) + + # We are requiring a PEP 440 version, which cannot contain dashes, + # so we split on the last dash. + name_part, sep, version_part = filename[:-7].rpartition("-") + if not sep: + raise InvalidSdistFilename("Invalid sdist filename: {0}".format(filename)) + + name = canonicalize_name(name_part) + version = Version(version_part) + return (name, version) diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 1ec29804f14..f36a78dbe78 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -6,7 +6,7 @@ distlib==0.3.1 distro==1.5.0 html5lib==1.1 msgpack==1.0.2 -packaging==20.8 +packaging==20.9 pep517==0.9.1 progress==1.5 pyparsing==2.4.7 From f529ae867f76f77b124cee07879341ddf13bdcd2 Mon Sep 17 00:00:00 2001 From: Darren Kavanagh <dkav@users.noreply.github.com> Date: Fri, 29 Jan 2021 20:47:09 -0800 Subject: [PATCH 2928/3170] Fix typo in _iter_built docstring --- news/5B0A5B91-D0A3-4599-B3B4-129240A99B99.trivial.rst | 0 src/pip/_internal/resolution/resolvelib/found_candidates.py | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 news/5B0A5B91-D0A3-4599-B3B4-129240A99B99.trivial.rst diff --git a/news/5B0A5B91-D0A3-4599-B3B4-129240A99B99.trivial.rst b/news/5B0A5B91-D0A3-4599-B3B4-129240A99B99.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/resolution/resolvelib/found_candidates.py b/src/pip/_internal/resolution/resolvelib/found_candidates.py index 5d4b4654988..f11b47603c3 100644 --- a/src/pip/_internal/resolution/resolvelib/found_candidates.py +++ b/src/pip/_internal/resolution/resolvelib/found_candidates.py @@ -28,7 +28,7 @@ def _iter_built(infos): # type: (Iterator[IndexCandidateInfo]) -> Iterator[Candidate] """Iterator for ``FoundCandidates``. - This iterator is used the package is not already installed. Candidates + This iterator is used when the package is not already installed. Candidates from index come later in their normal ordering. """ versions_found = set() # type: Set[_BaseVersion] From 8683413501cf77a86ffc8907656a24ee53a7f2ef Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Fri, 1 Jan 2021 10:55:24 -0800 Subject: [PATCH 2929/3170] Use keyword-only arguments https://www.python.org/dev/peps/pep-3102/ Replaces the pattern: self.name = kwargs.pop('name') Keyword-only arguments offer some advantages: - In the event of a typo or misuse, a more informative error is presented to the programmer. - More self documenting and makes interfaces more explicit. - They more easily allow explicit typing. Adding types to ConfigOptionParser required changing some call sites to pass arguments without using a dict due to mypy bug: https://github.com/python/mypy/issues/9676 --- ...63-d2a1-41f5-a2df-d11f8460551a.trivial.rst | 0 src/pip/_internal/cli/base_command.py | 19 +++++----- src/pip/_internal/cli/main_parser.py | 16 ++++----- src/pip/_internal/cli/parser.py | 17 ++++++--- src/pip/_internal/network/session.py | 18 ++++++---- src/pip/_internal/utils/logging.py | 14 ++++++-- tests/lib/__init__.py | 36 +++++++++---------- 7 files changed, 70 insertions(+), 50 deletions(-) create mode 100644 news/287c6463-d2a1-41f5-a2df-d11f8460551a.trivial.rst diff --git a/news/287c6463-d2a1-41f5-a2df-d11f8460551a.trivial.rst b/news/287c6463-d2a1-41f5-a2df-d11f8460551a.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index fac76bb12f7..108435a841e 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -52,19 +52,18 @@ class Command(CommandContextMixIn): def __init__(self, name, summary, isolated=False): # type: (str, str, bool) -> None super().__init__() - parser_kw = { - 'usage': self.usage, - 'prog': f'{get_prog()} {name}', - 'formatter': UpdatingDefaultsHelpFormatter(), - 'add_help_option': False, - 'name': name, - 'description': self.__doc__, - 'isolated': isolated, - } self.name = name self.summary = summary - self.parser = ConfigOptionParser(**parser_kw) + self.parser = ConfigOptionParser( + usage=self.usage, + prog=f'{get_prog()} {name}', + formatter=UpdatingDefaultsHelpFormatter(), + add_help_option=False, + name=name, + description=self.__doc__, + isolated=isolated, + ) self.tempdir_registry = None # type: Optional[TempDirRegistry] diff --git a/src/pip/_internal/cli/main_parser.py b/src/pip/_internal/cli/main_parser.py index fcee6a2c234..7351cdda0f1 100644 --- a/src/pip/_internal/cli/main_parser.py +++ b/src/pip/_internal/cli/main_parser.py @@ -23,15 +23,13 @@ def create_main_parser(): """Creates and returns the main parser for pip's CLI """ - parser_kw = { - 'usage': '\n%prog <command> [options]', - 'add_help_option': False, - 'formatter': UpdatingDefaultsHelpFormatter(), - 'name': 'global', - 'prog': get_prog(), - } - - parser = ConfigOptionParser(**parser_kw) + parser = ConfigOptionParser( + usage='\n%prog <command> [options]', + add_help_option=False, + formatter=UpdatingDefaultsHelpFormatter(), + name='global', + prog=get_prog(), + ) parser.disable_interspersed_args() parser.version = get_pip_version() diff --git a/src/pip/_internal/cli/parser.py b/src/pip/_internal/cli/parser.py index 60c61f30d3f..89591fa7f9a 100644 --- a/src/pip/_internal/cli/parser.py +++ b/src/pip/_internal/cli/parser.py @@ -14,6 +14,10 @@ from pip._internal.cli.status_codes import UNKNOWN_ERROR from pip._internal.configuration import Configuration, ConfigurationError from pip._internal.utils.misc import redact_auth_from_url, strtobool +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import Any logger = logging.getLogger(__name__) @@ -154,10 +158,15 @@ class ConfigOptionParser(CustomOptionParser): """Custom option parser which updates its defaults by checking the configuration files and environmental variables""" - def __init__(self, *args, **kwargs): - self.name = kwargs.pop('name') - - isolated = kwargs.pop("isolated", False) + def __init__( + self, + *args, # type: Any + name, # type: str + isolated=False, # type: bool + **kwargs, # type: Any + ): + # type: (...) -> None + self.name = name self.config = Configuration(isolated) assert self.name diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py index 5021b8eefaa..6fb6d7b8bc8 100644 --- a/src/pip/_internal/network/session.py +++ b/src/pip/_internal/network/session.py @@ -39,7 +39,7 @@ from pip._internal.utils.urls import url_to_path if MYPY_CHECK_RUNNING: - from typing import Iterator, List, Optional, Tuple, Union + from typing import Any, Iterator, List, Optional, Sequence, Tuple, Union from pip._internal.models.link import Link @@ -225,16 +225,20 @@ class PipSession(requests.Session): timeout = None # type: Optional[int] - def __init__(self, *args, **kwargs): + def __init__( + self, + *args, # type: Any + retries=0, # type: int + cache=None, # type: Optional[str] + trusted_hosts=(), # type: Sequence[str] + index_urls=None, # type: Optional[List[str]] + **kwargs, # type: Any + ): + # type: (...) -> None """ :param trusted_hosts: Domains not to emit warnings for when not using HTTPS. """ - retries = kwargs.pop("retries", 0) - cache = kwargs.pop("cache", None) - trusted_hosts = kwargs.pop("trusted_hosts", []) # type: List[str] - index_urls = kwargs.pop("index_urls", None) - super().__init__(*args, **kwargs) # Namespace the attribute with "pip_" just in case to prevent diff --git a/src/pip/_internal/utils/logging.py b/src/pip/_internal/utils/logging.py index 87b91d23d26..f82c5d56548 100644 --- a/src/pip/_internal/utils/logging.py +++ b/src/pip/_internal/utils/logging.py @@ -12,6 +12,10 @@ from pip._internal.utils.compat import WINDOWS from pip._internal.utils.deprecation import DEPRECATION_MSG_PREFIX from pip._internal.utils.misc import ensure_dir +from pip._internal.utils.typing import MYPY_CHECK_RUNNING + +if MYPY_CHECK_RUNNING: + from typing import Any try: import threading @@ -83,14 +87,20 @@ def get_indentation(): class IndentingFormatter(logging.Formatter): default_time_format = "%Y-%m-%dT%H:%M:%S" - def __init__(self, *args, **kwargs): + def __init__( + self, + *args, # type: Any + add_timestamp=False, # type: bool + **kwargs, # type: Any + ): + # type: (...) -> None """ A logging.Formatter that obeys the indent_log() context manager. :param add_timestamp: A bool indicating output lines should be prefixed with their record's timestamp. """ - self.add_timestamp = kwargs.pop("add_timestamp", False) + self.add_timestamp = add_timestamp super().__init__(*args, **kwargs) def get_message_start(self, formatted, levelno): diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index 6a98d4acf78..d7106bb6a51 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -454,16 +454,15 @@ class PipTestEnvironment(TestFileEnvironment): exe = sys.platform == 'win32' and '.exe' or '' verbose = False - def __init__(self, base_path, *args, **kwargs): + def __init__(self, base_path, *args, virtualenv, pip_expect_warning=None, **kwargs): # Make our base_path a test.lib.path.Path object base_path = Path(base_path) # Store paths related to the virtual environment - venv = kwargs.pop("virtualenv") - self.venv_path = venv.location - self.lib_path = venv.lib - self.site_packages_path = venv.site - self.bin_path = venv.bin + self.venv_path = virtualenv.location + self.lib_path = virtualenv.lib + self.site_packages_path = virtualenv.site + self.bin_path = virtualenv.bin self.user_base_path = self.venv_path.joinpath("user") self.user_site_path = self.venv_path.joinpath( @@ -503,7 +502,7 @@ def __init__(self, base_path, *args, **kwargs): # Whether all pip invocations should expect stderr # (useful for Python version deprecation) - self.pip_expect_warning = kwargs.pop('pip_expect_warning', None) + self.pip_expect_warning = pip_expect_warning # Call the TestFileEnvironment __init__ super().__init__(base_path, *args, **kwargs) @@ -544,7 +543,16 @@ def _find_traverse(self, path, result): else: super()._find_traverse(path, result) - def run(self, *args, **kw): + def run( + self, + *args, + cwd=None, + run_from=None, + allow_stderr_error=None, + allow_stderr_warning=None, + allow_error=None, + **kw, + ): """ :param allow_stderr_error: whether a logged error is allowed in stderr. Passing True for this argument implies @@ -567,20 +575,12 @@ def run(self, *args, **kw): if self.verbose: print('>> running {args} {kw}'.format(**locals())) - cwd = kw.pop('cwd', None) - run_from = kw.pop('run_from', None) assert not cwd or not run_from, "Don't use run_from; it's going away" cwd = cwd or run_from or self.cwd if sys.platform == 'win32': # Partial fix for ScriptTest.run using `shell=True` on Windows. args = [str(a).replace('^', '^^').replace('&', '^&') for a in args] - # Remove `allow_stderr_error`, `allow_stderr_warning` and - # `allow_error` before calling run() because PipTestEnvironment - # doesn't support them. - allow_stderr_error = kw.pop('allow_stderr_error', None) - allow_stderr_warning = kw.pop('allow_stderr_warning', None) - allow_error = kw.pop('allow_error', None) if allow_error: kw['expect_error'] = True @@ -634,11 +634,11 @@ def run(self, *args, **kw): return TestPipResult(result, verbose=self.verbose) - def pip(self, *args, **kwargs): + def pip(self, *args, use_module=True, **kwargs): __tracebackhide__ = True if self.pip_expect_warning: kwargs['allow_stderr_warning'] = True - if kwargs.pop('use_module', True): + if use_module: exe = 'python' args = ('-m', 'pip') + args else: From a085068760f6ffd4c3495d9322ac2f2e16fc0197 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Sat, 30 Jan 2021 12:10:01 +0000 Subject: [PATCH 2930/3170] Update AUTHORS.txt --- AUTHORS.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/AUTHORS.txt b/AUTHORS.txt index 6bfbf36e29a..764605f5359 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -228,6 +228,7 @@ gkdoc Gopinath M GOTO Hayato gpiks +Greg Roodt Greg Ward Guilherme Espada gutsytechster @@ -236,6 +237,7 @@ gzpan123 Hanjun Kim Hari Charan Harsh Vardhan +Henry Schreiner Herbert Pfennig Hsiaoming Yang Hugo From 22c6efd9cf514cd0c51e1e1d7de50aa2411689c2 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Sat, 30 Jan 2021 12:10:04 +0000 Subject: [PATCH 2931/3170] Bump for release --- NEWS.rst | 17 +++++++++++++++++ news/9461.bugfix.rst | 1 - news/9516.bugfix.rst | 3 --- news/packaging.vendor.rst | 1 - src/pip/__init__.py | 2 +- 5 files changed, 18 insertions(+), 6 deletions(-) delete mode 100644 news/9461.bugfix.rst delete mode 100644 news/9516.bugfix.rst delete mode 100644 news/packaging.vendor.rst diff --git a/NEWS.rst b/NEWS.rst index 83608ac9c6c..8e1e6e70c8a 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,20 @@ +21.0.1 (2021-01-30) +=================== + +Bug Fixes +--------- + +- commands: debug: Use packaging.version.parse to compare between versions. (`#9461 <https://github.com/pypa/pip/issues/9461>`_) +- New resolver: Download and prepare a distribution only at the last possible + moment to avoid unnecessary network access when the same version is already + installed locally. (`#9516 <https://github.com/pypa/pip/issues/9516>`_) + +Vendored Libraries +------------------ + +- Upgrade packaging to 20.9 + + 21.0 (2021-01-23) ================= diff --git a/news/9461.bugfix.rst b/news/9461.bugfix.rst deleted file mode 100644 index b1decff41fc..00000000000 --- a/news/9461.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -commands: debug: Use packaging.version.parse to compare between versions. diff --git a/news/9516.bugfix.rst b/news/9516.bugfix.rst deleted file mode 100644 index 9b9cc812614..00000000000 --- a/news/9516.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -New resolver: Download and prepare a distribution only at the last possible -moment to avoid unnecessary network access when the same version is already -installed locally. diff --git a/news/packaging.vendor.rst b/news/packaging.vendor.rst deleted file mode 100644 index ccaa7a9727f..00000000000 --- a/news/packaging.vendor.rst +++ /dev/null @@ -1 +0,0 @@ -Upgrade packaging to 20.9 diff --git a/src/pip/__init__.py b/src/pip/__init__.py index 9de6dc0f2a6..cb041b565ed 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -4,7 +4,7 @@ from typing import List, Optional -__version__ = "21.1.dev0" +__version__ = "21.0.1" def main(args=None): From 9408c638a36053ba9d1ce73620720b8b1f2ef589 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Sat, 30 Jan 2021 12:10:05 +0000 Subject: [PATCH 2932/3170] Bump for development --- src/pip/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/__init__.py b/src/pip/__init__.py index cb041b565ed..9de6dc0f2a6 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -4,7 +4,7 @@ from typing import List, Optional -__version__ = "21.0.1" +__version__ = "21.1.dev0" def main(args=None): From e6bd10d503d2c8d76eb95b5428b5a4467d6e1f5f Mon Sep 17 00:00:00 2001 From: Andrey Bienkowski <hexagonrecursion@gmail.com> Date: Sun, 31 Jan 2021 12:59:31 +0000 Subject: [PATCH 2933/3170] _vendor/README.rst: link to pypi.org/project/vendoring I got confused and searched for a tool called "vendoring" in this repository. Hopefully this will save some time to the next person trying to figure out how to run the vendoring tool. --- news/d809028d-331a-4700-bfc1-485814e6c90e.trivial.rst | 0 src/pip/_vendor/README.rst | 2 +- 2 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 news/d809028d-331a-4700-bfc1-485814e6c90e.trivial.rst diff --git a/news/d809028d-331a-4700-bfc1-485814e6c90e.trivial.rst b/news/d809028d-331a-4700-bfc1-485814e6c90e.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_vendor/README.rst b/src/pip/_vendor/README.rst index 6699d72c2f3..9363558129e 100644 --- a/src/pip/_vendor/README.rst +++ b/src/pip/_vendor/README.rst @@ -116,7 +116,7 @@ Modifications Automatic Vendoring =================== -Vendoring is automated via the ``vendoring`` tool from the content of +Vendoring is automated via the `vendoring <https://pypi.org/project/vendoring/>`_ tool from the content of ``pip/_vendor/vendor.txt`` and the different patches in ``tools/automation/vendoring/patches``. Launch it via ``vendoring sync . -v`` (requires ``vendoring>=0.2.2``). From 720202c9b7df658f34c31fc863b0fb043f348231 Mon Sep 17 00:00:00 2001 From: Martin Pavlasek <martin.pavlasek@tescan.com> Date: Wed, 3 Feb 2021 14:18:27 +0100 Subject: [PATCH 2934/3170] remove uninteded 'python -m' from installation reference docs --- docs/html/reference/pip_install.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index 1b53513266d..81e315ebaa2 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -1029,7 +1029,7 @@ Examples python -m pip install -e git+https://git.repo/some_pkg.git#egg=SomePackage # from git python -m pip install -e hg+https://hg.repo/some_pkg.git#egg=SomePackage # from mercurial - python -m python -m pip install -e svn+svn://svn.repo/some_pkg/trunk/#egg=SomePackage # from svn + python -m pip install -e svn+svn://svn.repo/some_pkg/trunk/#egg=SomePackage # from svn python -m pip install -e git+https://git.repo/some_pkg.git@feature#egg=SomePackage # from 'feature' branch python -m pip install -e "git+https://git.repo/some_repo.git#egg=subdir&subdirectory=subdir_path" # install a python package from a repo subdirectory From bb8a29cfed29f4793fd9c34f0e3c91df8c5fabf8 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko <wk@sydorenko.org.ua> Date: Wed, 3 Feb 2021 00:21:13 +0100 Subject: [PATCH 2935/3170] Use new issue forms for bug reports --- .github/ISSUE_TEMPLATE/bug-report.md | 36 ------------ .github/ISSUE_TEMPLATE/bug-report.yml | 85 +++++++++++++++++++++++++++ 2 files changed, 85 insertions(+), 36 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/bug-report.md create mode 100644 .github/ISSUE_TEMPLATE/bug-report.yml diff --git a/.github/ISSUE_TEMPLATE/bug-report.md b/.github/ISSUE_TEMPLATE/bug-report.md deleted file mode 100644 index 157be28b678..00000000000 --- a/.github/ISSUE_TEMPLATE/bug-report.md +++ /dev/null @@ -1,36 +0,0 @@ ---- -name: Bug report -about: Create a report to help us improve ---- - -<!-- -If you're reporting an issue for `--use-feature=2020-resolver`, use the "Dependency resolver failures / errors" template instead. ---> - -**Environment** - -* pip version: -* Python version: -* OS: - -<!-- Feel free to add more information about your environment here --> - -**Description** -<!-- A clear and concise description of what the bug is. --> - -**Expected behavior** -<!-- A clear and concise description of what you expected to happen. --> - -**How to Reproduce** -<!-- Describe the steps to reproduce this bug. --> - -1. Get package from '...' -2. Then run '...' -3. An error occurs. - -**Output** - -``` -Paste the output of the steps above, including the commands themselves and -pip's output/traceback etc. -``` diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml new file mode 100644 index 00000000000..004538bba30 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -0,0 +1,85 @@ +--- +name: Bug report +about: | + Create a report to help us improve + + ⚠ If you're reporting an issue for `--use-feature=2020-resolver`, use + the "Dependency resolver failures / errors" template instead. +title: '[Bug]: ' +labels: +- >- + S: needs triage +- >- + type: bug +issue_body: true # default: true, adds a classic WSYWIG textarea, if on +inputs: +- type: description + attributes: + value: | + **Environment** + + ⚠ If you're reporting an issue for `--use-feature=2020-resolver`, + use the "Dependency resolver failures / errors" template instead. + +- type: input + attributes: + label: pip version + required: true +- type: input + attributes: + label: Python version + required: true +- type: input + attributes: + label: OS + required: true +- type: textarea + attributes: + label: Additional information + description: >- + Feel free to add more information about your environment here. + +- type: textarea + attributes: + label: Description + description: >- + A clear and concise description of what the bug is. + +- type: textarea + attributes: + label: Expected behavior + description: >- + A clear and concise description of what you expected to happen. + +- type: textarea + attributes: + label: How to Reproduce + description: >- + Describe the steps to reproduce this bug. + value: | + 1. Get package from '...' + 2. Then run '...' + 3. An error occurs. + +- type: textarea + attributes: + label: Output + description: >- + Paste the output of the steps above, including the commands + themselves and pip's output/traceback etc. + value: | + ```console + + ``` + +- type: checkboxes + attributes: + label: Code of Conduct + description: | + Read the [PSF Code of Conduct][CoC] first. + + [CoC]: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md + choices: + - label: I agree to follow the PSF Code of Conduct + required: true +... From a80427931a4b23eb8d03c90635e28126d90042fc Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Date: Wed, 3 Feb 2021 21:08:44 +0000 Subject: [PATCH 2936/3170] Update bug-report.yml Fixes the malformed labels syntax --- .github/ISSUE_TEMPLATE/bug-report.yml | 6 +----- 1 file changed, 1 insertion(+), 5 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 004538bba30..3ad1a311c77 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -6,11 +6,7 @@ about: | ⚠ If you're reporting an issue for `--use-feature=2020-resolver`, use the "Dependency resolver failures / errors" template instead. title: '[Bug]: ' -labels: -- >- - S: needs triage -- >- - type: bug +labels: "S: needs triage, type: bug" issue_body: true # default: true, adds a classic WSYWIG textarea, if on inputs: - type: description From 79949fe045d04d2510decdac962333fea70d2371 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Date: Wed, 3 Feb 2021 21:12:54 +0000 Subject: [PATCH 2937/3170] Update bug-report.yml Tweak presentation of description and warnings. --- .github/ISSUE_TEMPLATE/bug-report.yml | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 3ad1a311c77..aa6d1e0c7c0 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -1,22 +1,19 @@ --- name: Bug report -about: | - Create a report to help us improve - - ⚠ If you're reporting an issue for `--use-feature=2020-resolver`, use - the "Dependency resolver failures / errors" template instead. -title: '[Bug]: ' +about: Something is not working correctly. +title: "" labels: "S: needs triage, type: bug" issue_body: true # default: true, adds a classic WSYWIG textarea, if on inputs: - type: description attributes: value: | - **Environment** - - ⚠ If you're reporting an issue for `--use-feature=2020-resolver`, + ⚠ + If you're reporting an issue for `--use-feature=2020-resolver`, use the "Dependency resolver failures / errors" template instead. - +- type: description + attributes: + value: **Environment** - type: input attributes: label: pip version From ac561083072eef7de1cd6f2448530571fb0bf83d Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Date: Wed, 3 Feb 2021 21:13:17 +0000 Subject: [PATCH 2938/3170] Update bug-report.yml --- .github/ISSUE_TEMPLATE/bug-report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index aa6d1e0c7c0..1b62b864bb4 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -13,7 +13,7 @@ inputs: use the "Dependency resolver failures / errors" template instead. - type: description attributes: - value: **Environment** + value: "**Environment**" - type: input attributes: label: pip version From 2c4fcce2651540902a128b5d60de8150684cb13d Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 3 Feb 2021 21:27:03 +0800 Subject: [PATCH 2939/3170] Replace pkg_resources usage in 'pip debug' --- src/pip/_internal/commands/debug.py | 11 ++++++----- src/pip/_internal/metadata/base.py | 7 +++++++ src/pip/_internal/metadata/pkg_resources.py | 7 +++++++ 3 files changed, 20 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/commands/debug.py b/src/pip/_internal/commands/debug.py index be4f4704053..c1af3630ec2 100644 --- a/src/pip/_internal/commands/debug.py +++ b/src/pip/_internal/commands/debug.py @@ -4,7 +4,6 @@ import sys import pip._vendor -from pip._vendor import pkg_resources from pip._vendor.certifi import where from pip._vendor.packaging.version import parse as parse_version @@ -13,6 +12,7 @@ from pip._internal.cli.base_command import Command from pip._internal.cli.cmdoptions import make_target_python from pip._internal.cli.status_codes import SUCCESS +from pip._internal.metadata import get_environment from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import get_pip_version from pip._internal.utils.typing import MYPY_CHECK_RUNNING @@ -81,10 +81,11 @@ def get_vendor_version_from_module(module_name): version = getattr(module, '__version__', None) if not version: - # Try to find version in debundled module info - pkg_set = pkg_resources.WorkingSet([os.path.dirname(module.__file__)]) - package = pkg_set.find(pkg_resources.Requirement.parse(module_name)) - version = getattr(package, 'version', None) + # Try to find version in debundled module info. + env = get_environment([os.path.dirname(module.__file__)]) + dist = env.get_distribution(module_name) + if dist: + version = str(dist.version) return version diff --git a/src/pip/_internal/metadata/base.py b/src/pip/_internal/metadata/base.py index 955682545cc..3aa3f6a036b 100644 --- a/src/pip/_internal/metadata/base.py +++ b/src/pip/_internal/metadata/base.py @@ -4,6 +4,8 @@ if MYPY_CHECK_RUNNING: from typing import Container, Iterator, List, Optional + from pip._vendor.packaging.version import _BaseVersion + class BaseDistribution: @property @@ -11,6 +13,11 @@ def canonical_name(self): # type: () -> str raise NotImplementedError() + @property + def version(self): + # type: () -> _BaseVersion + raise NotImplementedError() + @property def installer(self): # type: () -> str diff --git a/src/pip/_internal/metadata/pkg_resources.py b/src/pip/_internal/metadata/pkg_resources.py index d9db2955159..dfc326f9c69 100644 --- a/src/pip/_internal/metadata/pkg_resources.py +++ b/src/pip/_internal/metadata/pkg_resources.py @@ -10,6 +10,8 @@ if MYPY_CHECK_RUNNING: from typing import Iterator, List, Optional + from pip._vendor.packaging.version import _BaseVersion + class Distribution(BaseDistribution): def __init__(self, dist): @@ -21,6 +23,11 @@ def canonical_name(self): # type: () -> str return canonicalize_name(self._dist.project_name) + @property + def version(self): + # type: () -> _BaseVersion + return self._dist.parsed_version + @property def installer(self): # type: () -> str From 30a022ae3f7a30fc936c2a32638da61f8cf54018 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 4 Feb 2021 10:03:08 +0800 Subject: [PATCH 2940/3170] Remove pkg_resources usage in search --- src/pip/_internal/commands/search.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/commands/search.py b/src/pip/_internal/commands/search.py index b3143b72fba..90a5b512d69 100644 --- a/src/pip/_internal/commands/search.py +++ b/src/pip/_internal/commands/search.py @@ -4,7 +4,6 @@ import textwrap from collections import OrderedDict -from pip._vendor import pkg_resources from pip._vendor.packaging.version import parse as parse_version # NOTE: XMLRPC Client is not annotated in typeshed as on 2017-07-17, which is @@ -15,10 +14,11 @@ from pip._internal.cli.req_command import SessionCommandMixin from pip._internal.cli.status_codes import NO_MATCHES_FOUND, SUCCESS from pip._internal.exceptions import CommandError +from pip._internal.metadata import get_default_environment from pip._internal.models.index import PyPI from pip._internal.network.xmlrpc import PipXmlrpcTransport from pip._internal.utils.logging import indent_log -from pip._internal.utils.misc import get_distribution, write_output +from pip._internal.utils.misc import write_output from pip._internal.utils.typing import MYPY_CHECK_RUNNING if MYPY_CHECK_RUNNING: @@ -127,7 +127,7 @@ def print_results(hits, name_column_width=None, terminal_width=None): for hit in hits ]) + 4 - installed_packages = [p.project_name for p in pkg_resources.working_set] + env = get_default_environment() for hit in hits: name = hit['name'] summary = hit['summary'] or '' @@ -145,9 +145,8 @@ def print_results(hits, name_column_width=None, terminal_width=None): **locals()) try: write_output(line) - if name in installed_packages: - dist = get_distribution(name) - assert dist is not None + dist = env.get_distribution(name) + if dist is not None: with indent_log(): if dist.version == latest: write_output('INSTALLED: %s (latest)', dist.version) From d16a5036de3bb03ed4ad6592c4195edee68b74f0 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 4 Feb 2021 11:44:56 +0800 Subject: [PATCH 2941/3170] Remove direct pkg_resources usage in wheel_builder --- src/pip/_internal/metadata/__init__.py | 16 ++++++++- src/pip/_internal/metadata/base.py | 6 ++++ src/pip/_internal/metadata/pkg_resources.py | 18 ++++++++++ src/pip/_internal/wheel_builder.py | 38 ++++++++------------- 4 files changed, 53 insertions(+), 25 deletions(-) diff --git a/src/pip/_internal/metadata/__init__.py b/src/pip/_internal/metadata/__init__.py index da2c4355dfc..ba0471aa17c 100644 --- a/src/pip/_internal/metadata/__init__.py +++ b/src/pip/_internal/metadata/__init__.py @@ -3,7 +3,7 @@ if MYPY_CHECK_RUNNING: from typing import List, Optional - from .base import BaseEnvironment + from .base import BaseDistribution, BaseEnvironment def get_default_environment(): @@ -30,3 +30,17 @@ def get_environment(paths): from .pkg_resources import Environment return Environment.from_paths(paths) + + +def get_wheel_distribution(wheel_path, canonical_name): + # type: (str, str) -> BaseDistribution + """Get the representation of the specified wheel's distribution metadata. + + This returns a Distribution instance from the chosen backend based on + the given wheel's ``.dist-info`` directory. + + :param canonical_name: Normalized project name of the given wheel. + """ + from .pkg_resources import Distribution + + return Distribution.from_wheel(wheel_path, canonical_name) diff --git a/src/pip/_internal/metadata/base.py b/src/pip/_internal/metadata/base.py index 3aa3f6a036b..fb1f8579a38 100644 --- a/src/pip/_internal/metadata/base.py +++ b/src/pip/_internal/metadata/base.py @@ -8,6 +8,12 @@ class BaseDistribution: + @property + def metadata_version(self): + # type: () -> Optional[str] + """Value of "Metadata-Version:" in the distribution, if available.""" + raise NotImplementedError() + @property def canonical_name(self): # type: () -> str diff --git a/src/pip/_internal/metadata/pkg_resources.py b/src/pip/_internal/metadata/pkg_resources.py index dfc326f9c69..48d959a373b 100644 --- a/src/pip/_internal/metadata/pkg_resources.py +++ b/src/pip/_internal/metadata/pkg_resources.py @@ -1,9 +1,12 @@ +import zipfile + from pip._vendor import pkg_resources from pip._vendor.packaging.utils import canonicalize_name from pip._internal.utils import misc # TODO: Move definition here. from pip._internal.utils.packaging import get_installer from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from pip._internal.utils.wheel import pkg_resources_distribution_for_wheel from .base import BaseDistribution, BaseEnvironment @@ -18,6 +21,21 @@ def __init__(self, dist): # type: (pkg_resources.Distribution) -> None self._dist = dist + @classmethod + def from_wheel(cls, path, name): + # type: (str, str) -> Distribution + with zipfile.ZipFile(path, allowZip64=True) as zf: + dist = pkg_resources_distribution_for_wheel(zf, name, path) + return cls(dist) + + @property + def metadata_version(self): + # type: () -> Optional[str] + for line in self._dist.get_metadata_lines(self._dist.PKG_INFO): + if line.lower().startswith("metadata-version:"): + return line.split(":", 1)[-1].strip() + return None + @property def canonical_name(self): # type: () -> str diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index c23ee1ba764..9fbb5329f4c 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -5,13 +5,12 @@ import os.path import re import shutil -import zipfile from pip._vendor.packaging.utils import canonicalize_name, canonicalize_version from pip._vendor.packaging.version import InvalidVersion, Version -from pip._vendor.pkg_resources import Distribution from pip._internal.exceptions import InvalidWheelFilename, UnsupportedWheel +from pip._internal.metadata import get_wheel_distribution from pip._internal.models.link import Link from pip._internal.models.wheel import Wheel from pip._internal.operations.build.wheel import build_wheel_pep517 @@ -23,7 +22,6 @@ from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.urls import path_to_url -from pip._internal.utils.wheel import pkg_resources_distribution_for_wheel from pip._internal.vcs import vcs if MYPY_CHECK_RUNNING: @@ -171,19 +169,6 @@ def _always_true(_): return True -def _get_metadata_version(dist): - # type: (Distribution) -> Optional[Version] - for line in dist.get_metadata_lines(dist.PKG_INFO): - if line.lower().startswith("metadata-version:"): - value = line.split(":", 1)[-1].strip() - try: - return Version(value) - except InvalidVersion: - msg = "Invalid Metadata-Version: {}".format(value) - raise UnsupportedWheel(msg) - raise UnsupportedWheel("Missing Metadata-Version") - - def _verify_one(req, wheel_path): # type: (InstallRequirement, str) -> None canonical_name = canonicalize_name(req.name) @@ -193,20 +178,25 @@ def _verify_one(req, wheel_path): "Wheel has unexpected file name: expected {!r}, " "got {!r}".format(canonical_name, w.name), ) - with zipfile.ZipFile(wheel_path, allowZip64=True) as zf: - dist = pkg_resources_distribution_for_wheel( - zf, canonical_name, wheel_path, - ) + dist = get_wheel_distribution(wheel_path, canonical_name) if canonicalize_version(dist.version) != canonicalize_version(w.version): raise InvalidWheelFilename( "Wheel has unexpected file name: expected {!r}, " - "got {!r}".format(dist.version, w.version), + "got {!r}".format(str(dist.version), w.version), ) - if (_get_metadata_version(dist) >= Version("1.2") - and not isinstance(dist.parsed_version, Version)): + metadata_version_value = dist.metadata_version + if metadata_version_value is None: + raise UnsupportedWheel("Missing Metadata-Version") + try: + metadata_version = Version(metadata_version_value) + except InvalidVersion: + msg = "Invalid Metadata-Version: {}".format(metadata_version_value) + raise UnsupportedWheel(msg) + if (metadata_version >= Version("1.2") + and not isinstance(dist.version, Version)): raise UnsupportedWheel( "Metadata 1.2 mandates PEP 440 version, " - "but {!r} is not".format(dist.version) + "but {!r} is not".format(str(dist.version)) ) From 1dcc9084c73a9bd15087bba5ceca9c25d92510c6 Mon Sep 17 00:00:00 2001 From: Thomas Grainger <tagrain@gmail.com> Date: Mon, 8 Feb 2021 20:57:06 +0000 Subject: [PATCH 2942/3170] update setuptools extras URL --- docs/html/reference/pip_install.rst | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index 81e315ebaa2..921103a15d9 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -919,8 +919,7 @@ No other build system commands are invoked by the ``pip install`` command. Installing a package from a wheel does not invoke the build system at all. .. _PyPI: https://pypi.org/ -.. _setuptools extras: https://setuptools.readthedocs.io/en/latest/setuptools.html#declaring-extras-optional-features-with-their-own-dependencies - +.. _setuptools extras: https://setuptools.readthedocs.io/en/latest/userguide/dependency_management.html#optional-dependencies .. _`pip install Options`: From 264a65da801dd6e96937b1d50acffba92c911092 Mon Sep 17 00:00:00 2001 From: Andrey Bienkowski <hexagonrecursion@gmail.com> Date: Wed, 10 Feb 2021 11:45:29 +0300 Subject: [PATCH 2943/3170] Use the set literal syntax --- ...597c433-9c0b-4bec-a1e8-afd31786eaeb.trivial.rst | 0 tests/functional/test_new_resolver.py | 14 +++++++------- tests/unit/test_req_uninstall.py | 4 ++-- 3 files changed, 9 insertions(+), 9 deletions(-) create mode 100644 news/8597c433-9c0b-4bec-a1e8-afd31786eaeb.trivial.rst diff --git a/news/8597c433-9c0b-4bec-a1e8-afd31786eaeb.trivial.rst b/news/8597c433-9c0b-4bec-a1e8-afd31786eaeb.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 4d2acbb23b4..92a00f74a3b 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -16,23 +16,23 @@ def assert_installed(script, **kwargs): ret = script.pip('list', '--format=json') - installed = set( + installed = { (canonicalize_name(val['name']), val['version']) for val in json.loads(ret.stdout) - ) - expected = set((canonicalize_name(k), v) for k, v in kwargs.items()) + } + expected = {(canonicalize_name(k), v) for k, v in kwargs.items()} assert expected <= installed, f"{expected!r} not all in {installed!r}" def assert_not_installed(script, *args): ret = script.pip("list", "--format=json") - installed = set( + installed = { canonicalize_name(val["name"]) for val in json.loads(ret.stdout) - ) + } # None of the given names should be listed as installed, i.e. their # intersection should be empty. - expected = set(canonicalize_name(k) for k in args) + expected = {canonicalize_name(k) for k in args} assert not (expected & installed), f"{expected!r} contained in {installed!r}" @@ -40,7 +40,7 @@ def assert_editable(script, *args): # This simply checks whether all of the listed packages have a # corresponding .egg-link file installed. # TODO: Implement a more rigorous way to test for editable installations. - egg_links = set(f"{arg}.egg-link" for arg in args) + egg_links = {f"{arg}.egg-link" for arg in args} assert egg_links <= set(os.listdir(script.site_packages_path)), \ f"{args!r} not all found in {script.site_packages_path!r}" diff --git a/tests/unit/test_req_uninstall.py b/tests/unit/test_req_uninstall.py index 90bf0d50fbc..6923d34b851 100644 --- a/tests/unit/test_req_uninstall.py +++ b/tests/unit/test_req_uninstall.py @@ -162,9 +162,9 @@ def test_add_pth(self, tmpdir, monkeypatch): pth.add(share_com) # Check that the paths were added to entries if on_windows: - check = set([tmpdir, relative, share, share_com]) + check = {tmpdir, relative, share, share_com} else: - check = set([tmpdir, relative]) + check = {tmpdir, relative} assert pth.entries == check @pytest.mark.skipif("sys.platform == 'win32'") From 1c02a1258519a04df813c558974c03d90073df8f Mon Sep 17 00:00:00 2001 From: Andrey Bienkowski <hexagonrecursion@gmail.com> Date: Wed, 10 Feb 2021 11:46:25 +0300 Subject: [PATCH 2944/3170] Drop redundant parentheses --- news/76c758fb-6f07-4ec1-956b-d77c9f339773.trivial.rst | 0 src/pip/_internal/build_env.py | 4 ++-- 2 files changed, 2 insertions(+), 2 deletions(-) create mode 100644 news/76c758fb-6f07-4ec1-956b-d77c9f339773.trivial.rst diff --git a/news/76c758fb-6f07-4ec1-956b-d77c9f339773.trivial.rst b/news/76c758fb-6f07-4ec1-956b-d77c9f339773.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py index a587d9f7c8f..1f1b772c736 100644 --- a/src/pip/_internal/build_env.py +++ b/src/pip/_internal/build_env.py @@ -56,10 +56,10 @@ def __init__(self): kind=tempdir_kinds.BUILD_ENV, globally_managed=True ) - self._prefixes = OrderedDict(( + self._prefixes = OrderedDict( (name, _Prefix(os.path.join(temp_dir.path, name))) for name in ('normal', 'overlay') - )) + ) self._bin_dirs = [] # type: List[str] self._lib_dirs = [] # type: List[str] From f13e10c9156466af7dc80b298705cb322e75e607 Mon Sep 17 00:00:00 2001 From: Andrey Bienkowski <hexagonrecursion@gmail.com> Date: Wed, 10 Feb 2021 13:20:11 +0300 Subject: [PATCH 2945/3170] Replace `open(file, 'r')` with `open(file)` --- setup.py | 2 +- src/pip/_internal/req/constructors.py | 2 +- src/pip/_internal/req/req_uninstall.py | 2 +- tests/functional/test_install.py | 4 ++-- tests/functional/test_no_color.py | 2 +- tests/functional/test_vcs_bazaar.py | 2 +- tools/automation/release/__init__.py | 2 +- 7 files changed, 8 insertions(+), 8 deletions(-) diff --git a/setup.py b/setup.py index 66820387bb9..f2ee075adfd 100644 --- a/setup.py +++ b/setup.py @@ -11,7 +11,7 @@ def read(rel_path): here = os.path.abspath(os.path.dirname(__file__)) # intentionally *not* adding an encoding option to open, See: # https://github.com/pypa/virtualenv/issues/201#issuecomment-3145690 - with open(os.path.join(here, rel_path), 'r') as fp: + with open(os.path.join(here, rel_path)) as fp: return fp.read() diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index cfb1951b6b8..f722efe9a94 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -145,7 +145,7 @@ def deduce_helpful_msg(req): msg = " The path does exist. " # Try to parse and check if it is a requirements file. try: - with open(req, 'r') as fp: + with open(req) as fp: # parse first line only next(parse_requirements(fp.read())) msg += ( diff --git a/src/pip/_internal/req/req_uninstall.py b/src/pip/_internal/req/req_uninstall.py index a37a378dda7..0f2de9b0d0c 100644 --- a/src/pip/_internal/req/req_uninstall.py +++ b/src/pip/_internal/req/req_uninstall.py @@ -543,7 +543,7 @@ def from_dist(cls, dist): elif develop_egg_link: # develop egg - with open(develop_egg_link, 'r') as fh: + with open(develop_egg_link) as fh: link_pointer = os.path.normcase(fh.readline().strip()) assert (link_pointer == dist.location), ( 'Egg-link {} does not match installed location of {} ' diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 1c0650c6f5f..7b331164b9c 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1298,7 +1298,7 @@ def test_install_log(script, data, tmpdir): 'install', data.src.joinpath('chattymodule')] result = script.pip(*args) assert 0 == result.stdout.count("HELLO FROM CHATTYMODULE") - with open(f, 'r') as fp: + with open(f) as fp: # one from egg_info, one from install assert 2 == fp.read().count("HELLO FROM CHATTYMODULE") @@ -1321,7 +1321,7 @@ def test_cleanup_after_failed_wheel(script, with_wheel): # One of the effects of not cleaning up is broken scripts: script_py = script.bin_path / "script.py" assert script_py.exists(), script_py - shebang = open(script_py, 'r').readline().strip() + shebang = open(script_py).readline().strip() assert shebang != '#!python', shebang # OK, assert that we *said* we were cleaning up: # /!\ if in need to change this, also change test_pep517_no_legacy_cleanup diff --git a/tests/functional/test_no_color.py b/tests/functional/test_no_color.py index 48ed3ff7848..3fd943f9327 100644 --- a/tests/functional/test_no_color.py +++ b/tests/functional/test_no_color.py @@ -33,7 +33,7 @@ def get_run_output(option): pytest.skip("Unable to capture output using script: " + cmd) try: - with open("/tmp/pip-test-no-color.txt", "r") as output_file: + with open("/tmp/pip-test-no-color.txt") as output_file: retval = output_file.read() return retval finally: diff --git a/tests/functional/test_vcs_bazaar.py b/tests/functional/test_vcs_bazaar.py index ad24d73d5ba..57fee51e780 100644 --- a/tests/functional/test_vcs_bazaar.py +++ b/tests/functional/test_vcs_bazaar.py @@ -64,7 +64,7 @@ def test_export_rev(script, tmpdir): url = hide_url('bzr+' + _test_path_to_file_url(source_dir) + '@1') Bazaar().export(str(export_dir), url=url) - with open(export_dir / 'test_file', 'r') as f: + with open(export_dir / 'test_file') as f: assert f.read() == 'something initial' diff --git a/tools/automation/release/__init__.py b/tools/automation/release/__init__.py index 20775d5e21d..645e7a1164f 100644 --- a/tools/automation/release/__init__.py +++ b/tools/automation/release/__init__.py @@ -90,7 +90,7 @@ def generate_news(session: Session, version: str) -> None: def update_version_file(version: str, filepath: str) -> None: - with open(filepath, "r", encoding="utf-8") as f: + with open(filepath, encoding="utf-8") as f: content = list(f) file_modified = False From ab35018c04ede9539da665f302120a0c96209a7f Mon Sep 17 00:00:00 2001 From: Andrey Bienkowski <hexagonrecursion@gmail.com> Date: Wed, 10 Feb 2021 13:28:55 +0300 Subject: [PATCH 2946/3170] Use unittest.mock instead of mock --- tests/conftest.py | 2 +- tests/functional/test_help.py | 2 +- tests/lib/server.py | 2 +- tests/unit/resolution_resolvelib/test_resolver.py | 2 +- tests/unit/test_base_command.py | 2 +- tests/unit/test_check.py | 2 +- tests/unit/test_collector.py | 4 ++-- tests/unit/test_command_install.py | 2 +- tests/unit/test_commands.py | 2 +- tests/unit/test_configuration.py | 2 +- tests/unit/test_direct_url_helpers.py | 2 +- tests/unit/test_finder.py | 2 +- tests/unit/test_locations.py | 2 +- tests/unit/test_logging.py | 2 +- tests/unit/test_network_cache.py | 2 +- tests/unit/test_operations_prepare.py | 2 +- tests/unit/test_req.py | 2 +- tests/unit/test_req_file.py | 2 +- tests/unit/test_req_uninstall.py | 2 +- tests/unit/test_resolution_legacy_resolver.py | 2 +- tests/unit/test_target_python.py | 2 +- tests/unit/test_utils.py | 2 +- tests/unit/test_utils_compatibility_tags.py | 2 +- tests/unit/test_vcs.py | 2 +- tests/unit/test_wheel.py | 2 +- tests/unit/test_wheel_builder.py | 2 +- 26 files changed, 27 insertions(+), 27 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 0bb69dae6d7..6ca67f7ce4e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -10,7 +10,7 @@ from contextlib import contextmanager import pytest -from mock import patch +from unittest.mock import patch from pip._vendor.contextlib2 import ExitStack, nullcontext from setuptools.wheel import Wheel diff --git a/tests/functional/test_help.py b/tests/functional/test_help.py index a660cdf520d..d9c1850396e 100644 --- a/tests/functional/test_help.py +++ b/tests/functional/test_help.py @@ -1,5 +1,5 @@ import pytest -from mock import Mock +from unittest.mock import Mock from pip._internal.cli.status_codes import ERROR, SUCCESS from pip._internal.commands import commands_dict, create_command diff --git a/tests/lib/server.py b/tests/lib/server.py index cd3c522bfec..90db54e61c2 100644 --- a/tests/lib/server.py +++ b/tests/lib/server.py @@ -5,7 +5,7 @@ from contextlib import contextmanager from textwrap import dedent -from mock import Mock +from unittest.mock import Mock from pip._vendor.contextlib2 import nullcontext from werkzeug.serving import WSGIRequestHandler from werkzeug.serving import make_server as _make_server diff --git a/tests/unit/resolution_resolvelib/test_resolver.py b/tests/unit/resolution_resolvelib/test_resolver.py index 4a62cefb603..adcf783b0ba 100644 --- a/tests/unit/resolution_resolvelib/test_resolver.py +++ b/tests/unit/resolution_resolvelib/test_resolver.py @@ -1,4 +1,4 @@ -import mock +from unittest import mock import pytest from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.resolvelib.resolvers import Result diff --git a/tests/unit/test_base_command.py b/tests/unit/test_base_command.py index 857d4f4f300..ffb079f7a49 100644 --- a/tests/unit/test_base_command.py +++ b/tests/unit/test_base_command.py @@ -2,7 +2,7 @@ import os import pytest -from mock import Mock, patch +from unittest.mock import Mock, patch from pip._internal.cli.base_command import Command from pip._internal.cli.status_codes import SUCCESS diff --git a/tests/unit/test_check.py b/tests/unit/test_check.py index c53830aa099..5f1c6d119d0 100644 --- a/tests/unit/test_check.py +++ b/tests/unit/test_check.py @@ -1,7 +1,7 @@ """Unit Tests for pip's dependency checking logic """ -import mock +from unittest import mock from pip._internal.operations import check diff --git a/tests/unit/test_collector.py b/tests/unit/test_collector.py index 9fb920b32fe..503a8e1f831 100644 --- a/tests/unit/test_collector.py +++ b/tests/unit/test_collector.py @@ -5,10 +5,10 @@ import uuid from textwrap import dedent -import mock +from unittest import mock import pretend import pytest -from mock import Mock, patch +from unittest.mock import Mock, patch from pip._vendor import html5lib, requests from pip._internal.exceptions import NetworkConnectionError diff --git a/tests/unit/test_command_install.py b/tests/unit/test_command_install.py index 66eb8ef3881..f631d77b037 100644 --- a/tests/unit/test_command_install.py +++ b/tests/unit/test_command_install.py @@ -1,7 +1,7 @@ import errno import pytest -from mock import patch +from unittest.mock import patch from pip._vendor.packaging.requirements import Requirement from pip._internal.commands.install import ( diff --git a/tests/unit/test_commands.py b/tests/unit/test_commands.py index 59cbf930ffb..c7abdc8d3b4 100644 --- a/tests/unit/test_commands.py +++ b/tests/unit/test_commands.py @@ -1,5 +1,5 @@ import pytest -from mock import patch +from unittest.mock import patch from pip._internal.cli.req_command import ( IndexGroupCommand, diff --git a/tests/unit/test_configuration.py b/tests/unit/test_configuration.py index 0a45fc136d5..65b584982e1 100644 --- a/tests/unit/test_configuration.py +++ b/tests/unit/test_configuration.py @@ -2,7 +2,7 @@ """ import pytest -from mock import MagicMock +from unittest.mock import MagicMock from pip._internal.configuration import get_configuration_files, kinds from pip._internal.exceptions import ConfigurationError diff --git a/tests/unit/test_direct_url_helpers.py b/tests/unit/test_direct_url_helpers.py index b0cb50c6eb9..533e245ccd6 100644 --- a/tests/unit/test_direct_url_helpers.py +++ b/tests/unit/test_direct_url_helpers.py @@ -1,6 +1,6 @@ from functools import partial -from mock import MagicMock, patch +from unittest.mock import MagicMock, patch from pip._internal.models.direct_url import ( DIRECT_URL_METADATA_NAME, diff --git a/tests/unit/test_finder.py b/tests/unit/test_finder.py index 69ebd5d4b7c..f3972f0f51f 100644 --- a/tests/unit/test_finder.py +++ b/tests/unit/test_finder.py @@ -2,7 +2,7 @@ import sys import pytest -from mock import Mock, patch +from unittest.mock import Mock, patch from pip._vendor.packaging.specifiers import SpecifierSet from pip._vendor.packaging.tags import Tag from pkg_resources import parse_version diff --git a/tests/unit/test_locations.py b/tests/unit/test_locations.py index 2ede236f91f..d65b4e024c7 100644 --- a/tests/unit/test_locations.py +++ b/tests/unit/test_locations.py @@ -9,7 +9,7 @@ import tempfile import pytest -from mock import Mock +from unittest.mock import Mock from pip._internal.locations import distutils_scheme diff --git a/tests/unit/test_logging.py b/tests/unit/test_logging.py index b3da43cb857..d3e32358df7 100644 --- a/tests/unit/test_logging.py +++ b/tests/unit/test_logging.py @@ -2,7 +2,7 @@ from threading import Thread import pytest -from mock import patch +from unittest.mock import patch from pip._internal.utils.logging import ( BrokenStdoutLoggingError, diff --git a/tests/unit/test_network_cache.py b/tests/unit/test_network_cache.py index 5f1d0a0975a..74f78e12948 100644 --- a/tests/unit/test_network_cache.py +++ b/tests/unit/test_network_cache.py @@ -1,7 +1,7 @@ import os import pytest -from mock import Mock +from unittest.mock import Mock from pip._vendor.cachecontrol.caches import FileCache from pip._internal.network.cache import SafeFileCache diff --git a/tests/unit/test_operations_prepare.py b/tests/unit/test_operations_prepare.py index 17fc94929c0..d18e2495c4f 100644 --- a/tests/unit/test_operations_prepare.py +++ b/tests/unit/test_operations_prepare.py @@ -4,7 +4,7 @@ from tempfile import mkdtemp import pytest -from mock import Mock, patch +from unittest.mock import Mock, patch from pip._internal.exceptions import HashMismatch from pip._internal.models.link import Link diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index c7be5fe1bac..74239f47923 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -6,7 +6,7 @@ from functools import partial import pytest -from mock import patch +from unittest.mock import patch from pip._vendor import pkg_resources from pip._vendor.packaging.markers import Marker from pip._vendor.packaging.requirements import Requirement diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py index 86f2731e9e3..c942d304215 100644 --- a/tests/unit/test_req_file.py +++ b/tests/unit/test_req_file.py @@ -5,7 +5,7 @@ import textwrap import pytest -from mock import patch +from unittest.mock import patch from pretend import stub import pip._internal.req.req_file # this will be monkeypatched diff --git a/tests/unit/test_req_uninstall.py b/tests/unit/test_req_uninstall.py index 90bf0d50fbc..516bc245a8e 100644 --- a/tests/unit/test_req_uninstall.py +++ b/tests/unit/test_req_uninstall.py @@ -2,7 +2,7 @@ import sys import pytest -from mock import Mock +from unittest.mock import Mock import pip._internal.req.req_uninstall from pip._internal.req.req_uninstall import ( diff --git a/tests/unit/test_resolution_legacy_resolver.py b/tests/unit/test_resolution_legacy_resolver.py index 90e98a691d2..4d483bf47a6 100644 --- a/tests/unit/test_resolution_legacy_resolver.py +++ b/tests/unit/test_resolution_legacy_resolver.py @@ -1,6 +1,6 @@ import logging -import mock +from unittest import mock import pytest from pip._vendor import pkg_resources diff --git a/tests/unit/test_target_python.py b/tests/unit/test_target_python.py index a314988ebc0..c311c5a744b 100644 --- a/tests/unit/test_target_python.py +++ b/tests/unit/test_target_python.py @@ -1,5 +1,5 @@ import pytest -from mock import patch +from unittest.mock import patch from pip._internal.models.target_python import TargetPython from tests.lib import CURRENT_PY_VERSION_INFO, pyversion diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 4caf4cc754b..a21959c87b1 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -12,7 +12,7 @@ from io import BytesIO import pytest -from mock import Mock, patch +from unittest.mock import Mock, patch from pip._internal.exceptions import HashMismatch, HashMissing, InstallationError from pip._internal.utils.deprecation import PipDeprecationWarning, deprecated diff --git a/tests/unit/test_utils_compatibility_tags.py b/tests/unit/test_utils_compatibility_tags.py index 735f024c122..6095f9ac9e7 100644 --- a/tests/unit/test_utils_compatibility_tags.py +++ b/tests/unit/test_utils_compatibility_tags.py @@ -1,7 +1,7 @@ import sysconfig import pytest -from mock import patch +from unittest.mock import patch from pip._internal.utils import compatibility_tags diff --git a/tests/unit/test_vcs.py b/tests/unit/test_vcs.py index d36f9f01deb..32227577091 100644 --- a/tests/unit/test_vcs.py +++ b/tests/unit/test_vcs.py @@ -2,7 +2,7 @@ from unittest import TestCase import pytest -from mock import patch +from unittest.mock import patch from pip._vendor.packaging.version import parse as parse_version from pip._internal.exceptions import BadCommand, InstallationError diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 52a5fe0436a..0bb955ea159 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -6,7 +6,7 @@ from email import message_from_string import pytest -from mock import patch +from unittest.mock import patch from pip._vendor.packaging.requirements import Requirement from pip._internal.exceptions import InstallationError diff --git a/tests/unit/test_wheel_builder.py b/tests/unit/test_wheel_builder.py index bd42f305956..c1655e84d1d 100644 --- a/tests/unit/test_wheel_builder.py +++ b/tests/unit/test_wheel_builder.py @@ -1,7 +1,7 @@ import logging import pytest -from mock import patch +from unittest.mock import patch from pip._internal import wheel_builder from pip._internal.models.link import Link From cfd12510861b633dcf01b2536a1a8879fe641b88 Mon Sep 17 00:00:00 2001 From: Andrey Bienkowski <hexagonrecursion@gmail.com> Date: Wed, 10 Feb 2021 13:30:13 +0300 Subject: [PATCH 2947/3170] Remove coding: utf-8 --- tests/data/packages/HackedEggInfo/setup.py | 2 -- tests/data/packages/SetupPyUTF8/setup.py | 2 -- tests/data/src/prjwithdatafile/setup.py | 1 - 3 files changed, 5 deletions(-) diff --git a/tests/data/packages/HackedEggInfo/setup.py b/tests/data/packages/HackedEggInfo/setup.py index 171f5a2a34b..9e872e0b54f 100644 --- a/tests/data/packages/HackedEggInfo/setup.py +++ b/tests/data/packages/HackedEggInfo/setup.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from setuptools import setup from setuptools.command import egg_info as orig_egg_info diff --git a/tests/data/packages/SetupPyUTF8/setup.py b/tests/data/packages/SetupPyUTF8/setup.py index 9b65f5e79fc..1962a0060fd 100644 --- a/tests/data/packages/SetupPyUTF8/setup.py +++ b/tests/data/packages/SetupPyUTF8/setup.py @@ -1,5 +1,3 @@ -# -*- coding: utf-8 -*- - from distutils.core import setup setup(name="SetupPyUTF8", diff --git a/tests/data/src/prjwithdatafile/setup.py b/tests/data/src/prjwithdatafile/setup.py index 94863b57b01..240b7ea10c5 100755 --- a/tests/data/src/prjwithdatafile/setup.py +++ b/tests/data/src/prjwithdatafile/setup.py @@ -1,4 +1,3 @@ -# -*- coding: utf-8 -*- from setuptools import setup setup( From 99a6a0753fbbac4d9a0b9139cf34cb1e7a188530 Mon Sep 17 00:00:00 2001 From: Andrey Bienkowski <hexagonrecursion@gmail.com> Date: Wed, 10 Feb 2021 13:32:24 +0300 Subject: [PATCH 2948/3170] Remove redundant `opject` parent --- tests/unit/test_self_check_outdated.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/unit/test_self_check_outdated.py b/tests/unit/test_self_check_outdated.py index 2e8663a9c01..b5d55f1c553 100644 --- a/tests/unit/test_self_check_outdated.py +++ b/tests/unit/test_self_check_outdated.py @@ -57,7 +57,7 @@ def get_metadata_lines(self, name): raise NotImplementedError('nope') -class MockEnvironment(object): +class MockEnvironment: def __init__(self, installer): self.installer = installer From 50db373adb9cfaedf278fff06806a61464acbc42 Mon Sep 17 00:00:00 2001 From: Andrey Bienkowski <hexagonrecursion@gmail.com> Date: Wed, 10 Feb 2021 13:38:21 +0300 Subject: [PATCH 2949/3170] Lint --- tests/conftest.py | 2 +- tests/functional/test_help.py | 3 ++- tests/lib/server.py | 2 +- tests/unit/resolution_resolvelib/test_resolver.py | 1 + tests/unit/test_base_command.py | 2 +- tests/unit/test_collector.py | 4 ++-- tests/unit/test_command_install.py | 2 +- tests/unit/test_commands.py | 3 ++- tests/unit/test_configuration.py | 3 ++- tests/unit/test_direct_url_helpers.py | 1 - tests/unit/test_finder.py | 2 +- tests/unit/test_locations.py | 2 +- tests/unit/test_logging.py | 2 +- tests/unit/test_network_cache.py | 2 +- tests/unit/test_operations_prepare.py | 2 +- tests/unit/test_req.py | 2 +- tests/unit/test_req_file.py | 2 +- tests/unit/test_req_uninstall.py | 2 +- tests/unit/test_resolution_legacy_resolver.py | 2 +- tests/unit/test_target_python.py | 3 ++- tests/unit/test_utils.py | 2 +- tests/unit/test_utils_compatibility_tags.py | 2 +- tests/unit/test_vcs.py | 2 +- tests/unit/test_wheel.py | 2 +- tests/unit/test_wheel_builder.py | 2 +- 25 files changed, 29 insertions(+), 25 deletions(-) diff --git a/tests/conftest.py b/tests/conftest.py index 6ca67f7ce4e..be8d1199ee5 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,9 +8,9 @@ import sys import time from contextlib import contextmanager +from unittest.mock import patch import pytest -from unittest.mock import patch from pip._vendor.contextlib2 import ExitStack, nullcontext from setuptools.wheel import Wheel diff --git a/tests/functional/test_help.py b/tests/functional/test_help.py index d9c1850396e..81491662f1c 100644 --- a/tests/functional/test_help.py +++ b/tests/functional/test_help.py @@ -1,6 +1,7 @@ -import pytest from unittest.mock import Mock +import pytest + from pip._internal.cli.status_codes import ERROR, SUCCESS from pip._internal.commands import commands_dict, create_command from pip._internal.exceptions import CommandError diff --git a/tests/lib/server.py b/tests/lib/server.py index 90db54e61c2..199df22398c 100644 --- a/tests/lib/server.py +++ b/tests/lib/server.py @@ -4,8 +4,8 @@ import threading from contextlib import contextmanager from textwrap import dedent - from unittest.mock import Mock + from pip._vendor.contextlib2 import nullcontext from werkzeug.serving import WSGIRequestHandler from werkzeug.serving import make_server as _make_server diff --git a/tests/unit/resolution_resolvelib/test_resolver.py b/tests/unit/resolution_resolvelib/test_resolver.py index adcf783b0ba..b9772fb17b5 100644 --- a/tests/unit/resolution_resolvelib/test_resolver.py +++ b/tests/unit/resolution_resolvelib/test_resolver.py @@ -1,4 +1,5 @@ from unittest import mock + import pytest from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.resolvelib.resolvers import Result diff --git a/tests/unit/test_base_command.py b/tests/unit/test_base_command.py index ffb079f7a49..9b57339c012 100644 --- a/tests/unit/test_base_command.py +++ b/tests/unit/test_base_command.py @@ -1,8 +1,8 @@ import logging import os +from unittest.mock import Mock, patch import pytest -from unittest.mock import Mock, patch from pip._internal.cli.base_command import Command from pip._internal.cli.status_codes import SUCCESS diff --git a/tests/unit/test_collector.py b/tests/unit/test_collector.py index 503a8e1f831..87c947e905a 100644 --- a/tests/unit/test_collector.py +++ b/tests/unit/test_collector.py @@ -4,11 +4,11 @@ import urllib.request import uuid from textwrap import dedent - from unittest import mock +from unittest.mock import Mock, patch + import pretend import pytest -from unittest.mock import Mock, patch from pip._vendor import html5lib, requests from pip._internal.exceptions import NetworkConnectionError diff --git a/tests/unit/test_command_install.py b/tests/unit/test_command_install.py index f631d77b037..5c623e679b7 100644 --- a/tests/unit/test_command_install.py +++ b/tests/unit/test_command_install.py @@ -1,7 +1,7 @@ import errno +from unittest.mock import patch import pytest -from unittest.mock import patch from pip._vendor.packaging.requirements import Requirement from pip._internal.commands.install import ( diff --git a/tests/unit/test_commands.py b/tests/unit/test_commands.py index c7abdc8d3b4..f34f7e5387b 100644 --- a/tests/unit/test_commands.py +++ b/tests/unit/test_commands.py @@ -1,6 +1,7 @@ -import pytest from unittest.mock import patch +import pytest + from pip._internal.cli.req_command import ( IndexGroupCommand, RequirementCommand, diff --git a/tests/unit/test_configuration.py b/tests/unit/test_configuration.py index 65b584982e1..9af6d051526 100644 --- a/tests/unit/test_configuration.py +++ b/tests/unit/test_configuration.py @@ -1,9 +1,10 @@ """Tests for all things related to the configuration """ -import pytest from unittest.mock import MagicMock +import pytest + from pip._internal.configuration import get_configuration_files, kinds from pip._internal.exceptions import ConfigurationError from tests.lib.configuration_helpers import ConfigurationMixin diff --git a/tests/unit/test_direct_url_helpers.py b/tests/unit/test_direct_url_helpers.py index 533e245ccd6..d0c3ffdc2fb 100644 --- a/tests/unit/test_direct_url_helpers.py +++ b/tests/unit/test_direct_url_helpers.py @@ -1,5 +1,4 @@ from functools import partial - from unittest.mock import MagicMock, patch from pip._internal.models.direct_url import ( diff --git a/tests/unit/test_finder.py b/tests/unit/test_finder.py index f3972f0f51f..162a9b356f3 100644 --- a/tests/unit/test_finder.py +++ b/tests/unit/test_finder.py @@ -1,8 +1,8 @@ import logging import sys +from unittest.mock import Mock, patch import pytest -from unittest.mock import Mock, patch from pip._vendor.packaging.specifiers import SpecifierSet from pip._vendor.packaging.tags import Tag from pkg_resources import parse_version diff --git a/tests/unit/test_locations.py b/tests/unit/test_locations.py index d65b4e024c7..3d4ec946245 100644 --- a/tests/unit/test_locations.py +++ b/tests/unit/test_locations.py @@ -7,9 +7,9 @@ import shutil import sys import tempfile +from unittest.mock import Mock import pytest -from unittest.mock import Mock from pip._internal.locations import distutils_scheme diff --git a/tests/unit/test_logging.py b/tests/unit/test_logging.py index d3e32358df7..20eaf9712d4 100644 --- a/tests/unit/test_logging.py +++ b/tests/unit/test_logging.py @@ -1,8 +1,8 @@ import logging from threading import Thread +from unittest.mock import patch import pytest -from unittest.mock import patch from pip._internal.utils.logging import ( BrokenStdoutLoggingError, diff --git a/tests/unit/test_network_cache.py b/tests/unit/test_network_cache.py index 74f78e12948..dc5e0655718 100644 --- a/tests/unit/test_network_cache.py +++ b/tests/unit/test_network_cache.py @@ -1,7 +1,7 @@ import os +from unittest.mock import Mock import pytest -from unittest.mock import Mock from pip._vendor.cachecontrol.caches import FileCache from pip._internal.network.cache import SafeFileCache diff --git a/tests/unit/test_operations_prepare.py b/tests/unit/test_operations_prepare.py index d18e2495c4f..f6122cebeb4 100644 --- a/tests/unit/test_operations_prepare.py +++ b/tests/unit/test_operations_prepare.py @@ -2,9 +2,9 @@ import shutil from shutil import rmtree from tempfile import mkdtemp +from unittest.mock import Mock, patch import pytest -from unittest.mock import Mock, patch from pip._internal.exceptions import HashMismatch from pip._internal.models.link import Link diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index 74239f47923..b655a187ebe 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -4,9 +4,9 @@ import sys import tempfile from functools import partial +from unittest.mock import patch import pytest -from unittest.mock import patch from pip._vendor import pkg_resources from pip._vendor.packaging.markers import Marker from pip._vendor.packaging.requirements import Requirement diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py index c942d304215..8d61e2b6cf3 100644 --- a/tests/unit/test_req_file.py +++ b/tests/unit/test_req_file.py @@ -3,9 +3,9 @@ import os import subprocess import textwrap +from unittest.mock import patch import pytest -from unittest.mock import patch from pretend import stub import pip._internal.req.req_file # this will be monkeypatched diff --git a/tests/unit/test_req_uninstall.py b/tests/unit/test_req_uninstall.py index 516bc245a8e..3697a1f2947 100644 --- a/tests/unit/test_req_uninstall.py +++ b/tests/unit/test_req_uninstall.py @@ -1,8 +1,8 @@ import os import sys +from unittest.mock import Mock import pytest -from unittest.mock import Mock import pip._internal.req.req_uninstall from pip._internal.req.req_uninstall import ( diff --git a/tests/unit/test_resolution_legacy_resolver.py b/tests/unit/test_resolution_legacy_resolver.py index 4d483bf47a6..9fed3765058 100644 --- a/tests/unit/test_resolution_legacy_resolver.py +++ b/tests/unit/test_resolution_legacy_resolver.py @@ -1,6 +1,6 @@ import logging - from unittest import mock + import pytest from pip._vendor import pkg_resources diff --git a/tests/unit/test_target_python.py b/tests/unit/test_target_python.py index c311c5a744b..c6af078a989 100644 --- a/tests/unit/test_target_python.py +++ b/tests/unit/test_target_python.py @@ -1,6 +1,7 @@ -import pytest from unittest.mock import patch +import pytest + from pip._internal.models.target_python import TargetPython from tests.lib import CURRENT_PY_VERSION_INFO, pyversion diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index a21959c87b1..8b4f9e79777 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -10,9 +10,9 @@ import sys import time from io import BytesIO +from unittest.mock import Mock, patch import pytest -from unittest.mock import Mock, patch from pip._internal.exceptions import HashMismatch, HashMissing, InstallationError from pip._internal.utils.deprecation import PipDeprecationWarning, deprecated diff --git a/tests/unit/test_utils_compatibility_tags.py b/tests/unit/test_utils_compatibility_tags.py index 6095f9ac9e7..ded7ecb069b 100644 --- a/tests/unit/test_utils_compatibility_tags.py +++ b/tests/unit/test_utils_compatibility_tags.py @@ -1,7 +1,7 @@ import sysconfig +from unittest.mock import patch import pytest -from unittest.mock import patch from pip._internal.utils import compatibility_tags diff --git a/tests/unit/test_vcs.py b/tests/unit/test_vcs.py index 32227577091..8fe5d3e7178 100644 --- a/tests/unit/test_vcs.py +++ b/tests/unit/test_vcs.py @@ -1,8 +1,8 @@ import os from unittest import TestCase +from unittest.mock import patch import pytest -from unittest.mock import patch from pip._vendor.packaging.version import parse as parse_version from pip._internal.exceptions import BadCommand, InstallationError diff --git a/tests/unit/test_wheel.py b/tests/unit/test_wheel.py index 0bb955ea159..3b257e27374 100644 --- a/tests/unit/test_wheel.py +++ b/tests/unit/test_wheel.py @@ -4,9 +4,9 @@ import os import textwrap from email import message_from_string +from unittest.mock import patch import pytest -from unittest.mock import patch from pip._vendor.packaging.requirements import Requirement from pip._internal.exceptions import InstallationError diff --git a/tests/unit/test_wheel_builder.py b/tests/unit/test_wheel_builder.py index c1655e84d1d..2f180723cc5 100644 --- a/tests/unit/test_wheel_builder.py +++ b/tests/unit/test_wheel_builder.py @@ -1,7 +1,7 @@ import logging +from unittest.mock import patch import pytest -from unittest.mock import patch from pip._internal import wheel_builder from pip._internal.models.link import Link From 45132fa7c9f9959426a35be38161d04ac30a1749 Mon Sep 17 00:00:00 2001 From: Andrey Bienkowski <hexagonrecursion@gmail.com> Date: Wed, 10 Feb 2021 14:42:28 +0300 Subject: [PATCH 2950/3170] Refactor a confusing int to str conversion --- tests/lib/venv.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lib/venv.py b/tests/lib/venv.py index 6dbdb4dc75b..bd6426a81b8 100644 --- a/tests/lib/venv.py +++ b/tests/lib/venv.py @@ -32,7 +32,7 @@ def _update_paths(self): self.site = Path(lib) / 'site-packages' # Workaround for https://github.com/pypa/virtualenv/issues/306 if hasattr(sys, "pypy_version_info"): - version_dir = '{0}'.format(*sys.version_info) + version_dir = str(sys.version_info.major) self.lib = Path(home, 'lib-python', version_dir) else: self.lib = Path(lib) From 20688ee8e8df1229312b0ed1a0b556ed7045c6b0 Mon Sep 17 00:00:00 2001 From: Andrey Bienkowski <hexagonrecursion@gmail.com> Date: Wed, 10 Feb 2021 16:14:42 +0300 Subject: [PATCH 2951/3170] Don't forget to close an open file --- tests/functional/test_install.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 7b331164b9c..9986c65635b 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1321,7 +1321,8 @@ def test_cleanup_after_failed_wheel(script, with_wheel): # One of the effects of not cleaning up is broken scripts: script_py = script.bin_path / "script.py" assert script_py.exists(), script_py - shebang = open(script_py).readline().strip() + with open(script_py) as f: + shebang = f.readline().strip() assert shebang != '#!python', shebang # OK, assert that we *said* we were cleaning up: # /!\ if in need to change this, also change test_pep517_no_legacy_cleanup From 510e691bc0cd880c771895f451d6f177de32673d Mon Sep 17 00:00:00 2001 From: Andrey Bienkowski <hexagonrecursion@gmail.com> Date: Wed, 10 Feb 2021 21:02:28 +0300 Subject: [PATCH 2952/3170] Undo removal of test docstring It was very hard for me to figure out what this test was designed to test. It turns out that when this test was promoted from unit to functional someone forgot to copy the docstring 9faa9aef2939ddb1fb4023ed15a209f59f123690 --- tests/functional/test_download.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/functional/test_download.py b/tests/functional/test_download.py index 95f1b63cf83..21715734314 100644 --- a/tests/functional/test_download.py +++ b/tests/functional/test_download.py @@ -850,6 +850,9 @@ def test_download_file_url_existing_bad_download( def test_download_http_url_bad_hash( shared_script, shared_data, tmpdir, mock_server ): + """ + If already-downloaded file has bad checksum, re-download. + """ download_dir = tmpdir / 'download' download_dir.mkdir() downloaded_path = download_dir / 'simple-1.0.tar.gz' From 8db690e8a00271ddaba4eee9b7200e329bfcd725 Mon Sep 17 00:00:00 2001 From: Andrey Bienkowski <hexagonrecursion@gmail.com> Date: Wed, 10 Feb 2021 21:59:30 +0300 Subject: [PATCH 2953/3170] Fix MockServer not working right on python<3.8 --- tests/conftest.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/conftest.py b/tests/conftest.py index be8d1199ee5..39d75cc199e 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -551,7 +551,7 @@ def get_requests(self): """ assert not self._running, "cannot get mock from running server" return [ - call.args[0] for call in self._server.mock.call_args_list + call[0][0] for call in self._server.mock.call_args_list ] From f30f515a6aaebe0d0afd69c917e2166b51946a3e Mon Sep 17 00:00:00 2001 From: Andrey Bienkowski <hexagonrecursion@gmail.com> Date: Wed, 10 Feb 2021 22:03:20 +0300 Subject: [PATCH 2954/3170] Fix test_install_sends_client_cert --- tests/functional/test_install.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 1c0650c6f5f..c4ed541d4e7 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1850,7 +1850,7 @@ def test_install_sends_client_cert(install_args, script, cert_factory, data): assert server.mock.call_count == 2 for call_args in server.mock.call_args_list: - environ, _ = call_args.args + environ, _ = call_args[0] assert "SSL_CLIENT_CERT" in environ assert environ["SSL_CLIENT_CERT"] From 08bc2ab7f95f1622d631455a260362fed814687c Mon Sep 17 00:00:00 2001 From: Andrey Bienkowski <hexagonrecursion@gmail.com> Date: Thu, 11 Feb 2021 09:48:14 +0300 Subject: [PATCH 2955/3170] Add reminders to revert python3.7 compat fixes --- tests/conftest.py | 2 ++ tests/functional/test_install.py | 2 ++ 2 files changed, 4 insertions(+) diff --git a/tests/conftest.py b/tests/conftest.py index 39d75cc199e..f3f33d3d711 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -550,6 +550,8 @@ def get_requests(self): """Get environ for each received request. """ assert not self._running, "cannot get mock from running server" + # Legacy: replace call[0][0] with call.args[0] + # when pip drops support for python3.7 return [ call[0][0] for call in self._server.mock.call_args_list ] diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index c4ed541d4e7..2cfb3d3d272 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1850,6 +1850,8 @@ def test_install_sends_client_cert(install_args, script, cert_factory, data): assert server.mock.call_count == 2 for call_args in server.mock.call_args_list: + # Legacy: replace call_args[0] with call_args.args + # when pip drops support for python3.7 environ, _ = call_args[0] assert "SSL_CLIENT_CERT" in environ assert environ["SSL_CLIENT_CERT"] From c6deb376efe3443ee534c47643623e076d478718 Mon Sep 17 00:00:00 2001 From: David D Lowe <daviddlowe.flimm@gmail.com> Date: Thu, 11 Feb 2021 14:05:30 +0100 Subject: [PATCH 2956/3170] Document that pip supports Python 3.9 --- docs/html/installing.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/html/installing.rst b/docs/html/installing.rst index 9e2c7051ef3..95b21899dc6 100644 --- a/docs/html/installing.rst +++ b/docs/html/installing.rst @@ -206,7 +206,7 @@ Upgrading pip Python and OS Compatibility =========================== -pip works with CPython versions 3.6, 3.7, 3.8 and also PyPy. +pip works with CPython versions 3.6, 3.7, 3.8, 3.9 and also PyPy. This means pip works on the latest patch version of each of these minor versions. Previous patch versions are supported on a best effort approach. From 9b2cb894ba6ef667e093edd12bd37d114468c203 Mon Sep 17 00:00:00 2001 From: Andrey Bienkowski <hexagonrecursion@gmail.com> Date: Sat, 13 Feb 2021 09:27:17 +0300 Subject: [PATCH 2957/3170] Convert more str.format() calls to f-strings --- docs/pip_sphinxext.py | 6 +-- src/pip/_internal/cli/main_parser.py | 2 +- src/pip/_internal/cli/spinners.py | 3 +- src/pip/_internal/commands/search.py | 4 +- src/pip/_internal/commands/uninstall.py | 4 +- src/pip/_internal/models/link.py | 3 +- src/pip/_internal/req/constructors.py | 6 +-- src/pip/_internal/req/req_install.py | 3 +- src/pip/_internal/req/req_set.py | 2 +- src/pip/_internal/utils/filesystem.py | 3 +- src/pip/_internal/utils/urls.py | 6 +-- src/pip/_internal/vcs/subversion.py | 3 +- src/pip/_internal/vcs/versioncontrol.py | 5 +-- tests/conftest.py | 4 +- tests/data/src/chattymodule/setup.py | 2 +- tests/functional/test_build_env.py | 8 ++-- tests/functional/test_completion.py | 2 +- tests/functional/test_freeze.py | 12 +++--- tests/functional/test_install.py | 31 ++++++--------- tests/functional/test_install_compat.py | 6 +-- tests/functional/test_install_config.py | 2 +- tests/functional/test_install_extras.py | 4 +- tests/functional/test_install_reqs.py | 24 ++++++------ tests/functional/test_install_upgrade.py | 3 +- tests/functional/test_install_user.py | 18 ++++----- tests/functional/test_uninstall_user.py | 2 +- tests/functional/test_wheel.py | 32 ++++++---------- tests/lib/__init__.py | 48 +++++++++++------------- tests/lib/test_lib.py | 4 +- tests/unit/test_collector.py | 2 +- tests/unit/test_req.py | 20 +++++----- tests/unit/test_req_file.py | 12 +++--- 32 files changed, 123 insertions(+), 163 deletions(-) diff --git a/docs/pip_sphinxext.py b/docs/pip_sphinxext.py index df4390d8103..1ce526e0d6c 100644 --- a/docs/pip_sphinxext.py +++ b/docs/pip_sphinxext.py @@ -50,10 +50,10 @@ class PipOptions(rst.Directive): def _format_option(self, option, cmd_name=None): bookmark_line = ( - ".. _`{cmd_name}_{option._long_opts[0]}`:" + f".. _`{cmd_name}_{option._long_opts[0]}`:" if cmd_name else - ".. _`{option._long_opts[0]}`:" - ).format(**locals()) + f".. _`{option._long_opts[0]}`:" + ) line = ".. option:: " if option._short_opts: line += option._short_opts[0] diff --git a/src/pip/_internal/cli/main_parser.py b/src/pip/_internal/cli/main_parser.py index fcee6a2c234..bf4ffb8359b 100644 --- a/src/pip/_internal/cli/main_parser.py +++ b/src/pip/_internal/cli/main_parser.py @@ -45,7 +45,7 @@ def create_main_parser(): # create command listing for description description = [''] + [ - '{name:27} {command_info.summary}'.format(**locals()) + f'{name:27} {command_info.summary}' for name, command_info in commands_dict.items() ] parser.description = '\n'.join(description) diff --git a/src/pip/_internal/cli/spinners.py b/src/pip/_internal/cli/spinners.py index 05ec2dcc765..2fc3a43f5e6 100644 --- a/src/pip/_internal/cli/spinners.py +++ b/src/pip/_internal/cli/spinners.py @@ -104,8 +104,7 @@ def finish(self, final_status): # type: (str) -> None if self._finished: return - self._update( - "finished with status '{final_status}'".format(**locals())) + self._update(f"finished with status '{final_status}'") self._finished = True diff --git a/src/pip/_internal/commands/search.py b/src/pip/_internal/commands/search.py index 90a5b512d69..3d881f1550d 100644 --- a/src/pip/_internal/commands/search.py +++ b/src/pip/_internal/commands/search.py @@ -140,9 +140,7 @@ def print_results(hits, name_column_width=None, terminal_width=None): summary = ('\n' + ' ' * (name_column_width + 3)).join( summary_lines) - line = '{name_latest:{name_column_width}} - {summary}'.format( - name_latest='{name} ({latest})'.format(**locals()), - **locals()) + line = f'{{{name} ({latest}):{name_column_width}}} - {summary}' try: write_output(line) dist = env.get_distribution(name) diff --git a/src/pip/_internal/commands/uninstall.py b/src/pip/_internal/commands/uninstall.py index 6dc96c3d630..2603a25ebfd 100644 --- a/src/pip/_internal/commands/uninstall.py +++ b/src/pip/_internal/commands/uninstall.py @@ -75,8 +75,8 @@ def run(self, options, args): reqs_to_uninstall[canonicalize_name(req.name)] = req if not reqs_to_uninstall: raise InstallationError( - 'You must give at least one requirement to {self.name} (see ' - '"pip help {self.name}")'.format(**locals()) + f'You must give at least one requirement to {self.name} (see ' + f'"pip help {self.name}")' ) protect_pip_from_modification_on_windows( diff --git a/src/pip/_internal/models/link.py b/src/pip/_internal/models/link.py index 06a7ceb3fce..5259c4328d5 100644 --- a/src/pip/_internal/models/link.py +++ b/src/pip/_internal/models/link.py @@ -113,8 +113,7 @@ def filename(self): return netloc name = urllib.parse.unquote(name) - assert name, ( - 'URL {self._url!r} produced no filename'.format(**locals())) + assert name, f'URL {self._url!r} produced no filename' return name @property diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index cfb1951b6b8..6aa6012ac96 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -261,8 +261,8 @@ def _get_url_from_path(path, name): if is_installable_dir(path): return path_to_url(path) raise InstallationError( - "Directory {name!r} is not installable. Neither 'setup.py' " - "nor 'pyproject.toml' found.".format(**locals()) + f"Directory {name!r} is not installable. Neither 'setup.py' " + "nor 'pyproject.toml' found." ) if not is_archive_file(path): return None @@ -319,7 +319,7 @@ def parse_req_from_line(name, line_source): # wheel file if link.is_wheel: wheel = Wheel(link.filename) # can raise InvalidWheelFilename - req_as_string = "{wheel.name}=={wheel.version}".format(**locals()) + req_as_string = f"{wheel.name}=={wheel.version}" else: # set the req to the egg fragment. when it's not there, this # will become an 'unnamed' requirement diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 6d0aa30b418..9121f693cec 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -664,8 +664,7 @@ def _get_archive_name(self, path, parentdir, rootdir): def _clean_zip_name(name, prefix): # type: (str, str) -> str assert name.startswith(prefix + os.path.sep), ( - "name {name!r} doesn't start with prefix {prefix!r}" - .format(**locals()) + f"name {name!r} doesn't start with prefix {prefix!r}" ) name = name[len(prefix) + 1:] name = name.replace(os.path.sep, '/') diff --git a/src/pip/_internal/req/req_set.py b/src/pip/_internal/req/req_set.py index fa58be66341..cc94d9c393e 100644 --- a/src/pip/_internal/req/req_set.py +++ b/src/pip/_internal/req/req_set.py @@ -194,7 +194,7 @@ def get_requirement(self, name): if project_name in self.requirements: return self.requirements[project_name] - raise KeyError("No project with the name {name!r}".format(**locals())) + raise KeyError(f"No project with the name {name!r}") @property def all_requirements(self): diff --git a/src/pip/_internal/utils/filesystem.py b/src/pip/_internal/utils/filesystem.py index 1af8c10eaaa..a2b7c73e091 100644 --- a/src/pip/_internal/utils/filesystem.py +++ b/src/pip/_internal/utils/filesystem.py @@ -67,8 +67,7 @@ def copy2_fixed(src, dest): pass else: if is_socket_file: - raise shutil.SpecialFileError( - "`{f}` is a socket".format(**locals())) + raise shutil.SpecialFileError(f"`{f}` is a socket") raise diff --git a/src/pip/_internal/utils/urls.py b/src/pip/_internal/utils/urls.py index 0ef063c2198..8437dfa7d5f 100644 --- a/src/pip/_internal/utils/urls.py +++ b/src/pip/_internal/utils/urls.py @@ -33,8 +33,7 @@ def url_to_path(url): Convert a file: URL to a path. """ assert url.startswith('file:'), ( - "You can only turn file: urls into filenames (not {url!r})" - .format(**locals())) + f"You can only turn file: urls into filenames (not {url!r})") _, netloc, path, _, _ = urllib.parse.urlsplit(url) @@ -46,8 +45,7 @@ def url_to_path(url): netloc = '\\\\' + netloc else: raise ValueError( - 'non-local file URIs are not supported on this platform: {url!r}' - .format(**locals()) + f'non-local file URIs are not supported on this platform: {url!r}' ) path = urllib.request.url2pathname(netloc + path) diff --git a/src/pip/_internal/vcs/subversion.py b/src/pip/_internal/vcs/subversion.py index 56e8d4b4844..656ad123f56 100644 --- a/src/pip/_internal/vcs/subversion.py +++ b/src/pip/_internal/vcs/subversion.py @@ -158,8 +158,7 @@ def _get_svn_url_rev(cls, location): elif data.startswith('<?xml'): match = _svn_xml_url_re.search(data) if not match: - raise ValueError( - 'Badly formatted data: {data!r}'.format(**locals())) + raise ValueError(f'Badly formatted data: {data!r}') url = match.group(1) # get repository URL revs = [int(m.group(1)) for m in _svn_rev_re.finditer(data)] + [0] else: diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index 218de58a6d4..75fed614e5e 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -690,9 +690,8 @@ def run_command( # errno.ENOENT = no such file or directory # In other words, the VCS executable isn't available raise BadCommand( - 'Cannot find command {cls.name!r} - do you have ' - '{cls.name!r} installed and in your ' - 'PATH?'.format(**locals())) + f'Cannot find command {cls.name!r} - do you have ' + f'{cls.name!r} installed and in your PATH?') @classmethod def is_repository_directory(cls, path): diff --git a/tests/conftest.py b/tests/conftest.py index 0bb69dae6d7..d6ce04f6a03 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -287,12 +287,12 @@ def not_code_files_and_folders(path, names): def _common_wheel_editable_install(tmpdir_factory, common_wheels, package): wheel_candidates = list( - common_wheels.glob('{package}-*.whl'.format(**locals()))) + common_wheels.glob(f'{package}-*.whl')) assert len(wheel_candidates) == 1, wheel_candidates install_dir = Path(str(tmpdir_factory.mktemp(package))) / 'install' Wheel(wheel_candidates[0]).install_as_egg(install_dir) (install_dir / 'EGG-INFO').rename( - install_dir / '{package}.egg-info'.format(**locals())) + install_dir / f'{package}.egg-info') assert compileall.compile_dir(str(install_dir), quiet=1) return install_dir diff --git a/tests/data/src/chattymodule/setup.py b/tests/data/src/chattymodule/setup.py index 01d7720765f..68099f2f8c9 100644 --- a/tests/data/src/chattymodule/setup.py +++ b/tests/data/src/chattymodule/setup.py @@ -5,7 +5,7 @@ from setuptools import setup -print("HELLO FROM CHATTYMODULE {sys.argv[1]}".format(**locals())) +print(f"HELLO FROM CHATTYMODULE {sys.argv[1]}") print(os.environ) print(sys.argv) if "--fail" in sys.argv: diff --git a/tests/functional/test_build_env.py b/tests/functional/test_build_env.py index 7a392f42646..b3f51a8808e 100644 --- a/tests/functional/test_build_env.py +++ b/tests/functional/test_build_env.py @@ -79,15 +79,15 @@ def test_build_env_allow_only_one_install(script): for prefix in ('normal', 'overlay'): build_env.install_requirements( finder, ['foo'], prefix, - 'installing foo in {prefix}'.format(**locals())) + f'installing foo in {prefix}') with pytest.raises(AssertionError): build_env.install_requirements( finder, ['bar'], prefix, - 'installing bar in {prefix}'.format(**locals())) + f'installing bar in {prefix}') with pytest.raises(AssertionError): build_env.install_requirements( finder, [], prefix, - 'installing in {prefix}'.format(**locals())) + f'installing in {prefix}') def test_build_env_requirements_check(script): @@ -201,7 +201,7 @@ def test_build_env_isolation(script): pass else: print( - 'imported `pkg` from `{pkg.__file__}`'.format(**locals()), + f'imported `pkg` from `{pkg.__file__}`', file=sys.stderr) print('system sites:\n ' + '\n '.join(sorted({ get_python_lib(plat_specific=0), diff --git a/tests/functional/test_completion.py b/tests/functional/test_completion.py index a3986811b6f..8a7464982ee 100644 --- a/tests/functional/test_completion.py +++ b/tests/functional/test_completion.py @@ -230,7 +230,7 @@ def test_completion_not_files_after_nonexpecting_option( (e.g. ``pip install``) """ res, env = autocomplete( - words=('pip install {cl_opts} r'.format(**locals())), + words=(f'pip install {cl_opts} r'), cword='2', cwd=data.completion_paths, ) diff --git a/tests/functional/test_freeze.py b/tests/functional/test_freeze.py index 5d3d4968619..4fd91b5d8e5 100644 --- a/tests/functional/test_freeze.py +++ b/tests/functional/test_freeze.py @@ -42,7 +42,7 @@ def _check_output(result, expected): actual = distribute_re.sub('', actual) def banner(msg): - return '\n========== {msg} ==========\n'.format(**locals()) + return f'\n========== {msg} ==========\n' assert checker.check_output(expected, actual, ELLIPSIS), ( banner('EXPECTED') + expected + banner('ACTUAL') + actual + @@ -272,7 +272,7 @@ def test_freeze_git_clone(script, tmpdir): _check_output(result.stdout, expected) result = script.pip( - 'freeze', '-f', '{repo_dir}#egg=pip_test_package'.format(**locals()), + 'freeze', '-f', f'{repo_dir}#egg=pip_test_package', expect_stderr=True, ) expected = textwrap.dedent( @@ -337,7 +337,7 @@ def test_freeze_git_clone_srcdir(script, tmpdir): _check_output(result.stdout, expected) result = script.pip( - 'freeze', '-f', '{repo_dir}#egg=pip_test_package'.format(**locals()), + 'freeze', '-f', f'{repo_dir}#egg=pip_test_package', expect_stderr=True, ) expected = textwrap.dedent( @@ -378,7 +378,7 @@ def test_freeze_mercurial_clone_srcdir(script, tmpdir): _check_output(result.stdout, expected) result = script.pip( - 'freeze', '-f', '{repo_dir}#egg=pip_test_package'.format(**locals()), + 'freeze', '-f', f'{repo_dir}#egg=pip_test_package', expect_stderr=True, ) expected = textwrap.dedent( @@ -473,7 +473,7 @@ def test_freeze_mercurial_clone(script, tmpdir): _check_output(result.stdout, expected) result = script.pip( - 'freeze', '-f', '{repo_dir}#egg=pip_test_package'.format(**locals()), + 'freeze', '-f', f'{repo_dir}#egg=pip_test_package', expect_stderr=True, ) expected = textwrap.dedent( @@ -513,7 +513,7 @@ def test_freeze_bazaar_clone(script, tmpdir): result = script.pip( 'freeze', '-f', - '{checkout_path}/#egg=django-wikiapp'.format(**locals()), + f'{checkout_path}/#egg=django-wikiapp', expect_stderr=True, ) expected = textwrap.dedent("""\ diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 1c0650c6f5f..a836b4ad790 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -199,7 +199,7 @@ def test_pip_second_command_line_interface_works( if pyversion_tuple < (2, 7, 9): kwargs['expect_stderr'] = True - args = ['pip{pyversion}'.format(**globals())] + args = [f'pip{pyversion}'] args.extend(['install', 'INITools==0.2']) args.extend(['-f', data.packages]) result = script.run(*args, **kwargs) @@ -727,11 +727,10 @@ def test_install_using_install_option_and_editable(script, tmpdir): """ folder = 'script_folder' script.scratch_path.joinpath(folder).mkdir() - url = 'git+git://github.com/pypa/pip-test-package' + url = local_checkout('git+git://github.com/pypa/pip-test-package', tmpdir) result = script.pip( - 'install', '-e', '{url}#egg=pip-test-package' - .format(url=local_checkout(url, tmpdir)), - '--install-option=--script-dir={folder}'.format(**locals()), + 'install', '-e', f'{url}#egg=pip-test-package', + f'--install-option=--script-dir={folder}', expect_stderr=True) script_file = ( script.venv / 'src' / 'pip-test-package' / @@ -799,10 +798,7 @@ def test_install_folder_using_slash_in_the_end(script, with_wheel): pkg_path = script.scratch_path / 'mock' pkg_path.joinpath("setup.py").write_text(mock100_setup_py) result = script.pip('install', 'mock' + os.path.sep) - dist_info_folder = ( - script.site_packages / - 'mock-100.1.dist-info' - ) + dist_info_folder = script.site_packages / 'mock-100.1.dist-info' result.did_create(dist_info_folder) @@ -815,10 +811,7 @@ def test_install_folder_using_relative_path(script, with_wheel): pkg_path = script.scratch_path / 'initools' / 'mock' pkg_path.joinpath("setup.py").write_text(mock100_setup_py) result = script.pip('install', Path('initools') / 'mock') - dist_info_folder = ( - script.site_packages / - 'mock-100.1.dist-info'.format(**globals()) - ) + dist_info_folder = script.site_packages / 'mock-100.1.dist-info' result.did_create(dist_info_folder) @@ -1294,7 +1287,7 @@ def test_install_subprocess_output_handling(script, data): def test_install_log(script, data, tmpdir): # test that verbose logs go to "--log" file f = tmpdir.joinpath("log.txt") - args = ['--log={f}'.format(**locals()), + args = [f'--log={f}', 'install', data.src.joinpath('chattymodule')] result = script.pip(*args) assert 0 == result.stdout.count("HELLO FROM CHATTYMODULE") @@ -1448,7 +1441,7 @@ def test_install_editable_with_wrong_egg_name(script, resolver_variant): """)) result = script.pip( 'install', '--editable', - 'file://{pkga_path}#egg=pkgb'.format(**locals()), + f'file://{pkga_path}#egg=pkgb', expect_error=(resolver_variant == "2020-resolver"), ) assert ("Generating metadata for package pkgb produced metadata " @@ -1534,7 +1527,7 @@ def test_install_incompatible_python_requires_editable(script): """)) result = script.pip( 'install', - '--editable={pkga_path}'.format(**locals()), + f'--editable={pkga_path}', expect_error=True) assert _get_expected_error_text() in result.stderr, str(result) @@ -1651,7 +1644,7 @@ def test_installed_files_recorded_in_deterministic_order(script, data): to_install = data.packages.joinpath("FSPkg") result = script.pip('install', to_install) fspkg_folder = script.site_packages / 'fspkg' - egg_info = 'FSPkg-0.1.dev0-py{pyversion}.egg-info'.format(**globals()) + egg_info = f'FSPkg-0.1.dev0-py{pyversion}.egg-info' installed_files_path = ( script.site_packages / egg_info / 'installed-files.txt' ) @@ -1714,10 +1707,10 @@ def test_target_install_ignores_distutils_config_install_prefix(script): 'pydistutils.cfg' if sys.platform == 'win32' else '.pydistutils.cfg') distutils_config.write_text(textwrap.dedent( - ''' + f''' [install] prefix={prefix} - '''.format(**locals()))) + ''')) target = script.scratch_path / 'target' result = script.pip_install_local('simplewheel', '-t', target) diff --git a/tests/functional/test_install_compat.py b/tests/functional/test_install_compat.py index a5a0df65218..44b9b290e5d 100644 --- a/tests/functional/test_install_compat.py +++ b/tests/functional/test_install_compat.py @@ -26,7 +26,7 @@ def test_debian_egg_name_workaround(script): egg_info = os.path.join( script.site_packages, - "INITools-0.2-py{pyversion}.egg-info".format(**globals())) + f"INITools-0.2-py{pyversion}.egg-info") # Debian only removes pyversion for global installs, not inside a venv # so even if this test runs on a Debian/Ubuntu system with broken @@ -34,14 +34,14 @@ def test_debian_egg_name_workaround(script): # .egg-info result.did_create( egg_info, - message="Couldn't find {egg_info}".format(**locals()) + message=f"Couldn't find {egg_info}" ) # The Debian no-pyversion version of the .egg-info mangled = os.path.join(script.site_packages, "INITools-0.2.egg-info") result.did_not_create( mangled, - message="Found unexpected {mangled}".format(**locals()) + message=f"Found unexpected {mangled}" ) # Simulate a Debian install by copying the .egg-info to their name for it diff --git a/tests/functional/test_install_config.py b/tests/functional/test_install_config.py index 41be6fbbbb6..dd08f410560 100644 --- a/tests/functional/test_install_config.py +++ b/tests/functional/test_install_config.py @@ -112,7 +112,7 @@ def test_command_line_appends_correctly(script, data): """ script.environ['PIP_FIND_LINKS'] = ( - 'https://test.pypi.org {data.find_links}'.format(**locals()) + f'https://test.pypi.org {data.find_links}' ) result = script.pip( 'install', '-vvv', 'INITools', '--trusted-host', diff --git a/tests/functional/test_install_extras.py b/tests/functional/test_install_extras.py index e960100729f..de1ee3795ea 100644 --- a/tests/functional/test_install_extras.py +++ b/tests/functional/test_install_extras.py @@ -136,7 +136,7 @@ def test_install_special_extra(script): """)) result = script.pip( - 'install', '--no-index', '{pkga_path}[Hop_hOp-hoP]'.format(**locals()), + 'install', '--no-index', f'{pkga_path}[Hop_hOp-hoP]', expect_error=True) assert ( "Could not find a version that satisfies the requirement missing_pkg" @@ -165,7 +165,7 @@ def test_install_extra_merging(script, data, extra_to_install, simple_version): """)) result = script.pip_install_local( - '{pkga_path}{extra_to_install}'.format(**locals()), + f'{pkga_path}{extra_to_install}', ) assert f'Successfully installed pkga-0.1 simple-{simple_version}' in result.stdout diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index 9c35aee8320..ac5ff087b07 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -68,11 +68,11 @@ def test_requirements_file(script, with_wheel): """ other_lib_name, other_lib_version = 'anyjson', '0.3' - script.scratch_path.joinpath("initools-req.txt").write_text(textwrap.dedent("""\ + script.scratch_path.joinpath("initools-req.txt").write_text(textwrap.dedent(f"""\ INITools==0.2 # and something else to test out: {other_lib_name}<={other_lib_version} - """.format(**locals()))) + """)) result = script.pip( 'install', '-r', script.scratch_path / 'initools-req.txt' ) @@ -178,15 +178,14 @@ def test_multiple_requirements_files(script, tmpdir, with_wheel): other_lib_name ), ) - script.scratch_path.joinpath( - "{other_lib_name}-req.txt".format(**locals())).write_text( - "{other_lib_name}<={other_lib_version}".format(**locals()) + script.scratch_path.joinpath(f"{other_lib_name}-req.txt").write_text( + f"{other_lib_name}<={other_lib_version}" ) result = script.pip( 'install', '-r', script.scratch_path / 'initools-req.txt' ) assert result.files_created[script.site_packages / other_lib_name].dir - fn = '{other_lib_name}-{other_lib_version}.dist-info'.format(**locals()) + fn = f'{other_lib_name}-{other_lib_version}.dist-info' assert result.files_created[script.site_packages / fn].dir result.did_create(script.venv / 'src' / 'initools') @@ -295,9 +294,9 @@ def test_wheel_user_with_prefix_in_pydistutils_cfg( user_cfg = os.path.join(os.path.expanduser('~'), user_filename) script.scratch_path.joinpath("bin").mkdir() with open(user_cfg, "w") as cfg: - cfg.write(textwrap.dedent(""" + cfg.write(textwrap.dedent(f""" [install] - prefix={script.scratch_path}""".format(**locals()))) + prefix={script.scratch_path}""")) result = script.pip( 'install', '--user', '--no-index', @@ -559,8 +558,7 @@ def test_install_distribution_duplicate_extras(script, data): package_name = to_install + "[bar]" with pytest.raises(AssertionError): result = script.pip_install_local(package_name, package_name) - expected = ( - 'Double requirement given: {package_name}'.format(**locals())) + expected = (f'Double requirement given: {package_name}') assert expected in result.stderr @@ -571,7 +569,7 @@ def test_install_distribution_union_with_constraints( ): to_install = data.packages.joinpath("LocalExtras") script.scratch_path.joinpath("constraints.txt").write_text( - "{to_install}[bar]".format(**locals())) + f"{to_install}[bar]") result = script.pip_install_local( '-c', script.scratch_path / 'constraints.txt', to_install + '[baz]', allow_stderr_warning=True, @@ -647,9 +645,9 @@ def test_install_unsupported_wheel_file(script, data): # Trying to install a local wheel with an incompatible version/type # should fail. path = data.packages.joinpath("simple.dist-0.1-py1-none-invalid.whl") - script.scratch_path.joinpath("wheel-file.txt").write_text(textwrap.dedent("""\ + script.scratch_path.joinpath("wheel-file.txt").write_text(textwrap.dedent(f"""\ {path} - """.format(**locals()))) + """)) result = script.pip( 'install', '-r', script.scratch_path / 'wheel-file.txt', expect_error=True, diff --git a/tests/functional/test_install_upgrade.py b/tests/functional/test_install_upgrade.py index 46aac8f9d26..d7586cd5835 100644 --- a/tests/functional/test_install_upgrade.py +++ b/tests/functional/test_install_upgrade.py @@ -421,8 +421,7 @@ class TestUpgradeDistributeToSetuptools: def prep_ve(self, script, version, pip_src, distribute=False): self.script = script - self.script.pip_install_local( - 'virtualenv=={version}'.format(**locals())) + self.script.pip_install_local(f'virtualenv=={version}') args = ['virtualenv', self.script.scratch_path / 'VE'] if distribute: args.insert(1, '--distribute') diff --git a/tests/functional/test_install_user.py b/tests/functional/test_install_user.py index c5d7acced80..538556ed907 100644 --- a/tests/functional/test_install_user.py +++ b/tests/functional/test_install_user.py @@ -118,8 +118,7 @@ def test_install_user_conflict_in_usersite(self, script): # usersite has 0.1 # we still test for egg-info because no-binary implies setup.py install egg_info_folder = ( - script.user_site / - 'INITools-0.1-py{pyversion}.egg-info'.format(**globals()) + script.user_site / f'INITools-0.1-py{pyversion}.egg-info' ) initools_v3_file = ( # file only in 0.3 @@ -146,8 +145,7 @@ def test_install_user_conflict_in_globalsite(self, virtualenv, script): # usersite has 0.1 # we still test for egg-info because no-binary implies setup.py install egg_info_folder = ( - script.user_site / - 'INITools-0.1-py{pyversion}.egg-info'.format(**globals()) + script.user_site / f'INITools-0.1-py{pyversion}.egg-info' ) initools_folder = script.user_site / 'initools' result2.did_create(egg_info_folder) @@ -156,7 +154,7 @@ def test_install_user_conflict_in_globalsite(self, virtualenv, script): # site still has 0.2 (can't look in result1; have to check) egg_info_folder = ( script.base_path / script.site_packages / - 'INITools-0.2-py{pyversion}.egg-info'.format(**globals()) + f'INITools-0.2-py{pyversion}.egg-info' ) initools_folder = script.base_path / script.site_packages / 'initools' assert isdir(egg_info_folder) @@ -178,8 +176,7 @@ def test_upgrade_user_conflict_in_globalsite(self, virtualenv, script): # usersite has 0.3.1 # we still test for egg-info because no-binary implies setup.py install egg_info_folder = ( - script.user_site / - 'INITools-0.3.1-py{pyversion}.egg-info'.format(**globals()) + script.user_site / f'INITools-0.3.1-py{pyversion}.egg-info' ) initools_folder = script.user_site / 'initools' result2.did_create(egg_info_folder) @@ -188,7 +185,7 @@ def test_upgrade_user_conflict_in_globalsite(self, virtualenv, script): # site still has 0.2 (can't look in result1; have to check) egg_info_folder = ( script.base_path / script.site_packages / - 'INITools-0.2-py{pyversion}.egg-info'.format(**globals()) + f'INITools-0.2-py{pyversion}.egg-info' ) initools_folder = script.base_path / script.site_packages / 'initools' assert isdir(egg_info_folder), result2.stdout @@ -213,8 +210,7 @@ def test_install_user_conflict_in_globalsite_and_usersite( # usersite has 0.1 # we still test for egg-info because no-binary implies setup.py install egg_info_folder = ( - script.user_site / - 'INITools-0.1-py{pyversion}.egg-info'.format(**globals()) + script.user_site / f'INITools-0.1-py{pyversion}.egg-info' ) initools_v3_file = ( # file only in 0.3 @@ -227,7 +223,7 @@ def test_install_user_conflict_in_globalsite_and_usersite( # site still has 0.2 (can't just look in result1; have to check) egg_info_folder = ( script.base_path / script.site_packages / - 'INITools-0.2-py{pyversion}.egg-info'.format(**globals()) + f'INITools-0.2-py{pyversion}.egg-info' ) initools_folder = script.base_path / script.site_packages / 'initools' assert isdir(egg_info_folder) diff --git a/tests/functional/test_uninstall_user.py b/tests/functional/test_uninstall_user.py index 2dbf032ac38..7a0006d474b 100644 --- a/tests/functional/test_uninstall_user.py +++ b/tests/functional/test_uninstall_user.py @@ -46,7 +46,7 @@ def test_uninstall_from_usersite_with_dist_in_global_site( # keep checking for egg-info because no-binary implies setup.py install egg_info_folder = ( script.base_path / script.site_packages / - 'pip_test_package-0.1-py{pyversion}.egg-info'.format(**globals()) + f'pip_test_package-0.1-py{pyversion}.egg-info' ) assert isdir(egg_info_folder) diff --git a/tests/functional/test_wheel.py b/tests/functional/test_wheel.py index c5e16803983..da040c30765 100644 --- a/tests/functional/test_wheel.py +++ b/tests/functional/test_wheel.py @@ -49,8 +49,7 @@ def test_pip_wheel_success(script, data): 'wheel', '--no-index', '-f', data.find_links, 'simple==3.0', ) - wheel_file_name = 'simple-3.0-py{pyversion[0]}-none-any.whl' \ - .format(**globals()) + wheel_file_name = f'simple-3.0-py{pyversion[0]}-none-any.whl' wheel_file_path = script.scratch / wheel_file_name assert re.search( r"Created wheel for simple: " @@ -70,8 +69,7 @@ def test_pip_wheel_build_cache(script, data): 'wheel', '--no-index', '-f', data.find_links, 'simple==3.0', ) - wheel_file_name = 'simple-3.0-py{pyversion[0]}-none-any.whl' \ - .format(**globals()) + wheel_file_name = f'simple-3.0-py{pyversion[0]}-none-any.whl' wheel_file_path = script.scratch / wheel_file_name result.did_create(wheel_file_path) assert "Successfully built simple" in result.stdout, result.stdout @@ -148,8 +146,7 @@ def test_pip_wheel_builds_editable_deps(script, data): 'wheel', '--no-index', '-f', data.find_links, '-e', editable_path ) - wheel_file_name = 'simple-1.0-py{pyversion[0]}-none-any.whl' \ - .format(**globals()) + wheel_file_name = f'simple-1.0-py{pyversion[0]}-none-any.whl' wheel_file_path = script.scratch / wheel_file_name result.did_create(wheel_file_path) @@ -163,8 +160,7 @@ def test_pip_wheel_builds_editable(script, data): 'wheel', '--no-index', '-f', data.find_links, '-e', editable_path ) - wheel_file_name = 'simplewheel-1.0-py{pyversion[0]}-none-any.whl' \ - .format(**globals()) + wheel_file_name = f'simplewheel-1.0-py{pyversion[0]}-none-any.whl' wheel_file_path = script.scratch / wheel_file_name result.did_create(wheel_file_path) @@ -213,8 +209,7 @@ def test_pip_wheel_fail(script, data): 'wheelbroken==0.1', expect_error=True, ) - wheel_file_name = 'wheelbroken-0.1-py{pyversion[0]}-none-any.whl' \ - .format(**globals()) + wheel_file_name = f'wheelbroken-0.1-py{pyversion[0]}-none-any.whl' wheel_file_path = script.scratch / wheel_file_name result.did_not_create(wheel_file_path) assert "FakeError" in result.stderr, result.stderr @@ -236,7 +231,7 @@ def test_no_clean_option_blocks_cleaning_after_wheel( build = script.venv_path / 'build' result = script.pip( 'wheel', '--no-clean', '--no-index', '--build', build, - '--find-links={data.find_links}'.format(**locals()), + f'--find-links={data.find_links}', 'simple', expect_temp=True, # TODO: allow_stderr_warning is used for the --build deprecation, @@ -260,8 +255,7 @@ def test_pip_wheel_source_deps(script, data): 'wheel', '--no-index', '-f', data.find_links, 'requires_source', ) - wheel_file_name = 'source-1.0-py{pyversion[0]}-none-any.whl' \ - .format(**globals()) + wheel_file_name = f'source-1.0-py{pyversion[0]}-none-any.whl' wheel_file_path = script.scratch / wheel_file_name result.did_create(wheel_file_path) assert "Successfully built source" in result.stdout, result.stdout @@ -278,8 +272,7 @@ def test_wheel_package_with_latin1_setup(script, data): def test_pip_wheel_with_pep518_build_reqs(script, data, common_wheels): result = script.pip('wheel', '--no-index', '-f', data.find_links, '-f', common_wheels, 'pep518==3.0',) - wheel_file_name = 'pep518-3.0-py{pyversion[0]}-none-any.whl' \ - .format(**globals()) + wheel_file_name = f'pep518-3.0-py{pyversion[0]}-none-any.whl' wheel_file_path = script.scratch / wheel_file_name result.did_create(wheel_file_path) assert "Successfully built pep518" in result.stdout, result.stdout @@ -292,8 +285,7 @@ def test_pip_wheel_with_pep518_build_reqs_no_isolation(script, data): 'wheel', '--no-index', '-f', data.find_links, '--no-build-isolation', 'pep518==3.0', ) - wheel_file_name = 'pep518-3.0-py{pyversion[0]}-none-any.whl' \ - .format(**globals()) + wheel_file_name = f'pep518-3.0-py{pyversion[0]}-none-any.whl' wheel_file_path = script.scratch / wheel_file_name result.did_create(wheel_file_path) assert "Successfully built pep518" in result.stdout, result.stdout @@ -339,8 +331,7 @@ def test_pep517_wheels_are_not_confused_with_other_files(script, tmpdir, data): result = script.pip('wheel', pkg_to_wheel, '-w', script.scratch_path) assert "Installing build dependencies" in result.stdout, result.stdout - wheel_file_name = 'withpyproject-0.0.1-py{pyversion[0]}-none-any.whl' \ - .format(**globals()) + wheel_file_name = f'withpyproject-0.0.1-py{pyversion[0]}-none-any.whl' wheel_file_path = script.scratch / wheel_file_name result.did_create(wheel_file_path) @@ -354,7 +345,6 @@ def test_legacy_wheels_are_not_confused_with_other_files(script, tmpdir, data): result = script.pip('wheel', pkg_to_wheel, '-w', script.scratch_path) assert "Installing build dependencies" not in result.stdout, result.stdout - wheel_file_name = 'simplewheel-1.0-py{pyversion[0]}-none-any.whl' \ - .format(**globals()) + wheel_file_name = f'simplewheel-1.0-py{pyversion[0]}-none-any.whl' wheel_file_path = script.scratch / wheel_file_name result.did_create(wheel_file_path) diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index 6a98d4acf78..de4bcc7a769 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -288,15 +288,13 @@ def assert_installed(self, pkg_name, editable=True, with_files=None, if egg_link_path in self.files_created: raise TestFailure( 'unexpected egg link file created: ' - '{egg_link_path!r}\n{self}' - .format(**locals()) + f'{egg_link_path!r}\n{self}' ) else: if egg_link_path not in self.files_created: raise TestFailure( 'expected egg link file missing: ' - '{egg_link_path!r}\n{self}' - .format(**locals()) + f'{egg_link_path!r}\n{self}' ) egg_link_file = self.files_created[egg_link_path] @@ -305,15 +303,14 @@ def assert_installed(self, pkg_name, editable=True, with_files=None, # FIXME: I don't understand why there's a trailing . here if not (egg_link_contents.endswith('\n.') and egg_link_contents[:-2].endswith(pkg_dir)): + expected_ending = pkg_dir + '\n.' raise TestFailure(textwrap.dedent( - '''\ + f'''\ Incorrect egg_link file {egg_link_file!r} Expected ending: {expected_ending!r} ------- Actual contents ------- {egg_link_contents!r} - -------------------------------'''.format( - expected_ending=pkg_dir + '\n.', - **locals()) + -------------------------------''' )) if use_user_site: @@ -322,36 +319,33 @@ def assert_installed(self, pkg_name, editable=True, with_files=None, pth_file = e.site_packages / 'easy-install.pth' if (pth_file in self.files_updated) == without_egg_link: + maybe = '' if without_egg_link else 'not ' raise TestFailure( - '{pth_file} unexpectedly {maybe}updated by install'.format( - maybe=not without_egg_link and 'not ' or '', - **locals())) + f'{pth_file} unexpectedly {maybe}updated by install' + ) if (pkg_dir in self.files_created) == (curdir in without_files): - raise TestFailure(textwrap.dedent('''\ + maybe = 'not ' if curdir in without_files else '' + files = sorted(self.files_created.keys()) + raise TestFailure(textwrap.dedent(f'''\ expected package directory {pkg_dir!r} {maybe}to be created actually created: {files} - ''').format( - pkg_dir=pkg_dir, - maybe=curdir in without_files and 'not ' or '', - files=sorted(self.files_created.keys()), - )) + ''')) for f in with_files: normalized_path = os.path.normpath(pkg_dir / f) if normalized_path not in self.files_created: raise TestFailure( - 'Package directory {pkg_dir!r} missing ' - 'expected content {f!r}'.format(**locals()) + f'Package directory {pkg_dir!r} missing ' + f'expected content {f!r}' ) for f in without_files: normalized_path = os.path.normpath(pkg_dir / f) if normalized_path in self.files_created: raise TestFailure( - 'Package directory {pkg_dir!r} has unexpected content {f}' - .format(**locals()) + f'Package directory {pkg_dir!r} has unexpected content {f}' ) def did_create(self, path, message=None): @@ -511,7 +505,7 @@ def __init__(self, base_path, *args, **kwargs): # Expand our absolute path directories into relative for name in ["base", "venv", "bin", "lib", "site_packages", "user_base", "user_site", "user_bin", "scratch"]: - real_name = "{name}_path".format(**locals()) + real_name = f"{name}_path" relative_path = Path(os.path.relpath( getattr(self, real_name), self.base_path )) @@ -565,7 +559,7 @@ def run(self, *args, **kw): compatibility. """ if self.verbose: - print('>> running {args} {kw}'.format(**locals())) + print(f'>> running {args} {kw}') cwd = kw.pop('cwd', None) run_from = kw.pop('run_from', None) @@ -823,7 +817,7 @@ def _vcs_add(script, version_pkg_path, vcs='git'): '-m', 'initial version', cwd=version_pkg_path, ) else: - raise ValueError('Unknown vcs: {vcs}'.format(**locals())) + raise ValueError(f'Unknown vcs: {vcs}') return version_pkg_path @@ -932,7 +926,7 @@ def assert_raises_regexp(exception, reg, run, *args, **kwargs): try: run(*args, **kwargs) - assert False, "{exception} should have been thrown".format(**locals()) + assert False, f"{exception} should have been thrown" except exception: e = sys.exc_info()[1] p = re.compile(reg) @@ -958,11 +952,11 @@ def create_test_package_with_setup(script, **setup_kwargs): assert 'name' in setup_kwargs, setup_kwargs pkg_path = script.scratch_path / setup_kwargs['name'] pkg_path.mkdir() - pkg_path.joinpath("setup.py").write_text(textwrap.dedent(""" + pkg_path.joinpath("setup.py").write_text(textwrap.dedent(f""" from setuptools import setup kwargs = {setup_kwargs!r} setup(**kwargs) - """).format(**locals())) + """)) return pkg_path diff --git a/tests/lib/test_lib.py b/tests/lib/test_lib.py index 47b97724f23..88c10501b70 100644 --- a/tests/lib/test_lib.py +++ b/tests/lib/test_lib.py @@ -63,8 +63,8 @@ def test_correct_pip_version(script): if x.endswith('.py') ] assert not mismatch_py, ( - 'mismatched source files in {pip_folder!r} ' - 'and {pip_folder_outputed!r}: {mismatch_py!r}'.format(**locals()) + f'mismatched source files in {pip_folder!r} ' + f'and {pip_folder_outputed!r}: {mismatch_py!r}' ) diff --git a/tests/unit/test_collector.py b/tests/unit/test_collector.py index 9fb920b32fe..c8044183b8b 100644 --- a/tests/unit/test_collector.py +++ b/tests/unit/test_collector.py @@ -608,7 +608,7 @@ def test_group_locations__file_expand_dir(data): files, urls = group_locations([data.find_links], expand_dir=True) assert files and not urls, ( "files and not urls should have been found " - "at find-links url: {data.find_links}".format(**locals()) + f"at find-links url: {data.find_links}" ) diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index c7be5fe1bac..1530eea8c7b 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -194,7 +194,7 @@ def test_unsupported_hashes(self, data): )) dir_path = data.packages.joinpath('FSPkg') reqset.add_requirement(get_processed_req_from_line( - 'file://{dir_path}'.format(**locals()), + f'file://{dir_path}', lineno=2, )) finder = make_test_finder(find_links=[data.find_links]) @@ -254,7 +254,7 @@ def test_hash_mismatch(self, data): (data.packages / 'simple-1.0.tar.gz').resolve()) reqset = RequirementSet() reqset.add_requirement(get_processed_req_from_line( - '{file_url} --hash=sha256:badbad'.format(**locals()), lineno=1, + f'{file_url} --hash=sha256:badbad', lineno=1, )) finder = make_test_finder(find_links=[data.find_links]) with self._basic_resolver(finder, require_hashes=True) as resolver: @@ -470,7 +470,7 @@ def test_markers_match_from_line(self): # match for markers in ( 'python_version >= "1.0"', - 'sys_platform == {sys.platform!r}'.format(**globals()), + f'sys_platform == {sys.platform!r}', ): line = 'name; ' + markers req = install_req_from_line(line) @@ -480,7 +480,7 @@ def test_markers_match_from_line(self): # don't match for markers in ( 'python_version >= "5.0"', - 'sys_platform != {sys.platform!r}'.format(**globals()), + f'sys_platform != {sys.platform!r}', ): line = 'name; ' + markers req = install_req_from_line(line) @@ -491,7 +491,7 @@ def test_markers_match(self): # match for markers in ( 'python_version >= "1.0"', - 'sys_platform == {sys.platform!r}'.format(**globals()), + f'sys_platform == {sys.platform!r}', ): line = 'name; ' + markers req = install_req_from_line(line, comes_from='') @@ -501,7 +501,7 @@ def test_markers_match(self): # don't match for markers in ( 'python_version >= "5.0"', - 'sys_platform != {sys.platform!r}'.format(**globals()), + f'sys_platform != {sys.platform!r}', ): line = 'name; ' + markers req = install_req_from_line(line, comes_from='') @@ -511,7 +511,7 @@ def test_markers_match(self): def test_extras_for_line_path_requirement(self): line = 'SomeProject[ex1,ex2]' filename = 'filename' - comes_from = '-r {} (line {})'.format(filename, 1) + comes_from = f'-r {filename} (line {1})' req = install_req_from_line(line, comes_from=comes_from) assert len(req.extras) == 2 assert req.extras == {'ex1', 'ex2'} @@ -519,7 +519,7 @@ def test_extras_for_line_path_requirement(self): def test_extras_for_line_url_requirement(self): line = 'git+https://url#egg=SomeProject[ex1,ex2]' filename = 'filename' - comes_from = '-r {} (line {})'.format(filename, 1) + comes_from = f'-r {filename} (line {1})' req = install_req_from_line(line, comes_from=comes_from) assert len(req.extras) == 2 assert req.extras == {'ex1', 'ex2'} @@ -527,7 +527,7 @@ def test_extras_for_line_url_requirement(self): def test_extras_for_editable_path_requirement(self): url = '.[ex1,ex2]' filename = 'filename' - comes_from = '-r {} (line {})'.format(filename, 1) + comes_from = f'-r {filename} (line {1})' req = install_req_from_editable(url, comes_from=comes_from) assert len(req.extras) == 2 assert req.extras == {'ex1', 'ex2'} @@ -535,7 +535,7 @@ def test_extras_for_editable_path_requirement(self): def test_extras_for_editable_url_requirement(self): url = 'git+https://url#egg=SomeProject[ex1,ex2]' filename = 'filename' - comes_from = '-r {} (line {})'.format(filename, 1) + comes_from = f'-r {filename} (line {1})' req = install_req_from_editable(url, comes_from=comes_from) assert len(req.extras) == 2 assert req.extras == {'ex1', 'ex2'} diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py index 86f2731e9e3..a66a1b1e755 100644 --- a/tests/unit/test_req_file.py +++ b/tests/unit/test_req_file.py @@ -229,14 +229,14 @@ def test_error_message(self, line_processor): def test_yield_line_requirement(self, line_processor): line = 'SomeProject' filename = 'filename' - comes_from = '-r {} (line {})'.format(filename, 1) + comes_from = f'-r {filename} (line {1})' req = install_req_from_line(line, comes_from=comes_from) assert repr(line_processor(line, filename, 1)[0]) == repr(req) def test_yield_pep440_line_requirement(self, line_processor): line = 'SomeProject @ https://url/SomeProject-py2-py3-none-any.whl' filename = 'filename' - comes_from = '-r {} (line {})'.format(filename, 1) + comes_from = f'-r {filename} (line {1})' req = install_req_from_line(line, comes_from=comes_from) assert repr(line_processor(line, filename, 1)[0]) == repr(req) @@ -255,16 +255,16 @@ def test_yield_line_requirement_with_spaces_in_specifier( ): line = 'SomeProject >= 2' filename = 'filename' - comes_from = '-r {} (line {})'.format(filename, 1) + comes_from = f'-r {filename} (line {1})' req = install_req_from_line(line, comes_from=comes_from) assert repr(line_processor(line, filename, 1)[0]) == repr(req) assert str(req.req.specifier) == '>=2' def test_yield_editable_requirement(self, line_processor): url = 'git+https://url#egg=SomeProject' - line = '-e {url}'.format(**locals()) + line = f'-e {url}' filename = 'filename' - comes_from = '-r {} (line {})'.format(filename, 1) + comes_from = f'-r {filename} (line {1})' req = install_req_from_editable(url, comes_from=comes_from) assert repr(line_processor(line, filename, 1)[0]) == repr(req) @@ -588,7 +588,7 @@ def test_expand_existing_env_variables(self, tmpdir, finder): ) def make_var(name): - return '${{{name}}}'.format(**locals()) + return f'${{{name}}}' env_vars = collections.OrderedDict([ ('GITHUB_TOKEN', 'notarealtoken'), From fb62f2707ae545f58e62b588c3d44c53440d0e69 Mon Sep 17 00:00:00 2001 From: Inada Naoki <songofacandy@gmail.com> Date: Sat, 13 Feb 2021 20:45:16 +0900 Subject: [PATCH 2958/3170] Use `encoding` option or binary mode for open() --- src/pip/_internal/operations/install/wheel.py | 2 +- src/pip/_internal/req/req_tracker.py | 2 +- src/pip/_internal/self_outdated_check.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index 37dcb618c65..e3ce62c5ce7 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -766,7 +766,7 @@ def _generate_file(path, **kwargs): # Record the REQUESTED file if requested: requested_path = os.path.join(dest_info_dir, 'REQUESTED') - with open(requested_path, "w"): + with open(requested_path, "wb"): pass generated.append(requested_path) diff --git a/src/pip/_internal/req/req_tracker.py b/src/pip/_internal/req/req_tracker.py index daa5b44ca25..a6aeb188f30 100644 --- a/src/pip/_internal/req/req_tracker.py +++ b/src/pip/_internal/req/req_tracker.py @@ -113,7 +113,7 @@ def add(self, req): assert req not in self._entries # Start tracking this requirement. - with open(entry_path, 'w') as fp: + with open(entry_path, 'w', encoding="utf-8") as fp: fp.write(str(req)) self._entries.add(req) diff --git a/src/pip/_internal/self_outdated_check.py b/src/pip/_internal/self_outdated_check.py index e8c8282cbf9..09d73bcaef1 100644 --- a/src/pip/_internal/self_outdated_check.py +++ b/src/pip/_internal/self_outdated_check.py @@ -48,7 +48,7 @@ def __init__(self, cache_dir): cache_dir, "selfcheck", _get_statefile_name(self.key) ) try: - with open(self.statefile_path) as statefile: + with open(self.statefile_path, encoding="utf-8") as statefile: self.state = json.load(statefile) except (OSError, ValueError, KeyError): # Explicitly suppressing exceptions, since we don't want to From 9fa7920a4b0bf525b804a0826b921382232e0e88 Mon Sep 17 00:00:00 2001 From: Inada Naoki <songofacandy@gmail.com> Date: Sat, 13 Feb 2021 20:51:01 +0900 Subject: [PATCH 2959/3170] Add tirivial marker file --- news/9976b528-bf43-4225-b96f-e066910f309c.trivial.rst | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 news/9976b528-bf43-4225-b96f-e066910f309c.trivial.rst diff --git a/news/9976b528-bf43-4225-b96f-e066910f309c.trivial.rst b/news/9976b528-bf43-4225-b96f-e066910f309c.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d From 6045ade9a919bb6ab6b12eadc5bb8254ecbc00ba Mon Sep 17 00:00:00 2001 From: Andrey Bienkowski <hexagonrecursion@gmail.com> Date: Thu, 18 Feb 2021 11:32:41 +0300 Subject: [PATCH 2960/3170] Apply review suggestions --- src/pip/_internal/commands/search.py | 3 ++- tests/functional/test_install_reqs.py | 4 +--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/commands/search.py b/src/pip/_internal/commands/search.py index 3d881f1550d..88cc0e29e7a 100644 --- a/src/pip/_internal/commands/search.py +++ b/src/pip/_internal/commands/search.py @@ -140,7 +140,8 @@ def print_results(hits, name_column_width=None, terminal_width=None): summary = ('\n' + ' ' * (name_column_width + 3)).join( summary_lines) - line = f'{{{name} ({latest}):{name_column_width}}} - {summary}' + name_latest=f'{name} ({latest})' + line = f'{name_latest:{name_column_width}} - {summary}' try: write_output(line) dist = env.get_distribution(name) diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index ac5ff087b07..adf6014bc10 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -645,9 +645,7 @@ def test_install_unsupported_wheel_file(script, data): # Trying to install a local wheel with an incompatible version/type # should fail. path = data.packages.joinpath("simple.dist-0.1-py1-none-invalid.whl") - script.scratch_path.joinpath("wheel-file.txt").write_text(textwrap.dedent(f"""\ - {path} - """)) + script.scratch_path.joinpath("wheel-file.txt").write_text(path + '\n')) result = script.pip( 'install', '-r', script.scratch_path / 'wheel-file.txt', expect_error=True, From a9b8d122869b1032fbc5dd5c3ecdae818bffe169 Mon Sep 17 00:00:00 2001 From: Andrey Bienkowski <hexagonrecursion@gmail.com> Date: Thu, 18 Feb 2021 08:33:44 +0000 Subject: [PATCH 2961/3170] Apply review suggestion Co-authored-by: Tzu-ping Chung <uranusjr@gmail.com> --- tests/lib/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index de4bcc7a769..acfc24e69b0 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -326,7 +326,7 @@ def assert_installed(self, pkg_name, editable=True, with_files=None, if (pkg_dir in self.files_created) == (curdir in without_files): maybe = 'not ' if curdir in without_files else '' - files = sorted(self.files_created.keys()) + files = sorted(self.files_created) raise TestFailure(textwrap.dedent(f'''\ expected package directory {pkg_dir!r} {maybe}to be created actually created: From 39d2fd131bd66271d8c41da8175b420a9eb7ff91 Mon Sep 17 00:00:00 2001 From: Andrey Bienkowski <hexagonrecursion@gmail.com> Date: Thu, 18 Feb 2021 11:39:30 +0300 Subject: [PATCH 2962/3170] Oops --- tests/functional/test_install_reqs.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index adf6014bc10..9e6e5580ae2 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -645,7 +645,7 @@ def test_install_unsupported_wheel_file(script, data): # Trying to install a local wheel with an incompatible version/type # should fail. path = data.packages.joinpath("simple.dist-0.1-py1-none-invalid.whl") - script.scratch_path.joinpath("wheel-file.txt").write_text(path + '\n')) + script.scratch_path.joinpath("wheel-file.txt").write_text(path + '\n') result = script.pip( 'install', '-r', script.scratch_path / 'wheel-file.txt', expect_error=True, From e84b000a37907094bb4de812e02ddd32566cf0a8 Mon Sep 17 00:00:00 2001 From: Andrey Bienkowski <hexagonrecursion@gmail.com> Date: Thu, 18 Feb 2021 11:46:45 +0300 Subject: [PATCH 2963/3170] Lint --- src/pip/_internal/commands/search.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/commands/search.py b/src/pip/_internal/commands/search.py index 88cc0e29e7a..ce1792597b4 100644 --- a/src/pip/_internal/commands/search.py +++ b/src/pip/_internal/commands/search.py @@ -140,7 +140,7 @@ def print_results(hits, name_column_width=None, terminal_width=None): summary = ('\n' + ' ' * (name_column_width + 3)).join( summary_lines) - name_latest=f'{name} ({latest})' + name_latest = f'{name} ({latest})' line = f'{name_latest:{name_column_width}} - {summary}' try: write_output(line) From 3874afe72cbe338390a777106f47850013e3c0ca Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Sun, 27 Dec 2020 09:45:39 -0800 Subject: [PATCH 2964/3170] Drop mock test dependency --- news/f24d8f47-5750-4a13-b36f-d4a4622861cf.trivial.rst | 0 tools/requirements/tests.txt | 1 - 2 files changed, 1 deletion(-) create mode 100644 news/f24d8f47-5750-4a13-b36f-d4a4622861cf.trivial.rst diff --git a/news/f24d8f47-5750-4a13-b36f-d4a4622861cf.trivial.rst b/news/f24d8f47-5750-4a13-b36f-d4a4622861cf.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tools/requirements/tests.txt b/tools/requirements/tests.txt index 9b4e9849054..608d5d9f4bb 100644 --- a/tools/requirements/tests.txt +++ b/tools/requirements/tests.txt @@ -1,7 +1,6 @@ --use-feature=2020-resolver cryptography==2.8 freezegun -mock pretend pytest pytest-cov From a6392bd62e7d3b13698c4a4df64e5f51a5c67b9d Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Fri, 1 Jan 2021 15:16:16 -0800 Subject: [PATCH 2965/3170] Replace pip._internal.utils.typing with stdlib typing The stdlib module has been available since Python 3.5 and the TYPE_CHECKING constant has been available since 3.5.2. By using stdlib, this removes the need for pip to maintain its own Python 2 typing compatibility shim. --- ...41-404a-4f2b-afee-c931a5aa7d54.trivial.rst | 0 setup.cfg | 2 +- src/pip/__init__.py | 4 +- src/pip/_internal/__init__.py | 5 ++- src/pip/_internal/build_env.py | 4 +- src/pip/_internal/cache.py | 4 +- src/pip/_internal/cli/autocompletion.py | 4 +- src/pip/_internal/cli/base_command.py | 4 +- src/pip/_internal/cli/cmdoptions.py | 4 +- src/pip/_internal/cli/command_context.py | 5 +-- src/pip/_internal/cli/main.py | 4 +- src/pip/_internal/cli/main_parser.py | 4 +- src/pip/_internal/cli/parser.py | 4 +- src/pip/_internal/cli/progress_bars.py | 4 +- src/pip/_internal/cli/req_command.py | 4 +- src/pip/_internal/cli/spinners.py | 4 +- src/pip/_internal/commands/__init__.py | 5 +-- src/pip/_internal/commands/cache.py | 4 +- src/pip/_internal/commands/check.py | 4 +- src/pip/_internal/commands/completion.py | 4 +- src/pip/_internal/commands/configuration.py | 4 +- src/pip/_internal/commands/debug.py | 4 +- src/pip/_internal/commands/download.py | 4 +- src/pip/_internal/commands/freeze.py | 4 +- src/pip/_internal/commands/hash.py | 4 +- src/pip/_internal/commands/help.py | 5 ++- src/pip/_internal/commands/install.py | 4 +- src/pip/_internal/commands/list.py | 4 +- src/pip/_internal/commands/search.py | 4 +- src/pip/_internal/commands/show.py | 4 +- src/pip/_internal/commands/uninstall.py | 5 ++- src/pip/_internal/commands/wheel.py | 4 +- src/pip/_internal/configuration.py | 4 +- src/pip/_internal/distributions/__init__.py | 5 ++- src/pip/_internal/distributions/base.py | 5 +-- src/pip/_internal/distributions/installed.py | 5 ++- src/pip/_internal/distributions/sdist.py | 4 +- src/pip/_internal/distributions/wheel.py | 4 +- src/pip/_internal/exceptions.py | 5 +-- src/pip/_internal/index/collector.py | 4 +- src/pip/_internal/index/package_finder.py | 4 +- src/pip/_internal/locations.py | 4 +- src/pip/_internal/main.py | 4 +- src/pip/_internal/metadata/__init__.py | 4 +- src/pip/_internal/metadata/base.py | 5 ++- src/pip/_internal/metadata/pkg_resources.py | 4 +- src/pip/_internal/models/candidate.py | 5 ++- src/pip/_internal/models/direct_url.py | 7 ++-- src/pip/_internal/models/format_control.py | 5 ++- src/pip/_internal/models/link.py | 4 +- src/pip/_internal/models/search_scope.py | 4 +- src/pip/_internal/models/selection_prefs.py | 4 +- src/pip/_internal/models/target_python.py | 4 +- src/pip/_internal/models/wheel.py | 4 +- src/pip/_internal/network/auth.py | 4 +- src/pip/_internal/network/cache.py | 4 +- src/pip/_internal/network/download.py | 4 +- src/pip/_internal/network/lazy_wheel.py | 4 +- src/pip/_internal/network/session.py | 4 +- src/pip/_internal/network/utils.py | 5 ++- src/pip/_internal/network/xmlrpc.py | 4 +- .../_internal/operations/build/metadata.py | 4 +- .../operations/build/metadata_legacy.py | 4 +- src/pip/_internal/operations/build/wheel.py | 4 +- .../operations/build/wheel_legacy.py | 4 +- src/pip/_internal/operations/check.py | 4 +- src/pip/_internal/operations/freeze.py | 4 +- .../operations/install/editable_legacy.py | 4 +- .../_internal/operations/install/legacy.py | 4 +- src/pip/_internal/operations/install/wheel.py | 10 +---- src/pip/_internal/operations/prepare.py | 4 +- src/pip/_internal/pyproject.py | 4 +- src/pip/_internal/req/__init__.py | 4 +- src/pip/_internal/req/constructors.py | 4 +- src/pip/_internal/req/req_file.py | 4 +- src/pip/_internal/req/req_install.py | 4 +- src/pip/_internal/req/req_set.py | 4 +- src/pip/_internal/req/req_tracker.py | 4 +- src/pip/_internal/req/req_uninstall.py | 4 +- src/pip/_internal/resolution/base.py | 4 +- .../_internal/resolution/legacy/resolver.py | 4 +- .../_internal/resolution/resolvelib/base.py | 5 ++- .../resolution/resolvelib/candidates.py | 4 +- .../resolution/resolvelib/factory.py | 4 +- .../resolution/resolvelib/found_candidates.py | 5 +-- .../resolution/resolvelib/provider.py | 6 +-- .../resolution/resolvelib/reporter.py | 5 +-- .../resolution/resolvelib/requirements.py | 6 +-- .../resolution/resolvelib/resolver.py | 4 +- src/pip/_internal/self_outdated_check.py | 4 +- src/pip/_internal/utils/appdirs.py | 5 +-- src/pip/_internal/utils/compat.py | 5 +-- src/pip/_internal/utils/compatibility_tags.py | 5 +-- src/pip/_internal/utils/deprecation.py | 4 +- src/pip/_internal/utils/direct_url_helpers.py | 4 +- src/pip/_internal/utils/distutils_args.py | 5 +-- src/pip/_internal/utils/encoding.py | 5 +-- src/pip/_internal/utils/entrypoints.py | 4 +- src/pip/_internal/utils/filesystem.py | 4 +- src/pip/_internal/utils/filetypes.py | 5 ++- src/pip/_internal/utils/glibc.py | 5 +-- src/pip/_internal/utils/hashes.py | 4 +- src/pip/_internal/utils/logging.py | 4 +- src/pip/_internal/utils/misc.py | 4 +- src/pip/_internal/utils/packaging.py | 4 +- src/pip/_internal/utils/parallel.py | 5 +-- src/pip/_internal/utils/pkg_resources.py | 6 +-- src/pip/_internal/utils/setuptools_build.py | 5 +-- src/pip/_internal/utils/subprocess.py | 4 +- src/pip/_internal/utils/temp_dir.py | 4 +- src/pip/_internal/utils/typing.py | 38 ------------------- src/pip/_internal/utils/unpacking.py | 4 +- src/pip/_internal/utils/urls.py | 5 +-- src/pip/_internal/utils/virtualenv.py | 5 +-- src/pip/_internal/utils/wheel.py | 4 +- src/pip/_internal/vcs/__init__.py | 2 +- src/pip/_internal/vcs/bazaar.py | 4 +- src/pip/_internal/vcs/git.py | 4 +- src/pip/_internal/vcs/mercurial.py | 4 +- src/pip/_internal/vcs/subversion.py | 4 +- src/pip/_internal/vcs/versioncontrol.py | 4 +- src/pip/_internal/wheel_builder.py | 4 +- tests/conftest.py | 4 +- tests/lib/__init__.py | 4 +- tests/lib/certs.py | 5 +-- tests/lib/local_repos.py | 4 +- tests/lib/server.py | 5 +-- tests/lib/test_wheel.py | 4 +- tests/lib/wheel.py | 6 +-- tests/unit/test_utils_wheel.py | 4 +- 130 files changed, 270 insertions(+), 322 deletions(-) create mode 100644 news/afd07841-404a-4f2b-afee-c931a5aa7d54.trivial.rst delete mode 100644 src/pip/_internal/utils/typing.py diff --git a/news/afd07841-404a-4f2b-afee-c931a5aa7d54.trivial.rst b/news/afd07841-404a-4f2b-afee-c931a5aa7d54.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/setup.cfg b/setup.cfg index 25850c4cefa..a96614dc199 100644 --- a/setup.cfg +++ b/setup.cfg @@ -91,7 +91,7 @@ exclude_lines = # it. pragma: no cover # This excludes typing-specific code, which will be validated by mypy anyway. - if MYPY_CHECK_RUNNING + if TYPE_CHECKING # Can be set to exclude e.g. `if PY2:` on Python 3 ${PIP_CI_COVERAGE_EXCLUDES} diff --git a/src/pip/__init__.py b/src/pip/__init__.py index 9de6dc0f2a6..97b5e2f8839 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -1,6 +1,6 @@ -from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from typing import TYPE_CHECKING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import List, Optional diff --git a/src/pip/_internal/__init__.py b/src/pip/_internal/__init__.py index a778e99488e..23652fadc5e 100755 --- a/src/pip/_internal/__init__.py +++ b/src/pip/_internal/__init__.py @@ -1,7 +1,8 @@ +from typing import TYPE_CHECKING + import pip._internal.utils.inject_securetransport # noqa -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import List, Optional diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py index a587d9f7c8f..9df467f309a 100644 --- a/src/pip/_internal/build_env.py +++ b/src/pip/_internal/build_env.py @@ -8,6 +8,7 @@ from collections import OrderedDict from distutils.sysconfig import get_python_lib from sysconfig import get_paths +from typing import TYPE_CHECKING from pip._vendor.pkg_resources import Requirement, VersionConflict, WorkingSet @@ -15,9 +16,8 @@ from pip._internal.cli.spinners import open_spinner from pip._internal.utils.subprocess import call_subprocess from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from types import TracebackType from typing import Iterable, List, Optional, Set, Tuple, Type diff --git a/src/pip/_internal/cache.py b/src/pip/_internal/cache.py index 5e7db9c1f62..83ea57ad4b9 100644 --- a/src/pip/_internal/cache.py +++ b/src/pip/_internal/cache.py @@ -5,6 +5,7 @@ import json import logging import os +from typing import TYPE_CHECKING from pip._vendor.packaging.tags import interpreter_name, interpreter_version from pip._vendor.packaging.utils import canonicalize_name @@ -13,10 +14,9 @@ from pip._internal.models.link import Link from pip._internal.models.wheel import Wheel from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds -from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.urls import path_to_url -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Any, Dict, List, Optional, Set from pip._vendor.packaging.tags import Tag diff --git a/src/pip/_internal/cli/autocompletion.py b/src/pip/_internal/cli/autocompletion.py index 329de602513..3b4fc339e8a 100644 --- a/src/pip/_internal/cli/autocompletion.py +++ b/src/pip/_internal/cli/autocompletion.py @@ -5,13 +5,13 @@ import os import sys from itertools import chain +from typing import TYPE_CHECKING from pip._internal.cli.main_parser import create_main_parser from pip._internal.commands import commands_dict, create_command from pip._internal.utils.misc import get_installed_distributions -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Any, Iterable, List, Optional diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index 108435a841e..380ba8a7163 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -6,6 +6,7 @@ import os import sys import traceback +from typing import TYPE_CHECKING from pip._internal.cli import cmdoptions from pip._internal.cli.command_context import CommandContextMixIn @@ -29,10 +30,9 @@ from pip._internal.utils.logging import BrokenStdoutLoggingError, setup_logging from pip._internal.utils.misc import get_prog, normalize_path from pip._internal.utils.temp_dir import global_tempdir_manager, tempdir_registry -from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.virtualenv import running_under_virtualenv -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from optparse import Values from typing import Any, List, Optional, Tuple diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index 663143950ac..bcecf6748fb 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -16,6 +16,7 @@ from functools import partial from optparse import SUPPRESS_HELP, Option, OptionGroup from textwrap import dedent +from typing import TYPE_CHECKING from pip._vendor.packaging.utils import canonicalize_name @@ -27,9 +28,8 @@ from pip._internal.models.target_python import TargetPython from pip._internal.utils.hashes import STRONG_HASHES from pip._internal.utils.misc import strtobool -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from optparse import OptionParser, Values from typing import Any, Callable, Dict, Optional, Tuple diff --git a/src/pip/_internal/cli/command_context.py b/src/pip/_internal/cli/command_context.py index ade14f2f677..d04b35db4e7 100644 --- a/src/pip/_internal/cli/command_context.py +++ b/src/pip/_internal/cli/command_context.py @@ -1,10 +1,9 @@ from contextlib import contextmanager +from typing import TYPE_CHECKING from pip._vendor.contextlib2 import ExitStack -from pip._internal.utils.typing import MYPY_CHECK_RUNNING - -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import ContextManager, Iterator, TypeVar _T = TypeVar('_T', covariant=True) diff --git a/src/pip/_internal/cli/main.py b/src/pip/_internal/cli/main.py index ed59073072f..64210aeba6a 100644 --- a/src/pip/_internal/cli/main.py +++ b/src/pip/_internal/cli/main.py @@ -4,15 +4,15 @@ import logging import os import sys +from typing import TYPE_CHECKING from pip._internal.cli.autocompletion import autocomplete from pip._internal.cli.main_parser import parse_command from pip._internal.commands import create_command from pip._internal.exceptions import PipError from pip._internal.utils import deprecation -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import List, Optional logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/cli/main_parser.py b/src/pip/_internal/cli/main_parser.py index 7351cdda0f1..1ad8d7de3ca 100644 --- a/src/pip/_internal/cli/main_parser.py +++ b/src/pip/_internal/cli/main_parser.py @@ -3,15 +3,15 @@ import os import sys +from typing import TYPE_CHECKING from pip._internal.cli import cmdoptions from pip._internal.cli.parser import ConfigOptionParser, UpdatingDefaultsHelpFormatter from pip._internal.commands import commands_dict, get_similar_commands from pip._internal.exceptions import CommandError from pip._internal.utils.misc import get_pip_version, get_prog -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import List, Tuple diff --git a/src/pip/_internal/cli/parser.py b/src/pip/_internal/cli/parser.py index 89591fa7f9a..d5389a9a15a 100644 --- a/src/pip/_internal/cli/parser.py +++ b/src/pip/_internal/cli/parser.py @@ -8,15 +8,15 @@ import shutil import sys import textwrap +from typing import TYPE_CHECKING from pip._vendor.contextlib2 import suppress from pip._internal.cli.status_codes import UNKNOWN_ERROR from pip._internal.configuration import Configuration, ConfigurationError from pip._internal.utils.misc import redact_auth_from_url, strtobool -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Any logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/cli/progress_bars.py b/src/pip/_internal/cli/progress_bars.py index 59b01a6d0fd..2d3847082a8 100644 --- a/src/pip/_internal/cli/progress_bars.py +++ b/src/pip/_internal/cli/progress_bars.py @@ -1,6 +1,7 @@ import itertools import sys from signal import SIGINT, default_int_handler, signal +from typing import TYPE_CHECKING from pip._vendor.progress.bar import Bar, FillingCirclesBar, IncrementalBar from pip._vendor.progress.spinner import Spinner @@ -8,9 +9,8 @@ from pip._internal.utils.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: +if TYPE_CHECKING: from typing import Any, Dict, List try: diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 468b3cceab5..436cd8ddab5 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -8,6 +8,7 @@ import logging import os from functools import partial +from typing import TYPE_CHECKING from pip._internal.cli import cmdoptions from pip._internal.cli.base_command import Command @@ -27,9 +28,8 @@ from pip._internal.req.req_file import parse_requirements from pip._internal.self_outdated_check import pip_self_version_check from pip._internal.utils.temp_dir import tempdir_kinds -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from optparse import Values from typing import Any, List, Optional, Tuple diff --git a/src/pip/_internal/cli/spinners.py b/src/pip/_internal/cli/spinners.py index 05ec2dcc765..d55434e6024 100644 --- a/src/pip/_internal/cli/spinners.py +++ b/src/pip/_internal/cli/spinners.py @@ -3,14 +3,14 @@ import logging import sys import time +from typing import TYPE_CHECKING from pip._vendor.progress import HIDE_CURSOR, SHOW_CURSOR from pip._internal.utils.compat import WINDOWS from pip._internal.utils.logging import get_indentation -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import IO, Iterator logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/commands/__init__.py b/src/pip/_internal/commands/__init__.py index f2411201c47..3037e9da861 100644 --- a/src/pip/_internal/commands/__init__.py +++ b/src/pip/_internal/commands/__init__.py @@ -4,10 +4,9 @@ import importlib from collections import OrderedDict, namedtuple +from typing import TYPE_CHECKING -from pip._internal.utils.typing import MYPY_CHECK_RUNNING - -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Any, Optional from pip._internal.cli.base_command import Command diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index d5ac45ad738..4f746dd980c 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -1,14 +1,14 @@ import logging import os import textwrap +from typing import TYPE_CHECKING import pip._internal.utils.filesystem as filesystem from pip._internal.cli.base_command import Command from pip._internal.cli.status_codes import ERROR, SUCCESS from pip._internal.exceptions import CommandError, PipError -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from optparse import Values from typing import Any, List diff --git a/src/pip/_internal/commands/check.py b/src/pip/_internal/commands/check.py index e066bb63c74..5bc07cb7eb2 100644 --- a/src/pip/_internal/commands/check.py +++ b/src/pip/_internal/commands/check.py @@ -1,4 +1,5 @@ import logging +from typing import TYPE_CHECKING from pip._internal.cli.base_command import Command from pip._internal.cli.status_codes import ERROR, SUCCESS @@ -7,11 +8,10 @@ create_package_set_from_installed, ) from pip._internal.utils.misc import write_output -from pip._internal.utils.typing import MYPY_CHECK_RUNNING logger = logging.getLogger(__name__) -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from optparse import Values from typing import Any, List diff --git a/src/pip/_internal/commands/completion.py b/src/pip/_internal/commands/completion.py index 2c19d5686d2..ca336075210 100644 --- a/src/pip/_internal/commands/completion.py +++ b/src/pip/_internal/commands/completion.py @@ -1,12 +1,12 @@ import sys import textwrap +from typing import TYPE_CHECKING from pip._internal.cli.base_command import Command from pip._internal.cli.status_codes import SUCCESS from pip._internal.utils.misc import get_prog -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from optparse import Values from typing import List diff --git a/src/pip/_internal/commands/configuration.py b/src/pip/_internal/commands/configuration.py index a440a2b1774..8cf034aafb7 100644 --- a/src/pip/_internal/commands/configuration.py +++ b/src/pip/_internal/commands/configuration.py @@ -1,6 +1,7 @@ import logging import os import subprocess +from typing import TYPE_CHECKING from pip._internal.cli.base_command import Command from pip._internal.cli.status_codes import ERROR, SUCCESS @@ -8,9 +9,8 @@ from pip._internal.exceptions import PipError from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import get_prog, write_output -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from optparse import Values from typing import Any, List, Optional diff --git a/src/pip/_internal/commands/debug.py b/src/pip/_internal/commands/debug.py index c1af3630ec2..15c66fd531e 100644 --- a/src/pip/_internal/commands/debug.py +++ b/src/pip/_internal/commands/debug.py @@ -2,6 +2,7 @@ import logging import os import sys +from typing import TYPE_CHECKING import pip._vendor from pip._vendor.certifi import where @@ -15,9 +16,8 @@ from pip._internal.metadata import get_environment from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import get_pip_version -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from optparse import Values from types import ModuleType from typing import Dict, List, Optional diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index 0f09fcc0eed..212b75c7a67 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -1,5 +1,6 @@ import logging import os +from typing import TYPE_CHECKING from pip._internal.cli import cmdoptions from pip._internal.cli.cmdoptions import make_target_python @@ -8,9 +9,8 @@ from pip._internal.req.req_tracker import get_requirement_tracker from pip._internal.utils.misc import ensure_dir, normalize_path, write_output from pip._internal.utils.temp_dir import TempDirectory -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from optparse import Values from typing import List diff --git a/src/pip/_internal/commands/freeze.py b/src/pip/_internal/commands/freeze.py index bf20db6c370..6a3288d6be8 100644 --- a/src/pip/_internal/commands/freeze.py +++ b/src/pip/_internal/commands/freeze.py @@ -1,4 +1,5 @@ import sys +from typing import TYPE_CHECKING from pip._internal.cli import cmdoptions from pip._internal.cli.base_command import Command @@ -6,11 +7,10 @@ from pip._internal.operations.freeze import freeze from pip._internal.utils.compat import stdlib_pkgs from pip._internal.utils.deprecation import deprecated -from pip._internal.utils.typing import MYPY_CHECK_RUNNING DEV_PKGS = {'pip', 'setuptools', 'distribute', 'wheel'} -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from optparse import Values from typing import List diff --git a/src/pip/_internal/commands/hash.py b/src/pip/_internal/commands/hash.py index db68f6ce7bc..ff871b806ed 100644 --- a/src/pip/_internal/commands/hash.py +++ b/src/pip/_internal/commands/hash.py @@ -1,14 +1,14 @@ import hashlib import logging import sys +from typing import TYPE_CHECKING from pip._internal.cli.base_command import Command from pip._internal.cli.status_codes import ERROR, SUCCESS from pip._internal.utils.hashes import FAVORITE_HASH, STRONG_HASHES from pip._internal.utils.misc import read_chunks, write_output -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from optparse import Values from typing import List diff --git a/src/pip/_internal/commands/help.py b/src/pip/_internal/commands/help.py index 8372ac615dc..0e1dc81fffd 100644 --- a/src/pip/_internal/commands/help.py +++ b/src/pip/_internal/commands/help.py @@ -1,9 +1,10 @@ +from typing import TYPE_CHECKING + from pip._internal.cli.base_command import Command from pip._internal.cli.status_codes import SUCCESS from pip._internal.exceptions import CommandError -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from optparse import Values from typing import List diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index e303adf86fc..a6989a0f8df 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -5,6 +5,7 @@ import shutil import site from optparse import SUPPRESS_HELP +from typing import TYPE_CHECKING from pip._vendor import pkg_resources from pip._vendor.packaging.utils import canonicalize_name @@ -29,11 +30,10 @@ write_output, ) from pip._internal.utils.temp_dir import TempDirectory -from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.virtualenv import virtualenv_no_global from pip._internal.wheel_builder import build, should_build_for_install_command -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from optparse import Values from typing import Iterable, List, Optional diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index 89cfb625e59..4ee0d54b48f 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -1,5 +1,6 @@ import json import logging +from typing import TYPE_CHECKING from pip._internal.cli import cmdoptions from pip._internal.cli.req_command import IndexGroupCommand @@ -17,9 +18,8 @@ ) from pip._internal.utils.packaging import get_installer from pip._internal.utils.parallel import map_multithread -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from optparse import Values from typing import Iterator, List, Set, Tuple diff --git a/src/pip/_internal/commands/search.py b/src/pip/_internal/commands/search.py index 90a5b512d69..1c7fa74ae90 100644 --- a/src/pip/_internal/commands/search.py +++ b/src/pip/_internal/commands/search.py @@ -3,6 +3,7 @@ import sys import textwrap from collections import OrderedDict +from typing import TYPE_CHECKING from pip._vendor.packaging.version import parse as parse_version @@ -19,9 +20,8 @@ from pip._internal.network.xmlrpc import PipXmlrpcTransport from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import write_output -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from optparse import Values from typing import Dict, List, Optional diff --git a/src/pip/_internal/commands/show.py b/src/pip/_internal/commands/show.py index a6363cfd0a2..e4d2502908f 100644 --- a/src/pip/_internal/commands/show.py +++ b/src/pip/_internal/commands/show.py @@ -1,6 +1,7 @@ import logging import os from email.parser import FeedParser +from typing import TYPE_CHECKING from pip._vendor import pkg_resources from pip._vendor.packaging.utils import canonicalize_name @@ -8,9 +9,8 @@ from pip._internal.cli.base_command import Command from pip._internal.cli.status_codes import ERROR, SUCCESS from pip._internal.utils.misc import write_output -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from optparse import Values from typing import Dict, Iterator, List diff --git a/src/pip/_internal/commands/uninstall.py b/src/pip/_internal/commands/uninstall.py index 6dc96c3d630..5084f32d202 100644 --- a/src/pip/_internal/commands/uninstall.py +++ b/src/pip/_internal/commands/uninstall.py @@ -1,3 +1,5 @@ +from typing import TYPE_CHECKING + from pip._vendor.packaging.utils import canonicalize_name from pip._internal.cli.base_command import Command @@ -10,9 +12,8 @@ install_req_from_parsed_requirement, ) from pip._internal.utils.misc import protect_pip_from_modification_on_windows -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from optparse import Values from typing import List diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index 28918fa748a..d97ac00c41c 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -1,6 +1,7 @@ import logging import os import shutil +from typing import TYPE_CHECKING from pip._internal.cache import WheelCache from pip._internal.cli import cmdoptions @@ -10,10 +11,9 @@ from pip._internal.req.req_tracker import get_requirement_tracker from pip._internal.utils.misc import ensure_dir, normalize_path from pip._internal.utils.temp_dir import TempDirectory -from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.wheel_builder import build, should_build_for_wheel_command -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from optparse import Values from typing import List diff --git a/src/pip/_internal/configuration.py b/src/pip/_internal/configuration.py index 9d9a36e8077..a52b83a110a 100644 --- a/src/pip/_internal/configuration.py +++ b/src/pip/_internal/configuration.py @@ -16,6 +16,7 @@ import logging import os import sys +from typing import TYPE_CHECKING from pip._internal.exceptions import ( ConfigurationError, @@ -24,9 +25,8 @@ from pip._internal.utils import appdirs from pip._internal.utils.compat import WINDOWS from pip._internal.utils.misc import ensure_dir, enum -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Any, Dict, Iterable, List, NewType, Optional, Tuple RawConfigParser = configparser.RawConfigParser # Shorthand diff --git a/src/pip/_internal/distributions/__init__.py b/src/pip/_internal/distributions/__init__.py index d5c1afc5bc1..d68f358e244 100644 --- a/src/pip/_internal/distributions/__init__.py +++ b/src/pip/_internal/distributions/__init__.py @@ -1,8 +1,9 @@ +from typing import TYPE_CHECKING + from pip._internal.distributions.sdist import SourceDistribution from pip._internal.distributions.wheel import WheelDistribution -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from pip._internal.distributions.base import AbstractDistribution from pip._internal.req.req_install import InstallRequirement diff --git a/src/pip/_internal/distributions/base.py b/src/pip/_internal/distributions/base.py index 37db810b351..50a21deff70 100644 --- a/src/pip/_internal/distributions/base.py +++ b/src/pip/_internal/distributions/base.py @@ -1,8 +1,7 @@ import abc +from typing import TYPE_CHECKING -from pip._internal.utils.typing import MYPY_CHECK_RUNNING - -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Optional from pip._vendor.pkg_resources import Distribution diff --git a/src/pip/_internal/distributions/installed.py b/src/pip/_internal/distributions/installed.py index a813b211fe6..70f16499ba2 100644 --- a/src/pip/_internal/distributions/installed.py +++ b/src/pip/_internal/distributions/installed.py @@ -1,7 +1,8 @@ +from typing import TYPE_CHECKING + from pip._internal.distributions.base import AbstractDistribution -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Optional from pip._vendor.pkg_resources import Distribution diff --git a/src/pip/_internal/distributions/sdist.py b/src/pip/_internal/distributions/sdist.py index 9b708fdd83c..538bbfe8e74 100644 --- a/src/pip/_internal/distributions/sdist.py +++ b/src/pip/_internal/distributions/sdist.py @@ -1,12 +1,12 @@ import logging +from typing import TYPE_CHECKING from pip._internal.build_env import BuildEnvironment from pip._internal.distributions.base import AbstractDistribution from pip._internal.exceptions import InstallationError from pip._internal.utils.subprocess import runner_with_spinner_message -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Set, Tuple from pip._vendor.pkg_resources import Distribution diff --git a/src/pip/_internal/distributions/wheel.py b/src/pip/_internal/distributions/wheel.py index 2adc2286271..bc8ab99c0d9 100644 --- a/src/pip/_internal/distributions/wheel.py +++ b/src/pip/_internal/distributions/wheel.py @@ -1,10 +1,10 @@ +from typing import TYPE_CHECKING from zipfile import ZipFile from pip._internal.distributions.base import AbstractDistribution -from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.wheel import pkg_resources_distribution_for_wheel -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from pip._vendor.pkg_resources import Distribution from pip._internal.index.package_finder import PackageFinder diff --git a/src/pip/_internal/exceptions.py b/src/pip/_internal/exceptions.py index 891f8c37654..9af1961fff1 100644 --- a/src/pip/_internal/exceptions.py +++ b/src/pip/_internal/exceptions.py @@ -1,10 +1,9 @@ """Exceptions used throughout package""" from itertools import chain, groupby, repeat +from typing import TYPE_CHECKING -from pip._internal.utils.typing import MYPY_CHECK_RUNNING - -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: import configparser from hashlib import _Hash from typing import Dict, List, Optional diff --git a/src/pip/_internal/index/collector.py b/src/pip/_internal/index/collector.py index ee4eb719992..1472c763e9f 100644 --- a/src/pip/_internal/index/collector.py +++ b/src/pip/_internal/index/collector.py @@ -12,6 +12,7 @@ import urllib.parse import urllib.request from collections import OrderedDict +from typing import TYPE_CHECKING from pip._vendor import html5lib, requests from pip._vendor.distlib.compat import unescape @@ -23,11 +24,10 @@ from pip._internal.network.utils import raise_for_status from pip._internal.utils.filetypes import is_archive_file from pip._internal.utils.misc import pairwise, redact_auth_from_url -from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.urls import path_to_url, url_to_path from pip._internal.vcs import is_url, vcs -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: import xml.etree.ElementTree from optparse import Values from typing import ( diff --git a/src/pip/_internal/index/package_finder.py b/src/pip/_internal/index/package_finder.py index 731188926fd..5e603ef150b 100644 --- a/src/pip/_internal/index/package_finder.py +++ b/src/pip/_internal/index/package_finder.py @@ -6,6 +6,7 @@ import functools import logging import re +from typing import TYPE_CHECKING from pip._vendor.packaging import specifiers from pip._vendor.packaging.utils import canonicalize_name @@ -28,11 +29,10 @@ from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import build_netloc from pip._internal.utils.packaging import check_requires_python -from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.unpacking import SUPPORTED_EXTENSIONS from pip._internal.utils.urls import url_to_path -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import FrozenSet, Iterable, List, Optional, Set, Tuple, Union from pip._vendor.packaging.tags import Tag diff --git a/src/pip/_internal/locations.py b/src/pip/_internal/locations.py index 88b9e43cd86..4e6b2810518 100644 --- a/src/pip/_internal/locations.py +++ b/src/pip/_internal/locations.py @@ -10,14 +10,14 @@ import sysconfig from distutils.command.install import SCHEME_KEYS from distutils.command.install import install as distutils_install_command +from typing import TYPE_CHECKING, cast from pip._internal.models.scheme import Scheme from pip._internal.utils import appdirs from pip._internal.utils.compat import WINDOWS -from pip._internal.utils.typing import MYPY_CHECK_RUNNING, cast from pip._internal.utils.virtualenv import running_under_virtualenv -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from distutils.cmd import Command as DistutilsCommand from typing import Dict, List, Optional, Union diff --git a/src/pip/_internal/main.py b/src/pip/_internal/main.py index 1c99c49a1f1..647cde25f96 100644 --- a/src/pip/_internal/main.py +++ b/src/pip/_internal/main.py @@ -1,6 +1,6 @@ -from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from typing import TYPE_CHECKING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import List, Optional diff --git a/src/pip/_internal/metadata/__init__.py b/src/pip/_internal/metadata/__init__.py index ba0471aa17c..ecf2550e1dd 100644 --- a/src/pip/_internal/metadata/__init__.py +++ b/src/pip/_internal/metadata/__init__.py @@ -1,6 +1,6 @@ -from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from typing import TYPE_CHECKING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import List, Optional from .base import BaseDistribution, BaseEnvironment diff --git a/src/pip/_internal/metadata/base.py b/src/pip/_internal/metadata/base.py index fb1f8579a38..8ff57374c66 100644 --- a/src/pip/_internal/metadata/base.py +++ b/src/pip/_internal/metadata/base.py @@ -1,7 +1,8 @@ +from typing import TYPE_CHECKING + from pip._internal.utils.misc import stdlib_pkgs # TODO: Move definition here. -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Container, Iterator, List, Optional from pip._vendor.packaging.version import _BaseVersion diff --git a/src/pip/_internal/metadata/pkg_resources.py b/src/pip/_internal/metadata/pkg_resources.py index 48d959a373b..ccc3cc57b9d 100644 --- a/src/pip/_internal/metadata/pkg_resources.py +++ b/src/pip/_internal/metadata/pkg_resources.py @@ -1,16 +1,16 @@ import zipfile +from typing import TYPE_CHECKING from pip._vendor import pkg_resources from pip._vendor.packaging.utils import canonicalize_name from pip._internal.utils import misc # TODO: Move definition here. from pip._internal.utils.packaging import get_installer -from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.wheel import pkg_resources_distribution_for_wheel from .base import BaseDistribution, BaseEnvironment -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Iterator, List, Optional from pip._vendor.packaging.version import _BaseVersion diff --git a/src/pip/_internal/models/candidate.py b/src/pip/_internal/models/candidate.py index d8a8d42ebcf..d4eade13e60 100644 --- a/src/pip/_internal/models/candidate.py +++ b/src/pip/_internal/models/candidate.py @@ -1,9 +1,10 @@ +from typing import TYPE_CHECKING + from pip._vendor.packaging.version import parse as parse_version from pip._internal.utils.models import KeyBasedCompareMixin -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from pip._vendor.packaging.version import _BaseVersion from pip._internal.models.link import Link diff --git a/src/pip/_internal/models/direct_url.py b/src/pip/_internal/models/direct_url.py index a8869bd0442..d10a8044d24 100644 --- a/src/pip/_internal/models/direct_url.py +++ b/src/pip/_internal/models/direct_url.py @@ -2,10 +2,9 @@ import json import re import urllib.parse +from typing import TYPE_CHECKING -from pip._internal.utils.typing import MYPY_CHECK_RUNNING - -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Any, Dict, Iterable, Optional, Type, TypeVar, Union T = TypeVar("T") @@ -156,7 +155,7 @@ def _to_dict(self): return _filter_none(editable=self.editable or None) -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: InfoType = Union[ArchiveInfo, DirInfo, VcsInfo] diff --git a/src/pip/_internal/models/format_control.py b/src/pip/_internal/models/format_control.py index eb46f25359a..73d045728a6 100644 --- a/src/pip/_internal/models/format_control.py +++ b/src/pip/_internal/models/format_control.py @@ -1,9 +1,10 @@ +from typing import TYPE_CHECKING + from pip._vendor.packaging.utils import canonicalize_name from pip._internal.exceptions import CommandError -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import FrozenSet, Optional, Set diff --git a/src/pip/_internal/models/link.py b/src/pip/_internal/models/link.py index 06a7ceb3fce..757f43b9071 100644 --- a/src/pip/_internal/models/link.py +++ b/src/pip/_internal/models/link.py @@ -2,6 +2,7 @@ import posixpath import re import urllib.parse +from typing import TYPE_CHECKING from pip._internal.utils.filetypes import WHEEL_EXTENSION from pip._internal.utils.misc import ( @@ -10,10 +11,9 @@ splitext, ) from pip._internal.utils.models import KeyBasedCompareMixin -from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.urls import path_to_url, url_to_path -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Optional, Tuple, Union from pip._internal.index.collector import HTMLPage diff --git a/src/pip/_internal/models/search_scope.py b/src/pip/_internal/models/search_scope.py index c972f1d1704..21907aab740 100644 --- a/src/pip/_internal/models/search_scope.py +++ b/src/pip/_internal/models/search_scope.py @@ -3,15 +3,15 @@ import os import posixpath import urllib.parse +from typing import TYPE_CHECKING from pip._vendor.packaging.utils import canonicalize_name from pip._internal.models.index import PyPI from pip._internal.utils.compat import has_tls from pip._internal.utils.misc import normalize_path, redact_auth_from_url -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import List diff --git a/src/pip/_internal/models/selection_prefs.py b/src/pip/_internal/models/selection_prefs.py index 4d5822268b7..65750ebb2bc 100644 --- a/src/pip/_internal/models/selection_prefs.py +++ b/src/pip/_internal/models/selection_prefs.py @@ -1,6 +1,6 @@ -from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from typing import TYPE_CHECKING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Optional from pip._internal.models.format_control import FormatControl diff --git a/src/pip/_internal/models/target_python.py b/src/pip/_internal/models/target_python.py index 6e6e8b52eee..742c089eb13 100644 --- a/src/pip/_internal/models/target_python.py +++ b/src/pip/_internal/models/target_python.py @@ -1,10 +1,10 @@ import sys +from typing import TYPE_CHECKING from pip._internal.utils.compatibility_tags import get_supported, version_info_to_nodot from pip._internal.utils.misc import normalize_version_info -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import List, Optional, Tuple from pip._vendor.packaging.tags import Tag diff --git a/src/pip/_internal/models/wheel.py b/src/pip/_internal/models/wheel.py index 5e03f9ff83b..484596aaa32 100644 --- a/src/pip/_internal/models/wheel.py +++ b/src/pip/_internal/models/wheel.py @@ -2,13 +2,13 @@ name that have meaning. """ import re +from typing import TYPE_CHECKING from pip._vendor.packaging.tags import Tag from pip._internal.exceptions import InvalidWheelFilename -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import List diff --git a/src/pip/_internal/network/auth.py b/src/pip/_internal/network/auth.py index 99fa2bd6762..315aca69994 100644 --- a/src/pip/_internal/network/auth.py +++ b/src/pip/_internal/network/auth.py @@ -6,6 +6,7 @@ import logging import urllib.parse +from typing import TYPE_CHECKING from pip._vendor.requests.auth import AuthBase, HTTPBasicAuth from pip._vendor.requests.utils import get_netrc_auth @@ -17,9 +18,8 @@ remove_auth_from_url, split_auth_netloc_from_url, ) -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Any, Dict, List, Optional, Tuple from pip._vendor.requests.models import Request, Response diff --git a/src/pip/_internal/network/cache.py b/src/pip/_internal/network/cache.py index 9253b204769..cdec4949c36 100644 --- a/src/pip/_internal/network/cache.py +++ b/src/pip/_internal/network/cache.py @@ -3,6 +3,7 @@ import os from contextlib import contextmanager +from typing import TYPE_CHECKING from pip._vendor.cachecontrol.cache import BaseCache from pip._vendor.cachecontrol.caches import FileCache @@ -10,9 +11,8 @@ from pip._internal.utils.filesystem import adjacent_tmp_file, replace from pip._internal.utils.misc import ensure_dir -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Iterator, Optional diff --git a/src/pip/_internal/network/download.py b/src/pip/_internal/network/download.py index a2a0e8e2a81..7f02fa6f659 100644 --- a/src/pip/_internal/network/download.py +++ b/src/pip/_internal/network/download.py @@ -4,6 +4,7 @@ import logging import mimetypes import os +from typing import TYPE_CHECKING from pip._vendor.requests.models import CONTENT_CHUNK_SIZE @@ -13,9 +14,8 @@ from pip._internal.network.cache import is_from_cache from pip._internal.network.utils import HEADERS, raise_for_status, response_chunks from pip._internal.utils.misc import format_size, redact_auth_from_url, splitext -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Iterable, Optional, Tuple from pip._vendor.requests.models import Response diff --git a/src/pip/_internal/network/lazy_wheel.py b/src/pip/_internal/network/lazy_wheel.py index c5176a4bb2a..fd7ab7f03a3 100644 --- a/src/pip/_internal/network/lazy_wheel.py +++ b/src/pip/_internal/network/lazy_wheel.py @@ -5,15 +5,15 @@ from bisect import bisect_left, bisect_right from contextlib import contextmanager from tempfile import NamedTemporaryFile +from typing import TYPE_CHECKING from zipfile import BadZipfile, ZipFile from pip._vendor.requests.models import CONTENT_CHUNK_SIZE from pip._internal.network.utils import HEADERS, raise_for_status, response_chunks -from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.wheel import pkg_resources_distribution_for_wheel -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Any, Dict, Iterator, List, Optional, Tuple from pip._vendor.pkg_resources import Distribution diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py index 1128e625350..83bcecca382 100644 --- a/src/pip/_internal/network/session.py +++ b/src/pip/_internal/network/session.py @@ -15,6 +15,7 @@ import sys import urllib.parse import warnings +from typing import TYPE_CHECKING from pip._vendor import requests, urllib3 from pip._vendor.cachecontrol import CacheControlAdapter @@ -35,10 +36,9 @@ get_installed_version, parse_netloc, ) -from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.urls import url_to_path -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Any, Iterator, List, Optional, Sequence, Tuple, Union from pip._internal.models.link import Link diff --git a/src/pip/_internal/network/utils.py b/src/pip/_internal/network/utils.py index f4ff95010fc..47ece6d13dd 100644 --- a/src/pip/_internal/network/utils.py +++ b/src/pip/_internal/network/utils.py @@ -1,9 +1,10 @@ +from typing import TYPE_CHECKING + from pip._vendor.requests.models import CONTENT_CHUNK_SIZE, Response from pip._internal.exceptions import NetworkConnectionError -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Dict, Iterator # The following comments and HTTP headers were originally added by diff --git a/src/pip/_internal/network/xmlrpc.py b/src/pip/_internal/network/xmlrpc.py index 87490453259..d4aa71c0929 100644 --- a/src/pip/_internal/network/xmlrpc.py +++ b/src/pip/_internal/network/xmlrpc.py @@ -3,6 +3,7 @@ import logging import urllib.parse +from typing import TYPE_CHECKING # NOTE: XMLRPC Client is not annotated in typeshed as on 2017-07-17, which is # why we ignore the type on this import @@ -10,9 +11,8 @@ from pip._internal.exceptions import NetworkConnectionError from pip._internal.network.utils import raise_for_status -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Dict from pip._internal.network.session import PipSession diff --git a/src/pip/_internal/operations/build/metadata.py b/src/pip/_internal/operations/build/metadata.py index 5709962b09e..21f86c8dc88 100644 --- a/src/pip/_internal/operations/build/metadata.py +++ b/src/pip/_internal/operations/build/metadata.py @@ -2,12 +2,12 @@ """ import os +from typing import TYPE_CHECKING from pip._internal.utils.subprocess import runner_with_spinner_message from pip._internal.utils.temp_dir import TempDirectory -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from pip._vendor.pep517.wrappers import Pep517HookCaller from pip._internal.build_env import BuildEnvironment diff --git a/src/pip/_internal/operations/build/metadata_legacy.py b/src/pip/_internal/operations/build/metadata_legacy.py index d44589666f4..a113a4a4e87 100644 --- a/src/pip/_internal/operations/build/metadata_legacy.py +++ b/src/pip/_internal/operations/build/metadata_legacy.py @@ -3,14 +3,14 @@ import logging import os +from typing import TYPE_CHECKING from pip._internal.exceptions import InstallationError from pip._internal.utils.setuptools_build import make_setuptools_egg_info_args from pip._internal.utils.subprocess import call_subprocess from pip._internal.utils.temp_dir import TempDirectory -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from pip._internal.build_env import BuildEnvironment logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/operations/build/wheel.py b/src/pip/_internal/operations/build/wheel.py index d25f9c42f62..9af53caa2f1 100644 --- a/src/pip/_internal/operations/build/wheel.py +++ b/src/pip/_internal/operations/build/wheel.py @@ -1,10 +1,10 @@ import logging import os +from typing import TYPE_CHECKING from pip._internal.utils.subprocess import runner_with_spinner_message -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import List, Optional from pip._vendor.pep517.wrappers import Pep517HookCaller diff --git a/src/pip/_internal/operations/build/wheel_legacy.py b/src/pip/_internal/operations/build/wheel_legacy.py index 82fa44406e6..0a4a68d20bc 100644 --- a/src/pip/_internal/operations/build/wheel_legacy.py +++ b/src/pip/_internal/operations/build/wheel_legacy.py @@ -1,5 +1,6 @@ import logging import os.path +from typing import TYPE_CHECKING from pip._internal.cli.spinners import open_spinner from pip._internal.utils.setuptools_build import make_setuptools_bdist_wheel_args @@ -8,9 +9,8 @@ call_subprocess, format_command_args, ) -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import List, Optional logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/operations/check.py b/src/pip/_internal/operations/check.py index 5dee6bcb400..a3189061c71 100644 --- a/src/pip/_internal/operations/check.py +++ b/src/pip/_internal/operations/check.py @@ -3,17 +3,17 @@ import logging from collections import namedtuple +from typing import TYPE_CHECKING from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.pkg_resources import RequirementParseError from pip._internal.distributions import make_distribution_for_install_requirement from pip._internal.utils.misc import get_installed_distributions -from pip._internal.utils.typing import MYPY_CHECK_RUNNING logger = logging.getLogger(__name__) -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Any, Callable, Dict, List, Optional, Set, Tuple from pip._internal.req.req_install import InstallRequirement diff --git a/src/pip/_internal/operations/freeze.py b/src/pip/_internal/operations/freeze.py index 5d63c12fa1a..b082caa8ab2 100644 --- a/src/pip/_internal/operations/freeze.py +++ b/src/pip/_internal/operations/freeze.py @@ -1,6 +1,7 @@ import collections import logging import os +from typing import TYPE_CHECKING from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.pkg_resources import RequirementParseError @@ -16,9 +17,8 @@ dist_get_direct_url, ) from pip._internal.utils.misc import dist_is_editable, get_installed_distributions -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import ( Container, Dict, diff --git a/src/pip/_internal/operations/install/editable_legacy.py b/src/pip/_internal/operations/install/editable_legacy.py index a668a61dc60..f2ec1f882be 100644 --- a/src/pip/_internal/operations/install/editable_legacy.py +++ b/src/pip/_internal/operations/install/editable_legacy.py @@ -1,13 +1,13 @@ """Legacy editable installation process, i.e. `setup.py develop`. """ import logging +from typing import TYPE_CHECKING from pip._internal.utils.logging import indent_log from pip._internal.utils.setuptools_build import make_setuptools_develop_args from pip._internal.utils.subprocess import call_subprocess -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import List, Optional, Sequence from pip._internal.build_env import BuildEnvironment diff --git a/src/pip/_internal/operations/install/legacy.py b/src/pip/_internal/operations/install/legacy.py index 63a693a91ed..a70be0d22b9 100644 --- a/src/pip/_internal/operations/install/legacy.py +++ b/src/pip/_internal/operations/install/legacy.py @@ -5,6 +5,7 @@ import os import sys from distutils.util import change_root +from typing import TYPE_CHECKING from pip._internal.exceptions import InstallationError from pip._internal.utils.logging import indent_log @@ -12,9 +13,8 @@ from pip._internal.utils.setuptools_build import make_setuptools_install_args from pip._internal.utils.subprocess import runner_with_spinner_message from pip._internal.utils.temp_dir import TempDirectory -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import List, Optional, Sequence from pip._internal.build_env import BuildEnvironment diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index e3ce62c5ce7..db72f711192 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -14,6 +14,7 @@ import warnings from base64 import urlsafe_b64encode from itertools import chain, filterfalse, starmap +from typing import TYPE_CHECKING, cast from zipfile import ZipFile from pip._vendor import pkg_resources @@ -27,7 +28,6 @@ from pip._internal.models.scheme import SCHEME_KEYS from pip._internal.utils.filesystem import adjacent_tmp_file, replace from pip._internal.utils.misc import captured_stdout, ensure_dir, hash_file, partition -from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.unpacking import ( current_umask, is_within_directory, @@ -36,12 +36,7 @@ ) from pip._internal.utils.wheel import parse_wheel, pkg_resources_distribution_for_wheel -# Use the custom cast function at runtime to make cast work, -# and import typing.cast when performing pre-commit and type -# checks -if not MYPY_CHECK_RUNNING: - from pip._internal.utils.typing import cast -else: +if TYPE_CHECKING: from email.message import Message from typing import ( IO, @@ -59,7 +54,6 @@ Set, Tuple, Union, - cast, ) from zipfile import ZipInfo diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 715c3debdb6..e09fa7fb08f 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -8,6 +8,7 @@ import mimetypes import os import shutil +from typing import TYPE_CHECKING from pip._vendor.packaging.utils import canonicalize_name @@ -33,11 +34,10 @@ from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import display_path, hide_url, path_to_display, rmtree from pip._internal.utils.temp_dir import TempDirectory -from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.unpacking import unpack_file from pip._internal.vcs import vcs -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Dict, Iterable, List, Optional, Tuple from pip._vendor.pkg_resources import Distribution diff --git a/src/pip/_internal/pyproject.py b/src/pip/_internal/pyproject.py index 68ca53bf0bf..fdd289b5631 100644 --- a/src/pip/_internal/pyproject.py +++ b/src/pip/_internal/pyproject.py @@ -1,13 +1,13 @@ import os from collections import namedtuple +from typing import TYPE_CHECKING from pip._vendor import toml from pip._vendor.packaging.requirements import InvalidRequirement, Requirement from pip._internal.exceptions import InstallationError -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Any, List, Optional diff --git a/src/pip/_internal/req/__init__.py b/src/pip/_internal/req/__init__.py index 352d8923f17..ef6e162a21c 100644 --- a/src/pip/_internal/req/__init__.py +++ b/src/pip/_internal/req/__init__.py @@ -1,14 +1,14 @@ import collections import logging +from typing import TYPE_CHECKING from pip._internal.utils.logging import indent_log -from pip._internal.utils.typing import MYPY_CHECK_RUNNING from .req_file import parse_requirements from .req_install import InstallRequirement from .req_set import RequirementSet -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Iterator, List, Optional, Sequence, Tuple __all__ = [ diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index cfb1951b6b8..6a649f0d858 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -11,6 +11,7 @@ import logging import os import re +from typing import TYPE_CHECKING from pip._vendor.packaging.markers import Marker from pip._vendor.packaging.requirements import InvalidRequirement, Requirement @@ -25,11 +26,10 @@ from pip._internal.req.req_install import InstallRequirement from pip._internal.utils.filetypes import is_archive_file from pip._internal.utils.misc import is_installable_dir -from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.urls import path_to_url from pip._internal.vcs import is_url, vcs -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Any, Dict, Optional, Set, Tuple, Union from pip._internal.req.req_file import ParsedRequirement diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index 716005dc560..a2f87209b35 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -7,16 +7,16 @@ import re import shlex import urllib.parse +from typing import TYPE_CHECKING from pip._internal.cli import cmdoptions from pip._internal.exceptions import InstallationError, RequirementsFileParseError from pip._internal.models.search_scope import SearchScope from pip._internal.network.utils import raise_for_status from pip._internal.utils.encoding import auto_decode -from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.urls import get_url_scheme, url_to_path -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from optparse import Values from typing import ( Any, diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index b9562bedca3..4485634532c 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -7,6 +7,7 @@ import sys import uuid import zipfile +from typing import TYPE_CHECKING from pip._vendor import pkg_resources, six from pip._vendor.packaging.requirements import Requirement @@ -48,11 +49,10 @@ ) from pip._internal.utils.packaging import get_metadata from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds -from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.virtualenv import running_under_virtualenv from pip._internal.vcs import vcs -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Any, Dict, Iterable, List, Optional, Sequence, Union from pip._vendor.packaging.markers import Marker diff --git a/src/pip/_internal/req/req_set.py b/src/pip/_internal/req/req_set.py index fa58be66341..c9552183286 100644 --- a/src/pip/_internal/req/req_set.py +++ b/src/pip/_internal/req/req_set.py @@ -1,14 +1,14 @@ import logging from collections import OrderedDict +from typing import TYPE_CHECKING from pip._vendor.packaging.utils import canonicalize_name from pip._internal.exceptions import InstallationError from pip._internal.models.wheel import Wheel from pip._internal.utils import compatibility_tags -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Dict, Iterable, List, Optional, Tuple from pip._internal.req.req_install import InstallRequirement diff --git a/src/pip/_internal/req/req_tracker.py b/src/pip/_internal/req/req_tracker.py index a6aeb188f30..ac4831a8405 100644 --- a/src/pip/_internal/req/req_tracker.py +++ b/src/pip/_internal/req/req_tracker.py @@ -2,13 +2,13 @@ import hashlib import logging import os +from typing import TYPE_CHECKING from pip._vendor import contextlib2 from pip._internal.utils.temp_dir import TempDirectory -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from types import TracebackType from typing import Dict, Iterator, Optional, Set, Type, Union diff --git a/src/pip/_internal/req/req_uninstall.py b/src/pip/_internal/req/req_uninstall.py index a37a378dda7..d7e28dc004e 100644 --- a/src/pip/_internal/req/req_uninstall.py +++ b/src/pip/_internal/req/req_uninstall.py @@ -5,6 +5,7 @@ import sys import sysconfig from importlib.util import cache_from_source +from typing import TYPE_CHECKING from pip._vendor import pkg_resources @@ -23,9 +24,8 @@ rmtree, ) from pip._internal.utils.temp_dir import AdjacentTempDirectory, TempDirectory -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import ( Any, Callable, diff --git a/src/pip/_internal/resolution/base.py b/src/pip/_internal/resolution/base.py index f2816ab71c2..caf4e0d8cf6 100644 --- a/src/pip/_internal/resolution/base.py +++ b/src/pip/_internal/resolution/base.py @@ -1,6 +1,6 @@ -from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from typing import TYPE_CHECKING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Callable, List from pip._internal.req.req_install import InstallRequirement diff --git a/src/pip/_internal/resolution/legacy/resolver.py b/src/pip/_internal/resolution/legacy/resolver.py index 665dba128a9..8f5378a0787 100644 --- a/src/pip/_internal/resolution/legacy/resolver.py +++ b/src/pip/_internal/resolution/legacy/resolver.py @@ -18,6 +18,7 @@ import sys from collections import defaultdict from itertools import chain +from typing import TYPE_CHECKING from pip._vendor.packaging import specifiers @@ -35,9 +36,8 @@ from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import dist_in_usersite, normalize_version_info from pip._internal.utils.packaging import check_requires_python, get_requires_python -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import DefaultDict, List, Optional, Set, Tuple from pip._vendor.pkg_resources import Distribution diff --git a/src/pip/_internal/resolution/resolvelib/base.py b/src/pip/_internal/resolution/resolvelib/base.py index 82c5ec7c72d..29d798c3065 100644 --- a/src/pip/_internal/resolution/resolvelib/base.py +++ b/src/pip/_internal/resolution/resolvelib/base.py @@ -1,11 +1,12 @@ +from typing import TYPE_CHECKING + from pip._vendor.packaging.specifiers import SpecifierSet from pip._vendor.packaging.utils import canonicalize_name from pip._internal.req.req_install import InstallRequirement from pip._internal.utils.hashes import Hashes -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import FrozenSet, Iterable, Optional, Tuple from pip._vendor.packaging.version import _BaseVersion diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 91662b326b7..2240cac0893 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -1,5 +1,6 @@ import logging import sys +from typing import TYPE_CHECKING from pip._vendor.packaging.specifiers import InvalidSpecifier, SpecifierSet from pip._vendor.packaging.utils import canonicalize_name @@ -14,11 +15,10 @@ from pip._internal.req.req_install import InstallRequirement from pip._internal.utils.misc import dist_is_editable, normalize_version_info from pip._internal.utils.packaging import get_requires_python -from pip._internal.utils.typing import MYPY_CHECK_RUNNING from .base import Candidate, format_name -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Any, FrozenSet, Iterable, Optional, Tuple, Union from pip._vendor.packaging.version import _BaseVersion diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index be0729e3994..5bfe0cc066f 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -1,5 +1,6 @@ import functools import logging +from typing import TYPE_CHECKING from pip._vendor.packaging.utils import canonicalize_name @@ -20,7 +21,6 @@ dist_in_usersite, get_installed_distributions, ) -from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.virtualenv import running_under_virtualenv from .base import Constraint @@ -39,7 +39,7 @@ UnsatisfiableRequirement, ) -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import ( Dict, FrozenSet, diff --git a/src/pip/_internal/resolution/resolvelib/found_candidates.py b/src/pip/_internal/resolution/resolvelib/found_candidates.py index f11b47603c3..2a8d58ce2ef 100644 --- a/src/pip/_internal/resolution/resolvelib/found_candidates.py +++ b/src/pip/_internal/resolution/resolvelib/found_candidates.py @@ -9,12 +9,11 @@ """ import functools +from typing import TYPE_CHECKING from pip._vendor.six.moves import collections_abc # type: ignore -from pip._internal.utils.typing import MYPY_CHECK_RUNNING - -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Callable, Iterator, Optional, Set, Tuple from pip._vendor.packaging.version import _BaseVersion diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index 2ac3933b404..f8632410eb8 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -1,10 +1,10 @@ -from pip._vendor.resolvelib.providers import AbstractProvider +from typing import TYPE_CHECKING -from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from pip._vendor.resolvelib.providers import AbstractProvider from .base import Constraint -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Any, Dict, Iterable, Optional, Sequence, Tuple, Union from .base import Candidate, Requirement diff --git a/src/pip/_internal/resolution/resolvelib/reporter.py b/src/pip/_internal/resolution/resolvelib/reporter.py index d0ef3fadc67..6679d73f219 100644 --- a/src/pip/_internal/resolution/resolvelib/reporter.py +++ b/src/pip/_internal/resolution/resolvelib/reporter.py @@ -1,11 +1,10 @@ from collections import defaultdict from logging import getLogger +from typing import TYPE_CHECKING from pip._vendor.resolvelib.reporters import BaseReporter -from pip._internal.utils.typing import MYPY_CHECK_RUNNING - -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Any, DefaultDict from .base import Candidate, Requirement diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py index 61c81e00eee..a2fad4bdb2e 100644 --- a/src/pip/_internal/resolution/resolvelib/requirements.py +++ b/src/pip/_internal/resolution/resolvelib/requirements.py @@ -1,10 +1,10 @@ -from pip._vendor.packaging.utils import canonicalize_name +from typing import TYPE_CHECKING -from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from pip._vendor.packaging.utils import canonicalize_name from .base import Requirement, format_name -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from pip._vendor.packaging.specifiers import SpecifierSet from pip._internal.req.req_install import InstallRequirement diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index d02a49c7daa..935723737e9 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -1,6 +1,7 @@ import functools import logging import os +from typing import TYPE_CHECKING from pip._vendor import six from pip._vendor.packaging.utils import canonicalize_name @@ -19,12 +20,11 @@ from pip._internal.utils.deprecation import deprecated from pip._internal.utils.filetypes import is_archive_file from pip._internal.utils.misc import dist_is_editable -from pip._internal.utils.typing import MYPY_CHECK_RUNNING from .base import Constraint from .factory import Factory -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Dict, List, Optional, Set, Tuple from pip._vendor.resolvelib.resolvers import Result diff --git a/src/pip/_internal/self_outdated_check.py b/src/pip/_internal/self_outdated_check.py index 026378f2350..9281636fa30 100644 --- a/src/pip/_internal/self_outdated_check.py +++ b/src/pip/_internal/self_outdated_check.py @@ -4,6 +4,7 @@ import logging import os.path import sys +from typing import TYPE_CHECKING from pip._vendor.packaging import version as packaging_version @@ -13,9 +14,8 @@ from pip._internal.models.selection_prefs import SelectionPreferences from pip._internal.utils.filesystem import adjacent_tmp_file, check_path_owner, replace from pip._internal.utils.misc import ensure_dir, get_installed_version -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: import optparse from typing import Any, Dict diff --git a/src/pip/_internal/utils/appdirs.py b/src/pip/_internal/utils/appdirs.py index a0a37be8723..55e83e0d689 100644 --- a/src/pip/_internal/utils/appdirs.py +++ b/src/pip/_internal/utils/appdirs.py @@ -7,12 +7,11 @@ """ import os +from typing import TYPE_CHECKING from pip._vendor import appdirs as _appdirs -from pip._internal.utils.typing import MYPY_CHECK_RUNNING - -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import List diff --git a/src/pip/_internal/utils/compat.py b/src/pip/_internal/utils/compat.py index 0ae0483c813..0b059952348 100644 --- a/src/pip/_internal/utils/compat.py +++ b/src/pip/_internal/utils/compat.py @@ -9,10 +9,9 @@ import logging import os import sys +from typing import TYPE_CHECKING -from pip._internal.utils.typing import MYPY_CHECK_RUNNING - -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Optional, Union diff --git a/src/pip/_internal/utils/compatibility_tags.py b/src/pip/_internal/utils/compatibility_tags.py index ac37c3a17ba..cfba97a5328 100644 --- a/src/pip/_internal/utils/compatibility_tags.py +++ b/src/pip/_internal/utils/compatibility_tags.py @@ -2,6 +2,7 @@ """ import re +from typing import TYPE_CHECKING from pip._vendor.packaging.tags import ( Tag, @@ -13,9 +14,7 @@ mac_platforms, ) -from pip._internal.utils.typing import MYPY_CHECK_RUNNING - -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import List, Optional, Tuple from pip._vendor.packaging.tags import PythonVersion diff --git a/src/pip/_internal/utils/deprecation.py b/src/pip/_internal/utils/deprecation.py index 534d3fde86c..d4b60ea1a19 100644 --- a/src/pip/_internal/utils/deprecation.py +++ b/src/pip/_internal/utils/deprecation.py @@ -7,13 +7,13 @@ import logging import warnings +from typing import TYPE_CHECKING from pip._vendor.packaging.version import parse from pip import __version__ as current_version -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Any, Optional diff --git a/src/pip/_internal/utils/direct_url_helpers.py b/src/pip/_internal/utils/direct_url_helpers.py index 9598137aa06..caf2fa1481d 100644 --- a/src/pip/_internal/utils/direct_url_helpers.py +++ b/src/pip/_internal/utils/direct_url_helpers.py @@ -1,5 +1,6 @@ import json import logging +from typing import TYPE_CHECKING from pip._internal.models.direct_url import ( DIRECT_URL_METADATA_NAME, @@ -9,10 +10,9 @@ DirInfo, VcsInfo, ) -from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.vcs import vcs -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Optional from pip._vendor.pkg_resources import Distribution diff --git a/src/pip/_internal/utils/distutils_args.py b/src/pip/_internal/utils/distutils_args.py index e38e402d733..7d3dae78577 100644 --- a/src/pip/_internal/utils/distutils_args.py +++ b/src/pip/_internal/utils/distutils_args.py @@ -1,9 +1,8 @@ from distutils.errors import DistutilsArgError from distutils.fancy_getopt import FancyGetopt +from typing import TYPE_CHECKING -from pip._internal.utils.typing import MYPY_CHECK_RUNNING - -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Dict, List diff --git a/src/pip/_internal/utils/encoding.py b/src/pip/_internal/utils/encoding.py index 7df67987842..122c4ab29e4 100644 --- a/src/pip/_internal/utils/encoding.py +++ b/src/pip/_internal/utils/encoding.py @@ -2,10 +2,9 @@ import locale import re import sys +from typing import TYPE_CHECKING -from pip._internal.utils.typing import MYPY_CHECK_RUNNING - -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import List, Tuple BOMS = [ diff --git a/src/pip/_internal/utils/entrypoints.py b/src/pip/_internal/utils/entrypoints.py index 64d1cb2bd0b..9c0454a627d 100644 --- a/src/pip/_internal/utils/entrypoints.py +++ b/src/pip/_internal/utils/entrypoints.py @@ -1,9 +1,9 @@ import sys +from typing import TYPE_CHECKING from pip._internal.cli.main import main -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import List, Optional diff --git a/src/pip/_internal/utils/filesystem.py b/src/pip/_internal/utils/filesystem.py index 1af8c10eaaa..1a9d952f4f9 100644 --- a/src/pip/_internal/utils/filesystem.py +++ b/src/pip/_internal/utils/filesystem.py @@ -7,6 +7,7 @@ import sys from contextlib import contextmanager from tempfile import NamedTemporaryFile +from typing import TYPE_CHECKING, cast # NOTE: retrying is not annotated in typeshed as on 2017-07-17, which is # why we ignore the type on this import. @@ -14,9 +15,8 @@ from pip._internal.utils.compat import get_path_uid from pip._internal.utils.misc import format_size -from pip._internal.utils.typing import MYPY_CHECK_RUNNING, cast -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Any, BinaryIO, Iterator, List, Union diff --git a/src/pip/_internal/utils/filetypes.py b/src/pip/_internal/utils/filetypes.py index 201c6ebbed8..440151d5f32 100644 --- a/src/pip/_internal/utils/filetypes.py +++ b/src/pip/_internal/utils/filetypes.py @@ -1,9 +1,10 @@ """Filetype information. """ +from typing import TYPE_CHECKING + from pip._internal.utils.misc import splitext -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Tuple WHEEL_EXTENSION = '.whl' diff --git a/src/pip/_internal/utils/glibc.py b/src/pip/_internal/utils/glibc.py index 819979d8001..37caad45ef6 100644 --- a/src/pip/_internal/utils/glibc.py +++ b/src/pip/_internal/utils/glibc.py @@ -3,10 +3,9 @@ import os import sys +from typing import TYPE_CHECKING -from pip._internal.utils.typing import MYPY_CHECK_RUNNING - -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Optional, Tuple diff --git a/src/pip/_internal/utils/hashes.py b/src/pip/_internal/utils/hashes.py index d5ff90063c5..612c5e740d7 100644 --- a/src/pip/_internal/utils/hashes.py +++ b/src/pip/_internal/utils/hashes.py @@ -1,10 +1,10 @@ import hashlib +from typing import TYPE_CHECKING from pip._internal.exceptions import HashMismatch, HashMissing, InstallationError from pip._internal.utils.misc import read_chunks -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from hashlib import _Hash from typing import BinaryIO, Dict, Iterator, List, NoReturn diff --git a/src/pip/_internal/utils/logging.py b/src/pip/_internal/utils/logging.py index f82c5d56548..82b99762807 100644 --- a/src/pip/_internal/utils/logging.py +++ b/src/pip/_internal/utils/logging.py @@ -8,13 +8,13 @@ import os import sys from logging import Filter, getLogger +from typing import TYPE_CHECKING from pip._internal.utils.compat import WINDOWS from pip._internal.utils.deprecation import DEPRECATION_MSG_PREFIX from pip._internal.utils.misc import ensure_dir -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Any try: diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 809392865fb..984efbc662b 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -16,6 +16,7 @@ import urllib.parse from io import StringIO from itertools import filterfalse, tee, zip_longest +from typing import TYPE_CHECKING, cast from pip._vendor import pkg_resources @@ -27,13 +28,12 @@ from pip._internal.exceptions import CommandError from pip._internal.locations import get_major_minor_version, site_packages, user_site from pip._internal.utils.compat import WINDOWS, stdlib_pkgs -from pip._internal.utils.typing import MYPY_CHECK_RUNNING, cast from pip._internal.utils.virtualenv import ( running_under_virtualenv, virtualenv_no_global, ) -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import ( Any, AnyStr, diff --git a/src/pip/_internal/utils/packaging.py b/src/pip/_internal/utils/packaging.py index fae06070c87..f8de544d30c 100644 --- a/src/pip/_internal/utils/packaging.py +++ b/src/pip/_internal/utils/packaging.py @@ -1,14 +1,14 @@ import logging from email.parser import FeedParser +from typing import TYPE_CHECKING from pip._vendor import pkg_resources from pip._vendor.packaging import specifiers, version from pip._internal.exceptions import NoneMetadataError from pip._internal.utils.misc import display_path -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from email.message import Message from typing import Optional, Tuple diff --git a/src/pip/_internal/utils/parallel.py b/src/pip/_internal/utils/parallel.py index 57082367e18..af5d4a9df98 100644 --- a/src/pip/_internal/utils/parallel.py +++ b/src/pip/_internal/utils/parallel.py @@ -21,12 +21,11 @@ from contextlib import contextmanager from multiprocessing import Pool as ProcessPool from multiprocessing.dummy import Pool as ThreadPool +from typing import TYPE_CHECKING from pip._vendor.requests.adapters import DEFAULT_POOLSIZE -from pip._internal.utils.typing import MYPY_CHECK_RUNNING - -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from multiprocessing import pool from typing import Callable, Iterable, Iterator, TypeVar, Union diff --git a/src/pip/_internal/utils/pkg_resources.py b/src/pip/_internal/utils/pkg_resources.py index 816ac122369..913bebd9834 100644 --- a/src/pip/_internal/utils/pkg_resources.py +++ b/src/pip/_internal/utils/pkg_resources.py @@ -1,8 +1,8 @@ -from pip._vendor.pkg_resources import yield_lines +from typing import TYPE_CHECKING -from pip._internal.utils.typing import MYPY_CHECK_RUNNING +from pip._vendor.pkg_resources import yield_lines -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Dict, Iterable, List diff --git a/src/pip/_internal/utils/setuptools_build.py b/src/pip/_internal/utils/setuptools_build.py index 2a664b00703..49b0f22f5a2 100644 --- a/src/pip/_internal/utils/setuptools_build.py +++ b/src/pip/_internal/utils/setuptools_build.py @@ -1,8 +1,7 @@ import sys +from typing import TYPE_CHECKING -from pip._internal.utils.typing import MYPY_CHECK_RUNNING - -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import List, Optional, Sequence # Shim to wrap setup.py invocation with setuptools diff --git a/src/pip/_internal/utils/subprocess.py b/src/pip/_internal/utils/subprocess.py index f685b03b34f..82bc3987ccf 100644 --- a/src/pip/_internal/utils/subprocess.py +++ b/src/pip/_internal/utils/subprocess.py @@ -2,15 +2,15 @@ import os import shlex import subprocess +from typing import TYPE_CHECKING from pip._internal.cli.spinners import SpinnerInterface, open_spinner from pip._internal.exceptions import InstallationSubprocessError from pip._internal.utils.compat import console_to_str, str_to_display from pip._internal.utils.logging import subprocess_logger from pip._internal.utils.misc import HiddenText, path_to_display -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Any, Callable, Iterable, List, Mapping, Optional, Union CommandArgs = List[Union[str, HiddenText]] diff --git a/src/pip/_internal/utils/temp_dir.py b/src/pip/_internal/utils/temp_dir.py index c7fca502b50..562795a5a3f 100644 --- a/src/pip/_internal/utils/temp_dir.py +++ b/src/pip/_internal/utils/temp_dir.py @@ -4,13 +4,13 @@ import os.path import tempfile from contextlib import contextmanager +from typing import TYPE_CHECKING from pip._vendor.contextlib2 import ExitStack from pip._internal.utils.misc import enum, rmtree -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Any, Dict, Iterator, Optional, TypeVar, Union _T = TypeVar('_T', bound='TempDirectory') diff --git a/src/pip/_internal/utils/typing.py b/src/pip/_internal/utils/typing.py deleted file mode 100644 index 8505a29b15d..00000000000 --- a/src/pip/_internal/utils/typing.py +++ /dev/null @@ -1,38 +0,0 @@ -"""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._internal.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 - - -if MYPY_CHECK_RUNNING: - from typing import cast -else: - # typing's cast() is needed at runtime, but we don't want to import typing. - # Thus, we use a dummy no-op version, which we tell mypy to ignore. - def cast(type_, value): # type: ignore - return value diff --git a/src/pip/_internal/utils/unpacking.py b/src/pip/_internal/utils/unpacking.py index a24d7e55735..86c474458e2 100644 --- a/src/pip/_internal/utils/unpacking.py +++ b/src/pip/_internal/utils/unpacking.py @@ -7,6 +7,7 @@ import stat import tarfile import zipfile +from typing import TYPE_CHECKING from pip._internal.exceptions import InstallationError from pip._internal.utils.filetypes import ( @@ -16,9 +17,8 @@ ZIP_EXTENSIONS, ) from pip._internal.utils.misc import ensure_dir -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Iterable, List, Optional from zipfile import ZipInfo diff --git a/src/pip/_internal/utils/urls.py b/src/pip/_internal/utils/urls.py index 0ef063c2198..da8e91a4bdc 100644 --- a/src/pip/_internal/utils/urls.py +++ b/src/pip/_internal/utils/urls.py @@ -2,10 +2,9 @@ import sys import urllib.parse import urllib.request +from typing import TYPE_CHECKING -from pip._internal.utils.typing import MYPY_CHECK_RUNNING - -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Optional diff --git a/src/pip/_internal/utils/virtualenv.py b/src/pip/_internal/utils/virtualenv.py index 3086bf2fc8d..eb91c907185 100644 --- a/src/pip/_internal/utils/virtualenv.py +++ b/src/pip/_internal/utils/virtualenv.py @@ -3,10 +3,9 @@ import re import site import sys +from typing import TYPE_CHECKING -from pip._internal.utils.typing import MYPY_CHECK_RUNNING - -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import List, Optional logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/utils/wheel.py b/src/pip/_internal/utils/wheel.py index c84cae36a9d..65ebf6424df 100644 --- a/src/pip/_internal/utils/wheel.py +++ b/src/pip/_internal/utils/wheel.py @@ -3,6 +3,7 @@ import logging from email.parser import Parser +from typing import TYPE_CHECKING from zipfile import BadZipFile, ZipFile from pip._vendor.packaging.utils import canonicalize_name @@ -10,9 +11,8 @@ from pip._internal.exceptions import UnsupportedWheel from pip._internal.utils.pkg_resources import DictMetadata -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from email.message import Message from typing import Dict, Tuple diff --git a/src/pip/_internal/vcs/__init__.py b/src/pip/_internal/vcs/__init__.py index 2a4eb137576..2ed7c177f7c 100644 --- a/src/pip/_internal/vcs/__init__.py +++ b/src/pip/_internal/vcs/__init__.py @@ -1,6 +1,6 @@ # Expose a limited set of classes and functions so callers outside of # the vcs package don't need to import deeper than `pip._internal.vcs`. -# (The test directory and imports protected by MYPY_CHECK_RUNNING may +# (The test directory and imports protected by TYPE_CHECKING may # still need to import from a vcs sub-package.) # Import all vcs modules to register each VCS in the VcsSupport object. import pip._internal.vcs.bazaar diff --git a/src/pip/_internal/vcs/bazaar.py b/src/pip/_internal/vcs/bazaar.py index 13d2bc88aaf..6ccf8df5b96 100644 --- a/src/pip/_internal/vcs/bazaar.py +++ b/src/pip/_internal/vcs/bazaar.py @@ -1,13 +1,13 @@ import logging import os +from typing import TYPE_CHECKING from pip._internal.utils.misc import display_path, rmtree from pip._internal.utils.subprocess import make_command -from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.urls import path_to_url from pip._internal.vcs.versioncontrol import RemoteNotFoundError, VersionControl, vcs -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import List, Optional, Tuple from pip._internal.utils.misc import HiddenText diff --git a/src/pip/_internal/vcs/git.py b/src/pip/_internal/vcs/git.py index 846563294cf..7c7104c9f35 100644 --- a/src/pip/_internal/vcs/git.py +++ b/src/pip/_internal/vcs/git.py @@ -3,6 +3,7 @@ import re import urllib.parse import urllib.request +from typing import TYPE_CHECKING from pip._vendor.packaging.version import parse as parse_version @@ -10,7 +11,6 @@ from pip._internal.utils.misc import display_path, hide_url from pip._internal.utils.subprocess import make_command from pip._internal.utils.temp_dir import TempDirectory -from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.vcs.versioncontrol import ( RemoteNotFoundError, VersionControl, @@ -18,7 +18,7 @@ vcs, ) -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import List, Optional, Tuple from pip._vendor.packaging.version import _BaseVersion diff --git a/src/pip/_internal/vcs/mercurial.py b/src/pip/_internal/vcs/mercurial.py index 442ff421764..079672191f9 100644 --- a/src/pip/_internal/vcs/mercurial.py +++ b/src/pip/_internal/vcs/mercurial.py @@ -1,12 +1,12 @@ import configparser import logging import os +from typing import TYPE_CHECKING from pip._internal.exceptions import BadCommand, InstallationError from pip._internal.utils.misc import display_path from pip._internal.utils.subprocess import make_command from pip._internal.utils.temp_dir import TempDirectory -from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.urls import path_to_url from pip._internal.vcs.versioncontrol import ( VersionControl, @@ -14,7 +14,7 @@ vcs, ) -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import List, Optional from pip._internal.utils.misc import HiddenText diff --git a/src/pip/_internal/vcs/subversion.py b/src/pip/_internal/vcs/subversion.py index e93483739cc..2ddb9c0a150 100644 --- a/src/pip/_internal/vcs/subversion.py +++ b/src/pip/_internal/vcs/subversion.py @@ -1,6 +1,7 @@ import logging import os import re +from typing import TYPE_CHECKING from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import ( @@ -10,7 +11,6 @@ split_auth_from_netloc, ) from pip._internal.utils.subprocess import make_command -from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.vcs.versioncontrol import RemoteNotFoundError, VersionControl, vcs _svn_xml_url_re = re.compile('url="([^"]+)"') @@ -19,7 +19,7 @@ _svn_info_xml_url_re = re.compile(r'<url>(.*)</url>') -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import List, Optional, Tuple from pip._internal.utils.misc import HiddenText diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index 218de58a6d4..84ce0dffccb 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -5,6 +5,7 @@ import shutil import sys import urllib.parse +from typing import TYPE_CHECKING from pip._vendor import pkg_resources @@ -18,10 +19,9 @@ rmtree, ) from pip._internal.utils.subprocess import call_subprocess, make_command -from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.urls import get_url_scheme -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import ( Any, Dict, diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index 9fbb5329f4c..4b72e512466 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -5,6 +5,7 @@ import os.path import re import shutil +from typing import TYPE_CHECKING from pip._vendor.packaging.utils import canonicalize_name, canonicalize_version from pip._vendor.packaging.version import InvalidVersion, Version @@ -20,11 +21,10 @@ from pip._internal.utils.setuptools_build import make_setuptools_clean_args from pip._internal.utils.subprocess import call_subprocess from pip._internal.utils.temp_dir import TempDirectory -from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.utils.urls import path_to_url from pip._internal.vcs import vcs -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Any, Callable, Iterable, List, Optional, Tuple from pip._internal.cache import WheelCache diff --git a/tests/conftest.py b/tests/conftest.py index 257e5233169..f4ffee4697a 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,6 +8,7 @@ import sys import time from contextlib import contextmanager +from typing import TYPE_CHECKING from unittest.mock import patch import pytest @@ -16,14 +17,13 @@ from pip._internal.cli.main import main as pip_entry_point from pip._internal.utils.temp_dir import global_tempdir_manager -from pip._internal.utils.typing import MYPY_CHECK_RUNNING from tests.lib import DATA_DIR, SRC_DIR, PipTestEnvironment, TestData from tests.lib.certs import make_tls_cert, serialize_cert, serialize_key from tests.lib.path import Path from tests.lib.server import make_mock_server, server_running from tests.lib.venv import VirtualEnvironment -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Dict, Iterable from tests.lib.server import MockServer as _MockServer diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index f0a5a9b2db3..02395b75468 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -10,6 +10,7 @@ from hashlib import sha256 from io import BytesIO from textwrap import dedent +from typing import TYPE_CHECKING from zipfile import ZipFile import pytest @@ -22,11 +23,10 @@ from pip._internal.models.selection_prefs import SelectionPreferences from pip._internal.network.session import PipSession from pip._internal.utils.deprecation import DEPRECATION_MSG_PREFIX -from pip._internal.utils.typing import MYPY_CHECK_RUNNING from tests.lib.path import Path, curdir from tests.lib.wheel import make_wheel -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import List, Optional from pip._internal.models.target_python import TargetPython diff --git a/tests/lib/certs.py b/tests/lib/certs.py index 1f51f2174cf..779afd018e3 100644 --- a/tests/lib/certs.py +++ b/tests/lib/certs.py @@ -1,4 +1,5 @@ from datetime import datetime, timedelta +from typing import TYPE_CHECKING from cryptography import x509 from cryptography.hazmat.backends import default_backend @@ -6,9 +7,7 @@ from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.x509.oid import NameOID -from pip._internal.utils.typing import MYPY_CHECK_RUNNING - -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import Tuple diff --git a/tests/lib/local_repos.py b/tests/lib/local_repos.py index 63ec50ccb1d..09e38640e00 100644 --- a/tests/lib/local_repos.py +++ b/tests/lib/local_repos.py @@ -1,13 +1,13 @@ import os import subprocess import urllib.request +from typing import TYPE_CHECKING from pip._internal.utils.misc import hide_url -from pip._internal.utils.typing import MYPY_CHECK_RUNNING from pip._internal.vcs import vcs from tests.lib import path_to_url -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from tests.lib.path import Path diff --git a/tests/lib/server.py b/tests/lib/server.py index 966dfed58a4..169b2e01c35 100644 --- a/tests/lib/server.py +++ b/tests/lib/server.py @@ -5,15 +5,14 @@ from base64 import b64encode from contextlib import contextmanager from textwrap import dedent +from typing import TYPE_CHECKING from unittest.mock import Mock from pip._vendor.contextlib2 import nullcontext from werkzeug.serving import WSGIRequestHandler from werkzeug.serving import make_server as _make_server -from pip._internal.utils.typing import MYPY_CHECK_RUNNING - -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from types import TracebackType from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Type diff --git a/tests/lib/test_wheel.py b/tests/lib/test_wheel.py index 31d918abd73..294fd7c037a 100644 --- a/tests/lib/test_wheel.py +++ b/tests/lib/test_wheel.py @@ -3,9 +3,9 @@ import csv from email import message_from_string from functools import partial +from typing import TYPE_CHECKING from zipfile import ZipFile -from pip._internal.utils.typing import MYPY_CHECK_RUNNING from tests.lib.wheel import ( _default, make_metadata_file, @@ -14,7 +14,7 @@ message_from_dict, ) -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from email import Message diff --git a/tests/lib/wheel.py b/tests/lib/wheel.py index fee68b13b0b..6028f117d7a 100644 --- a/tests/lib/wheel.py +++ b/tests/lib/wheel.py @@ -10,14 +10,14 @@ from functools import partial from hashlib import sha256 from io import BytesIO, StringIO +from typing import TYPE_CHECKING from zipfile import ZipFile from pip._vendor.requests.structures import CaseInsensitiveDict -from pip._internal.utils.typing import MYPY_CHECK_RUNNING from tests.lib.path import Path -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from typing import ( AnyStr, Callable, @@ -51,7 +51,7 @@ class Default(Enum): _default = Default.token -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: T = TypeVar("T") class Defaulted(Union[Default, T]): diff --git a/tests/unit/test_utils_wheel.py b/tests/unit/test_utils_wheel.py index a73ecd6c3d6..9a41d2c911b 100644 --- a/tests/unit/test_utils_wheel.py +++ b/tests/unit/test_utils_wheel.py @@ -1,6 +1,7 @@ import os from email import message_from_string from io import BytesIO +from typing import TYPE_CHECKING from zipfile import ZipFile import pytest @@ -8,9 +9,8 @@ from pip._internal.exceptions import UnsupportedWheel from pip._internal.utils import wheel -from pip._internal.utils.typing import MYPY_CHECK_RUNNING -if MYPY_CHECK_RUNNING: +if TYPE_CHECKING: from tests.lib.path import Path From 383934eb830348afa2f9bfc40e30681ffcd776cf Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 4 Feb 2021 15:36:09 +0800 Subject: [PATCH 2966/3170] Incorporate get_installed_version() logic All usages of it now use Environment.get_distribution() instead. InstallRequirement.installed_version is also removed since it is no longer used anywhere in the code base. --- src/pip/_internal/commands/install.py | 13 +++--- src/pip/_internal/network/session.py | 13 +++--- src/pip/_internal/req/req_install.py | 6 --- src/pip/_internal/self_outdated_check.py | 12 +++--- src/pip/_internal/utils/misc.py | 22 +--------- tests/unit/test_req.py | 4 -- tests/unit/test_self_check_outdated.py | 51 ++++++++++++++---------- 7 files changed, 46 insertions(+), 75 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index a6989a0f8df..ca90a86f1dd 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -7,7 +7,6 @@ from optparse import SUPPRESS_HELP from typing import TYPE_CHECKING -from pip._vendor import pkg_resources from pip._vendor.packaging.utils import canonicalize_name from pip._internal.cache import WheelCache @@ -17,6 +16,7 @@ from pip._internal.cli.status_codes import ERROR, SUCCESS from pip._internal.exceptions import CommandError, InstallationError from pip._internal.locations import distutils_scheme +from pip._internal.metadata import get_environment from pip._internal.operations.check import check_install_conflicts from pip._internal.req import install_given_reqs from pip._internal.req.req_tracker import get_requirement_tracker @@ -24,7 +24,6 @@ from pip._internal.utils.filesystem import test_writable_dir from pip._internal.utils.misc import ( ensure_dir, - get_installed_version, get_pip_version, protect_pip_from_modification_on_windows, write_output, @@ -407,18 +406,16 @@ def run(self, options, args): prefix=options.prefix_path, isolated=options.isolated_mode, ) - working_set = pkg_resources.WorkingSet(lib_locations) + env = get_environment(lib_locations) installed.sort(key=operator.attrgetter('name')) items = [] for result in installed: item = result.name try: - installed_version = get_installed_version( - result.name, working_set=working_set - ) - if installed_version: - item += '-' + installed_version + installed_dist = env.get_distribution(item) + if installed_dist is not None: + item = f"{item}-{installed_dist.version}" except Exception: pass items.append(item) diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py index 83bcecca382..03c3725abd1 100644 --- a/src/pip/_internal/network/session.py +++ b/src/pip/_internal/network/session.py @@ -25,17 +25,14 @@ from pip._vendor.urllib3.exceptions import InsecureRequestWarning from pip import __version__ +from pip._internal.metadata import get_default_environment from pip._internal.network.auth import MultiDomainBasicAuth from pip._internal.network.cache import SafeFileCache # Import ssl from compat so the initial import occurs in only one place. from pip._internal.utils.compat import has_tls from pip._internal.utils.glibc import libc_ver -from pip._internal.utils.misc import ( - build_url_from_netloc, - get_installed_version, - parse_netloc, -) +from pip._internal.utils.misc import build_url_from_netloc, parse_netloc from pip._internal.utils.urls import url_to_path if TYPE_CHECKING: @@ -156,9 +153,9 @@ def user_agent(): import _ssl as ssl data["openssl_version"] = ssl.OPENSSL_VERSION - setuptools_version = get_installed_version("setuptools") - if setuptools_version is not None: - data["setuptools_version"] = setuptools_version + setuptools_dist = get_default_environment().get_distribution("setuptools") + if setuptools_dist is not None: + data["setuptools_version"] = str(setuptools_dist.version) # Use None rather than False so as not to give the impression that # pip knows it is not being run under CI. Rather, it is a null or diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 4485634532c..29a5cd275ee 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -43,7 +43,6 @@ dist_in_site_packages, dist_in_usersite, get_distribution, - get_installed_version, hide_url, redact_auth_from_url, ) @@ -273,11 +272,6 @@ def is_pinned(self): return (len(specifiers) == 1 and next(iter(specifiers)).operator in {'==', '==='}) - @property - def installed_version(self): - # type: () -> Optional[str] - return get_installed_version(self.name) - def match_markers(self, extras_requested=None): # type: (Optional[Iterable[str]]) -> bool if not extras_requested: diff --git a/src/pip/_internal/self_outdated_check.py b/src/pip/_internal/self_outdated_check.py index 9281636fa30..f705dbc8308 100644 --- a/src/pip/_internal/self_outdated_check.py +++ b/src/pip/_internal/self_outdated_check.py @@ -6,14 +6,14 @@ import sys from typing import TYPE_CHECKING -from pip._vendor.packaging import version as packaging_version +from pip._vendor.packaging.version import parse as parse_version from pip._internal.index.collector import LinkCollector from pip._internal.index.package_finder import PackageFinder from pip._internal.metadata import get_default_environment from pip._internal.models.selection_prefs import SelectionPreferences from pip._internal.utils.filesystem import adjacent_tmp_file, check_path_owner, replace -from pip._internal.utils.misc import ensure_dir, get_installed_version +from pip._internal.utils.misc import ensure_dir if TYPE_CHECKING: import optparse @@ -114,11 +114,11 @@ def pip_self_version_check(session, options): 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 not installed_version: + installed_dist = get_default_environment().get_distribution("pip") + if not installed_dist: return - pip_version = packaging_version.parse(installed_version) + pip_version = installed_dist.version pypi_version = None try: @@ -162,7 +162,7 @@ def pip_self_version_check(session, options): # save that we've performed a check state.save(pypi_version, current_time) - remote_version = packaging_version.parse(pypi_version) + remote_version = parse_version(pypi_version) local_version_is_older = ( pip_version < remote_version and diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 984efbc662b..200fe841814 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -18,8 +18,6 @@ from itertools import filterfalse, tee, zip_longest from typing import TYPE_CHECKING, cast -from pip._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 pip._vendor.retrying import retry # type: ignore @@ -59,7 +57,7 @@ 'normalize_path', 'renames', 'get_prog', 'captured_stdout', 'ensure_dir', - 'get_installed_version', 'remove_auth_from_url'] + 'remove_auth_from_url'] logger = logging.getLogger(__name__) @@ -560,24 +558,6 @@ def captured_stderr(): return captured_output('stderr') -def get_installed_version(dist_name, working_set=None): - """Get the installed version of dist_name avoiding pkg_resources cache""" - # Create a requirement that we'll look for inside of setuptools. - req = pkg_resources.Requirement.parse(dist_name) - - if working_set is None: - # We want to avoid having this cached, so we need to construct a new - # working set each time. - working_set = pkg_resources.WorkingSet() - - # Get the installed distribution from our working set - dist = working_set.find(req) - - # Check to see if we got an installed distribution or not, if we did - # we want to return it's version. - return dist.version if dist else None - - # Simulates an enum def enum(*sequential, **named): enums = dict(zip(sequential, range(len(sequential))), **named) diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index b655a187ebe..db638659bce 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -382,10 +382,6 @@ def test_unsupported_wheel_local_file_requirement_raises(self, data): with pytest.raises(InstallationError): reqset.add_requirement(req) - def test_installed_version_not_installed(self): - req = install_req_from_line('simple-0.1-py2.py3-none-any.whl') - assert req.installed_version is None - def test_str(self): req = install_req_from_line('simple==0.1') assert str(req) == 'simple==0.1' diff --git a/tests/unit/test_self_check_outdated.py b/tests/unit/test_self_check_outdated.py index 2e8663a9c01..65689506cc2 100644 --- a/tests/unit/test_self_check_outdated.py +++ b/tests/unit/test_self_check_outdated.py @@ -1,4 +1,5 @@ import datetime +import functools import json import os import sys @@ -6,6 +7,7 @@ import freezegun import pretend import pytest +from pip._vendor.packaging.version import parse as parse_version from pip._internal import self_outdated_check from pip._internal.models.candidate import InstallationCandidate @@ -44,25 +46,20 @@ def find_best_candidate(self, project_name): class MockDistribution: - def __init__(self, installer): + def __init__(self, installer, version): self.installer = installer - - def has_metadata(self, name): - return name == 'INSTALLER' - - def get_metadata_lines(self, name): - if self.has_metadata(name): - yield self.installer - else: - raise NotImplementedError('nope') + self.version = parse_version(version) class MockEnvironment(object): - def __init__(self, installer): + def __init__(self, installer, installed_version): self.installer = installer + self.installed_version = installed_version def get_distribution(self, name): - return MockDistribution(self.installer) + if self.installed_version is None: + return None + return MockDistribution(self.installer, self.installed_version) def _options(): @@ -97,16 +94,26 @@ def _options(): def test_pip_self_version_check(monkeypatch, stored_time, installed_ver, new_ver, installer, check_if_upgrade_required, check_warn_logs): - monkeypatch.setattr(self_outdated_check, 'get_installed_version', - lambda name: installed_ver) - monkeypatch.setattr(self_outdated_check, 'PackageFinder', - MockPackageFinder) - monkeypatch.setattr(logger, 'warning', - pretend.call_recorder(lambda *a, **kw: None)) - monkeypatch.setattr(logger, 'debug', - pretend.call_recorder(lambda s, exc_info=None: None)) - monkeypatch.setattr(self_outdated_check, 'get_default_environment', - lambda: MockEnvironment(installer)) + monkeypatch.setattr( + self_outdated_check, + "get_default_environment", + functools.partial(MockEnvironment, installer, installed_ver), + ) + monkeypatch.setattr( + self_outdated_check, + "PackageFinder", + MockPackageFinder, + ) + monkeypatch.setattr( + logger, + "warning", + pretend.call_recorder(lambda *a, **kw: None), + ) + monkeypatch.setattr( + logger, + "debug", + pretend.call_recorder(lambda s, exc_info=None: None), + ) fake_state = pretend.stub( state={"last_check": stored_time, 'pypi_version': installed_ver}, From 2ca21dfc12589b95ac8f2bcd123b2a12e5ad87b1 Mon Sep 17 00:00:00 2001 From: Jim Fisher <jameshfisher@gmail.com> Date: Fri, 19 Feb 2021 10:33:56 +0000 Subject: [PATCH 2967/3170] Point user to `pip debug --verbose` to debug incompatible wheel Resolves https://github.com/pypa/pip/issues/9621 --- src/pip/_internal/index/package_finder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/index/package_finder.py b/src/pip/_internal/index/package_finder.py index 5e603ef150b..0a5836ded9d 100644 --- a/src/pip/_internal/index/package_finder.py +++ b/src/pip/_internal/index/package_finder.py @@ -194,7 +194,7 @@ def evaluate_link(self, link): # simplify troubleshooting compatibility issues. file_tags = wheel.get_formatted_file_tags() reason = ( - "none of the wheel's tags match: {}".format( + "none of the wheel's tags ({}) are compatible (run pip debug --verbose to show compatible tags)".format( ', '.join(file_tags) ) ) From a1d41f3fe202dfe1e4be0170abfdadc4e2716a98 Mon Sep 17 00:00:00 2001 From: Jim Fisher <jameshfisher@gmail.com> Date: Fri, 19 Feb 2021 10:55:56 +0000 Subject: [PATCH 2968/3170] split line to please linter --- src/pip/_internal/index/package_finder.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/index/package_finder.py b/src/pip/_internal/index/package_finder.py index 0a5836ded9d..562ec4e9522 100644 --- a/src/pip/_internal/index/package_finder.py +++ b/src/pip/_internal/index/package_finder.py @@ -194,7 +194,8 @@ def evaluate_link(self, link): # simplify troubleshooting compatibility issues. file_tags = wheel.get_formatted_file_tags() reason = ( - "none of the wheel's tags ({}) are compatible (run pip debug --verbose to show compatible tags)".format( + "none of the wheel's tags ({}) are compatible " + "(run pip debug --verbose to show compatible tags)".format( ', '.join(file_tags) ) ) From 3bd019b9f6b380762e241992153cc9a8011315e0 Mon Sep 17 00:00:00 2001 From: Jim Fisher <jameshfisher@gmail.com> Date: Fri, 19 Feb 2021 10:58:14 +0000 Subject: [PATCH 2969/3170] fix test after changing debug string --- tests/unit/test_index.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/tests/unit/test_index.py b/tests/unit/test_index.py index b6f7f632cc0..8b56d7854af 100644 --- a/tests/unit/test_index.py +++ b/tests/unit/test_index.py @@ -170,7 +170,9 @@ def test_evaluate_link__incompatible_wheel(self): link = Link('https://example.com/sample-1.0-py2.py3-none-any.whl') actual = evaluator.evaluate_link(link) expected = ( - False, "none of the wheel's tags match: py2-none-any, py3-none-any" + False, + "none of the wheel's tags (py2-none-any, py3-none-any) are compatible " + "(run pip debug --verbose to show compatible tags)" ) assert actual == expected From d9b5525193c99fa222d6717f27ab72ca6fc0101b Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Sun, 27 Dec 2020 15:48:06 -0800 Subject: [PATCH 2970/3170] Prefer stdlib contextlib over contextlib2 when available Removes the vendored contextlib2. ExitStack and suppress are available from stdlib contextlib on all supported Python versions. The nullcontext context manager which isn't available in Python 3.6, but the function is simple to implement. Once Python 3.6 support is dropped, so too can the compat shim. --- news/contextlib2.vendor.rst | 1 + pyproject.toml | 1 - src/pip/_internal/cli/command_context.py | 4 +- src/pip/_internal/cli/parser.py | 3 +- src/pip/_internal/req/req_tracker.py | 4 +- src/pip/_internal/utils/temp_dir.py | 4 +- src/pip/_vendor/__init__.py | 1 - src/pip/_vendor/contextlib2.LICENSE.txt | 122 ------ src/pip/_vendor/contextlib2.py | 518 ----------------------- src/pip/_vendor/vendor.txt | 1 - tests/conftest.py | 5 +- tests/lib/compat.py | 21 + tests/lib/server.py | 3 +- tests/unit/test_utils_wheel.py | 2 +- 14 files changed, 32 insertions(+), 658 deletions(-) create mode 100644 news/contextlib2.vendor.rst delete mode 100644 src/pip/_vendor/contextlib2.LICENSE.txt delete mode 100644 src/pip/_vendor/contextlib2.py create mode 100644 tests/lib/compat.py diff --git a/news/contextlib2.vendor.rst b/news/contextlib2.vendor.rst new file mode 100644 index 00000000000..2a44430775a --- /dev/null +++ b/news/contextlib2.vendor.rst @@ -0,0 +1 @@ +Remove contextlib2. diff --git a/pyproject.toml b/pyproject.toml index 04f7258064e..281594b21c7 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -49,7 +49,6 @@ drop = [ [tool.vendoring.typing-stubs] six = ["six.__init__", "six.moves.__init__", "six.moves.configparser"] appdirs = [] -contextlib2 = [] [tool.vendoring.license.directories] setuptools = "pkg_resources" diff --git a/src/pip/_internal/cli/command_context.py b/src/pip/_internal/cli/command_context.py index d04b35db4e7..b8eb9ecbb85 100644 --- a/src/pip/_internal/cli/command_context.py +++ b/src/pip/_internal/cli/command_context.py @@ -1,8 +1,6 @@ -from contextlib import contextmanager +from contextlib import ExitStack, contextmanager from typing import TYPE_CHECKING -from pip._vendor.contextlib2 import ExitStack - if TYPE_CHECKING: from typing import ContextManager, Iterator, TypeVar diff --git a/src/pip/_internal/cli/parser.py b/src/pip/_internal/cli/parser.py index d5389a9a15a..1c8ce1451e6 100644 --- a/src/pip/_internal/cli/parser.py +++ b/src/pip/_internal/cli/parser.py @@ -8,10 +8,9 @@ import shutil import sys import textwrap +from contextlib import suppress from typing import TYPE_CHECKING -from pip._vendor.contextlib2 import suppress - from pip._internal.cli.status_codes import UNKNOWN_ERROR from pip._internal.configuration import Configuration, ConfigurationError from pip._internal.utils.misc import redact_auth_from_url, strtobool diff --git a/src/pip/_internal/req/req_tracker.py b/src/pip/_internal/req/req_tracker.py index ac4831a8405..ab753ce6ddd 100644 --- a/src/pip/_internal/req/req_tracker.py +++ b/src/pip/_internal/req/req_tracker.py @@ -4,8 +4,6 @@ import os from typing import TYPE_CHECKING -from pip._vendor import contextlib2 - from pip._internal.utils.temp_dir import TempDirectory if TYPE_CHECKING: @@ -49,7 +47,7 @@ def update_env_context_manager(**changes): def get_requirement_tracker(): # type: () -> Iterator[RequirementTracker] root = os.environ.get('PIP_REQ_TRACKER') - with contextlib2.ExitStack() as ctx: + with contextlib.ExitStack() as ctx: if root is None: root = ctx.enter_context( TempDirectory(kind='req-tracker') diff --git a/src/pip/_internal/utils/temp_dir.py b/src/pip/_internal/utils/temp_dir.py index 562795a5a3f..872b5b55fd5 100644 --- a/src/pip/_internal/utils/temp_dir.py +++ b/src/pip/_internal/utils/temp_dir.py @@ -3,11 +3,9 @@ import logging import os.path import tempfile -from contextlib import contextmanager +from contextlib import ExitStack, contextmanager from typing import TYPE_CHECKING -from pip._vendor.contextlib2 import ExitStack - from pip._internal.utils.misc import enum, rmtree if TYPE_CHECKING: diff --git a/src/pip/_vendor/__init__.py b/src/pip/_vendor/__init__.py index c3db83ff6aa..0abe99d4c98 100644 --- a/src/pip/_vendor/__init__.py +++ b/src/pip/_vendor/__init__.py @@ -62,7 +62,6 @@ def vendored(modulename): vendored("cachecontrol") vendored("certifi") vendored("colorama") - vendored("contextlib2") vendored("distlib") vendored("distro") vendored("html5lib") diff --git a/src/pip/_vendor/contextlib2.LICENSE.txt b/src/pip/_vendor/contextlib2.LICENSE.txt deleted file mode 100644 index 5de20277df9..00000000000 --- a/src/pip/_vendor/contextlib2.LICENSE.txt +++ /dev/null @@ -1,122 +0,0 @@ - - -A. HISTORY OF THE SOFTWARE -========================== - -contextlib2 is a derivative of the contextlib module distributed by the PSF -as part of the Python standard library. According, it is itself redistributed -under the PSF license (reproduced in full below). As the contextlib module -was added only in Python 2.5, the licenses for earlier Python versions are -not applicable and have not been included. - -Python was created in the early 1990s by Guido van Rossum at Stichting -Mathematisch Centrum (CWI, see http://www.cwi.nl) in the Netherlands -as a successor of a language called ABC. Guido remains Python's -principal author, although it includes many contributions from others. - -In 1995, Guido continued his work on Python at the Corporation for -National Research Initiatives (CNRI, see http://www.cnri.reston.va.us) -in Reston, Virginia where he released several versions of the -software. - -In May 2000, Guido and the Python core development team moved to -BeOpen.com to form the BeOpen PythonLabs team. In October of the same -year, the PythonLabs team moved to Digital Creations (now Zope -Corporation, see http://www.zope.com). In 2001, the Python Software -Foundation (PSF, see http://www.python.org/psf/) was formed, a -non-profit organization created specifically to own Python-related -Intellectual Property. Zope Corporation is a sponsoring member of -the PSF. - -All Python releases are Open Source (see http://www.opensource.org for -the Open Source Definition). Historically, most, but not all, Python -releases have also been GPL-compatible; the table below summarizes -the various releases that included the contextlib module. - - Release Derived Year Owner GPL- - from compatible? (1) - - 2.5 2.4 2006 PSF yes - 2.5.1 2.5 2007 PSF yes - 2.5.2 2.5.1 2008 PSF yes - 2.5.3 2.5.2 2008 PSF yes - 2.6 2.5 2008 PSF yes - 2.6.1 2.6 2008 PSF yes - 2.6.2 2.6.1 2009 PSF yes - 2.6.3 2.6.2 2009 PSF yes - 2.6.4 2.6.3 2009 PSF yes - 2.6.5 2.6.4 2010 PSF yes - 3.0 2.6 2008 PSF yes - 3.0.1 3.0 2009 PSF yes - 3.1 3.0.1 2009 PSF yes - 3.1.1 3.1 2009 PSF yes - 3.1.2 3.1.1 2010 PSF yes - 3.1.3 3.1.2 2010 PSF yes - 3.1.4 3.1.3 2011 PSF yes - 3.2 3.1 2011 PSF yes - 3.2.1 3.2 2011 PSF yes - 3.2.2 3.2.1 2011 PSF yes - 3.3 3.2 2012 PSF yes - -Footnotes: - -(1) GPL-compatible doesn't mean that we're distributing Python under - the GPL. All Python licenses, unlike the GPL, let you distribute - a modified version without making your changes open source. The - GPL-compatible licenses make it possible to combine Python with - other software that is released under the GPL; the others don't. - -Thanks to the many outside volunteers who have worked under Guido's -direction to make these releases possible. - - -B. TERMS AND CONDITIONS FOR ACCESSING OR OTHERWISE USING PYTHON -=============================================================== - -PYTHON SOFTWARE FOUNDATION LICENSE VERSION 2 --------------------------------------------- - -1. This LICENSE AGREEMENT is between the Python Software Foundation -("PSF"), and the Individual or Organization ("Licensee") accessing and -otherwise using this software ("Python") in source or binary form and -its associated documentation. - -2. Subject to the terms and conditions of this License Agreement, PSF hereby -grants Licensee a nonexclusive, royalty-free, world-wide license to reproduce, -analyze, test, perform and/or display publicly, prepare derivative works, -distribute, and otherwise use Python alone or in any derivative version, -provided, however, that PSF's License Agreement and PSF's notice of copyright, -i.e., "Copyright (c) 2001, 2002, 2003, 2004, 2005, 2006, 2007, 2008, 2009, 2010, -2011 Python Software Foundation; All Rights Reserved" are retained in Python -alone or in any derivative version prepared by Licensee. - -3. In the event Licensee prepares a derivative work that is based on -or incorporates Python or any part thereof, and wants to make -the derivative work available to others as provided herein, then -Licensee hereby agrees to include in any such work a brief summary of -the changes made to Python. - -4. PSF is making Python available to Licensee on an "AS IS" -basis. PSF MAKES NO REPRESENTATIONS OR WARRANTIES, EXPRESS OR -IMPLIED. BY WAY OF EXAMPLE, BUT NOT LIMITATION, PSF MAKES NO AND -DISCLAIMS ANY REPRESENTATION OR WARRANTY OF MERCHANTABILITY OR FITNESS -FOR ANY PARTICULAR PURPOSE OR THAT THE USE OF PYTHON WILL NOT -INFRINGE ANY THIRD PARTY RIGHTS. - -5. PSF SHALL NOT BE LIABLE TO LICENSEE OR ANY OTHER USERS OF PYTHON -FOR ANY INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES OR LOSS AS -A RESULT OF MODIFYING, DISTRIBUTING, OR OTHERWISE USING PYTHON, -OR ANY DERIVATIVE THEREOF, EVEN IF ADVISED OF THE POSSIBILITY THEREOF. - -6. This License Agreement will automatically terminate upon a material -breach of its terms and conditions. - -7. Nothing in this License Agreement shall be deemed to create any -relationship of agency, partnership, or joint venture between PSF and -Licensee. This License Agreement does not grant permission to use PSF -trademarks or trade name in a trademark sense to endorse or promote -products or services of Licensee, or any third party. - -8. By copying, installing or otherwise using Python, Licensee -agrees to be bound by the terms and conditions of this License -Agreement. diff --git a/src/pip/_vendor/contextlib2.py b/src/pip/_vendor/contextlib2.py deleted file mode 100644 index 3aae8f4117c..00000000000 --- a/src/pip/_vendor/contextlib2.py +++ /dev/null @@ -1,518 +0,0 @@ -"""contextlib2 - backports and enhancements to the contextlib module""" - -import abc -import sys -import warnings -from collections import deque -from functools import wraps - -__all__ = ["contextmanager", "closing", "nullcontext", - "AbstractContextManager", - "ContextDecorator", "ExitStack", - "redirect_stdout", "redirect_stderr", "suppress"] - -# Backwards compatibility -__all__ += ["ContextStack"] - - -# Backport abc.ABC -if sys.version_info[:2] >= (3, 4): - _abc_ABC = abc.ABC -else: - _abc_ABC = abc.ABCMeta('ABC', (object,), {'__slots__': ()}) - - -# Backport classic class MRO -def _classic_mro(C, result): - if C in result: - return - result.append(C) - for B in C.__bases__: - _classic_mro(B, result) - return result - - -# Backport _collections_abc._check_methods -def _check_methods(C, *methods): - try: - mro = C.__mro__ - except AttributeError: - mro = tuple(_classic_mro(C, [])) - - for method in methods: - for B in mro: - if method in B.__dict__: - if B.__dict__[method] is None: - return NotImplemented - break - else: - return NotImplemented - return True - - -class AbstractContextManager(_abc_ABC): - """An abstract base class for context managers.""" - - def __enter__(self): - """Return `self` upon entering the runtime context.""" - return self - - @abc.abstractmethod - def __exit__(self, exc_type, exc_value, traceback): - """Raise any exception triggered within the runtime context.""" - return None - - @classmethod - def __subclasshook__(cls, C): - """Check whether subclass is considered a subclass of this ABC.""" - if cls is AbstractContextManager: - return _check_methods(C, "__enter__", "__exit__") - return NotImplemented - - -class ContextDecorator(object): - """A base class or mixin that enables context managers to work as decorators.""" - - def refresh_cm(self): - """Returns the context manager used to actually wrap the call to the - decorated function. - - The default implementation just returns *self*. - - Overriding this method allows otherwise one-shot context managers - like _GeneratorContextManager to support use as decorators via - implicit recreation. - - DEPRECATED: refresh_cm was never added to the standard library's - ContextDecorator API - """ - warnings.warn("refresh_cm was never added to the standard library", - DeprecationWarning) - return self._recreate_cm() - - def _recreate_cm(self): - """Return a recreated instance of self. - - Allows an otherwise one-shot context manager like - _GeneratorContextManager to support use as - a decorator via implicit recreation. - - This is a private interface just for _GeneratorContextManager. - See issue #11647 for details. - """ - return self - - def __call__(self, func): - @wraps(func) - def inner(*args, **kwds): - with self._recreate_cm(): - return func(*args, **kwds) - return inner - - -class _GeneratorContextManager(ContextDecorator): - """Helper for @contextmanager decorator.""" - - def __init__(self, func, args, kwds): - self.gen = func(*args, **kwds) - self.func, self.args, self.kwds = func, args, kwds - # Issue 19330: ensure context manager instances have good docstrings - doc = getattr(func, "__doc__", None) - if doc is None: - doc = type(self).__doc__ - self.__doc__ = doc - # Unfortunately, this still doesn't provide good help output when - # inspecting the created context manager instances, since pydoc - # currently bypasses the instance docstring and shows the docstring - # for the class instead. - # See http://bugs.python.org/issue19404 for more details. - - def _recreate_cm(self): - # _GCM instances are one-shot context managers, so the - # CM must be recreated each time a decorated function is - # called - return self.__class__(self.func, self.args, self.kwds) - - def __enter__(self): - try: - return next(self.gen) - except StopIteration: - raise RuntimeError("generator didn't yield") - - def __exit__(self, type, value, traceback): - if type is None: - try: - next(self.gen) - except StopIteration: - return - else: - raise RuntimeError("generator didn't stop") - else: - if value is None: - # Need to force instantiation so we can reliably - # tell if we get the same exception back - value = type() - try: - self.gen.throw(type, value, traceback) - raise RuntimeError("generator didn't stop after throw()") - except StopIteration as exc: - # Suppress StopIteration *unless* it's the same exception that - # was passed to throw(). This prevents a StopIteration - # raised inside the "with" statement from being suppressed. - return exc is not value - except RuntimeError as exc: - # Don't re-raise the passed in exception - if exc is value: - return False - # Likewise, avoid suppressing if a StopIteration exception - # was passed to throw() and later wrapped into a RuntimeError - # (see PEP 479). - if _HAVE_EXCEPTION_CHAINING and exc.__cause__ is value: - return False - raise - except: - # only re-raise if it's *not* the exception that was - # passed to throw(), because __exit__() must not raise - # an exception unless __exit__() itself failed. But throw() - # has to raise the exception to signal propagation, so this - # fixes the impedance mismatch between the throw() protocol - # and the __exit__() protocol. - # - if sys.exc_info()[1] is not value: - raise - - -def contextmanager(func): - """@contextmanager decorator. - - Typical usage: - - @contextmanager - def some_generator(<arguments>): - <setup> - try: - yield <value> - finally: - <cleanup> - - This makes this: - - with some_generator(<arguments>) as <variable>: - <body> - - equivalent to this: - - <setup> - try: - <variable> = <value> - <body> - finally: - <cleanup> - - """ - @wraps(func) - def helper(*args, **kwds): - return _GeneratorContextManager(func, args, kwds) - return helper - - -class closing(object): - """Context to automatically close something at the end of a block. - - Code like this: - - with closing(<module>.open(<arguments>)) as f: - <block> - - is equivalent to this: - - f = <module>.open(<arguments>) - try: - <block> - finally: - f.close() - - """ - def __init__(self, thing): - self.thing = thing - - def __enter__(self): - return self.thing - - def __exit__(self, *exc_info): - self.thing.close() - - -class _RedirectStream(object): - - _stream = None - - def __init__(self, new_target): - self._new_target = new_target - # We use a list of old targets to make this CM re-entrant - self._old_targets = [] - - def __enter__(self): - self._old_targets.append(getattr(sys, self._stream)) - setattr(sys, self._stream, self._new_target) - return self._new_target - - def __exit__(self, exctype, excinst, exctb): - setattr(sys, self._stream, self._old_targets.pop()) - - -class redirect_stdout(_RedirectStream): - """Context manager for temporarily redirecting stdout to another file. - - # How to send help() to stderr - with redirect_stdout(sys.stderr): - help(dir) - - # How to write help() to a file - with open('help.txt', 'w') as f: - with redirect_stdout(f): - help(pow) - """ - - _stream = "stdout" - - -class redirect_stderr(_RedirectStream): - """Context manager for temporarily redirecting stderr to another file.""" - - _stream = "stderr" - - -class suppress(object): - """Context manager to suppress specified exceptions - - After the exception is suppressed, execution proceeds with the next - statement following the with statement. - - with suppress(FileNotFoundError): - os.remove(somefile) - # Execution still resumes here if the file was already removed - """ - - def __init__(self, *exceptions): - self._exceptions = exceptions - - def __enter__(self): - pass - - def __exit__(self, exctype, excinst, exctb): - # Unlike isinstance and issubclass, CPython exception handling - # currently only looks at the concrete type hierarchy (ignoring - # the instance and subclass checking hooks). While Guido considers - # that a bug rather than a feature, it's a fairly hard one to fix - # due to various internal implementation details. suppress provides - # the simpler issubclass based semantics, rather than trying to - # exactly reproduce the limitations of the CPython interpreter. - # - # See http://bugs.python.org/issue12029 for more details - return exctype is not None and issubclass(exctype, self._exceptions) - - -# Context manipulation is Python 3 only -_HAVE_EXCEPTION_CHAINING = sys.version_info[0] >= 3 -if _HAVE_EXCEPTION_CHAINING: - def _make_context_fixer(frame_exc): - def _fix_exception_context(new_exc, old_exc): - # Context may not be correct, so find the end of the chain - while 1: - exc_context = new_exc.__context__ - if exc_context is old_exc: - # Context is already set correctly (see issue 20317) - return - if exc_context is None or exc_context is frame_exc: - break - new_exc = exc_context - # Change the end of the chain to point to the exception - # we expect it to reference - new_exc.__context__ = old_exc - return _fix_exception_context - - def _reraise_with_existing_context(exc_details): - try: - # bare "raise exc_details[1]" replaces our carefully - # set-up context - fixed_ctx = exc_details[1].__context__ - raise exc_details[1] - except BaseException: - exc_details[1].__context__ = fixed_ctx - raise -else: - # No exception context in Python 2 - def _make_context_fixer(frame_exc): - return lambda new_exc, old_exc: None - - # Use 3 argument raise in Python 2, - # but use exec to avoid SyntaxError in Python 3 - def _reraise_with_existing_context(exc_details): - exc_type, exc_value, exc_tb = exc_details - exec("raise exc_type, exc_value, exc_tb") - -# Handle old-style classes if they exist -try: - from types import InstanceType -except ImportError: - # Python 3 doesn't have old-style classes - _get_type = type -else: - # Need to handle old-style context managers on Python 2 - def _get_type(obj): - obj_type = type(obj) - if obj_type is InstanceType: - return obj.__class__ # Old-style class - return obj_type # New-style class - - -# Inspired by discussions on http://bugs.python.org/issue13585 -class ExitStack(object): - """Context manager for dynamic management of a stack of exit callbacks - - For example: - - with ExitStack() as stack: - files = [stack.enter_context(open(fname)) for fname in filenames] - # All opened files will automatically be closed at the end of - # the with statement, even if attempts to open files later - # in the list raise an exception - - """ - def __init__(self): - self._exit_callbacks = deque() - - def pop_all(self): - """Preserve the context stack by transferring it to a new instance""" - new_stack = type(self)() - new_stack._exit_callbacks = self._exit_callbacks - self._exit_callbacks = deque() - return new_stack - - def _push_cm_exit(self, cm, cm_exit): - """Helper to correctly register callbacks to __exit__ methods""" - def _exit_wrapper(*exc_details): - return cm_exit(cm, *exc_details) - _exit_wrapper.__self__ = cm - self.push(_exit_wrapper) - - def push(self, exit): - """Registers a callback with the standard __exit__ method signature - - Can suppress exceptions the same way __exit__ methods can. - - Also accepts any object with an __exit__ method (registering a call - to the method instead of the object itself) - """ - # We use an unbound method rather than a bound method to follow - # the standard lookup behaviour for special methods - _cb_type = _get_type(exit) - try: - exit_method = _cb_type.__exit__ - except AttributeError: - # Not a context manager, so assume its a callable - self._exit_callbacks.append(exit) - else: - self._push_cm_exit(exit, exit_method) - return exit # Allow use as a decorator - - def callback(self, callback, *args, **kwds): - """Registers an arbitrary callback and arguments. - - Cannot suppress exceptions. - """ - def _exit_wrapper(exc_type, exc, tb): - callback(*args, **kwds) - # We changed the signature, so using @wraps is not appropriate, but - # setting __wrapped__ may still help with introspection - _exit_wrapper.__wrapped__ = callback - self.push(_exit_wrapper) - return callback # Allow use as a decorator - - def enter_context(self, cm): - """Enters the supplied context manager - - If successful, also pushes its __exit__ method as a callback and - returns the result of the __enter__ method. - """ - # We look up the special methods on the type to match the with statement - _cm_type = _get_type(cm) - _exit = _cm_type.__exit__ - result = _cm_type.__enter__(cm) - self._push_cm_exit(cm, _exit) - return result - - def close(self): - """Immediately unwind the context stack""" - self.__exit__(None, None, None) - - def __enter__(self): - return self - - def __exit__(self, *exc_details): - received_exc = exc_details[0] is not None - - # We manipulate the exception state so it behaves as though - # we were actually nesting multiple with statements - frame_exc = sys.exc_info()[1] - _fix_exception_context = _make_context_fixer(frame_exc) - - # Callbacks are invoked in LIFO order to match the behaviour of - # nested context managers - suppressed_exc = False - pending_raise = False - while self._exit_callbacks: - cb = self._exit_callbacks.pop() - try: - if cb(*exc_details): - suppressed_exc = True - pending_raise = False - exc_details = (None, None, None) - except: - new_exc_details = sys.exc_info() - # simulate the stack of exceptions by setting the context - _fix_exception_context(new_exc_details[1], exc_details[1]) - pending_raise = True - exc_details = new_exc_details - if pending_raise: - _reraise_with_existing_context(exc_details) - return received_exc and suppressed_exc - - -# Preserve backwards compatibility -class ContextStack(ExitStack): - """Backwards compatibility alias for ExitStack""" - - def __init__(self): - warnings.warn("ContextStack has been renamed to ExitStack", - DeprecationWarning) - super(ContextStack, self).__init__() - - def register_exit(self, callback): - return self.push(callback) - - def register(self, callback, *args, **kwds): - return self.callback(callback, *args, **kwds) - - def preserve(self): - return self.pop_all() - - -class nullcontext(AbstractContextManager): - """Context manager that does no additional processing. - Used as a stand-in for a normal context manager, when a particular - block of code is only sometimes used with a normal context manager: - cm = optional_cm if condition else nullcontext() - with cm: - # Perform operation, using optional_cm if condition is True - """ - - def __init__(self, enter_result=None): - self.enter_result = enter_result - - def __enter__(self): - return self.enter_result - - def __exit__(self, *excinfo): - pass diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index f36a78dbe78..51a5508479e 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -1,7 +1,6 @@ appdirs==1.4.4 CacheControl==0.12.6 colorama==0.4.4 -contextlib2==0.6.0.post1 distlib==0.3.1 distro==1.5.0 html5lib==1.1 diff --git a/tests/conftest.py b/tests/conftest.py index f4ffee4697a..e54b9d75ce1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -7,12 +7,11 @@ import subprocess import sys import time -from contextlib import contextmanager +from contextlib import ExitStack, contextmanager from typing import TYPE_CHECKING from unittest.mock import patch import pytest -from pip._vendor.contextlib2 import ExitStack, nullcontext from setuptools.wheel import Wheel from pip._internal.cli.main import main as pip_entry_point @@ -23,6 +22,8 @@ from tests.lib.server import make_mock_server, server_running from tests.lib.venv import VirtualEnvironment +from .lib.compat import nullcontext + if TYPE_CHECKING: from typing import Dict, Iterable diff --git a/tests/lib/compat.py b/tests/lib/compat.py new file mode 100644 index 00000000000..ab0ba287d51 --- /dev/null +++ b/tests/lib/compat.py @@ -0,0 +1,21 @@ +import contextlib +from typing import Iterator + + +@contextlib.contextmanager +def nullcontext(): + # type: () -> Iterator[None] + """ + Context manager that does no additional processing. + + Used as a stand-in for a normal context manager, when a particular block of + code is only sometimes used with a normal context manager: + + cm = optional_cm if condition else nullcontext() + with cm: + # Perform operation, using optional_cm if condition is True + + TODO: Replace with contextlib.nullcontext after dropping Python 3.6 + support. + """ + yield diff --git a/tests/lib/server.py b/tests/lib/server.py index 169b2e01c35..356495fa0ff 100644 --- a/tests/lib/server.py +++ b/tests/lib/server.py @@ -8,10 +8,11 @@ from typing import TYPE_CHECKING from unittest.mock import Mock -from pip._vendor.contextlib2 import nullcontext from werkzeug.serving import WSGIRequestHandler from werkzeug.serving import make_server as _make_server +from .compat import nullcontext + if TYPE_CHECKING: from types import TracebackType from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Type diff --git a/tests/unit/test_utils_wheel.py b/tests/unit/test_utils_wheel.py index 9a41d2c911b..461a29b5e01 100644 --- a/tests/unit/test_utils_wheel.py +++ b/tests/unit/test_utils_wheel.py @@ -1,11 +1,11 @@ import os +from contextlib import ExitStack from email import message_from_string from io import BytesIO from typing import TYPE_CHECKING from zipfile import ZipFile import pytest -from pip._vendor.contextlib2 import ExitStack from pip._internal.exceptions import UnsupportedWheel from pip._internal.utils import wheel From 3f6df167e0fdc7378ff706f1869c03b0c24676e3 Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Fri, 1 Jan 2021 16:25:40 -0800 Subject: [PATCH 2971/3170] Remove Python 2 compat shim path_to_display() Per the function's type signature, this accepted either a str or None. In both cases, the value was returned unaltered. Since dropping Python 2, it has been unnecessary. --- ...8d-1172-4012-a0a5-0fc42264a70d.trivial.rst | 0 src/pip/_internal/operations/prepare.py | 6 ++--- src/pip/_internal/utils/misc.py | 24 ------------------- src/pip/_internal/utils/subprocess.py | 5 ++-- tests/unit/test_utils.py | 17 ------------- 5 files changed, 5 insertions(+), 47 deletions(-) create mode 100644 news/a06e528d-1172-4012-a0a5-0fc42264a70d.trivial.rst diff --git a/news/a06e528d-1172-4012-a0a5-0fc42264a70d.trivial.rst b/news/a06e528d-1172-4012-a0a5-0fc42264a70d.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index e09fa7fb08f..917763149b7 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -32,7 +32,7 @@ from pip._internal.utils.filesystem import copy2_fixed from pip._internal.utils.hashes import MissingHashes from pip._internal.utils.logging import indent_log -from pip._internal.utils.misc import display_path, hide_url, path_to_display, rmtree +from pip._internal.utils.misc import display_path, hide_url, rmtree from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.unpacking import unpack_file from pip._internal.vcs import vcs @@ -128,8 +128,8 @@ def _copy2_ignoring_special_files(src, dest): logger.warning( "Ignoring special file error '%s' encountered copying %s to %s.", str(e), - path_to_display(src), - path_to_display(dest), + src, + dest, ) diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 200fe841814..952763d0c77 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -147,30 +147,6 @@ def rmtree_errorhandler(func, path, exc_info): raise -def path_to_display(path): - # type: (Optional[str]) -> Optional[str] - """ - Convert a bytes (or text) path to text (unicode in Python 2) for display - and logging purposes. - - This function should never error out. Also, this function is mainly needed - for Python 2 since in Python 3 str paths are already text. - """ - if path is None: - return None - if isinstance(path, str): - return path - # Otherwise, path is a bytes object (str in Python 2). - try: - display_path = path.decode(sys.getfilesystemencoding(), 'strict') - except UnicodeDecodeError: - # Include the full bytes to make troubleshooting easier, even though - # it may not be very human readable. - display_path = ascii(path) - - return display_path - - def display_path(path): # type: (str) -> str """Gives the display value for a given path, making it relative to cwd diff --git a/src/pip/_internal/utils/subprocess.py b/src/pip/_internal/utils/subprocess.py index 82bc3987ccf..87acd8e62e1 100644 --- a/src/pip/_internal/utils/subprocess.py +++ b/src/pip/_internal/utils/subprocess.py @@ -8,7 +8,7 @@ from pip._internal.exceptions import InstallationSubprocessError from pip._internal.utils.compat import console_to_str, str_to_display from pip._internal.utils.logging import subprocess_logger -from pip._internal.utils.misc import HiddenText, path_to_display +from pip._internal.utils.misc import HiddenText if TYPE_CHECKING: from typing import Any, Callable, Iterable, List, Mapping, Optional, Union @@ -82,7 +82,6 @@ def make_subprocess_output_error( # "UnicodeDecodeError: 'ascii' codec can't decode byte ..." in Python 2 # if either contains a non-ascii character. command_display = str_to_display(command, desc='command bytes') - cwd_display = path_to_display(cwd) # We know the joined output value ends in a newline. output = ''.join(lines) @@ -97,7 +96,7 @@ def make_subprocess_output_error( ).format( exit_status=exit_status, command_display=command_display, - cwd_display=cwd_display, + cwd_display=cwd, line_count=len(lines), output=output, divider=LOG_DIVIDER, diff --git a/tests/unit/test_utils.py b/tests/unit/test_utils.py index 8b4f9e79777..ebccd666011 100644 --- a/tests/unit/test_utils.py +++ b/tests/unit/test_utils.py @@ -38,7 +38,6 @@ normalize_path, normalize_version_info, parse_netloc, - path_to_display, redact_auth_from_url, redact_netloc, remove_auth_from_url, @@ -431,22 +430,6 @@ def test_rmtree_retries_for_3sec(tmpdir, monkeypatch): ) -@pytest.mark.parametrize('path, fs_encoding, expected', [ - (None, None, None), - # Test passing a text (unicode) string. - ('/path/déf', None, '/path/déf'), - # Test a bytes object with a non-ascii character. - ('/path/déf'.encode('utf-8'), 'utf-8', '/path/déf'), - # Test a bytes object with a character that can't be decoded. - ('/path/déf'.encode('utf-8'), 'ascii', "b'/path/d\\xc3\\xa9f'"), - ('/path/déf'.encode('utf-16'), 'utf-8', expected_byte_string), -]) -def test_path_to_display(monkeypatch, path, fs_encoding, expected): - monkeypatch.setattr(sys, 'getfilesystemencoding', lambda: fs_encoding) - actual = path_to_display(path) - assert actual == expected, f'actual: {actual!r}' - - class Test_normalize_path: # Technically, symlinks are possible on Windows, but you need a special # permission bit to create them, and Python 2 doesn't support it anyway, so From 7ffd0ca13e8cfca7e83fdcf8264ff011962f5c37 Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Fri, 1 Jan 2021 09:55:06 -0800 Subject: [PATCH 2972/3170] Complete typing of noxfile.py Allows removing a "mypy: disallow-untyped-defs=False" comment. To workaround a mypy bug, map(os.path.basename, distribution_files) was changed to use a generator expression. See: https://github.com/python/mypy/issues/9864 To verify correct usage of the nox API, it is now a dependency during pre-commit mypy runs. The mypy configuration "follow_imports = silent" allowed erroneous code to pass, so it has been removed. Now, all imports must be available during type. --- .pre-commit-config.yaml | 1 + ...7a-af01-49c9-9b72-2d3d4a89b11a.trivial.rst | 0 noxfile.py | 27 +++++++++++++------ setup.cfg | 1 - tools/automation/release/__init__.py | 8 +++--- 5 files changed, 25 insertions(+), 12 deletions(-) create mode 100644 news/adba2b7a-af01-49c9-9b72-2d3d4a89b11a.trivial.rst diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 164de60b05d..7b06692b237 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -74,6 +74,7 @@ repos: - id: mypy exclude: docs|tests args: ["--pretty"] + additional_dependencies: ['nox==2020.12.31'] - repo: https://github.com/pre-commit/pygrep-hooks rev: v1.7.0 diff --git a/news/adba2b7a-af01-49c9-9b72-2d3d4a89b11a.trivial.rst b/news/adba2b7a-af01-49c9-9b72-2d3d4a89b11a.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/noxfile.py b/noxfile.py index e89e73a8d0f..0dd382a24c4 100644 --- a/noxfile.py +++ b/noxfile.py @@ -1,14 +1,12 @@ """Automation using nox. """ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - import glob import os import shutil import sys from pathlib import Path +from typing import Iterator, List, Tuple import nox @@ -34,20 +32,22 @@ def run_with_protected_pip(session, *arguments): + # type: (nox.Session, *str) -> None """Do a session.run("pip", *arguments), using a "protected" pip. This invokes a wrapper script, that forwards calls to original virtualenv (stable) version, and not the code being tested. This ensures pip being used is not the code being tested. """ - env = {"VIRTUAL_ENV": session.virtualenv.location} + # https://github.com/theacodes/nox/pull/377 + env = {"VIRTUAL_ENV": session.virtualenv.location} # type: ignore command = ("python", LOCATIONS["protected-pip"]) + arguments - kwargs = {"env": env, "silent": True} - session.run(*command, **kwargs) + session.run(*command, env=env, silent=True) def should_update_common_wheels(): + # type: () -> bool # If the cache hasn't been created, create it. if not os.path.exists(LOCATIONS["common-wheels"]): return True @@ -72,6 +72,7 @@ def should_update_common_wheels(): # ----------------------------------------------------------------------------- @nox.session(python=["3.6", "3.7", "3.8", "3.9", "pypy3"]) def test(session): + # type: (nox.Session) -> None # Get the common wheels. if should_update_common_wheels(): run_with_protected_pip( @@ -88,7 +89,8 @@ def test(session): session.log(msg) # Build source distribution - sdist_dir = os.path.join(session.virtualenv.location, "sdist") + # https://github.com/theacodes/nox/pull/377 + sdist_dir = os.path.join(session.virtualenv.location, "sdist") # type: ignore if os.path.exists(sdist_dir): shutil.rmtree(sdist_dir, ignore_errors=True) session.run( @@ -117,10 +119,12 @@ def test(session): @nox.session def docs(session): + # type: (nox.Session) -> None session.install("-e", ".") session.install("-r", REQUIREMENTS["docs"]) def get_sphinx_build_command(kind): + # type: (str) -> List[str] # Having the conf.py in the docs/html is weird but needed because we # can not use a different configuration directory vs source directory # on RTD currently. So, we'll pass "-c docs/html" here. @@ -141,6 +145,7 @@ def get_sphinx_build_command(kind): @nox.session def lint(session): + # type: (nox.Session) -> None session.install("pre-commit") if session.posargs: @@ -154,6 +159,7 @@ def lint(session): @nox.session def vendoring(session): + # type: (nox.Session) -> None session.install("vendoring>=0.3.0") if "--upgrade" not in session.posargs: @@ -161,6 +167,7 @@ def vendoring(session): return def pinned_requirements(path): + # type: (Path) -> Iterator[Tuple[str, str]] for line in path.read_text().splitlines(): one, two = line.split("==", 1) name = one.strip() @@ -208,6 +215,7 @@ def pinned_requirements(path): # ----------------------------------------------------------------------------- @nox.session(name="prepare-release") def prepare_release(session): + # type: (nox.Session) -> None version = release.get_version_from_arguments(session) if not version: session.error("Usage: nox -s prepare-release -- <version>") @@ -243,6 +251,7 @@ def prepare_release(session): @nox.session(name="build-release") def build_release(session): + # type: (nox.Session) -> None version = release.get_version_from_arguments(session) if not version: session.error("Usage: nox -s build-release -- YY.N[.P]") @@ -274,6 +283,7 @@ def build_release(session): def build_dists(session): + # type: (nox.Session) -> List[str] """Return dists with valid metadata.""" session.log( "# Check if there's any Git-untracked files before building the wheel", @@ -302,6 +312,7 @@ def build_dists(session): @nox.session(name="upload-release") def upload_release(session): + # type: (nox.Session) -> None version = release.get_version_from_arguments(session) if not version: session.error("Usage: nox -s upload-release -- YY.N[.P]") @@ -320,7 +331,7 @@ def upload_release(session): f"Remove dist/ and run 'nox -s build-release -- {version}'" ) # Sanity check: Make sure the files are correctly named. - distfile_names = map(os.path.basename, distribution_files) + distfile_names = (os.path.basename(fn) for fn in distribution_files) expected_distribution_files = [ f"pip-{version}-py3-none-any.whl", f"pip-{version}.tar.gz", diff --git a/setup.cfg b/setup.cfg index a96614dc199..1d851d94929 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,7 +31,6 @@ per-file-ignores = tests/*: B011 [mypy] -follow_imports = silent ignore_missing_imports = True disallow_untyped_defs = True disallow_any_generics = True diff --git a/tools/automation/release/__init__.py b/tools/automation/release/__init__.py index 20775d5e21d..768bbcec6b1 100644 --- a/tools/automation/release/__init__.py +++ b/tools/automation/release/__init__.py @@ -26,7 +26,8 @@ def get_version_from_arguments(session: Session) -> Optional[str]: # We delegate to a script here, so that it can depend on packaging. session.install("packaging") cmd = [ - os.path.join(session.bin, "python"), + # https://github.com/theacodes/nox/pull/378 + os.path.join(session.bin, "python"), # type: ignore "tools/automation/release/check_version.py", version ] @@ -153,11 +154,12 @@ def workdir( """Temporarily chdir when entering CM and chdir back on exit.""" orig_dir = pathlib.Path.cwd() - nox_session.chdir(dir_path) + # https://github.com/theacodes/nox/pull/376 + nox_session.chdir(dir_path) # type: ignore try: yield dir_path finally: - nox_session.chdir(orig_dir) + nox_session.chdir(orig_dir) # type: ignore @contextlib.contextmanager From 4fa2466b7d8ca1aec005ecb1e6ea1ece384ceffd Mon Sep 17 00:00:00 2001 From: Denise Yu <deniseyu@github.com> Date: Fri, 19 Feb 2021 15:43:55 -0500 Subject: [PATCH 2973/3170] Update bug-report.yml Fix bug report template to work with new YAML config rules --- .github/ISSUE_TEMPLATE/bug-report.yml | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 1b62b864bb4..6b18de7bd1f 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -4,27 +4,30 @@ about: Something is not working correctly. title: "" labels: "S: needs triage, type: bug" issue_body: true # default: true, adds a classic WSYWIG textarea, if on -inputs: -- type: description +body: +- type: markdown attributes: value: | ⚠ If you're reporting an issue for `--use-feature=2020-resolver`, use the "Dependency resolver failures / errors" template instead. -- type: description +- type: markdown attributes: value: "**Environment**" - type: input attributes: label: pip version + validations: required: true - type: input attributes: label: Python version + validations: required: true - type: input attributes: label: OS + validations: required: true - type: textarea attributes: @@ -72,7 +75,7 @@ inputs: Read the [PSF Code of Conduct][CoC] first. [CoC]: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md - choices: + options: - label: I agree to follow the PSF Code of Conduct required: true ... From 0945809afc70cb591c07dfca4b4f8d23eef63e65 Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Fri, 19 Feb 2021 04:56:59 -0800 Subject: [PATCH 2974/3170] Remove typing.TYPE_CHECKING guards The typing module has been available since Python 3.5. Guarding the import has been unnecessary since dropping Python 2. Some guards remain to either: - Avoid circular imports - Importing objects that are also guarded by typing.TYPE_CHECKING - Avoid mypy_extensions dependency --- docs/docs_feedback_sphinxext.py | 8 +-- ...19-2486-45dc-b8dc-d2a5b9197ca4.trivial.rst | 0 src/pip/__init__.py | 6 +- src/pip/_internal/__init__.py | 5 +- src/pip/_internal/build_env.py | 6 +- src/pip/_internal/cache.py | 12 +--- src/pip/_internal/cli/autocompletion.py | 5 +- src/pip/_internal/cli/base_command.py | 12 +--- src/pip/_internal/cli/cmdoptions.py | 11 +--- src/pip/_internal/cli/command_context.py | 7 +-- src/pip/_internal/cli/main.py | 5 +- src/pip/_internal/cli/main_parser.py | 6 +- src/pip/_internal/cli/parser.py | 5 +- src/pip/_internal/cli/progress_bars.py | 5 +- src/pip/_internal/cli/req_command.py | 26 ++++---- src/pip/_internal/cli/spinners.py | 5 +- src/pip/_internal/commands/__init__.py | 8 +-- src/pip/_internal/commands/cache.py | 8 +-- src/pip/_internal/commands/check.py | 7 +-- src/pip/_internal/commands/completion.py | 7 +-- src/pip/_internal/commands/configuration.py | 16 ++--- src/pip/_internal/commands/debug.py | 12 ++-- src/pip/_internal/commands/download.py | 7 +-- src/pip/_internal/commands/freeze.py | 7 +-- src/pip/_internal/commands/hash.py | 7 +-- src/pip/_internal/commands/help.py | 7 +-- src/pip/_internal/commands/install.py | 24 +++---- src/pip/_internal/commands/list.py | 14 ++--- src/pip/_internal/commands/search.py | 7 +-- src/pip/_internal/commands/show.py | 7 +-- src/pip/_internal/commands/uninstall.py | 7 +-- src/pip/_internal/commands/wheel.py | 10 +-- src/pip/_internal/configuration.py | 9 +-- src/pip/_internal/distributions/__init__.py | 8 +-- src/pip/_internal/distributions/base.py | 11 ++-- src/pip/_internal/distributions/installed.py | 12 ++-- src/pip/_internal/distributions/sdist.py | 13 ++-- src/pip/_internal/distributions/wheel.py | 9 +-- src/pip/_internal/exceptions.py | 11 ++-- src/pip/_internal/index/collector.py | 40 +++++------- src/pip/_internal/index/package_finder.py | 31 ++++------ src/pip/_internal/locations.py | 8 +-- src/pip/_internal/main.py | 5 +- src/pip/_internal/metadata/__init__.py | 7 +-- src/pip/_internal/metadata/base.py | 9 +-- src/pip/_internal/metadata/pkg_resources.py | 8 +-- src/pip/_internal/models/candidate.py | 9 +-- src/pip/_internal/models/direct_url.py | 19 +++--- src/pip/_internal/models/format_control.py | 5 +- src/pip/_internal/models/link.py | 6 +- src/pip/_internal/models/search_scope.py | 6 +- src/pip/_internal/models/selection_prefs.py | 7 +-- src/pip/_internal/models/target_python.py | 9 +-- src/pip/_internal/models/wheel.py | 5 +- src/pip/_internal/network/auth.py | 15 ++--- src/pip/_internal/network/cache.py | 5 +- src/pip/_internal/network/download.py | 14 ++--- src/pip/_internal/network/lazy_wheel.py | 14 ++--- src/pip/_internal/network/session.py | 13 ++-- src/pip/_internal/network/utils.py | 5 +- src/pip/_internal/network/xmlrpc.py | 9 +-- .../_internal/operations/build/metadata.py | 9 +-- .../operations/build/metadata_legacy.py | 5 +- src/pip/_internal/operations/build/wheel.py | 9 +-- .../operations/build/wheel_legacy.py | 5 +- src/pip/_internal/operations/check.py | 24 +++---- src/pip/_internal/operations/freeze.py | 34 +++++----- .../operations/install/editable_legacy.py | 9 +-- .../_internal/operations/install/legacy.py | 11 +--- src/pip/_internal/operations/install/wheel.py | 55 ++++++++-------- src/pip/_internal/operations/prepare.py | 23 +++---- src/pip/_internal/pyproject.py | 5 +- src/pip/_internal/req/__init__.py | 5 +- src/pip/_internal/req/constructors.py | 9 +-- src/pip/_internal/req/req_file.py | 36 +++++------ src/pip/_internal/req/req_install.py | 17 ++--- src/pip/_internal/req/req_set.py | 9 +-- src/pip/_internal/req/req_tracker.py | 12 ++-- src/pip/_internal/req/req_uninstall.py | 18 +----- src/pip/_internal/resolution/base.py | 15 ++--- .../_internal/resolution/legacy/resolver.py | 30 ++++----- .../_internal/resolution/resolvelib/base.py | 16 ++--- .../resolution/resolvelib/candidates.py | 29 ++++----- .../resolution/resolvelib/factory.py | 62 ++++++++----------- .../resolution/resolvelib/found_candidates.py | 12 ++-- .../resolution/resolvelib/provider.py | 11 +--- .../resolution/resolvelib/reporter.py | 8 +-- .../resolution/resolvelib/requirements.py | 12 +--- .../resolution/resolvelib/resolver.py | 23 +++---- src/pip/_internal/self_outdated_check.py | 11 +--- src/pip/_internal/utils/appdirs.py | 5 +- src/pip/_internal/utils/compat.py | 6 +- src/pip/_internal/utils/compatibility_tags.py | 5 +- src/pip/_internal/utils/deprecation.py | 6 +- src/pip/_internal/utils/direct_url_helpers.py | 12 ++-- src/pip/_internal/utils/distutils_args.py | 6 +- src/pip/_internal/utils/encoding.py | 5 +- src/pip/_internal/utils/entrypoints.py | 5 +- src/pip/_internal/utils/filesystem.py | 7 +-- src/pip/_internal/utils/filetypes.py | 6 +- src/pip/_internal/utils/glibc.py | 5 +- src/pip/_internal/utils/hashes.py | 3 +- src/pip/_internal/utils/logging.py | 5 +- src/pip/_internal/utils/misc.py | 39 ++++++------ src/pip/_internal/utils/packaging.py | 11 +--- src/pip/_internal/utils/parallel.py | 13 ++-- src/pip/_internal/utils/pkg_resources.py | 5 +- src/pip/_internal/utils/setuptools_build.py | 5 +- src/pip/_internal/utils/subprocess.py | 7 +-- src/pip/_internal/utils/temp_dir.py | 10 +-- src/pip/_internal/utils/unpacking.py | 8 +-- src/pip/_internal/utils/urls.py | 5 +- src/pip/_internal/utils/virtualenv.py | 5 +- src/pip/_internal/utils/wheel.py | 12 +--- src/pip/_internal/vcs/__init__.py | 3 +- src/pip/_internal/vcs/bazaar.py | 19 +++--- src/pip/_internal/vcs/git.py | 16 ++--- src/pip/_internal/vcs/mercurial.py | 12 +--- src/pip/_internal/vcs/subversion.py | 26 ++++---- src/pip/_internal/vcs/versioncontrol.py | 40 +++++------- src/pip/_internal/wheel_builder.py | 16 ++--- tests/conftest.py | 11 +--- tests/lib/__init__.py | 9 +-- tests/lib/certs.py | 5 +- tests/lib/local_repos.py | 5 +- tests/lib/server.py | 29 ++++----- tests/lib/test_wheel.py | 5 +- tests/lib/wheel.py | 51 +++++++-------- tests/unit/test_utils_wheel.py | 5 +- 129 files changed, 539 insertions(+), 1034 deletions(-) create mode 100644 news/d0935419-2486-45dc-b8dc-d2a5b9197ca4.trivial.rst diff --git a/docs/docs_feedback_sphinxext.py b/docs/docs_feedback_sphinxext.py index a8ab94e5cbd..d0ff1f03da1 100644 --- a/docs/docs_feedback_sphinxext.py +++ b/docs/docs_feedback_sphinxext.py @@ -3,13 +3,9 @@ from __future__ import annotations from itertools import chain -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Dict, List, Union - - from sphinx.application import Sphinx +from typing import Dict, List, Union +from sphinx.application import Sphinx DEFAULT_DOC_LINES_THRESHOLD = 250 RST_INDENT = 4 diff --git a/news/d0935419-2486-45dc-b8dc-d2a5b9197ca4.trivial.rst b/news/d0935419-2486-45dc-b8dc-d2a5b9197ca4.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/__init__.py b/src/pip/__init__.py index 97b5e2f8839..ada5d647123 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -1,8 +1,4 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import List, Optional - +from typing import List, Optional __version__ = "21.1.dev0" diff --git a/src/pip/_internal/__init__.py b/src/pip/_internal/__init__.py index 23652fadc5e..41071cd8608 100755 --- a/src/pip/_internal/__init__.py +++ b/src/pip/_internal/__init__.py @@ -1,10 +1,7 @@ -from typing import TYPE_CHECKING +from typing import List, Optional import pip._internal.utils.inject_securetransport # noqa -if TYPE_CHECKING: - from typing import List, Optional - def main(args=None): # type: (Optional[List[str]]) -> int diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py index 9df467f309a..b1c877cfd9b 100644 --- a/src/pip/_internal/build_env.py +++ b/src/pip/_internal/build_env.py @@ -8,7 +8,8 @@ from collections import OrderedDict from distutils.sysconfig import get_python_lib from sysconfig import get_paths -from typing import TYPE_CHECKING +from types import TracebackType +from typing import TYPE_CHECKING, Iterable, List, Optional, Set, Tuple, Type from pip._vendor.pkg_resources import Requirement, VersionConflict, WorkingSet @@ -18,9 +19,6 @@ from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds if TYPE_CHECKING: - from types import TracebackType - from typing import Iterable, List, Optional, Set, Tuple, Type - from pip._internal.index.package_finder import PackageFinder logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/cache.py b/src/pip/_internal/cache.py index 83ea57ad4b9..7ef51b92e1d 100644 --- a/src/pip/_internal/cache.py +++ b/src/pip/_internal/cache.py @@ -5,24 +5,18 @@ import json import logging import os -from typing import TYPE_CHECKING +from typing import Any, Dict, List, Optional, Set -from pip._vendor.packaging.tags import interpreter_name, interpreter_version +from pip._vendor.packaging.tags import Tag, interpreter_name, interpreter_version from pip._vendor.packaging.utils import canonicalize_name from pip._internal.exceptions import InvalidWheelFilename +from pip._internal.models.format_control import FormatControl from pip._internal.models.link import Link from pip._internal.models.wheel import Wheel from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds from pip._internal.utils.urls import path_to_url -if TYPE_CHECKING: - from typing import Any, Dict, List, Optional, Set - - from pip._vendor.packaging.tags import Tag - - from pip._internal.models.format_control import FormatControl - logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/cli/autocompletion.py b/src/pip/_internal/cli/autocompletion.py index 3b4fc339e8a..3b5aa15deec 100644 --- a/src/pip/_internal/cli/autocompletion.py +++ b/src/pip/_internal/cli/autocompletion.py @@ -5,15 +5,12 @@ import os import sys from itertools import chain -from typing import TYPE_CHECKING +from typing import Any, Iterable, List, Optional from pip._internal.cli.main_parser import create_main_parser from pip._internal.commands import commands_dict, create_command from pip._internal.utils.misc import get_installed_distributions -if TYPE_CHECKING: - from typing import Any, Iterable, List, Optional - def autocomplete(): # type: () -> None diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index 380ba8a7163..87944e49577 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -6,7 +6,8 @@ import os import sys import traceback -from typing import TYPE_CHECKING +from optparse import Values +from typing import Any, List, Optional, Tuple from pip._internal.cli import cmdoptions from pip._internal.cli.command_context import CommandContextMixIn @@ -29,17 +30,10 @@ from pip._internal.utils.filesystem import check_path_owner from pip._internal.utils.logging import BrokenStdoutLoggingError, setup_logging from pip._internal.utils.misc import get_prog, normalize_path +from pip._internal.utils.temp_dir import TempDirectoryTypeRegistry as TempDirRegistry from pip._internal.utils.temp_dir import global_tempdir_manager, tempdir_registry from pip._internal.utils.virtualenv import running_under_virtualenv -if TYPE_CHECKING: - from optparse import Values - from typing import Any, List, Optional, Tuple - - from pip._internal.utils.temp_dir import ( - TempDirectoryTypeRegistry as TempDirRegistry, - ) - __all__ = ['Command'] logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index bcecf6748fb..0a7e28685f6 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -14,12 +14,13 @@ import textwrap import warnings from functools import partial -from optparse import SUPPRESS_HELP, Option, OptionGroup +from optparse import SUPPRESS_HELP, Option, OptionGroup, OptionParser, Values from textwrap import dedent -from typing import TYPE_CHECKING +from typing import Any, Callable, Dict, Optional, Tuple from pip._vendor.packaging.utils import canonicalize_name +from pip._internal.cli.parser import ConfigOptionParser from pip._internal.cli.progress_bars import BAR_TYPES from pip._internal.exceptions import CommandError from pip._internal.locations import USER_CACHE_DIR, get_src_prefix @@ -29,12 +30,6 @@ from pip._internal.utils.hashes import STRONG_HASHES from pip._internal.utils.misc import strtobool -if TYPE_CHECKING: - from optparse import OptionParser, Values - from typing import Any, Callable, Dict, Optional, Tuple - - from pip._internal.cli.parser import ConfigOptionParser - def raise_option_error(parser, option, msg): # type: (OptionParser, Option, str) -> None diff --git a/src/pip/_internal/cli/command_context.py b/src/pip/_internal/cli/command_context.py index b8eb9ecbb85..0f7c6afc4f9 100644 --- a/src/pip/_internal/cli/command_context.py +++ b/src/pip/_internal/cli/command_context.py @@ -1,10 +1,7 @@ from contextlib import ExitStack, contextmanager -from typing import TYPE_CHECKING +from typing import ContextManager, Iterator, TypeVar -if TYPE_CHECKING: - from typing import ContextManager, Iterator, TypeVar - - _T = TypeVar('_T', covariant=True) +_T = TypeVar('_T', covariant=True) class CommandContextMixIn: diff --git a/src/pip/_internal/cli/main.py b/src/pip/_internal/cli/main.py index 64210aeba6a..6f107a2e7c7 100644 --- a/src/pip/_internal/cli/main.py +++ b/src/pip/_internal/cli/main.py @@ -4,7 +4,7 @@ import logging import os import sys -from typing import TYPE_CHECKING +from typing import List, Optional from pip._internal.cli.autocompletion import autocomplete from pip._internal.cli.main_parser import parse_command @@ -12,9 +12,6 @@ from pip._internal.exceptions import PipError from pip._internal.utils import deprecation -if TYPE_CHECKING: - from typing import List, Optional - logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/cli/main_parser.py b/src/pip/_internal/cli/main_parser.py index 1ad8d7de3ca..97926975567 100644 --- a/src/pip/_internal/cli/main_parser.py +++ b/src/pip/_internal/cli/main_parser.py @@ -3,7 +3,7 @@ import os import sys -from typing import TYPE_CHECKING +from typing import List, Tuple from pip._internal.cli import cmdoptions from pip._internal.cli.parser import ConfigOptionParser, UpdatingDefaultsHelpFormatter @@ -11,10 +11,6 @@ from pip._internal.exceptions import CommandError from pip._internal.utils.misc import get_pip_version, get_prog -if TYPE_CHECKING: - from typing import List, Tuple - - __all__ = ["create_main_parser", "parse_command"] diff --git a/src/pip/_internal/cli/parser.py b/src/pip/_internal/cli/parser.py index 1c8ce1451e6..d3958727b22 100644 --- a/src/pip/_internal/cli/parser.py +++ b/src/pip/_internal/cli/parser.py @@ -9,15 +9,12 @@ import sys import textwrap from contextlib import suppress -from typing import TYPE_CHECKING +from typing import Any from pip._internal.cli.status_codes import UNKNOWN_ERROR from pip._internal.configuration import Configuration, ConfigurationError from pip._internal.utils.misc import redact_auth_from_url, strtobool -if TYPE_CHECKING: - from typing import Any - logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/cli/progress_bars.py b/src/pip/_internal/cli/progress_bars.py index 2d3847082a8..50cb74f5b7a 100644 --- a/src/pip/_internal/cli/progress_bars.py +++ b/src/pip/_internal/cli/progress_bars.py @@ -1,7 +1,7 @@ import itertools import sys from signal import SIGINT, default_int_handler, signal -from typing import TYPE_CHECKING +from typing import Any, Dict, List from pip._vendor.progress.bar import Bar, FillingCirclesBar, IncrementalBar from pip._vendor.progress.spinner import Spinner @@ -10,9 +10,6 @@ from pip._internal.utils.logging import get_indentation from pip._internal.utils.misc import format_size -if TYPE_CHECKING: - from typing import Any, Dict, List - try: from pip._vendor import colorama # Lots of different errors can come from this, including SystemError and diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 436cd8ddab5..3cb8ab0019e 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -8,8 +8,10 @@ import logging import os from functools import partial -from typing import TYPE_CHECKING +from optparse import Values +from typing import Any, List, Optional, Tuple +from pip._internal.cache import WheelCache from pip._internal.cli import cmdoptions from pip._internal.cli.base_command import Command from pip._internal.cli.command_context import CommandContextMixIn @@ -17,6 +19,7 @@ from pip._internal.index.collector import LinkCollector from pip._internal.index.package_finder import PackageFinder from pip._internal.models.selection_prefs import SelectionPreferences +from pip._internal.models.target_python import TargetPython from pip._internal.network.session import PipSession from pip._internal.operations.prepare import RequirementPreparer from pip._internal.req.constructors import ( @@ -26,20 +29,15 @@ install_req_from_req_string, ) from pip._internal.req.req_file import parse_requirements +from pip._internal.req.req_install import InstallRequirement +from pip._internal.req.req_tracker import RequirementTracker +from pip._internal.resolution.base import BaseResolver from pip._internal.self_outdated_check import pip_self_version_check -from pip._internal.utils.temp_dir import tempdir_kinds - -if TYPE_CHECKING: - from optparse import Values - from typing import Any, List, Optional, Tuple - - from pip._internal.cache import WheelCache - from pip._internal.models.target_python import TargetPython - from pip._internal.req.req_install import InstallRequirement - from pip._internal.req.req_tracker import RequirementTracker - from pip._internal.resolution.base import BaseResolver - from pip._internal.utils.temp_dir import TempDirectory, TempDirectoryTypeRegistry - +from pip._internal.utils.temp_dir import ( + TempDirectory, + TempDirectoryTypeRegistry, + tempdir_kinds, +) logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/cli/spinners.py b/src/pip/_internal/cli/spinners.py index d55434e6024..13984d3c494 100644 --- a/src/pip/_internal/cli/spinners.py +++ b/src/pip/_internal/cli/spinners.py @@ -3,16 +3,13 @@ import logging import sys import time -from typing import TYPE_CHECKING +from typing import IO, Iterator from pip._vendor.progress import HIDE_CURSOR, SHOW_CURSOR from pip._internal.utils.compat import WINDOWS from pip._internal.utils.logging import get_indentation -if TYPE_CHECKING: - from typing import IO, Iterator - logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/commands/__init__.py b/src/pip/_internal/commands/__init__.py index 3037e9da861..31c985fdca5 100644 --- a/src/pip/_internal/commands/__init__.py +++ b/src/pip/_internal/commands/__init__.py @@ -4,13 +4,9 @@ import importlib from collections import OrderedDict, namedtuple -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Optional - - from pip._internal.cli.base_command import Command +from typing import Any, Optional +from pip._internal.cli.base_command import Command CommandInfo = namedtuple('CommandInfo', 'module_path, class_name, summary') diff --git a/src/pip/_internal/commands/cache.py b/src/pip/_internal/commands/cache.py index 4f746dd980c..5155a5053e7 100644 --- a/src/pip/_internal/commands/cache.py +++ b/src/pip/_internal/commands/cache.py @@ -1,18 +1,14 @@ import logging import os import textwrap -from typing import TYPE_CHECKING +from optparse import Values +from typing import Any, List import pip._internal.utils.filesystem as filesystem from pip._internal.cli.base_command import Command from pip._internal.cli.status_codes import ERROR, SUCCESS from pip._internal.exceptions import CommandError, PipError -if TYPE_CHECKING: - from optparse import Values - from typing import Any, List - - logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/commands/check.py b/src/pip/_internal/commands/check.py index 5bc07cb7eb2..70aa5af2249 100644 --- a/src/pip/_internal/commands/check.py +++ b/src/pip/_internal/commands/check.py @@ -1,5 +1,6 @@ import logging -from typing import TYPE_CHECKING +from optparse import Values +from typing import Any, List from pip._internal.cli.base_command import Command from pip._internal.cli.status_codes import ERROR, SUCCESS @@ -11,10 +12,6 @@ logger = logging.getLogger(__name__) -if TYPE_CHECKING: - from optparse import Values - from typing import Any, List - class CheckCommand(Command): """Verify installed packages have compatible dependencies.""" diff --git a/src/pip/_internal/commands/completion.py b/src/pip/_internal/commands/completion.py index ca336075210..92cb7882770 100644 --- a/src/pip/_internal/commands/completion.py +++ b/src/pip/_internal/commands/completion.py @@ -1,15 +1,12 @@ import sys import textwrap -from typing import TYPE_CHECKING +from optparse import Values +from typing import List from pip._internal.cli.base_command import Command from pip._internal.cli.status_codes import SUCCESS from pip._internal.utils.misc import get_prog -if TYPE_CHECKING: - from optparse import Values - from typing import List - BASE_COMPLETION = """ # pip {shell} completion start{script}# pip {shell} completion end """ diff --git a/src/pip/_internal/commands/configuration.py b/src/pip/_internal/commands/configuration.py index 8cf034aafb7..e13f7142ca3 100644 --- a/src/pip/_internal/commands/configuration.py +++ b/src/pip/_internal/commands/configuration.py @@ -1,21 +1,21 @@ import logging import os import subprocess -from typing import TYPE_CHECKING +from optparse import Values +from typing import Any, List, Optional from pip._internal.cli.base_command import Command from pip._internal.cli.status_codes import ERROR, SUCCESS -from pip._internal.configuration import Configuration, get_configuration_files, kinds +from pip._internal.configuration import ( + Configuration, + Kind, + get_configuration_files, + kinds, +) from pip._internal.exceptions import PipError from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import get_prog, write_output -if TYPE_CHECKING: - from optparse import Values - from typing import Any, List, Optional - - from pip._internal.configuration import Kind - logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/commands/debug.py b/src/pip/_internal/commands/debug.py index 15c66fd531e..480c0444dd2 100644 --- a/src/pip/_internal/commands/debug.py +++ b/src/pip/_internal/commands/debug.py @@ -2,7 +2,9 @@ import logging import os import sys -from typing import TYPE_CHECKING +from optparse import Values +from types import ModuleType +from typing import Dict, List, Optional import pip._vendor from pip._vendor.certifi import where @@ -13,17 +15,11 @@ from pip._internal.cli.base_command import Command from pip._internal.cli.cmdoptions import make_target_python from pip._internal.cli.status_codes import SUCCESS +from pip._internal.configuration import Configuration from pip._internal.metadata import get_environment from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import get_pip_version -if TYPE_CHECKING: - from optparse import Values - from types import ModuleType - from typing import Dict, List, Optional - - from pip._internal.configuration import Configuration - logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/commands/download.py b/src/pip/_internal/commands/download.py index 212b75c7a67..19f8d6c0275 100644 --- a/src/pip/_internal/commands/download.py +++ b/src/pip/_internal/commands/download.py @@ -1,6 +1,7 @@ import logging import os -from typing import TYPE_CHECKING +from optparse import Values +from typing import List from pip._internal.cli import cmdoptions from pip._internal.cli.cmdoptions import make_target_python @@ -10,10 +11,6 @@ from pip._internal.utils.misc import ensure_dir, normalize_path, write_output from pip._internal.utils.temp_dir import TempDirectory -if TYPE_CHECKING: - from optparse import Values - from typing import List - logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/commands/freeze.py b/src/pip/_internal/commands/freeze.py index 6a3288d6be8..430d1018f04 100644 --- a/src/pip/_internal/commands/freeze.py +++ b/src/pip/_internal/commands/freeze.py @@ -1,5 +1,6 @@ import sys -from typing import TYPE_CHECKING +from optparse import Values +from typing import List from pip._internal.cli import cmdoptions from pip._internal.cli.base_command import Command @@ -10,10 +11,6 @@ DEV_PKGS = {'pip', 'setuptools', 'distribute', 'wheel'} -if TYPE_CHECKING: - from optparse import Values - from typing import List - class FreezeCommand(Command): """ diff --git a/src/pip/_internal/commands/hash.py b/src/pip/_internal/commands/hash.py index ff871b806ed..bca48dcc078 100644 --- a/src/pip/_internal/commands/hash.py +++ b/src/pip/_internal/commands/hash.py @@ -1,17 +1,14 @@ import hashlib import logging import sys -from typing import TYPE_CHECKING +from optparse import Values +from typing import List from pip._internal.cli.base_command import Command from pip._internal.cli.status_codes import ERROR, SUCCESS from pip._internal.utils.hashes import FAVORITE_HASH, STRONG_HASHES from pip._internal.utils.misc import read_chunks, write_output -if TYPE_CHECKING: - from optparse import Values - from typing import List - logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/commands/help.py b/src/pip/_internal/commands/help.py index 0e1dc81fffd..79d0eb49b1a 100644 --- a/src/pip/_internal/commands/help.py +++ b/src/pip/_internal/commands/help.py @@ -1,13 +1,10 @@ -from typing import TYPE_CHECKING +from optparse import Values +from typing import List from pip._internal.cli.base_command import Command from pip._internal.cli.status_codes import SUCCESS from pip._internal.exceptions import CommandError -if TYPE_CHECKING: - from optparse import Values - from typing import List - class HelpCommand(Command): """Show help for commands""" diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index ca90a86f1dd..78cd0b5cf68 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -4,8 +4,8 @@ import os import shutil import site -from optparse import SUPPRESS_HELP -from typing import TYPE_CHECKING +from optparse import SUPPRESS_HELP, Values +from typing import Iterable, List, Optional from pip._vendor.packaging.utils import canonicalize_name @@ -17,8 +17,10 @@ from pip._internal.exceptions import CommandError, InstallationError from pip._internal.locations import distutils_scheme from pip._internal.metadata import get_environment -from pip._internal.operations.check import check_install_conflicts +from pip._internal.models.format_control import FormatControl +from pip._internal.operations.check import ConflictDetails, check_install_conflicts from pip._internal.req import install_given_reqs +from pip._internal.req.req_install import InstallRequirement from pip._internal.req.req_tracker import get_requirement_tracker from pip._internal.utils.distutils_args import parse_distutils_args from pip._internal.utils.filesystem import test_writable_dir @@ -30,17 +32,11 @@ ) from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.virtualenv import virtualenv_no_global -from pip._internal.wheel_builder import build, should_build_for_install_command - -if TYPE_CHECKING: - from optparse import Values - from typing import Iterable, List, Optional - - from pip._internal.models.format_control import FormatControl - from pip._internal.operations.check import ConflictDetails - from pip._internal.req.req_install import InstallRequirement - from pip._internal.wheel_builder import BinaryAllowedPredicate - +from pip._internal.wheel_builder import ( + BinaryAllowedPredicate, + build, + should_build_for_install_command, +) logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/commands/list.py b/src/pip/_internal/commands/list.py index 4ee0d54b48f..dcf9432638a 100644 --- a/src/pip/_internal/commands/list.py +++ b/src/pip/_internal/commands/list.py @@ -1,6 +1,9 @@ import json import logging -from typing import TYPE_CHECKING +from optparse import Values +from typing import Iterator, List, Set, Tuple + +from pip._vendor.pkg_resources import Distribution from pip._internal.cli import cmdoptions from pip._internal.cli.req_command import IndexGroupCommand @@ -9,6 +12,7 @@ from pip._internal.index.collector import LinkCollector from pip._internal.index.package_finder import PackageFinder from pip._internal.models.selection_prefs import SelectionPreferences +from pip._internal.network.session import PipSession from pip._internal.utils.compat import stdlib_pkgs from pip._internal.utils.misc import ( dist_is_editable, @@ -19,14 +23,6 @@ from pip._internal.utils.packaging import get_installer from pip._internal.utils.parallel import map_multithread -if TYPE_CHECKING: - from optparse import Values - from typing import Iterator, List, Set, Tuple - - from pip._vendor.pkg_resources import Distribution - - from pip._internal.network.session import PipSession - logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/commands/search.py b/src/pip/_internal/commands/search.py index 1c7fa74ae90..6fcc9354fe8 100644 --- a/src/pip/_internal/commands/search.py +++ b/src/pip/_internal/commands/search.py @@ -3,7 +3,8 @@ import sys import textwrap from collections import OrderedDict -from typing import TYPE_CHECKING +from optparse import Values +from typing import TYPE_CHECKING, Dict, List, Optional from pip._vendor.packaging.version import parse as parse_version @@ -22,10 +23,8 @@ from pip._internal.utils.misc import write_output if TYPE_CHECKING: - from optparse import Values - from typing import Dict, List, Optional + from typing import TypedDict - from typing_extensions import TypedDict TransformedHit = TypedDict( 'TransformedHit', {'name': str, 'summary': str, 'versions': List[str]}, diff --git a/src/pip/_internal/commands/show.py b/src/pip/_internal/commands/show.py index e4d2502908f..24e855a80d8 100644 --- a/src/pip/_internal/commands/show.py +++ b/src/pip/_internal/commands/show.py @@ -1,7 +1,8 @@ import logging import os from email.parser import FeedParser -from typing import TYPE_CHECKING +from optparse import Values +from typing import Dict, Iterator, List from pip._vendor import pkg_resources from pip._vendor.packaging.utils import canonicalize_name @@ -10,10 +11,6 @@ from pip._internal.cli.status_codes import ERROR, SUCCESS from pip._internal.utils.misc import write_output -if TYPE_CHECKING: - from optparse import Values - from typing import Dict, Iterator, List - logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/commands/uninstall.py b/src/pip/_internal/commands/uninstall.py index 5084f32d202..d01fde9fe9d 100644 --- a/src/pip/_internal/commands/uninstall.py +++ b/src/pip/_internal/commands/uninstall.py @@ -1,4 +1,5 @@ -from typing import TYPE_CHECKING +from optparse import Values +from typing import List from pip._vendor.packaging.utils import canonicalize_name @@ -13,10 +14,6 @@ ) from pip._internal.utils.misc import protect_pip_from_modification_on_windows -if TYPE_CHECKING: - from optparse import Values - from typing import List - class UninstallCommand(Command, SessionCommandMixin): """ diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index d97ac00c41c..842988ba570 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -1,24 +1,20 @@ import logging import os import shutil -from typing import TYPE_CHECKING +from optparse import Values +from typing import List from pip._internal.cache import WheelCache from pip._internal.cli import cmdoptions from pip._internal.cli.req_command import RequirementCommand, with_cleanup from pip._internal.cli.status_codes import SUCCESS from pip._internal.exceptions import CommandError +from pip._internal.req.req_install import InstallRequirement from pip._internal.req.req_tracker import get_requirement_tracker from pip._internal.utils.misc import ensure_dir, normalize_path from pip._internal.utils.temp_dir import TempDirectory from pip._internal.wheel_builder import build, should_build_for_wheel_command -if TYPE_CHECKING: - from optparse import Values - from typing import List - - from pip._internal.req.req_install import InstallRequirement - logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/configuration.py b/src/pip/_internal/configuration.py index a52b83a110a..a4698ec1dda 100644 --- a/src/pip/_internal/configuration.py +++ b/src/pip/_internal/configuration.py @@ -16,7 +16,7 @@ import logging import os import sys -from typing import TYPE_CHECKING +from typing import Any, Dict, Iterable, List, NewType, Optional, Tuple from pip._internal.exceptions import ( ConfigurationError, @@ -26,11 +26,8 @@ from pip._internal.utils.compat import WINDOWS from pip._internal.utils.misc import ensure_dir, enum -if TYPE_CHECKING: - from typing import Any, Dict, Iterable, List, NewType, Optional, Tuple - - RawConfigParser = configparser.RawConfigParser # Shorthand - Kind = NewType("Kind", str) +RawConfigParser = configparser.RawConfigParser # Shorthand +Kind = NewType("Kind", str) CONFIG_BASENAME = 'pip.ini' if WINDOWS else 'pip.conf' ENV_NAMES_IGNORED = "version", "help" diff --git a/src/pip/_internal/distributions/__init__.py b/src/pip/_internal/distributions/__init__.py index d68f358e244..75bea848d2b 100644 --- a/src/pip/_internal/distributions/__init__.py +++ b/src/pip/_internal/distributions/__init__.py @@ -1,11 +1,7 @@ -from typing import TYPE_CHECKING - +from pip._internal.distributions.base import AbstractDistribution from pip._internal.distributions.sdist import SourceDistribution from pip._internal.distributions.wheel import WheelDistribution - -if TYPE_CHECKING: - from pip._internal.distributions.base import AbstractDistribution - from pip._internal.req.req_install import InstallRequirement +from pip._internal.req.req_install import InstallRequirement def make_distribution_for_install_requirement(install_req): diff --git a/src/pip/_internal/distributions/base.py b/src/pip/_internal/distributions/base.py index 50a21deff70..1798286edb0 100644 --- a/src/pip/_internal/distributions/base.py +++ b/src/pip/_internal/distributions/base.py @@ -1,13 +1,10 @@ import abc -from typing import TYPE_CHECKING +from typing import Optional -if TYPE_CHECKING: - from typing import Optional +from pip._vendor.pkg_resources import Distribution - from pip._vendor.pkg_resources import Distribution - - from pip._internal.index.package_finder import PackageFinder - from pip._internal.req import InstallRequirement +from pip._internal.index.package_finder import PackageFinder +from pip._internal.req import InstallRequirement class AbstractDistribution(metaclass=abc.ABCMeta): diff --git a/src/pip/_internal/distributions/installed.py b/src/pip/_internal/distributions/installed.py index 70f16499ba2..b19dfacb4db 100644 --- a/src/pip/_internal/distributions/installed.py +++ b/src/pip/_internal/distributions/installed.py @@ -1,13 +1,9 @@ -from typing import TYPE_CHECKING +from typing import Optional -from pip._internal.distributions.base import AbstractDistribution - -if TYPE_CHECKING: - from typing import Optional +from pip._vendor.pkg_resources import Distribution - from pip._vendor.pkg_resources import Distribution - - from pip._internal.index.package_finder import PackageFinder +from pip._internal.distributions.base import AbstractDistribution +from pip._internal.index.package_finder import PackageFinder class InstalledDistribution(AbstractDistribution): diff --git a/src/pip/_internal/distributions/sdist.py b/src/pip/_internal/distributions/sdist.py index 538bbfe8e74..28249076ceb 100644 --- a/src/pip/_internal/distributions/sdist.py +++ b/src/pip/_internal/distributions/sdist.py @@ -1,19 +1,14 @@ import logging -from typing import TYPE_CHECKING +from typing import Set, Tuple + +from pip._vendor.pkg_resources import Distribution from pip._internal.build_env import BuildEnvironment from pip._internal.distributions.base import AbstractDistribution from pip._internal.exceptions import InstallationError +from pip._internal.index.package_finder import PackageFinder from pip._internal.utils.subprocess import runner_with_spinner_message -if TYPE_CHECKING: - from typing import Set, Tuple - - from pip._vendor.pkg_resources import Distribution - - from pip._internal.index.package_finder import PackageFinder - - logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/distributions/wheel.py b/src/pip/_internal/distributions/wheel.py index bc8ab99c0d9..d0384797b46 100644 --- a/src/pip/_internal/distributions/wheel.py +++ b/src/pip/_internal/distributions/wheel.py @@ -1,14 +1,11 @@ -from typing import TYPE_CHECKING from zipfile import ZipFile +from pip._vendor.pkg_resources import Distribution + from pip._internal.distributions.base import AbstractDistribution +from pip._internal.index.package_finder import PackageFinder from pip._internal.utils.wheel import pkg_resources_distribution_for_wheel -if TYPE_CHECKING: - from pip._vendor.pkg_resources import Distribution - - from pip._internal.index.package_finder import PackageFinder - class WheelDistribution(AbstractDistribution): """Represents a wheel distribution. diff --git a/src/pip/_internal/exceptions.py b/src/pip/_internal/exceptions.py index 9af1961fff1..01ee4b76984 100644 --- a/src/pip/_internal/exceptions.py +++ b/src/pip/_internal/exceptions.py @@ -1,15 +1,14 @@ """Exceptions used throughout package""" +import configparser from itertools import chain, groupby, repeat -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Dict, List, Optional + +from pip._vendor.pkg_resources import Distribution +from pip._vendor.requests.models import Request, Response if TYPE_CHECKING: - import configparser from hashlib import _Hash - from typing import Dict, List, Optional - - from pip._vendor.pkg_resources import Distribution - from pip._vendor.requests.models import Request, Response from pip._internal.req.req_install import InstallRequirement diff --git a/src/pip/_internal/index/collector.py b/src/pip/_internal/index/collector.py index 1472c763e9f..3dd6c7df49d 100644 --- a/src/pip/_internal/index/collector.py +++ b/src/pip/_internal/index/collector.py @@ -11,46 +11,40 @@ import re import urllib.parse import urllib.request +import xml.etree.ElementTree from collections import OrderedDict -from typing import TYPE_CHECKING +from optparse import Values +from typing import ( + Callable, + Iterable, + List, + MutableMapping, + Optional, + Sequence, + Tuple, + Union, +) from pip._vendor import html5lib, requests from pip._vendor.distlib.compat import unescape +from pip._vendor.requests import Response from pip._vendor.requests.exceptions import RetryError, SSLError from pip._internal.exceptions import NetworkConnectionError from pip._internal.models.link import Link from pip._internal.models.search_scope import SearchScope +from pip._internal.network.session import PipSession from pip._internal.network.utils import raise_for_status from pip._internal.utils.filetypes import is_archive_file from pip._internal.utils.misc import pairwise, redact_auth_from_url from pip._internal.utils.urls import path_to_url, url_to_path from pip._internal.vcs import is_url, vcs -if TYPE_CHECKING: - import xml.etree.ElementTree - from optparse import Values - from typing import ( - Callable, - Iterable, - List, - MutableMapping, - Optional, - Sequence, - Tuple, - Union, - ) - - from pip._vendor.requests import Response - - from pip._internal.network.session import PipSession - - HTMLElement = xml.etree.ElementTree.Element - ResponseHeaders = MutableMapping[str, str] - - logger = logging.getLogger(__name__) +HTMLElement = xml.etree.ElementTree.Element +ResponseHeaders = MutableMapping[str, str] + def _match_vcs_scheme(url): # type: (str) -> Optional[str] diff --git a/src/pip/_internal/index/package_finder.py b/src/pip/_internal/index/package_finder.py index 562ec4e9522..b826690fa5f 100644 --- a/src/pip/_internal/index/package_finder.py +++ b/src/pip/_internal/index/package_finder.py @@ -6,10 +6,12 @@ import functools import logging import re -from typing import TYPE_CHECKING +from typing import FrozenSet, Iterable, List, Optional, Set, Tuple, Union from pip._vendor.packaging import specifiers +from pip._vendor.packaging.tags import Tag from pip._vendor.packaging.utils import canonicalize_name +from pip._vendor.packaging.version import _BaseVersion from pip._vendor.packaging.version import parse as parse_version from pip._internal.exceptions import ( @@ -18,42 +20,33 @@ InvalidWheelFilename, UnsupportedWheel, ) -from pip._internal.index.collector import parse_links +from pip._internal.index.collector import LinkCollector, parse_links from pip._internal.models.candidate import InstallationCandidate from pip._internal.models.format_control import FormatControl from pip._internal.models.link import Link +from pip._internal.models.search_scope import SearchScope from pip._internal.models.selection_prefs import SelectionPreferences from pip._internal.models.target_python import TargetPython from pip._internal.models.wheel import Wheel +from pip._internal.req import InstallRequirement from pip._internal.utils.filetypes import WHEEL_EXTENSION +from pip._internal.utils.hashes import Hashes from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import build_netloc from pip._internal.utils.packaging import check_requires_python from pip._internal.utils.unpacking import SUPPORTED_EXTENSIONS from pip._internal.utils.urls import url_to_path -if TYPE_CHECKING: - from typing import FrozenSet, Iterable, List, Optional, Set, Tuple, Union - - from pip._vendor.packaging.tags import Tag - from pip._vendor.packaging.version import _BaseVersion - - from pip._internal.index.collector import LinkCollector - from pip._internal.models.search_scope import SearchScope - from pip._internal.req import InstallRequirement - from pip._internal.utils.hashes import Hashes - - BuildTag = Union[Tuple[()], Tuple[int, str]] - CandidateSortingKey = ( - Tuple[int, int, int, _BaseVersion, BuildTag, Optional[int]] - ) - - __all__ = ['FormatControl', 'BestCandidateResult', 'PackageFinder'] logger = logging.getLogger(__name__) +BuildTag = Union[Tuple[()], Tuple[int, str]] +CandidateSortingKey = ( + Tuple[int, int, int, _BaseVersion, BuildTag, Optional[int]] +) + def _check_link_requires_python( link, # type: Link diff --git a/src/pip/_internal/locations.py b/src/pip/_internal/locations.py index 4e6b2810518..19c039eabf8 100644 --- a/src/pip/_internal/locations.py +++ b/src/pip/_internal/locations.py @@ -8,20 +8,16 @@ import site import sys import sysconfig +from distutils.cmd import Command as DistutilsCommand from distutils.command.install import SCHEME_KEYS from distutils.command.install import install as distutils_install_command -from typing import TYPE_CHECKING, cast +from typing import Dict, List, Optional, Union, cast from pip._internal.models.scheme import Scheme from pip._internal.utils import appdirs from pip._internal.utils.compat import WINDOWS from pip._internal.utils.virtualenv import running_under_virtualenv -if TYPE_CHECKING: - from distutils.cmd import Command as DistutilsCommand - from typing import Dict, List, Optional, Union - - # Application Directories USER_CACHE_DIR = appdirs.user_cache_dir("pip") diff --git a/src/pip/_internal/main.py b/src/pip/_internal/main.py index 647cde25f96..51eee1588d9 100644 --- a/src/pip/_internal/main.py +++ b/src/pip/_internal/main.py @@ -1,7 +1,4 @@ -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import List, Optional +from typing import List, Optional def main(args=None): diff --git a/src/pip/_internal/metadata/__init__.py b/src/pip/_internal/metadata/__init__.py index ecf2550e1dd..63335a19193 100644 --- a/src/pip/_internal/metadata/__init__.py +++ b/src/pip/_internal/metadata/__init__.py @@ -1,9 +1,6 @@ -from typing import TYPE_CHECKING +from typing import List, Optional -if TYPE_CHECKING: - from typing import List, Optional - - from .base import BaseDistribution, BaseEnvironment +from .base import BaseDistribution, BaseEnvironment def get_default_environment(): diff --git a/src/pip/_internal/metadata/base.py b/src/pip/_internal/metadata/base.py index 8ff57374c66..724b0c04494 100644 --- a/src/pip/_internal/metadata/base.py +++ b/src/pip/_internal/metadata/base.py @@ -1,11 +1,8 @@ -from typing import TYPE_CHECKING +from typing import Container, Iterator, List, Optional -from pip._internal.utils.misc import stdlib_pkgs # TODO: Move definition here. - -if TYPE_CHECKING: - from typing import Container, Iterator, List, Optional +from pip._vendor.packaging.version import _BaseVersion - from pip._vendor.packaging.version import _BaseVersion +from pip._internal.utils.misc import stdlib_pkgs # TODO: Move definition here. class BaseDistribution: diff --git a/src/pip/_internal/metadata/pkg_resources.py b/src/pip/_internal/metadata/pkg_resources.py index ccc3cc57b9d..d2fb29e2e9a 100644 --- a/src/pip/_internal/metadata/pkg_resources.py +++ b/src/pip/_internal/metadata/pkg_resources.py @@ -1,8 +1,9 @@ import zipfile -from typing import TYPE_CHECKING +from typing import Iterator, List, Optional from pip._vendor import pkg_resources from pip._vendor.packaging.utils import canonicalize_name +from pip._vendor.packaging.version import _BaseVersion from pip._internal.utils import misc # TODO: Move definition here. from pip._internal.utils.packaging import get_installer @@ -10,11 +11,6 @@ from .base import BaseDistribution, BaseEnvironment -if TYPE_CHECKING: - from typing import Iterator, List, Optional - - from pip._vendor.packaging.version import _BaseVersion - class Distribution(BaseDistribution): def __init__(self, dist): diff --git a/src/pip/_internal/models/candidate.py b/src/pip/_internal/models/candidate.py index d4eade13e60..10a144620ee 100644 --- a/src/pip/_internal/models/candidate.py +++ b/src/pip/_internal/models/candidate.py @@ -1,14 +1,9 @@ -from typing import TYPE_CHECKING - +from pip._vendor.packaging.version import _BaseVersion from pip._vendor.packaging.version import parse as parse_version +from pip._internal.models.link import Link from pip._internal.utils.models import KeyBasedCompareMixin -if TYPE_CHECKING: - from pip._vendor.packaging.version import _BaseVersion - - from pip._internal.models.link import Link - class InstallationCandidate(KeyBasedCompareMixin): """Represents a potential "candidate" for installation. diff --git a/src/pip/_internal/models/direct_url.py b/src/pip/_internal/models/direct_url.py index d10a8044d24..345dbaf109a 100644 --- a/src/pip/_internal/models/direct_url.py +++ b/src/pip/_internal/models/direct_url.py @@ -2,16 +2,7 @@ import json import re import urllib.parse -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Any, Dict, Iterable, Optional, Type, TypeVar, Union - - T = TypeVar("T") - - -DIRECT_URL_METADATA_NAME = "direct_url.json" -ENV_VAR_RE = re.compile(r"^\$\{[A-Za-z0-9-_]+\}(:\$\{[A-Za-z0-9-_]+\})?$") +from typing import Any, Dict, Iterable, Optional, Type, TypeVar, Union __all__ = [ "DirectUrl", @@ -21,6 +12,11 @@ "VcsInfo", ] +T = TypeVar("T") + +DIRECT_URL_METADATA_NAME = "direct_url.json" +ENV_VAR_RE = re.compile(r"^\$\{[A-Za-z0-9-_]+\}(:\$\{[A-Za-z0-9-_]+\})?$") + class DirectUrlValidationError(Exception): pass @@ -155,8 +151,7 @@ def _to_dict(self): return _filter_none(editable=self.editable or None) -if TYPE_CHECKING: - InfoType = Union[ArchiveInfo, DirInfo, VcsInfo] +InfoType = Union[ArchiveInfo, DirInfo, VcsInfo] class DirectUrl: diff --git a/src/pip/_internal/models/format_control.py b/src/pip/_internal/models/format_control.py index 73d045728a6..cf262af2918 100644 --- a/src/pip/_internal/models/format_control.py +++ b/src/pip/_internal/models/format_control.py @@ -1,12 +1,9 @@ -from typing import TYPE_CHECKING +from typing import FrozenSet, Optional, Set from pip._vendor.packaging.utils import canonicalize_name from pip._internal.exceptions import CommandError -if TYPE_CHECKING: - from typing import FrozenSet, Optional, Set - class FormatControl: """Helper for managing formats from which a package can be installed. diff --git a/src/pip/_internal/models/link.py b/src/pip/_internal/models/link.py index 757f43b9071..d79c66a90c2 100644 --- a/src/pip/_internal/models/link.py +++ b/src/pip/_internal/models/link.py @@ -2,9 +2,10 @@ import posixpath import re import urllib.parse -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Optional, Tuple, Union from pip._internal.utils.filetypes import WHEEL_EXTENSION +from pip._internal.utils.hashes import Hashes from pip._internal.utils.misc import ( redact_auth_from_url, split_auth_from_netloc, @@ -14,10 +15,7 @@ from pip._internal.utils.urls import path_to_url, url_to_path if TYPE_CHECKING: - from typing import Optional, Tuple, Union - from pip._internal.index.collector import HTMLPage - from pip._internal.utils.hashes import Hashes class Link(KeyBasedCompareMixin): diff --git a/src/pip/_internal/models/search_scope.py b/src/pip/_internal/models/search_scope.py index 21907aab740..a3f0a5c0f87 100644 --- a/src/pip/_internal/models/search_scope.py +++ b/src/pip/_internal/models/search_scope.py @@ -3,7 +3,7 @@ import os import posixpath import urllib.parse -from typing import TYPE_CHECKING +from typing import List from pip._vendor.packaging.utils import canonicalize_name @@ -11,10 +11,6 @@ from pip._internal.utils.compat import has_tls from pip._internal.utils.misc import normalize_path, redact_auth_from_url -if TYPE_CHECKING: - from typing import List - - logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/models/selection_prefs.py b/src/pip/_internal/models/selection_prefs.py index 65750ebb2bc..edc1cf79955 100644 --- a/src/pip/_internal/models/selection_prefs.py +++ b/src/pip/_internal/models/selection_prefs.py @@ -1,9 +1,6 @@ -from typing import TYPE_CHECKING +from typing import Optional -if TYPE_CHECKING: - from typing import Optional - - from pip._internal.models.format_control import FormatControl +from pip._internal.models.format_control import FormatControl class SelectionPreferences: diff --git a/src/pip/_internal/models/target_python.py b/src/pip/_internal/models/target_python.py index 742c089eb13..b91e349f566 100644 --- a/src/pip/_internal/models/target_python.py +++ b/src/pip/_internal/models/target_python.py @@ -1,14 +1,11 @@ import sys -from typing import TYPE_CHECKING +from typing import List, Optional, Tuple + +from pip._vendor.packaging.tags import Tag from pip._internal.utils.compatibility_tags import get_supported, version_info_to_nodot from pip._internal.utils.misc import normalize_version_info -if TYPE_CHECKING: - from typing import List, Optional, Tuple - - from pip._vendor.packaging.tags import Tag - class TargetPython: diff --git a/src/pip/_internal/models/wheel.py b/src/pip/_internal/models/wheel.py index 484596aaa32..708bff33067 100644 --- a/src/pip/_internal/models/wheel.py +++ b/src/pip/_internal/models/wheel.py @@ -2,15 +2,12 @@ name that have meaning. """ import re -from typing import TYPE_CHECKING +from typing import List from pip._vendor.packaging.tags import Tag from pip._internal.exceptions import InvalidWheelFilename -if TYPE_CHECKING: - from typing import List - class Wheel: """A wheel file""" diff --git a/src/pip/_internal/network/auth.py b/src/pip/_internal/network/auth.py index 315aca69994..cad22a02ce3 100644 --- a/src/pip/_internal/network/auth.py +++ b/src/pip/_internal/network/auth.py @@ -6,9 +6,10 @@ import logging import urllib.parse -from typing import TYPE_CHECKING +from typing import Any, Dict, List, Optional, Tuple from pip._vendor.requests.auth import AuthBase, HTTPBasicAuth +from pip._vendor.requests.models import Request, Response from pip._vendor.requests.utils import get_netrc_auth from pip._internal.utils.misc import ( @@ -18,18 +19,12 @@ remove_auth_from_url, split_auth_netloc_from_url, ) - -if TYPE_CHECKING: - from typing import Any, Dict, List, Optional, Tuple - - from pip._vendor.requests.models import Request, Response - - from pip._internal.vcs.versioncontrol import AuthInfo - - Credentials = Tuple[str, str, str] +from pip._internal.vcs.versioncontrol import AuthInfo logger = logging.getLogger(__name__) +Credentials = Tuple[str, str, str] + try: import keyring except ImportError: diff --git a/src/pip/_internal/network/cache.py b/src/pip/_internal/network/cache.py index cdec4949c36..ce08932a57f 100644 --- a/src/pip/_internal/network/cache.py +++ b/src/pip/_internal/network/cache.py @@ -3,7 +3,7 @@ import os from contextlib import contextmanager -from typing import TYPE_CHECKING +from typing import Iterator, Optional from pip._vendor.cachecontrol.cache import BaseCache from pip._vendor.cachecontrol.caches import FileCache @@ -12,9 +12,6 @@ from pip._internal.utils.filesystem import adjacent_tmp_file, replace from pip._internal.utils.misc import ensure_dir -if TYPE_CHECKING: - from typing import Iterator, Optional - def is_from_cache(response): # type: (Response) -> bool diff --git a/src/pip/_internal/network/download.py b/src/pip/_internal/network/download.py index 7f02fa6f659..1897d99a13f 100644 --- a/src/pip/_internal/network/download.py +++ b/src/pip/_internal/network/download.py @@ -4,25 +4,19 @@ import logging import mimetypes import os -from typing import TYPE_CHECKING +from typing import Iterable, Optional, Tuple -from pip._vendor.requests.models import CONTENT_CHUNK_SIZE +from pip._vendor.requests.models import CONTENT_CHUNK_SIZE, Response from pip._internal.cli.progress_bars import DownloadProgressProvider from pip._internal.exceptions import NetworkConnectionError from pip._internal.models.index import PyPI +from pip._internal.models.link import Link from pip._internal.network.cache import is_from_cache +from pip._internal.network.session import PipSession from pip._internal.network.utils import HEADERS, raise_for_status, response_chunks from pip._internal.utils.misc import format_size, redact_auth_from_url, splitext -if TYPE_CHECKING: - from typing import Iterable, Optional, Tuple - - from pip._vendor.requests.models import Response - - from pip._internal.models.link import Link - from pip._internal.network.session import PipSession - logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/network/lazy_wheel.py b/src/pip/_internal/network/lazy_wheel.py index fd7ab7f03a3..b877d3b7a67 100644 --- a/src/pip/_internal/network/lazy_wheel.py +++ b/src/pip/_internal/network/lazy_wheel.py @@ -5,22 +5,16 @@ from bisect import bisect_left, bisect_right from contextlib import contextmanager from tempfile import NamedTemporaryFile -from typing import TYPE_CHECKING +from typing import Any, Dict, Iterator, List, Optional, Tuple from zipfile import BadZipfile, ZipFile -from pip._vendor.requests.models import CONTENT_CHUNK_SIZE +from pip._vendor.pkg_resources import Distribution +from pip._vendor.requests.models import CONTENT_CHUNK_SIZE, Response +from pip._internal.network.session import PipSession from pip._internal.network.utils import HEADERS, raise_for_status, response_chunks from pip._internal.utils.wheel import pkg_resources_distribution_for_wheel -if TYPE_CHECKING: - from typing import Any, Dict, Iterator, List, Optional, Tuple - - from pip._vendor.pkg_resources import Distribution - from pip._vendor.requests.models import Response - - from pip._internal.network.session import PipSession - class HTTPRangeRequestUnsupported(Exception): pass diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py index 03c3725abd1..423922a007d 100644 --- a/src/pip/_internal/network/session.py +++ b/src/pip/_internal/network/session.py @@ -15,7 +15,7 @@ import sys import urllib.parse import warnings -from typing import TYPE_CHECKING +from typing import Any, Iterator, List, Optional, Sequence, Tuple, Union from pip._vendor import requests, urllib3 from pip._vendor.cachecontrol import CacheControlAdapter @@ -26,6 +26,7 @@ from pip import __version__ from pip._internal.metadata import get_default_environment +from pip._internal.models.link import Link from pip._internal.network.auth import MultiDomainBasicAuth from pip._internal.network.cache import SafeFileCache @@ -35,16 +36,10 @@ from pip._internal.utils.misc import build_url_from_netloc, parse_netloc from pip._internal.utils.urls import url_to_path -if TYPE_CHECKING: - from typing import Any, Iterator, List, Optional, Sequence, Tuple, Union - - from pip._internal.models.link import Link - - SecureOrigin = Tuple[str, str, Optional[Union[int, str]]] - - logger = logging.getLogger(__name__) +SecureOrigin = Tuple[str, str, Optional[Union[int, str]]] + # Ignore warning raised when using --trusted-host. warnings.filterwarnings("ignore", category=InsecureRequestWarning) diff --git a/src/pip/_internal/network/utils.py b/src/pip/_internal/network/utils.py index 47ece6d13dd..d29c7c0769d 100644 --- a/src/pip/_internal/network/utils.py +++ b/src/pip/_internal/network/utils.py @@ -1,12 +1,9 @@ -from typing import TYPE_CHECKING +from typing import Dict, Iterator from pip._vendor.requests.models import CONTENT_CHUNK_SIZE, Response from pip._internal.exceptions import NetworkConnectionError -if TYPE_CHECKING: - from typing import Dict, Iterator - # The following comments and HTTP headers were originally added by # Donald Stufft in git commit 22c562429a61bb77172039e480873fb239dd8c03. # diff --git a/src/pip/_internal/network/xmlrpc.py b/src/pip/_internal/network/xmlrpc.py index d4aa71c0929..c9f3c5db819 100644 --- a/src/pip/_internal/network/xmlrpc.py +++ b/src/pip/_internal/network/xmlrpc.py @@ -3,21 +3,16 @@ import logging import urllib.parse -from typing import TYPE_CHECKING +from typing import Dict # 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.exceptions import NetworkConnectionError +from pip._internal.network.session import PipSession from pip._internal.network.utils import raise_for_status -if TYPE_CHECKING: - from typing import Dict - - from pip._internal.network.session import PipSession - - logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/operations/build/metadata.py b/src/pip/_internal/operations/build/metadata.py index 21f86c8dc88..1c826835b49 100644 --- a/src/pip/_internal/operations/build/metadata.py +++ b/src/pip/_internal/operations/build/metadata.py @@ -2,16 +2,13 @@ """ import os -from typing import TYPE_CHECKING +from pip._vendor.pep517.wrappers import Pep517HookCaller + +from pip._internal.build_env import BuildEnvironment from pip._internal.utils.subprocess import runner_with_spinner_message from pip._internal.utils.temp_dir import TempDirectory -if TYPE_CHECKING: - from pip._vendor.pep517.wrappers import Pep517HookCaller - - from pip._internal.build_env import BuildEnvironment - def generate_metadata(build_env, backend): # type: (BuildEnvironment, Pep517HookCaller) -> str diff --git a/src/pip/_internal/operations/build/metadata_legacy.py b/src/pip/_internal/operations/build/metadata_legacy.py index a113a4a4e87..f46538a07f4 100644 --- a/src/pip/_internal/operations/build/metadata_legacy.py +++ b/src/pip/_internal/operations/build/metadata_legacy.py @@ -3,16 +3,13 @@ import logging import os -from typing import TYPE_CHECKING +from pip._internal.build_env import BuildEnvironment from pip._internal.exceptions import InstallationError from pip._internal.utils.setuptools_build import make_setuptools_egg_info_args from pip._internal.utils.subprocess import call_subprocess from pip._internal.utils.temp_dir import TempDirectory -if TYPE_CHECKING: - from pip._internal.build_env import BuildEnvironment - logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/operations/build/wheel.py b/src/pip/_internal/operations/build/wheel.py index 9af53caa2f1..83fac3b3187 100644 --- a/src/pip/_internal/operations/build/wheel.py +++ b/src/pip/_internal/operations/build/wheel.py @@ -1,13 +1,10 @@ import logging import os -from typing import TYPE_CHECKING +from typing import List, Optional -from pip._internal.utils.subprocess import runner_with_spinner_message - -if TYPE_CHECKING: - from typing import List, Optional +from pip._vendor.pep517.wrappers import Pep517HookCaller - from pip._vendor.pep517.wrappers import Pep517HookCaller +from pip._internal.utils.subprocess import runner_with_spinner_message logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/operations/build/wheel_legacy.py b/src/pip/_internal/operations/build/wheel_legacy.py index 0a4a68d20bc..755c3bc83a2 100644 --- a/src/pip/_internal/operations/build/wheel_legacy.py +++ b/src/pip/_internal/operations/build/wheel_legacy.py @@ -1,6 +1,6 @@ import logging import os.path -from typing import TYPE_CHECKING +from typing import List, Optional from pip._internal.cli.spinners import open_spinner from pip._internal.utils.setuptools_build import make_setuptools_bdist_wheel_args @@ -10,9 +10,6 @@ format_command_args, ) -if TYPE_CHECKING: - from typing import List, Optional - logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/operations/check.py b/src/pip/_internal/operations/check.py index a3189061c71..224633561aa 100644 --- a/src/pip/_internal/operations/check.py +++ b/src/pip/_internal/operations/check.py @@ -3,30 +3,26 @@ import logging from collections import namedtuple -from typing import TYPE_CHECKING +from typing import Any, Callable, Dict, List, Optional, Set, Tuple from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.pkg_resources import RequirementParseError from pip._internal.distributions import make_distribution_for_install_requirement +from pip._internal.req.req_install import InstallRequirement from pip._internal.utils.misc import get_installed_distributions logger = logging.getLogger(__name__) -if TYPE_CHECKING: - from typing import Any, Callable, Dict, List, Optional, Set, Tuple +# Shorthands +PackageSet = Dict[str, 'PackageDetails'] +Missing = Tuple[str, Any] +Conflicting = Tuple[str, str, Any] - from pip._internal.req.req_install import InstallRequirement - - # 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] - ConflictDetails = Tuple[PackageSet, CheckResult] +MissingDict = Dict[str, List[Missing]] +ConflictingDict = Dict[str, List[Conflicting]] +CheckResult = Tuple[MissingDict, ConflictingDict] +ConflictDetails = Tuple[PackageSet, CheckResult] PackageDetails = namedtuple('PackageDetails', ['version', 'requires']) diff --git a/src/pip/_internal/operations/freeze.py b/src/pip/_internal/operations/freeze.py index b082caa8ab2..f34a9d4be7e 100644 --- a/src/pip/_internal/operations/freeze.py +++ b/src/pip/_internal/operations/freeze.py @@ -1,10 +1,20 @@ import collections import logging import os -from typing import TYPE_CHECKING +from typing import ( + Container, + Dict, + Iterable, + Iterator, + List, + Optional, + Set, + Tuple, + Union, +) from pip._vendor.packaging.utils import canonicalize_name -from pip._vendor.pkg_resources import RequirementParseError +from pip._vendor.pkg_resources import Distribution, Requirement, RequirementParseError from pip._internal.exceptions import BadCommand, InstallationError from pip._internal.req.constructors import ( @@ -18,26 +28,10 @@ ) from pip._internal.utils.misc import dist_is_editable, get_installed_distributions -if TYPE_CHECKING: - from typing import ( - Container, - Dict, - Iterable, - Iterator, - List, - Optional, - Set, - Tuple, - Union, - ) - - from pip._vendor.pkg_resources import Distribution, Requirement - - RequirementInfo = Tuple[Optional[Union[str, Requirement]], bool, List[str]] - - logger = logging.getLogger(__name__) +RequirementInfo = Tuple[Optional[Union[str, Requirement]], bool, List[str]] + def freeze( requirement=None, # type: Optional[List[str]] diff --git a/src/pip/_internal/operations/install/editable_legacy.py b/src/pip/_internal/operations/install/editable_legacy.py index f2ec1f882be..6882c475cac 100644 --- a/src/pip/_internal/operations/install/editable_legacy.py +++ b/src/pip/_internal/operations/install/editable_legacy.py @@ -1,18 +1,13 @@ """Legacy editable installation process, i.e. `setup.py develop`. """ import logging -from typing import TYPE_CHECKING +from typing import List, Optional, Sequence +from pip._internal.build_env import BuildEnvironment from pip._internal.utils.logging import indent_log from pip._internal.utils.setuptools_build import make_setuptools_develop_args from pip._internal.utils.subprocess import call_subprocess -if TYPE_CHECKING: - from typing import List, Optional, Sequence - - from pip._internal.build_env import BuildEnvironment - - logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/operations/install/legacy.py b/src/pip/_internal/operations/install/legacy.py index a70be0d22b9..41d0c1f9d0e 100644 --- a/src/pip/_internal/operations/install/legacy.py +++ b/src/pip/_internal/operations/install/legacy.py @@ -5,22 +5,17 @@ import os import sys from distutils.util import change_root -from typing import TYPE_CHECKING +from typing import List, Optional, Sequence +from pip._internal.build_env import BuildEnvironment from pip._internal.exceptions import InstallationError +from pip._internal.models.scheme import Scheme from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import ensure_dir from pip._internal.utils.setuptools_build import make_setuptools_install_args from pip._internal.utils.subprocess import runner_with_spinner_message from pip._internal.utils.temp_dir import TempDirectory -if TYPE_CHECKING: - from typing import List, Optional, Sequence - - from pip._internal.build_env import BuildEnvironment - from pip._internal.models.scheme import Scheme - - logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/operations/install/wheel.py b/src/pip/_internal/operations/install/wheel.py index db72f711192..10e5b15fd5c 100644 --- a/src/pip/_internal/operations/install/wheel.py +++ b/src/pip/_internal/operations/install/wheel.py @@ -13,19 +13,38 @@ import sys import warnings from base64 import urlsafe_b64encode +from email.message import Message from itertools import chain, filterfalse, starmap -from typing import TYPE_CHECKING, cast -from zipfile import ZipFile +from typing import ( + IO, + TYPE_CHECKING, + Any, + BinaryIO, + Callable, + Dict, + Iterable, + Iterator, + List, + NewType, + Optional, + Sequence, + Set, + Tuple, + Union, + cast, +) +from zipfile import ZipFile, ZipInfo from pip._vendor import pkg_resources from pip._vendor.distlib.scripts import ScriptMaker from pip._vendor.distlib.util import get_export_entry +from pip._vendor.pkg_resources import Distribution from pip._vendor.six import ensure_str, ensure_text, reraise from pip._internal.exceptions import InstallationError from pip._internal.locations import get_major_minor_version from pip._internal.models.direct_url import DIRECT_URL_METADATA_NAME, DirectUrl -from pip._internal.models.scheme import SCHEME_KEYS +from pip._internal.models.scheme import SCHEME_KEYS, Scheme from pip._internal.utils.filesystem import adjacent_tmp_file, replace from pip._internal.utils.misc import captured_stdout, ensure_dir, hash_file, partition from pip._internal.utils.unpacking import ( @@ -37,32 +56,7 @@ from pip._internal.utils.wheel import parse_wheel, pkg_resources_distribution_for_wheel if TYPE_CHECKING: - from email.message import Message - from typing import ( - IO, - Any, - BinaryIO, - Callable, - Dict, - Iterable, - Iterator, - List, - NewType, - Optional, - Protocol, - Sequence, - Set, - Tuple, - Union, - ) - from zipfile import ZipInfo - - from pip._vendor.pkg_resources import Distribution - - from pip._internal.models.scheme import Scheme - - RecordPath = NewType('RecordPath', str) - InstalledCSVRow = Tuple[RecordPath, str, Union[int, str]] + from typing import Protocol class File(Protocol): src_record_path = None # type: RecordPath @@ -76,6 +70,9 @@ def save(self): logger = logging.getLogger(__name__) +RecordPath = NewType('RecordPath', str) +InstalledCSVRow = Tuple[RecordPath, str, Union[int, str]] + def rehash(path, blocksize=1 << 20): # type: (str, int) -> Tuple[str, str] diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index e09fa7fb08f..e267d44d4dc 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -8,9 +8,10 @@ import mimetypes import os import shutil -from typing import TYPE_CHECKING +from typing import Dict, Iterable, List, Optional, Tuple from pip._vendor.packaging.utils import canonicalize_name +from pip._vendor.pkg_resources import Distribution from pip._internal.distributions import make_distribution_for_install_requirement from pip._internal.distributions.installed import InstalledDistribution @@ -23,33 +24,25 @@ PreviousBuildDirError, VcsHashUnsupported, ) +from pip._internal.index.package_finder import PackageFinder +from pip._internal.models.link import Link from pip._internal.models.wheel import Wheel from pip._internal.network.download import BatchDownloader, Downloader from pip._internal.network.lazy_wheel import ( HTTPRangeRequestUnsupported, dist_from_wheel_url, ) +from pip._internal.network.session import PipSession +from pip._internal.req.req_install import InstallRequirement +from pip._internal.req.req_tracker import RequirementTracker from pip._internal.utils.filesystem import copy2_fixed -from pip._internal.utils.hashes import MissingHashes +from pip._internal.utils.hashes import Hashes, MissingHashes from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import display_path, hide_url, path_to_display, rmtree from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.unpacking import unpack_file from pip._internal.vcs import vcs -if TYPE_CHECKING: - from typing import Dict, Iterable, List, Optional, Tuple - - from pip._vendor.pkg_resources import Distribution - - from pip._internal.index.package_finder import PackageFinder - from pip._internal.models.link import Link - from pip._internal.network.session import PipSession - from pip._internal.req.req_install import InstallRequirement - from pip._internal.req.req_tracker import RequirementTracker - from pip._internal.utils.hashes import Hashes - - logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/pyproject.py b/src/pip/_internal/pyproject.py index fdd289b5631..9016d355f87 100644 --- a/src/pip/_internal/pyproject.py +++ b/src/pip/_internal/pyproject.py @@ -1,15 +1,12 @@ import os from collections import namedtuple -from typing import TYPE_CHECKING +from typing import Any, List, Optional from pip._vendor import toml from pip._vendor.packaging.requirements import InvalidRequirement, Requirement from pip._internal.exceptions import InstallationError -if TYPE_CHECKING: - from typing import Any, List, Optional - def _is_list_of_str(obj): # type: (Any) -> bool diff --git a/src/pip/_internal/req/__init__.py b/src/pip/_internal/req/__init__.py index ef6e162a21c..06f0a0823f1 100644 --- a/src/pip/_internal/req/__init__.py +++ b/src/pip/_internal/req/__init__.py @@ -1,6 +1,6 @@ import collections import logging -from typing import TYPE_CHECKING +from typing import Iterator, List, Optional, Sequence, Tuple from pip._internal.utils.logging import indent_log @@ -8,9 +8,6 @@ from .req_install import InstallRequirement from .req_set import RequirementSet -if TYPE_CHECKING: - from typing import Iterator, List, Optional, Sequence, Tuple - __all__ = [ "RequirementSet", "InstallRequirement", "parse_requirements", "install_given_reqs", diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index 6a649f0d858..784cd81f66d 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -11,7 +11,7 @@ import logging import os import re -from typing import TYPE_CHECKING +from typing import Any, Dict, Optional, Set, Tuple, Union from pip._vendor.packaging.markers import Marker from pip._vendor.packaging.requirements import InvalidRequirement, Requirement @@ -23,18 +23,13 @@ from pip._internal.models.link import Link from pip._internal.models.wheel import Wheel from pip._internal.pyproject import make_pyproject_path +from pip._internal.req.req_file import ParsedRequirement from pip._internal.req.req_install import InstallRequirement from pip._internal.utils.filetypes import is_archive_file from pip._internal.utils.misc import is_installable_dir from pip._internal.utils.urls import path_to_url from pip._internal.vcs import is_url, vcs -if TYPE_CHECKING: - from typing import Any, Dict, Optional, Set, Tuple, Union - - from pip._internal.req.req_file import ParsedRequirement - - __all__ = [ "install_req_from_editable", "install_req_from_line", "parse_editable" diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index a2f87209b35..336cd137e4c 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -7,38 +7,36 @@ import re import shlex import urllib.parse -from typing import TYPE_CHECKING +from optparse import Values +from typing import ( + TYPE_CHECKING, + Any, + Callable, + Dict, + Iterator, + List, + NoReturn, + Optional, + Text, + Tuple, +) from pip._internal.cli import cmdoptions from pip._internal.exceptions import InstallationError, RequirementsFileParseError from pip._internal.models.search_scope import SearchScope +from pip._internal.network.session import PipSession from pip._internal.network.utils import raise_for_status from pip._internal.utils.encoding import auto_decode from pip._internal.utils.urls import get_url_scheme, url_to_path if TYPE_CHECKING: - from optparse import Values - from typing import ( - Any, - Callable, - Dict, - Iterator, - List, - NoReturn, - Optional, - Text, - Tuple, - ) - from pip._internal.index.package_finder import PackageFinder - from pip._internal.network.session import PipSession - - ReqFileLines = Iterator[Tuple[int, Text]] - LineParser = Callable[[Text], Tuple[str, Values]] +__all__ = ['parse_requirements'] +ReqFileLines = Iterator[Tuple[int, Text]] -__all__ = ['parse_requirements'] +LineParser = Callable[[Text], Tuple[str, Values]] SCHEME_RE = re.compile(r'^(http|https|file):', re.I) COMMENT_RE = re.compile(r'(^|\s+)#.*$') diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 29a5cd275ee..f8643c21ac3 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -7,16 +7,19 @@ import sys import uuid import zipfile -from typing import TYPE_CHECKING +from typing import Any, Dict, Iterable, List, Optional, Sequence, Union from pip._vendor import pkg_resources, six +from pip._vendor.packaging.markers import Marker from pip._vendor.packaging.requirements import Requirement +from pip._vendor.packaging.specifiers import SpecifierSet from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.packaging.version import Version from pip._vendor.packaging.version import parse as parse_version from pip._vendor.pep517.wrappers import Pep517HookCaller +from pip._vendor.pkg_resources import Distribution -from pip._internal.build_env import NoOpBuildEnvironment +from pip._internal.build_env import BuildEnvironment, NoOpBuildEnvironment from pip._internal.exceptions import InstallationError from pip._internal.locations import get_scheme from pip._internal.models.link import Link @@ -51,16 +54,6 @@ from pip._internal.utils.virtualenv import running_under_virtualenv from pip._internal.vcs import vcs -if TYPE_CHECKING: - from typing import Any, Dict, Iterable, List, Optional, Sequence, Union - - from pip._vendor.packaging.markers import Marker - from pip._vendor.packaging.specifiers import SpecifierSet - from pip._vendor.pkg_resources import Distribution - - from pip._internal.build_env import BuildEnvironment - - logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/req/req_set.py b/src/pip/_internal/req/req_set.py index c9552183286..7ab236dc16f 100644 --- a/src/pip/_internal/req/req_set.py +++ b/src/pip/_internal/req/req_set.py @@ -1,19 +1,14 @@ import logging from collections import OrderedDict -from typing import TYPE_CHECKING +from typing import Dict, Iterable, List, Optional, Tuple from pip._vendor.packaging.utils import canonicalize_name from pip._internal.exceptions import InstallationError from pip._internal.models.wheel import Wheel +from pip._internal.req.req_install import InstallRequirement from pip._internal.utils import compatibility_tags -if TYPE_CHECKING: - from typing import Dict, Iterable, List, Optional, Tuple - - from pip._internal.req.req_install import InstallRequirement - - logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/req/req_tracker.py b/src/pip/_internal/req/req_tracker.py index ab753ce6ddd..542e0d94e37 100644 --- a/src/pip/_internal/req/req_tracker.py +++ b/src/pip/_internal/req/req_tracker.py @@ -2,17 +2,13 @@ import hashlib import logging import os -from typing import TYPE_CHECKING +from types import TracebackType +from typing import Dict, Iterator, Optional, Set, Type, Union +from pip._internal.models.link import Link +from pip._internal.req.req_install import InstallRequirement from pip._internal.utils.temp_dir import TempDirectory -if TYPE_CHECKING: - from types import TracebackType - from typing import Dict, Iterator, Optional, Set, Type, Union - - from pip._internal.models.link import Link - from pip._internal.req.req_install import InstallRequirement - logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/req/req_uninstall.py b/src/pip/_internal/req/req_uninstall.py index d7e28dc004e..519b79166a4 100644 --- a/src/pip/_internal/req/req_uninstall.py +++ b/src/pip/_internal/req/req_uninstall.py @@ -5,9 +5,10 @@ import sys import sysconfig from importlib.util import cache_from_source -from typing import TYPE_CHECKING +from typing import Any, Callable, Dict, Iterable, Iterator, List, Optional, Set, Tuple from pip._vendor import pkg_resources +from pip._vendor.pkg_resources import Distribution from pip._internal.exceptions import UninstallationError from pip._internal.locations import bin_py, bin_user @@ -25,21 +26,6 @@ ) from pip._internal.utils.temp_dir import AdjacentTempDirectory, TempDirectory -if TYPE_CHECKING: - from typing import ( - Any, - Callable, - Dict, - Iterable, - Iterator, - List, - Optional, - Set, - Tuple, - ) - - from pip._vendor.pkg_resources import Distribution - logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/resolution/base.py b/src/pip/_internal/resolution/base.py index caf4e0d8cf6..7526bfe84d7 100644 --- a/src/pip/_internal/resolution/base.py +++ b/src/pip/_internal/resolution/base.py @@ -1,14 +1,11 @@ -from typing import TYPE_CHECKING +from typing import Callable, List -if TYPE_CHECKING: - from typing import Callable, List +from pip._internal.req.req_install import InstallRequirement +from pip._internal.req.req_set import RequirementSet - from pip._internal.req.req_install import InstallRequirement - from pip._internal.req.req_set import RequirementSet - - InstallRequirementProvider = Callable[ - [str, InstallRequirement], InstallRequirement - ] +InstallRequirementProvider = Callable[ + [str, InstallRequirement], InstallRequirement +] class BaseResolver: diff --git a/src/pip/_internal/resolution/legacy/resolver.py b/src/pip/_internal/resolution/legacy/resolver.py index 8f5378a0787..d5ea3a0ac0b 100644 --- a/src/pip/_internal/resolution/legacy/resolver.py +++ b/src/pip/_internal/resolution/legacy/resolver.py @@ -18,10 +18,12 @@ import sys from collections import defaultdict from itertools import chain -from typing import TYPE_CHECKING +from typing import DefaultDict, List, Optional, Set, Tuple from pip._vendor.packaging import specifiers +from pip._vendor.pkg_resources import Distribution +from pip._internal.cache import WheelCache from pip._internal.exceptions import ( BestVersionAlreadyInstalled, DistributionNotFound, @@ -29,30 +31,24 @@ HashErrors, UnsupportedPythonVersion, ) -from pip._internal.req.req_install import check_invalid_constraint_type +from pip._internal.index.package_finder import PackageFinder +from pip._internal.models.link import Link +from pip._internal.operations.prepare import RequirementPreparer +from pip._internal.req.req_install import ( + InstallRequirement, + check_invalid_constraint_type, +) from pip._internal.req.req_set import RequirementSet -from pip._internal.resolution.base import BaseResolver +from pip._internal.resolution.base import BaseResolver, InstallRequirementProvider from pip._internal.utils.compatibility_tags import get_supported from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import dist_in_usersite, normalize_version_info from pip._internal.utils.packaging import check_requires_python, get_requires_python -if TYPE_CHECKING: - from typing import DefaultDict, List, Optional, Set, Tuple - - from pip._vendor.pkg_resources import Distribution - - from pip._internal.cache import WheelCache - from pip._internal.index.package_finder import PackageFinder - from pip._internal.models.link import Link - from pip._internal.operations.prepare import RequirementPreparer - from pip._internal.req.req_install import InstallRequirement - from pip._internal.resolution.base import InstallRequirementProvider - - DiscoveredDependencies = DefaultDict[str, List[InstallRequirement]] - logger = logging.getLogger(__name__) +DiscoveredDependencies = DefaultDict[str, List[InstallRequirement]] + def _check_dist_requires_python( dist, # type: Distribution diff --git a/src/pip/_internal/resolution/resolvelib/base.py b/src/pip/_internal/resolution/resolvelib/base.py index 29d798c3065..81fee9b9e3e 100644 --- a/src/pip/_internal/resolution/resolvelib/base.py +++ b/src/pip/_internal/resolution/resolvelib/base.py @@ -1,22 +1,14 @@ -from typing import TYPE_CHECKING +from typing import FrozenSet, Iterable, Optional, Tuple from pip._vendor.packaging.specifiers import SpecifierSet from pip._vendor.packaging.utils import canonicalize_name +from pip._vendor.packaging.version import _BaseVersion +from pip._internal.models.link import Link from pip._internal.req.req_install import InstallRequirement from pip._internal.utils.hashes import Hashes -if TYPE_CHECKING: - from typing import FrozenSet, Iterable, Optional, Tuple - - from pip._vendor.packaging.version import _BaseVersion - - from pip._internal.models.link import Link - - CandidateLookup = Tuple[ - Optional["Candidate"], - Optional[InstallRequirement], - ] +CandidateLookup = Tuple[Optional["Candidate"], Optional[InstallRequirement]] def format_name(project, extras): diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 2240cac0893..fbd0a613d33 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -1,12 +1,14 @@ import logging import sys -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Any, FrozenSet, Iterable, Optional, Tuple, Union from pip._vendor.packaging.specifiers import InvalidSpecifier, SpecifierSet from pip._vendor.packaging.utils import canonicalize_name -from pip._vendor.packaging.version import Version +from pip._vendor.packaging.version import Version, _BaseVersion +from pip._vendor.pkg_resources import Distribution from pip._internal.exceptions import HashError, MetadataInconsistent +from pip._internal.models.link import Link from pip._internal.models.wheel import Wheel from pip._internal.req.constructors import ( install_req_from_editable, @@ -16,28 +18,19 @@ from pip._internal.utils.misc import dist_is_editable, normalize_version_info from pip._internal.utils.packaging import get_requires_python -from .base import Candidate, format_name +from .base import Candidate, Requirement, format_name if TYPE_CHECKING: - from typing import Any, FrozenSet, Iterable, Optional, Tuple, Union - - from pip._vendor.packaging.version import _BaseVersion - from pip._vendor.pkg_resources import Distribution - - from pip._internal.models.link import Link - - from .base import Requirement from .factory import Factory - BaseCandidate = Union[ - "AlreadyInstalledCandidate", - "EditableCandidate", - "LinkCandidate", - ] - - logger = logging.getLogger(__name__) +BaseCandidate = Union[ + "AlreadyInstalledCandidate", + "EditableCandidate", + "LinkCandidate", +] + def make_install_req_from_link(link, template): # type: (Link, InstallRequirement) -> InstallRequirement diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 5bfe0cc066f..259d76af6fd 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -1,9 +1,25 @@ import functools import logging -from typing import TYPE_CHECKING +from typing import ( + Dict, + FrozenSet, + Iterable, + Iterator, + List, + Optional, + Sequence, + Set, + Tuple, + TypeVar, +) +from pip._vendor.packaging.specifiers import SpecifierSet from pip._vendor.packaging.utils import canonicalize_name +from pip._vendor.packaging.version import _BaseVersion +from pip._vendor.pkg_resources import Distribution +from pip._vendor.resolvelib import ResolutionImpossible +from pip._internal.cache import CacheEntry, WheelCache from pip._internal.exceptions import ( DistributionNotFound, InstallationError, @@ -12,8 +28,12 @@ UnsupportedPythonVersion, UnsupportedWheel, ) +from pip._internal.index.package_finder import PackageFinder +from pip._internal.models.link import Link from pip._internal.models.wheel import Wheel +from pip._internal.operations.prepare import RequirementPreparer from pip._internal.req.req_install import InstallRequirement +from pip._internal.resolution.base import InstallRequirementProvider from pip._internal.utils.compatibility_tags import get_supported from pip._internal.utils.hashes import Hashes from pip._internal.utils.misc import ( @@ -23,15 +43,16 @@ ) from pip._internal.utils.virtualenv import running_under_virtualenv -from .base import Constraint +from .base import Candidate, Constraint, Requirement from .candidates import ( AlreadyInstalledCandidate, + BaseCandidate, EditableCandidate, ExtrasCandidate, LinkCandidate, RequiresPythonCandidate, ) -from .found_candidates import FoundCandidates +from .found_candidates import FoundCandidates, IndexCandidateInfo from .requirements import ( ExplicitRequirement, RequiresPythonRequirement, @@ -39,40 +60,11 @@ UnsatisfiableRequirement, ) -if TYPE_CHECKING: - from typing import ( - Dict, - FrozenSet, - Iterable, - Iterator, - List, - Optional, - Sequence, - Set, - Tuple, - TypeVar, - ) - - from pip._vendor.packaging.specifiers import SpecifierSet - from pip._vendor.packaging.version import _BaseVersion - from pip._vendor.pkg_resources import Distribution - from pip._vendor.resolvelib import ResolutionImpossible - - from pip._internal.cache import CacheEntry, WheelCache - from pip._internal.index.package_finder import PackageFinder - from pip._internal.models.link import Link - from pip._internal.operations.prepare import RequirementPreparer - from pip._internal.resolution.base import InstallRequirementProvider - - from .base import Candidate, Requirement - from .candidates import BaseCandidate - from .found_candidates import IndexCandidateInfo - - C = TypeVar("C") - Cache = Dict[Link, C] - logger = logging.getLogger(__name__) +C = TypeVar("C") +Cache = Dict[Link, C] + class Factory: def __init__( diff --git a/src/pip/_internal/resolution/resolvelib/found_candidates.py b/src/pip/_internal/resolution/resolvelib/found_candidates.py index 2a8d58ce2ef..594485061c0 100644 --- a/src/pip/_internal/resolution/resolvelib/found_candidates.py +++ b/src/pip/_internal/resolution/resolvelib/found_candidates.py @@ -9,18 +9,14 @@ """ import functools -from typing import TYPE_CHECKING +from typing import Callable, Iterator, Optional, Set, Tuple +from pip._vendor.packaging.version import _BaseVersion from pip._vendor.six.moves import collections_abc # type: ignore -if TYPE_CHECKING: - from typing import Callable, Iterator, Optional, Set, Tuple +from .base import Candidate - from pip._vendor.packaging.version import _BaseVersion - - from .base import Candidate - - IndexCandidateInfo = Tuple[_BaseVersion, Callable[[], Optional[Candidate]]] +IndexCandidateInfo = Tuple[_BaseVersion, Callable[[], Optional[Candidate]]] def _iter_built(infos): diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index f8632410eb8..1f4439a1467 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -1,14 +1,9 @@ -from typing import TYPE_CHECKING +from typing import Any, Dict, Iterable, Optional, Sequence, Tuple, Union from pip._vendor.resolvelib.providers import AbstractProvider -from .base import Constraint - -if TYPE_CHECKING: - from typing import Any, Dict, Iterable, Optional, Sequence, Tuple, Union - - from .base import Candidate, Requirement - from .factory import Factory +from .base import Candidate, Constraint, Requirement +from .factory import Factory # Notes on the relationship between the provider, the factory, and the # candidate and requirement classes. diff --git a/src/pip/_internal/resolution/resolvelib/reporter.py b/src/pip/_internal/resolution/resolvelib/reporter.py index 6679d73f219..697351bd763 100644 --- a/src/pip/_internal/resolution/resolvelib/reporter.py +++ b/src/pip/_internal/resolution/resolvelib/reporter.py @@ -1,14 +1,10 @@ from collections import defaultdict from logging import getLogger -from typing import TYPE_CHECKING +from typing import Any, DefaultDict from pip._vendor.resolvelib.reporters import BaseReporter -if TYPE_CHECKING: - from typing import Any, DefaultDict - - from .base import Candidate, Requirement - +from .base import Candidate, Requirement logger = getLogger(__name__) diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py index a2fad4bdb2e..aec45aa6822 100644 --- a/src/pip/_internal/resolution/resolvelib/requirements.py +++ b/src/pip/_internal/resolution/resolvelib/requirements.py @@ -1,15 +1,9 @@ -from typing import TYPE_CHECKING - +from pip._vendor.packaging.specifiers import SpecifierSet from pip._vendor.packaging.utils import canonicalize_name -from .base import Requirement, format_name - -if TYPE_CHECKING: - from pip._vendor.packaging.specifiers import SpecifierSet - - from pip._internal.req.req_install import InstallRequirement +from pip._internal.req.req_install import InstallRequirement - from .base import Candidate, CandidateLookup +from .base import Candidate, CandidateLookup, Requirement, format_name class ExplicitRequirement(Requirement): diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 935723737e9..5bfe3712b0a 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -1,17 +1,24 @@ import functools import logging import os -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple from pip._vendor import six from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.resolvelib import ResolutionImpossible from pip._vendor.resolvelib import Resolver as RLResolver +from pip._vendor.resolvelib.resolvers import Result +from pip._internal.cache import WheelCache from pip._internal.exceptions import InstallationError -from pip._internal.req.req_install import check_invalid_constraint_type +from pip._internal.index.package_finder import PackageFinder +from pip._internal.operations.prepare import RequirementPreparer +from pip._internal.req.req_install import ( + InstallRequirement, + check_invalid_constraint_type, +) from pip._internal.req.req_set import RequirementSet -from pip._internal.resolution.base import BaseResolver +from pip._internal.resolution.base import BaseResolver, InstallRequirementProvider from pip._internal.resolution.resolvelib.provider import PipProvider from pip._internal.resolution.resolvelib.reporter import ( PipDebuggingReporter, @@ -25,18 +32,8 @@ from .factory import Factory if TYPE_CHECKING: - from typing import Dict, List, Optional, Set, Tuple - - from pip._vendor.resolvelib.resolvers import Result from pip._vendor.resolvelib.structs import Graph - from pip._internal.cache import WheelCache - from pip._internal.index.package_finder import PackageFinder - from pip._internal.operations.prepare import RequirementPreparer - from pip._internal.req.req_install import InstallRequirement - from pip._internal.resolution.base import InstallRequirementProvider - - logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/self_outdated_check.py b/src/pip/_internal/self_outdated_check.py index f705dbc8308..6b24965b802 100644 --- a/src/pip/_internal/self_outdated_check.py +++ b/src/pip/_internal/self_outdated_check.py @@ -2,9 +2,10 @@ import hashlib import json import logging +import optparse import os.path import sys -from typing import TYPE_CHECKING +from typing import Any, Dict from pip._vendor.packaging.version import parse as parse_version @@ -12,16 +13,10 @@ from pip._internal.index.package_finder import PackageFinder from pip._internal.metadata import get_default_environment from pip._internal.models.selection_prefs import SelectionPreferences +from pip._internal.network.session import PipSession from pip._internal.utils.filesystem import adjacent_tmp_file, check_path_owner, replace from pip._internal.utils.misc import ensure_dir -if TYPE_CHECKING: - import optparse - from typing import Any, Dict - - from pip._internal.network.session import PipSession - - SELFCHECK_DATE_FMT = "%Y-%m-%dT%H:%M:%SZ" diff --git a/src/pip/_internal/utils/appdirs.py b/src/pip/_internal/utils/appdirs.py index 55e83e0d689..b8c101b0cd5 100644 --- a/src/pip/_internal/utils/appdirs.py +++ b/src/pip/_internal/utils/appdirs.py @@ -7,13 +7,10 @@ """ import os -from typing import TYPE_CHECKING +from typing import List from pip._vendor import appdirs as _appdirs -if TYPE_CHECKING: - from typing import List - def user_cache_dir(appname): # type: (str) -> str diff --git a/src/pip/_internal/utils/compat.py b/src/pip/_internal/utils/compat.py index 0b059952348..a468a1825a4 100644 --- a/src/pip/_internal/utils/compat.py +++ b/src/pip/_internal/utils/compat.py @@ -9,11 +9,7 @@ import logging import os import sys -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Optional, Union - +from typing import Optional, Union __all__ = ["console_to_str", "get_path_uid", "stdlib_pkgs", "WINDOWS"] diff --git a/src/pip/_internal/utils/compatibility_tags.py b/src/pip/_internal/utils/compatibility_tags.py index cfba97a5328..5578f1f78eb 100644 --- a/src/pip/_internal/utils/compatibility_tags.py +++ b/src/pip/_internal/utils/compatibility_tags.py @@ -2,7 +2,7 @@ """ import re -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, List, Optional, Tuple from pip._vendor.packaging.tags import ( Tag, @@ -15,10 +15,9 @@ ) if TYPE_CHECKING: - from typing import List, Optional, Tuple - from pip._vendor.packaging.tags import PythonVersion + _osx_arch_pat = re.compile(r'(.+)_(\d+)_(\d+)_(.+)') diff --git a/src/pip/_internal/utils/deprecation.py b/src/pip/_internal/utils/deprecation.py index d4b60ea1a19..80383089222 100644 --- a/src/pip/_internal/utils/deprecation.py +++ b/src/pip/_internal/utils/deprecation.py @@ -7,16 +7,12 @@ import logging import warnings -from typing import TYPE_CHECKING +from typing import Any, Optional from pip._vendor.packaging.version import parse from pip import __version__ as current_version -if TYPE_CHECKING: - from typing import Any, Optional - - DEPRECATION_MSG_PREFIX = "DEPRECATION: " diff --git a/src/pip/_internal/utils/direct_url_helpers.py b/src/pip/_internal/utils/direct_url_helpers.py index caf2fa1481d..e5ddc6a5c4f 100644 --- a/src/pip/_internal/utils/direct_url_helpers.py +++ b/src/pip/_internal/utils/direct_url_helpers.py @@ -1,6 +1,8 @@ import json import logging -from typing import TYPE_CHECKING +from typing import Optional + +from pip._vendor.pkg_resources import Distribution from pip._internal.models.direct_url import ( DIRECT_URL_METADATA_NAME, @@ -10,15 +12,9 @@ DirInfo, VcsInfo, ) +from pip._internal.models.link import Link from pip._internal.vcs import vcs -if TYPE_CHECKING: - from typing import Optional - - from pip._vendor.pkg_resources import Distribution - - from pip._internal.models.link import Link - logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/utils/distutils_args.py b/src/pip/_internal/utils/distutils_args.py index 7d3dae78577..e886c8884d0 100644 --- a/src/pip/_internal/utils/distutils_args.py +++ b/src/pip/_internal/utils/distutils_args.py @@ -1,10 +1,6 @@ from distutils.errors import DistutilsArgError from distutils.fancy_getopt import FancyGetopt -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Dict, List - +from typing import Dict, List _options = [ ("exec-prefix=", None, ""), diff --git a/src/pip/_internal/utils/encoding.py b/src/pip/_internal/utils/encoding.py index 122c4ab29e4..67b0209f672 100644 --- a/src/pip/_internal/utils/encoding.py +++ b/src/pip/_internal/utils/encoding.py @@ -2,10 +2,7 @@ import locale import re import sys -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import List, Tuple +from typing import List, Tuple BOMS = [ (codecs.BOM_UTF8, 'utf-8'), diff --git a/src/pip/_internal/utils/entrypoints.py b/src/pip/_internal/utils/entrypoints.py index 9c0454a627d..879bf21ac5f 100644 --- a/src/pip/_internal/utils/entrypoints.py +++ b/src/pip/_internal/utils/entrypoints.py @@ -1,11 +1,8 @@ import sys -from typing import TYPE_CHECKING +from typing import List, Optional from pip._internal.cli.main import main -if TYPE_CHECKING: - from typing import List, Optional - def _wrapper(args=None): # type: (Optional[List[str]]) -> int diff --git a/src/pip/_internal/utils/filesystem.py b/src/pip/_internal/utils/filesystem.py index 1a9d952f4f9..e9aa97685a3 100644 --- a/src/pip/_internal/utils/filesystem.py +++ b/src/pip/_internal/utils/filesystem.py @@ -7,7 +7,7 @@ import sys from contextlib import contextmanager from tempfile import NamedTemporaryFile -from typing import TYPE_CHECKING, cast +from typing import Any, BinaryIO, Iterator, List, Union, cast # NOTE: retrying is not annotated in typeshed as on 2017-07-17, which is # why we ignore the type on this import. @@ -16,9 +16,6 @@ from pip._internal.utils.compat import get_path_uid from pip._internal.utils.misc import format_size -if TYPE_CHECKING: - from typing import Any, BinaryIO, Iterator, List, Union - def check_path_owner(path): # type: (str) -> bool @@ -96,7 +93,7 @@ def adjacent_tmp_file(path, **kwargs): suffix='.tmp', **kwargs ) as f: - result = cast('BinaryIO', f) + result = cast(BinaryIO, f) try: yield result finally: diff --git a/src/pip/_internal/utils/filetypes.py b/src/pip/_internal/utils/filetypes.py index 440151d5f32..117f38757b6 100644 --- a/src/pip/_internal/utils/filetypes.py +++ b/src/pip/_internal/utils/filetypes.py @@ -1,11 +1,9 @@ """Filetype information. """ -from typing import TYPE_CHECKING -from pip._internal.utils.misc import splitext +from typing import Tuple -if TYPE_CHECKING: - from typing import Tuple +from pip._internal.utils.misc import splitext WHEEL_EXTENSION = '.whl' BZ2_EXTENSIONS = ('.tar.bz2', '.tbz') # type: Tuple[str, ...] diff --git a/src/pip/_internal/utils/glibc.py b/src/pip/_internal/utils/glibc.py index 37caad45ef6..1c9ff35446d 100644 --- a/src/pip/_internal/utils/glibc.py +++ b/src/pip/_internal/utils/glibc.py @@ -3,10 +3,7 @@ import os import sys -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Optional, Tuple +from typing import Optional, Tuple def glibc_version_string(): diff --git a/src/pip/_internal/utils/hashes.py b/src/pip/_internal/utils/hashes.py index 612c5e740d7..f91e429730c 100644 --- a/src/pip/_internal/utils/hashes.py +++ b/src/pip/_internal/utils/hashes.py @@ -1,12 +1,11 @@ import hashlib -from typing import TYPE_CHECKING +from typing import TYPE_CHECKING, BinaryIO, Dict, Iterator, List, NoReturn from pip._internal.exceptions import HashMismatch, HashMissing, InstallationError from pip._internal.utils.misc import read_chunks if TYPE_CHECKING: from hashlib import _Hash - from typing import BinaryIO, Dict, Iterator, List, NoReturn # The recommended hash algo of the moment. Change this whenever the state of diff --git a/src/pip/_internal/utils/logging.py b/src/pip/_internal/utils/logging.py index 82b99762807..d1d46ab701a 100644 --- a/src/pip/_internal/utils/logging.py +++ b/src/pip/_internal/utils/logging.py @@ -8,15 +8,12 @@ import os import sys from logging import Filter, getLogger -from typing import TYPE_CHECKING +from typing import Any from pip._internal.utils.compat import WINDOWS from pip._internal.utils.deprecation import DEPRECATION_MSG_PREFIX from pip._internal.utils.misc import ensure_dir -if TYPE_CHECKING: - from typing import Any - try: import threading except ImportError: diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 200fe841814..9a593a7e382 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -16,7 +16,21 @@ import urllib.parse from io import StringIO from itertools import filterfalse, tee, zip_longest -from typing import TYPE_CHECKING, cast +from typing import ( + Any, + AnyStr, + Callable, + Container, + Iterable, + Iterator, + List, + Optional, + Tuple, + TypeVar, + cast, +) + +from pip._vendor.pkg_resources import Distribution # NOTE: retrying is not annotated in typeshed as on 2017-07-17, which is # why we ignore the type on this import. @@ -31,26 +45,6 @@ virtualenv_no_global, ) -if TYPE_CHECKING: - from typing import ( - Any, - AnyStr, - Callable, - Container, - Iterable, - Iterator, - List, - Optional, - Tuple, - TypeVar, - ) - - from pip._vendor.pkg_resources import Distribution - - VersionInfo = Tuple[int, int, int] - T = TypeVar("T") - - __all__ = ['rmtree', 'display_path', 'backup_dir', 'ask', 'splitext', 'format_size', 'is_installable_dir', @@ -62,6 +56,9 @@ logger = logging.getLogger(__name__) +VersionInfo = Tuple[int, int, int] +T = TypeVar("T") + def get_pip_version(): # type: () -> str diff --git a/src/pip/_internal/utils/packaging.py b/src/pip/_internal/utils/packaging.py index f8de544d30c..1be31ea9123 100644 --- a/src/pip/_internal/utils/packaging.py +++ b/src/pip/_internal/utils/packaging.py @@ -1,20 +1,15 @@ import logging +from email.message import Message from email.parser import FeedParser -from typing import TYPE_CHECKING +from typing import Optional, Tuple from pip._vendor import pkg_resources from pip._vendor.packaging import specifiers, version +from pip._vendor.pkg_resources import Distribution from pip._internal.exceptions import NoneMetadataError from pip._internal.utils.misc import display_path -if TYPE_CHECKING: - from email.message import Message - from typing import Optional, Tuple - - from pip._vendor.pkg_resources import Distribution - - logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/utils/parallel.py b/src/pip/_internal/utils/parallel.py index af5d4a9df98..6b0919f1422 100644 --- a/src/pip/_internal/utils/parallel.py +++ b/src/pip/_internal/utils/parallel.py @@ -20,18 +20,15 @@ from contextlib import contextmanager from multiprocessing import Pool as ProcessPool +from multiprocessing import pool from multiprocessing.dummy import Pool as ThreadPool -from typing import TYPE_CHECKING +from typing import Callable, Iterable, Iterator, TypeVar, Union from pip._vendor.requests.adapters import DEFAULT_POOLSIZE -if TYPE_CHECKING: - from multiprocessing import pool - from typing import Callable, Iterable, Iterator, TypeVar, Union - - Pool = Union[pool.Pool, pool.ThreadPool] - S = TypeVar('S') - T = TypeVar('T') +Pool = Union[pool.Pool, pool.ThreadPool] +S = TypeVar('S') +T = TypeVar('T') # On platforms without sem_open, multiprocessing[.dummy] Pool # cannot be created. diff --git a/src/pip/_internal/utils/pkg_resources.py b/src/pip/_internal/utils/pkg_resources.py index 913bebd9834..8c4974a703f 100644 --- a/src/pip/_internal/utils/pkg_resources.py +++ b/src/pip/_internal/utils/pkg_resources.py @@ -1,10 +1,7 @@ -from typing import TYPE_CHECKING +from typing import Dict, Iterable, List from pip._vendor.pkg_resources import yield_lines -if TYPE_CHECKING: - from typing import Dict, Iterable, List - class DictMetadata: """IMetadataProvider that reads metadata files from a dictionary. diff --git a/src/pip/_internal/utils/setuptools_build.py b/src/pip/_internal/utils/setuptools_build.py index 49b0f22f5a2..7d91f6f2677 100644 --- a/src/pip/_internal/utils/setuptools_build.py +++ b/src/pip/_internal/utils/setuptools_build.py @@ -1,8 +1,5 @@ import sys -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import List, Optional, Sequence +from typing import List, Optional, Sequence # Shim to wrap setup.py invocation with setuptools # diff --git a/src/pip/_internal/utils/subprocess.py b/src/pip/_internal/utils/subprocess.py index 82bc3987ccf..b0bbd46a893 100644 --- a/src/pip/_internal/utils/subprocess.py +++ b/src/pip/_internal/utils/subprocess.py @@ -2,7 +2,7 @@ import os import shlex import subprocess -from typing import TYPE_CHECKING +from typing import Any, Callable, Iterable, List, Mapping, Optional, Union from pip._internal.cli.spinners import SpinnerInterface, open_spinner from pip._internal.exceptions import InstallationSubprocessError @@ -10,10 +10,7 @@ from pip._internal.utils.logging import subprocess_logger from pip._internal.utils.misc import HiddenText, path_to_display -if TYPE_CHECKING: - from typing import Any, Callable, Iterable, List, Mapping, Optional, Union - - CommandArgs = List[Union[str, HiddenText]] +CommandArgs = List[Union[str, HiddenText]] LOG_DIVIDER = '----------------------------------------' diff --git a/src/pip/_internal/utils/temp_dir.py b/src/pip/_internal/utils/temp_dir.py index 872b5b55fd5..8c4aaba3a96 100644 --- a/src/pip/_internal/utils/temp_dir.py +++ b/src/pip/_internal/utils/temp_dir.py @@ -4,18 +4,14 @@ import os.path import tempfile from contextlib import ExitStack, contextmanager -from typing import TYPE_CHECKING +from typing import Any, Dict, Iterator, Optional, TypeVar, Union from pip._internal.utils.misc import enum, rmtree -if TYPE_CHECKING: - from typing import Any, Dict, Iterator, Optional, TypeVar, Union - - _T = TypeVar('_T', bound='TempDirectory') - - logger = logging.getLogger(__name__) +_T = TypeVar('_T', bound='TempDirectory') + # Kinds of temporary directories. Only needed for ones that are # globally-managed. diff --git a/src/pip/_internal/utils/unpacking.py b/src/pip/_internal/utils/unpacking.py index 86c474458e2..74d3f4a9cd1 100644 --- a/src/pip/_internal/utils/unpacking.py +++ b/src/pip/_internal/utils/unpacking.py @@ -7,7 +7,8 @@ import stat import tarfile import zipfile -from typing import TYPE_CHECKING +from typing import Iterable, List, Optional +from zipfile import ZipInfo from pip._internal.exceptions import InstallationError from pip._internal.utils.filetypes import ( @@ -18,11 +19,6 @@ ) from pip._internal.utils.misc import ensure_dir -if TYPE_CHECKING: - from typing import Iterable, List, Optional - from zipfile import ZipInfo - - logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/utils/urls.py b/src/pip/_internal/utils/urls.py index da8e91a4bdc..8ae11ce7d24 100644 --- a/src/pip/_internal/utils/urls.py +++ b/src/pip/_internal/utils/urls.py @@ -2,10 +2,7 @@ import sys import urllib.parse import urllib.request -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import Optional +from typing import Optional def get_url_scheme(url): diff --git a/src/pip/_internal/utils/virtualenv.py b/src/pip/_internal/utils/virtualenv.py index eb91c907185..c9c601f86a5 100644 --- a/src/pip/_internal/utils/virtualenv.py +++ b/src/pip/_internal/utils/virtualenv.py @@ -3,10 +3,7 @@ import re import site import sys -from typing import TYPE_CHECKING - -if TYPE_CHECKING: - from typing import List, Optional +from typing import List, Optional logger = logging.getLogger(__name__) _INCLUDE_SYSTEM_SITE_PACKAGES_REGEX = re.compile( diff --git a/src/pip/_internal/utils/wheel.py b/src/pip/_internal/utils/wheel.py index 65ebf6424df..de048581208 100644 --- a/src/pip/_internal/utils/wheel.py +++ b/src/pip/_internal/utils/wheel.py @@ -2,23 +2,17 @@ """ import logging +from email.message import Message from email.parser import Parser -from typing import TYPE_CHECKING +from typing import Dict, Tuple from zipfile import BadZipFile, ZipFile from pip._vendor.packaging.utils import canonicalize_name -from pip._vendor.pkg_resources import DistInfoDistribution +from pip._vendor.pkg_resources import DistInfoDistribution, Distribution from pip._internal.exceptions import UnsupportedWheel from pip._internal.utils.pkg_resources import DictMetadata -if TYPE_CHECKING: - from email.message import Message - from typing import Dict, Tuple - - from pip._vendor.pkg_resources import Distribution - - VERSION_COMPATIBLE = (1, 0) diff --git a/src/pip/_internal/vcs/__init__.py b/src/pip/_internal/vcs/__init__.py index 2ed7c177f7c..30025d632a9 100644 --- a/src/pip/_internal/vcs/__init__.py +++ b/src/pip/_internal/vcs/__init__.py @@ -1,7 +1,6 @@ # Expose a limited set of classes and functions so callers outside of # the vcs package don't need to import deeper than `pip._internal.vcs`. -# (The test directory and imports protected by TYPE_CHECKING may -# still need to import from a vcs sub-package.) +# (The test directory may still need to import from a vcs sub-package.) # Import all vcs modules to register each VCS in the VcsSupport object. import pip._internal.vcs.bazaar import pip._internal.vcs.git diff --git a/src/pip/_internal/vcs/bazaar.py b/src/pip/_internal/vcs/bazaar.py index 6ccf8df5b96..3d603727c57 100644 --- a/src/pip/_internal/vcs/bazaar.py +++ b/src/pip/_internal/vcs/bazaar.py @@ -1,18 +1,17 @@ import logging import os -from typing import TYPE_CHECKING +from typing import List, Optional, Tuple -from pip._internal.utils.misc import display_path, rmtree +from pip._internal.utils.misc import HiddenText, display_path, rmtree from pip._internal.utils.subprocess import make_command from pip._internal.utils.urls import path_to_url -from pip._internal.vcs.versioncontrol import RemoteNotFoundError, VersionControl, vcs - -if TYPE_CHECKING: - from typing import List, Optional, Tuple - - from pip._internal.utils.misc import HiddenText - from pip._internal.vcs.versioncontrol import AuthInfo, RevOptions - +from pip._internal.vcs.versioncontrol import ( + AuthInfo, + RemoteNotFoundError, + RevOptions, + VersionControl, + vcs, +) logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/vcs/git.py b/src/pip/_internal/vcs/git.py index 7c7104c9f35..e0704091dc6 100644 --- a/src/pip/_internal/vcs/git.py +++ b/src/pip/_internal/vcs/git.py @@ -3,30 +3,24 @@ import re import urllib.parse import urllib.request -from typing import TYPE_CHECKING +from typing import List, Optional, Tuple +from pip._vendor.packaging.version import _BaseVersion from pip._vendor.packaging.version import parse as parse_version from pip._internal.exceptions import BadCommand, InstallationError -from pip._internal.utils.misc import display_path, hide_url +from pip._internal.utils.misc import HiddenText, display_path, hide_url from pip._internal.utils.subprocess import make_command from pip._internal.utils.temp_dir import TempDirectory from pip._internal.vcs.versioncontrol import ( + AuthInfo, RemoteNotFoundError, + RevOptions, VersionControl, find_path_to_setup_from_repo_root, vcs, ) -if TYPE_CHECKING: - from typing import List, Optional, Tuple - - from pip._vendor.packaging.version import _BaseVersion - - from pip._internal.utils.misc import HiddenText - from pip._internal.vcs.versioncontrol import AuthInfo, RevOptions - - urlsplit = urllib.parse.urlsplit urlunsplit = urllib.parse.urlunsplit diff --git a/src/pip/_internal/vcs/mercurial.py b/src/pip/_internal/vcs/mercurial.py index 079672191f9..fdd71f43894 100644 --- a/src/pip/_internal/vcs/mercurial.py +++ b/src/pip/_internal/vcs/mercurial.py @@ -1,26 +1,20 @@ import configparser import logging import os -from typing import TYPE_CHECKING +from typing import List, Optional from pip._internal.exceptions import BadCommand, InstallationError -from pip._internal.utils.misc import display_path +from pip._internal.utils.misc import HiddenText, display_path from pip._internal.utils.subprocess import make_command from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.urls import path_to_url from pip._internal.vcs.versioncontrol import ( + RevOptions, VersionControl, find_path_to_setup_from_repo_root, vcs, ) -if TYPE_CHECKING: - from typing import List, Optional - - from pip._internal.utils.misc import HiddenText - from pip._internal.vcs.versioncontrol import RevOptions - - logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/vcs/subversion.py b/src/pip/_internal/vcs/subversion.py index 2ddb9c0a150..0819f1bc611 100644 --- a/src/pip/_internal/vcs/subversion.py +++ b/src/pip/_internal/vcs/subversion.py @@ -1,17 +1,26 @@ import logging import os import re -from typing import TYPE_CHECKING +from typing import List, Optional, Tuple from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import ( + HiddenText, display_path, is_console_interactive, rmtree, split_auth_from_netloc, ) -from pip._internal.utils.subprocess import make_command -from pip._internal.vcs.versioncontrol import RemoteNotFoundError, VersionControl, vcs +from pip._internal.utils.subprocess import CommandArgs, make_command +from pip._internal.vcs.versioncontrol import ( + AuthInfo, + RemoteNotFoundError, + RevOptions, + VersionControl, + vcs, +) + +logger = logging.getLogger(__name__) _svn_xml_url_re = re.compile('url="([^"]+)"') _svn_rev_re = re.compile(r'committed-rev="(\d+)"') @@ -19,17 +28,6 @@ _svn_info_xml_url_re = re.compile(r'<url>(.*)</url>') -if TYPE_CHECKING: - from typing import List, Optional, Tuple - - from pip._internal.utils.misc import HiddenText - from pip._internal.utils.subprocess import CommandArgs - from pip._internal.vcs.versioncontrol import AuthInfo, RevOptions - - -logger = logging.getLogger(__name__) - - class Subversion(VersionControl): name = 'svn' dirname = '.svn' diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index 84ce0dffccb..316143b6d03 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -5,12 +5,25 @@ import shutil import sys import urllib.parse -from typing import TYPE_CHECKING +from typing import ( + Any, + Dict, + Iterable, + Iterator, + List, + Mapping, + Optional, + Tuple, + Type, + Union, +) from pip._vendor import pkg_resources +from pip._internal.cli.spinners import SpinnerInterface from pip._internal.exceptions import BadCommand, InstallationError from pip._internal.utils.misc import ( + HiddenText, ask_path_exists, backup_dir, display_path, @@ -18,35 +31,16 @@ hide_value, rmtree, ) -from pip._internal.utils.subprocess import call_subprocess, make_command +from pip._internal.utils.subprocess import CommandArgs, call_subprocess, make_command from pip._internal.utils.urls import get_url_scheme -if TYPE_CHECKING: - from typing import ( - Any, - Dict, - Iterable, - Iterator, - List, - Mapping, - Optional, - Tuple, - Type, - Union, - ) - - from pip._internal.cli.spinners import SpinnerInterface - from pip._internal.utils.misc import HiddenText - from pip._internal.utils.subprocess import CommandArgs - - AuthInfo = Tuple[Optional[str], Optional[str]] - - __all__ = ['vcs'] logger = logging.getLogger(__name__) +AuthInfo = Tuple[Optional[str], Optional[str]] + def is_url(name): # type: (str) -> bool diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index 4b72e512466..d6314714acd 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -5,17 +5,19 @@ import os.path import re import shutil -from typing import TYPE_CHECKING +from typing import Any, Callable, Iterable, List, Optional, Tuple from pip._vendor.packaging.utils import canonicalize_name, canonicalize_version from pip._vendor.packaging.version import InvalidVersion, Version +from pip._internal.cache import WheelCache from pip._internal.exceptions import InvalidWheelFilename, UnsupportedWheel from pip._internal.metadata import get_wheel_distribution from pip._internal.models.link import Link from pip._internal.models.wheel import Wheel from pip._internal.operations.build.wheel import build_wheel_pep517 from pip._internal.operations.build.wheel_legacy import build_wheel_legacy +from pip._internal.req.req_install import InstallRequirement from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import ensure_dir, hash_file, is_wheel_installed from pip._internal.utils.setuptools_build import make_setuptools_clean_args @@ -24,19 +26,13 @@ from pip._internal.utils.urls import path_to_url from pip._internal.vcs import vcs -if TYPE_CHECKING: - from typing import Any, Callable, Iterable, List, Optional, Tuple - - from pip._internal.cache import WheelCache - from pip._internal.req.req_install import InstallRequirement - - BinaryAllowedPredicate = Callable[[InstallRequirement], bool] - BuildResult = Tuple[List[InstallRequirement], List[InstallRequirement]] - logger = logging.getLogger(__name__) _egg_info_re = re.compile(r'([a-z0-9_.]+)-([a-z0-9_.!+-]+)', re.IGNORECASE) +BinaryAllowedPredicate = Callable[[InstallRequirement], bool] +BuildResult = Tuple[List[InstallRequirement], List[InstallRequirement]] + def _contains_egg_info(s): # type: (str) -> bool diff --git a/tests/conftest.py b/tests/conftest.py index e54b9d75ce1..36f90653d6b 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -8,7 +8,7 @@ import sys import time from contextlib import ExitStack, contextmanager -from typing import TYPE_CHECKING +from typing import Dict, Iterable from unittest.mock import patch import pytest @@ -19,17 +19,12 @@ from tests.lib import DATA_DIR, SRC_DIR, PipTestEnvironment, TestData from tests.lib.certs import make_tls_cert, serialize_cert, serialize_key from tests.lib.path import Path -from tests.lib.server import make_mock_server, server_running +from tests.lib.server import MockServer as _MockServer +from tests.lib.server import Responder, make_mock_server, server_running from tests.lib.venv import VirtualEnvironment from .lib.compat import nullcontext -if TYPE_CHECKING: - from typing import Dict, Iterable - - from tests.lib.server import MockServer as _MockServer - from tests.lib.server import Responder - def pytest_addoption(parser): parser.addoption( diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index 02395b75468..059de3ba14f 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -10,7 +10,7 @@ from hashlib import sha256 from io import BytesIO from textwrap import dedent -from typing import TYPE_CHECKING +from typing import List, Optional from zipfile import ZipFile import pytest @@ -21,17 +21,12 @@ from pip._internal.locations import get_major_minor_version from pip._internal.models.search_scope import SearchScope from pip._internal.models.selection_prefs import SelectionPreferences +from pip._internal.models.target_python import TargetPython from pip._internal.network.session import PipSession from pip._internal.utils.deprecation import DEPRECATION_MSG_PREFIX from tests.lib.path import Path, curdir from tests.lib.wheel import make_wheel -if TYPE_CHECKING: - from typing import List, Optional - - from pip._internal.models.target_python import TargetPython - - DATA_DIR = Path(__file__).parent.parent.joinpath("data").resolve() SRC_DIR = Path(__file__).resolve().parent.parent.parent diff --git a/tests/lib/certs.py b/tests/lib/certs.py index 779afd018e3..b3a9b8e1046 100644 --- a/tests/lib/certs.py +++ b/tests/lib/certs.py @@ -1,5 +1,5 @@ from datetime import datetime, timedelta -from typing import TYPE_CHECKING +from typing import Tuple from cryptography import x509 from cryptography.hazmat.backends import default_backend @@ -7,9 +7,6 @@ from cryptography.hazmat.primitives.asymmetric import rsa from cryptography.x509.oid import NameOID -if TYPE_CHECKING: - from typing import Tuple - def make_tls_cert(hostname): # type: (str) -> Tuple[x509.Certificate, rsa.RSAPrivateKey] diff --git a/tests/lib/local_repos.py b/tests/lib/local_repos.py index 09e38640e00..0aa75787e0c 100644 --- a/tests/lib/local_repos.py +++ b/tests/lib/local_repos.py @@ -1,14 +1,11 @@ import os import subprocess import urllib.request -from typing import TYPE_CHECKING from pip._internal.utils.misc import hide_url from pip._internal.vcs import vcs from tests.lib import path_to_url - -if TYPE_CHECKING: - from tests.lib.path import Path +from tests.lib.path import Path def _create_svn_initools_repo(initools_dir): diff --git a/tests/lib/server.py b/tests/lib/server.py index 356495fa0ff..caaa3ffece6 100644 --- a/tests/lib/server.py +++ b/tests/lib/server.py @@ -5,31 +5,28 @@ from base64 import b64encode from contextlib import contextmanager from textwrap import dedent -from typing import TYPE_CHECKING +from types import TracebackType +from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Type from unittest.mock import Mock -from werkzeug.serving import WSGIRequestHandler +from werkzeug.serving import BaseWSGIServer, WSGIRequestHandler from werkzeug.serving import make_server as _make_server from .compat import nullcontext -if TYPE_CHECKING: - from types import TracebackType - from typing import Any, Callable, Dict, Iterable, List, Optional, Tuple, Type +Environ = Dict[str, str] +Status = str +Headers = Iterable[Tuple[str, str]] +ExcInfo = Tuple[Type[BaseException], BaseException, TracebackType] +Write = Callable[[bytes], None] +StartResponse = Callable[[Status, Headers, Optional[ExcInfo]], Write] +Body = List[bytes] +Responder = Callable[[Environ, StartResponse], Body] - from werkzeug.serving import BaseWSGIServer - Environ = Dict[str, str] - Status = str - Headers = Iterable[Tuple[str, str]] - ExcInfo = Tuple[Type[BaseException], BaseException, TracebackType] - Write = Callable[[bytes], None] - StartResponse = Callable[[Status, Headers, Optional[ExcInfo]], Write] - Body = List[bytes] - Responder = Callable[[Environ, StartResponse], Body] +class MockServer(BaseWSGIServer): + mock = Mock() # type: Mock - class MockServer(BaseWSGIServer): - mock = Mock() # type: Mock # Applies on Python 2 and Windows. if not hasattr(signal, "pthread_sigmask"): diff --git a/tests/lib/test_wheel.py b/tests/lib/test_wheel.py index 294fd7c037a..835ad31ec39 100644 --- a/tests/lib/test_wheel.py +++ b/tests/lib/test_wheel.py @@ -2,8 +2,8 @@ """ import csv from email import message_from_string +from email.message import Message from functools import partial -from typing import TYPE_CHECKING from zipfile import ZipFile from tests.lib.wheel import ( @@ -14,9 +14,6 @@ message_from_dict, ) -if TYPE_CHECKING: - from email import Message - def test_message_from_dict_one_value(): message = message_from_dict({"a": "1"}) diff --git a/tests/lib/wheel.py b/tests/lib/wheel.py index 6028f117d7a..e88ce8c6101 100644 --- a/tests/lib/wheel.py +++ b/tests/lib/wheel.py @@ -10,34 +10,31 @@ from functools import partial from hashlib import sha256 from io import BytesIO, StringIO -from typing import TYPE_CHECKING +from typing import ( + AnyStr, + Callable, + Dict, + Iterable, + List, + Optional, + Sequence, + Tuple, + TypeVar, + Union, +) from zipfile import ZipFile from pip._vendor.requests.structures import CaseInsensitiveDict from tests.lib.path import Path -if TYPE_CHECKING: - from typing import ( - AnyStr, - Callable, - Dict, - Iterable, - List, - Optional, - Sequence, - Tuple, - TypeVar, - Union, - ) - - # path, digest, size - RecordLike = Tuple[str, str, str] - RecordCallback = Callable[ - [List["Record"]], Union[str, bytes, List[RecordLike]] - ] - # As would be used in metadata - HeaderValue = Union[str, List[str]] +# path, digest, size +RecordLike = Tuple[str, str, str] +RecordCallback = Callable[ + [List["Record"]], Union[str, bytes, List[RecordLike]] +] +# As would be used in metadata +HeaderValue = Union[str, List[str]] File = namedtuple("File", ["name", "contents"]) @@ -50,14 +47,10 @@ class Default(Enum): _default = Default.token +T = TypeVar("T") -if TYPE_CHECKING: - T = TypeVar("T") - - class Defaulted(Union[Default, T]): - """A type which may be defaulted. - """ - pass +# A type which may be defaulted. +Defaulted = Union[Default, T] def ensure_binary(value): diff --git a/tests/unit/test_utils_wheel.py b/tests/unit/test_utils_wheel.py index 461a29b5e01..878d8d777e5 100644 --- a/tests/unit/test_utils_wheel.py +++ b/tests/unit/test_utils_wheel.py @@ -2,16 +2,13 @@ from contextlib import ExitStack from email import message_from_string from io import BytesIO -from typing import TYPE_CHECKING from zipfile import ZipFile import pytest from pip._internal.exceptions import UnsupportedWheel from pip._internal.utils import wheel - -if TYPE_CHECKING: - from tests.lib.path import Path +from tests.lib.path import Path @pytest.fixture From 2fdb7de25dcd1cf4e2fabf7c1bdc9e27599b7db8 Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Mon, 8 Feb 2021 19:43:01 -0800 Subject: [PATCH 2975/3170] Remove outdated compat shim for stdlib html.unescape --- src/pip/_internal/index/collector.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/index/collector.py b/src/pip/_internal/index/collector.py index 3dd6c7df49d..f6b0536d0d6 100644 --- a/src/pip/_internal/index/collector.py +++ b/src/pip/_internal/index/collector.py @@ -4,6 +4,7 @@ import cgi import functools +import html import itertools import logging import mimetypes @@ -26,7 +27,6 @@ ) from pip._vendor import html5lib, requests -from pip._vendor.distlib.compat import unescape from pip._vendor.requests import Response from pip._vendor.requests.exceptions import RetryError, SSLError @@ -261,12 +261,11 @@ def _create_link_from_element( url = _clean_link(urllib.parse.urljoin(base_url, href)) pyrequire = anchor.get('data-requires-python') - pyrequire = unescape(pyrequire) if pyrequire else None + pyrequire = html.unescape(pyrequire) if pyrequire else None yanked_reason = anchor.get('data-yanked') if yanked_reason: - # This is a unicode string in Python 2 (and 3). - yanked_reason = unescape(yanked_reason) + yanked_reason = html.unescape(yanked_reason) link = Link( url, From b75ea75ff933acd2907476a4e3bde5200b817606 Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Sat, 2 Jan 2021 10:05:49 -0800 Subject: [PATCH 2976/3170] Add a test for subprocess logging with UnicodeDecodeError --- tests/unit/test_utils_subprocess.py | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/tests/unit/test_utils_subprocess.py b/tests/unit/test_utils_subprocess.py index ecae2295c88..01ac50e2f11 100644 --- a/tests/unit/test_utils_subprocess.py +++ b/tests/unit/test_utils_subprocess.py @@ -430,3 +430,28 @@ def test_closes_stdin(self): [sys.executable, '-c', 'input()'], show_stdout=True, ) + + +def test_unicode_decode_error(caplog): + if locale.getpreferredencoding() != "UTF-8": + pytest.skip("locale.getpreferredencoding() is not UTF-8") + caplog.set_level(INFO) + call_subprocess( + [ + sys.executable, + "-c", + "import sys; sys.stdout.buffer.write(b'\\xff')", + ], + show_stdout=True + ) + + assert len(caplog.records) == 3 + # First log record is "Running command ..." + assert caplog.record_tuples[1:] == [ + ( + "pip._internal.utils.compat", + WARNING, + "Subprocess output does not appear to be encoded as UTF-8", + ), + ("pip.subprocessor", INFO, "\\xff"), + ] From cd9a0dd26d562fefa114a9b53bd40c2e5b8995c6 Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Sat, 2 Jan 2021 08:38:45 -0800 Subject: [PATCH 2977/3170] Simplify str_to_display() and its uses With Python 2 dropped, only bytes need to be handled. Passing str was a nop. These bytes/str boundaries are much clearer now. There remains one use of str_to_display(): constole_to_str(). It converts subprocess bytes output to str while handling decoding errors. --- ...62a11c-018c-4fde-ac8d-f674c6d9d190.trivial.rst | 0 src/pip/_internal/utils/compat.py | 7 ++----- src/pip/_internal/utils/subprocess.py | 9 ++------- tests/unit/test_compat.py | 15 --------------- 4 files changed, 4 insertions(+), 27 deletions(-) create mode 100644 news/fd62a11c-018c-4fde-ac8d-f674c6d9d190.trivial.rst diff --git a/news/fd62a11c-018c-4fde-ac8d-f674c6d9d190.trivial.rst b/news/fd62a11c-018c-4fde-ac8d-f674c6d9d190.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/utils/compat.py b/src/pip/_internal/utils/compat.py index a468a1825a4..2beff5ec7c8 100644 --- a/src/pip/_internal/utils/compat.py +++ b/src/pip/_internal/utils/compat.py @@ -9,7 +9,7 @@ import logging import os import sys -from typing import Optional, Union +from typing import Optional __all__ = ["console_to_str", "get_path_uid", "stdlib_pkgs", "WINDOWS"] @@ -30,7 +30,7 @@ def has_tls(): def str_to_display(data, desc=None): - # type: (Union[bytes, str], Optional[str]) -> str + # type: (bytes, Optional[str]) -> str """ For display or logging purposes, convert a bytes object (or text) to text (e.g. unicode in Python 2) safe for output. @@ -48,10 +48,7 @@ def str_to_display(data, desc=None): We also ensure that the output can be safely written to standard output without encoding errors. """ - if isinstance(data, str): - return data - # Otherwise, data is a bytes object (str in Python 2). # 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 diff --git a/src/pip/_internal/utils/subprocess.py b/src/pip/_internal/utils/subprocess.py index cec7896bf77..1f0f9cc3838 100644 --- a/src/pip/_internal/utils/subprocess.py +++ b/src/pip/_internal/utils/subprocess.py @@ -6,7 +6,7 @@ from pip._internal.cli.spinners import SpinnerInterface, open_spinner from pip._internal.exceptions import InstallationSubprocessError -from pip._internal.utils.compat import console_to_str, str_to_display +from pip._internal.utils.compat import console_to_str from pip._internal.utils.logging import subprocess_logger from pip._internal.utils.misc import HiddenText @@ -74,11 +74,6 @@ def make_subprocess_output_error( :param lines: A list of lines, each ending with a newline. """ command = format_command_args(cmd_args) - # Convert `command` and `cwd` to text (unicode in Python 2) so we can use - # them as arguments in the unicode format string below. This avoids - # "UnicodeDecodeError: 'ascii' codec can't decode byte ..." in Python 2 - # if either contains a non-ascii character. - command_display = str_to_display(command, desc='command bytes') # We know the joined output value ends in a newline. output = ''.join(lines) @@ -92,7 +87,7 @@ def make_subprocess_output_error( 'Complete output ({line_count} lines):\n{output}{divider}' ).format( exit_status=exit_status, - command_display=command_display, + command_display=command, cwd_display=cwd, line_count=len(lines), output=output, diff --git a/tests/unit/test_compat.py b/tests/unit/test_compat.py index 655e45ab75e..62dd3655a3e 100644 --- a/tests/unit/test_compat.py +++ b/tests/unit/test_compat.py @@ -46,22 +46,7 @@ def test_get_path_uid_symlink_without_NOFOLLOW(tmpdir, monkeypatch): get_path_uid(fs) -@pytest.mark.parametrize('data, expected', [ - ('abc', 'abc'), - # Test text input with non-ascii characters. - ('déf', 'déf'), -]) -def test_str_to_display(data, expected): - actual = str_to_display(data) - assert actual == expected, ( - # Show the encoding for easier troubleshooting. - f'encoding: {locale.getpreferredencoding()!r}' - ) - - @pytest.mark.parametrize('data, encoding, expected', [ - # Test str input with non-ascii characters. - ('déf', 'utf-8', 'déf'), # Test bytes input with non-ascii characters: ('déf'.encode('utf-8'), 'utf-8', 'déf'), # Test a Windows encoding. From ea576933b9364f4a680f63fd57dc941d3498617b Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Sat, 2 Jan 2021 10:30:08 -0800 Subject: [PATCH 2978/3170] Remove str_to_display/console_to_str in favor of subprocess decoding Since Python 3.6, subprocess supports decoding stdout and stderr. Like before, decoding errors are handled using "backslashreplace". One difference: In the event of a decoding error, pip previously logged a warning before trying again with "backslashreplace". Subprocess does not log such a warning. --- src/pip/_internal/utils/compat.py | 76 +-------------------------- src/pip/_internal/utils/subprocess.py | 9 ++-- tests/unit/test_compat.py | 68 +----------------------- tests/unit/test_utils_subprocess.py | 11 +--- 4 files changed, 7 insertions(+), 157 deletions(-) diff --git a/src/pip/_internal/utils/compat.py b/src/pip/_internal/utils/compat.py index 2beff5ec7c8..9600e0f44cc 100644 --- a/src/pip/_internal/utils/compat.py +++ b/src/pip/_internal/utils/compat.py @@ -4,14 +4,11 @@ # The following comment should be removed at some point in the future. # mypy: disallow-untyped-defs=False -import codecs -import locale import logging import os import sys -from typing import Optional -__all__ = ["console_to_str", "get_path_uid", "stdlib_pkgs", "WINDOWS"] +__all__ = ["get_path_uid", "stdlib_pkgs", "WINDOWS"] logger = logging.getLogger(__name__) @@ -29,77 +26,6 @@ def has_tls(): return IS_PYOPENSSL -def str_to_display(data, desc=None): - # type: (bytes, Optional[str]) -> str - """ - For display or logging purposes, convert a bytes object (or text) to - text (e.g. unicode in Python 2) safe for output. - - :param desc: An optional phrase describing the input data, for use in - the log message if a warning is logged. Defaults to "Bytes object". - - This function should never error out and so can take a best effort - approach. It is okay to be lossy if needed since the return value is - just for display. - - 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: - decoded_data = data.decode(encoding) - except UnicodeDecodeError: - logger.warning( - '%s does not appear to be encoded as %s', - desc or 'Bytes object', - encoding, - ) - decoded_data = data.decode(encoding, errors="backslashreplace") - - # 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: - output_encoded = decoded_data.encode( - output_encoding, - errors="backslashreplace" - ) - decoded_data = output_encoded.decode(output_encoding) - - return decoded_data - - -def console_to_str(data): - # type: (bytes) -> str - """Return a string, safe for output, of subprocess output. - """ - return str_to_display(data, desc='Subprocess output') - - def get_path_uid(path): # type: (str) -> int """ diff --git a/src/pip/_internal/utils/subprocess.py b/src/pip/_internal/utils/subprocess.py index 1f0f9cc3838..ccfc1432ff1 100644 --- a/src/pip/_internal/utils/subprocess.py +++ b/src/pip/_internal/utils/subprocess.py @@ -6,7 +6,6 @@ from pip._internal.cli.spinners import SpinnerInterface, open_spinner from pip._internal.exceptions import InstallationSubprocessError -from pip._internal.utils.compat import console_to_str from pip._internal.utils.logging import subprocess_logger from pip._internal.utils.misc import HiddenText @@ -177,6 +176,7 @@ def call_subprocess( stderr=subprocess.STDOUT if not stdout_only else subprocess.PIPE, cwd=cwd, env=env, + errors="backslashreplace", ) except Exception as exc: if log_failed_cmd: @@ -191,8 +191,7 @@ def call_subprocess( proc.stdin.close() # In this mode, stdout and stderr are in the same pipe. while True: - # The "line" value is a unicode string in Python 2. - line = console_to_str(proc.stdout.readline()) + line = proc.stdout.readline() if not line: break line = line.rstrip() @@ -213,13 +212,11 @@ def call_subprocess( else: # In this mode, stdout and stderr are in different pipes. # We must use communicate() which is the only safe way to read both. - out_bytes, err_bytes = proc.communicate() + out, err = proc.communicate() # log line by line to preserve pip log indenting - out = console_to_str(out_bytes) for out_line in out.splitlines(): log_subprocess(out_line) all_output.append(out) - err = console_to_str(err_bytes) for err_line in err.splitlines(): log_subprocess(err_line) all_output.append(err) diff --git a/tests/unit/test_compat.py b/tests/unit/test_compat.py index 62dd3655a3e..2d7cbf5c3af 100644 --- a/tests/unit/test_compat.py +++ b/tests/unit/test_compat.py @@ -1,11 +1,8 @@ -import locale import os -import sys import pytest -import pip._internal.utils.compat as pip_compat -from pip._internal.utils.compat import console_to_str, get_path_uid, str_to_display +from pip._internal.utils.compat import get_path_uid def test_get_path_uid(): @@ -44,66 +41,3 @@ def test_get_path_uid_symlink_without_NOFOLLOW(tmpdir, monkeypatch): os.symlink(f, fs) with pytest.raises(OSError): get_path_uid(fs) - - -@pytest.mark.parametrize('data, encoding, expected', [ - # Test bytes input with non-ascii characters: - ('déf'.encode('utf-8'), 'utf-8', 'déf'), - # Test a Windows encoding. - ('déf'.encode('cp1252'), 'cp1252', 'déf'), - # Test a Windows encoding with incompatibly encoded text. - ('déf'.encode('utf-8'), 'cp1252', 'déf'), -]) -def test_str_to_display__encoding(monkeypatch, data, encoding, expected): - monkeypatch.setattr(locale, 'getpreferredencoding', lambda: encoding) - actual = str_to_display(data) - assert actual == expected, ( - # Show the encoding for easier troubleshooting. - f'encoding: {locale.getpreferredencoding()!r}' - ) - - -def test_str_to_display__decode_error(monkeypatch, caplog): - monkeypatch.setattr(locale, 'getpreferredencoding', lambda: 'utf-8') - # Encode with an incompatible encoding. - data = 'ab'.encode('utf-16') - actual = str_to_display(data) - # Keep the expected value endian safe - if sys.byteorder == "little": - expected = "\\xff\\xfea\x00b\x00" - elif sys.byteorder == "big": - expected = "\\xfe\\xff\x00a\x00b" - - assert actual == expected, ( - # Show the encoding for easier troubleshooting. - f'encoding: {locale.getpreferredencoding()!r}' - ) - assert len(caplog.records) == 1 - record = caplog.records[0] - assert record.levelname == 'WARNING' - assert record.message == ( - 'Bytes object does not appear to be encoded as utf-8' - ) - - -def test_console_to_str(monkeypatch): - some_bytes = b"a\xE9\xC3\xE9b" - encodings = ('ascii', 'utf-8', 'iso-8859-1', 'iso-8859-5', - 'koi8_r', 'cp850') - for e in encodings: - monkeypatch.setattr(locale, 'getpreferredencoding', lambda: e) - result = console_to_str(some_bytes) - assert result.startswith("a") - assert result.endswith("b") - - -def test_console_to_str_warning(monkeypatch): - some_bytes = b"a\xE9b" - - def check_warning(msg, *args, **kwargs): - assert 'does not appear to be encoded as' in msg - assert args[0] == 'Subprocess output' - - monkeypatch.setattr(locale, 'getpreferredencoding', lambda: 'utf-8') - monkeypatch.setattr(pip_compat.logger, 'warning', check_warning) - console_to_str(some_bytes) diff --git a/tests/unit/test_utils_subprocess.py b/tests/unit/test_utils_subprocess.py index 01ac50e2f11..d64ffbe02e6 100644 --- a/tests/unit/test_utils_subprocess.py +++ b/tests/unit/test_utils_subprocess.py @@ -445,13 +445,6 @@ def test_unicode_decode_error(caplog): show_stdout=True ) - assert len(caplog.records) == 3 + assert len(caplog.records) == 2 # First log record is "Running command ..." - assert caplog.record_tuples[1:] == [ - ( - "pip._internal.utils.compat", - WARNING, - "Subprocess output does not appear to be encoded as UTF-8", - ), - ("pip.subprocessor", INFO, "\\xff"), - ] + assert caplog.record_tuples[1] == ("pip.subprocessor", INFO, "\\xff") From 2da75c4c657a1d23cff9074177ed82f13f08c0b7 Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Wed, 6 Jan 2021 17:09:22 -0800 Subject: [PATCH 2979/3170] Annotate variables assigned stdout lines as str --- src/pip/_internal/utils/subprocess.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/utils/subprocess.py b/src/pip/_internal/utils/subprocess.py index ccfc1432ff1..fc5a026b886 100644 --- a/src/pip/_internal/utils/subprocess.py +++ b/src/pip/_internal/utils/subprocess.py @@ -191,7 +191,7 @@ def call_subprocess( proc.stdin.close() # In this mode, stdout and stderr are in the same pipe. while True: - line = proc.stdout.readline() + line = proc.stdout.readline() # type: str if not line: break line = line.rstrip() From 5db17383ec88fa6c5a43545eb24c2d1e135940ee Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Sun, 27 Dec 2020 19:34:32 -0800 Subject: [PATCH 2980/3170] Replace six.moves.xmlrpc_client with stdlib Updated type signatures for compatibility with the stdlib class. --- ...e04056-e1d6-4f9a-bf46-8938d1936d9e.trivial.rst | 0 src/pip/_internal/commands/search.py | 10 ++++------ src/pip/_internal/network/xmlrpc.py | 15 ++++++++------- 3 files changed, 12 insertions(+), 13 deletions(-) create mode 100644 news/5be04056-e1d6-4f9a-bf46-8938d1936d9e.trivial.rst diff --git a/news/5be04056-e1d6-4f9a-bf46-8938d1936d9e.trivial.rst b/news/5be04056-e1d6-4f9a-bf46-8938d1936d9e.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/commands/search.py b/src/pip/_internal/commands/search.py index 6fcc9354fe8..f03cd6a3acd 100644 --- a/src/pip/_internal/commands/search.py +++ b/src/pip/_internal/commands/search.py @@ -2,16 +2,13 @@ import shutil import sys import textwrap +import xmlrpc.client from collections import OrderedDict from optparse import Values from typing import TYPE_CHECKING, Dict, List, Optional from pip._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 pip._vendor.six.moves import xmlrpc_client # type: ignore - from pip._internal.cli.base_command import Command from pip._internal.cli.req_command import SessionCommandMixin from pip._internal.cli.status_codes import NO_MATCHES_FOUND, SUCCESS @@ -75,15 +72,16 @@ def search(self, query, options): session = self.get_default_session(options) transport = PipXmlrpcTransport(index_url, session) - pypi = xmlrpc_client.ServerProxy(index_url, transport) + pypi = xmlrpc.client.ServerProxy(index_url, transport) try: hits = pypi.search({'name': query, 'summary': query}, 'or') - except xmlrpc_client.Fault as fault: + except xmlrpc.client.Fault as fault: message = "XMLRPC request failed [code: {code}]\n{string}".format( code=fault.faultCode, string=fault.faultString, ) raise CommandError(message) + assert isinstance(hits, list) return hits diff --git a/src/pip/_internal/network/xmlrpc.py b/src/pip/_internal/network/xmlrpc.py index c9f3c5db819..b92b8d9ae18 100644 --- a/src/pip/_internal/network/xmlrpc.py +++ b/src/pip/_internal/network/xmlrpc.py @@ -3,20 +3,20 @@ import logging import urllib.parse -from typing import Dict - -# 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 +import xmlrpc.client +from typing import TYPE_CHECKING, Tuple from pip._internal.exceptions import NetworkConnectionError from pip._internal.network.session import PipSession from pip._internal.network.utils import raise_for_status +if TYPE_CHECKING: + from xmlrpc.client import _HostType, _Marshallable + logger = logging.getLogger(__name__) -class PipXmlrpcTransport(xmlrpc_client.Transport): +class PipXmlrpcTransport(xmlrpc.client.Transport): """Provide a `xmlrpclib.Transport` implementation via a `PipSession` object. """ @@ -29,7 +29,8 @@ def __init__(self, index_url, session, use_datetime=False): self._session = session def request(self, host, handler, request_body, verbose=False): - # type: (str, str, Dict[str, str], bool) -> None + # type: (_HostType, str, bytes, bool) -> Tuple[_Marshallable, ...] + assert isinstance(host, str) parts = (self._scheme, host, handler, None, None, None) url = urllib.parse.urlunparse(parts) try: From 8a6f6ac19b80a6dc35900a47016c851d9fcd2ee2 Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Sat, 20 Feb 2021 06:51:11 -0800 Subject: [PATCH 2981/3170] Blacken src/pip/_internal/resolution directory --- .pre-commit-config.yaml | 1 - ...46-d005-46ca-b1ae-a3811357dba3.trivial.rst | 0 src/pip/_internal/resolution/base.py | 4 +- .../_internal/resolution/legacy/resolver.py | 80 +++++++++---------- .../resolution/resolvelib/candidates.py | 63 +++++++-------- .../resolution/resolvelib/factory.py | 56 +++++++------ .../resolution/resolvelib/found_candidates.py | 1 + .../resolution/resolvelib/provider.py | 16 ++-- .../resolution/resolvelib/reporter.py | 3 +- .../resolution/resolvelib/requirements.py | 16 ++-- .../resolution/resolvelib/resolver.py | 14 ++-- 11 files changed, 124 insertions(+), 130 deletions(-) create mode 100644 news/151a1e46-d005-46ca-b1ae-a3811357dba3.trivial.rst diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 7b06692b237..163beba71a5 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,7 +31,6 @@ repos: ^src/pip/_internal/network| ^src/pip/_internal/operations| ^src/pip/_internal/req| - ^src/pip/_internal/resolution| ^src/pip/_internal/utils| ^src/pip/_internal/vcs| ^src/pip/_internal/\w+\.py$| diff --git a/news/151a1e46-d005-46ca-b1ae-a3811357dba3.trivial.rst b/news/151a1e46-d005-46ca-b1ae-a3811357dba3.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/resolution/base.py b/src/pip/_internal/resolution/base.py index 7526bfe84d7..1be0cb279a0 100644 --- a/src/pip/_internal/resolution/base.py +++ b/src/pip/_internal/resolution/base.py @@ -3,9 +3,7 @@ from pip._internal.req.req_install import InstallRequirement from pip._internal.req.req_set import RequirementSet -InstallRequirementProvider = Callable[ - [str, InstallRequirement], InstallRequirement -] +InstallRequirementProvider = Callable[[str, InstallRequirement], InstallRequirement] class BaseResolver: diff --git a/src/pip/_internal/resolution/legacy/resolver.py b/src/pip/_internal/resolution/legacy/resolver.py index d5ea3a0ac0b..c37b396e21e 100644 --- a/src/pip/_internal/resolution/legacy/resolver.py +++ b/src/pip/_internal/resolution/legacy/resolver.py @@ -71,31 +71,32 @@ def _check_dist_requires_python( requires_python = get_requires_python(dist) try: is_compatible = check_requires_python( - requires_python, version_info=version_info, + requires_python, version_info=version_info ) except specifiers.InvalidSpecifier as exc: logger.warning( - "Package %r has an invalid Requires-Python: %s", - dist.project_name, exc, + "Package %r has an invalid Requires-Python: %s", dist.project_name, exc ) return if is_compatible: return - version = '.'.join(map(str, version_info)) + version = ".".join(map(str, version_info)) if ignore_requires_python: logger.debug( - 'Ignoring failed Requires-Python check for package %r: ' - '%s not in %r', - dist.project_name, version, requires_python, + "Ignoring failed Requires-Python check for package %r: " "%s not in %r", + dist.project_name, + version, + requires_python, ) return raise UnsupportedPythonVersion( - 'Package {!r} requires a different Python: {} not in {!r}'.format( - dist.project_name, version, requires_python, - )) + "Package {!r} requires a different Python: {} not in {!r}".format( + dist.project_name, version, requires_python + ) + ) class Resolver(BaseResolver): @@ -142,8 +143,9 @@ def __init__( self.use_user_site = use_user_site self._make_install_req = make_install_req - self._discovered_dependencies = \ - defaultdict(list) # type: DiscoveredDependencies + self._discovered_dependencies = defaultdict( + list + ) # type: DiscoveredDependencies def resolve(self, root_reqs, check_supported_wheels): # type: (List[InstallRequirement], bool) -> RequirementSet @@ -157,9 +159,7 @@ def resolve(self, root_reqs, check_supported_wheels): possible to move the preparation to become a step separated from dependency resolution. """ - requirement_set = RequirementSet( - check_supported_wheels=check_supported_wheels - ) + requirement_set = RequirementSet(check_supported_wheels=check_supported_wheels) for req in root_reqs: if req.constraint: check_invalid_constraint_type(req) @@ -236,8 +236,8 @@ def _check_skip_installed(self, req_to_install): if not self._is_upgrade_allowed(req_to_install): if self.upgrade_strategy == "only-if-needed": - return 'already satisfied, skipping upgrade' - return 'already satisfied' + return "already satisfied, skipping upgrade" + 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 @@ -247,7 +247,7 @@ def _check_skip_installed(self, req_to_install): self.finder.find_requirement(req_to_install, upgrade=True) except BestVersionAlreadyInstalled: # Then the best version is installed. - return 'already up-to-date' + 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. @@ -267,14 +267,14 @@ def _find_requirement_link(self, req): # Log a warning per PEP 592 if necessary before returning. link = best_candidate.link if link.is_yanked: - reason = link.yanked_reason or '<none given>' + reason = link.yanked_reason or "<none given>" msg = ( # Mark this as a unicode string to prevent # "UnicodeEncodeError: 'ascii' codec can't encode character" # in Python 2 when the reason contains non-ascii characters. - 'The candidate selected for download or install is a ' - 'yanked version: {candidate}\n' - 'Reason for being yanked: {reason}' + "The candidate selected for download or install is a " + "yanked version: {candidate}\n" + "Reason for being yanked: {reason}" ).format(candidate=best_candidate, reason=reason) logger.warning(msg) @@ -305,7 +305,7 @@ def _populate_link(self, req): supported_tags=get_supported(), ) if cache_entry is not None: - logger.debug('Using cached wheel link: %s', cache_entry.link) + logger.debug("Using cached wheel link: %s", cache_entry.link) if req.link is req.original_link and cache_entry.persistent: req.original_link_is_in_wheel_cache = True req.link = cache_entry.link @@ -324,9 +324,7 @@ def _get_dist_for(self, req): skip_reason = self._check_skip_installed(req) if req.satisfied_by: - return self.preparer.prepare_installed_requirement( - req, skip_reason - ) + return self.preparer.prepare_installed_requirement(req, skip_reason) # We eagerly populate the link, since that's our "legacy" behavior. self._populate_link(req) @@ -345,17 +343,17 @@ def _get_dist_for(self, req): 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' + 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, + "Requirement already satisfied (use --upgrade to upgrade):" " %s", + req, ) return dist @@ -382,7 +380,8 @@ def _resolve_one( # This will raise UnsupportedPythonVersion if the given Python # version isn't compatible with the distribution's Requires-Python. _check_dist_requires_python( - dist, version_info=self._py_version_info, + dist, + version_info=self._py_version_info, ignore_requires_python=self.ignore_requires_python, ) @@ -400,9 +399,7 @@ def add_req(subreq, extras_requested): extras_requested=extras_requested, ) if parent_req_name and add_to_parent: - self._discovered_dependencies[parent_req_name].append( - add_to_parent - ) + self._discovered_dependencies[parent_req_name].append(add_to_parent) more_reqs.extend(to_scan_again) with indent_log(): @@ -413,24 +410,19 @@ def add_req(subreq, extras_requested): # 'unnamed' requirements can only come from being directly # provided by the user. assert req_to_install.user_supplied - requirement_set.add_requirement( - req_to_install, parent_req_name=None, - ) + requirement_set.add_requirement(req_to_install, parent_req_name=None) if not self.ignore_dependencies: if req_to_install.extras: logger.debug( "Installing extra requirements: %r", - ','.join(req_to_install.extras), + ",".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 - ) + logger.warning("%s does not provide the extra '%s'", dist, missing) available_requested = sorted( set(dist.extras) & set(req_to_install.extras) diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index fbd0a613d33..035e118d022 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -49,7 +49,7 @@ def make_install_req_from_link(link, template): options=dict( install_options=template.install_options, global_options=template.global_options, - hashes=template.hash_options + hashes=template.hash_options, ), ) ireq.original_link = template.original_link @@ -70,7 +70,7 @@ def make_install_req_from_editable(link, template): options=dict( install_options=template.install_options, global_options=template.global_options, - hashes=template.hash_options + hashes=template.hash_options, ), ) @@ -94,7 +94,7 @@ def make_install_req_from_dist(dist, template): options=dict( install_options=template.install_options, global_options=template.global_options, - hashes=template.hash_options + hashes=template.hash_options, ), ) ireq.satisfied_by = dist @@ -116,15 +116,16 @@ class exposes appropriate information to the resolver. ``link`` would point to the wheel cache, while this points to the found remote link (e.g. from pypi.org). """ + is_installed = False def __init__( self, - link, # type: Link - source_link, # type: Link - ireq, # type: InstallRequirement - factory, # type: Factory - name=None, # type: Optional[str] + link, # type: Link + source_link, # type: Link + ireq, # type: InstallRequirement + factory, # type: Factory + name=None, # type: Optional[str] version=None, # type: Optional[_BaseVersion] ): # type: (...) -> None @@ -187,7 +188,7 @@ def format_for_error(self): return "{} {} (from {})".format( self.name, self.version, - self._link.file_path if self._link.is_file else self._link + self._link.file_path if self._link.is_file else self._link, ) def _prepare_distribution(self): @@ -256,10 +257,10 @@ class LinkCandidate(_InstallRequirementBackedCandidate): def __init__( self, - link, # type: Link - template, # type: InstallRequirement - factory, # type: Factory - name=None, # type: Optional[str] + link, # type: Link + template, # type: InstallRequirement + factory, # type: Factory + name=None, # type: Optional[str] version=None, # type: Optional[_BaseVersion] ): # type: (...) -> None @@ -273,21 +274,19 @@ def __init__( if ireq.link.is_wheel and not ireq.link.is_file: wheel = Wheel(ireq.link.filename) wheel_name = canonicalize_name(wheel.name) - assert name == wheel_name, ( - f"{name!r} != {wheel_name!r} for wheel" - ) + assert name == wheel_name, f"{name!r} != {wheel_name!r} for wheel" # Version may not be present for PEP 508 direct URLs if version is not None: wheel_version = Version(wheel.version) - assert version == wheel_version, ( - "{!r} != {!r} for wheel {}".format( - version, wheel_version, name - ) + assert version == wheel_version, "{!r} != {!r} for wheel {}".format( + version, wheel_version, name ) - if (cache_entry is not None and - cache_entry.persistent and - template.link is template.original_link): + if ( + cache_entry is not None + and cache_entry.persistent + and template.link is template.original_link + ): ireq.original_link_is_in_wheel_cache = True super().__init__( @@ -302,7 +301,7 @@ def __init__( def _prepare_distribution(self): # type: () -> Distribution return self._factory.preparer.prepare_linked_requirement( - self._ireq, parallel_builds=True, + self._ireq, parallel_builds=True ) @@ -311,10 +310,10 @@ class EditableCandidate(_InstallRequirementBackedCandidate): def __init__( self, - link, # type: Link - template, # type: InstallRequirement - factory, # type: Factory - name=None, # type: Optional[str] + link, # type: Link + template, # type: InstallRequirement + factory, # type: Factory + name=None, # type: Optional[str] version=None, # type: Optional[_BaseVersion] ): # type: (...) -> None @@ -435,6 +434,7 @@ class ExtrasCandidate(Candidate): version 2.0. Having those candidates depend on foo=1.0 and foo=2.0 respectively forces the resolver to recognise that this is a conflict. """ + def __init__( self, base, # type: BaseCandidate @@ -486,8 +486,7 @@ def version(self): def format_for_error(self): # type: () -> str return "{} [{}]".format( - self.base.format_for_error(), - ", ".join(sorted(self.extras)) + self.base.format_for_error(), ", ".join(sorted(self.extras)) ) @property @@ -524,12 +523,12 @@ def iter_dependencies(self, with_requires): "%s %s does not provide the extra '%s'", self.base.name, self.version, - extra + extra, ) for r in self.base.dist.requires(valid_extras): requirement = factory.make_requirement_from_spec( - str(r), self.base._ireq, valid_extras, + str(r), self.base._ireq, valid_extras ) if requirement: yield requirement diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 259d76af6fd..506a560d6fb 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -92,8 +92,9 @@ def __init__( self._build_failures = {} # type: Cache[InstallationError] self._link_candidate_cache = {} # type: Cache[LinkCandidate] self._editable_candidate_cache = {} # type: Cache[EditableCandidate] - self._installed_candidate_cache = { - } # type: Dict[str, AlreadyInstalledCandidate] + self._installed_candidate_cache = ( + {} + ) # type: Dict[str, AlreadyInstalledCandidate] if not ignore_installed: self._installed_dists = { @@ -145,8 +146,11 @@ def _make_candidate_from_link( if link not in self._editable_candidate_cache: try: self._editable_candidate_cache[link] = EditableCandidate( - link, template, factory=self, - name=name, version=version, + link, + template, + factory=self, + name=name, + version=version, ) except (InstallationSubprocessError, MetadataInconsistent) as e: logger.warning("Discarding %s. %s", link, e) @@ -157,8 +161,11 @@ def _make_candidate_from_link( if link not in self._link_candidate_cache: try: self._link_candidate_cache[link] = LinkCandidate( - link, template, factory=self, - name=name, version=version, + link, + template, + factory=self, + name=name, + version=version, ) except (InstallationSubprocessError, MetadataInconsistent) as e: logger.warning("Discarding %s. %s", link, e) @@ -268,7 +275,8 @@ def find_candidates( ) return ( - c for c in explicit_candidates + c + for c in explicit_candidates if constraint.is_satisfied_by(c) and all(req.is_satisfied_by(c) for req in requirements) ) @@ -278,7 +286,8 @@ def make_requirement_from_install_req(self, ireq, requested_extras): if not ireq.match_markers(requested_extras): logger.info( "Ignoring %s: markers '%s' don't match your environment", - ireq.name, ireq.markers, + ireq.name, + ireq.markers, ) return None if not ireq.link: @@ -372,7 +381,8 @@ def get_dist_to_uninstall(self, candidate): raise InstallationError( "Will not install to the user site because it will " "lack sys.path precedence to {} in {}".format( - dist.project_name, dist.location, + dist.project_name, + dist.location, ) ) return None @@ -418,14 +428,12 @@ def get_installation_error(self, e): if parent is None: req_disp = str(req) else: - req_disp = f'{req} (from {parent.name})' + req_disp = f"{req} (from {parent.name})" logger.critical( "Could not find a version that satisfies the requirement %s", req_disp, ) - return DistributionNotFound( - f'No matching distribution found for {req}' - ) + return DistributionNotFound(f"No matching distribution found for {req}") # OK, we now have a list of requirements that can't all be # satisfied at once. @@ -461,26 +469,28 @@ def describe_trigger(parent): else: info = "the requested packages" - msg = "Cannot install {} because these package versions " \ + msg = ( + "Cannot install {} because these package versions " "have conflicting dependencies.".format(info) + ) logger.critical(msg) msg = "\nThe conflict is caused by:" for req, parent in e.causes: msg = msg + "\n " if parent: - msg = msg + "{} {} depends on ".format( - parent.name, - parent.version - ) + msg = msg + "{} {} depends on ".format(parent.name, parent.version) else: msg = msg + "The user requested " msg = msg + req.format_for_error() - msg = msg + "\n\n" + \ - "To fix this you could try to:\n" + \ - "1. loosen the range of package versions you've specified\n" + \ - "2. remove package versions to allow pip attempt to solve " + \ - "the dependency conflict\n" + msg = ( + msg + + "\n\n" + + "To fix this you could try to:\n" + + "1. loosen the range of package versions you've specified\n" + + "2. remove package versions to allow pip attempt to solve " + + "the dependency conflict\n" + ) logger.info(msg) diff --git a/src/pip/_internal/resolution/resolvelib/found_candidates.py b/src/pip/_internal/resolution/resolvelib/found_candidates.py index 594485061c0..e8b72e66000 100644 --- a/src/pip/_internal/resolution/resolvelib/found_candidates.py +++ b/src/pip/_internal/resolution/resolvelib/found_candidates.py @@ -97,6 +97,7 @@ class FoundCandidates(collections_abc.Sequence): page when remote packages are actually needed. This improve performances when suitable candidates are already installed on disk. """ + def __init__( self, get_infos, # type: Callable[[], Iterator[IndexCandidateInfo]] diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index 1f4439a1467..2085a0714a3 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -58,7 +58,7 @@ def get_preference( self, resolution, # type: Optional[Candidate] candidates, # type: Sequence[Candidate] - information # type: Sequence[Tuple[Requirement, Candidate]] + information, # type: Sequence[Tuple[Requirement, Candidate]] ): # type: (...) -> Any """Produce a sort key for given requirement based on preference. @@ -99,9 +99,7 @@ def _get_restrictive_rating(requirements): return 0 spec_sets = (ireq.specifier for ireq in ireqs if ireq) operators = [ - specifier.operator - for spec_set in spec_sets - for specifier in spec_set + specifier.operator for spec_set in spec_sets for specifier in spec_set ] if any(op in ("==", "===") for op in operators): return 1 @@ -122,7 +120,7 @@ def _get_restrictive_rating(requirements): # delaying Setuptools helps reduce branches the resolver has to check. # This serves as a temporary fix for issues like "apache-airlfow[all]" # while we work on "proper" branch pruning techniques. - delay_this = (key == "setuptools") + delay_this = key == "setuptools" return (delay_this, restrictive, order, key) @@ -147,7 +145,7 @@ def _eligible_for_upgrade(name): if self._upgrade_strategy == "eager": return True elif self._upgrade_strategy == "only-if-needed": - return (name in self._user_requested) + return name in self._user_requested return False return self._factory.find_candidates( @@ -163,8 +161,4 @@ def is_satisfied_by(self, requirement, candidate): def get_dependencies(self, candidate): # type: (Candidate) -> Sequence[Requirement] with_requires = not self._ignore_dependencies - return [ - r - for r in candidate.iter_dependencies(with_requires) - if r is not None - ] + return [r for r in candidate.iter_dependencies(with_requires) if r is not None] diff --git a/src/pip/_internal/resolution/resolvelib/reporter.py b/src/pip/_internal/resolution/resolvelib/reporter.py index 697351bd763..074583de0d9 100644 --- a/src/pip/_internal/resolution/resolvelib/reporter.py +++ b/src/pip/_internal/resolution/resolvelib/reporter.py @@ -10,7 +10,6 @@ class PipReporter(BaseReporter): - def __init__(self): # type: () -> None self.backtracks_by_package = defaultdict(int) # type: DefaultDict[str, int] @@ -32,7 +31,7 @@ def __init__(self): "runtime. If you want to abort this run, you can press " "Ctrl + C to do so. To improve how pip performs, tell us what " "happened here: https://pip.pypa.io/surveys/backtracking" - ) + ), } def backtracking(self, candidate): diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py index aec45aa6822..70ad86af947 100644 --- a/src/pip/_internal/resolution/resolvelib/requirements.py +++ b/src/pip/_internal/resolution/resolvelib/requirements.py @@ -96,9 +96,11 @@ def get_candidate_lookup(self): def is_satisfied_by(self, candidate): # type: (Candidate) -> bool - assert candidate.name == self.name, \ - "Internal issue: Candidate is not for this requirement " \ - " {} vs {}".format(candidate.name, self.name) + assert ( + candidate.name == self.name + ), "Internal issue: Candidate is not for this requirement " " {} vs {}".format( + candidate.name, self.name + ) # We can safely always allow prereleases here since PackageFinder # already implements the prerelease logic, and would have filtered out # prerelease candidates if the user does not expect them. @@ -107,8 +109,8 @@ def is_satisfied_by(self, candidate): class RequiresPythonRequirement(Requirement): - """A requirement representing Requires-Python metadata. - """ + """A requirement representing Requires-Python metadata.""" + def __init__(self, specifier, match): # type: (SpecifierSet, Candidate) -> None self.specifier = specifier @@ -155,8 +157,8 @@ def is_satisfied_by(self, candidate): class UnsatisfiableRequirement(Requirement): - """A requirement that cannot be satisfied. - """ + """A requirement that cannot be satisfied.""" + def __init__(self, name): # type: (str) -> None self._name = name diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 5bfe3712b0a..8828155a228 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -97,7 +97,7 @@ def resolve(self, root_reqs, check_supported_wheels): if canonical_name not in user_requested: user_requested[canonical_name] = i r = self.factory.make_requirement_from_install_req( - req, requested_extras=(), + req, requested_extras=() ) if r is not None: requirements.append(r) @@ -118,7 +118,7 @@ def resolve(self, root_reqs, check_supported_wheels): try: try_to_avoid_resolution_too_deep = 2000000 self._result = resolver.resolve( - requirements, max_rounds=try_to_avoid_resolution_too_deep, + requirements, max_rounds=try_to_avoid_resolution_too_deep ) except ResolutionImpossible as e: @@ -188,14 +188,14 @@ def resolve(self, root_reqs, check_supported_wheels): # The reason can contain non-ASCII characters, Unicode # is required for Python 2. msg = ( - 'The candidate selected for download or install is a ' - 'yanked version: {name!r} candidate (version {version} ' - 'at {link})\nReason for being yanked: {reason}' + "The candidate selected for download or install is a " + "yanked version: {name!r} candidate (version {version} " + "at {link})\nReason for being yanked: {reason}" ).format( name=candidate.name, version=candidate.version, link=link, - reason=link.yanked_reason or '<none given>', + reason=link.yanked_reason or "<none given>", ) logger.warning(msg) @@ -281,7 +281,7 @@ def visit(node): def _req_set_item_sorter( - item, # type: Tuple[str, InstallRequirement] + item, # type: Tuple[str, InstallRequirement] weights, # type: Dict[Optional[str], int] ): # type: (...) -> Tuple[int, str] From 9485c4e6a0359aff268480520d00cbe8cb5f03c3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ha=CC=88cker?= <mhaecker@mac.com> Date: Sat, 20 Feb 2021 16:10:21 +0100 Subject: [PATCH 2982/3170] Fix --target not working with --editable as setup.py develop does not understand --home but instead requires the --install-dir option. Parially fixes #4390 --- src/pip/_internal/utils/setuptools_build.py | 3 ++- tests/functional/test_install.py | 22 +++++++++++++++++++++ 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/utils/setuptools_build.py b/src/pip/_internal/utils/setuptools_build.py index 7d91f6f2677..0458a4d0484 100644 --- a/src/pip/_internal/utils/setuptools_build.py +++ b/src/pip/_internal/utils/setuptools_build.py @@ -103,7 +103,8 @@ def make_setuptools_develop_args( if prefix: args += ["--prefix", prefix] if home is not None: - args += ["--home", home] + # args += ["--home", home] + args += ["--install-dir", home] if use_user_site: args += ["--user", "--prefix="] diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 2cfb3d3d272..39a06ec3f8f 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -1059,6 +1059,28 @@ def test_install_editable_with_prefix(script): result.did_create(install_path) +@pytest.mark.network +def test_install_editable_with_target(script): + pkg_path = script.scratch_path / 'pkg' + pkg_path.mkdir() + pkg_path.joinpath("setup.py").write_text(textwrap.dedent(""" + from setuptools import setup + setup( + name='pkg', + install_requires=['watching_testrunner'] + ) + """)) + + target = script.scratch_path / 'target' + target.mkdir() + result = script.pip( + 'install', '--editable', pkg_path, '--target', target + ) + + result.did_create(script.scratch / 'target' / 'pkg.egg-link') + result.did_create(script.scratch / 'target' / 'watching_testrunner.py') + + def test_install_package_conflict_prefix_and_user(script, data): """ Test installing a package using pip install --prefix --user errors out From 4d3b9fe7f628ff2e860ca175f95a81219af4e2a2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ha=CC=88cker?= <mhaecker@mac.com> Date: Sat, 20 Feb 2021 16:16:59 +0100 Subject: [PATCH 2983/3170] Add news entry. --- news/4390.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/4390.bugfix.rst diff --git a/news/4390.bugfix.rst b/news/4390.bugfix.rst new file mode 100644 index 00000000000..9f0afb5bb6f --- /dev/null +++ b/news/4390.bugfix.rst @@ -0,0 +1 @@ +Fix --target to work with --editable installs. From bfc4e9c33176d44e5d267f0e8eb1703e35e5c5c5 Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Sat, 20 Feb 2021 07:20:08 -0800 Subject: [PATCH 2984/3170] Replace for/if/append pattern with list comprehension in autocompletion.py --- news/11e1b2eb-6433-4f15-b70d-c2c514f72ebd.trivial.rst | 0 src/pip/_internal/cli/autocompletion.py | 9 +++++---- 2 files changed, 5 insertions(+), 4 deletions(-) create mode 100644 news/11e1b2eb-6433-4f15-b70d-c2c514f72ebd.trivial.rst diff --git a/news/11e1b2eb-6433-4f15-b70d-c2c514f72ebd.trivial.rst b/news/11e1b2eb-6433-4f15-b70d-c2c514f72ebd.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/cli/autocompletion.py b/src/pip/_internal/cli/autocompletion.py index 3b5aa15deec..8dc2be20055 100644 --- a/src/pip/_internal/cli/autocompletion.py +++ b/src/pip/_internal/cli/autocompletion.py @@ -47,11 +47,12 @@ def autocomplete(): 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) + installed = [ + dist.key + for dist in get_installed_distributions(local_only=True) + if dist.key.startswith(lc) and dist.key not in cwords[1:] + ] # if there are no dists installed, fall back to option completion if installed: for dist in installed: From e494e3748da0cb40c73051aa00f568e3b1302a50 Mon Sep 17 00:00:00 2001 From: Andrey Bienkowski <hexagonrecursion@gmail.com> Date: Sat, 20 Feb 2021 20:50:18 +0300 Subject: [PATCH 2985/3170] Apply review suggestion --- tests/unit/test_req.py | 8 ++++---- tests/unit/test_req_file.py | 8 ++++---- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index 1530eea8c7b..8d1254fd892 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -511,7 +511,7 @@ def test_markers_match(self): def test_extras_for_line_path_requirement(self): line = 'SomeProject[ex1,ex2]' filename = 'filename' - comes_from = f'-r {filename} (line {1})' + comes_from = f'-r {filename} (line 1)' req = install_req_from_line(line, comes_from=comes_from) assert len(req.extras) == 2 assert req.extras == {'ex1', 'ex2'} @@ -519,7 +519,7 @@ def test_extras_for_line_path_requirement(self): def test_extras_for_line_url_requirement(self): line = 'git+https://url#egg=SomeProject[ex1,ex2]' filename = 'filename' - comes_from = f'-r {filename} (line {1})' + comes_from = f'-r {filename} (line 1)' req = install_req_from_line(line, comes_from=comes_from) assert len(req.extras) == 2 assert req.extras == {'ex1', 'ex2'} @@ -527,7 +527,7 @@ def test_extras_for_line_url_requirement(self): def test_extras_for_editable_path_requirement(self): url = '.[ex1,ex2]' filename = 'filename' - comes_from = f'-r {filename} (line {1})' + comes_from = f'-r {filename} (line 1)' req = install_req_from_editable(url, comes_from=comes_from) assert len(req.extras) == 2 assert req.extras == {'ex1', 'ex2'} @@ -535,7 +535,7 @@ def test_extras_for_editable_path_requirement(self): def test_extras_for_editable_url_requirement(self): url = 'git+https://url#egg=SomeProject[ex1,ex2]' filename = 'filename' - comes_from = f'-r {filename} (line {1})' + comes_from = f'-r {filename} (line 1)' req = install_req_from_editable(url, comes_from=comes_from) assert len(req.extras) == 2 assert req.extras == {'ex1', 'ex2'} diff --git a/tests/unit/test_req_file.py b/tests/unit/test_req_file.py index a66a1b1e755..caa9e721ba7 100644 --- a/tests/unit/test_req_file.py +++ b/tests/unit/test_req_file.py @@ -229,14 +229,14 @@ def test_error_message(self, line_processor): def test_yield_line_requirement(self, line_processor): line = 'SomeProject' filename = 'filename' - comes_from = f'-r {filename} (line {1})' + comes_from = f'-r {filename} (line 1)' req = install_req_from_line(line, comes_from=comes_from) assert repr(line_processor(line, filename, 1)[0]) == repr(req) def test_yield_pep440_line_requirement(self, line_processor): line = 'SomeProject @ https://url/SomeProject-py2-py3-none-any.whl' filename = 'filename' - comes_from = f'-r {filename} (line {1})' + comes_from = f'-r {filename} (line 1)' req = install_req_from_line(line, comes_from=comes_from) assert repr(line_processor(line, filename, 1)[0]) == repr(req) @@ -255,7 +255,7 @@ def test_yield_line_requirement_with_spaces_in_specifier( ): line = 'SomeProject >= 2' filename = 'filename' - comes_from = f'-r {filename} (line {1})' + comes_from = f'-r {filename} (line 1)' req = install_req_from_line(line, comes_from=comes_from) assert repr(line_processor(line, filename, 1)[0]) == repr(req) assert str(req.req.specifier) == '>=2' @@ -264,7 +264,7 @@ def test_yield_editable_requirement(self, line_processor): url = 'git+https://url#egg=SomeProject' line = f'-e {url}' filename = 'filename' - comes_from = f'-r {filename} (line {1})' + comes_from = f'-r {filename} (line 1)' req = install_req_from_editable(url, comes_from=comes_from) assert repr(line_processor(line, filename, 1)[0]) == repr(req) From c348c8fbadcae9450ba54c0013e90c82761dd4e9 Mon Sep 17 00:00:00 2001 From: Andrey Bienkowski <hexagonrecursion@gmail.com> Date: Wed, 10 Feb 2021 13:22:35 +0300 Subject: [PATCH 2986/3170] Replace typing.Text with str --- src/pip/_internal/req/req_file.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index 336cd137e4c..4632e0fe9d3 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -34,9 +34,9 @@ __all__ = ['parse_requirements'] -ReqFileLines = Iterator[Tuple[int, Text]] +ReqFileLines = Iterator[Tuple[int, str]] -LineParser = Callable[[Text], Tuple[str, Values]] +LineParser = Callable[[str], Tuple[str, Values]] SCHEME_RE = re.compile(r'^(http|https|file):', re.I) COMMENT_RE = re.compile(r'(^|\s+)#.*$') From 19ba1c5db9f3896f51141b761b14944ad5a25274 Mon Sep 17 00:00:00 2001 From: Andrey Bienkowski <hexagonrecursion@gmail.com> Date: Sat, 20 Feb 2021 21:16:26 +0300 Subject: [PATCH 2987/3170] Lint --- src/pip/_internal/req/req_file.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pip/_internal/req/req_file.py b/src/pip/_internal/req/req_file.py index 4632e0fe9d3..f6bdfd19b7d 100644 --- a/src/pip/_internal/req/req_file.py +++ b/src/pip/_internal/req/req_file.py @@ -17,7 +17,6 @@ List, NoReturn, Optional, - Text, Tuple, ) From 94441ff2e87583ec7d189b7a6c8a931ca8ec6298 Mon Sep 17 00:00:00 2001 From: Andrey Bienkowski <hexagonrecursion@gmail.com> Date: Sat, 20 Feb 2021 21:17:47 +0300 Subject: [PATCH 2988/3170] Use the new typehint syntax --- src/pip/_internal/commands/search.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/commands/search.py b/src/pip/_internal/commands/search.py index 6fcc9354fe8..ab5cdc1443c 100644 --- a/src/pip/_internal/commands/search.py +++ b/src/pip/_internal/commands/search.py @@ -25,10 +25,10 @@ if TYPE_CHECKING: from typing import TypedDict - TransformedHit = TypedDict( - 'TransformedHit', - {'name': str, 'summary': str, 'versions': List[str]}, - ) + class TransformedHit(TypedDict): + name: str + summary: str + versions: List[str] logger = logging.getLogger(__name__) From 8add29f9605725d6b09784ca0fa8218c16829826 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E2=80=AErekc=C3=A4H=20nitraM=E2=80=AE?= <spamfaenger@gmx.de> Date: Sat, 20 Feb 2021 20:19:08 +0100 Subject: [PATCH 2989/3170] Better formatting Thanks @webknjaz Co-authored-by: Sviatoslav Sydorenko <wk.cvs.github@sydorenko.org.ua> --- news/4390.bugfix.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/4390.bugfix.rst b/news/4390.bugfix.rst index 9f0afb5bb6f..0d84de5cf48 100644 --- a/news/4390.bugfix.rst +++ b/news/4390.bugfix.rst @@ -1 +1 @@ -Fix --target to work with --editable installs. +Fixed ``--target`` to work with ``--editable`` installs. From 7b2548905db91b584b5f8a11e7d3c87bf807faae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Martin=20Ha=CC=88cker?= <mhaecker@mac.com> Date: Sat, 20 Feb 2021 20:17:59 +0100 Subject: [PATCH 2990/3170] Remove dead code --- src/pip/_internal/utils/setuptools_build.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pip/_internal/utils/setuptools_build.py b/src/pip/_internal/utils/setuptools_build.py index 0458a4d0484..d6eff99384f 100644 --- a/src/pip/_internal/utils/setuptools_build.py +++ b/src/pip/_internal/utils/setuptools_build.py @@ -103,7 +103,6 @@ def make_setuptools_develop_args( if prefix: args += ["--prefix", prefix] if home is not None: - # args += ["--home", home] args += ["--install-dir", home] if use_user_site: From 64ecfc8476fc74e524e2be110ae86b13a9ee9a17 Mon Sep 17 00:00:00 2001 From: cjc7373 <niuchangcun@gmail.com> Date: Wed, 10 Feb 2021 21:48:53 +0800 Subject: [PATCH 2991/3170] switch from 'retrying' to 'tenacity' --- news/tenacity.vendor.rst | 1 + pyproject.toml | 2 + src/pip/_internal/utils/filesystem.py | 7 +- src/pip/_internal/utils/misc.py | 8 +- src/pip/_vendor/__init__.py | 2 +- src/pip/_vendor/retrying.py | 267 --------- src/pip/_vendor/retrying.pyi | 1 - src/pip/_vendor/tenacity.pyi | 1 + .../{retrying.LICENSE => tenacity/LICENSE} | 0 src/pip/_vendor/tenacity/__init__.py | 516 ++++++++++++++++++ src/pip/_vendor/tenacity/_asyncio.py | 85 +++ src/pip/_vendor/tenacity/_utils.py | 154 ++++++ src/pip/_vendor/tenacity/after.py | 35 ++ src/pip/_vendor/tenacity/before.py | 32 ++ src/pip/_vendor/tenacity/before_sleep.py | 46 ++ src/pip/_vendor/tenacity/compat.py | 322 +++++++++++ src/pip/_vendor/tenacity/nap.py | 40 ++ src/pip/_vendor/tenacity/py.typed | 0 src/pip/_vendor/tenacity/retry.py | 193 +++++++ src/pip/_vendor/tenacity/stop.py | 103 ++++ src/pip/_vendor/tenacity/tornadoweb.py | 53 ++ src/pip/_vendor/tenacity/wait.py | 195 +++++++ src/pip/_vendor/vendor.txt | 2 +- 23 files changed, 1786 insertions(+), 279 deletions(-) create mode 100644 news/tenacity.vendor.rst delete mode 100644 src/pip/_vendor/retrying.py delete mode 100644 src/pip/_vendor/retrying.pyi create mode 100644 src/pip/_vendor/tenacity.pyi rename src/pip/_vendor/{retrying.LICENSE => tenacity/LICENSE} (100%) create mode 100644 src/pip/_vendor/tenacity/__init__.py create mode 100644 src/pip/_vendor/tenacity/_asyncio.py create mode 100644 src/pip/_vendor/tenacity/_utils.py create mode 100644 src/pip/_vendor/tenacity/after.py create mode 100644 src/pip/_vendor/tenacity/before.py create mode 100644 src/pip/_vendor/tenacity/before_sleep.py create mode 100644 src/pip/_vendor/tenacity/compat.py create mode 100644 src/pip/_vendor/tenacity/nap.py create mode 100644 src/pip/_vendor/tenacity/py.typed create mode 100644 src/pip/_vendor/tenacity/retry.py create mode 100644 src/pip/_vendor/tenacity/stop.py create mode 100644 src/pip/_vendor/tenacity/tornadoweb.py create mode 100644 src/pip/_vendor/tenacity/wait.py diff --git a/news/tenacity.vendor.rst b/news/tenacity.vendor.rst new file mode 100644 index 00000000000..b6938a56df0 --- /dev/null +++ b/news/tenacity.vendor.rst @@ -0,0 +1 @@ +Switch from retrying to tenacity diff --git a/pyproject.toml b/pyproject.toml index 281594b21c7..073362cebb6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -44,6 +44,8 @@ drop = [ "setuptools", "pkg_resources/_vendor/", "pkg_resources/extern/", + # unneeded part for tenacity + "tenacity/tests", ] [tool.vendoring.typing-stubs] diff --git a/src/pip/_internal/utils/filesystem.py b/src/pip/_internal/utils/filesystem.py index e9aa97685a3..14553d377c6 100644 --- a/src/pip/_internal/utils/filesystem.py +++ b/src/pip/_internal/utils/filesystem.py @@ -9,9 +9,7 @@ from tempfile import NamedTemporaryFile from typing import Any, BinaryIO, Iterator, List, Union, cast -# 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.tenacity import retry, stop_after_delay, wait_fixed from pip._internal.utils.compat import get_path_uid from pip._internal.utils.misc import format_size @@ -101,7 +99,8 @@ def adjacent_tmp_file(path, **kwargs): os.fsync(result.fileno()) -_replace_retry = retry(stop_max_delay=1000, wait_fixed=250) +# Tenacity raises RetryError by default, explictly raise the original exception +_replace_retry = retry(reraise=True, stop=stop_after_delay(1), wait=wait_fixed(0.25)) replace = _replace_retry(os.replace) diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index b3650084818..3e5f1753e1d 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -31,10 +31,7 @@ ) from pip._vendor.pkg_resources import Distribution - -# 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.tenacity import retry, stop_after_delay, wait_fixed from pip import __version__ from pip._internal.exceptions import CommandError @@ -117,7 +114,8 @@ def get_prog(): # Retry every half second for up to 3 seconds -@retry(stop_max_delay=3000, wait_fixed=500) +# Tenacity raises RetryError by default, explictly raise the original exception +@retry(reraise=True, stop=stop_after_delay(3), wait=wait_fixed(0.5)) def rmtree(dir, ignore_errors=False): # type: (AnyStr, bool) -> None shutil.rmtree(dir, ignore_errors=ignore_errors, diff --git a/src/pip/_vendor/__init__.py b/src/pip/_vendor/__init__.py index 0abe99d4c98..a10ecd6074a 100644 --- a/src/pip/_vendor/__init__.py +++ b/src/pip/_vendor/__init__.py @@ -75,7 +75,6 @@ def vendored(modulename): vendored("pep517") vendored("pkg_resources") vendored("progress") - vendored("retrying") vendored("requests") vendored("requests.exceptions") vendored("requests.packages") @@ -107,6 +106,7 @@ def vendored(modulename): vendored("requests.packages.urllib3.util.timeout") vendored("requests.packages.urllib3.util.url") vendored("resolvelib") + vendored("tenacity") vendored("toml") vendored("toml.encoder") vendored("toml.decoder") diff --git a/src/pip/_vendor/retrying.py b/src/pip/_vendor/retrying.py deleted file mode 100644 index 6d1e627aae8..00000000000 --- a/src/pip/_vendor/retrying.py +++ /dev/null @@ -1,267 +0,0 @@ -## Copyright 2013-2014 Ray Holder -## -## Licensed under the Apache License, Version 2.0 (the "License"); -## you may not use this file except in compliance with the License. -## You may obtain a copy of the License at -## -## http://www.apache.org/licenses/LICENSE-2.0 -## -## Unless required by applicable law or agreed to in writing, software -## distributed under the License is distributed on an "AS IS" BASIS, -## WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -## See the License for the specific language governing permissions and -## limitations under the License. - -import random -from pip._vendor import six -import sys -import time -import traceback - - -# sys.maxint / 2, since Python 3.2 doesn't have a sys.maxint... -MAX_WAIT = 1073741823 - - -def retry(*dargs, **dkw): - """ - Decorator function that instantiates the Retrying object - @param *dargs: positional arguments passed to Retrying object - @param **dkw: keyword arguments passed to the Retrying object - """ - # support both @retry and @retry() as valid syntax - if len(dargs) == 1 and callable(dargs[0]): - def wrap_simple(f): - - @six.wraps(f) - def wrapped_f(*args, **kw): - return Retrying().call(f, *args, **kw) - - return wrapped_f - - return wrap_simple(dargs[0]) - - else: - def wrap(f): - - @six.wraps(f) - def wrapped_f(*args, **kw): - return Retrying(*dargs, **dkw).call(f, *args, **kw) - - return wrapped_f - - return wrap - - -class Retrying(object): - - def __init__(self, - stop=None, wait=None, - stop_max_attempt_number=None, - stop_max_delay=None, - wait_fixed=None, - wait_random_min=None, wait_random_max=None, - wait_incrementing_start=None, wait_incrementing_increment=None, - wait_exponential_multiplier=None, wait_exponential_max=None, - retry_on_exception=None, - retry_on_result=None, - wrap_exception=False, - stop_func=None, - wait_func=None, - wait_jitter_max=None): - - self._stop_max_attempt_number = 5 if stop_max_attempt_number is None else stop_max_attempt_number - self._stop_max_delay = 100 if stop_max_delay is None else stop_max_delay - self._wait_fixed = 1000 if wait_fixed is None else wait_fixed - self._wait_random_min = 0 if wait_random_min is None else wait_random_min - self._wait_random_max = 1000 if wait_random_max is None else wait_random_max - self._wait_incrementing_start = 0 if wait_incrementing_start is None else wait_incrementing_start - self._wait_incrementing_increment = 100 if wait_incrementing_increment is None else wait_incrementing_increment - self._wait_exponential_multiplier = 1 if wait_exponential_multiplier is None else wait_exponential_multiplier - self._wait_exponential_max = MAX_WAIT if wait_exponential_max is None else wait_exponential_max - self._wait_jitter_max = 0 if wait_jitter_max is None else wait_jitter_max - - # TODO add chaining of stop behaviors - # stop behavior - stop_funcs = [] - if stop_max_attempt_number is not None: - stop_funcs.append(self.stop_after_attempt) - - if stop_max_delay is not None: - stop_funcs.append(self.stop_after_delay) - - if stop_func is not None: - self.stop = stop_func - - elif stop is None: - self.stop = lambda attempts, delay: any(f(attempts, delay) for f in stop_funcs) - - else: - self.stop = getattr(self, stop) - - # TODO add chaining of wait behaviors - # wait behavior - wait_funcs = [lambda *args, **kwargs: 0] - if wait_fixed is not None: - wait_funcs.append(self.fixed_sleep) - - if wait_random_min is not None or wait_random_max is not None: - wait_funcs.append(self.random_sleep) - - if wait_incrementing_start is not None or wait_incrementing_increment is not None: - wait_funcs.append(self.incrementing_sleep) - - if wait_exponential_multiplier is not None or wait_exponential_max is not None: - wait_funcs.append(self.exponential_sleep) - - if wait_func is not None: - self.wait = wait_func - - elif wait is None: - self.wait = lambda attempts, delay: max(f(attempts, delay) for f in wait_funcs) - - else: - self.wait = getattr(self, wait) - - # retry on exception filter - if retry_on_exception is None: - self._retry_on_exception = self.always_reject - else: - self._retry_on_exception = retry_on_exception - - # TODO simplify retrying by Exception types - # retry on result filter - if retry_on_result is None: - self._retry_on_result = self.never_reject - else: - self._retry_on_result = retry_on_result - - self._wrap_exception = wrap_exception - - def stop_after_attempt(self, previous_attempt_number, delay_since_first_attempt_ms): - """Stop after the previous attempt >= stop_max_attempt_number.""" - return previous_attempt_number >= self._stop_max_attempt_number - - def stop_after_delay(self, previous_attempt_number, delay_since_first_attempt_ms): - """Stop after the time from the first attempt >= stop_max_delay.""" - return delay_since_first_attempt_ms >= self._stop_max_delay - - def no_sleep(self, previous_attempt_number, delay_since_first_attempt_ms): - """Don't sleep at all before retrying.""" - return 0 - - def fixed_sleep(self, previous_attempt_number, delay_since_first_attempt_ms): - """Sleep a fixed amount of time between each retry.""" - return self._wait_fixed - - def random_sleep(self, previous_attempt_number, delay_since_first_attempt_ms): - """Sleep a random amount of time between wait_random_min and wait_random_max""" - return random.randint(self._wait_random_min, self._wait_random_max) - - def incrementing_sleep(self, previous_attempt_number, delay_since_first_attempt_ms): - """ - Sleep an incremental amount of time after each attempt, starting at - wait_incrementing_start and incrementing by wait_incrementing_increment - """ - result = self._wait_incrementing_start + (self._wait_incrementing_increment * (previous_attempt_number - 1)) - if result < 0: - result = 0 - return result - - def exponential_sleep(self, previous_attempt_number, delay_since_first_attempt_ms): - exp = 2 ** previous_attempt_number - result = self._wait_exponential_multiplier * exp - if result > self._wait_exponential_max: - result = self._wait_exponential_max - if result < 0: - result = 0 - return result - - def never_reject(self, result): - return False - - def always_reject(self, result): - return True - - def should_reject(self, attempt): - reject = False - if attempt.has_exception: - reject |= self._retry_on_exception(attempt.value[1]) - else: - reject |= self._retry_on_result(attempt.value) - - return reject - - def call(self, fn, *args, **kwargs): - start_time = int(round(time.time() * 1000)) - attempt_number = 1 - while True: - try: - attempt = Attempt(fn(*args, **kwargs), attempt_number, False) - except: - tb = sys.exc_info() - attempt = Attempt(tb, attempt_number, True) - - if not self.should_reject(attempt): - return attempt.get(self._wrap_exception) - - delay_since_first_attempt_ms = int(round(time.time() * 1000)) - start_time - if self.stop(attempt_number, delay_since_first_attempt_ms): - if not self._wrap_exception and attempt.has_exception: - # get() on an attempt with an exception should cause it to be raised, but raise just in case - raise attempt.get() - else: - raise RetryError(attempt) - else: - sleep = self.wait(attempt_number, delay_since_first_attempt_ms) - if self._wait_jitter_max: - jitter = random.random() * self._wait_jitter_max - sleep = sleep + max(0, jitter) - time.sleep(sleep / 1000.0) - - attempt_number += 1 - - -class Attempt(object): - """ - An Attempt encapsulates a call to a target function that may end as a - normal return value from the function or an Exception depending on what - occurred during the execution. - """ - - def __init__(self, value, attempt_number, has_exception): - self.value = value - self.attempt_number = attempt_number - self.has_exception = has_exception - - def get(self, wrap_exception=False): - """ - Return the return value of this Attempt instance or raise an Exception. - If wrap_exception is true, this Attempt is wrapped inside of a - RetryError before being raised. - """ - if self.has_exception: - if wrap_exception: - raise RetryError(self) - else: - six.reraise(self.value[0], self.value[1], self.value[2]) - else: - return self.value - - def __repr__(self): - if self.has_exception: - return "Attempts: {0}, Error:\n{1}".format(self.attempt_number, "".join(traceback.format_tb(self.value[2]))) - else: - return "Attempts: {0}, Value: {1}".format(self.attempt_number, self.value) - - -class RetryError(Exception): - """ - A RetryError encapsulates the last Attempt instance right before giving up. - """ - - def __init__(self, last_attempt): - self.last_attempt = last_attempt - - def __str__(self): - return "RetryError[{0}]".format(self.last_attempt) diff --git a/src/pip/_vendor/retrying.pyi b/src/pip/_vendor/retrying.pyi deleted file mode 100644 index 90f20c6dbc1..00000000000 --- a/src/pip/_vendor/retrying.pyi +++ /dev/null @@ -1 +0,0 @@ -from retrying import * \ No newline at end of file diff --git a/src/pip/_vendor/tenacity.pyi b/src/pip/_vendor/tenacity.pyi new file mode 100644 index 00000000000..baf1de9dd9f --- /dev/null +++ b/src/pip/_vendor/tenacity.pyi @@ -0,0 +1 @@ +from tenacity import * \ No newline at end of file diff --git a/src/pip/_vendor/retrying.LICENSE b/src/pip/_vendor/tenacity/LICENSE similarity index 100% rename from src/pip/_vendor/retrying.LICENSE rename to src/pip/_vendor/tenacity/LICENSE diff --git a/src/pip/_vendor/tenacity/__init__.py b/src/pip/_vendor/tenacity/__init__.py new file mode 100644 index 00000000000..339ca4c7283 --- /dev/null +++ b/src/pip/_vendor/tenacity/__init__.py @@ -0,0 +1,516 @@ +# -*- coding: utf-8 -*- +# Copyright 2016-2018 Julien Danjou +# Copyright 2017 Elisey Zanko +# Copyright 2016 Étienne Bersac +# Copyright 2016 Joshua Harlow +# Copyright 2013-2014 Ray Holder +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +try: + from inspect import iscoroutinefunction +except ImportError: + iscoroutinefunction = None + +try: + import tornado +except ImportError: + tornado = None + +import sys +import threading +import typing as t +import warnings +from abc import ABCMeta, abstractmethod +from concurrent import futures + + +from pip._vendor import six + +from pip._vendor.tenacity import _utils +from pip._vendor.tenacity import compat as _compat + +# Import all built-in retry strategies for easier usage. +from .retry import retry_all # noqa +from .retry import retry_always # noqa +from .retry import retry_any # noqa +from .retry import retry_if_exception # noqa +from .retry import retry_if_exception_type # noqa +from .retry import retry_if_not_result # noqa +from .retry import retry_if_result # noqa +from .retry import retry_never # noqa +from .retry import retry_unless_exception_type # noqa +from .retry import retry_if_exception_message # noqa +from .retry import retry_if_not_exception_message # noqa + +# Import all nap strategies for easier usage. +from .nap import sleep # noqa +from .nap import sleep_using_event # noqa + +# Import all built-in stop strategies for easier usage. +from .stop import stop_after_attempt # noqa +from .stop import stop_after_delay # noqa +from .stop import stop_all # noqa +from .stop import stop_any # noqa +from .stop import stop_never # noqa +from .stop import stop_when_event_set # noqa + +# Import all built-in wait strategies for easier usage. +from .wait import wait_chain # noqa +from .wait import wait_combine # noqa +from .wait import wait_exponential # noqa +from .wait import wait_fixed # noqa +from .wait import wait_incrementing # noqa +from .wait import wait_none # noqa +from .wait import wait_random # noqa +from .wait import wait_random_exponential # noqa +from .wait import wait_random_exponential as wait_full_jitter # noqa + +# Import all built-in before strategies for easier usage. +from .before import before_log # noqa +from .before import before_nothing # noqa + +# Import all built-in after strategies for easier usage. +from .after import after_log # noqa +from .after import after_nothing # noqa + +# Import all built-in after strategies for easier usage. +from .before_sleep import before_sleep_log # noqa +from .before_sleep import before_sleep_nothing # noqa + + +WrappedFn = t.TypeVar("WrappedFn", bound=t.Callable) + + +@t.overload +def retry(fn): + # type: (WrappedFn) -> WrappedFn + """Type signature for @retry as a raw decorator.""" + pass + + +@t.overload +def retry(*dargs, **dkw): # noqa + # type: (...) -> t.Callable[[WrappedFn], WrappedFn] + """Type signature for the @retry() decorator constructor.""" + pass + + +def retry(*dargs, **dkw): # noqa + """Wrap a function with a new `Retrying` object. + + :param dargs: positional arguments passed to Retrying object + :param dkw: keyword arguments passed to the Retrying object + """ + # support both @retry and @retry() as valid syntax + if len(dargs) == 1 and callable(dargs[0]): + return retry()(dargs[0]) + else: + def wrap(f): + if iscoroutinefunction is not None and iscoroutinefunction(f): + r = AsyncRetrying(*dargs, **dkw) + elif tornado and hasattr(tornado.gen, 'is_coroutine_function') \ + and tornado.gen.is_coroutine_function(f): + r = TornadoRetrying(*dargs, **dkw) + else: + r = Retrying(*dargs, **dkw) + + return r.wraps(f) + + return wrap + + +class TryAgain(Exception): + """Always retry the executed function when raised.""" + + +NO_RESULT = object() + + +class DoAttempt(object): + pass + + +class DoSleep(float): + pass + + +class BaseAction(object): + """Base class for representing actions to take by retry object. + + Concrete implementations must define: + - __init__: to initialize all necessary fields + - REPR_ATTRS: class variable specifying attributes to include in repr(self) + - NAME: for identification in retry object methods and callbacks + """ + + REPR_FIELDS = () + NAME = None + + def __repr__(self): + state_str = ', '.join('%s=%r' % (field, getattr(self, field)) + for field in self.REPR_FIELDS) + return '%s(%s)' % (type(self).__name__, state_str) + + def __str__(self): + return repr(self) + + +class RetryAction(BaseAction): + REPR_FIELDS = ('sleep',) + NAME = 'retry' + + def __init__(self, sleep): + self.sleep = float(sleep) + + +_unset = object() + + +class RetryError(Exception): + """Encapsulates the last attempt instance right before giving up.""" + + def __init__(self, last_attempt): + self.last_attempt = last_attempt + super(RetryError, self).__init__(last_attempt) + + def reraise(self): + if self.last_attempt.failed: + raise self.last_attempt.result() + raise self + + def __str__(self): + return "{0}[{1}]".format(self.__class__.__name__, self.last_attempt) + + +class AttemptManager(object): + """Manage attempt context.""" + + def __init__(self, retry_state): + self.retry_state = retry_state + + def __enter__(self): + pass + + def __exit__(self, exc_type, exc_value, traceback): + if isinstance(exc_value, BaseException): + self.retry_state.set_exception((exc_type, exc_value, traceback)) + return True # Swallow exception. + else: + # We don't have the result, actually. + self.retry_state.set_result(None) + + +class BaseRetrying(object): + __metaclass__ = ABCMeta + + def __init__(self, + sleep=sleep, + stop=stop_never, wait=wait_none(), + retry=retry_if_exception_type(), + before=before_nothing, + after=after_nothing, + before_sleep=None, + reraise=False, + retry_error_cls=RetryError, + retry_error_callback=None): + self.sleep = sleep + self._stop = stop + self._wait = wait + self._retry = retry + self._before = before + self._after = after + self._before_sleep = before_sleep + self.reraise = reraise + self._local = threading.local() + self.retry_error_cls = retry_error_cls + self._retry_error_callback = retry_error_callback + + # This attribute was moved to RetryCallState and is deprecated on + # Retrying objects but kept for backward compatibility. + self.fn = None + + @_utils.cached_property + def stop(self): + return _compat.stop_func_accept_retry_state(self._stop) + + @_utils.cached_property + def wait(self): + return _compat.wait_func_accept_retry_state(self._wait) + + @_utils.cached_property + def retry(self): + return _compat.retry_func_accept_retry_state(self._retry) + + @_utils.cached_property + def before(self): + return _compat.before_func_accept_retry_state(self._before) + + @_utils.cached_property + def after(self): + return _compat.after_func_accept_retry_state(self._after) + + @_utils.cached_property + def before_sleep(self): + return _compat.before_sleep_func_accept_retry_state(self._before_sleep) + + @_utils.cached_property + def retry_error_callback(self): + return _compat.retry_error_callback_accept_retry_state( + self._retry_error_callback) + + def copy(self, sleep=_unset, stop=_unset, wait=_unset, + retry=_unset, before=_unset, after=_unset, before_sleep=_unset, + reraise=_unset): + """Copy this object with some parameters changed if needed.""" + if before_sleep is _unset: + before_sleep = self.before_sleep + return self.__class__( + sleep=self.sleep if sleep is _unset else sleep, + stop=self.stop if stop is _unset else stop, + wait=self.wait if wait is _unset else wait, + retry=self.retry if retry is _unset else retry, + before=self.before if before is _unset else before, + after=self.after if after is _unset else after, + before_sleep=before_sleep, + reraise=self.reraise if after is _unset else reraise, + ) + + def __repr__(self): + attrs = dict( + _utils.visible_attrs(self, attrs={'me': id(self)}), + __class__=self.__class__.__name__, + ) + return ("<%(__class__)s object at 0x%(me)x (stop=%(stop)s, " + "wait=%(wait)s, sleep=%(sleep)s, retry=%(retry)s, " + "before=%(before)s, after=%(after)s)>") % (attrs) + + @property + def statistics(self): + """Return a dictionary of runtime statistics. + + This dictionary will be empty when the controller has never been + ran. When it is running or has ran previously it should have (but + may not) have useful and/or informational keys and values when + running is underway and/or completed. + + .. warning:: The keys in this dictionary **should** be some what + stable (not changing), but there existence **may** + change between major releases as new statistics are + gathered or removed so before accessing keys ensure that + they actually exist and handle when they do not. + + .. note:: The values in this dictionary are local to the thread + running call (so if multiple threads share the same retrying + object - either directly or indirectly) they will each have + there own view of statistics they have collected (in the + future we may provide a way to aggregate the various + statistics from each thread). + """ + try: + return self._local.statistics + except AttributeError: + self._local.statistics = {} + return self._local.statistics + + def wraps(self, f): + """Wrap a function for retrying. + + :param f: A function to wraps for retrying. + """ + @_utils.wraps(f) + def wrapped_f(*args, **kw): + return self(f, *args, **kw) + + def retry_with(*args, **kwargs): + return self.copy(*args, **kwargs).wraps(f) + + wrapped_f.retry = self + wrapped_f.retry_with = retry_with + + return wrapped_f + + def begin(self, fn): + self.statistics.clear() + self.statistics['start_time'] = _utils.now() + self.statistics['attempt_number'] = 1 + self.statistics['idle_for'] = 0 + self.fn = fn + + def iter(self, retry_state): # noqa + fut = retry_state.outcome + if fut is None: + if self.before is not None: + self.before(retry_state) + return DoAttempt() + + is_explicit_retry = retry_state.outcome.failed \ + and isinstance(retry_state.outcome.exception(), TryAgain) + if not (is_explicit_retry or self.retry(retry_state=retry_state)): + return fut.result() + + if self.after is not None: + self.after(retry_state=retry_state) + + self.statistics['delay_since_first_attempt'] = \ + retry_state.seconds_since_start + if self.stop(retry_state=retry_state): + if self.retry_error_callback: + return self.retry_error_callback(retry_state=retry_state) + retry_exc = self.retry_error_cls(fut) + if self.reraise: + raise retry_exc.reraise() + six.raise_from(retry_exc, fut.exception()) + + if self.wait: + sleep = self.wait(retry_state=retry_state) + else: + sleep = 0.0 + retry_state.next_action = RetryAction(sleep) + retry_state.idle_for += sleep + self.statistics['idle_for'] += sleep + self.statistics['attempt_number'] += 1 + + if self.before_sleep is not None: + self.before_sleep(retry_state=retry_state) + + return DoSleep(sleep) + + def __iter__(self): + self.begin(None) + + retry_state = RetryCallState(self, fn=None, args=(), kwargs={}) + while True: + do = self.iter(retry_state=retry_state) + if isinstance(do, DoAttempt): + yield AttemptManager(retry_state=retry_state) + elif isinstance(do, DoSleep): + retry_state.prepare_for_next_attempt() + self.sleep(do) + else: + break + + @abstractmethod + def __call__(self, *args, **kwargs): + pass + + def call(self, *args, **kwargs): + """Use ``__call__`` instead because this method is deprecated.""" + warnings.warn("'call()' method is deprecated. " + + "Use '__call__()' instead", DeprecationWarning) + return self.__call__(*args, **kwargs) + + +class Retrying(BaseRetrying): + """Retrying controller.""" + + def __call__(self, fn, *args, **kwargs): + self.begin(fn) + + retry_state = RetryCallState( + retry_object=self, fn=fn, args=args, kwargs=kwargs) + while True: + do = self.iter(retry_state=retry_state) + if isinstance(do, DoAttempt): + try: + result = fn(*args, **kwargs) + except BaseException: + retry_state.set_exception(sys.exc_info()) + else: + retry_state.set_result(result) + elif isinstance(do, DoSleep): + retry_state.prepare_for_next_attempt() + self.sleep(do) + else: + return do + + +class Future(futures.Future): + """Encapsulates a (future or past) attempted call to a target function.""" + + def __init__(self, attempt_number): + super(Future, self).__init__() + self.attempt_number = attempt_number + + @property + def failed(self): + """Return whether a exception is being held in this future.""" + return self.exception() is not None + + @classmethod + def construct(cls, attempt_number, value, has_exception): + """Construct a new Future object.""" + fut = cls(attempt_number) + if has_exception: + fut.set_exception(value) + else: + fut.set_result(value) + return fut + + +class RetryCallState(object): + """State related to a single call wrapped with Retrying.""" + + def __init__(self, retry_object, fn, args, kwargs): + #: Retry call start timestamp + self.start_time = _utils.now() + #: Retry manager object + self.retry_object = retry_object + #: Function wrapped by this retry call + self.fn = fn + #: Arguments of the function wrapped by this retry call + self.args = args + #: Keyword arguments of the function wrapped by this retry call + self.kwargs = kwargs + + #: The number of the current attempt + self.attempt_number = 1 + #: Last outcome (result or exception) produced by the function + self.outcome = None + #: Timestamp of the last outcome + self.outcome_timestamp = None + #: Time spent sleeping in retries + self.idle_for = 0 + #: Next action as decided by the retry manager + self.next_action = None + + @property + def seconds_since_start(self): + if self.outcome_timestamp is None: + return None + return self.outcome_timestamp - self.start_time + + def prepare_for_next_attempt(self): + self.outcome = None + self.outcome_timestamp = None + self.attempt_number += 1 + self.next_action = None + + def set_result(self, val): + ts = _utils.now() + fut = Future(self.attempt_number) + fut.set_result(val) + self.outcome, self.outcome_timestamp = fut, ts + + def set_exception(self, exc_info): + ts = _utils.now() + fut = Future(self.attempt_number) + _utils.capture(fut, exc_info) + self.outcome, self.outcome_timestamp = fut, ts + + +if iscoroutinefunction: + from pip._vendor.tenacity._asyncio import AsyncRetrying + +if tornado: + from pip._vendor.tenacity.tornadoweb import TornadoRetrying diff --git a/src/pip/_vendor/tenacity/_asyncio.py b/src/pip/_vendor/tenacity/_asyncio.py new file mode 100644 index 00000000000..51e348a3dc4 --- /dev/null +++ b/src/pip/_vendor/tenacity/_asyncio.py @@ -0,0 +1,85 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 Étienne Bersac +# Copyright 2016 Julien Danjou +# Copyright 2016 Joshua Harlow +# Copyright 2013-2014 Ray Holder +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import sys +from asyncio import sleep + +from pip._vendor.tenacity import AttemptManager +from pip._vendor.tenacity import BaseRetrying +from pip._vendor.tenacity import DoAttempt +from pip._vendor.tenacity import DoSleep +from pip._vendor.tenacity import RetryCallState + + +class AsyncRetrying(BaseRetrying): + + def __init__(self, + sleep=sleep, + **kwargs): + super(AsyncRetrying, self).__init__(**kwargs) + self.sleep = sleep + + async def __call__(self, fn, *args, **kwargs): + self.begin(fn) + + retry_state = RetryCallState( + retry_object=self, fn=fn, args=args, kwargs=kwargs) + while True: + do = self.iter(retry_state=retry_state) + if isinstance(do, DoAttempt): + try: + result = await fn(*args, **kwargs) + except BaseException: + retry_state.set_exception(sys.exc_info()) + else: + retry_state.set_result(result) + elif isinstance(do, DoSleep): + retry_state.prepare_for_next_attempt() + await self.sleep(do) + else: + return do + + def __aiter__(self): + self.begin(None) + self._retry_state = RetryCallState(self, fn=None, args=(), kwargs={}) + return self + + async def __anext__(self): + while True: + do = self.iter(retry_state=self._retry_state) + if do is None: + raise StopAsyncIteration + elif isinstance(do, DoAttempt): + return AttemptManager(retry_state=self._retry_state) + elif isinstance(do, DoSleep): + self._retry_state.prepare_for_next_attempt() + await self.sleep(do) + else: + return do + + def wraps(self, fn): + fn = super().wraps(fn) + # Ensure wrapper is recognized as a coroutine function. + + async def async_wrapped(*args, **kwargs): + return await fn(*args, **kwargs) + + # Preserve attributes + async_wrapped.retry = fn.retry + async_wrapped.retry_with = fn.retry_with + + return async_wrapped diff --git a/src/pip/_vendor/tenacity/_utils.py b/src/pip/_vendor/tenacity/_utils.py new file mode 100644 index 00000000000..365b11d4b16 --- /dev/null +++ b/src/pip/_vendor/tenacity/_utils.py @@ -0,0 +1,154 @@ +# Copyright 2016 Julien Danjou +# Copyright 2016 Joshua Harlow +# Copyright 2013-2014 Ray Holder +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import inspect +import sys +import time +from functools import update_wrapper + +from pip._vendor import six + +# sys.maxint / 2, since Python 3.2 doesn't have a sys.maxint... +try: + MAX_WAIT = sys.maxint / 2 +except AttributeError: + MAX_WAIT = 1073741823 + + +if six.PY2: + from functools import WRAPPER_ASSIGNMENTS, WRAPPER_UPDATES + + def wraps(fn): + """Do the same as six.wraps but only copy attributes that exist. + + For example, object instances don't have __name__ attribute, so + six.wraps fails. This is fixed in Python 3 + (https://bugs.python.org/issue3445), but didn't get backported to six. + + Also, see https://github.com/benjaminp/six/issues/250. + """ + def filter_hasattr(obj, attrs): + return tuple(a for a in attrs if hasattr(obj, a)) + return six.wraps( + fn, + assigned=filter_hasattr(fn, WRAPPER_ASSIGNMENTS), + updated=filter_hasattr(fn, WRAPPER_UPDATES)) + + def capture(fut, tb): + # TODO(harlowja): delete this in future, since its + # has to repeatedly calculate this crap. + fut.set_exception_info(tb[1], tb[2]) + + def getargspec(func): + # This was deprecated in Python 3. + return inspect.getargspec(func) +else: + from functools import wraps # noqa + + def capture(fut, tb): + fut.set_exception(tb[1]) + + def getargspec(func): + return inspect.getfullargspec(func) + + +def visible_attrs(obj, attrs=None): + if attrs is None: + attrs = {} + for attr_name, attr in inspect.getmembers(obj): + if attr_name.startswith("_"): + continue + attrs[attr_name] = attr + return attrs + + +def find_ordinal(pos_num): + # See: https://en.wikipedia.org/wiki/English_numerals#Ordinal_numbers + if pos_num == 0: + return "th" + elif pos_num == 1: + return 'st' + elif pos_num == 2: + return 'nd' + elif pos_num == 3: + return 'rd' + elif pos_num >= 4 and pos_num <= 20: + return 'th' + else: + return find_ordinal(pos_num % 10) + + +def to_ordinal(pos_num): + return "%i%s" % (pos_num, find_ordinal(pos_num)) + + +def get_callback_name(cb): + """Get a callback fully-qualified name. + + If no name can be produced ``repr(cb)`` is called and returned. + """ + segments = [] + try: + segments.append(cb.__qualname__) + except AttributeError: + try: + segments.append(cb.__name__) + if inspect.ismethod(cb): + try: + # This attribute doesn't exist on py3.x or newer, so + # we optionally ignore it... (on those versions of + # python `__qualname__` should have been found anyway). + segments.insert(0, cb.im_class.__name__) + except AttributeError: + pass + except AttributeError: + pass + if not segments: + return repr(cb) + else: + try: + # When running under sphinx it appears this can be none? + if cb.__module__: + segments.insert(0, cb.__module__) + except AttributeError: + pass + return ".".join(segments) + + +try: + now = time.monotonic # noqa +except AttributeError: + from monotonic import monotonic as now # noqa + + +class cached_property(object): + """A property that is computed once per instance. + + Upon being computed it replaces itself with an ordinary attribute. Deleting + the attribute resets the property. + + Source: https://github.com/bottlepy/bottle/blob/1de24157e74a6971d136550afe1b63eec5b0df2b/bottle.py#L234-L246 + """ # noqa: E501 + + def __init__(self, func): + update_wrapper(self, func) + self.func = func + + def __get__(self, obj, cls): + if obj is None: + return self + value = obj.__dict__[self.func.__name__] = self.func(obj) + return value diff --git a/src/pip/_vendor/tenacity/after.py b/src/pip/_vendor/tenacity/after.py new file mode 100644 index 00000000000..8b6082c683a --- /dev/null +++ b/src/pip/_vendor/tenacity/after.py @@ -0,0 +1,35 @@ +# Copyright 2016 Julien Danjou +# Copyright 2016 Joshua Harlow +# Copyright 2013-2014 Ray Holder +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pip._vendor.tenacity import _utils + + +def after_nothing(retry_state): + """After call strategy that does nothing.""" + + +def after_log(logger, log_level, sec_format="%0.3f"): + """After call strategy that logs to some logger the finished attempt.""" + log_tpl = ("Finished call to '%s' after " + str(sec_format) + "(s), " + "this was the %s time calling it.") + + def log_it(retry_state): + logger.log(log_level, log_tpl, + _utils.get_callback_name(retry_state.fn), + retry_state.seconds_since_start, + _utils.to_ordinal(retry_state.attempt_number)) + + return log_it diff --git a/src/pip/_vendor/tenacity/before.py b/src/pip/_vendor/tenacity/before.py new file mode 100644 index 00000000000..3eab08afb9d --- /dev/null +++ b/src/pip/_vendor/tenacity/before.py @@ -0,0 +1,32 @@ +# Copyright 2016 Julien Danjou +# Copyright 2016 Joshua Harlow +# Copyright 2013-2014 Ray Holder +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pip._vendor.tenacity import _utils + + +def before_nothing(retry_state): + """Before call strategy that does nothing.""" + + +def before_log(logger, log_level): + """Before call strategy that logs to some logger the attempt.""" + def log_it(retry_state): + logger.log(log_level, + "Starting call to '%s', this is the %s time calling it.", + _utils.get_callback_name(retry_state.fn), + _utils.to_ordinal(retry_state.attempt_number)) + + return log_it diff --git a/src/pip/_vendor/tenacity/before_sleep.py b/src/pip/_vendor/tenacity/before_sleep.py new file mode 100644 index 00000000000..4285922fd2d --- /dev/null +++ b/src/pip/_vendor/tenacity/before_sleep.py @@ -0,0 +1,46 @@ +# Copyright 2016 Julien Danjou +# Copyright 2016 Joshua Harlow +# Copyright 2013-2014 Ray Holder +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +from pip._vendor.tenacity import _utils +from pip._vendor.tenacity.compat import get_exc_info_from_future + + +def before_sleep_nothing(retry_state): + """Before call strategy that does nothing.""" + + +def before_sleep_log(logger, log_level, exc_info=False): + """Before call strategy that logs to some logger the attempt.""" + def log_it(retry_state): + if retry_state.outcome.failed: + ex = retry_state.outcome.exception() + verb, value = 'raised', '%s: %s' % (type(ex).__name__, ex) + + if exc_info: + local_exc_info = get_exc_info_from_future(retry_state.outcome) + else: + local_exc_info = False + else: + verb, value = 'returned', retry_state.outcome.result() + local_exc_info = False # exc_info does not apply when no exception + + logger.log(log_level, + "Retrying %s in %s seconds as it %s %s.", + _utils.get_callback_name(retry_state.fn), + getattr(retry_state.next_action, 'sleep'), + verb, value, + exc_info=local_exc_info) + return log_it diff --git a/src/pip/_vendor/tenacity/compat.py b/src/pip/_vendor/tenacity/compat.py new file mode 100644 index 00000000000..a71ba604813 --- /dev/null +++ b/src/pip/_vendor/tenacity/compat.py @@ -0,0 +1,322 @@ +"""Utilities for providing backward compatibility.""" + +import inspect +from fractions import Fraction +from warnings import warn + +from pip._vendor import six + +from pip._vendor.tenacity import _utils + + +def warn_about_non_retry_state_deprecation(cbname, func, stacklevel): + msg = ( + '"%s" function must accept single "retry_state" parameter,' + ' please update %s' % (cbname, _utils.get_callback_name(func))) + warn(msg, DeprecationWarning, stacklevel=stacklevel + 1) + + +def warn_about_dunder_non_retry_state_deprecation(fn, stacklevel): + msg = ( + '"%s" method must be called with' + ' single "retry_state" parameter' % (_utils.get_callback_name(fn))) + warn(msg, DeprecationWarning, stacklevel=stacklevel + 1) + + +def func_takes_retry_state(func): + if not six.callable(func): + raise Exception(func) + return False + if not inspect.isfunction(func) and not inspect.ismethod(func): + # func is a callable object rather than a function/method + func = func.__call__ + func_spec = _utils.getargspec(func) + return 'retry_state' in func_spec.args + + +_unset = object() + + +def _make_unset_exception(func_name, **kwargs): + missing = [] + for k, v in six.iteritems(kwargs): + if v is _unset: + missing.append(k) + missing_str = ', '.join(repr(s) for s in missing) + return TypeError(func_name + ' func missing parameters: ' + missing_str) + + +def _set_delay_since_start(retry_state, delay): + # Ensure outcome_timestamp - start_time is *exactly* equal to the delay to + # avoid complexity in test code. + retry_state.start_time = Fraction(retry_state.start_time) + retry_state.outcome_timestamp = (retry_state.start_time + Fraction(delay)) + assert retry_state.seconds_since_start == delay + + +def make_retry_state(previous_attempt_number, delay_since_first_attempt, + last_result=None): + """Construct RetryCallState for given attempt number & delay. + + Only used in testing and thus is extra careful about timestamp arithmetics. + """ + required_parameter_unset = (previous_attempt_number is _unset or + delay_since_first_attempt is _unset) + if required_parameter_unset: + raise _make_unset_exception( + 'wait/stop', + previous_attempt_number=previous_attempt_number, + delay_since_first_attempt=delay_since_first_attempt) + + from pip._vendor.tenacity import RetryCallState + retry_state = RetryCallState(None, None, (), {}) + retry_state.attempt_number = previous_attempt_number + if last_result is not None: + retry_state.outcome = last_result + else: + retry_state.set_result(None) + _set_delay_since_start(retry_state, delay_since_first_attempt) + return retry_state + + +def func_takes_last_result(waiter): + """Check if function has a "last_result" parameter. + + Needed to provide backward compatibility for wait functions that didn't + take "last_result" in the beginning. + """ + if not six.callable(waiter): + return False + if not inspect.isfunction(waiter) and not inspect.ismethod(waiter): + # waiter is a class, check dunder-call rather than dunder-init. + waiter = waiter.__call__ + waiter_spec = _utils.getargspec(waiter) + return 'last_result' in waiter_spec.args + + +def stop_dunder_call_accept_old_params(fn): + """Decorate cls.__call__ method to accept old "stop" signature.""" + @_utils.wraps(fn) + def new_fn(self, + previous_attempt_number=_unset, + delay_since_first_attempt=_unset, + retry_state=None): + if retry_state is None: + from pip._vendor.tenacity import RetryCallState + retry_state_passed_as_non_kwarg = ( + previous_attempt_number is not _unset and + isinstance(previous_attempt_number, RetryCallState)) + if retry_state_passed_as_non_kwarg: + retry_state = previous_attempt_number + else: + warn_about_dunder_non_retry_state_deprecation(fn, stacklevel=2) + retry_state = make_retry_state( + previous_attempt_number=previous_attempt_number, + delay_since_first_attempt=delay_since_first_attempt) + return fn(self, retry_state=retry_state) + return new_fn + + +def stop_func_accept_retry_state(stop_func): + """Wrap "stop" function to accept "retry_state" parameter.""" + if not six.callable(stop_func): + return stop_func + + if func_takes_retry_state(stop_func): + return stop_func + + @_utils.wraps(stop_func) + def wrapped_stop_func(retry_state): + warn_about_non_retry_state_deprecation( + 'stop', stop_func, stacklevel=4) + return stop_func( + retry_state.attempt_number, + retry_state.seconds_since_start, + ) + return wrapped_stop_func + + +def wait_dunder_call_accept_old_params(fn): + """Decorate cls.__call__ method to accept old "wait" signature.""" + @_utils.wraps(fn) + def new_fn(self, + previous_attempt_number=_unset, + delay_since_first_attempt=_unset, + last_result=None, + retry_state=None): + if retry_state is None: + from pip._vendor.tenacity import RetryCallState + retry_state_passed_as_non_kwarg = ( + previous_attempt_number is not _unset and + isinstance(previous_attempt_number, RetryCallState)) + if retry_state_passed_as_non_kwarg: + retry_state = previous_attempt_number + else: + warn_about_dunder_non_retry_state_deprecation(fn, stacklevel=2) + retry_state = make_retry_state( + previous_attempt_number=previous_attempt_number, + delay_since_first_attempt=delay_since_first_attempt, + last_result=last_result) + return fn(self, retry_state=retry_state) + return new_fn + + +def wait_func_accept_retry_state(wait_func): + """Wrap wait function to accept "retry_state" parameter.""" + if not six.callable(wait_func): + return wait_func + + if func_takes_retry_state(wait_func): + return wait_func + + if func_takes_last_result(wait_func): + @_utils.wraps(wait_func) + def wrapped_wait_func(retry_state): + warn_about_non_retry_state_deprecation( + 'wait', wait_func, stacklevel=4) + return wait_func( + retry_state.attempt_number, + retry_state.seconds_since_start, + last_result=retry_state.outcome, + ) + else: + @_utils.wraps(wait_func) + def wrapped_wait_func(retry_state): + warn_about_non_retry_state_deprecation( + 'wait', wait_func, stacklevel=4) + return wait_func( + retry_state.attempt_number, + retry_state.seconds_since_start, + ) + return wrapped_wait_func + + +def retry_dunder_call_accept_old_params(fn): + """Decorate cls.__call__ method to accept old "retry" signature.""" + @_utils.wraps(fn) + def new_fn(self, attempt=_unset, retry_state=None): + if retry_state is None: + from pip._vendor.tenacity import RetryCallState + if attempt is _unset: + raise _make_unset_exception('retry', attempt=attempt) + retry_state_passed_as_non_kwarg = ( + attempt is not _unset and + isinstance(attempt, RetryCallState)) + if retry_state_passed_as_non_kwarg: + retry_state = attempt + else: + warn_about_dunder_non_retry_state_deprecation(fn, stacklevel=2) + retry_state = RetryCallState(None, None, (), {}) + retry_state.outcome = attempt + return fn(self, retry_state=retry_state) + return new_fn + + +def retry_func_accept_retry_state(retry_func): + """Wrap "retry" function to accept "retry_state" parameter.""" + if not six.callable(retry_func): + return retry_func + + if func_takes_retry_state(retry_func): + return retry_func + + @_utils.wraps(retry_func) + def wrapped_retry_func(retry_state): + warn_about_non_retry_state_deprecation( + 'retry', retry_func, stacklevel=4) + return retry_func(retry_state.outcome) + return wrapped_retry_func + + +def before_func_accept_retry_state(fn): + """Wrap "before" function to accept "retry_state".""" + if not six.callable(fn): + return fn + + if func_takes_retry_state(fn): + return fn + + @_utils.wraps(fn) + def wrapped_before_func(retry_state): + # func, trial_number, trial_time_taken + warn_about_non_retry_state_deprecation('before', fn, stacklevel=4) + return fn( + retry_state.fn, + retry_state.attempt_number, + ) + return wrapped_before_func + + +def after_func_accept_retry_state(fn): + """Wrap "after" function to accept "retry_state".""" + if not six.callable(fn): + return fn + + if func_takes_retry_state(fn): + return fn + + @_utils.wraps(fn) + def wrapped_after_sleep_func(retry_state): + # func, trial_number, trial_time_taken + warn_about_non_retry_state_deprecation('after', fn, stacklevel=4) + return fn( + retry_state.fn, + retry_state.attempt_number, + retry_state.seconds_since_start) + return wrapped_after_sleep_func + + +def before_sleep_func_accept_retry_state(fn): + """Wrap "before_sleep" function to accept "retry_state".""" + if not six.callable(fn): + return fn + + if func_takes_retry_state(fn): + return fn + + @_utils.wraps(fn) + def wrapped_before_sleep_func(retry_state): + # retry_object, sleep, last_result + warn_about_non_retry_state_deprecation( + 'before_sleep', fn, stacklevel=4) + return fn( + retry_state.retry_object, + sleep=getattr(retry_state.next_action, 'sleep'), + last_result=retry_state.outcome) + return wrapped_before_sleep_func + + +def retry_error_callback_accept_retry_state(fn): + if not six.callable(fn): + return fn + + if func_takes_retry_state(fn): + return fn + + @_utils.wraps(fn) + def wrapped_retry_error_callback(retry_state): + warn_about_non_retry_state_deprecation( + 'retry_error_callback', fn, stacklevel=4) + return fn(retry_state.outcome) + return wrapped_retry_error_callback + + +def get_exc_info_from_future(future): + """ + Get an exc_info value from a Future. + + Given a a Future instance, retrieve an exc_info value suitable for passing + in as the exc_info parameter to logging.Logger.log() and related methods. + + On Python 2, this will be a (type, value, traceback) triple. + On Python 3, this will be an exception instance (with embedded traceback). + + If there was no exception, None is returned on both versions of Python. + """ + if six.PY3: + return future.exception() + else: + ex, tb = future.exception_info() + if ex is None: + return None + return type(ex), ex, tb diff --git a/src/pip/_vendor/tenacity/nap.py b/src/pip/_vendor/tenacity/nap.py new file mode 100644 index 00000000000..83ff839c361 --- /dev/null +++ b/src/pip/_vendor/tenacity/nap.py @@ -0,0 +1,40 @@ +# -*- coding: utf-8 -*- +# Copyright 2016 Étienne Bersac +# Copyright 2016 Julien Danjou +# Copyright 2016 Joshua Harlow +# Copyright 2013-2014 Ray Holder +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import time + + +def sleep(seconds): + """ + Sleep strategy that delays execution for a given number of seconds. + + This is the default strategy, and may be mocked out for unit testing. + """ + time.sleep(seconds) + + +class sleep_using_event(object): + """Sleep strategy that waits on an event to be set.""" + + def __init__(self, event): + self.event = event + + def __call__(self, timeout): + # NOTE(harlowja): this may *not* actually wait for timeout + # seconds if the event is set (ie this may eject out early). + self.event.wait(timeout=timeout) diff --git a/src/pip/_vendor/tenacity/py.typed b/src/pip/_vendor/tenacity/py.typed new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_vendor/tenacity/retry.py b/src/pip/_vendor/tenacity/retry.py new file mode 100644 index 00000000000..7340019ae05 --- /dev/null +++ b/src/pip/_vendor/tenacity/retry.py @@ -0,0 +1,193 @@ +# Copyright 2016 Julien Danjou +# Copyright 2016 Joshua Harlow +# Copyright 2013-2014 Ray Holder +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import abc +import re + +from pip._vendor import six + +from pip._vendor.tenacity import compat as _compat + + +@six.add_metaclass(abc.ABCMeta) +class retry_base(object): + """Abstract base class for retry strategies.""" + + @abc.abstractmethod + def __call__(self, retry_state): + pass + + def __and__(self, other): + return retry_all(self, other) + + def __or__(self, other): + return retry_any(self, other) + + +class _retry_never(retry_base): + """Retry strategy that never rejects any result.""" + + def __call__(self, retry_state): + return False + + +retry_never = _retry_never() + + +class _retry_always(retry_base): + """Retry strategy that always rejects any result.""" + + def __call__(self, retry_state): + return True + + +retry_always = _retry_always() + + +class retry_if_exception(retry_base): + """Retry strategy that retries if an exception verifies a predicate.""" + + def __init__(self, predicate): + self.predicate = predicate + + @_compat.retry_dunder_call_accept_old_params + def __call__(self, retry_state): + if retry_state.outcome.failed: + return self.predicate(retry_state.outcome.exception()) + else: + return False + + +class retry_if_exception_type(retry_if_exception): + """Retries if an exception has been raised of one or more types.""" + + def __init__(self, exception_types=Exception): + self.exception_types = exception_types + super(retry_if_exception_type, self).__init__( + lambda e: isinstance(e, exception_types)) + + +class retry_unless_exception_type(retry_if_exception): + """Retries until an exception is raised of one or more types.""" + + def __init__(self, exception_types=Exception): + self.exception_types = exception_types + super(retry_unless_exception_type, self).__init__( + lambda e: not isinstance(e, exception_types)) + + @_compat.retry_dunder_call_accept_old_params + def __call__(self, retry_state): + # always retry if no exception was raised + if not retry_state.outcome.failed: + return True + return self.predicate(retry_state.outcome.exception()) + + +class retry_if_result(retry_base): + """Retries if the result verifies a predicate.""" + + def __init__(self, predicate): + self.predicate = predicate + + @_compat.retry_dunder_call_accept_old_params + def __call__(self, retry_state): + if not retry_state.outcome.failed: + return self.predicate(retry_state.outcome.result()) + else: + return False + + +class retry_if_not_result(retry_base): + """Retries if the result refutes a predicate.""" + + def __init__(self, predicate): + self.predicate = predicate + + @_compat.retry_dunder_call_accept_old_params + def __call__(self, retry_state): + if not retry_state.outcome.failed: + return not self.predicate(retry_state.outcome.result()) + else: + return False + + +class retry_if_exception_message(retry_if_exception): + """Retries if an exception message equals or matches.""" + + def __init__(self, message=None, match=None): + if message and match: + raise TypeError( + "{}() takes either 'message' or 'match', not both".format( + self.__class__.__name__)) + + # set predicate + if message: + def message_fnc(exception): + return message == str(exception) + predicate = message_fnc + elif match: + prog = re.compile(match) + + def match_fnc(exception): + return prog.match(str(exception)) + predicate = match_fnc + else: + raise TypeError( + "{}() missing 1 required argument 'message' or 'match'". + format(self.__class__.__name__)) + + super(retry_if_exception_message, self).__init__(predicate) + + +class retry_if_not_exception_message(retry_if_exception_message): + """Retries until an exception message equals or matches.""" + + def __init__(self, *args, **kwargs): + super(retry_if_not_exception_message, self).__init__(*args, **kwargs) + # invert predicate + if_predicate = self.predicate + self.predicate = lambda *args_, **kwargs_: not if_predicate( + *args_, **kwargs_) + + @_compat.retry_dunder_call_accept_old_params + def __call__(self, retry_state): + if not retry_state.outcome.failed: + return True + return self.predicate(retry_state.outcome.exception()) + + +class retry_any(retry_base): + """Retries if any of the retries condition is valid.""" + + def __init__(self, *retries): + self.retries = tuple(_compat.retry_func_accept_retry_state(r) + for r in retries) + + @_compat.retry_dunder_call_accept_old_params + def __call__(self, retry_state): + return any(r(retry_state) for r in self.retries) + + +class retry_all(retry_base): + """Retries if all the retries condition are valid.""" + + def __init__(self, *retries): + self.retries = tuple(_compat.retry_func_accept_retry_state(r) + for r in retries) + + @_compat.retry_dunder_call_accept_old_params + def __call__(self, retry_state): + return all(r(retry_state) for r in self.retries) diff --git a/src/pip/_vendor/tenacity/stop.py b/src/pip/_vendor/tenacity/stop.py new file mode 100644 index 00000000000..a00c259b426 --- /dev/null +++ b/src/pip/_vendor/tenacity/stop.py @@ -0,0 +1,103 @@ +# Copyright 2016 Julien Danjou +# Copyright 2016 Joshua Harlow +# Copyright 2013-2014 Ray Holder +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +import abc + +from pip._vendor import six + +from pip._vendor.tenacity import compat as _compat + + +@six.add_metaclass(abc.ABCMeta) +class stop_base(object): + """Abstract base class for stop strategies.""" + + @abc.abstractmethod + def __call__(self, retry_state): + pass + + def __and__(self, other): + return stop_all(self, other) + + def __or__(self, other): + return stop_any(self, other) + + +class stop_any(stop_base): + """Stop if any of the stop condition is valid.""" + + def __init__(self, *stops): + self.stops = tuple(_compat.stop_func_accept_retry_state(stop_func) + for stop_func in stops) + + @_compat.stop_dunder_call_accept_old_params + def __call__(self, retry_state): + return any(x(retry_state) for x in self.stops) + + +class stop_all(stop_base): + """Stop if all the stop conditions are valid.""" + + def __init__(self, *stops): + self.stops = tuple(_compat.stop_func_accept_retry_state(stop_func) + for stop_func in stops) + + @_compat.stop_dunder_call_accept_old_params + def __call__(self, retry_state): + return all(x(retry_state) for x in self.stops) + + +class _stop_never(stop_base): + """Never stop.""" + + @_compat.stop_dunder_call_accept_old_params + def __call__(self, retry_state): + return False + + +stop_never = _stop_never() + + +class stop_when_event_set(stop_base): + """Stop when the given event is set.""" + + def __init__(self, event): + self.event = event + + @_compat.stop_dunder_call_accept_old_params + def __call__(self, retry_state): + return self.event.is_set() + + +class stop_after_attempt(stop_base): + """Stop when the previous attempt >= max_attempt.""" + + def __init__(self, max_attempt_number): + self.max_attempt_number = max_attempt_number + + @_compat.stop_dunder_call_accept_old_params + def __call__(self, retry_state): + return retry_state.attempt_number >= self.max_attempt_number + + +class stop_after_delay(stop_base): + """Stop when the time from the first attempt >= limit.""" + + def __init__(self, max_delay): + self.max_delay = max_delay + + @_compat.stop_dunder_call_accept_old_params + def __call__(self, retry_state): + return retry_state.seconds_since_start >= self.max_delay diff --git a/src/pip/_vendor/tenacity/tornadoweb.py b/src/pip/_vendor/tenacity/tornadoweb.py new file mode 100644 index 00000000000..c31f7ebb76e --- /dev/null +++ b/src/pip/_vendor/tenacity/tornadoweb.py @@ -0,0 +1,53 @@ +# -*- coding: utf-8 -*- +# Copyright 2017 Elisey Zanko +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import sys + +from pip._vendor.tenacity import BaseRetrying +from pip._vendor.tenacity import DoAttempt +from pip._vendor.tenacity import DoSleep +from pip._vendor.tenacity import RetryCallState + +from tornado import gen + + +class TornadoRetrying(BaseRetrying): + + def __init__(self, + sleep=gen.sleep, + **kwargs): + super(TornadoRetrying, self).__init__(**kwargs) + self.sleep = sleep + + @gen.coroutine + def __call__(self, fn, *args, **kwargs): + self.begin(fn) + + retry_state = RetryCallState( + retry_object=self, fn=fn, args=args, kwargs=kwargs) + while True: + do = self.iter(retry_state=retry_state) + if isinstance(do, DoAttempt): + try: + result = yield fn(*args, **kwargs) + except BaseException: + retry_state.set_exception(sys.exc_info()) + else: + retry_state.set_result(result) + elif isinstance(do, DoSleep): + retry_state.prepare_for_next_attempt() + yield self.sleep(do) + else: + raise gen.Return(do) diff --git a/src/pip/_vendor/tenacity/wait.py b/src/pip/_vendor/tenacity/wait.py new file mode 100644 index 00000000000..8ce205e58cf --- /dev/null +++ b/src/pip/_vendor/tenacity/wait.py @@ -0,0 +1,195 @@ +# Copyright 2016 Julien Danjou +# Copyright 2016 Joshua Harlow +# Copyright 2013-2014 Ray Holder +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +import abc +import random + +from pip._vendor import six + +from pip._vendor.tenacity import _utils +from pip._vendor.tenacity import compat as _compat + + +@six.add_metaclass(abc.ABCMeta) +class wait_base(object): + """Abstract base class for wait strategies.""" + + @abc.abstractmethod + def __call__(self, retry_state): + pass + + def __add__(self, other): + return wait_combine(self, other) + + def __radd__(self, other): + # make it possible to use multiple waits with the built-in sum function + if other == 0: + return self + return self.__add__(other) + + +class wait_fixed(wait_base): + """Wait strategy that waits a fixed amount of time between each retry.""" + + def __init__(self, wait): + self.wait_fixed = wait + + @_compat.wait_dunder_call_accept_old_params + def __call__(self, retry_state): + return self.wait_fixed + + +class wait_none(wait_fixed): + """Wait strategy that doesn't wait at all before retrying.""" + + def __init__(self): + super(wait_none, self).__init__(0) + + +class wait_random(wait_base): + """Wait strategy that waits a random amount of time between min/max.""" + + def __init__(self, min=0, max=1): # noqa + self.wait_random_min = min + self.wait_random_max = max + + @_compat.wait_dunder_call_accept_old_params + def __call__(self, retry_state): + return (self.wait_random_min + + (random.random() * + (self.wait_random_max - self.wait_random_min))) + + +class wait_combine(wait_base): + """Combine several waiting strategies.""" + + def __init__(self, *strategies): + self.wait_funcs = tuple(_compat.wait_func_accept_retry_state(strategy) + for strategy in strategies) + + @_compat.wait_dunder_call_accept_old_params + def __call__(self, retry_state): + return sum(x(retry_state=retry_state) for x in self.wait_funcs) + + +class wait_chain(wait_base): + """Chain two or more waiting strategies. + + If all strategies are exhausted, the very last strategy is used + thereafter. + + For example:: + + @retry(wait=wait_chain(*[wait_fixed(1) for i in range(3)] + + [wait_fixed(2) for j in range(5)] + + [wait_fixed(5) for k in range(4))) + def wait_chained(): + print("Wait 1s for 3 attempts, 2s for 5 attempts and 5s + thereafter.") + """ + + def __init__(self, *strategies): + self.strategies = [_compat.wait_func_accept_retry_state(strategy) + for strategy in strategies] + + @_compat.wait_dunder_call_accept_old_params + def __call__(self, retry_state): + wait_func_no = min(max(retry_state.attempt_number, 1), + len(self.strategies)) + wait_func = self.strategies[wait_func_no - 1] + return wait_func(retry_state=retry_state) + + +class wait_incrementing(wait_base): + """Wait an incremental amount of time after each attempt. + + Starting at a starting value and incrementing by a value for each attempt + (and restricting the upper limit to some maximum value). + """ + + def __init__(self, start=0, increment=100, max=_utils.MAX_WAIT): # noqa + self.start = start + self.increment = increment + self.max = max + + @_compat.wait_dunder_call_accept_old_params + def __call__(self, retry_state): + result = self.start + ( + self.increment * (retry_state.attempt_number - 1) + ) + return max(0, min(result, self.max)) + + +class wait_exponential(wait_base): + """Wait strategy that applies exponential backoff. + + It allows for a customized multiplier and an ability to restrict the + upper and lower limits to some maximum and minimum value. + + The intervals are fixed (i.e. there is no jitter), so this strategy is + suitable for balancing retries against latency when a required resource is + unavailable for an unknown duration, but *not* suitable for resolving + contention between multiple processes for a shared resource. Use + wait_random_exponential for the latter case. + """ + + def __init__(self, multiplier=1, max=_utils.MAX_WAIT, exp_base=2, min=0): # noqa + self.multiplier = multiplier + self.min = min + self.max = max + self.exp_base = exp_base + + @_compat.wait_dunder_call_accept_old_params + def __call__(self, retry_state): + try: + exp = self.exp_base ** (retry_state.attempt_number - 1) + result = self.multiplier * exp + except OverflowError: + return self.max + return max(max(0, self.min), min(result, self.max)) + + +class wait_random_exponential(wait_exponential): + """Random wait with exponentially widening window. + + An exponential backoff strategy used to mediate contention between multiple + uncoordinated processes for a shared resource in distributed systems. This + is the sense in which "exponential backoff" is meant in e.g. Ethernet + networking, and corresponds to the "Full Jitter" algorithm described in + this blog post: + + https://aws.amazon.com/blogs/architecture/exponential-backoff-and-jitter/ + + Each retry occurs at a random time in a geometrically expanding interval. + It allows for a custom multiplier and an ability to restrict the upper + limit of the random interval to some maximum value. + + Example:: + + wait_random_exponential(multiplier=0.5, # initial window 0.5s + max=60) # max 60s timeout + + When waiting for an unavailable resource to become available again, as + opposed to trying to resolve contention for a shared resource, the + wait_exponential strategy (which uses a fixed interval) may be preferable. + + """ + + @_compat.wait_dunder_call_accept_old_params + def __call__(self, retry_state): + high = super(wait_random_exponential, self).__call__( + retry_state=retry_state) + return random.uniform(0, high) diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 51a5508479e..0032327a291 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -15,8 +15,8 @@ requests==2.25.1 idna==2.10 urllib3==1.26.2 resolvelib==0.5.4 -retrying==1.3.3 setuptools==44.0.0 six==1.15.0 +tenacity==6.3.1 toml==0.10.2 webencodings==0.5.1 From 51f0fb278112fc7932e3be36bc600b5cdf43b285 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sun, 21 Feb 2021 13:03:38 +0000 Subject: [PATCH 2992/3170] Tweak and blacken noxfile.py --- .pre-commit-config.yaml | 1 - noxfile.py | 30 ++++++++++++++++-------------- 2 files changed, 16 insertions(+), 15 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 163beba71a5..32b4d62ed64 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -46,7 +46,6 @@ repos: ^tests/functional/test_install| # Files in the root of the repository ^setup.py| - ^noxfile.py| # A blank ignore, to avoid merge conflicts later. ^$ diff --git a/noxfile.py b/noxfile.py index 0dd382a24c4..a40dba5b647 100644 --- a/noxfile.py +++ b/noxfile.py @@ -10,9 +10,11 @@ import nox +# fmt: off sys.path.append(".") from tools.automation import release # isort:skip # noqa sys.path.pop() +# fmt: on nox.options.reuse_existing_virtualenvs = True nox.options.sessions = ["lint"] @@ -75,17 +77,16 @@ def test(session): # type: (nox.Session) -> None # Get the common wheels. if should_update_common_wheels(): + # fmt: off run_with_protected_pip( session, "wheel", "-w", LOCATIONS["common-wheels"], "-r", REQUIREMENTS["common-wheels"], ) + # fmt: on else: - msg = ( - "Re-using existing common-wheels at {}." - .format(LOCATIONS["common-wheels"]) - ) + msg = f"Re-using existing common-wheels at {LOCATIONS['common-wheels']}." session.log(msg) # Build source distribution @@ -93,11 +94,14 @@ def test(session): sdist_dir = os.path.join(session.virtualenv.location, "sdist") # type: ignore if os.path.exists(sdist_dir): shutil.rmtree(sdist_dir, ignore_errors=True) + + # fmt: off session.run( - "python", "setup.py", "sdist", - "--formats=zip", "--dist-dir", sdist_dir, + "python", "setup.py", "sdist", "--formats=zip", "--dist-dir", sdist_dir, silent=True, ) + # fmt: on + generated_files = os.listdir(sdist_dir) assert len(generated_files) == 1 generated_sdist = os.path.join(sdist_dir, generated_files[0]) @@ -129,6 +133,7 @@ def get_sphinx_build_command(kind): # can not use a different configuration directory vs source directory # on RTD currently. So, we'll pass "-c docs/html" here. # See https://github.com/rtfd/readthedocs.org/issues/1543. + # fmt: off return [ "sphinx-build", "-W", @@ -138,6 +143,7 @@ def get_sphinx_build_command(kind): "docs/" + kind, "docs/build/" + kind, ] + # fmt: on session.run(*get_sphinx_build_command("html")) session.run(*get_sphinx_build_command("man")) @@ -227,9 +233,7 @@ def prepare_release(session): session.log(f"# Updating {AUTHORS_FILE}") release.generate_authors(AUTHORS_FILE) if release.modified_files_in_git(): - release.commit_file( - session, AUTHORS_FILE, message=f"Update {AUTHORS_FILE}", - ) + release.commit_file(session, AUTHORS_FILE, message=f"Update {AUTHORS_FILE}") else: session.log(f"# No changes to {AUTHORS_FILE}") @@ -276,7 +280,7 @@ def build_release(session): tmp_dist_paths = (build_dir / p for p in tmp_dists) session.log(f"# Copying dists from {build_dir}") - os.makedirs('dist', exist_ok=True) + os.makedirs("dist", exist_ok=True) for dist, final in zip(tmp_dist_paths, tmp_dists): session.log(f"# Copying {dist} to {final}") shutil.copy(dist, final) @@ -291,7 +295,7 @@ def build_dists(session): has_forbidden_git_untracked_files = any( # Don't report the environment this session is running in - not untracked_file.startswith('.nox/build-release/') + not untracked_file.startswith(".nox/build-release/") for untracked_file in release.get_git_untracked_files() ) if has_forbidden_git_untracked_files: @@ -337,9 +341,7 @@ def upload_release(session): f"pip-{version}.tar.gz", ] if sorted(distfile_names) != sorted(expected_distribution_files): - session.error( - f"Distribution files do not seem to be for {version} release." - ) + session.error(f"Distribution files do not seem to be for {version} release.") session.log("# Upload distributions") session.run("twine", "upload", *distribution_files) From fba946b36d7b32eefa4c164fbb48702516e81c35 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sun, 21 Feb 2021 15:05:23 +0000 Subject: [PATCH 2993/3170] Update note on the legacy resolver's removal --- docs/html/user_guide.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 92887885baf..61e0cdf0769 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -1857,9 +1857,11 @@ We plan for the resolver changeover to proceed as follows, using environments, pip defaults to the old resolver, and the new one is available using the flag ``--use-feature=2020-resolver``. -* pip 21.0: pip uses new resolver, and the old resolver is no longer - available. Python 2 support is removed per our :ref:`Python 2 - Support` policy. +* pip 21.0: pip uses new resolver by default, and the old resolver is + no longer supported. It will be removed after a currently undecided + amount of time, as the removal is dependent on pip's volunteer + maintainers' availability. Python 2 support is removed per our + :ref:`Python 2 Support` policy. Since this work will not change user-visible behavior described in the pip documentation, this change is not covered by the :ref:`Deprecation From a9d4446dc4bdfdda803580ba6d8099a674a138fd Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Sat, 20 Feb 2021 06:41:09 -0800 Subject: [PATCH 2994/3170] Blacken src/pip/_internal/utils directory --- .pre-commit-config.yaml | 1 - ...27-049c-4d5d-b44d-daea0c2fd01a.trivial.rst | 0 src/pip/_internal/utils/appdirs.py | 4 +- src/pip/_internal/utils/compat.py | 10 +- src/pip/_internal/utils/compatibility_tags.py | 28 +-- src/pip/_internal/utils/deprecation.py | 19 +- src/pip/_internal/utils/direct_url_helpers.py | 6 +- src/pip/_internal/utils/encoding.py | 24 +-- src/pip/_internal/utils/filesystem.py | 16 +- src/pip/_internal/utils/filetypes.py | 21 ++- src/pip/_internal/utils/hashes.py | 26 +-- .../_internal/utils/inject_securetransport.py | 2 +- src/pip/_internal/utils/logging.py | 175 +++++++++-------- src/pip/_internal/utils/misc.py | 176 +++++++++--------- src/pip/_internal/utils/models.py | 5 +- src/pip/_internal/utils/packaging.py | 23 +-- src/pip/_internal/utils/parallel.py | 6 +- src/pip/_internal/utils/pkg_resources.py | 4 +- src/pip/_internal/utils/setuptools_build.py | 18 +- src/pip/_internal/utils/subprocess.py | 53 +++--- src/pip/_internal/utils/temp_dir.py | 38 ++-- src/pip/_internal/utils/unpacking.py | 114 ++++++------ src/pip/_internal/utils/urls.py | 21 ++- src/pip/_internal/utils/virtualenv.py | 17 +- src/pip/_internal/utils/wheel.py | 39 ++-- 25 files changed, 403 insertions(+), 443 deletions(-) create mode 100644 news/0a741827-049c-4d5d-b44d-daea0c2fd01a.trivial.rst diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 163beba71a5..5213a9f4a5b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -31,7 +31,6 @@ repos: ^src/pip/_internal/network| ^src/pip/_internal/operations| ^src/pip/_internal/req| - ^src/pip/_internal/utils| ^src/pip/_internal/vcs| ^src/pip/_internal/\w+\.py$| ^src/pip/__main__.py$| diff --git a/news/0a741827-049c-4d5d-b44d-daea0c2fd01a.trivial.rst b/news/0a741827-049c-4d5d-b44d-daea0c2fd01a.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/utils/appdirs.py b/src/pip/_internal/utils/appdirs.py index b8c101b0cd5..db974dad635 100644 --- a/src/pip/_internal/utils/appdirs.py +++ b/src/pip/_internal/utils/appdirs.py @@ -21,7 +21,7 @@ def user_config_dir(appname, roaming=True): # type: (str, bool) -> str path = _appdirs.user_config_dir(appname, appauthor=False, roaming=roaming) if _appdirs.system == "darwin" and not os.path.isdir(path): - path = os.path.expanduser('~/.config/') + path = os.path.expanduser("~/.config/") if appname: path = os.path.join(path, appname) return path @@ -34,5 +34,5 @@ def site_config_dirs(appname): dirval = _appdirs.site_config_dir(appname, appauthor=False, multipath=True) if _appdirs.system not in ["win32", "darwin"]: # always look in /etc directly as well - return dirval.split(os.pathsep) + ['/etc'] + return dirval.split(os.pathsep) + ["/etc"] return [dirval] diff --git a/src/pip/_internal/utils/compat.py b/src/pip/_internal/utils/compat.py index 9600e0f44cc..069e3091d29 100644 --- a/src/pip/_internal/utils/compat.py +++ b/src/pip/_internal/utils/compat.py @@ -18,11 +18,13 @@ def has_tls(): # type: () -> bool try: import _ssl # noqa: F401 # ignore unused + return True except ImportError: pass from pip._vendor.urllib3.util import IS_PYOPENSSL + return IS_PYOPENSSL @@ -39,7 +41,7 @@ def get_path_uid(path): :raises OSError: When path is a symlink or can't be read. """ - if hasattr(os, 'O_NOFOLLOW'): + 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) @@ -51,8 +53,7 @@ def get_path_uid(path): else: # raise OSError for parity with os.O_NOFOLLOW above raise OSError( - "{} is a symlink; Will not return uid for symlinks".format( - path) + "{} is a symlink; Will not return uid for symlinks".format(path) ) return file_uid @@ -66,5 +67,4 @@ def get_path_uid(path): # windows detection, covers cpython and ironpython -WINDOWS = (sys.platform.startswith("win") or - (sys.platform == 'cli' and os.name == 'nt')) +WINDOWS = sys.platform.startswith("win") or (sys.platform == "cli" and os.name == "nt") diff --git a/src/pip/_internal/utils/compatibility_tags.py b/src/pip/_internal/utils/compatibility_tags.py index 5578f1f78eb..14fe51c1a51 100644 --- a/src/pip/_internal/utils/compatibility_tags.py +++ b/src/pip/_internal/utils/compatibility_tags.py @@ -18,13 +18,13 @@ from pip._vendor.packaging.tags import PythonVersion -_osx_arch_pat = re.compile(r'(.+)_(\d+)_(\d+)_(.+)') +_osx_arch_pat = re.compile(r"(.+)_(\d+)_(\d+)_(.+)") def version_info_to_nodot(version_info): # type: (Tuple[int, ...]) -> str # Only use up to the first two numbers. - return ''.join(map(str, version_info[:2])) + return "".join(map(str, version_info[:2])) def _mac_platforms(arch): @@ -39,7 +39,7 @@ def _mac_platforms(arch): # actual prefix provided by the user in case they provided # something like "macosxcustom_". It may be good to remove # this as undocumented or deprecate it in the future. - '{}_{}'.format(name, arch[len('macosx_'):]) + "{}_{}".format(name, arch[len("macosx_") :]) for arch in mac_platforms(mac_version, actual_arch) ] else: @@ -51,31 +51,31 @@ def _mac_platforms(arch): def _custom_manylinux_platforms(arch): # type: (str) -> List[str] arches = [arch] - arch_prefix, arch_sep, arch_suffix = arch.partition('_') - if arch_prefix == 'manylinux2014': + arch_prefix, arch_sep, arch_suffix = arch.partition("_") + if arch_prefix == "manylinux2014": # manylinux1/manylinux2010 wheels run on most manylinux2014 systems # with the exception of wheels depending on ncurses. PEP 599 states # manylinux1/manylinux2010 wheels should be considered # manylinux2014 wheels: # https://www.python.org/dev/peps/pep-0599/#backwards-compatibility-with-manylinux2010-wheels - if arch_suffix in {'i686', 'x86_64'}: - arches.append('manylinux2010' + arch_sep + arch_suffix) - arches.append('manylinux1' + arch_sep + arch_suffix) - elif arch_prefix == 'manylinux2010': + if arch_suffix in {"i686", "x86_64"}: + arches.append("manylinux2010" + arch_sep + arch_suffix) + arches.append("manylinux1" + arch_sep + arch_suffix) + elif arch_prefix == "manylinux2010": # manylinux1 wheels run on most manylinux2010 systems with the # exception of wheels depending on ncurses. PEP 571 states # manylinux1 wheels should be considered manylinux2010 wheels: # https://www.python.org/dev/peps/pep-0571/#backwards-compatibility-with-manylinux1-wheels - arches.append('manylinux1' + arch_sep + arch_suffix) + arches.append("manylinux1" + arch_sep + arch_suffix) return arches def _get_custom_platforms(arch): # type: (str) -> List[str] - arch_prefix, arch_sep, arch_suffix = arch.partition('_') - if arch.startswith('macosx'): + arch_prefix, arch_sep, arch_suffix = arch.partition("_") + if arch.startswith("macosx"): arches = _mac_platforms(arch) - elif arch_prefix in ['manylinux2014', 'manylinux2010']: + elif arch_prefix in ["manylinux2014", "manylinux2010"]: arches = _custom_manylinux_platforms(arch) else: arches = [arch] @@ -121,7 +121,7 @@ def get_supported( version=None, # type: Optional[str] platforms=None, # type: Optional[List[str]] impl=None, # type: Optional[str] - abis=None # type: Optional[List[str]] + abis=None, # type: Optional[List[str]] ): # type: (...) -> List[Tag] """Return a list of supported tags for each version specified in diff --git a/src/pip/_internal/utils/deprecation.py b/src/pip/_internal/utils/deprecation.py index 80383089222..3d52e179ab2 100644 --- a/src/pip/_internal/utils/deprecation.py +++ b/src/pip/_internal/utils/deprecation.py @@ -27,18 +27,14 @@ class PipDeprecationWarning(Warning): def _showwarning(message, category, filename, lineno, file=None, line=None): if file is not None: if _original_showwarning is not None: - _original_showwarning( - message, category, filename, lineno, file, line, - ) + _original_showwarning(message, category, filename, lineno, file, line) elif issubclass(category, PipDeprecationWarning): # We use a specially named logger which will handle all of the # deprecation messages for pip. logger = logging.getLogger("pip._internal.deprecations") logger.warning(message) else: - _original_showwarning( - message, category, filename, lineno, file, line, - ) + _original_showwarning(message, category, filename, lineno, file, line) def install_warning_logger(): @@ -82,10 +78,13 @@ def deprecated(reason, replacement, gone_in, issue=None): (reason, DEPRECATION_MSG_PREFIX + "{}"), (gone_in, "pip {} will remove support for this functionality."), (replacement, "A possible replacement is {}."), - (issue, ( - "You can find discussion regarding this at " - "https://github.com/pypa/pip/issues/{}." - )), + ( + issue, + ( + "You can find discussion regarding this at " + "https://github.com/pypa/pip/issues/{}." + ), + ), ] message = " ".join( template.format(val) for val, template in sentences if val is not None diff --git a/src/pip/_internal/utils/direct_url_helpers.py b/src/pip/_internal/utils/direct_url_helpers.py index e5ddc6a5c4f..eb50ac42be8 100644 --- a/src/pip/_internal/utils/direct_url_helpers.py +++ b/src/pip/_internal/utils/direct_url_helpers.py @@ -47,8 +47,8 @@ def direct_url_from_link(link, source_dir=None, link_is_in_wheel_cache=False): if link.is_vcs: vcs_backend = vcs.get_backend_for_scheme(link.scheme) assert vcs_backend - url, requested_revision, _ = ( - vcs_backend.get_url_rev_and_auth(link.url_without_fragment) + url, requested_revision, _ = vcs_backend.get_url_rev_and_auth( + link.url_without_fragment ) # For VCS links, we need to find out and add commit_id. if link_is_in_wheel_cache: @@ -106,7 +106,7 @@ def dist_get_direct_url(dist): except ( DirectUrlValidationError, json.JSONDecodeError, - UnicodeDecodeError + UnicodeDecodeError, ) as e: logger.warning( "Error parsing %s for %s: %s", diff --git a/src/pip/_internal/utils/encoding.py b/src/pip/_internal/utils/encoding.py index 67b0209f672..7c8893d559e 100644 --- a/src/pip/_internal/utils/encoding.py +++ b/src/pip/_internal/utils/encoding.py @@ -5,16 +5,16 @@ from typing import List, Tuple BOMS = [ - (codecs.BOM_UTF8, 'utf-8'), - (codecs.BOM_UTF16, 'utf-16'), - (codecs.BOM_UTF16_BE, 'utf-16-be'), - (codecs.BOM_UTF16_LE, 'utf-16-le'), - (codecs.BOM_UTF32, 'utf-32'), - (codecs.BOM_UTF32_BE, 'utf-32-be'), - (codecs.BOM_UTF32_LE, 'utf-32-le'), + (codecs.BOM_UTF8, "utf-8"), + (codecs.BOM_UTF16, "utf-16"), + (codecs.BOM_UTF16_BE, "utf-16-be"), + (codecs.BOM_UTF16_LE, "utf-16-le"), + (codecs.BOM_UTF32, "utf-32"), + (codecs.BOM_UTF32_BE, "utf-32-be"), + (codecs.BOM_UTF32_LE, "utf-32-le"), ] # type: List[Tuple[bytes, str]] -ENCODING_RE = re.compile(br'coding[:=]\s*([-\w.]+)') +ENCODING_RE = re.compile(br"coding[:=]\s*([-\w.]+)") def auto_decode(data): @@ -24,13 +24,13 @@ def auto_decode(data): Fallback to locale.getpreferredencoding(False) like open() on Python3""" for bom, encoding in BOMS: if data.startswith(bom): - return data[len(bom):].decode(encoding) + return data[len(bom) :].decode(encoding) # Lets check the first two lines as in PEP263 - for line in data.split(b'\n')[:2]: - if line[0:1] == b'#' and ENCODING_RE.search(line): + for line in data.split(b"\n")[:2]: + if line[0:1] == b"#" and ENCODING_RE.search(line): result = ENCODING_RE.search(line) assert result is not None - encoding = result.groups()[0].decode('ascii') + encoding = result.groups()[0].decode("ascii") return data.decode(encoding) return data.decode( locale.getpreferredencoding(False) or sys.getdefaultencoding(), diff --git a/src/pip/_internal/utils/filesystem.py b/src/pip/_internal/utils/filesystem.py index 7a22edd65de..8c580adadc3 100644 --- a/src/pip/_internal/utils/filesystem.py +++ b/src/pip/_internal/utils/filesystem.py @@ -89,8 +89,8 @@ def adjacent_tmp_file(path, **kwargs): delete=False, dir=os.path.dirname(path), prefix=os.path.basename(path), - suffix='.tmp', - **kwargs + suffix=".tmp", + **kwargs, ) as f: result = cast(BinaryIO, f) try: @@ -120,7 +120,7 @@ def test_writable_dir(path): break # Should never get here, but infinite loops are bad path = parent - if os.name == 'posix': + if os.name == "posix": return os.access(path, os.W_OK) return _test_writable_dir_win(path) @@ -130,10 +130,10 @@ def _test_writable_dir_win(path): # type: (str) -> bool # os.access doesn't work on Windows: http://bugs.python.org/issue2528 # and we can't use tempfile: http://bugs.python.org/issue22107 - basename = 'accesstest_deleteme_fishfingers_custard_' - alphabet = 'abcdefghijklmnopqrstuvwxyz0123456789' + basename = "accesstest_deleteme_fishfingers_custard_" + alphabet = "abcdefghijklmnopqrstuvwxyz0123456789" for _ in range(10): - name = basename + ''.join(random.choice(alphabet) for _ in range(6)) + name = basename + "".join(random.choice(alphabet) for _ in range(6)) file = os.path.join(path, name) try: fd = os.open(file, os.O_RDWR | os.O_CREAT | os.O_EXCL) @@ -152,9 +152,7 @@ def _test_writable_dir_win(path): return True # This should never be reached - raise OSError( - 'Unexpected condition testing for writable directory' - ) + raise OSError("Unexpected condition testing for writable directory") def find_files(path, pattern): diff --git a/src/pip/_internal/utils/filetypes.py b/src/pip/_internal/utils/filetypes.py index 117f38757b6..da935846f61 100644 --- a/src/pip/_internal/utils/filetypes.py +++ b/src/pip/_internal/utils/filetypes.py @@ -5,15 +5,18 @@ from pip._internal.utils.misc import splitext -WHEEL_EXTENSION = '.whl' -BZ2_EXTENSIONS = ('.tar.bz2', '.tbz') # type: Tuple[str, ...] -XZ_EXTENSIONS = ('.tar.xz', '.txz', '.tlz', - '.tar.lz', '.tar.lzma') # type: Tuple[str, ...] -ZIP_EXTENSIONS = ('.zip', WHEEL_EXTENSION) # type: Tuple[str, ...] -TAR_EXTENSIONS = ('.tar.gz', '.tgz', '.tar') # type: Tuple[str, ...] -ARCHIVE_EXTENSIONS = ( - ZIP_EXTENSIONS + BZ2_EXTENSIONS + TAR_EXTENSIONS + XZ_EXTENSIONS -) +WHEEL_EXTENSION = ".whl" +BZ2_EXTENSIONS = (".tar.bz2", ".tbz") # type: Tuple[str, ...] +XZ_EXTENSIONS = ( + ".tar.xz", + ".txz", + ".tlz", + ".tar.lz", + ".tar.lzma", +) # type: Tuple[str, ...] +ZIP_EXTENSIONS = (".zip", WHEEL_EXTENSION) # type: Tuple[str, ...] +TAR_EXTENSIONS = (".tar.gz", ".tgz", ".tar") # type: Tuple[str, ...] +ARCHIVE_EXTENSIONS = ZIP_EXTENSIONS + BZ2_EXTENSIONS + TAR_EXTENSIONS + XZ_EXTENSIONS def is_archive_file(name): diff --git a/src/pip/_internal/utils/hashes.py b/src/pip/_internal/utils/hashes.py index f91e429730c..e0ecf6ee902 100644 --- a/src/pip/_internal/utils/hashes.py +++ b/src/pip/_internal/utils/hashes.py @@ -10,12 +10,12 @@ # The recommended hash algo of the moment. Change this whenever the state of # the art changes; it won't hurt backward compatibility. -FAVORITE_HASH = 'sha256' +FAVORITE_HASH = "sha256" # Names of hashlib algorithms allowed by the --hash option and ``pip hash`` # Currently, those are the ones at least as collision-resistant as sha256. -STRONG_HASHES = ['sha256', 'sha384', 'sha512'] +STRONG_HASHES = ["sha256", "sha384", "sha512"] class Hashes: @@ -23,6 +23,7 @@ class Hashes: known-good values """ + def __init__(self, hashes=None): # type: (Dict[str, List[str]]) -> None """ @@ -63,7 +64,7 @@ def digest_count(self): def is_hash_allowed( self, - hash_name, # type: str + hash_name, # type: str hex_digest, # type: str ): # type: (...) -> bool @@ -83,9 +84,7 @@ def check_against_chunks(self, chunks): try: gots[hash_name] = hashlib.new(hash_name) except (ValueError, TypeError): - raise InstallationError( - f'Unknown hash name: {hash_name}' - ) + raise InstallationError(f"Unknown hash name: {hash_name}") for chunk in chunks: for hash in gots.values(): @@ -111,7 +110,7 @@ def check_against_file(self, file): def check_against_path(self, path): # type: (str) -> None - with open(path, 'rb') as file: + with open(path, "rb") as file: return self.check_against_file(file) def __nonzero__(self): @@ -132,11 +131,13 @@ def __eq__(self, other): def __hash__(self): # type: () -> int return hash( - ",".join(sorted( - ":".join((alg, digest)) - for alg, digest_list in self._allowed.items() - for digest in digest_list - )) + ",".join( + sorted( + ":".join((alg, digest)) + for alg, digest_list in self._allowed.items() + for digest in digest_list + ) + ) ) @@ -147,6 +148,7 @@ class MissingHashes(Hashes): exception showing it to the user. """ + def __init__(self): # type: () -> None """Don't offer the ``hashes`` kwarg.""" diff --git a/src/pip/_internal/utils/inject_securetransport.py b/src/pip/_internal/utils/inject_securetransport.py index 5b93b1d6730..b6863d93405 100644 --- a/src/pip/_internal/utils/inject_securetransport.py +++ b/src/pip/_internal/utils/inject_securetransport.py @@ -22,7 +22,7 @@ def inject_securetransport(): return # Checks for OpenSSL 1.0.1 - if ssl.OPENSSL_VERSION_NUMBER >= 0x1000100f: + if ssl.OPENSSL_VERSION_NUMBER >= 0x1000100F: return try: diff --git a/src/pip/_internal/utils/logging.py b/src/pip/_internal/utils/logging.py index d1d46ab701a..ce78416ec01 100644 --- a/src/pip/_internal/utils/logging.py +++ b/src/pip/_internal/utils/logging.py @@ -29,13 +29,14 @@ _log_state = threading.local() -subprocess_logger = getLogger('pip.subprocessor') +subprocess_logger = getLogger("pip.subprocessor") class BrokenStdoutLoggingError(Exception): """ Raised if BrokenPipeError occurs for the stdout stream while logging. """ + pass @@ -46,9 +47,11 @@ class BrokenStdoutLoggingError(Exception): # https://bugs.python.org/issue30418 def _is_broken_pipe_error(exc_class, exc): """See the docstring for non-Windows below.""" - return ((exc_class is BrokenPipeError) or - (exc_class is OSError and - exc.errno in (errno.EINVAL, errno.EPIPE))) + return (exc_class is BrokenPipeError) or ( + exc_class is OSError and exc.errno in (errno.EINVAL, errno.EPIPE) + ) + + else: # Then we are in the non-Windows case. def _is_broken_pipe_error(exc_class, exc): @@ -59,7 +62,7 @@ def _is_broken_pipe_error(exc_class, exc): exc_class: an exception class. exc: an exception instance. """ - return (exc_class is BrokenPipeError) + return exc_class is BrokenPipeError @contextlib.contextmanager @@ -78,7 +81,7 @@ def indent_log(num=2): def get_indentation(): - return getattr(_log_state, 'indentation', 0) + return getattr(_log_state, "indentation", 0) class IndentingFormatter(logging.Formatter): @@ -106,15 +109,15 @@ def get_message_start(self, formatted, levelno): prefix to add to each line). """ if levelno < logging.WARNING: - return '' + return "" if formatted.startswith(DEPRECATION_MSG_PREFIX): # Then the message already has a prefix. We don't want it to # look like "WARNING: DEPRECATION: ...." - return '' + return "" if levelno < logging.ERROR: - return 'WARNING: ' + return "WARNING: " - return 'ERROR: ' + return "ERROR: " def format(self, record): """ @@ -125,20 +128,18 @@ def format(self, record): message_start = self.get_message_start(formatted, record.levelno) formatted = message_start + formatted - prefix = '' + prefix = "" if self.add_timestamp: prefix = f"{self.formatTime(record)} " prefix += " " * get_indentation() - formatted = "".join([ - prefix + line - for line in formatted.splitlines(True) - ]) + formatted = "".join([prefix + line for line in formatted.splitlines(True)]) return formatted def _color_wrap(*colors): def wrapped(inp): return "".join(list(colors) + [inp, colorama.Style.RESET_ALL]) + return wrapped @@ -177,7 +178,8 @@ def should_color(self): return False real_stream = ( - self.stream if not isinstance(self.stream, colorama.AnsiToWin32) + self.stream + if not isinstance(self.stream, colorama.AnsiToWin32) else self.stream.wrapped ) @@ -210,22 +212,19 @@ def handleError(self, record): # stdout stream in logging's Handler.emit(), then raise our special # exception so we can handle it in main() instead of logging the # broken pipe error and continuing. - if (exc_class and self._using_stdout() and - _is_broken_pipe_error(exc_class, exc)): + if exc_class and self._using_stdout() and _is_broken_pipe_error(exc_class, exc): raise BrokenStdoutLoggingError() return super().handleError(record) class BetterRotatingFileHandler(logging.handlers.RotatingFileHandler): - def _open(self): ensure_dir(os.path.dirname(self.baseFilename)) return logging.handlers.RotatingFileHandler._open(self) class MaxLevelFilter(Filter): - def __init__(self, level): self.level = level @@ -292,78 +291,76 @@ def setup_logging(verbosity, no_color, user_log_file): ["user_log"] if include_user_log else [] ) - logging.config.dictConfig({ - "version": 1, - "disable_existing_loggers": False, - "filters": { - "exclude_warnings": { - "()": "pip._internal.utils.logging.MaxLevelFilter", - "level": logging.WARNING, + logging.config.dictConfig( + { + "version": 1, + "disable_existing_loggers": False, + "filters": { + "exclude_warnings": { + "()": "pip._internal.utils.logging.MaxLevelFilter", + "level": logging.WARNING, + }, + "restrict_to_subprocess": { + "()": "logging.Filter", + "name": subprocess_logger.name, + }, + "exclude_subprocess": { + "()": "pip._internal.utils.logging.ExcludeLoggerFilter", + "name": subprocess_logger.name, + }, }, - "restrict_to_subprocess": { - "()": "logging.Filter", - "name": subprocess_logger.name, + "formatters": { + "indent": { + "()": IndentingFormatter, + "format": "%(message)s", + }, + "indent_with_timestamp": { + "()": IndentingFormatter, + "format": "%(message)s", + "add_timestamp": True, + }, }, - "exclude_subprocess": { - "()": "pip._internal.utils.logging.ExcludeLoggerFilter", - "name": subprocess_logger.name, + "handlers": { + "console": { + "level": level, + "class": handler_classes["stream"], + "no_color": no_color, + "stream": log_streams["stdout"], + "filters": ["exclude_subprocess", "exclude_warnings"], + "formatter": "indent", + }, + "console_errors": { + "level": "WARNING", + "class": handler_classes["stream"], + "no_color": no_color, + "stream": log_streams["stderr"], + "filters": ["exclude_subprocess"], + "formatter": "indent", + }, + # A handler responsible for logging to the console messages + # from the "subprocessor" logger. + "console_subprocess": { + "level": level, + "class": handler_classes["stream"], + "no_color": no_color, + "stream": log_streams["stderr"], + "filters": ["restrict_to_subprocess"], + "formatter": "indent", + }, + "user_log": { + "level": "DEBUG", + "class": handler_classes["file"], + "filename": additional_log_file, + "delay": True, + "formatter": "indent_with_timestamp", + }, }, - }, - "formatters": { - "indent": { - "()": IndentingFormatter, - "format": "%(message)s", + "root": { + "level": root_level, + "handlers": handlers, }, - "indent_with_timestamp": { - "()": IndentingFormatter, - "format": "%(message)s", - "add_timestamp": True, - }, - }, - "handlers": { - "console": { - "level": level, - "class": handler_classes["stream"], - "no_color": no_color, - "stream": log_streams["stdout"], - "filters": ["exclude_subprocess", "exclude_warnings"], - "formatter": "indent", - }, - "console_errors": { - "level": "WARNING", - "class": handler_classes["stream"], - "no_color": no_color, - "stream": log_streams["stderr"], - "filters": ["exclude_subprocess"], - "formatter": "indent", - }, - # A handler responsible for logging to the console messages - # from the "subprocessor" logger. - "console_subprocess": { - "level": level, - "class": handler_classes["stream"], - "no_color": no_color, - "stream": log_streams["stderr"], - "filters": ["restrict_to_subprocess"], - "formatter": "indent", - }, - "user_log": { - "level": "DEBUG", - "class": handler_classes["file"], - "filename": additional_log_file, - "delay": True, - "formatter": "indent_with_timestamp", - }, - }, - "root": { - "level": root_level, - "handlers": handlers, - }, - "loggers": { - "pip._vendor": { - "level": vendored_log_level - } - }, - }) + "loggers": {"pip._vendor": {"level": vendored_log_level}}, + } + ) return level_number diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index b3650084818..4b1fecc9342 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -45,13 +45,21 @@ virtualenv_no_global, ) -__all__ = ['rmtree', 'display_path', 'backup_dir', - 'ask', 'splitext', - 'format_size', 'is_installable_dir', - 'normalize_path', - 'renames', 'get_prog', - 'captured_stdout', 'ensure_dir', - 'remove_auth_from_url'] +__all__ = [ + "rmtree", + "display_path", + "backup_dir", + "ask", + "splitext", + "format_size", + "is_installable_dir", + "normalize_path", + "renames", + "get_prog", + "captured_stdout", + "ensure_dir", + "remove_auth_from_url", +] logger = logging.getLogger(__name__) @@ -65,10 +73,10 @@ def get_pip_version(): pip_pkg_dir = os.path.join(os.path.dirname(__file__), "..", "..") pip_pkg_dir = os.path.abspath(pip_pkg_dir) - return ( - 'pip {} from {} (python {})'.format( - __version__, pip_pkg_dir, get_major_minor_version(), - ) + return "pip {} from {} (python {})".format( + __version__, + pip_pkg_dir, + get_major_minor_version(), ) @@ -89,7 +97,7 @@ def normalize_version_info(py_version_info): elif len(py_version_info) > 3: py_version_info = py_version_info[:3] - return cast('VersionInfo', py_version_info) + return cast("VersionInfo", py_version_info) def ensure_dir(path): @@ -107,21 +115,20 @@ def get_prog(): # type: () -> str try: prog = os.path.basename(sys.argv[0]) - if prog in ('__main__.py', '-c'): + if prog in ("__main__.py", "-c"): return f"{sys.executable} -m pip" else: return prog except (AttributeError, TypeError, IndexError): pass - return 'pip' + return "pip" # Retry every half second for up to 3 seconds @retry(stop_max_delay=3000, wait_fixed=500) def rmtree(dir, ignore_errors=False): # type: (AnyStr, bool) -> None - shutil.rmtree(dir, ignore_errors=ignore_errors, - onerror=rmtree_errorhandler) + shutil.rmtree(dir, ignore_errors=ignore_errors, onerror=rmtree_errorhandler) def rmtree_errorhandler(func, path, exc_info): @@ -150,11 +157,11 @@ def display_path(path): if possible.""" path = os.path.normcase(os.path.abspath(path)) if path.startswith(os.getcwd() + os.path.sep): - path = '.' + path[len(os.getcwd()):] + path = "." + path[len(os.getcwd()) :] return path -def backup_dir(dir, ext='.bak'): +def backup_dir(dir, ext=".bak"): # type: (str, str) -> str """Figure out the name of a directory to back up the given dir to (adding .bak, .bak2, etc)""" @@ -168,7 +175,7 @@ def backup_dir(dir, ext='.bak'): def ask_path_exists(message, options): # type: (str, Iterable[str]) -> str - for action in os.environ.get('PIP_EXISTS_ACTION', '').split(): + for action in os.environ.get("PIP_EXISTS_ACTION", "").split(): if action in options: return action return ask(message, options) @@ -177,10 +184,9 @@ def ask_path_exists(message, options): def _check_no_input(message): # type: (str) -> None """Raise an error if no input is allowed.""" - if os.environ.get('PIP_NO_INPUT'): + if os.environ.get("PIP_NO_INPUT"): raise Exception( - 'No input was expected ($PIP_NO_INPUT set); question: {}'.format( - message) + "No input was expected ($PIP_NO_INPUT set); question: {}".format(message) ) @@ -193,8 +199,8 @@ def ask(message, options): response = response.strip().lower() if response not in options: print( - 'Your response ({!r}) was not one of the expected responses: ' - '{}'.format(response, ', '.join(options)) + "Your response ({!r}) was not one of the expected responses: " + "{}".format(response, ", ".join(options)) ) else: return response @@ -223,9 +229,9 @@ def strtobool(val): 'val' is anything else. """ val = val.lower() - if val in ('y', 'yes', 't', 'true', 'on', '1'): + if val in ("y", "yes", "t", "true", "on", "1"): return 1 - elif val in ('n', 'no', 'f', 'false', 'off', '0'): + elif val in ("n", "no", "f", "false", "off", "0"): return 0 else: raise ValueError("invalid truth value %r" % (val,)) @@ -234,13 +240,13 @@ def strtobool(val): def format_size(bytes): # type: (float) -> str if bytes > 1000 * 1000: - return '{:.1f} MB'.format(bytes / 1000.0 / 1000) + return "{:.1f} MB".format(bytes / 1000.0 / 1000) elif bytes > 10 * 1000: - return '{} kB'.format(int(bytes / 1000)) + return "{} kB".format(int(bytes / 1000)) elif bytes > 1000: - return '{:.1f} kB'.format(bytes / 1000.0) + return "{:.1f} kB".format(bytes / 1000.0) else: - return '{} bytes'.format(int(bytes)) + return "{} bytes".format(int(bytes)) def tabulate(rows): @@ -253,21 +259,20 @@ def tabulate(rows): (['foobar 2000', '3735928559'], [10, 4]) """ rows = [tuple(map(str, row)) for row in rows] - sizes = [max(map(len, col)) for col in zip_longest(*rows, fillvalue='')] + sizes = [max(map(len, col)) for col in zip_longest(*rows, fillvalue="")] table = [" ".join(map(str.ljust, row, sizes)).rstrip() for row in rows] return table, sizes def is_installable_dir(path): # type: (str) -> bool - """Is path is a directory containing setup.py or pyproject.toml? - """ + """Is path is a directory containing setup.py or pyproject.toml?""" if not os.path.isdir(path): return False - setup_py = os.path.join(path, 'setup.py') + setup_py = os.path.join(path, "setup.py") if os.path.isfile(setup_py): return True - pyproject_toml = os.path.join(path, 'pyproject.toml') + pyproject_toml = os.path.join(path, "pyproject.toml") if os.path.isfile(pyproject_toml): return True return False @@ -300,7 +305,7 @@ def splitext(path): # type: (str) -> Tuple[str, str] """Like os.path.splitext, but take off .tar too""" base, ext = posixpath.splitext(path) - if base.lower().endswith('.tar'): + if base.lower().endswith(".tar"): ext = base[-4:] + ext base = base[:-4] return base, ext @@ -374,19 +379,19 @@ def dist_is_editable(dist): Return True if given Distribution is an editable install. """ for path_item in sys.path: - egg_link = os.path.join(path_item, dist.project_name + '.egg-link') + egg_link = os.path.join(path_item, dist.project_name + ".egg-link") if os.path.isfile(egg_link): return True return False def get_installed_distributions( - local_only=True, # type: bool - skip=stdlib_pkgs, # type: Container[str] - include_editables=True, # type: bool - editables_only=False, # type: bool - user_only=False, # type: bool - paths=None # type: Optional[List[str]] + local_only=True, # type: bool + skip=stdlib_pkgs, # type: Container[str] + include_editables=True, # type: bool + editables_only=False, # type: bool + user_only=False, # type: bool + paths=None, # type: Optional[List[str]] ): # type: (...) -> List[Distribution] """Return a list of installed Distribution objects. @@ -421,6 +426,7 @@ def get_distribution(req_name): """ from pip._internal.metadata import get_default_environment from pip._internal.metadata.pkg_resources import Distribution as _Dist + dist = get_default_environment().get_distribution(req_name) if dist is None: return None @@ -457,7 +463,7 @@ def egg_link_path(dist): sites.append(site_packages) for site in sites: - egglink = os.path.join(site, dist.project_name) + '.egg-link' + egglink = os.path.join(site, dist.project_name) + ".egg-link" if os.path.isfile(egglink): return egglink return None @@ -485,7 +491,6 @@ def write_output(msg, *args): class StreamWrapper(StringIO): - @classmethod def from_stream(cls, orig_stream): cls.orig_stream = orig_stream @@ -521,22 +526,22 @@ def captured_stdout(): Taken from Lib/support/__init__.py in the CPython repo. """ - return captured_output('stdout') + return captured_output("stdout") def captured_stderr(): """ See captured_stdout(). """ - return captured_output('stderr') + return captured_output("stderr") # 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) + enums["reverse_mapping"] = reverse + return type("Enum", (), enums) def build_netloc(host, port): @@ -546,21 +551,21 @@ def build_netloc(host, port): """ if port is None: return host - if ':' in host: + if ":" in host: # Only wrap host with square brackets when it is IPv6 - host = f'[{host}]' - return f'{host}:{port}' + host = f"[{host}]" + return f"{host}:{port}" -def build_url_from_netloc(netloc, scheme='https'): +def build_url_from_netloc(netloc, scheme="https"): # type: (str, str) -> str """ Build a full URL from a netloc. """ - if netloc.count(':') >= 2 and '@' not in netloc and '[' not in netloc: + if netloc.count(":") >= 2 and "@" not in netloc and "[" not in netloc: # It must be a bare IPv6 address, so wrap it with brackets. - netloc = f'[{netloc}]' - return f'{scheme}://{netloc}' + netloc = f"[{netloc}]" + return f"{scheme}://{netloc}" def parse_netloc(netloc): @@ -579,24 +584,22 @@ def split_auth_from_netloc(netloc): Returns: (netloc, (username, password)). """ - if '@' not in netloc: + if "@" not in netloc: return netloc, (None, None) # Split from the right because that's how urllib.parse.urlsplit() # behaves if more than one @ is present (which can be checked using # the password attribute of urlsplit()'s return value). - auth, netloc = netloc.rsplit('@', 1) - if ':' in auth: + auth, netloc = netloc.rsplit("@", 1) + if ":" in auth: # Split from the left because that's how urllib.parse.urlsplit() # behaves if more than one : is present (which again can be checked # using the password attribute of the return value) - user_pass = auth.split(':', 1) + user_pass = auth.split(":", 1) else: user_pass = auth, None - user_pass = tuple( - None if x is None else urllib.parse.unquote(x) for x in user_pass - ) + user_pass = tuple(None if x is None else urllib.parse.unquote(x) for x in user_pass) return netloc, user_pass @@ -614,14 +617,14 @@ def redact_netloc(netloc): if user is None: return netloc if password is None: - user = '****' - password = '' + user = "****" + password = "" else: user = urllib.parse.quote(user) - password = ':****' - return '{user}{password}@{netloc}'.format(user=user, - password=password, - netloc=netloc) + password = ":****" + return "{user}{password}@{netloc}".format( + user=user, password=password, netloc=netloc + ) def _transform_url(url, transform_netloc): @@ -637,9 +640,7 @@ def _transform_url(url, transform_netloc): purl = urllib.parse.urlsplit(url) netloc_tuple = transform_netloc(purl.netloc) # stripped url - url_pieces = ( - purl.scheme, netloc_tuple[0], purl.path, purl.query, purl.fragment - ) + url_pieces = (purl.scheme, netloc_tuple[0], purl.path, purl.query, purl.fragment) surl = urllib.parse.urlunsplit(url_pieces) return surl, netloc_tuple @@ -680,7 +681,7 @@ def redact_auth_from_url(url): class HiddenText: def __init__( self, - secret, # type: str + secret, # type: str redacted, # type: str ): # type: (...) -> None @@ -689,7 +690,7 @@ def __init__( def __repr__(self): # type: (...) -> str - return '<HiddenText {!r}>'.format(str(self)) + return "<HiddenText {!r}>".format(str(self)) def __str__(self): # type: (...) -> str @@ -703,12 +704,12 @@ def __eq__(self, other): # The string being used for redaction doesn't also have to match, # just the raw, original string. - return (self.secret == other.secret) + return self.secret == other.secret def hide_value(value): # type: (str) -> HiddenText - return HiddenText(value, redacted='****') + return HiddenText(value, redacted="****") def hide_url(url): @@ -727,41 +728,36 @@ def protect_pip_from_modification_on_windows(modifying_pip): pip_names = [ "pip.exe", "pip{}.exe".format(sys.version_info[0]), - "pip{}.{}.exe".format(*sys.version_info[:2]) + "pip{}.{}.exe".format(*sys.version_info[:2]), ] # See https://github.com/pypa/pip/issues/1299 for more discussion should_show_use_python_msg = ( - modifying_pip and - WINDOWS and - os.path.basename(sys.argv[0]) in pip_names + modifying_pip and WINDOWS and os.path.basename(sys.argv[0]) in pip_names ) if should_show_use_python_msg: - new_command = [ - sys.executable, "-m", "pip" - ] + sys.argv[1:] + new_command = [sys.executable, "-m", "pip"] + sys.argv[1:] raise CommandError( - 'To modify pip, please run the following command:\n{}' - .format(" ".join(new_command)) + "To modify pip, please run the following command:\n{}".format( + " ".join(new_command) + ) ) def is_console_interactive(): # type: () -> bool - """Is this console interactive? - """ + """Is this console interactive?""" return sys.stdin is not None and sys.stdin.isatty() def hash_file(path, blocksize=1 << 20): # type: (str, int) -> Tuple[Any, int] - """Return (hash, length) for path using hashlib.sha256() - """ + """Return (hash, length) for path using hashlib.sha256()""" h = hashlib.sha256() length = 0 - with open(path, 'rb') as f: + with open(path, "rb") as f: for block in read_chunks(f, size=blocksize): length += len(block) h.update(block) diff --git a/src/pip/_internal/utils/models.py b/src/pip/_internal/utils/models.py index c14e9ff926e..daf065825c5 100644 --- a/src/pip/_internal/utils/models.py +++ b/src/pip/_internal/utils/models.py @@ -7,10 +7,9 @@ class KeyBasedCompareMixin: - """Provides comparison capabilities that is based on a key - """ + """Provides comparison capabilities that is based on a key""" - __slots__ = ['_compare_key', '_defining_class'] + __slots__ = ["_compare_key", "_defining_class"] def __init__(self, key, defining_class): self._compare_key = key diff --git a/src/pip/_internal/utils/packaging.py b/src/pip/_internal/utils/packaging.py index 1be31ea9123..3f9dbd3b7a0 100644 --- a/src/pip/_internal/utils/packaging.py +++ b/src/pip/_internal/utils/packaging.py @@ -31,7 +31,7 @@ def check_requires_python(requires_python, version_info): return True requires_python_specifier = specifiers.SpecifierSet(requires_python) - python_version = version.parse('.'.join(map(str, version_info))) + python_version = version.parse(".".join(map(str, version_info))) return python_version in requires_python_specifier @@ -41,16 +41,17 @@ def get_metadata(dist): :raises NoneMetadataError: if the distribution reports `has_metadata()` True but `get_metadata()` returns None. """ - metadata_name = 'METADATA' - if (isinstance(dist, pkg_resources.DistInfoDistribution) and - dist.has_metadata(metadata_name)): + metadata_name = "METADATA" + if isinstance(dist, pkg_resources.DistInfoDistribution) and dist.has_metadata( + metadata_name + ): metadata = dist.get_metadata(metadata_name) - elif dist.has_metadata('PKG-INFO'): - metadata_name = 'PKG-INFO' + elif dist.has_metadata("PKG-INFO"): + metadata_name = "PKG-INFO" metadata = dist.get_metadata(metadata_name) else: logger.warning("No metadata found in %s", display_path(dist.location)) - metadata = '' + metadata = "" if metadata is None: raise NoneMetadataError(dist, metadata_name) @@ -69,7 +70,7 @@ def get_requires_python(dist): if not present. """ pkg_info_dict = get_metadata(dist) - requires_python = pkg_info_dict.get('Requires-Python') + requires_python = pkg_info_dict.get("Requires-Python") if requires_python is not None: # Convert to a str to satisfy the type checker, since requires_python @@ -81,8 +82,8 @@ def get_requires_python(dist): def get_installer(dist): # type: (Distribution) -> str - if dist.has_metadata('INSTALLER'): - for line in dist.get_metadata_lines('INSTALLER'): + if dist.has_metadata("INSTALLER"): + for line in dist.get_metadata_lines("INSTALLER"): if line.strip(): return line.strip() - return '' + return "" diff --git a/src/pip/_internal/utils/parallel.py b/src/pip/_internal/utils/parallel.py index 6b0919f1422..de91dc8abc8 100644 --- a/src/pip/_internal/utils/parallel.py +++ b/src/pip/_internal/utils/parallel.py @@ -16,7 +16,7 @@ than using the default value of 1. """ -__all__ = ['map_multiprocess', 'map_multithread'] +__all__ = ["map_multiprocess", "map_multithread"] from contextlib import contextmanager from multiprocessing import Pool as ProcessPool @@ -27,8 +27,8 @@ from pip._vendor.requests.adapters import DEFAULT_POOLSIZE Pool = Union[pool.Pool, pool.ThreadPool] -S = TypeVar('S') -T = TypeVar('T') +S = TypeVar("S") +T = TypeVar("T") # On platforms without sem_open, multiprocessing[.dummy] Pool # cannot be created. diff --git a/src/pip/_internal/utils/pkg_resources.py b/src/pip/_internal/utils/pkg_resources.py index 8c4974a703f..ee1eca30081 100644 --- a/src/pip/_internal/utils/pkg_resources.py +++ b/src/pip/_internal/utils/pkg_resources.py @@ -4,8 +4,8 @@ class DictMetadata: - """IMetadataProvider that reads metadata files from a dictionary. - """ + """IMetadataProvider that reads metadata files from a dictionary.""" + def __init__(self, metadata): # type: (Dict[str, bytes]) -> None self._metadata = metadata diff --git a/src/pip/_internal/utils/setuptools_build.py b/src/pip/_internal/utils/setuptools_build.py index 7d91f6f2677..1f1980dd30a 100644 --- a/src/pip/_internal/utils/setuptools_build.py +++ b/src/pip/_internal/utils/setuptools_build.py @@ -20,7 +20,7 @@ def make_setuptools_shim_args( setup_py_path, # type: str global_options=None, # type: Sequence[str] no_user_config=False, # type: bool - unbuffered_output=False # type: bool + unbuffered_output=False, # type: bool ): # type: (...) -> List[str] """ @@ -55,9 +55,7 @@ def make_setuptools_bdist_wheel_args( # relies on site.py to find parts of the standard library outside the # virtualenv. args = make_setuptools_shim_args( - setup_py_path, - global_options=global_options, - unbuffered_output=True + setup_py_path, global_options=global_options, unbuffered_output=True ) args += ["bdist_wheel", "-d", destination_dir] args += build_options @@ -70,9 +68,7 @@ def make_setuptools_clean_args( ): # type: (...) -> List[str] args = make_setuptools_shim_args( - setup_py_path, - global_options=global_options, - unbuffered_output=True + setup_py_path, global_options=global_options, unbuffered_output=True ) args += ["clean", "--all"] return args @@ -117,9 +113,7 @@ def make_setuptools_egg_info_args( no_user_config, # type: bool ): # type: (...) -> List[str] - args = make_setuptools_shim_args( - setup_py_path, no_user_config=no_user_config - ) + args = make_setuptools_shim_args(setup_py_path, no_user_config=no_user_config) args += ["egg_info"] @@ -140,7 +134,7 @@ def make_setuptools_install_args( home, # type: Optional[str] use_user_site, # type: bool no_user_config, # type: bool - pycompile # type: bool + pycompile, # type: bool ): # type: (...) -> List[str] assert not (use_user_site and prefix) @@ -150,7 +144,7 @@ def make_setuptools_install_args( setup_py_path, global_options=global_options, no_user_config=no_user_config, - unbuffered_output=True + unbuffered_output=True, ) args += ["install", "--record", record_filename] args += ["--single-version-externally-managed"] diff --git a/src/pip/_internal/utils/subprocess.py b/src/pip/_internal/utils/subprocess.py index fc5a026b886..cfde1870081 100644 --- a/src/pip/_internal/utils/subprocess.py +++ b/src/pip/_internal/utils/subprocess.py @@ -12,7 +12,7 @@ CommandArgs = List[Union[str, HiddenText]] -LOG_DIVIDER = '----------------------------------------' +LOG_DIVIDER = "----------------------------------------" def make_command(*args): @@ -43,9 +43,9 @@ def format_command_args(args): # this can trigger a UnicodeDecodeError in Python 2 if the argument # has type unicode and includes a non-ascii character. (The type # checker doesn't ensure the annotations are correct in all cases.) - return ' '.join( - shlex.quote(str(arg)) if isinstance(arg, HiddenText) - else shlex.quote(arg) for arg in args + return " ".join( + shlex.quote(str(arg)) if isinstance(arg, HiddenText) else shlex.quote(arg) + for arg in args ) @@ -54,15 +54,13 @@ def reveal_command_args(args): """ Return the arguments in their raw, unredacted form. """ - return [ - arg.secret if isinstance(arg, HiddenText) else arg for arg in args - ] + return [arg.secret if isinstance(arg, HiddenText) else arg for arg in args] def make_subprocess_output_error( - cmd_args, # type: Union[List[str], CommandArgs] - cwd, # type: Optional[str] - lines, # type: List[str] + cmd_args, # type: Union[List[str], CommandArgs] + cwd, # type: Optional[str] + lines, # type: List[str] exit_status, # type: int ): # type: (...) -> str @@ -75,15 +73,15 @@ def make_subprocess_output_error( command = format_command_args(cmd_args) # We know the joined output value ends in a newline. - output = ''.join(lines) + output = "".join(lines) msg = ( # Use a unicode string to avoid "UnicodeEncodeError: 'ascii' # codec can't encode character ..." in Python 2 when a format # argument (e.g. `output`) has a non-ascii character. - 'Command errored out with exit status {exit_status}:\n' - ' command: {command_display}\n' - ' cwd: {cwd_display}\n' - 'Complete output ({line_count} lines):\n{output}{divider}' + "Command errored out with exit status {exit_status}:\n" + " command: {command_display}\n" + " cwd: {cwd_display}\n" + "Complete output ({line_count} lines):\n{output}{divider}" ).format( exit_status=exit_status, command_display=command, @@ -99,7 +97,7 @@ def call_subprocess( cmd, # type: Union[List[str], CommandArgs] show_stdout=False, # type: bool cwd=None, # type: Optional[str] - on_returncode='raise', # type: str + on_returncode="raise", # type: str extra_ok_returncodes=None, # type: Optional[Iterable[int]] command_desc=None, # type: Optional[str] extra_environ=None, # type: Optional[Mapping[str, Any]] @@ -181,7 +179,9 @@ def call_subprocess( except Exception as exc: if log_failed_cmd: subprocess_logger.critical( - "Error %s while executing command %s", exc, command_desc, + "Error %s while executing command %s", + exc, + command_desc, ) raise all_output = [] @@ -195,7 +195,7 @@ def call_subprocess( if not line: break line = line.rstrip() - all_output.append(line + '\n') + all_output.append(line + "\n") # Show the line immediately. log_subprocess(line) @@ -208,7 +208,7 @@ def call_subprocess( finally: if proc.stdout: proc.stdout.close() - output = ''.join(all_output) + output = "".join(all_output) else: # In this mode, stdout and stderr are in different pipes. # We must use communicate() which is the only safe way to read both. @@ -222,9 +222,7 @@ def call_subprocess( all_output.append(err) output = out - proc_had_error = ( - proc.returncode and proc.returncode not in extra_ok_returncodes - ) + proc_had_error = proc.returncode and proc.returncode not in extra_ok_returncodes if use_spinner: assert spinner if proc_had_error: @@ -232,7 +230,7 @@ def call_subprocess( else: spinner.finish("done") if proc_had_error: - if on_returncode == 'raise': + if on_returncode == "raise": if not showing_subprocess and log_failed_cmd: # Then the subprocess streams haven't been logged to the # console yet. @@ -244,18 +242,17 @@ def call_subprocess( ) subprocess_logger.error(msg) raise InstallationSubprocessError(proc.returncode, command_desc) - elif on_returncode == 'warn': + elif on_returncode == "warn": subprocess_logger.warning( 'Command "%s" had error code %s in %s', command_desc, proc.returncode, cwd, ) - elif on_returncode == 'ignore': + elif on_returncode == "ignore": pass else: - raise ValueError('Invalid value: on_returncode={!r}'.format( - on_returncode)) + raise ValueError("Invalid value: on_returncode={!r}".format(on_returncode)) return output @@ -270,7 +267,7 @@ def runner_with_spinner_message(message): def runner( cmd, # type: List[str] cwd=None, # type: Optional[str] - extra_environ=None # type: Optional[Mapping[str, Any]] + extra_environ=None, # type: Optional[Mapping[str, Any]] ): # type: (...) -> None with open_spinner(message) as spinner: diff --git a/src/pip/_internal/utils/temp_dir.py b/src/pip/_internal/utils/temp_dir.py index 8c4aaba3a96..477cbe6b1aa 100644 --- a/src/pip/_internal/utils/temp_dir.py +++ b/src/pip/_internal/utils/temp_dir.py @@ -10,7 +10,7 @@ logger = logging.getLogger(__name__) -_T = TypeVar('_T', bound='TempDirectory') +_T = TypeVar("_T", bound="TempDirectory") # Kinds of temporary directories. Only needed for ones that are @@ -38,8 +38,7 @@ def global_tempdir_manager(): class TempDirectoryTypeRegistry: - """Manages temp directory behavior - """ + """Manages temp directory behavior""" def __init__(self): # type: () -> None @@ -108,7 +107,7 @@ class TempDirectory: def __init__( self, - path=None, # type: Optional[str] + path=None, # type: Optional[str] delete=_default, # type: Union[bool, None, _Default] kind="temp", # type: str globally_managed=False, # type: bool @@ -142,9 +141,7 @@ def __init__( @property def path(self): # type: () -> str - assert not self._deleted, ( - f"Attempted to access deleted path: {self._path}" - ) + assert not self._deleted, f"Attempted to access deleted path: {self._path}" return self._path def __repr__(self): @@ -169,22 +166,18 @@ def __exit__(self, exc, value, tb): def _create(self, kind): # type: (str) -> str - """Create a temporary directory and store its path in self.path - """ + """Create a temporary directory and store its path in self.path""" # 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. - path = os.path.realpath( - tempfile.mkdtemp(prefix=f"pip-{kind}-") - ) + path = os.path.realpath(tempfile.mkdtemp(prefix=f"pip-{kind}-")) logger.debug("Created temporary directory: %s", path) return path def cleanup(self): # type: () -> None - """Remove the temporary directory created and reset state - """ + """Remove the temporary directory created and reset state""" self._deleted = True if not os.path.exists(self._path): return @@ -205,6 +198,7 @@ class AdjacentTempDirectory(TempDirectory): (when used as a contextmanager) """ + # The characters that may be used to name the temp directory # We always prepend a ~ and then rotate through these until # a usable name is found. @@ -214,7 +208,7 @@ class AdjacentTempDirectory(TempDirectory): def __init__(self, original, delete=None): # type: (str, Optional[bool]) -> None - self.original = original.rstrip('/\\') + self.original = original.rstrip("/\\") super().__init__(delete=delete) @classmethod @@ -229,16 +223,18 @@ def _generate_names(cls, name): """ for i in range(1, len(name)): for candidate in itertools.combinations_with_replacement( - cls.LEADING_CHARS, i - 1): - new_name = '~' + ''.join(candidate) + name[i:] + cls.LEADING_CHARS, i - 1 + ): + new_name = "~" + "".join(candidate) + name[i:] if new_name != name: yield new_name # If we make it this far, we will have to make a longer name for i in range(len(cls.LEADING_CHARS)): for candidate in itertools.combinations_with_replacement( - cls.LEADING_CHARS, i): - new_name = '~' + ''.join(candidate) + name + cls.LEADING_CHARS, i + ): + new_name = "~" + "".join(candidate) + name if new_name != name: yield new_name @@ -258,9 +254,7 @@ def _create(self, kind): break else: # Final fallback on the default behavior. - path = os.path.realpath( - tempfile.mkdtemp(prefix=f"pip-{kind}-") - ) + path = os.path.realpath(tempfile.mkdtemp(prefix=f"pip-{kind}-")) logger.debug("Created temporary directory: %s", path) return path diff --git a/src/pip/_internal/utils/unpacking.py b/src/pip/_internal/utils/unpacking.py index 74d3f4a9cd1..44ac475357d 100644 --- a/src/pip/_internal/utils/unpacking.py +++ b/src/pip/_internal/utils/unpacking.py @@ -26,16 +26,18 @@ try: import bz2 # noqa + SUPPORTED_EXTENSIONS += BZ2_EXTENSIONS except ImportError: - logger.debug('bz2 module is not available') + logger.debug("bz2 module is not available") try: # Only for Python 3.3+ import lzma # noqa + SUPPORTED_EXTENSIONS += XZ_EXTENSIONS except ImportError: - logger.debug('lzma module is not available') + logger.debug("lzma module is not available") def current_umask(): @@ -48,18 +50,15 @@ def current_umask(): def split_leading_dir(path): # type: (str) -> List[str] - path = path.lstrip('/').lstrip('\\') - if ( - '/' in path and ( - ('\\' in path and path.find('/') < path.find('\\')) or - '\\' not in path - ) + path = path.lstrip("/").lstrip("\\") + if "/" in path and ( + ("\\" in path and path.find("/") < path.find("\\")) or "\\" not in path ): - return path.split('/', 1) - elif '\\' in path: - return path.split('\\', 1) + return path.split("/", 1) + elif "\\" in path: + return path.split("\\", 1) else: - return [path, ''] + return [path, ""] def has_leading_dir(paths): @@ -118,7 +117,7 @@ def unzip_file(filename, location, flatten=True): no-ops per the python docs. """ ensure_dir(location) - zipfp = open(filename, 'rb') + zipfp = open(filename, "rb") try: zip = zipfile.ZipFile(zipfp, allowZip64=True) leading = has_leading_dir(zip.namelist()) and flatten @@ -131,11 +130,11 @@ def unzip_file(filename, location, flatten=True): dir = os.path.dirname(fn) if not is_within_directory(location, fn): message = ( - 'The zip file ({}) has a file ({}) trying to install ' - 'outside target directory ({})' + "The zip file ({}) has a file ({}) trying to install " + "outside target directory ({})" ) raise InstallationError(message.format(filename, fn, location)) - if fn.endswith('/') or fn.endswith('\\'): + if fn.endswith("/") or fn.endswith("\\"): # A directory ensure_dir(fn) else: @@ -144,7 +143,7 @@ def unzip_file(filename, location, flatten=True): # chunk of memory for the file's content fp = zip.open(name) try: - with open(fn, 'wb') as destfp: + with open(fn, "wb") as destfp: shutil.copyfileobj(fp, destfp) finally: fp.close() @@ -165,24 +164,23 @@ def untar_file(filename, location): no-ops per the python docs. """ ensure_dir(location) - if filename.lower().endswith('.gz') or filename.lower().endswith('.tgz'): - mode = 'r:gz' + if filename.lower().endswith(".gz") or filename.lower().endswith(".tgz"): + mode = "r:gz" elif filename.lower().endswith(BZ2_EXTENSIONS): - mode = 'r:bz2' + mode = "r:bz2" elif filename.lower().endswith(XZ_EXTENSIONS): - mode = 'r:xz' - elif filename.lower().endswith('.tar'): - mode = 'r' + mode = "r:xz" + elif filename.lower().endswith(".tar"): + mode = "r" else: logger.warning( - 'Cannot determine compression type for file %s', filename, + "Cannot determine compression type for file %s", + filename, ) - mode = 'r:*' + mode = "r:*" tar = tarfile.open(filename, mode) try: - leading = has_leading_dir([ - member.name for member in tar.getmembers() - ]) + leading = has_leading_dir([member.name for member in tar.getmembers()]) for member in tar.getmembers(): fn = member.name if leading: @@ -190,12 +188,10 @@ def untar_file(filename, location): path = os.path.join(location, fn) if not is_within_directory(location, path): message = ( - 'The tar file ({}) has a file ({}) trying to install ' - 'outside target directory ({})' - ) - raise InstallationError( - message.format(filename, path, location) + "The tar file ({}) has a file ({}) trying to install " + "outside target directory ({})" ) + raise InstallationError(message.format(filename, path, location)) if member.isdir(): ensure_dir(path) elif member.issym(): @@ -206,8 +202,10 @@ def untar_file(filename, location): # Some corrupt tar files seem to produce this # (specifically bad symlinks) logger.warning( - 'In the tar file %s the member %s is invalid: %s', - filename, member.name, exc, + "In the tar file %s the member %s is invalid: %s", + filename, + member.name, + exc, ) continue else: @@ -217,13 +215,15 @@ def untar_file(filename, location): # Some corrupt tar files seem to produce this # (specifically bad symlinks) logger.warning( - 'In the tar file %s the member %s is invalid: %s', - filename, member.name, exc, + "In the tar file %s the member %s is invalid: %s", + filename, + member.name, + exc, ) continue ensure_dir(os.path.dirname(path)) assert fp is not None - with open(path, 'wb') as destfp: + with open(path, "wb") as destfp: shutil.copyfileobj(fp, destfp) fp.close() # Update the timestamp (useful for cython compiled files) @@ -236,38 +236,32 @@ def untar_file(filename, location): def unpack_file( - filename, # type: str - location, # type: str - content_type=None, # type: Optional[str] + filename, # type: str + location, # type: str + content_type=None, # type: Optional[str] ): # type: (...) -> None filename = os.path.realpath(filename) if ( - content_type == 'application/zip' or - filename.lower().endswith(ZIP_EXTENSIONS) or - zipfile.is_zipfile(filename) + content_type == "application/zip" + or filename.lower().endswith(ZIP_EXTENSIONS) + or zipfile.is_zipfile(filename) ): - unzip_file( - filename, - location, - flatten=not filename.endswith('.whl') - ) + unzip_file(filename, location, flatten=not filename.endswith(".whl")) elif ( - content_type == 'application/x-gzip' or - tarfile.is_tarfile(filename) or - filename.lower().endswith( - TAR_EXTENSIONS + BZ2_EXTENSIONS + XZ_EXTENSIONS - ) + content_type == "application/x-gzip" + or tarfile.is_tarfile(filename) + or filename.lower().endswith(TAR_EXTENSIONS + BZ2_EXTENSIONS + XZ_EXTENSIONS) ): untar_file(filename, location) else: # FIXME: handle? # FIXME: magic signatures? logger.critical( - 'Cannot unpack file %s (downloaded from %s, content-type: %s); ' - 'cannot detect archive format', - filename, location, content_type, - ) - raise InstallationError( - f'Cannot determine archive format of {location}' + "Cannot unpack file %s (downloaded from %s, content-type: %s); " + "cannot detect archive format", + filename, + location, + content_type, ) + raise InstallationError(f"Cannot determine archive format of {location}") diff --git a/src/pip/_internal/utils/urls.py b/src/pip/_internal/utils/urls.py index d5401cecf74..50a04d861d4 100644 --- a/src/pip/_internal/utils/urls.py +++ b/src/pip/_internal/utils/urls.py @@ -7,9 +7,9 @@ def get_url_scheme(url): # type: (str) -> Optional[str] - if ':' not in url: + if ":" not in url: return None - return url.split(':', 1)[0].lower() + return url.split(":", 1)[0].lower() def path_to_url(path): @@ -19,7 +19,7 @@ def path_to_url(path): quoted path parts. """ path = os.path.normpath(os.path.abspath(path)) - url = urllib.parse.urljoin('file:', urllib.request.pathname2url(path)) + url = urllib.parse.urljoin("file:", urllib.request.pathname2url(path)) return url @@ -28,20 +28,21 @@ def url_to_path(url): """ Convert a file: URL to a path. """ - assert url.startswith('file:'), ( - f"You can only turn file: urls into filenames (not {url!r})") + assert url.startswith( + "file:" + ), f"You can only turn file: urls into filenames (not {url!r})" _, netloc, path, _, _ = urllib.parse.urlsplit(url) - if not netloc or netloc == 'localhost': + if not netloc or netloc == "localhost": # According to RFC 8089, same as empty authority. - netloc = '' - elif sys.platform == 'win32': + netloc = "" + elif sys.platform == "win32": # If we have a UNC path, prepend UNC share notation. - netloc = '\\\\' + netloc + netloc = "\\\\" + netloc else: raise ValueError( - f'non-local file URIs are not supported on this platform: {url!r}' + f"non-local file URIs are not supported on this platform: {url!r}" ) path = urllib.request.url2pathname(netloc + path) diff --git a/src/pip/_internal/utils/virtualenv.py b/src/pip/_internal/utils/virtualenv.py index c9c601f86a5..51cacb55ca1 100644 --- a/src/pip/_internal/utils/virtualenv.py +++ b/src/pip/_internal/utils/virtualenv.py @@ -27,13 +27,12 @@ def _running_under_regular_virtualenv(): This handles virtual environments created with pypa's virtualenv. """ # pypa/virtualenv case - return hasattr(sys, 'real_prefix') + return hasattr(sys, "real_prefix") def running_under_virtualenv(): # type: () -> bool - """Return True if we're running inside a virtualenv, False otherwise. - """ + """Return True if we're running inside a virtualenv, False otherwise.""" return _running_under_venv() or _running_under_regular_virtualenv() @@ -43,11 +42,11 @@ def _get_pyvenv_cfg_lines(): Returns None, if it could not read/access the file. """ - pyvenv_cfg_file = os.path.join(sys.prefix, 'pyvenv.cfg') + pyvenv_cfg_file = os.path.join(sys.prefix, "pyvenv.cfg") try: # Although PEP 405 does not specify, the built-in venv module always # writes with UTF-8. (pypa/pip#8717) - with open(pyvenv_cfg_file, encoding='utf-8') as f: + with open(pyvenv_cfg_file, encoding="utf-8") as f: return f.read().splitlines() # avoids trailing newlines except OSError: return None @@ -78,7 +77,7 @@ def _no_global_under_venv(): for line in cfg_lines: match = _INCLUDE_SYSTEM_SITE_PACKAGES_REGEX.match(line) - if match is not None and match.group('value') == 'false': + if match is not None and match.group("value") == "false": return True return False @@ -92,15 +91,15 @@ def _no_global_under_regular_virtualenv(): """ site_mod_dir = os.path.dirname(os.path.abspath(site.__file__)) no_global_site_packages_file = os.path.join( - site_mod_dir, 'no-global-site-packages.txt', + site_mod_dir, + "no-global-site-packages.txt", ) return os.path.exists(no_global_site_packages_file) def virtualenv_no_global(): # type: () -> bool - """Returns a boolean, whether running in venv with no system site-packages. - """ + """Returns a boolean, whether running in venv with no system site-packages.""" # PEP 405 compliance needs to be checked first since virtualenv >=20 would # return True for both checks, but is only able to use the PEP 405 config. if _running_under_venv(): diff --git a/src/pip/_internal/utils/wheel.py b/src/pip/_internal/utils/wheel.py index de048581208..982508eb4f6 100644 --- a/src/pip/_internal/utils/wheel.py +++ b/src/pip/_internal/utils/wheel.py @@ -23,6 +23,7 @@ class WheelMetadata(DictMetadata): """Metadata provider that maps metadata decoding exceptions to our internal exception type. """ + def __init__(self, metadata, wheel_name): # type: (Dict[str, bytes], str) -> None super().__init__(metadata) @@ -35,9 +36,7 @@ def get_metadata(self, name): except UnicodeDecodeError as e: # Augment the default error with the origin of the file. raise UnsupportedWheel( - "Error decoding metadata for {}: {}".format( - self._wheel_name, e - ) + "Error decoding metadata for {}: {}".format(self._wheel_name, e) ) @@ -49,9 +48,7 @@ def pkg_resources_distribution_for_wheel(wheel_zip, name, location): """ info_dir, _ = parse_wheel(wheel_zip, name) - metadata_files = [ - p for p in wheel_zip.namelist() if p.startswith(f"{info_dir}/") - ] + metadata_files = [p for p in wheel_zip.namelist() if p.startswith(f"{info_dir}/")] metadata_text = {} # type: Dict[str, bytes] for path in metadata_files: @@ -60,15 +57,11 @@ def pkg_resources_distribution_for_wheel(wheel_zip, name, location): try: metadata_text[metadata_name] = read_wheel_metadata_file(wheel_zip, path) except UnsupportedWheel as e: - raise UnsupportedWheel( - "{} has an invalid wheel, {}".format(name, str(e)) - ) + raise UnsupportedWheel("{} has an invalid wheel, {}".format(name, str(e))) metadata = WheelMetadata(metadata_text, location) - return DistInfoDistribution( - location=location, metadata=metadata, project_name=name - ) + return DistInfoDistribution(location=location, metadata=metadata, project_name=name) def parse_wheel(wheel_zip, name): @@ -83,9 +76,7 @@ def parse_wheel(wheel_zip, name): metadata = wheel_metadata(wheel_zip, info_dir) version = wheel_version(metadata) except UnsupportedWheel as e: - raise UnsupportedWheel( - "{} has an invalid wheel, {}".format(name, str(e)) - ) + raise UnsupportedWheel("{} has an invalid wheel, {}".format(name, str(e))) check_compatibility(version, name) @@ -102,16 +93,14 @@ def wheel_dist_info_dir(source, name): # Zip file path separators must be / subdirs = {p.split("/", 1)[0] for p in source.namelist()} - info_dirs = [s for s in subdirs if s.endswith('.dist-info')] + info_dirs = [s for s in subdirs if s.endswith(".dist-info")] if not info_dirs: raise UnsupportedWheel(".dist-info directory not found") if len(info_dirs) > 1: raise UnsupportedWheel( - "multiple .dist-info directories found: {}".format( - ", ".join(info_dirs) - ) + "multiple .dist-info directories found: {}".format(", ".join(info_dirs)) ) info_dir = info_dirs[0] @@ -135,9 +124,7 @@ def read_wheel_metadata_file(source, path): # BadZipFile for general corruption, KeyError for missing entry, # and RuntimeError for password-protected files except (BadZipFile, KeyError, RuntimeError) as e: - raise UnsupportedWheel( - f"could not read {path!r} file: {e!r}" - ) + raise UnsupportedWheel(f"could not read {path!r} file: {e!r}") def wheel_metadata(source, dist_info_dir): @@ -172,7 +159,7 @@ def wheel_version(wheel_data): version = version_text.strip() try: - return tuple(map(int, version.split('.'))) + return tuple(map(int, version.split("."))) except ValueError: raise UnsupportedWheel(f"invalid Wheel-Version: {version!r}") @@ -193,10 +180,10 @@ def check_compatibility(version, name): if version[0] > VERSION_COMPATIBLE[0]: raise UnsupportedWheel( "{}'s Wheel-Version ({}) is not compatible with this version " - "of pip".format(name, '.'.join(map(str, version))) + "of pip".format(name, ".".join(map(str, version))) ) elif version > VERSION_COMPATIBLE: logger.warning( - 'Installing from a newer Wheel-Version (%s)', - '.'.join(map(str, version)), + "Installing from a newer Wheel-Version (%s)", + ".".join(map(str, version)), ) From 83c596ff70bef7e14d865f0f64f283b25d1a606c Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Sat, 20 Feb 2021 07:13:27 -0800 Subject: [PATCH 2995/3170] Blacken src/pip/_internal/cli directory In autocompletion.py, the should_list_installed boolean expression was flipped to workaround upstream black bug: https://github.com/psf/black/issues/1629 The bug makes black fail to stabilize formatting when the list isn't the last piece of the expression. --- .pre-commit-config.yaml | 1 - ...ff-72ea-4db5-846a-30273dac1c0c.trivial.rst | 0 src/pip/_internal/cli/autocompletion.py | 58 +- src/pip/_internal/cli/base_command.py | 42 +- src/pip/_internal/cli/cmdoptions.py | 634 ++++++++++-------- src/pip/_internal/cli/command_context.py | 2 +- src/pip/_internal/cli/main.py | 3 +- src/pip/_internal/cli/main_parser.py | 17 +- src/pip/_internal/cli/parser.py | 67 +- src/pip/_internal/cli/progress_bars.py | 45 +- src/pip/_internal/cli/req_command.py | 111 +-- src/pip/_internal/cli/spinners.py | 11 +- 12 files changed, 519 insertions(+), 472 deletions(-) create mode 100644 news/917ab6ff-72ea-4db5-846a-30273dac1c0c.trivial.rst diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 163beba71a5..ccd8a1f530c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -23,7 +23,6 @@ repos: exclude: | (?x) ^docs/| - ^src/pip/_internal/cli| ^src/pip/_internal/commands| ^src/pip/_internal/distributions| ^src/pip/_internal/index| diff --git a/news/917ab6ff-72ea-4db5-846a-30273dac1c0c.trivial.rst b/news/917ab6ff-72ea-4db5-846a-30273dac1c0c.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/cli/autocompletion.py b/src/pip/_internal/cli/autocompletion.py index 8dc2be20055..3b1d2ac9b11 100644 --- a/src/pip/_internal/cli/autocompletion.py +++ b/src/pip/_internal/cli/autocompletion.py @@ -14,17 +14,16 @@ def autocomplete(): # type: () -> None - """Entry Point for completion of main and subcommand options. - """ + """Entry Point for completion of main and subcommand options.""" # Don't complete if user hasn't sourced bash_completion file. - if 'PIP_AUTO_COMPLETE' not in os.environ: + if "PIP_AUTO_COMPLETE" not in os.environ: return - cwords = os.environ['COMP_WORDS'].split()[1:] - cword = int(os.environ['COMP_CWORD']) + cwords = os.environ["COMP_WORDS"].split()[1:] + cword = int(os.environ["COMP_CWORD"]) try: current = cwords[cword - 1] except IndexError: - current = '' + current = "" parser = create_main_parser() subcommands = list(commands_dict) @@ -39,13 +38,13 @@ def autocomplete(): # subcommand options if subcommand_name is not None: # special case: 'help' subcommand has no options - if subcommand_name == 'help': + 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('-') - ) + should_list_installed = not current.startswith("-") and subcommand_name in [ + "show", + "uninstall", + ] if should_list_installed: lc = current.lower() installed = [ @@ -67,13 +66,15 @@ def autocomplete(): 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]] + 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)] # get completion type given cwords and available subcommand options completion_type = get_path_completion_type( - cwords, cword, subcommand.parser.option_list_all, + cwords, + cword, + subcommand.parser.option_list_all, ) # get completion files and directories if ``completion_type`` is # ``<file>``, ``<dir>`` or ``<path>`` @@ -84,7 +85,7 @@ def autocomplete(): opt_label = option[0] # append '=' to options which require args if option[1] and option[0][:2] == "--": - opt_label += '=' + opt_label += "=" print(opt_label) else: # show main parser options only when necessary @@ -92,19 +93,17 @@ def autocomplete(): opts = [i.option_list for i in parser.option_groups] opts.append(parser.option_list) flattened_opts = chain.from_iterable(opts) - if current.startswith('-'): + if current.startswith("-"): for opt in flattened_opts: if opt.help != optparse.SUPPRESS_HELP: subcommands += opt._long_opts + opt._short_opts else: # get completion type given cwords and all available options - completion_type = get_path_completion_type(cwords, cword, - flattened_opts) + completion_type = get_path_completion_type(cwords, cword, flattened_opts) if completion_type: - subcommands = list(auto_complete_paths(current, - completion_type)) + subcommands = list(auto_complete_paths(current, completion_type)) - print(' '.join([x for x in subcommands if x.startswith(current)])) + print(" ".join([x for x in subcommands if x.startswith(current)])) sys.exit(1) @@ -117,16 +116,16 @@ def get_path_completion_type(cwords, cword, opts): :param opts: The available options to check :return: path completion type (``file``, ``dir``, ``path`` or None) """ - if cword < 2 or not cwords[cword - 2].startswith('-'): + if cword < 2 or not cwords[cword - 2].startswith("-"): return None for opt in opts: if opt.help == optparse.SUPPRESS_HELP: continue - for o in str(opt).split('/'): - if cwords[cword - 2].split('=')[0] == o: + for o in str(opt).split("/"): + if cwords[cword - 2].split("=")[0] == o: if not opt.metavar or any( - x in ('path', 'file', 'dir') - for x in opt.metavar.split('/')): + x in ("path", "file", "dir") for x in opt.metavar.split("/") + ): return opt.metavar return None @@ -148,15 +147,16 @@ def auto_complete_paths(current, completion_type): return filename = os.path.normcase(filename) # list all files that start with ``filename`` - file_list = (x for x in os.listdir(current_path) - if os.path.normcase(x).startswith(filename)) + file_list = ( + x for x in os.listdir(current_path) if os.path.normcase(x).startswith(filename) + ) for f in file_list: opt = os.path.join(current_path, f) comp_file = os.path.normcase(os.path.join(directory, f)) # complete regular files when there is not ``<dir>`` after option # complete directories when there is ``<file>``, ``<path>`` or # ``<dir>``after option - if completion_type != 'dir' and os.path.isfile(opt): + if completion_type != "dir" and os.path.isfile(opt): yield comp_file elif os.path.isdir(opt): - yield os.path.join(comp_file, '') + yield os.path.join(comp_file, "") diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index 87944e49577..6d5798a0bdc 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -34,7 +34,7 @@ from pip._internal.utils.temp_dir import global_tempdir_manager, tempdir_registry from pip._internal.utils.virtualenv import running_under_virtualenv -__all__ = ['Command'] +__all__ = ["Command"] logger = logging.getLogger(__name__) @@ -51,7 +51,7 @@ def __init__(self, name, summary, isolated=False): self.summary = summary self.parser = ConfigOptionParser( usage=self.usage, - prog=f'{get_prog()} {name}', + prog=f"{get_prog()} {name}", formatter=UpdatingDefaultsHelpFormatter(), add_help_option=False, name=name, @@ -62,7 +62,7 @@ def __init__(self, name, summary, isolated=False): self.tempdir_registry = None # type: Optional[TempDirRegistry] # Commands should add options to this option group - optgroup_name = f'{self.name.capitalize()} Options' + optgroup_name = f"{self.name.capitalize()} Options" self.cmd_opts = optparse.OptionGroup(self.parser, optgroup_name) # Add the general options @@ -86,7 +86,7 @@ def handle_pip_version_check(self, options): """ # Make sure we do the pip version check if the index_group options # are present. - assert not hasattr(options, 'no_index') + assert not hasattr(options, "no_index") def run(self, options, args): # type: (Values, List[Any]) -> int @@ -131,17 +131,15 @@ def _main(self, args): # This also affects isolated builds and it should. if options.no_input: - os.environ['PIP_NO_INPUT'] = '1' + os.environ["PIP_NO_INPUT"] = "1" if options.exists_action: - os.environ['PIP_EXISTS_ACTION'] = ' '.join(options.exists_action) + os.environ["PIP_EXISTS_ACTION"] = " ".join(options.exists_action) 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( - 'Could not find an activated virtualenv (required).' - ) + logger.critical("Could not find an activated virtualenv (required).") sys.exit(VIRTUALENV_NOT_FOUND) if options.cache_dir: @@ -171,7 +169,7 @@ def _main(self, args): issue=8333, ) - if '2020-resolver' in options.features_enabled: + if "2020-resolver" in options.features_enabled: logger.warning( "--use-feature=2020-resolver no longer has any effect, " "since it is now the default dependency resolver in pip. " @@ -184,35 +182,39 @@ def _main(self, args): return status except PreviousBuildDirError as exc: logger.critical(str(exc)) - logger.debug('Exception information:', exc_info=True) + logger.debug("Exception information:", exc_info=True) return PREVIOUS_BUILD_DIR_ERROR - except (InstallationError, UninstallationError, BadCommand, - NetworkConnectionError) as exc: + except ( + InstallationError, + UninstallationError, + BadCommand, + NetworkConnectionError, + ) as exc: logger.critical(str(exc)) - logger.debug('Exception information:', exc_info=True) + logger.debug("Exception information:", exc_info=True) return ERROR except CommandError as exc: - logger.critical('%s', exc) - logger.debug('Exception information:', exc_info=True) + logger.critical("%s", exc) + logger.debug("Exception information:", exc_info=True) return ERROR except BrokenStdoutLoggingError: # Bypass our logger and write any remaining messages to stderr # because stdout no longer works. - print('ERROR: Pipe to stdout was broken', file=sys.stderr) + print("ERROR: Pipe to stdout was broken", file=sys.stderr) if level_number <= logging.DEBUG: traceback.print_exc(file=sys.stderr) return ERROR except KeyboardInterrupt: - logger.critical('Operation cancelled by user') - logger.debug('Exception information:', exc_info=True) + logger.critical("Operation cancelled by user") + logger.debug("Exception information:", exc_info=True) return ERROR except BaseException: - logger.critical('Exception:', exc_info=True) + logger.critical("Exception:", exc_info=True) return UNKNOWN_ERROR finally: diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index 0a7e28685f6..3075de94e39 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -41,8 +41,8 @@ def raise_option_error(parser, option, msg): option: an Option instance. msg: the error text. """ - msg = f'{option} error: {msg}' - msg = textwrap.fill(' '.join(msg.split())) + msg = f"{option} error: {msg}" + msg = textwrap.fill(" ".join(msg.split())) parser.error(msg) @@ -53,8 +53,8 @@ def make_option_group(group, parser): group -- assumed to be dict with 'name' and 'options' keys parser -- an optparse Parser """ - option_group = OptionGroup(parser, group['name']) - for option in group['options']: + option_group = OptionGroup(parser, group["name"]) + for option in group["options"]: option_group.add_option(option()) return option_group @@ -73,13 +73,15 @@ def check_install_build_global(options, check_options=None): def getname(n): # type: (str) -> Optional[Any] return getattr(check_options, n, None) + names = ["build_options", "global_options", "install_options"] if any(map(getname, names)): control = options.format_control control.disallow_binaries() warnings.warn( - 'Disabling all use of wheels due to the use of --build-option ' - '/ --global-option / --install-option.', stacklevel=2, + "Disabling all use of wheels due to the use of --build-option " + "/ --global-option / --install-option.", + stacklevel=2, ) @@ -90,17 +92,18 @@ def check_dist_restriction(options, check_target=False): :param options: The OptionParser options. :param check_target: Whether or not to check if --target is being used. """ - dist_restriction_set = any([ - options.python_version, - options.platforms, - options.abis, - options.implementation, - ]) - - binary_only = FormatControl(set(), {':all:'}) + dist_restriction_set = any( + [ + options.python_version, + options.platforms, + options.abis, + options.implementation, + ] + ) + + binary_only = FormatControl(set(), {":all:"}) sdist_dependencies_allowed = ( - options.format_control != binary_only and - not options.ignore_dependencies + options.format_control != binary_only and not options.ignore_dependencies ) # Installations or downloads using dist restrictions must not combine @@ -146,10 +149,11 @@ class PipOption(Option): help_ = partial( Option, - '-h', '--help', - dest='help', - action='help', - help='Show help.', + "-h", + "--help", + dest="help", + action="help", + help="Show help.", ) # type: Callable[..., Option] isolated_mode = partial( @@ -167,111 +171,119 @@ class PipOption(Option): require_virtualenv = partial( Option, # Run only if inside a virtualenv, bail if not. - '--require-virtualenv', '--require-venv', - dest='require_venv', - action='store_true', + "--require-virtualenv", + "--require-venv", + dest="require_venv", + action="store_true", default=False, - help=SUPPRESS_HELP + help=SUPPRESS_HELP, ) # type: Callable[..., Option] verbose = partial( Option, - '-v', '--verbose', - dest='verbose', - action='count', + "-v", + "--verbose", + dest="verbose", + action="count", default=0, - help='Give more output. Option is additive, and can be used up to 3 times.' + help="Give more output. Option is additive, and can be used up to 3 times.", ) # type: Callable[..., Option] no_color = partial( Option, - '--no-color', - dest='no_color', - action='store_true', + "--no-color", + dest="no_color", + action="store_true", default=False, help="Suppress colored output.", ) # type: Callable[..., Option] version = partial( Option, - '-V', '--version', - dest='version', - action='store_true', - help='Show version and exit.', + "-V", + "--version", + dest="version", + action="store_true", + help="Show version and exit.", ) # type: Callable[..., Option] quiet = partial( Option, - '-q', '--quiet', - dest='quiet', - action='count', + "-q", + "--quiet", + 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).' + "Give less output. Option is additive, and can be used up to 3" + " times (corresponding to WARNING, ERROR, and CRITICAL logging" + " levels)." ), ) # type: Callable[..., Option] progress_bar = partial( Option, - '--progress-bar', - dest='progress_bar', - type='choice', + "--progress-bar", + dest="progress_bar", + type="choice", choices=list(BAR_TYPES.keys()), - default='on', + default="on", help=( - 'Specify type of progress to be displayed [' + - '|'.join(BAR_TYPES.keys()) + '] (default: %default)' + "Specify type of progress to be displayed [" + + "|".join(BAR_TYPES.keys()) + + "] (default: %default)" ), ) # type: Callable[..., Option] log = partial( PipOption, - "--log", "--log-file", "--local-log", + "--log", + "--log-file", + "--local-log", dest="log", metavar="path", type="path", - help="Path to a verbose appending log." + help="Path to a verbose appending log.", ) # type: Callable[..., Option] no_input = partial( Option, # Don't ask for input - '--no-input', - dest='no_input', - action='store_true', + "--no-input", + dest="no_input", + action="store_true", default=False, - help="Disable prompting for input." + help="Disable prompting for input.", ) # type: Callable[..., Option] proxy = partial( Option, - '--proxy', - dest='proxy', - type='str', - default='', - help="Specify a proxy in the form [user:passwd@]proxy.server:port." + "--proxy", + dest="proxy", + type="str", + default="", + help="Specify a proxy in the form [user:passwd@]proxy.server:port.", ) # type: Callable[..., Option] retries = partial( Option, - '--retries', - dest='retries', - type='int', + "--retries", + dest="retries", + type="int", default=5, help="Maximum number of retries each connection should attempt " - "(default %default times).", + "(default %default times).", ) # type: Callable[..., Option] timeout = partial( Option, - '--timeout', '--default-timeout', - metavar='sec', - dest='timeout', - type='float', + "--timeout", + "--default-timeout", + metavar="sec", + dest="timeout", + type="float", default=15, - help='Set the socket timeout (default %default seconds).', + help="Set the socket timeout (default %default seconds).", ) # type: Callable[..., Option] @@ -279,88 +291,91 @@ def exists_action(): # type: () -> Option return Option( # Option when path already exist - '--exists-action', - dest='exists_action', - type='choice', - choices=['s', 'i', 'w', 'b', 'a'], + "--exists-action", + dest="exists_action", + type="choice", + choices=["s", "i", "w", "b", "a"], default=[], - action='append', - metavar='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( PipOption, - '--cert', - dest='cert', - type='path', - metavar='path', + "--cert", + dest="cert", + type="path", + metavar="path", help="Path to alternate CA bundle.", ) # type: Callable[..., Option] client_cert = partial( PipOption, - '--client-cert', - dest='client_cert', - type='path', + "--client-cert", + dest="client_cert", + type="path", default=None, - metavar='path', + 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: Callable[..., Option] index_url = partial( Option, - '-i', '--index-url', '--pypi-url', - dest='index_url', - metavar='URL', + "-i", + "--index-url", + "--pypi-url", + dest="index_url", + metavar="URL", default=PyPI.simple_url, help="Base URL of the 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.", + "This should point to a repository compliant with PEP 503 " + "(the simple repository API) or a local directory laid out " + "in the same format.", ) # type: Callable[..., Option] def extra_index_url(): # type: () -> Option return Option( - '--extra-index-url', - dest='extra_index_urls', - metavar='URL', - action='append', + "--extra-index-url", + dest="extra_index_urls", + metavar="URL", + action="append", default=[], help="Extra URLs of package indexes to use in addition to " - "--index-url. Should follow the same rules as " - "--index-url.", + "--index-url. Should follow the same rules as " + "--index-url.", ) no_index = partial( Option, - '--no-index', - dest='no_index', - action='store_true', + "--no-index", + 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: Callable[..., Option] def find_links(): # type: () -> Option return Option( - '-f', '--find-links', - dest='find_links', - action='append', + "-f", + "--find-links", + dest="find_links", + action="append", default=[], - metavar='url', + metavar="url", help="If a URL or path to an html file, then parse for links to " - "archives such as sdist (.tar.gz) or wheel (.whl) files. " - "If a local path or file:// URL that's a directory, " - "then look for archives in the directory listing. " - "Links to VCS project URLs are not supported.", + "archives such as sdist (.tar.gz) or wheel (.whl) files. " + "If a local path or file:// URL that's a directory, " + "then look for archives in the directory listing. " + "Links to VCS project URLs are not supported.", ) @@ -373,46 +388,51 @@ def trusted_host(): metavar="HOSTNAME", default=[], help="Mark this host or host:port pair as trusted, even though it " - "does not have valid or any HTTPS.", + "does not have valid or any HTTPS.", ) def constraints(): # type: () -> Option return Option( - '-c', '--constraint', - dest='constraints', - action='append', + "-c", + "--constraint", + dest="constraints", + action="append", default=[], - metavar='file', - help='Constrain versions using the given constraints file. ' - 'This option can be used multiple times.' + metavar="file", + help="Constrain versions using the given constraints file. " + "This option can be used multiple times.", ) def requirements(): # type: () -> Option return Option( - '-r', '--requirement', - dest='requirements', - action='append', + "-r", + "--requirement", + dest="requirements", + action="append", default=[], - metavar='file', - help='Install from the given requirements file. ' - 'This option can be used multiple times.' + metavar="file", + help="Install from the given requirements file. " + "This option can be used multiple times.", ) def editable(): # type: () -> Option return Option( - '-e', '--editable', - dest='editables', - action='append', + "-e", + "--editable", + dest="editables", + action="append", default=[], - metavar='path/url', - help=('Install a project in editable mode (i.e. setuptools ' - '"develop mode") from a local project path or a VCS url.'), + metavar="path/url", + help=( + "Install a project in editable mode (i.e. setuptools " + '"develop mode") from a local project path or a VCS url.' + ), ) @@ -424,16 +444,19 @@ def _handle_src(option, opt_str, value, parser): src = partial( PipOption, - '--src', '--source', '--source-dir', '--source-directory', - dest='src_dir', - type='path', - metavar='dir', + "--src", + "--source", + "--source-dir", + "--source-directory", + dest="src_dir", + type="path", + metavar="dir", default=get_src_prefix(), - action='callback', + action="callback", callback=_handle_src, - help='Directory to check out editable projects into. ' + 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".' + 'The default for global installs is "<current dir>/src".', ) # type: Callable[..., Option] @@ -447,7 +470,9 @@ def _handle_no_binary(option, opt_str, value, parser): # type: (Option, str, str, OptionParser) -> None existing = _get_format_control(parser.values, option) FormatControl.handle_mutual_excludes( - value, existing.no_binary, existing.only_binary, + value, + existing.no_binary, + existing.only_binary, ) @@ -455,7 +480,9 @@ def _handle_only_binary(option, opt_str, value, parser): # type: (Option, str, str, OptionParser) -> None existing = _get_format_control(parser.values, option) FormatControl.handle_mutual_excludes( - value, existing.only_binary, existing.no_binary, + value, + existing.only_binary, + existing.no_binary, ) @@ -463,15 +490,18 @@ def no_binary(): # type: () -> Option format_control = FormatControl(set(), set()) return Option( - "--no-binary", dest="format_control", action="callback", - callback=_handle_no_binary, type="str", + "--no-binary", + dest="format_control", + action="callback", + callback=_handle_no_binary, + type="str", default=format_control, - help='Do not use binary packages. Can be supplied multiple times, and ' - 'each time adds to the existing value. Accepts either ":all:" to ' - 'disable all binary packages, ":none:" to empty the set (notice ' - 'the colons), or one or more package names with commas between ' - 'them (no colons). Note that some packages are tricky to compile ' - 'and may fail to install when this option is used on them.', + help="Do not use binary packages. Can be supplied multiple times, and " + 'each time adds to the existing value. Accepts either ":all:" to ' + 'disable all binary packages, ":none:" to empty the set (notice ' + "the colons), or one or more package names with commas between " + "them (no colons). Note that some packages are tricky to compile " + "and may fail to install when this option is used on them.", ) @@ -479,28 +509,33 @@ def only_binary(): # type: () -> Option format_control = FormatControl(set(), set()) return Option( - "--only-binary", dest="format_control", action="callback", - callback=_handle_only_binary, type="str", + "--only-binary", + dest="format_control", + action="callback", + callback=_handle_only_binary, + type="str", default=format_control, - help='Do not use source packages. Can be supplied multiple times, and ' - 'each time adds to the existing value. Accepts either ":all:" to ' - '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.', + help="Do not use source packages. Can be supplied multiple times, and " + 'each time adds to the existing value. Accepts either ":all:" to ' + '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.", ) platforms = partial( Option, - '--platform', - dest='platforms', - metavar='platform', - action='append', + "--platform", + dest="platforms", + metavar="platform", + action="append", default=None, - help=("Only use wheels compatible with <platform>. Defaults to the " - "platform of the running system. Use this option multiple times to " - "specify multiple platforms supported by the target interpreter."), + help=( + "Only use wheels compatible with <platform>. Defaults to the " + "platform of the running system. Use this option multiple times to " + "specify multiple platforms supported by the target interpreter." + ), ) # type: Callable[..., Option] @@ -517,9 +552,9 @@ def _convert_python_version(value): # The empty string is the same as not providing a value. return (None, None) - parts = value.split('.') + parts = value.split(".") if len(parts) > 3: - return ((), 'at most three version parts are allowed') + return ((), "at most three version parts are allowed") if len(parts) == 1: # Then we are in the case of "3" or "37". @@ -530,7 +565,7 @@ def _convert_python_version(value): try: version_info = tuple(int(part) for part in parts) except ValueError: - return ((), 'each version part must be an integer') + return ((), "each version part must be an integer") return (version_info, None) @@ -542,10 +577,9 @@ def _handle_python_version(option, opt_str, value, parser): """ version_info, error_msg = _convert_python_version(value) if error_msg is not None: - msg = ( - 'invalid --python-version value: {!r}: {}'.format( - value, error_msg, - ) + msg = "invalid --python-version value: {!r}: {}".format( + value, + error_msg, ) raise_option_error(parser, option=option, msg=msg) @@ -554,49 +588,56 @@ def _handle_python_version(option, opt_str, value, parser): python_version = partial( Option, - '--python-version', - dest='python_version', - metavar='python_version', - action='callback', - callback=_handle_python_version, type='str', + "--python-version", + dest="python_version", + metavar="python_version", + action="callback", + callback=_handle_python_version, + type="str", default=None, - help=dedent("""\ + help=dedent( + """\ The Python interpreter version to use for wheel and "Requires-Python" compatibility checks. Defaults to a version derived from the running interpreter. The version can be specified using up to three dot-separated integers (e.g. "3" for 3.0.0, "3.7" for 3.7.0, or "3.7.3"). A major-minor version can also be given as a string without dots (e.g. "37" for 3.7.0). - """), + """ + ), ) # type: Callable[..., Option] implementation = partial( Option, - '--implementation', - dest='implementation', - metavar='implementation', + "--implementation", + dest="implementation", + metavar="implementation", default=None, - help=("Only use wheels compatible with Python " - "implementation <implementation>, e.g. 'pp', 'jy', 'cp', " - " or 'ip'. If not specified, then the current " - "interpreter implementation is used. Use 'py' to force " - "implementation-agnostic wheels."), + help=( + "Only use wheels compatible with Python " + "implementation <implementation>, e.g. 'pp', 'jy', 'cp', " + " or 'ip'. If not specified, then the current " + "interpreter implementation is used. Use 'py' to force " + "implementation-agnostic wheels." + ), ) # type: Callable[..., Option] abis = partial( Option, - '--abi', - dest='abis', - metavar='abi', - action='append', + "--abi", + dest="abis", + metavar="abi", + action="append", default=None, - help=("Only use wheels compatible with Python abi <abi>, e.g. 'pypy_41'. " - "If not specified, then the current interpreter abi tag is used. " - "Use this option multiple times to specify multiple abis supported " - "by the target interpreter. Generally you will need to specify " - "--implementation, --platform, and --python-version when using this " - "option."), + help=( + "Only use wheels compatible with Python abi <abi>, e.g. 'pypy_41'. " + "If not specified, then the current interpreter abi tag is used. " + "Use this option multiple times to specify multiple abis supported " + "by the target interpreter. Generally you will need to specify " + "--implementation, --platform, and --python-version when using this " + "option." + ), ) # type: Callable[..., Option] @@ -627,7 +668,7 @@ def prefer_binary(): dest="prefer_binary", action="store_true", default=False, - help="Prefer older binary packages over newer source packages." + help="Prefer older binary packages over newer source packages.", ) @@ -637,8 +678,8 @@ def prefer_binary(): dest="cache_dir", default=USER_CACHE_DIR, metavar="dir", - type='path', - help="Store the cache data in <dir>." + type="path", + help="Store the cache data in <dir>.", ) # type: Callable[..., Option] @@ -681,39 +722,43 @@ def _handle_no_cache_dir(option, opt, value, parser): no_deps = partial( Option, - '--no-deps', '--no-dependencies', - dest='ignore_dependencies', - action='store_true', + "--no-deps", + "--no-dependencies", + dest="ignore_dependencies", + action="store_true", default=False, help="Don't install package dependencies.", ) # type: Callable[..., Option] build_dir = partial( PipOption, - '-b', '--build', '--build-dir', '--build-directory', - dest='build_dir', - type='path', - metavar='dir', + "-b", + "--build", + "--build-dir", + "--build-directory", + dest="build_dir", + type="path", + metavar="dir", help=SUPPRESS_HELP, ) # type: Callable[..., Option] ignore_requires_python = partial( Option, - '--ignore-requires-python', - dest='ignore_requires_python', - action='store_true', - help='Ignore the Requires-Python information.' + "--ignore-requires-python", + dest="ignore_requires_python", + action="store_true", + help="Ignore the Requires-Python information.", ) # type: Callable[..., Option] no_build_isolation = partial( Option, - '--no-build-isolation', - dest='build_isolation', - action='store_false', + "--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.' + 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: Callable[..., Option] @@ -743,62 +788,62 @@ def _handle_no_use_pep517(option, opt, value, parser): use_pep517 = partial( Option, - '--use-pep517', - dest='use_pep517', - action='store_true', + "--use-pep517", + dest="use_pep517", + action="store_true", default=None, - help='Use PEP 517 for building source distributions ' - '(use --no-use-pep517 to force legacy behaviour).' + help="Use PEP 517 for building source distributions " + "(use --no-use-pep517 to force legacy behaviour).", ) # type: Any no_use_pep517 = partial( Option, - '--no-use-pep517', - dest='use_pep517', - action='callback', + "--no-use-pep517", + dest="use_pep517", + action="callback", callback=_handle_no_use_pep517, default=None, - help=SUPPRESS_HELP + help=SUPPRESS_HELP, ) # type: Any install_options = partial( Option, - '--install-option', - dest='install_options', - action='append', - metavar='options', + "--install-option", + dest="install_options", + action="append", + metavar="options", help="Extra arguments to be supplied to the setup.py install " - "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.", + '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.", ) # type: Callable[..., Option] global_options = partial( Option, - '--global-option', - dest='global_options', - action='append', - metavar='options', + "--global-option", + dest="global_options", + 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: Callable[..., Option] no_clean = partial( Option, - '--no-clean', - action='store_true', + "--no-clean", + action="store_true", default=False, - help="Don't clean up build directories." + help="Don't clean up build directories.", ) # type: Callable[..., Option] pre = partial( Option, - '--pre', - action='store_true', + "--pre", + action="store_true", default=False, help="Include pre-release and development versions. By default, " - "pip only finds stable versions.", + "pip only finds stable versions.", ) # type: Callable[..., Option] disable_pip_version_check = partial( @@ -808,7 +853,7 @@ def _handle_no_use_pep517(option, opt, value, parser): 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: Callable[..., Option] @@ -819,105 +864,106 @@ def _handle_merge_hash(option, opt_str, value, parser): if not parser.values.hashes: parser.values.hashes = {} try: - algo, digest = value.split(':', 1) + algo, digest = value.split(":", 1) except ValueError: - parser.error('Arguments to {} must be a hash name ' # noqa - 'followed by a value, like --hash=sha256:' - 'abcde...'.format(opt_str)) + parser.error( + "Arguments to {} must be a hash name " # noqa + "followed by a value, like --hash=sha256:" + "abcde...".format(opt_str) + ) if algo not in STRONG_HASHES: - parser.error('Allowed hash algorithms for {} are {}.'.format( # noqa - opt_str, ', '.join(STRONG_HASHES))) + parser.error( + "Allowed hash algorithms for {} are {}.".format( # noqa + opt_str, ", ".join(STRONG_HASHES) + ) + ) parser.values.hashes.setdefault(algo, []).append(digest) hash = partial( Option, - '--hash', + "--hash", # Hash values eventually end up in InstallRequirement.hashes due to # __dict__ copying in process_line(). - dest='hashes', - action='callback', + dest="hashes", + action="callback", callback=_handle_merge_hash, - type='string', + 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: Callable[..., Option] require_hashes = partial( Option, - '--require-hashes', - dest='require_hashes', - action='store_true', + "--require-hashes", + dest="require_hashes", + action="store_true", 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.', + 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.", ) # type: Callable[..., Option] list_path = partial( PipOption, - '--path', - dest='path', - type='path', - action='append', - help='Restrict to the specified installation path for listing ' - 'packages (can be used multiple times).' + "--path", + dest="path", + type="path", + action="append", + help="Restrict to the specified installation path for listing " + "packages (can be used multiple times).", ) # type: Callable[..., Option] def check_list_path_option(options): # type: (Values) -> None if options.path and (options.user or options.local): - raise CommandError( - "Cannot combine '--path' with '--user' or '--local'" - ) + raise CommandError("Cannot combine '--path' with '--user' or '--local'") list_exclude = partial( PipOption, - '--exclude', - dest='excludes', - action='append', - metavar='package', - type='package_name', + "--exclude", + dest="excludes", + action="append", + metavar="package", + type="package_name", help="Exclude specified package from the output", ) # type: Callable[..., Option] no_python_version_warning = partial( Option, - '--no-python-version-warning', - dest='no_python_version_warning', - action='store_true', + "--no-python-version-warning", + dest="no_python_version_warning", + action="store_true", default=False, - help='Silence deprecation warnings for upcoming unsupported Pythons.', + help="Silence deprecation warnings for upcoming unsupported Pythons.", ) # type: Callable[..., Option] use_new_feature = partial( Option, - '--use-feature', - dest='features_enabled', - metavar='feature', - action='append', + "--use-feature", + dest="features_enabled", + metavar="feature", + action="append", default=[], - choices=['2020-resolver', 'fast-deps'], - help='Enable new functionality, that may be backward incompatible.', + choices=["2020-resolver", "fast-deps"], + help="Enable new functionality, that may be backward incompatible.", ) # type: Callable[..., Option] use_deprecated_feature = partial( Option, - '--use-deprecated', - dest='deprecated_features_enabled', - metavar='feature', - action='append', + "--use-deprecated", + dest="deprecated_features_enabled", + metavar="feature", + action="append", default=[], - choices=['legacy-resolver'], - help=( - 'Enable deprecated functionality, that will be removed in the future.' - ), + choices=["legacy-resolver"], + help=("Enable deprecated functionality, that will be removed in the future."), ) # type: Callable[..., Option] @@ -926,8 +972,8 @@ def check_list_path_option(options): ########## general_group = { - 'name': 'General Options', - 'options': [ + "name": "General Options", + "options": [ help_, isolated_mode, require_virtualenv, @@ -950,15 +996,15 @@ def check_list_path_option(options): no_python_version_warning, use_new_feature, use_deprecated_feature, - ] + ], } # type: Dict[str, Any] index_group = { - 'name': 'Package Index Options', - 'options': [ + "name": "Package Index Options", + "options": [ index_url, extra_index_url, no_index, find_links, - ] + ], } # type: Dict[str, Any] diff --git a/src/pip/_internal/cli/command_context.py b/src/pip/_internal/cli/command_context.py index 0f7c6afc4f9..375a2e3660b 100644 --- a/src/pip/_internal/cli/command_context.py +++ b/src/pip/_internal/cli/command_context.py @@ -1,7 +1,7 @@ from contextlib import ExitStack, contextmanager from typing import ContextManager, Iterator, TypeVar -_T = TypeVar('_T', covariant=True) +_T = TypeVar("_T", covariant=True) class CommandContextMixIn: diff --git a/src/pip/_internal/cli/main.py b/src/pip/_internal/cli/main.py index 6f107a2e7c7..7ae074b59d5 100644 --- a/src/pip/_internal/cli/main.py +++ b/src/pip/_internal/cli/main.py @@ -41,6 +41,7 @@ # call to main. As it is not safe to do any processing after calling # main, this should not be an issue in practice. + def main(args=None): # type: (Optional[List[str]]) -> int if args is None: @@ -61,7 +62,7 @@ def main(args=None): # Needed for locale.getpreferredencoding(False) to work # in pip._internal.utils.encoding.auto_decode try: - locale.setlocale(locale.LC_ALL, '') + 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) diff --git a/src/pip/_internal/cli/main_parser.py b/src/pip/_internal/cli/main_parser.py index 3cc00d3a802..d0f58fe421b 100644 --- a/src/pip/_internal/cli/main_parser.py +++ b/src/pip/_internal/cli/main_parser.py @@ -16,14 +16,13 @@ def create_main_parser(): # type: () -> ConfigOptionParser - """Creates and returns the main parser for pip's CLI - """ + """Creates and returns the main parser for pip's CLI""" parser = ConfigOptionParser( - usage='\n%prog <command> [options]', + usage="\n%prog <command> [options]", add_help_option=False, formatter=UpdatingDefaultsHelpFormatter(), - name='global', + name="global", prog=get_prog(), ) parser.disable_interspersed_args() @@ -38,11 +37,11 @@ def create_main_parser(): parser.main = True # type: ignore # create command listing for description - description = [''] + [ - f'{name:27} {command_info.summary}' + description = [""] + [ + f"{name:27} {command_info.summary}" for name, command_info in commands_dict.items() ] - parser.description = '\n'.join(description) + parser.description = "\n".join(description) return parser @@ -67,7 +66,7 @@ def parse_command(args): sys.exit() # pip || pip help -> print_help() - if not args_else or (args_else[0] == 'help' and len(args_else) == 1): + if not args_else or (args_else[0] == "help" and len(args_else) == 1): parser.print_help() sys.exit() @@ -81,7 +80,7 @@ def parse_command(args): if guess: msg.append(f'maybe you meant "{guess}"') - raise CommandError(' - '.join(msg)) + raise CommandError(" - ".join(msg)) # all the args without the subcommand cmd_args = args[:] diff --git a/src/pip/_internal/cli/parser.py b/src/pip/_internal/cli/parser.py index d3958727b22..8fd3d2d6b31 100644 --- a/src/pip/_internal/cli/parser.py +++ b/src/pip/_internal/cli/parser.py @@ -23,15 +23,15 @@ class PrettyHelpFormatter(optparse.IndentedHelpFormatter): def __init__(self, *args, **kwargs): # help position must be aligned with __init__.parseopts.description - kwargs['max_help_position'] = 30 - kwargs['indent_increment'] = 1 - kwargs['width'] = shutil.get_terminal_size()[0] - 2 + kwargs["max_help_position"] = 30 + kwargs["indent_increment"] = 1 + kwargs["width"] = shutil.get_terminal_size()[0] - 2 super().__init__(*args, **kwargs) def format_option_strings(self, option): return self._format_option_strings(option) - def _format_option_strings(self, option, mvarfmt=' <{}>', optsep=', '): + def _format_option_strings(self, option, mvarfmt=" <{}>", optsep=", "): """ Return a comma-separated list of option strings and metavars. @@ -52,49 +52,48 @@ def _format_option_strings(self, option, mvarfmt=' <{}>', optsep=', '): metavar = option.metavar or option.dest.lower() opts.append(mvarfmt.format(metavar.lower())) - return ''.join(opts) + return "".join(opts) def format_heading(self, heading): - if heading == 'Options': - return '' - return heading + ':\n' + if heading == "Options": + return "" + return heading + ":\n" def format_usage(self, usage): """ Ensure there is only one newline between usage and the first heading if there is no description. """ - msg = '\nUsage: {}\n'.format( - self.indent_lines(textwrap.dedent(usage), " ")) + msg = "\nUsage: {}\n".format(self.indent_lines(textwrap.dedent(usage), " ")) return msg def format_description(self, description): # leave full control over description to us if description: - if hasattr(self.parser, 'main'): - label = 'Commands' + if hasattr(self.parser, "main"): + label = "Commands" else: - label = 'Description' + label = "Description" # some doc strings have initial newlines, some don't - description = description.lstrip('\n') + description = description.lstrip("\n") # some doc strings have final newlines and spaces, some don't description = description.rstrip() # dedent, then reindent description = self.indent_lines(textwrap.dedent(description), " ") - description = f'{label}:\n{description}\n' + description = f"{label}:\n{description}\n" return description else: - return '' + return "" def format_epilog(self, epilog): # leave full control over epilog to us if epilog: return epilog else: - return '' + return "" def indent_lines(self, text, indent): - new_lines = [indent + line for line in text.split('\n')] + new_lines = [indent + line for line in text.split("\n")] return "\n".join(new_lines) @@ -114,7 +113,7 @@ def expand_default(self, option): default_values = self.parser.defaults.get(option.dest) help_text = super().expand_default(option) - if default_values and option.metavar == 'URL': + if default_values and option.metavar == "URL": if isinstance(default_values, str): default_values = [default_values] @@ -123,14 +122,12 @@ def expand_default(self, option): default_values = [] for val in default_values: - help_text = help_text.replace( - val, redact_auth_from_url(val)) + help_text = help_text.replace(val, redact_auth_from_url(val)) return help_text class CustomOptionParser(optparse.OptionParser): - def insert_option_group(self, idx, *args, **kwargs): """Insert an OptionGroup at a given position.""" group = self.add_option_group(*args, **kwargs) @@ -186,7 +183,7 @@ def _get_ordered_configuration_items(self): if not val: logger.debug( "Ignoring configuration key '%s' as it's value is empty.", - section_key + section_key, ) continue @@ -210,7 +207,7 @@ def _update_defaults(self, defaults): # Then set the options with those values 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 @@ -218,31 +215,31 @@ def _update_defaults(self, defaults): if option is None: continue - if option.action in ('store_true', 'store_false'): + if option.action in ("store_true", "store_false"): try: val = strtobool(val) except ValueError: self.error( - '{} is not a valid value for {} option, ' # noqa - 'please specify a boolean value like yes/no, ' - 'true/false or 1/0 instead.'.format(val, key) + "{} is not a valid value for {} option, " # noqa + "please specify a boolean value like yes/no, " + "true/false or 1/0 instead.".format(val, key) ) - elif option.action == 'count': + elif option.action == "count": with suppress(ValueError): val = strtobool(val) with suppress(ValueError): val = int(val) if not isinstance(val, int) or val < 0: self.error( - '{} is not a valid value for {} option, ' # noqa - 'please instead specify either a non-negative integer ' - 'or a boolean value like yes/no or false/true ' - 'which is equivalent to 1/0.'.format(val, key) + "{} is not a valid value for {} option, " # noqa + "please instead specify either a non-negative integer " + "or a boolean value like yes/no or false/true " + "which is equivalent to 1/0.".format(val, key) ) - elif option.action == 'append': + elif option.action == "append": val = val.split() val = [self.check_default(option, key, v) for v in val] - elif option.action == 'callback': + elif option.action == "callback": late_eval.add(option.dest) opt_str = option.get_opt_string() val = option.convert_value(opt_str, val) diff --git a/src/pip/_internal/cli/progress_bars.py b/src/pip/_internal/cli/progress_bars.py index 50cb74f5b7a..3064c85697b 100644 --- a/src/pip/_internal/cli/progress_bars.py +++ b/src/pip/_internal/cli/progress_bars.py @@ -108,7 +108,6 @@ def handle_sigint(self, signum, frame): # type: ignore class SilentBar(Bar): - def update(self): # type: () -> None pass @@ -123,14 +122,11 @@ class BlueEmojiBar(IncrementalBar): class DownloadProgressMixin: - def __init__(self, *args, **kwargs): # type: (List[Any], Dict[Any, Any]) -> None # https://github.com/python/mypy/issues/5887 super().__init__(*args, **kwargs) # type: ignore - self.message = (" " * ( - get_indentation() + 2 - )) + self.message # type: str + self.message = (" " * (get_indentation() + 2)) + self.message # type: str @property def downloaded(self): @@ -162,7 +158,6 @@ def iter(self, it): # type: ignore class WindowsMixin: - def __init__(self, *args, **kwargs): # type: (List[Any], Dict[Any, Any]) -> None # The Windows terminal does not support the hide/show cursor ANSI codes @@ -192,16 +187,14 @@ def __init__(self, *args, **kwargs): self.file.flush = lambda: self.file.wrapped.flush() -class BaseDownloadProgressBar(WindowsMixin, InterruptibleMixin, - DownloadProgressMixin): +class BaseDownloadProgressBar(WindowsMixin, InterruptibleMixin, DownloadProgressMixin): file = sys.stdout message = "%(percent)d%%" suffix = "%(downloaded)s %(download_speed)s %(pretty_eta)s" -class DefaultDownloadProgressBar(BaseDownloadProgressBar, - _BaseBar): +class DefaultDownloadProgressBar(BaseDownloadProgressBar, _BaseBar): pass @@ -209,23 +202,21 @@ class DownloadSilentBar(BaseDownloadProgressBar, SilentBar): pass -class DownloadBar(BaseDownloadProgressBar, - Bar): +class DownloadBar(BaseDownloadProgressBar, Bar): pass -class DownloadFillingCirclesBar(BaseDownloadProgressBar, - FillingCirclesBar): +class DownloadFillingCirclesBar(BaseDownloadProgressBar, FillingCirclesBar): pass -class DownloadBlueEmojiProgressBar(BaseDownloadProgressBar, - BlueEmojiBar): +class DownloadBlueEmojiProgressBar(BaseDownloadProgressBar, BlueEmojiBar): pass -class DownloadProgressSpinner(WindowsMixin, InterruptibleMixin, - DownloadProgressMixin, Spinner): +class DownloadProgressSpinner( + WindowsMixin, InterruptibleMixin, DownloadProgressMixin, Spinner +): file = sys.stdout suffix = "%(downloaded)s %(download_speed)s" @@ -241,13 +232,15 @@ def update(self): message = self.message % self phase = self.next_phase() suffix = self.suffix % self - line = ''.join([ - message, - " " if message else "", - phase, - " " if suffix else "", - suffix, - ]) + line = "".join( + [ + message, + " " if message else "", + phase, + " " if suffix else "", + suffix, + ] + ) self.writeln(line) @@ -257,7 +250,7 @@ def update(self): "on": (DefaultDownloadProgressBar, DownloadProgressSpinner), "ascii": (DownloadBar, DownloadProgressSpinner), "pretty": (DownloadFillingCirclesBar, DownloadProgressSpinner), - "emoji": (DownloadBlueEmojiProgressBar, DownloadProgressSpinner) + "emoji": (DownloadBlueEmojiProgressBar, DownloadProgressSpinner), } diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 3cb8ab0019e..4302b5bdc8d 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -47,6 +47,7 @@ class SessionCommandMixin(CommandContextMixIn): """ A class mixin for command classes needing _build_session(). """ + def __init__(self): # type: () -> None super().__init__() @@ -83,8 +84,7 @@ def _build_session(self, options, retries=None, timeout=None): assert not options.cache_dir or os.path.isabs(options.cache_dir) session = PipSession( cache=( - os.path.join(options.cache_dir, "http") - if options.cache_dir else None + os.path.join(options.cache_dir, "http") if options.cache_dir else None ), retries=retries if retries is not None else options.retries, trusted_hosts=options.trusted_hosts, @@ -101,9 +101,7 @@ def _build_session(self, options, retries=None, timeout=None): # Handle timeouts if options.timeout or timeout: - session.timeout = ( - timeout if timeout is not None else options.timeout - ) + session.timeout = timeout if timeout is not None else options.timeout # Handle configured proxies if options.proxy: @@ -134,16 +132,14 @@ def handle_pip_version_check(self, options): This overrides the default behavior of not doing the check. """ # Make sure the index_group options are present. - assert hasattr(options, 'no_index') + assert hasattr(options, "no_index") if options.disable_pip_version_check or options.no_index: return # Otherwise, check if we're using the latest version of pip available. session = self._build_session( - options, - retries=0, - timeout=min(5, options.timeout) + options, retries=0, timeout=min(5, options.timeout) ) with session: pip_self_version_check(session, options) @@ -161,6 +157,7 @@ def with_cleanup(func): """Decorator for common logic related to managing temporary directories. """ + def configure_tempdir_registry(registry): # type: (TempDirectoryTypeRegistry) -> None for t in KEEPABLE_TEMPDIR_TYPES: @@ -185,7 +182,6 @@ def wrapper(self, options, args): class RequirementCommand(IndexGroupCommand): - def __init__(self, *args, **kw): # type: (Any, Any) -> None super().__init__(*args, **kw) @@ -204,13 +200,13 @@ def determine_resolver_variant(options): @classmethod def make_requirement_preparer( cls, - temp_build_dir, # type: TempDirectory - options, # type: Values - req_tracker, # type: RequirementTracker - session, # type: PipSession - finder, # type: PackageFinder - use_user_site, # type: bool - download_dir=None, # type: str + temp_build_dir, # type: TempDirectory + options, # type: Values + req_tracker, # type: RequirementTracker + session, # type: PipSession + finder, # type: PackageFinder + use_user_site, # type: bool + download_dir=None, # type: str ): # type: (...) -> RequirementPreparer """ @@ -221,20 +217,20 @@ def make_requirement_preparer( resolver_variant = cls.determine_resolver_variant(options) if resolver_variant == "2020-resolver": - lazy_wheel = 'fast-deps' in options.features_enabled + lazy_wheel = "fast-deps" in options.features_enabled if lazy_wheel: logger.warning( - 'pip is using lazily downloaded wheels using HTTP ' - 'range requests to obtain dependency information. ' - 'This experimental feature is enabled through ' - '--use-feature=fast-deps and it is not ready for ' - 'production.' + "pip is using lazily downloaded wheels using HTTP " + "range requests to obtain dependency information. " + "This experimental feature is enabled through " + "--use-feature=fast-deps and it is not ready for " + "production." ) else: lazy_wheel = False - if 'fast-deps' in options.features_enabled: + if "fast-deps" in options.features_enabled: logger.warning( - 'fast-deps has no effect when used with the legacy resolver.' + "fast-deps has no effect when used with the legacy resolver." ) return RequirementPreparer( @@ -254,17 +250,17 @@ def make_requirement_preparer( @classmethod def make_resolver( cls, - preparer, # type: RequirementPreparer - finder, # type: PackageFinder - options, # type: Values - wheel_cache=None, # type: Optional[WheelCache] - use_user_site=False, # type: bool - ignore_installed=True, # type: bool - ignore_requires_python=False, # type: bool - force_reinstall=False, # type: bool + preparer, # type: RequirementPreparer + finder, # type: PackageFinder + options, # type: Values + wheel_cache=None, # type: Optional[WheelCache] + use_user_site=False, # type: bool + ignore_installed=True, # type: bool + ignore_requires_python=False, # type: bool + force_reinstall=False, # type: bool upgrade_strategy="to-satisfy-only", # type: str - use_pep517=None, # type: Optional[bool] - py_version_info=None, # type: Optional[Tuple[int, ...]] + use_pep517=None, # type: Optional[bool] + py_version_info=None, # type: Optional[Tuple[int, ...]] ): # type: (...) -> BaseResolver """ @@ -296,6 +292,7 @@ def make_resolver( py_version_info=py_version_info, ) import pip._internal.resolution.legacy.resolver + return pip._internal.resolution.legacy.resolver.Resolver( preparer=preparer, finder=finder, @@ -312,10 +309,10 @@ def make_resolver( def get_requirements( self, - args, # type: List[str] - options, # type: Values - finder, # type: PackageFinder - session, # type: PipSession + args, # type: List[str] + options, # type: Values + finder, # type: PackageFinder + session, # type: PipSession ): # type: (...) -> List[InstallRequirement] """ @@ -324,9 +321,12 @@ def get_requirements( requirements = [] # type: List[InstallRequirement] for filename in options.constraints: for parsed_req in parse_requirements( - filename, - constraint=True, finder=finder, options=options, - session=session): + filename, + constraint=True, + finder=finder, + options=options, + session=session, + ): req_to_add = install_req_from_parsed_requirement( parsed_req, isolated=options.isolated_mode, @@ -336,7 +336,9 @@ def get_requirements( for req in args: req_to_add = install_req_from_line( - req, None, isolated=options.isolated_mode, + req, + None, + isolated=options.isolated_mode, use_pep517=options.use_pep517, user_supplied=True, ) @@ -354,8 +356,8 @@ def get_requirements( # NOTE: options.require_hashes may be set if --require-hashes is True for filename in options.requirements: for parsed_req in parse_requirements( - filename, - finder=finder, options=options, session=session): + filename, finder=finder, options=options, session=session + ): req_to_add = install_req_from_parsed_requirement( parsed_req, isolated=options.isolated_mode, @@ -369,16 +371,19 @@ def get_requirements( options.require_hashes = True if not (args or options.editables or options.requirements): - opts = {'name': self.name} + opts = {"name": self.name} if options.find_links: raise CommandError( - 'You must give at least one requirement to {name} ' + "You must give at least one requirement to {name} " '(maybe you meant "pip {name} {links}"?)'.format( - **dict(opts, links=' '.join(options.find_links)))) + **dict(opts, links=" ".join(options.find_links)) + ) + ) else: raise CommandError( - 'You must give at least one requirement to {name} ' - '(see "pip help {name}")'.format(**opts)) + "You must give at least one requirement to {name} " + '(see "pip help {name}")'.format(**opts) + ) return requirements @@ -396,9 +401,9 @@ def trace_basic_info(finder): def _build_package_finder( self, - options, # type: Values - session, # type: PipSession - target_python=None, # type: Optional[TargetPython] + options, # type: Values + session, # type: PipSession + target_python=None, # type: Optional[TargetPython] ignore_requires_python=None, # type: Optional[bool] ): # type: (...) -> PackageFinder diff --git a/src/pip/_internal/cli/spinners.py b/src/pip/_internal/cli/spinners.py index f0f895a2c2d..08e156617c4 100644 --- a/src/pip/_internal/cli/spinners.py +++ b/src/pip/_internal/cli/spinners.py @@ -24,9 +24,14 @@ def finish(self, final_status): class InteractiveSpinner(SpinnerInterface): - def __init__(self, message, file=None, spin_chars="-\\|/", - # Empirically, 8 updates/second looks nice - min_update_interval_seconds=0.125): + def __init__( + self, + message, + file=None, + spin_chars="-\\|/", + # Empirically, 8 updates/second looks nice + min_update_interval_seconds=0.125, + ): # type: (str, IO[str], str, float) -> None self._message = message if file is None: From cd1fb590e7add49756be087354ca1688bc3f2bbf Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Sun, 21 Feb 2021 08:52:50 -0800 Subject: [PATCH 2996/3170] Blacken the src/pip/_internal/distributions directory --- .pre-commit-config.yaml | 1 - ...d4-21e2-460f-9d80-455ff318c713.trivial.rst | 0 src/pip/_internal/distributions/__init__.py | 3 +-- src/pip/_internal/distributions/base.py | 1 + src/pip/_internal/distributions/sdist.py | 21 +++++++------------ 5 files changed, 10 insertions(+), 16 deletions(-) create mode 100644 news/dfaa54d4-21e2-460f-9d80-455ff318c713.trivial.rst diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 8299ef71e20..7af426649a3 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -25,7 +25,6 @@ repos: ^docs/| ^src/pip/_internal/cli| ^src/pip/_internal/commands| - ^src/pip/_internal/distributions| ^src/pip/_internal/index| ^src/pip/_internal/models| ^src/pip/_internal/network| diff --git a/news/dfaa54d4-21e2-460f-9d80-455ff318c713.trivial.rst b/news/dfaa54d4-21e2-460f-9d80-455ff318c713.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/distributions/__init__.py b/src/pip/_internal/distributions/__init__.py index 75bea848d2b..a222f248f34 100644 --- a/src/pip/_internal/distributions/__init__.py +++ b/src/pip/_internal/distributions/__init__.py @@ -6,8 +6,7 @@ def make_distribution_for_install_requirement(install_req): # type: (InstallRequirement) -> AbstractDistribution - """Returns a Distribution for the given InstallRequirement - """ + """Returns a Distribution for the given InstallRequirement""" # Editable requirements will always be source distributions. They use the # legacy logic until we create a modern standard for them. if install_req.editable: diff --git a/src/pip/_internal/distributions/base.py b/src/pip/_internal/distributions/base.py index 1798286edb0..78ee91e76f1 100644 --- a/src/pip/_internal/distributions/base.py +++ b/src/pip/_internal/distributions/base.py @@ -22,6 +22,7 @@ class AbstractDistribution(metaclass=abc.ABCMeta): - we must be able to create a Distribution object exposing the above metadata. """ + def __init__(self, req): # type: (InstallRequirement) -> None super().__init__() diff --git a/src/pip/_internal/distributions/sdist.py b/src/pip/_internal/distributions/sdist.py index 28249076ceb..c873a9f10e1 100644 --- a/src/pip/_internal/distributions/sdist.py +++ b/src/pip/_internal/distributions/sdist.py @@ -46,10 +46,10 @@ def _raise_conflicts(conflicting_with, conflicting_reqs): error_message = format_string.format( requirement=self.req, conflicting_with=conflicting_with, - description=', '.join( - f'{installed} is incompatible with {wanted}' + description=", ".join( + f"{installed} is incompatible with {wanted}" for installed, wanted in sorted(conflicting) - ) + ), ) raise InstallationError(error_message) @@ -60,15 +60,13 @@ def _raise_conflicts(conflicting_with, conflicting_reqs): self.req.build_env = BuildEnvironment() self.req.build_env.install_requirements( - finder, pyproject_requires, 'overlay', - "Installing build dependencies" + finder, pyproject_requires, "overlay", "Installing build dependencies" ) conflicting, missing = self.req.build_env.check_requirements( self.req.requirements_to_check ) if conflicting: - _raise_conflicts("PEP 517/518 supported requirements", - conflicting) + _raise_conflicts("PEP 517/518 supported requirements", conflicting) if missing: logger.warning( "Missing build requirements in pyproject.toml for %s.", @@ -77,15 +75,13 @@ def _raise_conflicts(conflicting_with, conflicting_reqs): logger.warning( "The project does not specify a build backend, and " "pip cannot fall back to setuptools without %s.", - " and ".join(map(repr, sorted(missing))) + " and ".join(map(repr, sorted(missing))), ) # Install any extra build dependencies that the backend requests. # This must be done in a second pass, as the pyproject.toml # dependencies must be installed before we can call the backend. with self.req.build_env: - runner = runner_with_spinner_message( - "Getting requirements to build wheel" - ) + runner = runner_with_spinner_message("Getting requirements to build wheel") backend = self.req.pep517_backend assert backend is not None with backend.subprocess_runner(runner): @@ -95,6 +91,5 @@ def _raise_conflicts(conflicting_with, conflicting_reqs): if conflicting: _raise_conflicts("the backend dependencies", conflicting) self.req.build_env.install_requirements( - finder, missing, 'normal', - "Installing backend dependencies" + finder, missing, "normal", "Installing backend dependencies" ) From 40e395d6be84341b54473a6e61eda147d1e54733 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 3 Feb 2021 21:36:29 +0800 Subject: [PATCH 2997/3170] Avoid pkg_resources API in versioncontrol --- src/pip/_internal/vcs/versioncontrol.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index 18e93ee5e1c..7600f6fef5e 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -18,8 +18,6 @@ Union, ) -from pip._vendor import pkg_resources - from pip._internal.cli.spinners import SpinnerInterface from pip._internal.exceptions import BadCommand, InstallationError from pip._internal.utils.misc import ( @@ -62,7 +60,7 @@ def make_vcs_requirement_url(repo_url, rev, project_name, subdir=None): repo_url: the remote VCS url, with any needed VCS prefix (e.g. "git+"). project_name: the (unescaped) project name. """ - egg_project_name = pkg_resources.to_filename(project_name) + egg_project_name = project_name.replace("-", "_") req = f'{repo_url}@{rev}#egg={egg_project_name}' if subdir: req += f'&subdirectory={subdir}' From 0b9008a8a48611ec8e9411f8e8da3010c8657800 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sat, 30 Jan 2021 09:42:44 +0800 Subject: [PATCH 2998/3170] Make vendoring task respond well to empty lines --- noxfile.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/noxfile.py b/noxfile.py index a40dba5b647..165a430d14b 100644 --- a/noxfile.py +++ b/noxfile.py @@ -174,11 +174,14 @@ def vendoring(session): def pinned_requirements(path): # type: (Path) -> Iterator[Tuple[str, str]] - for line in path.read_text().splitlines(): - one, two = line.split("==", 1) + for line in path.read_text().splitlines(keepends=False): + one, sep, two = line.partition("==") + if not sep: + continue name = one.strip() - version = two.split("#")[0].strip() - yield name, version + version = two.split("#", 1)[0].strip() + if name and version: + yield name, version vendor_txt = Path("src/pip/_vendor/vendor.txt") for name, old_version in pinned_requirements(vendor_txt): From 738e6005067618c339e637ad78a839c97ad771ca Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 18 Feb 2021 19:27:30 +0800 Subject: [PATCH 2999/3170] Move distuitls location logic into subpackage --- src/pip/_internal/commands/install.py | 22 ++++--- src/pip/_internal/locations/__init__.py | 53 +++++++++++++++++ .../{locations.py => locations/_distutils.py} | 59 ++++--------------- src/pip/_internal/locations/base.py | 51 ++++++++++++++++ src/pip/_internal/locations/sysconfig.py | 0 src/pip/_internal/req/req_uninstall.py | 10 ++-- 6 files changed, 136 insertions(+), 59 deletions(-) create mode 100644 src/pip/_internal/locations/__init__.py rename src/pip/_internal/{locations.py => locations/_distutils.py} (77%) create mode 100644 src/pip/_internal/locations/base.py create mode 100644 src/pip/_internal/locations/sysconfig.py diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index 78cd0b5cf68..dc743ee0b50 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -15,7 +15,7 @@ from pip._internal.cli.req_command import RequirementCommand, with_cleanup from pip._internal.cli.status_codes import ERROR, SUCCESS from pip._internal.exceptions import CommandError, InstallationError -from pip._internal.locations import distutils_scheme +from pip._internal.locations import get_scheme from pip._internal.metadata import get_environment from pip._internal.models.format_control import FormatControl from pip._internal.operations.check import ConflictDetails, check_install_conflicts @@ -455,10 +455,10 @@ def _handle_target_dir(self, target_dir, target_temp_dir, upgrade): # 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'] + scheme = get_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) @@ -574,9 +574,15 @@ def get_lib_location_guesses( prefix=None # type: Optional[str] ): # type:(...) -> List[str] - scheme = distutils_scheme('', user=user, home=home, root=root, - isolated=isolated, prefix=prefix) - return [scheme['purelib'], scheme['platlib']] + scheme = get_scheme( + '', + user=user, + home=home, + root=root, + isolated=isolated, + prefix=prefix, + ) + return [scheme.purelib, scheme.platlib] def site_packages_writable(root, isolated): diff --git a/src/pip/_internal/locations/__init__.py b/src/pip/_internal/locations/__init__.py new file mode 100644 index 00000000000..9285165d7ee --- /dev/null +++ b/src/pip/_internal/locations/__init__.py @@ -0,0 +1,53 @@ +from typing import Optional + +from pip._internal.models.scheme import Scheme + +from . import distutils as _distutils +from .base import ( + USER_CACHE_DIR, + get_major_minor_version, + get_src_prefix, + site_packages, + user_site, +) + +__all__ = [ + "USER_CACHE_DIR", + "get_bin_prefix", + "get_bin_user", + "get_major_minor_version", + "get_scheme", + "get_src_prefix", + "init_backend", + "site_packages", + "user_site", +] + + +def get_scheme( + dist_name, # type: str + user=False, # type: bool + home=None, # type: Optional[str] + root=None, # type: Optional[str] + isolated=False, # type: bool + prefix=None, # type: Optional[str] +): + # type: (...) -> Scheme + return _distutils.get_scheme( + dist_name, + user=user, + home=home, + root=root, + isolated=isolated, + prefix=prefix, + ) + + +def get_bin_prefix(): + # type: () -> str + return _distutils.get_bin_prefix() + + +def get_bin_user(): + # type: () -> str + return _distutils.get_bin_user() diff --git a/src/pip/_internal/locations.py b/src/pip/_internal/locations/_distutils.py similarity index 77% rename from src/pip/_internal/locations.py rename to src/pip/_internal/locations/_distutils.py index 19c039eabf8..efc12e8da29 100644 --- a/src/pip/_internal/locations.py +++ b/src/pip/_internal/locations/_distutils.py @@ -5,62 +5,19 @@ import os import os.path -import site import sys -import sysconfig from distutils.cmd import Command as DistutilsCommand from distutils.command.install import SCHEME_KEYS from distutils.command.install import install as distutils_install_command from typing import Dict, List, Optional, Union, cast from pip._internal.models.scheme import Scheme -from pip._internal.utils import appdirs from pip._internal.utils.compat import WINDOWS from pip._internal.utils.virtualenv import running_under_virtualenv -# Application Directories -USER_CACHE_DIR = appdirs.user_cache_dir("pip") +from .base import get_major_minor_version, user_site -def get_major_minor_version(): - # type: () -> str - """ - Return the major-minor version of the current Python as a string, e.g. - "3.7" or "3.10". - """ - return '{}.{}'.format(*sys.version_info) - - -def get_src_prefix(): - # type: () -> str - if running_under_virtualenv(): - src_prefix = os.path.join(sys.prefix, 'src') - else: - # FIXME: keep src in cwd for now (it is not a temporary folder) - try: - src_prefix = os.path.join(os.getcwd(), 'src') - except OSError: - # In case the current working directory has been renamed or deleted - sys.exit( - "The folder you are executing pip from can no longer be found." - ) - - # under macOS + virtualenv sys.prefix is not properly resolved - # it is something like /path/to/python/bin/.. - return os.path.abspath(src_prefix) - - -# FIXME doesn't account for venv linked to global site-packages - -site_packages = sysconfig.get_path("purelib") # type: Optional[str] - -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 - if WINDOWS: bin_py = os.path.join(sys.prefix, 'Scripts') bin_user = os.path.join(user_site, 'Scripts') @@ -78,7 +35,7 @@ def get_src_prefix(): bin_py = '/usr/local/bin' -def distutils_scheme( +def _distutils_scheme( dist_name, user=False, home=None, root=None, isolated=False, prefix=None ): # type:(str, bool, str, str, bool, str) -> Dict[str, str] @@ -168,7 +125,7 @@ def get_scheme( :param prefix: indicates to use the "prefix" scheme and provides the base directory for the same """ - scheme = distutils_scheme( + scheme = _distutils_scheme( dist_name, user, home, root, isolated, prefix ) return Scheme( @@ -178,3 +135,13 @@ def get_scheme( scripts=scheme["scripts"], data=scheme["data"], ) + + +def get_bin_prefix(): + # type: () -> str + return bin_py + + +def get_bin_user(): + # type: () -> str + return bin_user diff --git a/src/pip/_internal/locations/base.py b/src/pip/_internal/locations/base.py new file mode 100644 index 00000000000..8c9f7751179 --- /dev/null +++ b/src/pip/_internal/locations/base.py @@ -0,0 +1,51 @@ +import os +import site +import sys +import sysconfig +import typing + +from pip._internal.utils import appdirs +from pip._internal.utils.virtualenv import running_under_virtualenv + + +# Application Directories +USER_CACHE_DIR = appdirs.user_cache_dir("pip") + +# FIXME doesn't account for venv linked to global site-packages +site_packages = sysconfig.get_path("purelib") # type: typing.Optional[str] + + +def get_major_minor_version(): + # type: () -> str + """ + Return the major-minor version of the current Python as a string, e.g. + "3.7" or "3.10". + """ + return '{}.{}'.format(*sys.version_info) + + +def get_src_prefix(): + # type: () -> str + if running_under_virtualenv(): + src_prefix = os.path.join(sys.prefix, 'src') + else: + # FIXME: keep src in cwd for now (it is not a temporary folder) + try: + src_prefix = os.path.join(os.getcwd(), 'src') + except OSError: + # In case the current working directory has been renamed or deleted + sys.exit( + "The folder you are executing pip from can no longer be found." + ) + + # under macOS + virtualenv sys.prefix is not properly resolved + # it is something like /path/to/python/bin/.. + return os.path.abspath(src_prefix) + + +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 diff --git a/src/pip/_internal/locations/sysconfig.py b/src/pip/_internal/locations/sysconfig.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/req/req_uninstall.py b/src/pip/_internal/req/req_uninstall.py index 519b79166a4..776652de1cc 100644 --- a/src/pip/_internal/req/req_uninstall.py +++ b/src/pip/_internal/req/req_uninstall.py @@ -11,7 +11,7 @@ from pip._vendor.pkg_resources import Distribution from pip._internal.exceptions import UninstallationError -from pip._internal.locations import bin_py, bin_user +from pip._internal.locations import get_bin_prefix, get_bin_user from pip._internal.utils.compat import WINDOWS from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import ( @@ -36,9 +36,9 @@ def _script_names(dist, script_name, is_gui): Returns the list of file names """ if dist_in_usersite(dist): - bin_dir = bin_user + bin_dir = get_bin_user() else: - bin_dir = bin_py + bin_dir = get_bin_prefix() exe_name = os.path.join(bin_dir, script_name) paths_to_remove = [exe_name] if WINDOWS: @@ -551,9 +551,9 @@ def from_dist(cls, dist): 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 + bin_dir = get_bin_user() else: - bin_dir = bin_py + bin_dir = get_bin_prefix() paths_to_remove.add(os.path.join(bin_dir, script)) if WINDOWS: paths_to_remove.add(os.path.join(bin_dir, script) + '.bat') From 7662c5961e5d2fe8a9cbceb48206b3f7daaec169 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 18 Feb 2021 20:56:45 +0800 Subject: [PATCH 3000/3170] Implement sysconfig locations and warn on mismatch --- src/pip/_internal/exceptions.py | 15 +++ src/pip/_internal/locations/__init__.py | 61 +++++++++- src/pip/_internal/locations/_sysconfig.py | 137 ++++++++++++++++++++++ src/pip/_internal/locations/sysconfig.py | 0 tests/unit/test_locations.py | 19 +-- 5 files changed, 220 insertions(+), 12 deletions(-) create mode 100644 src/pip/_internal/locations/_sysconfig.py delete mode 100644 src/pip/_internal/locations/sysconfig.py diff --git a/src/pip/_internal/exceptions.py b/src/pip/_internal/exceptions.py index 01ee4b76984..8aacf812014 100644 --- a/src/pip/_internal/exceptions.py +++ b/src/pip/_internal/exceptions.py @@ -59,6 +59,21 @@ def __str__(self): ) +class UserInstallationInvalid(InstallationError): + """A --user install is requested on an environment without user site.""" + + def __str__(self): + # type: () -> str + return "User base directory is not specified" + + +class InvalidSchemeCombination(InstallationError): + def __str__(self): + # type: () -> str + before = ", ".join(str(a) for a in self.args[:-1]) + return f"Cannot set {before} and {self.args[-1]} together" + + class DistributionNotFound(InstallationError): """Raised when a distribution cannot be found to satisfy a requirement""" diff --git a/src/pip/_internal/locations/__init__.py b/src/pip/_internal/locations/__init__.py index 9285165d7ee..6fc4d53e15d 100644 --- a/src/pip/_internal/locations/__init__.py +++ b/src/pip/_internal/locations/__init__.py @@ -1,8 +1,11 @@ +import logging +import pathlib +import sysconfig from typing import Optional -from pip._internal.models.scheme import Scheme +from pip._internal.models.scheme import SCHEME_KEYS, Scheme -from . import distutils as _distutils +from . import _distutils, _sysconfig from .base import ( USER_CACHE_DIR, get_major_minor_version, @@ -24,6 +27,31 @@ ] +logger = logging.getLogger(__name__) + + +def _default_base(*, user): + # type: (bool) -> str + if user: + base = sysconfig.get_config_var("userbase") + else: + base = sysconfig.get_config_var("base") + assert base is not None + return base + + +def _warn_if_mismatch(old, new, *, key): + # type: (pathlib.Path, pathlib.Path, str) -> None + if old == new: + return + message = ( + "Value for %s does not match. Please report this: <URL HERE>" + "\ndistutils: %s" + "\nsysconfig: %s" + ) + logger.warning(message, key, old, new) + + def get_scheme( dist_name, # type: str user=False, # type: bool @@ -33,7 +61,15 @@ def get_scheme( prefix=None, # type: Optional[str] ): # type: (...) -> Scheme - return _distutils.get_scheme( + old = _distutils.get_scheme( + dist_name, + user=user, + home=home, + root=root, + isolated=isolated, + prefix=prefix, + ) + new = _sysconfig.get_scheme( dist_name, user=user, home=home, @@ -42,12 +78,27 @@ def get_scheme( prefix=prefix, ) + base = prefix or home or _default_base(user=user) + for k in SCHEME_KEYS: + # Extra join because distutils can return relative paths. + old_v = pathlib.Path(base, getattr(old, k)) + new_v = pathlib.Path(getattr(new, k)) + _warn_if_mismatch(old_v, new_v, key=f"scheme.{k}") + + return old + def get_bin_prefix(): # type: () -> str - return _distutils.get_bin_prefix() + old = _distutils.get_bin_prefix() + new = _sysconfig.get_bin_prefix() + _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="bin_prefix") + return old def get_bin_user(): # type: () -> str - return _distutils.get_bin_user() + old = _distutils.get_bin_user() + new = _sysconfig.get_bin_user() + _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="bin_user") + return old diff --git a/src/pip/_internal/locations/_sysconfig.py b/src/pip/_internal/locations/_sysconfig.py new file mode 100644 index 00000000000..b9687870466 --- /dev/null +++ b/src/pip/_internal/locations/_sysconfig.py @@ -0,0 +1,137 @@ +import distutils.util # FIXME: For change_root. +import logging +import os +import sys +import sysconfig +import typing + +from pip._internal.exceptions import ( + InvalidSchemeCombination, + UserInstallationInvalid, +) +from pip._internal.models.scheme import SCHEME_KEYS, Scheme +from pip._internal.utils.virtualenv import running_under_virtualenv + +from .base import get_major_minor_version + + +logger = logging.getLogger(__name__) + + +_AVAILABLE_SCHEMES = set(sysconfig.get_scheme_names()) + + +def _infer_scheme(variant): + # (typing.Literal["home", "prefix", "user"]) -> str + """Try to find a scheme for the current platform. + + Unfortunately ``_get_default_scheme()`` is private, so there's no way to + ask things like "what is the '_home' scheme on this platform". This tries + to answer that with some heuristics while accounting for ad-hoc platforms + not covered by CPython's default sysconfig implementation. + """ + # Most schemes are named like this e.g. "posix_home", "nt_user". + suffixed = f"{os.name}_{variant}" + if suffixed in _AVAILABLE_SCHEMES: + return suffixed + + # The user scheme is not available. + if variant == "user" and sysconfig.get_config_var("userbase") is None: + raise UserInstallationInvalid() + + # On Windows, prefx and home schemes are the same and just called "nt". + if os.name in _AVAILABLE_SCHEMES: + return os.name + + # Not sure what's happening, some obscure platform that does not fully + # implement sysconfig? Just use the POSIX scheme. + logger.warning("No %r scheme for %r; fallback to POSIX.", variant, os.name) + return f"posix_{variant}" + + +# Update these keys if the user sets a custom home. +_HOME_KEYS = ( + "installed_base", + "base", + "installed_platbase", + "platbase", + "prefix", + "exec_prefix", +) +if sysconfig.get_config_var("userbase") is not None: + _HOME_KEYS += ("userbase",) + + +def get_scheme( + dist_name, # type: str + user=False, # type: bool + home=None, # type: typing.Optional[str] + root=None, # type: typing.Optional[str] + isolated=False, # type: bool + prefix=None, # type: typing.Optional[str] +): + # type: (...) -> Scheme + """ + Get the "scheme" corresponding to the input parameters. + + :param dist_name: the name of the package to retrieve the scheme for, used + in the headers scheme path + :param user: indicates to use the "user" scheme + :param home: indicates to use the "home" scheme + :param root: root under which other directories are re-based + :param isolated: ignored, but kept for distutils compatibility (where + this controls whether the user-site pydistutils.cfg is honored) + :param prefix: indicates to use the "prefix" scheme and provides the + base directory for the same + """ + if user and prefix: + raise InvalidSchemeCombination("--user", "--prefix") + if home and prefix: + raise InvalidSchemeCombination("--home", "--prefix") + + if home is not None: + scheme = _infer_scheme("home") + elif user: + scheme = _infer_scheme("user") + else: + scheme = _infer_scheme("prefix") + + if home is not None: + variables = {k: home for k in _HOME_KEYS} + elif prefix is not None: + variables = {k: prefix for k in _HOME_KEYS} + else: + variables = {} + + paths = sysconfig.get_paths(scheme=scheme, vars=variables) + + # Special header path for compatibility to distutils. + if running_under_virtualenv(): + base = variables.get("base", sys.prefix) + python_xy = f"python{get_major_minor_version()}" + paths["include"] = os.path.join(base, "include", "site", python_xy) + + scheme = Scheme( + platlib=paths["platlib"], + purelib=paths["purelib"], + headers=os.path.join(paths["include"], dist_name), + scripts=paths["scripts"], + data=paths["data"], + ) + if root is not None: + for key in SCHEME_KEYS: + value = distutils.util.change_root(root, getattr(scheme, key)) + setattr(scheme, key, value) + return scheme + + +def get_bin_prefix(): + # type: () -> str + # Forcing to use /usr/local/bin for standard macOS framework installs. + if sys.platform[:6] == "darwin" and sys.prefix[:16] == "/System/Library/": + return "/usr/local/bin" + return sysconfig.get_path("scripts", scheme=_infer_scheme("prefix")) + + +def get_bin_user(): + return sysconfig.get_path("scripts", scheme=_infer_scheme("user")) diff --git a/src/pip/_internal/locations/sysconfig.py b/src/pip/_internal/locations/sysconfig.py deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/tests/unit/test_locations.py b/tests/unit/test_locations.py index 3d4ec946245..067f4e84486 100644 --- a/tests/unit/test_locations.py +++ b/tests/unit/test_locations.py @@ -11,7 +11,7 @@ import pytest -from pip._internal.locations import distutils_scheme +from pip._internal.locations import SCHEME_KEYS, get_scheme if sys.platform == 'win32': pwd = Mock() @@ -19,6 +19,11 @@ import pwd +def _get_scheme_dict(*args, **kwargs): + scheme = get_scheme(*args, **kwargs) + return {k: getattr(scheme, k) for k in SCHEME_KEYS} + + class TestLocations: def setup(self): self.tempdir = tempfile.mkdtemp() @@ -83,8 +88,8 @@ def test_root_modifies_appropriately(self, monkeypatch): # root is c:\somewhere\else or /somewhere/else root = os.path.normcase(os.path.abspath( os.path.join(os.path.sep, 'somewhere', 'else'))) - norm_scheme = distutils_scheme("example") - root_scheme = distutils_scheme("example", root=root) + norm_scheme = _get_scheme_dict("example") + root_scheme = _get_scheme_dict("example", root=root) for key, value in norm_scheme.items(): drive, path = os.path.splitdrive(os.path.abspath(value)) @@ -107,7 +112,7 @@ def test_distutils_config_file_read(self, tmpdir, monkeypatch): 'find_config_files', lambda self: [f], ) - scheme = distutils_scheme('example') + scheme = _get_scheme_dict('example') assert scheme['scripts'] == install_scripts @pytest.mark.incompatible_with_venv @@ -129,15 +134,15 @@ def test_install_lib_takes_precedence(self, tmpdir, monkeypatch): 'find_config_files', lambda self: [f], ) - scheme = distutils_scheme('example') + scheme = _get_scheme_dict('example') assert scheme['platlib'] == install_lib + os.path.sep assert scheme['purelib'] == install_lib + os.path.sep def test_prefix_modifies_appropriately(self): prefix = os.path.abspath(os.path.join('somewhere', 'else')) - normal_scheme = distutils_scheme("example") - prefix_scheme = distutils_scheme("example", prefix=prefix) + normal_scheme = _get_scheme_dict("example") + prefix_scheme = _get_scheme_dict("example", prefix=prefix) def _calculate_expected(value): path = os.path.join(prefix, os.path.relpath(value, sys.prefix)) From edbda257c6b19cbabd5a3f3328427a68f10fc780 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 19 Feb 2021 17:14:11 +0800 Subject: [PATCH 3001/3170] Abstract out get_python_lib() usages --- src/pip/_internal/build_env.py | 16 ++--------- src/pip/_internal/locations/__init__.py | 35 ++++++++++++++++++++++- src/pip/_internal/locations/_distutils.py | 21 +++++++++++++- src/pip/_internal/locations/_sysconfig.py | 16 +++++++++++ 4 files changed, 73 insertions(+), 15 deletions(-) diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py index b1c877cfd9b..f4a1c93353a 100644 --- a/src/pip/_internal/build_env.py +++ b/src/pip/_internal/build_env.py @@ -6,7 +6,6 @@ import sys import textwrap from collections import OrderedDict -from distutils.sysconfig import get_python_lib from sysconfig import get_paths from types import TracebackType from typing import TYPE_CHECKING, Iterable, List, Optional, Set, Tuple, Type @@ -15,6 +14,7 @@ from pip import __file__ as pip_location from pip._internal.cli.spinners import open_spinner +from pip._internal.locations import get_platlib, get_prefixed_libs, get_purelib from pip._internal.utils.subprocess import call_subprocess from pip._internal.utils.temp_dir import TempDirectory, tempdir_kinds @@ -34,14 +34,7 @@ def __init__(self, path): 'nt' if os.name == 'nt' else 'posix_prefix', vars={'base': path, 'platbase': path} )['scripts'] - # Note: prefer distutils' sysconfig to get the - # library paths so PyPy is correctly supported. - purelib = get_python_lib(plat_specific=False, prefix=path) - platlib = get_python_lib(plat_specific=True, prefix=path) - if purelib == platlib: - self.lib_dirs = [purelib] - else: - self.lib_dirs = [purelib, platlib] + self.lib_dirs = get_prefixed_libs(path) class BuildEnvironment: @@ -69,10 +62,7 @@ def __init__(self): # - ensure .pth files are honored # - prevent access to system site packages system_sites = { - os.path.normcase(site) for site in ( - get_python_lib(plat_specific=False), - get_python_lib(plat_specific=True), - ) + os.path.normcase(site) for site in (get_purelib(), get_platlib()) } self._site_dir = os.path.join(temp_dir.path, 'site') if not os.path.exists(self._site_dir): diff --git a/src/pip/_internal/locations/__init__.py b/src/pip/_internal/locations/__init__.py index 6fc4d53e15d..f57bb9a8b07 100644 --- a/src/pip/_internal/locations/__init__.py +++ b/src/pip/_internal/locations/__init__.py @@ -1,7 +1,7 @@ import logging import pathlib import sysconfig -from typing import Optional +from typing import List, Optional from pip._internal.models.scheme import SCHEME_KEYS, Scheme @@ -19,6 +19,9 @@ "get_bin_prefix", "get_bin_user", "get_major_minor_version", + "get_platlib", + "get_prefixed_libs", + "get_purelib", "get_scheme", "get_src_prefix", "init_backend", @@ -102,3 +105,33 @@ def get_bin_user(): new = _sysconfig.get_bin_user() _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="bin_user") return old + + +def get_purelib(): + # type: () -> str + """Return the default pure-Python lib location.""" + old = _distutils.get_purelib() + new = _sysconfig.get_purelib() + _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="purelib") + return old + + +def get_platlib(): + # type: () -> str + """Return the default platform-shared lib location.""" + old = _distutils.get_platlib() + new = _sysconfig.get_platlib() + _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="platlib") + return old + + +def get_prefixed_libs(prefix): + # type: (str) -> List[str] + """Return the lib locations under ``prefix``.""" + old_pure, old_plat = _distutils.get_prefixed_libs(prefix) + new_pure, new_plat = _sysconfig.get_prefixed_libs(prefix) + _warn_if_mismatch(old_pure, new_pure, key="prefixed-purelib") + _warn_if_mismatch(old_plat, new_plat, key="prefixed-platlib") + if old_pure == old_plat: + return [old_pure] + return [old_pure, old_plat] diff --git a/src/pip/_internal/locations/_distutils.py b/src/pip/_internal/locations/_distutils.py index efc12e8da29..bf3ff728f6d 100644 --- a/src/pip/_internal/locations/_distutils.py +++ b/src/pip/_internal/locations/_distutils.py @@ -9,7 +9,8 @@ from distutils.cmd import Command as DistutilsCommand from distutils.command.install import SCHEME_KEYS from distutils.command.install import install as distutils_install_command -from typing import Dict, List, Optional, Union, cast +from distutils.sysconfig import get_python_lib +from typing import Dict, List, Optional, Tuple, Union, cast from pip._internal.models.scheme import Scheme from pip._internal.utils.compat import WINDOWS @@ -145,3 +146,21 @@ def get_bin_prefix(): def get_bin_user(): # type: () -> str return bin_user + + +def get_purelib(): + # type: () -> str + return get_python_lib(plat_specific=False) + + +def get_platlib(): + # type: () -> str + return get_python_lib(plat_specific=True) + + +def get_prefixed_libs(prefix): + # type: (str) -> Tuple[str, str] + return ( + get_python_lib(plat_specific=False, prefix=prefix), + get_python_lib(plat_specific=True, prefix=prefix), + ) diff --git a/src/pip/_internal/locations/_sysconfig.py b/src/pip/_internal/locations/_sysconfig.py index b9687870466..bc49cb75f77 100644 --- a/src/pip/_internal/locations/_sysconfig.py +++ b/src/pip/_internal/locations/_sysconfig.py @@ -135,3 +135,19 @@ def get_bin_prefix(): def get_bin_user(): return sysconfig.get_path("scripts", scheme=_infer_scheme("user")) + + +def get_purelib(): + # type: () -> str + return sysconfig.get_path("purelib") + + +def get_platlib(): + # type: () -> str + return sysconfig.get_path("platlib") + + +def get_prefixed_libs(prefix): + # type: (str) -> typing.Tuple[str, str] + paths = sysconfig.get_paths(vars={"base": prefix, "platbase": prefix}) + return (paths["purelib"], paths["platlib"]) From 686734fdd1e3c6e280b84157e9abc3da41357bfb Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 19 Feb 2021 17:53:27 +0800 Subject: [PATCH 3002/3170] Provide issue URL to report --- src/pip/_internal/locations/__init__.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/locations/__init__.py b/src/pip/_internal/locations/__init__.py index f57bb9a8b07..db1c1309ee4 100644 --- a/src/pip/_internal/locations/__init__.py +++ b/src/pip/_internal/locations/__init__.py @@ -47,12 +47,13 @@ def _warn_if_mismatch(old, new, *, key): # type: (pathlib.Path, pathlib.Path, str) -> None if old == new: return + issue_url = "https://github.com/pypa/pip/issues/9617" message = ( - "Value for %s does not match. Please report this: <URL HERE>" + "Value for %s does not match. Please report this to %s" "\ndistutils: %s" "\nsysconfig: %s" ) - logger.warning(message, key, old, new) + logger.warning(message, key, issue_url, old, new) def get_scheme( From b4545a01441ec3df4130664cbe2c15f3dfd783cc Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 19 Feb 2021 17:59:18 +0800 Subject: [PATCH 3003/3170] News --- news/9617.process.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 news/9617.process.rst diff --git a/news/9617.process.rst b/news/9617.process.rst new file mode 100644 index 00000000000..f505c460541 --- /dev/null +++ b/news/9617.process.rst @@ -0,0 +1,3 @@ +Start installation scheme migration from ``distutils`` to ``sysconfig``. A +warning is implemented to detect differences between the two implementations to +encourage user reports, so we can avoid breakages before they happen. From 08c282919f28c324de48118828e670f8fac88fd3 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 19 Feb 2021 18:11:20 +0800 Subject: [PATCH 3004/3170] Blackify --- src/pip/_internal/locations/_distutils.py | 39 +++++++++++------------ src/pip/_internal/locations/_sysconfig.py | 6 +--- src/pip/_internal/locations/base.py | 11 +++---- 3 files changed, 23 insertions(+), 33 deletions(-) diff --git a/src/pip/_internal/locations/_distutils.py b/src/pip/_internal/locations/_distutils.py index bf3ff728f6d..70b7e8d6dd3 100644 --- a/src/pip/_internal/locations/_distutils.py +++ b/src/pip/_internal/locations/_distutils.py @@ -20,20 +20,20 @@ if WINDOWS: - bin_py = os.path.join(sys.prefix, 'Scripts') - bin_user = os.path.join(user_site, 'Scripts') + bin_py = os.path.join(sys.prefix, "Scripts") + bin_user = os.path.join(user_site, "Scripts") # buildout uses 'bin' on Windows too? if not os.path.exists(bin_py): - bin_py = os.path.join(sys.prefix, 'bin') - bin_user = os.path.join(user_site, 'bin') + bin_py = os.path.join(sys.prefix, "bin") + bin_user = os.path.join(user_site, "bin") else: - bin_py = os.path.join(sys.prefix, 'bin') - bin_user = os.path.join(user_site, 'bin') + bin_py = os.path.join(sys.prefix, "bin") + bin_user = os.path.join(user_site, "bin") # 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/': - bin_py = '/usr/local/bin' + if sys.platform[:6] == "darwin" and sys.prefix[:16] == "/System/Library/": + bin_py = "/usr/local/bin" def _distutils_scheme( @@ -45,14 +45,14 @@ def _distutils_scheme( """ from distutils.dist import Distribution - dist_args = {'name': dist_name} # type: Dict[str, Union[str, List[str]]] + dist_args = {"name": dist_name} # type: Dict[str, Union[str, List[str]]] if isolated: dist_args["script_args"] = ["--no-user-cfg"] d = Distribution(dist_args) d.parse_config_files() obj = None # type: Optional[DistutilsCommand] - obj = d.get_command_obj('install', create=True) + obj = d.get_command_obj("install", create=True) assert obj is not None i = cast(distutils_install_command, obj) # NOTE: setting user or home has the side-effect of creating the home dir @@ -70,28 +70,27 @@ def _distutils_scheme( scheme = {} for key in SCHEME_KEYS: - scheme[key] = getattr(i, 'install_' + key) + scheme[key] = getattr(i, "install_" + key) # install_lib specified in setup.cfg should install *everything* # into there (i.e. it takes precedence over both purelib and # platlib). Note, i.install_lib is *always* set after # finalize_options(); we only want to override here if the user # has explicitly requested it hence going back to the config - if 'install_lib' in d.get_option_dict('install'): + if "install_lib" in d.get_option_dict("install"): scheme.update(dict(purelib=i.install_lib, platlib=i.install_lib)) if running_under_virtualenv(): - scheme['headers'] = os.path.join( + scheme["headers"] = os.path.join( i.prefix, - 'include', - 'site', - f'python{get_major_minor_version()}', + "include", + "site", + f"python{get_major_minor_version()}", dist_name, ) if root is not None: - path_no_drive = os.path.splitdrive( - os.path.abspath(scheme["headers"]))[1] + path_no_drive = os.path.splitdrive(os.path.abspath(scheme["headers"]))[1] scheme["headers"] = os.path.join( root, path_no_drive[1:], @@ -126,9 +125,7 @@ def get_scheme( :param prefix: indicates to use the "prefix" scheme and provides the base directory for the same """ - scheme = _distutils_scheme( - dist_name, user, home, root, isolated, prefix - ) + scheme = _distutils_scheme(dist_name, user, home, root, isolated, prefix) return Scheme( platlib=scheme["platlib"], purelib=scheme["purelib"], diff --git a/src/pip/_internal/locations/_sysconfig.py b/src/pip/_internal/locations/_sysconfig.py index bc49cb75f77..d29580dcc9f 100644 --- a/src/pip/_internal/locations/_sysconfig.py +++ b/src/pip/_internal/locations/_sysconfig.py @@ -5,16 +5,12 @@ import sysconfig import typing -from pip._internal.exceptions import ( - InvalidSchemeCombination, - UserInstallationInvalid, -) +from pip._internal.exceptions import InvalidSchemeCombination, UserInstallationInvalid from pip._internal.models.scheme import SCHEME_KEYS, Scheme from pip._internal.utils.virtualenv import running_under_virtualenv from .base import get_major_minor_version - logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/locations/base.py b/src/pip/_internal/locations/base.py index 8c9f7751179..7f8ef670aac 100644 --- a/src/pip/_internal/locations/base.py +++ b/src/pip/_internal/locations/base.py @@ -7,7 +7,6 @@ from pip._internal.utils import appdirs from pip._internal.utils.virtualenv import running_under_virtualenv - # Application Directories USER_CACHE_DIR = appdirs.user_cache_dir("pip") @@ -21,22 +20,20 @@ def get_major_minor_version(): Return the major-minor version of the current Python as a string, e.g. "3.7" or "3.10". """ - return '{}.{}'.format(*sys.version_info) + return "{}.{}".format(*sys.version_info) def get_src_prefix(): # type: () -> str if running_under_virtualenv(): - src_prefix = os.path.join(sys.prefix, 'src') + src_prefix = os.path.join(sys.prefix, "src") else: # FIXME: keep src in cwd for now (it is not a temporary folder) try: - src_prefix = os.path.join(os.getcwd(), 'src') + src_prefix = os.path.join(os.getcwd(), "src") except OSError: # In case the current working directory has been renamed or deleted - sys.exit( - "The folder you are executing pip from can no longer be found." - ) + sys.exit("The folder you are executing pip from can no longer be found.") # under macOS + virtualenv sys.prefix is not properly resolved # it is something like /path/to/python/bin/.. From 790bd02ede18995195ed08fc0ac2aa142c56537c Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 19 Feb 2021 18:15:45 +0800 Subject: [PATCH 3005/3170] Type fixes --- src/pip/_internal/locations/__init__.py | 12 +++++++++-- src/pip/_internal/locations/_sysconfig.py | 25 ++++++++++++----------- src/pip/_internal/locations/base.py | 2 +- 3 files changed, 24 insertions(+), 15 deletions(-) diff --git a/src/pip/_internal/locations/__init__.py b/src/pip/_internal/locations/__init__.py index db1c1309ee4..327d75a957b 100644 --- a/src/pip/_internal/locations/__init__.py +++ b/src/pip/_internal/locations/__init__.py @@ -131,8 +131,16 @@ def get_prefixed_libs(prefix): """Return the lib locations under ``prefix``.""" old_pure, old_plat = _distutils.get_prefixed_libs(prefix) new_pure, new_plat = _sysconfig.get_prefixed_libs(prefix) - _warn_if_mismatch(old_pure, new_pure, key="prefixed-purelib") - _warn_if_mismatch(old_plat, new_plat, key="prefixed-platlib") + _warn_if_mismatch( + pathlib.Path(old_pure), + pathlib.Path(new_pure), + key="prefixed-purelib", + ) + _warn_if_mismatch( + pathlib.Path(old_plat), + pathlib.Path(new_plat), + key="prefixed-platlib", + ) if old_pure == old_plat: return [old_pure] return [old_pure, old_plat] diff --git a/src/pip/_internal/locations/_sysconfig.py b/src/pip/_internal/locations/_sysconfig.py index d29580dcc9f..64aa487dc68 100644 --- a/src/pip/_internal/locations/_sysconfig.py +++ b/src/pip/_internal/locations/_sysconfig.py @@ -18,7 +18,7 @@ def _infer_scheme(variant): - # (typing.Literal["home", "prefix", "user"]) -> str + # type: (typing.Literal["home", "prefix", "user"]) -> str """Try to find a scheme for the current platform. Unfortunately ``_get_default_scheme()`` is private, so there's no way to @@ -46,16 +46,16 @@ def _infer_scheme(variant): # Update these keys if the user sets a custom home. -_HOME_KEYS = ( +_HOME_KEYS = [ "installed_base", "base", "installed_platbase", "platbase", "prefix", "exec_prefix", -) +] if sysconfig.get_config_var("userbase") is not None: - _HOME_KEYS += ("userbase",) + _HOME_KEYS.append("userbase") def get_scheme( @@ -86,11 +86,11 @@ def get_scheme( raise InvalidSchemeCombination("--home", "--prefix") if home is not None: - scheme = _infer_scheme("home") + scheme_name = _infer_scheme("home") elif user: - scheme = _infer_scheme("user") + scheme_name = _infer_scheme("user") else: - scheme = _infer_scheme("prefix") + scheme_name = _infer_scheme("prefix") if home is not None: variables = {k: home for k in _HOME_KEYS} @@ -99,7 +99,7 @@ def get_scheme( else: variables = {} - paths = sysconfig.get_paths(scheme=scheme, vars=variables) + paths = sysconfig.get_paths(scheme=scheme_name, vars=variables) # Special header path for compatibility to distutils. if running_under_virtualenv(): @@ -126,21 +126,22 @@ def get_bin_prefix(): # Forcing to use /usr/local/bin for standard macOS framework installs. if sys.platform[:6] == "darwin" and sys.prefix[:16] == "/System/Library/": return "/usr/local/bin" - return sysconfig.get_path("scripts", scheme=_infer_scheme("prefix")) + return sysconfig.get_paths(scheme=_infer_scheme("prefix"))["scripts"] def get_bin_user(): - return sysconfig.get_path("scripts", scheme=_infer_scheme("user")) + # type: () -> str + return sysconfig.get_paths(scheme=_infer_scheme("user"))["scripts"] def get_purelib(): # type: () -> str - return sysconfig.get_path("purelib") + return sysconfig.get_paths()["purelib"] def get_platlib(): # type: () -> str - return sysconfig.get_path("platlib") + return sysconfig.get_paths()["platlib"] def get_prefixed_libs(prefix): diff --git a/src/pip/_internal/locations/base.py b/src/pip/_internal/locations/base.py index 7f8ef670aac..98557abbe63 100644 --- a/src/pip/_internal/locations/base.py +++ b/src/pip/_internal/locations/base.py @@ -43,6 +43,6 @@ def get_src_prefix(): try: # Use getusersitepackages if this is present, as it ensures that the # value is initialised properly. - user_site = site.getusersitepackages() + user_site = site.getusersitepackages() # type: typing.Optional[str] except AttributeError: user_site = site.USER_SITE From 7563a9c6cc1c576020d3e92ca0ddcae7e19e228d Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 23 Feb 2021 02:04:15 +0800 Subject: [PATCH 3006/3170] Goodness --- src/pip/_internal/locations/_distutils.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pip/_internal/locations/_distutils.py b/src/pip/_internal/locations/_distutils.py index 70b7e8d6dd3..d4cb6b17948 100644 --- a/src/pip/_internal/locations/_distutils.py +++ b/src/pip/_internal/locations/_distutils.py @@ -18,7 +18,6 @@ from .base import get_major_minor_version, user_site - if WINDOWS: bin_py = os.path.join(sys.prefix, "Scripts") bin_user = os.path.join(user_site, "Scripts") From 917ecadd77b889182da2f33fe989bafa2ff70f07 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 15 Dec 2020 20:36:45 +0800 Subject: [PATCH 3007/3170] Show constraint in error message --- .../resolution/resolvelib/factory.py | 38 +++++++++++++------ .../resolution/resolvelib/resolver.py | 2 +- tests/functional/test_install_reqs.py | 4 +- tests/functional/test_new_resolver.py | 2 +- 4 files changed, 31 insertions(+), 15 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 506a560d6fb..3181d575336 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -404,8 +404,24 @@ def _report_requires_python_error( ) return UnsupportedPythonVersion(message) - def get_installation_error(self, e): - # type: (ResolutionImpossible) -> InstallationError + def _report_single_requirement_conflict(self, req, parent): + # type: (Requirement, Candidate) -> DistributionNotFound + if parent is None: + req_disp = str(req) + else: + req_disp = f"{req} (from {parent.name})" + logger.critical( + "Could not find a version that satisfies the requirement %s", + req_disp, + ) + return DistributionNotFound(f"No matching distribution found for {req}") + + def get_installation_error( + self, + e, # type: ResolutionImpossible + constraints, # type: Dict[str, Constraint] + ): + # type: (...) -> InstallationError assert e.causes, "Installation error reported with no cause" @@ -425,15 +441,8 @@ def get_installation_error(self, e): # satisfied. We just report that case. if len(e.causes) == 1: req, parent = e.causes[0] - if parent is None: - req_disp = str(req) - else: - req_disp = f"{req} (from {parent.name})" - logger.critical( - "Could not find a version that satisfies the requirement %s", - req_disp, - ) - return DistributionNotFound(f"No matching distribution found for {req}") + if req.name not in constraints: + return self._report_single_requirement_conflict(req, parent) # OK, we now have a list of requirements that can't all be # satisfied at once. @@ -475,13 +484,20 @@ def describe_trigger(parent): ) logger.critical(msg) msg = "\nThe conflict is caused by:" + + relevant_constraints = set() for req, parent in e.causes: + if req.name in constraints: + relevant_constraints.add(req.name) msg = msg + "\n " if parent: msg = msg + "{} {} depends on ".format(parent.name, parent.version) else: msg = msg + "The user requested " msg = msg + req.format_for_error() + for key in relevant_constraints: + spec = constraints[key].specifier + msg += f"\n The user requested (constraint) {key}{spec}" msg = ( msg diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 8828155a228..247b1ddfc67 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -122,7 +122,7 @@ def resolve(self, root_reqs, check_supported_wheels): ) except ResolutionImpossible as e: - error = self.factory.get_installation_error(e) + error = self.factory.get_installation_error(e, constraints) six.raise_from(error, e) req_set = RequirementSet(check_supported_wheels=check_supported_wheels) diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index 9e6e5580ae2..d559e94be18 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -357,7 +357,7 @@ def test_constraints_local_editable_install_causes_error( assert 'Could not satisfy constraints' in result.stderr, str(result) else: # Because singlemodule only has 0.0.1 available. - assert 'No matching distribution found' in result.stderr, str(result) + assert 'Cannot install singlemodule 0.0.1' in result.stderr, str(result) @pytest.mark.network @@ -386,7 +386,7 @@ def test_constraints_local_install_causes_error( assert 'Could not satisfy constraints' in result.stderr, str(result) else: # Because singlemodule only has 0.0.1 available. - assert 'No matching distribution found' in result.stderr, str(result) + assert 'Cannot install singlemodule 0.0.1' in result.stderr, str(result) def test_constraints_constrain_to_local_editable( diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 4d2acbb23b4..16f9f4f4216 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -687,7 +687,7 @@ def test_new_resolver_constraint_on_dependency(script): @pytest.mark.parametrize( "constraint_version, expect_error, message", [ - ("1.0", True, "ERROR: No matching distribution found for foo 2.0"), + ("1.0", True, "Cannot install foo 2.0"), ("2.0", False, "Successfully installed foo-2.0"), ], ) From 7c0428e72f89050bebe546c259715b0d77db2a55 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 15 Dec 2020 22:13:01 +0800 Subject: [PATCH 3008/3170] News --- news/9300.bugfix.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 news/9300.bugfix.rst diff --git a/news/9300.bugfix.rst b/news/9300.bugfix.rst new file mode 100644 index 00000000000..7da27f9975e --- /dev/null +++ b/news/9300.bugfix.rst @@ -0,0 +1,2 @@ +New resolver: Show relevant entries from user-supplied constraint files in the +error message to improve debuggability. From 1e1289e550e9aa329b1e3aa3f49edb6022dde525 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 23 Feb 2021 03:51:12 +0800 Subject: [PATCH 3009/3170] User-site special case fixes --- src/pip/_internal/locations/_distutils.py | 20 +------------------- src/pip/_internal/locations/_sysconfig.py | 21 +++++++++++++++++---- src/pip/_internal/locations/base.py | 18 ++++++++++++++++++ 3 files changed, 36 insertions(+), 23 deletions(-) diff --git a/src/pip/_internal/locations/_distutils.py b/src/pip/_internal/locations/_distutils.py index d4cb6b17948..7eecf8d8fa4 100644 --- a/src/pip/_internal/locations/_distutils.py +++ b/src/pip/_internal/locations/_distutils.py @@ -5,7 +5,6 @@ import os import os.path -import sys from distutils.cmd import Command as DistutilsCommand from distutils.command.install import SCHEME_KEYS from distutils.command.install import install as distutils_install_command @@ -13,26 +12,9 @@ from typing import Dict, List, Optional, Tuple, Union, cast from pip._internal.models.scheme import Scheme -from pip._internal.utils.compat import WINDOWS from pip._internal.utils.virtualenv import running_under_virtualenv -from .base import get_major_minor_version, user_site - -if WINDOWS: - bin_py = os.path.join(sys.prefix, "Scripts") - bin_user = os.path.join(user_site, "Scripts") - # buildout uses 'bin' on Windows too? - if not os.path.exists(bin_py): - bin_py = os.path.join(sys.prefix, "bin") - bin_user = os.path.join(user_site, "bin") -else: - bin_py = os.path.join(sys.prefix, "bin") - bin_user = os.path.join(user_site, "bin") - - # 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/": - bin_py = "/usr/local/bin" +from .base import bin_py, bin_user, get_major_minor_version def _distutils_scheme( diff --git a/src/pip/_internal/locations/_sysconfig.py b/src/pip/_internal/locations/_sysconfig.py index 64aa487dc68..a71cd8adc43 100644 --- a/src/pip/_internal/locations/_sysconfig.py +++ b/src/pip/_internal/locations/_sysconfig.py @@ -9,7 +9,7 @@ from pip._internal.models.scheme import SCHEME_KEYS, Scheme from pip._internal.utils.virtualenv import running_under_virtualenv -from .base import get_major_minor_version +from .base import bin_user, get_major_minor_version logger = logging.getLogger(__name__) @@ -101,12 +101,22 @@ def get_scheme( paths = sysconfig.get_paths(scheme=scheme_name, vars=variables) - # Special header path for compatibility to distutils. + # Pip historically uses a special header path in virtual environments. if running_under_virtualenv(): - base = variables.get("base", sys.prefix) + if user: + base = variables.get("userbase", sys.prefix) + else: + base = variables.get("base", sys.prefix) python_xy = f"python{get_major_minor_version()}" paths["include"] = os.path.join(base, "include", "site", python_xy) + # Special user scripts path on Windows for compatibility to distutils. + # See ``distutils.commands.install.INSTALL_SCHEMES["nt_user"]["scritps"]``. + if scheme_name == "nt_user": + base = variables.get("userbase", sys.prefix) + python_xy = f"Python{sys.version_info.major}{sys.version_info.minor}" + paths["scripts"] = os.path.join(base, python_xy, "Scripts") + scheme = Scheme( platlib=paths["platlib"], purelib=paths["purelib"], @@ -131,7 +141,10 @@ def get_bin_prefix(): def get_bin_user(): # type: () -> str - return sysconfig.get_paths(scheme=_infer_scheme("user"))["scripts"] + # pip puts the scripts directory in site-packages, not under userbase. + # I'm honestly not sure if this is a bug (because ``get_scheme()`` puts it + # correctly under userbase), but we need to be compatible. + return bin_user def get_purelib(): diff --git a/src/pip/_internal/locations/base.py b/src/pip/_internal/locations/base.py index 98557abbe63..5035662e3a4 100644 --- a/src/pip/_internal/locations/base.py +++ b/src/pip/_internal/locations/base.py @@ -5,6 +5,7 @@ import typing from pip._internal.utils import appdirs +from pip._internal.utils.compat import WINDOWS from pip._internal.utils.virtualenv import running_under_virtualenv # Application Directories @@ -46,3 +47,20 @@ def get_src_prefix(): user_site = site.getusersitepackages() # type: typing.Optional[str] except AttributeError: user_site = site.USER_SITE + + +if WINDOWS: + bin_py = os.path.join(sys.prefix, "Scripts") + bin_user = os.path.join(user_site, "Scripts") + # buildout uses 'bin' on Windows too? + if not os.path.exists(bin_py): + bin_py = os.path.join(sys.prefix, "bin") + bin_user = os.path.join(user_site, "bin") +else: + bin_py = os.path.join(sys.prefix, "bin") + bin_user = os.path.join(user_site, "bin") + + # 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/": + bin_py = "/usr/local/bin" From b7068f643e1b0b4fddab040878319ae8276efd0c Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 23 Feb 2021 04:24:17 +0800 Subject: [PATCH 3010/3170] Split bin_user and bin_prefix implementations Module-level logic is bad. --- src/pip/_internal/locations/__init__.py | 9 +------- src/pip/_internal/locations/_distutils.py | 22 +++++++++++------- src/pip/_internal/locations/_sysconfig.py | 10 +------- src/pip/_internal/locations/base.py | 28 +++++++++++------------ 4 files changed, 30 insertions(+), 39 deletions(-) diff --git a/src/pip/_internal/locations/__init__.py b/src/pip/_internal/locations/__init__.py index 327d75a957b..78969546345 100644 --- a/src/pip/_internal/locations/__init__.py +++ b/src/pip/_internal/locations/__init__.py @@ -8,6 +8,7 @@ from . import _distutils, _sysconfig from .base import ( USER_CACHE_DIR, + get_bin_user, get_major_minor_version, get_src_prefix, site_packages, @@ -100,14 +101,6 @@ def get_bin_prefix(): return old -def get_bin_user(): - # type: () -> str - old = _distutils.get_bin_user() - new = _sysconfig.get_bin_user() - _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="bin_user") - return old - - def get_purelib(): # type: () -> str """Return the default pure-Python lib location.""" diff --git a/src/pip/_internal/locations/_distutils.py b/src/pip/_internal/locations/_distutils.py index 7eecf8d8fa4..2d7ab73213c 100644 --- a/src/pip/_internal/locations/_distutils.py +++ b/src/pip/_internal/locations/_distutils.py @@ -4,7 +4,7 @@ # mypy: strict-optional=False import os -import os.path +import sys from distutils.cmd import Command as DistutilsCommand from distutils.command.install import SCHEME_KEYS from distutils.command.install import install as distutils_install_command @@ -12,9 +12,10 @@ from typing import Dict, List, Optional, Tuple, Union, cast from pip._internal.models.scheme import Scheme +from pip._internal.utils.compat import WINDOWS from pip._internal.utils.virtualenv import running_under_virtualenv -from .base import bin_py, bin_user, get_major_minor_version +from .base import get_major_minor_version def _distutils_scheme( @@ -118,12 +119,17 @@ def get_scheme( def get_bin_prefix(): # type: () -> str - return bin_py - - -def get_bin_user(): - # type: () -> str - return bin_user + if WINDOWS: + bin_py = os.path.join(sys.prefix, "Scripts") + # buildout uses 'bin' on Windows too? + if not os.path.exists(bin_py): + bin_py = os.path.join(sys.prefix, "bin") + return bin_py + # 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/": + return "/usr/local/bin" + return os.path.join(sys.prefix, "bin") def get_purelib(): diff --git a/src/pip/_internal/locations/_sysconfig.py b/src/pip/_internal/locations/_sysconfig.py index a71cd8adc43..8ef72813b3b 100644 --- a/src/pip/_internal/locations/_sysconfig.py +++ b/src/pip/_internal/locations/_sysconfig.py @@ -9,7 +9,7 @@ from pip._internal.models.scheme import SCHEME_KEYS, Scheme from pip._internal.utils.virtualenv import running_under_virtualenv -from .base import bin_user, get_major_minor_version +from .base import get_major_minor_version logger = logging.getLogger(__name__) @@ -139,14 +139,6 @@ def get_bin_prefix(): return sysconfig.get_paths(scheme=_infer_scheme("prefix"))["scripts"] -def get_bin_user(): - # type: () -> str - # pip puts the scripts directory in site-packages, not under userbase. - # I'm honestly not sure if this is a bug (because ``get_scheme()`` puts it - # correctly under userbase), but we need to be compatible. - return bin_user - - def get_purelib(): # type: () -> str return sysconfig.get_paths()["purelib"] diff --git a/src/pip/_internal/locations/base.py b/src/pip/_internal/locations/base.py index 5035662e3a4..3a03a79565c 100644 --- a/src/pip/_internal/locations/base.py +++ b/src/pip/_internal/locations/base.py @@ -49,18 +49,18 @@ def get_src_prefix(): user_site = site.USER_SITE -if WINDOWS: - bin_py = os.path.join(sys.prefix, "Scripts") - bin_user = os.path.join(user_site, "Scripts") - # buildout uses 'bin' on Windows too? - if not os.path.exists(bin_py): - bin_py = os.path.join(sys.prefix, "bin") - bin_user = os.path.join(user_site, "bin") -else: - bin_py = os.path.join(sys.prefix, "bin") - bin_user = os.path.join(user_site, "bin") +def get_bin_user(): + # type: () -> str + """Get the user-site scripts directory. - # 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/": - bin_py = "/usr/local/bin" + Pip puts the scripts directory in site-packages, not under userbase. + I'm honestly not sure if this is a bug (because ``get_scheme()`` puts it + correctly under userbase), but we need to keep backwards compatibility. + """ + assert user_site is not None, "user site unavailable" + if not WINDOWS: + return os.path.join(user_site, "bin") + # Special case for buildout, which uses 'bin' on Windows too? + if not os.path.exists(os.path.join(sys.prefix, "Scripts")): + os.path.join(user_site, "bin") + return os.path.join(user_site, "Scripts") From 60a82e7a0eb47191ad92f045c1522dfce6faaeb7 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 23 Feb 2021 04:38:32 +0800 Subject: [PATCH 3011/3170] Better erroring --- src/pip/_internal/locations/base.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/locations/base.py b/src/pip/_internal/locations/base.py index 3a03a79565c..e373ab30c75 100644 --- a/src/pip/_internal/locations/base.py +++ b/src/pip/_internal/locations/base.py @@ -4,6 +4,7 @@ import sysconfig import typing +from pip._internal.exceptions import UserInstallationInvalid from pip._internal.utils import appdirs from pip._internal.utils.compat import WINDOWS from pip._internal.utils.virtualenv import running_under_virtualenv @@ -57,7 +58,8 @@ def get_bin_user(): I'm honestly not sure if this is a bug (because ``get_scheme()`` puts it correctly under userbase), but we need to keep backwards compatibility. """ - assert user_site is not None, "user site unavailable" + if user_site is None: + raise UserInstallationInvalid() if not WINDOWS: return os.path.join(user_site, "bin") # Special case for buildout, which uses 'bin' on Windows too? From 0c4bd55706cb85166c6b9634284c5b03c39c0103 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 23 Feb 2021 05:04:21 +0800 Subject: [PATCH 3012/3170] Stray name --- src/pip/_internal/locations/__init__.py | 1 - 1 file changed, 1 deletion(-) diff --git a/src/pip/_internal/locations/__init__.py b/src/pip/_internal/locations/__init__.py index 78969546345..57fbbfd250c 100644 --- a/src/pip/_internal/locations/__init__.py +++ b/src/pip/_internal/locations/__init__.py @@ -25,7 +25,6 @@ "get_purelib", "get_scheme", "get_src_prefix", - "init_backend", "site_packages", "user_site", ] From 838988cb447223622b0a1aa85011c52c5b19aea0 Mon Sep 17 00:00:00 2001 From: David Hewitt <1939362+davidhewitt@users.noreply.github.com> Date: Tue, 3 Nov 2020 09:13:02 +0000 Subject: [PATCH 3013/3170] Build local directories in-place with feature flag --- docs/html/reference/pip_install.rst | 10 +++++++- news/9091.feature.rst | 4 +++ src/pip/_internal/cli/cmdoptions.py | 2 +- src/pip/_internal/cli/req_command.py | 1 + src/pip/_internal/operations/prepare.py | 34 ++++++++++++++++++++++--- tests/functional/test_install.py | 22 ++++++++++++++++ tests/unit/test_req.py | 1 + 7 files changed, 69 insertions(+), 5 deletions(-) create mode 100644 news/9091.feature.rst diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index 81e315ebaa2..742c4ddb3c6 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -808,7 +808,15 @@ You can install local projects by specifying the project path to pip: During regular installation, pip will copy the entire project directory to a temporary location and install from there. The exception is that pip will exclude .tox and .nox directories present in the top level of the project from -being copied. +being copied. This approach is the cause of several performance and correctness +issues, so it is planned that pip 21.3 will change to install directly from the +local project directory. Depending on the build backend used by the project, +this may generate secondary build artifacts in the project directory, such as +the ``.egg-info`` and ``build`` directories in the case of the setuptools +backend. + +To opt in to the future behavior, specify the ``--use-feature=in-tree-build`` +option in pip's command line. .. _`editable-installs`: diff --git a/news/9091.feature.rst b/news/9091.feature.rst new file mode 100644 index 00000000000..8147e79c5e8 --- /dev/null +++ b/news/9091.feature.rst @@ -0,0 +1,4 @@ +Add a feature ``--use-feature=in-tree-build`` to build local projects in-place +when installing. This is expected to become the default behavior in pip 21.3; +see `Installing from local packages <https://pip.pypa.io/en/stable/user_guide/#installing-from-local-packages>`_ +for more information. diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index 3075de94e39..7dc3d30571f 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -951,7 +951,7 @@ def check_list_path_option(options): metavar="feature", action="append", default=[], - choices=["2020-resolver", "fast-deps"], + choices=["2020-resolver", "fast-deps", "in-tree-build"], help="Enable new functionality, that may be backward incompatible.", ) # type: Callable[..., Option] diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index 4302b5bdc8d..a55dd7516d8 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -245,6 +245,7 @@ def make_requirement_preparer( require_hashes=options.require_hashes, use_user_site=use_user_site, lazy_wheel=lazy_wheel, + in_tree_build="in-tree-build" in options.features_enabled, ) @classmethod diff --git a/src/pip/_internal/operations/prepare.py b/src/pip/_internal/operations/prepare.py index 72743648a7e..3d074f9f629 100644 --- a/src/pip/_internal/operations/prepare.py +++ b/src/pip/_internal/operations/prepare.py @@ -35,6 +35,7 @@ from pip._internal.network.session import PipSession from pip._internal.req.req_install import InstallRequirement from pip._internal.req.req_tracker import RequirementTracker +from pip._internal.utils.deprecation import deprecated from pip._internal.utils.filesystem import copy2_fixed from pip._internal.utils.hashes import Hashes, MissingHashes from pip._internal.utils.logging import indent_log @@ -207,8 +208,23 @@ def unpack_url( unpack_vcs_link(link, location) return None - # If it's a url to a local directory + # Once out-of-tree-builds are no longer supported, could potentially + # replace the below condition with `assert not link.is_existing_dir` + # - unpack_url does not need to be called for in-tree-builds. + # + # As further cleanup, _copy_source_tree and accompanying tests can + # be removed. if link.is_existing_dir(): + deprecated( + "A future pip version will change local packages to be built " + "in-place without first copying to a temporary directory. " + "We recommend you use --use-feature=in-tree-build to test " + "your packages with this new behavior before it becomes the " + "default.\n", + replacement=None, + gone_in="21.3", + issue=7555 + ) if os.path.isdir(location): rmtree(location) _copy_source_tree(link.file_path, location) @@ -278,6 +294,7 @@ def __init__( require_hashes, # type: bool use_user_site, # type: bool lazy_wheel, # type: bool + in_tree_build, # type: bool ): # type: (...) -> None super().__init__() @@ -306,6 +323,9 @@ def __init__( # Should wheels be downloaded lazily? self.use_lazy_wheel = lazy_wheel + # Should in-tree builds be used for local paths? + self.in_tree_build = in_tree_build + # Memoized downloaded files, as mapping of url: (path, mime type) self._downloaded = {} # type: Dict[str, Tuple[str, str]] @@ -339,6 +359,11 @@ def _ensure_link_req_src_dir(self, req, parallel_builds): # directory. return assert req.source_dir is None + if req.link.is_existing_dir() and self.in_tree_build: + # build local directories in-tree + req.source_dir = req.link.file_path + return + # We always delete unpacked sdists after pip runs. req.ensure_has_source_dir( self.build_dir, @@ -517,11 +542,14 @@ def _prepare_linked_requirement(self, req, parallel_builds): self._ensure_link_req_src_dir(req, parallel_builds) hashes = self._get_linked_req_hashes(req) - if link.url not in self._downloaded: + + if link.is_existing_dir() and self.in_tree_build: + local_file = None + elif link.url not in self._downloaded: try: local_file = unpack_url( link, req.source_dir, self._download, - self.download_dir, hashes, + self.download_dir, hashes ) except NetworkConnectionError as exc: raise InstallationError( diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 44342978e74..0b33afeac39 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -581,6 +581,28 @@ def test_install_from_local_directory_with_symlinks_to_directories( result.did_create(dist_info_folder) +def test_install_from_local_directory_with_in_tree_build( + script, data, with_wheel +): + """ + Test installing from a local directory with --use-feature=in-tree-build. + """ + to_install = data.packages.joinpath("FSPkg") + args = ["install", "--use-feature=in-tree-build", to_install] + + in_tree_build_dir = to_install / "build" + assert not in_tree_build_dir.exists() + result = script.pip(*args) + fspkg_folder = script.site_packages / 'fspkg' + dist_info_folder = ( + script.site_packages / + 'FSPkg-0.1.dev0.dist-info' + ) + result.did_create(fspkg_folder) + result.did_create(dist_info_folder) + assert in_tree_build_dir.exists() + + @pytest.mark.skipif("sys.platform == 'win32' or sys.version_info < (3,)") def test_install_from_local_directory_with_socket_file( script, data, tmpdir, with_wheel diff --git a/tests/unit/test_req.py b/tests/unit/test_req.py index 9eab6fab04a..5f01a9ecc23 100644 --- a/tests/unit/test_req.py +++ b/tests/unit/test_req.py @@ -89,6 +89,7 @@ def _basic_resolver(self, finder, require_hashes=False): require_hashes=require_hashes, use_user_site=False, lazy_wheel=False, + in_tree_build=False, ) yield Resolver( preparer=preparer, From 63ea2e8c0886ad5ae8c54427e4d75051d9943834 Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Fri, 25 Dec 2020 07:54:37 -0800 Subject: [PATCH 3014/3170] Handle several mypy TODO comments and exceptions Remove mypy exceptions that are straightforward to remove. --- ...60-12d9-4e58-8322-21e5975a804e.trivial.rst | 0 setup.py | 5 +- src/pip/_internal/cli/parser.py | 32 ++++++++-- src/pip/_internal/network/auth.py | 2 +- src/pip/_internal/network/session.py | 63 ++++++++++++++----- .../_internal/resolution/legacy/resolver.py | 5 +- src/pip/_internal/utils/compat.py | 3 - src/pip/_internal/utils/deprecation.py | 15 +++-- src/pip/_internal/utils/logging.py | 35 ++++++++--- src/pip/_internal/utils/misc.py | 40 +++++++++--- src/pip/_internal/utils/models.py | 11 +++- tools/tox_pip.py | 9 ++- 12 files changed, 165 insertions(+), 55 deletions(-) create mode 100644 news/40711960-12d9-4e58-8322-21e5975a804e.trivial.rst diff --git a/news/40711960-12d9-4e58-8322-21e5975a804e.trivial.rst b/news/40711960-12d9-4e58-8322-21e5975a804e.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/setup.py b/setup.py index 66820387bb9..a413c5f678f 100644 --- a/setup.py +++ b/setup.py @@ -1,6 +1,3 @@ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - import os import sys @@ -8,6 +5,7 @@ def read(rel_path): + # type: (str) -> str here = os.path.abspath(os.path.dirname(__file__)) # intentionally *not* adding an encoding option to open, See: # https://github.com/pypa/virtualenv/issues/201#issuecomment-3145690 @@ -16,6 +14,7 @@ def read(rel_path): def get_version(rel_path): + # type: (str) -> str for line in read(rel_path).splitlines(): if line.startswith('__version__'): # __version__ = "0.9" diff --git a/src/pip/_internal/cli/parser.py b/src/pip/_internal/cli/parser.py index 8fd3d2d6b31..16523c5a19c 100644 --- a/src/pip/_internal/cli/parser.py +++ b/src/pip/_internal/cli/parser.py @@ -1,15 +1,12 @@ """Base option parser setup""" -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - import logging import optparse import shutil import sys import textwrap from contextlib import suppress -from typing import Any +from typing import Any, Dict, Iterator, List, Tuple from pip._internal.cli.status_codes import UNKNOWN_ERROR from pip._internal.configuration import Configuration, ConfigurationError @@ -22,6 +19,7 @@ class PrettyHelpFormatter(optparse.IndentedHelpFormatter): """A prettier/less verbose help formatter for optparse.""" def __init__(self, *args, **kwargs): + # type: (*Any, **Any) -> None # help position must be aligned with __init__.parseopts.description kwargs["max_help_position"] = 30 kwargs["indent_increment"] = 1 @@ -29,9 +27,11 @@ def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) def format_option_strings(self, option): + # type: (optparse.Option) -> str return self._format_option_strings(option) def _format_option_strings(self, option, mvarfmt=" <{}>", optsep=", "): + # type: (optparse.Option, str, str) -> str """ Return a comma-separated list of option strings and metavars. @@ -49,17 +49,20 @@ def _format_option_strings(self, option, mvarfmt=" <{}>", optsep=", "): opts.insert(1, optsep) if option.takes_value(): + assert option.dest is not None metavar = option.metavar or option.dest.lower() opts.append(mvarfmt.format(metavar.lower())) return "".join(opts) def format_heading(self, heading): + # type: (str) -> str if heading == "Options": return "" return heading + ":\n" def format_usage(self, usage): + # type: (str) -> str """ Ensure there is only one newline between usage and the first heading if there is no description. @@ -68,6 +71,7 @@ def format_usage(self, usage): return msg def format_description(self, description): + # type: (str) -> str # leave full control over description to us if description: if hasattr(self.parser, "main"): @@ -86,6 +90,7 @@ def format_description(self, description): return "" def format_epilog(self, epilog): + # type: (str) -> str # leave full control over epilog to us if epilog: return epilog @@ -93,6 +98,7 @@ def format_epilog(self, epilog): return "" def indent_lines(self, text, indent): + # type: (str, str) -> str new_lines = [indent + line for line in text.split("\n")] return "\n".join(new_lines) @@ -107,9 +113,12 @@ class UpdatingDefaultsHelpFormatter(PrettyHelpFormatter): """ def expand_default(self, option): + # type: (optparse.Option) -> str default_values = None if self.parser is not None: + assert isinstance(self.parser, ConfigOptionParser) self.parser._update_defaults(self.parser.defaults) + assert option.dest is not None default_values = self.parser.defaults.get(option.dest) help_text = super().expand_default(option) @@ -129,6 +138,7 @@ def expand_default(self, option): class CustomOptionParser(optparse.OptionParser): def insert_option_group(self, idx, *args, **kwargs): + # type: (int, Any, Any) -> optparse.OptionGroup """Insert an OptionGroup at a given position.""" group = self.add_option_group(*args, **kwargs) @@ -139,6 +149,7 @@ def insert_option_group(self, idx, *args, **kwargs): @property def option_list_all(self): + # type: () -> List[optparse.Option] """Get a list of all options, including those in option groups.""" res = self.option_list[:] for i in self.option_groups: @@ -166,6 +177,7 @@ def __init__( super().__init__(*args, **kwargs) def check_default(self, option, key, val): + # type: (optparse.Option, str, Any) -> Any try: return option.check_value(key, val) except optparse.OptionValueError as exc: @@ -173,11 +185,14 @@ def check_default(self, option, key, val): sys.exit(3) def _get_ordered_configuration_items(self): + # type: () -> Iterator[Tuple[str, Any]] # 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} + section_items = { + name: [] for name in override_order + } # type: Dict[str, List[Tuple[str, Any]]] for section_key, val in self.config.items(): # ignore empty values if not val: @@ -197,6 +212,7 @@ def _get_ordered_configuration_items(self): yield key, val def _update_defaults(self, defaults): + # type: (Dict[str, Any]) -> Dict[str, Any] """Updates the given defaults with values from the config files and the environ. Does a little special handling for certain types of options (lists).""" @@ -215,6 +231,8 @@ def _update_defaults(self, defaults): if option is None: continue + assert option.dest is not None + if option.action in ("store_true", "store_false"): try: val = strtobool(val) @@ -240,6 +258,7 @@ def _update_defaults(self, defaults): val = val.split() val = [self.check_default(option, key, v) for v in val] elif option.action == "callback": + assert option.callback is not None late_eval.add(option.dest) opt_str = option.get_opt_string() val = option.convert_value(opt_str, val) @@ -258,6 +277,7 @@ def _update_defaults(self, defaults): return defaults def get_default_values(self): + # type: () -> optparse.Values """Overriding to make updating the defaults after instantiation of the option parser possible, _update_defaults() does the dirty work.""" if not self.process_default_values: @@ -272,6 +292,7 @@ def get_default_values(self): defaults = self._update_defaults(self.defaults.copy()) # ours for option in self._get_all_options(): + assert option.dest is not None default = defaults.get(option.dest) if isinstance(default, str): opt_str = option.get_opt_string() @@ -279,5 +300,6 @@ def get_default_values(self): return optparse.Values(defaults) def error(self, msg): + # type: (str) -> None self.print_usage(sys.stderr) self.exit(UNKNOWN_ERROR, f"{msg}\n") diff --git a/src/pip/_internal/network/auth.py b/src/pip/_internal/network/auth.py index cad22a02ce3..bd54a5cba99 100644 --- a/src/pip/_internal/network/auth.py +++ b/src/pip/_internal/network/auth.py @@ -37,7 +37,7 @@ def get_keyring_auth(url, username): - # type: (str, str) -> Optional[AuthInfo] + # type: (Optional[str], Optional[str]) -> Optional[AuthInfo] """Return the tuple auth for a given url from keyring.""" global keyring if not url or not keyring: diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py index 423922a007d..b42d06bc3d3 100644 --- a/src/pip/_internal/network/session.py +++ b/src/pip/_internal/network/session.py @@ -2,8 +2,15 @@ network request configuration and behavior. """ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False +# When mypy runs on Windows the call to distro.linux_distribution() is skipped +# resulting in the failure: +# +# error: unused 'type: ignore' comment +# +# If the upstream module adds typing, this comment should be removed. See +# https://github.com/nir0s/distro/pull/269 +# +# mypy: warn-unused-ignores=False import email.utils import ipaddress @@ -15,13 +22,14 @@ import sys import urllib.parse import warnings -from typing import Any, Iterator, List, Optional, Sequence, Tuple, Union +from typing import Any, Dict, Iterator, List, Mapping, Optional, Sequence, Tuple, Union from pip._vendor import requests, urllib3 from pip._vendor.cachecontrol import CacheControlAdapter from pip._vendor.requests.adapters import BaseAdapter, HTTPAdapter -from pip._vendor.requests.models import Response +from pip._vendor.requests.models import PreparedRequest, Response from pip._vendor.requests.structures import CaseInsensitiveDict +from pip._vendor.urllib3.connectionpool import ConnectionPool from pip._vendor.urllib3.exceptions import InsecureRequestWarning from pip import __version__ @@ -89,6 +97,7 @@ def looks_like_ci(): def user_agent(): + # type: () -> str """ Return a string representing the user agent. """ @@ -98,15 +107,14 @@ def user_agent(): "implementation": { "name": platform.python_implementation(), }, - } + } # type: Dict[str, Any] if data["implementation"]["name"] == 'CPython': data["implementation"]["version"] = platform.python_version() elif data["implementation"]["name"] == 'PyPy': - if sys.pypy_version_info.releaselevel == 'final': - pypy_version_info = sys.pypy_version_info[:3] - else: - pypy_version_info = sys.pypy_version_info + pypy_version_info = sys.pypy_version_info # type: ignore + if pypy_version_info.releaselevel == 'final': + pypy_version_info = pypy_version_info[:3] data["implementation"]["version"] = ".".join( [str(x) for x in pypy_version_info] ) @@ -119,9 +127,12 @@ def user_agent(): if sys.platform.startswith("linux"): from pip._vendor import distro + + # https://github.com/nir0s/distro/pull/269 + linux_distribution = distro.linux_distribution() # type: ignore distro_infos = dict(filter( lambda x: x[1], - zip(["name", "version", "id"], distro.linux_distribution()), + zip(["name", "version", "id"], linux_distribution), )) libc = dict(filter( lambda x: x[1], @@ -170,8 +181,16 @@ def user_agent(): class LocalFSAdapter(BaseAdapter): - def send(self, request, stream=None, timeout=None, verify=None, cert=None, - proxies=None): + def send( + self, + request, # type: PreparedRequest + stream=False, # type: bool + timeout=None, # type: Optional[Union[float, Tuple[float, float]]] + verify=True, # type: Union[bool, str] + cert=None, # type: Optional[Union[str, Tuple[str, str]]] + proxies=None, # type:Optional[Mapping[str, str]] + ): + # type: (...) -> Response pathname = url_to_path(request.url) resp = Response() @@ -198,18 +217,33 @@ def send(self, request, stream=None, timeout=None, verify=None, cert=None, return resp def close(self): + # type: () -> None pass class InsecureHTTPAdapter(HTTPAdapter): - def cert_verify(self, conn, url, verify, cert): + def cert_verify( + self, + conn, # type: ConnectionPool + url, # type: str + verify, # type: Union[bool, str] + cert, # type: Optional[Union[str, Tuple[str, str]]] + ): + # type: (...) -> None super().cert_verify(conn=conn, url=url, verify=False, cert=cert) class InsecureCacheControlAdapter(CacheControlAdapter): - def cert_verify(self, conn, url, verify, cert): + def cert_verify( + self, + conn, # type: ConnectionPool + url, # type: str + verify, # type: Union[bool, str] + cert, # type: Optional[Union[str, Tuple[str, str]]] + ): + # type: (...) -> None super().cert_verify(conn=conn, url=url, verify=False, cert=cert) @@ -407,6 +441,7 @@ def is_secure_origin(self, location): return False def request(self, method, url, *args, **kwargs): + # type: (str, str, *Any, **Any) -> Response # Allow setting a default timeout on a session kwargs.setdefault("timeout", self.timeout) diff --git a/src/pip/_internal/resolution/legacy/resolver.py b/src/pip/_internal/resolution/legacy/resolver.py index c37b396e21e..17de7f09a37 100644 --- a/src/pip/_internal/resolution/legacy/resolver.py +++ b/src/pip/_internal/resolution/legacy/resolver.py @@ -12,13 +12,12 @@ # The following comment should be removed at some point in the future. # mypy: strict-optional=False -# mypy: disallow-untyped-defs=False import logging import sys from collections import defaultdict from itertools import chain -from typing import DefaultDict, List, Optional, Set, Tuple +from typing import DefaultDict, Iterable, List, Optional, Set, Tuple from pip._vendor.packaging import specifiers from pip._vendor.pkg_resources import Distribution @@ -388,6 +387,7 @@ def _resolve_one( more_reqs = [] # type: List[InstallRequirement] def add_req(subreq, extras_requested): + # type: (Distribution, Iterable[str]) -> None sub_install_req = self._make_install_req( str(subreq), req_to_install, @@ -447,6 +447,7 @@ def get_installation_order(self, req_set): ordered_reqs = set() # type: Set[InstallRequirement] def schedule(req): + # type: (InstallRequirement) -> None if req.satisfied_by or req in ordered_reqs: return if req.constraint: diff --git a/src/pip/_internal/utils/compat.py b/src/pip/_internal/utils/compat.py index 069e3091d29..0cb1c469704 100644 --- a/src/pip/_internal/utils/compat.py +++ b/src/pip/_internal/utils/compat.py @@ -1,9 +1,6 @@ """Stuff that differs in different Python versions and platform distributions.""" -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - import logging import os import sys diff --git a/src/pip/_internal/utils/deprecation.py b/src/pip/_internal/utils/deprecation.py index 3d52e179ab2..b62b3fb6509 100644 --- a/src/pip/_internal/utils/deprecation.py +++ b/src/pip/_internal/utils/deprecation.py @@ -2,12 +2,9 @@ A module that implements tooling to enable easy warnings about deprecations. """ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - import logging import warnings -from typing import Any, Optional +from typing import Any, Optional, TextIO, Type, Union from pip._vendor.packaging.version import parse @@ -24,7 +21,15 @@ class PipDeprecationWarning(Warning): # Warnings <-> Logging Integration -def _showwarning(message, category, filename, lineno, file=None, line=None): +def _showwarning( + message, # type: Union[Warning, str] + category, # type: Type[Warning] + filename, # type: str + lineno, # type: int + file=None, # type: Optional[TextIO] + line=None, # type: Optional[str] +): + # type: (...) -> None if file is not None: if _original_showwarning is not None: _original_showwarning(message, category, filename, lineno, file, line) diff --git a/src/pip/_internal/utils/logging.py b/src/pip/_internal/utils/logging.py index ce78416ec01..70b86cc0e21 100644 --- a/src/pip/_internal/utils/logging.py +++ b/src/pip/_internal/utils/logging.py @@ -1,6 +1,3 @@ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - import contextlib import errno import logging @@ -8,7 +5,7 @@ import os import sys from logging import Filter, getLogger -from typing import Any +from typing import IO, Any, Callable, Iterator, Optional, TextIO, Type, cast from pip._internal.utils.compat import WINDOWS from pip._internal.utils.deprecation import DEPRECATION_MSG_PREFIX @@ -46,15 +43,17 @@ class BrokenStdoutLoggingError(Exception): # https://bugs.python.org/issue19612 # https://bugs.python.org/issue30418 def _is_broken_pipe_error(exc_class, exc): + # type: (Type[BaseException], BaseException) -> bool """See the docstring for non-Windows below.""" return (exc_class is BrokenPipeError) or ( - exc_class is OSError and exc.errno in (errno.EINVAL, errno.EPIPE) + isinstance(exc, OSError) and exc.errno in (errno.EINVAL, errno.EPIPE) ) else: # Then we are in the non-Windows case. def _is_broken_pipe_error(exc_class, exc): + # type: (Type[BaseException], BaseException) -> bool """ Return whether an exception is a broken pipe error. @@ -67,6 +66,7 @@ def _is_broken_pipe_error(exc_class, exc): @contextlib.contextmanager def indent_log(num=2): + # type: (int) -> Iterator[None] """ A context manager which will cause the log output to be indented for any log messages emitted inside it. @@ -81,6 +81,7 @@ def indent_log(num=2): def get_indentation(): + # type: () -> int return getattr(_log_state, "indentation", 0) @@ -104,6 +105,7 @@ def __init__( super().__init__(*args, **kwargs) def get_message_start(self, formatted, levelno): + # type: (str, int) -> str """ Return the start of the formatted log message (not counting the prefix to add to each line). @@ -120,6 +122,7 @@ def get_message_start(self, formatted, levelno): return "ERROR: " def format(self, record): + # type: (logging.LogRecord) -> str """ Calls the standard formatter, but will indent all of the log message lines by our current indentation level. @@ -137,7 +140,9 @@ def format(self, record): def _color_wrap(*colors): + # type: (*str) -> Callable[[str], str] def wrapped(inp): + # type: (str) -> str return "".join(list(colors) + [inp, colorama.Style.RESET_ALL]) return wrapped @@ -156,6 +161,7 @@ class ColorizedStreamHandler(logging.StreamHandler): COLORS = [] def __init__(self, stream=None, no_color=None): + # type: (Optional[TextIO], bool) -> None super().__init__(stream) self._no_color = no_color @@ -163,16 +169,19 @@ def __init__(self, stream=None, no_color=None): self.stream = colorama.AnsiToWin32(self.stream) def _using_stdout(self): + # type: () -> bool """ Return whether the handler is using sys.stdout. """ if WINDOWS and colorama: # Then self.stream is an AnsiToWin32 object. - return self.stream.wrapped is sys.stdout + stream = cast(colorama.AnsiToWin32, self.stream) + return stream.wrapped is sys.stdout return self.stream is sys.stdout def should_color(self): + # type: () -> bool # Don't colorize things if we do not have colorama or if told not to if not colorama or self._no_color: return False @@ -195,6 +204,7 @@ def should_color(self): return False def format(self, record): + # type: (logging.LogRecord) -> str msg = logging.StreamHandler.format(self, record) if self.should_color(): @@ -207,12 +217,18 @@ def format(self, record): # The logging module says handleError() can be customized. def handleError(self, record): + # type: (logging.LogRecord) -> None exc_class, exc = sys.exc_info()[:2] # If a broken pipe occurred while calling write() or flush() on the # stdout stream in logging's Handler.emit(), then raise our special # exception so we can handle it in main() instead of logging the # broken pipe error and continuing. - if exc_class and self._using_stdout() and _is_broken_pipe_error(exc_class, exc): + if ( + exc_class + and exc + and self._using_stdout() + and _is_broken_pipe_error(exc_class, exc) + ): raise BrokenStdoutLoggingError() return super().handleError(record) @@ -220,15 +236,18 @@ def handleError(self, record): class BetterRotatingFileHandler(logging.handlers.RotatingFileHandler): def _open(self): + # type: () -> IO[Any] ensure_dir(os.path.dirname(self.baseFilename)) return logging.handlers.RotatingFileHandler._open(self) class MaxLevelFilter(Filter): def __init__(self, level): + # type: (int) -> None self.level = level def filter(self, record): + # type: (logging.LogRecord) -> bool return record.levelno < self.level @@ -239,12 +258,14 @@ class ExcludeLoggerFilter(Filter): """ def filter(self, record): + # type: (logging.LogRecord) -> bool # The base Filter class allows only records from a logger (or its # children). return not super().filter(record) def setup_logging(verbosity, no_color, user_log_file): + # type: (int, bool, Optional[str]) -> int """Configures and sets up all of the logging Returns the requested logging level, as its integer value. diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 4b1fecc9342..e41a138a385 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -1,6 +1,5 @@ # The following comment should be removed at some point in the future. # mypy: strict-optional=False -# mypy: disallow-untyped-defs=False import contextlib import errno @@ -16,16 +15,21 @@ import urllib.parse from io import StringIO from itertools import filterfalse, tee, zip_longest +from types import TracebackType from typing import ( Any, AnyStr, + BinaryIO, Callable, Container, + ContextManager, Iterable, Iterator, List, Optional, + TextIO, Tuple, + Type, TypeVar, cast, ) @@ -64,8 +68,10 @@ logger = logging.getLogger(__name__) -VersionInfo = Tuple[int, int, int] T = TypeVar("T") +ExcInfo = Tuple[Type[BaseException], BaseException, TracebackType] +VersionInfo = Tuple[int, int, int] +NetlocTuple = Tuple[str, Tuple[Optional[str], Optional[str]]] def get_pip_version(): @@ -132,6 +138,7 @@ def rmtree(dir, ignore_errors=False): def rmtree_errorhandler(func, path, exc_info): + # type: (Callable[..., Any], str, ExcInfo) -> None """On Windows, the files in .svn are read-only, so when rmtree() tries to remove them, an exception is thrown. We catch that here, remove the read-only attribute, and hopefully continue without problems.""" @@ -279,6 +286,7 @@ def is_installable_dir(path): def read_chunks(file, size=io.DEFAULT_BUFFER_SIZE): + # type: (BinaryIO, int) -> Iterator[bytes] """Yield pieces of data from a file-like object until EOF.""" while True: chunk = file.read(size) @@ -491,19 +499,24 @@ def write_output(msg, *args): class StreamWrapper(StringIO): + orig_stream = None # type: TextIO + @classmethod def from_stream(cls, orig_stream): + # type: (TextIO) -> StreamWrapper cls.orig_stream = orig_stream return cls() # compileall.compile_dir() needs stdout.encoding to print to stdout + # https://github.com/python/mypy/issues/4125 @property - def encoding(self): + def encoding(self): # type: ignore return self.orig_stream.encoding @contextlib.contextmanager def captured_output(stream_name): + # type: (str) -> Iterator[StreamWrapper] """Return a context manager used by captured_stdout/stdin/stderr that temporarily replaces the sys stream *stream_name* with a StringIO. @@ -518,6 +531,7 @@ def captured_output(stream_name): def captured_stdout(): + # type: () -> ContextManager[StreamWrapper] """Capture the output of sys.stdout: with captured_stdout() as stdout: @@ -530,6 +544,7 @@ def captured_stdout(): def captured_stderr(): + # type: () -> ContextManager[StreamWrapper] """ See captured_stdout(). """ @@ -538,6 +553,7 @@ def captured_stderr(): # Simulates an enum def enum(*sequential, **named): + # type: (*Any, **Any) -> Type[Any] enums = dict(zip(sequential, range(len(sequential))), **named) reverse = {value: key for key, value in enums.items()} enums["reverse_mapping"] = reverse @@ -579,6 +595,7 @@ def parse_netloc(netloc): def split_auth_from_netloc(netloc): + # type: (str) -> NetlocTuple """ Parse out and remove the auth information from a netloc. @@ -591,17 +608,20 @@ def split_auth_from_netloc(netloc): # behaves if more than one @ is present (which can be checked using # the password attribute of urlsplit()'s return value). auth, netloc = netloc.rsplit("@", 1) + pw = None # type: Optional[str] if ":" in auth: # Split from the left because that's how urllib.parse.urlsplit() # behaves if more than one : is present (which again can be checked # using the password attribute of the return value) - user_pass = auth.split(":", 1) + user, pw = auth.split(":", 1) else: - user_pass = auth, None + user, pw = auth, None - user_pass = tuple(None if x is None else urllib.parse.unquote(x) for x in user_pass) + user = urllib.parse.unquote(user) + if pw is not None: + pw = urllib.parse.unquote(pw) - return netloc, user_pass + return netloc, (user, pw) def redact_netloc(netloc): @@ -628,6 +648,7 @@ def redact_netloc(netloc): def _transform_url(url, transform_netloc): + # type: (str, Callable[[str], Tuple[Any, ...]]) -> Tuple[str, NetlocTuple] """Transform and replace netloc in a url. transform_netloc is a function taking the netloc and returning a @@ -642,14 +663,16 @@ def _transform_url(url, transform_netloc): # stripped url url_pieces = (purl.scheme, netloc_tuple[0], purl.path, purl.query, purl.fragment) surl = urllib.parse.urlunsplit(url_pieces) - return surl, netloc_tuple + return surl, cast("NetlocTuple", netloc_tuple) def _get_netloc(netloc): + # type: (str) -> NetlocTuple return split_auth_from_netloc(netloc) def _redact_netloc(netloc): + # type: (str) -> Tuple[str,] return (redact_netloc(netloc),) @@ -765,6 +788,7 @@ def hash_file(path, blocksize=1 << 20): def is_wheel_installed(): + # type: () -> bool """ Return whether the wheel package is installed. """ diff --git a/src/pip/_internal/utils/models.py b/src/pip/_internal/utils/models.py index daf065825c5..0e02bc7a5b1 100644 --- a/src/pip/_internal/utils/models.py +++ b/src/pip/_internal/utils/models.py @@ -1,9 +1,8 @@ """Utilities for defining models """ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False import operator +from typing import Any, Callable, Type class KeyBasedCompareMixin: @@ -12,28 +11,36 @@ class KeyBasedCompareMixin: __slots__ = ["_compare_key", "_defining_class"] def __init__(self, key, defining_class): + # type: (Any, Type[KeyBasedCompareMixin]) -> None self._compare_key = key self._defining_class = defining_class def __hash__(self): + # type: () -> int return hash(self._compare_key) def __lt__(self, other): + # type: (Any) -> bool return self._compare(other, operator.__lt__) def __le__(self, other): + # type: (Any) -> bool return self._compare(other, operator.__le__) def __gt__(self, other): + # type: (Any) -> bool return self._compare(other, operator.__gt__) def __ge__(self, other): + # type: (Any) -> bool return self._compare(other, operator.__ge__) def __eq__(self, other): + # type: (Any) -> bool return self._compare(other, operator.__eq__) def _compare(self, other, method): + # type: (Any, Callable[[Any, Any], bool]) -> bool if not isinstance(other, self._defining_class): return NotImplemented diff --git a/tools/tox_pip.py b/tools/tox_pip.py index 5996dade6d2..fe7621342a6 100644 --- a/tools/tox_pip.py +++ b/tools/tox_pip.py @@ -1,17 +1,16 @@ -# The following comment should be removed at some point in the future. -# mypy: disallow-untyped-defs=False - import os import shutil import subprocess import sys from glob import glob +from typing import List VIRTUAL_ENV = os.environ['VIRTUAL_ENV'] TOX_PIP_DIR = os.path.join(VIRTUAL_ENV, 'pip') def pip(args): + # type: (List[str]) -> None # First things first, get a recent (stable) version of pip. if not os.path.exists(TOX_PIP_DIR): subprocess.check_call([sys.executable, '-m', 'pip', @@ -20,8 +19,8 @@ def pip(args): 'pip']) shutil.rmtree(glob(os.path.join(TOX_PIP_DIR, 'pip-*.dist-info'))[0]) # And use that version. - pypath = os.environ.get('PYTHONPATH') - pypath = pypath.split(os.pathsep) if pypath is not None else [] + pypath_env = os.environ.get('PYTHONPATH') + pypath = pypath_env.split(os.pathsep) if pypath_env is not None else [] pypath.insert(0, TOX_PIP_DIR) os.environ['PYTHONPATH'] = os.pathsep.join(pypath) subprocess.check_call([sys.executable, '-m', 'pip'] + args) From 69577a6e0803a01b20d7912a482344cb23339910 Mon Sep 17 00:00:00 2001 From: Klaas van Schelven <klaas@vanschelven.com> Date: Tue, 23 Feb 2021 11:17:54 +0100 Subject: [PATCH 3015/3170] Documentation: add a warning about "dependency confusion" vulnerability See #9612 --- docs/html/reference/pip_install.rst | 28 ++++++++++++++++++---------- docs/html/user_guide.rst | 2 +- 2 files changed, 19 insertions(+), 11 deletions(-) diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index 742c4ddb3c6..32dbbc78426 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -1123,37 +1123,45 @@ Examples py -m pip install --index-url http://my.package.repo/simple/ SomePackage - Search an additional index during install, in addition to `PyPI`_ + Install from a local flat directory containing archives (and don't scan indexes): .. tab:: Unix/macOS .. code-block:: shell - python -m pip install --extra-index-url http://my.package.repo/simple SomePackage + python -m pip install --no-index --find-links=file:///local/dir/ SomePackage + python -m pip install --no-index --find-links=/local/dir/ SomePackage + python -m pip install --no-index --find-links=relative/dir/ SomePackage .. tab:: Windows .. code-block:: shell - py -m pip install --extra-index-url http://my.package.repo/simple SomePackage + py -m pip install --no-index --find-links=file:///local/dir/ SomePackage + py -m pip install --no-index --find-links=/local/dir/ SomePackage + py -m pip install --no-index --find-links=relative/dir/ SomePackage - Install from a local flat directory containing archives (and don't scan indexes): + Search an additional index during install, in addition to `PyPI`_ + + .. warning:: + + Using this option to search for packages which are not in the main + repository (such as private packages) is unsafe, per a security + vulnerability called "dependency confusion": an attacker can claim the + package on the public repository in a way that will ensure it gets + chosen over the private package. .. tab:: Unix/macOS .. code-block:: shell - python -m pip install --no-index --find-links=file:///local/dir/ SomePackage - python -m pip install --no-index --find-links=/local/dir/ SomePackage - python -m pip install --no-index --find-links=relative/dir/ SomePackage + python -m pip install --extra-index-url http://my.package.repo/simple SomePackage .. tab:: Windows .. code-block:: shell - py -m pip install --no-index --find-links=file:///local/dir/ SomePackage - py -m pip install --no-index --find-links=/local/dir/ SomePackage - py -m pip install --no-index --find-links=relative/dir/ SomePackage + py -m pip install --extra-index-url http://my.package.repo/simple SomePackage #. Find pre-release and development versions, in addition to stable versions. By default, pip only finds stable versions. diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 92887885baf..527c14fd424 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -125,7 +125,7 @@ does not come with it included. pip install keyring echo your-password | keyring set pypi.company.com your-username - pip install your-package --extra-index-url https://pypi.company.com/ + pip install your-package --index-url https://pypi.company.com/ .. _keyring: https://pypi.org/project/keyring/ From e4e9af1d27deaa94543735c72872363b30ef00da Mon Sep 17 00:00:00 2001 From: Andrey Bienkowski <hexagonrecursion@gmail.com> Date: Wed, 10 Feb 2021 09:44:34 +0300 Subject: [PATCH 3016/3170] Drop python2 related stuff in tests --- ...51-9a1a-453e-af98-bbb35f7c3e66.trivial.rst | 0 tests/functional/test_install.py | 15 ++------ tests/functional/test_pep517.py | 3 +- tests/lib/__init__.py | 8 ----- tests/unit/test_operations_prepare.py | 4 +-- tests/unit/test_utils_subprocess.py | 36 ------------------- 6 files changed, 5 insertions(+), 61 deletions(-) create mode 100644 news/fc6b6951-9a1a-453e-af98-bbb35f7c3e66.trivial.rst diff --git a/news/fc6b6951-9a1a-453e-af98-bbb35f7c3e66.trivial.rst b/news/fc6b6951-9a1a-453e-af98-bbb35f7c3e66.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 0b33afeac39..b60221b3438 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -23,9 +23,7 @@ need_svn, path_to_url, pyversion, - pyversion_tuple, requirements_file, - windows_workaround_7667, ) from tests.lib.filesystem import make_socket_file from tests.lib.local_repos import local_checkout @@ -193,16 +191,10 @@ def test_pip_second_command_line_interface_works( """ # Re-install pip so we get the launchers. script.pip_install_local('-f', common_wheels, pip_src) - # On old versions of Python, urllib3/requests will raise a warning about - # the lack of an SSLContext. - kwargs = {'expect_stderr': deprecated_python} - if pyversion_tuple < (2, 7, 9): - kwargs['expect_stderr'] = True - args = [f'pip{pyversion}'] args.extend(['install', 'INITools==0.2']) args.extend(['-f', data.packages]) - result = script.run(*args, **kwargs) + result = script.run(*args) dist_info_folder = ( script.site_packages / 'INITools-0.2.dist-info' @@ -603,7 +595,7 @@ def test_install_from_local_directory_with_in_tree_build( assert in_tree_build_dir.exists() -@pytest.mark.skipif("sys.platform == 'win32' or sys.version_info < (3,)") +@pytest.mark.skipif("sys.platform == 'win32'") def test_install_from_local_directory_with_socket_file( script, data, tmpdir, with_wheel ): @@ -764,7 +756,6 @@ def test_install_using_install_option_and_editable(script, tmpdir): @pytest.mark.xfail @pytest.mark.network @need_mercurial -@windows_workaround_7667 def test_install_global_option_using_editable(script, tmpdir): """ Test using global distutils options, but in an editable installation @@ -1407,7 +1398,6 @@ def test_install_no_binary_disables_building_wheels(script, data, with_wheel): @pytest.mark.network -@windows_workaround_7667 def test_install_no_binary_builds_pep_517_wheel(script, data, with_wheel): to_install = data.packages.joinpath('pep517_setup_and_pyproject') res = script.pip( @@ -1422,7 +1412,6 @@ def test_install_no_binary_builds_pep_517_wheel(script, data, with_wheel): @pytest.mark.network -@windows_workaround_7667 def test_install_no_binary_uses_local_backend( script, data, with_wheel, tmpdir): to_install = data.packages.joinpath('pep517_wrapper_buildsys') diff --git a/tests/functional/test_pep517.py b/tests/functional/test_pep517.py index bcad4793672..a747b8a0756 100644 --- a/tests/functional/test_pep517.py +++ b/tests/functional/test_pep517.py @@ -3,7 +3,7 @@ from pip._internal.build_env import BuildEnvironment from pip._internal.req import InstallRequirement -from tests.lib import make_test_finder, path_to_url, windows_workaround_7667 +from tests.lib import make_test_finder, path_to_url def make_project(tmpdir, requires=None, backend=None, backend_path=None): @@ -255,7 +255,6 @@ def test_explicit_setuptools_backend(script, tmpdir, data, common_wheels): @pytest.mark.network -@windows_workaround_7667 def test_pep517_and_build_options(script, tmpdir, data, common_wheels): """Backend generated requirements are installed in the build env""" project_dir, name = make_pyproject_with_setup(tmpdir) diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index f349b52d3fa..98474dda46f 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -31,7 +31,6 @@ SRC_DIR = Path(__file__).resolve().parent.parent.parent pyversion = get_major_minor_version() -pyversion_tuple = sys.version_info CURRENT_PY_VERSION_INFO = sys.version_info[:3] @@ -1145,10 +1144,3 @@ def need_mercurial(fn): return pytest.mark.mercurial(need_executable( 'Mercurial', ('hg', 'version') )(fn)) - - -# Workaround for test failures after new wheel release. -windows_workaround_7667 = pytest.mark.skipif( - "sys.platform == 'win32' and sys.version_info < (3,)", - reason="Workaround for #7667", -) diff --git a/tests/unit/test_operations_prepare.py b/tests/unit/test_operations_prepare.py index f6122cebeb4..4d912fb6eac 100644 --- a/tests/unit/test_operations_prepare.py +++ b/tests/unit/test_operations_prepare.py @@ -102,7 +102,7 @@ def test_copy_source_tree(clean_project, tmpdir): assert expected_files == copied_files -@pytest.mark.skipif("sys.platform == 'win32' or sys.version_info < (3,)") +@pytest.mark.skipif("sys.platform == 'win32'") def test_copy_source_tree_with_socket(clean_project, tmpdir, caplog): target = tmpdir.joinpath("target") expected_files = get_filelist(clean_project) @@ -121,7 +121,7 @@ def test_copy_source_tree_with_socket(clean_project, tmpdir, caplog): assert socket_path in record.message -@pytest.mark.skipif("sys.platform == 'win32' or sys.version_info < (3,)") +@pytest.mark.skipif("sys.platform == 'win32'") def test_copy_source_tree_with_socket_fails_with_no_socket_error( clean_project, tmpdir ): diff --git a/tests/unit/test_utils_subprocess.py b/tests/unit/test_utils_subprocess.py index d64ffbe02e6..7a31eeb7425 100644 --- a/tests/unit/test_utils_subprocess.py +++ b/tests/unit/test_utils_subprocess.py @@ -57,11 +57,6 @@ def test_make_subprocess_output_error__non_ascii_command_arg(monkeypatch): Test a command argument with a non-ascii character. """ cmd_args = ['foo', 'déf'] - if sys.version_info[0] == 2: - # Check in Python 2 that the str (bytes object) with the non-ascii - # character has the encoding we expect. (This comes from the source - # code encoding at the top of the file.) - assert cmd_args[1].decode('utf-8') == 'déf' # We need to monkeypatch so the encoding will be correct on Windows. monkeypatch.setattr(locale, 'getpreferredencoding', lambda: 'utf-8') @@ -80,7 +75,6 @@ def test_make_subprocess_output_error__non_ascii_command_arg(monkeypatch): assert actual == expected, f'actual: {actual}' -@pytest.mark.skipif("sys.version_info < (3,)") def test_make_subprocess_output_error__non_ascii_cwd_python_3(monkeypatch): """ Test a str (text) cwd with a non-ascii character in Python 3. @@ -102,36 +96,6 @@ def test_make_subprocess_output_error__non_ascii_cwd_python_3(monkeypatch): assert actual == expected, f'actual: {actual}' -@pytest.mark.parametrize('encoding', [ - 'utf-8', - # Test a Windows encoding. - 'cp1252', -]) -@pytest.mark.skipif("sys.version_info >= (3,)") -def test_make_subprocess_output_error__non_ascii_cwd_python_2( - monkeypatch, encoding, -): - """ - Test a str (bytes object) cwd with a non-ascii character in Python 2. - """ - cmd_args = ['test'] - cwd = '/path/to/cwd/déf'.encode(encoding) - monkeypatch.setattr(sys, 'getfilesystemencoding', lambda: encoding) - actual = make_subprocess_output_error( - cmd_args=cmd_args, - cwd=cwd, - lines=[], - exit_status=1, - ) - expected = dedent("""\ - Command errored out with exit status 1: - command: test - cwd: /path/to/cwd/déf - Complete output (0 lines): - ----------------------------------------""") - assert actual == expected, f'actual: {actual}' - - # This test is mainly important for checking unicode in Python 2. def test_make_subprocess_output_error__non_ascii_line(): """ From c1d3b2786dd55f038c84d11adf344928f4b58109 Mon Sep 17 00:00:00 2001 From: Klaas van Schelven <klaas@vanschelven.com> Date: Tue, 23 Feb 2021 11:58:37 +0100 Subject: [PATCH 3017/3170] Add news entry --- news/9647.doc.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/9647.doc.rst diff --git a/news/9647.doc.rst b/news/9647.doc.rst new file mode 100644 index 00000000000..c087da23991 --- /dev/null +++ b/news/9647.doc.rst @@ -0,0 +1 @@ +Add warning about --extra-index-url and dependency confusion From a0a3bde15275d0d0b976c9080f210395a33f6d81 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 23 Feb 2021 19:27:27 +0800 Subject: [PATCH 3018/3170] Split scheme inference functions for clarity This also helps catch bugs in the logic, which are also fixed in this commit. --- src/pip/_internal/locations/_sysconfig.py | 59 ++++++++++++++--------- 1 file changed, 35 insertions(+), 24 deletions(-) diff --git a/src/pip/_internal/locations/_sysconfig.py b/src/pip/_internal/locations/_sysconfig.py index 8ef72813b3b..b78366a9db9 100644 --- a/src/pip/_internal/locations/_sysconfig.py +++ b/src/pip/_internal/locations/_sysconfig.py @@ -14,35 +14,46 @@ logger = logging.getLogger(__name__) -_AVAILABLE_SCHEMES = set(sysconfig.get_scheme_names()) +# Notes on _infer_* functions. +# Unfortunately ``_get_default_scheme()`` is private, so there's no way to +# ask things like "what is the '_prefix' scheme on this platform". These +# functions try to answer that with some heuristics while accounting for ad-hoc +# platforms not covered by CPython's default sysconfig implementation. If the +# ad-hoc implementation does not fully implement sysconfig, we'll fall back to +# a POSIX scheme. +_AVAILABLE_SCHEMES = set(sysconfig.get_scheme_names()) -def _infer_scheme(variant): - # type: (typing.Literal["home", "prefix", "user"]) -> str - """Try to find a scheme for the current platform. - Unfortunately ``_get_default_scheme()`` is private, so there's no way to - ask things like "what is the '_home' scheme on this platform". This tries - to answer that with some heuristics while accounting for ad-hoc platforms - not covered by CPython's default sysconfig implementation. - """ - # Most schemes are named like this e.g. "posix_home", "nt_user". - suffixed = f"{os.name}_{variant}" +def _infer_prefix(): + # type: () -> str + """Try to find a prefix scheme for the current platform.""" + suffixed = f"{os.name}_prefix" if suffixed in _AVAILABLE_SCHEMES: return suffixed + if os.name in _AVAILABLE_SCHEMES: # On Windows, prefx is just called "nt". + return os.name + return "posix_prefix" + - # The user scheme is not available. - if variant == "user" and sysconfig.get_config_var("userbase") is None: +def _infer_user(): + # type: () -> str + """Try to find a user scheme for the current platform.""" + suffixed = f"{os.name}_user" + if suffixed in _AVAILABLE_SCHEMES: + return suffixed + if "posix_user" not in _AVAILABLE_SCHEMES: # User scheme unavailable. raise UserInstallationInvalid() + return "posix_user" - # On Windows, prefx and home schemes are the same and just called "nt". - if os.name in _AVAILABLE_SCHEMES: - return os.name - # Not sure what's happening, some obscure platform that does not fully - # implement sysconfig? Just use the POSIX scheme. - logger.warning("No %r scheme for %r; fallback to POSIX.", variant, os.name) - return f"posix_{variant}" +def _infer_home(): + # type: () -> str + """Try to find a home for the current platform.""" + suffixed = f"{os.name}_home" + if suffixed in _AVAILABLE_SCHEMES: + return suffixed + return "posix_home" # Update these keys if the user sets a custom home. @@ -86,11 +97,11 @@ def get_scheme( raise InvalidSchemeCombination("--home", "--prefix") if home is not None: - scheme_name = _infer_scheme("home") + scheme_name = _infer_home() elif user: - scheme_name = _infer_scheme("user") + scheme_name = _infer_user() else: - scheme_name = _infer_scheme("prefix") + scheme_name = _infer_prefix() if home is not None: variables = {k: home for k in _HOME_KEYS} @@ -136,7 +147,7 @@ def get_bin_prefix(): # Forcing to use /usr/local/bin for standard macOS framework installs. if sys.platform[:6] == "darwin" and sys.prefix[:16] == "/System/Library/": return "/usr/local/bin" - return sysconfig.get_paths(scheme=_infer_scheme("prefix"))["scripts"] + return sysconfig.get_paths()["scripts"] def get_purelib(): From 581868484b0fe994341f9005497364669e4920b1 Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Sun, 3 Jan 2021 09:43:58 -0800 Subject: [PATCH 3019/3170] Use super() for Python 2 old-style classes Followup to c148bcc1aa4025a9dae5bcdabe1a1e01e6df28e5. --- news/1ab8f1c8-c115-4055-9a60-30a8f8eef7ba.trivial.rst | 0 src/pip/_internal/utils/logging.py | 4 ++-- tests/lib/path.py | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) create mode 100644 news/1ab8f1c8-c115-4055-9a60-30a8f8eef7ba.trivial.rst diff --git a/news/1ab8f1c8-c115-4055-9a60-30a8f8eef7ba.trivial.rst b/news/1ab8f1c8-c115-4055-9a60-30a8f8eef7ba.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/utils/logging.py b/src/pip/_internal/utils/logging.py index 70b86cc0e21..45798d54f7b 100644 --- a/src/pip/_internal/utils/logging.py +++ b/src/pip/_internal/utils/logging.py @@ -205,7 +205,7 @@ def should_color(self): def format(self, record): # type: (logging.LogRecord) -> str - msg = logging.StreamHandler.format(self, record) + msg = super().format(record) if self.should_color(): for level, color in self.COLORS: @@ -238,7 +238,7 @@ class BetterRotatingFileHandler(logging.handlers.RotatingFileHandler): def _open(self): # type: () -> IO[Any] ensure_dir(os.path.dirname(self.baseFilename)) - return logging.handlers.RotatingFileHandler._open(self) + return super()._open() class MaxLevelFilter(Filter): diff --git a/tests/lib/path.py b/tests/lib/path.py index a9dc29ad7a5..259293d0e38 100644 --- a/tests/lib/path.py +++ b/tests/lib/path.py @@ -22,8 +22,8 @@ class Path(str): def __new__(cls, *paths): if len(paths): - return str.__new__(cls, os.path.join(*paths)) - return str.__new__(cls) + return super().__new__(cls, os.path.join(*paths)) + return super().__new__(cls) def __div__(self, path): """ @@ -74,7 +74,7 @@ def __repr__(self): return "Path({inner})".format(inner=str.__repr__(self)) def __hash__(self): - return str.__hash__(self) + return super().__hash__() @property def name(self): From 6874e88ada70353f61e91ceeb750f00bb638127e Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Tue, 5 Jan 2021 16:00:41 -0800 Subject: [PATCH 3020/3170] Remove unnecessary method Path.__hash__ Only calls the parent implementation, so rely on inheritance. --- tests/lib/path.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/lib/path.py b/tests/lib/path.py index 259293d0e38..81c060b05a3 100644 --- a/tests/lib/path.py +++ b/tests/lib/path.py @@ -73,9 +73,6 @@ def __radd__(self, path): def __repr__(self): return "Path({inner})".format(inner=str.__repr__(self)) - def __hash__(self): - return super().__hash__() - @property def name(self): """ From 25a856bec4d9e9830ebca6ed2372ee79382319f5 Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller <bbodenmiller@gmail.com> Date: Wed, 24 Feb 2021 00:07:39 -0800 Subject: [PATCH 3021/3170] Update cmdoptions.py --- src/pip/_internal/cli/cmdoptions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index 4add6e9c0a5..f11bd6bac0f 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -310,7 +310,8 @@ def exists_action(): type='path', metavar='path', help="Path to PEM-encoded CA certificate bundle. " - "If provided, overrides the default.", + "If provided, overrides the default. " + "See 'SSL Certificate Verification' in pip documentation for more information.", ) # type: Callable[..., Option] client_cert = partial( From 527a2e95f8797b58edcfa68b7240576203d5a295 Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller <bbodenmiller@gmail.com> Date: Wed, 24 Feb 2021 00:18:45 -0800 Subject: [PATCH 3022/3170] Create 6720.doc.rst --- news/6720.doc.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/6720.doc.rst diff --git a/news/6720.doc.rst b/news/6720.doc.rst new file mode 100644 index 00000000000..8706e23899f --- /dev/null +++ b/news/6720.doc.rst @@ -0,0 +1 @@ +Improve SSL Certificate Verification docs and `--cert` help text. From 998d66aff8c6f3088e7c6f34f0147706019c38ee Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller <bbodenmiller@gmail.com> Date: Wed, 24 Feb 2021 00:22:23 -0800 Subject: [PATCH 3023/3170] Update cmdoptions.py --- src/pip/_internal/cli/cmdoptions.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index f11bd6bac0f..2d07d5bcc59 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -311,7 +311,8 @@ def exists_action(): metavar='path', help="Path to PEM-encoded CA certificate bundle. " "If provided, overrides the default. " - "See 'SSL Certificate Verification' in pip documentation for more information.", + "See 'SSL Certificate Verification' in pip documentation " + "for more information.", ) # type: Callable[..., Option] client_cert = partial( From 9e29b2351199d031baf489caef62a480178cdd4f Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller <bbodenmiller@gmail.com> Date: Wed, 24 Feb 2021 01:05:47 -0800 Subject: [PATCH 3024/3170] Update news/6720.doc.rst Co-authored-by: Tzu-ping Chung <uranusjr@gmail.com> --- news/6720.doc.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/6720.doc.rst b/news/6720.doc.rst index 8706e23899f..f5547dfe502 100644 --- a/news/6720.doc.rst +++ b/news/6720.doc.rst @@ -1 +1 @@ -Improve SSL Certificate Verification docs and `--cert` help text. +Improve SSL Certificate Verification docs and ``--cert`` help text. From e451d51870044762d9edad7e16f15e028e93e2a0 Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller <bbodenmiller@gmail.com> Date: Wed, 24 Feb 2021 01:07:05 -0800 Subject: [PATCH 3025/3170] Update cmdoptions.py --- src/pip/_internal/cli/cmdoptions.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index 2d07d5bcc59..a09ccee65ee 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -309,10 +309,12 @@ def exists_action(): dest='cert', type='path', metavar='path', - help="Path to PEM-encoded CA certificate bundle. " - "If provided, overrides the default. " - "See 'SSL Certificate Verification' in pip documentation " - "for more information.", + help=( + "Path to PEM-encoded CA certificate bundle. " + "If provided, overrides the default. " + "See 'SSL Certificate Verification' in pip documentation " + "for more information." + ), ) # type: Callable[..., Option] client_cert = partial( From e0d6028ebf5f136e16223714c4fb87decea3f456 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 24 Feb 2021 17:46:05 +0800 Subject: [PATCH 3026/3170] Additional logging to get context --- src/pip/_internal/locations/__init__.py | 61 ++++++++++++++++++------- 1 file changed, 44 insertions(+), 17 deletions(-) diff --git a/src/pip/_internal/locations/__init__.py b/src/pip/_internal/locations/__init__.py index 57fbbfd250c..9513d8b0fea 100644 --- a/src/pip/_internal/locations/__init__.py +++ b/src/pip/_internal/locations/__init__.py @@ -44,16 +44,30 @@ def _default_base(*, user): def _warn_if_mismatch(old, new, *, key): - # type: (pathlib.Path, pathlib.Path, str) -> None + # type: (pathlib.Path, pathlib.Path, str) -> bool if old == new: - return + return False issue_url = "https://github.com/pypa/pip/issues/9617" message = ( - "Value for %s does not match. Please report this to %s" + "Value for %s does not match. Please report this to <%s>" "\ndistutils: %s" "\nsysconfig: %s" ) logger.warning(message, key, issue_url, old, new) + return True + + +def _log_context( + *, + user: bool = False, + home: Optional[str] = None, + root: Optional[str] = None, + prefix: Optional[str] = None, +) -> None: + message = ( + "Additional context:" "\nuser = %r" "\nhome = %r" "\nroot = %r" "\nprefix = %r" + ) + logger.warning(message, user, home, root, prefix) def get_scheme( @@ -83,11 +97,15 @@ def get_scheme( ) base = prefix or home or _default_base(user=user) + warned = [] for k in SCHEME_KEYS: # Extra join because distutils can return relative paths. old_v = pathlib.Path(base, getattr(old, k)) new_v = pathlib.Path(getattr(new, k)) - _warn_if_mismatch(old_v, new_v, key=f"scheme.{k}") + warned.append(_warn_if_mismatch(old_v, new_v, key=f"scheme.{k}")) + + if any(warned): + _log_context(user=user, home=home, root=root, prefix=prefix) return old @@ -96,7 +114,8 @@ def get_bin_prefix(): # type: () -> str old = _distutils.get_bin_prefix() new = _sysconfig.get_bin_prefix() - _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="bin_prefix") + if _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="bin_prefix"): + _log_context() return old @@ -105,7 +124,8 @@ def get_purelib(): """Return the default pure-Python lib location.""" old = _distutils.get_purelib() new = _sysconfig.get_purelib() - _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="purelib") + if _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="purelib"): + _log_context() return old @@ -114,7 +134,8 @@ def get_platlib(): """Return the default platform-shared lib location.""" old = _distutils.get_platlib() new = _sysconfig.get_platlib() - _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="platlib") + if _warn_if_mismatch(pathlib.Path(old), pathlib.Path(new), key="platlib"): + _log_context() return old @@ -123,16 +144,22 @@ def get_prefixed_libs(prefix): """Return the lib locations under ``prefix``.""" old_pure, old_plat = _distutils.get_prefixed_libs(prefix) new_pure, new_plat = _sysconfig.get_prefixed_libs(prefix) - _warn_if_mismatch( - pathlib.Path(old_pure), - pathlib.Path(new_pure), - key="prefixed-purelib", - ) - _warn_if_mismatch( - pathlib.Path(old_plat), - pathlib.Path(new_plat), - key="prefixed-platlib", - ) + + warned = [ + _warn_if_mismatch( + pathlib.Path(old_pure), + pathlib.Path(new_pure), + key="prefixed-purelib", + ), + _warn_if_mismatch( + pathlib.Path(old_plat), + pathlib.Path(new_plat), + key="prefixed-platlib", + ), + ] + if any(warned): + _log_context(prefix=prefix) + if old_pure == old_plat: return [old_pure] return [old_pure, old_plat] From 0501ad7e4a64cd29d00382d1635aa3e9d69011a3 Mon Sep 17 00:00:00 2001 From: Klaas van Schelven <klaas@vanschelven.com> Date: Wed, 24 Feb 2021 14:38:52 +0100 Subject: [PATCH 3027/3170] Add link to paper about dependency confusion --- docs/html/reference/pip_install.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index 32dbbc78426..23137b1a7d0 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -1147,9 +1147,10 @@ Examples Using this option to search for packages which are not in the main repository (such as private packages) is unsafe, per a security - vulnerability called "dependency confusion": an attacker can claim the - package on the public repository in a way that will ensure it gets - chosen over the private package. + vulnerability called + `dependency confusion <https://azure.microsoft.com/en-us/resources/3-ways-to-mitigate-risk-using-private-package-feeds/>`_: + an attacker can claim the package on the public repository in a way that + will ensure it gets chosen over the private package. .. tab:: Unix/macOS From 32376bf577af51ed43819aa92e89231886e6b619 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 23 Feb 2021 02:32:07 +0800 Subject: [PATCH 3028/3170] Test for constraint in message --- tests/functional/test_new_resolver_errors.py | 21 ++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/functional/test_new_resolver_errors.py b/tests/functional/test_new_resolver_errors.py index 830acc764e9..e263f4206b8 100644 --- a/tests/functional/test_new_resolver_errors.py +++ b/tests/functional/test_new_resolver_errors.py @@ -24,3 +24,24 @@ def test_new_resolver_conflict_requirements_file(tmpdir, script): message = "package versions have conflicting dependencies" assert message in result.stderr, str(result) + + +def test_new_resolver_conflict_constraints_file(tmpdir, script): + create_basic_wheel_for_package(script, "pkg", "1.0") + + constrats_file = tmpdir.joinpath("constraints.txt") + constrats_file.write_text("pkg!=1.0") + + result = script.pip( + "install", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "-c", constrats_file, + "pkg==1.0", + expect_error=True, + ) + + assert "ResolutionImpossible" in result.stderr, str(result) + + message = "The user requested (constraint) pkg!=1.0" + assert message in result.stdout, str(result) From 3932c31fa71fe554521e85dfe459a1462e2e6da4 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 25 Feb 2021 13:59:55 +0800 Subject: [PATCH 3029/3170] Update news fragment from review suggestions Co-authored-by: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> --- news/8733.bugfix | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/8733.bugfix b/news/8733.bugfix index b07ed95698b..95fd675397f 100644 --- a/news/8733.bugfix +++ b/news/8733.bugfix @@ -1 +1 @@ -Correctly uninstall scripts installed with --user +Correctly uninstall script files (from setuptools' ``scripts`` argument), when installed with ``--user``. From c63e5d320994954ecd3fe6cb96cb5a9b9de4e346 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 25 Feb 2021 14:00:44 +0800 Subject: [PATCH 3030/3170] Add .rst suffix to news fragment --- news/{8733.bugfix => 8733.bugfix.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename news/{8733.bugfix => 8733.bugfix.rst} (100%) diff --git a/news/8733.bugfix b/news/8733.bugfix.rst similarity index 100% rename from news/8733.bugfix rename to news/8733.bugfix.rst From b656cefd48c5954b3479a80be5351c19ab2d5ed7 Mon Sep 17 00:00:00 2001 From: Quentin Lee <Q.C.L.Lee@student.tudelft.nl> Date: Mon, 22 Feb 2021 15:32:26 +0100 Subject: [PATCH 3031/3170] Update info for unit and integration tests on MacOs --- docs/html/development/ci.rst | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/docs/html/development/ci.rst b/docs/html/development/ci.rst index 5c33231b1b2..991c66c736a 100644 --- a/docs/html/development/ci.rst +++ b/docs/html/development/ci.rst @@ -133,11 +133,11 @@ Actual testing | | +-------+---------------+-----------------+ | | | PyPy3 | | | | MacOS +----------+-------+---------------+-----------------+ -| | | CP3.6 | Azure | Azure | +| | | CP3.6 | Github | Github | | | +-------+---------------+-----------------+ -| | x64 | CP3.7 | Azure | Azure | +| | x64 | CP3.7 | Github | Github | | | +-------+---------------+-----------------+ -| | | CP3.8 | Azure | Azure | +| | | CP3.8 | Github | Github | | | +-------+---------------+-----------------+ | | | PyPy3 | | | +-----------+----------+-------+---------------+-----------------+ From a1f70a7ad557f0b847b591957e4f0dc08e2969e2 Mon Sep 17 00:00:00 2001 From: Quentin Lee <Quentin.c.l.lee@gmail.com> Date: Thu, 25 Feb 2021 09:53:20 +0100 Subject: [PATCH 3032/3170] Add news.rst file --- news/855bfaed-4341-4d28-ab9e-e5ab43fb039f.trivial.rst | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 news/855bfaed-4341-4d28-ab9e-e5ab43fb039f.trivial.rst diff --git a/news/855bfaed-4341-4d28-ab9e-e5ab43fb039f.trivial.rst b/news/855bfaed-4341-4d28-ab9e-e5ab43fb039f.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d From 1a8ca4ca4e886379a5bc50954beb7e0dca284ea9 Mon Sep 17 00:00:00 2001 From: Klaas van Schelven <klaas@vanschelven.com> Date: Thu, 25 Feb 2021 16:30:54 +0100 Subject: [PATCH 3033/3170] Documentation formatting Co-authored-by: Tzu-ping Chung <uranusjr@gmail.com> --- news/9647.doc.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/9647.doc.rst b/news/9647.doc.rst index c087da23991..70917817611 100644 --- a/news/9647.doc.rst +++ b/news/9647.doc.rst @@ -1 +1 @@ -Add warning about --extra-index-url and dependency confusion +Add warning about ``--extra-index-url`` and dependency confusion From b920bbbe5b7595da9760a81066d810a7a780deae Mon Sep 17 00:00:00 2001 From: Ben Bodenmiller <bbodenmiller@gmail.com> Date: Fri, 26 Feb 2021 01:32:18 -0800 Subject: [PATCH 3034/3170] Update cmdoptions.py --- src/pip/_internal/cli/cmdoptions.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index a09ccee65ee..24dc9d14107 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -305,10 +305,10 @@ def exists_action(): cert = partial( PipOption, - '--cert', - dest='cert', - type='path', - metavar='path', + "--cert", + dest="cert", + type="path", + metavar="path", help=( "Path to PEM-encoded CA certificate bundle. " "If provided, overrides the default. " From 65934d3e8b85259c4b8b869fa35b45770690d561 Mon Sep 17 00:00:00 2001 From: Andrey Bienkowski <hexagonrecursion@gmail.com> Date: Fri, 26 Feb 2021 13:57:30 +0000 Subject: [PATCH 3035/3170] Upgrade from six.raise_from to the raise from syntax (#9590) * Upgrade from six.raise_from * Lint --- src/pip/_internal/resolution/resolvelib/resolver.py | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 247b1ddfc67..eba441091b6 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -3,7 +3,6 @@ import os from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple -from pip._vendor import six from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.resolvelib import ResolutionImpossible from pip._vendor.resolvelib import Resolver as RLResolver @@ -123,7 +122,7 @@ def resolve(self, root_reqs, check_supported_wheels): except ResolutionImpossible as e: error = self.factory.get_installation_error(e, constraints) - six.raise_from(error, e) + raise error from e req_set = RequirementSet(check_supported_wheels=check_supported_wheels) for candidate in self._result.mapping.values(): From a2c57948b654d79bc52fa5580bd9e8dc1f75d701 Mon Sep 17 00:00:00 2001 From: Blazej Michalik <blazej.michalik@nokia.com> Date: Thu, 31 Dec 2020 03:09:05 +0100 Subject: [PATCH 3036/3170] Bring back the "from versions:" message In the new resolver the "(from versions ...)" message, shown on failure to resolve a package, has been removed. This commit brings it back. --- news/9139.feature.rst | 1 + src/pip/_internal/resolution/resolvelib/factory.py | 9 ++++++++- 2 files changed, 9 insertions(+), 1 deletion(-) create mode 100644 news/9139.feature.rst diff --git a/news/9139.feature.rst b/news/9139.feature.rst new file mode 100644 index 00000000000..98dc133a1d6 --- /dev/null +++ b/news/9139.feature.rst @@ -0,0 +1 @@ +Bring back the "(from versions: ...)" message, that was shown on resolution failures. diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 3181d575336..f49288c83ef 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -410,10 +410,17 @@ def _report_single_requirement_conflict(self, req, parent): req_disp = str(req) else: req_disp = f"{req} (from {parent.name})" + + cands = self._finder.find_all_candidates(req.project_name) + versions = [str(v) for v in sorted(set(c.version for c in cands))] + logger.critical( - "Could not find a version that satisfies the requirement %s", + "Could not find a version that satisfies the requirement %s " + "(from versions: %s)", req_disp, + ", ".join(versions) or "none", ) + return DistributionNotFound(f"No matching distribution found for {req}") def get_installation_error( From b4ce28923710dd17f2b60d849fa97036dfd2020c Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sun, 28 Feb 2021 06:42:39 +0800 Subject: [PATCH 3037/3170] Add a couple of special cases for PyPy --- src/pip/_internal/locations/_sysconfig.py | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/locations/_sysconfig.py b/src/pip/_internal/locations/_sysconfig.py index b78366a9db9..ee75a6fb9b6 100644 --- a/src/pip/_internal/locations/_sysconfig.py +++ b/src/pip/_internal/locations/_sysconfig.py @@ -27,7 +27,22 @@ def _infer_prefix(): # type: () -> str - """Try to find a prefix scheme for the current platform.""" + """Try to find a prefix scheme for the current platform. + + This tries: + + * Implementation + OS, used by PyPy on Windows (``pypy_nt``). + * Implementation without OS, used by PyPy on POSIX (``pypy``). + * OS + "prefix", used by CPython on POSIX (``posix_prefix``). + * Just the OS name, used by CPython on Windows (``nt``). + + If none of the above works, fall back to ``posix_prefix``. + """ + implementation_suffixed = f"{sys.implementation.name}_{os.name}" + if implementation_suffixed in _AVAILABLE_SCHEMES: + return implementation_suffixed + if sys.implementation.name in _AVAILABLE_SCHEMES: + return sys.implementation.name suffixed = f"{os.name}_prefix" if suffixed in _AVAILABLE_SCHEMES: return suffixed From 2a5e84e1c4d9c8e4c4236e1eccfa580406a29b6b Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 3 Feb 2021 16:23:04 +0800 Subject: [PATCH 3038/3170] Add failing test --- tests/functional/test_new_resolver_errors.py | 30 +++++++++++++++++++- 1 file changed, 29 insertions(+), 1 deletion(-) diff --git a/tests/functional/test_new_resolver_errors.py b/tests/functional/test_new_resolver_errors.py index e263f4206b8..b4d63a99695 100644 --- a/tests/functional/test_new_resolver_errors.py +++ b/tests/functional/test_new_resolver_errors.py @@ -1,4 +1,6 @@ -from tests.lib import create_basic_wheel_for_package +import sys + +from tests.lib import create_basic_wheel_for_package, create_test_package_with_setup def test_new_resolver_conflict_requirements_file(tmpdir, script): @@ -45,3 +47,29 @@ def test_new_resolver_conflict_constraints_file(tmpdir, script): message = "The user requested (constraint) pkg!=1.0" assert message in result.stdout, str(result) + + +def test_new_resolver_requires_python_error(script): + compatible_python = ">={0.major}.{0.minor}".format(sys.version_info) + incompatible_python = "<{0.major}.{0.minor}".format(sys.version_info) + + pkga = create_test_package_with_setup( + script, + name="pkga", + version="1.0", + python_requires=compatible_python, + ) + pkgb = create_test_package_with_setup( + script, + name="pkgb", + version="1.0", + python_requires=incompatible_python, + ) + + # This always fails because pkgb can never be satisfied. + result = script.pip("install", "--no-index", pkga, pkgb, expect_error=True) + + # The error message should mention the Requires-Python: value causing the + # conflict, not the compatible one. + assert incompatible_python in result.stderr, str(result) + assert compatible_python not in result.stderr, str(result) From d87abdfdfe5175d76019ec2ce52b77656f3a3779 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 3 Feb 2021 16:47:22 +0800 Subject: [PATCH 3039/3170] Fix error reporting on Requires-Python conflicts --- news/9541.bugfix.rst | 1 + .../resolution/resolvelib/factory.py | 57 ++++++++++++------- 2 files changed, 37 insertions(+), 21 deletions(-) create mode 100644 news/9541.bugfix.rst diff --git a/news/9541.bugfix.rst b/news/9541.bugfix.rst new file mode 100644 index 00000000000..88180198c07 --- /dev/null +++ b/news/9541.bugfix.rst @@ -0,0 +1 @@ +Fix incorrect reporting on ``Requires-Python`` conflicts. diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 3181d575336..13ab7e904a0 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -1,6 +1,7 @@ import functools import logging from typing import ( + TYPE_CHECKING, Dict, FrozenSet, Iterable, @@ -60,6 +61,14 @@ UnsatisfiableRequirement, ) +if TYPE_CHECKING: + from typing import Protocol + + class ConflictCause(Protocol): + requirement: RequiresPythonRequirement + parent: Candidate + + logger = logging.getLogger(__name__) C = TypeVar("C") @@ -387,21 +396,25 @@ def get_dist_to_uninstall(self, candidate): ) return None - def _report_requires_python_error( - self, - requirement, # type: RequiresPythonRequirement - template, # type: Candidate - ): - # type: (...) -> UnsupportedPythonVersion - message_format = ( - "Package {package!r} requires a different Python: " - "{version} not in {specifier!r}" - ) - message = message_format.format( - package=template.name, - version=self._python_candidate.version, - specifier=str(requirement.specifier), - ) + def _report_requires_python_error(self, causes): + # type: (Sequence[ConflictCause]) -> UnsupportedPythonVersion + assert causes, "Requires-Python error reported with no cause" + + version = self._python_candidate.version + + if len(causes) == 1: + specifier = str(causes[0].requirement.specifier) + message = ( + f"Package {causes[0].parent.name!r} requires a different " + f"Python: {version} not in {specifier!r}" + ) + return UnsupportedPythonVersion(message) + + message = f"Packages require a different Python. {version} not in:" + for cause in causes: + package = cause.parent.format_for_error() + specifier = str(cause.requirement.specifier) + message += f"\n{specifier!r} (required by {package})" return UnsupportedPythonVersion(message) def _report_single_requirement_conflict(self, req, parent): @@ -427,12 +440,14 @@ def get_installation_error( # If one of the things we can't solve is "we need Python X.Y", # that is what we report. - for cause in e.causes: - if isinstance(cause.requirement, RequiresPythonRequirement): - return self._report_requires_python_error( - cause.requirement, - cause.parent, - ) + requires_python_causes = [ + cause + for cause in e.causes + if isinstance(cause.requirement, RequiresPythonRequirement) + and not cause.requirement.is_satisfied_by(self._python_candidate) + ] + if requires_python_causes: + return self._report_requires_python_error(requires_python_causes) # Otherwise, we have a set of causes which can't all be satisfied # at once. From 826234e3acf1406e855d69207b15caf17d541f6d Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sun, 28 Feb 2021 07:37:23 +0800 Subject: [PATCH 3040/3170] New style type hints --- src/pip/_internal/locations/__init__.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/locations/__init__.py b/src/pip/_internal/locations/__init__.py index 688403724bb..580da8eae38 100644 --- a/src/pip/_internal/locations/__init__.py +++ b/src/pip/_internal/locations/__init__.py @@ -32,8 +32,7 @@ logger = logging.getLogger(__name__) -def _default_base(*, user): - # type: (bool) -> str +def _default_base(*, user: bool) -> str: if user: base = sysconfig.get_config_var("userbase") else: @@ -42,8 +41,7 @@ def _default_base(*, user): return base -def _warn_if_mismatch(old, new, *, key): - # type: (pathlib.Path, pathlib.Path, str) -> bool +def _warn_if_mismatch(old: pathlib.Path, new: pathlib.Path, *, key: str) -> bool: if old == new: return False issue_url = "https://github.com/pypa/pip/issues/9617" From d1d914597c9bedef22cd30422f04ca4ea6799f3c Mon Sep 17 00:00:00 2001 From: Blazej Michalik <MrMino@users.noreply.github.com> Date: Sun, 28 Feb 2021 00:41:04 +0100 Subject: [PATCH 3041/3170] =?UTF-8?q?Refactor=20`set(...)`=20=E2=86=92=20`?= =?UTF-8?q?{...}`?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Tzu-ping Chung <uranusjr@gmail.com> --- src/pip/_internal/resolution/resolvelib/factory.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index f49288c83ef..fbf04fa4f09 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -412,7 +412,7 @@ def _report_single_requirement_conflict(self, req, parent): req_disp = f"{req} (from {parent.name})" cands = self._finder.find_all_candidates(req.project_name) - versions = [str(v) for v in sorted(set(c.version for c in cands))] + versions = [str(v) for v in sorted({c.version for c in cands})] logger.critical( "Could not find a version that satisfies the requirement %s " From 6d018bf2209cd4ade569a5333a52487c00be033e Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sun, 28 Feb 2021 07:47:17 +0800 Subject: [PATCH 3042/3170] Special-case PyPy's lib location differences --- src/pip/_internal/locations/__init__.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/pip/_internal/locations/__init__.py b/src/pip/_internal/locations/__init__.py index 580da8eae38..18bf0319f3d 100644 --- a/src/pip/_internal/locations/__init__.py +++ b/src/pip/_internal/locations/__init__.py @@ -1,5 +1,6 @@ import logging import pathlib +import sys import sysconfig from typing import List, Optional @@ -99,6 +100,22 @@ def get_scheme( # Extra join because distutils can return relative paths. old_v = pathlib.Path(base, getattr(old, k)) new_v = pathlib.Path(getattr(new, k)) + + # distutils incorrectly put PyPy packages under ``site-packages/python`` + # in the ``posix_home`` scheme, but PyPy devs said they expect the + # directory name to be ``pypy`` instead. So we treat this as a bug fix + # and not warn about it. See bpo-43307 and python/cpython#24628. + skip_pypy_special_case = ( + sys.implementation.name == "pypy" + and home is not None + and k in ("platlib", "purelib") + and old_v.parent == new_v.parent + and old_v.name == "python" + and new_v.name == "pypy" + ) + if skip_pypy_special_case: + continue + warned.append(_warn_if_mismatch(old_v, new_v, key=f"scheme.{k}")) if any(warned): From e4349aec70078b2f5d64bb0dbe74c5c4f2ed4b2a Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sun, 28 Feb 2021 20:01:28 +0800 Subject: [PATCH 3043/3170] Ignore dist-info directories with invalid name --- news/7269.bugfix.rst | 2 + src/pip/_internal/metadata/base.py | 44 ++++++++++++++++++++- src/pip/_internal/metadata/pkg_resources.py | 7 +++- tests/functional/test_freeze.py | 35 ++++++++-------- 4 files changed, 68 insertions(+), 20 deletions(-) create mode 100644 news/7269.bugfix.rst diff --git a/news/7269.bugfix.rst b/news/7269.bugfix.rst new file mode 100644 index 00000000000..46816692b0a --- /dev/null +++ b/news/7269.bugfix.rst @@ -0,0 +1,2 @@ +Ignore ``.dist-info`` directories if the stem is not a valid Python distribution +name, so they don't show up in e.g. ``pip freeze``. diff --git a/src/pip/_internal/metadata/base.py b/src/pip/_internal/metadata/base.py index 724b0c04494..100168b6ed2 100644 --- a/src/pip/_internal/metadata/base.py +++ b/src/pip/_internal/metadata/base.py @@ -1,11 +1,26 @@ +import logging +import re from typing import Container, Iterator, List, Optional from pip._vendor.packaging.version import _BaseVersion from pip._internal.utils.misc import stdlib_pkgs # TODO: Move definition here. +logger = logging.getLogger(__name__) + class BaseDistribution: + @property + def location(self): + # type: () -> Optional[str] + """Where the distribution is loaded from. + + A string value is not necessarily a filesystem path, since distributions + can be loaded from other sources, e.g. arbitrary zip archives. ``None`` + means the distribution is created in-memory. + """ + raise NotImplementedError() + @property def metadata_version(self): # type: () -> Optional[str] @@ -61,10 +76,37 @@ def get_distribution(self, name): """Given a requirement name, return the installed distributions.""" raise NotImplementedError() + def _iter_distributions(self): + # type: () -> Iterator[BaseDistribution] + """Iterate through installed distributions. + + This function should be implemented by subclass, but never called + directly. Use the public ``iter_distribution()`` instead, which + implements additional logic to make sure the distributions are valid. + """ + raise NotImplementedError() + def iter_distributions(self): # type: () -> Iterator[BaseDistribution] """Iterate through installed distributions.""" - raise NotImplementedError() + for dist in self._iter_distributions(): + # Make sure the distribution actually comes from a valid Python + # packaging distribution. Pip's AdjacentTempDirectory leaves folders + # e.g. ``~atplotlib.dist-info`` if cleanup was interrupted. The + # valid project name pattern is taken from PEP 508. + project_name_valid = re.match( + r"^([A-Z0-9]|[A-Z0-9][A-Z0-9._-]*[A-Z0-9])$", + dist.canonical_name, + flags=re.IGNORECASE, + ) + if not project_name_valid: + logger.warning( + "Ignoring invalid distribution %s (%s)", + dist.canonical_name, + dist.location, + ) + continue + yield dist def iter_installed_distributions( self, diff --git a/src/pip/_internal/metadata/pkg_resources.py b/src/pip/_internal/metadata/pkg_resources.py index d2fb29e2e9a..5cd9eaee641 100644 --- a/src/pip/_internal/metadata/pkg_resources.py +++ b/src/pip/_internal/metadata/pkg_resources.py @@ -24,6 +24,11 @@ def from_wheel(cls, path, name): dist = pkg_resources_distribution_for_wheel(zf, name, path) return cls(dist) + @property + def location(self): + # type: () -> Optional[str] + return self._dist.location + @property def metadata_version(self): # type: () -> Optional[str] @@ -115,7 +120,7 @@ def get_distribution(self, name): return None return self._search_distribution(name) - def iter_distributions(self): + def _iter_distributions(self): # type: () -> Iterator[BaseDistribution] for dist in self._ws: yield Distribution(dist) diff --git a/tests/functional/test_freeze.py b/tests/functional/test_freeze.py index 4fd91b5d8e5..858e43931db 100644 --- a/tests/functional/test_freeze.py +++ b/tests/functional/test_freeze.py @@ -5,6 +5,8 @@ from doctest import ELLIPSIS, OutputChecker import pytest +from pip._vendor.packaging.utils import canonicalize_name +from pip._vendor.pkg_resources import safe_name from tests.lib import ( _create_test_package, @@ -128,26 +130,23 @@ def fake_install(pkgname, dest): ) for pkgname in valid_pkgnames + invalid_pkgnames: fake_install(pkgname, script.site_packages_path) + result = script.pip('freeze', expect_stderr=True) - for pkgname in valid_pkgnames: - _check_output( - result.stdout, - '...{}==1.0...'.format(pkgname.replace('_', '-')) - ) - for pkgname in invalid_pkgnames: - # Check that the full distribution repr is present. - dist_repr = '{} 1.0 ('.format(pkgname.replace('_', '-')) - expected = ( - '...Could not generate requirement for ' - 'distribution {}...'.format(dist_repr) - ) - _check_output(result.stderr, expected) - # Also check that the parse error details occur at least once. - # We only need to find one occurrence to know that exception details - # are logged. - expected = '...site-packages): Parse error at "...' - _check_output(result.stderr, expected) + # Check all valid names are in the output. + output_lines = {line.strip() for line in result.stdout.splitlines()} + for name in valid_pkgnames: + assert f"{safe_name(name)}==1.0" in output_lines + + # Check all invalid names are excluded from the output. + canonical_invalid_names = {canonicalize_name(n) for n in invalid_pkgnames} + for line in output_lines: + output_name, _, _ = line.partition("=") + assert canonicalize_name(output_name) not in canonical_invalid_names + + # The invalid names should be logged. + for name in canonical_invalid_names: + assert f"Ignoring invalid distribution {name} (" in result.stderr @pytest.mark.git From 8f2e3d69782192920882bafc76c927a3887ee5b7 Mon Sep 17 00:00:00 2001 From: Stefano Rivera <stefano@rivera.za.net> Date: Tue, 2 Mar 2021 11:46:31 -0800 Subject: [PATCH 3044/3170] Parse pkg_resources Dist versions with packaging.version Due to a mix of bundled and unbundled dependencies, pkg_resources Version class may not be the same as packaging's Version class. See: https://github.com/pypa/setuptools/issues/2052 --- news/9e768673-6079-491e-bbe0-d1593952f1c7.trivial.rst | 0 src/pip/_internal/metadata/pkg_resources.py | 6 +++++- src/pip/_internal/resolution/resolvelib/candidates.py | 11 ++++++++--- src/pip/_internal/resolution/resolvelib/resolver.py | 3 ++- 4 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 news/9e768673-6079-491e-bbe0-d1593952f1c7.trivial.rst diff --git a/news/9e768673-6079-491e-bbe0-d1593952f1c7.trivial.rst b/news/9e768673-6079-491e-bbe0-d1593952f1c7.trivial.rst new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_internal/metadata/pkg_resources.py b/src/pip/_internal/metadata/pkg_resources.py index 5cd9eaee641..0196ff3476f 100644 --- a/src/pip/_internal/metadata/pkg_resources.py +++ b/src/pip/_internal/metadata/pkg_resources.py @@ -4,6 +4,7 @@ from pip._vendor import pkg_resources from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.packaging.version import _BaseVersion +from pip._vendor.packaging.version import parse as parse_version from pip._internal.utils import misc # TODO: Move definition here. from pip._internal.utils.packaging import get_installer @@ -16,6 +17,7 @@ class Distribution(BaseDistribution): def __init__(self, dist): # type: (pkg_resources.Distribution) -> None self._dist = dist + self._version = None @classmethod def from_wheel(cls, path, name): @@ -45,7 +47,9 @@ def canonical_name(self): @property def version(self): # type: () -> _BaseVersion - return self._dist.parsed_version + if self._version is None: + self._version = parse_version(self._dist.version) + return self._version @property def installer(self): diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 035e118d022..5638eb0c185 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -5,6 +5,7 @@ from pip._vendor.packaging.specifiers import InvalidSpecifier, SpecifierSet from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.packaging.version import Version, _BaseVersion +from pip._vendor.packaging.version import parse as parse_version from pip._vendor.pkg_resources import Distribution from pip._internal.exceptions import HashError, MetadataInconsistent @@ -180,7 +181,7 @@ def name(self): def version(self): # type: () -> _BaseVersion if self._version is None: - self._version = self.dist.parsed_version + self._version = parse_version(self.dist.version) return self._version def format_for_error(self): @@ -206,7 +207,8 @@ def _check_metadata_consistency(self, dist): self._name, dist.project_name, ) - if self._version is not None and self._version != dist.parsed_version: + parsed_version = parse_version(dist.version) + if self._version is not None and self._version != parsed_version: raise MetadataInconsistent( self._ireq, "version", @@ -345,6 +347,7 @@ def __init__( self.dist = dist self._ireq = make_install_req_from_dist(dist, template) self._factory = factory + self._version = None # This is just logging some messages, so we can do it eagerly. # The returned dist would be exactly the same as self.dist because we @@ -387,7 +390,9 @@ def name(self): @property def version(self): # type: () -> _BaseVersion - return self.dist.parsed_version + if self._version is None: + self._version = parse_version(self.dist.version) + return self._version @property def is_editable(self): diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index eba441091b6..7826cfc0fab 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -4,6 +4,7 @@ from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple from pip._vendor.packaging.utils import canonicalize_name +from pip._vendor.packaging.version import parse as parse_version from pip._vendor.resolvelib import ResolutionImpossible from pip._vendor.resolvelib import Resolver as RLResolver from pip._vendor.resolvelib.resolvers import Result @@ -139,7 +140,7 @@ def resolve(self, root_reqs, check_supported_wheels): elif self.factory.force_reinstall: # The --force-reinstall flag is set -- reinstall. ireq.should_reinstall = True - elif installed_dist.parsed_version != candidate.version: + elif parse_version(installed_dist.version) != candidate.version: # The installation is different in version -- reinstall. ireq.should_reinstall = True elif candidate.is_editable or dist_is_editable(installed_dist): From ba4b7f110ddf620e211be08ee1822362f9d564fc Mon Sep 17 00:00:00 2001 From: Stefano Rivera <stefano@rivera.za.net> Date: Wed, 3 Mar 2021 09:27:36 -0800 Subject: [PATCH 3045/3170] Don't cache _version --- src/pip/_internal/metadata/pkg_resources.py | 5 +---- src/pip/_internal/resolution/resolvelib/candidates.py | 5 +---- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/metadata/pkg_resources.py b/src/pip/_internal/metadata/pkg_resources.py index 0196ff3476f..81fc35c0ff0 100644 --- a/src/pip/_internal/metadata/pkg_resources.py +++ b/src/pip/_internal/metadata/pkg_resources.py @@ -17,7 +17,6 @@ class Distribution(BaseDistribution): def __init__(self, dist): # type: (pkg_resources.Distribution) -> None self._dist = dist - self._version = None @classmethod def from_wheel(cls, path, name): @@ -47,9 +46,7 @@ def canonical_name(self): @property def version(self): # type: () -> _BaseVersion - if self._version is None: - self._version = parse_version(self._dist.version) - return self._version + return parse_version(self._dist.version) @property def installer(self): diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 5638eb0c185..cb3a51b51fc 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -347,7 +347,6 @@ def __init__( self.dist = dist self._ireq = make_install_req_from_dist(dist, template) self._factory = factory - self._version = None # This is just logging some messages, so we can do it eagerly. # The returned dist would be exactly the same as self.dist because we @@ -390,9 +389,7 @@ def name(self): @property def version(self): # type: () -> _BaseVersion - if self._version is None: - self._version = parse_version(self.dist.version) - return self._version + return parse_version(self.dist.version) @property def is_editable(self): From 7a95720e796a5e56481c1cc20b6ce6249c50f357 Mon Sep 17 00:00:00 2001 From: KOLANICH <kolan_n@mail.ru> Date: Wed, 24 Feb 2021 11:50:16 +0300 Subject: [PATCH 3046/3170] Fixed --editable install for setuptools projects without setup.py. Co-Authored-By: Tzu-ping Chung <uranusjr@gmail.com> Co-Authored-By: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> --- news/9547.feature.rst | 1 + src/pip/_internal/req/constructors.py | 11 +++--- src/pip/_internal/utils/setuptools_build.py | 8 +++-- tests/functional/test_install.py | 40 +++++++++++++++------ 4 files changed, 43 insertions(+), 17 deletions(-) create mode 100644 news/9547.feature.rst diff --git a/news/9547.feature.rst b/news/9547.feature.rst new file mode 100644 index 00000000000..364a8f68817 --- /dev/null +++ b/news/9547.feature.rst @@ -0,0 +1 @@ +Add support for editable installs for project with only setup.cfg files. diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index 810fc085988..b279bccbcd2 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -77,16 +77,19 @@ def parse_editable(editable_req): 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')): + setup_py = os.path.join(url_no_extras, 'setup.py') + setup_cfg = os.path.join(url_no_extras, 'setup.cfg') + if not os.path.exists(setup_py) and not os.path.exists(setup_cfg): msg = ( - 'File "setup.py" not found. Directory cannot be installed ' - 'in editable mode: {}'.format(os.path.abspath(url_no_extras)) + 'File "setup.py" or "setup.cfg" not found. Directory cannot be ' + 'installed in editable mode: {}' + .format(os.path.abspath(url_no_extras)) ) pyproject_path = make_pyproject_path(url_no_extras) if os.path.isfile(pyproject_path): msg += ( '\n(A "pyproject.toml" file was found, but editable ' - 'mode currently requires a setup.py based build.)' + 'mode currently requires a setuptools-based build.)' ) raise InstallationError(msg) diff --git a/src/pip/_internal/utils/setuptools_build.py b/src/pip/_internal/utils/setuptools_build.py index e16deecd580..4b8e4b359f3 100644 --- a/src/pip/_internal/utils/setuptools_build.py +++ b/src/pip/_internal/utils/setuptools_build.py @@ -8,9 +8,11 @@ # invoking via the shim. This avoids e.g. the following manifest_maker # warning: "warning: manifest_maker: standard file '-c' not found". _SETUPTOOLS_SHIM = ( - "import sys, setuptools, tokenize; sys.argv[0] = {0!r}; __file__={0!r};" - "f=getattr(tokenize, 'open', open)(__file__);" - "code=f.read().replace('\\r\\n', '\\n');" + "import io, os, sys, setuptools, tokenize; sys.argv[0] = {0!r}; __file__={0!r};" + "f = getattr(tokenize, 'open', open)(__file__) " + "if os.path.exists(__file__) " + "else io.StringIO('from setuptools import setup; setup()');" + "code = f.read().replace('\\r\\n', '\\n');" "f.close();" "exec(compile(code, __file__, 'exec'))" ) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 95d9b73aa44..369c15a7619 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -641,9 +641,9 @@ def test_editable_install__local_dir_no_setup_py( msg = result.stderr if deprecated_python: - assert 'File "setup.py" not found. ' in msg + assert 'File "setup.py" or "setup.cfg" not found. ' in msg else: - assert msg.startswith('ERROR: File "setup.py" not found. ') + assert msg.startswith('ERROR: File "setup.py" or "setup.cfg" not found. ') assert 'pyproject.toml' not in msg @@ -663,9 +663,9 @@ def test_editable_install__local_dir_no_setup_py_with_pyproject( msg = result.stderr if deprecated_python: - assert 'File "setup.py" not found. ' in msg + assert 'File "setup.py" or "setup.cfg" not found. ' in msg else: - assert msg.startswith('ERROR: File "setup.py" not found. ') + assert msg.startswith('ERROR: File "setup.py" or "setup.cfg" not found. ') assert 'A "pyproject.toml" file was found' in msg @@ -1034,15 +1034,13 @@ def test_install_package_with_prefix(script, data): result.did_create(install_path) -def test_install_editable_with_prefix(script): +def _test_install_editable_with_prefix(script, files): # make a dummy project pkga_path = script.scratch_path / 'pkga' pkga_path.mkdir() - pkga_path.joinpath("setup.py").write_text(textwrap.dedent(""" - from setuptools import setup - setup(name='pkga', - version='0.1') - """)) + + for fn, contents in files.items(): + pkga_path.joinpath(fn).write_text(textwrap.dedent(contents)) if hasattr(sys, "pypy_version_info"): site_packages = os.path.join( @@ -1087,6 +1085,28 @@ def test_install_editable_with_target(script): result.did_create(script.scratch / 'target' / 'watching_testrunner.py') +def test_install_editable_with_prefix_setup_py(script): + setup_py = """ +from setuptools import setup +setup(name='pkga', version='0.1') +""" + _test_install_editable_with_prefix(script, {"setup.py": setup_py}) + + +def test_install_editable_with_prefix_setup_cfg(script): + setup_cfg = """[metadata] +name = pkga +version = 0.1 +""" + pyproject_toml = """[build-system] +requires = ["setuptools", "wheel"] +build-backend = "setuptools.build_meta" +""" + _test_install_editable_with_prefix( + script, {"setup.cfg": setup_cfg, "pyproject.toml": pyproject_toml} + ) + + def test_install_package_conflict_prefix_and_user(script, data): """ Test installing a package using pip install --prefix --user errors out From ac263f07b149b3d2c89539d6bff302a3e78df610 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Thu, 4 Feb 2021 09:10:43 +0000 Subject: [PATCH 3047/3170] Add a docs-live nox session --- noxfile.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/noxfile.py b/noxfile.py index 165a430d14b..a530b65ba20 100644 --- a/noxfile.py +++ b/noxfile.py @@ -149,6 +149,22 @@ def get_sphinx_build_command(kind): session.run(*get_sphinx_build_command("man")) +@nox.session(name="docs-live") +def docs_live(session): + # type: (nox.Session) -> None + session.install("-e", ".") + session.install("-r", REQUIREMENTS["docs"], "sphinx-autobuild") + + session.run( + "sphinx-autobuild", + "-d=docs/build/doctrees/livehtml", + "-b=dirhtml", + "docs/html", + "docs/build/livehtml", + *session.posargs, + ) + + @nox.session def lint(session): # type: (nox.Session) -> None From 1e2873a5202578cbea10830b40268b59fe43889d Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sun, 17 Jan 2021 14:55:13 +0000 Subject: [PATCH 3048/3170] Remove the docs-feedback extension --- docs/docs_feedback_sphinxext.py | 160 -------------------------------- docs/html/conf.py | 1 - 2 files changed, 161 deletions(-) delete mode 100644 docs/docs_feedback_sphinxext.py diff --git a/docs/docs_feedback_sphinxext.py b/docs/docs_feedback_sphinxext.py deleted file mode 100644 index d0ff1f03da1..00000000000 --- a/docs/docs_feedback_sphinxext.py +++ /dev/null @@ -1,160 +0,0 @@ -"""A sphinx extension for collecting per doc feedback.""" - -from __future__ import annotations - -from itertools import chain -from typing import Dict, List, Union - -from sphinx.application import Sphinx - -DEFAULT_DOC_LINES_THRESHOLD = 250 -RST_INDENT = 4 -EMAIL_INDENT = 6 - - -def _modify_rst_document_source_on_read( - app: Sphinx, - docname: str, - source: List[str], -) -> None: - """Add info block to top and bottom of each document source. - - This function modifies RST source in-place by adding an admonition - block at the top and the bottom of each document right after it's - been read from disk preserving :orphan: at top, if present. - """ - admonition_type = app.config.docs_feedback_admonition_type - big_doc_lines = app.config.docs_feedback_big_doc_lines - escaped_email = app.config.docs_feedback_email.replace(' ', r'\ ') - excluded_documents = set(app.config.docs_feedback_excluded_documents) - questions_list = app.config.docs_feedback_questions_list - - valid_admonitions = { - 'attention', 'caution', 'danger', 'error', 'hint', - 'important', 'note', 'tip', 'warning', 'admonition', - } - - if admonition_type not in valid_admonitions: - raise ValueError( - 'Expected `docs_feedback_admonition_type` to be one of ' - f'{valid_admonitions} but got {admonition_type}.' - ) - - if not questions_list: - raise ValueError( - 'Expected `docs_feedback_questions_list` to list questions ' - 'but got none.' - ) - - if docname in excluded_documents: - # NOTE: Completely ignore any document - # NOTE: listed in 'docs_feedback_excluded_documents'. - return - - is_doc_big = source[0].count('\n') >= big_doc_lines - - questions_list_rst = '\n'.join( - f'{" " * RST_INDENT}{number!s}. {question}' - for number, question in enumerate(questions_list, 1) - ) - questions_list_urlencoded = ( - '\n'.join( - f'\n{" " * RST_INDENT}{number!s}. {question} ' - for number, question in enumerate( - chain( - (f'Document: {docname}. Page URL: https://', ), - questions_list, - ), - ) - ). - rstrip('\r\n\t '). - replace('\r', '%0D'). - replace('\n', '%0A'). - replace(' ', '%20') - ) - - admonition_msg = rf""" - **Did this article help?** - - We are currently doing research to improve pip's documentation - and would love your feedback. - Please `email us`_ and let us know{{let_us_know_ending}} - -{{questions_list_rst}} - - .. _email us: - mailto:{escaped_email}\ - ?subject=[Doc:\ {docname}]\ Pip\ docs\ feedback\ \ - (URL\:\ https\://)\ - &body={questions_list_urlencoded} - """ - let_us_know_ending = ':' - - info_block_bottom = ( - f'.. {admonition_type}::\n\t\t{admonition_msg.format_map(locals())}\n' - ) - - questions_list_rst = '' - let_us_know_ending = ( - ' why you came to this page and what on it helped ' - 'you and what did not. ' - '(:issue:`Read more about this research <8517>`)' - ) - info_block_top = '' if is_doc_big else ( - f'.. {admonition_type}::\n\t\t{admonition_msg.format_map(locals())}\n' - ) - - orphan_mark = ':orphan:' - is_orphan = orphan_mark in source[0] - if is_orphan: - source[0] = source[0].replace(orphan_mark, '') - else: - orphan_mark = '' - - source[0] = '\n\n'.join(( - orphan_mark, info_block_top, source[0], info_block_bottom, - )) - - -def setup(app: Sphinx) -> Dict[str, Union[bool, str]]: - """Initialize the Sphinx extension. - - This function adds a callback for modifying the document sources - in-place on read. - - It also declares the extension settings changable via :file:`conf.py`. - """ - rebuild_trigger = 'html' # rebuild full html on settings change - app.add_config_value( - 'docs_feedback_admonition_type', - default='important', - rebuild=rebuild_trigger, - ) - app.add_config_value( - 'docs_feedback_big_doc_lines', - default=DEFAULT_DOC_LINES_THRESHOLD, - rebuild=rebuild_trigger, - ) - app.add_config_value( - 'docs_feedback_email', - default='Docs UX Team <docs-feedback@pypa.io>', - rebuild=rebuild_trigger, - ) - app.add_config_value( - 'docs_feedback_excluded_documents', - default=set(), - rebuild=rebuild_trigger, - ) - app.add_config_value( - 'docs_feedback_questions_list', - default=(), - rebuild=rebuild_trigger, - ) - - app.connect('source-read', _modify_rst_document_source_on_read) - - return { - 'parallel_read_safe': True, - 'parallel_write_safe': True, - 'version': 'builtin', - } diff --git a/docs/html/conf.py b/docs/html/conf.py index 2efb7135892..611832c1b1c 100644 --- a/docs/html/conf.py +++ b/docs/html/conf.py @@ -37,7 +37,6 @@ 'sphinx_inline_tabs', 'sphinxcontrib.towncrier', # in-tree: - 'docs_feedback_sphinxext', 'pip_sphinxext', ] From 11986d7641f6e37052cbacf4d810af971960b88b Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sun, 17 Jan 2021 14:51:49 +0000 Subject: [PATCH 3049/3170] Enable MyST in documentation --- MANIFEST.in | 2 +- docs/html/conf.py | 1 + tools/requirements/docs.txt | 1 + 3 files changed, 3 insertions(+), 1 deletion(-) diff --git a/MANIFEST.in b/MANIFEST.in index 24d4553785b..9148af0b652 100644 --- a/MANIFEST.in +++ b/MANIFEST.in @@ -22,7 +22,7 @@ exclude noxfile.py recursive-include src/pip/_vendor *.pem recursive-include src/pip/_vendor py.typed -recursive-include docs *.css *.rst *.py +recursive-include docs *.css *.py *.rst *.md exclude src/pip/_vendor/six exclude src/pip/_vendor/six/moves diff --git a/docs/html/conf.py b/docs/html/conf.py index 611832c1b1c..7b5eb5a2dbd 100644 --- a/docs/html/conf.py +++ b/docs/html/conf.py @@ -34,6 +34,7 @@ 'sphinx.ext.extlinks', 'sphinx.ext.intersphinx', # third-party: + 'myst_parser', 'sphinx_inline_tabs', 'sphinxcontrib.towncrier', # in-tree: diff --git a/tools/requirements/docs.txt b/tools/requirements/docs.txt index a5aae67c106..83eeea4e00d 100644 --- a/tools/requirements/docs.txt +++ b/tools/requirements/docs.txt @@ -1,5 +1,6 @@ sphinx == 3.2.1 furo +myst_parser sphinx-inline-tabs sphinxcontrib-towncrier From dfc509daee6ffd84f00cf384f009e70a9bfd0b15 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sun, 17 Jan 2021 17:11:54 +0000 Subject: [PATCH 3050/3170] Rewrite docs/conf.py This simplifies the Sphinx configuration and adds additional grouping and structure to the existing code as well. --- docs/html/conf.py | 358 ++++++++++------------------------------------ 1 file changed, 79 insertions(+), 279 deletions(-) diff --git a/docs/html/conf.py b/docs/html/conf.py index 7b5eb5a2dbd..7dece866d27 100644 --- a/docs/html/conf.py +++ b/docs/html/conf.py @@ -1,13 +1,4 @@ -# pip documentation build configuration file, created by -# sphinx-quickstart on Tue Apr 22 22:08:49 2008 -# -# This file is execfile()d with the current directory set to its containing dir -# -# Note that not all possible configuration values are present in this -# autogenerated file. -# -# All configuration values have a default; values that are commented out -# serve to show the default. +"""Sphinx configuration file for pip's documentation.""" import glob import os @@ -15,311 +6,120 @@ import re import sys -on_rtd = os.environ.get('READTHEDOCS', None) == 'True' - +# Add the docs/ directory to sys.path, because pip_sphinxext.py is there. docs_dir = os.path.dirname(os.path.dirname(__file__)) -# If extensions (or modules to document with autodoc) are in another directory, -# add these directories to sys.path here. If the directory is relative to the -# documentation root, use os.path.abspath to make it absolute, like shown here. sys.path.insert(0, docs_dir) -# sys.path.append(os.path.join(os.path.dirname(__file__), '../')) -# -- General configuration ---------------------------------------------------- +# -- General configuration ------------------------------------------------------------ -# Add any Sphinx extension module names here, as strings. They can be -# extensions coming with Sphinx (named 'sphinx.ext.*') or your custom ones. -# extensions = ['sphinx.ext.autodoc'] extensions = [ - # native: - 'sphinx.ext.extlinks', - 'sphinx.ext.intersphinx', - # third-party: - 'myst_parser', - 'sphinx_inline_tabs', - 'sphinxcontrib.towncrier', - # in-tree: - 'pip_sphinxext', + # first-party extensions + "sphinx.ext.extlinks", + "sphinx.ext.intersphinx", + # our extensions + "pip_sphinxext", + # third-party extensions + "myst_parser", + "sphinx_copybutton", + "sphinx_inline_tabs", + "sphinxcontrib.towncrier", ] -# intersphinx -intersphinx_cache_limit = 0 -intersphinx_mapping = { - 'pypug': ('https://packaging.python.org/', None), - 'pypa': ('https://www.pypa.io/en/latest/', None), -} - - -# Add any paths that contain templates here, relative to this directory. -templates_path = [] - -# The suffix of source filenames. -source_suffix = '.rst' - -# The encoding of source files. -# source_encoding = 'utf-8' - -# The master toctree document. -master_doc = 'index' - # General information about the project. -project = 'pip' -copyright = '2008-2020, PyPA' - -# The version info for the project you're documenting, acts as replacement for -# |version| and |release|, also used in various other places throughout the -# built documents. -# -# The short X.Y version. - -version = release = 'dev' - -# Readthedocs seems to install pip as an egg (via setup.py install) which -# is somehow resulting in "import pip" picking up an older copy of pip. -# Rather than trying to force RTD to install pip properly, we'll simply -# read the version direct from the __init__.py file. (Yes, this is -# fragile, but it works...) - -pip_init = os.path.join(docs_dir, '..', 'src', 'pip', '__init__.py') -with open(pip_init) as f: +project = "pip" +copyright = "2008-2020, PyPA" + +# Find the version and release information. +# We have a single source of truth for our version number: pip's __init__.py file. +# This next bit of code reads from it. +file_with_version = os.path.join(docs_dir, "..", "src", "pip", "__init__.py") +with open(file_with_version) as f: for line in f: m = re.match(r'__version__ = "(.*)"', line) if m: __version__ = m.group(1) # The short X.Y version. - version = '.'.join(__version__.split('.')[:2]) + version = ".".join(__version__.split(".")[:2]) # The full version, including alpha/beta/rc tags. release = __version__ break + else: # AKA no-break + version = release = "dev" -# We have this here because readthedocs plays tricks sometimes and there seems -# to be a heisenbug, related to the version of pip discovered. This is here to -# help debug that if someone decides to do that in the future. print("pip version:", version) print("pip release:", release) -# The language for content autogenerated by Sphinx. Refer to documentation -# for a list of supported languages. -# language = None - -# There are two options for replacing |today|: either, you set today to some -# non-false value, then it is used: -# today = '' -# Else, today_fmt is used as the format for a strftime call. -today_fmt = '%B %d, %Y' - -# List of documents that shouldn't be included in the build. -# unused_docs = [] - -# List of directories, relative to source directory, that shouldn't be searched -# for source files. -exclude_patterns = ['build/'] - -# The reST default role (used for this markup: `text`) to use for all documents -# default_role = None +# -- Options for smartquotes ---------------------------------------------------------- -# If true, '()' will be appended to :func: etc. cross-reference text. -# add_function_parentheses = True +# Disable the conversion of dashes so that long options like "--find-links" won't +# render as "-find-links" if included in the text.The default of "qDe" converts normal +# quote characters ('"' and "'"), en and em dashes ("--" and "---"), and ellipses "..." +smartquotes_action = "qe" -# If true, the current module name will be prepended to all description -# unit titles (such as .. function::). -# add_module_names = True +# -- Options for intersphinx ---------------------------------------------------------- -# If true, sectionauthor and moduleauthor directives will be shown in the -# output. They are ignored by default. -# show_authors = False +intersphinx_mapping = { + "python": ("https://docs.python.org/3", None), + "pypug": ("https://packaging.python.org", None), +} -# A list of ignored prefixes for module index sorting. -# modindex_common_prefix = [] +# -- Options for extlinks ------------------------------------------------------------- extlinks = { - 'issue': ('https://github.com/pypa/pip/issues/%s', '#'), - 'pull': ('https://github.com/pypa/pip/pull/%s', 'PR #'), - 'pypi': ('https://pypi.org/project/%s/', ''), + "issue": ("https://github.com/pypa/pip/issues/%s", "#"), + "pull": ("https://github.com/pypa/pip/pull/%s", "PR #"), + "pypi": ("https://pypi.org/project/%s/", ""), } -# Turn off sphinx build warnings because of sphinx tabs during man pages build -sphinx_tabs_nowarn = True - -# -- Options for HTML output -------------------------------------------------- +# -- Options for towncrier_draft extension -------------------------------------------- -# The theme to use for HTML and HTML Help pages. Major themes that come with -# Sphinx are currently 'default' and 'sphinxdoc'. -html_theme = "furo" - -# Theme options are theme-specific and customize the look and feel of a theme -# further. For a list of options available for each theme, see the -# documentation. -html_theme_options = {} +towncrier_draft_autoversion_mode = "draft" # or: 'sphinx-release', 'sphinx-version' +towncrier_draft_include_empty = True +towncrier_draft_working_directory = pathlib.Path(docs_dir).parent +# Not yet supported: towncrier_draft_config_path = 'pyproject.toml' # relative to cwd -# Add any paths that contain custom themes here, relative to this directory. +# -- Options for HTML ----------------------------------------------------------------- -# The name for this set of Sphinx documents. If None, it defaults to -# "<project> v<release> documentation". +html_theme = "furo" html_title = f"{project} documentation v{release}" -# A shorter title for the navigation bar. Default is the same as html_title. -# html_short_title = None - -# The name of an image file (relative to this directory) to place at the top -# of the sidebar. -# html_logo = '_static/piplogo.png' - -# The name of an image file (within the static path) to use as favicon of the -# docs. This file should be a Windows icon file (.ico) being 16x16 or 32x32 -# pixels large. -# html_favicon = 'favicon.png' - -# Add any paths that contain custom static files (such as style sheets) here, -# relative to this directory. They are copied after the builtin static files, -# so a file named "default.css" will overwrite the builtin "default.css". -html_static_path = [] - -# If not '', a 'Last updated on:' timestamp is inserted at every page bottom, -# using the given strftime format. -html_last_updated_fmt = '%b %d, %Y' - -# If true, the Docutils Smart Quotes transform (originally based on -# SmartyPants) will be used to convert characters like quotes and dashes -# to typographically correct entities. The default is True. -smartquotes = True - -# This string, for use with Docutils 0.14 or later, customizes the -# SmartQuotes transform. The default of "qDe" converts normal quote -# characters ('"' and "'"), en and em dashes ("--" and "---"), and -# ellipses "...". -# For now, we disable the conversion of dashes so that long options -# like "--find-links" won't render as "-find-links" if included in the -# text in places where monospaced type can't be used. For example, backticks -# can't be used inside roles like :ref:`--no-index <--no-index>` because -# of nesting. -smartquotes_action = "qe" - -# Custom sidebar templates, maps document names to template names. -html_sidebars = {} - -# Additional templates that should be rendered to pages, maps page names to -# template names. -# html_additional_pages = {} - -# If false, no module index is generated. +# Disable the generation of the various indexes html_use_modindex = False - -# If false, no index is generated. html_use_index = False -# If true, the index is split into individual pages for each letter. -# html_split_index = False - -# If true, links to the reST sources are added to the pages. -html_show_sourcelink = False - -# If true, an OpenSearch description file will be output, and all pages will -# contain a <link> tag referring to it. The value of this option must be the -# base URL from which the finished HTML is served. -# html_use_opensearch = '' - -# If nonempty, this is the file name suffix for HTML files (e.g. ".xhtml"). -# html_file_suffix = '' - -# Output file base name for HTML help builder. -htmlhelp_basename = 'pipdocs' - - -# -- Options for LaTeX output ------------------------------------------------- - -# The paper size ('letter' or 'a4'). -# latex_paper_size = 'letter' - -# The font size ('10pt', '11pt' or '12pt'). -# latex_font_size = '10pt' - -# Grouping the document tree into LaTeX files. List of tuples -# (source start file, target name, title, author, documentclass [howto/manual]) -latex_documents = [ - ( - 'index', - 'pip.tex', - 'pip Documentation', - 'pip developers', - 'manual', - ), -] - -# The name of an image file (relative to this directory) to place at the top of -# the title page. -# latex_logo = None - -# For "manual" documents, if this is true, then toplevel headings are parts, -# not chapters. -# latex_use_parts = False +# -- Options for Manual Pages --------------------------------------------------------- -# Additional stuff for the LaTeX preamble. -# latex_preamble = '' - -# Documents to append as an appendix to all manuals. -# latex_appendices = [] - -# If false, no module index is generated. -# latex_use_modindex = True - -# -- Options for Manual Pages ------------------------------------------------- # List of manual pages generated -man_pages = [ - ( - 'index', - 'pip', - 'package manager for Python packages', - 'pip developers', - 1 - ) -] - - -def to_document_name(path, base_dir): - """Convert a provided path to a Sphinx "document name". - """ - relative_path = os.path.relpath(path, base_dir) - root, _ = os.path.splitext(relative_path) - return root.replace(os.sep, '/') - - -# Here, we crawl the entire man/commands/ directory and list every file with -# appropriate name and details -man_dir = os.path.join(docs_dir, 'man') -raw_subcommands = glob.glob(os.path.join(man_dir, 'commands/*.rst')) -if not raw_subcommands: - raise FileNotFoundError( - 'The individual subcommand manpages could not be found!' - ) -for fname in raw_subcommands: - fname_base = to_document_name(fname, man_dir) - outname = 'pip-' + fname_base.split('/')[1] - description = 'description of {} command'.format( - outname.replace('-', ' ') - ) - - man_pages.append((fname_base, outname, description, 'pip developers', 1)) - -# -- Options for docs_feedback_sphinxext -------------------------------------- - -# NOTE: Must be one of 'attention', 'caution', 'danger', 'error', 'hint', -# NOTE: 'important', 'note', 'tip', 'warning' or 'admonition'. -docs_feedback_admonition_type = 'important' -docs_feedback_big_doc_lines = 50 # bigger docs will have a banner on top -docs_feedback_email = 'Docs UX Team <docs-feedback@pypa.io>' -docs_feedback_excluded_documents = { # these won't have any banners - 'news', 'reference/index', -} -docs_feedback_questions_list = ( - 'What problem were you trying to solve when you came to this page?', - 'What content was useful?', - 'What content was not useful?', -) - -# -- Options for towncrier_draft extension ----------------------------------- - -towncrier_draft_autoversion_mode = 'draft' # or: 'sphinx-release', 'sphinx-version' -towncrier_draft_include_empty = False -towncrier_draft_working_directory = pathlib.Path(docs_dir).parent -# Not yet supported: towncrier_draft_config_path = 'pyproject.toml' # relative to cwd +def determine_man_pages(): + """Determine which man pages need to be generated.""" + + def to_document_name(path, base_dir): + """Convert a provided path to a Sphinx "document name".""" + relative_path = os.path.relpath(path, base_dir) + root, _ = os.path.splitext(relative_path) + return root.replace(os.sep, "/") + + # Crawl the entire man/commands/ directory and list every file with appropriate + # name and details. + man_dir = os.path.join(docs_dir, "man") + raw_subcommands = glob.glob(os.path.join(man_dir, "commands/*.rst")) + if not raw_subcommands: + raise FileNotFoundError( + "The individual subcommand manpages could not be found!" + ) + + retval = [ + ("index", "pip", "package manager for Python packages", "pip developers", 1), + ] + for fname in raw_subcommands: + fname_base = to_document_name(fname, man_dir) + outname = "pip-" + fname_base.split("/")[1] + description = "description of {} command".format(outname.replace("-", " ")) + + retval.append((fname_base, outname, description, "pip developers", 1)) + + return retval + + +man_pages = determine_man_pages() From 1cbeb04ae7e1a82689f38baa44c7cc125a7ddfef Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sun, 17 Jan 2021 17:12:26 +0000 Subject: [PATCH 3051/3170] Add autodoc and todo plugins for Sphinx --- docs/html/conf.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/docs/html/conf.py b/docs/html/conf.py index 7dece866d27..14ea84bfba9 100644 --- a/docs/html/conf.py +++ b/docs/html/conf.py @@ -14,6 +14,8 @@ extensions = [ # first-party extensions + "sphinx.ext.autodoc", + "sphinx.ext.todo", "sphinx.ext.extlinks", "sphinx.ext.intersphinx", # our extensions From d4431996db0904d8e652a38759100afca1852502 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sun, 17 Jan 2021 19:22:25 +0000 Subject: [PATCH 3052/3170] Add sphinx-copybutton --- tools/requirements/docs.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/tools/requirements/docs.txt b/tools/requirements/docs.txt index 83eeea4e00d..aed18b5084b 100644 --- a/tools/requirements/docs.txt +++ b/tools/requirements/docs.txt @@ -1,6 +1,7 @@ sphinx == 3.2.1 furo myst_parser +sphinx-copybutton sphinx-inline-tabs sphinxcontrib-towncrier From 6b076e53d74e833776ce5740a3497dabef0ebfee Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Mon, 18 Jan 2021 22:24:30 +0000 Subject: [PATCH 3053/3170] Blacken pip_sphinxext.py --- docs/pip_sphinxext.py | 54 +++++++++++++++++++------------------------ 1 file changed, 24 insertions(+), 30 deletions(-) diff --git a/docs/pip_sphinxext.py b/docs/pip_sphinxext.py index 1ce526e0d6c..c054035f163 100644 --- a/docs/pip_sphinxext.py +++ b/docs/pip_sphinxext.py @@ -19,14 +19,12 @@ class PipCommandUsage(rst.Directive): def run(self): cmd = create_command(self.arguments[0]) - cmd_prefix = 'python -m pip' + cmd_prefix = "python -m pip" if len(self.arguments) > 1: cmd_prefix = " ".join(self.arguments[1:]) cmd_prefix = cmd_prefix.strip('"') cmd_prefix = cmd_prefix.strip("'") - usage = dedent( - cmd.usage.replace('%prog', f'{cmd_prefix} {cmd.name}') - ).strip() + usage = dedent(cmd.usage.replace("%prog", f"{cmd_prefix} {cmd.name}")).strip() node = nodes.literal_block(usage, usage) return [node] @@ -40,19 +38,18 @@ def run(self): desc = ViewList() cmd = create_command(self.arguments[0]) description = dedent(cmd.__doc__) - for line in description.split('\n'): + for line in description.split("\n"): desc.append(line, "") self.state.nested_parse(desc, 0, node) return [node] class PipOptions(rst.Directive): - def _format_option(self, option, cmd_name=None): bookmark_line = ( f".. _`{cmd_name}_{option._long_opts[0]}`:" - if cmd_name else - f".. _`{option._long_opts[0]}`:" + if cmd_name + else f".. _`{option._long_opts[0]}`:" ) line = ".. option:: " if option._short_opts: @@ -65,7 +62,7 @@ def _format_option(self, option, cmd_name=None): metavar = option.metavar or option.dest.lower() line += f" <{metavar.lower()}>" # fix defaults - opt_help = option.help.replace('%default', str(option.default)) + opt_help = option.help.replace("%default", str(option.default)) # fix paths with sys.prefix opt_help = opt_help.replace(sys.prefix, "<sys.prefix>") return [bookmark_line, "", line, "", " " + opt_help, ""] @@ -88,9 +85,7 @@ def run(self): class PipGeneralOptions(PipOptions): def process_options(self): - self._format_options( - [o() for o in cmdoptions.general_group['options']] - ) + self._format_options([o() for o in cmdoptions.general_group["options"]]) class PipIndexOptions(PipOptions): @@ -99,7 +94,7 @@ class PipIndexOptions(PipOptions): def process_options(self): cmd_name = self.arguments[0] self._format_options( - [o() for o in cmdoptions.index_group['options']], + [o() for o in cmdoptions.index_group["options"]], cmd_name=cmd_name, ) @@ -116,49 +111,48 @@ def process_options(self): class PipReqFileOptionsReference(PipOptions): - def determine_opt_prefix(self, opt_name): for command in commands_dict: cmd = create_command(command) if cmd.cmd_opts.has_option(opt_name): return command - raise KeyError(f'Could not identify prefix of opt {opt_name}') + raise KeyError(f"Could not identify prefix of opt {opt_name}") def process_options(self): for option in SUPPORTED_OPTIONS: - if getattr(option, 'deprecated', False): + if getattr(option, "deprecated", False): continue opt = option() opt_name = opt._long_opts[0] if opt._short_opts: - short_opt_name = '{}, '.format(opt._short_opts[0]) + short_opt_name = "{}, ".format(opt._short_opts[0]) else: - short_opt_name = '' + short_opt_name = "" - if option in cmdoptions.general_group['options']: - prefix = '' + if option in cmdoptions.general_group["options"]: + prefix = "" else: - prefix = '{}_'.format(self.determine_opt_prefix(opt_name)) + prefix = "{}_".format(self.determine_opt_prefix(opt_name)) self.view_list.append( - '* :ref:`{short}{long}<{prefix}{opt_name}>`'.format( + "* :ref:`{short}{long}<{prefix}{opt_name}>`".format( short=short_opt_name, long=opt_name, prefix=prefix, - opt_name=opt_name + opt_name=opt_name, ), - "\n" + "\n", ) def setup(app): - app.add_directive('pip-command-usage', PipCommandUsage) - app.add_directive('pip-command-description', PipCommandDescription) - app.add_directive('pip-command-options', PipCommandOptions) - app.add_directive('pip-general-options', PipGeneralOptions) - app.add_directive('pip-index-options', PipIndexOptions) + app.add_directive("pip-command-usage", PipCommandUsage) + app.add_directive("pip-command-description", PipCommandDescription) + app.add_directive("pip-command-options", PipCommandOptions) + app.add_directive("pip-general-options", PipGeneralOptions) + app.add_directive("pip-index-options", PipIndexOptions) app.add_directive( - 'pip-requirements-file-options-ref-list', PipReqFileOptionsReference + "pip-requirements-file-options-ref-list", PipReqFileOptionsReference ) From 714b6f13af82873066ab7db2323b0d316edfb038 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Mon, 18 Jan 2021 22:24:40 +0000 Subject: [PATCH 3054/3170] Add a pip-cli directive for MyST --- docs/pip_sphinxext.py | 82 ++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 81 insertions(+), 1 deletion(-) diff --git a/docs/pip_sphinxext.py b/docs/pip_sphinxext.py index c054035f163..8e5cc7a2cc5 100644 --- a/docs/pip_sphinxext.py +++ b/docs/pip_sphinxext.py @@ -1,12 +1,13 @@ """pip sphinx extensions""" import optparse +import re import sys from textwrap import dedent from docutils import nodes from docutils.parsers import rst -from docutils.statemachine import ViewList +from docutils.statemachine import StringList, ViewList from pip._internal.cli import cmdoptions from pip._internal.commands import commands_dict, create_command @@ -147,6 +148,84 @@ def process_options(self): ) +class PipCLIDirective(rst.Directive): + """ + - Only works when used in a MyST document. + - Requires sphinx-inline-tabs' tab directive. + """ + + has_content = True + optional_arguments = 1 + + def run(self): + node = nodes.paragraph() + node.document = self.state.document + + os_variants = { + "Linux": { + "highlighter": "console", + "executable": "python", + "prompt": "$", + }, + "MacOS": { + "highlighter": "console", + "executable": "python", + "prompt": "$", + }, + "Windows": { + "highlighter": "doscon", + "executable": "py", + "prompt": "C:>", + }, + } + + if self.arguments: + assert self.arguments == ["in-a-venv"] + in_virtual_environment = True + else: + in_virtual_environment = False + + lines = [] + # Create a tab for each OS + for os, variant in os_variants.items(): + + # Unpack the values + prompt = variant["prompt"] + highlighter = variant["highlighter"] + if in_virtual_environment: + executable = "python" + pip_spelling = "pip" + else: + executable = variant["executable"] + pip_spelling = f"{executable} -m pip" + + # Substitute the various "prompts" into the correct variants + substitution_pipeline = [ + ( + r"(^|(?<=\n))\$ python", + f"{prompt} {executable}", + ), + ( + r"(^|(?<=\n))\$ pip", + f"{prompt} {pip_spelling}", + ), + ] + content = self.block_text + for pattern, substitution in substitution_pipeline: + content = re.sub(pattern, substitution, content) + + # Write the tab + lines.append(f"````{{tab}} {os}") + lines.append(f"```{highlighter}") + lines.append(f"{content}") + lines.append("```") + lines.append("````") + + string_list = StringList(lines) + self.state.nested_parse(string_list, 0, node) + return [node] + + def setup(app): app.add_directive("pip-command-usage", PipCommandUsage) app.add_directive("pip-command-description", PipCommandDescription) @@ -156,3 +235,4 @@ def setup(app): app.add_directive( "pip-requirements-file-options-ref-list", PipReqFileOptionsReference ) + app.add_directive("pip-cli", PipCLIDirective) From b5d4b27aefc4d76edbb5a771c3d482956a538ac3 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Mon, 18 Jan 2021 22:26:59 +0000 Subject: [PATCH 3055/3170] Enable black on docs/ --- .pre-commit-config.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0afa776e702..0521f261f7b 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -22,7 +22,6 @@ repos: - id: black exclude: | (?x) - ^docs/| ^src/pip/_internal/commands| ^src/pip/_internal/index| ^src/pip/_internal/models| From c6933d5c5df880f883ddd77d8a9a11fbfa15fa55 Mon Sep 17 00:00:00 2001 From: Winson Luk <winson.luk@gmail.com> Date: Sat, 6 Mar 2021 07:59:39 -0500 Subject: [PATCH 3056/3170] Add a warning when run as root (e.g., sudo pip) (#9394) --- news/6409.bugfix.rst | 1 + src/pip/_internal/cli/base_command.py | 4 ++-- src/pip/_internal/cli/req_command.py | 31 +++++++++++++++++++++++++ src/pip/_internal/commands/install.py | 7 +++++- src/pip/_internal/commands/uninstall.py | 3 ++- 5 files changed, 42 insertions(+), 4 deletions(-) create mode 100644 news/6409.bugfix.rst diff --git a/news/6409.bugfix.rst b/news/6409.bugfix.rst new file mode 100644 index 00000000000..e906c15fac6 --- /dev/null +++ b/news/6409.bugfix.rst @@ -0,0 +1 @@ +Add a warning, discouraging the usage of pip as root, outside a virtual environment. diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index 6d5798a0bdc..2dc9845c2ab 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -149,8 +149,8 @@ def _main(self, args): "The directory '%s' or its parent directory is not owned " "or is not writable by the current user. The cache " "has been disabled. Check the permissions and owner of " - "that directory. If executing pip with sudo, you may want " - "sudo's -H flag.", + "that directory. If executing pip with sudo, you should " + "use sudo's -H flag.", options.cache_dir, ) options.cache_dir = None diff --git a/src/pip/_internal/cli/req_command.py b/src/pip/_internal/cli/req_command.py index a55dd7516d8..3fc00d4f47b 100644 --- a/src/pip/_internal/cli/req_command.py +++ b/src/pip/_internal/cli/req_command.py @@ -7,6 +7,7 @@ import logging import os +import sys from functools import partial from optparse import Values from typing import Any, List, Optional, Tuple @@ -38,6 +39,7 @@ TempDirectoryTypeRegistry, tempdir_kinds, ) +from pip._internal.utils.virtualenv import running_under_virtualenv logger = logging.getLogger(__name__) @@ -152,6 +154,35 @@ def handle_pip_version_check(self, options): ] +def warn_if_run_as_root(): + # type: () -> None + """Output a warning for sudo users on Unix. + + In a virtual environment, sudo pip still writes to virtualenv. + On Windows, users may run pip as Administrator without issues. + This warning only applies to Unix root users outside of virtualenv. + """ + if running_under_virtualenv(): + return + if not hasattr(os, "getuid"): + return + # On Windows, there are no "system managed" Python packages. Installing as + # Administrator via pip is the correct way of updating system environments. + # + # We choose sys.platform over utils.compat.WINDOWS here to enable Mypy platform + # checks: https://mypy.readthedocs.io/en/stable/common_issues.html + if sys.platform == "win32" or sys.platform == "cygwin": + return + if sys.platform == "darwin" or sys.platform == "linux": + if os.getuid() != 0: + return + logger.warning( + "Running pip as root will break packages and permissions. " + "You should install packages reliably by using venv: " + "https://pip.pypa.io/warnings/venv" + ) + + def with_cleanup(func): # type: (Any) -> Any """Decorator for common logic related to managing temporary diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index dc743ee0b50..c4273eda9cf 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -12,7 +12,11 @@ from pip._internal.cache import WheelCache from pip._internal.cli import cmdoptions from pip._internal.cli.cmdoptions import make_target_python -from pip._internal.cli.req_command import RequirementCommand, with_cleanup +from pip._internal.cli.req_command import ( + RequirementCommand, + warn_if_run_as_root, + with_cleanup, +) from pip._internal.cli.status_codes import ERROR, SUCCESS from pip._internal.exceptions import CommandError, InstallationError from pip._internal.locations import get_scheme @@ -443,6 +447,7 @@ def run(self, options, args): options.target_dir, target_temp_dir, options.upgrade ) + warn_if_run_as_root() return SUCCESS def _handle_target_dir(self, target_dir, target_temp_dir, upgrade): diff --git a/src/pip/_internal/commands/uninstall.py b/src/pip/_internal/commands/uninstall.py index da56f4f5582..9a3c9f8815c 100644 --- a/src/pip/_internal/commands/uninstall.py +++ b/src/pip/_internal/commands/uninstall.py @@ -4,7 +4,7 @@ from pip._vendor.packaging.utils import canonicalize_name from pip._internal.cli.base_command import Command -from pip._internal.cli.req_command import SessionCommandMixin +from pip._internal.cli.req_command import SessionCommandMixin, warn_if_run_as_root from pip._internal.cli.status_codes import SUCCESS from pip._internal.exceptions import InstallationError from pip._internal.req import parse_requirements @@ -88,4 +88,5 @@ def run(self, options, args): if uninstall_pathset: uninstall_pathset.commit() + warn_if_run_as_root() return SUCCESS From 96615e92c4b265011295ec514feb91ce405c0602 Mon Sep 17 00:00:00 2001 From: Jon Dufresne <jon.dufresne@gmail.com> Date: Sun, 28 Feb 2021 08:32:22 -0800 Subject: [PATCH 3057/3170] Complete typing of docs directory --- .pre-commit-config.yaml | 2 +- docs/html/conf.py | 5 +++-- docs/pip_sphinxext.py | 38 ++++++++++++++++++++++++-------------- 3 files changed, 28 insertions(+), 17 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0521f261f7b..3661edb15d1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -66,7 +66,7 @@ repos: rev: v0.800 hooks: - id: mypy - exclude: docs|tests + exclude: tests args: ["--pretty"] additional_dependencies: ['nox==2020.12.31'] diff --git a/docs/html/conf.py b/docs/html/conf.py index 14ea84bfba9..2a4387a352a 100644 --- a/docs/html/conf.py +++ b/docs/html/conf.py @@ -5,6 +5,7 @@ import pathlib import re import sys +from typing import List, Tuple # Add the docs/ directory to sys.path, because pip_sphinxext.py is there. docs_dir = os.path.dirname(os.path.dirname(__file__)) @@ -93,10 +94,10 @@ # List of manual pages generated -def determine_man_pages(): +def determine_man_pages() -> List[Tuple[str, str, str, str, int]]: """Determine which man pages need to be generated.""" - def to_document_name(path, base_dir): + def to_document_name(path: str, base_dir: str) -> str: """Convert a provided path to a Sphinx "document name".""" relative_path = os.path.relpath(path, base_dir) root, _ = os.path.splitext(relative_path) diff --git a/docs/pip_sphinxext.py b/docs/pip_sphinxext.py index 8e5cc7a2cc5..b6321686f93 100644 --- a/docs/pip_sphinxext.py +++ b/docs/pip_sphinxext.py @@ -4,10 +4,12 @@ import re import sys from textwrap import dedent +from typing import Iterable, List, Optional from docutils import nodes from docutils.parsers import rst from docutils.statemachine import StringList, ViewList +from sphinx.application import Sphinx from pip._internal.cli import cmdoptions from pip._internal.commands import commands_dict, create_command @@ -18,7 +20,7 @@ class PipCommandUsage(rst.Directive): required_arguments = 1 optional_arguments = 3 - def run(self): + def run(self) -> List[nodes.Node]: cmd = create_command(self.arguments[0]) cmd_prefix = "python -m pip" if len(self.arguments) > 1: @@ -33,11 +35,12 @@ def run(self): class PipCommandDescription(rst.Directive): required_arguments = 1 - def run(self): + def run(self) -> List[nodes.Node]: node = nodes.paragraph() node.document = self.state.document desc = ViewList() cmd = create_command(self.arguments[0]) + assert cmd.__doc__ is not None description = dedent(cmd.__doc__) for line in description.split("\n"): desc.append(line, "") @@ -46,7 +49,9 @@ def run(self): class PipOptions(rst.Directive): - def _format_option(self, option, cmd_name=None): + def _format_option( + self, option: optparse.Option, cmd_name: Optional[str] = None + ) -> List[str]: bookmark_line = ( f".. _`{cmd_name}_{option._long_opts[0]}`:" if cmd_name @@ -60,22 +65,27 @@ def _format_option(self, option, cmd_name=None): elif option._long_opts: line += option._long_opts[0] if option.takes_value(): - metavar = option.metavar or option.dest.lower() + metavar = option.metavar or option.dest + assert metavar is not None line += f" <{metavar.lower()}>" # fix defaults - opt_help = option.help.replace("%default", str(option.default)) + assert option.help is not None + # https://github.com/python/typeshed/pull/5080 + opt_help = option.help.replace("%default", str(option.default)) # type: ignore # fix paths with sys.prefix opt_help = opt_help.replace(sys.prefix, "<sys.prefix>") return [bookmark_line, "", line, "", " " + opt_help, ""] - def _format_options(self, options, cmd_name=None): + def _format_options( + self, options: Iterable[optparse.Option], cmd_name: Optional[str] = None + ) -> None: for option in options: if option.help == optparse.SUPPRESS_HELP: continue for line in self._format_option(option, cmd_name): self.view_list.append(line, "") - def run(self): + def run(self) -> List[nodes.Node]: node = nodes.paragraph() node.document = self.state.document self.view_list = ViewList() @@ -85,14 +95,14 @@ def run(self): class PipGeneralOptions(PipOptions): - def process_options(self): + def process_options(self) -> None: self._format_options([o() for o in cmdoptions.general_group["options"]]) class PipIndexOptions(PipOptions): required_arguments = 1 - def process_options(self): + def process_options(self) -> None: cmd_name = self.arguments[0] self._format_options( [o() for o in cmdoptions.index_group["options"]], @@ -103,7 +113,7 @@ def process_options(self): class PipCommandOptions(PipOptions): required_arguments = 1 - def process_options(self): + def process_options(self) -> None: cmd = create_command(self.arguments[0]) self._format_options( cmd.parser.option_groups[0].option_list, @@ -112,7 +122,7 @@ def process_options(self): class PipReqFileOptionsReference(PipOptions): - def determine_opt_prefix(self, opt_name): + def determine_opt_prefix(self, opt_name: str) -> str: for command in commands_dict: cmd = create_command(command) if cmd.cmd_opts.has_option(opt_name): @@ -120,7 +130,7 @@ def determine_opt_prefix(self, opt_name): raise KeyError(f"Could not identify prefix of opt {opt_name}") - def process_options(self): + def process_options(self) -> None: for option in SUPPORTED_OPTIONS: if getattr(option, "deprecated", False): continue @@ -157,7 +167,7 @@ class PipCLIDirective(rst.Directive): has_content = True optional_arguments = 1 - def run(self): + def run(self) -> List[nodes.Node]: node = nodes.paragraph() node.document = self.state.document @@ -226,7 +236,7 @@ def run(self): return [node] -def setup(app): +def setup(app: Sphinx) -> None: app.add_directive("pip-command-usage", PipCommandUsage) app.add_directive("pip-command-description", PipCommandDescription) app.add_directive("pip-command-options", PipCommandOptions) From 4807de8e73bcd8052506ac0eff870d9575175b6d Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 10 Mar 2021 11:53:19 +0800 Subject: [PATCH 3058/3170] Set follow_imports = silent --- setup.cfg | 18 ++++++++++++++++-- 1 file changed, 16 insertions(+), 2 deletions(-) diff --git a/setup.cfg b/setup.cfg index 1d851d94929..f78cfece7d3 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,15 +31,29 @@ per-file-ignores = tests/*: B011 [mypy] +follow_imports = silent ignore_missing_imports = True disallow_untyped_defs = True disallow_any_generics = True warn_unused_ignores = True -[mypy-pip/_vendor/*] -follow_imports = skip +[mypy-pip._vendor.*] ignore_errors = True +# These vendored libraries use runtime magic to populate things and don't sit +# well with static typing out of the box. Eventually we should provide correct +# typing information for their public interface and remove these configs. +[mypy-pip._vendor.colorama] +follow_imports = skip +[mypy-pip._vendor.pkg_resources] +follow_imports = skip +[mypy-pip._vendor.progress.*] +follow_imports = skip +[mypy-pip._vendor.requests.*] +follow_imports = skip +[mypy-pip._vendor.retrying] +follow_imports = skip + [tool:pytest] addopts = --ignore src/pip/_vendor --ignore tests/tests_cache -r aR markers = From 56a8f3d8bd43d88c5936715d87c1284505a312e8 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 10 Mar 2021 13:06:35 +0800 Subject: [PATCH 3059/3170] Typing fixes --- src/pip/_internal/commands/debug.py | 4 +- src/pip/_internal/commands/install.py | 9 +++-- src/pip/_internal/metadata/base.py | 8 ++-- src/pip/_internal/metadata/pkg_resources.py | 5 +-- src/pip/_internal/models/candidate.py | 3 +- src/pip/_internal/network/session.py | 2 +- src/pip/_internal/operations/check.py | 37 +++++++++--------- src/pip/_internal/req/constructors.py | 8 +++- src/pip/_internal/req/req_install.py | 2 +- src/pip/_internal/req/req_set.py | 6 ++- .../_internal/resolution/resolvelib/base.py | 13 ++++--- .../resolution/resolvelib/candidates.py | 38 +++++++++---------- .../resolution/resolvelib/factory.py | 13 ++++--- .../resolution/resolvelib/requirements.py | 21 +++++----- .../resolution/resolvelib/resolver.py | 13 ++++--- src/pip/_internal/wheel_builder.py | 10 +++-- 16 files changed, 105 insertions(+), 87 deletions(-) diff --git a/src/pip/_internal/commands/debug.py b/src/pip/_internal/commands/debug.py index 480c0444dd2..ead5119a27a 100644 --- a/src/pip/_internal/commands/debug.py +++ b/src/pip/_internal/commands/debug.py @@ -4,7 +4,7 @@ import sys from optparse import Values from types import ModuleType -from typing import Dict, List, Optional +from typing import Any, Dict, List, Optional import pip._vendor from pip._vendor.certifi import where @@ -24,7 +24,7 @@ def show_value(name, value): - # type: (str, Optional[str]) -> None + # type: (str, Any) -> None logger.info('%s: %s', name, value) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index c4273eda9cf..cd7d1ff90e3 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -5,7 +5,7 @@ import shutil import site from optparse import SUPPRESS_HELP, Values -from typing import Iterable, List, Optional +from typing import TYPE_CHECKING, Iterable, List, Optional from pip._vendor.packaging.utils import canonicalize_name @@ -22,7 +22,7 @@ from pip._internal.locations import get_scheme from pip._internal.metadata import get_environment from pip._internal.models.format_control import FormatControl -from pip._internal.operations.check import ConflictDetails, check_install_conflicts +from pip._internal.operations.check import check_install_conflicts from pip._internal.req import install_given_reqs from pip._internal.req.req_install import InstallRequirement from pip._internal.req.req_tracker import get_requirement_tracker @@ -42,6 +42,9 @@ should_build_for_install_command, ) +if TYPE_CHECKING: + from pip._internal.operations.check import ConflictDetails + logger = logging.getLogger(__name__) @@ -49,7 +52,7 @@ def get_check_binary_allowed(format_control): # type: (FormatControl) -> BinaryAllowedPredicate def check_binary_allowed(req): # type: (InstallRequirement) -> bool - canonical_name = canonicalize_name(req.name) + canonical_name = canonicalize_name(req.name or "") allowed_formats = format_control.get_allowed_formats(canonical_name) return "binary" in allowed_formats diff --git a/src/pip/_internal/metadata/base.py b/src/pip/_internal/metadata/base.py index 100168b6ed2..37f9a8232f6 100644 --- a/src/pip/_internal/metadata/base.py +++ b/src/pip/_internal/metadata/base.py @@ -1,11 +1,13 @@ import logging import re -from typing import Container, Iterator, List, Optional +from typing import Container, Iterator, List, Optional, Union -from pip._vendor.packaging.version import _BaseVersion +from pip._vendor.packaging.version import LegacyVersion, Version from pip._internal.utils.misc import stdlib_pkgs # TODO: Move definition here. +DistributionVersion = Union[LegacyVersion, Version] + logger = logging.getLogger(__name__) @@ -34,7 +36,7 @@ def canonical_name(self): @property def version(self): - # type: () -> _BaseVersion + # type: () -> DistributionVersion raise NotImplementedError() @property diff --git a/src/pip/_internal/metadata/pkg_resources.py b/src/pip/_internal/metadata/pkg_resources.py index 81fc35c0ff0..f39a39ebebf 100644 --- a/src/pip/_internal/metadata/pkg_resources.py +++ b/src/pip/_internal/metadata/pkg_resources.py @@ -3,14 +3,13 @@ from pip._vendor import pkg_resources from pip._vendor.packaging.utils import canonicalize_name -from pip._vendor.packaging.version import _BaseVersion from pip._vendor.packaging.version import parse as parse_version from pip._internal.utils import misc # TODO: Move definition here. from pip._internal.utils.packaging import get_installer from pip._internal.utils.wheel import pkg_resources_distribution_for_wheel -from .base import BaseDistribution, BaseEnvironment +from .base import BaseDistribution, BaseEnvironment, DistributionVersion class Distribution(BaseDistribution): @@ -45,7 +44,7 @@ def canonical_name(self): @property def version(self): - # type: () -> _BaseVersion + # type: () -> DistributionVersion return parse_version(self._dist.version) @property diff --git a/src/pip/_internal/models/candidate.py b/src/pip/_internal/models/candidate.py index 10a144620ee..3b91704a21c 100644 --- a/src/pip/_internal/models/candidate.py +++ b/src/pip/_internal/models/candidate.py @@ -1,4 +1,3 @@ -from pip._vendor.packaging.version import _BaseVersion from pip._vendor.packaging.version import parse as parse_version from pip._internal.models.link import Link @@ -14,7 +13,7 @@ class InstallationCandidate(KeyBasedCompareMixin): def __init__(self, name, version, link): # type: (str, str, Link) -> None self.name = name - self.version = parse_version(version) # type: _BaseVersion + self.version = parse_version(version) self.link = link super().__init__( diff --git a/src/pip/_internal/network/session.py b/src/pip/_internal/network/session.py index b42d06bc3d3..4af800f12fe 100644 --- a/src/pip/_internal/network/session.py +++ b/src/pip/_internal/network/session.py @@ -295,7 +295,7 @@ def __init__( # Add a small amount of back off between failed requests in # order to prevent hammering the service. backoff_factor=0.25, - ) + ) # type: ignore # Our Insecure HTTPAdapter disables HTTPS validation. It does not # support caching so we'll use it for all http:// URLs. diff --git a/src/pip/_internal/operations/check.py b/src/pip/_internal/operations/check.py index 224633561aa..03fd75c9d9d 100644 --- a/src/pip/_internal/operations/check.py +++ b/src/pip/_internal/operations/check.py @@ -3,7 +3,7 @@ import logging from collections import namedtuple -from typing import Any, Callable, Dict, List, Optional, Set, Tuple +from typing import TYPE_CHECKING, Any, Callable, Dict, List, Optional, Set, Tuple from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.pkg_resources import RequirementParseError @@ -12,23 +12,26 @@ from pip._internal.req.req_install import InstallRequirement from pip._internal.utils.misc import get_installed_distributions -logger = logging.getLogger(__name__) +if TYPE_CHECKING: + from pip._vendor.packaging.utils import NormalizedName + + # Shorthands + PackageSet = Dict[NormalizedName, 'PackageDetails'] + Missing = Tuple[str, Any] + Conflicting = Tuple[str, str, Any] + + MissingDict = Dict[NormalizedName, List[Missing]] + ConflictingDict = Dict[NormalizedName, List[Conflicting]] + CheckResult = Tuple[MissingDict, ConflictingDict] + ConflictDetails = Tuple[PackageSet, CheckResult] -# 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] -ConflictDetails = Tuple[PackageSet, CheckResult] +logger = logging.getLogger(__name__) PackageDetails = namedtuple('PackageDetails', ['version', 'requires']) -def create_package_set_from_installed(**kwargs): - # type: (**Any) -> Tuple[PackageSet, bool] +def create_package_set_from_installed(**kwargs: Any) -> Tuple["PackageSet", bool]: """Converts a list of distributions into a PackageSet. """ # Default to using all packages installed on the system @@ -59,7 +62,7 @@ def check_package_set(package_set, should_ignore=None): missing = {} conflicting = {} - for package_name in package_set: + for package_name, package_detail in package_set.items(): # Info about dependencies of package_name missing_deps = set() # type: Set[Missing] conflicting_deps = set() # type: Set[Conflicting] @@ -67,8 +70,8 @@ def check_package_set(package_set, should_ignore=None): if should_ignore and should_ignore(package_name): continue - for req in package_set[package_name].requires: - name = canonicalize_name(req.project_name) # type: str + for req in package_detail.requires: + name = canonicalize_name(req.project_name) # Check if it's missing if name not in package_set: @@ -114,7 +117,7 @@ def check_install_conflicts(to_install): def _simulate_installation_of(to_install, package_set): - # type: (List[InstallRequirement], PackageSet) -> Set[str] + # type: (List[InstallRequirement], PackageSet) -> Set[NormalizedName] """Computes the version of packages after installing to_install. """ @@ -136,7 +139,7 @@ def _simulate_installation_of(to_install, package_set): def _create_whitelist(would_be_installed, package_set): - # type: (Set[str], PackageSet) -> Set[str] + # type: (Set[NormalizedName], PackageSet) -> Set[NormalizedName] packages_affected = set(would_be_installed) for package_name in package_set: diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index b279bccbcd2..a659b4d6eab 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -182,7 +182,7 @@ def parse_req_from_editable(editable_req): if name is not None: try: - req = Requirement(name) + req = Requirement(name) # type: Optional[Requirement] except InvalidRequirement: raise InstallationError(f"Invalid requirement: '{name}'") else: @@ -335,7 +335,7 @@ def with_source(text): return text return f'{text} (from {line_source})' - if req_as_string is not None: + def _parse_req_string(req_as_string: str) -> Requirement: try: req = Requirement(req_as_string) except InvalidRequirement: @@ -363,6 +363,10 @@ def with_source(text): if spec_str.endswith(']'): msg = f"Extras after version '{spec_str}'." raise InstallationError(msg) + return req + + if req_as_string is not None: + req = _parse_req_string(req_as_string) # type: Optional[Requirement] else: req = None diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 9419eec496c..7f4e974cca4 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -349,7 +349,7 @@ def ensure_build_location(self, build_dir, autodelete, parallel_builds): # When parallel builds are enabled, add a UUID to the build directory # name so multiple builds do not interfere with each other. - dir_name = canonicalize_name(self.name) + dir_name = canonicalize_name(self.name) # type: str if parallel_builds: dir_name = f"{dir_name}_{uuid.uuid4().hex}" diff --git a/src/pip/_internal/req/req_set.py b/src/pip/_internal/req/req_set.py index 7ff137b73ee..59c58435574 100644 --- a/src/pip/_internal/req/req_set.py +++ b/src/pip/_internal/req/req_set.py @@ -28,7 +28,7 @@ def __str__(self): # type: () -> str requirements = sorted( (req for req in self.requirements.values() if not req.comes_from), - key=lambda req: canonicalize_name(req.name), + key=lambda req: canonicalize_name(req.name or ""), ) return ' '.join(str(req.req) for req in requirements) @@ -36,7 +36,7 @@ def __repr__(self): # type: () -> str requirements = sorted( self.requirements.values(), - key=lambda req: canonicalize_name(req.name), + key=lambda req: canonicalize_name(req.name or ""), ) format_string = '<{classname} object; {count} requirement(s): {reqs}>' @@ -122,6 +122,8 @@ def add_requirement( existing_req and not existing_req.constraint and existing_req.extras == install_req.extras and + existing_req.req and + install_req.req and existing_req.req.specifier != install_req.req.specifier ) if has_conflicting_requirement: diff --git a/src/pip/_internal/resolution/resolvelib/base.py b/src/pip/_internal/resolution/resolvelib/base.py index 81fee9b9e3e..0295b0ed8d2 100644 --- a/src/pip/_internal/resolution/resolvelib/base.py +++ b/src/pip/_internal/resolution/resolvelib/base.py @@ -1,14 +1,15 @@ -from typing import FrozenSet, Iterable, Optional, Tuple +from typing import FrozenSet, Iterable, Optional, Tuple, Union from pip._vendor.packaging.specifiers import SpecifierSet -from pip._vendor.packaging.utils import canonicalize_name -from pip._vendor.packaging.version import _BaseVersion +from pip._vendor.packaging.utils import NormalizedName, canonicalize_name +from pip._vendor.packaging.version import LegacyVersion, Version from pip._internal.models.link import Link from pip._internal.req.req_install import InstallRequirement from pip._internal.utils.hashes import Hashes CandidateLookup = Tuple[Optional["Candidate"], Optional[InstallRequirement]] +CandidateVersion = Union[LegacyVersion, Version] def format_name(project, extras): @@ -62,7 +63,7 @@ def is_satisfied_by(self, candidate): class Requirement: @property def project_name(self): - # type: () -> str + # type: () -> NormalizedName """The "project name" of a requirement. This is different from ``name`` if this requirement contains extras, @@ -97,7 +98,7 @@ def format_for_error(self): class Candidate: @property def project_name(self): - # type: () -> str + # type: () -> NormalizedName """The "project name" of the candidate. This is different from ``name`` if this candidate contains extras, @@ -118,7 +119,7 @@ def name(self): @property def version(self): - # type: () -> _BaseVersion + # type: () -> CandidateVersion raise NotImplementedError("Override in subclass") @property diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index cb3a51b51fc..184884cbd3d 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -1,10 +1,10 @@ import logging import sys -from typing import TYPE_CHECKING, Any, FrozenSet, Iterable, Optional, Tuple, Union +from typing import TYPE_CHECKING, Any, FrozenSet, Iterable, Optional, Tuple, Union, cast from pip._vendor.packaging.specifiers import InvalidSpecifier, SpecifierSet -from pip._vendor.packaging.utils import canonicalize_name -from pip._vendor.packaging.version import Version, _BaseVersion +from pip._vendor.packaging.utils import NormalizedName, canonicalize_name +from pip._vendor.packaging.version import Version from pip._vendor.packaging.version import parse as parse_version from pip._vendor.pkg_resources import Distribution @@ -19,7 +19,7 @@ from pip._internal.utils.misc import dist_is_editable, normalize_version_info from pip._internal.utils.packaging import get_requires_python -from .base import Candidate, Requirement, format_name +from .base import Candidate, CandidateVersion, Requirement, format_name if TYPE_CHECKING: from .factory import Factory @@ -126,8 +126,8 @@ def __init__( source_link, # type: Link ireq, # type: InstallRequirement factory, # type: Factory - name=None, # type: Optional[str] - version=None, # type: Optional[_BaseVersion] + name=None, # type: Optional[NormalizedName] + version=None, # type: Optional[CandidateVersion] ): # type: (...) -> None self._link = link @@ -166,7 +166,7 @@ def source_link(self): @property def project_name(self): - # type: () -> str + # type: () -> NormalizedName """The normalised name of the project the candidate refers to""" if self._name is None: self._name = canonicalize_name(self.dist.project_name) @@ -179,7 +179,7 @@ def name(self): @property def version(self): - # type: () -> _BaseVersion + # type: () -> CandidateVersion if self._version is None: self._version = parse_version(self.dist.version) return self._version @@ -262,8 +262,8 @@ def __init__( link, # type: Link template, # type: InstallRequirement factory, # type: Factory - name=None, # type: Optional[str] - version=None, # type: Optional[_BaseVersion] + name=None, # type: Optional[NormalizedName] + version=None, # type: Optional[CandidateVersion] ): # type: (...) -> None source_link = link @@ -315,8 +315,8 @@ def __init__( link, # type: Link template, # type: InstallRequirement factory, # type: Factory - name=None, # type: Optional[str] - version=None, # type: Optional[_BaseVersion] + name=None, # type: Optional[NormalizedName] + version=None, # type: Optional[CandidateVersion] ): # type: (...) -> None super().__init__( @@ -378,7 +378,7 @@ def __eq__(self, other): @property def project_name(self): - # type: () -> str + # type: () -> NormalizedName return canonicalize_name(self.dist.project_name) @property @@ -388,7 +388,7 @@ def name(self): @property def version(self): - # type: () -> _BaseVersion + # type: () -> CandidateVersion return parse_version(self.dist.version) @property @@ -471,7 +471,7 @@ def __eq__(self, other): @property def project_name(self): - # type: () -> str + # type: () -> NormalizedName return self.base.project_name @property @@ -482,7 +482,7 @@ def name(self): @property def version(self): - # type: () -> _BaseVersion + # type: () -> CandidateVersion return self.base.version def format_for_error(self): @@ -565,9 +565,9 @@ def __str__(self): @property def project_name(self): - # type: () -> str + # type: () -> NormalizedName # Avoid conflicting with the PyPI package "Python". - return "<Python from Requires-Python>" + return cast(NormalizedName, "<Python from Requires-Python>") @property def name(self): @@ -576,7 +576,7 @@ def name(self): @property def version(self): - # type: () -> _BaseVersion + # type: () -> CandidateVersion return self._version def format_for_error(self): diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 188a54bf21d..aa6c4781d2e 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -15,8 +15,7 @@ ) from pip._vendor.packaging.specifiers import SpecifierSet -from pip._vendor.packaging.utils import canonicalize_name -from pip._vendor.packaging.version import _BaseVersion +from pip._vendor.packaging.utils import NormalizedName, canonicalize_name from pip._vendor.pkg_resources import Distribution from pip._vendor.resolvelib import ResolutionImpossible @@ -44,7 +43,7 @@ ) from pip._internal.utils.virtualenv import running_under_virtualenv -from .base import Candidate, Constraint, Requirement +from .base import Candidate, CandidateVersion, Constraint, Requirement from .candidates import ( AlreadyInstalledCandidate, BaseCandidate, @@ -139,8 +138,8 @@ def _make_candidate_from_link( link, # type: Link extras, # type: FrozenSet[str] template, # type: InstallRequirement - name, # type: Optional[str] - version, # type: Optional[_BaseVersion] + name, # type: Optional[NormalizedName] + version, # type: Optional[CandidateVersion] ): # type: (...) -> Optional[Candidate] # TODO: Check already installed candidate, and use it if the link and @@ -202,10 +201,12 @@ def _iter_found_candidates( # all of them. # Hopefully the Project model can correct this mismatch in the future. template = ireqs[0] + assert template.req, "Candidates found on index must be PEP 508" name = canonicalize_name(template.req.name) extras = frozenset() # type: FrozenSet[str] for ireq in ireqs: + assert ireq.req, "Candidates found on index must be PEP 508" specifier &= ireq.req.specifier hashes &= ireq.hashes(trust_internet=False) extras |= frozenset(ireq.extras) @@ -368,7 +369,7 @@ def get_wheel_cache_entry(self, link, name): def get_dist_to_uninstall(self, candidate): # type: (Candidate) -> Optional[Distribution] # TODO: Are there more cases this needs to return True? Editable? - dist = self._installed_dists.get(candidate.name) + dist = self._installed_dists.get(candidate.project_name) if dist is None: # Not installed, no uninstallation required. return None diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py index 70ad86af947..b4cb571401e 100644 --- a/src/pip/_internal/resolution/resolvelib/requirements.py +++ b/src/pip/_internal/resolution/resolvelib/requirements.py @@ -1,5 +1,5 @@ from pip._vendor.packaging.specifiers import SpecifierSet -from pip._vendor.packaging.utils import canonicalize_name +from pip._vendor.packaging.utils import NormalizedName, canonicalize_name from pip._internal.req.req_install import InstallRequirement @@ -24,7 +24,7 @@ def __repr__(self): @property def project_name(self): - # type: () -> str + # type: () -> NormalizedName # No need to canonicalise - the candidate did this return self.candidate.project_name @@ -67,7 +67,8 @@ def __repr__(self): @property def project_name(self): - # type: () -> str + # type: () -> NormalizedName + assert self._ireq.req, "Specifier-backed ireq is always PEP 508" return canonicalize_name(self._ireq.req.name) @property @@ -96,14 +97,14 @@ def get_candidate_lookup(self): def is_satisfied_by(self, candidate): # type: (Candidate) -> bool - assert ( - candidate.name == self.name - ), "Internal issue: Candidate is not for this requirement " " {} vs {}".format( - candidate.name, self.name + assert candidate.name == self.name, ( + f"Internal issue: Candidate is not for this requirement " + f"{candidate.name} vs {self.name}" ) # We can safely always allow prereleases here since PackageFinder # already implements the prerelease logic, and would have filtered out # prerelease candidates if the user does not expect them. + assert self._ireq.req, "Specifier-backed ireq is always PEP 508" spec = self._ireq.req.specifier return spec.contains(candidate.version, prereleases=True) @@ -129,7 +130,7 @@ def __repr__(self): @property def project_name(self): - # type: () -> str + # type: () -> NormalizedName return self._candidate.project_name @property @@ -160,7 +161,7 @@ class UnsatisfiableRequirement(Requirement): """A requirement that cannot be satisfied.""" def __init__(self, name): - # type: (str) -> None + # type: (NormalizedName) -> None self._name = name def __str__(self): @@ -176,7 +177,7 @@ def __repr__(self): @property def project_name(self): - # type: () -> str + # type: () -> NormalizedName return self._name @property diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 7826cfc0fab..4f8a53d0d07 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -5,7 +5,7 @@ from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.packaging.version import parse as parse_version -from pip._vendor.resolvelib import ResolutionImpossible +from pip._vendor.resolvelib import BaseReporter, ResolutionImpossible from pip._vendor.resolvelib import Resolver as RLResolver from pip._vendor.resolvelib.resolvers import Result @@ -32,7 +32,7 @@ from .factory import Factory if TYPE_CHECKING: - from pip._vendor.resolvelib.structs import Graph + from pip._vendor.resolvelib.structs import DirectedGraph logger = logging.getLogger(__name__) @@ -86,6 +86,7 @@ def resolve(self, root_reqs, check_supported_wheels): raise InstallationError(problem) if not req.match_markers(): continue + assert req.name, "Constraint must be named" name = canonicalize_name(req.name) if name in constraints: constraints[name] &= req @@ -110,14 +111,14 @@ def resolve(self, root_reqs, check_supported_wheels): user_requested=user_requested, ) if "PIP_RESOLVER_DEBUG" in os.environ: - reporter = PipDebuggingReporter() + reporter = PipDebuggingReporter() # type: BaseReporter else: reporter = PipReporter() resolver = RLResolver(provider, reporter) try: try_to_avoid_resolution_too_deep = 2000000 - self._result = resolver.resolve( + result = self._result = resolver.resolve( requirements, max_rounds=try_to_avoid_resolution_too_deep ) @@ -126,7 +127,7 @@ def resolve(self, root_reqs, check_supported_wheels): raise error from e req_set = RequirementSet(check_supported_wheels=check_supported_wheels) - for candidate in self._result.mapping.values(): + for candidate in result.mapping.values(): ireq = candidate.get_install_requirement() if ireq is None: continue @@ -235,7 +236,7 @@ def get_installation_order(self, req_set): def get_topological_weights(graph, expected_node_count): - # type: (Graph, int) -> Dict[Optional[str], int] + # type: (DirectedGraph, int) -> Dict[Optional[str], int] """Assign weights to each node based on how "deep" they are. This implementation may change at any point in the future without prior diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index d6314714acd..90966d9ed08 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -167,7 +167,7 @@ def _always_true(_): def _verify_one(req, wheel_path): # type: (InstallRequirement, str) -> None - canonical_name = canonicalize_name(req.name) + canonical_name = canonicalize_name(req.name or "") w = Wheel(os.path.basename(wheel_path)) if canonicalize_name(w.name) != canonical_name: raise InvalidWheelFilename( @@ -175,10 +175,11 @@ def _verify_one(req, wheel_path): "got {!r}".format(canonical_name, w.name), ) dist = get_wheel_distribution(wheel_path, canonical_name) - if canonicalize_version(dist.version) != canonicalize_version(w.version): + dist_verstr = str(dist.version) + if canonicalize_version(dist_verstr) != canonicalize_version(w.version): raise InvalidWheelFilename( "Wheel has unexpected file name: expected {!r}, " - "got {!r}".format(str(dist.version), w.version), + "got {!r}".format(dist_verstr, w.version), ) metadata_version_value = dist.metadata_version if metadata_version_value is None: @@ -192,7 +193,7 @@ def _verify_one(req, wheel_path): and not isinstance(dist.version, Version)): raise UnsupportedWheel( "Metadata 1.2 mandates PEP 440 version, " - "but {!r} is not".format(str(dist.version)) + "but {!r} is not".format(dist_verstr) ) @@ -242,6 +243,7 @@ def _build_one_inside_env( assert req.name if req.use_pep517: assert req.metadata_directory + assert req.pep517_backend wheel_path = build_wheel_pep517( name=req.name, backend=req.pep517_backend, From c76e9c4d8ecf7c6e14b0981b780964959b8e04e4 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Mon, 15 Mar 2021 14:51:15 +0800 Subject: [PATCH 3060/3170] Reduce diff --- src/pip/_internal/commands/install.py | 7 ++----- src/pip/_internal/operations/check.py | 19 +++++++++---------- 2 files changed, 11 insertions(+), 15 deletions(-) diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index cd7d1ff90e3..dc637d87635 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -5,7 +5,7 @@ import shutil import site from optparse import SUPPRESS_HELP, Values -from typing import TYPE_CHECKING, Iterable, List, Optional +from typing import Iterable, List, Optional from pip._vendor.packaging.utils import canonicalize_name @@ -22,7 +22,7 @@ from pip._internal.locations import get_scheme from pip._internal.metadata import get_environment from pip._internal.models.format_control import FormatControl -from pip._internal.operations.check import check_install_conflicts +from pip._internal.operations.check import ConflictDetails, check_install_conflicts from pip._internal.req import install_given_reqs from pip._internal.req.req_install import InstallRequirement from pip._internal.req.req_tracker import get_requirement_tracker @@ -42,9 +42,6 @@ should_build_for_install_command, ) -if TYPE_CHECKING: - from pip._internal.operations.check import ConflictDetails - logger = logging.getLogger(__name__) diff --git a/src/pip/_internal/operations/check.py b/src/pip/_internal/operations/check.py index 03fd75c9d9d..5699c0b91ee 100644 --- a/src/pip/_internal/operations/check.py +++ b/src/pip/_internal/operations/check.py @@ -15,18 +15,17 @@ if TYPE_CHECKING: from pip._vendor.packaging.utils import NormalizedName - # Shorthands - PackageSet = Dict[NormalizedName, 'PackageDetails'] - Missing = Tuple[str, Any] - Conflicting = Tuple[str, str, Any] - - MissingDict = Dict[NormalizedName, List[Missing]] - ConflictingDict = Dict[NormalizedName, List[Conflicting]] - CheckResult = Tuple[MissingDict, ConflictingDict] - ConflictDetails = Tuple[PackageSet, CheckResult] +logger = logging.getLogger(__name__) +# Shorthands +PackageSet = Dict['NormalizedName', 'PackageDetails'] +Missing = Tuple[str, Any] +Conflicting = Tuple[str, str, Any] -logger = logging.getLogger(__name__) +MissingDict = Dict['NormalizedName', List[Missing]] +ConflictingDict = Dict['NormalizedName', List[Conflicting]] +CheckResult = Tuple[MissingDict, ConflictingDict] +ConflictDetails = Tuple[PackageSet, CheckResult] PackageDetails = namedtuple('PackageDetails', ['version', 'requires']) From e2892355d8e5decb40881bd4c5fd9d4f25dab4ed Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 16 Mar 2021 00:32:18 +0800 Subject: [PATCH 3061/3170] Default to follow-imports = normal instead --- setup.cfg | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.cfg b/setup.cfg index f78cfece7d3..7746f08c932 100644 --- a/setup.cfg +++ b/setup.cfg @@ -31,7 +31,6 @@ per-file-ignores = tests/*: B011 [mypy] -follow_imports = silent ignore_missing_imports = True disallow_untyped_defs = True disallow_any_generics = True From 8e2071e5573b608cbff5d1c5adeb3f2a7bdc6104 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sun, 21 Mar 2021 09:14:14 +0000 Subject: [PATCH 3062/3170] Rewrite our documentation landing page - Change to Markdown - Split the Table of Contents --- docs/html/index.md | 48 ++++++++++++++++++++++++++++++++++ docs/html/index.rst | 63 --------------------------------------------- 2 files changed, 48 insertions(+), 63 deletions(-) create mode 100644 docs/html/index.md delete mode 100644 docs/html/index.rst diff --git a/docs/html/index.md b/docs/html/index.md new file mode 100644 index 00000000000..351d8f79ff5 --- /dev/null +++ b/docs/html/index.md @@ -0,0 +1,48 @@ +--- +hide-toc: true +--- + +# pip + +pip is the [package installer for Python][recommended]. You can use it to +install packages from the [Python Package Index][pypi] and other indexes. + +```{toctree} +:hidden: + +quickstart +installing +user_guide +reference/index +``` + +```{toctree} +:caption: Project +:hidden: + +development/index +ux_research_design +news +Code of Conduct <https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md> +GitHub <https://github.com/pypa/pip> +``` + +If you want to learn about how to use pip, check out the following resources: + +- [Quickstart](quickstart) +- [Python Packaging User Guide](https://packaging.python.org) + +If you find bugs, need help, or want to talk to the developers, use our mailing +lists or chat rooms: + +- [GitHub Issues][issue-tracker] +- [Discourse channel][packaging-discourse] +- [User IRC][irc-pypa] +- [Development IRC][irc-pypa-dev] + +[recommended]: https://packaging.python.org/guides/tool-recommendations/ +[pypi]: https://pypi.org/ +[issue-tracker]: https://github.com/pypa/pip/issues/ +[packaging-discourse]: https://discuss.python.org/c/packaging/14 +[irc-pypa]: https://webchat.freenode.net/#pypa +[irc-pypa-dev]: https://webchat.freenode.net/#pypa-dev diff --git a/docs/html/index.rst b/docs/html/index.rst deleted file mode 100644 index b92a23e020a..00000000000 --- a/docs/html/index.rst +++ /dev/null @@ -1,63 +0,0 @@ -================================== -pip - The Python Package Installer -================================== - -pip is the `package installer`_ for Python. You can use pip to install packages from the `Python Package Index`_ and other indexes. - -Please take a look at our documentation for how to install and use pip: - -.. toctree:: - :maxdepth: 1 - - quickstart - installing - user_guide - reference/index - development/index - ux_research_design - news - -.. warning:: - - In pip 20.3, we've `made a big improvement to the heart of pip`_; - :ref:`Resolver changes 2020`. We want your input, so `sign up for - our user experience research studies`_ to help us do it right. - -.. warning:: - - pip 21.0, in January 2021, removed Python 2 support, per pip's - :ref:`Python 2 Support` policy. Please migrate to Python 3. - -If you find bugs, need help, or want to talk to the developers, please use our mailing lists or chat rooms: - -* `Issue tracking`_ -* `Discourse channel`_ -* `User IRC`_ - -If you want to get involved, head over to GitHub to get the source code, and feel free to jump on the developer mailing lists and chat rooms: - -* `GitHub page`_ -* `Development mailing list`_ -* `Development IRC`_ - - -Code of Conduct -=============== - -Everyone interacting in the pip project's codebases, issue trackers, chat -rooms, and mailing lists is expected to follow the `PSF Code of Conduct`_. - -.. _package installer: https://packaging.python.org/guides/tool-recommendations/ -.. _Python Package Index: https://pypi.org -.. _made a big improvement to the heart of pip: https://pyfound.blogspot.com/2020/11/pip-20-3-new-resolver.html -.. _sign up for our user experience research studies: https://pyfound.blogspot.com/2020/03/new-pip-resolver-to-roll-out-this-year.html -.. _Installation: https://pip.pypa.io/en/stable/installing.html -.. _Documentation: https://pip.pypa.io/en/stable/ -.. _Changelog: https://pip.pypa.io/en/stable/news.html -.. _GitHub page: https://github.com/pypa/pip -.. _Issue tracking: https://github.com/pypa/pip/issues -.. _Discourse channel: https://discuss.python.org/c/packaging -.. _Development mailing list: https://mail.python.org/mailman3/lists/distutils-sig.python.org/ -.. _User IRC: https://webchat.freenode.net/?channels=%23pypa -.. _Development IRC: https://webchat.freenode.net/?channels=%23pypa-dev -.. _PSF Code of Conduct: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md From ad636667492d8249f9b9340b9a7409d68d23d7ed Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sun, 21 Mar 2021 11:55:07 +0000 Subject: [PATCH 3063/3170] Drop YAML tests for the resolver We've created a fairly capable setup with the new resolver tests, which has now rendered this logic redundant and generally unnecessary. --- .pre-commit-config.yaml | 2 - .../html/development/architecture/anatomy.rst | 1 - setup.cfg | 1 - tests/functional/test_yaml.py | 203 --- tests/yaml/ERRORS.md | 60 - tests/yaml/README.md | 74 - tests/yaml/backtrack.yml | 40 - tests/yaml/circular.yml | 45 - tests/yaml/conflict_1.yml | 77 - tests/yaml/conflict_2.yml | 28 - tests/yaml/conflict_3.yml | 22 - tests/yaml/conflicting_diamond.yml | 19 - tests/yaml/conflicting_triangle.yml | 18 - tests/yaml/extras.yml | 49 - tests/yaml/fallback.yml | 20 - tests/yaml/huge.yml | 1260 ----------------- tests/yaml/large.yml | 295 ---- tests/yaml/linter.py | 108 -- tests/yaml/non_pinned.yml | 24 - tests/yaml/overlap1.yml | 44 - tests/yaml/pinned.yml | 29 - tests/yaml/pip988.yml | 37 - tests/yaml/poetry2298.yml | 24 - tests/yaml/simple.yml | 47 - tests/yaml/trivial.yml | 24 - tools/requirements/tests.txt | 1 - 26 files changed, 2552 deletions(-) delete mode 100644 tests/functional/test_yaml.py delete mode 100644 tests/yaml/ERRORS.md delete mode 100644 tests/yaml/README.md delete mode 100644 tests/yaml/backtrack.yml delete mode 100644 tests/yaml/circular.yml delete mode 100644 tests/yaml/conflict_1.yml delete mode 100644 tests/yaml/conflict_2.yml delete mode 100644 tests/yaml/conflict_3.yml delete mode 100644 tests/yaml/conflicting_diamond.yml delete mode 100644 tests/yaml/conflicting_triangle.yml delete mode 100644 tests/yaml/extras.yml delete mode 100644 tests/yaml/fallback.yml delete mode 100644 tests/yaml/huge.yml delete mode 100644 tests/yaml/large.yml delete mode 100644 tests/yaml/linter.py delete mode 100644 tests/yaml/non_pinned.yml delete mode 100644 tests/yaml/overlap1.yml delete mode 100644 tests/yaml/pinned.yml delete mode 100644 tests/yaml/pip988.yml delete mode 100644 tests/yaml/poetry2298.yml delete mode 100644 tests/yaml/simple.yml delete mode 100644 tests/yaml/trivial.yml diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 0521f261f7b..5b470e36f2e 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -34,7 +34,6 @@ repos: ^tools/| # Tests ^tests/conftest.py| - ^tests/yaml| ^tests/lib| ^tests/data| ^tests/unit| @@ -45,7 +44,6 @@ repos: # A blank ignore, to avoid merge conflicts later. ^$ - - repo: https://gitlab.com/pycqa/flake8 rev: 3.8.4 hooks: diff --git a/docs/html/development/architecture/anatomy.rst b/docs/html/development/architecture/anatomy.rst index 46bba448944..e30e03e2129 100644 --- a/docs/html/development/architecture/anatomy.rst +++ b/docs/html/development/architecture/anatomy.rst @@ -51,7 +51,6 @@ The ``README``, license, ``pyproject.toml``, ``setup.py``, and so on are in the * ``functional/`` *[functional tests of pip’s CLI -- end-to-end, invoke pip in subprocess & check results of execution against desired result. This also is what makes test suite slow]* * ``lib/`` *[helpers for tests]* * ``unit/`` *[unit tests -- fast and small and nice!]* - * ``yaml/`` *[resolver tests! They’re written in YAML. This folder just contains .yaml files -- actual code for reading/running them is in lib/yaml.py . This is fine!]* * ``tools`` *[misc development workflow tools, like requirements files & Travis CI files & helpers for tox]* * ``.azure-pipelines`` diff --git a/setup.cfg b/setup.cfg index 1d851d94929..aab1807f4f0 100644 --- a/setup.cfg +++ b/setup.cfg @@ -53,7 +53,6 @@ markers = svn: VCS: Subversion mercurial: VCS: Mercurial git: VCS: git - yaml: yaml based tests search: tests for 'pip search' [coverage:run] diff --git a/tests/functional/test_yaml.py b/tests/functional/test_yaml.py deleted file mode 100644 index ba7b17531ef..00000000000 --- a/tests/functional/test_yaml.py +++ /dev/null @@ -1,203 +0,0 @@ -""" -Tests for the resolver -""" - -import os -import re -import sys - -import pytest -import yaml - -from tests.lib import DATA_DIR, create_basic_wheel_for_package, path_to_url - - -def generate_yaml_tests(directory): - """ - Generate yaml test cases from the yaml files in the given directory - """ - for yml_file in directory.glob("*.yml"): - data = yaml.safe_load(yml_file.read_text()) - assert "cases" in data, "A fixture needs cases to be used in testing" - - # Strip the parts of the directory to only get a name without - # extension and resolver directory - base_name = str(yml_file)[len(str(directory)) + 1:-4] - - base = data.get("base", {}) - cases = data["cases"] - - for resolver in 'legacy', '2020-resolver': - for i, case_template in enumerate(cases): - case = base.copy() - case.update(case_template) - - case[":name:"] = base_name - if len(cases) > 1: - case[":name:"] += "-" + str(i) - case[":name:"] += "*" + resolver - case[":resolver:"] = resolver - - skip = case.pop("skip", False) - assert skip in [False, True, 'legacy', '2020-resolver'] - if skip is True or skip == resolver: - case = pytest.param(case, marks=pytest.mark.xfail) - - yield case - - -def id_func(param): - """ - Give a nice parameter name to the generated function parameters - """ - if isinstance(param, dict) and ":name:" in param: - return param[":name:"] - - retval = str(param) - if len(retval) > 25: - retval = retval[:20] + "..." + retval[-2:] - return retval - - -def convert_to_dict(string): - - def stripping_split(my_str, splitwith, count=None): - if count is None: - return [x.strip() for x in my_str.strip().split(splitwith)] - else: - return [x.strip() for x in my_str.strip().split(splitwith, count)] - - parts = stripping_split(string, ";") - - retval = {} - retval["depends"] = [] - retval["extras"] = {} - - retval["name"], retval["version"] = stripping_split(parts[0], " ") - - for part in parts[1:]: - verb, args_str = stripping_split(part, " ", 1) - assert verb in ["depends"], f"Unknown verb {verb!r}" - - retval[verb] = stripping_split(args_str, ",") - - return retval - - -def handle_request(script, action, requirement, options, resolver_variant): - if action == 'install': - args = ['install'] - if resolver_variant == "legacy": - args.append("--use-deprecated=legacy-resolver") - args.extend(["--no-index", "--find-links", - path_to_url(script.scratch_path)]) - elif action == 'uninstall': - args = ['uninstall', '--yes'] - else: - raise f"Did not excpet action: {action!r}" - - if isinstance(requirement, str): - args.append(requirement) - elif isinstance(requirement, list): - args.extend(requirement) - else: - raise f"requirement neither str nor list {requirement!r}" - - args.extend(options) - args.append("--verbose") - - result = script.pip(*args, - allow_stderr_error=True, - allow_stderr_warning=True, - allow_error=True) - - # Check which packages got installed - state = [] - for path in os.listdir(script.site_packages_path): - if path.endswith(".dist-info"): - name, version = ( - os.path.basename(path)[:-len(".dist-info")] - ).rsplit("-", 1) - # TODO: information about extras. - state.append(" ".join((name, version))) - - return {"result": result, "state": sorted(state)} - - -def check_error(error, result): - return_code = error.get('code') - if return_code: - assert result.returncode == return_code - - stderr = error.get('stderr') - if not stderr: - return - - if isinstance(stderr, str): - patters = [stderr] - elif isinstance(stderr, list): - patters = stderr - else: - raise "string or list expected, found %r" % stderr - - for patter in patters: - match = re.search(patter, result.stderr, re.I) - assert match, 'regex %r not found in stderr: %r' % ( - stderr, result.stderr) - - -@pytest.mark.yaml -@pytest.mark.parametrize( - "case", generate_yaml_tests(DATA_DIR.parent / "yaml"), ids=id_func -) -def test_yaml_based(script, case): - available = case.get("available", []) - requests = case.get("request", []) - responses = case.get("response", []) - - assert len(requests) == len(responses), ( - "Expected requests and responses counts to be same" - ) - - # Create a custom index of all the packages that are supposed to be - # available - # XXX: This doesn't work because this isn't making an index of files. - for package in available: - if isinstance(package, str): - package = convert_to_dict(package) - - assert isinstance(package, dict), "Needs to be a dictionary" - - create_basic_wheel_for_package(script, **package) - - # use scratch path for index - for request, response in zip(requests, responses): - - for action in 'install', 'uninstall': - if action in request: - break - else: - raise f"Unsupported request {request!r}" - - # Perform the requested action - effect = handle_request(script, action, - request[action], - request.get('options', '').split(), - resolver_variant=case[':resolver:']) - result = effect['result'] - - if 0: # for analyzing output easier - with open(DATA_DIR.parent / "yaml" / - case[':name:'].replace('*', '-'), 'w') as fo: - fo.write("=== RETURNCODE = %d\n" % result.returncode) - fo.write("=== STDERR ===:\n%s\n" % result.stderr) - - if 'state' in response: - assert effect['state'] == (response['state'] or []), str(result) - - error = response.get('error') - if error and case[":resolver:"] == 'new' and sys.platform != 'win32': - # Note: we currently skip running these tests on Windows, as they - # were failing due to different error codes. There should not - # be a reason for not running these this check on Windows. - check_error(error, result) diff --git a/tests/yaml/ERRORS.md b/tests/yaml/ERRORS.md deleted file mode 100644 index 700e3d4ea5d..00000000000 --- a/tests/yaml/ERRORS.md +++ /dev/null @@ -1,60 +0,0 @@ -# New resolver error messages - - -## Incompatible requirements - -Most resolver error messages are due to incompatible requirements. -That is, the dependency tree contains conflicting versions of the same -package. Take the example: - - base: - available: - - A 1.0.0; depends B == 1.0.0, C == 2.0.0 - - B 1.0.0; depends C == 1.0.0 - - C 1.0.0 - - C 2.0.0 - -Here, `A` cannot be installed because it depends on `B` (which depends on -a different version of `C` than `A` itself. In real world examples, the -conflicting version are not so easy to spot. I'm suggesting an error -message which looks something like this: - - A 1.0.0 -> B 1.0.0 -> C 1.0.0 - A 1.0.0 -> C 2.0.0 - -That is, for the conflicting package, we show the user where exactly the -requirement came from. - - -## Double requirement - -I've noticed that in many cases the old resolver messages are more -informative. For example, in the simple example: - - base: - available: - - B 1.0.0 - - B 2.0.0 - -Now if we want to install both version of `B` at the same time, -i.e. the requirement `B==1.0.0 B==2.0.0`, we get: - - ERROR: Could not find a version that satisfies the requirement B==1.0.0 - ERROR: Could not find a version that satisfies the requirement B==2.0.0 - No matching distribution found for b, b - -Even though both version are actually available and satisfy each requirement, -just not at once. When trying to install a version of `B` which does not -exist, say requirement `B==1.5.0`, you get the same type of error message: - - Could not find a version that satisfies the requirement B==1.5.0 - No matching distribution found for b - -For this case, the old error message was: - - Could not find a version that satisfies the requirement B==1.5.0 (from versions: 1.0.0, 2.0.0) - No matching distribution found for B==1.5.0 - -And the old error message for the requirement `B==1.0.0 B==2.0.0`: - - Double requirement given: B==2.0.0 (already in B==1.0.0, name='B') diff --git a/tests/yaml/README.md b/tests/yaml/README.md deleted file mode 100644 index 1a379fdcbb0..00000000000 --- a/tests/yaml/README.md +++ /dev/null @@ -1,74 +0,0 @@ -# YAML tests for pip's resolver - -This directory contains fixtures for testing pip's resolver. -The fixtures are written as `.yml` files, with a convenient format -that allows for specifying a custom index for temporary use. - -The `.yml` files are typically organized in the following way. Here, we are -going to take a closer look at the `simple.yml` file and step through the -test cases. A `base` section defines which packages are available upstream: - - base: - available: - - simple 0.1.0 - - simple 0.2.0 - - base 0.1.0; depends dep - - dep 0.1.0 - -Each package has a name and version number. Here, there are two -packages `simple` (with versoin `0.1.0` and `0.2.0`). The package -`base 0.1.0` depends on the requirement `dep` (which simply means it -depends on any version of `dep`. More generally, a package can also -depend on a specific version of another package, or a range of versions. - -Next, in our yaml file, we have the `cases:` section which is a list of -test cases. Each test case has a request and a response. The request -is what the user would want to do: - - cases: - - - request: - - install: simple - - uninstall: simple - response: - - state: - - simple 0.2.0 - - state: null - -Here the first request is to install the package simple, this would -basically be equivalent to typing `pip install simple`, and the corresponding -first response is that the state of installed packages is `simple 0.2.0`. -Note that by default the highest version of an available package will be -installed. - -The second request is to uninstall simple again, which will result in the -state `null` (basically an empty list of installed packages). - -When the yaml tests are run, each response is verified by checking which -packages got actually installed. Note that this is check is done in -alphabetical order. - - - -The linter is very useful for initally checking `.yml` files, e.g.: - - $ python linter.py -v simple.yml - -To run only the yaml tests, use (from the root of the source tree): - - $ tox -e py38 -- -m yaml -vv - -Or, in order to avoid collecting all the test cases: - - $ tox -e py38 -- tests/functional/test_yaml.py - -Or, only a specific test: - - $ tox -e py38 -- tests/functional/test_yaml.py -k simple - -Or, just a specific test case: - - $ tox -e py38 -- tests/functional/test_yaml.py -k simple-0 - - -<!-- TODO: Add a good description of the format and how it can be used. --> diff --git a/tests/yaml/backtrack.yml b/tests/yaml/backtrack.yml deleted file mode 100644 index ffcb722b88c..00000000000 --- a/tests/yaml/backtrack.yml +++ /dev/null @@ -1,40 +0,0 @@ -# Pradyun's backtracking example -base: - available: - - A 1.0.0; depends B == 1.0.0 - - A 2.0.0; depends B == 2.0.0, C == 1.0.0 - - A 3.0.0; depends B == 3.0.0, C == 2.0.0 - - A 4.0.0; depends B == 4.0.0, C == 3.0.0 - - A 5.0.0; depends B == 5.0.0, C == 4.0.0 - - A 6.0.0; depends B == 6.0.0, C == 5.0.0 - - A 7.0.0; depends B == 7.0.0, C == 6.0.0 - - A 8.0.0; depends B == 8.0.0, C == 7.0.0 - - - B 1.0.0; depends C == 1.0.0 - - B 2.0.0; depends C == 2.0.0 - - B 3.0.0; depends C == 3.0.0 - - B 4.0.0; depends C == 4.0.0 - - B 5.0.0; depends C == 5.0.0 - - B 6.0.0; depends C == 6.0.0 - - B 7.0.0; depends C == 7.0.0 - - B 8.0.0; depends C == 8.0.0 - - - C 1.0.0 - - C 2.0.0 - - C 3.0.0 - - C 4.0.0 - - C 5.0.0 - - C 6.0.0 - - C 7.0.0 - - C 8.0.0 - -cases: -- - request: - - install: A - response: - - state: - - A 1.0.0 - - B 1.0.0 - - C 1.0.0 - skip: legacy diff --git a/tests/yaml/circular.yml b/tests/yaml/circular.yml deleted file mode 100644 index 95c535454fa..00000000000 --- a/tests/yaml/circular.yml +++ /dev/null @@ -1,45 +0,0 @@ -base: - available: - - A 1.0.0; depends B == 1.0.0 - - B 1.0.0; depends C == 1.0.0 - - C 1.0.0; depends D == 1.0.0 - - D 1.0.0; depends A == 1.0.0 - -cases: -# NOTE: Do we want to check the order? -- - request: - - install: A - response: - - state: - - A 1.0.0 - - B 1.0.0 - - C 1.0.0 - - D 1.0.0 -- - request: - - install: B - response: - - state: - - A 1.0.0 - - B 1.0.0 - - C 1.0.0 - - D 1.0.0 -- - request: - - install: C - response: - - state: - - A 1.0.0 - - B 1.0.0 - - C 1.0.0 - - D 1.0.0 -- - request: - - install: D - response: - - state: - - A 1.0.0 - - B 1.0.0 - - C 1.0.0 - - D 1.0.0 diff --git a/tests/yaml/conflict_1.yml b/tests/yaml/conflict_1.yml deleted file mode 100644 index dc18be32a1f..00000000000 --- a/tests/yaml/conflict_1.yml +++ /dev/null @@ -1,77 +0,0 @@ -base: - available: - - A 1.0.0; depends B == 1.0.0, B == 2.0.0 - - B 1.0.0 - - B 2.0.0 - -cases: -- - request: - - install: A - response: - - error: - code: 0 - stderr: ['incompatible'] - skip: legacy - # -- a good error message would be: - # A 1.0.0 has incompatible requirements B==1.0.0, B==2.0.0 - -- - request: - - install: ['B==1.0.0', 'B'] - response: - - state: - - B 1.0.0 - skip: legacy - # -- old error: - # Double requirement given: B (already in B==1.0.0, name='B') - -- - request: - - install: ['B==1.0.0', 'B==2.0.0'] - response: - - state: null - error: - code: 1 - stderr: >- - Cannot install B==1.0.0 and B==2.0.0 because these - package versions have conflicting dependencies. - skip: legacy - # -- currently the (new resolver) error message is: - # Could not find a version that satisfies the requirement B==1.0.0 - # Could not find a version that satisfies the requirement B==2.0.0 - # No matching distribution found for b, b - # -- better would be: - # cannot install different version (1.0.0, 2.0.0) of package B at the - # same time. - # -- the old error message was actually better here: - # Double requirement given: B==2.0.0 (already in B==1.0.0, name='B') - -- - request: - - install: B==1.5.0 - response: - - state: null - error: - code: 1 - stderr: 'no\s+matching\s+distribution' - skip: legacy - # -- currently (new resolver) error message is: - # Could not find a version that satisfies the requirement B==1.5.0 - # No matching distribution found for b - # -- the old error message was actually better here: - # Could not find a version that satisfies the requirement B==1.5.0 (from versions: 1.0.0, 2.0.0) - # No matching distribution found for B==1.5.0 - -- - request: - - install: A==2.0 - response: - - state: null - error: - code: 1 - stderr: 'no\s+matching\s+distribution' - skip: legacy - # -- currently the error message is: - # Could not find a version that satisfies the requirement A==2.0 - # No matching distribution found for a diff --git a/tests/yaml/conflict_2.yml b/tests/yaml/conflict_2.yml deleted file mode 100644 index 7ec5848ed8f..00000000000 --- a/tests/yaml/conflict_2.yml +++ /dev/null @@ -1,28 +0,0 @@ -# Tzu-ping mentioned this example -base: - available: - - name: virtualenv - version: 20.0.2 - depends: ['six>=1.12.0,<2'] - - six 1.11 - - six 1.12 - - six 1.13 - -cases: -- - request: - - install: virtualenv - response: - - state: - - six 1.13 - - virtualenv 20.0.2 -- - request: - - install: ['six<1.12', 'virtualenv==20.0.2'] - response: - - state: null - error: - stderr: >- - Cannot install six<1.12 and virtualenv 20.0.2 because these - package versions have conflicting dependencies. - skip: legacy diff --git a/tests/yaml/conflict_3.yml b/tests/yaml/conflict_3.yml deleted file mode 100644 index 53f2b4a981f..00000000000 --- a/tests/yaml/conflict_3.yml +++ /dev/null @@ -1,22 +0,0 @@ -base: - available: - - A 1.0.0; depends B == 1.0.0, C == 2.0.0 - - B 1.0.0; depends C == 1.0.0 - - C 1.0.0 - - C 2.0.0 - -cases: -- - request: - - install: A - response: - - state: null - skip: legacy - # -- currently the error message is: - # Could not find a version that satisfies the requirement C==2.0.0 (from a) - # Could not find a version that satisfies the requirement C==1.0.0 (from b) - # No matching distribution found for c, c - # -- This is a bit confusing, as both versions of C are available. - # -- better would be something like: - # A 1.0.0 -> B 1.0.0 -> C 1.0.0 - # A 1.0.0 -> C 2.0.0 diff --git a/tests/yaml/conflicting_diamond.yml b/tests/yaml/conflicting_diamond.yml deleted file mode 100644 index c28b667ac6b..00000000000 --- a/tests/yaml/conflicting_diamond.yml +++ /dev/null @@ -1,19 +0,0 @@ -cases: -- - available: - - A 1.0.0; depends B == 1.0.0, C == 1.0.0 - - B 1.0.0; depends D == 1.0.0 - - C 1.0.0; depends D == 2.0.0 - - D 1.0.0 - - D 2.0.0 - request: - - install: A - response: - - error: - code: 1 - stderr: >- - Cannot install A and A because these package - versions have conflicting dependencies. - # TODO: Tweak this error message to make sense. - # https://github.com/pypa/pip/issues/8495 - skip: legacy diff --git a/tests/yaml/conflicting_triangle.yml b/tests/yaml/conflicting_triangle.yml deleted file mode 100644 index 02b348ca2f2..00000000000 --- a/tests/yaml/conflicting_triangle.yml +++ /dev/null @@ -1,18 +0,0 @@ -cases: -- - available: - - A 1.0.0; depends C == 1.0.0 - - B 1.0.0; depends C == 2.0.0 - - C 1.0.0 - - C 2.0.0 - request: - - install: A - - install: B - response: - - state: - - A 1.0.0 - - C 1.0.0 - - error: - code: 0 - stderr: ['c==1\.0\.0', 'incompatible'] - skip: legacy diff --git a/tests/yaml/extras.yml b/tests/yaml/extras.yml deleted file mode 100644 index b0f4e992c9c..00000000000 --- a/tests/yaml/extras.yml +++ /dev/null @@ -1,49 +0,0 @@ -base: - available: - - A 1.0.0; depends B == 1.0.0, C == 1.0.0, D == 1.0.0 - - B 1.0.0; depends D[extra_1] == 1.0.0 - - C 1.0.0; depends D[extra_2] == 1.0.0 - - name: D - version: 1.0.0 - depends: [] - extras: - extra_1: [E == 1.0.0] - extra_2: [F == 1.0.0] - - E 1.0.0 - - F 1.0.0 -cases: -- - request: - - install: B - response: - - state: - - B 1.0.0 - - D 1.0.0 - - E 1.0.0 -- - request: - - install: C - response: - - state: - - C 1.0.0 - - D 1.0.0 - - F 1.0.0 -- - request: - - install: A - response: - - state: - - A 1.0.0 - - B 1.0.0 - - C 1.0.0 - - D 1.0.0 - - E 1.0.0 - - F 1.0.0 - skip: legacy -- - request: - - install: D[extra_1] - options: --no-deps - response: - - state: - - D 1.0.0 diff --git a/tests/yaml/fallback.yml b/tests/yaml/fallback.yml deleted file mode 100644 index 86925398a56..00000000000 --- a/tests/yaml/fallback.yml +++ /dev/null @@ -1,20 +0,0 @@ -base: - available: - - A 1.0.0; depends B == 1.0.0, C == 1.0.0 - - A 0.8.0 - - B 1.0.0; depends D == 1.0.0 - - C 1.0.0; depends D == 2.0.0 - - D 1.0.0 - - D 2.0.0 - -cases: -- - request: - - install: A - response: - - state: - - A 0.8.0 - # the old resolver tries to install A 1.0.0 (which fails), but the new - # resolver realises that A 1.0.0 cannot be installed and falls back to - # installing the older version A 0.8.0 instead. - skip: legacy diff --git a/tests/yaml/huge.yml b/tests/yaml/huge.yml deleted file mode 100644 index 01bfdf26f3c..00000000000 --- a/tests/yaml/huge.yml +++ /dev/null @@ -1,1260 +0,0 @@ -base: - available: - - alabaster 0.7.10 - - alabaster 0.7.11 - - appdirs 1.4.3 - - asn1crypto 0.22.0 - - asn1crypto 0.23.0 - - asn1crypto 0.24.0 - - name: astroid - version: 1.5.3 - depends: ['lazy-object-proxy', 'setuptools', 'six', 'wrapt'] - - name: astroid - version: 1.6.0 - depends: ['lazy-object-proxy', 'setuptools', 'six', 'wrapt'] - - name: astroid - version: 1.6.1 - depends: ['lazy-object-proxy', 'setuptools', 'six', 'wrapt'] - - name: astroid - version: 1.6.2 - depends: ['lazy-object-proxy', 'setuptools', 'six', 'wrapt'] - - name: astroid - version: 1.6.3 - depends: ['lazy-object-proxy', 'setuptools', 'six', 'wrapt'] - - name: astroid - version: 1.6.4 - depends: ['lazy-object-proxy', 'setuptools', 'six', 'wrapt'] - - name: astroid - version: 1.6.5 - depends: ['lazy-object-proxy', 'setuptools', 'six', 'wrapt'] - - name: astroid - version: 2.0.2 - depends: ['lazy-object-proxy', 'six', 'wrapt'] - - name: astroid - version: 2.0.4 - depends: ['lazy-object-proxy', 'six', 'wrapt'] - - name: attrs - version: 17.2.0 - depends: ['hypothesis', 'pympler', 'zope', 'zope.interface'] - - name: attrs - version: 17.3.0 - depends: ['hypothesis', 'pympler', 'zope', 'zope.interface'] - - attrs 17.4.0 - - attrs 18.1.0 - - name: automat - version: 0.6.0 - depends: ['attrs', 'six'] - - name: automat - version: 0.7.0 - depends: ['attrs', 'six'] - - name: babel - version: 2.5.0 - depends: ['pytz'] - - name: babel - version: 2.5.1 - depends: ['pytz'] - - name: babel - version: 2.5.3 - depends: ['pytz'] - - name: babel - version: 2.6.0 - depends: ['pytz'] - - backcall 0.1.0 - - backports 1.0 - - name: backports.functools_lru_cache - version: '1.4' - depends: ['backports', 'setuptools'] - - name: backports.functools_lru_cache - version: '1.5' - depends: ['backports', 'setuptools'] - - name: backports.shutil_get_terminal_size - version: 1.0.0 - depends: ['backports'] - - backports_abc 0.5 - - beautifulsoup4 4.6.0 - - beautifulsoup4 4.6.1 - - beautifulsoup4 4.6.3 - - bitarray 0.8.1 - - bitarray 0.8.2 - - bitarray 0.8.3 - - name: bkcharts - version: '0.2' - depends: ['numpy >=1.7.1', 'pandas', 'six >=1.5.2'] - - name: bleach - version: 2.0.0 - depends: ['html5lib >=0.99999999', 'six'] - - name: bleach - version: 2.1.1 - depends: ['html5lib >=0.99999999', 'setuptools', 'six'] - - name: bleach - version: 2.1.2 - depends: ['html5lib >=0.99999999', 'setuptools', 'six'] - - name: bleach - version: 2.1.3 - depends: ['html5lib >=0.99999999', 'setuptools', 'six'] - - name: bokeh - version: 0.12.10 - depends: ['jinja2 >=2.7', 'numpy >=1.7.1', 'python-dateutil >=2.1', 'pyyaml >=3.10', 'six >=1.5.2', 'tornado >=4.3'] - - name: bokeh - version: 0.12.11 - depends: ['jinja2 >=2.7', 'numpy >=1.7.1', 'python-dateutil >=2.1', 'pyyaml >=3.10', 'six >=1.5.2', 'tornado >=4.3'] - - name: bokeh - version: 0.12.13 - depends: ['jinja2 >=2.7', 'numpy >=1.7.1', 'python-dateutil >=2.1', 'pyyaml >=3.10', 'six >=1.5.2', 'tornado >=4.3'] - - name: bokeh - version: 0.12.14 - depends: ['jinja2 >=2.7', 'numpy >=1.7.1', 'packaging >=16.8', 'python-dateutil >=2.1', 'pyyaml >=3.10', 'six >=1.5.2', 'tornado >=4.3'] - - name: bokeh - version: 0.12.15 - depends: ['jinja2 >=2.7', 'numpy >=1.7.1', 'packaging >=16.8', 'python-dateutil >=2.1', 'pyyaml >=3.10', 'six >=1.5.2', 'tornado >=4.3'] - - name: bokeh - version: 0.12.16 - depends: ['jinja2 >=2.7', 'numpy >=1.7.1', 'packaging >=16.8', 'python-dateutil >=2.1', 'pyyaml >=3.10', 'six >=1.5.2', 'tornado >=4.3'] - - name: bokeh - version: 0.12.7 - depends: ['bkcharts >=0.2', 'jinja2 >=2.7', 'matplotlib', 'numpy >=1.7.1', 'pandas', 'python-dateutil >=2.1', 'pyyaml >=3.10', 'requests >=1.2.3', 'six >=1.5.2', 'tornado >=4.3'] - - name: bokeh - version: 0.12.9 - depends: ['jinja2 >=2.7', 'numpy >=1.7.1', 'python-dateutil >=2.1', 'pyyaml >=3.10', 'six >=1.5.2', 'tornado >=4.3'] - - name: bokeh - version: 0.13.0 - depends: ['jinja2 >=2.7', 'numpy >=1.7.1', 'packaging >=16.8', 'python-dateutil >=2.1', 'pyyaml >=3.10', 'six >=1.5.2', 'tornado >=4.3'] - - name: boto3 - version: 1.4.7 - depends: ['botocore >=1.7.0,<1.8.0', 'jmespath >=0.7.1,<1.0.0', 's3transfer >=0.1.10,<0.2.0'] - - name: boto3 - version: 1.4.8 - depends: ['botocore >=1.8.0,<1.9.0', 'jmespath >=0.7.1,<1.0.0', 's3transfer >=0.1.10,<0.2.0'] - - name: boto3 - version: 1.5.32 - depends: ['botocore >=1.8.46,<1.9.0', 'jmespath >=0.7.1,<1.0.0', 's3transfer >=0.1.10,<0.2.0'] - - name: boto3 - version: 1.6.18 - depends: ['botocore >=1.9.18,<1.10.0', 'jmespath >=0.7.1,<1.0.0', 's3transfer >=0.1.10,<0.2.0'] - - name: boto3 - version: 1.7.24 - depends: ['botocore >=1.10.24,<1.11.0', 'jmespath >=0.7.1,<1.0.0', 's3transfer >=0.1.10,<0.2.0'] - - name: boto3 - version: 1.7.32 - depends: ['botocore >=1.10.32,<1.11.0', 'jmespath >=0.7.1,<1.0.0', 's3transfer >=0.1.10,<0.2.0'] - - name: boto3 - version: 1.7.4 - depends: ['botocore >=1.10.4,<1.11.0', 'jmespath >=0.7.1,<1.0.0', 's3transfer >=0.1.10,<0.2.0'] - - name: boto3 - version: 1.7.45 - depends: ['botocore >=1.10.45,<1.11.0', 'jmespath >=0.7.1,<1.0.0', 's3transfer >=0.1.10,<0.2.0'] - - name: boto3 - version: 1.7.62 - depends: ['botocore >=1.10.62,<1.11.0', 'jmespath >=0.7.1,<1.0.0', 's3transfer >=0.1.10,<0.2.0'] - - name: botocore - version: 1.10.12 - depends: ['docutils >=0.10', 'jmespath >=0.7.1,<1.0.0', 'python-dateutil >=2.1,<3.0.0'] - - name: botocore - version: 1.10.24 - depends: ['docutils >=0.10', 'jmespath >=0.7.1,<1.0.0', 'python-dateutil >=2.1,<3.0.0'] - - name: botocore - version: 1.10.32 - depends: ['docutils >=0.10', 'jmespath >=0.7.1,<1.0.0', 'python-dateutil >=2.1,<3.0.0'] - - name: botocore - version: 1.10.4 - depends: ['docutils >=0.10', 'jmespath >=0.7.1,<1.0.0', 'python-dateutil >=2.1,<2.7.0'] - - name: botocore - version: 1.10.45 - depends: ['docutils >=0.10', 'jmespath >=0.7.1,<1.0.0', 'python-dateutil >=2.1,<3.0.0'] - - name: botocore - version: 1.10.62 - depends: ['docutils >=0.10', 'jmespath >=0.7.1,<1.0.0', 'python-dateutil >=2.1,<3.0.0'] - - name: botocore - version: 1.5.78 - depends: ['docutils >=0.10', 'jmespath >=0.7.1,<1.0.0', 'python-dateutil >=2.1,<3.0.0'] - - name: botocore - version: 1.7.14 - depends: ['docutils >=0.10', 'jmespath >=0.7.1,<1.0.0', 'python-dateutil >=2.1,<3.0.0'] - - name: botocore - version: 1.7.20 - depends: ['docutils >=0.10', 'jmespath >=0.7.1,<1.0.0', 'python-dateutil >=2.1,<3.0.0'] - - name: botocore - version: 1.7.40 - depends: ['docutils >=0.10', 'jmespath >=0.7.1,<1.0.0', 'python-dateutil >=2.1,<3.0.0'] - - name: botocore - version: 1.7.5 - depends: ['docutils >=0.10', 'jmespath >=0.7.1,<1.0.0', 'python-dateutil >=2.1,<3.0.0'] - - name: botocore - version: 1.8.21 - depends: ['docutils >=0.10', 'jmespath >=0.7.1,<1.0.0', 'python-dateutil >=2.1,<3.0.0'] - - name: botocore - version: 1.8.46 - depends: ['docutils >=0.10', 'jmespath >=0.7.1,<1.0.0', 'python-dateutil >=2.1,<3.0.0'] - - name: botocore - version: 1.8.5 - depends: ['docutils >=0.10', 'jmespath >=0.7.1,<1.0.0', 'python-dateutil >=2.1,<3.0.0'] - - name: botocore - version: 1.9.18 - depends: ['docutils >=0.10', 'jmespath >=0.7.1,<1.0.0', 'python-dateutil >=2.1,<2.7.0'] - - certifi 2017.11.5 - - certifi 2017.7.27.1 - - certifi 2018.1.18 - - certifi 2018.4.16 - - certifi 2018.8.13 - # cffi is a bundled module in PyPy and causes resolution errors if pip - # tries to installed it. Give it a different name since we are simply - # checking the graph anyway and the identifier doesn't really matter. - - name: cffi_not_really - version: 1.10.0 - depends: ['pycparser'] - - name: cffi_not_really - version: 1.11.2 - depends: ['pycparser'] - - name: cffi_not_really - version: 1.11.4 - depends: ['pycparser'] - - name: cffi_not_really - version: 1.11.5 - depends: ['pycparser'] - - chardet 3.0.4 - - click 6.7 - - cloudpickle 0.4.0 - - cloudpickle 0.4.2 - - cloudpickle 0.5.2 - - cloudpickle 0.5.3 - - colorama 0.3.9 - - configparser 3.5.0 - - constantly 15.1.0 - - contextlib2 0.5.5 - - coverage 4.4.2 - - coverage 4.5.1 - - name: cryptography - version: 2.0.3 - depends: ['asn1crypto >=0.21.0', 'cffi_not_really >=1.7', 'idna >=2.1', 'openssl 1.0.*', 'six >=1.4.1'] - - name: cryptography - version: 2.1.3 - depends: ['asn1crypto >=0.21.0', 'cffi_not_really >=1.7', 'idna >=2.1', 'openssl 1.0.*', 'openssl >=1.0.2m,<1.0.3a', 'six >=1.4.1'] - - name: cryptography - version: 2.1.4 - depends: ['asn1crypto >=0.21.0', 'cffi_not_really >=1.7', 'idna >=2.1', 'openssl 1.0.*', 'openssl >=1.0.2m,<1.0.3a', 'six >=1.4.1'] - - name: cryptography - version: 2.2.1 - depends: ['asn1crypto >=0.21.0', 'cffi_not_really >=1.7', 'idna >=2.1', 'openssl 1.0.*', 'openssl >=1.0.2n,<1.0.3a', 'six >=1.4.1'] - - name: cryptography - version: 2.2.2 - depends: ['asn1crypto >=0.21.0', 'cffi_not_really >=1.7', 'idna >=2.1', 'openssl 1.0.*', 'openssl >=1.0.2o,<1.0.3a', 'six >=1.4.1'] - - name: cryptography - version: '2.3' - depends: ['asn1crypto >=0.21.0', 'cffi_not_really >=1.7', 'cryptography-vectors 2.3.*', 'idna >=2.1', 'openssl >=1.0.2o,<1.0.3a', 'six >=1.4.1'] - - cryptography-vectors 2.0.3 - - cryptography-vectors 2.1.3 - - cryptography-vectors 2.1.4 - - cryptography-vectors 2.2.1 - - cryptography-vectors 2.2.2 - - cryptography-vectors 2.3 - - name: cycler - version: 0.10.0 - depends: ['six'] - - name: cytoolz - version: 0.8.2 - depends: ['toolz >=0.8.0'] - - name: cytoolz - version: 0.9.0 - depends: ['toolz >=0.8.0'] - - name: cytoolz - version: 0.9.0.1 - depends: ['toolz >=0.8.0'] - - name: dask - version: 0.15.2 - depends: ['bokeh', 'cloudpickle >=0.2.1', 'dask-core 0.15.2.*', 'distributed >=1.16.0', 'numpy >=1.10', 'pandas >=0.19.0', 'partd >=0.3.8', 'toolz >=0.7.3'] - - name: dask - version: 0.15.3 - depends: ['bokeh', 'cloudpickle >=0.2.1', 'dask-core 0.15.3.*', 'distributed >=1.19.0', 'numpy >=1.10', 'pandas >=0.19.0', 'partd >=0.3.8', 'toolz >=0.7.3'] - - name: dask - version: 0.15.4 - depends: ['bokeh', 'cloudpickle >=0.2.1', 'dask-core 0.15.4.*', 'distributed >=1.19.0', 'numpy >=1.10', 'pandas >=0.19.0', 'partd >=0.3.8', 'toolz >=0.7.3'] - - name: dask - version: 0.16.0 - depends: ['bokeh', 'cloudpickle >=0.2.1', 'dask-core 0.16.0.*', 'distributed >=1.20.0', 'numpy >=1.10', 'pandas >=0.19.0', 'partd >=0.3.8', 'toolz >=0.7.3'] - - name: dask - version: 0.16.1 - depends: ['bokeh', 'cloudpickle >=0.2.1', 'dask-core 0.16.1.*', 'distributed >=1.20.0', 'numpy >=1.10', 'pandas >=0.19.0', 'partd >=0.3.8', 'toolz >=0.7.3'] - - name: dask - version: 0.17.0 - depends: ['bokeh', 'cloudpickle >=0.2.1', 'dask-core 0.17.0.*', 'distributed >=1.21.0', 'numpy >=1.10', 'pandas >=0.19.0', 'partd >=0.3.8', 'toolz >=0.7.3'] - - name: dask - version: 0.17.1 - depends: ['bokeh', 'cloudpickle >=0.2.1', 'dask-core 0.17.1.*', 'distributed >=1.21.1', 'numpy >=1.10', 'pandas >=0.19.0', 'partd >=0.3.8', 'toolz >=0.7.3'] - - name: dask - version: 0.17.2 - depends: ['bokeh', 'cloudpickle >=0.2.1', 'cytoolz >=0.7.3', 'dask-core 0.17.2.*', 'distributed >=1.21.0', 'numpy >=1.10.4', 'pandas >=0.19.0', 'partd >=0.3.8', 'toolz >=0.7.3'] - - name: dask - version: 0.17.3 - depends: ['bokeh', 'cloudpickle >=0.2.1', 'cytoolz >=0.7.3', 'dask-core 0.17.3.*', 'distributed >=1.21.0', 'numpy >=1.11.0', 'pandas >=0.19.0', 'partd >=0.3.8', 'toolz >=0.7.3'] - - name: dask - version: 0.17.4 - depends: ['bokeh', 'cloudpickle >=0.2.1', 'cytoolz >=0.7.3', 'dask-core 0.17.4.*', 'distributed >=1.21.0', 'numpy >=1.11.0', 'pandas >=0.19.0', 'partd >=0.3.8', 'toolz >=0.7.3'] - - name: dask - version: 0.17.5 - depends: ['bokeh', 'cloudpickle >=0.2.1', 'cytoolz >=0.7.3', 'dask-core 0.17.5.*', 'distributed >=1.21.0', 'numpy >=1.11.0', 'pandas >=0.19.0', 'partd >=0.3.8', 'toolz >=0.7.3'] - - name: dask - version: 0.18.0 - depends: ['bokeh', 'cloudpickle >=0.2.1', 'cytoolz >=0.7.3', 'dask-core 0.18.0.*', 'distributed >=1.22.0', 'numpy >=1.11.0', 'pandas >=0.19.0', 'partd >=0.3.8', 'toolz >=0.7.3'] - - name: dask - version: 0.18.1 - depends: ['bokeh', 'cloudpickle >=0.2.1', 'cytoolz >=0.7.3', 'dask-core 0.18.1.*', 'distributed >=1.22.0', 'numpy >=1.11.0', 'pandas >=0.19.0', 'partd >=0.3.8', 'toolz >=0.7.3'] - - name: dask - version: 0.18.2 - depends: ['bokeh', 'cloudpickle >=0.2.1', 'cytoolz >=0.7.3', 'dask-core 0.18.2.*', 'distributed >=1.22.0', 'numpy >=1.11.0', 'pandas >=0.19.0', 'partd >=0.3.8', 'toolz >=0.7.3'] - - dask-core 0.15.2 - - dask-core 0.15.3 - - dask-core 0.15.4 - - dask-core 0.16.0 - - dask-core 0.16.1 - - dask-core 0.17.0 - - dask-core 0.17.1 - - dask-core 0.17.2 - - dask-core 0.17.3 - - dask-core 0.17.4 - - dask-core 0.17.5 - - dask-core 0.18.0 - - dask-core 0.18.1 - - dask-core 0.18.2 - - decorator 4.1.2 - - decorator 4.2.1 - - decorator 4.3.0 - - dill 0.2.7.1 - - dill 0.2.8.2 - - name: distributed - version: 1.18.3 - depends: ['click >=6.6', 'cloudpickle >=0.2.2', 'dask-core >=0.15.2', 'msgpack-python', 'psutil', 'six', 'sortedcontainers', 'tblib', 'toolz >=0.7.4', 'tornado >=4.5.1', 'zict >=0.1.2'] - - name: distributed - version: 1.19.1 - depends: ['click >=6.6', 'cloudpickle >=0.2.2', 'dask-core >=0.15.2', 'msgpack-python', 'psutil', 'six', 'sortedcontainers', 'tblib', 'toolz >=0.7.4', 'tornado >=4.5.1', 'zict >=0.1.3'] - - name: distributed - version: 1.20.0 - depends: ['click >=6.6', 'cloudpickle >=0.2.2', 'dask-core >=0.16.0', 'msgpack-python', 'psutil', 'pyyaml', 'six', 'sortedcontainers', 'tblib', 'toolz >=0.7.4', 'tornado >=4.5.1', 'zict >=0.1.3'] - - name: distributed - version: 1.20.1 - depends: ['click >=6.6', 'cloudpickle >=0.2.2', 'dask-core >=0.16.0', 'msgpack-python', 'psutil', 'pyyaml', 'six', 'sortedcontainers', 'tblib', 'toolz >=0.7.4', 'tornado >=4.5.1', 'zict >=0.1.3'] - - name: distributed - version: 1.20.2 - depends: ['click >=6.6', 'cloudpickle >=0.2.2', 'dask-core >=0.16.0', 'msgpack-python', 'psutil', 'pyyaml', 'six', 'sortedcontainers', 'tblib', 'toolz >=0.7.4', 'tornado >=4.5.1', 'zict >=0.1.3'] - - name: distributed - version: 1.21.0 - depends: ['click >=6.6', 'cloudpickle >=0.2.2', 'dask-core >=0.17.0', 'msgpack-python', 'psutil', 'pyyaml', 'six', 'sortedcontainers', 'tblib', 'toolz >=0.7.4', 'tornado >=4.5.1', 'zict >=0.1.3'] - - name: distributed - version: 1.21.1 - depends: ['click >=6.6', 'cloudpickle >=0.2.2', 'dask-core >=0.17.0', 'msgpack-python', 'psutil', 'pyyaml', 'six', 'sortedcontainers', 'tblib', 'toolz >=0.7.4', 'tornado >=4.5.1', 'zict >=0.1.3'] - - name: distributed - version: 1.21.2 - depends: ['click >=6.6', 'cloudpickle >=0.2.2', 'dask-core >=0.17.0', 'msgpack-python', 'psutil', 'pyyaml', 'six', 'sortedcontainers', 'tblib', 'toolz >=0.7.4', 'tornado >=4.5.1', 'zict >=0.1.3'] - - name: distributed - version: 1.21.3 - depends: ['click >=6.6', 'cloudpickle >=0.2.2', 'cytoolz >=0.7.4', 'dask-core >=0.17.0', 'msgpack-python', 'psutil', 'pyyaml', 'six', 'sortedcontainers', 'tblib', 'toolz >=0.7.4', 'tornado >=4.5.1', 'zict >=0.1.3'] - - name: distributed - version: 1.21.4 - depends: ['click >=6.6', 'cloudpickle >=0.2.2', 'cytoolz >=0.7.4', 'dask-core >=0.17.0', 'msgpack-python', 'psutil', 'pyyaml', 'six', 'sortedcontainers', 'tblib', 'toolz >=0.7.4', 'tornado >=4.5.1', 'zict >=0.1.3'] - - name: distributed - version: 1.21.5 - depends: ['click >=6.6', 'cloudpickle >=0.2.2', 'cytoolz >=0.7.4', 'dask-core >=0.17.0', 'msgpack-python', 'psutil', 'pyyaml', 'six', 'sortedcontainers', 'tblib', 'toolz >=0.7.4', 'tornado >=4.5.1', 'zict >=0.1.3'] - - name: distributed - version: 1.21.6 - depends: ['click >=6.6', 'cloudpickle >=0.2.2', 'cytoolz >=0.7.4', 'dask-core >=0.17.0', 'msgpack-python', 'psutil', 'pyyaml', 'six', 'sortedcontainers', 'tblib', 'toolz >=0.7.4', 'tornado >=4.5.1', 'zict >=0.1.3'] - - name: distributed - version: 1.21.8 - depends: ['click >=6.6', 'cloudpickle >=0.2.2', 'cytoolz >=0.7.4', 'dask-core >=0.17.0', 'msgpack-python', 'psutil', 'pyyaml', 'six', 'sortedcontainers', 'tblib', 'toolz >=0.7.4', 'tornado >=4.5.1', 'zict >=0.1.3'] - - name: distributed - version: 1.22.0 - depends: ['click >=6.6', 'cloudpickle >=0.2.2', 'cytoolz >=0.7.4', 'dask-core >=0.18.0', 'msgpack-python', 'psutil', 'pyyaml', 'six', 'sortedcontainers', 'tblib', 'toolz >=0.7.4', 'tornado >=4.5.1', 'zict >=0.1.3'] - - name: distributed - version: 1.22.1 - depends: ['click >=6.6', 'cloudpickle >=0.2.2', 'cytoolz >=0.7.4', 'dask-core >=0.18.0', 'msgpack-python', 'psutil', 'pyyaml', 'six', 'sortedcontainers', 'tblib', 'toolz >=0.7.4', 'tornado >=4.5.1', 'zict >=0.1.3'] - - docutils 0.14 - - entrypoints 0.2.3 - - enum34 1.1.6 - - expat 2.2.4 - - expat 2.2.5 - - filelock 2.0.12 - - filelock 2.0.13 - - filelock 3.0.4 - - name: flask - version: 0.12.2 - depends: ['click >=2.0', 'itsdangerous >=0.21', 'jinja2 >=2.4', 'werkzeug >=0.7'] - - name: flask - version: 1.0.2 - depends: ['click >=5.1', 'itsdangerous >=0.24', 'jinja2 >=2.10', 'werkzeug >=0.14'] - - fribidi 1.0.2 - - fribidi 1.0.4 - - funcsigs 1.0.2 - - functools32 3.2.3.2 - - future 0.16.0 - - futures 3.1.1 - - futures 3.2.0 - - name: gevent - version: 1.2.2 - depends: ['cffi_not_really >=1.3.0', 'greenlet >=0.4.10'] - - name: gevent - version: 1.3.0 - depends: ['cffi_not_really >=1.11.5', 'greenlet >=0.4.10'] - - name: gevent - version: 1.3.2.post0 - depends: ['cffi_not_really >=1.11.5', 'greenlet >=0.4.13'] - - name: gevent - version: 1.3.3 - depends: ['cffi_not_really >=1.11.5', 'greenlet >=0.4.13'] - - name: gevent - version: 1.3.4 - depends: ['cffi_not_really >=1.11.5', 'greenlet >=0.4.13'] - - name: gevent - version: 1.3.5 - depends: ['cffi_not_really >=1.11.5', 'greenlet >=0.4.13'] - - glob2 0.5 - - glob2 0.6 - - gmp 6.1.2 - - graphite2 1.3.10 - - graphite2 1.3.11 - - greenlet 0.4.12 - - greenlet 0.4.13 - - greenlet 0.4.14 - - name: html5lib - version: '0.999999999' - depends: ['six >=1.9', 'webencodings'] - - name: html5lib - version: 1.0.1 - depends: ['six >=1.9', 'webencodings'] - - name: hyperlink - version: 18.0.0 - depends: ['idna >=2.5'] - - hypothesis 3.23.0 - - name: hypothesis - version: 3.37.0 - depends: ['attrs', 'coverage'] - - name: hypothesis - version: 3.38.5 - depends: ['attrs', 'coverage'] - - name: hypothesis - version: 3.46.0 - depends: ['attrs', 'coverage'] - - name: hypothesis - version: 3.52.0 - depends: ['attrs >=16.0.0', 'coverage'] - - name: hypothesis - version: 3.53.0 - depends: ['attrs >=16.0.0', 'coverage'] - - name: hypothesis - version: 3.56.0 - depends: ['attrs >=16.0.0', 'coverage'] - - name: hypothesis - version: 3.57.0 - depends: ['attrs >=16.0.0', 'coverage'] - - name: hypothesis - version: 3.59.1 - depends: ['attrs >=16.0.0', 'coverage'] - - name: ibis-framework - version: 0.12.0 - depends: ['impyla >=0.14.0', 'multipledispatch', 'numpy >=1.10.0', 'pandas >=0.18.1', 'psycopg2', 'python-graphviz', 'setuptools', 'six', 'sqlalchemy >=1.0.0', 'thrift', 'thriftpy <=0.3.9', 'toolz'] - - name: ibis-framework - version: 0.13.0 - depends: ['impyla >=0.14.0', 'multipledispatch', 'numpy >=1.10.0', 'pandas >=0.18.1', 'psycopg2', 'python-graphviz', 'setuptools', 'six', 'sqlalchemy >=1.0.0', 'thrift', 'thriftpy <=0.3.9', 'toolz'] - - icu 58.2 - - idna 2.6 - - idna 2.7 - - imagesize 0.7.1 - - imagesize 1.0.0 - - name: impyla - version: 0.14.0 - depends: ['bitarray', 'setuptools', 'six', 'thriftpy >=0.3.5'] - - name: impyla - version: 0.14.1 - depends: ['bitarray', 'setuptools', 'six', 'thriftpy >=0.3.5'] - - incremental 17.5.0 - - ipaddress 1.0.18 - - ipaddress 1.0.19 - - ipaddress 1.0.22 - - name: ipykernel - version: 4.6.1 - depends: ['ipython', 'jupyter_client', 'tornado >=4.0', 'traitlets >=4.1'] - - name: ipykernel - version: 4.7.0 - depends: ['ipython', 'jupyter_client', 'tornado >=4.0', 'traitlets >=4.1'] - - name: ipykernel - version: 4.8.0 - depends: ['ipython >=4.0.0', 'jupyter_client', 'tornado >=4.0', 'traitlets >=4.1'] - - name: ipykernel - version: 4.8.2 - depends: ['ipython >=4.0.0', 'jupyter_client', 'tornado >=4.0', 'traitlets >=4.1'] - - name: ipython - version: 5.4.1 - depends: ['decorator', 'pexpect', 'pickleshare', 'prompt_toolkit >=1.0.4,<2.0.0', 'pygments', 'simplegeneric >0.8', 'traitlets'] - - name: ipython - version: 5.5.0 - depends: ['decorator', 'pexpect', 'pickleshare', 'prompt_toolkit >=1.0.4,<2.0.0', 'pygments', 'simplegeneric >0.8', 'traitlets'] - - name: ipython - version: 5.6.0 - depends: ['decorator', 'pexpect', 'pickleshare', 'prompt_toolkit >=1.0.4,<2.0.0', 'pygments', 'simplegeneric >0.8', 'traitlets'] - - name: ipython - version: 5.7.0 - depends: ['backports.shutil_get_terminal_size', 'decorator', 'pathlib2', 'pexpect', 'pickleshare', 'prompt_toolkit >=1.0.4,<2.0.0', 'pygments', 'simplegeneric >0.8', 'traitlets'] - - name: ipython - version: 5.8.0 - depends: ['decorator', 'pexpect', 'pickleshare', 'prompt_toolkit >=1.0.4,<2.0.0', 'pygments', 'simplegeneric >0.8', 'traitlets'] - - name: ipython - version: 6.1.0 - depends: ['decorator', 'jedi >=0.10', 'pexpect', 'pickleshare', 'prompt_toolkit >=1.0.4,<2.0.0', 'pygments', 'simplegeneric >0.8', 'traitlets'] - - name: ipython - version: 6.2.1 - depends: ['decorator', 'jedi >=0.10', 'pexpect', 'pickleshare', 'prompt_toolkit >=1.0.4,<2.0.0', 'pygments', 'simplegeneric >0.8', 'traitlets'] - - name: ipython - version: 6.3.0 - depends: ['backcall', 'decorator', 'jedi >=0.10', 'pexpect', 'pickleshare', 'prompt_toolkit >=1.0.4,<2.0.0', 'pygments', 'simplegeneric >0.8', 'traitlets >=4.2'] - - name: ipython - version: 6.3.1 - depends: ['backcall', 'decorator', 'jedi >=0.10', 'pexpect', 'pickleshare', 'prompt_toolkit >=1.0.4,<2.0.0', 'pygments', 'simplegeneric >0.8', 'traitlets >=4.2'] - - name: ipython - version: 6.4.0 - depends: ['backcall', 'decorator', 'jedi >=0.10', 'pexpect', 'pickleshare', 'prompt_toolkit >=1.0.4,<2.0.0', 'pygments', 'simplegeneric >0.8', 'traitlets >=4.2'] - - name: ipython - version: 6.5.0 - depends: ['backcall', 'decorator', 'jedi >=0.10', 'pexpect', 'pickleshare', 'prompt_toolkit >=1.0.4,<2.0.0', 'pygments', 'simplegeneric >0.8', 'traitlets >=4.2'] - - name: ipython-notebook - version: 0.13.2 - depends: ['ipython 0.13.2', 'pyzmq 2.2.0.1', 'tornado'] - - name: ipython-notebook - version: 1.0.0 - depends: ['ipython 1.0.0', 'pyzmq 2.2.0.1', 'tornado'] - - name: ipython-notebook - version: 1.1.0 - depends: ['ipython 1.1.0', 'jinja2', 'pyzmq 2.2.0.1', 'tornado'] - - name: ipython-notebook - version: 2.0.0 - depends: ['ipython 2.0.0', 'jinja2', 'pyzmq 14.*', 'tornado'] - - name: ipython-notebook - version: 2.1.0 - depends: ['ipython 2.1.0', 'jinja2', 'pyzmq 14.*', 'tornado'] - - name: ipython-notebook - version: 2.2.0 - depends: ['ipython 2.2.0', 'jinja2', 'pyzmq 14.*', 'tornado'] - - name: ipython-notebook - version: 2.3.0 - depends: ['ipython 2.3.0', 'jinja2', 'pyzmq 14.*', 'tornado'] - - name: ipython-notebook - version: 2.3.1 - depends: ['ipython 2.3.1', 'jinja2', 'pyzmq 14.*', 'tornado'] - - name: ipython-notebook - version: 2.4.1 - depends: ['ipython 2.4.1', 'jinja2', 'pyzmq 14.*', 'tornado'] - - name: ipython-notebook - version: 3.0.0 - depends: ['ipython 3.0.0', 'jinja2', 'jsonschema 2.4.0', 'mistune', 'pygments', 'pyzmq 14.*', 'terminado 0.5', 'tornado'] - - name: ipython-notebook - version: 3.1.0 - depends: ['ipython 3.1.0', 'jinja2', 'jsonschema 2.4.0', 'mistune', 'pygments', 'pyzmq 14.*', 'terminado 0.5', 'tornado'] - - name: ipython-notebook - version: 3.2.0 - depends: ['ipython 3.2.0', 'jinja2', 'jsonschema 2.4.0', 'mistune', 'pygments', 'pyzmq 14.*', 'terminado 0.5', 'tornado'] - - name: ipython-notebook - version: 3.2.1 - depends: ['ipython 3.2.1', 'jinja2', 'jsonschema 2.4.0', 'mistune', 'pygments', 'pyzmq 14.*', 'terminado 0.5', 'tornado'] - - name: ipython-notebook - version: 4.0.4 - depends: ['notebook'] - - ipython_genutils 0.2.0 - - name: ipywidgets - version: 7.0.0 - depends: ['ipykernel >=4.5.1', 'ipython', 'nbformat >=4.2.0', 'traitlets >=4.3.1', 'widgetsnbextension >=3.0.0'] - - name: ipywidgets - version: 7.0.5 - depends: ['ipykernel >=4.5.1', 'ipython', 'nbformat >=4.2.0', 'traitlets >=4.3.1', 'widgetsnbextension >=3.0.0'] - - name: ipywidgets - version: 7.1.0 - depends: ['ipykernel >=4.5.1', 'ipython', 'nbformat >=4.2.0', 'traitlets >=4.3.1', 'widgetsnbextension >=3.0.0'] - - name: ipywidgets - version: 7.1.1 - depends: ['ipykernel >=4.5.1', 'ipython >=4.0.0', 'nbformat >=4.2.0', 'traitlets >=4.3.1,<5.0.0', 'widgetsnbextension >=3.1.0,<4.0'] - - name: ipywidgets - version: 7.1.2 - depends: ['ipykernel >=4.5.1', 'ipython >=4.0.0', 'nbformat >=4.2.0', 'traitlets >=4.3.1,<5.0.0', 'widgetsnbextension >=3.1.0,<4.0'] - - name: ipywidgets - version: 7.2.0 - depends: ['ipykernel >=4.5.1', 'ipython >=4.0.0', 'nbformat >=4.2.0', 'traitlets >=4.3.1,<5.0.0', 'widgetsnbextension >=3.2.0,<4.0.0'] - - name: ipywidgets - version: 7.2.1 - depends: ['ipykernel >=4.5.1', 'ipython >=4.0.0', 'nbformat >=4.2.0', 'traitlets >=4.3.1,<5.0.0', 'widgetsnbextension >=3.2.0,<4.0.0'] - - name: ipywidgets - version: 7.3.0 - depends: ['ipykernel >=4.5.1', 'ipython >=4.0.0', 'nbformat >=4.2.0', 'traitlets >=4.3.1,<5.0.0', 'widgetsnbextension >=3.3.0,<3.4.0'] - - name: ipywidgets - version: 7.3.1 - depends: ['ipykernel >=4.5.1', 'ipython >=4.0.0', 'nbformat >=4.2.0', 'traitlets >=4.3.1,<5.0.0', 'widgetsnbextension >=3.3.0,<3.4.0'] - - name: ipywidgets - version: 7.4.0 - depends: ['ipykernel >=4.5.1', 'ipython >=4.0.0', 'nbformat >=4.2.0', 'traitlets >=4.3.1,<5.0.0', 'widgetsnbextension >=3.4.0,<3.5.0'] - - itsdangerous 0.24 - - jedi 0.10.2 - - name: jedi - version: 0.11.0 - depends: ['parso ==0.1.0'] - - name: jedi - version: 0.11.1 - depends: ['numpydoc', 'parso >=0.1.0,<0.2'] - - name: jedi - version: 0.12.0 - depends: ['parso >=0.2.0'] - - name: jedi - version: 0.12.1 - depends: ['parso >=0.3.0'] - - name: jinja2 - version: '2.10' - depends: ['markupsafe >=0.23', 'setuptools'] - - name: jinja2 - version: 2.9.6 - depends: ['markupsafe >=0.23', 'setuptools'] - - jmespath 0.9.3 - - jpeg 9b - - name: jsonschema - version: 2.6.0 - depends: ['setuptools'] - - name: jupyter - version: 1.0.0 - depends: ['ipykernel', 'ipywidgets', 'jupyter_console', 'nbconvert', 'notebook', 'qtconsole'] - - name: jupyter_client - version: 5.1.0 - depends: ['jupyter_core', 'python-dateutil >=2.1', 'pyzmq >=13', 'traitlets'] - - name: jupyter_client - version: 5.2.1 - depends: ['jupyter_core', 'python-dateutil >=2.1', 'pyzmq >=13', 'traitlets'] - - name: jupyter_client - version: 5.2.2 - depends: ['jupyter_core', 'python-dateutil >=2.1', 'pyzmq >=13', 'tornado', 'traitlets'] - - name: jupyter_client - version: 5.2.3 - depends: ['jupyter_core', 'python-dateutil >=2.1', 'pyzmq >=13', 'tornado', 'traitlets'] - - name: jupyter_console - version: 5.2.0 - depends: ['ipykernel', 'ipython', 'jupyter_client', 'pexpect', 'prompt_toolkit', 'pygments'] - - name: jupyter_core - version: 4.3.0 - depends: ['traitlets'] - - name: jupyter_core - version: 4.4.0 - depends: ['traitlets'] - - kiwisolver 1.0.0 - - kiwisolver 1.0.1 - - lazy-object-proxy 1.3.1 - - llvmlite 0.20.0 - - llvmlite 0.21.0 - - llvmlite 0.22.0 - - locket 0.2.0 - - name: logilab-common - version: 1.4.1 - depends: ['setuptools', 'six >=1.4.0'] - - make 4.2.1 - - markupsafe 1.0 - - name: matplotlib - version: 2.0.2 - depends: ['cycler >=0.10', 'numpy', 'pyparsing', 'pyqt 5.6.*', 'python-dateutil', 'pytz', 'setuptools', 'tornado'] - - name: matplotlib - version: 2.1.0 - depends: ['cycler >=0.10', 'numpy', 'pyparsing', 'pyqt 5.6.*', 'python-dateutil', 'pytz', 'setuptools', 'tornado'] - - name: matplotlib - version: 2.1.1 - depends: ['cycler >=0.10', 'numpy', 'pyparsing', 'pyqt 5.6.*', 'python-dateutil', 'pytz', 'setuptools', 'tornado'] - - name: matplotlib - version: 2.1.2 - depends: ['cycler >=0.10', 'numpy', 'pyparsing', 'pyqt 5.6.*', 'python-dateutil', 'pytz', 'setuptools', 'tornado'] - - name: matplotlib - version: 2.2.0 - depends: ['cycler >=0.10', 'numpy', 'pyparsing', 'pyqt 5.6.*', 'python-dateutil', 'pytz', 'setuptools', 'tornado'] - - name: matplotlib - version: 2.2.2 - depends: ['cycler >=0.10', 'numpy', 'pyparsing', 'pyqt >=5.6,<6.0a0', 'python-dateutil', 'pytz', 'setuptools', 'tornado'] - - name: matplotlib - version: 2.2.3 - depends: ['cycler >=0.10', 'numpy', 'pyparsing', 'pyqt 5.9.*', 'python-dateutil', 'pytz', 'setuptools', 'tornado'] - - mistune 0.7.4 - - mistune 0.8.1 - - mistune 0.8.3 - - msgpack-python 0.4.8 - - msgpack-python 0.5.1 - - msgpack-python 0.5.5 - - msgpack-python 0.5.6 - - multipledispatch 0.4.9 - - multipledispatch 0.5.0 - - name: multipledispatch - version: 0.6.0 - depends: ['six'] - - name: nbconvert - version: 5.3.1 - depends: ['bleach', 'entrypoints >=0.2.2', 'jinja2', 'jupyter_client >=4.2', 'jupyter_core', 'mistune >0.6', 'nbformat', 'pandoc', 'pandocfilters >=1.4.1', 'pygments', 'testpath', 'traitlets'] - - name: nbformat - version: 4.4.0 - depends: ['ipython_genutils', 'jsonschema >=2.4,!=2.5.0', 'jupyter_core', 'traitlets >=4.1'] - - ncurses 6.0 - - ncurses 6.1 - - name: nose - version: 1.3.7 - depends: ['setuptools'] - - name: notebook - version: 5.0.0 - depends: ['ipykernel', 'ipython_genutils', 'jinja2', 'jupyter_client', 'jupyter_core', 'nbconvert', 'nbformat', 'terminado >=0.3.3', 'tornado >=4', 'traitlets >=4.3'] - - name: notebook - version: 5.1.0 - depends: ['ipykernel', 'ipython_genutils', 'jinja2', 'jupyter_client', 'jupyter_core', 'nbconvert', 'nbformat', 'terminado >=0.3.3', 'tornado >=4', 'traitlets >=4.3'] - - name: notebook - version: 5.2.0 - depends: ['ipykernel', 'ipython_genutils', 'jinja2', 'jupyter_client', 'jupyter_core', 'nbconvert', 'nbformat', 'terminado >=0.3.3', 'tornado >=4', 'traitlets >=4.3'] - - name: notebook - version: 5.2.1 - depends: ['ipykernel', 'ipython_genutils', 'jinja2', 'jupyter_client', 'jupyter_core', 'nbconvert', 'nbformat', 'terminado >=0.3.3', 'tornado >=4', 'traitlets >=4.3'] - - name: notebook - version: 5.2.2 - depends: ['ipykernel', 'ipython_genutils', 'jinja2', 'jupyter_client', 'jupyter_core', 'nbconvert', 'nbformat', 'terminado >=0.3.3', 'tornado >=4', 'traitlets >=4.3'] - - name: notebook - version: 5.3.1 - depends: ['ipykernel', 'ipython_genutils', 'jinja2', 'jupyter_client >=5.2.0', 'jupyter_core >=4.4.0', 'nbconvert', 'nbformat', 'send2trash', 'terminado >=0.8.1', 'tornado >=4', 'traitlets >=4.2.1'] - - name: notebook - version: 5.4.0 - depends: ['ipykernel', 'ipython_genutils', 'jinja2', 'jupyter_client >=5.2.0', 'jupyter_core >=4.4.0', 'nbconvert', 'nbformat', 'send2trash', 'terminado >=0.8.1', 'tornado >=4', 'traitlets >=4.2.1'] - - name: notebook - version: 5.4.1 - depends: ['ipykernel', 'ipython_genutils', 'jinja2', 'jupyter_client >=5.2.0', 'jupyter_core >=4.4.0', 'nbconvert', 'nbformat', 'send2trash', 'terminado >=0.8.1', 'tornado >=4', 'traitlets >=4.2.1'] - - name: notebook - version: 5.5.0 - depends: ['ipykernel', 'ipython_genutils', 'jinja2', 'jupyter_client >=5.2.0', 'jupyter_core >=4.4.0', 'nbconvert', 'nbformat', 'pyzmq >=17', 'send2trash', 'terminado >=0.8.1', 'tornado >=4', 'traitlets >=4.2.1'] - - name: notebook - version: 5.6.0 - depends: ['ipykernel', 'ipython_genutils', 'jinja2', 'jupyter_client >=5.2.0', 'jupyter_core >=4.4.0', 'nbconvert', 'nbformat', 'prometheus_client', 'pyzmq >=17', 'send2trash', 'terminado >=0.8.1', 'tornado >=4', 'traitlets >=4.2.1'] - - numpy 1.11.3 - - numpy 1.12.1 - - numpy 1.13.1 - - numpy 1.13.3 - - numpy 1.14.0 - - numpy 1.14.1 - - numpy 1.14.2 - - numpy 1.14.3 - - numpy 1.14.4 - - numpy 1.14.5 - - numpy 1.15.0 - - numpy 1.9.3 - - name: numpydoc - version: 0.7.0 - depends: ['sphinx'] - - name: numpydoc - version: 0.8.0 - depends: ['sphinx'] - - name: openssl - version: 1.0.2l - depends: ['ca-certificates'] - - name: openssl - version: 1.0.2m - depends: ['ca-certificates'] - - name: openssl - version: 1.0.2n - depends: ['ca-certificates'] - - name: openssl - version: 1.0.2o - depends: ['ca-certificates'] - - name: openssl - version: 1.0.2p - depends: ['ca-certificates'] - - name: packaging - version: '16.8' - depends: ['pyparsing', 'six'] - - name: packaging - version: '17.1' - depends: ['pyparsing', 'six'] - - name: pandas - version: 0.20.3 - depends: ['numpy >=1.9', 'python-dateutil', 'pytz'] - - name: pandas - version: 0.21.0 - depends: ['numpy >=1.9.3,<2.0a0', 'python-dateutil', 'pytz'] - - name: pandas - version: 0.21.1 - depends: ['numpy >=1.9.3,<2.0a0', 'python-dateutil', 'pytz'] - - name: pandas - version: 0.22.0 - depends: ['numpy >=1.9.3,<2.0a0', 'python-dateutil', 'pytz'] - - name: pandas - version: 0.23.0 - depends: ['numpy >=1.9.3,<2.0a0', 'python-dateutil', 'pytz'] - - name: pandas - version: 0.23.1 - depends: ['numpy >=1.9.3,<2.0a0', 'python-dateutil >=2.5.*', 'pytz'] - - name: pandas - version: 0.23.2 - depends: ['numpy >=1.11.3,<2.0a0', 'python-dateutil >=2.5.*', 'pytz'] - - name: pandas - version: 0.23.3 - depends: ['numpy >=1.11.3,<2.0a0', 'python-dateutil >=2.5.*', 'pytz'] - - name: pandas - version: 0.23.4 - depends: ['numpy >=1.11.3,<2.0a0', 'python-dateutil >=2.5.*', 'pytz'] - - pandocfilters 1.4.2 - - parso 0.1.0 - - parso 0.1.1 - - parso 0.2.0 - - parso 0.2.1 - - parso 0.3.0 - - parso 0.3.1 - - name: partd - version: 0.3.8 - depends: ['locket', 'toolz'] - - patchelf 0.9 - - path.py 10.3.1 - - path.py 10.5 - - path.py 11.0 - - path.py 11.0.1 - - name: pathlib2 - version: 2.3.0 - depends: ['six'] - - name: pathlib2 - version: 2.3.2 - depends: ['six'] - - pcre 8.41 - - pcre 8.42 - - perl 5.26.2 - - name: perl-app-cpanminus - version: '1.7039' - depends: ['perl 5.22.0*'] - - name: perl-encode-locale - version: '1.05' - depends: ['perl >=5.26.2,<5.27.0a0'] - - name: pexpect - version: 4.2.1 - depends: ['ptyprocess >=0.5'] - - name: pexpect - version: 4.3.0 - depends: ['ptyprocess >=0.5'] - - name: pexpect - version: 4.3.1 - depends: ['ptyprocess >=0.5'] - - name: pexpect - version: 4.4.0 - depends: ['ptyprocess >=0.5'] - - name: pexpect - version: 4.5.0 - depends: ['ptyprocess >=0.5'] - - name: pexpect - version: 4.6.0 - depends: ['ptyprocess >=0.5'] - - pickleshare 0.7.4 - - name: pip - version: 10.0.1 - depends: ['setuptools', 'wheel'] - - name: pip - version: 9.0.1 - depends: ['setuptools', 'wheel'] - - name: pip - version: 9.0.3 - depends: ['setuptools', 'wheel'] - - pixman 0.34.0 - - pkginfo 1.4.1 - - pkginfo 1.4.2 - - ply 3.10 - - ply 3.11 - - name: prometheus_client - version: 0.2.0 - depends: ['twisted'] - - name: prometheus_client - version: 0.3.0 - depends: ['twisted'] - - name: prometheus_client - version: 0.3.1 - depends: ['twisted'] - - name: prompt_toolkit - version: 1.0.15 - depends: ['pygments', 'six >=1.9.0', 'wcwidth'] - - name: prompt_toolkit - version: 2.0.2 - depends: ['pygments', 'six >=1.9.0', 'wcwidth'] - - name: prompt_toolkit - version: 2.0.3 - depends: ['pygments', 'six >=1.9.0', 'wcwidth'] - - name: prompt_toolkit - version: 2.0.4 - depends: ['pygments', 'six >=1.9.0', 'wcwidth'] - - psutil 5.2.2 - - psutil 5.3.1 - - psutil 5.4.0 - - psutil 5.4.1 - - psutil 5.4.3 - - psutil 5.4.5 - - psutil 5.4.6 - - psycopg2 2.7.3.1 - - psycopg2 2.7.3.2 - - psycopg2 2.7.4 - - psycopg2 2.7.5 - - ptyprocess 0.5.2 - - ptyprocess 0.6.0 - - pyasn1 0.3.7 - - pyasn1 0.4.2 - - pyasn1 0.4.3 - - pyasn1 0.4.4 - - name: pyasn1-modules - version: 0.2.1 - depends: ['pyasn1 >=0.4.1,<0.5.0'] - - name: pyasn1-modules - version: 0.2.2 - depends: ['pyasn1 >=0.4.1,<0.5.0'] - - pycosat 0.6.2 - - pycosat 0.6.3 - - pycparser 2.18 - - name: pygments - version: 2.2.0 - depends: ['setuptools'] - - pympler 0.5 - - name: pyopenssl - version: 17.2.0 - depends: ['cryptography >=1.9', 'six >=1.5.2'] - - name: pyopenssl - version: 17.4.0 - depends: ['cryptography >=1.9', 'six >=1.5.2'] - - name: pyopenssl - version: 17.5.0 - depends: ['cryptography >=2.1.4', 'six >=1.5.2'] - - name: pyopenssl - version: 18.0.0 - depends: ['cryptography >=2.2.1', 'six >=1.5.2'] - - pyparsing 2.2.0 - - name: pyqt - version: 5.6.0 - depends: ['qt 5.6.*', 'sip 4.18.*'] - - name: pyqt - version: 5.9.2 - depends: ['dbus >=1.13.2,<2.0a0', 'qt 5.9.*', 'qt >=5.9.6,<5.10.0a0', 'sip >=4.19.4'] - - pysocks 1.6.7 - - pysocks 1.6.8 - - name: python-dateutil - version: 2.6.1 - depends: ['six'] - - name: python-dateutil - version: 2.7.0 - depends: ['six >=1.5'] - - name: python-dateutil - version: 2.7.2 - depends: ['six >=1.5'] - - name: python-dateutil - version: 2.7.3 - depends: ['six >=1.5'] - - name: python-digest - version: 1.1.1 - depends: ['cryptography <2.2'] - - python-graphviz 0.8.2 - - python-graphviz 0.8.3 - - python-graphviz 0.8.4 - - pytz 2017.2 - - pytz 2017.3 - - pytz 2018.3 - - pytz 2018.4 - - pytz 2018.5 - - pyyaml 3.12 - - pyyaml 3.13 - - pyzmq 16.0.2 - - pyzmq 16.0.3 - - pyzmq 17.0.0 - - pyzmq 17.1.0 - - pyzmq 17.1.2 - - name: qtconsole - version: 4.3.1 - depends: ['ipykernel >=4.1', 'jupyter_client >=4.1', 'jupyter_core', 'pygments', 'pyqt', 'traitlets'] - - name: qtconsole - version: 4.4.0 - depends: ['ipykernel >=4.1', 'jupyter_client >=4.1', 'jupyter_core', 'pygments', 'pyqt >=5.9.2,<5.10.0a0', 'traitlets'] - - redis 4.0.10 - - redis 4.0.2 - - redis 4.0.8 - - redis 4.0.9 - - redis-py 2.10.6 - - name: requests - version: 2.18.4 - depends: ['certifi >=2017.4.17', 'chardet >=3.0.2,<3.1.0', 'idna >=2.5,<2.7', 'urllib3 >=1.21.1,<1.23'] - - name: requests - version: 2.19.1 - depends: ['certifi >=2017.4.17', 'chardet >=3.0.2,<3.1.0', 'idna >=2.5,<2.8', 'urllib3 >=1.21.1,<1.24'] - - name: ruamel_yaml - version: 0.11.14 - depends: ['yaml'] - - name: ruamel_yaml - version: 0.15.35 - depends: ['yaml', 'yaml >=0.1.7,<0.2.0a0'] - - name: ruamel_yaml - version: 0.15.37 - depends: ['yaml >=0.1.7,<0.2.0a0'] - - name: ruamel_yaml - version: 0.15.40 - depends: ['yaml >=0.1.7,<0.2.0a0'] - - name: ruamel_yaml - version: 0.15.42 - depends: ['yaml >=0.1.7,<0.2.0a0'] - - name: ruamel_yaml - version: 0.15.46 - depends: ['yaml >=0.1.7,<0.2.0a0'] - - name: s3fs - version: 0.1.3 - depends: ['boto3'] - - name: s3fs - version: 0.1.4 - depends: ['boto3'] - - name: s3fs - version: 0.1.5 - depends: ['boto3'] - - name: s3transfer - version: 0.1.10 - depends: ['botocore >=1.3.0,<2.0.0'] - - name: s3transfer - version: 0.1.11 - depends: ['botocore >=1.3.0,<2.0.0'] - - name: s3transfer - version: 0.1.13 - depends: ['botocore >=1.3.0,<2.0.0'] - - scandir 1.5 - - scandir 1.6 - - scandir 1.7 - - scandir 1.8 - - scandir 1.9.0 - - name: scipy - version: 0.19.1 - depends: ['numpy >=1.9.3,<2.0a0'] - - name: scipy - version: 1.0.0 - depends: ['numpy >=1.9.3,<2.0a0'] - - name: scipy - version: 1.0.1 - depends: ['numpy >=1.9.3,<2.0a0'] - - name: scipy - version: 1.1.0 - depends: ['numpy >=1.11.3,<2.0a0'] - - send2trash 1.4.2 - - send2trash 1.5.0 - - name: service_identity - version: 17.0.0 - depends: ['attrs >=16.0.0', 'pyasn1', 'pyasn1-modules', 'pyopenssl >=0.12'] - - name: setuptools - version: 36.5.0 - depends: ['certifi'] - - name: setuptools - version: 38.4.0 - depends: ['certifi >=2016.09'] - - name: setuptools - version: 38.5.1 - depends: ['certifi >=2016.09'] - - name: setuptools - version: 39.0.1 - depends: ['certifi >=2016.09'] - - name: setuptools - version: 39.1.0 - depends: ['certifi >=2016.09'] - - name: setuptools - version: 39.2.0 - depends: ['certifi >=2016.09'] - - name: setuptools - version: 40.0.0 - depends: ['certifi >=2016.09'] - - simplegeneric 0.8.1 - - name: singledispatch - version: 3.4.0.3 - depends: ['six'] - - sip 4.18.1 - - sip 4.19.8 - - six 1.10.0 - - six 1.11.0 - - snowballstemmer 1.2.1 - - name: sortedcollections - version: 0.5.3 - depends: ['sortedcontainers'] - - name: sortedcollections - version: 0.6.1 - depends: ['sortedcontainers'] - - name: sortedcollections - version: 1.0.1 - depends: ['sortedcontainers >=2.0'] - - sortedcontainers 1.5.10 - - sortedcontainers 1.5.7 - - sortedcontainers 1.5.9 - - sortedcontainers 2.0.2 - - sortedcontainers 2.0.3 - - sortedcontainers 2.0.4 - - name: sphinx - version: 1.6.3 - depends: ['alabaster', 'babel', 'docutils', 'imagesize', 'jinja2', 'pygments', 'requests', 'six', 'snowballstemmer', 'sphinxcontrib-websupport', 'typing'] - - name: sphinx - version: 1.6.6 - depends: ['alabaster', 'babel', 'docutils', 'imagesize', 'jinja2', 'pygments', 'requests', 'six', 'snowballstemmer', 'sphinxcontrib-websupport', 'typing'] - - name: sphinx - version: 1.7.0 - depends: ['alabaster', 'babel', 'docutils', 'imagesize', 'jinja2', 'packaging', 'pygments', 'requests', 'six', 'snowballstemmer', 'sphinxcontrib-websupport', 'typing'] - - name: sphinx - version: 1.7.1 - depends: ['alabaster', 'babel', 'docutils', 'imagesize', 'jinja2', 'packaging', 'pygments', 'requests', 'six', 'snowballstemmer', 'sphinxcontrib-websupport', 'typing'] - - name: sphinx - version: 1.7.2 - depends: ['alabaster', 'babel', 'docutils', 'imagesize', 'jinja2', 'packaging', 'pygments', 'requests', 'six', 'snowballstemmer', 'sphinxcontrib-websupport', 'typing'] - - name: sphinx - version: 1.7.3 - depends: ['alabaster', 'babel', 'docutils', 'imagesize', 'jinja2', 'packaging', 'pygments', 'requests', 'six', 'snowballstemmer', 'sphinxcontrib-websupport', 'typing'] - - name: sphinx - version: 1.7.4 - depends: ['alabaster', 'babel', 'docutils', 'imagesize', 'jinja2', 'packaging', 'pygments', 'requests', 'six', 'snowballstemmer', 'sphinxcontrib-websupport', 'typing'] - - name: sphinx - version: 1.7.5 - depends: ['alabaster >=0.7,<0.8', 'babel >=1.3,!=2.0', 'docutils >=0.11', 'imagesize', 'jinja2 >=2.3', 'packaging', 'pygments >2.0', 'requests >2.0.0', 'six >=1.5', 'snowballstemmer >=1.1', 'sphinxcontrib-websupport'] - - name: sphinx - version: 1.7.6 - depends: ['alabaster >=0.7,<0.8', 'babel >=1.3,!=2.0', 'docutils >=0.11', 'imagesize', 'jinja2 >=2.3', 'packaging', 'pygments >2.0', 'requests >2.0.0', 'six >=1.5', 'snowballstemmer >=1.1', 'sphinxcontrib-websupport'] - - sphinxcontrib 1.0 - - name: sphinxcontrib-websupport - version: 1.0.1 - depends: ['sphinxcontrib'] - - name: sphinxcontrib-websupport - version: 1.1.0 - depends: ['sphinxcontrib'] - - sqlalchemy 1.1.13 - - sqlalchemy 1.2.0 - - sqlalchemy 1.2.1 - - sqlalchemy 1.2.10 - - sqlalchemy 1.2.3 - - sqlalchemy 1.2.4 - - sqlalchemy 1.2.5 - - sqlalchemy 1.2.6 - - sqlalchemy 1.2.7 - - sqlalchemy 1.2.8 - - name: ssl_match_hostname - version: 3.5.0.1 - depends: ['backports'] - - subprocess32 3.2.7 - - subprocess32 3.5.0 - - subprocess32 3.5.1 - - subprocess32 3.5.2 - - tblib 1.3.2 - - name: terminado - version: '0.6' - depends: ['ptyprocess', 'tornado >=4'] - - name: terminado - version: 0.8.1 - depends: ['ptyprocess', 'tornado >=4'] - - testpath 0.3.1 - - name: thrift - version: 0.11.0 - depends: ['six >=1.7.2'] - - thrift 0.9.3 - - name: thriftpy - version: 0.3.9 - depends: ['ply >=3.4,<4.0'] - - toolz 0.8.2 - - toolz 0.9.0 - - tornado 4.5.2 - - tornado 4.5.3 - - tornado 5.0 - - tornado 5.0.1 - - tornado 5.0.2 - - tornado 5.1 - - name: traitlets - version: 4.3.2 - depends: ['decorator', 'ipython_genutils', 'six'] - - name: twisted - version: 17.9.0 - depends: ['appdirs >=1.4.0', 'automat >=0.3.0', 'constantly >=15.1', 'cryptography >=1.5', 'hyperlink >=17.1.1', 'idna >=0.6,!=2.3', 'incremental >=16.10.1', 'pyasn1', 'pyopenssl >=16.0.0', 'service_identity', 'zope.interface >=4.0.2'] - - name: twisted - version: 18.4.0 - depends: ['appdirs >=1.4.0', 'automat >=0.3.0', 'constantly >=15.1', 'cryptography >=1.5', 'hyperlink >=17.1.1', 'idna >=0.6,!=2.3', 'incremental >=16.10.1', 'pyasn1', 'pyopenssl >=16.0.0', 'service_identity', 'zope.interface >=4.0.2'] - - name: twisted - version: 18.7.0 - depends: ['appdirs >=1.4.0', 'automat >=0.3.0', 'constantly >=15.1', 'cryptography >=1.5', 'hyperlink >=17.1.1', 'idna >=0.6,!=2.3', 'incremental >=16.10.1', 'pyasn1', 'pyopenssl >=16.0.0', 'service_identity', 'zope.interface >=4.0.2'] - - typed-ast 1.1.0 - - typing 3.6.2 - - typing 3.6.4 - - ujson 1.35 - - name: urllib3 - version: '1.22' - depends: ['certifi', 'cryptography >=1.3.4', 'idna >=2.0.0', 'pyopenssl >=0.14', 'pysocks >=1.5.6,<2.0,!=1.5.7'] - - name: urllib3 - version: '1.23' - depends: ['certifi', 'cryptography >=1.3.4', 'idna >=2.0.0', 'pyopenssl >=0.14', 'pysocks >=1.5.6,<2.0,!=1.5.7'] - - wcwidth 0.1.7 - - webencodings 0.5.1 - - werkzeug 0.12.2 - - werkzeug 0.14.1 - - name: wheel - version: 0.29.0 - depends: ['setuptools'] - - name: wheel - version: 0.30.0 - depends: ['setuptools'] - - name: wheel - version: 0.31.0 - depends: ['setuptools'] - - name: wheel - version: 0.31.1 - depends: ['setuptools'] - - name: widgetsnbextension - version: 3.0.2 - depends: ['notebook >=4.4.1'] - - name: widgetsnbextension - version: 3.0.8 - depends: ['notebook >=4.4.1'] - - name: widgetsnbextension - version: 3.1.0 - depends: ['notebook >=4.4.1'] - - name: widgetsnbextension - version: 3.1.4 - depends: ['notebook >=4.4.1'] - - name: widgetsnbextension - version: 3.2.0 - depends: ['notebook >=4.4.1'] - - name: widgetsnbextension - version: 3.2.1 - depends: ['notebook >=4.4.1'] - - name: widgetsnbextension - version: 3.3.0 - depends: ['notebook >=4.4.1'] - - name: widgetsnbextension - version: 3.3.1 - depends: ['notebook >=4.4.1'] - - name: widgetsnbextension - version: 3.4.0 - depends: ['notebook >=4.4.1'] - - wrapt 1.10.11 - - xz 5.2.3 - - xz 5.2.4 - - yaml 0.1.7 - - zeromq 4.2.2 - - zeromq 4.2.3 - - zeromq 4.2.5 - - name: zict - version: 0.1.2 - depends: ['heapdict'] - - name: zict - version: 0.1.3 - depends: ['heapdict'] - - zope 1.0 - - name: zope.interface - version: 4.4.3 - depends: ['zope'] - - name: zope.interface - version: 4.5.0 - depends: ['zope'] - -cases: -- - request: - - install: alabaster - response: - - state: - - alabaster 0.7.11 -- - request: - - install: ipython==6.3.1 - response: - - state: - - backcall 0.1.0 - - decorator 4.3.0 - - ipython 6.3.1 - - ipython_genutils 0.2.0 - - jedi 0.12.1 - - parso 0.3.1 - - pexpect 4.6.0 - - pickleshare 0.7.4 - - prompt_toolkit 1.0.15 - - ptyprocess 0.6.0 - - pygments 2.2.0 - - simplegeneric 0.8.1 - - six 1.11.0 - - traitlets 4.3.2 - - wcwidth 0.1.7 diff --git a/tests/yaml/large.yml b/tests/yaml/large.yml deleted file mode 100644 index fbb1c737eca..00000000000 --- a/tests/yaml/large.yml +++ /dev/null @@ -1,295 +0,0 @@ -# The 129 available packages have been obtained by transforming a -# conda repodata.json, and doing some manual fixes. -base: - available: - - affine 2.2.0 - - affine 2.2.1 - - asn1crypto 0.22.0 - - asn1crypto 0.23.0 - - asn1crypto 0.24.0 - - backports 1.0 - - name: backports.functools_lru_cache - version: '1.4' - depends: ['backports', 'setuptools'] - - name: backports.functools_lru_cache - version: '1.5' - depends: ['backports', 'setuptools'] - - beautifulsoup4 4.6.0 - - beautifulsoup4 4.6.1 - - beautifulsoup4 4.6.3 - - name: cachecontrol - version: 0.12.3 - depends: ['msgpack_python', 'requests'] - - name: cachecontrol - version: 0.12.4 - depends: ['msgpack_python', 'requests'] - - name: cachecontrol - version: 0.12.5 - depends: ['msgpack_python', 'requests'] - - certifi 2017.11.5 - - certifi 2017.7.27.1 - - certifi 2018.1.18 - - certifi 2018.4.16 - - certifi 2018.8.13 - # cffi is a bundled module in PyPy and causes resolution errors if pip - # tries to installed it. Give it a different name since we are simply - # checking the graph anyway and the identifier doesn't really matter. - - name: cffi_not_really - version: 1.10.0 - depends: ['pycparser'] - - name: cffi_not_really - version: 1.11.2 - depends: ['pycparser'] - - name: cffi_not_really - version: 1.11.4 - depends: ['pycparser'] - - name: cffi_not_really - version: 1.11.5 - depends: ['pycparser'] - - chardet 3.0.4 - - click 6.7 - - colorama 0.3.9 - - colour 0.1.4 - - colour 0.1.5 - - contextlib2 0.5.5 - - name: cryptography - version: 2.0.3 - depends: ['asn1crypto >=0.21.0', 'cffi_not_really >=1.7', 'idna >=2.1', 'six >=1.4.1'] - - name: cryptography - version: 2.1.3 - depends: ['asn1crypto >=0.21.0', 'cffi_not_really >=1.7', 'idna >=2.1', 'six >=1.4.1'] - - name: cryptography - version: 2.1.4 - depends: ['asn1crypto >=0.21.0', 'cffi_not_really >=1.7', 'idna >=2.1', 'six >=1.4.1'] - - name: cryptography - version: 2.2.1 - depends: ['asn1crypto >=0.21.0', 'cffi_not_really >=1.7', 'idna >=2.1', 'six >=1.4.1'] - - name: cryptography - version: '2.3' - depends: ['asn1crypto >=0.21.0', 'cffi_not_really >=1.7', 'cryptography_vectors ~=2.3', 'idna >=2.1', 'six >=1.4.1'] - - cryptography_vectors 2.0.3 - - cryptography_vectors 2.1.3 - - cryptography_vectors 2.1.4 - - cryptography_vectors 2.2.1 - - cryptography_vectors 2.2.2 - - cryptography_vectors 2.3.0 - - name: cytoolz - version: 0.8.2 - depends: ['toolz >=0.8.0'] - - name: cytoolz - version: 0.9.0 - depends: ['toolz >=0.8.0'] - - name: cytoolz - version: 0.9.0.1 - depends: ['toolz >=0.8.0'] - - distlib 0.2.5 - - distlib 0.2.6 - - distlib 0.2.7 - - enum34 1.1.6 - - filelock 2.0.12 - - filelock 2.0.13 - - filelock 3.0.4 - - future 0.16.0 - - futures 3.1.1 - - futures 3.2.0 - - glob2 0.5 - - glob2 0.6 - - name: html5lib - version: '0.999999999' - depends: ['six >=1.9', 'webencodings'] - - name: html5lib - version: 1.0.1 - depends: ['six >=1.9', 'webencodings'] - - idna 2.6 - - idna 2.7 - - ipaddress 1.0.18 - - ipaddress 1.0.19 - - ipaddress 1.0.22 - - name: jinja2 - version: '2.10' - depends: ['markupsafe >=0.23', 'setuptools'] - - name: jinja2 - version: 2.9.6 - depends: ['markupsafe >=0.23', 'setuptools'] - - lockfile 0.12.2 - - markupsafe 1.0 - - msgpack_python 0.4.8 - - msgpack_python 0.5.1 - - msgpack_python 0.5.5 - - msgpack_python 0.5.6 - - name: packaging - version: '16.8' - depends: ['pyparsing', 'six'] - - name: packaging - version: '17.1' - depends: ['pyparsing', 'six'] - - name: pip - version: 10.0.1 - depends: ['setuptools', 'wheel'] - - name: pip - version: 9.0.1 - depends: ['cachecontrol', 'colorama', 'distlib', 'html5lib', 'lockfile', 'packaging', 'progress', 'requests', 'setuptools', 'webencodings', 'wheel'] - - name: pip - version: 9.0.3 - depends: ['setuptools', 'wheel'] - - pkginfo 1.4.1 - - pkginfo 1.4.2 - - progress 1.3 - - progress 1.4 - - psutil 5.2.2 - - psutil 5.3.1 - - psutil 5.4.0 - - psutil 5.4.1 - - psutil 5.4.3 - - psutil 5.4.5 - - psutil 5.4.6 - - pycosat 0.6.2 - - pycosat 0.6.3 - - pycparser 2.18 - - name: pyopenssl - version: 17.2.0 - depends: ['cryptography >=1.9', 'six >=1.5.2'] - - name: pyopenssl - version: 17.4.0 - depends: ['cryptography >=1.9', 'six >=1.5.2'] - - name: pyopenssl - version: 17.5.0 - depends: ['cryptography >=2.1.4', 'six >=1.5.2'] - - name: pyopenssl - version: 18.0.0 - depends: ['cryptography >=2.2.1', 'six >=1.5.2'] - - pyparsing 2.2.0 - - name: pysocks - version: 1.6.7 - depends: ['win_inet_pton'] - - name: pysocks - version: 1.6.8 - depends: ['win_inet_pton'] - - pywin32 221 - - pywin32 222 - - pywin32 223 - - pyyaml 3.12 - - pyyaml 3.13 - - name: requests - version: 2.18.4 - depends: ['certifi >=2017.4.17', 'chardet >=3.0.2,<3.1.0', 'idna >=2.5,<2.7', 'urllib3 >=1.21.1,<1.23'] - - name: requests - version: 2.19.1 - depends: ['certifi >=2017.4.17', 'chardet >=3.0.2,<3.1.0', 'idna >=2.5,<2.8', 'urllib3 >=1.21.1,<1.24'] - - scandir 1.5 - - scandir 1.6 - - scandir 1.7 - - scandir 1.8 - - scandir 1.9.0 - - name: setuptools - version: 36.2.2 - depends: ['certifi', 'wincertstore'] - - name: setuptools - version: 36.5.0 - depends: ['certifi', 'wincertstore'] - - name: setuptools - version: 38.4.0 - depends: ['certifi >=2016.09', 'wincertstore >=0.2'] - - name: setuptools - version: 38.5.1 - depends: ['certifi >=2016.09', 'wincertstore >=0.2'] - - name: setuptools - version: 39.0.1 - depends: ['certifi >=2016.09', 'wincertstore >=0.2'] - - name: setuptools - version: 39.1.0 - depends: ['certifi >=2016.09', 'wincertstore >=0.2'] - - name: setuptools - version: 39.2.0 - depends: ['certifi >=2016.09', 'wincertstore >=0.2'] - - name: setuptools - version: 40.0.0 - depends: ['certifi >=2016.09', 'wincertstore >=0.2'] - - six 1.8.2 - - six 1.10.0 - - six 1.11.0 - - toolz 0.8.2 - - toolz 0.9.0 - - name: urllib3 - version: '1.22' - depends: ['certifi', 'cryptography >=1.3.4', 'idna >=2.0.0', 'pyopenssl >=0.14', 'pysocks >=1.5.6,<2.0,!=1.5.7'] - - name: urllib3 - version: '1.23' - depends: ['certifi', 'cryptography >=1.3.4', 'idna >=2.0.0', 'pyopenssl >=0.14', 'pysocks >=1.5.6,<2.0,!=1.5.7'] - - webencodings 0.5.1 - - name: wheel - version: 0.29.0 - depends: ['setuptools'] - - name: wheel - version: 0.30.0 - depends: ['setuptools'] - - name: wheel - version: 0.31.0 - depends: ['setuptools'] - - name: wheel - version: 0.31.1 - depends: ['setuptools'] - - win_inet_pton 1.0.1 - - wincertstore 0.2 - -cases: -- - request: - - install: affine - response: - - state: - - affine 2.2.1 -- - request: - - install: cryptography - response: - - state: - - asn1crypto 0.24.0 - - cffi_not_really 1.11.5 - - cryptography 2.3 - - cryptography_vectors 2.3.0 - - idna 2.7 - - pycparser 2.18 - - six 1.11.0 - skip: legacy -- - request: - - install: cachecontrol - response: - - state: - - asn1crypto 0.24.0 - - cachecontrol 0.12.5 - - certifi 2018.8.13 - - cffi_not_really 1.11.5 - - chardet 3.0.4 - - cryptography 2.3 - - cryptography_vectors 2.3.0 - - idna 2.7 - - msgpack_python 0.5.6 - - pycparser 2.18 - - pyopenssl 18.0.0 - - pysocks 1.6.8 - - requests 2.19.1 - - six 1.11.0 - - urllib3 1.23 - - win_inet_pton 1.0.1 -- - request: - - install: cytoolz - response: - - state: - - cytoolz 0.9.0.1 - - toolz 0.9.0 -- - request: - - install: ['html5lib', 'six ==1.8.2'] - response: - - state: null - error: - code: 1 - stderr: >- - Cannot install six==1.8.2, html5lib 1.0.1, six==1.8.2 and - html5lib 0.999999999 because these package versions have - conflicting dependencies. - - skip: legacy diff --git a/tests/yaml/linter.py b/tests/yaml/linter.py deleted file mode 100644 index ac17bbc41be..00000000000 --- a/tests/yaml/linter.py +++ /dev/null @@ -1,108 +0,0 @@ -import re -import sys -from pprint import pprint - -import yaml - -sys.path.insert(0, '../../src') -sys.path.insert(0, '../..') - - -def check_dict(d, required=None, optional=None): - assert isinstance(d, dict) - if required is None: - required = [] - if optional is None: - optional = [] - for key in required: - if key not in d: - sys.exit("key %r is required" % key) - allowed_keys = set(required) - allowed_keys.update(optional) - for key in d.keys(): - if key not in allowed_keys: - sys.exit("key %r is not allowed. Allowed keys are: %r" % - (key, allowed_keys)) - - -def lint_case(case, verbose=False): - from tests.functional.test_yaml import convert_to_dict - - if verbose: - print("--- linting case ---") - pprint(case) - - check_dict(case, optional=['available', 'request', 'response', 'skip']) - available = case.get("available", []) - requests = case.get("request", []) - responses = case.get("response", []) - assert isinstance(available, list) - assert isinstance(requests, list) - assert isinstance(responses, list) - assert len(requests) == len(responses) - - for package in available: - if isinstance(package, str): - package = convert_to_dict(package) - if verbose: - pprint(package) - check_dict(package, - required=['name', 'version'], - optional=['depends', 'extras']) - version = package['version'] - assert isinstance(version, str), repr(version) - - for request, response in zip(requests, responses): - check_dict(request, optional=['install', 'uninstall', 'options']) - check_dict(response, optional=['state', 'error']) - assert len(response) >= 1 - assert isinstance(response.get('state') or [], list) - error = response.get('error') - if error: - check_dict(error, optional=['code', 'stderr']) - stderr = error.get('stderr') - if stderr: - if isinstance(stderr, str): - patters = [stderr] - elif isinstance(stderr, list): - patters = stderr - else: - raise "string or list expected, found %r" % stderr - for patter in patters: - re.compile(patter, re.I) - - -def lint_yml(yml_file, verbose=False): - if verbose: - print("=== linting: %s ===" % yml_file) - assert yml_file.endswith(".yml") - with open(yml_file) as fi: - data = yaml.safe_load(fi) - if verbose: - pprint(data) - - check_dict(data, required=['cases'], optional=['base']) - base = data.get("base", {}) - cases = data["cases"] - for _, case_template in enumerate(cases): - case = base.copy() - case.update(case_template) - lint_case(case, verbose) - - -if __name__ == '__main__': - from optparse import OptionParser - - p = OptionParser(usage="usage: %prog [options] FILE ...", - description="linter for pip's yaml test FILE(s)") - - p.add_option('-v', '--verbose', - action="store_true") - - opts, args = p.parse_args() - - if len(args) < 1: - p.error('at least one argument required, try -h') - - for yml_file in args: - lint_yml(yml_file, opts.verbose) diff --git a/tests/yaml/non_pinned.yml b/tests/yaml/non_pinned.yml deleted file mode 100644 index 6e9b26c4c10..00000000000 --- a/tests/yaml/non_pinned.yml +++ /dev/null @@ -1,24 +0,0 @@ -base: - available: - - A 1.0.0; depends B < 2.0.0 - - A 2.0.0; depends B < 3.0.0 - - B 1.0.0 - - B 2.0.0 - - B 2.1.0 - - B 3.0.0 - -cases: -- - request: - - install: A >= 2.0.0 - response: - - state: - - A 2.0.0 - - B 2.1.0 -- - request: - - install: A < 2.0.0 - response: - - state: - - A 1.0.0 - - B 1.0.0 diff --git a/tests/yaml/overlap1.yml b/tests/yaml/overlap1.yml deleted file mode 100644 index 9afbb04c379..00000000000 --- a/tests/yaml/overlap1.yml +++ /dev/null @@ -1,44 +0,0 @@ -# https://medium.com/knerd/the-nine-circles-of-python-dependency-hell-481d53e3e025 -# Circle 4: Overlapping transitive dependencies -base: - available: - - myapp 0.2.4; depends fussy, capridous - - name: fussy - version: 3.8.0 - depends: ['requests >=1.2.0,<3'] - - name: capridous - version: 1.1.0 - depends: ['requests >=1.0.3,<2'] - - requests 1.0.1 - - requests 1.0.3 - - requests 1.1.0 - - requests 1.2.0 - - requests 1.3.0 - - requests 2.1.0 - - requests 3.2.0 - -cases: -- - request: - - install: myapp - response: - - state: - - capridous 1.1.0 - - fussy 3.8.0 - - myapp 0.2.4 - - requests 1.3.0 - skip: legacy -- - request: - - install: fussy - response: - - state: - - fussy 3.8.0 - - requests 2.1.0 -- - request: - - install: capridous - response: - - state: - - capridous 1.1.0 - - requests 1.3.0 diff --git a/tests/yaml/pinned.yml b/tests/yaml/pinned.yml deleted file mode 100644 index c8bd3f35dbf..00000000000 --- a/tests/yaml/pinned.yml +++ /dev/null @@ -1,29 +0,0 @@ -base: - available: - - A 1.0.0 - - A 2.0.0 - - B 1.0.0; depends A == 1.0.0 - - B 2.0.0; depends A == 2.0.0 - -cases: -- - request: - - install: B - response: - - state: - - A 2.0.0 - - B 2.0.0 -- - request: - - install: B == 2.0.0 - response: - - state: - - A 2.0.0 - - B 2.0.0 -- - request: - - install: B == 1.0.0 - response: - - state: - - A 1.0.0 - - B 1.0.0 diff --git a/tests/yaml/pip988.yml b/tests/yaml/pip988.yml deleted file mode 100644 index 1190d2a4e07..00000000000 --- a/tests/yaml/pip988.yml +++ /dev/null @@ -1,37 +0,0 @@ -# https://github.com/pypa/pip/issues/988#issuecomment-606967707 -base: - available: - - A 1.0.0; depends B >= 1.0.0, C >= 1.0.0 - - A 2.0.0; depends B >= 2.0.0, C >= 1.0.0 - - B 1.0.0; depends C >= 1.0.0 - - B 2.0.0; depends C >= 2.0.0 - - C 1.0.0 - - C 2.0.0 - -cases: -- - request: - - install: C==1.0.0 - - install: B==1.0.0 - - install: A==1.0.0 - - install: A==2.0.0 - response: - - state: - - C 1.0.0 - - state: - - B 1.0.0 - - C 1.0.0 - - state: - - A 1.0.0 - - B 1.0.0 - - C 1.0.0 - - state: - - A 2.0.0 - - B 2.0.0 - - C 2.0.0 - # for the last install (A==2.0.0) the old resolver gives - # - A 2.0.0 - # - B 2.0.0 - # - C 1.0.0 - # but because B 2.0.0 depends on C >=2.0.0 this is wrong - skip: legacy diff --git a/tests/yaml/poetry2298.yml b/tests/yaml/poetry2298.yml deleted file mode 100644 index 8b0670896ae..00000000000 --- a/tests/yaml/poetry2298.yml +++ /dev/null @@ -1,24 +0,0 @@ -# see: https://github.com/python-poetry/poetry/issues/2298 -base: - available: - - poetry 1.0.5; depends zappa == 0.51.0, sphinx == 3.0.1 - - zappa 0.51.0; depends boto3 - - sphinx 3.0.1; depends docutils - - boto3 1.4.5; depends botocore ~=1.5.0 - - botocore 1.5.92; depends docutils <0.16 - - docutils 0.16.0 - - docutils 0.15.0 - -cases: -- - request: - - install: poetry - response: - - state: - - boto3 1.4.5 - - botocore 1.5.92 - - docutils 0.15.0 - - poetry 1.0.5 - - sphinx 3.0.1 - - zappa 0.51.0 - skip: legacy diff --git a/tests/yaml/simple.yml b/tests/yaml/simple.yml deleted file mode 100644 index 8e90e605d54..00000000000 --- a/tests/yaml/simple.yml +++ /dev/null @@ -1,47 +0,0 @@ -base: - available: - - simple 0.1.0 - - simple 0.2.0 - - base 0.1.0; depends dep - - dep 0.1.0 - -cases: -- - request: - - install: simple - - uninstall: simple - response: - - state: - - simple 0.2.0 - - state: null -- - request: - - install: simple - - install: dep - response: - - state: - - simple 0.2.0 - - state: - - dep 0.1.0 - - simple 0.2.0 -- - request: - - install: base - response: - - state: - - base 0.1.0 - - dep 0.1.0 -- - request: - - install: base - options: --no-deps - response: - - state: - - base 0.1.0 -- - request: - - install: ['dep', 'simple==0.1.0'] - response: - - state: - - dep 0.1.0 - - simple 0.1.0 diff --git a/tests/yaml/trivial.yml b/tests/yaml/trivial.yml deleted file mode 100644 index 418422044e4..00000000000 --- a/tests/yaml/trivial.yml +++ /dev/null @@ -1,24 +0,0 @@ -base: - available: - - a 0.1.0 - - b 0.2.0 - - c 0.3.0 - -cases: -- - request: - - install: ['a', 'b'] - - install: c - - uninstall: ['b', 'c'] - - uninstall: a - response: - - state: - - a 0.1.0 - - b 0.2.0 - - state: - - a 0.1.0 - - b 0.2.0 - - c 0.3.0 - - state: - - a 0.1.0 - - state: null diff --git a/tools/requirements/tests.txt b/tools/requirements/tests.txt index 608d5d9f4bb..601cff2ab08 100644 --- a/tools/requirements/tests.txt +++ b/tools/requirements/tests.txt @@ -7,7 +7,6 @@ pytest-cov pytest-rerunfailures pytest-timeout pytest-xdist -pyyaml scripttest setuptools>=39.2.0 # Needed for `setuptools.wheel.Wheel` support. https://github.com/pypa/virtualenv/archive/legacy.zip#egg=virtualenv From 1068e75e9c8bd85782617cddf256c18d1911bbdf Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Mon, 22 Mar 2021 11:05:51 +0800 Subject: [PATCH 3064/3170] Drop the SCHEMES['nt_user']['scripts'] shim I dug into CPython commit history and this was fixed in 3.5, so we shouldn't need this? Let's find out... --- src/pip/_internal/locations/_sysconfig.py | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/pip/_internal/locations/_sysconfig.py b/src/pip/_internal/locations/_sysconfig.py index 93e8d40b125..e4d66d25d24 100644 --- a/src/pip/_internal/locations/_sysconfig.py +++ b/src/pip/_internal/locations/_sysconfig.py @@ -136,13 +136,6 @@ def get_scheme( python_xy = f"python{get_major_minor_version()}" paths["include"] = os.path.join(base, "include", "site", python_xy) - # Special user scripts path on Windows for compatibility to distutils. - # See ``distutils.commands.install.INSTALL_SCHEMES["nt_user"]["scripts"]``. - if scheme_name == "nt_user": - base = variables.get("userbase", sys.prefix) - python_xy = f"Python{sys.version_info.major}{sys.version_info.minor}" - paths["scripts"] = os.path.join(base, python_xy, "Scripts") - scheme = Scheme( platlib=paths["platlib"], purelib=paths["purelib"], From 38160547f982329d9853bc384ff8ebf222b9b7ac Mon Sep 17 00:00:00 2001 From: Laurent LAPORTE <laurent.laporte.pro@gmail.com> Date: Sat, 9 Jan 2021 16:22:01 +0100 Subject: [PATCH 3065/3170] run_command() raises BadCommand if the user don't have permission to run the VCS command. --- news/8418.bugfix.rst | 1 + src/pip/_internal/vcs/versioncontrol.py | 12 ++++++++++++ tests/unit/test_vcs.py | 21 +++++++++++++++++++++ 3 files changed, 34 insertions(+) create mode 100644 news/8418.bugfix.rst diff --git a/news/8418.bugfix.rst b/news/8418.bugfix.rst new file mode 100644 index 00000000000..1bcc9b78709 --- /dev/null +++ b/news/8418.bugfix.rst @@ -0,0 +1 @@ +Fix ``pip freeze`` permission denied error in order to display an understandable error message and offer solutions. diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index 7600f6fef5e..4ca3b49ef2c 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -684,6 +684,18 @@ def run_command( raise BadCommand( f'Cannot find command {cls.name!r} - do you have ' f'{cls.name!r} installed and in your PATH?') + except PermissionError: + # errno.EACCES = Permission denied + # This error occurs, for instance, when the command is installed + # only for another user. So, the current user don't have + # permission to call the other user command. + raise BadCommand( + f'No permission to execute {cls.name!r} - install it ' + f'locally, globally (ask admin), or check your PATH. ' + f'See possible solutions at ' + f'https://pip.pypa.io/en/latest/reference/pip_freeze/' + f'#issues.' + ) @classmethod def is_repository_directory(cls, path): diff --git a/tests/unit/test_vcs.py b/tests/unit/test_vcs.py index 8fe5d3e7178..6d82e139a48 100644 --- a/tests/unit/test_vcs.py +++ b/tests/unit/test_vcs.py @@ -303,6 +303,27 @@ def test_version_control__get_url_rev_and_auth__no_revision(url): assert 'an empty revision (after @)' in str(excinfo.value) +@pytest.mark.parametrize("vcs_cls", [Bazaar, Git, Mercurial, Subversion]) +@pytest.mark.parametrize( + "exc_cls, msg_re", + [ + (FileNotFoundError, r"Cannot find command '{name}'"), + (PermissionError, r"No permission to execute '{name}'"), + ], + ids=["FileNotFoundError", "PermissionError"], +) +def test_version_control__run_command__fails(vcs_cls, exc_cls, msg_re): + """ + Test that ``VersionControl.run_command()`` raises ``BadCommand`` + when the command is not found or when the user have no permission + to execute it. The error message must contains the command name. + """ + with patch("pip._internal.vcs.versioncontrol.call_subprocess") as call: + call.side_effect = exc_cls + with pytest.raises(BadCommand, match=msg_re.format(name=vcs_cls.name)): + vcs_cls.run_command([]) + + @pytest.mark.parametrize('url, expected', [ # Test http. ('bzr+http://bzr.myproject.org/MyProject/trunk/#egg=MyProject', From 35c21e936f68cf551d10c2f573418c901b9c0598 Mon Sep 17 00:00:00 2001 From: Laurent LAPORTE <laurent.laporte.pro@gmail.com> Date: Mon, 11 Jan 2021 08:56:09 +0100 Subject: [PATCH 3066/3170] Add a section in the documentation to suggest solutions to the ``pip freeze`` permission denied issue. Update link in source code. --- docs/html/reference/pip_freeze.rst | 23 +++++++++++++++++++++++ news/8418.doc.rst | 1 + src/pip/_internal/vcs/versioncontrol.py | 2 +- 3 files changed, 25 insertions(+), 1 deletion(-) create mode 100644 news/8418.doc.rst diff --git a/docs/html/reference/pip_freeze.rst b/docs/html/reference/pip_freeze.rst index 352f7d32168..ac0532b478f 100644 --- a/docs/html/reference/pip_freeze.rst +++ b/docs/html/reference/pip_freeze.rst @@ -72,3 +72,26 @@ Examples env1\bin\python -m pip freeze > requirements.txt env2\bin\python -m pip install -r requirements.txt + + +Fixing permission denied +======================== + +The purpose of this section of documentation is to provide practical suggestions to +pip users who encounter an error where ``pip freeze`` issue a permission error +during requirements info extraction. See issue: +`pip freeze returns "Permission denied: 'hg'" <https://github.com/pypa/pip/issues/8418>`_. + +When you get a "No permission to execute 'cmd'" error, where *cmd* is 'bzr', +'git', 'hg' or 'svn', it means that the VCS command exists, but you have +no permission to execute it. + +This error occurs, for instance, when the command is installed only for another user. +So, the current user don't have permission to execute the other user command. + +To solve that issue, you can: + +- install the command for yourself (local installation), +- ask admin support to install for all users (global installation), +- check and correct the PATH variable of your own environment, +- check the ACL (Access Control List) for this command. diff --git a/news/8418.doc.rst b/news/8418.doc.rst new file mode 100644 index 00000000000..6634f6cd619 --- /dev/null +++ b/news/8418.doc.rst @@ -0,0 +1 @@ +Add a section in the documentation to suggest solutions to the ``pip freeze`` permission denied issue. diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index 4ca3b49ef2c..0ba0d86c500 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -694,7 +694,7 @@ def run_command( f'locally, globally (ask admin), or check your PATH. ' f'See possible solutions at ' f'https://pip.pypa.io/en/latest/reference/pip_freeze/' - f'#issues.' + f'#fixing-permission-denied.' ) @classmethod From f162f9d849c835a1407e4ff394c08541d56ed14f Mon Sep 17 00:00:00 2001 From: Laurent LAPORTE <laurent.laporte.pro@gmail.com> Date: Mon, 11 Jan 2021 09:00:18 +0100 Subject: [PATCH 3067/3170] Correct the coding-style in `VersionControl.run_command()` to use f-string instead of `.format(**locals())`. --- src/pip/_internal/vcs/versioncontrol.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index 0ba0d86c500..cd6213bb132 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -690,11 +690,11 @@ def run_command( # only for another user. So, the current user don't have # permission to call the other user command. raise BadCommand( - f'No permission to execute {cls.name!r} - install it ' - f'locally, globally (ask admin), or check your PATH. ' - f'See possible solutions at ' - f'https://pip.pypa.io/en/latest/reference/pip_freeze/' - f'#fixing-permission-denied.' + f"No permission to execute {cls.name!r} - install it " + f"locally, globally (ask admin), or check your PATH. " + f"See possible solutions at " + f"https://pip.pypa.io/en/latest/reference/pip_freeze/" + f"#fixing-permission-denied." ) @classmethod From 724bf3df0c96c2e94b17606daeffc5a76707dd53 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Fri, 26 Mar 2021 07:01:39 +0000 Subject: [PATCH 3068/3170] Drop pytest-timeout It is incompatible with pytest-xdist and causes crashes when used with it. Also removes the marker from the only test that uses it. --- tests/functional/test_install.py | 1 - tools/requirements/tests.txt | 1 - tox.ini | 4 ++-- 3 files changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/functional/test_install.py b/tests/functional/test_install.py index 369c15a7619..2742e873e33 100644 --- a/tests/functional/test_install.py +++ b/tests/functional/test_install.py @@ -163,7 +163,6 @@ def test_pep518_with_namespace_package(script, data, common_wheels): ) -@pytest.mark.timeout(60) @pytest.mark.parametrize('command', ('install', 'wheel')) @pytest.mark.parametrize('package', ('pep518_forkbomb', 'pep518_twin_forkbombs_first', diff --git a/tools/requirements/tests.txt b/tools/requirements/tests.txt index 608d5d9f4bb..93d2272e19e 100644 --- a/tools/requirements/tests.txt +++ b/tools/requirements/tests.txt @@ -5,7 +5,6 @@ pretend pytest pytest-cov pytest-rerunfailures -pytest-timeout pytest-xdist pyyaml scripttest diff --git a/tox.ini b/tox.ini index 79586eba8c3..0b9511bf98b 100644 --- a/tox.ini +++ b/tox.ini @@ -27,7 +27,7 @@ deps = -r{toxinidir}/tools/requirements/tests.txt commands_pre = python -c 'import shutil, sys; shutil.rmtree(sys.argv[1], ignore_errors=True)' {toxinidir}/tests/data/common_wheels {[helpers]pip} wheel -w {toxinidir}/tests/data/common_wheels -r {toxinidir}/tools/requirements/tests-common_wheels.txt -commands = pytest --timeout 300 [] +commands = pytest [] install_command = {[helpers]pip} install {opts} {packages} list_dependencies_command = {[helpers]pip} freeze --all @@ -35,7 +35,7 @@ list_dependencies_command = {[helpers]pip} freeze --all basepython = python3 commands = {[helpers]mkdirp} {toxinidir}/.coverage-output - pytest --timeout 300 --cov=pip --cov-config={toxinidir}/setup.cfg [] + pytest --cov=pip --cov-config={toxinidir}/setup.cfg [] setenv = # Used in coverage configuration in setup.cfg. From c651ecf25a26d18f6265c7d454f963c991fa7428 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sun, 28 Mar 2021 07:29:37 +0100 Subject: [PATCH 3069/3170] Tweak some references in the documentation --- .github/ISSUE_TEMPLATE/resolver-failure.md | 2 +- docs/html/development/issue-triage.rst | 2 +- docs/html/reference/pip_install.rst | 6 +++--- docs/html/user_guide.rst | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/resolver-failure.md b/.github/ISSUE_TEMPLATE/resolver-failure.md index b5215cef94d..2b835b8b2ee 100644 --- a/.github/ISSUE_TEMPLATE/resolver-failure.md +++ b/.github/ISSUE_TEMPLATE/resolver-failure.md @@ -7,7 +7,7 @@ labels: ["K: UX", "K: crash", "C: new resolver", "C: dependency resolution"] <!-- Please provide as much information as you can about your failure, so that we can understand the root cause. -Try if your issue has been fixed in the in-development version of pip. Use the following command to install pip from master: +Try if your issue has been fixed in the in-development version of pip. Use the following command to install pip from the current development branch: python -m pip install -U "pip @ https://github.com/pypa/pip/archive/master.zip" --> diff --git a/docs/html/development/issue-triage.rst b/docs/html/development/issue-triage.rst index 9b5e5cc1c3e..e2e10287ecc 100644 --- a/docs/html/development/issue-triage.rst +++ b/docs/html/development/issue-triage.rst @@ -229,7 +229,7 @@ Examples: (`link <https://github.com/pypa/pip/issues/6498#issuecomment-513501112>`__) - get-pip on system with no ``/usr/lib64`` (`link <https://github.com/pypa/pip/issues/5379#issuecomment-515270576>`__) -- reproducing with ``pip`` from master branch +- reproducing with ``pip`` from current development branch (`link <https://github.com/pypa/pip/issues/6707#issue-467770959>`__) diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst index 2a60e7188b4..c60267f1cf1 100644 --- a/docs/html/reference/pip_install.rst +++ b/docs/html/reference/pip_install.rst @@ -448,7 +448,7 @@ Here are the supported forms:: Passing a branch name, a commit hash, a tag name or a git ref is possible like so:: - [-e] git+https://git.example.com/MyProject.git@master#egg=MyProject + [-e] git+https://git.example.com/MyProject.git@main#egg=MyProject [-e] git+https://git.example.com/MyProject.git@v1.0#egg=MyProject [-e] git+https://git.example.com/MyProject.git@da39a3ee5e6b4b0d3255bfef95601890afd80709#egg=MyProject [-e] git+https://git.example.com/MyProject.git@refs/pull/123/head#egg=MyProject @@ -1062,7 +1062,7 @@ Examples .. code-block:: shell python -m pip install SomePackage[PDF] - python -m pip install "SomePackage[PDF] @ git+https://git.repo/SomePackage@master#subdirectory=subdir_path" + python -m pip install "SomePackage[PDF] @ git+https://git.repo/SomePackage@main#subdirectory=subdir_path" python -m pip install .[PDF] # project in current directory python -m pip install SomePackage[PDF]==3.0 python -m pip install SomePackage[PDF,EPUB] # multiple extras @@ -1072,7 +1072,7 @@ Examples .. code-block:: shell py -m pip install SomePackage[PDF] - py -m pip install "SomePackage[PDF] @ git+https://git.repo/SomePackage@master#subdirectory=subdir_path" + py -m pip install "SomePackage[PDF] @ git+https://git.repo/SomePackage@main#subdirectory=subdir_path" py -m pip install .[PDF] # project in current directory py -m pip install SomePackage[PDF]==3.0 py -m pip install SomePackage[PDF,EPUB] # multiple extras diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 61e0cdf0769..4bdf4731f2e 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -1887,6 +1887,6 @@ announcements on the `low-traffic packaging announcements list`_ and .. _low-traffic packaging announcements list: https://mail.python.org/mailman3/lists/pypi-announce.python.org/ .. _our survey on upgrades that create conflicts: https://docs.google.com/forms/d/e/1FAIpQLSeBkbhuIlSofXqCyhi3kGkLmtrpPOEBwr6iJA6SzHdxWKfqdA/viewform .. _the official Python blog: https://blog.python.org/ -.. _requests: https://requests.readthedocs.io/en/master/user/authentication/#netrc-authentication +.. _requests: https://requests.readthedocs.io/en/latest/user/authentication/#netrc-authentication .. _Python standard library: https://docs.python.org/3/library/netrc.html .. _Python Windows launcher: https://docs.python.org/3/using/windows.html#launcher From 1db5ce02af1051244493d029883c125ed48961ff Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sun, 28 Mar 2021 07:30:21 +0100 Subject: [PATCH 3070/3170] Update the default branch name for this repository --- .github/ISSUE_TEMPLATE/resolver-failure.md | 2 +- docs/html/copyright.rst | 2 +- .../html/development/architecture/anatomy.rst | 2 +- docs/html/development/contributing.rst | 34 +++++++++---------- docs/html/development/issue-triage.rst | 2 +- docs/html/development/release-process.rst | 22 ++++++------ 6 files changed, 32 insertions(+), 32 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/resolver-failure.md b/.github/ISSUE_TEMPLATE/resolver-failure.md index 2b835b8b2ee..09c23a040d9 100644 --- a/.github/ISSUE_TEMPLATE/resolver-failure.md +++ b/.github/ISSUE_TEMPLATE/resolver-failure.md @@ -9,7 +9,7 @@ Please provide as much information as you can about your failure, so that we can Try if your issue has been fixed in the in-development version of pip. Use the following command to install pip from the current development branch: - python -m pip install -U "pip @ https://github.com/pypa/pip/archive/master.zip" + python -m pip install -U "pip @ https://github.com/pypa/pip/archive/main.zip" --> **What did you want to do?** diff --git a/docs/html/copyright.rst b/docs/html/copyright.rst index fd0212f53ec..0e2ede5ee05 100644 --- a/docs/html/copyright.rst +++ b/docs/html/copyright.rst @@ -6,4 +6,4 @@ Copyright pip and this documentation is: -Copyright © 2008-2020 The pip developers (see `AUTHORS.txt <https://github.com/pypa/pip/blob/master/AUTHORS.txt>`_ file). All rights reserved. +Copyright © 2008-2020 The pip developers (see `AUTHORS.txt <https://github.com/pypa/pip/blob/main/AUTHORS.txt>`_ file). All rights reserved. diff --git a/docs/html/development/architecture/anatomy.rst b/docs/html/development/architecture/anatomy.rst index 46bba448944..e08ecebfd19 100644 --- a/docs/html/development/architecture/anatomy.rst +++ b/docs/html/development/architecture/anatomy.rst @@ -105,5 +105,5 @@ Within ``src/``: .. _`tracking issue`: https://github.com/pypa/pip/issues/6831 .. _GitHub repository: https://github.com/pypa/pip/ -.. _tox.ini: https://github.com/pypa/pip/blob/master/tox.ini +.. _tox.ini: https://github.com/pypa/pip/blob/main/tox.ini .. _improving the pip dependency resolver: https://github.com/pypa/pip/issues/988 diff --git a/docs/html/development/contributing.rst b/docs/html/development/contributing.rst index 63eb4c33ee7..7d2e64902d6 100644 --- a/docs/html/development/contributing.rst +++ b/docs/html/development/contributing.rst @@ -11,7 +11,7 @@ We have an in-progress guide to the Submitting Pull Requests ======================== -Submit pull requests against the ``master`` branch, providing a good +Submit pull requests against the ``main`` branch, providing a good description of what you're doing and why. You must have legal permission to distribute any code you contribute to pip and it must be available under the MIT License. @@ -39,7 +39,7 @@ separately, as a "formatting cleanup" PR, if needed. Automated Testing ================= -All pull requests and merges to 'master' branch are tested using `Travis CI`_, +All pull requests and merges to 'main' branch are tested using `Travis CI`_, `Azure Pipelines`_ and `GitHub Actions`_ based on our `.travis.yml`_, `.azure-pipelines`_ and `.github/workflows`_ files. More details about pip's Continuous Integration can be found in the `CI Documentation`_ @@ -131,8 +131,8 @@ updating deprecation policy, etc. Updating your branch ==================== -As you work, you might need to update your local master branch up-to-date with -the ``master`` branch in the main pip repository, which moves forward as the +As you work, you might need to update your local main branch up-to-date with +the ``main`` branch in the main pip repository, which moves forward as the maintainers merge pull requests. Most people working on the project use the following workflow. @@ -160,24 +160,24 @@ First, fetch the latest changes from the main pip repository, ``upstream``: git fetch upstream -Then, check out your local ``master`` branch, and rebase the changes on top of +Then, check out your local ``main`` branch, and rebase the changes on top of it: .. code-block:: console - git checkout master - git rebase upstream/master + git checkout main + git rebase upstream/main At this point, you might have to `resolve merge conflicts`_. Once this is done, -push the updates you have just made to your local ``master`` branch to your +push the updates you have just made to your local ``main`` branch to your ``origin`` repository on GitHub: .. code-block:: console - git checkout master - git push origin master + git checkout main + git push origin main -Now your local ``master`` branch and the ``master`` branch in your ``origin`` +Now your local ``main`` branch and the ``main`` branch in your ``origin`` repo have been updated with the most recent changes from the main pip repository. @@ -187,10 +187,10 @@ To keep your branches updated, the process is similar: git checkout awesome-feature git fetch upstream - git rebase upstream/master + git rebase upstream/main Now your branch has been updated with the latest changes from the -``master`` branch on the upstream pip repository. +``main`` branch on the upstream pip repository. It's good practice to back up your branches by pushing them to your ``origin`` on GitHub as you are working on them. To push a branch, @@ -230,7 +230,7 @@ If you get an error message like this: Try force-pushing your branch with ``push -f``. -The ``master`` branch in the main pip repository gets updated frequently, so +The ``main`` branch in the main pip repository gets updated frequently, so you might have to update your branch at least once while you are working on it. Thank you for your contribution! @@ -267,9 +267,9 @@ will initiate a vote among the existing maintainers. .. _`Travis CI`: https://travis-ci.org/ .. _`Azure Pipelines`: https://azure.microsoft.com/en-in/services/devops/pipelines/ .. _`GitHub Actions`: https://github.com/features/actions -.. _`.travis.yml`: https://github.com/pypa/pip/blob/master/.travis.yml -.. _`.azure-pipelines`: https://github.com/pypa/pip/blob/master/.azure-pipelines -.. _`.github/workflows`: https://github.com/pypa/pip/blob/master/.github/workflows +.. _`.travis.yml`: https://github.com/pypa/pip/blob/main/.travis.yml +.. _`.azure-pipelines`: https://github.com/pypa/pip/blob/main/.azure-pipelines +.. _`.github/workflows`: https://github.com/pypa/pip/blob/main/.github/workflows .. _`CI Documentation`: https://pip.pypa.io/en/latest/development/ci/ .. _`towncrier`: https://pypi.org/project/towncrier/ .. _`Testing the next-gen pip dependency resolver`: https://pradyunsg.me/blog/2020/03/27/pip-resolver-testing/ diff --git a/docs/html/development/issue-triage.rst b/docs/html/development/issue-triage.rst index e2e10287ecc..c21da1fc6dd 100644 --- a/docs/html/development/issue-triage.rst +++ b/docs/html/development/issue-triage.rst @@ -285,7 +285,7 @@ An issue may be considered resolved and closed when: - already tracked by another issue - A project-specific issue has been identified and the issue no - longer occurs as of the latest commit on the master branch. + longer occurs as of the latest commit on the main branch. - An enhancement or feature request no longer has a proponent and the maintainers don't think it's worth keeping open. diff --git a/docs/html/development/release-process.rst b/docs/html/development/release-process.rst index a133e57f20c..ee1595cec56 100644 --- a/docs/html/development/release-process.rst +++ b/docs/html/development/release-process.rst @@ -7,7 +7,7 @@ Release process Release Cadence =============== -The pip project has a release cadence of releasing whatever is on ``master`` +The pip project has a release cadence of releasing whatever is on ``main`` every 3 months. This gives users a predictable pattern for when releases are going to happen and prevents locking up improvements for fixes for long periods of time, while still preventing massively fracturing the user base @@ -22,8 +22,8 @@ The release manager may, at their discretion, choose whether or not there will be a pre-release period for a release, and if there is may extend that period into the next month if needed. -Because releases are made direct from the ``master`` branch, it is essential -that ``master`` is always in a releasable state. It is acceptable to merge +Because releases are made direct from the ``main`` branch, it is essential +that ``main`` is always in a releasable state. It is acceptable to merge PRs that partially implement a new feature, but only if the partially implemented version is usable in that state (for example, with reduced functionality or disabled by default). In the case where a merged PR is found @@ -116,13 +116,13 @@ Release Process Creating a new release ---------------------- -#. Checkout the current pip ``master`` branch. +#. Checkout the current pip ``main`` branch. #. Ensure you have the latest ``nox`` installed. #. Prepare for release using ``nox -s prepare-release -- YY.N``. This will update the relevant files and tag the correct commit. #. Build the release artifacts using ``nox -s build-release -- YY.N``. This will checkout the tag, generate the distribution files to be - uploaded and checkout the master branch again. + uploaded and checkout the main branch again. #. Upload the release to PyPI using ``nox -s upload-release -- YY.N``. #. Push all of the changes including the tag. #. Regenerate the ``get-pip.py`` script in the `get-pip repository`_ (as @@ -155,20 +155,20 @@ Creating a bug-fix release Sometimes we need to release a bugfix release of the form ``YY.N.Z+1``. In order to create one of these the changes should already be merged into the -``master`` branch. +``main`` branch. #. Create a new ``release/YY.N.Z+1`` branch off of the ``YY.N`` tag using the command ``git checkout -b release/YY.N.Z+1 YY.N``. -#. Cherry pick the fixed commits off of the ``master`` branch, fixing any +#. Cherry pick the fixed commits off of the ``main`` branch, fixing any conflicts. #. Run ``nox -s prepare-release -- YY.N.Z+1``. -#. Merge master into your release branch and drop the news files that have been +#. Merge main into your release branch and drop the news files that have been included in your release (otherwise they would also appear in the ``YY.N+1`` changelog) #. Push the ``release/YY.N.Z+1`` branch to github and submit a PR for it against - the ``master`` branch and wait for the tests to run. -#. Once tests run, merge the ``release/YY.N.Z+1`` branch into master, and follow - the above release process starting with step 4. + the ``main`` branch and wait for the tests to run. +#. Once tests run, merge the ``release/YY.N.Z+1`` branch into ``main``, and + follow the above release process starting with step 4. .. _`get-pip repository`: https://github.com/pypa/get-pip .. _`psf-salt repository`: https://github.com/python/psf-salt From cd7b50277e9ca3e28037c03d7d53bb9e9ccf2fd3 Mon Sep 17 00:00:00 2001 From: Andrey Bienkowski <hexagonrecursion@gmail.com> Date: Wed, 10 Feb 2021 14:35:10 +0300 Subject: [PATCH 3071/3170] Use f-strings for formatting --- src/pip/_internal/network/utils.py | 6 ++---- src/pip/_internal/resolution/resolvelib/requirements.py | 2 +- src/pip/_internal/utils/misc.py | 2 +- src/pip/_internal/wheel_builder.py | 2 +- 4 files changed, 5 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/network/utils.py b/src/pip/_internal/network/utils.py index d29c7c0769d..7370314b58c 100644 --- a/src/pip/_internal/network/utils.py +++ b/src/pip/_internal/network/utils.py @@ -42,12 +42,10 @@ def raise_for_status(resp): reason = resp.reason if 400 <= resp.status_code < 500: - http_error_msg = '%s Client Error: %s for url: %s' % ( - resp.status_code, reason, resp.url) + http_error_msg = f'{resp.status_code} Client Error: {reason} for url: {resp.url}' elif 500 <= resp.status_code < 600: - http_error_msg = '%s Server Error: %s for url: %s' % ( - resp.status_code, reason, resp.url) + http_error_msg = f'{resp.status_code} Server Error: {reason} for url: {resp.url}' if http_error_msg: raise NetworkConnectionError(http_error_msg, response=resp) diff --git a/src/pip/_internal/resolution/resolvelib/requirements.py b/src/pip/_internal/resolution/resolvelib/requirements.py index b4cb571401e..a7fcdd1e345 100644 --- a/src/pip/_internal/resolution/resolvelib/requirements.py +++ b/src/pip/_internal/resolution/resolvelib/requirements.py @@ -166,7 +166,7 @@ def __init__(self, name): def __str__(self): # type: () -> str - return "{} (unavailable)".format(self._name) + return f"{self._name} (unavailable)" def __repr__(self): # type: () -> str diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index e41a138a385..96e26a09ca4 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -241,7 +241,7 @@ def strtobool(val): elif val in ("n", "no", "f", "false", "off", "0"): return 0 else: - raise ValueError("invalid truth value %r" % (val,)) + raise ValueError(f"invalid truth value {val!r}") def format_size(bytes): diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index 90966d9ed08..4b2f6d5d4df 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -187,7 +187,7 @@ def _verify_one(req, wheel_path): try: metadata_version = Version(metadata_version_value) except InvalidVersion: - msg = "Invalid Metadata-Version: {}".format(metadata_version_value) + msg = f"Invalid Metadata-Version: {metadata_version_value}" raise UnsupportedWheel(msg) if (metadata_version >= Version("1.2") and not isinstance(dist.version, Version)): From afecf80cff747dcdac5bf1be64a06edd9aae620d Mon Sep 17 00:00:00 2001 From: Andrey Bienkowski <hexagonrecursion@gmail.com> Date: Wed, 10 Feb 2021 14:54:46 +0300 Subject: [PATCH 3072/3170] Lint --- src/pip/_internal/network/utils.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/network/utils.py b/src/pip/_internal/network/utils.py index 7370314b58c..6e5cf0d1d1a 100644 --- a/src/pip/_internal/network/utils.py +++ b/src/pip/_internal/network/utils.py @@ -42,10 +42,12 @@ def raise_for_status(resp): reason = resp.reason if 400 <= resp.status_code < 500: - http_error_msg = f'{resp.status_code} Client Error: {reason} for url: {resp.url}' + http_error_msg = ( + f'{resp.status_code} Client Error: {reason} for url: {resp.url}') elif 500 <= resp.status_code < 600: - http_error_msg = f'{resp.status_code} Server Error: {reason} for url: {resp.url}' + http_error_msg = ( + f'{resp.status_code} Server Error: {reason} for url: {resp.url}') if http_error_msg: raise NetworkConnectionError(http_error_msg, response=resp) From 622f104694e3091f194b6f01d94154f8f4c56348 Mon Sep 17 00:00:00 2001 From: Andrey Bienkowski <hexagonrecursion@gmail.com> Date: Tue, 30 Mar 2021 15:48:27 +0300 Subject: [PATCH 3073/3170] Use f-strings for formatting suggested by pyupgrade --py36-plus --- src/pip/_internal/resolution/resolvelib/factory.py | 2 +- src/pip/_internal/utils/compat.py | 4 +--- src/pip/_internal/utils/misc.py | 2 +- src/pip/_internal/utils/subprocess.py | 2 +- src/pip/_internal/utils/wheel.py | 2 +- tests/functional/test_install_config.py | 2 +- 6 files changed, 6 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index aa6c4781d2e..a4eec7136bb 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -514,7 +514,7 @@ def describe_trigger(parent): relevant_constraints.add(req.name) msg = msg + "\n " if parent: - msg = msg + "{} {} depends on ".format(parent.name, parent.version) + msg = msg + f"{parent.name} {parent.version} depends on " else: msg = msg + "The user requested " msg = msg + req.format_for_error() diff --git a/src/pip/_internal/utils/compat.py b/src/pip/_internal/utils/compat.py index 0cb1c469704..1fb2dc729e0 100644 --- a/src/pip/_internal/utils/compat.py +++ b/src/pip/_internal/utils/compat.py @@ -49,9 +49,7 @@ def get_path_uid(path): file_uid = os.stat(path).st_uid else: # raise OSError for parity with os.O_NOFOLLOW above - raise OSError( - "{} is a symlink; Will not return uid for symlinks".format(path) - ) + raise OSError(f"{path} is a symlink; Will not return uid for symlinks") return file_uid diff --git a/src/pip/_internal/utils/misc.py b/src/pip/_internal/utils/misc.py index 96e26a09ca4..714a36e9c41 100644 --- a/src/pip/_internal/utils/misc.py +++ b/src/pip/_internal/utils/misc.py @@ -193,7 +193,7 @@ def _check_no_input(message): """Raise an error if no input is allowed.""" if os.environ.get("PIP_NO_INPUT"): raise Exception( - "No input was expected ($PIP_NO_INPUT set); question: {}".format(message) + f"No input was expected ($PIP_NO_INPUT set); question: {message}" ) diff --git a/src/pip/_internal/utils/subprocess.py b/src/pip/_internal/utils/subprocess.py index cfde1870081..2c8cf21231d 100644 --- a/src/pip/_internal/utils/subprocess.py +++ b/src/pip/_internal/utils/subprocess.py @@ -252,7 +252,7 @@ def call_subprocess( elif on_returncode == "ignore": pass else: - raise ValueError("Invalid value: on_returncode={!r}".format(on_returncode)) + raise ValueError(f"Invalid value: on_returncode={on_returncode!r}") return output diff --git a/src/pip/_internal/utils/wheel.py b/src/pip/_internal/utils/wheel.py index 982508eb4f6..42f080845cf 100644 --- a/src/pip/_internal/utils/wheel.py +++ b/src/pip/_internal/utils/wheel.py @@ -36,7 +36,7 @@ def get_metadata(self, name): except UnicodeDecodeError as e: # Augment the default error with the origin of the file. raise UnsupportedWheel( - "Error decoding metadata for {}: {}".format(self._wheel_name, e) + f"Error decoding metadata for {self._wheel_name}: {e}" ) diff --git a/tests/functional/test_install_config.py b/tests/functional/test_install_config.py index ed33b0c9f83..59aec65ffe8 100644 --- a/tests/functional/test_install_config.py +++ b/tests/functional/test_install_config.py @@ -298,7 +298,7 @@ def test_prompt_for_keyring_if_needed(script, data, cert_factory, auth_needed): response(str(data.packages / "simple-3.0.tar.gz")), ] - url = "https://{}:{}/simple".format(server.host, server.port) + url = f"https://{server.host}:{server.port}/simple" keyring_content = textwrap.dedent("""\ import os From 3edbd7cb917ea8e33dc9b1b7c07e76528b0d0bb5 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Thu, 1 Apr 2021 17:42:15 +0100 Subject: [PATCH 3074/3170] Drop the resolver failure issue template --- .github/ISSUE_TEMPLATE/resolver-failure.md | 34 ---------------------- 1 file changed, 34 deletions(-) delete mode 100644 .github/ISSUE_TEMPLATE/resolver-failure.md diff --git a/.github/ISSUE_TEMPLATE/resolver-failure.md b/.github/ISSUE_TEMPLATE/resolver-failure.md deleted file mode 100644 index 09c23a040d9..00000000000 --- a/.github/ISSUE_TEMPLATE/resolver-failure.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -name: Dependency resolver failures / errors -about: Report when the pip dependency resolver fails -labels: ["K: UX", "K: crash", "C: new resolver", "C: dependency resolution"] ---- - -<!-- -Please provide as much information as you can about your failure, so that we can understand the root cause. - -Try if your issue has been fixed in the in-development version of pip. Use the following command to install pip from the current development branch: - - python -m pip install -U "pip @ https://github.com/pypa/pip/archive/main.zip" ---> - -**What did you want to do?** -<!-- Include any inputs you gave to pip, for example: - -* Package requirements: any CLI arguments and/or your requirements.txt file -* Already installed packages, outputted via `pip freeze` ---> - -**Output** - -``` -Paste what pip outputted in a code block. https://github.github.com/gfm/#fenced-code-blocks -``` - -**Additional information** - -<!-- -It would be great if you could also include your dependency tree. For this you can use pipdeptree: https://pypi.org/project/pipdeptree/ - -For users installing packages from a private repository or local directory, please try your best to describe your setup. We'd like to understand how to reproduce the error locally, so would need (at a minimum) a description of the packages you are trying to install, and a list of dependencies for each package. ---> From 3173bffd384ea78b02bd743e2305b6d82265bbeb Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Thu, 1 Apr 2021 17:49:51 +0100 Subject: [PATCH 3075/3170] Tweak the bug report template - Ask for a description in the first textarea. - Remove warning block about resolver report. - Mark "How to Reproduce" as a required validation. - Add `render: sh-session` to the output handling. - Simplify CoC agreement checkbox. --- .github/ISSUE_TEMPLATE/bug-report.yml | 123 +++++++++++--------------- 1 file changed, 52 insertions(+), 71 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 6b18de7bd1f..b24b305367f 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -1,81 +1,62 @@ ---- name: Bug report about: Something is not working correctly. title: "" labels: "S: needs triage, type: bug" -issue_body: true # default: true, adds a classic WSYWIG textarea, if on -body: -- type: markdown - attributes: - value: | - ⚠ - If you're reporting an issue for `--use-feature=2020-resolver`, - use the "Dependency resolver failures / errors" template instead. -- type: markdown - attributes: - value: "**Environment**" -- type: input - attributes: - label: pip version - validations: - required: true -- type: input - attributes: - label: Python version - validations: - required: true -- type: input - attributes: - label: OS - validations: - required: true -- type: textarea - attributes: - label: Additional information - description: >- - Feel free to add more information about your environment here. - -- type: textarea - attributes: - label: Description - description: >- - A clear and concise description of what the bug is. -- type: textarea - attributes: - label: Expected behavior - description: >- - A clear and concise description of what you expected to happen. +body: + - type: textarea + attributes: + label: Description + description: >- + A clear and concise description of what the bug is. + validations: + required: true -- type: textarea - attributes: - label: How to Reproduce - description: >- - Describe the steps to reproduce this bug. - value: | - 1. Get package from '...' - 2. Then run '...' - 3. An error occurs. + - type: textarea + attributes: + label: Expected behavior + description: >- + A clear and concise description of what you expected to happen. -- type: textarea - attributes: - label: Output - description: >- - Paste the output of the steps above, including the commands - themselves and pip's output/traceback etc. - value: | - ```console + - type: input + attributes: + label: pip version + validations: + required: true + - type: input + attributes: + label: Python version + validations: + required: true + - type: input + attributes: + label: OS + validations: + required: true - ``` + - type: textarea + attributes: + label: How to Reproduce + description: Please provide steps to reproduce this bug. + value: | + 1. Get package from '...' + 2. Then run '...' + 3. An error occurs. + validations: + required: true -- type: checkboxes - attributes: - label: Code of Conduct - description: | - Read the [PSF Code of Conduct][CoC] first. + - type: textarea + attributes: + label: Output + description: >- + Provide the output of the steps above, including the commands + themselves and pip's output/traceback etc. + render: sh-session - [CoC]: https://github.com/pypa/.github/blob/main/CODE_OF_CONDUCT.md - options: - - label: I agree to follow the PSF Code of Conduct - required: true -... + - type: checkboxes + attributes: + label: Code of Conduct + options: + - label: >- + I agree to follow the [PSF Code of Conduct](https://www.python.org/psf/conduct/). + required: true From 44c9035c16056ac52c4c7f55f4aa79ed1c0c7cbc Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Thu, 1 Apr 2021 17:54:05 +0100 Subject: [PATCH 3076/3170] Fix indentation of validations This should fix the issue form template failing due to this. --- .github/ISSUE_TEMPLATE/bug-report.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index b24b305367f..1fae5ada17d 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -9,8 +9,8 @@ body: label: Description description: >- A clear and concise description of what the bug is. - validations: - required: true + validations: + required: true - type: textarea attributes: @@ -42,8 +42,8 @@ body: 1. Get package from '...' 2. Then run '...' 3. An error occurs. - validations: - required: true + validations: + required: true - type: textarea attributes: From ade72202c5f02c89e02260e11d275cc51f2c9a60 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Thu, 1 Apr 2021 17:57:16 +0100 Subject: [PATCH 3077/3170] Transition away from deprecated keys > The top-level key `about:` will be deprecated in favor of > `description:`. Please use `description:` instead of `about:`. --- .github/ISSUE_TEMPLATE/bug-report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/ISSUE_TEMPLATE/bug-report.yml b/.github/ISSUE_TEMPLATE/bug-report.yml index 1fae5ada17d..f9f4a73c250 100644 --- a/.github/ISSUE_TEMPLATE/bug-report.yml +++ b/.github/ISSUE_TEMPLATE/bug-report.yml @@ -1,5 +1,5 @@ name: Bug report -about: Something is not working correctly. +description: Something is not working correctly. title: "" labels: "S: needs triage, type: bug" From df0955d535672fce8f44c996e6c596f08800c645 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Thu, 1 Apr 2021 23:00:10 +0100 Subject: [PATCH 3078/3170] Blacken setup.py --- .pre-commit-config.yaml | 2 -- setup.py | 24 +++++++++--------------- 2 files changed, 9 insertions(+), 17 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 71d2754892f..31d41399ef6 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -39,8 +39,6 @@ repos: ^tests/unit| ^tests/functional/(?!test_install)| ^tests/functional/test_install| - # Files in the root of the repository - ^setup.py| # A blank ignore, to avoid merge conflicts later. ^$ diff --git a/setup.py b/setup.py index 26056f280aa..c2992ddfce4 100644 --- a/setup.py +++ b/setup.py @@ -16,22 +16,21 @@ def read(rel_path): def get_version(rel_path): # type: (str) -> str for line in read(rel_path).splitlines(): - if line.startswith('__version__'): + if line.startswith("__version__"): # __version__ = "0.9" delim = '"' if '"' in line else "'" return line.split(delim)[1] raise RuntimeError("Unable to find version string.") -long_description = read('README.rst') +long_description = read("README.rst") setup( name="pip", version=get_version("src/pip/__init__.py"), description="The PyPA recommended tool for installing Python packages.", long_description=long_description, - - license='MIT', + license="MIT", classifiers=[ "Development Status :: 5 - Production/Stable", "Intended Audience :: Developers", @@ -47,17 +46,15 @@ def get_version(rel_path): "Programming Language :: Python :: Implementation :: CPython", "Programming Language :: Python :: Implementation :: PyPy", ], - url='https://pip.pypa.io/', - keywords='distutils easy_install egg setuptools wheel virtualenv', + url="https://pip.pypa.io/", + keywords="distutils easy_install egg setuptools wheel virtualenv", project_urls={ "Documentation": "https://pip.pypa.io", "Source": "https://github.com/pypa/pip", "Changelog": "https://pip.pypa.io/en/stable/news/", }, - - author='The pip developers', - author_email='distutils-sig@python.org', - + author="The pip developers", + author_email="distutils-sig@python.org", package_dir={"": "src"}, packages=find_packages( where="src", @@ -75,12 +72,9 @@ def get_version(rel_path): "console_scripts": [ "pip=pip._internal.cli.main:main", "pip{}=pip._internal.cli.main:main".format(sys.version_info[0]), - "pip{}.{}=pip._internal.cli.main:main".format( - *sys.version_info[:2] - ), + "pip{}.{}=pip._internal.cli.main:main".format(*sys.version_info[:2]), ], }, - zip_safe=False, - python_requires='>=3.6', + python_requires=">=3.6", ) From cc89f8c04df7f87ff5a6eea43c771960561a59fb Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Thu, 1 Apr 2021 23:01:07 +0100 Subject: [PATCH 3079/3170] Drop keywords --- setup.py | 1 - 1 file changed, 1 deletion(-) diff --git a/setup.py b/setup.py index c2992ddfce4..91f537a40f0 100644 --- a/setup.py +++ b/setup.py @@ -47,7 +47,6 @@ def get_version(rel_path): "Programming Language :: Python :: Implementation :: PyPy", ], url="https://pip.pypa.io/", - keywords="distutils easy_install egg setuptools wheel virtualenv", project_urls={ "Documentation": "https://pip.pypa.io", "Source": "https://github.com/pypa/pip", From 2495cf95a6c7eb61ccf1f9f0e8b8d736af914e53 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Thu, 1 Apr 2021 23:04:23 +0100 Subject: [PATCH 3080/3170] Blacken __main__.py --- .pre-commit-config.yaml | 1 - src/pip/__main__.py | 6 +++--- 2 files changed, 3 insertions(+), 4 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 71d2754892f..8001c38e954 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -30,7 +30,6 @@ repos: ^src/pip/_internal/req| ^src/pip/_internal/vcs| ^src/pip/_internal/\w+\.py$| - ^src/pip/__main__.py$| ^tools/| # Tests ^tests/conftest.py| diff --git a/src/pip/__main__.py b/src/pip/__main__.py index 1005489f32f..063fd1aacfd 100644 --- a/src/pip/__main__.py +++ b/src/pip/__main__.py @@ -5,12 +5,12 @@ # of sys.path, if present to avoid using current directory # in pip commands check, freeze, install, list and show, # when invoked as python -m pip <command> -if sys.path[0] in ('', os.getcwd()): +if sys.path[0] in ("", os.getcwd()): sys.path.pop(0) # If we are running from a wheel, add the wheel to sys.path # This allows the usage python pip-*.whl/pip install pip-*.whl -if __package__ == '': +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 @@ -20,5 +20,5 @@ from pip._internal.cli.main import main as _main -if __name__ == '__main__': +if __name__ == "__main__": sys.exit(_main()) From 1720aee672601bf6c2d2a04377d3f0c4ecba1923 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Thu, 1 Apr 2021 23:21:21 +0100 Subject: [PATCH 3081/3170] No `--hook-stage=manual` to pre-commit in nox --- noxfile.py | 1 - 1 file changed, 1 deletion(-) diff --git a/noxfile.py b/noxfile.py index a530b65ba20..eab26503439 100644 --- a/noxfile.py +++ b/noxfile.py @@ -174,7 +174,6 @@ def lint(session): args = session.posargs + ["--all-files"] else: args = ["--all-files", "--show-diff-on-failure"] - args.append("--hook-stage=manual") session.run("pre-commit", "run", *args) From 147b9147826fe4752fe8b1edfd91d6f43e952eaa Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Thu, 1 Apr 2021 23:25:09 +0100 Subject: [PATCH 3082/3170] Get rid of the tools/automation folder It was an unnecessary level added to the folder hierarchy. --- .gitattributes | 2 +- noxfile.py | 2 +- pyproject.toml | 4 ++-- tools/{automation => }/news/template.rst | 0 tools/{automation => }/release/__init__.py | 2 +- tools/{automation => }/release/check_version.py | 0 tools/{automation => }/vendoring/patches/appdirs.patch | 0 tools/{automation => }/vendoring/patches/certifi.patch | 0 tools/{automation => }/vendoring/patches/requests.patch | 0 9 files changed, 5 insertions(+), 5 deletions(-) rename tools/{automation => }/news/template.rst (100%) rename tools/{automation => }/release/__init__.py (99%) rename tools/{automation => }/release/check_version.py (100%) rename tools/{automation => }/vendoring/patches/appdirs.patch (100%) rename tools/{automation => }/vendoring/patches/certifi.patch (100%) rename tools/{automation => }/vendoring/patches/requests.patch (100%) diff --git a/.gitattributes b/.gitattributes index 7b547a58cc2..6a0fc6943c1 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1,4 +1,4 @@ # Patches must have Unix-style line endings, even on Windows -tools/automation/vendoring/patches/* eol=lf +tools/vendoring/patches/* eol=lf # The CA Bundle should always use Unix-style line endings, even on Windows src/pip/_vendor/certifi/*.pem eol=lf diff --git a/noxfile.py b/noxfile.py index eab26503439..d9e34454382 100644 --- a/noxfile.py +++ b/noxfile.py @@ -12,7 +12,7 @@ # fmt: off sys.path.append(".") -from tools.automation import release # isort:skip # noqa +from tools import release # isort:skip # noqa sys.path.pop() # fmt: on diff --git a/pyproject.toml b/pyproject.toml index 281594b21c7..b7e20bbe29a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -9,7 +9,7 @@ filename = "NEWS.rst" directory = "news/" title_format = "{version} ({project_date})" issue_format = "`#{issue} <https://github.com/pypa/pip/issues/{issue}>`_" -template = "tools/automation/news/template.rst" +template = "tools/news/template.rst" type = [ { name = "Process", directory = "process", showcontent = true }, { name = "Deprecations and Removals", directory = "removal", showcontent = true }, @@ -26,7 +26,7 @@ requirements = "src/pip/_vendor/vendor.txt" namespace = "pip._vendor" protected-files = ["__init__.py", "README.rst", "vendor.txt"] -patches-dir = "tools/automation/vendoring/patches" +patches-dir = "tools/vendoring/patches" [tool.vendoring.transformations] substitute = [ diff --git a/tools/automation/news/template.rst b/tools/news/template.rst similarity index 100% rename from tools/automation/news/template.rst rename to tools/news/template.rst diff --git a/tools/automation/release/__init__.py b/tools/release/__init__.py similarity index 99% rename from tools/automation/release/__init__.py rename to tools/release/__init__.py index a10ccd1f55c..ec3a0eeb78b 100644 --- a/tools/automation/release/__init__.py +++ b/tools/release/__init__.py @@ -28,7 +28,7 @@ def get_version_from_arguments(session: Session) -> Optional[str]: cmd = [ # https://github.com/theacodes/nox/pull/378 os.path.join(session.bin, "python"), # type: ignore - "tools/automation/release/check_version.py", + "tools/release/check_version.py", version ] not_ok = subprocess.run(cmd).returncode diff --git a/tools/automation/release/check_version.py b/tools/release/check_version.py similarity index 100% rename from tools/automation/release/check_version.py rename to tools/release/check_version.py diff --git a/tools/automation/vendoring/patches/appdirs.patch b/tools/vendoring/patches/appdirs.patch similarity index 100% rename from tools/automation/vendoring/patches/appdirs.patch rename to tools/vendoring/patches/appdirs.patch diff --git a/tools/automation/vendoring/patches/certifi.patch b/tools/vendoring/patches/certifi.patch similarity index 100% rename from tools/automation/vendoring/patches/certifi.patch rename to tools/vendoring/patches/certifi.patch diff --git a/tools/automation/vendoring/patches/requests.patch b/tools/vendoring/patches/requests.patch similarity index 100% rename from tools/automation/vendoring/patches/requests.patch rename to tools/vendoring/patches/requests.patch From 0cdf9d7260611155d8f8bf90e7403c1c7bbc611c Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Thu, 1 Apr 2021 23:29:19 +0100 Subject: [PATCH 3083/3170] Drop all existing CI --- .azure-pipelines/jobs/package.yml | 36 ------ .azure-pipelines/jobs/test-windows.yml | 53 -------- .azure-pipelines/jobs/test.yml | 38 ------ .azure-pipelines/linux.yml | 11 -- .azure-pipelines/scripts/New-RAMDisk.ps1 | 74 ----------- .azure-pipelines/steps/run-tests-windows.yml | 54 -------- .azure-pipelines/steps/run-tests.yml | 25 ---- .azure-pipelines/windows.yml | 11 -- .github/workflows/linting.yml | 53 -------- .github/workflows/macos.yml | 127 ------------------- .travis.yml | 32 ----- tools/travis/install.sh | 7 - tools/travis/run.sh | 65 ---------- tools/travis/setup.sh | 6 - 14 files changed, 592 deletions(-) delete mode 100644 .azure-pipelines/jobs/package.yml delete mode 100644 .azure-pipelines/jobs/test-windows.yml delete mode 100644 .azure-pipelines/jobs/test.yml delete mode 100644 .azure-pipelines/linux.yml delete mode 100644 .azure-pipelines/scripts/New-RAMDisk.ps1 delete mode 100644 .azure-pipelines/steps/run-tests-windows.yml delete mode 100644 .azure-pipelines/steps/run-tests.yml delete mode 100644 .azure-pipelines/windows.yml delete mode 100644 .github/workflows/linting.yml delete mode 100644 .github/workflows/macos.yml delete mode 100644 .travis.yml delete mode 100755 tools/travis/install.sh delete mode 100755 tools/travis/run.sh delete mode 100755 tools/travis/setup.sh diff --git a/.azure-pipelines/jobs/package.yml b/.azure-pipelines/jobs/package.yml deleted file mode 100644 index bdb0254a1ba..00000000000 --- a/.azure-pipelines/jobs/package.yml +++ /dev/null @@ -1,36 +0,0 @@ -parameters: - vmImage: - -jobs: -- job: Package - dependsOn: - - Test_Primary - - Test_Secondary - pool: - vmImage: ${{ parameters.vmImage }} - - steps: - - task: UsePythonVersion@0 - displayName: Use Python 3 latest - inputs: - versionSpec: '3' - - - bash: | - git config --global user.email "distutils-sig@python.org" - git config --global user.name "pip" - displayName: Setup Git credentials - - - bash: pip install nox - displayName: Install dependencies - - - bash: nox -s prepare-release -- 99.9 - displayName: Prepare dummy release - - - bash: nox -s build-release -- 99.9 - displayName: Generate distributions for the dummy release - - - task: PublishBuildArtifacts@1 - displayName: 'Publish Artifact: dist' - inputs: - pathtoPublish: dist - artifactName: dist diff --git a/.azure-pipelines/jobs/test-windows.yml b/.azure-pipelines/jobs/test-windows.yml deleted file mode 100644 index 99cd8a836bd..00000000000 --- a/.azure-pipelines/jobs/test-windows.yml +++ /dev/null @@ -1,53 +0,0 @@ -parameters: - vmImage: - -jobs: -- job: Test_Primary - displayName: Tests / - - pool: - vmImage: ${{ parameters.vmImage }} - strategy: - matrix: - "3.6": # lowest Python version - python.version: '3.6' - python.architecture: x64 - "3.8": # current - python.version: '3.8' - python.architecture: x64 - maxParallel: 6 - - steps: - - template: ../steps/run-tests-windows.yml - parameters: - runIntegrationTests: true - -- job: Test_Secondary - displayName: Tests / - # Don't run integration tests for these runs - # Run after Test_Primary so we don't devour time and jobs if tests are going to fail - dependsOn: Test_Primary - - pool: - vmImage: ${{ parameters.vmImage }} - strategy: - matrix: - "3.7": - python.version: '3.7' - python.architecture: x64 - # This is for Windows, so test x86 builds - "3.6-x86": - python.version: '3.6' - python.architecture: x86 - "3.7-x86": - python.version: '3.7' - python.architecture: x86 - "3.8-x86": - python.version: '3.8' - python.architecture: x86 - maxParallel: 6 - - steps: - - template: ../steps/run-tests-windows.yml - parameters: - runIntegrationTests: false diff --git a/.azure-pipelines/jobs/test.yml b/.azure-pipelines/jobs/test.yml deleted file mode 100644 index a3a0ef80b6d..00000000000 --- a/.azure-pipelines/jobs/test.yml +++ /dev/null @@ -1,38 +0,0 @@ -parameters: - vmImage: - -jobs: -- job: Test_Primary - displayName: Tests / - - pool: - vmImage: ${{ parameters.vmImage }} - strategy: - matrix: - "3.6": # lowest Python version - python.version: '3.6' - python.architecture: x64 - "3.8": - python.version: '3.8' - python.architecture: x64 - maxParallel: 2 - - steps: - - template: ../steps/run-tests.yml - -- job: Test_Secondary - displayName: Tests / - # Run after Test_Primary so we don't devour time and jobs if tests are going to fail - dependsOn: Test_Primary - - pool: - vmImage: ${{ parameters.vmImage }} - strategy: - matrix: - "3.7": - python.version: '3.7' - python.architecture: x64 - maxParallel: 4 - - steps: - - template: ../steps/run-tests.yml diff --git a/.azure-pipelines/linux.yml b/.azure-pipelines/linux.yml deleted file mode 100644 index e5598074344..00000000000 --- a/.azure-pipelines/linux.yml +++ /dev/null @@ -1,11 +0,0 @@ -variables: - CI: true - -jobs: -- template: jobs/test.yml - parameters: - vmImage: ubuntu-16.04 - -- template: jobs/package.yml - parameters: - vmImage: ubuntu-16.04 diff --git a/.azure-pipelines/scripts/New-RAMDisk.ps1 b/.azure-pipelines/scripts/New-RAMDisk.ps1 deleted file mode 100644 index 21b1a573a49..00000000000 --- a/.azure-pipelines/scripts/New-RAMDisk.ps1 +++ /dev/null @@ -1,74 +0,0 @@ -[CmdletBinding()] -param( - [Parameter(Mandatory=$true, - HelpMessage="Drive letter to use for the RAMDisk")] - [String]$drive, - [Parameter(HelpMessage="Size to allocate to the RAMDisk")] - [UInt64]$size=1GB -) - -$ErrorActionPreference = "Stop" -Set-StrictMode -Version Latest - -Write-Output "Installing FS-iSCSITarget-Server" -Install-WindowsFeature -Name FS-iSCSITarget-Server - -Write-Output "Starting MSiSCSI" -Start-Service MSiSCSI -$retry = 10 -do { - $service = Get-Service MSiSCSI - if ($service.Status -eq "Running") { - break; - } - $retry-- - Start-Sleep -Milliseconds 500 -} until ($retry -eq 0) - -$service = Get-Service MSiSCSI -if ($service.Status -ne "Running") { - throw "MSiSCSI is not running" -} - -Write-Output "Configuring Firewall" -Get-NetFirewallServiceFilter -Service MSiSCSI | Enable-NetFirewallRule - -Write-Output "Configuring RAMDisk" -# Must use external-facing IP address, otherwise New-IscsiTargetPortal is -# unable to connect. -$ip = ( - Get-NetIPAddress -AddressFamily IPv4 | - Where-Object {$_.IPAddress -ne "127.0.0.1"} -)[0].IPAddress -if ( - -not (Get-IscsiServerTarget -ComputerName localhost | Where-Object {$_.TargetName -eq "ramdisks"}) -) { - New-IscsiServerTarget ` - -ComputerName localhost ` - -TargetName ramdisks ` - -InitiatorId IPAddress:$ip -} - -$newVirtualDisk = New-IscsiVirtualDisk ` - -ComputerName localhost ` - -Path ramdisk:local$drive.vhdx ` - -Size $size -Add-IscsiVirtualDiskTargetMapping ` - -ComputerName localhost ` - -TargetName ramdisks ` - -Path ramdisk:local$drive.vhdx - -Write-Output "Connecting to iSCSI" -New-IscsiTargetPortal -TargetPortalAddress $ip -Get-IscsiTarget | Where-Object {!$_.IsConnected} | Connect-IscsiTarget - -Write-Output "Configuring disk" -$newDisk = Get-IscsiConnection | - Get-Disk | - Where-Object {$_.SerialNumber -eq $newVirtualDisk.SerialNumber} - -Set-Disk -InputObject $newDisk -IsOffline $false -Initialize-Disk -InputObject $newDisk -PartitionStyle MBR -New-Partition -InputObject $newDisk -UseMaximumSize -DriveLetter $drive - -Format-Volume -DriveLetter $drive -NewFileSystemLabel Temp -FileSystem NTFS diff --git a/.azure-pipelines/steps/run-tests-windows.yml b/.azure-pipelines/steps/run-tests-windows.yml deleted file mode 100644 index 39282a3cc80..00000000000 --- a/.azure-pipelines/steps/run-tests-windows.yml +++ /dev/null @@ -1,54 +0,0 @@ -parameters: - runIntegrationTests: - -steps: -- task: UsePythonVersion@0 - displayName: Use Python $(python.version) - inputs: - versionSpec: '$(python.version)' - architecture: '$(python.architecture)' - -- task: PowerShell@2 - inputs: - filePath: .azure-pipelines/scripts/New-RAMDisk.ps1 - arguments: "-Drive R -Size 1GB" - displayName: Setup RAMDisk - -- powershell: | - mkdir R:\Temp - $acl = Get-Acl "R:\Temp" - $rule = New-Object System.Security.AccessControl.FileSystemAccessRule( - "Everyone", "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow" - ) - $acl.AddAccessRule($rule) - Set-Acl "R:\Temp" $acl - displayName: Set RAMDisk Permissions - -- bash: pip install --upgrade 'virtualenv<20' setuptools tox - displayName: Install Tox - -- script: tox -e py -- -m unit -n auto --junit-xml=junit/unit-test.xml - env: - TEMP: "R:\\Temp" - displayName: Tox run unit tests - -- ${{ if eq(parameters.runIntegrationTests, 'true') }}: - - powershell: | - # Fix Git SSL errors - pip install certifi tox - python -m certifi > cacert.txt - $env:GIT_SSL_CAINFO = $(Get-Content cacert.txt) - - # Shorten paths to get under MAX_PATH or else integration tests will fail - # https://bugs.python.org/issue18199 - $env:TEMP = "R:\Temp" - - tox -e py -- -m integration -n auto --durations=5 --junit-xml=junit/integration-test.xml - displayName: Tox run integration tests - -- task: PublishTestResults@2 - displayName: Publish Test Results - inputs: - testResultsFiles: junit/*.xml - testRunTitle: 'Python $(python.version)' - condition: succeededOrFailed() diff --git a/.azure-pipelines/steps/run-tests.yml b/.azure-pipelines/steps/run-tests.yml deleted file mode 100644 index 5b9a9c50c89..00000000000 --- a/.azure-pipelines/steps/run-tests.yml +++ /dev/null @@ -1,25 +0,0 @@ -steps: -- task: UsePythonVersion@0 - displayName: Use Python $(python.version) - inputs: - versionSpec: '$(python.version)' - -- bash: pip install --upgrade 'virtualenv<20' setuptools tox - displayName: Install Tox - -- script: tox -e py -- -m unit -n auto --junit-xml=junit/unit-test.xml - displayName: Tox run unit tests - -# Run integration tests in two groups so we will fail faster if there is a failure in the first group -- script: tox -e py -- -m integration -n auto --durations=5 -k "not test_install" --junit-xml=junit/integration-test-group0.xml - displayName: Tox run Group 0 integration tests - -- script: tox -e py -- -m integration -n auto --durations=5 -k "test_install" --junit-xml=junit/integration-test-group1.xml - displayName: Tox run Group 1 integration tests - -- task: PublishTestResults@2 - displayName: Publish Test Results - inputs: - testResultsFiles: junit/*.xml - testRunTitle: 'Python $(python.version)' - condition: succeededOrFailed() diff --git a/.azure-pipelines/windows.yml b/.azure-pipelines/windows.yml deleted file mode 100644 index f56b8f50486..00000000000 --- a/.azure-pipelines/windows.yml +++ /dev/null @@ -1,11 +0,0 @@ -variables: - CI: true - -jobs: -- template: jobs/test-windows.yml - parameters: - vmImage: vs2017-win2016 - -- template: jobs/package.yml - parameters: - vmImage: vs2017-win2016 diff --git a/.github/workflows/linting.yml b/.github/workflows/linting.yml deleted file mode 100644 index 71459d660e8..00000000000 --- a/.github/workflows/linting.yml +++ /dev/null @@ -1,53 +0,0 @@ -name: Linting - -on: - push: - pull_request: - schedule: - # Run every Friday at 18:02 UTC - - cron: 2 18 * * 5 - -jobs: - lint: - name: ${{ matrix.os }} - runs-on: ${{ matrix.os }}-latest - env: - TOXENV: lint,docs,vendoring - - strategy: - matrix: - os: - - Ubuntu - - Windows - - steps: - - uses: actions/checkout@v2 - - name: Set up Python 3.9 - uses: actions/setup-python@v2 - with: - python-version: 3.9 - - # Setup Caching - - name: pip cache - uses: actions/cache@v1 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('tools/requirements/tests.txt') }}-${{ hashFiles('tools/requirements/docs.txt') }}-${{ hashFiles('tox.ini') }} - restore-keys: | - ${{ runner.os }}-pip- - ${{ runner.os }}- - - - name: Set PY (for pre-commit cache) - run: echo "PY=$(python -c 'import hashlib, sys;print(hashlib.sha256(sys.version.encode()+sys.executable.encode()).hexdigest())')" >> $GITHUB_ENV - - name: pre-commit cache - uses: actions/cache@v1 - with: - path: ~/.cache/pre-commit - key: pre-commit|2020-02-14|${{ env.PY }}|${{ hashFiles('.pre-commit-config.yaml') }} - - # Get the latest tox - - name: Install tox - run: python -m pip install tox - - # Main check - - run: python -m tox diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml deleted file mode 100644 index de226389d26..00000000000 --- a/.github/workflows/macos.yml +++ /dev/null @@ -1,127 +0,0 @@ -name: MacOS - -on: - push: - pull_request: - schedule: - # Run every Friday at 18:02 UTC - - cron: 2 18 * * 5 - -jobs: - dev-tools: - name: Quality Check - runs-on: macos-latest - - steps: - # Caches - - name: pip cache - uses: actions/cache@v1 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('tools/requirements/tests.txt') }}-${{ hashFiles('tools/requirements/docs.txt') }}-${{ hashFiles('tox.ini') }} - restore-keys: | - ${{ runner.os }}-pip- - ${{ runner.os }}- - - name: Set PY (for pre-commit cache) - run: echo "PY=$(python -c 'import hashlib, sys;print(hashlib.sha256(sys.version.encode()+sys.executable.encode()).hexdigest())')" >> $GITHUB_ENV - - name: pre-commit cache - uses: actions/cache@v1 - with: - path: ~/.cache/pre-commit - key: pre-commit|2020-02-14|${{ env.PY }}|${{ hashFiles('.pre-commit-config.yaml') }} - - # Setup - - uses: actions/checkout@v2 - - name: Set up Python 3.8 - uses: actions/setup-python@v2 - with: - python-version: 3.8 - - - name: Install tox - run: python -m pip install tox - - # Main check - - run: python -m tox -e "lint,docs" - - packaging: - name: Packaging - runs-on: macos-latest - - steps: - # Caches - - name: pip cache - uses: actions/cache@v1 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('tools/requirements/tests.txt') }}-${{ hashFiles('tools/requirements/docs.txt') }}-${{ hashFiles('tox.ini') }} - restore-keys: | - ${{ runner.os }}-pip- - ${{ runner.os }}- - # Setup - - name: Set up git credentials - run: | - git config --global user.email "pypa-dev@googlegroups.com" - git config --global user.name "pip" - - uses: actions/checkout@v2 - - name: Set up Python 3.8 - uses: actions/setup-python@v2 - with: - python-version: 3.8 - - name: Install tox and nox - run: python -m pip install tox nox - - # Main check - - name: Check vendored packages - run: python -m tox -e "vendoring" - - - name: Prepare dummy release - run: nox -s prepare-release -- 99.9 - - - name: Generate distributions for the dummy release - run: nox -s build-release -- 99.9 - - tests: - name: Tests / ${{ matrix.python }} - runs-on: macos-latest - - needs: dev-tools - - strategy: - fail-fast: false - matrix: - python: [3.6, 3.7, 3.8, 3.9] - - steps: - # Caches - - name: pip cache - uses: actions/cache@v1 - with: - path: ~/.cache/pip - key: ${{ runner.os }}-pip-${{ hashFiles('tools/requirements/tests.txt') }}-${{ hashFiles('tools/requirements/docs.txt') }}-${{ hashFiles('tox.ini') }} - restore-keys: | - ${{ runner.os }}-pip- - ${{ runner.os }}- - # Setup - - uses: actions/checkout@v2 - - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python }} - - - name: Install tox - run: python -m pip install tox 'virtualenv<20' - - # Main check - - name: Run unit tests - run: >- - python -m tox -e py -- - -m unit - --verbose - --numprocesses auto - - - name: Run integration tests - run: >- - python -m tox -e py -- - -m integration - --verbose - --numprocesses auto - --durations=5 diff --git a/.travis.yml b/.travis.yml deleted file mode 100644 index 6610b6eb019..00000000000 --- a/.travis.yml +++ /dev/null @@ -1,32 +0,0 @@ -language: python -cache: pip -dist: xenial -python: 3.9 -addons: - apt: - packages: - - bzr - -stages: -- primary -- secondary - -jobs: - include: - # Basic Checks - - stage: primary - env: TOXENV=docs - - env: TOXENV=lint - - env: TOXENV=vendoring - - # Complete checking for ensuring compatibility - # PyPy - - stage: secondary - env: GROUP=1 - python: pypy3.6-7.3.1 - - env: GROUP=2 - python: pypy3.6-7.3.1 - -before_install: tools/travis/setup.sh -install: travis_retry tools/travis/install.sh -script: tools/travis/run.sh diff --git a/tools/travis/install.sh b/tools/travis/install.sh deleted file mode 100755 index 3b12d69a26b..00000000000 --- a/tools/travis/install.sh +++ /dev/null @@ -1,7 +0,0 @@ -#!/bin/bash -set -e -set -x - -pip install --upgrade setuptools -pip install --upgrade tox tox-venv -pip freeze --all diff --git a/tools/travis/run.sh b/tools/travis/run.sh deleted file mode 100755 index df8f03e7a57..00000000000 --- a/tools/travis/run.sh +++ /dev/null @@ -1,65 +0,0 @@ -#!/bin/bash -set -e - -# Short circuit test runs if there are no code changes involved. -if [[ $TOXENV != docs ]] || [[ $TOXENV != lint ]]; then - if [[ "$TRAVIS_PULL_REQUEST" == "false" ]] - then - echo "This is not a PR -- will do a complete build." - else - # Pull requests are slightly complicated because $TRAVIS_COMMIT_RANGE - # may include more changes than desired if the history is convoluted. - # Instead, explicitly fetch the base branch and compare against the - # merge-base commit. - git fetch -q origin +refs/heads/$TRAVIS_BRANCH - changes=$(git diff --name-only HEAD $(git merge-base HEAD FETCH_HEAD)) - echo "Files changed:" - echo "$changes" - if ! echo "$changes" | grep -qvE '(\.rst$)|(^docs)|(^news)|(^\.github)' - then - echo "Code was not changed -- skipping build." - exit - fi - fi -fi - -# Export the correct TOXENV when not provided. -echo "Determining correct TOXENV..." -if [[ -z "$TOXENV" ]]; then - if [[ ${TRAVIS_PYTHON_VERSION} == pypy* ]]; then - export TOXENV=pypy - else - # We use the syntax ${string:index:length} to make 2.7 -> py27 - _major=${TRAVIS_PYTHON_VERSION:0:1} - _minor=${TRAVIS_PYTHON_VERSION:2:1} - export TOXENV="py${_major}${_minor}" - fi -fi -echo "TOXENV=${TOXENV}" - -if [[ -z "$NEW_RESOLVER" ]]; then - RESOLVER_SWITCH='' -else - RESOLVER_SWITCH='--new-resolver' -fi - -# Print the commands run for this test. -set -x -if [[ "$GROUP" == "1" ]]; then - # Unit tests - tox -- --use-venv -m unit -n auto - # Integration tests (not the ones for 'pip install') - tox -- -m integration -n auto --durations=5 -k "not test_install" \ - --use-venv $RESOLVER_SWITCH -elif [[ "$GROUP" == "2" ]]; then - # Separate Job for running integration tests for 'pip install' - tox -- -m integration -n auto --durations=5 -k "test_install" \ - --use-venv $RESOLVER_SWITCH -elif [[ "$GROUP" == "3" ]]; then - # Separate Job for tests that fail with the new resolver - tox -- -m fails_on_new_resolver -n auto --durations=5 \ - --use-venv $RESOLVER_SWITCH --new-resolver-runtests -else - # Non-Testing Jobs should run once - tox -fi diff --git a/tools/travis/setup.sh b/tools/travis/setup.sh deleted file mode 100755 index c52ce5f167e..00000000000 --- a/tools/travis/setup.sh +++ /dev/null @@ -1,6 +0,0 @@ -#!/bin/bash -set -e - -echo "Setting Git Credentials..." -git config --global user.email "distutils-sig@python.org" -git config --global user.name "pip" From c022615961b3b6fa5b1a5aa6b6f620c8eee7f7da Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Thu, 1 Apr 2021 23:48:39 +0100 Subject: [PATCH 3084/3170] Add a single GitHub Actions workflow for CI --- .github/workflows/ci.yml | 189 +++++++++++++++++++++++++++++++++++ tools/ci/New-RAMDisk.ps1 | 74 ++++++++++++++ tools/requirements/tests.txt | 9 +- 3 files changed, 267 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/ci.yml create mode 100644 tools/ci/New-RAMDisk.ps1 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000000..950239b6aab --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,189 @@ +name: CI + +on: + push: + branches: [master] + tags: + # Tags for all potential release numbers till 2030. + - "2[0-9].[0-3]" # 20.0 -> 29.3 + - "2[0-9].[0-3].[0-9]+" # 20.0.0 -> 29.3.[0-9]+ + pull_request: + schedule: + - cron: 0 0 * * MON # Run every Monday at 00:00 UTC + +jobs: + determine-changes: + runs-on: ubuntu-latest + outputs: + tests: ${{ steps.filter.outputs.tests }} + vendoring: ${{ steps.filter.outputs.vendoring }} + steps: + # For pull requests it's not necessary to checkout the code + - uses: dorny/paths-filter@v2 + id: filter + with: + filters: | + vendoring: + # Anything that's touching "vendored code" + - "src/pip/_vendor/**" + - "pyproject.toml" + tests: + # Anything that's touching testable stuff + - ".github/workflows/ci.yml" + - "tools/requirements/tests.txt" + - "src/**" + - "tests/**" + + pre-commit: + name: pre-commit + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - uses: pre-commit/action@v2.0.0 + with: + extra_args: --hook-stage=manual + + packaging: + name: packaging + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + - name: Set up git credentials + run: | + git config --global user.email "pypa-dev@googlegroups.com" + git config --global user.name "pip" + + - run: pip install nox + - run: nox -s prepare-release -- 99.9 + - run: nox -s build-release -- 99.9 + + vendoring: + name: vendoring + runs-on: ubuntu-latest + + needs: [determine-changes] + if: ${{ needs.determine-changes.outputs.vendoring == 'true' }} + + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + + - run: pip install vendoring + - run: vendoring sync . --verbose + - run: git diff --exit-code + + tests-unix: + name: tests / ${{ matrix.python }} / ${{ matrix.os }} + runs-on: ${{ matrix.os }}-latest + + needs: [pre-commit, packaging, determine-changes] + if: ${{ needs.determine-changes.outputs.tests == 'true' }} + + strategy: + fail-fast: true + matrix: + os: [Ubuntu, MacOS] + python: + - 3.6 + - 3.7 + - 3.8 + - 3.9 + include: + - os: Ubuntu + python: pypy3 + + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + + - run: pip install tox 'virtualenv<20' + + # Main check + - name: Run unit tests + run: >- + tox -e py -- + -m unit + --verbose --numprocesses auto --showlocals + - name: Run integration tests + run: >- + tox -e py -- + -m integration + --verbose --numprocesses auto --showlocals + --durations=5 + + tests-windows: + name: tests / ${{ matrix.python }} / ${{ matrix.os }} / ${{ matrix.group }} + runs-on: ${{ matrix.os }}-latest + + needs: [pre-commit, packaging, determine-changes] + if: ${{ needs.determine-changes.outputs.tests == 'true' }} + + strategy: + fail-fast: true + matrix: + os: [Windows] + python: + - 3.6 + - 3.7 + - 3.8 + - 3.9 + group: [1, 2] + + steps: + - uses: actions/checkout@v2 + - uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python }} + + # We use a RAMDisk on Windows, since filesystem IO is a big slowdown + # for our tests. + - name: Create a RAMDisk + run: ./tools/ci/New-RAMDisk.ps1 -Drive R -Size 1GB + + - name: Setup RAMDisk permissions + run: | + mkdir R:\Temp + $acl = Get-Acl "R:\Temp" + $rule = New-Object System.Security.AccessControl.FileSystemAccessRule( + "Everyone", "FullControl", "ContainerInherit,ObjectInherit", "None", "Allow" + ) + $acl.AddAccessRule($rule) + Set-Acl "R:\Temp" $acl + + - run: pip install tox 'virtualenv<20' + env: + TEMP: "R:\\Temp" + + # Main check + - name: Run unit tests + if: matrix.group == 1 + run: >- + tox -e py -- + -m unit + --verbose --numprocesses auto --showlocals + env: + TEMP: "R:\\Temp" + + - name: Run integration tests (group 1) + if: matrix.group == 1 + run: >- + tox -e py -- + -m integration -k "not test_install" + --verbose --numprocesses auto --showlocals + env: + TEMP: "R:\\Temp" + + - name: Run integration tests (group 2) + if: matrix.group == 2 + run: >- + tox -e py -- + -m integration -k "test_install" + --verbose --numprocesses auto --showlocals + env: + TEMP: "R:\\Temp" diff --git a/tools/ci/New-RAMDisk.ps1 b/tools/ci/New-RAMDisk.ps1 new file mode 100644 index 00000000000..21b1a573a49 --- /dev/null +++ b/tools/ci/New-RAMDisk.ps1 @@ -0,0 +1,74 @@ +[CmdletBinding()] +param( + [Parameter(Mandatory=$true, + HelpMessage="Drive letter to use for the RAMDisk")] + [String]$drive, + [Parameter(HelpMessage="Size to allocate to the RAMDisk")] + [UInt64]$size=1GB +) + +$ErrorActionPreference = "Stop" +Set-StrictMode -Version Latest + +Write-Output "Installing FS-iSCSITarget-Server" +Install-WindowsFeature -Name FS-iSCSITarget-Server + +Write-Output "Starting MSiSCSI" +Start-Service MSiSCSI +$retry = 10 +do { + $service = Get-Service MSiSCSI + if ($service.Status -eq "Running") { + break; + } + $retry-- + Start-Sleep -Milliseconds 500 +} until ($retry -eq 0) + +$service = Get-Service MSiSCSI +if ($service.Status -ne "Running") { + throw "MSiSCSI is not running" +} + +Write-Output "Configuring Firewall" +Get-NetFirewallServiceFilter -Service MSiSCSI | Enable-NetFirewallRule + +Write-Output "Configuring RAMDisk" +# Must use external-facing IP address, otherwise New-IscsiTargetPortal is +# unable to connect. +$ip = ( + Get-NetIPAddress -AddressFamily IPv4 | + Where-Object {$_.IPAddress -ne "127.0.0.1"} +)[0].IPAddress +if ( + -not (Get-IscsiServerTarget -ComputerName localhost | Where-Object {$_.TargetName -eq "ramdisks"}) +) { + New-IscsiServerTarget ` + -ComputerName localhost ` + -TargetName ramdisks ` + -InitiatorId IPAddress:$ip +} + +$newVirtualDisk = New-IscsiVirtualDisk ` + -ComputerName localhost ` + -Path ramdisk:local$drive.vhdx ` + -Size $size +Add-IscsiVirtualDiskTargetMapping ` + -ComputerName localhost ` + -TargetName ramdisks ` + -Path ramdisk:local$drive.vhdx + +Write-Output "Connecting to iSCSI" +New-IscsiTargetPortal -TargetPortalAddress $ip +Get-IscsiTarget | Where-Object {!$_.IsConnected} | Connect-IscsiTarget + +Write-Output "Configuring disk" +$newDisk = Get-IscsiConnection | + Get-Disk | + Where-Object {$_.SerialNumber -eq $newVirtualDisk.SerialNumber} + +Set-Disk -InputObject $newDisk -IsOffline $false +Initialize-Disk -InputObject $newDisk -PartitionStyle MBR +New-Partition -InputObject $newDisk -UseMaximumSize -DriveLetter $drive + +Format-Volume -DriveLetter $drive -NewFileSystemLabel Temp -FileSystem NTFS diff --git a/tools/requirements/tests.txt b/tools/requirements/tests.txt index f760f004ad6..7badf2a27ff 100644 --- a/tools/requirements/tests.txt +++ b/tools/requirements/tests.txt @@ -1,5 +1,4 @@ ---use-feature=2020-resolver -cryptography==2.8 +cryptography freezegun pretend pytest @@ -7,7 +6,7 @@ pytest-cov pytest-rerunfailures pytest-xdist scripttest -setuptools>=39.2.0 # Needed for `setuptools.wheel.Wheel` support. -https://github.com/pypa/virtualenv/archive/legacy.zip#egg=virtualenv -werkzeug==0.16.0 +setuptools +virtualenv < 20.0 +werkzeug wheel From 876df5a57fe46a2831995164836bf2410a37307b Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Fri, 2 Apr 2021 01:16:35 +0100 Subject: [PATCH 3085/3170] Remove PyPy3 for now --- .github/workflows/ci.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 950239b6aab..c9e3a16c4a3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -92,9 +92,6 @@ jobs: - 3.7 - 3.8 - 3.9 - include: - - os: Ubuntu - python: pypy3 steps: - uses: actions/checkout@v2 From 1ab662f3302a5d8e3592113d1e1a006df628c80d Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Fri, 2 Apr 2021 01:25:40 +0100 Subject: [PATCH 3086/3170] Trim the number of Windows jobs we run --- .github/workflows/ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c9e3a16c4a3..420bbfab227 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -127,8 +127,9 @@ jobs: os: [Windows] python: - 3.6 - - 3.7 - - 3.8 + # Commented out, since Windows tests are expensively slow. + # - 3.7 + # - 3.8 - 3.9 group: [1, 2] From 879cee8a20d439d1f4e15b121d64cfaf1c3168cf Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Fri, 2 Apr 2021 01:32:05 +0100 Subject: [PATCH 3087/3170] Update note that CI docs are out of date --- docs/html/development/ci.rst | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/docs/html/development/ci.rst b/docs/html/development/ci.rst index 991c66c736a..ac51e9ffa05 100644 --- a/docs/html/development/ci.rst +++ b/docs/html/development/ci.rst @@ -1,7 +1,8 @@ .. note:: - This section of the documentation is currently being written. pip - developers welcome your help to complete this documentation. If + This section of the documentation is currently out of date. + + pip developers welcome your help to update this documentation. If you're interested in helping out, please let us know in the `tracking issue`_, or just submit a pull request and mention it in that tracking issue. From 960c01adce491de00ef7a8d02a32fea31b15a1dc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Miro=20Hron=C4=8Dok?= <miro@hroncok.cz> Date: Fri, 2 Apr 2021 02:39:11 +0200 Subject: [PATCH 3088/3170] Update urllib3 to 1.26.4 to fix CVE-2021-28363 --- news/CVE-2021-28363.vendor.rst | 1 + src/pip/_vendor/urllib3/_version.py | 2 +- src/pip/_vendor/urllib3/connection.py | 8 ++++++-- src/pip/_vendor/urllib3/exceptions.py | 12 +++++++++++- src/pip/_vendor/urllib3/util/retry.py | 1 + src/pip/_vendor/vendor.txt | 2 +- 6 files changed, 21 insertions(+), 5 deletions(-) create mode 100644 news/CVE-2021-28363.vendor.rst diff --git a/news/CVE-2021-28363.vendor.rst b/news/CVE-2021-28363.vendor.rst new file mode 100644 index 00000000000..29700ab7469 --- /dev/null +++ b/news/CVE-2021-28363.vendor.rst @@ -0,0 +1 @@ +Update urllib3 to 1.26.4 to fix CVE-2021-28363 diff --git a/src/pip/_vendor/urllib3/_version.py b/src/pip/_vendor/urllib3/_version.py index 2dba29e3fbe..97c983300b0 100644 --- a/src/pip/_vendor/urllib3/_version.py +++ b/src/pip/_vendor/urllib3/_version.py @@ -1,2 +1,2 @@ # This file is protected via CODEOWNERS -__version__ = "1.26.2" +__version__ = "1.26.4" diff --git a/src/pip/_vendor/urllib3/connection.py b/src/pip/_vendor/urllib3/connection.py index 660d679c361..45580b7e1ea 100644 --- a/src/pip/_vendor/urllib3/connection.py +++ b/src/pip/_vendor/urllib3/connection.py @@ -67,7 +67,7 @@ class BrokenPipeError(Exception): # When it comes time to update this value as a part of regular maintenance # (ie test_recent_date is failing) update it to ~6 months before the current date. -RECENT_DATE = datetime.date(2019, 1, 1) +RECENT_DATE = datetime.date(2020, 7, 1) _CONTAINS_CONTROL_CHAR_RE = re.compile(r"[^-!#$%&'*+.^_`|~0-9a-zA-Z]") @@ -215,7 +215,7 @@ def putrequest(self, method, url, *args, **kwargs): def putheader(self, header, *values): """""" - if SKIP_HEADER not in values: + if not any(isinstance(v, str) and v == SKIP_HEADER for v in values): _HTTPConnection.putheader(self, header, *values) elif six.ensure_str(header.lower()) not in SKIPPABLE_HEADERS: raise ValueError( @@ -490,6 +490,10 @@ def _connect_tls_proxy(self, hostname, conn): self.ca_cert_dir, self.ca_cert_data, ) + # By default urllib3's SSLContext disables `check_hostname` and uses + # a custom check. For proxies we're good with relying on the default + # verification. + ssl_context.check_hostname = True # If no cert was provided, use only the default options for server # certificate validation diff --git a/src/pip/_vendor/urllib3/exceptions.py b/src/pip/_vendor/urllib3/exceptions.py index d69958d5dfc..cba6f3f560f 100644 --- a/src/pip/_vendor/urllib3/exceptions.py +++ b/src/pip/_vendor/urllib3/exceptions.py @@ -289,7 +289,17 @@ class ProxySchemeUnknown(AssertionError, URLSchemeUnknown): # TODO(t-8ch): Stop inheriting from AssertionError in v2.0. def __init__(self, scheme): - message = "Not supported proxy scheme %s" % scheme + # 'localhost' is here because our URL parser parses + # localhost:8080 -> scheme=localhost, remove if we fix this. + if scheme == "localhost": + scheme = None + if scheme is None: + message = "Proxy URL had no scheme, should start with http:// or https://" + else: + message = ( + "Proxy URL had unsupported scheme %s, should use http:// or https://" + % scheme + ) super(ProxySchemeUnknown, self).__init__(message) diff --git a/src/pip/_vendor/urllib3/util/retry.py b/src/pip/_vendor/urllib3/util/retry.py index ee51f922f84..d25a41b42ea 100644 --- a/src/pip/_vendor/urllib3/util/retry.py +++ b/src/pip/_vendor/urllib3/util/retry.py @@ -253,6 +253,7 @@ def __init__( "Using 'method_whitelist' with Retry is deprecated and " "will be removed in v2.0. Use 'allowed_methods' instead", DeprecationWarning, + stacklevel=2, ) allowed_methods = method_whitelist if allowed_methods is _Default: diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 51a5508479e..868baba6f01 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -13,7 +13,7 @@ requests==2.25.1 certifi==2020.12.05 chardet==4.0.0 idna==2.10 - urllib3==1.26.2 + urllib3==1.26.4 resolvelib==0.5.4 retrying==1.3.3 setuptools==44.0.0 From 55b12ba23ad992fc2039af89792efaa7c4c64e8a Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Fri, 2 Apr 2021 10:22:08 +0100 Subject: [PATCH 3089/3170] Blacken conftest.py --- .pre-commit-config.yaml | 1 - tests/conftest.py | 221 ++++++++++++++++++++-------------------- 2 files changed, 113 insertions(+), 109 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index 00b25ff6053..b3c61d7843c 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,7 +32,6 @@ repos: ^src/pip/_internal/\w+\.py$| ^tools/| # Tests - ^tests/conftest.py| ^tests/lib| ^tests/data| ^tests/unit| diff --git a/tests/conftest.py b/tests/conftest.py index 9d1ee4443b9..a53e0c4f7b1 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -56,26 +56,26 @@ def pytest_addoption(parser): def pytest_collection_modifyitems(config, items): for item in items: - if not hasattr(item, 'module'): # e.g.: DoctestTextfile + if not hasattr(item, "module"): # e.g.: DoctestTextfile continue - if (item.get_closest_marker('search') and - not config.getoption('--run-search')): - item.add_marker(pytest.mark.skip('pip search test skipped')) + if item.get_closest_marker("search") and not config.getoption("--run-search"): + item.add_marker(pytest.mark.skip("pip search test skipped")) if "CI" in os.environ: # Mark network tests as flaky - if item.get_closest_marker('network') is not None: + if item.get_closest_marker("network") is not None: item.add_marker(pytest.mark.flaky(reruns=3, reruns_delay=2)) - if (item.get_closest_marker('incompatible_with_test_venv') and - config.getoption("--use-venv")): - item.add_marker(pytest.mark.skip( - 'Incompatible with test venv')) - if (item.get_closest_marker('incompatible_with_venv') and - sys.prefix != sys.base_prefix): - item.add_marker(pytest.mark.skip( - 'Incompatible with venv')) + if item.get_closest_marker("incompatible_with_test_venv") and config.getoption( + "--use-venv" + ): + item.add_marker(pytest.mark.skip("Incompatible with test venv")) + if ( + item.get_closest_marker("incompatible_with_venv") + and sys.prefix != sys.base_prefix + ): + item.add_marker(pytest.mark.skip("Incompatible with venv")) module_path = os.path.relpath( item.module.__file__, @@ -83,22 +83,21 @@ def pytest_collection_modifyitems(config, items): ) module_root_dir = module_path.split(os.pathsep)[0] - if (module_root_dir.startswith("functional") or - module_root_dir.startswith("integration") or - module_root_dir.startswith("lib")): + if ( + module_root_dir.startswith("functional") + or module_root_dir.startswith("integration") + or module_root_dir.startswith("lib") + ): item.add_marker(pytest.mark.integration) elif module_root_dir.startswith("unit"): item.add_marker(pytest.mark.unit) else: - raise RuntimeError( - f"Unknown test type (filename = {module_path})" - ) + raise RuntimeError(f"Unknown test type (filename = {module_path})") @pytest.fixture(scope="session", autouse=True) def resolver_variant(request): - """Set environment variable to make pip default to the correct resolver. - """ + """Set environment variable to make pip default to the correct resolver.""" resolver = request.config.getoption("--resolver") # Handle the environment variables for this test. @@ -118,9 +117,9 @@ def resolver_variant(request): yield resolver -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def tmpdir_factory(request, tmpdir_factory): - """ Modified `tmpdir_factory` session fixture + """Modified `tmpdir_factory` session fixture that will automatically cleanup after itself. """ yield tmpdir_factory @@ -172,17 +171,17 @@ def isolate(tmpdir, monkeypatch): fake_root = os.path.join(str(tmpdir), "fake-root") os.makedirs(fake_root) - if sys.platform == 'win32': + if sys.platform == "win32": # Note: this will only take effect in subprocesses... home_drive, home_path = os.path.splitdrive(home_dir) - monkeypatch.setenv('USERPROFILE', home_dir) - monkeypatch.setenv('HOMEDRIVE', home_drive) - monkeypatch.setenv('HOMEPATH', home_path) + monkeypatch.setenv("USERPROFILE", home_dir) + monkeypatch.setenv("HOMEDRIVE", home_drive) + monkeypatch.setenv("HOMEPATH", home_path) for env_var, sub_path in ( - ('APPDATA', 'AppData/Roaming'), - ('LOCALAPPDATA', 'AppData/Local'), + ("APPDATA", "AppData/Roaming"), + ("LOCALAPPDATA", "AppData/Local"), ): - path = os.path.join(home_dir, *sub_path.split('/')) + path = os.path.join(home_dir, *sub_path.split("/")) monkeypatch.setenv(env_var, path) os.makedirs(path) else: @@ -191,23 +190,46 @@ def isolate(tmpdir, monkeypatch): # of the user's actual $HOME directory. monkeypatch.setenv("HOME", home_dir) # Isolate ourselves from XDG directories - monkeypatch.setenv("XDG_DATA_HOME", os.path.join( - home_dir, ".local", "share", - )) - monkeypatch.setenv("XDG_CONFIG_HOME", os.path.join( - home_dir, ".config", - )) + monkeypatch.setenv( + "XDG_DATA_HOME", + os.path.join( + home_dir, + ".local", + "share", + ), + ) + monkeypatch.setenv( + "XDG_CONFIG_HOME", + os.path.join( + home_dir, + ".config", + ), + ) monkeypatch.setenv("XDG_CACHE_HOME", os.path.join(home_dir, ".cache")) - monkeypatch.setenv("XDG_RUNTIME_DIR", os.path.join( - home_dir, ".runtime", - )) - monkeypatch.setenv("XDG_DATA_DIRS", os.pathsep.join([ - os.path.join(fake_root, "usr", "local", "share"), - os.path.join(fake_root, "usr", "share"), - ])) - monkeypatch.setenv("XDG_CONFIG_DIRS", os.path.join( - fake_root, "etc", "xdg", - )) + monkeypatch.setenv( + "XDG_RUNTIME_DIR", + os.path.join( + home_dir, + ".runtime", + ), + ) + monkeypatch.setenv( + "XDG_DATA_DIRS", + os.pathsep.join( + [ + os.path.join(fake_root, "usr", "local", "share"), + os.path.join(fake_root, "usr", "share"), + ] + ), + ) + monkeypatch.setenv( + "XDG_CONFIG_DIRS", + os.path.join( + fake_root, + "etc", + "xdg", + ), + ) # Configure git, because without an author name/email git will complain # and cause test failures. @@ -224,9 +246,7 @@ def isolate(tmpdir, monkeypatch): # FIXME: Windows... os.makedirs(os.path.join(home_dir, ".config", "git")) with open(os.path.join(home_dir, ".config", "git", "config"), "wb") as fp: - fp.write( - b"[user]\n\tname = pip\n\temail = distutils-sig@python.org\n" - ) + fp.write(b"[user]\n\tname = pip\n\temail = distutils-sig@python.org\n") @pytest.fixture(autouse=True) @@ -245,7 +265,7 @@ def scoped_global_tempdir_manager(request): yield -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def pip_src(tmpdir_factory): def not_code_files_and_folders(path, names): # In the root directory... @@ -265,7 +285,7 @@ def not_code_files_and_folders(path, names): ignored.update(fnmatch.filter(names, pattern)) return ignored - pip_src = Path(str(tmpdir_factory.mktemp('pip_src'))).joinpath('pip_src') + pip_src = Path(str(tmpdir_factory.mktemp("pip_src"))).joinpath("pip_src") # Copy over our source tree so that each use is self contained shutil.copytree( SRC_DIR, @@ -276,83 +296,77 @@ def not_code_files_and_folders(path, names): def _common_wheel_editable_install(tmpdir_factory, common_wheels, package): - wheel_candidates = list( - common_wheels.glob(f'{package}-*.whl')) + wheel_candidates = list(common_wheels.glob(f"{package}-*.whl")) assert len(wheel_candidates) == 1, wheel_candidates - install_dir = Path(str(tmpdir_factory.mktemp(package))) / 'install' + install_dir = Path(str(tmpdir_factory.mktemp(package))) / "install" Wheel(wheel_candidates[0]).install_as_egg(install_dir) - (install_dir / 'EGG-INFO').rename( - install_dir / f'{package}.egg-info') + (install_dir / "EGG-INFO").rename(install_dir / f"{package}.egg-info") assert compileall.compile_dir(str(install_dir), quiet=1) return install_dir -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def setuptools_install(tmpdir_factory, common_wheels): - return _common_wheel_editable_install(tmpdir_factory, - common_wheels, - 'setuptools') + return _common_wheel_editable_install(tmpdir_factory, common_wheels, "setuptools") -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def wheel_install(tmpdir_factory, common_wheels): - return _common_wheel_editable_install(tmpdir_factory, - common_wheels, - 'wheel') + return _common_wheel_editable_install(tmpdir_factory, common_wheels, "wheel") -@pytest.fixture(scope='session') +@pytest.fixture(scope="session") def coverage_install(tmpdir_factory, common_wheels): - return _common_wheel_editable_install(tmpdir_factory, - common_wheels, - 'coverage') + return _common_wheel_editable_install(tmpdir_factory, common_wheels, "coverage") def install_egg_link(venv, project_name, egg_info_dir): - with open(venv.site / 'easy-install.pth', 'a') as fp: - fp.write(str(egg_info_dir.resolve()) + '\n') - with open(venv.site / (project_name + '.egg-link'), 'w') as fp: - fp.write(str(egg_info_dir) + '\n.') + with open(venv.site / "easy-install.pth", "a") as fp: + fp.write(str(egg_info_dir.resolve()) + "\n") + with open(venv.site / (project_name + ".egg-link"), "w") as fp: + fp.write(str(egg_info_dir) + "\n.") -@pytest.fixture(scope='session') -def virtualenv_template(request, tmpdir_factory, pip_src, - setuptools_install, coverage_install): +@pytest.fixture(scope="session") +def virtualenv_template( + request, tmpdir_factory, pip_src, setuptools_install, coverage_install +): - if request.config.getoption('--use-venv'): - venv_type = 'venv' + if request.config.getoption("--use-venv"): + venv_type = "venv" else: - venv_type = 'virtualenv' + venv_type = "virtualenv" # Create the virtual environment - tmpdir = Path(str(tmpdir_factory.mktemp('virtualenv'))) - venv = VirtualEnvironment( - tmpdir.joinpath("venv_orig"), venv_type=venv_type - ) + tmpdir = Path(str(tmpdir_factory.mktemp("virtualenv"))) + venv = VirtualEnvironment(tmpdir.joinpath("venv_orig"), venv_type=venv_type) # Install setuptools and pip. - install_egg_link(venv, 'setuptools', setuptools_install) - pip_editable = Path(str(tmpdir_factory.mktemp('pip'))) / 'pip' + install_egg_link(venv, "setuptools", setuptools_install) + pip_editable = Path(str(tmpdir_factory.mktemp("pip"))) / "pip" shutil.copytree(pip_src, pip_editable, symlinks=True) # noxfile.py is Python 3 only assert compileall.compile_dir( - str(pip_editable), quiet=1, rx=re.compile("noxfile.py$"), + str(pip_editable), + quiet=1, + rx=re.compile("noxfile.py$"), + ) + subprocess.check_call( + [venv.bin / "python", "setup.py", "-q", "develop"], cwd=pip_editable ) - subprocess.check_call([venv.bin / 'python', 'setup.py', '-q', 'develop'], - cwd=pip_editable) # Install coverage and pth file for executing it in any spawned processes # in this virtual environment. - install_egg_link(venv, 'coverage', coverage_install) + install_egg_link(venv, "coverage", coverage_install) # zz prefix ensures the file is after easy-install.pth. - with open(venv.site / 'zz-coverage-helper.pth', 'a') as f: - f.write('import coverage; coverage.process_startup()') + with open(venv.site / "zz-coverage-helper.pth", "a") as f: + f.write("import coverage; coverage.process_startup()") # Drop (non-relocatable) launchers. for exe in os.listdir(venv.bin): if not ( - exe.startswith('python') or - exe.startswith('libpy') # Don't remove libpypy-c.so... + exe.startswith("python") + or exe.startswith("libpy") # Don't remove libpypy-c.so... ): (venv.bin / exe).unlink() @@ -387,7 +401,7 @@ def virtualenv(virtualenv_factory, tmpdir): @pytest.fixture def with_wheel(virtualenv, wheel_install): - install_egg_link(virtualenv, 'wheel', wheel_install) + install_egg_link(virtualenv, "wheel", wheel_install) @pytest.fixture(scope="session") @@ -398,21 +412,16 @@ def factory(tmpdir, virtualenv=None): return PipTestEnvironment( # The base location for our test environment tmpdir, - # Tell the Test Environment where our virtualenv is located virtualenv=virtualenv, - # Do not ignore hidden files, they need to be checked as well ignore_hidden=False, - # We are starting with an already empty directory start_clear=False, - # We want to ensure no temporary files are left behind, so the # PipTestEnvironment needs to capture and assert against temp capture_temp=True, assert_no_temp=True, - # Deprecated python versions produce an extra deprecation warning pip_expect_warning=deprecated_python, ) @@ -434,7 +443,7 @@ def script(tmpdir, virtualenv, script_factory): @pytest.fixture(scope="session") def common_wheels(): """Provide a directory with latest setuptools and wheel wheels""" - return DATA_DIR.joinpath('common_wheels') + return DATA_DIR.joinpath("common_wheels") @pytest.fixture(scope="session") @@ -482,8 +491,7 @@ def deprecated_python(): def cert_factory(tmpdir_factory): def factory(): # type: () -> str - """Returns path to cert/key file. - """ + """Returns path to cert/key file.""" output_path = Path(str(tmpdir_factory.mktemp("certs"))) / "cert.pem" # Must be Text on PY2. cert, key = make_tls_cert("localhost") @@ -537,14 +545,11 @@ def stop(self): def get_requests(self): # type: () -> Dict[str, str] - """Get environ for each received request. - """ + """Get environ for each received request.""" assert not self._running, "cannot get mock from running server" # Legacy: replace call[0][0] with call.args[0] # when pip drops support for python3.7 - return [ - call[0][0] for call in self._server.mock.call_args_list - ] + return [call[0][0] for call in self._server.mock.call_args_list] @pytest.fixture @@ -558,8 +563,8 @@ def mock_server(): @pytest.fixture def utc(): # time.tzset() is not implemented on some platforms, e.g. Windows. - tzset = getattr(time, 'tzset', lambda: None) - with patch.dict(os.environ, {'TZ': 'UTC'}): + tzset = getattr(time, "tzset", lambda: None) + with patch.dict(os.environ, {"TZ": "UTC"}): tzset() yield tzset() From 0147cd1e29a0786a6c0b55b5875b3d363c705bfd Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sun, 21 Mar 2021 11:35:45 +0000 Subject: [PATCH 3090/3170] Move /reference/ to /cli/ --- docs/html/{reference => cli}/index.rst | 0 docs/html/{reference => cli}/pip.rst | 0 docs/html/{reference => cli}/pip_cache.rst | 0 docs/html/{reference => cli}/pip_check.rst | 0 docs/html/{reference => cli}/pip_config.rst | 0 docs/html/{reference => cli}/pip_debug.rst | 0 docs/html/{reference => cli}/pip_download.rst | 0 docs/html/{reference => cli}/pip_freeze.rst | 0 docs/html/{reference => cli}/pip_hash.rst | 0 docs/html/{reference => cli}/pip_install.rst | 0 docs/html/{reference => cli}/pip_list.rst | 0 docs/html/{reference => cli}/pip_search.rst | 0 docs/html/{reference => cli}/pip_show.rst | 0 docs/html/{reference => cli}/pip_uninstall.rst | 0 docs/html/{reference => cli}/pip_wheel.rst | 0 docs/html/index.md | 2 +- 16 files changed, 1 insertion(+), 1 deletion(-) rename docs/html/{reference => cli}/index.rst (100%) rename docs/html/{reference => cli}/pip.rst (100%) rename docs/html/{reference => cli}/pip_cache.rst (100%) rename docs/html/{reference => cli}/pip_check.rst (100%) rename docs/html/{reference => cli}/pip_config.rst (100%) rename docs/html/{reference => cli}/pip_debug.rst (100%) rename docs/html/{reference => cli}/pip_download.rst (100%) rename docs/html/{reference => cli}/pip_freeze.rst (100%) rename docs/html/{reference => cli}/pip_hash.rst (100%) rename docs/html/{reference => cli}/pip_install.rst (100%) rename docs/html/{reference => cli}/pip_list.rst (100%) rename docs/html/{reference => cli}/pip_search.rst (100%) rename docs/html/{reference => cli}/pip_show.rst (100%) rename docs/html/{reference => cli}/pip_uninstall.rst (100%) rename docs/html/{reference => cli}/pip_wheel.rst (100%) diff --git a/docs/html/reference/index.rst b/docs/html/cli/index.rst similarity index 100% rename from docs/html/reference/index.rst rename to docs/html/cli/index.rst diff --git a/docs/html/reference/pip.rst b/docs/html/cli/pip.rst similarity index 100% rename from docs/html/reference/pip.rst rename to docs/html/cli/pip.rst diff --git a/docs/html/reference/pip_cache.rst b/docs/html/cli/pip_cache.rst similarity index 100% rename from docs/html/reference/pip_cache.rst rename to docs/html/cli/pip_cache.rst diff --git a/docs/html/reference/pip_check.rst b/docs/html/cli/pip_check.rst similarity index 100% rename from docs/html/reference/pip_check.rst rename to docs/html/cli/pip_check.rst diff --git a/docs/html/reference/pip_config.rst b/docs/html/cli/pip_config.rst similarity index 100% rename from docs/html/reference/pip_config.rst rename to docs/html/cli/pip_config.rst diff --git a/docs/html/reference/pip_debug.rst b/docs/html/cli/pip_debug.rst similarity index 100% rename from docs/html/reference/pip_debug.rst rename to docs/html/cli/pip_debug.rst diff --git a/docs/html/reference/pip_download.rst b/docs/html/cli/pip_download.rst similarity index 100% rename from docs/html/reference/pip_download.rst rename to docs/html/cli/pip_download.rst diff --git a/docs/html/reference/pip_freeze.rst b/docs/html/cli/pip_freeze.rst similarity index 100% rename from docs/html/reference/pip_freeze.rst rename to docs/html/cli/pip_freeze.rst diff --git a/docs/html/reference/pip_hash.rst b/docs/html/cli/pip_hash.rst similarity index 100% rename from docs/html/reference/pip_hash.rst rename to docs/html/cli/pip_hash.rst diff --git a/docs/html/reference/pip_install.rst b/docs/html/cli/pip_install.rst similarity index 100% rename from docs/html/reference/pip_install.rst rename to docs/html/cli/pip_install.rst diff --git a/docs/html/reference/pip_list.rst b/docs/html/cli/pip_list.rst similarity index 100% rename from docs/html/reference/pip_list.rst rename to docs/html/cli/pip_list.rst diff --git a/docs/html/reference/pip_search.rst b/docs/html/cli/pip_search.rst similarity index 100% rename from docs/html/reference/pip_search.rst rename to docs/html/cli/pip_search.rst diff --git a/docs/html/reference/pip_show.rst b/docs/html/cli/pip_show.rst similarity index 100% rename from docs/html/reference/pip_show.rst rename to docs/html/cli/pip_show.rst diff --git a/docs/html/reference/pip_uninstall.rst b/docs/html/cli/pip_uninstall.rst similarity index 100% rename from docs/html/reference/pip_uninstall.rst rename to docs/html/cli/pip_uninstall.rst diff --git a/docs/html/reference/pip_wheel.rst b/docs/html/cli/pip_wheel.rst similarity index 100% rename from docs/html/reference/pip_wheel.rst rename to docs/html/cli/pip_wheel.rst diff --git a/docs/html/index.md b/docs/html/index.md index 351d8f79ff5..a84c2665d0e 100644 --- a/docs/html/index.md +++ b/docs/html/index.md @@ -13,7 +13,7 @@ install packages from the [Python Package Index][pypi] and other indexes. quickstart installing user_guide -reference/index +cli/index ``` ```{toctree} From b2c5e1078b76edd60cfd0214500ccee4ba89833c Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sun, 21 Mar 2021 11:37:36 +0000 Subject: [PATCH 3091/3170] Rewrite Commands section's landing page - reST -> MyST - Roughly group the commands, based on what they work on. --- docs/html/cli/index.md | 48 +++++++++++++++++++++++++++++++++++++++++ docs/html/cli/index.rst | 21 ------------------ 2 files changed, 48 insertions(+), 21 deletions(-) create mode 100644 docs/html/cli/index.md delete mode 100644 docs/html/cli/index.rst diff --git a/docs/html/cli/index.md b/docs/html/cli/index.md new file mode 100644 index 00000000000..f608da52113 --- /dev/null +++ b/docs/html/cli/index.md @@ -0,0 +1,48 @@ +# Commands + +The general options that apply to all the commands listed below can be +found [under the `pip` page in this section](pip). + +```{toctree} +:maxdepth: 1 +:hidden: + +pip +``` + +```{toctree} +:maxdepth: 1 +:caption: Environment Management and Introspection + +pip_install +pip_uninstall +pip_list +pip_freeze +pip_check +``` + +```{toctree} +:maxdepth: 1 +:caption: Handling Distribution Files + +pip_download +pip_wheel +pip_hash +``` + +```{toctree} +:maxdepth: 1 +:caption: Package Index information + +pip_show +pip_search +``` + +```{toctree} +:maxdepth: 1 +:caption: Managing pip itself + +pip_cache +pip_config +pip_debug +``` diff --git a/docs/html/cli/index.rst b/docs/html/cli/index.rst deleted file mode 100644 index d21b7a9801a..00000000000 --- a/docs/html/cli/index.rst +++ /dev/null @@ -1,21 +0,0 @@ -=============== -Reference Guide -=============== - -.. toctree:: - :maxdepth: 2 - - pip - pip_install - pip_download - pip_uninstall - pip_freeze - pip_list - pip_show - pip_search - pip_cache - pip_check - pip_config - pip_wheel - pip_hash - pip_debug From 3fe1954e10fb96834731a111cb98932635b8cfaa Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Fri, 2 Apr 2021 11:32:19 +0100 Subject: [PATCH 3092/3170] Add redirects from the old /reference/ pages --- docs/html/reference/index.rst | 11 +++++++++++ docs/html/reference/pip.rst | 11 +++++++++++ docs/html/reference/pip_cache.rst | 11 +++++++++++ docs/html/reference/pip_check.rst | 11 +++++++++++ docs/html/reference/pip_config.rst | 11 +++++++++++ docs/html/reference/pip_debug.rst | 11 +++++++++++ docs/html/reference/pip_download.rst | 11 +++++++++++ docs/html/reference/pip_freeze.rst | 11 +++++++++++ docs/html/reference/pip_hash.rst | 11 +++++++++++ docs/html/reference/pip_install.rst | 11 +++++++++++ docs/html/reference/pip_list.rst | 11 +++++++++++ docs/html/reference/pip_search.rst | 11 +++++++++++ docs/html/reference/pip_show.rst | 11 +++++++++++ docs/html/reference/pip_uninstall.rst | 11 +++++++++++ docs/html/reference/pip_wheel.rst | 11 +++++++++++ 15 files changed, 165 insertions(+) create mode 100644 docs/html/reference/index.rst create mode 100644 docs/html/reference/pip.rst create mode 100644 docs/html/reference/pip_cache.rst create mode 100644 docs/html/reference/pip_check.rst create mode 100644 docs/html/reference/pip_config.rst create mode 100644 docs/html/reference/pip_debug.rst create mode 100644 docs/html/reference/pip_download.rst create mode 100644 docs/html/reference/pip_freeze.rst create mode 100644 docs/html/reference/pip_hash.rst create mode 100644 docs/html/reference/pip_install.rst create mode 100644 docs/html/reference/pip_list.rst create mode 100644 docs/html/reference/pip_search.rst create mode 100644 docs/html/reference/pip_show.rst create mode 100644 docs/html/reference/pip_uninstall.rst create mode 100644 docs/html/reference/pip_wheel.rst diff --git a/docs/html/reference/index.rst b/docs/html/reference/index.rst new file mode 100644 index 00000000000..5e81105c9ad --- /dev/null +++ b/docs/html/reference/index.rst @@ -0,0 +1,11 @@ +:orphan: + +.. meta:: + + :http-equiv=refresh: 3; url=../cli/ + +This page has moved +=================== + +You should be redirected automatically in 3 seconds. If that didn't +work, here's a link: :doc:`../cli/index` diff --git a/docs/html/reference/pip.rst b/docs/html/reference/pip.rst new file mode 100644 index 00000000000..53b1c9e0d41 --- /dev/null +++ b/docs/html/reference/pip.rst @@ -0,0 +1,11 @@ +:orphan: + +.. meta:: + + :http-equiv=refresh: 3; url=../../cli/pip/ + +This page has moved +=================== + +You should be redirected automatically in 3 seconds. If that didn't +work, here's a link: :doc:`../cli/pip` diff --git a/docs/html/reference/pip_cache.rst b/docs/html/reference/pip_cache.rst new file mode 100644 index 00000000000..a9cbd69dae5 --- /dev/null +++ b/docs/html/reference/pip_cache.rst @@ -0,0 +1,11 @@ +:orphan: + +.. meta:: + + :http-equiv=refresh: 3; url=../../cli/pip_cache/ + +This page has moved +=================== + +You should be redirected automatically in 3 seconds. If that didn't +work, here's a link: :doc:`../cli/pip_cache` diff --git a/docs/html/reference/pip_check.rst b/docs/html/reference/pip_check.rst new file mode 100644 index 00000000000..5bb7fc84fcb --- /dev/null +++ b/docs/html/reference/pip_check.rst @@ -0,0 +1,11 @@ +:orphan: + +.. meta:: + + :http-equiv=refresh: 3; url=../../cli/pip_check/ + +This page has moved +=================== + +You should be redirected automatically in 3 seconds. If that didn't +work, here's a link: :doc:`../cli/pip_check` diff --git a/docs/html/reference/pip_config.rst b/docs/html/reference/pip_config.rst new file mode 100644 index 00000000000..31a048a513a --- /dev/null +++ b/docs/html/reference/pip_config.rst @@ -0,0 +1,11 @@ +:orphan: + +.. meta:: + + :http-equiv=refresh: 3; url=../../cli/pip_config/ + +This page has moved +=================== + +You should be redirected automatically in 3 seconds. If that didn't +work, here's a link: :doc:`../cli/pip_config` diff --git a/docs/html/reference/pip_debug.rst b/docs/html/reference/pip_debug.rst new file mode 100644 index 00000000000..b0de682751f --- /dev/null +++ b/docs/html/reference/pip_debug.rst @@ -0,0 +1,11 @@ +:orphan: + +.. meta:: + + :http-equiv=refresh: 3; url=../../cli/pip_debug/ + +This page has moved +=================== + +You should be redirected automatically in 3 seconds. If that didn't +work, here's a link: :doc:`../cli/pip_debug` diff --git a/docs/html/reference/pip_download.rst b/docs/html/reference/pip_download.rst new file mode 100644 index 00000000000..d54a7bec554 --- /dev/null +++ b/docs/html/reference/pip_download.rst @@ -0,0 +1,11 @@ +:orphan: + +.. meta:: + + :http-equiv=refresh: 3; url=../../cli/pip_download/ + +This page has moved +=================== + +You should be redirected automatically in 3 seconds. If that didn't +work, here's a link: :doc:`../cli/pip_download` diff --git a/docs/html/reference/pip_freeze.rst b/docs/html/reference/pip_freeze.rst new file mode 100644 index 00000000000..1cf31d5d708 --- /dev/null +++ b/docs/html/reference/pip_freeze.rst @@ -0,0 +1,11 @@ +:orphan: + +.. meta:: + + :http-equiv=refresh: 3; url=../../cli/pip_freeze/ + +This page has moved +=================== + +You should be redirected automatically in 3 seconds. If that didn't +work, here's a link: :doc:`../cli/pip_freeze` diff --git a/docs/html/reference/pip_hash.rst b/docs/html/reference/pip_hash.rst new file mode 100644 index 00000000000..6112bec5fa3 --- /dev/null +++ b/docs/html/reference/pip_hash.rst @@ -0,0 +1,11 @@ +:orphan: + +.. meta:: + + :http-equiv=refresh: 3; url=../../cli/pip_hash/ + +This page has moved +=================== + +You should be redirected automatically in 3 seconds. If that didn't +work, here's a link: :doc:`../cli/pip_hash` diff --git a/docs/html/reference/pip_install.rst b/docs/html/reference/pip_install.rst new file mode 100644 index 00000000000..580900cfb93 --- /dev/null +++ b/docs/html/reference/pip_install.rst @@ -0,0 +1,11 @@ +:orphan: + +.. meta:: + + :http-equiv=refresh: 3; url=../../cli/pip_install/ + +This page has moved +=================== + +You should be redirected automatically in 3 seconds. If that didn't +work, here's a link: :doc:`../cli/pip_install` diff --git a/docs/html/reference/pip_list.rst b/docs/html/reference/pip_list.rst new file mode 100644 index 00000000000..3768baf60d6 --- /dev/null +++ b/docs/html/reference/pip_list.rst @@ -0,0 +1,11 @@ +:orphan: + +.. meta:: + + :http-equiv=refresh: 3; url=../../cli/pip_list/ + +This page has moved +=================== + +You should be redirected automatically in 3 seconds. If that didn't +work, here's a link: :doc:`../cli/pip_list` diff --git a/docs/html/reference/pip_search.rst b/docs/html/reference/pip_search.rst new file mode 100644 index 00000000000..0a7532ee79d --- /dev/null +++ b/docs/html/reference/pip_search.rst @@ -0,0 +1,11 @@ +:orphan: + +.. meta:: + + :http-equiv=refresh: 3; url=../../cli/pip_search/ + +This page has moved +=================== + +You should be redirected automatically in 3 seconds. If that didn't +work, here's a link: :doc:`../cli/pip_search` diff --git a/docs/html/reference/pip_show.rst b/docs/html/reference/pip_show.rst new file mode 100644 index 00000000000..b2ce3c7d8c3 --- /dev/null +++ b/docs/html/reference/pip_show.rst @@ -0,0 +1,11 @@ +:orphan: + +.. meta:: + + :http-equiv=refresh: 3; url=../../cli/pip_show/ + +This page has moved +=================== + +You should be redirected automatically in 3 seconds. If that didn't +work, here's a link: :doc:`../cli/pip_show` diff --git a/docs/html/reference/pip_uninstall.rst b/docs/html/reference/pip_uninstall.rst new file mode 100644 index 00000000000..db84476c859 --- /dev/null +++ b/docs/html/reference/pip_uninstall.rst @@ -0,0 +1,11 @@ +:orphan: + +.. meta:: + + :http-equiv=refresh: 3; url=../../cli/pip_uninstall/ + +This page has moved +=================== + +You should be redirected automatically in 3 seconds. If that didn't +work, here's a link: :doc:`../cli/pip_uninstall` diff --git a/docs/html/reference/pip_wheel.rst b/docs/html/reference/pip_wheel.rst new file mode 100644 index 00000000000..06861f60763 --- /dev/null +++ b/docs/html/reference/pip_wheel.rst @@ -0,0 +1,11 @@ +:orphan: + +.. meta:: + + :http-equiv=refresh: 3; url=../../cli/pip_wheel/ + +This page has moved +=================== + +You should be redirected automatically in 3 seconds. If that didn't +work, here's a link: :doc:`../cli/pip_wheel` From ebb80861eea893f470a4eddd7294474d957a11dc Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Fri, 2 Apr 2021 10:21:40 +0100 Subject: [PATCH 3093/3170] Blacken tests/lib --- .pre-commit-config.yaml | 1 - tests/lib/__init__.py | 460 ++++++++++++++++------------- tests/lib/certs.py | 12 +- tests/lib/configuration_helpers.py | 5 +- tests/lib/filesystem.py | 4 +- tests/lib/git_submodule_helpers.py | 46 +-- tests/lib/index.py | 6 +- tests/lib/local_repos.py | 26 +- tests/lib/options_helpers.py | 10 +- tests/lib/path.py | 3 +- tests/lib/requests_mocks.py | 6 +- tests/lib/server.py | 58 ++-- tests/lib/test_lib.py | 140 +++++---- tests/lib/test_wheel.py | 13 +- tests/lib/venv.py | 53 ++-- tests/lib/wheel.py | 70 ++--- 16 files changed, 485 insertions(+), 428 deletions(-) diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index b3c61d7843c..a343d1a99a1 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -32,7 +32,6 @@ repos: ^src/pip/_internal/\w+\.py$| ^tools/| # Tests - ^tests/lib| ^tests/data| ^tests/unit| ^tests/functional/(?!test_install)| diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index 98474dda46f..9f99bcef020 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -48,12 +48,12 @@ def path_to_url(path): path = os.path.normpath(os.path.abspath(path)) drive, path = os.path.splitdrive(path) filepath = path.split(os.path.sep) - url = '/'.join(filepath) + url = "/".join(filepath) if drive: # Note: match urllib.request.pathname2url's # behavior: uppercase the drive letter. - return 'file:///' + drive.upper() + url - return 'file://' + url + return "file:///" + drive.upper() + url + return "file://" + url def _test_path_to_file_url(path): @@ -63,12 +63,11 @@ def _test_path_to_file_url(path): Args: path: a tests.lib.path.Path object. """ - return 'file://' + path.resolve().replace('\\', '/') + return "file://" + path.resolve().replace("\\", "/") def create_file(path, contents=None): - """Create a file on the path, with the given contents - """ + """Create a file on the path, with the given contents""" from pip._internal.utils.misc import ensure_dir ensure_dir(os.path.dirname(path)) @@ -94,7 +93,7 @@ def make_test_search_scope( def make_test_link_collector( find_links=None, # type: Optional[List[str]] index_urls=None, # type: Optional[List[str]] - session=None, # type: Optional[PipSession] + session=None, # type: Optional[PipSession] ): # type: (...) -> LinkCollector """ @@ -115,8 +114,8 @@ def make_test_finder( find_links=None, # type: Optional[List[str]] index_urls=None, # type: Optional[List[str]] allow_all_prereleases=False, # type: bool - session=None, # type: Optional[PipSession] - target_python=None, # type: Optional[TargetPython] + session=None, # type: Optional[PipSession] + target_python=None, # type: Optional[TargetPython] ): # type: (...) -> PackageFinder """ @@ -220,51 +219,59 @@ class TestFailure(AssertionError): """ An "assertion" failed during testing. """ + pass class TestPipResult: - def __init__(self, impl, verbose=False): self._impl = impl if verbose: print(self.stdout) if self.stderr: - print('======= stderr ========') + print("======= stderr ========") print(self.stderr) - print('=======================') + print("=======================") def __getattr__(self, attr): return getattr(self._impl, attr) - if sys.platform == 'win32': + if sys.platform == "win32": @property def stdout(self): - return self._impl.stdout.replace('\r\n', '\n') + return self._impl.stdout.replace("\r\n", "\n") @property def stderr(self): - return self._impl.stderr.replace('\r\n', '\n') + return self._impl.stderr.replace("\r\n", "\n") def __str__(self): - return str(self._impl).replace('\r\n', '\n') + return str(self._impl).replace("\r\n", "\n") + else: # Python doesn't automatically forward __str__ through __getattr__ def __str__(self): return str(self._impl) - def assert_installed(self, pkg_name, editable=True, with_files=None, - without_files=None, without_egg_link=False, - use_user_site=False, sub_dir=False): + def assert_installed( + self, + pkg_name, + editable=True, + with_files=None, + without_files=None, + without_egg_link=False, + use_user_site=False, + sub_dir=False, + ): with_files = with_files or [] without_files = without_files or [] e = self.test_env if editable: - pkg_dir = e.venv / 'src' / pkg_name.lower() + pkg_dir = e.venv / "src" / pkg_name.lower() # If package was installed in a sub directory if sub_dir: pkg_dir = pkg_dir / sub_dir @@ -273,72 +280,75 @@ def assert_installed(self, pkg_name, editable=True, with_files=None, pkg_dir = e.site_packages / pkg_name if use_user_site: - egg_link_path = e.user_site / pkg_name + '.egg-link' + egg_link_path = e.user_site / pkg_name + ".egg-link" else: - egg_link_path = e.site_packages / pkg_name + '.egg-link' + egg_link_path = e.site_packages / pkg_name + ".egg-link" if without_egg_link: if egg_link_path in self.files_created: raise TestFailure( - 'unexpected egg link file created: ' - f'{egg_link_path!r}\n{self}' + "unexpected egg link file created: " f"{egg_link_path!r}\n{self}" ) else: if egg_link_path not in self.files_created: raise TestFailure( - 'expected egg link file missing: ' - f'{egg_link_path!r}\n{self}' + "expected egg link file missing: " f"{egg_link_path!r}\n{self}" ) egg_link_file = self.files_created[egg_link_path] - egg_link_contents = egg_link_file.bytes.replace(os.linesep, '\n') + egg_link_contents = egg_link_file.bytes.replace(os.linesep, "\n") # FIXME: I don't understand why there's a trailing . here - if not (egg_link_contents.endswith('\n.') and - egg_link_contents[:-2].endswith(pkg_dir)): - expected_ending = pkg_dir + '\n.' - raise TestFailure(textwrap.dedent( - f'''\ + if not ( + egg_link_contents.endswith("\n.") + and egg_link_contents[:-2].endswith(pkg_dir) + ): + expected_ending = pkg_dir + "\n." + raise TestFailure( + textwrap.dedent( + f"""\ Incorrect egg_link file {egg_link_file!r} Expected ending: {expected_ending!r} ------- Actual contents ------- {egg_link_contents!r} - -------------------------------''' - )) + -------------------------------""" + ) + ) if use_user_site: - pth_file = e.user_site / 'easy-install.pth' + pth_file = e.user_site / "easy-install.pth" else: - pth_file = e.site_packages / 'easy-install.pth' + pth_file = e.site_packages / "easy-install.pth" if (pth_file in self.files_updated) == without_egg_link: - maybe = '' if without_egg_link else 'not ' - raise TestFailure( - f'{pth_file} unexpectedly {maybe}updated by install' - ) + maybe = "" if without_egg_link else "not " + raise TestFailure(f"{pth_file} unexpectedly {maybe}updated by install") if (pkg_dir in self.files_created) == (curdir in without_files): - maybe = 'not ' if curdir in without_files else '' + maybe = "not " if curdir in without_files else "" files = sorted(self.files_created) - raise TestFailure(textwrap.dedent(f'''\ + raise TestFailure( + textwrap.dedent( + f"""\ expected package directory {pkg_dir!r} {maybe}to be created actually created: {files} - ''')) + """ + ) + ) for f in with_files: normalized_path = os.path.normpath(pkg_dir / f) if normalized_path not in self.files_created: raise TestFailure( - f'Package directory {pkg_dir!r} missing ' - f'expected content {f!r}' + f"Package directory {pkg_dir!r} missing " f"expected content {f!r}" ) for f in without_files: normalized_path = os.path.normpath(pkg_dir / f) if normalized_path in self.files_created: raise TestFailure( - f'Package directory {pkg_dir!r} has unexpected content {f}' + f"Package directory {pkg_dir!r} has unexpected content {f}" ) def did_create(self, path, message=None): @@ -355,8 +365,7 @@ def did_not_update(self, path, message=None): def _one_or_both(a, b): - """Returns f"{a}\n{b}" if a is truthy, else returns str(b). - """ + """Returns f"{a}\n{b}" if a is truthy, else returns str(b).""" if not a: return str(b) @@ -367,15 +376,19 @@ def make_check_stderr_message(stderr, line, reason): """ Create an exception message to use inside check_stderr(). """ - return dedent("""\ + return dedent( + """\ {reason}: Caused by line: {line!r} Complete stderr: {stderr} - """).format(stderr=stderr, line=line, reason=reason) + """ + ).format(stderr=stderr, line=line, reason=reason) def _check_stderr( - stderr, allow_stderr_warning, allow_stderr_error, + stderr, + allow_stderr_warning, + allow_stderr_error, ): """ Check the given stderr for logged warnings and errors. @@ -396,29 +409,29 @@ def _check_stderr( # sent directly to stderr and so bypass any configured log formatter. # The "--- Logging error ---" string is used in Python 3.4+, and # "Logged from file " is used in Python 2. - if (line.startswith('--- Logging error ---') or - line.startswith('Logged from file ')): - reason = 'stderr has a logging error, which is never allowed' + if line.startswith("--- Logging error ---") or line.startswith( + "Logged from file " + ): + reason = "stderr has a logging error, which is never allowed" msg = make_check_stderr_message(stderr, line=line, reason=reason) raise RuntimeError(msg) if allow_stderr_error: continue - if line.startswith('ERROR: '): + if line.startswith("ERROR: "): reason = ( - 'stderr has an unexpected error ' - '(pass allow_stderr_error=True to permit this)' + "stderr has an unexpected error " + "(pass allow_stderr_error=True to permit this)" ) msg = make_check_stderr_message(stderr, line=line, reason=reason) raise RuntimeError(msg) if allow_stderr_warning: continue - if (line.startswith('WARNING: ') or - line.startswith(DEPRECATION_MSG_PREFIX)): + if line.startswith("WARNING: ") or line.startswith(DEPRECATION_MSG_PREFIX): reason = ( - 'stderr has an unexpected warning ' - '(pass allow_stderr_warning=True to permit this)' + "stderr has an unexpected warning " + "(pass allow_stderr_warning=True to permit this)" ) msg = make_check_stderr_message(stderr, line=line, reason=reason) raise RuntimeError(msg) @@ -438,7 +451,7 @@ class PipTestEnvironment(TestFileEnvironment): # a name of the form xxxx_path and relative paths have a name that # does not end in '_path'. - exe = sys.platform == 'win32' and '.exe' or '' + exe = sys.platform == "win32" and ".exe" or "" verbose = False def __init__(self, base_path, *args, virtualenv, pip_expect_warning=None, **kwargs): @@ -454,16 +467,16 @@ def __init__(self, base_path, *args, virtualenv, pip_expect_warning=None, **kwar self.user_base_path = self.venv_path.joinpath("user") self.user_site_path = self.venv_path.joinpath( "user", - site.USER_SITE[len(site.USER_BASE) + 1:], + site.USER_SITE[len(site.USER_BASE) + 1 :], ) - if sys.platform == 'win32': + if sys.platform == "win32": if sys.version_info >= (3, 5): scripts_base = Path( - os.path.normpath(self.user_site_path.joinpath('..')) + os.path.normpath(self.user_site_path.joinpath("..")) ) else: scripts_base = self.user_base_path - self.user_bin_path = scripts_base.joinpath('Scripts') + self.user_bin_path = scripts_base.joinpath("Scripts") else: self.user_bin_path = self.user_base_path.joinpath( os.path.relpath(self.bin_path, self.venv_path) @@ -495,12 +508,21 @@ def __init__(self, base_path, *args, virtualenv, pip_expect_warning=None, **kwar super().__init__(base_path, *args, **kwargs) # Expand our absolute path directories into relative - for name in ["base", "venv", "bin", "lib", "site_packages", - "user_base", "user_site", "user_bin", "scratch"]: + for name in [ + "base", + "venv", + "bin", + "lib", + "site_packages", + "user_base", + "user_site", + "user_bin", + "scratch", + ]: real_name = f"{name}_path" - relative_path = Path(os.path.relpath( - getattr(self, real_name), self.base_path - )) + relative_path = Path( + os.path.relpath(getattr(self, real_name), self.base_path) + ) setattr(self, name, relative_path) # Make sure temp_path is a Path object @@ -514,7 +536,7 @@ def __init__(self, base_path, *args, virtualenv, pip_expect_warning=None, **kwar self.user_site_path.joinpath("easy-install.pth").touch() def _ignore_file(self, fn): - if fn.endswith('__pycache__') or fn.endswith(".pyc"): + if fn.endswith("__pycache__") or fn.endswith(".pyc"): result = True else: result = super()._ignore_file(fn) @@ -525,7 +547,7 @@ def _find_traverse(self, path, result): # results because of venv `lib64 -> lib/` symlink on Linux. full = os.path.join(self.base_path, path) if os.path.isdir(full) and os.path.islink(full): - if not self.temp_path or path != 'tmp': + if not self.temp_path or path != "tmp": result[path] = FoundDir(self.base_path, path) else: super()._find_traverse(path, result) @@ -560,42 +582,40 @@ def run( compatibility. """ if self.verbose: - print(f'>> running {args} {kw}') + print(f">> running {args} {kw}") assert not cwd or not run_from, "Don't use run_from; it's going away" cwd = cwd or run_from or self.cwd - if sys.platform == 'win32': + if sys.platform == "win32": # Partial fix for ScriptTest.run using `shell=True` on Windows. - args = [str(a).replace('^', '^^').replace('&', '^&') for a in args] + args = [str(a).replace("^", "^^").replace("&", "^&") for a in args] if allow_error: - kw['expect_error'] = True + kw["expect_error"] = True # Propagate default values. - expect_error = kw.get('expect_error') + expect_error = kw.get("expect_error") if expect_error: # Then default to allowing logged errors. if allow_stderr_error is not None and not allow_stderr_error: raise RuntimeError( - 'cannot pass allow_stderr_error=False with ' - 'expect_error=True' + "cannot pass allow_stderr_error=False with " "expect_error=True" ) allow_stderr_error = True - elif kw.get('expect_stderr'): + elif kw.get("expect_stderr"): # Then default to allowing logged warnings. if allow_stderr_warning is not None and not allow_stderr_warning: raise RuntimeError( - 'cannot pass allow_stderr_warning=False with ' - 'expect_stderr=True' + "cannot pass allow_stderr_warning=False with " "expect_stderr=True" ) allow_stderr_warning = True if allow_stderr_error: if allow_stderr_warning is not None and not allow_stderr_warning: raise RuntimeError( - 'cannot pass allow_stderr_warning=False with ' - 'allow_stderr_error=True' + "cannot pass allow_stderr_warning=False with " + "allow_stderr_error=True" ) # Default values if not set. @@ -606,7 +626,7 @@ def run( # Pass expect_stderr=True to allow any stderr. We do this because # we do our checking of stderr further on in check_stderr(). - kw['expect_stderr'] = True + kw["expect_stderr"] = True result = super().run(cwd=cwd, *args, **kw) if expect_error and not allow_error: @@ -615,7 +635,8 @@ def run( raise AssertionError("Script passed unexpectedly.") _check_stderr( - result.stderr, allow_stderr_error=allow_stderr_error, + result.stderr, + allow_stderr_error=allow_stderr_error, allow_stderr_warning=allow_stderr_warning, ) @@ -624,24 +645,27 @@ def run( def pip(self, *args, use_module=True, **kwargs): __tracebackhide__ = True if self.pip_expect_warning: - kwargs['allow_stderr_warning'] = True + kwargs["allow_stderr_warning"] = True if use_module: - exe = 'python' - args = ('-m', 'pip') + args + exe = "python" + args = ("-m", "pip") + args else: - exe = 'pip' + exe = "pip" return self.run(exe, *args, **kwargs) def pip_install_local(self, *args, **kwargs): return self.pip( - "install", "--no-index", - "--find-links", path_to_url(os.path.join(DATA_DIR, "packages")), - *args, **kwargs + "install", + "--no-index", + "--find-links", + path_to_url(os.path.join(DATA_DIR, "packages")), + *args, + **kwargs, ) def easy_install(self, *args, **kwargs): - args = ('-m', 'easy_install') + args - return self.run('python', *args, **kwargs) + args = ("-m", "easy_install") + args + return self.run("python", *args, **kwargs) # FIXME ScriptTest does something similar, but only within a single @@ -679,15 +703,15 @@ def prefix_match(path, prefix): prefix = prefix.rstrip(os.path.sep) + os.path.sep return path.startswith(prefix) - start_keys = {k for k in start.keys() - if not any([prefix_match(k, i) for i in ignore])} - end_keys = {k for k in end.keys() - if not any([prefix_match(k, i) for i in ignore])} + start_keys = { + k for k in start.keys() if not any([prefix_match(k, i) for i in ignore]) + } + end_keys = {k for k in end.keys() if not any([prefix_match(k, i) for i in ignore])} deleted = {k: start[k] for k in start_keys.difference(end_keys)} created = {k: end[k] for k in end_keys.difference(start_keys)} updated = {} for k in start_keys.intersection(end_keys): - if (start[k].size != end[k].size): + if start[k].size != end[k].size: updated[k] = end[k] return dict(deleted=deleted, created=created, updated=updated) @@ -716,8 +740,10 @@ def assert_all_changes(start_state, end_state, expected_changes): diff = diff_states(start_files, end_files, ignore=expected_changes) if list(diff.values()) != [{}, {}, {}]: - raise TestFailure('Unexpected changes:\n' + '\n'.join( - [k + ': ' + ', '.join(v.keys()) for k, v in diff.items()])) + raise TestFailure( + "Unexpected changes:\n" + + "\n".join([k + ": " + ", ".join(v.keys()) for k, v in diff.items()]) + ) # Don't throw away this potentially useful information return diff @@ -728,14 +754,18 @@ def _create_main_file(dir_path, name=None, output=None): Create a module with a main() function that prints the given output. """ if name is None: - name = 'version_pkg' + name = "version_pkg" if output is None: - output = '0.1' - text = textwrap.dedent("""\ + output = "0.1" + text = textwrap.dedent( + """\ def main(): print({!r}) - """.format(output)) - filename = f'{name}.py' + """.format( + output + ) + ) + filename = f"{name}.py" dir_path.joinpath(filename).write_text(text) @@ -755,7 +785,7 @@ def _git_commit( message: an optional commit message. """ if message is None: - message = 'test commit' + message = "test commit" args = [] @@ -766,100 +796,121 @@ def _git_commit( args.append("--all") new_args = [ - 'git', 'commit', '-q', '--author', 'pip <distutils-sig@python.org>', + "git", + "commit", + "-q", + "--author", + "pip <distutils-sig@python.org>", ] new_args.extend(args) - new_args.extend(['-m', message]) + new_args.extend(["-m", message]) env_or_script.run(*new_args, cwd=repo_dir) -def _vcs_add(script, version_pkg_path, vcs='git'): - if vcs == 'git': - script.run('git', 'init', cwd=version_pkg_path) - script.run('git', 'add', '.', cwd=version_pkg_path) - _git_commit(script, version_pkg_path, message='initial version') - elif vcs == 'hg': - script.run('hg', 'init', cwd=version_pkg_path) - script.run('hg', 'add', '.', cwd=version_pkg_path) +def _vcs_add(script, version_pkg_path, vcs="git"): + if vcs == "git": + script.run("git", "init", cwd=version_pkg_path) + script.run("git", "add", ".", cwd=version_pkg_path) + _git_commit(script, version_pkg_path, message="initial version") + elif vcs == "hg": + script.run("hg", "init", cwd=version_pkg_path) + script.run("hg", "add", ".", cwd=version_pkg_path) script.run( - 'hg', 'commit', '-q', - '--user', 'pip <distutils-sig@python.org>', - '-m', 'initial version', cwd=version_pkg_path, + "hg", + "commit", + "-q", + "--user", + "pip <distutils-sig@python.org>", + "-m", + "initial version", + cwd=version_pkg_path, ) - elif vcs == 'svn': + elif vcs == "svn": repo_url = _create_svn_repo(script, version_pkg_path) script.run( - 'svn', 'checkout', repo_url, 'pip-test-package', - cwd=script.scratch_path + "svn", "checkout", repo_url, "pip-test-package", cwd=script.scratch_path ) - checkout_path = script.scratch_path / 'pip-test-package' + checkout_path = script.scratch_path / "pip-test-package" # svn internally stores windows drives as uppercase; we'll match that. - checkout_path = checkout_path.replace('c:', 'C:') + checkout_path = checkout_path.replace("c:", "C:") version_pkg_path = checkout_path - elif vcs == 'bazaar': - script.run('bzr', 'init', cwd=version_pkg_path) - script.run('bzr', 'add', '.', cwd=version_pkg_path) + elif vcs == "bazaar": + script.run("bzr", "init", cwd=version_pkg_path) + script.run("bzr", "add", ".", cwd=version_pkg_path) script.run( - 'bzr', 'whoami', 'pip <distutils-sig@python.org>', - cwd=version_pkg_path) + "bzr", "whoami", "pip <distutils-sig@python.org>", cwd=version_pkg_path + ) script.run( - 'bzr', 'commit', '-q', - '--author', 'pip <distutils-sig@python.org>', - '-m', 'initial version', cwd=version_pkg_path, + "bzr", + "commit", + "-q", + "--author", + "pip <distutils-sig@python.org>", + "-m", + "initial version", + cwd=version_pkg_path, ) else: - raise ValueError(f'Unknown vcs: {vcs}') + raise ValueError(f"Unknown vcs: {vcs}") return version_pkg_path def _create_test_package_with_subdirectory(script, subdirectory): script.scratch_path.joinpath("version_pkg").mkdir() - version_pkg_path = script.scratch_path / 'version_pkg' + version_pkg_path = script.scratch_path / "version_pkg" _create_main_file(version_pkg_path, name="version_pkg", output="0.1") version_pkg_path.joinpath("setup.py").write_text( - textwrap.dedent(""" + textwrap.dedent( + """ from setuptools import setup, find_packages setup(name='version_pkg', version='0.1', packages=find_packages(), py_modules=['version_pkg'], entry_points=dict(console_scripts=['version_pkg=version_pkg:main'])) - """)) + """ + ) + ) subdirectory_path = version_pkg_path.joinpath(subdirectory) subdirectory_path.mkdir() _create_main_file(subdirectory_path, name="version_subpkg", output="0.1") - subdirectory_path.joinpath('setup.py').write_text( - textwrap.dedent(""" + subdirectory_path.joinpath("setup.py").write_text( + textwrap.dedent( + """ from setuptools import setup, find_packages setup(name='version_subpkg', version='0.1', packages=find_packages(), py_modules=['version_subpkg'], entry_points=dict(console_scripts=['version_pkg=version_subpkg:main'])) - """)) + """ + ) + ) - script.run('git', 'init', cwd=version_pkg_path) - script.run('git', 'add', '.', cwd=version_pkg_path) - _git_commit(script, version_pkg_path, message='initial version') + script.run("git", "init", cwd=version_pkg_path) + script.run("git", "add", ".", cwd=version_pkg_path) + _git_commit(script, version_pkg_path, message="initial version") return version_pkg_path -def _create_test_package_with_srcdir(script, name='version_pkg', vcs='git'): +def _create_test_package_with_srcdir(script, name="version_pkg", vcs="git"): script.scratch_path.joinpath(name).mkdir() version_pkg_path = script.scratch_path / name - subdir_path = version_pkg_path.joinpath('subdir') + subdir_path = version_pkg_path.joinpath("subdir") subdir_path.mkdir() - src_path = subdir_path.joinpath('src') + src_path = subdir_path.joinpath("src") src_path.mkdir() - pkg_path = src_path.joinpath('pkg') + pkg_path = src_path.joinpath("pkg") pkg_path.mkdir() - pkg_path.joinpath('__init__.py').write_text('') - subdir_path.joinpath("setup.py").write_text(textwrap.dedent(""" + pkg_path.joinpath("__init__.py").write_text("") + subdir_path.joinpath("setup.py").write_text( + textwrap.dedent( + """ from setuptools import setup, find_packages setup( name='{name}', @@ -867,15 +918,21 @@ def _create_test_package_with_srcdir(script, name='version_pkg', vcs='git'): packages=find_packages(), package_dir={{'': 'src'}}, ) - """.format(name=name))) + """.format( + name=name + ) + ) + ) return _vcs_add(script, version_pkg_path, vcs) -def _create_test_package(script, name='version_pkg', vcs='git'): +def _create_test_package(script, name="version_pkg", vcs="git"): script.scratch_path.joinpath(name).mkdir() version_pkg_path = script.scratch_path / name - _create_main_file(version_pkg_path, name=name, output='0.1') - version_pkg_path.joinpath("setup.py").write_text(textwrap.dedent(""" + _create_main_file(version_pkg_path, name=name, output="0.1") + version_pkg_path.joinpath("setup.py").write_text( + textwrap.dedent( + """ from setuptools import setup, find_packages setup( name='{name}', @@ -884,33 +941,35 @@ def _create_test_package(script, name='version_pkg', vcs='git'): py_modules=['{name}'], entry_points=dict(console_scripts=['{name}={name}:main']) ) - """.format(name=name))) + """.format( + name=name + ) + ) + ) return _vcs_add(script, version_pkg_path, vcs) def _create_svn_repo(script, version_pkg_path): - repo_url = path_to_url( - script.scratch_path / 'pip-test-package-repo' / 'trunk') - script.run( - 'svnadmin', 'create', 'pip-test-package-repo', - cwd=script.scratch_path - ) + repo_url = path_to_url(script.scratch_path / "pip-test-package-repo" / "trunk") + script.run("svnadmin", "create", "pip-test-package-repo", cwd=script.scratch_path) script.run( - 'svn', 'import', version_pkg_path, repo_url, - '-m', 'Initial import of pip-test-package', - cwd=script.scratch_path + "svn", + "import", + version_pkg_path, + repo_url, + "-m", + "Initial import of pip-test-package", + cwd=script.scratch_path, ) return repo_url def _change_test_package_version(script, version_pkg_path): _create_main_file( - version_pkg_path, name='version_pkg', output='some different version' + version_pkg_path, name="version_pkg", output="some different version" ) # Pass -a to stage the change to the main file. - _git_commit( - script, version_pkg_path, message='messed version', stage_modified=True - ) + _git_commit(script, version_pkg_path, message="messed version", stage_modified=True) def assert_raises_regexp(exception, reg, run, *args, **kwargs): @@ -935,21 +994,25 @@ def requirements_file(contents, tmpdir): :param tmpdir: A Path to the folder in which to create the file """ - path = tmpdir / 'reqs.txt' + path = tmpdir / "reqs.txt" path.write_text(contents) yield path path.unlink() def create_test_package_with_setup(script, **setup_kwargs): - assert 'name' in setup_kwargs, setup_kwargs - pkg_path = script.scratch_path / setup_kwargs['name'] + assert "name" in setup_kwargs, setup_kwargs + pkg_path = script.scratch_path / setup_kwargs["name"] pkg_path.mkdir() - pkg_path.joinpath("setup.py").write_text(textwrap.dedent(f""" + pkg_path.joinpath("setup.py").write_text( + textwrap.dedent( + f""" from setuptools import setup kwargs = {setup_kwargs!r} setup(**kwargs) - """)) + """ + ) + ) return pkg_path @@ -961,9 +1024,7 @@ def urlsafe_b64encode_nopad(data): def create_really_basic_wheel(name, version): # type: (str, str) -> bytes def digest(contents): - return "sha256={}".format( - urlsafe_b64encode_nopad(sha256(contents).digest()) - ) + return "sha256={}".format(urlsafe_b64encode_nopad(sha256(contents).digest())) def add_file(path, text): contents = text.encode("utf-8") @@ -983,7 +1044,9 @@ def add_file(path, text): Metadata-Version: 2.1 Name: {} Version: {} - """.format(name, version) + """.format( + name, version + ) ), ) z.writestr(record_path, "\n".join(",".join(r) for r in records)) @@ -1043,7 +1106,6 @@ def hello(): metadata_updates=metadata_updates, extra_metadata_files={"top_level.txt": name}, extra_files=extra_files, - # Have an empty RECORD because we don't want to be checking hashes. record="", ) @@ -1052,9 +1114,7 @@ def hello(): return archive_path -def create_basic_sdist_for_package( - script, name, version, extra_files=None -): +def create_basic_sdist_for_package(script, name, version, extra_files=None): files = { "setup.py": """ from setuptools import find_packages, setup @@ -1063,17 +1123,13 @@ def create_basic_sdist_for_package( } # Some useful shorthands - archive_name = "{name}-{version}.tar.gz".format( - name=name, version=version - ) + archive_name = "{name}-{version}.tar.gz".format(name=name, version=version) # Replace key-values with formatted values for key, value in list(files.items()): del files[key] key = key.format(name=name) - files[key] = textwrap.dedent(value).format( - name=name, version=version - ).strip() + files[key] = textwrap.dedent(value).format(name=name, version=version).strip() # Add new files after formatting if extra_files: @@ -1087,7 +1143,7 @@ def create_basic_sdist_for_package( retval = script.scratch_path / archive_name generated = shutil.make_archive( retval, - 'gztar', + "gztar", root_dir=script.temp_path, base_dir=os.curdir, ) @@ -1104,15 +1160,15 @@ def wrapper(fn): try: subprocess.check_output(check_cmd) except (OSError, subprocess.CalledProcessError): - return pytest.mark.skip( - reason=f'{name} is not available')(fn) + return pytest.mark.skip(reason=f"{name} is not available")(fn) return fn + return wrapper def is_bzr_installed(): try: - subprocess.check_output(('bzr', 'version', '--short')) + subprocess.check_output(("bzr", "version", "--short")) except OSError: return False return True @@ -1120,27 +1176,23 @@ def is_bzr_installed(): def is_svn_installed(): try: - subprocess.check_output(('svn', '--version')) + subprocess.check_output(("svn", "--version")) except OSError: return False return True def need_bzr(fn): - return pytest.mark.bzr(need_executable( - 'Bazaar', ('bzr', 'version', '--short') - )(fn)) + return pytest.mark.bzr(need_executable("Bazaar", ("bzr", "version", "--short"))(fn)) def need_svn(fn): - return pytest.mark.svn(need_executable( - 'Subversion', ('svn', '--version') - )(need_executable( - 'Subversion Admin', ('svnadmin', '--version') - )(fn))) + return pytest.mark.svn( + need_executable("Subversion", ("svn", "--version"))( + need_executable("Subversion Admin", ("svnadmin", "--version"))(fn) + ) + ) def need_mercurial(fn): - return pytest.mark.mercurial(need_executable( - 'Mercurial', ('hg', 'version') - )(fn)) + return pytest.mark.mercurial(need_executable("Mercurial", ("hg", "version"))(fn)) diff --git a/tests/lib/certs.py b/tests/lib/certs.py index b3a9b8e1046..6d69395b291 100644 --- a/tests/lib/certs.py +++ b/tests/lib/certs.py @@ -11,13 +11,13 @@ def make_tls_cert(hostname): # type: (str) -> Tuple[x509.Certificate, rsa.RSAPrivateKey] key = rsa.generate_private_key( - public_exponent=65537, - key_size=2048, - backend=default_backend() + public_exponent=65537, key_size=2048, backend=default_backend() + ) + subject = issuer = x509.Name( + [ + x509.NameAttribute(NameOID.COMMON_NAME, hostname), + ] ) - subject = issuer = x509.Name([ - x509.NameAttribute(NameOID.COMMON_NAME, hostname), - ]) cert = ( x509.CertificateBuilder() .subject_name(subject) diff --git a/tests/lib/configuration_helpers.py b/tests/lib/configuration_helpers.py index 384a424e2d0..5b20aafa1e4 100644 --- a/tests/lib/configuration_helpers.py +++ b/tests/lib/configuration_helpers.py @@ -15,7 +15,6 @@ class ConfigurationMixin: - def setup(self): self.configuration = pip._internal.configuration.Configuration( isolated=False, @@ -41,9 +40,7 @@ def overridden(): @contextlib.contextmanager def tmpfile(self, contents): # Create a temporary file - fd, path = tempfile.mkstemp( - prefix="pip_", suffix="_config.ini", text=True - ) + fd, path = tempfile.mkstemp(prefix="pip_", suffix="_config.ini", text=True) os.close(fd) contents = textwrap.dedent(contents).lstrip() diff --git a/tests/lib/filesystem.py b/tests/lib/filesystem.py index dc14b323e33..05e2db62cfd 100644 --- a/tests/lib/filesystem.py +++ b/tests/lib/filesystem.py @@ -43,6 +43,4 @@ def join(dirpath, dirnames, filenames): (join_dirpath(p) for p in filenames), ) - return set(chain.from_iterable( - join(*dirinfo) for dirinfo in os.walk(base) - )) + return set(chain.from_iterable(join(*dirinfo) for dirinfo in os.walk(base))) diff --git a/tests/lib/git_submodule_helpers.py b/tests/lib/git_submodule_helpers.py index 494d329cac1..220a926b57a 100644 --- a/tests/lib/git_submodule_helpers.py +++ b/tests/lib/git_submodule_helpers.py @@ -5,11 +5,11 @@ def _create_test_package_submodule(env): env.scratch_path.joinpath("version_pkg_submodule").mkdir() - submodule_path = env.scratch_path / 'version_pkg_submodule' - env.run('touch', 'testfile', cwd=submodule_path) - env.run('git', 'init', cwd=submodule_path) - env.run('git', 'add', '.', cwd=submodule_path) - _git_commit(env, submodule_path, message='initial version / submodule') + submodule_path = env.scratch_path / "version_pkg_submodule" + env.run("touch", "testfile", cwd=submodule_path) + env.run("git", "init", cwd=submodule_path) + env.run("git", "add", ".", cwd=submodule_path) + _git_commit(env, submodule_path, message="initial version / submodule") return submodule_path @@ -17,8 +17,8 @@ def _create_test_package_submodule(env): def _change_test_package_submodule(env, submodule_path): submodule_path.joinpath("testfile").write_text("this is a changed file") submodule_path.joinpath("testfile2").write_text("this is an added file") - env.run('git', 'add', '.', cwd=submodule_path) - _git_commit(env, submodule_path, message='submodule change') + env.run("git", "add", ".", cwd=submodule_path) + _git_commit(env, submodule_path, message="submodule change") def _pull_in_submodule_changes_to_module(env, module_path, rel_path): @@ -27,11 +27,9 @@ def _pull_in_submodule_changes_to_module(env, module_path, rel_path): rel_path: the location of the submodule relative to the superproject. """ submodule_path = module_path / rel_path - env.run('git', 'pull', '-q', 'origin', 'master', cwd=submodule_path) + env.run("git", "pull", "-q", "origin", "master", cwd=submodule_path) # Pass -a to stage the submodule changes that were just pulled in. - _git_commit( - env, module_path, message='submodule change', stage_modified=True - ) + _git_commit(env, module_path, message="submodule change", stage_modified=True) def _create_test_package_with_submodule(env, rel_path): @@ -40,33 +38,37 @@ def _create_test_package_with_submodule(env, rel_path): rel_path: the location of the submodule relative to the superproject. """ env.scratch_path.joinpath("version_pkg").mkdir() - version_pkg_path = env.scratch_path / 'version_pkg' + version_pkg_path = env.scratch_path / "version_pkg" version_pkg_path.joinpath("testpkg").mkdir() - pkg_path = version_pkg_path / 'testpkg' + pkg_path = version_pkg_path / "testpkg" pkg_path.joinpath("__init__.py").write_text("# hello there") _create_main_file(pkg_path, name="version_pkg", output="0.1") - version_pkg_path.joinpath("setup.py").write_text(textwrap.dedent('''\ + version_pkg_path.joinpath("setup.py").write_text( + textwrap.dedent( + """\ from setuptools import setup, find_packages setup(name='version_pkg', version='0.1', packages=find_packages(), ) - ''')) - env.run('git', 'init', cwd=version_pkg_path) - env.run('git', 'add', '.', cwd=version_pkg_path) - _git_commit(env, version_pkg_path, message='initial version') + """ + ) + ) + env.run("git", "init", cwd=version_pkg_path) + env.run("git", "add", ".", cwd=version_pkg_path) + _git_commit(env, version_pkg_path, message="initial version") submodule_path = _create_test_package_submodule(env) env.run( - 'git', - 'submodule', - 'add', + "git", + "submodule", + "add", submodule_path, rel_path, cwd=version_pkg_path, ) - _git_commit(env, version_pkg_path, message='initial version w submodule') + _git_commit(env, version_pkg_path, message="initial version w submodule") return version_pkg_path, submodule_path diff --git a/tests/lib/index.py b/tests/lib/index.py index e6dc2a58bea..dff0ac10341 100644 --- a/tests/lib/index.py +++ b/tests/lib/index.py @@ -3,12 +3,12 @@ def make_mock_candidate(version, yanked_reason=None, hex_digest=None): - url = f'https://example.com/pkg-{version}.tar.gz' + url = f"https://example.com/pkg-{version}.tar.gz" if hex_digest is not None: assert len(hex_digest) == 64 - url += f'#sha256={hex_digest}' + url += f"#sha256={hex_digest}" link = Link(url, yanked_reason=yanked_reason) - candidate = InstallationCandidate('mypackage', version, link) + candidate = InstallationCandidate("mypackage", version, link) return candidate diff --git a/tests/lib/local_repos.py b/tests/lib/local_repos.py index 0aa75787e0c..c57ab16f8d1 100644 --- a/tests/lib/local_repos.py +++ b/tests/lib/local_repos.py @@ -13,15 +13,15 @@ def _create_svn_initools_repo(initools_dir): Create the SVN INITools repo. """ directory = os.path.dirname(initools_dir) - subprocess.check_call('svnadmin create INITools'.split(), cwd=directory) + subprocess.check_call("svnadmin create INITools".split(), cwd=directory) filename, _ = urllib.request.urlretrieve( - 'http://bitbucket.org/hltbra/pip-initools-dump/raw/8b55c908a320/' - 'INITools_modified.dump' + "http://bitbucket.org/hltbra/pip-initools-dump/raw/8b55c908a320/" + "INITools_modified.dump" ) with open(filename) as dump: subprocess.check_call( - ['svnadmin', 'load', initools_dir], + ["svnadmin", "load", initools_dir], stdin=dump, stdout=subprocess.DEVNULL, ) @@ -30,7 +30,7 @@ def _create_svn_initools_repo(initools_dir): def local_checkout( remote_repo, # type: str - temp_path, # type: Path + temp_path, # type: Path ): # type: (...) -> str """ @@ -38,27 +38,27 @@ def local_checkout( temp directory Path object unique to each test function invocation, created as a sub directory of the base temp directory. """ - assert '+' in remote_repo - vcs_name = remote_repo.split('+', 1)[0] + assert "+" in remote_repo + vcs_name = remote_repo.split("+", 1)[0] repository_name = os.path.basename(remote_repo) - directory = temp_path.joinpath('cache') + directory = temp_path.joinpath("cache") repo_url_path = os.path.join(directory, repository_name) assert not os.path.exists(repo_url_path) if not os.path.exists(directory): os.mkdir(directory) - if vcs_name == 'svn': - assert repository_name == 'INITools' + if vcs_name == "svn": + assert repository_name == "INITools" _create_svn_initools_repo(repo_url_path) - repo_url_path = os.path.join(repo_url_path, 'trunk') + repo_url_path = os.path.join(repo_url_path, "trunk") else: vcs_backend = vcs.get_backend(vcs_name) vcs_backend.obtain(repo_url_path, url=hide_url(remote_repo)) - return '{}+{}'.format(vcs_name, path_to_url(repo_url_path)) + return "{}+{}".format(vcs_name, path_to_url(repo_url_path)) def local_repo(remote_repo, temp_path): - return local_checkout(remote_repo, temp_path).split('+', 1)[1] + return local_checkout(remote_repo, temp_path).split("+", 1)[1] diff --git a/tests/lib/options_helpers.py b/tests/lib/options_helpers.py index 8cc5e306d52..8aa105b9655 100644 --- a/tests/lib/options_helpers.py +++ b/tests/lib/options_helpers.py @@ -7,7 +7,6 @@ class FakeCommand(Command): - def main(self, args): index_opts = cmdoptions.make_option_group( cmdoptions.index_group, @@ -18,11 +17,12 @@ def main(self, args): class AddFakeCommandMixin: - def setup(self): - commands_dict['fake'] = CommandInfo( - 'tests.lib.options_helpers', 'FakeCommand', 'fake summary', + commands_dict["fake"] = CommandInfo( + "tests.lib.options_helpers", + "FakeCommand", + "fake summary", ) def teardown(self): - commands_dict.pop('fake') + commands_dict.pop("fake") diff --git a/tests/lib/path.py b/tests/lib/path.py index 81c060b05a3..77d78cce585 100644 --- a/tests/lib/path.py +++ b/tests/lib/path.py @@ -157,7 +157,7 @@ def joinpath(self, *parts): # TODO: Remove after removing inheritance from str. def join(self, *parts): - raise RuntimeError('Path.join is invalid, use joinpath instead.') + raise RuntimeError("Path.join is invalid, use joinpath instead.") def read_bytes(self): # type: () -> bytes @@ -188,4 +188,5 @@ def symlink_to(self, target): def stat(self): return os.stat(self) + curdir = Path(os.path.curdir) diff --git a/tests/lib/requests_mocks.py b/tests/lib/requests_mocks.py index b8ae2d232d2..5db3970cbb2 100644 --- a/tests/lib/requests_mocks.py +++ b/tests/lib/requests_mocks.py @@ -5,7 +5,6 @@ class FakeStream: - def __init__(self, contents): self._io = BytesIO(contents) @@ -20,7 +19,6 @@ def release_conn(self): class MockResponse: - def __init__(self, contents): self.raw = FakeStream(contents) self.content = contents @@ -29,12 +27,11 @@ def __init__(self, contents): self.status_code = 200 self.connection = None self.url = None - self.headers = {'Content-Length': len(contents)} + self.headers = {"Content-Length": len(contents)} self.history = [] class MockConnection: - def _send(self, req, **kwargs): raise NotImplementedError("_send must be overridden for tests") @@ -46,7 +43,6 @@ def send(self, req, **kwargs): class MockRequest: - def __init__(self, url): self.url = url self.headers = {} diff --git a/tests/lib/server.py b/tests/lib/server.py index caaa3ffece6..6db46d166e3 100644 --- a/tests/lib/server.py +++ b/tests/lib/server.py @@ -34,10 +34,10 @@ class MockServer(BaseWSGIServer): # practice. blocked_signals = nullcontext else: + @contextmanager def blocked_signals(): - """Block all signals for e.g. starting a worker thread. - """ + """Block all signals for e.g. starting a worker thread.""" # valid_signals() was added in Python 3.8 (and not using it results # in a warning on pthread_sigmask() call) try: @@ -82,12 +82,13 @@ def _mock_wsgi_adapter(mock): """Uses a mock to record function arguments and provide the actual function that should respond. """ + def adapter(environ, start_response): # type: (Environ, StartResponse) -> Body try: responder = mock(environ, start_response) except StopIteration: - raise RuntimeError('Ran out of mocked responses.') + raise RuntimeError("Ran out of mocked responses.") return responder(environ, start_response) return adapter @@ -136,8 +137,7 @@ def make_mock_server(**kwargs): @contextmanager def server_running(server): # type: (BaseWSGIServer) -> None - """Context manager for running the provided server in a separate thread. - """ + """Context manager for running the provided server in a separate thread.""" thread = threading.Thread(target=server.serve_forever) thread.daemon = True with blocked_signals(): @@ -156,45 +156,50 @@ def text_html_response(text): # type: (str) -> Responder def responder(environ, start_response): # type: (Environ, StartResponse) -> Body - start_response("200 OK", [ - ("Content-Type", "text/html; charset=UTF-8"), - ]) - return [text.encode('utf-8')] + start_response( + "200 OK", + [ + ("Content-Type", "text/html; charset=UTF-8"), + ], + ) + return [text.encode("utf-8")] return responder def html5_page(text): # type: (str) -> str - return dedent(""" + return ( + dedent( + """ <!DOCTYPE html> <html> <body> {} </body> </html> - """).strip().format(text) + """ + ) + .strip() + .format(text) + ) def index_page(spec): # type: (Dict[str, str]) -> Responder def link(name, value): - return '<a href="{}">{}</a>'.format( - value, name - ) + return '<a href="{}">{}</a>'.format(value, name) - links = ''.join(link(*kv) for kv in spec.items()) + links = "".join(link(*kv) for kv in spec.items()) return text_html_response(html5_page(links)) def package_page(spec): # type: (Dict[str, str]) -> Responder def link(name, value): - return '<a href="{}">{}</a>'.format( - value, name - ) + return '<a href="{}">{}</a>'.format(value, name) - links = ''.join(link(*kv) for kv in spec.items()) + links = "".join(link(*kv) for kv in spec.items()) return text_html_response(html5_page(links)) @@ -204,13 +209,14 @@ def responder(environ, start_response): # type: (Environ, StartResponse) -> Body size = os.stat(path).st_size start_response( - "200 OK", [ + "200 OK", + [ ("Content-Type", "application/octet-stream"), ("Content-Length", str(size)), ], ) - with open(path, 'rb') as f: + with open(path, "rb") as f: return [f.read()] return responder @@ -223,22 +229,24 @@ def authorization_response(path): def responder(environ, start_response): # type: (Environ, StartResponse) -> Body - if environ.get('HTTP_AUTHORIZATION') == correct_auth: + if environ.get("HTTP_AUTHORIZATION") == correct_auth: size = os.stat(path).st_size start_response( - "200 OK", [ + "200 OK", + [ ("Content-Type", "application/octet-stream"), ("Content-Length", str(size)), ], ) else: start_response( - "401 Unauthorized", [ + "401 Unauthorized", + [ ("WWW-Authenticate", "Basic"), ], ) - with open(path, 'rb') as f: + with open(path, "rb") as f: return [f.read()] return responder diff --git a/tests/lib/test_lib.py b/tests/lib/test_lib.py index 88c10501b70..54e8764f0b5 100644 --- a/tests/lib/test_lib.py +++ b/tests/lib/test_lib.py @@ -18,9 +18,7 @@ def assert_error_startswith(exc_type, expected_start): with pytest.raises(exc_type) as err: yield - assert str(err.value).startswith(expected_start), ( - f'full message: {err.value}' - ) + assert str(err.value).startswith(expected_start), f"full message: {err.value}" def test_tmp_dir_exists_in_env(script): @@ -31,7 +29,7 @@ def test_tmp_dir_exists_in_env(script): # need these tests to ensure the assert_no_temp feature of scripttest is # working script.assert_no_temp() # this fails if env.tmp_path doesn't exist - assert script.environ['TMPDIR'] == script.temp_path + assert script.environ["TMPDIR"] == script.temp_path assert isdir(script.temp_path) @@ -41,16 +39,16 @@ def test_correct_pip_version(script): """ # output is like: # pip PIPVERSION from PIPDIRECTORY (python PYVERSION) - result = script.pip('--version') + result = script.pip("--version") # compare the directory tree of the invoked pip with that of this source # distribution pip_folder_outputed = re.match( - r'pip \d+(\.[\d]+)+(\.?(b|rc|dev|pre|post)\d+)? from (.*) ' - r'\(python \d(.[\d])+\)$', - result.stdout + r"pip \d+(\.[\d]+)+(\.?(b|rc|dev|pre|post)\d+)? from (.*) " + r"\(python \d(.[\d])+\)$", + result.stdout, ).group(4) - pip_folder = join(SRC_DIR, 'src', 'pip') + pip_folder = join(SRC_DIR, "src", "pip") diffs = filecmp.dircmp(pip_folder, pip_folder_outputed) @@ -59,32 +57,33 @@ def test_correct_pip_version(script): # primary resources other than .py files, this code will need # maintenance mismatch_py = [ - x for x in diffs.left_only + diffs.right_only + diffs.diff_files - if x.endswith('.py') + x + for x in diffs.left_only + diffs.right_only + diffs.diff_files + if x.endswith(".py") ] assert not mismatch_py, ( - f'mismatched source files in {pip_folder!r} ' - f'and {pip_folder_outputed!r}: {mismatch_py!r}' + f"mismatched source files in {pip_folder!r} " + f"and {pip_folder_outputed!r}: {mismatch_py!r}" ) def test_as_import(script): - """ test that pip.__init__.py does not shadow + """test that pip.__init__.py does not shadow the command submodule with a dictionary """ import pip._internal.commands.install as inst + assert inst is not None class TestPipTestEnvironment: - def run_stderr_with_prefix(self, script, prefix, **kwargs): """ Call run() that prints stderr with the given prefix. """ - text = f'{prefix}: hello, world\\n' + text = f"{prefix}: hello, world\\n" command = f'import sys; sys.stderr.write("{text}")' - args = [sys.executable, '-c', command] + args = [sys.executable, "-c", command] script.run(*args, **kwargs) def run_with_log_command(self, script, sub_string, **kwargs): @@ -96,14 +95,17 @@ def run_with_log_command(self, script, sub_string, **kwargs): "import logging; logging.basicConfig(level='INFO'); " "logging.getLogger().info('sub: {}', 'foo')" ).format(sub_string) - args = [sys.executable, '-c', command] + args = [sys.executable, "-c", command] script.run(*args, **kwargs) - @pytest.mark.parametrize('prefix', ( - 'DEBUG', - 'INFO', - 'FOO', - )) + @pytest.mark.parametrize( + "prefix", + ( + "DEBUG", + "INFO", + "FOO", + ), + ) def test_run__allowed_stderr(self, script, prefix): """ Test calling run() with allowed stderr. @@ -117,21 +119,28 @@ def test_run__allow_stderr_warning(self, script): """ # Check that no error happens. self.run_stderr_with_prefix( - script, 'WARNING', allow_stderr_warning=True, + script, + "WARNING", + allow_stderr_warning=True, ) # Check that an error still happens with ERROR. - expected_start = 'stderr has an unexpected error' + expected_start = "stderr has an unexpected error" with assert_error_startswith(RuntimeError, expected_start): self.run_stderr_with_prefix( - script, 'ERROR', allow_stderr_warning=True, + script, + "ERROR", + allow_stderr_warning=True, ) - @pytest.mark.parametrize('prefix', ( - 'DEPRECATION', - 'WARNING', - 'ERROR', - )) + @pytest.mark.parametrize( + "prefix", + ( + "DEPRECATION", + "WARNING", + "ERROR", + ), + ) def test_run__allow_stderr_error(self, script, prefix): """ Test passing allow_stderr_error=True. @@ -139,11 +148,14 @@ def test_run__allow_stderr_error(self, script, prefix): # Check that no error happens. self.run_stderr_with_prefix(script, prefix, allow_stderr_error=True) - @pytest.mark.parametrize('prefix, expected_start', ( - ('DEPRECATION', 'stderr has an unexpected warning'), - ('WARNING', 'stderr has an unexpected warning'), - ('ERROR', 'stderr has an unexpected error'), - )) + @pytest.mark.parametrize( + "prefix, expected_start", + ( + ("DEPRECATION", "stderr has an unexpected warning"), + ("WARNING", "stderr has an unexpected warning"), + ("ERROR", "stderr has an unexpected error"), + ), + ) def test_run__unexpected_stderr(self, script, prefix, expected_start): """ Test calling run() with unexpected stderr output. @@ -156,70 +168,72 @@ def test_run__logging_error(self, script): Test calling run() with an unexpected logging error. """ # Pass a good substitution string. - self.run_with_log_command(script, sub_string='%r') + self.run_with_log_command(script, sub_string="%r") - expected_start = 'stderr has a logging error, which is never allowed' + expected_start = "stderr has a logging error, which is never allowed" with assert_error_startswith(RuntimeError, expected_start): # Pass a bad substitution string. Also, pass # allow_stderr_error=True to check that the RuntimeError occurs # even under the stricter test condition of when we are allowing # other types of errors. self.run_with_log_command( - script, sub_string='{!r}', allow_stderr_error=True, + script, + sub_string="{!r}", + allow_stderr_error=True, ) def test_run__allow_stderr_error_false_error_with_expect_error( - self, script, + self, + script, ): """ Test passing allow_stderr_error=False with expect_error=True. """ - expected_start = ( - 'cannot pass allow_stderr_error=False with expect_error=True' - ) + expected_start = "cannot pass allow_stderr_error=False with expect_error=True" with assert_error_startswith(RuntimeError, expected_start): - script.run('python', allow_stderr_error=False, expect_error=True) + script.run("python", allow_stderr_error=False, expect_error=True) def test_run__allow_stderr_warning_false_error_with_expect_stderr( - self, script, + self, + script, ): """ Test passing allow_stderr_warning=False with expect_stderr=True. """ expected_start = ( - 'cannot pass allow_stderr_warning=False with expect_stderr=True' + "cannot pass allow_stderr_warning=False with expect_stderr=True" ) with assert_error_startswith(RuntimeError, expected_start): script.run( - 'python', allow_stderr_warning=False, expect_stderr=True, + "python", + allow_stderr_warning=False, + expect_stderr=True, ) - @pytest.mark.parametrize('arg_name', ( - 'expect_error', - 'allow_stderr_error', - )) + @pytest.mark.parametrize( + "arg_name", + ( + "expect_error", + "allow_stderr_error", + ), + ) def test_run__allow_stderr_warning_false_error(self, script, arg_name): """ Test passing allow_stderr_warning=False when it is not allowed. """ - kwargs = {'allow_stderr_warning': False, arg_name: True} + kwargs = {"allow_stderr_warning": False, arg_name: True} expected_start = ( - 'cannot pass allow_stderr_warning=False with ' - 'allow_stderr_error=True' + "cannot pass allow_stderr_warning=False with " "allow_stderr_error=True" ) with assert_error_startswith(RuntimeError, expected_start): - script.run('python', **kwargs) + script.run("python", **kwargs) def test_run__expect_error_fails_when_zero_returncode(self, script): - expected_start = 'Script passed unexpectedly' + expected_start = "Script passed unexpectedly" with assert_error_startswith(AssertionError, expected_start): - script.run( - 'python', expect_error=True - ) + script.run("python", expect_error=True) def test_run__no_expect_error_fails_when_nonzero_returncode(self, script): - expected_start = 'Script returned code: 1' + expected_start = "Script returned code: 1" with assert_error_startswith(AssertionError, expected_start): - script.run( - 'python', '-c', 'import sys; sys.exit(1)' - ) + script.run("python", "-c", "import sys; sys.exit(1)") diff --git a/tests/lib/test_wheel.py b/tests/lib/test_wheel.py index 835ad31ec39..579d48660dd 100644 --- a/tests/lib/test_wheel.py +++ b/tests/lib/test_wheel.py @@ -161,19 +161,20 @@ def test_make_wheel_default_record(): record_bytes = z.read("simple-0.1.0.dist-info/RECORD") record_text = record_bytes.decode() record_rows = list(csv.reader(record_text.splitlines())) - records = { - row[0]: row[1:] for row in record_rows - } + records = {row[0]: row[1:] for row in record_rows} expected = { "simple/__init__.py": [ - "sha256=ypeBEsobvcr6wjGzmiPcTaeG7_gUfE5yuYB3ha_uSLs", "1" + "sha256=ypeBEsobvcr6wjGzmiPcTaeG7_gUfE5yuYB3ha_uSLs", + "1", ], "simple-0.1.0.data/purelib/info.txt": [ - "sha256=Ln0sA6lQeuJl7PW1NWiFpTOTogKdJBOUmXJloaJa78Y", "1" + "sha256=Ln0sA6lQeuJl7PW1NWiFpTOTogKdJBOUmXJloaJa78Y", + "1", ], "simple-0.1.0.dist-info/LICENSE": [ - "sha256=PiPoFgA5WUoziU9lZOGxNIu9egCI1CxKy3PurtWcAJ0", "1" + "sha256=PiPoFgA5WUoziU9lZOGxNIu9egCI1CxKy3PurtWcAJ0", + "1", ], "simple-0.1.0.dist-info/RECORD": ["", ""], } diff --git a/tests/lib/venv.py b/tests/lib/venv.py index bd6426a81b8..bbf6eaa38be 100644 --- a/tests/lib/venv.py +++ b/tests/lib/venv.py @@ -17,9 +17,9 @@ class VirtualEnvironment: def __init__(self, location, template=None, venv_type=None): assert template is None or venv_type is None - assert venv_type in (None, 'virtualenv', 'venv') + assert venv_type in (None, "virtualenv", "venv") self.location = Path(location) - self._venv_type = venv_type or template._venv_type or 'virtualenv' + self._venv_type = venv_type or template._venv_type or "virtualenv" self._user_site_packages = False self._template = template self._sitecustomize = None @@ -29,11 +29,11 @@ def __init__(self, location, template=None, venv_type=None): def _update_paths(self): home, lib, inc, bin = _virtualenv.path_locations(self.location) self.bin = Path(bin) - self.site = Path(lib) / 'site-packages' + self.site = Path(lib) / "site-packages" # Workaround for https://github.com/pypa/virtualenv/issues/306 if hasattr(sys, "pypy_version_info"): version_dir = str(sys.version_info.major) - self.lib = Path(home, 'lib-python', version_dir) + self.lib = Path(home, "lib-python", version_dir) else: self.lib = Path(lib) @@ -46,17 +46,15 @@ def _create(self, clear=False): if self._template: # On Windows, calling `_virtualenv.path_locations(target)` # will have created the `target` directory... - if sys.platform == 'win32' and self.location.exists(): + if sys.platform == "win32" and self.location.exists(): self.location.rmdir() # Clone virtual environment from template. - shutil.copytree( - self._template.location, self.location, symlinks=True - ) + shutil.copytree(self._template.location, self.location, symlinks=True) self._sitecustomize = self._template.sitecustomize self._user_site_packages = self._template.user_site_packages else: # Create a new virtual environment. - if self._venv_type == 'virtualenv': + if self._venv_type == "virtualenv": _virtualenv.create_environment( self.location, no_pip=True, @@ -64,7 +62,7 @@ def _create(self, clear=False): no_setuptools=True, ) self._fix_virtualenv_site_module() - elif self._venv_type == 'venv': + elif self._venv_type == "venv": builder = _venv.EnvBuilder() context = builder.ensure_directories(self.location) builder.create_configuration(context) @@ -75,46 +73,44 @@ def _create(self, clear=False): def _fix_virtualenv_site_module(self): # Patch `site.py` so user site work as expected. - site_py = self.lib / 'site.py' + site_py = self.lib / "site.py" with open(site_py) as fp: site_contents = fp.read() for pattern, replace in ( ( # Ensure enabling user site does not result in adding # the real site-packages' directory to `sys.path`. + ("\ndef virtual_addsitepackages(known_paths):\n"), ( - '\ndef virtual_addsitepackages(known_paths):\n' - ), - ( - '\ndef virtual_addsitepackages(known_paths):\n' - ' return known_paths\n' + "\ndef virtual_addsitepackages(known_paths):\n" + " return known_paths\n" ), ), ( # Fix sites ordering: user site must be added before system. ( - '\n paths_in_sys = addsitepackages(paths_in_sys)' - '\n paths_in_sys = addusersitepackages(paths_in_sys)\n' + "\n paths_in_sys = addsitepackages(paths_in_sys)" + "\n paths_in_sys = addusersitepackages(paths_in_sys)\n" ), ( - '\n paths_in_sys = addusersitepackages(paths_in_sys)' - '\n paths_in_sys = addsitepackages(paths_in_sys)\n' + "\n paths_in_sys = addusersitepackages(paths_in_sys)" + "\n paths_in_sys = addsitepackages(paths_in_sys)\n" ), ), ): assert pattern in site_contents site_contents = site_contents.replace(pattern, replace) - with open(site_py, 'w') as fp: + with open(site_py, "w") as fp: fp.write(site_contents) # Make sure bytecode is up-to-date too. assert compileall.compile_file(str(site_py), quiet=1, force=True) def _customize_site(self): - contents = '' - if self._venv_type == 'venv': + contents = "" + if self._venv_type == "venv": # Enable user site (before system). contents += textwrap.dedent( - ''' + """ import os, site, sys if not os.environ.get('PYTHONNOUSERSITE', False): @@ -138,9 +134,10 @@ def _customize_site(self): # Third, add back system-sites related paths. for path in site.getsitepackages(): site.addsitedir(path) - ''').strip() + """ + ).strip() if self._sitecustomize is not None: - contents += '\n' + self._sitecustomize + contents += "\n" + self._sitecustomize sitecustomize = self.site / "sitecustomize.py" sitecustomize.write_text(contents) # Make sure bytecode is up-to-date too. @@ -170,11 +167,11 @@ def user_site_packages(self): @user_site_packages.setter def user_site_packages(self, value): self._user_site_packages = value - if self._venv_type == 'virtualenv': + if self._venv_type == "virtualenv": marker = self.lib / "no-global-site-packages.txt" if self._user_site_packages: marker.unlink() else: marker.touch() - elif self._venv_type == 'venv': + elif self._venv_type == "venv": self._customize_site() diff --git a/tests/lib/wheel.py b/tests/lib/wheel.py index e88ce8c6101..bfcdc9d272f 100644 --- a/tests/lib/wheel.py +++ b/tests/lib/wheel.py @@ -30,9 +30,7 @@ # path, digest, size RecordLike = Tuple[str, str, str] -RecordCallback = Callable[ - [List["Record"]], Union[str, bytes, List[RecordLike]] -] +RecordCallback = Callable[[List["Record"]], Union[str, bytes, List[RecordLike]]] # As would be used in metadata HeaderValue = Union[str, List[str]] @@ -97,11 +95,13 @@ def make_metadata_file( if value is not _default: return File(path, ensure_binary(value)) - metadata = CaseInsensitiveDict({ - "Metadata-Version": "2.1", - "Name": name, - "Version": version, - }) + metadata = CaseInsensitiveDict( + { + "Metadata-Version": "2.1", + "Name": name, + "Version": version, + } + ) if updates is not _default: metadata.update(updates) @@ -128,12 +128,14 @@ def make_wheel_metadata_file( if value is not _default: return File(path, ensure_binary(value)) - metadata = CaseInsensitiveDict({ - "Wheel-Version": "1.0", - "Generator": "pip-test-suite", - "Root-Is-Purelib": "true", - "Tag": ["-".join(parts) for parts in tags], - }) + metadata = CaseInsensitiveDict( + { + "Wheel-Version": "1.0", + "Generator": "pip-test-suite", + "Root-Is-Purelib": "true", + "Tag": ["-".join(parts) for parts in tags], + } + ) if updates is not _default: metadata.update(updates) @@ -172,10 +174,7 @@ def make_entry_points_file( def make_files(files): # type: (Dict[str, AnyStr]) -> List[File] - return [ - File(name, ensure_binary(contents)) - for name, contents in files.items() - ] + return [File(name, ensure_binary(contents)) for name, contents in files.items()] def make_metadata_files(name, version, files): @@ -203,9 +202,7 @@ def urlsafe_b64encode_nopad(data): def digest(contents): # type: (bytes) -> str - return "sha256={}".format( - urlsafe_b64encode_nopad(sha256(contents).digest()) - ) + return "sha256={}".format(urlsafe_b64encode_nopad(sha256(contents).digest())) def record_file_maker_wrapper( @@ -219,9 +216,7 @@ def record_file_maker_wrapper( records = [] # type: List[Record] for file in files: records.append( - Record( - file.name, digest(file.contents), str(len(file.contents)) - ) + Record(file.name, digest(file.contents), str(len(file.contents))) ) yield file @@ -250,19 +245,20 @@ def record_file_maker_wrapper( def wheel_name(name, version, pythons, abis, platforms): # type: (str, str, str, str, str) -> str - stem = "-".join([ - name, - version, - ".".join(pythons), - ".".join(abis), - ".".join(platforms), - ]) + stem = "-".join( + [ + name, + version, + ".".join(pythons), + ".".join(abis), + ".".join(platforms), + ] + ) return f"{stem}.whl" class WheelBuilder: - """A wheel that can be saved or converted to several formats. - """ + """A wheel that can be saved or converted to several formats.""" def __init__(self, name, files): # type: (str, List[File]) -> None @@ -390,9 +386,7 @@ def make_wheel( tags = list(itertools.product(pythons, abis, platforms)) possible_files = [ - make_metadata_file( - name, version, metadata, metadata_updates, metadata_body - ), + make_metadata_file(name, version, metadata, metadata_updates, metadata_body), make_wheel_metadata_file( name, version, wheel_metadata, tags, wheel_metadata_updates ), @@ -403,9 +397,7 @@ def make_wheel( possible_files.extend(make_files(extra_files)) if extra_metadata_files is not _default: - possible_files.extend( - make_metadata_files(name, version, extra_metadata_files) - ) + possible_files.extend(make_metadata_files(name, version, extra_metadata_files)) if extra_data_files is not _default: possible_files.extend(make_data_files(name, version, extra_data_files)) From 37ad296684a8cdb7b883da5b2ca3db1b9504b564 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Fri, 2 Apr 2021 11:03:39 +0100 Subject: [PATCH 3094/3170] Tweak textwrap.dedent strings --- tests/lib/__init__.py | 111 ++++++++++++++++++++++-------------------- 1 file changed, 58 insertions(+), 53 deletions(-) diff --git a/tests/lib/__init__.py b/tests/lib/__init__.py index 9f99bcef020..cbb7b7b6d86 100644 --- a/tests/lib/__init__.py +++ b/tests/lib/__init__.py @@ -306,13 +306,14 @@ def assert_installed( expected_ending = pkg_dir + "\n." raise TestFailure( textwrap.dedent( - f"""\ - Incorrect egg_link file {egg_link_file!r} - Expected ending: {expected_ending!r} - ------- Actual contents ------- - {egg_link_contents!r} - -------------------------------""" - ) + f""" + Incorrect egg_link file {egg_link_file!r} + Expected ending: {expected_ending!r} + ------- Actual contents ------- + {egg_link_contents!r} + ------------------------------- + """ + ).strip() ) if use_user_site: @@ -329,11 +330,11 @@ def assert_installed( files = sorted(self.files_created) raise TestFailure( textwrap.dedent( - f"""\ - expected package directory {pkg_dir!r} {maybe}to be created - actually created: - {files} - """ + f""" + expected package directory {pkg_dir!r} {maybe}to be created + actually created: + {files} + """ ) ) @@ -758,12 +759,10 @@ def _create_main_file(dir_path, name=None, output=None): if output is None: output = "0.1" text = textwrap.dedent( - """\ - def main(): - print({!r}) - """.format( - output - ) + f""" + def main(): + print({output!r}) + """ ) filename = f"{name}.py" dir_path.joinpath(filename).write_text(text) @@ -864,13 +863,16 @@ def _create_test_package_with_subdirectory(script, subdirectory): version_pkg_path.joinpath("setup.py").write_text( textwrap.dedent( """ - from setuptools import setup, find_packages - setup(name='version_pkg', - version='0.1', - packages=find_packages(), - py_modules=['version_pkg'], - entry_points=dict(console_scripts=['version_pkg=version_pkg:main'])) - """ + from setuptools import setup, find_packages + + setup( + name="version_pkg", + version="0.1", + packages=find_packages(), + py_modules=["version_pkg"], + entry_points=dict(console_scripts=["version_pkg=version_pkg:main"]), + ) + """ ) ) @@ -881,13 +883,16 @@ def _create_test_package_with_subdirectory(script, subdirectory): subdirectory_path.joinpath("setup.py").write_text( textwrap.dedent( """ -from setuptools import setup, find_packages -setup(name='version_subpkg', - version='0.1', - packages=find_packages(), - py_modules=['version_subpkg'], - entry_points=dict(console_scripts=['version_pkg=version_subpkg:main'])) - """ + from setuptools import find_packages, setup + + setup( + name="version_subpkg", + version="0.1", + packages=find_packages(), + py_modules=["version_subpkg"], + entry_points=dict(console_scripts=["version_pkg=version_subpkg:main"]), + ) + """ ) ) @@ -911,14 +916,14 @@ def _create_test_package_with_srcdir(script, name="version_pkg", vcs="git"): subdir_path.joinpath("setup.py").write_text( textwrap.dedent( """ - from setuptools import setup, find_packages - setup( - name='{name}', - version='0.1', - packages=find_packages(), - package_dir={{'': 'src'}}, - ) - """.format( + from setuptools import setup, find_packages + setup( + name="{name}", + version="0.1", + packages=find_packages(), + package_dir={{"": "src"}}, + ) + """.format( name=name ) ) @@ -933,15 +938,15 @@ def _create_test_package(script, name="version_pkg", vcs="git"): version_pkg_path.joinpath("setup.py").write_text( textwrap.dedent( """ - from setuptools import setup, find_packages - setup( - name='{name}', - version='0.1', - packages=find_packages(), - py_modules=['{name}'], - entry_points=dict(console_scripts=['{name}={name}:main']) - ) - """.format( + from setuptools import setup, find_packages + setup( + name="{name}", + version="0.1", + packages=find_packages(), + py_modules=["{name}"], + entry_points=dict(console_scripts=["{name}={name}:main"]), + ) + """.format( name=name ) ) @@ -1007,10 +1012,10 @@ def create_test_package_with_setup(script, **setup_kwargs): pkg_path.joinpath("setup.py").write_text( textwrap.dedent( f""" - from setuptools import setup - kwargs = {setup_kwargs!r} - setup(**kwargs) - """ + from setuptools import setup + kwargs = {setup_kwargs!r} + setup(**kwargs) + """ ) ) return pkg_path From 015b2c19da61cbde46e53cb3020895f3c9843fa7 Mon Sep 17 00:00:00 2001 From: Andrey Bienkowski <hexagonrecursion@gmail.com> Date: Tue, 9 Feb 2021 19:04:45 +0300 Subject: [PATCH 3095/3170] Add test: build tag is less important than other tags --- tests/unit/test_finder.py | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/tests/unit/test_finder.py b/tests/unit/test_finder.py index 162a9b356f3..89cc655bd9f 100644 --- a/tests/unit/test_finder.py +++ b/tests/unit/test_finder.py @@ -209,6 +209,8 @@ def test_existing_over_wheel_priority(self, data): with pytest.raises(BestVersionAlreadyInstalled): finder.find_requirement(req, True) + +class TestCandidateEvaluator: def test_link_sorting(self): """ Test link sorting @@ -276,6 +278,37 @@ def test_link_sorting_wheels_with_build_tags(self): results2 = sorted(reversed(links), key=sort_key, reverse=True) assert links == results == results2, results2 + def test_build_tag_is_less_important_than_other_tags(self): + links = [ + InstallationCandidate( + "simple", + "1.0", + Link('simple-1.0-1-py3-abi3-linux_x86_64.whl'), + ), + InstallationCandidate( + "simple", + '1.0', + Link('simple-1.0-2-py3-abi3-linux_i386.whl'), + ), + InstallationCandidate( + "simple", + '1.0', + Link('simple-1.0.tar.gz'), + ), + ] + valid_tags = [ + Tag('py3', 'abi3', 'linux_x86_64'), + Tag('py3', 'abi3', 'linux_i386'), + ] + evaluator = CandidateEvaluator( + 'my-project', supported_tags=valid_tags, specifier = SpecifierSet(), + ) + sort_key = evaluator._sort_key + results = sorted(links, key=sort_key, reverse=True) + results2 = sorted(reversed(links), key=sort_key, reverse=True) + + assert links == results == results2, results2 + def test_finder_priority_file_over_page(data): """Test PackageFinder prefers file links over equivalent page links""" From 426279c39a2e0790a943b9f9753c7371f334be9b Mon Sep 17 00:00:00 2001 From: Andrey Bienkowski <hexagonrecursion@gmail.com> Date: Tue, 9 Feb 2021 19:09:10 +0300 Subject: [PATCH 3096/3170] Sort order: compatibility tags are more important than build tag --- src/pip/_internal/index/package_finder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/index/package_finder.py b/src/pip/_internal/index/package_finder.py index b826690fa5f..c50885430ef 100644 --- a/src/pip/_internal/index/package_finder.py +++ b/src/pip/_internal/index/package_finder.py @@ -530,7 +530,7 @@ def _sort_key(self, candidate): yank_value = -1 * int(link.is_yanked) # -1 for yanked. return ( has_allowed_hash, yank_value, binary_preference, candidate.version, - build_tag, pri, + pri, build_tag, ) def sort_best_candidate( From 0faf750a5192f700901ec45e0a67651a8f39c0d8 Mon Sep 17 00:00:00 2001 From: Andrey Bienkowski <hexagonrecursion@gmail.com> Date: Tue, 9 Feb 2021 19:33:55 +0300 Subject: [PATCH 3097/3170] Add a news entry --- news/9565.bugfix.rst | 9 +++++++++ 1 file changed, 9 insertions(+) create mode 100644 news/9565.bugfix.rst diff --git a/news/9565.bugfix.rst b/news/9565.bugfix.rst new file mode 100644 index 00000000000..4acdcd06a56 --- /dev/null +++ b/news/9565.bugfix.rst @@ -0,0 +1,9 @@ +Make wheel compatibility tag preferences more important than the build tag + +For example: if both linux_x86_64 and linux_i386 are supported and linux_x86_64 +is preferred over linux_i386 the old behavior was to prefer +simple-1.0-2-py3-abi3-linux_i386.whl over +simple-1.0-1-py3-abi3-linux_x86_64.whl +but now pip prefers +simple-1.0-1-py3-abi3-linux_x86_64.whl over +simple-1.0-2-py3-abi3-linux_i386.whl From d8c8c00d7c285be03c734b9c45de300dde2445e5 Mon Sep 17 00:00:00 2001 From: Andrey Bienkowski <hexagonrecursion@gmail.com> Date: Tue, 9 Feb 2021 20:16:41 +0300 Subject: [PATCH 3098/3170] Shorten the news entry --- news/9565.bugfix.rst | 8 -------- 1 file changed, 8 deletions(-) diff --git a/news/9565.bugfix.rst b/news/9565.bugfix.rst index 4acdcd06a56..b8f95fd3592 100644 --- a/news/9565.bugfix.rst +++ b/news/9565.bugfix.rst @@ -1,9 +1 @@ Make wheel compatibility tag preferences more important than the build tag - -For example: if both linux_x86_64 and linux_i386 are supported and linux_x86_64 -is preferred over linux_i386 the old behavior was to prefer -simple-1.0-2-py3-abi3-linux_i386.whl over -simple-1.0-1-py3-abi3-linux_x86_64.whl -but now pip prefers -simple-1.0-1-py3-abi3-linux_x86_64.whl over -simple-1.0-2-py3-abi3-linux_i386.whl From 5297d6927ffc0d06dd200a5c7268b8170d20b8ae Mon Sep 17 00:00:00 2001 From: Andrey Bienkowski <hexagonrecursion@gmail.com> Date: Tue, 9 Feb 2021 21:11:50 +0300 Subject: [PATCH 3099/3170] Add one more test --- tests/unit/test_finder.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/tests/unit/test_finder.py b/tests/unit/test_finder.py index 89cc655bd9f..bde91c897b9 100644 --- a/tests/unit/test_finder.py +++ b/tests/unit/test_finder.py @@ -290,6 +290,11 @@ def test_build_tag_is_less_important_than_other_tags(self): '1.0', Link('simple-1.0-2-py3-abi3-linux_i386.whl'), ), + InstallationCandidate( + "simple", + '1.0', + Link('simple-1.0-2-py3-any-none.whl'), + ), InstallationCandidate( "simple", '1.0', @@ -299,6 +304,7 @@ def test_build_tag_is_less_important_than_other_tags(self): valid_tags = [ Tag('py3', 'abi3', 'linux_x86_64'), Tag('py3', 'abi3', 'linux_i386'), + Tag('py3', 'any', 'none'), ] evaluator = CandidateEvaluator( 'my-project', supported_tags=valid_tags, specifier = SpecifierSet(), From 6db6964a985c88140b8c8dfd5161c5561cbf19d9 Mon Sep 17 00:00:00 2001 From: Andrey Bienkowski <hexagonrecursion@gmail.com> Date: Fri, 2 Apr 2021 14:58:17 +0300 Subject: [PATCH 3100/3170] Rewrite the assertions to be less confusing --- tests/unit/test_finder.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/tests/unit/test_finder.py b/tests/unit/test_finder.py index bde91c897b9..09b4ee13388 100644 --- a/tests/unit/test_finder.py +++ b/tests/unit/test_finder.py @@ -251,7 +251,8 @@ def test_link_sorting(self): results = sorted(links, key=sort_key, reverse=True) results2 = sorted(reversed(links), key=sort_key, reverse=True) - assert links == results == results2, results2 + assert links == results, results + assert links == results2, results2 def test_link_sorting_wheels_with_build_tags(self): """Verify build tags affect sorting.""" @@ -276,7 +277,9 @@ def test_link_sorting_wheels_with_build_tags(self): sort_key = candidate_evaluator._sort_key results = sorted(links, key=sort_key, reverse=True) results2 = sorted(reversed(links), key=sort_key, reverse=True) - assert links == results == results2, results2 + + assert links == results, results + assert links == results2, results2 def test_build_tag_is_less_important_than_other_tags(self): links = [ @@ -313,7 +316,8 @@ def test_build_tag_is_less_important_than_other_tags(self): results = sorted(links, key=sort_key, reverse=True) results2 = sorted(reversed(links), key=sort_key, reverse=True) - assert links == results == results2, results2 + assert links == results, results + assert links == results2, results2 def test_finder_priority_file_over_page(data): From e96791f106e8b721cec3dbbbab1423a173889682 Mon Sep 17 00:00:00 2001 From: Andrey Bienkowski <hexagonrecursion@gmail.com> Date: Fri, 2 Apr 2021 15:17:59 +0300 Subject: [PATCH 3101/3170] Lint --- src/pip/_internal/index/package_finder.py | 4 ++-- tests/unit/test_finder.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/index/package_finder.py b/src/pip/_internal/index/package_finder.py index c50885430ef..4cc9ffd4dfa 100644 --- a/src/pip/_internal/index/package_finder.py +++ b/src/pip/_internal/index/package_finder.py @@ -44,7 +44,7 @@ BuildTag = Union[Tuple[()], Tuple[int, str]] CandidateSortingKey = ( - Tuple[int, int, int, _BaseVersion, BuildTag, Optional[int]] + Tuple[int, int, int, _BaseVersion, Optional[int], BuildTag] ) @@ -530,7 +530,7 @@ def _sort_key(self, candidate): yank_value = -1 * int(link.is_yanked) # -1 for yanked. return ( has_allowed_hash, yank_value, binary_preference, candidate.version, - pri, build_tag, + pri, build_tag, ) def sort_best_candidate( diff --git a/tests/unit/test_finder.py b/tests/unit/test_finder.py index 09b4ee13388..9638199fbf1 100644 --- a/tests/unit/test_finder.py +++ b/tests/unit/test_finder.py @@ -310,7 +310,7 @@ def test_build_tag_is_less_important_than_other_tags(self): Tag('py3', 'any', 'none'), ] evaluator = CandidateEvaluator( - 'my-project', supported_tags=valid_tags, specifier = SpecifierSet(), + 'my-project', supported_tags=valid_tags, specifier=SpecifierSet(), ) sort_key = evaluator._sort_key results = sorted(links, key=sort_key, reverse=True) From 4ec34b9013ccbb322120a1561520cf4fab31c7f7 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 5 Mar 2021 17:29:21 +0800 Subject: [PATCH 3102/3170] Run pip in isolated env by building zip --- news/8214.bugfix.rst | 2 + src/pip/_internal/build_env.py | 100 ++++++++++++++++++++++----------- 2 files changed, 69 insertions(+), 33 deletions(-) create mode 100644 news/8214.bugfix.rst diff --git a/news/8214.bugfix.rst b/news/8214.bugfix.rst new file mode 100644 index 00000000000..22224f380e5 --- /dev/null +++ b/news/8214.bugfix.rst @@ -0,0 +1,2 @@ +Prevent packages already-installed alongside with pip to be injected into an +isolated build environment during build-time dependency population. diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py index fa22d6377c6..fc8a0f1e2c2 100644 --- a/src/pip/_internal/build_env.py +++ b/src/pip/_internal/build_env.py @@ -1,14 +1,17 @@ """Build Environment used for isolation during sdist building """ +import contextlib import logging import os +import pathlib import sys import textwrap +import zipfile from collections import OrderedDict from sysconfig import get_paths from types import TracebackType -from typing import TYPE_CHECKING, Iterable, List, Optional, Set, Tuple, Type +from typing import TYPE_CHECKING, Iterable, Iterator, List, Optional, Set, Tuple, Type from pip._vendor.pkg_resources import Requirement, VersionConflict, WorkingSet @@ -37,6 +40,61 @@ def __init__(self, path): self.lib_dirs = get_prefixed_libs(path) +@contextlib.contextmanager +def _create_standalone_pip() -> Iterator[str]: + """Create a zip file containing specified pip installation.""" + source = pathlib.Path(pip_location).resolve().parent + with TempDirectory() as tmp_dir: + pip_zip = os.path.join(tmp_dir.path, "pip.zip") + with zipfile.ZipFile(pip_zip, "w") as zf: + for child in source.rglob("*"): + arcname = child.relative_to(source.parent) + zf.write(child, arcname.as_posix()) + yield os.path.join(pip_zip, "pip") + + +def _install_requirements( + standalone_pip: str, + finder: "PackageFinder", + requirements: Iterable[str], + prefix: _Prefix, + message: str, +) -> None: + args = [ + sys.executable, standalone_pip, 'install', + '--ignore-installed', '--no-user', '--prefix', prefix.path, + '--no-warn-script-location', + ] # type: List[str] + if logger.getEffectiveLevel() <= logging.DEBUG: + args.append('-v') + for format_control in ('no_binary', 'only_binary'): + formats = getattr(finder.format_control, format_control) + args += [ + '--' + format_control.replace('_', '-'), + ','.join(sorted(formats or {':none:'})) + ] + index_urls = finder.index_urls + if index_urls: + args.extend(['-i', index_urls[0]]) + for extra_index in index_urls[1:]: + args.extend(['--extra-index-url', extra_index]) + else: + args.append('--no-index') + for link in finder.find_links: + args.extend(['--find-links', link]) + + for host in finder.trusted_hosts: + args.extend(['--trusted-host', host]) + if finder.allow_all_prereleases: + args.append('--pre') + if finder.prefer_binary: + args.append('--prefer-binary') + args.append('--') + args.extend(requirements) + with open_spinner(message) as spinner: + call_subprocess(args, spinner=spinner) + + class BuildEnvironment: """Creates and manages an isolated environment to install build deps """ @@ -160,38 +218,14 @@ def install_requirements( prefix.setup = True if not requirements: return - args = [ - sys.executable, os.path.dirname(pip_location), 'install', - '--ignore-installed', '--no-user', '--prefix', prefix.path, - '--no-warn-script-location', - ] # type: List[str] - if logger.getEffectiveLevel() <= logging.DEBUG: - args.append('-v') - for format_control in ('no_binary', 'only_binary'): - formats = getattr(finder.format_control, format_control) - args.extend(('--' + format_control.replace('_', '-'), - ','.join(sorted(formats or {':none:'})))) - - index_urls = finder.index_urls - if index_urls: - args.extend(['-i', index_urls[0]]) - for extra_index in index_urls[1:]: - args.extend(['--extra-index-url', extra_index]) - else: - args.append('--no-index') - for link in finder.find_links: - args.extend(['--find-links', link]) - - for host in finder.trusted_hosts: - args.extend(['--trusted-host', host]) - if finder.allow_all_prereleases: - args.append('--pre') - if finder.prefer_binary: - args.append('--prefer-binary') - args.append('--') - args.extend(requirements) - with open_spinner(message) as spinner: - call_subprocess(args, spinner=spinner) + with _create_standalone_pip() as standalone_pip: + _install_requirements( + standalone_pip, + finder, + requirements, + prefix, + message, + ) class NoOpBuildEnvironment(BuildEnvironment): From 638b562048f033cd79b236e673cba09202a396b9 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 5 Mar 2021 22:57:45 +0800 Subject: [PATCH 3103/3170] Pass parent certificate location to isolated pip --- src/pip/_internal/build_env.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py index fc8a0f1e2c2..0067d840536 100644 --- a/src/pip/_internal/build_env.py +++ b/src/pip/_internal/build_env.py @@ -13,6 +13,7 @@ from types import TracebackType from typing import TYPE_CHECKING, Iterable, Iterator, List, Optional, Set, Tuple, Type +from pip._vendor.certifi import where from pip._vendor.pkg_resources import Requirement, VersionConflict, WorkingSet from pip import __file__ as pip_location @@ -63,7 +64,7 @@ def _install_requirements( args = [ sys.executable, standalone_pip, 'install', '--ignore-installed', '--no-user', '--prefix', prefix.path, - '--no-warn-script-location', + '--cert', where(), '--no-warn-script-location', ] # type: List[str] if logger.getEffectiveLevel() <= logging.DEBUG: args.append('-v') @@ -82,7 +83,6 @@ def _install_requirements( args.append('--no-index') for link in finder.find_links: args.extend(['--find-links', link]) - for host in finder.trusted_hosts: args.extend(['--trusted-host', host]) if finder.allow_all_prereleases: From e8e5153612287d9114ec1a3fdac719ebd02826d3 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sat, 6 Mar 2021 02:26:56 +0800 Subject: [PATCH 3104/3170] Better name temp directory --- src/pip/_internal/build_env.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py index 0067d840536..7c07f8e256e 100644 --- a/src/pip/_internal/build_env.py +++ b/src/pip/_internal/build_env.py @@ -45,7 +45,7 @@ def __init__(self, path): def _create_standalone_pip() -> Iterator[str]: """Create a zip file containing specified pip installation.""" source = pathlib.Path(pip_location).resolve().parent - with TempDirectory() as tmp_dir: + with TempDirectory(kind="standalone-pip") as tmp_dir: pip_zip = os.path.join(tmp_dir.path, "pip.zip") with zipfile.ZipFile(pip_zip, "w") as zf: for child in source.rglob("*"): From 7067359751247a6c4a2f5f3f8d91c21e45ef009c Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sat, 6 Mar 2021 02:27:40 +0800 Subject: [PATCH 3105/3170] Less diff --- src/pip/_internal/build_env.py | 84 +++++++++++++++++----------------- 1 file changed, 42 insertions(+), 42 deletions(-) diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py index 7c07f8e256e..a9e6262457f 100644 --- a/src/pip/_internal/build_env.py +++ b/src/pip/_internal/build_env.py @@ -54,47 +54,6 @@ def _create_standalone_pip() -> Iterator[str]: yield os.path.join(pip_zip, "pip") -def _install_requirements( - standalone_pip: str, - finder: "PackageFinder", - requirements: Iterable[str], - prefix: _Prefix, - message: str, -) -> None: - args = [ - sys.executable, standalone_pip, 'install', - '--ignore-installed', '--no-user', '--prefix', prefix.path, - '--cert', where(), '--no-warn-script-location', - ] # type: List[str] - if logger.getEffectiveLevel() <= logging.DEBUG: - args.append('-v') - for format_control in ('no_binary', 'only_binary'): - formats = getattr(finder.format_control, format_control) - args += [ - '--' + format_control.replace('_', '-'), - ','.join(sorted(formats or {':none:'})) - ] - index_urls = finder.index_urls - if index_urls: - args.extend(['-i', index_urls[0]]) - for extra_index in index_urls[1:]: - args.extend(['--extra-index-url', extra_index]) - else: - args.append('--no-index') - for link in finder.find_links: - args.extend(['--find-links', link]) - for host in finder.trusted_hosts: - args.extend(['--trusted-host', host]) - if finder.allow_all_prereleases: - args.append('--pre') - if finder.prefer_binary: - args.append('--prefer-binary') - args.append('--') - args.extend(requirements) - with open_spinner(message) as spinner: - call_subprocess(args, spinner=spinner) - - class BuildEnvironment: """Creates and manages an isolated environment to install build deps """ @@ -219,7 +178,7 @@ def install_requirements( if not requirements: return with _create_standalone_pip() as standalone_pip: - _install_requirements( + self._install_requirements( standalone_pip, finder, requirements, @@ -227,6 +186,47 @@ def install_requirements( message, ) + @staticmethod + def _install_requirements( + standalone_pip: str, + finder: "PackageFinder", + requirements: Iterable[str], + prefix: _Prefix, + message: str, + ) -> None: + args = [ + sys.executable, standalone_pip, 'install', + '--ignore-installed', '--no-user', '--prefix', prefix.path, + '--no-warn-script-location', '--cert', where(), + ] # type: List[str] + if logger.getEffectiveLevel() <= logging.DEBUG: + args.append('-v') + for format_control in ('no_binary', 'only_binary'): + formats = getattr(finder.format_control, format_control) + args.extend(('--' + format_control.replace('_', '-'), + ','.join(sorted(formats or {':none:'})))) + + index_urls = finder.index_urls + if index_urls: + args.extend(['-i', index_urls[0]]) + for extra_index in index_urls[1:]: + args.extend(['--extra-index-url', extra_index]) + else: + args.append('--no-index') + for link in finder.find_links: + args.extend(['--find-links', link]) + + for host in finder.trusted_hosts: + args.extend(['--trusted-host', host]) + if finder.allow_all_prereleases: + args.append('--pre') + if finder.prefer_binary: + args.append('--prefer-binary') + args.append('--') + args.extend(requirements) + with open_spinner(message) as spinner: + call_subprocess(args, spinner=spinner) + class NoOpBuildEnvironment(BuildEnvironment): """A no-op drop-in replacement for BuildEnvironment From 4bf083cde6a88359446f76da53e1a942eb00058e Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sat, 6 Mar 2021 04:40:14 +0800 Subject: [PATCH 3106/3170] Monkey-patch .pem path into standalone pip instead --- src/pip/_internal/build_env.py | 31 ++++++++++++++++++++++++++++--- 1 file changed, 28 insertions(+), 3 deletions(-) diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py index a9e6262457f..0b5ee7e2f38 100644 --- a/src/pip/_internal/build_env.py +++ b/src/pip/_internal/build_env.py @@ -41,6 +41,28 @@ def __init__(self, path): self.lib_dirs = get_prefixed_libs(path) +_CERTIFI_WHERE_PATCH = """ +from pip._vendor import certifi +certifi.where = lambda: {pem!r} +""" + + +def _format_main_py(source: pathlib.Path) -> bytes: + """Create a patched pip/__main__.py for the standalone pip. + + The default ``certifi.where()`` relies on the certificate bundle being a + real physical file on-disk, so we monkey-patch it to return the one used + by this process instead. + + Passing ``--cert`` to the standalone pip does not work, since ``requests`` + calls ``where()`` unconditionally on import. + """ + with source.open("rb") as f: + content = f.read() + patch = _CERTIFI_WHERE_PATCH.format(pem=where()).encode("utf-8") + return patch + content + + @contextlib.contextmanager def _create_standalone_pip() -> Iterator[str]: """Create a zip file containing specified pip installation.""" @@ -49,8 +71,11 @@ def _create_standalone_pip() -> Iterator[str]: pip_zip = os.path.join(tmp_dir.path, "pip.zip") with zipfile.ZipFile(pip_zip, "w") as zf: for child in source.rglob("*"): - arcname = child.relative_to(source.parent) - zf.write(child, arcname.as_posix()) + arcname = child.relative_to(source.parent).as_posix() + if arcname == "pip/__main__.py": + zf.writestr(arcname, _format_main_py(child)) + else: + zf.write(child, arcname) yield os.path.join(pip_zip, "pip") @@ -197,7 +222,7 @@ def _install_requirements( args = [ sys.executable, standalone_pip, 'install', '--ignore-installed', '--no-user', '--prefix', prefix.path, - '--no-warn-script-location', '--cert', where(), + '--no-warn-script-location', ] # type: List[str] if logger.getEffectiveLevel() <= logging.DEBUG: args.append('-v') From 0d183d3ee3474e2679663833e323c100c46403ef Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sat, 6 Mar 2021 05:01:12 +0800 Subject: [PATCH 3107/3170] Better docstring --- src/pip/_internal/build_env.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py index 0b5ee7e2f38..45670734f0e 100644 --- a/src/pip/_internal/build_env.py +++ b/src/pip/_internal/build_env.py @@ -65,7 +65,11 @@ def _format_main_py(source: pathlib.Path) -> bytes: @contextlib.contextmanager def _create_standalone_pip() -> Iterator[str]: - """Create a zip file containing specified pip installation.""" + """Create a "standalone pip" zip file. + + The zip file contains a (modified) copy of the pip currently running. + It will be used to install requirements into the build environment. + """ source = pathlib.Path(pip_location).resolve().parent with TempDirectory(kind="standalone-pip") as tmp_dir: pip_zip = os.path.join(tmp_dir.path, "pip.zip") From 40d0529ee3f6e8a602ddc27d7d636c25108a63cd Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sat, 6 Mar 2021 17:35:34 +0800 Subject: [PATCH 3108/3170] Patch __init__.py instead of __main__.py The standalone pip doesn't have a correct sys.path when __main__.py is invoked, and we'd be patching the wrong certifi at that point. The sys.path is guaranteed to be correct when __init__.py is loaded (otherwise we wouldn't be able to import it in the first place). --- src/pip/_internal/build_env.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py index 45670734f0e..85691a1f2e2 100644 --- a/src/pip/_internal/build_env.py +++ b/src/pip/_internal/build_env.py @@ -47,8 +47,8 @@ def __init__(self, path): """ -def _format_main_py(source: pathlib.Path) -> bytes: - """Create a patched pip/__main__.py for the standalone pip. +def _format_init_py(source: pathlib.Path) -> bytes: + """Create a patched pip/__init__.py for the standalone pip. The default ``certifi.where()`` relies on the certificate bundle being a real physical file on-disk, so we monkey-patch it to return the one used @@ -76,8 +76,8 @@ def _create_standalone_pip() -> Iterator[str]: with zipfile.ZipFile(pip_zip, "w") as zf: for child in source.rglob("*"): arcname = child.relative_to(source.parent).as_posix() - if arcname == "pip/__main__.py": - zf.writestr(arcname, _format_main_py(child)) + if arcname == "pip/__init__.py": + zf.writestr(arcname, _format_init_py(child)) else: zf.write(child, arcname) yield os.path.join(pip_zip, "pip") From 2eb7e887ffb291c7eca279fccf9789e64a50b9a0 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sat, 6 Mar 2021 20:52:13 +0800 Subject: [PATCH 3109/3170] Patch certifi to use environ for passing cert --- src/pip/_vendor/certifi/core.py | 16 +++++++++++++ tools/vendoring/patches/certifi.patch | 34 ++++++++++++++++++++++++--- 2 files changed, 47 insertions(+), 3 deletions(-) diff --git a/src/pip/_vendor/certifi/core.py b/src/pip/_vendor/certifi/core.py index 8987449f6b5..b8140cf1ae7 100644 --- a/src/pip/_vendor/certifi/core.py +++ b/src/pip/_vendor/certifi/core.py @@ -8,7 +8,21 @@ """ import os + +class _PipPatchedCertificate(Exception): + pass + + try: + # Return a certificate file on disk for a standalone pip zipapp running in + # an isolated build environment to use. Passing --cert to the standalone + # pip does not work since requests calls where() unconditionally on import. + _PIP_STANDALONE_CERT = os.environ.get("_PIP_STANDALONE_CERT") + if _PIP_STANDALONE_CERT: + def where(): + return _PIP_STANDALONE_CERT + raise _PipPatchedCertificate() + from importlib.resources import path as get_path, read_text _CACERT_CTX = None @@ -38,6 +52,8 @@ def where(): return _CACERT_PATH +except _PipPatchedCertificate: + pass except ImportError: # This fallback will work for Python versions prior to 3.7 that lack the diff --git a/tools/vendoring/patches/certifi.patch b/tools/vendoring/patches/certifi.patch index 9d5395a7b6b..a36a0020ff5 100644 --- a/tools/vendoring/patches/certifi.patch +++ b/tools/vendoring/patches/certifi.patch @@ -1,13 +1,41 @@ diff --git a/src/pip/_vendor/certifi/core.py b/src/pip/_vendor/certifi/core.py -index 5d2b8cd32..8987449f6 100644 +index 5d2b8cd32..b8140cf1a 100644 --- a/src/pip/_vendor/certifi/core.py +++ b/src/pip/_vendor/certifi/core.py -@@ -33,7 +33,7 @@ try: +@@ -8,7 +8,21 @@ This module returns the installation location of cacert.pem or its contents. + """ + import os + ++ ++class _PipPatchedCertificate(Exception): ++ pass ++ ++ + try: ++ # Return a certificate file on disk for a standalone pip zipapp running in ++ # an isolated build environment to use. Passing --cert to the standalone ++ # pip does not work since requests calls where() unconditionally on import. ++ _PIP_STANDALONE_CERT = os.environ.get("_PIP_STANDALONE_CERT") ++ if _PIP_STANDALONE_CERT: ++ def where(): ++ return _PIP_STANDALONE_CERT ++ raise _PipPatchedCertificate() ++ + from importlib.resources import path as get_path, read_text + + _CACERT_CTX = None +@@ -33,11 +47,13 @@ try: # We also have to hold onto the actual context manager, because # it will do the cleanup whenever it gets garbage collected, so # we will also store that at the global level as well. - _CACERT_CTX = get_path("certifi", "cacert.pem") + _CACERT_CTX = get_path("pip._vendor.certifi", "cacert.pem") _CACERT_PATH = str(_CACERT_CTX.__enter__()) - + return _CACERT_PATH + ++except _PipPatchedCertificate: ++ pass + + except ImportError: + # This fallback will work for Python versions prior to 3.7 that lack the From 9cab77eb17c3ed32298d554e5283bbe82d6f9579 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sat, 6 Mar 2021 20:55:02 +0800 Subject: [PATCH 3110/3170] Pass in cert path with private environ --- src/pip/_internal/build_env.py | 31 +++---------------------------- 1 file changed, 3 insertions(+), 28 deletions(-) diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py index 85691a1f2e2..82bb7295da0 100644 --- a/src/pip/_internal/build_env.py +++ b/src/pip/_internal/build_env.py @@ -41,28 +41,6 @@ def __init__(self, path): self.lib_dirs = get_prefixed_libs(path) -_CERTIFI_WHERE_PATCH = """ -from pip._vendor import certifi -certifi.where = lambda: {pem!r} -""" - - -def _format_init_py(source: pathlib.Path) -> bytes: - """Create a patched pip/__init__.py for the standalone pip. - - The default ``certifi.where()`` relies on the certificate bundle being a - real physical file on-disk, so we monkey-patch it to return the one used - by this process instead. - - Passing ``--cert`` to the standalone pip does not work, since ``requests`` - calls ``where()`` unconditionally on import. - """ - with source.open("rb") as f: - content = f.read() - patch = _CERTIFI_WHERE_PATCH.format(pem=where()).encode("utf-8") - return patch + content - - @contextlib.contextmanager def _create_standalone_pip() -> Iterator[str]: """Create a "standalone pip" zip file. @@ -75,11 +53,7 @@ def _create_standalone_pip() -> Iterator[str]: pip_zip = os.path.join(tmp_dir.path, "pip.zip") with zipfile.ZipFile(pip_zip, "w") as zf: for child in source.rglob("*"): - arcname = child.relative_to(source.parent).as_posix() - if arcname == "pip/__init__.py": - zf.writestr(arcname, _format_init_py(child)) - else: - zf.write(child, arcname) + zf.write(child, child.relative_to(source.parent).as_posix()) yield os.path.join(pip_zip, "pip") @@ -253,8 +227,9 @@ def _install_requirements( args.append('--prefer-binary') args.append('--') args.extend(requirements) + extra_environ = {"_PIP_STANDALONE_CERT": where()} with open_spinner(message) as spinner: - call_subprocess(args, spinner=spinner) + call_subprocess(args, spinner=spinner, extra_environ=extra_environ) class NoOpBuildEnvironment(BuildEnvironment): From bba1226a031d44cd745dca11610113aba6bb5099 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sat, 6 Mar 2021 21:10:06 +0800 Subject: [PATCH 3111/3170] Handle zipapp inception --- src/pip/_internal/build_env.py | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/build_env.py b/src/pip/_internal/build_env.py index 82bb7295da0..755dbb1f712 100644 --- a/src/pip/_internal/build_env.py +++ b/src/pip/_internal/build_env.py @@ -45,12 +45,19 @@ def __init__(self, path): def _create_standalone_pip() -> Iterator[str]: """Create a "standalone pip" zip file. - The zip file contains a (modified) copy of the pip currently running. + The zip file's content is identical to the currently-running pip. It will be used to install requirements into the build environment. """ source = pathlib.Path(pip_location).resolve().parent + + # Return the current instance if it is already a zip file. This can happen + # if a PEP 517 requirement is an sdist itself. + if not source.is_dir() and source.parent.name == "__env_pip__.zip": + yield str(source) + return + with TempDirectory(kind="standalone-pip") as tmp_dir: - pip_zip = os.path.join(tmp_dir.path, "pip.zip") + pip_zip = os.path.join(tmp_dir.path, "__env_pip__.zip") with zipfile.ZipFile(pip_zip, "w") as zf: for child in source.rglob("*"): zf.write(child, child.relative_to(source.parent).as_posix()) From 2d759ab0420bfbaa8817f088aae2ec5ebc221be1 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sat, 3 Apr 2021 16:05:55 +0800 Subject: [PATCH 3112/3170] Add news --- news/4822829F-6A45-4202-87BA-A80482DF6D4E.doc.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 news/4822829F-6A45-4202-87BA-A80482DF6D4E.doc.rst diff --git a/news/4822829F-6A45-4202-87BA-A80482DF6D4E.doc.rst b/news/4822829F-6A45-4202-87BA-A80482DF6D4E.doc.rst new file mode 100644 index 00000000000..d688402d1e3 --- /dev/null +++ b/news/4822829F-6A45-4202-87BA-A80482DF6D4E.doc.rst @@ -0,0 +1,2 @@ +Update "setuptools extras" ink to match upstream. + From a41a18fa00dd9e5479542d093173e84e19de7eea Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sat, 3 Apr 2021 16:17:42 +0800 Subject: [PATCH 3113/3170] Revert documentation change --- docs/html/reference/pip_freeze.rst | 23 ----------------------- 1 file changed, 23 deletions(-) diff --git a/docs/html/reference/pip_freeze.rst b/docs/html/reference/pip_freeze.rst index ac0532b478f..352f7d32168 100644 --- a/docs/html/reference/pip_freeze.rst +++ b/docs/html/reference/pip_freeze.rst @@ -72,26 +72,3 @@ Examples env1\bin\python -m pip freeze > requirements.txt env2\bin\python -m pip install -r requirements.txt - - -Fixing permission denied -======================== - -The purpose of this section of documentation is to provide practical suggestions to -pip users who encounter an error where ``pip freeze`` issue a permission error -during requirements info extraction. See issue: -`pip freeze returns "Permission denied: 'hg'" <https://github.com/pypa/pip/issues/8418>`_. - -When you get a "No permission to execute 'cmd'" error, where *cmd* is 'bzr', -'git', 'hg' or 'svn', it means that the VCS command exists, but you have -no permission to execute it. - -This error occurs, for instance, when the command is installed only for another user. -So, the current user don't have permission to execute the other user command. - -To solve that issue, you can: - -- install the command for yourself (local installation), -- ask admin support to install for all users (global installation), -- check and correct the PATH variable of your own environment, -- check the ACL (Access Control List) for this command. From b88de7d6ad5ecdc4318db0abf776139c238aecf2 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sat, 3 Apr 2021 16:23:33 +0800 Subject: [PATCH 3114/3170] Re-apply documentation addition --- docs/html/cli/pip_freeze.rst | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/docs/html/cli/pip_freeze.rst b/docs/html/cli/pip_freeze.rst index 352f7d32168..3533db7930c 100644 --- a/docs/html/cli/pip_freeze.rst +++ b/docs/html/cli/pip_freeze.rst @@ -72,3 +72,21 @@ Examples env1\bin\python -m pip freeze > requirements.txt env2\bin\python -m pip install -r requirements.txt + + +Fixing "Permission denied:" errors +================================== + +The purpose of this section of documentation is to provide practical +suggestions to users seeing a `"Permission denied" error <https://github.com/pypa/pip/issues/8418>`__ on ``pip freeze``. + +This error occurs, for instance, when the command is installed only for another +user, and the current user doesn't have the permission to execute the other +user's command. + +To solve that issue, you can try one of the followings: + +- Install the command for yourself (e.g. in your home directory). +- Ask the system admin to allow this command for all users. +- Check and correct the PATH variable of your own environment. +- Check the `ACL (Access-Control List) <https://en.wikipedia.org/wiki/Access-control_list>`_ for this command. From 0d39ae9734d054c25e48e2da7ddb76545e5de330 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 9 Mar 2021 05:20:23 +0800 Subject: [PATCH 3115/3170] Pull in resolvelib's typing information --- .../resolution/resolvelib/factory.py | 9 ++- .../resolution/resolvelib/provider.py | 24 ++++-- .../resolution/resolvelib/resolver.py | 25 +++++-- src/pip/_vendor/resolvelib.pyi | 1 - src/pip/_vendor/resolvelib/__init__.py | 2 +- src/pip/_vendor/resolvelib/__init__.pyi | 15 ++++ src/pip/_vendor/resolvelib/providers.py | 8 +- src/pip/_vendor/resolvelib/providers.pyi | 44 +++++++++++ src/pip/_vendor/resolvelib/py.typed | 0 src/pip/_vendor/resolvelib/reporters.pyi | 10 +++ src/pip/_vendor/resolvelib/resolvers.py | 39 +++++----- src/pip/_vendor/resolvelib/resolvers.pyi | 73 +++++++++++++++++++ src/pip/_vendor/resolvelib/structs.pyi | 35 +++++++++ src/pip/_vendor/vendor.txt | 2 +- 14 files changed, 242 insertions(+), 45 deletions(-) delete mode 100644 src/pip/_vendor/resolvelib.pyi create mode 100644 src/pip/_vendor/resolvelib/__init__.pyi create mode 100644 src/pip/_vendor/resolvelib/providers.pyi create mode 100644 src/pip/_vendor/resolvelib/py.typed create mode 100644 src/pip/_vendor/resolvelib/reporters.pyi create mode 100644 src/pip/_vendor/resolvelib/resolvers.pyi create mode 100644 src/pip/_vendor/resolvelib/structs.pyi diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index a4eec7136bb..4424b2881f6 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -12,6 +12,7 @@ Set, Tuple, TypeVar, + cast, ) from pip._vendor.packaging.specifiers import SpecifierSet @@ -419,7 +420,7 @@ def _report_requires_python_error(self, causes): return UnsupportedPythonVersion(message) def _report_single_requirement_conflict(self, req, parent): - # type: (Requirement, Candidate) -> DistributionNotFound + # type: (Requirement, Optional[Candidate]) -> DistributionNotFound if parent is None: req_disp = str(req) else: @@ -439,7 +440,7 @@ def _report_single_requirement_conflict(self, req, parent): def get_installation_error( self, - e, # type: ResolutionImpossible + e, # type: ResolutionImpossible[Requirement, Candidate] constraints, # type: Dict[str, Constraint] ): # type: (...) -> InstallationError @@ -455,7 +456,9 @@ def get_installation_error( and not cause.requirement.is_satisfied_by(self._python_candidate) ] if requires_python_causes: - return self._report_requires_python_error(requires_python_causes) + return self._report_requires_python_error( + cast("Sequence[ConflictCause]", requires_python_causes), + ) # Otherwise, we have a set of causes which can't all be satisfied # at once. diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index 2085a0714a3..cd2ccfa60a3 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -1,10 +1,20 @@ -from typing import Any, Dict, Iterable, Optional, Sequence, Tuple, Union +from typing import TYPE_CHECKING, Dict, Iterable, Optional, Sequence, Union from pip._vendor.resolvelib.providers import AbstractProvider from .base import Candidate, Constraint, Requirement from .factory import Factory +if TYPE_CHECKING: + from pip._vendor.resolvelib.providers import Preference + from pip._vendor.resolvelib.resolvers import RequirementInformation + + PreferenceInformation = RequirementInformation[Requirement, Candidate] + + _ProviderBase = AbstractProvider[Requirement, Candidate, str] +else: + _ProviderBase = AbstractProvider + # Notes on the relationship between the provider, the factory, and the # candidate and requirement classes. # @@ -24,7 +34,7 @@ # services to those objects (access to pip's finder and preparer). -class PipProvider(AbstractProvider): +class PipProvider(_ProviderBase): """Pip's provider implementation for resolvelib. :params constraints: A mapping of constraints specified by the user. Keys @@ -50,17 +60,17 @@ def __init__( self._upgrade_strategy = upgrade_strategy self._user_requested = user_requested - def identify(self, dependency): + def identify(self, requirement_or_candidate): # type: (Union[Requirement, Candidate]) -> str - return dependency.name + return requirement_or_candidate.name def get_preference( self, resolution, # type: Optional[Candidate] - candidates, # type: Sequence[Candidate] - information, # type: Sequence[Tuple[Requirement, Candidate]] + candidates, # type: Iterable[Candidate] + information, # type: Iterable[PreferenceInformation] ): - # type: (...) -> Any + # type: (...) -> Preference """Produce a sort key for given requirement based on preference. The lower the return value is, the more preferred this group of diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 4f8a53d0d07..0eab785d85a 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -1,13 +1,13 @@ import functools import logging import os -from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple +from typing import TYPE_CHECKING, Dict, List, Optional, Set, Tuple, cast from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.packaging.version import parse as parse_version from pip._vendor.resolvelib import BaseReporter, ResolutionImpossible from pip._vendor.resolvelib import Resolver as RLResolver -from pip._vendor.resolvelib.resolvers import Result +from pip._vendor.resolvelib.structs import DirectedGraph from pip._internal.cache import WheelCache from pip._internal.exceptions import InstallationError @@ -28,11 +28,14 @@ from pip._internal.utils.filetypes import is_archive_file from pip._internal.utils.misc import dist_is_editable -from .base import Constraint +from .base import Candidate, Constraint, Requirement from .factory import Factory if TYPE_CHECKING: - from pip._vendor.resolvelib.structs import DirectedGraph + from pip._vendor.resolvelib.resolvers import Result as RLResult + + Result = RLResult[Requirement, Candidate, str] + logger = logging.getLogger(__name__) @@ -114,7 +117,10 @@ def resolve(self, root_reqs, check_supported_wheels): reporter = PipDebuggingReporter() # type: BaseReporter else: reporter = PipReporter() - resolver = RLResolver(provider, reporter) + resolver = RLResolver( + provider, + reporter, + ) # type: RLResolver[Requirement, Candidate, str] try: try_to_avoid_resolution_too_deep = 2000000 @@ -123,7 +129,10 @@ def resolve(self, root_reqs, check_supported_wheels): ) except ResolutionImpossible as e: - error = self.factory.get_installation_error(e, constraints) + error = self.factory.get_installation_error( + cast("ResolutionImpossible[Requirement, Candidate]", e), + constraints, + ) raise error from e req_set = RequirementSet(check_supported_wheels=check_supported_wheels) @@ -148,7 +157,7 @@ def resolve(self, root_reqs, check_supported_wheels): # The incoming distribution is editable, or different in # editable-ness to installation -- reinstall. ireq.should_reinstall = True - elif candidate.source_link.is_file: + elif candidate.source_link and candidate.source_link.is_file: # The incoming distribution is under file:// if candidate.source_link.is_wheel: # is a local wheel -- do nothing. @@ -236,7 +245,7 @@ def get_installation_order(self, req_set): def get_topological_weights(graph, expected_node_count): - # type: (DirectedGraph, int) -> Dict[Optional[str], int] + # type: (DirectedGraph[Optional[str]], int) -> Dict[Optional[str], int] """Assign weights to each node based on how "deep" they are. This implementation may change at any point in the future without prior diff --git a/src/pip/_vendor/resolvelib.pyi b/src/pip/_vendor/resolvelib.pyi deleted file mode 100644 index b4ef4e108c4..00000000000 --- a/src/pip/_vendor/resolvelib.pyi +++ /dev/null @@ -1 +0,0 @@ -from resolvelib import * \ No newline at end of file diff --git a/src/pip/_vendor/resolvelib/__init__.py b/src/pip/_vendor/resolvelib/__init__.py index f023ad63154..63ee53446a5 100644 --- a/src/pip/_vendor/resolvelib/__init__.py +++ b/src/pip/_vendor/resolvelib/__init__.py @@ -11,7 +11,7 @@ "ResolutionTooDeep", ] -__version__ = "0.5.4" +__version__ = "0.5.5" from .providers import AbstractProvider, AbstractResolver diff --git a/src/pip/_vendor/resolvelib/__init__.pyi b/src/pip/_vendor/resolvelib/__init__.pyi new file mode 100644 index 00000000000..4a84f8f3045 --- /dev/null +++ b/src/pip/_vendor/resolvelib/__init__.pyi @@ -0,0 +1,15 @@ +__version__: str + +from .providers import ( + AbstractResolver as AbstractResolver, + AbstractProvider as AbstractProvider, +) +from .reporters import BaseReporter as BaseReporter +from .resolvers import ( + InconsistentCandidate as InconsistentCandidate, + RequirementsConflicted as RequirementsConflicted, + Resolver as Resolver, + ResolutionError as ResolutionError, + ResolutionImpossible as ResolutionImpossible, + ResolutionTooDeep as ResolutionTooDeep, +) diff --git a/src/pip/_vendor/resolvelib/providers.py b/src/pip/_vendor/resolvelib/providers.py index 965cf9c138f..8ef700cc0d7 100644 --- a/src/pip/_vendor/resolvelib/providers.py +++ b/src/pip/_vendor/resolvelib/providers.py @@ -2,12 +2,10 @@ class AbstractProvider(object): """Delegate class to provide requirement interface for the resolver.""" def identify(self, requirement_or_candidate): - """Given a requirement or candidate, return an identifier for it. + """Given a requirement, return an identifier for it. - This is used in many places to identify a requirement or candidate, - e.g. whether two requirements should have their specifier parts merged, - whether two candidates would conflict with each other (because they - have same name but different versions). + This is used to identify a requirement, e.g. whether two requirements + should have their specifier parts merged. """ raise NotImplementedError diff --git a/src/pip/_vendor/resolvelib/providers.pyi b/src/pip/_vendor/resolvelib/providers.pyi new file mode 100644 index 00000000000..3c8ff24d4b3 --- /dev/null +++ b/src/pip/_vendor/resolvelib/providers.pyi @@ -0,0 +1,44 @@ +from typing import ( + Any, + Collection, + Generic, + Iterable, + Mapping, + Optional, + Protocol, + Sequence, + Union, +) + +from .reporters import BaseReporter +from .resolvers import RequirementInformation +from .structs import ( + KT, + RT, + CT, + IterableView, + Matches, +) + +class Preference(Protocol): + def __lt__(self, __other: Any) -> bool: ... + +class AbstractProvider(Generic[RT, CT, KT]): + def identify(self, requirement_or_candidate: Union[RT, CT]) -> KT: ... + def get_preference( + self, + resolution: Optional[CT], + candidates: IterableView[CT], + information: Collection[RequirementInformation[RT, CT]], + ) -> Preference: ... + def find_matches(self, requirements: Sequence[RT]) -> Matches: ... + def is_satisfied_by(self, requirement: RT, candidate: CT) -> bool: ... + def get_dependencies(self, candidate: CT) -> Iterable[RT]: ... + +class AbstractResolver(Generic[RT, CT, KT]): + base_exception = Exception + provider: AbstractProvider[RT, CT, KT] + reporter: BaseReporter + def __init__( + self, provider: AbstractProvider[RT, CT, KT], reporter: BaseReporter + ): ... diff --git a/src/pip/_vendor/resolvelib/py.typed b/src/pip/_vendor/resolvelib/py.typed new file mode 100644 index 00000000000..e69de29bb2d diff --git a/src/pip/_vendor/resolvelib/reporters.pyi b/src/pip/_vendor/resolvelib/reporters.pyi new file mode 100644 index 00000000000..55e38ab88d6 --- /dev/null +++ b/src/pip/_vendor/resolvelib/reporters.pyi @@ -0,0 +1,10 @@ +from typing import Any + +class BaseReporter: + def starting(self) -> Any: ... + def starting_round(self, index: int) -> Any: ... + def ending_round(self, index: int, state: Any) -> Any: ... + def ending(self, state: Any) -> Any: ... + def adding_requirement(self, requirement: Any, parent: Any) -> Any: ... + def backtracking(self, candidate: Any) -> Any: ... + def pinning(self, candidate: Any) -> Any: ... diff --git a/src/pip/_vendor/resolvelib/resolvers.py b/src/pip/_vendor/resolvelib/resolvers.py index bb88d8c2c75..60a30ee4f59 100644 --- a/src/pip/_vendor/resolvelib/resolvers.py +++ b/src/pip/_vendor/resolvelib/resolvers.py @@ -76,7 +76,8 @@ def __repr__(self): @classmethod def from_requirement(cls, provider, requirement, parent): """Build an instance from a requirement.""" - cands = build_iter_view(provider.find_matches([requirement])) + matches = provider.find_matches(requirements=[requirement]) + cands = build_iter_view(matches) infos = [RequirementInformation(requirement, parent)] criterion = cls(cands, infos, incompatibilities=[]) if not cands: @@ -93,7 +94,8 @@ def merged_with(self, provider, requirement, parent): """Build a new instance from this and a new requirement.""" infos = list(self.information) infos.append(RequirementInformation(requirement, parent)) - cands = build_iter_view(provider.find_matches([r for r, _ in infos])) + matches = provider.find_matches([r for r, _ in infos]) + cands = build_iter_view(matches) criterion = type(self)(cands, infos, list(self.incompatibilities)) if not cands: raise RequirementsConflicted(criterion) @@ -165,22 +167,21 @@ def _push_new_state(self): self._states.append(state) def _merge_into_criterion(self, requirement, parent): - self._r.adding_requirement(requirement, parent) - name = self._p.identify(requirement) - try: + self._r.adding_requirement(requirement=requirement, parent=parent) + name = self._p.identify(requirement_or_candidate=requirement) + if name in self.state.criteria: crit = self.state.criteria[name] - except KeyError: - crit = Criterion.from_requirement(self._p, requirement, parent) - else: crit = crit.merged_with(self._p, requirement, parent) + else: + crit = Criterion.from_requirement(self._p, requirement, parent) return name, crit def _get_criterion_item_preference(self, item): name, criterion = item return self._p.get_preference( - self.state.mapping.get(name), - criterion.candidates.for_preference(), - criterion.information, + resolution=self.state.mapping.get(name), + candidates=criterion.candidates.for_preference(), + information=criterion.information, ) def _is_current_pin_satisfying(self, name, criterion): @@ -189,13 +190,13 @@ def _is_current_pin_satisfying(self, name, criterion): except KeyError: return False return all( - self._p.is_satisfied_by(r, current_pin) + self._p.is_satisfied_by(requirement=r, candidate=current_pin) for r in criterion.iter_requirement() ) def _get_criteria_to_update(self, candidate): criteria = {} - for r in self._p.get_dependencies(candidate): + for r in self._p.get_dependencies(candidate=candidate): name, crit = self._merge_into_criterion(r, parent=candidate) criteria[name] = crit return criteria @@ -214,7 +215,7 @@ def _attempt_to_pin_criterion(self, name, criterion): # faulty provider, we will raise an error to notify the implementer # to fix find_matches() and/or is_satisfied_by(). satisfied = all( - self._p.is_satisfied_by(r, candidate) + self._p.is_satisfied_by(requirement=r, candidate=candidate) for r in criterion.iter_requirement() ) if not satisfied: @@ -222,7 +223,7 @@ def _attempt_to_pin_criterion(self, name, criterion): # Put newly-pinned candidate at the end. This is essential because # backtracking looks at this mapping to get the last pin. - self._r.pinning(candidate) + self._r.pinning(candidate=candidate) self.state.mapping.pop(name, None) self.state.mapping[name] = candidate self.state.criteria.update(criteria) @@ -274,7 +275,7 @@ def _backtrack(self): # Also mark the newly known incompatibility. incompatibilities_from_broken.append((name, [candidate])) - self._r.backtracking(candidate) + self._r.backtracking(candidate=candidate) # Create a new state from the last known-to-work one, and apply # the previously gathered incompatibility information. @@ -326,7 +327,7 @@ def resolve(self, requirements, max_rounds): self._push_new_state() for round_index in range(max_rounds): - self._r.starting_round(round_index) + self._r.starting_round(index=round_index) unsatisfied_criterion_items = [ item @@ -336,7 +337,7 @@ def resolve(self, requirements, max_rounds): # All criteria are accounted for. Nothing more to pin, we are done! if not unsatisfied_criterion_items: - self._r.ending(self.state) + self._r.ending(state=self.state) return self.state # Choose the most preferred unpinned criterion to try. @@ -359,7 +360,7 @@ def resolve(self, requirements, max_rounds): # Pinning was successful. Push a new state to do another pin. self._push_new_state() - self._r.ending_round(round_index, self.state) + self._r.ending_round(index=round_index, state=self.state) raise ResolutionTooDeep(max_rounds) diff --git a/src/pip/_vendor/resolvelib/resolvers.pyi b/src/pip/_vendor/resolvelib/resolvers.pyi new file mode 100644 index 00000000000..e61b0bcb40b --- /dev/null +++ b/src/pip/_vendor/resolvelib/resolvers.pyi @@ -0,0 +1,73 @@ +from typing import ( + Collection, + Generic, + Iterable, + Iterator, + List, + Mapping, + Optional, +) + +from .providers import AbstractProvider, AbstractResolver +from .structs import ( + CT, + KT, + RT, + DirectedGraph, + IterableView, +) + +# This should be a NamedTuple, but Python 3.6 has a bug that prevents it. +# https://stackoverflow.com/a/50531189/1376863 +class RequirementInformation(tuple, Generic[RT, CT]): + requirement: RT + parent: Optional[CT] + +class Criterion(Generic[RT, CT, KT]): + candidates: IterableView[CT] + information: Collection[RequirementInformation[RT, CT]] + incompatibilities: List[CT] + @classmethod + def from_requirement( + cls, + provider: AbstractProvider[RT, CT, KT], + requirement: RT, + parent: Optional[CT], + ) -> Criterion[RT, CT, KT]: ... + def iter_requirement(self) -> Iterator[RT]: ... + def iter_parent(self) -> Iterator[Optional[CT]]: ... + def merged_with( + self, + provider: AbstractProvider[RT, CT, KT], + requirement: RT, + parent: Optional[CT], + ) -> Criterion[RT, CT, KT]: ... + def excluded_of(self, candidates: List[CT]) -> Criterion[RT, CT, KT]: ... + +class ResolverException(Exception): ... + +class RequirementsConflicted(ResolverException, Generic[RT, CT, KT]): + criterion: Criterion[RT, CT, KT] + +class ResolutionError(ResolverException): ... + +class InconsistentCandidate(ResolverException, Generic[RT, CT, KT]): + candidate: CT + criterion: Criterion[RT, CT, KT] + +class ResolutionImpossible(ResolutionError, Generic[RT, CT]): + causes: List[RequirementInformation[RT, CT]] + +class ResolutionTooDeep(ResolutionError): + round_count: int + +class Result(Generic[RT, CT, KT]): + mapping: Mapping[KT, CT] + graph: DirectedGraph[Optional[KT]] + criteria: Mapping[KT, Criterion[RT, CT, KT]] + +class Resolver(AbstractResolver, Generic[RT, CT, KT]): + base_exception = ResolverException + def resolve( + self, requirements: Iterable[RT], max_rounds: int = 100 + ) -> Result[RT, CT, KT]: ... diff --git a/src/pip/_vendor/resolvelib/structs.pyi b/src/pip/_vendor/resolvelib/structs.pyi new file mode 100644 index 00000000000..1122d17aac6 --- /dev/null +++ b/src/pip/_vendor/resolvelib/structs.pyi @@ -0,0 +1,35 @@ +from abc import ABCMeta +from typing import ( + Callable, + Container, + Generic, + Iterable, + Iterator, + Tuple, + TypeVar, + Union, +) + +KT = TypeVar("KT") +RT = TypeVar("RT") +CT = TypeVar("CT") +_T = TypeVar("_T") +Matches = Union[Iterable[CT], Callable[[], Iterator[CT]]] + +class IterableView(Container[CT], Iterable[CT], metaclass=ABCMeta): + def excluding(self: _T, candidates: Container[CT]) -> _T: ... + +class DirectedGraph(Generic[KT]): + def __iter__(self) -> Iterator[KT]: ... + def __len__(self) -> int: ... + def __contains__(self, key: KT) -> bool: ... + def copy(self) -> "DirectedGraph[KT]": ... + def add(self, key: KT) -> None: ... + def remove(self, key: KT) -> None: ... + def connected(self, f: KT, t: KT) -> bool: ... + def connect(self, f: KT, t: KT) -> None: ... + def iter_edges(self) -> Iterable[Tuple[KT, KT]]: ... + def iter_children(self, key: KT) -> Iterable[KT]: ... + def iter_parents(self, key: KT) -> Iterable[KT]: ... + +def build_iter_view(matches: Matches) -> IterableView[CT]: ... diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 868baba6f01..44ffff933f7 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -14,7 +14,7 @@ requests==2.25.1 chardet==4.0.0 idna==2.10 urllib3==1.26.4 -resolvelib==0.5.4 +resolvelib==0.5.5 retrying==1.3.3 setuptools==44.0.0 six==1.15.0 From f938eda3205ba8ba93c13545c781172a1456c621 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 10 Mar 2021 17:26:19 +0800 Subject: [PATCH 3116/3170] News for resolvelib upgrade --- news/resolvelib.vendor.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/resolvelib.vendor.rst diff --git a/news/resolvelib.vendor.rst b/news/resolvelib.vendor.rst new file mode 100644 index 00000000000..4f102fc0daa --- /dev/null +++ b/news/resolvelib.vendor.rst @@ -0,0 +1 @@ +Upgrade vendored resolvelib to 0.5.5. From 9ceabb568f64548d82fde84ed14f5311d9ca9b60 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Mon, 22 Mar 2021 13:38:08 +0800 Subject: [PATCH 3117/3170] Add note to explain the cast --- src/pip/_internal/resolution/resolvelib/factory.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 4424b2881f6..dd747198fcf 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -456,6 +456,8 @@ def get_installation_error( and not cause.requirement.is_satisfied_by(self._python_candidate) ] if requires_python_causes: + # The comprehension above makes sure all Requirement instances are + # RequiresPythonRequirement, so let's cast for convinience. return self._report_requires_python_error( cast("Sequence[ConflictCause]", requires_python_causes), ) From ecdcfeb8d695c34350cc825758a2af77c9b62148 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sat, 20 Mar 2021 14:32:44 +0100 Subject: [PATCH 3118/3170] Remove now unused VCS export code --- src/pip/_internal/vcs/bazaar.py | 18 +-------- src/pip/_internal/vcs/git.py | 14 ------- src/pip/_internal/vcs/mercurial.py | 11 ------ src/pip/_internal/vcs/subversion.py | 20 ---------- src/pip/_internal/vcs/versioncontrol.py | 14 +------ tests/functional/test_vcs_bazaar.py | 51 +------------------------ tests/unit/test_vcs.py | 8 ---- 7 files changed, 4 insertions(+), 132 deletions(-) diff --git a/src/pip/_internal/vcs/bazaar.py b/src/pip/_internal/vcs/bazaar.py index 3d603727c57..42b68773b14 100644 --- a/src/pip/_internal/vcs/bazaar.py +++ b/src/pip/_internal/vcs/bazaar.py @@ -1,8 +1,7 @@ import logging -import os from typing import List, Optional, Tuple -from pip._internal.utils.misc import HiddenText, display_path, rmtree +from pip._internal.utils.misc import HiddenText, display_path from pip._internal.utils.subprocess import make_command from pip._internal.utils.urls import path_to_url from pip._internal.vcs.versioncontrol import ( @@ -30,21 +29,6 @@ def get_base_rev_args(rev): # type: (str) -> List[str] return ['-r', rev] - def export(self, location, url): - # type: (str, HiddenText) -> None - """ - Export the Bazaar repository at the url to the destination location - """ - # Remove the location to make sure Bazaar can export it correctly - if os.path.exists(location): - rmtree(location) - - url, rev_options = self.get_url_rev_options(url) - self.run_command( - make_command('export', location, url, rev_options.to_args()), - show_stdout=False, - ) - def fetch_new(self, dest, url, rev_options): # type: (str, HiddenText, RevOptions) -> None rev_display = rev_options.to_display() diff --git a/src/pip/_internal/vcs/git.py b/src/pip/_internal/vcs/git.py index e0704091dc6..9f24ccdf5ee 100644 --- a/src/pip/_internal/vcs/git.py +++ b/src/pip/_internal/vcs/git.py @@ -11,7 +11,6 @@ from pip._internal.exceptions import BadCommand, InstallationError from pip._internal.utils.misc import HiddenText, display_path, hide_url from pip._internal.utils.subprocess import make_command -from pip._internal.utils.temp_dir import TempDirectory from pip._internal.vcs.versioncontrol import ( AuthInfo, RemoteNotFoundError, @@ -112,19 +111,6 @@ def get_current_branch(cls, location): return None - def export(self, location, url): - # type: (str, HiddenText) -> None - """Export the Git repository at the url to the destination location""" - if not location.endswith('/'): - location = location + '/' - - with TempDirectory(kind="export") as temp_dir: - self.unpack(temp_dir.path, url=url) - self.run_command( - ['checkout-index', '-a', '-f', '--prefix', location], - show_stdout=False, cwd=temp_dir.path - ) - @classmethod def get_revision_sha(cls, dest, rev): # type: (str, str) -> Tuple[Optional[str], bool] diff --git a/src/pip/_internal/vcs/mercurial.py b/src/pip/_internal/vcs/mercurial.py index fdd71f43894..b4f887d327b 100644 --- a/src/pip/_internal/vcs/mercurial.py +++ b/src/pip/_internal/vcs/mercurial.py @@ -6,7 +6,6 @@ from pip._internal.exceptions import BadCommand, InstallationError from pip._internal.utils.misc import HiddenText, display_path from pip._internal.utils.subprocess import make_command -from pip._internal.utils.temp_dir import TempDirectory from pip._internal.utils.urls import path_to_url from pip._internal.vcs.versioncontrol import ( RevOptions, @@ -31,16 +30,6 @@ def get_base_rev_args(rev): # type: (str) -> List[str] return [rev] - def export(self, location, url): - # type: (str, HiddenText) -> None - """Export the Hg repository at the url to the destination location""" - with TempDirectory(kind="export") as temp_dir: - self.unpack(temp_dir.path, url=url) - - self.run_command( - ['archive', location], show_stdout=False, cwd=temp_dir.path - ) - def fetch_new(self, dest, url, rev_options): # type: (str, HiddenText, RevOptions) -> None rev_display = rev_options.to_display() diff --git a/src/pip/_internal/vcs/subversion.py b/src/pip/_internal/vcs/subversion.py index f58264446eb..4d1237ca0ca 100644 --- a/src/pip/_internal/vcs/subversion.py +++ b/src/pip/_internal/vcs/subversion.py @@ -3,12 +3,10 @@ import re from typing import List, Optional, Tuple -from pip._internal.utils.logging import indent_log from pip._internal.utils.misc import ( HiddenText, display_path, is_console_interactive, - rmtree, split_auth_from_netloc, ) from pip._internal.utils.subprocess import CommandArgs, make_command @@ -272,7 +270,6 @@ def get_remote_call_options(self): in this class. - checkout - - export - switch - update @@ -297,23 +294,6 @@ def get_remote_call_options(self): return [] - def export(self, location, url): - # type: (str, HiddenText) -> None - """Export the svn repository at the url to the destination location""" - url, rev_options = self.get_url_rev_options(url) - - logger.info('Exporting svn repository %s to %s', url, location) - with indent_log(): - if os.path.exists(location): - # Subversion doesn't like to check out over an existing - # directory --force fixes this, but was only added in svn 1.5 - rmtree(location) - cmd_args = make_command( - 'export', self.get_remote_call_options(), - rev_options.to_args(), url, location, - ) - self.run_command(cmd_args, show_stdout=False) - def fetch_new(self, dest, url, rev_options): # type: (str, HiddenText, RevOptions) -> None rev_display = rev_options.to_display() diff --git a/src/pip/_internal/vcs/versioncontrol.py b/src/pip/_internal/vcs/versioncontrol.py index cd6213bb132..97977b5799c 100644 --- a/src/pip/_internal/vcs/versioncontrol.py +++ b/src/pip/_internal/vcs/versioncontrol.py @@ -376,16 +376,6 @@ def _is_local_repository(cls, repo): drive, tail = os.path.splitdrive(repo) return repo.startswith(os.path.sep) or bool(drive) - def export(self, location, url): - # type: (str, HiddenText) -> None - """ - Export the repository at the url to the destination location - i.e. only download the files, without vcs informations - - :param url: the repository URL starting with a vcs prefix. - """ - raise NotImplementedError - @classmethod def get_netloc_and_auth(cls, netloc, scheme): # type: (str, str) -> Tuple[str, Tuple[Optional[str], Optional[str]]] @@ -448,8 +438,8 @@ def make_rev_args(username, password): def get_url_rev_options(self, url): # type: (HiddenText) -> Tuple[HiddenText, RevOptions] """ - Return the URL and RevOptions object to use in obtain() and in - some cases export(), as a tuple (url, rev_options). + Return the URL and RevOptions object to use in obtain(), + as a tuple (url, rev_options). """ secret_url, rev, user_pass = self.get_url_rev_and_auth(url.secret) username, secret_password = user_pass diff --git a/tests/functional/test_vcs_bazaar.py b/tests/functional/test_vcs_bazaar.py index 57fee51e780..0e598382a8e 100644 --- a/tests/functional/test_vcs_bazaar.py +++ b/tests/functional/test_vcs_bazaar.py @@ -6,16 +6,9 @@ import pytest -from pip._internal.utils.misc import hide_url from pip._internal.vcs.bazaar import Bazaar from pip._internal.vcs.versioncontrol import RemoteNotFoundError -from tests.lib import ( - _test_path_to_file_url, - _vcs_add, - create_file, - is_bzr_installed, - need_bzr, -) +from tests.lib import is_bzr_installed, need_bzr @pytest.mark.skipif( @@ -26,48 +19,6 @@ def test_ensure_bzr_available(): assert is_bzr_installed() -@need_bzr -def test_export(script, tmpdir): - """Test that a Bazaar branch can be exported.""" - source_dir = tmpdir / 'test-source' - source_dir.mkdir() - - create_file(source_dir / 'test_file', 'something') - - _vcs_add(script, str(source_dir), vcs='bazaar') - - export_dir = str(tmpdir / 'export') - url = hide_url('bzr+' + _test_path_to_file_url(source_dir)) - Bazaar().export(export_dir, url=url) - - assert os.listdir(export_dir) == ['test_file'] - - -@need_bzr -def test_export_rev(script, tmpdir): - """Test that a Bazaar branch can be exported, specifying a rev.""" - source_dir = tmpdir / 'test-source' - source_dir.mkdir() - - # Create a single file that is changed by two revisions. - create_file(source_dir / 'test_file', 'something initial') - _vcs_add(script, str(source_dir), vcs='bazaar') - - create_file(source_dir / 'test_file', 'something new') - script.run( - 'bzr', 'commit', '-q', - '--author', 'pip <distutils-sig@python.org>', - '-m', 'change test file', cwd=source_dir, - ) - - export_dir = tmpdir / 'export' - url = hide_url('bzr+' + _test_path_to_file_url(source_dir) + '@1') - Bazaar().export(str(export_dir), url=url) - - with open(export_dir / 'test_file') as f: - assert f.read() == 'something initial' - - @need_bzr def test_get_remote_url__no_remote(script, tmpdir): repo_dir = tmpdir / 'temp-repo' diff --git a/tests/unit/test_vcs.py b/tests/unit/test_vcs.py index 6d82e139a48..f86d04d7425 100644 --- a/tests/unit/test_vcs.py +++ b/tests/unit/test_vcs.py @@ -570,14 +570,6 @@ def test_obtain(self): hide_url('http://svn.example.com/'), '/tmp/test', ]) - def test_export(self): - self.svn.export(self.dest, hide_url(self.url)) - self.assert_call_args([ - 'svn', 'export', '--non-interactive', '--username', 'username', - '--password', hide_value('password'), - hide_url('http://svn.example.com/'), '/tmp/test', - ]) - def test_fetch_new(self): self.svn.fetch_new(self.dest, hide_url(self.url), self.rev_options) self.assert_call_args([ From b43a8ed51ef4e10c55f20e63c6d7658d066dde5a Mon Sep 17 00:00:00 2001 From: Eric Cousineau <eric.cousineau@tri.global> Date: Fri, 5 Mar 2021 11:52:30 -0500 Subject: [PATCH 3119/3170] docs: Describe --upgrade-strategy and direct requirements explicitly Add a brief example --- docs/html/cli/pip_install.rst | 5 +++++ .../development/architecture/upgrade-options.rst | 6 ++++-- docs/html/user_guide.rst | 15 +++++++++++++++ 3 files changed, 24 insertions(+), 2 deletions(-) diff --git a/docs/html/cli/pip_install.rst b/docs/html/cli/pip_install.rst index d97ce0f981b..664f8e2a9da 100644 --- a/docs/html/cli/pip_install.rst +++ b/docs/html/cli/pip_install.rst @@ -1000,6 +1000,11 @@ Examples py -m pip install --upgrade SomePackage + .. note:: + + This will only update ``SomePackage`` as it is a direct requirement. Any + of its dependencies (indirect requirements) will be affected by the + ``--upgrade-strategy`` command. #. Install a local project in "editable" mode. See the section on :ref:`Editable Installs <editable-installs>`. diff --git a/docs/html/development/architecture/upgrade-options.rst b/docs/html/development/architecture/upgrade-options.rst index 6196413ef93..2f1ced013c2 100644 --- a/docs/html/development/architecture/upgrade-options.rst +++ b/docs/html/development/architecture/upgrade-options.rst @@ -30,7 +30,8 @@ candidate. ``--upgrade-strategy`` This option affects which packages are allowed to be installed. It is only -relevant if ``--upgrade`` is specified. The base behaviour is to allow +relevant if ``--upgrade`` is specified (except for the ``to-satisfy-only`` +option mentioned below). The base behaviour is to allow packages specified on pip's command line to be upgraded. This option controls what *other* packages can be upgraded: @@ -45,7 +46,8 @@ what *other* packages can be upgraded: currently installed. * ``to-satisfy-only`` (**undocumented**) - packages are not upgraded (not even direct requirements) unless the currently installed version fails to - satisfy a requirement (either explicitly specified or a dependency). + satisfy a requirement (either explicitly specified or a dependency). This + is actually the "default" strategy when ``--upgrade`` is not set. ``--force-reinstall`` diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 23d6b7c6f1b..10e372b19fd 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -825,6 +825,21 @@ strategies supported: The default strategy is ``only-if-needed``. This was changed in pip 10.0 due to the breaking nature of ``eager`` when upgrading conflicting dependencies. +It is important to note that ``--upgrade`` affects *direct requirements* (e.g. +those specified on the command-line or via a requirements file) while +``--upgrade-strategy`` affects *indirect requirements* (dependencies of direct +requirements). + +As an example, say ``SomePackage`` has a dependency, ``SomeDependency``, and +both of them are already installed but are not the latest avaialable versions: + +- ``pip install SomePackage``: will not upgrade the existing ``SomePackage`` or + ``SomeDependency``. +- ``pip install --upgrade SomePackage``: will upgrade ``SomePackage``, but not + ``SomeDependency`` (unless a minimum requirement is not met). +- ``pip install --upgrade SomePackage --upgrade-strategy=eager``: upgrades both + ``SomePackage`` and ``SomeDependency``. + As an historic note, an earlier "fix" for getting the ``only-if-needed`` behaviour was: From 9f73611df6ad95693b93a6c4fae0aed0e209bb17 Mon Sep 17 00:00:00 2001 From: Eric Cousineau <eric.cousineau@tri.global> Date: Fri, 5 Mar 2021 11:58:38 -0500 Subject: [PATCH 3120/3170] address review, I think? --- docs/html/cli/pip_install.rst | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/html/cli/pip_install.rst b/docs/html/cli/pip_install.rst index 664f8e2a9da..9ebb6e3f78b 100644 --- a/docs/html/cli/pip_install.rst +++ b/docs/html/cli/pip_install.rst @@ -1002,9 +1002,11 @@ Examples .. note:: - This will only update ``SomePackage`` as it is a direct requirement. Any - of its dependencies (indirect requirements) will be affected by the - ``--upgrade-strategy`` command. + This will guarantee an update to ``SomePackage`` as it is a direct + requirement, and possibly upgrade dependencies if their installed + versions do not meet the minimum requirements of ``SomePackage``. + Any non-requisite updates of its dependencies (indirect requirements) + will be affected by the ``--upgrade-strategy`` command. #. Install a local project in "editable" mode. See the section on :ref:`Editable Installs <editable-installs>`. From f1f94baf5c56738174644e6bd45d19cf80aa8597 Mon Sep 17 00:00:00 2001 From: Eric Cousineau <eric.cousineau@tri.global> Date: Fri, 5 Mar 2021 12:08:13 -0500 Subject: [PATCH 3121/3170] try to encode more deets --- .../development/architecture/upgrade-options.rst | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/docs/html/development/architecture/upgrade-options.rst b/docs/html/development/architecture/upgrade-options.rst index 2f1ced013c2..76c7d1fc00c 100644 --- a/docs/html/development/architecture/upgrade-options.rst +++ b/docs/html/development/architecture/upgrade-options.rst @@ -44,10 +44,15 @@ what *other* packages can be upgraded: pip command or a requirement file (i.e, they are direct requirements), or an upgraded parent needs a later version of the dependency than is currently installed. -* ``to-satisfy-only`` (**undocumented**) - packages are not upgraded (not - even direct requirements) unless the currently installed version fails to - satisfy a requirement (either explicitly specified or a dependency). This - is actually the "default" strategy when ``--upgrade`` is not set. +* ``to-satisfy-only`` (**undocumented, please avoid**) - packages are not + upgraded (not even direct requirements) unless the currently installed + version fails to satisfy a requirement (either explicitly specified or a + dependency). + + * This is actually the "default" upgrade strategy when ``--upgrade`` is + *not set*, i.e. ``pip install AlreadyInstalled`` and + ``pip install --upgrade --upgrade-strategy=to-satisfy-only AlreadyInstalled`` + yield the same behavior. ``--force-reinstall`` From 81896cf38b95454d621c2ab036100a7f0775d96a Mon Sep 17 00:00:00 2001 From: Eric Cousineau <eric.cousineau@tri.global> Date: Mon, 8 Mar 2021 09:27:33 -0500 Subject: [PATCH 3122/3170] fixup! add news --- news/9692.doc.rst | 2 ++ 1 file changed, 2 insertions(+) create mode 100644 news/9692.doc.rst diff --git a/news/9692.doc.rst b/news/9692.doc.rst new file mode 100644 index 00000000000..2ef9623707f --- /dev/null +++ b/news/9692.doc.rst @@ -0,0 +1,2 @@ +Describe ``--upgrade-strategy`` and direct requirements explicitly; add a brief +example. From d2c280be64e7e6413a481e1b2548d8908fc9c3ae Mon Sep 17 00:00:00 2001 From: Joseph Bylund <joseph.bylund+github@gmail.com> Date: Sat, 3 Apr 2021 11:25:36 -0400 Subject: [PATCH 3123/3170] Improve Performance of Picking Best Candidate from Indexes Use a mapping for random lookup instead of list traversal. --- .gitignore | 3 +++ news/9748.feature.rst | 1 + src/pip/_internal/index/package_finder.py | 13 +++++++++++-- src/pip/_internal/models/wheel.py | 22 ++++++++++++++++++++-- 4 files changed, 35 insertions(+), 4 deletions(-) create mode 100644 news/9748.feature.rst diff --git a/.gitignore b/.gitignore index dc6244855fe..da9a31ab521 100644 --- a/.gitignore +++ b/.gitignore @@ -48,3 +48,6 @@ tests/data/common_wheels/ # Mac .DS_Store + +# Profiling related artifacts +*.prof diff --git a/news/9748.feature.rst b/news/9748.feature.rst new file mode 100644 index 00000000000..cb4a1cdede2 --- /dev/null +++ b/news/9748.feature.rst @@ -0,0 +1 @@ +Improve performance when picking the best file from indexes during `pip install`. diff --git a/src/pip/_internal/index/package_finder.py b/src/pip/_internal/index/package_finder.py index 4cc9ffd4dfa..a6423cce186 100644 --- a/src/pip/_internal/index/package_finder.py +++ b/src/pip/_internal/index/package_finder.py @@ -434,6 +434,12 @@ def __init__( self._project_name = project_name self._specifier = specifier self._supported_tags = supported_tags + # Since the index of the tag in the _supported_tags list is used + # as a priority, precompute a map from tag to index/priority to be + # used in wheel.find_most_preferred_tag. + self._wheel_tag_preferences = { + tag: idx for idx, tag in enumerate(supported_tags) + } def get_applicable_candidates( self, @@ -512,14 +518,17 @@ def _sort_key(self, candidate): if link.is_wheel: # can raise InvalidWheelFilename wheel = Wheel(link.filename) - if not wheel.supported(valid_tags): + try: + pri = -(wheel.find_most_preferred_tag( + valid_tags, self._wheel_tag_preferences + )) + except ValueError: raise UnsupportedWheel( "{} is not a supported wheel for this platform. It " "can't be sorted.".format(wheel.filename) ) if self._prefer_binary: binary_preference = 1 - pri = -(wheel.support_index_min(valid_tags)) if wheel.build_tag is not None: match = re.match(r'^(\d+)(.*)$', wheel.build_tag) build_tag_groups = match.groups() diff --git a/src/pip/_internal/models/wheel.py b/src/pip/_internal/models/wheel.py index 708bff33067..c206d13cb7a 100644 --- a/src/pip/_internal/models/wheel.py +++ b/src/pip/_internal/models/wheel.py @@ -2,7 +2,7 @@ name that have meaning. """ import re -from typing import List +from typing import Dict, List from pip._vendor.packaging.tags import Tag @@ -66,8 +66,26 @@ def support_index_min(self, tags): """ return min(tags.index(tag) for tag in self.file_tags if tag in tags) + def find_most_preferred_tag(self, tags, tag_to_priority): + # type: (List[Tag], Dict[Tag, int]) -> int + """Return the priority of the most preferred tag that one of the wheel's file + tag combinations acheives in the given list of supported tags using the given + tag_to_priority mapping, where lower priorities are more-preferred. + + This is used in place of support_index_min in some cases in order to avoid + an expensive linear scan of a large list of tags. + + :param tags: the PEP 425 tags to check the wheel against. + :param tag_to_priority: a mapping from tag to priority of that tag, where + lower is more preferred. + + :raises ValueError: If none of the wheel's file tags match one of + the supported tags. + """ + return min(tag_to_priority[tag] for tag in self.file_tags if tag in tag_to_priority) + def supported(self, tags): - # type: (List[Tag]) -> bool + # type: (Iterable[Tag]) -> bool """Return whether the wheel is compatible with one of the given tags. :param tags: the PEP 425 tags to check the wheel against. From 770e5aa7eb66c06a6952ad369986db9dfeef2f7e Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sat, 3 Apr 2021 23:22:46 +0800 Subject: [PATCH 3124/3170] Typo --- news/4822829F-6A45-4202-87BA-A80482DF6D4E.doc.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/4822829F-6A45-4202-87BA-A80482DF6D4E.doc.rst b/news/4822829F-6A45-4202-87BA-A80482DF6D4E.doc.rst index d688402d1e3..6c3ef3653b7 100644 --- a/news/4822829F-6A45-4202-87BA-A80482DF6D4E.doc.rst +++ b/news/4822829F-6A45-4202-87BA-A80482DF6D4E.doc.rst @@ -1,2 +1,2 @@ -Update "setuptools extras" ink to match upstream. +Update "setuptools extras" link to match upstream. From dee71ce661d0d0fcc9916e9e6fe7abe1514fc513 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sun, 4 Apr 2021 06:20:27 +0800 Subject: [PATCH 3125/3170] Run lint on code base --- docs/pip_sphinxext.py | 8 ++------ news/4822829F-6A45-4202-87BA-A80482DF6D4E.doc.rst | 1 - 2 files changed, 2 insertions(+), 7 deletions(-) diff --git a/docs/pip_sphinxext.py b/docs/pip_sphinxext.py index c5990288689..97f07cfe36a 100644 --- a/docs/pip_sphinxext.py +++ b/docs/pip_sphinxext.py @@ -58,11 +58,7 @@ def run(self): self.lineno - self.state_machine.input_offset - 1, ) path = ( - pathlib.Path(source) - .resolve() - .parent - .joinpath(self.arguments[0]) - .resolve() + pathlib.Path(source).resolve().parent.joinpath(self.arguments[0]).resolve() ) include_lines = statemachine.string2lines( path.read_text(encoding="utf-8"), @@ -303,5 +299,5 @@ def setup(app: Sphinx) -> None: app.add_directive( "pip-requirements-file-options-ref-list", PipReqFileOptionsReference ) - app.add_directive('pip-news-include', PipNewsInclude) + app.add_directive("pip-news-include", PipNewsInclude) app.add_directive("pip-cli", PipCLIDirective) diff --git a/news/4822829F-6A45-4202-87BA-A80482DF6D4E.doc.rst b/news/4822829F-6A45-4202-87BA-A80482DF6D4E.doc.rst index 6c3ef3653b7..a67474c7803 100644 --- a/news/4822829F-6A45-4202-87BA-A80482DF6D4E.doc.rst +++ b/news/4822829F-6A45-4202-87BA-A80482DF6D4E.doc.rst @@ -1,2 +1 @@ Update "setuptools extras" link to match upstream. - From ec0a1cb9f0c89caa508106f96b42ea820bf01b0c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sun, 4 Apr 2021 13:39:10 +0200 Subject: [PATCH 3126/3170] ci: fix pre-commit action --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 420bbfab227..09724e8f968 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,7 +43,7 @@ jobs: - uses: actions/setup-python@v2 - uses: pre-commit/action@v2.0.0 with: - extra_args: --hook-stage=manual + extra_args: --all-files --hook-stage=manual packaging: name: packaging From 43e4b418588e99da923bfd2835b7c5aca6add2a6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sun, 4 Apr 2021 13:47:41 +0200 Subject: [PATCH 3127/3170] Use double backticks in rst files --- news/9748.feature.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/news/9748.feature.rst b/news/9748.feature.rst index cb4a1cdede2..28cb3b9228d 100644 --- a/news/9748.feature.rst +++ b/news/9748.feature.rst @@ -1 +1 @@ -Improve performance when picking the best file from indexes during `pip install`. +Improve performance when picking the best file from indexes during ``pip install``. From ff19ecacda3219ef18f550f5ff1aa3d5575634d4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sun, 4 Apr 2021 13:48:24 +0200 Subject: [PATCH 3128/3170] Fix a couple of flake8 issues --- src/pip/_internal/models/wheel.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/models/wheel.py b/src/pip/_internal/models/wheel.py index c206d13cb7a..0a582b305c9 100644 --- a/src/pip/_internal/models/wheel.py +++ b/src/pip/_internal/models/wheel.py @@ -2,7 +2,7 @@ name that have meaning. """ import re -from typing import Dict, List +from typing import Dict, Iterable, List from pip._vendor.packaging.tags import Tag @@ -82,7 +82,9 @@ def find_most_preferred_tag(self, tags, tag_to_priority): :raises ValueError: If none of the wheel's file tags match one of the supported tags. """ - return min(tag_to_priority[tag] for tag in self.file_tags if tag in tag_to_priority) + return min( + tag_to_priority[tag] for tag in self.file_tags if tag in tag_to_priority + ) def supported(self, tags): # type: (Iterable[Tag]) -> bool From 29c50c81884ad17494c7cb9b0af56c30b4c1be39 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sun, 4 Apr 2021 13:58:59 +0200 Subject: [PATCH 3129/3170] Add missing type annotation in docs/ --- docs/pip_sphinxext.py | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/docs/pip_sphinxext.py b/docs/pip_sphinxext.py index 97f07cfe36a..8e4385a33ef 100644 --- a/docs/pip_sphinxext.py +++ b/docs/pip_sphinxext.py @@ -5,7 +5,7 @@ import re import sys from textwrap import dedent -from typing import Iterable, List, Optional +from typing import Iterable, Iterator, List, Optional from docutils import nodes, statemachine from docutils.parsers import rst @@ -20,7 +20,9 @@ class PipNewsInclude(rst.Directive): required_arguments = 1 - def _is_version_section_title_underline(self, prev, curr): + def _is_version_section_title_underline( + self, prev: Optional[str], curr: str + ) -> bool: """Find a ==== line that marks the version section title.""" if prev is None: return False @@ -30,7 +32,7 @@ def _is_version_section_title_underline(self, prev, curr): return False return True - def _iter_lines_with_refs(self, lines): + def _iter_lines_with_refs(self, lines: Iterable[str]) -> Iterator[str]: """Transform the input lines to add a ref before each section title. This is done by looking one line ahead and locate a title's underline, @@ -44,6 +46,7 @@ def _iter_lines_with_refs(self, lines): for line in lines: # Transform the previous line to include an explicit ref. if self._is_version_section_title_underline(prev, line): + assert prev is not None vref = prev.split(None, 1)[0].replace(".", "-") yield f".. _`v{vref}`:" yield "" # Empty line between ref and the title. @@ -53,7 +56,7 @@ def _iter_lines_with_refs(self, lines): if prev is not None: yield prev - def run(self): + def run(self) -> List[nodes.Node]: source = self.state_machine.input_lines.source( self.lineno - self.state_machine.input_offset - 1, ) From 778778c0d3926c8399a650c38c816edc94ea19c2 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sun, 4 Apr 2021 06:03:19 +0800 Subject: [PATCH 3130/3170] Upgrade vendored resolvelib to 0.6.0 --- news/resolvelib.vendor.rst | 2 +- .../resolution/resolvelib/provider.py | 30 ++++-- src/pip/_vendor/resolvelib/__init__.py | 2 +- .../resolvelib/compat/collections_abc.py | 6 +- src/pip/_vendor/resolvelib/providers.py | 18 ++-- src/pip/_vendor/resolvelib/providers.pyi | 9 +- src/pip/_vendor/resolvelib/resolvers.py | 102 ++++++++++-------- src/pip/_vendor/resolvelib/structs.py | 26 +++++ src/pip/_vendor/vendor.txt | 2 +- 9 files changed, 131 insertions(+), 66 deletions(-) diff --git a/news/resolvelib.vendor.rst b/news/resolvelib.vendor.rst index 4f102fc0daa..ebad91f25a9 100644 --- a/news/resolvelib.vendor.rst +++ b/news/resolvelib.vendor.rst @@ -1 +1 @@ -Upgrade vendored resolvelib to 0.5.5. +Upgrade vendored resolvelib to 0.6.0. diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index cd2ccfa60a3..89b68489249 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -1,4 +1,13 @@ -from typing import TYPE_CHECKING, Dict, Iterable, Optional, Sequence, Union +from typing import ( + TYPE_CHECKING, + Dict, + Iterable, + Iterator, + Mapping, + Optional, + Sequence, + Union, +) from pip._vendor.resolvelib.providers import AbstractProvider @@ -134,11 +143,16 @@ def _get_restrictive_rating(requirements): return (delay_this, restrictive, order, key) - def find_matches(self, requirements): - # type: (Sequence[Requirement]) -> Iterable[Candidate] - if not requirements: + def find_matches( + self, + identifier: str, + requirements: Mapping[str, Iterator[Requirement]], + incompatibilities: Mapping[str, Iterator[Candidate]], + ) -> Iterable[Candidate]: + try: + current_requirements = requirements[identifier] + except KeyError: return [] - name = requirements[0].project_name def _eligible_for_upgrade(name): # type: (str) -> bool @@ -159,9 +173,9 @@ def _eligible_for_upgrade(name): return False return self._factory.find_candidates( - requirements, - constraint=self._constraints.get(name, Constraint.empty()), - prefers_installed=(not _eligible_for_upgrade(name)), + list(current_requirements), + constraint=self._constraints.get(identifier, Constraint.empty()), + prefers_installed=(not _eligible_for_upgrade(identifier)), ) def is_satisfied_by(self, requirement, candidate): diff --git a/src/pip/_vendor/resolvelib/__init__.py b/src/pip/_vendor/resolvelib/__init__.py index 63ee53446a5..34be7ee0f42 100644 --- a/src/pip/_vendor/resolvelib/__init__.py +++ b/src/pip/_vendor/resolvelib/__init__.py @@ -11,7 +11,7 @@ "ResolutionTooDeep", ] -__version__ = "0.5.5" +__version__ = "0.6.0" from .providers import AbstractProvider, AbstractResolver diff --git a/src/pip/_vendor/resolvelib/compat/collections_abc.py b/src/pip/_vendor/resolvelib/compat/collections_abc.py index 366cc5e2e12..1becc5093c5 100644 --- a/src/pip/_vendor/resolvelib/compat/collections_abc.py +++ b/src/pip/_vendor/resolvelib/compat/collections_abc.py @@ -1,6 +1,6 @@ -__all__ = ["Sequence"] +__all__ = ["Mapping", "Sequence"] try: - from collections.abc import Sequence + from collections.abc import Mapping, Sequence except ImportError: - from collections import Sequence + from collections import Mapping, Sequence diff --git a/src/pip/_vendor/resolvelib/providers.py b/src/pip/_vendor/resolvelib/providers.py index 8ef700cc0d7..852ee8f483b 100644 --- a/src/pip/_vendor/resolvelib/providers.py +++ b/src/pip/_vendor/resolvelib/providers.py @@ -50,8 +50,18 @@ def get_preference(self, resolution, candidates, information): """ raise NotImplementedError - def find_matches(self, requirements): - """Find all possible candidates that satisfy the given requirements. + def find_matches(self, identifier, requirements, incompatibilities): + """Find all possible candidates that satisfy given constraints. + + :param identifier: An identifier as returned by ``identify()``. This + identifies the dependency matches of which should be returned. + :param requirements: A mapping of requirements that all returned + candidates must satisfy. Each key is an identifier, and the value + an iterator of requirements for that dependency. + :param incompatibilities: A mapping of known incompatibilities of + each dependency. Each key is an identifier, and the value an + iterator of incompatibilities known to the resolver. All + incompatibilities *must* be excluded from the return value. This should try to get candidates based on the requirements' types. For VCS, local, and archive requirements, the one-and-only match is @@ -66,10 +76,6 @@ def find_matches(self, requirements): * An collection of candidates. * An iterable of candidates. This will be consumed immediately into a list of candidates. - - :param requirements: A collection of requirements which all of the - returned candidates must match. All requirements are guaranteed to - have the same identifier. The collection is never empty. """ raise NotImplementedError diff --git a/src/pip/_vendor/resolvelib/providers.pyi b/src/pip/_vendor/resolvelib/providers.pyi index 3c8ff24d4b3..42c19c95f85 100644 --- a/src/pip/_vendor/resolvelib/providers.pyi +++ b/src/pip/_vendor/resolvelib/providers.pyi @@ -3,10 +3,10 @@ from typing import ( Collection, Generic, Iterable, + Iterator, Mapping, Optional, Protocol, - Sequence, Union, ) @@ -31,7 +31,12 @@ class AbstractProvider(Generic[RT, CT, KT]): candidates: IterableView[CT], information: Collection[RequirementInformation[RT, CT]], ) -> Preference: ... - def find_matches(self, requirements: Sequence[RT]) -> Matches: ... + def find_matches( + self, + identifier: KT, + requirements: Mapping[KT, Iterator[RT]], + incompatibilities: Mapping[KT, Iterator[CT]], + ) -> Matches: ... def is_satisfied_by(self, requirement: RT, candidate: CT) -> bool: ... def get_dependencies(self, candidate: CT) -> Iterable[RT]: ... diff --git a/src/pip/_vendor/resolvelib/resolvers.py b/src/pip/_vendor/resolvelib/resolvers.py index 60a30ee4f59..c79ccc4516b 100644 --- a/src/pip/_vendor/resolvelib/resolvers.py +++ b/src/pip/_vendor/resolvelib/resolvers.py @@ -1,7 +1,8 @@ import collections +import operator from .providers import AbstractResolver -from .structs import DirectedGraph, build_iter_view +from .structs import DirectedGraph, IteratorMapping, build_iter_view RequirementInformation = collections.namedtuple( @@ -73,45 +74,12 @@ def __repr__(self): ) return "Criterion({})".format(requirements) - @classmethod - def from_requirement(cls, provider, requirement, parent): - """Build an instance from a requirement.""" - matches = provider.find_matches(requirements=[requirement]) - cands = build_iter_view(matches) - infos = [RequirementInformation(requirement, parent)] - criterion = cls(cands, infos, incompatibilities=[]) - if not cands: - raise RequirementsConflicted(criterion) - return criterion - def iter_requirement(self): return (i.requirement for i in self.information) def iter_parent(self): return (i.parent for i in self.information) - def merged_with(self, provider, requirement, parent): - """Build a new instance from this and a new requirement.""" - infos = list(self.information) - infos.append(RequirementInformation(requirement, parent)) - matches = provider.find_matches([r for r, _ in infos]) - cands = build_iter_view(matches) - criterion = type(self)(cands, infos, list(self.incompatibilities)) - if not cands: - raise RequirementsConflicted(criterion) - return criterion - - def excluded_of(self, candidates): - """Build a new instance from this, but excluding specified candidates. - - Returns the new instance, or None if we still have no valid candidates. - """ - cands = self.candidates.excluding(candidates) - if not cands: - return None - incompats = self.incompatibilities + candidates - return type(self)(cands, list(self.information), incompats) - class ResolutionError(ResolverException): pass @@ -168,13 +136,42 @@ def _push_new_state(self): def _merge_into_criterion(self, requirement, parent): self._r.adding_requirement(requirement=requirement, parent=parent) - name = self._p.identify(requirement_or_candidate=requirement) - if name in self.state.criteria: - crit = self.state.criteria[name] - crit = crit.merged_with(self._p, requirement, parent) + + identifier = self._p.identify(requirement_or_candidate=requirement) + criterion = self.state.criteria.get(identifier) + if criterion: + incompatibilities = list(criterion.incompatibilities) + else: + incompatibilities = [] + + matches = self._p.find_matches( + identifier=identifier, + requirements=IteratorMapping( + self.state.criteria, + operator.methodcaller("iter_requirement"), + {identifier: [requirement]}, + ), + incompatibilities=IteratorMapping( + self.state.criteria, + operator.attrgetter("incompatibilities"), + {identifier: incompatibilities}, + ), + ) + + if criterion: + information = list(criterion.information) + information.append(RequirementInformation(requirement, parent)) else: - crit = Criterion.from_requirement(self._p, requirement, parent) - return name, crit + information = [RequirementInformation(requirement, parent)] + + criterion = Criterion( + candidates=build_iter_view(matches), + information=information, + incompatibilities=incompatibilities, + ) + if not criterion.candidates: + raise RequirementsConflicted(criterion) + return identifier, criterion def _get_criterion_item_preference(self, item): name, criterion = item @@ -268,7 +265,7 @@ def _backtrack(self): broken_state = self._states.pop() name, candidate = broken_state.mapping.popitem() incompatibilities_from_broken = [ - (k, v.incompatibilities) + (k, list(v.incompatibilities)) for k, v in broken_state.criteria.items() ] @@ -287,10 +284,27 @@ def _patch_criteria(): criterion = self.state.criteria[k] except KeyError: continue - criterion = criterion.excluded_of(incompatibilities) - if criterion is None: + matches = self._p.find_matches( + identifier=k, + requirements=IteratorMapping( + self.state.criteria, + operator.methodcaller("iter_requirement"), + ), + incompatibilities=IteratorMapping( + self.state.criteria, + operator.attrgetter("incompatibilities"), + {k: incompatibilities}, + ), + ) + candidates = build_iter_view(matches) + if not candidates: return False - self.state.criteria[k] = criterion + incompatibilities.extend(criterion.incompatibilities) + self.state.criteria[k] = Criterion( + candidates=candidates, + information=list(criterion.information), + incompatibilities=incompatibilities, + ) return True self._push_new_state() diff --git a/src/pip/_vendor/resolvelib/structs.py b/src/pip/_vendor/resolvelib/structs.py index c4542f08a06..72f2e604237 100644 --- a/src/pip/_vendor/resolvelib/structs.py +++ b/src/pip/_vendor/resolvelib/structs.py @@ -1,3 +1,4 @@ +import itertools from .compat import collections_abc @@ -67,6 +68,31 @@ def iter_parents(self, key): return iter(self._backwards[key]) +class IteratorMapping(collections_abc.Mapping): + def __init__(self, mapping, accessor, appends=None): + self._mapping = mapping + self._accessor = accessor + self._appends = appends or {} + + def __contains__(self, key): + return key in self._mapping or key in self._appends + + def __getitem__(self, k): + try: + v = self._mapping[k] + except KeyError: + return iter(self._appends[k]) + return itertools.chain(self._accessor(v), self._appends.get(k, ())) + + def __iter__(self): + more = (k for k in self._appends if k not in self._mapping) + return itertools.chain(self._mapping, more) + + def __len__(self): + more = len(k for k in self._appends if k not in self._mapping) + return len(self._mapping) + more + + class _FactoryIterableView(object): """Wrap an iterator factory returned by `find_matches()`. diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index c5d1b643efd..59d41e50f80 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -14,7 +14,7 @@ requests==2.25.1 chardet==4.0.0 idna==2.10 urllib3==1.26.4 -resolvelib==0.5.5 +resolvelib==0.6.0 setuptools==44.0.0 six==1.15.0 tenacity==6.3.1 From 4e56f1c52d45c20c2452daa822ca11bcbf2d67dd Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sun, 4 Apr 2021 20:26:39 +0800 Subject: [PATCH 3131/3170] Take responsibility to remove incompatibilities --- .../resolution/resolvelib/factory.py | 62 +++++++++++++------ .../resolution/resolvelib/found_candidates.py | 18 +++--- .../resolution/resolvelib/provider.py | 9 +-- .../resolution_resolvelib/test_requirement.py | 12 +++- 4 files changed, 66 insertions(+), 35 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index dd747198fcf..6cada5be038 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -7,6 +7,7 @@ Iterable, Iterator, List, + Mapping, Optional, Sequence, Set, @@ -104,6 +105,9 @@ def __init__( self._installed_candidate_cache = ( {} ) # type: Dict[str, AlreadyInstalledCandidate] + self._extras_candidate_cache = ( + {} + ) # type: Dict[Tuple[int, FrozenSet[str]], ExtrasCandidate] if not ignore_installed: self._installed_dists = { @@ -118,6 +122,16 @@ def force_reinstall(self): # type: () -> bool return self._force_reinstall + def _make_extras_candidate(self, base, extras): + # type: (BaseCandidate, FrozenSet[str]) -> ExtrasCandidate + cache_key = (id(base), extras) + try: + candidate = self._extras_candidate_cache[cache_key] + except KeyError: + candidate = ExtrasCandidate(base, extras) + self._extras_candidate_cache[cache_key] = candidate + return candidate + def _make_candidate_from_dist( self, dist, # type: Distribution @@ -130,9 +144,9 @@ def _make_candidate_from_dist( except KeyError: base = AlreadyInstalledCandidate(dist, template, factory=self) self._installed_candidate_cache[dist.key] = base - if extras: - return ExtrasCandidate(base, extras) - return base + if not extras: + return base + return self._make_extras_candidate(base, extras) def _make_candidate_from_link( self, @@ -182,18 +196,18 @@ def _make_candidate_from_link( return None base = self._link_candidate_cache[link] - if extras: - return ExtrasCandidate(base, extras) - return base + if not extras: + return base + return self._make_extras_candidate(base, extras) def _iter_found_candidates( self, - ireqs, # type: Sequence[InstallRequirement] - specifier, # type: SpecifierSet - hashes, # type: Hashes - prefers_installed, # type: bool - ): - # type: (...) -> Iterable[Candidate] + ireqs: Sequence[InstallRequirement], + specifier: SpecifierSet, + hashes: Hashes, + prefers_installed: bool, + incompatible_ids: Set[int], + ) -> Iterable[Candidate]: if not ireqs: return () @@ -257,20 +271,27 @@ def iter_index_candidate_infos(): iter_index_candidate_infos, installed_candidate, prefers_installed, + incompatible_ids, ) def find_candidates( self, - requirements, # type: Sequence[Requirement] - constraint, # type: Constraint - prefers_installed, # type: bool - ): - # type: (...) -> Iterable[Candidate] + identifier: str, + requirements: Mapping[str, Iterator[Requirement]], + incompatibilities: Mapping[str, Iterator[Candidate]], + constraint: Constraint, + prefers_installed: bool, + ) -> Iterable[Candidate]: + + # Since we cache all the candidates, incompatibility identification + # can be made quicker by comparing only the id() values. + incompat_ids = {id(c) for c in incompatibilities.get(identifier, ())} + explicit_candidates = set() # type: Set[Candidate] ireqs = [] # type: List[InstallRequirement] - for req in requirements: + for req in requirements[identifier]: cand, ireq = req.get_candidate_lookup() - if cand is not None: + if cand is not None and id(cand) not in incompat_ids: explicit_candidates.add(cand) if ireq is not None: ireqs.append(ireq) @@ -283,13 +304,14 @@ def find_candidates( constraint.specifier, constraint.hashes, prefers_installed, + incompat_ids, ) return ( c for c in explicit_candidates if constraint.is_satisfied_by(c) - and all(req.is_satisfied_by(c) for req in requirements) + and all(req.is_satisfied_by(c) for req in requirements[identifier]) ) def make_requirement_from_install_req(self, ireq, requested_extras): diff --git a/src/pip/_internal/resolution/resolvelib/found_candidates.py b/src/pip/_internal/resolution/resolvelib/found_candidates.py index e8b72e66000..21fa08ec938 100644 --- a/src/pip/_internal/resolution/resolvelib/found_candidates.py +++ b/src/pip/_internal/resolution/resolvelib/found_candidates.py @@ -100,13 +100,15 @@ class FoundCandidates(collections_abc.Sequence): def __init__( self, - get_infos, # type: Callable[[], Iterator[IndexCandidateInfo]] - installed, # type: Optional[Candidate] - prefers_installed, # type: bool + get_infos: Callable[[], Iterator[IndexCandidateInfo]], + installed: Optional[Candidate], + prefers_installed: bool, + incompatible_ids: Set[int], ): self._get_infos = get_infos self._installed = installed self._prefers_installed = prefers_installed + self._incompatible_ids = incompatible_ids def __getitem__(self, index): # type: (int) -> Candidate @@ -119,10 +121,12 @@ def __iter__(self): # type: () -> Iterator[Candidate] infos = self._get_infos() if not self._installed: - return _iter_built(infos) - if self._prefers_installed: - return _iter_built_with_prepended(self._installed, infos) - return _iter_built_with_inserted(self._installed, infos) + iterator = _iter_built(infos) + elif self._prefers_installed: + iterator = _iter_built_with_prepended(self._installed, infos) + else: + iterator = _iter_built_with_inserted(self._installed, infos) + return (c for c in iterator if id(c) not in self._incompatible_ids) def __len__(self): # type: () -> int diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index 89b68489249..32597f7e093 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -149,11 +149,6 @@ def find_matches( requirements: Mapping[str, Iterator[Requirement]], incompatibilities: Mapping[str, Iterator[Candidate]], ) -> Iterable[Candidate]: - try: - current_requirements = requirements[identifier] - except KeyError: - return [] - def _eligible_for_upgrade(name): # type: (str) -> bool """Are upgrades allowed for this project? @@ -173,9 +168,11 @@ def _eligible_for_upgrade(name): return False return self._factory.find_candidates( - list(current_requirements), + identifier=identifier, + requirements=requirements, constraint=self._constraints.get(identifier, Constraint.empty()), prefers_installed=(not _eligible_for_upgrade(identifier)), + incompatibilities=incompatibilities, ) def is_satisfied_by(self, requirement, candidate): diff --git a/tests/unit/resolution_resolvelib/test_requirement.py b/tests/unit/resolution_resolvelib/test_requirement.py index 6149fd1aece..1f7b0c53d17 100644 --- a/tests/unit/resolution_resolvelib/test_requirement.py +++ b/tests/unit/resolution_resolvelib/test_requirement.py @@ -59,7 +59,11 @@ def test_new_resolver_correct_number_of_matches(test_cases, factory): for spec, _, match_count in test_cases: req = factory.make_requirement_from_spec(spec, comes_from=None) matches = factory.find_candidates( - [req], Constraint.empty(), prefers_installed=False, + req.name, + {req.name: [req]}, + {}, + Constraint.empty(), + prefers_installed=False, ) assert sum(1 for _ in matches) == match_count @@ -70,7 +74,11 @@ def test_new_resolver_candidates_match_requirement(test_cases, factory): for spec, _, _ in test_cases: req = factory.make_requirement_from_spec(spec, comes_from=None) candidates = factory.find_candidates( - [req], Constraint.empty(), prefers_installed=False, + req.name, + {req.name: [req]}, + {}, + Constraint.empty(), + prefers_installed=False, ) for c in candidates: assert isinstance(c, Candidate) From 69dee6286d06b779dc51087a119448b1a94aba76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sun, 4 Apr 2021 14:37:23 +0200 Subject: [PATCH 3132/3170] PEP 517 + --build-option is now a warning We warn instead of erroring out when --build-option is present when doing a PEP 517 build. There is no strong reason to error out, and this will avoid backward compatibility issues when we support build options in requirement files and installation. --- news/9774.feature.rst | 2 ++ src/pip/_internal/operations/build/wheel.py | 8 +------- src/pip/_internal/wheel_builder.py | 5 ++++- tests/functional/test_pep517.py | 5 ++--- 4 files changed, 9 insertions(+), 11 deletions(-) create mode 100644 news/9774.feature.rst diff --git a/news/9774.feature.rst b/news/9774.feature.rst new file mode 100644 index 00000000000..298362f8be2 --- /dev/null +++ b/news/9774.feature.rst @@ -0,0 +1,2 @@ +Warn instead of erroring out when doing a PEP 517 build in presence of +``--build-option``. diff --git a/src/pip/_internal/operations/build/wheel.py b/src/pip/_internal/operations/build/wheel.py index 83fac3b3187..903bd7a0567 100644 --- a/src/pip/_internal/operations/build/wheel.py +++ b/src/pip/_internal/operations/build/wheel.py @@ -1,6 +1,6 @@ import logging import os -from typing import List, Optional +from typing import Optional from pip._vendor.pep517.wrappers import Pep517HookCaller @@ -13,7 +13,6 @@ def build_wheel_pep517( name, # type: str backend, # type: Pep517HookCaller metadata_directory, # type: str - build_options, # type: List[str] tempd, # type: str ): # type: (...) -> Optional[str] @@ -22,11 +21,6 @@ def build_wheel_pep517( Returns path to wheel if successfully built. Otherwise, returns None. """ assert metadata_directory is not None - if build_options: - # PEP 517 does not support --build-options - logger.error('Cannot build wheel for %s using PEP 517 when ' - '--build-option is present', name) - return None try: logger.debug('Destination directory: %s', tempd) diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index 4b2f6d5d4df..c2cbf1eebb7 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -244,11 +244,14 @@ def _build_one_inside_env( if req.use_pep517: assert req.metadata_directory assert req.pep517_backend + if build_options: + logger.warning( + 'Ignoring --build-option when building %s using PEP 517', req.name + ) wheel_path = build_wheel_pep517( name=req.name, backend=req.pep517_backend, metadata_directory=req.metadata_directory, - build_options=build_options, tempd=temp_dir.path, ) else: diff --git a/tests/functional/test_pep517.py b/tests/functional/test_pep517.py index a747b8a0756..ae747d9fda1 100644 --- a/tests/functional/test_pep517.py +++ b/tests/functional/test_pep517.py @@ -263,7 +263,6 @@ def test_pep517_and_build_options(script, tmpdir, data, common_wheels): '--build-option', 'foo', '-f', common_wheels, project_dir, - expect_error=True ) - assert 'Cannot build wheel' in result.stderr - assert 'when --build-option is present' in result.stderr + assert 'Ignoring --build-option when building' in result.stderr + assert 'using PEP 517' in result.stderr From 7c6ee48b041972e067bf2f1211e25959f8b95982 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sun, 4 Apr 2021 14:39:24 +0200 Subject: [PATCH 3133/3170] PEP 517 build in presence of --global-option emits a warning So these are not ignored silently. --- news/9774.feature.rst | 3 ++- src/pip/_internal/wheel_builder.py | 4 ++++ tests/functional/test_pep517.py | 14 ++++++++++++++ 3 files changed, 20 insertions(+), 1 deletion(-) diff --git a/news/9774.feature.rst b/news/9774.feature.rst index 298362f8be2..8baac5e967f 100644 --- a/news/9774.feature.rst +++ b/news/9774.feature.rst @@ -1,2 +1,3 @@ Warn instead of erroring out when doing a PEP 517 build in presence of -``--build-option``. +``--build-option``. Warn when doing a PEP 517 build in presence of +``--global-option``. diff --git a/src/pip/_internal/wheel_builder.py b/src/pip/_internal/wheel_builder.py index c2cbf1eebb7..92f172bca16 100644 --- a/src/pip/_internal/wheel_builder.py +++ b/src/pip/_internal/wheel_builder.py @@ -244,6 +244,10 @@ def _build_one_inside_env( if req.use_pep517: assert req.metadata_directory assert req.pep517_backend + if global_options: + logger.warning( + 'Ignoring --global-option when building %s using PEP 517', req.name + ) if build_options: logger.warning( 'Ignoring --build-option when building %s using PEP 517', req.name diff --git a/tests/functional/test_pep517.py b/tests/functional/test_pep517.py index ae747d9fda1..4458a7ad56e 100644 --- a/tests/functional/test_pep517.py +++ b/tests/functional/test_pep517.py @@ -266,3 +266,17 @@ def test_pep517_and_build_options(script, tmpdir, data, common_wheels): ) assert 'Ignoring --build-option when building' in result.stderr assert 'using PEP 517' in result.stderr + + +@pytest.mark.network +def test_pep517_and_global_options(script, tmpdir, data, common_wheels): + """Backend generated requirements are installed in the build env""" + project_dir, name = make_pyproject_with_setup(tmpdir) + result = script.pip( + 'wheel', '--wheel-dir', tmpdir, + '--global-option', 'foo', + '-f', common_wheels, + project_dir, + ) + assert 'Ignoring --global-option when building' in result.stderr + assert 'using PEP 517' in result.stderr From d53ddb938031b67d0030f9d6cff75083090ef2b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sun, 3 Jan 2021 13:26:28 +0100 Subject: [PATCH 3134/3170] Refactor --build-option declaration --- src/pip/_internal/cli/cmdoptions.py | 9 +++++++++ src/pip/_internal/commands/wheel.py | 8 +------- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index 24dc9d14107..8502da97b29 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -824,6 +824,15 @@ def _handle_no_use_pep517(option, opt, value, parser): "directory path, be sure to use absolute path.", ) # type: Callable[..., Option] +build_options = partial( + Option, + "--build-option", + dest="build_options", + metavar="options", + action="append", + help="Extra arguments to be supplied to 'setup.py bdist_wheel'.", +) # type: Callable[..., Option] + global_options = partial( Option, "--global-option", diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index 842988ba570..507913fe5fb 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -54,13 +54,6 @@ def add_options(self): self.cmd_opts.add_option(cmdoptions.no_binary()) self.cmd_opts.add_option(cmdoptions.only_binary()) self.cmd_opts.add_option(cmdoptions.prefer_binary()) - self.cmd_opts.add_option( - '--build-option', - dest='build_options', - metavar='options', - action='append', - help="Extra arguments to be supplied to 'setup.py bdist_wheel'.", - ) self.cmd_opts.add_option(cmdoptions.no_build_isolation()) self.cmd_opts.add_option(cmdoptions.use_pep517()) self.cmd_opts.add_option(cmdoptions.no_use_pep517()) @@ -81,6 +74,7 @@ def add_options(self): help="Don't verify if built wheel is valid.", ) + self.cmd_opts.add_option(cmdoptions.build_options()) self.cmd_opts.add_option( '--global-option', dest='global_options', From 5b9a628873c338f56c5ecef2d55b38ccc9967508 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sun, 4 Apr 2021 17:07:31 +0200 Subject: [PATCH 3135/3170] Refactor --global-option declaration in wheel command --- src/pip/_internal/cli/cmdoptions.py | 2 +- src/pip/_internal/commands/wheel.py | 8 +------- 2 files changed, 2 insertions(+), 8 deletions(-) diff --git a/src/pip/_internal/cli/cmdoptions.py b/src/pip/_internal/cli/cmdoptions.py index 8502da97b29..f71c0b02011 100644 --- a/src/pip/_internal/cli/cmdoptions.py +++ b/src/pip/_internal/cli/cmdoptions.py @@ -840,7 +840,7 @@ def _handle_no_use_pep517(option, opt, value, parser): action="append", metavar="options", help="Extra global options to be supplied to the setup.py " - "call before the install command.", + "call before the install or bdist_wheel command.", ) # type: Callable[..., Option] no_clean = partial( diff --git a/src/pip/_internal/commands/wheel.py b/src/pip/_internal/commands/wheel.py index 507913fe5fb..ff47dbac51c 100644 --- a/src/pip/_internal/commands/wheel.py +++ b/src/pip/_internal/commands/wheel.py @@ -75,13 +75,7 @@ def add_options(self): ) self.cmd_opts.add_option(cmdoptions.build_options()) - self.cmd_opts.add_option( - '--global-option', - dest='global_options', - action='append', - metavar='options', - help="Extra global options to be supplied to the setup.py " - "call before the 'bdist_wheel' command.") + self.cmd_opts.add_option(cmdoptions.global_options()) self.cmd_opts.add_option( '--pre', From cc3adad1b24b1573781e2c7b4c0f0ed0f19aded7 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Mon, 5 Apr 2021 13:15:36 +0100 Subject: [PATCH 3136/3170] Don't use virtualenv internals in tests --- tests/lib/venv.py | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/tests/lib/venv.py b/tests/lib/venv.py index bbf6eaa38be..e297d6462d9 100644 --- a/tests/lib/venv.py +++ b/tests/lib/venv.py @@ -1,5 +1,6 @@ import compileall import shutil +import subprocess import sys import textwrap import venv as _venv @@ -55,11 +56,16 @@ def _create(self, clear=False): else: # Create a new virtual environment. if self._venv_type == "virtualenv": - _virtualenv.create_environment( - self.location, - no_pip=True, - no_wheel=True, - no_setuptools=True, + subprocess.check_call( + [ + sys.executable, + "-m", + "virtualenv", + "--no-pip", + "--no-wheel", + "--no-setuptools", + str(self.location), + ] ) self._fix_virtualenv_site_module() elif self._venv_type == "venv": From ef35e73a711f980972d1ed5f7514bd816c1ccca5 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Mon, 5 Apr 2021 15:31:08 +0100 Subject: [PATCH 3137/3170] Make pip work with warnings converted to errors --- src/pip/__main__.py | 11 +++++++++-- tests/functional/test_warning.py | 5 +++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/src/pip/__main__.py b/src/pip/__main__.py index 063fd1aacfd..fe34a7b7772 100644 --- a/src/pip/__main__.py +++ b/src/pip/__main__.py @@ -1,5 +1,6 @@ import os import sys +import warnings # Remove '' and current working directory from the first entry # of sys.path, if present to avoid using current directory @@ -18,7 +19,13 @@ path = os.path.dirname(os.path.dirname(__file__)) sys.path.insert(0, path) -from pip._internal.cli.main import main as _main - if __name__ == "__main__": + # Work around the error reported in #9540, pending a proper fix. + # Note: It is essential the warning filter is set *before* importing + # pip, as the deprecation happens at import time, not runtime. + warnings.filterwarnings( + "ignore", category=DeprecationWarning, module=".*packaging\\.version" + ) + from pip._internal.cli.main import main as _main + sys.exit(_main()) diff --git a/tests/functional/test_warning.py b/tests/functional/test_warning.py index 3558704bcef..5c5b1a201ce 100644 --- a/tests/functional/test_warning.py +++ b/tests/functional/test_warning.py @@ -42,3 +42,8 @@ def test_version_warning_is_not_shown_if_python_version_is_not_2(script): def test_flag_does_nothing_if_python_version_is_not_2(script): script.pip("list", "--no-python-version-warning") + + +def test_pip_works_with_warnings_as_errors(script): + script.environ['PYTHONWARNINGS'] = 'error' + script.pip("--version") From 8d6870aba5241468897fed5cbb109e16ac319b27 Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Mon, 5 Apr 2021 15:35:29 +0100 Subject: [PATCH 3138/3170] News file --- news/9779.bugfix.rst | 1 + 1 file changed, 1 insertion(+) create mode 100644 news/9779.bugfix.rst diff --git a/news/9779.bugfix.rst b/news/9779.bugfix.rst new file mode 100644 index 00000000000..2145b641e27 --- /dev/null +++ b/news/9779.bugfix.rst @@ -0,0 +1 @@ +Fix pip to work with warnings converted to errors. From 55d2d0dcf76ab0c885a2064b6391f36d488824bf Mon Sep 17 00:00:00 2001 From: Paul Moore <p.f.moore@gmail.com> Date: Mon, 5 Apr 2021 15:39:01 +0100 Subject: [PATCH 3139/3170] Fix some tests that are checking logs incorrectly --- tests/unit/test_resolution_legacy_resolver.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/unit/test_resolution_legacy_resolver.py b/tests/unit/test_resolution_legacy_resolver.py index 9fed3765058..236a4c624e7 100644 --- a/tests/unit/test_resolution_legacy_resolver.py +++ b/tests/unit/test_resolution_legacy_resolver.py @@ -201,6 +201,10 @@ def test_sort_best_candidate__has_non_yanked(self, caplog, monkeypatch): """ Test unyanked candidate preferred over yanked. """ + # Ignore spurious DEBUG level messages + # TODO: Probably better to work out why they are occurring, but IMO the + # tests are at fault here for being to dependent on exact output. + caplog.set_level(logging.WARNING) candidates = [ make_mock_candidate('1.0'), make_mock_candidate('2.0', yanked_reason='bad metadata #2'), @@ -217,6 +221,10 @@ def test_sort_best_candidate__all_yanked(self, caplog, monkeypatch): """ Test all candidates yanked. """ + # Ignore spurious DEBUG level messages + # TODO: Probably better to work out why they are occurring, but IMO the + # tests are at fault here for being to dependent on exact output. + caplog.set_level(logging.WARNING) candidates = [ make_mock_candidate('1.0', yanked_reason='bad metadata #1'), # Put the best candidate in the middle, to test sorting. @@ -253,6 +261,10 @@ def test_sort_best_candidate__yanked_reason( """ Test the log message with various reason strings. """ + # Ignore spurious DEBUG level messages + # TODO: Probably better to work out why they are occurring, but IMO the + # tests are at fault here for being to dependent on exact output. + caplog.set_level(logging.WARNING) candidates = [ make_mock_candidate('1.0', yanked_reason=yanked_reason), ] From d3089e188a03d5f62a8959911692c5ad7099681e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Mon, 5 Apr 2021 18:39:47 +0200 Subject: [PATCH 3140/3170] Postpone --build-dir removal --- src/pip/_internal/cli/base_command.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/cli/base_command.py b/src/pip/_internal/cli/base_command.py index 2dc9845c2ab..b59420dda22 100644 --- a/src/pip/_internal/cli/base_command.py +++ b/src/pip/_internal/cli/base_command.py @@ -165,7 +165,7 @@ def _main(self, args): "use the TMPDIR/TEMP/TMP environment variable, " "possibly combined with --no-clean" ), - gone_in="21.1", + gone_in="21.3", issue=8333, ) From 80039e937667a0257ae417b67ef0307164997f19 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Wed, 7 Apr 2021 20:18:49 +0800 Subject: [PATCH 3141/3170] Pin towncrier under 19.9.0 sphinxcontrib-towncrier uses towncrier internals and has not been updated to work with later versions. --- tools/requirements/docs.txt | 2 ++ 1 file changed, 2 insertions(+) diff --git a/tools/requirements/docs.txt b/tools/requirements/docs.txt index aed18b5084b..199edcfd6b3 100644 --- a/tools/requirements/docs.txt +++ b/tools/requirements/docs.txt @@ -1,4 +1,6 @@ sphinx == 3.2.1 +# FIXME: Remove towncrier constraint after upgrading sphinxcontrib-towncrier. +towncrier < 19.9.0 furo myst_parser sphinx-copybutton From ec59cc09683f8150a6d1d9148b721bda50afca93 Mon Sep 17 00:00:00 2001 From: Hugo van Kemenade <hugovk@users.noreply.github.com> Date: Wed, 7 Apr 2021 17:48:55 +0300 Subject: [PATCH 3142/3170] Run CI on main not master --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 09724e8f968..753f636c7ec 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,7 +2,7 @@ name: CI on: push: - branches: [master] + branches: [main] tags: # Tags for all potential release numbers till 2030. - "2[0-9].[0-3]" # 20.0 -> 29.3 From d8787277728051d2ce560bb4c5ea0fc2d6891ae2 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Fri, 1 Jan 2021 21:57:45 +0800 Subject: [PATCH 3143/3170] Don't suggest --user in virtual environment --- news/9409.bugfix.rst | 2 ++ src/pip/_internal/commands/install.py | 7 +++++-- tests/unit/test_command_install.py | 4 +++- 3 files changed, 10 insertions(+), 3 deletions(-) create mode 100644 news/9409.bugfix.rst diff --git a/news/9409.bugfix.rst b/news/9409.bugfix.rst new file mode 100644 index 00000000000..10cd36b1960 --- /dev/null +++ b/news/9409.bugfix.rst @@ -0,0 +1,2 @@ +``--user`` is no longer suggested incorrectly when pip fails with a permission +error in a virtual environment. diff --git a/src/pip/_internal/commands/install.py b/src/pip/_internal/commands/install.py index dc637d87635..6932f5a6d8b 100644 --- a/src/pip/_internal/commands/install.py +++ b/src/pip/_internal/commands/install.py @@ -35,7 +35,10 @@ write_output, ) from pip._internal.utils.temp_dir import TempDirectory -from pip._internal.utils.virtualenv import virtualenv_no_global +from pip._internal.utils.virtualenv import ( + running_under_virtualenv, + virtualenv_no_global, +) from pip._internal.wheel_builder import ( BinaryAllowedPredicate, build, @@ -725,7 +728,7 @@ def create_os_error_message(error, show_traceback, using_user_site): user_option_part = "Consider using the `--user` option" permissions_part = "Check the permissions" - if not using_user_site: + if not running_under_virtualenv() and not using_user_site: parts.extend([ user_option_part, " or ", permissions_part.lower(), diff --git a/tests/unit/test_command_install.py b/tests/unit/test_command_install.py index 5c623e679b7..dc8365efc78 100644 --- a/tests/unit/test_command_install.py +++ b/tests/unit/test_command_install.py @@ -4,6 +4,7 @@ import pytest from pip._vendor.packaging.requirements import Requirement +from pip._internal.commands import install from pip._internal.commands.install import ( create_os_error_message, decide_user_install, @@ -109,7 +110,8 @@ def test_rejection_for_location_requirement_options(): ' permissions.\n'), ]) def test_create_os_error_message( - error, show_traceback, using_user_site, expected + monkeypatch, error, show_traceback, using_user_site, expected ): + monkeypatch.setattr(install, "running_under_virtualenv", lambda: False) msg = create_os_error_message(error, show_traceback, using_user_site) assert msg == expected From 9aa6cbda47ee416089f037e264b6171f520a119d Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <3275593+pradyunsg@users.noreply.github.com> Date: Sun, 11 Apr 2021 20:07:49 +0100 Subject: [PATCH 3144/3170] Bump ReadTheDocs Python version to 3.8 Required for compatibility with the latest sphinx-inline-tabs release. --- .readthedocs.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.readthedocs.yml b/.readthedocs.yml index c123a1939fb..bd4cb9cd3cd 100644 --- a/.readthedocs.yml +++ b/.readthedocs.yml @@ -5,6 +5,6 @@ sphinx: configuration: docs/html/conf.py python: - version: 3.7 + version: 3.8 install: - requirements: tools/requirements/docs.txt From 4517306a6dc231c2582a81e2d83f119730c58acd Mon Sep 17 00:00:00 2001 From: Dominic Davis-Foster <dominic@davis-foster.co.uk> Date: Sun, 11 Apr 2021 21:49:25 +0100 Subject: [PATCH 3145/3170] Remove third backtick from inline code block. --- docs/html/user_guide.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 10e372b19fd..9cd280e9b7b 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -1368,7 +1368,7 @@ When pip finds that an assumption is incorrect, it has to try another approach (backtrack), which means discarding some of the work that has already been done, and going back to choose another path. -For example; The user requests ``pip install tea``. ```tea`` has dependencies of +For example; The user requests ``pip install tea``. ``tea`` has dependencies of ``cup``, ``hot-water``, ``spoon`` amongst others. pip starts by installing a version of ``cup``. If it finds out it isn’t From a912c5530dc362f10bec559da410b698d00c89f5 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 23 Mar 2021 22:34:20 +0800 Subject: [PATCH 3146/3170] Rewrite collect_links This introduces a collect_sources() method to do the same thing, but instead of flattening links eagerly, return each repository entry separately (and return a None for invalid repository options), so subsequent code can better distinguish which link comes from which repository. --- .../architecture/package-finding.rst | 4 +- src/pip/_internal/index/collector.py | 205 ++++------------ src/pip/_internal/index/package_finder.py | 50 ++-- src/pip/_internal/index/sources.py | 224 ++++++++++++++++++ tests/unit/test_collector.py | 124 +++++++--- 5 files changed, 389 insertions(+), 218 deletions(-) create mode 100644 src/pip/_internal/index/sources.py diff --git a/docs/html/development/architecture/package-finding.rst b/docs/html/development/architecture/package-finding.rst index 18545275de2..0b64d420d93 100644 --- a/docs/html/development/architecture/package-finding.rst +++ b/docs/html/development/architecture/package-finding.rst @@ -101,7 +101,7 @@ One of ``PackageFinder``'s main top-level methods is 1. Calls its ``find_all_candidates()`` method, which gathers all possible package links by reading and parsing the index URL's and locations provided by the user (the :ref:`LinkCollector - <link-collector-class>` class's ``collect_links()`` method), constructs a + <link-collector-class>` class's ``collect_sources()`` method), constructs a :ref:`LinkEvaluator <link-evaluator-class>` object to filter out some of those links, and then returns a list of ``InstallationCandidates`` (aka candidates for install). This corresponds to steps 1-3 of the @@ -131,7 +131,7 @@ responsible for collecting the raw list of "links" to package files The ``LinkCollector`` class takes into account the user's :ref:`--find-links <install_--find-links>`, :ref:`--extra-index-url <install_--extra-index-url>`, and related options when deciding which locations to collect links from. The -class's main method is the ``collect_links()`` method. The :ref:`PackageFinder +class's main method is the ``collect_sources()`` method. The :ref:`PackageFinder <package-finder-class>` class invokes this method as the first step of its ``find_all_candidates()`` method. diff --git a/src/pip/_internal/index/collector.py b/src/pip/_internal/index/collector.py index f6b0536d0d6..0721e3683f9 100644 --- a/src/pip/_internal/index/collector.py +++ b/src/pip/_internal/index/collector.py @@ -1,28 +1,27 @@ """ -The main purpose of this module is to expose LinkCollector.collect_links(). +The main purpose of this module is to expose LinkCollector.collect_sources(). """ import cgi +import collections import functools import html import itertools import logging -import mimetypes import os import re import urllib.parse import urllib.request import xml.etree.ElementTree -from collections import OrderedDict from optparse import Values from typing import ( Callable, Iterable, List, MutableMapping, + NamedTuple, Optional, Sequence, - Tuple, Union, ) @@ -37,8 +36,9 @@ from pip._internal.network.utils import raise_for_status from pip._internal.utils.filetypes import is_archive_file from pip._internal.utils.misc import pairwise, redact_auth_from_url -from pip._internal.utils.urls import path_to_url, url_to_path -from pip._internal.vcs import is_url, vcs +from pip._internal.vcs import vcs + +from .sources import CandidatesFromPage, LinkSource, build_source logger = logging.getLogger(__name__) @@ -449,107 +449,9 @@ def _get_html_page(link, session=None): return None -def _remove_duplicate_links(links): - # type: (Iterable[Link]) -> List[Link] - """ - Return a list of links, with duplicates removed and ordering preserved. - """ - # We preserve the ordering when removing duplicates because we can. - return list(OrderedDict.fromkeys(links)) - - -def group_locations(locations, expand_dir=False): - # type: (Sequence[str], bool) -> Tuple[List[str], List[str]] - """ - Divide a list of locations into two groups: "files" (archives) and "urls." - - :return: A pair of lists (files, urls). - """ - files = [] - urls = [] - - # puts the url for the given file path into the appropriate list - def sort_path(path): - # type: (str) -> None - url = path_to_url(path) - if mimetypes.guess_type(url, strict=False)[0] == 'text/html': - urls.append(url) - else: - files.append(url) - - for url in locations: - - is_local_path = os.path.exists(url) - is_file_url = url.startswith('file:') - - if is_local_path or is_file_url: - if is_local_path: - path = url - else: - path = url_to_path(url) - if os.path.isdir(path): - if expand_dir: - path = os.path.realpath(path) - for item in os.listdir(path): - sort_path(os.path.join(path, item)) - elif is_file_url: - urls.append(url) - else: - logger.warning( - "Path '%s' is ignored: it is a directory.", path, - ) - elif os.path.isfile(path): - sort_path(path) - else: - logger.warning( - "Url '%s' is ignored: it is neither a file " - "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, - ) - - return files, urls - - -class CollectedLinks: - - """ - Encapsulates the return value of a call to LinkCollector.collect_links(). - - The return value includes both URLs to project pages containing package - links, as well as individual package Link objects collected from other - sources. - - This info is stored separately as: - - (1) links from the configured file locations, - (2) links from the configured find_links, and - (3) urls to HTML project pages, as described by the PEP 503 simple - repository API. - """ - - def __init__( - self, - files, # type: List[Link] - find_links, # type: List[Link] - project_urls, # type: List[Link] - ): - # type: (...) -> None - """ - :param files: Links from file locations. - :param find_links: Links from find_links. - :param project_urls: URLs to HTML project pages, as described by - the PEP 503 simple repository API. - """ - self.files = files - self.find_links = find_links - self.project_urls = project_urls +class CollectedSources(NamedTuple): + find_links: Sequence[Optional[LinkSource]] + index_urls: Sequence[Optional[LinkSource]] class LinkCollector: @@ -558,7 +460,7 @@ class LinkCollector: Responsible for collecting Link objects from all configured locations, making network requests as needed. - The class's main method is its collect_links() method. + The class's main method is its collect_sources() method. """ def __init__( @@ -609,51 +511,46 @@ def fetch_page(self, location): """ return _get_html_page(location, session=self.session) - def collect_links(self, project_name): - # type: (str) -> CollectedLinks - """Find all available links for the given project name. - - :return: All the Link objects (unfiltered), as a CollectedLinks object. - """ - search_scope = self.search_scope - index_locations = search_scope.get_index_urls_locations(project_name) - index_file_loc, index_url_loc = group_locations(index_locations) - fl_file_loc, fl_url_loc = group_locations( - self.find_links, expand_dir=True, - ) - - file_links = [ - Link(url) for url in itertools.chain(index_file_loc, fl_file_loc) - ] - - # We trust every directly linked archive in find_links - find_link_links = [Link(url, '-f') for url in self.find_links] - - # We trust every url that the user has given us whether it was given - # via --index-url or --find-links. - # We want to filter out anything that does not have a secure origin. - url_locations = [ - link for link in itertools.chain( - # Mark PyPI indices as "cache_link_parsing == False" -- this - # will avoid caching the result of parsing the page for links. - (Link(url, cache_link_parsing=False) for url in index_url_loc), - (Link(url) for url in fl_url_loc), + def collect_sources( + self, + project_name: str, + candidates_from_page: CandidatesFromPage, + ) -> CollectedSources: + # The OrderedDict calls deduplicate sources by URL. + index_url_sources = collections.OrderedDict( + build_source( + loc, + candidates_from_page=candidates_from_page, + page_validator=self.session.is_secure_origin, + expand_dir=False, + cache_link_parsing=False, + ) + for loc in self.search_scope.get_index_urls_locations(project_name) + ).values() + find_links_sources = collections.OrderedDict( + build_source( + loc, + candidates_from_page=candidates_from_page, + page_validator=self.session.is_secure_origin, + expand_dir=True, + cache_link_parsing=True, ) - if self.session.is_secure_origin(link) - ] - - url_locations = _remove_duplicate_links(url_locations) - lines = [ - '{} location(s) to search for versions of {}:'.format( - len(url_locations), project_name, - ), - ] - for link in url_locations: - lines.append(f'* {link}') - logger.debug('\n'.join(lines)) - - return CollectedLinks( - files=file_links, - find_links=find_link_links, - project_urls=url_locations, + for loc in self.find_links + ).values() + + if logger.isEnabledFor(logging.DEBUG): + lines = [ + f"* {s.link}" + for s in itertools.chain(find_links_sources, index_url_sources) + if s is not None and s.link is not None + ] + lines = [ + f"{len(lines)} location(s) to search " + f"for versions of {project_name}:" + ] + lines + logger.debug("\n".join(lines)) + + return CollectedSources( + find_links=list(find_links_sources), + index_urls=list(index_url_sources), ) diff --git a/src/pip/_internal/index/package_finder.py b/src/pip/_internal/index/package_finder.py index a6423cce186..7f2e04e7c37 100644 --- a/src/pip/_internal/index/package_finder.py +++ b/src/pip/_internal/index/package_finder.py @@ -4,6 +4,7 @@ # mypy: strict-optional=False import functools +import itertools import logging import re from typing import FrozenSet, Iterable, List, Optional, Set, Tuple, Union @@ -804,38 +805,41 @@ def find_all_candidates(self, project_name): See LinkEvaluator.evaluate_link() for details on which files are accepted. """ - collected_links = self._link_collector.collect_links(project_name) - link_evaluator = self.make_link_evaluator(project_name) - find_links_versions = self.evaluate_links( - link_evaluator, - links=collected_links.find_links, + collected_sources = self._link_collector.collect_sources( + project_name=project_name, + candidates_from_page=functools.partial( + self.process_project_url, + link_evaluator=link_evaluator, + ), ) - page_versions = [] - for project_url in collected_links.project_urls: - package_links = self.process_project_url( - project_url, link_evaluator=link_evaluator, - ) - page_versions.extend(package_links) + page_candidates_it = itertools.chain.from_iterable( + source.page_candidates() + for sources in collected_sources + for source in sources + if source is not None + ) + page_candidates = list(page_candidates_it) - file_versions = self.evaluate_links( + file_links_it = itertools.chain.from_iterable( + source.file_links() + for sources in collected_sources + for source in sources + if source is not None + ) + file_candidates = self.evaluate_links( link_evaluator, - links=collected_links.files, + sorted(file_links_it, reverse=True), ) - if file_versions: - file_versions.sort(reverse=True) - logger.debug( - 'Local files found: %s', - ', '.join([ - url_to_path(candidate.link.url) - for candidate in file_versions - ]) - ) + + if logger.isEnabledFor(logging.DEBUG) and file_candidates: + paths = [url_to_path(c.link.url) for c in file_candidates] + logger.debug("Local files found: %s", ", ".join(paths)) # This is an intentional priority ordering - return file_versions + find_links_versions + page_versions + return file_candidates + page_candidates def make_candidate_evaluator( self, diff --git a/src/pip/_internal/index/sources.py b/src/pip/_internal/index/sources.py new file mode 100644 index 00000000000..eec3f12f7e3 --- /dev/null +++ b/src/pip/_internal/index/sources.py @@ -0,0 +1,224 @@ +import logging +import mimetypes +import os +import pathlib +from typing import Callable, Iterable, Optional, Tuple + +from pip._internal.models.candidate import InstallationCandidate +from pip._internal.models.link import Link +from pip._internal.utils.urls import path_to_url, url_to_path +from pip._internal.vcs import is_url + +logger = logging.getLogger(__name__) + +FoundCandidates = Iterable[InstallationCandidate] +FoundLinks = Iterable[Link] +CandidatesFromPage = Callable[[Link], Iterable[InstallationCandidate]] +PageValidator = Callable[[Link], bool] + + +class LinkSource: + @property + def link(self) -> Optional[Link]: + """Returns the underlying link, if there's one.""" + raise NotImplementedError() + + def page_candidates(self) -> FoundCandidates: + """Candidates found by parsing an archive listing HTML file.""" + raise NotImplementedError() + + def file_links(self) -> FoundLinks: + """Links found by specifying archives directly.""" + raise NotImplementedError() + + +def _is_html_file(file_url: str) -> bool: + return mimetypes.guess_type(file_url, strict=False)[0] == "text/html" + + +class _FlatDirectorySource(LinkSource): + """Link source specified by ``--find-links=<path-to-dir>``. + + This looks the content of the directory, and returns: + + * ``page_candidates``: Links listed on each HTML file in the directory. + * ``file_candidates``: Archives in the directory. + """ + + def __init__( + self, + candidates_from_page: CandidatesFromPage, + path: str, + ) -> None: + self._candidates_from_page = candidates_from_page + self._path = pathlib.Path(os.path.realpath(path)) + + @property + def link(self) -> Optional[Link]: + return None + + def page_candidates(self) -> FoundCandidates: + for path in self._path.iterdir(): + url = path_to_url(str(path)) + if not _is_html_file(url): + continue + yield from self._candidates_from_page(Link(url)) + + def file_links(self) -> FoundLinks: + for path in self._path.iterdir(): + url = path_to_url(str(path)) + if _is_html_file(url): + continue + yield Link(url) + + +class _LocalFileSource(LinkSource): + """``--find-links=<path-or-url>`` or ``--[extra-]index-url=<path-or-url>``. + + If a URL is supplied, it must be a ``file:`` URL. If a path is supplied to + the option, it is converted to a URL first. This returns: + + * ``page_candidates``: Links listed on an HTML file. + * ``file_candidates``: The non-HTML file. + """ + + def __init__( + self, + candidates_from_page: CandidatesFromPage, + link: Link, + ) -> None: + self._candidates_from_page = candidates_from_page + self._link = link + + @property + def link(self) -> Optional[Link]: + return self._link + + def page_candidates(self) -> FoundCandidates: + if not _is_html_file(self._link.url): + return + yield from self._candidates_from_page(self._link) + + def file_links(self) -> FoundLinks: + if _is_html_file(self._link.url): + return + yield self._link + + +class _RemoteFileSource(LinkSource): + """``--find-links=<url>`` or ``--[extra-]index-url=<url>``. + + This returns: + + * ``page_candidates``: Links listed on an HTML file. + * ``file_candidates``: The non-HTML file. + """ + + def __init__( + self, + candidates_from_page: CandidatesFromPage, + page_validator: PageValidator, + link: Link, + ) -> None: + self._candidates_from_page = candidates_from_page + self._page_validator = page_validator + self._link = link + + @property + def link(self) -> Optional[Link]: + return self._link + + def page_candidates(self) -> FoundCandidates: + if not self._page_validator(self._link): + return + yield from self._candidates_from_page(self._link) + + def file_links(self) -> FoundLinks: + yield self._link + + +class _IndexDirectorySource(LinkSource): + """``--[extra-]index-url=<path-to-directory>``. + + This is treated like a remote URL; ``candidates_from_page`` contains logic + for this by appending ``index.html`` to the link. + """ + + def __init__( + self, + candidates_from_page: CandidatesFromPage, + link: Link, + ) -> None: + self._candidates_from_page = candidates_from_page + self._link = link + + @property + def link(self) -> Optional[Link]: + return self._link + + def page_candidates(self) -> FoundCandidates: + yield from self._candidates_from_page(self._link) + + def file_links(self) -> FoundLinks: + return () + + +def build_source( + location: str, + *, + candidates_from_page: CandidatesFromPage, + page_validator: PageValidator, + expand_dir: bool, + cache_link_parsing: bool, +) -> Tuple[Optional[str], Optional[LinkSource]]: + + path: Optional[str] = None + url: Optional[str] = None + if os.path.exists(location): # Is a local path. + url = path_to_url(location) + path = location + elif location.startswith("file:"): # A file: URL. + url = location + path = url_to_path(location) + elif is_url(location): + url = location + + if url is None: + msg = ( + "Location '%s' is ignored: " + "it is either a non-existing path or lacks a specific scheme." + ) + logger.warning(msg, location) + return (None, None) + + if path is None: + source: LinkSource = _RemoteFileSource( + candidates_from_page=candidates_from_page, + page_validator=page_validator, + link=Link(url, cache_link_parsing=cache_link_parsing), + ) + return (url, source) + + if os.path.isdir(path): + if expand_dir: + source = _FlatDirectorySource( + candidates_from_page=candidates_from_page, + path=path, + ) + else: + source = _IndexDirectorySource( + candidates_from_page=candidates_from_page, + link=Link(url, cache_link_parsing=cache_link_parsing), + ) + return (url, source) + elif os.path.isfile(path): + source = _LocalFileSource( + candidates_from_page=candidates_from_page, + link=Link(url, cache_link_parsing=cache_link_parsing), + ) + return (url, source) + logger.warning( + "Location '%s' is ignored: it is neither a file nor a directory.", + location, + ) + return (url, None) diff --git a/tests/unit/test_collector.py b/tests/unit/test_collector.py index 059fbc71985..969a920070a 100644 --- a/tests/unit/test_collector.py +++ b/tests/unit/test_collector.py @@ -1,3 +1,4 @@ +import itertools import logging import os.path import re @@ -23,10 +24,9 @@ _make_html_page, _NotHTML, _NotHTTP, - _remove_duplicate_links, - group_locations, parse_links, ) +from pip._internal.index.sources import _FlatDirectorySource, _IndexDirectorySource from pip._internal.models.index import PyPI from pip._internal.models.link import Link from pip._internal.network.session import PipSession @@ -587,46 +587,79 @@ def test_get_html_page_directory_append_index(tmpdir): assert actual.url == expected_url -def test_remove_duplicate_links(): - links = [ - # We choose Links that will test that ordering is preserved. - Link('https://example.com/2'), - Link('https://example.com/1'), - Link('https://example.com/2'), - ] - actual = _remove_duplicate_links(links) - assert actual == [ - Link('https://example.com/2'), - Link('https://example.com/1'), - ] - - -def test_group_locations__file_expand_dir(data): +def test_collect_sources__file_expand_dir(data): """ - Test that a file:// dir gets listdir run with expand_dir + Test that a file:// dir from --find-links becomes _FlatDirectorySource """ - files, urls = group_locations([data.find_links], expand_dir=True) - assert files and not urls, ( - "files and not urls should have been found " + collector = LinkCollector.create( + session=pretend.stub(is_secure_origin=None), # Shouldn't be used. + options=pretend.stub( + index_url="ignored-by-no-index", + extra_index_urls=[], + no_index=True, + find_links=[data.find_links], + ), + ) + sources = collector.collect_sources( + project_name=None, # Shouldn't be used. + candidates_from_page=None, # Shouldn't be used. + ) + assert ( + not sources.index_urls + and len(sources.find_links) == 1 + and isinstance(sources.find_links[0], _FlatDirectorySource) + ), ( + "Directory source should have been found " f"at find-links url: {data.find_links}" ) -def test_group_locations__file_not_find_link(data): +def test_collect_sources__file_not_find_link(data): """ - Test that a file:// url dir that's not a find-link, doesn't get a listdir + Test that a file:// dir from --index-url doesn't become _FlatDirectorySource run """ - files, urls = group_locations([data.index_url("empty_with_pkg")]) - assert urls and not files, "urls, but not files should have been found" + collector = LinkCollector.create( + session=pretend.stub(is_secure_origin=None), # Shouldn't be used. + options=pretend.stub( + index_url=data.index_url("empty_with_pkg"), + extra_index_urls=[], + no_index=False, + find_links=[], + ), + ) + sources = collector.collect_sources( + project_name="", + candidates_from_page=None, # Shouldn't be used. + ) + assert ( + not sources.find_links + and len(sources.index_urls) == 1 + and isinstance(sources.index_urls[0], _IndexDirectorySource) + ), "Directory specified as index should be treated as a page" -def test_group_locations__non_existing_path(): +def test_collect_sources__non_existing_path(): """ Test that a non-existing path is ignored. """ - files, urls = group_locations([os.path.join('this', 'doesnt', 'exist')]) - assert not urls and not files, "nothing should have been found" + collector = LinkCollector.create( + session=pretend.stub(is_secure_origin=None), # Shouldn't be used. + options=pretend.stub( + index_url="ignored-by-no-index", + extra_index_urls=[], + no_index=True, + find_links=[os.path.join("this", "doesnt", "exist")], + ), + ) + sources = collector.collect_sources( + project_name=None, # Shouldn't be used. + candidates_from_page=None, # Shouldn't be used. + ) + assert ( + not sources.index_urls + and sources.find_links == [None] + ), "Nothing should have been found" def check_links_include(links, names): @@ -664,7 +697,7 @@ def test_fetch_page(self, mock_get_html_response): url, session=link_collector.session, ) - def test_collect_links(self, caplog, data): + def test_collect_sources(self, caplog, data): caplog.set_level(logging.DEBUG) link_collector = make_test_link_collector( @@ -673,20 +706,33 @@ def test_collect_links(self, caplog, data): # is skipped. index_urls=[PyPI.simple_url, PyPI.simple_url], ) - actual = link_collector.collect_links('twine') + collected_sources = link_collector.collect_sources( + "twine", + candidates_from_page=lambda link: [link], + ) - # Spot-check the CollectedLinks return value. - assert len(actual.files) > 20 - check_links_include(actual.files, names=['simple-1.0.tar.gz']) + files_it = itertools.chain.from_iterable( + source.file_links() + for sources in collected_sources + for source in sources + if source is not None + ) + pages_it = itertools.chain.from_iterable( + source.page_candidates() + for sources in collected_sources + for source in sources + if source is not None + ) + files = list(files_it) + pages = list(pages_it) - assert len(actual.find_links) == 1 - check_links_include(actual.find_links, names=['packages']) - # Check that find-links URLs are marked as cacheable. - assert actual.find_links[0].cache_link_parsing + # Spot-check the returned sources. + assert len(files) > 20 + check_links_include(files, names=["simple-1.0.tar.gz"]) - assert actual.project_urls == [Link('https://pypi.org/simple/twine/')] + assert pages == [Link('https://pypi.org/simple/twine/')] # Check that index URLs are marked as *un*cacheable. - assert not actual.project_urls[0].cache_link_parsing + assert not pages[0].cache_link_parsing expected_message = dedent("""\ 1 location(s) to search for versions of twine: From 5ab17b96bd02ae8f60214fbf7669c9bc71e0f297 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 13 Apr 2021 04:13:17 +0800 Subject: [PATCH 3147/3170] Update vendored resolvelib to 0.7.0 --- src/pip/_vendor/resolvelib/__init__.py | 2 +- src/pip/_vendor/resolvelib/providers.py | 35 +++++++++++----------- src/pip/_vendor/resolvelib/providers.pyi | 15 ++++------ src/pip/_vendor/resolvelib/resolvers.py | 37 ++++++++++++++---------- src/pip/_vendor/resolvelib/structs.py | 24 +-------------- src/pip/_vendor/resolvelib/structs.pyi | 2 +- src/pip/_vendor/vendor.txt | 2 +- 7 files changed, 48 insertions(+), 69 deletions(-) diff --git a/src/pip/_vendor/resolvelib/__init__.py b/src/pip/_vendor/resolvelib/__init__.py index 34be7ee0f42..184874d45cd 100644 --- a/src/pip/_vendor/resolvelib/__init__.py +++ b/src/pip/_vendor/resolvelib/__init__.py @@ -11,7 +11,7 @@ "ResolutionTooDeep", ] -__version__ = "0.6.0" +__version__ = "0.7.0" from .providers import AbstractProvider, AbstractResolver diff --git a/src/pip/_vendor/resolvelib/providers.py b/src/pip/_vendor/resolvelib/providers.py index 852ee8f483b..4822d166551 100644 --- a/src/pip/_vendor/resolvelib/providers.py +++ b/src/pip/_vendor/resolvelib/providers.py @@ -9,28 +9,29 @@ def identify(self, requirement_or_candidate): """ raise NotImplementedError - def get_preference(self, resolution, candidates, information): + def get_preference(self, identifier, resolutions, candidates, information): """Produce a sort key for given requirement based on preference. The preference is defined as "I think this requirement should be resolved first". The lower the return value is, the more preferred this group of arguments is. - :param resolution: Currently pinned candidate, or `None`. - :param candidates: An iterable of possible candidates. - :param information: A list of requirement information. - - The `candidates` iterable's exact type depends on the return type of - `find_matches()`. A sequence is passed-in as-is if possible. If it - returns a callble, the iterator returned by that callable is passed - in here. + :param identifier: An identifier as returned by ``identify()``. This + identifies the dependency matches of which should be returned. + :param resolutions: Mapping of candidates currently pinned by the + resolver. Each key is an identifier, and the value a candidate. + The candidate may conflict with requirements from ``information``. + :param candidates: Mapping of each dependency's possible candidates. + Each value is an iterator of candidates. + :param information: Mapping of requirement information of each package. + Each value is an iterator of *requirement information*. - Each element in `information` is a named tuple with two entries: + A *requirement information* instance is a named tuple with two members: - * `requirement` specifies a requirement contributing to the current - candidate list. - * `parent` specifies the candidate that provides (dependend on) the - requirement, or `None` to indicate a root requirement. + * ``requirement`` specifies a requirement contributing to the current + list of candidates. + * ``parent`` specifies the candidate that provides (dependend on) the + requirement, or ``None`` to indicate a root requirement. The preference could depend on a various of issues, including (not necessarily in this order): @@ -43,10 +44,10 @@ def get_preference(self, resolution, candidates, information): * Are there any known conflicts for this requirement? We should probably work on those with the most known conflicts. - A sortable value should be returned (this will be used as the `key` + A sortable value should be returned (this will be used as the ``key`` parameter of the built-in sorting function). The smaller the value is, the more preferred this requirement is (i.e. the sorting function - is called with `reverse=False`). + is called with ``reverse=False``). """ raise NotImplementedError @@ -85,7 +86,7 @@ def is_satisfied_by(self, requirement, candidate): The candidate is guarenteed to have been generated from the requirement. - A boolean should be returned to indicate whether `candidate` is a + A boolean should be returned to indicate whether ``candidate`` is a viable solution to the requirement. """ raise NotImplementedError diff --git a/src/pip/_vendor/resolvelib/providers.pyi b/src/pip/_vendor/resolvelib/providers.pyi index 42c19c95f85..86ada59c419 100644 --- a/src/pip/_vendor/resolvelib/providers.pyi +++ b/src/pip/_vendor/resolvelib/providers.pyi @@ -12,13 +12,7 @@ from typing import ( from .reporters import BaseReporter from .resolvers import RequirementInformation -from .structs import ( - KT, - RT, - CT, - IterableView, - Matches, -) +from .structs import KT, RT, CT, Matches class Preference(Protocol): def __lt__(self, __other: Any) -> bool: ... @@ -27,9 +21,10 @@ class AbstractProvider(Generic[RT, CT, KT]): def identify(self, requirement_or_candidate: Union[RT, CT]) -> KT: ... def get_preference( self, - resolution: Optional[CT], - candidates: IterableView[CT], - information: Collection[RequirementInformation[RT, CT]], + identifier: KT, + resolutions: Mapping[KT, CT], + candidates: Mapping[KT, Iterator[CT]], + information: Mapping[KT, Iterator[RequirementInformation[RT, CT]]], ) -> Preference: ... def find_matches( self, diff --git a/src/pip/_vendor/resolvelib/resolvers.py b/src/pip/_vendor/resolvelib/resolvers.py index c79ccc4516b..99ee10516b8 100644 --- a/src/pip/_vendor/resolvelib/resolvers.py +++ b/src/pip/_vendor/resolvelib/resolvers.py @@ -173,12 +173,18 @@ def _merge_into_criterion(self, requirement, parent): raise RequirementsConflicted(criterion) return identifier, criterion - def _get_criterion_item_preference(self, item): - name, criterion = item + def _get_preference(self, name): return self._p.get_preference( - resolution=self.state.mapping.get(name), - candidates=criterion.candidates.for_preference(), - information=criterion.information, + identifier=name, + resolutions=self.state.mapping, + candidates=IteratorMapping( + self.state.criteria, + operator.attrgetter("candidates"), + ), + information=IteratorMapping( + self.state.criteria, + operator.attrgetter("information"), + ), ) def _is_current_pin_satisfying(self, name, criterion): @@ -198,7 +204,9 @@ def _get_criteria_to_update(self, candidate): criteria[name] = crit return criteria - def _attempt_to_pin_criterion(self, name, criterion): + def _attempt_to_pin_criterion(self, name): + criterion = self.state.criteria[name] + causes = [] for candidate in criterion.candidates: try: @@ -343,23 +351,20 @@ def resolve(self, requirements, max_rounds): for round_index in range(max_rounds): self._r.starting_round(index=round_index) - unsatisfied_criterion_items = [ - item - for item in self.state.criteria.items() - if not self._is_current_pin_satisfying(*item) + unsatisfied_names = [ + key + for key, criterion in self.state.criteria.items() + if not self._is_current_pin_satisfying(key, criterion) ] # All criteria are accounted for. Nothing more to pin, we are done! - if not unsatisfied_criterion_items: + if not unsatisfied_names: self._r.ending(state=self.state) return self.state # Choose the most preferred unpinned criterion to try. - name, criterion = min( - unsatisfied_criterion_items, - key=self._get_criterion_item_preference, - ) - failure_causes = self._attempt_to_pin_criterion(name, criterion) + name = min(unsatisfied_names, key=self._get_preference) + failure_causes = self._attempt_to_pin_criterion(name) if failure_causes: # Backtrack if pinning fails. The backtrack process puts us in diff --git a/src/pip/_vendor/resolvelib/structs.py b/src/pip/_vendor/resolvelib/structs.py index 72f2e604237..e1e7aa429e3 100644 --- a/src/pip/_vendor/resolvelib/structs.py +++ b/src/pip/_vendor/resolvelib/structs.py @@ -1,4 +1,5 @@ import itertools + from .compat import collections_abc @@ -120,18 +121,6 @@ def __bool__(self): def __iter__(self): return self._factory() - def for_preference(self): - """Provide an candidate iterable for `get_preference()`""" - return self._factory() - - def excluding(self, candidates): - """Create a new instance excluding specified candidates.""" - - def factory(): - return (c for c in self._factory() if c not in candidates) - - return type(self)(factory) - class _SequenceIterableView(object): """Wrap an iterable returned by find_matches(). @@ -154,17 +143,6 @@ def __bool__(self): def __iter__(self): return iter(self._sequence) - def __len__(self): - return len(self._sequence) - - def for_preference(self): - """Provide an candidate iterable for `get_preference()`""" - return self._sequence - - def excluding(self, candidates): - """Create a new instance excluding specified candidates.""" - return type(self)([c for c in self._sequence if c not in candidates]) - def build_iter_view(matches): """Build an iterable view from the value returned by `find_matches()`.""" diff --git a/src/pip/_vendor/resolvelib/structs.pyi b/src/pip/_vendor/resolvelib/structs.pyi index 1122d17aac6..14cd4644412 100644 --- a/src/pip/_vendor/resolvelib/structs.pyi +++ b/src/pip/_vendor/resolvelib/structs.pyi @@ -17,7 +17,7 @@ _T = TypeVar("_T") Matches = Union[Iterable[CT], Callable[[], Iterator[CT]]] class IterableView(Container[CT], Iterable[CT], metaclass=ABCMeta): - def excluding(self: _T, candidates: Container[CT]) -> _T: ... + pass class DirectedGraph(Generic[KT]): def __iter__(self) -> Iterator[KT]: ... diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 59d41e50f80..ce4c73e1d00 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -14,7 +14,7 @@ requests==2.25.1 chardet==4.0.0 idna==2.10 urllib3==1.26.4 -resolvelib==0.6.0 +resolvelib==0.7.0 setuptools==44.0.0 six==1.15.0 tenacity==6.3.1 From 602b22fcc6113be09eea7398bd81a6bc8bc8a79f Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Tue, 13 Apr 2021 04:23:09 +0800 Subject: [PATCH 3148/3170] Update factory implementation for resolvelib 0.7.0 --- news/resolvelib.vendor.rst | 2 +- .../resolution/resolvelib/provider.py | 30 +++++++------------ 2 files changed, 11 insertions(+), 21 deletions(-) diff --git a/news/resolvelib.vendor.rst b/news/resolvelib.vendor.rst index ebad91f25a9..f3d1df264d2 100644 --- a/news/resolvelib.vendor.rst +++ b/news/resolvelib.vendor.rst @@ -1 +1 @@ -Upgrade vendored resolvelib to 0.6.0. +Upgrade vendored resolvelib to 0.7.0. diff --git a/src/pip/_internal/resolution/resolvelib/provider.py b/src/pip/_internal/resolution/resolvelib/provider.py index 32597f7e093..0be58fd3ba8 100644 --- a/src/pip/_internal/resolution/resolvelib/provider.py +++ b/src/pip/_internal/resolution/resolvelib/provider.py @@ -1,13 +1,4 @@ -from typing import ( - TYPE_CHECKING, - Dict, - Iterable, - Iterator, - Mapping, - Optional, - Sequence, - Union, -) +from typing import TYPE_CHECKING, Dict, Iterable, Iterator, Mapping, Sequence, Union from pip._vendor.resolvelib.providers import AbstractProvider @@ -75,11 +66,11 @@ def identify(self, requirement_or_candidate): def get_preference( self, - resolution, # type: Optional[Candidate] - candidates, # type: Iterable[Candidate] - information, # type: Iterable[PreferenceInformation] - ): - # type: (...) -> Preference + identifier: str, + resolutions: Mapping[str, Candidate], + candidates: Mapping[str, Iterator[Candidate]], + information: Mapping[str, Iterator["PreferenceInformation"]], + ) -> "Preference": """Produce a sort key for given requirement based on preference. The lower the return value is, the more preferred this group of @@ -127,9 +118,8 @@ def _get_restrictive_rating(requirements): # A "bare" requirement without any version requirements. return 3 - restrictive = _get_restrictive_rating(req for req, _ in information) - key = next(iter(candidates)).name if candidates else "" - order = self._user_requested.get(key, float("inf")) + rating = _get_restrictive_rating(r for r, _ in information[identifier]) + order = self._user_requested.get(identifier, float("inf")) # HACK: Setuptools have a very long and solid backward compatibility # track record, and extremely few projects would request a narrow, @@ -139,9 +129,9 @@ def _get_restrictive_rating(requirements): # delaying Setuptools helps reduce branches the resolver has to check. # This serves as a temporary fix for issues like "apache-airlfow[all]" # while we work on "proper" branch pruning techniques. - delay_this = key == "setuptools" + delay_this = identifier == "setuptools" - return (delay_this, restrictive, order, key) + return (delay_this, rating, order, identifier) def find_matches( self, From df84d1ce6297818cb069378a15eab4946b06b880 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 15 Apr 2021 01:31:51 +0800 Subject: [PATCH 3149/3170] Upgrade sphinxcontrib-towncrier for docs --- tools/requirements/docs.txt | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/tools/requirements/docs.txt b/tools/requirements/docs.txt index 199edcfd6b3..1ab19c9b9ab 100644 --- a/tools/requirements/docs.txt +++ b/tools/requirements/docs.txt @@ -1,11 +1,10 @@ sphinx == 3.2.1 -# FIXME: Remove towncrier constraint after upgrading sphinxcontrib-towncrier. -towncrier < 19.9.0 +towncrier furo myst_parser sphinx-copybutton sphinx-inline-tabs -sphinxcontrib-towncrier +sphinxcontrib-towncrier >= 0.2.0a0 # `docs.pipext` uses pip's internals to generate documentation. So, we install # the current directory to make it work. From a79c6267de60410de4aa37fa65637d4a4d0ff45e Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Thu, 15 Apr 2021 07:21:24 +0800 Subject: [PATCH 3150/3170] Render top_line explicitly in towncrier template This allows sphinxcontrib-towncrier to render it in the documentation. The rendering of NEWS.rst is unchanged since Towncrier detects automatically if that line exists and does not render a duplicated section title. --- tools/news/template.rst | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/tools/news/template.rst b/tools/news/template.rst index 7ea90573dc2..70d6ab75def 100644 --- a/tools/news/template.rst +++ b/tools/news/template.rst @@ -1,7 +1,9 @@ -{% for section in sections %} +{{ top_line -}} + +{%- for section in sections %} {% set underline = "-" %} {% if section %} -{{section}} +{{ section }} {{ underline * section|length }}{% set underline = "~" %} {% endif %} From 4c69ab2a2c5103d468f9dae77453f06d0813359a Mon Sep 17 00:00:00 2001 From: Max W Chase <max.chase@gmail.com> Date: Sun, 17 Jan 2021 23:01:24 -0500 Subject: [PATCH 3151/3170] Support URL constraints in the new resolver Fixes #8253 --- docs/html/user_guide.rst | 8 +- news/8253.feature.rst | 1 + src/pip/_internal/models/link.py | 6 + src/pip/_internal/req/constructors.py | 16 + src/pip/_internal/req/req_install.py | 4 +- .../_internal/resolution/resolvelib/base.py | 29 +- .../resolution/resolvelib/candidates.py | 4 +- .../resolution/resolvelib/factory.py | 41 ++ tests/functional/test_install_direct_url.py | 23 + tests/functional/test_install_reqs.py | 20 +- tests/functional/test_new_resolver.py | 514 +++++++++++++++++- tests/functional/test_new_resolver_hashes.py | 104 ++++ 12 files changed, 744 insertions(+), 26 deletions(-) create mode 100644 news/8253.feature.rst diff --git a/docs/html/user_guide.rst b/docs/html/user_guide.rst index 61e0cdf0769..c36ac520528 100644 --- a/docs/html/user_guide.rst +++ b/docs/html/user_guide.rst @@ -254,9 +254,11 @@ Constraints Files Constraints files are requirements files that only control which version of a requirement is installed, not whether it is installed or not. Their syntax and -contents is nearly identical to :ref:`Requirements Files`. There is one key -difference: Including a package in a constraints file does not trigger -installation of the package. +contents is a subset of :ref:`Requirements Files`, with several kinds of syntax +not allowed: constraints must have a name, they cannot be editable, and they +cannot specify extras. In terms of semantics, there is one key difference: +Including a package in a constraints file does not trigger installation of the +package. Use a constraints file like so: diff --git a/news/8253.feature.rst b/news/8253.feature.rst new file mode 100644 index 00000000000..196e4dd9613 --- /dev/null +++ b/news/8253.feature.rst @@ -0,0 +1 @@ +Add the ability for the new resolver to process URL constraints. diff --git a/src/pip/_internal/models/link.py b/src/pip/_internal/models/link.py index 6f9a3244383..86d0be4079d 100644 --- a/src/pip/_internal/models/link.py +++ b/src/pip/_internal/models/link.py @@ -240,3 +240,9 @@ def is_hash_allowed(self, hashes): assert self.hash is not None return hashes.is_hash_allowed(self.hash_name, hex_digest=self.hash) + + +# TODO: Relax this comparison logic to ignore, for example, fragments. +def links_equivalent(link1, link2): + # type: (Link, Link) -> bool + return link1 == link2 diff --git a/src/pip/_internal/req/constructors.py b/src/pip/_internal/req/constructors.py index 810fc085988..b5e7e7c3e14 100644 --- a/src/pip/_internal/req/constructors.py +++ b/src/pip/_internal/req/constructors.py @@ -461,3 +461,19 @@ def install_req_from_parsed_requirement( user_supplied=user_supplied, ) return req + + +def install_req_from_link_and_ireq(link, ireq): + # type: (Link, InstallRequirement) -> InstallRequirement + return InstallRequirement( + req=ireq.req, + comes_from=ireq.comes_from, + editable=ireq.editable, + link=link, + markers=ireq.markers, + use_pep517=ireq.use_pep517, + isolated=ireq.isolated, + install_options=ireq.install_options, + global_options=ireq.global_options, + hash_options=ireq.hash_options, + ) diff --git a/src/pip/_internal/req/req_install.py b/src/pip/_internal/req/req_install.py index 5eba2c2ae74..2c5434830d8 100644 --- a/src/pip/_internal/req/req_install.py +++ b/src/pip/_internal/req/req_install.py @@ -840,8 +840,8 @@ def check_invalid_constraint_type(req): problem = "" if not req.name: problem = "Unnamed requirements are not allowed as constraints" - elif req.link: - problem = "Links are not allowed as constraints" + elif req.editable: + problem = "Editable requirements are not allowed as constraints" elif req.extras: problem = "Constraints cannot have extras" diff --git a/src/pip/_internal/resolution/resolvelib/base.py b/src/pip/_internal/resolution/resolvelib/base.py index 81fee9b9e3e..d42bca8bfa4 100644 --- a/src/pip/_internal/resolution/resolvelib/base.py +++ b/src/pip/_internal/resolution/resolvelib/base.py @@ -4,7 +4,7 @@ from pip._vendor.packaging.utils import canonicalize_name from pip._vendor.packaging.version import _BaseVersion -from pip._internal.models.link import Link +from pip._internal.models.link import Link, links_equivalent from pip._internal.req.req_install import InstallRequirement from pip._internal.utils.hashes import Hashes @@ -20,24 +20,26 @@ def format_name(project, extras): class Constraint: - def __init__(self, specifier, hashes): - # type: (SpecifierSet, Hashes) -> None + def __init__(self, specifier, hashes, links): + # type: (SpecifierSet, Hashes, FrozenSet[Link]) -> None self.specifier = specifier self.hashes = hashes + self.links = links @classmethod def empty(cls): # type: () -> Constraint - return Constraint(SpecifierSet(), Hashes()) + return Constraint(SpecifierSet(), Hashes(), frozenset()) @classmethod def from_ireq(cls, ireq): # type: (InstallRequirement) -> Constraint - return Constraint(ireq.specifier, ireq.hashes(trust_internet=False)) + links = frozenset([ireq.link]) if ireq.link else frozenset() + return Constraint(ireq.specifier, ireq.hashes(trust_internet=False), links) def __nonzero__(self): # type: () -> bool - return bool(self.specifier) or bool(self.hashes) + return bool(self.specifier) or bool(self.hashes) or bool(self.links) def __bool__(self): # type: () -> bool @@ -49,10 +51,16 @@ def __and__(self, other): return NotImplemented specifier = self.specifier & other.specifier hashes = self.hashes & other.hashes(trust_internet=False) - return Constraint(specifier, hashes) + links = self.links + if other.link: + links = links.union([other.link]) + return Constraint(specifier, hashes, links) def is_satisfied_by(self, candidate): # type: (Candidate) -> bool + # Reject if there are any mismatched URL constraints on this package. + if self.links and not all(_match_link(link, candidate) for link in self.links): + return False # We can safely always allow prereleases here since PackageFinder # already implements the prerelease logic, and would have filtered out # prerelease candidates if the user does not expect them. @@ -94,6 +102,13 @@ def format_for_error(self): raise NotImplementedError("Subclass should override") +def _match_link(link, candidate): + # type: (Link, Candidate) -> bool + if candidate.source_link: + return links_equivalent(link, candidate.source_link) + return False + + class Candidate: @property def project_name(self): diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index 035e118d022..ccd1129dfc9 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -8,7 +8,7 @@ from pip._vendor.pkg_resources import Distribution from pip._internal.exceptions import HashError, MetadataInconsistent -from pip._internal.models.link import Link +from pip._internal.models.link import Link, links_equivalent from pip._internal.models.wheel import Wheel from pip._internal.req.constructors import ( install_req_from_editable, @@ -155,7 +155,7 @@ def __hash__(self): def __eq__(self, other): # type: (Any) -> bool if isinstance(other, self.__class__): - return self._link == other._link + return links_equivalent(self._link, other._link) return False @property diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 3181d575336..f740734061d 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -32,6 +32,7 @@ from pip._internal.models.link import Link from pip._internal.models.wheel import Wheel from pip._internal.operations.prepare import RequirementPreparer +from pip._internal.req.constructors import install_req_from_link_and_ireq from pip._internal.req.req_install import InstallRequirement from pip._internal.resolution.base import InstallRequirementProvider from pip._internal.utils.compatibility_tags import get_supported @@ -264,6 +265,46 @@ def find_candidates( if ireq is not None: ireqs.append(ireq) + for link in constraint.links: + if not ireqs: + # If we hit this condition, then we cannot construct a candidate. + # However, if we hit this condition, then none of the requirements + # provided an ireq, so they must have provided an explicit candidate. + # In that case, either the candidate matches, in which case this loop + # doesn't need to do anything, or it doesn't, in which case there's + # nothing this loop can do to recover. + break + if link.is_wheel: + wheel = Wheel(link.filename) + # Check whether the provided wheel is compatible with the target + # platform. + if not wheel.supported(self._finder.target_python.get_tags()): + # We are constrained to install a wheel that is incompatible with + # the target architecture, so there are no valid candidates. + # Return early, with no candidates. + return () + # Create a "fake" InstallRequirement that's basically a clone of + # what "should" be the template, but with original_link set to link. + # Using the given requirement is necessary for preserving hash + # requirements, but without the original_link, direct_url.json + # won't be created. + ireq = install_req_from_link_and_ireq(link, ireqs[0]) + candidate = self._make_candidate_from_link( + link, + extras=frozenset(), + template=ireq, + name=canonicalize_name(ireq.name) if ireq.name else None, + version=None, + ) + if candidate is None: + # _make_candidate_from_link returns None if the wheel fails to build. + # We are constrained to install this wheel, so there are no valid + # candidates. + # Return early, with no candidates. + return () + + explicit_candidates.add(candidate) + # If none of the requirements want an explicit candidate, we can ask # the finder for candidates. if not explicit_candidates: diff --git a/tests/functional/test_install_direct_url.py b/tests/functional/test_install_direct_url.py index 23273774d16..e28a7e9b57e 100644 --- a/tests/functional/test_install_direct_url.py +++ b/tests/functional/test_install_direct_url.py @@ -1,5 +1,7 @@ import re +import pytest + from pip._internal.models.direct_url import DIRECT_URL_METADATA_NAME, DirectUrl from tests.lib import _create_test_package, path_to_url @@ -46,3 +48,24 @@ def test_install_archive_direct_url(script, data, with_wheel): assert req.startswith("simple @ file://") result = script.pip("install", req) assert _get_created_direct_url(result, "simple") + + +@pytest.mark.network +def test_install_vcs_constraint_direct_url(script, with_wheel): + constraints_file = script.scratch_path / "constraints.txt" + constraints_file.write_text( + "git+https://github.com/pypa/pip-test-package" + "@5547fa909e83df8bd743d3978d6667497983a4b7" + "#egg=pip-test-package" + ) + result = script.pip("install", "pip-test-package", "-c", constraints_file) + assert _get_created_direct_url(result, "pip_test_package") + + +def test_install_vcs_constraint_direct_file_url(script, with_wheel): + pkg_path = _create_test_package(script, name="testpkg") + url = path_to_url(pkg_path) + constraints_file = script.scratch_path / "constraints.txt" + constraints_file.write_text(f"git+{url}#egg=testpkg") + result = script.pip("install", "testpkg", "-c", constraints_file) + assert _get_created_direct_url(result, "testpkg") diff --git a/tests/functional/test_install_reqs.py b/tests/functional/test_install_reqs.py index d559e94be18..d8de6248b7d 100644 --- a/tests/functional/test_install_reqs.py +++ b/tests/functional/test_install_reqs.py @@ -405,7 +405,7 @@ def test_constraints_constrain_to_local_editable( expect_error=(resolver_variant == "2020-resolver"), ) if resolver_variant == "2020-resolver": - assert 'Links are not allowed as constraints' in result.stderr + assert 'Editable requirements are not allowed as constraints' in result.stderr else: assert 'Running setup.py develop for singlemodule' in result.stdout @@ -419,12 +419,8 @@ def test_constraints_constrain_to_local(script, data, resolver_variant): 'install', '--no-index', '-f', data.find_links, '-c', script.scratch_path / 'constraints.txt', 'singlemodule', allow_stderr_warning=True, - expect_error=(resolver_variant == "2020-resolver"), ) - if resolver_variant == "2020-resolver": - assert 'Links are not allowed as constraints' in result.stderr - else: - assert 'Running setup.py install for singlemodule' in result.stdout + assert 'Running setup.py install for singlemodule' in result.stdout def test_constrained_to_url_install_same_url(script, data, resolver_variant): @@ -438,7 +434,11 @@ def test_constrained_to_url_install_same_url(script, data, resolver_variant): expect_error=(resolver_variant == "2020-resolver"), ) if resolver_variant == "2020-resolver": - assert 'Links are not allowed as constraints' in result.stderr + assert 'Cannot install singlemodule 0.0.1' in result.stderr, str(result) + assert ( + 'because these package versions have conflicting dependencies.' + in result.stderr + ), str(result) else: assert ('Running setup.py install for singlemodule' in result.stdout), str(result) @@ -489,7 +489,7 @@ def test_install_with_extras_from_constraints(script, data, resolver_variant): expect_error=(resolver_variant == "2020-resolver"), ) if resolver_variant == "2020-resolver": - assert 'Links are not allowed as constraints' in result.stderr + assert 'Constraints cannot have extras' in result.stderr else: result.did_create(script.site_packages / 'simple') @@ -521,7 +521,7 @@ def test_install_with_extras_joined(script, data, resolver_variant): expect_error=(resolver_variant == "2020-resolver"), ) if resolver_variant == "2020-resolver": - assert 'Links are not allowed as constraints' in result.stderr + assert 'Constraints cannot have extras' in result.stderr else: result.did_create(script.site_packages / 'simple') result.did_create(script.site_packages / 'singlemodule.py') @@ -538,7 +538,7 @@ def test_install_with_extras_editable_joined(script, data, resolver_variant): expect_error=(resolver_variant == "2020-resolver"), ) if resolver_variant == "2020-resolver": - assert 'Links are not allowed as constraints' in result.stderr + assert 'Editable requirements are not allowed as constraints' in result.stderr else: result.did_create(script.site_packages / 'simple') result.did_create(script.site_packages / 'singlemodule.py') diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index 16f9f4f4216..78a21a3c115 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -10,7 +10,9 @@ create_basic_sdist_for_package, create_basic_wheel_for_package, create_test_package_with_setup, + path_to_url, ) +from tests.lib.path import Path from tests.lib.wheel import make_wheel @@ -45,6 +47,24 @@ def assert_editable(script, *args): f"{args!r} not all found in {script.site_packages_path!r}" +@pytest.fixture() +def make_fake_wheel(script): + + def _make_fake_wheel(name, version, wheel_tag): + wheel_house = script.scratch_path.joinpath("wheelhouse") + wheel_house.mkdir() + wheel_builder = make_wheel( + name=name, + version=version, + wheel_metadata_updates={"Tag": []}, + ) + wheel_path = wheel_house.joinpath(f"{name}-{version}-{wheel_tag}.whl") + wheel_builder.save_to(wheel_path) + return wheel_path + + return _make_fake_wheel + + def test_new_resolver_can_install(script): create_basic_wheel_for_package( script, @@ -641,8 +661,8 @@ def test_new_resolver_constraint_no_specifier(script): "Unnamed requirements are not allowed as constraints", ), ( - "req @ https://example.com/dist.zip", - "Links are not allowed as constraints", + "-e git+https://example.com/dist.git#egg=req", + "Editable requirements are not allowed as constraints", ), ( "pkg[extra]", @@ -1278,3 +1298,493 @@ def test_new_resolver_no_fetch_no_satisfying(script): "myuberpkg", ) assert "Processing " not in result.stdout, str(result) + + +def test_new_resolver_does_not_install_unneeded_packages_with_url_constraint(script): + archive_path = create_basic_wheel_for_package( + script, + "installed", + "0.1.0", + ) + not_installed_path = create_basic_wheel_for_package( + script, + "not_installed", + "0.1.0", + ) + + constraints_file = script.scratch_path / "constraints.txt" + constraints_file.write_text("not_installed @ " + path_to_url(not_installed_path)) + + (script.scratch_path / "index").mkdir() + archive_path.rename(script.scratch_path / "index" / archive_path.name) + + script.pip( + "install", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path / "index", + "-c", constraints_file, + "installed" + ) + + assert_installed(script, installed="0.1.0") + assert_not_installed(script, "not_installed") + + +def test_new_resolver_installs_packages_with_url_constraint(script): + installed_path = create_basic_wheel_for_package( + script, + "installed", + "0.1.0", + ) + + constraints_file = script.scratch_path / "constraints.txt" + constraints_file.write_text("installed @ " + path_to_url(installed_path)) + + script.pip( + "install", + "--no-cache-dir", "--no-index", + "-c", constraints_file, + "installed" + ) + + assert_installed(script, installed="0.1.0") + + +def test_new_resolver_reinstall_link_requirement_with_constraint(script): + installed_path = create_basic_wheel_for_package( + script, + "installed", + "0.1.0", + ) + + cr_file = script.scratch_path / "constraints.txt" + cr_file.write_text("installed @ " + path_to_url(installed_path)) + + script.pip( + "install", + "--no-cache-dir", "--no-index", + "-r", cr_file, + ) + + script.pip( + "install", + "--no-cache-dir", "--no-index", + "-c", cr_file, + "-r", cr_file, + ) + # TODO: strengthen assertion to "second invocation does no work" + # I don't think this is true yet, but it should be in the future. + + assert_installed(script, installed="0.1.0") + + +def test_new_resolver_prefers_url_constraint(script): + installed_path = create_basic_wheel_for_package( + script, + "test_pkg", + "0.1.0", + ) + not_installed_path = create_basic_wheel_for_package( + script, + "test_pkg", + "0.2.0", + ) + + constraints_file = script.scratch_path / "constraints.txt" + constraints_file.write_text("test_pkg @ " + path_to_url(installed_path)) + + (script.scratch_path / "index").mkdir() + not_installed_path.rename(script.scratch_path / "index" / not_installed_path.name) + + script.pip( + "install", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path / "index", + "-c", constraints_file, + "test_pkg" + ) + + assert_installed(script, test_pkg="0.1.0") + + +def test_new_resolver_prefers_url_constraint_on_update(script): + installed_path = create_basic_wheel_for_package( + script, + "test_pkg", + "0.1.0", + ) + not_installed_path = create_basic_wheel_for_package( + script, + "test_pkg", + "0.2.0", + ) + + constraints_file = script.scratch_path / "constraints.txt" + constraints_file.write_text("test_pkg @ " + path_to_url(installed_path)) + + (script.scratch_path / "index").mkdir() + not_installed_path.rename(script.scratch_path / "index" / not_installed_path.name) + + script.pip( + "install", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path / "index", + "test_pkg" + ) + + assert_installed(script, test_pkg="0.2.0") + + script.pip( + "install", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path / "index", + "-c", constraints_file, + "test_pkg" + ) + + assert_installed(script, test_pkg="0.1.0") + + +@pytest.mark.parametrize("version_option", ["--constraint", "--requirement"]) +def test_new_resolver_fails_with_url_constraint_and_incompatible_version( + script, version_option, +): + not_installed_path = create_basic_wheel_for_package( + script, + "test_pkg", + "0.1.0", + ) + not_installed_path = create_basic_wheel_for_package( + script, + "test_pkg", + "0.2.0", + ) + + url_constraint = script.scratch_path / "constraints.txt" + url_constraint.write_text("test_pkg @ " + path_to_url(not_installed_path)) + + version_req = script.scratch_path / "requirements.txt" + version_req.write_text("test_pkg<0.2.0") + + result = script.pip( + "install", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "--constraint", url_constraint, + version_option, version_req, + "test_pkg", + expect_error=True, + ) + + assert "Cannot install test_pkg" in result.stderr, str(result) + assert ( + "because these package versions have conflicting dependencies." + ) in result.stderr, str(result) + + assert_not_installed(script, "test_pkg") + + # Assert that pip works properly in the absence of the constraints file. + script.pip( + "install", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + version_option, version_req, + "test_pkg" + ) + + +def test_new_resolver_ignores_unneeded_conflicting_constraints(script): + version_1 = create_basic_wheel_for_package( + script, + "test_pkg", + "0.1.0", + ) + version_2 = create_basic_wheel_for_package( + script, + "test_pkg", + "0.2.0", + ) + create_basic_wheel_for_package( + script, + "installed", + "0.1.0", + ) + + constraints = [ + "test_pkg @ " + path_to_url(version_1), + "test_pkg @ " + path_to_url(version_2), + ] + + constraints_file = script.scratch_path / "constraints.txt" + constraints_file.write_text("\n".join(constraints)) + + script.pip( + "install", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "-c", constraints_file, + "installed" + ) + + assert_not_installed(script, "test_pkg") + assert_installed(script, installed="0.1.0") + + +def test_new_resolver_fails_on_needed_conflicting_constraints(script): + version_1 = create_basic_wheel_for_package( + script, + "test_pkg", + "0.1.0", + ) + version_2 = create_basic_wheel_for_package( + script, + "test_pkg", + "0.2.0", + ) + + constraints = [ + "test_pkg @ " + path_to_url(version_1), + "test_pkg @ " + path_to_url(version_2), + ] + + constraints_file = script.scratch_path / "constraints.txt" + constraints_file.write_text("\n".join(constraints)) + + result = script.pip( + "install", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "-c", constraints_file, + "test_pkg", + expect_error=True, + ) + + assert ( + "Cannot install test_pkg because these package versions have conflicting " + "dependencies." + ) in result.stderr, str(result) + + assert_not_installed(script, "test_pkg") + + # Assert that pip works properly in the absence of the constraints file. + script.pip( + "install", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "test_pkg", + ) + + +def test_new_resolver_fails_on_conflicting_constraint_and_requirement(script): + version_1 = create_basic_wheel_for_package( + script, + "test_pkg", + "0.1.0", + ) + version_2 = create_basic_wheel_for_package( + script, + "test_pkg", + "0.2.0", + ) + + constraints_file = script.scratch_path / "constraints.txt" + constraints_file.write_text("test_pkg @ " + path_to_url(version_1)) + + result = script.pip( + "install", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "-c", constraints_file, + "test_pkg @ " + path_to_url(version_2), + expect_error=True, + ) + + assert "Cannot install test-pkg 0.2.0" in result.stderr, str(result) + assert ( + "because these package versions have conflicting dependencies." + ) in result.stderr, str(result) + + assert_not_installed(script, "test_pkg") + + # Assert that pip works properly in the absence of the constraints file. + script.pip( + "install", + "--no-cache-dir", "--no-index", + "--find-links", script.scratch_path, + "test_pkg @ " + path_to_url(version_2), + ) + + +@pytest.mark.parametrize("editable", [False, True]) +def test_new_resolver_succeeds_on_matching_constraint_and_requirement(script, editable): + if editable: + source_dir = create_test_package_with_setup( + script, + name="test_pkg", + version="0.1.0" + ) + else: + source_dir = create_basic_wheel_for_package( + script, + "test_pkg", + "0.1.0", + ) + + req_line = "test_pkg @ " + path_to_url(source_dir) + + constraints_file = script.scratch_path / "constraints.txt" + constraints_file.write_text(req_line) + + if editable: + last_args = ("-e", source_dir) + else: + last_args = (req_line,) + + script.pip( + "install", + "--no-cache-dir", "--no-index", + "-c", constraints_file, + *last_args, + ) + + assert_installed(script, test_pkg="0.1.0") + if editable: + assert_editable(script, "test-pkg") + + +def test_new_resolver_applies_url_constraint_to_dep(script): + version_1 = create_basic_wheel_for_package( + script, + "dep", + "0.1.0", + ) + version_2 = create_basic_wheel_for_package( + script, + "dep", + "0.2.0", + ) + + base = create_basic_wheel_for_package(script, "base", "0.1.0", depends=["dep"]) + + (script.scratch_path / "index").mkdir() + base.rename(script.scratch_path / "index" / base.name) + version_2.rename(script.scratch_path / "index" / version_2.name) + + constraints_file = script.scratch_path / "constraints.txt" + constraints_file.write_text("dep @ " + path_to_url(version_1)) + + script.pip( + "install", + "--no-cache-dir", "--no-index", + "-c", constraints_file, + "--find-links", script.scratch_path / "index", + "base", + ) + + assert_installed(script, dep="0.1.0") + + +def test_new_resolver_handles_compatible_wheel_tags_in_constraint_url( + script, make_fake_wheel +): + initial_path = make_fake_wheel("base", "0.1.0", "fakepy1-fakeabi-fakeplat") + + constrained = script.scratch_path / "constrained" + constrained.mkdir() + + final_path = constrained / initial_path.name + + initial_path.rename(final_path) + + constraints_file = script.scratch_path / "constraints.txt" + constraints_file.write_text("base @ " + path_to_url(final_path)) + + result = script.pip( + "install", + "--implementation", "fakepy", + '--only-binary=:all:', + "--python-version", "1", + "--abi", "fakeabi", + "--platform", "fakeplat", + "--target", script.scratch_path / "target", + "--no-cache-dir", "--no-index", + "-c", constraints_file, + "base", + ) + + dist_info = Path("scratch", "target", "base-0.1.0.dist-info") + result.did_create(dist_info) + + +def test_new_resolver_handles_incompatible_wheel_tags_in_constraint_url( + script, make_fake_wheel +): + initial_path = make_fake_wheel("base", "0.1.0", "fakepy1-fakeabi-fakeplat") + + constrained = script.scratch_path / "constrained" + constrained.mkdir() + + final_path = constrained / initial_path.name + + initial_path.rename(final_path) + + constraints_file = script.scratch_path / "constraints.txt" + constraints_file.write_text("base @ " + path_to_url(final_path)) + + result = script.pip( + "install", + "--no-cache-dir", "--no-index", + "-c", constraints_file, + "base", + expect_error=True, + ) + + assert ( + "Cannot install base because these package versions have conflicting " + "dependencies." + ) in result.stderr, str(result) + + assert_not_installed(script, "base") + + +def test_new_resolver_avoids_incompatible_wheel_tags_in_constraint_url( + script, make_fake_wheel +): + initial_path = make_fake_wheel("dep", "0.1.0", "fakepy1-fakeabi-fakeplat") + + constrained = script.scratch_path / "constrained" + constrained.mkdir() + + final_path = constrained / initial_path.name + + initial_path.rename(final_path) + + constraints_file = script.scratch_path / "constraints.txt" + constraints_file.write_text("dep @ " + path_to_url(final_path)) + + index = script.scratch_path / "index" + index.mkdir() + + index_dep = create_basic_wheel_for_package(script, "dep", "0.2.0") + + base = create_basic_wheel_for_package( + script, "base", "0.1.0" + ) + base_2 = create_basic_wheel_for_package( + script, "base", "0.2.0", depends=["dep"] + ) + + index_dep.rename(index / index_dep.name) + base.rename(index / base.name) + base_2.rename(index / base_2.name) + + script.pip( + "install", + "--no-cache-dir", "--no-index", + "-c", constraints_file, + "--find-links", script.scratch_path / "index", + "base", + ) + + assert_installed(script, base="0.1.0") + assert_not_installed(script, "dep") diff --git a/tests/functional/test_new_resolver_hashes.py b/tests/functional/test_new_resolver_hashes.py index 854b66418ae..02397616ab7 100644 --- a/tests/functional/test_new_resolver_hashes.py +++ b/tests/functional/test_new_resolver_hashes.py @@ -1,7 +1,9 @@ import collections import hashlib +import json import pytest +from pip._vendor.packaging.utils import canonicalize_name from pip._internal.utils.urls import path_to_url from tests.lib import create_basic_sdist_for_package, create_basic_wheel_for_package @@ -11,6 +13,30 @@ ) +def assert_installed(script, **kwargs): + ret = script.pip('list', '--format=json') + installed = set( + (canonicalize_name(val['name']), val['version']) + for val in json.loads(ret.stdout) + ) + expected = set((canonicalize_name(k), v) for k, v in kwargs.items()) + assert expected <= installed, \ + "{!r} not all in {!r}".format(expected, installed) + + +def assert_not_installed(script, *args): + ret = script.pip("list", "--format=json") + installed = set( + canonicalize_name(val["name"]) + for val in json.loads(ret.stdout) + ) + # None of the given names should be listed as installed, i.e. their + # intersection should be empty. + expected = set(canonicalize_name(k) for k in args) + assert not (expected & installed), \ + "{!r} contained in {!r}".format(expected, installed) + + def _create_find_links(script): sdist_path = create_basic_sdist_for_package(script, "base", "0.1.0") wheel_path = create_basic_wheel_for_package(script, "base", "0.1.0") @@ -204,3 +230,81 @@ def test_new_resolver_hash_intersect_empty_from_constraint(script): "from some requirements." ) assert message in result.stderr, str(result) + + +@pytest.mark.parametrize("constrain_by_hash", [False, True]) +def test_new_resolver_hash_requirement_and_url_constraint_can_succeed( + script, constrain_by_hash, +): + wheel_path = create_basic_wheel_for_package(script, "base", "0.1.0") + + wheel_hash = hashlib.sha256(wheel_path.read_bytes()).hexdigest() + + requirements_txt = script.scratch_path / "requirements.txt" + requirements_txt.write_text( + """ + base==0.1.0 --hash=sha256:{wheel_hash} + """.format( + wheel_hash=wheel_hash, + ), + ) + + constraints_txt = script.scratch_path / "constraints.txt" + constraint_text = "base @ {wheel_url}\n".format(wheel_url=path_to_url(wheel_path)) + if constrain_by_hash: + constraint_text += "base==0.1.0 --hash=sha256:{wheel_hash}\n".format( + wheel_hash=wheel_hash, + ) + constraints_txt.write_text(constraint_text) + + script.pip( + "install", + "--no-cache-dir", + "--no-index", + "--constraint", constraints_txt, + "--requirement", requirements_txt, + ) + + assert_installed(script, base="0.1.0") + + +@pytest.mark.parametrize("constrain_by_hash", [False, True]) +def test_new_resolver_hash_requirement_and_url_constraint_can_fail( + script, constrain_by_hash, +): + wheel_path = create_basic_wheel_for_package(script, "base", "0.1.0") + other_path = create_basic_wheel_for_package(script, "other", "0.1.0") + + other_hash = hashlib.sha256(other_path.read_bytes()).hexdigest() + + requirements_txt = script.scratch_path / "requirements.txt" + requirements_txt.write_text( + """ + base==0.1.0 --hash=sha256:{other_hash} + """.format( + other_hash=other_hash, + ), + ) + + constraints_txt = script.scratch_path / "constraints.txt" + constraint_text = "base @ {wheel_url}\n".format(wheel_url=path_to_url(wheel_path)) + if constrain_by_hash: + constraint_text += "base==0.1.0 --hash=sha256:{other_hash}\n".format( + other_hash=other_hash, + ) + constraints_txt.write_text(constraint_text) + + result = script.pip( + "install", + "--no-cache-dir", + "--no-index", + "--constraint", constraints_txt, + "--requirement", requirements_txt, + expect_error=True, + ) + + assert ( + "THESE PACKAGES DO NOT MATCH THE HASHES FROM THE REQUIREMENTS FILE." + ) in result.stderr, str(result) + + assert_not_installed(script, "base", "other") From 7bea3ec262df2e98b5eb64baf506a51f1be73136 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sat, 17 Apr 2021 16:27:56 +0200 Subject: [PATCH 3152/3170] Upgrade pep517 to 0.10.0 --- news/pep517.vendor.rst | 1 + src/pip/_vendor/pep517/__init__.py | 2 +- src/pip/_vendor/pep517/build.py | 3 +++ src/pip/_vendor/pep517/check.py | 3 +++ src/pip/_vendor/pep517/in_process/__init__.py | 17 +++++++++++++++++ .../pep517/{ => in_process}/_in_process.py | 0 src/pip/_vendor/pep517/wrappers.py | 13 ++----------- src/pip/_vendor/resolvelib.pyi | 1 + src/pip/_vendor/vendor.txt | 2 +- 9 files changed, 29 insertions(+), 13 deletions(-) create mode 100644 news/pep517.vendor.rst create mode 100644 src/pip/_vendor/pep517/in_process/__init__.py rename src/pip/_vendor/pep517/{ => in_process}/_in_process.py (100%) create mode 100644 src/pip/_vendor/resolvelib.pyi diff --git a/news/pep517.vendor.rst b/news/pep517.vendor.rst new file mode 100644 index 00000000000..f9ddefc00ff --- /dev/null +++ b/news/pep517.vendor.rst @@ -0,0 +1 @@ +Upgrade pep517 to 0.10.0 diff --git a/src/pip/_vendor/pep517/__init__.py b/src/pip/_vendor/pep517/__init__.py index 10687486e2b..3b07c639c46 100644 --- a/src/pip/_vendor/pep517/__init__.py +++ b/src/pip/_vendor/pep517/__init__.py @@ -1,6 +1,6 @@ """Wrappers to build Python packages using PEP 517 hooks """ -__version__ = '0.9.1' +__version__ = '0.10.0' from .wrappers import * # noqa: F401, F403 diff --git a/src/pip/_vendor/pep517/build.py b/src/pip/_vendor/pep517/build.py index 264301447e2..f884bcf1097 100644 --- a/src/pip/_vendor/pep517/build.py +++ b/src/pip/_vendor/pep517/build.py @@ -110,6 +110,9 @@ def build(source_dir, dist, dest=None, system=None): def main(args): + log.warning('pep517.build is deprecated. ' + 'Consider switching to https://pypi.org/project/build/') + # determine which dists to build dists = list(filter(None, ( 'sdist' if args.source or not args.binary else None, diff --git a/src/pip/_vendor/pep517/check.py b/src/pip/_vendor/pep517/check.py index 13e722a3748..decab8a3423 100644 --- a/src/pip/_vendor/pep517/check.py +++ b/src/pip/_vendor/pep517/check.py @@ -167,6 +167,9 @@ def check(source_dir): def main(argv=None): + log.warning('pep517.check is deprecated. ' + 'Consider switching to https://pypi.org/project/build/') + ap = argparse.ArgumentParser() ap.add_argument( 'source_dir', diff --git a/src/pip/_vendor/pep517/in_process/__init__.py b/src/pip/_vendor/pep517/in_process/__init__.py new file mode 100644 index 00000000000..c932313b328 --- /dev/null +++ b/src/pip/_vendor/pep517/in_process/__init__.py @@ -0,0 +1,17 @@ +"""This is a subpackage because the directory is on sys.path for _in_process.py + +The subpackage should stay as empty as possible to avoid shadowing modules that +the backend might import. +""" +from os.path import dirname, abspath, join as pjoin +from contextlib import contextmanager + +try: + import importlib.resources as resources + + def _in_proc_script_path(): + return resources.path(__package__, '_in_process.py') +except ImportError: + @contextmanager + def _in_proc_script_path(): + yield pjoin(dirname(abspath(__file__)), '_in_process.py') diff --git a/src/pip/_vendor/pep517/_in_process.py b/src/pip/_vendor/pep517/in_process/_in_process.py similarity index 100% rename from src/pip/_vendor/pep517/_in_process.py rename to src/pip/_vendor/pep517/in_process/_in_process.py diff --git a/src/pip/_vendor/pep517/wrappers.py b/src/pip/_vendor/pep517/wrappers.py index d6338ea5201..00974aa8bea 100644 --- a/src/pip/_vendor/pep517/wrappers.py +++ b/src/pip/_vendor/pep517/wrappers.py @@ -1,13 +1,14 @@ import threading from contextlib import contextmanager import os -from os.path import dirname, abspath, join as pjoin +from os.path import abspath, join as pjoin import shutil from subprocess import check_call, check_output, STDOUT import sys from tempfile import mkdtemp from . import compat +from .in_process import _in_proc_script_path __all__ = [ 'BackendUnavailable', @@ -19,16 +20,6 @@ 'Pep517HookCaller', ] -try: - import importlib.resources as resources - - def _in_proc_script_path(): - return resources.path(__package__, '_in_process.py') -except ImportError: - @contextmanager - def _in_proc_script_path(): - yield pjoin(dirname(abspath(__file__)), '_in_process.py') - @contextmanager def tempdir(): diff --git a/src/pip/_vendor/resolvelib.pyi b/src/pip/_vendor/resolvelib.pyi new file mode 100644 index 00000000000..b4ef4e108c4 --- /dev/null +++ b/src/pip/_vendor/resolvelib.pyi @@ -0,0 +1 @@ +from resolvelib import * \ No newline at end of file diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index ce4c73e1d00..713f42d4e38 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -6,7 +6,7 @@ distro==1.5.0 html5lib==1.1 msgpack==1.0.2 packaging==20.9 -pep517==0.9.1 +pep517==0.10.0 progress==1.5 pyparsing==2.4.7 requests==2.25.1 From 79173f9091e1b1e16ef384e82851bb922e9d91ae Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sat, 17 Apr 2021 16:28:23 +0200 Subject: [PATCH 3153/3170] Upgrade idna to 3.1 --- news/idna.vendor.rst | 1 + src/pip/_vendor/idna/LICENSE.md | 29 + src/pip/_vendor/idna/LICENSE.rst | 34 - src/pip/_vendor/idna/codec.py | 40 +- src/pip/_vendor/idna/compat.py | 2 +- src/pip/_vendor/idna/core.py | 66 +- src/pip/_vendor/idna/idnadata.py | 2 +- src/pip/_vendor/idna/package_data.py | 2 +- src/pip/_vendor/idna/uts46data.py | 11659 ++++++++++++------------- src/pip/_vendor/vendor.txt | 2 +- 10 files changed, 5910 insertions(+), 5927 deletions(-) create mode 100644 news/idna.vendor.rst create mode 100644 src/pip/_vendor/idna/LICENSE.md delete mode 100644 src/pip/_vendor/idna/LICENSE.rst diff --git a/news/idna.vendor.rst b/news/idna.vendor.rst new file mode 100644 index 00000000000..253933c9b6e --- /dev/null +++ b/news/idna.vendor.rst @@ -0,0 +1 @@ +Upgrade idna to 3.1 diff --git a/src/pip/_vendor/idna/LICENSE.md b/src/pip/_vendor/idna/LICENSE.md new file mode 100644 index 00000000000..b6f87326ffb --- /dev/null +++ b/src/pip/_vendor/idna/LICENSE.md @@ -0,0 +1,29 @@ +BSD 3-Clause License + +Copyright (c) 2013-2021, Kim Davies +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +1. Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +2. Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +3. Neither the name of the copyright holder nor the names of its + contributors may be used to endorse or promote products derived from + this software without specific prior written permission. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/src/pip/_vendor/idna/LICENSE.rst b/src/pip/_vendor/idna/LICENSE.rst deleted file mode 100644 index 63664b82e7a..00000000000 --- a/src/pip/_vendor/idna/LICENSE.rst +++ /dev/null @@ -1,34 +0,0 @@ -License -------- - -License: bsd-3-clause - -Copyright (c) 2013-2020, Kim Davies. All rights reserved. - -Redistribution and use in source and binary forms, with or without -modification, are permitted provided that the following conditions are met: - -#. Redistributions of source code must retain the above copyright - notice, this list of conditions and the following disclaimer. - -#. Redistributions in binary form must reproduce the above - copyright notice, this list of conditions and the following - disclaimer in the documentation and/or other materials provided with - the distribution. - -#. Neither the name of the copyright holder nor the names of the - contributors may be used to endorse or promote products derived - from this software without specific prior written permission. - -#. THIS SOFTWARE IS PROVIDED BY THE CONTRIBUTORS "AS IS" AND ANY - EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE - IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR - PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDERS OR - CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, - SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT - LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, - DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY - THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT - (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE - USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH - DAMAGE. diff --git a/src/pip/_vendor/idna/codec.py b/src/pip/_vendor/idna/codec.py index 98c65ead146..30fe72fbd40 100644 --- a/src/pip/_vendor/idna/codec.py +++ b/src/pip/_vendor/idna/codec.py @@ -2,14 +2,14 @@ import codecs import re -_unicode_dots_re = re.compile(u'[\u002e\u3002\uff0e\uff61]') +_unicode_dots_re = re.compile('[\u002e\u3002\uff0e\uff61]') class Codec(codecs.Codec): def encode(self, data, errors='strict'): if errors != 'strict': - raise IDNAError("Unsupported error handling \"{0}\"".format(errors)) + raise IDNAError('Unsupported error handling \"{}\"'.format(errors)) if not data: return "", 0 @@ -19,23 +19,23 @@ def encode(self, data, errors='strict'): def decode(self, data, errors='strict'): if errors != 'strict': - raise IDNAError("Unsupported error handling \"{0}\"".format(errors)) + raise IDNAError('Unsupported error handling \"{}\"'.format(errors)) if not data: - return u"", 0 + return '', 0 return decode(data), len(data) class IncrementalEncoder(codecs.BufferedIncrementalEncoder): def _buffer_encode(self, data, errors, final): if errors != 'strict': - raise IDNAError("Unsupported error handling \"{0}\"".format(errors)) + raise IDNAError('Unsupported error handling \"{}\"'.format(errors)) if not data: - return ("", 0) + return ('', 0) labels = _unicode_dots_re.split(data) - trailing_dot = u'' + trailing_dot = '' if labels: if not labels[-1]: trailing_dot = '.' @@ -55,37 +55,29 @@ def _buffer_encode(self, data, errors, final): size += len(label) # Join with U+002E - result = ".".join(result) + trailing_dot + result = '.'.join(result) + trailing_dot size += len(trailing_dot) return (result, size) class IncrementalDecoder(codecs.BufferedIncrementalDecoder): def _buffer_decode(self, data, errors, final): if errors != 'strict': - raise IDNAError("Unsupported error handling \"{0}\"".format(errors)) + raise IDNAError('Unsupported error handling \"{}\"'.format(errors)) if not data: - return (u"", 0) - - # IDNA allows decoding to operate on Unicode strings, too. - if isinstance(data, unicode): - labels = _unicode_dots_re.split(data) - else: - # Must be ASCII string - data = str(data) - unicode(data, "ascii") - labels = data.split(".") - - trailing_dot = u'' + return ('', 0) + + labels = _unicode_dots_re.split(data) + trailing_dot = '' if labels: if not labels[-1]: - trailing_dot = u'.' + trailing_dot = '.' del labels[-1] elif not final: # Keep potentially unfinished label until the next call del labels[-1] if labels: - trailing_dot = u'.' + trailing_dot = '.' result = [] size = 0 @@ -95,7 +87,7 @@ def _buffer_decode(self, data, errors, final): size += 1 size += len(label) - result = u".".join(result) + trailing_dot + result = '.'.join(result) + trailing_dot size += len(trailing_dot) return (result, size) diff --git a/src/pip/_vendor/idna/compat.py b/src/pip/_vendor/idna/compat.py index 4d47f336dbc..2e622d6f942 100644 --- a/src/pip/_vendor/idna/compat.py +++ b/src/pip/_vendor/idna/compat.py @@ -8,5 +8,5 @@ def ToUnicode(label): return decode(label) def nameprep(s): - raise NotImplementedError("IDNA 2008 does not utilise nameprep protocol") + raise NotImplementedError('IDNA 2008 does not utilise nameprep protocol') diff --git a/src/pip/_vendor/idna/core.py b/src/pip/_vendor/idna/core.py index 41ec5c711d1..2c193d631cd 100644 --- a/src/pip/_vendor/idna/core.py +++ b/src/pip/_vendor/idna/core.py @@ -7,11 +7,7 @@ _virama_combining_class = 9 _alabel_prefix = b'xn--' -_unicode_dots_re = re.compile(u'[\u002e\u3002\uff0e\uff61]') - -if sys.version_info[0] >= 3: - unicode = str - unichr = chr +_unicode_dots_re = re.compile('[\u002e\u3002\uff0e\uff61]') class IDNAError(UnicodeError): """ Base exception for all IDNA-encoding related problems """ @@ -34,10 +30,10 @@ class InvalidCodepointContext(IDNAError): def _combining_class(cp): - v = unicodedata.combining(unichr(cp)) + v = unicodedata.combining(chr(cp)) if v == 0: - if not unicodedata.name(unichr(cp)): - raise ValueError("Unknown character in unicodedata") + if not unicodedata.name(chr(cp)): + raise ValueError('Unknown character in unicodedata') return v def _is_script(cp, script): @@ -47,7 +43,7 @@ def _punycode(s): return s.encode('punycode') def _unot(s): - return 'U+{0:04X}'.format(s) + return 'U+{:04X}'.format(s) def valid_label_length(label): @@ -72,7 +68,7 @@ def check_bidi(label, check_ltr=False): direction = unicodedata.bidirectional(cp) if direction == '': # String likely comes from a newer version of Unicode - raise IDNABidiError('Unknown directionality in label {0} at position {1}'.format(repr(label), idx)) + raise IDNABidiError('Unknown directionality in label {} at position {}'.format(repr(label), idx)) if direction in ['R', 'AL', 'AN']: bidi_label = True if not bidi_label and not check_ltr: @@ -85,7 +81,7 @@ def check_bidi(label, check_ltr=False): elif direction == 'L': rtl = False else: - raise IDNABidiError('First codepoint in label {0} must be directionality L, R or AL'.format(repr(label))) + raise IDNABidiError('First codepoint in label {} must be directionality L, R or AL'.format(repr(label))) valid_ending = False number_type = False @@ -95,7 +91,7 @@ def check_bidi(label, check_ltr=False): if rtl: # Bidi rule 2 if not direction in ['R', 'AL', 'AN', 'EN', 'ES', 'CS', 'ET', 'ON', 'BN', 'NSM']: - raise IDNABidiError('Invalid direction for codepoint at position {0} in a right-to-left label'.format(idx)) + raise IDNABidiError('Invalid direction for codepoint at position {} in a right-to-left label'.format(idx)) # Bidi rule 3 if direction in ['R', 'AL', 'EN', 'AN']: valid_ending = True @@ -111,7 +107,7 @@ def check_bidi(label, check_ltr=False): else: # Bidi rule 5 if not direction in ['L', 'EN', 'ES', 'CS', 'ET', 'ON', 'BN', 'NSM']: - raise IDNABidiError('Invalid direction for codepoint at position {0} in a left-to-right label'.format(idx)) + raise IDNABidiError('Invalid direction for codepoint at position {} in a left-to-right label'.format(idx)) # Bidi rule 6 if direction in ['L', 'EN']: valid_ending = True @@ -212,7 +208,7 @@ def valid_contexto(label, pos, exception=False): elif cp_value == 0x30fb: for cp in label: - if cp == u'\u30fb': + if cp == '\u30fb': continue if _is_script(cp, 'Hiragana') or _is_script(cp, 'Katakana') or _is_script(cp, 'Han'): return True @@ -249,16 +245,16 @@ def check_label(label): elif intranges_contain(cp_value, idnadata.codepoint_classes['CONTEXTJ']): try: if not valid_contextj(label, pos): - raise InvalidCodepointContext('Joiner {0} not allowed at position {1} in {2}'.format( + raise InvalidCodepointContext('Joiner {} not allowed at position {} in {}'.format( _unot(cp_value), pos+1, repr(label))) except ValueError: - raise IDNAError('Unknown codepoint adjacent to joiner {0} at position {1} in {2}'.format( + raise IDNAError('Unknown codepoint adjacent to joiner {} at position {} in {}'.format( _unot(cp_value), pos+1, repr(label))) elif intranges_contain(cp_value, idnadata.codepoint_classes['CONTEXTO']): if not valid_contexto(label, pos): - raise InvalidCodepointContext('Codepoint {0} not allowed at position {1} in {2}'.format(_unot(cp_value), pos+1, repr(label))) + raise InvalidCodepointContext('Codepoint {} not allowed at position {} in {}'.format(_unot(cp_value), pos+1, repr(label))) else: - raise InvalidCodepoint('Codepoint {0} at position {1} of {2} not allowed'.format(_unot(cp_value), pos+1, repr(label))) + raise InvalidCodepoint('Codepoint {} at position {} of {} not allowed'.format(_unot(cp_value), pos+1, repr(label))) check_bidi(label) @@ -277,7 +273,7 @@ def alabel(label): if not label: raise IDNAError('No Input') - label = unicode(label) + label = str(label) check_label(label) label = _punycode(label) label = _alabel_prefix + label @@ -316,35 +312,35 @@ def ulabel(label): def uts46_remap(domain, std3_rules=True, transitional=False): """Re-map the characters in the string according to UTS46 processing.""" from .uts46data import uts46data - output = u"" + output = '' try: for pos, char in enumerate(domain): code_point = ord(char) uts46row = uts46data[code_point if code_point < 256 else - bisect.bisect_left(uts46data, (code_point, "Z")) - 1] + bisect.bisect_left(uts46data, (code_point, 'Z')) - 1] status = uts46row[1] replacement = uts46row[2] if len(uts46row) == 3 else None - if (status == "V" or - (status == "D" and not transitional) or - (status == "3" and not std3_rules and replacement is None)): + if (status == 'V' or + (status == 'D' and not transitional) or + (status == '3' and not std3_rules and replacement is None)): output += char - elif replacement is not None and (status == "M" or - (status == "3" and not std3_rules) or - (status == "D" and transitional)): + elif replacement is not None and (status == 'M' or + (status == '3' and not std3_rules) or + (status == 'D' and transitional)): output += replacement - elif status != "I": + elif status != 'I': raise IndexError() - return unicodedata.normalize("NFC", output) + return unicodedata.normalize('NFC', output) except IndexError: raise InvalidCodepoint( - "Codepoint {0} not allowed at position {1} in {2}".format( + 'Codepoint {} not allowed at position {} in {}'.format( _unot(code_point), pos + 1, repr(domain))) def encode(s, strict=False, uts46=False, std3_rules=False, transitional=False): if isinstance(s, (bytes, bytearray)): - s = s.decode("ascii") + s = s.decode('ascii') if uts46: s = uts46_remap(s, std3_rules, transitional) trailing_dot = False @@ -375,7 +371,7 @@ def encode(s, strict=False, uts46=False, std3_rules=False, transitional=False): def decode(s, strict=False, uts46=False, std3_rules=False): if isinstance(s, (bytes, bytearray)): - s = s.decode("ascii") + s = s.decode('ascii') if uts46: s = uts46_remap(s, std3_rules, False) trailing_dot = False @@ -383,7 +379,7 @@ def decode(s, strict=False, uts46=False, std3_rules=False): if not strict: labels = _unicode_dots_re.split(s) else: - labels = s.split(u'.') + labels = s.split('.') if not labels or labels == ['']: raise IDNAError('Empty domain') if not labels[-1]: @@ -396,5 +392,5 @@ def decode(s, strict=False, uts46=False, std3_rules=False): else: raise IDNAError('Empty label') if trailing_dot: - result.append(u'') - return u'.'.join(result) + result.append('') + return '.'.join(result) diff --git a/src/pip/_vendor/idna/idnadata.py b/src/pip/_vendor/idna/idnadata.py index a284e4c84ac..b86a3e06e42 100644 --- a/src/pip/_vendor/idna/idnadata.py +++ b/src/pip/_vendor/idna/idnadata.py @@ -1,6 +1,6 @@ # This file is automatically generated by tools/idna-data -__version__ = "13.0.0" +__version__ = '13.0.0' scripts = { 'Greek': ( 0x37000000374, diff --git a/src/pip/_vendor/idna/package_data.py b/src/pip/_vendor/idna/package_data.py index ce1c521d23a..1420ea2f146 100644 --- a/src/pip/_vendor/idna/package_data.py +++ b/src/pip/_vendor/idna/package_data.py @@ -1,2 +1,2 @@ -__version__ = '2.10' +__version__ = '3.1' diff --git a/src/pip/_vendor/idna/uts46data.py b/src/pip/_vendor/idna/uts46data.py index 3766dd49f6d..8ae36cbe7f1 100644 --- a/src/pip/_vendor/idna/uts46data.py +++ b/src/pip/_vendor/idna/uts46data.py @@ -1,10 +1,9 @@ # This file is automatically generated by tools/idna-data -# vim: set fileencoding=utf-8 : """IDNA Mapping Table from UTS46.""" -__version__ = "13.0.0" +__version__ = '13.0.0' def _seg_0(): return [ (0x0, '3'), @@ -72,32 +71,32 @@ def _seg_0(): (0x3E, '3'), (0x3F, '3'), (0x40, '3'), - (0x41, 'M', u'a'), - (0x42, 'M', u'b'), - (0x43, 'M', u'c'), - (0x44, 'M', u'd'), - (0x45, 'M', u'e'), - (0x46, 'M', u'f'), - (0x47, 'M', u'g'), - (0x48, 'M', u'h'), - (0x49, 'M', u'i'), - (0x4A, 'M', u'j'), - (0x4B, 'M', u'k'), - (0x4C, 'M', u'l'), - (0x4D, 'M', u'm'), - (0x4E, 'M', u'n'), - (0x4F, 'M', u'o'), - (0x50, 'M', u'p'), - (0x51, 'M', u'q'), - (0x52, 'M', u'r'), - (0x53, 'M', u's'), - (0x54, 'M', u't'), - (0x55, 'M', u'u'), - (0x56, 'M', u'v'), - (0x57, 'M', u'w'), - (0x58, 'M', u'x'), - (0x59, 'M', u'y'), - (0x5A, 'M', u'z'), + (0x41, 'M', 'a'), + (0x42, 'M', 'b'), + (0x43, 'M', 'c'), + (0x44, 'M', 'd'), + (0x45, 'M', 'e'), + (0x46, 'M', 'f'), + (0x47, 'M', 'g'), + (0x48, 'M', 'h'), + (0x49, 'M', 'i'), + (0x4A, 'M', 'j'), + (0x4B, 'M', 'k'), + (0x4C, 'M', 'l'), + (0x4D, 'M', 'm'), + (0x4E, 'M', 'n'), + (0x4F, 'M', 'o'), + (0x50, 'M', 'p'), + (0x51, 'M', 'q'), + (0x52, 'M', 'r'), + (0x53, 'M', 's'), + (0x54, 'M', 't'), + (0x55, 'M', 'u'), + (0x56, 'M', 'v'), + (0x57, 'M', 'w'), + (0x58, 'M', 'x'), + (0x59, 'M', 'y'), + (0x5A, 'M', 'z'), (0x5B, '3'), (0x5C, '3'), (0x5D, '3'), @@ -171,7 +170,7 @@ def _seg_1(): (0x9D, 'X'), (0x9E, 'X'), (0x9F, 'X'), - (0xA0, '3', u' '), + (0xA0, '3', ' '), (0xA1, 'V'), (0xA2, 'V'), (0xA3, 'V'), @@ -179,66 +178,66 @@ def _seg_1(): (0xA5, 'V'), (0xA6, 'V'), (0xA7, 'V'), - (0xA8, '3', u' ̈'), + (0xA8, '3', ' ̈'), (0xA9, 'V'), - (0xAA, 'M', u'a'), + (0xAA, 'M', 'a'), (0xAB, 'V'), (0xAC, 'V'), (0xAD, 'I'), (0xAE, 'V'), - (0xAF, '3', u' ̄'), + (0xAF, '3', ' ̄'), (0xB0, 'V'), (0xB1, 'V'), - (0xB2, 'M', u'2'), - (0xB3, 'M', u'3'), - (0xB4, '3', u' ́'), - (0xB5, 'M', u'μ'), + (0xB2, 'M', '2'), + (0xB3, 'M', '3'), + (0xB4, '3', ' ́'), + (0xB5, 'M', 'μ'), (0xB6, 'V'), (0xB7, 'V'), - (0xB8, '3', u' ̧'), - (0xB9, 'M', u'1'), - (0xBA, 'M', u'o'), + (0xB8, '3', ' ̧'), + (0xB9, 'M', '1'), + (0xBA, 'M', 'o'), (0xBB, 'V'), - (0xBC, 'M', u'1⁄4'), - (0xBD, 'M', u'1⁄2'), - (0xBE, 'M', u'3⁄4'), + (0xBC, 'M', '1⁄4'), + (0xBD, 'M', '1⁄2'), + (0xBE, 'M', '3⁄4'), (0xBF, 'V'), - (0xC0, 'M', u'à'), - (0xC1, 'M', u'á'), - (0xC2, 'M', u'â'), - (0xC3, 'M', u'ã'), - (0xC4, 'M', u'ä'), - (0xC5, 'M', u'å'), - (0xC6, 'M', u'æ'), - (0xC7, 'M', u'ç'), + (0xC0, 'M', 'à'), + (0xC1, 'M', 'á'), + (0xC2, 'M', 'â'), + (0xC3, 'M', 'ã'), + (0xC4, 'M', 'ä'), + (0xC5, 'M', 'å'), + (0xC6, 'M', 'æ'), + (0xC7, 'M', 'ç'), ] def _seg_2(): return [ - (0xC8, 'M', u'è'), - (0xC9, 'M', u'é'), - (0xCA, 'M', u'ê'), - (0xCB, 'M', u'ë'), - (0xCC, 'M', u'ì'), - (0xCD, 'M', u'í'), - (0xCE, 'M', u'î'), - (0xCF, 'M', u'ï'), - (0xD0, 'M', u'ð'), - (0xD1, 'M', u'ñ'), - (0xD2, 'M', u'ò'), - (0xD3, 'M', u'ó'), - (0xD4, 'M', u'ô'), - (0xD5, 'M', u'õ'), - (0xD6, 'M', u'ö'), + (0xC8, 'M', 'è'), + (0xC9, 'M', 'é'), + (0xCA, 'M', 'ê'), + (0xCB, 'M', 'ë'), + (0xCC, 'M', 'ì'), + (0xCD, 'M', 'í'), + (0xCE, 'M', 'î'), + (0xCF, 'M', 'ï'), + (0xD0, 'M', 'ð'), + (0xD1, 'M', 'ñ'), + (0xD2, 'M', 'ò'), + (0xD3, 'M', 'ó'), + (0xD4, 'M', 'ô'), + (0xD5, 'M', 'õ'), + (0xD6, 'M', 'ö'), (0xD7, 'V'), - (0xD8, 'M', u'ø'), - (0xD9, 'M', u'ù'), - (0xDA, 'M', u'ú'), - (0xDB, 'M', u'û'), - (0xDC, 'M', u'ü'), - (0xDD, 'M', u'ý'), - (0xDE, 'M', u'þ'), - (0xDF, 'D', u'ss'), + (0xD8, 'M', 'ø'), + (0xD9, 'M', 'ù'), + (0xDA, 'M', 'ú'), + (0xDB, 'M', 'û'), + (0xDC, 'M', 'ü'), + (0xDD, 'M', 'ý'), + (0xDE, 'M', 'þ'), + (0xDF, 'D', 'ss'), (0xE0, 'V'), (0xE1, 'V'), (0xE2, 'V'), @@ -271,765 +270,765 @@ def _seg_2(): (0xFD, 'V'), (0xFE, 'V'), (0xFF, 'V'), - (0x100, 'M', u'ā'), + (0x100, 'M', 'ā'), (0x101, 'V'), - (0x102, 'M', u'ă'), + (0x102, 'M', 'ă'), (0x103, 'V'), - (0x104, 'M', u'ą'), + (0x104, 'M', 'ą'), (0x105, 'V'), - (0x106, 'M', u'ć'), + (0x106, 'M', 'ć'), (0x107, 'V'), - (0x108, 'M', u'ĉ'), + (0x108, 'M', 'ĉ'), (0x109, 'V'), - (0x10A, 'M', u'ċ'), + (0x10A, 'M', 'ċ'), (0x10B, 'V'), - (0x10C, 'M', u'č'), + (0x10C, 'M', 'č'), (0x10D, 'V'), - (0x10E, 'M', u'ď'), + (0x10E, 'M', 'ď'), (0x10F, 'V'), - (0x110, 'M', u'đ'), + (0x110, 'M', 'đ'), (0x111, 'V'), - (0x112, 'M', u'ē'), + (0x112, 'M', 'ē'), (0x113, 'V'), - (0x114, 'M', u'ĕ'), + (0x114, 'M', 'ĕ'), (0x115, 'V'), - (0x116, 'M', u'ė'), + (0x116, 'M', 'ė'), (0x117, 'V'), - (0x118, 'M', u'ę'), + (0x118, 'M', 'ę'), (0x119, 'V'), - (0x11A, 'M', u'ě'), + (0x11A, 'M', 'ě'), (0x11B, 'V'), - (0x11C, 'M', u'ĝ'), + (0x11C, 'M', 'ĝ'), (0x11D, 'V'), - (0x11E, 'M', u'ğ'), + (0x11E, 'M', 'ğ'), (0x11F, 'V'), - (0x120, 'M', u'ġ'), + (0x120, 'M', 'ġ'), (0x121, 'V'), - (0x122, 'M', u'ģ'), + (0x122, 'M', 'ģ'), (0x123, 'V'), - (0x124, 'M', u'ĥ'), + (0x124, 'M', 'ĥ'), (0x125, 'V'), - (0x126, 'M', u'ħ'), + (0x126, 'M', 'ħ'), (0x127, 'V'), - (0x128, 'M', u'ĩ'), + (0x128, 'M', 'ĩ'), (0x129, 'V'), - (0x12A, 'M', u'ī'), + (0x12A, 'M', 'ī'), (0x12B, 'V'), ] def _seg_3(): return [ - (0x12C, 'M', u'ĭ'), + (0x12C, 'M', 'ĭ'), (0x12D, 'V'), - (0x12E, 'M', u'į'), + (0x12E, 'M', 'į'), (0x12F, 'V'), - (0x130, 'M', u'i̇'), + (0x130, 'M', 'i̇'), (0x131, 'V'), - (0x132, 'M', u'ij'), - (0x134, 'M', u'ĵ'), + (0x132, 'M', 'ij'), + (0x134, 'M', 'ĵ'), (0x135, 'V'), - (0x136, 'M', u'ķ'), + (0x136, 'M', 'ķ'), (0x137, 'V'), - (0x139, 'M', u'ĺ'), + (0x139, 'M', 'ĺ'), (0x13A, 'V'), - (0x13B, 'M', u'ļ'), + (0x13B, 'M', 'ļ'), (0x13C, 'V'), - (0x13D, 'M', u'ľ'), + (0x13D, 'M', 'ľ'), (0x13E, 'V'), - (0x13F, 'M', u'l·'), - (0x141, 'M', u'ł'), + (0x13F, 'M', 'l·'), + (0x141, 'M', 'ł'), (0x142, 'V'), - (0x143, 'M', u'ń'), + (0x143, 'M', 'ń'), (0x144, 'V'), - (0x145, 'M', u'ņ'), + (0x145, 'M', 'ņ'), (0x146, 'V'), - (0x147, 'M', u'ň'), + (0x147, 'M', 'ň'), (0x148, 'V'), - (0x149, 'M', u'ʼn'), - (0x14A, 'M', u'ŋ'), + (0x149, 'M', 'ʼn'), + (0x14A, 'M', 'ŋ'), (0x14B, 'V'), - (0x14C, 'M', u'ō'), + (0x14C, 'M', 'ō'), (0x14D, 'V'), - (0x14E, 'M', u'ŏ'), + (0x14E, 'M', 'ŏ'), (0x14F, 'V'), - (0x150, 'M', u'ő'), + (0x150, 'M', 'ő'), (0x151, 'V'), - (0x152, 'M', u'œ'), + (0x152, 'M', 'œ'), (0x153, 'V'), - (0x154, 'M', u'ŕ'), + (0x154, 'M', 'ŕ'), (0x155, 'V'), - (0x156, 'M', u'ŗ'), + (0x156, 'M', 'ŗ'), (0x157, 'V'), - (0x158, 'M', u'ř'), + (0x158, 'M', 'ř'), (0x159, 'V'), - (0x15A, 'M', u'ś'), + (0x15A, 'M', 'ś'), (0x15B, 'V'), - (0x15C, 'M', u'ŝ'), + (0x15C, 'M', 'ŝ'), (0x15D, 'V'), - (0x15E, 'M', u'ş'), + (0x15E, 'M', 'ş'), (0x15F, 'V'), - (0x160, 'M', u'š'), + (0x160, 'M', 'š'), (0x161, 'V'), - (0x162, 'M', u'ţ'), + (0x162, 'M', 'ţ'), (0x163, 'V'), - (0x164, 'M', u'ť'), + (0x164, 'M', 'ť'), (0x165, 'V'), - (0x166, 'M', u'ŧ'), + (0x166, 'M', 'ŧ'), (0x167, 'V'), - (0x168, 'M', u'ũ'), + (0x168, 'M', 'ũ'), (0x169, 'V'), - (0x16A, 'M', u'ū'), + (0x16A, 'M', 'ū'), (0x16B, 'V'), - (0x16C, 'M', u'ŭ'), + (0x16C, 'M', 'ŭ'), (0x16D, 'V'), - (0x16E, 'M', u'ů'), + (0x16E, 'M', 'ů'), (0x16F, 'V'), - (0x170, 'M', u'ű'), + (0x170, 'M', 'ű'), (0x171, 'V'), - (0x172, 'M', u'ų'), + (0x172, 'M', 'ų'), (0x173, 'V'), - (0x174, 'M', u'ŵ'), + (0x174, 'M', 'ŵ'), (0x175, 'V'), - (0x176, 'M', u'ŷ'), + (0x176, 'M', 'ŷ'), (0x177, 'V'), - (0x178, 'M', u'ÿ'), - (0x179, 'M', u'ź'), + (0x178, 'M', 'ÿ'), + (0x179, 'M', 'ź'), (0x17A, 'V'), - (0x17B, 'M', u'ż'), + (0x17B, 'M', 'ż'), (0x17C, 'V'), - (0x17D, 'M', u'ž'), + (0x17D, 'M', 'ž'), (0x17E, 'V'), - (0x17F, 'M', u's'), + (0x17F, 'M', 's'), (0x180, 'V'), - (0x181, 'M', u'ɓ'), - (0x182, 'M', u'ƃ'), + (0x181, 'M', 'ɓ'), + (0x182, 'M', 'ƃ'), (0x183, 'V'), - (0x184, 'M', u'ƅ'), + (0x184, 'M', 'ƅ'), (0x185, 'V'), - (0x186, 'M', u'ɔ'), - (0x187, 'M', u'ƈ'), + (0x186, 'M', 'ɔ'), + (0x187, 'M', 'ƈ'), (0x188, 'V'), - (0x189, 'M', u'ɖ'), - (0x18A, 'M', u'ɗ'), - (0x18B, 'M', u'ƌ'), + (0x189, 'M', 'ɖ'), + (0x18A, 'M', 'ɗ'), + (0x18B, 'M', 'ƌ'), (0x18C, 'V'), - (0x18E, 'M', u'ǝ'), - (0x18F, 'M', u'ə'), - (0x190, 'M', u'ɛ'), - (0x191, 'M', u'ƒ'), + (0x18E, 'M', 'ǝ'), + (0x18F, 'M', 'ə'), + (0x190, 'M', 'ɛ'), + (0x191, 'M', 'ƒ'), (0x192, 'V'), - (0x193, 'M', u'ɠ'), + (0x193, 'M', 'ɠ'), ] def _seg_4(): return [ - (0x194, 'M', u'ɣ'), + (0x194, 'M', 'ɣ'), (0x195, 'V'), - (0x196, 'M', u'ɩ'), - (0x197, 'M', u'ɨ'), - (0x198, 'M', u'ƙ'), + (0x196, 'M', 'ɩ'), + (0x197, 'M', 'ɨ'), + (0x198, 'M', 'ƙ'), (0x199, 'V'), - (0x19C, 'M', u'ɯ'), - (0x19D, 'M', u'ɲ'), + (0x19C, 'M', 'ɯ'), + (0x19D, 'M', 'ɲ'), (0x19E, 'V'), - (0x19F, 'M', u'ɵ'), - (0x1A0, 'M', u'ơ'), + (0x19F, 'M', 'ɵ'), + (0x1A0, 'M', 'ơ'), (0x1A1, 'V'), - (0x1A2, 'M', u'ƣ'), + (0x1A2, 'M', 'ƣ'), (0x1A3, 'V'), - (0x1A4, 'M', u'ƥ'), + (0x1A4, 'M', 'ƥ'), (0x1A5, 'V'), - (0x1A6, 'M', u'ʀ'), - (0x1A7, 'M', u'ƨ'), + (0x1A6, 'M', 'ʀ'), + (0x1A7, 'M', 'ƨ'), (0x1A8, 'V'), - (0x1A9, 'M', u'ʃ'), + (0x1A9, 'M', 'ʃ'), (0x1AA, 'V'), - (0x1AC, 'M', u'ƭ'), + (0x1AC, 'M', 'ƭ'), (0x1AD, 'V'), - (0x1AE, 'M', u'ʈ'), - (0x1AF, 'M', u'ư'), + (0x1AE, 'M', 'ʈ'), + (0x1AF, 'M', 'ư'), (0x1B0, 'V'), - (0x1B1, 'M', u'ʊ'), - (0x1B2, 'M', u'ʋ'), - (0x1B3, 'M', u'ƴ'), + (0x1B1, 'M', 'ʊ'), + (0x1B2, 'M', 'ʋ'), + (0x1B3, 'M', 'ƴ'), (0x1B4, 'V'), - (0x1B5, 'M', u'ƶ'), + (0x1B5, 'M', 'ƶ'), (0x1B6, 'V'), - (0x1B7, 'M', u'ʒ'), - (0x1B8, 'M', u'ƹ'), + (0x1B7, 'M', 'ʒ'), + (0x1B8, 'M', 'ƹ'), (0x1B9, 'V'), - (0x1BC, 'M', u'ƽ'), + (0x1BC, 'M', 'ƽ'), (0x1BD, 'V'), - (0x1C4, 'M', u'dž'), - (0x1C7, 'M', u'lj'), - (0x1CA, 'M', u'nj'), - (0x1CD, 'M', u'ǎ'), + (0x1C4, 'M', 'dž'), + (0x1C7, 'M', 'lj'), + (0x1CA, 'M', 'nj'), + (0x1CD, 'M', 'ǎ'), (0x1CE, 'V'), - (0x1CF, 'M', u'ǐ'), + (0x1CF, 'M', 'ǐ'), (0x1D0, 'V'), - (0x1D1, 'M', u'ǒ'), + (0x1D1, 'M', 'ǒ'), (0x1D2, 'V'), - (0x1D3, 'M', u'ǔ'), + (0x1D3, 'M', 'ǔ'), (0x1D4, 'V'), - (0x1D5, 'M', u'ǖ'), + (0x1D5, 'M', 'ǖ'), (0x1D6, 'V'), - (0x1D7, 'M', u'ǘ'), + (0x1D7, 'M', 'ǘ'), (0x1D8, 'V'), - (0x1D9, 'M', u'ǚ'), + (0x1D9, 'M', 'ǚ'), (0x1DA, 'V'), - (0x1DB, 'M', u'ǜ'), + (0x1DB, 'M', 'ǜ'), (0x1DC, 'V'), - (0x1DE, 'M', u'ǟ'), + (0x1DE, 'M', 'ǟ'), (0x1DF, 'V'), - (0x1E0, 'M', u'ǡ'), + (0x1E0, 'M', 'ǡ'), (0x1E1, 'V'), - (0x1E2, 'M', u'ǣ'), + (0x1E2, 'M', 'ǣ'), (0x1E3, 'V'), - (0x1E4, 'M', u'ǥ'), + (0x1E4, 'M', 'ǥ'), (0x1E5, 'V'), - (0x1E6, 'M', u'ǧ'), + (0x1E6, 'M', 'ǧ'), (0x1E7, 'V'), - (0x1E8, 'M', u'ǩ'), + (0x1E8, 'M', 'ǩ'), (0x1E9, 'V'), - (0x1EA, 'M', u'ǫ'), + (0x1EA, 'M', 'ǫ'), (0x1EB, 'V'), - (0x1EC, 'M', u'ǭ'), + (0x1EC, 'M', 'ǭ'), (0x1ED, 'V'), - (0x1EE, 'M', u'ǯ'), + (0x1EE, 'M', 'ǯ'), (0x1EF, 'V'), - (0x1F1, 'M', u'dz'), - (0x1F4, 'M', u'ǵ'), + (0x1F1, 'M', 'dz'), + (0x1F4, 'M', 'ǵ'), (0x1F5, 'V'), - (0x1F6, 'M', u'ƕ'), - (0x1F7, 'M', u'ƿ'), - (0x1F8, 'M', u'ǹ'), + (0x1F6, 'M', 'ƕ'), + (0x1F7, 'M', 'ƿ'), + (0x1F8, 'M', 'ǹ'), (0x1F9, 'V'), - (0x1FA, 'M', u'ǻ'), + (0x1FA, 'M', 'ǻ'), (0x1FB, 'V'), - (0x1FC, 'M', u'ǽ'), + (0x1FC, 'M', 'ǽ'), (0x1FD, 'V'), - (0x1FE, 'M', u'ǿ'), + (0x1FE, 'M', 'ǿ'), (0x1FF, 'V'), - (0x200, 'M', u'ȁ'), + (0x200, 'M', 'ȁ'), (0x201, 'V'), - (0x202, 'M', u'ȃ'), + (0x202, 'M', 'ȃ'), (0x203, 'V'), - (0x204, 'M', u'ȅ'), + (0x204, 'M', 'ȅ'), (0x205, 'V'), - (0x206, 'M', u'ȇ'), + (0x206, 'M', 'ȇ'), (0x207, 'V'), - (0x208, 'M', u'ȉ'), + (0x208, 'M', 'ȉ'), (0x209, 'V'), - (0x20A, 'M', u'ȋ'), + (0x20A, 'M', 'ȋ'), (0x20B, 'V'), - (0x20C, 'M', u'ȍ'), + (0x20C, 'M', 'ȍ'), ] def _seg_5(): return [ (0x20D, 'V'), - (0x20E, 'M', u'ȏ'), + (0x20E, 'M', 'ȏ'), (0x20F, 'V'), - (0x210, 'M', u'ȑ'), + (0x210, 'M', 'ȑ'), (0x211, 'V'), - (0x212, 'M', u'ȓ'), + (0x212, 'M', 'ȓ'), (0x213, 'V'), - (0x214, 'M', u'ȕ'), + (0x214, 'M', 'ȕ'), (0x215, 'V'), - (0x216, 'M', u'ȗ'), + (0x216, 'M', 'ȗ'), (0x217, 'V'), - (0x218, 'M', u'ș'), + (0x218, 'M', 'ș'), (0x219, 'V'), - (0x21A, 'M', u'ț'), + (0x21A, 'M', 'ț'), (0x21B, 'V'), - (0x21C, 'M', u'ȝ'), + (0x21C, 'M', 'ȝ'), (0x21D, 'V'), - (0x21E, 'M', u'ȟ'), + (0x21E, 'M', 'ȟ'), (0x21F, 'V'), - (0x220, 'M', u'ƞ'), + (0x220, 'M', 'ƞ'), (0x221, 'V'), - (0x222, 'M', u'ȣ'), + (0x222, 'M', 'ȣ'), (0x223, 'V'), - (0x224, 'M', u'ȥ'), + (0x224, 'M', 'ȥ'), (0x225, 'V'), - (0x226, 'M', u'ȧ'), + (0x226, 'M', 'ȧ'), (0x227, 'V'), - (0x228, 'M', u'ȩ'), + (0x228, 'M', 'ȩ'), (0x229, 'V'), - (0x22A, 'M', u'ȫ'), + (0x22A, 'M', 'ȫ'), (0x22B, 'V'), - (0x22C, 'M', u'ȭ'), + (0x22C, 'M', 'ȭ'), (0x22D, 'V'), - (0x22E, 'M', u'ȯ'), + (0x22E, 'M', 'ȯ'), (0x22F, 'V'), - (0x230, 'M', u'ȱ'), + (0x230, 'M', 'ȱ'), (0x231, 'V'), - (0x232, 'M', u'ȳ'), + (0x232, 'M', 'ȳ'), (0x233, 'V'), - (0x23A, 'M', u'ⱥ'), - (0x23B, 'M', u'ȼ'), + (0x23A, 'M', 'ⱥ'), + (0x23B, 'M', 'ȼ'), (0x23C, 'V'), - (0x23D, 'M', u'ƚ'), - (0x23E, 'M', u'ⱦ'), + (0x23D, 'M', 'ƚ'), + (0x23E, 'M', 'ⱦ'), (0x23F, 'V'), - (0x241, 'M', u'ɂ'), + (0x241, 'M', 'ɂ'), (0x242, 'V'), - (0x243, 'M', u'ƀ'), - (0x244, 'M', u'ʉ'), - (0x245, 'M', u'ʌ'), - (0x246, 'M', u'ɇ'), + (0x243, 'M', 'ƀ'), + (0x244, 'M', 'ʉ'), + (0x245, 'M', 'ʌ'), + (0x246, 'M', 'ɇ'), (0x247, 'V'), - (0x248, 'M', u'ɉ'), + (0x248, 'M', 'ɉ'), (0x249, 'V'), - (0x24A, 'M', u'ɋ'), + (0x24A, 'M', 'ɋ'), (0x24B, 'V'), - (0x24C, 'M', u'ɍ'), + (0x24C, 'M', 'ɍ'), (0x24D, 'V'), - (0x24E, 'M', u'ɏ'), + (0x24E, 'M', 'ɏ'), (0x24F, 'V'), - (0x2B0, 'M', u'h'), - (0x2B1, 'M', u'ɦ'), - (0x2B2, 'M', u'j'), - (0x2B3, 'M', u'r'), - (0x2B4, 'M', u'ɹ'), - (0x2B5, 'M', u'ɻ'), - (0x2B6, 'M', u'ʁ'), - (0x2B7, 'M', u'w'), - (0x2B8, 'M', u'y'), + (0x2B0, 'M', 'h'), + (0x2B1, 'M', 'ɦ'), + (0x2B2, 'M', 'j'), + (0x2B3, 'M', 'r'), + (0x2B4, 'M', 'ɹ'), + (0x2B5, 'M', 'ɻ'), + (0x2B6, 'M', 'ʁ'), + (0x2B7, 'M', 'w'), + (0x2B8, 'M', 'y'), (0x2B9, 'V'), - (0x2D8, '3', u' ̆'), - (0x2D9, '3', u' ̇'), - (0x2DA, '3', u' ̊'), - (0x2DB, '3', u' ̨'), - (0x2DC, '3', u' ̃'), - (0x2DD, '3', u' ̋'), + (0x2D8, '3', ' ̆'), + (0x2D9, '3', ' ̇'), + (0x2DA, '3', ' ̊'), + (0x2DB, '3', ' ̨'), + (0x2DC, '3', ' ̃'), + (0x2DD, '3', ' ̋'), (0x2DE, 'V'), - (0x2E0, 'M', u'ɣ'), - (0x2E1, 'M', u'l'), - (0x2E2, 'M', u's'), - (0x2E3, 'M', u'x'), - (0x2E4, 'M', u'ʕ'), + (0x2E0, 'M', 'ɣ'), + (0x2E1, 'M', 'l'), + (0x2E2, 'M', 's'), + (0x2E3, 'M', 'x'), + (0x2E4, 'M', 'ʕ'), (0x2E5, 'V'), - (0x340, 'M', u'̀'), - (0x341, 'M', u'́'), + (0x340, 'M', '̀'), + (0x341, 'M', '́'), (0x342, 'V'), - (0x343, 'M', u'̓'), - (0x344, 'M', u'̈́'), - (0x345, 'M', u'ι'), + (0x343, 'M', '̓'), + (0x344, 'M', '̈́'), + (0x345, 'M', 'ι'), (0x346, 'V'), (0x34F, 'I'), (0x350, 'V'), - (0x370, 'M', u'ͱ'), + (0x370, 'M', 'ͱ'), (0x371, 'V'), - (0x372, 'M', u'ͳ'), + (0x372, 'M', 'ͳ'), (0x373, 'V'), - (0x374, 'M', u'ʹ'), + (0x374, 'M', 'ʹ'), (0x375, 'V'), - (0x376, 'M', u'ͷ'), + (0x376, 'M', 'ͷ'), (0x377, 'V'), ] def _seg_6(): return [ (0x378, 'X'), - (0x37A, '3', u' ι'), + (0x37A, '3', ' ι'), (0x37B, 'V'), - (0x37E, '3', u';'), - (0x37F, 'M', u'ϳ'), + (0x37E, '3', ';'), + (0x37F, 'M', 'ϳ'), (0x380, 'X'), - (0x384, '3', u' ́'), - (0x385, '3', u' ̈́'), - (0x386, 'M', u'ά'), - (0x387, 'M', u'·'), - (0x388, 'M', u'έ'), - (0x389, 'M', u'ή'), - (0x38A, 'M', u'ί'), + (0x384, '3', ' ́'), + (0x385, '3', ' ̈́'), + (0x386, 'M', 'ά'), + (0x387, 'M', '·'), + (0x388, 'M', 'έ'), + (0x389, 'M', 'ή'), + (0x38A, 'M', 'ί'), (0x38B, 'X'), - (0x38C, 'M', u'ό'), + (0x38C, 'M', 'ό'), (0x38D, 'X'), - (0x38E, 'M', u'ύ'), - (0x38F, 'M', u'ώ'), + (0x38E, 'M', 'ύ'), + (0x38F, 'M', 'ώ'), (0x390, 'V'), - (0x391, 'M', u'α'), - (0x392, 'M', u'β'), - (0x393, 'M', u'γ'), - (0x394, 'M', u'δ'), - (0x395, 'M', u'ε'), - (0x396, 'M', u'ζ'), - (0x397, 'M', u'η'), - (0x398, 'M', u'θ'), - (0x399, 'M', u'ι'), - (0x39A, 'M', u'κ'), - (0x39B, 'M', u'λ'), - (0x39C, 'M', u'μ'), - (0x39D, 'M', u'ν'), - (0x39E, 'M', u'ξ'), - (0x39F, 'M', u'ο'), - (0x3A0, 'M', u'π'), - (0x3A1, 'M', u'ρ'), + (0x391, 'M', 'α'), + (0x392, 'M', 'β'), + (0x393, 'M', 'γ'), + (0x394, 'M', 'δ'), + (0x395, 'M', 'ε'), + (0x396, 'M', 'ζ'), + (0x397, 'M', 'η'), + (0x398, 'M', 'θ'), + (0x399, 'M', 'ι'), + (0x39A, 'M', 'κ'), + (0x39B, 'M', 'λ'), + (0x39C, 'M', 'μ'), + (0x39D, 'M', 'ν'), + (0x39E, 'M', 'ξ'), + (0x39F, 'M', 'ο'), + (0x3A0, 'M', 'π'), + (0x3A1, 'M', 'ρ'), (0x3A2, 'X'), - (0x3A3, 'M', u'σ'), - (0x3A4, 'M', u'τ'), - (0x3A5, 'M', u'υ'), - (0x3A6, 'M', u'φ'), - (0x3A7, 'M', u'χ'), - (0x3A8, 'M', u'ψ'), - (0x3A9, 'M', u'ω'), - (0x3AA, 'M', u'ϊ'), - (0x3AB, 'M', u'ϋ'), + (0x3A3, 'M', 'σ'), + (0x3A4, 'M', 'τ'), + (0x3A5, 'M', 'υ'), + (0x3A6, 'M', 'φ'), + (0x3A7, 'M', 'χ'), + (0x3A8, 'M', 'ψ'), + (0x3A9, 'M', 'ω'), + (0x3AA, 'M', 'ϊ'), + (0x3AB, 'M', 'ϋ'), (0x3AC, 'V'), - (0x3C2, 'D', u'σ'), + (0x3C2, 'D', 'σ'), (0x3C3, 'V'), - (0x3CF, 'M', u'ϗ'), - (0x3D0, 'M', u'β'), - (0x3D1, 'M', u'θ'), - (0x3D2, 'M', u'υ'), - (0x3D3, 'M', u'ύ'), - (0x3D4, 'M', u'ϋ'), - (0x3D5, 'M', u'φ'), - (0x3D6, 'M', u'π'), + (0x3CF, 'M', 'ϗ'), + (0x3D0, 'M', 'β'), + (0x3D1, 'M', 'θ'), + (0x3D2, 'M', 'υ'), + (0x3D3, 'M', 'ύ'), + (0x3D4, 'M', 'ϋ'), + (0x3D5, 'M', 'φ'), + (0x3D6, 'M', 'π'), (0x3D7, 'V'), - (0x3D8, 'M', u'ϙ'), + (0x3D8, 'M', 'ϙ'), (0x3D9, 'V'), - (0x3DA, 'M', u'ϛ'), + (0x3DA, 'M', 'ϛ'), (0x3DB, 'V'), - (0x3DC, 'M', u'ϝ'), + (0x3DC, 'M', 'ϝ'), (0x3DD, 'V'), - (0x3DE, 'M', u'ϟ'), + (0x3DE, 'M', 'ϟ'), (0x3DF, 'V'), - (0x3E0, 'M', u'ϡ'), + (0x3E0, 'M', 'ϡ'), (0x3E1, 'V'), - (0x3E2, 'M', u'ϣ'), + (0x3E2, 'M', 'ϣ'), (0x3E3, 'V'), - (0x3E4, 'M', u'ϥ'), + (0x3E4, 'M', 'ϥ'), (0x3E5, 'V'), - (0x3E6, 'M', u'ϧ'), + (0x3E6, 'M', 'ϧ'), (0x3E7, 'V'), - (0x3E8, 'M', u'ϩ'), + (0x3E8, 'M', 'ϩ'), (0x3E9, 'V'), - (0x3EA, 'M', u'ϫ'), + (0x3EA, 'M', 'ϫ'), (0x3EB, 'V'), - (0x3EC, 'M', u'ϭ'), + (0x3EC, 'M', 'ϭ'), (0x3ED, 'V'), - (0x3EE, 'M', u'ϯ'), + (0x3EE, 'M', 'ϯ'), (0x3EF, 'V'), - (0x3F0, 'M', u'κ'), - (0x3F1, 'M', u'ρ'), - (0x3F2, 'M', u'σ'), + (0x3F0, 'M', 'κ'), + (0x3F1, 'M', 'ρ'), + (0x3F2, 'M', 'σ'), (0x3F3, 'V'), - (0x3F4, 'M', u'θ'), - (0x3F5, 'M', u'ε'), + (0x3F4, 'M', 'θ'), + (0x3F5, 'M', 'ε'), (0x3F6, 'V'), - (0x3F7, 'M', u'ϸ'), + (0x3F7, 'M', 'ϸ'), (0x3F8, 'V'), - (0x3F9, 'M', u'σ'), - (0x3FA, 'M', u'ϻ'), + (0x3F9, 'M', 'σ'), + (0x3FA, 'M', 'ϻ'), (0x3FB, 'V'), - (0x3FD, 'M', u'ͻ'), - (0x3FE, 'M', u'ͼ'), - (0x3FF, 'M', u'ͽ'), - (0x400, 'M', u'ѐ'), - (0x401, 'M', u'ё'), - (0x402, 'M', u'ђ'), + (0x3FD, 'M', 'ͻ'), + (0x3FE, 'M', 'ͼ'), + (0x3FF, 'M', 'ͽ'), + (0x400, 'M', 'ѐ'), + (0x401, 'M', 'ё'), + (0x402, 'M', 'ђ'), ] def _seg_7(): return [ - (0x403, 'M', u'ѓ'), - (0x404, 'M', u'є'), - (0x405, 'M', u'ѕ'), - (0x406, 'M', u'і'), - (0x407, 'M', u'ї'), - (0x408, 'M', u'ј'), - (0x409, 'M', u'љ'), - (0x40A, 'M', u'њ'), - (0x40B, 'M', u'ћ'), - (0x40C, 'M', u'ќ'), - (0x40D, 'M', u'ѝ'), - (0x40E, 'M', u'ў'), - (0x40F, 'M', u'џ'), - (0x410, 'M', u'а'), - (0x411, 'M', u'б'), - (0x412, 'M', u'в'), - (0x413, 'M', u'г'), - (0x414, 'M', u'д'), - (0x415, 'M', u'е'), - (0x416, 'M', u'ж'), - (0x417, 'M', u'з'), - (0x418, 'M', u'и'), - (0x419, 'M', u'й'), - (0x41A, 'M', u'к'), - (0x41B, 'M', u'л'), - (0x41C, 'M', u'м'), - (0x41D, 'M', u'н'), - (0x41E, 'M', u'о'), - (0x41F, 'M', u'п'), - (0x420, 'M', u'р'), - (0x421, 'M', u'с'), - (0x422, 'M', u'т'), - (0x423, 'M', u'у'), - (0x424, 'M', u'ф'), - (0x425, 'M', u'х'), - (0x426, 'M', u'ц'), - (0x427, 'M', u'ч'), - (0x428, 'M', u'ш'), - (0x429, 'M', u'щ'), - (0x42A, 'M', u'ъ'), - (0x42B, 'M', u'ы'), - (0x42C, 'M', u'ь'), - (0x42D, 'M', u'э'), - (0x42E, 'M', u'ю'), - (0x42F, 'M', u'я'), + (0x403, 'M', 'ѓ'), + (0x404, 'M', 'є'), + (0x405, 'M', 'ѕ'), + (0x406, 'M', 'і'), + (0x407, 'M', 'ї'), + (0x408, 'M', 'ј'), + (0x409, 'M', 'љ'), + (0x40A, 'M', 'њ'), + (0x40B, 'M', 'ћ'), + (0x40C, 'M', 'ќ'), + (0x40D, 'M', 'ѝ'), + (0x40E, 'M', 'ў'), + (0x40F, 'M', 'џ'), + (0x410, 'M', 'а'), + (0x411, 'M', 'б'), + (0x412, 'M', 'в'), + (0x413, 'M', 'г'), + (0x414, 'M', 'д'), + (0x415, 'M', 'е'), + (0x416, 'M', 'ж'), + (0x417, 'M', 'з'), + (0x418, 'M', 'и'), + (0x419, 'M', 'й'), + (0x41A, 'M', 'к'), + (0x41B, 'M', 'л'), + (0x41C, 'M', 'м'), + (0x41D, 'M', 'н'), + (0x41E, 'M', 'о'), + (0x41F, 'M', 'п'), + (0x420, 'M', 'р'), + (0x421, 'M', 'с'), + (0x422, 'M', 'т'), + (0x423, 'M', 'у'), + (0x424, 'M', 'ф'), + (0x425, 'M', 'х'), + (0x426, 'M', 'ц'), + (0x427, 'M', 'ч'), + (0x428, 'M', 'ш'), + (0x429, 'M', 'щ'), + (0x42A, 'M', 'ъ'), + (0x42B, 'M', 'ы'), + (0x42C, 'M', 'ь'), + (0x42D, 'M', 'э'), + (0x42E, 'M', 'ю'), + (0x42F, 'M', 'я'), (0x430, 'V'), - (0x460, 'M', u'ѡ'), + (0x460, 'M', 'ѡ'), (0x461, 'V'), - (0x462, 'M', u'ѣ'), + (0x462, 'M', 'ѣ'), (0x463, 'V'), - (0x464, 'M', u'ѥ'), + (0x464, 'M', 'ѥ'), (0x465, 'V'), - (0x466, 'M', u'ѧ'), + (0x466, 'M', 'ѧ'), (0x467, 'V'), - (0x468, 'M', u'ѩ'), + (0x468, 'M', 'ѩ'), (0x469, 'V'), - (0x46A, 'M', u'ѫ'), + (0x46A, 'M', 'ѫ'), (0x46B, 'V'), - (0x46C, 'M', u'ѭ'), + (0x46C, 'M', 'ѭ'), (0x46D, 'V'), - (0x46E, 'M', u'ѯ'), + (0x46E, 'M', 'ѯ'), (0x46F, 'V'), - (0x470, 'M', u'ѱ'), + (0x470, 'M', 'ѱ'), (0x471, 'V'), - (0x472, 'M', u'ѳ'), + (0x472, 'M', 'ѳ'), (0x473, 'V'), - (0x474, 'M', u'ѵ'), + (0x474, 'M', 'ѵ'), (0x475, 'V'), - (0x476, 'M', u'ѷ'), + (0x476, 'M', 'ѷ'), (0x477, 'V'), - (0x478, 'M', u'ѹ'), + (0x478, 'M', 'ѹ'), (0x479, 'V'), - (0x47A, 'M', u'ѻ'), + (0x47A, 'M', 'ѻ'), (0x47B, 'V'), - (0x47C, 'M', u'ѽ'), + (0x47C, 'M', 'ѽ'), (0x47D, 'V'), - (0x47E, 'M', u'ѿ'), + (0x47E, 'M', 'ѿ'), (0x47F, 'V'), - (0x480, 'M', u'ҁ'), + (0x480, 'M', 'ҁ'), (0x481, 'V'), - (0x48A, 'M', u'ҋ'), + (0x48A, 'M', 'ҋ'), (0x48B, 'V'), - (0x48C, 'M', u'ҍ'), + (0x48C, 'M', 'ҍ'), (0x48D, 'V'), - (0x48E, 'M', u'ҏ'), + (0x48E, 'M', 'ҏ'), (0x48F, 'V'), - (0x490, 'M', u'ґ'), + (0x490, 'M', 'ґ'), (0x491, 'V'), - (0x492, 'M', u'ғ'), + (0x492, 'M', 'ғ'), (0x493, 'V'), - (0x494, 'M', u'ҕ'), + (0x494, 'M', 'ҕ'), (0x495, 'V'), - (0x496, 'M', u'җ'), + (0x496, 'M', 'җ'), (0x497, 'V'), - (0x498, 'M', u'ҙ'), + (0x498, 'M', 'ҙ'), (0x499, 'V'), - (0x49A, 'M', u'қ'), + (0x49A, 'M', 'қ'), (0x49B, 'V'), - (0x49C, 'M', u'ҝ'), + (0x49C, 'M', 'ҝ'), (0x49D, 'V'), ] def _seg_8(): return [ - (0x49E, 'M', u'ҟ'), + (0x49E, 'M', 'ҟ'), (0x49F, 'V'), - (0x4A0, 'M', u'ҡ'), + (0x4A0, 'M', 'ҡ'), (0x4A1, 'V'), - (0x4A2, 'M', u'ң'), + (0x4A2, 'M', 'ң'), (0x4A3, 'V'), - (0x4A4, 'M', u'ҥ'), + (0x4A4, 'M', 'ҥ'), (0x4A5, 'V'), - (0x4A6, 'M', u'ҧ'), + (0x4A6, 'M', 'ҧ'), (0x4A7, 'V'), - (0x4A8, 'M', u'ҩ'), + (0x4A8, 'M', 'ҩ'), (0x4A9, 'V'), - (0x4AA, 'M', u'ҫ'), + (0x4AA, 'M', 'ҫ'), (0x4AB, 'V'), - (0x4AC, 'M', u'ҭ'), + (0x4AC, 'M', 'ҭ'), (0x4AD, 'V'), - (0x4AE, 'M', u'ү'), + (0x4AE, 'M', 'ү'), (0x4AF, 'V'), - (0x4B0, 'M', u'ұ'), + (0x4B0, 'M', 'ұ'), (0x4B1, 'V'), - (0x4B2, 'M', u'ҳ'), + (0x4B2, 'M', 'ҳ'), (0x4B3, 'V'), - (0x4B4, 'M', u'ҵ'), + (0x4B4, 'M', 'ҵ'), (0x4B5, 'V'), - (0x4B6, 'M', u'ҷ'), + (0x4B6, 'M', 'ҷ'), (0x4B7, 'V'), - (0x4B8, 'M', u'ҹ'), + (0x4B8, 'M', 'ҹ'), (0x4B9, 'V'), - (0x4BA, 'M', u'һ'), + (0x4BA, 'M', 'һ'), (0x4BB, 'V'), - (0x4BC, 'M', u'ҽ'), + (0x4BC, 'M', 'ҽ'), (0x4BD, 'V'), - (0x4BE, 'M', u'ҿ'), + (0x4BE, 'M', 'ҿ'), (0x4BF, 'V'), (0x4C0, 'X'), - (0x4C1, 'M', u'ӂ'), + (0x4C1, 'M', 'ӂ'), (0x4C2, 'V'), - (0x4C3, 'M', u'ӄ'), + (0x4C3, 'M', 'ӄ'), (0x4C4, 'V'), - (0x4C5, 'M', u'ӆ'), + (0x4C5, 'M', 'ӆ'), (0x4C6, 'V'), - (0x4C7, 'M', u'ӈ'), + (0x4C7, 'M', 'ӈ'), (0x4C8, 'V'), - (0x4C9, 'M', u'ӊ'), + (0x4C9, 'M', 'ӊ'), (0x4CA, 'V'), - (0x4CB, 'M', u'ӌ'), + (0x4CB, 'M', 'ӌ'), (0x4CC, 'V'), - (0x4CD, 'M', u'ӎ'), + (0x4CD, 'M', 'ӎ'), (0x4CE, 'V'), - (0x4D0, 'M', u'ӑ'), + (0x4D0, 'M', 'ӑ'), (0x4D1, 'V'), - (0x4D2, 'M', u'ӓ'), + (0x4D2, 'M', 'ӓ'), (0x4D3, 'V'), - (0x4D4, 'M', u'ӕ'), + (0x4D4, 'M', 'ӕ'), (0x4D5, 'V'), - (0x4D6, 'M', u'ӗ'), + (0x4D6, 'M', 'ӗ'), (0x4D7, 'V'), - (0x4D8, 'M', u'ә'), + (0x4D8, 'M', 'ә'), (0x4D9, 'V'), - (0x4DA, 'M', u'ӛ'), + (0x4DA, 'M', 'ӛ'), (0x4DB, 'V'), - (0x4DC, 'M', u'ӝ'), + (0x4DC, 'M', 'ӝ'), (0x4DD, 'V'), - (0x4DE, 'M', u'ӟ'), + (0x4DE, 'M', 'ӟ'), (0x4DF, 'V'), - (0x4E0, 'M', u'ӡ'), + (0x4E0, 'M', 'ӡ'), (0x4E1, 'V'), - (0x4E2, 'M', u'ӣ'), + (0x4E2, 'M', 'ӣ'), (0x4E3, 'V'), - (0x4E4, 'M', u'ӥ'), + (0x4E4, 'M', 'ӥ'), (0x4E5, 'V'), - (0x4E6, 'M', u'ӧ'), + (0x4E6, 'M', 'ӧ'), (0x4E7, 'V'), - (0x4E8, 'M', u'ө'), + (0x4E8, 'M', 'ө'), (0x4E9, 'V'), - (0x4EA, 'M', u'ӫ'), + (0x4EA, 'M', 'ӫ'), (0x4EB, 'V'), - (0x4EC, 'M', u'ӭ'), + (0x4EC, 'M', 'ӭ'), (0x4ED, 'V'), - (0x4EE, 'M', u'ӯ'), + (0x4EE, 'M', 'ӯ'), (0x4EF, 'V'), - (0x4F0, 'M', u'ӱ'), + (0x4F0, 'M', 'ӱ'), (0x4F1, 'V'), - (0x4F2, 'M', u'ӳ'), + (0x4F2, 'M', 'ӳ'), (0x4F3, 'V'), - (0x4F4, 'M', u'ӵ'), + (0x4F4, 'M', 'ӵ'), (0x4F5, 'V'), - (0x4F6, 'M', u'ӷ'), + (0x4F6, 'M', 'ӷ'), (0x4F7, 'V'), - (0x4F8, 'M', u'ӹ'), + (0x4F8, 'M', 'ӹ'), (0x4F9, 'V'), - (0x4FA, 'M', u'ӻ'), + (0x4FA, 'M', 'ӻ'), (0x4FB, 'V'), - (0x4FC, 'M', u'ӽ'), + (0x4FC, 'M', 'ӽ'), (0x4FD, 'V'), - (0x4FE, 'M', u'ӿ'), + (0x4FE, 'M', 'ӿ'), (0x4FF, 'V'), - (0x500, 'M', u'ԁ'), + (0x500, 'M', 'ԁ'), (0x501, 'V'), - (0x502, 'M', u'ԃ'), + (0x502, 'M', 'ԃ'), ] def _seg_9(): return [ (0x503, 'V'), - (0x504, 'M', u'ԅ'), + (0x504, 'M', 'ԅ'), (0x505, 'V'), - (0x506, 'M', u'ԇ'), + (0x506, 'M', 'ԇ'), (0x507, 'V'), - (0x508, 'M', u'ԉ'), + (0x508, 'M', 'ԉ'), (0x509, 'V'), - (0x50A, 'M', u'ԋ'), + (0x50A, 'M', 'ԋ'), (0x50B, 'V'), - (0x50C, 'M', u'ԍ'), + (0x50C, 'M', 'ԍ'), (0x50D, 'V'), - (0x50E, 'M', u'ԏ'), + (0x50E, 'M', 'ԏ'), (0x50F, 'V'), - (0x510, 'M', u'ԑ'), + (0x510, 'M', 'ԑ'), (0x511, 'V'), - (0x512, 'M', u'ԓ'), + (0x512, 'M', 'ԓ'), (0x513, 'V'), - (0x514, 'M', u'ԕ'), + (0x514, 'M', 'ԕ'), (0x515, 'V'), - (0x516, 'M', u'ԗ'), + (0x516, 'M', 'ԗ'), (0x517, 'V'), - (0x518, 'M', u'ԙ'), + (0x518, 'M', 'ԙ'), (0x519, 'V'), - (0x51A, 'M', u'ԛ'), + (0x51A, 'M', 'ԛ'), (0x51B, 'V'), - (0x51C, 'M', u'ԝ'), + (0x51C, 'M', 'ԝ'), (0x51D, 'V'), - (0x51E, 'M', u'ԟ'), + (0x51E, 'M', 'ԟ'), (0x51F, 'V'), - (0x520, 'M', u'ԡ'), + (0x520, 'M', 'ԡ'), (0x521, 'V'), - (0x522, 'M', u'ԣ'), + (0x522, 'M', 'ԣ'), (0x523, 'V'), - (0x524, 'M', u'ԥ'), + (0x524, 'M', 'ԥ'), (0x525, 'V'), - (0x526, 'M', u'ԧ'), + (0x526, 'M', 'ԧ'), (0x527, 'V'), - (0x528, 'M', u'ԩ'), + (0x528, 'M', 'ԩ'), (0x529, 'V'), - (0x52A, 'M', u'ԫ'), + (0x52A, 'M', 'ԫ'), (0x52B, 'V'), - (0x52C, 'M', u'ԭ'), + (0x52C, 'M', 'ԭ'), (0x52D, 'V'), - (0x52E, 'M', u'ԯ'), + (0x52E, 'M', 'ԯ'), (0x52F, 'V'), (0x530, 'X'), - (0x531, 'M', u'ա'), - (0x532, 'M', u'բ'), - (0x533, 'M', u'գ'), - (0x534, 'M', u'դ'), - (0x535, 'M', u'ե'), - (0x536, 'M', u'զ'), - (0x537, 'M', u'է'), - (0x538, 'M', u'ը'), - (0x539, 'M', u'թ'), - (0x53A, 'M', u'ժ'), - (0x53B, 'M', u'ի'), - (0x53C, 'M', u'լ'), - (0x53D, 'M', u'խ'), - (0x53E, 'M', u'ծ'), - (0x53F, 'M', u'կ'), - (0x540, 'M', u'հ'), - (0x541, 'M', u'ձ'), - (0x542, 'M', u'ղ'), - (0x543, 'M', u'ճ'), - (0x544, 'M', u'մ'), - (0x545, 'M', u'յ'), - (0x546, 'M', u'ն'), - (0x547, 'M', u'շ'), - (0x548, 'M', u'ո'), - (0x549, 'M', u'չ'), - (0x54A, 'M', u'պ'), - (0x54B, 'M', u'ջ'), - (0x54C, 'M', u'ռ'), - (0x54D, 'M', u'ս'), - (0x54E, 'M', u'վ'), - (0x54F, 'M', u'տ'), - (0x550, 'M', u'ր'), - (0x551, 'M', u'ց'), - (0x552, 'M', u'ւ'), - (0x553, 'M', u'փ'), - (0x554, 'M', u'ք'), - (0x555, 'M', u'օ'), - (0x556, 'M', u'ֆ'), + (0x531, 'M', 'ա'), + (0x532, 'M', 'բ'), + (0x533, 'M', 'գ'), + (0x534, 'M', 'դ'), + (0x535, 'M', 'ե'), + (0x536, 'M', 'զ'), + (0x537, 'M', 'է'), + (0x538, 'M', 'ը'), + (0x539, 'M', 'թ'), + (0x53A, 'M', 'ժ'), + (0x53B, 'M', 'ի'), + (0x53C, 'M', 'լ'), + (0x53D, 'M', 'խ'), + (0x53E, 'M', 'ծ'), + (0x53F, 'M', 'կ'), + (0x540, 'M', 'հ'), + (0x541, 'M', 'ձ'), + (0x542, 'M', 'ղ'), + (0x543, 'M', 'ճ'), + (0x544, 'M', 'մ'), + (0x545, 'M', 'յ'), + (0x546, 'M', 'ն'), + (0x547, 'M', 'շ'), + (0x548, 'M', 'ո'), + (0x549, 'M', 'չ'), + (0x54A, 'M', 'պ'), + (0x54B, 'M', 'ջ'), + (0x54C, 'M', 'ռ'), + (0x54D, 'M', 'ս'), + (0x54E, 'M', 'վ'), + (0x54F, 'M', 'տ'), + (0x550, 'M', 'ր'), + (0x551, 'M', 'ց'), + (0x552, 'M', 'ւ'), + (0x553, 'M', 'փ'), + (0x554, 'M', 'ք'), + (0x555, 'M', 'օ'), + (0x556, 'M', 'ֆ'), (0x557, 'X'), (0x559, 'V'), - (0x587, 'M', u'եւ'), + (0x587, 'M', 'եւ'), (0x588, 'V'), (0x58B, 'X'), (0x58D, 'V'), @@ -1047,10 +1046,10 @@ def _seg_9(): def _seg_10(): return [ - (0x675, 'M', u'اٴ'), - (0x676, 'M', u'وٴ'), - (0x677, 'M', u'ۇٴ'), - (0x678, 'M', u'يٴ'), + (0x675, 'M', 'اٴ'), + (0x676, 'M', 'وٴ'), + (0x677, 'M', 'ۇٴ'), + (0x678, 'M', 'يٴ'), (0x679, 'V'), (0x6DD, 'X'), (0x6DE, 'V'), @@ -1078,14 +1077,14 @@ def _seg_10(): (0x8D3, 'V'), (0x8E2, 'X'), (0x8E3, 'V'), - (0x958, 'M', u'क़'), - (0x959, 'M', u'ख़'), - (0x95A, 'M', u'ग़'), - (0x95B, 'M', u'ज़'), - (0x95C, 'M', u'ड़'), - (0x95D, 'M', u'ढ़'), - (0x95E, 'M', u'फ़'), - (0x95F, 'M', u'य़'), + (0x958, 'M', 'क़'), + (0x959, 'M', 'ख़'), + (0x95A, 'M', 'ग़'), + (0x95B, 'M', 'ज़'), + (0x95C, 'M', 'ड़'), + (0x95D, 'M', 'ढ़'), + (0x95E, 'M', 'फ़'), + (0x95F, 'M', 'य़'), (0x960, 'V'), (0x984, 'X'), (0x985, 'V'), @@ -1108,10 +1107,10 @@ def _seg_10(): (0x9CF, 'X'), (0x9D7, 'V'), (0x9D8, 'X'), - (0x9DC, 'M', u'ড়'), - (0x9DD, 'M', u'ঢ়'), + (0x9DC, 'M', 'ড়'), + (0x9DD, 'M', 'ঢ়'), (0x9DE, 'X'), - (0x9DF, 'M', u'য়'), + (0x9DF, 'M', 'য়'), (0x9E0, 'V'), (0x9E4, 'X'), (0x9E6, 'V'), @@ -1127,10 +1126,10 @@ def _seg_10(): (0xA2A, 'V'), (0xA31, 'X'), (0xA32, 'V'), - (0xA33, 'M', u'ਲ਼'), + (0xA33, 'M', 'ਲ਼'), (0xA34, 'X'), (0xA35, 'V'), - (0xA36, 'M', u'ਸ਼'), + (0xA36, 'M', 'ਸ਼'), (0xA37, 'X'), (0xA38, 'V'), (0xA3A, 'X'), @@ -1144,16 +1143,16 @@ def _seg_10(): (0xA4E, 'X'), (0xA51, 'V'), (0xA52, 'X'), - (0xA59, 'M', u'ਖ਼'), - (0xA5A, 'M', u'ਗ਼'), - (0xA5B, 'M', u'ਜ਼'), + (0xA59, 'M', 'ਖ਼'), + (0xA5A, 'M', 'ਗ਼'), + (0xA5B, 'M', 'ਜ਼'), ] def _seg_11(): return [ (0xA5C, 'V'), (0xA5D, 'X'), - (0xA5E, 'M', u'ਫ਼'), + (0xA5E, 'M', 'ਫ਼'), (0xA5F, 'X'), (0xA66, 'V'), (0xA77, 'X'), @@ -1207,8 +1206,8 @@ def _seg_11(): (0xB4E, 'X'), (0xB55, 'V'), (0xB58, 'X'), - (0xB5C, 'M', u'ଡ଼'), - (0xB5D, 'M', u'ଢ଼'), + (0xB5C, 'M', 'ଡ଼'), + (0xB5D, 'M', 'ଢ଼'), (0xB5E, 'X'), (0xB5F, 'V'), (0xB64, 'X'), @@ -1337,7 +1336,7 @@ def _seg_12(): (0xDF2, 'V'), (0xDF5, 'X'), (0xE01, 'V'), - (0xE33, 'M', u'ํา'), + (0xE33, 'M', 'ํา'), (0xE34, 'V'), (0xE3B, 'X'), (0xE3F, 'V'), @@ -1353,7 +1352,7 @@ def _seg_12(): (0xEA5, 'V'), (0xEA6, 'X'), (0xEA7, 'V'), - (0xEB3, 'M', u'ໍາ'), + (0xEB3, 'M', 'ໍາ'), (0xEB4, 'V'), ] @@ -1368,52 +1367,52 @@ def _seg_13(): (0xECE, 'X'), (0xED0, 'V'), (0xEDA, 'X'), - (0xEDC, 'M', u'ຫນ'), - (0xEDD, 'M', u'ຫມ'), + (0xEDC, 'M', 'ຫນ'), + (0xEDD, 'M', 'ຫມ'), (0xEDE, 'V'), (0xEE0, 'X'), (0xF00, 'V'), - (0xF0C, 'M', u'་'), + (0xF0C, 'M', '་'), (0xF0D, 'V'), - (0xF43, 'M', u'གྷ'), + (0xF43, 'M', 'གྷ'), (0xF44, 'V'), (0xF48, 'X'), (0xF49, 'V'), - (0xF4D, 'M', u'ཌྷ'), + (0xF4D, 'M', 'ཌྷ'), (0xF4E, 'V'), - (0xF52, 'M', u'དྷ'), + (0xF52, 'M', 'དྷ'), (0xF53, 'V'), - (0xF57, 'M', u'བྷ'), + (0xF57, 'M', 'བྷ'), (0xF58, 'V'), - (0xF5C, 'M', u'ཛྷ'), + (0xF5C, 'M', 'ཛྷ'), (0xF5D, 'V'), - (0xF69, 'M', u'ཀྵ'), + (0xF69, 'M', 'ཀྵ'), (0xF6A, 'V'), (0xF6D, 'X'), (0xF71, 'V'), - (0xF73, 'M', u'ཱི'), + (0xF73, 'M', 'ཱི'), (0xF74, 'V'), - (0xF75, 'M', u'ཱུ'), - (0xF76, 'M', u'ྲྀ'), - (0xF77, 'M', u'ྲཱྀ'), - (0xF78, 'M', u'ླྀ'), - (0xF79, 'M', u'ླཱྀ'), + (0xF75, 'M', 'ཱུ'), + (0xF76, 'M', 'ྲྀ'), + (0xF77, 'M', 'ྲཱྀ'), + (0xF78, 'M', 'ླྀ'), + (0xF79, 'M', 'ླཱྀ'), (0xF7A, 'V'), - (0xF81, 'M', u'ཱྀ'), + (0xF81, 'M', 'ཱྀ'), (0xF82, 'V'), - (0xF93, 'M', u'ྒྷ'), + (0xF93, 'M', 'ྒྷ'), (0xF94, 'V'), (0xF98, 'X'), (0xF99, 'V'), - (0xF9D, 'M', u'ྜྷ'), + (0xF9D, 'M', 'ྜྷ'), (0xF9E, 'V'), - (0xFA2, 'M', u'ྡྷ'), + (0xFA2, 'M', 'ྡྷ'), (0xFA3, 'V'), - (0xFA7, 'M', u'ྦྷ'), + (0xFA7, 'M', 'ྦྷ'), (0xFA8, 'V'), - (0xFAC, 'M', u'ྫྷ'), + (0xFAC, 'M', 'ྫྷ'), (0xFAD, 'V'), - (0xFB9, 'M', u'ྐྵ'), + (0xFB9, 'M', 'ྐྵ'), (0xFBA, 'V'), (0xFBD, 'X'), (0xFBE, 'V'), @@ -1422,12 +1421,12 @@ def _seg_13(): (0xFDB, 'X'), (0x1000, 'V'), (0x10A0, 'X'), - (0x10C7, 'M', u'ⴧ'), + (0x10C7, 'M', 'ⴧ'), (0x10C8, 'X'), - (0x10CD, 'M', u'ⴭ'), + (0x10CD, 'M', 'ⴭ'), (0x10CE, 'X'), (0x10D0, 'V'), - (0x10FC, 'M', u'ნ'), + (0x10FC, 'M', 'ნ'), (0x10FD, 'V'), (0x115F, 'X'), (0x1161, 'V'), @@ -1472,12 +1471,12 @@ def _seg_14(): (0x139A, 'X'), (0x13A0, 'V'), (0x13F6, 'X'), - (0x13F8, 'M', u'Ᏸ'), - (0x13F9, 'M', u'Ᏹ'), - (0x13FA, 'M', u'Ᏺ'), - (0x13FB, 'M', u'Ᏻ'), - (0x13FC, 'M', u'Ᏼ'), - (0x13FD, 'M', u'Ᏽ'), + (0x13F8, 'M', 'Ᏸ'), + (0x13F9, 'M', 'Ᏹ'), + (0x13FA, 'M', 'Ᏺ'), + (0x13FB, 'M', 'Ᏻ'), + (0x13FC, 'M', 'Ᏼ'), + (0x13FD, 'M', 'Ᏽ'), (0x13FE, 'X'), (0x1400, 'V'), (0x1680, 'X'), @@ -1567,1192 +1566,1192 @@ def _seg_14(): def _seg_15(): return [ - (0x1C80, 'M', u'в'), - (0x1C81, 'M', u'д'), - (0x1C82, 'M', u'о'), - (0x1C83, 'M', u'с'), - (0x1C84, 'M', u'т'), - (0x1C86, 'M', u'ъ'), - (0x1C87, 'M', u'ѣ'), - (0x1C88, 'M', u'ꙋ'), + (0x1C80, 'M', 'в'), + (0x1C81, 'M', 'д'), + (0x1C82, 'M', 'о'), + (0x1C83, 'M', 'с'), + (0x1C84, 'M', 'т'), + (0x1C86, 'M', 'ъ'), + (0x1C87, 'M', 'ѣ'), + (0x1C88, 'M', 'ꙋ'), (0x1C89, 'X'), - (0x1C90, 'M', u'ა'), - (0x1C91, 'M', u'ბ'), - (0x1C92, 'M', u'გ'), - (0x1C93, 'M', u'დ'), - (0x1C94, 'M', u'ე'), - (0x1C95, 'M', u'ვ'), - (0x1C96, 'M', u'ზ'), - (0x1C97, 'M', u'თ'), - (0x1C98, 'M', u'ი'), - (0x1C99, 'M', u'კ'), - (0x1C9A, 'M', u'ლ'), - (0x1C9B, 'M', u'მ'), - (0x1C9C, 'M', u'ნ'), - (0x1C9D, 'M', u'ო'), - (0x1C9E, 'M', u'პ'), - (0x1C9F, 'M', u'ჟ'), - (0x1CA0, 'M', u'რ'), - (0x1CA1, 'M', u'ს'), - (0x1CA2, 'M', u'ტ'), - (0x1CA3, 'M', u'უ'), - (0x1CA4, 'M', u'ფ'), - (0x1CA5, 'M', u'ქ'), - (0x1CA6, 'M', u'ღ'), - (0x1CA7, 'M', u'ყ'), - (0x1CA8, 'M', u'შ'), - (0x1CA9, 'M', u'ჩ'), - (0x1CAA, 'M', u'ც'), - (0x1CAB, 'M', u'ძ'), - (0x1CAC, 'M', u'წ'), - (0x1CAD, 'M', u'ჭ'), - (0x1CAE, 'M', u'ხ'), - (0x1CAF, 'M', u'ჯ'), - (0x1CB0, 'M', u'ჰ'), - (0x1CB1, 'M', u'ჱ'), - (0x1CB2, 'M', u'ჲ'), - (0x1CB3, 'M', u'ჳ'), - (0x1CB4, 'M', u'ჴ'), - (0x1CB5, 'M', u'ჵ'), - (0x1CB6, 'M', u'ჶ'), - (0x1CB7, 'M', u'ჷ'), - (0x1CB8, 'M', u'ჸ'), - (0x1CB9, 'M', u'ჹ'), - (0x1CBA, 'M', u'ჺ'), + (0x1C90, 'M', 'ა'), + (0x1C91, 'M', 'ბ'), + (0x1C92, 'M', 'გ'), + (0x1C93, 'M', 'დ'), + (0x1C94, 'M', 'ე'), + (0x1C95, 'M', 'ვ'), + (0x1C96, 'M', 'ზ'), + (0x1C97, 'M', 'თ'), + (0x1C98, 'M', 'ი'), + (0x1C99, 'M', 'კ'), + (0x1C9A, 'M', 'ლ'), + (0x1C9B, 'M', 'მ'), + (0x1C9C, 'M', 'ნ'), + (0x1C9D, 'M', 'ო'), + (0x1C9E, 'M', 'პ'), + (0x1C9F, 'M', 'ჟ'), + (0x1CA0, 'M', 'რ'), + (0x1CA1, 'M', 'ს'), + (0x1CA2, 'M', 'ტ'), + (0x1CA3, 'M', 'უ'), + (0x1CA4, 'M', 'ფ'), + (0x1CA5, 'M', 'ქ'), + (0x1CA6, 'M', 'ღ'), + (0x1CA7, 'M', 'ყ'), + (0x1CA8, 'M', 'შ'), + (0x1CA9, 'M', 'ჩ'), + (0x1CAA, 'M', 'ც'), + (0x1CAB, 'M', 'ძ'), + (0x1CAC, 'M', 'წ'), + (0x1CAD, 'M', 'ჭ'), + (0x1CAE, 'M', 'ხ'), + (0x1CAF, 'M', 'ჯ'), + (0x1CB0, 'M', 'ჰ'), + (0x1CB1, 'M', 'ჱ'), + (0x1CB2, 'M', 'ჲ'), + (0x1CB3, 'M', 'ჳ'), + (0x1CB4, 'M', 'ჴ'), + (0x1CB5, 'M', 'ჵ'), + (0x1CB6, 'M', 'ჶ'), + (0x1CB7, 'M', 'ჷ'), + (0x1CB8, 'M', 'ჸ'), + (0x1CB9, 'M', 'ჹ'), + (0x1CBA, 'M', 'ჺ'), (0x1CBB, 'X'), - (0x1CBD, 'M', u'ჽ'), - (0x1CBE, 'M', u'ჾ'), - (0x1CBF, 'M', u'ჿ'), + (0x1CBD, 'M', 'ჽ'), + (0x1CBE, 'M', 'ჾ'), + (0x1CBF, 'M', 'ჿ'), (0x1CC0, 'V'), (0x1CC8, 'X'), (0x1CD0, 'V'), (0x1CFB, 'X'), (0x1D00, 'V'), - (0x1D2C, 'M', u'a'), - (0x1D2D, 'M', u'æ'), - (0x1D2E, 'M', u'b'), + (0x1D2C, 'M', 'a'), + (0x1D2D, 'M', 'æ'), + (0x1D2E, 'M', 'b'), (0x1D2F, 'V'), - (0x1D30, 'M', u'd'), - (0x1D31, 'M', u'e'), - (0x1D32, 'M', u'ǝ'), - (0x1D33, 'M', u'g'), - (0x1D34, 'M', u'h'), - (0x1D35, 'M', u'i'), - (0x1D36, 'M', u'j'), - (0x1D37, 'M', u'k'), - (0x1D38, 'M', u'l'), - (0x1D39, 'M', u'm'), - (0x1D3A, 'M', u'n'), + (0x1D30, 'M', 'd'), + (0x1D31, 'M', 'e'), + (0x1D32, 'M', 'ǝ'), + (0x1D33, 'M', 'g'), + (0x1D34, 'M', 'h'), + (0x1D35, 'M', 'i'), + (0x1D36, 'M', 'j'), + (0x1D37, 'M', 'k'), + (0x1D38, 'M', 'l'), + (0x1D39, 'M', 'm'), + (0x1D3A, 'M', 'n'), (0x1D3B, 'V'), - (0x1D3C, 'M', u'o'), - (0x1D3D, 'M', u'ȣ'), - (0x1D3E, 'M', u'p'), - (0x1D3F, 'M', u'r'), - (0x1D40, 'M', u't'), - (0x1D41, 'M', u'u'), - (0x1D42, 'M', u'w'), - (0x1D43, 'M', u'a'), - (0x1D44, 'M', u'ɐ'), - (0x1D45, 'M', u'ɑ'), - (0x1D46, 'M', u'ᴂ'), - (0x1D47, 'M', u'b'), - (0x1D48, 'M', u'd'), - (0x1D49, 'M', u'e'), - (0x1D4A, 'M', u'ə'), - (0x1D4B, 'M', u'ɛ'), - (0x1D4C, 'M', u'ɜ'), - (0x1D4D, 'M', u'g'), + (0x1D3C, 'M', 'o'), + (0x1D3D, 'M', 'ȣ'), + (0x1D3E, 'M', 'p'), + (0x1D3F, 'M', 'r'), + (0x1D40, 'M', 't'), + (0x1D41, 'M', 'u'), + (0x1D42, 'M', 'w'), + (0x1D43, 'M', 'a'), + (0x1D44, 'M', 'ɐ'), + (0x1D45, 'M', 'ɑ'), + (0x1D46, 'M', 'ᴂ'), + (0x1D47, 'M', 'b'), + (0x1D48, 'M', 'd'), + (0x1D49, 'M', 'e'), + (0x1D4A, 'M', 'ə'), + (0x1D4B, 'M', 'ɛ'), + (0x1D4C, 'M', 'ɜ'), + (0x1D4D, 'M', 'g'), (0x1D4E, 'V'), - (0x1D4F, 'M', u'k'), - (0x1D50, 'M', u'm'), - (0x1D51, 'M', u'ŋ'), - (0x1D52, 'M', u'o'), + (0x1D4F, 'M', 'k'), + (0x1D50, 'M', 'm'), + (0x1D51, 'M', 'ŋ'), + (0x1D52, 'M', 'o'), ] def _seg_16(): return [ - (0x1D53, 'M', u'ɔ'), - (0x1D54, 'M', u'ᴖ'), - (0x1D55, 'M', u'ᴗ'), - (0x1D56, 'M', u'p'), - (0x1D57, 'M', u't'), - (0x1D58, 'M', u'u'), - (0x1D59, 'M', u'ᴝ'), - (0x1D5A, 'M', u'ɯ'), - (0x1D5B, 'M', u'v'), - (0x1D5C, 'M', u'ᴥ'), - (0x1D5D, 'M', u'β'), - (0x1D5E, 'M', u'γ'), - (0x1D5F, 'M', u'δ'), - (0x1D60, 'M', u'φ'), - (0x1D61, 'M', u'χ'), - (0x1D62, 'M', u'i'), - (0x1D63, 'M', u'r'), - (0x1D64, 'M', u'u'), - (0x1D65, 'M', u'v'), - (0x1D66, 'M', u'β'), - (0x1D67, 'M', u'γ'), - (0x1D68, 'M', u'ρ'), - (0x1D69, 'M', u'φ'), - (0x1D6A, 'M', u'χ'), + (0x1D53, 'M', 'ɔ'), + (0x1D54, 'M', 'ᴖ'), + (0x1D55, 'M', 'ᴗ'), + (0x1D56, 'M', 'p'), + (0x1D57, 'M', 't'), + (0x1D58, 'M', 'u'), + (0x1D59, 'M', 'ᴝ'), + (0x1D5A, 'M', 'ɯ'), + (0x1D5B, 'M', 'v'), + (0x1D5C, 'M', 'ᴥ'), + (0x1D5D, 'M', 'β'), + (0x1D5E, 'M', 'γ'), + (0x1D5F, 'M', 'δ'), + (0x1D60, 'M', 'φ'), + (0x1D61, 'M', 'χ'), + (0x1D62, 'M', 'i'), + (0x1D63, 'M', 'r'), + (0x1D64, 'M', 'u'), + (0x1D65, 'M', 'v'), + (0x1D66, 'M', 'β'), + (0x1D67, 'M', 'γ'), + (0x1D68, 'M', 'ρ'), + (0x1D69, 'M', 'φ'), + (0x1D6A, 'M', 'χ'), (0x1D6B, 'V'), - (0x1D78, 'M', u'н'), + (0x1D78, 'M', 'н'), (0x1D79, 'V'), - (0x1D9B, 'M', u'ɒ'), - (0x1D9C, 'M', u'c'), - (0x1D9D, 'M', u'ɕ'), - (0x1D9E, 'M', u'ð'), - (0x1D9F, 'M', u'ɜ'), - (0x1DA0, 'M', u'f'), - (0x1DA1, 'M', u'ɟ'), - (0x1DA2, 'M', u'ɡ'), - (0x1DA3, 'M', u'ɥ'), - (0x1DA4, 'M', u'ɨ'), - (0x1DA5, 'M', u'ɩ'), - (0x1DA6, 'M', u'ɪ'), - (0x1DA7, 'M', u'ᵻ'), - (0x1DA8, 'M', u'ʝ'), - (0x1DA9, 'M', u'ɭ'), - (0x1DAA, 'M', u'ᶅ'), - (0x1DAB, 'M', u'ʟ'), - (0x1DAC, 'M', u'ɱ'), - (0x1DAD, 'M', u'ɰ'), - (0x1DAE, 'M', u'ɲ'), - (0x1DAF, 'M', u'ɳ'), - (0x1DB0, 'M', u'ɴ'), - (0x1DB1, 'M', u'ɵ'), - (0x1DB2, 'M', u'ɸ'), - (0x1DB3, 'M', u'ʂ'), - (0x1DB4, 'M', u'ʃ'), - (0x1DB5, 'M', u'ƫ'), - (0x1DB6, 'M', u'ʉ'), - (0x1DB7, 'M', u'ʊ'), - (0x1DB8, 'M', u'ᴜ'), - (0x1DB9, 'M', u'ʋ'), - (0x1DBA, 'M', u'ʌ'), - (0x1DBB, 'M', u'z'), - (0x1DBC, 'M', u'ʐ'), - (0x1DBD, 'M', u'ʑ'), - (0x1DBE, 'M', u'ʒ'), - (0x1DBF, 'M', u'θ'), + (0x1D9B, 'M', 'ɒ'), + (0x1D9C, 'M', 'c'), + (0x1D9D, 'M', 'ɕ'), + (0x1D9E, 'M', 'ð'), + (0x1D9F, 'M', 'ɜ'), + (0x1DA0, 'M', 'f'), + (0x1DA1, 'M', 'ɟ'), + (0x1DA2, 'M', 'ɡ'), + (0x1DA3, 'M', 'ɥ'), + (0x1DA4, 'M', 'ɨ'), + (0x1DA5, 'M', 'ɩ'), + (0x1DA6, 'M', 'ɪ'), + (0x1DA7, 'M', 'ᵻ'), + (0x1DA8, 'M', 'ʝ'), + (0x1DA9, 'M', 'ɭ'), + (0x1DAA, 'M', 'ᶅ'), + (0x1DAB, 'M', 'ʟ'), + (0x1DAC, 'M', 'ɱ'), + (0x1DAD, 'M', 'ɰ'), + (0x1DAE, 'M', 'ɲ'), + (0x1DAF, 'M', 'ɳ'), + (0x1DB0, 'M', 'ɴ'), + (0x1DB1, 'M', 'ɵ'), + (0x1DB2, 'M', 'ɸ'), + (0x1DB3, 'M', 'ʂ'), + (0x1DB4, 'M', 'ʃ'), + (0x1DB5, 'M', 'ƫ'), + (0x1DB6, 'M', 'ʉ'), + (0x1DB7, 'M', 'ʊ'), + (0x1DB8, 'M', 'ᴜ'), + (0x1DB9, 'M', 'ʋ'), + (0x1DBA, 'M', 'ʌ'), + (0x1DBB, 'M', 'z'), + (0x1DBC, 'M', 'ʐ'), + (0x1DBD, 'M', 'ʑ'), + (0x1DBE, 'M', 'ʒ'), + (0x1DBF, 'M', 'θ'), (0x1DC0, 'V'), (0x1DFA, 'X'), (0x1DFB, 'V'), - (0x1E00, 'M', u'ḁ'), + (0x1E00, 'M', 'ḁ'), (0x1E01, 'V'), - (0x1E02, 'M', u'ḃ'), + (0x1E02, 'M', 'ḃ'), (0x1E03, 'V'), - (0x1E04, 'M', u'ḅ'), + (0x1E04, 'M', 'ḅ'), (0x1E05, 'V'), - (0x1E06, 'M', u'ḇ'), + (0x1E06, 'M', 'ḇ'), (0x1E07, 'V'), - (0x1E08, 'M', u'ḉ'), + (0x1E08, 'M', 'ḉ'), (0x1E09, 'V'), - (0x1E0A, 'M', u'ḋ'), + (0x1E0A, 'M', 'ḋ'), (0x1E0B, 'V'), - (0x1E0C, 'M', u'ḍ'), + (0x1E0C, 'M', 'ḍ'), (0x1E0D, 'V'), - (0x1E0E, 'M', u'ḏ'), + (0x1E0E, 'M', 'ḏ'), (0x1E0F, 'V'), - (0x1E10, 'M', u'ḑ'), + (0x1E10, 'M', 'ḑ'), (0x1E11, 'V'), - (0x1E12, 'M', u'ḓ'), + (0x1E12, 'M', 'ḓ'), (0x1E13, 'V'), - (0x1E14, 'M', u'ḕ'), + (0x1E14, 'M', 'ḕ'), (0x1E15, 'V'), - (0x1E16, 'M', u'ḗ'), + (0x1E16, 'M', 'ḗ'), (0x1E17, 'V'), - (0x1E18, 'M', u'ḙ'), + (0x1E18, 'M', 'ḙ'), (0x1E19, 'V'), - (0x1E1A, 'M', u'ḛ'), + (0x1E1A, 'M', 'ḛ'), (0x1E1B, 'V'), - (0x1E1C, 'M', u'ḝ'), + (0x1E1C, 'M', 'ḝ'), (0x1E1D, 'V'), - (0x1E1E, 'M', u'ḟ'), + (0x1E1E, 'M', 'ḟ'), (0x1E1F, 'V'), - (0x1E20, 'M', u'ḡ'), + (0x1E20, 'M', 'ḡ'), ] def _seg_17(): return [ (0x1E21, 'V'), - (0x1E22, 'M', u'ḣ'), + (0x1E22, 'M', 'ḣ'), (0x1E23, 'V'), - (0x1E24, 'M', u'ḥ'), + (0x1E24, 'M', 'ḥ'), (0x1E25, 'V'), - (0x1E26, 'M', u'ḧ'), + (0x1E26, 'M', 'ḧ'), (0x1E27, 'V'), - (0x1E28, 'M', u'ḩ'), + (0x1E28, 'M', 'ḩ'), (0x1E29, 'V'), - (0x1E2A, 'M', u'ḫ'), + (0x1E2A, 'M', 'ḫ'), (0x1E2B, 'V'), - (0x1E2C, 'M', u'ḭ'), + (0x1E2C, 'M', 'ḭ'), (0x1E2D, 'V'), - (0x1E2E, 'M', u'ḯ'), + (0x1E2E, 'M', 'ḯ'), (0x1E2F, 'V'), - (0x1E30, 'M', u'ḱ'), + (0x1E30, 'M', 'ḱ'), (0x1E31, 'V'), - (0x1E32, 'M', u'ḳ'), + (0x1E32, 'M', 'ḳ'), (0x1E33, 'V'), - (0x1E34, 'M', u'ḵ'), + (0x1E34, 'M', 'ḵ'), (0x1E35, 'V'), - (0x1E36, 'M', u'ḷ'), + (0x1E36, 'M', 'ḷ'), (0x1E37, 'V'), - (0x1E38, 'M', u'ḹ'), + (0x1E38, 'M', 'ḹ'), (0x1E39, 'V'), - (0x1E3A, 'M', u'ḻ'), + (0x1E3A, 'M', 'ḻ'), (0x1E3B, 'V'), - (0x1E3C, 'M', u'ḽ'), + (0x1E3C, 'M', 'ḽ'), (0x1E3D, 'V'), - (0x1E3E, 'M', u'ḿ'), + (0x1E3E, 'M', 'ḿ'), (0x1E3F, 'V'), - (0x1E40, 'M', u'ṁ'), + (0x1E40, 'M', 'ṁ'), (0x1E41, 'V'), - (0x1E42, 'M', u'ṃ'), + (0x1E42, 'M', 'ṃ'), (0x1E43, 'V'), - (0x1E44, 'M', u'ṅ'), + (0x1E44, 'M', 'ṅ'), (0x1E45, 'V'), - (0x1E46, 'M', u'ṇ'), + (0x1E46, 'M', 'ṇ'), (0x1E47, 'V'), - (0x1E48, 'M', u'ṉ'), + (0x1E48, 'M', 'ṉ'), (0x1E49, 'V'), - (0x1E4A, 'M', u'ṋ'), + (0x1E4A, 'M', 'ṋ'), (0x1E4B, 'V'), - (0x1E4C, 'M', u'ṍ'), + (0x1E4C, 'M', 'ṍ'), (0x1E4D, 'V'), - (0x1E4E, 'M', u'ṏ'), + (0x1E4E, 'M', 'ṏ'), (0x1E4F, 'V'), - (0x1E50, 'M', u'ṑ'), + (0x1E50, 'M', 'ṑ'), (0x1E51, 'V'), - (0x1E52, 'M', u'ṓ'), + (0x1E52, 'M', 'ṓ'), (0x1E53, 'V'), - (0x1E54, 'M', u'ṕ'), + (0x1E54, 'M', 'ṕ'), (0x1E55, 'V'), - (0x1E56, 'M', u'ṗ'), + (0x1E56, 'M', 'ṗ'), (0x1E57, 'V'), - (0x1E58, 'M', u'ṙ'), + (0x1E58, 'M', 'ṙ'), (0x1E59, 'V'), - (0x1E5A, 'M', u'ṛ'), + (0x1E5A, 'M', 'ṛ'), (0x1E5B, 'V'), - (0x1E5C, 'M', u'ṝ'), + (0x1E5C, 'M', 'ṝ'), (0x1E5D, 'V'), - (0x1E5E, 'M', u'ṟ'), + (0x1E5E, 'M', 'ṟ'), (0x1E5F, 'V'), - (0x1E60, 'M', u'ṡ'), + (0x1E60, 'M', 'ṡ'), (0x1E61, 'V'), - (0x1E62, 'M', u'ṣ'), + (0x1E62, 'M', 'ṣ'), (0x1E63, 'V'), - (0x1E64, 'M', u'ṥ'), + (0x1E64, 'M', 'ṥ'), (0x1E65, 'V'), - (0x1E66, 'M', u'ṧ'), + (0x1E66, 'M', 'ṧ'), (0x1E67, 'V'), - (0x1E68, 'M', u'ṩ'), + (0x1E68, 'M', 'ṩ'), (0x1E69, 'V'), - (0x1E6A, 'M', u'ṫ'), + (0x1E6A, 'M', 'ṫ'), (0x1E6B, 'V'), - (0x1E6C, 'M', u'ṭ'), + (0x1E6C, 'M', 'ṭ'), (0x1E6D, 'V'), - (0x1E6E, 'M', u'ṯ'), + (0x1E6E, 'M', 'ṯ'), (0x1E6F, 'V'), - (0x1E70, 'M', u'ṱ'), + (0x1E70, 'M', 'ṱ'), (0x1E71, 'V'), - (0x1E72, 'M', u'ṳ'), + (0x1E72, 'M', 'ṳ'), (0x1E73, 'V'), - (0x1E74, 'M', u'ṵ'), + (0x1E74, 'M', 'ṵ'), (0x1E75, 'V'), - (0x1E76, 'M', u'ṷ'), + (0x1E76, 'M', 'ṷ'), (0x1E77, 'V'), - (0x1E78, 'M', u'ṹ'), + (0x1E78, 'M', 'ṹ'), (0x1E79, 'V'), - (0x1E7A, 'M', u'ṻ'), + (0x1E7A, 'M', 'ṻ'), (0x1E7B, 'V'), - (0x1E7C, 'M', u'ṽ'), + (0x1E7C, 'M', 'ṽ'), (0x1E7D, 'V'), - (0x1E7E, 'M', u'ṿ'), + (0x1E7E, 'M', 'ṿ'), (0x1E7F, 'V'), - (0x1E80, 'M', u'ẁ'), + (0x1E80, 'M', 'ẁ'), (0x1E81, 'V'), - (0x1E82, 'M', u'ẃ'), + (0x1E82, 'M', 'ẃ'), (0x1E83, 'V'), - (0x1E84, 'M', u'ẅ'), + (0x1E84, 'M', 'ẅ'), ] def _seg_18(): return [ (0x1E85, 'V'), - (0x1E86, 'M', u'ẇ'), + (0x1E86, 'M', 'ẇ'), (0x1E87, 'V'), - (0x1E88, 'M', u'ẉ'), + (0x1E88, 'M', 'ẉ'), (0x1E89, 'V'), - (0x1E8A, 'M', u'ẋ'), + (0x1E8A, 'M', 'ẋ'), (0x1E8B, 'V'), - (0x1E8C, 'M', u'ẍ'), + (0x1E8C, 'M', 'ẍ'), (0x1E8D, 'V'), - (0x1E8E, 'M', u'ẏ'), + (0x1E8E, 'M', 'ẏ'), (0x1E8F, 'V'), - (0x1E90, 'M', u'ẑ'), + (0x1E90, 'M', 'ẑ'), (0x1E91, 'V'), - (0x1E92, 'M', u'ẓ'), + (0x1E92, 'M', 'ẓ'), (0x1E93, 'V'), - (0x1E94, 'M', u'ẕ'), + (0x1E94, 'M', 'ẕ'), (0x1E95, 'V'), - (0x1E9A, 'M', u'aʾ'), - (0x1E9B, 'M', u'ṡ'), + (0x1E9A, 'M', 'aʾ'), + (0x1E9B, 'M', 'ṡ'), (0x1E9C, 'V'), - (0x1E9E, 'M', u'ss'), + (0x1E9E, 'M', 'ss'), (0x1E9F, 'V'), - (0x1EA0, 'M', u'ạ'), + (0x1EA0, 'M', 'ạ'), (0x1EA1, 'V'), - (0x1EA2, 'M', u'ả'), + (0x1EA2, 'M', 'ả'), (0x1EA3, 'V'), - (0x1EA4, 'M', u'ấ'), + (0x1EA4, 'M', 'ấ'), (0x1EA5, 'V'), - (0x1EA6, 'M', u'ầ'), + (0x1EA6, 'M', 'ầ'), (0x1EA7, 'V'), - (0x1EA8, 'M', u'ẩ'), + (0x1EA8, 'M', 'ẩ'), (0x1EA9, 'V'), - (0x1EAA, 'M', u'ẫ'), + (0x1EAA, 'M', 'ẫ'), (0x1EAB, 'V'), - (0x1EAC, 'M', u'ậ'), + (0x1EAC, 'M', 'ậ'), (0x1EAD, 'V'), - (0x1EAE, 'M', u'ắ'), + (0x1EAE, 'M', 'ắ'), (0x1EAF, 'V'), - (0x1EB0, 'M', u'ằ'), + (0x1EB0, 'M', 'ằ'), (0x1EB1, 'V'), - (0x1EB2, 'M', u'ẳ'), + (0x1EB2, 'M', 'ẳ'), (0x1EB3, 'V'), - (0x1EB4, 'M', u'ẵ'), + (0x1EB4, 'M', 'ẵ'), (0x1EB5, 'V'), - (0x1EB6, 'M', u'ặ'), + (0x1EB6, 'M', 'ặ'), (0x1EB7, 'V'), - (0x1EB8, 'M', u'ẹ'), + (0x1EB8, 'M', 'ẹ'), (0x1EB9, 'V'), - (0x1EBA, 'M', u'ẻ'), + (0x1EBA, 'M', 'ẻ'), (0x1EBB, 'V'), - (0x1EBC, 'M', u'ẽ'), + (0x1EBC, 'M', 'ẽ'), (0x1EBD, 'V'), - (0x1EBE, 'M', u'ế'), + (0x1EBE, 'M', 'ế'), (0x1EBF, 'V'), - (0x1EC0, 'M', u'ề'), + (0x1EC0, 'M', 'ề'), (0x1EC1, 'V'), - (0x1EC2, 'M', u'ể'), + (0x1EC2, 'M', 'ể'), (0x1EC3, 'V'), - (0x1EC4, 'M', u'ễ'), + (0x1EC4, 'M', 'ễ'), (0x1EC5, 'V'), - (0x1EC6, 'M', u'ệ'), + (0x1EC6, 'M', 'ệ'), (0x1EC7, 'V'), - (0x1EC8, 'M', u'ỉ'), + (0x1EC8, 'M', 'ỉ'), (0x1EC9, 'V'), - (0x1ECA, 'M', u'ị'), + (0x1ECA, 'M', 'ị'), (0x1ECB, 'V'), - (0x1ECC, 'M', u'ọ'), + (0x1ECC, 'M', 'ọ'), (0x1ECD, 'V'), - (0x1ECE, 'M', u'ỏ'), + (0x1ECE, 'M', 'ỏ'), (0x1ECF, 'V'), - (0x1ED0, 'M', u'ố'), + (0x1ED0, 'M', 'ố'), (0x1ED1, 'V'), - (0x1ED2, 'M', u'ồ'), + (0x1ED2, 'M', 'ồ'), (0x1ED3, 'V'), - (0x1ED4, 'M', u'ổ'), + (0x1ED4, 'M', 'ổ'), (0x1ED5, 'V'), - (0x1ED6, 'M', u'ỗ'), + (0x1ED6, 'M', 'ỗ'), (0x1ED7, 'V'), - (0x1ED8, 'M', u'ộ'), + (0x1ED8, 'M', 'ộ'), (0x1ED9, 'V'), - (0x1EDA, 'M', u'ớ'), + (0x1EDA, 'M', 'ớ'), (0x1EDB, 'V'), - (0x1EDC, 'M', u'ờ'), + (0x1EDC, 'M', 'ờ'), (0x1EDD, 'V'), - (0x1EDE, 'M', u'ở'), + (0x1EDE, 'M', 'ở'), (0x1EDF, 'V'), - (0x1EE0, 'M', u'ỡ'), + (0x1EE0, 'M', 'ỡ'), (0x1EE1, 'V'), - (0x1EE2, 'M', u'ợ'), + (0x1EE2, 'M', 'ợ'), (0x1EE3, 'V'), - (0x1EE4, 'M', u'ụ'), + (0x1EE4, 'M', 'ụ'), (0x1EE5, 'V'), - (0x1EE6, 'M', u'ủ'), + (0x1EE6, 'M', 'ủ'), (0x1EE7, 'V'), - (0x1EE8, 'M', u'ứ'), + (0x1EE8, 'M', 'ứ'), (0x1EE9, 'V'), - (0x1EEA, 'M', u'ừ'), + (0x1EEA, 'M', 'ừ'), (0x1EEB, 'V'), - (0x1EEC, 'M', u'ử'), + (0x1EEC, 'M', 'ử'), (0x1EED, 'V'), ] def _seg_19(): return [ - (0x1EEE, 'M', u'ữ'), + (0x1EEE, 'M', 'ữ'), (0x1EEF, 'V'), - (0x1EF0, 'M', u'ự'), + (0x1EF0, 'M', 'ự'), (0x1EF1, 'V'), - (0x1EF2, 'M', u'ỳ'), + (0x1EF2, 'M', 'ỳ'), (0x1EF3, 'V'), - (0x1EF4, 'M', u'ỵ'), + (0x1EF4, 'M', 'ỵ'), (0x1EF5, 'V'), - (0x1EF6, 'M', u'ỷ'), + (0x1EF6, 'M', 'ỷ'), (0x1EF7, 'V'), - (0x1EF8, 'M', u'ỹ'), + (0x1EF8, 'M', 'ỹ'), (0x1EF9, 'V'), - (0x1EFA, 'M', u'ỻ'), + (0x1EFA, 'M', 'ỻ'), (0x1EFB, 'V'), - (0x1EFC, 'M', u'ỽ'), + (0x1EFC, 'M', 'ỽ'), (0x1EFD, 'V'), - (0x1EFE, 'M', u'ỿ'), + (0x1EFE, 'M', 'ỿ'), (0x1EFF, 'V'), - (0x1F08, 'M', u'ἀ'), - (0x1F09, 'M', u'ἁ'), - (0x1F0A, 'M', u'ἂ'), - (0x1F0B, 'M', u'ἃ'), - (0x1F0C, 'M', u'ἄ'), - (0x1F0D, 'M', u'ἅ'), - (0x1F0E, 'M', u'ἆ'), - (0x1F0F, 'M', u'ἇ'), + (0x1F08, 'M', 'ἀ'), + (0x1F09, 'M', 'ἁ'), + (0x1F0A, 'M', 'ἂ'), + (0x1F0B, 'M', 'ἃ'), + (0x1F0C, 'M', 'ἄ'), + (0x1F0D, 'M', 'ἅ'), + (0x1F0E, 'M', 'ἆ'), + (0x1F0F, 'M', 'ἇ'), (0x1F10, 'V'), (0x1F16, 'X'), - (0x1F18, 'M', u'ἐ'), - (0x1F19, 'M', u'ἑ'), - (0x1F1A, 'M', u'ἒ'), - (0x1F1B, 'M', u'ἓ'), - (0x1F1C, 'M', u'ἔ'), - (0x1F1D, 'M', u'ἕ'), + (0x1F18, 'M', 'ἐ'), + (0x1F19, 'M', 'ἑ'), + (0x1F1A, 'M', 'ἒ'), + (0x1F1B, 'M', 'ἓ'), + (0x1F1C, 'M', 'ἔ'), + (0x1F1D, 'M', 'ἕ'), (0x1F1E, 'X'), (0x1F20, 'V'), - (0x1F28, 'M', u'ἠ'), - (0x1F29, 'M', u'ἡ'), - (0x1F2A, 'M', u'ἢ'), - (0x1F2B, 'M', u'ἣ'), - (0x1F2C, 'M', u'ἤ'), - (0x1F2D, 'M', u'ἥ'), - (0x1F2E, 'M', u'ἦ'), - (0x1F2F, 'M', u'ἧ'), + (0x1F28, 'M', 'ἠ'), + (0x1F29, 'M', 'ἡ'), + (0x1F2A, 'M', 'ἢ'), + (0x1F2B, 'M', 'ἣ'), + (0x1F2C, 'M', 'ἤ'), + (0x1F2D, 'M', 'ἥ'), + (0x1F2E, 'M', 'ἦ'), + (0x1F2F, 'M', 'ἧ'), (0x1F30, 'V'), - (0x1F38, 'M', u'ἰ'), - (0x1F39, 'M', u'ἱ'), - (0x1F3A, 'M', u'ἲ'), - (0x1F3B, 'M', u'ἳ'), - (0x1F3C, 'M', u'ἴ'), - (0x1F3D, 'M', u'ἵ'), - (0x1F3E, 'M', u'ἶ'), - (0x1F3F, 'M', u'ἷ'), + (0x1F38, 'M', 'ἰ'), + (0x1F39, 'M', 'ἱ'), + (0x1F3A, 'M', 'ἲ'), + (0x1F3B, 'M', 'ἳ'), + (0x1F3C, 'M', 'ἴ'), + (0x1F3D, 'M', 'ἵ'), + (0x1F3E, 'M', 'ἶ'), + (0x1F3F, 'M', 'ἷ'), (0x1F40, 'V'), (0x1F46, 'X'), - (0x1F48, 'M', u'ὀ'), - (0x1F49, 'M', u'ὁ'), - (0x1F4A, 'M', u'ὂ'), - (0x1F4B, 'M', u'ὃ'), - (0x1F4C, 'M', u'ὄ'), - (0x1F4D, 'M', u'ὅ'), + (0x1F48, 'M', 'ὀ'), + (0x1F49, 'M', 'ὁ'), + (0x1F4A, 'M', 'ὂ'), + (0x1F4B, 'M', 'ὃ'), + (0x1F4C, 'M', 'ὄ'), + (0x1F4D, 'M', 'ὅ'), (0x1F4E, 'X'), (0x1F50, 'V'), (0x1F58, 'X'), - (0x1F59, 'M', u'ὑ'), + (0x1F59, 'M', 'ὑ'), (0x1F5A, 'X'), - (0x1F5B, 'M', u'ὓ'), + (0x1F5B, 'M', 'ὓ'), (0x1F5C, 'X'), - (0x1F5D, 'M', u'ὕ'), + (0x1F5D, 'M', 'ὕ'), (0x1F5E, 'X'), - (0x1F5F, 'M', u'ὗ'), + (0x1F5F, 'M', 'ὗ'), (0x1F60, 'V'), - (0x1F68, 'M', u'ὠ'), - (0x1F69, 'M', u'ὡ'), - (0x1F6A, 'M', u'ὢ'), - (0x1F6B, 'M', u'ὣ'), - (0x1F6C, 'M', u'ὤ'), - (0x1F6D, 'M', u'ὥ'), - (0x1F6E, 'M', u'ὦ'), - (0x1F6F, 'M', u'ὧ'), + (0x1F68, 'M', 'ὠ'), + (0x1F69, 'M', 'ὡ'), + (0x1F6A, 'M', 'ὢ'), + (0x1F6B, 'M', 'ὣ'), + (0x1F6C, 'M', 'ὤ'), + (0x1F6D, 'M', 'ὥ'), + (0x1F6E, 'M', 'ὦ'), + (0x1F6F, 'M', 'ὧ'), (0x1F70, 'V'), - (0x1F71, 'M', u'ά'), + (0x1F71, 'M', 'ά'), (0x1F72, 'V'), - (0x1F73, 'M', u'έ'), + (0x1F73, 'M', 'έ'), (0x1F74, 'V'), - (0x1F75, 'M', u'ή'), + (0x1F75, 'M', 'ή'), (0x1F76, 'V'), - (0x1F77, 'M', u'ί'), + (0x1F77, 'M', 'ί'), (0x1F78, 'V'), - (0x1F79, 'M', u'ό'), + (0x1F79, 'M', 'ό'), (0x1F7A, 'V'), - (0x1F7B, 'M', u'ύ'), + (0x1F7B, 'M', 'ύ'), (0x1F7C, 'V'), - (0x1F7D, 'M', u'ώ'), + (0x1F7D, 'M', 'ώ'), (0x1F7E, 'X'), - (0x1F80, 'M', u'ἀι'), - (0x1F81, 'M', u'ἁι'), - (0x1F82, 'M', u'ἂι'), - (0x1F83, 'M', u'ἃι'), - (0x1F84, 'M', u'ἄι'), + (0x1F80, 'M', 'ἀι'), + (0x1F81, 'M', 'ἁι'), + (0x1F82, 'M', 'ἂι'), + (0x1F83, 'M', 'ἃι'), + (0x1F84, 'M', 'ἄι'), ] def _seg_20(): return [ - (0x1F85, 'M', u'ἅι'), - (0x1F86, 'M', u'ἆι'), - (0x1F87, 'M', u'ἇι'), - (0x1F88, 'M', u'ἀι'), - (0x1F89, 'M', u'ἁι'), - (0x1F8A, 'M', u'ἂι'), - (0x1F8B, 'M', u'ἃι'), - (0x1F8C, 'M', u'ἄι'), - (0x1F8D, 'M', u'ἅι'), - (0x1F8E, 'M', u'ἆι'), - (0x1F8F, 'M', u'ἇι'), - (0x1F90, 'M', u'ἠι'), - (0x1F91, 'M', u'ἡι'), - (0x1F92, 'M', u'ἢι'), - (0x1F93, 'M', u'ἣι'), - (0x1F94, 'M', u'ἤι'), - (0x1F95, 'M', u'ἥι'), - (0x1F96, 'M', u'ἦι'), - (0x1F97, 'M', u'ἧι'), - (0x1F98, 'M', u'ἠι'), - (0x1F99, 'M', u'ἡι'), - (0x1F9A, 'M', u'ἢι'), - (0x1F9B, 'M', u'ἣι'), - (0x1F9C, 'M', u'ἤι'), - (0x1F9D, 'M', u'ἥι'), - (0x1F9E, 'M', u'ἦι'), - (0x1F9F, 'M', u'ἧι'), - (0x1FA0, 'M', u'ὠι'), - (0x1FA1, 'M', u'ὡι'), - (0x1FA2, 'M', u'ὢι'), - (0x1FA3, 'M', u'ὣι'), - (0x1FA4, 'M', u'ὤι'), - (0x1FA5, 'M', u'ὥι'), - (0x1FA6, 'M', u'ὦι'), - (0x1FA7, 'M', u'ὧι'), - (0x1FA8, 'M', u'ὠι'), - (0x1FA9, 'M', u'ὡι'), - (0x1FAA, 'M', u'ὢι'), - (0x1FAB, 'M', u'ὣι'), - (0x1FAC, 'M', u'ὤι'), - (0x1FAD, 'M', u'ὥι'), - (0x1FAE, 'M', u'ὦι'), - (0x1FAF, 'M', u'ὧι'), + (0x1F85, 'M', 'ἅι'), + (0x1F86, 'M', 'ἆι'), + (0x1F87, 'M', 'ἇι'), + (0x1F88, 'M', 'ἀι'), + (0x1F89, 'M', 'ἁι'), + (0x1F8A, 'M', 'ἂι'), + (0x1F8B, 'M', 'ἃι'), + (0x1F8C, 'M', 'ἄι'), + (0x1F8D, 'M', 'ἅι'), + (0x1F8E, 'M', 'ἆι'), + (0x1F8F, 'M', 'ἇι'), + (0x1F90, 'M', 'ἠι'), + (0x1F91, 'M', 'ἡι'), + (0x1F92, 'M', 'ἢι'), + (0x1F93, 'M', 'ἣι'), + (0x1F94, 'M', 'ἤι'), + (0x1F95, 'M', 'ἥι'), + (0x1F96, 'M', 'ἦι'), + (0x1F97, 'M', 'ἧι'), + (0x1F98, 'M', 'ἠι'), + (0x1F99, 'M', 'ἡι'), + (0x1F9A, 'M', 'ἢι'), + (0x1F9B, 'M', 'ἣι'), + (0x1F9C, 'M', 'ἤι'), + (0x1F9D, 'M', 'ἥι'), + (0x1F9E, 'M', 'ἦι'), + (0x1F9F, 'M', 'ἧι'), + (0x1FA0, 'M', 'ὠι'), + (0x1FA1, 'M', 'ὡι'), + (0x1FA2, 'M', 'ὢι'), + (0x1FA3, 'M', 'ὣι'), + (0x1FA4, 'M', 'ὤι'), + (0x1FA5, 'M', 'ὥι'), + (0x1FA6, 'M', 'ὦι'), + (0x1FA7, 'M', 'ὧι'), + (0x1FA8, 'M', 'ὠι'), + (0x1FA9, 'M', 'ὡι'), + (0x1FAA, 'M', 'ὢι'), + (0x1FAB, 'M', 'ὣι'), + (0x1FAC, 'M', 'ὤι'), + (0x1FAD, 'M', 'ὥι'), + (0x1FAE, 'M', 'ὦι'), + (0x1FAF, 'M', 'ὧι'), (0x1FB0, 'V'), - (0x1FB2, 'M', u'ὰι'), - (0x1FB3, 'M', u'αι'), - (0x1FB4, 'M', u'άι'), + (0x1FB2, 'M', 'ὰι'), + (0x1FB3, 'M', 'αι'), + (0x1FB4, 'M', 'άι'), (0x1FB5, 'X'), (0x1FB6, 'V'), - (0x1FB7, 'M', u'ᾶι'), - (0x1FB8, 'M', u'ᾰ'), - (0x1FB9, 'M', u'ᾱ'), - (0x1FBA, 'M', u'ὰ'), - (0x1FBB, 'M', u'ά'), - (0x1FBC, 'M', u'αι'), - (0x1FBD, '3', u' ̓'), - (0x1FBE, 'M', u'ι'), - (0x1FBF, '3', u' ̓'), - (0x1FC0, '3', u' ͂'), - (0x1FC1, '3', u' ̈͂'), - (0x1FC2, 'M', u'ὴι'), - (0x1FC3, 'M', u'ηι'), - (0x1FC4, 'M', u'ήι'), + (0x1FB7, 'M', 'ᾶι'), + (0x1FB8, 'M', 'ᾰ'), + (0x1FB9, 'M', 'ᾱ'), + (0x1FBA, 'M', 'ὰ'), + (0x1FBB, 'M', 'ά'), + (0x1FBC, 'M', 'αι'), + (0x1FBD, '3', ' ̓'), + (0x1FBE, 'M', 'ι'), + (0x1FBF, '3', ' ̓'), + (0x1FC0, '3', ' ͂'), + (0x1FC1, '3', ' ̈͂'), + (0x1FC2, 'M', 'ὴι'), + (0x1FC3, 'M', 'ηι'), + (0x1FC4, 'M', 'ήι'), (0x1FC5, 'X'), (0x1FC6, 'V'), - (0x1FC7, 'M', u'ῆι'), - (0x1FC8, 'M', u'ὲ'), - (0x1FC9, 'M', u'έ'), - (0x1FCA, 'M', u'ὴ'), - (0x1FCB, 'M', u'ή'), - (0x1FCC, 'M', u'ηι'), - (0x1FCD, '3', u' ̓̀'), - (0x1FCE, '3', u' ̓́'), - (0x1FCF, '3', u' ̓͂'), + (0x1FC7, 'M', 'ῆι'), + (0x1FC8, 'M', 'ὲ'), + (0x1FC9, 'M', 'έ'), + (0x1FCA, 'M', 'ὴ'), + (0x1FCB, 'M', 'ή'), + (0x1FCC, 'M', 'ηι'), + (0x1FCD, '3', ' ̓̀'), + (0x1FCE, '3', ' ̓́'), + (0x1FCF, '3', ' ̓͂'), (0x1FD0, 'V'), - (0x1FD3, 'M', u'ΐ'), + (0x1FD3, 'M', 'ΐ'), (0x1FD4, 'X'), (0x1FD6, 'V'), - (0x1FD8, 'M', u'ῐ'), - (0x1FD9, 'M', u'ῑ'), - (0x1FDA, 'M', u'ὶ'), - (0x1FDB, 'M', u'ί'), + (0x1FD8, 'M', 'ῐ'), + (0x1FD9, 'M', 'ῑ'), + (0x1FDA, 'M', 'ὶ'), + (0x1FDB, 'M', 'ί'), (0x1FDC, 'X'), - (0x1FDD, '3', u' ̔̀'), - (0x1FDE, '3', u' ̔́'), - (0x1FDF, '3', u' ̔͂'), + (0x1FDD, '3', ' ̔̀'), + (0x1FDE, '3', ' ̔́'), + (0x1FDF, '3', ' ̔͂'), (0x1FE0, 'V'), - (0x1FE3, 'M', u'ΰ'), + (0x1FE3, 'M', 'ΰ'), (0x1FE4, 'V'), - (0x1FE8, 'M', u'ῠ'), - (0x1FE9, 'M', u'ῡ'), - (0x1FEA, 'M', u'ὺ'), - (0x1FEB, 'M', u'ύ'), - (0x1FEC, 'M', u'ῥ'), - (0x1FED, '3', u' ̈̀'), - (0x1FEE, '3', u' ̈́'), - (0x1FEF, '3', u'`'), + (0x1FE8, 'M', 'ῠ'), + (0x1FE9, 'M', 'ῡ'), + (0x1FEA, 'M', 'ὺ'), + (0x1FEB, 'M', 'ύ'), + (0x1FEC, 'M', 'ῥ'), + (0x1FED, '3', ' ̈̀'), + (0x1FEE, '3', ' ̈́'), + (0x1FEF, '3', '`'), (0x1FF0, 'X'), - (0x1FF2, 'M', u'ὼι'), - (0x1FF3, 'M', u'ωι'), + (0x1FF2, 'M', 'ὼι'), + (0x1FF3, 'M', 'ωι'), ] def _seg_21(): return [ - (0x1FF4, 'M', u'ώι'), + (0x1FF4, 'M', 'ώι'), (0x1FF5, 'X'), (0x1FF6, 'V'), - (0x1FF7, 'M', u'ῶι'), - (0x1FF8, 'M', u'ὸ'), - (0x1FF9, 'M', u'ό'), - (0x1FFA, 'M', u'ὼ'), - (0x1FFB, 'M', u'ώ'), - (0x1FFC, 'M', u'ωι'), - (0x1FFD, '3', u' ́'), - (0x1FFE, '3', u' ̔'), + (0x1FF7, 'M', 'ῶι'), + (0x1FF8, 'M', 'ὸ'), + (0x1FF9, 'M', 'ό'), + (0x1FFA, 'M', 'ὼ'), + (0x1FFB, 'M', 'ώ'), + (0x1FFC, 'M', 'ωι'), + (0x1FFD, '3', ' ́'), + (0x1FFE, '3', ' ̔'), (0x1FFF, 'X'), - (0x2000, '3', u' '), + (0x2000, '3', ' '), (0x200B, 'I'), - (0x200C, 'D', u''), + (0x200C, 'D', ''), (0x200E, 'X'), (0x2010, 'V'), - (0x2011, 'M', u'‐'), + (0x2011, 'M', '‐'), (0x2012, 'V'), - (0x2017, '3', u' ̳'), + (0x2017, '3', ' ̳'), (0x2018, 'V'), (0x2024, 'X'), (0x2027, 'V'), (0x2028, 'X'), - (0x202F, '3', u' '), + (0x202F, '3', ' '), (0x2030, 'V'), - (0x2033, 'M', u'′′'), - (0x2034, 'M', u'′′′'), + (0x2033, 'M', '′′'), + (0x2034, 'M', '′′′'), (0x2035, 'V'), - (0x2036, 'M', u'‵‵'), - (0x2037, 'M', u'‵‵‵'), + (0x2036, 'M', '‵‵'), + (0x2037, 'M', '‵‵‵'), (0x2038, 'V'), - (0x203C, '3', u'!!'), + (0x203C, '3', '!!'), (0x203D, 'V'), - (0x203E, '3', u' ̅'), + (0x203E, '3', ' ̅'), (0x203F, 'V'), - (0x2047, '3', u'??'), - (0x2048, '3', u'?!'), - (0x2049, '3', u'!?'), + (0x2047, '3', '??'), + (0x2048, '3', '?!'), + (0x2049, '3', '!?'), (0x204A, 'V'), - (0x2057, 'M', u'′′′′'), + (0x2057, 'M', '′′′′'), (0x2058, 'V'), - (0x205F, '3', u' '), + (0x205F, '3', ' '), (0x2060, 'I'), (0x2061, 'X'), (0x2064, 'I'), (0x2065, 'X'), - (0x2070, 'M', u'0'), - (0x2071, 'M', u'i'), + (0x2070, 'M', '0'), + (0x2071, 'M', 'i'), (0x2072, 'X'), - (0x2074, 'M', u'4'), - (0x2075, 'M', u'5'), - (0x2076, 'M', u'6'), - (0x2077, 'M', u'7'), - (0x2078, 'M', u'8'), - (0x2079, 'M', u'9'), - (0x207A, '3', u'+'), - (0x207B, 'M', u'−'), - (0x207C, '3', u'='), - (0x207D, '3', u'('), - (0x207E, '3', u')'), - (0x207F, 'M', u'n'), - (0x2080, 'M', u'0'), - (0x2081, 'M', u'1'), - (0x2082, 'M', u'2'), - (0x2083, 'M', u'3'), - (0x2084, 'M', u'4'), - (0x2085, 'M', u'5'), - (0x2086, 'M', u'6'), - (0x2087, 'M', u'7'), - (0x2088, 'M', u'8'), - (0x2089, 'M', u'9'), - (0x208A, '3', u'+'), - (0x208B, 'M', u'−'), - (0x208C, '3', u'='), - (0x208D, '3', u'('), - (0x208E, '3', u')'), + (0x2074, 'M', '4'), + (0x2075, 'M', '5'), + (0x2076, 'M', '6'), + (0x2077, 'M', '7'), + (0x2078, 'M', '8'), + (0x2079, 'M', '9'), + (0x207A, '3', '+'), + (0x207B, 'M', '−'), + (0x207C, '3', '='), + (0x207D, '3', '('), + (0x207E, '3', ')'), + (0x207F, 'M', 'n'), + (0x2080, 'M', '0'), + (0x2081, 'M', '1'), + (0x2082, 'M', '2'), + (0x2083, 'M', '3'), + (0x2084, 'M', '4'), + (0x2085, 'M', '5'), + (0x2086, 'M', '6'), + (0x2087, 'M', '7'), + (0x2088, 'M', '8'), + (0x2089, 'M', '9'), + (0x208A, '3', '+'), + (0x208B, 'M', '−'), + (0x208C, '3', '='), + (0x208D, '3', '('), + (0x208E, '3', ')'), (0x208F, 'X'), - (0x2090, 'M', u'a'), - (0x2091, 'M', u'e'), - (0x2092, 'M', u'o'), - (0x2093, 'M', u'x'), - (0x2094, 'M', u'ə'), - (0x2095, 'M', u'h'), - (0x2096, 'M', u'k'), - (0x2097, 'M', u'l'), - (0x2098, 'M', u'm'), - (0x2099, 'M', u'n'), - (0x209A, 'M', u'p'), - (0x209B, 'M', u's'), - (0x209C, 'M', u't'), + (0x2090, 'M', 'a'), + (0x2091, 'M', 'e'), + (0x2092, 'M', 'o'), + (0x2093, 'M', 'x'), + (0x2094, 'M', 'ə'), + (0x2095, 'M', 'h'), + (0x2096, 'M', 'k'), + (0x2097, 'M', 'l'), + (0x2098, 'M', 'm'), + (0x2099, 'M', 'n'), + (0x209A, 'M', 'p'), + (0x209B, 'M', 's'), + (0x209C, 'M', 't'), (0x209D, 'X'), (0x20A0, 'V'), - (0x20A8, 'M', u'rs'), + (0x20A8, 'M', 'rs'), (0x20A9, 'V'), (0x20C0, 'X'), (0x20D0, 'V'), (0x20F1, 'X'), - (0x2100, '3', u'a/c'), - (0x2101, '3', u'a/s'), + (0x2100, '3', 'a/c'), + (0x2101, '3', 'a/s'), ] def _seg_22(): return [ - (0x2102, 'M', u'c'), - (0x2103, 'M', u'°c'), + (0x2102, 'M', 'c'), + (0x2103, 'M', '°c'), (0x2104, 'V'), - (0x2105, '3', u'c/o'), - (0x2106, '3', u'c/u'), - (0x2107, 'M', u'ɛ'), + (0x2105, '3', 'c/o'), + (0x2106, '3', 'c/u'), + (0x2107, 'M', 'ɛ'), (0x2108, 'V'), - (0x2109, 'M', u'°f'), - (0x210A, 'M', u'g'), - (0x210B, 'M', u'h'), - (0x210F, 'M', u'ħ'), - (0x2110, 'M', u'i'), - (0x2112, 'M', u'l'), + (0x2109, 'M', '°f'), + (0x210A, 'M', 'g'), + (0x210B, 'M', 'h'), + (0x210F, 'M', 'ħ'), + (0x2110, 'M', 'i'), + (0x2112, 'M', 'l'), (0x2114, 'V'), - (0x2115, 'M', u'n'), - (0x2116, 'M', u'no'), + (0x2115, 'M', 'n'), + (0x2116, 'M', 'no'), (0x2117, 'V'), - (0x2119, 'M', u'p'), - (0x211A, 'M', u'q'), - (0x211B, 'M', u'r'), + (0x2119, 'M', 'p'), + (0x211A, 'M', 'q'), + (0x211B, 'M', 'r'), (0x211E, 'V'), - (0x2120, 'M', u'sm'), - (0x2121, 'M', u'tel'), - (0x2122, 'M', u'tm'), + (0x2120, 'M', 'sm'), + (0x2121, 'M', 'tel'), + (0x2122, 'M', 'tm'), (0x2123, 'V'), - (0x2124, 'M', u'z'), + (0x2124, 'M', 'z'), (0x2125, 'V'), - (0x2126, 'M', u'ω'), + (0x2126, 'M', 'ω'), (0x2127, 'V'), - (0x2128, 'M', u'z'), + (0x2128, 'M', 'z'), (0x2129, 'V'), - (0x212A, 'M', u'k'), - (0x212B, 'M', u'å'), - (0x212C, 'M', u'b'), - (0x212D, 'M', u'c'), + (0x212A, 'M', 'k'), + (0x212B, 'M', 'å'), + (0x212C, 'M', 'b'), + (0x212D, 'M', 'c'), (0x212E, 'V'), - (0x212F, 'M', u'e'), - (0x2131, 'M', u'f'), + (0x212F, 'M', 'e'), + (0x2131, 'M', 'f'), (0x2132, 'X'), - (0x2133, 'M', u'm'), - (0x2134, 'M', u'o'), - (0x2135, 'M', u'א'), - (0x2136, 'M', u'ב'), - (0x2137, 'M', u'ג'), - (0x2138, 'M', u'ד'), - (0x2139, 'M', u'i'), + (0x2133, 'M', 'm'), + (0x2134, 'M', 'o'), + (0x2135, 'M', 'א'), + (0x2136, 'M', 'ב'), + (0x2137, 'M', 'ג'), + (0x2138, 'M', 'ד'), + (0x2139, 'M', 'i'), (0x213A, 'V'), - (0x213B, 'M', u'fax'), - (0x213C, 'M', u'π'), - (0x213D, 'M', u'γ'), - (0x213F, 'M', u'π'), - (0x2140, 'M', u'∑'), + (0x213B, 'M', 'fax'), + (0x213C, 'M', 'π'), + (0x213D, 'M', 'γ'), + (0x213F, 'M', 'π'), + (0x2140, 'M', '∑'), (0x2141, 'V'), - (0x2145, 'M', u'd'), - (0x2147, 'M', u'e'), - (0x2148, 'M', u'i'), - (0x2149, 'M', u'j'), + (0x2145, 'M', 'd'), + (0x2147, 'M', 'e'), + (0x2148, 'M', 'i'), + (0x2149, 'M', 'j'), (0x214A, 'V'), - (0x2150, 'M', u'1⁄7'), - (0x2151, 'M', u'1⁄9'), - (0x2152, 'M', u'1⁄10'), - (0x2153, 'M', u'1⁄3'), - (0x2154, 'M', u'2⁄3'), - (0x2155, 'M', u'1⁄5'), - (0x2156, 'M', u'2⁄5'), - (0x2157, 'M', u'3⁄5'), - (0x2158, 'M', u'4⁄5'), - (0x2159, 'M', u'1⁄6'), - (0x215A, 'M', u'5⁄6'), - (0x215B, 'M', u'1⁄8'), - (0x215C, 'M', u'3⁄8'), - (0x215D, 'M', u'5⁄8'), - (0x215E, 'M', u'7⁄8'), - (0x215F, 'M', u'1⁄'), - (0x2160, 'M', u'i'), - (0x2161, 'M', u'ii'), - (0x2162, 'M', u'iii'), - (0x2163, 'M', u'iv'), - (0x2164, 'M', u'v'), - (0x2165, 'M', u'vi'), - (0x2166, 'M', u'vii'), - (0x2167, 'M', u'viii'), - (0x2168, 'M', u'ix'), - (0x2169, 'M', u'x'), - (0x216A, 'M', u'xi'), - (0x216B, 'M', u'xii'), - (0x216C, 'M', u'l'), - (0x216D, 'M', u'c'), - (0x216E, 'M', u'd'), - (0x216F, 'M', u'm'), - (0x2170, 'M', u'i'), - (0x2171, 'M', u'ii'), - (0x2172, 'M', u'iii'), - (0x2173, 'M', u'iv'), - (0x2174, 'M', u'v'), - (0x2175, 'M', u'vi'), - (0x2176, 'M', u'vii'), - (0x2177, 'M', u'viii'), - (0x2178, 'M', u'ix'), - (0x2179, 'M', u'x'), + (0x2150, 'M', '1⁄7'), + (0x2151, 'M', '1⁄9'), + (0x2152, 'M', '1⁄10'), + (0x2153, 'M', '1⁄3'), + (0x2154, 'M', '2⁄3'), + (0x2155, 'M', '1⁄5'), + (0x2156, 'M', '2⁄5'), + (0x2157, 'M', '3⁄5'), + (0x2158, 'M', '4⁄5'), + (0x2159, 'M', '1⁄6'), + (0x215A, 'M', '5⁄6'), + (0x215B, 'M', '1⁄8'), + (0x215C, 'M', '3⁄8'), + (0x215D, 'M', '5⁄8'), + (0x215E, 'M', '7⁄8'), + (0x215F, 'M', '1⁄'), + (0x2160, 'M', 'i'), + (0x2161, 'M', 'ii'), + (0x2162, 'M', 'iii'), + (0x2163, 'M', 'iv'), + (0x2164, 'M', 'v'), + (0x2165, 'M', 'vi'), + (0x2166, 'M', 'vii'), + (0x2167, 'M', 'viii'), + (0x2168, 'M', 'ix'), + (0x2169, 'M', 'x'), + (0x216A, 'M', 'xi'), + (0x216B, 'M', 'xii'), + (0x216C, 'M', 'l'), + (0x216D, 'M', 'c'), + (0x216E, 'M', 'd'), + (0x216F, 'M', 'm'), + (0x2170, 'M', 'i'), + (0x2171, 'M', 'ii'), + (0x2172, 'M', 'iii'), + (0x2173, 'M', 'iv'), + (0x2174, 'M', 'v'), + (0x2175, 'M', 'vi'), + (0x2176, 'M', 'vii'), + (0x2177, 'M', 'viii'), + (0x2178, 'M', 'ix'), + (0x2179, 'M', 'x'), ] def _seg_23(): return [ - (0x217A, 'M', u'xi'), - (0x217B, 'M', u'xii'), - (0x217C, 'M', u'l'), - (0x217D, 'M', u'c'), - (0x217E, 'M', u'd'), - (0x217F, 'M', u'm'), + (0x217A, 'M', 'xi'), + (0x217B, 'M', 'xii'), + (0x217C, 'M', 'l'), + (0x217D, 'M', 'c'), + (0x217E, 'M', 'd'), + (0x217F, 'M', 'm'), (0x2180, 'V'), (0x2183, 'X'), (0x2184, 'V'), - (0x2189, 'M', u'0⁄3'), + (0x2189, 'M', '0⁄3'), (0x218A, 'V'), (0x218C, 'X'), (0x2190, 'V'), - (0x222C, 'M', u'∫∫'), - (0x222D, 'M', u'∫∫∫'), + (0x222C, 'M', '∫∫'), + (0x222D, 'M', '∫∫∫'), (0x222E, 'V'), - (0x222F, 'M', u'∮∮'), - (0x2230, 'M', u'∮∮∮'), + (0x222F, 'M', '∮∮'), + (0x2230, 'M', '∮∮∮'), (0x2231, 'V'), (0x2260, '3'), (0x2261, 'V'), (0x226E, '3'), (0x2270, 'V'), - (0x2329, 'M', u'〈'), - (0x232A, 'M', u'〉'), + (0x2329, 'M', '〈'), + (0x232A, 'M', '〉'), (0x232B, 'V'), (0x2427, 'X'), (0x2440, 'V'), (0x244B, 'X'), - (0x2460, 'M', u'1'), - (0x2461, 'M', u'2'), - (0x2462, 'M', u'3'), - (0x2463, 'M', u'4'), - (0x2464, 'M', u'5'), - (0x2465, 'M', u'6'), - (0x2466, 'M', u'7'), - (0x2467, 'M', u'8'), - (0x2468, 'M', u'9'), - (0x2469, 'M', u'10'), - (0x246A, 'M', u'11'), - (0x246B, 'M', u'12'), - (0x246C, 'M', u'13'), - (0x246D, 'M', u'14'), - (0x246E, 'M', u'15'), - (0x246F, 'M', u'16'), - (0x2470, 'M', u'17'), - (0x2471, 'M', u'18'), - (0x2472, 'M', u'19'), - (0x2473, 'M', u'20'), - (0x2474, '3', u'(1)'), - (0x2475, '3', u'(2)'), - (0x2476, '3', u'(3)'), - (0x2477, '3', u'(4)'), - (0x2478, '3', u'(5)'), - (0x2479, '3', u'(6)'), - (0x247A, '3', u'(7)'), - (0x247B, '3', u'(8)'), - (0x247C, '3', u'(9)'), - (0x247D, '3', u'(10)'), - (0x247E, '3', u'(11)'), - (0x247F, '3', u'(12)'), - (0x2480, '3', u'(13)'), - (0x2481, '3', u'(14)'), - (0x2482, '3', u'(15)'), - (0x2483, '3', u'(16)'), - (0x2484, '3', u'(17)'), - (0x2485, '3', u'(18)'), - (0x2486, '3', u'(19)'), - (0x2487, '3', u'(20)'), + (0x2460, 'M', '1'), + (0x2461, 'M', '2'), + (0x2462, 'M', '3'), + (0x2463, 'M', '4'), + (0x2464, 'M', '5'), + (0x2465, 'M', '6'), + (0x2466, 'M', '7'), + (0x2467, 'M', '8'), + (0x2468, 'M', '9'), + (0x2469, 'M', '10'), + (0x246A, 'M', '11'), + (0x246B, 'M', '12'), + (0x246C, 'M', '13'), + (0x246D, 'M', '14'), + (0x246E, 'M', '15'), + (0x246F, 'M', '16'), + (0x2470, 'M', '17'), + (0x2471, 'M', '18'), + (0x2472, 'M', '19'), + (0x2473, 'M', '20'), + (0x2474, '3', '(1)'), + (0x2475, '3', '(2)'), + (0x2476, '3', '(3)'), + (0x2477, '3', '(4)'), + (0x2478, '3', '(5)'), + (0x2479, '3', '(6)'), + (0x247A, '3', '(7)'), + (0x247B, '3', '(8)'), + (0x247C, '3', '(9)'), + (0x247D, '3', '(10)'), + (0x247E, '3', '(11)'), + (0x247F, '3', '(12)'), + (0x2480, '3', '(13)'), + (0x2481, '3', '(14)'), + (0x2482, '3', '(15)'), + (0x2483, '3', '(16)'), + (0x2484, '3', '(17)'), + (0x2485, '3', '(18)'), + (0x2486, '3', '(19)'), + (0x2487, '3', '(20)'), (0x2488, 'X'), - (0x249C, '3', u'(a)'), - (0x249D, '3', u'(b)'), - (0x249E, '3', u'(c)'), - (0x249F, '3', u'(d)'), - (0x24A0, '3', u'(e)'), - (0x24A1, '3', u'(f)'), - (0x24A2, '3', u'(g)'), - (0x24A3, '3', u'(h)'), - (0x24A4, '3', u'(i)'), - (0x24A5, '3', u'(j)'), - (0x24A6, '3', u'(k)'), - (0x24A7, '3', u'(l)'), - (0x24A8, '3', u'(m)'), - (0x24A9, '3', u'(n)'), - (0x24AA, '3', u'(o)'), - (0x24AB, '3', u'(p)'), - (0x24AC, '3', u'(q)'), - (0x24AD, '3', u'(r)'), - (0x24AE, '3', u'(s)'), - (0x24AF, '3', u'(t)'), - (0x24B0, '3', u'(u)'), - (0x24B1, '3', u'(v)'), - (0x24B2, '3', u'(w)'), - (0x24B3, '3', u'(x)'), - (0x24B4, '3', u'(y)'), - (0x24B5, '3', u'(z)'), - (0x24B6, 'M', u'a'), - (0x24B7, 'M', u'b'), - (0x24B8, 'M', u'c'), - (0x24B9, 'M', u'd'), + (0x249C, '3', '(a)'), + (0x249D, '3', '(b)'), + (0x249E, '3', '(c)'), + (0x249F, '3', '(d)'), + (0x24A0, '3', '(e)'), + (0x24A1, '3', '(f)'), + (0x24A2, '3', '(g)'), + (0x24A3, '3', '(h)'), + (0x24A4, '3', '(i)'), + (0x24A5, '3', '(j)'), + (0x24A6, '3', '(k)'), + (0x24A7, '3', '(l)'), + (0x24A8, '3', '(m)'), + (0x24A9, '3', '(n)'), + (0x24AA, '3', '(o)'), + (0x24AB, '3', '(p)'), + (0x24AC, '3', '(q)'), + (0x24AD, '3', '(r)'), + (0x24AE, '3', '(s)'), + (0x24AF, '3', '(t)'), + (0x24B0, '3', '(u)'), + (0x24B1, '3', '(v)'), + (0x24B2, '3', '(w)'), + (0x24B3, '3', '(x)'), + (0x24B4, '3', '(y)'), + (0x24B5, '3', '(z)'), + (0x24B6, 'M', 'a'), + (0x24B7, 'M', 'b'), + (0x24B8, 'M', 'c'), + (0x24B9, 'M', 'd'), ] def _seg_24(): return [ - (0x24BA, 'M', u'e'), - (0x24BB, 'M', u'f'), - (0x24BC, 'M', u'g'), - (0x24BD, 'M', u'h'), - (0x24BE, 'M', u'i'), - (0x24BF, 'M', u'j'), - (0x24C0, 'M', u'k'), - (0x24C1, 'M', u'l'), - (0x24C2, 'M', u'm'), - (0x24C3, 'M', u'n'), - (0x24C4, 'M', u'o'), - (0x24C5, 'M', u'p'), - (0x24C6, 'M', u'q'), - (0x24C7, 'M', u'r'), - (0x24C8, 'M', u's'), - (0x24C9, 'M', u't'), - (0x24CA, 'M', u'u'), - (0x24CB, 'M', u'v'), - (0x24CC, 'M', u'w'), - (0x24CD, 'M', u'x'), - (0x24CE, 'M', u'y'), - (0x24CF, 'M', u'z'), - (0x24D0, 'M', u'a'), - (0x24D1, 'M', u'b'), - (0x24D2, 'M', u'c'), - (0x24D3, 'M', u'd'), - (0x24D4, 'M', u'e'), - (0x24D5, 'M', u'f'), - (0x24D6, 'M', u'g'), - (0x24D7, 'M', u'h'), - (0x24D8, 'M', u'i'), - (0x24D9, 'M', u'j'), - (0x24DA, 'M', u'k'), - (0x24DB, 'M', u'l'), - (0x24DC, 'M', u'm'), - (0x24DD, 'M', u'n'), - (0x24DE, 'M', u'o'), - (0x24DF, 'M', u'p'), - (0x24E0, 'M', u'q'), - (0x24E1, 'M', u'r'), - (0x24E2, 'M', u's'), - (0x24E3, 'M', u't'), - (0x24E4, 'M', u'u'), - (0x24E5, 'M', u'v'), - (0x24E6, 'M', u'w'), - (0x24E7, 'M', u'x'), - (0x24E8, 'M', u'y'), - (0x24E9, 'M', u'z'), - (0x24EA, 'M', u'0'), + (0x24BA, 'M', 'e'), + (0x24BB, 'M', 'f'), + (0x24BC, 'M', 'g'), + (0x24BD, 'M', 'h'), + (0x24BE, 'M', 'i'), + (0x24BF, 'M', 'j'), + (0x24C0, 'M', 'k'), + (0x24C1, 'M', 'l'), + (0x24C2, 'M', 'm'), + (0x24C3, 'M', 'n'), + (0x24C4, 'M', 'o'), + (0x24C5, 'M', 'p'), + (0x24C6, 'M', 'q'), + (0x24C7, 'M', 'r'), + (0x24C8, 'M', 's'), + (0x24C9, 'M', 't'), + (0x24CA, 'M', 'u'), + (0x24CB, 'M', 'v'), + (0x24CC, 'M', 'w'), + (0x24CD, 'M', 'x'), + (0x24CE, 'M', 'y'), + (0x24CF, 'M', 'z'), + (0x24D0, 'M', 'a'), + (0x24D1, 'M', 'b'), + (0x24D2, 'M', 'c'), + (0x24D3, 'M', 'd'), + (0x24D4, 'M', 'e'), + (0x24D5, 'M', 'f'), + (0x24D6, 'M', 'g'), + (0x24D7, 'M', 'h'), + (0x24D8, 'M', 'i'), + (0x24D9, 'M', 'j'), + (0x24DA, 'M', 'k'), + (0x24DB, 'M', 'l'), + (0x24DC, 'M', 'm'), + (0x24DD, 'M', 'n'), + (0x24DE, 'M', 'o'), + (0x24DF, 'M', 'p'), + (0x24E0, 'M', 'q'), + (0x24E1, 'M', 'r'), + (0x24E2, 'M', 's'), + (0x24E3, 'M', 't'), + (0x24E4, 'M', 'u'), + (0x24E5, 'M', 'v'), + (0x24E6, 'M', 'w'), + (0x24E7, 'M', 'x'), + (0x24E8, 'M', 'y'), + (0x24E9, 'M', 'z'), + (0x24EA, 'M', '0'), (0x24EB, 'V'), - (0x2A0C, 'M', u'∫∫∫∫'), + (0x2A0C, 'M', '∫∫∫∫'), (0x2A0D, 'V'), - (0x2A74, '3', u'::='), - (0x2A75, '3', u'=='), - (0x2A76, '3', u'==='), + (0x2A74, '3', '::='), + (0x2A75, '3', '=='), + (0x2A76, '3', '==='), (0x2A77, 'V'), - (0x2ADC, 'M', u'⫝̸'), + (0x2ADC, 'M', '⫝̸'), (0x2ADD, 'V'), (0x2B74, 'X'), (0x2B76, 'V'), (0x2B96, 'X'), (0x2B97, 'V'), - (0x2C00, 'M', u'ⰰ'), - (0x2C01, 'M', u'ⰱ'), - (0x2C02, 'M', u'ⰲ'), - (0x2C03, 'M', u'ⰳ'), - (0x2C04, 'M', u'ⰴ'), - (0x2C05, 'M', u'ⰵ'), - (0x2C06, 'M', u'ⰶ'), - (0x2C07, 'M', u'ⰷ'), - (0x2C08, 'M', u'ⰸ'), - (0x2C09, 'M', u'ⰹ'), - (0x2C0A, 'M', u'ⰺ'), - (0x2C0B, 'M', u'ⰻ'), - (0x2C0C, 'M', u'ⰼ'), - (0x2C0D, 'M', u'ⰽ'), - (0x2C0E, 'M', u'ⰾ'), - (0x2C0F, 'M', u'ⰿ'), - (0x2C10, 'M', u'ⱀ'), - (0x2C11, 'M', u'ⱁ'), - (0x2C12, 'M', u'ⱂ'), - (0x2C13, 'M', u'ⱃ'), - (0x2C14, 'M', u'ⱄ'), - (0x2C15, 'M', u'ⱅ'), - (0x2C16, 'M', u'ⱆ'), - (0x2C17, 'M', u'ⱇ'), - (0x2C18, 'M', u'ⱈ'), - (0x2C19, 'M', u'ⱉ'), - (0x2C1A, 'M', u'ⱊ'), - (0x2C1B, 'M', u'ⱋ'), - (0x2C1C, 'M', u'ⱌ'), - (0x2C1D, 'M', u'ⱍ'), - (0x2C1E, 'M', u'ⱎ'), - (0x2C1F, 'M', u'ⱏ'), - (0x2C20, 'M', u'ⱐ'), - (0x2C21, 'M', u'ⱑ'), - (0x2C22, 'M', u'ⱒ'), - (0x2C23, 'M', u'ⱓ'), - (0x2C24, 'M', u'ⱔ'), - (0x2C25, 'M', u'ⱕ'), + (0x2C00, 'M', 'ⰰ'), + (0x2C01, 'M', 'ⰱ'), + (0x2C02, 'M', 'ⰲ'), + (0x2C03, 'M', 'ⰳ'), + (0x2C04, 'M', 'ⰴ'), + (0x2C05, 'M', 'ⰵ'), + (0x2C06, 'M', 'ⰶ'), + (0x2C07, 'M', 'ⰷ'), + (0x2C08, 'M', 'ⰸ'), + (0x2C09, 'M', 'ⰹ'), + (0x2C0A, 'M', 'ⰺ'), + (0x2C0B, 'M', 'ⰻ'), + (0x2C0C, 'M', 'ⰼ'), + (0x2C0D, 'M', 'ⰽ'), + (0x2C0E, 'M', 'ⰾ'), + (0x2C0F, 'M', 'ⰿ'), + (0x2C10, 'M', 'ⱀ'), + (0x2C11, 'M', 'ⱁ'), + (0x2C12, 'M', 'ⱂ'), + (0x2C13, 'M', 'ⱃ'), + (0x2C14, 'M', 'ⱄ'), + (0x2C15, 'M', 'ⱅ'), + (0x2C16, 'M', 'ⱆ'), + (0x2C17, 'M', 'ⱇ'), + (0x2C18, 'M', 'ⱈ'), + (0x2C19, 'M', 'ⱉ'), + (0x2C1A, 'M', 'ⱊ'), + (0x2C1B, 'M', 'ⱋ'), + (0x2C1C, 'M', 'ⱌ'), + (0x2C1D, 'M', 'ⱍ'), + (0x2C1E, 'M', 'ⱎ'), + (0x2C1F, 'M', 'ⱏ'), + (0x2C20, 'M', 'ⱐ'), + (0x2C21, 'M', 'ⱑ'), + (0x2C22, 'M', 'ⱒ'), + (0x2C23, 'M', 'ⱓ'), + (0x2C24, 'M', 'ⱔ'), + (0x2C25, 'M', 'ⱕ'), ] def _seg_25(): return [ - (0x2C26, 'M', u'ⱖ'), - (0x2C27, 'M', u'ⱗ'), - (0x2C28, 'M', u'ⱘ'), - (0x2C29, 'M', u'ⱙ'), - (0x2C2A, 'M', u'ⱚ'), - (0x2C2B, 'M', u'ⱛ'), - (0x2C2C, 'M', u'ⱜ'), - (0x2C2D, 'M', u'ⱝ'), - (0x2C2E, 'M', u'ⱞ'), + (0x2C26, 'M', 'ⱖ'), + (0x2C27, 'M', 'ⱗ'), + (0x2C28, 'M', 'ⱘ'), + (0x2C29, 'M', 'ⱙ'), + (0x2C2A, 'M', 'ⱚ'), + (0x2C2B, 'M', 'ⱛ'), + (0x2C2C, 'M', 'ⱜ'), + (0x2C2D, 'M', 'ⱝ'), + (0x2C2E, 'M', 'ⱞ'), (0x2C2F, 'X'), (0x2C30, 'V'), (0x2C5F, 'X'), - (0x2C60, 'M', u'ⱡ'), + (0x2C60, 'M', 'ⱡ'), (0x2C61, 'V'), - (0x2C62, 'M', u'ɫ'), - (0x2C63, 'M', u'ᵽ'), - (0x2C64, 'M', u'ɽ'), + (0x2C62, 'M', 'ɫ'), + (0x2C63, 'M', 'ᵽ'), + (0x2C64, 'M', 'ɽ'), (0x2C65, 'V'), - (0x2C67, 'M', u'ⱨ'), + (0x2C67, 'M', 'ⱨ'), (0x2C68, 'V'), - (0x2C69, 'M', u'ⱪ'), + (0x2C69, 'M', 'ⱪ'), (0x2C6A, 'V'), - (0x2C6B, 'M', u'ⱬ'), + (0x2C6B, 'M', 'ⱬ'), (0x2C6C, 'V'), - (0x2C6D, 'M', u'ɑ'), - (0x2C6E, 'M', u'ɱ'), - (0x2C6F, 'M', u'ɐ'), - (0x2C70, 'M', u'ɒ'), + (0x2C6D, 'M', 'ɑ'), + (0x2C6E, 'M', 'ɱ'), + (0x2C6F, 'M', 'ɐ'), + (0x2C70, 'M', 'ɒ'), (0x2C71, 'V'), - (0x2C72, 'M', u'ⱳ'), + (0x2C72, 'M', 'ⱳ'), (0x2C73, 'V'), - (0x2C75, 'M', u'ⱶ'), + (0x2C75, 'M', 'ⱶ'), (0x2C76, 'V'), - (0x2C7C, 'M', u'j'), - (0x2C7D, 'M', u'v'), - (0x2C7E, 'M', u'ȿ'), - (0x2C7F, 'M', u'ɀ'), - (0x2C80, 'M', u'ⲁ'), + (0x2C7C, 'M', 'j'), + (0x2C7D, 'M', 'v'), + (0x2C7E, 'M', 'ȿ'), + (0x2C7F, 'M', 'ɀ'), + (0x2C80, 'M', 'ⲁ'), (0x2C81, 'V'), - (0x2C82, 'M', u'ⲃ'), + (0x2C82, 'M', 'ⲃ'), (0x2C83, 'V'), - (0x2C84, 'M', u'ⲅ'), + (0x2C84, 'M', 'ⲅ'), (0x2C85, 'V'), - (0x2C86, 'M', u'ⲇ'), + (0x2C86, 'M', 'ⲇ'), (0x2C87, 'V'), - (0x2C88, 'M', u'ⲉ'), + (0x2C88, 'M', 'ⲉ'), (0x2C89, 'V'), - (0x2C8A, 'M', u'ⲋ'), + (0x2C8A, 'M', 'ⲋ'), (0x2C8B, 'V'), - (0x2C8C, 'M', u'ⲍ'), + (0x2C8C, 'M', 'ⲍ'), (0x2C8D, 'V'), - (0x2C8E, 'M', u'ⲏ'), + (0x2C8E, 'M', 'ⲏ'), (0x2C8F, 'V'), - (0x2C90, 'M', u'ⲑ'), + (0x2C90, 'M', 'ⲑ'), (0x2C91, 'V'), - (0x2C92, 'M', u'ⲓ'), + (0x2C92, 'M', 'ⲓ'), (0x2C93, 'V'), - (0x2C94, 'M', u'ⲕ'), + (0x2C94, 'M', 'ⲕ'), (0x2C95, 'V'), - (0x2C96, 'M', u'ⲗ'), + (0x2C96, 'M', 'ⲗ'), (0x2C97, 'V'), - (0x2C98, 'M', u'ⲙ'), + (0x2C98, 'M', 'ⲙ'), (0x2C99, 'V'), - (0x2C9A, 'M', u'ⲛ'), + (0x2C9A, 'M', 'ⲛ'), (0x2C9B, 'V'), - (0x2C9C, 'M', u'ⲝ'), + (0x2C9C, 'M', 'ⲝ'), (0x2C9D, 'V'), - (0x2C9E, 'M', u'ⲟ'), + (0x2C9E, 'M', 'ⲟ'), (0x2C9F, 'V'), - (0x2CA0, 'M', u'ⲡ'), + (0x2CA0, 'M', 'ⲡ'), (0x2CA1, 'V'), - (0x2CA2, 'M', u'ⲣ'), + (0x2CA2, 'M', 'ⲣ'), (0x2CA3, 'V'), - (0x2CA4, 'M', u'ⲥ'), + (0x2CA4, 'M', 'ⲥ'), (0x2CA5, 'V'), - (0x2CA6, 'M', u'ⲧ'), + (0x2CA6, 'M', 'ⲧ'), (0x2CA7, 'V'), - (0x2CA8, 'M', u'ⲩ'), + (0x2CA8, 'M', 'ⲩ'), (0x2CA9, 'V'), - (0x2CAA, 'M', u'ⲫ'), + (0x2CAA, 'M', 'ⲫ'), (0x2CAB, 'V'), - (0x2CAC, 'M', u'ⲭ'), + (0x2CAC, 'M', 'ⲭ'), (0x2CAD, 'V'), - (0x2CAE, 'M', u'ⲯ'), + (0x2CAE, 'M', 'ⲯ'), (0x2CAF, 'V'), - (0x2CB0, 'M', u'ⲱ'), + (0x2CB0, 'M', 'ⲱ'), (0x2CB1, 'V'), - (0x2CB2, 'M', u'ⲳ'), + (0x2CB2, 'M', 'ⲳ'), (0x2CB3, 'V'), - (0x2CB4, 'M', u'ⲵ'), + (0x2CB4, 'M', 'ⲵ'), (0x2CB5, 'V'), - (0x2CB6, 'M', u'ⲷ'), + (0x2CB6, 'M', 'ⲷ'), (0x2CB7, 'V'), - (0x2CB8, 'M', u'ⲹ'), + (0x2CB8, 'M', 'ⲹ'), (0x2CB9, 'V'), - (0x2CBA, 'M', u'ⲻ'), + (0x2CBA, 'M', 'ⲻ'), (0x2CBB, 'V'), - (0x2CBC, 'M', u'ⲽ'), + (0x2CBC, 'M', 'ⲽ'), (0x2CBD, 'V'), - (0x2CBE, 'M', u'ⲿ'), + (0x2CBE, 'M', 'ⲿ'), ] def _seg_26(): return [ (0x2CBF, 'V'), - (0x2CC0, 'M', u'ⳁ'), + (0x2CC0, 'M', 'ⳁ'), (0x2CC1, 'V'), - (0x2CC2, 'M', u'ⳃ'), + (0x2CC2, 'M', 'ⳃ'), (0x2CC3, 'V'), - (0x2CC4, 'M', u'ⳅ'), + (0x2CC4, 'M', 'ⳅ'), (0x2CC5, 'V'), - (0x2CC6, 'M', u'ⳇ'), + (0x2CC6, 'M', 'ⳇ'), (0x2CC7, 'V'), - (0x2CC8, 'M', u'ⳉ'), + (0x2CC8, 'M', 'ⳉ'), (0x2CC9, 'V'), - (0x2CCA, 'M', u'ⳋ'), + (0x2CCA, 'M', 'ⳋ'), (0x2CCB, 'V'), - (0x2CCC, 'M', u'ⳍ'), + (0x2CCC, 'M', 'ⳍ'), (0x2CCD, 'V'), - (0x2CCE, 'M', u'ⳏ'), + (0x2CCE, 'M', 'ⳏ'), (0x2CCF, 'V'), - (0x2CD0, 'M', u'ⳑ'), + (0x2CD0, 'M', 'ⳑ'), (0x2CD1, 'V'), - (0x2CD2, 'M', u'ⳓ'), + (0x2CD2, 'M', 'ⳓ'), (0x2CD3, 'V'), - (0x2CD4, 'M', u'ⳕ'), + (0x2CD4, 'M', 'ⳕ'), (0x2CD5, 'V'), - (0x2CD6, 'M', u'ⳗ'), + (0x2CD6, 'M', 'ⳗ'), (0x2CD7, 'V'), - (0x2CD8, 'M', u'ⳙ'), + (0x2CD8, 'M', 'ⳙ'), (0x2CD9, 'V'), - (0x2CDA, 'M', u'ⳛ'), + (0x2CDA, 'M', 'ⳛ'), (0x2CDB, 'V'), - (0x2CDC, 'M', u'ⳝ'), + (0x2CDC, 'M', 'ⳝ'), (0x2CDD, 'V'), - (0x2CDE, 'M', u'ⳟ'), + (0x2CDE, 'M', 'ⳟ'), (0x2CDF, 'V'), - (0x2CE0, 'M', u'ⳡ'), + (0x2CE0, 'M', 'ⳡ'), (0x2CE1, 'V'), - (0x2CE2, 'M', u'ⳣ'), + (0x2CE2, 'M', 'ⳣ'), (0x2CE3, 'V'), - (0x2CEB, 'M', u'ⳬ'), + (0x2CEB, 'M', 'ⳬ'), (0x2CEC, 'V'), - (0x2CED, 'M', u'ⳮ'), + (0x2CED, 'M', 'ⳮ'), (0x2CEE, 'V'), - (0x2CF2, 'M', u'ⳳ'), + (0x2CF2, 'M', 'ⳳ'), (0x2CF3, 'V'), (0x2CF4, 'X'), (0x2CF9, 'V'), @@ -2763,7 +2762,7 @@ def _seg_26(): (0x2D2E, 'X'), (0x2D30, 'V'), (0x2D68, 'X'), - (0x2D6F, 'M', u'ⵡ'), + (0x2D6F, 'M', 'ⵡ'), (0x2D70, 'V'), (0x2D71, 'X'), (0x2D7F, 'V'), @@ -2789,902 +2788,902 @@ def _seg_26(): (0x2E80, 'V'), (0x2E9A, 'X'), (0x2E9B, 'V'), - (0x2E9F, 'M', u'母'), + (0x2E9F, 'M', '母'), (0x2EA0, 'V'), - (0x2EF3, 'M', u'龟'), + (0x2EF3, 'M', '龟'), (0x2EF4, 'X'), - (0x2F00, 'M', u'一'), - (0x2F01, 'M', u'丨'), - (0x2F02, 'M', u'丶'), - (0x2F03, 'M', u'丿'), - (0x2F04, 'M', u'乙'), - (0x2F05, 'M', u'亅'), - (0x2F06, 'M', u'二'), - (0x2F07, 'M', u'亠'), - (0x2F08, 'M', u'人'), - (0x2F09, 'M', u'儿'), - (0x2F0A, 'M', u'入'), - (0x2F0B, 'M', u'八'), - (0x2F0C, 'M', u'冂'), - (0x2F0D, 'M', u'冖'), - (0x2F0E, 'M', u'冫'), - (0x2F0F, 'M', u'几'), - (0x2F10, 'M', u'凵'), - (0x2F11, 'M', u'刀'), + (0x2F00, 'M', '一'), + (0x2F01, 'M', '丨'), + (0x2F02, 'M', '丶'), + (0x2F03, 'M', '丿'), + (0x2F04, 'M', '乙'), + (0x2F05, 'M', '亅'), + (0x2F06, 'M', '二'), + (0x2F07, 'M', '亠'), + (0x2F08, 'M', '人'), + (0x2F09, 'M', '儿'), + (0x2F0A, 'M', '入'), + (0x2F0B, 'M', '八'), + (0x2F0C, 'M', '冂'), + (0x2F0D, 'M', '冖'), + (0x2F0E, 'M', '冫'), + (0x2F0F, 'M', '几'), + (0x2F10, 'M', '凵'), + (0x2F11, 'M', '刀'), ] def _seg_27(): return [ - (0x2F12, 'M', u'力'), - (0x2F13, 'M', u'勹'), - (0x2F14, 'M', u'匕'), - (0x2F15, 'M', u'匚'), - (0x2F16, 'M', u'匸'), - (0x2F17, 'M', u'十'), - (0x2F18, 'M', u'卜'), - (0x2F19, 'M', u'卩'), - (0x2F1A, 'M', u'厂'), - (0x2F1B, 'M', u'厶'), - (0x2F1C, 'M', u'又'), - (0x2F1D, 'M', u'口'), - (0x2F1E, 'M', u'囗'), - (0x2F1F, 'M', u'土'), - (0x2F20, 'M', u'士'), - (0x2F21, 'M', u'夂'), - (0x2F22, 'M', u'夊'), - (0x2F23, 'M', u'夕'), - (0x2F24, 'M', u'大'), - (0x2F25, 'M', u'女'), - (0x2F26, 'M', u'子'), - (0x2F27, 'M', u'宀'), - (0x2F28, 'M', u'寸'), - (0x2F29, 'M', u'小'), - (0x2F2A, 'M', u'尢'), - (0x2F2B, 'M', u'尸'), - (0x2F2C, 'M', u'屮'), - (0x2F2D, 'M', u'山'), - (0x2F2E, 'M', u'巛'), - (0x2F2F, 'M', u'工'), - (0x2F30, 'M', u'己'), - (0x2F31, 'M', u'巾'), - (0x2F32, 'M', u'干'), - (0x2F33, 'M', u'幺'), - (0x2F34, 'M', u'广'), - (0x2F35, 'M', u'廴'), - (0x2F36, 'M', u'廾'), - (0x2F37, 'M', u'弋'), - (0x2F38, 'M', u'弓'), - (0x2F39, 'M', u'彐'), - (0x2F3A, 'M', u'彡'), - (0x2F3B, 'M', u'彳'), - (0x2F3C, 'M', u'心'), - (0x2F3D, 'M', u'戈'), - (0x2F3E, 'M', u'戶'), - (0x2F3F, 'M', u'手'), - (0x2F40, 'M', u'支'), - (0x2F41, 'M', u'攴'), - (0x2F42, 'M', u'文'), - (0x2F43, 'M', u'斗'), - (0x2F44, 'M', u'斤'), - (0x2F45, 'M', u'方'), - (0x2F46, 'M', u'无'), - (0x2F47, 'M', u'日'), - (0x2F48, 'M', u'曰'), - (0x2F49, 'M', u'月'), - (0x2F4A, 'M', u'木'), - (0x2F4B, 'M', u'欠'), - (0x2F4C, 'M', u'止'), - (0x2F4D, 'M', u'歹'), - (0x2F4E, 'M', u'殳'), - (0x2F4F, 'M', u'毋'), - (0x2F50, 'M', u'比'), - (0x2F51, 'M', u'毛'), - (0x2F52, 'M', u'氏'), - (0x2F53, 'M', u'气'), - (0x2F54, 'M', u'水'), - (0x2F55, 'M', u'火'), - (0x2F56, 'M', u'爪'), - (0x2F57, 'M', u'父'), - (0x2F58, 'M', u'爻'), - (0x2F59, 'M', u'爿'), - (0x2F5A, 'M', u'片'), - (0x2F5B, 'M', u'牙'), - (0x2F5C, 'M', u'牛'), - (0x2F5D, 'M', u'犬'), - (0x2F5E, 'M', u'玄'), - (0x2F5F, 'M', u'玉'), - (0x2F60, 'M', u'瓜'), - (0x2F61, 'M', u'瓦'), - (0x2F62, 'M', u'甘'), - (0x2F63, 'M', u'生'), - (0x2F64, 'M', u'用'), - (0x2F65, 'M', u'田'), - (0x2F66, 'M', u'疋'), - (0x2F67, 'M', u'疒'), - (0x2F68, 'M', u'癶'), - (0x2F69, 'M', u'白'), - (0x2F6A, 'M', u'皮'), - (0x2F6B, 'M', u'皿'), - (0x2F6C, 'M', u'目'), - (0x2F6D, 'M', u'矛'), - (0x2F6E, 'M', u'矢'), - (0x2F6F, 'M', u'石'), - (0x2F70, 'M', u'示'), - (0x2F71, 'M', u'禸'), - (0x2F72, 'M', u'禾'), - (0x2F73, 'M', u'穴'), - (0x2F74, 'M', u'立'), - (0x2F75, 'M', u'竹'), + (0x2F12, 'M', '力'), + (0x2F13, 'M', '勹'), + (0x2F14, 'M', '匕'), + (0x2F15, 'M', '匚'), + (0x2F16, 'M', '匸'), + (0x2F17, 'M', '十'), + (0x2F18, 'M', '卜'), + (0x2F19, 'M', '卩'), + (0x2F1A, 'M', '厂'), + (0x2F1B, 'M', '厶'), + (0x2F1C, 'M', '又'), + (0x2F1D, 'M', '口'), + (0x2F1E, 'M', '囗'), + (0x2F1F, 'M', '土'), + (0x2F20, 'M', '士'), + (0x2F21, 'M', '夂'), + (0x2F22, 'M', '夊'), + (0x2F23, 'M', '夕'), + (0x2F24, 'M', '大'), + (0x2F25, 'M', '女'), + (0x2F26, 'M', '子'), + (0x2F27, 'M', '宀'), + (0x2F28, 'M', '寸'), + (0x2F29, 'M', '小'), + (0x2F2A, 'M', '尢'), + (0x2F2B, 'M', '尸'), + (0x2F2C, 'M', '屮'), + (0x2F2D, 'M', '山'), + (0x2F2E, 'M', '巛'), + (0x2F2F, 'M', '工'), + (0x2F30, 'M', '己'), + (0x2F31, 'M', '巾'), + (0x2F32, 'M', '干'), + (0x2F33, 'M', '幺'), + (0x2F34, 'M', '广'), + (0x2F35, 'M', '廴'), + (0x2F36, 'M', '廾'), + (0x2F37, 'M', '弋'), + (0x2F38, 'M', '弓'), + (0x2F39, 'M', '彐'), + (0x2F3A, 'M', '彡'), + (0x2F3B, 'M', '彳'), + (0x2F3C, 'M', '心'), + (0x2F3D, 'M', '戈'), + (0x2F3E, 'M', '戶'), + (0x2F3F, 'M', '手'), + (0x2F40, 'M', '支'), + (0x2F41, 'M', '攴'), + (0x2F42, 'M', '文'), + (0x2F43, 'M', '斗'), + (0x2F44, 'M', '斤'), + (0x2F45, 'M', '方'), + (0x2F46, 'M', '无'), + (0x2F47, 'M', '日'), + (0x2F48, 'M', '曰'), + (0x2F49, 'M', '月'), + (0x2F4A, 'M', '木'), + (0x2F4B, 'M', '欠'), + (0x2F4C, 'M', '止'), + (0x2F4D, 'M', '歹'), + (0x2F4E, 'M', '殳'), + (0x2F4F, 'M', '毋'), + (0x2F50, 'M', '比'), + (0x2F51, 'M', '毛'), + (0x2F52, 'M', '氏'), + (0x2F53, 'M', '气'), + (0x2F54, 'M', '水'), + (0x2F55, 'M', '火'), + (0x2F56, 'M', '爪'), + (0x2F57, 'M', '父'), + (0x2F58, 'M', '爻'), + (0x2F59, 'M', '爿'), + (0x2F5A, 'M', '片'), + (0x2F5B, 'M', '牙'), + (0x2F5C, 'M', '牛'), + (0x2F5D, 'M', '犬'), + (0x2F5E, 'M', '玄'), + (0x2F5F, 'M', '玉'), + (0x2F60, 'M', '瓜'), + (0x2F61, 'M', '瓦'), + (0x2F62, 'M', '甘'), + (0x2F63, 'M', '生'), + (0x2F64, 'M', '用'), + (0x2F65, 'M', '田'), + (0x2F66, 'M', '疋'), + (0x2F67, 'M', '疒'), + (0x2F68, 'M', '癶'), + (0x2F69, 'M', '白'), + (0x2F6A, 'M', '皮'), + (0x2F6B, 'M', '皿'), + (0x2F6C, 'M', '目'), + (0x2F6D, 'M', '矛'), + (0x2F6E, 'M', '矢'), + (0x2F6F, 'M', '石'), + (0x2F70, 'M', '示'), + (0x2F71, 'M', '禸'), + (0x2F72, 'M', '禾'), + (0x2F73, 'M', '穴'), + (0x2F74, 'M', '立'), + (0x2F75, 'M', '竹'), ] def _seg_28(): return [ - (0x2F76, 'M', u'米'), - (0x2F77, 'M', u'糸'), - (0x2F78, 'M', u'缶'), - (0x2F79, 'M', u'网'), - (0x2F7A, 'M', u'羊'), - (0x2F7B, 'M', u'羽'), - (0x2F7C, 'M', u'老'), - (0x2F7D, 'M', u'而'), - (0x2F7E, 'M', u'耒'), - (0x2F7F, 'M', u'耳'), - (0x2F80, 'M', u'聿'), - (0x2F81, 'M', u'肉'), - (0x2F82, 'M', u'臣'), - (0x2F83, 'M', u'自'), - (0x2F84, 'M', u'至'), - (0x2F85, 'M', u'臼'), - (0x2F86, 'M', u'舌'), - (0x2F87, 'M', u'舛'), - (0x2F88, 'M', u'舟'), - (0x2F89, 'M', u'艮'), - (0x2F8A, 'M', u'色'), - (0x2F8B, 'M', u'艸'), - (0x2F8C, 'M', u'虍'), - (0x2F8D, 'M', u'虫'), - (0x2F8E, 'M', u'血'), - (0x2F8F, 'M', u'行'), - (0x2F90, 'M', u'衣'), - (0x2F91, 'M', u'襾'), - (0x2F92, 'M', u'見'), - (0x2F93, 'M', u'角'), - (0x2F94, 'M', u'言'), - (0x2F95, 'M', u'谷'), - (0x2F96, 'M', u'豆'), - (0x2F97, 'M', u'豕'), - (0x2F98, 'M', u'豸'), - (0x2F99, 'M', u'貝'), - (0x2F9A, 'M', u'赤'), - (0x2F9B, 'M', u'走'), - (0x2F9C, 'M', u'足'), - (0x2F9D, 'M', u'身'), - (0x2F9E, 'M', u'車'), - (0x2F9F, 'M', u'辛'), - (0x2FA0, 'M', u'辰'), - (0x2FA1, 'M', u'辵'), - (0x2FA2, 'M', u'邑'), - (0x2FA3, 'M', u'酉'), - (0x2FA4, 'M', u'釆'), - (0x2FA5, 'M', u'里'), - (0x2FA6, 'M', u'金'), - (0x2FA7, 'M', u'長'), - (0x2FA8, 'M', u'門'), - (0x2FA9, 'M', u'阜'), - (0x2FAA, 'M', u'隶'), - (0x2FAB, 'M', u'隹'), - (0x2FAC, 'M', u'雨'), - (0x2FAD, 'M', u'靑'), - (0x2FAE, 'M', u'非'), - (0x2FAF, 'M', u'面'), - (0x2FB0, 'M', u'革'), - (0x2FB1, 'M', u'韋'), - (0x2FB2, 'M', u'韭'), - (0x2FB3, 'M', u'音'), - (0x2FB4, 'M', u'頁'), - (0x2FB5, 'M', u'風'), - (0x2FB6, 'M', u'飛'), - (0x2FB7, 'M', u'食'), - (0x2FB8, 'M', u'首'), - (0x2FB9, 'M', u'香'), - (0x2FBA, 'M', u'馬'), - (0x2FBB, 'M', u'骨'), - (0x2FBC, 'M', u'高'), - (0x2FBD, 'M', u'髟'), - (0x2FBE, 'M', u'鬥'), - (0x2FBF, 'M', u'鬯'), - (0x2FC0, 'M', u'鬲'), - (0x2FC1, 'M', u'鬼'), - (0x2FC2, 'M', u'魚'), - (0x2FC3, 'M', u'鳥'), - (0x2FC4, 'M', u'鹵'), - (0x2FC5, 'M', u'鹿'), - (0x2FC6, 'M', u'麥'), - (0x2FC7, 'M', u'麻'), - (0x2FC8, 'M', u'黃'), - (0x2FC9, 'M', u'黍'), - (0x2FCA, 'M', u'黑'), - (0x2FCB, 'M', u'黹'), - (0x2FCC, 'M', u'黽'), - (0x2FCD, 'M', u'鼎'), - (0x2FCE, 'M', u'鼓'), - (0x2FCF, 'M', u'鼠'), - (0x2FD0, 'M', u'鼻'), - (0x2FD1, 'M', u'齊'), - (0x2FD2, 'M', u'齒'), - (0x2FD3, 'M', u'龍'), - (0x2FD4, 'M', u'龜'), - (0x2FD5, 'M', u'龠'), + (0x2F76, 'M', '米'), + (0x2F77, 'M', '糸'), + (0x2F78, 'M', '缶'), + (0x2F79, 'M', '网'), + (0x2F7A, 'M', '羊'), + (0x2F7B, 'M', '羽'), + (0x2F7C, 'M', '老'), + (0x2F7D, 'M', '而'), + (0x2F7E, 'M', '耒'), + (0x2F7F, 'M', '耳'), + (0x2F80, 'M', '聿'), + (0x2F81, 'M', '肉'), + (0x2F82, 'M', '臣'), + (0x2F83, 'M', '自'), + (0x2F84, 'M', '至'), + (0x2F85, 'M', '臼'), + (0x2F86, 'M', '舌'), + (0x2F87, 'M', '舛'), + (0x2F88, 'M', '舟'), + (0x2F89, 'M', '艮'), + (0x2F8A, 'M', '色'), + (0x2F8B, 'M', '艸'), + (0x2F8C, 'M', '虍'), + (0x2F8D, 'M', '虫'), + (0x2F8E, 'M', '血'), + (0x2F8F, 'M', '行'), + (0x2F90, 'M', '衣'), + (0x2F91, 'M', '襾'), + (0x2F92, 'M', '見'), + (0x2F93, 'M', '角'), + (0x2F94, 'M', '言'), + (0x2F95, 'M', '谷'), + (0x2F96, 'M', '豆'), + (0x2F97, 'M', '豕'), + (0x2F98, 'M', '豸'), + (0x2F99, 'M', '貝'), + (0x2F9A, 'M', '赤'), + (0x2F9B, 'M', '走'), + (0x2F9C, 'M', '足'), + (0x2F9D, 'M', '身'), + (0x2F9E, 'M', '車'), + (0x2F9F, 'M', '辛'), + (0x2FA0, 'M', '辰'), + (0x2FA1, 'M', '辵'), + (0x2FA2, 'M', '邑'), + (0x2FA3, 'M', '酉'), + (0x2FA4, 'M', '釆'), + (0x2FA5, 'M', '里'), + (0x2FA6, 'M', '金'), + (0x2FA7, 'M', '長'), + (0x2FA8, 'M', '門'), + (0x2FA9, 'M', '阜'), + (0x2FAA, 'M', '隶'), + (0x2FAB, 'M', '隹'), + (0x2FAC, 'M', '雨'), + (0x2FAD, 'M', '靑'), + (0x2FAE, 'M', '非'), + (0x2FAF, 'M', '面'), + (0x2FB0, 'M', '革'), + (0x2FB1, 'M', '韋'), + (0x2FB2, 'M', '韭'), + (0x2FB3, 'M', '音'), + (0x2FB4, 'M', '頁'), + (0x2FB5, 'M', '風'), + (0x2FB6, 'M', '飛'), + (0x2FB7, 'M', '食'), + (0x2FB8, 'M', '首'), + (0x2FB9, 'M', '香'), + (0x2FBA, 'M', '馬'), + (0x2FBB, 'M', '骨'), + (0x2FBC, 'M', '高'), + (0x2FBD, 'M', '髟'), + (0x2FBE, 'M', '鬥'), + (0x2FBF, 'M', '鬯'), + (0x2FC0, 'M', '鬲'), + (0x2FC1, 'M', '鬼'), + (0x2FC2, 'M', '魚'), + (0x2FC3, 'M', '鳥'), + (0x2FC4, 'M', '鹵'), + (0x2FC5, 'M', '鹿'), + (0x2FC6, 'M', '麥'), + (0x2FC7, 'M', '麻'), + (0x2FC8, 'M', '黃'), + (0x2FC9, 'M', '黍'), + (0x2FCA, 'M', '黑'), + (0x2FCB, 'M', '黹'), + (0x2FCC, 'M', '黽'), + (0x2FCD, 'M', '鼎'), + (0x2FCE, 'M', '鼓'), + (0x2FCF, 'M', '鼠'), + (0x2FD0, 'M', '鼻'), + (0x2FD1, 'M', '齊'), + (0x2FD2, 'M', '齒'), + (0x2FD3, 'M', '龍'), + (0x2FD4, 'M', '龜'), + (0x2FD5, 'M', '龠'), (0x2FD6, 'X'), - (0x3000, '3', u' '), + (0x3000, '3', ' '), (0x3001, 'V'), - (0x3002, 'M', u'.'), + (0x3002, 'M', '.'), ] def _seg_29(): return [ (0x3003, 'V'), - (0x3036, 'M', u'〒'), + (0x3036, 'M', '〒'), (0x3037, 'V'), - (0x3038, 'M', u'十'), - (0x3039, 'M', u'卄'), - (0x303A, 'M', u'卅'), + (0x3038, 'M', '十'), + (0x3039, 'M', '卄'), + (0x303A, 'M', '卅'), (0x303B, 'V'), (0x3040, 'X'), (0x3041, 'V'), (0x3097, 'X'), (0x3099, 'V'), - (0x309B, '3', u' ゙'), - (0x309C, '3', u' ゚'), + (0x309B, '3', ' ゙'), + (0x309C, '3', ' ゚'), (0x309D, 'V'), - (0x309F, 'M', u'より'), + (0x309F, 'M', 'より'), (0x30A0, 'V'), - (0x30FF, 'M', u'コト'), + (0x30FF, 'M', 'コト'), (0x3100, 'X'), (0x3105, 'V'), (0x3130, 'X'), - (0x3131, 'M', u'ᄀ'), - (0x3132, 'M', u'ᄁ'), - (0x3133, 'M', u'ᆪ'), - (0x3134, 'M', u'ᄂ'), - (0x3135, 'M', u'ᆬ'), - (0x3136, 'M', u'ᆭ'), - (0x3137, 'M', u'ᄃ'), - (0x3138, 'M', u'ᄄ'), - (0x3139, 'M', u'ᄅ'), - (0x313A, 'M', u'ᆰ'), - (0x313B, 'M', u'ᆱ'), - (0x313C, 'M', u'ᆲ'), - (0x313D, 'M', u'ᆳ'), - (0x313E, 'M', u'ᆴ'), - (0x313F, 'M', u'ᆵ'), - (0x3140, 'M', u'ᄚ'), - (0x3141, 'M', u'ᄆ'), - (0x3142, 'M', u'ᄇ'), - (0x3143, 'M', u'ᄈ'), - (0x3144, 'M', u'ᄡ'), - (0x3145, 'M', u'ᄉ'), - (0x3146, 'M', u'ᄊ'), - (0x3147, 'M', u'ᄋ'), - (0x3148, 'M', u'ᄌ'), - (0x3149, 'M', u'ᄍ'), - (0x314A, 'M', u'ᄎ'), - (0x314B, 'M', u'ᄏ'), - (0x314C, 'M', u'ᄐ'), - (0x314D, 'M', u'ᄑ'), - (0x314E, 'M', u'ᄒ'), - (0x314F, 'M', u'ᅡ'), - (0x3150, 'M', u'ᅢ'), - (0x3151, 'M', u'ᅣ'), - (0x3152, 'M', u'ᅤ'), - (0x3153, 'M', u'ᅥ'), - (0x3154, 'M', u'ᅦ'), - (0x3155, 'M', u'ᅧ'), - (0x3156, 'M', u'ᅨ'), - (0x3157, 'M', u'ᅩ'), - (0x3158, 'M', u'ᅪ'), - (0x3159, 'M', u'ᅫ'), - (0x315A, 'M', u'ᅬ'), - (0x315B, 'M', u'ᅭ'), - (0x315C, 'M', u'ᅮ'), - (0x315D, 'M', u'ᅯ'), - (0x315E, 'M', u'ᅰ'), - (0x315F, 'M', u'ᅱ'), - (0x3160, 'M', u'ᅲ'), - (0x3161, 'M', u'ᅳ'), - (0x3162, 'M', u'ᅴ'), - (0x3163, 'M', u'ᅵ'), + (0x3131, 'M', 'ᄀ'), + (0x3132, 'M', 'ᄁ'), + (0x3133, 'M', 'ᆪ'), + (0x3134, 'M', 'ᄂ'), + (0x3135, 'M', 'ᆬ'), + (0x3136, 'M', 'ᆭ'), + (0x3137, 'M', 'ᄃ'), + (0x3138, 'M', 'ᄄ'), + (0x3139, 'M', 'ᄅ'), + (0x313A, 'M', 'ᆰ'), + (0x313B, 'M', 'ᆱ'), + (0x313C, 'M', 'ᆲ'), + (0x313D, 'M', 'ᆳ'), + (0x313E, 'M', 'ᆴ'), + (0x313F, 'M', 'ᆵ'), + (0x3140, 'M', 'ᄚ'), + (0x3141, 'M', 'ᄆ'), + (0x3142, 'M', 'ᄇ'), + (0x3143, 'M', 'ᄈ'), + (0x3144, 'M', 'ᄡ'), + (0x3145, 'M', 'ᄉ'), + (0x3146, 'M', 'ᄊ'), + (0x3147, 'M', 'ᄋ'), + (0x3148, 'M', 'ᄌ'), + (0x3149, 'M', 'ᄍ'), + (0x314A, 'M', 'ᄎ'), + (0x314B, 'M', 'ᄏ'), + (0x314C, 'M', 'ᄐ'), + (0x314D, 'M', 'ᄑ'), + (0x314E, 'M', 'ᄒ'), + (0x314F, 'M', 'ᅡ'), + (0x3150, 'M', 'ᅢ'), + (0x3151, 'M', 'ᅣ'), + (0x3152, 'M', 'ᅤ'), + (0x3153, 'M', 'ᅥ'), + (0x3154, 'M', 'ᅦ'), + (0x3155, 'M', 'ᅧ'), + (0x3156, 'M', 'ᅨ'), + (0x3157, 'M', 'ᅩ'), + (0x3158, 'M', 'ᅪ'), + (0x3159, 'M', 'ᅫ'), + (0x315A, 'M', 'ᅬ'), + (0x315B, 'M', 'ᅭ'), + (0x315C, 'M', 'ᅮ'), + (0x315D, 'M', 'ᅯ'), + (0x315E, 'M', 'ᅰ'), + (0x315F, 'M', 'ᅱ'), + (0x3160, 'M', 'ᅲ'), + (0x3161, 'M', 'ᅳ'), + (0x3162, 'M', 'ᅴ'), + (0x3163, 'M', 'ᅵ'), (0x3164, 'X'), - (0x3165, 'M', u'ᄔ'), - (0x3166, 'M', u'ᄕ'), - (0x3167, 'M', u'ᇇ'), - (0x3168, 'M', u'ᇈ'), - (0x3169, 'M', u'ᇌ'), - (0x316A, 'M', u'ᇎ'), - (0x316B, 'M', u'ᇓ'), - (0x316C, 'M', u'ᇗ'), - (0x316D, 'M', u'ᇙ'), - (0x316E, 'M', u'ᄜ'), - (0x316F, 'M', u'ᇝ'), - (0x3170, 'M', u'ᇟ'), - (0x3171, 'M', u'ᄝ'), - (0x3172, 'M', u'ᄞ'), - (0x3173, 'M', u'ᄠ'), - (0x3174, 'M', u'ᄢ'), - (0x3175, 'M', u'ᄣ'), - (0x3176, 'M', u'ᄧ'), - (0x3177, 'M', u'ᄩ'), - (0x3178, 'M', u'ᄫ'), - (0x3179, 'M', u'ᄬ'), - (0x317A, 'M', u'ᄭ'), - (0x317B, 'M', u'ᄮ'), - (0x317C, 'M', u'ᄯ'), - (0x317D, 'M', u'ᄲ'), - (0x317E, 'M', u'ᄶ'), - (0x317F, 'M', u'ᅀ'), - (0x3180, 'M', u'ᅇ'), + (0x3165, 'M', 'ᄔ'), + (0x3166, 'M', 'ᄕ'), + (0x3167, 'M', 'ᇇ'), + (0x3168, 'M', 'ᇈ'), + (0x3169, 'M', 'ᇌ'), + (0x316A, 'M', 'ᇎ'), + (0x316B, 'M', 'ᇓ'), + (0x316C, 'M', 'ᇗ'), + (0x316D, 'M', 'ᇙ'), + (0x316E, 'M', 'ᄜ'), + (0x316F, 'M', 'ᇝ'), + (0x3170, 'M', 'ᇟ'), + (0x3171, 'M', 'ᄝ'), + (0x3172, 'M', 'ᄞ'), + (0x3173, 'M', 'ᄠ'), + (0x3174, 'M', 'ᄢ'), + (0x3175, 'M', 'ᄣ'), + (0x3176, 'M', 'ᄧ'), + (0x3177, 'M', 'ᄩ'), + (0x3178, 'M', 'ᄫ'), + (0x3179, 'M', 'ᄬ'), + (0x317A, 'M', 'ᄭ'), + (0x317B, 'M', 'ᄮ'), + (0x317C, 'M', 'ᄯ'), + (0x317D, 'M', 'ᄲ'), + (0x317E, 'M', 'ᄶ'), + (0x317F, 'M', 'ᅀ'), + (0x3180, 'M', 'ᅇ'), ] def _seg_30(): return [ - (0x3181, 'M', u'ᅌ'), - (0x3182, 'M', u'ᇱ'), - (0x3183, 'M', u'ᇲ'), - (0x3184, 'M', u'ᅗ'), - (0x3185, 'M', u'ᅘ'), - (0x3186, 'M', u'ᅙ'), - (0x3187, 'M', u'ᆄ'), - (0x3188, 'M', u'ᆅ'), - (0x3189, 'M', u'ᆈ'), - (0x318A, 'M', u'ᆑ'), - (0x318B, 'M', u'ᆒ'), - (0x318C, 'M', u'ᆔ'), - (0x318D, 'M', u'ᆞ'), - (0x318E, 'M', u'ᆡ'), + (0x3181, 'M', 'ᅌ'), + (0x3182, 'M', 'ᇱ'), + (0x3183, 'M', 'ᇲ'), + (0x3184, 'M', 'ᅗ'), + (0x3185, 'M', 'ᅘ'), + (0x3186, 'M', 'ᅙ'), + (0x3187, 'M', 'ᆄ'), + (0x3188, 'M', 'ᆅ'), + (0x3189, 'M', 'ᆈ'), + (0x318A, 'M', 'ᆑ'), + (0x318B, 'M', 'ᆒ'), + (0x318C, 'M', 'ᆔ'), + (0x318D, 'M', 'ᆞ'), + (0x318E, 'M', 'ᆡ'), (0x318F, 'X'), (0x3190, 'V'), - (0x3192, 'M', u'一'), - (0x3193, 'M', u'二'), - (0x3194, 'M', u'三'), - (0x3195, 'M', u'四'), - (0x3196, 'M', u'上'), - (0x3197, 'M', u'中'), - (0x3198, 'M', u'下'), - (0x3199, 'M', u'甲'), - (0x319A, 'M', u'乙'), - (0x319B, 'M', u'丙'), - (0x319C, 'M', u'丁'), - (0x319D, 'M', u'天'), - (0x319E, 'M', u'地'), - (0x319F, 'M', u'人'), + (0x3192, 'M', '一'), + (0x3193, 'M', '二'), + (0x3194, 'M', '三'), + (0x3195, 'M', '四'), + (0x3196, 'M', '上'), + (0x3197, 'M', '中'), + (0x3198, 'M', '下'), + (0x3199, 'M', '甲'), + (0x319A, 'M', '乙'), + (0x319B, 'M', '丙'), + (0x319C, 'M', '丁'), + (0x319D, 'M', '天'), + (0x319E, 'M', '地'), + (0x319F, 'M', '人'), (0x31A0, 'V'), (0x31E4, 'X'), (0x31F0, 'V'), - (0x3200, '3', u'(ᄀ)'), - (0x3201, '3', u'(ᄂ)'), - (0x3202, '3', u'(ᄃ)'), - (0x3203, '3', u'(ᄅ)'), - (0x3204, '3', u'(ᄆ)'), - (0x3205, '3', u'(ᄇ)'), - (0x3206, '3', u'(ᄉ)'), - (0x3207, '3', u'(ᄋ)'), - (0x3208, '3', u'(ᄌ)'), - (0x3209, '3', u'(ᄎ)'), - (0x320A, '3', u'(ᄏ)'), - (0x320B, '3', u'(ᄐ)'), - (0x320C, '3', u'(ᄑ)'), - (0x320D, '3', u'(ᄒ)'), - (0x320E, '3', u'(가)'), - (0x320F, '3', u'(나)'), - (0x3210, '3', u'(다)'), - (0x3211, '3', u'(라)'), - (0x3212, '3', u'(마)'), - (0x3213, '3', u'(바)'), - (0x3214, '3', u'(사)'), - (0x3215, '3', u'(아)'), - (0x3216, '3', u'(자)'), - (0x3217, '3', u'(차)'), - (0x3218, '3', u'(카)'), - (0x3219, '3', u'(타)'), - (0x321A, '3', u'(파)'), - (0x321B, '3', u'(하)'), - (0x321C, '3', u'(주)'), - (0x321D, '3', u'(오전)'), - (0x321E, '3', u'(오후)'), + (0x3200, '3', '(ᄀ)'), + (0x3201, '3', '(ᄂ)'), + (0x3202, '3', '(ᄃ)'), + (0x3203, '3', '(ᄅ)'), + (0x3204, '3', '(ᄆ)'), + (0x3205, '3', '(ᄇ)'), + (0x3206, '3', '(ᄉ)'), + (0x3207, '3', '(ᄋ)'), + (0x3208, '3', '(ᄌ)'), + (0x3209, '3', '(ᄎ)'), + (0x320A, '3', '(ᄏ)'), + (0x320B, '3', '(ᄐ)'), + (0x320C, '3', '(ᄑ)'), + (0x320D, '3', '(ᄒ)'), + (0x320E, '3', '(가)'), + (0x320F, '3', '(나)'), + (0x3210, '3', '(다)'), + (0x3211, '3', '(라)'), + (0x3212, '3', '(마)'), + (0x3213, '3', '(바)'), + (0x3214, '3', '(사)'), + (0x3215, '3', '(아)'), + (0x3216, '3', '(자)'), + (0x3217, '3', '(차)'), + (0x3218, '3', '(카)'), + (0x3219, '3', '(타)'), + (0x321A, '3', '(파)'), + (0x321B, '3', '(하)'), + (0x321C, '3', '(주)'), + (0x321D, '3', '(오전)'), + (0x321E, '3', '(오후)'), (0x321F, 'X'), - (0x3220, '3', u'(一)'), - (0x3221, '3', u'(二)'), - (0x3222, '3', u'(三)'), - (0x3223, '3', u'(四)'), - (0x3224, '3', u'(五)'), - (0x3225, '3', u'(六)'), - (0x3226, '3', u'(七)'), - (0x3227, '3', u'(八)'), - (0x3228, '3', u'(九)'), - (0x3229, '3', u'(十)'), - (0x322A, '3', u'(月)'), - (0x322B, '3', u'(火)'), - (0x322C, '3', u'(水)'), - (0x322D, '3', u'(木)'), - (0x322E, '3', u'(金)'), - (0x322F, '3', u'(土)'), - (0x3230, '3', u'(日)'), - (0x3231, '3', u'(株)'), - (0x3232, '3', u'(有)'), - (0x3233, '3', u'(社)'), - (0x3234, '3', u'(名)'), - (0x3235, '3', u'(特)'), - (0x3236, '3', u'(財)'), - (0x3237, '3', u'(祝)'), - (0x3238, '3', u'(労)'), - (0x3239, '3', u'(代)'), - (0x323A, '3', u'(呼)'), - (0x323B, '3', u'(学)'), - (0x323C, '3', u'(監)'), - (0x323D, '3', u'(企)'), - (0x323E, '3', u'(資)'), - (0x323F, '3', u'(協)'), - (0x3240, '3', u'(祭)'), - (0x3241, '3', u'(休)'), - (0x3242, '3', u'(自)'), + (0x3220, '3', '(一)'), + (0x3221, '3', '(二)'), + (0x3222, '3', '(三)'), + (0x3223, '3', '(四)'), + (0x3224, '3', '(五)'), + (0x3225, '3', '(六)'), + (0x3226, '3', '(七)'), + (0x3227, '3', '(八)'), + (0x3228, '3', '(九)'), + (0x3229, '3', '(十)'), + (0x322A, '3', '(月)'), + (0x322B, '3', '(火)'), + (0x322C, '3', '(水)'), + (0x322D, '3', '(木)'), + (0x322E, '3', '(金)'), + (0x322F, '3', '(土)'), + (0x3230, '3', '(日)'), + (0x3231, '3', '(株)'), + (0x3232, '3', '(有)'), + (0x3233, '3', '(社)'), + (0x3234, '3', '(名)'), + (0x3235, '3', '(特)'), + (0x3236, '3', '(財)'), + (0x3237, '3', '(祝)'), + (0x3238, '3', '(労)'), + (0x3239, '3', '(代)'), + (0x323A, '3', '(呼)'), + (0x323B, '3', '(学)'), + (0x323C, '3', '(監)'), + (0x323D, '3', '(企)'), + (0x323E, '3', '(資)'), + (0x323F, '3', '(協)'), + (0x3240, '3', '(祭)'), + (0x3241, '3', '(休)'), + (0x3242, '3', '(自)'), ] def _seg_31(): return [ - (0x3243, '3', u'(至)'), - (0x3244, 'M', u'問'), - (0x3245, 'M', u'幼'), - (0x3246, 'M', u'文'), - (0x3247, 'M', u'箏'), + (0x3243, '3', '(至)'), + (0x3244, 'M', '問'), + (0x3245, 'M', '幼'), + (0x3246, 'M', '文'), + (0x3247, 'M', '箏'), (0x3248, 'V'), - (0x3250, 'M', u'pte'), - (0x3251, 'M', u'21'), - (0x3252, 'M', u'22'), - (0x3253, 'M', u'23'), - (0x3254, 'M', u'24'), - (0x3255, 'M', u'25'), - (0x3256, 'M', u'26'), - (0x3257, 'M', u'27'), - (0x3258, 'M', u'28'), - (0x3259, 'M', u'29'), - (0x325A, 'M', u'30'), - (0x325B, 'M', u'31'), - (0x325C, 'M', u'32'), - (0x325D, 'M', u'33'), - (0x325E, 'M', u'34'), - (0x325F, 'M', u'35'), - (0x3260, 'M', u'ᄀ'), - (0x3261, 'M', u'ᄂ'), - (0x3262, 'M', u'ᄃ'), - (0x3263, 'M', u'ᄅ'), - (0x3264, 'M', u'ᄆ'), - (0x3265, 'M', u'ᄇ'), - (0x3266, 'M', u'ᄉ'), - (0x3267, 'M', u'ᄋ'), - (0x3268, 'M', u'ᄌ'), - (0x3269, 'M', u'ᄎ'), - (0x326A, 'M', u'ᄏ'), - (0x326B, 'M', u'ᄐ'), - (0x326C, 'M', u'ᄑ'), - (0x326D, 'M', u'ᄒ'), - (0x326E, 'M', u'가'), - (0x326F, 'M', u'나'), - (0x3270, 'M', u'다'), - (0x3271, 'M', u'라'), - (0x3272, 'M', u'마'), - (0x3273, 'M', u'바'), - (0x3274, 'M', u'사'), - (0x3275, 'M', u'아'), - (0x3276, 'M', u'자'), - (0x3277, 'M', u'차'), - (0x3278, 'M', u'카'), - (0x3279, 'M', u'타'), - (0x327A, 'M', u'파'), - (0x327B, 'M', u'하'), - (0x327C, 'M', u'참고'), - (0x327D, 'M', u'주의'), - (0x327E, 'M', u'우'), + (0x3250, 'M', 'pte'), + (0x3251, 'M', '21'), + (0x3252, 'M', '22'), + (0x3253, 'M', '23'), + (0x3254, 'M', '24'), + (0x3255, 'M', '25'), + (0x3256, 'M', '26'), + (0x3257, 'M', '27'), + (0x3258, 'M', '28'), + (0x3259, 'M', '29'), + (0x325A, 'M', '30'), + (0x325B, 'M', '31'), + (0x325C, 'M', '32'), + (0x325D, 'M', '33'), + (0x325E, 'M', '34'), + (0x325F, 'M', '35'), + (0x3260, 'M', 'ᄀ'), + (0x3261, 'M', 'ᄂ'), + (0x3262, 'M', 'ᄃ'), + (0x3263, 'M', 'ᄅ'), + (0x3264, 'M', 'ᄆ'), + (0x3265, 'M', 'ᄇ'), + (0x3266, 'M', 'ᄉ'), + (0x3267, 'M', 'ᄋ'), + (0x3268, 'M', 'ᄌ'), + (0x3269, 'M', 'ᄎ'), + (0x326A, 'M', 'ᄏ'), + (0x326B, 'M', 'ᄐ'), + (0x326C, 'M', 'ᄑ'), + (0x326D, 'M', 'ᄒ'), + (0x326E, 'M', '가'), + (0x326F, 'M', '나'), + (0x3270, 'M', '다'), + (0x3271, 'M', '라'), + (0x3272, 'M', '마'), + (0x3273, 'M', '바'), + (0x3274, 'M', '사'), + (0x3275, 'M', '아'), + (0x3276, 'M', '자'), + (0x3277, 'M', '차'), + (0x3278, 'M', '카'), + (0x3279, 'M', '타'), + (0x327A, 'M', '파'), + (0x327B, 'M', '하'), + (0x327C, 'M', '참고'), + (0x327D, 'M', '주의'), + (0x327E, 'M', '우'), (0x327F, 'V'), - (0x3280, 'M', u'一'), - (0x3281, 'M', u'二'), - (0x3282, 'M', u'三'), - (0x3283, 'M', u'四'), - (0x3284, 'M', u'五'), - (0x3285, 'M', u'六'), - (0x3286, 'M', u'七'), - (0x3287, 'M', u'八'), - (0x3288, 'M', u'九'), - (0x3289, 'M', u'十'), - (0x328A, 'M', u'月'), - (0x328B, 'M', u'火'), - (0x328C, 'M', u'水'), - (0x328D, 'M', u'木'), - (0x328E, 'M', u'金'), - (0x328F, 'M', u'土'), - (0x3290, 'M', u'日'), - (0x3291, 'M', u'株'), - (0x3292, 'M', u'有'), - (0x3293, 'M', u'社'), - (0x3294, 'M', u'名'), - (0x3295, 'M', u'特'), - (0x3296, 'M', u'財'), - (0x3297, 'M', u'祝'), - (0x3298, 'M', u'労'), - (0x3299, 'M', u'秘'), - (0x329A, 'M', u'男'), - (0x329B, 'M', u'女'), - (0x329C, 'M', u'適'), - (0x329D, 'M', u'優'), - (0x329E, 'M', u'印'), - (0x329F, 'M', u'注'), - (0x32A0, 'M', u'項'), - (0x32A1, 'M', u'休'), - (0x32A2, 'M', u'写'), - (0x32A3, 'M', u'正'), - (0x32A4, 'M', u'上'), - (0x32A5, 'M', u'中'), - (0x32A6, 'M', u'下'), - (0x32A7, 'M', u'左'), - (0x32A8, 'M', u'右'), - (0x32A9, 'M', u'医'), - (0x32AA, 'M', u'宗'), - (0x32AB, 'M', u'学'), - (0x32AC, 'M', u'監'), - (0x32AD, 'M', u'企'), + (0x3280, 'M', '一'), + (0x3281, 'M', '二'), + (0x3282, 'M', '三'), + (0x3283, 'M', '四'), + (0x3284, 'M', '五'), + (0x3285, 'M', '六'), + (0x3286, 'M', '七'), + (0x3287, 'M', '八'), + (0x3288, 'M', '九'), + (0x3289, 'M', '十'), + (0x328A, 'M', '月'), + (0x328B, 'M', '火'), + (0x328C, 'M', '水'), + (0x328D, 'M', '木'), + (0x328E, 'M', '金'), + (0x328F, 'M', '土'), + (0x3290, 'M', '日'), + (0x3291, 'M', '株'), + (0x3292, 'M', '有'), + (0x3293, 'M', '社'), + (0x3294, 'M', '名'), + (0x3295, 'M', '特'), + (0x3296, 'M', '財'), + (0x3297, 'M', '祝'), + (0x3298, 'M', '労'), + (0x3299, 'M', '秘'), + (0x329A, 'M', '男'), + (0x329B, 'M', '女'), + (0x329C, 'M', '適'), + (0x329D, 'M', '優'), + (0x329E, 'M', '印'), + (0x329F, 'M', '注'), + (0x32A0, 'M', '項'), + (0x32A1, 'M', '休'), + (0x32A2, 'M', '写'), + (0x32A3, 'M', '正'), + (0x32A4, 'M', '上'), + (0x32A5, 'M', '中'), + (0x32A6, 'M', '下'), + (0x32A7, 'M', '左'), + (0x32A8, 'M', '右'), + (0x32A9, 'M', '医'), + (0x32AA, 'M', '宗'), + (0x32AB, 'M', '学'), + (0x32AC, 'M', '監'), + (0x32AD, 'M', '企'), ] def _seg_32(): return [ - (0x32AE, 'M', u'資'), - (0x32AF, 'M', u'協'), - (0x32B0, 'M', u'夜'), - (0x32B1, 'M', u'36'), - (0x32B2, 'M', u'37'), - (0x32B3, 'M', u'38'), - (0x32B4, 'M', u'39'), - (0x32B5, 'M', u'40'), - (0x32B6, 'M', u'41'), - (0x32B7, 'M', u'42'), - (0x32B8, 'M', u'43'), - (0x32B9, 'M', u'44'), - (0x32BA, 'M', u'45'), - (0x32BB, 'M', u'46'), - (0x32BC, 'M', u'47'), - (0x32BD, 'M', u'48'), - (0x32BE, 'M', u'49'), - (0x32BF, 'M', u'50'), - (0x32C0, 'M', u'1月'), - (0x32C1, 'M', u'2月'), - (0x32C2, 'M', u'3月'), - (0x32C3, 'M', u'4月'), - (0x32C4, 'M', u'5月'), - (0x32C5, 'M', u'6月'), - (0x32C6, 'M', u'7月'), - (0x32C7, 'M', u'8月'), - (0x32C8, 'M', u'9月'), - (0x32C9, 'M', u'10月'), - (0x32CA, 'M', u'11月'), - (0x32CB, 'M', u'12月'), - (0x32CC, 'M', u'hg'), - (0x32CD, 'M', u'erg'), - (0x32CE, 'M', u'ev'), - (0x32CF, 'M', u'ltd'), - (0x32D0, 'M', u'ア'), - (0x32D1, 'M', u'イ'), - (0x32D2, 'M', u'ウ'), - (0x32D3, 'M', u'エ'), - (0x32D4, 'M', u'オ'), - (0x32D5, 'M', u'カ'), - (0x32D6, 'M', u'キ'), - (0x32D7, 'M', u'ク'), - (0x32D8, 'M', u'ケ'), - (0x32D9, 'M', u'コ'), - (0x32DA, 'M', u'サ'), - (0x32DB, 'M', u'シ'), - (0x32DC, 'M', u'ス'), - (0x32DD, 'M', u'セ'), - (0x32DE, 'M', u'ソ'), - (0x32DF, 'M', u'タ'), - (0x32E0, 'M', u'チ'), - (0x32E1, 'M', u'ツ'), - (0x32E2, 'M', u'テ'), - (0x32E3, 'M', u'ト'), - (0x32E4, 'M', u'ナ'), - (0x32E5, 'M', u'ニ'), - (0x32E6, 'M', u'ヌ'), - (0x32E7, 'M', u'ネ'), - (0x32E8, 'M', u'ノ'), - (0x32E9, 'M', u'ハ'), - (0x32EA, 'M', u'ヒ'), - (0x32EB, 'M', u'フ'), - (0x32EC, 'M', u'ヘ'), - (0x32ED, 'M', u'ホ'), - (0x32EE, 'M', u'マ'), - (0x32EF, 'M', u'ミ'), - (0x32F0, 'M', u'ム'), - (0x32F1, 'M', u'メ'), - (0x32F2, 'M', u'モ'), - (0x32F3, 'M', u'ヤ'), - (0x32F4, 'M', u'ユ'), - (0x32F5, 'M', u'ヨ'), - (0x32F6, 'M', u'ラ'), - (0x32F7, 'M', u'リ'), - (0x32F8, 'M', u'ル'), - (0x32F9, 'M', u'レ'), - (0x32FA, 'M', u'ロ'), - (0x32FB, 'M', u'ワ'), - (0x32FC, 'M', u'ヰ'), - (0x32FD, 'M', u'ヱ'), - (0x32FE, 'M', u'ヲ'), - (0x32FF, 'M', u'令和'), - (0x3300, 'M', u'アパート'), - (0x3301, 'M', u'アルファ'), - (0x3302, 'M', u'アンペア'), - (0x3303, 'M', u'アール'), - (0x3304, 'M', u'イニング'), - (0x3305, 'M', u'インチ'), - (0x3306, 'M', u'ウォン'), - (0x3307, 'M', u'エスクード'), - (0x3308, 'M', u'エーカー'), - (0x3309, 'M', u'オンス'), - (0x330A, 'M', u'オーム'), - (0x330B, 'M', u'カイリ'), - (0x330C, 'M', u'カラット'), - (0x330D, 'M', u'カロリー'), - (0x330E, 'M', u'ガロン'), - (0x330F, 'M', u'ガンマ'), - (0x3310, 'M', u'ギガ'), - (0x3311, 'M', u'ギニー'), + (0x32AE, 'M', '資'), + (0x32AF, 'M', '協'), + (0x32B0, 'M', '夜'), + (0x32B1, 'M', '36'), + (0x32B2, 'M', '37'), + (0x32B3, 'M', '38'), + (0x32B4, 'M', '39'), + (0x32B5, 'M', '40'), + (0x32B6, 'M', '41'), + (0x32B7, 'M', '42'), + (0x32B8, 'M', '43'), + (0x32B9, 'M', '44'), + (0x32BA, 'M', '45'), + (0x32BB, 'M', '46'), + (0x32BC, 'M', '47'), + (0x32BD, 'M', '48'), + (0x32BE, 'M', '49'), + (0x32BF, 'M', '50'), + (0x32C0, 'M', '1月'), + (0x32C1, 'M', '2月'), + (0x32C2, 'M', '3月'), + (0x32C3, 'M', '4月'), + (0x32C4, 'M', '5月'), + (0x32C5, 'M', '6月'), + (0x32C6, 'M', '7月'), + (0x32C7, 'M', '8月'), + (0x32C8, 'M', '9月'), + (0x32C9, 'M', '10月'), + (0x32CA, 'M', '11月'), + (0x32CB, 'M', '12月'), + (0x32CC, 'M', 'hg'), + (0x32CD, 'M', 'erg'), + (0x32CE, 'M', 'ev'), + (0x32CF, 'M', 'ltd'), + (0x32D0, 'M', 'ア'), + (0x32D1, 'M', 'イ'), + (0x32D2, 'M', 'ウ'), + (0x32D3, 'M', 'エ'), + (0x32D4, 'M', 'オ'), + (0x32D5, 'M', 'カ'), + (0x32D6, 'M', 'キ'), + (0x32D7, 'M', 'ク'), + (0x32D8, 'M', 'ケ'), + (0x32D9, 'M', 'コ'), + (0x32DA, 'M', 'サ'), + (0x32DB, 'M', 'シ'), + (0x32DC, 'M', 'ス'), + (0x32DD, 'M', 'セ'), + (0x32DE, 'M', 'ソ'), + (0x32DF, 'M', 'タ'), + (0x32E0, 'M', 'チ'), + (0x32E1, 'M', 'ツ'), + (0x32E2, 'M', 'テ'), + (0x32E3, 'M', 'ト'), + (0x32E4, 'M', 'ナ'), + (0x32E5, 'M', 'ニ'), + (0x32E6, 'M', 'ヌ'), + (0x32E7, 'M', 'ネ'), + (0x32E8, 'M', 'ノ'), + (0x32E9, 'M', 'ハ'), + (0x32EA, 'M', 'ヒ'), + (0x32EB, 'M', 'フ'), + (0x32EC, 'M', 'ヘ'), + (0x32ED, 'M', 'ホ'), + (0x32EE, 'M', 'マ'), + (0x32EF, 'M', 'ミ'), + (0x32F0, 'M', 'ム'), + (0x32F1, 'M', 'メ'), + (0x32F2, 'M', 'モ'), + (0x32F3, 'M', 'ヤ'), + (0x32F4, 'M', 'ユ'), + (0x32F5, 'M', 'ヨ'), + (0x32F6, 'M', 'ラ'), + (0x32F7, 'M', 'リ'), + (0x32F8, 'M', 'ル'), + (0x32F9, 'M', 'レ'), + (0x32FA, 'M', 'ロ'), + (0x32FB, 'M', 'ワ'), + (0x32FC, 'M', 'ヰ'), + (0x32FD, 'M', 'ヱ'), + (0x32FE, 'M', 'ヲ'), + (0x32FF, 'M', '令和'), + (0x3300, 'M', 'アパート'), + (0x3301, 'M', 'アルファ'), + (0x3302, 'M', 'アンペア'), + (0x3303, 'M', 'アール'), + (0x3304, 'M', 'イニング'), + (0x3305, 'M', 'インチ'), + (0x3306, 'M', 'ウォン'), + (0x3307, 'M', 'エスクード'), + (0x3308, 'M', 'エーカー'), + (0x3309, 'M', 'オンス'), + (0x330A, 'M', 'オーム'), + (0x330B, 'M', 'カイリ'), + (0x330C, 'M', 'カラット'), + (0x330D, 'M', 'カロリー'), + (0x330E, 'M', 'ガロン'), + (0x330F, 'M', 'ガンマ'), + (0x3310, 'M', 'ギガ'), + (0x3311, 'M', 'ギニー'), ] def _seg_33(): return [ - (0x3312, 'M', u'キュリー'), - (0x3313, 'M', u'ギルダー'), - (0x3314, 'M', u'キロ'), - (0x3315, 'M', u'キログラム'), - (0x3316, 'M', u'キロメートル'), - (0x3317, 'M', u'キロワット'), - (0x3318, 'M', u'グラム'), - (0x3319, 'M', u'グラムトン'), - (0x331A, 'M', u'クルゼイロ'), - (0x331B, 'M', u'クローネ'), - (0x331C, 'M', u'ケース'), - (0x331D, 'M', u'コルナ'), - (0x331E, 'M', u'コーポ'), - (0x331F, 'M', u'サイクル'), - (0x3320, 'M', u'サンチーム'), - (0x3321, 'M', u'シリング'), - (0x3322, 'M', u'センチ'), - (0x3323, 'M', u'セント'), - (0x3324, 'M', u'ダース'), - (0x3325, 'M', u'デシ'), - (0x3326, 'M', u'ドル'), - (0x3327, 'M', u'トン'), - (0x3328, 'M', u'ナノ'), - (0x3329, 'M', u'ノット'), - (0x332A, 'M', u'ハイツ'), - (0x332B, 'M', u'パーセント'), - (0x332C, 'M', u'パーツ'), - (0x332D, 'M', u'バーレル'), - (0x332E, 'M', u'ピアストル'), - (0x332F, 'M', u'ピクル'), - (0x3330, 'M', u'ピコ'), - (0x3331, 'M', u'ビル'), - (0x3332, 'M', u'ファラッド'), - (0x3333, 'M', u'フィート'), - (0x3334, 'M', u'ブッシェル'), - (0x3335, 'M', u'フラン'), - (0x3336, 'M', u'ヘクタール'), - (0x3337, 'M', u'ペソ'), - (0x3338, 'M', u'ペニヒ'), - (0x3339, 'M', u'ヘルツ'), - (0x333A, 'M', u'ペンス'), - (0x333B, 'M', u'ページ'), - (0x333C, 'M', u'ベータ'), - (0x333D, 'M', u'ポイント'), - (0x333E, 'M', u'ボルト'), - (0x333F, 'M', u'ホン'), - (0x3340, 'M', u'ポンド'), - (0x3341, 'M', u'ホール'), - (0x3342, 'M', u'ホーン'), - (0x3343, 'M', u'マイクロ'), - (0x3344, 'M', u'マイル'), - (0x3345, 'M', u'マッハ'), - (0x3346, 'M', u'マルク'), - (0x3347, 'M', u'マンション'), - (0x3348, 'M', u'ミクロン'), - (0x3349, 'M', u'ミリ'), - (0x334A, 'M', u'ミリバール'), - (0x334B, 'M', u'メガ'), - (0x334C, 'M', u'メガトン'), - (0x334D, 'M', u'メートル'), - (0x334E, 'M', u'ヤード'), - (0x334F, 'M', u'ヤール'), - (0x3350, 'M', u'ユアン'), - (0x3351, 'M', u'リットル'), - (0x3352, 'M', u'リラ'), - (0x3353, 'M', u'ルピー'), - (0x3354, 'M', u'ルーブル'), - (0x3355, 'M', u'レム'), - (0x3356, 'M', u'レントゲン'), - (0x3357, 'M', u'ワット'), - (0x3358, 'M', u'0点'), - (0x3359, 'M', u'1点'), - (0x335A, 'M', u'2点'), - (0x335B, 'M', u'3点'), - (0x335C, 'M', u'4点'), - (0x335D, 'M', u'5点'), - (0x335E, 'M', u'6点'), - (0x335F, 'M', u'7点'), - (0x3360, 'M', u'8点'), - (0x3361, 'M', u'9点'), - (0x3362, 'M', u'10点'), - (0x3363, 'M', u'11点'), - (0x3364, 'M', u'12点'), - (0x3365, 'M', u'13点'), - (0x3366, 'M', u'14点'), - (0x3367, 'M', u'15点'), - (0x3368, 'M', u'16点'), - (0x3369, 'M', u'17点'), - (0x336A, 'M', u'18点'), - (0x336B, 'M', u'19点'), - (0x336C, 'M', u'20点'), - (0x336D, 'M', u'21点'), - (0x336E, 'M', u'22点'), - (0x336F, 'M', u'23点'), - (0x3370, 'M', u'24点'), - (0x3371, 'M', u'hpa'), - (0x3372, 'M', u'da'), - (0x3373, 'M', u'au'), - (0x3374, 'M', u'bar'), - (0x3375, 'M', u'ov'), + (0x3312, 'M', 'キュリー'), + (0x3313, 'M', 'ギルダー'), + (0x3314, 'M', 'キロ'), + (0x3315, 'M', 'キログラム'), + (0x3316, 'M', 'キロメートル'), + (0x3317, 'M', 'キロワット'), + (0x3318, 'M', 'グラム'), + (0x3319, 'M', 'グラムトン'), + (0x331A, 'M', 'クルゼイロ'), + (0x331B, 'M', 'クローネ'), + (0x331C, 'M', 'ケース'), + (0x331D, 'M', 'コルナ'), + (0x331E, 'M', 'コーポ'), + (0x331F, 'M', 'サイクル'), + (0x3320, 'M', 'サンチーム'), + (0x3321, 'M', 'シリング'), + (0x3322, 'M', 'センチ'), + (0x3323, 'M', 'セント'), + (0x3324, 'M', 'ダース'), + (0x3325, 'M', 'デシ'), + (0x3326, 'M', 'ドル'), + (0x3327, 'M', 'トン'), + (0x3328, 'M', 'ナノ'), + (0x3329, 'M', 'ノット'), + (0x332A, 'M', 'ハイツ'), + (0x332B, 'M', 'パーセント'), + (0x332C, 'M', 'パーツ'), + (0x332D, 'M', 'バーレル'), + (0x332E, 'M', 'ピアストル'), + (0x332F, 'M', 'ピクル'), + (0x3330, 'M', 'ピコ'), + (0x3331, 'M', 'ビル'), + (0x3332, 'M', 'ファラッド'), + (0x3333, 'M', 'フィート'), + (0x3334, 'M', 'ブッシェル'), + (0x3335, 'M', 'フラン'), + (0x3336, 'M', 'ヘクタール'), + (0x3337, 'M', 'ペソ'), + (0x3338, 'M', 'ペニヒ'), + (0x3339, 'M', 'ヘルツ'), + (0x333A, 'M', 'ペンス'), + (0x333B, 'M', 'ページ'), + (0x333C, 'M', 'ベータ'), + (0x333D, 'M', 'ポイント'), + (0x333E, 'M', 'ボルト'), + (0x333F, 'M', 'ホン'), + (0x3340, 'M', 'ポンド'), + (0x3341, 'M', 'ホール'), + (0x3342, 'M', 'ホーン'), + (0x3343, 'M', 'マイクロ'), + (0x3344, 'M', 'マイル'), + (0x3345, 'M', 'マッハ'), + (0x3346, 'M', 'マルク'), + (0x3347, 'M', 'マンション'), + (0x3348, 'M', 'ミクロン'), + (0x3349, 'M', 'ミリ'), + (0x334A, 'M', 'ミリバール'), + (0x334B, 'M', 'メガ'), + (0x334C, 'M', 'メガトン'), + (0x334D, 'M', 'メートル'), + (0x334E, 'M', 'ヤード'), + (0x334F, 'M', 'ヤール'), + (0x3350, 'M', 'ユアン'), + (0x3351, 'M', 'リットル'), + (0x3352, 'M', 'リラ'), + (0x3353, 'M', 'ルピー'), + (0x3354, 'M', 'ルーブル'), + (0x3355, 'M', 'レム'), + (0x3356, 'M', 'レントゲン'), + (0x3357, 'M', 'ワット'), + (0x3358, 'M', '0点'), + (0x3359, 'M', '1点'), + (0x335A, 'M', '2点'), + (0x335B, 'M', '3点'), + (0x335C, 'M', '4点'), + (0x335D, 'M', '5点'), + (0x335E, 'M', '6点'), + (0x335F, 'M', '7点'), + (0x3360, 'M', '8点'), + (0x3361, 'M', '9点'), + (0x3362, 'M', '10点'), + (0x3363, 'M', '11点'), + (0x3364, 'M', '12点'), + (0x3365, 'M', '13点'), + (0x3366, 'M', '14点'), + (0x3367, 'M', '15点'), + (0x3368, 'M', '16点'), + (0x3369, 'M', '17点'), + (0x336A, 'M', '18点'), + (0x336B, 'M', '19点'), + (0x336C, 'M', '20点'), + (0x336D, 'M', '21点'), + (0x336E, 'M', '22点'), + (0x336F, 'M', '23点'), + (0x3370, 'M', '24点'), + (0x3371, 'M', 'hpa'), + (0x3372, 'M', 'da'), + (0x3373, 'M', 'au'), + (0x3374, 'M', 'bar'), + (0x3375, 'M', 'ov'), ] def _seg_34(): return [ - (0x3376, 'M', u'pc'), - (0x3377, 'M', u'dm'), - (0x3378, 'M', u'dm2'), - (0x3379, 'M', u'dm3'), - (0x337A, 'M', u'iu'), - (0x337B, 'M', u'平成'), - (0x337C, 'M', u'昭和'), - (0x337D, 'M', u'大正'), - (0x337E, 'M', u'明治'), - (0x337F, 'M', u'株式会社'), - (0x3380, 'M', u'pa'), - (0x3381, 'M', u'na'), - (0x3382, 'M', u'μa'), - (0x3383, 'M', u'ma'), - (0x3384, 'M', u'ka'), - (0x3385, 'M', u'kb'), - (0x3386, 'M', u'mb'), - (0x3387, 'M', u'gb'), - (0x3388, 'M', u'cal'), - (0x3389, 'M', u'kcal'), - (0x338A, 'M', u'pf'), - (0x338B, 'M', u'nf'), - (0x338C, 'M', u'μf'), - (0x338D, 'M', u'μg'), - (0x338E, 'M', u'mg'), - (0x338F, 'M', u'kg'), - (0x3390, 'M', u'hz'), - (0x3391, 'M', u'khz'), - (0x3392, 'M', u'mhz'), - (0x3393, 'M', u'ghz'), - (0x3394, 'M', u'thz'), - (0x3395, 'M', u'μl'), - (0x3396, 'M', u'ml'), - (0x3397, 'M', u'dl'), - (0x3398, 'M', u'kl'), - (0x3399, 'M', u'fm'), - (0x339A, 'M', u'nm'), - (0x339B, 'M', u'μm'), - (0x339C, 'M', u'mm'), - (0x339D, 'M', u'cm'), - (0x339E, 'M', u'km'), - (0x339F, 'M', u'mm2'), - (0x33A0, 'M', u'cm2'), - (0x33A1, 'M', u'm2'), - (0x33A2, 'M', u'km2'), - (0x33A3, 'M', u'mm3'), - (0x33A4, 'M', u'cm3'), - (0x33A5, 'M', u'm3'), - (0x33A6, 'M', u'km3'), - (0x33A7, 'M', u'm∕s'), - (0x33A8, 'M', u'm∕s2'), - (0x33A9, 'M', u'pa'), - (0x33AA, 'M', u'kpa'), - (0x33AB, 'M', u'mpa'), - (0x33AC, 'M', u'gpa'), - (0x33AD, 'M', u'rad'), - (0x33AE, 'M', u'rad∕s'), - (0x33AF, 'M', u'rad∕s2'), - (0x33B0, 'M', u'ps'), - (0x33B1, 'M', u'ns'), - (0x33B2, 'M', u'μs'), - (0x33B3, 'M', u'ms'), - (0x33B4, 'M', u'pv'), - (0x33B5, 'M', u'nv'), - (0x33B6, 'M', u'μv'), - (0x33B7, 'M', u'mv'), - (0x33B8, 'M', u'kv'), - (0x33B9, 'M', u'mv'), - (0x33BA, 'M', u'pw'), - (0x33BB, 'M', u'nw'), - (0x33BC, 'M', u'μw'), - (0x33BD, 'M', u'mw'), - (0x33BE, 'M', u'kw'), - (0x33BF, 'M', u'mw'), - (0x33C0, 'M', u'kω'), - (0x33C1, 'M', u'mω'), + (0x3376, 'M', 'pc'), + (0x3377, 'M', 'dm'), + (0x3378, 'M', 'dm2'), + (0x3379, 'M', 'dm3'), + (0x337A, 'M', 'iu'), + (0x337B, 'M', '平成'), + (0x337C, 'M', '昭和'), + (0x337D, 'M', '大正'), + (0x337E, 'M', '明治'), + (0x337F, 'M', '株式会社'), + (0x3380, 'M', 'pa'), + (0x3381, 'M', 'na'), + (0x3382, 'M', 'μa'), + (0x3383, 'M', 'ma'), + (0x3384, 'M', 'ka'), + (0x3385, 'M', 'kb'), + (0x3386, 'M', 'mb'), + (0x3387, 'M', 'gb'), + (0x3388, 'M', 'cal'), + (0x3389, 'M', 'kcal'), + (0x338A, 'M', 'pf'), + (0x338B, 'M', 'nf'), + (0x338C, 'M', 'μf'), + (0x338D, 'M', 'μg'), + (0x338E, 'M', 'mg'), + (0x338F, 'M', 'kg'), + (0x3390, 'M', 'hz'), + (0x3391, 'M', 'khz'), + (0x3392, 'M', 'mhz'), + (0x3393, 'M', 'ghz'), + (0x3394, 'M', 'thz'), + (0x3395, 'M', 'μl'), + (0x3396, 'M', 'ml'), + (0x3397, 'M', 'dl'), + (0x3398, 'M', 'kl'), + (0x3399, 'M', 'fm'), + (0x339A, 'M', 'nm'), + (0x339B, 'M', 'μm'), + (0x339C, 'M', 'mm'), + (0x339D, 'M', 'cm'), + (0x339E, 'M', 'km'), + (0x339F, 'M', 'mm2'), + (0x33A0, 'M', 'cm2'), + (0x33A1, 'M', 'm2'), + (0x33A2, 'M', 'km2'), + (0x33A3, 'M', 'mm3'), + (0x33A4, 'M', 'cm3'), + (0x33A5, 'M', 'm3'), + (0x33A6, 'M', 'km3'), + (0x33A7, 'M', 'm∕s'), + (0x33A8, 'M', 'm∕s2'), + (0x33A9, 'M', 'pa'), + (0x33AA, 'M', 'kpa'), + (0x33AB, 'M', 'mpa'), + (0x33AC, 'M', 'gpa'), + (0x33AD, 'M', 'rad'), + (0x33AE, 'M', 'rad∕s'), + (0x33AF, 'M', 'rad∕s2'), + (0x33B0, 'M', 'ps'), + (0x33B1, 'M', 'ns'), + (0x33B2, 'M', 'μs'), + (0x33B3, 'M', 'ms'), + (0x33B4, 'M', 'pv'), + (0x33B5, 'M', 'nv'), + (0x33B6, 'M', 'μv'), + (0x33B7, 'M', 'mv'), + (0x33B8, 'M', 'kv'), + (0x33B9, 'M', 'mv'), + (0x33BA, 'M', 'pw'), + (0x33BB, 'M', 'nw'), + (0x33BC, 'M', 'μw'), + (0x33BD, 'M', 'mw'), + (0x33BE, 'M', 'kw'), + (0x33BF, 'M', 'mw'), + (0x33C0, 'M', 'kω'), + (0x33C1, 'M', 'mω'), (0x33C2, 'X'), - (0x33C3, 'M', u'bq'), - (0x33C4, 'M', u'cc'), - (0x33C5, 'M', u'cd'), - (0x33C6, 'M', u'c∕kg'), + (0x33C3, 'M', 'bq'), + (0x33C4, 'M', 'cc'), + (0x33C5, 'M', 'cd'), + (0x33C6, 'M', 'c∕kg'), (0x33C7, 'X'), - (0x33C8, 'M', u'db'), - (0x33C9, 'M', u'gy'), - (0x33CA, 'M', u'ha'), - (0x33CB, 'M', u'hp'), - (0x33CC, 'M', u'in'), - (0x33CD, 'M', u'kk'), - (0x33CE, 'M', u'km'), - (0x33CF, 'M', u'kt'), - (0x33D0, 'M', u'lm'), - (0x33D1, 'M', u'ln'), - (0x33D2, 'M', u'log'), - (0x33D3, 'M', u'lx'), - (0x33D4, 'M', u'mb'), - (0x33D5, 'M', u'mil'), - (0x33D6, 'M', u'mol'), - (0x33D7, 'M', u'ph'), + (0x33C8, 'M', 'db'), + (0x33C9, 'M', 'gy'), + (0x33CA, 'M', 'ha'), + (0x33CB, 'M', 'hp'), + (0x33CC, 'M', 'in'), + (0x33CD, 'M', 'kk'), + (0x33CE, 'M', 'km'), + (0x33CF, 'M', 'kt'), + (0x33D0, 'M', 'lm'), + (0x33D1, 'M', 'ln'), + (0x33D2, 'M', 'log'), + (0x33D3, 'M', 'lx'), + (0x33D4, 'M', 'mb'), + (0x33D5, 'M', 'mil'), + (0x33D6, 'M', 'mol'), + (0x33D7, 'M', 'ph'), (0x33D8, 'X'), - (0x33D9, 'M', u'ppm'), + (0x33D9, 'M', 'ppm'), ] def _seg_35(): return [ - (0x33DA, 'M', u'pr'), - (0x33DB, 'M', u'sr'), - (0x33DC, 'M', u'sv'), - (0x33DD, 'M', u'wb'), - (0x33DE, 'M', u'v∕m'), - (0x33DF, 'M', u'a∕m'), - (0x33E0, 'M', u'1日'), - (0x33E1, 'M', u'2日'), - (0x33E2, 'M', u'3日'), - (0x33E3, 'M', u'4日'), - (0x33E4, 'M', u'5日'), - (0x33E5, 'M', u'6日'), - (0x33E6, 'M', u'7日'), - (0x33E7, 'M', u'8日'), - (0x33E8, 'M', u'9日'), - (0x33E9, 'M', u'10日'), - (0x33EA, 'M', u'11日'), - (0x33EB, 'M', u'12日'), - (0x33EC, 'M', u'13日'), - (0x33ED, 'M', u'14日'), - (0x33EE, 'M', u'15日'), - (0x33EF, 'M', u'16日'), - (0x33F0, 'M', u'17日'), - (0x33F1, 'M', u'18日'), - (0x33F2, 'M', u'19日'), - (0x33F3, 'M', u'20日'), - (0x33F4, 'M', u'21日'), - (0x33F5, 'M', u'22日'), - (0x33F6, 'M', u'23日'), - (0x33F7, 'M', u'24日'), - (0x33F8, 'M', u'25日'), - (0x33F9, 'M', u'26日'), - (0x33FA, 'M', u'27日'), - (0x33FB, 'M', u'28日'), - (0x33FC, 'M', u'29日'), - (0x33FD, 'M', u'30日'), - (0x33FE, 'M', u'31日'), - (0x33FF, 'M', u'gal'), + (0x33DA, 'M', 'pr'), + (0x33DB, 'M', 'sr'), + (0x33DC, 'M', 'sv'), + (0x33DD, 'M', 'wb'), + (0x33DE, 'M', 'v∕m'), + (0x33DF, 'M', 'a∕m'), + (0x33E0, 'M', '1日'), + (0x33E1, 'M', '2日'), + (0x33E2, 'M', '3日'), + (0x33E3, 'M', '4日'), + (0x33E4, 'M', '5日'), + (0x33E5, 'M', '6日'), + (0x33E6, 'M', '7日'), + (0x33E7, 'M', '8日'), + (0x33E8, 'M', '9日'), + (0x33E9, 'M', '10日'), + (0x33EA, 'M', '11日'), + (0x33EB, 'M', '12日'), + (0x33EC, 'M', '13日'), + (0x33ED, 'M', '14日'), + (0x33EE, 'M', '15日'), + (0x33EF, 'M', '16日'), + (0x33F0, 'M', '17日'), + (0x33F1, 'M', '18日'), + (0x33F2, 'M', '19日'), + (0x33F3, 'M', '20日'), + (0x33F4, 'M', '21日'), + (0x33F5, 'M', '22日'), + (0x33F6, 'M', '23日'), + (0x33F7, 'M', '24日'), + (0x33F8, 'M', '25日'), + (0x33F9, 'M', '26日'), + (0x33FA, 'M', '27日'), + (0x33FB, 'M', '28日'), + (0x33FC, 'M', '29日'), + (0x33FD, 'M', '30日'), + (0x33FE, 'M', '31日'), + (0x33FF, 'M', 'gal'), (0x3400, 'V'), (0x9FFD, 'X'), (0xA000, 'V'), @@ -3693,251 +3692,251 @@ def _seg_35(): (0xA4C7, 'X'), (0xA4D0, 'V'), (0xA62C, 'X'), - (0xA640, 'M', u'ꙁ'), + (0xA640, 'M', 'ꙁ'), (0xA641, 'V'), - (0xA642, 'M', u'ꙃ'), + (0xA642, 'M', 'ꙃ'), (0xA643, 'V'), - (0xA644, 'M', u'ꙅ'), + (0xA644, 'M', 'ꙅ'), (0xA645, 'V'), - (0xA646, 'M', u'ꙇ'), + (0xA646, 'M', 'ꙇ'), (0xA647, 'V'), - (0xA648, 'M', u'ꙉ'), + (0xA648, 'M', 'ꙉ'), (0xA649, 'V'), - (0xA64A, 'M', u'ꙋ'), + (0xA64A, 'M', 'ꙋ'), (0xA64B, 'V'), - (0xA64C, 'M', u'ꙍ'), + (0xA64C, 'M', 'ꙍ'), (0xA64D, 'V'), - (0xA64E, 'M', u'ꙏ'), + (0xA64E, 'M', 'ꙏ'), (0xA64F, 'V'), - (0xA650, 'M', u'ꙑ'), + (0xA650, 'M', 'ꙑ'), (0xA651, 'V'), - (0xA652, 'M', u'ꙓ'), + (0xA652, 'M', 'ꙓ'), (0xA653, 'V'), - (0xA654, 'M', u'ꙕ'), + (0xA654, 'M', 'ꙕ'), (0xA655, 'V'), - (0xA656, 'M', u'ꙗ'), + (0xA656, 'M', 'ꙗ'), (0xA657, 'V'), - (0xA658, 'M', u'ꙙ'), + (0xA658, 'M', 'ꙙ'), (0xA659, 'V'), - (0xA65A, 'M', u'ꙛ'), + (0xA65A, 'M', 'ꙛ'), (0xA65B, 'V'), - (0xA65C, 'M', u'ꙝ'), + (0xA65C, 'M', 'ꙝ'), (0xA65D, 'V'), - (0xA65E, 'M', u'ꙟ'), + (0xA65E, 'M', 'ꙟ'), (0xA65F, 'V'), - (0xA660, 'M', u'ꙡ'), + (0xA660, 'M', 'ꙡ'), (0xA661, 'V'), - (0xA662, 'M', u'ꙣ'), + (0xA662, 'M', 'ꙣ'), (0xA663, 'V'), - (0xA664, 'M', u'ꙥ'), + (0xA664, 'M', 'ꙥ'), (0xA665, 'V'), - (0xA666, 'M', u'ꙧ'), + (0xA666, 'M', 'ꙧ'), (0xA667, 'V'), - (0xA668, 'M', u'ꙩ'), + (0xA668, 'M', 'ꙩ'), (0xA669, 'V'), - (0xA66A, 'M', u'ꙫ'), + (0xA66A, 'M', 'ꙫ'), (0xA66B, 'V'), - (0xA66C, 'M', u'ꙭ'), + (0xA66C, 'M', 'ꙭ'), (0xA66D, 'V'), - (0xA680, 'M', u'ꚁ'), + (0xA680, 'M', 'ꚁ'), (0xA681, 'V'), - (0xA682, 'M', u'ꚃ'), + (0xA682, 'M', 'ꚃ'), (0xA683, 'V'), - (0xA684, 'M', u'ꚅ'), + (0xA684, 'M', 'ꚅ'), (0xA685, 'V'), - (0xA686, 'M', u'ꚇ'), + (0xA686, 'M', 'ꚇ'), (0xA687, 'V'), ] def _seg_36(): return [ - (0xA688, 'M', u'ꚉ'), + (0xA688, 'M', 'ꚉ'), (0xA689, 'V'), - (0xA68A, 'M', u'ꚋ'), + (0xA68A, 'M', 'ꚋ'), (0xA68B, 'V'), - (0xA68C, 'M', u'ꚍ'), + (0xA68C, 'M', 'ꚍ'), (0xA68D, 'V'), - (0xA68E, 'M', u'ꚏ'), + (0xA68E, 'M', 'ꚏ'), (0xA68F, 'V'), - (0xA690, 'M', u'ꚑ'), + (0xA690, 'M', 'ꚑ'), (0xA691, 'V'), - (0xA692, 'M', u'ꚓ'), + (0xA692, 'M', 'ꚓ'), (0xA693, 'V'), - (0xA694, 'M', u'ꚕ'), + (0xA694, 'M', 'ꚕ'), (0xA695, 'V'), - (0xA696, 'M', u'ꚗ'), + (0xA696, 'M', 'ꚗ'), (0xA697, 'V'), - (0xA698, 'M', u'ꚙ'), + (0xA698, 'M', 'ꚙ'), (0xA699, 'V'), - (0xA69A, 'M', u'ꚛ'), + (0xA69A, 'M', 'ꚛ'), (0xA69B, 'V'), - (0xA69C, 'M', u'ъ'), - (0xA69D, 'M', u'ь'), + (0xA69C, 'M', 'ъ'), + (0xA69D, 'M', 'ь'), (0xA69E, 'V'), (0xA6F8, 'X'), (0xA700, 'V'), - (0xA722, 'M', u'ꜣ'), + (0xA722, 'M', 'ꜣ'), (0xA723, 'V'), - (0xA724, 'M', u'ꜥ'), + (0xA724, 'M', 'ꜥ'), (0xA725, 'V'), - (0xA726, 'M', u'ꜧ'), + (0xA726, 'M', 'ꜧ'), (0xA727, 'V'), - (0xA728, 'M', u'ꜩ'), + (0xA728, 'M', 'ꜩ'), (0xA729, 'V'), - (0xA72A, 'M', u'ꜫ'), + (0xA72A, 'M', 'ꜫ'), (0xA72B, 'V'), - (0xA72C, 'M', u'ꜭ'), + (0xA72C, 'M', 'ꜭ'), (0xA72D, 'V'), - (0xA72E, 'M', u'ꜯ'), + (0xA72E, 'M', 'ꜯ'), (0xA72F, 'V'), - (0xA732, 'M', u'ꜳ'), + (0xA732, 'M', 'ꜳ'), (0xA733, 'V'), - (0xA734, 'M', u'ꜵ'), + (0xA734, 'M', 'ꜵ'), (0xA735, 'V'), - (0xA736, 'M', u'ꜷ'), + (0xA736, 'M', 'ꜷ'), (0xA737, 'V'), - (0xA738, 'M', u'ꜹ'), + (0xA738, 'M', 'ꜹ'), (0xA739, 'V'), - (0xA73A, 'M', u'ꜻ'), + (0xA73A, 'M', 'ꜻ'), (0xA73B, 'V'), - (0xA73C, 'M', u'ꜽ'), + (0xA73C, 'M', 'ꜽ'), (0xA73D, 'V'), - (0xA73E, 'M', u'ꜿ'), + (0xA73E, 'M', 'ꜿ'), (0xA73F, 'V'), - (0xA740, 'M', u'ꝁ'), + (0xA740, 'M', 'ꝁ'), (0xA741, 'V'), - (0xA742, 'M', u'ꝃ'), + (0xA742, 'M', 'ꝃ'), (0xA743, 'V'), - (0xA744, 'M', u'ꝅ'), + (0xA744, 'M', 'ꝅ'), (0xA745, 'V'), - (0xA746, 'M', u'ꝇ'), + (0xA746, 'M', 'ꝇ'), (0xA747, 'V'), - (0xA748, 'M', u'ꝉ'), + (0xA748, 'M', 'ꝉ'), (0xA749, 'V'), - (0xA74A, 'M', u'ꝋ'), + (0xA74A, 'M', 'ꝋ'), (0xA74B, 'V'), - (0xA74C, 'M', u'ꝍ'), + (0xA74C, 'M', 'ꝍ'), (0xA74D, 'V'), - (0xA74E, 'M', u'ꝏ'), + (0xA74E, 'M', 'ꝏ'), (0xA74F, 'V'), - (0xA750, 'M', u'ꝑ'), + (0xA750, 'M', 'ꝑ'), (0xA751, 'V'), - (0xA752, 'M', u'ꝓ'), + (0xA752, 'M', 'ꝓ'), (0xA753, 'V'), - (0xA754, 'M', u'ꝕ'), + (0xA754, 'M', 'ꝕ'), (0xA755, 'V'), - (0xA756, 'M', u'ꝗ'), + (0xA756, 'M', 'ꝗ'), (0xA757, 'V'), - (0xA758, 'M', u'ꝙ'), + (0xA758, 'M', 'ꝙ'), (0xA759, 'V'), - (0xA75A, 'M', u'ꝛ'), + (0xA75A, 'M', 'ꝛ'), (0xA75B, 'V'), - (0xA75C, 'M', u'ꝝ'), + (0xA75C, 'M', 'ꝝ'), (0xA75D, 'V'), - (0xA75E, 'M', u'ꝟ'), + (0xA75E, 'M', 'ꝟ'), (0xA75F, 'V'), - (0xA760, 'M', u'ꝡ'), + (0xA760, 'M', 'ꝡ'), (0xA761, 'V'), - (0xA762, 'M', u'ꝣ'), + (0xA762, 'M', 'ꝣ'), (0xA763, 'V'), - (0xA764, 'M', u'ꝥ'), + (0xA764, 'M', 'ꝥ'), (0xA765, 'V'), - (0xA766, 'M', u'ꝧ'), + (0xA766, 'M', 'ꝧ'), (0xA767, 'V'), - (0xA768, 'M', u'ꝩ'), + (0xA768, 'M', 'ꝩ'), (0xA769, 'V'), - (0xA76A, 'M', u'ꝫ'), + (0xA76A, 'M', 'ꝫ'), (0xA76B, 'V'), - (0xA76C, 'M', u'ꝭ'), + (0xA76C, 'M', 'ꝭ'), (0xA76D, 'V'), - (0xA76E, 'M', u'ꝯ'), + (0xA76E, 'M', 'ꝯ'), ] def _seg_37(): return [ (0xA76F, 'V'), - (0xA770, 'M', u'ꝯ'), + (0xA770, 'M', 'ꝯ'), (0xA771, 'V'), - (0xA779, 'M', u'ꝺ'), + (0xA779, 'M', 'ꝺ'), (0xA77A, 'V'), - (0xA77B, 'M', u'ꝼ'), + (0xA77B, 'M', 'ꝼ'), (0xA77C, 'V'), - (0xA77D, 'M', u'ᵹ'), - (0xA77E, 'M', u'ꝿ'), + (0xA77D, 'M', 'ᵹ'), + (0xA77E, 'M', 'ꝿ'), (0xA77F, 'V'), - (0xA780, 'M', u'ꞁ'), + (0xA780, 'M', 'ꞁ'), (0xA781, 'V'), - (0xA782, 'M', u'ꞃ'), + (0xA782, 'M', 'ꞃ'), (0xA783, 'V'), - (0xA784, 'M', u'ꞅ'), + (0xA784, 'M', 'ꞅ'), (0xA785, 'V'), - (0xA786, 'M', u'ꞇ'), + (0xA786, 'M', 'ꞇ'), (0xA787, 'V'), - (0xA78B, 'M', u'ꞌ'), + (0xA78B, 'M', 'ꞌ'), (0xA78C, 'V'), - (0xA78D, 'M', u'ɥ'), + (0xA78D, 'M', 'ɥ'), (0xA78E, 'V'), - (0xA790, 'M', u'ꞑ'), + (0xA790, 'M', 'ꞑ'), (0xA791, 'V'), - (0xA792, 'M', u'ꞓ'), + (0xA792, 'M', 'ꞓ'), (0xA793, 'V'), - (0xA796, 'M', u'ꞗ'), + (0xA796, 'M', 'ꞗ'), (0xA797, 'V'), - (0xA798, 'M', u'ꞙ'), + (0xA798, 'M', 'ꞙ'), (0xA799, 'V'), - (0xA79A, 'M', u'ꞛ'), + (0xA79A, 'M', 'ꞛ'), (0xA79B, 'V'), - (0xA79C, 'M', u'ꞝ'), + (0xA79C, 'M', 'ꞝ'), (0xA79D, 'V'), - (0xA79E, 'M', u'ꞟ'), + (0xA79E, 'M', 'ꞟ'), (0xA79F, 'V'), - (0xA7A0, 'M', u'ꞡ'), + (0xA7A0, 'M', 'ꞡ'), (0xA7A1, 'V'), - (0xA7A2, 'M', u'ꞣ'), + (0xA7A2, 'M', 'ꞣ'), (0xA7A3, 'V'), - (0xA7A4, 'M', u'ꞥ'), + (0xA7A4, 'M', 'ꞥ'), (0xA7A5, 'V'), - (0xA7A6, 'M', u'ꞧ'), + (0xA7A6, 'M', 'ꞧ'), (0xA7A7, 'V'), - (0xA7A8, 'M', u'ꞩ'), + (0xA7A8, 'M', 'ꞩ'), (0xA7A9, 'V'), - (0xA7AA, 'M', u'ɦ'), - (0xA7AB, 'M', u'ɜ'), - (0xA7AC, 'M', u'ɡ'), - (0xA7AD, 'M', u'ɬ'), - (0xA7AE, 'M', u'ɪ'), + (0xA7AA, 'M', 'ɦ'), + (0xA7AB, 'M', 'ɜ'), + (0xA7AC, 'M', 'ɡ'), + (0xA7AD, 'M', 'ɬ'), + (0xA7AE, 'M', 'ɪ'), (0xA7AF, 'V'), - (0xA7B0, 'M', u'ʞ'), - (0xA7B1, 'M', u'ʇ'), - (0xA7B2, 'M', u'ʝ'), - (0xA7B3, 'M', u'ꭓ'), - (0xA7B4, 'M', u'ꞵ'), + (0xA7B0, 'M', 'ʞ'), + (0xA7B1, 'M', 'ʇ'), + (0xA7B2, 'M', 'ʝ'), + (0xA7B3, 'M', 'ꭓ'), + (0xA7B4, 'M', 'ꞵ'), (0xA7B5, 'V'), - (0xA7B6, 'M', u'ꞷ'), + (0xA7B6, 'M', 'ꞷ'), (0xA7B7, 'V'), - (0xA7B8, 'M', u'ꞹ'), + (0xA7B8, 'M', 'ꞹ'), (0xA7B9, 'V'), - (0xA7BA, 'M', u'ꞻ'), + (0xA7BA, 'M', 'ꞻ'), (0xA7BB, 'V'), - (0xA7BC, 'M', u'ꞽ'), + (0xA7BC, 'M', 'ꞽ'), (0xA7BD, 'V'), - (0xA7BE, 'M', u'ꞿ'), + (0xA7BE, 'M', 'ꞿ'), (0xA7BF, 'V'), (0xA7C0, 'X'), - (0xA7C2, 'M', u'ꟃ'), + (0xA7C2, 'M', 'ꟃ'), (0xA7C3, 'V'), - (0xA7C4, 'M', u'ꞔ'), - (0xA7C5, 'M', u'ʂ'), - (0xA7C6, 'M', u'ᶎ'), - (0xA7C7, 'M', u'ꟈ'), + (0xA7C4, 'M', 'ꞔ'), + (0xA7C5, 'M', 'ʂ'), + (0xA7C6, 'M', 'ᶎ'), + (0xA7C7, 'M', 'ꟈ'), (0xA7C8, 'V'), - (0xA7C9, 'M', u'ꟊ'), + (0xA7C9, 'M', 'ꟊ'), (0xA7CA, 'V'), (0xA7CB, 'X'), - (0xA7F5, 'M', u'ꟶ'), + (0xA7F5, 'M', 'ꟶ'), (0xA7F6, 'V'), - (0xA7F8, 'M', u'ħ'), - (0xA7F9, 'M', u'œ'), + (0xA7F8, 'M', 'ħ'), + (0xA7F9, 'M', 'œ'), (0xA7FA, 'V'), (0xA82D, 'X'), (0xA830, 'V'), @@ -3983,98 +3982,98 @@ def _seg_38(): (0xAB28, 'V'), (0xAB2F, 'X'), (0xAB30, 'V'), - (0xAB5C, 'M', u'ꜧ'), - (0xAB5D, 'M', u'ꬷ'), - (0xAB5E, 'M', u'ɫ'), - (0xAB5F, 'M', u'ꭒ'), + (0xAB5C, 'M', 'ꜧ'), + (0xAB5D, 'M', 'ꬷ'), + (0xAB5E, 'M', 'ɫ'), + (0xAB5F, 'M', 'ꭒ'), (0xAB60, 'V'), - (0xAB69, 'M', u'ʍ'), + (0xAB69, 'M', 'ʍ'), (0xAB6A, 'V'), (0xAB6C, 'X'), - (0xAB70, 'M', u'Ꭰ'), - (0xAB71, 'M', u'Ꭱ'), - (0xAB72, 'M', u'Ꭲ'), - (0xAB73, 'M', u'Ꭳ'), - (0xAB74, 'M', u'Ꭴ'), - (0xAB75, 'M', u'Ꭵ'), - (0xAB76, 'M', u'Ꭶ'), - (0xAB77, 'M', u'Ꭷ'), - (0xAB78, 'M', u'Ꭸ'), - (0xAB79, 'M', u'Ꭹ'), - (0xAB7A, 'M', u'Ꭺ'), - (0xAB7B, 'M', u'Ꭻ'), - (0xAB7C, 'M', u'Ꭼ'), - (0xAB7D, 'M', u'Ꭽ'), - (0xAB7E, 'M', u'Ꭾ'), - (0xAB7F, 'M', u'Ꭿ'), - (0xAB80, 'M', u'Ꮀ'), - (0xAB81, 'M', u'Ꮁ'), - (0xAB82, 'M', u'Ꮂ'), - (0xAB83, 'M', u'Ꮃ'), - (0xAB84, 'M', u'Ꮄ'), - (0xAB85, 'M', u'Ꮅ'), - (0xAB86, 'M', u'Ꮆ'), - (0xAB87, 'M', u'Ꮇ'), - (0xAB88, 'M', u'Ꮈ'), - (0xAB89, 'M', u'Ꮉ'), - (0xAB8A, 'M', u'Ꮊ'), - (0xAB8B, 'M', u'Ꮋ'), - (0xAB8C, 'M', u'Ꮌ'), - (0xAB8D, 'M', u'Ꮍ'), - (0xAB8E, 'M', u'Ꮎ'), - (0xAB8F, 'M', u'Ꮏ'), - (0xAB90, 'M', u'Ꮐ'), - (0xAB91, 'M', u'Ꮑ'), - (0xAB92, 'M', u'Ꮒ'), - (0xAB93, 'M', u'Ꮓ'), - (0xAB94, 'M', u'Ꮔ'), - (0xAB95, 'M', u'Ꮕ'), - (0xAB96, 'M', u'Ꮖ'), - (0xAB97, 'M', u'Ꮗ'), - (0xAB98, 'M', u'Ꮘ'), - (0xAB99, 'M', u'Ꮙ'), - (0xAB9A, 'M', u'Ꮚ'), - (0xAB9B, 'M', u'Ꮛ'), - (0xAB9C, 'M', u'Ꮜ'), - (0xAB9D, 'M', u'Ꮝ'), - (0xAB9E, 'M', u'Ꮞ'), - (0xAB9F, 'M', u'Ꮟ'), - (0xABA0, 'M', u'Ꮠ'), - (0xABA1, 'M', u'Ꮡ'), - (0xABA2, 'M', u'Ꮢ'), - (0xABA3, 'M', u'Ꮣ'), - (0xABA4, 'M', u'Ꮤ'), - (0xABA5, 'M', u'Ꮥ'), - (0xABA6, 'M', u'Ꮦ'), - (0xABA7, 'M', u'Ꮧ'), - (0xABA8, 'M', u'Ꮨ'), - (0xABA9, 'M', u'Ꮩ'), - (0xABAA, 'M', u'Ꮪ'), - (0xABAB, 'M', u'Ꮫ'), - (0xABAC, 'M', u'Ꮬ'), - (0xABAD, 'M', u'Ꮭ'), - (0xABAE, 'M', u'Ꮮ'), - (0xABAF, 'M', u'Ꮯ'), - (0xABB0, 'M', u'Ꮰ'), - (0xABB1, 'M', u'Ꮱ'), - (0xABB2, 'M', u'Ꮲ'), - (0xABB3, 'M', u'Ꮳ'), + (0xAB70, 'M', 'Ꭰ'), + (0xAB71, 'M', 'Ꭱ'), + (0xAB72, 'M', 'Ꭲ'), + (0xAB73, 'M', 'Ꭳ'), + (0xAB74, 'M', 'Ꭴ'), + (0xAB75, 'M', 'Ꭵ'), + (0xAB76, 'M', 'Ꭶ'), + (0xAB77, 'M', 'Ꭷ'), + (0xAB78, 'M', 'Ꭸ'), + (0xAB79, 'M', 'Ꭹ'), + (0xAB7A, 'M', 'Ꭺ'), + (0xAB7B, 'M', 'Ꭻ'), + (0xAB7C, 'M', 'Ꭼ'), + (0xAB7D, 'M', 'Ꭽ'), + (0xAB7E, 'M', 'Ꭾ'), + (0xAB7F, 'M', 'Ꭿ'), + (0xAB80, 'M', 'Ꮀ'), + (0xAB81, 'M', 'Ꮁ'), + (0xAB82, 'M', 'Ꮂ'), + (0xAB83, 'M', 'Ꮃ'), + (0xAB84, 'M', 'Ꮄ'), + (0xAB85, 'M', 'Ꮅ'), + (0xAB86, 'M', 'Ꮆ'), + (0xAB87, 'M', 'Ꮇ'), + (0xAB88, 'M', 'Ꮈ'), + (0xAB89, 'M', 'Ꮉ'), + (0xAB8A, 'M', 'Ꮊ'), + (0xAB8B, 'M', 'Ꮋ'), + (0xAB8C, 'M', 'Ꮌ'), + (0xAB8D, 'M', 'Ꮍ'), + (0xAB8E, 'M', 'Ꮎ'), + (0xAB8F, 'M', 'Ꮏ'), + (0xAB90, 'M', 'Ꮐ'), + (0xAB91, 'M', 'Ꮑ'), + (0xAB92, 'M', 'Ꮒ'), + (0xAB93, 'M', 'Ꮓ'), + (0xAB94, 'M', 'Ꮔ'), + (0xAB95, 'M', 'Ꮕ'), + (0xAB96, 'M', 'Ꮖ'), + (0xAB97, 'M', 'Ꮗ'), + (0xAB98, 'M', 'Ꮘ'), + (0xAB99, 'M', 'Ꮙ'), + (0xAB9A, 'M', 'Ꮚ'), + (0xAB9B, 'M', 'Ꮛ'), + (0xAB9C, 'M', 'Ꮜ'), + (0xAB9D, 'M', 'Ꮝ'), + (0xAB9E, 'M', 'Ꮞ'), + (0xAB9F, 'M', 'Ꮟ'), + (0xABA0, 'M', 'Ꮠ'), + (0xABA1, 'M', 'Ꮡ'), + (0xABA2, 'M', 'Ꮢ'), + (0xABA3, 'M', 'Ꮣ'), + (0xABA4, 'M', 'Ꮤ'), + (0xABA5, 'M', 'Ꮥ'), + (0xABA6, 'M', 'Ꮦ'), + (0xABA7, 'M', 'Ꮧ'), + (0xABA8, 'M', 'Ꮨ'), + (0xABA9, 'M', 'Ꮩ'), + (0xABAA, 'M', 'Ꮪ'), + (0xABAB, 'M', 'Ꮫ'), + (0xABAC, 'M', 'Ꮬ'), + (0xABAD, 'M', 'Ꮭ'), + (0xABAE, 'M', 'Ꮮ'), + (0xABAF, 'M', 'Ꮯ'), + (0xABB0, 'M', 'Ꮰ'), + (0xABB1, 'M', 'Ꮱ'), + (0xABB2, 'M', 'Ꮲ'), + (0xABB3, 'M', 'Ꮳ'), ] def _seg_39(): return [ - (0xABB4, 'M', u'Ꮴ'), - (0xABB5, 'M', u'Ꮵ'), - (0xABB6, 'M', u'Ꮶ'), - (0xABB7, 'M', u'Ꮷ'), - (0xABB8, 'M', u'Ꮸ'), - (0xABB9, 'M', u'Ꮹ'), - (0xABBA, 'M', u'Ꮺ'), - (0xABBB, 'M', u'Ꮻ'), - (0xABBC, 'M', u'Ꮼ'), - (0xABBD, 'M', u'Ꮽ'), - (0xABBE, 'M', u'Ꮾ'), - (0xABBF, 'M', u'Ꮿ'), + (0xABB4, 'M', 'Ꮴ'), + (0xABB5, 'M', 'Ꮵ'), + (0xABB6, 'M', 'Ꮶ'), + (0xABB7, 'M', 'Ꮷ'), + (0xABB8, 'M', 'Ꮸ'), + (0xABB9, 'M', 'Ꮹ'), + (0xABBA, 'M', 'Ꮺ'), + (0xABBB, 'M', 'Ꮻ'), + (0xABBC, 'M', 'Ꮼ'), + (0xABBD, 'M', 'Ꮽ'), + (0xABBE, 'M', 'Ꮾ'), + (0xABBF, 'M', 'Ꮿ'), (0xABC0, 'V'), (0xABEE, 'X'), (0xABF0, 'V'), @@ -4085,1432 +4084,1432 @@ def _seg_39(): (0xD7C7, 'X'), (0xD7CB, 'V'), (0xD7FC, 'X'), - (0xF900, 'M', u'豈'), - (0xF901, 'M', u'更'), - (0xF902, 'M', u'車'), - (0xF903, 'M', u'賈'), - (0xF904, 'M', u'滑'), - (0xF905, 'M', u'串'), - (0xF906, 'M', u'句'), - (0xF907, 'M', u'龜'), - (0xF909, 'M', u'契'), - (0xF90A, 'M', u'金'), - (0xF90B, 'M', u'喇'), - (0xF90C, 'M', u'奈'), - (0xF90D, 'M', u'懶'), - (0xF90E, 'M', u'癩'), - (0xF90F, 'M', u'羅'), - (0xF910, 'M', u'蘿'), - (0xF911, 'M', u'螺'), - (0xF912, 'M', u'裸'), - (0xF913, 'M', u'邏'), - (0xF914, 'M', u'樂'), - (0xF915, 'M', u'洛'), - (0xF916, 'M', u'烙'), - (0xF917, 'M', u'珞'), - (0xF918, 'M', u'落'), - (0xF919, 'M', u'酪'), - (0xF91A, 'M', u'駱'), - (0xF91B, 'M', u'亂'), - (0xF91C, 'M', u'卵'), - (0xF91D, 'M', u'欄'), - (0xF91E, 'M', u'爛'), - (0xF91F, 'M', u'蘭'), - (0xF920, 'M', u'鸞'), - (0xF921, 'M', u'嵐'), - (0xF922, 'M', u'濫'), - (0xF923, 'M', u'藍'), - (0xF924, 'M', u'襤'), - (0xF925, 'M', u'拉'), - (0xF926, 'M', u'臘'), - (0xF927, 'M', u'蠟'), - (0xF928, 'M', u'廊'), - (0xF929, 'M', u'朗'), - (0xF92A, 'M', u'浪'), - (0xF92B, 'M', u'狼'), - (0xF92C, 'M', u'郎'), - (0xF92D, 'M', u'來'), - (0xF92E, 'M', u'冷'), - (0xF92F, 'M', u'勞'), - (0xF930, 'M', u'擄'), - (0xF931, 'M', u'櫓'), - (0xF932, 'M', u'爐'), - (0xF933, 'M', u'盧'), - (0xF934, 'M', u'老'), - (0xF935, 'M', u'蘆'), - (0xF936, 'M', u'虜'), - (0xF937, 'M', u'路'), - (0xF938, 'M', u'露'), - (0xF939, 'M', u'魯'), - (0xF93A, 'M', u'鷺'), - (0xF93B, 'M', u'碌'), - (0xF93C, 'M', u'祿'), - (0xF93D, 'M', u'綠'), - (0xF93E, 'M', u'菉'), - (0xF93F, 'M', u'錄'), - (0xF940, 'M', u'鹿'), - (0xF941, 'M', u'論'), - (0xF942, 'M', u'壟'), - (0xF943, 'M', u'弄'), - (0xF944, 'M', u'籠'), - (0xF945, 'M', u'聾'), - (0xF946, 'M', u'牢'), - (0xF947, 'M', u'磊'), - (0xF948, 'M', u'賂'), - (0xF949, 'M', u'雷'), - (0xF94A, 'M', u'壘'), - (0xF94B, 'M', u'屢'), - (0xF94C, 'M', u'樓'), - (0xF94D, 'M', u'淚'), - (0xF94E, 'M', u'漏'), + (0xF900, 'M', '豈'), + (0xF901, 'M', '更'), + (0xF902, 'M', '車'), + (0xF903, 'M', '賈'), + (0xF904, 'M', '滑'), + (0xF905, 'M', '串'), + (0xF906, 'M', '句'), + (0xF907, 'M', '龜'), + (0xF909, 'M', '契'), + (0xF90A, 'M', '金'), + (0xF90B, 'M', '喇'), + (0xF90C, 'M', '奈'), + (0xF90D, 'M', '懶'), + (0xF90E, 'M', '癩'), + (0xF90F, 'M', '羅'), + (0xF910, 'M', '蘿'), + (0xF911, 'M', '螺'), + (0xF912, 'M', '裸'), + (0xF913, 'M', '邏'), + (0xF914, 'M', '樂'), + (0xF915, 'M', '洛'), + (0xF916, 'M', '烙'), + (0xF917, 'M', '珞'), + (0xF918, 'M', '落'), + (0xF919, 'M', '酪'), + (0xF91A, 'M', '駱'), + (0xF91B, 'M', '亂'), + (0xF91C, 'M', '卵'), + (0xF91D, 'M', '欄'), + (0xF91E, 'M', '爛'), + (0xF91F, 'M', '蘭'), + (0xF920, 'M', '鸞'), + (0xF921, 'M', '嵐'), + (0xF922, 'M', '濫'), + (0xF923, 'M', '藍'), + (0xF924, 'M', '襤'), + (0xF925, 'M', '拉'), + (0xF926, 'M', '臘'), + (0xF927, 'M', '蠟'), + (0xF928, 'M', '廊'), + (0xF929, 'M', '朗'), + (0xF92A, 'M', '浪'), + (0xF92B, 'M', '狼'), + (0xF92C, 'M', '郎'), + (0xF92D, 'M', '來'), + (0xF92E, 'M', '冷'), + (0xF92F, 'M', '勞'), + (0xF930, 'M', '擄'), + (0xF931, 'M', '櫓'), + (0xF932, 'M', '爐'), + (0xF933, 'M', '盧'), + (0xF934, 'M', '老'), + (0xF935, 'M', '蘆'), + (0xF936, 'M', '虜'), + (0xF937, 'M', '路'), + (0xF938, 'M', '露'), + (0xF939, 'M', '魯'), + (0xF93A, 'M', '鷺'), + (0xF93B, 'M', '碌'), + (0xF93C, 'M', '祿'), + (0xF93D, 'M', '綠'), + (0xF93E, 'M', '菉'), + (0xF93F, 'M', '錄'), + (0xF940, 'M', '鹿'), + (0xF941, 'M', '論'), + (0xF942, 'M', '壟'), + (0xF943, 'M', '弄'), + (0xF944, 'M', '籠'), + (0xF945, 'M', '聾'), + (0xF946, 'M', '牢'), + (0xF947, 'M', '磊'), + (0xF948, 'M', '賂'), + (0xF949, 'M', '雷'), + (0xF94A, 'M', '壘'), + (0xF94B, 'M', '屢'), + (0xF94C, 'M', '樓'), + (0xF94D, 'M', '淚'), + (0xF94E, 'M', '漏'), ] def _seg_40(): return [ - (0xF94F, 'M', u'累'), - (0xF950, 'M', u'縷'), - (0xF951, 'M', u'陋'), - (0xF952, 'M', u'勒'), - (0xF953, 'M', u'肋'), - (0xF954, 'M', u'凜'), - (0xF955, 'M', u'凌'), - (0xF956, 'M', u'稜'), - (0xF957, 'M', u'綾'), - (0xF958, 'M', u'菱'), - (0xF959, 'M', u'陵'), - (0xF95A, 'M', u'讀'), - (0xF95B, 'M', u'拏'), - (0xF95C, 'M', u'樂'), - (0xF95D, 'M', u'諾'), - (0xF95E, 'M', u'丹'), - (0xF95F, 'M', u'寧'), - (0xF960, 'M', u'怒'), - (0xF961, 'M', u'率'), - (0xF962, 'M', u'異'), - (0xF963, 'M', u'北'), - (0xF964, 'M', u'磻'), - (0xF965, 'M', u'便'), - (0xF966, 'M', u'復'), - (0xF967, 'M', u'不'), - (0xF968, 'M', u'泌'), - (0xF969, 'M', u'數'), - (0xF96A, 'M', u'索'), - (0xF96B, 'M', u'參'), - (0xF96C, 'M', u'塞'), - (0xF96D, 'M', u'省'), - (0xF96E, 'M', u'葉'), - (0xF96F, 'M', u'說'), - (0xF970, 'M', u'殺'), - (0xF971, 'M', u'辰'), - (0xF972, 'M', u'沈'), - (0xF973, 'M', u'拾'), - (0xF974, 'M', u'若'), - (0xF975, 'M', u'掠'), - (0xF976, 'M', u'略'), - (0xF977, 'M', u'亮'), - (0xF978, 'M', u'兩'), - (0xF979, 'M', u'凉'), - (0xF97A, 'M', u'梁'), - (0xF97B, 'M', u'糧'), - (0xF97C, 'M', u'良'), - (0xF97D, 'M', u'諒'), - (0xF97E, 'M', u'量'), - (0xF97F, 'M', u'勵'), - (0xF980, 'M', u'呂'), - (0xF981, 'M', u'女'), - (0xF982, 'M', u'廬'), - (0xF983, 'M', u'旅'), - (0xF984, 'M', u'濾'), - (0xF985, 'M', u'礪'), - (0xF986, 'M', u'閭'), - (0xF987, 'M', u'驪'), - (0xF988, 'M', u'麗'), - (0xF989, 'M', u'黎'), - (0xF98A, 'M', u'力'), - (0xF98B, 'M', u'曆'), - (0xF98C, 'M', u'歷'), - (0xF98D, 'M', u'轢'), - (0xF98E, 'M', u'年'), - (0xF98F, 'M', u'憐'), - (0xF990, 'M', u'戀'), - (0xF991, 'M', u'撚'), - (0xF992, 'M', u'漣'), - (0xF993, 'M', u'煉'), - (0xF994, 'M', u'璉'), - (0xF995, 'M', u'秊'), - (0xF996, 'M', u'練'), - (0xF997, 'M', u'聯'), - (0xF998, 'M', u'輦'), - (0xF999, 'M', u'蓮'), - (0xF99A, 'M', u'連'), - (0xF99B, 'M', u'鍊'), - (0xF99C, 'M', u'列'), - (0xF99D, 'M', u'劣'), - (0xF99E, 'M', u'咽'), - (0xF99F, 'M', u'烈'), - (0xF9A0, 'M', u'裂'), - (0xF9A1, 'M', u'說'), - (0xF9A2, 'M', u'廉'), - (0xF9A3, 'M', u'念'), - (0xF9A4, 'M', u'捻'), - (0xF9A5, 'M', u'殮'), - (0xF9A6, 'M', u'簾'), - (0xF9A7, 'M', u'獵'), - (0xF9A8, 'M', u'令'), - (0xF9A9, 'M', u'囹'), - (0xF9AA, 'M', u'寧'), - (0xF9AB, 'M', u'嶺'), - (0xF9AC, 'M', u'怜'), - (0xF9AD, 'M', u'玲'), - (0xF9AE, 'M', u'瑩'), - (0xF9AF, 'M', u'羚'), - (0xF9B0, 'M', u'聆'), - (0xF9B1, 'M', u'鈴'), - (0xF9B2, 'M', u'零'), + (0xF94F, 'M', '累'), + (0xF950, 'M', '縷'), + (0xF951, 'M', '陋'), + (0xF952, 'M', '勒'), + (0xF953, 'M', '肋'), + (0xF954, 'M', '凜'), + (0xF955, 'M', '凌'), + (0xF956, 'M', '稜'), + (0xF957, 'M', '綾'), + (0xF958, 'M', '菱'), + (0xF959, 'M', '陵'), + (0xF95A, 'M', '讀'), + (0xF95B, 'M', '拏'), + (0xF95C, 'M', '樂'), + (0xF95D, 'M', '諾'), + (0xF95E, 'M', '丹'), + (0xF95F, 'M', '寧'), + (0xF960, 'M', '怒'), + (0xF961, 'M', '率'), + (0xF962, 'M', '異'), + (0xF963, 'M', '北'), + (0xF964, 'M', '磻'), + (0xF965, 'M', '便'), + (0xF966, 'M', '復'), + (0xF967, 'M', '不'), + (0xF968, 'M', '泌'), + (0xF969, 'M', '數'), + (0xF96A, 'M', '索'), + (0xF96B, 'M', '參'), + (0xF96C, 'M', '塞'), + (0xF96D, 'M', '省'), + (0xF96E, 'M', '葉'), + (0xF96F, 'M', '說'), + (0xF970, 'M', '殺'), + (0xF971, 'M', '辰'), + (0xF972, 'M', '沈'), + (0xF973, 'M', '拾'), + (0xF974, 'M', '若'), + (0xF975, 'M', '掠'), + (0xF976, 'M', '略'), + (0xF977, 'M', '亮'), + (0xF978, 'M', '兩'), + (0xF979, 'M', '凉'), + (0xF97A, 'M', '梁'), + (0xF97B, 'M', '糧'), + (0xF97C, 'M', '良'), + (0xF97D, 'M', '諒'), + (0xF97E, 'M', '量'), + (0xF97F, 'M', '勵'), + (0xF980, 'M', '呂'), + (0xF981, 'M', '女'), + (0xF982, 'M', '廬'), + (0xF983, 'M', '旅'), + (0xF984, 'M', '濾'), + (0xF985, 'M', '礪'), + (0xF986, 'M', '閭'), + (0xF987, 'M', '驪'), + (0xF988, 'M', '麗'), + (0xF989, 'M', '黎'), + (0xF98A, 'M', '力'), + (0xF98B, 'M', '曆'), + (0xF98C, 'M', '歷'), + (0xF98D, 'M', '轢'), + (0xF98E, 'M', '年'), + (0xF98F, 'M', '憐'), + (0xF990, 'M', '戀'), + (0xF991, 'M', '撚'), + (0xF992, 'M', '漣'), + (0xF993, 'M', '煉'), + (0xF994, 'M', '璉'), + (0xF995, 'M', '秊'), + (0xF996, 'M', '練'), + (0xF997, 'M', '聯'), + (0xF998, 'M', '輦'), + (0xF999, 'M', '蓮'), + (0xF99A, 'M', '連'), + (0xF99B, 'M', '鍊'), + (0xF99C, 'M', '列'), + (0xF99D, 'M', '劣'), + (0xF99E, 'M', '咽'), + (0xF99F, 'M', '烈'), + (0xF9A0, 'M', '裂'), + (0xF9A1, 'M', '說'), + (0xF9A2, 'M', '廉'), + (0xF9A3, 'M', '念'), + (0xF9A4, 'M', '捻'), + (0xF9A5, 'M', '殮'), + (0xF9A6, 'M', '簾'), + (0xF9A7, 'M', '獵'), + (0xF9A8, 'M', '令'), + (0xF9A9, 'M', '囹'), + (0xF9AA, 'M', '寧'), + (0xF9AB, 'M', '嶺'), + (0xF9AC, 'M', '怜'), + (0xF9AD, 'M', '玲'), + (0xF9AE, 'M', '瑩'), + (0xF9AF, 'M', '羚'), + (0xF9B0, 'M', '聆'), + (0xF9B1, 'M', '鈴'), + (0xF9B2, 'M', '零'), ] def _seg_41(): return [ - (0xF9B3, 'M', u'靈'), - (0xF9B4, 'M', u'領'), - (0xF9B5, 'M', u'例'), - (0xF9B6, 'M', u'禮'), - (0xF9B7, 'M', u'醴'), - (0xF9B8, 'M', u'隸'), - (0xF9B9, 'M', u'惡'), - (0xF9BA, 'M', u'了'), - (0xF9BB, 'M', u'僚'), - (0xF9BC, 'M', u'寮'), - (0xF9BD, 'M', u'尿'), - (0xF9BE, 'M', u'料'), - (0xF9BF, 'M', u'樂'), - (0xF9C0, 'M', u'燎'), - (0xF9C1, 'M', u'療'), - (0xF9C2, 'M', u'蓼'), - (0xF9C3, 'M', u'遼'), - (0xF9C4, 'M', u'龍'), - (0xF9C5, 'M', u'暈'), - (0xF9C6, 'M', u'阮'), - (0xF9C7, 'M', u'劉'), - (0xF9C8, 'M', u'杻'), - (0xF9C9, 'M', u'柳'), - (0xF9CA, 'M', u'流'), - (0xF9CB, 'M', u'溜'), - (0xF9CC, 'M', u'琉'), - (0xF9CD, 'M', u'留'), - (0xF9CE, 'M', u'硫'), - (0xF9CF, 'M', u'紐'), - (0xF9D0, 'M', u'類'), - (0xF9D1, 'M', u'六'), - (0xF9D2, 'M', u'戮'), - (0xF9D3, 'M', u'陸'), - (0xF9D4, 'M', u'倫'), - (0xF9D5, 'M', u'崙'), - (0xF9D6, 'M', u'淪'), - (0xF9D7, 'M', u'輪'), - (0xF9D8, 'M', u'律'), - (0xF9D9, 'M', u'慄'), - (0xF9DA, 'M', u'栗'), - (0xF9DB, 'M', u'率'), - (0xF9DC, 'M', u'隆'), - (0xF9DD, 'M', u'利'), - (0xF9DE, 'M', u'吏'), - (0xF9DF, 'M', u'履'), - (0xF9E0, 'M', u'易'), - (0xF9E1, 'M', u'李'), - (0xF9E2, 'M', u'梨'), - (0xF9E3, 'M', u'泥'), - (0xF9E4, 'M', u'理'), - (0xF9E5, 'M', u'痢'), - (0xF9E6, 'M', u'罹'), - (0xF9E7, 'M', u'裏'), - (0xF9E8, 'M', u'裡'), - (0xF9E9, 'M', u'里'), - (0xF9EA, 'M', u'離'), - (0xF9EB, 'M', u'匿'), - (0xF9EC, 'M', u'溺'), - (0xF9ED, 'M', u'吝'), - (0xF9EE, 'M', u'燐'), - (0xF9EF, 'M', u'璘'), - (0xF9F0, 'M', u'藺'), - (0xF9F1, 'M', u'隣'), - (0xF9F2, 'M', u'鱗'), - (0xF9F3, 'M', u'麟'), - (0xF9F4, 'M', u'林'), - (0xF9F5, 'M', u'淋'), - (0xF9F6, 'M', u'臨'), - (0xF9F7, 'M', u'立'), - (0xF9F8, 'M', u'笠'), - (0xF9F9, 'M', u'粒'), - (0xF9FA, 'M', u'狀'), - (0xF9FB, 'M', u'炙'), - (0xF9FC, 'M', u'識'), - (0xF9FD, 'M', u'什'), - (0xF9FE, 'M', u'茶'), - (0xF9FF, 'M', u'刺'), - (0xFA00, 'M', u'切'), - (0xFA01, 'M', u'度'), - (0xFA02, 'M', u'拓'), - (0xFA03, 'M', u'糖'), - (0xFA04, 'M', u'宅'), - (0xFA05, 'M', u'洞'), - (0xFA06, 'M', u'暴'), - (0xFA07, 'M', u'輻'), - (0xFA08, 'M', u'行'), - (0xFA09, 'M', u'降'), - (0xFA0A, 'M', u'見'), - (0xFA0B, 'M', u'廓'), - (0xFA0C, 'M', u'兀'), - (0xFA0D, 'M', u'嗀'), + (0xF9B3, 'M', '靈'), + (0xF9B4, 'M', '領'), + (0xF9B5, 'M', '例'), + (0xF9B6, 'M', '禮'), + (0xF9B7, 'M', '醴'), + (0xF9B8, 'M', '隸'), + (0xF9B9, 'M', '惡'), + (0xF9BA, 'M', '了'), + (0xF9BB, 'M', '僚'), + (0xF9BC, 'M', '寮'), + (0xF9BD, 'M', '尿'), + (0xF9BE, 'M', '料'), + (0xF9BF, 'M', '樂'), + (0xF9C0, 'M', '燎'), + (0xF9C1, 'M', '療'), + (0xF9C2, 'M', '蓼'), + (0xF9C3, 'M', '遼'), + (0xF9C4, 'M', '龍'), + (0xF9C5, 'M', '暈'), + (0xF9C6, 'M', '阮'), + (0xF9C7, 'M', '劉'), + (0xF9C8, 'M', '杻'), + (0xF9C9, 'M', '柳'), + (0xF9CA, 'M', '流'), + (0xF9CB, 'M', '溜'), + (0xF9CC, 'M', '琉'), + (0xF9CD, 'M', '留'), + (0xF9CE, 'M', '硫'), + (0xF9CF, 'M', '紐'), + (0xF9D0, 'M', '類'), + (0xF9D1, 'M', '六'), + (0xF9D2, 'M', '戮'), + (0xF9D3, 'M', '陸'), + (0xF9D4, 'M', '倫'), + (0xF9D5, 'M', '崙'), + (0xF9D6, 'M', '淪'), + (0xF9D7, 'M', '輪'), + (0xF9D8, 'M', '律'), + (0xF9D9, 'M', '慄'), + (0xF9DA, 'M', '栗'), + (0xF9DB, 'M', '率'), + (0xF9DC, 'M', '隆'), + (0xF9DD, 'M', '利'), + (0xF9DE, 'M', '吏'), + (0xF9DF, 'M', '履'), + (0xF9E0, 'M', '易'), + (0xF9E1, 'M', '李'), + (0xF9E2, 'M', '梨'), + (0xF9E3, 'M', '泥'), + (0xF9E4, 'M', '理'), + (0xF9E5, 'M', '痢'), + (0xF9E6, 'M', '罹'), + (0xF9E7, 'M', '裏'), + (0xF9E8, 'M', '裡'), + (0xF9E9, 'M', '里'), + (0xF9EA, 'M', '離'), + (0xF9EB, 'M', '匿'), + (0xF9EC, 'M', '溺'), + (0xF9ED, 'M', '吝'), + (0xF9EE, 'M', '燐'), + (0xF9EF, 'M', '璘'), + (0xF9F0, 'M', '藺'), + (0xF9F1, 'M', '隣'), + (0xF9F2, 'M', '鱗'), + (0xF9F3, 'M', '麟'), + (0xF9F4, 'M', '林'), + (0xF9F5, 'M', '淋'), + (0xF9F6, 'M', '臨'), + (0xF9F7, 'M', '立'), + (0xF9F8, 'M', '笠'), + (0xF9F9, 'M', '粒'), + (0xF9FA, 'M', '狀'), + (0xF9FB, 'M', '炙'), + (0xF9FC, 'M', '識'), + (0xF9FD, 'M', '什'), + (0xF9FE, 'M', '茶'), + (0xF9FF, 'M', '刺'), + (0xFA00, 'M', '切'), + (0xFA01, 'M', '度'), + (0xFA02, 'M', '拓'), + (0xFA03, 'M', '糖'), + (0xFA04, 'M', '宅'), + (0xFA05, 'M', '洞'), + (0xFA06, 'M', '暴'), + (0xFA07, 'M', '輻'), + (0xFA08, 'M', '行'), + (0xFA09, 'M', '降'), + (0xFA0A, 'M', '見'), + (0xFA0B, 'M', '廓'), + (0xFA0C, 'M', '兀'), + (0xFA0D, 'M', '嗀'), (0xFA0E, 'V'), - (0xFA10, 'M', u'塚'), + (0xFA10, 'M', '塚'), (0xFA11, 'V'), - (0xFA12, 'M', u'晴'), + (0xFA12, 'M', '晴'), (0xFA13, 'V'), - (0xFA15, 'M', u'凞'), - (0xFA16, 'M', u'猪'), - (0xFA17, 'M', u'益'), - (0xFA18, 'M', u'礼'), + (0xFA15, 'M', '凞'), + (0xFA16, 'M', '猪'), + (0xFA17, 'M', '益'), + (0xFA18, 'M', '礼'), ] def _seg_42(): return [ - (0xFA19, 'M', u'神'), - (0xFA1A, 'M', u'祥'), - (0xFA1B, 'M', u'福'), - (0xFA1C, 'M', u'靖'), - (0xFA1D, 'M', u'精'), - (0xFA1E, 'M', u'羽'), + (0xFA19, 'M', '神'), + (0xFA1A, 'M', '祥'), + (0xFA1B, 'M', '福'), + (0xFA1C, 'M', '靖'), + (0xFA1D, 'M', '精'), + (0xFA1E, 'M', '羽'), (0xFA1F, 'V'), - (0xFA20, 'M', u'蘒'), + (0xFA20, 'M', '蘒'), (0xFA21, 'V'), - (0xFA22, 'M', u'諸'), + (0xFA22, 'M', '諸'), (0xFA23, 'V'), - (0xFA25, 'M', u'逸'), - (0xFA26, 'M', u'都'), + (0xFA25, 'M', '逸'), + (0xFA26, 'M', '都'), (0xFA27, 'V'), - (0xFA2A, 'M', u'飯'), - (0xFA2B, 'M', u'飼'), - (0xFA2C, 'M', u'館'), - (0xFA2D, 'M', u'鶴'), - (0xFA2E, 'M', u'郞'), - (0xFA2F, 'M', u'隷'), - (0xFA30, 'M', u'侮'), - (0xFA31, 'M', u'僧'), - (0xFA32, 'M', u'免'), - (0xFA33, 'M', u'勉'), - (0xFA34, 'M', u'勤'), - (0xFA35, 'M', u'卑'), - (0xFA36, 'M', u'喝'), - (0xFA37, 'M', u'嘆'), - (0xFA38, 'M', u'器'), - (0xFA39, 'M', u'塀'), - (0xFA3A, 'M', u'墨'), - (0xFA3B, 'M', u'層'), - (0xFA3C, 'M', u'屮'), - (0xFA3D, 'M', u'悔'), - (0xFA3E, 'M', u'慨'), - (0xFA3F, 'M', u'憎'), - (0xFA40, 'M', u'懲'), - (0xFA41, 'M', u'敏'), - (0xFA42, 'M', u'既'), - (0xFA43, 'M', u'暑'), - (0xFA44, 'M', u'梅'), - (0xFA45, 'M', u'海'), - (0xFA46, 'M', u'渚'), - (0xFA47, 'M', u'漢'), - (0xFA48, 'M', u'煮'), - (0xFA49, 'M', u'爫'), - (0xFA4A, 'M', u'琢'), - (0xFA4B, 'M', u'碑'), - (0xFA4C, 'M', u'社'), - (0xFA4D, 'M', u'祉'), - (0xFA4E, 'M', u'祈'), - (0xFA4F, 'M', u'祐'), - (0xFA50, 'M', u'祖'), - (0xFA51, 'M', u'祝'), - (0xFA52, 'M', u'禍'), - (0xFA53, 'M', u'禎'), - (0xFA54, 'M', u'穀'), - (0xFA55, 'M', u'突'), - (0xFA56, 'M', u'節'), - (0xFA57, 'M', u'練'), - (0xFA58, 'M', u'縉'), - (0xFA59, 'M', u'繁'), - (0xFA5A, 'M', u'署'), - (0xFA5B, 'M', u'者'), - (0xFA5C, 'M', u'臭'), - (0xFA5D, 'M', u'艹'), - (0xFA5F, 'M', u'著'), - (0xFA60, 'M', u'褐'), - (0xFA61, 'M', u'視'), - (0xFA62, 'M', u'謁'), - (0xFA63, 'M', u'謹'), - (0xFA64, 'M', u'賓'), - (0xFA65, 'M', u'贈'), - (0xFA66, 'M', u'辶'), - (0xFA67, 'M', u'逸'), - (0xFA68, 'M', u'難'), - (0xFA69, 'M', u'響'), - (0xFA6A, 'M', u'頻'), - (0xFA6B, 'M', u'恵'), - (0xFA6C, 'M', u'𤋮'), - (0xFA6D, 'M', u'舘'), + (0xFA2A, 'M', '飯'), + (0xFA2B, 'M', '飼'), + (0xFA2C, 'M', '館'), + (0xFA2D, 'M', '鶴'), + (0xFA2E, 'M', '郞'), + (0xFA2F, 'M', '隷'), + (0xFA30, 'M', '侮'), + (0xFA31, 'M', '僧'), + (0xFA32, 'M', '免'), + (0xFA33, 'M', '勉'), + (0xFA34, 'M', '勤'), + (0xFA35, 'M', '卑'), + (0xFA36, 'M', '喝'), + (0xFA37, 'M', '嘆'), + (0xFA38, 'M', '器'), + (0xFA39, 'M', '塀'), + (0xFA3A, 'M', '墨'), + (0xFA3B, 'M', '層'), + (0xFA3C, 'M', '屮'), + (0xFA3D, 'M', '悔'), + (0xFA3E, 'M', '慨'), + (0xFA3F, 'M', '憎'), + (0xFA40, 'M', '懲'), + (0xFA41, 'M', '敏'), + (0xFA42, 'M', '既'), + (0xFA43, 'M', '暑'), + (0xFA44, 'M', '梅'), + (0xFA45, 'M', '海'), + (0xFA46, 'M', '渚'), + (0xFA47, 'M', '漢'), + (0xFA48, 'M', '煮'), + (0xFA49, 'M', '爫'), + (0xFA4A, 'M', '琢'), + (0xFA4B, 'M', '碑'), + (0xFA4C, 'M', '社'), + (0xFA4D, 'M', '祉'), + (0xFA4E, 'M', '祈'), + (0xFA4F, 'M', '祐'), + (0xFA50, 'M', '祖'), + (0xFA51, 'M', '祝'), + (0xFA52, 'M', '禍'), + (0xFA53, 'M', '禎'), + (0xFA54, 'M', '穀'), + (0xFA55, 'M', '突'), + (0xFA56, 'M', '節'), + (0xFA57, 'M', '練'), + (0xFA58, 'M', '縉'), + (0xFA59, 'M', '繁'), + (0xFA5A, 'M', '署'), + (0xFA5B, 'M', '者'), + (0xFA5C, 'M', '臭'), + (0xFA5D, 'M', '艹'), + (0xFA5F, 'M', '著'), + (0xFA60, 'M', '褐'), + (0xFA61, 'M', '視'), + (0xFA62, 'M', '謁'), + (0xFA63, 'M', '謹'), + (0xFA64, 'M', '賓'), + (0xFA65, 'M', '贈'), + (0xFA66, 'M', '辶'), + (0xFA67, 'M', '逸'), + (0xFA68, 'M', '難'), + (0xFA69, 'M', '響'), + (0xFA6A, 'M', '頻'), + (0xFA6B, 'M', '恵'), + (0xFA6C, 'M', '𤋮'), + (0xFA6D, 'M', '舘'), (0xFA6E, 'X'), - (0xFA70, 'M', u'並'), - (0xFA71, 'M', u'况'), - (0xFA72, 'M', u'全'), - (0xFA73, 'M', u'侀'), - (0xFA74, 'M', u'充'), - (0xFA75, 'M', u'冀'), - (0xFA76, 'M', u'勇'), - (0xFA77, 'M', u'勺'), - (0xFA78, 'M', u'喝'), - (0xFA79, 'M', u'啕'), - (0xFA7A, 'M', u'喙'), - (0xFA7B, 'M', u'嗢'), - (0xFA7C, 'M', u'塚'), - (0xFA7D, 'M', u'墳'), - (0xFA7E, 'M', u'奄'), - (0xFA7F, 'M', u'奔'), - (0xFA80, 'M', u'婢'), - (0xFA81, 'M', u'嬨'), + (0xFA70, 'M', '並'), + (0xFA71, 'M', '况'), + (0xFA72, 'M', '全'), + (0xFA73, 'M', '侀'), + (0xFA74, 'M', '充'), + (0xFA75, 'M', '冀'), + (0xFA76, 'M', '勇'), + (0xFA77, 'M', '勺'), + (0xFA78, 'M', '喝'), + (0xFA79, 'M', '啕'), + (0xFA7A, 'M', '喙'), + (0xFA7B, 'M', '嗢'), + (0xFA7C, 'M', '塚'), + (0xFA7D, 'M', '墳'), + (0xFA7E, 'M', '奄'), + (0xFA7F, 'M', '奔'), + (0xFA80, 'M', '婢'), + (0xFA81, 'M', '嬨'), ] def _seg_43(): return [ - (0xFA82, 'M', u'廒'), - (0xFA83, 'M', u'廙'), - (0xFA84, 'M', u'彩'), - (0xFA85, 'M', u'徭'), - (0xFA86, 'M', u'惘'), - (0xFA87, 'M', u'慎'), - (0xFA88, 'M', u'愈'), - (0xFA89, 'M', u'憎'), - (0xFA8A, 'M', u'慠'), - (0xFA8B, 'M', u'懲'), - (0xFA8C, 'M', u'戴'), - (0xFA8D, 'M', u'揄'), - (0xFA8E, 'M', u'搜'), - (0xFA8F, 'M', u'摒'), - (0xFA90, 'M', u'敖'), - (0xFA91, 'M', u'晴'), - (0xFA92, 'M', u'朗'), - (0xFA93, 'M', u'望'), - (0xFA94, 'M', u'杖'), - (0xFA95, 'M', u'歹'), - (0xFA96, 'M', u'殺'), - (0xFA97, 'M', u'流'), - (0xFA98, 'M', u'滛'), - (0xFA99, 'M', u'滋'), - (0xFA9A, 'M', u'漢'), - (0xFA9B, 'M', u'瀞'), - (0xFA9C, 'M', u'煮'), - (0xFA9D, 'M', u'瞧'), - (0xFA9E, 'M', u'爵'), - (0xFA9F, 'M', u'犯'), - (0xFAA0, 'M', u'猪'), - (0xFAA1, 'M', u'瑱'), - (0xFAA2, 'M', u'甆'), - (0xFAA3, 'M', u'画'), - (0xFAA4, 'M', u'瘝'), - (0xFAA5, 'M', u'瘟'), - (0xFAA6, 'M', u'益'), - (0xFAA7, 'M', u'盛'), - (0xFAA8, 'M', u'直'), - (0xFAA9, 'M', u'睊'), - (0xFAAA, 'M', u'着'), - (0xFAAB, 'M', u'磌'), - (0xFAAC, 'M', u'窱'), - (0xFAAD, 'M', u'節'), - (0xFAAE, 'M', u'类'), - (0xFAAF, 'M', u'絛'), - (0xFAB0, 'M', u'練'), - (0xFAB1, 'M', u'缾'), - (0xFAB2, 'M', u'者'), - (0xFAB3, 'M', u'荒'), - (0xFAB4, 'M', u'華'), - (0xFAB5, 'M', u'蝹'), - (0xFAB6, 'M', u'襁'), - (0xFAB7, 'M', u'覆'), - (0xFAB8, 'M', u'視'), - (0xFAB9, 'M', u'調'), - (0xFABA, 'M', u'諸'), - (0xFABB, 'M', u'請'), - (0xFABC, 'M', u'謁'), - (0xFABD, 'M', u'諾'), - (0xFABE, 'M', u'諭'), - (0xFABF, 'M', u'謹'), - (0xFAC0, 'M', u'變'), - (0xFAC1, 'M', u'贈'), - (0xFAC2, 'M', u'輸'), - (0xFAC3, 'M', u'遲'), - (0xFAC4, 'M', u'醙'), - (0xFAC5, 'M', u'鉶'), - (0xFAC6, 'M', u'陼'), - (0xFAC7, 'M', u'難'), - (0xFAC8, 'M', u'靖'), - (0xFAC9, 'M', u'韛'), - (0xFACA, 'M', u'響'), - (0xFACB, 'M', u'頋'), - (0xFACC, 'M', u'頻'), - (0xFACD, 'M', u'鬒'), - (0xFACE, 'M', u'龜'), - (0xFACF, 'M', u'𢡊'), - (0xFAD0, 'M', u'𢡄'), - (0xFAD1, 'M', u'𣏕'), - (0xFAD2, 'M', u'㮝'), - (0xFAD3, 'M', u'䀘'), - (0xFAD4, 'M', u'䀹'), - (0xFAD5, 'M', u'𥉉'), - (0xFAD6, 'M', u'𥳐'), - (0xFAD7, 'M', u'𧻓'), - (0xFAD8, 'M', u'齃'), - (0xFAD9, 'M', u'龎'), + (0xFA82, 'M', '廒'), + (0xFA83, 'M', '廙'), + (0xFA84, 'M', '彩'), + (0xFA85, 'M', '徭'), + (0xFA86, 'M', '惘'), + (0xFA87, 'M', '慎'), + (0xFA88, 'M', '愈'), + (0xFA89, 'M', '憎'), + (0xFA8A, 'M', '慠'), + (0xFA8B, 'M', '懲'), + (0xFA8C, 'M', '戴'), + (0xFA8D, 'M', '揄'), + (0xFA8E, 'M', '搜'), + (0xFA8F, 'M', '摒'), + (0xFA90, 'M', '敖'), + (0xFA91, 'M', '晴'), + (0xFA92, 'M', '朗'), + (0xFA93, 'M', '望'), + (0xFA94, 'M', '杖'), + (0xFA95, 'M', '歹'), + (0xFA96, 'M', '殺'), + (0xFA97, 'M', '流'), + (0xFA98, 'M', '滛'), + (0xFA99, 'M', '滋'), + (0xFA9A, 'M', '漢'), + (0xFA9B, 'M', '瀞'), + (0xFA9C, 'M', '煮'), + (0xFA9D, 'M', '瞧'), + (0xFA9E, 'M', '爵'), + (0xFA9F, 'M', '犯'), + (0xFAA0, 'M', '猪'), + (0xFAA1, 'M', '瑱'), + (0xFAA2, 'M', '甆'), + (0xFAA3, 'M', '画'), + (0xFAA4, 'M', '瘝'), + (0xFAA5, 'M', '瘟'), + (0xFAA6, 'M', '益'), + (0xFAA7, 'M', '盛'), + (0xFAA8, 'M', '直'), + (0xFAA9, 'M', '睊'), + (0xFAAA, 'M', '着'), + (0xFAAB, 'M', '磌'), + (0xFAAC, 'M', '窱'), + (0xFAAD, 'M', '節'), + (0xFAAE, 'M', '类'), + (0xFAAF, 'M', '絛'), + (0xFAB0, 'M', '練'), + (0xFAB1, 'M', '缾'), + (0xFAB2, 'M', '者'), + (0xFAB3, 'M', '荒'), + (0xFAB4, 'M', '華'), + (0xFAB5, 'M', '蝹'), + (0xFAB6, 'M', '襁'), + (0xFAB7, 'M', '覆'), + (0xFAB8, 'M', '視'), + (0xFAB9, 'M', '調'), + (0xFABA, 'M', '諸'), + (0xFABB, 'M', '請'), + (0xFABC, 'M', '謁'), + (0xFABD, 'M', '諾'), + (0xFABE, 'M', '諭'), + (0xFABF, 'M', '謹'), + (0xFAC0, 'M', '變'), + (0xFAC1, 'M', '贈'), + (0xFAC2, 'M', '輸'), + (0xFAC3, 'M', '遲'), + (0xFAC4, 'M', '醙'), + (0xFAC5, 'M', '鉶'), + (0xFAC6, 'M', '陼'), + (0xFAC7, 'M', '難'), + (0xFAC8, 'M', '靖'), + (0xFAC9, 'M', '韛'), + (0xFACA, 'M', '響'), + (0xFACB, 'M', '頋'), + (0xFACC, 'M', '頻'), + (0xFACD, 'M', '鬒'), + (0xFACE, 'M', '龜'), + (0xFACF, 'M', '𢡊'), + (0xFAD0, 'M', '𢡄'), + (0xFAD1, 'M', '𣏕'), + (0xFAD2, 'M', '㮝'), + (0xFAD3, 'M', '䀘'), + (0xFAD4, 'M', '䀹'), + (0xFAD5, 'M', '𥉉'), + (0xFAD6, 'M', '𥳐'), + (0xFAD7, 'M', '𧻓'), + (0xFAD8, 'M', '齃'), + (0xFAD9, 'M', '龎'), (0xFADA, 'X'), - (0xFB00, 'M', u'ff'), - (0xFB01, 'M', u'fi'), - (0xFB02, 'M', u'fl'), - (0xFB03, 'M', u'ffi'), - (0xFB04, 'M', u'ffl'), - (0xFB05, 'M', u'st'), + (0xFB00, 'M', 'ff'), + (0xFB01, 'M', 'fi'), + (0xFB02, 'M', 'fl'), + (0xFB03, 'M', 'ffi'), + (0xFB04, 'M', 'ffl'), + (0xFB05, 'M', 'st'), (0xFB07, 'X'), - (0xFB13, 'M', u'մն'), - (0xFB14, 'M', u'մե'), - (0xFB15, 'M', u'մի'), - (0xFB16, 'M', u'վն'), + (0xFB13, 'M', 'մն'), + (0xFB14, 'M', 'մե'), + (0xFB15, 'M', 'մի'), + (0xFB16, 'M', 'վն'), ] def _seg_44(): return [ - (0xFB17, 'M', u'մխ'), + (0xFB17, 'M', 'մխ'), (0xFB18, 'X'), - (0xFB1D, 'M', u'יִ'), + (0xFB1D, 'M', 'יִ'), (0xFB1E, 'V'), - (0xFB1F, 'M', u'ײַ'), - (0xFB20, 'M', u'ע'), - (0xFB21, 'M', u'א'), - (0xFB22, 'M', u'ד'), - (0xFB23, 'M', u'ה'), - (0xFB24, 'M', u'כ'), - (0xFB25, 'M', u'ל'), - (0xFB26, 'M', u'ם'), - (0xFB27, 'M', u'ר'), - (0xFB28, 'M', u'ת'), - (0xFB29, '3', u'+'), - (0xFB2A, 'M', u'שׁ'), - (0xFB2B, 'M', u'שׂ'), - (0xFB2C, 'M', u'שּׁ'), - (0xFB2D, 'M', u'שּׂ'), - (0xFB2E, 'M', u'אַ'), - (0xFB2F, 'M', u'אָ'), - (0xFB30, 'M', u'אּ'), - (0xFB31, 'M', u'בּ'), - (0xFB32, 'M', u'גּ'), - (0xFB33, 'M', u'דּ'), - (0xFB34, 'M', u'הּ'), - (0xFB35, 'M', u'וּ'), - (0xFB36, 'M', u'זּ'), + (0xFB1F, 'M', 'ײַ'), + (0xFB20, 'M', 'ע'), + (0xFB21, 'M', 'א'), + (0xFB22, 'M', 'ד'), + (0xFB23, 'M', 'ה'), + (0xFB24, 'M', 'כ'), + (0xFB25, 'M', 'ל'), + (0xFB26, 'M', 'ם'), + (0xFB27, 'M', 'ר'), + (0xFB28, 'M', 'ת'), + (0xFB29, '3', '+'), + (0xFB2A, 'M', 'שׁ'), + (0xFB2B, 'M', 'שׂ'), + (0xFB2C, 'M', 'שּׁ'), + (0xFB2D, 'M', 'שּׂ'), + (0xFB2E, 'M', 'אַ'), + (0xFB2F, 'M', 'אָ'), + (0xFB30, 'M', 'אּ'), + (0xFB31, 'M', 'בּ'), + (0xFB32, 'M', 'גּ'), + (0xFB33, 'M', 'דּ'), + (0xFB34, 'M', 'הּ'), + (0xFB35, 'M', 'וּ'), + (0xFB36, 'M', 'זּ'), (0xFB37, 'X'), - (0xFB38, 'M', u'טּ'), - (0xFB39, 'M', u'יּ'), - (0xFB3A, 'M', u'ךּ'), - (0xFB3B, 'M', u'כּ'), - (0xFB3C, 'M', u'לּ'), + (0xFB38, 'M', 'טּ'), + (0xFB39, 'M', 'יּ'), + (0xFB3A, 'M', 'ךּ'), + (0xFB3B, 'M', 'כּ'), + (0xFB3C, 'M', 'לּ'), (0xFB3D, 'X'), - (0xFB3E, 'M', u'מּ'), + (0xFB3E, 'M', 'מּ'), (0xFB3F, 'X'), - (0xFB40, 'M', u'נּ'), - (0xFB41, 'M', u'סּ'), + (0xFB40, 'M', 'נּ'), + (0xFB41, 'M', 'סּ'), (0xFB42, 'X'), - (0xFB43, 'M', u'ףּ'), - (0xFB44, 'M', u'פּ'), + (0xFB43, 'M', 'ףּ'), + (0xFB44, 'M', 'פּ'), (0xFB45, 'X'), - (0xFB46, 'M', u'צּ'), - (0xFB47, 'M', u'קּ'), - (0xFB48, 'M', u'רּ'), - (0xFB49, 'M', u'שּ'), - (0xFB4A, 'M', u'תּ'), - (0xFB4B, 'M', u'וֹ'), - (0xFB4C, 'M', u'בֿ'), - (0xFB4D, 'M', u'כֿ'), - (0xFB4E, 'M', u'פֿ'), - (0xFB4F, 'M', u'אל'), - (0xFB50, 'M', u'ٱ'), - (0xFB52, 'M', u'ٻ'), - (0xFB56, 'M', u'پ'), - (0xFB5A, 'M', u'ڀ'), - (0xFB5E, 'M', u'ٺ'), - (0xFB62, 'M', u'ٿ'), - (0xFB66, 'M', u'ٹ'), - (0xFB6A, 'M', u'ڤ'), - (0xFB6E, 'M', u'ڦ'), - (0xFB72, 'M', u'ڄ'), - (0xFB76, 'M', u'ڃ'), - (0xFB7A, 'M', u'چ'), - (0xFB7E, 'M', u'ڇ'), - (0xFB82, 'M', u'ڍ'), - (0xFB84, 'M', u'ڌ'), - (0xFB86, 'M', u'ڎ'), - (0xFB88, 'M', u'ڈ'), - (0xFB8A, 'M', u'ژ'), - (0xFB8C, 'M', u'ڑ'), - (0xFB8E, 'M', u'ک'), - (0xFB92, 'M', u'گ'), - (0xFB96, 'M', u'ڳ'), - (0xFB9A, 'M', u'ڱ'), - (0xFB9E, 'M', u'ں'), - (0xFBA0, 'M', u'ڻ'), - (0xFBA4, 'M', u'ۀ'), - (0xFBA6, 'M', u'ہ'), - (0xFBAA, 'M', u'ھ'), - (0xFBAE, 'M', u'ے'), - (0xFBB0, 'M', u'ۓ'), + (0xFB46, 'M', 'צּ'), + (0xFB47, 'M', 'קּ'), + (0xFB48, 'M', 'רּ'), + (0xFB49, 'M', 'שּ'), + (0xFB4A, 'M', 'תּ'), + (0xFB4B, 'M', 'וֹ'), + (0xFB4C, 'M', 'בֿ'), + (0xFB4D, 'M', 'כֿ'), + (0xFB4E, 'M', 'פֿ'), + (0xFB4F, 'M', 'אל'), + (0xFB50, 'M', 'ٱ'), + (0xFB52, 'M', 'ٻ'), + (0xFB56, 'M', 'پ'), + (0xFB5A, 'M', 'ڀ'), + (0xFB5E, 'M', 'ٺ'), + (0xFB62, 'M', 'ٿ'), + (0xFB66, 'M', 'ٹ'), + (0xFB6A, 'M', 'ڤ'), + (0xFB6E, 'M', 'ڦ'), + (0xFB72, 'M', 'ڄ'), + (0xFB76, 'M', 'ڃ'), + (0xFB7A, 'M', 'چ'), + (0xFB7E, 'M', 'ڇ'), + (0xFB82, 'M', 'ڍ'), + (0xFB84, 'M', 'ڌ'), + (0xFB86, 'M', 'ڎ'), + (0xFB88, 'M', 'ڈ'), + (0xFB8A, 'M', 'ژ'), + (0xFB8C, 'M', 'ڑ'), + (0xFB8E, 'M', 'ک'), + (0xFB92, 'M', 'گ'), + (0xFB96, 'M', 'ڳ'), + (0xFB9A, 'M', 'ڱ'), + (0xFB9E, 'M', 'ں'), + (0xFBA0, 'M', 'ڻ'), + (0xFBA4, 'M', 'ۀ'), + (0xFBA6, 'M', 'ہ'), + (0xFBAA, 'M', 'ھ'), + (0xFBAE, 'M', 'ے'), + (0xFBB0, 'M', 'ۓ'), (0xFBB2, 'V'), (0xFBC2, 'X'), - (0xFBD3, 'M', u'ڭ'), - (0xFBD7, 'M', u'ۇ'), - (0xFBD9, 'M', u'ۆ'), - (0xFBDB, 'M', u'ۈ'), - (0xFBDD, 'M', u'ۇٴ'), - (0xFBDE, 'M', u'ۋ'), - (0xFBE0, 'M', u'ۅ'), - (0xFBE2, 'M', u'ۉ'), - (0xFBE4, 'M', u'ې'), - (0xFBE8, 'M', u'ى'), - (0xFBEA, 'M', u'ئا'), - (0xFBEC, 'M', u'ئە'), - (0xFBEE, 'M', u'ئو'), - (0xFBF0, 'M', u'ئۇ'), - (0xFBF2, 'M', u'ئۆ'), + (0xFBD3, 'M', 'ڭ'), + (0xFBD7, 'M', 'ۇ'), + (0xFBD9, 'M', 'ۆ'), + (0xFBDB, 'M', 'ۈ'), + (0xFBDD, 'M', 'ۇٴ'), + (0xFBDE, 'M', 'ۋ'), + (0xFBE0, 'M', 'ۅ'), + (0xFBE2, 'M', 'ۉ'), + (0xFBE4, 'M', 'ې'), + (0xFBE8, 'M', 'ى'), + (0xFBEA, 'M', 'ئا'), + (0xFBEC, 'M', 'ئە'), + (0xFBEE, 'M', 'ئو'), + (0xFBF0, 'M', 'ئۇ'), + (0xFBF2, 'M', 'ئۆ'), ] def _seg_45(): return [ - (0xFBF4, 'M', u'ئۈ'), - (0xFBF6, 'M', u'ئې'), - (0xFBF9, 'M', u'ئى'), - (0xFBFC, 'M', u'ی'), - (0xFC00, 'M', u'ئج'), - (0xFC01, 'M', u'ئح'), - (0xFC02, 'M', u'ئم'), - (0xFC03, 'M', u'ئى'), - (0xFC04, 'M', u'ئي'), - (0xFC05, 'M', u'بج'), - (0xFC06, 'M', u'بح'), - (0xFC07, 'M', u'بخ'), - (0xFC08, 'M', u'بم'), - (0xFC09, 'M', u'بى'), - (0xFC0A, 'M', u'بي'), - (0xFC0B, 'M', u'تج'), - (0xFC0C, 'M', u'تح'), - (0xFC0D, 'M', u'تخ'), - (0xFC0E, 'M', u'تم'), - (0xFC0F, 'M', u'تى'), - (0xFC10, 'M', u'تي'), - (0xFC11, 'M', u'ثج'), - (0xFC12, 'M', u'ثم'), - (0xFC13, 'M', u'ثى'), - (0xFC14, 'M', u'ثي'), - (0xFC15, 'M', u'جح'), - (0xFC16, 'M', u'جم'), - (0xFC17, 'M', u'حج'), - (0xFC18, 'M', u'حم'), - (0xFC19, 'M', u'خج'), - (0xFC1A, 'M', u'خح'), - (0xFC1B, 'M', u'خم'), - (0xFC1C, 'M', u'سج'), - (0xFC1D, 'M', u'سح'), - (0xFC1E, 'M', u'سخ'), - (0xFC1F, 'M', u'سم'), - (0xFC20, 'M', u'صح'), - (0xFC21, 'M', u'صم'), - (0xFC22, 'M', u'ضج'), - (0xFC23, 'M', u'ضح'), - (0xFC24, 'M', u'ضخ'), - (0xFC25, 'M', u'ضم'), - (0xFC26, 'M', u'طح'), - (0xFC27, 'M', u'طم'), - (0xFC28, 'M', u'ظم'), - (0xFC29, 'M', u'عج'), - (0xFC2A, 'M', u'عم'), - (0xFC2B, 'M', u'غج'), - (0xFC2C, 'M', u'غم'), - (0xFC2D, 'M', u'فج'), - (0xFC2E, 'M', u'فح'), - (0xFC2F, 'M', u'فخ'), - (0xFC30, 'M', u'فم'), - (0xFC31, 'M', u'فى'), - (0xFC32, 'M', u'في'), - (0xFC33, 'M', u'قح'), - (0xFC34, 'M', u'قم'), - (0xFC35, 'M', u'قى'), - (0xFC36, 'M', u'قي'), - (0xFC37, 'M', u'كا'), - (0xFC38, 'M', u'كج'), - (0xFC39, 'M', u'كح'), - (0xFC3A, 'M', u'كخ'), - (0xFC3B, 'M', u'كل'), - (0xFC3C, 'M', u'كم'), - (0xFC3D, 'M', u'كى'), - (0xFC3E, 'M', u'كي'), - (0xFC3F, 'M', u'لج'), - (0xFC40, 'M', u'لح'), - (0xFC41, 'M', u'لخ'), - (0xFC42, 'M', u'لم'), - (0xFC43, 'M', u'لى'), - (0xFC44, 'M', u'لي'), - (0xFC45, 'M', u'مج'), - (0xFC46, 'M', u'مح'), - (0xFC47, 'M', u'مخ'), - (0xFC48, 'M', u'مم'), - (0xFC49, 'M', u'مى'), - (0xFC4A, 'M', u'مي'), - (0xFC4B, 'M', u'نج'), - (0xFC4C, 'M', u'نح'), - (0xFC4D, 'M', u'نخ'), - (0xFC4E, 'M', u'نم'), - (0xFC4F, 'M', u'نى'), - (0xFC50, 'M', u'ني'), - (0xFC51, 'M', u'هج'), - (0xFC52, 'M', u'هم'), - (0xFC53, 'M', u'هى'), - (0xFC54, 'M', u'هي'), - (0xFC55, 'M', u'يج'), - (0xFC56, 'M', u'يح'), - (0xFC57, 'M', u'يخ'), - (0xFC58, 'M', u'يم'), - (0xFC59, 'M', u'يى'), - (0xFC5A, 'M', u'يي'), - (0xFC5B, 'M', u'ذٰ'), - (0xFC5C, 'M', u'رٰ'), - (0xFC5D, 'M', u'ىٰ'), - (0xFC5E, '3', u' ٌّ'), - (0xFC5F, '3', u' ٍّ'), + (0xFBF4, 'M', 'ئۈ'), + (0xFBF6, 'M', 'ئې'), + (0xFBF9, 'M', 'ئى'), + (0xFBFC, 'M', 'ی'), + (0xFC00, 'M', 'ئج'), + (0xFC01, 'M', 'ئح'), + (0xFC02, 'M', 'ئم'), + (0xFC03, 'M', 'ئى'), + (0xFC04, 'M', 'ئي'), + (0xFC05, 'M', 'بج'), + (0xFC06, 'M', 'بح'), + (0xFC07, 'M', 'بخ'), + (0xFC08, 'M', 'بم'), + (0xFC09, 'M', 'بى'), + (0xFC0A, 'M', 'بي'), + (0xFC0B, 'M', 'تج'), + (0xFC0C, 'M', 'تح'), + (0xFC0D, 'M', 'تخ'), + (0xFC0E, 'M', 'تم'), + (0xFC0F, 'M', 'تى'), + (0xFC10, 'M', 'تي'), + (0xFC11, 'M', 'ثج'), + (0xFC12, 'M', 'ثم'), + (0xFC13, 'M', 'ثى'), + (0xFC14, 'M', 'ثي'), + (0xFC15, 'M', 'جح'), + (0xFC16, 'M', 'جم'), + (0xFC17, 'M', 'حج'), + (0xFC18, 'M', 'حم'), + (0xFC19, 'M', 'خج'), + (0xFC1A, 'M', 'خح'), + (0xFC1B, 'M', 'خم'), + (0xFC1C, 'M', 'سج'), + (0xFC1D, 'M', 'سح'), + (0xFC1E, 'M', 'سخ'), + (0xFC1F, 'M', 'سم'), + (0xFC20, 'M', 'صح'), + (0xFC21, 'M', 'صم'), + (0xFC22, 'M', 'ضج'), + (0xFC23, 'M', 'ضح'), + (0xFC24, 'M', 'ضخ'), + (0xFC25, 'M', 'ضم'), + (0xFC26, 'M', 'طح'), + (0xFC27, 'M', 'طم'), + (0xFC28, 'M', 'ظم'), + (0xFC29, 'M', 'عج'), + (0xFC2A, 'M', 'عم'), + (0xFC2B, 'M', 'غج'), + (0xFC2C, 'M', 'غم'), + (0xFC2D, 'M', 'فج'), + (0xFC2E, 'M', 'فح'), + (0xFC2F, 'M', 'فخ'), + (0xFC30, 'M', 'فم'), + (0xFC31, 'M', 'فى'), + (0xFC32, 'M', 'في'), + (0xFC33, 'M', 'قح'), + (0xFC34, 'M', 'قم'), + (0xFC35, 'M', 'قى'), + (0xFC36, 'M', 'قي'), + (0xFC37, 'M', 'كا'), + (0xFC38, 'M', 'كج'), + (0xFC39, 'M', 'كح'), + (0xFC3A, 'M', 'كخ'), + (0xFC3B, 'M', 'كل'), + (0xFC3C, 'M', 'كم'), + (0xFC3D, 'M', 'كى'), + (0xFC3E, 'M', 'كي'), + (0xFC3F, 'M', 'لج'), + (0xFC40, 'M', 'لح'), + (0xFC41, 'M', 'لخ'), + (0xFC42, 'M', 'لم'), + (0xFC43, 'M', 'لى'), + (0xFC44, 'M', 'لي'), + (0xFC45, 'M', 'مج'), + (0xFC46, 'M', 'مح'), + (0xFC47, 'M', 'مخ'), + (0xFC48, 'M', 'مم'), + (0xFC49, 'M', 'مى'), + (0xFC4A, 'M', 'مي'), + (0xFC4B, 'M', 'نج'), + (0xFC4C, 'M', 'نح'), + (0xFC4D, 'M', 'نخ'), + (0xFC4E, 'M', 'نم'), + (0xFC4F, 'M', 'نى'), + (0xFC50, 'M', 'ني'), + (0xFC51, 'M', 'هج'), + (0xFC52, 'M', 'هم'), + (0xFC53, 'M', 'هى'), + (0xFC54, 'M', 'هي'), + (0xFC55, 'M', 'يج'), + (0xFC56, 'M', 'يح'), + (0xFC57, 'M', 'يخ'), + (0xFC58, 'M', 'يم'), + (0xFC59, 'M', 'يى'), + (0xFC5A, 'M', 'يي'), + (0xFC5B, 'M', 'ذٰ'), + (0xFC5C, 'M', 'رٰ'), + (0xFC5D, 'M', 'ىٰ'), + (0xFC5E, '3', ' ٌّ'), + (0xFC5F, '3', ' ٍّ'), ] def _seg_46(): return [ - (0xFC60, '3', u' َّ'), - (0xFC61, '3', u' ُّ'), - (0xFC62, '3', u' ِّ'), - (0xFC63, '3', u' ّٰ'), - (0xFC64, 'M', u'ئر'), - (0xFC65, 'M', u'ئز'), - (0xFC66, 'M', u'ئم'), - (0xFC67, 'M', u'ئن'), - (0xFC68, 'M', u'ئى'), - (0xFC69, 'M', u'ئي'), - (0xFC6A, 'M', u'بر'), - (0xFC6B, 'M', u'بز'), - (0xFC6C, 'M', u'بم'), - (0xFC6D, 'M', u'بن'), - (0xFC6E, 'M', u'بى'), - (0xFC6F, 'M', u'بي'), - (0xFC70, 'M', u'تر'), - (0xFC71, 'M', u'تز'), - (0xFC72, 'M', u'تم'), - (0xFC73, 'M', u'تن'), - (0xFC74, 'M', u'تى'), - (0xFC75, 'M', u'تي'), - (0xFC76, 'M', u'ثر'), - (0xFC77, 'M', u'ثز'), - (0xFC78, 'M', u'ثم'), - (0xFC79, 'M', u'ثن'), - (0xFC7A, 'M', u'ثى'), - (0xFC7B, 'M', u'ثي'), - (0xFC7C, 'M', u'فى'), - (0xFC7D, 'M', u'في'), - (0xFC7E, 'M', u'قى'), - (0xFC7F, 'M', u'قي'), - (0xFC80, 'M', u'كا'), - (0xFC81, 'M', u'كل'), - (0xFC82, 'M', u'كم'), - (0xFC83, 'M', u'كى'), - (0xFC84, 'M', u'كي'), - (0xFC85, 'M', u'لم'), - (0xFC86, 'M', u'لى'), - (0xFC87, 'M', u'لي'), - (0xFC88, 'M', u'ما'), - (0xFC89, 'M', u'مم'), - (0xFC8A, 'M', u'نر'), - (0xFC8B, 'M', u'نز'), - (0xFC8C, 'M', u'نم'), - (0xFC8D, 'M', u'نن'), - (0xFC8E, 'M', u'نى'), - (0xFC8F, 'M', u'ني'), - (0xFC90, 'M', u'ىٰ'), - (0xFC91, 'M', u'ير'), - (0xFC92, 'M', u'يز'), - (0xFC93, 'M', u'يم'), - (0xFC94, 'M', u'ين'), - (0xFC95, 'M', u'يى'), - (0xFC96, 'M', u'يي'), - (0xFC97, 'M', u'ئج'), - (0xFC98, 'M', u'ئح'), - (0xFC99, 'M', u'ئخ'), - (0xFC9A, 'M', u'ئم'), - (0xFC9B, 'M', u'ئه'), - (0xFC9C, 'M', u'بج'), - (0xFC9D, 'M', u'بح'), - (0xFC9E, 'M', u'بخ'), - (0xFC9F, 'M', u'بم'), - (0xFCA0, 'M', u'به'), - (0xFCA1, 'M', u'تج'), - (0xFCA2, 'M', u'تح'), - (0xFCA3, 'M', u'تخ'), - (0xFCA4, 'M', u'تم'), - (0xFCA5, 'M', u'ته'), - (0xFCA6, 'M', u'ثم'), - (0xFCA7, 'M', u'جح'), - (0xFCA8, 'M', u'جم'), - (0xFCA9, 'M', u'حج'), - (0xFCAA, 'M', u'حم'), - (0xFCAB, 'M', u'خج'), - (0xFCAC, 'M', u'خم'), - (0xFCAD, 'M', u'سج'), - (0xFCAE, 'M', u'سح'), - (0xFCAF, 'M', u'سخ'), - (0xFCB0, 'M', u'سم'), - (0xFCB1, 'M', u'صح'), - (0xFCB2, 'M', u'صخ'), - (0xFCB3, 'M', u'صم'), - (0xFCB4, 'M', u'ضج'), - (0xFCB5, 'M', u'ضح'), - (0xFCB6, 'M', u'ضخ'), - (0xFCB7, 'M', u'ضم'), - (0xFCB8, 'M', u'طح'), - (0xFCB9, 'M', u'ظم'), - (0xFCBA, 'M', u'عج'), - (0xFCBB, 'M', u'عم'), - (0xFCBC, 'M', u'غج'), - (0xFCBD, 'M', u'غم'), - (0xFCBE, 'M', u'فج'), - (0xFCBF, 'M', u'فح'), - (0xFCC0, 'M', u'فخ'), - (0xFCC1, 'M', u'فم'), - (0xFCC2, 'M', u'قح'), - (0xFCC3, 'M', u'قم'), + (0xFC60, '3', ' َّ'), + (0xFC61, '3', ' ُّ'), + (0xFC62, '3', ' ِّ'), + (0xFC63, '3', ' ّٰ'), + (0xFC64, 'M', 'ئر'), + (0xFC65, 'M', 'ئز'), + (0xFC66, 'M', 'ئم'), + (0xFC67, 'M', 'ئن'), + (0xFC68, 'M', 'ئى'), + (0xFC69, 'M', 'ئي'), + (0xFC6A, 'M', 'بر'), + (0xFC6B, 'M', 'بز'), + (0xFC6C, 'M', 'بم'), + (0xFC6D, 'M', 'بن'), + (0xFC6E, 'M', 'بى'), + (0xFC6F, 'M', 'بي'), + (0xFC70, 'M', 'تر'), + (0xFC71, 'M', 'تز'), + (0xFC72, 'M', 'تم'), + (0xFC73, 'M', 'تن'), + (0xFC74, 'M', 'تى'), + (0xFC75, 'M', 'تي'), + (0xFC76, 'M', 'ثر'), + (0xFC77, 'M', 'ثز'), + (0xFC78, 'M', 'ثم'), + (0xFC79, 'M', 'ثن'), + (0xFC7A, 'M', 'ثى'), + (0xFC7B, 'M', 'ثي'), + (0xFC7C, 'M', 'فى'), + (0xFC7D, 'M', 'في'), + (0xFC7E, 'M', 'قى'), + (0xFC7F, 'M', 'قي'), + (0xFC80, 'M', 'كا'), + (0xFC81, 'M', 'كل'), + (0xFC82, 'M', 'كم'), + (0xFC83, 'M', 'كى'), + (0xFC84, 'M', 'كي'), + (0xFC85, 'M', 'لم'), + (0xFC86, 'M', 'لى'), + (0xFC87, 'M', 'لي'), + (0xFC88, 'M', 'ما'), + (0xFC89, 'M', 'مم'), + (0xFC8A, 'M', 'نر'), + (0xFC8B, 'M', 'نز'), + (0xFC8C, 'M', 'نم'), + (0xFC8D, 'M', 'نن'), + (0xFC8E, 'M', 'نى'), + (0xFC8F, 'M', 'ني'), + (0xFC90, 'M', 'ىٰ'), + (0xFC91, 'M', 'ير'), + (0xFC92, 'M', 'يز'), + (0xFC93, 'M', 'يم'), + (0xFC94, 'M', 'ين'), + (0xFC95, 'M', 'يى'), + (0xFC96, 'M', 'يي'), + (0xFC97, 'M', 'ئج'), + (0xFC98, 'M', 'ئح'), + (0xFC99, 'M', 'ئخ'), + (0xFC9A, 'M', 'ئم'), + (0xFC9B, 'M', 'ئه'), + (0xFC9C, 'M', 'بج'), + (0xFC9D, 'M', 'بح'), + (0xFC9E, 'M', 'بخ'), + (0xFC9F, 'M', 'بم'), + (0xFCA0, 'M', 'به'), + (0xFCA1, 'M', 'تج'), + (0xFCA2, 'M', 'تح'), + (0xFCA3, 'M', 'تخ'), + (0xFCA4, 'M', 'تم'), + (0xFCA5, 'M', 'ته'), + (0xFCA6, 'M', 'ثم'), + (0xFCA7, 'M', 'جح'), + (0xFCA8, 'M', 'جم'), + (0xFCA9, 'M', 'حج'), + (0xFCAA, 'M', 'حم'), + (0xFCAB, 'M', 'خج'), + (0xFCAC, 'M', 'خم'), + (0xFCAD, 'M', 'سج'), + (0xFCAE, 'M', 'سح'), + (0xFCAF, 'M', 'سخ'), + (0xFCB0, 'M', 'سم'), + (0xFCB1, 'M', 'صح'), + (0xFCB2, 'M', 'صخ'), + (0xFCB3, 'M', 'صم'), + (0xFCB4, 'M', 'ضج'), + (0xFCB5, 'M', 'ضح'), + (0xFCB6, 'M', 'ضخ'), + (0xFCB7, 'M', 'ضم'), + (0xFCB8, 'M', 'طح'), + (0xFCB9, 'M', 'ظم'), + (0xFCBA, 'M', 'عج'), + (0xFCBB, 'M', 'عم'), + (0xFCBC, 'M', 'غج'), + (0xFCBD, 'M', 'غم'), + (0xFCBE, 'M', 'فج'), + (0xFCBF, 'M', 'فح'), + (0xFCC0, 'M', 'فخ'), + (0xFCC1, 'M', 'فم'), + (0xFCC2, 'M', 'قح'), + (0xFCC3, 'M', 'قم'), ] def _seg_47(): return [ - (0xFCC4, 'M', u'كج'), - (0xFCC5, 'M', u'كح'), - (0xFCC6, 'M', u'كخ'), - (0xFCC7, 'M', u'كل'), - (0xFCC8, 'M', u'كم'), - (0xFCC9, 'M', u'لج'), - (0xFCCA, 'M', u'لح'), - (0xFCCB, 'M', u'لخ'), - (0xFCCC, 'M', u'لم'), - (0xFCCD, 'M', u'له'), - (0xFCCE, 'M', u'مج'), - (0xFCCF, 'M', u'مح'), - (0xFCD0, 'M', u'مخ'), - (0xFCD1, 'M', u'مم'), - (0xFCD2, 'M', u'نج'), - (0xFCD3, 'M', u'نح'), - (0xFCD4, 'M', u'نخ'), - (0xFCD5, 'M', u'نم'), - (0xFCD6, 'M', u'نه'), - (0xFCD7, 'M', u'هج'), - (0xFCD8, 'M', u'هم'), - (0xFCD9, 'M', u'هٰ'), - (0xFCDA, 'M', u'يج'), - (0xFCDB, 'M', u'يح'), - (0xFCDC, 'M', u'يخ'), - (0xFCDD, 'M', u'يم'), - (0xFCDE, 'M', u'يه'), - (0xFCDF, 'M', u'ئم'), - (0xFCE0, 'M', u'ئه'), - (0xFCE1, 'M', u'بم'), - (0xFCE2, 'M', u'به'), - (0xFCE3, 'M', u'تم'), - (0xFCE4, 'M', u'ته'), - (0xFCE5, 'M', u'ثم'), - (0xFCE6, 'M', u'ثه'), - (0xFCE7, 'M', u'سم'), - (0xFCE8, 'M', u'سه'), - (0xFCE9, 'M', u'شم'), - (0xFCEA, 'M', u'شه'), - (0xFCEB, 'M', u'كل'), - (0xFCEC, 'M', u'كم'), - (0xFCED, 'M', u'لم'), - (0xFCEE, 'M', u'نم'), - (0xFCEF, 'M', u'نه'), - (0xFCF0, 'M', u'يم'), - (0xFCF1, 'M', u'يه'), - (0xFCF2, 'M', u'ـَّ'), - (0xFCF3, 'M', u'ـُّ'), - (0xFCF4, 'M', u'ـِّ'), - (0xFCF5, 'M', u'طى'), - (0xFCF6, 'M', u'طي'), - (0xFCF7, 'M', u'عى'), - (0xFCF8, 'M', u'عي'), - (0xFCF9, 'M', u'غى'), - (0xFCFA, 'M', u'غي'), - (0xFCFB, 'M', u'سى'), - (0xFCFC, 'M', u'سي'), - (0xFCFD, 'M', u'شى'), - (0xFCFE, 'M', u'شي'), - (0xFCFF, 'M', u'حى'), - (0xFD00, 'M', u'حي'), - (0xFD01, 'M', u'جى'), - (0xFD02, 'M', u'جي'), - (0xFD03, 'M', u'خى'), - (0xFD04, 'M', u'خي'), - (0xFD05, 'M', u'صى'), - (0xFD06, 'M', u'صي'), - (0xFD07, 'M', u'ضى'), - (0xFD08, 'M', u'ضي'), - (0xFD09, 'M', u'شج'), - (0xFD0A, 'M', u'شح'), - (0xFD0B, 'M', u'شخ'), - (0xFD0C, 'M', u'شم'), - (0xFD0D, 'M', u'شر'), - (0xFD0E, 'M', u'سر'), - (0xFD0F, 'M', u'صر'), - (0xFD10, 'M', u'ضر'), - (0xFD11, 'M', u'طى'), - (0xFD12, 'M', u'طي'), - (0xFD13, 'M', u'عى'), - (0xFD14, 'M', u'عي'), - (0xFD15, 'M', u'غى'), - (0xFD16, 'M', u'غي'), - (0xFD17, 'M', u'سى'), - (0xFD18, 'M', u'سي'), - (0xFD19, 'M', u'شى'), - (0xFD1A, 'M', u'شي'), - (0xFD1B, 'M', u'حى'), - (0xFD1C, 'M', u'حي'), - (0xFD1D, 'M', u'جى'), - (0xFD1E, 'M', u'جي'), - (0xFD1F, 'M', u'خى'), - (0xFD20, 'M', u'خي'), - (0xFD21, 'M', u'صى'), - (0xFD22, 'M', u'صي'), - (0xFD23, 'M', u'ضى'), - (0xFD24, 'M', u'ضي'), - (0xFD25, 'M', u'شج'), - (0xFD26, 'M', u'شح'), - (0xFD27, 'M', u'شخ'), + (0xFCC4, 'M', 'كج'), + (0xFCC5, 'M', 'كح'), + (0xFCC6, 'M', 'كخ'), + (0xFCC7, 'M', 'كل'), + (0xFCC8, 'M', 'كم'), + (0xFCC9, 'M', 'لج'), + (0xFCCA, 'M', 'لح'), + (0xFCCB, 'M', 'لخ'), + (0xFCCC, 'M', 'لم'), + (0xFCCD, 'M', 'له'), + (0xFCCE, 'M', 'مج'), + (0xFCCF, 'M', 'مح'), + (0xFCD0, 'M', 'مخ'), + (0xFCD1, 'M', 'مم'), + (0xFCD2, 'M', 'نج'), + (0xFCD3, 'M', 'نح'), + (0xFCD4, 'M', 'نخ'), + (0xFCD5, 'M', 'نم'), + (0xFCD6, 'M', 'نه'), + (0xFCD7, 'M', 'هج'), + (0xFCD8, 'M', 'هم'), + (0xFCD9, 'M', 'هٰ'), + (0xFCDA, 'M', 'يج'), + (0xFCDB, 'M', 'يح'), + (0xFCDC, 'M', 'يخ'), + (0xFCDD, 'M', 'يم'), + (0xFCDE, 'M', 'يه'), + (0xFCDF, 'M', 'ئم'), + (0xFCE0, 'M', 'ئه'), + (0xFCE1, 'M', 'بم'), + (0xFCE2, 'M', 'به'), + (0xFCE3, 'M', 'تم'), + (0xFCE4, 'M', 'ته'), + (0xFCE5, 'M', 'ثم'), + (0xFCE6, 'M', 'ثه'), + (0xFCE7, 'M', 'سم'), + (0xFCE8, 'M', 'سه'), + (0xFCE9, 'M', 'شم'), + (0xFCEA, 'M', 'شه'), + (0xFCEB, 'M', 'كل'), + (0xFCEC, 'M', 'كم'), + (0xFCED, 'M', 'لم'), + (0xFCEE, 'M', 'نم'), + (0xFCEF, 'M', 'نه'), + (0xFCF0, 'M', 'يم'), + (0xFCF1, 'M', 'يه'), + (0xFCF2, 'M', 'ـَّ'), + (0xFCF3, 'M', 'ـُّ'), + (0xFCF4, 'M', 'ـِّ'), + (0xFCF5, 'M', 'طى'), + (0xFCF6, 'M', 'طي'), + (0xFCF7, 'M', 'عى'), + (0xFCF8, 'M', 'عي'), + (0xFCF9, 'M', 'غى'), + (0xFCFA, 'M', 'غي'), + (0xFCFB, 'M', 'سى'), + (0xFCFC, 'M', 'سي'), + (0xFCFD, 'M', 'شى'), + (0xFCFE, 'M', 'شي'), + (0xFCFF, 'M', 'حى'), + (0xFD00, 'M', 'حي'), + (0xFD01, 'M', 'جى'), + (0xFD02, 'M', 'جي'), + (0xFD03, 'M', 'خى'), + (0xFD04, 'M', 'خي'), + (0xFD05, 'M', 'صى'), + (0xFD06, 'M', 'صي'), + (0xFD07, 'M', 'ضى'), + (0xFD08, 'M', 'ضي'), + (0xFD09, 'M', 'شج'), + (0xFD0A, 'M', 'شح'), + (0xFD0B, 'M', 'شخ'), + (0xFD0C, 'M', 'شم'), + (0xFD0D, 'M', 'شر'), + (0xFD0E, 'M', 'سر'), + (0xFD0F, 'M', 'صر'), + (0xFD10, 'M', 'ضر'), + (0xFD11, 'M', 'طى'), + (0xFD12, 'M', 'طي'), + (0xFD13, 'M', 'عى'), + (0xFD14, 'M', 'عي'), + (0xFD15, 'M', 'غى'), + (0xFD16, 'M', 'غي'), + (0xFD17, 'M', 'سى'), + (0xFD18, 'M', 'سي'), + (0xFD19, 'M', 'شى'), + (0xFD1A, 'M', 'شي'), + (0xFD1B, 'M', 'حى'), + (0xFD1C, 'M', 'حي'), + (0xFD1D, 'M', 'جى'), + (0xFD1E, 'M', 'جي'), + (0xFD1F, 'M', 'خى'), + (0xFD20, 'M', 'خي'), + (0xFD21, 'M', 'صى'), + (0xFD22, 'M', 'صي'), + (0xFD23, 'M', 'ضى'), + (0xFD24, 'M', 'ضي'), + (0xFD25, 'M', 'شج'), + (0xFD26, 'M', 'شح'), + (0xFD27, 'M', 'شخ'), ] def _seg_48(): return [ - (0xFD28, 'M', u'شم'), - (0xFD29, 'M', u'شر'), - (0xFD2A, 'M', u'سر'), - (0xFD2B, 'M', u'صر'), - (0xFD2C, 'M', u'ضر'), - (0xFD2D, 'M', u'شج'), - (0xFD2E, 'M', u'شح'), - (0xFD2F, 'M', u'شخ'), - (0xFD30, 'M', u'شم'), - (0xFD31, 'M', u'سه'), - (0xFD32, 'M', u'شه'), - (0xFD33, 'M', u'طم'), - (0xFD34, 'M', u'سج'), - (0xFD35, 'M', u'سح'), - (0xFD36, 'M', u'سخ'), - (0xFD37, 'M', u'شج'), - (0xFD38, 'M', u'شح'), - (0xFD39, 'M', u'شخ'), - (0xFD3A, 'M', u'طم'), - (0xFD3B, 'M', u'ظم'), - (0xFD3C, 'M', u'اً'), + (0xFD28, 'M', 'شم'), + (0xFD29, 'M', 'شر'), + (0xFD2A, 'M', 'سر'), + (0xFD2B, 'M', 'صر'), + (0xFD2C, 'M', 'ضر'), + (0xFD2D, 'M', 'شج'), + (0xFD2E, 'M', 'شح'), + (0xFD2F, 'M', 'شخ'), + (0xFD30, 'M', 'شم'), + (0xFD31, 'M', 'سه'), + (0xFD32, 'M', 'شه'), + (0xFD33, 'M', 'طم'), + (0xFD34, 'M', 'سج'), + (0xFD35, 'M', 'سح'), + (0xFD36, 'M', 'سخ'), + (0xFD37, 'M', 'شج'), + (0xFD38, 'M', 'شح'), + (0xFD39, 'M', 'شخ'), + (0xFD3A, 'M', 'طم'), + (0xFD3B, 'M', 'ظم'), + (0xFD3C, 'M', 'اً'), (0xFD3E, 'V'), (0xFD40, 'X'), - (0xFD50, 'M', u'تجم'), - (0xFD51, 'M', u'تحج'), - (0xFD53, 'M', u'تحم'), - (0xFD54, 'M', u'تخم'), - (0xFD55, 'M', u'تمج'), - (0xFD56, 'M', u'تمح'), - (0xFD57, 'M', u'تمخ'), - (0xFD58, 'M', u'جمح'), - (0xFD5A, 'M', u'حمي'), - (0xFD5B, 'M', u'حمى'), - (0xFD5C, 'M', u'سحج'), - (0xFD5D, 'M', u'سجح'), - (0xFD5E, 'M', u'سجى'), - (0xFD5F, 'M', u'سمح'), - (0xFD61, 'M', u'سمج'), - (0xFD62, 'M', u'سمم'), - (0xFD64, 'M', u'صحح'), - (0xFD66, 'M', u'صمم'), - (0xFD67, 'M', u'شحم'), - (0xFD69, 'M', u'شجي'), - (0xFD6A, 'M', u'شمخ'), - (0xFD6C, 'M', u'شمم'), - (0xFD6E, 'M', u'ضحى'), - (0xFD6F, 'M', u'ضخم'), - (0xFD71, 'M', u'طمح'), - (0xFD73, 'M', u'طمم'), - (0xFD74, 'M', u'طمي'), - (0xFD75, 'M', u'عجم'), - (0xFD76, 'M', u'عمم'), - (0xFD78, 'M', u'عمى'), - (0xFD79, 'M', u'غمم'), - (0xFD7A, 'M', u'غمي'), - (0xFD7B, 'M', u'غمى'), - (0xFD7C, 'M', u'فخم'), - (0xFD7E, 'M', u'قمح'), - (0xFD7F, 'M', u'قمم'), - (0xFD80, 'M', u'لحم'), - (0xFD81, 'M', u'لحي'), - (0xFD82, 'M', u'لحى'), - (0xFD83, 'M', u'لجج'), - (0xFD85, 'M', u'لخم'), - (0xFD87, 'M', u'لمح'), - (0xFD89, 'M', u'محج'), - (0xFD8A, 'M', u'محم'), - (0xFD8B, 'M', u'محي'), - (0xFD8C, 'M', u'مجح'), - (0xFD8D, 'M', u'مجم'), - (0xFD8E, 'M', u'مخج'), - (0xFD8F, 'M', u'مخم'), + (0xFD50, 'M', 'تجم'), + (0xFD51, 'M', 'تحج'), + (0xFD53, 'M', 'تحم'), + (0xFD54, 'M', 'تخم'), + (0xFD55, 'M', 'تمج'), + (0xFD56, 'M', 'تمح'), + (0xFD57, 'M', 'تمخ'), + (0xFD58, 'M', 'جمح'), + (0xFD5A, 'M', 'حمي'), + (0xFD5B, 'M', 'حمى'), + (0xFD5C, 'M', 'سحج'), + (0xFD5D, 'M', 'سجح'), + (0xFD5E, 'M', 'سجى'), + (0xFD5F, 'M', 'سمح'), + (0xFD61, 'M', 'سمج'), + (0xFD62, 'M', 'سمم'), + (0xFD64, 'M', 'صحح'), + (0xFD66, 'M', 'صمم'), + (0xFD67, 'M', 'شحم'), + (0xFD69, 'M', 'شجي'), + (0xFD6A, 'M', 'شمخ'), + (0xFD6C, 'M', 'شمم'), + (0xFD6E, 'M', 'ضحى'), + (0xFD6F, 'M', 'ضخم'), + (0xFD71, 'M', 'طمح'), + (0xFD73, 'M', 'طمم'), + (0xFD74, 'M', 'طمي'), + (0xFD75, 'M', 'عجم'), + (0xFD76, 'M', 'عمم'), + (0xFD78, 'M', 'عمى'), + (0xFD79, 'M', 'غمم'), + (0xFD7A, 'M', 'غمي'), + (0xFD7B, 'M', 'غمى'), + (0xFD7C, 'M', 'فخم'), + (0xFD7E, 'M', 'قمح'), + (0xFD7F, 'M', 'قمم'), + (0xFD80, 'M', 'لحم'), + (0xFD81, 'M', 'لحي'), + (0xFD82, 'M', 'لحى'), + (0xFD83, 'M', 'لجج'), + (0xFD85, 'M', 'لخم'), + (0xFD87, 'M', 'لمح'), + (0xFD89, 'M', 'محج'), + (0xFD8A, 'M', 'محم'), + (0xFD8B, 'M', 'محي'), + (0xFD8C, 'M', 'مجح'), + (0xFD8D, 'M', 'مجم'), + (0xFD8E, 'M', 'مخج'), + (0xFD8F, 'M', 'مخم'), (0xFD90, 'X'), - (0xFD92, 'M', u'مجخ'), - (0xFD93, 'M', u'همج'), - (0xFD94, 'M', u'همم'), - (0xFD95, 'M', u'نحم'), - (0xFD96, 'M', u'نحى'), - (0xFD97, 'M', u'نجم'), - (0xFD99, 'M', u'نجى'), - (0xFD9A, 'M', u'نمي'), - (0xFD9B, 'M', u'نمى'), - (0xFD9C, 'M', u'يمم'), - (0xFD9E, 'M', u'بخي'), - (0xFD9F, 'M', u'تجي'), - (0xFDA0, 'M', u'تجى'), - (0xFDA1, 'M', u'تخي'), - (0xFDA2, 'M', u'تخى'), - (0xFDA3, 'M', u'تمي'), - (0xFDA4, 'M', u'تمى'), - (0xFDA5, 'M', u'جمي'), - (0xFDA6, 'M', u'جحى'), - (0xFDA7, 'M', u'جمى'), - (0xFDA8, 'M', u'سخى'), - (0xFDA9, 'M', u'صحي'), - (0xFDAA, 'M', u'شحي'), - (0xFDAB, 'M', u'ضحي'), - (0xFDAC, 'M', u'لجي'), - (0xFDAD, 'M', u'لمي'), - (0xFDAE, 'M', u'يحي'), + (0xFD92, 'M', 'مجخ'), + (0xFD93, 'M', 'همج'), + (0xFD94, 'M', 'همم'), + (0xFD95, 'M', 'نحم'), + (0xFD96, 'M', 'نحى'), + (0xFD97, 'M', 'نجم'), + (0xFD99, 'M', 'نجى'), + (0xFD9A, 'M', 'نمي'), + (0xFD9B, 'M', 'نمى'), + (0xFD9C, 'M', 'يمم'), + (0xFD9E, 'M', 'بخي'), + (0xFD9F, 'M', 'تجي'), + (0xFDA0, 'M', 'تجى'), + (0xFDA1, 'M', 'تخي'), + (0xFDA2, 'M', 'تخى'), + (0xFDA3, 'M', 'تمي'), + (0xFDA4, 'M', 'تمى'), + (0xFDA5, 'M', 'جمي'), + (0xFDA6, 'M', 'جحى'), + (0xFDA7, 'M', 'جمى'), + (0xFDA8, 'M', 'سخى'), + (0xFDA9, 'M', 'صحي'), + (0xFDAA, 'M', 'شحي'), + (0xFDAB, 'M', 'ضحي'), + (0xFDAC, 'M', 'لجي'), + (0xFDAD, 'M', 'لمي'), + (0xFDAE, 'M', 'يحي'), ] def _seg_49(): return [ - (0xFDAF, 'M', u'يجي'), - (0xFDB0, 'M', u'يمي'), - (0xFDB1, 'M', u'ممي'), - (0xFDB2, 'M', u'قمي'), - (0xFDB3, 'M', u'نحي'), - (0xFDB4, 'M', u'قمح'), - (0xFDB5, 'M', u'لحم'), - (0xFDB6, 'M', u'عمي'), - (0xFDB7, 'M', u'كمي'), - (0xFDB8, 'M', u'نجح'), - (0xFDB9, 'M', u'مخي'), - (0xFDBA, 'M', u'لجم'), - (0xFDBB, 'M', u'كمم'), - (0xFDBC, 'M', u'لجم'), - (0xFDBD, 'M', u'نجح'), - (0xFDBE, 'M', u'جحي'), - (0xFDBF, 'M', u'حجي'), - (0xFDC0, 'M', u'مجي'), - (0xFDC1, 'M', u'فمي'), - (0xFDC2, 'M', u'بحي'), - (0xFDC3, 'M', u'كمم'), - (0xFDC4, 'M', u'عجم'), - (0xFDC5, 'M', u'صمم'), - (0xFDC6, 'M', u'سخي'), - (0xFDC7, 'M', u'نجي'), + (0xFDAF, 'M', 'يجي'), + (0xFDB0, 'M', 'يمي'), + (0xFDB1, 'M', 'ممي'), + (0xFDB2, 'M', 'قمي'), + (0xFDB3, 'M', 'نحي'), + (0xFDB4, 'M', 'قمح'), + (0xFDB5, 'M', 'لحم'), + (0xFDB6, 'M', 'عمي'), + (0xFDB7, 'M', 'كمي'), + (0xFDB8, 'M', 'نجح'), + (0xFDB9, 'M', 'مخي'), + (0xFDBA, 'M', 'لجم'), + (0xFDBB, 'M', 'كمم'), + (0xFDBC, 'M', 'لجم'), + (0xFDBD, 'M', 'نجح'), + (0xFDBE, 'M', 'جحي'), + (0xFDBF, 'M', 'حجي'), + (0xFDC0, 'M', 'مجي'), + (0xFDC1, 'M', 'فمي'), + (0xFDC2, 'M', 'بحي'), + (0xFDC3, 'M', 'كمم'), + (0xFDC4, 'M', 'عجم'), + (0xFDC5, 'M', 'صمم'), + (0xFDC6, 'M', 'سخي'), + (0xFDC7, 'M', 'نجي'), (0xFDC8, 'X'), - (0xFDF0, 'M', u'صلے'), - (0xFDF1, 'M', u'قلے'), - (0xFDF2, 'M', u'الله'), - (0xFDF3, 'M', u'اكبر'), - (0xFDF4, 'M', u'محمد'), - (0xFDF5, 'M', u'صلعم'), - (0xFDF6, 'M', u'رسول'), - (0xFDF7, 'M', u'عليه'), - (0xFDF8, 'M', u'وسلم'), - (0xFDF9, 'M', u'صلى'), - (0xFDFA, '3', u'صلى الله عليه وسلم'), - (0xFDFB, '3', u'جل جلاله'), - (0xFDFC, 'M', u'ریال'), + (0xFDF0, 'M', 'صلے'), + (0xFDF1, 'M', 'قلے'), + (0xFDF2, 'M', 'الله'), + (0xFDF3, 'M', 'اكبر'), + (0xFDF4, 'M', 'محمد'), + (0xFDF5, 'M', 'صلعم'), + (0xFDF6, 'M', 'رسول'), + (0xFDF7, 'M', 'عليه'), + (0xFDF8, 'M', 'وسلم'), + (0xFDF9, 'M', 'صلى'), + (0xFDFA, '3', 'صلى الله عليه وسلم'), + (0xFDFB, '3', 'جل جلاله'), + (0xFDFC, 'M', 'ریال'), (0xFDFD, 'V'), (0xFDFE, 'X'), (0xFE00, 'I'), - (0xFE10, '3', u','), - (0xFE11, 'M', u'、'), + (0xFE10, '3', ','), + (0xFE11, 'M', '、'), (0xFE12, 'X'), - (0xFE13, '3', u':'), - (0xFE14, '3', u';'), - (0xFE15, '3', u'!'), - (0xFE16, '3', u'?'), - (0xFE17, 'M', u'〖'), - (0xFE18, 'M', u'〗'), + (0xFE13, '3', ':'), + (0xFE14, '3', ';'), + (0xFE15, '3', '!'), + (0xFE16, '3', '?'), + (0xFE17, 'M', '〖'), + (0xFE18, 'M', '〗'), (0xFE19, 'X'), (0xFE20, 'V'), (0xFE30, 'X'), - (0xFE31, 'M', u'—'), - (0xFE32, 'M', u'–'), - (0xFE33, '3', u'_'), - (0xFE35, '3', u'('), - (0xFE36, '3', u')'), - (0xFE37, '3', u'{'), - (0xFE38, '3', u'}'), - (0xFE39, 'M', u'〔'), - (0xFE3A, 'M', u'〕'), - (0xFE3B, 'M', u'【'), - (0xFE3C, 'M', u'】'), - (0xFE3D, 'M', u'《'), - (0xFE3E, 'M', u'》'), - (0xFE3F, 'M', u'〈'), - (0xFE40, 'M', u'〉'), - (0xFE41, 'M', u'「'), - (0xFE42, 'M', u'」'), - (0xFE43, 'M', u'『'), - (0xFE44, 'M', u'』'), + (0xFE31, 'M', '—'), + (0xFE32, 'M', '–'), + (0xFE33, '3', '_'), + (0xFE35, '3', '('), + (0xFE36, '3', ')'), + (0xFE37, '3', '{'), + (0xFE38, '3', '}'), + (0xFE39, 'M', '〔'), + (0xFE3A, 'M', '〕'), + (0xFE3B, 'M', '【'), + (0xFE3C, 'M', '】'), + (0xFE3D, 'M', '《'), + (0xFE3E, 'M', '》'), + (0xFE3F, 'M', '〈'), + (0xFE40, 'M', '〉'), + (0xFE41, 'M', '「'), + (0xFE42, 'M', '」'), + (0xFE43, 'M', '『'), + (0xFE44, 'M', '』'), (0xFE45, 'V'), - (0xFE47, '3', u'['), - (0xFE48, '3', u']'), - (0xFE49, '3', u' ̅'), - (0xFE4D, '3', u'_'), - (0xFE50, '3', u','), - (0xFE51, 'M', u'、'), + (0xFE47, '3', '['), + (0xFE48, '3', ']'), + (0xFE49, '3', ' ̅'), + (0xFE4D, '3', '_'), + (0xFE50, '3', ','), + (0xFE51, 'M', '、'), (0xFE52, 'X'), - (0xFE54, '3', u';'), - (0xFE55, '3', u':'), - (0xFE56, '3', u'?'), - (0xFE57, '3', u'!'), - (0xFE58, 'M', u'—'), - (0xFE59, '3', u'('), - (0xFE5A, '3', u')'), - (0xFE5B, '3', u'{'), - (0xFE5C, '3', u'}'), - (0xFE5D, 'M', u'〔'), - (0xFE5E, 'M', u'〕'), - (0xFE5F, '3', u'#'), - (0xFE60, '3', u'&'), - (0xFE61, '3', u'*'), - (0xFE62, '3', u'+'), - (0xFE63, 'M', u'-'), - (0xFE64, '3', u'<'), - (0xFE65, '3', u'>'), - (0xFE66, '3', u'='), + (0xFE54, '3', ';'), + (0xFE55, '3', ':'), + (0xFE56, '3', '?'), + (0xFE57, '3', '!'), + (0xFE58, 'M', '—'), + (0xFE59, '3', '('), + (0xFE5A, '3', ')'), + (0xFE5B, '3', '{'), + (0xFE5C, '3', '}'), + (0xFE5D, 'M', '〔'), + (0xFE5E, 'M', '〕'), + (0xFE5F, '3', '#'), + (0xFE60, '3', '&'), + (0xFE61, '3', '*'), + (0xFE62, '3', '+'), + (0xFE63, 'M', '-'), + (0xFE64, '3', '<'), + (0xFE65, '3', '>'), + (0xFE66, '3', '='), ] def _seg_50(): return [ (0xFE67, 'X'), - (0xFE68, '3', u'\\'), - (0xFE69, '3', u'$'), - (0xFE6A, '3', u'%'), - (0xFE6B, '3', u'@'), + (0xFE68, '3', '\\'), + (0xFE69, '3', '$'), + (0xFE6A, '3', '%'), + (0xFE6B, '3', '@'), (0xFE6C, 'X'), - (0xFE70, '3', u' ً'), - (0xFE71, 'M', u'ـً'), - (0xFE72, '3', u' ٌ'), + (0xFE70, '3', ' ً'), + (0xFE71, 'M', 'ـً'), + (0xFE72, '3', ' ٌ'), (0xFE73, 'V'), - (0xFE74, '3', u' ٍ'), + (0xFE74, '3', ' ٍ'), (0xFE75, 'X'), - (0xFE76, '3', u' َ'), - (0xFE77, 'M', u'ـَ'), - (0xFE78, '3', u' ُ'), - (0xFE79, 'M', u'ـُ'), - (0xFE7A, '3', u' ِ'), - (0xFE7B, 'M', u'ـِ'), - (0xFE7C, '3', u' ّ'), - (0xFE7D, 'M', u'ـّ'), - (0xFE7E, '3', u' ْ'), - (0xFE7F, 'M', u'ـْ'), - (0xFE80, 'M', u'ء'), - (0xFE81, 'M', u'آ'), - (0xFE83, 'M', u'أ'), - (0xFE85, 'M', u'ؤ'), - (0xFE87, 'M', u'إ'), - (0xFE89, 'M', u'ئ'), - (0xFE8D, 'M', u'ا'), - (0xFE8F, 'M', u'ب'), - (0xFE93, 'M', u'ة'), - (0xFE95, 'M', u'ت'), - (0xFE99, 'M', u'ث'), - (0xFE9D, 'M', u'ج'), - (0xFEA1, 'M', u'ح'), - (0xFEA5, 'M', u'خ'), - (0xFEA9, 'M', u'د'), - (0xFEAB, 'M', u'ذ'), - (0xFEAD, 'M', u'ر'), - (0xFEAF, 'M', u'ز'), - (0xFEB1, 'M', u'س'), - (0xFEB5, 'M', u'ش'), - (0xFEB9, 'M', u'ص'), - (0xFEBD, 'M', u'ض'), - (0xFEC1, 'M', u'ط'), - (0xFEC5, 'M', u'ظ'), - (0xFEC9, 'M', u'ع'), - (0xFECD, 'M', u'غ'), - (0xFED1, 'M', u'ف'), - (0xFED5, 'M', u'ق'), - (0xFED9, 'M', u'ك'), - (0xFEDD, 'M', u'ل'), - (0xFEE1, 'M', u'م'), - (0xFEE5, 'M', u'ن'), - (0xFEE9, 'M', u'ه'), - (0xFEED, 'M', u'و'), - (0xFEEF, 'M', u'ى'), - (0xFEF1, 'M', u'ي'), - (0xFEF5, 'M', u'لآ'), - (0xFEF7, 'M', u'لأ'), - (0xFEF9, 'M', u'لإ'), - (0xFEFB, 'M', u'لا'), + (0xFE76, '3', ' َ'), + (0xFE77, 'M', 'ـَ'), + (0xFE78, '3', ' ُ'), + (0xFE79, 'M', 'ـُ'), + (0xFE7A, '3', ' ِ'), + (0xFE7B, 'M', 'ـِ'), + (0xFE7C, '3', ' ّ'), + (0xFE7D, 'M', 'ـّ'), + (0xFE7E, '3', ' ْ'), + (0xFE7F, 'M', 'ـْ'), + (0xFE80, 'M', 'ء'), + (0xFE81, 'M', 'آ'), + (0xFE83, 'M', 'أ'), + (0xFE85, 'M', 'ؤ'), + (0xFE87, 'M', 'إ'), + (0xFE89, 'M', 'ئ'), + (0xFE8D, 'M', 'ا'), + (0xFE8F, 'M', 'ب'), + (0xFE93, 'M', 'ة'), + (0xFE95, 'M', 'ت'), + (0xFE99, 'M', 'ث'), + (0xFE9D, 'M', 'ج'), + (0xFEA1, 'M', 'ح'), + (0xFEA5, 'M', 'خ'), + (0xFEA9, 'M', 'د'), + (0xFEAB, 'M', 'ذ'), + (0xFEAD, 'M', 'ر'), + (0xFEAF, 'M', 'ز'), + (0xFEB1, 'M', 'س'), + (0xFEB5, 'M', 'ش'), + (0xFEB9, 'M', 'ص'), + (0xFEBD, 'M', 'ض'), + (0xFEC1, 'M', 'ط'), + (0xFEC5, 'M', 'ظ'), + (0xFEC9, 'M', 'ع'), + (0xFECD, 'M', 'غ'), + (0xFED1, 'M', 'ف'), + (0xFED5, 'M', 'ق'), + (0xFED9, 'M', 'ك'), + (0xFEDD, 'M', 'ل'), + (0xFEE1, 'M', 'م'), + (0xFEE5, 'M', 'ن'), + (0xFEE9, 'M', 'ه'), + (0xFEED, 'M', 'و'), + (0xFEEF, 'M', 'ى'), + (0xFEF1, 'M', 'ي'), + (0xFEF5, 'M', 'لآ'), + (0xFEF7, 'M', 'لأ'), + (0xFEF9, 'M', 'لإ'), + (0xFEFB, 'M', 'لا'), (0xFEFD, 'X'), (0xFEFF, 'I'), (0xFF00, 'X'), - (0xFF01, '3', u'!'), - (0xFF02, '3', u'"'), - (0xFF03, '3', u'#'), - (0xFF04, '3', u'$'), - (0xFF05, '3', u'%'), - (0xFF06, '3', u'&'), - (0xFF07, '3', u'\''), - (0xFF08, '3', u'('), - (0xFF09, '3', u')'), - (0xFF0A, '3', u'*'), - (0xFF0B, '3', u'+'), - (0xFF0C, '3', u','), - (0xFF0D, 'M', u'-'), - (0xFF0E, 'M', u'.'), - (0xFF0F, '3', u'/'), - (0xFF10, 'M', u'0'), - (0xFF11, 'M', u'1'), - (0xFF12, 'M', u'2'), - (0xFF13, 'M', u'3'), - (0xFF14, 'M', u'4'), - (0xFF15, 'M', u'5'), - (0xFF16, 'M', u'6'), - (0xFF17, 'M', u'7'), - (0xFF18, 'M', u'8'), - (0xFF19, 'M', u'9'), - (0xFF1A, '3', u':'), - (0xFF1B, '3', u';'), - (0xFF1C, '3', u'<'), - (0xFF1D, '3', u'='), - (0xFF1E, '3', u'>'), - (0xFF1F, '3', u'?'), - (0xFF20, '3', u'@'), - (0xFF21, 'M', u'a'), - (0xFF22, 'M', u'b'), - (0xFF23, 'M', u'c'), + (0xFF01, '3', '!'), + (0xFF02, '3', '"'), + (0xFF03, '3', '#'), + (0xFF04, '3', '$'), + (0xFF05, '3', '%'), + (0xFF06, '3', '&'), + (0xFF07, '3', '\''), + (0xFF08, '3', '('), + (0xFF09, '3', ')'), + (0xFF0A, '3', '*'), + (0xFF0B, '3', '+'), + (0xFF0C, '3', ','), + (0xFF0D, 'M', '-'), + (0xFF0E, 'M', '.'), + (0xFF0F, '3', '/'), + (0xFF10, 'M', '0'), + (0xFF11, 'M', '1'), + (0xFF12, 'M', '2'), + (0xFF13, 'M', '3'), + (0xFF14, 'M', '4'), + (0xFF15, 'M', '5'), + (0xFF16, 'M', '6'), + (0xFF17, 'M', '7'), + (0xFF18, 'M', '8'), + (0xFF19, 'M', '9'), + (0xFF1A, '3', ':'), + (0xFF1B, '3', ';'), + (0xFF1C, '3', '<'), + (0xFF1D, '3', '='), + (0xFF1E, '3', '>'), + (0xFF1F, '3', '?'), + (0xFF20, '3', '@'), + (0xFF21, 'M', 'a'), + (0xFF22, 'M', 'b'), + (0xFF23, 'M', 'c'), ] def _seg_51(): return [ - (0xFF24, 'M', u'd'), - (0xFF25, 'M', u'e'), - (0xFF26, 'M', u'f'), - (0xFF27, 'M', u'g'), - (0xFF28, 'M', u'h'), - (0xFF29, 'M', u'i'), - (0xFF2A, 'M', u'j'), - (0xFF2B, 'M', u'k'), - (0xFF2C, 'M', u'l'), - (0xFF2D, 'M', u'm'), - (0xFF2E, 'M', u'n'), - (0xFF2F, 'M', u'o'), - (0xFF30, 'M', u'p'), - (0xFF31, 'M', u'q'), - (0xFF32, 'M', u'r'), - (0xFF33, 'M', u's'), - (0xFF34, 'M', u't'), - (0xFF35, 'M', u'u'), - (0xFF36, 'M', u'v'), - (0xFF37, 'M', u'w'), - (0xFF38, 'M', u'x'), - (0xFF39, 'M', u'y'), - (0xFF3A, 'M', u'z'), - (0xFF3B, '3', u'['), - (0xFF3C, '3', u'\\'), - (0xFF3D, '3', u']'), - (0xFF3E, '3', u'^'), - (0xFF3F, '3', u'_'), - (0xFF40, '3', u'`'), - (0xFF41, 'M', u'a'), - (0xFF42, 'M', u'b'), - (0xFF43, 'M', u'c'), - (0xFF44, 'M', u'd'), - (0xFF45, 'M', u'e'), - (0xFF46, 'M', u'f'), - (0xFF47, 'M', u'g'), - (0xFF48, 'M', u'h'), - (0xFF49, 'M', u'i'), - (0xFF4A, 'M', u'j'), - (0xFF4B, 'M', u'k'), - (0xFF4C, 'M', u'l'), - (0xFF4D, 'M', u'm'), - (0xFF4E, 'M', u'n'), - (0xFF4F, 'M', u'o'), - (0xFF50, 'M', u'p'), - (0xFF51, 'M', u'q'), - (0xFF52, 'M', u'r'), - (0xFF53, 'M', u's'), - (0xFF54, 'M', u't'), - (0xFF55, 'M', u'u'), - (0xFF56, 'M', u'v'), - (0xFF57, 'M', u'w'), - (0xFF58, 'M', u'x'), - (0xFF59, 'M', u'y'), - (0xFF5A, 'M', u'z'), - (0xFF5B, '3', u'{'), - (0xFF5C, '3', u'|'), - (0xFF5D, '3', u'}'), - (0xFF5E, '3', u'~'), - (0xFF5F, 'M', u'⦅'), - (0xFF60, 'M', u'⦆'), - (0xFF61, 'M', u'.'), - (0xFF62, 'M', u'「'), - (0xFF63, 'M', u'」'), - (0xFF64, 'M', u'、'), - (0xFF65, 'M', u'・'), - (0xFF66, 'M', u'ヲ'), - (0xFF67, 'M', u'ァ'), - (0xFF68, 'M', u'ィ'), - (0xFF69, 'M', u'ゥ'), - (0xFF6A, 'M', u'ェ'), - (0xFF6B, 'M', u'ォ'), - (0xFF6C, 'M', u'ャ'), - (0xFF6D, 'M', u'ュ'), - (0xFF6E, 'M', u'ョ'), - (0xFF6F, 'M', u'ッ'), - (0xFF70, 'M', u'ー'), - (0xFF71, 'M', u'ア'), - (0xFF72, 'M', u'イ'), - (0xFF73, 'M', u'ウ'), - (0xFF74, 'M', u'エ'), - (0xFF75, 'M', u'オ'), - (0xFF76, 'M', u'カ'), - (0xFF77, 'M', u'キ'), - (0xFF78, 'M', u'ク'), - (0xFF79, 'M', u'ケ'), - (0xFF7A, 'M', u'コ'), - (0xFF7B, 'M', u'サ'), - (0xFF7C, 'M', u'シ'), - (0xFF7D, 'M', u'ス'), - (0xFF7E, 'M', u'セ'), - (0xFF7F, 'M', u'ソ'), - (0xFF80, 'M', u'タ'), - (0xFF81, 'M', u'チ'), - (0xFF82, 'M', u'ツ'), - (0xFF83, 'M', u'テ'), - (0xFF84, 'M', u'ト'), - (0xFF85, 'M', u'ナ'), - (0xFF86, 'M', u'ニ'), - (0xFF87, 'M', u'ヌ'), + (0xFF24, 'M', 'd'), + (0xFF25, 'M', 'e'), + (0xFF26, 'M', 'f'), + (0xFF27, 'M', 'g'), + (0xFF28, 'M', 'h'), + (0xFF29, 'M', 'i'), + (0xFF2A, 'M', 'j'), + (0xFF2B, 'M', 'k'), + (0xFF2C, 'M', 'l'), + (0xFF2D, 'M', 'm'), + (0xFF2E, 'M', 'n'), + (0xFF2F, 'M', 'o'), + (0xFF30, 'M', 'p'), + (0xFF31, 'M', 'q'), + (0xFF32, 'M', 'r'), + (0xFF33, 'M', 's'), + (0xFF34, 'M', 't'), + (0xFF35, 'M', 'u'), + (0xFF36, 'M', 'v'), + (0xFF37, 'M', 'w'), + (0xFF38, 'M', 'x'), + (0xFF39, 'M', 'y'), + (0xFF3A, 'M', 'z'), + (0xFF3B, '3', '['), + (0xFF3C, '3', '\\'), + (0xFF3D, '3', ']'), + (0xFF3E, '3', '^'), + (0xFF3F, '3', '_'), + (0xFF40, '3', '`'), + (0xFF41, 'M', 'a'), + (0xFF42, 'M', 'b'), + (0xFF43, 'M', 'c'), + (0xFF44, 'M', 'd'), + (0xFF45, 'M', 'e'), + (0xFF46, 'M', 'f'), + (0xFF47, 'M', 'g'), + (0xFF48, 'M', 'h'), + (0xFF49, 'M', 'i'), + (0xFF4A, 'M', 'j'), + (0xFF4B, 'M', 'k'), + (0xFF4C, 'M', 'l'), + (0xFF4D, 'M', 'm'), + (0xFF4E, 'M', 'n'), + (0xFF4F, 'M', 'o'), + (0xFF50, 'M', 'p'), + (0xFF51, 'M', 'q'), + (0xFF52, 'M', 'r'), + (0xFF53, 'M', 's'), + (0xFF54, 'M', 't'), + (0xFF55, 'M', 'u'), + (0xFF56, 'M', 'v'), + (0xFF57, 'M', 'w'), + (0xFF58, 'M', 'x'), + (0xFF59, 'M', 'y'), + (0xFF5A, 'M', 'z'), + (0xFF5B, '3', '{'), + (0xFF5C, '3', '|'), + (0xFF5D, '3', '}'), + (0xFF5E, '3', '~'), + (0xFF5F, 'M', '⦅'), + (0xFF60, 'M', '⦆'), + (0xFF61, 'M', '.'), + (0xFF62, 'M', '「'), + (0xFF63, 'M', '」'), + (0xFF64, 'M', '、'), + (0xFF65, 'M', '・'), + (0xFF66, 'M', 'ヲ'), + (0xFF67, 'M', 'ァ'), + (0xFF68, 'M', 'ィ'), + (0xFF69, 'M', 'ゥ'), + (0xFF6A, 'M', 'ェ'), + (0xFF6B, 'M', 'ォ'), + (0xFF6C, 'M', 'ャ'), + (0xFF6D, 'M', 'ュ'), + (0xFF6E, 'M', 'ョ'), + (0xFF6F, 'M', 'ッ'), + (0xFF70, 'M', 'ー'), + (0xFF71, 'M', 'ア'), + (0xFF72, 'M', 'イ'), + (0xFF73, 'M', 'ウ'), + (0xFF74, 'M', 'エ'), + (0xFF75, 'M', 'オ'), + (0xFF76, 'M', 'カ'), + (0xFF77, 'M', 'キ'), + (0xFF78, 'M', 'ク'), + (0xFF79, 'M', 'ケ'), + (0xFF7A, 'M', 'コ'), + (0xFF7B, 'M', 'サ'), + (0xFF7C, 'M', 'シ'), + (0xFF7D, 'M', 'ス'), + (0xFF7E, 'M', 'セ'), + (0xFF7F, 'M', 'ソ'), + (0xFF80, 'M', 'タ'), + (0xFF81, 'M', 'チ'), + (0xFF82, 'M', 'ツ'), + (0xFF83, 'M', 'テ'), + (0xFF84, 'M', 'ト'), + (0xFF85, 'M', 'ナ'), + (0xFF86, 'M', 'ニ'), + (0xFF87, 'M', 'ヌ'), ] def _seg_52(): return [ - (0xFF88, 'M', u'ネ'), - (0xFF89, 'M', u'ノ'), - (0xFF8A, 'M', u'ハ'), - (0xFF8B, 'M', u'ヒ'), - (0xFF8C, 'M', u'フ'), - (0xFF8D, 'M', u'ヘ'), - (0xFF8E, 'M', u'ホ'), - (0xFF8F, 'M', u'マ'), - (0xFF90, 'M', u'ミ'), - (0xFF91, 'M', u'ム'), - (0xFF92, 'M', u'メ'), - (0xFF93, 'M', u'モ'), - (0xFF94, 'M', u'ヤ'), - (0xFF95, 'M', u'ユ'), - (0xFF96, 'M', u'ヨ'), - (0xFF97, 'M', u'ラ'), - (0xFF98, 'M', u'リ'), - (0xFF99, 'M', u'ル'), - (0xFF9A, 'M', u'レ'), - (0xFF9B, 'M', u'ロ'), - (0xFF9C, 'M', u'ワ'), - (0xFF9D, 'M', u'ン'), - (0xFF9E, 'M', u'゙'), - (0xFF9F, 'M', u'゚'), + (0xFF88, 'M', 'ネ'), + (0xFF89, 'M', 'ノ'), + (0xFF8A, 'M', 'ハ'), + (0xFF8B, 'M', 'ヒ'), + (0xFF8C, 'M', 'フ'), + (0xFF8D, 'M', 'ヘ'), + (0xFF8E, 'M', 'ホ'), + (0xFF8F, 'M', 'マ'), + (0xFF90, 'M', 'ミ'), + (0xFF91, 'M', 'ム'), + (0xFF92, 'M', 'メ'), + (0xFF93, 'M', 'モ'), + (0xFF94, 'M', 'ヤ'), + (0xFF95, 'M', 'ユ'), + (0xFF96, 'M', 'ヨ'), + (0xFF97, 'M', 'ラ'), + (0xFF98, 'M', 'リ'), + (0xFF99, 'M', 'ル'), + (0xFF9A, 'M', 'レ'), + (0xFF9B, 'M', 'ロ'), + (0xFF9C, 'M', 'ワ'), + (0xFF9D, 'M', 'ン'), + (0xFF9E, 'M', '゙'), + (0xFF9F, 'M', '゚'), (0xFFA0, 'X'), - (0xFFA1, 'M', u'ᄀ'), - (0xFFA2, 'M', u'ᄁ'), - (0xFFA3, 'M', u'ᆪ'), - (0xFFA4, 'M', u'ᄂ'), - (0xFFA5, 'M', u'ᆬ'), - (0xFFA6, 'M', u'ᆭ'), - (0xFFA7, 'M', u'ᄃ'), - (0xFFA8, 'M', u'ᄄ'), - (0xFFA9, 'M', u'ᄅ'), - (0xFFAA, 'M', u'ᆰ'), - (0xFFAB, 'M', u'ᆱ'), - (0xFFAC, 'M', u'ᆲ'), - (0xFFAD, 'M', u'ᆳ'), - (0xFFAE, 'M', u'ᆴ'), - (0xFFAF, 'M', u'ᆵ'), - (0xFFB0, 'M', u'ᄚ'), - (0xFFB1, 'M', u'ᄆ'), - (0xFFB2, 'M', u'ᄇ'), - (0xFFB3, 'M', u'ᄈ'), - (0xFFB4, 'M', u'ᄡ'), - (0xFFB5, 'M', u'ᄉ'), - (0xFFB6, 'M', u'ᄊ'), - (0xFFB7, 'M', u'ᄋ'), - (0xFFB8, 'M', u'ᄌ'), - (0xFFB9, 'M', u'ᄍ'), - (0xFFBA, 'M', u'ᄎ'), - (0xFFBB, 'M', u'ᄏ'), - (0xFFBC, 'M', u'ᄐ'), - (0xFFBD, 'M', u'ᄑ'), - (0xFFBE, 'M', u'ᄒ'), + (0xFFA1, 'M', 'ᄀ'), + (0xFFA2, 'M', 'ᄁ'), + (0xFFA3, 'M', 'ᆪ'), + (0xFFA4, 'M', 'ᄂ'), + (0xFFA5, 'M', 'ᆬ'), + (0xFFA6, 'M', 'ᆭ'), + (0xFFA7, 'M', 'ᄃ'), + (0xFFA8, 'M', 'ᄄ'), + (0xFFA9, 'M', 'ᄅ'), + (0xFFAA, 'M', 'ᆰ'), + (0xFFAB, 'M', 'ᆱ'), + (0xFFAC, 'M', 'ᆲ'), + (0xFFAD, 'M', 'ᆳ'), + (0xFFAE, 'M', 'ᆴ'), + (0xFFAF, 'M', 'ᆵ'), + (0xFFB0, 'M', 'ᄚ'), + (0xFFB1, 'M', 'ᄆ'), + (0xFFB2, 'M', 'ᄇ'), + (0xFFB3, 'M', 'ᄈ'), + (0xFFB4, 'M', 'ᄡ'), + (0xFFB5, 'M', 'ᄉ'), + (0xFFB6, 'M', 'ᄊ'), + (0xFFB7, 'M', 'ᄋ'), + (0xFFB8, 'M', 'ᄌ'), + (0xFFB9, 'M', 'ᄍ'), + (0xFFBA, 'M', 'ᄎ'), + (0xFFBB, 'M', 'ᄏ'), + (0xFFBC, 'M', 'ᄐ'), + (0xFFBD, 'M', 'ᄑ'), + (0xFFBE, 'M', 'ᄒ'), (0xFFBF, 'X'), - (0xFFC2, 'M', u'ᅡ'), - (0xFFC3, 'M', u'ᅢ'), - (0xFFC4, 'M', u'ᅣ'), - (0xFFC5, 'M', u'ᅤ'), - (0xFFC6, 'M', u'ᅥ'), - (0xFFC7, 'M', u'ᅦ'), + (0xFFC2, 'M', 'ᅡ'), + (0xFFC3, 'M', 'ᅢ'), + (0xFFC4, 'M', 'ᅣ'), + (0xFFC5, 'M', 'ᅤ'), + (0xFFC6, 'M', 'ᅥ'), + (0xFFC7, 'M', 'ᅦ'), (0xFFC8, 'X'), - (0xFFCA, 'M', u'ᅧ'), - (0xFFCB, 'M', u'ᅨ'), - (0xFFCC, 'M', u'ᅩ'), - (0xFFCD, 'M', u'ᅪ'), - (0xFFCE, 'M', u'ᅫ'), - (0xFFCF, 'M', u'ᅬ'), + (0xFFCA, 'M', 'ᅧ'), + (0xFFCB, 'M', 'ᅨ'), + (0xFFCC, 'M', 'ᅩ'), + (0xFFCD, 'M', 'ᅪ'), + (0xFFCE, 'M', 'ᅫ'), + (0xFFCF, 'M', 'ᅬ'), (0xFFD0, 'X'), - (0xFFD2, 'M', u'ᅭ'), - (0xFFD3, 'M', u'ᅮ'), - (0xFFD4, 'M', u'ᅯ'), - (0xFFD5, 'M', u'ᅰ'), - (0xFFD6, 'M', u'ᅱ'), - (0xFFD7, 'M', u'ᅲ'), + (0xFFD2, 'M', 'ᅭ'), + (0xFFD3, 'M', 'ᅮ'), + (0xFFD4, 'M', 'ᅯ'), + (0xFFD5, 'M', 'ᅰ'), + (0xFFD6, 'M', 'ᅱ'), + (0xFFD7, 'M', 'ᅲ'), (0xFFD8, 'X'), - (0xFFDA, 'M', u'ᅳ'), - (0xFFDB, 'M', u'ᅴ'), - (0xFFDC, 'M', u'ᅵ'), + (0xFFDA, 'M', 'ᅳ'), + (0xFFDB, 'M', 'ᅴ'), + (0xFFDC, 'M', 'ᅵ'), (0xFFDD, 'X'), - (0xFFE0, 'M', u'¢'), - (0xFFE1, 'M', u'£'), - (0xFFE2, 'M', u'¬'), - (0xFFE3, '3', u' ̄'), - (0xFFE4, 'M', u'¦'), - (0xFFE5, 'M', u'¥'), - (0xFFE6, 'M', u'₩'), + (0xFFE0, 'M', '¢'), + (0xFFE1, 'M', '£'), + (0xFFE2, 'M', '¬'), + (0xFFE3, '3', ' ̄'), + (0xFFE4, 'M', '¦'), + (0xFFE5, 'M', '¥'), + (0xFFE6, 'M', '₩'), (0xFFE7, 'X'), - (0xFFE8, 'M', u'│'), - (0xFFE9, 'M', u'←'), - (0xFFEA, 'M', u'↑'), - (0xFFEB, 'M', u'→'), - (0xFFEC, 'M', u'↓'), - (0xFFED, 'M', u'■'), - (0xFFEE, 'M', u'○'), + (0xFFE8, 'M', '│'), + (0xFFE9, 'M', '←'), + (0xFFEA, 'M', '↑'), + (0xFFEB, 'M', '→'), + (0xFFEC, 'M', '↓'), + (0xFFED, 'M', '■'), + (0xFFEE, 'M', '○'), (0xFFEF, 'X'), (0x10000, 'V'), (0x1000C, 'X'), @@ -5560,90 +5559,90 @@ def _seg_53(): (0x103C4, 'X'), (0x103C8, 'V'), (0x103D6, 'X'), - (0x10400, 'M', u'𐐨'), - (0x10401, 'M', u'𐐩'), - (0x10402, 'M', u'𐐪'), - (0x10403, 'M', u'𐐫'), - (0x10404, 'M', u'𐐬'), - (0x10405, 'M', u'𐐭'), - (0x10406, 'M', u'𐐮'), - (0x10407, 'M', u'𐐯'), - (0x10408, 'M', u'𐐰'), - (0x10409, 'M', u'𐐱'), - (0x1040A, 'M', u'𐐲'), - (0x1040B, 'M', u'𐐳'), - (0x1040C, 'M', u'𐐴'), - (0x1040D, 'M', u'𐐵'), - (0x1040E, 'M', u'𐐶'), - (0x1040F, 'M', u'𐐷'), - (0x10410, 'M', u'𐐸'), - (0x10411, 'M', u'𐐹'), - (0x10412, 'M', u'𐐺'), - (0x10413, 'M', u'𐐻'), - (0x10414, 'M', u'𐐼'), - (0x10415, 'M', u'𐐽'), - (0x10416, 'M', u'𐐾'), - (0x10417, 'M', u'𐐿'), - (0x10418, 'M', u'𐑀'), - (0x10419, 'M', u'𐑁'), - (0x1041A, 'M', u'𐑂'), - (0x1041B, 'M', u'𐑃'), - (0x1041C, 'M', u'𐑄'), - (0x1041D, 'M', u'𐑅'), - (0x1041E, 'M', u'𐑆'), - (0x1041F, 'M', u'𐑇'), - (0x10420, 'M', u'𐑈'), - (0x10421, 'M', u'𐑉'), - (0x10422, 'M', u'𐑊'), - (0x10423, 'M', u'𐑋'), - (0x10424, 'M', u'𐑌'), - (0x10425, 'M', u'𐑍'), - (0x10426, 'M', u'𐑎'), - (0x10427, 'M', u'𐑏'), + (0x10400, 'M', '𐐨'), + (0x10401, 'M', '𐐩'), + (0x10402, 'M', '𐐪'), + (0x10403, 'M', '𐐫'), + (0x10404, 'M', '𐐬'), + (0x10405, 'M', '𐐭'), + (0x10406, 'M', '𐐮'), + (0x10407, 'M', '𐐯'), + (0x10408, 'M', '𐐰'), + (0x10409, 'M', '𐐱'), + (0x1040A, 'M', '𐐲'), + (0x1040B, 'M', '𐐳'), + (0x1040C, 'M', '𐐴'), + (0x1040D, 'M', '𐐵'), + (0x1040E, 'M', '𐐶'), + (0x1040F, 'M', '𐐷'), + (0x10410, 'M', '𐐸'), + (0x10411, 'M', '𐐹'), + (0x10412, 'M', '𐐺'), + (0x10413, 'M', '𐐻'), + (0x10414, 'M', '𐐼'), + (0x10415, 'M', '𐐽'), + (0x10416, 'M', '𐐾'), + (0x10417, 'M', '𐐿'), + (0x10418, 'M', '𐑀'), + (0x10419, 'M', '𐑁'), + (0x1041A, 'M', '𐑂'), + (0x1041B, 'M', '𐑃'), + (0x1041C, 'M', '𐑄'), + (0x1041D, 'M', '𐑅'), + (0x1041E, 'M', '𐑆'), + (0x1041F, 'M', '𐑇'), + (0x10420, 'M', '𐑈'), + (0x10421, 'M', '𐑉'), + (0x10422, 'M', '𐑊'), + (0x10423, 'M', '𐑋'), + (0x10424, 'M', '𐑌'), + (0x10425, 'M', '𐑍'), + (0x10426, 'M', '𐑎'), + (0x10427, 'M', '𐑏'), (0x10428, 'V'), (0x1049E, 'X'), (0x104A0, 'V'), (0x104AA, 'X'), - (0x104B0, 'M', u'𐓘'), - (0x104B1, 'M', u'𐓙'), - (0x104B2, 'M', u'𐓚'), - (0x104B3, 'M', u'𐓛'), - (0x104B4, 'M', u'𐓜'), - (0x104B5, 'M', u'𐓝'), - (0x104B6, 'M', u'𐓞'), - (0x104B7, 'M', u'𐓟'), - (0x104B8, 'M', u'𐓠'), - (0x104B9, 'M', u'𐓡'), - (0x104BA, 'M', u'𐓢'), - (0x104BB, 'M', u'𐓣'), - (0x104BC, 'M', u'𐓤'), - (0x104BD, 'M', u'𐓥'), - (0x104BE, 'M', u'𐓦'), + (0x104B0, 'M', '𐓘'), + (0x104B1, 'M', '𐓙'), + (0x104B2, 'M', '𐓚'), + (0x104B3, 'M', '𐓛'), + (0x104B4, 'M', '𐓜'), + (0x104B5, 'M', '𐓝'), + (0x104B6, 'M', '𐓞'), + (0x104B7, 'M', '𐓟'), + (0x104B8, 'M', '𐓠'), + (0x104B9, 'M', '𐓡'), + (0x104BA, 'M', '𐓢'), + (0x104BB, 'M', '𐓣'), + (0x104BC, 'M', '𐓤'), + (0x104BD, 'M', '𐓥'), + (0x104BE, 'M', '𐓦'), ] def _seg_54(): return [ - (0x104BF, 'M', u'𐓧'), - (0x104C0, 'M', u'𐓨'), - (0x104C1, 'M', u'𐓩'), - (0x104C2, 'M', u'𐓪'), - (0x104C3, 'M', u'𐓫'), - (0x104C4, 'M', u'𐓬'), - (0x104C5, 'M', u'𐓭'), - (0x104C6, 'M', u'𐓮'), - (0x104C7, 'M', u'𐓯'), - (0x104C8, 'M', u'𐓰'), - (0x104C9, 'M', u'𐓱'), - (0x104CA, 'M', u'𐓲'), - (0x104CB, 'M', u'𐓳'), - (0x104CC, 'M', u'𐓴'), - (0x104CD, 'M', u'𐓵'), - (0x104CE, 'M', u'𐓶'), - (0x104CF, 'M', u'𐓷'), - (0x104D0, 'M', u'𐓸'), - (0x104D1, 'M', u'𐓹'), - (0x104D2, 'M', u'𐓺'), - (0x104D3, 'M', u'𐓻'), + (0x104BF, 'M', '𐓧'), + (0x104C0, 'M', '𐓨'), + (0x104C1, 'M', '𐓩'), + (0x104C2, 'M', '𐓪'), + (0x104C3, 'M', '𐓫'), + (0x104C4, 'M', '𐓬'), + (0x104C5, 'M', '𐓭'), + (0x104C6, 'M', '𐓮'), + (0x104C7, 'M', '𐓯'), + (0x104C8, 'M', '𐓰'), + (0x104C9, 'M', '𐓱'), + (0x104CA, 'M', '𐓲'), + (0x104CB, 'M', '𐓳'), + (0x104CC, 'M', '𐓴'), + (0x104CD, 'M', '𐓵'), + (0x104CE, 'M', '𐓶'), + (0x104CF, 'M', '𐓷'), + (0x104D0, 'M', '𐓸'), + (0x104D1, 'M', '𐓹'), + (0x104D2, 'M', '𐓺'), + (0x104D3, 'M', '𐓻'), (0x104D4, 'X'), (0x104D8, 'V'), (0x104FC, 'X'), @@ -5729,57 +5728,57 @@ def _seg_55(): return [ (0x10C00, 'V'), (0x10C49, 'X'), - (0x10C80, 'M', u'𐳀'), - (0x10C81, 'M', u'𐳁'), - (0x10C82, 'M', u'𐳂'), - (0x10C83, 'M', u'𐳃'), - (0x10C84, 'M', u'𐳄'), - (0x10C85, 'M', u'𐳅'), - (0x10C86, 'M', u'𐳆'), - (0x10C87, 'M', u'𐳇'), - (0x10C88, 'M', u'𐳈'), - (0x10C89, 'M', u'𐳉'), - (0x10C8A, 'M', u'𐳊'), - (0x10C8B, 'M', u'𐳋'), - (0x10C8C, 'M', u'𐳌'), - (0x10C8D, 'M', u'𐳍'), - (0x10C8E, 'M', u'𐳎'), - (0x10C8F, 'M', u'𐳏'), - (0x10C90, 'M', u'𐳐'), - (0x10C91, 'M', u'𐳑'), - (0x10C92, 'M', u'𐳒'), - (0x10C93, 'M', u'𐳓'), - (0x10C94, 'M', u'𐳔'), - (0x10C95, 'M', u'𐳕'), - (0x10C96, 'M', u'𐳖'), - (0x10C97, 'M', u'𐳗'), - (0x10C98, 'M', u'𐳘'), - (0x10C99, 'M', u'𐳙'), - (0x10C9A, 'M', u'𐳚'), - (0x10C9B, 'M', u'𐳛'), - (0x10C9C, 'M', u'𐳜'), - (0x10C9D, 'M', u'𐳝'), - (0x10C9E, 'M', u'𐳞'), - (0x10C9F, 'M', u'𐳟'), - (0x10CA0, 'M', u'𐳠'), - (0x10CA1, 'M', u'𐳡'), - (0x10CA2, 'M', u'𐳢'), - (0x10CA3, 'M', u'𐳣'), - (0x10CA4, 'M', u'𐳤'), - (0x10CA5, 'M', u'𐳥'), - (0x10CA6, 'M', u'𐳦'), - (0x10CA7, 'M', u'𐳧'), - (0x10CA8, 'M', u'𐳨'), - (0x10CA9, 'M', u'𐳩'), - (0x10CAA, 'M', u'𐳪'), - (0x10CAB, 'M', u'𐳫'), - (0x10CAC, 'M', u'𐳬'), - (0x10CAD, 'M', u'𐳭'), - (0x10CAE, 'M', u'𐳮'), - (0x10CAF, 'M', u'𐳯'), - (0x10CB0, 'M', u'𐳰'), - (0x10CB1, 'M', u'𐳱'), - (0x10CB2, 'M', u'𐳲'), + (0x10C80, 'M', '𐳀'), + (0x10C81, 'M', '𐳁'), + (0x10C82, 'M', '𐳂'), + (0x10C83, 'M', '𐳃'), + (0x10C84, 'M', '𐳄'), + (0x10C85, 'M', '𐳅'), + (0x10C86, 'M', '𐳆'), + (0x10C87, 'M', '𐳇'), + (0x10C88, 'M', '𐳈'), + (0x10C89, 'M', '𐳉'), + (0x10C8A, 'M', '𐳊'), + (0x10C8B, 'M', '𐳋'), + (0x10C8C, 'M', '𐳌'), + (0x10C8D, 'M', '𐳍'), + (0x10C8E, 'M', '𐳎'), + (0x10C8F, 'M', '𐳏'), + (0x10C90, 'M', '𐳐'), + (0x10C91, 'M', '𐳑'), + (0x10C92, 'M', '𐳒'), + (0x10C93, 'M', '𐳓'), + (0x10C94, 'M', '𐳔'), + (0x10C95, 'M', '𐳕'), + (0x10C96, 'M', '𐳖'), + (0x10C97, 'M', '𐳗'), + (0x10C98, 'M', '𐳘'), + (0x10C99, 'M', '𐳙'), + (0x10C9A, 'M', '𐳚'), + (0x10C9B, 'M', '𐳛'), + (0x10C9C, 'M', '𐳜'), + (0x10C9D, 'M', '𐳝'), + (0x10C9E, 'M', '𐳞'), + (0x10C9F, 'M', '𐳟'), + (0x10CA0, 'M', '𐳠'), + (0x10CA1, 'M', '𐳡'), + (0x10CA2, 'M', '𐳢'), + (0x10CA3, 'M', '𐳣'), + (0x10CA4, 'M', '𐳤'), + (0x10CA5, 'M', '𐳥'), + (0x10CA6, 'M', '𐳦'), + (0x10CA7, 'M', '𐳧'), + (0x10CA8, 'M', '𐳨'), + (0x10CA9, 'M', '𐳩'), + (0x10CAA, 'M', '𐳪'), + (0x10CAB, 'M', '𐳫'), + (0x10CAC, 'M', '𐳬'), + (0x10CAD, 'M', '𐳭'), + (0x10CAE, 'M', '𐳮'), + (0x10CAF, 'M', '𐳯'), + (0x10CB0, 'M', '𐳰'), + (0x10CB1, 'M', '𐳱'), + (0x10CB2, 'M', '𐳲'), (0x10CB3, 'X'), (0x10CC0, 'V'), (0x10CF3, 'X'), @@ -5907,42 +5906,42 @@ def _seg_56(): (0x11740, 'X'), (0x11800, 'V'), (0x1183C, 'X'), - (0x118A0, 'M', u'𑣀'), - (0x118A1, 'M', u'𑣁'), - (0x118A2, 'M', u'𑣂'), - (0x118A3, 'M', u'𑣃'), - (0x118A4, 'M', u'𑣄'), - (0x118A5, 'M', u'𑣅'), - (0x118A6, 'M', u'𑣆'), - (0x118A7, 'M', u'𑣇'), - (0x118A8, 'M', u'𑣈'), - (0x118A9, 'M', u'𑣉'), - (0x118AA, 'M', u'𑣊'), - (0x118AB, 'M', u'𑣋'), - (0x118AC, 'M', u'𑣌'), - (0x118AD, 'M', u'𑣍'), - (0x118AE, 'M', u'𑣎'), - (0x118AF, 'M', u'𑣏'), - (0x118B0, 'M', u'𑣐'), - (0x118B1, 'M', u'𑣑'), - (0x118B2, 'M', u'𑣒'), - (0x118B3, 'M', u'𑣓'), - (0x118B4, 'M', u'𑣔'), - (0x118B5, 'M', u'𑣕'), - (0x118B6, 'M', u'𑣖'), - (0x118B7, 'M', u'𑣗'), + (0x118A0, 'M', '𑣀'), + (0x118A1, 'M', '𑣁'), + (0x118A2, 'M', '𑣂'), + (0x118A3, 'M', '𑣃'), + (0x118A4, 'M', '𑣄'), + (0x118A5, 'M', '𑣅'), + (0x118A6, 'M', '𑣆'), + (0x118A7, 'M', '𑣇'), + (0x118A8, 'M', '𑣈'), + (0x118A9, 'M', '𑣉'), + (0x118AA, 'M', '𑣊'), + (0x118AB, 'M', '𑣋'), + (0x118AC, 'M', '𑣌'), + (0x118AD, 'M', '𑣍'), + (0x118AE, 'M', '𑣎'), + (0x118AF, 'M', '𑣏'), + (0x118B0, 'M', '𑣐'), + (0x118B1, 'M', '𑣑'), + (0x118B2, 'M', '𑣒'), + (0x118B3, 'M', '𑣓'), + (0x118B4, 'M', '𑣔'), + (0x118B5, 'M', '𑣕'), + (0x118B6, 'M', '𑣖'), + (0x118B7, 'M', '𑣗'), ] def _seg_57(): return [ - (0x118B8, 'M', u'𑣘'), - (0x118B9, 'M', u'𑣙'), - (0x118BA, 'M', u'𑣚'), - (0x118BB, 'M', u'𑣛'), - (0x118BC, 'M', u'𑣜'), - (0x118BD, 'M', u'𑣝'), - (0x118BE, 'M', u'𑣞'), - (0x118BF, 'M', u'𑣟'), + (0x118B8, 'M', '𑣘'), + (0x118B9, 'M', '𑣙'), + (0x118BA, 'M', '𑣚'), + (0x118BB, 'M', '𑣛'), + (0x118BC, 'M', '𑣜'), + (0x118BD, 'M', '𑣝'), + (0x118BE, 'M', '𑣞'), + (0x118BF, 'M', '𑣟'), (0x118C0, 'V'), (0x118F3, 'X'), (0x118FF, 'V'), @@ -6057,38 +6056,38 @@ def _seg_58(): (0x16B78, 'X'), (0x16B7D, 'V'), (0x16B90, 'X'), - (0x16E40, 'M', u'𖹠'), - (0x16E41, 'M', u'𖹡'), - (0x16E42, 'M', u'𖹢'), - (0x16E43, 'M', u'𖹣'), - (0x16E44, 'M', u'𖹤'), - (0x16E45, 'M', u'𖹥'), - (0x16E46, 'M', u'𖹦'), - (0x16E47, 'M', u'𖹧'), - (0x16E48, 'M', u'𖹨'), - (0x16E49, 'M', u'𖹩'), - (0x16E4A, 'M', u'𖹪'), - (0x16E4B, 'M', u'𖹫'), - (0x16E4C, 'M', u'𖹬'), - (0x16E4D, 'M', u'𖹭'), - (0x16E4E, 'M', u'𖹮'), - (0x16E4F, 'M', u'𖹯'), - (0x16E50, 'M', u'𖹰'), - (0x16E51, 'M', u'𖹱'), - (0x16E52, 'M', u'𖹲'), - (0x16E53, 'M', u'𖹳'), - (0x16E54, 'M', u'𖹴'), - (0x16E55, 'M', u'𖹵'), - (0x16E56, 'M', u'𖹶'), - (0x16E57, 'M', u'𖹷'), - (0x16E58, 'M', u'𖹸'), - (0x16E59, 'M', u'𖹹'), - (0x16E5A, 'M', u'𖹺'), - (0x16E5B, 'M', u'𖹻'), - (0x16E5C, 'M', u'𖹼'), - (0x16E5D, 'M', u'𖹽'), - (0x16E5E, 'M', u'𖹾'), - (0x16E5F, 'M', u'𖹿'), + (0x16E40, 'M', '𖹠'), + (0x16E41, 'M', '𖹡'), + (0x16E42, 'M', '𖹢'), + (0x16E43, 'M', '𖹣'), + (0x16E44, 'M', '𖹤'), + (0x16E45, 'M', '𖹥'), + (0x16E46, 'M', '𖹦'), + (0x16E47, 'M', '𖹧'), + (0x16E48, 'M', '𖹨'), + (0x16E49, 'M', '𖹩'), + (0x16E4A, 'M', '𖹪'), + (0x16E4B, 'M', '𖹫'), + (0x16E4C, 'M', '𖹬'), + (0x16E4D, 'M', '𖹭'), + (0x16E4E, 'M', '𖹮'), + (0x16E4F, 'M', '𖹯'), + (0x16E50, 'M', '𖹰'), + (0x16E51, 'M', '𖹱'), + (0x16E52, 'M', '𖹲'), + (0x16E53, 'M', '𖹳'), + (0x16E54, 'M', '𖹴'), + (0x16E55, 'M', '𖹵'), + (0x16E56, 'M', '𖹶'), + (0x16E57, 'M', '𖹷'), + (0x16E58, 'M', '𖹸'), + (0x16E59, 'M', '𖹹'), + (0x16E5A, 'M', '𖹺'), + (0x16E5B, 'M', '𖹻'), + (0x16E5C, 'M', '𖹼'), + (0x16E5D, 'M', '𖹽'), + (0x16E5E, 'M', '𖹾'), + (0x16E5F, 'M', '𖹿'), (0x16E60, 'V'), (0x16E9B, 'X'), (0x16F00, 'V'), @@ -6131,13 +6130,13 @@ def _seg_58(): (0x1D100, 'V'), (0x1D127, 'X'), (0x1D129, 'V'), - (0x1D15E, 'M', u'𝅗𝅥'), - (0x1D15F, 'M', u'𝅘𝅥'), - (0x1D160, 'M', u'𝅘𝅥𝅮'), - (0x1D161, 'M', u'𝅘𝅥𝅯'), - (0x1D162, 'M', u'𝅘𝅥𝅰'), - (0x1D163, 'M', u'𝅘𝅥𝅱'), - (0x1D164, 'M', u'𝅘𝅥𝅲'), + (0x1D15E, 'M', '𝅗𝅥'), + (0x1D15F, 'M', '𝅘𝅥'), + (0x1D160, 'M', '𝅘𝅥𝅮'), + (0x1D161, 'M', '𝅘𝅥𝅯'), + (0x1D162, 'M', '𝅘𝅥𝅰'), + (0x1D163, 'M', '𝅘𝅥𝅱'), + (0x1D164, 'M', '𝅘𝅥𝅲'), (0x1D165, 'V'), ] @@ -6145,12 +6144,12 @@ def _seg_59(): return [ (0x1D173, 'X'), (0x1D17B, 'V'), - (0x1D1BB, 'M', u'𝆹𝅥'), - (0x1D1BC, 'M', u'𝆺𝅥'), - (0x1D1BD, 'M', u'𝆹𝅥𝅮'), - (0x1D1BE, 'M', u'𝆺𝅥𝅮'), - (0x1D1BF, 'M', u'𝆹𝅥𝅯'), - (0x1D1C0, 'M', u'𝆺𝅥𝅯'), + (0x1D1BB, 'M', '𝆹𝅥'), + (0x1D1BC, 'M', '𝆺𝅥'), + (0x1D1BD, 'M', '𝆹𝅥𝅮'), + (0x1D1BE, 'M', '𝆺𝅥𝅮'), + (0x1D1BF, 'M', '𝆹𝅥𝅯'), + (0x1D1C0, 'M', '𝆺𝅥𝅯'), (0x1D1C1, 'V'), (0x1D1E9, 'X'), (0x1D200, 'V'), @@ -6161,1056 +6160,1056 @@ def _seg_59(): (0x1D357, 'X'), (0x1D360, 'V'), (0x1D379, 'X'), - (0x1D400, 'M', u'a'), - (0x1D401, 'M', u'b'), - (0x1D402, 'M', u'c'), - (0x1D403, 'M', u'd'), - (0x1D404, 'M', u'e'), - (0x1D405, 'M', u'f'), - (0x1D406, 'M', u'g'), - (0x1D407, 'M', u'h'), - (0x1D408, 'M', u'i'), - (0x1D409, 'M', u'j'), - (0x1D40A, 'M', u'k'), - (0x1D40B, 'M', u'l'), - (0x1D40C, 'M', u'm'), - (0x1D40D, 'M', u'n'), - (0x1D40E, 'M', u'o'), - (0x1D40F, 'M', u'p'), - (0x1D410, 'M', u'q'), - (0x1D411, 'M', u'r'), - (0x1D412, 'M', u's'), - (0x1D413, 'M', u't'), - (0x1D414, 'M', u'u'), - (0x1D415, 'M', u'v'), - (0x1D416, 'M', u'w'), - (0x1D417, 'M', u'x'), - (0x1D418, 'M', u'y'), - (0x1D419, 'M', u'z'), - (0x1D41A, 'M', u'a'), - (0x1D41B, 'M', u'b'), - (0x1D41C, 'M', u'c'), - (0x1D41D, 'M', u'd'), - (0x1D41E, 'M', u'e'), - (0x1D41F, 'M', u'f'), - (0x1D420, 'M', u'g'), - (0x1D421, 'M', u'h'), - (0x1D422, 'M', u'i'), - (0x1D423, 'M', u'j'), - (0x1D424, 'M', u'k'), - (0x1D425, 'M', u'l'), - (0x1D426, 'M', u'm'), - (0x1D427, 'M', u'n'), - (0x1D428, 'M', u'o'), - (0x1D429, 'M', u'p'), - (0x1D42A, 'M', u'q'), - (0x1D42B, 'M', u'r'), - (0x1D42C, 'M', u's'), - (0x1D42D, 'M', u't'), - (0x1D42E, 'M', u'u'), - (0x1D42F, 'M', u'v'), - (0x1D430, 'M', u'w'), - (0x1D431, 'M', u'x'), - (0x1D432, 'M', u'y'), - (0x1D433, 'M', u'z'), - (0x1D434, 'M', u'a'), - (0x1D435, 'M', u'b'), - (0x1D436, 'M', u'c'), - (0x1D437, 'M', u'd'), - (0x1D438, 'M', u'e'), - (0x1D439, 'M', u'f'), - (0x1D43A, 'M', u'g'), - (0x1D43B, 'M', u'h'), - (0x1D43C, 'M', u'i'), - (0x1D43D, 'M', u'j'), - (0x1D43E, 'M', u'k'), - (0x1D43F, 'M', u'l'), - (0x1D440, 'M', u'm'), - (0x1D441, 'M', u'n'), - (0x1D442, 'M', u'o'), - (0x1D443, 'M', u'p'), - (0x1D444, 'M', u'q'), - (0x1D445, 'M', u'r'), - (0x1D446, 'M', u's'), - (0x1D447, 'M', u't'), - (0x1D448, 'M', u'u'), - (0x1D449, 'M', u'v'), - (0x1D44A, 'M', u'w'), - (0x1D44B, 'M', u'x'), - (0x1D44C, 'M', u'y'), - (0x1D44D, 'M', u'z'), - (0x1D44E, 'M', u'a'), - (0x1D44F, 'M', u'b'), - (0x1D450, 'M', u'c'), - (0x1D451, 'M', u'd'), + (0x1D400, 'M', 'a'), + (0x1D401, 'M', 'b'), + (0x1D402, 'M', 'c'), + (0x1D403, 'M', 'd'), + (0x1D404, 'M', 'e'), + (0x1D405, 'M', 'f'), + (0x1D406, 'M', 'g'), + (0x1D407, 'M', 'h'), + (0x1D408, 'M', 'i'), + (0x1D409, 'M', 'j'), + (0x1D40A, 'M', 'k'), + (0x1D40B, 'M', 'l'), + (0x1D40C, 'M', 'm'), + (0x1D40D, 'M', 'n'), + (0x1D40E, 'M', 'o'), + (0x1D40F, 'M', 'p'), + (0x1D410, 'M', 'q'), + (0x1D411, 'M', 'r'), + (0x1D412, 'M', 's'), + (0x1D413, 'M', 't'), + (0x1D414, 'M', 'u'), + (0x1D415, 'M', 'v'), + (0x1D416, 'M', 'w'), + (0x1D417, 'M', 'x'), + (0x1D418, 'M', 'y'), + (0x1D419, 'M', 'z'), + (0x1D41A, 'M', 'a'), + (0x1D41B, 'M', 'b'), + (0x1D41C, 'M', 'c'), + (0x1D41D, 'M', 'd'), + (0x1D41E, 'M', 'e'), + (0x1D41F, 'M', 'f'), + (0x1D420, 'M', 'g'), + (0x1D421, 'M', 'h'), + (0x1D422, 'M', 'i'), + (0x1D423, 'M', 'j'), + (0x1D424, 'M', 'k'), + (0x1D425, 'M', 'l'), + (0x1D426, 'M', 'm'), + (0x1D427, 'M', 'n'), + (0x1D428, 'M', 'o'), + (0x1D429, 'M', 'p'), + (0x1D42A, 'M', 'q'), + (0x1D42B, 'M', 'r'), + (0x1D42C, 'M', 's'), + (0x1D42D, 'M', 't'), + (0x1D42E, 'M', 'u'), + (0x1D42F, 'M', 'v'), + (0x1D430, 'M', 'w'), + (0x1D431, 'M', 'x'), + (0x1D432, 'M', 'y'), + (0x1D433, 'M', 'z'), + (0x1D434, 'M', 'a'), + (0x1D435, 'M', 'b'), + (0x1D436, 'M', 'c'), + (0x1D437, 'M', 'd'), + (0x1D438, 'M', 'e'), + (0x1D439, 'M', 'f'), + (0x1D43A, 'M', 'g'), + (0x1D43B, 'M', 'h'), + (0x1D43C, 'M', 'i'), + (0x1D43D, 'M', 'j'), + (0x1D43E, 'M', 'k'), + (0x1D43F, 'M', 'l'), + (0x1D440, 'M', 'm'), + (0x1D441, 'M', 'n'), + (0x1D442, 'M', 'o'), + (0x1D443, 'M', 'p'), + (0x1D444, 'M', 'q'), + (0x1D445, 'M', 'r'), + (0x1D446, 'M', 's'), + (0x1D447, 'M', 't'), + (0x1D448, 'M', 'u'), + (0x1D449, 'M', 'v'), + (0x1D44A, 'M', 'w'), + (0x1D44B, 'M', 'x'), + (0x1D44C, 'M', 'y'), + (0x1D44D, 'M', 'z'), + (0x1D44E, 'M', 'a'), + (0x1D44F, 'M', 'b'), + (0x1D450, 'M', 'c'), + (0x1D451, 'M', 'd'), ] def _seg_60(): return [ - (0x1D452, 'M', u'e'), - (0x1D453, 'M', u'f'), - (0x1D454, 'M', u'g'), + (0x1D452, 'M', 'e'), + (0x1D453, 'M', 'f'), + (0x1D454, 'M', 'g'), (0x1D455, 'X'), - (0x1D456, 'M', u'i'), - (0x1D457, 'M', u'j'), - (0x1D458, 'M', u'k'), - (0x1D459, 'M', u'l'), - (0x1D45A, 'M', u'm'), - (0x1D45B, 'M', u'n'), - (0x1D45C, 'M', u'o'), - (0x1D45D, 'M', u'p'), - (0x1D45E, 'M', u'q'), - (0x1D45F, 'M', u'r'), - (0x1D460, 'M', u's'), - (0x1D461, 'M', u't'), - (0x1D462, 'M', u'u'), - (0x1D463, 'M', u'v'), - (0x1D464, 'M', u'w'), - (0x1D465, 'M', u'x'), - (0x1D466, 'M', u'y'), - (0x1D467, 'M', u'z'), - (0x1D468, 'M', u'a'), - (0x1D469, 'M', u'b'), - (0x1D46A, 'M', u'c'), - (0x1D46B, 'M', u'd'), - (0x1D46C, 'M', u'e'), - (0x1D46D, 'M', u'f'), - (0x1D46E, 'M', u'g'), - (0x1D46F, 'M', u'h'), - (0x1D470, 'M', u'i'), - (0x1D471, 'M', u'j'), - (0x1D472, 'M', u'k'), - (0x1D473, 'M', u'l'), - (0x1D474, 'M', u'm'), - (0x1D475, 'M', u'n'), - (0x1D476, 'M', u'o'), - (0x1D477, 'M', u'p'), - (0x1D478, 'M', u'q'), - (0x1D479, 'M', u'r'), - (0x1D47A, 'M', u's'), - (0x1D47B, 'M', u't'), - (0x1D47C, 'M', u'u'), - (0x1D47D, 'M', u'v'), - (0x1D47E, 'M', u'w'), - (0x1D47F, 'M', u'x'), - (0x1D480, 'M', u'y'), - (0x1D481, 'M', u'z'), - (0x1D482, 'M', u'a'), - (0x1D483, 'M', u'b'), - (0x1D484, 'M', u'c'), - (0x1D485, 'M', u'd'), - (0x1D486, 'M', u'e'), - (0x1D487, 'M', u'f'), - (0x1D488, 'M', u'g'), - (0x1D489, 'M', u'h'), - (0x1D48A, 'M', u'i'), - (0x1D48B, 'M', u'j'), - (0x1D48C, 'M', u'k'), - (0x1D48D, 'M', u'l'), - (0x1D48E, 'M', u'm'), - (0x1D48F, 'M', u'n'), - (0x1D490, 'M', u'o'), - (0x1D491, 'M', u'p'), - (0x1D492, 'M', u'q'), - (0x1D493, 'M', u'r'), - (0x1D494, 'M', u's'), - (0x1D495, 'M', u't'), - (0x1D496, 'M', u'u'), - (0x1D497, 'M', u'v'), - (0x1D498, 'M', u'w'), - (0x1D499, 'M', u'x'), - (0x1D49A, 'M', u'y'), - (0x1D49B, 'M', u'z'), - (0x1D49C, 'M', u'a'), + (0x1D456, 'M', 'i'), + (0x1D457, 'M', 'j'), + (0x1D458, 'M', 'k'), + (0x1D459, 'M', 'l'), + (0x1D45A, 'M', 'm'), + (0x1D45B, 'M', 'n'), + (0x1D45C, 'M', 'o'), + (0x1D45D, 'M', 'p'), + (0x1D45E, 'M', 'q'), + (0x1D45F, 'M', 'r'), + (0x1D460, 'M', 's'), + (0x1D461, 'M', 't'), + (0x1D462, 'M', 'u'), + (0x1D463, 'M', 'v'), + (0x1D464, 'M', 'w'), + (0x1D465, 'M', 'x'), + (0x1D466, 'M', 'y'), + (0x1D467, 'M', 'z'), + (0x1D468, 'M', 'a'), + (0x1D469, 'M', 'b'), + (0x1D46A, 'M', 'c'), + (0x1D46B, 'M', 'd'), + (0x1D46C, 'M', 'e'), + (0x1D46D, 'M', 'f'), + (0x1D46E, 'M', 'g'), + (0x1D46F, 'M', 'h'), + (0x1D470, 'M', 'i'), + (0x1D471, 'M', 'j'), + (0x1D472, 'M', 'k'), + (0x1D473, 'M', 'l'), + (0x1D474, 'M', 'm'), + (0x1D475, 'M', 'n'), + (0x1D476, 'M', 'o'), + (0x1D477, 'M', 'p'), + (0x1D478, 'M', 'q'), + (0x1D479, 'M', 'r'), + (0x1D47A, 'M', 's'), + (0x1D47B, 'M', 't'), + (0x1D47C, 'M', 'u'), + (0x1D47D, 'M', 'v'), + (0x1D47E, 'M', 'w'), + (0x1D47F, 'M', 'x'), + (0x1D480, 'M', 'y'), + (0x1D481, 'M', 'z'), + (0x1D482, 'M', 'a'), + (0x1D483, 'M', 'b'), + (0x1D484, 'M', 'c'), + (0x1D485, 'M', 'd'), + (0x1D486, 'M', 'e'), + (0x1D487, 'M', 'f'), + (0x1D488, 'M', 'g'), + (0x1D489, 'M', 'h'), + (0x1D48A, 'M', 'i'), + (0x1D48B, 'M', 'j'), + (0x1D48C, 'M', 'k'), + (0x1D48D, 'M', 'l'), + (0x1D48E, 'M', 'm'), + (0x1D48F, 'M', 'n'), + (0x1D490, 'M', 'o'), + (0x1D491, 'M', 'p'), + (0x1D492, 'M', 'q'), + (0x1D493, 'M', 'r'), + (0x1D494, 'M', 's'), + (0x1D495, 'M', 't'), + (0x1D496, 'M', 'u'), + (0x1D497, 'M', 'v'), + (0x1D498, 'M', 'w'), + (0x1D499, 'M', 'x'), + (0x1D49A, 'M', 'y'), + (0x1D49B, 'M', 'z'), + (0x1D49C, 'M', 'a'), (0x1D49D, 'X'), - (0x1D49E, 'M', u'c'), - (0x1D49F, 'M', u'd'), + (0x1D49E, 'M', 'c'), + (0x1D49F, 'M', 'd'), (0x1D4A0, 'X'), - (0x1D4A2, 'M', u'g'), + (0x1D4A2, 'M', 'g'), (0x1D4A3, 'X'), - (0x1D4A5, 'M', u'j'), - (0x1D4A6, 'M', u'k'), + (0x1D4A5, 'M', 'j'), + (0x1D4A6, 'M', 'k'), (0x1D4A7, 'X'), - (0x1D4A9, 'M', u'n'), - (0x1D4AA, 'M', u'o'), - (0x1D4AB, 'M', u'p'), - (0x1D4AC, 'M', u'q'), + (0x1D4A9, 'M', 'n'), + (0x1D4AA, 'M', 'o'), + (0x1D4AB, 'M', 'p'), + (0x1D4AC, 'M', 'q'), (0x1D4AD, 'X'), - (0x1D4AE, 'M', u's'), - (0x1D4AF, 'M', u't'), - (0x1D4B0, 'M', u'u'), - (0x1D4B1, 'M', u'v'), - (0x1D4B2, 'M', u'w'), - (0x1D4B3, 'M', u'x'), - (0x1D4B4, 'M', u'y'), - (0x1D4B5, 'M', u'z'), - (0x1D4B6, 'M', u'a'), - (0x1D4B7, 'M', u'b'), - (0x1D4B8, 'M', u'c'), + (0x1D4AE, 'M', 's'), + (0x1D4AF, 'M', 't'), + (0x1D4B0, 'M', 'u'), + (0x1D4B1, 'M', 'v'), + (0x1D4B2, 'M', 'w'), + (0x1D4B3, 'M', 'x'), + (0x1D4B4, 'M', 'y'), + (0x1D4B5, 'M', 'z'), + (0x1D4B6, 'M', 'a'), + (0x1D4B7, 'M', 'b'), + (0x1D4B8, 'M', 'c'), ] def _seg_61(): return [ - (0x1D4B9, 'M', u'd'), + (0x1D4B9, 'M', 'd'), (0x1D4BA, 'X'), - (0x1D4BB, 'M', u'f'), + (0x1D4BB, 'M', 'f'), (0x1D4BC, 'X'), - (0x1D4BD, 'M', u'h'), - (0x1D4BE, 'M', u'i'), - (0x1D4BF, 'M', u'j'), - (0x1D4C0, 'M', u'k'), - (0x1D4C1, 'M', u'l'), - (0x1D4C2, 'M', u'm'), - (0x1D4C3, 'M', u'n'), + (0x1D4BD, 'M', 'h'), + (0x1D4BE, 'M', 'i'), + (0x1D4BF, 'M', 'j'), + (0x1D4C0, 'M', 'k'), + (0x1D4C1, 'M', 'l'), + (0x1D4C2, 'M', 'm'), + (0x1D4C3, 'M', 'n'), (0x1D4C4, 'X'), - (0x1D4C5, 'M', u'p'), - (0x1D4C6, 'M', u'q'), - (0x1D4C7, 'M', u'r'), - (0x1D4C8, 'M', u's'), - (0x1D4C9, 'M', u't'), - (0x1D4CA, 'M', u'u'), - (0x1D4CB, 'M', u'v'), - (0x1D4CC, 'M', u'w'), - (0x1D4CD, 'M', u'x'), - (0x1D4CE, 'M', u'y'), - (0x1D4CF, 'M', u'z'), - (0x1D4D0, 'M', u'a'), - (0x1D4D1, 'M', u'b'), - (0x1D4D2, 'M', u'c'), - (0x1D4D3, 'M', u'd'), - (0x1D4D4, 'M', u'e'), - (0x1D4D5, 'M', u'f'), - (0x1D4D6, 'M', u'g'), - (0x1D4D7, 'M', u'h'), - (0x1D4D8, 'M', u'i'), - (0x1D4D9, 'M', u'j'), - (0x1D4DA, 'M', u'k'), - (0x1D4DB, 'M', u'l'), - (0x1D4DC, 'M', u'm'), - (0x1D4DD, 'M', u'n'), - (0x1D4DE, 'M', u'o'), - (0x1D4DF, 'M', u'p'), - (0x1D4E0, 'M', u'q'), - (0x1D4E1, 'M', u'r'), - (0x1D4E2, 'M', u's'), - (0x1D4E3, 'M', u't'), - (0x1D4E4, 'M', u'u'), - (0x1D4E5, 'M', u'v'), - (0x1D4E6, 'M', u'w'), - (0x1D4E7, 'M', u'x'), - (0x1D4E8, 'M', u'y'), - (0x1D4E9, 'M', u'z'), - (0x1D4EA, 'M', u'a'), - (0x1D4EB, 'M', u'b'), - (0x1D4EC, 'M', u'c'), - (0x1D4ED, 'M', u'd'), - (0x1D4EE, 'M', u'e'), - (0x1D4EF, 'M', u'f'), - (0x1D4F0, 'M', u'g'), - (0x1D4F1, 'M', u'h'), - (0x1D4F2, 'M', u'i'), - (0x1D4F3, 'M', u'j'), - (0x1D4F4, 'M', u'k'), - (0x1D4F5, 'M', u'l'), - (0x1D4F6, 'M', u'm'), - (0x1D4F7, 'M', u'n'), - (0x1D4F8, 'M', u'o'), - (0x1D4F9, 'M', u'p'), - (0x1D4FA, 'M', u'q'), - (0x1D4FB, 'M', u'r'), - (0x1D4FC, 'M', u's'), - (0x1D4FD, 'M', u't'), - (0x1D4FE, 'M', u'u'), - (0x1D4FF, 'M', u'v'), - (0x1D500, 'M', u'w'), - (0x1D501, 'M', u'x'), - (0x1D502, 'M', u'y'), - (0x1D503, 'M', u'z'), - (0x1D504, 'M', u'a'), - (0x1D505, 'M', u'b'), + (0x1D4C5, 'M', 'p'), + (0x1D4C6, 'M', 'q'), + (0x1D4C7, 'M', 'r'), + (0x1D4C8, 'M', 's'), + (0x1D4C9, 'M', 't'), + (0x1D4CA, 'M', 'u'), + (0x1D4CB, 'M', 'v'), + (0x1D4CC, 'M', 'w'), + (0x1D4CD, 'M', 'x'), + (0x1D4CE, 'M', 'y'), + (0x1D4CF, 'M', 'z'), + (0x1D4D0, 'M', 'a'), + (0x1D4D1, 'M', 'b'), + (0x1D4D2, 'M', 'c'), + (0x1D4D3, 'M', 'd'), + (0x1D4D4, 'M', 'e'), + (0x1D4D5, 'M', 'f'), + (0x1D4D6, 'M', 'g'), + (0x1D4D7, 'M', 'h'), + (0x1D4D8, 'M', 'i'), + (0x1D4D9, 'M', 'j'), + (0x1D4DA, 'M', 'k'), + (0x1D4DB, 'M', 'l'), + (0x1D4DC, 'M', 'm'), + (0x1D4DD, 'M', 'n'), + (0x1D4DE, 'M', 'o'), + (0x1D4DF, 'M', 'p'), + (0x1D4E0, 'M', 'q'), + (0x1D4E1, 'M', 'r'), + (0x1D4E2, 'M', 's'), + (0x1D4E3, 'M', 't'), + (0x1D4E4, 'M', 'u'), + (0x1D4E5, 'M', 'v'), + (0x1D4E6, 'M', 'w'), + (0x1D4E7, 'M', 'x'), + (0x1D4E8, 'M', 'y'), + (0x1D4E9, 'M', 'z'), + (0x1D4EA, 'M', 'a'), + (0x1D4EB, 'M', 'b'), + (0x1D4EC, 'M', 'c'), + (0x1D4ED, 'M', 'd'), + (0x1D4EE, 'M', 'e'), + (0x1D4EF, 'M', 'f'), + (0x1D4F0, 'M', 'g'), + (0x1D4F1, 'M', 'h'), + (0x1D4F2, 'M', 'i'), + (0x1D4F3, 'M', 'j'), + (0x1D4F4, 'M', 'k'), + (0x1D4F5, 'M', 'l'), + (0x1D4F6, 'M', 'm'), + (0x1D4F7, 'M', 'n'), + (0x1D4F8, 'M', 'o'), + (0x1D4F9, 'M', 'p'), + (0x1D4FA, 'M', 'q'), + (0x1D4FB, 'M', 'r'), + (0x1D4FC, 'M', 's'), + (0x1D4FD, 'M', 't'), + (0x1D4FE, 'M', 'u'), + (0x1D4FF, 'M', 'v'), + (0x1D500, 'M', 'w'), + (0x1D501, 'M', 'x'), + (0x1D502, 'M', 'y'), + (0x1D503, 'M', 'z'), + (0x1D504, 'M', 'a'), + (0x1D505, 'M', 'b'), (0x1D506, 'X'), - (0x1D507, 'M', u'd'), - (0x1D508, 'M', u'e'), - (0x1D509, 'M', u'f'), - (0x1D50A, 'M', u'g'), + (0x1D507, 'M', 'd'), + (0x1D508, 'M', 'e'), + (0x1D509, 'M', 'f'), + (0x1D50A, 'M', 'g'), (0x1D50B, 'X'), - (0x1D50D, 'M', u'j'), - (0x1D50E, 'M', u'k'), - (0x1D50F, 'M', u'l'), - (0x1D510, 'M', u'm'), - (0x1D511, 'M', u'n'), - (0x1D512, 'M', u'o'), - (0x1D513, 'M', u'p'), - (0x1D514, 'M', u'q'), + (0x1D50D, 'M', 'j'), + (0x1D50E, 'M', 'k'), + (0x1D50F, 'M', 'l'), + (0x1D510, 'M', 'm'), + (0x1D511, 'M', 'n'), + (0x1D512, 'M', 'o'), + (0x1D513, 'M', 'p'), + (0x1D514, 'M', 'q'), (0x1D515, 'X'), - (0x1D516, 'M', u's'), - (0x1D517, 'M', u't'), - (0x1D518, 'M', u'u'), - (0x1D519, 'M', u'v'), - (0x1D51A, 'M', u'w'), - (0x1D51B, 'M', u'x'), - (0x1D51C, 'M', u'y'), + (0x1D516, 'M', 's'), + (0x1D517, 'M', 't'), + (0x1D518, 'M', 'u'), + (0x1D519, 'M', 'v'), + (0x1D51A, 'M', 'w'), + (0x1D51B, 'M', 'x'), + (0x1D51C, 'M', 'y'), (0x1D51D, 'X'), ] def _seg_62(): return [ - (0x1D51E, 'M', u'a'), - (0x1D51F, 'M', u'b'), - (0x1D520, 'M', u'c'), - (0x1D521, 'M', u'd'), - (0x1D522, 'M', u'e'), - (0x1D523, 'M', u'f'), - (0x1D524, 'M', u'g'), - (0x1D525, 'M', u'h'), - (0x1D526, 'M', u'i'), - (0x1D527, 'M', u'j'), - (0x1D528, 'M', u'k'), - (0x1D529, 'M', u'l'), - (0x1D52A, 'M', u'm'), - (0x1D52B, 'M', u'n'), - (0x1D52C, 'M', u'o'), - (0x1D52D, 'M', u'p'), - (0x1D52E, 'M', u'q'), - (0x1D52F, 'M', u'r'), - (0x1D530, 'M', u's'), - (0x1D531, 'M', u't'), - (0x1D532, 'M', u'u'), - (0x1D533, 'M', u'v'), - (0x1D534, 'M', u'w'), - (0x1D535, 'M', u'x'), - (0x1D536, 'M', u'y'), - (0x1D537, 'M', u'z'), - (0x1D538, 'M', u'a'), - (0x1D539, 'M', u'b'), + (0x1D51E, 'M', 'a'), + (0x1D51F, 'M', 'b'), + (0x1D520, 'M', 'c'), + (0x1D521, 'M', 'd'), + (0x1D522, 'M', 'e'), + (0x1D523, 'M', 'f'), + (0x1D524, 'M', 'g'), + (0x1D525, 'M', 'h'), + (0x1D526, 'M', 'i'), + (0x1D527, 'M', 'j'), + (0x1D528, 'M', 'k'), + (0x1D529, 'M', 'l'), + (0x1D52A, 'M', 'm'), + (0x1D52B, 'M', 'n'), + (0x1D52C, 'M', 'o'), + (0x1D52D, 'M', 'p'), + (0x1D52E, 'M', 'q'), + (0x1D52F, 'M', 'r'), + (0x1D530, 'M', 's'), + (0x1D531, 'M', 't'), + (0x1D532, 'M', 'u'), + (0x1D533, 'M', 'v'), + (0x1D534, 'M', 'w'), + (0x1D535, 'M', 'x'), + (0x1D536, 'M', 'y'), + (0x1D537, 'M', 'z'), + (0x1D538, 'M', 'a'), + (0x1D539, 'M', 'b'), (0x1D53A, 'X'), - (0x1D53B, 'M', u'd'), - (0x1D53C, 'M', u'e'), - (0x1D53D, 'M', u'f'), - (0x1D53E, 'M', u'g'), + (0x1D53B, 'M', 'd'), + (0x1D53C, 'M', 'e'), + (0x1D53D, 'M', 'f'), + (0x1D53E, 'M', 'g'), (0x1D53F, 'X'), - (0x1D540, 'M', u'i'), - (0x1D541, 'M', u'j'), - (0x1D542, 'M', u'k'), - (0x1D543, 'M', u'l'), - (0x1D544, 'M', u'm'), + (0x1D540, 'M', 'i'), + (0x1D541, 'M', 'j'), + (0x1D542, 'M', 'k'), + (0x1D543, 'M', 'l'), + (0x1D544, 'M', 'm'), (0x1D545, 'X'), - (0x1D546, 'M', u'o'), + (0x1D546, 'M', 'o'), (0x1D547, 'X'), - (0x1D54A, 'M', u's'), - (0x1D54B, 'M', u't'), - (0x1D54C, 'M', u'u'), - (0x1D54D, 'M', u'v'), - (0x1D54E, 'M', u'w'), - (0x1D54F, 'M', u'x'), - (0x1D550, 'M', u'y'), + (0x1D54A, 'M', 's'), + (0x1D54B, 'M', 't'), + (0x1D54C, 'M', 'u'), + (0x1D54D, 'M', 'v'), + (0x1D54E, 'M', 'w'), + (0x1D54F, 'M', 'x'), + (0x1D550, 'M', 'y'), (0x1D551, 'X'), - (0x1D552, 'M', u'a'), - (0x1D553, 'M', u'b'), - (0x1D554, 'M', u'c'), - (0x1D555, 'M', u'd'), - (0x1D556, 'M', u'e'), - (0x1D557, 'M', u'f'), - (0x1D558, 'M', u'g'), - (0x1D559, 'M', u'h'), - (0x1D55A, 'M', u'i'), - (0x1D55B, 'M', u'j'), - (0x1D55C, 'M', u'k'), - (0x1D55D, 'M', u'l'), - (0x1D55E, 'M', u'm'), - (0x1D55F, 'M', u'n'), - (0x1D560, 'M', u'o'), - (0x1D561, 'M', u'p'), - (0x1D562, 'M', u'q'), - (0x1D563, 'M', u'r'), - (0x1D564, 'M', u's'), - (0x1D565, 'M', u't'), - (0x1D566, 'M', u'u'), - (0x1D567, 'M', u'v'), - (0x1D568, 'M', u'w'), - (0x1D569, 'M', u'x'), - (0x1D56A, 'M', u'y'), - (0x1D56B, 'M', u'z'), - (0x1D56C, 'M', u'a'), - (0x1D56D, 'M', u'b'), - (0x1D56E, 'M', u'c'), - (0x1D56F, 'M', u'd'), - (0x1D570, 'M', u'e'), - (0x1D571, 'M', u'f'), - (0x1D572, 'M', u'g'), - (0x1D573, 'M', u'h'), - (0x1D574, 'M', u'i'), - (0x1D575, 'M', u'j'), - (0x1D576, 'M', u'k'), - (0x1D577, 'M', u'l'), - (0x1D578, 'M', u'm'), - (0x1D579, 'M', u'n'), - (0x1D57A, 'M', u'o'), - (0x1D57B, 'M', u'p'), - (0x1D57C, 'M', u'q'), - (0x1D57D, 'M', u'r'), - (0x1D57E, 'M', u's'), - (0x1D57F, 'M', u't'), - (0x1D580, 'M', u'u'), - (0x1D581, 'M', u'v'), - (0x1D582, 'M', u'w'), - (0x1D583, 'M', u'x'), + (0x1D552, 'M', 'a'), + (0x1D553, 'M', 'b'), + (0x1D554, 'M', 'c'), + (0x1D555, 'M', 'd'), + (0x1D556, 'M', 'e'), + (0x1D557, 'M', 'f'), + (0x1D558, 'M', 'g'), + (0x1D559, 'M', 'h'), + (0x1D55A, 'M', 'i'), + (0x1D55B, 'M', 'j'), + (0x1D55C, 'M', 'k'), + (0x1D55D, 'M', 'l'), + (0x1D55E, 'M', 'm'), + (0x1D55F, 'M', 'n'), + (0x1D560, 'M', 'o'), + (0x1D561, 'M', 'p'), + (0x1D562, 'M', 'q'), + (0x1D563, 'M', 'r'), + (0x1D564, 'M', 's'), + (0x1D565, 'M', 't'), + (0x1D566, 'M', 'u'), + (0x1D567, 'M', 'v'), + (0x1D568, 'M', 'w'), + (0x1D569, 'M', 'x'), + (0x1D56A, 'M', 'y'), + (0x1D56B, 'M', 'z'), + (0x1D56C, 'M', 'a'), + (0x1D56D, 'M', 'b'), + (0x1D56E, 'M', 'c'), + (0x1D56F, 'M', 'd'), + (0x1D570, 'M', 'e'), + (0x1D571, 'M', 'f'), + (0x1D572, 'M', 'g'), + (0x1D573, 'M', 'h'), + (0x1D574, 'M', 'i'), + (0x1D575, 'M', 'j'), + (0x1D576, 'M', 'k'), + (0x1D577, 'M', 'l'), + (0x1D578, 'M', 'm'), + (0x1D579, 'M', 'n'), + (0x1D57A, 'M', 'o'), + (0x1D57B, 'M', 'p'), + (0x1D57C, 'M', 'q'), + (0x1D57D, 'M', 'r'), + (0x1D57E, 'M', 's'), + (0x1D57F, 'M', 't'), + (0x1D580, 'M', 'u'), + (0x1D581, 'M', 'v'), + (0x1D582, 'M', 'w'), + (0x1D583, 'M', 'x'), ] def _seg_63(): return [ - (0x1D584, 'M', u'y'), - (0x1D585, 'M', u'z'), - (0x1D586, 'M', u'a'), - (0x1D587, 'M', u'b'), - (0x1D588, 'M', u'c'), - (0x1D589, 'M', u'd'), - (0x1D58A, 'M', u'e'), - (0x1D58B, 'M', u'f'), - (0x1D58C, 'M', u'g'), - (0x1D58D, 'M', u'h'), - (0x1D58E, 'M', u'i'), - (0x1D58F, 'M', u'j'), - (0x1D590, 'M', u'k'), - (0x1D591, 'M', u'l'), - (0x1D592, 'M', u'm'), - (0x1D593, 'M', u'n'), - (0x1D594, 'M', u'o'), - (0x1D595, 'M', u'p'), - (0x1D596, 'M', u'q'), - (0x1D597, 'M', u'r'), - (0x1D598, 'M', u's'), - (0x1D599, 'M', u't'), - (0x1D59A, 'M', u'u'), - (0x1D59B, 'M', u'v'), - (0x1D59C, 'M', u'w'), - (0x1D59D, 'M', u'x'), - (0x1D59E, 'M', u'y'), - (0x1D59F, 'M', u'z'), - (0x1D5A0, 'M', u'a'), - (0x1D5A1, 'M', u'b'), - (0x1D5A2, 'M', u'c'), - (0x1D5A3, 'M', u'd'), - (0x1D5A4, 'M', u'e'), - (0x1D5A5, 'M', u'f'), - (0x1D5A6, 'M', u'g'), - (0x1D5A7, 'M', u'h'), - (0x1D5A8, 'M', u'i'), - (0x1D5A9, 'M', u'j'), - (0x1D5AA, 'M', u'k'), - (0x1D5AB, 'M', u'l'), - (0x1D5AC, 'M', u'm'), - (0x1D5AD, 'M', u'n'), - (0x1D5AE, 'M', u'o'), - (0x1D5AF, 'M', u'p'), - (0x1D5B0, 'M', u'q'), - (0x1D5B1, 'M', u'r'), - (0x1D5B2, 'M', u's'), - (0x1D5B3, 'M', u't'), - (0x1D5B4, 'M', u'u'), - (0x1D5B5, 'M', u'v'), - (0x1D5B6, 'M', u'w'), - (0x1D5B7, 'M', u'x'), - (0x1D5B8, 'M', u'y'), - (0x1D5B9, 'M', u'z'), - (0x1D5BA, 'M', u'a'), - (0x1D5BB, 'M', u'b'), - (0x1D5BC, 'M', u'c'), - (0x1D5BD, 'M', u'd'), - (0x1D5BE, 'M', u'e'), - (0x1D5BF, 'M', u'f'), - (0x1D5C0, 'M', u'g'), - (0x1D5C1, 'M', u'h'), - (0x1D5C2, 'M', u'i'), - (0x1D5C3, 'M', u'j'), - (0x1D5C4, 'M', u'k'), - (0x1D5C5, 'M', u'l'), - (0x1D5C6, 'M', u'm'), - (0x1D5C7, 'M', u'n'), - (0x1D5C8, 'M', u'o'), - (0x1D5C9, 'M', u'p'), - (0x1D5CA, 'M', u'q'), - (0x1D5CB, 'M', u'r'), - (0x1D5CC, 'M', u's'), - (0x1D5CD, 'M', u't'), - (0x1D5CE, 'M', u'u'), - (0x1D5CF, 'M', u'v'), - (0x1D5D0, 'M', u'w'), - (0x1D5D1, 'M', u'x'), - (0x1D5D2, 'M', u'y'), - (0x1D5D3, 'M', u'z'), - (0x1D5D4, 'M', u'a'), - (0x1D5D5, 'M', u'b'), - (0x1D5D6, 'M', u'c'), - (0x1D5D7, 'M', u'd'), - (0x1D5D8, 'M', u'e'), - (0x1D5D9, 'M', u'f'), - (0x1D5DA, 'M', u'g'), - (0x1D5DB, 'M', u'h'), - (0x1D5DC, 'M', u'i'), - (0x1D5DD, 'M', u'j'), - (0x1D5DE, 'M', u'k'), - (0x1D5DF, 'M', u'l'), - (0x1D5E0, 'M', u'm'), - (0x1D5E1, 'M', u'n'), - (0x1D5E2, 'M', u'o'), - (0x1D5E3, 'M', u'p'), - (0x1D5E4, 'M', u'q'), - (0x1D5E5, 'M', u'r'), - (0x1D5E6, 'M', u's'), - (0x1D5E7, 'M', u't'), + (0x1D584, 'M', 'y'), + (0x1D585, 'M', 'z'), + (0x1D586, 'M', 'a'), + (0x1D587, 'M', 'b'), + (0x1D588, 'M', 'c'), + (0x1D589, 'M', 'd'), + (0x1D58A, 'M', 'e'), + (0x1D58B, 'M', 'f'), + (0x1D58C, 'M', 'g'), + (0x1D58D, 'M', 'h'), + (0x1D58E, 'M', 'i'), + (0x1D58F, 'M', 'j'), + (0x1D590, 'M', 'k'), + (0x1D591, 'M', 'l'), + (0x1D592, 'M', 'm'), + (0x1D593, 'M', 'n'), + (0x1D594, 'M', 'o'), + (0x1D595, 'M', 'p'), + (0x1D596, 'M', 'q'), + (0x1D597, 'M', 'r'), + (0x1D598, 'M', 's'), + (0x1D599, 'M', 't'), + (0x1D59A, 'M', 'u'), + (0x1D59B, 'M', 'v'), + (0x1D59C, 'M', 'w'), + (0x1D59D, 'M', 'x'), + (0x1D59E, 'M', 'y'), + (0x1D59F, 'M', 'z'), + (0x1D5A0, 'M', 'a'), + (0x1D5A1, 'M', 'b'), + (0x1D5A2, 'M', 'c'), + (0x1D5A3, 'M', 'd'), + (0x1D5A4, 'M', 'e'), + (0x1D5A5, 'M', 'f'), + (0x1D5A6, 'M', 'g'), + (0x1D5A7, 'M', 'h'), + (0x1D5A8, 'M', 'i'), + (0x1D5A9, 'M', 'j'), + (0x1D5AA, 'M', 'k'), + (0x1D5AB, 'M', 'l'), + (0x1D5AC, 'M', 'm'), + (0x1D5AD, 'M', 'n'), + (0x1D5AE, 'M', 'o'), + (0x1D5AF, 'M', 'p'), + (0x1D5B0, 'M', 'q'), + (0x1D5B1, 'M', 'r'), + (0x1D5B2, 'M', 's'), + (0x1D5B3, 'M', 't'), + (0x1D5B4, 'M', 'u'), + (0x1D5B5, 'M', 'v'), + (0x1D5B6, 'M', 'w'), + (0x1D5B7, 'M', 'x'), + (0x1D5B8, 'M', 'y'), + (0x1D5B9, 'M', 'z'), + (0x1D5BA, 'M', 'a'), + (0x1D5BB, 'M', 'b'), + (0x1D5BC, 'M', 'c'), + (0x1D5BD, 'M', 'd'), + (0x1D5BE, 'M', 'e'), + (0x1D5BF, 'M', 'f'), + (0x1D5C0, 'M', 'g'), + (0x1D5C1, 'M', 'h'), + (0x1D5C2, 'M', 'i'), + (0x1D5C3, 'M', 'j'), + (0x1D5C4, 'M', 'k'), + (0x1D5C5, 'M', 'l'), + (0x1D5C6, 'M', 'm'), + (0x1D5C7, 'M', 'n'), + (0x1D5C8, 'M', 'o'), + (0x1D5C9, 'M', 'p'), + (0x1D5CA, 'M', 'q'), + (0x1D5CB, 'M', 'r'), + (0x1D5CC, 'M', 's'), + (0x1D5CD, 'M', 't'), + (0x1D5CE, 'M', 'u'), + (0x1D5CF, 'M', 'v'), + (0x1D5D0, 'M', 'w'), + (0x1D5D1, 'M', 'x'), + (0x1D5D2, 'M', 'y'), + (0x1D5D3, 'M', 'z'), + (0x1D5D4, 'M', 'a'), + (0x1D5D5, 'M', 'b'), + (0x1D5D6, 'M', 'c'), + (0x1D5D7, 'M', 'd'), + (0x1D5D8, 'M', 'e'), + (0x1D5D9, 'M', 'f'), + (0x1D5DA, 'M', 'g'), + (0x1D5DB, 'M', 'h'), + (0x1D5DC, 'M', 'i'), + (0x1D5DD, 'M', 'j'), + (0x1D5DE, 'M', 'k'), + (0x1D5DF, 'M', 'l'), + (0x1D5E0, 'M', 'm'), + (0x1D5E1, 'M', 'n'), + (0x1D5E2, 'M', 'o'), + (0x1D5E3, 'M', 'p'), + (0x1D5E4, 'M', 'q'), + (0x1D5E5, 'M', 'r'), + (0x1D5E6, 'M', 's'), + (0x1D5E7, 'M', 't'), ] def _seg_64(): return [ - (0x1D5E8, 'M', u'u'), - (0x1D5E9, 'M', u'v'), - (0x1D5EA, 'M', u'w'), - (0x1D5EB, 'M', u'x'), - (0x1D5EC, 'M', u'y'), - (0x1D5ED, 'M', u'z'), - (0x1D5EE, 'M', u'a'), - (0x1D5EF, 'M', u'b'), - (0x1D5F0, 'M', u'c'), - (0x1D5F1, 'M', u'd'), - (0x1D5F2, 'M', u'e'), - (0x1D5F3, 'M', u'f'), - (0x1D5F4, 'M', u'g'), - (0x1D5F5, 'M', u'h'), - (0x1D5F6, 'M', u'i'), - (0x1D5F7, 'M', u'j'), - (0x1D5F8, 'M', u'k'), - (0x1D5F9, 'M', u'l'), - (0x1D5FA, 'M', u'm'), - (0x1D5FB, 'M', u'n'), - (0x1D5FC, 'M', u'o'), - (0x1D5FD, 'M', u'p'), - (0x1D5FE, 'M', u'q'), - (0x1D5FF, 'M', u'r'), - (0x1D600, 'M', u's'), - (0x1D601, 'M', u't'), - (0x1D602, 'M', u'u'), - (0x1D603, 'M', u'v'), - (0x1D604, 'M', u'w'), - (0x1D605, 'M', u'x'), - (0x1D606, 'M', u'y'), - (0x1D607, 'M', u'z'), - (0x1D608, 'M', u'a'), - (0x1D609, 'M', u'b'), - (0x1D60A, 'M', u'c'), - (0x1D60B, 'M', u'd'), - (0x1D60C, 'M', u'e'), - (0x1D60D, 'M', u'f'), - (0x1D60E, 'M', u'g'), - (0x1D60F, 'M', u'h'), - (0x1D610, 'M', u'i'), - (0x1D611, 'M', u'j'), - (0x1D612, 'M', u'k'), - (0x1D613, 'M', u'l'), - (0x1D614, 'M', u'm'), - (0x1D615, 'M', u'n'), - (0x1D616, 'M', u'o'), - (0x1D617, 'M', u'p'), - (0x1D618, 'M', u'q'), - (0x1D619, 'M', u'r'), - (0x1D61A, 'M', u's'), - (0x1D61B, 'M', u't'), - (0x1D61C, 'M', u'u'), - (0x1D61D, 'M', u'v'), - (0x1D61E, 'M', u'w'), - (0x1D61F, 'M', u'x'), - (0x1D620, 'M', u'y'), - (0x1D621, 'M', u'z'), - (0x1D622, 'M', u'a'), - (0x1D623, 'M', u'b'), - (0x1D624, 'M', u'c'), - (0x1D625, 'M', u'd'), - (0x1D626, 'M', u'e'), - (0x1D627, 'M', u'f'), - (0x1D628, 'M', u'g'), - (0x1D629, 'M', u'h'), - (0x1D62A, 'M', u'i'), - (0x1D62B, 'M', u'j'), - (0x1D62C, 'M', u'k'), - (0x1D62D, 'M', u'l'), - (0x1D62E, 'M', u'm'), - (0x1D62F, 'M', u'n'), - (0x1D630, 'M', u'o'), - (0x1D631, 'M', u'p'), - (0x1D632, 'M', u'q'), - (0x1D633, 'M', u'r'), - (0x1D634, 'M', u's'), - (0x1D635, 'M', u't'), - (0x1D636, 'M', u'u'), - (0x1D637, 'M', u'v'), - (0x1D638, 'M', u'w'), - (0x1D639, 'M', u'x'), - (0x1D63A, 'M', u'y'), - (0x1D63B, 'M', u'z'), - (0x1D63C, 'M', u'a'), - (0x1D63D, 'M', u'b'), - (0x1D63E, 'M', u'c'), - (0x1D63F, 'M', u'd'), - (0x1D640, 'M', u'e'), - (0x1D641, 'M', u'f'), - (0x1D642, 'M', u'g'), - (0x1D643, 'M', u'h'), - (0x1D644, 'M', u'i'), - (0x1D645, 'M', u'j'), - (0x1D646, 'M', u'k'), - (0x1D647, 'M', u'l'), - (0x1D648, 'M', u'm'), - (0x1D649, 'M', u'n'), - (0x1D64A, 'M', u'o'), - (0x1D64B, 'M', u'p'), + (0x1D5E8, 'M', 'u'), + (0x1D5E9, 'M', 'v'), + (0x1D5EA, 'M', 'w'), + (0x1D5EB, 'M', 'x'), + (0x1D5EC, 'M', 'y'), + (0x1D5ED, 'M', 'z'), + (0x1D5EE, 'M', 'a'), + (0x1D5EF, 'M', 'b'), + (0x1D5F0, 'M', 'c'), + (0x1D5F1, 'M', 'd'), + (0x1D5F2, 'M', 'e'), + (0x1D5F3, 'M', 'f'), + (0x1D5F4, 'M', 'g'), + (0x1D5F5, 'M', 'h'), + (0x1D5F6, 'M', 'i'), + (0x1D5F7, 'M', 'j'), + (0x1D5F8, 'M', 'k'), + (0x1D5F9, 'M', 'l'), + (0x1D5FA, 'M', 'm'), + (0x1D5FB, 'M', 'n'), + (0x1D5FC, 'M', 'o'), + (0x1D5FD, 'M', 'p'), + (0x1D5FE, 'M', 'q'), + (0x1D5FF, 'M', 'r'), + (0x1D600, 'M', 's'), + (0x1D601, 'M', 't'), + (0x1D602, 'M', 'u'), + (0x1D603, 'M', 'v'), + (0x1D604, 'M', 'w'), + (0x1D605, 'M', 'x'), + (0x1D606, 'M', 'y'), + (0x1D607, 'M', 'z'), + (0x1D608, 'M', 'a'), + (0x1D609, 'M', 'b'), + (0x1D60A, 'M', 'c'), + (0x1D60B, 'M', 'd'), + (0x1D60C, 'M', 'e'), + (0x1D60D, 'M', 'f'), + (0x1D60E, 'M', 'g'), + (0x1D60F, 'M', 'h'), + (0x1D610, 'M', 'i'), + (0x1D611, 'M', 'j'), + (0x1D612, 'M', 'k'), + (0x1D613, 'M', 'l'), + (0x1D614, 'M', 'm'), + (0x1D615, 'M', 'n'), + (0x1D616, 'M', 'o'), + (0x1D617, 'M', 'p'), + (0x1D618, 'M', 'q'), + (0x1D619, 'M', 'r'), + (0x1D61A, 'M', 's'), + (0x1D61B, 'M', 't'), + (0x1D61C, 'M', 'u'), + (0x1D61D, 'M', 'v'), + (0x1D61E, 'M', 'w'), + (0x1D61F, 'M', 'x'), + (0x1D620, 'M', 'y'), + (0x1D621, 'M', 'z'), + (0x1D622, 'M', 'a'), + (0x1D623, 'M', 'b'), + (0x1D624, 'M', 'c'), + (0x1D625, 'M', 'd'), + (0x1D626, 'M', 'e'), + (0x1D627, 'M', 'f'), + (0x1D628, 'M', 'g'), + (0x1D629, 'M', 'h'), + (0x1D62A, 'M', 'i'), + (0x1D62B, 'M', 'j'), + (0x1D62C, 'M', 'k'), + (0x1D62D, 'M', 'l'), + (0x1D62E, 'M', 'm'), + (0x1D62F, 'M', 'n'), + (0x1D630, 'M', 'o'), + (0x1D631, 'M', 'p'), + (0x1D632, 'M', 'q'), + (0x1D633, 'M', 'r'), + (0x1D634, 'M', 's'), + (0x1D635, 'M', 't'), + (0x1D636, 'M', 'u'), + (0x1D637, 'M', 'v'), + (0x1D638, 'M', 'w'), + (0x1D639, 'M', 'x'), + (0x1D63A, 'M', 'y'), + (0x1D63B, 'M', 'z'), + (0x1D63C, 'M', 'a'), + (0x1D63D, 'M', 'b'), + (0x1D63E, 'M', 'c'), + (0x1D63F, 'M', 'd'), + (0x1D640, 'M', 'e'), + (0x1D641, 'M', 'f'), + (0x1D642, 'M', 'g'), + (0x1D643, 'M', 'h'), + (0x1D644, 'M', 'i'), + (0x1D645, 'M', 'j'), + (0x1D646, 'M', 'k'), + (0x1D647, 'M', 'l'), + (0x1D648, 'M', 'm'), + (0x1D649, 'M', 'n'), + (0x1D64A, 'M', 'o'), + (0x1D64B, 'M', 'p'), ] def _seg_65(): return [ - (0x1D64C, 'M', u'q'), - (0x1D64D, 'M', u'r'), - (0x1D64E, 'M', u's'), - (0x1D64F, 'M', u't'), - (0x1D650, 'M', u'u'), - (0x1D651, 'M', u'v'), - (0x1D652, 'M', u'w'), - (0x1D653, 'M', u'x'), - (0x1D654, 'M', u'y'), - (0x1D655, 'M', u'z'), - (0x1D656, 'M', u'a'), - (0x1D657, 'M', u'b'), - (0x1D658, 'M', u'c'), - (0x1D659, 'M', u'd'), - (0x1D65A, 'M', u'e'), - (0x1D65B, 'M', u'f'), - (0x1D65C, 'M', u'g'), - (0x1D65D, 'M', u'h'), - (0x1D65E, 'M', u'i'), - (0x1D65F, 'M', u'j'), - (0x1D660, 'M', u'k'), - (0x1D661, 'M', u'l'), - (0x1D662, 'M', u'm'), - (0x1D663, 'M', u'n'), - (0x1D664, 'M', u'o'), - (0x1D665, 'M', u'p'), - (0x1D666, 'M', u'q'), - (0x1D667, 'M', u'r'), - (0x1D668, 'M', u's'), - (0x1D669, 'M', u't'), - (0x1D66A, 'M', u'u'), - (0x1D66B, 'M', u'v'), - (0x1D66C, 'M', u'w'), - (0x1D66D, 'M', u'x'), - (0x1D66E, 'M', u'y'), - (0x1D66F, 'M', u'z'), - (0x1D670, 'M', u'a'), - (0x1D671, 'M', u'b'), - (0x1D672, 'M', u'c'), - (0x1D673, 'M', u'd'), - (0x1D674, 'M', u'e'), - (0x1D675, 'M', u'f'), - (0x1D676, 'M', u'g'), - (0x1D677, 'M', u'h'), - (0x1D678, 'M', u'i'), - (0x1D679, 'M', u'j'), - (0x1D67A, 'M', u'k'), - (0x1D67B, 'M', u'l'), - (0x1D67C, 'M', u'm'), - (0x1D67D, 'M', u'n'), - (0x1D67E, 'M', u'o'), - (0x1D67F, 'M', u'p'), - (0x1D680, 'M', u'q'), - (0x1D681, 'M', u'r'), - (0x1D682, 'M', u's'), - (0x1D683, 'M', u't'), - (0x1D684, 'M', u'u'), - (0x1D685, 'M', u'v'), - (0x1D686, 'M', u'w'), - (0x1D687, 'M', u'x'), - (0x1D688, 'M', u'y'), - (0x1D689, 'M', u'z'), - (0x1D68A, 'M', u'a'), - (0x1D68B, 'M', u'b'), - (0x1D68C, 'M', u'c'), - (0x1D68D, 'M', u'd'), - (0x1D68E, 'M', u'e'), - (0x1D68F, 'M', u'f'), - (0x1D690, 'M', u'g'), - (0x1D691, 'M', u'h'), - (0x1D692, 'M', u'i'), - (0x1D693, 'M', u'j'), - (0x1D694, 'M', u'k'), - (0x1D695, 'M', u'l'), - (0x1D696, 'M', u'm'), - (0x1D697, 'M', u'n'), - (0x1D698, 'M', u'o'), - (0x1D699, 'M', u'p'), - (0x1D69A, 'M', u'q'), - (0x1D69B, 'M', u'r'), - (0x1D69C, 'M', u's'), - (0x1D69D, 'M', u't'), - (0x1D69E, 'M', u'u'), - (0x1D69F, 'M', u'v'), - (0x1D6A0, 'M', u'w'), - (0x1D6A1, 'M', u'x'), - (0x1D6A2, 'M', u'y'), - (0x1D6A3, 'M', u'z'), - (0x1D6A4, 'M', u'ı'), - (0x1D6A5, 'M', u'ȷ'), + (0x1D64C, 'M', 'q'), + (0x1D64D, 'M', 'r'), + (0x1D64E, 'M', 's'), + (0x1D64F, 'M', 't'), + (0x1D650, 'M', 'u'), + (0x1D651, 'M', 'v'), + (0x1D652, 'M', 'w'), + (0x1D653, 'M', 'x'), + (0x1D654, 'M', 'y'), + (0x1D655, 'M', 'z'), + (0x1D656, 'M', 'a'), + (0x1D657, 'M', 'b'), + (0x1D658, 'M', 'c'), + (0x1D659, 'M', 'd'), + (0x1D65A, 'M', 'e'), + (0x1D65B, 'M', 'f'), + (0x1D65C, 'M', 'g'), + (0x1D65D, 'M', 'h'), + (0x1D65E, 'M', 'i'), + (0x1D65F, 'M', 'j'), + (0x1D660, 'M', 'k'), + (0x1D661, 'M', 'l'), + (0x1D662, 'M', 'm'), + (0x1D663, 'M', 'n'), + (0x1D664, 'M', 'o'), + (0x1D665, 'M', 'p'), + (0x1D666, 'M', 'q'), + (0x1D667, 'M', 'r'), + (0x1D668, 'M', 's'), + (0x1D669, 'M', 't'), + (0x1D66A, 'M', 'u'), + (0x1D66B, 'M', 'v'), + (0x1D66C, 'M', 'w'), + (0x1D66D, 'M', 'x'), + (0x1D66E, 'M', 'y'), + (0x1D66F, 'M', 'z'), + (0x1D670, 'M', 'a'), + (0x1D671, 'M', 'b'), + (0x1D672, 'M', 'c'), + (0x1D673, 'M', 'd'), + (0x1D674, 'M', 'e'), + (0x1D675, 'M', 'f'), + (0x1D676, 'M', 'g'), + (0x1D677, 'M', 'h'), + (0x1D678, 'M', 'i'), + (0x1D679, 'M', 'j'), + (0x1D67A, 'M', 'k'), + (0x1D67B, 'M', 'l'), + (0x1D67C, 'M', 'm'), + (0x1D67D, 'M', 'n'), + (0x1D67E, 'M', 'o'), + (0x1D67F, 'M', 'p'), + (0x1D680, 'M', 'q'), + (0x1D681, 'M', 'r'), + (0x1D682, 'M', 's'), + (0x1D683, 'M', 't'), + (0x1D684, 'M', 'u'), + (0x1D685, 'M', 'v'), + (0x1D686, 'M', 'w'), + (0x1D687, 'M', 'x'), + (0x1D688, 'M', 'y'), + (0x1D689, 'M', 'z'), + (0x1D68A, 'M', 'a'), + (0x1D68B, 'M', 'b'), + (0x1D68C, 'M', 'c'), + (0x1D68D, 'M', 'd'), + (0x1D68E, 'M', 'e'), + (0x1D68F, 'M', 'f'), + (0x1D690, 'M', 'g'), + (0x1D691, 'M', 'h'), + (0x1D692, 'M', 'i'), + (0x1D693, 'M', 'j'), + (0x1D694, 'M', 'k'), + (0x1D695, 'M', 'l'), + (0x1D696, 'M', 'm'), + (0x1D697, 'M', 'n'), + (0x1D698, 'M', 'o'), + (0x1D699, 'M', 'p'), + (0x1D69A, 'M', 'q'), + (0x1D69B, 'M', 'r'), + (0x1D69C, 'M', 's'), + (0x1D69D, 'M', 't'), + (0x1D69E, 'M', 'u'), + (0x1D69F, 'M', 'v'), + (0x1D6A0, 'M', 'w'), + (0x1D6A1, 'M', 'x'), + (0x1D6A2, 'M', 'y'), + (0x1D6A3, 'M', 'z'), + (0x1D6A4, 'M', 'ı'), + (0x1D6A5, 'M', 'ȷ'), (0x1D6A6, 'X'), - (0x1D6A8, 'M', u'α'), - (0x1D6A9, 'M', u'β'), - (0x1D6AA, 'M', u'γ'), - (0x1D6AB, 'M', u'δ'), - (0x1D6AC, 'M', u'ε'), - (0x1D6AD, 'M', u'ζ'), - (0x1D6AE, 'M', u'η'), - (0x1D6AF, 'M', u'θ'), - (0x1D6B0, 'M', u'ι'), + (0x1D6A8, 'M', 'α'), + (0x1D6A9, 'M', 'β'), + (0x1D6AA, 'M', 'γ'), + (0x1D6AB, 'M', 'δ'), + (0x1D6AC, 'M', 'ε'), + (0x1D6AD, 'M', 'ζ'), + (0x1D6AE, 'M', 'η'), + (0x1D6AF, 'M', 'θ'), + (0x1D6B0, 'M', 'ι'), ] def _seg_66(): return [ - (0x1D6B1, 'M', u'κ'), - (0x1D6B2, 'M', u'λ'), - (0x1D6B3, 'M', u'μ'), - (0x1D6B4, 'M', u'ν'), - (0x1D6B5, 'M', u'ξ'), - (0x1D6B6, 'M', u'ο'), - (0x1D6B7, 'M', u'π'), - (0x1D6B8, 'M', u'ρ'), - (0x1D6B9, 'M', u'θ'), - (0x1D6BA, 'M', u'σ'), - (0x1D6BB, 'M', u'τ'), - (0x1D6BC, 'M', u'υ'), - (0x1D6BD, 'M', u'φ'), - (0x1D6BE, 'M', u'χ'), - (0x1D6BF, 'M', u'ψ'), - (0x1D6C0, 'M', u'ω'), - (0x1D6C1, 'M', u'∇'), - (0x1D6C2, 'M', u'α'), - (0x1D6C3, 'M', u'β'), - (0x1D6C4, 'M', u'γ'), - (0x1D6C5, 'M', u'δ'), - (0x1D6C6, 'M', u'ε'), - (0x1D6C7, 'M', u'ζ'), - (0x1D6C8, 'M', u'η'), - (0x1D6C9, 'M', u'θ'), - (0x1D6CA, 'M', u'ι'), - (0x1D6CB, 'M', u'κ'), - (0x1D6CC, 'M', u'λ'), - (0x1D6CD, 'M', u'μ'), - (0x1D6CE, 'M', u'ν'), - (0x1D6CF, 'M', u'ξ'), - (0x1D6D0, 'M', u'ο'), - (0x1D6D1, 'M', u'π'), - (0x1D6D2, 'M', u'ρ'), - (0x1D6D3, 'M', u'σ'), - (0x1D6D5, 'M', u'τ'), - (0x1D6D6, 'M', u'υ'), - (0x1D6D7, 'M', u'φ'), - (0x1D6D8, 'M', u'χ'), - (0x1D6D9, 'M', u'ψ'), - (0x1D6DA, 'M', u'ω'), - (0x1D6DB, 'M', u'∂'), - (0x1D6DC, 'M', u'ε'), - (0x1D6DD, 'M', u'θ'), - (0x1D6DE, 'M', u'κ'), - (0x1D6DF, 'M', u'φ'), - (0x1D6E0, 'M', u'ρ'), - (0x1D6E1, 'M', u'π'), - (0x1D6E2, 'M', u'α'), - (0x1D6E3, 'M', u'β'), - (0x1D6E4, 'M', u'γ'), - (0x1D6E5, 'M', u'δ'), - (0x1D6E6, 'M', u'ε'), - (0x1D6E7, 'M', u'ζ'), - (0x1D6E8, 'M', u'η'), - (0x1D6E9, 'M', u'θ'), - (0x1D6EA, 'M', u'ι'), - (0x1D6EB, 'M', u'κ'), - (0x1D6EC, 'M', u'λ'), - (0x1D6ED, 'M', u'μ'), - (0x1D6EE, 'M', u'ν'), - (0x1D6EF, 'M', u'ξ'), - (0x1D6F0, 'M', u'ο'), - (0x1D6F1, 'M', u'π'), - (0x1D6F2, 'M', u'ρ'), - (0x1D6F3, 'M', u'θ'), - (0x1D6F4, 'M', u'σ'), - (0x1D6F5, 'M', u'τ'), - (0x1D6F6, 'M', u'υ'), - (0x1D6F7, 'M', u'φ'), - (0x1D6F8, 'M', u'χ'), - (0x1D6F9, 'M', u'ψ'), - (0x1D6FA, 'M', u'ω'), - (0x1D6FB, 'M', u'∇'), - (0x1D6FC, 'M', u'α'), - (0x1D6FD, 'M', u'β'), - (0x1D6FE, 'M', u'γ'), - (0x1D6FF, 'M', u'δ'), - (0x1D700, 'M', u'ε'), - (0x1D701, 'M', u'ζ'), - (0x1D702, 'M', u'η'), - (0x1D703, 'M', u'θ'), - (0x1D704, 'M', u'ι'), - (0x1D705, 'M', u'κ'), - (0x1D706, 'M', u'λ'), - (0x1D707, 'M', u'μ'), - (0x1D708, 'M', u'ν'), - (0x1D709, 'M', u'ξ'), - (0x1D70A, 'M', u'ο'), - (0x1D70B, 'M', u'π'), - (0x1D70C, 'M', u'ρ'), - (0x1D70D, 'M', u'σ'), - (0x1D70F, 'M', u'τ'), - (0x1D710, 'M', u'υ'), - (0x1D711, 'M', u'φ'), - (0x1D712, 'M', u'χ'), - (0x1D713, 'M', u'ψ'), - (0x1D714, 'M', u'ω'), - (0x1D715, 'M', u'∂'), - (0x1D716, 'M', u'ε'), + (0x1D6B1, 'M', 'κ'), + (0x1D6B2, 'M', 'λ'), + (0x1D6B3, 'M', 'μ'), + (0x1D6B4, 'M', 'ν'), + (0x1D6B5, 'M', 'ξ'), + (0x1D6B6, 'M', 'ο'), + (0x1D6B7, 'M', 'π'), + (0x1D6B8, 'M', 'ρ'), + (0x1D6B9, 'M', 'θ'), + (0x1D6BA, 'M', 'σ'), + (0x1D6BB, 'M', 'τ'), + (0x1D6BC, 'M', 'υ'), + (0x1D6BD, 'M', 'φ'), + (0x1D6BE, 'M', 'χ'), + (0x1D6BF, 'M', 'ψ'), + (0x1D6C0, 'M', 'ω'), + (0x1D6C1, 'M', '∇'), + (0x1D6C2, 'M', 'α'), + (0x1D6C3, 'M', 'β'), + (0x1D6C4, 'M', 'γ'), + (0x1D6C5, 'M', 'δ'), + (0x1D6C6, 'M', 'ε'), + (0x1D6C7, 'M', 'ζ'), + (0x1D6C8, 'M', 'η'), + (0x1D6C9, 'M', 'θ'), + (0x1D6CA, 'M', 'ι'), + (0x1D6CB, 'M', 'κ'), + (0x1D6CC, 'M', 'λ'), + (0x1D6CD, 'M', 'μ'), + (0x1D6CE, 'M', 'ν'), + (0x1D6CF, 'M', 'ξ'), + (0x1D6D0, 'M', 'ο'), + (0x1D6D1, 'M', 'π'), + (0x1D6D2, 'M', 'ρ'), + (0x1D6D3, 'M', 'σ'), + (0x1D6D5, 'M', 'τ'), + (0x1D6D6, 'M', 'υ'), + (0x1D6D7, 'M', 'φ'), + (0x1D6D8, 'M', 'χ'), + (0x1D6D9, 'M', 'ψ'), + (0x1D6DA, 'M', 'ω'), + (0x1D6DB, 'M', '∂'), + (0x1D6DC, 'M', 'ε'), + (0x1D6DD, 'M', 'θ'), + (0x1D6DE, 'M', 'κ'), + (0x1D6DF, 'M', 'φ'), + (0x1D6E0, 'M', 'ρ'), + (0x1D6E1, 'M', 'π'), + (0x1D6E2, 'M', 'α'), + (0x1D6E3, 'M', 'β'), + (0x1D6E4, 'M', 'γ'), + (0x1D6E5, 'M', 'δ'), + (0x1D6E6, 'M', 'ε'), + (0x1D6E7, 'M', 'ζ'), + (0x1D6E8, 'M', 'η'), + (0x1D6E9, 'M', 'θ'), + (0x1D6EA, 'M', 'ι'), + (0x1D6EB, 'M', 'κ'), + (0x1D6EC, 'M', 'λ'), + (0x1D6ED, 'M', 'μ'), + (0x1D6EE, 'M', 'ν'), + (0x1D6EF, 'M', 'ξ'), + (0x1D6F0, 'M', 'ο'), + (0x1D6F1, 'M', 'π'), + (0x1D6F2, 'M', 'ρ'), + (0x1D6F3, 'M', 'θ'), + (0x1D6F4, 'M', 'σ'), + (0x1D6F5, 'M', 'τ'), + (0x1D6F6, 'M', 'υ'), + (0x1D6F7, 'M', 'φ'), + (0x1D6F8, 'M', 'χ'), + (0x1D6F9, 'M', 'ψ'), + (0x1D6FA, 'M', 'ω'), + (0x1D6FB, 'M', '∇'), + (0x1D6FC, 'M', 'α'), + (0x1D6FD, 'M', 'β'), + (0x1D6FE, 'M', 'γ'), + (0x1D6FF, 'M', 'δ'), + (0x1D700, 'M', 'ε'), + (0x1D701, 'M', 'ζ'), + (0x1D702, 'M', 'η'), + (0x1D703, 'M', 'θ'), + (0x1D704, 'M', 'ι'), + (0x1D705, 'M', 'κ'), + (0x1D706, 'M', 'λ'), + (0x1D707, 'M', 'μ'), + (0x1D708, 'M', 'ν'), + (0x1D709, 'M', 'ξ'), + (0x1D70A, 'M', 'ο'), + (0x1D70B, 'M', 'π'), + (0x1D70C, 'M', 'ρ'), + (0x1D70D, 'M', 'σ'), + (0x1D70F, 'M', 'τ'), + (0x1D710, 'M', 'υ'), + (0x1D711, 'M', 'φ'), + (0x1D712, 'M', 'χ'), + (0x1D713, 'M', 'ψ'), + (0x1D714, 'M', 'ω'), + (0x1D715, 'M', '∂'), + (0x1D716, 'M', 'ε'), ] def _seg_67(): return [ - (0x1D717, 'M', u'θ'), - (0x1D718, 'M', u'κ'), - (0x1D719, 'M', u'φ'), - (0x1D71A, 'M', u'ρ'), - (0x1D71B, 'M', u'π'), - (0x1D71C, 'M', u'α'), - (0x1D71D, 'M', u'β'), - (0x1D71E, 'M', u'γ'), - (0x1D71F, 'M', u'δ'), - (0x1D720, 'M', u'ε'), - (0x1D721, 'M', u'ζ'), - (0x1D722, 'M', u'η'), - (0x1D723, 'M', u'θ'), - (0x1D724, 'M', u'ι'), - (0x1D725, 'M', u'κ'), - (0x1D726, 'M', u'λ'), - (0x1D727, 'M', u'μ'), - (0x1D728, 'M', u'ν'), - (0x1D729, 'M', u'ξ'), - (0x1D72A, 'M', u'ο'), - (0x1D72B, 'M', u'π'), - (0x1D72C, 'M', u'ρ'), - (0x1D72D, 'M', u'θ'), - (0x1D72E, 'M', u'σ'), - (0x1D72F, 'M', u'τ'), - (0x1D730, 'M', u'υ'), - (0x1D731, 'M', u'φ'), - (0x1D732, 'M', u'χ'), - (0x1D733, 'M', u'ψ'), - (0x1D734, 'M', u'ω'), - (0x1D735, 'M', u'∇'), - (0x1D736, 'M', u'α'), - (0x1D737, 'M', u'β'), - (0x1D738, 'M', u'γ'), - (0x1D739, 'M', u'δ'), - (0x1D73A, 'M', u'ε'), - (0x1D73B, 'M', u'ζ'), - (0x1D73C, 'M', u'η'), - (0x1D73D, 'M', u'θ'), - (0x1D73E, 'M', u'ι'), - (0x1D73F, 'M', u'κ'), - (0x1D740, 'M', u'λ'), - (0x1D741, 'M', u'μ'), - (0x1D742, 'M', u'ν'), - (0x1D743, 'M', u'ξ'), - (0x1D744, 'M', u'ο'), - (0x1D745, 'M', u'π'), - (0x1D746, 'M', u'ρ'), - (0x1D747, 'M', u'σ'), - (0x1D749, 'M', u'τ'), - (0x1D74A, 'M', u'υ'), - (0x1D74B, 'M', u'φ'), - (0x1D74C, 'M', u'χ'), - (0x1D74D, 'M', u'ψ'), - (0x1D74E, 'M', u'ω'), - (0x1D74F, 'M', u'∂'), - (0x1D750, 'M', u'ε'), - (0x1D751, 'M', u'θ'), - (0x1D752, 'M', u'κ'), - (0x1D753, 'M', u'φ'), - (0x1D754, 'M', u'ρ'), - (0x1D755, 'M', u'π'), - (0x1D756, 'M', u'α'), - (0x1D757, 'M', u'β'), - (0x1D758, 'M', u'γ'), - (0x1D759, 'M', u'δ'), - (0x1D75A, 'M', u'ε'), - (0x1D75B, 'M', u'ζ'), - (0x1D75C, 'M', u'η'), - (0x1D75D, 'M', u'θ'), - (0x1D75E, 'M', u'ι'), - (0x1D75F, 'M', u'κ'), - (0x1D760, 'M', u'λ'), - (0x1D761, 'M', u'μ'), - (0x1D762, 'M', u'ν'), - (0x1D763, 'M', u'ξ'), - (0x1D764, 'M', u'ο'), - (0x1D765, 'M', u'π'), - (0x1D766, 'M', u'ρ'), - (0x1D767, 'M', u'θ'), - (0x1D768, 'M', u'σ'), - (0x1D769, 'M', u'τ'), - (0x1D76A, 'M', u'υ'), - (0x1D76B, 'M', u'φ'), - (0x1D76C, 'M', u'χ'), - (0x1D76D, 'M', u'ψ'), - (0x1D76E, 'M', u'ω'), - (0x1D76F, 'M', u'∇'), - (0x1D770, 'M', u'α'), - (0x1D771, 'M', u'β'), - (0x1D772, 'M', u'γ'), - (0x1D773, 'M', u'δ'), - (0x1D774, 'M', u'ε'), - (0x1D775, 'M', u'ζ'), - (0x1D776, 'M', u'η'), - (0x1D777, 'M', u'θ'), - (0x1D778, 'M', u'ι'), - (0x1D779, 'M', u'κ'), - (0x1D77A, 'M', u'λ'), - (0x1D77B, 'M', u'μ'), + (0x1D717, 'M', 'θ'), + (0x1D718, 'M', 'κ'), + (0x1D719, 'M', 'φ'), + (0x1D71A, 'M', 'ρ'), + (0x1D71B, 'M', 'π'), + (0x1D71C, 'M', 'α'), + (0x1D71D, 'M', 'β'), + (0x1D71E, 'M', 'γ'), + (0x1D71F, 'M', 'δ'), + (0x1D720, 'M', 'ε'), + (0x1D721, 'M', 'ζ'), + (0x1D722, 'M', 'η'), + (0x1D723, 'M', 'θ'), + (0x1D724, 'M', 'ι'), + (0x1D725, 'M', 'κ'), + (0x1D726, 'M', 'λ'), + (0x1D727, 'M', 'μ'), + (0x1D728, 'M', 'ν'), + (0x1D729, 'M', 'ξ'), + (0x1D72A, 'M', 'ο'), + (0x1D72B, 'M', 'π'), + (0x1D72C, 'M', 'ρ'), + (0x1D72D, 'M', 'θ'), + (0x1D72E, 'M', 'σ'), + (0x1D72F, 'M', 'τ'), + (0x1D730, 'M', 'υ'), + (0x1D731, 'M', 'φ'), + (0x1D732, 'M', 'χ'), + (0x1D733, 'M', 'ψ'), + (0x1D734, 'M', 'ω'), + (0x1D735, 'M', '∇'), + (0x1D736, 'M', 'α'), + (0x1D737, 'M', 'β'), + (0x1D738, 'M', 'γ'), + (0x1D739, 'M', 'δ'), + (0x1D73A, 'M', 'ε'), + (0x1D73B, 'M', 'ζ'), + (0x1D73C, 'M', 'η'), + (0x1D73D, 'M', 'θ'), + (0x1D73E, 'M', 'ι'), + (0x1D73F, 'M', 'κ'), + (0x1D740, 'M', 'λ'), + (0x1D741, 'M', 'μ'), + (0x1D742, 'M', 'ν'), + (0x1D743, 'M', 'ξ'), + (0x1D744, 'M', 'ο'), + (0x1D745, 'M', 'π'), + (0x1D746, 'M', 'ρ'), + (0x1D747, 'M', 'σ'), + (0x1D749, 'M', 'τ'), + (0x1D74A, 'M', 'υ'), + (0x1D74B, 'M', 'φ'), + (0x1D74C, 'M', 'χ'), + (0x1D74D, 'M', 'ψ'), + (0x1D74E, 'M', 'ω'), + (0x1D74F, 'M', '∂'), + (0x1D750, 'M', 'ε'), + (0x1D751, 'M', 'θ'), + (0x1D752, 'M', 'κ'), + (0x1D753, 'M', 'φ'), + (0x1D754, 'M', 'ρ'), + (0x1D755, 'M', 'π'), + (0x1D756, 'M', 'α'), + (0x1D757, 'M', 'β'), + (0x1D758, 'M', 'γ'), + (0x1D759, 'M', 'δ'), + (0x1D75A, 'M', 'ε'), + (0x1D75B, 'M', 'ζ'), + (0x1D75C, 'M', 'η'), + (0x1D75D, 'M', 'θ'), + (0x1D75E, 'M', 'ι'), + (0x1D75F, 'M', 'κ'), + (0x1D760, 'M', 'λ'), + (0x1D761, 'M', 'μ'), + (0x1D762, 'M', 'ν'), + (0x1D763, 'M', 'ξ'), + (0x1D764, 'M', 'ο'), + (0x1D765, 'M', 'π'), + (0x1D766, 'M', 'ρ'), + (0x1D767, 'M', 'θ'), + (0x1D768, 'M', 'σ'), + (0x1D769, 'M', 'τ'), + (0x1D76A, 'M', 'υ'), + (0x1D76B, 'M', 'φ'), + (0x1D76C, 'M', 'χ'), + (0x1D76D, 'M', 'ψ'), + (0x1D76E, 'M', 'ω'), + (0x1D76F, 'M', '∇'), + (0x1D770, 'M', 'α'), + (0x1D771, 'M', 'β'), + (0x1D772, 'M', 'γ'), + (0x1D773, 'M', 'δ'), + (0x1D774, 'M', 'ε'), + (0x1D775, 'M', 'ζ'), + (0x1D776, 'M', 'η'), + (0x1D777, 'M', 'θ'), + (0x1D778, 'M', 'ι'), + (0x1D779, 'M', 'κ'), + (0x1D77A, 'M', 'λ'), + (0x1D77B, 'M', 'μ'), ] def _seg_68(): return [ - (0x1D77C, 'M', u'ν'), - (0x1D77D, 'M', u'ξ'), - (0x1D77E, 'M', u'ο'), - (0x1D77F, 'M', u'π'), - (0x1D780, 'M', u'ρ'), - (0x1D781, 'M', u'σ'), - (0x1D783, 'M', u'τ'), - (0x1D784, 'M', u'υ'), - (0x1D785, 'M', u'φ'), - (0x1D786, 'M', u'χ'), - (0x1D787, 'M', u'ψ'), - (0x1D788, 'M', u'ω'), - (0x1D789, 'M', u'∂'), - (0x1D78A, 'M', u'ε'), - (0x1D78B, 'M', u'θ'), - (0x1D78C, 'M', u'κ'), - (0x1D78D, 'M', u'φ'), - (0x1D78E, 'M', u'ρ'), - (0x1D78F, 'M', u'π'), - (0x1D790, 'M', u'α'), - (0x1D791, 'M', u'β'), - (0x1D792, 'M', u'γ'), - (0x1D793, 'M', u'δ'), - (0x1D794, 'M', u'ε'), - (0x1D795, 'M', u'ζ'), - (0x1D796, 'M', u'η'), - (0x1D797, 'M', u'θ'), - (0x1D798, 'M', u'ι'), - (0x1D799, 'M', u'κ'), - (0x1D79A, 'M', u'λ'), - (0x1D79B, 'M', u'μ'), - (0x1D79C, 'M', u'ν'), - (0x1D79D, 'M', u'ξ'), - (0x1D79E, 'M', u'ο'), - (0x1D79F, 'M', u'π'), - (0x1D7A0, 'M', u'ρ'), - (0x1D7A1, 'M', u'θ'), - (0x1D7A2, 'M', u'σ'), - (0x1D7A3, 'M', u'τ'), - (0x1D7A4, 'M', u'υ'), - (0x1D7A5, 'M', u'φ'), - (0x1D7A6, 'M', u'χ'), - (0x1D7A7, 'M', u'ψ'), - (0x1D7A8, 'M', u'ω'), - (0x1D7A9, 'M', u'∇'), - (0x1D7AA, 'M', u'α'), - (0x1D7AB, 'M', u'β'), - (0x1D7AC, 'M', u'γ'), - (0x1D7AD, 'M', u'δ'), - (0x1D7AE, 'M', u'ε'), - (0x1D7AF, 'M', u'ζ'), - (0x1D7B0, 'M', u'η'), - (0x1D7B1, 'M', u'θ'), - (0x1D7B2, 'M', u'ι'), - (0x1D7B3, 'M', u'κ'), - (0x1D7B4, 'M', u'λ'), - (0x1D7B5, 'M', u'μ'), - (0x1D7B6, 'M', u'ν'), - (0x1D7B7, 'M', u'ξ'), - (0x1D7B8, 'M', u'ο'), - (0x1D7B9, 'M', u'π'), - (0x1D7BA, 'M', u'ρ'), - (0x1D7BB, 'M', u'σ'), - (0x1D7BD, 'M', u'τ'), - (0x1D7BE, 'M', u'υ'), - (0x1D7BF, 'M', u'φ'), - (0x1D7C0, 'M', u'χ'), - (0x1D7C1, 'M', u'ψ'), - (0x1D7C2, 'M', u'ω'), - (0x1D7C3, 'M', u'∂'), - (0x1D7C4, 'M', u'ε'), - (0x1D7C5, 'M', u'θ'), - (0x1D7C6, 'M', u'κ'), - (0x1D7C7, 'M', u'φ'), - (0x1D7C8, 'M', u'ρ'), - (0x1D7C9, 'M', u'π'), - (0x1D7CA, 'M', u'ϝ'), + (0x1D77C, 'M', 'ν'), + (0x1D77D, 'M', 'ξ'), + (0x1D77E, 'M', 'ο'), + (0x1D77F, 'M', 'π'), + (0x1D780, 'M', 'ρ'), + (0x1D781, 'M', 'σ'), + (0x1D783, 'M', 'τ'), + (0x1D784, 'M', 'υ'), + (0x1D785, 'M', 'φ'), + (0x1D786, 'M', 'χ'), + (0x1D787, 'M', 'ψ'), + (0x1D788, 'M', 'ω'), + (0x1D789, 'M', '∂'), + (0x1D78A, 'M', 'ε'), + (0x1D78B, 'M', 'θ'), + (0x1D78C, 'M', 'κ'), + (0x1D78D, 'M', 'φ'), + (0x1D78E, 'M', 'ρ'), + (0x1D78F, 'M', 'π'), + (0x1D790, 'M', 'α'), + (0x1D791, 'M', 'β'), + (0x1D792, 'M', 'γ'), + (0x1D793, 'M', 'δ'), + (0x1D794, 'M', 'ε'), + (0x1D795, 'M', 'ζ'), + (0x1D796, 'M', 'η'), + (0x1D797, 'M', 'θ'), + (0x1D798, 'M', 'ι'), + (0x1D799, 'M', 'κ'), + (0x1D79A, 'M', 'λ'), + (0x1D79B, 'M', 'μ'), + (0x1D79C, 'M', 'ν'), + (0x1D79D, 'M', 'ξ'), + (0x1D79E, 'M', 'ο'), + (0x1D79F, 'M', 'π'), + (0x1D7A0, 'M', 'ρ'), + (0x1D7A1, 'M', 'θ'), + (0x1D7A2, 'M', 'σ'), + (0x1D7A3, 'M', 'τ'), + (0x1D7A4, 'M', 'υ'), + (0x1D7A5, 'M', 'φ'), + (0x1D7A6, 'M', 'χ'), + (0x1D7A7, 'M', 'ψ'), + (0x1D7A8, 'M', 'ω'), + (0x1D7A9, 'M', '∇'), + (0x1D7AA, 'M', 'α'), + (0x1D7AB, 'M', 'β'), + (0x1D7AC, 'M', 'γ'), + (0x1D7AD, 'M', 'δ'), + (0x1D7AE, 'M', 'ε'), + (0x1D7AF, 'M', 'ζ'), + (0x1D7B0, 'M', 'η'), + (0x1D7B1, 'M', 'θ'), + (0x1D7B2, 'M', 'ι'), + (0x1D7B3, 'M', 'κ'), + (0x1D7B4, 'M', 'λ'), + (0x1D7B5, 'M', 'μ'), + (0x1D7B6, 'M', 'ν'), + (0x1D7B7, 'M', 'ξ'), + (0x1D7B8, 'M', 'ο'), + (0x1D7B9, 'M', 'π'), + (0x1D7BA, 'M', 'ρ'), + (0x1D7BB, 'M', 'σ'), + (0x1D7BD, 'M', 'τ'), + (0x1D7BE, 'M', 'υ'), + (0x1D7BF, 'M', 'φ'), + (0x1D7C0, 'M', 'χ'), + (0x1D7C1, 'M', 'ψ'), + (0x1D7C2, 'M', 'ω'), + (0x1D7C3, 'M', '∂'), + (0x1D7C4, 'M', 'ε'), + (0x1D7C5, 'M', 'θ'), + (0x1D7C6, 'M', 'κ'), + (0x1D7C7, 'M', 'φ'), + (0x1D7C8, 'M', 'ρ'), + (0x1D7C9, 'M', 'π'), + (0x1D7CA, 'M', 'ϝ'), (0x1D7CC, 'X'), - (0x1D7CE, 'M', u'0'), - (0x1D7CF, 'M', u'1'), - (0x1D7D0, 'M', u'2'), - (0x1D7D1, 'M', u'3'), - (0x1D7D2, 'M', u'4'), - (0x1D7D3, 'M', u'5'), - (0x1D7D4, 'M', u'6'), - (0x1D7D5, 'M', u'7'), - (0x1D7D6, 'M', u'8'), - (0x1D7D7, 'M', u'9'), - (0x1D7D8, 'M', u'0'), - (0x1D7D9, 'M', u'1'), - (0x1D7DA, 'M', u'2'), - (0x1D7DB, 'M', u'3'), - (0x1D7DC, 'M', u'4'), - (0x1D7DD, 'M', u'5'), - (0x1D7DE, 'M', u'6'), - (0x1D7DF, 'M', u'7'), - (0x1D7E0, 'M', u'8'), - (0x1D7E1, 'M', u'9'), - (0x1D7E2, 'M', u'0'), - (0x1D7E3, 'M', u'1'), + (0x1D7CE, 'M', '0'), + (0x1D7CF, 'M', '1'), + (0x1D7D0, 'M', '2'), + (0x1D7D1, 'M', '3'), + (0x1D7D2, 'M', '4'), + (0x1D7D3, 'M', '5'), + (0x1D7D4, 'M', '6'), + (0x1D7D5, 'M', '7'), + (0x1D7D6, 'M', '8'), + (0x1D7D7, 'M', '9'), + (0x1D7D8, 'M', '0'), + (0x1D7D9, 'M', '1'), + (0x1D7DA, 'M', '2'), + (0x1D7DB, 'M', '3'), + (0x1D7DC, 'M', '4'), + (0x1D7DD, 'M', '5'), + (0x1D7DE, 'M', '6'), + (0x1D7DF, 'M', '7'), + (0x1D7E0, 'M', '8'), + (0x1D7E1, 'M', '9'), + (0x1D7E2, 'M', '0'), + (0x1D7E3, 'M', '1'), ] def _seg_69(): return [ - (0x1D7E4, 'M', u'2'), - (0x1D7E5, 'M', u'3'), - (0x1D7E6, 'M', u'4'), - (0x1D7E7, 'M', u'5'), - (0x1D7E8, 'M', u'6'), - (0x1D7E9, 'M', u'7'), - (0x1D7EA, 'M', u'8'), - (0x1D7EB, 'M', u'9'), - (0x1D7EC, 'M', u'0'), - (0x1D7ED, 'M', u'1'), - (0x1D7EE, 'M', u'2'), - (0x1D7EF, 'M', u'3'), - (0x1D7F0, 'M', u'4'), - (0x1D7F1, 'M', u'5'), - (0x1D7F2, 'M', u'6'), - (0x1D7F3, 'M', u'7'), - (0x1D7F4, 'M', u'8'), - (0x1D7F5, 'M', u'9'), - (0x1D7F6, 'M', u'0'), - (0x1D7F7, 'M', u'1'), - (0x1D7F8, 'M', u'2'), - (0x1D7F9, 'M', u'3'), - (0x1D7FA, 'M', u'4'), - (0x1D7FB, 'M', u'5'), - (0x1D7FC, 'M', u'6'), - (0x1D7FD, 'M', u'7'), - (0x1D7FE, 'M', u'8'), - (0x1D7FF, 'M', u'9'), + (0x1D7E4, 'M', '2'), + (0x1D7E5, 'M', '3'), + (0x1D7E6, 'M', '4'), + (0x1D7E7, 'M', '5'), + (0x1D7E8, 'M', '6'), + (0x1D7E9, 'M', '7'), + (0x1D7EA, 'M', '8'), + (0x1D7EB, 'M', '9'), + (0x1D7EC, 'M', '0'), + (0x1D7ED, 'M', '1'), + (0x1D7EE, 'M', '2'), + (0x1D7EF, 'M', '3'), + (0x1D7F0, 'M', '4'), + (0x1D7F1, 'M', '5'), + (0x1D7F2, 'M', '6'), + (0x1D7F3, 'M', '7'), + (0x1D7F4, 'M', '8'), + (0x1D7F5, 'M', '9'), + (0x1D7F6, 'M', '0'), + (0x1D7F7, 'M', '1'), + (0x1D7F8, 'M', '2'), + (0x1D7F9, 'M', '3'), + (0x1D7FA, 'M', '4'), + (0x1D7FB, 'M', '5'), + (0x1D7FC, 'M', '6'), + (0x1D7FD, 'M', '7'), + (0x1D7FE, 'M', '8'), + (0x1D7FF, 'M', '9'), (0x1D800, 'V'), (0x1DA8C, 'X'), (0x1DA9B, 'V'), @@ -7243,40 +7242,40 @@ def _seg_69(): (0x1E8C5, 'X'), (0x1E8C7, 'V'), (0x1E8D7, 'X'), - (0x1E900, 'M', u'𞤢'), - (0x1E901, 'M', u'𞤣'), - (0x1E902, 'M', u'𞤤'), - (0x1E903, 'M', u'𞤥'), - (0x1E904, 'M', u'𞤦'), - (0x1E905, 'M', u'𞤧'), - (0x1E906, 'M', u'𞤨'), - (0x1E907, 'M', u'𞤩'), - (0x1E908, 'M', u'𞤪'), - (0x1E909, 'M', u'𞤫'), - (0x1E90A, 'M', u'𞤬'), - (0x1E90B, 'M', u'𞤭'), - (0x1E90C, 'M', u'𞤮'), - (0x1E90D, 'M', u'𞤯'), - (0x1E90E, 'M', u'𞤰'), - (0x1E90F, 'M', u'𞤱'), - (0x1E910, 'M', u'𞤲'), - (0x1E911, 'M', u'𞤳'), - (0x1E912, 'M', u'𞤴'), - (0x1E913, 'M', u'𞤵'), - (0x1E914, 'M', u'𞤶'), - (0x1E915, 'M', u'𞤷'), - (0x1E916, 'M', u'𞤸'), - (0x1E917, 'M', u'𞤹'), - (0x1E918, 'M', u'𞤺'), - (0x1E919, 'M', u'𞤻'), - (0x1E91A, 'M', u'𞤼'), - (0x1E91B, 'M', u'𞤽'), - (0x1E91C, 'M', u'𞤾'), - (0x1E91D, 'M', u'𞤿'), - (0x1E91E, 'M', u'𞥀'), - (0x1E91F, 'M', u'𞥁'), - (0x1E920, 'M', u'𞥂'), - (0x1E921, 'M', u'𞥃'), + (0x1E900, 'M', '𞤢'), + (0x1E901, 'M', '𞤣'), + (0x1E902, 'M', '𞤤'), + (0x1E903, 'M', '𞤥'), + (0x1E904, 'M', '𞤦'), + (0x1E905, 'M', '𞤧'), + (0x1E906, 'M', '𞤨'), + (0x1E907, 'M', '𞤩'), + (0x1E908, 'M', '𞤪'), + (0x1E909, 'M', '𞤫'), + (0x1E90A, 'M', '𞤬'), + (0x1E90B, 'M', '𞤭'), + (0x1E90C, 'M', '𞤮'), + (0x1E90D, 'M', '𞤯'), + (0x1E90E, 'M', '𞤰'), + (0x1E90F, 'M', '𞤱'), + (0x1E910, 'M', '𞤲'), + (0x1E911, 'M', '𞤳'), + (0x1E912, 'M', '𞤴'), + (0x1E913, 'M', '𞤵'), + (0x1E914, 'M', '𞤶'), + (0x1E915, 'M', '𞤷'), + (0x1E916, 'M', '𞤸'), + (0x1E917, 'M', '𞤹'), + (0x1E918, 'M', '𞤺'), + (0x1E919, 'M', '𞤻'), + (0x1E91A, 'M', '𞤼'), + (0x1E91B, 'M', '𞤽'), + (0x1E91C, 'M', '𞤾'), + (0x1E91D, 'M', '𞤿'), + (0x1E91E, 'M', '𞥀'), + (0x1E91F, 'M', '𞥁'), + (0x1E920, 'M', '𞥂'), + (0x1E921, 'M', '𞥃'), (0x1E922, 'V'), (0x1E94C, 'X'), (0x1E950, 'V'), @@ -7291,183 +7290,183 @@ def _seg_70(): (0x1ECB5, 'X'), (0x1ED01, 'V'), (0x1ED3E, 'X'), - (0x1EE00, 'M', u'ا'), - (0x1EE01, 'M', u'ب'), - (0x1EE02, 'M', u'ج'), - (0x1EE03, 'M', u'د'), + (0x1EE00, 'M', 'ا'), + (0x1EE01, 'M', 'ب'), + (0x1EE02, 'M', 'ج'), + (0x1EE03, 'M', 'د'), (0x1EE04, 'X'), - (0x1EE05, 'M', u'و'), - (0x1EE06, 'M', u'ز'), - (0x1EE07, 'M', u'ح'), - (0x1EE08, 'M', u'ط'), - (0x1EE09, 'M', u'ي'), - (0x1EE0A, 'M', u'ك'), - (0x1EE0B, 'M', u'ل'), - (0x1EE0C, 'M', u'م'), - (0x1EE0D, 'M', u'ن'), - (0x1EE0E, 'M', u'س'), - (0x1EE0F, 'M', u'ع'), - (0x1EE10, 'M', u'ف'), - (0x1EE11, 'M', u'ص'), - (0x1EE12, 'M', u'ق'), - (0x1EE13, 'M', u'ر'), - (0x1EE14, 'M', u'ش'), - (0x1EE15, 'M', u'ت'), - (0x1EE16, 'M', u'ث'), - (0x1EE17, 'M', u'خ'), - (0x1EE18, 'M', u'ذ'), - (0x1EE19, 'M', u'ض'), - (0x1EE1A, 'M', u'ظ'), - (0x1EE1B, 'M', u'غ'), - (0x1EE1C, 'M', u'ٮ'), - (0x1EE1D, 'M', u'ں'), - (0x1EE1E, 'M', u'ڡ'), - (0x1EE1F, 'M', u'ٯ'), + (0x1EE05, 'M', 'و'), + (0x1EE06, 'M', 'ز'), + (0x1EE07, 'M', 'ح'), + (0x1EE08, 'M', 'ط'), + (0x1EE09, 'M', 'ي'), + (0x1EE0A, 'M', 'ك'), + (0x1EE0B, 'M', 'ل'), + (0x1EE0C, 'M', 'م'), + (0x1EE0D, 'M', 'ن'), + (0x1EE0E, 'M', 'س'), + (0x1EE0F, 'M', 'ع'), + (0x1EE10, 'M', 'ف'), + (0x1EE11, 'M', 'ص'), + (0x1EE12, 'M', 'ق'), + (0x1EE13, 'M', 'ر'), + (0x1EE14, 'M', 'ش'), + (0x1EE15, 'M', 'ت'), + (0x1EE16, 'M', 'ث'), + (0x1EE17, 'M', 'خ'), + (0x1EE18, 'M', 'ذ'), + (0x1EE19, 'M', 'ض'), + (0x1EE1A, 'M', 'ظ'), + (0x1EE1B, 'M', 'غ'), + (0x1EE1C, 'M', 'ٮ'), + (0x1EE1D, 'M', 'ں'), + (0x1EE1E, 'M', 'ڡ'), + (0x1EE1F, 'M', 'ٯ'), (0x1EE20, 'X'), - (0x1EE21, 'M', u'ب'), - (0x1EE22, 'M', u'ج'), + (0x1EE21, 'M', 'ب'), + (0x1EE22, 'M', 'ج'), (0x1EE23, 'X'), - (0x1EE24, 'M', u'ه'), + (0x1EE24, 'M', 'ه'), (0x1EE25, 'X'), - (0x1EE27, 'M', u'ح'), + (0x1EE27, 'M', 'ح'), (0x1EE28, 'X'), - (0x1EE29, 'M', u'ي'), - (0x1EE2A, 'M', u'ك'), - (0x1EE2B, 'M', u'ل'), - (0x1EE2C, 'M', u'م'), - (0x1EE2D, 'M', u'ن'), - (0x1EE2E, 'M', u'س'), - (0x1EE2F, 'M', u'ع'), - (0x1EE30, 'M', u'ف'), - (0x1EE31, 'M', u'ص'), - (0x1EE32, 'M', u'ق'), + (0x1EE29, 'M', 'ي'), + (0x1EE2A, 'M', 'ك'), + (0x1EE2B, 'M', 'ل'), + (0x1EE2C, 'M', 'م'), + (0x1EE2D, 'M', 'ن'), + (0x1EE2E, 'M', 'س'), + (0x1EE2F, 'M', 'ع'), + (0x1EE30, 'M', 'ف'), + (0x1EE31, 'M', 'ص'), + (0x1EE32, 'M', 'ق'), (0x1EE33, 'X'), - (0x1EE34, 'M', u'ش'), - (0x1EE35, 'M', u'ت'), - (0x1EE36, 'M', u'ث'), - (0x1EE37, 'M', u'خ'), + (0x1EE34, 'M', 'ش'), + (0x1EE35, 'M', 'ت'), + (0x1EE36, 'M', 'ث'), + (0x1EE37, 'M', 'خ'), (0x1EE38, 'X'), - (0x1EE39, 'M', u'ض'), + (0x1EE39, 'M', 'ض'), (0x1EE3A, 'X'), - (0x1EE3B, 'M', u'غ'), + (0x1EE3B, 'M', 'غ'), (0x1EE3C, 'X'), - (0x1EE42, 'M', u'ج'), + (0x1EE42, 'M', 'ج'), (0x1EE43, 'X'), - (0x1EE47, 'M', u'ح'), + (0x1EE47, 'M', 'ح'), (0x1EE48, 'X'), - (0x1EE49, 'M', u'ي'), + (0x1EE49, 'M', 'ي'), (0x1EE4A, 'X'), - (0x1EE4B, 'M', u'ل'), + (0x1EE4B, 'M', 'ل'), (0x1EE4C, 'X'), - (0x1EE4D, 'M', u'ن'), - (0x1EE4E, 'M', u'س'), - (0x1EE4F, 'M', u'ع'), + (0x1EE4D, 'M', 'ن'), + (0x1EE4E, 'M', 'س'), + (0x1EE4F, 'M', 'ع'), (0x1EE50, 'X'), - (0x1EE51, 'M', u'ص'), - (0x1EE52, 'M', u'ق'), + (0x1EE51, 'M', 'ص'), + (0x1EE52, 'M', 'ق'), (0x1EE53, 'X'), - (0x1EE54, 'M', u'ش'), + (0x1EE54, 'M', 'ش'), (0x1EE55, 'X'), - (0x1EE57, 'M', u'خ'), + (0x1EE57, 'M', 'خ'), (0x1EE58, 'X'), - (0x1EE59, 'M', u'ض'), + (0x1EE59, 'M', 'ض'), (0x1EE5A, 'X'), - (0x1EE5B, 'M', u'غ'), + (0x1EE5B, 'M', 'غ'), (0x1EE5C, 'X'), - (0x1EE5D, 'M', u'ں'), + (0x1EE5D, 'M', 'ں'), (0x1EE5E, 'X'), - (0x1EE5F, 'M', u'ٯ'), + (0x1EE5F, 'M', 'ٯ'), (0x1EE60, 'X'), - (0x1EE61, 'M', u'ب'), - (0x1EE62, 'M', u'ج'), + (0x1EE61, 'M', 'ب'), + (0x1EE62, 'M', 'ج'), (0x1EE63, 'X'), - (0x1EE64, 'M', u'ه'), + (0x1EE64, 'M', 'ه'), (0x1EE65, 'X'), - (0x1EE67, 'M', u'ح'), - (0x1EE68, 'M', u'ط'), - (0x1EE69, 'M', u'ي'), - (0x1EE6A, 'M', u'ك'), + (0x1EE67, 'M', 'ح'), + (0x1EE68, 'M', 'ط'), + (0x1EE69, 'M', 'ي'), + (0x1EE6A, 'M', 'ك'), ] def _seg_71(): return [ (0x1EE6B, 'X'), - (0x1EE6C, 'M', u'م'), - (0x1EE6D, 'M', u'ن'), - (0x1EE6E, 'M', u'س'), - (0x1EE6F, 'M', u'ع'), - (0x1EE70, 'M', u'ف'), - (0x1EE71, 'M', u'ص'), - (0x1EE72, 'M', u'ق'), + (0x1EE6C, 'M', 'م'), + (0x1EE6D, 'M', 'ن'), + (0x1EE6E, 'M', 'س'), + (0x1EE6F, 'M', 'ع'), + (0x1EE70, 'M', 'ف'), + (0x1EE71, 'M', 'ص'), + (0x1EE72, 'M', 'ق'), (0x1EE73, 'X'), - (0x1EE74, 'M', u'ش'), - (0x1EE75, 'M', u'ت'), - (0x1EE76, 'M', u'ث'), - (0x1EE77, 'M', u'خ'), + (0x1EE74, 'M', 'ش'), + (0x1EE75, 'M', 'ت'), + (0x1EE76, 'M', 'ث'), + (0x1EE77, 'M', 'خ'), (0x1EE78, 'X'), - (0x1EE79, 'M', u'ض'), - (0x1EE7A, 'M', u'ظ'), - (0x1EE7B, 'M', u'غ'), - (0x1EE7C, 'M', u'ٮ'), + (0x1EE79, 'M', 'ض'), + (0x1EE7A, 'M', 'ظ'), + (0x1EE7B, 'M', 'غ'), + (0x1EE7C, 'M', 'ٮ'), (0x1EE7D, 'X'), - (0x1EE7E, 'M', u'ڡ'), + (0x1EE7E, 'M', 'ڡ'), (0x1EE7F, 'X'), - (0x1EE80, 'M', u'ا'), - (0x1EE81, 'M', u'ب'), - (0x1EE82, 'M', u'ج'), - (0x1EE83, 'M', u'د'), - (0x1EE84, 'M', u'ه'), - (0x1EE85, 'M', u'و'), - (0x1EE86, 'M', u'ز'), - (0x1EE87, 'M', u'ح'), - (0x1EE88, 'M', u'ط'), - (0x1EE89, 'M', u'ي'), + (0x1EE80, 'M', 'ا'), + (0x1EE81, 'M', 'ب'), + (0x1EE82, 'M', 'ج'), + (0x1EE83, 'M', 'د'), + (0x1EE84, 'M', 'ه'), + (0x1EE85, 'M', 'و'), + (0x1EE86, 'M', 'ز'), + (0x1EE87, 'M', 'ح'), + (0x1EE88, 'M', 'ط'), + (0x1EE89, 'M', 'ي'), (0x1EE8A, 'X'), - (0x1EE8B, 'M', u'ل'), - (0x1EE8C, 'M', u'م'), - (0x1EE8D, 'M', u'ن'), - (0x1EE8E, 'M', u'س'), - (0x1EE8F, 'M', u'ع'), - (0x1EE90, 'M', u'ف'), - (0x1EE91, 'M', u'ص'), - (0x1EE92, 'M', u'ق'), - (0x1EE93, 'M', u'ر'), - (0x1EE94, 'M', u'ش'), - (0x1EE95, 'M', u'ت'), - (0x1EE96, 'M', u'ث'), - (0x1EE97, 'M', u'خ'), - (0x1EE98, 'M', u'ذ'), - (0x1EE99, 'M', u'ض'), - (0x1EE9A, 'M', u'ظ'), - (0x1EE9B, 'M', u'غ'), + (0x1EE8B, 'M', 'ل'), + (0x1EE8C, 'M', 'م'), + (0x1EE8D, 'M', 'ن'), + (0x1EE8E, 'M', 'س'), + (0x1EE8F, 'M', 'ع'), + (0x1EE90, 'M', 'ف'), + (0x1EE91, 'M', 'ص'), + (0x1EE92, 'M', 'ق'), + (0x1EE93, 'M', 'ر'), + (0x1EE94, 'M', 'ش'), + (0x1EE95, 'M', 'ت'), + (0x1EE96, 'M', 'ث'), + (0x1EE97, 'M', 'خ'), + (0x1EE98, 'M', 'ذ'), + (0x1EE99, 'M', 'ض'), + (0x1EE9A, 'M', 'ظ'), + (0x1EE9B, 'M', 'غ'), (0x1EE9C, 'X'), - (0x1EEA1, 'M', u'ب'), - (0x1EEA2, 'M', u'ج'), - (0x1EEA3, 'M', u'د'), + (0x1EEA1, 'M', 'ب'), + (0x1EEA2, 'M', 'ج'), + (0x1EEA3, 'M', 'د'), (0x1EEA4, 'X'), - (0x1EEA5, 'M', u'و'), - (0x1EEA6, 'M', u'ز'), - (0x1EEA7, 'M', u'ح'), - (0x1EEA8, 'M', u'ط'), - (0x1EEA9, 'M', u'ي'), + (0x1EEA5, 'M', 'و'), + (0x1EEA6, 'M', 'ز'), + (0x1EEA7, 'M', 'ح'), + (0x1EEA8, 'M', 'ط'), + (0x1EEA9, 'M', 'ي'), (0x1EEAA, 'X'), - (0x1EEAB, 'M', u'ل'), - (0x1EEAC, 'M', u'م'), - (0x1EEAD, 'M', u'ن'), - (0x1EEAE, 'M', u'س'), - (0x1EEAF, 'M', u'ع'), - (0x1EEB0, 'M', u'ف'), - (0x1EEB1, 'M', u'ص'), - (0x1EEB2, 'M', u'ق'), - (0x1EEB3, 'M', u'ر'), - (0x1EEB4, 'M', u'ش'), - (0x1EEB5, 'M', u'ت'), - (0x1EEB6, 'M', u'ث'), - (0x1EEB7, 'M', u'خ'), - (0x1EEB8, 'M', u'ذ'), - (0x1EEB9, 'M', u'ض'), - (0x1EEBA, 'M', u'ظ'), - (0x1EEBB, 'M', u'غ'), + (0x1EEAB, 'M', 'ل'), + (0x1EEAC, 'M', 'م'), + (0x1EEAD, 'M', 'ن'), + (0x1EEAE, 'M', 'س'), + (0x1EEAF, 'M', 'ع'), + (0x1EEB0, 'M', 'ف'), + (0x1EEB1, 'M', 'ص'), + (0x1EEB2, 'M', 'ق'), + (0x1EEB3, 'M', 'ر'), + (0x1EEB4, 'M', 'ش'), + (0x1EEB5, 'M', 'ت'), + (0x1EEB6, 'M', 'ث'), + (0x1EEB7, 'M', 'خ'), + (0x1EEB8, 'M', 'ذ'), + (0x1EEB9, 'M', 'ض'), + (0x1EEBA, 'M', 'ظ'), + (0x1EEBB, 'M', 'غ'), (0x1EEBC, 'X'), (0x1EEF0, 'V'), (0x1EEF2, 'X'), @@ -7483,159 +7482,159 @@ def _seg_71(): (0x1F0D0, 'X'), (0x1F0D1, 'V'), (0x1F0F6, 'X'), - (0x1F101, '3', u'0,'), - (0x1F102, '3', u'1,'), - (0x1F103, '3', u'2,'), - (0x1F104, '3', u'3,'), - (0x1F105, '3', u'4,'), - (0x1F106, '3', u'5,'), - (0x1F107, '3', u'6,'), - (0x1F108, '3', u'7,'), + (0x1F101, '3', '0,'), + (0x1F102, '3', '1,'), + (0x1F103, '3', '2,'), + (0x1F104, '3', '3,'), + (0x1F105, '3', '4,'), + (0x1F106, '3', '5,'), + (0x1F107, '3', '6,'), + (0x1F108, '3', '7,'), ] def _seg_72(): return [ - (0x1F109, '3', u'8,'), - (0x1F10A, '3', u'9,'), + (0x1F109, '3', '8,'), + (0x1F10A, '3', '9,'), (0x1F10B, 'V'), - (0x1F110, '3', u'(a)'), - (0x1F111, '3', u'(b)'), - (0x1F112, '3', u'(c)'), - (0x1F113, '3', u'(d)'), - (0x1F114, '3', u'(e)'), - (0x1F115, '3', u'(f)'), - (0x1F116, '3', u'(g)'), - (0x1F117, '3', u'(h)'), - (0x1F118, '3', u'(i)'), - (0x1F119, '3', u'(j)'), - (0x1F11A, '3', u'(k)'), - (0x1F11B, '3', u'(l)'), - (0x1F11C, '3', u'(m)'), - (0x1F11D, '3', u'(n)'), - (0x1F11E, '3', u'(o)'), - (0x1F11F, '3', u'(p)'), - (0x1F120, '3', u'(q)'), - (0x1F121, '3', u'(r)'), - (0x1F122, '3', u'(s)'), - (0x1F123, '3', u'(t)'), - (0x1F124, '3', u'(u)'), - (0x1F125, '3', u'(v)'), - (0x1F126, '3', u'(w)'), - (0x1F127, '3', u'(x)'), - (0x1F128, '3', u'(y)'), - (0x1F129, '3', u'(z)'), - (0x1F12A, 'M', u'〔s〕'), - (0x1F12B, 'M', u'c'), - (0x1F12C, 'M', u'r'), - (0x1F12D, 'M', u'cd'), - (0x1F12E, 'M', u'wz'), + (0x1F110, '3', '(a)'), + (0x1F111, '3', '(b)'), + (0x1F112, '3', '(c)'), + (0x1F113, '3', '(d)'), + (0x1F114, '3', '(e)'), + (0x1F115, '3', '(f)'), + (0x1F116, '3', '(g)'), + (0x1F117, '3', '(h)'), + (0x1F118, '3', '(i)'), + (0x1F119, '3', '(j)'), + (0x1F11A, '3', '(k)'), + (0x1F11B, '3', '(l)'), + (0x1F11C, '3', '(m)'), + (0x1F11D, '3', '(n)'), + (0x1F11E, '3', '(o)'), + (0x1F11F, '3', '(p)'), + (0x1F120, '3', '(q)'), + (0x1F121, '3', '(r)'), + (0x1F122, '3', '(s)'), + (0x1F123, '3', '(t)'), + (0x1F124, '3', '(u)'), + (0x1F125, '3', '(v)'), + (0x1F126, '3', '(w)'), + (0x1F127, '3', '(x)'), + (0x1F128, '3', '(y)'), + (0x1F129, '3', '(z)'), + (0x1F12A, 'M', '〔s〕'), + (0x1F12B, 'M', 'c'), + (0x1F12C, 'M', 'r'), + (0x1F12D, 'M', 'cd'), + (0x1F12E, 'M', 'wz'), (0x1F12F, 'V'), - (0x1F130, 'M', u'a'), - (0x1F131, 'M', u'b'), - (0x1F132, 'M', u'c'), - (0x1F133, 'M', u'd'), - (0x1F134, 'M', u'e'), - (0x1F135, 'M', u'f'), - (0x1F136, 'M', u'g'), - (0x1F137, 'M', u'h'), - (0x1F138, 'M', u'i'), - (0x1F139, 'M', u'j'), - (0x1F13A, 'M', u'k'), - (0x1F13B, 'M', u'l'), - (0x1F13C, 'M', u'm'), - (0x1F13D, 'M', u'n'), - (0x1F13E, 'M', u'o'), - (0x1F13F, 'M', u'p'), - (0x1F140, 'M', u'q'), - (0x1F141, 'M', u'r'), - (0x1F142, 'M', u's'), - (0x1F143, 'M', u't'), - (0x1F144, 'M', u'u'), - (0x1F145, 'M', u'v'), - (0x1F146, 'M', u'w'), - (0x1F147, 'M', u'x'), - (0x1F148, 'M', u'y'), - (0x1F149, 'M', u'z'), - (0x1F14A, 'M', u'hv'), - (0x1F14B, 'M', u'mv'), - (0x1F14C, 'M', u'sd'), - (0x1F14D, 'M', u'ss'), - (0x1F14E, 'M', u'ppv'), - (0x1F14F, 'M', u'wc'), + (0x1F130, 'M', 'a'), + (0x1F131, 'M', 'b'), + (0x1F132, 'M', 'c'), + (0x1F133, 'M', 'd'), + (0x1F134, 'M', 'e'), + (0x1F135, 'M', 'f'), + (0x1F136, 'M', 'g'), + (0x1F137, 'M', 'h'), + (0x1F138, 'M', 'i'), + (0x1F139, 'M', 'j'), + (0x1F13A, 'M', 'k'), + (0x1F13B, 'M', 'l'), + (0x1F13C, 'M', 'm'), + (0x1F13D, 'M', 'n'), + (0x1F13E, 'M', 'o'), + (0x1F13F, 'M', 'p'), + (0x1F140, 'M', 'q'), + (0x1F141, 'M', 'r'), + (0x1F142, 'M', 's'), + (0x1F143, 'M', 't'), + (0x1F144, 'M', 'u'), + (0x1F145, 'M', 'v'), + (0x1F146, 'M', 'w'), + (0x1F147, 'M', 'x'), + (0x1F148, 'M', 'y'), + (0x1F149, 'M', 'z'), + (0x1F14A, 'M', 'hv'), + (0x1F14B, 'M', 'mv'), + (0x1F14C, 'M', 'sd'), + (0x1F14D, 'M', 'ss'), + (0x1F14E, 'M', 'ppv'), + (0x1F14F, 'M', 'wc'), (0x1F150, 'V'), - (0x1F16A, 'M', u'mc'), - (0x1F16B, 'M', u'md'), - (0x1F16C, 'M', u'mr'), + (0x1F16A, 'M', 'mc'), + (0x1F16B, 'M', 'md'), + (0x1F16C, 'M', 'mr'), (0x1F16D, 'V'), - (0x1F190, 'M', u'dj'), + (0x1F190, 'M', 'dj'), (0x1F191, 'V'), (0x1F1AE, 'X'), (0x1F1E6, 'V'), - (0x1F200, 'M', u'ほか'), - (0x1F201, 'M', u'ココ'), - (0x1F202, 'M', u'サ'), + (0x1F200, 'M', 'ほか'), + (0x1F201, 'M', 'ココ'), + (0x1F202, 'M', 'サ'), (0x1F203, 'X'), - (0x1F210, 'M', u'手'), - (0x1F211, 'M', u'字'), - (0x1F212, 'M', u'双'), - (0x1F213, 'M', u'デ'), - (0x1F214, 'M', u'二'), - (0x1F215, 'M', u'多'), - (0x1F216, 'M', u'解'), - (0x1F217, 'M', u'天'), - (0x1F218, 'M', u'交'), - (0x1F219, 'M', u'映'), - (0x1F21A, 'M', u'無'), - (0x1F21B, 'M', u'料'), - (0x1F21C, 'M', u'前'), - (0x1F21D, 'M', u'後'), - (0x1F21E, 'M', u'再'), - (0x1F21F, 'M', u'新'), - (0x1F220, 'M', u'初'), - (0x1F221, 'M', u'終'), - (0x1F222, 'M', u'生'), - (0x1F223, 'M', u'販'), + (0x1F210, 'M', '手'), + (0x1F211, 'M', '字'), + (0x1F212, 'M', '双'), + (0x1F213, 'M', 'デ'), + (0x1F214, 'M', '二'), + (0x1F215, 'M', '多'), + (0x1F216, 'M', '解'), + (0x1F217, 'M', '天'), + (0x1F218, 'M', '交'), + (0x1F219, 'M', '映'), + (0x1F21A, 'M', '無'), + (0x1F21B, 'M', '料'), + (0x1F21C, 'M', '前'), + (0x1F21D, 'M', '後'), + (0x1F21E, 'M', '再'), + (0x1F21F, 'M', '新'), + (0x1F220, 'M', '初'), + (0x1F221, 'M', '終'), + (0x1F222, 'M', '生'), + (0x1F223, 'M', '販'), ] def _seg_73(): return [ - (0x1F224, 'M', u'声'), - (0x1F225, 'M', u'吹'), - (0x1F226, 'M', u'演'), - (0x1F227, 'M', u'投'), - (0x1F228, 'M', u'捕'), - (0x1F229, 'M', u'一'), - (0x1F22A, 'M', u'三'), - (0x1F22B, 'M', u'遊'), - (0x1F22C, 'M', u'左'), - (0x1F22D, 'M', u'中'), - (0x1F22E, 'M', u'右'), - (0x1F22F, 'M', u'指'), - (0x1F230, 'M', u'走'), - (0x1F231, 'M', u'打'), - (0x1F232, 'M', u'禁'), - (0x1F233, 'M', u'空'), - (0x1F234, 'M', u'合'), - (0x1F235, 'M', u'満'), - (0x1F236, 'M', u'有'), - (0x1F237, 'M', u'月'), - (0x1F238, 'M', u'申'), - (0x1F239, 'M', u'割'), - (0x1F23A, 'M', u'営'), - (0x1F23B, 'M', u'配'), + (0x1F224, 'M', '声'), + (0x1F225, 'M', '吹'), + (0x1F226, 'M', '演'), + (0x1F227, 'M', '投'), + (0x1F228, 'M', '捕'), + (0x1F229, 'M', '一'), + (0x1F22A, 'M', '三'), + (0x1F22B, 'M', '遊'), + (0x1F22C, 'M', '左'), + (0x1F22D, 'M', '中'), + (0x1F22E, 'M', '右'), + (0x1F22F, 'M', '指'), + (0x1F230, 'M', '走'), + (0x1F231, 'M', '打'), + (0x1F232, 'M', '禁'), + (0x1F233, 'M', '空'), + (0x1F234, 'M', '合'), + (0x1F235, 'M', '満'), + (0x1F236, 'M', '有'), + (0x1F237, 'M', '月'), + (0x1F238, 'M', '申'), + (0x1F239, 'M', '割'), + (0x1F23A, 'M', '営'), + (0x1F23B, 'M', '配'), (0x1F23C, 'X'), - (0x1F240, 'M', u'〔本〕'), - (0x1F241, 'M', u'〔三〕'), - (0x1F242, 'M', u'〔二〕'), - (0x1F243, 'M', u'〔安〕'), - (0x1F244, 'M', u'〔点〕'), - (0x1F245, 'M', u'〔打〕'), - (0x1F246, 'M', u'〔盗〕'), - (0x1F247, 'M', u'〔勝〕'), - (0x1F248, 'M', u'〔敗〕'), + (0x1F240, 'M', '〔本〕'), + (0x1F241, 'M', '〔三〕'), + (0x1F242, 'M', '〔二〕'), + (0x1F243, 'M', '〔安〕'), + (0x1F244, 'M', '〔点〕'), + (0x1F245, 'M', '〔打〕'), + (0x1F246, 'M', '〔盗〕'), + (0x1F247, 'M', '〔勝〕'), + (0x1F248, 'M', '〔敗〕'), (0x1F249, 'X'), - (0x1F250, 'M', u'得'), - (0x1F251, 'M', u'可'), + (0x1F250, 'M', '得'), + (0x1F251, 'M', '可'), (0x1F252, 'X'), (0x1F260, 'V'), (0x1F266, 'X'), @@ -7689,16 +7688,16 @@ def _seg_73(): (0x1FB93, 'X'), (0x1FB94, 'V'), (0x1FBCB, 'X'), - (0x1FBF0, 'M', u'0'), - (0x1FBF1, 'M', u'1'), - (0x1FBF2, 'M', u'2'), - (0x1FBF3, 'M', u'3'), - (0x1FBF4, 'M', u'4'), - (0x1FBF5, 'M', u'5'), - (0x1FBF6, 'M', u'6'), - (0x1FBF7, 'M', u'7'), - (0x1FBF8, 'M', u'8'), - (0x1FBF9, 'M', u'9'), + (0x1FBF0, 'M', '0'), + (0x1FBF1, 'M', '1'), + (0x1FBF2, 'M', '2'), + (0x1FBF3, 'M', '3'), + (0x1FBF4, 'M', '4'), + (0x1FBF5, 'M', '5'), + (0x1FBF6, 'M', '6'), + (0x1FBF7, 'M', '7'), + (0x1FBF8, 'M', '8'), + (0x1FBF9, 'M', '9'), ] def _seg_74(): @@ -7714,558 +7713,558 @@ def _seg_74(): (0x2CEA2, 'X'), (0x2CEB0, 'V'), (0x2EBE1, 'X'), - (0x2F800, 'M', u'丽'), - (0x2F801, 'M', u'丸'), - (0x2F802, 'M', u'乁'), - (0x2F803, 'M', u'𠄢'), - (0x2F804, 'M', u'你'), - (0x2F805, 'M', u'侮'), - (0x2F806, 'M', u'侻'), - (0x2F807, 'M', u'倂'), - (0x2F808, 'M', u'偺'), - (0x2F809, 'M', u'備'), - (0x2F80A, 'M', u'僧'), - (0x2F80B, 'M', u'像'), - (0x2F80C, 'M', u'㒞'), - (0x2F80D, 'M', u'𠘺'), - (0x2F80E, 'M', u'免'), - (0x2F80F, 'M', u'兔'), - (0x2F810, 'M', u'兤'), - (0x2F811, 'M', u'具'), - (0x2F812, 'M', u'𠔜'), - (0x2F813, 'M', u'㒹'), - (0x2F814, 'M', u'內'), - (0x2F815, 'M', u'再'), - (0x2F816, 'M', u'𠕋'), - (0x2F817, 'M', u'冗'), - (0x2F818, 'M', u'冤'), - (0x2F819, 'M', u'仌'), - (0x2F81A, 'M', u'冬'), - (0x2F81B, 'M', u'况'), - (0x2F81C, 'M', u'𩇟'), - (0x2F81D, 'M', u'凵'), - (0x2F81E, 'M', u'刃'), - (0x2F81F, 'M', u'㓟'), - (0x2F820, 'M', u'刻'), - (0x2F821, 'M', u'剆'), - (0x2F822, 'M', u'割'), - (0x2F823, 'M', u'剷'), - (0x2F824, 'M', u'㔕'), - (0x2F825, 'M', u'勇'), - (0x2F826, 'M', u'勉'), - (0x2F827, 'M', u'勤'), - (0x2F828, 'M', u'勺'), - (0x2F829, 'M', u'包'), - (0x2F82A, 'M', u'匆'), - (0x2F82B, 'M', u'北'), - (0x2F82C, 'M', u'卉'), - (0x2F82D, 'M', u'卑'), - (0x2F82E, 'M', u'博'), - (0x2F82F, 'M', u'即'), - (0x2F830, 'M', u'卽'), - (0x2F831, 'M', u'卿'), - (0x2F834, 'M', u'𠨬'), - (0x2F835, 'M', u'灰'), - (0x2F836, 'M', u'及'), - (0x2F837, 'M', u'叟'), - (0x2F838, 'M', u'𠭣'), - (0x2F839, 'M', u'叫'), - (0x2F83A, 'M', u'叱'), - (0x2F83B, 'M', u'吆'), - (0x2F83C, 'M', u'咞'), - (0x2F83D, 'M', u'吸'), - (0x2F83E, 'M', u'呈'), - (0x2F83F, 'M', u'周'), - (0x2F840, 'M', u'咢'), - (0x2F841, 'M', u'哶'), - (0x2F842, 'M', u'唐'), - (0x2F843, 'M', u'啓'), - (0x2F844, 'M', u'啣'), - (0x2F845, 'M', u'善'), - (0x2F847, 'M', u'喙'), - (0x2F848, 'M', u'喫'), - (0x2F849, 'M', u'喳'), - (0x2F84A, 'M', u'嗂'), - (0x2F84B, 'M', u'圖'), - (0x2F84C, 'M', u'嘆'), - (0x2F84D, 'M', u'圗'), - (0x2F84E, 'M', u'噑'), - (0x2F84F, 'M', u'噴'), - (0x2F850, 'M', u'切'), - (0x2F851, 'M', u'壮'), - (0x2F852, 'M', u'城'), - (0x2F853, 'M', u'埴'), - (0x2F854, 'M', u'堍'), - (0x2F855, 'M', u'型'), - (0x2F856, 'M', u'堲'), - (0x2F857, 'M', u'報'), - (0x2F858, 'M', u'墬'), - (0x2F859, 'M', u'𡓤'), - (0x2F85A, 'M', u'売'), - (0x2F85B, 'M', u'壷'), + (0x2F800, 'M', '丽'), + (0x2F801, 'M', '丸'), + (0x2F802, 'M', '乁'), + (0x2F803, 'M', '𠄢'), + (0x2F804, 'M', '你'), + (0x2F805, 'M', '侮'), + (0x2F806, 'M', '侻'), + (0x2F807, 'M', '倂'), + (0x2F808, 'M', '偺'), + (0x2F809, 'M', '備'), + (0x2F80A, 'M', '僧'), + (0x2F80B, 'M', '像'), + (0x2F80C, 'M', '㒞'), + (0x2F80D, 'M', '𠘺'), + (0x2F80E, 'M', '免'), + (0x2F80F, 'M', '兔'), + (0x2F810, 'M', '兤'), + (0x2F811, 'M', '具'), + (0x2F812, 'M', '𠔜'), + (0x2F813, 'M', '㒹'), + (0x2F814, 'M', '內'), + (0x2F815, 'M', '再'), + (0x2F816, 'M', '𠕋'), + (0x2F817, 'M', '冗'), + (0x2F818, 'M', '冤'), + (0x2F819, 'M', '仌'), + (0x2F81A, 'M', '冬'), + (0x2F81B, 'M', '况'), + (0x2F81C, 'M', '𩇟'), + (0x2F81D, 'M', '凵'), + (0x2F81E, 'M', '刃'), + (0x2F81F, 'M', '㓟'), + (0x2F820, 'M', '刻'), + (0x2F821, 'M', '剆'), + (0x2F822, 'M', '割'), + (0x2F823, 'M', '剷'), + (0x2F824, 'M', '㔕'), + (0x2F825, 'M', '勇'), + (0x2F826, 'M', '勉'), + (0x2F827, 'M', '勤'), + (0x2F828, 'M', '勺'), + (0x2F829, 'M', '包'), + (0x2F82A, 'M', '匆'), + (0x2F82B, 'M', '北'), + (0x2F82C, 'M', '卉'), + (0x2F82D, 'M', '卑'), + (0x2F82E, 'M', '博'), + (0x2F82F, 'M', '即'), + (0x2F830, 'M', '卽'), + (0x2F831, 'M', '卿'), + (0x2F834, 'M', '𠨬'), + (0x2F835, 'M', '灰'), + (0x2F836, 'M', '及'), + (0x2F837, 'M', '叟'), + (0x2F838, 'M', '𠭣'), + (0x2F839, 'M', '叫'), + (0x2F83A, 'M', '叱'), + (0x2F83B, 'M', '吆'), + (0x2F83C, 'M', '咞'), + (0x2F83D, 'M', '吸'), + (0x2F83E, 'M', '呈'), + (0x2F83F, 'M', '周'), + (0x2F840, 'M', '咢'), + (0x2F841, 'M', '哶'), + (0x2F842, 'M', '唐'), + (0x2F843, 'M', '啓'), + (0x2F844, 'M', '啣'), + (0x2F845, 'M', '善'), + (0x2F847, 'M', '喙'), + (0x2F848, 'M', '喫'), + (0x2F849, 'M', '喳'), + (0x2F84A, 'M', '嗂'), + (0x2F84B, 'M', '圖'), + (0x2F84C, 'M', '嘆'), + (0x2F84D, 'M', '圗'), + (0x2F84E, 'M', '噑'), + (0x2F84F, 'M', '噴'), + (0x2F850, 'M', '切'), + (0x2F851, 'M', '壮'), + (0x2F852, 'M', '城'), + (0x2F853, 'M', '埴'), + (0x2F854, 'M', '堍'), + (0x2F855, 'M', '型'), + (0x2F856, 'M', '堲'), + (0x2F857, 'M', '報'), + (0x2F858, 'M', '墬'), + (0x2F859, 'M', '𡓤'), + (0x2F85A, 'M', '売'), + (0x2F85B, 'M', '壷'), ] def _seg_75(): return [ - (0x2F85C, 'M', u'夆'), - (0x2F85D, 'M', u'多'), - (0x2F85E, 'M', u'夢'), - (0x2F85F, 'M', u'奢'), - (0x2F860, 'M', u'𡚨'), - (0x2F861, 'M', u'𡛪'), - (0x2F862, 'M', u'姬'), - (0x2F863, 'M', u'娛'), - (0x2F864, 'M', u'娧'), - (0x2F865, 'M', u'姘'), - (0x2F866, 'M', u'婦'), - (0x2F867, 'M', u'㛮'), + (0x2F85C, 'M', '夆'), + (0x2F85D, 'M', '多'), + (0x2F85E, 'M', '夢'), + (0x2F85F, 'M', '奢'), + (0x2F860, 'M', '𡚨'), + (0x2F861, 'M', '𡛪'), + (0x2F862, 'M', '姬'), + (0x2F863, 'M', '娛'), + (0x2F864, 'M', '娧'), + (0x2F865, 'M', '姘'), + (0x2F866, 'M', '婦'), + (0x2F867, 'M', '㛮'), (0x2F868, 'X'), - (0x2F869, 'M', u'嬈'), - (0x2F86A, 'M', u'嬾'), - (0x2F86C, 'M', u'𡧈'), - (0x2F86D, 'M', u'寃'), - (0x2F86E, 'M', u'寘'), - (0x2F86F, 'M', u'寧'), - (0x2F870, 'M', u'寳'), - (0x2F871, 'M', u'𡬘'), - (0x2F872, 'M', u'寿'), - (0x2F873, 'M', u'将'), + (0x2F869, 'M', '嬈'), + (0x2F86A, 'M', '嬾'), + (0x2F86C, 'M', '𡧈'), + (0x2F86D, 'M', '寃'), + (0x2F86E, 'M', '寘'), + (0x2F86F, 'M', '寧'), + (0x2F870, 'M', '寳'), + (0x2F871, 'M', '𡬘'), + (0x2F872, 'M', '寿'), + (0x2F873, 'M', '将'), (0x2F874, 'X'), - (0x2F875, 'M', u'尢'), - (0x2F876, 'M', u'㞁'), - (0x2F877, 'M', u'屠'), - (0x2F878, 'M', u'屮'), - (0x2F879, 'M', u'峀'), - (0x2F87A, 'M', u'岍'), - (0x2F87B, 'M', u'𡷤'), - (0x2F87C, 'M', u'嵃'), - (0x2F87D, 'M', u'𡷦'), - (0x2F87E, 'M', u'嵮'), - (0x2F87F, 'M', u'嵫'), - (0x2F880, 'M', u'嵼'), - (0x2F881, 'M', u'巡'), - (0x2F882, 'M', u'巢'), - (0x2F883, 'M', u'㠯'), - (0x2F884, 'M', u'巽'), - (0x2F885, 'M', u'帨'), - (0x2F886, 'M', u'帽'), - (0x2F887, 'M', u'幩'), - (0x2F888, 'M', u'㡢'), - (0x2F889, 'M', u'𢆃'), - (0x2F88A, 'M', u'㡼'), - (0x2F88B, 'M', u'庰'), - (0x2F88C, 'M', u'庳'), - (0x2F88D, 'M', u'庶'), - (0x2F88E, 'M', u'廊'), - (0x2F88F, 'M', u'𪎒'), - (0x2F890, 'M', u'廾'), - (0x2F891, 'M', u'𢌱'), - (0x2F893, 'M', u'舁'), - (0x2F894, 'M', u'弢'), - (0x2F896, 'M', u'㣇'), - (0x2F897, 'M', u'𣊸'), - (0x2F898, 'M', u'𦇚'), - (0x2F899, 'M', u'形'), - (0x2F89A, 'M', u'彫'), - (0x2F89B, 'M', u'㣣'), - (0x2F89C, 'M', u'徚'), - (0x2F89D, 'M', u'忍'), - (0x2F89E, 'M', u'志'), - (0x2F89F, 'M', u'忹'), - (0x2F8A0, 'M', u'悁'), - (0x2F8A1, 'M', u'㤺'), - (0x2F8A2, 'M', u'㤜'), - (0x2F8A3, 'M', u'悔'), - (0x2F8A4, 'M', u'𢛔'), - (0x2F8A5, 'M', u'惇'), - (0x2F8A6, 'M', u'慈'), - (0x2F8A7, 'M', u'慌'), - (0x2F8A8, 'M', u'慎'), - (0x2F8A9, 'M', u'慌'), - (0x2F8AA, 'M', u'慺'), - (0x2F8AB, 'M', u'憎'), - (0x2F8AC, 'M', u'憲'), - (0x2F8AD, 'M', u'憤'), - (0x2F8AE, 'M', u'憯'), - (0x2F8AF, 'M', u'懞'), - (0x2F8B0, 'M', u'懲'), - (0x2F8B1, 'M', u'懶'), - (0x2F8B2, 'M', u'成'), - (0x2F8B3, 'M', u'戛'), - (0x2F8B4, 'M', u'扝'), - (0x2F8B5, 'M', u'抱'), - (0x2F8B6, 'M', u'拔'), - (0x2F8B7, 'M', u'捐'), - (0x2F8B8, 'M', u'𢬌'), - (0x2F8B9, 'M', u'挽'), - (0x2F8BA, 'M', u'拼'), - (0x2F8BB, 'M', u'捨'), - (0x2F8BC, 'M', u'掃'), - (0x2F8BD, 'M', u'揤'), - (0x2F8BE, 'M', u'𢯱'), - (0x2F8BF, 'M', u'搢'), - (0x2F8C0, 'M', u'揅'), - (0x2F8C1, 'M', u'掩'), - (0x2F8C2, 'M', u'㨮'), + (0x2F875, 'M', '尢'), + (0x2F876, 'M', '㞁'), + (0x2F877, 'M', '屠'), + (0x2F878, 'M', '屮'), + (0x2F879, 'M', '峀'), + (0x2F87A, 'M', '岍'), + (0x2F87B, 'M', '𡷤'), + (0x2F87C, 'M', '嵃'), + (0x2F87D, 'M', '𡷦'), + (0x2F87E, 'M', '嵮'), + (0x2F87F, 'M', '嵫'), + (0x2F880, 'M', '嵼'), + (0x2F881, 'M', '巡'), + (0x2F882, 'M', '巢'), + (0x2F883, 'M', '㠯'), + (0x2F884, 'M', '巽'), + (0x2F885, 'M', '帨'), + (0x2F886, 'M', '帽'), + (0x2F887, 'M', '幩'), + (0x2F888, 'M', '㡢'), + (0x2F889, 'M', '𢆃'), + (0x2F88A, 'M', '㡼'), + (0x2F88B, 'M', '庰'), + (0x2F88C, 'M', '庳'), + (0x2F88D, 'M', '庶'), + (0x2F88E, 'M', '廊'), + (0x2F88F, 'M', '𪎒'), + (0x2F890, 'M', '廾'), + (0x2F891, 'M', '𢌱'), + (0x2F893, 'M', '舁'), + (0x2F894, 'M', '弢'), + (0x2F896, 'M', '㣇'), + (0x2F897, 'M', '𣊸'), + (0x2F898, 'M', '𦇚'), + (0x2F899, 'M', '形'), + (0x2F89A, 'M', '彫'), + (0x2F89B, 'M', '㣣'), + (0x2F89C, 'M', '徚'), + (0x2F89D, 'M', '忍'), + (0x2F89E, 'M', '志'), + (0x2F89F, 'M', '忹'), + (0x2F8A0, 'M', '悁'), + (0x2F8A1, 'M', '㤺'), + (0x2F8A2, 'M', '㤜'), + (0x2F8A3, 'M', '悔'), + (0x2F8A4, 'M', '𢛔'), + (0x2F8A5, 'M', '惇'), + (0x2F8A6, 'M', '慈'), + (0x2F8A7, 'M', '慌'), + (0x2F8A8, 'M', '慎'), + (0x2F8A9, 'M', '慌'), + (0x2F8AA, 'M', '慺'), + (0x2F8AB, 'M', '憎'), + (0x2F8AC, 'M', '憲'), + (0x2F8AD, 'M', '憤'), + (0x2F8AE, 'M', '憯'), + (0x2F8AF, 'M', '懞'), + (0x2F8B0, 'M', '懲'), + (0x2F8B1, 'M', '懶'), + (0x2F8B2, 'M', '成'), + (0x2F8B3, 'M', '戛'), + (0x2F8B4, 'M', '扝'), + (0x2F8B5, 'M', '抱'), + (0x2F8B6, 'M', '拔'), + (0x2F8B7, 'M', '捐'), + (0x2F8B8, 'M', '𢬌'), + (0x2F8B9, 'M', '挽'), + (0x2F8BA, 'M', '拼'), + (0x2F8BB, 'M', '捨'), + (0x2F8BC, 'M', '掃'), + (0x2F8BD, 'M', '揤'), + (0x2F8BE, 'M', '𢯱'), + (0x2F8BF, 'M', '搢'), + (0x2F8C0, 'M', '揅'), + (0x2F8C1, 'M', '掩'), + (0x2F8C2, 'M', '㨮'), ] def _seg_76(): return [ - (0x2F8C3, 'M', u'摩'), - (0x2F8C4, 'M', u'摾'), - (0x2F8C5, 'M', u'撝'), - (0x2F8C6, 'M', u'摷'), - (0x2F8C7, 'M', u'㩬'), - (0x2F8C8, 'M', u'敏'), - (0x2F8C9, 'M', u'敬'), - (0x2F8CA, 'M', u'𣀊'), - (0x2F8CB, 'M', u'旣'), - (0x2F8CC, 'M', u'書'), - (0x2F8CD, 'M', u'晉'), - (0x2F8CE, 'M', u'㬙'), - (0x2F8CF, 'M', u'暑'), - (0x2F8D0, 'M', u'㬈'), - (0x2F8D1, 'M', u'㫤'), - (0x2F8D2, 'M', u'冒'), - (0x2F8D3, 'M', u'冕'), - (0x2F8D4, 'M', u'最'), - (0x2F8D5, 'M', u'暜'), - (0x2F8D6, 'M', u'肭'), - (0x2F8D7, 'M', u'䏙'), - (0x2F8D8, 'M', u'朗'), - (0x2F8D9, 'M', u'望'), - (0x2F8DA, 'M', u'朡'), - (0x2F8DB, 'M', u'杞'), - (0x2F8DC, 'M', u'杓'), - (0x2F8DD, 'M', u'𣏃'), - (0x2F8DE, 'M', u'㭉'), - (0x2F8DF, 'M', u'柺'), - (0x2F8E0, 'M', u'枅'), - (0x2F8E1, 'M', u'桒'), - (0x2F8E2, 'M', u'梅'), - (0x2F8E3, 'M', u'𣑭'), - (0x2F8E4, 'M', u'梎'), - (0x2F8E5, 'M', u'栟'), - (0x2F8E6, 'M', u'椔'), - (0x2F8E7, 'M', u'㮝'), - (0x2F8E8, 'M', u'楂'), - (0x2F8E9, 'M', u'榣'), - (0x2F8EA, 'M', u'槪'), - (0x2F8EB, 'M', u'檨'), - (0x2F8EC, 'M', u'𣚣'), - (0x2F8ED, 'M', u'櫛'), - (0x2F8EE, 'M', u'㰘'), - (0x2F8EF, 'M', u'次'), - (0x2F8F0, 'M', u'𣢧'), - (0x2F8F1, 'M', u'歔'), - (0x2F8F2, 'M', u'㱎'), - (0x2F8F3, 'M', u'歲'), - (0x2F8F4, 'M', u'殟'), - (0x2F8F5, 'M', u'殺'), - (0x2F8F6, 'M', u'殻'), - (0x2F8F7, 'M', u'𣪍'), - (0x2F8F8, 'M', u'𡴋'), - (0x2F8F9, 'M', u'𣫺'), - (0x2F8FA, 'M', u'汎'), - (0x2F8FB, 'M', u'𣲼'), - (0x2F8FC, 'M', u'沿'), - (0x2F8FD, 'M', u'泍'), - (0x2F8FE, 'M', u'汧'), - (0x2F8FF, 'M', u'洖'), - (0x2F900, 'M', u'派'), - (0x2F901, 'M', u'海'), - (0x2F902, 'M', u'流'), - (0x2F903, 'M', u'浩'), - (0x2F904, 'M', u'浸'), - (0x2F905, 'M', u'涅'), - (0x2F906, 'M', u'𣴞'), - (0x2F907, 'M', u'洴'), - (0x2F908, 'M', u'港'), - (0x2F909, 'M', u'湮'), - (0x2F90A, 'M', u'㴳'), - (0x2F90B, 'M', u'滋'), - (0x2F90C, 'M', u'滇'), - (0x2F90D, 'M', u'𣻑'), - (0x2F90E, 'M', u'淹'), - (0x2F90F, 'M', u'潮'), - (0x2F910, 'M', u'𣽞'), - (0x2F911, 'M', u'𣾎'), - (0x2F912, 'M', u'濆'), - (0x2F913, 'M', u'瀹'), - (0x2F914, 'M', u'瀞'), - (0x2F915, 'M', u'瀛'), - (0x2F916, 'M', u'㶖'), - (0x2F917, 'M', u'灊'), - (0x2F918, 'M', u'災'), - (0x2F919, 'M', u'灷'), - (0x2F91A, 'M', u'炭'), - (0x2F91B, 'M', u'𠔥'), - (0x2F91C, 'M', u'煅'), - (0x2F91D, 'M', u'𤉣'), - (0x2F91E, 'M', u'熜'), + (0x2F8C3, 'M', '摩'), + (0x2F8C4, 'M', '摾'), + (0x2F8C5, 'M', '撝'), + (0x2F8C6, 'M', '摷'), + (0x2F8C7, 'M', '㩬'), + (0x2F8C8, 'M', '敏'), + (0x2F8C9, 'M', '敬'), + (0x2F8CA, 'M', '𣀊'), + (0x2F8CB, 'M', '旣'), + (0x2F8CC, 'M', '書'), + (0x2F8CD, 'M', '晉'), + (0x2F8CE, 'M', '㬙'), + (0x2F8CF, 'M', '暑'), + (0x2F8D0, 'M', '㬈'), + (0x2F8D1, 'M', '㫤'), + (0x2F8D2, 'M', '冒'), + (0x2F8D3, 'M', '冕'), + (0x2F8D4, 'M', '最'), + (0x2F8D5, 'M', '暜'), + (0x2F8D6, 'M', '肭'), + (0x2F8D7, 'M', '䏙'), + (0x2F8D8, 'M', '朗'), + (0x2F8D9, 'M', '望'), + (0x2F8DA, 'M', '朡'), + (0x2F8DB, 'M', '杞'), + (0x2F8DC, 'M', '杓'), + (0x2F8DD, 'M', '𣏃'), + (0x2F8DE, 'M', '㭉'), + (0x2F8DF, 'M', '柺'), + (0x2F8E0, 'M', '枅'), + (0x2F8E1, 'M', '桒'), + (0x2F8E2, 'M', '梅'), + (0x2F8E3, 'M', '𣑭'), + (0x2F8E4, 'M', '梎'), + (0x2F8E5, 'M', '栟'), + (0x2F8E6, 'M', '椔'), + (0x2F8E7, 'M', '㮝'), + (0x2F8E8, 'M', '楂'), + (0x2F8E9, 'M', '榣'), + (0x2F8EA, 'M', '槪'), + (0x2F8EB, 'M', '檨'), + (0x2F8EC, 'M', '𣚣'), + (0x2F8ED, 'M', '櫛'), + (0x2F8EE, 'M', '㰘'), + (0x2F8EF, 'M', '次'), + (0x2F8F0, 'M', '𣢧'), + (0x2F8F1, 'M', '歔'), + (0x2F8F2, 'M', '㱎'), + (0x2F8F3, 'M', '歲'), + (0x2F8F4, 'M', '殟'), + (0x2F8F5, 'M', '殺'), + (0x2F8F6, 'M', '殻'), + (0x2F8F7, 'M', '𣪍'), + (0x2F8F8, 'M', '𡴋'), + (0x2F8F9, 'M', '𣫺'), + (0x2F8FA, 'M', '汎'), + (0x2F8FB, 'M', '𣲼'), + (0x2F8FC, 'M', '沿'), + (0x2F8FD, 'M', '泍'), + (0x2F8FE, 'M', '汧'), + (0x2F8FF, 'M', '洖'), + (0x2F900, 'M', '派'), + (0x2F901, 'M', '海'), + (0x2F902, 'M', '流'), + (0x2F903, 'M', '浩'), + (0x2F904, 'M', '浸'), + (0x2F905, 'M', '涅'), + (0x2F906, 'M', '𣴞'), + (0x2F907, 'M', '洴'), + (0x2F908, 'M', '港'), + (0x2F909, 'M', '湮'), + (0x2F90A, 'M', '㴳'), + (0x2F90B, 'M', '滋'), + (0x2F90C, 'M', '滇'), + (0x2F90D, 'M', '𣻑'), + (0x2F90E, 'M', '淹'), + (0x2F90F, 'M', '潮'), + (0x2F910, 'M', '𣽞'), + (0x2F911, 'M', '𣾎'), + (0x2F912, 'M', '濆'), + (0x2F913, 'M', '瀹'), + (0x2F914, 'M', '瀞'), + (0x2F915, 'M', '瀛'), + (0x2F916, 'M', '㶖'), + (0x2F917, 'M', '灊'), + (0x2F918, 'M', '災'), + (0x2F919, 'M', '灷'), + (0x2F91A, 'M', '炭'), + (0x2F91B, 'M', '𠔥'), + (0x2F91C, 'M', '煅'), + (0x2F91D, 'M', '𤉣'), + (0x2F91E, 'M', '熜'), (0x2F91F, 'X'), - (0x2F920, 'M', u'爨'), - (0x2F921, 'M', u'爵'), - (0x2F922, 'M', u'牐'), - (0x2F923, 'M', u'𤘈'), - (0x2F924, 'M', u'犀'), - (0x2F925, 'M', u'犕'), - (0x2F926, 'M', u'𤜵'), + (0x2F920, 'M', '爨'), + (0x2F921, 'M', '爵'), + (0x2F922, 'M', '牐'), + (0x2F923, 'M', '𤘈'), + (0x2F924, 'M', '犀'), + (0x2F925, 'M', '犕'), + (0x2F926, 'M', '𤜵'), ] def _seg_77(): return [ - (0x2F927, 'M', u'𤠔'), - (0x2F928, 'M', u'獺'), - (0x2F929, 'M', u'王'), - (0x2F92A, 'M', u'㺬'), - (0x2F92B, 'M', u'玥'), - (0x2F92C, 'M', u'㺸'), - (0x2F92E, 'M', u'瑇'), - (0x2F92F, 'M', u'瑜'), - (0x2F930, 'M', u'瑱'), - (0x2F931, 'M', u'璅'), - (0x2F932, 'M', u'瓊'), - (0x2F933, 'M', u'㼛'), - (0x2F934, 'M', u'甤'), - (0x2F935, 'M', u'𤰶'), - (0x2F936, 'M', u'甾'), - (0x2F937, 'M', u'𤲒'), - (0x2F938, 'M', u'異'), - (0x2F939, 'M', u'𢆟'), - (0x2F93A, 'M', u'瘐'), - (0x2F93B, 'M', u'𤾡'), - (0x2F93C, 'M', u'𤾸'), - (0x2F93D, 'M', u'𥁄'), - (0x2F93E, 'M', u'㿼'), - (0x2F93F, 'M', u'䀈'), - (0x2F940, 'M', u'直'), - (0x2F941, 'M', u'𥃳'), - (0x2F942, 'M', u'𥃲'), - (0x2F943, 'M', u'𥄙'), - (0x2F944, 'M', u'𥄳'), - (0x2F945, 'M', u'眞'), - (0x2F946, 'M', u'真'), - (0x2F948, 'M', u'睊'), - (0x2F949, 'M', u'䀹'), - (0x2F94A, 'M', u'瞋'), - (0x2F94B, 'M', u'䁆'), - (0x2F94C, 'M', u'䂖'), - (0x2F94D, 'M', u'𥐝'), - (0x2F94E, 'M', u'硎'), - (0x2F94F, 'M', u'碌'), - (0x2F950, 'M', u'磌'), - (0x2F951, 'M', u'䃣'), - (0x2F952, 'M', u'𥘦'), - (0x2F953, 'M', u'祖'), - (0x2F954, 'M', u'𥚚'), - (0x2F955, 'M', u'𥛅'), - (0x2F956, 'M', u'福'), - (0x2F957, 'M', u'秫'), - (0x2F958, 'M', u'䄯'), - (0x2F959, 'M', u'穀'), - (0x2F95A, 'M', u'穊'), - (0x2F95B, 'M', u'穏'), - (0x2F95C, 'M', u'𥥼'), - (0x2F95D, 'M', u'𥪧'), + (0x2F927, 'M', '𤠔'), + (0x2F928, 'M', '獺'), + (0x2F929, 'M', '王'), + (0x2F92A, 'M', '㺬'), + (0x2F92B, 'M', '玥'), + (0x2F92C, 'M', '㺸'), + (0x2F92E, 'M', '瑇'), + (0x2F92F, 'M', '瑜'), + (0x2F930, 'M', '瑱'), + (0x2F931, 'M', '璅'), + (0x2F932, 'M', '瓊'), + (0x2F933, 'M', '㼛'), + (0x2F934, 'M', '甤'), + (0x2F935, 'M', '𤰶'), + (0x2F936, 'M', '甾'), + (0x2F937, 'M', '𤲒'), + (0x2F938, 'M', '異'), + (0x2F939, 'M', '𢆟'), + (0x2F93A, 'M', '瘐'), + (0x2F93B, 'M', '𤾡'), + (0x2F93C, 'M', '𤾸'), + (0x2F93D, 'M', '𥁄'), + (0x2F93E, 'M', '㿼'), + (0x2F93F, 'M', '䀈'), + (0x2F940, 'M', '直'), + (0x2F941, 'M', '𥃳'), + (0x2F942, 'M', '𥃲'), + (0x2F943, 'M', '𥄙'), + (0x2F944, 'M', '𥄳'), + (0x2F945, 'M', '眞'), + (0x2F946, 'M', '真'), + (0x2F948, 'M', '睊'), + (0x2F949, 'M', '䀹'), + (0x2F94A, 'M', '瞋'), + (0x2F94B, 'M', '䁆'), + (0x2F94C, 'M', '䂖'), + (0x2F94D, 'M', '𥐝'), + (0x2F94E, 'M', '硎'), + (0x2F94F, 'M', '碌'), + (0x2F950, 'M', '磌'), + (0x2F951, 'M', '䃣'), + (0x2F952, 'M', '𥘦'), + (0x2F953, 'M', '祖'), + (0x2F954, 'M', '𥚚'), + (0x2F955, 'M', '𥛅'), + (0x2F956, 'M', '福'), + (0x2F957, 'M', '秫'), + (0x2F958, 'M', '䄯'), + (0x2F959, 'M', '穀'), + (0x2F95A, 'M', '穊'), + (0x2F95B, 'M', '穏'), + (0x2F95C, 'M', '𥥼'), + (0x2F95D, 'M', '𥪧'), (0x2F95F, 'X'), - (0x2F960, 'M', u'䈂'), - (0x2F961, 'M', u'𥮫'), - (0x2F962, 'M', u'篆'), - (0x2F963, 'M', u'築'), - (0x2F964, 'M', u'䈧'), - (0x2F965, 'M', u'𥲀'), - (0x2F966, 'M', u'糒'), - (0x2F967, 'M', u'䊠'), - (0x2F968, 'M', u'糨'), - (0x2F969, 'M', u'糣'), - (0x2F96A, 'M', u'紀'), - (0x2F96B, 'M', u'𥾆'), - (0x2F96C, 'M', u'絣'), - (0x2F96D, 'M', u'䌁'), - (0x2F96E, 'M', u'緇'), - (0x2F96F, 'M', u'縂'), - (0x2F970, 'M', u'繅'), - (0x2F971, 'M', u'䌴'), - (0x2F972, 'M', u'𦈨'), - (0x2F973, 'M', u'𦉇'), - (0x2F974, 'M', u'䍙'), - (0x2F975, 'M', u'𦋙'), - (0x2F976, 'M', u'罺'), - (0x2F977, 'M', u'𦌾'), - (0x2F978, 'M', u'羕'), - (0x2F979, 'M', u'翺'), - (0x2F97A, 'M', u'者'), - (0x2F97B, 'M', u'𦓚'), - (0x2F97C, 'M', u'𦔣'), - (0x2F97D, 'M', u'聠'), - (0x2F97E, 'M', u'𦖨'), - (0x2F97F, 'M', u'聰'), - (0x2F980, 'M', u'𣍟'), - (0x2F981, 'M', u'䏕'), - (0x2F982, 'M', u'育'), - (0x2F983, 'M', u'脃'), - (0x2F984, 'M', u'䐋'), - (0x2F985, 'M', u'脾'), - (0x2F986, 'M', u'媵'), - (0x2F987, 'M', u'𦞧'), - (0x2F988, 'M', u'𦞵'), - (0x2F989, 'M', u'𣎓'), - (0x2F98A, 'M', u'𣎜'), - (0x2F98B, 'M', u'舁'), - (0x2F98C, 'M', u'舄'), - (0x2F98D, 'M', u'辞'), + (0x2F960, 'M', '䈂'), + (0x2F961, 'M', '𥮫'), + (0x2F962, 'M', '篆'), + (0x2F963, 'M', '築'), + (0x2F964, 'M', '䈧'), + (0x2F965, 'M', '𥲀'), + (0x2F966, 'M', '糒'), + (0x2F967, 'M', '䊠'), + (0x2F968, 'M', '糨'), + (0x2F969, 'M', '糣'), + (0x2F96A, 'M', '紀'), + (0x2F96B, 'M', '𥾆'), + (0x2F96C, 'M', '絣'), + (0x2F96D, 'M', '䌁'), + (0x2F96E, 'M', '緇'), + (0x2F96F, 'M', '縂'), + (0x2F970, 'M', '繅'), + (0x2F971, 'M', '䌴'), + (0x2F972, 'M', '𦈨'), + (0x2F973, 'M', '𦉇'), + (0x2F974, 'M', '䍙'), + (0x2F975, 'M', '𦋙'), + (0x2F976, 'M', '罺'), + (0x2F977, 'M', '𦌾'), + (0x2F978, 'M', '羕'), + (0x2F979, 'M', '翺'), + (0x2F97A, 'M', '者'), + (0x2F97B, 'M', '𦓚'), + (0x2F97C, 'M', '𦔣'), + (0x2F97D, 'M', '聠'), + (0x2F97E, 'M', '𦖨'), + (0x2F97F, 'M', '聰'), + (0x2F980, 'M', '𣍟'), + (0x2F981, 'M', '䏕'), + (0x2F982, 'M', '育'), + (0x2F983, 'M', '脃'), + (0x2F984, 'M', '䐋'), + (0x2F985, 'M', '脾'), + (0x2F986, 'M', '媵'), + (0x2F987, 'M', '𦞧'), + (0x2F988, 'M', '𦞵'), + (0x2F989, 'M', '𣎓'), + (0x2F98A, 'M', '𣎜'), + (0x2F98B, 'M', '舁'), + (0x2F98C, 'M', '舄'), + (0x2F98D, 'M', '辞'), ] def _seg_78(): return [ - (0x2F98E, 'M', u'䑫'), - (0x2F98F, 'M', u'芑'), - (0x2F990, 'M', u'芋'), - (0x2F991, 'M', u'芝'), - (0x2F992, 'M', u'劳'), - (0x2F993, 'M', u'花'), - (0x2F994, 'M', u'芳'), - (0x2F995, 'M', u'芽'), - (0x2F996, 'M', u'苦'), - (0x2F997, 'M', u'𦬼'), - (0x2F998, 'M', u'若'), - (0x2F999, 'M', u'茝'), - (0x2F99A, 'M', u'荣'), - (0x2F99B, 'M', u'莭'), - (0x2F99C, 'M', u'茣'), - (0x2F99D, 'M', u'莽'), - (0x2F99E, 'M', u'菧'), - (0x2F99F, 'M', u'著'), - (0x2F9A0, 'M', u'荓'), - (0x2F9A1, 'M', u'菊'), - (0x2F9A2, 'M', u'菌'), - (0x2F9A3, 'M', u'菜'), - (0x2F9A4, 'M', u'𦰶'), - (0x2F9A5, 'M', u'𦵫'), - (0x2F9A6, 'M', u'𦳕'), - (0x2F9A7, 'M', u'䔫'), - (0x2F9A8, 'M', u'蓱'), - (0x2F9A9, 'M', u'蓳'), - (0x2F9AA, 'M', u'蔖'), - (0x2F9AB, 'M', u'𧏊'), - (0x2F9AC, 'M', u'蕤'), - (0x2F9AD, 'M', u'𦼬'), - (0x2F9AE, 'M', u'䕝'), - (0x2F9AF, 'M', u'䕡'), - (0x2F9B0, 'M', u'𦾱'), - (0x2F9B1, 'M', u'𧃒'), - (0x2F9B2, 'M', u'䕫'), - (0x2F9B3, 'M', u'虐'), - (0x2F9B4, 'M', u'虜'), - (0x2F9B5, 'M', u'虧'), - (0x2F9B6, 'M', u'虩'), - (0x2F9B7, 'M', u'蚩'), - (0x2F9B8, 'M', u'蚈'), - (0x2F9B9, 'M', u'蜎'), - (0x2F9BA, 'M', u'蛢'), - (0x2F9BB, 'M', u'蝹'), - (0x2F9BC, 'M', u'蜨'), - (0x2F9BD, 'M', u'蝫'), - (0x2F9BE, 'M', u'螆'), + (0x2F98E, 'M', '䑫'), + (0x2F98F, 'M', '芑'), + (0x2F990, 'M', '芋'), + (0x2F991, 'M', '芝'), + (0x2F992, 'M', '劳'), + (0x2F993, 'M', '花'), + (0x2F994, 'M', '芳'), + (0x2F995, 'M', '芽'), + (0x2F996, 'M', '苦'), + (0x2F997, 'M', '𦬼'), + (0x2F998, 'M', '若'), + (0x2F999, 'M', '茝'), + (0x2F99A, 'M', '荣'), + (0x2F99B, 'M', '莭'), + (0x2F99C, 'M', '茣'), + (0x2F99D, 'M', '莽'), + (0x2F99E, 'M', '菧'), + (0x2F99F, 'M', '著'), + (0x2F9A0, 'M', '荓'), + (0x2F9A1, 'M', '菊'), + (0x2F9A2, 'M', '菌'), + (0x2F9A3, 'M', '菜'), + (0x2F9A4, 'M', '𦰶'), + (0x2F9A5, 'M', '𦵫'), + (0x2F9A6, 'M', '𦳕'), + (0x2F9A7, 'M', '䔫'), + (0x2F9A8, 'M', '蓱'), + (0x2F9A9, 'M', '蓳'), + (0x2F9AA, 'M', '蔖'), + (0x2F9AB, 'M', '𧏊'), + (0x2F9AC, 'M', '蕤'), + (0x2F9AD, 'M', '𦼬'), + (0x2F9AE, 'M', '䕝'), + (0x2F9AF, 'M', '䕡'), + (0x2F9B0, 'M', '𦾱'), + (0x2F9B1, 'M', '𧃒'), + (0x2F9B2, 'M', '䕫'), + (0x2F9B3, 'M', '虐'), + (0x2F9B4, 'M', '虜'), + (0x2F9B5, 'M', '虧'), + (0x2F9B6, 'M', '虩'), + (0x2F9B7, 'M', '蚩'), + (0x2F9B8, 'M', '蚈'), + (0x2F9B9, 'M', '蜎'), + (0x2F9BA, 'M', '蛢'), + (0x2F9BB, 'M', '蝹'), + (0x2F9BC, 'M', '蜨'), + (0x2F9BD, 'M', '蝫'), + (0x2F9BE, 'M', '螆'), (0x2F9BF, 'X'), - (0x2F9C0, 'M', u'蟡'), - (0x2F9C1, 'M', u'蠁'), - (0x2F9C2, 'M', u'䗹'), - (0x2F9C3, 'M', u'衠'), - (0x2F9C4, 'M', u'衣'), - (0x2F9C5, 'M', u'𧙧'), - (0x2F9C6, 'M', u'裗'), - (0x2F9C7, 'M', u'裞'), - (0x2F9C8, 'M', u'䘵'), - (0x2F9C9, 'M', u'裺'), - (0x2F9CA, 'M', u'㒻'), - (0x2F9CB, 'M', u'𧢮'), - (0x2F9CC, 'M', u'𧥦'), - (0x2F9CD, 'M', u'䚾'), - (0x2F9CE, 'M', u'䛇'), - (0x2F9CF, 'M', u'誠'), - (0x2F9D0, 'M', u'諭'), - (0x2F9D1, 'M', u'變'), - (0x2F9D2, 'M', u'豕'), - (0x2F9D3, 'M', u'𧲨'), - (0x2F9D4, 'M', u'貫'), - (0x2F9D5, 'M', u'賁'), - (0x2F9D6, 'M', u'贛'), - (0x2F9D7, 'M', u'起'), - (0x2F9D8, 'M', u'𧼯'), - (0x2F9D9, 'M', u'𠠄'), - (0x2F9DA, 'M', u'跋'), - (0x2F9DB, 'M', u'趼'), - (0x2F9DC, 'M', u'跰'), - (0x2F9DD, 'M', u'𠣞'), - (0x2F9DE, 'M', u'軔'), - (0x2F9DF, 'M', u'輸'), - (0x2F9E0, 'M', u'𨗒'), - (0x2F9E1, 'M', u'𨗭'), - (0x2F9E2, 'M', u'邔'), - (0x2F9E3, 'M', u'郱'), - (0x2F9E4, 'M', u'鄑'), - (0x2F9E5, 'M', u'𨜮'), - (0x2F9E6, 'M', u'鄛'), - (0x2F9E7, 'M', u'鈸'), - (0x2F9E8, 'M', u'鋗'), - (0x2F9E9, 'M', u'鋘'), - (0x2F9EA, 'M', u'鉼'), - (0x2F9EB, 'M', u'鏹'), - (0x2F9EC, 'M', u'鐕'), - (0x2F9ED, 'M', u'𨯺'), - (0x2F9EE, 'M', u'開'), - (0x2F9EF, 'M', u'䦕'), - (0x2F9F0, 'M', u'閷'), - (0x2F9F1, 'M', u'𨵷'), + (0x2F9C0, 'M', '蟡'), + (0x2F9C1, 'M', '蠁'), + (0x2F9C2, 'M', '䗹'), + (0x2F9C3, 'M', '衠'), + (0x2F9C4, 'M', '衣'), + (0x2F9C5, 'M', '𧙧'), + (0x2F9C6, 'M', '裗'), + (0x2F9C7, 'M', '裞'), + (0x2F9C8, 'M', '䘵'), + (0x2F9C9, 'M', '裺'), + (0x2F9CA, 'M', '㒻'), + (0x2F9CB, 'M', '𧢮'), + (0x2F9CC, 'M', '𧥦'), + (0x2F9CD, 'M', '䚾'), + (0x2F9CE, 'M', '䛇'), + (0x2F9CF, 'M', '誠'), + (0x2F9D0, 'M', '諭'), + (0x2F9D1, 'M', '變'), + (0x2F9D2, 'M', '豕'), + (0x2F9D3, 'M', '𧲨'), + (0x2F9D4, 'M', '貫'), + (0x2F9D5, 'M', '賁'), + (0x2F9D6, 'M', '贛'), + (0x2F9D7, 'M', '起'), + (0x2F9D8, 'M', '𧼯'), + (0x2F9D9, 'M', '𠠄'), + (0x2F9DA, 'M', '跋'), + (0x2F9DB, 'M', '趼'), + (0x2F9DC, 'M', '跰'), + (0x2F9DD, 'M', '𠣞'), + (0x2F9DE, 'M', '軔'), + (0x2F9DF, 'M', '輸'), + (0x2F9E0, 'M', '𨗒'), + (0x2F9E1, 'M', '𨗭'), + (0x2F9E2, 'M', '邔'), + (0x2F9E3, 'M', '郱'), + (0x2F9E4, 'M', '鄑'), + (0x2F9E5, 'M', '𨜮'), + (0x2F9E6, 'M', '鄛'), + (0x2F9E7, 'M', '鈸'), + (0x2F9E8, 'M', '鋗'), + (0x2F9E9, 'M', '鋘'), + (0x2F9EA, 'M', '鉼'), + (0x2F9EB, 'M', '鏹'), + (0x2F9EC, 'M', '鐕'), + (0x2F9ED, 'M', '𨯺'), + (0x2F9EE, 'M', '開'), + (0x2F9EF, 'M', '䦕'), + (0x2F9F0, 'M', '閷'), + (0x2F9F1, 'M', '𨵷'), ] def _seg_79(): return [ - (0x2F9F2, 'M', u'䧦'), - (0x2F9F3, 'M', u'雃'), - (0x2F9F4, 'M', u'嶲'), - (0x2F9F5, 'M', u'霣'), - (0x2F9F6, 'M', u'𩅅'), - (0x2F9F7, 'M', u'𩈚'), - (0x2F9F8, 'M', u'䩮'), - (0x2F9F9, 'M', u'䩶'), - (0x2F9FA, 'M', u'韠'), - (0x2F9FB, 'M', u'𩐊'), - (0x2F9FC, 'M', u'䪲'), - (0x2F9FD, 'M', u'𩒖'), - (0x2F9FE, 'M', u'頋'), - (0x2FA00, 'M', u'頩'), - (0x2FA01, 'M', u'𩖶'), - (0x2FA02, 'M', u'飢'), - (0x2FA03, 'M', u'䬳'), - (0x2FA04, 'M', u'餩'), - (0x2FA05, 'M', u'馧'), - (0x2FA06, 'M', u'駂'), - (0x2FA07, 'M', u'駾'), - (0x2FA08, 'M', u'䯎'), - (0x2FA09, 'M', u'𩬰'), - (0x2FA0A, 'M', u'鬒'), - (0x2FA0B, 'M', u'鱀'), - (0x2FA0C, 'M', u'鳽'), - (0x2FA0D, 'M', u'䳎'), - (0x2FA0E, 'M', u'䳭'), - (0x2FA0F, 'M', u'鵧'), - (0x2FA10, 'M', u'𪃎'), - (0x2FA11, 'M', u'䳸'), - (0x2FA12, 'M', u'𪄅'), - (0x2FA13, 'M', u'𪈎'), - (0x2FA14, 'M', u'𪊑'), - (0x2FA15, 'M', u'麻'), - (0x2FA16, 'M', u'䵖'), - (0x2FA17, 'M', u'黹'), - (0x2FA18, 'M', u'黾'), - (0x2FA19, 'M', u'鼅'), - (0x2FA1A, 'M', u'鼏'), - (0x2FA1B, 'M', u'鼖'), - (0x2FA1C, 'M', u'鼻'), - (0x2FA1D, 'M', u'𪘀'), + (0x2F9F2, 'M', '䧦'), + (0x2F9F3, 'M', '雃'), + (0x2F9F4, 'M', '嶲'), + (0x2F9F5, 'M', '霣'), + (0x2F9F6, 'M', '𩅅'), + (0x2F9F7, 'M', '𩈚'), + (0x2F9F8, 'M', '䩮'), + (0x2F9F9, 'M', '䩶'), + (0x2F9FA, 'M', '韠'), + (0x2F9FB, 'M', '𩐊'), + (0x2F9FC, 'M', '䪲'), + (0x2F9FD, 'M', '𩒖'), + (0x2F9FE, 'M', '頋'), + (0x2FA00, 'M', '頩'), + (0x2FA01, 'M', '𩖶'), + (0x2FA02, 'M', '飢'), + (0x2FA03, 'M', '䬳'), + (0x2FA04, 'M', '餩'), + (0x2FA05, 'M', '馧'), + (0x2FA06, 'M', '駂'), + (0x2FA07, 'M', '駾'), + (0x2FA08, 'M', '䯎'), + (0x2FA09, 'M', '𩬰'), + (0x2FA0A, 'M', '鬒'), + (0x2FA0B, 'M', '鱀'), + (0x2FA0C, 'M', '鳽'), + (0x2FA0D, 'M', '䳎'), + (0x2FA0E, 'M', '䳭'), + (0x2FA0F, 'M', '鵧'), + (0x2FA10, 'M', '𪃎'), + (0x2FA11, 'M', '䳸'), + (0x2FA12, 'M', '𪄅'), + (0x2FA13, 'M', '𪈎'), + (0x2FA14, 'M', '𪊑'), + (0x2FA15, 'M', '麻'), + (0x2FA16, 'M', '䵖'), + (0x2FA17, 'M', '黹'), + (0x2FA18, 'M', '黾'), + (0x2FA19, 'M', '鼅'), + (0x2FA1A, 'M', '鼏'), + (0x2FA1B, 'M', '鼖'), + (0x2FA1C, 'M', '鼻'), + (0x2FA1D, 'M', '𪘀'), (0x2FA1E, 'X'), (0x30000, 'V'), (0x3134B, 'X'), diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index 713f42d4e38..aa08874dc58 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -12,7 +12,7 @@ pyparsing==2.4.7 requests==2.25.1 certifi==2020.12.05 chardet==4.0.0 - idna==2.10 + idna==3.1 urllib3==1.26.4 resolvelib==0.7.0 setuptools==44.0.0 From d69a2d7bab9a32b86cf162f8157a18179b9ba13d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@gmail.com> Date: Sat, 17 Apr 2021 16:28:50 +0200 Subject: [PATCH 3154/3170] Upgrade tenacity to 7.0.0 --- news/tenacity.vendor.rst | 2 +- src/pip/_vendor/tenacity/__init__.py | 177 +++++++------- src/pip/_vendor/tenacity/_asyncio.py | 10 +- src/pip/_vendor/tenacity/_utils.py | 15 +- src/pip/_vendor/tenacity/after.py | 17 +- src/pip/_vendor/tenacity/before.py | 11 +- src/pip/_vendor/tenacity/before_sleep.py | 21 +- src/pip/_vendor/tenacity/compat.py | 299 ----------------------- src/pip/_vendor/tenacity/retry.py | 41 ++-- src/pip/_vendor/tenacity/stop.py | 18 +- src/pip/_vendor/tenacity/tornadoweb.py | 10 +- src/pip/_vendor/tenacity/wait.py | 34 +-- src/pip/_vendor/vendor.txt | 2 +- 13 files changed, 177 insertions(+), 480 deletions(-) diff --git a/news/tenacity.vendor.rst b/news/tenacity.vendor.rst index b6938a56df0..01d2838892a 100644 --- a/news/tenacity.vendor.rst +++ b/news/tenacity.vendor.rst @@ -1 +1 @@ -Switch from retrying to tenacity +Upgrade tenacity to 7.0.0 diff --git a/src/pip/_vendor/tenacity/__init__.py b/src/pip/_vendor/tenacity/__init__.py index 339ca4c7283..5f8cb505896 100644 --- a/src/pip/_vendor/tenacity/__init__.py +++ b/src/pip/_vendor/tenacity/__init__.py @@ -38,9 +38,9 @@ from pip._vendor import six from pip._vendor.tenacity import _utils -from pip._vendor.tenacity import compat as _compat # Import all built-in retry strategies for easier usage. +from .retry import retry_base # noqa from .retry import retry_all # noqa from .retry import retry_always # noqa from .retry import retry_any # noqa @@ -116,11 +116,23 @@ def retry(*dargs, **dkw): # noqa if len(dargs) == 1 and callable(dargs[0]): return retry()(dargs[0]) else: + def wrap(f): + if isinstance(f, retry_base): + warnings.warn( + ( + "Got retry_base instance ({cls}) as callable argument, " + + "this will probably hang indefinitely (did you mean " + + "retry={cls}(...)?)" + ).format(cls=f.__class__.__name__) + ) if iscoroutinefunction is not None and iscoroutinefunction(f): r = AsyncRetrying(*dargs, **dkw) - elif tornado and hasattr(tornado.gen, 'is_coroutine_function') \ - and tornado.gen.is_coroutine_function(f): + elif ( + tornado + and hasattr(tornado.gen, "is_coroutine_function") + and tornado.gen.is_coroutine_function(f) + ): r = TornadoRetrying(*dargs, **dkw) else: r = Retrying(*dargs, **dkw) @@ -158,17 +170,18 @@ class BaseAction(object): NAME = None def __repr__(self): - state_str = ', '.join('%s=%r' % (field, getattr(self, field)) - for field in self.REPR_FIELDS) - return '%s(%s)' % (type(self).__name__, state_str) + state_str = ", ".join( + "%s=%r" % (field, getattr(self, field)) for field in self.REPR_FIELDS + ) + return "%s(%s)" % (type(self).__name__, state_str) def __str__(self): return repr(self) class RetryAction(BaseAction): - REPR_FIELDS = ('sleep',) - NAME = 'retry' + REPR_FIELDS = ("sleep",) + NAME = "retry" def __init__(self, sleep): self.sleep = float(sleep) @@ -177,6 +190,10 @@ def __init__(self, sleep): _unset = object() +def _first_set(first, second): + return second if first is _unset else first + + class RetryError(Exception): """Encapsulates the last attempt instance right before giving up.""" @@ -214,86 +231,74 @@ def __exit__(self, exc_type, exc_value, traceback): class BaseRetrying(object): __metaclass__ = ABCMeta - def __init__(self, - sleep=sleep, - stop=stop_never, wait=wait_none(), - retry=retry_if_exception_type(), - before=before_nothing, - after=after_nothing, - before_sleep=None, - reraise=False, - retry_error_cls=RetryError, - retry_error_callback=None): + def __init__( + self, + sleep=sleep, + stop=stop_never, + wait=wait_none(), + retry=retry_if_exception_type(), + before=before_nothing, + after=after_nothing, + before_sleep=None, + reraise=False, + retry_error_cls=RetryError, + retry_error_callback=None, + ): self.sleep = sleep - self._stop = stop - self._wait = wait - self._retry = retry - self._before = before - self._after = after - self._before_sleep = before_sleep + self.stop = stop + self.wait = wait + self.retry = retry + self.before = before + self.after = after + self.before_sleep = before_sleep self.reraise = reraise self._local = threading.local() self.retry_error_cls = retry_error_cls - self._retry_error_callback = retry_error_callback + self.retry_error_callback = retry_error_callback # This attribute was moved to RetryCallState and is deprecated on # Retrying objects but kept for backward compatibility. self.fn = None - @_utils.cached_property - def stop(self): - return _compat.stop_func_accept_retry_state(self._stop) - - @_utils.cached_property - def wait(self): - return _compat.wait_func_accept_retry_state(self._wait) - - @_utils.cached_property - def retry(self): - return _compat.retry_func_accept_retry_state(self._retry) - - @_utils.cached_property - def before(self): - return _compat.before_func_accept_retry_state(self._before) - - @_utils.cached_property - def after(self): - return _compat.after_func_accept_retry_state(self._after) - - @_utils.cached_property - def before_sleep(self): - return _compat.before_sleep_func_accept_retry_state(self._before_sleep) - - @_utils.cached_property - def retry_error_callback(self): - return _compat.retry_error_callback_accept_retry_state( - self._retry_error_callback) - - def copy(self, sleep=_unset, stop=_unset, wait=_unset, - retry=_unset, before=_unset, after=_unset, before_sleep=_unset, - reraise=_unset): + def copy( + self, + sleep=_unset, + stop=_unset, + wait=_unset, + retry=_unset, + before=_unset, + after=_unset, + before_sleep=_unset, + reraise=_unset, + retry_error_cls=_unset, + retry_error_callback=_unset, + ): """Copy this object with some parameters changed if needed.""" - if before_sleep is _unset: - before_sleep = self.before_sleep return self.__class__( - sleep=self.sleep if sleep is _unset else sleep, - stop=self.stop if stop is _unset else stop, - wait=self.wait if wait is _unset else wait, - retry=self.retry if retry is _unset else retry, - before=self.before if before is _unset else before, - after=self.after if after is _unset else after, - before_sleep=before_sleep, - reraise=self.reraise if after is _unset else reraise, + sleep=_first_set(sleep, self.sleep), + stop=_first_set(stop, self.stop), + wait=_first_set(wait, self.wait), + retry=_first_set(retry, self.retry), + before=_first_set(before, self.before), + after=_first_set(after, self.after), + before_sleep=_first_set(before_sleep, self.before_sleep), + reraise=_first_set(reraise, self.reraise), + retry_error_cls=_first_set(retry_error_cls, self.retry_error_cls), + retry_error_callback=_first_set( + retry_error_callback, self.retry_error_callback + ), ) def __repr__(self): attrs = dict( - _utils.visible_attrs(self, attrs={'me': id(self)}), + _utils.visible_attrs(self, attrs={"me": id(self)}), __class__=self.__class__.__name__, ) - return ("<%(__class__)s object at 0x%(me)x (stop=%(stop)s, " - "wait=%(wait)s, sleep=%(sleep)s, retry=%(retry)s, " - "before=%(before)s, after=%(after)s)>") % (attrs) + return ( + "<%(__class__)s object at 0x%(me)x (stop=%(stop)s, " + "wait=%(wait)s, sleep=%(sleep)s, retry=%(retry)s, " + "before=%(before)s, after=%(after)s)>" + ) % (attrs) @property def statistics(self): @@ -328,6 +333,7 @@ def wraps(self, f): :param f: A function to wraps for retrying. """ + @_utils.wraps(f) def wrapped_f(*args, **kw): return self(f, *args, **kw) @@ -342,9 +348,9 @@ def retry_with(*args, **kwargs): def begin(self, fn): self.statistics.clear() - self.statistics['start_time'] = _utils.now() - self.statistics['attempt_number'] = 1 - self.statistics['idle_for'] = 0 + self.statistics["start_time"] = _utils.now() + self.statistics["attempt_number"] = 1 + self.statistics["idle_for"] = 0 self.fn = fn def iter(self, retry_state): # noqa @@ -354,16 +360,16 @@ def iter(self, retry_state): # noqa self.before(retry_state) return DoAttempt() - is_explicit_retry = retry_state.outcome.failed \ - and isinstance(retry_state.outcome.exception(), TryAgain) + is_explicit_retry = retry_state.outcome.failed and isinstance( + retry_state.outcome.exception(), TryAgain + ) if not (is_explicit_retry or self.retry(retry_state=retry_state)): return fut.result() if self.after is not None: self.after(retry_state=retry_state) - self.statistics['delay_since_first_attempt'] = \ - retry_state.seconds_since_start + self.statistics["delay_since_first_attempt"] = retry_state.seconds_since_start if self.stop(retry_state=retry_state): if self.retry_error_callback: return self.retry_error_callback(retry_state=retry_state) @@ -378,8 +384,8 @@ def iter(self, retry_state): # noqa sleep = 0.0 retry_state.next_action = RetryAction(sleep) retry_state.idle_for += sleep - self.statistics['idle_for'] += sleep - self.statistics['attempt_number'] += 1 + self.statistics["idle_for"] += sleep + self.statistics["attempt_number"] += 1 if self.before_sleep is not None: self.before_sleep(retry_state=retry_state) @@ -406,8 +412,10 @@ def __call__(self, *args, **kwargs): def call(self, *args, **kwargs): """Use ``__call__`` instead because this method is deprecated.""" - warnings.warn("'call()' method is deprecated. " + - "Use '__call__()' instead", DeprecationWarning) + warnings.warn( + "'call()' method is deprecated. " + "Use '__call__()' instead", + DeprecationWarning, + ) return self.__call__(*args, **kwargs) @@ -417,14 +425,13 @@ class Retrying(BaseRetrying): def __call__(self, fn, *args, **kwargs): self.begin(fn) - retry_state = RetryCallState( - retry_object=self, fn=fn, args=args, kwargs=kwargs) + retry_state = RetryCallState(retry_object=self, fn=fn, args=args, kwargs=kwargs) while True: do = self.iter(retry_state=retry_state) if isinstance(do, DoAttempt): try: result = fn(*args, **kwargs) - except BaseException: + except BaseException: # noqa: B902 retry_state.set_exception(sys.exc_info()) else: retry_state.set_result(result) diff --git a/src/pip/_vendor/tenacity/_asyncio.py b/src/pip/_vendor/tenacity/_asyncio.py index 51e348a3dc4..d9a2d4634fe 100644 --- a/src/pip/_vendor/tenacity/_asyncio.py +++ b/src/pip/_vendor/tenacity/_asyncio.py @@ -26,24 +26,20 @@ class AsyncRetrying(BaseRetrying): - - def __init__(self, - sleep=sleep, - **kwargs): + def __init__(self, sleep=sleep, **kwargs): super(AsyncRetrying, self).__init__(**kwargs) self.sleep = sleep async def __call__(self, fn, *args, **kwargs): self.begin(fn) - retry_state = RetryCallState( - retry_object=self, fn=fn, args=args, kwargs=kwargs) + retry_state = RetryCallState(retry_object=self, fn=fn, args=args, kwargs=kwargs) while True: do = self.iter(retry_state=retry_state) if isinstance(do, DoAttempt): try: result = await fn(*args, **kwargs) - except BaseException: + except BaseException: # noqa: B902 retry_state.set_exception(sys.exc_info()) else: retry_state.set_result(result) diff --git a/src/pip/_vendor/tenacity/_utils.py b/src/pip/_vendor/tenacity/_utils.py index 365b11d4b16..8c0ca788c4d 100644 --- a/src/pip/_vendor/tenacity/_utils.py +++ b/src/pip/_vendor/tenacity/_utils.py @@ -40,12 +40,15 @@ def wraps(fn): Also, see https://github.com/benjaminp/six/issues/250. """ + def filter_hasattr(obj, attrs): return tuple(a for a in attrs if hasattr(obj, a)) + return six.wraps( fn, assigned=filter_hasattr(fn, WRAPPER_ASSIGNMENTS), - updated=filter_hasattr(fn, WRAPPER_UPDATES)) + updated=filter_hasattr(fn, WRAPPER_UPDATES), + ) def capture(fut, tb): # TODO(harlowja): delete this in future, since its @@ -55,6 +58,8 @@ def capture(fut, tb): def getargspec(func): # This was deprecated in Python 3. return inspect.getargspec(func) + + else: from functools import wraps # noqa @@ -80,13 +85,13 @@ def find_ordinal(pos_num): if pos_num == 0: return "th" elif pos_num == 1: - return 'st' + return "st" elif pos_num == 2: - return 'nd' + return "nd" elif pos_num == 3: - return 'rd' + return "rd" elif pos_num >= 4 and pos_num <= 20: - return 'th' + return "th" else: return find_ordinal(pos_num % 10) diff --git a/src/pip/_vendor/tenacity/after.py b/src/pip/_vendor/tenacity/after.py index 8b6082c683a..c04e7c18325 100644 --- a/src/pip/_vendor/tenacity/after.py +++ b/src/pip/_vendor/tenacity/after.py @@ -23,13 +23,18 @@ def after_nothing(retry_state): def after_log(logger, log_level, sec_format="%0.3f"): """After call strategy that logs to some logger the finished attempt.""" - log_tpl = ("Finished call to '%s' after " + str(sec_format) + "(s), " - "this was the %s time calling it.") + log_tpl = ( + "Finished call to '%s' after " + str(sec_format) + "(s), " + "this was the %s time calling it." + ) def log_it(retry_state): - logger.log(log_level, log_tpl, - _utils.get_callback_name(retry_state.fn), - retry_state.seconds_since_start, - _utils.to_ordinal(retry_state.attempt_number)) + logger.log( + log_level, + log_tpl, + _utils.get_callback_name(retry_state.fn), + retry_state.seconds_since_start, + _utils.to_ordinal(retry_state.attempt_number), + ) return log_it diff --git a/src/pip/_vendor/tenacity/before.py b/src/pip/_vendor/tenacity/before.py index 3eab08afb9d..3229517d5f8 100644 --- a/src/pip/_vendor/tenacity/before.py +++ b/src/pip/_vendor/tenacity/before.py @@ -23,10 +23,13 @@ def before_nothing(retry_state): def before_log(logger, log_level): """Before call strategy that logs to some logger the attempt.""" + def log_it(retry_state): - logger.log(log_level, - "Starting call to '%s', this is the %s time calling it.", - _utils.get_callback_name(retry_state.fn), - _utils.to_ordinal(retry_state.attempt_number)) + logger.log( + log_level, + "Starting call to '%s', this is the %s time calling it.", + _utils.get_callback_name(retry_state.fn), + _utils.to_ordinal(retry_state.attempt_number), + ) return log_it diff --git a/src/pip/_vendor/tenacity/before_sleep.py b/src/pip/_vendor/tenacity/before_sleep.py index 4285922fd2d..a051acacb2e 100644 --- a/src/pip/_vendor/tenacity/before_sleep.py +++ b/src/pip/_vendor/tenacity/before_sleep.py @@ -24,23 +24,28 @@ def before_sleep_nothing(retry_state): def before_sleep_log(logger, log_level, exc_info=False): """Before call strategy that logs to some logger the attempt.""" + def log_it(retry_state): if retry_state.outcome.failed: ex = retry_state.outcome.exception() - verb, value = 'raised', '%s: %s' % (type(ex).__name__, ex) + verb, value = "raised", "%s: %s" % (type(ex).__name__, ex) if exc_info: local_exc_info = get_exc_info_from_future(retry_state.outcome) else: local_exc_info = False else: - verb, value = 'returned', retry_state.outcome.result() + verb, value = "returned", retry_state.outcome.result() local_exc_info = False # exc_info does not apply when no exception - logger.log(log_level, - "Retrying %s in %s seconds as it %s %s.", - _utils.get_callback_name(retry_state.fn), - getattr(retry_state.next_action, 'sleep'), - verb, value, - exc_info=local_exc_info) + logger.log( + log_level, + "Retrying %s in %s seconds as it %s %s.", + _utils.get_callback_name(retry_state.fn), + getattr(retry_state.next_action, "sleep"), + verb, + value, + exc_info=local_exc_info, + ) + return log_it diff --git a/src/pip/_vendor/tenacity/compat.py b/src/pip/_vendor/tenacity/compat.py index a71ba604813..ce4796b1330 100644 --- a/src/pip/_vendor/tenacity/compat.py +++ b/src/pip/_vendor/tenacity/compat.py @@ -1,305 +1,6 @@ """Utilities for providing backward compatibility.""" - -import inspect -from fractions import Fraction -from warnings import warn - from pip._vendor import six -from pip._vendor.tenacity import _utils - - -def warn_about_non_retry_state_deprecation(cbname, func, stacklevel): - msg = ( - '"%s" function must accept single "retry_state" parameter,' - ' please update %s' % (cbname, _utils.get_callback_name(func))) - warn(msg, DeprecationWarning, stacklevel=stacklevel + 1) - - -def warn_about_dunder_non_retry_state_deprecation(fn, stacklevel): - msg = ( - '"%s" method must be called with' - ' single "retry_state" parameter' % (_utils.get_callback_name(fn))) - warn(msg, DeprecationWarning, stacklevel=stacklevel + 1) - - -def func_takes_retry_state(func): - if not six.callable(func): - raise Exception(func) - return False - if not inspect.isfunction(func) and not inspect.ismethod(func): - # func is a callable object rather than a function/method - func = func.__call__ - func_spec = _utils.getargspec(func) - return 'retry_state' in func_spec.args - - -_unset = object() - - -def _make_unset_exception(func_name, **kwargs): - missing = [] - for k, v in six.iteritems(kwargs): - if v is _unset: - missing.append(k) - missing_str = ', '.join(repr(s) for s in missing) - return TypeError(func_name + ' func missing parameters: ' + missing_str) - - -def _set_delay_since_start(retry_state, delay): - # Ensure outcome_timestamp - start_time is *exactly* equal to the delay to - # avoid complexity in test code. - retry_state.start_time = Fraction(retry_state.start_time) - retry_state.outcome_timestamp = (retry_state.start_time + Fraction(delay)) - assert retry_state.seconds_since_start == delay - - -def make_retry_state(previous_attempt_number, delay_since_first_attempt, - last_result=None): - """Construct RetryCallState for given attempt number & delay. - - Only used in testing and thus is extra careful about timestamp arithmetics. - """ - required_parameter_unset = (previous_attempt_number is _unset or - delay_since_first_attempt is _unset) - if required_parameter_unset: - raise _make_unset_exception( - 'wait/stop', - previous_attempt_number=previous_attempt_number, - delay_since_first_attempt=delay_since_first_attempt) - - from pip._vendor.tenacity import RetryCallState - retry_state = RetryCallState(None, None, (), {}) - retry_state.attempt_number = previous_attempt_number - if last_result is not None: - retry_state.outcome = last_result - else: - retry_state.set_result(None) - _set_delay_since_start(retry_state, delay_since_first_attempt) - return retry_state - - -def func_takes_last_result(waiter): - """Check if function has a "last_result" parameter. - - Needed to provide backward compatibility for wait functions that didn't - take "last_result" in the beginning. - """ - if not six.callable(waiter): - return False - if not inspect.isfunction(waiter) and not inspect.ismethod(waiter): - # waiter is a class, check dunder-call rather than dunder-init. - waiter = waiter.__call__ - waiter_spec = _utils.getargspec(waiter) - return 'last_result' in waiter_spec.args - - -def stop_dunder_call_accept_old_params(fn): - """Decorate cls.__call__ method to accept old "stop" signature.""" - @_utils.wraps(fn) - def new_fn(self, - previous_attempt_number=_unset, - delay_since_first_attempt=_unset, - retry_state=None): - if retry_state is None: - from pip._vendor.tenacity import RetryCallState - retry_state_passed_as_non_kwarg = ( - previous_attempt_number is not _unset and - isinstance(previous_attempt_number, RetryCallState)) - if retry_state_passed_as_non_kwarg: - retry_state = previous_attempt_number - else: - warn_about_dunder_non_retry_state_deprecation(fn, stacklevel=2) - retry_state = make_retry_state( - previous_attempt_number=previous_attempt_number, - delay_since_first_attempt=delay_since_first_attempt) - return fn(self, retry_state=retry_state) - return new_fn - - -def stop_func_accept_retry_state(stop_func): - """Wrap "stop" function to accept "retry_state" parameter.""" - if not six.callable(stop_func): - return stop_func - - if func_takes_retry_state(stop_func): - return stop_func - - @_utils.wraps(stop_func) - def wrapped_stop_func(retry_state): - warn_about_non_retry_state_deprecation( - 'stop', stop_func, stacklevel=4) - return stop_func( - retry_state.attempt_number, - retry_state.seconds_since_start, - ) - return wrapped_stop_func - - -def wait_dunder_call_accept_old_params(fn): - """Decorate cls.__call__ method to accept old "wait" signature.""" - @_utils.wraps(fn) - def new_fn(self, - previous_attempt_number=_unset, - delay_since_first_attempt=_unset, - last_result=None, - retry_state=None): - if retry_state is None: - from pip._vendor.tenacity import RetryCallState - retry_state_passed_as_non_kwarg = ( - previous_attempt_number is not _unset and - isinstance(previous_attempt_number, RetryCallState)) - if retry_state_passed_as_non_kwarg: - retry_state = previous_attempt_number - else: - warn_about_dunder_non_retry_state_deprecation(fn, stacklevel=2) - retry_state = make_retry_state( - previous_attempt_number=previous_attempt_number, - delay_since_first_attempt=delay_since_first_attempt, - last_result=last_result) - return fn(self, retry_state=retry_state) - return new_fn - - -def wait_func_accept_retry_state(wait_func): - """Wrap wait function to accept "retry_state" parameter.""" - if not six.callable(wait_func): - return wait_func - - if func_takes_retry_state(wait_func): - return wait_func - - if func_takes_last_result(wait_func): - @_utils.wraps(wait_func) - def wrapped_wait_func(retry_state): - warn_about_non_retry_state_deprecation( - 'wait', wait_func, stacklevel=4) - return wait_func( - retry_state.attempt_number, - retry_state.seconds_since_start, - last_result=retry_state.outcome, - ) - else: - @_utils.wraps(wait_func) - def wrapped_wait_func(retry_state): - warn_about_non_retry_state_deprecation( - 'wait', wait_func, stacklevel=4) - return wait_func( - retry_state.attempt_number, - retry_state.seconds_since_start, - ) - return wrapped_wait_func - - -def retry_dunder_call_accept_old_params(fn): - """Decorate cls.__call__ method to accept old "retry" signature.""" - @_utils.wraps(fn) - def new_fn(self, attempt=_unset, retry_state=None): - if retry_state is None: - from pip._vendor.tenacity import RetryCallState - if attempt is _unset: - raise _make_unset_exception('retry', attempt=attempt) - retry_state_passed_as_non_kwarg = ( - attempt is not _unset and - isinstance(attempt, RetryCallState)) - if retry_state_passed_as_non_kwarg: - retry_state = attempt - else: - warn_about_dunder_non_retry_state_deprecation(fn, stacklevel=2) - retry_state = RetryCallState(None, None, (), {}) - retry_state.outcome = attempt - return fn(self, retry_state=retry_state) - return new_fn - - -def retry_func_accept_retry_state(retry_func): - """Wrap "retry" function to accept "retry_state" parameter.""" - if not six.callable(retry_func): - return retry_func - - if func_takes_retry_state(retry_func): - return retry_func - - @_utils.wraps(retry_func) - def wrapped_retry_func(retry_state): - warn_about_non_retry_state_deprecation( - 'retry', retry_func, stacklevel=4) - return retry_func(retry_state.outcome) - return wrapped_retry_func - - -def before_func_accept_retry_state(fn): - """Wrap "before" function to accept "retry_state".""" - if not six.callable(fn): - return fn - - if func_takes_retry_state(fn): - return fn - - @_utils.wraps(fn) - def wrapped_before_func(retry_state): - # func, trial_number, trial_time_taken - warn_about_non_retry_state_deprecation('before', fn, stacklevel=4) - return fn( - retry_state.fn, - retry_state.attempt_number, - ) - return wrapped_before_func - - -def after_func_accept_retry_state(fn): - """Wrap "after" function to accept "retry_state".""" - if not six.callable(fn): - return fn - - if func_takes_retry_state(fn): - return fn - - @_utils.wraps(fn) - def wrapped_after_sleep_func(retry_state): - # func, trial_number, trial_time_taken - warn_about_non_retry_state_deprecation('after', fn, stacklevel=4) - return fn( - retry_state.fn, - retry_state.attempt_number, - retry_state.seconds_since_start) - return wrapped_after_sleep_func - - -def before_sleep_func_accept_retry_state(fn): - """Wrap "before_sleep" function to accept "retry_state".""" - if not six.callable(fn): - return fn - - if func_takes_retry_state(fn): - return fn - - @_utils.wraps(fn) - def wrapped_before_sleep_func(retry_state): - # retry_object, sleep, last_result - warn_about_non_retry_state_deprecation( - 'before_sleep', fn, stacklevel=4) - return fn( - retry_state.retry_object, - sleep=getattr(retry_state.next_action, 'sleep'), - last_result=retry_state.outcome) - return wrapped_before_sleep_func - - -def retry_error_callback_accept_retry_state(fn): - if not six.callable(fn): - return fn - - if func_takes_retry_state(fn): - return fn - - @_utils.wraps(fn) - def wrapped_retry_error_callback(retry_state): - warn_about_non_retry_state_deprecation( - 'retry_error_callback', fn, stacklevel=4) - return fn(retry_state.outcome) - return wrapped_retry_error_callback - def get_exc_info_from_future(future): """ diff --git a/src/pip/_vendor/tenacity/retry.py b/src/pip/_vendor/tenacity/retry.py index 7340019ae05..ddaf8e7fd73 100644 --- a/src/pip/_vendor/tenacity/retry.py +++ b/src/pip/_vendor/tenacity/retry.py @@ -1,4 +1,6 @@ -# Copyright 2016 Julien Danjou +# -*- encoding: utf-8 -*- +# +# Copyright 2016–2021 Julien Danjou # Copyright 2016 Joshua Harlow # Copyright 2013-2014 Ray Holder # @@ -19,8 +21,6 @@ from pip._vendor import six -from pip._vendor.tenacity import compat as _compat - @six.add_metaclass(abc.ABCMeta) class retry_base(object): @@ -63,7 +63,6 @@ class retry_if_exception(retry_base): def __init__(self, predicate): self.predicate = predicate - @_compat.retry_dunder_call_accept_old_params def __call__(self, retry_state): if retry_state.outcome.failed: return self.predicate(retry_state.outcome.exception()) @@ -77,7 +76,8 @@ class retry_if_exception_type(retry_if_exception): def __init__(self, exception_types=Exception): self.exception_types = exception_types super(retry_if_exception_type, self).__init__( - lambda e: isinstance(e, exception_types)) + lambda e: isinstance(e, exception_types) + ) class retry_unless_exception_type(retry_if_exception): @@ -86,9 +86,9 @@ class retry_unless_exception_type(retry_if_exception): def __init__(self, exception_types=Exception): self.exception_types = exception_types super(retry_unless_exception_type, self).__init__( - lambda e: not isinstance(e, exception_types)) + lambda e: not isinstance(e, exception_types) + ) - @_compat.retry_dunder_call_accept_old_params def __call__(self, retry_state): # always retry if no exception was raised if not retry_state.outcome.failed: @@ -102,7 +102,6 @@ class retry_if_result(retry_base): def __init__(self, predicate): self.predicate = predicate - @_compat.retry_dunder_call_accept_old_params def __call__(self, retry_state): if not retry_state.outcome.failed: return self.predicate(retry_state.outcome.result()) @@ -116,7 +115,6 @@ class retry_if_not_result(retry_base): def __init__(self, predicate): self.predicate = predicate - @_compat.retry_dunder_call_accept_old_params def __call__(self, retry_state): if not retry_state.outcome.failed: return not self.predicate(retry_state.outcome.result()) @@ -131,23 +129,30 @@ def __init__(self, message=None, match=None): if message and match: raise TypeError( "{}() takes either 'message' or 'match', not both".format( - self.__class__.__name__)) + self.__class__.__name__ + ) + ) # set predicate if message: + def message_fnc(exception): return message == str(exception) + predicate = message_fnc elif match: prog = re.compile(match) def match_fnc(exception): return prog.match(str(exception)) + predicate = match_fnc else: raise TypeError( - "{}() missing 1 required argument 'message' or 'match'". - format(self.__class__.__name__)) + "{}() missing 1 required argument 'message' or 'match'".format( + self.__class__.__name__ + ) + ) super(retry_if_exception_message, self).__init__(predicate) @@ -159,10 +164,8 @@ def __init__(self, *args, **kwargs): super(retry_if_not_exception_message, self).__init__(*args, **kwargs) # invert predicate if_predicate = self.predicate - self.predicate = lambda *args_, **kwargs_: not if_predicate( - *args_, **kwargs_) + self.predicate = lambda *args_, **kwargs_: not if_predicate(*args_, **kwargs_) - @_compat.retry_dunder_call_accept_old_params def __call__(self, retry_state): if not retry_state.outcome.failed: return True @@ -173,10 +176,8 @@ class retry_any(retry_base): """Retries if any of the retries condition is valid.""" def __init__(self, *retries): - self.retries = tuple(_compat.retry_func_accept_retry_state(r) - for r in retries) + self.retries = retries - @_compat.retry_dunder_call_accept_old_params def __call__(self, retry_state): return any(r(retry_state) for r in self.retries) @@ -185,9 +186,7 @@ class retry_all(retry_base): """Retries if all the retries condition are valid.""" def __init__(self, *retries): - self.retries = tuple(_compat.retry_func_accept_retry_state(r) - for r in retries) + self.retries = retries - @_compat.retry_dunder_call_accept_old_params def __call__(self, retry_state): return all(r(retry_state) for r in self.retries) diff --git a/src/pip/_vendor/tenacity/stop.py b/src/pip/_vendor/tenacity/stop.py index a00c259b426..4db27f14431 100644 --- a/src/pip/_vendor/tenacity/stop.py +++ b/src/pip/_vendor/tenacity/stop.py @@ -1,4 +1,6 @@ -# Copyright 2016 Julien Danjou +# -*- encoding: utf-8 -*- +# +# Copyright 2016–2021 Julien Danjou # Copyright 2016 Joshua Harlow # Copyright 2013-2014 Ray Holder # @@ -17,8 +19,6 @@ from pip._vendor import six -from pip._vendor.tenacity import compat as _compat - @six.add_metaclass(abc.ABCMeta) class stop_base(object): @@ -39,10 +39,8 @@ class stop_any(stop_base): """Stop if any of the stop condition is valid.""" def __init__(self, *stops): - self.stops = tuple(_compat.stop_func_accept_retry_state(stop_func) - for stop_func in stops) + self.stops = stops - @_compat.stop_dunder_call_accept_old_params def __call__(self, retry_state): return any(x(retry_state) for x in self.stops) @@ -51,10 +49,8 @@ class stop_all(stop_base): """Stop if all the stop conditions are valid.""" def __init__(self, *stops): - self.stops = tuple(_compat.stop_func_accept_retry_state(stop_func) - for stop_func in stops) + self.stops = stops - @_compat.stop_dunder_call_accept_old_params def __call__(self, retry_state): return all(x(retry_state) for x in self.stops) @@ -62,7 +58,6 @@ def __call__(self, retry_state): class _stop_never(stop_base): """Never stop.""" - @_compat.stop_dunder_call_accept_old_params def __call__(self, retry_state): return False @@ -76,7 +71,6 @@ class stop_when_event_set(stop_base): def __init__(self, event): self.event = event - @_compat.stop_dunder_call_accept_old_params def __call__(self, retry_state): return self.event.is_set() @@ -87,7 +81,6 @@ class stop_after_attempt(stop_base): def __init__(self, max_attempt_number): self.max_attempt_number = max_attempt_number - @_compat.stop_dunder_call_accept_old_params def __call__(self, retry_state): return retry_state.attempt_number >= self.max_attempt_number @@ -98,6 +91,5 @@ class stop_after_delay(stop_base): def __init__(self, max_delay): self.max_delay = max_delay - @_compat.stop_dunder_call_accept_old_params def __call__(self, retry_state): return retry_state.seconds_since_start >= self.max_delay diff --git a/src/pip/_vendor/tenacity/tornadoweb.py b/src/pip/_vendor/tenacity/tornadoweb.py index c31f7ebb76e..dbf9f762578 100644 --- a/src/pip/_vendor/tenacity/tornadoweb.py +++ b/src/pip/_vendor/tenacity/tornadoweb.py @@ -24,10 +24,7 @@ class TornadoRetrying(BaseRetrying): - - def __init__(self, - sleep=gen.sleep, - **kwargs): + def __init__(self, sleep=gen.sleep, **kwargs): super(TornadoRetrying, self).__init__(**kwargs) self.sleep = sleep @@ -35,14 +32,13 @@ def __init__(self, def __call__(self, fn, *args, **kwargs): self.begin(fn) - retry_state = RetryCallState( - retry_object=self, fn=fn, args=args, kwargs=kwargs) + retry_state = RetryCallState(retry_object=self, fn=fn, args=args, kwargs=kwargs) while True: do = self.iter(retry_state=retry_state) if isinstance(do, DoAttempt): try: result = yield fn(*args, **kwargs) - except BaseException: + except BaseException: # noqa: B902 retry_state.set_exception(sys.exc_info()) else: retry_state.set_result(result) diff --git a/src/pip/_vendor/tenacity/wait.py b/src/pip/_vendor/tenacity/wait.py index 8ce205e58cf..625b0e368ef 100644 --- a/src/pip/_vendor/tenacity/wait.py +++ b/src/pip/_vendor/tenacity/wait.py @@ -1,4 +1,6 @@ -# Copyright 2016 Julien Danjou +# -*- encoding: utf-8 -*- +# +# Copyright 2016–2021 Julien Danjou # Copyright 2016 Joshua Harlow # Copyright 2013-2014 Ray Holder # @@ -20,7 +22,6 @@ from pip._vendor import six from pip._vendor.tenacity import _utils -from pip._vendor.tenacity import compat as _compat @six.add_metaclass(abc.ABCMeta) @@ -47,7 +48,6 @@ class wait_fixed(wait_base): def __init__(self, wait): self.wait_fixed = wait - @_compat.wait_dunder_call_accept_old_params def __call__(self, retry_state): return self.wait_fixed @@ -66,21 +66,18 @@ def __init__(self, min=0, max=1): # noqa self.wait_random_min = min self.wait_random_max = max - @_compat.wait_dunder_call_accept_old_params def __call__(self, retry_state): - return (self.wait_random_min + - (random.random() * - (self.wait_random_max - self.wait_random_min))) + return self.wait_random_min + ( + random.random() * (self.wait_random_max - self.wait_random_min) + ) class wait_combine(wait_base): """Combine several waiting strategies.""" def __init__(self, *strategies): - self.wait_funcs = tuple(_compat.wait_func_accept_retry_state(strategy) - for strategy in strategies) + self.wait_funcs = strategies - @_compat.wait_dunder_call_accept_old_params def __call__(self, retry_state): return sum(x(retry_state=retry_state) for x in self.wait_funcs) @@ -102,13 +99,10 @@ def wait_chained(): """ def __init__(self, *strategies): - self.strategies = [_compat.wait_func_accept_retry_state(strategy) - for strategy in strategies] + self.strategies = strategies - @_compat.wait_dunder_call_accept_old_params def __call__(self, retry_state): - wait_func_no = min(max(retry_state.attempt_number, 1), - len(self.strategies)) + wait_func_no = min(max(retry_state.attempt_number, 1), len(self.strategies)) wait_func = self.strategies[wait_func_no - 1] return wait_func(retry_state=retry_state) @@ -125,11 +119,8 @@ def __init__(self, start=0, increment=100, max=_utils.MAX_WAIT): # noqa self.increment = increment self.max = max - @_compat.wait_dunder_call_accept_old_params def __call__(self, retry_state): - result = self.start + ( - self.increment * (retry_state.attempt_number - 1) - ) + result = self.start + (self.increment * (retry_state.attempt_number - 1)) return max(0, min(result, self.max)) @@ -152,7 +143,6 @@ def __init__(self, multiplier=1, max=_utils.MAX_WAIT, exp_base=2, min=0): # noq self.max = max self.exp_base = exp_base - @_compat.wait_dunder_call_accept_old_params def __call__(self, retry_state): try: exp = self.exp_base ** (retry_state.attempt_number - 1) @@ -188,8 +178,6 @@ class wait_random_exponential(wait_exponential): """ - @_compat.wait_dunder_call_accept_old_params def __call__(self, retry_state): - high = super(wait_random_exponential, self).__call__( - retry_state=retry_state) + high = super(wait_random_exponential, self).__call__(retry_state=retry_state) return random.uniform(0, high) diff --git a/src/pip/_vendor/vendor.txt b/src/pip/_vendor/vendor.txt index aa08874dc58..6c9732e97d8 100644 --- a/src/pip/_vendor/vendor.txt +++ b/src/pip/_vendor/vendor.txt @@ -17,6 +17,6 @@ requests==2.25.1 resolvelib==0.7.0 setuptools==44.0.0 six==1.15.0 -tenacity==6.3.1 +tenacity==7.0.0 toml==0.10.2 webencodings==0.5.1 From 3df69bcb29c0377f8ca9dfe960d724cecea16ddc Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sat, 17 Apr 2021 15:33:58 +0100 Subject: [PATCH 3155/3170] Run all of CI, on scheduled runs --- .github/workflows/ci.yml | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 753f636c7ec..e552f8331e4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,6 +33,7 @@ jobs: - "tools/requirements/tests.txt" - "src/**" - "tests/**" + if: github.event_name != "schedule" pre-commit: name: pre-commit @@ -66,7 +67,9 @@ jobs: runs-on: ubuntu-latest needs: [determine-changes] - if: ${{ needs.determine-changes.outputs.vendoring == 'true' }} + if: >- + needs.determine-changes.outputs.vendoring == 'true' || + github.event_name == "schedule" steps: - uses: actions/checkout@v2 @@ -81,7 +84,9 @@ jobs: runs-on: ${{ matrix.os }}-latest needs: [pre-commit, packaging, determine-changes] - if: ${{ needs.determine-changes.outputs.tests == 'true' }} + if: >- + needs.determine-changes.outputs.tests == 'true' || + github.event_name == "schedule" strategy: fail-fast: true @@ -119,7 +124,9 @@ jobs: runs-on: ${{ matrix.os }}-latest needs: [pre-commit, packaging, determine-changes] - if: ${{ needs.determine-changes.outputs.tests == 'true' }} + if: >- + needs.determine-changes.outputs.tests == 'true' || + github.event_name == "schedule" strategy: fail-fast: true From cf2c2ccf6fd68140c7b17296b1aeb3a43848fbb2 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sun, 18 Apr 2021 07:44:38 +0800 Subject: [PATCH 3156/3170] Fix changelog misformat (#9817) --- tools/news/template.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/tools/news/template.rst b/tools/news/template.rst index 70d6ab75def..cff0db4f6fb 100644 --- a/tools/news/template.rst +++ b/tools/news/template.rst @@ -1,6 +1,7 @@ -{{ top_line -}} - -{%- for section in sections %} +{% set underline = "=" %} +{{ top_line }} +{{ underline * top_line|length }} +{% for section in sections %} {% set underline = "-" %} {% if section %} {{ section }} From a31f8fd5eda0e99d8b21ec8b71e153ef604c4d8c Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sun, 4 Apr 2021 21:39:39 +0800 Subject: [PATCH 3157/3170] Add failed test case --- tests/functional/test_install_direct_url.py | 28 ++++----------- tests/functional/test_new_resolver.py | 39 +++++++++++++++++++++ tests/lib/direct_url.py | 15 ++++++++ 3 files changed, 61 insertions(+), 21 deletions(-) create mode 100644 tests/lib/direct_url.py diff --git a/tests/functional/test_install_direct_url.py b/tests/functional/test_install_direct_url.py index e28a7e9b57e..baa5a3f2c28 100644 --- a/tests/functional/test_install_direct_url.py +++ b/tests/functional/test_install_direct_url.py @@ -1,26 +1,12 @@ -import re - import pytest -from pip._internal.models.direct_url import DIRECT_URL_METADATA_NAME, DirectUrl from tests.lib import _create_test_package, path_to_url - - -def _get_created_direct_url(result, pkg): - direct_url_metadata_re = re.compile( - pkg + r"-[\d\.]+\.dist-info." + DIRECT_URL_METADATA_NAME + r"$" - ) - for filename in result.files_created: - if direct_url_metadata_re.search(filename): - direct_url_path = result.test_env.base_path / filename - with open(direct_url_path) as f: - return DirectUrl.from_json(f.read()) - return None +from tests.lib.direct_url import get_created_direct_url def test_install_find_links_no_direct_url(script, with_wheel): result = script.pip_install_local("simple") - assert not _get_created_direct_url(result, "simple") + assert not get_created_direct_url(result, "simple") def test_install_vcs_editable_no_direct_url(script, with_wheel): @@ -29,7 +15,7 @@ def test_install_vcs_editable_no_direct_url(script, with_wheel): result = script.pip(*args) # legacy editable installs do not generate .dist-info, # hence no direct_url.json - assert not _get_created_direct_url(result, "testpkg") + assert not get_created_direct_url(result, "testpkg") def test_install_vcs_non_editable_direct_url(script, with_wheel): @@ -37,7 +23,7 @@ def test_install_vcs_non_editable_direct_url(script, with_wheel): url = path_to_url(pkg_path) args = ["install", f"git+{url}#egg=testpkg"] result = script.pip(*args) - direct_url = _get_created_direct_url(result, "testpkg") + direct_url = get_created_direct_url(result, "testpkg") assert direct_url assert direct_url.url == url assert direct_url.info.vcs == "git" @@ -47,7 +33,7 @@ def test_install_archive_direct_url(script, data, with_wheel): req = "simple @ " + path_to_url(data.packages / "simple-2.0.tar.gz") assert req.startswith("simple @ file://") result = script.pip("install", req) - assert _get_created_direct_url(result, "simple") + assert get_created_direct_url(result, "simple") @pytest.mark.network @@ -59,7 +45,7 @@ def test_install_vcs_constraint_direct_url(script, with_wheel): "#egg=pip-test-package" ) result = script.pip("install", "pip-test-package", "-c", constraints_file) - assert _get_created_direct_url(result, "pip_test_package") + assert get_created_direct_url(result, "pip_test_package") def test_install_vcs_constraint_direct_file_url(script, with_wheel): @@ -68,4 +54,4 @@ def test_install_vcs_constraint_direct_file_url(script, with_wheel): constraints_file = script.scratch_path / "constraints.txt" constraints_file.write_text(f"git+{url}#egg=testpkg") result = script.pip("install", "testpkg", "-c", constraints_file) - assert _get_created_direct_url(result, "testpkg") + assert get_created_direct_url(result, "testpkg") diff --git a/tests/functional/test_new_resolver.py b/tests/functional/test_new_resolver.py index b2d3625a43e..0938768a2ce 100644 --- a/tests/functional/test_new_resolver.py +++ b/tests/functional/test_new_resolver.py @@ -12,6 +12,7 @@ create_test_package_with_setup, path_to_url, ) +from tests.lib.direct_url import get_created_direct_url from tests.lib.path import Path from tests.lib.wheel import make_wheel @@ -1788,3 +1789,41 @@ def test_new_resolver_avoids_incompatible_wheel_tags_in_constraint_url( assert_installed(script, base="0.1.0") assert_not_installed(script, "dep") + + +def test_new_resolver_direct_url_with_extras(tmp_path, script): + pkg1 = create_basic_wheel_for_package(script, name="pkg1", version="1") + pkg2 = create_basic_wheel_for_package( + script, + name="pkg2", + version="1", + extras={"ext": ["pkg1"]}, + ) + pkg3 = create_basic_wheel_for_package( + script, + name="pkg3", + version="1", + depends=["pkg2[ext]"], + ) + + # Make pkg1 and pkg3 visible via --find-links, but not pkg2. + find_links = tmp_path.joinpath("find_links") + find_links.mkdir() + with open(pkg1, "rb") as f: + find_links.joinpath(pkg1.name).write_bytes(f.read()) + with open(pkg3, "rb") as f: + find_links.joinpath(pkg3.name).write_bytes(f.read()) + + # Install with pkg2 only available with direct URL. The extra-ed direct + # URL pkg2 should be able to provide pkg2[ext] required by pkg3. + result = script.pip( + "install", + "--no-cache-dir", "--no-index", + "--find-links", str(find_links), + pkg2, "pkg3", + ) + + assert_installed(script, pkg1="1", pkg2="1", pkg3="1") + assert not get_created_direct_url(result, "pkg1") + assert get_created_direct_url(result, "pkg2") + assert not get_created_direct_url(result, "pkg3") diff --git a/tests/lib/direct_url.py b/tests/lib/direct_url.py new file mode 100644 index 00000000000..497e10c6be1 --- /dev/null +++ b/tests/lib/direct_url.py @@ -0,0 +1,15 @@ +import re + +from pip._internal.models.direct_url import DIRECT_URL_METADATA_NAME, DirectUrl + + +def get_created_direct_url(result, pkg): + direct_url_metadata_re = re.compile( + pkg + r"-[\d\.]+\.dist-info." + DIRECT_URL_METADATA_NAME + r"$" + ) + for filename in result.files_created: + if direct_url_metadata_re.search(filename): + direct_url_path = result.test_env.base_path / filename + with open(direct_url_path) as f: + return DirectUrl.from_json(f.read()) + return None From 0305e0d596e34a4647f0a8b63564f11c9cfa4ee7 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sun, 4 Apr 2021 22:01:59 +0800 Subject: [PATCH 3158/3170] Implement extra-ed requirement merging When a requirement is requested multiple times, some via a direct URL ("req @ URL") and some not but with extras ("req[extra] VERSION"), the resolver previous could not correctly find "req[extra]" if "req" is available in an index. This additional logic makes the resolver, when encountering a requirement with identifier "req[extra]", to also look for explicit candidates listed under "req", and add them as found matches for "req[extra]". --- news/8785.bugfix.rst | 4 +++ .../resolution/resolvelib/candidates.py | 12 +++++++++ .../resolution/resolvelib/factory.py | 25 +++++++++++++++++++ 3 files changed, 41 insertions(+) create mode 100644 news/8785.bugfix.rst diff --git a/news/8785.bugfix.rst b/news/8785.bugfix.rst new file mode 100644 index 00000000000..b84d8d8d58e --- /dev/null +++ b/news/8785.bugfix.rst @@ -0,0 +1,4 @@ +New resolver: When a requirement is requested both via a direct URL +(``req @ URL``) and via version specifier with extras (``req[extra]``), the +resolver will now be able to use the URL to correctly resolve the requirement +with extras. diff --git a/src/pip/_internal/resolution/resolvelib/candidates.py b/src/pip/_internal/resolution/resolvelib/candidates.py index b3c3d019c7b..da516ad3c87 100644 --- a/src/pip/_internal/resolution/resolvelib/candidates.py +++ b/src/pip/_internal/resolution/resolvelib/candidates.py @@ -33,6 +33,18 @@ ] +def as_base_candidate(candidate: Candidate) -> Optional[BaseCandidate]: + """The runtime version of BaseCandidate.""" + base_candidate_classes = ( + AlreadyInstalledCandidate, + EditableCandidate, + LinkCandidate, + ) + if isinstance(candidate, base_candidate_classes): + return candidate + return None + + def make_install_req_from_link(link, template): # type: (Link, InstallRequirement) -> InstallRequirement assert not template.editable, "template is editable" diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 2dcd8389470..ee4494f09ee 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -16,6 +16,8 @@ cast, ) +from pip._vendor.packaging.requirements import InvalidRequirement +from pip._vendor.packaging.requirements import Requirement as PackagingRequirement from pip._vendor.packaging.specifiers import SpecifierSet from pip._vendor.packaging.utils import NormalizedName, canonicalize_name from pip._vendor.pkg_resources import Distribution @@ -54,6 +56,7 @@ ExtrasCandidate, LinkCandidate, RequiresPythonCandidate, + as_base_candidate, ) from .found_candidates import FoundCandidates, IndexCandidateInfo from .requirements import ( @@ -337,6 +340,28 @@ def find_candidates( explicit_candidates.add(candidate) + # If the current identifier contains extras, also add explicit + # candidates from entries from extra-less identifier. + try: + identifier_req = PackagingRequirement(identifier) + except InvalidRequirement: + base_identifier = None + extras: FrozenSet[str] = frozenset() + else: + base_identifier = identifier_req.name + extras = frozenset(identifier_req.extras) + if base_identifier and base_identifier in requirements: + for req in requirements[base_identifier]: + lookup_cand, _ = req.get_candidate_lookup() + if lookup_cand is None: # Not explicit. + continue + # We've stripped extras from the identifier, and should always + # get a BaseCandidate here, unless there's a bug elsewhere. + base_cand = as_base_candidate(lookup_cand) + assert base_cand is not None + candidate = self._make_extras_candidate(base_cand, extras) + explicit_candidates.add(candidate) + # If none of the requirements want an explicit candidate, we can ask # the finder for candidates. if not explicit_candidates: From 9cab9834fa72893b25c2f703c875dd7acab5ea33 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Mon, 19 Apr 2021 11:44:58 +0800 Subject: [PATCH 3159/3170] Refactor Factory.find_candidates() for readability --- .../resolution/resolvelib/factory.py | 154 ++++++++++-------- 1 file changed, 86 insertions(+), 68 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index ee4494f09ee..835b69f41a4 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -1,3 +1,4 @@ +import contextlib import functools import logging from typing import ( @@ -126,6 +127,15 @@ def force_reinstall(self): # type: () -> bool return self._force_reinstall + def _fail_if_link_is_unsupported_wheel(self, link: Link) -> None: + if not link.is_wheel: + return + wheel = Wheel(link.filename) + if wheel.supported(self._finder.target_python.get_tags()): + return + msg = f"{link.filename} is not a supported wheel on this platform." + raise UnsupportedWheel(msg) + def _make_extras_candidate(self, base, extras): # type: (BaseCandidate, FrozenSet[str]) -> ExtrasCandidate cache_key = (id(base), extras) @@ -278,6 +288,51 @@ def iter_index_candidate_infos(): incompatible_ids, ) + def _iter_explicit_candidates_from_base( + self, + base_requirements: Iterable[Requirement], + extras: FrozenSet[str], + ) -> Iterator[Candidate]: + """Produce explicit candidates from the base given an extra-ed package. + + :param base_requirements: Requirements known to the resolver. The + requirements are guaranteed to not have extras. + :param extras: The extras to inject into the explicit requirements' + candidates. + """ + for req in base_requirements: + lookup_cand, _ = req.get_candidate_lookup() + if lookup_cand is None: # Not explicit. + continue + # We've stripped extras from the identifier, and should always + # get a BaseCandidate here, unless there's a bug elsewhere. + base_cand = as_base_candidate(lookup_cand) + assert base_cand is not None, "no extras here" + yield self._make_extras_candidate(base_cand, extras) + + def _iter_candidates_from_constraints( + self, + identifier: str, + constraint: Constraint, + template: InstallRequirement, + ) -> Iterator[Candidate]: + """Produce explicit candidates from constraints. + + This creates "fake" InstallRequirement objects that are basically clones + of what "should" be the template, but with original_link set to link. + """ + for link in constraint.links: + self._fail_if_link_is_unsupported_wheel(link) + candidate = self._make_candidate_from_link( + link, + extras=frozenset(), + template=install_req_from_link_and_ireq(link, template), + name=canonicalize_name(identifier), + version=None, + ) + if candidate: + yield candidate + def find_candidates( self, identifier: str, @@ -291,76 +346,44 @@ def find_candidates( # can be made quicker by comparing only the id() values. incompat_ids = {id(c) for c in incompatibilities.get(identifier, ())} + # Collect basic lookup information from the requirements. explicit_candidates = set() # type: Set[Candidate] ireqs = [] # type: List[InstallRequirement] for req in requirements[identifier]: cand, ireq = req.get_candidate_lookup() - if cand is not None and id(cand) not in incompat_ids: + if cand is not None and id(cand): explicit_candidates.add(cand) if ireq is not None: ireqs.append(ireq) - for link in constraint.links: - if not ireqs: - # If we hit this condition, then we cannot construct a candidate. - # However, if we hit this condition, then none of the requirements - # provided an ireq, so they must have provided an explicit candidate. - # In that case, either the candidate matches, in which case this loop - # doesn't need to do anything, or it doesn't, in which case there's - # nothing this loop can do to recover. - break - if link.is_wheel: - wheel = Wheel(link.filename) - # Check whether the provided wheel is compatible with the target - # platform. - if not wheel.supported(self._finder.target_python.get_tags()): - # We are constrained to install a wheel that is incompatible with - # the target architecture, so there are no valid candidates. - # Return early, with no candidates. - return () - # Create a "fake" InstallRequirement that's basically a clone of - # what "should" be the template, but with original_link set to link. - # Using the given requirement is necessary for preserving hash - # requirements, but without the original_link, direct_url.json - # won't be created. - ireq = install_req_from_link_and_ireq(link, ireqs[0]) - candidate = self._make_candidate_from_link( - link, - extras=frozenset(), - template=ireq, - name=canonicalize_name(ireq.name) if ireq.name else None, - version=None, + # If the current identifier contains extras, add explicit candidates + # from entries from extra-less identifier. + with contextlib.suppress(InvalidRequirement): + parsed_requirement = PackagingRequirement(identifier) + explicit_candidates.update( + self._iter_explicit_candidates_from_base( + requirements.get(parsed_requirement.name, ()), + frozenset(parsed_requirement.extras), + ), ) - if candidate is None: - # _make_candidate_from_link returns None if the wheel fails to build. - # We are constrained to install this wheel, so there are no valid - # candidates. - # Return early, with no candidates. - return () - explicit_candidates.add(candidate) - - # If the current identifier contains extras, also add explicit - # candidates from entries from extra-less identifier. - try: - identifier_req = PackagingRequirement(identifier) - except InvalidRequirement: - base_identifier = None - extras: FrozenSet[str] = frozenset() - else: - base_identifier = identifier_req.name - extras = frozenset(identifier_req.extras) - if base_identifier and base_identifier in requirements: - for req in requirements[base_identifier]: - lookup_cand, _ = req.get_candidate_lookup() - if lookup_cand is None: # Not explicit. - continue - # We've stripped extras from the identifier, and should always - # get a BaseCandidate here, unless there's a bug elsewhere. - base_cand = as_base_candidate(lookup_cand) - assert base_cand is not None - candidate = self._make_extras_candidate(base_cand, extras) - explicit_candidates.add(candidate) + # Add explicit candidates from constraints. We only do this if there are + # kown ireqs, which represent requirements not already explicit. If + # there are no ireqs, we're constraining already-explicit requirements, + # which is handled later when we return the explicit candidates. + if ireqs: + try: + explicit_candidates.update( + self._iter_candidates_from_constraints( + identifier, + constraint, + template=ireqs[0], + ), + ) + except UnsupportedWheel: + # If we're constrained to install a wheel incompatible with the + # target architecture, no candidates will ever be valid. + return () # If none of the requirements want an explicit candidate, we can ask # the finder for candidates. @@ -376,7 +399,8 @@ def find_candidates( return ( c for c in explicit_candidates - if constraint.is_satisfied_by(c) + if id(c) not in incompat_ids + and constraint.is_satisfied_by(c) and all(req.is_satisfied_by(c) for req in requirements[identifier]) ) @@ -391,13 +415,7 @@ def make_requirement_from_install_req(self, ireq, requested_extras): return None if not ireq.link: return SpecifierRequirement(ireq) - if ireq.link.is_wheel: - wheel = Wheel(ireq.link.filename) - if not wheel.supported(self._finder.target_python.get_tags()): - msg = "{} is not a supported wheel on this platform.".format( - wheel.filename, - ) - raise UnsupportedWheel(msg) + self._fail_if_link_is_unsupported_wheel(ireq.link) cand = self._make_candidate_from_link( ireq.link, extras=frozenset(ireq.extras), From 92862e28ec3f3cd0701f07bc047acab1e8643490 Mon Sep 17 00:00:00 2001 From: Sviatoslav Sydorenko <wk@sydorenko.org.ua> Date: Wed, 14 Apr 2021 20:19:26 +0200 Subject: [PATCH 3160/3170] Enable parallelism in Sphinx --- docs/pip_sphinxext.py | 8 ++++++-- tox.ini | 4 ++-- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/docs/pip_sphinxext.py b/docs/pip_sphinxext.py index 8e4385a33ef..12e098fc0d7 100644 --- a/docs/pip_sphinxext.py +++ b/docs/pip_sphinxext.py @@ -5,7 +5,7 @@ import re import sys from textwrap import dedent -from typing import Iterable, Iterator, List, Optional +from typing import Dict, Iterable, Iterator, List, Optional, Union from docutils import nodes, statemachine from docutils.parsers import rst @@ -293,7 +293,7 @@ def run(self) -> List[nodes.Node]: return [node] -def setup(app: Sphinx) -> None: +def setup(app: Sphinx) -> Dict[str, Union[bool, str]]: app.add_directive("pip-command-usage", PipCommandUsage) app.add_directive("pip-command-description", PipCommandDescription) app.add_directive("pip-command-options", PipCommandOptions) @@ -304,3 +304,7 @@ def setup(app: Sphinx) -> None: ) app.add_directive("pip-news-include", PipNewsInclude) app.add_directive("pip-cli", PipCLIDirective) + return { + "parallel_read_safe": True, + "parallel_write_safe": True, + } diff --git a/tox.ini b/tox.ini index 0b9511bf98b..60f133cea91 100644 --- a/tox.ini +++ b/tox.ini @@ -51,12 +51,12 @@ setenv = deps = -r{toxinidir}/tools/requirements/docs.txt basepython = python3 commands = - sphinx-build -W -d {envtmpdir}/doctrees/html -b html docs/html docs/build/html + sphinx-build -W -j auto -d {envtmpdir}/doctrees/html -b html docs/html docs/build/html # Having the conf.py in the docs/html is weird but needed because we # can not use a different configuration directory vs source directory on RTD # currently -- https://github.com/rtfd/readthedocs.org/issues/1543. # That is why we have a "-c docs/html" in the next line. - sphinx-build -W -d {envtmpdir}/doctrees/man -b man docs/man docs/build/man -c docs/html + sphinx-build -W -j auto -d {envtmpdir}/doctrees/man -b man docs/man docs/build/man -c docs/html [testenv:lint] skip_install = True From 73dca3a43a1e4c9167145beff433afe61d667ee2 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Wed, 21 Apr 2021 10:00:46 +0100 Subject: [PATCH 3161/3170] Change quotes --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e552f8331e4..d5139664c4e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: - "tools/requirements/tests.txt" - "src/**" - "tests/**" - if: github.event_name != "schedule" + if: github.event_name != 'schedule' pre-commit: name: pre-commit @@ -69,7 +69,7 @@ jobs: needs: [determine-changes] if: >- needs.determine-changes.outputs.vendoring == 'true' || - github.event_name == "schedule" + github.event_name == 'schedule' steps: - uses: actions/checkout@v2 @@ -86,7 +86,7 @@ jobs: needs: [pre-commit, packaging, determine-changes] if: >- needs.determine-changes.outputs.tests == 'true' || - github.event_name == "schedule" + github.event_name == 'schedule' strategy: fail-fast: true @@ -126,7 +126,7 @@ jobs: needs: [pre-commit, packaging, determine-changes] if: >- needs.determine-changes.outputs.tests == 'true' || - github.event_name == "schedule" + github.event_name == 'schedule' strategy: fail-fast: true From cf4e3aa055b521a26d258a83dc1f0a0a38b54245 Mon Sep 17 00:00:00 2001 From: Tzu-ping Chung <uranusjr@gmail.com> Date: Sat, 24 Apr 2021 02:09:24 +0800 Subject: [PATCH 3162/3170] Remove stray id(candidate) call Also moves the incompatibility candidate calculation to closer to their usages. --- src/pip/_internal/resolution/resolvelib/factory.py | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/pip/_internal/resolution/resolvelib/factory.py b/src/pip/_internal/resolution/resolvelib/factory.py index 835b69f41a4..6e3f195187b 100644 --- a/src/pip/_internal/resolution/resolvelib/factory.py +++ b/src/pip/_internal/resolution/resolvelib/factory.py @@ -341,17 +341,12 @@ def find_candidates( constraint: Constraint, prefers_installed: bool, ) -> Iterable[Candidate]: - - # Since we cache all the candidates, incompatibility identification - # can be made quicker by comparing only the id() values. - incompat_ids = {id(c) for c in incompatibilities.get(identifier, ())} - # Collect basic lookup information from the requirements. explicit_candidates = set() # type: Set[Candidate] ireqs = [] # type: List[InstallRequirement] for req in requirements[identifier]: cand, ireq = req.get_candidate_lookup() - if cand is not None and id(cand): + if cand is not None: explicit_candidates.add(cand) if ireq is not None: ireqs.append(ireq) @@ -385,6 +380,10 @@ def find_candidates( # target architecture, no candidates will ever be valid. return () + # Since we cache all the candidates, incompatibility identification + # can be made quicker by comparing only the id() values. + incompat_ids = {id(c) for c in incompatibilities.get(identifier, ())} + # If none of the requirements want an explicit candidate, we can ask # the finder for candidates. if not explicit_candidates: From e9cc23ffd97cb6d66d32dc3ec27cf832524bb33d Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sat, 24 Apr 2021 10:00:54 +0100 Subject: [PATCH 3163/3170] Skip checks on PRs only --- .github/workflows/ci.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index d5139664c4e..a7ce634abb4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,7 +33,7 @@ jobs: - "tools/requirements/tests.txt" - "src/**" - "tests/**" - if: github.event_name != 'schedule' + if: github.event_name == 'pull_request' pre-commit: name: pre-commit @@ -69,7 +69,7 @@ jobs: needs: [determine-changes] if: >- needs.determine-changes.outputs.vendoring == 'true' || - github.event_name == 'schedule' + github.event_name != 'pull_request' steps: - uses: actions/checkout@v2 @@ -86,7 +86,7 @@ jobs: needs: [pre-commit, packaging, determine-changes] if: >- needs.determine-changes.outputs.tests == 'true' || - github.event_name == 'schedule' + github.event_name != 'pull_request' strategy: fail-fast: true @@ -126,7 +126,7 @@ jobs: needs: [pre-commit, packaging, determine-changes] if: >- needs.determine-changes.outputs.tests == 'true' || - github.event_name == 'schedule' + github.event_name != 'pull_request' strategy: fail-fast: true From ca832b2836e0bffa7cf95589acdcd71230f5834e Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sat, 24 Apr 2021 10:13:15 +0100 Subject: [PATCH 3164/3170] Don't split git references on unicode separators Previously, maliciously formatted tags could be used to hijack a commit-based pin. Using the fact that the split here allowed for all of unicode's whitespace characters as separators -- which git allows as a part of a tag name -- it is possible to force a different revision to be installed; if an attacker gains access to the repository. This change stops splitting the string on unicode characters, by forcing the splits to happen on newlines and ASCII spaces. --- src/pip/_internal/vcs/git.py | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/pip/_internal/vcs/git.py b/src/pip/_internal/vcs/git.py index 9f24ccdf5ee..b7c1b9fe7b5 100644 --- a/src/pip/_internal/vcs/git.py +++ b/src/pip/_internal/vcs/git.py @@ -131,9 +131,15 @@ def get_revision_sha(cls, dest, rev): on_returncode='ignore', ) refs = {} - for line in output.strip().splitlines(): + # NOTE: We do not use splitlines here since that would split on other + # unicode separators, which can be maliciously used to install a + # different revision. + for line in output.strip().split("\n"): + line = line.rstrip("\r") + if not line: + continue try: - ref_sha, ref_name = line.split() + ref_sha, ref_name = line.split(" ", maxsplit=2) except ValueError: # Include the offending line to simplify troubleshooting if # this error ever occurs. From 0e4938d269815a5bf1dd8c16e851cb1199fc5317 Mon Sep 17 00:00:00 2001 From: Pradyun Gedam <pradyunsg@users.noreply.github.com> Date: Sat, 24 Apr 2021 10:17:20 +0100 Subject: [PATCH 3165/3170] :newspaper: --- news/9827.bugfix.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 news/9827.bugfix.rst diff --git a/news/9827.bugfix.rst b/news/9827.bugfix.rst new file mode 100644 index 00000000000..e0d27c36cfe --- /dev/null +++ b/news/9827.bugfix.rst @@ -0,0 +1,3 @@ +**SECURITY**: Stop splitting on unicode separators in git references, +which could be maliciously used to install a different revision on the +repository. From 21cd124b5d40b510295c201b9152a65ac3337a37 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@acsone.eu> Date: Sat, 24 Apr 2021 10:12:17 +0200 Subject: [PATCH 3166/3170] Fix NEWS.rst placeholder position --- NEWS.rst | 23 ++++++++++++----------- 1 file changed, 12 insertions(+), 11 deletions(-) diff --git a/NEWS.rst b/NEWS.rst index 8e1e6e70c8a..9031bc48291 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -1,3 +1,15 @@ +.. note + + You should *NOT* be adding new change log entries to this file, this + file is managed by towncrier. You *may* edit previous change logs to + fix problems like typo corrections or such. + + To add a new change log entry, please see + https://pip.pypa.io/en/latest/development/contributing/#news-entries + +.. towncrier release notes start + + 21.0.1 (2021-01-30) =================== @@ -61,17 +73,6 @@ Improved Documentation - Fix broken email link in docs feedback banners. (`#9343 <https://github.com/pypa/pip/issues/9343>`_) -.. note - - You should *NOT* be adding new change log entries to this file, this - file is managed by towncrier. You *may* edit previous change logs to - fix problems like typo corrections or such. - - To add a new change log entry, please see - https://pip.pypa.io/en/latest/development/contributing/#news-entries - -.. towncrier release notes start - 20.3.4 (2021-01-23) =================== From 3cccfbf169bd35133ee25d2543659b9c1e262f8c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@acsone.eu> Date: Sat, 24 Apr 2021 10:25:08 +0200 Subject: [PATCH 3167/3170] Rename mislabeled news fragment --- ...ugfix.rst => bca635a1-abe3-4532-8add-bf7491b0eea5.trivial.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename news/{bca635a1-abe3-4532-8add-bf7491b0eea5.bugfix.rst => bca635a1-abe3-4532-8add-bf7491b0eea5.trivial.rst} (100%) diff --git a/news/bca635a1-abe3-4532-8add-bf7491b0eea5.bugfix.rst b/news/bca635a1-abe3-4532-8add-bf7491b0eea5.trivial.rst similarity index 100% rename from news/bca635a1-abe3-4532-8add-bf7491b0eea5.bugfix.rst rename to news/bca635a1-abe3-4532-8add-bf7491b0eea5.trivial.rst From 2edd3fdf2af2f09dce5085ef0eb54684b4f9bc04 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@acsone.eu> Date: Sat, 24 Apr 2021 12:26:22 +0200 Subject: [PATCH 3168/3170] Postpone a deprecation to 21.2 --- src/pip/_internal/resolution/resolvelib/resolver.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/pip/_internal/resolution/resolvelib/resolver.py b/src/pip/_internal/resolution/resolvelib/resolver.py index 0eab785d85a..b90f82cfa26 100644 --- a/src/pip/_internal/resolution/resolvelib/resolver.py +++ b/src/pip/_internal/resolution/resolvelib/resolver.py @@ -184,7 +184,7 @@ def resolve(self, root_reqs, check_supported_wheels): deprecated( reason=reason, replacement=replacement, - gone_in="21.1", + gone_in="21.2", issue=8711, ) From ea761a6575f37b90cf89035ee8be3808cf872184 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@acsone.eu> Date: Sat, 24 Apr 2021 11:46:16 +0200 Subject: [PATCH 3169/3170] Update AUTHORS.txt --- AUTHORS.txt | 23 ++++++++++++++++++++++- 1 file changed, 22 insertions(+), 1 deletion(-) diff --git a/AUTHORS.txt b/AUTHORS.txt index 764605f5359..23217834795 100644 --- a/AUTHORS.txt +++ b/AUTHORS.txt @@ -37,6 +37,7 @@ Andre Aguiar Andreas Lutro Andrei Geacar Andrew Gaul +Andrey Bienkowski Andrey Bulgakov Andrés Delfino Andy Freeland @@ -71,6 +72,7 @@ Barney Gale barneygale Bartek Ogryczak Bastian Venthur +Ben Bodenmiller Ben Darnell Ben Hoyt Ben Rosser @@ -85,6 +87,7 @@ Bernardo B. Marques Bernhard M. Wiedemann Bertil Hatt Bhavam Vidyarthi +Blazej Michalik Bogdan Opanchuk BorisZZZ Brad Erickson @@ -139,6 +142,7 @@ Cristina Cristina Muñoz Curtis Doty cytolentino +Daan De Meyer Damian Quiroga Dan Black Dan Savilonis @@ -154,6 +158,7 @@ Daniele Esposti Daniele Procida Danny Hermes Danny McClanahan +Darren Kavanagh Dav Clark Dave Abrahams Dave Jones @@ -161,7 +166,9 @@ David Aguilar David Black David Bordeynik David Caro +David D Lowe David Evans +David Hewitt David Linke David Poggi David Pursehouse @@ -169,6 +176,7 @@ David Tucker David Wales Davidovich Deepak Sharma +Denise Yu derwolfe Desetude Devesh Kumar Singh @@ -176,6 +184,7 @@ Diego Caraballo DiegoCaraballo Dmitry Gladkov Domen Kožar +Dominic Davis-Foster Donald Stufft Dongweiming Douglas Thor @@ -195,6 +204,7 @@ Emmanuel Arias Endoh Takanao enoch Erdinc Mutlu +Eric Cousineau Eric Gillingham Eric Hanchrow Eric Hopper @@ -254,7 +264,7 @@ Igor Kuzmitshov Igor Sobreira Ilan Schnell Ilya Baryshev -INADA Naoki +Inada Naoki Ionel Cristian Mărieș Ionel Maries Cristian Ivan Pozdeev @@ -279,6 +289,7 @@ jenix21 Jeremy Stanley Jeremy Zafran Jiashuo Li +Jim Fisher Jim Garrison Jivan Amara John Paton @@ -292,6 +303,7 @@ Jonas Nockert Jonathan Herbert Joost Molenaar Jorge Niedbalski +Joseph Bylund Joseph Long Josh Bronson Josh Hansen @@ -317,6 +329,7 @@ Kevin Frommelt Kevin R Patterson Kexuan Sun Kit Randel +Klaas van Schelven KOLANICH kpinc Krishna Oza @@ -325,6 +338,7 @@ Kyle Persohn lakshmanaram Laszlo Kiss-Kollar Laurent Bristiel +Laurent LAPORTE Laurie O Laurie Opperman Leon Sasson @@ -346,6 +360,8 @@ Mariatta Mark Kohler Mark Williams Markus Hametner +Martin Häcker +Martin Pavlasek Masaki Masklinn Matej Stuchlik @@ -362,6 +378,7 @@ Matthew Trumbell Matthew Willson Matthias Bussonnier mattip +Max W Chase Maxim Kurnikov Maxime Rouyrre mayeut @@ -458,6 +475,7 @@ Preston Holmes Przemek Wrzos Pulkit Goyal Qiangning Hong +Quentin Lee Quentin Pradet R. David Murray Rafael Caricio @@ -577,7 +595,9 @@ William ML Leslie William T Olson Wilson Mo wim glenn +Winson Luk Wolfgang Maier +XAMES3 Xavier Fernandez xoviat xtreak @@ -592,3 +612,4 @@ Zhiping Deng Zvezdan Petkovic Łukasz Langa Семён Марьясин +‮rekcäH nitraM‮ From 2b2a268d25963727c2a1c805de8f0246b9cd63f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= <stephane.bidoul@acsone.eu> Date: Sat, 24 Apr 2021 11:46:17 +0200 Subject: [PATCH 3170/3170] Bump for release --- NEWS.rst | 76 +++++++++++++++++++ ...27-049c-4d5d-b44d-daea0c2fd01a.trivial.rst | 0 ...eb-6433-4f15-b70d-c2c514f72ebd.trivial.rst | 0 ...46-d005-46ca-b1ae-a3811357dba3.trivial.rst | 0 ...c8-c115-4055-9a60-30a8f8eef7ba.trivial.rst | 0 ...63-d2a1-41f5-a2df-d11f8460551a.trivial.rst | 0 ...60-12d9-4e58-8322-21e5975a804e.trivial.rst | 0 news/4390.bugfix.rst | 1 - ...22829F-6A45-4202-87BA-A80482DF6D4E.doc.rst | 1 - ...91-D0A3-4599-B3B4-129240A99B99.trivial.rst | 0 ...56-e1d6-4f9a-bf46-8938d1936d9e.trivial.rst | 0 news/6409.bugfix.rst | 1 - news/6720.doc.rst | 1 - news/7269.bugfix.rst | 2 - ...fb-6f07-4ec1-956b-d77c9f339773.trivial.rst | 0 news/8090.bugfix.rst | 3 - news/8214.bugfix.rst | 2 - news/8253.feature.rst | 1 - news/8418.bugfix.rst | 1 - news/8418.doc.rst | 1 - ...ed-4341-4d28-ab9e-e5ab43fb039f.trivial.rst | 0 ...33-9c0b-4bec-a1e8-afd31786eaeb.trivial.rst | 0 news/8733.bugfix.rst | 1 - news/8785.bugfix.rst | 4 - news/8896.trivial.rst | 1 - news/9091.feature.rst | 4 - news/9139.feature.rst | 1 - ...ff-72ea-4db5-846a-30273dac1c0c.trivial.rst | 0 news/9300.bugfix.rst | 2 - news/9348.bugfix.rst | 2 - news/9409.bugfix.rst | 2 - news/9541.bugfix.rst | 1 - news/9547.feature.rst | 1 - news/9565.bugfix.rst | 1 - news/9617.process.rst | 3 - news/9647.doc.rst | 1 - news/9692.doc.rst | 2 - news/9748.feature.rst | 1 - news/9774.feature.rst | 3 - news/9779.bugfix.rst | 1 - news/9827.bugfix.rst | 3 - ...28-bf43-4225-b96f-e066910f309c.trivial.rst | 0 ...73-6079-491e-bbe0-d1593952f1c7.trivial.rst | 0 news/CVE-2021-28363.vendor.rst | 1 - ...8d-1172-4012-a0a5-0fc42264a70d.trivial.rst | 0 ...7a-af01-49c9-9b72-2d3d4a89b11a.trivial.rst | 0 ...41-404a-4f2b-afee-c931a5aa7d54.trivial.rst | 0 ...a1-abe3-4532-8add-bf7491b0eea5.trivial.rst | 0 news/contextlib2.vendor.rst | 1 - ...19-2486-45dc-b8dc-d2a5b9197ca4.trivial.rst | 0 ...8d-331a-4700-bfc1-485814e6c90e.trivial.rst | 0 ...d4-21e2-460f-9d80-455ff318c713.trivial.rst | 0 ...47-5750-4a13-b36f-d4a4622861cf.trivial.rst | 0 ...57-9ed5-4d22-a877-50f2d5adcae0.trivial.rst | 0 ...51-9a1a-453e-af98-bbb35f7c3e66.trivial.rst | 0 ...1c-018c-4fde-ac8d-f674c6d9d190.trivial.rst | 0 news/idna.vendor.rst | 1 - news/pep517.vendor.rst | 1 - news/resolvelib.vendor.rst | 1 - news/tenacity.vendor.rst | 1 - src/pip/__init__.py | 2 +- 61 files changed, 77 insertions(+), 55 deletions(-) delete mode 100644 news/0a741827-049c-4d5d-b44d-daea0c2fd01a.trivial.rst delete mode 100644 news/11e1b2eb-6433-4f15-b70d-c2c514f72ebd.trivial.rst delete mode 100644 news/151a1e46-d005-46ca-b1ae-a3811357dba3.trivial.rst delete mode 100644 news/1ab8f1c8-c115-4055-9a60-30a8f8eef7ba.trivial.rst delete mode 100644 news/287c6463-d2a1-41f5-a2df-d11f8460551a.trivial.rst delete mode 100644 news/40711960-12d9-4e58-8322-21e5975a804e.trivial.rst delete mode 100644 news/4390.bugfix.rst delete mode 100644 news/4822829F-6A45-4202-87BA-A80482DF6D4E.doc.rst delete mode 100644 news/5B0A5B91-D0A3-4599-B3B4-129240A99B99.trivial.rst delete mode 100644 news/5be04056-e1d6-4f9a-bf46-8938d1936d9e.trivial.rst delete mode 100644 news/6409.bugfix.rst delete mode 100644 news/6720.doc.rst delete mode 100644 news/7269.bugfix.rst delete mode 100644 news/76c758fb-6f07-4ec1-956b-d77c9f339773.trivial.rst delete mode 100644 news/8090.bugfix.rst delete mode 100644 news/8214.bugfix.rst delete mode 100644 news/8253.feature.rst delete mode 100644 news/8418.bugfix.rst delete mode 100644 news/8418.doc.rst delete mode 100644 news/855bfaed-4341-4d28-ab9e-e5ab43fb039f.trivial.rst delete mode 100644 news/8597c433-9c0b-4bec-a1e8-afd31786eaeb.trivial.rst delete mode 100644 news/8733.bugfix.rst delete mode 100644 news/8785.bugfix.rst delete mode 100644 news/8896.trivial.rst delete mode 100644 news/9091.feature.rst delete mode 100644 news/9139.feature.rst delete mode 100644 news/917ab6ff-72ea-4db5-846a-30273dac1c0c.trivial.rst delete mode 100644 news/9300.bugfix.rst delete mode 100644 news/9348.bugfix.rst delete mode 100644 news/9409.bugfix.rst delete mode 100644 news/9541.bugfix.rst delete mode 100644 news/9547.feature.rst delete mode 100644 news/9565.bugfix.rst delete mode 100644 news/9617.process.rst delete mode 100644 news/9647.doc.rst delete mode 100644 news/9692.doc.rst delete mode 100644 news/9748.feature.rst delete mode 100644 news/9774.feature.rst delete mode 100644 news/9779.bugfix.rst delete mode 100644 news/9827.bugfix.rst delete mode 100644 news/9976b528-bf43-4225-b96f-e066910f309c.trivial.rst delete mode 100644 news/9e768673-6079-491e-bbe0-d1593952f1c7.trivial.rst delete mode 100644 news/CVE-2021-28363.vendor.rst delete mode 100644 news/a06e528d-1172-4012-a0a5-0fc42264a70d.trivial.rst delete mode 100644 news/adba2b7a-af01-49c9-9b72-2d3d4a89b11a.trivial.rst delete mode 100644 news/afd07841-404a-4f2b-afee-c931a5aa7d54.trivial.rst delete mode 100644 news/bca635a1-abe3-4532-8add-bf7491b0eea5.trivial.rst delete mode 100644 news/contextlib2.vendor.rst delete mode 100644 news/d0935419-2486-45dc-b8dc-d2a5b9197ca4.trivial.rst delete mode 100644 news/d809028d-331a-4700-bfc1-485814e6c90e.trivial.rst delete mode 100644 news/dfaa54d4-21e2-460f-9d80-455ff318c713.trivial.rst delete mode 100644 news/f24d8f47-5750-4a13-b36f-d4a4622861cf.trivial.rst delete mode 100644 news/f8f0d057-9ed5-4d22-a877-50f2d5adcae0.trivial.rst delete mode 100644 news/fc6b6951-9a1a-453e-af98-bbb35f7c3e66.trivial.rst delete mode 100644 news/fd62a11c-018c-4fde-ac8d-f674c6d9d190.trivial.rst delete mode 100644 news/idna.vendor.rst delete mode 100644 news/pep517.vendor.rst delete mode 100644 news/resolvelib.vendor.rst delete mode 100644 news/tenacity.vendor.rst diff --git a/NEWS.rst b/NEWS.rst index 9031bc48291..0ad74cf9a0e 100644 --- a/NEWS.rst +++ b/NEWS.rst @@ -9,6 +9,82 @@ .. towncrier release notes start +21.1 (2021-04-24) +================= + +Process +------- + +- Start installation scheme migration from ``distutils`` to ``sysconfig``. A + warning is implemented to detect differences between the two implementations to + encourage user reports, so we can avoid breakages before they happen. + +Features +-------- + +- Add the ability for the new resolver to process URL constraints. (`#8253 <https://github.com/pypa/pip/issues/8253>`_) +- Add a feature ``--use-feature=in-tree-build`` to build local projects in-place + when installing. This is expected to become the default behavior in pip 21.3; + see `Installing from local packages <https://pip.pypa.io/en/stable/user_guide/#installing-from-local-packages>`_ + for more information. (`#9091 <https://github.com/pypa/pip/issues/9091>`_) +- Bring back the "(from versions: ...)" message, that was shown on resolution failures. (`#9139 <https://github.com/pypa/pip/issues/9139>`_) +- Add support for editable installs for project with only setup.cfg files. (`#9547 <https://github.com/pypa/pip/issues/9547>`_) +- Improve performance when picking the best file from indexes during ``pip install``. (`#9748 <https://github.com/pypa/pip/issues/9748>`_) +- Warn instead of erroring out when doing a PEP 517 build in presence of + ``--build-option``. Warn when doing a PEP 517 build in presence of + ``--global-option``. (`#9774 <https://github.com/pypa/pip/issues/9774>`_) + +Bug Fixes +--------- + +- Fixed ``--target`` to work with ``--editable`` installs. (`#4390 <https://github.com/pypa/pip/issues/4390>`_) +- Add a warning, discouraging the usage of pip as root, outside a virtual environment. (`#6409 <https://github.com/pypa/pip/issues/6409>`_) +- Ignore ``.dist-info`` directories if the stem is not a valid Python distribution + name, so they don't show up in e.g. ``pip freeze``. (`#7269 <https://github.com/pypa/pip/issues/7269>`_) +- Only query the keyring for URLs that actually trigger error 401. + This prevents an unnecessary keyring unlock prompt on every pip install + invocation (even with default index URL which is not password protected). (`#8090 <https://github.com/pypa/pip/issues/8090>`_) +- Prevent packages already-installed alongside with pip to be injected into an + isolated build environment during build-time dependency population. (`#8214 <https://github.com/pypa/pip/issues/8214>`_) +- Fix ``pip freeze`` permission denied error in order to display an understandable error message and offer solutions. (`#8418 <https://github.com/pypa/pip/issues/8418>`_) +- Correctly uninstall script files (from setuptools' ``scripts`` argument), when installed with ``--user``. (`#8733 <https://github.com/pypa/pip/issues/8733>`_) +- New resolver: When a requirement is requested both via a direct URL + (``req @ URL``) and via version specifier with extras (``req[extra]``), the + resolver will now be able to use the URL to correctly resolve the requirement + with extras. (`#8785 <https://github.com/pypa/pip/issues/8785>`_) +- New resolver: Show relevant entries from user-supplied constraint files in the + error message to improve debuggability. (`#9300 <https://github.com/pypa/pip/issues/9300>`_) +- Avoid parsing version to make the version check more robust against lousily + debundled downstream distributions. (`#9348 <https://github.com/pypa/pip/issues/9348>`_) +- ``--user`` is no longer suggested incorrectly when pip fails with a permission + error in a virtual environment. (`#9409 <https://github.com/pypa/pip/issues/9409>`_) +- Fix incorrect reporting on ``Requires-Python`` conflicts. (`#9541 <https://github.com/pypa/pip/issues/9541>`_) +- Make wheel compatibility tag preferences more important than the build tag (`#9565 <https://github.com/pypa/pip/issues/9565>`_) +- Fix pip to work with warnings converted to errors. (`#9779 <https://github.com/pypa/pip/issues/9779>`_) +- **SECURITY**: Stop splitting on unicode separators in git references, + which could be maliciously used to install a different revision on the + repository. (`#9827 <https://github.com/pypa/pip/issues/9827>`_) + +Vendored Libraries +------------------ + +- Update urllib3 to 1.26.4 to fix CVE-2021-28363 +- Remove contextlib2. +- Upgrade idna to 3.1 +- Upgrade pep517 to 0.10.0 +- Upgrade vendored resolvelib to 0.7.0. +- Upgrade tenacity to 7.0.0 + +Improved Documentation +---------------------- + +- Update "setuptools extras" link to match upstream. (`#4822829F-6A45-4202-87BA-A80482DF6D4E <https://github.com/pypa/pip/issues/4822829F-6A45-4202-87BA-A80482DF6D4E>`_) +- Improve SSL Certificate Verification docs and ``--cert`` help text. (`#6720 <https://github.com/pypa/pip/issues/6720>`_) +- Add a section in the documentation to suggest solutions to the ``pip freeze`` permission denied issue. (`#8418 <https://github.com/pypa/pip/issues/8418>`_) +- Add warning about ``--extra-index-url`` and dependency confusion (`#9647 <https://github.com/pypa/pip/issues/9647>`_) +- Describe ``--upgrade-strategy`` and direct requirements explicitly; add a brief + example. (`#9692 <https://github.com/pypa/pip/issues/9692>`_) + 21.0.1 (2021-01-30) =================== diff --git a/news/0a741827-049c-4d5d-b44d-daea0c2fd01a.trivial.rst b/news/0a741827-049c-4d5d-b44d-daea0c2fd01a.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/11e1b2eb-6433-4f15-b70d-c2c514f72ebd.trivial.rst b/news/11e1b2eb-6433-4f15-b70d-c2c514f72ebd.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/151a1e46-d005-46ca-b1ae-a3811357dba3.trivial.rst b/news/151a1e46-d005-46ca-b1ae-a3811357dba3.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/1ab8f1c8-c115-4055-9a60-30a8f8eef7ba.trivial.rst b/news/1ab8f1c8-c115-4055-9a60-30a8f8eef7ba.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/287c6463-d2a1-41f5-a2df-d11f8460551a.trivial.rst b/news/287c6463-d2a1-41f5-a2df-d11f8460551a.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/40711960-12d9-4e58-8322-21e5975a804e.trivial.rst b/news/40711960-12d9-4e58-8322-21e5975a804e.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/4390.bugfix.rst b/news/4390.bugfix.rst deleted file mode 100644 index 0d84de5cf48..00000000000 --- a/news/4390.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fixed ``--target`` to work with ``--editable`` installs. diff --git a/news/4822829F-6A45-4202-87BA-A80482DF6D4E.doc.rst b/news/4822829F-6A45-4202-87BA-A80482DF6D4E.doc.rst deleted file mode 100644 index a67474c7803..00000000000 --- a/news/4822829F-6A45-4202-87BA-A80482DF6D4E.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Update "setuptools extras" link to match upstream. diff --git a/news/5B0A5B91-D0A3-4599-B3B4-129240A99B99.trivial.rst b/news/5B0A5B91-D0A3-4599-B3B4-129240A99B99.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/5be04056-e1d6-4f9a-bf46-8938d1936d9e.trivial.rst b/news/5be04056-e1d6-4f9a-bf46-8938d1936d9e.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/6409.bugfix.rst b/news/6409.bugfix.rst deleted file mode 100644 index e906c15fac6..00000000000 --- a/news/6409.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Add a warning, discouraging the usage of pip as root, outside a virtual environment. diff --git a/news/6720.doc.rst b/news/6720.doc.rst deleted file mode 100644 index f5547dfe502..00000000000 --- a/news/6720.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Improve SSL Certificate Verification docs and ``--cert`` help text. diff --git a/news/7269.bugfix.rst b/news/7269.bugfix.rst deleted file mode 100644 index 46816692b0a..00000000000 --- a/news/7269.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Ignore ``.dist-info`` directories if the stem is not a valid Python distribution -name, so they don't show up in e.g. ``pip freeze``. diff --git a/news/76c758fb-6f07-4ec1-956b-d77c9f339773.trivial.rst b/news/76c758fb-6f07-4ec1-956b-d77c9f339773.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/8090.bugfix.rst b/news/8090.bugfix.rst deleted file mode 100644 index ff8b80f3c8e..00000000000 --- a/news/8090.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -Only query the keyring for URLs that actually trigger error 401. -This prevents an unnecessary keyring unlock prompt on every pip install -invocation (even with default index URL which is not password protected). diff --git a/news/8214.bugfix.rst b/news/8214.bugfix.rst deleted file mode 100644 index 22224f380e5..00000000000 --- a/news/8214.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Prevent packages already-installed alongside with pip to be injected into an -isolated build environment during build-time dependency population. diff --git a/news/8253.feature.rst b/news/8253.feature.rst deleted file mode 100644 index 196e4dd9613..00000000000 --- a/news/8253.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Add the ability for the new resolver to process URL constraints. diff --git a/news/8418.bugfix.rst b/news/8418.bugfix.rst deleted file mode 100644 index 1bcc9b78709..00000000000 --- a/news/8418.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix ``pip freeze`` permission denied error in order to display an understandable error message and offer solutions. diff --git a/news/8418.doc.rst b/news/8418.doc.rst deleted file mode 100644 index 6634f6cd619..00000000000 --- a/news/8418.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Add a section in the documentation to suggest solutions to the ``pip freeze`` permission denied issue. diff --git a/news/855bfaed-4341-4d28-ab9e-e5ab43fb039f.trivial.rst b/news/855bfaed-4341-4d28-ab9e-e5ab43fb039f.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/8597c433-9c0b-4bec-a1e8-afd31786eaeb.trivial.rst b/news/8597c433-9c0b-4bec-a1e8-afd31786eaeb.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/8733.bugfix.rst b/news/8733.bugfix.rst deleted file mode 100644 index 95fd675397f..00000000000 --- a/news/8733.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Correctly uninstall script files (from setuptools' ``scripts`` argument), when installed with ``--user``. diff --git a/news/8785.bugfix.rst b/news/8785.bugfix.rst deleted file mode 100644 index b84d8d8d58e..00000000000 --- a/news/8785.bugfix.rst +++ /dev/null @@ -1,4 +0,0 @@ -New resolver: When a requirement is requested both via a direct URL -(``req @ URL``) and via version specifier with extras (``req[extra]``), the -resolver will now be able to use the URL to correctly resolve the requirement -with extras. diff --git a/news/8896.trivial.rst b/news/8896.trivial.rst deleted file mode 100644 index 3488b8e057a..00000000000 --- a/news/8896.trivial.rst +++ /dev/null @@ -1 +0,0 @@ -Separate the batched *download* of lazily-fetched wheel files from the preparation of regularly-downloaded requirements in ``RequirementPreparer.prepare_linked_requirements_more()``. diff --git a/news/9091.feature.rst b/news/9091.feature.rst deleted file mode 100644 index 8147e79c5e8..00000000000 --- a/news/9091.feature.rst +++ /dev/null @@ -1,4 +0,0 @@ -Add a feature ``--use-feature=in-tree-build`` to build local projects in-place -when installing. This is expected to become the default behavior in pip 21.3; -see `Installing from local packages <https://pip.pypa.io/en/stable/user_guide/#installing-from-local-packages>`_ -for more information. diff --git a/news/9139.feature.rst b/news/9139.feature.rst deleted file mode 100644 index 98dc133a1d6..00000000000 --- a/news/9139.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Bring back the "(from versions: ...)" message, that was shown on resolution failures. diff --git a/news/917ab6ff-72ea-4db5-846a-30273dac1c0c.trivial.rst b/news/917ab6ff-72ea-4db5-846a-30273dac1c0c.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/9300.bugfix.rst b/news/9300.bugfix.rst deleted file mode 100644 index 7da27f9975e..00000000000 --- a/news/9300.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -New resolver: Show relevant entries from user-supplied constraint files in the -error message to improve debuggability. diff --git a/news/9348.bugfix.rst b/news/9348.bugfix.rst deleted file mode 100644 index 99e673954c9..00000000000 --- a/news/9348.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -Avoid parsing version to make the version check more robust against lousily -debundled downstream distributions. diff --git a/news/9409.bugfix.rst b/news/9409.bugfix.rst deleted file mode 100644 index 10cd36b1960..00000000000 --- a/news/9409.bugfix.rst +++ /dev/null @@ -1,2 +0,0 @@ -``--user`` is no longer suggested incorrectly when pip fails with a permission -error in a virtual environment. diff --git a/news/9541.bugfix.rst b/news/9541.bugfix.rst deleted file mode 100644 index 88180198c07..00000000000 --- a/news/9541.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix incorrect reporting on ``Requires-Python`` conflicts. diff --git a/news/9547.feature.rst b/news/9547.feature.rst deleted file mode 100644 index 364a8f68817..00000000000 --- a/news/9547.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Add support for editable installs for project with only setup.cfg files. diff --git a/news/9565.bugfix.rst b/news/9565.bugfix.rst deleted file mode 100644 index b8f95fd3592..00000000000 --- a/news/9565.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Make wheel compatibility tag preferences more important than the build tag diff --git a/news/9617.process.rst b/news/9617.process.rst deleted file mode 100644 index f505c460541..00000000000 --- a/news/9617.process.rst +++ /dev/null @@ -1,3 +0,0 @@ -Start installation scheme migration from ``distutils`` to ``sysconfig``. A -warning is implemented to detect differences between the two implementations to -encourage user reports, so we can avoid breakages before they happen. diff --git a/news/9647.doc.rst b/news/9647.doc.rst deleted file mode 100644 index 70917817611..00000000000 --- a/news/9647.doc.rst +++ /dev/null @@ -1 +0,0 @@ -Add warning about ``--extra-index-url`` and dependency confusion diff --git a/news/9692.doc.rst b/news/9692.doc.rst deleted file mode 100644 index 2ef9623707f..00000000000 --- a/news/9692.doc.rst +++ /dev/null @@ -1,2 +0,0 @@ -Describe ``--upgrade-strategy`` and direct requirements explicitly; add a brief -example. diff --git a/news/9748.feature.rst b/news/9748.feature.rst deleted file mode 100644 index 28cb3b9228d..00000000000 --- a/news/9748.feature.rst +++ /dev/null @@ -1 +0,0 @@ -Improve performance when picking the best file from indexes during ``pip install``. diff --git a/news/9774.feature.rst b/news/9774.feature.rst deleted file mode 100644 index 8baac5e967f..00000000000 --- a/news/9774.feature.rst +++ /dev/null @@ -1,3 +0,0 @@ -Warn instead of erroring out when doing a PEP 517 build in presence of -``--build-option``. Warn when doing a PEP 517 build in presence of -``--global-option``. diff --git a/news/9779.bugfix.rst b/news/9779.bugfix.rst deleted file mode 100644 index 2145b641e27..00000000000 --- a/news/9779.bugfix.rst +++ /dev/null @@ -1 +0,0 @@ -Fix pip to work with warnings converted to errors. diff --git a/news/9827.bugfix.rst b/news/9827.bugfix.rst deleted file mode 100644 index e0d27c36cfe..00000000000 --- a/news/9827.bugfix.rst +++ /dev/null @@ -1,3 +0,0 @@ -**SECURITY**: Stop splitting on unicode separators in git references, -which could be maliciously used to install a different revision on the -repository. diff --git a/news/9976b528-bf43-4225-b96f-e066910f309c.trivial.rst b/news/9976b528-bf43-4225-b96f-e066910f309c.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/9e768673-6079-491e-bbe0-d1593952f1c7.trivial.rst b/news/9e768673-6079-491e-bbe0-d1593952f1c7.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/CVE-2021-28363.vendor.rst b/news/CVE-2021-28363.vendor.rst deleted file mode 100644 index 29700ab7469..00000000000 --- a/news/CVE-2021-28363.vendor.rst +++ /dev/null @@ -1 +0,0 @@ -Update urllib3 to 1.26.4 to fix CVE-2021-28363 diff --git a/news/a06e528d-1172-4012-a0a5-0fc42264a70d.trivial.rst b/news/a06e528d-1172-4012-a0a5-0fc42264a70d.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/adba2b7a-af01-49c9-9b72-2d3d4a89b11a.trivial.rst b/news/adba2b7a-af01-49c9-9b72-2d3d4a89b11a.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/afd07841-404a-4f2b-afee-c931a5aa7d54.trivial.rst b/news/afd07841-404a-4f2b-afee-c931a5aa7d54.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/bca635a1-abe3-4532-8add-bf7491b0eea5.trivial.rst b/news/bca635a1-abe3-4532-8add-bf7491b0eea5.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/contextlib2.vendor.rst b/news/contextlib2.vendor.rst deleted file mode 100644 index 2a44430775a..00000000000 --- a/news/contextlib2.vendor.rst +++ /dev/null @@ -1 +0,0 @@ -Remove contextlib2. diff --git a/news/d0935419-2486-45dc-b8dc-d2a5b9197ca4.trivial.rst b/news/d0935419-2486-45dc-b8dc-d2a5b9197ca4.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/d809028d-331a-4700-bfc1-485814e6c90e.trivial.rst b/news/d809028d-331a-4700-bfc1-485814e6c90e.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/dfaa54d4-21e2-460f-9d80-455ff318c713.trivial.rst b/news/dfaa54d4-21e2-460f-9d80-455ff318c713.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/f24d8f47-5750-4a13-b36f-d4a4622861cf.trivial.rst b/news/f24d8f47-5750-4a13-b36f-d4a4622861cf.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/f8f0d057-9ed5-4d22-a877-50f2d5adcae0.trivial.rst b/news/f8f0d057-9ed5-4d22-a877-50f2d5adcae0.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/fc6b6951-9a1a-453e-af98-bbb35f7c3e66.trivial.rst b/news/fc6b6951-9a1a-453e-af98-bbb35f7c3e66.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/fd62a11c-018c-4fde-ac8d-f674c6d9d190.trivial.rst b/news/fd62a11c-018c-4fde-ac8d-f674c6d9d190.trivial.rst deleted file mode 100644 index e69de29bb2d..00000000000 diff --git a/news/idna.vendor.rst b/news/idna.vendor.rst deleted file mode 100644 index 253933c9b6e..00000000000 --- a/news/idna.vendor.rst +++ /dev/null @@ -1 +0,0 @@ -Upgrade idna to 3.1 diff --git a/news/pep517.vendor.rst b/news/pep517.vendor.rst deleted file mode 100644 index f9ddefc00ff..00000000000 --- a/news/pep517.vendor.rst +++ /dev/null @@ -1 +0,0 @@ -Upgrade pep517 to 0.10.0 diff --git a/news/resolvelib.vendor.rst b/news/resolvelib.vendor.rst deleted file mode 100644 index f3d1df264d2..00000000000 --- a/news/resolvelib.vendor.rst +++ /dev/null @@ -1 +0,0 @@ -Upgrade vendored resolvelib to 0.7.0. diff --git a/news/tenacity.vendor.rst b/news/tenacity.vendor.rst deleted file mode 100644 index 01d2838892a..00000000000 --- a/news/tenacity.vendor.rst +++ /dev/null @@ -1 +0,0 @@ -Upgrade tenacity to 7.0.0 diff --git a/src/pip/__init__.py b/src/pip/__init__.py index ada5d647123..9a8c59275a7 100644 --- a/src/pip/__init__.py +++ b/src/pip/__init__.py @@ -1,6 +1,6 @@ from typing import List, Optional -__version__ = "21.1.dev0" +__version__ = "21.1" def main(args=None):